mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
fix(ui): split-view pre-push review round — mode-distinct chip, anchoring, light-theme AA
Dual designer review (one primed on the branch context, one cold), all measured findings applied: - The per-pane chip was a mode-error trap: identical glyph at the identical locus, reversible in split mode (hide cell) but destructive single-pane (close pane). Now − hides, ✕ closes, and the close mode wears a danger hover/focus ring so the irreversible action telegraphs before the click lands. - Single-pane chip anchored to the VIEWPORT: an unpositioned section resolves absolutes to <body>, so the chip only coincidentally landed near the pane corner. .panes > section.pane is now position:relative in both modes (all pane-content absolutes verified to anchor to their own local relative parents). - Light-theme AA (measured): .shown tab underline 55% mix composited to 2.34:1 -> 80% (~3.7:1 light / ~5:1 dark); focused-cell ring 2.60:1 on light -> 75% mix override there (dark keeps 55% at 3.75:1). - Chip: border --hair-2 measured ~1.3:1 (invisible) -> --ink-4; 22px target under WCAG 2.5.8's 24px floor -> 28px; right offset clears the message scrollbar gutter; light resting glyph one ink step up. - Focus bar inset 1px from cell sides (no doubled-accent stripe where it butted a separator at the T-junction); greyscale font smoothing on the tail glyphs (subpixel RGB fringed the box-drawing characters). Rejected with rationale: aria-pressed on the split buttons (they are one-shot verbs — splitting again nests — not mode toggles).
This commit is contained in:
@@ -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 <body>)
|
||||
assert ".panes > section.pane" in css
|
||||
|
||||
|
||||
def test_step7_auth_gated_open_pane() -> None:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 <body>) 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;
|
||||
|
||||
Reference in New Issue
Block a user