@poodle64/ui 2026.10.15 → 2026.10.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -119,6 +119,7 @@ rather than rebuilding it:
119
119
  | `search-results` | A ranked retrieval answer: title, the matched passage with accent-tinted highlights, source chip, mapped state and a mono relevance figure per hit; plus the before-any-search and matched-nothing empties. |
120
120
  | `ledger` | The one ledger every module draws: dated rows of money, coins or units, two lines a row, grouped by the viewer's period, the viewer's columns and running balance. See [The ledger](#the-ledger). |
121
121
  | `resize-handle` | The drag edge of a side panel: pointer, arrow keys, double-click reset, remembered width, ceiling measured against the content beside it. AppShell's rail uses it; an app with its own `<aside>` renders `<ResizeHandle bind:width storageKey="…" />` as the aside's last child (positioned) and sets the aside's width from `width`. |
122
+ | `graph-canvas` | A live force-directed graph of entities and their relations: pan, zoom, pinch, find, a legend that filters, a selected node's neighbourhood lit and framed, its card. See [A graph of relations](#a-graph-of-relations). |
122
123
 
123
124
  **The application shell** (`app-shell`, `command-palette`). Page chrome is not
124
125
  what makes an app feel like an app either — the shell is. Five household
@@ -1051,6 +1052,46 @@ the page opened; that item's document stands beside it (`?surface=record`).
1051
1052
  into the same pane opens on its first page, fitted; so does one the column
1052
1053
  moves beside itself as the row crosses 1250px.
1053
1054
 
1055
+ ## A graph of relations
1056
+
1057
+ `GraphCanvas` (`@poodle64/ui/graph-canvas`) draws people, organisations, sources, any entities, as nodes and their relations as edges, laid out live by d3-force and painted on one canvas. It fills its container, so the page gives it a height.
1058
+
1059
+ ```svelte
1060
+ <script lang="ts">
1061
+ import GraphCanvas, { type GraphKind } from '@poodle64/ui/graph-canvas';
1062
+ const kinds: GraphKind[] = [
1063
+ { id: 'person', label: 'People', color: 'var(--chart-1)' },
1064
+ { id: 'organisation', label: 'Organisations', color: 'var(--chart-2)', shape: 'square' },
1065
+ { id: 'source', label: 'Sources', color: 'var(--chart-3)', shape: 'diamond' }
1066
+ ];
1067
+ let graph = $state.raw({ nodes: [], edges: [] });
1068
+ let selected = $state<string | null>(null);
1069
+ </script>
1070
+
1071
+ <div class="h-[70vh]">
1072
+ <GraphCanvas nodes={graph.nodes} edges={graph.edges} {kinds} directed bind:selected
1073
+ onactivate={(n) => goto(`/entities/${n.id}`)} />
1074
+ </div>
1075
+ ```
1076
+
1077
+ | Prop | |
1078
+ | --- | --- |
1079
+ | `nodes` | `{ id, label, kind, description?, emphasis?, size? }[]`. Extra fields ride along to the callbacks and the `detail` snippet, typed. |
1080
+ | `edges` | `{ source, target, label? }[]` by node id; an edge naming an unknown id is dropped. Parallel edges fan out. |
1081
+ | `kinds` | Legend order, `label`, `color` (any CSS colour; default the primary) and `shape` (`circle`, `square`, `diamond`, `triangle`). The package owns no categorical palette, as with `Sparkline`. |
1082
+ | `selected`, `hiddenKinds` | Bindable: the selected id, and the kinds the legend has switched off. |
1083
+ | `directed` | Arrowheads at each target, and the card names a relation pointing here as "Director of this". |
1084
+ | `onselect(node \| null)`, `onactivate(node)` | A click, find or card pick; a double-click, Enter or the card's `activateLabel` button. |
1085
+ | `detail` | `Snippet<[node, neighbours]>` under the card's title, for the app's own actions. |
1086
+ | `search`, `label` | The find field (default on); the accessible name. |
1087
+
1088
+ Exports on the instance (`bind:this`): `fit()`, `focus(id)` (select and frame the neighbourhood), `locate(id)` (where it is drawn, in px) and `relayout()`.
1089
+
1090
+ - **Live data.** Pass `$state.raw` and assign a new array to change it. Every surviving node keeps its place, a newcomer starts beside the nodes it joins, and the layout warms only as much as the change needs. A deep `$state` proxy works, but every paint then reads through it.
1091
+ - **Themed where it is placed.** Each colour is resolved against the component's own element, so inside an element's shadow root it takes the host page's palette, and it follows the mode class, an element host's `data-dark`, a palette swap and the OS scheme without a reload. Nothing portals out.
1092
+ - **Size.** The layout ticks within a time budget per frame, edges and nodes are culled and stroked as a few batched paths, and names are placed in screen space against what is already drawn, a quota by area at rest and every neighbour under a focus. A few thousand nodes is the design point; well past ten thousand, a WebGL renderer is the better tool.
1093
+ - **Load it lazily.** It brings d3-force and a canvas engine, so a route imports it with a dynamic `import()` (`sveltekit-quality.md`).
1094
+
1054
1095
  ## Hand-written forms
1055
1096
 
1056
1097
  The shadcn-svelte Formsnap wrapper set, over `sveltekit-superforms`:
@@ -825,11 +825,12 @@
825
825
  -->
826
826
  <!--
827
827
  `measure` caps and centres this box, through `.ds-shell-measure`
828
- in styles.css rather than a utility here — an arbitrary
829
- `max-w-[var(…)]` only exists if the consumer's Tailwind build
830
- scanned the file spelling it, and a cap that silently does not
831
- compile is the failure mode this package gates against everywhere
832
- else.
828
+ in styles.css rather than a utility here — an arbitrary max-width
829
+ utility only exists if the consumer's Tailwind build scanned the
830
+ file spelling it, and a cap that silently does not compile is the
831
+ failure mode this package gates against everywhere else. (Never
832
+ write the utility's class name in a comment: Tailwind scans
833
+ comments too, and compiles the placeholder it finds there.)
833
834
 
834
835
  At `full` the class and the attribute are both ABSENT, not set to
835
836
  a no-op value. That is what makes the prop additive: a consumer