Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"updateInternalDependencies": "patch",
"ignore": [
"orbit-demo",
"orbit-spike"
"orbit-spike",
"orbit-storybook"
]
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ packages/omnigraph/test/__generated__/

# Local-only probe/perf evidence (never committed)
.evidence/
apps/storybook/storybook-static/
15 changes: 15 additions & 0 deletions apps/storybook/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -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'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Selection actions panel is unavailable

When users perform the selection interactions documented by the new story, the callbacks execute but this configuration registers only the docs addon, so Storybook provides no Actions panel in which to inspect the promised onSelectionChange events.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not applicable on Storybook 10: controls and actions moved into core (addon-essentials no longer exists), so the Actions panel is present with only the docs addon registered — fn() spies from storybook/test log there. Verified in the running catalog: the Selection story shows Controls/Actions/Interactions tabs and onSelectionChange events stream into the Actions panel.

framework: {
name: '@storybook/react-vite',
options: {},
},
core: {
disableTelemetry: true,
},
};

export default config;
61 changes: 61 additions & 0 deletions apps/storybook/.storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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', 'Topologies', 'Nodes', 'Edges', 'Labels', 'Themes'],
'Layouts',
['Force', 'Fixed', 'Fixed to force'],
'Interaction',
['Selection', 'Hover & emphasis', 'Context menu'],
'Exploration',
'Analytics',
'Components',
'Persistence',
'Scale',
],
},
},
},
tags: ['autodocs'],
globalTypes: {
theme: {
description: 'Graph theme',
toolbar: {
icon: 'mirror',
items: ['dark', 'light'],
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',
},
};

export default preview;
29 changes: 29 additions & 0 deletions apps/storybook/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
40 changes: 40 additions & 0 deletions apps/storybook/src/fixtures/DemoGraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Concrete alias over the generic <Graph<N, E>> 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 { forwardRef } from 'react';
import type { ReactNode } from 'react';
import { Graph } from '@modernrelay/orbit-react';
import type { GraphHandle, GraphProps } from '@modernrelay/orbit-react';
import type { DemoEdgeAttrs, DemoNodeAttrs } from './generate';

export type DemoGraphProps = GraphProps<DemoNodeAttrs, DemoEdgeAttrs>;
export type DemoGraphHandle = GraphHandle<DemoNodeAttrs, DemoEdgeAttrs>;

export const DemoGraph = forwardRef<DemoGraphHandle, DemoGraphProps>(function DemoGraph(
props,
ref,
) {
return <Graph<DemoNodeAttrs, DemoEdgeAttrs> 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. */
export function GraphFrame(props: { background: string; children: ReactNode }): ReactNode {
return (
<div
style={{
height: 520,
width: '100%',
position: 'relative',
background: props.background,
borderRadius: 8,
overflow: 'hidden',
}}
>
{props.children}
</div>
);
}
51 changes: 51 additions & 0 deletions apps/storybook/src/fixtures/config.ts
Original file line number Diff line number Diff line change
@@ -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<DemoNodeAttrs>): string =>
node.attrs?.label ?? node.id;

/** label lane: zoom-LOD at 1.2, ranked cap 48, cluster hubs forced. */
export const LABELS: LabelConfig<DemoNodeAttrs> = {
minZoom: 1.2,
maxVisible: 48,
showFor: ['n0', 'n1'],
getText: labelOf,
};

export const ACCESSIBILITY: AccessibilityConfig<DemoNodeAttrs> = {
label: 'orbit storybook graph',
getAccessibleLabel: labelOf,
};

/** construction-only: <Graph> reads searchIndex once at mount. */
export const SEARCH_INDEX: readonly string[] = ['label'];

export const SCORE_DIM: DimensionSpec<DemoNodeAttrs> = {
key: 'score',
kind: 'numeric',
bins: 30,
get: (node) => node.attrs?.score,
};

export const CREATED_DIM: DimensionSpec<DemoNodeAttrs> = {
key: 'createdAt',
kind: 'temporal',
bins: 60,
get: (node) => node.attrs?.createdAt,
};

export const CROSSFILTER_DIMS: readonly DimensionSpec<DemoNodeAttrs>[] = [SCORE_DIM, CREATED_DIM];
31 changes: 31 additions & 0 deletions apps/storybook/src/fixtures/engines.ts
Original file line number Diff line number Diff line change
@@ -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 <Graph engine> — 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,
};
}
Loading
Loading