Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
fbe2c7e
fix(graph): frame the graph, keep dragged nodes grabbable, use the de…
jhweir Aug 11, 2026
5507f90
refactor(design-system): one way to reach the JSX types, and both eve…
jhweir Aug 11, 2026
858765f
feat(graph): reified edges, metrics, and a harness that can show them
jhweir Aug 11, 2026
f28a1fa
fix(graph): centre nodes on their own coordinates, drag from where yo…
jhweir Aug 11, 2026
f03b68c
feat(themes): make a theme applicable outside app-shell
jhweir Aug 11, 2026
0672ce8
fix(graph): release a node on click, and give cards their own text
jhweir Aug 11, 2026
3c3ddb7
fix(graph): pick a card by its box, not by a dot in the middle of it
jhweir Aug 11, 2026
a2bcf63
fix(graph): keep the canvas sharp when zoomed
jhweir Aug 11, 2026
7125f45
feat(graph): zoom-scaling as a style option, and write the convention…
jhweir Aug 11, 2026
d89dc14
fix(graph): stop edge labels jittering when zooming
jhweir Aug 11, 2026
f5ca759
fix(graph): draw edge labels as DOM, not SVG text
jhweir Aug 11, 2026
d512c59
fix(graph): make the zoom controls actually work
jhweir Aug 11, 2026
d243b62
refactor(graph): the engine owns edge picking, and chrome is a registry
jhweir Aug 11, 2026
625572d
fix(graph): stop the drawing layer blanking part of the canvas
jhweir Aug 11, 2026
840d21e
fix(primitives): only create the gradient overlay when there is a gra…
jhweir Aug 11, 2026
5bd6764
fix(design-system): stop `bg` clobbering the background longhands
jhweir Aug 11, 2026
e53c233
fix(design-system): stop state transitions animating geometry
jhweir Aug 11, 2026
bdf4b3a
fix(design-system): make hover state feedback stop flickering
jhweir Aug 11, 2026
0ecb6ba
test(playground): a bisecting probe for hover rendering artefacts
jhweir Aug 11, 2026
db228d8
feat(graph): four edge shapes, and an arrowhead the line does not sho…
jhweir Aug 11, 2026
3ecfb05
feat(graph): smooth by default, and keep a chosen shape when edges ar…
jhweir Aug 11, 2026
eca137b
fix(graph): repaint on restyle, fan mutual edges apart, land arrows w…
jhweir Aug 11, 2026
e78cd4e
fix(graph): separate axis-aligned edges at the nodes, not only in the…
jhweir Aug 11, 2026
a9a8a5f
fix(graph): rebuild a layout when its options change, not only its type
jhweir Aug 11, 2026
e43baec
fix(graph): follow a settling layout with the camera, and let a layou…
jhweir Aug 11, 2026
d605aca
test(graph-explorer): persist board positions the way a board would
jhweir Aug 11, 2026
522b94f
feat(graph): controls that have a state, and a lock and a pin built o…
jhweir Aug 11, 2026
0769ec8
fix(graph): mark a pinned node only where being pinned is exceptional
jhweir Aug 11, 2026
442b82a
fix(graph): let a settled layout start moving again, and own pinned s…
jhweir Aug 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion .cursor/rules/we-schema.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -917,7 +917,7 @@ Common recipes:
the relations between them. Picks up model types added later with no template change.
- **Hierarchy** — `layout: { type: 'tree' }` with a `collection` expansion for nested content.
- **Static diagram** — `seeds: { literal: true, nodes: [...], edges: [...] }` and no expansion at all.
Props: seeds?: SeedSpec | SeedSpec[], expansion?: ExpansionSpec, layout?: LayoutSpec, nodeStyle?: NodeStyleRules, edgeStyle?: EdgeStyleRules, behaviours?: BehaviourSpec[], width?: string, height?: string, bg?: string, showStatus?: boolean, showControls?: boolean, onNodeClick?: ((node: GraphNode) => void), onNodeDoubleClick?: ((node: GraphNode) => void), onEdgeClick?: ((edge: GraphEdge) => void), onSelectionChange?: ((ids: string[]) => void), onNodeDragEnd?: ((payload: { id: string; x: number; y: number; }) => void), host?: GraphHostBindings
Props: seeds?: SeedSpec | SeedSpec[], expansion?: ExpansionSpec, layout?: LayoutSpec, nodeStyle?: NodeStyleRules, edgeStyle?: EdgeStyleRules, behaviours?: BehaviourSpec[], reified?: Record<string, { source: string; target: string; type?: string; }>, width?: string, height?: string, bg?: string, showStatus?: boolean, showControls?: boolean, controls?: string[], onNodeClick?: ((node: GraphNode) => void), onNodeDoubleClick?: ((node: GraphNode) => void), onEdgeClick?: ((edge: GraphEdge) => void), onSelectionChange?: ((ids: string[]) => void), onNodeDragEnd?: ((payload: { id: string; x: number; y: number; }) => void), host?: GraphHostBindings

---

Expand Down Expand Up @@ -989,6 +989,41 @@ Names resolvable inside GraphView props: seed sources (seeds.source), expanders
- yField: string — Node data field holding y. Default "y".
- Example: `{ "type": "manual" }`

**style**

- `curve` — Edge style — the shape a connection is drawn with. "smooth" (default) leaves and arrives along the axis the edge mostly runs on, the flow-chart S, so it reads as direction and suits hierarchies and pipelines. "straight" is a direct line, right when the layout is already doing the talking. "arc" bows to one side, for a graph dense enough that lines need telling apart by shape. "step" turns at right angles, for containment and org charts where the eye follows a rank. Two nodes related in both directions are always separated — shifted sideways, or crossed at different points — so picking a shape never hides a relationship.
- Example: `"edgeStyle": [{ "style": { "curve": "smooth" } }]`
- `arrow` — Edge style — which ends carry an arrowhead. "target" (default) points at the thing being related to; "both" for a mutual relationship drawn as one line; "none" when the relation has no direction worth showing. The head scales with the line's width, and the line stops short of it rather than running underneath.
- Example: `"edgeStyle": [{ "style": { "arrow": "none" } }]`
- `scaleWithZoom` — Edge style. true (default) treats the line as part of the drawing, so it thickens as you zoom in — right for a board. false pins it to a constant on-screen width, so hairlines stay visible when you zoom out to see a whole network.
- Example: `"edgeStyle": [{ "style": { "scaleWithZoom": false } }]`
- `scaleLabelWithZoom` — Node style. true (default) scales the label with the camera; false keeps it a constant on-screen size, which keeps text readable at any zoom on a map you navigate by reading. Affects the label only — a node mark always scales, because its size and its hit area are both world units.
- Example: `"nodeStyle": [{ "style": { "scaleLabelWithZoom": false } }]`
- `labelMinZoom` — Node style. Hides the label below this zoom level, so a dense graph stays readable when zoomed out and gains its detail as you move in.
- Example: `"nodeStyle": [{ "style": { "labelMinZoom": 0.6 } }]`

**metric**

- `degree` — How connected a node is, normalised 0..1. The usual answer to "make the important things bigger". Reference it from a style value rather than a fixed number.
- range: [number, number] — Output range, e.g. [8, 30].
- Example: `"nodeStyle": [{ "style": { "size": { "metric": "degree", "range": [10, 34] } } }]`
- `community` — Groups the visible graph by label propagation. Pair with scale: "categorical" to colour each cluster differently — this is what makes a cluster map.
- rounds: number — Propagation rounds. Default 8.
- Example: `"nodeStyle": [{ "style": { "color": { "metric": "community", "scale": "categorical" } } }]`

**control**

- `zoom-in` — Zooms toward the centre of the view. Shown by default.
- Example: `"controls": ["zoom-in", "zoom-out", "fit"]`
- `zoom-out` — Zooms out from the centre. Shown by default.
- `fit` — Frames everything currently on the graph. Deliberately not a re-layout — it moves the camera, never the nodes.
- `pin` — Holds the selected nodes where they are, so the layout stops moving them; press again to release. The usual way to shape a force graph — put the thing you care about where you want it, hold it there, and let the rest settle around it. Held nodes are ringed so the state is visible. Not shown by default: on a board every node is placed already and it means nothing.
- Example: `"controls": ["zoom-in", "zoom-out", "fit", "pin"]`
- `lock` — Blocks moving nodes, so a graph cannot be rearranged by accident while it is being read or shown to someone. Affects dragging only — panning, zooming and a settling force layout all carry on. Not shown by default, and only meaningful where the template allows dragging at all.
- Example: `"controls": ["zoom-in", "zoom-out", "fit", "lock"]`
- `relayout` — Re-runs the layout. Not shown by default: a rescue for a tangled force graph, and destructive on a board, where it would discard every position somebody chose.
- Example: `"controls": ["zoom-in", "zoom-out", "fit", "relayout"]`

**behaviour**

- `pan-zoom` — Drag the background to pan, wheel to zoom about the pointer. List it last — it is the fallback.
Expand Down
37 changes: 36 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -917,7 +917,7 @@ Common recipes:
the relations between them. Picks up model types added later with no template change.
- **Hierarchy** — `layout: { type: 'tree' }` with a `collection` expansion for nested content.
- **Static diagram** — `seeds: { literal: true, nodes: [...], edges: [...] }` and no expansion at all.
Props: seeds?: SeedSpec | SeedSpec[], expansion?: ExpansionSpec, layout?: LayoutSpec, nodeStyle?: NodeStyleRules, edgeStyle?: EdgeStyleRules, behaviours?: BehaviourSpec[], width?: string, height?: string, bg?: string, showStatus?: boolean, showControls?: boolean, onNodeClick?: ((node: GraphNode) => void), onNodeDoubleClick?: ((node: GraphNode) => void), onEdgeClick?: ((edge: GraphEdge) => void), onSelectionChange?: ((ids: string[]) => void), onNodeDragEnd?: ((payload: { id: string; x: number; y: number; }) => void), host?: GraphHostBindings
Props: seeds?: SeedSpec | SeedSpec[], expansion?: ExpansionSpec, layout?: LayoutSpec, nodeStyle?: NodeStyleRules, edgeStyle?: EdgeStyleRules, behaviours?: BehaviourSpec[], reified?: Record<string, { source: string; target: string; type?: string; }>, width?: string, height?: string, bg?: string, showStatus?: boolean, showControls?: boolean, controls?: string[], onNodeClick?: ((node: GraphNode) => void), onNodeDoubleClick?: ((node: GraphNode) => void), onEdgeClick?: ((edge: GraphEdge) => void), onSelectionChange?: ((ids: string[]) => void), onNodeDragEnd?: ((payload: { id: string; x: number; y: number; }) => void), host?: GraphHostBindings

---

Expand Down Expand Up @@ -989,6 +989,41 @@ Names resolvable inside GraphView props: seed sources (seeds.source), expanders
- yField: string — Node data field holding y. Default "y".
- Example: `{ "type": "manual" }`

**style**

- `curve` — Edge style — the shape a connection is drawn with. "smooth" (default) leaves and arrives along the axis the edge mostly runs on, the flow-chart S, so it reads as direction and suits hierarchies and pipelines. "straight" is a direct line, right when the layout is already doing the talking. "arc" bows to one side, for a graph dense enough that lines need telling apart by shape. "step" turns at right angles, for containment and org charts where the eye follows a rank. Two nodes related in both directions are always separated — shifted sideways, or crossed at different points — so picking a shape never hides a relationship.
- Example: `"edgeStyle": [{ "style": { "curve": "smooth" } }]`
- `arrow` — Edge style — which ends carry an arrowhead. "target" (default) points at the thing being related to; "both" for a mutual relationship drawn as one line; "none" when the relation has no direction worth showing. The head scales with the line's width, and the line stops short of it rather than running underneath.
- Example: `"edgeStyle": [{ "style": { "arrow": "none" } }]`
- `scaleWithZoom` — Edge style. true (default) treats the line as part of the drawing, so it thickens as you zoom in — right for a board. false pins it to a constant on-screen width, so hairlines stay visible when you zoom out to see a whole network.
- Example: `"edgeStyle": [{ "style": { "scaleWithZoom": false } }]`
- `scaleLabelWithZoom` — Node style. true (default) scales the label with the camera; false keeps it a constant on-screen size, which keeps text readable at any zoom on a map you navigate by reading. Affects the label only — a node mark always scales, because its size and its hit area are both world units.
- Example: `"nodeStyle": [{ "style": { "scaleLabelWithZoom": false } }]`
- `labelMinZoom` — Node style. Hides the label below this zoom level, so a dense graph stays readable when zoomed out and gains its detail as you move in.
- Example: `"nodeStyle": [{ "style": { "labelMinZoom": 0.6 } }]`

**metric**

- `degree` — How connected a node is, normalised 0..1. The usual answer to "make the important things bigger". Reference it from a style value rather than a fixed number.
- range: [number, number] — Output range, e.g. [8, 30].
- Example: `"nodeStyle": [{ "style": { "size": { "metric": "degree", "range": [10, 34] } } }]`
- `community` — Groups the visible graph by label propagation. Pair with scale: "categorical" to colour each cluster differently — this is what makes a cluster map.
- rounds: number — Propagation rounds. Default 8.
- Example: `"nodeStyle": [{ "style": { "color": { "metric": "community", "scale": "categorical" } } }]`

**control**

- `zoom-in` — Zooms toward the centre of the view. Shown by default.
- Example: `"controls": ["zoom-in", "zoom-out", "fit"]`
- `zoom-out` — Zooms out from the centre. Shown by default.
- `fit` — Frames everything currently on the graph. Deliberately not a re-layout — it moves the camera, never the nodes.
- `pin` — Holds the selected nodes where they are, so the layout stops moving them; press again to release. The usual way to shape a force graph — put the thing you care about where you want it, hold it there, and let the rest settle around it. Held nodes are ringed so the state is visible. Not shown by default: on a board every node is placed already and it means nothing.
- Example: `"controls": ["zoom-in", "zoom-out", "fit", "pin"]`
- `lock` — Blocks moving nodes, so a graph cannot be rearranged by accident while it is being read or shown to someone. Affects dragging only — panning, zooming and a settling force layout all carry on. Not shown by default, and only meaningful where the template allows dragging at all.
- Example: `"controls": ["zoom-in", "zoom-out", "fit", "lock"]`
- `relayout` — Re-runs the layout. Not shown by default: a rescue for a tangled force graph, and destructive on a board, where it would discard every position somebody chose.
- Example: `"controls": ["zoom-in", "zoom-out", "fit", "relayout"]`

**behaviour**

- `pan-zoom` — Drag the background to pan, wheel to zoom about the pointer. List it last — it is the fallback.
Expand Down
Loading
Loading