opendevbrowser 0.0.16 → 0.0.18
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/README.md +201 -79
- package/dist/annotate/agent-inbox-store.d.ts +58 -0
- package/dist/annotate/agent-inbox-store.d.ts.map +1 -0
- package/dist/annotate/agent-inbox.d.ts +25 -0
- package/dist/annotate/agent-inbox.d.ts.map +1 -0
- package/dist/annotate/direct-annotator.d.ts.map +1 -1
- package/dist/annotate/timeout-messages.d.ts +4 -0
- package/dist/annotate/timeout-messages.d.ts.map +1 -0
- package/dist/automation/coordinator.d.ts +55 -0
- package/dist/automation/coordinator.d.ts.map +1 -0
- package/dist/browser/annotation-manager.d.ts +7 -1
- package/dist/browser/annotation-manager.d.ts.map +1 -1
- package/dist/browser/browser-manager.d.ts +153 -48
- package/dist/browser/browser-manager.d.ts.map +1 -1
- package/dist/browser/canvas-client.d.ts +54 -0
- package/dist/browser/canvas-client.d.ts.map +1 -0
- package/dist/browser/canvas-code-sync-manager.d.ts +87 -0
- package/dist/browser/canvas-code-sync-manager.d.ts.map +1 -0
- package/dist/browser/canvas-manager.d.ts +122 -0
- package/dist/browser/canvas-manager.d.ts.map +1 -0
- package/dist/browser/canvas-runtime-preview-bridge.d.ts +20 -0
- package/dist/browser/canvas-runtime-preview-bridge.d.ts.map +1 -0
- package/dist/browser/canvas-session-sync-manager.d.ts +21 -0
- package/dist/browser/canvas-session-sync-manager.d.ts.map +1 -0
- package/dist/browser/global-challenge-coordinator.d.ts +27 -0
- package/dist/browser/global-challenge-coordinator.d.ts.map +1 -0
- package/dist/browser/manager-types.d.ts +179 -1
- package/dist/browser/manager-types.d.ts.map +1 -1
- package/dist/browser/ops-browser-manager.d.ts +114 -4
- package/dist/browser/ops-browser-manager.d.ts.map +1 -1
- package/dist/browser/ops-client.d.ts +17 -1
- package/dist/browser/ops-client.d.ts.map +1 -1
- package/dist/browser/playwright-runtime.d.ts +4 -0
- package/dist/browser/playwright-runtime.d.ts.map +1 -0
- package/dist/browser/review-surface.d.ts +9 -0
- package/dist/browser/review-surface.d.ts.map +1 -0
- package/dist/browser/screencast-recorder.d.ts +57 -0
- package/dist/browser/screencast-recorder.d.ts.map +1 -0
- package/dist/browser/session-inspector.d.ts +71 -0
- package/dist/browser/session-inspector.d.ts.map +1 -0
- package/dist/browser/session-store.d.ts +5 -1
- package/dist/browser/session-store.d.ts.map +1 -1
- package/dist/browser/system-chrome-cookies.d.ts +46 -0
- package/dist/browser/system-chrome-cookies.d.ts.map +1 -0
- package/dist/browser/target-manager.d.ts +1 -0
- package/dist/browser/target-manager.d.ts.map +1 -1
- package/dist/cache/chrome-locator.d.ts.map +1 -1
- package/dist/cache/chrome-user-data.d.ts +17 -0
- package/dist/cache/chrome-user-data.d.ts.map +1 -0
- package/dist/canvas/adapter-plugins/loader.d.ts +13 -0
- package/dist/canvas/adapter-plugins/loader.d.ts.map +1 -0
- package/dist/canvas/adapter-plugins/manifest.d.ts +146 -0
- package/dist/canvas/adapter-plugins/manifest.d.ts.map +1 -0
- package/dist/canvas/adapter-plugins/types.d.ts +83 -0
- package/dist/canvas/adapter-plugins/types.d.ts.map +1 -0
- package/dist/canvas/adapter-plugins/validator.d.ts +10 -0
- package/dist/canvas/adapter-plugins/validator.d.ts.map +1 -0
- package/dist/canvas/code-sync/apply-tsx.d.ts +25 -0
- package/dist/canvas/code-sync/apply-tsx.d.ts.map +1 -0
- package/dist/canvas/code-sync/graph.d.ts +5 -0
- package/dist/canvas/code-sync/graph.d.ts.map +1 -0
- package/dist/canvas/code-sync/hash.d.ts +3 -0
- package/dist/canvas/code-sync/hash.d.ts.map +1 -0
- package/dist/canvas/code-sync/import.d.ts +19 -0
- package/dist/canvas/code-sync/import.d.ts.map +1 -0
- package/dist/canvas/code-sync/manifest.d.ts +6 -0
- package/dist/canvas/code-sync/manifest.d.ts.map +1 -0
- package/dist/canvas/code-sync/tsx-adapter.d.ts +8 -0
- package/dist/canvas/code-sync/tsx-adapter.d.ts.map +1 -0
- package/dist/canvas/code-sync/types.d.ts +244 -0
- package/dist/canvas/code-sync/types.d.ts.map +1 -0
- package/dist/canvas/code-sync/write.d.ts +9 -0
- package/dist/canvas/code-sync/write.d.ts.map +1 -0
- package/dist/canvas/document-store.d.ts +91 -0
- package/dist/canvas/document-store.d.ts.map +1 -0
- package/dist/canvas/export.d.ts +12 -0
- package/dist/canvas/export.d.ts.map +1 -0
- package/dist/canvas/framework-adapters/custom-elements-v1.d.ts +3 -0
- package/dist/canvas/framework-adapters/custom-elements-v1.d.ts.map +1 -0
- package/dist/canvas/framework-adapters/html-static-v1.d.ts +3 -0
- package/dist/canvas/framework-adapters/html-static-v1.d.ts.map +1 -0
- package/dist/canvas/framework-adapters/markup.d.ts +9 -0
- package/dist/canvas/framework-adapters/markup.d.ts.map +1 -0
- package/dist/canvas/framework-adapters/react-tsx-v2.d.ts +3 -0
- package/dist/canvas/framework-adapters/react-tsx-v2.d.ts.map +1 -0
- package/dist/canvas/framework-adapters/registry.d.ts +12 -0
- package/dist/canvas/framework-adapters/registry.d.ts.map +1 -0
- package/dist/canvas/framework-adapters/svelte-sfc-v1.d.ts +3 -0
- package/dist/canvas/framework-adapters/svelte-sfc-v1.d.ts.map +1 -0
- package/dist/canvas/framework-adapters/types.d.ts +57 -0
- package/dist/canvas/framework-adapters/types.d.ts.map +1 -0
- package/dist/canvas/framework-adapters/vue-sfc-v1.d.ts +3 -0
- package/dist/canvas/framework-adapters/vue-sfc-v1.d.ts.map +1 -0
- package/dist/canvas/kits/catalog.d.ts +5 -0
- package/dist/canvas/kits/catalog.d.ts.map +1 -0
- package/dist/canvas/library-adapters/react/index.d.ts +3 -0
- package/dist/canvas/library-adapters/react/index.d.ts.map +1 -0
- package/dist/canvas/library-adapters/registry.d.ts +11 -0
- package/dist/canvas/library-adapters/registry.d.ts.map +1 -0
- package/dist/canvas/library-adapters/types.d.ts +43 -0
- package/dist/canvas/library-adapters/types.d.ts.map +1 -0
- package/dist/canvas/repo-store.d.ts +12 -0
- package/dist/canvas/repo-store.d.ts.map +1 -0
- package/dist/canvas/starters/catalog.d.ts +34 -0
- package/dist/canvas/starters/catalog.d.ts.map +1 -0
- package/dist/canvas/surface-palette.d.ts +15 -0
- package/dist/canvas/surface-palette.d.ts.map +1 -0
- package/dist/canvas/token-references.d.ts +22 -0
- package/dist/canvas/token-references.d.ts.map +1 -0
- package/dist/canvas/types.d.ts +594 -0
- package/dist/canvas/types.d.ts.map +1 -0
- package/dist/canvas-runtime-preview-bridge-HBEHXM4T.js +7 -0
- package/dist/canvas-runtime-preview-bridge-HBEHXM4T.js.map +1 -0
- package/dist/challenges/action-loop.d.ts +13 -0
- package/dist/challenges/action-loop.d.ts.map +1 -0
- package/dist/challenges/capability-matrix.d.ts +3 -0
- package/dist/challenges/capability-matrix.d.ts.map +1 -0
- package/dist/challenges/evidence-bundle.d.ts +48 -0
- package/dist/challenges/evidence-bundle.d.ts.map +1 -0
- package/dist/challenges/governed-adapter-gateway.d.ts +4 -0
- package/dist/challenges/governed-adapter-gateway.d.ts.map +1 -0
- package/dist/challenges/human-yield-gate.d.ts +20 -0
- package/dist/challenges/human-yield-gate.d.ts.map +1 -0
- package/dist/challenges/index.d.ts +15 -0
- package/dist/challenges/index.d.ts.map +1 -0
- package/dist/challenges/interpreter.d.ts +3 -0
- package/dist/challenges/interpreter.d.ts.map +1 -0
- package/dist/challenges/optional-computer-use-bridge.d.ts +9 -0
- package/dist/challenges/optional-computer-use-bridge.d.ts.map +1 -0
- package/dist/challenges/orchestrator.d.ts +32 -0
- package/dist/challenges/orchestrator.d.ts.map +1 -0
- package/dist/challenges/outcome-recorder.d.ts +8 -0
- package/dist/challenges/outcome-recorder.d.ts.map +1 -0
- package/dist/challenges/owned-environment-lane.d.ts +3 -0
- package/dist/challenges/owned-environment-lane.d.ts.map +1 -0
- package/dist/challenges/policy-gate.d.ts +9 -0
- package/dist/challenges/policy-gate.d.ts.map +1 -0
- package/dist/challenges/sanctioned-identity-lane.d.ts +3 -0
- package/dist/challenges/sanctioned-identity-lane.d.ts.map +1 -0
- package/dist/challenges/service-adapter-lane.d.ts +3 -0
- package/dist/challenges/service-adapter-lane.d.ts.map +1 -0
- package/dist/challenges/strategy-selector.d.ts +10 -0
- package/dist/challenges/strategy-selector.d.ts.map +1 -0
- package/dist/challenges/types.d.ts +277 -0
- package/dist/challenges/types.d.ts.map +1 -0
- package/dist/challenges/verification-gate.d.ts +15 -0
- package/dist/challenges/verification-gate.d.ts.map +1 -0
- package/dist/chunk-5FZQJRBQ.js +15256 -0
- package/dist/chunk-5FZQJRBQ.js.map +1 -0
- package/dist/{chunk-7W3SPXIB.js → chunk-FUSXMW3G.js} +4 -1
- package/dist/chunk-L57D35TB.js +33513 -0
- package/dist/chunk-L57D35TB.js.map +1 -0
- package/dist/chunk-TBUCZX4A.js +34 -0
- package/dist/chunk-TBUCZX4A.js.map +1 -0
- package/dist/chunk-Y2KL55OG.js +59 -0
- package/dist/chunk-Y2KL55OG.js.map +1 -0
- package/dist/chunk-YBQECXZX.js +409 -0
- package/dist/chunk-YBQECXZX.js.map +1 -0
- package/dist/cli/args.d.ts +4 -4
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/commands/annotate.d.ts +11 -0
- package/dist/cli/commands/annotate.d.ts.map +1 -1
- package/dist/cli/commands/artifacts.d.ts.map +1 -1
- package/dist/cli/commands/canvas.d.ts +45 -0
- package/dist/cli/commands/canvas.d.ts.map +1 -0
- package/dist/cli/commands/daemon.d.ts +7 -0
- package/dist/cli/commands/daemon.d.ts.map +1 -1
- package/dist/cli/commands/desktop/accessibility-snapshot.d.ts +3 -0
- package/dist/cli/commands/desktop/accessibility-snapshot.d.ts.map +1 -0
- package/dist/cli/commands/desktop/active-window.d.ts +3 -0
- package/dist/cli/commands/desktop/active-window.d.ts.map +1 -0
- package/dist/cli/commands/desktop/capture-desktop.d.ts +3 -0
- package/dist/cli/commands/desktop/capture-desktop.d.ts.map +1 -0
- package/dist/cli/commands/desktop/capture-window.d.ts +3 -0
- package/dist/cli/commands/desktop/capture-window.d.ts.map +1 -0
- package/dist/cli/commands/desktop/shared.d.ts +19 -0
- package/dist/cli/commands/desktop/shared.d.ts.map +1 -0
- package/dist/cli/commands/desktop/status.d.ts +3 -0
- package/dist/cli/commands/desktop/status.d.ts.map +1 -0
- package/dist/cli/commands/desktop/windows.d.ts +3 -0
- package/dist/cli/commands/desktop/windows.d.ts.map +1 -0
- package/dist/cli/commands/devtools/dialog.d.ts +19 -0
- package/dist/cli/commands/devtools/dialog.d.ts.map +1 -0
- package/dist/cli/commands/devtools/perf.d.ts.map +1 -1
- package/dist/cli/commands/devtools/screencast-start.d.ts +20 -0
- package/dist/cli/commands/devtools/screencast-start.d.ts.map +1 -0
- package/dist/cli/commands/devtools/screencast-stop.d.ts +17 -0
- package/dist/cli/commands/devtools/screencast-stop.d.ts.map +1 -0
- package/dist/cli/commands/devtools/screenshot.d.ts +3 -0
- package/dist/cli/commands/devtools/screenshot.d.ts.map +1 -1
- package/dist/cli/commands/dom/attr.d.ts.map +1 -1
- package/dist/cli/commands/dom/checked.d.ts.map +1 -1
- package/dist/cli/commands/dom/enabled.d.ts.map +1 -1
- package/dist/cli/commands/dom/html.d.ts.map +1 -1
- package/dist/cli/commands/dom/text.d.ts.map +1 -1
- package/dist/cli/commands/dom/value.d.ts.map +1 -1
- package/dist/cli/commands/dom/visible.d.ts.map +1 -1
- package/dist/cli/commands/export/clone-component.d.ts +9 -0
- package/dist/cli/commands/export/clone-component.d.ts.map +1 -1
- package/dist/cli/commands/export/clone-page.d.ts +8 -0
- package/dist/cli/commands/export/clone-page.d.ts.map +1 -1
- package/dist/cli/commands/interact/check.d.ts.map +1 -1
- package/dist/cli/commands/interact/click.d.ts.map +1 -1
- package/dist/cli/commands/interact/hover.d.ts.map +1 -1
- package/dist/cli/commands/interact/pointer-down.d.ts +7 -0
- package/dist/cli/commands/interact/pointer-down.d.ts.map +1 -0
- package/dist/cli/commands/interact/pointer-drag.d.ts +7 -0
- package/dist/cli/commands/interact/pointer-drag.d.ts.map +1 -0
- package/dist/cli/commands/interact/pointer-move.d.ts +7 -0
- package/dist/cli/commands/interact/pointer-move.d.ts.map +1 -0
- package/dist/cli/commands/interact/pointer-shared.d.ts +6 -0
- package/dist/cli/commands/interact/pointer-shared.d.ts.map +1 -0
- package/dist/cli/commands/interact/pointer-up.d.ts +7 -0
- package/dist/cli/commands/interact/pointer-up.d.ts.map +1 -0
- package/dist/cli/commands/interact/press.d.ts.map +1 -1
- package/dist/cli/commands/interact/scroll-into-view.d.ts.map +1 -1
- package/dist/cli/commands/interact/scroll.d.ts.map +1 -1
- package/dist/cli/commands/interact/select.d.ts.map +1 -1
- package/dist/cli/commands/interact/type.d.ts.map +1 -1
- package/dist/cli/commands/interact/uncheck.d.ts.map +1 -1
- package/dist/cli/commands/interact/upload.d.ts +18 -0
- package/dist/cli/commands/interact/upload.d.ts.map +1 -0
- package/dist/cli/commands/macro-resolve.d.ts +2 -0
- package/dist/cli/commands/macro-resolve.d.ts.map +1 -1
- package/dist/cli/commands/native.d.ts +22 -8
- package/dist/cli/commands/native.d.ts.map +1 -1
- package/dist/cli/commands/nav/goto.d.ts.map +1 -1
- package/dist/cli/commands/nav/review.d.ts +7 -0
- package/dist/cli/commands/nav/review.d.ts.map +1 -0
- package/dist/cli/commands/nav/snapshot.d.ts.map +1 -1
- package/dist/cli/commands/nav/wait.d.ts.map +1 -1
- package/dist/cli/commands/pages/open.d.ts.map +1 -1
- package/dist/cli/commands/product-video.d.ts +2 -0
- package/dist/cli/commands/product-video.d.ts.map +1 -1
- package/dist/cli/commands/research.d.ts +3 -0
- package/dist/cli/commands/research.d.ts.map +1 -1
- package/dist/cli/commands/run.d.ts +14 -0
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/serve.d.ts +1 -21
- package/dist/cli/commands/serve.d.ts.map +1 -1
- package/dist/cli/commands/session/connect.d.ts.map +1 -1
- package/dist/cli/commands/session/disconnect.d.ts.map +1 -1
- package/dist/cli/commands/session/inspector.d.ts +21 -0
- package/dist/cli/commands/session/inspector.d.ts.map +1 -0
- package/dist/cli/commands/session/launch.d.ts.map +1 -1
- package/dist/cli/commands/shopping.d.ts +5 -0
- package/dist/cli/commands/shopping.d.ts.map +1 -1
- package/dist/cli/commands/status.d.ts +2 -9
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/targets/new.d.ts.map +1 -1
- package/dist/cli/daemon-autostart.d.ts +11 -0
- package/dist/cli/daemon-autostart.d.ts.map +1 -1
- package/dist/cli/daemon-client.d.ts +3 -0
- package/dist/cli/daemon-client.d.ts.map +1 -1
- package/dist/cli/daemon-commands.d.ts.map +1 -1
- package/dist/cli/daemon-state.d.ts +16 -0
- package/dist/cli/daemon-state.d.ts.map +1 -1
- package/dist/cli/daemon-status.d.ts +7 -2
- package/dist/cli/daemon-status.d.ts.map +1 -1
- package/dist/cli/daemon.d.ts +1 -0
- package/dist/cli/daemon.d.ts.map +1 -1
- package/dist/cli/help.d.ts +19 -3
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/index.js +2927 -932
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/install-autostart-output.d.ts +6 -0
- package/dist/cli/install-autostart-output.d.ts.map +1 -0
- package/dist/cli/install-autostart-reconciliation.d.ts +23 -0
- package/dist/cli/install-autostart-reconciliation.d.ts.map +1 -0
- package/dist/cli/installers/skills.d.ts +42 -6
- package/dist/cli/installers/skills.d.ts.map +1 -1
- package/dist/cli/output.d.ts +3 -0
- package/dist/cli/output.d.ts.map +1 -1
- package/dist/cli/remote-canvas-manager.d.ts +8 -0
- package/dist/cli/remote-canvas-manager.d.ts.map +1 -0
- package/dist/cli/remote-desktop-runtime.d.ts +15 -0
- package/dist/cli/remote-desktop-runtime.d.ts.map +1 -0
- package/dist/cli/remote-manager.d.ts +27 -3
- package/dist/cli/remote-manager.d.ts.map +1 -1
- package/dist/cli/remote-relay.d.ts +2 -0
- package/dist/cli/remote-relay.d.ts.map +1 -1
- package/dist/cli/transport-timeouts.d.ts +8 -0
- package/dist/cli/transport-timeouts.d.ts.map +1 -0
- package/dist/cli/utils/http.d.ts +9 -0
- package/dist/cli/utils/http.d.ts.map +1 -1
- package/dist/cli/utils/parse.d.ts +3 -0
- package/dist/cli/utils/parse.d.ts.map +1 -1
- package/dist/cli/utils/skills.d.ts +1 -2
- package/dist/cli/utils/skills.d.ts.map +1 -1
- package/dist/cli/utils/workflow-message.d.ts +2 -0
- package/dist/cli/utils/workflow-message.d.ts.map +1 -0
- package/dist/config.d.ts +47 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/core/bootstrap.d.ts.map +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/logging.d.ts +3 -1
- package/dist/core/logging.d.ts.map +1 -1
- package/dist/core/runtime-assemblies.d.ts +22 -0
- package/dist/core/runtime-assemblies.d.ts.map +1 -0
- package/dist/core/types.d.ts +17 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/desktop/audit.d.ts +37 -0
- package/dist/desktop/audit.d.ts.map +1 -0
- package/dist/desktop/errors.d.ts +7 -0
- package/dist/desktop/errors.d.ts.map +1 -0
- package/dist/desktop/index.d.ts +6 -0
- package/dist/desktop/index.d.ts.map +1 -0
- package/dist/desktop/runtime.d.ts +26 -0
- package/dist/desktop/runtime.d.ts.map +1 -0
- package/dist/desktop/types.d.ts +76 -0
- package/dist/desktop/types.d.ts.map +1 -0
- package/dist/extension-extractor.d.ts +6 -0
- package/dist/extension-extractor.d.ts.map +1 -1
- package/dist/fs-UMRKOBNN.js +7 -0
- package/dist/fs-UMRKOBNN.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1221 -460
- package/dist/index.js.map +1 -1
- package/dist/integrations/figma/assets.d.ts +13 -0
- package/dist/integrations/figma/assets.d.ts.map +1 -0
- package/dist/integrations/figma/auth.d.ts +3 -0
- package/dist/integrations/figma/auth.d.ts.map +1 -0
- package/dist/integrations/figma/client.d.ts +42 -0
- package/dist/integrations/figma/client.d.ts.map +1 -0
- package/dist/integrations/figma/mappers.d.ts +23 -0
- package/dist/integrations/figma/mappers.d.ts.map +1 -0
- package/dist/integrations/figma/normalize.d.ts +99 -0
- package/dist/integrations/figma/normalize.d.ts.map +1 -0
- package/dist/integrations/figma/url.d.ts +17 -0
- package/dist/integrations/figma/url.d.ts.map +1 -0
- package/dist/integrations/figma/variables.d.ts +21 -0
- package/dist/integrations/figma/variables.d.ts.map +1 -0
- package/dist/macros/execute-runtime.d.ts +19 -0
- package/dist/macros/execute-runtime.d.ts.map +1 -0
- package/dist/macros/execute.d.ts +3 -1
- package/dist/macros/execute.d.ts.map +1 -1
- package/dist/{macros-NUBRM44Y.js → macros-ND2M7LWU.js} +2 -2
- package/dist/opendevbrowser.d.ts.map +1 -1
- package/dist/opendevbrowser.js +1221 -460
- package/dist/opendevbrowser.js.map +1 -1
- package/dist/providers/blocker.d.ts.map +1 -1
- package/dist/providers/browser-fallback.d.ts +30 -0
- package/dist/providers/browser-fallback.d.ts.map +1 -0
- package/dist/providers/constraint.d.ts +45 -0
- package/dist/providers/constraint.d.ts.map +1 -0
- package/dist/providers/index.d.ts +11 -2
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/policy.d.ts.map +1 -1
- package/dist/providers/product-video-compiler.d.ts +92 -0
- package/dist/providers/product-video-compiler.d.ts.map +1 -0
- package/dist/providers/registry.d.ts +37 -1
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/renderer.d.ts.map +1 -1
- package/dist/providers/research-compiler.d.ts +64 -0
- package/dist/providers/research-compiler.d.ts.map +1 -0
- package/dist/providers/research-executor.d.ts +27 -0
- package/dist/providers/research-executor.d.ts.map +1 -0
- package/dist/providers/runtime-bundle.d.ts +26 -0
- package/dist/providers/runtime-bundle.d.ts.map +1 -0
- package/dist/providers/runtime-factory.d.ts +6 -1
- package/dist/providers/runtime-factory.d.ts.map +1 -1
- package/dist/providers/runtime-policy.d.ts +24 -0
- package/dist/providers/runtime-policy.d.ts.map +1 -0
- package/dist/providers/shared/anti-bot-policy.d.ts +3 -2
- package/dist/providers/shared/anti-bot-policy.d.ts.map +1 -1
- package/dist/providers/shopping/index.d.ts +11 -1
- package/dist/providers/shopping/index.d.ts.map +1 -1
- package/dist/providers/shopping-compiler.d.ts +51 -0
- package/dist/providers/shopping-compiler.d.ts.map +1 -0
- package/dist/providers/shopping-executor.d.ts +18 -0
- package/dist/providers/shopping-executor.d.ts.map +1 -0
- package/dist/providers/shopping-postprocess.d.ts +46 -0
- package/dist/providers/shopping-postprocess.d.ts.map +1 -0
- package/dist/providers/shopping-workflow.d.ts +33 -0
- package/dist/providers/shopping-workflow.d.ts.map +1 -0
- package/dist/providers/social/platform.d.ts +2 -1
- package/dist/providers/social/platform.d.ts.map +1 -1
- package/dist/providers/social/search-quality.d.ts +16 -0
- package/dist/providers/social/search-quality.d.ts.map +1 -0
- package/dist/providers/social/youtube-resolver.d.ts +2 -1
- package/dist/providers/social/youtube-resolver.d.ts.map +1 -1
- package/dist/providers/social/youtube.d.ts.map +1 -1
- package/dist/providers/types.d.ts +116 -4
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/web/crawl-worker.d.ts.map +1 -1
- package/dist/providers/web/extract.d.ts +16 -0
- package/dist/providers/web/extract.d.ts.map +1 -1
- package/dist/providers/web/index.d.ts.map +1 -1
- package/dist/providers/workflow-contracts.d.ts +53 -0
- package/dist/providers/workflow-contracts.d.ts.map +1 -0
- package/dist/providers/workflows.d.ts +30 -6
- package/dist/providers/workflows.d.ts.map +1 -1
- package/dist/providers-G36AM3Z2.js +121 -0
- package/dist/providers-G36AM3Z2.js.map +1 -0
- package/dist/public-surface/generated-manifest.d.ts +1168 -0
- package/dist/public-surface/generated-manifest.d.ts.map +1 -0
- package/dist/public-surface/source.d.ts +437 -0
- package/dist/public-surface/source.d.ts.map +1 -0
- package/dist/relay/protocol.d.ts +108 -4
- package/dist/relay/protocol.d.ts.map +1 -1
- package/dist/relay/relay-endpoints.d.ts +21 -0
- package/dist/relay/relay-endpoints.d.ts.map +1 -1
- package/dist/relay/relay-server.d.ts +32 -1
- package/dist/relay/relay-server.d.ts.map +1 -1
- package/dist/relay/relay-types.d.ts +3 -0
- package/dist/relay/relay-types.d.ts.map +1 -1
- package/dist/skills/bundled-skill-directories.d.ts +8 -0
- package/dist/skills/bundled-skill-directories.d.ts.map +1 -0
- package/dist/skills/skill-loader.d.ts +9 -1
- package/dist/skills/skill-loader.d.ts.map +1 -1
- package/dist/skills/skill-loader.js +7 -0
- package/dist/skills/skill-loader.js.map +1 -0
- package/dist/skills/skill-nudge.d.ts.map +1 -1
- package/dist/skills/types.d.ts +31 -0
- package/dist/skills/types.d.ts.map +1 -1
- package/dist/snapshot/ops-snapshot.d.ts +1 -1
- package/dist/snapshot/ops-snapshot.d.ts.map +1 -1
- package/dist/snapshot/refs.d.ts +6 -1
- package/dist/snapshot/refs.d.ts.map +1 -1
- package/dist/snapshot/snapshotter.d.ts.map +1 -1
- package/dist/tools/annotate.d.ts.map +1 -1
- package/dist/tools/canvas.d.ts +4 -0
- package/dist/tools/canvas.d.ts.map +1 -0
- package/dist/tools/check.d.ts.map +1 -1
- package/dist/tools/click.d.ts.map +1 -1
- package/dist/tools/clone_component.d.ts.map +1 -1
- package/dist/tools/clone_page.d.ts.map +1 -1
- package/dist/tools/connect.d.ts.map +1 -1
- package/dist/tools/deps.d.ts +6 -0
- package/dist/tools/deps.d.ts.map +1 -1
- package/dist/tools/desktop-shared.d.ts +6 -0
- package/dist/tools/desktop-shared.d.ts.map +1 -0
- package/dist/tools/desktop_accessibility_snapshot.d.ts +4 -0
- package/dist/tools/desktop_accessibility_snapshot.d.ts.map +1 -0
- package/dist/tools/desktop_active_window.d.ts +4 -0
- package/dist/tools/desktop_active_window.d.ts.map +1 -0
- package/dist/tools/desktop_capture_desktop.d.ts +4 -0
- package/dist/tools/desktop_capture_desktop.d.ts.map +1 -0
- package/dist/tools/desktop_capture_window.d.ts +4 -0
- package/dist/tools/desktop_capture_window.d.ts.map +1 -0
- package/dist/tools/desktop_status.d.ts +4 -0
- package/dist/tools/desktop_status.d.ts.map +1 -0
- package/dist/tools/desktop_windows.d.ts +4 -0
- package/dist/tools/desktop_windows.d.ts.map +1 -0
- package/dist/tools/dialog.d.ts +4 -0
- package/dist/tools/dialog.d.ts.map +1 -0
- package/dist/tools/dom_get_html.d.ts.map +1 -1
- package/dist/tools/dom_get_text.d.ts.map +1 -1
- package/dist/tools/get_attr.d.ts.map +1 -1
- package/dist/tools/get_value.d.ts.map +1 -1
- package/dist/tools/goto.d.ts.map +1 -1
- package/dist/tools/hover.d.ts.map +1 -1
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/is_checked.d.ts.map +1 -1
- package/dist/tools/is_enabled.d.ts.map +1 -1
- package/dist/tools/is_visible.d.ts.map +1 -1
- package/dist/tools/launch.d.ts.map +1 -1
- package/dist/tools/macro_resolve.d.ts.map +1 -1
- package/dist/tools/perf.d.ts.map +1 -1
- package/dist/tools/pointer_down.d.ts +4 -0
- package/dist/tools/pointer_down.d.ts.map +1 -0
- package/dist/tools/pointer_drag.d.ts +4 -0
- package/dist/tools/pointer_drag.d.ts.map +1 -0
- package/dist/tools/pointer_move.d.ts +4 -0
- package/dist/tools/pointer_move.d.ts.map +1 -0
- package/dist/tools/pointer_up.d.ts +4 -0
- package/dist/tools/pointer_up.d.ts.map +1 -0
- package/dist/tools/press.d.ts.map +1 -1
- package/dist/tools/product_video_run.d.ts.map +1 -1
- package/dist/tools/prompting_guide.d.ts.map +1 -1
- package/dist/tools/research_run.d.ts.map +1 -1
- package/dist/tools/response.d.ts +4 -1
- package/dist/tools/response.d.ts.map +1 -1
- package/dist/tools/review.d.ts +4 -0
- package/dist/tools/review.d.ts.map +1 -0
- package/dist/tools/screencast_start.d.ts +4 -0
- package/dist/tools/screencast_start.d.ts.map +1 -0
- package/dist/tools/screencast_stop.d.ts +4 -0
- package/dist/tools/screencast_stop.d.ts.map +1 -0
- package/dist/tools/screenshot.d.ts.map +1 -1
- package/dist/tools/scroll.d.ts.map +1 -1
- package/dist/tools/scroll_into_view.d.ts.map +1 -1
- package/dist/tools/select.d.ts.map +1 -1
- package/dist/tools/session_inspector.d.ts +4 -0
- package/dist/tools/session_inspector.d.ts.map +1 -0
- package/dist/tools/shopping_run.d.ts.map +1 -1
- package/dist/tools/skill_list.d.ts.map +1 -1
- package/dist/tools/skill_load.d.ts.map +1 -1
- package/dist/tools/snapshot.d.ts.map +1 -1
- package/dist/tools/type.d.ts.map +1 -1
- package/dist/tools/uncheck.d.ts.map +1 -1
- package/dist/tools/upload.d.ts +4 -0
- package/dist/tools/upload.d.ts.map +1 -0
- package/dist/tools/wait.d.ts.map +1 -1
- package/dist/tools/workflow-runtime.d.ts +4 -2
- package/dist/tools/workflow-runtime.d.ts.map +1 -1
- package/dist/utils/package-assets.d.ts +4 -0
- package/dist/utils/package-assets.d.ts.map +1 -0
- package/extension/canvas.html +1006 -0
- package/extension/dist/annotate-content.css +15 -6
- package/extension/dist/annotate-content.js +175 -35
- package/extension/dist/annotation-payload.js +199 -0
- package/extension/dist/background.js +544 -69
- package/extension/dist/canvas/canvas-runtime.js +1490 -0
- package/extension/dist/canvas/model.js +341 -0
- package/extension/dist/canvas/viewport-fit.js +67 -0
- package/extension/dist/canvas-page.js +3609 -0
- package/extension/dist/ops/dom-bridge.js +255 -3
- package/extension/dist/ops/ops-runtime.js +3324 -301
- package/extension/dist/ops/ops-session-store.js +97 -112
- package/extension/dist/ops/snapshot-builder.js +2 -2
- package/extension/dist/ops/snapshot-shared.js +2 -2
- package/extension/dist/ops/target-session-coordinator.js +159 -0
- package/extension/dist/popup.js +201 -42
- package/extension/dist/services/CDPRouter.js +1567 -63
- package/extension/dist/services/ConnectionManager.js +453 -78
- package/extension/dist/services/RelayClient.js +79 -30
- package/extension/dist/services/TabManager.js +118 -22
- package/extension/dist/services/TargetSessionMap.js +127 -3
- package/extension/dist/services/attach-errors.js +20 -0
- package/extension/dist/services/cdp-router-commands.js +135 -8
- package/extension/dist/services/url-restrictions.js +9 -13
- package/extension/dist/types.js +2 -0
- package/extension/manifest.json +2 -2
- package/extension/popup.html +59 -6
- package/package.json +19 -9
- package/skills/AGENTS.md +8 -4
- package/skills/opendevbrowser-best-practices/SKILL.md +183 -6
- package/skills/opendevbrowser-best-practices/artifacts/browser-agent-known-issues-matrix.md +1 -0
- package/skills/opendevbrowser-best-practices/artifacts/canvas-governance-playbook.md +141 -0
- package/skills/opendevbrowser-best-practices/artifacts/command-channel-reference.md +129 -19
- package/skills/opendevbrowser-best-practices/artifacts/parity-gates.md +9 -2
- package/skills/opendevbrowser-best-practices/artifacts/provider-workflows.md +6 -0
- package/skills/opendevbrowser-best-practices/artifacts/skill-runtime-surface-matrix.md +58 -0
- package/skills/opendevbrowser-best-practices/assets/templates/canvas-blocker-checklist.json +70 -0
- package/skills/opendevbrowser-best-practices/assets/templates/canvas-feedback-eval.json +73 -0
- package/skills/opendevbrowser-best-practices/assets/templates/canvas-generation-plan.v1.json +67 -0
- package/skills/opendevbrowser-best-practices/assets/templates/canvas-handshake-example.json +126 -0
- package/skills/opendevbrowser-best-practices/assets/templates/robustness-checklist.json +57 -0
- package/skills/opendevbrowser-best-practices/assets/templates/skill-runtime-pack-matrix.json +674 -0
- package/skills/opendevbrowser-best-practices/assets/templates/surface-audit-checklist.json +12 -3
- package/skills/opendevbrowser-best-practices/scripts/odb-workflow.sh +107 -12
- package/skills/opendevbrowser-best-practices/scripts/resolve-odb-cli.sh +100 -0
- package/skills/opendevbrowser-best-practices/scripts/run-robustness-audit.sh +83 -1
- package/skills/opendevbrowser-best-practices/scripts/validate-skill-assets.sh +365 -84
- package/skills/opendevbrowser-best-practices/scripts/validator-fixture-cli.sh +208 -0
- package/skills/opendevbrowser-continuity-ledger/SKILL.md +14 -1
- package/skills/opendevbrowser-continuity-ledger/scripts/validate-skill-assets.sh +61 -0
- package/skills/opendevbrowser-data-extraction/SKILL.md +6 -0
- package/skills/opendevbrowser-data-extraction/scripts/validate-skill-assets.sh +112 -0
- package/skills/opendevbrowser-design-agent/SKILL.md +275 -0
- package/skills/opendevbrowser-design-agent/artifacts/app-shell-and-state-wiring.md +84 -0
- package/skills/opendevbrowser-design-agent/artifacts/async-search-state-ownership.md +58 -0
- package/skills/opendevbrowser-design-agent/artifacts/component-pattern-index.md +130 -0
- package/skills/opendevbrowser-design-agent/artifacts/design-contract-playbook.md +157 -0
- package/skills/opendevbrowser-design-agent/artifacts/design-release-gate.md +40 -0
- package/skills/opendevbrowser-design-agent/artifacts/design-workflows.md +153 -0
- package/skills/opendevbrowser-design-agent/artifacts/existing-surface-adaptation.md +56 -0
- package/skills/opendevbrowser-design-agent/artifacts/external-pattern-synthesis.md +103 -0
- package/skills/opendevbrowser-design-agent/artifacts/frontend-evaluation-rubric.md +61 -0
- package/skills/opendevbrowser-design-agent/artifacts/implementation-anti-patterns.md +163 -0
- package/skills/opendevbrowser-design-agent/artifacts/isolated-preview-validation.md +68 -0
- package/skills/opendevbrowser-design-agent/artifacts/loading-and-feedback-surfaces.md +56 -0
- package/skills/opendevbrowser-design-agent/artifacts/opendevbrowser-ui-example-map.md +44 -0
- package/skills/opendevbrowser-design-agent/artifacts/performance-audit-playbook.md +70 -0
- package/skills/opendevbrowser-design-agent/artifacts/research-harvest-workflow.md +81 -0
- package/skills/opendevbrowser-design-agent/artifacts/scroll-reveal-surface-planning.md +64 -0
- package/skills/opendevbrowser-design-agent/artifacts/state-ownership-matrix.md +36 -0
- package/skills/opendevbrowser-design-agent/artifacts/theming-and-token-ownership.md +43 -0
- package/skills/opendevbrowser-design-agent/assets/templates/canvas-generation-plan.design.v1.json +58 -0
- package/skills/opendevbrowser-design-agent/assets/templates/design-audit-report.v1.md +34 -0
- package/skills/opendevbrowser-design-agent/assets/templates/design-brief.v1.md +40 -0
- package/skills/opendevbrowser-design-agent/assets/templates/design-contract.v1.json +226 -0
- package/skills/opendevbrowser-design-agent/assets/templates/design-release-gate.v1.json +35 -0
- package/skills/opendevbrowser-design-agent/assets/templates/design-review-checklist.json +57 -0
- package/skills/opendevbrowser-design-agent/assets/templates/real-surface-design-matrix.json +32 -0
- package/skills/opendevbrowser-design-agent/assets/templates/reference-pattern-board.v1.json +31 -0
- package/skills/opendevbrowser-design-agent/scripts/design-workflow.sh +171 -0
- package/skills/opendevbrowser-design-agent/scripts/extract-canvas-plan.sh +56 -0
- package/skills/opendevbrowser-design-agent/scripts/validate-skill-assets.sh +223 -0
- package/skills/opendevbrowser-form-testing/SKILL.md +19 -3
- package/skills/opendevbrowser-form-testing/artifacts/form-workflows.md +5 -4
- package/skills/opendevbrowser-form-testing/assets/templates/challenge-decision-tree.json +2 -0
- package/skills/opendevbrowser-form-testing/scripts/validate-skill-assets.sh +109 -0
- package/skills/opendevbrowser-login-automation/SKILL.md +21 -3
- package/skills/opendevbrowser-login-automation/artifacts/login-workflows.md +5 -4
- package/skills/opendevbrowser-login-automation/assets/templates/auth-signals.json +5 -0
- package/skills/opendevbrowser-login-automation/assets/templates/login-scenario-matrix.json +3 -2
- package/skills/opendevbrowser-login-automation/scripts/run-login-workflow.sh +17 -1
- package/skills/opendevbrowser-login-automation/scripts/validate-skill-assets.sh +133 -0
- package/skills/opendevbrowser-product-presentation-asset/SKILL.md +23 -11
- package/skills/opendevbrowser-product-presentation-asset/artifacts/asset-pack-assembly.md +5 -3
- package/skills/opendevbrowser-product-presentation-asset/assets/templates/shot-list.md +2 -0
- package/skills/opendevbrowser-product-presentation-asset/assets/templates/video-assembly.md +3 -2
- package/skills/opendevbrowser-product-presentation-asset/scripts/capture-screenshots.sh +5 -1
- package/skills/opendevbrowser-product-presentation-asset/scripts/collect-product.sh +6 -2
- package/skills/opendevbrowser-product-presentation-asset/scripts/download-images.sh +5 -1
- package/skills/opendevbrowser-product-presentation-asset/scripts/render-video-brief.sh +20 -7
- package/skills/opendevbrowser-product-presentation-asset/scripts/validate-skill-assets.sh +39 -0
- package/skills/opendevbrowser-product-presentation-asset/scripts/write-manifest.sh +5 -1
- package/skills/opendevbrowser-research/SKILL.md +14 -6
- package/skills/opendevbrowser-research/scripts/render-output.sh +5 -1
- package/skills/opendevbrowser-research/scripts/run-research.sh +5 -1
- package/skills/opendevbrowser-research/scripts/validate-skill-assets.sh +45 -0
- package/skills/opendevbrowser-research/scripts/write-artifacts.sh +5 -1
- package/skills/opendevbrowser-shopping/SKILL.md +20 -1
- package/skills/opendevbrowser-shopping/scripts/normalize-offers.sh +6 -2
- package/skills/opendevbrowser-shopping/scripts/run-deal-hunt.sh +5 -1
- package/skills/opendevbrowser-shopping/scripts/run-shopping.sh +5 -1
- package/skills/opendevbrowser-shopping/scripts/validate-skill-assets.sh +54 -0
- package/dist/chunk-ST7CO5FA.js +0 -18668
- package/dist/chunk-ST7CO5FA.js.map +0 -1
- /package/dist/{chunk-7W3SPXIB.js.map → chunk-FUSXMW3G.js.map} +0 -0
- /package/dist/{macros-NUBRM44Y.js.map → macros-ND2M7LWU.js.map} +0 -0
|
@@ -0,0 +1,3609 @@
|
|
|
1
|
+
import { summarizeCanvasProjectionState, summarizeCanvasHistoryState, readLatestImportProvenance, readSelectedBindingIdentity, normalizeCanvasSessionSummary, normalizeCanvasTargetStateSummaries } from "./canvas/model.js";
|
|
2
|
+
import { buildCanvasAnnotationPayload, describeAnnotationItem, formatAnnotationDispatchReceipt } from "./annotation-payload.js";
|
|
3
|
+
import { DEFAULT_EDITOR_VIEWPORT, computeFittedViewport, computeViewportCanvasCenter, isDefaultEditorViewport } from "./canvas/viewport-fit.js";
|
|
4
|
+
const DB_NAME = "opendevbrowser-canvas";
|
|
5
|
+
const DB_VERSION = 2;
|
|
6
|
+
const STORE_NAME = "editor-state";
|
|
7
|
+
const CHANNEL_NAME = "opendevbrowser-canvas";
|
|
8
|
+
const SAVE_DEBOUNCE_MS = 180;
|
|
9
|
+
const UNIT_LESS_STYLES = new Set(["fontWeight", "lineHeight", "opacity", "zIndex"]);
|
|
10
|
+
const titleElement = requiredElement("canvas-title");
|
|
11
|
+
const badgesElement = requiredElement("canvas-badges");
|
|
12
|
+
const metaElement = requiredElement("canvas-meta");
|
|
13
|
+
const toolbarMetaElement = requiredElement("canvas-toolbar-meta");
|
|
14
|
+
const summaryElement = requiredElement("canvas-summary");
|
|
15
|
+
const feedbackElement = requiredElement("canvas-feedback");
|
|
16
|
+
const pageDetailsElement = requiredElement("canvas-page-details");
|
|
17
|
+
const pageSelectElement = requiredElement("canvas-page-select");
|
|
18
|
+
const layersTreeElement = requiredElement("canvas-layers-tree");
|
|
19
|
+
const selectionMetaElement = requiredElement("canvas-selection-meta");
|
|
20
|
+
const stageElement = requiredElement("canvas-stage");
|
|
21
|
+
const stageInnerElement = requiredElement("canvas-stage-inner");
|
|
22
|
+
const stageOverlayElement = requiredElement("canvas-stage-overlay");
|
|
23
|
+
const stageMetaElement = requiredElement("canvas-stage-meta");
|
|
24
|
+
const stageHintElement = requiredElement("canvas-stage-hint");
|
|
25
|
+
const previewElement = requiredElement("canvas-preview");
|
|
26
|
+
const emptyElement = requiredElement("canvas-empty");
|
|
27
|
+
const historyUndoButton = requiredElement("canvas-history-undo");
|
|
28
|
+
const historyRedoButton = requiredElement("canvas-history-redo");
|
|
29
|
+
const panelHistoryUndoButton = requiredElement("canvas-history-panel-undo");
|
|
30
|
+
const panelHistoryRedoButton = requiredElement("canvas-history-panel-redo");
|
|
31
|
+
const historyStatusElement = requiredElement("canvas-history-status");
|
|
32
|
+
const nameInput = requiredElement("canvas-node-name");
|
|
33
|
+
const textInput = requiredElement("canvas-node-text");
|
|
34
|
+
const nodeXInput = requiredElement("canvas-node-x");
|
|
35
|
+
const nodeYInput = requiredElement("canvas-node-y");
|
|
36
|
+
const nodeWidthInput = requiredElement("canvas-node-width");
|
|
37
|
+
const nodeHeightInput = requiredElement("canvas-node-height");
|
|
38
|
+
const paddingInput = requiredElement("canvas-style-padding");
|
|
39
|
+
const gapInput = requiredElement("canvas-style-gap");
|
|
40
|
+
const fontSizeInput = requiredElement("canvas-style-font-size");
|
|
41
|
+
const fontWeightInput = requiredElement("canvas-style-font-weight");
|
|
42
|
+
const lineHeightInput = requiredElement("canvas-style-line-height");
|
|
43
|
+
const colorInput = requiredElement("canvas-style-color");
|
|
44
|
+
const backgroundInput = requiredElement("canvas-style-background");
|
|
45
|
+
const borderColorInput = requiredElement("canvas-style-border-color");
|
|
46
|
+
const borderWidthInput = requiredElement("canvas-style-border-width");
|
|
47
|
+
const borderRadiusInput = requiredElement("canvas-style-border-radius");
|
|
48
|
+
const shadowInput = requiredElement("canvas-style-shadow");
|
|
49
|
+
const bindingKindInput = requiredElement("canvas-binding-kind");
|
|
50
|
+
const bindingComponentInput = requiredElement("canvas-binding-component");
|
|
51
|
+
const bindingSelectorInput = requiredElement("canvas-binding-selector");
|
|
52
|
+
const a11yRoleInput = requiredElement("canvas-a11y-role");
|
|
53
|
+
const a11yLabelInput = requiredElement("canvas-a11y-label");
|
|
54
|
+
const propertiesStatusElement = requiredElement("canvas-properties-status");
|
|
55
|
+
const tokenStatusElement = requiredElement("canvas-token-status");
|
|
56
|
+
const tokenCollectionSelect = requiredElement("canvas-token-collection-select");
|
|
57
|
+
const tokenCollectionNameInput = requiredElement("canvas-token-collection-name");
|
|
58
|
+
const tokenCollectionCreateButton = requiredElement("canvas-token-collection-create");
|
|
59
|
+
const tokenModeSelect = requiredElement("canvas-token-mode-select");
|
|
60
|
+
const tokenModeNameInput = requiredElement("canvas-token-mode-name");
|
|
61
|
+
const tokenModeCreateButton = requiredElement("canvas-token-mode-create");
|
|
62
|
+
const tokenPathInput = requiredElement("canvas-token-path");
|
|
63
|
+
const tokenValueInput = requiredElement("canvas-token-value");
|
|
64
|
+
const tokenAliasInput = requiredElement("canvas-token-alias");
|
|
65
|
+
const tokenBindingPropertySelect = requiredElement("canvas-token-binding-property");
|
|
66
|
+
const tokenSaveButton = requiredElement("canvas-token-save");
|
|
67
|
+
const tokenBindButton = requiredElement("canvas-token-bind");
|
|
68
|
+
const tokenBindingClearButton = requiredElement("canvas-token-binding-clear");
|
|
69
|
+
const tokenSummaryElement = requiredElement("canvas-token-summary");
|
|
70
|
+
const tokenUsageElement = requiredElement("canvas-token-usage");
|
|
71
|
+
const duplicateNodeButton = requiredElement("canvas-duplicate-node");
|
|
72
|
+
const addNoteButton = requiredElement("canvas-add-note");
|
|
73
|
+
const resetViewButton = requiredElement("canvas-reset-view");
|
|
74
|
+
const deleteNodeButton = requiredElement("canvas-delete-node");
|
|
75
|
+
const annotationModeSelect = requiredElement("canvas-annotation-mode");
|
|
76
|
+
const annotationAddButton = requiredElement("canvas-annotation-add");
|
|
77
|
+
const annotationCopyButton = requiredElement("canvas-annotation-copy");
|
|
78
|
+
const annotationSendButton = requiredElement("canvas-annotation-send");
|
|
79
|
+
const annotationContextInput = requiredElement("canvas-annotation-context");
|
|
80
|
+
const annotationListElement = requiredElement("canvas-annotation-list");
|
|
81
|
+
const broadcastChannel = typeof BroadcastChannel === "function" ? new BroadcastChannel(CHANNEL_NAME) : null;
|
|
82
|
+
const port = chrome.runtime.connect({ name: "canvas-page" });
|
|
83
|
+
let currentState = null;
|
|
84
|
+
let currentTabId = null;
|
|
85
|
+
let databasePromise = null;
|
|
86
|
+
let persistTimer = null;
|
|
87
|
+
let fitViewportFrame = null;
|
|
88
|
+
let activePageId = null;
|
|
89
|
+
let annotationMode = "selected";
|
|
90
|
+
let annotationDrafts = [];
|
|
91
|
+
let expandedLayerNodeIds = new Set();
|
|
92
|
+
let selectedTokenCollectionId = "__values__";
|
|
93
|
+
let selectedTokenModeId = "__base__";
|
|
94
|
+
let selectedTokenPath = "";
|
|
95
|
+
let draggingNode = null;
|
|
96
|
+
let layerDragState = null;
|
|
97
|
+
let panningState = null;
|
|
98
|
+
let marqueeState = null;
|
|
99
|
+
let spacePanActive = false;
|
|
100
|
+
void bootstrap();
|
|
101
|
+
async function bootstrap() {
|
|
102
|
+
currentTabId = await getCurrentTabId();
|
|
103
|
+
const cached = await loadCachedState(currentTabId);
|
|
104
|
+
if (cached) {
|
|
105
|
+
applyState(cached, false);
|
|
106
|
+
}
|
|
107
|
+
port.onMessage.addListener((message) => {
|
|
108
|
+
handlePortMessage(message);
|
|
109
|
+
});
|
|
110
|
+
port.onDisconnect.addListener(() => {
|
|
111
|
+
renderMeta("Disconnected from canvas runtime");
|
|
112
|
+
});
|
|
113
|
+
broadcastChannel?.addEventListener("message", (event) => {
|
|
114
|
+
const state = normalizeCanvasPageState(isRecord(event.data) ? event.data.state : null);
|
|
115
|
+
if (!state || !shouldAcceptBroadcast(state)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
applyState(state, false);
|
|
119
|
+
});
|
|
120
|
+
port.postMessage({ type: "canvas-page-ready" });
|
|
121
|
+
bindStageInteractions();
|
|
122
|
+
bindInspector();
|
|
123
|
+
bindTokenPanel();
|
|
124
|
+
bindToolbar();
|
|
125
|
+
bindAnnotationPanel();
|
|
126
|
+
bindPageSelector();
|
|
127
|
+
bindKeyboardShortcuts();
|
|
128
|
+
document.addEventListener("visibilitychange", () => {
|
|
129
|
+
if (document.visibilityState === "hidden") {
|
|
130
|
+
flushPersist();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
window.addEventListener("beforeunload", () => {
|
|
134
|
+
flushPersist();
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function bindToolbar() {
|
|
138
|
+
const handleUndo = () => {
|
|
139
|
+
requestHistory("undo");
|
|
140
|
+
};
|
|
141
|
+
const handleRedo = () => {
|
|
142
|
+
requestHistory("redo");
|
|
143
|
+
};
|
|
144
|
+
historyUndoButton.addEventListener("click", handleUndo);
|
|
145
|
+
historyRedoButton.addEventListener("click", handleRedo);
|
|
146
|
+
panelHistoryUndoButton.addEventListener("click", handleUndo);
|
|
147
|
+
panelHistoryRedoButton.addEventListener("click", handleRedo);
|
|
148
|
+
duplicateNodeButton.addEventListener("click", () => {
|
|
149
|
+
duplicateSelectedNode();
|
|
150
|
+
});
|
|
151
|
+
addNoteButton.addEventListener("click", () => {
|
|
152
|
+
if (!currentState || currentState.pendingMutation) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const page = getActivePage();
|
|
156
|
+
if (!page) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const nodeId = `node_note_${crypto.randomUUID().slice(0, 8)}`;
|
|
160
|
+
const parentId = currentState.selection.nodeId ?? page.rootNodeId;
|
|
161
|
+
const center = computeViewportCanvasCenter(currentState.viewport, stageElement.clientWidth, stageElement.clientHeight);
|
|
162
|
+
const noteNode = {
|
|
163
|
+
id: nodeId,
|
|
164
|
+
kind: "note",
|
|
165
|
+
name: "New Note",
|
|
166
|
+
rect: { x: center.x - 120, y: center.y - 60, width: 240, height: 120 },
|
|
167
|
+
props: { text: "New note" },
|
|
168
|
+
style: {},
|
|
169
|
+
metadata: {}
|
|
170
|
+
};
|
|
171
|
+
applyOptimisticPatch([
|
|
172
|
+
{
|
|
173
|
+
op: "node.insert",
|
|
174
|
+
pageId: page.id,
|
|
175
|
+
parentId,
|
|
176
|
+
node: noteNode
|
|
177
|
+
}
|
|
178
|
+
], {
|
|
179
|
+
pageId: page.id,
|
|
180
|
+
nodeId,
|
|
181
|
+
targetId: currentState.selection.targetId
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
resetViewButton.addEventListener("click", () => {
|
|
185
|
+
if (!currentState) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
currentState.viewport = resolvePreferredViewport(currentState);
|
|
189
|
+
postViewState();
|
|
190
|
+
renderState();
|
|
191
|
+
schedulePersist(currentState);
|
|
192
|
+
});
|
|
193
|
+
deleteNodeButton.addEventListener("click", () => {
|
|
194
|
+
if (!currentState || currentState.pendingMutation || !currentState.selection.nodeId) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const nodeId = currentState.selection.nodeId;
|
|
198
|
+
applyOptimisticPatch([{ op: "node.remove", nodeId }], {
|
|
199
|
+
pageId: currentState.selection.pageId,
|
|
200
|
+
nodeId: null,
|
|
201
|
+
targetId: currentState.selection.targetId
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
function bindAnnotationPanel() {
|
|
206
|
+
annotationModeSelect.addEventListener("change", () => {
|
|
207
|
+
annotationMode = annotationModeSelect.value === "region" ? "region" : "selected";
|
|
208
|
+
renderState();
|
|
209
|
+
});
|
|
210
|
+
annotationAddButton.addEventListener("click", () => {
|
|
211
|
+
if (annotationMode === "region") {
|
|
212
|
+
setCanvasButtonFeedback(annotationAddButton, "Drag on stage");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
addSelectedAnnotationDraft();
|
|
216
|
+
});
|
|
217
|
+
annotationCopyButton.addEventListener("click", () => {
|
|
218
|
+
void copyCanvasAnnotation(undefined, annotationCopyButton).catch((error) => {
|
|
219
|
+
setCanvasButtonFeedback(annotationCopyButton, "Copy failed");
|
|
220
|
+
console.error("[opendevbrowser canvas]", error);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
annotationSendButton.addEventListener("click", () => {
|
|
224
|
+
void sendCanvasAnnotation(undefined, "canvas_all", "Canvas annotation payload", annotationSendButton).catch((error) => {
|
|
225
|
+
setCanvasButtonFeedback(annotationSendButton, "Send failed");
|
|
226
|
+
console.error("[opendevbrowser canvas]", error);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function bindPageSelector() {
|
|
231
|
+
pageSelectElement.addEventListener("change", () => {
|
|
232
|
+
if (!currentState) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const pageId = pageSelectElement.value || null;
|
|
236
|
+
setActivePage(pageId, { clearSelectionIfMissing: true, broadcast: true });
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function bindKeyboardShortcuts() {
|
|
240
|
+
document.addEventListener("keydown", (event) => {
|
|
241
|
+
if (!currentState) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (event.key === " ") {
|
|
245
|
+
if (!isEditableTarget(event.target)) {
|
|
246
|
+
spacePanActive = true;
|
|
247
|
+
stageElement.dataset.mode = "panning";
|
|
248
|
+
event.preventDefault();
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (isEditableTarget(event.target)) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const modifier = event.metaKey || event.ctrlKey;
|
|
256
|
+
if (modifier && event.key.toLowerCase() === "z") {
|
|
257
|
+
event.preventDefault();
|
|
258
|
+
requestHistory(event.shiftKey ? "redo" : "undo");
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (modifier && event.key.toLowerCase() === "d") {
|
|
262
|
+
event.preventDefault();
|
|
263
|
+
duplicateSelectedNode();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if ((event.key === "Delete" || event.key === "Backspace") && currentState.selection.nodeId) {
|
|
267
|
+
event.preventDefault();
|
|
268
|
+
deleteNodeButton.click();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (event.key.startsWith("Arrow") && currentState.selection.nodeId && !event.altKey) {
|
|
272
|
+
event.preventDefault();
|
|
273
|
+
nudgeSelectedNode(event.key, event.shiftKey ? 10 : 1);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (event.key === "0") {
|
|
277
|
+
event.preventDefault();
|
|
278
|
+
fitActivePageViewport();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (event.key === "1") {
|
|
282
|
+
event.preventDefault();
|
|
283
|
+
resetZoomToDefault();
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
document.addEventListener("keyup", (event) => {
|
|
287
|
+
if (event.key === " ") {
|
|
288
|
+
spacePanActive = false;
|
|
289
|
+
if (!panningState) {
|
|
290
|
+
stageElement.dataset.mode = "";
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
function bindInspector() {
|
|
296
|
+
nameInput.addEventListener("change", () => {
|
|
297
|
+
const node = getSelectedNode();
|
|
298
|
+
if (!node || !currentState || currentState.pendingMutation) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
applyOptimisticPatch([{
|
|
302
|
+
op: "node.update",
|
|
303
|
+
nodeId: node.id,
|
|
304
|
+
changes: { name: nameInput.value.trim() || node.name }
|
|
305
|
+
}], currentState.selection);
|
|
306
|
+
});
|
|
307
|
+
textInput.addEventListener("change", () => {
|
|
308
|
+
const node = getSelectedNode();
|
|
309
|
+
if (!node || !currentState || currentState.pendingMutation) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
applyOptimisticPatch([{
|
|
313
|
+
op: "node.update",
|
|
314
|
+
nodeId: node.id,
|
|
315
|
+
changes: { "props.text": textInput.value }
|
|
316
|
+
}], currentState.selection);
|
|
317
|
+
});
|
|
318
|
+
bindFieldCommit(nodeXInput, () => {
|
|
319
|
+
commitSelectedNodeChanges({ "rect.x": readNumberInput(nodeXInput, getSelectedNode()?.rect.x ?? 0) });
|
|
320
|
+
});
|
|
321
|
+
bindFieldCommit(nodeYInput, () => {
|
|
322
|
+
commitSelectedNodeChanges({ "rect.y": readNumberInput(nodeYInput, getSelectedNode()?.rect.y ?? 0) });
|
|
323
|
+
});
|
|
324
|
+
bindFieldCommit(nodeWidthInput, () => {
|
|
325
|
+
commitSelectedNodeChanges({ "rect.width": Math.max(readNumberInput(nodeWidthInput, getSelectedNode()?.rect.width ?? 1), 1) });
|
|
326
|
+
});
|
|
327
|
+
bindFieldCommit(nodeHeightInput, () => {
|
|
328
|
+
commitSelectedNodeChanges({ "rect.height": Math.max(readNumberInput(nodeHeightInput, getSelectedNode()?.rect.height ?? 1), 1) });
|
|
329
|
+
});
|
|
330
|
+
bindFieldCommit(paddingInput, () => {
|
|
331
|
+
commitSelectedNodeChanges({ "style.padding": readTextInput(paddingInput) });
|
|
332
|
+
});
|
|
333
|
+
bindFieldCommit(gapInput, () => {
|
|
334
|
+
commitSelectedNodeChanges({ "style.gap": readTextInput(gapInput) });
|
|
335
|
+
});
|
|
336
|
+
bindFieldCommit(fontSizeInput, () => {
|
|
337
|
+
commitSelectedNodeChanges({ "style.fontSize": readTextInput(fontSizeInput) });
|
|
338
|
+
});
|
|
339
|
+
bindFieldCommit(fontWeightInput, () => {
|
|
340
|
+
commitSelectedNodeChanges({ "style.fontWeight": readTextInput(fontWeightInput) });
|
|
341
|
+
});
|
|
342
|
+
bindFieldCommit(lineHeightInput, () => {
|
|
343
|
+
commitSelectedNodeChanges({ "style.lineHeight": readTextInput(lineHeightInput) });
|
|
344
|
+
});
|
|
345
|
+
bindFieldCommit(colorInput, () => {
|
|
346
|
+
commitSelectedNodeChanges({ "style.color": readTextInput(colorInput) });
|
|
347
|
+
});
|
|
348
|
+
bindFieldCommit(backgroundInput, () => {
|
|
349
|
+
commitSelectedNodeChanges({ "style.backgroundColor": readTextInput(backgroundInput) });
|
|
350
|
+
});
|
|
351
|
+
bindFieldCommit(borderColorInput, () => {
|
|
352
|
+
commitSelectedNodeChanges({ "style.borderColor": readTextInput(borderColorInput) });
|
|
353
|
+
});
|
|
354
|
+
bindFieldCommit(borderWidthInput, () => {
|
|
355
|
+
commitSelectedNodeChanges({ "style.borderWidth": readTextInput(borderWidthInput) });
|
|
356
|
+
});
|
|
357
|
+
bindFieldCommit(borderRadiusInput, () => {
|
|
358
|
+
commitSelectedNodeChanges({ "style.borderRadius": readTextInput(borderRadiusInput) });
|
|
359
|
+
});
|
|
360
|
+
bindFieldCommit(shadowInput, () => {
|
|
361
|
+
commitSelectedNodeChanges({ "style.boxShadow": readTextInput(shadowInput) });
|
|
362
|
+
});
|
|
363
|
+
bindFieldCommit(a11yRoleInput, () => {
|
|
364
|
+
commitSelectedNodeChanges({ "metadata.accessibility.role": readTextInput(a11yRoleInput) });
|
|
365
|
+
});
|
|
366
|
+
bindFieldCommit(a11yLabelInput, () => {
|
|
367
|
+
commitSelectedNodeChanges({ "metadata.accessibility.label": readTextInput(a11yLabelInput) });
|
|
368
|
+
});
|
|
369
|
+
bindFieldCommit(bindingKindInput, () => {
|
|
370
|
+
commitSelectedBindingPatch();
|
|
371
|
+
});
|
|
372
|
+
bindFieldCommit(bindingComponentInput, () => {
|
|
373
|
+
commitSelectedBindingPatch();
|
|
374
|
+
});
|
|
375
|
+
bindFieldCommit(bindingSelectorInput, () => {
|
|
376
|
+
commitSelectedBindingPatch();
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
function bindTokenPanel() {
|
|
380
|
+
tokenCollectionSelect.addEventListener("change", () => {
|
|
381
|
+
selectedTokenCollectionId = tokenCollectionSelect.value || "__values__";
|
|
382
|
+
selectedTokenModeId = selectedTokenCollectionId === "__values__" ? "__base__" : tokenModeSelect.value || "__base__";
|
|
383
|
+
syncTokenEditorSelection();
|
|
384
|
+
renderTokenSummary();
|
|
385
|
+
});
|
|
386
|
+
tokenCollectionCreateButton.addEventListener("click", () => {
|
|
387
|
+
createTokenCollection();
|
|
388
|
+
});
|
|
389
|
+
tokenModeSelect.addEventListener("change", () => {
|
|
390
|
+
selectedTokenModeId = tokenModeSelect.value || "__base__";
|
|
391
|
+
updateActiveTokenMode();
|
|
392
|
+
syncTokenEditorSelection();
|
|
393
|
+
renderTokenSummary();
|
|
394
|
+
});
|
|
395
|
+
tokenModeCreateButton.addEventListener("click", () => {
|
|
396
|
+
createTokenMode();
|
|
397
|
+
});
|
|
398
|
+
tokenPathInput.addEventListener("input", () => {
|
|
399
|
+
selectedTokenPath = normalizeTokenPath(tokenPathInput.value);
|
|
400
|
+
renderTokenSummary();
|
|
401
|
+
});
|
|
402
|
+
tokenSaveButton.addEventListener("click", () => {
|
|
403
|
+
saveTokenEditor();
|
|
404
|
+
});
|
|
405
|
+
tokenBindButton.addEventListener("click", () => {
|
|
406
|
+
bindSelectedNodeToToken();
|
|
407
|
+
});
|
|
408
|
+
tokenBindingClearButton.addEventListener("click", () => {
|
|
409
|
+
clearSelectedTokenBinding();
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
function bindStageInteractions() {
|
|
413
|
+
stageElement.addEventListener("pointerdown", (event) => {
|
|
414
|
+
const target = event.target instanceof HTMLElement ? event.target.closest("[data-node-id]") : null;
|
|
415
|
+
const targetNodeId = target?.dataset.nodeId ?? null;
|
|
416
|
+
if (!currentState) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (annotationMode === "region" && event.button === 0) {
|
|
420
|
+
marqueeState = {
|
|
421
|
+
originClientX: event.clientX,
|
|
422
|
+
originClientY: event.clientY,
|
|
423
|
+
currentClientX: event.clientX,
|
|
424
|
+
currentClientY: event.clientY,
|
|
425
|
+
targetNodeId
|
|
426
|
+
};
|
|
427
|
+
renderStageOverlay();
|
|
428
|
+
stageElement.setPointerCapture(event.pointerId);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (spacePanActive) {
|
|
432
|
+
panningState = {
|
|
433
|
+
originX: event.clientX,
|
|
434
|
+
originY: event.clientY,
|
|
435
|
+
startX: currentState.viewport.x,
|
|
436
|
+
startY: currentState.viewport.y
|
|
437
|
+
};
|
|
438
|
+
stageElement.dataset.mode = "panning";
|
|
439
|
+
stageElement.setPointerCapture(event.pointerId);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (target) {
|
|
443
|
+
const node = targetNodeId ? findNode(currentState.document, targetNodeId) : null;
|
|
444
|
+
if (!node) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
currentState.selection = {
|
|
448
|
+
pageId: node.pageId ?? getActivePage()?.id ?? null,
|
|
449
|
+
nodeId: node.id,
|
|
450
|
+
targetId: currentState.selection.targetId,
|
|
451
|
+
updatedAt: new Date().toISOString()
|
|
452
|
+
};
|
|
453
|
+
activePageId = node.pageId ?? activePageId;
|
|
454
|
+
draggingNode = {
|
|
455
|
+
nodeId: node.id,
|
|
456
|
+
originX: event.clientX,
|
|
457
|
+
originY: event.clientY,
|
|
458
|
+
startRectX: node.rect.x,
|
|
459
|
+
startRectY: node.rect.y
|
|
460
|
+
};
|
|
461
|
+
postViewState();
|
|
462
|
+
renderState();
|
|
463
|
+
stageElement.setPointerCapture(event.pointerId);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
panningState = {
|
|
467
|
+
originX: event.clientX,
|
|
468
|
+
originY: event.clientY,
|
|
469
|
+
startX: currentState.viewport.x,
|
|
470
|
+
startY: currentState.viewport.y
|
|
471
|
+
};
|
|
472
|
+
stageElement.dataset.mode = "panning";
|
|
473
|
+
stageElement.setPointerCapture(event.pointerId);
|
|
474
|
+
});
|
|
475
|
+
stageElement.addEventListener("pointermove", (event) => {
|
|
476
|
+
if (!currentState) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (marqueeState) {
|
|
480
|
+
marqueeState.currentClientX = event.clientX;
|
|
481
|
+
marqueeState.currentClientY = event.clientY;
|
|
482
|
+
renderStageOverlay();
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (draggingNode) {
|
|
486
|
+
const node = findNode(currentState.document, draggingNode.nodeId);
|
|
487
|
+
if (!node) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const dx = (event.clientX - draggingNode.originX) / currentState.viewport.zoom;
|
|
491
|
+
const dy = (event.clientY - draggingNode.originY) / currentState.viewport.zoom;
|
|
492
|
+
node.rect.x = Math.round(draggingNode.startRectX + dx);
|
|
493
|
+
node.rect.y = Math.round(draggingNode.startRectY + dy);
|
|
494
|
+
renderStage();
|
|
495
|
+
renderSelectionMeta();
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (panningState) {
|
|
499
|
+
currentState.viewport.x = Math.round(panningState.startX + (event.clientX - panningState.originX));
|
|
500
|
+
currentState.viewport.y = Math.round(panningState.startY + (event.clientY - panningState.originY));
|
|
501
|
+
renderStage();
|
|
502
|
+
toolbarMetaElement.textContent = formatViewport(currentState.viewport);
|
|
503
|
+
schedulePersist(currentState);
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
const endPointerInteraction = () => {
|
|
507
|
+
if (!currentState) {
|
|
508
|
+
draggingNode = null;
|
|
509
|
+
panningState = null;
|
|
510
|
+
marqueeState = null;
|
|
511
|
+
stageElement.dataset.mode = "";
|
|
512
|
+
renderStageOverlay();
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (marqueeState) {
|
|
516
|
+
const completedMarquee = marqueeState;
|
|
517
|
+
marqueeState = null;
|
|
518
|
+
renderStageOverlay();
|
|
519
|
+
if (annotationMode === "region") {
|
|
520
|
+
commitAnnotationCapture(completedMarquee);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
else if (draggingNode && !currentState.pendingMutation) {
|
|
524
|
+
const node = findNode(currentState.document, draggingNode.nodeId);
|
|
525
|
+
if (node) {
|
|
526
|
+
applyOptimisticPatch([{
|
|
527
|
+
op: "node.update",
|
|
528
|
+
nodeId: node.id,
|
|
529
|
+
changes: {
|
|
530
|
+
"rect.x": node.rect.x,
|
|
531
|
+
"rect.y": node.rect.y
|
|
532
|
+
}
|
|
533
|
+
}], currentState.selection, { skipOptimisticMutation: true });
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
else if (panningState) {
|
|
537
|
+
postViewState();
|
|
538
|
+
}
|
|
539
|
+
draggingNode = null;
|
|
540
|
+
panningState = null;
|
|
541
|
+
stageElement.dataset.mode = spacePanActive ? "panning" : "";
|
|
542
|
+
};
|
|
543
|
+
stageElement.addEventListener("pointerup", endPointerInteraction);
|
|
544
|
+
stageElement.addEventListener("pointercancel", endPointerInteraction);
|
|
545
|
+
stageElement.addEventListener("wheel", (event) => {
|
|
546
|
+
if (!currentState) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
event.preventDefault();
|
|
550
|
+
const nextZoom = clamp(currentState.viewport.zoom * (event.deltaY < 0 ? 1.08 : 0.92), 0.35, 2.4);
|
|
551
|
+
currentState.viewport.zoom = Math.round(nextZoom * 100) / 100;
|
|
552
|
+
renderStage();
|
|
553
|
+
toolbarMetaElement.textContent = formatViewport(currentState.viewport);
|
|
554
|
+
postViewState();
|
|
555
|
+
schedulePersist(currentState);
|
|
556
|
+
}, { passive: false });
|
|
557
|
+
}
|
|
558
|
+
function handlePortMessage(message) {
|
|
559
|
+
if (!message) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (message.type === "canvas-page-action-request") {
|
|
563
|
+
const response = runCanvasPageAction(message.selector ?? null, message.action);
|
|
564
|
+
port.postMessage({
|
|
565
|
+
type: "canvas-page-action-response",
|
|
566
|
+
requestId: message.requestId,
|
|
567
|
+
...(response.ok ? { ok: true, value: response.value } : { ok: false, error: response.error })
|
|
568
|
+
});
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
if (message.type !== "canvas-page:init" && message.type !== "canvas-page:update" && message.type !== "canvas-page:closed") {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (message.type === "canvas-page:closed") {
|
|
575
|
+
currentState = null;
|
|
576
|
+
annotationDrafts = [];
|
|
577
|
+
previewElement.srcdoc = "";
|
|
578
|
+
emptyElement.hidden = false;
|
|
579
|
+
renderMeta(`Canvas closed${message.reason ? `: ${message.reason}` : "."}`);
|
|
580
|
+
stageInnerElement.innerHTML = "";
|
|
581
|
+
renderAnnotationPanel();
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const state = normalizeCanvasPageState(message.state);
|
|
585
|
+
if (!state) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
applyState(state, true);
|
|
589
|
+
}
|
|
590
|
+
function runCanvasPageAction(selector, action) {
|
|
591
|
+
const resolveElement = (allowActive = false) => {
|
|
592
|
+
if (selector && selector.trim().length > 0) {
|
|
593
|
+
return document.querySelector(selector);
|
|
594
|
+
}
|
|
595
|
+
return allowActive ? document.activeElement : null;
|
|
596
|
+
};
|
|
597
|
+
const dispatchPointer = (target, type, buttons) => {
|
|
598
|
+
const rect = target.getBoundingClientRect();
|
|
599
|
+
const init = {
|
|
600
|
+
bubbles: true,
|
|
601
|
+
cancelable: true,
|
|
602
|
+
composed: true,
|
|
603
|
+
clientX: rect.left + rect.width / 2,
|
|
604
|
+
clientY: rect.top + rect.height / 2,
|
|
605
|
+
button: 0,
|
|
606
|
+
buttons
|
|
607
|
+
};
|
|
608
|
+
if (typeof PointerEvent === "function") {
|
|
609
|
+
target.dispatchEvent(new PointerEvent(type, init));
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
target.dispatchEvent(new MouseEvent(type.replace(/^pointer/, "mouse"), init));
|
|
613
|
+
};
|
|
614
|
+
const dispatchMouse = (target, type, buttons) => {
|
|
615
|
+
const rect = target.getBoundingClientRect();
|
|
616
|
+
target.dispatchEvent(new MouseEvent(type, {
|
|
617
|
+
bubbles: true,
|
|
618
|
+
cancelable: true,
|
|
619
|
+
composed: true,
|
|
620
|
+
view: window,
|
|
621
|
+
clientX: rect.left + rect.width / 2,
|
|
622
|
+
clientY: rect.top + rect.height / 2,
|
|
623
|
+
button: 0,
|
|
624
|
+
buttons
|
|
625
|
+
}));
|
|
626
|
+
};
|
|
627
|
+
const dispatchHover = (target) => {
|
|
628
|
+
dispatchPointer(target, "pointerover", 0);
|
|
629
|
+
dispatchPointer(target, "pointerenter", 0);
|
|
630
|
+
dispatchMouse(target, "mouseover", 0);
|
|
631
|
+
dispatchMouse(target, "mouseenter", 0);
|
|
632
|
+
dispatchPointer(target, "pointermove", 0);
|
|
633
|
+
dispatchMouse(target, "mousemove", 0);
|
|
634
|
+
};
|
|
635
|
+
const dispatchClick = (target) => {
|
|
636
|
+
dispatchHover(target);
|
|
637
|
+
dispatchPointer(target, "pointerdown", 1);
|
|
638
|
+
dispatchMouse(target, "mousedown", 1);
|
|
639
|
+
if (target instanceof HTMLElement) {
|
|
640
|
+
target.focus();
|
|
641
|
+
}
|
|
642
|
+
dispatchPointer(target, "pointerup", 0);
|
|
643
|
+
dispatchMouse(target, "mouseup", 0);
|
|
644
|
+
if (target instanceof HTMLElement) {
|
|
645
|
+
target.click();
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
target.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, composed: true, view: window }));
|
|
649
|
+
};
|
|
650
|
+
const selectorState = (target) => {
|
|
651
|
+
if (!target) {
|
|
652
|
+
return { attached: false, visible: false };
|
|
653
|
+
}
|
|
654
|
+
const style = window.getComputedStyle(target);
|
|
655
|
+
const rect = target.getBoundingClientRect();
|
|
656
|
+
return {
|
|
657
|
+
attached: true,
|
|
658
|
+
visible: style.display !== "none"
|
|
659
|
+
&& style.visibility !== "hidden"
|
|
660
|
+
&& style.opacity !== "0"
|
|
661
|
+
&& rect.width > 0
|
|
662
|
+
&& rect.height > 0
|
|
663
|
+
};
|
|
664
|
+
};
|
|
665
|
+
if (action.type === "scroll") {
|
|
666
|
+
const target = resolveElement(false);
|
|
667
|
+
if (target instanceof HTMLElement) {
|
|
668
|
+
target.scrollBy(0, action.dy);
|
|
669
|
+
return { ok: true, value: true };
|
|
670
|
+
}
|
|
671
|
+
window.scrollBy(0, action.dy);
|
|
672
|
+
return { ok: true, value: true };
|
|
673
|
+
}
|
|
674
|
+
if (action.type === "getSelectorState") {
|
|
675
|
+
return { ok: true, value: selectorState(resolveElement(false)) };
|
|
676
|
+
}
|
|
677
|
+
const target = resolveElement(action.type === "press");
|
|
678
|
+
if (!target) {
|
|
679
|
+
return { ok: false, error: "Element not found" };
|
|
680
|
+
}
|
|
681
|
+
switch (action.type) {
|
|
682
|
+
case "outerHTML":
|
|
683
|
+
return { ok: true, value: target.outerHTML };
|
|
684
|
+
case "innerText":
|
|
685
|
+
return { ok: true, value: target instanceof HTMLElement ? target.innerText || target.textContent || "" : target.textContent || "" };
|
|
686
|
+
case "getAttr":
|
|
687
|
+
return { ok: true, value: target.getAttribute(action.name) };
|
|
688
|
+
case "getValue":
|
|
689
|
+
if ("value" in target) {
|
|
690
|
+
return { ok: true, value: String(target.value ?? "") };
|
|
691
|
+
}
|
|
692
|
+
return { ok: true, value: null };
|
|
693
|
+
case "isEnabled":
|
|
694
|
+
if ("disabled" in target) {
|
|
695
|
+
return { ok: true, value: !target.disabled };
|
|
696
|
+
}
|
|
697
|
+
return { ok: true, value: true };
|
|
698
|
+
case "isChecked":
|
|
699
|
+
if ("checked" in target) {
|
|
700
|
+
return { ok: true, value: Boolean(target.checked) };
|
|
701
|
+
}
|
|
702
|
+
return { ok: true, value: false };
|
|
703
|
+
case "click":
|
|
704
|
+
dispatchClick(target);
|
|
705
|
+
return { ok: true, value: true };
|
|
706
|
+
case "hover":
|
|
707
|
+
dispatchHover(target);
|
|
708
|
+
return { ok: true, value: true };
|
|
709
|
+
case "focus":
|
|
710
|
+
if (target instanceof HTMLElement) {
|
|
711
|
+
target.focus();
|
|
712
|
+
}
|
|
713
|
+
return { ok: true, value: true };
|
|
714
|
+
case "press": {
|
|
715
|
+
if (target instanceof HTMLElement) {
|
|
716
|
+
target.focus();
|
|
717
|
+
}
|
|
718
|
+
const init = { key: action.key, bubbles: true, cancelable: true };
|
|
719
|
+
target.dispatchEvent(new KeyboardEvent("keydown", init));
|
|
720
|
+
target.dispatchEvent(new KeyboardEvent("keypress", init));
|
|
721
|
+
target.dispatchEvent(new KeyboardEvent("keyup", init));
|
|
722
|
+
return { ok: true, value: true };
|
|
723
|
+
}
|
|
724
|
+
case "type": {
|
|
725
|
+
if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)) {
|
|
726
|
+
return { ok: false, error: "Element does not support typing" };
|
|
727
|
+
}
|
|
728
|
+
if (action.clear) {
|
|
729
|
+
target.value = "";
|
|
730
|
+
}
|
|
731
|
+
target.value = String(action.value ?? "");
|
|
732
|
+
target.dispatchEvent(new Event("input", { bubbles: true }));
|
|
733
|
+
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
734
|
+
if (action.submit) {
|
|
735
|
+
target.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
|
736
|
+
}
|
|
737
|
+
return { ok: true, value: true };
|
|
738
|
+
}
|
|
739
|
+
case "setChecked":
|
|
740
|
+
if (!("checked" in target)) {
|
|
741
|
+
return { ok: false, error: "Element does not support checked state" };
|
|
742
|
+
}
|
|
743
|
+
target.checked = Boolean(action.checked);
|
|
744
|
+
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
745
|
+
return { ok: true, value: true };
|
|
746
|
+
case "select":
|
|
747
|
+
if (!(target instanceof HTMLSelectElement)) {
|
|
748
|
+
return { ok: false, error: "Element is not a select" };
|
|
749
|
+
}
|
|
750
|
+
const wanted = new Set(action.values);
|
|
751
|
+
for (const option of Array.from(target.options)) {
|
|
752
|
+
option.selected = wanted.has(option.value);
|
|
753
|
+
}
|
|
754
|
+
target.dispatchEvent(new Event("input", { bubbles: true }));
|
|
755
|
+
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
756
|
+
return { ok: true, value: true };
|
|
757
|
+
case "scrollIntoView":
|
|
758
|
+
if (target instanceof HTMLElement) {
|
|
759
|
+
target.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
target.scrollIntoView();
|
|
763
|
+
}
|
|
764
|
+
return { ok: true, value: true };
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function applyState(state, persist) {
|
|
768
|
+
currentState = state;
|
|
769
|
+
if (!activePageId || !state.document.pages.some((page) => page.id === activePageId)) {
|
|
770
|
+
activePageId = state.selection.pageId ?? state.document.pages[0]?.id ?? null;
|
|
771
|
+
}
|
|
772
|
+
annotationModeSelect.value = annotationMode;
|
|
773
|
+
titleElement.textContent = state.title;
|
|
774
|
+
renderBadges(state);
|
|
775
|
+
renderMeta(`Document ${state.documentId} updated ${formatTimestamp(state.updatedAt)}`);
|
|
776
|
+
toolbarMetaElement.textContent = formatViewport(state.viewport);
|
|
777
|
+
renderSummary(state.summary, state);
|
|
778
|
+
renderFeedback(state.feedback);
|
|
779
|
+
renderState();
|
|
780
|
+
previewElement.srcdoc = state.html;
|
|
781
|
+
emptyElement.hidden = Boolean(state.html);
|
|
782
|
+
queueViewportFitIfNeeded();
|
|
783
|
+
if (persist) {
|
|
784
|
+
schedulePersist(state);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
function renderState() {
|
|
788
|
+
if (!currentState) {
|
|
789
|
+
renderAnnotationPanel();
|
|
790
|
+
renderHistoryState();
|
|
791
|
+
renderTokenSummary();
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
const projectionSummary = summarizeCanvasProjectionState(currentState.summary, currentState.targets);
|
|
795
|
+
const activePage = getActivePage();
|
|
796
|
+
const syncFragments = [
|
|
797
|
+
currentState.pendingMutation ? "sync pending" : "live",
|
|
798
|
+
currentState.summary.codeSyncState ?? null,
|
|
799
|
+
projectionSummary.conflictCount > 0 ? `${projectionSummary.conflictCount} conflict${projectionSummary.conflictCount === 1 ? "" : "s"}` : null
|
|
800
|
+
].filter((entry) => typeof entry === "string");
|
|
801
|
+
stageMetaElement.textContent = `${activePage?.nodes.length ?? 0} nodes • ${syncFragments.join(" • ")}`;
|
|
802
|
+
stageHintElement.textContent = annotationMode === "region"
|
|
803
|
+
? "Drag on the stage to capture a region, or click a node to capture it."
|
|
804
|
+
: "Drag to pan. Scroll to zoom. Drag nodes to move them.";
|
|
805
|
+
renderPagesAndLayers();
|
|
806
|
+
renderHistoryState();
|
|
807
|
+
renderStage();
|
|
808
|
+
renderInspector();
|
|
809
|
+
renderTokenSummary();
|
|
810
|
+
syncAnnotationDrafts();
|
|
811
|
+
}
|
|
812
|
+
function renderBadges(state) {
|
|
813
|
+
badgesElement.innerHTML = "";
|
|
814
|
+
const projectionSummary = summarizeCanvasProjectionState(state.summary, state.targets);
|
|
815
|
+
const latestImport = readLatestImportProvenance(state.summary, state.document);
|
|
816
|
+
for (const label of [
|
|
817
|
+
state.previewState,
|
|
818
|
+
state.previewMode,
|
|
819
|
+
state.documentRevision === null ? "revision pending" : `revision ${state.documentRevision}`,
|
|
820
|
+
state.pendingMutation ? "sync pending" : "synced",
|
|
821
|
+
state.summary.codeSyncState,
|
|
822
|
+
projectionSummary.activeProjections[0],
|
|
823
|
+
projectionSummary.conflictCount > 0 ? `${projectionSummary.conflictCount} conflicts` : null,
|
|
824
|
+
latestImport ? `import ${latestImport}` : null
|
|
825
|
+
]) {
|
|
826
|
+
if (typeof label !== "string" || label.trim().length === 0) {
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
const badge = document.createElement("span");
|
|
830
|
+
badge.className = "canvas-badge";
|
|
831
|
+
badge.textContent = label;
|
|
832
|
+
badgesElement.append(badge);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
function renderMeta(text) {
|
|
836
|
+
metaElement.textContent = text;
|
|
837
|
+
}
|
|
838
|
+
function renderSummary(summary, state) {
|
|
839
|
+
summaryElement.innerHTML = "";
|
|
840
|
+
const componentLibraries = formatSummaryList(readSummaryLibraryList(summary, "components"));
|
|
841
|
+
const iconLibraries = formatSummaryList(readSummaryLibraryList(summary, "icons"));
|
|
842
|
+
const stylingLibraries = formatSummaryList(readSummaryLibraryList(summary, "styling"));
|
|
843
|
+
const inventorySources = formatSummaryList(readSummaryStringArray(summary.componentSourceKinds));
|
|
844
|
+
const projectionSummary = summarizeCanvasProjectionState(state.summary, state.targets);
|
|
845
|
+
const appliedStarter = formatAppliedStarterSummary(summary, state.document);
|
|
846
|
+
const latestImport = readLatestImportProvenance(summary, state.document);
|
|
847
|
+
const items = [
|
|
848
|
+
["Target", state.targetId],
|
|
849
|
+
["Session", formatSummaryValue(summary.canvasSessionId)],
|
|
850
|
+
["Mode", formatSummaryValue(summary.mode)],
|
|
851
|
+
["Plan", formatSummaryValue(summary.planStatus)],
|
|
852
|
+
["Preflight", formatSummaryValue(summary.preflightState)],
|
|
853
|
+
["Attached clients", formatAttachedClients(state.summary.attachedClients)],
|
|
854
|
+
["Lease holder", state.summary.leaseHolderClientId ?? "none"],
|
|
855
|
+
["Code sync", formatCodeSyncStatus(state.summary, projectionSummary.watchConflict)],
|
|
856
|
+
["Projection", formatProjectionSummary(projectionSummary)],
|
|
857
|
+
["Fallbacks", formatSummaryList(projectionSummary.fallbackReasons)],
|
|
858
|
+
["Components", componentLibraries],
|
|
859
|
+
["Icons", iconLibraries],
|
|
860
|
+
["Styling", stylingLibraries],
|
|
861
|
+
["Inventory", `${formatSummaryValue(summary.componentInventoryCount)} mapped`],
|
|
862
|
+
["Starters", typeof summary.availableStarterCount === "number" ? `${summary.availableStarterCount} available` : "n/a"],
|
|
863
|
+
["Applied starter", appliedStarter],
|
|
864
|
+
["Inventory sources", inventorySources],
|
|
865
|
+
["Targets", String(state.targets.length)],
|
|
866
|
+
["Overlays", String(state.overlayMounts.length)],
|
|
867
|
+
["Feedback", String(countFeedbackItems(state.feedback))],
|
|
868
|
+
["History", summarizeCanvasHistoryState(summary)],
|
|
869
|
+
["Latest import", latestImport ?? "none"]
|
|
870
|
+
];
|
|
871
|
+
for (const [label, value] of items) {
|
|
872
|
+
const row = document.createElement("div");
|
|
873
|
+
row.className = "canvas-summary-item";
|
|
874
|
+
const title = document.createElement("div");
|
|
875
|
+
title.className = "canvas-summary-label";
|
|
876
|
+
title.textContent = label;
|
|
877
|
+
const body = document.createElement("div");
|
|
878
|
+
body.className = "canvas-summary-value";
|
|
879
|
+
body.textContent = value;
|
|
880
|
+
row.append(title, body);
|
|
881
|
+
summaryElement.append(row);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
function formatAppliedStarterSummary(summary, documentState) {
|
|
885
|
+
const starter = isRecord(documentState.meta.starter) ? documentState.meta.starter : null;
|
|
886
|
+
const name = summary.starterName
|
|
887
|
+
?? (typeof starter?.template === "object" && starter.template && typeof starter.template.name === "string"
|
|
888
|
+
? starter.template.name
|
|
889
|
+
: null);
|
|
890
|
+
if (!name) {
|
|
891
|
+
return "none";
|
|
892
|
+
}
|
|
893
|
+
const frameworkId = summary.starterFrameworkId
|
|
894
|
+
?? (typeof starter?.frameworkId === "string" ? starter.frameworkId : null);
|
|
895
|
+
const appliedAt = summary.starterAppliedAt
|
|
896
|
+
?? (typeof starter?.appliedAt === "string" ? starter.appliedAt : null);
|
|
897
|
+
return [name, frameworkId, appliedAt].filter((entry) => typeof entry === "string" && entry.trim().length > 0).join(" • ");
|
|
898
|
+
}
|
|
899
|
+
function renderFeedback(events) {
|
|
900
|
+
feedbackElement.innerHTML = "";
|
|
901
|
+
const latest = events.slice(-8).reverse();
|
|
902
|
+
for (const event of latest) {
|
|
903
|
+
const row = document.createElement("div");
|
|
904
|
+
row.className = "canvas-feedback-item";
|
|
905
|
+
const meta = document.createElement("div");
|
|
906
|
+
meta.className = "canvas-feedback-meta";
|
|
907
|
+
if (event.eventType === "feedback.item") {
|
|
908
|
+
meta.textContent = `${event.item.category} • ${event.item.severity}`;
|
|
909
|
+
meta.classList.add(`canvas-feedback-severity-${event.item.severity}`);
|
|
910
|
+
const body = document.createElement("div");
|
|
911
|
+
body.className = "canvas-feedback-message";
|
|
912
|
+
body.textContent = event.item.message;
|
|
913
|
+
row.append(meta, body);
|
|
914
|
+
}
|
|
915
|
+
else if (event.eventType === "feedback.heartbeat") {
|
|
916
|
+
meta.textContent = "heartbeat";
|
|
917
|
+
const body = document.createElement("div");
|
|
918
|
+
body.className = "canvas-feedback-message";
|
|
919
|
+
body.textContent = `${event.activeTargetIds.length} active targets`;
|
|
920
|
+
row.append(meta, body);
|
|
921
|
+
}
|
|
922
|
+
else {
|
|
923
|
+
meta.textContent = "stream complete";
|
|
924
|
+
const body = document.createElement("div");
|
|
925
|
+
body.className = "canvas-feedback-message";
|
|
926
|
+
body.textContent = event.reason;
|
|
927
|
+
row.append(meta, body);
|
|
928
|
+
}
|
|
929
|
+
feedbackElement.append(row);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
function renderHistoryState() {
|
|
933
|
+
const history = currentState?.summary.history;
|
|
934
|
+
const canUndo = Boolean(history?.canUndo) && !currentState?.pendingMutation;
|
|
935
|
+
const canRedo = Boolean(history?.canRedo) && !currentState?.pendingMutation;
|
|
936
|
+
historyUndoButton.disabled = !canUndo;
|
|
937
|
+
historyRedoButton.disabled = !canRedo;
|
|
938
|
+
panelHistoryUndoButton.disabled = !canUndo;
|
|
939
|
+
panelHistoryRedoButton.disabled = !canRedo;
|
|
940
|
+
historyStatusElement.textContent = currentState ? summarizeCanvasHistoryState(currentState.summary) : "No history yet";
|
|
941
|
+
}
|
|
942
|
+
function renderPagesAndLayers() {
|
|
943
|
+
pageSelectElement.innerHTML = "";
|
|
944
|
+
layersTreeElement.innerHTML = "";
|
|
945
|
+
if (!currentState) {
|
|
946
|
+
pageDetailsElement.textContent = "Waiting for canvas state.";
|
|
947
|
+
const empty = document.createElement("div");
|
|
948
|
+
empty.className = "canvas-empty-inline";
|
|
949
|
+
empty.textContent = "No pages loaded.";
|
|
950
|
+
layersTreeElement.append(empty);
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
const activePage = getActivePage();
|
|
954
|
+
for (const page of currentState.document.pages) {
|
|
955
|
+
const option = document.createElement("option");
|
|
956
|
+
option.value = page.id;
|
|
957
|
+
option.textContent = `${page.name} (${page.nodes.length})`;
|
|
958
|
+
option.selected = page.id === activePage?.id;
|
|
959
|
+
pageSelectElement.append(option);
|
|
960
|
+
}
|
|
961
|
+
pageSelectElement.disabled = currentState.document.pages.length <= 1;
|
|
962
|
+
if (!activePage) {
|
|
963
|
+
pageDetailsElement.textContent = "No page selected.";
|
|
964
|
+
const empty = document.createElement("div");
|
|
965
|
+
empty.className = "canvas-empty-inline";
|
|
966
|
+
empty.textContent = "No nodes on this page.";
|
|
967
|
+
layersTreeElement.append(empty);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
ensureExpandedNodePath(activePage, currentState.selection.nodeId);
|
|
971
|
+
pageDetailsElement.textContent = `${activePage.path} • ${activePage.nodes.length} nodes`;
|
|
972
|
+
const rootNodes = getRootNodes(activePage);
|
|
973
|
+
if (rootNodes.length === 0) {
|
|
974
|
+
const empty = document.createElement("div");
|
|
975
|
+
empty.className = "canvas-empty-inline";
|
|
976
|
+
empty.textContent = "No nodes on this page.";
|
|
977
|
+
layersTreeElement.append(empty);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
for (const node of rootNodes) {
|
|
981
|
+
layersTreeElement.append(renderLayerNode(activePage, node, 0));
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
function renderLayerNode(page, node, depth) {
|
|
985
|
+
const row = document.createElement("div");
|
|
986
|
+
row.className = "canvas-layer-row";
|
|
987
|
+
row.dataset.nodeId = node.id;
|
|
988
|
+
row.dataset.selected = String(currentState?.selection.nodeId === node.id);
|
|
989
|
+
row.dataset.hidden = String(isNodeHidden(node));
|
|
990
|
+
row.draggable = true;
|
|
991
|
+
row.style.marginLeft = `${depth * 14}px`;
|
|
992
|
+
const head = document.createElement("div");
|
|
993
|
+
head.className = "canvas-layer-head";
|
|
994
|
+
const toggleButton = document.createElement("button");
|
|
995
|
+
toggleButton.className = "canvas-button canvas-layer-toggle";
|
|
996
|
+
toggleButton.type = "button";
|
|
997
|
+
toggleButton.textContent = node.childIds.length > 0 ? (expandedLayerNodeIds.has(node.id) ? "−" : "+") : "·";
|
|
998
|
+
toggleButton.disabled = node.childIds.length === 0;
|
|
999
|
+
toggleButton.addEventListener("click", (event) => {
|
|
1000
|
+
event.stopPropagation();
|
|
1001
|
+
if (expandedLayerNodeIds.has(node.id)) {
|
|
1002
|
+
expandedLayerNodeIds.delete(node.id);
|
|
1003
|
+
}
|
|
1004
|
+
else {
|
|
1005
|
+
expandedLayerNodeIds.add(node.id);
|
|
1006
|
+
}
|
|
1007
|
+
renderPagesAndLayers();
|
|
1008
|
+
});
|
|
1009
|
+
const visibilityButton = document.createElement("button");
|
|
1010
|
+
visibilityButton.className = "canvas-button canvas-layer-visibility";
|
|
1011
|
+
visibilityButton.type = "button";
|
|
1012
|
+
visibilityButton.textContent = isNodeHidden(node) ? "🙈" : "👁";
|
|
1013
|
+
visibilityButton.addEventListener("click", (event) => {
|
|
1014
|
+
event.stopPropagation();
|
|
1015
|
+
if (!currentState?.pendingMutation) {
|
|
1016
|
+
applyOptimisticPatch([{ op: "node.visibility.set", nodeId: node.id, hidden: !isNodeHidden(node) }], {
|
|
1017
|
+
pageId: page.id,
|
|
1018
|
+
nodeId: node.id,
|
|
1019
|
+
targetId: currentState?.selection.targetId ?? null
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
const main = document.createElement("div");
|
|
1024
|
+
main.className = "canvas-layer-main";
|
|
1025
|
+
main.addEventListener("click", () => {
|
|
1026
|
+
selectNode(node.id, page.id);
|
|
1027
|
+
});
|
|
1028
|
+
const renameInput = document.createElement("input");
|
|
1029
|
+
renameInput.className = "canvas-layer-name-input";
|
|
1030
|
+
renameInput.value = node.name;
|
|
1031
|
+
renameInput.disabled = Boolean(currentState?.pendingMutation);
|
|
1032
|
+
renameInput.addEventListener("click", (event) => {
|
|
1033
|
+
event.stopPropagation();
|
|
1034
|
+
});
|
|
1035
|
+
renameInput.addEventListener("change", () => {
|
|
1036
|
+
if (!currentState?.pendingMutation && renameInput.value.trim().length > 0 && renameInput.value !== node.name) {
|
|
1037
|
+
applyOptimisticPatch([{ op: "node.update", nodeId: node.id, changes: { name: renameInput.value.trim() } }], {
|
|
1038
|
+
pageId: page.id,
|
|
1039
|
+
nodeId: node.id,
|
|
1040
|
+
targetId: currentState?.selection.targetId ?? null
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
const meta = document.createElement("div");
|
|
1045
|
+
meta.className = "canvas-layer-meta";
|
|
1046
|
+
meta.textContent = [node.kind, node.childIds.length > 0 ? `${node.childIds.length} children` : "leaf", isNodeHidden(node) ? "hidden" : "visible"].join(" • ");
|
|
1047
|
+
main.append(renameInput, meta);
|
|
1048
|
+
head.append(toggleButton, visibilityButton, main);
|
|
1049
|
+
row.append(head);
|
|
1050
|
+
row.addEventListener("dragstart", (event) => {
|
|
1051
|
+
layerDragState = { nodeId: node.id };
|
|
1052
|
+
event.dataTransfer?.setData("text/plain", node.id);
|
|
1053
|
+
});
|
|
1054
|
+
row.addEventListener("dragend", () => {
|
|
1055
|
+
layerDragState = null;
|
|
1056
|
+
});
|
|
1057
|
+
row.addEventListener("dragover", (event) => {
|
|
1058
|
+
event.preventDefault();
|
|
1059
|
+
});
|
|
1060
|
+
row.addEventListener("drop", (event) => {
|
|
1061
|
+
event.preventDefault();
|
|
1062
|
+
const draggedId = layerDragState?.nodeId;
|
|
1063
|
+
if (!draggedId || draggedId === node.id) {
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
commitLayerMove(draggedId, node.parentId ?? null, findSiblingInsertIndex(page, node.id));
|
|
1067
|
+
});
|
|
1068
|
+
if (node.childIds.length > 0) {
|
|
1069
|
+
const children = document.createElement("div");
|
|
1070
|
+
children.className = "canvas-layer-children";
|
|
1071
|
+
children.addEventListener("dragover", (event) => {
|
|
1072
|
+
event.preventDefault();
|
|
1073
|
+
});
|
|
1074
|
+
children.addEventListener("drop", (event) => {
|
|
1075
|
+
event.preventDefault();
|
|
1076
|
+
const draggedId = layerDragState?.nodeId;
|
|
1077
|
+
if (!draggedId || draggedId === node.id) {
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
commitLayerMove(draggedId, node.id, node.childIds.length);
|
|
1081
|
+
});
|
|
1082
|
+
if (expandedLayerNodeIds.has(node.id)) {
|
|
1083
|
+
for (const childId of node.childIds) {
|
|
1084
|
+
const child = page.nodes.find((entry) => entry.id === childId);
|
|
1085
|
+
if (child) {
|
|
1086
|
+
children.append(renderLayerNode(page, child, depth + 1));
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
row.append(children);
|
|
1091
|
+
}
|
|
1092
|
+
return row;
|
|
1093
|
+
}
|
|
1094
|
+
function renderTokenSummary() {
|
|
1095
|
+
populateTokenControls();
|
|
1096
|
+
tokenSummaryElement.innerHTML = "";
|
|
1097
|
+
tokenUsageElement.innerHTML = "";
|
|
1098
|
+
const controlsDisabled = !currentState || Boolean(currentState.pendingMutation);
|
|
1099
|
+
for (const control of [
|
|
1100
|
+
tokenCollectionSelect,
|
|
1101
|
+
tokenCollectionNameInput,
|
|
1102
|
+
tokenCollectionCreateButton,
|
|
1103
|
+
tokenModeSelect,
|
|
1104
|
+
tokenModeNameInput,
|
|
1105
|
+
tokenModeCreateButton,
|
|
1106
|
+
tokenPathInput,
|
|
1107
|
+
tokenValueInput,
|
|
1108
|
+
tokenAliasInput,
|
|
1109
|
+
tokenBindingPropertySelect,
|
|
1110
|
+
tokenSaveButton,
|
|
1111
|
+
tokenBindButton,
|
|
1112
|
+
tokenBindingClearButton
|
|
1113
|
+
]) {
|
|
1114
|
+
control.disabled = controlsDisabled;
|
|
1115
|
+
}
|
|
1116
|
+
if (!currentState) {
|
|
1117
|
+
tokenStatusElement.textContent = "Waiting for canvas state...";
|
|
1118
|
+
const empty = document.createElement("div");
|
|
1119
|
+
empty.className = "canvas-empty-inline";
|
|
1120
|
+
empty.textContent = "Waiting for canvas state...";
|
|
1121
|
+
tokenSummaryElement.append(empty.cloneNode(true));
|
|
1122
|
+
tokenUsageElement.append(empty);
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
const node = getSelectedNode();
|
|
1126
|
+
const normalizedPath = normalizeTokenPath(selectedTokenPath || tokenPathInput.value);
|
|
1127
|
+
const resolvedValue = normalizedPath
|
|
1128
|
+
? resolveTokenValue(currentState.document.tokens, normalizedPath, selectedTokenModeId === "__base__" ? null : selectedTokenModeId)
|
|
1129
|
+
: null;
|
|
1130
|
+
const aliasTarget = normalizedPath
|
|
1131
|
+
? readTokenAliasTarget(currentState.document.tokens, normalizedPath, selectedTokenModeId === "__base__" ? null : selectedTokenModeId)
|
|
1132
|
+
: null;
|
|
1133
|
+
const bindingProperty = tokenBindingPropertySelect.value || "backgroundColor";
|
|
1134
|
+
tokenPathInput.value = selectedTokenPath;
|
|
1135
|
+
tokenValueInput.value = formatTokenEditorValue(resolvedValue);
|
|
1136
|
+
tokenAliasInput.value = aliasTarget ?? "";
|
|
1137
|
+
tokenBindButton.disabled = controlsDisabled || !node || normalizedPath.length === 0;
|
|
1138
|
+
tokenBindingClearButton.disabled = controlsDisabled || !node || !hasSelectedTokenBinding(node, bindingProperty);
|
|
1139
|
+
tokenStatusElement.textContent = normalizedPath
|
|
1140
|
+
? aliasTarget
|
|
1141
|
+
? `${normalizedPath} aliases ${aliasTarget}${resolvedValue !== null ? ` • ${formatTokenEditorValue(resolvedValue)}` : ""}`
|
|
1142
|
+
: `${normalizedPath}${resolvedValue !== null ? ` • ${formatTokenEditorValue(resolvedValue)}` : ""}`
|
|
1143
|
+
: "Edit collections, modes, aliases, and bindings.";
|
|
1144
|
+
if (!node) {
|
|
1145
|
+
const empty = document.createElement("div");
|
|
1146
|
+
empty.className = "canvas-empty-inline";
|
|
1147
|
+
empty.textContent = "Select a node to inspect token usage.";
|
|
1148
|
+
tokenSummaryElement.append(empty);
|
|
1149
|
+
}
|
|
1150
|
+
else {
|
|
1151
|
+
const tokenRefs = Object.entries(isRecord(node.tokenRefs) ? node.tokenRefs : {});
|
|
1152
|
+
if (tokenRefs.length === 0) {
|
|
1153
|
+
const empty = document.createElement("div");
|
|
1154
|
+
empty.className = "canvas-empty-inline";
|
|
1155
|
+
empty.textContent = "No token bindings on this node.";
|
|
1156
|
+
tokenSummaryElement.append(empty);
|
|
1157
|
+
}
|
|
1158
|
+
else {
|
|
1159
|
+
for (const [property, value] of tokenRefs) {
|
|
1160
|
+
const row = document.createElement("div");
|
|
1161
|
+
row.className = "canvas-token-item";
|
|
1162
|
+
const title = document.createElement("div");
|
|
1163
|
+
title.className = "canvas-summary-label";
|
|
1164
|
+
title.textContent = property;
|
|
1165
|
+
const body = document.createElement("div");
|
|
1166
|
+
body.className = "canvas-summary-value";
|
|
1167
|
+
const tokenPath = readTokenPath(value);
|
|
1168
|
+
const resolved = tokenPath
|
|
1169
|
+
? resolveTokenValue(currentState.document.tokens, tokenPath, readActiveTokenModeId(currentState.document.tokens))
|
|
1170
|
+
: null;
|
|
1171
|
+
body.textContent = tokenPath
|
|
1172
|
+
? resolved !== null ? `${tokenPath} → ${formatTokenEditorValue(resolved)}` : tokenPath
|
|
1173
|
+
: JSON.stringify(value);
|
|
1174
|
+
row.append(title, body);
|
|
1175
|
+
tokenSummaryElement.append(row);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
const usages = normalizedPath ? collectTokenUsages(currentState.document, normalizedPath) : [];
|
|
1180
|
+
if (usages.length === 0) {
|
|
1181
|
+
const empty = document.createElement("div");
|
|
1182
|
+
empty.className = "canvas-empty-inline";
|
|
1183
|
+
empty.textContent = normalizedPath ? "No bound nodes use this token yet." : "Choose a token path to inspect usage.";
|
|
1184
|
+
tokenUsageElement.append(empty);
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
for (const usage of usages) {
|
|
1188
|
+
const row = document.createElement("div");
|
|
1189
|
+
row.className = "canvas-summary-item";
|
|
1190
|
+
const title = document.createElement("div");
|
|
1191
|
+
title.className = "canvas-summary-label";
|
|
1192
|
+
title.textContent = `${usage.pageName} • ${usage.property}`;
|
|
1193
|
+
const body = document.createElement("div");
|
|
1194
|
+
body.className = "canvas-summary-value";
|
|
1195
|
+
body.textContent = `${usage.nodeName} (${usage.nodeId})${usage.resolvedValue !== null ? ` → ${formatTokenEditorValue(usage.resolvedValue)}` : ""}`;
|
|
1196
|
+
row.append(title, body);
|
|
1197
|
+
tokenUsageElement.append(row);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
function populateTokenControls() {
|
|
1201
|
+
if (!currentState) {
|
|
1202
|
+
tokenCollectionSelect.innerHTML = "";
|
|
1203
|
+
tokenModeSelect.innerHTML = "";
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
const tokens = currentState.document.tokens;
|
|
1207
|
+
const collections = [
|
|
1208
|
+
{ id: "__values__", name: "Values" },
|
|
1209
|
+
...tokens.collections.map((collection) => ({ id: collection.id, name: collection.name }))
|
|
1210
|
+
];
|
|
1211
|
+
if (!collections.some((entry) => entry.id === selectedTokenCollectionId)) {
|
|
1212
|
+
selectedTokenCollectionId = findTokenCollectionId(tokens, selectedTokenPath) ?? "__values__";
|
|
1213
|
+
}
|
|
1214
|
+
tokenCollectionSelect.innerHTML = collections
|
|
1215
|
+
.map((collection) => `<option value="${escapeHtmlAttribute(collection.id)}">${escapeHtmlText(collection.name)}</option>`)
|
|
1216
|
+
.join("");
|
|
1217
|
+
tokenCollectionSelect.value = selectedTokenCollectionId;
|
|
1218
|
+
const modes = listTokenModes(tokens, selectedTokenCollectionId);
|
|
1219
|
+
if (!modes.some((entry) => entry.id === selectedTokenModeId)) {
|
|
1220
|
+
const activeModeId = readActiveTokenModeId(tokens);
|
|
1221
|
+
selectedTokenModeId = activeModeId && modes.some((entry) => entry.id === activeModeId) ? activeModeId : "__base__";
|
|
1222
|
+
}
|
|
1223
|
+
tokenModeSelect.innerHTML = modes
|
|
1224
|
+
.map((mode) => `<option value="${escapeHtmlAttribute(mode.id)}">${escapeHtmlText(mode.name)}</option>`)
|
|
1225
|
+
.join("");
|
|
1226
|
+
tokenModeSelect.value = selectedTokenModeId;
|
|
1227
|
+
}
|
|
1228
|
+
function renderStage() {
|
|
1229
|
+
if (!currentState) {
|
|
1230
|
+
stageInnerElement.innerHTML = "";
|
|
1231
|
+
renderStageOverlay();
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
const page = getActivePage();
|
|
1235
|
+
if (!page) {
|
|
1236
|
+
stageInnerElement.innerHTML = "";
|
|
1237
|
+
renderStageOverlay();
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
const visibleNodes = page.nodes.filter((node) => !isNodeHidden(node));
|
|
1241
|
+
const bounds = computeDocumentBounds(visibleNodes);
|
|
1242
|
+
stageInnerElement.style.width = `${bounds.width}px`;
|
|
1243
|
+
stageInnerElement.style.height = `${bounds.height}px`;
|
|
1244
|
+
stageInnerElement.style.transform = `translate(${currentState.viewport.x}px, ${currentState.viewport.y}px) scale(${currentState.viewport.zoom})`;
|
|
1245
|
+
stageInnerElement.innerHTML = "";
|
|
1246
|
+
const sortedNodes = [...visibleNodes].sort(compareStageNodes);
|
|
1247
|
+
for (const node of sortedNodes) {
|
|
1248
|
+
stageInnerElement.append(buildStageNodeElement(currentState.document, node, currentState.selection.nodeId === node.id));
|
|
1249
|
+
}
|
|
1250
|
+
renderStageOverlay();
|
|
1251
|
+
}
|
|
1252
|
+
function compareStageNodes(left, right) {
|
|
1253
|
+
const rootOrder = Number(left.parentId !== null) - Number(right.parentId !== null);
|
|
1254
|
+
if (rootOrder !== 0) {
|
|
1255
|
+
return rootOrder;
|
|
1256
|
+
}
|
|
1257
|
+
const areaOrder = (right.rect.width * right.rect.height) - (left.rect.width * left.rect.height);
|
|
1258
|
+
if (areaOrder !== 0) {
|
|
1259
|
+
return areaOrder;
|
|
1260
|
+
}
|
|
1261
|
+
const verticalOrder = left.rect.y - right.rect.y;
|
|
1262
|
+
return verticalOrder !== 0 ? verticalOrder : left.rect.x - right.rect.x;
|
|
1263
|
+
}
|
|
1264
|
+
function buildStageNodeElement(documentState, node, selected) {
|
|
1265
|
+
const binding = resolveStageBinding(documentState, node);
|
|
1266
|
+
const componentKind = resolveStageComponentKind(node, binding);
|
|
1267
|
+
const media = resolveStageMediaDescriptor(documentState, node);
|
|
1268
|
+
const text = nodeText(node);
|
|
1269
|
+
const tag = componentKind === "button"
|
|
1270
|
+
? "button"
|
|
1271
|
+
: media
|
|
1272
|
+
? "div"
|
|
1273
|
+
: node.kind === "text"
|
|
1274
|
+
? "p"
|
|
1275
|
+
: node.kind === "note"
|
|
1276
|
+
? "aside"
|
|
1277
|
+
: "div";
|
|
1278
|
+
const element = document.createElement(tag);
|
|
1279
|
+
element.className = [
|
|
1280
|
+
"canvas-node",
|
|
1281
|
+
componentKind ? `canvas-node-${componentKind}` : "",
|
|
1282
|
+
binding?.sourceKind ? `canvas-node-source-${binding.sourceKind.replace(/[^a-z0-9]+/gi, "-").toLowerCase()}` : ""
|
|
1283
|
+
].filter(Boolean).join(" ");
|
|
1284
|
+
element.dataset.nodeId = node.id;
|
|
1285
|
+
element.dataset.selected = String(selected);
|
|
1286
|
+
const accessibleLabel = describeStageNode(node, componentKind, media, text);
|
|
1287
|
+
element.title = accessibleLabel;
|
|
1288
|
+
element.setAttribute("aria-label", selected ? `${accessibleLabel}, selected` : accessibleLabel);
|
|
1289
|
+
element.setAttribute("aria-roledescription", componentKind ?? node.kind);
|
|
1290
|
+
if (element instanceof HTMLButtonElement) {
|
|
1291
|
+
element.type = "button";
|
|
1292
|
+
}
|
|
1293
|
+
else {
|
|
1294
|
+
element.setAttribute("role", "button");
|
|
1295
|
+
element.tabIndex = 0;
|
|
1296
|
+
}
|
|
1297
|
+
element.style.left = `${node.rect.x}px`;
|
|
1298
|
+
element.style.top = `${node.rect.y}px`;
|
|
1299
|
+
element.style.width = `${Math.max(node.rect.width, 40)}px`;
|
|
1300
|
+
element.style.minHeight = `${Math.max(node.rect.height, node.kind === "connector" ? 2 : componentKind === "badge" ? 28 : 40)}px`;
|
|
1301
|
+
applyDeclaredStageStyles(element, documentState, node);
|
|
1302
|
+
if (text.includes("\n") && element.style.whiteSpace.length === 0) {
|
|
1303
|
+
element.style.whiteSpace = "pre-line";
|
|
1304
|
+
}
|
|
1305
|
+
const icons = readStageIcons(node);
|
|
1306
|
+
if (componentKind === "button" || componentKind === "badge" || componentKind === "tabs") {
|
|
1307
|
+
appendStageButtonLikeContent(element, text || node.name, icons, componentKind);
|
|
1308
|
+
return element;
|
|
1309
|
+
}
|
|
1310
|
+
if (componentKind === "card" || componentKind === "dialog" || componentKind === "motion") {
|
|
1311
|
+
appendStageCardContent(element, text || node.name, icons);
|
|
1312
|
+
return element;
|
|
1313
|
+
}
|
|
1314
|
+
if (media) {
|
|
1315
|
+
appendStageMediaContent(element, media, text || node.name);
|
|
1316
|
+
return element;
|
|
1317
|
+
}
|
|
1318
|
+
if (node.kind === "connector") {
|
|
1319
|
+
return element;
|
|
1320
|
+
}
|
|
1321
|
+
appendStageTextContent(element, text || node.name);
|
|
1322
|
+
return element;
|
|
1323
|
+
}
|
|
1324
|
+
function describeStageNode(node, componentKind, media, text) {
|
|
1325
|
+
const parts = [
|
|
1326
|
+
node.name.trim(),
|
|
1327
|
+
text.trim() && text.trim() !== node.name.trim() ? text.trim() : "",
|
|
1328
|
+
media ? media.kind : "",
|
|
1329
|
+
componentKind ?? node.kind
|
|
1330
|
+
].filter((value) => value.length > 0);
|
|
1331
|
+
return parts.join(" • ");
|
|
1332
|
+
}
|
|
1333
|
+
function resolveStageBinding(documentState, node) {
|
|
1334
|
+
const bindingId = typeof node.bindingRefs.primary === "string" ? node.bindingRefs.primary : null;
|
|
1335
|
+
if (!bindingId) {
|
|
1336
|
+
return null;
|
|
1337
|
+
}
|
|
1338
|
+
const binding = documentState.bindings.find((entry) => entry.id === bindingId);
|
|
1339
|
+
if (!binding) {
|
|
1340
|
+
return null;
|
|
1341
|
+
}
|
|
1342
|
+
const metadata = isRecord(binding.metadata) ? binding.metadata : {};
|
|
1343
|
+
return {
|
|
1344
|
+
componentName: typeof binding.componentName === "string" ? binding.componentName : null,
|
|
1345
|
+
sourceKind: typeof metadata.sourceKind === "string" ? metadata.sourceKind : null
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
function resolveStageComponentKind(node, binding) {
|
|
1349
|
+
const componentName = binding?.componentName?.toLowerCase() ?? "";
|
|
1350
|
+
if (componentName.includes("button")) {
|
|
1351
|
+
return "button";
|
|
1352
|
+
}
|
|
1353
|
+
if (componentName.includes("badge")) {
|
|
1354
|
+
return "badge";
|
|
1355
|
+
}
|
|
1356
|
+
if (componentName.includes("tabs")) {
|
|
1357
|
+
return "tabs";
|
|
1358
|
+
}
|
|
1359
|
+
if (componentName.includes("card")) {
|
|
1360
|
+
return "card";
|
|
1361
|
+
}
|
|
1362
|
+
if (componentName.includes("dialog")) {
|
|
1363
|
+
return "dialog";
|
|
1364
|
+
}
|
|
1365
|
+
if (componentName.includes("motion")) {
|
|
1366
|
+
return "motion";
|
|
1367
|
+
}
|
|
1368
|
+
if (node.kind !== "component-instance") {
|
|
1369
|
+
return null;
|
|
1370
|
+
}
|
|
1371
|
+
const lineCount = nodeText(node).split("\n").filter((entry) => entry.trim().length > 0).length;
|
|
1372
|
+
if (lineCount > 1 || node.rect.height >= 96) {
|
|
1373
|
+
return "card";
|
|
1374
|
+
}
|
|
1375
|
+
if (node.rect.height <= 56) {
|
|
1376
|
+
return "badge";
|
|
1377
|
+
}
|
|
1378
|
+
return "button";
|
|
1379
|
+
}
|
|
1380
|
+
function readStageIcons(node) {
|
|
1381
|
+
const refs = Array.isArray(node.metadata.iconRefs)
|
|
1382
|
+
? node.metadata.iconRefs
|
|
1383
|
+
: node.metadata.iconRef
|
|
1384
|
+
? [node.metadata.iconRef]
|
|
1385
|
+
: [];
|
|
1386
|
+
return refs.flatMap((entry) => {
|
|
1387
|
+
if (!isRecord(entry) || typeof entry.sourceLibrary !== "string") {
|
|
1388
|
+
return [];
|
|
1389
|
+
}
|
|
1390
|
+
return [{
|
|
1391
|
+
identifier: typeof entry.identifier === "string" ? entry.identifier : "generic",
|
|
1392
|
+
sourceLibrary: entry.sourceLibrary
|
|
1393
|
+
}];
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
function readStageAttributes(node) {
|
|
1397
|
+
return isRecord(node.props.attributes) ? node.props.attributes : {};
|
|
1398
|
+
}
|
|
1399
|
+
function resolveStageTagName(node) {
|
|
1400
|
+
if (typeof node.props.tagName === "string" && node.props.tagName.trim().length > 0) {
|
|
1401
|
+
return node.props.tagName.trim().toLowerCase();
|
|
1402
|
+
}
|
|
1403
|
+
const codeSync = isRecord(node.metadata.codeSync) ? node.metadata.codeSync : null;
|
|
1404
|
+
if (codeSync && typeof codeSync.tagName === "string" && codeSync.tagName.trim().length > 0) {
|
|
1405
|
+
return codeSync.tagName.trim().toLowerCase();
|
|
1406
|
+
}
|
|
1407
|
+
return null;
|
|
1408
|
+
}
|
|
1409
|
+
function resolveStageMediaDescriptor(documentState, node) {
|
|
1410
|
+
const tagName = resolveStageTagName(node);
|
|
1411
|
+
const attributes = readStageAttributes(node);
|
|
1412
|
+
const assetIds = Array.isArray(node.metadata.assetIds)
|
|
1413
|
+
? node.metadata.assetIds.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
|
|
1414
|
+
: [];
|
|
1415
|
+
const asset = assetIds.length > 0
|
|
1416
|
+
? documentState.assets.find((entry) => entry.id === assetIds[0])
|
|
1417
|
+
: null;
|
|
1418
|
+
const assetKind = typeof asset?.kind === "string" ? asset.kind.toLowerCase() : null;
|
|
1419
|
+
const assetMime = typeof asset?.mime === "string" ? asset.mime.toLowerCase() : null;
|
|
1420
|
+
const src = typeof node.props.src === "string"
|
|
1421
|
+
? node.props.src
|
|
1422
|
+
: typeof attributes.src === "string"
|
|
1423
|
+
? attributes.src
|
|
1424
|
+
: typeof asset?.url === "string"
|
|
1425
|
+
? asset.url
|
|
1426
|
+
: typeof asset?.repoPath === "string"
|
|
1427
|
+
? asset.repoPath
|
|
1428
|
+
: null;
|
|
1429
|
+
const poster = typeof node.props.poster === "string"
|
|
1430
|
+
? node.props.poster
|
|
1431
|
+
: typeof attributes.poster === "string"
|
|
1432
|
+
? attributes.poster
|
|
1433
|
+
: null;
|
|
1434
|
+
const alt = typeof node.props.alt === "string"
|
|
1435
|
+
? node.props.alt
|
|
1436
|
+
: typeof attributes.alt === "string"
|
|
1437
|
+
? attributes.alt
|
|
1438
|
+
: node.name;
|
|
1439
|
+
if (tagName === "img" || assetKind === "image" || assetMime?.startsWith("image/")) {
|
|
1440
|
+
return { kind: "image", src, poster: null, alt };
|
|
1441
|
+
}
|
|
1442
|
+
if (tagName === "video" || assetKind === "video" || assetMime?.startsWith("video/")) {
|
|
1443
|
+
return { kind: "video", src, poster, alt };
|
|
1444
|
+
}
|
|
1445
|
+
if (tagName === "audio" || assetKind === "audio" || assetMime?.startsWith("audio/")) {
|
|
1446
|
+
return { kind: "audio", src, poster: null, alt };
|
|
1447
|
+
}
|
|
1448
|
+
return null;
|
|
1449
|
+
}
|
|
1450
|
+
function appendStageMediaContent(element, media, label) {
|
|
1451
|
+
element.classList.add("canvas-node-media-shell");
|
|
1452
|
+
if (!media.src) {
|
|
1453
|
+
const placeholder = document.createElement("div");
|
|
1454
|
+
placeholder.className = "canvas-node-media-placeholder";
|
|
1455
|
+
placeholder.textContent = `${media.kind} source missing`;
|
|
1456
|
+
element.append(placeholder);
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
if (media.kind === "image") {
|
|
1460
|
+
const image = document.createElement("img");
|
|
1461
|
+
image.className = "canvas-node-media canvas-node-media-image";
|
|
1462
|
+
image.src = media.src;
|
|
1463
|
+
image.alt = media.alt ?? label;
|
|
1464
|
+
image.loading = "lazy";
|
|
1465
|
+
image.draggable = false;
|
|
1466
|
+
image.style.pointerEvents = "none";
|
|
1467
|
+
element.append(image);
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
if (media.kind === "video") {
|
|
1471
|
+
const video = document.createElement("video");
|
|
1472
|
+
video.className = "canvas-node-media canvas-node-media-video";
|
|
1473
|
+
video.src = media.src;
|
|
1474
|
+
if (media.poster) {
|
|
1475
|
+
video.poster = media.poster;
|
|
1476
|
+
}
|
|
1477
|
+
video.muted = true;
|
|
1478
|
+
video.loop = true;
|
|
1479
|
+
video.autoplay = true;
|
|
1480
|
+
video.playsInline = true;
|
|
1481
|
+
video.preload = "metadata";
|
|
1482
|
+
video.style.pointerEvents = "none";
|
|
1483
|
+
element.append(video);
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
const audio = document.createElement("audio");
|
|
1487
|
+
audio.className = "canvas-node-media canvas-node-media-audio";
|
|
1488
|
+
audio.src = media.src;
|
|
1489
|
+
audio.controls = true;
|
|
1490
|
+
audio.preload = "metadata";
|
|
1491
|
+
audio.style.pointerEvents = "none";
|
|
1492
|
+
element.append(audio);
|
|
1493
|
+
}
|
|
1494
|
+
function appendStageButtonLikeContent(element, text, icons, componentKind) {
|
|
1495
|
+
const row = document.createElement(componentKind === "tabs" ? "div" : "span");
|
|
1496
|
+
row.className = componentKind === "tabs" ? "canvas-node-tabs-trigger" : "canvas-node-row";
|
|
1497
|
+
for (const icon of icons) {
|
|
1498
|
+
row.append(buildStageIconElement(icon));
|
|
1499
|
+
}
|
|
1500
|
+
const label = document.createElement("span");
|
|
1501
|
+
label.className = "canvas-node-label";
|
|
1502
|
+
label.textContent = text;
|
|
1503
|
+
row.append(label);
|
|
1504
|
+
element.append(row);
|
|
1505
|
+
}
|
|
1506
|
+
function appendStageCardContent(element, text, icons) {
|
|
1507
|
+
const lines = text.split("\n").map((entry) => entry.trim()).filter(Boolean);
|
|
1508
|
+
const header = document.createElement("div");
|
|
1509
|
+
header.className = "canvas-node-card-header";
|
|
1510
|
+
const title = document.createElement("div");
|
|
1511
|
+
title.className = "canvas-node-card-title";
|
|
1512
|
+
title.textContent = lines[0] ?? text;
|
|
1513
|
+
header.append(title);
|
|
1514
|
+
if (icons.length > 0) {
|
|
1515
|
+
const iconWrap = document.createElement("span");
|
|
1516
|
+
iconWrap.className = "canvas-node-icon-stack";
|
|
1517
|
+
for (const icon of icons) {
|
|
1518
|
+
iconWrap.append(buildStageIconElement(icon));
|
|
1519
|
+
}
|
|
1520
|
+
header.append(iconWrap);
|
|
1521
|
+
}
|
|
1522
|
+
element.append(header);
|
|
1523
|
+
if (lines.length > 1) {
|
|
1524
|
+
const body = document.createElement("div");
|
|
1525
|
+
body.className = "canvas-node-card-copy";
|
|
1526
|
+
for (const line of lines.slice(1)) {
|
|
1527
|
+
const paragraph = document.createElement("p");
|
|
1528
|
+
paragraph.textContent = line;
|
|
1529
|
+
body.append(paragraph);
|
|
1530
|
+
}
|
|
1531
|
+
element.append(body);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
function appendStageTextContent(element, text) {
|
|
1535
|
+
const inlineItems = text.split(/\s{2,}/).map((entry) => entry.trim()).filter(Boolean);
|
|
1536
|
+
if (inlineItems.length > 1) {
|
|
1537
|
+
const list = document.createElement("div");
|
|
1538
|
+
list.className = "canvas-node-inline-list";
|
|
1539
|
+
for (const item of inlineItems) {
|
|
1540
|
+
const chip = document.createElement("span");
|
|
1541
|
+
chip.className = "canvas-node-inline-item";
|
|
1542
|
+
chip.textContent = item;
|
|
1543
|
+
list.append(chip);
|
|
1544
|
+
}
|
|
1545
|
+
element.append(list);
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
element.textContent = text;
|
|
1549
|
+
}
|
|
1550
|
+
function buildStageIconElement(icon) {
|
|
1551
|
+
const span = document.createElement("span");
|
|
1552
|
+
span.className = "canvas-node-icon";
|
|
1553
|
+
span.dataset.library = icon.sourceLibrary;
|
|
1554
|
+
if (icon.sourceLibrary === "tabler") {
|
|
1555
|
+
span.append(buildStageTablerIcon(icon.identifier));
|
|
1556
|
+
return span;
|
|
1557
|
+
}
|
|
1558
|
+
if (icon.sourceLibrary === "microsoft-fluent-ui-system-icons") {
|
|
1559
|
+
span.append(buildStageFluentIcon(icon.identifier));
|
|
1560
|
+
return span;
|
|
1561
|
+
}
|
|
1562
|
+
if (icon.sourceLibrary === "3dicons") {
|
|
1563
|
+
span.style.background = "linear-gradient(145deg, #7ef9e9 0%, #22c3ee 48%, #ff7aa2 100%)";
|
|
1564
|
+
span.style.boxShadow = "0 8px 18px rgba(34, 195, 238, 0.28)";
|
|
1565
|
+
span.textContent = "";
|
|
1566
|
+
return span;
|
|
1567
|
+
}
|
|
1568
|
+
span.textContent = icon.identifier.includes("party") ? "🎉" : "✨";
|
|
1569
|
+
return span;
|
|
1570
|
+
}
|
|
1571
|
+
function buildStageIconSvg() {
|
|
1572
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
1573
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
1574
|
+
svg.setAttribute("fill", "none");
|
|
1575
|
+
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
1576
|
+
svg.setAttribute("aria-hidden", "true");
|
|
1577
|
+
return svg;
|
|
1578
|
+
}
|
|
1579
|
+
function appendStageStrokePath(svg, attributes) {
|
|
1580
|
+
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1581
|
+
path.setAttribute("stroke", "currentColor");
|
|
1582
|
+
path.setAttribute("stroke-linecap", "round");
|
|
1583
|
+
path.setAttribute("stroke-linejoin", "round");
|
|
1584
|
+
path.setAttribute("stroke-width", "1.85");
|
|
1585
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
1586
|
+
path.setAttribute(key, value);
|
|
1587
|
+
}
|
|
1588
|
+
svg.append(path);
|
|
1589
|
+
}
|
|
1590
|
+
function buildStageTablerIcon(identifier) {
|
|
1591
|
+
const svg = buildStageIconSvg();
|
|
1592
|
+
switch (identifier) {
|
|
1593
|
+
case "arrow-right":
|
|
1594
|
+
appendStageStrokePath(svg, { d: "M5 12h14" });
|
|
1595
|
+
appendStageStrokePath(svg, { d: "m12 5 7 7-7 7" });
|
|
1596
|
+
return svg;
|
|
1597
|
+
case "rocket":
|
|
1598
|
+
appendStageStrokePath(svg, { d: "M5 19c2.5-6.5 7.5-11.5 14-14-2.5 6.5-7.5 11.5-14 14Z" });
|
|
1599
|
+
appendStageStrokePath(svg, { d: "m9 15-4 4" });
|
|
1600
|
+
{
|
|
1601
|
+
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
1602
|
+
circle.setAttribute("cx", "14.5");
|
|
1603
|
+
circle.setAttribute("cy", "9.5");
|
|
1604
|
+
circle.setAttribute("r", "1.75");
|
|
1605
|
+
circle.setAttribute("stroke", "currentColor");
|
|
1606
|
+
circle.setAttribute("stroke-width", "1.85");
|
|
1607
|
+
svg.append(circle);
|
|
1608
|
+
}
|
|
1609
|
+
return svg;
|
|
1610
|
+
case "components":
|
|
1611
|
+
case "layout-dashboard": {
|
|
1612
|
+
const shapes = identifier === "components"
|
|
1613
|
+
? [
|
|
1614
|
+
["4", "4", "7", "7"],
|
|
1615
|
+
["13", "4", "7", "7"],
|
|
1616
|
+
["8.5", "13", "7", "7"]
|
|
1617
|
+
]
|
|
1618
|
+
: [
|
|
1619
|
+
["4", "4", "7", "7"],
|
|
1620
|
+
["13", "4", "7", "5"],
|
|
1621
|
+
["13", "11", "7", "9"],
|
|
1622
|
+
["4", "13", "7", "7"]
|
|
1623
|
+
];
|
|
1624
|
+
for (const [x, y, width, height] of shapes) {
|
|
1625
|
+
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
1626
|
+
rect.setAttribute("x", x);
|
|
1627
|
+
rect.setAttribute("y", y);
|
|
1628
|
+
rect.setAttribute("width", width);
|
|
1629
|
+
rect.setAttribute("height", height);
|
|
1630
|
+
rect.setAttribute("rx", "2");
|
|
1631
|
+
rect.setAttribute("stroke", "currentColor");
|
|
1632
|
+
rect.setAttribute("stroke-width", "1.85");
|
|
1633
|
+
svg.append(rect);
|
|
1634
|
+
}
|
|
1635
|
+
return svg;
|
|
1636
|
+
}
|
|
1637
|
+
default:
|
|
1638
|
+
appendStageStrokePath(svg, { d: "M12 9v6" });
|
|
1639
|
+
appendStageStrokePath(svg, { d: "M9 12h6" });
|
|
1640
|
+
{
|
|
1641
|
+
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
1642
|
+
circle.setAttribute("cx", "12");
|
|
1643
|
+
circle.setAttribute("cy", "12");
|
|
1644
|
+
circle.setAttribute("r", "7");
|
|
1645
|
+
circle.setAttribute("stroke", "currentColor");
|
|
1646
|
+
circle.setAttribute("stroke-width", "1.85");
|
|
1647
|
+
svg.append(circle);
|
|
1648
|
+
}
|
|
1649
|
+
return svg;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
function buildStageFluentIcon(identifier) {
|
|
1653
|
+
const svg = buildStageIconSvg();
|
|
1654
|
+
switch (identifier) {
|
|
1655
|
+
case "grid-dots-24":
|
|
1656
|
+
for (let index = 0; index < 9; index += 1) {
|
|
1657
|
+
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
1658
|
+
circle.setAttribute("cx", String(7 + (index % 3) * 5));
|
|
1659
|
+
circle.setAttribute("cy", String(7 + Math.floor(index / 3) * 5));
|
|
1660
|
+
circle.setAttribute("r", "1.4");
|
|
1661
|
+
circle.setAttribute("fill", "currentColor");
|
|
1662
|
+
svg.append(circle);
|
|
1663
|
+
}
|
|
1664
|
+
return svg;
|
|
1665
|
+
case "chat-bubbles-24": {
|
|
1666
|
+
const bubble = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1667
|
+
bubble.setAttribute("d", "M5.5 8.5a3.5 3.5 0 0 1 3.5-3.5h8a3.5 3.5 0 0 1 3.5 3.5v4A3.5 3.5 0 0 1 17 16H11l-4.5 3v-3.1A3.49 3.49 0 0 1 5.5 12.5v-4Z");
|
|
1668
|
+
bubble.setAttribute("stroke", "currentColor");
|
|
1669
|
+
bubble.setAttribute("stroke-width", "1.8");
|
|
1670
|
+
svg.append(bubble);
|
|
1671
|
+
const lines = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1672
|
+
lines.setAttribute("d", "M8 9.75h7M8 12.75h4.5");
|
|
1673
|
+
lines.setAttribute("stroke", "currentColor");
|
|
1674
|
+
lines.setAttribute("stroke-linecap", "round");
|
|
1675
|
+
lines.setAttribute("stroke-width", "1.8");
|
|
1676
|
+
svg.append(lines);
|
|
1677
|
+
return svg;
|
|
1678
|
+
}
|
|
1679
|
+
case "branch-24":
|
|
1680
|
+
appendStageStrokePath(svg, { d: "M8 7.5v9" });
|
|
1681
|
+
appendStageStrokePath(svg, { d: "M8 12.5h7" });
|
|
1682
|
+
appendStageStrokePath(svg, { d: "M15 12.5V7.5" });
|
|
1683
|
+
for (const [cx, cy] of [["8", "6"], ["15", "6"], ["15", "18"]]) {
|
|
1684
|
+
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
1685
|
+
circle.setAttribute("cx", cx);
|
|
1686
|
+
circle.setAttribute("cy", cy);
|
|
1687
|
+
circle.setAttribute("r", "2");
|
|
1688
|
+
circle.setAttribute("fill", "currentColor");
|
|
1689
|
+
svg.append(circle);
|
|
1690
|
+
}
|
|
1691
|
+
return svg;
|
|
1692
|
+
case "sparkle-24":
|
|
1693
|
+
default: {
|
|
1694
|
+
const sparkle = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1695
|
+
sparkle.setAttribute("d", "M12 3.5 14.4 9l5.6 2.4-5.6 2.4L12 19.5l-2.4-5.7L4 11.4 9.6 9 12 3.5Z");
|
|
1696
|
+
sparkle.setAttribute("fill", "currentColor");
|
|
1697
|
+
svg.append(sparkle);
|
|
1698
|
+
return svg;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
function applyDeclaredStageStyles(element, documentState, node) {
|
|
1703
|
+
for (const [key, value] of Object.entries(resolveStageStyle(documentState, node))) {
|
|
1704
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
1705
|
+
continue;
|
|
1706
|
+
}
|
|
1707
|
+
const cssName = key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
|
|
1708
|
+
const cssValue = typeof value === "number" && !UNIT_LESS_STYLES.has(key) ? `${value}px` : String(value);
|
|
1709
|
+
element.style.setProperty(cssName, cssValue);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
function renderInspector() {
|
|
1713
|
+
const node = getSelectedNode();
|
|
1714
|
+
const disabled = !node || Boolean(currentState?.pendingMutation);
|
|
1715
|
+
const inputs = [
|
|
1716
|
+
nameInput,
|
|
1717
|
+
textInput,
|
|
1718
|
+
nodeXInput,
|
|
1719
|
+
nodeYInput,
|
|
1720
|
+
nodeWidthInput,
|
|
1721
|
+
nodeHeightInput,
|
|
1722
|
+
paddingInput,
|
|
1723
|
+
gapInput,
|
|
1724
|
+
fontSizeInput,
|
|
1725
|
+
fontWeightInput,
|
|
1726
|
+
lineHeightInput,
|
|
1727
|
+
colorInput,
|
|
1728
|
+
backgroundInput,
|
|
1729
|
+
borderColorInput,
|
|
1730
|
+
borderWidthInput,
|
|
1731
|
+
borderRadiusInput,
|
|
1732
|
+
shadowInput,
|
|
1733
|
+
bindingKindInput,
|
|
1734
|
+
bindingComponentInput,
|
|
1735
|
+
bindingSelectorInput,
|
|
1736
|
+
a11yRoleInput,
|
|
1737
|
+
a11yLabelInput
|
|
1738
|
+
];
|
|
1739
|
+
for (const input of inputs) {
|
|
1740
|
+
input.disabled = disabled;
|
|
1741
|
+
}
|
|
1742
|
+
duplicateNodeButton.disabled = disabled;
|
|
1743
|
+
deleteNodeButton.disabled = disabled;
|
|
1744
|
+
propertiesStatusElement.textContent = node
|
|
1745
|
+
? `${node.kind} • ${node.rect.width}×${node.rect.height}`
|
|
1746
|
+
: "No node selected.";
|
|
1747
|
+
nameInput.value = node?.name ?? "";
|
|
1748
|
+
textInput.value = node ? nodeText(node) : "";
|
|
1749
|
+
nodeXInput.value = node ? String(node.rect.x) : "";
|
|
1750
|
+
nodeYInput.value = node ? String(node.rect.y) : "";
|
|
1751
|
+
nodeWidthInput.value = node ? String(node.rect.width) : "";
|
|
1752
|
+
nodeHeightInput.value = node ? String(node.rect.height) : "";
|
|
1753
|
+
paddingInput.value = node ? readStyleText(node.style.padding) : "";
|
|
1754
|
+
gapInput.value = node ? readStyleText(node.style.gap) : "";
|
|
1755
|
+
fontSizeInput.value = node ? readStyleText(node.style.fontSize) : "";
|
|
1756
|
+
fontWeightInput.value = node ? readStyleText(node.style.fontWeight) : "";
|
|
1757
|
+
lineHeightInput.value = node ? readStyleText(node.style.lineHeight) : "";
|
|
1758
|
+
colorInput.value = node ? readStyleText(node.style.color) : "";
|
|
1759
|
+
backgroundInput.value = node ? readStyleText(node.style.backgroundColor) : "";
|
|
1760
|
+
borderColorInput.value = node ? readStyleText(node.style.borderColor) : "";
|
|
1761
|
+
borderWidthInput.value = node ? readStyleText(node.style.borderWidth) : "";
|
|
1762
|
+
borderRadiusInput.value = node ? readStyleText(node.style.borderRadius) : "";
|
|
1763
|
+
shadowInput.value = node ? readStyleText(node.style.boxShadow) : "";
|
|
1764
|
+
const binding = currentState && node ? readSelectedBindingIdentity(currentState.document, node.id) : null;
|
|
1765
|
+
bindingKindInput.value = binding?.bindingKind ?? "";
|
|
1766
|
+
bindingComponentInput.value = binding?.componentName ?? "";
|
|
1767
|
+
bindingSelectorInput.value = node && currentState
|
|
1768
|
+
? readBindingSelector(currentState.document, node.id)
|
|
1769
|
+
: "";
|
|
1770
|
+
const accessibility = node && isRecord(node.metadata.accessibility) ? node.metadata.accessibility : {};
|
|
1771
|
+
a11yRoleInput.value = typeof accessibility.role === "string" ? accessibility.role : "";
|
|
1772
|
+
a11yLabelInput.value = typeof accessibility.label === "string" ? accessibility.label : "";
|
|
1773
|
+
renderSelectionMeta();
|
|
1774
|
+
syncTokenEditorSelection();
|
|
1775
|
+
}
|
|
1776
|
+
function renderSelectionMeta() {
|
|
1777
|
+
selectionMetaElement.innerHTML = "";
|
|
1778
|
+
const node = getSelectedNode();
|
|
1779
|
+
const bindingIdentity = currentState ? readSelectedBindingIdentity(currentState.document, node?.id ?? null) : null;
|
|
1780
|
+
const latestImport = currentState ? readLatestImportProvenance(currentState.summary, currentState.document) : null;
|
|
1781
|
+
const items = [
|
|
1782
|
+
["Selected", node?.id ?? "none"],
|
|
1783
|
+
["Page", getActivePage()?.name ?? "n/a"],
|
|
1784
|
+
["Position", node ? `${node.rect.x}, ${node.rect.y}` : "n/a"],
|
|
1785
|
+
["Size", node ? `${node.rect.width} × ${node.rect.height}` : "n/a"],
|
|
1786
|
+
["Binding", bindingIdentity?.componentName ?? "none"],
|
|
1787
|
+
["Framework", bindingIdentity?.framework ?? "n/a"],
|
|
1788
|
+
["Adapter", bindingIdentity?.adapter ?? "n/a"],
|
|
1789
|
+
["Plugin", bindingIdentity?.plugin ?? "n/a"],
|
|
1790
|
+
["Variants", node ? String(node.variantPatches.length) : "0"],
|
|
1791
|
+
["Latest import", latestImport ?? "none"]
|
|
1792
|
+
];
|
|
1793
|
+
for (const [label, value] of items) {
|
|
1794
|
+
const row = document.createElement("div");
|
|
1795
|
+
row.className = "canvas-summary-item";
|
|
1796
|
+
const title = document.createElement("div");
|
|
1797
|
+
title.className = "canvas-summary-label";
|
|
1798
|
+
title.textContent = label;
|
|
1799
|
+
const body = document.createElement("div");
|
|
1800
|
+
body.className = "canvas-summary-value";
|
|
1801
|
+
body.textContent = value;
|
|
1802
|
+
row.append(title, body);
|
|
1803
|
+
selectionMetaElement.append(row);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
function renderAnnotationPanel() {
|
|
1807
|
+
const node = getSelectedNode();
|
|
1808
|
+
annotationAddButton.textContent = annotationMode === "region" ? "Capture Region" : "Add Selected";
|
|
1809
|
+
annotationAddButton.disabled = annotationMode === "selected"
|
|
1810
|
+
? (!node || !currentState || annotationDrafts.some((entry) => entry.kind !== "region" && entry.nodeId === node.id))
|
|
1811
|
+
: !currentState;
|
|
1812
|
+
annotationCopyButton.disabled = annotationDrafts.length === 0 || !currentState;
|
|
1813
|
+
annotationSendButton.disabled = annotationDrafts.length === 0 || !currentState;
|
|
1814
|
+
annotationListElement.innerHTML = "";
|
|
1815
|
+
if (!currentState || annotationDrafts.length === 0) {
|
|
1816
|
+
const empty = document.createElement("div");
|
|
1817
|
+
empty.className = "canvas-annotation-empty";
|
|
1818
|
+
empty.textContent = currentState ? "No canvas annotations captured yet." : "Waiting for canvas state...";
|
|
1819
|
+
annotationListElement.append(empty);
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
for (const draft of annotationDrafts) {
|
|
1823
|
+
const itemPayload = buildCanvasAnnotationPayloadForDrafts([draft]);
|
|
1824
|
+
const annotation = itemPayload?.annotations[0];
|
|
1825
|
+
const nodeLabel = annotation
|
|
1826
|
+
? describeAnnotationItem(annotation)
|
|
1827
|
+
: draft.kind === "region"
|
|
1828
|
+
? draft.label ?? draft.regionId
|
|
1829
|
+
: draft.nodeId;
|
|
1830
|
+
const row = document.createElement("div");
|
|
1831
|
+
row.className = "canvas-annotation-item";
|
|
1832
|
+
const head = document.createElement("div");
|
|
1833
|
+
head.className = "canvas-annotation-head";
|
|
1834
|
+
const summary = document.createElement("div");
|
|
1835
|
+
const title = document.createElement("div");
|
|
1836
|
+
title.className = "canvas-annotation-title";
|
|
1837
|
+
title.textContent = nodeLabel;
|
|
1838
|
+
const meta = document.createElement("div");
|
|
1839
|
+
meta.className = "canvas-annotation-meta";
|
|
1840
|
+
meta.textContent = annotation
|
|
1841
|
+
? `${annotation.tag} • ${Math.round(annotation.rect.width)}×${Math.round(annotation.rect.height)}`
|
|
1842
|
+
: draft.kind === "region"
|
|
1843
|
+
? `${Math.round(draft.rect.width)}×${Math.round(draft.rect.height)}`
|
|
1844
|
+
: draft.nodeId;
|
|
1845
|
+
summary.append(title, meta);
|
|
1846
|
+
const removeButton = document.createElement("button");
|
|
1847
|
+
removeButton.className = "canvas-button";
|
|
1848
|
+
removeButton.type = "button";
|
|
1849
|
+
removeButton.textContent = "Remove";
|
|
1850
|
+
removeButton.addEventListener("click", () => {
|
|
1851
|
+
removeAnnotationDraft(getDraftId(draft));
|
|
1852
|
+
});
|
|
1853
|
+
head.append(summary, removeButton);
|
|
1854
|
+
const noteField = document.createElement("textarea");
|
|
1855
|
+
noteField.className = "canvas-textarea";
|
|
1856
|
+
noteField.value = draft.note ?? "";
|
|
1857
|
+
noteField.placeholder = draft.kind === "region" ? "Add note for this captured region" : "Add note for this node";
|
|
1858
|
+
noteField.addEventListener("input", () => {
|
|
1859
|
+
updateAnnotationDraft(getDraftId(draft), { note: noteField.value });
|
|
1860
|
+
});
|
|
1861
|
+
const actions = document.createElement("div");
|
|
1862
|
+
actions.className = "canvas-actions-grid";
|
|
1863
|
+
const copyButton = document.createElement("button");
|
|
1864
|
+
copyButton.className = "canvas-button";
|
|
1865
|
+
copyButton.type = "button";
|
|
1866
|
+
copyButton.textContent = "Copy item";
|
|
1867
|
+
copyButton.addEventListener("click", () => {
|
|
1868
|
+
void copyCanvasAnnotation([draft], copyButton).catch((error) => {
|
|
1869
|
+
setCanvasButtonFeedback(copyButton, "Copy failed");
|
|
1870
|
+
console.error("[opendevbrowser canvas]", error);
|
|
1871
|
+
});
|
|
1872
|
+
});
|
|
1873
|
+
const sendButton = document.createElement("button");
|
|
1874
|
+
sendButton.className = "canvas-button";
|
|
1875
|
+
sendButton.type = "button";
|
|
1876
|
+
sendButton.textContent = "Send item";
|
|
1877
|
+
sendButton.addEventListener("click", () => {
|
|
1878
|
+
void sendCanvasAnnotation([draft], "canvas_item", nodeLabel, sendButton).catch((error) => {
|
|
1879
|
+
setCanvasButtonFeedback(sendButton, "Send failed");
|
|
1880
|
+
console.error("[opendevbrowser canvas]", error);
|
|
1881
|
+
});
|
|
1882
|
+
});
|
|
1883
|
+
actions.append(copyButton, sendButton);
|
|
1884
|
+
row.append(head, noteField, actions);
|
|
1885
|
+
annotationListElement.append(row);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
function buildCanvasAnnotationPayloadForDrafts(drafts) {
|
|
1889
|
+
if (!currentState) {
|
|
1890
|
+
return null;
|
|
1891
|
+
}
|
|
1892
|
+
const page = getActivePage();
|
|
1893
|
+
if (!page || drafts.length === 0) {
|
|
1894
|
+
return null;
|
|
1895
|
+
}
|
|
1896
|
+
return buildCanvasAnnotationPayload({
|
|
1897
|
+
document: currentState.document,
|
|
1898
|
+
page,
|
|
1899
|
+
drafts,
|
|
1900
|
+
context: annotationContextInput.value.trim() || undefined
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
function addSelectedAnnotationDraft() {
|
|
1904
|
+
const node = getSelectedNode();
|
|
1905
|
+
if (!node || annotationDrafts.some((entry) => entry.kind !== "region" && entry.nodeId === node.id)) {
|
|
1906
|
+
renderAnnotationPanel();
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
annotationDrafts = [...annotationDrafts, { kind: "node", nodeId: node.id, note: "" }];
|
|
1910
|
+
renderAnnotationPanel();
|
|
1911
|
+
}
|
|
1912
|
+
function updateAnnotationDraft(draftId, patch) {
|
|
1913
|
+
annotationDrafts = annotationDrafts.map((entry) => getDraftId(entry) === draftId
|
|
1914
|
+
? { ...entry, note: patch.note ?? entry.note }
|
|
1915
|
+
: entry);
|
|
1916
|
+
}
|
|
1917
|
+
function removeAnnotationDraft(draftId) {
|
|
1918
|
+
annotationDrafts = annotationDrafts.filter((entry) => getDraftId(entry) !== draftId);
|
|
1919
|
+
renderAnnotationPanel();
|
|
1920
|
+
}
|
|
1921
|
+
function syncAnnotationDrafts() {
|
|
1922
|
+
if (!currentState) {
|
|
1923
|
+
annotationDrafts = [];
|
|
1924
|
+
renderAnnotationPanel();
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
const page = getActivePage();
|
|
1928
|
+
const validIds = new Set(page?.nodes.map((node) => node.id) ?? []);
|
|
1929
|
+
const next = annotationDrafts.filter((entry) => entry.kind === "region" || validIds.has(entry.nodeId));
|
|
1930
|
+
if (next.length !== annotationDrafts.length) {
|
|
1931
|
+
annotationDrafts = next;
|
|
1932
|
+
}
|
|
1933
|
+
renderAnnotationPanel();
|
|
1934
|
+
}
|
|
1935
|
+
async function copyCanvasAnnotation(drafts, button) {
|
|
1936
|
+
const payload = buildCanvasAnnotationPayloadForDrafts(drafts ?? annotationDrafts);
|
|
1937
|
+
if (!payload) {
|
|
1938
|
+
setCanvasButtonFeedback(button, "No items");
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
await writeTextToClipboard(JSON.stringify(payload, null, 2));
|
|
1942
|
+
setCanvasButtonFeedback(button, "Copied");
|
|
1943
|
+
}
|
|
1944
|
+
async function sendCanvasAnnotation(drafts, source, label, button) {
|
|
1945
|
+
const payload = buildCanvasAnnotationPayloadForDrafts(drafts ?? annotationDrafts);
|
|
1946
|
+
if (!payload) {
|
|
1947
|
+
setCanvasButtonFeedback(button, "No items");
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
const response = await new Promise((resolve, reject) => {
|
|
1951
|
+
chrome.runtime.sendMessage({
|
|
1952
|
+
type: "annotation:sendPayload",
|
|
1953
|
+
payload,
|
|
1954
|
+
source,
|
|
1955
|
+
label
|
|
1956
|
+
}, (message) => {
|
|
1957
|
+
const lastError = chrome.runtime.lastError;
|
|
1958
|
+
if (lastError) {
|
|
1959
|
+
reject(new Error(lastError.message));
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
resolve(message);
|
|
1963
|
+
});
|
|
1964
|
+
});
|
|
1965
|
+
if (!response.ok) {
|
|
1966
|
+
throw new Error(response.error?.message ?? "Canvas annotation send failed.");
|
|
1967
|
+
}
|
|
1968
|
+
setCanvasButtonFeedback(button, formatAnnotationDispatchReceipt(response.receipt));
|
|
1969
|
+
}
|
|
1970
|
+
async function writeTextToClipboard(text) {
|
|
1971
|
+
if (navigator.clipboard?.writeText) {
|
|
1972
|
+
try {
|
|
1973
|
+
await navigator.clipboard.writeText(text);
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
catch {
|
|
1977
|
+
// Fall back to execCommand below.
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
const textarea = document.createElement("textarea");
|
|
1981
|
+
textarea.value = text;
|
|
1982
|
+
textarea.setAttribute("readonly", "true");
|
|
1983
|
+
textarea.style.position = "fixed";
|
|
1984
|
+
textarea.style.left = "-9999px";
|
|
1985
|
+
document.body.append(textarea);
|
|
1986
|
+
textarea.select();
|
|
1987
|
+
const ok = document.execCommand("copy");
|
|
1988
|
+
textarea.remove();
|
|
1989
|
+
if (!ok) {
|
|
1990
|
+
throw new Error("Clipboard copy failed");
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
function setCanvasButtonFeedback(button, label) {
|
|
1994
|
+
const original = button.dataset.originalLabel ?? button.textContent ?? "Action";
|
|
1995
|
+
if (!button.dataset.originalLabel) {
|
|
1996
|
+
button.dataset.originalLabel = original;
|
|
1997
|
+
}
|
|
1998
|
+
button.textContent = label;
|
|
1999
|
+
window.setTimeout(() => {
|
|
2000
|
+
button.textContent = original;
|
|
2001
|
+
}, 1500);
|
|
2002
|
+
}
|
|
2003
|
+
function getSelectedNode() {
|
|
2004
|
+
if (!currentState?.selection.nodeId) {
|
|
2005
|
+
return null;
|
|
2006
|
+
}
|
|
2007
|
+
return findNode(currentState.document, currentState.selection.nodeId);
|
|
2008
|
+
}
|
|
2009
|
+
function applyOptimisticPatch(patches, selection, options = {}) {
|
|
2010
|
+
if (!currentState || currentState.documentRevision === null || currentState.pendingMutation) {
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
if (!options.skipOptimisticMutation) {
|
|
2014
|
+
applyLocalPatchMutation(currentState.document, patches);
|
|
2015
|
+
}
|
|
2016
|
+
currentState.pendingMutation = true;
|
|
2017
|
+
currentState.selection = {
|
|
2018
|
+
pageId: selection?.pageId ?? currentState.selection.pageId,
|
|
2019
|
+
nodeId: selection?.nodeId ?? currentState.selection.nodeId,
|
|
2020
|
+
targetId: selection?.targetId ?? currentState.selection.targetId,
|
|
2021
|
+
updatedAt: new Date().toISOString()
|
|
2022
|
+
};
|
|
2023
|
+
activePageId = currentState.selection.pageId ?? activePageId;
|
|
2024
|
+
renderState();
|
|
2025
|
+
schedulePersist(currentState);
|
|
2026
|
+
port.postMessage({
|
|
2027
|
+
type: "canvas-page-patch-request",
|
|
2028
|
+
baseRevision: currentState.documentRevision,
|
|
2029
|
+
patches,
|
|
2030
|
+
selection: currentState.selection,
|
|
2031
|
+
viewport: currentState.viewport
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
function applyLocalPatchMutation(document, patches) {
|
|
2035
|
+
for (const patch of patches) {
|
|
2036
|
+
if (!isRecord(patch) || typeof patch.op !== "string") {
|
|
2037
|
+
continue;
|
|
2038
|
+
}
|
|
2039
|
+
if (patch.op === "node.insert") {
|
|
2040
|
+
const page = document.pages.find((entry) => entry.id === patch.pageId);
|
|
2041
|
+
const node = isRecord(patch.node) ? patch.node : null;
|
|
2042
|
+
if (!page || !node || typeof node.id !== "string" || typeof node.kind !== "string") {
|
|
2043
|
+
continue;
|
|
2044
|
+
}
|
|
2045
|
+
const inserted = {
|
|
2046
|
+
id: node.id,
|
|
2047
|
+
kind: node.kind,
|
|
2048
|
+
name: typeof node.name === "string" ? node.name : node.id,
|
|
2049
|
+
pageId: typeof patch.pageId === "string" ? patch.pageId : page.id,
|
|
2050
|
+
parentId: typeof patch.parentId === "string" ? patch.parentId : null,
|
|
2051
|
+
childIds: Array.isArray(node.childIds) ? node.childIds.filter((entry) => typeof entry === "string") : [],
|
|
2052
|
+
rect: isRecord(node.rect) ? {
|
|
2053
|
+
x: typeof node.rect.x === "number" ? node.rect.x : 0,
|
|
2054
|
+
y: typeof node.rect.y === "number" ? node.rect.y : 0,
|
|
2055
|
+
width: typeof node.rect.width === "number" ? node.rect.width : 240,
|
|
2056
|
+
height: typeof node.rect.height === "number" ? node.rect.height : 120
|
|
2057
|
+
} : { x: 0, y: 0, width: 240, height: 120 },
|
|
2058
|
+
props: isRecord(node.props) ? { ...node.props } : {},
|
|
2059
|
+
style: isRecord(node.style) ? { ...node.style } : {},
|
|
2060
|
+
tokenRefs: isRecord(node.tokenRefs) ? { ...node.tokenRefs } : {},
|
|
2061
|
+
bindingRefs: isRecord(node.bindingRefs) ? { ...node.bindingRefs } : {},
|
|
2062
|
+
variantPatches: Array.isArray(node.variantPatches) ? node.variantPatches.filter(isRecord) : [],
|
|
2063
|
+
metadata: isRecord(node.metadata) ? { ...node.metadata } : {}
|
|
2064
|
+
};
|
|
2065
|
+
page.nodes.push(inserted);
|
|
2066
|
+
if (inserted.parentId) {
|
|
2067
|
+
const parent = page.nodes.find((entry) => entry.id === inserted.parentId);
|
|
2068
|
+
if (parent && !parent.childIds.includes(inserted.id)) {
|
|
2069
|
+
parent.childIds.push(inserted.id);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
else {
|
|
2073
|
+
page.rootNodeId = inserted.id;
|
|
2074
|
+
}
|
|
2075
|
+
continue;
|
|
2076
|
+
}
|
|
2077
|
+
if (patch.op === "node.remove" && typeof patch.nodeId === "string") {
|
|
2078
|
+
for (const page of document.pages) {
|
|
2079
|
+
const removedIds = collectNodeSubtreeIds(page, patch.nodeId);
|
|
2080
|
+
if (removedIds.length === 0) {
|
|
2081
|
+
continue;
|
|
2082
|
+
}
|
|
2083
|
+
const removedSet = new Set(removedIds);
|
|
2084
|
+
page.nodes = page.nodes.filter((entry) => !removedSet.has(entry.id));
|
|
2085
|
+
for (const node of page.nodes) {
|
|
2086
|
+
node.childIds = node.childIds.filter((entry) => !removedSet.has(entry));
|
|
2087
|
+
}
|
|
2088
|
+
if (page.rootNodeId && removedSet.has(page.rootNodeId)) {
|
|
2089
|
+
page.rootNodeId = null;
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
continue;
|
|
2093
|
+
}
|
|
2094
|
+
if (patch.op === "node.update" && typeof patch.nodeId === "string" && isRecord(patch.changes)) {
|
|
2095
|
+
const node = findNode(document, patch.nodeId);
|
|
2096
|
+
if (!node) {
|
|
2097
|
+
continue;
|
|
2098
|
+
}
|
|
2099
|
+
for (const [path, value] of Object.entries(patch.changes)) {
|
|
2100
|
+
setNestedValue(node, path, value);
|
|
2101
|
+
}
|
|
2102
|
+
continue;
|
|
2103
|
+
}
|
|
2104
|
+
if (patch.op === "node.visibility.set" && typeof patch.nodeId === "string" && typeof patch.hidden === "boolean") {
|
|
2105
|
+
const node = findNode(document, patch.nodeId);
|
|
2106
|
+
if (!node) {
|
|
2107
|
+
continue;
|
|
2108
|
+
}
|
|
2109
|
+
const visibility = isRecord(node.metadata.visibility) ? { ...node.metadata.visibility } : {};
|
|
2110
|
+
visibility.hidden = patch.hidden;
|
|
2111
|
+
node.metadata.visibility = visibility;
|
|
2112
|
+
continue;
|
|
2113
|
+
}
|
|
2114
|
+
if (patch.op === "node.reorder" && typeof patch.nodeId === "string" && typeof patch.index === "number") {
|
|
2115
|
+
const page = findPageForNode(document, patch.nodeId);
|
|
2116
|
+
const node = page?.nodes.find((entry) => entry.id === patch.nodeId);
|
|
2117
|
+
if (!page || !node) {
|
|
2118
|
+
continue;
|
|
2119
|
+
}
|
|
2120
|
+
const siblings = getSiblingIds(page, node.parentId ?? null);
|
|
2121
|
+
const currentIndex = siblings.indexOf(node.id);
|
|
2122
|
+
if (currentIndex === -1) {
|
|
2123
|
+
continue;
|
|
2124
|
+
}
|
|
2125
|
+
siblings.splice(currentIndex, 1);
|
|
2126
|
+
const nextIndex = clampIndex(patch.index, siblings.length);
|
|
2127
|
+
siblings.splice(nextIndex, 0, node.id);
|
|
2128
|
+
assignSiblingIds(page, node.parentId ?? null, siblings);
|
|
2129
|
+
continue;
|
|
2130
|
+
}
|
|
2131
|
+
if (patch.op === "node.reparent" && typeof patch.nodeId === "string" && typeof patch.index === "number") {
|
|
2132
|
+
const page = findPageForNode(document, patch.nodeId);
|
|
2133
|
+
const node = page?.nodes.find((entry) => entry.id === patch.nodeId);
|
|
2134
|
+
if (!page || !node) {
|
|
2135
|
+
continue;
|
|
2136
|
+
}
|
|
2137
|
+
const previousParentId = node.parentId ?? null;
|
|
2138
|
+
const currentSiblings = getSiblingIds(page, previousParentId).filter((entry) => entry !== node.id);
|
|
2139
|
+
assignSiblingIds(page, previousParentId, currentSiblings);
|
|
2140
|
+
node.parentId = typeof patch.parentId === "string" ? patch.parentId : null;
|
|
2141
|
+
const nextSiblings = getSiblingIds(page, node.parentId ?? null);
|
|
2142
|
+
const nextIndex = clampIndex(patch.index, nextSiblings.length);
|
|
2143
|
+
nextSiblings.splice(nextIndex, 0, node.id);
|
|
2144
|
+
assignSiblingIds(page, node.parentId ?? null, nextSiblings);
|
|
2145
|
+
continue;
|
|
2146
|
+
}
|
|
2147
|
+
if (patch.op === "node.duplicate" && typeof patch.nodeId === "string") {
|
|
2148
|
+
const page = findPageForNode(document, patch.nodeId);
|
|
2149
|
+
const sourceNode = page?.nodes.find((entry) => entry.id === patch.nodeId);
|
|
2150
|
+
if (!page || !sourceNode) {
|
|
2151
|
+
continue;
|
|
2152
|
+
}
|
|
2153
|
+
const idMap = isRecord(patch.idMap) ? patch.idMap : {};
|
|
2154
|
+
const sourceIds = collectNodeSubtreeIds(page, sourceNode.id);
|
|
2155
|
+
for (const sourceId of sourceIds) {
|
|
2156
|
+
if (typeof idMap[sourceId] !== "string") {
|
|
2157
|
+
idMap[sourceId] = `node_${crypto.randomUUID().slice(0, 8)}`;
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
const clones = [];
|
|
2161
|
+
for (const sourceId of sourceIds) {
|
|
2162
|
+
const original = page.nodes.find((entry) => entry.id === sourceId);
|
|
2163
|
+
if (!original) {
|
|
2164
|
+
continue;
|
|
2165
|
+
}
|
|
2166
|
+
const cloneNode = structuredCloneNode(original);
|
|
2167
|
+
cloneNode.id = String(idMap[sourceId]);
|
|
2168
|
+
cloneNode.pageId = page.id;
|
|
2169
|
+
cloneNode.parentId = sourceId === sourceNode.id
|
|
2170
|
+
? (typeof patch.parentId === "string" ? patch.parentId : sourceNode.parentId)
|
|
2171
|
+
: (original.parentId ? String(idMap[original.parentId] ?? original.parentId) : null);
|
|
2172
|
+
cloneNode.childIds = original.childIds.map((childId) => String(idMap[childId] ?? childId));
|
|
2173
|
+
cloneNode.rect = {
|
|
2174
|
+
...cloneNode.rect,
|
|
2175
|
+
x: cloneNode.rect.x + 24,
|
|
2176
|
+
y: cloneNode.rect.y + 24
|
|
2177
|
+
};
|
|
2178
|
+
if (cloneNode.bindingRefs.primary && typeof cloneNode.bindingRefs.primary === "string") {
|
|
2179
|
+
cloneNode.bindingRefs.primary = `${cloneNode.bindingRefs.primary}_${cloneNode.id}`;
|
|
2180
|
+
}
|
|
2181
|
+
clones.push(cloneNode);
|
|
2182
|
+
}
|
|
2183
|
+
page.nodes.push(...clones);
|
|
2184
|
+
const duplicateRootId = String(idMap[sourceNode.id]);
|
|
2185
|
+
const parentId = typeof patch.parentId === "string" ? patch.parentId : sourceNode.parentId ?? null;
|
|
2186
|
+
const siblings = getSiblingIds(page, parentId);
|
|
2187
|
+
const nextIndex = clampIndex(typeof patch.index === "number" ? patch.index : siblings.length, siblings.length);
|
|
2188
|
+
siblings.splice(nextIndex, 0, duplicateRootId);
|
|
2189
|
+
assignSiblingIds(page, parentId, siblings);
|
|
2190
|
+
continue;
|
|
2191
|
+
}
|
|
2192
|
+
if (patch.op === "token.set" && typeof patch.path === "string") {
|
|
2193
|
+
setNestedValue(document.tokens.values, normalizeTokenPath(patch.path), patch.value);
|
|
2194
|
+
continue;
|
|
2195
|
+
}
|
|
2196
|
+
if (patch.op === "tokens.merge" && isRecord(patch.tokens)) {
|
|
2197
|
+
document.tokens = normalizeTokenStore({
|
|
2198
|
+
...structuredClone(document.tokens),
|
|
2199
|
+
...patch.tokens
|
|
2200
|
+
});
|
|
2201
|
+
continue;
|
|
2202
|
+
}
|
|
2203
|
+
if (patch.op === "tokens.replace" && isRecord(patch.tokens)) {
|
|
2204
|
+
document.tokens = normalizeTokenStore(patch.tokens);
|
|
2205
|
+
continue;
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
function getActivePage() {
|
|
2210
|
+
if (!currentState) {
|
|
2211
|
+
return null;
|
|
2212
|
+
}
|
|
2213
|
+
const pageId = activePageId ?? currentState.selection.pageId ?? currentState.document.pages[0]?.id ?? null;
|
|
2214
|
+
return currentState.document.pages.find((entry) => entry.id === pageId) ?? currentState.document.pages[0] ?? null;
|
|
2215
|
+
}
|
|
2216
|
+
function setActivePage(pageId, options = {}) {
|
|
2217
|
+
if (!currentState) {
|
|
2218
|
+
activePageId = pageId;
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
2221
|
+
const page = currentState.document.pages.find((entry) => entry.id === pageId) ?? currentState.document.pages[0] ?? null;
|
|
2222
|
+
activePageId = page?.id ?? null;
|
|
2223
|
+
if (!page) {
|
|
2224
|
+
renderState();
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
const selectedNode = currentState.selection.nodeId ? findNode(currentState.document, currentState.selection.nodeId) : null;
|
|
2228
|
+
if (!selectedNode || selectedNode.pageId !== page.id || options.clearSelectionIfMissing) {
|
|
2229
|
+
currentState.selection = {
|
|
2230
|
+
pageId: page.id,
|
|
2231
|
+
nodeId: selectedNode?.pageId === page.id ? selectedNode.id : null,
|
|
2232
|
+
targetId: currentState.selection.targetId,
|
|
2233
|
+
updatedAt: new Date().toISOString()
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
else {
|
|
2237
|
+
currentState.selection.pageId = page.id;
|
|
2238
|
+
}
|
|
2239
|
+
renderState();
|
|
2240
|
+
if (options.broadcast) {
|
|
2241
|
+
postViewState();
|
|
2242
|
+
schedulePersist(currentState);
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
function selectNode(nodeId, pageId) {
|
|
2246
|
+
if (!currentState) {
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
activePageId = pageId;
|
|
2250
|
+
currentState.selection = {
|
|
2251
|
+
pageId,
|
|
2252
|
+
nodeId,
|
|
2253
|
+
targetId: currentState.selection.targetId,
|
|
2254
|
+
updatedAt: new Date().toISOString()
|
|
2255
|
+
};
|
|
2256
|
+
renderState();
|
|
2257
|
+
postViewState();
|
|
2258
|
+
schedulePersist(currentState);
|
|
2259
|
+
}
|
|
2260
|
+
function requestHistory(direction) {
|
|
2261
|
+
if (!currentState || currentState.pendingMutation) {
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
const history = currentState.summary.history;
|
|
2265
|
+
if ((direction === "undo" && !history?.canUndo) || (direction === "redo" && !history?.canRedo)) {
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
currentState.pendingMutation = true;
|
|
2269
|
+
renderState();
|
|
2270
|
+
port.postMessage({
|
|
2271
|
+
type: "canvas-page-history-request",
|
|
2272
|
+
direction
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
function duplicateSelectedNode() {
|
|
2276
|
+
if (!currentState || currentState.pendingMutation) {
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
const node = getSelectedNode();
|
|
2280
|
+
const page = getActivePage();
|
|
2281
|
+
if (!node || !page) {
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
const siblingIds = getSiblingIds(page, node.parentId ?? null);
|
|
2285
|
+
const currentIndex = siblingIds.indexOf(node.id);
|
|
2286
|
+
const idMap = buildDuplicateIdMap(page, node.id);
|
|
2287
|
+
const duplicateRootId = idMap[node.id];
|
|
2288
|
+
if (!duplicateRootId) {
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
2291
|
+
applyOptimisticPatch([{
|
|
2292
|
+
op: "node.duplicate",
|
|
2293
|
+
nodeId: node.id,
|
|
2294
|
+
parentId: node.parentId ?? null,
|
|
2295
|
+
index: currentIndex >= 0 ? currentIndex + 1 : siblingIds.length,
|
|
2296
|
+
idMap
|
|
2297
|
+
}], {
|
|
2298
|
+
pageId: page.id,
|
|
2299
|
+
nodeId: duplicateRootId,
|
|
2300
|
+
targetId: currentState.selection.targetId
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
function nudgeSelectedNode(key, delta) {
|
|
2304
|
+
const node = getSelectedNode();
|
|
2305
|
+
if (!node || !currentState || currentState.pendingMutation) {
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
const changes = {};
|
|
2309
|
+
if (key === "ArrowUp") {
|
|
2310
|
+
changes["rect.y"] = node.rect.y - delta;
|
|
2311
|
+
}
|
|
2312
|
+
else if (key === "ArrowDown") {
|
|
2313
|
+
changes["rect.y"] = node.rect.y + delta;
|
|
2314
|
+
}
|
|
2315
|
+
else if (key === "ArrowLeft") {
|
|
2316
|
+
changes["rect.x"] = node.rect.x - delta;
|
|
2317
|
+
}
|
|
2318
|
+
else if (key === "ArrowRight") {
|
|
2319
|
+
changes["rect.x"] = node.rect.x + delta;
|
|
2320
|
+
}
|
|
2321
|
+
if (Object.keys(changes).length > 0) {
|
|
2322
|
+
applyOptimisticPatch([{ op: "node.update", nodeId: node.id, changes }], currentState.selection);
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
function fitActivePageViewport() {
|
|
2326
|
+
if (!currentState) {
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
currentState.viewport = resolvePreferredViewport(currentState);
|
|
2330
|
+
renderState();
|
|
2331
|
+
postViewState();
|
|
2332
|
+
schedulePersist(currentState);
|
|
2333
|
+
}
|
|
2334
|
+
function resetZoomToDefault() {
|
|
2335
|
+
if (!currentState) {
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
currentState.viewport = {
|
|
2339
|
+
...currentState.viewport,
|
|
2340
|
+
zoom: DEFAULT_EDITOR_VIEWPORT.zoom
|
|
2341
|
+
};
|
|
2342
|
+
renderState();
|
|
2343
|
+
postViewState();
|
|
2344
|
+
schedulePersist(currentState);
|
|
2345
|
+
}
|
|
2346
|
+
function commitSelectedNodeChanges(changes) {
|
|
2347
|
+
const node = getSelectedNode();
|
|
2348
|
+
if (!node || !currentState || currentState.pendingMutation) {
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
applyOptimisticPatch([{ op: "node.update", nodeId: node.id, changes }], currentState.selection);
|
|
2352
|
+
}
|
|
2353
|
+
function commitSelectedBindingPatch() {
|
|
2354
|
+
const node = getSelectedNode();
|
|
2355
|
+
if (!node || !currentState || currentState.pendingMutation) {
|
|
2356
|
+
return;
|
|
2357
|
+
}
|
|
2358
|
+
const page = getActivePage();
|
|
2359
|
+
if (!page) {
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2362
|
+
const bindingIdentity = readSelectedBindingIdentity(currentState.document, node.id);
|
|
2363
|
+
const bindingId = bindingIdentity.bindingId ?? `binding_${crypto.randomUUID().slice(0, 8)}`;
|
|
2364
|
+
applyOptimisticPatch([{
|
|
2365
|
+
op: "binding.set",
|
|
2366
|
+
nodeId: node.id,
|
|
2367
|
+
binding: {
|
|
2368
|
+
id: bindingId,
|
|
2369
|
+
kind: readTextInput(bindingKindInput) || bindingIdentity.bindingKind || "component",
|
|
2370
|
+
selector: readTextInput(bindingSelectorInput) || undefined,
|
|
2371
|
+
componentName: readTextInput(bindingComponentInput) || undefined,
|
|
2372
|
+
metadata: {
|
|
2373
|
+
...(readExistingBindingMetadata(currentState.document, bindingId) ?? {}),
|
|
2374
|
+
sourceKind: bindingIdentity.sourceKind ?? undefined
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
}], {
|
|
2378
|
+
pageId: page.id,
|
|
2379
|
+
nodeId: node.id,
|
|
2380
|
+
targetId: currentState.selection.targetId
|
|
2381
|
+
});
|
|
2382
|
+
}
|
|
2383
|
+
function commitLayerMove(nodeId, parentId, index) {
|
|
2384
|
+
if (!currentState || currentState.pendingMutation) {
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
const page = getActivePage();
|
|
2388
|
+
const node = findNode(currentState.document, nodeId);
|
|
2389
|
+
if (!page || !node || parentId === node.id) {
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
2392
|
+
const patch = node.parentId === parentId
|
|
2393
|
+
? { op: "node.reorder", nodeId, index }
|
|
2394
|
+
: { op: "node.reparent", nodeId, parentId, index };
|
|
2395
|
+
applyOptimisticPatch([patch], {
|
|
2396
|
+
pageId: page.id,
|
|
2397
|
+
nodeId,
|
|
2398
|
+
targetId: currentState.selection.targetId
|
|
2399
|
+
});
|
|
2400
|
+
}
|
|
2401
|
+
function commitAnnotationCapture(marquee) {
|
|
2402
|
+
const page = getActivePage();
|
|
2403
|
+
if (!currentState || !page) {
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
const dx = Math.abs(marquee.currentClientX - marquee.originClientX);
|
|
2407
|
+
const dy = Math.abs(marquee.currentClientY - marquee.originClientY);
|
|
2408
|
+
if (dx < 6 && dy < 6) {
|
|
2409
|
+
if (marquee.targetNodeId && !annotationDrafts.some((entry) => entry.kind !== "region" && entry.nodeId === marquee.targetNodeId)) {
|
|
2410
|
+
annotationDrafts = [...annotationDrafts, { kind: "node", nodeId: marquee.targetNodeId, note: "" }];
|
|
2411
|
+
renderAnnotationPanel();
|
|
2412
|
+
}
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2415
|
+
const rect = marqueeClientRectToCanvasRect(marquee);
|
|
2416
|
+
const intersectingNodes = page.nodes.filter((node) => rectsIntersect(node.rect, rect));
|
|
2417
|
+
const regionId = `region_${crypto.randomUUID().slice(0, 8)}`;
|
|
2418
|
+
annotationDrafts = [
|
|
2419
|
+
...annotationDrafts,
|
|
2420
|
+
{
|
|
2421
|
+
kind: "region",
|
|
2422
|
+
regionId,
|
|
2423
|
+
rect,
|
|
2424
|
+
pageId: page.id,
|
|
2425
|
+
label: intersectingNodes.length > 0 ? `Region • ${intersectingNodes.length} nodes` : "Region",
|
|
2426
|
+
note: ""
|
|
2427
|
+
}
|
|
2428
|
+
];
|
|
2429
|
+
renderAnnotationPanel();
|
|
2430
|
+
}
|
|
2431
|
+
function renderStageOverlay() {
|
|
2432
|
+
stageOverlayElement.innerHTML = "";
|
|
2433
|
+
if (!marqueeState) {
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
const marquee = document.createElement("div");
|
|
2437
|
+
marquee.className = "canvas-stage-marquee";
|
|
2438
|
+
const bounds = marqueeClientRectToStageRect(marqueeState);
|
|
2439
|
+
marquee.style.left = `${bounds.left}px`;
|
|
2440
|
+
marquee.style.top = `${bounds.top}px`;
|
|
2441
|
+
marquee.style.width = `${bounds.width}px`;
|
|
2442
|
+
marquee.style.height = `${bounds.height}px`;
|
|
2443
|
+
stageOverlayElement.append(marquee);
|
|
2444
|
+
}
|
|
2445
|
+
function bindFieldCommit(input, callback) {
|
|
2446
|
+
input.addEventListener("change", callback);
|
|
2447
|
+
input.addEventListener("keydown", (event) => {
|
|
2448
|
+
const keyboardEvent = event;
|
|
2449
|
+
if (keyboardEvent.key === "Enter" && !(input instanceof HTMLTextAreaElement && !keyboardEvent.metaKey && !keyboardEvent.ctrlKey)) {
|
|
2450
|
+
keyboardEvent.preventDefault();
|
|
2451
|
+
callback();
|
|
2452
|
+
}
|
|
2453
|
+
});
|
|
2454
|
+
}
|
|
2455
|
+
function readTextInput(input) {
|
|
2456
|
+
return input.value.trim();
|
|
2457
|
+
}
|
|
2458
|
+
function readNumberInput(input, fallback) {
|
|
2459
|
+
const value = Number(input.value);
|
|
2460
|
+
return Number.isFinite(value) ? value : fallback;
|
|
2461
|
+
}
|
|
2462
|
+
function readStyleText(value) {
|
|
2463
|
+
if (typeof value === "string") {
|
|
2464
|
+
return value;
|
|
2465
|
+
}
|
|
2466
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2467
|
+
return String(value);
|
|
2468
|
+
}
|
|
2469
|
+
return "";
|
|
2470
|
+
}
|
|
2471
|
+
function readExistingBindingMetadata(document, bindingId) {
|
|
2472
|
+
const binding = document.bindings.find((entry) => entry.id === bindingId);
|
|
2473
|
+
return binding ? { ...binding.metadata } : null;
|
|
2474
|
+
}
|
|
2475
|
+
function readBindingSelector(document, nodeId) {
|
|
2476
|
+
const bindingIdentity = readSelectedBindingIdentity(document, nodeId);
|
|
2477
|
+
const binding = bindingIdentity.bindingId
|
|
2478
|
+
? document.bindings.find((entry) => entry.id === bindingIdentity.bindingId) ?? null
|
|
2479
|
+
: null;
|
|
2480
|
+
if (binding && typeof binding.selector === "string") {
|
|
2481
|
+
return binding.selector;
|
|
2482
|
+
}
|
|
2483
|
+
return binding && typeof binding.metadata.selector === "string"
|
|
2484
|
+
? binding.metadata.selector
|
|
2485
|
+
: "";
|
|
2486
|
+
}
|
|
2487
|
+
function getRootNodes(page) {
|
|
2488
|
+
if (page.rootNodeId) {
|
|
2489
|
+
const root = page.nodes.find((entry) => entry.id === page.rootNodeId);
|
|
2490
|
+
if (root) {
|
|
2491
|
+
return [root, ...page.nodes.filter((entry) => entry.parentId === null && entry.id !== root.id)];
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
return page.nodes.filter((entry) => entry.parentId === null);
|
|
2495
|
+
}
|
|
2496
|
+
function ensureExpandedNodePath(page, nodeId) {
|
|
2497
|
+
if (!page.rootNodeId) {
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2500
|
+
expandedLayerNodeIds.add(page.rootNodeId);
|
|
2501
|
+
let cursor = nodeId ? page.nodes.find((entry) => entry.id === nodeId) ?? null : null;
|
|
2502
|
+
while (cursor?.parentId) {
|
|
2503
|
+
expandedLayerNodeIds.add(cursor.parentId);
|
|
2504
|
+
cursor = page.nodes.find((entry) => entry.id === cursor?.parentId) ?? null;
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
function getSiblingIds(page, parentId) {
|
|
2508
|
+
if (parentId) {
|
|
2509
|
+
return [...(page.nodes.find((entry) => entry.id === parentId)?.childIds ?? [])];
|
|
2510
|
+
}
|
|
2511
|
+
return page.nodes.filter((entry) => entry.parentId === null).map((entry) => entry.id);
|
|
2512
|
+
}
|
|
2513
|
+
function assignSiblingIds(page, parentId, ids) {
|
|
2514
|
+
if (parentId) {
|
|
2515
|
+
const parent = page.nodes.find((entry) => entry.id === parentId);
|
|
2516
|
+
if (parent) {
|
|
2517
|
+
parent.childIds = ids;
|
|
2518
|
+
}
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2521
|
+
for (const node of page.nodes) {
|
|
2522
|
+
if (ids.includes(node.id)) {
|
|
2523
|
+
node.parentId = null;
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
page.rootNodeId = ids[0] ?? null;
|
|
2527
|
+
}
|
|
2528
|
+
function findSiblingInsertIndex(page, nodeId) {
|
|
2529
|
+
const node = page.nodes.find((entry) => entry.id === nodeId);
|
|
2530
|
+
if (!node) {
|
|
2531
|
+
return 0;
|
|
2532
|
+
}
|
|
2533
|
+
const siblings = getSiblingIds(page, node.parentId ?? null);
|
|
2534
|
+
const currentIndex = siblings.indexOf(node.id);
|
|
2535
|
+
return currentIndex >= 0 ? currentIndex : siblings.length;
|
|
2536
|
+
}
|
|
2537
|
+
function clampIndex(index, maxLength) {
|
|
2538
|
+
return Math.max(0, Math.min(index, maxLength));
|
|
2539
|
+
}
|
|
2540
|
+
function findPageForNode(document, nodeId) {
|
|
2541
|
+
for (const page of document.pages) {
|
|
2542
|
+
if (page.nodes.some((entry) => entry.id === nodeId)) {
|
|
2543
|
+
return page;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
return null;
|
|
2547
|
+
}
|
|
2548
|
+
function collectNodeSubtreeIds(page, nodeId) {
|
|
2549
|
+
const collected = [];
|
|
2550
|
+
const visit = (id) => {
|
|
2551
|
+
const node = page.nodes.find((entry) => entry.id === id);
|
|
2552
|
+
if (!node) {
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2555
|
+
collected.push(node.id);
|
|
2556
|
+
for (const childId of node.childIds) {
|
|
2557
|
+
visit(childId);
|
|
2558
|
+
}
|
|
2559
|
+
};
|
|
2560
|
+
visit(nodeId);
|
|
2561
|
+
return collected;
|
|
2562
|
+
}
|
|
2563
|
+
function buildDuplicateIdMap(page, nodeId) {
|
|
2564
|
+
return Object.fromEntries(collectNodeSubtreeIds(page, nodeId).map((id) => [id, `node_${crypto.randomUUID().slice(0, 8)}`]));
|
|
2565
|
+
}
|
|
2566
|
+
function structuredCloneNode(node) {
|
|
2567
|
+
return {
|
|
2568
|
+
...node,
|
|
2569
|
+
rect: { ...node.rect },
|
|
2570
|
+
props: { ...node.props },
|
|
2571
|
+
style: { ...node.style },
|
|
2572
|
+
tokenRefs: { ...node.tokenRefs },
|
|
2573
|
+
bindingRefs: { ...node.bindingRefs },
|
|
2574
|
+
variantPatches: node.variantPatches.map((entry) => ({ ...entry })),
|
|
2575
|
+
metadata: { ...node.metadata }
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
function marqueeClientRectToStageRect(marquee) {
|
|
2579
|
+
const stageRect = stageElement.getBoundingClientRect();
|
|
2580
|
+
const left = Math.min(marquee.originClientX, marquee.currentClientX) - stageRect.left;
|
|
2581
|
+
const top = Math.min(marquee.originClientY, marquee.currentClientY) - stageRect.top;
|
|
2582
|
+
const width = Math.abs(marquee.currentClientX - marquee.originClientX);
|
|
2583
|
+
const height = Math.abs(marquee.currentClientY - marquee.originClientY);
|
|
2584
|
+
return { left, top, width, height };
|
|
2585
|
+
}
|
|
2586
|
+
function marqueeClientRectToCanvasRect(marquee) {
|
|
2587
|
+
const stageRect = marqueeClientRectToStageRect(marquee);
|
|
2588
|
+
const viewport = currentState?.viewport ?? DEFAULT_EDITOR_VIEWPORT;
|
|
2589
|
+
return {
|
|
2590
|
+
x: Math.round((stageRect.left - viewport.x) / viewport.zoom),
|
|
2591
|
+
y: Math.round((stageRect.top - viewport.y) / viewport.zoom),
|
|
2592
|
+
width: Math.round(stageRect.width / viewport.zoom),
|
|
2593
|
+
height: Math.round(stageRect.height / viewport.zoom)
|
|
2594
|
+
};
|
|
2595
|
+
}
|
|
2596
|
+
function rectsIntersect(left, right) {
|
|
2597
|
+
return left.x < right.x + right.width
|
|
2598
|
+
&& left.x + left.width > right.x
|
|
2599
|
+
&& left.y < right.y + right.height
|
|
2600
|
+
&& left.y + left.height > right.y;
|
|
2601
|
+
}
|
|
2602
|
+
function isNodeHidden(node) {
|
|
2603
|
+
return isRecord(node.metadata.visibility) && node.metadata.visibility.hidden === true;
|
|
2604
|
+
}
|
|
2605
|
+
function readTokenPath(value) {
|
|
2606
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
2607
|
+
return normalizeTokenPath(value);
|
|
2608
|
+
}
|
|
2609
|
+
if (isRecord(value)) {
|
|
2610
|
+
if (typeof value.path === "string" && value.path.trim().length > 0) {
|
|
2611
|
+
return normalizeTokenPath(value.path);
|
|
2612
|
+
}
|
|
2613
|
+
if (typeof value.tokenPath === "string" && value.tokenPath.trim().length > 0) {
|
|
2614
|
+
return normalizeTokenPath(value.tokenPath);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
return null;
|
|
2618
|
+
}
|
|
2619
|
+
function normalizeTokenPath(path) {
|
|
2620
|
+
return path.trim().replace(/^tokens\./, "");
|
|
2621
|
+
}
|
|
2622
|
+
function escapeHtmlText(value) {
|
|
2623
|
+
return value
|
|
2624
|
+
.replaceAll("&", "&")
|
|
2625
|
+
.replaceAll("<", "<")
|
|
2626
|
+
.replaceAll(">", ">");
|
|
2627
|
+
}
|
|
2628
|
+
function escapeHtmlAttribute(value) {
|
|
2629
|
+
return escapeHtmlText(value).replaceAll("\"", """);
|
|
2630
|
+
}
|
|
2631
|
+
function readActiveTokenModeId(tokens) {
|
|
2632
|
+
return typeof tokens.metadata.activeModeId === "string" && tokens.metadata.activeModeId.trim().length > 0
|
|
2633
|
+
? tokens.metadata.activeModeId
|
|
2634
|
+
: null;
|
|
2635
|
+
}
|
|
2636
|
+
function readNestedTokenValue(values, tokenPath) {
|
|
2637
|
+
const segments = normalizeTokenPath(tokenPath).split(".").filter(Boolean);
|
|
2638
|
+
let current = values;
|
|
2639
|
+
for (const segment of segments) {
|
|
2640
|
+
if (!isRecord(current) || !(segment in current)) {
|
|
2641
|
+
return null;
|
|
2642
|
+
}
|
|
2643
|
+
current = current[segment];
|
|
2644
|
+
}
|
|
2645
|
+
return current;
|
|
2646
|
+
}
|
|
2647
|
+
function findTokenCollectionId(tokens, tokenPath) {
|
|
2648
|
+
const normalized = normalizeTokenPath(tokenPath);
|
|
2649
|
+
for (const collection of tokens.collections) {
|
|
2650
|
+
if (collection.items.some((item) => item.path === normalized)) {
|
|
2651
|
+
return collection.id;
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
return null;
|
|
2655
|
+
}
|
|
2656
|
+
function findTokenItem(tokens, tokenPath) {
|
|
2657
|
+
const normalized = normalizeTokenPath(tokenPath);
|
|
2658
|
+
for (const collection of tokens.collections) {
|
|
2659
|
+
const item = collection.items.find((entry) => entry.path === normalized);
|
|
2660
|
+
if (item) {
|
|
2661
|
+
return { collection, item };
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
return null;
|
|
2665
|
+
}
|
|
2666
|
+
function readTokenAliasTarget(tokens, tokenPath, modeId) {
|
|
2667
|
+
const normalized = normalizeTokenPath(tokenPath);
|
|
2668
|
+
const exact = tokens.aliases.find((entry) => entry.path === normalized && (entry.modeId ?? null) === modeId);
|
|
2669
|
+
if (exact) {
|
|
2670
|
+
return exact.targetPath;
|
|
2671
|
+
}
|
|
2672
|
+
const shared = tokens.aliases.find((entry) => entry.path === normalized && (entry.modeId ?? null) === null);
|
|
2673
|
+
return shared?.targetPath ?? null;
|
|
2674
|
+
}
|
|
2675
|
+
function resolveTokenValue(tokens, tokenPath, modeId, seen = new Set()) {
|
|
2676
|
+
const normalized = normalizeTokenPath(tokenPath);
|
|
2677
|
+
const visitKey = `${normalized}:${modeId ?? ""}`;
|
|
2678
|
+
if (seen.has(visitKey)) {
|
|
2679
|
+
return null;
|
|
2680
|
+
}
|
|
2681
|
+
seen.add(visitKey);
|
|
2682
|
+
const aliasTarget = readTokenAliasTarget(tokens, normalized, modeId);
|
|
2683
|
+
if (aliasTarget) {
|
|
2684
|
+
const aliasedValue = resolveTokenValue(tokens, aliasTarget, modeId, seen);
|
|
2685
|
+
if (aliasedValue !== null && aliasedValue !== undefined) {
|
|
2686
|
+
return aliasedValue;
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
const location = findTokenItem(tokens, normalized);
|
|
2690
|
+
if (location) {
|
|
2691
|
+
if (modeId) {
|
|
2692
|
+
const mode = location.item.modes.find((entry) => entry.id === modeId);
|
|
2693
|
+
if (mode) {
|
|
2694
|
+
return mode.value;
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
if (location.item.value !== undefined) {
|
|
2698
|
+
return location.item.value;
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
return readNestedTokenValue(tokens.values, normalized);
|
|
2702
|
+
}
|
|
2703
|
+
function formatTokenEditorValue(value) {
|
|
2704
|
+
if (typeof value === "string") {
|
|
2705
|
+
return value;
|
|
2706
|
+
}
|
|
2707
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2708
|
+
return String(value);
|
|
2709
|
+
}
|
|
2710
|
+
if (typeof value === "boolean") {
|
|
2711
|
+
return value ? "true" : "false";
|
|
2712
|
+
}
|
|
2713
|
+
return "";
|
|
2714
|
+
}
|
|
2715
|
+
function parseTokenEditorValue(raw) {
|
|
2716
|
+
const value = raw.trim();
|
|
2717
|
+
if (value === "true") {
|
|
2718
|
+
return true;
|
|
2719
|
+
}
|
|
2720
|
+
if (value === "false") {
|
|
2721
|
+
return false;
|
|
2722
|
+
}
|
|
2723
|
+
if (/^-?\d+(?:\.\d+)?$/.test(value)) {
|
|
2724
|
+
const parsed = Number(value);
|
|
2725
|
+
if (Number.isFinite(parsed)) {
|
|
2726
|
+
return parsed;
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
return value;
|
|
2730
|
+
}
|
|
2731
|
+
function collectLeafTokenPaths(value, prefix, target) {
|
|
2732
|
+
if (!isRecord(value)) {
|
|
2733
|
+
if (prefix.length > 0) {
|
|
2734
|
+
target.add(prefix);
|
|
2735
|
+
}
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
2739
|
+
const nextPath = prefix ? `${prefix}.${key}` : key;
|
|
2740
|
+
if (isRecord(entry)) {
|
|
2741
|
+
collectLeafTokenPaths(entry, nextPath, target);
|
|
2742
|
+
continue;
|
|
2743
|
+
}
|
|
2744
|
+
if (!Array.isArray(entry)) {
|
|
2745
|
+
target.add(nextPath);
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
function listTokenModes(tokens, collectionId) {
|
|
2750
|
+
const modeMap = new Map([["__base__", "Base value"]]);
|
|
2751
|
+
if (collectionId !== "__values__") {
|
|
2752
|
+
const collection = tokens.collections.find((entry) => entry.id === collectionId);
|
|
2753
|
+
for (const item of collection?.items ?? []) {
|
|
2754
|
+
for (const mode of item.modes) {
|
|
2755
|
+
modeMap.set(mode.id, mode.name);
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
const activeModeId = readActiveTokenModeId(tokens);
|
|
2760
|
+
if (activeModeId && !modeMap.has(activeModeId)) {
|
|
2761
|
+
modeMap.set(activeModeId, activeModeId);
|
|
2762
|
+
}
|
|
2763
|
+
return [...modeMap.entries()].map(([id, name]) => ({ id, name }));
|
|
2764
|
+
}
|
|
2765
|
+
function collectTokenUsages(document, tokenPath) {
|
|
2766
|
+
const normalized = normalizeTokenPath(tokenPath);
|
|
2767
|
+
const modeId = readActiveTokenModeId(document.tokens);
|
|
2768
|
+
return document.pages.flatMap((page) => page.nodes.flatMap((node) => {
|
|
2769
|
+
const entries = Object.entries(node.tokenRefs)
|
|
2770
|
+
.flatMap(([property, value]) => {
|
|
2771
|
+
const refPath = readTokenPath(value);
|
|
2772
|
+
return refPath === normalized ? [{
|
|
2773
|
+
pageName: page.name,
|
|
2774
|
+
nodeId: node.id,
|
|
2775
|
+
nodeName: node.name,
|
|
2776
|
+
property,
|
|
2777
|
+
resolvedValue: resolveTokenValue(document.tokens, normalized, modeId)
|
|
2778
|
+
}] : [];
|
|
2779
|
+
});
|
|
2780
|
+
return entries;
|
|
2781
|
+
}));
|
|
2782
|
+
}
|
|
2783
|
+
function cloneTokenStore(tokens) {
|
|
2784
|
+
return structuredClone(tokens);
|
|
2785
|
+
}
|
|
2786
|
+
function hasSelectedTokenBinding(node, property) {
|
|
2787
|
+
return typeof readTokenPath(node.tokenRefs[property]) === "string";
|
|
2788
|
+
}
|
|
2789
|
+
function syncTokenEditorSelection() {
|
|
2790
|
+
if (!currentState) {
|
|
2791
|
+
selectedTokenPath = "";
|
|
2792
|
+
selectedTokenCollectionId = "__values__";
|
|
2793
|
+
selectedTokenModeId = "__base__";
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2796
|
+
const node = getSelectedNode();
|
|
2797
|
+
const selectedEntry = node
|
|
2798
|
+
? Object.entries(node.tokenRefs)
|
|
2799
|
+
.map(([property, value]) => ({ property, path: readTokenPath(value) }))
|
|
2800
|
+
.find((entry) => typeof entry.path === "string")
|
|
2801
|
+
: null;
|
|
2802
|
+
const knownPaths = new Set();
|
|
2803
|
+
collectLeafTokenPaths(currentState.document.tokens.values, "", knownPaths);
|
|
2804
|
+
for (const collection of currentState.document.tokens.collections) {
|
|
2805
|
+
for (const item of collection.items) {
|
|
2806
|
+
knownPaths.add(item.path);
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
if (selectedEntry && selectedTokenPath.length === 0) {
|
|
2810
|
+
selectedTokenPath = selectedEntry.path;
|
|
2811
|
+
tokenBindingPropertySelect.value = selectedEntry.property;
|
|
2812
|
+
}
|
|
2813
|
+
else if (selectedTokenPath.length === 0 || !knownPaths.has(selectedTokenPath)) {
|
|
2814
|
+
selectedTokenPath = [...knownPaths][0] ?? "";
|
|
2815
|
+
}
|
|
2816
|
+
const collectionStillExists = selectedTokenCollectionId === "__values__"
|
|
2817
|
+
|| currentState.document.tokens.collections.some((entry) => entry.id === selectedTokenCollectionId);
|
|
2818
|
+
if (!collectionStillExists) {
|
|
2819
|
+
selectedTokenCollectionId = selectedTokenPath
|
|
2820
|
+
? findTokenCollectionId(currentState.document.tokens, selectedTokenPath) ?? "__values__"
|
|
2821
|
+
: "__values__";
|
|
2822
|
+
}
|
|
2823
|
+
const activeModeId = readActiveTokenModeId(currentState.document.tokens);
|
|
2824
|
+
selectedTokenModeId = activeModeId ?? "__base__";
|
|
2825
|
+
}
|
|
2826
|
+
function updateActiveTokenMode() {
|
|
2827
|
+
if (!currentState || currentState.pendingMutation) {
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2830
|
+
const nextTokens = cloneTokenStore(currentState.document.tokens);
|
|
2831
|
+
if (selectedTokenModeId === "__base__") {
|
|
2832
|
+
delete nextTokens.metadata.activeModeId;
|
|
2833
|
+
}
|
|
2834
|
+
else {
|
|
2835
|
+
nextTokens.metadata.activeModeId = selectedTokenModeId;
|
|
2836
|
+
}
|
|
2837
|
+
applyOptimisticPatch([{ op: "tokens.replace", tokens: nextTokens }], currentState.selection);
|
|
2838
|
+
}
|
|
2839
|
+
function createTokenCollection() {
|
|
2840
|
+
if (!currentState || currentState.pendingMutation) {
|
|
2841
|
+
return;
|
|
2842
|
+
}
|
|
2843
|
+
const rawName = tokenCollectionNameInput.value.trim();
|
|
2844
|
+
if (rawName.length === 0) {
|
|
2845
|
+
tokenStatusElement.textContent = "Enter a collection name first.";
|
|
2846
|
+
return;
|
|
2847
|
+
}
|
|
2848
|
+
const collectionId = rawName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") || `collection-${crypto.randomUUID().slice(0, 6)}`;
|
|
2849
|
+
const nextTokens = cloneTokenStore(currentState.document.tokens);
|
|
2850
|
+
if (!nextTokens.collections.some((entry) => entry.id === collectionId)) {
|
|
2851
|
+
nextTokens.collections.push({
|
|
2852
|
+
id: collectionId,
|
|
2853
|
+
name: rawName,
|
|
2854
|
+
items: [],
|
|
2855
|
+
metadata: {}
|
|
2856
|
+
});
|
|
2857
|
+
}
|
|
2858
|
+
selectedTokenCollectionId = collectionId;
|
|
2859
|
+
tokenCollectionNameInput.value = "";
|
|
2860
|
+
applyOptimisticPatch([{ op: "tokens.replace", tokens: nextTokens }], currentState.selection);
|
|
2861
|
+
}
|
|
2862
|
+
function createTokenMode() {
|
|
2863
|
+
if (!currentState || currentState.pendingMutation) {
|
|
2864
|
+
return;
|
|
2865
|
+
}
|
|
2866
|
+
const rawName = tokenModeNameInput.value.trim();
|
|
2867
|
+
const tokenPath = normalizeTokenPath(tokenPathInput.value);
|
|
2868
|
+
if (rawName.length === 0 || tokenPath.length === 0) {
|
|
2869
|
+
tokenStatusElement.textContent = "Choose a token path and enter a mode name first.";
|
|
2870
|
+
return;
|
|
2871
|
+
}
|
|
2872
|
+
const nextTokens = cloneTokenStore(currentState.document.tokens);
|
|
2873
|
+
const collectionId = selectedTokenCollectionId === "__values__"
|
|
2874
|
+
? (findTokenCollectionId(nextTokens, tokenPath) ?? "__values__")
|
|
2875
|
+
: selectedTokenCollectionId;
|
|
2876
|
+
if (collectionId === "__values__") {
|
|
2877
|
+
tokenStatusElement.textContent = "Create or select a token collection before adding modes.";
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2880
|
+
const collection = nextTokens.collections.find((entry) => entry.id === collectionId);
|
|
2881
|
+
if (!collection) {
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
let item = collection.items.find((entry) => entry.path === tokenPath);
|
|
2885
|
+
if (!item) {
|
|
2886
|
+
item = {
|
|
2887
|
+
id: tokenPath.replace(/[^a-z0-9]+/gi, "_").toLowerCase(),
|
|
2888
|
+
path: tokenPath,
|
|
2889
|
+
value: parseTokenEditorValue(tokenValueInput.value || formatTokenEditorValue(resolveTokenValue(nextTokens, tokenPath, null))),
|
|
2890
|
+
modes: [],
|
|
2891
|
+
metadata: {}
|
|
2892
|
+
};
|
|
2893
|
+
collection.items.push(item);
|
|
2894
|
+
}
|
|
2895
|
+
const modeId = rawName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") || `mode-${crypto.randomUUID().slice(0, 6)}`;
|
|
2896
|
+
if (!item.modes.some((entry) => entry.id === modeId)) {
|
|
2897
|
+
item.modes.push({
|
|
2898
|
+
id: modeId,
|
|
2899
|
+
name: rawName,
|
|
2900
|
+
value: parseTokenEditorValue(tokenValueInput.value || formatTokenEditorValue(item.value)),
|
|
2901
|
+
metadata: {}
|
|
2902
|
+
});
|
|
2903
|
+
}
|
|
2904
|
+
nextTokens.metadata.activeModeId = modeId;
|
|
2905
|
+
selectedTokenModeId = modeId;
|
|
2906
|
+
tokenModeNameInput.value = "";
|
|
2907
|
+
applyOptimisticPatch([{ op: "tokens.replace", tokens: nextTokens }], currentState.selection);
|
|
2908
|
+
}
|
|
2909
|
+
function saveTokenEditor() {
|
|
2910
|
+
if (!currentState || currentState.pendingMutation) {
|
|
2911
|
+
return;
|
|
2912
|
+
}
|
|
2913
|
+
const tokenPath = normalizeTokenPath(tokenPathInput.value);
|
|
2914
|
+
if (tokenPath.length === 0) {
|
|
2915
|
+
tokenStatusElement.textContent = "Enter a token path first.";
|
|
2916
|
+
return;
|
|
2917
|
+
}
|
|
2918
|
+
const nextTokens = cloneTokenStore(currentState.document.tokens);
|
|
2919
|
+
const nextValue = parseTokenEditorValue(tokenValueInput.value);
|
|
2920
|
+
if (selectedTokenCollectionId === "__values__") {
|
|
2921
|
+
setNestedValue(nextTokens.values, tokenPath, nextValue);
|
|
2922
|
+
}
|
|
2923
|
+
else {
|
|
2924
|
+
const collection = nextTokens.collections.find((entry) => entry.id === selectedTokenCollectionId);
|
|
2925
|
+
if (!collection) {
|
|
2926
|
+
tokenStatusElement.textContent = "Choose a valid token collection.";
|
|
2927
|
+
return;
|
|
2928
|
+
}
|
|
2929
|
+
let item = collection.items.find((entry) => entry.path === tokenPath);
|
|
2930
|
+
if (!item) {
|
|
2931
|
+
item = {
|
|
2932
|
+
id: tokenPath.replace(/[^a-z0-9]+/gi, "_").toLowerCase(),
|
|
2933
|
+
path: tokenPath,
|
|
2934
|
+
value: nextValue,
|
|
2935
|
+
modes: [],
|
|
2936
|
+
metadata: {}
|
|
2937
|
+
};
|
|
2938
|
+
collection.items.push(item);
|
|
2939
|
+
}
|
|
2940
|
+
if (selectedTokenModeId === "__base__") {
|
|
2941
|
+
item.value = nextValue;
|
|
2942
|
+
}
|
|
2943
|
+
else {
|
|
2944
|
+
const modeName = tokenModeSelect.selectedOptions[0]?.textContent?.trim() || selectedTokenModeId;
|
|
2945
|
+
const existingMode = item.modes.find((entry) => entry.id === selectedTokenModeId);
|
|
2946
|
+
if (existingMode) {
|
|
2947
|
+
existingMode.value = nextValue;
|
|
2948
|
+
}
|
|
2949
|
+
else {
|
|
2950
|
+
item.modes.push({
|
|
2951
|
+
id: selectedTokenModeId,
|
|
2952
|
+
name: modeName,
|
|
2953
|
+
value: nextValue,
|
|
2954
|
+
metadata: {}
|
|
2955
|
+
});
|
|
2956
|
+
}
|
|
2957
|
+
nextTokens.metadata.activeModeId = selectedTokenModeId;
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
const aliasTarget = normalizeTokenPath(tokenAliasInput.value);
|
|
2961
|
+
nextTokens.aliases = nextTokens.aliases.filter((entry) => !(entry.path === tokenPath && (entry.modeId ?? null) === (selectedTokenModeId === "__base__" ? null : selectedTokenModeId)));
|
|
2962
|
+
if (aliasTarget.length > 0) {
|
|
2963
|
+
nextTokens.aliases.push({
|
|
2964
|
+
path: tokenPath,
|
|
2965
|
+
targetPath: aliasTarget,
|
|
2966
|
+
modeId: selectedTokenModeId === "__base__" ? null : selectedTokenModeId,
|
|
2967
|
+
metadata: {}
|
|
2968
|
+
});
|
|
2969
|
+
}
|
|
2970
|
+
selectedTokenPath = tokenPath;
|
|
2971
|
+
applyOptimisticPatch([{ op: "tokens.replace", tokens: nextTokens }], currentState.selection);
|
|
2972
|
+
}
|
|
2973
|
+
function bindSelectedNodeToToken() {
|
|
2974
|
+
const node = getSelectedNode();
|
|
2975
|
+
if (!node || !currentState || currentState.pendingMutation) {
|
|
2976
|
+
return;
|
|
2977
|
+
}
|
|
2978
|
+
const tokenPath = normalizeTokenPath(tokenPathInput.value);
|
|
2979
|
+
const property = tokenBindingPropertySelect.value || "backgroundColor";
|
|
2980
|
+
if (tokenPath.length === 0) {
|
|
2981
|
+
tokenStatusElement.textContent = "Choose a token path first.";
|
|
2982
|
+
return;
|
|
2983
|
+
}
|
|
2984
|
+
const nextTokenRefs = { ...node.tokenRefs, [property]: tokenPath };
|
|
2985
|
+
const nextStyle = { ...node.style };
|
|
2986
|
+
const resolvedValue = resolveTokenValue(currentState.document.tokens, tokenPath, readActiveTokenModeId(currentState.document.tokens));
|
|
2987
|
+
if (typeof resolvedValue === "string" || typeof resolvedValue === "number") {
|
|
2988
|
+
nextStyle[property] = resolvedValue;
|
|
2989
|
+
}
|
|
2990
|
+
const nextTokens = cloneTokenStore(currentState.document.tokens);
|
|
2991
|
+
const bindingIdentity = readSelectedBindingIdentity(currentState.document, node.id);
|
|
2992
|
+
nextTokens.bindings = nextTokens.bindings.filter((entry) => !(entry.nodeId === node.id && entry.property === property));
|
|
2993
|
+
nextTokens.bindings.push({
|
|
2994
|
+
path: tokenPath,
|
|
2995
|
+
nodeId: node.id,
|
|
2996
|
+
bindingId: bindingIdentity.bindingId ?? null,
|
|
2997
|
+
property,
|
|
2998
|
+
metadata: {}
|
|
2999
|
+
});
|
|
3000
|
+
selectedTokenPath = tokenPath;
|
|
3001
|
+
applyOptimisticPatch([
|
|
3002
|
+
{
|
|
3003
|
+
op: "node.update",
|
|
3004
|
+
nodeId: node.id,
|
|
3005
|
+
changes: {
|
|
3006
|
+
tokenRefs: nextTokenRefs,
|
|
3007
|
+
style: nextStyle
|
|
3008
|
+
}
|
|
3009
|
+
},
|
|
3010
|
+
{
|
|
3011
|
+
op: "tokens.replace",
|
|
3012
|
+
tokens: nextTokens
|
|
3013
|
+
}
|
|
3014
|
+
], currentState.selection);
|
|
3015
|
+
}
|
|
3016
|
+
function clearSelectedTokenBinding() {
|
|
3017
|
+
const node = getSelectedNode();
|
|
3018
|
+
if (!node || !currentState || currentState.pendingMutation) {
|
|
3019
|
+
return;
|
|
3020
|
+
}
|
|
3021
|
+
const property = tokenBindingPropertySelect.value || "backgroundColor";
|
|
3022
|
+
if (!hasSelectedTokenBinding(node, property)) {
|
|
3023
|
+
return;
|
|
3024
|
+
}
|
|
3025
|
+
const nextTokenRefs = { ...node.tokenRefs };
|
|
3026
|
+
delete nextTokenRefs[property];
|
|
3027
|
+
const nextTokens = cloneTokenStore(currentState.document.tokens);
|
|
3028
|
+
nextTokens.bindings = nextTokens.bindings.filter((entry) => !(entry.nodeId === node.id && entry.property === property));
|
|
3029
|
+
applyOptimisticPatch([
|
|
3030
|
+
{
|
|
3031
|
+
op: "node.update",
|
|
3032
|
+
nodeId: node.id,
|
|
3033
|
+
changes: {
|
|
3034
|
+
tokenRefs: nextTokenRefs
|
|
3035
|
+
}
|
|
3036
|
+
},
|
|
3037
|
+
{
|
|
3038
|
+
op: "tokens.replace",
|
|
3039
|
+
tokens: nextTokens
|
|
3040
|
+
}
|
|
3041
|
+
], currentState.selection);
|
|
3042
|
+
}
|
|
3043
|
+
function resolveStageStyle(documentState, node) {
|
|
3044
|
+
const style = { ...node.style };
|
|
3045
|
+
const modeId = readActiveTokenModeId(documentState.tokens);
|
|
3046
|
+
for (const [property, value] of Object.entries(node.tokenRefs)) {
|
|
3047
|
+
const tokenPath = readTokenPath(value);
|
|
3048
|
+
if (!tokenPath) {
|
|
3049
|
+
continue;
|
|
3050
|
+
}
|
|
3051
|
+
const resolvedValue = resolveTokenValue(documentState.tokens, tokenPath, modeId);
|
|
3052
|
+
if (typeof resolvedValue === "string" || typeof resolvedValue === "number") {
|
|
3053
|
+
style[property] = resolvedValue;
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
return style;
|
|
3057
|
+
}
|
|
3058
|
+
function getDraftId(draft) {
|
|
3059
|
+
return draft.kind === "region" ? draft.regionId : draft.nodeId;
|
|
3060
|
+
}
|
|
3061
|
+
function isEditableTarget(target) {
|
|
3062
|
+
return target instanceof HTMLInputElement
|
|
3063
|
+
|| target instanceof HTMLTextAreaElement
|
|
3064
|
+
|| (target instanceof HTMLElement && target.isContentEditable);
|
|
3065
|
+
}
|
|
3066
|
+
function postViewState() {
|
|
3067
|
+
if (!currentState) {
|
|
3068
|
+
return;
|
|
3069
|
+
}
|
|
3070
|
+
port.postMessage({
|
|
3071
|
+
type: "canvas-page-view-state",
|
|
3072
|
+
viewport: currentState.viewport,
|
|
3073
|
+
selection: currentState.selection
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
3076
|
+
function schedulePersist(state) {
|
|
3077
|
+
if (persistTimer !== null) {
|
|
3078
|
+
window.clearTimeout(persistTimer);
|
|
3079
|
+
}
|
|
3080
|
+
persistTimer = window.setTimeout(() => {
|
|
3081
|
+
void flushPersist(state);
|
|
3082
|
+
}, SAVE_DEBOUNCE_MS);
|
|
3083
|
+
}
|
|
3084
|
+
async function flushPersist(state = currentState) {
|
|
3085
|
+
if (!state) {
|
|
3086
|
+
return;
|
|
3087
|
+
}
|
|
3088
|
+
if (persistTimer !== null) {
|
|
3089
|
+
window.clearTimeout(persistTimer);
|
|
3090
|
+
persistTimer = null;
|
|
3091
|
+
}
|
|
3092
|
+
await saveCachedState(currentTabId, state);
|
|
3093
|
+
broadcastChannel?.postMessage({ type: "canvas-page:broadcast", state });
|
|
3094
|
+
}
|
|
3095
|
+
async function getCurrentTabId() {
|
|
3096
|
+
return await new Promise((resolve) => {
|
|
3097
|
+
chrome.tabs.getCurrent((tab) => {
|
|
3098
|
+
const tabId = tab?.id;
|
|
3099
|
+
resolve(typeof tabId === "number" ? tabId : null);
|
|
3100
|
+
});
|
|
3101
|
+
});
|
|
3102
|
+
}
|
|
3103
|
+
async function openDatabase() {
|
|
3104
|
+
if (databasePromise) {
|
|
3105
|
+
return await databasePromise;
|
|
3106
|
+
}
|
|
3107
|
+
if (typeof indexedDB === "undefined") {
|
|
3108
|
+
return null;
|
|
3109
|
+
}
|
|
3110
|
+
databasePromise = new Promise((resolve) => {
|
|
3111
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
3112
|
+
request.onupgradeneeded = () => {
|
|
3113
|
+
const db = request.result;
|
|
3114
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
3115
|
+
db.createObjectStore(STORE_NAME);
|
|
3116
|
+
}
|
|
3117
|
+
};
|
|
3118
|
+
request.onsuccess = () => resolve(request.result);
|
|
3119
|
+
request.onerror = () => resolve(null);
|
|
3120
|
+
});
|
|
3121
|
+
return await databasePromise;
|
|
3122
|
+
}
|
|
3123
|
+
async function loadCachedState(tabId) {
|
|
3124
|
+
const db = await openDatabase();
|
|
3125
|
+
if (!db || tabId === null) {
|
|
3126
|
+
return null;
|
|
3127
|
+
}
|
|
3128
|
+
const tabState = await readStoredState(db, `tab:${tabId}`);
|
|
3129
|
+
if (tabState) {
|
|
3130
|
+
return tabState;
|
|
3131
|
+
}
|
|
3132
|
+
return null;
|
|
3133
|
+
}
|
|
3134
|
+
async function readStoredState(db, key) {
|
|
3135
|
+
return await new Promise((resolve) => {
|
|
3136
|
+
const transaction = db.transaction(STORE_NAME, "readonly");
|
|
3137
|
+
const request = transaction.objectStore(STORE_NAME).get(key);
|
|
3138
|
+
request.onsuccess = () => resolve(normalizeCanvasPageState(request.result));
|
|
3139
|
+
request.onerror = () => resolve(null);
|
|
3140
|
+
});
|
|
3141
|
+
}
|
|
3142
|
+
async function saveCachedState(tabId, state) {
|
|
3143
|
+
const db = await openDatabase();
|
|
3144
|
+
if (!db) {
|
|
3145
|
+
return;
|
|
3146
|
+
}
|
|
3147
|
+
const writes = [`doc:${state.documentId}`, `session:${state.canvasSessionId}`];
|
|
3148
|
+
if (tabId !== null) {
|
|
3149
|
+
writes.push(`tab:${tabId}`);
|
|
3150
|
+
}
|
|
3151
|
+
await Promise.all(writes.map((key) => writeStoredState(db, key, state)));
|
|
3152
|
+
}
|
|
3153
|
+
async function writeStoredState(db, key, state) {
|
|
3154
|
+
await new Promise((resolve) => {
|
|
3155
|
+
const transaction = db.transaction(STORE_NAME, "readwrite");
|
|
3156
|
+
const request = transaction.objectStore(STORE_NAME).put(state, key);
|
|
3157
|
+
request.onsuccess = () => resolve();
|
|
3158
|
+
request.onerror = () => resolve();
|
|
3159
|
+
});
|
|
3160
|
+
}
|
|
3161
|
+
function shouldAcceptBroadcast(state) {
|
|
3162
|
+
if (!currentState) {
|
|
3163
|
+
return true;
|
|
3164
|
+
}
|
|
3165
|
+
return state.documentId === currentState.documentId && state.updatedAt >= currentState.updatedAt;
|
|
3166
|
+
}
|
|
3167
|
+
function normalizeCanvasPageState(value) {
|
|
3168
|
+
if (!isRecord(value)) {
|
|
3169
|
+
return null;
|
|
3170
|
+
}
|
|
3171
|
+
if (typeof value.tabId !== "number"
|
|
3172
|
+
|| typeof value.targetId !== "string"
|
|
3173
|
+
|| typeof value.canvasSessionId !== "string"
|
|
3174
|
+
|| typeof value.documentId !== "string"
|
|
3175
|
+
|| typeof value.title !== "string"
|
|
3176
|
+
|| typeof value.html !== "string"
|
|
3177
|
+
|| typeof value.updatedAt !== "string"
|
|
3178
|
+
|| !isRecord(value.document)) {
|
|
3179
|
+
return null;
|
|
3180
|
+
}
|
|
3181
|
+
const summary = normalizeCanvasSessionSummary(value.summary);
|
|
3182
|
+
return {
|
|
3183
|
+
tabId: value.tabId,
|
|
3184
|
+
targetId: value.targetId,
|
|
3185
|
+
canvasSessionId: value.canvasSessionId,
|
|
3186
|
+
documentId: value.documentId,
|
|
3187
|
+
documentRevision: typeof value.documentRevision === "number" ? value.documentRevision : null,
|
|
3188
|
+
title: value.title,
|
|
3189
|
+
document: normalizeDocument(value.document),
|
|
3190
|
+
html: value.html,
|
|
3191
|
+
previewMode: normalizePreviewState(value.previewMode) ?? "background",
|
|
3192
|
+
previewState: normalizePreviewState(value.previewState) ?? "background",
|
|
3193
|
+
updatedAt: value.updatedAt,
|
|
3194
|
+
summary,
|
|
3195
|
+
targets: normalizeCanvasTargetStateSummaries(value.targets ?? summary.targets),
|
|
3196
|
+
overlayMounts: Array.isArray(value.overlayMounts) ? value.overlayMounts.filter(isRecord) : [],
|
|
3197
|
+
feedback: Array.isArray(value.feedback) ? value.feedback.filter(isRecord) : [],
|
|
3198
|
+
feedbackCursor: typeof value.feedbackCursor === "string" ? value.feedbackCursor : null,
|
|
3199
|
+
selection: normalizeSelection(value.selection),
|
|
3200
|
+
viewport: normalizeViewport(value.viewport),
|
|
3201
|
+
pendingMutation: value.pendingMutation === true
|
|
3202
|
+
};
|
|
3203
|
+
}
|
|
3204
|
+
function normalizeDocument(value) {
|
|
3205
|
+
return {
|
|
3206
|
+
documentId: typeof value.documentId === "string" ? value.documentId : "dc_unknown",
|
|
3207
|
+
title: typeof value.title === "string" ? value.title : "OpenDevBrowser Canvas",
|
|
3208
|
+
updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : undefined,
|
|
3209
|
+
pages: Array.isArray(value.pages) ? value.pages.flatMap((entry) => normalizePage(entry)) : [],
|
|
3210
|
+
bindings: Array.isArray(value.bindings) ? value.bindings.flatMap((entry) => normalizeBinding(entry)) : [],
|
|
3211
|
+
assets: Array.isArray(value.assets) ? value.assets.flatMap((entry) => normalizeAsset(entry)) : [],
|
|
3212
|
+
componentInventory: Array.isArray(value.componentInventory)
|
|
3213
|
+
? value.componentInventory.flatMap((entry, index) => normalizeComponentInventoryItem(entry, index))
|
|
3214
|
+
: [],
|
|
3215
|
+
tokens: normalizeTokenStore(value.tokens),
|
|
3216
|
+
meta: normalizeDocumentMeta(value.meta)
|
|
3217
|
+
};
|
|
3218
|
+
}
|
|
3219
|
+
function normalizeComponentInventoryItem(value, index) {
|
|
3220
|
+
if (!isRecord(value)) {
|
|
3221
|
+
return [];
|
|
3222
|
+
}
|
|
3223
|
+
const id = typeof value.id === "string" ? value.id : `inventory_${index + 1}`;
|
|
3224
|
+
const name = typeof value.name === "string"
|
|
3225
|
+
? value.name
|
|
3226
|
+
: typeof value.componentName === "string"
|
|
3227
|
+
? value.componentName
|
|
3228
|
+
: id;
|
|
3229
|
+
return [{
|
|
3230
|
+
id,
|
|
3231
|
+
name,
|
|
3232
|
+
componentName: typeof value.componentName === "string" ? value.componentName : undefined,
|
|
3233
|
+
sourceKind: typeof value.sourceKind === "string" ? value.sourceKind : undefined,
|
|
3234
|
+
sourceFamily: typeof value.sourceFamily === "string" ? value.sourceFamily : undefined,
|
|
3235
|
+
origin: typeof value.origin === "string" ? value.origin : undefined,
|
|
3236
|
+
framework: normalizeComponentRef(value.framework),
|
|
3237
|
+
adapter: normalizeComponentRef(value.adapter),
|
|
3238
|
+
plugin: normalizeComponentRef(value.plugin),
|
|
3239
|
+
variants: Array.isArray(value.variants) ? value.variants.filter(isRecord) : [],
|
|
3240
|
+
props: Array.isArray(value.props) ? value.props.filter(isRecord) : [],
|
|
3241
|
+
slots: Array.isArray(value.slots) ? value.slots.filter(isRecord) : [],
|
|
3242
|
+
events: Array.isArray(value.events) ? value.events.filter(isRecord) : [],
|
|
3243
|
+
content: isRecord(value.content) ? value.content : {},
|
|
3244
|
+
metadata: isRecord(value.metadata) ? value.metadata : {}
|
|
3245
|
+
}];
|
|
3246
|
+
}
|
|
3247
|
+
function normalizeComponentRef(value) {
|
|
3248
|
+
if (!isRecord(value) || typeof value.id !== "string") {
|
|
3249
|
+
return null;
|
|
3250
|
+
}
|
|
3251
|
+
return {
|
|
3252
|
+
id: value.id,
|
|
3253
|
+
label: typeof value.label === "string" ? value.label : typeof value.name === "string" ? value.name : undefined,
|
|
3254
|
+
packageName: typeof value.packageName === "string" ? value.packageName : undefined,
|
|
3255
|
+
version: typeof value.version === "string" ? value.version : undefined,
|
|
3256
|
+
metadata: isRecord(value.metadata) ? value.metadata : {}
|
|
3257
|
+
};
|
|
3258
|
+
}
|
|
3259
|
+
function normalizeTokenStore(value) {
|
|
3260
|
+
if (!isRecord(value)) {
|
|
3261
|
+
return { values: {}, collections: [], aliases: [], bindings: [], metadata: {} };
|
|
3262
|
+
}
|
|
3263
|
+
const structured = "values" in value || "collections" in value || "aliases" in value || "bindings" in value || "metadata" in value;
|
|
3264
|
+
return {
|
|
3265
|
+
values: structured && isRecord(value.values) ? value.values : structured ? {} : value,
|
|
3266
|
+
collections: Array.isArray(value.collections) ? value.collections.flatMap((entry) => normalizeTokenCollection(entry)) : [],
|
|
3267
|
+
aliases: Array.isArray(value.aliases) ? value.aliases.flatMap((entry) => normalizeTokenAlias(entry)) : [],
|
|
3268
|
+
bindings: Array.isArray(value.bindings) ? value.bindings.flatMap((entry) => normalizeTokenBinding(entry)) : [],
|
|
3269
|
+
metadata: isRecord(value.metadata) ? value.metadata : {}
|
|
3270
|
+
};
|
|
3271
|
+
}
|
|
3272
|
+
function normalizeTokenCollection(value) {
|
|
3273
|
+
if (!isRecord(value) || typeof value.id !== "string") {
|
|
3274
|
+
return [];
|
|
3275
|
+
}
|
|
3276
|
+
return [{
|
|
3277
|
+
id: value.id,
|
|
3278
|
+
name: typeof value.name === "string" ? value.name : value.id,
|
|
3279
|
+
items: Array.isArray(value.items) ? value.items.flatMap((entry) => normalizeTokenItem(entry)) : [],
|
|
3280
|
+
metadata: isRecord(value.metadata) ? value.metadata : {}
|
|
3281
|
+
}];
|
|
3282
|
+
}
|
|
3283
|
+
function normalizeTokenItem(value) {
|
|
3284
|
+
if (!isRecord(value) || typeof value.id !== "string" || typeof value.path !== "string") {
|
|
3285
|
+
return [];
|
|
3286
|
+
}
|
|
3287
|
+
return [{
|
|
3288
|
+
id: value.id,
|
|
3289
|
+
path: normalizeTokenPath(value.path),
|
|
3290
|
+
value: value.value,
|
|
3291
|
+
type: typeof value.type === "string" ? value.type : undefined,
|
|
3292
|
+
description: typeof value.description === "string" ? value.description : undefined,
|
|
3293
|
+
modes: Array.isArray(value.modes) ? value.modes.flatMap((entry) => normalizeTokenMode(entry)) : [],
|
|
3294
|
+
metadata: isRecord(value.metadata) ? value.metadata : {}
|
|
3295
|
+
}];
|
|
3296
|
+
}
|
|
3297
|
+
function normalizeTokenMode(value) {
|
|
3298
|
+
if (!isRecord(value) || typeof value.id !== "string" || typeof value.name !== "string") {
|
|
3299
|
+
return [];
|
|
3300
|
+
}
|
|
3301
|
+
return [{
|
|
3302
|
+
id: value.id,
|
|
3303
|
+
name: value.name,
|
|
3304
|
+
value: value.value,
|
|
3305
|
+
metadata: isRecord(value.metadata) ? value.metadata : {}
|
|
3306
|
+
}];
|
|
3307
|
+
}
|
|
3308
|
+
function normalizeTokenAlias(value) {
|
|
3309
|
+
if (!isRecord(value) || typeof value.path !== "string" || typeof value.targetPath !== "string") {
|
|
3310
|
+
return [];
|
|
3311
|
+
}
|
|
3312
|
+
return [{
|
|
3313
|
+
path: normalizeTokenPath(value.path),
|
|
3314
|
+
targetPath: normalizeTokenPath(value.targetPath),
|
|
3315
|
+
modeId: typeof value.modeId === "string" ? value.modeId : null,
|
|
3316
|
+
metadata: isRecord(value.metadata) ? value.metadata : {}
|
|
3317
|
+
}];
|
|
3318
|
+
}
|
|
3319
|
+
function normalizeTokenBinding(value) {
|
|
3320
|
+
if (!isRecord(value) || typeof value.path !== "string") {
|
|
3321
|
+
return [];
|
|
3322
|
+
}
|
|
3323
|
+
return [{
|
|
3324
|
+
path: normalizeTokenPath(value.path),
|
|
3325
|
+
nodeId: typeof value.nodeId === "string" ? value.nodeId : null,
|
|
3326
|
+
bindingId: typeof value.bindingId === "string" ? value.bindingId : null,
|
|
3327
|
+
property: typeof value.property === "string" ? value.property : null,
|
|
3328
|
+
metadata: isRecord(value.metadata) ? value.metadata : {}
|
|
3329
|
+
}];
|
|
3330
|
+
}
|
|
3331
|
+
function normalizeDocumentMeta(value) {
|
|
3332
|
+
if (!isRecord(value)) {
|
|
3333
|
+
return { imports: [], starter: null, adapterPlugins: [], pluginErrors: [], metadata: {} };
|
|
3334
|
+
}
|
|
3335
|
+
return {
|
|
3336
|
+
imports: Array.isArray(value.imports) ? value.imports.filter(isRecord) : [],
|
|
3337
|
+
starter: isRecord(value.starter) ? value.starter : null,
|
|
3338
|
+
adapterPlugins: Array.isArray(value.adapterPlugins) ? value.adapterPlugins.filter(isRecord) : [],
|
|
3339
|
+
pluginErrors: Array.isArray(value.pluginErrors)
|
|
3340
|
+
? value.pluginErrors.flatMap((entry) => normalizePluginError(entry))
|
|
3341
|
+
: [],
|
|
3342
|
+
metadata: isRecord(value.metadata) ? value.metadata : {}
|
|
3343
|
+
};
|
|
3344
|
+
}
|
|
3345
|
+
function normalizePluginError(value) {
|
|
3346
|
+
if (!isRecord(value) || typeof value.code !== "string" || typeof value.message !== "string") {
|
|
3347
|
+
return [];
|
|
3348
|
+
}
|
|
3349
|
+
return [{
|
|
3350
|
+
pluginId: typeof value.pluginId === "string" ? value.pluginId : undefined,
|
|
3351
|
+
code: value.code,
|
|
3352
|
+
message: value.message,
|
|
3353
|
+
details: isRecord(value.details) ? value.details : {}
|
|
3354
|
+
}];
|
|
3355
|
+
}
|
|
3356
|
+
function normalizePage(value) {
|
|
3357
|
+
if (!isRecord(value) || typeof value.id !== "string") {
|
|
3358
|
+
return [];
|
|
3359
|
+
}
|
|
3360
|
+
const pageId = value.id;
|
|
3361
|
+
return [{
|
|
3362
|
+
id: pageId,
|
|
3363
|
+
name: typeof value.name === "string" ? value.name : pageId,
|
|
3364
|
+
path: typeof value.path === "string" ? value.path : "/",
|
|
3365
|
+
rootNodeId: typeof value.rootNodeId === "string" ? value.rootNodeId : null,
|
|
3366
|
+
prototypeIds: Array.isArray(value.prototypeIds) ? value.prototypeIds.filter((entry) => typeof entry === "string") : [],
|
|
3367
|
+
nodes: Array.isArray(value.nodes) ? value.nodes.flatMap((entry) => normalizeNode(entry, pageId)) : [],
|
|
3368
|
+
metadata: isRecord(value.metadata) ? value.metadata : {}
|
|
3369
|
+
}];
|
|
3370
|
+
}
|
|
3371
|
+
function normalizeNode(value, pageId) {
|
|
3372
|
+
if (!isRecord(value) || typeof value.id !== "string") {
|
|
3373
|
+
return [];
|
|
3374
|
+
}
|
|
3375
|
+
return [{
|
|
3376
|
+
id: value.id,
|
|
3377
|
+
kind: typeof value.kind === "string" ? value.kind : "frame",
|
|
3378
|
+
name: typeof value.name === "string" ? value.name : value.id,
|
|
3379
|
+
pageId,
|
|
3380
|
+
parentId: typeof value.parentId === "string" ? value.parentId : null,
|
|
3381
|
+
childIds: Array.isArray(value.childIds) ? value.childIds.filter((entry) => typeof entry === "string") : [],
|
|
3382
|
+
rect: isRecord(value.rect) ? {
|
|
3383
|
+
x: typeof value.rect.x === "number" ? value.rect.x : 0,
|
|
3384
|
+
y: typeof value.rect.y === "number" ? value.rect.y : 0,
|
|
3385
|
+
width: typeof value.rect.width === "number" ? value.rect.width : 240,
|
|
3386
|
+
height: typeof value.rect.height === "number" ? value.rect.height : 120
|
|
3387
|
+
} : { x: 0, y: 0, width: 240, height: 120 },
|
|
3388
|
+
props: isRecord(value.props) ? value.props : {},
|
|
3389
|
+
style: isRecord(value.style) ? value.style : {},
|
|
3390
|
+
tokenRefs: isRecord(value.tokenRefs) ? value.tokenRefs : {},
|
|
3391
|
+
bindingRefs: isRecord(value.bindingRefs) ? value.bindingRefs : {},
|
|
3392
|
+
variantPatches: Array.isArray(value.variantPatches) ? value.variantPatches.filter(isRecord) : [],
|
|
3393
|
+
metadata: isRecord(value.metadata) ? value.metadata : {}
|
|
3394
|
+
}];
|
|
3395
|
+
}
|
|
3396
|
+
function normalizeBinding(value) {
|
|
3397
|
+
if (!isRecord(value) || typeof value.id !== "string" || typeof value.nodeId !== "string") {
|
|
3398
|
+
return [];
|
|
3399
|
+
}
|
|
3400
|
+
return [{
|
|
3401
|
+
id: value.id,
|
|
3402
|
+
nodeId: value.nodeId,
|
|
3403
|
+
kind: typeof value.kind === "string" ? value.kind : "component",
|
|
3404
|
+
selector: typeof value.selector === "string" ? value.selector : undefined,
|
|
3405
|
+
componentName: typeof value.componentName === "string" ? value.componentName : undefined,
|
|
3406
|
+
metadata: isRecord(value.metadata) ? value.metadata : {}
|
|
3407
|
+
}];
|
|
3408
|
+
}
|
|
3409
|
+
function normalizeAsset(value) {
|
|
3410
|
+
if (!isRecord(value) || typeof value.id !== "string") {
|
|
3411
|
+
return [];
|
|
3412
|
+
}
|
|
3413
|
+
return [{
|
|
3414
|
+
id: value.id,
|
|
3415
|
+
sourceType: typeof value.sourceType === "string" ? value.sourceType : undefined,
|
|
3416
|
+
kind: typeof value.kind === "string" ? value.kind : undefined,
|
|
3417
|
+
repoPath: typeof value.repoPath === "string" ? value.repoPath : null,
|
|
3418
|
+
url: typeof value.url === "string" ? value.url : null,
|
|
3419
|
+
mime: typeof value.mime === "string" ? value.mime : undefined,
|
|
3420
|
+
metadata: isRecord(value.metadata) ? value.metadata : {}
|
|
3421
|
+
}];
|
|
3422
|
+
}
|
|
3423
|
+
function normalizePreviewState(value) {
|
|
3424
|
+
return value === "focused" || value === "pinned" || value === "background" || value === "degraded"
|
|
3425
|
+
? value
|
|
3426
|
+
: null;
|
|
3427
|
+
}
|
|
3428
|
+
function normalizeSelection(value) {
|
|
3429
|
+
if (!isRecord(value)) {
|
|
3430
|
+
return { pageId: null, nodeId: null, targetId: null, updatedAt: new Date().toISOString() };
|
|
3431
|
+
}
|
|
3432
|
+
return {
|
|
3433
|
+
pageId: typeof value.pageId === "string" || value.pageId === null ? value.pageId : null,
|
|
3434
|
+
nodeId: typeof value.nodeId === "string" || value.nodeId === null ? value.nodeId : null,
|
|
3435
|
+
targetId: typeof value.targetId === "string" || value.targetId === null ? value.targetId : null,
|
|
3436
|
+
updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : new Date().toISOString()
|
|
3437
|
+
};
|
|
3438
|
+
}
|
|
3439
|
+
function normalizeViewport(value) {
|
|
3440
|
+
if (!isRecord(value)) {
|
|
3441
|
+
return { ...DEFAULT_EDITOR_VIEWPORT };
|
|
3442
|
+
}
|
|
3443
|
+
return {
|
|
3444
|
+
x: typeof value.x === "number" ? value.x : DEFAULT_EDITOR_VIEWPORT.x,
|
|
3445
|
+
y: typeof value.y === "number" ? value.y : DEFAULT_EDITOR_VIEWPORT.y,
|
|
3446
|
+
zoom: typeof value.zoom === "number" ? value.zoom : DEFAULT_EDITOR_VIEWPORT.zoom
|
|
3447
|
+
};
|
|
3448
|
+
}
|
|
3449
|
+
function nodeText(node) {
|
|
3450
|
+
const raw = node.props.text ?? node.metadata.text;
|
|
3451
|
+
if (raw !== undefined && raw !== null) {
|
|
3452
|
+
return typeof raw === "string" ? raw : String(raw);
|
|
3453
|
+
}
|
|
3454
|
+
return node.kind === "text" || node.kind === "note" || node.kind === "component-instance"
|
|
3455
|
+
? node.name
|
|
3456
|
+
: "";
|
|
3457
|
+
}
|
|
3458
|
+
function formatSummaryValue(value) {
|
|
3459
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
3460
|
+
return value;
|
|
3461
|
+
}
|
|
3462
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
3463
|
+
return String(value);
|
|
3464
|
+
}
|
|
3465
|
+
return "n/a";
|
|
3466
|
+
}
|
|
3467
|
+
function formatAttachedClients(clients) {
|
|
3468
|
+
if (clients.length === 0) {
|
|
3469
|
+
return "none";
|
|
3470
|
+
}
|
|
3471
|
+
return clients
|
|
3472
|
+
.map((entry) => `${entry.clientId} (${entry.role === "lease_holder" ? "lease" : "observer"})`)
|
|
3473
|
+
.join(", ");
|
|
3474
|
+
}
|
|
3475
|
+
function formatCodeSyncStatus(summary, watchConflict) {
|
|
3476
|
+
if (!summary.codeSyncState && !summary.watchState && summary.bindings.length === 0) {
|
|
3477
|
+
return "not bound";
|
|
3478
|
+
}
|
|
3479
|
+
const segments = [
|
|
3480
|
+
summary.watchState ?? "idle",
|
|
3481
|
+
summary.codeSyncState ?? "idle",
|
|
3482
|
+
summary.driftState ?? "clean",
|
|
3483
|
+
watchConflict ? "watch conflict" : null
|
|
3484
|
+
].filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
3485
|
+
return segments.join(" • ");
|
|
3486
|
+
}
|
|
3487
|
+
function formatProjectionSummary(summary) {
|
|
3488
|
+
if (summary.activeProjections.length === 0) {
|
|
3489
|
+
return "n/a";
|
|
3490
|
+
}
|
|
3491
|
+
return summary.activeProjections.join(", ");
|
|
3492
|
+
}
|
|
3493
|
+
function readSummaryLibraryList(summary, key) {
|
|
3494
|
+
const libraryPolicy = isRecord(summary.libraryPolicy) ? summary.libraryPolicy : null;
|
|
3495
|
+
return libraryPolicy ? readSummaryStringArray(libraryPolicy[key]) : [];
|
|
3496
|
+
}
|
|
3497
|
+
function readSummaryStringArray(value) {
|
|
3498
|
+
return Array.isArray(value)
|
|
3499
|
+
? value.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
|
|
3500
|
+
: [];
|
|
3501
|
+
}
|
|
3502
|
+
function formatSummaryList(values) {
|
|
3503
|
+
return values.length > 0 ? values.join(", ") : "none";
|
|
3504
|
+
}
|
|
3505
|
+
function formatTimestamp(value) {
|
|
3506
|
+
const date = new Date(value);
|
|
3507
|
+
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
|
|
3508
|
+
}
|
|
3509
|
+
function formatViewport(viewport) {
|
|
3510
|
+
return `View ${viewport.zoom.toFixed(2)}× @ ${viewport.x}, ${viewport.y}`;
|
|
3511
|
+
}
|
|
3512
|
+
function countFeedbackItems(events) {
|
|
3513
|
+
return events.filter((entry) => entry.eventType === "feedback.item").length;
|
|
3514
|
+
}
|
|
3515
|
+
function computeDocumentBounds(nodes) {
|
|
3516
|
+
if (nodes.length === 0) {
|
|
3517
|
+
return { width: 1600, height: 1200 };
|
|
3518
|
+
}
|
|
3519
|
+
const maxX = Math.max(...nodes.map((node) => node.rect.x + node.rect.width));
|
|
3520
|
+
const maxY = Math.max(...nodes.map((node) => node.rect.y + node.rect.height));
|
|
3521
|
+
return {
|
|
3522
|
+
width: Math.max(maxX + 240, 1600),
|
|
3523
|
+
height: Math.max(maxY + 240, 1200)
|
|
3524
|
+
};
|
|
3525
|
+
}
|
|
3526
|
+
function queueViewportFitIfNeeded() {
|
|
3527
|
+
if (!currentState || !isDefaultEditorViewport(currentState.viewport)) {
|
|
3528
|
+
return;
|
|
3529
|
+
}
|
|
3530
|
+
const page = getActivePage();
|
|
3531
|
+
if (!page || page.nodes.length === 0) {
|
|
3532
|
+
return;
|
|
3533
|
+
}
|
|
3534
|
+
if (fitViewportFrame !== null) {
|
|
3535
|
+
cancelAnimationFrame(fitViewportFrame);
|
|
3536
|
+
}
|
|
3537
|
+
fitViewportFrame = requestAnimationFrame(() => {
|
|
3538
|
+
fitViewportFrame = null;
|
|
3539
|
+
if (!currentState || !isDefaultEditorViewport(currentState.viewport)) {
|
|
3540
|
+
return;
|
|
3541
|
+
}
|
|
3542
|
+
const nextViewport = resolvePreferredViewport(currentState);
|
|
3543
|
+
if (nextViewport.x === currentState.viewport.x
|
|
3544
|
+
&& nextViewport.y === currentState.viewport.y
|
|
3545
|
+
&& nextViewport.zoom === currentState.viewport.zoom) {
|
|
3546
|
+
return;
|
|
3547
|
+
}
|
|
3548
|
+
currentState.viewport = nextViewport;
|
|
3549
|
+
toolbarMetaElement.textContent = formatViewport(currentState.viewport);
|
|
3550
|
+
renderStage();
|
|
3551
|
+
postViewState();
|
|
3552
|
+
schedulePersist(currentState);
|
|
3553
|
+
});
|
|
3554
|
+
}
|
|
3555
|
+
function resolvePreferredViewport(state) {
|
|
3556
|
+
const page = (activePageId ? state.document.pages.find((entry) => entry.id === activePageId) : null)
|
|
3557
|
+
?? state.document.pages.find((entry) => entry.id === state.selection.pageId)
|
|
3558
|
+
?? state.document.pages[0];
|
|
3559
|
+
const visibleNodes = page?.nodes.filter((node) => !isNodeHidden(node)) ?? [];
|
|
3560
|
+
if (!page || visibleNodes.length === 0) {
|
|
3561
|
+
return { ...DEFAULT_EDITOR_VIEWPORT };
|
|
3562
|
+
}
|
|
3563
|
+
return computeFittedViewport(visibleNodes, stageElement.clientWidth, stageElement.clientHeight);
|
|
3564
|
+
}
|
|
3565
|
+
function findNode(document, nodeId) {
|
|
3566
|
+
if (!document) {
|
|
3567
|
+
return null;
|
|
3568
|
+
}
|
|
3569
|
+
for (const page of document.pages) {
|
|
3570
|
+
const match = page.nodes.find((node) => node.id === nodeId);
|
|
3571
|
+
if (match) {
|
|
3572
|
+
return match;
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
return null;
|
|
3576
|
+
}
|
|
3577
|
+
function setNestedValue(target, path, value) {
|
|
3578
|
+
const segments = path.split(".");
|
|
3579
|
+
let current = target;
|
|
3580
|
+
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
3581
|
+
const segment = segments[index];
|
|
3582
|
+
if (!segment) {
|
|
3583
|
+
return;
|
|
3584
|
+
}
|
|
3585
|
+
const existing = current[segment];
|
|
3586
|
+
if (!isRecord(existing)) {
|
|
3587
|
+
current[segment] = {};
|
|
3588
|
+
}
|
|
3589
|
+
current = current[segment];
|
|
3590
|
+
}
|
|
3591
|
+
const last = segments[segments.length - 1];
|
|
3592
|
+
if (!last) {
|
|
3593
|
+
return;
|
|
3594
|
+
}
|
|
3595
|
+
current[last] = value;
|
|
3596
|
+
}
|
|
3597
|
+
function clamp(value, min, max) {
|
|
3598
|
+
return Math.min(Math.max(value, min), max);
|
|
3599
|
+
}
|
|
3600
|
+
function requiredElement(id) {
|
|
3601
|
+
const element = document.getElementById(id);
|
|
3602
|
+
if (!element) {
|
|
3603
|
+
throw new Error(`Missing element: ${id}`);
|
|
3604
|
+
}
|
|
3605
|
+
return element;
|
|
3606
|
+
}
|
|
3607
|
+
function isRecord(value) {
|
|
3608
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
3609
|
+
}
|