diff --git a/docs/architecture/09-visual-design.md b/docs/architecture/09-visual-design.md index 306cdad..b2c8fb1 100644 --- a/docs/architecture/09-visual-design.md +++ b/docs/architecture/09-visual-design.md @@ -377,7 +377,9 @@ drawn on the same 32-grid (stroked where a line reads truer than a fill). It lab leaves of the composition wireframe so views read apart at a glance; mark synonyms (circle/square → point, trail → line, image → rect) collapse onto it via `markIconName` (`mark-icon.ts`), and an unknown or absent mark falls to `mark-generic`. Decorative there -(the box's `aria-label` names the view), so these are not in the ⭐ icon-only set. +(the box's `aria-label` names the view), so these are not in the ⭐ icon-only set. The sibling +`layers` glyph (two offset planes) badges a `layer` node in the same wireframe — a row of marks +in one frame — as a single shared space rather than a concat's box-per-view. **Scoped set** — single-surface, glyph **reserved** in the ledger but **not yet in the `Icon` registry**: diff --git a/docs/architecture/10-interaction-and-feedback.md b/docs/architecture/10-interaction-and-feedback.md index 808b829..057e4e7 100644 --- a/docs/architecture/10-interaction-and-feedback.md +++ b/docs/architecture/10-interaction-and-feedback.md @@ -228,7 +228,10 @@ schematic of the spec's multi-view composition — `CompositionWireframe`, arch tabindex**, arrow keys in **logical (document) order** — Up/Down between nodes, Left → parent, Right → first child, Home/End, Enter/Space activate — not spatial, since a mixed horizontal/ vertical layout makes spatial arrows ambiguous. Each leaf carries a glyph of its mark type (the -`mark-*` icon sub-family, arch 09 §5) so views read apart at a glance. Selecting a box reveals + +`mark-*` icon sub-family, arch 09 §5) so views read apart at a glance. A `layer` — one plotting +space with several marks stacked — renders as **one frame** holding its child marks as a row of +glyphs, badged as layered (the `layers` glyph), rather than the box-per-view of a concat; each +mark stays an individual `treeitem` so selection and z-order reorder still work. Selecting a box reveals + selects that view's source range in the editor (`AppStore.requestRevealView`) but **does not steal focus**, so the wireframe stays the active surface while the editor scrolls to follow; the editor selection is the single source of truth. The toolbar glyph appears **only for a composed spec** — diff --git a/docs/exploration/visual-composition-editing-exploration.md b/docs/exploration/visual-composition-editing-exploration.md index 2d389b3..de4da1a 100644 --- a/docs/exploration/visual-composition-editing-exploration.md +++ b/docs/exploration/visual-composition-editing-exploration.md @@ -149,14 +149,13 @@ DOM `` nodes measurable via `getBoundingClientRect()`. The app alr ## 6a. Deferred polish (Phase A follow-ons) -Built but parked for a later pass: - -- **Mark-type glyph per leaf.** A simplified icon of each unit view's mark (bar/line/point/…) - inside its box, so which-is-which reads at a glance without hovering. Needs a small mark→glyph - set (its own icon-ledger sub-family). -- **A more legible `layered` primitive.** Layers currently render as offset stacked rectangles. - A dedicated "stacked planes" metaphor — overlapping sheets/disks, like the database glyph's - stacked cylinders — would convey "one space, several layers" more elegantly than nudged boxes. +- **Mark-type glyph per leaf.** _(Shipped.)_ A simplified glyph of each unit view's mark inside + its box (the `mark-*` icon sub-family), so which-is-which reads at a glance. +- **A more legible `layered` primitive.** _(Shipped.)_ A layer renders as **one frame** holding + its child marks as a row of glyphs, badged as layered (the `layers` glyph) — not separate boxes, + so it reads as one space and stays distinct from a concat (which is box-per-view). The + overlapping/stacked-planes options weighed here were rejected: at glyph scale, overlapping + line-art muddies the very marks the glyphs exist to show; legibility beat the z-order-depth cue. - **Hide the affordance for single-view specs.** _(Shipped.)_ The toolbar glyph appears only when the spec has a composition. diff --git a/src/app/components/CompositionWireframe.module.css b/src/app/components/CompositionWireframe.module.css index b7d3d7a..d7347a1 100644 --- a/src/app/components/CompositionWireframe.module.css +++ b/src/app/components/CompositionWireframe.module.css @@ -119,23 +119,38 @@ .grid > * { flex: 1 1 84px; } -/* Layer: children share one plotting area — overlap them in a single grid cell so - the stack reads as depth, not as siblings. */ +/* Layer: one plotting space holding several marks (z-order). Render it as a single + frame — a row of the child mark glyphs, badged as layered — not as separate boxes, + so it reads as one space and the marks stay legible (vs. the concat box-per-view). */ +.layerBox { + position: relative; +} .layered { - display: grid; - padding: 0 14px 14px 0; + display: flex; + flex-flow: row wrap; + align-items: center; + justify-content: center; + gap: var(--space-3); + min-height: 40px; } -.layered > * { - grid-area: 1 / 1; +/* A layer's mark is a bare glyph, not a boxed leaf — the frame is the box. The + transparent border keeps the hover/selection outline (from `.node`) consistent. */ +.layerMark { + min-height: 0; + padding: var(--space-2) var(--space-3); + border-color: transparent; + background: transparent; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; } -.layered > *:nth-child(2) { - transform: translate(7px, 7px); -} -.layered > *:nth-child(3) { - transform: translate(14px, 14px); -} -.layered > *:nth-child(n + 4) { - transform: translate(21px, 21px); +.layerBadge { + position: absolute; + top: var(--space-2); + right: var(--space-2); + color: var(--text-placeholder); + pointer-events: none; } /* Facet / repeat: one authored child stands for many generated cells — a card diff --git a/src/app/components/CompositionWireframe.test.tsx b/src/app/components/CompositionWireframe.test.tsx index 507f5f0..dda893b 100644 --- a/src/app/components/CompositionWireframe.test.tsx +++ b/src/app/components/CompositionWireframe.test.tsx @@ -20,6 +20,7 @@ import { markIconName } from './mark-icon'; (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; const COMPOSED = JSON.stringify({ vconcat: [{ mark: 'point' }, { mark: 'bar' }] }, null, 2); +const LAYERED = JSON.stringify({ layer: [{ mark: 'bar' }, { mark: 'line' }] }, null, 2); let container: HTMLDivElement; let root: Root; @@ -109,6 +110,26 @@ describe('CompositionWireframe', () => { expect(COMPOSED.slice(target.offset, target.offset + target.length)).toContain('"bar"'); }); + test('a layer discloses its marks as reorderable treeitems (z-order)', async () => { + // The layer renders as one frame of mark glyphs, but each mark stays a treeitem + // so selection and Alt+arrow z-order reorder keep working. + setEditableSpec(LAYERED); + await renderOpen(); + expect(treeItems().map((el) => el.dataset.key)).toEqual(['root', 'layer|0', 'layer|1']); + act(() => { + item('root').dispatchEvent(key({ key: 'ArrowDown' })); + }); + act(() => { + item('layer|0').dispatchEvent(key({ key: 'ArrowDown', altKey: true })); + }); + expect(useAppStore.getState().composeRequest).toMatchObject({ + kind: 'move', + arrayPath: ['layer'], + from: 0, + to: 1, + }); + }); + test('Alt+ArrowDown asks the editor to reorder the focused view down', async () => { setEditableSpec(COMPOSED); await renderOpen(); diff --git a/src/app/components/CompositionWireframe.tsx b/src/app/components/CompositionWireframe.tsx index fcb613f..ee76eb8 100644 --- a/src/app/components/CompositionWireframe.tsx +++ b/src/app/components/CompositionWireframe.tsx @@ -75,11 +75,7 @@ const ariaLabelOf = (n: ViewNode): string => : 'view' : `${n.op}, ${n.orientation}, ${n.children.length} views`; -// TODO (deferred polish — docs/exploration/visual-composition-editing-exploration.md §6a): -// replace the offset-rectangle `layered` look with a "stacked planes" primitive -// (overlapping sheets/disks, like the database glyph) to read as one shared space. - -/** Children layout class per orientation (layered overlaps in one grid cell). */ +/** Children layout class per orientation (layered is a row of marks in one frame). */ const LAYOUT: Record = { horizontal: styles.row, vertical: styles.col, @@ -407,10 +403,14 @@ function WireframeTree({ tree }: { tree: ViewNode }) { const idx = n.path.at(-1); const draggable = editable && parent != null && typeof idx === 'number'; const onTarget = drag?.resolution?.targetKey === key ? drag.resolution : null; + // A unit view inside a layer is a bare mark glyph in the layer's shared frame, + // not its own box — a layer is one plotting space with several marks stacked. + const compactMark = parent?.op === 'layer' && !container; const cls = [ styles.node, - container ? styles.container : styles.leaf, + compactMark ? styles.layerMark : container ? styles.container : styles.leaf, + n.op === 'layer' ? styles.layerBox : '', key === selectedKey ? styles.selected : '', generated ? styles.generated : '', ] @@ -451,13 +451,16 @@ function WireframeTree({ tree }: { tree: ViewNode }) { }} > {container ? ( -
- {n.children.map((c) => renderNode(c, n))} -
+ <> + {n.op === 'layer' && } +
+ {n.children.map((c) => renderNode(c, n))} +
+ ) : ( )} diff --git a/src/app/components/Icon.tsx b/src/app/components/Icon.tsx index 1eda6af..3f45b3f 100644 --- a/src/app/components/Icon.tsx +++ b/src/app/components/Icon.tsx @@ -35,6 +35,7 @@ export type IconName = | 'info' // about / information — Carbon Information (outline) | 'revert' // revert draft to last published — Carbon Reset | 'structure' // composition-structure wireframe disclosure (preview toolbar) — nested view blocks + | 'layers' // layered-composition badge (wireframe) — two stacked planes (one shared space) // Mark sub-family (composition wireframe leaves) — a simplified glyph of a unit // view's mark type, so which-is-which reads at a glance. Vega-Lite mark synonyms // collapse onto these via `markIconName` (CompositionWireframe); unknown → generic. @@ -177,6 +178,21 @@ const GLYPHS: Record = { ), + // Two offset planes — a layer is one space with several marks stacked in z-order. + layers: ( + <> + + + + ), // Mark glyphs: simplified renderings of each Vega-Lite mark, drawn on the same // 32-grid. Stroke-based where a line reads truer than a fill (line/rule/generic). 'mark-bar': (