@ship-it-ui/cytoscape 0.0.8 → 0.0.10
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 +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +15 -15
package/dist/index.cjs
CHANGED
|
@@ -38,8 +38,8 @@ var import_icons = require("@ship-it-ui/icons");
|
|
|
38
38
|
var import_shipit2 = require("@ship-it-ui/shipit");
|
|
39
39
|
|
|
40
40
|
// src/theme-tokens.ts
|
|
41
|
-
var import_shipit = require("@ship-it-ui/shipit");
|
|
42
41
|
var import_graph_tokens = require("@ship-it-ui/graph-tokens");
|
|
42
|
+
var import_shipit = require("@ship-it-ui/shipit");
|
|
43
43
|
var import_graph_tokens2 = require("@ship-it-ui/graph-tokens");
|
|
44
44
|
function resolveEntityColor(type, palette) {
|
|
45
45
|
const meta = (0, import_shipit.getEntityTypeMeta)(type);
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/stylesheet.ts","../src/theme-tokens.ts","../src/useShipItStylesheet.ts","../src/GraphCanvas.tsx"],"sourcesContent":["/**\n * @ship-it-ui/cytoscape — Cytoscape adapter for the Ship-It design system.\n *\n * Three layered exports:\n * - {@link buildShipItStylesheet} — token-driven stylesheet array.\n * - {@link useShipItStylesheet} — theme-aware re-resolver hook.\n * - {@link GraphCanvas} — `<GraphCanvas>` React wrapper that\n * owns the data-theme ↔ stylesheet ↔\n * inspector dance.\n *\n * Cytoscape is a peer dependency — pass `engine={cytoscape}` so the consumer\n * controls the version and any registered extensions.\n */\n\nexport {\n buildShipItStylesheet,\n GRAPH_CANVAS_CLASS,\n type BuildStylesheetOptions,\n type ShipItStylesheetBlock,\n} from './stylesheet';\n\nexport {\n useShipItStylesheet,\n type UseShipItStylesheetOptions,\n type UseShipItStylesheetReturn,\n} from './useShipItStylesheet';\n\nexport {\n GraphCanvas,\n type GraphCanvasProps,\n type GraphCanvasHandle,\n type CytoscapeEngine,\n} from './GraphCanvas';\n\nexport {\n readThemeTokens,\n resolveCssVar,\n resolveColorReference,\n resolveEntityColor,\n type ThemeTokenPalette,\n} from './theme-tokens';\n","import { iconToSvgDataUrl } from '@ship-it-ui/icons';\nimport { listEntityTypes } from '@ship-it-ui/shipit';\nimport type cytoscape from 'cytoscape';\n\nimport { readThemeTokens, resolveColorReference, type ThemeTokenPalette } from './theme-tokens';\n\n/**\n * Build a Cytoscape stylesheet from the live design tokens. The result is a\n * plain JSON array suitable for `cytoscape({ style: ... })` — re-run after a\n * `data-theme` change to pick up the new palette.\n *\n * The stylesheet is opinionated:\n * - Nodes are square-ish rounded glyphs colored by `data(entityType)`.\n * - Edges are token-colored thin lines with arrowheads.\n * - Selected / on-path / dimmed states are driven by class names that the\n * consumer toggles (`graph-canvas:selected`, `graph-canvas:path`,\n * `graph-canvas:dim`).\n *\n * Pass `palette` to override the token read — useful for SSR or tests.\n */\n\nexport interface BuildStylesheetOptions {\n /** Pre-resolved palette. When omitted, tokens are read from the document. */\n palette?: ThemeTokenPalette;\n /**\n * Additional entries appended to the stylesheet — handy for app-specific\n * selectors without forking the builder.\n */\n extra?: ReadonlyArray<cytoscape.StylesheetJsonBlock>;\n /**\n * Render each registered entity type's glyph (◇, ○, ▤, ↑, ◎, ▢ …) inside\n * the cytoscape node as a centered SVG data URL. Closes the visual gap\n * with the `<GraphNode>` React component — the docs page and the canvas\n * now share a vocabulary. Defaults to `true`. Pass `false` to fall back\n * to the original wireframe (border-only) per-type rule.\n */\n renderGlyphs?: boolean;\n /**\n * Fraction of the node that the rendered glyph occupies. Default `0.5` —\n * matches `<GraphNode>` where the icon is sized at ~42% of the square and\n * leaves breathing room inside the border. Set to `1` to revert to the\n * pre-0.0.7 behavior where the glyph filled the node edge-to-edge.\n */\n glyphScale?: number;\n}\n\n// Re-export the block type so consumers can declare typed `extra` entries.\nexport type ShipItStylesheetBlock = cytoscape.StylesheetJsonBlock;\n\nexport function buildShipItStylesheet(\n options: BuildStylesheetOptions = {},\n): cytoscape.StylesheetJson {\n const palette = options.palette ?? readThemeTokens();\n const color = (cssVar: string) => resolveColorReference(cssVar, palette);\n const renderGlyphs = options.renderGlyphs !== false;\n // Clamp into a sensible range. `0` would hide the glyph entirely; `1` paints\n // edge-to-edge (the legacy behavior). The default `0.5` matches the 26/52\n // ratio used by the `<GraphNode>` React component.\n const glyphScale = Math.max(0, Math.min(1, options.glyphScale ?? 0.5));\n const glyphSizePct = `${Math.round(glyphScale * 100)}%`;\n\n const base: cytoscape.StylesheetJsonBlock[] = [\n {\n selector: 'node',\n style: {\n 'background-color': palette.panel,\n 'border-width': 1.5,\n 'border-color': palette.accent,\n 'border-opacity': 1,\n label: 'data(label)',\n color: palette.textMuted,\n // Static stack instead of `var(--font-mono, monospace)` — cytoscape\n // can't resolve CSS variables outside the DOM cascade and emits a\n // warning per node selector at every mount. Consumers who need to\n // override the canvas font can do so via `options.extra`.\n 'font-family': 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',\n 'font-size': 10,\n 'text-valign': 'bottom',\n 'text-halign': 'center',\n 'text-margin-y': 6,\n // 52 matches `<GraphNode>`'s default size — the docs page and the\n // canvas now share dimensions. Pre-Issue-4 the canvas was 36×36.\n width: 52,\n height: 52,\n shape: 'round-rectangle',\n },\n },\n // One selector per entity type registered with @ship-it-ui/shipit. Built-in\n // types are seeded automatically; custom types registered via\n // `registerEntityType(...)` pick up their `colorVar` here without a docs\n // patch or a forked stylesheet.\n ...listEntityTypes().map<cytoscape.StylesheetJsonBlock>(([type, meta]) => {\n const c = color(meta.colorVar);\n // Pre-0.0.7 the glyph used `background-fit: contain`, which scales the\n // image to fill the node edge-to-edge and ignores the painted SVG's\n // intrinsic size. The icon ended up touching the border on every side\n // and looked cramped against the 52×52 node. We now set\n // `background-fit: none` and pin the painted width/height to a\n // fraction of the node (default 50%), with the image's anchor centered\n // — that gives the glyph breathing room and visually aligns with the\n // `<GraphNode>` React component used elsewhere in the design system.\n //\n // Cast through `unknown` because `cytoscape.Css.Node` doesn't (yet)\n // expose `background-width`/`background-height` in its TS types,\n // though the runtime accepts them per the cytoscape.js reference.\n const glyphStyle = renderGlyphs\n ? ({\n 'border-color': c,\n 'background-image': iconToSvgDataUrl(meta.iconName, { color: c }),\n 'background-fit': 'none',\n 'background-clip': 'none',\n 'background-width': glyphSizePct,\n 'background-height': glyphSizePct,\n 'background-position-x': '50%',\n 'background-position-y': '50%',\n } as unknown as cytoscape.Css.Node)\n : { 'border-color': c };\n return {\n selector: `node[entityType = \"${escapeCytoscapeAttr(type)}\"]`,\n style: glyphStyle,\n };\n }),\n {\n selector: 'node:selected',\n style: {\n 'border-width': 3,\n 'overlay-color': palette.accent,\n 'overlay-opacity': 0.15,\n 'overlay-padding': 4,\n },\n },\n {\n selector: 'node.graph-canvas\\\\:path',\n style: { 'border-color': palette.purple },\n },\n {\n selector: 'node.graph-canvas\\\\:dim',\n style: { opacity: 0.35 },\n },\n {\n // Default edges tone with `accent` so they read against the panel\n // background — matches `<GraphEdge edgeStyle=\"solid\">` in the docs.\n // Pre-fix the line drew in `palette.border` (the same tone as\n // surface-divider lines) and effectively disappeared on dark themes.\n selector: 'edge',\n style: {\n width: 1,\n 'line-color': palette.accent,\n 'target-arrow-color': palette.accent,\n 'target-arrow-shape': 'triangle',\n 'arrow-scale': 0.8,\n 'curve-style': 'bezier',\n },\n },\n {\n selector: 'edge.graph-canvas\\\\:path',\n style: {\n 'line-color': palette.purple,\n 'target-arrow-color': palette.purple,\n width: 1.5,\n },\n },\n {\n selector: 'edge.graph-canvas\\\\:dim',\n style: { opacity: 0.25 },\n },\n ];\n\n return [...base, ...(options.extra ?? [])] as cytoscape.StylesheetJson;\n}\n\n/**\n * Convenience class names the stylesheet recognizes. Toggle these on Cytoscape\n * nodes / edges to drive the on-path and dimmed visuals.\n */\nexport const GRAPH_CANVAS_CLASS = {\n path: 'graph-canvas:path',\n dim: 'graph-canvas:dim',\n} as const;\n\n// Cytoscape's attribute selector accepts a double-quoted string. Escape\n// embedded backslashes and double quotes so a malformed (or hostile)\n// registered key can't break out of the selector. The grammar\n// (https://js.cytoscape.org/#selectors/data) is more permissive than CSS, but\n// these two characters are the only ones that would change selector semantics.\nfunction escapeCytoscapeAttr(value: string): string {\n return value.replace(/[\\\\\"]/g, (c) => `\\\\${c}`);\n}\n","/**\n * Cytoscape-side wrapper around `@ship-it-ui/graph-tokens`. The token-reader\n * primitives (`resolveCssVar`, `toSrgb`, `readThemeTokens`,\n * `resolveColorReference`, `ThemeTokenPalette`) live in the shared package so\n * `@ship-it-ui/graph-editor` (React Flow) can consume them without dragging\n * Cytoscape in. Only `resolveEntityColor` lives here because it bridges to the\n * `@ship-it-ui/shipit` entity-type registry.\n */\n\nimport { getEntityTypeMeta, type EntityType } from '@ship-it-ui/shipit';\nimport { resolveColorReference, type ThemeTokenPalette } from '@ship-it-ui/graph-tokens';\n\nexport {\n readThemeTokens,\n resolveColorReference,\n resolveCssVar,\n toSrgb,\n type ThemeTokenPalette,\n} from '@ship-it-ui/graph-tokens';\n\n/**\n * Resolve the concrete color for a registered entity type. Reads the type's\n * `colorVar` (a `var(--color-…)` string) and looks the value up in the\n * palette. Falls back to the palette's `accent` color when the var is\n * malformed or unknown.\n */\nexport function resolveEntityColor(type: EntityType, palette: ThemeTokenPalette): string {\n const meta = getEntityTypeMeta(type);\n return resolveColorReference(meta.colorVar, palette);\n}\n","'use client';\n\nimport type cytoscape from 'cytoscape';\nimport { useCallback, useEffect, useMemo } from 'react';\n\nimport { buildShipItStylesheet, type BuildStylesheetOptions } from './stylesheet';\n\n/**\n * useShipItStylesheet — keeps a Cytoscape instance's stylesheet in sync with\n * the live design-token palette. Re-applies the stylesheet whenever\n * `<html data-theme>` flips, so toggling between dark and light propagates to\n * the graph without remounting.\n *\n * Returns a `refresh()` callback the consumer can invoke after any other\n * theme-affecting change (e.g., a `--color-accent` hue knob update).\n *\n * ```ts\n * const cyRef = useRef<cytoscape.Core | null>(null);\n * const { refresh } = useShipItStylesheet(cyRef);\n * ```\n */\n\nexport interface UseShipItStylesheetOptions extends BuildStylesheetOptions {\n /** Skip the MutationObserver wiring (e.g., when the host owns its own observer). */\n observe?: boolean;\n}\n\nexport interface UseShipItStylesheetReturn {\n /** Re-read tokens and re-apply the stylesheet. */\n refresh: () => void;\n}\n\nexport function useShipItStylesheet(\n cyRef: { current: cytoscape.Core | null },\n options: UseShipItStylesheetOptions = {},\n): UseShipItStylesheetReturn {\n const { observe = true, palette, extra } = options;\n // Memoize the build options against their flat constituents so callers that\n // pass `options` inline (a fresh object each render) don't churn `apply` /\n // disconnect+reconnect the MutationObserver on every render.\n const buildOptions = useMemo<BuildStylesheetOptions>(\n () => ({ palette, extra }),\n [palette, extra],\n );\n\n const apply = useCallback(() => {\n const cy = cyRef.current;\n if (!cy) return;\n cy.style(buildShipItStylesheet(buildOptions)).update();\n }, [cyRef, buildOptions]);\n\n useEffect(() => {\n apply();\n if (!observe || typeof document === 'undefined') return undefined;\n const observer = new MutationObserver(apply);\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: ['data-theme'],\n });\n return () => observer.disconnect();\n }, [apply, observe]);\n\n return { refresh: apply };\n}\n","'use client';\n\nimport type cytoscape from 'cytoscape';\nimport {\n forwardRef,\n useEffect,\n useImperativeHandle,\n useRef,\n type HTMLAttributes,\n type ReactNode,\n} from 'react';\n\nimport { buildShipItStylesheet, type BuildStylesheetOptions } from './stylesheet';\nimport { useShipItStylesheet } from './useShipItStylesheet';\n\n/**\n * GraphCanvas — high-level wrapper around a Cytoscape instance. Owns the\n * `data-theme` ↔ stylesheet sync (via {@link useShipItStylesheet}), the\n * cytoscape lifecycle (create / destroy on mount / unmount), and a thin\n * selection API on top of Cytoscape's events.\n *\n * The component never bundles Cytoscape itself — pass the engine factory in\n * via the `engine` prop so the consumer controls the Cytoscape version and\n * any registered extensions:\n *\n * ```tsx\n * import cytoscape from 'cytoscape';\n *\n * <GraphCanvas\n * engine={cytoscape}\n * elements={[...]}\n * layout={{ name: 'cose' }}\n * onSelect={(node) => setSelected(node.id())}\n * inspector={selected && <GraphInspector …/>}\n * />\n * ```\n */\n\nexport type CytoscapeEngine = (options: cytoscape.CytoscapeOptions) => cytoscape.Core;\n\nexport interface GraphCanvasHandle {\n /** Live Cytoscape instance. `null` until mount. */\n cy: cytoscape.Core | null;\n /** Re-read tokens and re-apply the stylesheet. */\n refreshStyles: () => void;\n}\n\nexport interface GraphCanvasProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n 'onSelect' | 'children'\n> {\n /** Cytoscape factory. Pass the imported `cytoscape` default export. */\n engine: CytoscapeEngine;\n /** Graph elements (nodes + edges). Passed straight through to Cytoscape. */\n elements: cytoscape.ElementDefinition[];\n /** Layout config. Defaults to a static `preset` layout (no auto-layout). */\n layout?: cytoscape.LayoutOptions;\n /** Fires when a node is tapped/selected. */\n onSelect?: (node: cytoscape.NodeSingular) => void;\n /** Fires when the selection is cleared (background tap). */\n onClearSelection?: () => void;\n /** Fires when the pointer enters a node. */\n onNodeHover?: (node: cytoscape.NodeSingular) => void;\n /** Fires when the pointer leaves a node. */\n onNodeLeave?: () => void;\n /** Slot rendered over the graph (e.g., a `<GraphInspector>`). Positioned top-right. */\n inspector?: ReactNode;\n /** Overrides for the stylesheet builder. */\n styleOptions?: BuildStylesheetOptions;\n /** Accessible label for the container. */\n 'aria-label'?: string;\n}\n\nconst DEFAULT_LAYOUT: cytoscape.LayoutOptions = { name: 'preset' };\n\nexport const GraphCanvas = forwardRef<GraphCanvasHandle, GraphCanvasProps>(function GraphCanvas(\n {\n engine,\n elements,\n layout = DEFAULT_LAYOUT,\n onSelect,\n onClearSelection,\n onNodeHover,\n onNodeLeave,\n inspector,\n styleOptions,\n className,\n 'aria-label': ariaLabel = 'Graph canvas',\n ...props\n },\n forwardedRef,\n) {\n const containerRef = useRef<HTMLDivElement | null>(null);\n const cyRef = useRef<cytoscape.Core | null>(null);\n\n // Create the cytoscape instance once on mount.\n useEffect(() => {\n if (!containerRef.current) return undefined;\n const cy = engine({\n container: containerRef.current,\n elements,\n layout,\n style: buildShipItStylesheet(styleOptions),\n });\n cyRef.current = cy;\n return () => {\n cy.destroy();\n cyRef.current = null;\n };\n // We intentionally re-create on engine swap or container remount only.\n // Elements / layout / style updates are handled in the effects below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [engine]);\n\n // Keep elements in sync.\n useEffect(() => {\n const cy = cyRef.current;\n if (!cy) return;\n cy.json({ elements });\n }, [elements]);\n\n // Re-run layout when the layout config changes.\n useEffect(() => {\n const cy = cyRef.current;\n if (!cy) return;\n cy.layout(layout).run();\n }, [layout]);\n\n const { refresh: refreshStyles } = useShipItStylesheet(cyRef, styleOptions);\n\n // Wire selection events. `engine` is in the deps so the listeners re-bind\n // whenever the upstream effect destroys + recreates the Cytoscape instance —\n // without it, an `engine` swap would leave the new `cy` without handlers.\n useEffect(() => {\n const cy = cyRef.current;\n if (!cy) return undefined;\n const handleSelect = (event: cytoscape.EventObject) => {\n if (event.target?.isNode?.()) {\n onSelect?.(event.target as cytoscape.NodeSingular);\n }\n };\n const handleBackgroundTap = (event: cytoscape.EventObject) => {\n if (event.target === cy) onClearSelection?.();\n };\n const handleEnter = (event: cytoscape.EventObject) => {\n onNodeHover?.(event.target as cytoscape.NodeSingular);\n };\n const handleLeave = () => onNodeLeave?.();\n cy.on('tap', 'node', handleSelect);\n cy.on('tap', handleBackgroundTap);\n cy.on('mouseover', 'node', handleEnter);\n cy.on('mouseout', 'node', handleLeave);\n return () => {\n cy.off('tap', 'node', handleSelect);\n cy.off('tap', handleBackgroundTap);\n cy.off('mouseover', 'node', handleEnter);\n cy.off('mouseout', 'node', handleLeave);\n };\n }, [engine, onSelect, onClearSelection, onNodeHover, onNodeLeave]);\n\n // Expose the imperative handle via getters so consumers always read the live\n // `cyRef.current`. Snapshotting `cyRef.current` here would freeze it at\n // `null` because the instance is assigned inside an effect that runs *after*\n // the initial render — Copilot/Claude both flagged this.\n useImperativeHandle(\n forwardedRef,\n (): GraphCanvasHandle => ({\n get cy() {\n return cyRef.current;\n },\n refreshStyles,\n }),\n [refreshStyles],\n );\n\n return (\n <div\n role=\"region\"\n aria-label={ariaLabel}\n className={['relative h-full w-full', className].filter(Boolean).join(' ')}\n {...props}\n >\n {/*\n * Inline styles, not Tailwind utilities. Cytoscape injects an unlayered\n * `.__________cytoscape_container { position: relative }` stylesheet at\n * init time, and Tailwind v4 emits `.absolute`/`.inset-0` into\n * `@layer utilities` — unlayered rules outrank layered ones regardless\n * of source order, so the canvas would collapse to 0×0 in a static-\n * height parent. Inline styles win against both.\n */}\n <div ref={containerRef} style={{ position: 'absolute', inset: 0 }} />\n {inspector && <div className=\"absolute top-4 right-4 z-10\">{inspector}</div>}\n </div>\n );\n});\n\nGraphCanvas.displayName = 'GraphCanvas';\n"],"mappings":"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","names":["import_shipit","import_graph_tokens","import_react","GraphCanvas"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/stylesheet.ts","../src/theme-tokens.ts","../src/useShipItStylesheet.ts","../src/GraphCanvas.tsx"],"sourcesContent":["/**\n * @ship-it-ui/cytoscape — Cytoscape adapter for the Ship-It design system.\n *\n * Three layered exports:\n * - {@link buildShipItStylesheet} — token-driven stylesheet array.\n * - {@link useShipItStylesheet} — theme-aware re-resolver hook.\n * - {@link GraphCanvas} — `<GraphCanvas>` React wrapper that\n * owns the data-theme ↔ stylesheet ↔\n * inspector dance.\n *\n * Cytoscape is a peer dependency — pass `engine={cytoscape}` so the consumer\n * controls the version and any registered extensions.\n */\n\nexport {\n buildShipItStylesheet,\n GRAPH_CANVAS_CLASS,\n type BuildStylesheetOptions,\n type ShipItStylesheetBlock,\n} from './stylesheet';\n\nexport {\n useShipItStylesheet,\n type UseShipItStylesheetOptions,\n type UseShipItStylesheetReturn,\n} from './useShipItStylesheet';\n\nexport {\n GraphCanvas,\n type GraphCanvasProps,\n type GraphCanvasHandle,\n type CytoscapeEngine,\n} from './GraphCanvas';\n\nexport {\n readThemeTokens,\n resolveCssVar,\n resolveColorReference,\n resolveEntityColor,\n type ThemeTokenPalette,\n} from './theme-tokens';\n","import { iconToSvgDataUrl } from '@ship-it-ui/icons';\nimport { listEntityTypes } from '@ship-it-ui/shipit';\nimport type cytoscape from 'cytoscape';\n\nimport { readThemeTokens, resolveColorReference, type ThemeTokenPalette } from './theme-tokens';\n\n/**\n * Build a Cytoscape stylesheet from the live design tokens. The result is a\n * plain JSON array suitable for `cytoscape({ style: ... })` — re-run after a\n * `data-theme` change to pick up the new palette.\n *\n * The stylesheet is opinionated:\n * - Nodes are square-ish rounded glyphs colored by `data(entityType)`.\n * - Edges are token-colored thin lines with arrowheads.\n * - Selected / on-path / dimmed states are driven by class names that the\n * consumer toggles (`graph-canvas:selected`, `graph-canvas:path`,\n * `graph-canvas:dim`).\n *\n * Pass `palette` to override the token read — useful for SSR or tests.\n */\n\nexport interface BuildStylesheetOptions {\n /** Pre-resolved palette. When omitted, tokens are read from the document. */\n palette?: ThemeTokenPalette;\n /**\n * Additional entries appended to the stylesheet — handy for app-specific\n * selectors without forking the builder.\n */\n extra?: ReadonlyArray<cytoscape.StylesheetJsonBlock>;\n /**\n * Render each registered entity type's glyph (◇, ○, ▤, ↑, ◎, ▢ …) inside\n * the cytoscape node as a centered SVG data URL. Closes the visual gap\n * with the `<GraphNode>` React component — the docs page and the canvas\n * now share a vocabulary. Defaults to `true`. Pass `false` to fall back\n * to the original wireframe (border-only) per-type rule.\n */\n renderGlyphs?: boolean;\n /**\n * Fraction of the node that the rendered glyph occupies. Default `0.5` —\n * matches `<GraphNode>` where the icon is sized at ~42% of the square and\n * leaves breathing room inside the border. Set to `1` to revert to the\n * pre-0.0.7 behavior where the glyph filled the node edge-to-edge.\n */\n glyphScale?: number;\n}\n\n// Re-export the block type so consumers can declare typed `extra` entries.\nexport type ShipItStylesheetBlock = cytoscape.StylesheetJsonBlock;\n\nexport function buildShipItStylesheet(\n options: BuildStylesheetOptions = {},\n): cytoscape.StylesheetJson {\n const palette = options.palette ?? readThemeTokens();\n const color = (cssVar: string) => resolveColorReference(cssVar, palette);\n const renderGlyphs = options.renderGlyphs !== false;\n // Clamp into a sensible range. `0` would hide the glyph entirely; `1` paints\n // edge-to-edge (the legacy behavior). The default `0.5` matches the 26/52\n // ratio used by the `<GraphNode>` React component.\n const glyphScale = Math.max(0, Math.min(1, options.glyphScale ?? 0.5));\n const glyphSizePct = `${Math.round(glyphScale * 100)}%`;\n\n const base: cytoscape.StylesheetJsonBlock[] = [\n {\n selector: 'node',\n style: {\n 'background-color': palette.panel,\n 'border-width': 1.5,\n 'border-color': palette.accent,\n 'border-opacity': 1,\n label: 'data(label)',\n color: palette.textMuted,\n // Static stack instead of `var(--font-mono, monospace)` — cytoscape\n // can't resolve CSS variables outside the DOM cascade and emits a\n // warning per node selector at every mount. Consumers who need to\n // override the canvas font can do so via `options.extra`.\n 'font-family': 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',\n 'font-size': 10,\n 'text-valign': 'bottom',\n 'text-halign': 'center',\n 'text-margin-y': 6,\n // 52 matches `<GraphNode>`'s default size — the docs page and the\n // canvas now share dimensions. Pre-Issue-4 the canvas was 36×36.\n width: 52,\n height: 52,\n shape: 'round-rectangle',\n },\n },\n // One selector per entity type registered with @ship-it-ui/shipit. Built-in\n // types are seeded automatically; custom types registered via\n // `registerEntityType(...)` pick up their `colorVar` here without a docs\n // patch or a forked stylesheet.\n ...listEntityTypes().map<cytoscape.StylesheetJsonBlock>(([type, meta]) => {\n const c = color(meta.colorVar);\n // Pre-0.0.7 the glyph used `background-fit: contain`, which scales the\n // image to fill the node edge-to-edge and ignores the painted SVG's\n // intrinsic size. The icon ended up touching the border on every side\n // and looked cramped against the 52×52 node. We now set\n // `background-fit: none` and pin the painted width/height to a\n // fraction of the node (default 50%), with the image's anchor centered\n // — that gives the glyph breathing room and visually aligns with the\n // `<GraphNode>` React component used elsewhere in the design system.\n //\n // Cast through `unknown` because `cytoscape.Css.Node` doesn't (yet)\n // expose `background-width`/`background-height` in its TS types,\n // though the runtime accepts them per the cytoscape.js reference.\n const glyphStyle = renderGlyphs\n ? ({\n 'border-color': c,\n 'background-image': iconToSvgDataUrl(meta.iconName, { color: c }),\n 'background-fit': 'none',\n 'background-clip': 'none',\n 'background-width': glyphSizePct,\n 'background-height': glyphSizePct,\n 'background-position-x': '50%',\n 'background-position-y': '50%',\n } as unknown as cytoscape.Css.Node)\n : { 'border-color': c };\n return {\n selector: `node[entityType = \"${escapeCytoscapeAttr(type)}\"]`,\n style: glyphStyle,\n };\n }),\n {\n selector: 'node:selected',\n style: {\n 'border-width': 3,\n 'overlay-color': palette.accent,\n 'overlay-opacity': 0.15,\n 'overlay-padding': 4,\n },\n },\n {\n selector: 'node.graph-canvas\\\\:path',\n style: { 'border-color': palette.purple },\n },\n {\n selector: 'node.graph-canvas\\\\:dim',\n style: { opacity: 0.35 },\n },\n {\n // Default edges tone with `accent` so they read against the panel\n // background — matches `<GraphEdge edgeStyle=\"solid\">` in the docs.\n // Pre-fix the line drew in `palette.border` (the same tone as\n // surface-divider lines) and effectively disappeared on dark themes.\n selector: 'edge',\n style: {\n width: 1,\n 'line-color': palette.accent,\n 'target-arrow-color': palette.accent,\n 'target-arrow-shape': 'triangle',\n 'arrow-scale': 0.8,\n 'curve-style': 'bezier',\n },\n },\n {\n selector: 'edge.graph-canvas\\\\:path',\n style: {\n 'line-color': palette.purple,\n 'target-arrow-color': palette.purple,\n width: 1.5,\n },\n },\n {\n selector: 'edge.graph-canvas\\\\:dim',\n style: { opacity: 0.25 },\n },\n ];\n\n return [...base, ...(options.extra ?? [])] as cytoscape.StylesheetJson;\n}\n\n/**\n * Convenience class names the stylesheet recognizes. Toggle these on Cytoscape\n * nodes / edges to drive the on-path and dimmed visuals.\n */\nexport const GRAPH_CANVAS_CLASS = {\n path: 'graph-canvas:path',\n dim: 'graph-canvas:dim',\n} as const;\n\n// Cytoscape's attribute selector accepts a double-quoted string. Escape\n// embedded backslashes and double quotes so a malformed (or hostile)\n// registered key can't break out of the selector. The grammar\n// (https://js.cytoscape.org/#selectors/data) is more permissive than CSS, but\n// these two characters are the only ones that would change selector semantics.\nfunction escapeCytoscapeAttr(value: string): string {\n return value.replace(/[\\\\\"]/g, (c) => `\\\\${c}`);\n}\n","/**\n * Cytoscape-side wrapper around `@ship-it-ui/graph-tokens`. The token-reader\n * primitives (`resolveCssVar`, `toSrgb`, `readThemeTokens`,\n * `resolveColorReference`, `ThemeTokenPalette`) live in the shared package so\n * `@ship-it-ui/graph-editor` (React Flow) can consume them without dragging\n * Cytoscape in. Only `resolveEntityColor` lives here because it bridges to the\n * `@ship-it-ui/shipit` entity-type registry.\n */\n\nimport { resolveColorReference, type ThemeTokenPalette } from '@ship-it-ui/graph-tokens';\nimport { getEntityTypeMeta, type EntityType } from '@ship-it-ui/shipit';\n\nexport {\n readThemeTokens,\n resolveColorReference,\n resolveCssVar,\n toSrgb,\n type ThemeTokenPalette,\n} from '@ship-it-ui/graph-tokens';\n\n/**\n * Resolve the concrete color for a registered entity type. Reads the type's\n * `colorVar` (a `var(--color-…)` string) and looks the value up in the\n * palette. Falls back to the palette's `accent` color when the var is\n * malformed or unknown.\n */\nexport function resolveEntityColor(type: EntityType, palette: ThemeTokenPalette): string {\n const meta = getEntityTypeMeta(type);\n return resolveColorReference(meta.colorVar, palette);\n}\n","'use client';\n\nimport type cytoscape from 'cytoscape';\nimport { useCallback, useEffect, useMemo } from 'react';\n\nimport { buildShipItStylesheet, type BuildStylesheetOptions } from './stylesheet';\n\n/**\n * useShipItStylesheet — keeps a Cytoscape instance's stylesheet in sync with\n * the live design-token palette. Re-applies the stylesheet whenever\n * `<html data-theme>` flips, so toggling between dark and light propagates to\n * the graph without remounting.\n *\n * Returns a `refresh()` callback the consumer can invoke after any other\n * theme-affecting change (e.g., a `--color-accent` hue knob update).\n *\n * ```ts\n * const cyRef = useRef<cytoscape.Core | null>(null);\n * const { refresh } = useShipItStylesheet(cyRef);\n * ```\n */\n\nexport interface UseShipItStylesheetOptions extends BuildStylesheetOptions {\n /** Skip the MutationObserver wiring (e.g., when the host owns its own observer). */\n observe?: boolean;\n}\n\nexport interface UseShipItStylesheetReturn {\n /** Re-read tokens and re-apply the stylesheet. */\n refresh: () => void;\n}\n\nexport function useShipItStylesheet(\n cyRef: { current: cytoscape.Core | null },\n options: UseShipItStylesheetOptions = {},\n): UseShipItStylesheetReturn {\n const { observe = true, palette, extra } = options;\n // Memoize the build options against their flat constituents so callers that\n // pass `options` inline (a fresh object each render) don't churn `apply` /\n // disconnect+reconnect the MutationObserver on every render.\n const buildOptions = useMemo<BuildStylesheetOptions>(\n () => ({ palette, extra }),\n [palette, extra],\n );\n\n const apply = useCallback(() => {\n const cy = cyRef.current;\n if (!cy) return;\n cy.style(buildShipItStylesheet(buildOptions)).update();\n }, [cyRef, buildOptions]);\n\n useEffect(() => {\n apply();\n if (!observe || typeof document === 'undefined') return undefined;\n const observer = new MutationObserver(apply);\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: ['data-theme'],\n });\n return () => observer.disconnect();\n }, [apply, observe]);\n\n return { refresh: apply };\n}\n","'use client';\n\nimport type cytoscape from 'cytoscape';\nimport {\n forwardRef,\n useEffect,\n useImperativeHandle,\n useRef,\n type HTMLAttributes,\n type ReactNode,\n} from 'react';\n\nimport { buildShipItStylesheet, type BuildStylesheetOptions } from './stylesheet';\nimport { useShipItStylesheet } from './useShipItStylesheet';\n\n/**\n * GraphCanvas — high-level wrapper around a Cytoscape instance. Owns the\n * `data-theme` ↔ stylesheet sync (via {@link useShipItStylesheet}), the\n * cytoscape lifecycle (create / destroy on mount / unmount), and a thin\n * selection API on top of Cytoscape's events.\n *\n * The component never bundles Cytoscape itself — pass the engine factory in\n * via the `engine` prop so the consumer controls the Cytoscape version and\n * any registered extensions:\n *\n * ```tsx\n * import cytoscape from 'cytoscape';\n *\n * <GraphCanvas\n * engine={cytoscape}\n * elements={[...]}\n * layout={{ name: 'cose' }}\n * onSelect={(node) => setSelected(node.id())}\n * inspector={selected && <GraphInspector …/>}\n * />\n * ```\n */\n\nexport type CytoscapeEngine = (options: cytoscape.CytoscapeOptions) => cytoscape.Core;\n\nexport interface GraphCanvasHandle {\n /** Live Cytoscape instance. `null` until mount. */\n cy: cytoscape.Core | null;\n /** Re-read tokens and re-apply the stylesheet. */\n refreshStyles: () => void;\n}\n\nexport interface GraphCanvasProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n 'onSelect' | 'children'\n> {\n /** Cytoscape factory. Pass the imported `cytoscape` default export. */\n engine: CytoscapeEngine;\n /** Graph elements (nodes + edges). Passed straight through to Cytoscape. */\n elements: cytoscape.ElementDefinition[];\n /** Layout config. Defaults to a static `preset` layout (no auto-layout). */\n layout?: cytoscape.LayoutOptions;\n /** Fires when a node is tapped/selected. */\n onSelect?: (node: cytoscape.NodeSingular) => void;\n /** Fires when the selection is cleared (background tap). */\n onClearSelection?: () => void;\n /** Fires when the pointer enters a node. */\n onNodeHover?: (node: cytoscape.NodeSingular) => void;\n /** Fires when the pointer leaves a node. */\n onNodeLeave?: () => void;\n /** Slot rendered over the graph (e.g., a `<GraphInspector>`). Positioned top-right. */\n inspector?: ReactNode;\n /** Overrides for the stylesheet builder. */\n styleOptions?: BuildStylesheetOptions;\n /** Accessible label for the container. */\n 'aria-label'?: string;\n}\n\nconst DEFAULT_LAYOUT: cytoscape.LayoutOptions = { name: 'preset' };\n\nexport const GraphCanvas = forwardRef<GraphCanvasHandle, GraphCanvasProps>(function GraphCanvas(\n {\n engine,\n elements,\n layout = DEFAULT_LAYOUT,\n onSelect,\n onClearSelection,\n onNodeHover,\n onNodeLeave,\n inspector,\n styleOptions,\n className,\n 'aria-label': ariaLabel = 'Graph canvas',\n ...props\n },\n forwardedRef,\n) {\n const containerRef = useRef<HTMLDivElement | null>(null);\n const cyRef = useRef<cytoscape.Core | null>(null);\n\n // Create the cytoscape instance once on mount.\n useEffect(() => {\n if (!containerRef.current) return undefined;\n const cy = engine({\n container: containerRef.current,\n elements,\n layout,\n style: buildShipItStylesheet(styleOptions),\n });\n cyRef.current = cy;\n return () => {\n cy.destroy();\n cyRef.current = null;\n };\n // We intentionally re-create on engine swap or container remount only.\n // Elements / layout / style updates are handled in the effects below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [engine]);\n\n // Keep elements in sync.\n useEffect(() => {\n const cy = cyRef.current;\n if (!cy) return;\n cy.json({ elements });\n }, [elements]);\n\n // Re-run layout when the layout config changes.\n useEffect(() => {\n const cy = cyRef.current;\n if (!cy) return;\n cy.layout(layout).run();\n }, [layout]);\n\n const { refresh: refreshStyles } = useShipItStylesheet(cyRef, styleOptions);\n\n // Wire selection events. `engine` is in the deps so the listeners re-bind\n // whenever the upstream effect destroys + recreates the Cytoscape instance —\n // without it, an `engine` swap would leave the new `cy` without handlers.\n useEffect(() => {\n const cy = cyRef.current;\n if (!cy) return undefined;\n const handleSelect = (event: cytoscape.EventObject) => {\n if (event.target?.isNode?.()) {\n onSelect?.(event.target as cytoscape.NodeSingular);\n }\n };\n const handleBackgroundTap = (event: cytoscape.EventObject) => {\n if (event.target === cy) onClearSelection?.();\n };\n const handleEnter = (event: cytoscape.EventObject) => {\n onNodeHover?.(event.target as cytoscape.NodeSingular);\n };\n const handleLeave = () => onNodeLeave?.();\n cy.on('tap', 'node', handleSelect);\n cy.on('tap', handleBackgroundTap);\n cy.on('mouseover', 'node', handleEnter);\n cy.on('mouseout', 'node', handleLeave);\n return () => {\n cy.off('tap', 'node', handleSelect);\n cy.off('tap', handleBackgroundTap);\n cy.off('mouseover', 'node', handleEnter);\n cy.off('mouseout', 'node', handleLeave);\n };\n }, [engine, onSelect, onClearSelection, onNodeHover, onNodeLeave]);\n\n // Expose the imperative handle via getters so consumers always read the live\n // `cyRef.current`. Snapshotting `cyRef.current` here would freeze it at\n // `null` because the instance is assigned inside an effect that runs *after*\n // the initial render — Copilot/Claude both flagged this.\n useImperativeHandle(\n forwardedRef,\n (): GraphCanvasHandle => ({\n get cy() {\n return cyRef.current;\n },\n refreshStyles,\n }),\n [refreshStyles],\n );\n\n return (\n <div\n role=\"region\"\n aria-label={ariaLabel}\n className={['relative h-full w-full', className].filter(Boolean).join(' ')}\n {...props}\n >\n {/*\n * Inline styles, not Tailwind utilities. Cytoscape injects an unlayered\n * `.__________cytoscape_container { position: relative }` stylesheet at\n * init time, and Tailwind v4 emits `.absolute`/`.inset-0` into\n * `@layer utilities` — unlayered rules outrank layered ones regardless\n * of source order, so the canvas would collapse to 0×0 in a static-\n * height parent. Inline styles win against both.\n */}\n <div ref={containerRef} style={{ position: 'absolute', inset: 0 }} />\n {inspector && <div className=\"absolute top-4 right-4 z-10\">{inspector}</div>}\n </div>\n );\n});\n\nGraphCanvas.displayName = 'GraphCanvas';\n"],"mappings":"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","names":["import_shipit","import_graph_tokens","import_react","GraphCanvas"]}
|
package/dist/index.js
CHANGED
|
@@ -5,8 +5,8 @@ import { iconToSvgDataUrl } from "@ship-it-ui/icons";
|
|
|
5
5
|
import { listEntityTypes } from "@ship-it-ui/shipit";
|
|
6
6
|
|
|
7
7
|
// src/theme-tokens.ts
|
|
8
|
-
import { getEntityTypeMeta } from "@ship-it-ui/shipit";
|
|
9
8
|
import { resolveColorReference } from "@ship-it-ui/graph-tokens";
|
|
9
|
+
import { getEntityTypeMeta } from "@ship-it-ui/shipit";
|
|
10
10
|
import {
|
|
11
11
|
readThemeTokens,
|
|
12
12
|
resolveColorReference as resolveColorReference2,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/stylesheet.ts","../src/theme-tokens.ts","../src/useShipItStylesheet.ts","../src/GraphCanvas.tsx"],"sourcesContent":["import { iconToSvgDataUrl } from '@ship-it-ui/icons';\nimport { listEntityTypes } from '@ship-it-ui/shipit';\nimport type cytoscape from 'cytoscape';\n\nimport { readThemeTokens, resolveColorReference, type ThemeTokenPalette } from './theme-tokens';\n\n/**\n * Build a Cytoscape stylesheet from the live design tokens. The result is a\n * plain JSON array suitable for `cytoscape({ style: ... })` — re-run after a\n * `data-theme` change to pick up the new palette.\n *\n * The stylesheet is opinionated:\n * - Nodes are square-ish rounded glyphs colored by `data(entityType)`.\n * - Edges are token-colored thin lines with arrowheads.\n * - Selected / on-path / dimmed states are driven by class names that the\n * consumer toggles (`graph-canvas:selected`, `graph-canvas:path`,\n * `graph-canvas:dim`).\n *\n * Pass `palette` to override the token read — useful for SSR or tests.\n */\n\nexport interface BuildStylesheetOptions {\n /** Pre-resolved palette. When omitted, tokens are read from the document. */\n palette?: ThemeTokenPalette;\n /**\n * Additional entries appended to the stylesheet — handy for app-specific\n * selectors without forking the builder.\n */\n extra?: ReadonlyArray<cytoscape.StylesheetJsonBlock>;\n /**\n * Render each registered entity type's glyph (◇, ○, ▤, ↑, ◎, ▢ …) inside\n * the cytoscape node as a centered SVG data URL. Closes the visual gap\n * with the `<GraphNode>` React component — the docs page and the canvas\n * now share a vocabulary. Defaults to `true`. Pass `false` to fall back\n * to the original wireframe (border-only) per-type rule.\n */\n renderGlyphs?: boolean;\n /**\n * Fraction of the node that the rendered glyph occupies. Default `0.5` —\n * matches `<GraphNode>` where the icon is sized at ~42% of the square and\n * leaves breathing room inside the border. Set to `1` to revert to the\n * pre-0.0.7 behavior where the glyph filled the node edge-to-edge.\n */\n glyphScale?: number;\n}\n\n// Re-export the block type so consumers can declare typed `extra` entries.\nexport type ShipItStylesheetBlock = cytoscape.StylesheetJsonBlock;\n\nexport function buildShipItStylesheet(\n options: BuildStylesheetOptions = {},\n): cytoscape.StylesheetJson {\n const palette = options.palette ?? readThemeTokens();\n const color = (cssVar: string) => resolveColorReference(cssVar, palette);\n const renderGlyphs = options.renderGlyphs !== false;\n // Clamp into a sensible range. `0` would hide the glyph entirely; `1` paints\n // edge-to-edge (the legacy behavior). The default `0.5` matches the 26/52\n // ratio used by the `<GraphNode>` React component.\n const glyphScale = Math.max(0, Math.min(1, options.glyphScale ?? 0.5));\n const glyphSizePct = `${Math.round(glyphScale * 100)}%`;\n\n const base: cytoscape.StylesheetJsonBlock[] = [\n {\n selector: 'node',\n style: {\n 'background-color': palette.panel,\n 'border-width': 1.5,\n 'border-color': palette.accent,\n 'border-opacity': 1,\n label: 'data(label)',\n color: palette.textMuted,\n // Static stack instead of `var(--font-mono, monospace)` — cytoscape\n // can't resolve CSS variables outside the DOM cascade and emits a\n // warning per node selector at every mount. Consumers who need to\n // override the canvas font can do so via `options.extra`.\n 'font-family': 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',\n 'font-size': 10,\n 'text-valign': 'bottom',\n 'text-halign': 'center',\n 'text-margin-y': 6,\n // 52 matches `<GraphNode>`'s default size — the docs page and the\n // canvas now share dimensions. Pre-Issue-4 the canvas was 36×36.\n width: 52,\n height: 52,\n shape: 'round-rectangle',\n },\n },\n // One selector per entity type registered with @ship-it-ui/shipit. Built-in\n // types are seeded automatically; custom types registered via\n // `registerEntityType(...)` pick up their `colorVar` here without a docs\n // patch or a forked stylesheet.\n ...listEntityTypes().map<cytoscape.StylesheetJsonBlock>(([type, meta]) => {\n const c = color(meta.colorVar);\n // Pre-0.0.7 the glyph used `background-fit: contain`, which scales the\n // image to fill the node edge-to-edge and ignores the painted SVG's\n // intrinsic size. The icon ended up touching the border on every side\n // and looked cramped against the 52×52 node. We now set\n // `background-fit: none` and pin the painted width/height to a\n // fraction of the node (default 50%), with the image's anchor centered\n // — that gives the glyph breathing room and visually aligns with the\n // `<GraphNode>` React component used elsewhere in the design system.\n //\n // Cast through `unknown` because `cytoscape.Css.Node` doesn't (yet)\n // expose `background-width`/`background-height` in its TS types,\n // though the runtime accepts them per the cytoscape.js reference.\n const glyphStyle = renderGlyphs\n ? ({\n 'border-color': c,\n 'background-image': iconToSvgDataUrl(meta.iconName, { color: c }),\n 'background-fit': 'none',\n 'background-clip': 'none',\n 'background-width': glyphSizePct,\n 'background-height': glyphSizePct,\n 'background-position-x': '50%',\n 'background-position-y': '50%',\n } as unknown as cytoscape.Css.Node)\n : { 'border-color': c };\n return {\n selector: `node[entityType = \"${escapeCytoscapeAttr(type)}\"]`,\n style: glyphStyle,\n };\n }),\n {\n selector: 'node:selected',\n style: {\n 'border-width': 3,\n 'overlay-color': palette.accent,\n 'overlay-opacity': 0.15,\n 'overlay-padding': 4,\n },\n },\n {\n selector: 'node.graph-canvas\\\\:path',\n style: { 'border-color': palette.purple },\n },\n {\n selector: 'node.graph-canvas\\\\:dim',\n style: { opacity: 0.35 },\n },\n {\n // Default edges tone with `accent` so they read against the panel\n // background — matches `<GraphEdge edgeStyle=\"solid\">` in the docs.\n // Pre-fix the line drew in `palette.border` (the same tone as\n // surface-divider lines) and effectively disappeared on dark themes.\n selector: 'edge',\n style: {\n width: 1,\n 'line-color': palette.accent,\n 'target-arrow-color': palette.accent,\n 'target-arrow-shape': 'triangle',\n 'arrow-scale': 0.8,\n 'curve-style': 'bezier',\n },\n },\n {\n selector: 'edge.graph-canvas\\\\:path',\n style: {\n 'line-color': palette.purple,\n 'target-arrow-color': palette.purple,\n width: 1.5,\n },\n },\n {\n selector: 'edge.graph-canvas\\\\:dim',\n style: { opacity: 0.25 },\n },\n ];\n\n return [...base, ...(options.extra ?? [])] as cytoscape.StylesheetJson;\n}\n\n/**\n * Convenience class names the stylesheet recognizes. Toggle these on Cytoscape\n * nodes / edges to drive the on-path and dimmed visuals.\n */\nexport const GRAPH_CANVAS_CLASS = {\n path: 'graph-canvas:path',\n dim: 'graph-canvas:dim',\n} as const;\n\n// Cytoscape's attribute selector accepts a double-quoted string. Escape\n// embedded backslashes and double quotes so a malformed (or hostile)\n// registered key can't break out of the selector. The grammar\n// (https://js.cytoscape.org/#selectors/data) is more permissive than CSS, but\n// these two characters are the only ones that would change selector semantics.\nfunction escapeCytoscapeAttr(value: string): string {\n return value.replace(/[\\\\\"]/g, (c) => `\\\\${c}`);\n}\n","/**\n * Cytoscape-side wrapper around `@ship-it-ui/graph-tokens`. The token-reader\n * primitives (`resolveCssVar`, `toSrgb`, `readThemeTokens`,\n * `resolveColorReference`, `ThemeTokenPalette`) live in the shared package so\n * `@ship-it-ui/graph-editor` (React Flow) can consume them without dragging\n * Cytoscape in. Only `resolveEntityColor` lives here because it bridges to the\n * `@ship-it-ui/shipit` entity-type registry.\n */\n\nimport { getEntityTypeMeta, type EntityType } from '@ship-it-ui/shipit';\nimport { resolveColorReference, type ThemeTokenPalette } from '@ship-it-ui/graph-tokens';\n\nexport {\n readThemeTokens,\n resolveColorReference,\n resolveCssVar,\n toSrgb,\n type ThemeTokenPalette,\n} from '@ship-it-ui/graph-tokens';\n\n/**\n * Resolve the concrete color for a registered entity type. Reads the type's\n * `colorVar` (a `var(--color-…)` string) and looks the value up in the\n * palette. Falls back to the palette's `accent` color when the var is\n * malformed or unknown.\n */\nexport function resolveEntityColor(type: EntityType, palette: ThemeTokenPalette): string {\n const meta = getEntityTypeMeta(type);\n return resolveColorReference(meta.colorVar, palette);\n}\n","'use client';\n\nimport type cytoscape from 'cytoscape';\nimport { useCallback, useEffect, useMemo } from 'react';\n\nimport { buildShipItStylesheet, type BuildStylesheetOptions } from './stylesheet';\n\n/**\n * useShipItStylesheet — keeps a Cytoscape instance's stylesheet in sync with\n * the live design-token palette. Re-applies the stylesheet whenever\n * `<html data-theme>` flips, so toggling between dark and light propagates to\n * the graph without remounting.\n *\n * Returns a `refresh()` callback the consumer can invoke after any other\n * theme-affecting change (e.g., a `--color-accent` hue knob update).\n *\n * ```ts\n * const cyRef = useRef<cytoscape.Core | null>(null);\n * const { refresh } = useShipItStylesheet(cyRef);\n * ```\n */\n\nexport interface UseShipItStylesheetOptions extends BuildStylesheetOptions {\n /** Skip the MutationObserver wiring (e.g., when the host owns its own observer). */\n observe?: boolean;\n}\n\nexport interface UseShipItStylesheetReturn {\n /** Re-read tokens and re-apply the stylesheet. */\n refresh: () => void;\n}\n\nexport function useShipItStylesheet(\n cyRef: { current: cytoscape.Core | null },\n options: UseShipItStylesheetOptions = {},\n): UseShipItStylesheetReturn {\n const { observe = true, palette, extra } = options;\n // Memoize the build options against their flat constituents so callers that\n // pass `options` inline (a fresh object each render) don't churn `apply` /\n // disconnect+reconnect the MutationObserver on every render.\n const buildOptions = useMemo<BuildStylesheetOptions>(\n () => ({ palette, extra }),\n [palette, extra],\n );\n\n const apply = useCallback(() => {\n const cy = cyRef.current;\n if (!cy) return;\n cy.style(buildShipItStylesheet(buildOptions)).update();\n }, [cyRef, buildOptions]);\n\n useEffect(() => {\n apply();\n if (!observe || typeof document === 'undefined') return undefined;\n const observer = new MutationObserver(apply);\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: ['data-theme'],\n });\n return () => observer.disconnect();\n }, [apply, observe]);\n\n return { refresh: apply };\n}\n","'use client';\n\nimport type cytoscape from 'cytoscape';\nimport {\n forwardRef,\n useEffect,\n useImperativeHandle,\n useRef,\n type HTMLAttributes,\n type ReactNode,\n} from 'react';\n\nimport { buildShipItStylesheet, type BuildStylesheetOptions } from './stylesheet';\nimport { useShipItStylesheet } from './useShipItStylesheet';\n\n/**\n * GraphCanvas — high-level wrapper around a Cytoscape instance. Owns the\n * `data-theme` ↔ stylesheet sync (via {@link useShipItStylesheet}), the\n * cytoscape lifecycle (create / destroy on mount / unmount), and a thin\n * selection API on top of Cytoscape's events.\n *\n * The component never bundles Cytoscape itself — pass the engine factory in\n * via the `engine` prop so the consumer controls the Cytoscape version and\n * any registered extensions:\n *\n * ```tsx\n * import cytoscape from 'cytoscape';\n *\n * <GraphCanvas\n * engine={cytoscape}\n * elements={[...]}\n * layout={{ name: 'cose' }}\n * onSelect={(node) => setSelected(node.id())}\n * inspector={selected && <GraphInspector …/>}\n * />\n * ```\n */\n\nexport type CytoscapeEngine = (options: cytoscape.CytoscapeOptions) => cytoscape.Core;\n\nexport interface GraphCanvasHandle {\n /** Live Cytoscape instance. `null` until mount. */\n cy: cytoscape.Core | null;\n /** Re-read tokens and re-apply the stylesheet. */\n refreshStyles: () => void;\n}\n\nexport interface GraphCanvasProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n 'onSelect' | 'children'\n> {\n /** Cytoscape factory. Pass the imported `cytoscape` default export. */\n engine: CytoscapeEngine;\n /** Graph elements (nodes + edges). Passed straight through to Cytoscape. */\n elements: cytoscape.ElementDefinition[];\n /** Layout config. Defaults to a static `preset` layout (no auto-layout). */\n layout?: cytoscape.LayoutOptions;\n /** Fires when a node is tapped/selected. */\n onSelect?: (node: cytoscape.NodeSingular) => void;\n /** Fires when the selection is cleared (background tap). */\n onClearSelection?: () => void;\n /** Fires when the pointer enters a node. */\n onNodeHover?: (node: cytoscape.NodeSingular) => void;\n /** Fires when the pointer leaves a node. */\n onNodeLeave?: () => void;\n /** Slot rendered over the graph (e.g., a `<GraphInspector>`). Positioned top-right. */\n inspector?: ReactNode;\n /** Overrides for the stylesheet builder. */\n styleOptions?: BuildStylesheetOptions;\n /** Accessible label for the container. */\n 'aria-label'?: string;\n}\n\nconst DEFAULT_LAYOUT: cytoscape.LayoutOptions = { name: 'preset' };\n\nexport const GraphCanvas = forwardRef<GraphCanvasHandle, GraphCanvasProps>(function GraphCanvas(\n {\n engine,\n elements,\n layout = DEFAULT_LAYOUT,\n onSelect,\n onClearSelection,\n onNodeHover,\n onNodeLeave,\n inspector,\n styleOptions,\n className,\n 'aria-label': ariaLabel = 'Graph canvas',\n ...props\n },\n forwardedRef,\n) {\n const containerRef = useRef<HTMLDivElement | null>(null);\n const cyRef = useRef<cytoscape.Core | null>(null);\n\n // Create the cytoscape instance once on mount.\n useEffect(() => {\n if (!containerRef.current) return undefined;\n const cy = engine({\n container: containerRef.current,\n elements,\n layout,\n style: buildShipItStylesheet(styleOptions),\n });\n cyRef.current = cy;\n return () => {\n cy.destroy();\n cyRef.current = null;\n };\n // We intentionally re-create on engine swap or container remount only.\n // Elements / layout / style updates are handled in the effects below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [engine]);\n\n // Keep elements in sync.\n useEffect(() => {\n const cy = cyRef.current;\n if (!cy) return;\n cy.json({ elements });\n }, [elements]);\n\n // Re-run layout when the layout config changes.\n useEffect(() => {\n const cy = cyRef.current;\n if (!cy) return;\n cy.layout(layout).run();\n }, [layout]);\n\n const { refresh: refreshStyles } = useShipItStylesheet(cyRef, styleOptions);\n\n // Wire selection events. `engine` is in the deps so the listeners re-bind\n // whenever the upstream effect destroys + recreates the Cytoscape instance —\n // without it, an `engine` swap would leave the new `cy` without handlers.\n useEffect(() => {\n const cy = cyRef.current;\n if (!cy) return undefined;\n const handleSelect = (event: cytoscape.EventObject) => {\n if (event.target?.isNode?.()) {\n onSelect?.(event.target as cytoscape.NodeSingular);\n }\n };\n const handleBackgroundTap = (event: cytoscape.EventObject) => {\n if (event.target === cy) onClearSelection?.();\n };\n const handleEnter = (event: cytoscape.EventObject) => {\n onNodeHover?.(event.target as cytoscape.NodeSingular);\n };\n const handleLeave = () => onNodeLeave?.();\n cy.on('tap', 'node', handleSelect);\n cy.on('tap', handleBackgroundTap);\n cy.on('mouseover', 'node', handleEnter);\n cy.on('mouseout', 'node', handleLeave);\n return () => {\n cy.off('tap', 'node', handleSelect);\n cy.off('tap', handleBackgroundTap);\n cy.off('mouseover', 'node', handleEnter);\n cy.off('mouseout', 'node', handleLeave);\n };\n }, [engine, onSelect, onClearSelection, onNodeHover, onNodeLeave]);\n\n // Expose the imperative handle via getters so consumers always read the live\n // `cyRef.current`. Snapshotting `cyRef.current` here would freeze it at\n // `null` because the instance is assigned inside an effect that runs *after*\n // the initial render — Copilot/Claude both flagged this.\n useImperativeHandle(\n forwardedRef,\n (): GraphCanvasHandle => ({\n get cy() {\n return cyRef.current;\n },\n refreshStyles,\n }),\n [refreshStyles],\n );\n\n return (\n <div\n role=\"region\"\n aria-label={ariaLabel}\n className={['relative h-full w-full', className].filter(Boolean).join(' ')}\n {...props}\n >\n {/*\n * Inline styles, not Tailwind utilities. Cytoscape injects an unlayered\n * `.__________cytoscape_container { position: relative }` stylesheet at\n * init time, and Tailwind v4 emits `.absolute`/`.inset-0` into\n * `@layer utilities` — unlayered rules outrank layered ones regardless\n * of source order, so the canvas would collapse to 0×0 in a static-\n * height parent. Inline styles win against both.\n */}\n <div ref={containerRef} style={{ position: 'absolute', inset: 0 }} />\n {inspector && <div className=\"absolute top-4 right-4 z-10\">{inspector}</div>}\n </div>\n );\n});\n\nGraphCanvas.displayName = 'GraphCanvas';\n"],"mappings":"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","names":["resolveColorReference","resolveColorReference","useEffect","GraphCanvas","useEffect"]}
|
|
1
|
+
{"version":3,"sources":["../src/stylesheet.ts","../src/theme-tokens.ts","../src/useShipItStylesheet.ts","../src/GraphCanvas.tsx"],"sourcesContent":["import { iconToSvgDataUrl } from '@ship-it-ui/icons';\nimport { listEntityTypes } from '@ship-it-ui/shipit';\nimport type cytoscape from 'cytoscape';\n\nimport { readThemeTokens, resolveColorReference, type ThemeTokenPalette } from './theme-tokens';\n\n/**\n * Build a Cytoscape stylesheet from the live design tokens. The result is a\n * plain JSON array suitable for `cytoscape({ style: ... })` — re-run after a\n * `data-theme` change to pick up the new palette.\n *\n * The stylesheet is opinionated:\n * - Nodes are square-ish rounded glyphs colored by `data(entityType)`.\n * - Edges are token-colored thin lines with arrowheads.\n * - Selected / on-path / dimmed states are driven by class names that the\n * consumer toggles (`graph-canvas:selected`, `graph-canvas:path`,\n * `graph-canvas:dim`).\n *\n * Pass `palette` to override the token read — useful for SSR or tests.\n */\n\nexport interface BuildStylesheetOptions {\n /** Pre-resolved palette. When omitted, tokens are read from the document. */\n palette?: ThemeTokenPalette;\n /**\n * Additional entries appended to the stylesheet — handy for app-specific\n * selectors without forking the builder.\n */\n extra?: ReadonlyArray<cytoscape.StylesheetJsonBlock>;\n /**\n * Render each registered entity type's glyph (◇, ○, ▤, ↑, ◎, ▢ …) inside\n * the cytoscape node as a centered SVG data URL. Closes the visual gap\n * with the `<GraphNode>` React component — the docs page and the canvas\n * now share a vocabulary. Defaults to `true`. Pass `false` to fall back\n * to the original wireframe (border-only) per-type rule.\n */\n renderGlyphs?: boolean;\n /**\n * Fraction of the node that the rendered glyph occupies. Default `0.5` —\n * matches `<GraphNode>` where the icon is sized at ~42% of the square and\n * leaves breathing room inside the border. Set to `1` to revert to the\n * pre-0.0.7 behavior where the glyph filled the node edge-to-edge.\n */\n glyphScale?: number;\n}\n\n// Re-export the block type so consumers can declare typed `extra` entries.\nexport type ShipItStylesheetBlock = cytoscape.StylesheetJsonBlock;\n\nexport function buildShipItStylesheet(\n options: BuildStylesheetOptions = {},\n): cytoscape.StylesheetJson {\n const palette = options.palette ?? readThemeTokens();\n const color = (cssVar: string) => resolveColorReference(cssVar, palette);\n const renderGlyphs = options.renderGlyphs !== false;\n // Clamp into a sensible range. `0` would hide the glyph entirely; `1` paints\n // edge-to-edge (the legacy behavior). The default `0.5` matches the 26/52\n // ratio used by the `<GraphNode>` React component.\n const glyphScale = Math.max(0, Math.min(1, options.glyphScale ?? 0.5));\n const glyphSizePct = `${Math.round(glyphScale * 100)}%`;\n\n const base: cytoscape.StylesheetJsonBlock[] = [\n {\n selector: 'node',\n style: {\n 'background-color': palette.panel,\n 'border-width': 1.5,\n 'border-color': palette.accent,\n 'border-opacity': 1,\n label: 'data(label)',\n color: palette.textMuted,\n // Static stack instead of `var(--font-mono, monospace)` — cytoscape\n // can't resolve CSS variables outside the DOM cascade and emits a\n // warning per node selector at every mount. Consumers who need to\n // override the canvas font can do so via `options.extra`.\n 'font-family': 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',\n 'font-size': 10,\n 'text-valign': 'bottom',\n 'text-halign': 'center',\n 'text-margin-y': 6,\n // 52 matches `<GraphNode>`'s default size — the docs page and the\n // canvas now share dimensions. Pre-Issue-4 the canvas was 36×36.\n width: 52,\n height: 52,\n shape: 'round-rectangle',\n },\n },\n // One selector per entity type registered with @ship-it-ui/shipit. Built-in\n // types are seeded automatically; custom types registered via\n // `registerEntityType(...)` pick up their `colorVar` here without a docs\n // patch or a forked stylesheet.\n ...listEntityTypes().map<cytoscape.StylesheetJsonBlock>(([type, meta]) => {\n const c = color(meta.colorVar);\n // Pre-0.0.7 the glyph used `background-fit: contain`, which scales the\n // image to fill the node edge-to-edge and ignores the painted SVG's\n // intrinsic size. The icon ended up touching the border on every side\n // and looked cramped against the 52×52 node. We now set\n // `background-fit: none` and pin the painted width/height to a\n // fraction of the node (default 50%), with the image's anchor centered\n // — that gives the glyph breathing room and visually aligns with the\n // `<GraphNode>` React component used elsewhere in the design system.\n //\n // Cast through `unknown` because `cytoscape.Css.Node` doesn't (yet)\n // expose `background-width`/`background-height` in its TS types,\n // though the runtime accepts them per the cytoscape.js reference.\n const glyphStyle = renderGlyphs\n ? ({\n 'border-color': c,\n 'background-image': iconToSvgDataUrl(meta.iconName, { color: c }),\n 'background-fit': 'none',\n 'background-clip': 'none',\n 'background-width': glyphSizePct,\n 'background-height': glyphSizePct,\n 'background-position-x': '50%',\n 'background-position-y': '50%',\n } as unknown as cytoscape.Css.Node)\n : { 'border-color': c };\n return {\n selector: `node[entityType = \"${escapeCytoscapeAttr(type)}\"]`,\n style: glyphStyle,\n };\n }),\n {\n selector: 'node:selected',\n style: {\n 'border-width': 3,\n 'overlay-color': palette.accent,\n 'overlay-opacity': 0.15,\n 'overlay-padding': 4,\n },\n },\n {\n selector: 'node.graph-canvas\\\\:path',\n style: { 'border-color': palette.purple },\n },\n {\n selector: 'node.graph-canvas\\\\:dim',\n style: { opacity: 0.35 },\n },\n {\n // Default edges tone with `accent` so they read against the panel\n // background — matches `<GraphEdge edgeStyle=\"solid\">` in the docs.\n // Pre-fix the line drew in `palette.border` (the same tone as\n // surface-divider lines) and effectively disappeared on dark themes.\n selector: 'edge',\n style: {\n width: 1,\n 'line-color': palette.accent,\n 'target-arrow-color': palette.accent,\n 'target-arrow-shape': 'triangle',\n 'arrow-scale': 0.8,\n 'curve-style': 'bezier',\n },\n },\n {\n selector: 'edge.graph-canvas\\\\:path',\n style: {\n 'line-color': palette.purple,\n 'target-arrow-color': palette.purple,\n width: 1.5,\n },\n },\n {\n selector: 'edge.graph-canvas\\\\:dim',\n style: { opacity: 0.25 },\n },\n ];\n\n return [...base, ...(options.extra ?? [])] as cytoscape.StylesheetJson;\n}\n\n/**\n * Convenience class names the stylesheet recognizes. Toggle these on Cytoscape\n * nodes / edges to drive the on-path and dimmed visuals.\n */\nexport const GRAPH_CANVAS_CLASS = {\n path: 'graph-canvas:path',\n dim: 'graph-canvas:dim',\n} as const;\n\n// Cytoscape's attribute selector accepts a double-quoted string. Escape\n// embedded backslashes and double quotes so a malformed (or hostile)\n// registered key can't break out of the selector. The grammar\n// (https://js.cytoscape.org/#selectors/data) is more permissive than CSS, but\n// these two characters are the only ones that would change selector semantics.\nfunction escapeCytoscapeAttr(value: string): string {\n return value.replace(/[\\\\\"]/g, (c) => `\\\\${c}`);\n}\n","/**\n * Cytoscape-side wrapper around `@ship-it-ui/graph-tokens`. The token-reader\n * primitives (`resolveCssVar`, `toSrgb`, `readThemeTokens`,\n * `resolveColorReference`, `ThemeTokenPalette`) live in the shared package so\n * `@ship-it-ui/graph-editor` (React Flow) can consume them without dragging\n * Cytoscape in. Only `resolveEntityColor` lives here because it bridges to the\n * `@ship-it-ui/shipit` entity-type registry.\n */\n\nimport { resolveColorReference, type ThemeTokenPalette } from '@ship-it-ui/graph-tokens';\nimport { getEntityTypeMeta, type EntityType } from '@ship-it-ui/shipit';\n\nexport {\n readThemeTokens,\n resolveColorReference,\n resolveCssVar,\n toSrgb,\n type ThemeTokenPalette,\n} from '@ship-it-ui/graph-tokens';\n\n/**\n * Resolve the concrete color for a registered entity type. Reads the type's\n * `colorVar` (a `var(--color-…)` string) and looks the value up in the\n * palette. Falls back to the palette's `accent` color when the var is\n * malformed or unknown.\n */\nexport function resolveEntityColor(type: EntityType, palette: ThemeTokenPalette): string {\n const meta = getEntityTypeMeta(type);\n return resolveColorReference(meta.colorVar, palette);\n}\n","'use client';\n\nimport type cytoscape from 'cytoscape';\nimport { useCallback, useEffect, useMemo } from 'react';\n\nimport { buildShipItStylesheet, type BuildStylesheetOptions } from './stylesheet';\n\n/**\n * useShipItStylesheet — keeps a Cytoscape instance's stylesheet in sync with\n * the live design-token palette. Re-applies the stylesheet whenever\n * `<html data-theme>` flips, so toggling between dark and light propagates to\n * the graph without remounting.\n *\n * Returns a `refresh()` callback the consumer can invoke after any other\n * theme-affecting change (e.g., a `--color-accent` hue knob update).\n *\n * ```ts\n * const cyRef = useRef<cytoscape.Core | null>(null);\n * const { refresh } = useShipItStylesheet(cyRef);\n * ```\n */\n\nexport interface UseShipItStylesheetOptions extends BuildStylesheetOptions {\n /** Skip the MutationObserver wiring (e.g., when the host owns its own observer). */\n observe?: boolean;\n}\n\nexport interface UseShipItStylesheetReturn {\n /** Re-read tokens and re-apply the stylesheet. */\n refresh: () => void;\n}\n\nexport function useShipItStylesheet(\n cyRef: { current: cytoscape.Core | null },\n options: UseShipItStylesheetOptions = {},\n): UseShipItStylesheetReturn {\n const { observe = true, palette, extra } = options;\n // Memoize the build options against their flat constituents so callers that\n // pass `options` inline (a fresh object each render) don't churn `apply` /\n // disconnect+reconnect the MutationObserver on every render.\n const buildOptions = useMemo<BuildStylesheetOptions>(\n () => ({ palette, extra }),\n [palette, extra],\n );\n\n const apply = useCallback(() => {\n const cy = cyRef.current;\n if (!cy) return;\n cy.style(buildShipItStylesheet(buildOptions)).update();\n }, [cyRef, buildOptions]);\n\n useEffect(() => {\n apply();\n if (!observe || typeof document === 'undefined') return undefined;\n const observer = new MutationObserver(apply);\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: ['data-theme'],\n });\n return () => observer.disconnect();\n }, [apply, observe]);\n\n return { refresh: apply };\n}\n","'use client';\n\nimport type cytoscape from 'cytoscape';\nimport {\n forwardRef,\n useEffect,\n useImperativeHandle,\n useRef,\n type HTMLAttributes,\n type ReactNode,\n} from 'react';\n\nimport { buildShipItStylesheet, type BuildStylesheetOptions } from './stylesheet';\nimport { useShipItStylesheet } from './useShipItStylesheet';\n\n/**\n * GraphCanvas — high-level wrapper around a Cytoscape instance. Owns the\n * `data-theme` ↔ stylesheet sync (via {@link useShipItStylesheet}), the\n * cytoscape lifecycle (create / destroy on mount / unmount), and a thin\n * selection API on top of Cytoscape's events.\n *\n * The component never bundles Cytoscape itself — pass the engine factory in\n * via the `engine` prop so the consumer controls the Cytoscape version and\n * any registered extensions:\n *\n * ```tsx\n * import cytoscape from 'cytoscape';\n *\n * <GraphCanvas\n * engine={cytoscape}\n * elements={[...]}\n * layout={{ name: 'cose' }}\n * onSelect={(node) => setSelected(node.id())}\n * inspector={selected && <GraphInspector …/>}\n * />\n * ```\n */\n\nexport type CytoscapeEngine = (options: cytoscape.CytoscapeOptions) => cytoscape.Core;\n\nexport interface GraphCanvasHandle {\n /** Live Cytoscape instance. `null` until mount. */\n cy: cytoscape.Core | null;\n /** Re-read tokens and re-apply the stylesheet. */\n refreshStyles: () => void;\n}\n\nexport interface GraphCanvasProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n 'onSelect' | 'children'\n> {\n /** Cytoscape factory. Pass the imported `cytoscape` default export. */\n engine: CytoscapeEngine;\n /** Graph elements (nodes + edges). Passed straight through to Cytoscape. */\n elements: cytoscape.ElementDefinition[];\n /** Layout config. Defaults to a static `preset` layout (no auto-layout). */\n layout?: cytoscape.LayoutOptions;\n /** Fires when a node is tapped/selected. */\n onSelect?: (node: cytoscape.NodeSingular) => void;\n /** Fires when the selection is cleared (background tap). */\n onClearSelection?: () => void;\n /** Fires when the pointer enters a node. */\n onNodeHover?: (node: cytoscape.NodeSingular) => void;\n /** Fires when the pointer leaves a node. */\n onNodeLeave?: () => void;\n /** Slot rendered over the graph (e.g., a `<GraphInspector>`). Positioned top-right. */\n inspector?: ReactNode;\n /** Overrides for the stylesheet builder. */\n styleOptions?: BuildStylesheetOptions;\n /** Accessible label for the container. */\n 'aria-label'?: string;\n}\n\nconst DEFAULT_LAYOUT: cytoscape.LayoutOptions = { name: 'preset' };\n\nexport const GraphCanvas = forwardRef<GraphCanvasHandle, GraphCanvasProps>(function GraphCanvas(\n {\n engine,\n elements,\n layout = DEFAULT_LAYOUT,\n onSelect,\n onClearSelection,\n onNodeHover,\n onNodeLeave,\n inspector,\n styleOptions,\n className,\n 'aria-label': ariaLabel = 'Graph canvas',\n ...props\n },\n forwardedRef,\n) {\n const containerRef = useRef<HTMLDivElement | null>(null);\n const cyRef = useRef<cytoscape.Core | null>(null);\n\n // Create the cytoscape instance once on mount.\n useEffect(() => {\n if (!containerRef.current) return undefined;\n const cy = engine({\n container: containerRef.current,\n elements,\n layout,\n style: buildShipItStylesheet(styleOptions),\n });\n cyRef.current = cy;\n return () => {\n cy.destroy();\n cyRef.current = null;\n };\n // We intentionally re-create on engine swap or container remount only.\n // Elements / layout / style updates are handled in the effects below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [engine]);\n\n // Keep elements in sync.\n useEffect(() => {\n const cy = cyRef.current;\n if (!cy) return;\n cy.json({ elements });\n }, [elements]);\n\n // Re-run layout when the layout config changes.\n useEffect(() => {\n const cy = cyRef.current;\n if (!cy) return;\n cy.layout(layout).run();\n }, [layout]);\n\n const { refresh: refreshStyles } = useShipItStylesheet(cyRef, styleOptions);\n\n // Wire selection events. `engine` is in the deps so the listeners re-bind\n // whenever the upstream effect destroys + recreates the Cytoscape instance —\n // without it, an `engine` swap would leave the new `cy` without handlers.\n useEffect(() => {\n const cy = cyRef.current;\n if (!cy) return undefined;\n const handleSelect = (event: cytoscape.EventObject) => {\n if (event.target?.isNode?.()) {\n onSelect?.(event.target as cytoscape.NodeSingular);\n }\n };\n const handleBackgroundTap = (event: cytoscape.EventObject) => {\n if (event.target === cy) onClearSelection?.();\n };\n const handleEnter = (event: cytoscape.EventObject) => {\n onNodeHover?.(event.target as cytoscape.NodeSingular);\n };\n const handleLeave = () => onNodeLeave?.();\n cy.on('tap', 'node', handleSelect);\n cy.on('tap', handleBackgroundTap);\n cy.on('mouseover', 'node', handleEnter);\n cy.on('mouseout', 'node', handleLeave);\n return () => {\n cy.off('tap', 'node', handleSelect);\n cy.off('tap', handleBackgroundTap);\n cy.off('mouseover', 'node', handleEnter);\n cy.off('mouseout', 'node', handleLeave);\n };\n }, [engine, onSelect, onClearSelection, onNodeHover, onNodeLeave]);\n\n // Expose the imperative handle via getters so consumers always read the live\n // `cyRef.current`. Snapshotting `cyRef.current` here would freeze it at\n // `null` because the instance is assigned inside an effect that runs *after*\n // the initial render — Copilot/Claude both flagged this.\n useImperativeHandle(\n forwardedRef,\n (): GraphCanvasHandle => ({\n get cy() {\n return cyRef.current;\n },\n refreshStyles,\n }),\n [refreshStyles],\n );\n\n return (\n <div\n role=\"region\"\n aria-label={ariaLabel}\n className={['relative h-full w-full', className].filter(Boolean).join(' ')}\n {...props}\n >\n {/*\n * Inline styles, not Tailwind utilities. Cytoscape injects an unlayered\n * `.__________cytoscape_container { position: relative }` stylesheet at\n * init time, and Tailwind v4 emits `.absolute`/`.inset-0` into\n * `@layer utilities` — unlayered rules outrank layered ones regardless\n * of source order, so the canvas would collapse to 0×0 in a static-\n * height parent. Inline styles win against both.\n */}\n <div ref={containerRef} style={{ position: 'absolute', inset: 0 }} />\n {inspector && <div className=\"absolute top-4 right-4 z-10\">{inspector}</div>}\n </div>\n );\n});\n\nGraphCanvas.displayName = 'GraphCanvas';\n"],"mappings":"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","names":["resolveColorReference","resolveColorReference","useEffect","GraphCanvas","useEffect"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ship-it-ui/cytoscape",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
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/",
|
|
@@ -45,33 +45,33 @@
|
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"cytoscape": "^3.28.0",
|
|
48
|
-
"react": "^
|
|
49
|
-
"react-dom": "^
|
|
50
|
-
"@ship-it-ui/icons": "0.0.
|
|
51
|
-
"@ship-it-ui/shipit": "0.0.
|
|
52
|
-
"@ship-it-ui/ui": "0.0.
|
|
48
|
+
"react": "^19.0.0",
|
|
49
|
+
"react-dom": "^19.0.0",
|
|
50
|
+
"@ship-it-ui/icons": "0.0.10",
|
|
51
|
+
"@ship-it-ui/shipit": "0.0.11",
|
|
52
|
+
"@ship-it-ui/ui": "0.0.10"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@testing-library/jest-dom": "^6.6.3",
|
|
56
56
|
"@testing-library/react": "^16.0.1",
|
|
57
|
-
"@types/react": "^
|
|
58
|
-
"@types/react-dom": "^
|
|
57
|
+
"@types/react": "^19.2.0",
|
|
58
|
+
"@types/react-dom": "^19.2.0",
|
|
59
59
|
"axe-core": "^4.10.2",
|
|
60
|
-
"cytoscape": "^3.
|
|
60
|
+
"cytoscape": "^3.33.4",
|
|
61
61
|
"esbuild-plugin-preserve-directives": "^0.0.11",
|
|
62
62
|
"jsdom": "^29.1.1",
|
|
63
|
-
"react": "^
|
|
64
|
-
"react-dom": "^
|
|
63
|
+
"react": "^19.2.0",
|
|
64
|
+
"react-dom": "^19.2.0",
|
|
65
65
|
"tsup": "^8.3.0",
|
|
66
66
|
"typescript": "^5.6.3",
|
|
67
67
|
"vitest": "^2.1.3",
|
|
68
68
|
"vitest-axe": "^0.1.0",
|
|
69
69
|
"@ship-it-ui/eslint-config": "0.0.1",
|
|
70
|
-
"@ship-it-ui/icons": "0.0.
|
|
71
|
-
"@ship-it-ui/shipit": "0.0.
|
|
72
|
-
"@ship-it-ui/tokens": "0.0.
|
|
70
|
+
"@ship-it-ui/icons": "0.0.10",
|
|
71
|
+
"@ship-it-ui/shipit": "0.0.11",
|
|
72
|
+
"@ship-it-ui/tokens": "0.0.7",
|
|
73
73
|
"@ship-it-ui/tsconfig": "0.0.1",
|
|
74
|
-
"@ship-it-ui/ui": "0.0.
|
|
74
|
+
"@ship-it-ui/ui": "0.0.10"
|
|
75
75
|
},
|
|
76
76
|
"scripts": {
|
|
77
77
|
"build": "tsup",
|