feat(grid): split-grid v2 — 1×3/2×3, drag-to-quadrant, Ctrl+` cycle, maximize
Builds on the v1 watch board: - Two more layouts: row-3 (1×3) and grid-6 (2×3). A single `grid` marker class on #term now carries the shared cell chrome so it no longer enumerates each lay-*. - Ctrl+` cycles the focused quadrant (Ctrl+Shift+` reverses); matchFocusCycleKey is an exported pure helper so it's unit-tested. No-op / passthrough in single mode. - Per-quadrant ⛶ maximize: the focused cell expands to fill the grid as an absolute overlay while siblings stay live behind it; follows focus, resets on layout change / tab close. - Drag a tab from the tab bar onto a quadrant to assign it there (reuses the existing tab-drag dragIndex); grid-only. Adversarial review (3 lenses → per-finding verify, incl. a headless-Chrome repro) caught a HIGH: maximizing via `grid-column/row: 1/-1` shoved siblings into implicit rows — a strip instead of fullscreen AND a spurious resize to backgrounded live PTYs. Fixed with an absolute-overlay (`position:absolute; inset:0`), then verified in real Chromium (Playwright): maximized cell fills #term, siblings 0px size delta. Also: silence a covered pane's pending pulse under maximize; coarse-pointer target for .cell-max. Verified: typecheck + build:web clean, 1579 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -327,8 +327,9 @@ body {
|
||||
}
|
||||
|
||||
/* ── Split-grid layouts (desktop watch board) ────────────────────────── */
|
||||
#term.lay-split-2,
|
||||
#term.lay-grid-4 {
|
||||
/* Shared cell chrome keys off the single `grid` marker so it is independent of
|
||||
* how many concrete layouts exist; each `lay-*` only sets the grid template. */
|
||||
#term.grid {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 6px;
|
||||
@@ -337,20 +338,40 @@ body {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
#term.lay-row-3 {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
#term.lay-grid-4 {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
}
|
||||
#term.lay-split-2 .term-cell,
|
||||
#term.lay-grid-4 .term-cell {
|
||||
#term.lay-grid-6 {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
}
|
||||
#term.grid .term-cell {
|
||||
position: relative;
|
||||
inset: auto;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg);
|
||||
}
|
||||
#term.lay-split-2 .cell-head,
|
||||
#term.lay-grid-4 .cell-head {
|
||||
/* Maximized quadrant fills the whole grid as an ABSOLUTE overlay (taken out of
|
||||
* grid flow so the siblings keep their placement — spanning grid tracks instead
|
||||
* would shove auto-placed siblings into implicit rows, resizing their live PTYs).
|
||||
* #term is a positioned containing block, so inset:0 covers the whole area. */
|
||||
#term.grid .term-cell.maximized {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 4;
|
||||
}
|
||||
/* A tab dragged from the tab bar is hovering this quadrant. */
|
||||
#term.grid .term-cell.drag-target {
|
||||
outline: 2px dashed var(--accent);
|
||||
outline-offset: -3px;
|
||||
}
|
||||
#term.grid .cell-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
@@ -361,6 +382,20 @@ body {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.cell-max {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--text-faint);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
padding: 2px 4px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.cell-max:hover {
|
||||
color: var(--text);
|
||||
background: var(--surface-3);
|
||||
}
|
||||
.cell-name {
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
@@ -388,16 +423,14 @@ body {
|
||||
}
|
||||
|
||||
/* Focus ring — the quadrant that owns keyboard / keybar / voice / approvals. */
|
||||
#term.lay-split-2 .term-cell.focused,
|
||||
#term.lay-grid-4 .term-cell.focused {
|
||||
#term.grid .term-cell.focused {
|
||||
border-color: var(--accent);
|
||||
box-shadow:
|
||||
0 0 0 1px var(--accent),
|
||||
var(--shadow);
|
||||
}
|
||||
/* Pending glow — a non-focused quadrant is waiting for approval. */
|
||||
#term.lay-split-2 .term-cell.pending:not(.focused),
|
||||
#term.lay-grid-4 .term-cell.pending:not(.focused) {
|
||||
#term.grid .term-cell.pending:not(.focused) {
|
||||
border-color: var(--amber);
|
||||
animation: cell-pending 2.4s ease-in-out infinite;
|
||||
}
|
||||
@@ -413,15 +446,13 @@ body {
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
#term.lay-split-2 .term-cell.pending,
|
||||
#term.lay-grid-4 .term-cell.pending {
|
||||
#term.grid .term-cell.pending {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Inline per-quadrant approve footer. */
|
||||
#term.lay-split-2 .cell-approve,
|
||||
#term.lay-grid-4 .cell-approve {
|
||||
#term.grid .cell-approve {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
@@ -548,10 +579,17 @@ body {
|
||||
.grid-glyph.gl-split-2 {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.grid-glyph.gl-row-3 {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
.grid-glyph.gl-grid-4 {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
}
|
||||
.grid-glyph.gl-grid-6 {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
}
|
||||
.grid-glyph i {
|
||||
background: currentColor;
|
||||
border-radius: 1px;
|
||||
@@ -566,6 +604,10 @@ body {
|
||||
width: 36px;
|
||||
height: 34px;
|
||||
}
|
||||
.cell-max {
|
||||
padding: 6px 9px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Key bar (rounded chips) ─────────────────────────────────────── */
|
||||
|
||||
Reference in New Issue
Block a user