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