fix(tasks-ui): postmodern chrome, working clicks, adaptive layout, lean cards
Review feedback from the live 3384 install, fixed at the root: - COLORS: the new stylesheet used var(--dsw-alias-accent) in 17 places — a token the DSH theme does NOT define, so every declaration was dropped (older files only survived because they carry fallbacks); container edges used border-l1 (4% black, invisible). Accent now rides --dsw-alias-state-business-primary, container edges --dsw-alias-border-l4. - CLICKS: the canvas called setPointerCapture on pointerdown, which retargets the derived click to the container and killed every node (and ⓘ) interaction; jsdom cannot see this, a real browser can. Capture is gone and panning only starts from the background. - LAYOUT: fit gave up silently at zero container size and never retried, centered only horizontally, and capped at 100% while a five-child level scaled to 35%. Now: ResizeObserver + first non-zero fit, both axes centered, a 0.78 readability floor, and layoutTasksGraphForWidth picks the flattest sibling arrangement that still meets that floor. - CARDS: title (one line) + mono meta + live line only; display title, team role, full model id and latest text moved into the ⓘ popover. - PANEL: metrics follow the approved 360x660 mockup (132x46 cards, 112 row stride), explicit line-heights, user-select:none, horizontal control cluster with a labelled mode button, always-visible team board strip. - Tokens for two new popover headings across all 20 dictionaries. Tests: 3 new page-interaction specs (click after background pointerdown, node gestures never pan, team board visible without a click) and a rewritten 9-case layout suite (band wrap, row reservation, width solver, reserved live row). Full suite 1400 passed / 133 files.
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
/**
|
||||
* Unit tests for the layered graph layout (layoutTasksGraph): parent
|
||||
* centering over children bands, depth rows, and live-line heights.
|
||||
* Unit tests for the layered graph layout: parent centering, depth rows, the
|
||||
* sibling-band WRAP that keeps a narrow sidebar readable (and the row
|
||||
* reservation that stops a wrapped band from landing on a sibling's
|
||||
* children), the live-line height, and the width-aware solver.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
@@ -9,7 +11,10 @@ import {
|
||||
GRAPH_NODE_H,
|
||||
GRAPH_NODE_W,
|
||||
GRAPH_PAD,
|
||||
GRAPH_ROW_STRIDE,
|
||||
bandColsFor,
|
||||
layoutTasksGraph,
|
||||
layoutTasksGraphForWidth,
|
||||
} from '../src/client/tasks-graph-layout.ts'
|
||||
import type { TasksAgentNode, TasksNode } from '../src/client/tasks-model.ts'
|
||||
|
||||
@@ -22,14 +27,21 @@ function agent(id: string, parentId?: string, live = false): TasksAgentNode {
|
||||
}
|
||||
}
|
||||
|
||||
/** The row index of one laid-out node. */
|
||||
function layoutRow(layout: ReturnType<typeof layoutTasksGraph>, id: string): number {
|
||||
const box = layout.boxes.get(id)
|
||||
if (box === undefined) throw new Error(`no box for ${id}`)
|
||||
return (box.y - GRAPH_PAD) / GRAPH_ROW_STRIDE
|
||||
}
|
||||
|
||||
describe('layoutTasksGraph', () => {
|
||||
it('places the root on row 0 and children one row below', () => {
|
||||
const nodes: TasksNode[] = [agent('root'), agent('a', 'root'), agent('b', 'root')]
|
||||
const layout = layoutTasksGraph(nodes)
|
||||
expect(layout.boxes.get('root')).toMatchObject({ y: GRAPH_PAD, h: GRAPH_NODE_H })
|
||||
const rowY = GRAPH_PAD + GRAPH_NODE_H + GRAPH_LIVE_H + GRAPH_GAP_Y
|
||||
expect(layout.boxes.get('a')?.y).toBe(rowY)
|
||||
expect(layout.boxes.get('b')?.y).toBe(rowY)
|
||||
// A running agent reserves the live-line row (its "thinking" fallback).
|
||||
expect(layout.boxes.get('root')).toMatchObject({ y: GRAPH_PAD, h: GRAPH_NODE_H + GRAPH_LIVE_H })
|
||||
expect(layout.boxes.get('a')?.y).toBe(GRAPH_PAD + GRAPH_ROW_STRIDE)
|
||||
expect(layout.boxes.get('b')?.y).toBe(GRAPH_PAD + GRAPH_ROW_STRIDE)
|
||||
})
|
||||
|
||||
it('centers a parent over its children band', () => {
|
||||
@@ -39,11 +51,11 @@ describe('layoutTasksGraph', () => {
|
||||
const b = layout.boxes.get('b')
|
||||
const root = layout.boxes.get('root')
|
||||
expect(a).toBeDefined(); expect(b).toBeDefined(); expect(root).toBeDefined()
|
||||
const bandCenter = ((a!.x + b!.x + GRAPH_NODE_W) / 2)
|
||||
const bandCenter = (a!.x + b!.x + GRAPH_NODE_W) / 2
|
||||
expect(root!.x + GRAPH_NODE_W / 2).toBeCloseTo(bandCenter, 5)
|
||||
})
|
||||
|
||||
it('widens the canvas for deep/wide forests and grows live rows', () => {
|
||||
it('grows live rows and widens the canvas for deep/wide forests', () => {
|
||||
const nodes: TasksNode[] = [
|
||||
agent('root'), agent('a', 'root', true),
|
||||
agent('a1', 'a'), agent('a2', 'a'), agent('a3', 'a'),
|
||||
@@ -60,4 +72,53 @@ describe('layoutTasksGraph', () => {
|
||||
expect(layout.boxes.get('root')).toBeDefined()
|
||||
expect(layout.boxes.get('ghost')).toBeDefined()
|
||||
})
|
||||
|
||||
it('wraps a wide sibling band into rows of maxBandCols', () => {
|
||||
const nodes: TasksNode[] = [agent('root')]
|
||||
for (let index = 0; index < 5; index += 1) nodes.push(agent(`c${index}`, 'root'))
|
||||
const layout = layoutTasksGraph(nodes, { maxBandCols: 2 })
|
||||
// Three wrapped rows of 2/2/1, never five on one line.
|
||||
expect([layoutRow(layout, 'c0'), layoutRow(layout, 'c1')]).toEqual([1, 1])
|
||||
expect([layoutRow(layout, 'c2'), layoutRow(layout, 'c3')]).toEqual([2, 2])
|
||||
expect(layoutRow(layout, 'c4')).toBe(3)
|
||||
// The canvas stays as narrow as one wrapped band.
|
||||
expect(layout.width).toBeLessThanOrEqual(2 * GRAPH_NODE_W + 28 + 2 * GRAPH_PAD)
|
||||
})
|
||||
|
||||
it('never lets a wrapped band land on a sibling subtree row', () => {
|
||||
// root → [a (with a child), b, c, d] wrapped at 2 columns: the second
|
||||
// band must start BELOW a's child, not on the same row as it.
|
||||
const nodes: TasksNode[] = [
|
||||
agent('root'), agent('a', 'root'), agent('b', 'root'),
|
||||
agent('c', 'root'), agent('d', 'root'), agent('a1', 'a'),
|
||||
]
|
||||
const layout = layoutTasksGraph(nodes, { maxBandCols: 2 })
|
||||
expect(layoutRow(layout, 'c')).toBeGreaterThan(layoutRow(layout, 'a1'))
|
||||
expect(layoutRow(layout, 'd')).toBe(layoutRow(layout, 'c'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('bandColsFor / layoutTasksGraphForWidth', () => {
|
||||
it('derives the column count from the container width', () => {
|
||||
expect(bandColsFor(360)).toBe(2)
|
||||
expect(bandColsFor(720)).toBe(4)
|
||||
expect(bandColsFor(120)).toBe(1)
|
||||
})
|
||||
|
||||
it('prefers a flat band when the container is wide', () => {
|
||||
const nodes: TasksNode[] = [agent('root')]
|
||||
for (let index = 0; index < 4; index += 1) nodes.push(agent(`c${index}`, 'root'))
|
||||
const wide = layoutTasksGraphForWidth(nodes, 720)
|
||||
expect(['c0', 'c1', 'c2', 'c3'].map(id => layoutRow(wide, id))).toEqual([1, 1, 1, 1])
|
||||
})
|
||||
|
||||
it('wraps instead of overflowing wildly when the container is narrow', () => {
|
||||
const nodes: TasksNode[] = [agent('root')]
|
||||
for (let index = 0; index < 6; index += 1) nodes.push(agent(`c${index}`, 'root'))
|
||||
const narrow = layoutTasksGraphForWidth(nodes, 360)
|
||||
// Never wider than the tolerant budget (a small pan is allowed, a 3x
|
||||
// overflow is not), and never a one-child-per-row ladder.
|
||||
expect(narrow.width).toBeLessThanOrEqual(((360 - 24) / 0.78) * 1.2)
|
||||
expect(layoutRow(narrow, 'c1')).toBe(layoutRow(narrow, 'c0'))
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user