Skip to content

@adecore/diagram ​

Directed graphs that are written rather than drawn. A diagram file says which nodes exist, which groups hold them and what points at what; the package computes where everything goes, routes the edges, and writes the result as SVG or as lines of text. The same file always gives the same picture, so an agent can write a diagram without placing a single box.

sh
bun add @adecore/diagram
ts
import { layoutOf, readingOrder, toSvg } from '@adecore/diagram';
import { diagramProblemIn, migrateDiagram } from '@adecore/diagram/protocol';

const diagram = migrateDiagram(JSON.parse(file));
const problem = diagram && diagramProblemIn(diagram);
if (diagram === null || problem) {
    throw new Error(problem ?? 'Not a diagram');
}

const layout = layoutOf(diagram);
const svg = toSvg(diagram, { layout });
const lines = readingOrder(diagram);
tsx
import { useState } from 'react';
import { ArrowDown, ArrowRight } from 'lucide-react';
import { layoutOf, readingOrder, toSvg } from '@adecore/diagram';
import type { DiagramContent, DiagramDirection } from '@adecore/diagram/protocol';
import { Segmented } from '@adecore/ui';
import { THEME_PALETTE, THEME_PAPER } from '../shared/canvas-theme.ts';

const RELEASE: DiagramContent = {
    meta: { title: 'Release', direction: 'right' },
    nodes: [
        { id: 'commit', label: 'Commit', shape: 'pill', tone: 'muted' },
        { id: 'build', label: 'Build', sub: 'bun run build' },
        { id: 'test', label: 'Test', sub: 'bun test' },
        { id: 'approved', label: 'Approved?', shape: 'diamond', tone: 'orange' },
        { id: 'publish', label: 'Publish', shape: 'round', tone: 'green' },
        { id: 'registry', label: 'Registry', shape: 'cylinder', tone: 'blue' },
        { id: 'docs', label: 'Docs site', shape: 'round', tone: 'purple' }
    ],
    groups: [{ id: 'ci', label: 'CI', wraps: ['build', 'test'], tone: 'accent' }],
    edges: [
        { from: 'commit', to: 'build' },
        { from: 'build', to: 'test' },
        { from: 'test', to: 'approved', label: 'green' },
        { from: 'approved', to: 'publish', label: 'yes' },
        { from: 'approved', to: 'commit', label: 'changes', style: 'dashed', tone: 'red' },
        { from: 'publish', to: 'registry' },
        { from: 'publish', to: 'docs', style: 'dotted' }
    ]
};

export default function DiagramLayoutDemo() {
    const [direction, setDirection] = useState<DiagramDirection>('right');
    const content = { ...RELEASE, meta: { ...RELEASE.meta, direction } };
    const layout = layoutOf(content);
    const svg = toSvg(content, { layout, palette: THEME_PALETTE, paper: THEME_PAPER });

    return (
        <div className="flex w-full flex-col gap-4">
            <Segmented<DiagramDirection>
                label="Direction"
                value={direction}
                onValueChange={setDirection}
                options={[
                    { id: 'right', label: 'Right', icon: ArrowRight },
                    { id: 'down', label: 'Down', icon: ArrowDown }
                ]}
                className="self-start"
            />
            <div
                className="flex justify-center [&_svg]:h-auto [&_svg]:max-h-[480px] [&_svg]:w-auto [&_svg]:max-w-full"
                dangerouslySetInnerHTML={{ __html: svg }}
            />
            <ol className="flex flex-col gap-1 font-mono text-code text-text-muted">
                {readingOrder(content).map((line) => (
                    <li key={line}>{line}</li>
                ))}
            </ol>
        </div>
    );
}

The dashed edge from Approved? back to Commit closes a cycle. The layout ranks the nodes without it and still draws it. Under the diagram are the lines readingOrder writes for the same file.

What is in it ​

  • Documents: the schemas of nodes, groups and edges on @adecore/diagram/protocol, and diagramProblemIn, which checks what a schema cannot.
  • Layout: how layoutOf ranks, orders and places nodes, wraps groups around them and routes edges, and what a node a person dragged does.
  • Painting and reading: toSvg, the shape and text helpers for a painter of your own, and readingOrder.

The package depends on @adecore/drawing for its palette names, its Point and Rect and its default colors, so a diagram follows a theme the way a drawing does. The protocol entry point loads Zod and the drawing protocol, without the layout or the renderer.

What the app does ​

The package keeps no state. The app owns storage and the rev check on save, dragging (which writes a node's pos), selection, and the colors and font it paints in.

Licenses and third-party notices are listed in each package.