diff --git a/tests/test_shell_js.py b/tests/test_shell_js.py index 5c77e6a9..bdfae21d 100644 --- a/tests/test_shell_js.py +++ b/tests/test_shell_js.py @@ -751,6 +751,10 @@ def test_pane_manager_split_engine() -> None: assert "closeCell(paneId)" in pane and "_refreshCellChips()" in pane assert 'b.className = "cell-unsplit"' in pane assert '"Close pane"' in pane, "the single-pane chip mode" + # mode-DISTINCT glyphs (designer P1: identical signifier + locus with a + # reversible/destructive divergence is a mode-error trap) + assert 'b.textContent = multi ? "−" : "✕"' in pane + assert '"cell-unsplit--close"' in pane assert "this._removeCellChip(pane)" in pane # open-beside: the coordinator child-link placement (split right of the # focused cell, degrade to the plain swap on deny) @@ -768,6 +772,10 @@ def test_pane_manager_split_engine() -> None: # occlusion bug); the ::before bar sits above the ring line assert ".panes--split > section.pane.split-focused::after" in css assert ".cell-unsplit" in css, "the per-cell hide-from-split chip style" + assert ".cell-unsplit--close:hover" in css, "destructive mode telegraphs on hover" + # the pane is the chip's containing block in BOTH modes — unpositioned, + # the single-pane chip anchored to the VIEWPORT (offsetParent ) + assert ".panes > section.pane" in css def test_step7_auth_gated_open_pane() -> None: diff --git a/turnstone/shared_static/pane.js b/turnstone/shared_static/pane.js index e1ae67d2..2619c7eb 100644 --- a/turnstone/shared_static/pane.js +++ b/turnstone/shared_static/pane.js @@ -857,7 +857,6 @@ export class PaneManager { b = document.createElement("button"); b.type = "button"; b.className = "cell-unsplit"; - b.textContent = "✕"; b.addEventListener("click", () => { if (this._layout && this._leafFor(pane.id)) this.closeCell(pane.id); else this.close(pane.id); @@ -865,6 +864,13 @@ export class PaneManager { pane.el.append(b); pane._cellChip = b; } + // Mode-DISTINCT glyphs — an identical signifier at an identical locus with + // divergent outcomes is a mode-error trap (split-mode muscle memory would + // fire the destructive close): − hides the cell (reversible — the tab + // stays), ✕ closes the pane. Close mode also wears a danger hover + // (shell.css .cell-unsplit--close). + b.textContent = multi ? "−" : "✕"; + b.classList.toggle("cell-unsplit--close", !multi); const label = multi ? "Hide from split — the tab stays open" : "Close pane"; b.title = label; b.setAttribute("aria-label", label); diff --git a/turnstone/shared_static/shell.css b/turnstone/shared_static/shell.css index 17eb68ef..1dc10cd5 100644 --- a/turnstone/shared_static/shell.css +++ b/turnstone/shared_static/shell.css @@ -590,6 +590,10 @@ } .tb-split .tb-glyph { display: inline-block; + /* greyscale AA — subpixel RGB rendering fringes these box-drawing glyphs + with blue/amber bleed against the panel */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } .tb-split--down .tb-glyph { transform: rotate(90deg); @@ -777,6 +781,13 @@ .panes--split { position: relative; } +/* The pane is the containing block in BOTH modes: unpositioned, the per-pane + ✕ chip's absolute would resolve to the VIEWPORT (offsetParent ) and + only coincidentally land near the pane corner. The split rule below wins + on specificity while split. */ +.panes > section.pane { + position: relative; +} .panes--split > section.pane { position: absolute; overflow: hidden; @@ -799,10 +810,18 @@ z-index: 12; pointer-events: none; } +/* light needs a hotter mix: 55% measured 2.60:1 there (sub-3:1); 75% clears. + Dark stays 55% (3.75:1) — hotter reads as a heavy border on dark. */ +[data-theme="light"] .panes--split > section.pane.split-focused::after { + box-shadow: inset 0 0 0 1px + color-mix(in srgb, var(--accent) 75%, var(--hair-2)); +} .panes--split > section.pane.split-focused::before { content: ""; position: absolute; - inset: 0 0 auto 0; /* top edge, full cell width */ + /* 1px in from the cell sides so the bar never butts a separator line into + one doubled-accent stripe at the T-junction */ + inset: 0 1px auto 1px; height: 2px; background: var(--accent); z-index: 13; /* above the ring overlay — a clean bar, not bar-plus-ring-line */ @@ -815,33 +834,50 @@ the 2px focus bar and the cell corner. */ .cell-unsplit { position: absolute; - top: 6px; - right: 10px; + top: 5px; + right: 14px; /* clear of the message scroller's scrollbar gutter */ z-index: 14; - width: 22px; - height: 22px; + /* 28px: WCAG 2.5.8's 24px floor + a hair (22px under-shot it) */ + width: 28px; + height: 28px; display: flex; align-items: center; justify-content: center; - border: 1px solid var(--hair-2); + /* --hair-2 measured ~1.3:1 against the chip bg — an invisible boundary; + --ink-4 clears 3:1 in both themes */ + border: 1px solid var(--ink-4); border-radius: var(--r-sm); background: var(--panel-2); color: var(--ink-3); - font-size: 11px; + font-size: 12px; line-height: 1; cursor: pointer; - opacity: 0.75; + opacity: 0.85; } .cell-unsplit:hover { opacity: 1; color: var(--ink); border-color: var(--accent-dim); } +/* the destructive mode (single-pane "Close pane" ✕) telegraphs BEFORE the + click lands — split-mode muscle memory must not fire it blind */ +.cell-unsplit--close:hover { + color: var(--err); + border-color: var(--err); +} .cell-unsplit:focus-visible { opacity: 1; outline: 2px solid var(--accent); outline-offset: -2px; } +.cell-unsplit--close:focus-visible { + outline-color: var(--err); +} +/* light resting glyph at .85 sat right at the 3:1 floor with --ink-3 — one + ink step restores headroom (~4.5:1) without making the chip shout */ +[data-theme="light"] .cell-unsplit { + color: var(--ink-2); +} /* separator — a 7px hit area straddling the cell boundary with a 1px visual line; drag (pointer capture) or arrow keys resize, see pane.js _wireHandle. Resting line is --ink-4, the lightest token clearing 3:1 in BOTH themes @@ -914,7 +950,9 @@ bottom: 2px; height: 2px; border-radius: 2px; - background: color-mix(in srgb, var(--accent) 55%, transparent); + /* 80%: 55% composited to 2.34:1 on the light --panel (sub-3:1 for a + load-bearing state mark); 80% = ~3.7:1 light / ~5:1 dark */ + background: color-mix(in srgb, var(--accent) 80%, transparent); } .pane-head { display: flex;