From bdf098b2c7339fd8d2972e414406ec2588dbea24 Mon Sep 17 00:00:00 2001 From: aaltshuler Date: Tue, 25 Aug 2026 00:19:39 +0300 Subject: [PATCH 1/3] Storybook catalog: workspace scaffold, fixtures, five anchor stories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New apps/storybook workspace (Storybook 10.5.10, react-vite, exact-pinned): - .storybook config: docs addon, autodocs, dark/light theme toolbar, section-ordered sidebar, telemetry off - fixtures copied from the demo (seeded generator, themes, scale descriptors, crossfilter dims, sunflower positions) plus a concrete DemoGraph alias and FakeEngine holder for later component stories - stories: Graph/Minimal, Graph/Styling (scale switching), Graph/Themes, Graph/Layout: force (live sim sliders), Interaction/Selection (lasso + actions) — all on CosmosEngine, framed per-story - Introduction MDX: What is orbit, Data shapes, Testing without WebGL Root wiring: storybook:dev/storybook:build scripts, changesets ignore, eslint + gitignore entries for storybook-static. CI untouched. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018SeFxK217ZrHSERqK6kjcb --- .changeset/config.json | 3 +- .gitignore | 1 + apps/storybook/.storybook/main.ts | 15 + apps/storybook/.storybook/preview.ts | 44 + apps/storybook/package.json | 29 + apps/storybook/src/fixtures/DemoGraph.tsx | 35 + apps/storybook/src/fixtures/config.ts | 51 + apps/storybook/src/fixtures/engines.ts | 31 + apps/storybook/src/fixtures/generate.ts | 189 +++ apps/storybook/src/fixtures/index.ts | 7 + apps/storybook/src/fixtures/positions.ts | 26 + apps/storybook/src/fixtures/scales.ts | 57 + apps/storybook/src/fixtures/themes.ts | 31 + .../src/graph/LayoutForce.stories.tsx | 62 + apps/storybook/src/graph/Minimal.stories.tsx | 57 + apps/storybook/src/graph/Styling.stories.tsx | 82 ++ apps/storybook/src/graph/Themes.stories.tsx | 66 + .../src/interaction/Selection.stories.tsx | 72 + .../storybook/src/introduction/DataShapes.mdx | 45 + .../src/introduction/TestingWithoutWebGL.mdx | 51 + .../src/introduction/WhatIsOrbit.mdx | 49 + apps/storybook/tsconfig.json | 7 + apps/storybook/vite.config.ts | 9 + eslint.config.mjs | 1 + package.json | 2 + pnpm-lock.yaml | 1217 ++++++++++++++++- 26 files changed, 2236 insertions(+), 3 deletions(-) create mode 100644 apps/storybook/.storybook/main.ts create mode 100644 apps/storybook/.storybook/preview.ts create mode 100644 apps/storybook/package.json create mode 100644 apps/storybook/src/fixtures/DemoGraph.tsx create mode 100644 apps/storybook/src/fixtures/config.ts create mode 100644 apps/storybook/src/fixtures/engines.ts create mode 100644 apps/storybook/src/fixtures/generate.ts create mode 100644 apps/storybook/src/fixtures/index.ts create mode 100644 apps/storybook/src/fixtures/positions.ts create mode 100644 apps/storybook/src/fixtures/scales.ts create mode 100644 apps/storybook/src/fixtures/themes.ts create mode 100644 apps/storybook/src/graph/LayoutForce.stories.tsx create mode 100644 apps/storybook/src/graph/Minimal.stories.tsx create mode 100644 apps/storybook/src/graph/Styling.stories.tsx create mode 100644 apps/storybook/src/graph/Themes.stories.tsx create mode 100644 apps/storybook/src/interaction/Selection.stories.tsx create mode 100644 apps/storybook/src/introduction/DataShapes.mdx create mode 100644 apps/storybook/src/introduction/TestingWithoutWebGL.mdx create mode 100644 apps/storybook/src/introduction/WhatIsOrbit.mdx create mode 100644 apps/storybook/tsconfig.json create mode 100644 apps/storybook/vite.config.ts diff --git a/.changeset/config.json b/.changeset/config.json index 100b60e..b1b3e5d 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -17,6 +17,7 @@ "updateInternalDependencies": "patch", "ignore": [ "orbit-demo", - "orbit-spike" + "orbit-spike", + "orbit-storybook" ] } diff --git a/.gitignore b/.gitignore index 47d435a..b23103c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ packages/omnigraph/test/__generated__/ # Local-only probe/perf evidence (never committed) .evidence/ +apps/storybook/storybook-static/ diff --git a/apps/storybook/.storybook/main.ts b/apps/storybook/.storybook/main.ts new file mode 100644 index 0000000..fcdb848 --- /dev/null +++ b/apps/storybook/.storybook/main.ts @@ -0,0 +1,15 @@ +import type { StorybookConfig } from '@storybook/react-vite'; + +const config: StorybookConfig = { + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(ts|tsx)'], + addons: ['@storybook/addon-docs'], + framework: { + name: '@storybook/react-vite', + options: {}, + }, + core: { + disableTelemetry: true, + }, +}; + +export default config; diff --git a/apps/storybook/.storybook/preview.ts b/apps/storybook/.storybook/preview.ts new file mode 100644 index 0000000..bf22d0e --- /dev/null +++ b/apps/storybook/.storybook/preview.ts @@ -0,0 +1,44 @@ +import type { Preview } from '@storybook/react-vite'; + +const preview: Preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + options: { + storySort: { + order: [ + 'Introduction', + ['What is orbit', 'Data shapes', 'Testing without WebGL'], + 'Graph', + ['Minimal', 'Styling', 'Themes', 'Layout: force'], + 'Interaction', + 'Exploration', + 'Analytics', + 'Components', + 'Persistence', + 'Scale', + ], + }, + }, + }, + tags: ['autodocs'], + globalTypes: { + theme: { + description: 'Graph theme', + toolbar: { + icon: 'mirror', + items: ['dark', 'light'], + dynamicTitle: true, + }, + }, + }, + initialGlobals: { + theme: 'dark', + }, +}; + +export default preview; diff --git a/apps/storybook/package.json b/apps/storybook/package.json new file mode 100644 index 0000000..3b7d09b --- /dev/null +++ b/apps/storybook/package.json @@ -0,0 +1,29 @@ +{ + "name": "orbit-storybook", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "storybook dev -p 5399 --exact-port", + "build": "storybook build", + "typecheck": "tsc --noEmit", + "test": "echo \"storybook has no unit tests\" && exit 0" + }, + "dependencies": { + "@modernrelay/orbit-core": "workspace:*", + "@modernrelay/orbit-engine-cosmos": "workspace:*", + "@modernrelay/orbit-react": "workspace:*", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@storybook/addon-docs": "10.5.10", + "@storybook/react-vite": "10.5.10", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.0", + "storybook": "10.5.10", + "typescript": "^5.6.0", + "vite": "^6.0.0" + } +} diff --git a/apps/storybook/src/fixtures/DemoGraph.tsx b/apps/storybook/src/fixtures/DemoGraph.tsx new file mode 100644 index 0000000..e178434 --- /dev/null +++ b/apps/storybook/src/fixtures/DemoGraph.tsx @@ -0,0 +1,35 @@ +/** + * Concrete alias over the generic > so stories get full prop + * typing without generic-inference friction (Storybook's Meta/StoryObj work + * against a concrete component type), plus the standard story frame. + */ + +import type { ReactNode } from 'react'; +import { Graph } from '@modernrelay/orbit-react'; +import type { GraphProps } from '@modernrelay/orbit-react'; +import type { DemoEdgeAttrs, DemoNodeAttrs } from './generate'; + +export type DemoGraphProps = GraphProps; + +export function DemoGraph(props: DemoGraphProps): ReactNode { + return {...props} />; +} + +/** Standard story frame: the graph fills its parent, so stories mount inside + * a fixed-height surface whose background matches the active theme. */ +export function GraphFrame(props: { background: string; children: ReactNode }): ReactNode { + return ( +
+ {props.children} +
+ ); +} diff --git a/apps/storybook/src/fixtures/config.ts b/apps/storybook/src/fixtures/config.ts new file mode 100644 index 0000000..0d2d852 --- /dev/null +++ b/apps/storybook/src/fixtures/config.ts @@ -0,0 +1,51 @@ +/** + * Copied from apps/demo/src/App.tsx module-scope config constants, simplified + * to DemoNodeAttrs (no omnigraph union) — keep in sync by hand. All values are + * module-scope: dimension/label identity changes would rebuild sessions. + */ + +import type { + AccessibilityConfig, + DimensionSpec, + GraphNode, + LabelConfig, + SimulationConfig, +} from '@modernrelay/orbit-core'; +import type { DemoNodeAttrs } from './generate'; + +export const SIMULATION: SimulationConfig = { repulsion: 0.6, gravity: 0.25 }; + +export const labelOf = (node: GraphNode): string => + node.attrs?.label ?? node.id; + +/** label lane: zoom-LOD at 1.2, ranked cap 48, cluster hubs forced. */ +export const LABELS: LabelConfig = { + minZoom: 1.2, + maxVisible: 48, + showFor: ['n0', 'n1'], + getText: labelOf, +}; + +export const ACCESSIBILITY: AccessibilityConfig = { + label: 'orbit storybook graph', + getAccessibleLabel: labelOf, +}; + +/** construction-only: reads searchIndex once at mount. */ +export const SEARCH_INDEX: readonly string[] = ['label']; + +export const SCORE_DIM: DimensionSpec = { + key: 'score', + kind: 'numeric', + bins: 30, + get: (node) => node.attrs?.score, +}; + +export const CREATED_DIM: DimensionSpec = { + key: 'createdAt', + kind: 'temporal', + bins: 60, + get: (node) => node.attrs?.createdAt, +}; + +export const CROSSFILTER_DIMS: readonly DimensionSpec[] = [SCORE_DIM, CREATED_DIM]; diff --git a/apps/storybook/src/fixtures/engines.ts b/apps/storybook/src/fixtures/engines.ts new file mode 100644 index 0000000..9ff9874 --- /dev/null +++ b/apps/storybook/src/fixtures/engines.ts @@ -0,0 +1,31 @@ +/** + * Engine factories for stories. + * + * Cosmos = real WebGL (the visual catalog). FakeEngine = headless double for + * component stories and play functions — its inject* methods drive hover, + * click, context-menu, and viewport events without a GPU. + */ + +import { FakeEngine } from '@modernrelay/orbit-core/testing'; +import type { FakeEngineOptions } from '@modernrelay/orbit-core/testing'; +import { CosmosEngine } from '@modernrelay/orbit-engine-cosmos'; + +export const cosmosEngine = () => new CosmosEngine(); + +export interface FakeEngineHolder { + /** pass to — one engine per mounted Graph */ + factory: () => FakeEngine; + /** the most recently constructed engine (for play-function inject* calls) */ + current: () => FakeEngine | null; +} + +export function makeFakeEngineHolder(opts?: FakeEngineOptions): FakeEngineHolder { + let engine: FakeEngine | null = null; + return { + factory: () => { + engine = opts === undefined ? new FakeEngine() : new FakeEngine(opts); + return engine; + }, + current: () => engine, + }; +} diff --git a/apps/storybook/src/fixtures/generate.ts b/apps/storybook/src/fixtures/generate.ts new file mode 100644 index 0000000..b95a994 --- /dev/null +++ b/apps/storybook/src/fixtures/generate.ts @@ -0,0 +1,189 @@ +/** + * Copied from apps/demo/src/generate.ts — keep in sync by hand. + * (Apps do not import across apps; the generator is small and stable.) + */ +/** + * Seeded, deterministic clustered-graph generator for the demo app. + * + * Growth stability invariant: every node draws from its own rng stream seeded + * by (seed, index), and only ever connects to LOWER-indexed nodes. Therefore + * `generateGraph({...p, nodes: n + 500 })` is an exact superset of + * `generateGraph({...p, nodes: n })` — same node ids, same edges, appended + * tail — which is what the "Add 500 nodes" button relies on to exercise the + * incremental diff and position preservation. (Degrees of existing nodes may + * grow when new nodes attach to them; that is an attrs-only change.) + */ + +import type { GraphEdge, GraphNode, GraphSnapshot } from '@modernrelay/orbit-core'; + +export interface DemoNodeAttrs { + cluster: number; + label: string; + /** Computed after edge generation. */ + degree: number; + /** Cluster-correlated pseudo-normal score (v0.7 histogram dimension). */ + score: number; + /** Epoch ms inside a 180-day cluster-phased window (v0.7 timeline dimension). */ + createdAt: number; +} + +export interface DemoEdgeAttrs { + kind: 'intra' | 'inter'; +} + +export type DemoSnapshot = GraphSnapshot; + +export interface GenerateParams { + seed: number; + nodes: number; + clusters: number; + /** Average intra-cluster edges created per node (e.g. 1.6). */ + intraEdgeFactor: number; + /** Probability a node also gets one inter-cluster bridge edge. */ + interEdgeProb: number; + datasetKey: string; + sourceRevision: number | string; +} + +export const DEFAULT_GENERATE = { + nodes: 3000, + clusters: 6, + intraEdgeFactor: 1.6, + interEdgeProb: 0.06, +} as const; + +/** Classic mulberry32 PRNG — 32-bit state, deterministic across platforms. */ +export function mulberry32(seed: number): () => number { + let a = seed >>> 0; + return () => { + a = (a + 0x6d2b79f5) | 0; + let t = Math.imul(a ^ (a >>> 15), 1 | a); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +/** Deterministic LCG step used by the UI to advance to a fresh seed. */ +export function nextSeed(seed: number): number { + return (Math.imul(seed, 1664525) + 1013904223) >>> 0; +} + +/** Independent per-node stream seed so edge draws never depend on node count. */ +function nodeStreamSeed(seed: number, index: number): number { + let h = (seed ^ Math.imul(index + 1, 0x9e3779b9)) >>> 0; + h = Math.imul(h ^ (h >>> 16), 0x45d9f3b) >>> 0; + return (h ^ (h >>> 16)) >>> 0; +} + +const CLUSTER_NAMES = ['alpha', 'bravo', 'coral', 'delta', 'ember', 'fjord', 'gale', 'helix']; + +export function clusterName(cluster: number): string { + return CLUSTER_NAMES[cluster % CLUSTER_NAMES.length] ?? `k${cluster}`; +} + +const idOf = (i: number): string => `n${i}`; + +// --- v0.7 metric attrs for the filtering demo ------------------------------- + +const DAY_MS = 86_400_000; +/** Fixed window end (2026-06-30T00:00:00Z) — deterministic, never Date.now. */ +export const CREATED_AT_END_MS = Date.UTC(2026, 5, 30); +export const CREATED_AT_SPAN_DAYS = 180; +export const CREATED_AT_START_MS = CREATED_AT_END_MS - CREATED_AT_SPAN_DAYS * DAY_MS; +/** Width of each cluster's createdAt window (overlapping phases). */ +const CREATED_AT_PHASE_DAYS = 60; + +/** + * Deterministic per-node metric attrs, on an INDEPENDENT rng stream (salted + * node-stream seed) so edge draws stay byte-identical to pre-v0.7 output and + * the superset invariant is untouched — metrics depend only on + * (seed, index, cluster, clusters). + * + * `score`: cluster-correlated pseudo-normal (sum of three uniforms) around a + * cluster mean spread across ~15..85, clamped to [0, 100]. + * `createdAt`: cluster-PHASED across the 180-day window — cluster k's window + * starts at k · (span − phase)/(clusters − 1) days — so a timeline sweep + * lights the clusters up in order, with overlap. + */ +export function nodeMetrics( + seed: number, + index: number, + cluster: number, + clusters: number, +): { score: number; createdAt: number } { + const rng = mulberry32(nodeStreamSeed(seed ^ 0x5f356495, index)); + const spread = Math.max(1, clusters - 1); + const mean = 15 + (70 * (cluster % clusters)) / spread; + const noise = (rng() + rng() + rng() - 1.5) * 12; + const score = Math.round(Math.min(100, Math.max(0, mean + noise)) * 100) / 100; + const phaseDays = ((cluster % clusters) * (CREATED_AT_SPAN_DAYS - CREATED_AT_PHASE_DAYS)) / spread; + const createdAt = Math.round( + CREATED_AT_START_MS + (phaseDays + rng() * CREATED_AT_PHASE_DAYS) * DAY_MS, + ); + return { score, createdAt }; +} + +export function generateGraph(p: GenerateParams): DemoSnapshot { + const { seed, nodes: n, clusters, intraEdgeFactor, interEdgeProb } = p; + const edges: GraphEdge[] = []; + const degree: number[] = new Array(n).fill(0); + + for (let i = 0; i < n; i++) { + const rng = mulberry32(nodeStreamSeed(seed, i)); + const cluster = i % clusters; + + // Intra-cluster edges to earlier nodes of the same cluster (cluster = i % clusters, + // so earlier peers are exactly i - clusters*m for m in [1.. floor(i/clusters)]). + const earlierPeers = Math.floor(i / clusters); + if (earlierPeers > 0) { + const frac = intraEdgeFactor % 1; + const k = Math.max(1, Math.floor(intraEdgeFactor) + (rng() < frac ? 1 : 0)); + const chosen = new Set(); + for (let e = 0; e < k; e++) { + const m = 1 + Math.floor(rng() * earlierPeers); + const j = i - clusters * m; + if (chosen.has(j)) continue; + chosen.add(j); + edges.push({ source: idOf(j), target: idOf(i), attrs: { kind: 'intra' } }); + degree[j] = (degree[j] ?? 0) + 1; + degree[i] = (degree[i] ?? 0) + 1; + } + } + + // Occasional inter-cluster bridge to an earlier node of a different cluster. + if (i > 0 && rng() < interEdgeProb) { + for (let attempt = 0; attempt < 8; attempt++) { + const j = Math.floor(rng() * i); + if (j % clusters !== cluster) { + edges.push({ source: idOf(j), target: idOf(i), attrs: { kind: 'inter' } }); + degree[j] = (degree[j] ?? 0) + 1; + degree[i] = (degree[i] ?? 0) + 1; + break; + } + } + } + } + + const nodes: GraphNode[] = new Array(n); + for (let i = 0; i < n; i++) { + const cluster = i % clusters; + const { score, createdAt } = nodeMetrics(seed, i, cluster, clusters); + nodes[i] = { + id: idOf(i), + attrs: { + cluster, + label: `${clusterName(cluster)}-${String(i).padStart(4, '0')}`, + degree: degree[i] ?? 0, + score, + createdAt, + }, + }; + } + + return { + datasetKey: p.datasetKey, + sourceRevision: p.sourceRevision, + nodes, + edges, + }; +} diff --git a/apps/storybook/src/fixtures/index.ts b/apps/storybook/src/fixtures/index.ts new file mode 100644 index 0000000..4f40b51 --- /dev/null +++ b/apps/storybook/src/fixtures/index.ts @@ -0,0 +1,7 @@ +export * from './generate'; +export * from './themes'; +export * from './scales'; +export * from './config'; +export * from './positions'; +export * from './engines'; +export * from './DemoGraph'; diff --git a/apps/storybook/src/fixtures/positions.ts b/apps/storybook/src/fixtures/positions.ts new file mode 100644 index 0000000..7f48f7c --- /dev/null +++ b/apps/storybook/src/fixtures/positions.ts @@ -0,0 +1,26 @@ +/** + * Copied from apps/demo/src/App.tsx (minimap seed positions) — keep in sync + * by hand. Declares a deterministic golden-angle sunflower position for every + * position-less node: the minimap's CPU fallback rasterizes DECLARED + * coordinates, and under the force layout the declaration doubles as the + * simulation's initial placement (unchanged declarations defer to live sim + * positions on later revisions). + */ + +import type { GraphSnapshot } from '@modernrelay/orbit-core'; + +const GOLDEN_ANGLE = 2.399963229728653; +/** Cosmos space is [0, 4096] with ring seeding at r=1024 around the center — + * the sunflower disc matches that envelope. */ +const SEED_CENTER = 2048; +const SEED_RADIUS_STEP = 17; // r = 17·√i → ≈1005 at i=3500 + +export function seedSnapshotPositions(snap: GraphSnapshot): GraphSnapshot { + const nodes = snap.nodes.map((node, i) => { + if (node.x !== undefined && node.y !== undefined) return node; + const r = SEED_RADIUS_STEP * Math.sqrt(i); + const a = i * GOLDEN_ANGLE; + return { ...node, x: SEED_CENTER + Math.cos(a) * r, y: SEED_CENTER + Math.sin(a) * r }; + }); + return { ...snap, nodes }; +} diff --git a/apps/storybook/src/fixtures/scales.ts b/apps/storybook/src/fixtures/scales.ts new file mode 100644 index 0000000..183fc14 --- /dev/null +++ b/apps/storybook/src/fixtures/scales.ts @@ -0,0 +1,57 @@ +/** + * Copied from apps/demo (styles.ts palette + App.tsx scale descriptors), + * simplified to DemoNodeAttrs — keep in sync by hand. Scale descriptors are + * module-scope so their identities stay trivially stable across renders. + */ + +import type { GraphNode, Scale } from '@modernrelay/orbit-core'; +import { DEFAULT_GENERATE } from './generate'; +import type { DemoNodeAttrs } from './generate'; + +export const CLUSTER_PALETTE = [ + '#58a6ff', // blue + '#f77f5f', // coral + '#3fb950', // green + '#d2a8ff', // lavender + '#f2cc60', // gold + '#39c5cf', // teal +] as const; + +export function clusterColor(cluster: number): string { + return CLUSTER_PALETTE[ + ((cluster % CLUSTER_PALETTE.length) + CLUSTER_PALETTE.length) % CLUSTER_PALETTE.length + ]!; +} + +/** degree ramp: sequential blue→amber over the degree metric. */ +export const DEGREE_COLOR_SCALE: Scale = { + kind: 'sequential', + metric: 'degree', + range: ['#3b82f6', '#f59e0b'], +}; + +/** degree size ramp: sequential 2..14px over the degree metric. */ +export const DEGREE_SIZE_SCALE: Scale = { + kind: 'sequential', + metric: 'degree', + range: [2, 14], +}; + +const CLUSTER_DOMAIN: readonly string[] = Array.from( + { length: DEFAULT_GENERATE.clusters }, + (_, c) => String(c), +); + +const clusterByString = (node: GraphNode): string | null => { + const a = node.attrs; + return a !== undefined ? String(a.cluster) : null; +}; + +/** cluster-as-string categorical scale; fixed domain order keeps colors and + * legend rows stable. */ +export const CLUSTER_COLOR_SCALE: Scale = { + kind: 'categorical', + by: clusterByString, + domain: CLUSTER_DOMAIN, + palette: CLUSTER_DOMAIN.map((v) => clusterColor(Number(v))), +}; diff --git a/apps/storybook/src/fixtures/themes.ts b/apps/storybook/src/fixtures/themes.ts new file mode 100644 index 0000000..e4722cd --- /dev/null +++ b/apps/storybook/src/fixtures/themes.ts @@ -0,0 +1,31 @@ +/** + * Copied from apps/demo (styles.ts + App.tsx theme constants) — keep in sync + * by hand. Theme inputs are partial-over-base: only the stated tokens differ + * from orbit's built-in dark/light bases. + */ + +import type { ThemeInput } from '@modernrelay/orbit-core'; + +export const BACKGROUND = '#0b0e14'; +export const LIGHT_BACKGROUND = '#f6f8fa'; + +export const DARK_THEME: ThemeInput = { base: 'dark', background: BACKGROUND }; +export const LIGHT_THEME: ThemeInput = { base: 'light', background: LIGHT_BACKGROUND }; + +export const LINK_COLOR = 'rgba(255,255,255,0.15)'; +export const LINK_COLOR_LIGHT = 'rgba(0,0,0,0.15)'; + +export interface ActiveTheme { + base: 'dark' | 'light'; + theme: ThemeInput; + background: string; + linkColor: string; +} + +/** Resolve the Storybook global toolbar value into orbit theme pieces. */ +export function themeFromGlobals(globals: Record): ActiveTheme { + const base = globals['theme'] === 'light' ? 'light' : 'dark'; + return base === 'light' + ? { base, theme: LIGHT_THEME, background: LIGHT_BACKGROUND, linkColor: LINK_COLOR_LIGHT } + : { base, theme: DARK_THEME, background: BACKGROUND, linkColor: LINK_COLOR }; +} diff --git a/apps/storybook/src/graph/LayoutForce.stories.tsx b/apps/storybook/src/graph/LayoutForce.stories.tsx new file mode 100644 index 0000000..26b0ee9 --- /dev/null +++ b/apps/storybook/src/graph/LayoutForce.stories.tsx @@ -0,0 +1,62 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { DemoGraph, GraphFrame } from '../fixtures/DemoGraph'; +import { cosmosEngine } from '../fixtures/engines'; +import { generateGraph } from '../fixtures/generate'; +import { themeFromGlobals } from '../fixtures/themes'; + +const data = generateGraph({ + seed: 21, + nodes: 1500, + clusters: 6, + intraEdgeFactor: 1.6, + interEdgeProb: 0.06, + datasetKey: 'layout-force', + sourceRevision: 1, +}); + +interface ForceArgs { + repulsion: number; + gravity: number; +} + +const meta = { + title: 'Graph/Layout: force', + parameters: { + docs: { + description: { + component: + 'The GPU force layout runs in the engine; the simulation prop exposes live ' + + 'tunables. Move the sliders — each change re-heats the running simulation.', + }, + }, + }, + args: { + repulsion: 0.6, + gravity: 0.25, + }, + argTypes: { + repulsion: { control: { type: 'range', min: 0, max: 2, step: 0.05 } }, + gravity: { control: { type: 'range', min: 0, max: 1, step: 0.05 } }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Force: Story = { + render: (args, { globals }) => { + const active = themeFromGlobals(globals); + return ( + + + + ); + }, +}; diff --git a/apps/storybook/src/graph/Minimal.stories.tsx b/apps/storybook/src/graph/Minimal.stories.tsx new file mode 100644 index 0000000..4e86458 --- /dev/null +++ b/apps/storybook/src/graph/Minimal.stories.tsx @@ -0,0 +1,57 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { DemoGraph, GraphFrame } from '../fixtures/DemoGraph'; +import { cosmosEngine } from '../fixtures/engines'; +import { generateGraph } from '../fixtures/generate'; +import { themeFromGlobals } from '../fixtures/themes'; + +const data = generateGraph({ + seed: 7, + nodes: 300, + clusters: 4, + intraEdgeFactor: 1.6, + interEdgeProb: 0.06, + datasetKey: 'minimal', + sourceRevision: 1, +}); + +const meta = { + title: 'Graph/Minimal', + component: DemoGraph, + parameters: { + docs: { + description: { + component: + 'The smallest possible orbit graph: an engine factory and a data snapshot. ' + + 'Everything else — layout, theme, interaction — is defaults.', + }, + source: { + code: `import { Graph } from '@modernrelay/orbit-react'; +import { CosmosEngine } from '@modernrelay/orbit-engine-cosmos'; + + new CosmosEngine()} data={snapshot} />`, + }, + }, + }, + argTypes: { + engine: { table: { disable: true } }, + data: { table: { disable: true } }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Minimal: Story = { + args: { + engine: cosmosEngine, + data, + }, + render: (args, { globals }) => { + const active = themeFromGlobals(globals); + return ( + + + + ); + }, +}; diff --git a/apps/storybook/src/graph/Styling.stories.tsx b/apps/storybook/src/graph/Styling.stories.tsx new file mode 100644 index 0000000..6fe03e1 --- /dev/null +++ b/apps/storybook/src/graph/Styling.stories.tsx @@ -0,0 +1,82 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { fn } from 'storybook/test'; +import { DemoGraph, GraphFrame } from '../fixtures/DemoGraph'; +import { cosmosEngine } from '../fixtures/engines'; +import { generateGraph } from '../fixtures/generate'; +import type { DemoNodeAttrs } from '../fixtures/generate'; +import { CLUSTER_COLOR_SCALE, DEGREE_COLOR_SCALE, DEGREE_SIZE_SCALE } from '../fixtures/scales'; +import { themeFromGlobals } from '../fixtures/themes'; + +const data = generateGraph({ + seed: 11, + nodes: 800, + clusters: 6, + intraEdgeFactor: 1.6, + interEdgeProb: 0.06, + datasetKey: 'styling', + sourceRevision: 1, +}); + +const UNIFORM_SIZE = (_node: { attrs?: DemoNodeAttrs }): number => 4; + +interface StylingArgs { + colorBy: 'cluster' | 'degree'; + sizeBy: 'uniform' | 'degree'; + edgeArrows: boolean; + showLinks: boolean; + onNodeClick: ReturnType; + onBackgroundClick: ReturnType; +} + +const meta = { + title: 'Graph/Styling', + parameters: { + docs: { + description: { + component: + 'Node color and size take either a plain per-node function or a declarative ' + + 'Scale descriptor (categorical or sequential over a metric). Scales also feed ' + + 'the legend. Link color/width, arrows, and link visibility are props too.', + }, + }, + }, + args: { + colorBy: 'cluster', + sizeBy: 'degree', + edgeArrows: false, + showLinks: true, + onNodeClick: fn(), + onBackgroundClick: fn(), + }, + argTypes: { + colorBy: { control: 'radio', options: ['cluster', 'degree'] }, + sizeBy: { control: 'radio', options: ['uniform', 'degree'] }, + onNodeClick: { table: { disable: true } }, + onBackgroundClick: { table: { disable: true } }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Styling: Story = { + render: (args, { globals }) => { + const active = themeFromGlobals(globals); + return ( + + + + ); + }, +}; diff --git a/apps/storybook/src/graph/Themes.stories.tsx b/apps/storybook/src/graph/Themes.stories.tsx new file mode 100644 index 0000000..6680772 --- /dev/null +++ b/apps/storybook/src/graph/Themes.stories.tsx @@ -0,0 +1,66 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { DemoGraph, GraphFrame } from '../fixtures/DemoGraph'; +import { cosmosEngine } from '../fixtures/engines'; +import { generateGraph } from '../fixtures/generate'; +import { + BACKGROUND, + DARK_THEME, + LIGHT_BACKGROUND, + LIGHT_THEME, + LINK_COLOR, + LINK_COLOR_LIGHT, +} from '../fixtures/themes'; + +const data = generateGraph({ + seed: 17, + nodes: 800, + clusters: 6, + intraEdgeFactor: 1.6, + interEdgeProb: 0.06, + datasetKey: 'themes', + sourceRevision: 1, +}); + +interface ThemesArgs { + base: 'dark' | 'light'; +} + +const meta = { + title: 'Graph/Themes', + parameters: { + docs: { + description: { + component: + 'Themes are partial-over-base inputs: pick the dark or light base and override ' + + 'only the tokens that differ (here: the canvas background). Swapping the theme ' + + 'prop restyles the live scene — no remount. This story drives the theme with ' + + 'its own control and ignores the global toolbar.', + }, + }, + }, + args: { + base: 'dark', + }, + argTypes: { + base: { control: 'radio', options: ['dark', 'light'] }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Themes: Story = { + render: (args) => { + const dark = args.base === 'dark'; + return ( + + + + ); + }, +}; diff --git a/apps/storybook/src/interaction/Selection.stories.tsx b/apps/storybook/src/interaction/Selection.stories.tsx new file mode 100644 index 0000000..b77de55 --- /dev/null +++ b/apps/storybook/src/interaction/Selection.stories.tsx @@ -0,0 +1,72 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { fn } from 'storybook/test'; +import { DemoGraph, GraphFrame } from '../fixtures/DemoGraph'; +import { cosmosEngine } from '../fixtures/engines'; +import { generateGraph } from '../fixtures/generate'; +import { themeFromGlobals } from '../fixtures/themes'; + +const data = generateGraph({ + seed: 33, + nodes: 600, + clusters: 5, + intraEdgeFactor: 1.6, + interEdgeProb: 0.06, + datasetKey: 'selection', + sourceRevision: 1, +}); + +interface SelectionArgs { + enableLasso: boolean; + onSelectionChange: ReturnType; + onNodeClick: ReturnType; + onBackgroundClick: ReturnType; +} + +const meta = { + title: 'Interaction/Selection', + parameters: { + docs: { + description: { + component: + 'Click a node to select it; meta-click accumulates; click the background to ' + + 'clear. With lasso enabled, shift-drag draws a polygon selection. Selection ' + + 'changes stream into the Actions panel via onSelectionChange. A host can also ' + + 'control selection fully through the `selection` prop (not shown here).', + }, + }, + }, + args: { + enableLasso: true, + onSelectionChange: fn(), + onNodeClick: fn(), + onBackgroundClick: fn(), + }, + argTypes: { + onSelectionChange: { table: { disable: true } }, + onNodeClick: { table: { disable: true } }, + onBackgroundClick: { table: { disable: true } }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Selection: Story = { + render: (args, { globals }) => { + const active = themeFromGlobals(globals); + return ( + + + + ); + }, +}; diff --git a/apps/storybook/src/introduction/DataShapes.mdx b/apps/storybook/src/introduction/DataShapes.mdx new file mode 100644 index 0000000..df1e36a --- /dev/null +++ b/apps/storybook/src/introduction/DataShapes.mdx @@ -0,0 +1,45 @@ +import { Meta } from '@storybook/addon-docs/blocks'; + + + +# Data shapes + +## The snapshot + +A graph is a plain object: identity fields plus nodes and edges. + +```ts +const snapshot: GraphSnapshot = { + datasetKey: 'my-dataset', // which dataset this is + sourceRevision: 1, // which version of it + nodes: [{ id: 'a' }, { id: 'b', attrs: { label: 'B' } }], + edges: [{ source: 'a', target: 'b' }], +}; +``` + +- `attrs` is your typed payload — `GraphSnapshot` carries your node and edge + attribute types end to end, into every accessor, dimension, and callback. +- Node `x`/`y` are optional declared positions: the fixed layout renders them exactly, + and under the force layout they seed the simulation's starting placement. +- Edges may carry an optional stable `id`; without one, orbit synthesizes a + deterministic id from the endpoints. +- Malformed rows never throw mid-render — they are excluded with batched diagnostics. + +## Updates are diffs, not rebuilds + +Passing a new snapshot with the same `datasetKey` updates the live scene in place: +appended nodes join the running simulation, attribute-only changes restyle without a +relayout, and node identity (by `id`) is preserved across revisions. + +## Beyond object arrays + +- **Columnar snapshots** (typed arrays, transferable buffers) for large graphs. +- **Streaming ingest** for feeding a graph in batches with atomic commit. +- **Worker execution** (`execution="auto"`) moves columnar acceptance off the main thread. + +## The story fixtures + +Every story in this catalog uses one seeded, deterministic generator (clustered +topology with per-node `cluster`, `label`, `degree`, `score`, and `createdAt` attrs). +Same seed, same graph — stories are reproducible, and the metric attrs give the +analytics stories real distributions to work with. diff --git a/apps/storybook/src/introduction/TestingWithoutWebGL.mdx b/apps/storybook/src/introduction/TestingWithoutWebGL.mdx new file mode 100644 index 0000000..98918f7 --- /dev/null +++ b/apps/storybook/src/introduction/TestingWithoutWebGL.mdx @@ -0,0 +1,51 @@ +import { Meta } from '@storybook/addon-docs/blocks'; + + + +# Testing without WebGL + +Orbit ships a headless engine double, so integration tests run in plain jsdom — no GPU, +no browser, no flakes. + +```ts +import { FakeEngine } from '@modernrelay/orbit-core/testing'; +``` + +## Mounting a graph on the double + +```tsx +import { render, act } from '@testing-library/react'; +import { Graph } from '@modernrelay/orbit-react'; +import { FakeEngine } from '@modernrelay/orbit-core/testing'; + +test('selection follows a click', async () => { + const fake = new FakeEngine(); + render( + fake} data={snapshot} onSelectionChange={onSelectionChange} />, + ); + await act(async () => {}); + await act(async () => {}); // second pass drains engine readiness + + fake.injectPointClick(0); // click node at index 0 — no DOM gymnastics + expect(onSelectionChange).toHaveBeenCalled(); +}); +``` + +## What the double gives you + +- **Deterministic positions** — declared `x`/`y` pass through verbatim; undeclared + nodes land on a fixed grid. Labels, exports, and hit tests all work. +- **Event injection** — `injectPointClick`, `injectPointHover`, `injectLinkClick`, + `injectLinkHover`, `injectDragStart` / `injectDragEnd`, `injectContextMenu`, + `injectViewportChange`, `injectSimulationEnd`, plus error and context-loss paths. +- **Inspection** — every engine call is recorded: commits, camera moves, selection + pushes, per-channel buffers (`lastBuffer('pointColor')`), pinned indices. +- **Options** — `new FakeEngine({ capabilities, screenshot, manualFrames })`: + override capability flags (for example `linkPicking: true` to enable edge hover), + supply a screenshot blob, or queue commits and step them manually. + +## In this catalog + +The Components section mounts every packaged UI component on the double — a headless +canvas with fully live component behavior. What you see there is exactly what your +tests can drive. diff --git a/apps/storybook/src/introduction/WhatIsOrbit.mdx b/apps/storybook/src/introduction/WhatIsOrbit.mdx new file mode 100644 index 0000000..1c96275 --- /dev/null +++ b/apps/storybook/src/introduction/WhatIsOrbit.mdx @@ -0,0 +1,49 @@ +import { Meta } from '@storybook/addon-docs/blocks'; + + + +# orbit + +A typed, declarative React library for rendering and exploring graphs with WebGL. +Handles 100K+ node graphs. UI components included: search, tables, histograms, and more. + +## The mental model + +The graph is a prop. You hand `` a data snapshot and an engine factory; orbit +validates, projects, and renders it, and every later change is just another prop value: + +```tsx +import { Graph } from '@modernrelay/orbit-react'; +import { CosmosEngine } from '@modernrelay/orbit-engine-cosmos'; + + new CosmosEngine()} data={snapshot} /> +``` + +- **The engine is a factory** — one engine instance per mounted graph. `CosmosEngine` + renders with WebGL and runs the GPU force simulation; `FakeEngine` (from + `@modernrelay/orbit-core/testing`) is a headless double for tests and component demos. +- **UI components are children.** Search, toolbar, minimap, histograms, tables, and the + rest mount inside `` and find the live instance through context: + +```tsx + + + + + +``` + +- **Styling is data-driven.** Node color and size accept per-node functions or + declarative scale descriptors; scales also feed the legend. + +## Packages + +| Package | What it is | +|---|---| +| `@modernrelay/orbit-react` | ``, the UI components, hooks | +| `@modernrelay/orbit-core` | engine-agnostic instance: data, exploration, analytics | +| `@modernrelay/orbit-engine-cosmos` | the default WebGL rendering engine | +| `@modernrelay/orbit-data` | columnar snapshot helpers | +| `@modernrelay/orbit-omnigraph` | Omnigraph server adapter | + +Source and issues: [github.com/ModernRelay/orbit](https://github.com/ModernRelay/orbit) diff --git a/apps/storybook/tsconfig.json b/apps/storybook/tsconfig.json new file mode 100644 index 0000000..40fbdbc --- /dev/null +++ b/apps/storybook/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx" + }, + "include": ["src", ".storybook", "vite.config.ts"] +} diff --git a/apps/storybook/vite.config.ts b/apps/storybook/vite.config.ts new file mode 100644 index 0000000..6a500bc --- /dev/null +++ b/apps/storybook/vite.config.ts @@ -0,0 +1,9 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +// The Storybook CLI owns the dev server (port/open); builder-vite auto-merges +// this config. If a dual-React "invalid hook call" ever appears after version +// drift, add `resolve: { dedupe: ['react', 'react-dom'] }` here. +export default defineConfig({ + plugins: [react()], +}); diff --git a/eslint.config.mjs b/eslint.config.mjs index fc2b550..cc82bed 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -8,6 +8,7 @@ export default tseslint.config( 'apps/demo/.vite/**', '.smoke/**', 'apps/spike/results/**', + 'apps/storybook/storybook-static/**', '.evidence/**', ], }, diff --git a/package.json b/package.json index 6bbd464..be117a6 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "check": "pnpm run boundaries && pnpm run anchors && pnpm run cosmos:pin-check && pnpm run lint && pnpm run typecheck && pnpm run test && pnpm run perf:gate", "demo:dev": "pnpm --filter orbit-demo dev", "demo:build": "pnpm --filter orbit-demo build", + "storybook:dev": "pnpm --filter orbit-storybook dev", + "storybook:build": "pnpm --filter orbit-storybook build", "version": "changeset version && pnpm install --lockfile-only", "release": "pnpm run build && pnpm run smoke && changeset publish", "svg:node-fixture": "node scripts/svg-node-fixture.mjs" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7e028f..353f7e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,11 +101,54 @@ importers: specifier: ^6.0.0 version: 6.4.3(@types/node@25.9.5) + apps/storybook: + dependencies: + '@modernrelay/orbit-core': + specifier: workspace:* + version: link:../../packages/core + '@modernrelay/orbit-engine-cosmos': + specifier: workspace:* + version: link:../../packages/engine-cosmos + '@modernrelay/orbit-react': + specifier: workspace:* + version: link:../../packages/react + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + devDependencies: + '@storybook/addon-docs': + specifier: 10.5.10 + version: 10.5.10(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(esbuild@0.28.1)(rollup@4.62.2)(storybook@10.5.10(@types/react@18.3.31)(prettier@2.8.8)(react@18.3.1))(vite@6.4.3(@types/node@25.9.5)) + '@storybook/react-vite': + specifier: 10.5.10 + version: 10.5.10(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(esbuild@0.28.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.62.2)(storybook@10.5.10(@types/react@18.3.31)(prettier@2.8.8)(react@18.3.1))(typescript@5.9.3)(vite@6.4.3(@types/node@25.9.5)) + '@types/react': + specifier: ^18.3.0 + version: 18.3.31 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.31) + '@vitejs/plugin-react': + specifier: ^4.3.0 + version: 4.7.0(vite@6.4.3(@types/node@25.9.5)) + storybook: + specifier: 10.5.10 + version: 10.5.10(@types/react@18.3.31)(prettier@2.8.8)(react@18.3.1) + typescript: + specifier: ^5.6.0 + version: 5.9.3 + vite: + specifier: ^6.0.0 + version: 6.4.3(@types/node@25.9.5) + packages/core: dependencies: zustand: specifier: ^5.0.0 - version: 5.0.14(@types/react@18.3.31)(react@18.3.1) + version: 5.0.14(@types/react@18.3.31)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) devDependencies: typescript: specifier: ^5.6.0 @@ -198,6 +241,9 @@ importers: packages: + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + '@andrewbranch/untar.js@1.0.3': resolution: {integrity: sha512-Jh15/qVmrLGhkKJBdXlK1+9tY4lZruYjsgkDFj08ZmDiWVBLJcqkok7Z0/R0In+i1rScBpJlSvrTS2Lm41Pbnw==} @@ -399,6 +445,24 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} + '@emnapi/core@1.11.0': + resolution: {integrity: sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==} + + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + + '@emnapi/runtime@1.11.0': + resolution: {integrity: sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==} + + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -926,6 +990,15 @@ packages: '@types/node': optional: true + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0': + resolution: {integrity: sha512-qvsTEwEFefhdirGOPnu9Wp6ChfIwy2dBCRuETU3uE+4cC+PFoxMSiiEhxk4lOluA34eARHA0OxqsEUYDqRMgeQ==} + peerDependencies: + typescript: '>= 4.3.x' + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -976,10 +1049,23 @@ packages: '@math.gl/types@4.1.0': resolution: {integrity: sha512-clYZdHcmRvMzVK5fjeDkQlHUzXQSNdZ7s4xOqC3nJPgz4C/TZkUecTo9YS4PruZqtDda/ag4erndP0MIn40dGA==} + '@mdx-js/react@3.1.1': + resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==} + peerDependencies: + '@types/react': '>=16' + react: '>=16' + '@modernrelay/omnigraph@0.9.0': resolution: {integrity: sha512-3FinbZ2knvqj3nnI/Ot/f+8lceQjg0GonLdOVswMSPY3XmXvJ5lDgRo97TO+RcI5ArPesFFNmrSoSL3U6OhKSg==} engines: {node: '>=22'} + '@napi-rs/wasm-runtime@1.2.3': + resolution: {integrity: sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -992,6 +1078,239 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@oxc-parser/binding-android-arm-eabi@0.127.0': + resolution: {integrity: sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.127.0': + resolution: {integrity: sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.127.0': + resolution: {integrity: sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.127.0': + resolution: {integrity: sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.127.0': + resolution: {integrity: sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + resolution: {integrity: sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + resolution: {integrity: sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + resolution: {integrity: sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + resolution: {integrity: sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + resolution: {integrity: sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + resolution: {integrity: sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + resolution: {integrity: sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + resolution: {integrity: sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + resolution: {integrity: sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + resolution: {integrity: sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + resolution: {integrity: sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + resolution: {integrity: sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + resolution: {integrity: sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + resolution: {integrity: sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + resolution: {integrity: sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + + '@oxc-resolver/binding-android-arm-eabi@11.21.2': + resolution: {integrity: sha512-xQoCRv+gKax9KTdwdaQNnAFOai8neay7g3jExDIORzhbrejwGSJaZNTdOJHR5ziLg2joMxOCMOFMo4zuxza2uQ==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.21.2': + resolution: {integrity: sha512-HF5oiE2L05yInPYCFD/4uxSrEZW4SuIfn99Y6L1xnJnzl066JR+MJs2rIdstw8A2MPlAKH+13dpFPNycjqzvGg==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.21.2': + resolution: {integrity: sha512-UX4u49CVCAD8QZNELaW8eMGgMAGwFWYEPbvNsh+3r/gs4NX3KfpiACMVwRQT0EuH3uat9hM5Zl+Ppm9pJD8tgg==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.21.2': + resolution: {integrity: sha512-J9xPx7YBkrRmJ+xl561ztnMWEc1aOyjEIxBiGX1dVb3u7bGSnfObfcZk+Pd+uM0HZAPNsQ1xvD8j52A/uOSqNQ==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.21.2': + resolution: {integrity: sha512-fRlt7OvSaQkWj6+EDTVxawVxOlqJB2QSnBfkeCyK4RTvsGctbw3BiH2Tb7DzMs7bikc4BRBpvWP5zF9K8b54Zg==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.21.2': + resolution: {integrity: sha512-gK+vPUcPQITkGwBKpZGrcDHSlU6eDGl7AQacxS2CEKAZIBHWkOVFeJwLZ4tYnA1acJqRM5lt7yYwPCVqGHIJ7A==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.21.2': + resolution: {integrity: sha512-L/Rgas7SrOKy/z7IH+HxSFRqVO4PuLDKLEGKvnhoCBBq3UJ0YzGBou3qMzaAGgkJwsIvX2pBF+7ojzfUhLiZxQ==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.21.2': + resolution: {integrity: sha512-CUEYvlX1Fk7E9kUMzuswru1J9HLxMwnpDeQGjQuI4ZH+iNCoa2X9T+pvyzrbsgl7WnIeTFTlNlHsRfVdf5g9/g==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-arm64-musl@11.21.2': + resolution: {integrity: sha512-ViN1ZibQyxwC67GpoP33oo+S9UyUnkog13vzQb9+v9bCNvrVzJuk0MRdacjtv/9xfRMVF/eqHFyl8YOeykI10Q==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.21.2': + resolution: {integrity: sha512-CU1sCqWnhGqYD5I1HedHk5pujrn7ssDkNB/AEQd/pd3D/EojVSgJUlpbafwIbyhia3PgIfkvdFpRPWSALzVumA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.21.2': + resolution: {integrity: sha512-jWVyZtIHca4Gb96x7dag+y69vlei7ffjrsveLkmf2ZhqEAz6ZSBnY1GWvgZUaZlwZP62A3xD092BW97Q5VGc+g==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-musl@11.21.2': + resolution: {integrity: sha512-LF29obFqNgBUgDX7rmUK7M4D0JQG5LxhYzn3xXmECcHU9aQAdWG7NiY052qybtesEdwHQXKNTWYQ7mTsybNvWg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-s390x-gnu@11.21.2': + resolution: {integrity: sha512-+NYcm+cCHBbtdQQ3A4phQTSuVRYnNHz7wrl9XRAPEovcdoqi0mb1K5ZOl+jN54ZD+q1zz3V0vltbFJmzecJKmw==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-gnu@11.21.2': + resolution: {integrity: sha512-UQqZDdG2r2HhAOsZEgufkIWHPQ886IUyuJQkoZByvzhW8j51R4UNzGBJFkTiTnLhQnggwJRdJgFWK4uY6ZVIMw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-musl@11.21.2': + resolution: {integrity: sha512-3Q9PMRjalWkT6NZ4jfujuqTCFwoWErg3y3BnOgb544B8IMw4PktiwWOigMfOHNRLMghZeJ7hpfpZf4CP7rV7Og==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-openharmony-arm64@11.21.2': + resolution: {integrity: sha512-Eljeq3ndtyKhM+Es8LITi4Zl2htzuRZcrPMF3kMCsrILztvU6AjZ3FuEhHHKobPUB9rMpzLpJP5bDqXI7r+iog==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.21.2': + resolution: {integrity: sha512-HGDbNsIywqc4LxU38+CTJNnB/6BF7rheWOJ259b4eE0aEnelYblC8x+1tEd63bp31fYne63RQz9Jb4yVnC7Yig==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.21.2': + resolution: {integrity: sha512-iWx25CBEgH49iE9q5coEGI/jb1jl5kkCY9z6U5Og67xCkQ/WFMDc2J5U78+AE91SUxM2NqSLhJC8/PLfWnImww==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.21.2': + resolution: {integrity: sha512-VPoCAhKvCQTlG7vxqaBXcmuvbh77BfnGXj8g0pbvVXpm1F/R8rDVwqIWcEbMzrI1JvJlm8v7T9uMVQb6UctMRg==} + cpu: [x64] + os: [win32] + '@playwright/test@1.61.1': resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==} engines: {node: '>=18'} @@ -1013,6 +1332,15 @@ packages: '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rollup/pluginutils@5.4.0': + resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/rollup-android-arm-eabi@4.62.2': resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} cpu: [arm] @@ -1155,10 +1483,98 @@ packages: resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} + '@storybook/addon-docs@10.5.10': + resolution: {integrity: sha512-06JoK3/a7FWI/6GzuidJP9iHp1/Vejboe6lzS1jW+d8ItpecriBt+oXh1VNmUM7i7PjI6pZnet+j51QnLyeOoQ==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.5.10 + peerDependenciesMeta: + '@types/react': + optional: true + + '@storybook/builder-vite@10.5.10': + resolution: {integrity: sha512-O4GgIP0tKLRueom3EmU3OaBUHKjNYj+jkOvmTIkn3PYTiWVkCuHqSKEs4ADvRyaQuLH+peHhFe4JtkNC9KbtrQ==} + peerDependencies: + storybook: ^10.5.10 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@storybook/csf-plugin@10.5.10': + resolution: {integrity: sha512-TaCLBrqVEr767+w58QDotDUiCTuE5cyRJuRcDlsKQyUIyGBv+lYD3lu8wBiVCYxgIjB/gu9HmqiC+0yx1rHzaw==} + peerDependencies: + esbuild: '*' + rollup: '*' + storybook: ^10.5.10 + vite: '*' + webpack: '*' + peerDependenciesMeta: + esbuild: + optional: true + rollup: + optional: true + vite: + optional: true + webpack: + optional: true + + '@storybook/global@5.0.0': + resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} + + '@storybook/icons@2.1.0': + resolution: {integrity: sha512-Fxh9vYpX9bQqFeHRiY8h2ApeRGDzRSMLwJwNZ/AIRqnyOKHxRKL+yFe+ctEkVJmuptRE9u1Hrn8ZZNHyfDKKNg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@storybook/react-dom-shim@10.5.10': + resolution: {integrity: sha512-rbu62ILo/VE3iXKmu+kWXFpD1H1Lwi0f19q/x7JnDsD2dxKS9w5znLEqPIq2qxpzi/wjjIb2iUP1cRG1d/9W5A==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.5.10 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@storybook/react-vite@10.5.10': + resolution: {integrity: sha512-xOztxefUnqKeuyvcnjspqmlDnER4cExL+liltrpdXLPJVqfFNr9lgM49FyEPajzsUVG9W/vHJWjbaQGGu1UsYQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.5.10 + typescript: '>= 4.9.x' + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@storybook/react@10.5.10': + resolution: {integrity: sha512-4MBV5e1SXIMfPynLHzr+Mp0dwGv/FW1bklWAsS4ynBOAbC98W9p/I9vqBnUctsvE3BJkhzHQQyPwMHL5tTcHVA==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.5.10 + typescript: '>= 4.9.x' + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + typescript: + optional: true + '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + '@testing-library/react@16.3.2': resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} engines: {node: '>=18'} @@ -1174,6 +1590,15 @@ packages: '@types/react-dom': optional: true + '@testing-library/user-event@14.6.6': + resolution: {integrity: sha512-Jbs9FpkkIDw8FgSc6kOVsOv8JuuqGAL7J4X1oot77JxAoDlkNn2GRkd0aYRVuQ+pVQAiHWVkE4rX/dkF5fBiCw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -1195,6 +1620,9 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/doctrine@0.0.9': + resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} + '@types/esrecurse@4.3.1': resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} @@ -1204,6 +1632,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/mdx@2.0.14': + resolution: {integrity: sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==} + '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} @@ -1224,6 +1655,9 @@ packages: '@types/react@18.3.31': resolution: {integrity: sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==} + '@types/resolve@1.20.6': + resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -1292,6 +1726,9 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + '@vitest/expect@3.2.7': resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} @@ -1306,6 +1743,9 @@ packages: vite: optional: true + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + '@vitest/pretty-format@3.2.7': resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} @@ -1315,12 +1755,21 @@ packages: '@vitest/snapshot@3.2.7': resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + '@vitest/spy@3.2.7': resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@vitest/utils@3.2.7': resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} + '@webcontainer/env@1.1.1': + resolution: {integrity: sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1386,6 +1835,10 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + axe-core@4.12.1: resolution: {integrity: sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==} engines: {node: '>=4'} @@ -1416,6 +1869,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1500,6 +1957,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + cssstyle@4.6.0: resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} engines: {node: '>=18'} @@ -1588,6 +2048,18 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.1: + resolution: {integrity: sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1600,9 +2072,16 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dompurify@3.4.12: resolution: {integrity: sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==} @@ -1615,6 +2094,10 @@ packages: emojilib@2.4.0: resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} + empathic@2.0.1: + resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} + engines: {node: '>=14'} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} @@ -1627,6 +2110,10 @@ packages: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} @@ -1696,6 +2183,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -1785,6 +2275,9 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -1807,6 +2300,10 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -1818,6 +2315,10 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} @@ -1860,10 +2361,23 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + internmap@2.0.3: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1876,6 +2390,11 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -1891,6 +2410,10 @@ packages: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1943,6 +2466,9 @@ packages: engines: {node: '>=6'} hasBin: true + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -2018,10 +2544,21 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} @@ -2058,6 +2595,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -2065,6 +2606,13 @@ packages: outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + oxc-parser@0.127.0: + resolution: {integrity: sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-resolver@11.21.2: + resolution: {integrity: sha512-w5tLwYN3Zo24w5EeWJjJWZOwhYqTtC8PS2B1tIt7BZUuqTIcU07sQValbDw+rq7+AuAGzOHklgK+ifsy4lpXfw==} + p-filter@2.1.0: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} engines: {node: '>=8'} @@ -2119,6 +2667,13 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -2216,6 +2771,15 @@ packages: resolution: {integrity: sha512-6Ajb7XmMSE9EFAMGC3kg9mvE7fGlBip25mYYuSMzw/uUSrmGilvZo2qwX3RnTRjwXkwkS+4swse9otZ92VjAtQ==} engines: {node: '>=14'} + react-docgen-typescript@2.4.0: + resolution: {integrity: sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==} + peerDependencies: + typescript: '>= 4.3.x' + + react-docgen@8.0.3: + resolution: {integrity: sha512-aEZ9qP+/M+58x2qgfSFEWH1BxLyHe5+qkLNJOZQb5iGS017jpbRnoKhNRrXPeA6RfBrZO5wZrT9DMC1UqE1f1w==} + engines: {node: ^20.9.0 || >=22} + react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: @@ -2240,6 +2804,14 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + recast@0.23.21: + resolution: {integrity: sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==} + engines: {node: '>= 4'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -2248,6 +2820,11 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -2260,6 +2837,10 @@ packages: rrweb-cssom@0.8.0: resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -2316,6 +2897,10 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + source-map@0.7.6: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} @@ -2332,6 +2917,21 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + storybook@10.5.10: + resolution: {integrity: sha512-Rz8k9ejFHsi7lbtJTaxZlhCUz4GkbJIKEoKDjXeLfr/ZhXip73E6keKxW0KH8iGeKiCqHAbJCV4YIQrxTOLiig==} + hasBin: true + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + prettier: ^2 || ^3 + vite-plus: ^0.1.15 || ^0.2.0 + peerDependenciesMeta: + '@types/react': + optional: true + prettier: + optional: true + vite-plus: + optional: true + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2344,6 +2944,14 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-indent@4.1.1: + resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} + engines: {node: '>=12'} + strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} @@ -2360,6 +2968,10 @@ packages: resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} engines: {node: '>=14.18'} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -2374,6 +2986,9 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -2429,9 +3044,17 @@ packages: peerDependencies: typescript: '>=4.8.4' + ts-dedent@2.3.0: + resolution: {integrity: sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==} + engines: {node: '>=6.10'} + ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -2489,6 +3112,10 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -2498,6 +3125,11 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + validate-npm-package-name@5.0.1: resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -2583,6 +3215,9 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} @@ -2626,6 +3261,10 @@ packages: utf-8-validate: optional: true + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} @@ -2672,6 +3311,8 @@ packages: snapshots: + '@adobe/css-tools@4.5.0': {} + '@andrewbranch/untar.js@1.0.3': {} '@arethetypeswrong/cli@0.18.5': @@ -3009,6 +3650,38 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} + '@emnapi/core@1.11.0': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.25.12': optional: true @@ -3296,6 +3969,14 @@ snapshots: optionalDependencies: '@types/node': 25.9.5 + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(typescript@5.9.3)(vite@6.4.3(@types/node@25.9.5))': + dependencies: + glob: 13.0.6 + react-docgen-typescript: 2.4.0(typescript@5.9.3) + vite: 6.4.3(@types/node@25.9.5) + optionalDependencies: + typescript: 5.9.3 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -3370,8 +4051,28 @@ snapshots: '@math.gl/types@4.1.0': {} + '@mdx-js/react@3.1.1(@types/react@18.3.31)(react@18.3.1)': + dependencies: + '@types/mdx': 2.0.14 + '@types/react': 18.3.31 + react: 18.3.1 + '@modernrelay/omnigraph@0.9.0': {} + '@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)': + dependencies: + '@emnapi/core': 1.11.0 + '@emnapi/runtime': 1.11.0 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.3 + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -3384,6 +4085,133 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@oxc-parser/binding-android-arm-eabi@0.127.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.127.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + optional: true + + '@oxc-project/types@0.127.0': {} + + '@oxc-resolver/binding-android-arm-eabi@11.21.2': + optional: true + + '@oxc-resolver/binding-android-arm64@11.21.2': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.21.2': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.21.2': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.21.2': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.21.2': + optional: true + + '@oxc-resolver/binding-linux-arm-musleabihf@11.21.2': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.21.2': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.21.2': + optional: true + + '@oxc-resolver/binding-linux-ppc64-gnu@11.21.2': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.21.2': + optional: true + + '@oxc-resolver/binding-linux-riscv64-musl@11.21.2': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.21.2': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.21.2': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.21.2': + optional: true + + '@oxc-resolver/binding-openharmony-arm64@11.21.2': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.21.2': + dependencies: + '@emnapi/core': 1.11.0 + '@emnapi/runtime': 1.11.0 + '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0) + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.21.2': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.21.2': + optional: true + '@playwright/test@1.61.1': dependencies: playwright: 1.61.1 @@ -3402,6 +4230,14 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rollup/pluginutils@5.4.0(rollup@4.62.2)': + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.5 + optionalDependencies: + rollup: 4.62.2 + '@rollup/rollup-android-arm-eabi@4.62.2': optional: true @@ -3479,6 +4315,101 @@ snapshots: '@sindresorhus/is@4.6.0': {} + '@storybook/addon-docs@10.5.10(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(esbuild@0.28.1)(rollup@4.62.2)(storybook@10.5.10(@types/react@18.3.31)(prettier@2.8.8)(react@18.3.1))(vite@6.4.3(@types/node@25.9.5))': + dependencies: + '@mdx-js/react': 3.1.1(@types/react@18.3.31)(react@18.3.1) + '@storybook/csf-plugin': 10.5.10(esbuild@0.28.1)(rollup@4.62.2)(storybook@10.5.10(@types/react@18.3.31)(prettier@2.8.8)(react@18.3.1))(vite@6.4.3(@types/node@25.9.5)) + '@storybook/icons': 2.1.0(react@18.3.1) + '@storybook/react-dom-shim': 10.5.10(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.10(@types/react@18.3.31)(prettier@2.8.8)(react@18.3.1)) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + storybook: 10.5.10(@types/react@18.3.31)(prettier@2.8.8)(react@18.3.1) + ts-dedent: 2.3.0 + optionalDependencies: + '@types/react': 18.3.31 + transitivePeerDependencies: + - '@types/react-dom' + - esbuild + - rollup + - vite + - webpack + + '@storybook/builder-vite@10.5.10(esbuild@0.28.1)(rollup@4.62.2)(storybook@10.5.10(@types/react@18.3.31)(prettier@2.8.8)(react@18.3.1))(vite@6.4.3(@types/node@25.9.5))': + dependencies: + '@storybook/csf-plugin': 10.5.10(esbuild@0.28.1)(rollup@4.62.2)(storybook@10.5.10(@types/react@18.3.31)(prettier@2.8.8)(react@18.3.1))(vite@6.4.3(@types/node@25.9.5)) + storybook: 10.5.10(@types/react@18.3.31)(prettier@2.8.8)(react@18.3.1) + ts-dedent: 2.3.0 + vite: 6.4.3(@types/node@25.9.5) + transitivePeerDependencies: + - esbuild + - rollup + - webpack + + '@storybook/csf-plugin@10.5.10(esbuild@0.28.1)(rollup@4.62.2)(storybook@10.5.10(@types/react@18.3.31)(prettier@2.8.8)(react@18.3.1))(vite@6.4.3(@types/node@25.9.5))': + dependencies: + storybook: 10.5.10(@types/react@18.3.31)(prettier@2.8.8)(react@18.3.1) + unplugin: 2.3.11 + optionalDependencies: + esbuild: 0.28.1 + rollup: 4.62.2 + vite: 6.4.3(@types/node@25.9.5) + + '@storybook/global@5.0.0': {} + + '@storybook/icons@2.1.0(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@storybook/react-dom-shim@10.5.10(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.10(@types/react@18.3.31)(prettier@2.8.8)(react@18.3.1))': + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + storybook: 10.5.10(@types/react@18.3.31)(prettier@2.8.8)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) + + '@storybook/react-vite@10.5.10(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(esbuild@0.28.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.62.2)(storybook@10.5.10(@types/react@18.3.31)(prettier@2.8.8)(react@18.3.1))(typescript@5.9.3)(vite@6.4.3(@types/node@25.9.5))': + dependencies: + '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@5.9.3)(vite@6.4.3(@types/node@25.9.5)) + '@rollup/pluginutils': 5.4.0(rollup@4.62.2) + '@storybook/builder-vite': 10.5.10(esbuild@0.28.1)(rollup@4.62.2)(storybook@10.5.10(@types/react@18.3.31)(prettier@2.8.8)(react@18.3.1))(vite@6.4.3(@types/node@25.9.5)) + '@storybook/react': 10.5.10(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.10(@types/react@18.3.31)(prettier@2.8.8)(react@18.3.1))(typescript@5.9.3) + empathic: 2.0.1 + magic-string: 0.30.21 + react: 18.3.1 + react-docgen: 8.0.3 + react-dom: 18.3.1(react@18.3.1) + resolve: 1.22.12 + storybook: 10.5.10(@types/react@18.3.31)(prettier@2.8.8)(react@18.3.1) + tsconfig-paths: 4.2.0 + vite: 6.4.3(@types/node@25.9.5) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - esbuild + - rollup + - supports-color + - webpack + + '@storybook/react@10.5.10(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.10(@types/react@18.3.31)(prettier@2.8.8)(react@18.3.1))(typescript@5.9.3)': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/react-dom-shim': 10.5.10(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.10(@types/react@18.3.31)(prettier@2.8.8)(react@18.3.1)) + react: 18.3.1 + react-docgen: 8.0.3 + react-docgen-typescript: 2.4.0(typescript@5.9.3) + react-dom: 18.3.1(react@18.3.1) + storybook: 10.5.10(@types/react@18.3.31)(prettier@2.8.8)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.29.7 @@ -3490,6 +4421,15 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.5.0 + aria-query: 5.3.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.29.7 @@ -3500,6 +4440,15 @@ snapshots: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) + '@testing-library/user-event@14.6.6(@testing-library/dom@10.4.1)': + dependencies: + '@testing-library/dom': 10.4.1 + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + '@types/aria-query@5.0.4': {} '@types/babel__core@7.20.5': @@ -3530,12 +4479,16 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/doctrine@0.0.9': {} + '@types/esrecurse@4.3.1': {} '@types/estree@1.0.9': {} '@types/json-schema@7.0.15': {} + '@types/mdx@2.0.14': {} + '@types/node@12.20.55': {} '@types/node@25.9.5': @@ -3555,6 +4508,8 @@ snapshots: '@types/prop-types': 15.7.15 csstype: 3.2.3 + '@types/resolve@1.20.6': {} + '@types/trusted-types@2.0.7': optional: true @@ -3661,6 +4616,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + '@vitest/expect@3.2.7': dependencies: '@types/chai': 5.2.3 @@ -3677,6 +4640,10 @@ snapshots: optionalDependencies: vite: 6.4.3(@types/node@25.9.5) + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + '@vitest/pretty-format@3.2.7': dependencies: tinyrainbow: 2.0.0 @@ -3693,16 +4660,28 @@ snapshots: magic-string: 0.30.21 pathe: 2.0.3 + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + '@vitest/spy@3.2.7': dependencies: tinyspy: 4.0.4 + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + '@vitest/utils@3.2.7': dependencies: '@vitest/pretty-format': 3.2.7 loupe: 3.2.1 tinyrainbow: 2.0.0 + '@webcontainer/env@1.1.1': {} + acorn-jsx@5.3.2(acorn@8.17.0): dependencies: acorn: 8.17.0 @@ -3757,6 +4736,10 @@ snapshots: assertion-error@2.0.1: {} + ast-types@0.16.1: + dependencies: + tslib: 2.8.1 + axe-core@4.12.1: {} balanced-match@4.0.4: {} @@ -3783,6 +4766,10 @@ snapshots: node-releases: 2.0.51 update-browserslist-db: 1.2.3(browserslist@4.28.6) + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + bundle-require@5.1.0(esbuild@0.27.7): dependencies: esbuild: 0.27.7 @@ -3862,6 +4849,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css.escape@1.5.1: {} + cssstyle@4.6.0: dependencies: '@asamuzakjp/css-color': 3.2.0 @@ -3942,6 +4931,15 @@ snapshots: deep-is@0.1.4: {} + default-browser-id@5.0.1: {} + + default-browser@5.5.1: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + dequal@2.0.3: {} detect-indent@6.1.0: {} @@ -3950,8 +4948,14 @@ snapshots: dependencies: path-type: 4.0.0 + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + dom-accessibility-api@0.5.16: {} + dom-accessibility-api@0.6.3: {} + dompurify@3.4.12: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -3962,6 +4966,8 @@ snapshots: emojilib@2.4.0: {} + empathic@2.0.1: {} + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 @@ -3971,6 +4977,8 @@ snapshots: environment@1.1.0: {} + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} esbuild@0.25.12: @@ -4128,6 +5136,8 @@ snapshots: estraverse@5.3.0: {} + estree-walker@2.0.2: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 @@ -4213,6 +5223,8 @@ snapshots: fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -4229,6 +5241,12 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + globby@11.1.0: dependencies: array-union: 2.1.0 @@ -4242,6 +5260,10 @@ snapshots: has-flag@4.0.0: {} + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + highlight.js@10.7.3: {} html-encoding-sniffer@4.0.0: @@ -4280,8 +5302,16 @@ snapshots: imurmurhash@0.1.4: {} + indent-string@4.0.0: {} + internmap@2.0.3: {} + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-docker@3.0.0: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -4290,6 +5320,10 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-number@7.0.0: {} is-potential-custom-element-name@1.0.1: {} @@ -4300,6 +5334,10 @@ snapshots: is-windows@1.0.2: {} + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + isexe@2.0.0: {} joycon@3.1.1: {} @@ -4356,6 +5394,8 @@ snapshots: json5@2.2.3: {} + jsonc-parser@3.3.1: {} + jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -4425,10 +5465,16 @@ snapshots: braces: 3.0.3 picomatch: 2.3.2 + min-indent@1.0.1: {} + minimatch@10.2.5: dependencies: brace-expansion: 5.0.7 + minimist@1.2.8: {} + + minipass@7.1.3: {} + mlly@1.8.2: dependencies: acorn: 8.17.0 @@ -4463,6 +5509,13 @@ snapshots: object-assign@4.1.1: {} + open@10.2.0: + dependencies: + default-browser: 5.5.1 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -4474,6 +5527,53 @@ snapshots: outdent@0.5.0: {} + oxc-parser@0.127.0: + dependencies: + '@oxc-project/types': 0.127.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.127.0 + '@oxc-parser/binding-android-arm64': 0.127.0 + '@oxc-parser/binding-darwin-arm64': 0.127.0 + '@oxc-parser/binding-darwin-x64': 0.127.0 + '@oxc-parser/binding-freebsd-x64': 0.127.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.127.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.127.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.127.0 + '@oxc-parser/binding-linux-arm64-musl': 0.127.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.127.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-musl': 0.127.0 + '@oxc-parser/binding-openharmony-arm64': 0.127.0 + '@oxc-parser/binding-wasm32-wasi': 0.127.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.127.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.127.0 + '@oxc-parser/binding-win32-x64-msvc': 0.127.0 + + oxc-resolver@11.21.2: + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.21.2 + '@oxc-resolver/binding-android-arm64': 11.21.2 + '@oxc-resolver/binding-darwin-arm64': 11.21.2 + '@oxc-resolver/binding-darwin-x64': 11.21.2 + '@oxc-resolver/binding-freebsd-x64': 11.21.2 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.21.2 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.21.2 + '@oxc-resolver/binding-linux-arm64-gnu': 11.21.2 + '@oxc-resolver/binding-linux-arm64-musl': 11.21.2 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.21.2 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.21.2 + '@oxc-resolver/binding-linux-riscv64-musl': 11.21.2 + '@oxc-resolver/binding-linux-s390x-gnu': 11.21.2 + '@oxc-resolver/binding-linux-x64-gnu': 11.21.2 + '@oxc-resolver/binding-linux-x64-musl': 11.21.2 + '@oxc-resolver/binding-openharmony-arm64': 11.21.2 + '@oxc-resolver/binding-wasm32-wasi': 11.21.2 + '@oxc-resolver/binding-win32-arm64-msvc': 11.21.2 + '@oxc-resolver/binding-win32-x64-msvc': 11.21.2 + p-filter@2.1.0: dependencies: p-map: 2.1.0 @@ -4520,6 +5620,13 @@ snapshots: path-key@3.1.1: {} + path-parse@1.0.7: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.2 + minipass: 7.1.3 + path-type@4.0.0: {} pathe@2.0.3: {} @@ -4589,6 +5696,25 @@ snapshots: dependencies: seedrandom: 3.0.5 + react-docgen-typescript@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + react-docgen@8.0.3: + dependencies: + '@babel/core': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.28.0 + '@types/doctrine': 0.0.9 + '@types/resolve': 1.20.6 + doctrine: 3.0.0 + resolve: 1.22.12 + strip-indent: 4.1.1 + transitivePeerDependencies: + - supports-color + react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 @@ -4612,10 +5738,30 @@ snapshots: readdirp@4.1.2: {} + recast@0.23.21: + dependencies: + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + require-directory@2.1.1: {} resolve-from@5.0.0: {} + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + reusify@1.1.0: {} rollup@4.62.2: @@ -4651,6 +5797,8 @@ snapshots: rrweb-cssom@0.8.0: {} + run-applescript@7.1.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -4693,6 +5841,8 @@ snapshots: source-map-js@1.2.1: {} + source-map@0.6.1: {} + source-map@0.7.6: {} spawndamnit@3.0.1: @@ -4706,6 +5856,33 @@ snapshots: std-env@3.10.0: {} + storybook@10.5.10(@types/react@18.3.31)(prettier@2.8.8)(react@18.3.1): + dependencies: + '@storybook/global': 5.0.0 + '@storybook/icons': 2.1.0(react@18.3.1) + '@testing-library/dom': 10.4.1 + '@testing-library/jest-dom': 6.9.1 + '@testing-library/user-event': 14.6.6(@testing-library/dom@10.4.1) + '@vitest/expect': 3.2.4 + '@vitest/spy': 3.2.4 + '@webcontainer/env': 1.1.1 + esbuild: 0.28.1 + jsonc-parser: 3.3.1 + open: 10.2.0 + oxc-parser: 0.127.0 + oxc-resolver: 11.21.2 + recast: 0.23.21 + semver: 7.8.5 + use-sync-external-store: 1.6.0(react@18.3.1) + ws: 8.21.1 + optionalDependencies: + '@types/react': 18.3.31 + prettier: 2.8.8 + transitivePeerDependencies: + - bufferutil + - react + - utf-8-validate + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -4718,6 +5895,12 @@ snapshots: strip-bom@3.0.0: {} + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-indent@4.1.1: {} + strip-literal@3.1.0: dependencies: js-tokens: 9.0.1 @@ -4741,6 +5924,8 @@ snapshots: has-flag: 4.0.0 supports-color: 7.2.0 + supports-preserve-symlinks-flag@1.0.0: {} + symbol-tree@3.2.4: {} term-size@2.2.1: {} @@ -4753,6 +5938,8 @@ snapshots: dependencies: any-promise: 1.3.0 + tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -4794,8 +5981,16 @@ snapshots: dependencies: typescript: 5.9.3 + ts-dedent@2.3.0: {} + ts-interface-checker@0.1.13: {} + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + tslib@2.8.1: {} tsup@8.5.1(postcss@8.5.20)(typescript@5.9.3): @@ -4853,6 +6048,13 @@ snapshots: universalify@0.1.2: {} + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.17.0 + picomatch: 4.0.5 + webpack-virtual-modules: 0.6.2 + update-browserslist-db@1.2.3(browserslist@4.28.6): dependencies: browserslist: 4.28.6 @@ -4863,6 +6065,10 @@ snapshots: dependencies: punycode: 2.3.1 + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + validate-npm-package-name@5.0.1: {} vite-node@3.2.4(@types/node@25.9.5): @@ -4946,6 +6152,8 @@ snapshots: webidl-conversions@7.0.0: {} + webpack-virtual-modules@0.6.2: {} + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 @@ -4976,6 +6184,10 @@ snapshots: ws@8.21.1: {} + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.1 + xml-name-validator@5.0.0: {} xmlchars@2.2.0: {} @@ -4998,7 +6210,8 @@ snapshots: yocto-queue@0.1.0: {} - zustand@5.0.14(@types/react@18.3.31)(react@18.3.1): + zustand@5.0.14(@types/react@18.3.31)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)): optionalDependencies: '@types/react': 18.3.31 react: 18.3.1 + use-sync-external-store: 1.6.0(react@18.3.1) From afaa6922b814ad48654bdcfe38eabe59f113d628 Mon Sep 17 00:00:00 2001 From: aaltshuler Date: Wed, 26 Aug 2026 21:16:43 +0300 Subject: [PATCH 2/3] Storybook restructure: topology library, size global, deep prop stories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addressing 'shallow, one dataset shape, no size control': - fixtures/topologies.ts: six deterministic force topologies (clustered, tree, scale-free, bipartite, ring, islands) all emitting the same typed attrs so every scale/dimension/search fixture composes with every shape, plus three fixed-layout topologies with exact declared positions (circle, tidy radial tree partitioned by subtree share, lattice) - global size toolbar (S 300 / M 1,500 / L 8,000 / XL 40,000) read by all stories through a per-size snapshot cache; theme toolbar unchanged - catalog deepened reagraph-style, one story per prop value: Graph: Minimal, Topologies (radio), Nodes (function/categorical/ sequential color, degree/uniform size), Edges (per-edge color/width, arrows, hidden), Labels (zoom threshold, pinned ids, custom JSX pills), Themes (base swap + custom tokens) Layouts: Force (preset radio + full tunables), Fixed (circular/radial tree/grid), Fixed-to-force handoff (declared positions double as simulation seeds — one prop flips designed geometry into organic motion) Interaction: Selection, Hover & emphasis, Context menu (find-path flow) - DemoGraph now forwards refs (handle access in stories) Verified live: 12 story files register, tree/radial/sequential/preset stories render framed at natural node scale via the new clamp. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018SeFxK217ZrHSERqK6kjcb --- apps/storybook/.storybook/preview.ts | 19 +- apps/storybook/src/fixtures/DemoGraph.tsx | 13 +- apps/storybook/src/fixtures/index.ts | 2 + apps/storybook/src/fixtures/sizes.ts | 31 ++ apps/storybook/src/fixtures/topologies.ts | 336 ++++++++++++++++++ apps/storybook/src/graph/Edges.stories.tsx | 68 ++++ apps/storybook/src/graph/Labels.stories.tsx | 89 +++++ .../src/graph/LayoutForce.stories.tsx | 62 ---- apps/storybook/src/graph/Minimal.stories.tsx | 19 +- apps/storybook/src/graph/Nodes.stories.tsx | 75 ++++ apps/storybook/src/graph/Styling.stories.tsx | 82 ----- apps/storybook/src/graph/Themes.stories.tsx | 46 ++- .../src/graph/Topologies.stories.tsx | 66 ++++ .../src/interaction/ContextMenu.stories.tsx | 58 +++ .../src/interaction/HoverEmphasis.stories.tsx | 62 ++++ .../src/interaction/Selection.stories.tsx | 15 +- apps/storybook/src/layouts/Fixed.stories.tsx | 73 ++++ apps/storybook/src/layouts/Force.stories.tsx | 98 +++++ .../storybook/src/layouts/Handoff.stories.tsx | 76 ++++ 19 files changed, 1100 insertions(+), 190 deletions(-) create mode 100644 apps/storybook/src/fixtures/sizes.ts create mode 100644 apps/storybook/src/fixtures/topologies.ts create mode 100644 apps/storybook/src/graph/Edges.stories.tsx create mode 100644 apps/storybook/src/graph/Labels.stories.tsx delete mode 100644 apps/storybook/src/graph/LayoutForce.stories.tsx create mode 100644 apps/storybook/src/graph/Nodes.stories.tsx delete mode 100644 apps/storybook/src/graph/Styling.stories.tsx create mode 100644 apps/storybook/src/graph/Topologies.stories.tsx create mode 100644 apps/storybook/src/interaction/ContextMenu.stories.tsx create mode 100644 apps/storybook/src/interaction/HoverEmphasis.stories.tsx create mode 100644 apps/storybook/src/layouts/Fixed.stories.tsx create mode 100644 apps/storybook/src/layouts/Force.stories.tsx create mode 100644 apps/storybook/src/layouts/Handoff.stories.tsx diff --git a/apps/storybook/.storybook/preview.ts b/apps/storybook/.storybook/preview.ts index bf22d0e..2686e79 100644 --- a/apps/storybook/.storybook/preview.ts +++ b/apps/storybook/.storybook/preview.ts @@ -14,8 +14,11 @@ const preview: Preview = { 'Introduction', ['What is orbit', 'Data shapes', 'Testing without WebGL'], 'Graph', - ['Minimal', 'Styling', 'Themes', 'Layout: force'], + ['Minimal', 'Topologies', 'Nodes', 'Edges', 'Labels', 'Themes'], + 'Layouts', + ['Force', 'Fixed', 'Fixed to force'], 'Interaction', + ['Selection', 'Hover & emphasis', 'Context menu'], 'Exploration', 'Analytics', 'Components', @@ -35,9 +38,23 @@ const preview: Preview = { dynamicTitle: true, }, }, + size: { + description: 'Graph size (nodes)', + toolbar: { + icon: 'grow', + items: [ + { value: 'S', title: 'S — 300 nodes' }, + { value: 'M', title: 'M — 1,500 nodes' }, + { value: 'L', title: 'L — 8,000 nodes' }, + { value: 'XL', title: 'XL — 40,000 nodes' }, + ], + dynamicTitle: true, + }, + }, }, initialGlobals: { theme: 'dark', + size: 'M', }, }; diff --git a/apps/storybook/src/fixtures/DemoGraph.tsx b/apps/storybook/src/fixtures/DemoGraph.tsx index e178434..2571429 100644 --- a/apps/storybook/src/fixtures/DemoGraph.tsx +++ b/apps/storybook/src/fixtures/DemoGraph.tsx @@ -4,16 +4,21 @@ * against a concrete component type), plus the standard story frame. */ +import { forwardRef } from 'react'; import type { ReactNode } from 'react'; import { Graph } from '@modernrelay/orbit-react'; -import type { GraphProps } from '@modernrelay/orbit-react'; +import type { GraphHandle, GraphProps } from '@modernrelay/orbit-react'; import type { DemoEdgeAttrs, DemoNodeAttrs } from './generate'; export type DemoGraphProps = GraphProps; +export type DemoGraphHandle = GraphHandle; -export function DemoGraph(props: DemoGraphProps): ReactNode { - return {...props} />; -} +export const DemoGraph = forwardRef(function DemoGraph( + props, + ref, +) { + return ref={ref} {...props} />; +}); /** Standard story frame: the graph fills its parent, so stories mount inside * a fixed-height surface whose background matches the active theme. */ diff --git a/apps/storybook/src/fixtures/index.ts b/apps/storybook/src/fixtures/index.ts index 4f40b51..a83acae 100644 --- a/apps/storybook/src/fixtures/index.ts +++ b/apps/storybook/src/fixtures/index.ts @@ -5,3 +5,5 @@ export * from './config'; export * from './positions'; export * from './engines'; export * from './DemoGraph'; +export * from './sizes'; +export * from './topologies'; diff --git a/apps/storybook/src/fixtures/sizes.ts b/apps/storybook/src/fixtures/sizes.ts new file mode 100644 index 0000000..5ef2d64 --- /dev/null +++ b/apps/storybook/src/fixtures/sizes.ts @@ -0,0 +1,31 @@ +/** + * Graph-size global: every story reads the toolbar's S/M/L/XL selection, so + * the whole catalog rescales without per-story wiring. Snapshots are cached + * per size so control toggles never regenerate data. + */ + +import type { DemoSnapshot } from './generate'; + +export const SIZES = { S: 300, M: 1500, L: 8000, XL: 40_000 } as const; +export type SizeKey = keyof typeof SIZES; + +export function sizeFromGlobals(globals: Record): number { + const k = globals['size']; + return SIZES[typeof k === 'string' && k in SIZES ? (k as SizeKey) : 'M']; +} + +export function sizedCache( + maker: (seed: number, n: number) => DemoSnapshot, + seed = 7, +): (globals: Record) => DemoSnapshot { + const cache = new Map(); + return (globals) => { + const n = sizeFromGlobals(globals); + let snap = cache.get(n); + if (snap === undefined) { + snap = maker(seed, n); + cache.set(n, snap); + } + return snap; + }; +} diff --git a/apps/storybook/src/fixtures/topologies.ts b/apps/storybook/src/fixtures/topologies.ts new file mode 100644 index 0000000..f004da1 --- /dev/null +++ b/apps/storybook/src/fixtures/topologies.ts @@ -0,0 +1,336 @@ +/** + * Deterministic topology fixtures. Every generator emits DemoNodeAttrs-shaped + * attrs (cluster, label, degree, score, createdAt), so the color scales, + * crossfilter dimensions, and search index compose with EVERY shape. Fixed- + * layout topologies additionally declare exact x/y positions. + */ + +import type { GraphEdge, GraphNode } from '@modernrelay/orbit-core'; +import { generateGraph, mulberry32, nodeMetrics } from './generate'; +import type { DemoEdgeAttrs, DemoNodeAttrs, DemoSnapshot } from './generate'; + +export type TopologyKind = + | 'clustered' + | 'tree' + | 'scale-free' + | 'bipartite' + | 'ring' + | 'islands'; + +const idOf = (i: number): string => `n${i}`; + +interface BuildArgs { + seed: number; + n: number; + datasetKey: string; + /** node index → cluster id (drives color + labels) */ + clusterOf: (i: number) => number; + clusters: number; + edges: GraphEdge[]; + degree: Uint32Array; + positions?: (i: number) => readonly [number, number]; +} + +function build(args: BuildArgs): DemoSnapshot { + const nodes: GraphNode[] = new Array(args.n); + for (let i = 0; i < args.n; i++) { + const cluster = args.clusterOf(i); + const { score, createdAt } = nodeMetrics(args.seed, i, cluster, args.clusters); + const node: GraphNode = { + id: idOf(i), + attrs: { + cluster, + label: `${args.datasetKey}-${String(i).padStart(4, '0')}`, + degree: args.degree[i] ?? 0, + score, + createdAt, + }, + }; + if (args.positions !== undefined) { + const [x, y] = args.positions(i); + node.x = x; + node.y = y; + } + nodes[i] = node; + } + return { + datasetKey: args.datasetKey, + sourceRevision: 1, + nodes, + edges: args.edges, + }; +} + +/** the demo's clustered communities (delegates to the seeded generator). */ +export function clustered(seed: number, n: number): DemoSnapshot { + return generateGraph({ + seed, + nodes: n, + clusters: 6, + intraEdgeFactor: 1.6, + interEdgeProb: 0.06, + datasetKey: 'clustered', + sourceRevision: 1, + }); +} + +/** branching tree; cluster = depth band. */ +export function tree(seed: number, n: number): DemoSnapshot { + const rng = mulberry32(seed); + const edges: GraphEdge[] = []; + const degree = new Uint32Array(n); + const depth = new Uint32Array(n); + let maxDepth = 1; + for (let i = 1; i < n; i++) { + // parent among recent nodes → varied branching, moderate depth + const span = Math.max(1, Math.floor(i * 0.35)); + const parent = i - 1 - Math.floor(rng() * span); + const p = Math.max(0, parent); + edges.push({ source: idOf(p), target: idOf(i), attrs: { kind: 'intra' } }); + degree[p] = (degree[p] ?? 0) + 1; + degree[i] = (degree[i] ?? 0) + 1; + const di = (depth[p] ?? 0) + 1; + depth[i] = di; + if (di > maxDepth) maxDepth = di; + } + return build({ + seed, + n, + datasetKey: 'tree', + clusterOf: (i) => Math.min(5, Math.floor(((depth[i] ?? 0) / (maxDepth + 1)) * 6)), + clusters: 6, + edges, + degree, + }); +} + +/** preferential attachment; cluster = hub tier (log-degree band). */ +export function scaleFree(seed: number, n: number): DemoSnapshot { + const rng = mulberry32(seed); + const edges: GraphEdge[] = []; + const degree = new Uint32Array(n); + const bag: number[] = [0]; + for (let i = 1; i < n; i++) { + const m = 1 + (rng() < 0.35 ? 1 : 0); + const chosen = new Set(); + for (let e = 0; e < m; e++) { + const t = bag[Math.floor(rng() * bag.length)] ?? 0; + if (t === i || chosen.has(t)) continue; + chosen.add(t); + edges.push({ source: idOf(t), target: idOf(i), attrs: { kind: 'intra' } }); + degree[t] = (degree[t] ?? 0) + 1; + degree[i] = (degree[i] ?? 0) + 1; + bag.push(t, i); + } + if (chosen.size === 0) { + edges.push({ source: idOf(i - 1), target: idOf(i), attrs: { kind: 'intra' } }); + degree[i - 1] = (degree[i - 1] ?? 0) + 1; + degree[i] = (degree[i] ?? 0) + 1; + bag.push(i - 1, i); + } + } + return build({ + seed, + n, + datasetKey: 'hubs', + clusterOf: (i) => Math.min(5, Math.floor(Math.log2((degree[i] ?? 0) + 1))), + clusters: 6, + edges, + degree, + }); +} + +/** two partitions, edges only across; cluster = side. */ +export function bipartite(seed: number, n: number): DemoSnapshot { + const rng = mulberry32(seed); + const left = Math.floor(n * 0.4); + const edges: GraphEdge[] = []; + const degree = new Uint32Array(n); + for (let i = left; i < n; i++) { + const links = 1 + Math.floor(rng() * 3); + const chosen = new Set(); + for (let e = 0; e < links; e++) { + const t = Math.floor(rng() * left); + if (chosen.has(t)) continue; + chosen.add(t); + edges.push({ source: idOf(t), target: idOf(i), attrs: { kind: 'inter' } }); + degree[t] = (degree[t] ?? 0) + 1; + degree[i] = (degree[i] ?? 0) + 1; + } + } + return build({ + seed, + n, + datasetKey: 'bipartite', + clusterOf: (i) => (i < left ? 0 : 2), + clusters: 6, + edges, + degree, + }); +} + +/** one big cycle with sparse chords; cluster = arc segment. */ +export function ring(seed: number, n: number): DemoSnapshot { + const rng = mulberry32(seed); + const edges: GraphEdge[] = []; + const degree = new Uint32Array(n); + for (let i = 0; i < n; i++) { + const j = (i + 1) % n; + edges.push({ source: idOf(i), target: idOf(j), attrs: { kind: 'intra' } }); + degree[i] = (degree[i] ?? 0) + 1; + degree[j] = (degree[j] ?? 0) + 1; + if (rng() < 0.04) { + const far = (i + Math.floor(n / 3) + Math.floor(rng() * (n / 3))) % n; + edges.push({ source: idOf(i), target: idOf(far), attrs: { kind: 'inter' } }); + degree[i] = (degree[i] ?? 0) + 1; + degree[far] = (degree[far] ?? 0) + 1; + } + } + return build({ + seed, + n, + datasetKey: 'ring', + clusterOf: (i) => Math.floor((i / n) * 6), + clusters: 6, + edges, + degree, + }); +} + +/** disconnected components — one clustered blob per island. */ +export function islands(seed: number, n: number): DemoSnapshot { + const rng = mulberry32(seed); + const k = 5; + const per = Math.floor(n / k); + const edges: GraphEdge[] = []; + const degree = new Uint32Array(n); + for (let island = 0; island < k; island++) { + const b = island * per; + const size = island === k - 1 ? n - b : per; + for (let v = 1; v < size; v++) { + const g = b + v; + const links = 1 + (rng() < 0.5 ? 1 : 0); + for (let e = 0; e < links; e++) { + const t = b + Math.floor(rng() * v); + edges.push({ source: idOf(t), target: idOf(g), attrs: { kind: 'intra' } }); + degree[t] = (degree[t] ?? 0) + 1; + degree[g] = (degree[g] ?? 0) + 1; + } + } + } + return build({ + seed, + n, + datasetKey: 'islands', + clusterOf: (i) => Math.min(5, Math.floor(i / per)), + clusters: 6, + edges, + degree, + }); +} + +export const FORCE_TOPOLOGIES: Record DemoSnapshot> = { + clustered, + tree, + 'scale-free': scaleFree, + bipartite, + ring, + islands, +}; + +// --- fixed-layout topologies: exact declared positions ---------------------- + +const CX = 2048; +const CY = 2048; + +/** perfect circle; sequential edges + sparse chords (ring topology, exact). */ +export function circularFixed(seed: number, n: number): DemoSnapshot { + const snap = ring(seed, n); + const r = 1500; + const nodes = snap.nodes.map((node, i) => ({ + ...node, + x: CX + r * Math.cos((i / n) * 2 * Math.PI), + y: CY + r * Math.sin((i / n) * 2 * Math.PI), + })); + return { ...snap, datasetKey: 'circular', nodes }; +} + +/** square lattice with right/down edges; cluster = 3x2 region. */ +export function gridFixed(seed: number, n: number): DemoSnapshot { + const cols = Math.ceil(Math.sqrt(n)); + const rows = Math.ceil(n / cols); + const cell = 3000 / Math.max(cols, rows); + const x0 = CX - (cols * cell) / 2; + const y0 = CY - (rows * cell) / 2; + const edges: GraphEdge[] = []; + const degree = new Uint32Array(n); + const link = (a: number, b: number): void => { + edges.push({ source: idOf(a), target: idOf(b), attrs: { kind: 'intra' } }); + degree[a] = (degree[a] ?? 0) + 1; + degree[b] = (degree[b] ?? 0) + 1; + }; + for (let i = 0; i < n; i++) { + const c = i % cols; + const rw = Math.floor(i / cols); + if (c + 1 < cols && i + 1 < n) link(i, i + 1); + if (rw + 1 < rows && i + cols < n) link(i, i + cols); + } + return build({ + seed, + n, + datasetKey: 'grid', + clusterOf: (i) => + Math.min(2, Math.floor(((i % cols) / cols) * 3)) + 3 * Math.min(1, Math.floor((Math.floor(i / cols) / rows) * 2)), + clusters: 6, + edges, + degree, + positions: (i) => [x0 + (i % cols) * cell, y0 + Math.floor(i / cols) * cell], + }); +} + +/** tidy radial tree: concentric depth rings, children fan under parents. */ +export function radialTreeFixed(seed: number, n: number): DemoSnapshot { + const snap = tree(seed, n); + // recover parents from the tree edge list (source = parent) + const parent = new Int32Array(n).fill(-1); + const childrenOf: number[][] = Array.from({ length: n }, () => []); + for (const e of snap.edges) { + const p = Number(e.source.slice(1)); + const c = Number(e.target.slice(1)); + parent[c] = p; + childrenOf[p]!.push(c); + } + // subtree sizes → angular share + const sizeOf = new Float64Array(n).fill(1); + for (let i = n - 1; i >= 1; i--) { + const pi = parent[i]!; + sizeOf[pi] = (sizeOf[pi] ?? 0) + (sizeOf[i] ?? 0); + } + const angle = new Float64Array(n); + const depth = new Uint32Array(n); + const span = new Float64Array(n); + angle[0] = 0; + span[0] = 2 * Math.PI; + let maxDepth = 1; + // children partition the parent's angular span by subtree share + for (let i = 0; i < n; i++) { + const kids = childrenOf[i]!; + if (kids.length === 0) continue; + let cursor = angle[i]! - span[i]! / 2; + for (const kid of kids) { + const share = (sizeOf[kid]! / (sizeOf[i]! - 1)) * span[i]!; + angle[kid] = cursor + share / 2; + span[kid] = share; + depth[kid] = depth[i]! + 1; + if (depth[kid]! > maxDepth) maxDepth = depth[kid]!; + cursor += share; + } + } + const ringStep = 1500 / (maxDepth + 1); + const nodes = snap.nodes.map((node, i) => ({ + ...node, + x: CX + depth[i]! * ringStep * Math.cos(angle[i]!), + y: CY + depth[i]! * ringStep * Math.sin(angle[i]!), + })); + return { ...snap, datasetKey: 'radial-tree', nodes }; +} diff --git a/apps/storybook/src/graph/Edges.stories.tsx b/apps/storybook/src/graph/Edges.stories.tsx new file mode 100644 index 0000000..04323c5 --- /dev/null +++ b/apps/storybook/src/graph/Edges.stories.tsx @@ -0,0 +1,68 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import type { ReactElement } from 'react'; +import { DemoGraph, GraphFrame } from '../fixtures/DemoGraph'; +import type { DemoGraphProps } from '../fixtures/DemoGraph'; +import { cosmosEngine } from '../fixtures/engines'; +import type { DemoEdgeAttrs } from '../fixtures/generate'; +import { sizedCache } from '../fixtures/sizes'; +import { themeFromGlobals } from '../fixtures/themes'; +import { clustered } from '../fixtures/topologies'; + +const data = sizedCache(clustered, 13); + +function frame(globals: Record, props: Partial): ReactElement { + const active = themeFromGlobals(globals); + return ( + + + + ); +} + +const meta = { + title: 'Graph/Edges', + parameters: { + docs: { + description: { + component: + 'Link color and width are per-edge accessors over your typed edge attrs; ' + + 'arrows and link visibility are plain props. Here intra-community edges ' + + 'stay quiet while bridges between communities read as bright strands.', + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/** bridges bright, intra-community edges quiet */ +const bridgeColor = (edge: { attrs?: DemoEdgeAttrs }): string => + edge.attrs?.kind === 'inter' ? 'rgba(122, 162, 247, 0.9)' : 'rgba(148, 163, 184, 0.15)'; +const bridgeWidth = (edge: { attrs?: DemoEdgeAttrs }): number => + edge.attrs?.kind === 'inter' ? 2.5 : 1; + +export const PerEdgeColor: Story = { + render: (_args, { globals }) => frame(globals, { linkColor: bridgeColor }), +}; + +export const PerEdgeWidth: Story = { + render: (_args, { globals }) => + frame(globals, { linkColor: bridgeColor, linkWidth: bridgeWidth }), +}; + +export const Arrows: Story = { + render: (_args, { globals }) => { + const active = themeFromGlobals(globals); + return frame(globals, { linkColor: active.linkColor, edgeArrows: true }); + }, +}; + +export const HiddenLinks: Story = { + render: (_args, { globals }) => frame(globals, { showLinks: false }), +}; diff --git a/apps/storybook/src/graph/Labels.stories.tsx b/apps/storybook/src/graph/Labels.stories.tsx new file mode 100644 index 0000000..8ece142 --- /dev/null +++ b/apps/storybook/src/graph/Labels.stories.tsx @@ -0,0 +1,89 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import type { ReactElement } from 'react'; +import { DemoGraph, GraphFrame } from '../fixtures/DemoGraph'; +import type { DemoGraphProps } from '../fixtures/DemoGraph'; +import { cosmosEngine } from '../fixtures/engines'; +import type { DemoNodeAttrs } from '../fixtures/generate'; +import type { GraphNode, LabelConfig } from '@modernrelay/orbit-core'; +import { CLUSTER_COLOR_SCALE, clusterColor } from '../fixtures/scales'; +import { sizedCache } from '../fixtures/sizes'; +import { themeFromGlobals } from '../fixtures/themes'; +import { clustered } from '../fixtures/topologies'; + +const data = sizedCache(clustered, 17); + +const labelOf = (node: GraphNode): string => node.attrs?.label ?? node.id; + +/** labels appear once you zoom past 1.2, capped at the 48 top-ranked */ +const LOD_LABELS: LabelConfig = { minZoom: 1.2, maxVisible: 48, getText: labelOf }; +/** always-on labels for a handful of pinned ids */ +const PINNED_LABELS: LabelConfig = { + minZoom: 1.2, + maxVisible: 24, + showFor: ['n0', 'n1', 'n2', 'n3'], + getText: labelOf, +}; + +function frame(globals: Record, props: Partial): ReactElement { + const active = themeFromGlobals(globals); + return ( + + + + ); +} + +const meta = { + title: 'Graph/Labels', + parameters: { + docs: { + description: { + component: + 'The label lane is config, not markup: a zoom threshold, a ranked ' + + 'visibility cap, always-on ids, and a text accessor. `renderNodeLabel` is ' + + 'the JSX escape hatch — your component renders inside the label layer. ' + + 'Zoom in to see labels arrive.', + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const ZoomThreshold: Story = { + render: (_args, { globals }) => frame(globals, { labels: LOD_LABELS }), +}; + +export const PinnedIds: Story = { + render: (_args, { globals }) => frame(globals, { labels: PINNED_LABELS }), +}; + +export const CustomPills: Story = { + render: (_args, { globals }) => + frame(globals, { + labels: PINNED_LABELS, + renderNodeLabel: ({ node, text }) => ( + + {text} + + ), + }), +}; diff --git a/apps/storybook/src/graph/LayoutForce.stories.tsx b/apps/storybook/src/graph/LayoutForce.stories.tsx deleted file mode 100644 index 26b0ee9..0000000 --- a/apps/storybook/src/graph/LayoutForce.stories.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react-vite'; -import { DemoGraph, GraphFrame } from '../fixtures/DemoGraph'; -import { cosmosEngine } from '../fixtures/engines'; -import { generateGraph } from '../fixtures/generate'; -import { themeFromGlobals } from '../fixtures/themes'; - -const data = generateGraph({ - seed: 21, - nodes: 1500, - clusters: 6, - intraEdgeFactor: 1.6, - interEdgeProb: 0.06, - datasetKey: 'layout-force', - sourceRevision: 1, -}); - -interface ForceArgs { - repulsion: number; - gravity: number; -} - -const meta = { - title: 'Graph/Layout: force', - parameters: { - docs: { - description: { - component: - 'The GPU force layout runs in the engine; the simulation prop exposes live ' + - 'tunables. Move the sliders — each change re-heats the running simulation.', - }, - }, - }, - args: { - repulsion: 0.6, - gravity: 0.25, - }, - argTypes: { - repulsion: { control: { type: 'range', min: 0, max: 2, step: 0.05 } }, - gravity: { control: { type: 'range', min: 0, max: 1, step: 0.05 } }, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const Force: Story = { - render: (args, { globals }) => { - const active = themeFromGlobals(globals); - return ( - - - - ); - }, -}; diff --git a/apps/storybook/src/graph/Minimal.stories.tsx b/apps/storybook/src/graph/Minimal.stories.tsx index 4e86458..0948943 100644 --- a/apps/storybook/src/graph/Minimal.stories.tsx +++ b/apps/storybook/src/graph/Minimal.stories.tsx @@ -1,18 +1,11 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { DemoGraph, GraphFrame } from '../fixtures/DemoGraph'; import { cosmosEngine } from '../fixtures/engines'; -import { generateGraph } from '../fixtures/generate'; +import { sizedCache } from '../fixtures/sizes'; import { themeFromGlobals } from '../fixtures/themes'; +import { clustered } from '../fixtures/topologies'; -const data = generateGraph({ - seed: 7, - nodes: 300, - clusters: 4, - intraEdgeFactor: 1.6, - interEdgeProb: 0.06, - datasetKey: 'minimal', - sourceRevision: 1, -}); +const data = sizedCache(clustered, 7); const meta = { title: 'Graph/Minimal', @@ -22,7 +15,8 @@ const meta = { description: { component: 'The smallest possible orbit graph: an engine factory and a data snapshot. ' + - 'Everything else — layout, theme, interaction — is defaults.', + 'Everything else — layout, camera, theme, interaction — is defaults: the ' + + 'calm simulation preset, the settle-following camera, the fit zoom clamp.', }, source: { code: `import { Graph } from '@modernrelay/orbit-react'; @@ -44,13 +38,12 @@ type Story = StoryObj; export const Minimal: Story = { args: { engine: cosmosEngine, - data, }, render: (args, { globals }) => { const active = themeFromGlobals(globals); return ( - + ); }, diff --git a/apps/storybook/src/graph/Nodes.stories.tsx b/apps/storybook/src/graph/Nodes.stories.tsx new file mode 100644 index 0000000..e5f2124 --- /dev/null +++ b/apps/storybook/src/graph/Nodes.stories.tsx @@ -0,0 +1,75 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import type { ReactElement } from 'react'; +import { DemoGraph, GraphFrame } from '../fixtures/DemoGraph'; +import { cosmosEngine } from '../fixtures/engines'; +import type { DemoGraphProps } from '../fixtures/DemoGraph'; +import type { DemoNodeAttrs } from '../fixtures/generate'; +import { CLUSTER_COLOR_SCALE, DEGREE_COLOR_SCALE, DEGREE_SIZE_SCALE, clusterColor } from '../fixtures/scales'; +import { sizedCache } from '../fixtures/sizes'; +import { themeFromGlobals } from '../fixtures/themes'; +import { clustered } from '../fixtures/topologies'; + +const data = sizedCache(clustered, 11); + +function frame( + globals: Record, + props: Partial, +): ReactElement { + const active = themeFromGlobals(globals); + return ( + + + + ); +} + +const meta = { + title: 'Graph/Nodes', + parameters: { + docs: { + description: { + component: + 'Node color and size each accept a per-node function or a declarative ' + + 'scale descriptor. Scales are structural values — categorical over a ' + + 'field, sequential over a metric — and they also feed the legend.', + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/** plain per-node accessor — full control, no legend integration */ +const colorFn = (node: { attrs?: DemoNodeAttrs }): string => + clusterColor(node.attrs?.cluster ?? 0); + +export const FunctionColor: Story = { + render: (_args, { globals }) => frame(globals, { nodeColor: colorFn }), +}; + +export const CategoricalScale: Story = { + render: (_args, { globals }) => frame(globals, { nodeColor: CLUSTER_COLOR_SCALE }), +}; + +export const SequentialScale: Story = { + render: (_args, { globals }) => frame(globals, { nodeColor: DEGREE_COLOR_SCALE }), +}; + +export const DegreeSize: Story = { + render: (_args, { globals }) => + frame(globals, { nodeColor: CLUSTER_COLOR_SCALE, nodeSize: DEGREE_SIZE_SCALE }), +}; + +const UNIFORM = (_node: { attrs?: DemoNodeAttrs }): number => 4; + +export const UniformSize: Story = { + render: (_args, { globals }) => + frame(globals, { nodeColor: CLUSTER_COLOR_SCALE, nodeSize: UNIFORM }), +}; diff --git a/apps/storybook/src/graph/Styling.stories.tsx b/apps/storybook/src/graph/Styling.stories.tsx deleted file mode 100644 index 6fe03e1..0000000 --- a/apps/storybook/src/graph/Styling.stories.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react-vite'; -import { fn } from 'storybook/test'; -import { DemoGraph, GraphFrame } from '../fixtures/DemoGraph'; -import { cosmosEngine } from '../fixtures/engines'; -import { generateGraph } from '../fixtures/generate'; -import type { DemoNodeAttrs } from '../fixtures/generate'; -import { CLUSTER_COLOR_SCALE, DEGREE_COLOR_SCALE, DEGREE_SIZE_SCALE } from '../fixtures/scales'; -import { themeFromGlobals } from '../fixtures/themes'; - -const data = generateGraph({ - seed: 11, - nodes: 800, - clusters: 6, - intraEdgeFactor: 1.6, - interEdgeProb: 0.06, - datasetKey: 'styling', - sourceRevision: 1, -}); - -const UNIFORM_SIZE = (_node: { attrs?: DemoNodeAttrs }): number => 4; - -interface StylingArgs { - colorBy: 'cluster' | 'degree'; - sizeBy: 'uniform' | 'degree'; - edgeArrows: boolean; - showLinks: boolean; - onNodeClick: ReturnType; - onBackgroundClick: ReturnType; -} - -const meta = { - title: 'Graph/Styling', - parameters: { - docs: { - description: { - component: - 'Node color and size take either a plain per-node function or a declarative ' + - 'Scale descriptor (categorical or sequential over a metric). Scales also feed ' + - 'the legend. Link color/width, arrows, and link visibility are props too.', - }, - }, - }, - args: { - colorBy: 'cluster', - sizeBy: 'degree', - edgeArrows: false, - showLinks: true, - onNodeClick: fn(), - onBackgroundClick: fn(), - }, - argTypes: { - colorBy: { control: 'radio', options: ['cluster', 'degree'] }, - sizeBy: { control: 'radio', options: ['uniform', 'degree'] }, - onNodeClick: { table: { disable: true } }, - onBackgroundClick: { table: { disable: true } }, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const Styling: Story = { - render: (args, { globals }) => { - const active = themeFromGlobals(globals); - return ( - - - - ); - }, -}; diff --git a/apps/storybook/src/graph/Themes.stories.tsx b/apps/storybook/src/graph/Themes.stories.tsx index 6680772..d4c6219 100644 --- a/apps/storybook/src/graph/Themes.stories.tsx +++ b/apps/storybook/src/graph/Themes.stories.tsx @@ -1,7 +1,8 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; +import type { ThemeInput } from '@modernrelay/orbit-core'; import { DemoGraph, GraphFrame } from '../fixtures/DemoGraph'; import { cosmosEngine } from '../fixtures/engines'; -import { generateGraph } from '../fixtures/generate'; +import { sizedCache } from '../fixtures/sizes'; import { BACKGROUND, DARK_THEME, @@ -10,16 +11,9 @@ import { LINK_COLOR, LINK_COLOR_LIGHT, } from '../fixtures/themes'; +import { clustered } from '../fixtures/topologies'; -const data = generateGraph({ - seed: 17, - nodes: 800, - clusters: 6, - intraEdgeFactor: 1.6, - interEdgeProb: 0.06, - datasetKey: 'themes', - sourceRevision: 1, -}); +const data = sizedCache(clustered, 17); interface ThemesArgs { base: 'dark' | 'light'; @@ -31,10 +25,10 @@ const meta = { docs: { description: { component: - 'Themes are partial-over-base inputs: pick the dark or light base and override ' + - 'only the tokens that differ (here: the canvas background). Swapping the theme ' + - 'prop restyles the live scene — no remount. This story drives the theme with ' + - 'its own control and ignores the global toolbar.', + 'Themes are partial-over-base inputs: pick the dark or light base and ' + + 'override only the tokens that differ. Swapping the theme prop restyles ' + + 'the live scene — no remount. This page drives themes with its own ' + + 'controls and ignores the global toolbar.', }, }, }, @@ -49,14 +43,14 @@ const meta = { export default meta; type Story = StoryObj; -export const Themes: Story = { - render: (args) => { +export const BaseSwap: Story = { + render: (args, { globals }) => { const dark = args.base === 'dark'; return ( @@ -64,3 +58,21 @@ export const Themes: Story = { ); }, }; + +/** every token stated: a custom brand theme over the dark base */ +const MIDNIGHT: ThemeInput = { + base: 'dark', + background: '#020617', + nodeDefault: '#7dd3fc', + edgeDefault: 'rgba(125, 211, 252, 0.16)', + accent: '#f472b6', + emphasisRing: '#facc15', +}; + +export const CustomTokens: StoryObj = { + render: (_args, { globals }) => ( + + + + ), +}; diff --git a/apps/storybook/src/graph/Topologies.stories.tsx b/apps/storybook/src/graph/Topologies.stories.tsx new file mode 100644 index 0000000..258ee69 --- /dev/null +++ b/apps/storybook/src/graph/Topologies.stories.tsx @@ -0,0 +1,66 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { DemoGraph, GraphFrame } from '../fixtures/DemoGraph'; +import { cosmosEngine } from '../fixtures/engines'; +import { sizeFromGlobals } from '../fixtures/sizes'; +import { themeFromGlobals } from '../fixtures/themes'; +import { FORCE_TOPOLOGIES } from '../fixtures/topologies'; +import type { TopologyKind } from '../fixtures/topologies'; +import type { DemoSnapshot } from '../fixtures/generate'; + +const cache = new Map(); +function snapFor(kind: TopologyKind, n: number): DemoSnapshot { + const key = `${kind}:${n}`; + let snap = cache.get(key); + if (snap === undefined) { + snap = FORCE_TOPOLOGIES[kind](7, n); + cache.set(key, snap); + } + return snap; +} + +interface TopologyArgs { + topology: TopologyKind; +} + +const meta = { + title: 'Graph/Topologies', + parameters: { + docs: { + description: { + component: + 'The same component, props, scales, and dimensions across very different ' + + 'structures — communities, a tree, a hub-and-spoke network, a bipartite ' + + 'graph, a ring, and disconnected islands. Structure comes from the data; ' + + 'orbit lays it out and keeps every feature working.', + }, + }, + }, + args: { + topology: 'tree', + }, + argTypes: { + topology: { + control: 'radio', + options: ['clustered', 'tree', 'scale-free', 'bipartite', 'ring', 'islands'], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Topologies: Story = { + render: (args, { globals }) => { + const active = themeFromGlobals(globals); + return ( + + + + ); + }, +}; diff --git a/apps/storybook/src/interaction/ContextMenu.stories.tsx b/apps/storybook/src/interaction/ContextMenu.stories.tsx new file mode 100644 index 0000000..daae5cc --- /dev/null +++ b/apps/storybook/src/interaction/ContextMenu.stories.tsx @@ -0,0 +1,58 @@ +import { useRef } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import type { NodeId } from '@modernrelay/orbit-core'; +import { GraphContextMenu } from '@modernrelay/orbit-react/components/ContextMenu'; +import type { GraphHandle } from '@modernrelay/orbit-react'; +import { DemoGraph, GraphFrame } from '../fixtures/DemoGraph'; +import { cosmosEngine } from '../fixtures/engines'; +import type { DemoEdgeAttrs, DemoNodeAttrs } from '../fixtures/generate'; +import { CLUSTER_COLOR_SCALE } from '../fixtures/scales'; +import { sizedCache } from '../fixtures/sizes'; +import { themeFromGlobals } from '../fixtures/themes'; +import { clustered } from '../fixtures/topologies'; + +const data = sizedCache(clustered, 31); + +function ContextMenuDemo(props: { globals: Record }) { + const ref = useRef | null>(null); + const active = themeFromGlobals(props.globals); + const onFindPath = (sourceId: NodeId, targetId: NodeId): void => { + void ref.current?.findPath(sourceId, targetId, { direction: 'either' }).catch(() => {}); + }; + return ( + + + + + + ); +} + +const meta = { + title: 'Interaction/Context menu', + parameters: { + docs: { + description: { + component: + 'Right-click (or long-press) a node for the typed context menu — select, ' + + 'isolate, expand, hide, and a two-step find-path flow: pick "find path ' + + 'from here" on one node, then a target on another, and the shortest path ' + + 'lights up through the path-emphasis lane.', + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const ContextMenu: Story = { + render: (_args, { globals }) => , +}; diff --git a/apps/storybook/src/interaction/HoverEmphasis.stories.tsx b/apps/storybook/src/interaction/HoverEmphasis.stories.tsx new file mode 100644 index 0000000..e7527a3 --- /dev/null +++ b/apps/storybook/src/interaction/HoverEmphasis.stories.tsx @@ -0,0 +1,62 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { fn } from 'storybook/test'; +import { DemoGraph, GraphFrame } from '../fixtures/DemoGraph'; +import { cosmosEngine } from '../fixtures/engines'; +import { CLUSTER_COLOR_SCALE } from '../fixtures/scales'; +import { sizedCache } from '../fixtures/sizes'; +import { themeFromGlobals } from '../fixtures/themes'; +import { clustered } from '../fixtures/topologies'; + +const data = sizedCache(clustered, 29); + +interface HoverArgs { + emphasisRing: boolean; + onNodeHover: ReturnType; + onEdgeHover: ReturnType; +} + +const meta = { + title: 'Interaction/Hover & emphasis', + parameters: { + docs: { + description: { + component: + 'Pointer hover rings the node under the cursor (the emphasis ring — ' + + 'deliberately distinct from selection) and streams typed hover events. ' + + 'Toggle the ring off and hover becomes data-only.', + }, + }, + }, + args: { + emphasisRing: true, + onNodeHover: fn(), + onEdgeHover: fn(), + }, + argTypes: { + onNodeHover: { table: { disable: true } }, + onEdgeHover: { table: { disable: true } }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const HoverEmphasis: Story = { + render: (args, { globals }) => { + const active = themeFromGlobals(globals); + return ( + + + + ); + }, +}; diff --git a/apps/storybook/src/interaction/Selection.stories.tsx b/apps/storybook/src/interaction/Selection.stories.tsx index b77de55..a4540b6 100644 --- a/apps/storybook/src/interaction/Selection.stories.tsx +++ b/apps/storybook/src/interaction/Selection.stories.tsx @@ -2,18 +2,11 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { fn } from 'storybook/test'; import { DemoGraph, GraphFrame } from '../fixtures/DemoGraph'; import { cosmosEngine } from '../fixtures/engines'; -import { generateGraph } from '../fixtures/generate'; +import { sizedCache } from '../fixtures/sizes'; import { themeFromGlobals } from '../fixtures/themes'; +import { clustered } from '../fixtures/topologies'; -const data = generateGraph({ - seed: 33, - nodes: 600, - clusters: 5, - intraEdgeFactor: 1.6, - interEdgeProb: 0.06, - datasetKey: 'selection', - sourceRevision: 1, -}); +const data = sizedCache(clustered, 33); interface SelectionArgs { enableLasso: boolean; @@ -58,7 +51,7 @@ export const Selection: Story = { (); +function snapFor( + kind: string, + maker: (seed: number, n: number) => DemoSnapshot, + n: number, +): DemoSnapshot { + const key = `${kind}:${n}`; + let snap = cache.get(key); + if (snap === undefined) { + snap = maker(7, n); + cache.set(key, snap); + } + return snap; +} + +function frame(globals: Record, snap: DemoSnapshot): ReactElement { + const active = themeFromGlobals(globals); + return ( + + + + ); +} + +const meta = { + title: 'Layouts/Fixed', + parameters: { + docs: { + description: { + component: + 'The fixed layout renders declared x/y exactly — bring positions from any ' + + 'algorithm and orbit draws them pixel-faithfully, no simulation involved. ' + + 'These layouts are computed in fixture code: a circle, a tidy radial tree ' + + '(children fan under parents by subtree share), and a lattice.', + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Circular: Story = { + render: (_args, { globals }) => + frame(globals, snapFor('circular', circularFixed, sizeFromGlobals(globals))), +}; + +export const RadialTree: Story = { + render: (_args, { globals }) => + frame(globals, snapFor('radial', radialTreeFixed, sizeFromGlobals(globals))), +}; + +export const Grid: Story = { + render: (_args, { globals }) => + frame(globals, snapFor('grid', gridFixed, sizeFromGlobals(globals))), +}; diff --git a/apps/storybook/src/layouts/Force.stories.tsx b/apps/storybook/src/layouts/Force.stories.tsx new file mode 100644 index 0000000..482f7f7 --- /dev/null +++ b/apps/storybook/src/layouts/Force.stories.tsx @@ -0,0 +1,98 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { SIMULATION_PRESETS } from '@modernrelay/orbit-core'; +import type { SimulationPreset } from '@modernrelay/orbit-core'; +import { DemoGraph, GraphFrame } from '../fixtures/DemoGraph'; +import { cosmosEngine } from '../fixtures/engines'; +import { sizedCache } from '../fixtures/sizes'; +import { themeFromGlobals } from '../fixtures/themes'; +import { clustered } from '../fixtures/topologies'; + +const data = sizedCache(clustered, 21); + +interface PresetArgs { + preset: SimulationPreset; +} + +const presetMeta = { + title: 'Layouts/Force', + parameters: { + docs: { + description: { + component: + 'The GPU force layout ships measured presets — calm (the default: ' + + 'visually still in about five seconds), spread, tight, and lively (the ' + + "engine's own continuous-motion defaults). A full SimulationConfig is " + + 'accepted anywhere a preset is.', + }, + }, + }, + args: { + preset: 'calm', + }, + argTypes: { + preset: { control: 'radio', options: ['calm', 'spread', 'tight', 'lively'] }, + }, +} satisfies Meta; + +export default presetMeta; +type PresetStory = StoryObj; + +export const Presets: PresetStory = { + render: (args, { globals }) => { + const active = themeFromGlobals(globals); + return ( + + + + ); + }, +}; + +interface TunableArgs { + repulsion: number; + gravity: number; + friction: number; + decay: number; +} + +export const Tunables: StoryObj = { + args: { + repulsion: SIMULATION_PRESETS.calm.repulsion ?? 1.4, + gravity: SIMULATION_PRESETS.calm.gravity ?? 0.15, + friction: SIMULATION_PRESETS.calm.friction ?? 0.6, + decay: SIMULATION_PRESETS.calm.decay ?? 1000, + }, + argTypes: { + repulsion: { control: { type: 'range', min: 0, max: 2.5, step: 0.05 } }, + gravity: { control: { type: 'range', min: 0, max: 1, step: 0.05 } }, + friction: { control: { type: 'range', min: 0.1, max: 1, step: 0.05 } }, + decay: { control: { type: 'range', min: 200, max: 8000, step: 100 } }, + }, + render: (args, { globals }) => { + const active = themeFromGlobals(globals); + return ( + + + + ); + }, +}; diff --git a/apps/storybook/src/layouts/Handoff.stories.tsx b/apps/storybook/src/layouts/Handoff.stories.tsx new file mode 100644 index 0000000..4ae9835 --- /dev/null +++ b/apps/storybook/src/layouts/Handoff.stories.tsx @@ -0,0 +1,76 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import type { LayoutKind } from '@modernrelay/orbit-core'; +import { DemoGraph, GraphFrame } from '../fixtures/DemoGraph'; +import { cosmosEngine } from '../fixtures/engines'; +import { CLUSTER_COLOR_SCALE } from '../fixtures/scales'; +import { sizeFromGlobals } from '../fixtures/sizes'; +import { themeFromGlobals } from '../fixtures/themes'; +import { radialTreeFixed } from '../fixtures/topologies'; +import type { DemoSnapshot } from '../fixtures/generate'; + +const cache = new Map(); +function snapFor(n: number): DemoSnapshot { + let snap = cache.get(n); + if (snap === undefined) { + snap = radialTreeFixed(7, n); + cache.set(n, snap); + } + return snap; +} + +function HandoffDemo(props: { globals: Record }) { + const [layout, setLayout] = useState('fixed'); + const active = themeFromGlobals(props.globals); + return ( + + + + + ); +} + +const meta = { + title: 'Layouts/Fixed to force', + parameters: { + docs: { + description: { + component: + 'Declared positions double as simulation seeds: switch the layout prop ' + + 'from fixed to force and the simulation relaxes FROM the drawn ' + + 'arrangement — no re-seed, no jump. Switch back and the declared design ' + + 'returns. Designed geometry and organic motion are one prop apart.', + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const FixedToForce: Story = { + render: (_args, { globals }) => , +}; From 104638841cecce4043995f157089edd4c54370df Mon Sep 17 00:00:00 2001 From: aaltshuler Date: Thu, 27 Aug 2026 00:37:23 +0300 Subject: [PATCH 3/3] Storybook: force stories reheat on simulation changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review finding: a simulation-only update deliberately preserves positions and carries no restart (core contract), so once the calm preset settled, preset/slider changes showed nothing. The force stories now resume the simulation on config change through the shipped reheat API — verified live: settled graph visibly rearranges on a slider move. --- apps/storybook/src/layouts/Force.stories.tsx | 88 ++++++++++++-------- 1 file changed, 52 insertions(+), 36 deletions(-) diff --git a/apps/storybook/src/layouts/Force.stories.tsx b/apps/storybook/src/layouts/Force.stories.tsx index 482f7f7..d21005a 100644 --- a/apps/storybook/src/layouts/Force.stories.tsx +++ b/apps/storybook/src/layouts/Force.stories.tsx @@ -1,7 +1,10 @@ +import { useEffect, useRef } from 'react'; +import type { ReactElement } from 'react'; import type { Meta, StoryObj } from '@storybook/react-vite'; import { SIMULATION_PRESETS } from '@modernrelay/orbit-core'; -import type { SimulationPreset } from '@modernrelay/orbit-core'; +import type { SimulationInput, SimulationPreset } from '@modernrelay/orbit-core'; import { DemoGraph, GraphFrame } from '../fixtures/DemoGraph'; +import type { DemoGraphHandle } from '../fixtures/DemoGraph'; import { cosmosEngine } from '../fixtures/engines'; import { sizedCache } from '../fixtures/sizes'; import { themeFromGlobals } from '../fixtures/themes'; @@ -9,6 +12,42 @@ import { clustered } from '../fixtures/topologies'; const data = sizedCache(clustered, 21); +/** + * A simulation-only update deliberately preserves positions and does NOT + * restart the engine (core contract) — so once the calm preset settles, + * changing a value would show nothing. The host-side answer is the shipped + * reheat API: resume the simulation when the config changes. + */ +function ReheatingGraph(props: { + globals: Record; + simulation: SimulationInput; +}): ReactElement { + const ref = useRef(null); + const simKey = JSON.stringify(props.simulation); + const first = useRef(true); + useEffect(() => { + if (first.current) { + first.current = false; + return; + } + ref.current?.instance.resumeSimulation(); + }, [simKey]); + const active = themeFromGlobals(props.globals); + return ( + + + + ); +} + interface PresetArgs { preset: SimulationPreset; } @@ -38,21 +77,7 @@ export default presetMeta; type PresetStory = StoryObj; export const Presets: PresetStory = { - render: (args, { globals }) => { - const active = themeFromGlobals(globals); - return ( - - - - ); - }, + render: (args, { globals }) => , }; interface TunableArgs { @@ -75,24 +100,15 @@ export const Tunables: StoryObj = { friction: { control: { type: 'range', min: 0.1, max: 1, step: 0.05 } }, decay: { control: { type: 'range', min: 200, max: 8000, step: 100 } }, }, - render: (args, { globals }) => { - const active = themeFromGlobals(globals); - return ( - - - - ); - }, + render: (args, { globals }) => ( + + ), };