diff --git a/package-lock.json b/package-lock.json index 462b3da6..58f333eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@codemirror/theme-one-dark": "^6.0.0", "@xyflow/svelte": "^1.5.0", "codemirror": "^6.0.0", + "driver.js": "^1.4.0", "jspdf": "^4.1.0", "katex": "^0.16.0", "opentype.js": "^1.3.4", @@ -1272,7 +1273,6 @@ "integrity": "sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", @@ -1312,7 +1312,6 @@ "integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", @@ -1512,7 +1511,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2055,7 +2053,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -2175,8 +2172,7 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1566079.tgz", "integrity": "sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==", "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/dompurify": { "version": "3.3.1", @@ -2188,6 +2184,12 @@ "@types/trusted-types": "^2.0.7" } }, + "node_modules/driver.js": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/driver.js/-/driver.js-1.4.0.tgz", + "integrity": "sha512-Gm64jm6PmcU+si21sQhBrTAM1JvUrR0QhNmjkprNLxohOBzul9+pNHXgQaT9lW84gwg9GMLB3NZGuGolsz5uew==", + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -2298,7 +2300,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2968,7 +2969,6 @@ "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.1.0.tgz", "integrity": "sha512-xd1d/XRkwqnsq6FP3zH1Q+Ejqn2ULIJeDZ+FTKpaabVpZREjsJKRJwuokTNgdqOU+fl55KgbvgZ1pRTSWCP2kQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "fast-png": "^6.2.0", @@ -3363,7 +3363,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3397,7 +3396,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3521,7 +3519,6 @@ "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -3970,7 +3967,6 @@ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.46.0.tgz", "integrity": "sha512-ZhLtvroYxUxr+HQJfMZEDRsGsmU46x12RvAv/zi9584f5KOX7bUrEbhPJ7cKFmUvZTJXi/CFZUYwDC6M1FigPw==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -4239,7 +4235,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4288,7 +4283,6 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", diff --git a/package.json b/package.json index 2e96f67e..e197eea7 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@codemirror/theme-one-dark": "^6.0.0", "@xyflow/svelte": "^1.5.0", "codemirror": "^6.0.0", + "driver.js": "^1.4.0", "jspdf": "^4.1.0", "katex": "^0.16.0", "opentype.js": "^1.3.4", diff --git a/src/app.css b/src/app.css index ae20dca8..e570d513 100644 --- a/src/app.css +++ b/src/app.css @@ -717,3 +717,4 @@ body.assembly-pending .svelte-flow__edge { transform: scale(1); } } + diff --git a/src/lib/components/FlowCanvas.svelte b/src/lib/components/FlowCanvas.svelte index 911f3991..55b0a35b 100644 --- a/src/lib/components/FlowCanvas.svelte +++ b/src/lib/components/FlowCanvas.svelte @@ -14,6 +14,7 @@ import '@xyflow/svelte/dist/style.css'; import { isInputFocused } from '$lib/utils/focus'; + import { isTourActive } from '$lib/tours/inputMode'; import BaseNode from './nodes/BaseNode.svelte'; import EventNode from './nodes/EventNode.svelte'; import AnnotationNode from './nodes/AnnotationNode.svelte'; @@ -31,6 +32,7 @@ import { dropTargetBridge } from '$lib/stores/dropTargetBridge'; import { contextMenuStore } from '$lib/stores/contextMenu'; import { nodeUpdatesStore } from '$lib/stores/nodeUpdates'; + import { assemblyAnimationTrigger } from '$lib/animation/assemblyAnimation'; import { nodeRegistry } from '$lib/nodes'; import { NODE_TYPES } from '$lib/constants/nodeTypes'; import { GRID_SIZE, SNAP_GRID, BACKGROUND_GAP } from '$lib/constants/grid'; @@ -74,6 +76,8 @@ // Keyboard shortcuts for node manipulation function handleKeydown(event: KeyboardEvent) { + // While a guided tour runs, driver.js owns the keyboard. + if (isTourActive()) return; if (isInputFocused(event)) return; // Handle Delete key (SvelteFlow's deleteKeyCode doesn't work reliably for 'Delete') @@ -444,6 +448,19 @@ } }); + // On model load, the initial routing pass runs with fallback 80×40 + // dimensions because nodes haven't been measured yet, and the per-node + // $effect above silently records first measurements without rerouting. + // Trigger a full reroute once measurements have settled. + let lastAssemblyTrigger = 0; + const unsubAssembly = assemblyAnimationTrigger.subscribe((v) => { + if (v > lastAssemblyTrigger) { + lastAssemblyTrigger = v; + setTimeout(() => updateRoutingContext(), 400); + } + }); + onDestroy(() => unsubAssembly()); + // Track if we're currently syncing to prevent loops let isSyncing = false; diff --git a/src/lib/components/ResizablePanel.svelte b/src/lib/components/ResizablePanel.svelte index cd56bf33..0f428449 100644 --- a/src/lib/components/ResizablePanel.svelte +++ b/src/lib/components/ResizablePanel.svelte @@ -179,6 +179,8 @@