@ship-it-ui/cytoscape 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -34,6 +34,7 @@ __export(index_exports, {
34
34
  module.exports = __toCommonJS(index_exports);
35
35
 
36
36
  // src/stylesheet.ts
37
+ var import_icons = require("@ship-it-ui/icons");
37
38
  var import_shipit2 = require("@ship-it-ui/shipit");
38
39
 
39
40
  // src/theme-tokens.ts
@@ -205,7 +206,7 @@ function buildShipItStylesheet(options = {}) {
205
206
  selector: `node[entityType = "${escapeCytoscapeAttr(type)}"]`,
206
207
  style: renderGlyphs ? {
207
208
  "border-color": c,
208
- "background-image": glyphDataUrl(meta.glyph, c),
209
+ "background-image": (0, import_icons.iconToSvgDataUrl)(meta.iconName, { color: c }),
209
210
  "background-fit": "contain",
210
211
  "background-clip": "none"
211
212
  } : { "border-color": c }
@@ -265,22 +266,6 @@ var GRAPH_CANVAS_CLASS = {
265
266
  function escapeCytoscapeAttr(value) {
266
267
  return value.replace(/[\\"]/g, (c) => `\\${c}`);
267
268
  }
268
- var XML_ESCAPES = {
269
- "<": "&lt;",
270
- ">": "&gt;",
271
- "&": "&amp;",
272
- '"': "&quot;",
273
- "'": "&apos;"
274
- };
275
- function escapeXml(value) {
276
- return value.replace(/[<>&"']/g, (c) => XML_ESCAPES[c] ?? c);
277
- }
278
- function glyphDataUrl(glyph, color) {
279
- const safeGlyph = escapeXml(glyph);
280
- const safeColor = escapeXml(color);
281
- const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='52' height='52' viewBox='0 0 52 52'><text x='26' y='34' text-anchor='middle' font-family='ui-monospace,SFMono-Regular,monospace' font-size='26' fill='${safeColor}'>${safeGlyph}</text></svg>`;
282
- return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
283
- }
284
269
 
285
270
  // src/useShipItStylesheet.ts
286
271
  var import_react = require("react");
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/stylesheet.ts","../src/theme-tokens.ts","../src/useShipItStylesheet.ts","../src/GraphCanvas.tsx"],"sourcesContent":["/**\n * @ship-it-ui/cytoscape — Cytoscape adapter for the Ship-It design system.\n *\n * Three layered exports:\n * - {@link buildShipItStylesheet} — token-driven stylesheet array.\n * - {@link useShipItStylesheet} — theme-aware re-resolver hook.\n * - {@link GraphCanvas} — `<GraphCanvas>` React wrapper that\n * owns the data-theme ↔ stylesheet ↔\n * inspector dance.\n *\n * Cytoscape is a peer dependency — pass `engine={cytoscape}` so the consumer\n * controls the version and any registered extensions.\n */\n\nexport {\n buildShipItStylesheet,\n GRAPH_CANVAS_CLASS,\n type BuildStylesheetOptions,\n type ShipItStylesheetBlock,\n} from './stylesheet';\n\nexport {\n useShipItStylesheet,\n type UseShipItStylesheetOptions,\n type UseShipItStylesheetReturn,\n} from './useShipItStylesheet';\n\nexport {\n GraphCanvas,\n type GraphCanvasProps,\n type GraphCanvasHandle,\n type CytoscapeEngine,\n} from './GraphCanvas';\n\nexport {\n readThemeTokens,\n resolveCssVar,\n resolveColorReference,\n resolveEntityColor,\n type ThemeTokenPalette,\n} from './theme-tokens';\n","import { listEntityTypes } from '@ship-it-ui/shipit';\nimport type cytoscape from 'cytoscape';\n\nimport { readThemeTokens, resolveColorReference, type ThemeTokenPalette } from './theme-tokens';\n\n/**\n * Build a Cytoscape stylesheet from the live design tokens. The result is a\n * plain JSON array suitable for `cytoscape({ style: ... })` — re-run after a\n * `data-theme` change to pick up the new palette.\n *\n * The stylesheet is opinionated:\n * - Nodes are square-ish rounded glyphs colored by `data(entityType)`.\n * - Edges are token-colored thin lines with arrowheads.\n * - Selected / on-path / dimmed states are driven by class names that the\n * consumer toggles (`graph-canvas:selected`, `graph-canvas:path`,\n * `graph-canvas:dim`).\n *\n * Pass `palette` to override the token read — useful for SSR or tests.\n */\n\nexport interface BuildStylesheetOptions {\n /** Pre-resolved palette. When omitted, tokens are read from the document. */\n palette?: ThemeTokenPalette;\n /**\n * Additional entries appended to the stylesheet — handy for app-specific\n * selectors without forking the builder.\n */\n extra?: ReadonlyArray<cytoscape.StylesheetJsonBlock>;\n /**\n * Render each registered entity type's glyph (◇, ○, ▤, ↑, ◎, ▢ …) inside\n * the cytoscape node as a centered SVG data URL. Closes the visual gap\n * with the `<GraphNode>` React component — the docs page and the canvas\n * now share a vocabulary. Defaults to `true`. Pass `false` to fall back\n * to the original wireframe (border-only) per-type rule.\n */\n renderGlyphs?: boolean;\n}\n\n// Re-export the block type so consumers can declare typed `extra` entries.\nexport type ShipItStylesheetBlock = cytoscape.StylesheetJsonBlock;\n\nexport function buildShipItStylesheet(\n options: BuildStylesheetOptions = {},\n): cytoscape.StylesheetJson {\n const palette = options.palette ?? readThemeTokens();\n const color = (cssVar: string) => resolveColorReference(cssVar, palette);\n const renderGlyphs = options.renderGlyphs !== false;\n\n const base: cytoscape.StylesheetJsonBlock[] = [\n {\n selector: 'node',\n style: {\n 'background-color': palette.panel,\n 'border-width': 1.5,\n 'border-color': palette.accent,\n 'border-opacity': 1,\n label: 'data(label)',\n color: palette.textMuted,\n // Static stack instead of `var(--font-mono, monospace)` — cytoscape\n // can't resolve CSS variables outside the DOM cascade and emits a\n // warning per node selector at every mount. Consumers who need to\n // override the canvas font can do so via `options.extra`.\n 'font-family': 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',\n 'font-size': 10,\n 'text-valign': 'bottom',\n 'text-halign': 'center',\n 'text-margin-y': 6,\n // 52 matches `<GraphNode>`'s default size — the docs page and the\n // canvas now share dimensions. Pre-Issue-4 the canvas was 36×36.\n width: 52,\n height: 52,\n shape: 'round-rectangle',\n },\n },\n // One selector per entity type registered with @ship-it-ui/shipit. Built-in\n // types are seeded automatically; custom types registered via\n // `registerEntityType(...)` pick up their `colorVar` here without a docs\n // patch or a forked stylesheet.\n ...listEntityTypes().map<cytoscape.StylesheetJsonBlock>(([type, meta]) => {\n const c = color(meta.colorVar);\n return {\n selector: `node[entityType = \"${escapeCytoscapeAttr(type)}\"]`,\n style: renderGlyphs\n ? {\n 'border-color': c,\n 'background-image': glyphDataUrl(meta.glyph, c),\n 'background-fit': 'contain',\n 'background-clip': 'none',\n }\n : { 'border-color': c },\n };\n }),\n {\n selector: 'node:selected',\n style: {\n 'border-width': 3,\n 'overlay-color': palette.accent,\n 'overlay-opacity': 0.15,\n 'overlay-padding': 4,\n },\n },\n {\n selector: 'node.graph-canvas\\\\:path',\n style: { 'border-color': palette.purple },\n },\n {\n selector: 'node.graph-canvas\\\\:dim',\n style: { opacity: 0.35 },\n },\n {\n // Default edges tone with `accent` so they read against the panel\n // background — matches `<GraphEdge edgeStyle=\"solid\">` in the docs.\n // Pre-fix the line drew in `palette.border` (the same tone as\n // surface-divider lines) and effectively disappeared on dark themes.\n selector: 'edge',\n style: {\n width: 1,\n 'line-color': palette.accent,\n 'target-arrow-color': palette.accent,\n 'target-arrow-shape': 'triangle',\n 'arrow-scale': 0.8,\n 'curve-style': 'bezier',\n },\n },\n {\n selector: 'edge.graph-canvas\\\\:path',\n style: {\n 'line-color': palette.purple,\n 'target-arrow-color': palette.purple,\n width: 1.5,\n },\n },\n {\n selector: 'edge.graph-canvas\\\\:dim',\n style: { opacity: 0.25 },\n },\n ];\n\n return [...base, ...(options.extra ?? [])] as cytoscape.StylesheetJson;\n}\n\n/**\n * Convenience class names the stylesheet recognizes. Toggle these on Cytoscape\n * nodes / edges to drive the on-path and dimmed visuals.\n */\nexport const GRAPH_CANVAS_CLASS = {\n path: 'graph-canvas:path',\n dim: 'graph-canvas:dim',\n} as const;\n\n// Cytoscape's attribute selector accepts a double-quoted string. Escape\n// embedded backslashes and double quotes so a malformed (or hostile)\n// registered key can't break out of the selector. The grammar\n// (https://js.cytoscape.org/#selectors/data) is more permissive than CSS, but\n// these two characters are the only ones that would change selector semantics.\nfunction escapeCytoscapeAttr(value: string): string {\n return value.replace(/[\\\\\"]/g, (c) => `\\\\${c}`);\n}\n\nconst XML_ESCAPES: Record<string, string> = {\n '<': '&lt;',\n '>': '&gt;',\n '&': '&amp;',\n '\"': '&quot;',\n \"'\": '&apos;',\n};\n\nfunction escapeXml(value: string): string {\n return value.replace(/[<>&\"']/g, (c) => XML_ESCAPES[c] ?? c);\n}\n\n/**\n * Build a `data:image/svg+xml;...` URL containing the entity glyph centered\n * in a 52×52 viewBox, filled with `color`. Cytoscape draws this as the\n * node's `background-image`, on top of the panel-colored fill, beneath the\n * entity-color border. Glyphs are single unicode characters from the\n * `EntityTypeMeta.glyph` registry (◇ ○ ▤ ↑ ◎ ▢ …).\n *\n * `y='34'` lands the visual centre of typical box-drawing / shape glyphs on\n * the geometric centre of the node — `y='26'` (true centre) sits the\n * baseline at the centre and the glyph reads as if it's floating above.\n */\nfunction glyphDataUrl(glyph: string, color: string): string {\n const safeGlyph = escapeXml(glyph);\n const safeColor = escapeXml(color);\n const svg =\n `<svg xmlns='http://www.w3.org/2000/svg' width='52' height='52' viewBox='0 0 52 52'>` +\n `<text x='26' y='34' text-anchor='middle' ` +\n `font-family='ui-monospace,SFMono-Regular,monospace' ` +\n `font-size='26' fill='${safeColor}'>${safeGlyph}</text>` +\n `</svg>`;\n return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;\n}\n","/**\n * Token resolution helpers. The Ship-It design system stores colors as CSS\n * custom properties on `<html>` (`--color-accent`, `--color-bg`, …). Cytoscape\n * renders to a canvas / SVG layer outside Tailwind, so those vars never\n * resolve — we read the *computed* values at runtime and feed them into the\n * stylesheet builder as concrete color strings.\n *\n * Two layers:\n * 1. `resolveCssVar` — single-var reader with a fallback.\n * 2. `readThemeTokens` — pulls every color the stylesheet uses, returning a\n * flat `Record<string, string>` keyed by short name (no `--color-`\n * prefix). Re-running this after a `data-theme` flip yields a fresh\n * palette.\n */\n\nimport { getEntityTypeMeta, type EntityType } from '@ship-it-ui/shipit';\n\nconst DEFAULT_FALLBACK: Record<string, string> = {\n bg: '#0a0a0a',\n panel: '#0f0f0f',\n 'panel-2': '#161616',\n border: '#262626',\n 'border-strong': '#383838',\n text: '#fafafa',\n 'text-muted': '#a3a3a3',\n 'text-dim': '#737373',\n accent: '#3b82f6',\n ok: '#10b981',\n warn: '#f59e0b',\n err: '#ef4444',\n purple: '#a855f7',\n pink: '#ec4899',\n};\n\n/**\n * Read a single CSS variable from the document root and return its trimmed\n * computed value, falling back to `fallback` when the document is missing\n * (SSR) or the variable is unset.\n */\nexport function resolveCssVar(name: string, fallback = ''): string {\n if (typeof document === 'undefined') return fallback;\n const raw = getComputedStyle(document.documentElement).getPropertyValue(name);\n const trimmed = raw.trim();\n return trimmed.length > 0 ? trimmed : fallback;\n}\n\n// Lazily-created 1×1 canvas used by `toSrgb` to coerce browser-parseable color\n// strings (oklch, lab, lch, named, etc.) into a plain `rgb()` / `rgba()` that\n// cytoscape's color parser accepts. Created on first use; the same canvas is\n// reused across calls so we don't allocate per token.\nlet coerceCanvas: HTMLCanvasElement | null = null;\n\n/**\n * Coerce an arbitrary CSS color string into an sRGB `rgb()` / `rgba()` string\n * via canvas pixel readback. Cytoscape's color parser doesn't accept modern\n * color functions (`oklch(...)`, `lab(...)`), and Tailwind v4 compiles every\n * `--color-*` token to `oklch(...)` — so when `readThemeTokens` reads those\n * tokens with `getComputedStyle`, the resulting palette would produce 70+\n * console warnings and silently fall back to defaults on every cytoscape\n * mount. Coercing once at the boundary fixes both the warnings and the\n * fallback colors.\n *\n * The implementation deliberately uses `fillRect` + `getImageData` rather\n * than reading `ctx.fillStyle` back — modern Chromium returns the literal\n * `oklch(...)` string from `ctx.fillStyle`, so the readback is the only\n * reliable way to force the browser's color pipeline to rasterize the value\n * down to sRGB.\n */\nexport function toSrgb(value: string): string {\n if (!value || typeof document === 'undefined') return value;\n // Fast path: already a parser-friendly value. Skips both the canvas\n // allocation and the readback round-trip for the common case where tokens\n // are already authored as `#xxx`, `rgb(...)`, `rgba(...)`, or `hsl(...)`.\n if (/^#|^rgb\\(|^rgba\\(|^hsl\\(|^hsla\\(/i.test(value)) return value;\n coerceCanvas ??= document.createElement('canvas');\n coerceCanvas.width = 1;\n coerceCanvas.height = 1;\n const ctx = coerceCanvas.getContext('2d', { willReadFrequently: true });\n if (!ctx) return value;\n ctx.clearRect(0, 0, 1, 1);\n // Two assignments so an unparseable `value` falls back to the seeded `#000`\n // instead of inheriting a stale fillStyle from a previous call.\n ctx.fillStyle = '#000';\n ctx.fillStyle = value;\n ctx.fillRect(0, 0, 1, 1);\n const data = ctx.getImageData(0, 0, 1, 1).data;\n const r = data[0];\n const g = data[1];\n const b = data[2];\n const a = data[3];\n if (r === undefined || g === undefined || b === undefined || a === undefined) return value;\n return a === 255 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${a / 255})`;\n}\n\nexport interface ThemeTokenPalette {\n /** Surface backgrounds. */\n bg: string;\n panel: string;\n panel2: string;\n /** Hairline + emphasis border. */\n border: string;\n borderStrong: string;\n /** Foreground tiers. */\n text: string;\n textMuted: string;\n textDim: string;\n /** Brand + status. */\n accent: string;\n ok: string;\n warn: string;\n err: string;\n /** Extras the graph uses for entity-type ring colors. */\n purple: string;\n pink: string;\n}\n\n/**\n * Read the canonical Ship-It color tokens from the document root, coerced to\n * sRGB. The `toSrgb` wrapper exists because Tailwind v4 compiles every\n * `--color-*` token to `oklch(...)`, which cytoscape's color parser rejects —\n * see {@link toSrgb} for the full reasoning.\n */\nexport function readThemeTokens(): ThemeTokenPalette {\n return {\n bg: toSrgb(resolveCssVar('--color-bg', DEFAULT_FALLBACK.bg)),\n panel: toSrgb(resolveCssVar('--color-panel', DEFAULT_FALLBACK.panel)),\n panel2: toSrgb(resolveCssVar('--color-panel-2', DEFAULT_FALLBACK['panel-2'])),\n border: toSrgb(resolveCssVar('--color-border', DEFAULT_FALLBACK.border)),\n borderStrong: toSrgb(resolveCssVar('--color-border-strong', DEFAULT_FALLBACK['border-strong'])),\n text: toSrgb(resolveCssVar('--color-text', DEFAULT_FALLBACK.text)),\n textMuted: toSrgb(resolveCssVar('--color-text-muted', DEFAULT_FALLBACK['text-muted'])),\n textDim: toSrgb(resolveCssVar('--color-text-dim', DEFAULT_FALLBACK['text-dim'])),\n accent: toSrgb(resolveCssVar('--color-accent', DEFAULT_FALLBACK.accent)),\n ok: toSrgb(resolveCssVar('--color-ok', DEFAULT_FALLBACK.ok)),\n warn: toSrgb(resolveCssVar('--color-warn', DEFAULT_FALLBACK.warn)),\n err: toSrgb(resolveCssVar('--color-err', DEFAULT_FALLBACK.err)),\n purple: toSrgb(resolveCssVar('--color-purple', DEFAULT_FALLBACK.purple)),\n pink: toSrgb(resolveCssVar('--color-pink', DEFAULT_FALLBACK.pink)),\n };\n}\n\n/**\n * Resolve the concrete color for a registered entity type. Reads the type's\n * `colorVar` (a `var(--color-…)` string) and looks the value up in the\n * palette. Falls back to the palette's `accent` color when the var is\n * malformed or unknown.\n */\nexport function resolveEntityColor(type: EntityType, palette: ThemeTokenPalette): string {\n const meta = getEntityTypeMeta(type);\n return resolveColorReference(meta.colorVar, palette);\n}\n\n/**\n * Extract `foo` from `var(--color-foo)` or `var(--color-foo, fallback)`.\n * Returns `undefined` if the input doesn't match. O(n), no backtracking.\n */\nfunction parseColorVarName(value: string): string | undefined {\n // Strip surrounding whitespace once. `String#trim` is linear, not a regex.\n let i = 0;\n while (i < value.length && isWhitespace(value.charCodeAt(i))) i++;\n if (!value.startsWith('var(', i)) return undefined;\n i += 4;\n // Skip whitespace inside the parens.\n while (i < value.length && isWhitespace(value.charCodeAt(i))) i++;\n if (!value.startsWith('--color-', i)) return undefined;\n i += 8;\n // Read the name: stop at the first whitespace, comma, or `)`.\n const start = i;\n while (i < value.length) {\n const c = value.charCodeAt(i);\n if (c === CC_COMMA || c === CC_PAREN_CLOSE || isWhitespace(c)) break;\n i++;\n }\n if (i === start) return undefined;\n // Require a closing `)` somewhere after, so we don't classify malformed\n // inputs (`var(--color-foo`) as valid references.\n const close = value.indexOf(')', i);\n if (close === -1) return undefined;\n return value.slice(start, i);\n}\n\nconst CC_COMMA = 0x2c;\nconst CC_PAREN_CLOSE = 0x29;\n\nfunction isWhitespace(cc: number): boolean {\n // ASCII whitespace: space, tab, LF, CR, FF, VT.\n return cc === 0x20 || cc === 0x09 || cc === 0x0a || cc === 0x0d || cc === 0x0c || cc === 0x0b;\n}\n\n/**\n * Map a `var(--color-foo)` reference or a raw color literal to a concrete\n * color string, using the supplied palette. Unrecognized references fall back\n * to `accent`.\n */\nexport function resolveColorReference(value: string, palette: ThemeTokenPalette): string {\n // Deterministic parser instead of a regex — regex variants with overlapping\n // quantifiers around the color name produced quadratic backtracking on\n // adversarial inputs (CodeQL js/polynomial-redos).\n const key = parseColorVarName(value);\n if (key === undefined) return value;\n switch (key) {\n case 'bg':\n return palette.bg;\n case 'panel':\n return palette.panel;\n case 'panel-2':\n return palette.panel2;\n case 'border':\n return palette.border;\n case 'border-strong':\n return palette.borderStrong;\n case 'text':\n return palette.text;\n case 'text-muted':\n return palette.textMuted;\n case 'text-dim':\n return palette.textDim;\n case 'accent':\n return palette.accent;\n case 'ok':\n return palette.ok;\n case 'warn':\n return palette.warn;\n case 'err':\n return palette.err;\n case 'purple':\n return palette.purple;\n case 'pink':\n return palette.pink;\n default:\n return palette.accent;\n }\n}\n","'use client';\n\nimport type cytoscape from 'cytoscape';\nimport { useCallback, useEffect, useMemo } from 'react';\n\nimport { buildShipItStylesheet, type BuildStylesheetOptions } from './stylesheet';\n\n/**\n * useShipItStylesheet — keeps a Cytoscape instance's stylesheet in sync with\n * the live design-token palette. Re-applies the stylesheet whenever\n * `<html data-theme>` flips, so toggling between dark and light propagates to\n * the graph without remounting.\n *\n * Returns a `refresh()` callback the consumer can invoke after any other\n * theme-affecting change (e.g., a `--color-accent` hue knob update).\n *\n * ```ts\n * const cyRef = useRef<cytoscape.Core | null>(null);\n * const { refresh } = useShipItStylesheet(cyRef);\n * ```\n */\n\nexport interface UseShipItStylesheetOptions extends BuildStylesheetOptions {\n /** Skip the MutationObserver wiring (e.g., when the host owns its own observer). */\n observe?: boolean;\n}\n\nexport interface UseShipItStylesheetReturn {\n /** Re-read tokens and re-apply the stylesheet. */\n refresh: () => void;\n}\n\nexport function useShipItStylesheet(\n cyRef: { current: cytoscape.Core | null },\n options: UseShipItStylesheetOptions = {},\n): UseShipItStylesheetReturn {\n const { observe = true, palette, extra } = options;\n // Memoize the build options against their flat constituents so callers that\n // pass `options` inline (a fresh object each render) don't churn `apply` /\n // disconnect+reconnect the MutationObserver on every render.\n const buildOptions = useMemo<BuildStylesheetOptions>(\n () => ({ palette, extra }),\n [palette, extra],\n );\n\n const apply = useCallback(() => {\n const cy = cyRef.current;\n if (!cy) return;\n cy.style(buildShipItStylesheet(buildOptions)).update();\n }, [cyRef, buildOptions]);\n\n useEffect(() => {\n apply();\n if (!observe || typeof document === 'undefined') return undefined;\n const observer = new MutationObserver(apply);\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: ['data-theme'],\n });\n return () => observer.disconnect();\n }, [apply, observe]);\n\n return { refresh: apply };\n}\n","'use client';\n\nimport type cytoscape from 'cytoscape';\nimport {\n forwardRef,\n useEffect,\n useImperativeHandle,\n useRef,\n type HTMLAttributes,\n type ReactNode,\n} from 'react';\n\nimport { buildShipItStylesheet, type BuildStylesheetOptions } from './stylesheet';\nimport { useShipItStylesheet } from './useShipItStylesheet';\n\n/**\n * GraphCanvas — high-level wrapper around a Cytoscape instance. Owns the\n * `data-theme` ↔ stylesheet sync (via {@link useShipItStylesheet}), the\n * cytoscape lifecycle (create / destroy on mount / unmount), and a thin\n * selection API on top of Cytoscape's events.\n *\n * The component never bundles Cytoscape itself — pass the engine factory in\n * via the `engine` prop so the consumer controls the Cytoscape version and\n * any registered extensions:\n *\n * ```tsx\n * import cytoscape from 'cytoscape';\n *\n * <GraphCanvas\n * engine={cytoscape}\n * elements={[...]}\n * layout={{ name: 'cose' }}\n * onSelect={(node) => setSelected(node.id())}\n * inspector={selected && <GraphInspector …/>}\n * />\n * ```\n */\n\nexport type CytoscapeEngine = (options: cytoscape.CytoscapeOptions) => cytoscape.Core;\n\nexport interface GraphCanvasHandle {\n /** Live Cytoscape instance. `null` until mount. */\n cy: cytoscape.Core | null;\n /** Re-read tokens and re-apply the stylesheet. */\n refreshStyles: () => void;\n}\n\nexport interface GraphCanvasProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n 'onSelect' | 'children'\n> {\n /** Cytoscape factory. Pass the imported `cytoscape` default export. */\n engine: CytoscapeEngine;\n /** Graph elements (nodes + edges). Passed straight through to Cytoscape. */\n elements: cytoscape.ElementDefinition[];\n /** Layout config. Defaults to a static `preset` layout (no auto-layout). */\n layout?: cytoscape.LayoutOptions;\n /** Fires when a node is tapped/selected. */\n onSelect?: (node: cytoscape.NodeSingular) => void;\n /** Fires when the selection is cleared (background tap). */\n onClearSelection?: () => void;\n /** Fires when the pointer enters a node. */\n onNodeHover?: (node: cytoscape.NodeSingular) => void;\n /** Fires when the pointer leaves a node. */\n onNodeLeave?: () => void;\n /** Slot rendered over the graph (e.g., a `<GraphInspector>`). Positioned top-right. */\n inspector?: ReactNode;\n /** Overrides for the stylesheet builder. */\n styleOptions?: BuildStylesheetOptions;\n /** Accessible label for the container. */\n 'aria-label'?: string;\n}\n\nconst DEFAULT_LAYOUT: cytoscape.LayoutOptions = { name: 'preset' };\n\nexport const GraphCanvas = forwardRef<GraphCanvasHandle, GraphCanvasProps>(function GraphCanvas(\n {\n engine,\n elements,\n layout = DEFAULT_LAYOUT,\n onSelect,\n onClearSelection,\n onNodeHover,\n onNodeLeave,\n inspector,\n styleOptions,\n className,\n 'aria-label': ariaLabel = 'Graph canvas',\n ...props\n },\n forwardedRef,\n) {\n const containerRef = useRef<HTMLDivElement | null>(null);\n const cyRef = useRef<cytoscape.Core | null>(null);\n\n // Create the cytoscape instance once on mount.\n useEffect(() => {\n if (!containerRef.current) return undefined;\n const cy = engine({\n container: containerRef.current,\n elements,\n layout,\n style: buildShipItStylesheet(styleOptions),\n });\n cyRef.current = cy;\n return () => {\n cy.destroy();\n cyRef.current = null;\n };\n // We intentionally re-create on engine swap or container remount only.\n // Elements / layout / style updates are handled in the effects below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [engine]);\n\n // Keep elements in sync.\n useEffect(() => {\n const cy = cyRef.current;\n if (!cy) return;\n cy.json({ elements });\n }, [elements]);\n\n // Re-run layout when the layout config changes.\n useEffect(() => {\n const cy = cyRef.current;\n if (!cy) return;\n cy.layout(layout).run();\n }, [layout]);\n\n const { refresh: refreshStyles } = useShipItStylesheet(cyRef, styleOptions);\n\n // Wire selection events. `engine` is in the deps so the listeners re-bind\n // whenever the upstream effect destroys + recreates the Cytoscape instance —\n // without it, an `engine` swap would leave the new `cy` without handlers.\n useEffect(() => {\n const cy = cyRef.current;\n if (!cy) return undefined;\n const handleSelect = (event: cytoscape.EventObject) => {\n if (event.target?.isNode?.()) {\n onSelect?.(event.target as cytoscape.NodeSingular);\n }\n };\n const handleBackgroundTap = (event: cytoscape.EventObject) => {\n if (event.target === cy) onClearSelection?.();\n };\n const handleEnter = (event: cytoscape.EventObject) => {\n onNodeHover?.(event.target as cytoscape.NodeSingular);\n };\n const handleLeave = () => onNodeLeave?.();\n cy.on('tap', 'node', handleSelect);\n cy.on('tap', handleBackgroundTap);\n cy.on('mouseover', 'node', handleEnter);\n cy.on('mouseout', 'node', handleLeave);\n return () => {\n cy.off('tap', 'node', handleSelect);\n cy.off('tap', handleBackgroundTap);\n cy.off('mouseover', 'node', handleEnter);\n cy.off('mouseout', 'node', handleLeave);\n };\n }, [engine, onSelect, onClearSelection, onNodeHover, onNodeLeave]);\n\n // Expose the imperative handle via getters so consumers always read the live\n // `cyRef.current`. Snapshotting `cyRef.current` here would freeze it at\n // `null` because the instance is assigned inside an effect that runs *after*\n // the initial render — Copilot/Claude both flagged this.\n useImperativeHandle(\n forwardedRef,\n (): GraphCanvasHandle => ({\n get cy() {\n return cyRef.current;\n },\n refreshStyles,\n }),\n [refreshStyles],\n );\n\n return (\n <div\n role=\"region\"\n aria-label={ariaLabel}\n className={['relative h-full w-full', className].filter(Boolean).join(' ')}\n {...props}\n >\n {/*\n * Inline styles, not Tailwind utilities. Cytoscape injects an unlayered\n * `.__________cytoscape_container { position: relative }` stylesheet at\n * init time, and Tailwind v4 emits `.absolute`/`.inset-0` into\n * `@layer utilities` — unlayered rules outrank layered ones regardless\n * of source order, so the canvas would collapse to 0×0 in a static-\n * height parent. Inline styles win against both.\n */}\n <div ref={containerRef} style={{ position: 'absolute', inset: 0 }} />\n {inspector && <div className=\"absolute top-4 right-4 z-10\">{inspector}</div>}\n </div>\n );\n});\n\nGraphCanvas.displayName = 'GraphCanvas';\n"],"mappings":"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","names":["import_shipit","import_react","GraphCanvas"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/stylesheet.ts","../src/theme-tokens.ts","../src/useShipItStylesheet.ts","../src/GraphCanvas.tsx"],"sourcesContent":["/**\n * @ship-it-ui/cytoscape — Cytoscape adapter for the Ship-It design system.\n *\n * Three layered exports:\n * - {@link buildShipItStylesheet} — token-driven stylesheet array.\n * - {@link useShipItStylesheet} — theme-aware re-resolver hook.\n * - {@link GraphCanvas} — `<GraphCanvas>` React wrapper that\n * owns the data-theme ↔ stylesheet ↔\n * inspector dance.\n *\n * Cytoscape is a peer dependency — pass `engine={cytoscape}` so the consumer\n * controls the version and any registered extensions.\n */\n\nexport {\n buildShipItStylesheet,\n GRAPH_CANVAS_CLASS,\n type BuildStylesheetOptions,\n type ShipItStylesheetBlock,\n} from './stylesheet';\n\nexport {\n useShipItStylesheet,\n type UseShipItStylesheetOptions,\n type UseShipItStylesheetReturn,\n} from './useShipItStylesheet';\n\nexport {\n GraphCanvas,\n type GraphCanvasProps,\n type GraphCanvasHandle,\n type CytoscapeEngine,\n} from './GraphCanvas';\n\nexport {\n readThemeTokens,\n resolveCssVar,\n resolveColorReference,\n resolveEntityColor,\n type ThemeTokenPalette,\n} from './theme-tokens';\n","import { iconToSvgDataUrl } from '@ship-it-ui/icons';\nimport { listEntityTypes } from '@ship-it-ui/shipit';\nimport type cytoscape from 'cytoscape';\n\nimport { readThemeTokens, resolveColorReference, type ThemeTokenPalette } from './theme-tokens';\n\n/**\n * Build a Cytoscape stylesheet from the live design tokens. The result is a\n * plain JSON array suitable for `cytoscape({ style: ... })` — re-run after a\n * `data-theme` change to pick up the new palette.\n *\n * The stylesheet is opinionated:\n * - Nodes are square-ish rounded glyphs colored by `data(entityType)`.\n * - Edges are token-colored thin lines with arrowheads.\n * - Selected / on-path / dimmed states are driven by class names that the\n * consumer toggles (`graph-canvas:selected`, `graph-canvas:path`,\n * `graph-canvas:dim`).\n *\n * Pass `palette` to override the token read — useful for SSR or tests.\n */\n\nexport interface BuildStylesheetOptions {\n /** Pre-resolved palette. When omitted, tokens are read from the document. */\n palette?: ThemeTokenPalette;\n /**\n * Additional entries appended to the stylesheet — handy for app-specific\n * selectors without forking the builder.\n */\n extra?: ReadonlyArray<cytoscape.StylesheetJsonBlock>;\n /**\n * Render each registered entity type's glyph (◇, ○, ▤, ↑, ◎, ▢ …) inside\n * the cytoscape node as a centered SVG data URL. Closes the visual gap\n * with the `<GraphNode>` React component — the docs page and the canvas\n * now share a vocabulary. Defaults to `true`. Pass `false` to fall back\n * to the original wireframe (border-only) per-type rule.\n */\n renderGlyphs?: boolean;\n}\n\n// Re-export the block type so consumers can declare typed `extra` entries.\nexport type ShipItStylesheetBlock = cytoscape.StylesheetJsonBlock;\n\nexport function buildShipItStylesheet(\n options: BuildStylesheetOptions = {},\n): cytoscape.StylesheetJson {\n const palette = options.palette ?? readThemeTokens();\n const color = (cssVar: string) => resolveColorReference(cssVar, palette);\n const renderGlyphs = options.renderGlyphs !== false;\n\n const base: cytoscape.StylesheetJsonBlock[] = [\n {\n selector: 'node',\n style: {\n 'background-color': palette.panel,\n 'border-width': 1.5,\n 'border-color': palette.accent,\n 'border-opacity': 1,\n label: 'data(label)',\n color: palette.textMuted,\n // Static stack instead of `var(--font-mono, monospace)` — cytoscape\n // can't resolve CSS variables outside the DOM cascade and emits a\n // warning per node selector at every mount. Consumers who need to\n // override the canvas font can do so via `options.extra`.\n 'font-family': 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',\n 'font-size': 10,\n 'text-valign': 'bottom',\n 'text-halign': 'center',\n 'text-margin-y': 6,\n // 52 matches `<GraphNode>`'s default size — the docs page and the\n // canvas now share dimensions. Pre-Issue-4 the canvas was 36×36.\n width: 52,\n height: 52,\n shape: 'round-rectangle',\n },\n },\n // One selector per entity type registered with @ship-it-ui/shipit. Built-in\n // types are seeded automatically; custom types registered via\n // `registerEntityType(...)` pick up their `colorVar` here without a docs\n // patch or a forked stylesheet.\n ...listEntityTypes().map<cytoscape.StylesheetJsonBlock>(([type, meta]) => {\n const c = color(meta.colorVar);\n return {\n selector: `node[entityType = \"${escapeCytoscapeAttr(type)}\"]`,\n style: renderGlyphs\n ? {\n 'border-color': c,\n 'background-image': iconToSvgDataUrl(meta.iconName, { color: c }),\n 'background-fit': 'contain',\n 'background-clip': 'none',\n }\n : { 'border-color': c },\n };\n }),\n {\n selector: 'node:selected',\n style: {\n 'border-width': 3,\n 'overlay-color': palette.accent,\n 'overlay-opacity': 0.15,\n 'overlay-padding': 4,\n },\n },\n {\n selector: 'node.graph-canvas\\\\:path',\n style: { 'border-color': palette.purple },\n },\n {\n selector: 'node.graph-canvas\\\\:dim',\n style: { opacity: 0.35 },\n },\n {\n // Default edges tone with `accent` so they read against the panel\n // background — matches `<GraphEdge edgeStyle=\"solid\">` in the docs.\n // Pre-fix the line drew in `palette.border` (the same tone as\n // surface-divider lines) and effectively disappeared on dark themes.\n selector: 'edge',\n style: {\n width: 1,\n 'line-color': palette.accent,\n 'target-arrow-color': palette.accent,\n 'target-arrow-shape': 'triangle',\n 'arrow-scale': 0.8,\n 'curve-style': 'bezier',\n },\n },\n {\n selector: 'edge.graph-canvas\\\\:path',\n style: {\n 'line-color': palette.purple,\n 'target-arrow-color': palette.purple,\n width: 1.5,\n },\n },\n {\n selector: 'edge.graph-canvas\\\\:dim',\n style: { opacity: 0.25 },\n },\n ];\n\n return [...base, ...(options.extra ?? [])] as cytoscape.StylesheetJson;\n}\n\n/**\n * Convenience class names the stylesheet recognizes. Toggle these on Cytoscape\n * nodes / edges to drive the on-path and dimmed visuals.\n */\nexport const GRAPH_CANVAS_CLASS = {\n path: 'graph-canvas:path',\n dim: 'graph-canvas:dim',\n} as const;\n\n// Cytoscape's attribute selector accepts a double-quoted string. Escape\n// embedded backslashes and double quotes so a malformed (or hostile)\n// registered key can't break out of the selector. The grammar\n// (https://js.cytoscape.org/#selectors/data) is more permissive than CSS, but\n// these two characters are the only ones that would change selector semantics.\nfunction escapeCytoscapeAttr(value: string): string {\n return value.replace(/[\\\\\"]/g, (c) => `\\\\${c}`);\n}\n","/**\n * Token resolution helpers. The Ship-It design system stores colors as CSS\n * custom properties on `<html>` (`--color-accent`, `--color-bg`, …). Cytoscape\n * renders to a canvas / SVG layer outside Tailwind, so those vars never\n * resolve — we read the *computed* values at runtime and feed them into the\n * stylesheet builder as concrete color strings.\n *\n * Two layers:\n * 1. `resolveCssVar` — single-var reader with a fallback.\n * 2. `readThemeTokens` — pulls every color the stylesheet uses, returning a\n * flat `Record<string, string>` keyed by short name (no `--color-`\n * prefix). Re-running this after a `data-theme` flip yields a fresh\n * palette.\n */\n\nimport { getEntityTypeMeta, type EntityType } from '@ship-it-ui/shipit';\n\nconst DEFAULT_FALLBACK: Record<string, string> = {\n bg: '#0a0a0a',\n panel: '#0f0f0f',\n 'panel-2': '#161616',\n border: '#262626',\n 'border-strong': '#383838',\n text: '#fafafa',\n 'text-muted': '#a3a3a3',\n 'text-dim': '#737373',\n accent: '#3b82f6',\n ok: '#10b981',\n warn: '#f59e0b',\n err: '#ef4444',\n purple: '#a855f7',\n pink: '#ec4899',\n};\n\n/**\n * Read a single CSS variable from the document root and return its trimmed\n * computed value, falling back to `fallback` when the document is missing\n * (SSR) or the variable is unset.\n */\nexport function resolveCssVar(name: string, fallback = ''): string {\n if (typeof document === 'undefined') return fallback;\n const raw = getComputedStyle(document.documentElement).getPropertyValue(name);\n const trimmed = raw.trim();\n return trimmed.length > 0 ? trimmed : fallback;\n}\n\n// Lazily-created 1×1 canvas used by `toSrgb` to coerce browser-parseable color\n// strings (oklch, lab, lch, named, etc.) into a plain `rgb()` / `rgba()` that\n// cytoscape's color parser accepts. Created on first use; the same canvas is\n// reused across calls so we don't allocate per token.\nlet coerceCanvas: HTMLCanvasElement | null = null;\n\n/**\n * Coerce an arbitrary CSS color string into an sRGB `rgb()` / `rgba()` string\n * via canvas pixel readback. Cytoscape's color parser doesn't accept modern\n * color functions (`oklch(...)`, `lab(...)`), and Tailwind v4 compiles every\n * `--color-*` token to `oklch(...)` — so when `readThemeTokens` reads those\n * tokens with `getComputedStyle`, the resulting palette would produce 70+\n * console warnings and silently fall back to defaults on every cytoscape\n * mount. Coercing once at the boundary fixes both the warnings and the\n * fallback colors.\n *\n * The implementation deliberately uses `fillRect` + `getImageData` rather\n * than reading `ctx.fillStyle` back — modern Chromium returns the literal\n * `oklch(...)` string from `ctx.fillStyle`, so the readback is the only\n * reliable way to force the browser's color pipeline to rasterize the value\n * down to sRGB.\n */\nexport function toSrgb(value: string): string {\n if (!value || typeof document === 'undefined') return value;\n // Fast path: already a parser-friendly value. Skips both the canvas\n // allocation and the readback round-trip for the common case where tokens\n // are already authored as `#xxx`, `rgb(...)`, `rgba(...)`, or `hsl(...)`.\n if (/^#|^rgb\\(|^rgba\\(|^hsl\\(|^hsla\\(/i.test(value)) return value;\n coerceCanvas ??= document.createElement('canvas');\n coerceCanvas.width = 1;\n coerceCanvas.height = 1;\n const ctx = coerceCanvas.getContext('2d', { willReadFrequently: true });\n if (!ctx) return value;\n ctx.clearRect(0, 0, 1, 1);\n // Two assignments so an unparseable `value` falls back to the seeded `#000`\n // instead of inheriting a stale fillStyle from a previous call.\n ctx.fillStyle = '#000';\n ctx.fillStyle = value;\n ctx.fillRect(0, 0, 1, 1);\n const data = ctx.getImageData(0, 0, 1, 1).data;\n const r = data[0];\n const g = data[1];\n const b = data[2];\n const a = data[3];\n if (r === undefined || g === undefined || b === undefined || a === undefined) return value;\n return a === 255 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${a / 255})`;\n}\n\nexport interface ThemeTokenPalette {\n /** Surface backgrounds. */\n bg: string;\n panel: string;\n panel2: string;\n /** Hairline + emphasis border. */\n border: string;\n borderStrong: string;\n /** Foreground tiers. */\n text: string;\n textMuted: string;\n textDim: string;\n /** Brand + status. */\n accent: string;\n ok: string;\n warn: string;\n err: string;\n /** Extras the graph uses for entity-type ring colors. */\n purple: string;\n pink: string;\n}\n\n/**\n * Read the canonical Ship-It color tokens from the document root, coerced to\n * sRGB. The `toSrgb` wrapper exists because Tailwind v4 compiles every\n * `--color-*` token to `oklch(...)`, which cytoscape's color parser rejects —\n * see {@link toSrgb} for the full reasoning.\n */\nexport function readThemeTokens(): ThemeTokenPalette {\n return {\n bg: toSrgb(resolveCssVar('--color-bg', DEFAULT_FALLBACK.bg)),\n panel: toSrgb(resolveCssVar('--color-panel', DEFAULT_FALLBACK.panel)),\n panel2: toSrgb(resolveCssVar('--color-panel-2', DEFAULT_FALLBACK['panel-2'])),\n border: toSrgb(resolveCssVar('--color-border', DEFAULT_FALLBACK.border)),\n borderStrong: toSrgb(resolveCssVar('--color-border-strong', DEFAULT_FALLBACK['border-strong'])),\n text: toSrgb(resolveCssVar('--color-text', DEFAULT_FALLBACK.text)),\n textMuted: toSrgb(resolveCssVar('--color-text-muted', DEFAULT_FALLBACK['text-muted'])),\n textDim: toSrgb(resolveCssVar('--color-text-dim', DEFAULT_FALLBACK['text-dim'])),\n accent: toSrgb(resolveCssVar('--color-accent', DEFAULT_FALLBACK.accent)),\n ok: toSrgb(resolveCssVar('--color-ok', DEFAULT_FALLBACK.ok)),\n warn: toSrgb(resolveCssVar('--color-warn', DEFAULT_FALLBACK.warn)),\n err: toSrgb(resolveCssVar('--color-err', DEFAULT_FALLBACK.err)),\n purple: toSrgb(resolveCssVar('--color-purple', DEFAULT_FALLBACK.purple)),\n pink: toSrgb(resolveCssVar('--color-pink', DEFAULT_FALLBACK.pink)),\n };\n}\n\n/**\n * Resolve the concrete color for a registered entity type. Reads the type's\n * `colorVar` (a `var(--color-…)` string) and looks the value up in the\n * palette. Falls back to the palette's `accent` color when the var is\n * malformed or unknown.\n */\nexport function resolveEntityColor(type: EntityType, palette: ThemeTokenPalette): string {\n const meta = getEntityTypeMeta(type);\n return resolveColorReference(meta.colorVar, palette);\n}\n\n/**\n * Extract `foo` from `var(--color-foo)` or `var(--color-foo, fallback)`.\n * Returns `undefined` if the input doesn't match. O(n), no backtracking.\n */\nfunction parseColorVarName(value: string): string | undefined {\n // Strip surrounding whitespace once. `String#trim` is linear, not a regex.\n let i = 0;\n while (i < value.length && isWhitespace(value.charCodeAt(i))) i++;\n if (!value.startsWith('var(', i)) return undefined;\n i += 4;\n // Skip whitespace inside the parens.\n while (i < value.length && isWhitespace(value.charCodeAt(i))) i++;\n if (!value.startsWith('--color-', i)) return undefined;\n i += 8;\n // Read the name: stop at the first whitespace, comma, or `)`.\n const start = i;\n while (i < value.length) {\n const c = value.charCodeAt(i);\n if (c === CC_COMMA || c === CC_PAREN_CLOSE || isWhitespace(c)) break;\n i++;\n }\n if (i === start) return undefined;\n // Require a closing `)` somewhere after, so we don't classify malformed\n // inputs (`var(--color-foo`) as valid references.\n const close = value.indexOf(')', i);\n if (close === -1) return undefined;\n return value.slice(start, i);\n}\n\nconst CC_COMMA = 0x2c;\nconst CC_PAREN_CLOSE = 0x29;\n\nfunction isWhitespace(cc: number): boolean {\n // ASCII whitespace: space, tab, LF, CR, FF, VT.\n return cc === 0x20 || cc === 0x09 || cc === 0x0a || cc === 0x0d || cc === 0x0c || cc === 0x0b;\n}\n\n/**\n * Map a `var(--color-foo)` reference or a raw color literal to a concrete\n * color string, using the supplied palette. Unrecognized references fall back\n * to `accent`.\n */\nexport function resolveColorReference(value: string, palette: ThemeTokenPalette): string {\n // Deterministic parser instead of a regex — regex variants with overlapping\n // quantifiers around the color name produced quadratic backtracking on\n // adversarial inputs (CodeQL js/polynomial-redos).\n const key = parseColorVarName(value);\n if (key === undefined) return value;\n switch (key) {\n case 'bg':\n return palette.bg;\n case 'panel':\n return palette.panel;\n case 'panel-2':\n return palette.panel2;\n case 'border':\n return palette.border;\n case 'border-strong':\n return palette.borderStrong;\n case 'text':\n return palette.text;\n case 'text-muted':\n return palette.textMuted;\n case 'text-dim':\n return palette.textDim;\n case 'accent':\n return palette.accent;\n case 'ok':\n return palette.ok;\n case 'warn':\n return palette.warn;\n case 'err':\n return palette.err;\n case 'purple':\n return palette.purple;\n case 'pink':\n return palette.pink;\n default:\n return palette.accent;\n }\n}\n","'use client';\n\nimport type cytoscape from 'cytoscape';\nimport { useCallback, useEffect, useMemo } from 'react';\n\nimport { buildShipItStylesheet, type BuildStylesheetOptions } from './stylesheet';\n\n/**\n * useShipItStylesheet — keeps a Cytoscape instance's stylesheet in sync with\n * the live design-token palette. Re-applies the stylesheet whenever\n * `<html data-theme>` flips, so toggling between dark and light propagates to\n * the graph without remounting.\n *\n * Returns a `refresh()` callback the consumer can invoke after any other\n * theme-affecting change (e.g., a `--color-accent` hue knob update).\n *\n * ```ts\n * const cyRef = useRef<cytoscape.Core | null>(null);\n * const { refresh } = useShipItStylesheet(cyRef);\n * ```\n */\n\nexport interface UseShipItStylesheetOptions extends BuildStylesheetOptions {\n /** Skip the MutationObserver wiring (e.g., when the host owns its own observer). */\n observe?: boolean;\n}\n\nexport interface UseShipItStylesheetReturn {\n /** Re-read tokens and re-apply the stylesheet. */\n refresh: () => void;\n}\n\nexport function useShipItStylesheet(\n cyRef: { current: cytoscape.Core | null },\n options: UseShipItStylesheetOptions = {},\n): UseShipItStylesheetReturn {\n const { observe = true, palette, extra } = options;\n // Memoize the build options against their flat constituents so callers that\n // pass `options` inline (a fresh object each render) don't churn `apply` /\n // disconnect+reconnect the MutationObserver on every render.\n const buildOptions = useMemo<BuildStylesheetOptions>(\n () => ({ palette, extra }),\n [palette, extra],\n );\n\n const apply = useCallback(() => {\n const cy = cyRef.current;\n if (!cy) return;\n cy.style(buildShipItStylesheet(buildOptions)).update();\n }, [cyRef, buildOptions]);\n\n useEffect(() => {\n apply();\n if (!observe || typeof document === 'undefined') return undefined;\n const observer = new MutationObserver(apply);\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: ['data-theme'],\n });\n return () => observer.disconnect();\n }, [apply, observe]);\n\n return { refresh: apply };\n}\n","'use client';\n\nimport type cytoscape from 'cytoscape';\nimport {\n forwardRef,\n useEffect,\n useImperativeHandle,\n useRef,\n type HTMLAttributes,\n type ReactNode,\n} from 'react';\n\nimport { buildShipItStylesheet, type BuildStylesheetOptions } from './stylesheet';\nimport { useShipItStylesheet } from './useShipItStylesheet';\n\n/**\n * GraphCanvas — high-level wrapper around a Cytoscape instance. Owns the\n * `data-theme` ↔ stylesheet sync (via {@link useShipItStylesheet}), the\n * cytoscape lifecycle (create / destroy on mount / unmount), and a thin\n * selection API on top of Cytoscape's events.\n *\n * The component never bundles Cytoscape itself — pass the engine factory in\n * via the `engine` prop so the consumer controls the Cytoscape version and\n * any registered extensions:\n *\n * ```tsx\n * import cytoscape from 'cytoscape';\n *\n * <GraphCanvas\n * engine={cytoscape}\n * elements={[...]}\n * layout={{ name: 'cose' }}\n * onSelect={(node) => setSelected(node.id())}\n * inspector={selected && <GraphInspector …/>}\n * />\n * ```\n */\n\nexport type CytoscapeEngine = (options: cytoscape.CytoscapeOptions) => cytoscape.Core;\n\nexport interface GraphCanvasHandle {\n /** Live Cytoscape instance. `null` until mount. */\n cy: cytoscape.Core | null;\n /** Re-read tokens and re-apply the stylesheet. */\n refreshStyles: () => void;\n}\n\nexport interface GraphCanvasProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n 'onSelect' | 'children'\n> {\n /** Cytoscape factory. Pass the imported `cytoscape` default export. */\n engine: CytoscapeEngine;\n /** Graph elements (nodes + edges). Passed straight through to Cytoscape. */\n elements: cytoscape.ElementDefinition[];\n /** Layout config. Defaults to a static `preset` layout (no auto-layout). */\n layout?: cytoscape.LayoutOptions;\n /** Fires when a node is tapped/selected. */\n onSelect?: (node: cytoscape.NodeSingular) => void;\n /** Fires when the selection is cleared (background tap). */\n onClearSelection?: () => void;\n /** Fires when the pointer enters a node. */\n onNodeHover?: (node: cytoscape.NodeSingular) => void;\n /** Fires when the pointer leaves a node. */\n onNodeLeave?: () => void;\n /** Slot rendered over the graph (e.g., a `<GraphInspector>`). Positioned top-right. */\n inspector?: ReactNode;\n /** Overrides for the stylesheet builder. */\n styleOptions?: BuildStylesheetOptions;\n /** Accessible label for the container. */\n 'aria-label'?: string;\n}\n\nconst DEFAULT_LAYOUT: cytoscape.LayoutOptions = { name: 'preset' };\n\nexport const GraphCanvas = forwardRef<GraphCanvasHandle, GraphCanvasProps>(function GraphCanvas(\n {\n engine,\n elements,\n layout = DEFAULT_LAYOUT,\n onSelect,\n onClearSelection,\n onNodeHover,\n onNodeLeave,\n inspector,\n styleOptions,\n className,\n 'aria-label': ariaLabel = 'Graph canvas',\n ...props\n },\n forwardedRef,\n) {\n const containerRef = useRef<HTMLDivElement | null>(null);\n const cyRef = useRef<cytoscape.Core | null>(null);\n\n // Create the cytoscape instance once on mount.\n useEffect(() => {\n if (!containerRef.current) return undefined;\n const cy = engine({\n container: containerRef.current,\n elements,\n layout,\n style: buildShipItStylesheet(styleOptions),\n });\n cyRef.current = cy;\n return () => {\n cy.destroy();\n cyRef.current = null;\n };\n // We intentionally re-create on engine swap or container remount only.\n // Elements / layout / style updates are handled in the effects below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [engine]);\n\n // Keep elements in sync.\n useEffect(() => {\n const cy = cyRef.current;\n if (!cy) return;\n cy.json({ elements });\n }, [elements]);\n\n // Re-run layout when the layout config changes.\n useEffect(() => {\n const cy = cyRef.current;\n if (!cy) return;\n cy.layout(layout).run();\n }, [layout]);\n\n const { refresh: refreshStyles } = useShipItStylesheet(cyRef, styleOptions);\n\n // Wire selection events. `engine` is in the deps so the listeners re-bind\n // whenever the upstream effect destroys + recreates the Cytoscape instance —\n // without it, an `engine` swap would leave the new `cy` without handlers.\n useEffect(() => {\n const cy = cyRef.current;\n if (!cy) return undefined;\n const handleSelect = (event: cytoscape.EventObject) => {\n if (event.target?.isNode?.()) {\n onSelect?.(event.target as cytoscape.NodeSingular);\n }\n };\n const handleBackgroundTap = (event: cytoscape.EventObject) => {\n if (event.target === cy) onClearSelection?.();\n };\n const handleEnter = (event: cytoscape.EventObject) => {\n onNodeHover?.(event.target as cytoscape.NodeSingular);\n };\n const handleLeave = () => onNodeLeave?.();\n cy.on('tap', 'node', handleSelect);\n cy.on('tap', handleBackgroundTap);\n cy.on('mouseover', 'node', handleEnter);\n cy.on('mouseout', 'node', handleLeave);\n return () => {\n cy.off('tap', 'node', handleSelect);\n cy.off('tap', handleBackgroundTap);\n cy.off('mouseover', 'node', handleEnter);\n cy.off('mouseout', 'node', handleLeave);\n };\n }, [engine, onSelect, onClearSelection, onNodeHover, onNodeLeave]);\n\n // Expose the imperative handle via getters so consumers always read the live\n // `cyRef.current`. Snapshotting `cyRef.current` here would freeze it at\n // `null` because the instance is assigned inside an effect that runs *after*\n // the initial render — Copilot/Claude both flagged this.\n useImperativeHandle(\n forwardedRef,\n (): GraphCanvasHandle => ({\n get cy() {\n return cyRef.current;\n },\n refreshStyles,\n }),\n [refreshStyles],\n );\n\n return (\n <div\n role=\"region\"\n aria-label={ariaLabel}\n className={['relative h-full w-full', className].filter(Boolean).join(' ')}\n {...props}\n >\n {/*\n * Inline styles, not Tailwind utilities. Cytoscape injects an unlayered\n * `.__________cytoscape_container { position: relative }` stylesheet at\n * init time, and Tailwind v4 emits `.absolute`/`.inset-0` into\n * `@layer utilities` — unlayered rules outrank layered ones regardless\n * of source order, so the canvas would collapse to 0×0 in a static-\n * height parent. Inline styles win against both.\n */}\n <div ref={containerRef} style={{ position: 'absolute', inset: 0 }} />\n {inspector && <div className=\"absolute top-4 right-4 z-10\">{inspector}</div>}\n </div>\n );\n});\n\nGraphCanvas.displayName = 'GraphCanvas';\n"],"mappings":"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","names":["import_shipit","import_react","GraphCanvas"]}
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  // src/stylesheet.ts
4
+ import { iconToSvgDataUrl } from "@ship-it-ui/icons";
4
5
  import { listEntityTypes } from "@ship-it-ui/shipit";
5
6
 
6
7
  // src/theme-tokens.ts
@@ -172,7 +173,7 @@ function buildShipItStylesheet(options = {}) {
172
173
  selector: `node[entityType = "${escapeCytoscapeAttr(type)}"]`,
173
174
  style: renderGlyphs ? {
174
175
  "border-color": c,
175
- "background-image": glyphDataUrl(meta.glyph, c),
176
+ "background-image": iconToSvgDataUrl(meta.iconName, { color: c }),
176
177
  "background-fit": "contain",
177
178
  "background-clip": "none"
178
179
  } : { "border-color": c }
@@ -232,22 +233,6 @@ var GRAPH_CANVAS_CLASS = {
232
233
  function escapeCytoscapeAttr(value) {
233
234
  return value.replace(/[\\"]/g, (c) => `\\${c}`);
234
235
  }
235
- var XML_ESCAPES = {
236
- "<": "&lt;",
237
- ">": "&gt;",
238
- "&": "&amp;",
239
- '"': "&quot;",
240
- "'": "&apos;"
241
- };
242
- function escapeXml(value) {
243
- return value.replace(/[<>&"']/g, (c) => XML_ESCAPES[c] ?? c);
244
- }
245
- function glyphDataUrl(glyph, color) {
246
- const safeGlyph = escapeXml(glyph);
247
- const safeColor = escapeXml(color);
248
- const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='52' height='52' viewBox='0 0 52 52'><text x='26' y='34' text-anchor='middle' font-family='ui-monospace,SFMono-Regular,monospace' font-size='26' fill='${safeColor}'>${safeGlyph}</text></svg>`;
249
- return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
250
- }
251
236
 
252
237
  // src/useShipItStylesheet.ts
253
238
  import { useCallback, useEffect, useMemo } from "react";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/stylesheet.ts","../src/theme-tokens.ts","../src/useShipItStylesheet.ts","../src/GraphCanvas.tsx"],"sourcesContent":["import { listEntityTypes } from '@ship-it-ui/shipit';\nimport type cytoscape from 'cytoscape';\n\nimport { readThemeTokens, resolveColorReference, type ThemeTokenPalette } from './theme-tokens';\n\n/**\n * Build a Cytoscape stylesheet from the live design tokens. The result is a\n * plain JSON array suitable for `cytoscape({ style: ... })` — re-run after a\n * `data-theme` change to pick up the new palette.\n *\n * The stylesheet is opinionated:\n * - Nodes are square-ish rounded glyphs colored by `data(entityType)`.\n * - Edges are token-colored thin lines with arrowheads.\n * - Selected / on-path / dimmed states are driven by class names that the\n * consumer toggles (`graph-canvas:selected`, `graph-canvas:path`,\n * `graph-canvas:dim`).\n *\n * Pass `palette` to override the token read — useful for SSR or tests.\n */\n\nexport interface BuildStylesheetOptions {\n /** Pre-resolved palette. When omitted, tokens are read from the document. */\n palette?: ThemeTokenPalette;\n /**\n * Additional entries appended to the stylesheet — handy for app-specific\n * selectors without forking the builder.\n */\n extra?: ReadonlyArray<cytoscape.StylesheetJsonBlock>;\n /**\n * Render each registered entity type's glyph (◇, ○, ▤, ↑, ◎, ▢ …) inside\n * the cytoscape node as a centered SVG data URL. Closes the visual gap\n * with the `<GraphNode>` React component — the docs page and the canvas\n * now share a vocabulary. Defaults to `true`. Pass `false` to fall back\n * to the original wireframe (border-only) per-type rule.\n */\n renderGlyphs?: boolean;\n}\n\n// Re-export the block type so consumers can declare typed `extra` entries.\nexport type ShipItStylesheetBlock = cytoscape.StylesheetJsonBlock;\n\nexport function buildShipItStylesheet(\n options: BuildStylesheetOptions = {},\n): cytoscape.StylesheetJson {\n const palette = options.palette ?? readThemeTokens();\n const color = (cssVar: string) => resolveColorReference(cssVar, palette);\n const renderGlyphs = options.renderGlyphs !== false;\n\n const base: cytoscape.StylesheetJsonBlock[] = [\n {\n selector: 'node',\n style: {\n 'background-color': palette.panel,\n 'border-width': 1.5,\n 'border-color': palette.accent,\n 'border-opacity': 1,\n label: 'data(label)',\n color: palette.textMuted,\n // Static stack instead of `var(--font-mono, monospace)` — cytoscape\n // can't resolve CSS variables outside the DOM cascade and emits a\n // warning per node selector at every mount. Consumers who need to\n // override the canvas font can do so via `options.extra`.\n 'font-family': 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',\n 'font-size': 10,\n 'text-valign': 'bottom',\n 'text-halign': 'center',\n 'text-margin-y': 6,\n // 52 matches `<GraphNode>`'s default size — the docs page and the\n // canvas now share dimensions. Pre-Issue-4 the canvas was 36×36.\n width: 52,\n height: 52,\n shape: 'round-rectangle',\n },\n },\n // One selector per entity type registered with @ship-it-ui/shipit. Built-in\n // types are seeded automatically; custom types registered via\n // `registerEntityType(...)` pick up their `colorVar` here without a docs\n // patch or a forked stylesheet.\n ...listEntityTypes().map<cytoscape.StylesheetJsonBlock>(([type, meta]) => {\n const c = color(meta.colorVar);\n return {\n selector: `node[entityType = \"${escapeCytoscapeAttr(type)}\"]`,\n style: renderGlyphs\n ? {\n 'border-color': c,\n 'background-image': glyphDataUrl(meta.glyph, c),\n 'background-fit': 'contain',\n 'background-clip': 'none',\n }\n : { 'border-color': c },\n };\n }),\n {\n selector: 'node:selected',\n style: {\n 'border-width': 3,\n 'overlay-color': palette.accent,\n 'overlay-opacity': 0.15,\n 'overlay-padding': 4,\n },\n },\n {\n selector: 'node.graph-canvas\\\\:path',\n style: { 'border-color': palette.purple },\n },\n {\n selector: 'node.graph-canvas\\\\:dim',\n style: { opacity: 0.35 },\n },\n {\n // Default edges tone with `accent` so they read against the panel\n // background — matches `<GraphEdge edgeStyle=\"solid\">` in the docs.\n // Pre-fix the line drew in `palette.border` (the same tone as\n // surface-divider lines) and effectively disappeared on dark themes.\n selector: 'edge',\n style: {\n width: 1,\n 'line-color': palette.accent,\n 'target-arrow-color': palette.accent,\n 'target-arrow-shape': 'triangle',\n 'arrow-scale': 0.8,\n 'curve-style': 'bezier',\n },\n },\n {\n selector: 'edge.graph-canvas\\\\:path',\n style: {\n 'line-color': palette.purple,\n 'target-arrow-color': palette.purple,\n width: 1.5,\n },\n },\n {\n selector: 'edge.graph-canvas\\\\:dim',\n style: { opacity: 0.25 },\n },\n ];\n\n return [...base, ...(options.extra ?? [])] as cytoscape.StylesheetJson;\n}\n\n/**\n * Convenience class names the stylesheet recognizes. Toggle these on Cytoscape\n * nodes / edges to drive the on-path and dimmed visuals.\n */\nexport const GRAPH_CANVAS_CLASS = {\n path: 'graph-canvas:path',\n dim: 'graph-canvas:dim',\n} as const;\n\n// Cytoscape's attribute selector accepts a double-quoted string. Escape\n// embedded backslashes and double quotes so a malformed (or hostile)\n// registered key can't break out of the selector. The grammar\n// (https://js.cytoscape.org/#selectors/data) is more permissive than CSS, but\n// these two characters are the only ones that would change selector semantics.\nfunction escapeCytoscapeAttr(value: string): string {\n return value.replace(/[\\\\\"]/g, (c) => `\\\\${c}`);\n}\n\nconst XML_ESCAPES: Record<string, string> = {\n '<': '&lt;',\n '>': '&gt;',\n '&': '&amp;',\n '\"': '&quot;',\n \"'\": '&apos;',\n};\n\nfunction escapeXml(value: string): string {\n return value.replace(/[<>&\"']/g, (c) => XML_ESCAPES[c] ?? c);\n}\n\n/**\n * Build a `data:image/svg+xml;...` URL containing the entity glyph centered\n * in a 52×52 viewBox, filled with `color`. Cytoscape draws this as the\n * node's `background-image`, on top of the panel-colored fill, beneath the\n * entity-color border. Glyphs are single unicode characters from the\n * `EntityTypeMeta.glyph` registry (◇ ○ ▤ ↑ ◎ ▢ …).\n *\n * `y='34'` lands the visual centre of typical box-drawing / shape glyphs on\n * the geometric centre of the node — `y='26'` (true centre) sits the\n * baseline at the centre and the glyph reads as if it's floating above.\n */\nfunction glyphDataUrl(glyph: string, color: string): string {\n const safeGlyph = escapeXml(glyph);\n const safeColor = escapeXml(color);\n const svg =\n `<svg xmlns='http://www.w3.org/2000/svg' width='52' height='52' viewBox='0 0 52 52'>` +\n `<text x='26' y='34' text-anchor='middle' ` +\n `font-family='ui-monospace,SFMono-Regular,monospace' ` +\n `font-size='26' fill='${safeColor}'>${safeGlyph}</text>` +\n `</svg>`;\n return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;\n}\n","/**\n * Token resolution helpers. The Ship-It design system stores colors as CSS\n * custom properties on `<html>` (`--color-accent`, `--color-bg`, …). Cytoscape\n * renders to a canvas / SVG layer outside Tailwind, so those vars never\n * resolve — we read the *computed* values at runtime and feed them into the\n * stylesheet builder as concrete color strings.\n *\n * Two layers:\n * 1. `resolveCssVar` — single-var reader with a fallback.\n * 2. `readThemeTokens` — pulls every color the stylesheet uses, returning a\n * flat `Record<string, string>` keyed by short name (no `--color-`\n * prefix). Re-running this after a `data-theme` flip yields a fresh\n * palette.\n */\n\nimport { getEntityTypeMeta, type EntityType } from '@ship-it-ui/shipit';\n\nconst DEFAULT_FALLBACK: Record<string, string> = {\n bg: '#0a0a0a',\n panel: '#0f0f0f',\n 'panel-2': '#161616',\n border: '#262626',\n 'border-strong': '#383838',\n text: '#fafafa',\n 'text-muted': '#a3a3a3',\n 'text-dim': '#737373',\n accent: '#3b82f6',\n ok: '#10b981',\n warn: '#f59e0b',\n err: '#ef4444',\n purple: '#a855f7',\n pink: '#ec4899',\n};\n\n/**\n * Read a single CSS variable from the document root and return its trimmed\n * computed value, falling back to `fallback` when the document is missing\n * (SSR) or the variable is unset.\n */\nexport function resolveCssVar(name: string, fallback = ''): string {\n if (typeof document === 'undefined') return fallback;\n const raw = getComputedStyle(document.documentElement).getPropertyValue(name);\n const trimmed = raw.trim();\n return trimmed.length > 0 ? trimmed : fallback;\n}\n\n// Lazily-created 1×1 canvas used by `toSrgb` to coerce browser-parseable color\n// strings (oklch, lab, lch, named, etc.) into a plain `rgb()` / `rgba()` that\n// cytoscape's color parser accepts. Created on first use; the same canvas is\n// reused across calls so we don't allocate per token.\nlet coerceCanvas: HTMLCanvasElement | null = null;\n\n/**\n * Coerce an arbitrary CSS color string into an sRGB `rgb()` / `rgba()` string\n * via canvas pixel readback. Cytoscape's color parser doesn't accept modern\n * color functions (`oklch(...)`, `lab(...)`), and Tailwind v4 compiles every\n * `--color-*` token to `oklch(...)` — so when `readThemeTokens` reads those\n * tokens with `getComputedStyle`, the resulting palette would produce 70+\n * console warnings and silently fall back to defaults on every cytoscape\n * mount. Coercing once at the boundary fixes both the warnings and the\n * fallback colors.\n *\n * The implementation deliberately uses `fillRect` + `getImageData` rather\n * than reading `ctx.fillStyle` back — modern Chromium returns the literal\n * `oklch(...)` string from `ctx.fillStyle`, so the readback is the only\n * reliable way to force the browser's color pipeline to rasterize the value\n * down to sRGB.\n */\nexport function toSrgb(value: string): string {\n if (!value || typeof document === 'undefined') return value;\n // Fast path: already a parser-friendly value. Skips both the canvas\n // allocation and the readback round-trip for the common case where tokens\n // are already authored as `#xxx`, `rgb(...)`, `rgba(...)`, or `hsl(...)`.\n if (/^#|^rgb\\(|^rgba\\(|^hsl\\(|^hsla\\(/i.test(value)) return value;\n coerceCanvas ??= document.createElement('canvas');\n coerceCanvas.width = 1;\n coerceCanvas.height = 1;\n const ctx = coerceCanvas.getContext('2d', { willReadFrequently: true });\n if (!ctx) return value;\n ctx.clearRect(0, 0, 1, 1);\n // Two assignments so an unparseable `value` falls back to the seeded `#000`\n // instead of inheriting a stale fillStyle from a previous call.\n ctx.fillStyle = '#000';\n ctx.fillStyle = value;\n ctx.fillRect(0, 0, 1, 1);\n const data = ctx.getImageData(0, 0, 1, 1).data;\n const r = data[0];\n const g = data[1];\n const b = data[2];\n const a = data[3];\n if (r === undefined || g === undefined || b === undefined || a === undefined) return value;\n return a === 255 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${a / 255})`;\n}\n\nexport interface ThemeTokenPalette {\n /** Surface backgrounds. */\n bg: string;\n panel: string;\n panel2: string;\n /** Hairline + emphasis border. */\n border: string;\n borderStrong: string;\n /** Foreground tiers. */\n text: string;\n textMuted: string;\n textDim: string;\n /** Brand + status. */\n accent: string;\n ok: string;\n warn: string;\n err: string;\n /** Extras the graph uses for entity-type ring colors. */\n purple: string;\n pink: string;\n}\n\n/**\n * Read the canonical Ship-It color tokens from the document root, coerced to\n * sRGB. The `toSrgb` wrapper exists because Tailwind v4 compiles every\n * `--color-*` token to `oklch(...)`, which cytoscape's color parser rejects —\n * see {@link toSrgb} for the full reasoning.\n */\nexport function readThemeTokens(): ThemeTokenPalette {\n return {\n bg: toSrgb(resolveCssVar('--color-bg', DEFAULT_FALLBACK.bg)),\n panel: toSrgb(resolveCssVar('--color-panel', DEFAULT_FALLBACK.panel)),\n panel2: toSrgb(resolveCssVar('--color-panel-2', DEFAULT_FALLBACK['panel-2'])),\n border: toSrgb(resolveCssVar('--color-border', DEFAULT_FALLBACK.border)),\n borderStrong: toSrgb(resolveCssVar('--color-border-strong', DEFAULT_FALLBACK['border-strong'])),\n text: toSrgb(resolveCssVar('--color-text', DEFAULT_FALLBACK.text)),\n textMuted: toSrgb(resolveCssVar('--color-text-muted', DEFAULT_FALLBACK['text-muted'])),\n textDim: toSrgb(resolveCssVar('--color-text-dim', DEFAULT_FALLBACK['text-dim'])),\n accent: toSrgb(resolveCssVar('--color-accent', DEFAULT_FALLBACK.accent)),\n ok: toSrgb(resolveCssVar('--color-ok', DEFAULT_FALLBACK.ok)),\n warn: toSrgb(resolveCssVar('--color-warn', DEFAULT_FALLBACK.warn)),\n err: toSrgb(resolveCssVar('--color-err', DEFAULT_FALLBACK.err)),\n purple: toSrgb(resolveCssVar('--color-purple', DEFAULT_FALLBACK.purple)),\n pink: toSrgb(resolveCssVar('--color-pink', DEFAULT_FALLBACK.pink)),\n };\n}\n\n/**\n * Resolve the concrete color for a registered entity type. Reads the type's\n * `colorVar` (a `var(--color-…)` string) and looks the value up in the\n * palette. Falls back to the palette's `accent` color when the var is\n * malformed or unknown.\n */\nexport function resolveEntityColor(type: EntityType, palette: ThemeTokenPalette): string {\n const meta = getEntityTypeMeta(type);\n return resolveColorReference(meta.colorVar, palette);\n}\n\n/**\n * Extract `foo` from `var(--color-foo)` or `var(--color-foo, fallback)`.\n * Returns `undefined` if the input doesn't match. O(n), no backtracking.\n */\nfunction parseColorVarName(value: string): string | undefined {\n // Strip surrounding whitespace once. `String#trim` is linear, not a regex.\n let i = 0;\n while (i < value.length && isWhitespace(value.charCodeAt(i))) i++;\n if (!value.startsWith('var(', i)) return undefined;\n i += 4;\n // Skip whitespace inside the parens.\n while (i < value.length && isWhitespace(value.charCodeAt(i))) i++;\n if (!value.startsWith('--color-', i)) return undefined;\n i += 8;\n // Read the name: stop at the first whitespace, comma, or `)`.\n const start = i;\n while (i < value.length) {\n const c = value.charCodeAt(i);\n if (c === CC_COMMA || c === CC_PAREN_CLOSE || isWhitespace(c)) break;\n i++;\n }\n if (i === start) return undefined;\n // Require a closing `)` somewhere after, so we don't classify malformed\n // inputs (`var(--color-foo`) as valid references.\n const close = value.indexOf(')', i);\n if (close === -1) return undefined;\n return value.slice(start, i);\n}\n\nconst CC_COMMA = 0x2c;\nconst CC_PAREN_CLOSE = 0x29;\n\nfunction isWhitespace(cc: number): boolean {\n // ASCII whitespace: space, tab, LF, CR, FF, VT.\n return cc === 0x20 || cc === 0x09 || cc === 0x0a || cc === 0x0d || cc === 0x0c || cc === 0x0b;\n}\n\n/**\n * Map a `var(--color-foo)` reference or a raw color literal to a concrete\n * color string, using the supplied palette. Unrecognized references fall back\n * to `accent`.\n */\nexport function resolveColorReference(value: string, palette: ThemeTokenPalette): string {\n // Deterministic parser instead of a regex — regex variants with overlapping\n // quantifiers around the color name produced quadratic backtracking on\n // adversarial inputs (CodeQL js/polynomial-redos).\n const key = parseColorVarName(value);\n if (key === undefined) return value;\n switch (key) {\n case 'bg':\n return palette.bg;\n case 'panel':\n return palette.panel;\n case 'panel-2':\n return palette.panel2;\n case 'border':\n return palette.border;\n case 'border-strong':\n return palette.borderStrong;\n case 'text':\n return palette.text;\n case 'text-muted':\n return palette.textMuted;\n case 'text-dim':\n return palette.textDim;\n case 'accent':\n return palette.accent;\n case 'ok':\n return palette.ok;\n case 'warn':\n return palette.warn;\n case 'err':\n return palette.err;\n case 'purple':\n return palette.purple;\n case 'pink':\n return palette.pink;\n default:\n return palette.accent;\n }\n}\n","'use client';\n\nimport type cytoscape from 'cytoscape';\nimport { useCallback, useEffect, useMemo } from 'react';\n\nimport { buildShipItStylesheet, type BuildStylesheetOptions } from './stylesheet';\n\n/**\n * useShipItStylesheet — keeps a Cytoscape instance's stylesheet in sync with\n * the live design-token palette. Re-applies the stylesheet whenever\n * `<html data-theme>` flips, so toggling between dark and light propagates to\n * the graph without remounting.\n *\n * Returns a `refresh()` callback the consumer can invoke after any other\n * theme-affecting change (e.g., a `--color-accent` hue knob update).\n *\n * ```ts\n * const cyRef = useRef<cytoscape.Core | null>(null);\n * const { refresh } = useShipItStylesheet(cyRef);\n * ```\n */\n\nexport interface UseShipItStylesheetOptions extends BuildStylesheetOptions {\n /** Skip the MutationObserver wiring (e.g., when the host owns its own observer). */\n observe?: boolean;\n}\n\nexport interface UseShipItStylesheetReturn {\n /** Re-read tokens and re-apply the stylesheet. */\n refresh: () => void;\n}\n\nexport function useShipItStylesheet(\n cyRef: { current: cytoscape.Core | null },\n options: UseShipItStylesheetOptions = {},\n): UseShipItStylesheetReturn {\n const { observe = true, palette, extra } = options;\n // Memoize the build options against their flat constituents so callers that\n // pass `options` inline (a fresh object each render) don't churn `apply` /\n // disconnect+reconnect the MutationObserver on every render.\n const buildOptions = useMemo<BuildStylesheetOptions>(\n () => ({ palette, extra }),\n [palette, extra],\n );\n\n const apply = useCallback(() => {\n const cy = cyRef.current;\n if (!cy) return;\n cy.style(buildShipItStylesheet(buildOptions)).update();\n }, [cyRef, buildOptions]);\n\n useEffect(() => {\n apply();\n if (!observe || typeof document === 'undefined') return undefined;\n const observer = new MutationObserver(apply);\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: ['data-theme'],\n });\n return () => observer.disconnect();\n }, [apply, observe]);\n\n return { refresh: apply };\n}\n","'use client';\n\nimport type cytoscape from 'cytoscape';\nimport {\n forwardRef,\n useEffect,\n useImperativeHandle,\n useRef,\n type HTMLAttributes,\n type ReactNode,\n} from 'react';\n\nimport { buildShipItStylesheet, type BuildStylesheetOptions } from './stylesheet';\nimport { useShipItStylesheet } from './useShipItStylesheet';\n\n/**\n * GraphCanvas — high-level wrapper around a Cytoscape instance. Owns the\n * `data-theme` ↔ stylesheet sync (via {@link useShipItStylesheet}), the\n * cytoscape lifecycle (create / destroy on mount / unmount), and a thin\n * selection API on top of Cytoscape's events.\n *\n * The component never bundles Cytoscape itself — pass the engine factory in\n * via the `engine` prop so the consumer controls the Cytoscape version and\n * any registered extensions:\n *\n * ```tsx\n * import cytoscape from 'cytoscape';\n *\n * <GraphCanvas\n * engine={cytoscape}\n * elements={[...]}\n * layout={{ name: 'cose' }}\n * onSelect={(node) => setSelected(node.id())}\n * inspector={selected && <GraphInspector …/>}\n * />\n * ```\n */\n\nexport type CytoscapeEngine = (options: cytoscape.CytoscapeOptions) => cytoscape.Core;\n\nexport interface GraphCanvasHandle {\n /** Live Cytoscape instance. `null` until mount. */\n cy: cytoscape.Core | null;\n /** Re-read tokens and re-apply the stylesheet. */\n refreshStyles: () => void;\n}\n\nexport interface GraphCanvasProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n 'onSelect' | 'children'\n> {\n /** Cytoscape factory. Pass the imported `cytoscape` default export. */\n engine: CytoscapeEngine;\n /** Graph elements (nodes + edges). Passed straight through to Cytoscape. */\n elements: cytoscape.ElementDefinition[];\n /** Layout config. Defaults to a static `preset` layout (no auto-layout). */\n layout?: cytoscape.LayoutOptions;\n /** Fires when a node is tapped/selected. */\n onSelect?: (node: cytoscape.NodeSingular) => void;\n /** Fires when the selection is cleared (background tap). */\n onClearSelection?: () => void;\n /** Fires when the pointer enters a node. */\n onNodeHover?: (node: cytoscape.NodeSingular) => void;\n /** Fires when the pointer leaves a node. */\n onNodeLeave?: () => void;\n /** Slot rendered over the graph (e.g., a `<GraphInspector>`). Positioned top-right. */\n inspector?: ReactNode;\n /** Overrides for the stylesheet builder. */\n styleOptions?: BuildStylesheetOptions;\n /** Accessible label for the container. */\n 'aria-label'?: string;\n}\n\nconst DEFAULT_LAYOUT: cytoscape.LayoutOptions = { name: 'preset' };\n\nexport const GraphCanvas = forwardRef<GraphCanvasHandle, GraphCanvasProps>(function GraphCanvas(\n {\n engine,\n elements,\n layout = DEFAULT_LAYOUT,\n onSelect,\n onClearSelection,\n onNodeHover,\n onNodeLeave,\n inspector,\n styleOptions,\n className,\n 'aria-label': ariaLabel = 'Graph canvas',\n ...props\n },\n forwardedRef,\n) {\n const containerRef = useRef<HTMLDivElement | null>(null);\n const cyRef = useRef<cytoscape.Core | null>(null);\n\n // Create the cytoscape instance once on mount.\n useEffect(() => {\n if (!containerRef.current) return undefined;\n const cy = engine({\n container: containerRef.current,\n elements,\n layout,\n style: buildShipItStylesheet(styleOptions),\n });\n cyRef.current = cy;\n return () => {\n cy.destroy();\n cyRef.current = null;\n };\n // We intentionally re-create on engine swap or container remount only.\n // Elements / layout / style updates are handled in the effects below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [engine]);\n\n // Keep elements in sync.\n useEffect(() => {\n const cy = cyRef.current;\n if (!cy) return;\n cy.json({ elements });\n }, [elements]);\n\n // Re-run layout when the layout config changes.\n useEffect(() => {\n const cy = cyRef.current;\n if (!cy) return;\n cy.layout(layout).run();\n }, [layout]);\n\n const { refresh: refreshStyles } = useShipItStylesheet(cyRef, styleOptions);\n\n // Wire selection events. `engine` is in the deps so the listeners re-bind\n // whenever the upstream effect destroys + recreates the Cytoscape instance —\n // without it, an `engine` swap would leave the new `cy` without handlers.\n useEffect(() => {\n const cy = cyRef.current;\n if (!cy) return undefined;\n const handleSelect = (event: cytoscape.EventObject) => {\n if (event.target?.isNode?.()) {\n onSelect?.(event.target as cytoscape.NodeSingular);\n }\n };\n const handleBackgroundTap = (event: cytoscape.EventObject) => {\n if (event.target === cy) onClearSelection?.();\n };\n const handleEnter = (event: cytoscape.EventObject) => {\n onNodeHover?.(event.target as cytoscape.NodeSingular);\n };\n const handleLeave = () => onNodeLeave?.();\n cy.on('tap', 'node', handleSelect);\n cy.on('tap', handleBackgroundTap);\n cy.on('mouseover', 'node', handleEnter);\n cy.on('mouseout', 'node', handleLeave);\n return () => {\n cy.off('tap', 'node', handleSelect);\n cy.off('tap', handleBackgroundTap);\n cy.off('mouseover', 'node', handleEnter);\n cy.off('mouseout', 'node', handleLeave);\n };\n }, [engine, onSelect, onClearSelection, onNodeHover, onNodeLeave]);\n\n // Expose the imperative handle via getters so consumers always read the live\n // `cyRef.current`. Snapshotting `cyRef.current` here would freeze it at\n // `null` because the instance is assigned inside an effect that runs *after*\n // the initial render — Copilot/Claude both flagged this.\n useImperativeHandle(\n forwardedRef,\n (): GraphCanvasHandle => ({\n get cy() {\n return cyRef.current;\n },\n refreshStyles,\n }),\n [refreshStyles],\n );\n\n return (\n <div\n role=\"region\"\n aria-label={ariaLabel}\n className={['relative h-full w-full', className].filter(Boolean).join(' ')}\n {...props}\n >\n {/*\n * Inline styles, not Tailwind utilities. Cytoscape injects an unlayered\n * `.__________cytoscape_container { position: relative }` stylesheet at\n * init time, and Tailwind v4 emits `.absolute`/`.inset-0` into\n * `@layer utilities` — unlayered rules outrank layered ones regardless\n * of source order, so the canvas would collapse to 0×0 in a static-\n * height parent. Inline styles win against both.\n */}\n <div ref={containerRef} style={{ position: 'absolute', inset: 0 }} />\n {inspector && <div className=\"absolute top-4 right-4 z-10\">{inspector}</div>}\n </div>\n );\n});\n\nGraphCanvas.displayName = 'GraphCanvas';\n"],"mappings":"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","names":["useEffect","GraphCanvas","useEffect"]}
1
+ {"version":3,"sources":["../src/stylesheet.ts","../src/theme-tokens.ts","../src/useShipItStylesheet.ts","../src/GraphCanvas.tsx"],"sourcesContent":["import { iconToSvgDataUrl } from '@ship-it-ui/icons';\nimport { listEntityTypes } from '@ship-it-ui/shipit';\nimport type cytoscape from 'cytoscape';\n\nimport { readThemeTokens, resolveColorReference, type ThemeTokenPalette } from './theme-tokens';\n\n/**\n * Build a Cytoscape stylesheet from the live design tokens. The result is a\n * plain JSON array suitable for `cytoscape({ style: ... })` — re-run after a\n * `data-theme` change to pick up the new palette.\n *\n * The stylesheet is opinionated:\n * - Nodes are square-ish rounded glyphs colored by `data(entityType)`.\n * - Edges are token-colored thin lines with arrowheads.\n * - Selected / on-path / dimmed states are driven by class names that the\n * consumer toggles (`graph-canvas:selected`, `graph-canvas:path`,\n * `graph-canvas:dim`).\n *\n * Pass `palette` to override the token read — useful for SSR or tests.\n */\n\nexport interface BuildStylesheetOptions {\n /** Pre-resolved palette. When omitted, tokens are read from the document. */\n palette?: ThemeTokenPalette;\n /**\n * Additional entries appended to the stylesheet — handy for app-specific\n * selectors without forking the builder.\n */\n extra?: ReadonlyArray<cytoscape.StylesheetJsonBlock>;\n /**\n * Render each registered entity type's glyph (◇, ○, ▤, ↑, ◎, ▢ …) inside\n * the cytoscape node as a centered SVG data URL. Closes the visual gap\n * with the `<GraphNode>` React component — the docs page and the canvas\n * now share a vocabulary. Defaults to `true`. Pass `false` to fall back\n * to the original wireframe (border-only) per-type rule.\n */\n renderGlyphs?: boolean;\n}\n\n// Re-export the block type so consumers can declare typed `extra` entries.\nexport type ShipItStylesheetBlock = cytoscape.StylesheetJsonBlock;\n\nexport function buildShipItStylesheet(\n options: BuildStylesheetOptions = {},\n): cytoscape.StylesheetJson {\n const palette = options.palette ?? readThemeTokens();\n const color = (cssVar: string) => resolveColorReference(cssVar, palette);\n const renderGlyphs = options.renderGlyphs !== false;\n\n const base: cytoscape.StylesheetJsonBlock[] = [\n {\n selector: 'node',\n style: {\n 'background-color': palette.panel,\n 'border-width': 1.5,\n 'border-color': palette.accent,\n 'border-opacity': 1,\n label: 'data(label)',\n color: palette.textMuted,\n // Static stack instead of `var(--font-mono, monospace)` — cytoscape\n // can't resolve CSS variables outside the DOM cascade and emits a\n // warning per node selector at every mount. Consumers who need to\n // override the canvas font can do so via `options.extra`.\n 'font-family': 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',\n 'font-size': 10,\n 'text-valign': 'bottom',\n 'text-halign': 'center',\n 'text-margin-y': 6,\n // 52 matches `<GraphNode>`'s default size — the docs page and the\n // canvas now share dimensions. Pre-Issue-4 the canvas was 36×36.\n width: 52,\n height: 52,\n shape: 'round-rectangle',\n },\n },\n // One selector per entity type registered with @ship-it-ui/shipit. Built-in\n // types are seeded automatically; custom types registered via\n // `registerEntityType(...)` pick up their `colorVar` here without a docs\n // patch or a forked stylesheet.\n ...listEntityTypes().map<cytoscape.StylesheetJsonBlock>(([type, meta]) => {\n const c = color(meta.colorVar);\n return {\n selector: `node[entityType = \"${escapeCytoscapeAttr(type)}\"]`,\n style: renderGlyphs\n ? {\n 'border-color': c,\n 'background-image': iconToSvgDataUrl(meta.iconName, { color: c }),\n 'background-fit': 'contain',\n 'background-clip': 'none',\n }\n : { 'border-color': c },\n };\n }),\n {\n selector: 'node:selected',\n style: {\n 'border-width': 3,\n 'overlay-color': palette.accent,\n 'overlay-opacity': 0.15,\n 'overlay-padding': 4,\n },\n },\n {\n selector: 'node.graph-canvas\\\\:path',\n style: { 'border-color': palette.purple },\n },\n {\n selector: 'node.graph-canvas\\\\:dim',\n style: { opacity: 0.35 },\n },\n {\n // Default edges tone with `accent` so they read against the panel\n // background — matches `<GraphEdge edgeStyle=\"solid\">` in the docs.\n // Pre-fix the line drew in `palette.border` (the same tone as\n // surface-divider lines) and effectively disappeared on dark themes.\n selector: 'edge',\n style: {\n width: 1,\n 'line-color': palette.accent,\n 'target-arrow-color': palette.accent,\n 'target-arrow-shape': 'triangle',\n 'arrow-scale': 0.8,\n 'curve-style': 'bezier',\n },\n },\n {\n selector: 'edge.graph-canvas\\\\:path',\n style: {\n 'line-color': palette.purple,\n 'target-arrow-color': palette.purple,\n width: 1.5,\n },\n },\n {\n selector: 'edge.graph-canvas\\\\:dim',\n style: { opacity: 0.25 },\n },\n ];\n\n return [...base, ...(options.extra ?? [])] as cytoscape.StylesheetJson;\n}\n\n/**\n * Convenience class names the stylesheet recognizes. Toggle these on Cytoscape\n * nodes / edges to drive the on-path and dimmed visuals.\n */\nexport const GRAPH_CANVAS_CLASS = {\n path: 'graph-canvas:path',\n dim: 'graph-canvas:dim',\n} as const;\n\n// Cytoscape's attribute selector accepts a double-quoted string. Escape\n// embedded backslashes and double quotes so a malformed (or hostile)\n// registered key can't break out of the selector. The grammar\n// (https://js.cytoscape.org/#selectors/data) is more permissive than CSS, but\n// these two characters are the only ones that would change selector semantics.\nfunction escapeCytoscapeAttr(value: string): string {\n return value.replace(/[\\\\\"]/g, (c) => `\\\\${c}`);\n}\n","/**\n * Token resolution helpers. The Ship-It design system stores colors as CSS\n * custom properties on `<html>` (`--color-accent`, `--color-bg`, …). Cytoscape\n * renders to a canvas / SVG layer outside Tailwind, so those vars never\n * resolve — we read the *computed* values at runtime and feed them into the\n * stylesheet builder as concrete color strings.\n *\n * Two layers:\n * 1. `resolveCssVar` — single-var reader with a fallback.\n * 2. `readThemeTokens` — pulls every color the stylesheet uses, returning a\n * flat `Record<string, string>` keyed by short name (no `--color-`\n * prefix). Re-running this after a `data-theme` flip yields a fresh\n * palette.\n */\n\nimport { getEntityTypeMeta, type EntityType } from '@ship-it-ui/shipit';\n\nconst DEFAULT_FALLBACK: Record<string, string> = {\n bg: '#0a0a0a',\n panel: '#0f0f0f',\n 'panel-2': '#161616',\n border: '#262626',\n 'border-strong': '#383838',\n text: '#fafafa',\n 'text-muted': '#a3a3a3',\n 'text-dim': '#737373',\n accent: '#3b82f6',\n ok: '#10b981',\n warn: '#f59e0b',\n err: '#ef4444',\n purple: '#a855f7',\n pink: '#ec4899',\n};\n\n/**\n * Read a single CSS variable from the document root and return its trimmed\n * computed value, falling back to `fallback` when the document is missing\n * (SSR) or the variable is unset.\n */\nexport function resolveCssVar(name: string, fallback = ''): string {\n if (typeof document === 'undefined') return fallback;\n const raw = getComputedStyle(document.documentElement).getPropertyValue(name);\n const trimmed = raw.trim();\n return trimmed.length > 0 ? trimmed : fallback;\n}\n\n// Lazily-created 1×1 canvas used by `toSrgb` to coerce browser-parseable color\n// strings (oklch, lab, lch, named, etc.) into a plain `rgb()` / `rgba()` that\n// cytoscape's color parser accepts. Created on first use; the same canvas is\n// reused across calls so we don't allocate per token.\nlet coerceCanvas: HTMLCanvasElement | null = null;\n\n/**\n * Coerce an arbitrary CSS color string into an sRGB `rgb()` / `rgba()` string\n * via canvas pixel readback. Cytoscape's color parser doesn't accept modern\n * color functions (`oklch(...)`, `lab(...)`), and Tailwind v4 compiles every\n * `--color-*` token to `oklch(...)` — so when `readThemeTokens` reads those\n * tokens with `getComputedStyle`, the resulting palette would produce 70+\n * console warnings and silently fall back to defaults on every cytoscape\n * mount. Coercing once at the boundary fixes both the warnings and the\n * fallback colors.\n *\n * The implementation deliberately uses `fillRect` + `getImageData` rather\n * than reading `ctx.fillStyle` back — modern Chromium returns the literal\n * `oklch(...)` string from `ctx.fillStyle`, so the readback is the only\n * reliable way to force the browser's color pipeline to rasterize the value\n * down to sRGB.\n */\nexport function toSrgb(value: string): string {\n if (!value || typeof document === 'undefined') return value;\n // Fast path: already a parser-friendly value. Skips both the canvas\n // allocation and the readback round-trip for the common case where tokens\n // are already authored as `#xxx`, `rgb(...)`, `rgba(...)`, or `hsl(...)`.\n if (/^#|^rgb\\(|^rgba\\(|^hsl\\(|^hsla\\(/i.test(value)) return value;\n coerceCanvas ??= document.createElement('canvas');\n coerceCanvas.width = 1;\n coerceCanvas.height = 1;\n const ctx = coerceCanvas.getContext('2d', { willReadFrequently: true });\n if (!ctx) return value;\n ctx.clearRect(0, 0, 1, 1);\n // Two assignments so an unparseable `value` falls back to the seeded `#000`\n // instead of inheriting a stale fillStyle from a previous call.\n ctx.fillStyle = '#000';\n ctx.fillStyle = value;\n ctx.fillRect(0, 0, 1, 1);\n const data = ctx.getImageData(0, 0, 1, 1).data;\n const r = data[0];\n const g = data[1];\n const b = data[2];\n const a = data[3];\n if (r === undefined || g === undefined || b === undefined || a === undefined) return value;\n return a === 255 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${a / 255})`;\n}\n\nexport interface ThemeTokenPalette {\n /** Surface backgrounds. */\n bg: string;\n panel: string;\n panel2: string;\n /** Hairline + emphasis border. */\n border: string;\n borderStrong: string;\n /** Foreground tiers. */\n text: string;\n textMuted: string;\n textDim: string;\n /** Brand + status. */\n accent: string;\n ok: string;\n warn: string;\n err: string;\n /** Extras the graph uses for entity-type ring colors. */\n purple: string;\n pink: string;\n}\n\n/**\n * Read the canonical Ship-It color tokens from the document root, coerced to\n * sRGB. The `toSrgb` wrapper exists because Tailwind v4 compiles every\n * `--color-*` token to `oklch(...)`, which cytoscape's color parser rejects —\n * see {@link toSrgb} for the full reasoning.\n */\nexport function readThemeTokens(): ThemeTokenPalette {\n return {\n bg: toSrgb(resolveCssVar('--color-bg', DEFAULT_FALLBACK.bg)),\n panel: toSrgb(resolveCssVar('--color-panel', DEFAULT_FALLBACK.panel)),\n panel2: toSrgb(resolveCssVar('--color-panel-2', DEFAULT_FALLBACK['panel-2'])),\n border: toSrgb(resolveCssVar('--color-border', DEFAULT_FALLBACK.border)),\n borderStrong: toSrgb(resolveCssVar('--color-border-strong', DEFAULT_FALLBACK['border-strong'])),\n text: toSrgb(resolveCssVar('--color-text', DEFAULT_FALLBACK.text)),\n textMuted: toSrgb(resolveCssVar('--color-text-muted', DEFAULT_FALLBACK['text-muted'])),\n textDim: toSrgb(resolveCssVar('--color-text-dim', DEFAULT_FALLBACK['text-dim'])),\n accent: toSrgb(resolveCssVar('--color-accent', DEFAULT_FALLBACK.accent)),\n ok: toSrgb(resolveCssVar('--color-ok', DEFAULT_FALLBACK.ok)),\n warn: toSrgb(resolveCssVar('--color-warn', DEFAULT_FALLBACK.warn)),\n err: toSrgb(resolveCssVar('--color-err', DEFAULT_FALLBACK.err)),\n purple: toSrgb(resolveCssVar('--color-purple', DEFAULT_FALLBACK.purple)),\n pink: toSrgb(resolveCssVar('--color-pink', DEFAULT_FALLBACK.pink)),\n };\n}\n\n/**\n * Resolve the concrete color for a registered entity type. Reads the type's\n * `colorVar` (a `var(--color-…)` string) and looks the value up in the\n * palette. Falls back to the palette's `accent` color when the var is\n * malformed or unknown.\n */\nexport function resolveEntityColor(type: EntityType, palette: ThemeTokenPalette): string {\n const meta = getEntityTypeMeta(type);\n return resolveColorReference(meta.colorVar, palette);\n}\n\n/**\n * Extract `foo` from `var(--color-foo)` or `var(--color-foo, fallback)`.\n * Returns `undefined` if the input doesn't match. O(n), no backtracking.\n */\nfunction parseColorVarName(value: string): string | undefined {\n // Strip surrounding whitespace once. `String#trim` is linear, not a regex.\n let i = 0;\n while (i < value.length && isWhitespace(value.charCodeAt(i))) i++;\n if (!value.startsWith('var(', i)) return undefined;\n i += 4;\n // Skip whitespace inside the parens.\n while (i < value.length && isWhitespace(value.charCodeAt(i))) i++;\n if (!value.startsWith('--color-', i)) return undefined;\n i += 8;\n // Read the name: stop at the first whitespace, comma, or `)`.\n const start = i;\n while (i < value.length) {\n const c = value.charCodeAt(i);\n if (c === CC_COMMA || c === CC_PAREN_CLOSE || isWhitespace(c)) break;\n i++;\n }\n if (i === start) return undefined;\n // Require a closing `)` somewhere after, so we don't classify malformed\n // inputs (`var(--color-foo`) as valid references.\n const close = value.indexOf(')', i);\n if (close === -1) return undefined;\n return value.slice(start, i);\n}\n\nconst CC_COMMA = 0x2c;\nconst CC_PAREN_CLOSE = 0x29;\n\nfunction isWhitespace(cc: number): boolean {\n // ASCII whitespace: space, tab, LF, CR, FF, VT.\n return cc === 0x20 || cc === 0x09 || cc === 0x0a || cc === 0x0d || cc === 0x0c || cc === 0x0b;\n}\n\n/**\n * Map a `var(--color-foo)` reference or a raw color literal to a concrete\n * color string, using the supplied palette. Unrecognized references fall back\n * to `accent`.\n */\nexport function resolveColorReference(value: string, palette: ThemeTokenPalette): string {\n // Deterministic parser instead of a regex — regex variants with overlapping\n // quantifiers around the color name produced quadratic backtracking on\n // adversarial inputs (CodeQL js/polynomial-redos).\n const key = parseColorVarName(value);\n if (key === undefined) return value;\n switch (key) {\n case 'bg':\n return palette.bg;\n case 'panel':\n return palette.panel;\n case 'panel-2':\n return palette.panel2;\n case 'border':\n return palette.border;\n case 'border-strong':\n return palette.borderStrong;\n case 'text':\n return palette.text;\n case 'text-muted':\n return palette.textMuted;\n case 'text-dim':\n return palette.textDim;\n case 'accent':\n return palette.accent;\n case 'ok':\n return palette.ok;\n case 'warn':\n return palette.warn;\n case 'err':\n return palette.err;\n case 'purple':\n return palette.purple;\n case 'pink':\n return palette.pink;\n default:\n return palette.accent;\n }\n}\n","'use client';\n\nimport type cytoscape from 'cytoscape';\nimport { useCallback, useEffect, useMemo } from 'react';\n\nimport { buildShipItStylesheet, type BuildStylesheetOptions } from './stylesheet';\n\n/**\n * useShipItStylesheet — keeps a Cytoscape instance's stylesheet in sync with\n * the live design-token palette. Re-applies the stylesheet whenever\n * `<html data-theme>` flips, so toggling between dark and light propagates to\n * the graph without remounting.\n *\n * Returns a `refresh()` callback the consumer can invoke after any other\n * theme-affecting change (e.g., a `--color-accent` hue knob update).\n *\n * ```ts\n * const cyRef = useRef<cytoscape.Core | null>(null);\n * const { refresh } = useShipItStylesheet(cyRef);\n * ```\n */\n\nexport interface UseShipItStylesheetOptions extends BuildStylesheetOptions {\n /** Skip the MutationObserver wiring (e.g., when the host owns its own observer). */\n observe?: boolean;\n}\n\nexport interface UseShipItStylesheetReturn {\n /** Re-read tokens and re-apply the stylesheet. */\n refresh: () => void;\n}\n\nexport function useShipItStylesheet(\n cyRef: { current: cytoscape.Core | null },\n options: UseShipItStylesheetOptions = {},\n): UseShipItStylesheetReturn {\n const { observe = true, palette, extra } = options;\n // Memoize the build options against their flat constituents so callers that\n // pass `options` inline (a fresh object each render) don't churn `apply` /\n // disconnect+reconnect the MutationObserver on every render.\n const buildOptions = useMemo<BuildStylesheetOptions>(\n () => ({ palette, extra }),\n [palette, extra],\n );\n\n const apply = useCallback(() => {\n const cy = cyRef.current;\n if (!cy) return;\n cy.style(buildShipItStylesheet(buildOptions)).update();\n }, [cyRef, buildOptions]);\n\n useEffect(() => {\n apply();\n if (!observe || typeof document === 'undefined') return undefined;\n const observer = new MutationObserver(apply);\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: ['data-theme'],\n });\n return () => observer.disconnect();\n }, [apply, observe]);\n\n return { refresh: apply };\n}\n","'use client';\n\nimport type cytoscape from 'cytoscape';\nimport {\n forwardRef,\n useEffect,\n useImperativeHandle,\n useRef,\n type HTMLAttributes,\n type ReactNode,\n} from 'react';\n\nimport { buildShipItStylesheet, type BuildStylesheetOptions } from './stylesheet';\nimport { useShipItStylesheet } from './useShipItStylesheet';\n\n/**\n * GraphCanvas — high-level wrapper around a Cytoscape instance. Owns the\n * `data-theme` ↔ stylesheet sync (via {@link useShipItStylesheet}), the\n * cytoscape lifecycle (create / destroy on mount / unmount), and a thin\n * selection API on top of Cytoscape's events.\n *\n * The component never bundles Cytoscape itself — pass the engine factory in\n * via the `engine` prop so the consumer controls the Cytoscape version and\n * any registered extensions:\n *\n * ```tsx\n * import cytoscape from 'cytoscape';\n *\n * <GraphCanvas\n * engine={cytoscape}\n * elements={[...]}\n * layout={{ name: 'cose' }}\n * onSelect={(node) => setSelected(node.id())}\n * inspector={selected && <GraphInspector …/>}\n * />\n * ```\n */\n\nexport type CytoscapeEngine = (options: cytoscape.CytoscapeOptions) => cytoscape.Core;\n\nexport interface GraphCanvasHandle {\n /** Live Cytoscape instance. `null` until mount. */\n cy: cytoscape.Core | null;\n /** Re-read tokens and re-apply the stylesheet. */\n refreshStyles: () => void;\n}\n\nexport interface GraphCanvasProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n 'onSelect' | 'children'\n> {\n /** Cytoscape factory. Pass the imported `cytoscape` default export. */\n engine: CytoscapeEngine;\n /** Graph elements (nodes + edges). Passed straight through to Cytoscape. */\n elements: cytoscape.ElementDefinition[];\n /** Layout config. Defaults to a static `preset` layout (no auto-layout). */\n layout?: cytoscape.LayoutOptions;\n /** Fires when a node is tapped/selected. */\n onSelect?: (node: cytoscape.NodeSingular) => void;\n /** Fires when the selection is cleared (background tap). */\n onClearSelection?: () => void;\n /** Fires when the pointer enters a node. */\n onNodeHover?: (node: cytoscape.NodeSingular) => void;\n /** Fires when the pointer leaves a node. */\n onNodeLeave?: () => void;\n /** Slot rendered over the graph (e.g., a `<GraphInspector>`). Positioned top-right. */\n inspector?: ReactNode;\n /** Overrides for the stylesheet builder. */\n styleOptions?: BuildStylesheetOptions;\n /** Accessible label for the container. */\n 'aria-label'?: string;\n}\n\nconst DEFAULT_LAYOUT: cytoscape.LayoutOptions = { name: 'preset' };\n\nexport const GraphCanvas = forwardRef<GraphCanvasHandle, GraphCanvasProps>(function GraphCanvas(\n {\n engine,\n elements,\n layout = DEFAULT_LAYOUT,\n onSelect,\n onClearSelection,\n onNodeHover,\n onNodeLeave,\n inspector,\n styleOptions,\n className,\n 'aria-label': ariaLabel = 'Graph canvas',\n ...props\n },\n forwardedRef,\n) {\n const containerRef = useRef<HTMLDivElement | null>(null);\n const cyRef = useRef<cytoscape.Core | null>(null);\n\n // Create the cytoscape instance once on mount.\n useEffect(() => {\n if (!containerRef.current) return undefined;\n const cy = engine({\n container: containerRef.current,\n elements,\n layout,\n style: buildShipItStylesheet(styleOptions),\n });\n cyRef.current = cy;\n return () => {\n cy.destroy();\n cyRef.current = null;\n };\n // We intentionally re-create on engine swap or container remount only.\n // Elements / layout / style updates are handled in the effects below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [engine]);\n\n // Keep elements in sync.\n useEffect(() => {\n const cy = cyRef.current;\n if (!cy) return;\n cy.json({ elements });\n }, [elements]);\n\n // Re-run layout when the layout config changes.\n useEffect(() => {\n const cy = cyRef.current;\n if (!cy) return;\n cy.layout(layout).run();\n }, [layout]);\n\n const { refresh: refreshStyles } = useShipItStylesheet(cyRef, styleOptions);\n\n // Wire selection events. `engine` is in the deps so the listeners re-bind\n // whenever the upstream effect destroys + recreates the Cytoscape instance —\n // without it, an `engine` swap would leave the new `cy` without handlers.\n useEffect(() => {\n const cy = cyRef.current;\n if (!cy) return undefined;\n const handleSelect = (event: cytoscape.EventObject) => {\n if (event.target?.isNode?.()) {\n onSelect?.(event.target as cytoscape.NodeSingular);\n }\n };\n const handleBackgroundTap = (event: cytoscape.EventObject) => {\n if (event.target === cy) onClearSelection?.();\n };\n const handleEnter = (event: cytoscape.EventObject) => {\n onNodeHover?.(event.target as cytoscape.NodeSingular);\n };\n const handleLeave = () => onNodeLeave?.();\n cy.on('tap', 'node', handleSelect);\n cy.on('tap', handleBackgroundTap);\n cy.on('mouseover', 'node', handleEnter);\n cy.on('mouseout', 'node', handleLeave);\n return () => {\n cy.off('tap', 'node', handleSelect);\n cy.off('tap', handleBackgroundTap);\n cy.off('mouseover', 'node', handleEnter);\n cy.off('mouseout', 'node', handleLeave);\n };\n }, [engine, onSelect, onClearSelection, onNodeHover, onNodeLeave]);\n\n // Expose the imperative handle via getters so consumers always read the live\n // `cyRef.current`. Snapshotting `cyRef.current` here would freeze it at\n // `null` because the instance is assigned inside an effect that runs *after*\n // the initial render — Copilot/Claude both flagged this.\n useImperativeHandle(\n forwardedRef,\n (): GraphCanvasHandle => ({\n get cy() {\n return cyRef.current;\n },\n refreshStyles,\n }),\n [refreshStyles],\n );\n\n return (\n <div\n role=\"region\"\n aria-label={ariaLabel}\n className={['relative h-full w-full', className].filter(Boolean).join(' ')}\n {...props}\n >\n {/*\n * Inline styles, not Tailwind utilities. Cytoscape injects an unlayered\n * `.__________cytoscape_container { position: relative }` stylesheet at\n * init time, and Tailwind v4 emits `.absolute`/`.inset-0` into\n * `@layer utilities` — unlayered rules outrank layered ones regardless\n * of source order, so the canvas would collapse to 0×0 in a static-\n * height parent. Inline styles win against both.\n */}\n <div ref={containerRef} style={{ position: 'absolute', inset: 0 }} />\n {inspector && <div className=\"absolute top-4 right-4 z-10\">{inspector}</div>}\n </div>\n );\n});\n\nGraphCanvas.displayName = 'GraphCanvas';\n"],"mappings":"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","names":["useEffect","GraphCanvas","useEffect"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ship-it-ui/cytoscape",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "Cytoscape adapter for the Ship-It design system. Token-driven stylesheet, theme-aware re-resolver hook, and a `<GraphCanvas>` wrapper that owns the data-theme ↔ stylesheet ↔ inspector dance.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://ship-it-ops.github.io/ship-it-design/",
@@ -44,8 +44,9 @@
44
44
  "cytoscape": "^3.28.0",
45
45
  "react": "^18.0.0 || ^19.0.0",
46
46
  "react-dom": "^18.0.0 || ^19.0.0",
47
- "@ship-it-ui/shipit": "0.0.6",
48
- "@ship-it-ui/ui": "0.0.5"
47
+ "@ship-it-ui/icons": "0.0.6",
48
+ "@ship-it-ui/ui": "0.0.6",
49
+ "@ship-it-ui/shipit": "0.0.7"
49
50
  },
50
51
  "devDependencies": {
51
52
  "@testing-library/jest-dom": "^6.6.3",
@@ -63,10 +64,11 @@
63
64
  "vitest": "^2.1.3",
64
65
  "vitest-axe": "^0.1.0",
65
66
  "@ship-it-ui/eslint-config": "0.0.1",
66
- "@ship-it-ui/shipit": "0.0.6",
67
- "@ship-it-ui/ui": "0.0.5",
67
+ "@ship-it-ui/icons": "0.0.6",
68
+ "@ship-it-ui/shipit": "0.0.7",
68
69
  "@ship-it-ui/tokens": "0.0.5",
69
- "@ship-it-ui/tsconfig": "0.0.1"
70
+ "@ship-it-ui/tsconfig": "0.0.1",
71
+ "@ship-it-ui/ui": "0.0.6"
70
72
  },
71
73
  "scripts": {
72
74
  "build": "tsup",