@ship-it-ui/cytoscape 0.0.6 → 0.0.8
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 +21 -133
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -54
- package/dist/index.d.ts +17 -54
- package/dist/index.js +22 -129
- package/dist/index.js.map +1 -1
- package/package.json +11 -8
package/dist/index.cjs
CHANGED
|
@@ -25,9 +25,9 @@ __export(index_exports, {
|
|
|
25
25
|
GRAPH_CANVAS_CLASS: () => GRAPH_CANVAS_CLASS,
|
|
26
26
|
GraphCanvas: () => GraphCanvas,
|
|
27
27
|
buildShipItStylesheet: () => buildShipItStylesheet,
|
|
28
|
-
readThemeTokens: () => readThemeTokens,
|
|
29
|
-
resolveColorReference: () => resolveColorReference,
|
|
30
|
-
resolveCssVar: () => resolveCssVar,
|
|
28
|
+
readThemeTokens: () => import_graph_tokens2.readThemeTokens,
|
|
29
|
+
resolveColorReference: () => import_graph_tokens2.resolveColorReference,
|
|
30
|
+
resolveCssVar: () => import_graph_tokens2.resolveCssVar,
|
|
31
31
|
resolveEntityColor: () => resolveEntityColor,
|
|
32
32
|
useShipItStylesheet: () => useShipItStylesheet
|
|
33
33
|
});
|
|
@@ -39,137 +39,20 @@ var import_shipit2 = require("@ship-it-ui/shipit");
|
|
|
39
39
|
|
|
40
40
|
// src/theme-tokens.ts
|
|
41
41
|
var import_shipit = require("@ship-it-ui/shipit");
|
|
42
|
-
var
|
|
43
|
-
|
|
44
|
-
panel: "#0f0f0f",
|
|
45
|
-
"panel-2": "#161616",
|
|
46
|
-
border: "#262626",
|
|
47
|
-
"border-strong": "#383838",
|
|
48
|
-
text: "#fafafa",
|
|
49
|
-
"text-muted": "#a3a3a3",
|
|
50
|
-
"text-dim": "#737373",
|
|
51
|
-
accent: "#3b82f6",
|
|
52
|
-
ok: "#10b981",
|
|
53
|
-
warn: "#f59e0b",
|
|
54
|
-
err: "#ef4444",
|
|
55
|
-
purple: "#a855f7",
|
|
56
|
-
pink: "#ec4899"
|
|
57
|
-
};
|
|
58
|
-
function resolveCssVar(name, fallback = "") {
|
|
59
|
-
if (typeof document === "undefined") return fallback;
|
|
60
|
-
const raw = getComputedStyle(document.documentElement).getPropertyValue(name);
|
|
61
|
-
const trimmed = raw.trim();
|
|
62
|
-
return trimmed.length > 0 ? trimmed : fallback;
|
|
63
|
-
}
|
|
64
|
-
var coerceCanvas = null;
|
|
65
|
-
function toSrgb(value) {
|
|
66
|
-
if (!value || typeof document === "undefined") return value;
|
|
67
|
-
if (/^#|^rgb\(|^rgba\(|^hsl\(|^hsla\(/i.test(value)) return value;
|
|
68
|
-
coerceCanvas ??= document.createElement("canvas");
|
|
69
|
-
coerceCanvas.width = 1;
|
|
70
|
-
coerceCanvas.height = 1;
|
|
71
|
-
const ctx = coerceCanvas.getContext("2d", { willReadFrequently: true });
|
|
72
|
-
if (!ctx) return value;
|
|
73
|
-
ctx.clearRect(0, 0, 1, 1);
|
|
74
|
-
ctx.fillStyle = "#000";
|
|
75
|
-
ctx.fillStyle = value;
|
|
76
|
-
ctx.fillRect(0, 0, 1, 1);
|
|
77
|
-
const data = ctx.getImageData(0, 0, 1, 1).data;
|
|
78
|
-
const r = data[0];
|
|
79
|
-
const g = data[1];
|
|
80
|
-
const b = data[2];
|
|
81
|
-
const a = data[3];
|
|
82
|
-
if (r === void 0 || g === void 0 || b === void 0 || a === void 0) return value;
|
|
83
|
-
return a === 255 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${a / 255})`;
|
|
84
|
-
}
|
|
85
|
-
function readThemeTokens() {
|
|
86
|
-
return {
|
|
87
|
-
bg: toSrgb(resolveCssVar("--color-bg", DEFAULT_FALLBACK.bg)),
|
|
88
|
-
panel: toSrgb(resolveCssVar("--color-panel", DEFAULT_FALLBACK.panel)),
|
|
89
|
-
panel2: toSrgb(resolveCssVar("--color-panel-2", DEFAULT_FALLBACK["panel-2"])),
|
|
90
|
-
border: toSrgb(resolveCssVar("--color-border", DEFAULT_FALLBACK.border)),
|
|
91
|
-
borderStrong: toSrgb(resolveCssVar("--color-border-strong", DEFAULT_FALLBACK["border-strong"])),
|
|
92
|
-
text: toSrgb(resolveCssVar("--color-text", DEFAULT_FALLBACK.text)),
|
|
93
|
-
textMuted: toSrgb(resolveCssVar("--color-text-muted", DEFAULT_FALLBACK["text-muted"])),
|
|
94
|
-
textDim: toSrgb(resolveCssVar("--color-text-dim", DEFAULT_FALLBACK["text-dim"])),
|
|
95
|
-
accent: toSrgb(resolveCssVar("--color-accent", DEFAULT_FALLBACK.accent)),
|
|
96
|
-
ok: toSrgb(resolveCssVar("--color-ok", DEFAULT_FALLBACK.ok)),
|
|
97
|
-
warn: toSrgb(resolveCssVar("--color-warn", DEFAULT_FALLBACK.warn)),
|
|
98
|
-
err: toSrgb(resolveCssVar("--color-err", DEFAULT_FALLBACK.err)),
|
|
99
|
-
purple: toSrgb(resolveCssVar("--color-purple", DEFAULT_FALLBACK.purple)),
|
|
100
|
-
pink: toSrgb(resolveCssVar("--color-pink", DEFAULT_FALLBACK.pink))
|
|
101
|
-
};
|
|
102
|
-
}
|
|
42
|
+
var import_graph_tokens = require("@ship-it-ui/graph-tokens");
|
|
43
|
+
var import_graph_tokens2 = require("@ship-it-ui/graph-tokens");
|
|
103
44
|
function resolveEntityColor(type, palette) {
|
|
104
45
|
const meta = (0, import_shipit.getEntityTypeMeta)(type);
|
|
105
|
-
return resolveColorReference(meta.colorVar, palette);
|
|
106
|
-
}
|
|
107
|
-
function parseColorVarName(value) {
|
|
108
|
-
let i = 0;
|
|
109
|
-
while (i < value.length && isWhitespace(value.charCodeAt(i))) i++;
|
|
110
|
-
if (!value.startsWith("var(", i)) return void 0;
|
|
111
|
-
i += 4;
|
|
112
|
-
while (i < value.length && isWhitespace(value.charCodeAt(i))) i++;
|
|
113
|
-
if (!value.startsWith("--color-", i)) return void 0;
|
|
114
|
-
i += 8;
|
|
115
|
-
const start = i;
|
|
116
|
-
while (i < value.length) {
|
|
117
|
-
const c = value.charCodeAt(i);
|
|
118
|
-
if (c === CC_COMMA || c === CC_PAREN_CLOSE || isWhitespace(c)) break;
|
|
119
|
-
i++;
|
|
120
|
-
}
|
|
121
|
-
if (i === start) return void 0;
|
|
122
|
-
const close = value.indexOf(")", i);
|
|
123
|
-
if (close === -1) return void 0;
|
|
124
|
-
return value.slice(start, i);
|
|
125
|
-
}
|
|
126
|
-
var CC_COMMA = 44;
|
|
127
|
-
var CC_PAREN_CLOSE = 41;
|
|
128
|
-
function isWhitespace(cc) {
|
|
129
|
-
return cc === 32 || cc === 9 || cc === 10 || cc === 13 || cc === 12 || cc === 11;
|
|
130
|
-
}
|
|
131
|
-
function resolveColorReference(value, palette) {
|
|
132
|
-
const key = parseColorVarName(value);
|
|
133
|
-
if (key === void 0) return value;
|
|
134
|
-
switch (key) {
|
|
135
|
-
case "bg":
|
|
136
|
-
return palette.bg;
|
|
137
|
-
case "panel":
|
|
138
|
-
return palette.panel;
|
|
139
|
-
case "panel-2":
|
|
140
|
-
return palette.panel2;
|
|
141
|
-
case "border":
|
|
142
|
-
return palette.border;
|
|
143
|
-
case "border-strong":
|
|
144
|
-
return palette.borderStrong;
|
|
145
|
-
case "text":
|
|
146
|
-
return palette.text;
|
|
147
|
-
case "text-muted":
|
|
148
|
-
return palette.textMuted;
|
|
149
|
-
case "text-dim":
|
|
150
|
-
return palette.textDim;
|
|
151
|
-
case "accent":
|
|
152
|
-
return palette.accent;
|
|
153
|
-
case "ok":
|
|
154
|
-
return palette.ok;
|
|
155
|
-
case "warn":
|
|
156
|
-
return palette.warn;
|
|
157
|
-
case "err":
|
|
158
|
-
return palette.err;
|
|
159
|
-
case "purple":
|
|
160
|
-
return palette.purple;
|
|
161
|
-
case "pink":
|
|
162
|
-
return palette.pink;
|
|
163
|
-
default:
|
|
164
|
-
return palette.accent;
|
|
165
|
-
}
|
|
46
|
+
return (0, import_graph_tokens.resolveColorReference)(meta.colorVar, palette);
|
|
166
47
|
}
|
|
167
48
|
|
|
168
49
|
// src/stylesheet.ts
|
|
169
50
|
function buildShipItStylesheet(options = {}) {
|
|
170
|
-
const palette = options.palette ?? readThemeTokens();
|
|
171
|
-
const color = (cssVar) => resolveColorReference(cssVar, palette);
|
|
51
|
+
const palette = options.palette ?? (0, import_graph_tokens2.readThemeTokens)();
|
|
52
|
+
const color = (cssVar) => (0, import_graph_tokens2.resolveColorReference)(cssVar, palette);
|
|
172
53
|
const renderGlyphs = options.renderGlyphs !== false;
|
|
54
|
+
const glyphScale = Math.max(0, Math.min(1, options.glyphScale ?? 0.5));
|
|
55
|
+
const glyphSizePct = `${Math.round(glyphScale * 100)}%`;
|
|
173
56
|
const base = [
|
|
174
57
|
{
|
|
175
58
|
selector: "node",
|
|
@@ -202,14 +85,19 @@ function buildShipItStylesheet(options = {}) {
|
|
|
202
85
|
// patch or a forked stylesheet.
|
|
203
86
|
...(0, import_shipit2.listEntityTypes)().map(([type, meta]) => {
|
|
204
87
|
const c = color(meta.colorVar);
|
|
88
|
+
const glyphStyle = renderGlyphs ? {
|
|
89
|
+
"border-color": c,
|
|
90
|
+
"background-image": (0, import_icons.iconToSvgDataUrl)(meta.iconName, { color: c }),
|
|
91
|
+
"background-fit": "none",
|
|
92
|
+
"background-clip": "none",
|
|
93
|
+
"background-width": glyphSizePct,
|
|
94
|
+
"background-height": glyphSizePct,
|
|
95
|
+
"background-position-x": "50%",
|
|
96
|
+
"background-position-y": "50%"
|
|
97
|
+
} : { "border-color": c };
|
|
205
98
|
return {
|
|
206
99
|
selector: `node[entityType = "${escapeCytoscapeAttr(type)}"]`,
|
|
207
|
-
style:
|
|
208
|
-
"border-color": c,
|
|
209
|
-
"background-image": (0, import_icons.iconToSvgDataUrl)(meta.iconName, { color: c }),
|
|
210
|
-
"background-fit": "contain",
|
|
211
|
-
"background-clip": "none"
|
|
212
|
-
} : { "border-color": c }
|
|
100
|
+
style: glyphStyle
|
|
213
101
|
};
|
|
214
102
|
}),
|
|
215
103
|
{
|
package/dist/index.cjs.map
CHANGED
|
@@ -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 { 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"]}
|
|
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 * Fraction of the node that the rendered glyph occupies. Default `0.5` —\n * matches `<GraphNode>` where the icon is sized at ~42% of the square and\n * leaves breathing room inside the border. Set to `1` to revert to the\n * pre-0.0.7 behavior where the glyph filled the node edge-to-edge.\n */\n glyphScale?: number;\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 // Clamp into a sensible range. `0` would hide the glyph entirely; `1` paints\n // edge-to-edge (the legacy behavior). The default `0.5` matches the 26/52\n // ratio used by the `<GraphNode>` React component.\n const glyphScale = Math.max(0, Math.min(1, options.glyphScale ?? 0.5));\n const glyphSizePct = `${Math.round(glyphScale * 100)}%`;\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 // Pre-0.0.7 the glyph used `background-fit: contain`, which scales the\n // image to fill the node edge-to-edge and ignores the painted SVG's\n // intrinsic size. The icon ended up touching the border on every side\n // and looked cramped against the 52×52 node. We now set\n // `background-fit: none` and pin the painted width/height to a\n // fraction of the node (default 50%), with the image's anchor centered\n // — that gives the glyph breathing room and visually aligns with the\n // `<GraphNode>` React component used elsewhere in the design system.\n //\n // Cast through `unknown` because `cytoscape.Css.Node` doesn't (yet)\n // expose `background-width`/`background-height` in its TS types,\n // though the runtime accepts them per the cytoscape.js reference.\n const glyphStyle = renderGlyphs\n ? ({\n 'border-color': c,\n 'background-image': iconToSvgDataUrl(meta.iconName, { color: c }),\n 'background-fit': 'none',\n 'background-clip': 'none',\n 'background-width': glyphSizePct,\n 'background-height': glyphSizePct,\n 'background-position-x': '50%',\n 'background-position-y': '50%',\n } as unknown as cytoscape.Css.Node)\n : { 'border-color': c };\n return {\n selector: `node[entityType = \"${escapeCytoscapeAttr(type)}\"]`,\n style: glyphStyle,\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 * Cytoscape-side wrapper around `@ship-it-ui/graph-tokens`. The token-reader\n * primitives (`resolveCssVar`, `toSrgb`, `readThemeTokens`,\n * `resolveColorReference`, `ThemeTokenPalette`) live in the shared package so\n * `@ship-it-ui/graph-editor` (React Flow) can consume them without dragging\n * Cytoscape in. Only `resolveEntityColor` lives here because it bridges to the\n * `@ship-it-ui/shipit` entity-type registry.\n */\n\nimport { getEntityTypeMeta, type EntityType } from '@ship-it-ui/shipit';\nimport { resolveColorReference, type ThemeTokenPalette } from '@ship-it-ui/graph-tokens';\n\nexport {\n readThemeTokens,\n resolveColorReference,\n resolveCssVar,\n toSrgb,\n type ThemeTokenPalette,\n} from '@ship-it-ui/graph-tokens';\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","'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_graph_tokens","import_react","GraphCanvas"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,57 +1,19 @@
|
|
|
1
1
|
import cytoscape from 'cytoscape';
|
|
2
|
-
import {
|
|
2
|
+
import { ThemeTokenPalette } from '@ship-it-ui/graph-tokens';
|
|
3
|
+
export { ThemeTokenPalette, readThemeTokens, resolveColorReference, resolveCssVar } from '@ship-it-ui/graph-tokens';
|
|
3
4
|
import * as react from 'react';
|
|
4
5
|
import { HTMLAttributes, ReactNode } from 'react';
|
|
6
|
+
import { EntityType } from '@ship-it-ui/shipit';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* Two layers:
|
|
14
|
-
* 1. `resolveCssVar` — single-var reader with a fallback.
|
|
15
|
-
* 2. `readThemeTokens` — pulls every color the stylesheet uses, returning a
|
|
16
|
-
* flat `Record<string, string>` keyed by short name (no `--color-`
|
|
17
|
-
* prefix). Re-running this after a `data-theme` flip yields a fresh
|
|
18
|
-
* palette.
|
|
9
|
+
* Cytoscape-side wrapper around `@ship-it-ui/graph-tokens`. The token-reader
|
|
10
|
+
* primitives (`resolveCssVar`, `toSrgb`, `readThemeTokens`,
|
|
11
|
+
* `resolveColorReference`, `ThemeTokenPalette`) live in the shared package so
|
|
12
|
+
* `@ship-it-ui/graph-editor` (React Flow) can consume them without dragging
|
|
13
|
+
* Cytoscape in. Only `resolveEntityColor` lives here because it bridges to the
|
|
14
|
+
* `@ship-it-ui/shipit` entity-type registry.
|
|
19
15
|
*/
|
|
20
16
|
|
|
21
|
-
/**
|
|
22
|
-
* Read a single CSS variable from the document root and return its trimmed
|
|
23
|
-
* computed value, falling back to `fallback` when the document is missing
|
|
24
|
-
* (SSR) or the variable is unset.
|
|
25
|
-
*/
|
|
26
|
-
declare function resolveCssVar(name: string, fallback?: string): string;
|
|
27
|
-
interface ThemeTokenPalette {
|
|
28
|
-
/** Surface backgrounds. */
|
|
29
|
-
bg: string;
|
|
30
|
-
panel: string;
|
|
31
|
-
panel2: string;
|
|
32
|
-
/** Hairline + emphasis border. */
|
|
33
|
-
border: string;
|
|
34
|
-
borderStrong: string;
|
|
35
|
-
/** Foreground tiers. */
|
|
36
|
-
text: string;
|
|
37
|
-
textMuted: string;
|
|
38
|
-
textDim: string;
|
|
39
|
-
/** Brand + status. */
|
|
40
|
-
accent: string;
|
|
41
|
-
ok: string;
|
|
42
|
-
warn: string;
|
|
43
|
-
err: string;
|
|
44
|
-
/** Extras the graph uses for entity-type ring colors. */
|
|
45
|
-
purple: string;
|
|
46
|
-
pink: string;
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Read the canonical Ship-It color tokens from the document root, coerced to
|
|
50
|
-
* sRGB. The `toSrgb` wrapper exists because Tailwind v4 compiles every
|
|
51
|
-
* `--color-*` token to `oklch(...)`, which cytoscape's color parser rejects —
|
|
52
|
-
* see {@link toSrgb} for the full reasoning.
|
|
53
|
-
*/
|
|
54
|
-
declare function readThemeTokens(): ThemeTokenPalette;
|
|
55
17
|
/**
|
|
56
18
|
* Resolve the concrete color for a registered entity type. Reads the type's
|
|
57
19
|
* `colorVar` (a `var(--color-…)` string) and looks the value up in the
|
|
@@ -59,12 +21,6 @@ declare function readThemeTokens(): ThemeTokenPalette;
|
|
|
59
21
|
* malformed or unknown.
|
|
60
22
|
*/
|
|
61
23
|
declare function resolveEntityColor(type: EntityType, palette: ThemeTokenPalette): string;
|
|
62
|
-
/**
|
|
63
|
-
* Map a `var(--color-foo)` reference or a raw color literal to a concrete
|
|
64
|
-
* color string, using the supplied palette. Unrecognized references fall back
|
|
65
|
-
* to `accent`.
|
|
66
|
-
*/
|
|
67
|
-
declare function resolveColorReference(value: string, palette: ThemeTokenPalette): string;
|
|
68
24
|
|
|
69
25
|
/**
|
|
70
26
|
* Build a Cytoscape stylesheet from the live design tokens. The result is a
|
|
@@ -96,6 +52,13 @@ interface BuildStylesheetOptions {
|
|
|
96
52
|
* to the original wireframe (border-only) per-type rule.
|
|
97
53
|
*/
|
|
98
54
|
renderGlyphs?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Fraction of the node that the rendered glyph occupies. Default `0.5` —
|
|
57
|
+
* matches `<GraphNode>` where the icon is sized at ~42% of the square and
|
|
58
|
+
* leaves breathing room inside the border. Set to `1` to revert to the
|
|
59
|
+
* pre-0.0.7 behavior where the glyph filled the node edge-to-edge.
|
|
60
|
+
*/
|
|
61
|
+
glyphScale?: number;
|
|
99
62
|
}
|
|
100
63
|
type ShipItStylesheetBlock = cytoscape.StylesheetJsonBlock;
|
|
101
64
|
declare function buildShipItStylesheet(options?: BuildStylesheetOptions): cytoscape.StylesheetJson;
|
|
@@ -187,4 +150,4 @@ interface GraphCanvasProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onSelec
|
|
|
187
150
|
}
|
|
188
151
|
declare const GraphCanvas: react.ForwardRefExoticComponent<GraphCanvasProps & react.RefAttributes<GraphCanvasHandle>>;
|
|
189
152
|
|
|
190
|
-
export { type BuildStylesheetOptions, type CytoscapeEngine, GRAPH_CANVAS_CLASS, GraphCanvas, type GraphCanvasHandle, type GraphCanvasProps, type ShipItStylesheetBlock, type
|
|
153
|
+
export { type BuildStylesheetOptions, type CytoscapeEngine, GRAPH_CANVAS_CLASS, GraphCanvas, type GraphCanvasHandle, type GraphCanvasProps, type ShipItStylesheetBlock, type UseShipItStylesheetOptions, type UseShipItStylesheetReturn, buildShipItStylesheet, resolveEntityColor, useShipItStylesheet };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,57 +1,19 @@
|
|
|
1
1
|
import cytoscape from 'cytoscape';
|
|
2
|
-
import {
|
|
2
|
+
import { ThemeTokenPalette } from '@ship-it-ui/graph-tokens';
|
|
3
|
+
export { ThemeTokenPalette, readThemeTokens, resolveColorReference, resolveCssVar } from '@ship-it-ui/graph-tokens';
|
|
3
4
|
import * as react from 'react';
|
|
4
5
|
import { HTMLAttributes, ReactNode } from 'react';
|
|
6
|
+
import { EntityType } from '@ship-it-ui/shipit';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* Two layers:
|
|
14
|
-
* 1. `resolveCssVar` — single-var reader with a fallback.
|
|
15
|
-
* 2. `readThemeTokens` — pulls every color the stylesheet uses, returning a
|
|
16
|
-
* flat `Record<string, string>` keyed by short name (no `--color-`
|
|
17
|
-
* prefix). Re-running this after a `data-theme` flip yields a fresh
|
|
18
|
-
* palette.
|
|
9
|
+
* Cytoscape-side wrapper around `@ship-it-ui/graph-tokens`. The token-reader
|
|
10
|
+
* primitives (`resolveCssVar`, `toSrgb`, `readThemeTokens`,
|
|
11
|
+
* `resolveColorReference`, `ThemeTokenPalette`) live in the shared package so
|
|
12
|
+
* `@ship-it-ui/graph-editor` (React Flow) can consume them without dragging
|
|
13
|
+
* Cytoscape in. Only `resolveEntityColor` lives here because it bridges to the
|
|
14
|
+
* `@ship-it-ui/shipit` entity-type registry.
|
|
19
15
|
*/
|
|
20
16
|
|
|
21
|
-
/**
|
|
22
|
-
* Read a single CSS variable from the document root and return its trimmed
|
|
23
|
-
* computed value, falling back to `fallback` when the document is missing
|
|
24
|
-
* (SSR) or the variable is unset.
|
|
25
|
-
*/
|
|
26
|
-
declare function resolveCssVar(name: string, fallback?: string): string;
|
|
27
|
-
interface ThemeTokenPalette {
|
|
28
|
-
/** Surface backgrounds. */
|
|
29
|
-
bg: string;
|
|
30
|
-
panel: string;
|
|
31
|
-
panel2: string;
|
|
32
|
-
/** Hairline + emphasis border. */
|
|
33
|
-
border: string;
|
|
34
|
-
borderStrong: string;
|
|
35
|
-
/** Foreground tiers. */
|
|
36
|
-
text: string;
|
|
37
|
-
textMuted: string;
|
|
38
|
-
textDim: string;
|
|
39
|
-
/** Brand + status. */
|
|
40
|
-
accent: string;
|
|
41
|
-
ok: string;
|
|
42
|
-
warn: string;
|
|
43
|
-
err: string;
|
|
44
|
-
/** Extras the graph uses for entity-type ring colors. */
|
|
45
|
-
purple: string;
|
|
46
|
-
pink: string;
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Read the canonical Ship-It color tokens from the document root, coerced to
|
|
50
|
-
* sRGB. The `toSrgb` wrapper exists because Tailwind v4 compiles every
|
|
51
|
-
* `--color-*` token to `oklch(...)`, which cytoscape's color parser rejects —
|
|
52
|
-
* see {@link toSrgb} for the full reasoning.
|
|
53
|
-
*/
|
|
54
|
-
declare function readThemeTokens(): ThemeTokenPalette;
|
|
55
17
|
/**
|
|
56
18
|
* Resolve the concrete color for a registered entity type. Reads the type's
|
|
57
19
|
* `colorVar` (a `var(--color-…)` string) and looks the value up in the
|
|
@@ -59,12 +21,6 @@ declare function readThemeTokens(): ThemeTokenPalette;
|
|
|
59
21
|
* malformed or unknown.
|
|
60
22
|
*/
|
|
61
23
|
declare function resolveEntityColor(type: EntityType, palette: ThemeTokenPalette): string;
|
|
62
|
-
/**
|
|
63
|
-
* Map a `var(--color-foo)` reference or a raw color literal to a concrete
|
|
64
|
-
* color string, using the supplied palette. Unrecognized references fall back
|
|
65
|
-
* to `accent`.
|
|
66
|
-
*/
|
|
67
|
-
declare function resolveColorReference(value: string, palette: ThemeTokenPalette): string;
|
|
68
24
|
|
|
69
25
|
/**
|
|
70
26
|
* Build a Cytoscape stylesheet from the live design tokens. The result is a
|
|
@@ -96,6 +52,13 @@ interface BuildStylesheetOptions {
|
|
|
96
52
|
* to the original wireframe (border-only) per-type rule.
|
|
97
53
|
*/
|
|
98
54
|
renderGlyphs?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Fraction of the node that the rendered glyph occupies. Default `0.5` —
|
|
57
|
+
* matches `<GraphNode>` where the icon is sized at ~42% of the square and
|
|
58
|
+
* leaves breathing room inside the border. Set to `1` to revert to the
|
|
59
|
+
* pre-0.0.7 behavior where the glyph filled the node edge-to-edge.
|
|
60
|
+
*/
|
|
61
|
+
glyphScale?: number;
|
|
99
62
|
}
|
|
100
63
|
type ShipItStylesheetBlock = cytoscape.StylesheetJsonBlock;
|
|
101
64
|
declare function buildShipItStylesheet(options?: BuildStylesheetOptions): cytoscape.StylesheetJson;
|
|
@@ -187,4 +150,4 @@ interface GraphCanvasProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onSelec
|
|
|
187
150
|
}
|
|
188
151
|
declare const GraphCanvas: react.ForwardRefExoticComponent<GraphCanvasProps & react.RefAttributes<GraphCanvasHandle>>;
|
|
189
152
|
|
|
190
|
-
export { type BuildStylesheetOptions, type CytoscapeEngine, GRAPH_CANVAS_CLASS, GraphCanvas, type GraphCanvasHandle, type GraphCanvasProps, type ShipItStylesheetBlock, type
|
|
153
|
+
export { type BuildStylesheetOptions, type CytoscapeEngine, GRAPH_CANVAS_CLASS, GraphCanvas, type GraphCanvasHandle, type GraphCanvasProps, type ShipItStylesheetBlock, type UseShipItStylesheetOptions, type UseShipItStylesheetReturn, buildShipItStylesheet, resolveEntityColor, useShipItStylesheet };
|
package/dist/index.js
CHANGED
|
@@ -6,137 +6,25 @@ import { listEntityTypes } from "@ship-it-ui/shipit";
|
|
|
6
6
|
|
|
7
7
|
// src/theme-tokens.ts
|
|
8
8
|
import { getEntityTypeMeta } from "@ship-it-ui/shipit";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"text-muted": "#a3a3a3",
|
|
17
|
-
"text-dim": "#737373",
|
|
18
|
-
accent: "#3b82f6",
|
|
19
|
-
ok: "#10b981",
|
|
20
|
-
warn: "#f59e0b",
|
|
21
|
-
err: "#ef4444",
|
|
22
|
-
purple: "#a855f7",
|
|
23
|
-
pink: "#ec4899"
|
|
24
|
-
};
|
|
25
|
-
function resolveCssVar(name, fallback = "") {
|
|
26
|
-
if (typeof document === "undefined") return fallback;
|
|
27
|
-
const raw = getComputedStyle(document.documentElement).getPropertyValue(name);
|
|
28
|
-
const trimmed = raw.trim();
|
|
29
|
-
return trimmed.length > 0 ? trimmed : fallback;
|
|
30
|
-
}
|
|
31
|
-
var coerceCanvas = null;
|
|
32
|
-
function toSrgb(value) {
|
|
33
|
-
if (!value || typeof document === "undefined") return value;
|
|
34
|
-
if (/^#|^rgb\(|^rgba\(|^hsl\(|^hsla\(/i.test(value)) return value;
|
|
35
|
-
coerceCanvas ??= document.createElement("canvas");
|
|
36
|
-
coerceCanvas.width = 1;
|
|
37
|
-
coerceCanvas.height = 1;
|
|
38
|
-
const ctx = coerceCanvas.getContext("2d", { willReadFrequently: true });
|
|
39
|
-
if (!ctx) return value;
|
|
40
|
-
ctx.clearRect(0, 0, 1, 1);
|
|
41
|
-
ctx.fillStyle = "#000";
|
|
42
|
-
ctx.fillStyle = value;
|
|
43
|
-
ctx.fillRect(0, 0, 1, 1);
|
|
44
|
-
const data = ctx.getImageData(0, 0, 1, 1).data;
|
|
45
|
-
const r = data[0];
|
|
46
|
-
const g = data[1];
|
|
47
|
-
const b = data[2];
|
|
48
|
-
const a = data[3];
|
|
49
|
-
if (r === void 0 || g === void 0 || b === void 0 || a === void 0) return value;
|
|
50
|
-
return a === 255 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${a / 255})`;
|
|
51
|
-
}
|
|
52
|
-
function readThemeTokens() {
|
|
53
|
-
return {
|
|
54
|
-
bg: toSrgb(resolveCssVar("--color-bg", DEFAULT_FALLBACK.bg)),
|
|
55
|
-
panel: toSrgb(resolveCssVar("--color-panel", DEFAULT_FALLBACK.panel)),
|
|
56
|
-
panel2: toSrgb(resolveCssVar("--color-panel-2", DEFAULT_FALLBACK["panel-2"])),
|
|
57
|
-
border: toSrgb(resolveCssVar("--color-border", DEFAULT_FALLBACK.border)),
|
|
58
|
-
borderStrong: toSrgb(resolveCssVar("--color-border-strong", DEFAULT_FALLBACK["border-strong"])),
|
|
59
|
-
text: toSrgb(resolveCssVar("--color-text", DEFAULT_FALLBACK.text)),
|
|
60
|
-
textMuted: toSrgb(resolveCssVar("--color-text-muted", DEFAULT_FALLBACK["text-muted"])),
|
|
61
|
-
textDim: toSrgb(resolveCssVar("--color-text-dim", DEFAULT_FALLBACK["text-dim"])),
|
|
62
|
-
accent: toSrgb(resolveCssVar("--color-accent", DEFAULT_FALLBACK.accent)),
|
|
63
|
-
ok: toSrgb(resolveCssVar("--color-ok", DEFAULT_FALLBACK.ok)),
|
|
64
|
-
warn: toSrgb(resolveCssVar("--color-warn", DEFAULT_FALLBACK.warn)),
|
|
65
|
-
err: toSrgb(resolveCssVar("--color-err", DEFAULT_FALLBACK.err)),
|
|
66
|
-
purple: toSrgb(resolveCssVar("--color-purple", DEFAULT_FALLBACK.purple)),
|
|
67
|
-
pink: toSrgb(resolveCssVar("--color-pink", DEFAULT_FALLBACK.pink))
|
|
68
|
-
};
|
|
69
|
-
}
|
|
9
|
+
import { resolveColorReference } from "@ship-it-ui/graph-tokens";
|
|
10
|
+
import {
|
|
11
|
+
readThemeTokens,
|
|
12
|
+
resolveColorReference as resolveColorReference2,
|
|
13
|
+
resolveCssVar,
|
|
14
|
+
toSrgb
|
|
15
|
+
} from "@ship-it-ui/graph-tokens";
|
|
70
16
|
function resolveEntityColor(type, palette) {
|
|
71
17
|
const meta = getEntityTypeMeta(type);
|
|
72
18
|
return resolveColorReference(meta.colorVar, palette);
|
|
73
19
|
}
|
|
74
|
-
function parseColorVarName(value) {
|
|
75
|
-
let i = 0;
|
|
76
|
-
while (i < value.length && isWhitespace(value.charCodeAt(i))) i++;
|
|
77
|
-
if (!value.startsWith("var(", i)) return void 0;
|
|
78
|
-
i += 4;
|
|
79
|
-
while (i < value.length && isWhitespace(value.charCodeAt(i))) i++;
|
|
80
|
-
if (!value.startsWith("--color-", i)) return void 0;
|
|
81
|
-
i += 8;
|
|
82
|
-
const start = i;
|
|
83
|
-
while (i < value.length) {
|
|
84
|
-
const c = value.charCodeAt(i);
|
|
85
|
-
if (c === CC_COMMA || c === CC_PAREN_CLOSE || isWhitespace(c)) break;
|
|
86
|
-
i++;
|
|
87
|
-
}
|
|
88
|
-
if (i === start) return void 0;
|
|
89
|
-
const close = value.indexOf(")", i);
|
|
90
|
-
if (close === -1) return void 0;
|
|
91
|
-
return value.slice(start, i);
|
|
92
|
-
}
|
|
93
|
-
var CC_COMMA = 44;
|
|
94
|
-
var CC_PAREN_CLOSE = 41;
|
|
95
|
-
function isWhitespace(cc) {
|
|
96
|
-
return cc === 32 || cc === 9 || cc === 10 || cc === 13 || cc === 12 || cc === 11;
|
|
97
|
-
}
|
|
98
|
-
function resolveColorReference(value, palette) {
|
|
99
|
-
const key = parseColorVarName(value);
|
|
100
|
-
if (key === void 0) return value;
|
|
101
|
-
switch (key) {
|
|
102
|
-
case "bg":
|
|
103
|
-
return palette.bg;
|
|
104
|
-
case "panel":
|
|
105
|
-
return palette.panel;
|
|
106
|
-
case "panel-2":
|
|
107
|
-
return palette.panel2;
|
|
108
|
-
case "border":
|
|
109
|
-
return palette.border;
|
|
110
|
-
case "border-strong":
|
|
111
|
-
return palette.borderStrong;
|
|
112
|
-
case "text":
|
|
113
|
-
return palette.text;
|
|
114
|
-
case "text-muted":
|
|
115
|
-
return palette.textMuted;
|
|
116
|
-
case "text-dim":
|
|
117
|
-
return palette.textDim;
|
|
118
|
-
case "accent":
|
|
119
|
-
return palette.accent;
|
|
120
|
-
case "ok":
|
|
121
|
-
return palette.ok;
|
|
122
|
-
case "warn":
|
|
123
|
-
return palette.warn;
|
|
124
|
-
case "err":
|
|
125
|
-
return palette.err;
|
|
126
|
-
case "purple":
|
|
127
|
-
return palette.purple;
|
|
128
|
-
case "pink":
|
|
129
|
-
return palette.pink;
|
|
130
|
-
default:
|
|
131
|
-
return palette.accent;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
20
|
|
|
135
21
|
// src/stylesheet.ts
|
|
136
22
|
function buildShipItStylesheet(options = {}) {
|
|
137
23
|
const palette = options.palette ?? readThemeTokens();
|
|
138
|
-
const color = (cssVar) =>
|
|
24
|
+
const color = (cssVar) => resolveColorReference2(cssVar, palette);
|
|
139
25
|
const renderGlyphs = options.renderGlyphs !== false;
|
|
26
|
+
const glyphScale = Math.max(0, Math.min(1, options.glyphScale ?? 0.5));
|
|
27
|
+
const glyphSizePct = `${Math.round(glyphScale * 100)}%`;
|
|
140
28
|
const base = [
|
|
141
29
|
{
|
|
142
30
|
selector: "node",
|
|
@@ -169,14 +57,19 @@ function buildShipItStylesheet(options = {}) {
|
|
|
169
57
|
// patch or a forked stylesheet.
|
|
170
58
|
...listEntityTypes().map(([type, meta]) => {
|
|
171
59
|
const c = color(meta.colorVar);
|
|
60
|
+
const glyphStyle = renderGlyphs ? {
|
|
61
|
+
"border-color": c,
|
|
62
|
+
"background-image": iconToSvgDataUrl(meta.iconName, { color: c }),
|
|
63
|
+
"background-fit": "none",
|
|
64
|
+
"background-clip": "none",
|
|
65
|
+
"background-width": glyphSizePct,
|
|
66
|
+
"background-height": glyphSizePct,
|
|
67
|
+
"background-position-x": "50%",
|
|
68
|
+
"background-position-y": "50%"
|
|
69
|
+
} : { "border-color": c };
|
|
172
70
|
return {
|
|
173
71
|
selector: `node[entityType = "${escapeCytoscapeAttr(type)}"]`,
|
|
174
|
-
style:
|
|
175
|
-
"border-color": c,
|
|
176
|
-
"background-image": iconToSvgDataUrl(meta.iconName, { color: c }),
|
|
177
|
-
"background-fit": "contain",
|
|
178
|
-
"background-clip": "none"
|
|
179
|
-
} : { "border-color": c }
|
|
72
|
+
style: glyphStyle
|
|
180
73
|
};
|
|
181
74
|
}),
|
|
182
75
|
{
|
|
@@ -366,7 +259,7 @@ export {
|
|
|
366
259
|
GraphCanvas,
|
|
367
260
|
buildShipItStylesheet,
|
|
368
261
|
readThemeTokens,
|
|
369
|
-
resolveColorReference,
|
|
262
|
+
resolveColorReference2 as resolveColorReference,
|
|
370
263
|
resolveCssVar,
|
|
371
264
|
resolveEntityColor,
|
|
372
265
|
useShipItStylesheet
|
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 { 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":";AAAA,SAAS,wBAAwB;AACjC,SAAS,uBAAuB;;;ACchC,SAAS,yBAA0C;AAEnD,IAAM,mBAA2C;AAAA,EAC/C,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,iBAAiB;AAAA,EACjB,MAAM;AAAA,EACN,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,MAAM;AACR;AAOO,SAAS,cAAc,MAAc,WAAW,IAAY;AACjE,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,QAAM,MAAM,iBAAiB,SAAS,eAAe,EAAE,iBAAiB,IAAI;AAC5E,QAAM,UAAU,IAAI,KAAK;AACzB,SAAO,QAAQ,SAAS,IAAI,UAAU;AACxC;AAMA,IAAI,eAAyC;AAkBtC,SAAS,OAAO,OAAuB;AAC5C,MAAI,CAAC,SAAS,OAAO,aAAa,YAAa,QAAO;AAItD,MAAI,oCAAoC,KAAK,KAAK,EAAG,QAAO;AAC5D,mBAAiB,SAAS,cAAc,QAAQ;AAChD,eAAa,QAAQ;AACrB,eAAa,SAAS;AACtB,QAAM,MAAM,aAAa,WAAW,MAAM,EAAE,oBAAoB,KAAK,CAAC;AACtE,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI,UAAU,GAAG,GAAG,GAAG,CAAC;AAGxB,MAAI,YAAY;AAChB,MAAI,YAAY;AAChB,MAAI,SAAS,GAAG,GAAG,GAAG,CAAC;AACvB,QAAM,OAAO,IAAI,aAAa,GAAG,GAAG,GAAG,CAAC,EAAE;AAC1C,QAAM,IAAI,KAAK,CAAC;AAChB,QAAM,IAAI,KAAK,CAAC;AAChB,QAAM,IAAI,KAAK,CAAC;AAChB,QAAM,IAAI,KAAK,CAAC;AAChB,MAAI,MAAM,UAAa,MAAM,UAAa,MAAM,UAAa,MAAM,OAAW,QAAO;AACrF,SAAO,MAAM,MAAM,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,IAAI,GAAG;AAChF;AA8BO,SAAS,kBAAqC;AACnD,SAAO;AAAA,IACL,IAAI,OAAO,cAAc,cAAc,iBAAiB,EAAE,CAAC;AAAA,IAC3D,OAAO,OAAO,cAAc,iBAAiB,iBAAiB,KAAK,CAAC;AAAA,IACpE,QAAQ,OAAO,cAAc,mBAAmB,iBAAiB,SAAS,CAAC,CAAC;AAAA,IAC5E,QAAQ,OAAO,cAAc,kBAAkB,iBAAiB,MAAM,CAAC;AAAA,IACvE,cAAc,OAAO,cAAc,yBAAyB,iBAAiB,eAAe,CAAC,CAAC;AAAA,IAC9F,MAAM,OAAO,cAAc,gBAAgB,iBAAiB,IAAI,CAAC;AAAA,IACjE,WAAW,OAAO,cAAc,sBAAsB,iBAAiB,YAAY,CAAC,CAAC;AAAA,IACrF,SAAS,OAAO,cAAc,oBAAoB,iBAAiB,UAAU,CAAC,CAAC;AAAA,IAC/E,QAAQ,OAAO,cAAc,kBAAkB,iBAAiB,MAAM,CAAC;AAAA,IACvE,IAAI,OAAO,cAAc,cAAc,iBAAiB,EAAE,CAAC;AAAA,IAC3D,MAAM,OAAO,cAAc,gBAAgB,iBAAiB,IAAI,CAAC;AAAA,IACjE,KAAK,OAAO,cAAc,eAAe,iBAAiB,GAAG,CAAC;AAAA,IAC9D,QAAQ,OAAO,cAAc,kBAAkB,iBAAiB,MAAM,CAAC;AAAA,IACvE,MAAM,OAAO,cAAc,gBAAgB,iBAAiB,IAAI,CAAC;AAAA,EACnE;AACF;AAQO,SAAS,mBAAmB,MAAkB,SAAoC;AACvF,QAAM,OAAO,kBAAkB,IAAI;AACnC,SAAO,sBAAsB,KAAK,UAAU,OAAO;AACrD;AAMA,SAAS,kBAAkB,OAAmC;AAE5D,MAAI,IAAI;AACR,SAAO,IAAI,MAAM,UAAU,aAAa,MAAM,WAAW,CAAC,CAAC,EAAG;AAC9D,MAAI,CAAC,MAAM,WAAW,QAAQ,CAAC,EAAG,QAAO;AACzC,OAAK;AAEL,SAAO,IAAI,MAAM,UAAU,aAAa,MAAM,WAAW,CAAC,CAAC,EAAG;AAC9D,MAAI,CAAC,MAAM,WAAW,YAAY,CAAC,EAAG,QAAO;AAC7C,OAAK;AAEL,QAAM,QAAQ;AACd,SAAO,IAAI,MAAM,QAAQ;AACvB,UAAM,IAAI,MAAM,WAAW,CAAC;AAC5B,QAAI,MAAM,YAAY,MAAM,kBAAkB,aAAa,CAAC,EAAG;AAC/D;AAAA,EACF;AACA,MAAI,MAAM,MAAO,QAAO;AAGxB,QAAM,QAAQ,MAAM,QAAQ,KAAK,CAAC;AAClC,MAAI,UAAU,GAAI,QAAO;AACzB,SAAO,MAAM,MAAM,OAAO,CAAC;AAC7B;AAEA,IAAM,WAAW;AACjB,IAAM,iBAAiB;AAEvB,SAAS,aAAa,IAAqB;AAEzC,SAAO,OAAO,MAAQ,OAAO,KAAQ,OAAO,MAAQ,OAAO,MAAQ,OAAO,MAAQ,OAAO;AAC3F;AAOO,SAAS,sBAAsB,OAAe,SAAoC;AAIvF,QAAM,MAAM,kBAAkB,KAAK;AACnC,MAAI,QAAQ,OAAW,QAAO;AAC9B,UAAQ,KAAK;AAAA,IACX,KAAK;AACH,aAAO,QAAQ;AAAA,IACjB,KAAK;AACH,aAAO,QAAQ;AAAA,IACjB,KAAK;AACH,aAAO,QAAQ;AAAA,IACjB,KAAK;AACH,aAAO,QAAQ;AAAA,IACjB,KAAK;AACH,aAAO,QAAQ;AAAA,IACjB,KAAK;AACH,aAAO,QAAQ;AAAA,IACjB,KAAK;AACH,aAAO,QAAQ;AAAA,IACjB,KAAK;AACH,aAAO,QAAQ;AAAA,IACjB,KAAK;AACH,aAAO,QAAQ;AAAA,IACjB,KAAK;AACH,aAAO,QAAQ;AAAA,IACjB,KAAK;AACH,aAAO,QAAQ;AAAA,IACjB,KAAK;AACH,aAAO,QAAQ;AAAA,IACjB,KAAK;AACH,aAAO,QAAQ;AAAA,IACjB,KAAK;AACH,aAAO,QAAQ;AAAA,IACjB;AACE,aAAO,QAAQ;AAAA,EACnB;AACF;;;AD9LO,SAAS,sBACd,UAAkC,CAAC,GACT;AAC1B,QAAM,UAAU,QAAQ,WAAW,gBAAgB;AACnD,QAAM,QAAQ,CAAC,WAAmB,sBAAsB,QAAQ,OAAO;AACvE,QAAM,eAAe,QAAQ,iBAAiB;AAE9C,QAAM,OAAwC;AAAA,IAC5C;AAAA,MACE,UAAU;AAAA,MACV,OAAO;AAAA,QACL,oBAAoB,QAAQ;AAAA,QAC5B,gBAAgB;AAAA,QAChB,gBAAgB,QAAQ;AAAA,QACxB,kBAAkB;AAAA,QAClB,OAAO;AAAA,QACP,OAAO,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA,QAKf,eAAe;AAAA,QACf,aAAa;AAAA,QACb,eAAe;AAAA,QACf,eAAe;AAAA,QACf,iBAAiB;AAAA;AAAA;AAAA,QAGjB,OAAO;AAAA,QACP,QAAQ;AAAA,QACR,OAAO;AAAA,MACT;AAAA,IACF;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA,GAAG,gBAAgB,EAAE,IAAmC,CAAC,CAAC,MAAM,IAAI,MAAM;AACxE,YAAM,IAAI,MAAM,KAAK,QAAQ;AAC7B,aAAO;AAAA,QACL,UAAU,sBAAsB,oBAAoB,IAAI,CAAC;AAAA,QACzD,OAAO,eACH;AAAA,UACE,gBAAgB;AAAA,UAChB,oBAAoB,iBAAiB,KAAK,UAAU,EAAE,OAAO,EAAE,CAAC;AAAA,UAChE,kBAAkB;AAAA,UAClB,mBAAmB;AAAA,QACrB,IACA,EAAE,gBAAgB,EAAE;AAAA,MAC1B;AAAA,IACF,CAAC;AAAA,IACD;AAAA,MACE,UAAU;AAAA,MACV,OAAO;AAAA,QACL,gBAAgB;AAAA,QAChB,iBAAiB,QAAQ;AAAA,QACzB,mBAAmB;AAAA,QACnB,mBAAmB;AAAA,MACrB;AAAA,IACF;AAAA,IACA;AAAA,MACE,UAAU;AAAA,MACV,OAAO,EAAE,gBAAgB,QAAQ,OAAO;AAAA,IAC1C;AAAA,IACA;AAAA,MACE,UAAU;AAAA,MACV,OAAO,EAAE,SAAS,KAAK;AAAA,IACzB;AAAA,IACA;AAAA;AAAA;AAAA;AAAA;AAAA,MAKE,UAAU;AAAA,MACV,OAAO;AAAA,QACL,OAAO;AAAA,QACP,cAAc,QAAQ;AAAA,QACtB,sBAAsB,QAAQ;AAAA,QAC9B,sBAAsB;AAAA,QACtB,eAAe;AAAA,QACf,eAAe;AAAA,MACjB;AAAA,IACF;AAAA,IACA;AAAA,MACE,UAAU;AAAA,MACV,OAAO;AAAA,QACL,cAAc,QAAQ;AAAA,QACtB,sBAAsB,QAAQ;AAAA,QAC9B,OAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA;AAAA,MACE,UAAU;AAAA,MACV,OAAO,EAAE,SAAS,KAAK;AAAA,IACzB;AAAA,EACF;AAEA,SAAO,CAAC,GAAG,MAAM,GAAI,QAAQ,SAAS,CAAC,CAAE;AAC3C;AAMO,IAAM,qBAAqB;AAAA,EAChC,MAAM;AAAA,EACN,KAAK;AACP;AAOA,SAAS,oBAAoB,OAAuB;AAClD,SAAO,MAAM,QAAQ,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE;AAChD;;;AE3JA,SAAS,aAAa,WAAW,eAAe;AA6BzC,SAAS,oBACd,OACA,UAAsC,CAAC,GACZ;AAC3B,QAAM,EAAE,UAAU,MAAM,SAAS,MAAM,IAAI;AAI3C,QAAM,eAAe;AAAA,IACnB,OAAO,EAAE,SAAS,MAAM;AAAA,IACxB,CAAC,SAAS,KAAK;AAAA,EACjB;AAEA,QAAM,QAAQ,YAAY,MAAM;AAC9B,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AACT,OAAG,MAAM,sBAAsB,YAAY,CAAC,EAAE,OAAO;AAAA,EACvD,GAAG,CAAC,OAAO,YAAY,CAAC;AAExB,YAAU,MAAM;AACd,UAAM;AACN,QAAI,CAAC,WAAW,OAAO,aAAa,YAAa,QAAO;AACxD,UAAM,WAAW,IAAI,iBAAiB,KAAK;AAC3C,aAAS,QAAQ,SAAS,iBAAiB;AAAA,MACzC,YAAY;AAAA,MACZ,iBAAiB,CAAC,YAAY;AAAA,IAChC,CAAC;AACD,WAAO,MAAM,SAAS,WAAW;AAAA,EACnC,GAAG,CAAC,OAAO,OAAO,CAAC;AAEnB,SAAO,EAAE,SAAS,MAAM;AAC1B;;;AC5DA;AAAA,EACE;AAAA,EACA,aAAAA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAsKH,SAcE,KAdF;AAvGJ,IAAM,iBAA0C,EAAE,MAAM,SAAS;AAE1D,IAAM,cAAc,WAAgD,SAASC,aAClF;AAAA,EACE;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc,YAAY;AAAA,EAC1B,GAAG;AACL,GACA,cACA;AACA,QAAM,eAAe,OAA8B,IAAI;AACvD,QAAM,QAAQ,OAA8B,IAAI;AAGhD,EAAAC,WAAU,MAAM;AACd,QAAI,CAAC,aAAa,QAAS,QAAO;AAClC,UAAM,KAAK,OAAO;AAAA,MAChB,WAAW,aAAa;AAAA,MACxB;AAAA,MACA;AAAA,MACA,OAAO,sBAAsB,YAAY;AAAA,IAC3C,CAAC;AACD,UAAM,UAAU;AAChB,WAAO,MAAM;AACX,SAAG,QAAQ;AACX,YAAM,UAAU;AAAA,IAClB;AAAA,EAIF,GAAG,CAAC,MAAM,CAAC;AAGX,EAAAA,WAAU,MAAM;AACd,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AACT,OAAG,KAAK,EAAE,SAAS,CAAC;AAAA,EACtB,GAAG,CAAC,QAAQ,CAAC;AAGb,EAAAA,WAAU,MAAM;AACd,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AACT,OAAG,OAAO,MAAM,EAAE,IAAI;AAAA,EACxB,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,EAAE,SAAS,cAAc,IAAI,oBAAoB,OAAO,YAAY;AAK1E,EAAAA,WAAU,MAAM;AACd,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI,QAAO;AAChB,UAAM,eAAe,CAAC,UAAiC;AACrD,UAAI,MAAM,QAAQ,SAAS,GAAG;AAC5B,mBAAW,MAAM,MAAgC;AAAA,MACnD;AAAA,IACF;AACA,UAAM,sBAAsB,CAAC,UAAiC;AAC5D,UAAI,MAAM,WAAW,GAAI,oBAAmB;AAAA,IAC9C;AACA,UAAM,cAAc,CAAC,UAAiC;AACpD,oBAAc,MAAM,MAAgC;AAAA,IACtD;AACA,UAAM,cAAc,MAAM,cAAc;AACxC,OAAG,GAAG,OAAO,QAAQ,YAAY;AACjC,OAAG,GAAG,OAAO,mBAAmB;AAChC,OAAG,GAAG,aAAa,QAAQ,WAAW;AACtC,OAAG,GAAG,YAAY,QAAQ,WAAW;AACrC,WAAO,MAAM;AACX,SAAG,IAAI,OAAO,QAAQ,YAAY;AAClC,SAAG,IAAI,OAAO,mBAAmB;AACjC,SAAG,IAAI,aAAa,QAAQ,WAAW;AACvC,SAAG,IAAI,YAAY,QAAQ,WAAW;AAAA,IACxC;AAAA,EACF,GAAG,CAAC,QAAQ,UAAU,kBAAkB,aAAa,WAAW,CAAC;AAMjE;AAAA,IACE;AAAA,IACA,OAA0B;AAAA,MACxB,IAAI,KAAK;AACP,eAAO,MAAM;AAAA,MACf;AAAA,MACA;AAAA,IACF;AAAA,IACA,CAAC,aAAa;AAAA,EAChB;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,WAAW,CAAC,0BAA0B,SAAS,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAAA,MACxE,GAAG;AAAA,MAUJ;AAAA,4BAAC,SAAI,KAAK,cAAc,OAAO,EAAE,UAAU,YAAY,OAAO,EAAE,GAAG;AAAA,QAClE,aAAa,oBAAC,SAAI,WAAU,+BAA+B,qBAAU;AAAA;AAAA;AAAA,EACxE;AAEJ,CAAC;AAED,YAAY,cAAc;","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 * Fraction of the node that the rendered glyph occupies. Default `0.5` —\n * matches `<GraphNode>` where the icon is sized at ~42% of the square and\n * leaves breathing room inside the border. Set to `1` to revert to the\n * pre-0.0.7 behavior where the glyph filled the node edge-to-edge.\n */\n glyphScale?: number;\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 // Clamp into a sensible range. `0` would hide the glyph entirely; `1` paints\n // edge-to-edge (the legacy behavior). The default `0.5` matches the 26/52\n // ratio used by the `<GraphNode>` React component.\n const glyphScale = Math.max(0, Math.min(1, options.glyphScale ?? 0.5));\n const glyphSizePct = `${Math.round(glyphScale * 100)}%`;\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 // Pre-0.0.7 the glyph used `background-fit: contain`, which scales the\n // image to fill the node edge-to-edge and ignores the painted SVG's\n // intrinsic size. The icon ended up touching the border on every side\n // and looked cramped against the 52×52 node. We now set\n // `background-fit: none` and pin the painted width/height to a\n // fraction of the node (default 50%), with the image's anchor centered\n // — that gives the glyph breathing room and visually aligns with the\n // `<GraphNode>` React component used elsewhere in the design system.\n //\n // Cast through `unknown` because `cytoscape.Css.Node` doesn't (yet)\n // expose `background-width`/`background-height` in its TS types,\n // though the runtime accepts them per the cytoscape.js reference.\n const glyphStyle = renderGlyphs\n ? ({\n 'border-color': c,\n 'background-image': iconToSvgDataUrl(meta.iconName, { color: c }),\n 'background-fit': 'none',\n 'background-clip': 'none',\n 'background-width': glyphSizePct,\n 'background-height': glyphSizePct,\n 'background-position-x': '50%',\n 'background-position-y': '50%',\n } as unknown as cytoscape.Css.Node)\n : { 'border-color': c };\n return {\n selector: `node[entityType = \"${escapeCytoscapeAttr(type)}\"]`,\n style: glyphStyle,\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 * Cytoscape-side wrapper around `@ship-it-ui/graph-tokens`. The token-reader\n * primitives (`resolveCssVar`, `toSrgb`, `readThemeTokens`,\n * `resolveColorReference`, `ThemeTokenPalette`) live in the shared package so\n * `@ship-it-ui/graph-editor` (React Flow) can consume them without dragging\n * Cytoscape in. Only `resolveEntityColor` lives here because it bridges to the\n * `@ship-it-ui/shipit` entity-type registry.\n */\n\nimport { getEntityTypeMeta, type EntityType } from '@ship-it-ui/shipit';\nimport { resolveColorReference, type ThemeTokenPalette } from '@ship-it-ui/graph-tokens';\n\nexport {\n readThemeTokens,\n resolveColorReference,\n resolveCssVar,\n toSrgb,\n type ThemeTokenPalette,\n} from '@ship-it-ui/graph-tokens';\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","'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":["resolveColorReference","resolveColorReference","useEffect","GraphCanvas","useEffect"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ship-it-ui/cytoscape",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
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/",
|
|
@@ -40,13 +40,16 @@
|
|
|
40
40
|
"access": "public",
|
|
41
41
|
"provenance": true
|
|
42
42
|
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@ship-it-ui/graph-tokens": "0.0.2"
|
|
45
|
+
},
|
|
43
46
|
"peerDependencies": {
|
|
44
47
|
"cytoscape": "^3.28.0",
|
|
45
48
|
"react": "^18.0.0 || ^19.0.0",
|
|
46
49
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
47
|
-
"@ship-it-ui/icons": "0.0.
|
|
48
|
-
"@ship-it-ui/
|
|
49
|
-
"@ship-it-ui/
|
|
50
|
+
"@ship-it-ui/icons": "0.0.8",
|
|
51
|
+
"@ship-it-ui/shipit": "0.0.9",
|
|
52
|
+
"@ship-it-ui/ui": "0.0.8"
|
|
50
53
|
},
|
|
51
54
|
"devDependencies": {
|
|
52
55
|
"@testing-library/jest-dom": "^6.6.3",
|
|
@@ -64,11 +67,11 @@
|
|
|
64
67
|
"vitest": "^2.1.3",
|
|
65
68
|
"vitest-axe": "^0.1.0",
|
|
66
69
|
"@ship-it-ui/eslint-config": "0.0.1",
|
|
67
|
-
"@ship-it-ui/icons": "0.0.
|
|
68
|
-
"@ship-it-ui/shipit": "0.0.
|
|
69
|
-
"@ship-it-ui/tokens": "0.0.
|
|
70
|
+
"@ship-it-ui/icons": "0.0.8",
|
|
71
|
+
"@ship-it-ui/shipit": "0.0.9",
|
|
72
|
+
"@ship-it-ui/tokens": "0.0.6",
|
|
70
73
|
"@ship-it-ui/tsconfig": "0.0.1",
|
|
71
|
-
"@ship-it-ui/ui": "0.0.
|
|
74
|
+
"@ship-it-ui/ui": "0.0.8"
|
|
72
75
|
},
|
|
73
76
|
"scripts": {
|
|
74
77
|
"build": "tsup",
|