From d369f0872958e586f0f676a46a6bc751370957ee Mon Sep 17 00:00:00 2001 From: Yash Bavadiya Date: Sun, 18 Oct 2026 11:40:45 +0530 Subject: [PATCH] fix(canvas): draw the kind icons from lucide's public exports The kind icons were read from lucide-react's per-icon modules through their internal __iconNode export. Newer lucide-react builds replaced it with an __iconData object, so every icon came back undefined and the canvas threw on first paint, which is why the lucide-react bumps keep failing. The framework-free lucide package exports the same [tag, attrs] lists by name, so the icons now come from there. It is pinned to the lucide-react version so both draw the same icon set. --- bun.lock | 3 + mcp/tsconfig.json | 8 +-- package.json | 1 + src/components/Canvas.tsx | 7 +-- src/components/Palette.tsx | 7 +-- src/components/lucide-icons.d.ts | 19 ------ src/components/nodeVisuals.ts | 104 ++++++++++++++++--------------- 7 files changed, 66 insertions(+), 83 deletions(-) delete mode 100644 src/components/lucide-icons.d.ts diff --git a/bun.lock b/bun.lock index 212d6cd..0b1f1d1 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "dependencies": { "@vercel/analytics": "^2.0.1", "@vercel/speed-insights": "^2.0.0", + "lucide": "^1.41.0", "lucide-react": "^1.41.0", "react": "^19.3.0", "react-dom": "^19.3.0", @@ -273,6 +274,8 @@ "lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], + "lucide": ["lucide@1.41.0", "", {}, "sha512-fIXP7JzC1vehdqOrMhwpbcXzX6CvUMuoDf16HY1FmC4sDBeuwrH2A0c6QmLn3wnSvaMchpEyM+JWijyTvOs3kw=="], + "lucide-react": ["lucide-react@1.41.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-6lksP35l6KszDKUeRTi4LV7i6DEe0Yzl2ALJm9j4c5xEYN91GdW1xGsawGMOg2mgjF5GHBVX8pKX9kP+cWsP3Q=="], "magic-string": ["magic-string@1.2.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g=="], diff --git a/mcp/tsconfig.json b/mcp/tsconfig.json index b111432..a882d31 100644 --- a/mcp/tsconfig.json +++ b/mcp/tsconfig.json @@ -17,11 +17,5 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, - "include": [ - "src", - "scripts", - "widget", - "vite.config.ts", - "../src/components/lucide-icons.d.ts" - ] + "include": ["src", "scripts", "widget", "vite.config.ts"] } diff --git a/package.json b/package.json index a8237c5..844352a 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "dependencies": { "@vercel/analytics": "^2.0.1", "@vercel/speed-insights": "^2.0.0", + "lucide": "^1.41.0", "lucide-react": "^1.41.0", "react": "^19.3.0", "react-dom": "^19.3.0" diff --git a/src/components/Canvas.tsx b/src/components/Canvas.tsx index fc2e231..08e741d 100644 --- a/src/components/Canvas.tsx +++ b/src/components/Canvas.tsx @@ -1576,10 +1576,9 @@ const Glyph = memo(function Glyph({ kind }: { kind: NodeKind }) { strokeLinecap="round" strokeLinejoin="round" > - {KIND_ICON[kind].map(([tag, attrs]) => { - const { key, ...rest } = attrs; - return createElement(tag, { key, ...rest }); - })} + {KIND_ICON[kind].map(([tag, attrs], i) => + createElement(tag, { key: i, ...attrs }), + )} ); }); diff --git a/src/components/Palette.tsx b/src/components/Palette.tsx index 34df78d..6bc9822 100644 --- a/src/components/Palette.tsx +++ b/src/components/Palette.tsx @@ -99,10 +99,9 @@ function Glyph({ kind }: { kind: NodeKind }) { role="presentation" aria-hidden="true" > - {KIND_ICON[kind].map(([tag, attrs]) => { - const { key, ...rest } = attrs; - return createElement(tag, { key, ...rest }); - })} + {KIND_ICON[kind].map(([tag, attrs], i) => + createElement(tag, { key: i, ...attrs }), + )} ); } diff --git a/src/components/lucide-icons.d.ts b/src/components/lucide-icons.d.ts deleted file mode 100644 index ba5b1fc..0000000 --- a/src/components/lucide-icons.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Typings for deep imports of Lucide's per-icon modules. - * - * Each module under lucide-react/dist/esm/icons/ exports two things: the - * React component (default) and `__iconNode`, the raw list of SVG primitives - * the component is built from. The canvas draws nodes inside one big , - * where a React component that renders its own root is the wrong tool; - * the primitive list is exactly the right one, and importing it directly - * also lets the bundler skip the component wrapper for icons only the - * canvas uses. The package ships no typings for these deep paths, so this - * wildcard declaration provides them. - */ -declare module 'lucide-react/dist/esm/icons/*' { - /** - * The icon's SVG primitives as [tagName, attributes] pairs, drawn in a - * 24x24 viewBox with stroke-width 2 expected from the container. - */ - export const __iconNode: [elementName: string, attrs: Record][]; -} diff --git a/src/components/nodeVisuals.ts b/src/components/nodeVisuals.ts index db920a4..41e0851 100644 --- a/src/components/nodeVisuals.ts +++ b/src/components/nodeVisuals.ts @@ -1,3 +1,4 @@ +import type { IconNode } from 'lucide'; import type { NodeKind } from '../sim/types'; /* ------------------------------------------------------------------ * @@ -7,16 +8,19 @@ import type { NodeKind } from '../sim/types'; * stroke weight, which is what makes thirty-three of them read as a family * rather than as thirty-three separately-sourced pictures. * - * WHY `__iconNode` AND NOT THE COMPONENTS. Each lucide-react icon module - * exports the React component (default) and `__iconNode`, the list of SVG - * primitives ([tag, attrs] pairs) the component is built from. The canvas - * draws every node inside ONE parent ; a component that renders its - * own root would have to be nested as an inner viewport, while the - * primitive list can be painted straight into a positioned exactly the - * way the old hand-drawn paths were. So this module re-exports the - * primitives, and each consumer owns one tiny renderer. The deep imports - * are typed by lucide-icons.d.ts and are exactly as tree-shakeable as the - * barrel: only the thirty-three modules named here are bundled. + * WHY `lucide` AND NOT THE lucide-react COMPONENTS. The framework-free + * `lucide` package exports each icon as its list of SVG primitives + * ([tag, attrs] pairs), the same drawing the React components are built + * from. The canvas draws every node inside ONE parent ; a component + * that renders its own root would have to be nested as an inner + * viewport, while the primitive list can be painted straight into a + * positioned exactly the way the old hand-drawn paths were. So this + * module re-exports the primitives, and each consumer owns one tiny + * renderer. These are the package's public named exports, so a version + * bump cannot quietly change their shape the way lucide-react's internal + * per-icon modules did, and `lucide` is side-effect free, so only the + * thirty-three icons named here are bundled. Keep its version in step with + * lucide-react so the two packages draw the same icon set. * * CHOOSING RULES, in priority order: * BEHAVIOUR the icon depicts what the component DOES to traffic, not @@ -31,44 +35,46 @@ import type { NodeKind } from '../sim/types'; * outlines (globe, clock) stay unique in the set. * ------------------------------------------------------------------ */ -/* Traffic */ -import { __iconNode as icClient } from 'lucide-react/dist/esm/icons/monitor-smartphone.mjs'; -import { __iconNode as icLb } from 'lucide-react/dist/esm/icons/split.mjs'; -import { __iconNode as icCdn } from 'lucide-react/dist/esm/icons/cloud-download.mjs'; -import { __iconNode as icRegion } from 'lucide-react/dist/esm/icons/globe.mjs'; -/* Compute */ -import { __iconNode as icService } from 'lucide-react/dist/esm/icons/server.mjs'; -import { __iconNode as icWorker } from 'lucide-react/dist/esm/icons/cog.mjs'; -import { __iconNode as icLambda } from 'lucide-react/dist/esm/icons/square-function.mjs'; -import { __iconNode as icEdgecompute } from 'lucide-react/dist/esm/icons/cloud-lightning.mjs'; -import { __iconNode as icTranscoder } from 'lucide-react/dist/esm/icons/film.mjs'; -import { __iconNode as icCron } from 'lucide-react/dist/esm/icons/calendar-clock.mjs'; -/* Data */ -import { __iconNode as icDb } from 'lucide-react/dist/esm/icons/database.mjs'; -import { __iconNode as icCache } from 'lucide-react/dist/esm/icons/database-zap.mjs'; -import { __iconNode as icReplica } from 'lucide-react/dist/esm/icons/database-backup.mjs'; -import { __iconNode as icShard } from 'lucide-react/dist/esm/icons/columns-3.mjs'; -import { __iconNode as icObjectstore } from 'lucide-react/dist/esm/icons/package.mjs'; -import { __iconNode as icSearchindex } from 'lucide-react/dist/esm/icons/text-search.mjs'; -import { __iconNode as icTimeseriesdb } from 'lucide-react/dist/esm/icons/chart-line.mjs'; -import { __iconNode as icGraphdb } from 'lucide-react/dist/esm/icons/waypoints.mjs'; -import { __iconNode as icColdstorage } from 'lucide-react/dist/esm/icons/snowflake.mjs'; -import { __iconNode as icVectordb } from 'lucide-react/dist/esm/icons/chart-scatter.mjs'; -import { __iconNode as icWritebehind } from 'lucide-react/dist/esm/icons/hard-drive-download.mjs'; -/* Messaging */ -import { __iconNode as icQueue } from 'lucide-react/dist/esm/icons/rows-3.mjs'; -import { __iconNode as icStreambroker } from 'lucide-react/dist/esm/icons/logs.mjs'; -import { __iconNode as icPubsub } from 'lucide-react/dist/esm/icons/share-2.mjs'; -import { __iconNode as icWebsocket } from 'lucide-react/dist/esm/icons/cable.mjs'; -import { __iconNode as icRetryqueue } from 'lucide-react/dist/esm/icons/list-restart.mjs'; -/* Control */ -import { __iconNode as icAutoscaler } from 'lucide-react/dist/esm/icons/scaling.mjs'; -import { __iconNode as icRatelimiter } from 'lucide-react/dist/esm/icons/funnel.mjs'; -import { __iconNode as icBreaker } from 'lucide-react/dist/esm/icons/unplug.mjs'; -import { __iconNode as icApigateway } from 'lucide-react/dist/esm/icons/door-open.mjs'; -import { __iconNode as icSidecar } from 'lucide-react/dist/esm/icons/blocks.mjs'; -import { __iconNode as icBulkhead } from 'lucide-react/dist/esm/icons/square-split-horizontal.mjs'; -import { __iconNode as icLoadshedder } from 'lucide-react/dist/esm/icons/funnel-x.mjs'; +import { + /* Traffic */ + MonitorSmartphone as icClient, + Split as icLb, + CloudDownload as icCdn, + Globe as icRegion, + /* Compute */ + Server as icService, + Cog as icWorker, + SquareFunction as icLambda, + CloudLightning as icEdgecompute, + Film as icTranscoder, + CalendarClock as icCron, + /* Data */ + Database as icDb, + DatabaseZap as icCache, + DatabaseBackup as icReplica, + Columns3 as icShard, + Package as icObjectstore, + TextSearch as icSearchindex, + ChartLine as icTimeseriesdb, + Waypoints as icGraphdb, + Snowflake as icColdstorage, + ChartScatter as icVectordb, + HardDriveDownload as icWritebehind, + /* Messaging */ + Rows3 as icQueue, + Logs as icStreambroker, + Share2 as icPubsub, + Cable as icWebsocket, + ListRestart as icRetryqueue, + /* Control */ + Scaling as icAutoscaler, + Funnel as icRatelimiter, + Unplug as icBreaker, + DoorOpen as icApigateway, + Blocks as icSidecar, + SquareSplitHorizontal as icBulkhead, + FunnelX as icLoadshedder, +} from 'lucide'; /** * Shared, non-component values used by both the canvas and the palette. @@ -87,7 +93,7 @@ export const NODE_DND_MIME = 'application/x-breakscale-node'; * primitives (chart-scatter's points) carry their own fill="currentColor", * which the renderer must pass through untouched. */ -export type IconNode = [elementName: string, attrs: Record][]; +export type { IconNode }; /** * The kind -> icon table. The reasoning for each pick is inline, because a