feat(grid): v3b/c — resizable splitters + saved layout presets
Resizable splitters: draggable gutters between grid tracks adjust per-layout column/row fractions (fr units), applied as an inline grid-template and persisted (web-terminal:grid-splits). The fraction math (adjustSplit — trades delta between adjacent tracks, clamps to a minimum, conserves the total) is a pure, unit-tested helper in grid-layout.ts; the drag translates pixel motion into an fr delta. Saved presets: public/grid-presets.ts — a toolbar dropdown to save the current layout + its split under a name, re-apply it in one click, or delete it (web-terminal:grid-presets). Desktop-only, like the layout toggle. - grid-layout.ts: layoutTracks, defaultSplit, adjustSplit, tracksToTemplate, load/saveGridSplits, splitForLayout (validates stored shape). - tabs.ts: renderGutters/beginGutterDrag/setSplit; applyLayout sets the inline template; gridArrangement()/applyGridPreset() hooks. - main.ts: mount the presets dropdown. style.css: gutters + presets menu. - tests: splitter math + persistence + a stubbed drag repro (1.2fr/0.8fr); presets persistence + dropdown (save/apply/delete/close). typecheck+build clean, 1612 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1460,4 +1460,70 @@ describe('TabApp — split-grid watch board (v1)', () => {
|
||||
app.setGridLayout('single')
|
||||
expect(monitorHandles[0]!.dispose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// ── v3: resizable splitters ──────────────────────────────────────────────────
|
||||
|
||||
it('grid-4 renders one column + one row splitter and sets an fr template', () => {
|
||||
const { app, paneHost } = withTabs(4)
|
||||
app.focusTab(0)
|
||||
app.setGridLayout('grid-4')
|
||||
expect(paneHost.querySelectorAll('.grid-gutter-col')).toHaveLength(1)
|
||||
expect(paneHost.querySelectorAll('.grid-gutter-row')).toHaveLength(1)
|
||||
expect(paneHost.style.gridTemplateColumns).toBe('1fr 1fr')
|
||||
expect(paneHost.style.gridTemplateRows).toBe('1fr 1fr')
|
||||
})
|
||||
|
||||
it('grid-6 renders two column + one row splitters', () => {
|
||||
const { app, paneHost } = withTabs(6)
|
||||
app.focusTab(0)
|
||||
app.setGridLayout('grid-6')
|
||||
expect(paneHost.querySelectorAll('.grid-gutter-col')).toHaveLength(2)
|
||||
expect(paneHost.querySelectorAll('.grid-gutter-row')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('single mode has no splitters and clears the inline grid template', () => {
|
||||
const { app, paneHost } = withTabs(3)
|
||||
app.setGridLayout('grid-4')
|
||||
app.setGridLayout('single')
|
||||
expect(paneHost.querySelectorAll('.grid-gutter')).toHaveLength(0)
|
||||
expect(paneHost.style.gridTemplateColumns).toBe('')
|
||||
})
|
||||
|
||||
it('a persisted custom split is applied to the grid template on load', () => {
|
||||
localStorage.setItem(
|
||||
'web-terminal:grid-splits',
|
||||
JSON.stringify({ 'grid-4': { cols: [1.5, 0.5], rows: [1, 1] } }),
|
||||
)
|
||||
const { paneHost, tabBar } = makeHosts()
|
||||
const app = new TabApp(paneHost, tabBar)
|
||||
app.newTab()
|
||||
app.newTab()
|
||||
app.setGridLayout('grid-4')
|
||||
expect(paneHost.style.gridTemplateColumns).toBe('1.5fr 0.5fr')
|
||||
})
|
||||
|
||||
it('dragging a column splitter re-templates the grid and persists on release', () => {
|
||||
const { app, paneHost } = withTabs(4)
|
||||
app.focusTab(0)
|
||||
app.setGridLayout('grid-4')
|
||||
// jsdom has no layout — give #term a measurable width for the drag math.
|
||||
vi.spyOn(paneHost, 'getBoundingClientRect').mockReturnValue({
|
||||
width: 800,
|
||||
height: 600,
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 800,
|
||||
bottom: 600,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: () => ({}),
|
||||
} as DOMRect)
|
||||
const gutter = paneHost.querySelector<HTMLElement>('.grid-gutter-col')!
|
||||
gutter.dispatchEvent(new MouseEvent('pointerdown', { clientX: 400, bubbles: true }))
|
||||
gutter.dispatchEvent(new MouseEvent('pointermove', { clientX: 480, bubbles: true })) // +0.2fr
|
||||
gutter.dispatchEvent(new MouseEvent('pointerup', { bubbles: true }))
|
||||
expect(paneHost.style.gridTemplateColumns).toBe('1.2fr 0.8fr')
|
||||
const saved = JSON.parse(localStorage.getItem('web-terminal:grid-splits')!)
|
||||
expect(saved['grid-4'].cols[0]).toBeCloseTo(1.2)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user