import type { Meta, StoryObj } from '@storybook/react'; import React from 'react'; import { NetworkGraph, type NetworkGraphControl } from './network-graph'; import { GraphNodeList } from './graph-node-list'; import { Button } from '../button'; import type { GraphEdge, GraphNode } from './graph.types'; /* -------------------------------------------------------------------------- */ /* A small judicial-process ontology. */ /* -------------------------------------------------------------------------- */ const ONTOLOGY_NODES: GraphNode[] = [ { id: 'processo', label: 'Processo', type: 'Classe', weight: 3 }, { id: 'documento', label: 'Documento', type: 'Classe', weight: 2.4 }, { id: 'parte', label: 'Parte', type: 'Classe', weight: 2.4 }, { id: 'orgao', label: 'Órgão julgador', type: 'Classe', weight: 2 }, { id: 'movimento', label: 'Movimento', type: 'Classe', weight: 2 }, { id: 'peticao', label: 'Petição inicial', type: 'Subclasse' }, { id: 'sentenca', label: 'Sentença', type: 'Subclasse' }, { id: 'despacho', label: 'Despacho', type: 'Subclasse' }, { id: 'autor', label: 'Autor', type: 'Subclasse' }, { id: 'reu', label: 'Réu', type: 'Subclasse' }, { id: 'advogado', label: 'Advogado', type: 'Subclasse' }, { id: 'vara', label: '3ª Vara Cível', type: 'Indivíduo' }, { id: 'proc-0001', label: 'Processo 0001-23', type: 'Indivíduo' }, { id: 'joana', label: 'Joana Ribeiro', type: 'Indivíduo' }, ]; const ONTOLOGY_EDGES: GraphEdge[] = [ { source: 'peticao', target: 'documento', label: 'é subclasse de', relation: 'is-a' }, { source: 'sentenca', target: 'documento', label: 'é subclasse de', relation: 'is-a' }, { source: 'despacho', target: 'documento', label: 'é subclasse de', relation: 'is-a' }, { source: 'autor', target: 'parte', label: 'é subclasse de', relation: 'is-a' }, { source: 'reu', target: 'parte', label: 'é subclasse de', relation: 'is-a' }, { source: 'advogado', target: 'parte', label: 'é subclasse de', relation: 'is-a' }, { source: 'documento', target: 'processo', label: 'compõe', relation: 'part-of' }, { source: 'movimento', target: 'processo', label: 'compõe', relation: 'part-of' }, { source: 'parte', target: 'processo', label: 'figura em', relation: 'association' }, { source: 'processo', target: 'orgao', label: 'tramita em', relation: 'association' }, { source: 'vara', target: 'orgao', label: 'é instância de', relation: 'instance-of' }, { source: 'proc-0001', target: 'processo', label: 'é instância de', relation: 'instance-of' }, { source: 'joana', target: 'autor', label: 'é instância de', relation: 'instance-of' }, { source: 'proc-0001', target: 'vara', label: 'tramita em', relation: 'association' }, { source: 'joana', target: 'proc-0001', label: 'figura em', relation: 'association' }, ]; /** A denser synthetic graph, for judging behaviour at scale. */ function buildLargeGraph(size: number) { const types = ['Pessoa', 'Empresa', 'Endereço', 'Veículo']; const nodes: GraphNode[] = Array.from({ length: size }, (_, i) => ({ id: `n${i}`, label: `Vínculo ${i}`, type: types[i % types.length], })); const edges: GraphEdge[] = []; for (let i = 1; i < size; i += 1) { // Attach each node to an earlier one, plus the occasional cross link. edges.push({ source: `n${i}`, target: `n${Math.floor(i / 3)}`, relation: 'vínculo' }); if (i % 11 === 0) edges.push({ source: `n${i}`, target: `n${(i * 7) % size}`, relation: 'coincidência' }); } return { nodes, edges }; } const meta: Meta = { title: 'UI/NetworkGraph', component: NetworkGraph, parameters: { layout: 'centered' }, argTypes: { layout: { control: 'inline-radio', options: ['3d', '2d'] }, labels: { control: 'inline-radio', options: ['auto', 'always', 'hover', 'none'] }, focusMode: { control: 'inline-radio', options: ['neighbors', 'none'] }, nodeRadius: { control: { type: 'range', min: 2, max: 20 } }, directed: { control: 'boolean' }, autoRotate: { control: 'boolean' }, }, render: args => (

Arraste para orbitar, role para aproximar, clique para selecionar. Com foco no canvas: setas orbitam, Tab percorre os nós, Esc limpa.

), }; export default meta; type Story = StoryObj; /** A judicial-process ontology: classes, subclasses and individuals. */ export const Ontology: Story = { args: { nodes: ONTOLOGY_NODES, edges: ONTOLOGY_EDGES, height: '520px', directed: true, ariaLabel: 'Ontologia de processo judicial', }, }; /** Flattened to two dimensions, where dragging pans instead of orbiting. */ export const TwoDimensional: Story = { args: { nodes: ONTOLOGY_NODES, edges: ONTOLOGY_EDGES, layout: '2d', height: '520px', directed: true, labels: 'always', }, }; /** * Selecting a node dims everything outside its immediate neighbourhood — the * only practical way to read a dense graph. */ export const FocusNeighbourhood: Story = { render: function FocusStory(args) { const [selectedId, setSelectedId] = React.useState('documento'); return (

Selecionado:{' '} {ONTOLOGY_NODES.find(n => n.id === selectedId)?.label ?? 'nenhum'}

); }, args: { nodes: ONTOLOGY_NODES, edges: ONTOLOGY_EDGES, height: '520px', directed: true, focusMode: 'neighbors', }, }; /** * The canvas paired with its accessible list. Selection flows both ways, and the * list states each relationship in words. */ export const WithAccessibleList: Story = { render: function PairedStory(args) { const [selectedId, setSelectedId] = React.useState(null); return (
); }, args: { nodes: ONTOLOGY_NODES, edges: ONTOLOGY_EDGES, height: '520px', directed: true, }, }; /** Camera driven from outside the component. */ export const ImperativeCamera: Story = { render: function CameraStory(args) { const control = React.useRef(null); return (
); }, args: { nodes: ONTOLOGY_NODES, edges: ONTOLOGY_EDGES, height: '480px', directed: true }, }; /** Two thousand nodes, to judge the layout and the frame budget at scale. */ export const AtScale: Story = { args: { ...buildLargeGraph(2000), height: '560px', nodeRadius: 3, labels: 'hover', focusMode: 'neighbors', ariaLabel: 'Grafo de vínculos', }, }; /** Slow rotation, suppressed automatically under reduced motion. */ export const AutoRotate: Story = { args: { nodes: ONTOLOGY_NODES, edges: ONTOLOGY_EDGES, height: '480px', autoRotate: true }, }; export const Empty: Story = { args: { nodes: [], edges: [], height: '320px' }, };