diff --git a/.claude/skills/ui-chrome/SKILL.md b/.claude/skills/ui-chrome/SKILL.md index 237d537..20de754 100644 --- a/.claude/skills/ui-chrome/SKILL.md +++ b/.claude/skills/ui-chrome/SKILL.md @@ -60,7 +60,9 @@ Two more worth keeping straight. **A strip holds one tab or two** — `grid-auto Below `WINDOW_MIN_ROWS` the list is drawn whole, spacers and measurement included, so the small-machine path is exactly what it was. -**The docked left panel gives its spare height to one list.** Every list is capped at `--lp-list-max-h`, which left ten transitions scrolling in a 168px box above two hundred pixels of empty panel. `syncDockFill(side)` in [js/panel-sections-ui.js](../../../js/panel-sections-ui.js) marks the *last* open section that declares a `fill` region, and that region takes the leftover — the cap becomes its floor, so a panel with no room scrolls exactly as it did. The last, because growing it moves nothing but the collapsed headers under it. It is asked of the DOM on every collapse, reorder, tear-off, dock and machine switch, because an id stopped being the right key the moment sections could move (it used to be `#lp-transitions { flex: 1 }`). The region is `flex: 1 1 0`, sized to the leftover alone: a windowed list slices its rows from its own height, and a box as tall as its content would draw every row. Left panel only (`dockFill` in the registry) — the right panel's regions are drawn boxes, and a stretched trace log is a tall empty card. +**The docked left panel gives its spare height to one list.** Every list is capped at `--lp-list-max-h`, which left ten transitions scrolling in a 168px box above two hundred pixels of empty panel. `syncDockFill(side)` in [js/panel-sections-ui.js](../../../js/panel-sections-ui.js) marks the *last* open section that declares a `fill` region, and that region may grow past the cap into the leftover — **as far as its own rows reach, and no further**. It used to take the leftover whatever it held, so folding Transitions left five states standing over five hundred pixels of empty panel with the folded header pinned to the bottom edge. The section is `flex: 0 1 auto` (content-sized, shrinking only when its rows are taller than the room) and a windowed list is still bounded: its spacers make it tall, the shrink takes it back to the leftover, and it slices its window from that. It is asked of the DOM on every collapse, reorder, tear-off, dock and machine switch, because an id stopped being the right key the moment sections could move (it used to be `#lp-transitions { flex: 1 }`). + +The floors are **published by `publishListHeight` in [js/panel-list.js](../../../js/panel-list.js)**, not left to the browser, and that is the part easy to undo. The list's floor (`--lw-floor`) is the cap or its own height, whichever is less — not a size CSS can take from a scroll container. The section's (`--dock-fill-min`) is that plus its header and search row, because a flex or grid item's automatic minimum is its *content's* height: without it a 200-state list held the section at twelve thousand pixels and the panel scrolled instead of the list. The section floor is measured with the old one lifted, or a stale floor reads itself back as padding. The Blocks list is not drawn by `panel-list.js` and calls `publishListHeight` itself. Left panel only (`dockFill` in the registry) — the right panel's regions are drawn boxes, and a stretched trace log is a tall empty card. **A tab name is an element, not a text node.** `text-overflow` has nothing to apply to on the anonymous flex item a loose string becomes, so `.panel-tab-label` is what ellipsizes; before it, a name simply drew over the next tab, since the tracks are `minmax(0, 1fr)` and a tab is *allowed* to be narrower than its own text. Below 300px — measured by `@container`, because these panels are resized and pinned independently of the window — the tab icon is dropped rather than the words, two glyphs that repeat the names being cheaper to lose than the ends of both names. diff --git a/css/chrome-polish.css b/css/chrome-polish.css index f62be5a..037a8ff 100644 --- a/css/chrome-polish.css +++ b/css/chrome-polish.css @@ -46,9 +46,16 @@ .lp-section-header:hover, .rp-section-header:hover { background: color-mix(in srgb, var(--surface) 78%, transparent); } +/* No `scrollbar-gutter: stable`. A reserved gutter is outside the scrollport, + where nothing inside can paint, so every section rule and every header's + hover fill stopped ~10px short of the panel edge while the panel header's + rule above them ran the full width. And the strip was nearly always empty: + the docked panel hands its spare height to one list (syncDockFill) rather + than overflowing, so the bar only appears once the open sections really are + taller than the panel — and then it takes its width beside them the + ordinary way. */ .lpanel-content, .rpanel-content { - scrollbar-gutter: stable; scrollbar-color: var(--sb-thumb) transparent; } diff --git a/css/modals.css b/css/modals.css index bd3438f..931c15e 100644 --- a/css/modals.css +++ b/css/modals.css @@ -3235,6 +3235,9 @@ button.sm-stat:disabled { background: var(--surface); border: 1px solid var(--border2); border-radius: 20px; + /* The one piece of floating canvas chrome without a shadow: the toolbox, + the minimap, the zoom bar and the scope bar all lift off the well. */ + box-shadow: var(--shadow-sm); color: var(--text2); pointer-events: none; opacity: 0; diff --git a/css/panels.css b/css/panels.css index 7eadc2a..4b51cc4 100644 --- a/css/panels.css +++ b/css/panels.css @@ -13,6 +13,17 @@ .lp-/.rp- rules below. */ :root { --sec-head-pad: 9px 13px 7px; + /* A docked header's left padding is wider than the token's, because the + reorder grip lives in it (see .panel-sec-grip): at 13px the grip's dots + ended ~3px from the title and read as part of the label. 20px leaves a + clear gap and puts the title in line with the text of the rows under it. + A window's title bar has no grip and keeps the token. */ + --sec-head-pad-start: 20px; + /* And the body under the header, in both panels. The left's was + 3px 14px 12px and the right's 4px 14px 14px, so a card's content sat a + pixel higher and ended two pixels sooner depending on which side it was + docked to. */ + --sec-body-pad: 4px 14px 14px; --sec-head-gap: 8px; --sec-label-size: .68rem; --sec-label-tracking: .085em; @@ -104,9 +115,32 @@ box-shadow: inset 0 0 0 2px var(--focus-ring); } +/* Every docked header carries the grip in its left padding. */ +:is(.lpanel-content, .rpanel-content) :is(.lp-section-header, .rp-section-header) { + padding-left: var(--sec-head-pad-start); +} + +/* The title is the last thing in the row to give way, but it can give: with + `flex: 1` it had a basis of zero and a minimum of its own text, so a row + that did not fit overflowed instead — at the 220px minimum, with a + three-digit count, the collapse arrow went out past the panel edge. + + Shrink is shared in proportion to factor × basis, so the status chip and + the machine badge shrink at 100 against the title's 1: a narrow row + truncates those first (the chip carries its full text on the tooltip), and + clips the title only once they are gone. The title's factor stays at 1 and + not below it on purpose — when the factors in a row sum to less than one, + only that fraction of the overflow is taken up at all, and the rest still + spills past the edge. */ +:is(.lpanel-content, .rpanel-content) .lp-section-header .sec-lbl, +.rp-section-title { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; +} + :is(.lpanel-content, .rpanel-content) .lp-section-header .sec-lbl { margin-bottom: 0; - flex: 1; font-size: var(--sec-label-size); letter-spacing: var(--sec-label-tracking); } @@ -170,7 +204,8 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - flex: 0 1 auto; + /* 100, against the title's 1 — see the note over the title's flex. */ + flex: 0 100 auto; } .sec-status[data-tone="acc"] { @@ -196,7 +231,8 @@ /* Beside it, the title gives up its trailing rule rather than the status giving up its text: the rule is ornament, and it is what a narrow header has to spare. */ -.collapsed > .rp-section-header:has(> .sec-status:not(:empty)) .rp-section-title::after { +.collapsed > .rp-section-header:has(> .sec-status:not(:empty)) .rp-section-title::after, +.collapsed > .lp-section-header:has(> .sec-status:not(:empty)) .sec-lbl::after { display: none; } @@ -219,7 +255,7 @@ } :is(.lpanel-content, .rpanel-content) .lp-section-body { - padding: 3px 14px 12px; + padding: var(--sec-body-pad); } .lp-collapsible.collapsed .lp-section-body { @@ -755,14 +791,23 @@ } /* Long symbol names (whole-word alphabet entries) must never wrap the row - or clip off-panel — each segment truncates on its own, full text on hover. */ + or clip off-panel — each segment truncates on its own, full text on hover. + + The names do not shrink until they pass 40% of the row each; the label + gives way first. All three shrinking together shared the shortfall by + width, so a multi-tape label three times a name's length still took a cut + out of the names — and any cut at all is an ellipsis, which is how a row + read `ca… –0 → 0, R | 0 →… ca…` with "carry 0" and "carry 1" both lost, + while the label was the part the row could spare. The cap is what keeps a + long subset-construction name from doing the same to the label. */ .ti-from, .ti-to { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 3ch; - flex: 0 1 auto; + max-width: 40%; + flex: 0 0 auto; } /* Shrinks but does not grow: growing is what used to strand the destination @@ -1326,32 +1371,56 @@ body:has(.is-reordering) .tooltip { } /* The docked panel's spare height, to exactly one list. syncDockFill marks the - last open section that declares a growable region, and the region is what - grows — the section and its body only pass the height down. Both are - `1 0 auto`: grow into what is spare, never shrink past what they were drawn - at, so a panel with no room scrolls exactly as it always has. The region is - `1 1 0`, which sizes it to the leftover alone rather than to its content: a - windowed list slices its rows from its own height, and a box as tall as a - thousand rows would draw a thousand rows. + last open section that declares a growable region, and that region may grow + past --lp-list-max-h into the height the panel has left — **as far as its + own rows reach, and no further**. + + It used to grow to the bottom of the panel whatever it held: section and + body `1 0 auto`, region `1 1 0`. With Transitions open and last, that was + invisible, since nothing sits under the region. But folding Transitions + hands the fill to States Q, and five states then stood over five hundred + pixels of empty panel with the folded TRANSITIONS δ header pinned to the + bottom edge — the one card in the panel that did not size to what it held. + + So the section is content-sized (`0 1 auto`) and shrinks only when its + content is taller than the room, and the list's row takes whatever height + the section is left with, between its floor and its rows. Section and body + are grids with `align-content: start`, so a grid with room to spare does + not stretch its search row to use it. + + The floors are set by js/panel-list.js (publishListHeight), because the + browser's would be wrong in both places. The list's floor is the cap, or + its own height when that is less, so a short panel cannot crush it — "as + tall as the content but no more than 168px" is not a size CSS can take from + a scroll container. And the section's automatic minimum is its content's + height — in a grid as much as a flex column — so a 200-state list held the + section at twelve thousand pixels and the whole panel scrolled instead of + the list. The section's floor is published beside the list's (its header + and search row plus the list's floor), the body takes `min-height: 0`, and + a panel with no room still scrolls rather than squeezing the list. + + A windowed list is still bounded by the room rather than drawing every row: + its spacers make its content tall, the shrink takes it back to the + leftover, and it slices its window from that. Scoped to children of the panel's container, so a section torn off into a window is not addressed here — the window has its own fill, and the selector carries .lp-collapsible to outrank the `:last-child` reset above. */ :is(.lpanel-content, .rpanel-content) > .sec.lp-collapsible.panel-dock-fill { - flex: 1 0 auto; - display: flex; - flex-direction: column; + flex: 0 1 auto; + min-height: var(--dock-fill-min, auto); + display: grid; + align-content: start; } :is(.lpanel-content, .rpanel-content) > .panel-dock-fill > .lp-section-body { - flex: 1 0 auto; - display: flex; - flex-direction: column; + min-height: 0; + display: grid; + align-content: start; } :is(.lpanel-content, .rpanel-content) > .panel-dock-fill .panel-dock-fill-region { - flex: 1 1 0; - min-height: var(--lp-list-max-h); + min-height: var(--lw-floor, var(--lp-list-max-h)); max-height: none; /* A wrapping flex container shares spare height among its lines by default, which turned every alphabet chip into a tall pill. Same @@ -1365,12 +1434,20 @@ body:has(.is-reordering) .tooltip { symbols), the placeholder in the UI face, and a clear button that appears only once there is something to clear. type="search" gives Escape-to-clear for free; the native cancel glyph is redrawn as a mask so it takes the - theme's text colour. */ + theme's text colour. + + `flex: none` here, and the stretch only where it is meant — beside the δ + view switch, in `.lp-search-row`. A bare `flex: 1` is a grow on whichever + axis the parent runs, and States Q's filter sits straight in its section + body: whenever that body becomes a column — States taking the spare height + with Transitions folded, or torn off into a tall window — the field split + the slack with the list, and the input floated in a band of empty panel + with the rows stranded far below it. */ .search-field { position: relative; display: flex; align-items: center; - flex: 1; + flex: none; min-width: 0; margin-bottom: 6px; } @@ -1395,12 +1472,19 @@ body:has(.is-reordering) .tooltip { border: 1px solid var(--border); color: var(--text); font-family: var(--mono); - font-size: .72rem; + /* The entry fields' size (.inp): a filter typed into the States list read a + size smaller than a symbol typed into Σ two cards above it. */ + font-size: .78rem; padding: 0 6px 0 28px; border-radius: var(--field-radius); outline: none; -webkit-appearance: none; appearance: none; + /* Beside the δ view switch in a narrow panel "Filter transitions…" does not + fit, and without this it was cut through a glyph ("Filter transitions.") + rather than ending in an ellipsis. Set on the input, not on ::placeholder, + which does not take it; the input's applies to its placeholder. */ + text-overflow: ellipsis; transition: border-color var(--transition-fast), background var(--transition-fast), box-shadow var(--transition-fast); } @@ -1516,18 +1600,31 @@ body:has(.is-reordering) .tooltip { /* The header row and the rows are separate grids — the rows are drawn by the windowed list and rewritten on scroll, the header is not — so both read one template, set as a custom property by the renderer. The header scrolls with - the host from js/delta-table.js; both reserve the same scrollbar gutter, or - the host's vertical bar would take width the header still has and every - column would sit a scrollbar's width out of line. */ + the host from js/delta-table.js, which also pads it by the width of the + host's vertical bar when there is one, or that bar would take width the + header still has and every column would sit a scrollbar's width out of line. + + That used to be a `scrollbar-gutter: stable` on both, and it went wrong two + ways. The header's border was drawn outside its gutter while each row's was + drawn inside the host's, so the header's frame ran ~10px past the rows under + it; and the reserved strip was there with nothing to scroll, leaving the + whole table that much narrower than the fields above it. The frame is the + header *row's* now, so it ends wherever the rows do. */ .dt-head { overflow: hidden; - scrollbar-gutter: stable; +} + +.dt-hrow { border: 1px solid var(--border); border-bottom: none; border-radius: 6px 6px 0 0; background: var(--bg3); } +.dt-hq { + border-top-left-radius: 5px; +} + .dt-head[hidden] { display: none; } @@ -1572,7 +1669,6 @@ body:has(.is-reordering) .tooltip { .tlist.is-table { gap: 0; overflow: auto; - scrollbar-gutter: stable; } /* The frame is drawn by the rows, not by the host. The host is the panel's @@ -2035,7 +2131,7 @@ button.dt-cell:hover { display: flex; align-items: center; gap: 8px; - flex: 1; + /* flex: set with the left panel's title, near the top of this file. */ } .rp-section-title::after { @@ -2061,13 +2157,25 @@ button.dt-cell:hover { } /* Match the left panel's count chip: fill only, no ring. Scoped to the - header so the .badge/.bd-* pills used elsewhere keep their own look. */ + header so the .badge/.bd-* pills used elsewhere keep their own look. + + It gives way before the title does (100 against the title's 1 — see the + note over the title's flex): the Language card names the machine here, and + some names are phrases — "Pushdown Transducer" pushed the PDT's collapse + arrow 17px past the right panel's edge at its default width. */ .rp-section-header .badge { border-color: transparent; font-size: .62rem; padding: 1px 7px; letter-spacing: .03em; - flex-shrink: 0; + flex: 0 100 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + /* 12 + 1px padding + 1px border each side: the count chip's 16px. On its + own line height the badge's sans face drew it a pixel taller. */ + line-height: 12px; } .rp-collapsible.collapsed .rp-toggle-arrow { @@ -2085,7 +2193,7 @@ button.dt-cell:hover { } .rp-section-body { - padding: 4px 14px 14px; + padding: var(--sec-body-pad); } /* ─── ALGO SEARCH ─── */ @@ -2183,15 +2291,29 @@ button.dt-cell:hover { be guessed at from pointer travel and the keyboard would be left with no way to reorder at all. See js/panel-sections-ui.js. - It follows .lp-row-btn's rule — revealed on :hover and :focus-within, - and always shown where there is no hover to reveal it with. */ + It follows .lp-row-btn's rule — revealed on :hover and on keyboard focus, + and always shown where there is no hover to reveal it with. Keyboard focus + is `:focus-visible`, not `:focus-within`: the header is `tabindex="0"`, so + a click that collapses it also focuses it, and nothing on the canvas takes + focus back — the grip and the pop-out stayed lit on whichever header was + clicked last. + + It sits in the header's left padding rather than in the row. In the row it + held 18px — its width and a gap — through every moment it was invisible, + which pushed each title in from the body below it and, at the panel's + 220px minimum, pushed TRANSITIONS δ's collapse arrow out past the edge. + Out of the flow it costs the row nothing and still appears without moving + anything. The header is `position: relative` (css/chrome-polish.css). */ .panel-sec-grip { + position: absolute; + left: 2px; + top: 50%; + translate: 0 -50%; display: inline-flex; align-items: center; justify-content: center; - width: 14px; + width: 12px; height: 18px; - margin-left: -4px; padding: 0; border: none; border-radius: 3px; @@ -2206,9 +2328,9 @@ button.dt-cell:hover { } .lp-section-header:hover .panel-sec-grip, -.lp-section-header:focus-within .panel-sec-grip, +.lp-section-header:is(:focus-visible, :has(:focus-visible)) .panel-sec-grip, .rp-section-header:hover .panel-sec-grip, -.rp-section-header:focus-within .panel-sec-grip { +.rp-section-header:is(:focus-visible, :has(:focus-visible)) .panel-sec-grip { opacity: 1; } @@ -2597,9 +2719,9 @@ button.dt-cell:hover { } .lp-section-header:hover .panel-float-btn, -.lp-section-header:focus-within .panel-float-btn, +.lp-section-header:is(:focus-visible, :has(:focus-visible)) .panel-float-btn, .rp-section-header:hover .panel-float-btn, -.rp-section-header:focus-within .panel-float-btn { +.rp-section-header:is(:focus-visible, :has(:focus-visible)) .panel-float-btn { opacity: 1; } diff --git a/css/variables.css b/css/variables.css index b2cde6d..74e8443 100644 --- a/css/variables.css +++ b/css/variables.css @@ -1,104 +1,4 @@ :root { - color-scheme: dark; - --bg: #12151b; - --bg2: #0c0e13; - --bg3: #171b23; - --surface: #1c212b; - --surface2: #232935; - --surface3: #2c3341; - --border: rgba(230, 233, 240, 0.07); - --border2: rgba(230, 233, 240, 0.13); - --sb-thumb: rgba(230, 233, 240, 0.16); - --sb-thumb-hover: rgba(230, 233, 240, 0.26); - --sb-thumb-active: rgba(230, 233, 240, 0.36); - --text: #e6e9f0; - --text2: #a7afbe; - --text3: #737d90; - --accent: #5ea1ff; - --violet: #a78bfa; - --indigo: #8b93ff; - --blue: #5ea1ff; - --gold: #f0c14b; - --green: #4dca8b; - --red: #f26d6d; - --orange: #f59a45; - /* Turing machines in the Library: an orange of its own, because --orange - already means "no verdict" and a machine family must not read as a warning. - Each theme derives it from its own orange and red. */ - --coral: #f48855; - --purple: #d68cf2; - --on-accent: #0c0e13; - --mono: 'JetBrains Mono', monospace; - --sans: 'DM Sans', sans-serif; - --serif: 'Crimson Pro', serif; - --r: 8px; - /* Single unified header bar; there is no longer a second toolbar row. */ - --hdr: 48px; - /* The header and both sidebars are one unbroken --bg2 surface, and the canvas - is a well cut into it. This is the radius of that cutout — so the curve - belongs to the canvas, and what each panel shows at its inner corners is - the concave outside of it. mobile.css zeroes it where the panels stop - being rails and the well stops having chrome beside it. */ - --canvas-radius: 14px; - /* The well had chrome on three sides only — header above, a sidebar either - side, and nothing under it, which left the bottom corners curving into a - window edge instead of into a frame. This is the sill that closes it: the - canvas stops short by this much and the panels carry --bg2 across the gap, - so the frame runs all the way round. */ - --canvas-sill: 14px; - --overlay: rgba(7, 8, 11, 0.7); - --overlay-blur: 3px; - --overlay-ring: rgba(255, 255, 255, 0.025); - --focus-ring: rgba(94, 161, 255, 0.14); - --panel-shadow-left: 2px 0 12px rgba(0, 0, 0, 0.22); - --panel-shadow-right: -2px 0 12px rgba(0, 0, 0, 0.22); - --accent-soft: rgba(94, 161, 255, 0.12); - --accent-soft-strong: rgba(94, 161, 255, 0.2); - --accent-border: rgba(94, 161, 255, 0.3); - --accent-border-strong: rgba(94, 161, 255, 0.45); - --accent-glow: 0 0 14px rgba(94, 161, 255, 0.32); - --tab-hover-bg: rgba(230, 233, 240, 0.045); - --tab-active-bg: rgba(94, 161, 255, 0.13); - --tab-active-border: rgba(94, 161, 255, 0.3); - --tab-active-line: rgba(94, 161, 255, 0.95); - --tab-overflow-shadow: rgba(8, 10, 13, 0.62); - --violet-soft: rgba(167, 139, 250, 0.14); - --violet-border: rgba(167, 139, 250, 0.3); - --indigo-soft: rgba(139, 147, 255, 0.14); - --indigo-border: rgba(139, 147, 255, 0.3); - --blue-soft: rgba(94, 161, 255, 0.12); - --blue-border: rgba(94, 161, 255, 0.3); - --white-note: #f8f8fb; - --white-note-soft: rgba(248, 248, 251, 0.92); - --white-note-border: rgba(248, 248, 251, 0.72); - --white-note-text: #0c0e13; - --purple-soft: rgba(214, 140, 242, 0.14); - --purple-border: rgba(214, 140, 242, 0.3); - --green-soft: rgba(77, 202, 139, 0.12); - --green-border: rgba(77, 202, 139, 0.3); - --gold-soft: rgba(240, 193, 75, 0.12); - --gold-border: rgba(240, 193, 75, 0.3); - --note-yellow: #f5dc6e; - --note-yellow-soft: rgba(245, 220, 110, 0.18); - --note-yellow-border: rgba(245, 220, 110, 0.42); - --orange-soft: rgba(245, 154, 69, 0.12); - --orange-border: rgba(245, 154, 69, 0.3); - --red-soft: rgba(242, 109, 109, 0.12); - --red-border: rgba(242, 109, 109, 0.3); - --canvas-glow-a: rgba(94, 161, 255, 0.04); - --canvas-glow-b: rgba(167, 139, 250, 0.03); - --state-active-fill: rgba(94, 161, 255, 0.2); - --state-active-shadow: drop-shadow(0 0 7px rgba(94, 161, 255, 0.45)); - --state-reject-fill: rgba(242, 109, 109, 0.12); - --marquee-fill: rgba(94, 161, 255, 0.1); - --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.35); - --shadow-md: 0 4px 18px rgba(0, 0, 0, 0.45); - --shadow-lg: 0 20px 60px rgba(0, 0, 0, 0.6); - --transition-fast: 0.12s ease; - --transition-base: 0.18s ease; -} - -:root[data-theme="light"] { color-scheme: light; --bg: #f7f8fa; --bg2: #eef0f4; @@ -122,6 +22,9 @@ --green: #16855a; --red: #cc3434; --orange: #c95f0c; + /* Turing machines in the Library: an orange of its own, because --orange + already means "no verdict" and a machine family must not read as a warning. + Each theme derives it from its own orange and red. */ --coral: #ca4e1c; --purple: #9a3fc0; --on-accent: #ffffff; @@ -172,6 +75,106 @@ --shadow-sm: 0 2px 10px rgba(23, 28, 38, 0.08); --shadow-md: 0 10px 28px rgba(23, 28, 38, 0.12); --shadow-lg: 0 20px 56px rgba(23, 28, 38, 0.16); + /* Shared by every theme: type, geometry and timing. The colours above are + the light theme, which is the default — every other theme, dark + included, is a block below keyed on its data-theme id that restates them. */ + --mono: 'JetBrains Mono', monospace; + --sans: 'DM Sans', sans-serif; + --serif: 'Crimson Pro', serif; + --r: 8px; + /* Single unified header bar; there is no longer a second toolbar row. */ + --hdr: 48px; + /* The header and both sidebars are one unbroken --bg2 surface, and the canvas + is a well cut into it. This is the radius of that cutout — so the curve + belongs to the canvas, and what each panel shows at its inner corners is + the concave outside of it. mobile.css zeroes it where the panels stop + being rails and the well stops having chrome beside it. */ + --canvas-radius: 14px; + /* The well had chrome on three sides only — header above, a sidebar either + side, and nothing under it, which left the bottom corners curving into a + window edge instead of into a frame. This is the sill that closes it: the + canvas stops short by this much and the panels carry --bg2 across the gap, + so the frame runs all the way round. */ + --canvas-sill: 14px; + --overlay-blur: 3px; + --transition-fast: 0.12s ease; + --transition-base: 0.18s ease; +} + +:root[data-theme="dark"] { + color-scheme: dark; + --bg: #12151b; + --bg2: #0c0e13; + --bg3: #171b23; + --surface: #1c212b; + --surface2: #232935; + --surface3: #2c3341; + --border: rgba(230, 233, 240, 0.07); + --border2: rgba(230, 233, 240, 0.13); + --sb-thumb: rgba(230, 233, 240, 0.16); + --sb-thumb-hover: rgba(230, 233, 240, 0.26); + --sb-thumb-active: rgba(230, 233, 240, 0.36); + --text: #e6e9f0; + --text2: #a7afbe; + --text3: #737d90; + --accent: #5ea1ff; + --violet: #a78bfa; + --indigo: #8b93ff; + --blue: #5ea1ff; + --gold: #f0c14b; + --green: #4dca8b; + --red: #f26d6d; + --orange: #f59a45; + --coral: #f48855; + --purple: #d68cf2; + --on-accent: #0c0e13; + --overlay: rgba(7, 8, 11, 0.7); + --overlay-ring: rgba(255, 255, 255, 0.025); + --focus-ring: rgba(94, 161, 255, 0.14); + --panel-shadow-left: 2px 0 12px rgba(0, 0, 0, 0.22); + --panel-shadow-right: -2px 0 12px rgba(0, 0, 0, 0.22); + --accent-soft: rgba(94, 161, 255, 0.12); + --accent-soft-strong: rgba(94, 161, 255, 0.2); + --accent-border: rgba(94, 161, 255, 0.3); + --accent-border-strong: rgba(94, 161, 255, 0.45); + --accent-glow: 0 0 14px rgba(94, 161, 255, 0.32); + --tab-hover-bg: rgba(230, 233, 240, 0.045); + --tab-active-bg: rgba(94, 161, 255, 0.13); + --tab-active-border: rgba(94, 161, 255, 0.3); + --tab-active-line: rgba(94, 161, 255, 0.95); + --tab-overflow-shadow: rgba(8, 10, 13, 0.62); + --violet-soft: rgba(167, 139, 250, 0.14); + --violet-border: rgba(167, 139, 250, 0.3); + --indigo-soft: rgba(139, 147, 255, 0.14); + --indigo-border: rgba(139, 147, 255, 0.3); + --blue-soft: rgba(94, 161, 255, 0.12); + --blue-border: rgba(94, 161, 255, 0.3); + --white-note: #f8f8fb; + --white-note-soft: rgba(248, 248, 251, 0.92); + --white-note-border: rgba(248, 248, 251, 0.72); + --white-note-text: #0c0e13; + --purple-soft: rgba(214, 140, 242, 0.14); + --purple-border: rgba(214, 140, 242, 0.3); + --green-soft: rgba(77, 202, 139, 0.12); + --green-border: rgba(77, 202, 139, 0.3); + --gold-soft: rgba(240, 193, 75, 0.12); + --gold-border: rgba(240, 193, 75, 0.3); + --note-yellow: #f5dc6e; + --note-yellow-soft: rgba(245, 220, 110, 0.18); + --note-yellow-border: rgba(245, 220, 110, 0.42); + --orange-soft: rgba(245, 154, 69, 0.12); + --orange-border: rgba(245, 154, 69, 0.3); + --red-soft: rgba(242, 109, 109, 0.12); + --red-border: rgba(242, 109, 109, 0.3); + --canvas-glow-a: rgba(94, 161, 255, 0.04); + --canvas-glow-b: rgba(167, 139, 250, 0.03); + --state-active-fill: rgba(94, 161, 255, 0.2); + --state-active-shadow: drop-shadow(0 0 7px rgba(94, 161, 255, 0.45)); + --state-reject-fill: rgba(242, 109, 109, 0.12); + --marquee-fill: rgba(94, 161, 255, 0.1); + --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.35); + --shadow-md: 0 4px 18px rgba(0, 0, 0, 0.45); + --shadow-lg: 0 20px 60px rgba(0, 0, 0, 0.6); } :root[data-theme="nord"] { diff --git a/css/views.css b/css/views.css index 742b7ea..1754d77 100644 --- a/css/views.css +++ b/css/views.css @@ -471,6 +471,39 @@ margin-bottom: var(--canvas-sill); } +/* The chrome's shadow on the well. The header, both panels and the sill sit + around the canvas as one raised surface, and the well had no edge but its + radius — so on a dark theme, where --bg2 and --bg are a few levels apart, + nothing said which was above which. A soft inset shade along each edge says + it: strongest under the header, a little lighter off the panels, faintest + up from the sill, the way light from above would fall. + + An overlay rather than a shadow on .canvas-area itself, because an inset + shadow paints in the box's own background — under the grid, the diagram + and everything else in the well — and a shadow cast by the chrome has to + fall across what is under it. z-index 40 is above the diagram and below + all of the floating chrome (49 and up), so the toolbox, the minimap and the + label editor are never shaded. It takes no pointer, and it is static: a + pan or zoom never touches it. + + The ink is --tab-overflow-shadow, the app's "there is more past this edge" + colour, which every theme already tunes for its own ground — a literal + black is invisible on the dark palettes and a smudge on the light ones. */ +.canvas-area::after { + --well-ink: color-mix(in srgb, var(--tab-overflow-shadow) 72%, transparent); + content: ''; + position: absolute; + inset: 0; + z-index: 40; + border-radius: inherit; + pointer-events: none; + box-shadow: + inset 0 9px 10px -9px var(--well-ink), + inset 8px 0 10px -8px var(--well-ink), + inset -8px 0 10px -8px var(--well-ink), + inset 0 -6px 8px -7px var(--well-ink); +} + /* A real element, not .canvas-area::before, and that is the point of it. The camera variables below change on every frame of a pan or zoom, and custom properties inherit: set on #canvas-wrap they changed an inherited value on diff --git a/electron/main.cjs b/electron/main.cjs index 512b08f..4ccd900 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -530,7 +530,10 @@ function createWindow() { height: 900, minWidth: 960, minHeight: 600, - backgroundColor: '#0f0f14', + // The default theme's --bg2, so the window does not open on a dark frame + // before the page paints. A reader who chose a dark theme sees one light + // frame instead, which is the cheaper of the two to get wrong. + backgroundColor: '#eef0f4', show: false, frame: false, webPreferences: { diff --git a/index.html b/index.html index f12e009..d9b9d0a 100644 --- a/index.html +++ b/index.html @@ -21,7 +21,7 @@