mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Editor: render a layer as one framed row of mark glyphs
A layer is one plotting space with several marks stacked in z-order, so it now renders as a single frame holding its child marks as a row of glyphs, badged with a new 'layers' glyph — instead of offset stacked cards that read as separate spaces and hid the lower glyphs. Each mark stays an individual treeitem, so selection and Alt+up/down z-order reorder are unchanged.
This commit is contained in:
@@ -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
|
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`
|
(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
|
(`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
|
**Scoped set** — single-surface, glyph **reserved** in the ledger but **not yet in
|
||||||
the `Icon` registry**:
|
the `Icon` registry**:
|
||||||
|
|||||||
@@ -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,
|
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/
|
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
|
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
|
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
|
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** —
|
selection is the single source of truth. The toolbar glyph appears **only for a composed spec** —
|
||||||
|
|||||||
@@ -149,14 +149,13 @@ DOM `<g name="…">` nodes measurable via `getBoundingClientRect()`. The app alr
|
|||||||
|
|
||||||
## 6a. Deferred polish (Phase A follow-ons)
|
## 6a. Deferred polish (Phase A follow-ons)
|
||||||
|
|
||||||
Built but parked for a later pass:
|
- **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.
|
||||||
- **Mark-type glyph per leaf.** A simplified icon of each unit view's mark (bar/line/point/…)
|
- **A more legible `layered` primitive.** _(Shipped.)_ A layer renders as **one frame** holding
|
||||||
inside its box, so which-is-which reads at a glance without hovering. Needs a small mark→glyph
|
its child marks as a row of glyphs, badged as layered (the `layers` glyph) — not separate boxes,
|
||||||
set (its own icon-ledger sub-family).
|
so it reads as one space and stays distinct from a concat (which is box-per-view). The
|
||||||
- **A more legible `layered` primitive.** Layers currently render as offset stacked rectangles.
|
overlapping/stacked-planes options weighed here were rejected: at glyph scale, overlapping
|
||||||
A dedicated "stacked planes" metaphor — overlapping sheets/disks, like the database glyph's
|
line-art muddies the very marks the glyphs exist to show; legibility beat the z-order-depth cue.
|
||||||
stacked cylinders — would convey "one space, several layers" more elegantly than nudged boxes.
|
|
||||||
- **Hide the affordance for single-view specs.** _(Shipped.)_ The toolbar glyph appears only when
|
- **Hide the affordance for single-view specs.** _(Shipped.)_ The toolbar glyph appears only when
|
||||||
the spec has a composition.
|
the spec has a composition.
|
||||||
|
|
||||||
|
|||||||
@@ -119,23 +119,38 @@
|
|||||||
.grid > * {
|
.grid > * {
|
||||||
flex: 1 1 84px;
|
flex: 1 1 84px;
|
||||||
}
|
}
|
||||||
/* Layer: children share one plotting area — overlap them in a single grid cell so
|
/* Layer: one plotting space holding several marks (z-order). Render it as a single
|
||||||
the stack reads as depth, not as siblings. */
|
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 {
|
.layered {
|
||||||
display: grid;
|
display: flex;
|
||||||
padding: 0 14px 14px 0;
|
flex-flow: row wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
min-height: 40px;
|
||||||
}
|
}
|
||||||
.layered > * {
|
/* A layer's mark is a bare glyph, not a boxed leaf — the frame is the box. The
|
||||||
grid-area: 1 / 1;
|
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) {
|
.layerBadge {
|
||||||
transform: translate(7px, 7px);
|
position: absolute;
|
||||||
}
|
top: var(--space-2);
|
||||||
.layered > *:nth-child(3) {
|
right: var(--space-2);
|
||||||
transform: translate(14px, 14px);
|
color: var(--text-placeholder);
|
||||||
}
|
pointer-events: none;
|
||||||
.layered > *:nth-child(n + 4) {
|
|
||||||
transform: translate(21px, 21px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Facet / repeat: one authored child stands for many generated cells — a card
|
/* Facet / repeat: one authored child stands for many generated cells — a card
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { markIconName } from './mark-icon';
|
|||||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||||
|
|
||||||
const COMPOSED = JSON.stringify({ vconcat: [{ mark: 'point' }, { mark: 'bar' }] }, null, 2);
|
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 container: HTMLDivElement;
|
||||||
let root: Root;
|
let root: Root;
|
||||||
@@ -109,6 +110,26 @@ describe('CompositionWireframe', () => {
|
|||||||
expect(COMPOSED.slice(target.offset, target.offset + target.length)).toContain('"bar"');
|
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 () => {
|
test('Alt+ArrowDown asks the editor to reorder the focused view down', async () => {
|
||||||
setEditableSpec(COMPOSED);
|
setEditableSpec(COMPOSED);
|
||||||
await renderOpen();
|
await renderOpen();
|
||||||
|
|||||||
@@ -75,11 +75,7 @@ const ariaLabelOf = (n: ViewNode): string =>
|
|||||||
: 'view'
|
: 'view'
|
||||||
: `${n.op}, ${n.orientation}, ${n.children.length} views`;
|
: `${n.op}, ${n.orientation}, ${n.children.length} views`;
|
||||||
|
|
||||||
// TODO (deferred polish — docs/exploration/visual-composition-editing-exploration.md §6a):
|
/** Children layout class per orientation (layered is a row of marks in one frame). */
|
||||||
// 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). */
|
|
||||||
const LAYOUT: Record<Orientation, string> = {
|
const LAYOUT: Record<Orientation, string> = {
|
||||||
horizontal: styles.row,
|
horizontal: styles.row,
|
||||||
vertical: styles.col,
|
vertical: styles.col,
|
||||||
@@ -407,10 +403,14 @@ function WireframeTree({ tree }: { tree: ViewNode }) {
|
|||||||
const idx = n.path.at(-1);
|
const idx = n.path.at(-1);
|
||||||
const draggable = editable && parent != null && typeof idx === 'number';
|
const draggable = editable && parent != null && typeof idx === 'number';
|
||||||
const onTarget = drag?.resolution?.targetKey === key ? drag.resolution : null;
|
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 = [
|
const cls = [
|
||||||
styles.node,
|
styles.node,
|
||||||
container ? styles.container : styles.leaf,
|
compactMark ? styles.layerMark : container ? styles.container : styles.leaf,
|
||||||
|
n.op === 'layer' ? styles.layerBox : '',
|
||||||
key === selectedKey ? styles.selected : '',
|
key === selectedKey ? styles.selected : '',
|
||||||
generated ? styles.generated : '',
|
generated ? styles.generated : '',
|
||||||
]
|
]
|
||||||
@@ -451,13 +451,16 @@ function WireframeTree({ tree }: { tree: ViewNode }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{container ? (
|
{container ? (
|
||||||
<div
|
<>
|
||||||
role="group"
|
{n.op === 'layer' && <Icon name="layers" className={styles.layerBadge} />}
|
||||||
data-orientation={n.orientation}
|
<div
|
||||||
className={`${styles.children} ${n.orientation ? LAYOUT[n.orientation] : ''}`}
|
role="group"
|
||||||
>
|
data-orientation={n.orientation}
|
||||||
{n.children.map((c) => renderNode(c, n))}
|
className={`${styles.children} ${n.orientation ? LAYOUT[n.orientation] : ''}`}
|
||||||
</div>
|
>
|
||||||
|
{n.children.map((c) => renderNode(c, n))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Icon name={markIconName(n.mark)} size="md" className={styles.markIcon} />
|
<Icon name={markIconName(n.mark)} size="md" className={styles.markIcon} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export type IconName =
|
|||||||
| 'info' // about / information — Carbon Information (outline)
|
| 'info' // about / information — Carbon Information (outline)
|
||||||
| 'revert' // revert draft to last published — Carbon Reset
|
| 'revert' // revert draft to last published — Carbon Reset
|
||||||
| 'structure' // composition-structure wireframe disclosure (preview toolbar) — nested view blocks
|
| '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
|
// 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
|
// view's mark type, so which-is-which reads at a glance. Vega-Lite mark synonyms
|
||||||
// collapse onto these via `markIconName` (CompositionWireframe); unknown → generic.
|
// collapse onto these via `markIconName` (CompositionWireframe); unknown → generic.
|
||||||
@@ -177,6 +178,21 @@ const GLYPHS: Record<IconName, ReactNode> = {
|
|||||||
<rect x="9" y="17" width="14" height="5" />
|
<rect x="9" y="17" width="14" height="5" />
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
// Two offset planes — a layer is one space with several marks stacked in z-order.
|
||||||
|
layers: (
|
||||||
|
<>
|
||||||
|
<rect x="6" y="6" width="15" height="15" fill="none" stroke="currentColor" strokeWidth={2} />
|
||||||
|
<rect
|
||||||
|
x="11"
|
||||||
|
y="11"
|
||||||
|
width="15"
|
||||||
|
height="15"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
),
|
||||||
// Mark glyphs: simplified renderings of each Vega-Lite mark, drawn on the same
|
// 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).
|
// 32-grid. Stroke-based where a line reads truer than a fill (line/rule/generic).
|
||||||
'mark-bar': (
|
'mark-bar': (
|
||||||
|
|||||||
Reference in New Issue
Block a user