@1agh/maude 0.28.1 → 0.29.0
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 +2 -2
- package/{plugins/design/dev-server → apps/studio}/ai-banner.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/annotations-context-toolbar.tsx +285 -5
- package/{plugins/design/dev-server → apps/studio}/annotations-layer.tsx +1206 -119
- package/{plugins/design/dev-server → apps/studio}/api.ts +486 -32
- package/{plugins/design/dev-server → apps/studio}/artboard-activity-overlay.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/artboard-marquee.tsx +1 -1
- package/apps/studio/bin/_motion-sample-playwright.mjs +129 -0
- package/{plugins/design/dev-server → apps/studio}/bin/canvas-edit.sh +1 -1
- package/{plugins/design/dev-server → apps/studio}/bin/draw-proof.sh +37 -2
- package/{plugins/design/dev-server → apps/studio}/bin/preflight.sh +1 -1
- package/apps/studio/bin/read-annotations.mjs +438 -0
- package/apps/studio/bin/read-annotations.sh +11 -0
- package/{plugins/design/dev-server → apps/studio}/bin/runtime-health.sh +1 -1
- package/{plugins/design/dev-server → apps/studio}/bin/screenshot.sh +62 -2
- package/{plugins/design/dev-server → apps/studio}/bin/server-up.sh +9 -3
- package/apps/studio/bin/to-lottie-verify.html +68 -0
- package/apps/studio/bin/to-lottie.sh +128 -0
- package/{plugins/design/dev-server → apps/studio}/build.ts +5 -3
- package/{plugins/design/dev-server → apps/studio}/canvas-arrowheads.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/canvas-comment-mount.tsx +1 -1
- package/apps/studio/canvas-create.ts +104 -0
- package/{plugins/design/dev-server → apps/studio}/canvas-cursors.ts +46 -12
- package/{plugins/design/dev-server → apps/studio}/canvas-icons.tsx +60 -1
- package/{plugins/design/dev-server → apps/studio}/canvas-lib-inline.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/canvas-lib-resolver.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/canvas-lib.tsx +31 -12
- package/{plugins/design/dev-server → apps/studio}/canvas-shell.tsx +56 -41
- package/{plugins/design/dev-server → apps/studio}/client/app.jsx +1713 -530
- package/apps/studio/client/canvas-url.js +81 -0
- package/{plugins/design/dev-server → apps/studio}/client/comments-overlay.css +41 -27
- package/{plugins/design/dev-server → apps/studio}/client/index.html +2 -1
- package/apps/studio/client/styles/1-tokens-maude.css +296 -0
- package/apps/studio/client/styles/3-shell-maude.css +459 -0
- package/{plugins/design/dev-server → apps/studio}/client/styles/3-shell.css +129 -0
- package/apps/studio/client/styles/4-components-maude.css +74 -0
- package/{plugins/design/dev-server → apps/studio}/client/styles/4-components.css +357 -34
- package/apps/studio/client/styles/5-maude-overrides.css +123 -0
- package/{plugins/design/dev-server → apps/studio}/client/styles/_index.css +9 -0
- package/{plugins/design/dev-server → apps/studio}/client/styles.css +1 -1
- package/apps/studio/client/tour/overlay.jsx +191 -0
- package/apps/studio/client/tour/usage-tour.js +35 -0
- package/apps/studio/client/whats-new-seen.js +52 -0
- package/apps/studio/client/whats-new.jsx +250 -0
- package/{plugins/design/dev-server → apps/studio}/commands/annotation-strokes-command.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/commands/equal-spacing-command.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/commands/move-artboards-command.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/comments-overlay.tsx +25 -6
- package/{plugins/design/dev-server → apps/studio}/context-menu.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/contextual-toolbar.tsx +4 -7
- package/{plugins/design/dev-server → apps/studio}/cursors-overlay.tsx +1 -1
- package/apps/studio/dist/client.bundle.js +22825 -0
- package/apps/studio/dist/comment-mount.js +1960 -0
- package/apps/studio/dist/styles.css +5695 -0
- package/{plugins/design/dev-server → apps/studio}/dom-selection.ts +75 -4
- package/apps/studio/draw/animate.ts +333 -0
- package/{plugins/design/dev-server → apps/studio}/draw/brush.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/draw/composition.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/draw/geometry.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/draw/index.ts +4 -1
- package/{plugins/design/dev-server → apps/studio}/draw/layout.ts +1 -1
- package/apps/studio/draw/morph.ts +204 -0
- package/{plugins/design/dev-server → apps/studio}/draw/optimize.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/draw/palette.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/draw/primitives.ts +1 -1
- package/apps/studio/draw/serialize-animate.ts +324 -0
- package/{plugins/design/dev-server → apps/studio}/draw/serialize.ts +9 -3
- package/apps/studio/draw/test/animate.test.ts +172 -0
- package/apps/studio/draw/test/morph.test.ts +107 -0
- package/apps/studio/draw/test/serialize-animate.test.ts +142 -0
- package/{plugins/design/dev-server → apps/studio}/equal-spacing-detector.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/equal-spacing-handles.tsx +7 -29
- package/{plugins/design/dev-server → apps/studio}/export-dialog.tsx +13 -1
- package/{plugins/design/dev-server → apps/studio}/http.ts +102 -1
- package/{plugins/design/dev-server → apps/studio}/input-router.tsx +8 -1
- package/{plugins/design/dev-server → apps/studio}/inspect.ts +20 -3
- package/{plugins/design/dev-server → apps/studio}/marquee-overlay.tsx +5 -2
- package/{plugins/design/dev-server → apps/studio}/participants-chrome.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/paths.ts +15 -11
- package/{plugins/design/dev-server → apps/studio}/server.ts +18 -0
- package/{plugins/design/dev-server → apps/studio}/sync/index.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/test/annotations-layer.test.ts +63 -0
- package/{plugins/design/dev-server → apps/studio}/test/annotations-roundtrip.test.ts +97 -0
- package/apps/studio/test/annotations-text-format.test.ts +396 -0
- package/apps/studio/test/asset-api.test.ts +149 -0
- package/apps/studio/test/canvas-create-api.test.ts +361 -0
- package/{plugins/design/dev-server → apps/studio}/test/canvas-cursors.test.ts +2 -0
- package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-resolver.test.ts +1 -1
- package/apps/studio/test/canvas-media-drop.test.ts +158 -0
- package/{plugins/design/dev-server → apps/studio}/test/canvas-origin-gate.test.ts +17 -1
- package/apps/studio/test/canvas-url.test.ts +99 -0
- package/apps/studio/test/index-data-canvas-ds.test.ts +97 -0
- package/apps/studio/test/motion-sample.test.ts +35 -0
- package/{plugins/design/dev-server → apps/studio}/test/perf-harness.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/test/phase-3.6-smoke.test.ts +3 -1
- package/apps/studio/test/read-annotations.test.ts +409 -0
- package/{plugins/design/dev-server → apps/studio}/test/sanitize-annotation-svg.test.ts +108 -2
- package/{plugins/design/dev-server → apps/studio}/test/shared-doc-convergence.test.ts +1 -1
- package/apps/studio/test/tour-overlay.test.tsx +56 -0
- package/{plugins/design/dev-server → apps/studio}/test/use-annotation-resize.test.ts +43 -0
- package/{plugins/design/dev-server → apps/studio}/test/use-tool-mode.test.tsx +4 -1
- package/apps/studio/test/whats-new.test.ts +101 -0
- package/apps/studio/text-imports.d.ts +9 -0
- package/{plugins/design/dev-server → apps/studio}/tool-palette.tsx +4 -69
- package/{plugins/design/dev-server → apps/studio}/undo-hud.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/undo-stack.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-agent-presence.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-annotation-resize.tsx +40 -7
- package/{plugins/design/dev-server → apps/studio}/use-annotation-selection.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-annotations-visibility.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-artboard-drag.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-canvas-activity.tsx +1 -1
- package/apps/studio/use-canvas-media-drop.tsx +343 -0
- package/{plugins/design/dev-server → apps/studio}/use-collab.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-cursor-modifiers.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-keyboard-discipline.tsx +5 -2
- package/{plugins/design/dev-server → apps/studio}/use-selection-set.tsx +9 -1
- package/{plugins/design/dev-server → apps/studio}/use-snap-guides.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-tool-mode.tsx +3 -1
- package/{plugins/design/dev-server → apps/studio}/use-undo-stack.tsx +1 -1
- package/apps/studio/whats-new.json +139 -0
- package/apps/studio/whats-new.schema.json +70 -0
- package/apps/studio/whats-new.ts +87 -0
- package/cli/bin/maude.mjs +10 -0
- package/cli/commands/design.mjs +15 -7
- package/cli/commands/design.test.mjs +36 -0
- package/cli/commands/help.mjs +8 -2
- package/cli/commands/hub.mjs +3 -3
- package/cli/commands/hub.test.mjs +1 -1
- package/cli/commands/scenario-report.mjs +2 -9
- package/cli/lib/flow-design-integration.test.mjs +1 -1
- package/cli/lib/gitignore-block.mjs +1 -0
- package/cli/lib/plugin-cli-reachability.test.mjs +8 -7
- package/cli/lib/preflight.mjs +1 -1
- package/package.json +15 -15
- package/plugins/design/templates/brief-board.tsx.template +61 -0
- package/plugins/design/templates/canvas.tsx.template +1 -1
- package/plugins/design/templates/design-system-inspiration/core/config.json.tpl +1 -1
- package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +1 -1
- package/plugins/flow/.claude-plugin/config.schema.json +16 -0
- package/plugins/design/dev-server/dist/client.bundle.js +0 -20692
- package/plugins/design/dev-server/dist/comment-mount.js +0 -1924
- package/plugins/design/dev-server/dist/styles.css +0 -2710
- /package/{plugins/design/dev-server → apps/studio}/activity.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_enumerate-artboards-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_html-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_pdf-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_png-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_pptx-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_pw-launch.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_screenshot-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_svg-optimize.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_svg-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/asset-sweep.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/bootstrap-check.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/check-runtime-bundles.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/draw-build.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/handoff.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/prep.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/scenario-report.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/slug.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/smoke.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/svg-optimize.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/visual-sanity.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/boot-self-heal.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/canvas-build.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/canvas-edit.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/canvas-header.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/canvas-meta.schema.json +0 -0
- /package/{plugins/design/dev-server → apps/studio}/canvas-pipeline.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/hmr.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/iframe-lazy.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/styles/0-reset.css +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/styles/1-tokens.css +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/styles/2-layout.css +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/styles/5-utilities.css +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/ai-activity.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/awareness-bridge.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/git-lifecycle.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/index.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/persistence.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/protocol.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/registry.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/room.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/config.schema.json +0 -0
- /package/{plugins/design/dev-server → apps/studio}/context.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/.min-sizes.json +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/lib0_decoding.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/lib0_encoding.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/motion.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/motion_react.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/pixi-js.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react-dom.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react-dom_client.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react_jsx-dev-runtime.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react_jsx-runtime.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/y-protocols_awareness.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/y-protocols_sync.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/yjs.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/brush.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/composition.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/geometry.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/gradient.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/layout.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/optimize.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/palette.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/primitives.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/serialize.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/examples/README.md +0 -0
- /package/{plugins/design/dev-server → apps/studio}/examples/perf-100-artboards.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/_browser-bundles.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/canva-handoff-prompt.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/canva.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/html.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/index.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/pdf.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/png.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/pptx.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/scope.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/svg.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/zip.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/fs-watch.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/handoff.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/history.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/hmr-broadcast.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/locator.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/mem.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/runtime-bundle.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/server.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/agent.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/atomic-write.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/codec.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/connection-state.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/echo-guard.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/fs-mirror.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/hubs-config.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/materialize.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/migrate-seed.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/origins.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/projection.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/status.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/untrusted.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/_helpers.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/active-state.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/activity.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/ai-activity.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/annotation-strokes-command.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/annotations-api.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/annotations-draw-modifiers.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/artboard-activity-overlay.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/binary-smoke.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/boot-self-heal.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/bundle-smoke.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-build.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-edit.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-header.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-hmr-runtime.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-inline.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-motion.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-meta-api.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-pipeline.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-route.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-annotations-bridge.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-awareness-bridge.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-bridge.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-loopback.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-protocol.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-reseed-guard.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-room.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-stress.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/comment-mount.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/comments-api.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/compile-entry.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/context-resolve-tokens.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/csp-canvas-shell.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/equal-spacing-detector.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/canva.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/endpoint.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/history.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/html.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/pdf.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/png.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx-deck.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/pw-launch.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/scope.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/svg.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/zip.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-20-annotations.svg +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-21-annotations.svg +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/fs-watch.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/git-lifecycle.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/handoff-static-frames.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/handoff.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/history-rollback.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/hmr-broadcast.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/hmr-classify.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/input-router.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/locator.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/marquee-overlay.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/move-artboards-command.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/participants-chrome.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/paths.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle-error-mapping.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/server-lifecycle.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-foundation.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-migrate.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-projection.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/snap-distance-pill.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-agent.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-atomic-write.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-codec.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-connection-state.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-echo-guard.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-fs-mirror.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-hardening.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-hubs-config.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-meta-codec.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-runtime.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-status.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-untrusted.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/system-endpoint.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/tool-palette.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/undo-stack.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-agent-presence.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-annotation-selection.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-artboard-drag.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-canvas-activity.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-collab.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-cursor-modifiers.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-keyboard-discipline.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-selection-set.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-snap-guides.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-undo-stack.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/ws-handshake.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/tsconfig.json +0 -0
- /package/{plugins/design/dev-server → apps/studio}/ws.ts +0 -0
|
@@ -11,6 +11,12 @@ import { createRoot } from 'react-dom/client';
|
|
|
11
11
|
// import that Bun erases), so this pulls only string constants into the client
|
|
12
12
|
// bundle — no React, no input-router. See the tool-cursor handler below.
|
|
13
13
|
import { resolveToolCursor } from '../canvas-cursors.ts';
|
|
14
|
+
import { canvasUrl } from './canvas-url.js';
|
|
15
|
+
import { TourOverlay } from './tour/overlay.jsx';
|
|
16
|
+
import { USAGE_TOUR } from './tour/usage-tour.js';
|
|
17
|
+
import { useWhatsNew, WhatsNewPanel, WhatsNewToast } from './whats-new.jsx';
|
|
18
|
+
|
|
19
|
+
const USAGE_TOUR_STORE = 'mdcc-usage-tour-seen';
|
|
14
20
|
|
|
15
21
|
const SYSTEM_TAB = '__system__';
|
|
16
22
|
const THEME_STORE = 'mdcc-theme';
|
|
@@ -63,73 +69,12 @@ function sectionDefaultOpen(g) {
|
|
|
63
69
|
|
|
64
70
|
// ---------- Utility ----------
|
|
65
71
|
|
|
66
|
-
function urlOf(p) {
|
|
67
|
-
return '/' + p.split('/').map(encodeURIComponent).join('/');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
72
|
// Iframe src for a canvas path. TSX canvases go through _canvas-shell.html so
|
|
71
73
|
// the bundled React 19 runtime + importmap can mount the default export. HTML
|
|
72
74
|
// canvases keep the legacy "serve the file with inspector + Babel injected"
|
|
73
75
|
// path. Phase 3.6 contract; the path argument is repo-root-relative
|
|
74
|
-
// (e.g. ".design/ui/Foo.tsx").
|
|
75
|
-
|
|
76
|
-
if (!p.endsWith('.tsx')) return urlOf(p);
|
|
77
|
-
const designRel = (cfg?.designRel || '.design').replace(/^\/+|\/+$/g, '');
|
|
78
|
-
// Path under designRoot.
|
|
79
|
-
let rel = p;
|
|
80
|
-
if (rel.startsWith(designRel + '/')) rel = rel.slice(designRel.length + 1);
|
|
81
|
-
// Pass `rel` to URLSearchParams RAW — it does encoding once. Pre-encoding
|
|
82
|
-
// with encodeURIComponent then handing to URLSearchParams produced
|
|
83
|
-
// `Docs%2520Site.tsx` (the `%` of `%20` got re-encoded as `%25`) and broke
|
|
84
|
-
// every UI canvas with a space in its filename.
|
|
85
|
-
const params = new URLSearchParams();
|
|
86
|
-
params.set('canvas', rel);
|
|
87
|
-
params.set('designRel', designRel);
|
|
88
|
-
// Gallery thumbnails suppress the shell-owned comment layer (`?comments=0`)
|
|
89
|
-
// so previews stay non-interactive; opening the same canvas as a real tab
|
|
90
|
-
// omits the flag and gets comments. See canvas-comment-mount.tsx.
|
|
91
|
-
if (opts?.thumbnail) params.set('comments', '0');
|
|
92
|
-
const ds0 = cfg?.designSystems?.[0];
|
|
93
|
-
// Specimen detection: anything under `system/<ds>/preview/` belongs to that
|
|
94
|
-
// specific DS, so it must render with *that* DS's tokens — not always the
|
|
95
|
-
// first one. In a multi-DS project this is what keeps each design system's
|
|
96
|
-
// preview distinct (beta previews use beta tokens, not alpha's).
|
|
97
|
-
const specMatch = rel.match(/^system\/([^/]+)\/preview\//);
|
|
98
|
-
const specDsEntry = specMatch
|
|
99
|
-
? cfg?.designSystems?.find(
|
|
100
|
-
(d) => d.path === `system/${specMatch[1]}` || d.path.endsWith(`/${specMatch[1]}`)
|
|
101
|
-
)
|
|
102
|
-
: null;
|
|
103
|
-
// Resolve tokens path. For a specimen, prefer the matching DS's tokensCssRel
|
|
104
|
-
// (fall back to the `system/<ds>/colors_and_type.css` convention). Otherwise
|
|
105
|
-
// prefer the first designSystem's tokensCssRel — the project's authoritative
|
|
106
|
-
// tokens file. The top-level cfg.tokensCssRel is the legacy default
|
|
107
|
-
// (`system/colors_and_type.css`) and usually doesn't exist post-bootstrap.
|
|
108
|
-
const tokens = specMatch
|
|
109
|
-
? specDsEntry?.tokensCssRel || `system/${specMatch[1]}/colors_and_type.css`
|
|
110
|
-
: ds0?.tokensCssRel || cfg?.tokensCssRel;
|
|
111
|
-
if (tokens) params.set('tokens', tokens);
|
|
112
|
-
if (cfg?.componentsCssRel) params.set('components', cfg.componentsCssRel);
|
|
113
|
-
if (specMatch) {
|
|
114
|
-
const dsName = specMatch[1];
|
|
115
|
-
params.set('layout', `system/${dsName}/preview/_layout.css`);
|
|
116
|
-
if (!cfg?.componentsCssRel) {
|
|
117
|
-
params.set('components', `system/${dsName}/preview/_components.css`);
|
|
118
|
-
}
|
|
119
|
-
} else if (ds0?.path) {
|
|
120
|
-
// UI canvas — load the project DS's `_components.css` so the dc-canvas /
|
|
121
|
-
// dc-section / dc-artboard chrome (and any DS classes the canvas reuses)
|
|
122
|
-
// renders correctly.
|
|
123
|
-
if (!cfg?.componentsCssRel) {
|
|
124
|
-
params.set('components', `${ds0.path}/preview/_components.css`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
// T2 (9.1-A) — load the iframe from the segregated canvas origin when the
|
|
128
|
-
// server advertises one; fall back to a same-origin relative URL otherwise
|
|
129
|
-
// (older server, tests). The trailing path + query are identical either way.
|
|
130
|
-
const origin = cfg?.canvasOrigin || '';
|
|
131
|
-
return `${origin}/_canvas-shell.html?${params.toString()}`;
|
|
132
|
-
}
|
|
76
|
+
// (e.g. ".design/ui/Foo.tsx"). Pure resolver extracted to ./canvas-url.js so
|
|
77
|
+
// the token-resolution branches are unit-testable without a DOM (DDR-093).
|
|
133
78
|
|
|
134
79
|
function basename(p) {
|
|
135
80
|
return p.split('/').pop();
|
|
@@ -298,6 +243,556 @@ function Icon({ d, size = 14, color }) {
|
|
|
298
243
|
);
|
|
299
244
|
}
|
|
300
245
|
|
|
246
|
+
// ───── maude DS icon set (Plan B) ─────
|
|
247
|
+
// Thin-stroke (1.4) 16×16 geometric glyphs lifted from .design/ui/Studio.tsx —
|
|
248
|
+
// the icon vocabulary the ported `.st-*` chrome composes with. Distinct from the
|
|
249
|
+
// legacy 24×24 single-path `Icon` above (kept for not-yet-ported chrome). Grown
|
|
250
|
+
// per slice; this slice (menubar/statusbar) needs sparkle/check/sun/moon plus a
|
|
251
|
+
// few the sidebar + panels reuse later.
|
|
252
|
+
const STICONS = {
|
|
253
|
+
'chevron-down': <polyline points="3.5 6 8 10.5 12.5 6" />,
|
|
254
|
+
'chevron-right': <polyline points="6 3.5 10.5 8 6 12.5" />,
|
|
255
|
+
file: (
|
|
256
|
+
<>
|
|
257
|
+
<path d="M4 2h5l3 3v9H4z" />
|
|
258
|
+
<polyline points="9 2 9 5 12 5" />
|
|
259
|
+
</>
|
|
260
|
+
),
|
|
261
|
+
folder: <path d="M2 4.5h4l1.3 1.5H14V13H2z" />,
|
|
262
|
+
search: (
|
|
263
|
+
<>
|
|
264
|
+
<circle cx="7" cy="7" r="4" />
|
|
265
|
+
<line x1="10" y1="10" x2="13.5" y2="13.5" />
|
|
266
|
+
</>
|
|
267
|
+
),
|
|
268
|
+
plus: (
|
|
269
|
+
<>
|
|
270
|
+
<line x1="8" y1="3" x2="8" y2="13" />
|
|
271
|
+
<line x1="3" y1="8" x2="13" y2="8" />
|
|
272
|
+
</>
|
|
273
|
+
),
|
|
274
|
+
check: <polyline points="3 8.2 6.4 11.5 13 4.2" />,
|
|
275
|
+
x: (
|
|
276
|
+
<>
|
|
277
|
+
<line x1="4.3" y1="4.3" x2="11.7" y2="11.7" />
|
|
278
|
+
<line x1="11.7" y1="4.3" x2="4.3" y2="11.7" />
|
|
279
|
+
</>
|
|
280
|
+
),
|
|
281
|
+
sun: (
|
|
282
|
+
<>
|
|
283
|
+
<circle cx="8" cy="8" r="2.6" />
|
|
284
|
+
<line x1="8" y1="1.5" x2="8" y2="3" />
|
|
285
|
+
<line x1="8" y1="13" x2="8" y2="14.5" />
|
|
286
|
+
<line x1="1.5" y1="8" x2="3" y2="8" />
|
|
287
|
+
<line x1="13" y1="8" x2="14.5" y2="8" />
|
|
288
|
+
<line x1="3.4" y1="3.4" x2="4.4" y2="4.4" />
|
|
289
|
+
<line x1="11.6" y1="11.6" x2="12.6" y2="12.6" />
|
|
290
|
+
<line x1="12.6" y1="3.4" x2="11.6" y2="4.4" />
|
|
291
|
+
<line x1="4.4" y1="11.6" x2="3.4" y2="12.6" />
|
|
292
|
+
</>
|
|
293
|
+
),
|
|
294
|
+
moon: <path d="M12.5 9.6A5 5 0 1 1 7 3a4 4 0 0 0 5.5 6.6z" />,
|
|
295
|
+
sparkle: (
|
|
296
|
+
<path d="M8 1.8l1.4 4.8L14 8l-4.6 1.4L8 14.2l-1.4-4.8L2 8l4.6-1.4z" fill="currentColor" stroke="none" />
|
|
297
|
+
),
|
|
298
|
+
'panel-left': (
|
|
299
|
+
<>
|
|
300
|
+
<rect x="2.5" y="3" width="11" height="10" rx="1.5" />
|
|
301
|
+
<line x1="6.4" y1="3" x2="6.4" y2="13" />
|
|
302
|
+
</>
|
|
303
|
+
),
|
|
304
|
+
resolve: (
|
|
305
|
+
<>
|
|
306
|
+
<circle cx="8" cy="8" r="5.6" />
|
|
307
|
+
<polyline points="5.4 8 7.2 9.9 10.6 6" />
|
|
308
|
+
</>
|
|
309
|
+
),
|
|
310
|
+
reopen: (
|
|
311
|
+
<>
|
|
312
|
+
<path d="M3.2 8a5 5 0 1 1 1.4 3.5" />
|
|
313
|
+
<polyline points="3.2 11.4 3.2 8 6.6 8" />
|
|
314
|
+
</>
|
|
315
|
+
),
|
|
316
|
+
layers: (
|
|
317
|
+
<>
|
|
318
|
+
<polygon points="8 2.2 13.8 5.5 8 8.8 2.2 5.5" />
|
|
319
|
+
<polyline points="2.2 9 8 12.3 13.8 9" />
|
|
320
|
+
</>
|
|
321
|
+
),
|
|
322
|
+
sliders: (
|
|
323
|
+
<>
|
|
324
|
+
<line x1="3" y1="5" x2="13" y2="5" />
|
|
325
|
+
<circle cx="6" cy="5" r="1.7" fill="currentColor" />
|
|
326
|
+
<line x1="3" y1="11" x2="13" y2="11" />
|
|
327
|
+
<circle cx="10" cy="11" r="1.7" fill="currentColor" />
|
|
328
|
+
</>
|
|
329
|
+
),
|
|
330
|
+
code: (
|
|
331
|
+
<>
|
|
332
|
+
<polyline points="6 5 3 8 6 11" />
|
|
333
|
+
<polyline points="10 5 13 8 10 11" />
|
|
334
|
+
</>
|
|
335
|
+
),
|
|
336
|
+
download: (
|
|
337
|
+
<>
|
|
338
|
+
<line x1="8" y1="2.5" x2="8" y2="10" />
|
|
339
|
+
<polyline points="4.5 7 8 10.5 11.5 7" />
|
|
340
|
+
<polyline points="3 12.8 3 13.6 13 13.6 13 12.8" />
|
|
341
|
+
</>
|
|
342
|
+
),
|
|
343
|
+
reload: (
|
|
344
|
+
<>
|
|
345
|
+
<path d="M3.2 8a5 5 0 1 1 1.4 3.5" />
|
|
346
|
+
<polyline points="3.2 11.4 3.2 8 6.6 8" />
|
|
347
|
+
</>
|
|
348
|
+
),
|
|
349
|
+
help: (
|
|
350
|
+
<>
|
|
351
|
+
<circle cx="8" cy="8" r="6" />
|
|
352
|
+
<path d="M6.3 6.2a1.8 1.8 0 1 1 2.3 1.9c-.5.2-.6.5-.6 1v.3" />
|
|
353
|
+
<line x1="8" y1="11.4" x2="8" y2="11.5" />
|
|
354
|
+
</>
|
|
355
|
+
),
|
|
356
|
+
// Export-format glyphs (Plan C) — one distinct mark per format card.
|
|
357
|
+
image: (
|
|
358
|
+
<>
|
|
359
|
+
<rect x="2.5" y="3" width="11" height="10" rx="1.5" />
|
|
360
|
+
<circle cx="6" cy="6.3" r="1.1" />
|
|
361
|
+
<path d="M3 12l3-2.8 2.2 1.8 2.4-3L13.5 12" />
|
|
362
|
+
</>
|
|
363
|
+
),
|
|
364
|
+
vector: (
|
|
365
|
+
<>
|
|
366
|
+
<path d="M3.6 11.2C6 5 10 5 12.4 11.2" />
|
|
367
|
+
<rect x="1.7" y="9.8" width="2.6" height="2.6" rx="0.4" />
|
|
368
|
+
<rect x="11.7" y="9.8" width="2.6" height="2.6" rx="0.4" />
|
|
369
|
+
<rect x="6.7" y="2.6" width="2.6" height="2.6" rx="0.4" />
|
|
370
|
+
</>
|
|
371
|
+
),
|
|
372
|
+
presentation: (
|
|
373
|
+
<>
|
|
374
|
+
<rect x="2.5" y="3" width="11" height="7.4" rx="1" />
|
|
375
|
+
<line x1="8" y1="10.4" x2="8" y2="13" />
|
|
376
|
+
<line x1="5.6" y1="13.4" x2="10.4" y2="13.4" />
|
|
377
|
+
</>
|
|
378
|
+
),
|
|
379
|
+
archive: (
|
|
380
|
+
<>
|
|
381
|
+
<rect x="2.5" y="3" width="11" height="3" rx="0.8" />
|
|
382
|
+
<path d="M3.6 6v6.2a1 1 0 0 0 1 1h6.8a1 1 0 0 0 1-1V6" />
|
|
383
|
+
<line x1="6.6" y1="8.8" x2="9.4" y2="8.8" />
|
|
384
|
+
</>
|
|
385
|
+
),
|
|
386
|
+
external: (
|
|
387
|
+
<>
|
|
388
|
+
<path d="M11 8.5V12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h3.5" />
|
|
389
|
+
<polyline points="9.5 3 13 3 13 6.5" />
|
|
390
|
+
<line x1="13" y1="3" x2="7.6" y2="8.4" />
|
|
391
|
+
</>
|
|
392
|
+
),
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// ⌘K command palette — the mockup's signature surface, wired to real shell
|
|
396
|
+
// actions (theme, system view, comments, reload, help, what's new, new board).
|
|
397
|
+
// Scoped to shell-doable actions only — in-canvas export lives in the iframe.
|
|
398
|
+
function CommandPalette({ open, onClose, actions }) {
|
|
399
|
+
const [q, setQ] = useState('');
|
|
400
|
+
const [active, setActive] = useState(0);
|
|
401
|
+
useEffect(() => {
|
|
402
|
+
if (open) {
|
|
403
|
+
setQ('');
|
|
404
|
+
setActive(0);
|
|
405
|
+
}
|
|
406
|
+
}, [open]);
|
|
407
|
+
const filtered = useMemo(() => {
|
|
408
|
+
const needle = q.trim().toLowerCase();
|
|
409
|
+
if (!needle) return actions;
|
|
410
|
+
return actions.filter(
|
|
411
|
+
(a) =>
|
|
412
|
+
a.label.toLowerCase().includes(needle) ||
|
|
413
|
+
(a.group && a.group.toLowerCase().includes(needle))
|
|
414
|
+
);
|
|
415
|
+
}, [q, actions]);
|
|
416
|
+
useEffect(() => {
|
|
417
|
+
if (active >= filtered.length) setActive(0);
|
|
418
|
+
}, [filtered.length, active]);
|
|
419
|
+
if (!open) return null;
|
|
420
|
+
const run = (a) => {
|
|
421
|
+
onClose();
|
|
422
|
+
a.run();
|
|
423
|
+
};
|
|
424
|
+
return (
|
|
425
|
+
<div
|
|
426
|
+
className="st-scrim"
|
|
427
|
+
role="presentation"
|
|
428
|
+
onMouseDown={(e) => {
|
|
429
|
+
if (e.target === e.currentTarget) onClose();
|
|
430
|
+
}}
|
|
431
|
+
>
|
|
432
|
+
<div className="st-palette" role="dialog" aria-modal="true" aria-label="Command palette">
|
|
433
|
+
<div className="st-pal-search">
|
|
434
|
+
<StIcon name="search" size={18} />
|
|
435
|
+
<input
|
|
436
|
+
// biome-ignore lint/a11y/noAutofocus: command palette opens on an explicit ⌘K.
|
|
437
|
+
autoFocus
|
|
438
|
+
placeholder="Type a command or search…"
|
|
439
|
+
value={q}
|
|
440
|
+
aria-label="Command search"
|
|
441
|
+
onChange={(e) => setQ(e.target.value)}
|
|
442
|
+
onKeyDown={(e) => {
|
|
443
|
+
if (e.key === 'Escape') {
|
|
444
|
+
e.preventDefault();
|
|
445
|
+
onClose();
|
|
446
|
+
} else if (e.key === 'ArrowDown') {
|
|
447
|
+
e.preventDefault();
|
|
448
|
+
setActive((i) => Math.min(filtered.length - 1, i + 1));
|
|
449
|
+
} else if (e.key === 'ArrowUp') {
|
|
450
|
+
e.preventDefault();
|
|
451
|
+
setActive((i) => Math.max(0, i - 1));
|
|
452
|
+
} else if (e.key === 'Enter') {
|
|
453
|
+
e.preventDefault();
|
|
454
|
+
if (filtered[active]) run(filtered[active]);
|
|
455
|
+
}
|
|
456
|
+
}}
|
|
457
|
+
/>
|
|
458
|
+
<Kbd>⌘K</Kbd>
|
|
459
|
+
</div>
|
|
460
|
+
<div className="st-pal-list">
|
|
461
|
+
{filtered.length === 0 ? (
|
|
462
|
+
<div className="st-pal-empty">No matching command.</div>
|
|
463
|
+
) : (
|
|
464
|
+
filtered.map((a, i) => {
|
|
465
|
+
// Emit a group header when the group changes (flat list → grouped
|
|
466
|
+
// display; keyboard nav still indexes across the whole array).
|
|
467
|
+
const header =
|
|
468
|
+
a.group && (i === 0 || filtered[i - 1].group !== a.group) ? (
|
|
469
|
+
<div className="st-pal-group" key={'g-' + a.group}>
|
|
470
|
+
{a.group}
|
|
471
|
+
</div>
|
|
472
|
+
) : null;
|
|
473
|
+
return (
|
|
474
|
+
<Fragment key={a.id}>
|
|
475
|
+
{header}
|
|
476
|
+
<button
|
|
477
|
+
type="button"
|
|
478
|
+
className={'st-pal-item' + (i === active ? ' is-active' : '')}
|
|
479
|
+
onMouseEnter={() => setActive(i)}
|
|
480
|
+
onClick={() => run(a)}
|
|
481
|
+
>
|
|
482
|
+
<span className="st-pal-icon">
|
|
483
|
+
<StIcon name={a.icon} size={15} />
|
|
484
|
+
</span>
|
|
485
|
+
<span className="st-pal-label">{a.label}</span>
|
|
486
|
+
{a.kbd ? (
|
|
487
|
+
<span className="st-pal-kbd">
|
|
488
|
+
<Kbd>{a.kbd}</Kbd>
|
|
489
|
+
</span>
|
|
490
|
+
) : null}
|
|
491
|
+
</button>
|
|
492
|
+
</Fragment>
|
|
493
|
+
);
|
|
494
|
+
})
|
|
495
|
+
)}
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
</div>
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function StIcon({ name, size = 16, className }) {
|
|
503
|
+
return (
|
|
504
|
+
<svg
|
|
505
|
+
className={className}
|
|
506
|
+
width={size}
|
|
507
|
+
height={size}
|
|
508
|
+
viewBox="0 0 16 16"
|
|
509
|
+
fill="none"
|
|
510
|
+
stroke="currentColor"
|
|
511
|
+
strokeWidth="1.4"
|
|
512
|
+
strokeLinecap="round"
|
|
513
|
+
strokeLinejoin="round"
|
|
514
|
+
aria-hidden="true"
|
|
515
|
+
style={{ flex: 'none' }}
|
|
516
|
+
>
|
|
517
|
+
{STICONS[name]}
|
|
518
|
+
</svg>
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// P3 (Plan C) — menubar presence avatar (matches `.design/ui/Studio.tsx` Avatar).
|
|
523
|
+
// Up to two uppercase glyphs from a name; falls back to "?" for empties.
|
|
524
|
+
function initialsOf(name) {
|
|
525
|
+
if (!name || typeof name !== 'string') return '?';
|
|
526
|
+
const parts = name.trim().split(/[\s._-]+/).filter(Boolean);
|
|
527
|
+
if (!parts.length) return '?';
|
|
528
|
+
// Single-token names (e.g. a git username "1aGh") → first two chars, so the
|
|
529
|
+
// avatar reads as initials rather than a lone count badge.
|
|
530
|
+
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
|
531
|
+
return ((parts[0][0] || '') + (parts[parts.length - 1][0] || '')).toUpperCase() || '?';
|
|
532
|
+
}
|
|
533
|
+
function StAvatar({ initials, hue, title }) {
|
|
534
|
+
return (
|
|
535
|
+
<span className="st-avatar" style={{ background: hue }} title={title} aria-label={title}>
|
|
536
|
+
{initials}
|
|
537
|
+
</span>
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function Kbd({ children }) {
|
|
542
|
+
return <span className="kbd">{children}</span>;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// T5 (Plan C) — shell-level Export & Handoff dialog (maude `.st-dialog`), per
|
|
546
|
+
// `.design/ui/Studio.tsx` HandoffBoard. Wired to the privileged main-origin
|
|
547
|
+
// `POST /_api/export` (7 real formats × scopes). The shadcn card is HANDOFF —
|
|
548
|
+
// a privileged disk-write kept off HTTP routes (DDR-054), so it surfaces the
|
|
549
|
+
// `/design:handoff` command instead. PPTX/Canva kept reachable (no silent cap).
|
|
550
|
+
const EXPORT_CARDS = [
|
|
551
|
+
{ id: 'png', label: 'PNG', sub: 'raster · 2×', icon: 'image', format: 'png', options: { scale: 2 } },
|
|
552
|
+
{ id: 'pdf', label: 'PDF', sub: 'vector · print', icon: 'file', format: 'pdf' },
|
|
553
|
+
{ id: 'svg', label: 'SVG', sub: 'per artboard', icon: 'vector', format: 'svg' },
|
|
554
|
+
{ id: 'html', label: 'HTML', sub: 'self-contained', icon: 'code', format: 'html' },
|
|
555
|
+
{ id: 'pptx', label: 'PPTX', sub: 'slides', icon: 'presentation', format: 'pptx' },
|
|
556
|
+
{ id: 'canva', label: 'Canva', sub: 'handoff bundle', icon: 'external', format: 'canva' },
|
|
557
|
+
{ id: 'zip', label: 'ZIP', sub: 'project bundle', icon: 'archive', format: 'zip' },
|
|
558
|
+
{ id: 'shadcn', label: 'AI handoff', sub: 'production drop', icon: 'sparkle', handoff: true },
|
|
559
|
+
];
|
|
560
|
+
|
|
561
|
+
// Mirrors export-dialog.tsx (the in-canvas dialog) so both entry points offer
|
|
562
|
+
// the same settings. Scope validity + PNG scale presets are ported verbatim.
|
|
563
|
+
const EXPORT_SCOPE_LABELS = {
|
|
564
|
+
selection: 'Current selection',
|
|
565
|
+
artboard: 'Active artboard',
|
|
566
|
+
'canvas-as-separate': 'Canvas · artboards separate',
|
|
567
|
+
'project-raw': 'Whole project (raw)',
|
|
568
|
+
};
|
|
569
|
+
const EXPORT_VALID_SCOPES = {
|
|
570
|
+
png: ['selection', 'artboard', 'canvas-as-separate'],
|
|
571
|
+
pdf: ['selection', 'artboard', 'canvas-as-separate'],
|
|
572
|
+
svg: ['selection', 'artboard', 'canvas-as-separate'],
|
|
573
|
+
html: ['artboard', 'canvas-as-separate'],
|
|
574
|
+
pptx: ['canvas-as-separate'],
|
|
575
|
+
canva: ['canvas-as-separate'],
|
|
576
|
+
zip: ['project-raw'],
|
|
577
|
+
};
|
|
578
|
+
const PNG_SCALES = [
|
|
579
|
+
{ value: 1, label: '1× (native)' },
|
|
580
|
+
{ value: 2, label: '2× (retina)' },
|
|
581
|
+
{ value: 3, label: '3× (max)' },
|
|
582
|
+
];
|
|
583
|
+
|
|
584
|
+
function ExportDialog({ mode, initialScope, activePath, onClose }) {
|
|
585
|
+
const [sel, setSel] = useState(mode === 'handoff' ? 'shadcn' : 'png');
|
|
586
|
+
const [scope, setScope] = useState(
|
|
587
|
+
initialScope && EXPORT_SCOPE_LABELS[initialScope] ? initialScope : 'artboard'
|
|
588
|
+
);
|
|
589
|
+
const [scale, setScale] = useState(2);
|
|
590
|
+
const [busy, setBusy] = useState(false);
|
|
591
|
+
const [status, setStatus] = useState(null); // { ok, msg }
|
|
592
|
+
const [recent, setRecent] = useState([]);
|
|
593
|
+
const card = EXPORT_CARDS.find((c) => c.id === sel) || EXPORT_CARDS[0];
|
|
594
|
+
const validScopes = card.handoff ? [] : EXPORT_VALID_SCOPES[card.format] || ['artboard'];
|
|
595
|
+
|
|
596
|
+
// Keep the scope valid for the chosen format (pptx/zip etc. only allow a
|
|
597
|
+
// subset) — mirrors VALID_SCOPES_PER_FORMAT in the in-canvas dialog.
|
|
598
|
+
useEffect(() => {
|
|
599
|
+
if (validScopes.length && !validScopes.includes(scope)) setScope(validScopes[0]);
|
|
600
|
+
}, [validScopes, scope]);
|
|
601
|
+
|
|
602
|
+
const loadRecent = useCallback(() => {
|
|
603
|
+
fetch('/_api/export-history')
|
|
604
|
+
.then((r) => r.json())
|
|
605
|
+
.then((d) => setRecent(Array.isArray(d?.history) ? d.history.slice(0, 6) : []))
|
|
606
|
+
.catch(() => {});
|
|
607
|
+
}, []);
|
|
608
|
+
useEffect(() => {
|
|
609
|
+
loadRecent();
|
|
610
|
+
}, [loadRecent]);
|
|
611
|
+
|
|
612
|
+
useEffect(() => {
|
|
613
|
+
function onKey(e) {
|
|
614
|
+
if (e.key === 'Escape') {
|
|
615
|
+
e.preventDefault();
|
|
616
|
+
onClose();
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
window.addEventListener('keydown', onKey);
|
|
620
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
621
|
+
}, [onClose]);
|
|
622
|
+
|
|
623
|
+
async function doExport() {
|
|
624
|
+
if (card.handoff) {
|
|
625
|
+
const p = activePath && activePath !== SYSTEM_TAB ? activePath : '<canvas>.tsx';
|
|
626
|
+
const cmd = `/design:handoff ${p}`;
|
|
627
|
+
try {
|
|
628
|
+
await navigator.clipboard?.writeText(cmd);
|
|
629
|
+
} catch {}
|
|
630
|
+
setStatus({ ok: true, msg: `Copied: ${cmd} — run it in Claude Code.` });
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
setBusy(true);
|
|
634
|
+
setStatus(null);
|
|
635
|
+
const options = card.format === 'png' ? { scale } : {};
|
|
636
|
+
try {
|
|
637
|
+
const r = await fetch('/_api/export', {
|
|
638
|
+
method: 'POST',
|
|
639
|
+
headers: { 'content-type': 'application/json' },
|
|
640
|
+
body: JSON.stringify({ format: card.format, scope, options }),
|
|
641
|
+
});
|
|
642
|
+
if (!r.ok) {
|
|
643
|
+
setStatus({ ok: false, msg: (await r.text()) || `Export failed (${r.status})` });
|
|
644
|
+
setBusy(false);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const disp = r.headers.get('Content-Disposition') || '';
|
|
648
|
+
const fn = /filename="([^"]+)"/.exec(disp);
|
|
649
|
+
const filename = (fn && fn[1]) || `export.${card.format}`;
|
|
650
|
+
const blob = await r.blob();
|
|
651
|
+
const url = URL.createObjectURL(blob);
|
|
652
|
+
const a = document.createElement('a');
|
|
653
|
+
a.href = url;
|
|
654
|
+
a.download = filename;
|
|
655
|
+
document.body.appendChild(a);
|
|
656
|
+
a.click();
|
|
657
|
+
a.remove();
|
|
658
|
+
URL.revokeObjectURL(url);
|
|
659
|
+
setStatus({ ok: true, msg: `Exported ${filename}` });
|
|
660
|
+
loadRecent();
|
|
661
|
+
} catch (err) {
|
|
662
|
+
setStatus({ ok: false, msg: err && err.message ? err.message : String(err) });
|
|
663
|
+
}
|
|
664
|
+
setBusy(false);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return (
|
|
668
|
+
<div
|
|
669
|
+
className="st-scrim"
|
|
670
|
+
role="presentation"
|
|
671
|
+
onMouseDown={(e) => {
|
|
672
|
+
if (e.target === e.currentTarget) onClose();
|
|
673
|
+
}}
|
|
674
|
+
>
|
|
675
|
+
<div className="st-dialog" role="dialog" aria-modal="true" aria-label="Export and handoff">
|
|
676
|
+
<div className="st-dialog-hd">
|
|
677
|
+
<span className="st-dialog-title">Export & handoff</span>
|
|
678
|
+
<button type="button" className="st-iconbtn" aria-label="Close" onClick={onClose}>
|
|
679
|
+
<StIcon name="x" size={15} />
|
|
680
|
+
</button>
|
|
681
|
+
</div>
|
|
682
|
+
<div className="st-dialog-bd">
|
|
683
|
+
<div className="st-rp-hd">
|
|
684
|
+
{activePath && activePath !== SYSTEM_TAB
|
|
685
|
+
? `Format · ${displayName(basename(activePath))}`
|
|
686
|
+
: 'Format'}
|
|
687
|
+
</div>
|
|
688
|
+
<div className="st-fmt-grid">
|
|
689
|
+
{EXPORT_CARDS.map((c) => (
|
|
690
|
+
<button
|
|
691
|
+
type="button"
|
|
692
|
+
key={c.id}
|
|
693
|
+
className={'st-fmt' + (c.id === sel ? ' is-on' : '')}
|
|
694
|
+
onClick={() => {
|
|
695
|
+
setSel(c.id);
|
|
696
|
+
setStatus(null);
|
|
697
|
+
}}
|
|
698
|
+
>
|
|
699
|
+
<StIcon name={c.icon} size={16} />
|
|
700
|
+
<span className="st-fmt-name">{c.label}</span>
|
|
701
|
+
<span className="st-fmt-sub">{c.sub}</span>
|
|
702
|
+
</button>
|
|
703
|
+
))}
|
|
704
|
+
</div>
|
|
705
|
+
{!card.handoff && (
|
|
706
|
+
<div className="st-dialog-row">
|
|
707
|
+
<label className="st-dialog-lbl" htmlFor="st-export-scope">
|
|
708
|
+
Scope
|
|
709
|
+
</label>
|
|
710
|
+
<select
|
|
711
|
+
id="st-export-scope"
|
|
712
|
+
className="st-select"
|
|
713
|
+
value={scope}
|
|
714
|
+
onChange={(e) => setScope(e.target.value)}
|
|
715
|
+
>
|
|
716
|
+
{validScopes.map((s) => (
|
|
717
|
+
<option key={s} value={s}>
|
|
718
|
+
{EXPORT_SCOPE_LABELS[s]}
|
|
719
|
+
</option>
|
|
720
|
+
))}
|
|
721
|
+
</select>
|
|
722
|
+
</div>
|
|
723
|
+
)}
|
|
724
|
+
{!card.handoff && card.format === 'png' && (
|
|
725
|
+
<div className="st-dialog-row">
|
|
726
|
+
<label className="st-dialog-lbl" htmlFor="st-export-size">
|
|
727
|
+
Size
|
|
728
|
+
</label>
|
|
729
|
+
<select
|
|
730
|
+
id="st-export-size"
|
|
731
|
+
className="st-select"
|
|
732
|
+
value={scale}
|
|
733
|
+
onChange={(e) => setScale(Number(e.target.value))}
|
|
734
|
+
>
|
|
735
|
+
{PNG_SCALES.map((s) => (
|
|
736
|
+
<option key={s.value} value={s.value}>
|
|
737
|
+
{s.label}
|
|
738
|
+
</option>
|
|
739
|
+
))}
|
|
740
|
+
</select>
|
|
741
|
+
</div>
|
|
742
|
+
)}
|
|
743
|
+
{!card.handoff && card.format === 'png' && (
|
|
744
|
+
<div className="st-mono" style={{ fontSize: 11, color: 'var(--fg-3)' }}>
|
|
745
|
+
Resolution multiplier — {scale}× ≈ {1440 * scale}×{900 * scale} for a 1440×900 artboard.
|
|
746
|
+
</div>
|
|
747
|
+
)}
|
|
748
|
+
{card.handoff && (
|
|
749
|
+
<div className="callout callout--info" style={{ fontSize: 12 }}>
|
|
750
|
+
Hands the active canvas off to production. Copies{' '}
|
|
751
|
+
<span className="st-mono">/design:handoff <path></span> — run it in Claude Code to
|
|
752
|
+
emit a ready-to-drop production component next to the canvas.
|
|
753
|
+
</div>
|
|
754
|
+
)}
|
|
755
|
+
{status && (
|
|
756
|
+
<div
|
|
757
|
+
className={'callout ' + (status.ok ? 'callout--success' : 'callout--error')}
|
|
758
|
+
style={{ fontSize: 12 }}
|
|
759
|
+
>
|
|
760
|
+
{status.msg}
|
|
761
|
+
</div>
|
|
762
|
+
)}
|
|
763
|
+
{recent.length > 0 && (
|
|
764
|
+
<div className="st-export-recent">
|
|
765
|
+
<div className="st-rp-hd">Recent</div>
|
|
766
|
+
{recent.map((h, i) => (
|
|
767
|
+
<div className="st-export-recent-row" key={i}>
|
|
768
|
+
<span>
|
|
769
|
+
{String(h.format || '').toUpperCase()} ·{' '}
|
|
770
|
+
{EXPORT_SCOPE_LABELS[h.scope] || h.scope}
|
|
771
|
+
</span>
|
|
772
|
+
<span className="st-mono">{h.filename}</span>
|
|
773
|
+
</div>
|
|
774
|
+
))}
|
|
775
|
+
</div>
|
|
776
|
+
)}
|
|
777
|
+
</div>
|
|
778
|
+
<div className="st-dialog-ft">
|
|
779
|
+
<button type="button" className="btn btn--ghost" onClick={onClose}>
|
|
780
|
+
Cancel
|
|
781
|
+
</button>
|
|
782
|
+
<button type="button" className="btn btn--primary" disabled={busy} onClick={doExport}>
|
|
783
|
+
<StIcon name="download" size={14} />
|
|
784
|
+
{card.handoff
|
|
785
|
+
? 'Copy handoff command'
|
|
786
|
+
: busy
|
|
787
|
+
? 'Exporting…'
|
|
788
|
+
: `Export ${card.label}`}
|
|
789
|
+
</button>
|
|
790
|
+
</div>
|
|
791
|
+
</div>
|
|
792
|
+
</div>
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
|
|
301
796
|
// ───── Tree (CV-08 spec) ─────
|
|
302
797
|
// File rows use `.tp-row` with optional .dir / .sel / .star / .modified
|
|
303
798
|
// modifiers + a leading `.glyph` (▾ open dir, ▸ closed dir / selected file,
|
|
@@ -317,14 +812,14 @@ function DirRow({ name, depth, defaultOpen, children }) {
|
|
|
317
812
|
role="treeitem"
|
|
318
813
|
aria-expanded={open}
|
|
319
814
|
tabIndex={-1}
|
|
320
|
-
className="
|
|
815
|
+
className="st-row"
|
|
321
816
|
style={{ paddingLeft: TREE_INDENT_BASE + depth * TREE_INDENT_STEP + 'px' }}
|
|
322
817
|
onClick={() => setOpen((v) => !v)}
|
|
323
818
|
>
|
|
324
|
-
<span className="glyph"
|
|
325
|
-
{open ? '
|
|
819
|
+
<span className="st-row-glyph">
|
|
820
|
+
<StIcon name={open ? 'chevron-down' : 'chevron-right'} size={13} />
|
|
326
821
|
</span>
|
|
327
|
-
<span className="name">{name}</span>
|
|
822
|
+
<span className="st-row-name">{name}</span>
|
|
328
823
|
</button>
|
|
329
824
|
{open && children}
|
|
330
825
|
</Fragment>
|
|
@@ -340,30 +835,31 @@ function DsFolderRow({ name, dsName, depth, defaultOpen, active, onOpenSystem, c
|
|
|
340
835
|
return (
|
|
341
836
|
<Fragment>
|
|
342
837
|
<div
|
|
343
|
-
className={'
|
|
838
|
+
className={'st-row st-ds-folder' + (active ? ' is-sel' : '')}
|
|
344
839
|
style={{ paddingLeft: TREE_INDENT_BASE + depth * TREE_INDENT_STEP + 'px' }}
|
|
345
840
|
role="treeitem"
|
|
346
841
|
aria-expanded={open}
|
|
347
842
|
>
|
|
348
843
|
<button
|
|
349
844
|
type="button"
|
|
350
|
-
className="ds-
|
|
845
|
+
className="st-ds-chev"
|
|
351
846
|
onClick={() => setOpen((v) => !v)}
|
|
352
847
|
aria-label={open ? 'Collapse design system' : 'Expand design system'}
|
|
353
848
|
title={open ? 'Collapse' : 'Expand'}
|
|
354
849
|
>
|
|
355
|
-
<
|
|
356
|
-
{open ? '▾' : '▸'}
|
|
357
|
-
</span>
|
|
850
|
+
<StIcon name={open ? 'chevron-down' : 'chevron-right'} size={13} />
|
|
358
851
|
</button>
|
|
359
852
|
<button
|
|
360
853
|
type="button"
|
|
361
|
-
className="ds-
|
|
854
|
+
className="st-ds-open"
|
|
362
855
|
onClick={() => onOpenSystem(dsName)}
|
|
363
856
|
aria-label={`Open ${dsName} design system view`}
|
|
364
857
|
title="Open the design system view"
|
|
365
858
|
>
|
|
366
|
-
<span className="
|
|
859
|
+
<span className="st-row-glyph">
|
|
860
|
+
<StIcon name="folder" size={13} />
|
|
861
|
+
</span>
|
|
862
|
+
<span className="st-row-name">{name}</span>
|
|
367
863
|
</button>
|
|
368
864
|
</div>
|
|
369
865
|
{open && children}
|
|
@@ -371,7 +867,7 @@ function DsFolderRow({ name, dsName, depth, defaultOpen, active, onOpenSystem, c
|
|
|
371
867
|
);
|
|
372
868
|
}
|
|
373
869
|
|
|
374
|
-
function FileRow({ file, activePath, onOpen, openCount: oc, depth, kind, sidecar }) {
|
|
870
|
+
function FileRow({ file, activePath, onOpen, onDelete, openCount: oc, depth, kind, sidecar }) {
|
|
375
871
|
const isSel = file.path === activePath;
|
|
376
872
|
const isCanvas = CANVAS_EXT_RE.test(file.name);
|
|
377
873
|
// Non-canvas rows (PROJECT *.md, RUNTIME _active.json, ...) are display-only —
|
|
@@ -379,7 +875,10 @@ function FileRow({ file, activePath, onOpen, openCount: oc, depth, kind, sidecar
|
|
|
379
875
|
// hint via `aria-disabled`.
|
|
380
876
|
const inert = !isCanvas;
|
|
381
877
|
const label = isCanvas ? displayName(file.name) : file.name;
|
|
382
|
-
|
|
878
|
+
// Delete only real canvases in a deletable group (onDelete is undefined for the
|
|
879
|
+
// DS group + runtime files); the server enforces the rest.
|
|
880
|
+
const canDelete = isCanvas && typeof onDelete === 'function' && kind !== 'runtime';
|
|
881
|
+
const row = (
|
|
383
882
|
<button
|
|
384
883
|
type="button"
|
|
385
884
|
role="treeitem"
|
|
@@ -387,10 +886,7 @@ function FileRow({ file, activePath, onOpen, openCount: oc, depth, kind, sidecar
|
|
|
387
886
|
aria-disabled={inert ? 'true' : undefined}
|
|
388
887
|
tabIndex={isSel ? 0 : -1}
|
|
389
888
|
className={
|
|
390
|
-
'
|
|
391
|
-
(isSel ? ' sel' : '') +
|
|
392
|
-
(kind === 'runtime' ? ' muted' : '') +
|
|
393
|
-
(sidecar ? ' sidecar' : '')
|
|
889
|
+
'st-row' + (isSel ? ' is-sel' : '') + (kind === 'runtime' ? ' is-muted' : '')
|
|
394
890
|
}
|
|
395
891
|
style={{ paddingLeft: TREE_INDENT_BASE + depth * TREE_INDENT_STEP + 'px' }}
|
|
396
892
|
title={file.path + (oc ? ` — ${oc} open` : inert ? ' (file index only)' : '')}
|
|
@@ -398,13 +894,33 @@ function FileRow({ file, activePath, onOpen, openCount: oc, depth, kind, sidecar
|
|
|
398
894
|
if (!inert) onOpen(file.path);
|
|
399
895
|
}}
|
|
400
896
|
>
|
|
401
|
-
<span className="glyph"
|
|
402
|
-
|
|
897
|
+
<span className="st-row-glyph">
|
|
898
|
+
<StIcon name="file" size={13} />
|
|
403
899
|
</span>
|
|
404
|
-
<span className="name">{label}</span>
|
|
405
|
-
{oc > 0 && <span className="badge">{oc}</span>}
|
|
900
|
+
<span className="st-row-name">{label}</span>
|
|
901
|
+
{oc > 0 && <span className="st-row-badge">{oc}</span>}
|
|
406
902
|
</button>
|
|
407
903
|
);
|
|
904
|
+
if (!canDelete) return row;
|
|
905
|
+
// A sibling delete button (can't nest a button in the row button). The wrapper
|
|
906
|
+
// is presentational so the treeitem stays the tree's child for a11y.
|
|
907
|
+
return (
|
|
908
|
+
<div className="st-row-wrap" role="none">
|
|
909
|
+
{row}
|
|
910
|
+
<button
|
|
911
|
+
type="button"
|
|
912
|
+
className="st-row-del"
|
|
913
|
+
title={`Delete ${label}`}
|
|
914
|
+
aria-label={`Delete canvas ${label}`}
|
|
915
|
+
onClick={(e) => {
|
|
916
|
+
e.stopPropagation();
|
|
917
|
+
onDelete(file.path, label);
|
|
918
|
+
}}
|
|
919
|
+
>
|
|
920
|
+
<Icon d="M3 6h18 M8 6V4h8v2 M6 6l1 14h10l1-14 M10 11v6 M14 11v6" size={12} />
|
|
921
|
+
</button>
|
|
922
|
+
</div>
|
|
923
|
+
);
|
|
408
924
|
}
|
|
409
925
|
|
|
410
926
|
function CanvasRow({
|
|
@@ -414,6 +930,7 @@ function CanvasRow({
|
|
|
414
930
|
kind,
|
|
415
931
|
activePath,
|
|
416
932
|
onOpen,
|
|
933
|
+
onDelete,
|
|
417
934
|
openCount: oc,
|
|
418
935
|
showHidden,
|
|
419
936
|
forceOpen,
|
|
@@ -432,6 +949,7 @@ function CanvasRow({
|
|
|
432
949
|
file={primary}
|
|
433
950
|
activePath={activePath}
|
|
434
951
|
onOpen={onOpen}
|
|
952
|
+
onDelete={onDelete}
|
|
435
953
|
openCount={oc}
|
|
436
954
|
depth={depth}
|
|
437
955
|
kind={kind}
|
|
@@ -446,12 +964,12 @@ function CanvasRow({
|
|
|
446
964
|
aria-selected={isSel}
|
|
447
965
|
aria-expanded={open}
|
|
448
966
|
tabIndex={isSel ? 0 : -1}
|
|
449
|
-
className={'
|
|
967
|
+
className={'st-row st-canvas-row' + (isSel ? ' is-sel' : '')}
|
|
450
968
|
style={{ paddingLeft: TREE_INDENT_BASE + depth * TREE_INDENT_STEP + 'px' }}
|
|
451
969
|
title={primary.path}
|
|
452
970
|
onClick={(e) => {
|
|
453
971
|
// Click the chevron region → toggle disclosure. Click anywhere else → open canvas.
|
|
454
|
-
if (e.target.closest('.canvas-chev')) {
|
|
972
|
+
if (e.target.closest('.st-canvas-chev')) {
|
|
455
973
|
setOpenState((v) => !v);
|
|
456
974
|
return;
|
|
457
975
|
}
|
|
@@ -459,17 +977,16 @@ function CanvasRow({
|
|
|
459
977
|
}}
|
|
460
978
|
>
|
|
461
979
|
<span
|
|
462
|
-
className="glyph canvas-chev"
|
|
463
|
-
aria-hidden="true"
|
|
980
|
+
className="st-row-glyph st-canvas-chev"
|
|
464
981
|
onClick={(e) => {
|
|
465
982
|
e.stopPropagation();
|
|
466
983
|
setOpenState((v) => !v);
|
|
467
984
|
}}
|
|
468
985
|
>
|
|
469
|
-
{open ? '
|
|
986
|
+
<StIcon name={open ? 'chevron-down' : 'chevron-right'} size={13} />
|
|
470
987
|
</span>
|
|
471
|
-
<span className="name">{displayName(primary.name)}</span>
|
|
472
|
-
{oc > 0 && <span className="badge">{oc}</span>}
|
|
988
|
+
<span className="st-row-name">{displayName(primary.name)}</span>
|
|
989
|
+
{oc > 0 && <span className="st-row-badge">{oc}</span>}
|
|
473
990
|
</button>
|
|
474
991
|
{open &&
|
|
475
992
|
sidecars.map((sc) => (
|
|
@@ -500,6 +1017,7 @@ function Tree({
|
|
|
500
1017
|
dsFolders,
|
|
501
1018
|
activeDsName,
|
|
502
1019
|
onOpenSystem,
|
|
1020
|
+
onDelete,
|
|
503
1021
|
}) {
|
|
504
1022
|
const dirs = Object.keys(node)
|
|
505
1023
|
.filter((k) => k !== '_files')
|
|
@@ -535,6 +1053,7 @@ function Tree({
|
|
|
535
1053
|
sidecars={entry.sidecars}
|
|
536
1054
|
activePath={activePath}
|
|
537
1055
|
onOpen={onOpen}
|
|
1056
|
+
onDelete={onDelete}
|
|
538
1057
|
openCount={openCount(commentsByFile[entry.primary.path])}
|
|
539
1058
|
depth={depth}
|
|
540
1059
|
kind={kind}
|
|
@@ -555,6 +1074,7 @@ function Tree({
|
|
|
555
1074
|
kind={kind}
|
|
556
1075
|
/>
|
|
557
1076
|
))}
|
|
1077
|
+
{/* orphans are sidecars/loose files — no canvas to delete, so no onDelete */}
|
|
558
1078
|
{dirs.map((d) => {
|
|
559
1079
|
const dsMatch = dsFolderByName?.get(d);
|
|
560
1080
|
const childTree = (
|
|
@@ -569,6 +1089,7 @@ function Tree({
|
|
|
569
1089
|
search={search}
|
|
570
1090
|
activeDsName={activeDsName}
|
|
571
1091
|
onOpenSystem={onOpenSystem}
|
|
1092
|
+
onDelete={onDelete}
|
|
572
1093
|
/>
|
|
573
1094
|
);
|
|
574
1095
|
if (dsMatch && onOpenSystem) {
|
|
@@ -630,12 +1151,39 @@ function Sidebar({
|
|
|
630
1151
|
showHidden,
|
|
631
1152
|
sectionsExpanded,
|
|
632
1153
|
onToggleSection,
|
|
1154
|
+
onNewBoard,
|
|
1155
|
+
onDeleteBoard,
|
|
1156
|
+
collapsed,
|
|
1157
|
+
onCollapse,
|
|
633
1158
|
}) {
|
|
634
1159
|
const filteredGroups = useMemo(() => {
|
|
635
1160
|
if (!search) return groups;
|
|
636
1161
|
return groups.map((g) => ({ ...g, tree: filterTree(g.tree, search), filtered: !!search }));
|
|
637
1162
|
}, [groups, search]);
|
|
638
1163
|
|
|
1164
|
+
// Phase 22 — inline "new brief board" composer in the tree header. Click +,
|
|
1165
|
+
// type a name, Enter to create (Esc cancels). The board opens active so it's
|
|
1166
|
+
// ready to annotate; generation (ingest) still goes through /design:new.
|
|
1167
|
+
const [creating, setCreating] = useState(false);
|
|
1168
|
+
const [newName, setNewName] = useState('');
|
|
1169
|
+
const [newErr, setNewErr] = useState('');
|
|
1170
|
+
const [newBusy, setNewBusy] = useState(false);
|
|
1171
|
+
|
|
1172
|
+
const submitNewBoard = useCallback(async () => {
|
|
1173
|
+
const name = newName.trim();
|
|
1174
|
+
if (!name || newBusy) return;
|
|
1175
|
+
setNewBusy(true);
|
|
1176
|
+
setNewErr('');
|
|
1177
|
+
const res = await onNewBoard(name);
|
|
1178
|
+
setNewBusy(false);
|
|
1179
|
+
if (res?.ok) {
|
|
1180
|
+
setCreating(false);
|
|
1181
|
+
setNewName('');
|
|
1182
|
+
} else {
|
|
1183
|
+
setNewErr(res?.error || 'could not create board');
|
|
1184
|
+
}
|
|
1185
|
+
}, [newName, newBusy, onNewBoard]);
|
|
1186
|
+
|
|
639
1187
|
// Mock uses `42 / 42` — total openable canvases, not every listed file.
|
|
640
1188
|
// We count canvas files (TSX Phase 3.6+ default, HTML legacy) so the counter
|
|
641
1189
|
// matches "canvases you can mount".
|
|
@@ -652,41 +1200,109 @@ function Sidebar({
|
|
|
652
1200
|
}, [filteredGroups]);
|
|
653
1201
|
|
|
654
1202
|
return (
|
|
655
|
-
<nav className=
|
|
656
|
-
<div className="
|
|
657
|
-
<span>
|
|
658
|
-
<
|
|
659
|
-
<
|
|
660
|
-
|
|
661
|
-
|
|
1203
|
+
<nav className={'st-sidebar' + (collapsed ? ' is-collapsed' : '')} aria-label="Files">
|
|
1204
|
+
<div className="st-sb-hd">
|
|
1205
|
+
<span className="st-sb-title">Files</span>
|
|
1206
|
+
<div className="st-sb-hd-actions">
|
|
1207
|
+
<button
|
|
1208
|
+
type="button"
|
|
1209
|
+
className="st-iconbtn"
|
|
1210
|
+
title="New blank brief board"
|
|
1211
|
+
aria-label="New blank brief board"
|
|
1212
|
+
aria-expanded={creating}
|
|
1213
|
+
onClick={() => {
|
|
1214
|
+
setNewErr('');
|
|
1215
|
+
setCreating((v) => !v);
|
|
1216
|
+
}}
|
|
1217
|
+
>
|
|
1218
|
+
<StIcon name="plus" size={15} />
|
|
1219
|
+
</button>
|
|
1220
|
+
<span className="st-live" title={wsConnected ? 'live · file index synced' : 'reconnecting…'}>
|
|
1221
|
+
<span className={'st-live-dot' + (wsConnected ? ' is-connected' : '')} aria-hidden="true" />
|
|
1222
|
+
{htmlShown} / {htmlCount}
|
|
1223
|
+
</span>
|
|
1224
|
+
{onCollapse && (
|
|
1225
|
+
<button
|
|
1226
|
+
type="button"
|
|
1227
|
+
className="st-iconbtn"
|
|
1228
|
+
aria-label="Collapse sidebar"
|
|
1229
|
+
title="Collapse sidebar (T)"
|
|
1230
|
+
onClick={onCollapse}
|
|
1231
|
+
>
|
|
1232
|
+
<StIcon name="panel-left" size={15} />
|
|
1233
|
+
</button>
|
|
1234
|
+
)}
|
|
1235
|
+
</div>
|
|
662
1236
|
</div>
|
|
663
1237
|
|
|
664
|
-
|
|
665
|
-
<
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
1238
|
+
{creating ? (
|
|
1239
|
+
<div className="st-newboard">
|
|
1240
|
+
<input
|
|
1241
|
+
type="text"
|
|
1242
|
+
// biome-ignore lint/a11y/noAutofocus: deliberate — the composer opens on an explicit click.
|
|
1243
|
+
autoFocus
|
|
1244
|
+
placeholder="brief board name…"
|
|
1245
|
+
value={newName}
|
|
1246
|
+
maxLength={60}
|
|
1247
|
+
disabled={newBusy}
|
|
1248
|
+
aria-label="New brief board name"
|
|
1249
|
+
onChange={(e) => setNewName(e.target.value)}
|
|
1250
|
+
onKeyDown={(e) => {
|
|
1251
|
+
if (e.key === 'Enter') {
|
|
1252
|
+
e.preventDefault();
|
|
1253
|
+
submitNewBoard();
|
|
1254
|
+
} else if (e.key === 'Escape') {
|
|
1255
|
+
e.preventDefault();
|
|
1256
|
+
setCreating(false);
|
|
1257
|
+
setNewName('');
|
|
1258
|
+
setNewErr('');
|
|
1259
|
+
}
|
|
1260
|
+
}}
|
|
1261
|
+
/>
|
|
674
1262
|
<button
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
1263
|
+
type="button"
|
|
1264
|
+
className="st-newboard-go"
|
|
1265
|
+
disabled={newBusy || !newName.trim()}
|
|
1266
|
+
title="Create (Enter)"
|
|
1267
|
+
aria-label="Create brief board"
|
|
1268
|
+
onClick={submitNewBoard}
|
|
679
1269
|
>
|
|
680
|
-
|
|
1270
|
+
{newBusy ? '…' : '↵'}
|
|
681
1271
|
</button>
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
1272
|
+
</div>
|
|
1273
|
+
) : null}
|
|
1274
|
+
{newErr ? (
|
|
1275
|
+
<div className="st-newboard-err" role="alert">
|
|
1276
|
+
{newErr}
|
|
1277
|
+
</div>
|
|
1278
|
+
) : null}
|
|
1279
|
+
|
|
1280
|
+
<div className="st-search">
|
|
1281
|
+
<div className="st-search-box">
|
|
1282
|
+
<StIcon name="search" size={13} />
|
|
1283
|
+
<input
|
|
1284
|
+
type="search"
|
|
1285
|
+
placeholder="Search canvases…"
|
|
1286
|
+
value={search}
|
|
1287
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
1288
|
+
aria-label="Filter files"
|
|
1289
|
+
/>
|
|
1290
|
+
{search ? (
|
|
1291
|
+
<button
|
|
1292
|
+
className="st-search-clear"
|
|
1293
|
+
onClick={() => setSearch('')}
|
|
1294
|
+
title="Clear (Esc)"
|
|
1295
|
+
aria-label="Clear search"
|
|
1296
|
+
>
|
|
1297
|
+
×
|
|
1298
|
+
</button>
|
|
1299
|
+
) : (
|
|
1300
|
+
<Kbd>/</Kbd>
|
|
1301
|
+
)}
|
|
1302
|
+
</div>
|
|
687
1303
|
</div>
|
|
688
1304
|
|
|
689
|
-
<div className="tree
|
|
1305
|
+
<div className="st-tree" role="tree" aria-label="Project file tree">
|
|
690
1306
|
{filteredGroups.map((g) => {
|
|
691
1307
|
// Hide gitignored runtime / orphan-only project sections by default.
|
|
692
1308
|
// Active search overrides — if the user typed a query, they want hits
|
|
@@ -711,21 +1327,18 @@ function Sidebar({
|
|
|
711
1327
|
const explicit = sectionsExpanded[g.label];
|
|
712
1328
|
// Active search forces every section open so hits aren't hidden.
|
|
713
1329
|
const sectionOpen = !!search || (explicit === undefined ? defaultOpen : explicit);
|
|
714
|
-
const chev = sectionOpen ? '▾' : '▸';
|
|
715
1330
|
return (
|
|
716
|
-
<
|
|
1331
|
+
<div className="st-tree-section" key={g.label}>
|
|
717
1332
|
<button
|
|
718
1333
|
type="button"
|
|
719
|
-
className="
|
|
1334
|
+
className="st-tree-sec-hd"
|
|
720
1335
|
onClick={() => onToggleSection(g.label, defaultOpen)}
|
|
721
1336
|
aria-expanded={sectionOpen}
|
|
722
1337
|
title={sectionOpen ? 'Collapse section' : 'Expand section'}
|
|
723
1338
|
>
|
|
724
|
-
<
|
|
725
|
-
|
|
726
|
-
</span>
|
|
727
|
-
<span className="section-label">{meta.title}</span>
|
|
728
|
-
{pill && <span className="pill">{pill}</span>}
|
|
1339
|
+
<StIcon name={sectionOpen ? 'chevron-down' : 'chevron-right'} size={13} />
|
|
1340
|
+
<span className="st-sec-name">{meta.title}</span>
|
|
1341
|
+
{pill && <span className="st-pill">{pill}</span>}
|
|
729
1342
|
</button>
|
|
730
1343
|
{sectionOpen &&
|
|
731
1344
|
(hasItems ? (
|
|
@@ -741,11 +1354,12 @@ function Sidebar({
|
|
|
741
1354
|
dsFolders={g.dsFolders}
|
|
742
1355
|
activeDsName={activeDsName}
|
|
743
1356
|
onOpenSystem={isDs ? onOpenSystem : undefined}
|
|
1357
|
+
onDelete={isDs ? undefined : onDeleteBoard}
|
|
744
1358
|
/>
|
|
745
1359
|
) : (
|
|
746
|
-
<div className="
|
|
1360
|
+
<div className="st-tree-empty">{search ? 'No matches.' : 'Empty.'}</div>
|
|
747
1361
|
))}
|
|
748
|
-
</
|
|
1362
|
+
</div>
|
|
749
1363
|
);
|
|
750
1364
|
})}
|
|
751
1365
|
</div>
|
|
@@ -753,9 +1367,29 @@ function Sidebar({
|
|
|
753
1367
|
);
|
|
754
1368
|
}
|
|
755
1369
|
|
|
1370
|
+
// Collapsed rail — a thin strip shown when the sidebar is collapsed, with a
|
|
1371
|
+
// re-open affordance + quick search/files shortcuts (mockup CollapsedRail).
|
|
1372
|
+
function CollapsedRail({ shown, onExpand, onSearch }) {
|
|
1373
|
+
return (
|
|
1374
|
+
<div className={'st-rail' + (shown ? ' is-shown' : '')}>
|
|
1375
|
+
<div className="st-rail-inner">
|
|
1376
|
+
<button type="button" className="st-iconbtn" aria-label="Expand sidebar" title="Expand sidebar (T)" onClick={onExpand}>
|
|
1377
|
+
<StIcon name="panel-left" size={15} />
|
|
1378
|
+
</button>
|
|
1379
|
+
<button type="button" className="st-iconbtn" aria-label="Search" title="Search (/)" onClick={onSearch}>
|
|
1380
|
+
<StIcon name="search" size={15} />
|
|
1381
|
+
</button>
|
|
1382
|
+
<button type="button" className="st-iconbtn" aria-label="Files" title="Files" onClick={onExpand}>
|
|
1383
|
+
<StIcon name="folder" size={15} />
|
|
1384
|
+
</button>
|
|
1385
|
+
</div>
|
|
1386
|
+
</div>
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
756
1390
|
// Help modal — hosts the cheatsheet that used to live in the left sidebar.
|
|
757
1391
|
// Triggered from the menubar's Help item. Esc + backdrop click close it.
|
|
758
|
-
function HelpModal({ open, onClose }) {
|
|
1392
|
+
function HelpModal({ open, onClose, onStartTour }) {
|
|
759
1393
|
useEffect(() => {
|
|
760
1394
|
if (!open) return;
|
|
761
1395
|
function onKey(e) {
|
|
@@ -784,6 +1418,16 @@ function HelpModal({ open, onClose }) {
|
|
|
784
1418
|
Help · shortcuts & commands
|
|
785
1419
|
</span>
|
|
786
1420
|
<span className="sku">MDCC-DEV-SRV / v{MDCC_VERSION}</span>
|
|
1421
|
+
{onStartTour && (
|
|
1422
|
+
<button
|
|
1423
|
+
type="button"
|
|
1424
|
+
className="mdcc-tour__back"
|
|
1425
|
+
style={{ marginLeft: 'auto' }}
|
|
1426
|
+
onClick={onStartTour}
|
|
1427
|
+
>
|
|
1428
|
+
▶ Take the tour
|
|
1429
|
+
</button>
|
|
1430
|
+
)}
|
|
787
1431
|
<button
|
|
788
1432
|
type="button"
|
|
789
1433
|
className="help-modal-close"
|
|
@@ -1067,13 +1711,14 @@ function HelpModal({ open, onClose }) {
|
|
|
1067
1711
|
|
|
1068
1712
|
const MENU_NAMES = ['File', 'Edit', 'View', 'Selection', 'Tools', 'Help'];
|
|
1069
1713
|
|
|
1070
|
-
|
|
1714
|
+
// Shared close-on-Esc / outside-click effect for the menubar dropdowns.
|
|
1715
|
+
function useDropdownClose(onClose) {
|
|
1071
1716
|
useEffect(() => {
|
|
1072
1717
|
function onKey(e) {
|
|
1073
1718
|
if (e.key === 'Escape') onClose();
|
|
1074
1719
|
}
|
|
1075
1720
|
function onDocClick(e) {
|
|
1076
|
-
if (!e.target.closest('.
|
|
1721
|
+
if (!e.target.closest('.st-dropdown, .st-menu')) onClose();
|
|
1077
1722
|
}
|
|
1078
1723
|
window.addEventListener('keydown', onKey);
|
|
1079
1724
|
window.addEventListener('mousedown', onDocClick);
|
|
@@ -1082,16 +1727,19 @@ function ViewDropdown({ panels, onToggle, onClose }) {
|
|
|
1082
1727
|
window.removeEventListener('mousedown', onDocClick);
|
|
1083
1728
|
};
|
|
1084
1729
|
}, [onClose]);
|
|
1730
|
+
}
|
|
1085
1731
|
|
|
1732
|
+
function ViewDropdown({ panels, onToggle, onClose }) {
|
|
1733
|
+
useDropdownClose(onClose);
|
|
1086
1734
|
return (
|
|
1087
|
-
<div className="
|
|
1088
|
-
<div className="
|
|
1735
|
+
<div className="st-dropdown" role="menu" aria-label="View" style={{ left: 152 }}>
|
|
1736
|
+
<div className="st-dd-hd">Panels</div>
|
|
1089
1737
|
{panels.map((p) => (
|
|
1090
1738
|
<button
|
|
1091
1739
|
key={p.id}
|
|
1092
1740
|
type="button"
|
|
1093
1741
|
role="menuitem"
|
|
1094
|
-
className={'
|
|
1742
|
+
className={'st-dd-item' + (p.checked ? ' is-on' : '')}
|
|
1095
1743
|
aria-disabled={p.disabled ? 'true' : undefined}
|
|
1096
1744
|
onClick={() => {
|
|
1097
1745
|
if (!p.disabled) {
|
|
@@ -1100,37 +1748,27 @@ function ViewDropdown({ panels, onToggle, onClose }) {
|
|
|
1100
1748
|
}
|
|
1101
1749
|
}}
|
|
1102
1750
|
>
|
|
1103
|
-
<span className="
|
|
1104
|
-
<span className="check">{p.checked ?
|
|
1751
|
+
<span className="st-dd-lead">
|
|
1752
|
+
<span className="st-dd-check">{p.checked ? <StIcon name="check" size={13} /> : null}</span>
|
|
1105
1753
|
<span>{p.label}</span>
|
|
1106
1754
|
</span>
|
|
1107
|
-
{p.phase ?
|
|
1108
|
-
<span className="phase-tag">{p.phase}</span>
|
|
1109
|
-
) : (
|
|
1110
|
-
<span className="shortcut">{p.shortcut || ''}</span>
|
|
1111
|
-
)}
|
|
1755
|
+
{p.phase ? <span className="st-dd-phase">{p.phase}</span> : <Kbd>{p.shortcut || ''}</Kbd>}
|
|
1112
1756
|
</button>
|
|
1113
1757
|
))}
|
|
1114
|
-
<div className="
|
|
1115
|
-
<div className="
|
|
1758
|
+
<div className="st-dd-sep" />
|
|
1759
|
+
<div className="st-dd-hd">Zoom</div>
|
|
1116
1760
|
{[
|
|
1117
1761
|
{ label: 'Zoom In', shortcut: '⌘ +' },
|
|
1118
1762
|
{ label: 'Zoom Out', shortcut: '⌘ −' },
|
|
1119
1763
|
{ label: 'Fit to Screen', shortcut: '⌘ 0' },
|
|
1120
1764
|
{ label: 'Actual Size · 100 %', shortcut: '⌥ ⌘ 0' },
|
|
1121
1765
|
].map((z) => (
|
|
1122
|
-
<button
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
role="menuitem"
|
|
1126
|
-
className="mb-dd-item"
|
|
1127
|
-
aria-disabled="true"
|
|
1128
|
-
>
|
|
1129
|
-
<span className="lbl">
|
|
1130
|
-
<span className="check" />
|
|
1766
|
+
<button key={z.label} type="button" role="menuitem" className="st-dd-item" aria-disabled="true">
|
|
1767
|
+
<span className="st-dd-lead">
|
|
1768
|
+
<span className="st-dd-check" />
|
|
1131
1769
|
<span>{z.label}</span>
|
|
1132
1770
|
</span>
|
|
1133
|
-
<span className="phase
|
|
1771
|
+
<span className="st-dd-phase">Phase 4</span>
|
|
1134
1772
|
</button>
|
|
1135
1773
|
))}
|
|
1136
1774
|
</div>
|
|
@@ -1141,42 +1779,29 @@ function ViewDropdown({ panels, onToggle, onClose }) {
|
|
|
1141
1779
|
// Phase 5.1 — Selection + Tools dropdowns (mirror ViewDropdown shape).
|
|
1142
1780
|
|
|
1143
1781
|
function SelectionDropdown({ onAction, onClose }) {
|
|
1144
|
-
|
|
1145
|
-
function onKey(e) {
|
|
1146
|
-
if (e.key === 'Escape') onClose();
|
|
1147
|
-
}
|
|
1148
|
-
function onDocClick(e) {
|
|
1149
|
-
if (!e.target.closest('.mb-dropdown, .mb-menu')) onClose();
|
|
1150
|
-
}
|
|
1151
|
-
window.addEventListener('keydown', onKey);
|
|
1152
|
-
window.addEventListener('mousedown', onDocClick);
|
|
1153
|
-
return () => {
|
|
1154
|
-
window.removeEventListener('keydown', onKey);
|
|
1155
|
-
window.removeEventListener('mousedown', onDocClick);
|
|
1156
|
-
};
|
|
1157
|
-
}, [onClose]);
|
|
1782
|
+
useDropdownClose(onClose);
|
|
1158
1783
|
const items = [
|
|
1159
1784
|
{ id: 'deselect-all', label: 'Deselect all', shortcut: 'Esc' },
|
|
1160
1785
|
{ id: 'select-all-annotations', label: 'Select all annotations', shortcut: '⌘ ⇧ A' },
|
|
1161
1786
|
];
|
|
1162
1787
|
return (
|
|
1163
|
-
<div className="
|
|
1788
|
+
<div className="st-dropdown" role="menu" aria-label="Selection" style={{ left: 214 }}>
|
|
1164
1789
|
{items.map((it) => (
|
|
1165
1790
|
<button
|
|
1166
1791
|
key={it.id}
|
|
1167
1792
|
type="button"
|
|
1168
1793
|
role="menuitem"
|
|
1169
|
-
className="
|
|
1794
|
+
className="st-dd-item"
|
|
1170
1795
|
onClick={() => {
|
|
1171
1796
|
onAction(it.id);
|
|
1172
1797
|
onClose();
|
|
1173
1798
|
}}
|
|
1174
1799
|
>
|
|
1175
|
-
<span className="
|
|
1176
|
-
<span className="check" />
|
|
1800
|
+
<span className="st-dd-lead">
|
|
1801
|
+
<span className="st-dd-check" />
|
|
1177
1802
|
<span>{it.label}</span>
|
|
1178
1803
|
</span>
|
|
1179
|
-
<
|
|
1804
|
+
<Kbd>{it.shortcut}</Kbd>
|
|
1180
1805
|
</button>
|
|
1181
1806
|
))}
|
|
1182
1807
|
</div>
|
|
@@ -1184,21 +1809,8 @@ function SelectionDropdown({ onAction, onClose }) {
|
|
|
1184
1809
|
}
|
|
1185
1810
|
|
|
1186
1811
|
function ToolsDropdown({ onAction, onClose }) {
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
if (e.key === 'Escape') onClose();
|
|
1190
|
-
}
|
|
1191
|
-
function onDocClick(e) {
|
|
1192
|
-
if (!e.target.closest('.mb-dropdown, .mb-menu')) onClose();
|
|
1193
|
-
}
|
|
1194
|
-
window.addEventListener('keydown', onKey);
|
|
1195
|
-
window.addEventListener('mousedown', onDocClick);
|
|
1196
|
-
return () => {
|
|
1197
|
-
window.removeEventListener('keydown', onKey);
|
|
1198
|
-
window.removeEventListener('mousedown', onDocClick);
|
|
1199
|
-
};
|
|
1200
|
-
}, [onClose]);
|
|
1201
|
-
// Mirrors DEFAULT_TOOLS in plugins/design/dev-server/use-tool-mode.tsx —
|
|
1812
|
+
useDropdownClose(onClose);
|
|
1813
|
+
// Mirrors DEFAULT_TOOLS in apps/studio/use-tool-mode.tsx —
|
|
1202
1814
|
// kept in sync by hand because the menubar lives in the dev-server shell
|
|
1203
1815
|
// (no shared bundle with the canvas iframes).
|
|
1204
1816
|
const tools = [
|
|
@@ -1214,25 +1826,102 @@ function ToolsDropdown({ onAction, onClose }) {
|
|
|
1214
1826
|
{ id: 'eraser', label: 'Eraser', shortcut: 'E' },
|
|
1215
1827
|
];
|
|
1216
1828
|
return (
|
|
1217
|
-
<div className="
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
<span
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1829
|
+
<div className="st-dropdown" role="menu" aria-label="Tools" style={{ left: 290 }}>
|
|
1830
|
+
<div className="st-dd-hd">Tool palette</div>
|
|
1831
|
+
{tools.map((t) => (
|
|
1832
|
+
<button
|
|
1833
|
+
key={t.id}
|
|
1834
|
+
type="button"
|
|
1835
|
+
role="menuitem"
|
|
1836
|
+
className="st-dd-item"
|
|
1837
|
+
onClick={() => {
|
|
1838
|
+
onAction(t.id);
|
|
1839
|
+
onClose();
|
|
1840
|
+
}}
|
|
1841
|
+
>
|
|
1842
|
+
<span className="st-dd-lead">
|
|
1843
|
+
<span className="st-dd-check" />
|
|
1844
|
+
<span>{t.label}</span>
|
|
1845
|
+
</span>
|
|
1846
|
+
<Kbd>{t.shortcut}</Kbd>
|
|
1847
|
+
</button>
|
|
1848
|
+
))}
|
|
1849
|
+
</div>
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// Plan C follow-up — File + Edit menus, previously inert. Both dispatch to real
|
|
1854
|
+
// shell flows (File) or the in-canvas undo stack / selection bridges (Edit).
|
|
1855
|
+
function FileDropdown({ onAction, onClose }) {
|
|
1856
|
+
useDropdownClose(onClose);
|
|
1857
|
+
const items = [
|
|
1858
|
+
{ id: 'new', label: 'New canvas…', shortcut: '⌘N' },
|
|
1859
|
+
{ id: 'export', label: 'Export…', shortcut: '⇧⌘E' },
|
|
1860
|
+
{ id: 'handoff', label: 'Handoff to production', shortcut: '⇧⌘H' },
|
|
1861
|
+
{ sep: true },
|
|
1862
|
+
{ id: 'reload', label: 'Reload canvas', shortcut: '⌘R' },
|
|
1863
|
+
];
|
|
1864
|
+
return (
|
|
1865
|
+
<div className="st-dropdown" role="menu" aria-label="File" style={{ left: 40 }}>
|
|
1866
|
+
{items.map((it, i) =>
|
|
1867
|
+
it.sep ? (
|
|
1868
|
+
<div key={'s' + i} className="st-dd-sep" />
|
|
1869
|
+
) : (
|
|
1870
|
+
<button
|
|
1871
|
+
key={it.id}
|
|
1872
|
+
type="button"
|
|
1873
|
+
role="menuitem"
|
|
1874
|
+
className="st-dd-item"
|
|
1875
|
+
onClick={() => {
|
|
1876
|
+
onAction(it.id);
|
|
1877
|
+
onClose();
|
|
1878
|
+
}}
|
|
1879
|
+
>
|
|
1880
|
+
<span className="st-dd-lead">
|
|
1881
|
+
<span className="st-dd-check" />
|
|
1882
|
+
<span>{it.label}</span>
|
|
1883
|
+
</span>
|
|
1884
|
+
<Kbd>{it.shortcut}</Kbd>
|
|
1885
|
+
</button>
|
|
1886
|
+
)
|
|
1887
|
+
)}
|
|
1888
|
+
</div>
|
|
1889
|
+
);
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
function EditDropdown({ onAction, onClose }) {
|
|
1893
|
+
useDropdownClose(onClose);
|
|
1894
|
+
const items = [
|
|
1895
|
+
{ id: 'undo', label: 'Undo', shortcut: '⌘Z' },
|
|
1896
|
+
{ id: 'redo', label: 'Redo', shortcut: '⇧⌘Z' },
|
|
1897
|
+
{ sep: true },
|
|
1898
|
+
{ id: 'deselect-all', label: 'Deselect all', shortcut: 'Esc' },
|
|
1899
|
+
{ id: 'select-all-annotations', label: 'Select all annotations', shortcut: '⇧⌘A' },
|
|
1900
|
+
];
|
|
1901
|
+
return (
|
|
1902
|
+
<div className="st-dropdown" role="menu" aria-label="Edit" style={{ left: 90 }}>
|
|
1903
|
+
{items.map((it, i) =>
|
|
1904
|
+
it.sep ? (
|
|
1905
|
+
<div key={'s' + i} className="st-dd-sep" />
|
|
1906
|
+
) : (
|
|
1907
|
+
<button
|
|
1908
|
+
key={it.id}
|
|
1909
|
+
type="button"
|
|
1910
|
+
role="menuitem"
|
|
1911
|
+
className="st-dd-item"
|
|
1912
|
+
onClick={() => {
|
|
1913
|
+
onAction(it.id);
|
|
1914
|
+
onClose();
|
|
1915
|
+
}}
|
|
1916
|
+
>
|
|
1917
|
+
<span className="st-dd-lead">
|
|
1918
|
+
<span className="st-dd-check" />
|
|
1919
|
+
<span>{it.label}</span>
|
|
1920
|
+
</span>
|
|
1921
|
+
<Kbd>{it.shortcut}</Kbd>
|
|
1922
|
+
</button>
|
|
1923
|
+
)
|
|
1924
|
+
)}
|
|
1236
1925
|
</div>
|
|
1237
1926
|
);
|
|
1238
1927
|
}
|
|
@@ -1254,6 +1943,15 @@ function Menubar({
|
|
|
1254
1943
|
annotationsVisible,
|
|
1255
1944
|
onToggleAnnotations,
|
|
1256
1945
|
postToActiveCanvas,
|
|
1946
|
+
onOpenWhatsNew,
|
|
1947
|
+
whatsNewCount,
|
|
1948
|
+
artboardCount = 0,
|
|
1949
|
+
presence = null,
|
|
1950
|
+
inspectorOpen,
|
|
1951
|
+
onToggleInspector,
|
|
1952
|
+
onNewCanvas,
|
|
1953
|
+
onOpenExport,
|
|
1954
|
+
onReload,
|
|
1257
1955
|
}) {
|
|
1258
1956
|
const isSystem = activePath === SYSTEM_TAB;
|
|
1259
1957
|
const stamp = isSystem ? 'SYSTEM' : activePath ? 'CANVAS' : 'IDLE';
|
|
@@ -1284,7 +1982,7 @@ function Menubar({
|
|
|
1284
1982
|
disabled: false,
|
|
1285
1983
|
},
|
|
1286
1984
|
{ id: 'layers', label: 'Layers Panel', phase: 'Phase 12', disabled: true },
|
|
1287
|
-
{ id: 'inspector', label: 'Inspector',
|
|
1985
|
+
{ id: 'inspector', label: 'Inspector', shortcut: 'I', checked: inspectorOpen, disabled: false },
|
|
1288
1986
|
{
|
|
1289
1987
|
id: 'annotate',
|
|
1290
1988
|
label: 'Annotations',
|
|
@@ -1295,8 +1993,9 @@ function Menubar({
|
|
|
1295
1993
|
{ id: 'present', label: 'Presentation Mode', phase: 'Phase 6', disabled: true },
|
|
1296
1994
|
];
|
|
1297
1995
|
|
|
1996
|
+
const DROPDOWN_MENUS = ['file', 'edit', 'view', 'selection', 'tools'];
|
|
1298
1997
|
function onMenuClick(key) {
|
|
1299
|
-
if (key
|
|
1998
|
+
if (DROPDOWN_MENUS.includes(key)) {
|
|
1300
1999
|
setOpenMenu(openMenu === key ? null : key);
|
|
1301
2000
|
} else if (key === 'help') {
|
|
1302
2001
|
setOpenMenu(null);
|
|
@@ -1305,34 +2004,63 @@ function Menubar({
|
|
|
1305
2004
|
}
|
|
1306
2005
|
|
|
1307
2006
|
return (
|
|
1308
|
-
<header className="
|
|
1309
|
-
<span className="
|
|
1310
|
-
<span className="
|
|
1311
|
-
|
|
2007
|
+
<header className="st-menubar" role="menubar" aria-label="Application menubar">
|
|
2008
|
+
<span className="st-brand">
|
|
2009
|
+
<span className="st-brand-mark">
|
|
2010
|
+
<svg viewBox="0 0 32 32" width="100%" height="100%" fill="none" aria-hidden="true"><path d="M16 5l2.8 8.2L27 16l-8.2 2.8L16 27l-2.8-8.2L5 16l8.2-2.8z" fill="currentColor" /></svg>
|
|
2011
|
+
</span>
|
|
2012
|
+
<span className="st-brand-name">maude</span>
|
|
1312
2013
|
</span>
|
|
1313
|
-
<nav className="
|
|
2014
|
+
<nav className="st-menus" aria-label="Application menus">
|
|
1314
2015
|
{MENU_NAMES.map((name) => {
|
|
1315
2016
|
const key = name.toLowerCase();
|
|
1316
|
-
const hasDropdown = key
|
|
2017
|
+
const hasDropdown = DROPDOWN_MENUS.includes(key);
|
|
1317
2018
|
const interactive = hasDropdown || key === 'help';
|
|
1318
2019
|
const open = openMenu === key;
|
|
1319
2020
|
return (
|
|
1320
2021
|
<button
|
|
1321
2022
|
key={key}
|
|
1322
2023
|
type="button"
|
|
1323
|
-
className="
|
|
2024
|
+
className="st-menu"
|
|
1324
2025
|
role="menuitem"
|
|
2026
|
+
data-tour={key === 'help' ? 'help' : undefined}
|
|
1325
2027
|
aria-haspopup={hasDropdown ? 'menu' : undefined}
|
|
1326
2028
|
aria-expanded={hasDropdown ? open : undefined}
|
|
1327
|
-
aria-disabled={interactive ? undefined : 'true'}
|
|
1328
|
-
title={interactive ? '' : 'Coming in a later phase'}
|
|
1329
2029
|
onClick={() => onMenuClick(key)}
|
|
2030
|
+
// F4 — once any menu is open, hovering another trigger switches to
|
|
2031
|
+
// it (base-ui menubar behavior). Only among dropdown menus.
|
|
2032
|
+
onMouseEnter={() => {
|
|
2033
|
+
if (openMenu !== null && hasDropdown) setOpenMenu(key);
|
|
2034
|
+
}}
|
|
1330
2035
|
>
|
|
1331
2036
|
{name}
|
|
1332
2037
|
</button>
|
|
1333
2038
|
);
|
|
1334
2039
|
})}
|
|
1335
2040
|
</nav>
|
|
2041
|
+
{openMenu === 'file' && (
|
|
2042
|
+
<FileDropdown
|
|
2043
|
+
onAction={(id) => {
|
|
2044
|
+
if (id === 'new') onNewCanvas?.();
|
|
2045
|
+
else if (id === 'export') onOpenExport?.('export');
|
|
2046
|
+
else if (id === 'handoff') onOpenExport?.('handoff');
|
|
2047
|
+
else if (id === 'reload') onReload?.();
|
|
2048
|
+
}}
|
|
2049
|
+
onClose={() => setOpenMenu(null)}
|
|
2050
|
+
/>
|
|
2051
|
+
)}
|
|
2052
|
+
{openMenu === 'edit' && (
|
|
2053
|
+
<EditDropdown
|
|
2054
|
+
onAction={(id) => {
|
|
2055
|
+
if (id === 'undo') postToActiveCanvas({ dgn: 'undo' });
|
|
2056
|
+
else if (id === 'redo') postToActiveCanvas({ dgn: 'redo' });
|
|
2057
|
+
else if (id === 'deselect-all') postToActiveCanvas({ dgn: 'selection-clear' });
|
|
2058
|
+
else if (id === 'select-all-annotations')
|
|
2059
|
+
postToActiveCanvas({ dgn: 'annotation-select-all' });
|
|
2060
|
+
}}
|
|
2061
|
+
onClose={() => setOpenMenu(null)}
|
|
2062
|
+
/>
|
|
2063
|
+
)}
|
|
1336
2064
|
{openMenu === 'view' && (
|
|
1337
2065
|
<ViewDropdown
|
|
1338
2066
|
panels={panels}
|
|
@@ -1341,6 +2069,7 @@ function Menubar({
|
|
|
1341
2069
|
else if (id === 'comments') onToggleComments();
|
|
1342
2070
|
else if (id === 'hidden') onToggleShowHidden();
|
|
1343
2071
|
else if (id === 'annotate') onToggleAnnotations();
|
|
2072
|
+
else if (id === 'inspector') onToggleInspector();
|
|
1344
2073
|
}}
|
|
1345
2074
|
onClose={() => setOpenMenu(null)}
|
|
1346
2075
|
/>
|
|
@@ -1361,65 +2090,34 @@ function Menubar({
|
|
|
1361
2090
|
onClose={() => setOpenMenu(null)}
|
|
1362
2091
|
/>
|
|
1363
2092
|
)}
|
|
1364
|
-
<div className="mb-
|
|
1365
|
-
|
|
1366
|
-
<
|
|
1367
|
-
|
|
2093
|
+
<div className="st-mb-right">
|
|
2094
|
+
{presence ? <div className="st-presence">{presence}</div> : null}
|
|
2095
|
+
<button
|
|
2096
|
+
type="button"
|
|
2097
|
+
className="st-whatsnew"
|
|
2098
|
+
data-unseen={whatsNewCount > 0 ? 'true' : 'false'}
|
|
2099
|
+
aria-label={`What's new${whatsNewCount > 0 ? ` — ${whatsNewCount} unseen` : ''}`}
|
|
2100
|
+
title="What's new"
|
|
2101
|
+
onClick={onOpenWhatsNew}
|
|
2102
|
+
>
|
|
2103
|
+
<StIcon name="sparkle" size={15} />
|
|
2104
|
+
</button>
|
|
2105
|
+
<span className="st-stamp">{stamp}</span>
|
|
2106
|
+
<span className="st-mb-file" title={activePath || ''}>
|
|
1368
2107
|
{fileLabel}
|
|
1369
2108
|
</span>
|
|
1370
|
-
<span className="sep" />
|
|
1371
|
-
<span>
|
|
1372
|
-
<span className="
|
|
1373
|
-
|
|
1374
|
-
<span className="sep" />
|
|
1375
|
-
<span title="Pan/zoom in Phase 4">
|
|
1376
|
-
ZOOM <b>100%</b>
|
|
1377
|
-
</span>
|
|
1378
|
-
<span className="sep" />
|
|
1379
|
-
<span className="ok">
|
|
1380
|
-
<b>{project || 'MDCC'}</b>
|
|
2109
|
+
<span className="st-mb-sep" />
|
|
2110
|
+
<span className="st-mb-count" title="Artboards in the open canvas">
|
|
2111
|
+
<span className="st-dot" style={{ background: 'var(--accent)' }} />
|
|
2112
|
+
{artboardCount} ARTBOARDS
|
|
1381
2113
|
</span>
|
|
2114
|
+
<span className="st-mb-sep" />
|
|
2115
|
+
<span className="st-mb-proj">{project || 'maude'}</span>
|
|
1382
2116
|
</div>
|
|
1383
2117
|
</header>
|
|
1384
2118
|
);
|
|
1385
2119
|
}
|
|
1386
2120
|
|
|
1387
|
-
function ThemeToggle({ theme, onToggle }) {
|
|
1388
|
-
// Show the icon of the theme you'll switch TO — clearer affordance than current state.
|
|
1389
|
-
// Sun + Moon paths are condensed Lucide-style (single-path so the existing <Icon> works).
|
|
1390
|
-
const sun =
|
|
1391
|
-
'M12 7a5 5 0 100 10 5 5 0 000-10z M12 3v1 M12 20v1 M21 12h-1 M4 12H3 M16.95 7.05l-.71.71 M7.05 16.95l-.71.71 M16.95 16.95l-.71-.71 M7.05 7.05l-.71-.71';
|
|
1392
|
-
const moon = 'M21 12.79A9 9 0 1 1 11.21 3a7 7 0 0 0 9.79 9.79z';
|
|
1393
|
-
const next = theme === 'dark' ? 'light' : 'dark';
|
|
1394
|
-
return (
|
|
1395
|
-
<button
|
|
1396
|
-
type="button"
|
|
1397
|
-
className="theme-toggle"
|
|
1398
|
-
onClick={onToggle}
|
|
1399
|
-
title={`Switch to ${next} theme`}
|
|
1400
|
-
aria-label={`Switch to ${next} theme`}
|
|
1401
|
-
>
|
|
1402
|
-
<Icon d={theme === 'dark' ? sun : moon} size={14} />
|
|
1403
|
-
<span className="theme-toggle-label">{next}</span>
|
|
1404
|
-
</button>
|
|
1405
|
-
);
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
function Wordmark({ project, port, version }) {
|
|
1409
|
-
return (
|
|
1410
|
-
<div className="wm" aria-label="maude design server">
|
|
1411
|
-
<span className="wm-glyph">maude-design-server</span>
|
|
1412
|
-
<span className="wm-sub">
|
|
1413
|
-
<span>CANVAS · {(project || 'MAUDE').toUpperCase()}</span>
|
|
1414
|
-
<span className="wm-sep">/</span>
|
|
1415
|
-
<b>v{version}</b>
|
|
1416
|
-
<span className="wm-sep">/</span>
|
|
1417
|
-
<span>localhost:{port || '4399'}</span>
|
|
1418
|
-
</span>
|
|
1419
|
-
</div>
|
|
1420
|
-
);
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
2121
|
function Viewport({
|
|
1424
2122
|
tabs,
|
|
1425
2123
|
activePath,
|
|
@@ -1431,35 +2129,38 @@ function Viewport({
|
|
|
1431
2129
|
cfg,
|
|
1432
2130
|
}) {
|
|
1433
2131
|
return (
|
|
1434
|
-
<div className="viewport">
|
|
2132
|
+
<div className="viewport st-stage">
|
|
1435
2133
|
{tabs.length === 0 && (
|
|
1436
|
-
|
|
1437
|
-
<
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
the <strong>Design system</strong> view above it.
|
|
1447
|
-
<br />
|
|
1448
|
-
<br />
|
|
1449
|
-
Tabs work like in an editor — close with the × on each tab. <kbd>⌘R</kbd> reloads the
|
|
1450
|
-
active iframe.
|
|
1451
|
-
<br />
|
|
1452
|
-
<br />
|
|
1453
|
-
<strong>Element selection:</strong> hold <kbd>⌘</kbd> inside the canvas and hover for
|
|
1454
|
-
a preview, click to select. <kbd>⌘⇧</kbd>+click adds to a multi-selection.{' '}
|
|
1455
|
-
<kbd>V</kbd>/<kbd>H</kbd>/<kbd>C</kbd> swap tool; right-click opens the context menu.
|
|
1456
|
-
<br />
|
|
1457
|
-
<br />
|
|
1458
|
-
Active file, selection, and comments are tracked in <code>_active.json</code> +{' '}
|
|
1459
|
-
<code>_comments/</code> — Claude reads them when you run <code>/design</code>.
|
|
1460
|
-
</div>
|
|
2134
|
+
<div className="st-empty">
|
|
2135
|
+
<div className="st-empty-brand">
|
|
2136
|
+
<span className="st-brand-mark">
|
|
2137
|
+
<svg viewBox="0 0 32 32" width="100%" height="100%" fill="none" aria-hidden="true"><path d="M16 5l2.8 8.2L27 16l-8.2 2.8L16 27l-2.8-8.2L5 16l8.2-2.8z" fill="currentColor" /></svg>
|
|
2138
|
+
</span>
|
|
2139
|
+
<span className="st-empty-wm">maude</span>
|
|
2140
|
+
<span className="st-empty-sub st-mono">
|
|
2141
|
+
CANVAS · {(project || 'MAUDE').toUpperCase()} / v{MDCC_VERSION} /
|
|
2142
|
+
localhost:{typeof window !== 'undefined' ? window.location.port : '4399'}
|
|
2143
|
+
</span>
|
|
1461
2144
|
</div>
|
|
1462
|
-
|
|
2145
|
+
<div className="st-empty-title">No canvas open</div>
|
|
2146
|
+
<div className="st-empty-body">
|
|
2147
|
+
← Click a <code>.tsx</code> (or legacy <code>.html</code>) file in the tree, or open the{' '}
|
|
2148
|
+
<strong>Design system</strong> view above it.
|
|
2149
|
+
<br />
|
|
2150
|
+
<br />
|
|
2151
|
+
Tabs work like in an editor — close with the × on each tab. <Kbd>⌘R</Kbd> reloads the
|
|
2152
|
+
active iframe.
|
|
2153
|
+
<br />
|
|
2154
|
+
<br />
|
|
2155
|
+
<strong>Element selection:</strong> hold <Kbd>⌘</Kbd> inside the canvas and hover for a
|
|
2156
|
+
preview, click to select. <Kbd>⌘⇧</Kbd>+click adds to a multi-selection. <Kbd>V</Kbd>
|
|
2157
|
+
/<Kbd>H</Kbd>/<Kbd>C</Kbd> swap tool; right-click opens the context menu.
|
|
2158
|
+
<br />
|
|
2159
|
+
<br />
|
|
2160
|
+
Active file, selection, and comments are tracked in <code>_active.json</code> +{' '}
|
|
2161
|
+
<code>_comments/</code> — Claude reads them when you run <code>/design</code>.
|
|
2162
|
+
</div>
|
|
2163
|
+
</div>
|
|
1463
2164
|
)}
|
|
1464
2165
|
{tabs.map((t) => {
|
|
1465
2166
|
if (t.path === SYSTEM_TAB) {
|
|
@@ -1749,33 +2450,6 @@ function Gallery({ title, items, onOpen, kind, cfg }) {
|
|
|
1749
2450
|
|
|
1750
2451
|
// ---------- Comment composer / viewer ----------
|
|
1751
2452
|
|
|
1752
|
-
function CommentBar({ activePath, comments }) {
|
|
1753
|
-
// Phase 6 — the shell-side composer + chip strip + focused-row chrome were
|
|
1754
|
-
// removed. The iframe overlay (comments-overlay.tsx) owns composer + pin
|
|
1755
|
-
// bubbles + thread popover. BottomBar shrinks to a live open-count summary
|
|
1756
|
-
// so the shell still surfaces total review activity at a glance.
|
|
1757
|
-
if (!activePath) return null;
|
|
1758
|
-
const openComments = (comments || []).filter((c) => c.status !== 'resolved');
|
|
1759
|
-
if (openComments.length === 0) return null;
|
|
1760
|
-
return (
|
|
1761
|
-
<div className="comment-bar">
|
|
1762
|
-
<div className="cb-row strip">
|
|
1763
|
-
<span className="cb-label">
|
|
1764
|
-
{openComments.length} open comment{openComments.length === 1 ? '' : 's'}
|
|
1765
|
-
</span>
|
|
1766
|
-
</div>
|
|
1767
|
-
</div>
|
|
1768
|
-
);
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
function StatusBarSlot({ label, children, className = '' }) {
|
|
1772
|
-
return (
|
|
1773
|
-
<span className={'sb-slot ' + className} role="group" aria-label={label}>
|
|
1774
|
-
{children}
|
|
1775
|
-
</span>
|
|
1776
|
-
);
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
2453
|
function StatusBar({
|
|
1780
2454
|
activePath,
|
|
1781
2455
|
selected,
|
|
@@ -1797,66 +2471,108 @@ function StatusBar({
|
|
|
1797
2471
|
: selected
|
|
1798
2472
|
? selected.selector
|
|
1799
2473
|
: '';
|
|
2474
|
+
const nextTheme = theme === 'dark' ? 'light' : 'dark';
|
|
2475
|
+
|
|
2476
|
+
// P5 (Plan C) — always-on hub-sync slot. `/_sync-status` returns one of three
|
|
2477
|
+
// shapes: solo `{linked:false}` (hide), DDR-060 `{notSyncable,tsxCount,reason}`
|
|
2478
|
+
// (linked but 0 syncable), or the connection-state machine `{state,queuedOps,
|
|
2479
|
+
// flash,…}` (the common linked case the old notSyncable-only guard never
|
|
2480
|
+
// showed — the "hub sync se neukazuje" bug). Map each to a label + dot tone.
|
|
2481
|
+
const syncSlot = (() => {
|
|
2482
|
+
if (!syncStatus || syncStatus.linked === false) return null;
|
|
2483
|
+
if (syncStatus.notSyncable) {
|
|
2484
|
+
return {
|
|
2485
|
+
online: false,
|
|
2486
|
+
label: `0 syncable${syncStatus.tsxCount > 0 ? ` · ${syncStatus.tsxCount} tsx` : ''}`,
|
|
2487
|
+
title: syncStatus.reason || 'Linked to a hub, but no canvases are syncable.',
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
const q = syncStatus.queuedOps ?? 0;
|
|
2491
|
+
const synced = syncStatus.state === 'online' || syncStatus.flash === 'synced';
|
|
2492
|
+
if (synced) {
|
|
2493
|
+
return {
|
|
2494
|
+
online: true,
|
|
2495
|
+
label: q > 0 ? `${q} ↑` : 'synced',
|
|
2496
|
+
title: q > 0 ? `${q} edit(s) queued to push` : 'All changes synced to the hub',
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
return {
|
|
2500
|
+
online: false,
|
|
2501
|
+
label: `${q} ↑`,
|
|
2502
|
+
title:
|
|
2503
|
+
syncStatus.state === 'connecting'
|
|
2504
|
+
? 'Connecting to the hub…'
|
|
2505
|
+
: 'Offline — edits queued, will sync when the hub reconnects',
|
|
2506
|
+
};
|
|
2507
|
+
})();
|
|
2508
|
+
|
|
1800
2509
|
return (
|
|
1801
|
-
<
|
|
1802
|
-
<
|
|
1803
|
-
<span className="
|
|
1804
|
-
<span className="
|
|
2510
|
+
<footer className="st-statusbar" role="contentinfo">
|
|
2511
|
+
<span className="st-sb-slot st-sb-active" role="group" aria-label="Active file">
|
|
2512
|
+
<span className="lead" aria-hidden="true" />
|
|
2513
|
+
<span className="lbl">active</span>
|
|
2514
|
+
<span className="val" title={activePath || ''}>
|
|
1805
2515
|
{isSystem ? '▦ design system' : activePath || '—'}
|
|
1806
2516
|
</span>
|
|
1807
|
-
</
|
|
2517
|
+
</span>
|
|
1808
2518
|
|
|
1809
2519
|
{selected && selected.selector && !isSystem && (
|
|
1810
|
-
<
|
|
1811
|
-
<span className="
|
|
1812
|
-
|
|
1813
|
-
</span>
|
|
1814
|
-
<span className="sb-sel-text" title={title}>
|
|
2520
|
+
<span className="st-sb-slot st-sb-sel" role="group" aria-label="Selected element">
|
|
2521
|
+
<span className="lbl">selected</span>
|
|
2522
|
+
<span className="val" title={title}>
|
|
1815
2523
|
{text}
|
|
1816
2524
|
</span>
|
|
1817
2525
|
<button
|
|
1818
2526
|
type="button"
|
|
1819
|
-
className="sb-clear
|
|
2527
|
+
className="st-sb-sel-clear"
|
|
1820
2528
|
onClick={onClearSelected}
|
|
1821
2529
|
title="Clear (Esc inside iframe)"
|
|
1822
2530
|
aria-label="Clear selection"
|
|
1823
2531
|
>
|
|
1824
2532
|
×
|
|
1825
2533
|
</button>
|
|
1826
|
-
</
|
|
2534
|
+
</span>
|
|
1827
2535
|
)}
|
|
1828
2536
|
|
|
1829
|
-
<
|
|
1830
|
-
<span className="
|
|
1831
|
-
<span className="
|
|
1832
|
-
</
|
|
1833
|
-
|
|
1834
|
-
<
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
2537
|
+
<span className="st-sb-slot" role="group" aria-label="Open comments">
|
|
2538
|
+
<span className="lbl">comments</span>
|
|
2539
|
+
<span className="val">{openCount} open</span>
|
|
2540
|
+
</span>
|
|
2541
|
+
|
|
2542
|
+
<span className="st-sb-spacer" />
|
|
2543
|
+
|
|
2544
|
+
<span className="st-sb-slot" role="group" aria-label="Connection">
|
|
2545
|
+
<span className={'st-live-dot' + (wsConnected ? ' is-connected' : '')} aria-hidden="true" />
|
|
2546
|
+
<span className="lbl">{wsConnected ? 'live' : 'reconnecting'}</span>
|
|
2547
|
+
</span>
|
|
2548
|
+
|
|
2549
|
+
{/* P5 (Plan C) — always-on hub-sync slot (was notSyncable-only per DDR-060
|
|
2550
|
+
/ 9.1-D). Now also surfaces the connection-state machine's queued/synced
|
|
2551
|
+
counter for the common linked case. Solo projects render nothing. */}
|
|
2552
|
+
{syncSlot && (
|
|
2553
|
+
<span className="st-sb-slot st-sb-sync" role="group" aria-label="Hub sync">
|
|
1845
2554
|
<span
|
|
1846
|
-
className=
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
2555
|
+
className={'st-sb-sync-dot' + (syncSlot.online ? ' is-online' : '')}
|
|
2556
|
+
aria-hidden="true"
|
|
2557
|
+
/>
|
|
2558
|
+
<span className="lbl">hub sync</span>
|
|
2559
|
+
<span className="val" title={syncSlot.title}>
|
|
2560
|
+
{syncSlot.label}
|
|
1850
2561
|
</span>
|
|
1851
|
-
</
|
|
2562
|
+
</span>
|
|
1852
2563
|
)}
|
|
1853
2564
|
|
|
1854
|
-
<
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
2565
|
+
<button
|
|
2566
|
+
type="button"
|
|
2567
|
+
className="st-sb-theme"
|
|
2568
|
+
onClick={onToggleTheme}
|
|
2569
|
+
title={`Switch to ${nextTheme} theme`}
|
|
2570
|
+
aria-label={`Switch to ${nextTheme} theme`}
|
|
2571
|
+
>
|
|
2572
|
+
<StIcon name={theme === 'dark' ? 'sun' : 'moon'} size={13} />
|
|
2573
|
+
{nextTheme}
|
|
2574
|
+
</button>
|
|
2575
|
+
</footer>
|
|
1860
2576
|
);
|
|
1861
2577
|
}
|
|
1862
2578
|
|
|
@@ -1897,108 +2613,120 @@ function CommentsPanel({
|
|
|
1897
2613
|
}
|
|
1898
2614
|
|
|
1899
2615
|
return (
|
|
1900
|
-
<aside className="
|
|
1901
|
-
<div className="
|
|
1902
|
-
<
|
|
1903
|
-
<span>Comments</span>
|
|
1904
|
-
<span className="total">{counts.all}</span>
|
|
1905
|
-
</h2>
|
|
1906
|
-
<div className="rsidebar-filters" role="tablist">
|
|
2616
|
+
<aside className="st-rpanel" aria-label="Comments">
|
|
2617
|
+
<div className="st-rp-tabs st-rp-tabs--filters">
|
|
2618
|
+
<div className="st-cm-filters" role="tablist">
|
|
1907
2619
|
<button
|
|
1908
|
-
|
|
2620
|
+
type="button"
|
|
2621
|
+
className={'st-cm-filter' + (filter === 'all' ? ' is-active' : '')}
|
|
1909
2622
|
role="tab"
|
|
1910
2623
|
aria-selected={filter === 'all'}
|
|
1911
2624
|
onClick={() => setFilter('all')}
|
|
1912
2625
|
>
|
|
1913
|
-
All
|
|
2626
|
+
All · {counts.all}
|
|
1914
2627
|
</button>
|
|
1915
2628
|
<button
|
|
1916
|
-
|
|
2629
|
+
type="button"
|
|
2630
|
+
className={'st-cm-filter' + (filter === 'open' ? ' is-active' : '')}
|
|
1917
2631
|
role="tab"
|
|
1918
2632
|
aria-selected={filter === 'open'}
|
|
1919
2633
|
onClick={() => setFilter('open')}
|
|
1920
2634
|
>
|
|
1921
|
-
Open
|
|
2635
|
+
Open · {counts.open}
|
|
1922
2636
|
</button>
|
|
1923
2637
|
<button
|
|
1924
|
-
|
|
2638
|
+
type="button"
|
|
2639
|
+
className={'st-cm-filter' + (filter === 'resolved' ? ' is-active' : '')}
|
|
1925
2640
|
role="tab"
|
|
1926
2641
|
aria-selected={filter === 'resolved'}
|
|
1927
2642
|
onClick={() => setFilter('resolved')}
|
|
1928
2643
|
>
|
|
1929
|
-
Resolved
|
|
2644
|
+
Resolved · {counts.resolved}
|
|
1930
2645
|
</button>
|
|
1931
2646
|
</div>
|
|
1932
2647
|
</div>
|
|
1933
|
-
<div className="
|
|
2648
|
+
<div className="st-rp-body" style={{ gap: 'var(--space-4)' }}>
|
|
1934
2649
|
{groups.length === 0 ? (
|
|
1935
|
-
<div className="
|
|
2650
|
+
<div className="st-rp-empty">
|
|
1936
2651
|
<p>No comments {filter !== 'all' ? `with status “${filter}”` : 'yet'}.</p>
|
|
1937
|
-
<p
|
|
1938
|
-
Open a canvas, hold <
|
|
1939
|
-
hold <
|
|
2652
|
+
<p>
|
|
2653
|
+
Open a canvas, hold <Kbd>⌘</Kbd> and click an element, then press <Kbd>C</Kbd> — or
|
|
2654
|
+
hold <Kbd>⌘⇧</Kbd> and click directly.
|
|
1940
2655
|
</p>
|
|
1941
2656
|
</div>
|
|
1942
2657
|
) : (
|
|
1943
2658
|
groups.map((g) => (
|
|
1944
|
-
<
|
|
1945
|
-
<button
|
|
1946
|
-
|
|
1947
|
-
|
|
2659
|
+
<Fragment key={g.file}>
|
|
2660
|
+
<button
|
|
2661
|
+
type="button"
|
|
2662
|
+
className="st-cm-group-hd"
|
|
2663
|
+
onClick={() => onJump(g.file, null)}
|
|
2664
|
+
title={g.file}
|
|
2665
|
+
>
|
|
2666
|
+
<span>{displayName(basename(g.file))}</span>
|
|
2667
|
+
<span className="st-mono">{g.comments.length}</span>
|
|
1948
2668
|
</button>
|
|
1949
2669
|
{g.comments.map((c) => (
|
|
1950
2670
|
<div
|
|
1951
2671
|
key={c.id}
|
|
1952
2672
|
className={
|
|
1953
|
-
'
|
|
1954
|
-
(c.status === 'resolved' ? ' resolved' : '') +
|
|
1955
|
-
(c.id === focusedId ? ' active
|
|
2673
|
+
'st-comment' +
|
|
2674
|
+
(c.status === 'resolved' ? ' is-resolved' : '') +
|
|
2675
|
+
(c.id === focusedId ? ' is-active' : '')
|
|
1956
2676
|
}
|
|
1957
2677
|
onClick={() => onJump(g.file, c.id)}
|
|
1958
2678
|
>
|
|
1959
|
-
<div className="
|
|
1960
|
-
<span className="
|
|
1961
|
-
<span className="
|
|
2679
|
+
<div className="st-comment-hd">
|
|
2680
|
+
<span className="st-pin st-pin--inline">{c.n || '·'}</span>
|
|
2681
|
+
<span className="st-comment-time">{timeAgo(c.created)}</span>
|
|
1962
2682
|
</div>
|
|
1963
|
-
<div className="
|
|
1964
|
-
<div className="
|
|
1965
|
-
<
|
|
1966
|
-
|
|
2683
|
+
<div className="st-comment-txt">{c.text}</div>
|
|
2684
|
+
<div className="st-comment-foot">
|
|
2685
|
+
<span className="st-comment-sel" title={(c.dom_path || []).join(' > ')}>
|
|
2686
|
+
{c.selector || '—'}
|
|
2687
|
+
</span>
|
|
2688
|
+
<span className="st-mini-act">
|
|
1967
2689
|
{c.status === 'resolved' ? (
|
|
1968
2690
|
<button
|
|
1969
|
-
|
|
2691
|
+
type="button"
|
|
2692
|
+
className="st-iconbtn"
|
|
2693
|
+
aria-label="Reopen"
|
|
1970
2694
|
onClick={(e) => {
|
|
1971
2695
|
e.stopPropagation();
|
|
1972
2696
|
onReopen(c.id);
|
|
1973
2697
|
}}
|
|
1974
2698
|
>
|
|
1975
|
-
|
|
2699
|
+
<StIcon name="reopen" size={14} />
|
|
1976
2700
|
</button>
|
|
1977
2701
|
) : (
|
|
1978
2702
|
<button
|
|
1979
|
-
|
|
2703
|
+
type="button"
|
|
2704
|
+
className="st-iconbtn"
|
|
2705
|
+
aria-label="Resolve"
|
|
1980
2706
|
onClick={(e) => {
|
|
1981
2707
|
e.stopPropagation();
|
|
1982
2708
|
onResolve(c.id);
|
|
1983
2709
|
}}
|
|
1984
2710
|
>
|
|
1985
|
-
|
|
2711
|
+
<StIcon name="resolve" size={14} />
|
|
1986
2712
|
</button>
|
|
1987
2713
|
)}
|
|
1988
2714
|
<button
|
|
1989
|
-
|
|
2715
|
+
type="button"
|
|
2716
|
+
className="st-iconbtn"
|
|
2717
|
+
aria-label="Delete"
|
|
1990
2718
|
onClick={(e) => {
|
|
1991
2719
|
e.stopPropagation();
|
|
1992
2720
|
onDelete(c.id);
|
|
1993
2721
|
}}
|
|
1994
2722
|
>
|
|
1995
|
-
|
|
2723
|
+
<StIcon name="x" size={14} />
|
|
1996
2724
|
</button>
|
|
1997
|
-
</
|
|
2725
|
+
</span>
|
|
1998
2726
|
</div>
|
|
1999
2727
|
</div>
|
|
2000
2728
|
))}
|
|
2001
|
-
</
|
|
2729
|
+
</Fragment>
|
|
2002
2730
|
))
|
|
2003
2731
|
)}
|
|
2004
2732
|
</div>
|
|
@@ -2013,6 +2741,10 @@ function CommentsPanel({
|
|
|
2013
2741
|
// for 3s right after a reconnect. Driven entirely by the 'sync:status' payload
|
|
2014
2742
|
// the dev-server's linked-mode sync runtime broadcasts.
|
|
2015
2743
|
function SyncBanner({ status }) {
|
|
2744
|
+
// Plan C follow-up — the banner overlapped the menubar and wasn't dismissable.
|
|
2745
|
+
// Dismissal is keyed on the connection state, so a transition (reconnect flash,
|
|
2746
|
+
// escalation to offline-long) re-surfaces it; a stable state stays hidden.
|
|
2747
|
+
const [dismissedKey, setDismissedKey] = useState(null);
|
|
2016
2748
|
if (!status || status.linked === false) return null;
|
|
2017
2749
|
// DDR-060 / 9.1-D — the "linked but 0 syncable" state is surfaced in the
|
|
2018
2750
|
// status bar (sb-sync slot), NOT as a floating banner. This component owns
|
|
@@ -2022,61 +2754,179 @@ function SyncBanner({ status }) {
|
|
|
2022
2754
|
const showFlash = flash === 'synced';
|
|
2023
2755
|
const offline = state === 'offline' || state === 'offline-long';
|
|
2024
2756
|
if (!offline && !showFlash) return null;
|
|
2757
|
+
const dismissKey = `${state}:${showFlash ? 'flash' : 'offline'}`;
|
|
2758
|
+
if (dismissedKey === dismissKey) return null;
|
|
2025
2759
|
|
|
2026
|
-
let
|
|
2027
|
-
let fg;
|
|
2028
|
-
let border;
|
|
2760
|
+
let variant;
|
|
2029
2761
|
let text;
|
|
2030
2762
|
if (showFlash) {
|
|
2031
|
-
|
|
2032
|
-
fg = '#166534';
|
|
2033
|
-
border = '#86efac';
|
|
2763
|
+
variant = 'success';
|
|
2034
2764
|
text = 'Synced with hub';
|
|
2035
2765
|
} else if (state === 'offline-long') {
|
|
2036
|
-
|
|
2037
|
-
fg = '#991b1b';
|
|
2038
|
-
border = '#fca5a5';
|
|
2766
|
+
variant = 'error';
|
|
2039
2767
|
text = `Long offline — ${queuedOps} edit(s) queued. Consider \`git commit && git push\` as backup.`;
|
|
2040
2768
|
} else {
|
|
2041
|
-
|
|
2042
|
-
fg = '#854d0e';
|
|
2043
|
-
border = '#fde047';
|
|
2769
|
+
variant = 'warn';
|
|
2044
2770
|
text = `Working offline · ${queuedOps} edit(s) queued · will sync when the hub reconnects.`;
|
|
2045
2771
|
}
|
|
2046
2772
|
const conflictNote =
|
|
2047
2773
|
conflicts && conflicts.length > 0 ? ` (${conflicts.length} conflict notice(s))` : '';
|
|
2048
2774
|
|
|
2049
2775
|
return (
|
|
2050
|
-
<div
|
|
2051
|
-
|
|
2052
|
-
aria-live="polite"
|
|
2053
|
-
style={{
|
|
2054
|
-
position: 'fixed',
|
|
2055
|
-
top: 12,
|
|
2056
|
-
left: '50%',
|
|
2057
|
-
transform: 'translateX(-50%)',
|
|
2058
|
-
zIndex: 10003,
|
|
2059
|
-
display: 'flex',
|
|
2060
|
-
alignItems: 'center',
|
|
2061
|
-
gap: 10,
|
|
2062
|
-
padding: '8px 14px',
|
|
2063
|
-
background: bg,
|
|
2064
|
-
color: fg,
|
|
2065
|
-
border: `1px solid ${border}`,
|
|
2066
|
-
borderRadius: 999,
|
|
2067
|
-
boxShadow: '0 2px 8px rgba(0,0,0,0.12)',
|
|
2068
|
-
font: '500 13px/1.2 system-ui, -apple-system, sans-serif',
|
|
2069
|
-
maxWidth: '80vw',
|
|
2070
|
-
}}
|
|
2071
|
-
>
|
|
2776
|
+
<div role="status" aria-live="polite" className={`st-banner st-banner--${variant}`}>
|
|
2777
|
+
<span className="st-banner-dot" aria-hidden="true" />
|
|
2072
2778
|
<span>
|
|
2073
2779
|
{text}
|
|
2074
2780
|
{conflictNote}
|
|
2075
2781
|
</span>
|
|
2782
|
+
<button
|
|
2783
|
+
type="button"
|
|
2784
|
+
className="st-banner-close"
|
|
2785
|
+
aria-label="Dismiss"
|
|
2786
|
+
title="Dismiss"
|
|
2787
|
+
onClick={() => setDismissedKey(dismissKey)}
|
|
2788
|
+
>
|
|
2789
|
+
×
|
|
2790
|
+
</button>
|
|
2076
2791
|
</div>
|
|
2077
2792
|
);
|
|
2078
2793
|
}
|
|
2079
2794
|
|
|
2795
|
+
// ---------- Inspector panel (display-only) ----------
|
|
2796
|
+
//
|
|
2797
|
+
// T6 (Plan C) — right-dock Inspect / Layers / CSS tabs per `.design/ui/Studio.tsx`
|
|
2798
|
+
// InspectorPanel. DISPLAY-ONLY: reads the live `selected` payload from the
|
|
2799
|
+
// inspector bridge (`bounds`, `tag`, `classes`, `dom_path`, `html`). The
|
|
2800
|
+
// mockup's live-CSS-knob WRITEBACK is Phase 12 (needs a canvas-origin write
|
|
2801
|
+
// bridge, DDR-054) — the CSS tab shows markup read-only + keeps that callout, so
|
|
2802
|
+
// it never implies functionality it lacks (the exact reason DDR-096 deferred it).
|
|
2803
|
+
function InspectorPanel({ selected, onClose }) {
|
|
2804
|
+
const [tab, setTab] = useState('inspect');
|
|
2805
|
+
// `selected` may be a single element, an array (multi-select), or null.
|
|
2806
|
+
const el = Array.isArray(selected) ? selected[0] : selected;
|
|
2807
|
+
const tabBtn = (id, label, icon) => (
|
|
2808
|
+
<button
|
|
2809
|
+
type="button"
|
|
2810
|
+
className={'st-rp-tab' + (tab === id ? ' is-active' : '')}
|
|
2811
|
+
onClick={() => setTab(id)}
|
|
2812
|
+
>
|
|
2813
|
+
<StIcon name={icon} size={14} />
|
|
2814
|
+
{label}
|
|
2815
|
+
</button>
|
|
2816
|
+
);
|
|
2817
|
+
const b = el?.bounds || null;
|
|
2818
|
+
return (
|
|
2819
|
+
<aside className="st-rpanel" aria-label="Inspector">
|
|
2820
|
+
<div className="st-rp-tabs">
|
|
2821
|
+
{tabBtn('inspect', 'Inspect', 'sliders')}
|
|
2822
|
+
{tabBtn('layers', 'Layers', 'layers')}
|
|
2823
|
+
{tabBtn('css', 'CSS', 'code')}
|
|
2824
|
+
<button
|
|
2825
|
+
type="button"
|
|
2826
|
+
className="st-iconbtn"
|
|
2827
|
+
aria-label="Close inspector"
|
|
2828
|
+
style={{ marginLeft: 'auto' }}
|
|
2829
|
+
onClick={onClose}
|
|
2830
|
+
>
|
|
2831
|
+
<StIcon name="x" size={14} />
|
|
2832
|
+
</button>
|
|
2833
|
+
</div>
|
|
2834
|
+
<div className="st-rp-body">
|
|
2835
|
+
{!el ? (
|
|
2836
|
+
<div className="st-rp-empty">
|
|
2837
|
+
Hold <Kbd>⌘</Kbd> inside the canvas and click an element to inspect it.
|
|
2838
|
+
</div>
|
|
2839
|
+
) : tab === 'inspect' ? (
|
|
2840
|
+
<>
|
|
2841
|
+
<div className="st-rp-hd">{el.selector || el.tag || 'element'}</div>
|
|
2842
|
+
<div className="st-insp-row">
|
|
2843
|
+
<span className="st-insp-label">Pos</span>
|
|
2844
|
+
<div className="st-insp-fields">
|
|
2845
|
+
<span className="st-field-lead">
|
|
2846
|
+
<span className="k">X</span>
|
|
2847
|
+
<input className="st-field" value={b ? Math.round(b.x) : '—'} readOnly />
|
|
2848
|
+
</span>
|
|
2849
|
+
<span className="st-field-lead">
|
|
2850
|
+
<span className="k">Y</span>
|
|
2851
|
+
<input className="st-field" value={b ? Math.round(b.y) : '—'} readOnly />
|
|
2852
|
+
</span>
|
|
2853
|
+
</div>
|
|
2854
|
+
</div>
|
|
2855
|
+
<div className="st-insp-row">
|
|
2856
|
+
<span className="st-insp-label">Size</span>
|
|
2857
|
+
<div className="st-insp-fields">
|
|
2858
|
+
<span className="st-field-lead">
|
|
2859
|
+
<span className="k">W</span>
|
|
2860
|
+
<input className="st-field" value={b ? Math.round(b.w) : '—'} readOnly />
|
|
2861
|
+
</span>
|
|
2862
|
+
<span className="st-field-lead">
|
|
2863
|
+
<span className="k">H</span>
|
|
2864
|
+
<input className="st-field" value={b ? Math.round(b.h) : '—'} readOnly />
|
|
2865
|
+
</span>
|
|
2866
|
+
</div>
|
|
2867
|
+
</div>
|
|
2868
|
+
<div className="st-insp-row">
|
|
2869
|
+
<span className="st-insp-label">Tag</span>
|
|
2870
|
+
<div className="st-insp-fields">
|
|
2871
|
+
<span className="st-mono" style={{ fontSize: 11, color: 'var(--fg-0)' }}>
|
|
2872
|
+
{el.tag || '—'}
|
|
2873
|
+
</span>
|
|
2874
|
+
</div>
|
|
2875
|
+
</div>
|
|
2876
|
+
{el.classes ? (
|
|
2877
|
+
<div className="st-insp-row">
|
|
2878
|
+
<span className="st-insp-label">Class</span>
|
|
2879
|
+
<div className="st-insp-fields">
|
|
2880
|
+
<span className="st-mono" style={{ fontSize: 11, color: 'var(--fg-1)' }}>
|
|
2881
|
+
{el.classes}
|
|
2882
|
+
</span>
|
|
2883
|
+
</div>
|
|
2884
|
+
</div>
|
|
2885
|
+
) : null}
|
|
2886
|
+
<div className="callout callout--info" style={{ fontSize: 12 }}>
|
|
2887
|
+
Computed fill / radius / type readout lands with the live CSS bridge (Phase 12).
|
|
2888
|
+
</div>
|
|
2889
|
+
</>
|
|
2890
|
+
) : tab === 'layers' ? (
|
|
2891
|
+
<>
|
|
2892
|
+
<div className="st-rp-hd">Layers · ancestry</div>
|
|
2893
|
+
{Array.isArray(el.dom_path) && el.dom_path.length ? (
|
|
2894
|
+
el.dom_path.map((node, i) => (
|
|
2895
|
+
<div
|
|
2896
|
+
key={i}
|
|
2897
|
+
className={'st-layer' + (i === el.dom_path.length - 1 ? ' is-sel' : '')}
|
|
2898
|
+
style={{ paddingLeft: 8 + i * 12 }}
|
|
2899
|
+
>
|
|
2900
|
+
<StIcon name="square" size={13} />
|
|
2901
|
+
{node}
|
|
2902
|
+
</div>
|
|
2903
|
+
))
|
|
2904
|
+
) : (
|
|
2905
|
+
<div className="st-rp-empty">No ancestry path for this selection.</div>
|
|
2906
|
+
)}
|
|
2907
|
+
</>
|
|
2908
|
+
) : (
|
|
2909
|
+
<>
|
|
2910
|
+
<div className="st-rp-hd">Markup · {el.selector || el.tag}</div>
|
|
2911
|
+
<div className="st-css">
|
|
2912
|
+
<div>
|
|
2913
|
+
<span className="comment">/* read-only — outerHTML snapshot */</span>
|
|
2914
|
+
</div>
|
|
2915
|
+
<div style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
|
2916
|
+
{(el.html || '').slice(0, 600) || '(no markup captured)'}
|
|
2917
|
+
</div>
|
|
2918
|
+
</div>
|
|
2919
|
+
<div className="callout callout--info" style={{ fontSize: 12 }}>
|
|
2920
|
+
Phase 12 — knob edits will write back to the artboard live and stage a diff for
|
|
2921
|
+
handoff. Today the inspector is read-only.
|
|
2922
|
+
</div>
|
|
2923
|
+
</>
|
|
2924
|
+
)}
|
|
2925
|
+
</div>
|
|
2926
|
+
</aside>
|
|
2927
|
+
);
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2080
2930
|
// ---------- App ----------
|
|
2081
2931
|
|
|
2082
2932
|
function App() {
|
|
@@ -2105,7 +2955,11 @@ function App() {
|
|
|
2105
2955
|
.then((data) => {
|
|
2106
2956
|
if (cancelled) return;
|
|
2107
2957
|
const designRel = (data.designRoot || '.design').replace(/^\/+|\/+$/g, '');
|
|
2108
|
-
|
|
2958
|
+
// Functional merge — the `/_config` and `/_index-data` fetches race, and
|
|
2959
|
+
// the latter contributes `canvasDesignSystems` (DDR-093). A full-replace
|
|
2960
|
+
// here would clobber that map if it resolved second.
|
|
2961
|
+
setCfg((prev) => ({
|
|
2962
|
+
...prev,
|
|
2109
2963
|
designRel,
|
|
2110
2964
|
tokensCssRel: data.tokensCssRel,
|
|
2111
2965
|
// Pass through designSystems so canvasUrl can resolve the right
|
|
@@ -2118,7 +2972,7 @@ function App() {
|
|
|
2118
2972
|
// from the main origin's /_api). Absent on older servers → relative
|
|
2119
2973
|
// URL fallback keeps same-origin behavior.
|
|
2120
2974
|
canvasOrigin: data.canvasOrigin,
|
|
2121
|
-
});
|
|
2975
|
+
}));
|
|
2122
2976
|
})
|
|
2123
2977
|
.catch(() => {});
|
|
2124
2978
|
return () => {
|
|
@@ -2154,7 +3008,35 @@ function App() {
|
|
|
2154
3008
|
const [showHidden, setShowHidden] = useState(() => readBoolStore(SHOW_HIDDEN_STORE, false));
|
|
2155
3009
|
const [sectionsExpanded, setSectionsExpanded] = useState(() => readJsonStore(SECTIONS_STORE, {}));
|
|
2156
3010
|
const [helpOpen, setHelpOpen] = useState(false);
|
|
3011
|
+
const [paletteOpen, setPaletteOpen] = useState(false);
|
|
3012
|
+
// T5/T6 (Plan C) — shell-level export/handoff dialog + inspector panel state.
|
|
3013
|
+
// The palette (T4) drives them; the dialog (T5) + panel (T6) consume them.
|
|
3014
|
+
const [exportDialog, setExportDialog] = useState(null); // null | { mode: 'export'|'handoff', scope? }
|
|
3015
|
+
const [inspectorOpen, setInspectorOpen] = useState(false);
|
|
3016
|
+
const whatsNew = useWhatsNew(MDCC_VERSION);
|
|
3017
|
+
const [tourSteps, setTourSteps] = useState(null);
|
|
3018
|
+
const [usageNudge, setUsageNudge] = useState(() => !readBoolStore(USAGE_TOUR_STORE, false));
|
|
3019
|
+
const startTour = useCallback((steps) => {
|
|
3020
|
+
setTourSteps(Array.isArray(steps) && steps.length ? steps : null);
|
|
3021
|
+
}, []);
|
|
3022
|
+
const markUsageSeen = useCallback(() => {
|
|
3023
|
+
setUsageNudge(false);
|
|
3024
|
+
try {
|
|
3025
|
+
localStorage.setItem(USAGE_TOUR_STORE, '1');
|
|
3026
|
+
} catch {}
|
|
3027
|
+
}, []);
|
|
2157
3028
|
const [annotationsVisible, setAnnotationsVisible] = useState(true);
|
|
3029
|
+
// P2/P3 (Plan C) — top-bar live state. (Zoom lives in the canvas toolbar pill,
|
|
3030
|
+
// so the top bar no longer mirrors it.)
|
|
3031
|
+
// activeArtboards — real artboard count of the open canvas, read from its
|
|
3032
|
+
// `<canvas>.meta.json` sidecar (shell-side, no iframe dep).
|
|
3033
|
+
// gitUser — local user (name/initials) for the menubar presence avatar.
|
|
3034
|
+
// agentActive — transient flag set on `ai-activity`, cleared after idle, so
|
|
3035
|
+
// the menubar shows a live agent avatar while Claude edits.
|
|
3036
|
+
const [activeArtboards, setActiveArtboards] = useState(0);
|
|
3037
|
+
const [gitUser, setGitUser] = useState(null);
|
|
3038
|
+
const [agentActive, setAgentActive] = useState(false);
|
|
3039
|
+
const agentIdleRef = useRef(null);
|
|
2158
3040
|
const wsRef = useRef(null);
|
|
2159
3041
|
const iframesRef = useRef(new Map());
|
|
2160
3042
|
|
|
@@ -2186,6 +3068,45 @@ function App() {
|
|
|
2186
3068
|
});
|
|
2187
3069
|
}, [activePath]);
|
|
2188
3070
|
|
|
3071
|
+
// P3 (Plan C) — local git user for the menubar presence avatar. One-shot.
|
|
3072
|
+
useEffect(() => {
|
|
3073
|
+
let cancelled = false;
|
|
3074
|
+
fetch('/_api/git-user')
|
|
3075
|
+
.then((r) => r.json())
|
|
3076
|
+
.then((d) => {
|
|
3077
|
+
if (cancelled) return;
|
|
3078
|
+
const n = d && typeof d.name === 'string' ? d.name.trim() : '';
|
|
3079
|
+
if (n) setGitUser(n);
|
|
3080
|
+
})
|
|
3081
|
+
.catch(() => {});
|
|
3082
|
+
return () => {
|
|
3083
|
+
cancelled = true;
|
|
3084
|
+
};
|
|
3085
|
+
}, []);
|
|
3086
|
+
|
|
3087
|
+
// P2 (Plan C) — when the active canvas changes, read its real artboard count
|
|
3088
|
+
// from the `<canvas>.meta.json` sidecar (shell-side, no iframe dep).
|
|
3089
|
+
useEffect(() => {
|
|
3090
|
+
if (!activePath || activePath === SYSTEM_TAB) {
|
|
3091
|
+
setActiveArtboards(0);
|
|
3092
|
+
return;
|
|
3093
|
+
}
|
|
3094
|
+
let cancelled = false;
|
|
3095
|
+
fetch('/_api/canvas-meta?file=' + encodeURIComponent(activePath))
|
|
3096
|
+
.then((r) => r.json())
|
|
3097
|
+
.then((meta) => {
|
|
3098
|
+
if (cancelled) return;
|
|
3099
|
+
const n = Array.isArray(meta?.artboards) ? meta.artboards.length : 0;
|
|
3100
|
+
setActiveArtboards(n);
|
|
3101
|
+
})
|
|
3102
|
+
.catch(() => {
|
|
3103
|
+
if (!cancelled) setActiveArtboards(0);
|
|
3104
|
+
});
|
|
3105
|
+
return () => {
|
|
3106
|
+
cancelled = true;
|
|
3107
|
+
};
|
|
3108
|
+
}, [activePath]);
|
|
3109
|
+
|
|
2189
3110
|
// Sync theme to <html data-theme> + localStorage on every change.
|
|
2190
3111
|
useEffect(() => {
|
|
2191
3112
|
try {
|
|
@@ -2247,6 +3168,13 @@ function App() {
|
|
|
2247
3168
|
tree: buildTree(g.paths, g.stripPrefix),
|
|
2248
3169
|
}));
|
|
2249
3170
|
setGroups(built);
|
|
3171
|
+
// DDR-093 — fold the server-resolved per-canvas DS map into cfg so
|
|
3172
|
+
// canvasUrl() injects each UI canvas's OWN design-system tokens instead of
|
|
3173
|
+
// always designSystems[0]. Functional merge to coexist with the /_config
|
|
3174
|
+
// fetch (either may land first). `?? {}` keeps older servers (no map) on
|
|
3175
|
+
// the ds0 fallback. Re-runs on every tree reload, so adding/retargeting a
|
|
3176
|
+
// canvas refreshes the map.
|
|
3177
|
+
setCfg((prev) => ({ ...prev, canvasDesignSystems: data.canvasDesignSystems ?? {} }));
|
|
2250
3178
|
} catch (e) {
|
|
2251
3179
|
console.error('failed to load tree', e);
|
|
2252
3180
|
}
|
|
@@ -2323,6 +3251,12 @@ function App() {
|
|
|
2323
3251
|
} else if (m.type === 'comments' && typeof m.file === 'string') {
|
|
2324
3252
|
setCommentsByFile((prev) => ({ ...prev, [m.file]: m.comments || [] }));
|
|
2325
3253
|
} else if (m.type === 'ai-activity' && typeof m.file === 'string') {
|
|
3254
|
+
// P3 (Plan C) — surface live agent activity as a menubar presence
|
|
3255
|
+
// avatar. Set the flag and (re)arm an idle timer so the avatar
|
|
3256
|
+
// fades once Claude stops editing.
|
|
3257
|
+
setAgentActive(true);
|
|
3258
|
+
if (agentIdleRef.current) clearTimeout(agentIdleRef.current);
|
|
3259
|
+
agentIdleRef.current = setTimeout(() => setAgentActive(false), 8000);
|
|
2326
3260
|
// Phase 8 Task 4 — relay to every open iframe; each canvas's
|
|
2327
3261
|
// AiBanner filters by its own file path. Lightweight broadcast
|
|
2328
3262
|
// (one envelope per change, not per iframe count).
|
|
@@ -2430,6 +3364,63 @@ function App() {
|
|
|
2430
3364
|
|
|
2431
3365
|
const reloadTree = useCallback(() => loadTree(), [loadTree]);
|
|
2432
3366
|
|
|
3367
|
+
// Phase 22 — create a blank brief board from the tree header. POSTs to the
|
|
3368
|
+
// main-origin-only /_api/canvas (the untrusted canvas iframe can't reach it),
|
|
3369
|
+
// then refreshes the tree and opens the new board so it's immediately the
|
|
3370
|
+
// active canvas to annotate. Returns {ok} | {ok:false,error} so the Sidebar
|
|
3371
|
+
// can surface a validation/duplicate message inline.
|
|
3372
|
+
const createBoard = useCallback(
|
|
3373
|
+
async (name) => {
|
|
3374
|
+
try {
|
|
3375
|
+
const r = await fetch('/_api/canvas', {
|
|
3376
|
+
method: 'POST',
|
|
3377
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3378
|
+
body: JSON.stringify({ name, kind: 'brief-board' }),
|
|
3379
|
+
});
|
|
3380
|
+
const j = await r.json().catch(() => ({}));
|
|
3381
|
+
if (!r.ok || !j.ok) return { ok: false, error: j.error || `create failed (${r.status})` };
|
|
3382
|
+
await loadTree();
|
|
3383
|
+
openTab(j.file);
|
|
3384
|
+
return { ok: true, file: j.file };
|
|
3385
|
+
} catch (e) {
|
|
3386
|
+
return { ok: false, error: e instanceof Error ? e.message : 'network error' };
|
|
3387
|
+
}
|
|
3388
|
+
},
|
|
3389
|
+
[loadTree, openTab]
|
|
3390
|
+
);
|
|
3391
|
+
|
|
3392
|
+
// Phase 22 — soft-delete a canvas from the file tree. Confirms (destructive),
|
|
3393
|
+
// DELETEs to the main-origin-only endpoint, refreshes the tree, and resets the
|
|
3394
|
+
// active tab if the deleted canvas was open. The server moves the whole sidecar
|
|
3395
|
+
// set to .design/_trash/ — recoverable locally.
|
|
3396
|
+
const deleteBoard = useCallback(
|
|
3397
|
+
async (filePath, label) => {
|
|
3398
|
+
const ok = window.confirm(
|
|
3399
|
+
`Move “${label}” to trash?\n\nIts annotations, history and comments move with it. ` +
|
|
3400
|
+
`You can restore it from .design/_trash/.`
|
|
3401
|
+
);
|
|
3402
|
+
if (!ok) return;
|
|
3403
|
+
try {
|
|
3404
|
+
const r = await fetch(`/_api/canvas?file=${encodeURIComponent(filePath)}`, {
|
|
3405
|
+
method: 'DELETE',
|
|
3406
|
+
});
|
|
3407
|
+
const j = await r.json().catch(() => ({}));
|
|
3408
|
+
if (!r.ok || !j.ok) {
|
|
3409
|
+
window.alert(`Could not delete: ${j.error || `error ${r.status}`}`);
|
|
3410
|
+
return;
|
|
3411
|
+
}
|
|
3412
|
+
await loadTree();
|
|
3413
|
+
if (activePath === filePath) {
|
|
3414
|
+
setTabs([]);
|
|
3415
|
+
setActivePath(null);
|
|
3416
|
+
}
|
|
3417
|
+
} catch (e) {
|
|
3418
|
+
window.alert(`Delete failed: ${e instanceof Error ? e.message : 'network error'}`);
|
|
3419
|
+
}
|
|
3420
|
+
},
|
|
3421
|
+
[loadTree, activePath]
|
|
3422
|
+
);
|
|
3423
|
+
|
|
2433
3424
|
const clearSelected = useCallback(() => {
|
|
2434
3425
|
wsSend({ type: 'clear-select' });
|
|
2435
3426
|
setSelected(null);
|
|
@@ -2536,6 +3527,7 @@ function App() {
|
|
|
2536
3527
|
payload: {
|
|
2537
3528
|
file: p.file,
|
|
2538
3529
|
selector: p.selector,
|
|
3530
|
+
index: p.index,
|
|
2539
3531
|
dom_path: p.dom_path,
|
|
2540
3532
|
tag: p.tag,
|
|
2541
3533
|
classes: p.classes,
|
|
@@ -2553,6 +3545,24 @@ function App() {
|
|
|
2553
3545
|
setFocusedCommentId((prev) => (prev === m.id ? null : prev));
|
|
2554
3546
|
} else if (m.dgn === 'comment-click' && m.id) {
|
|
2555
3547
|
setFocusedCommentId(m.id);
|
|
3548
|
+
} else if (m.dgn === 'artboards' && typeof m.count === 'number') {
|
|
3549
|
+
// P2 (Plan C) — optional iframe-reported artboard count; overrides the
|
|
3550
|
+
// meta.json-derived seed when the canvas knows better. Clamp.
|
|
3551
|
+
const n = Math.round(m.count);
|
|
3552
|
+
if (Number.isFinite(n) && n >= 0 && n <= 999) setActiveArtboards(n);
|
|
3553
|
+
} else if (m.dgn === 'toggle-palette') {
|
|
3554
|
+
// ⌘K pressed while focus was inside the canvas iframe — the injected
|
|
3555
|
+
// inspector forwards the chord here since the iframe's keydown never
|
|
3556
|
+
// reaches the shell's window listener. Mirror that handler's toggle.
|
|
3557
|
+
setPaletteOpen((v) => !v);
|
|
3558
|
+
} else if (m.dgn === 'open-export') {
|
|
3559
|
+
// Plan C — the in-canvas toolbar / context menu route here so they open
|
|
3560
|
+
// the SAME shell Export dialog as the menubar (one look, all settings).
|
|
3561
|
+
// Carry the context-menu's scope hint (e.g. "Export selection").
|
|
3562
|
+
setExportDialog({
|
|
3563
|
+
mode: 'export',
|
|
3564
|
+
scope: m.detail && typeof m.detail.scope === 'string' ? m.detail.scope : undefined,
|
|
3565
|
+
});
|
|
2556
3566
|
} else if (m.dgn === 'loaded' && m.file) {
|
|
2557
3567
|
// iframe finished loading — push current comments + carry over focused pin if any
|
|
2558
3568
|
const list = commentsByFile[m.file] || [];
|
|
@@ -2709,6 +3719,12 @@ function App() {
|
|
|
2709
3719
|
// ⌘F) still fire regardless of focus, mirroring browser convention.
|
|
2710
3720
|
const inCanvasIframe = document.activeElement?.tagName === 'IFRAME';
|
|
2711
3721
|
|
|
3722
|
+
// Cmd+K / Ctrl+K — toggle the command palette (works even in inputs).
|
|
3723
|
+
if (meta && (e.key === 'k' || e.key === 'K')) {
|
|
3724
|
+
e.preventDefault();
|
|
3725
|
+
setPaletteOpen((v) => !v);
|
|
3726
|
+
return;
|
|
3727
|
+
}
|
|
2712
3728
|
// Cmd+R — reload active iframe (override browser reload)
|
|
2713
3729
|
if (meta && (e.key === 'r' || e.key === 'R')) {
|
|
2714
3730
|
e.preventDefault();
|
|
@@ -2745,7 +3761,7 @@ function App() {
|
|
|
2745
3761
|
// / — focus search (or ⌘F per CV-08 placeholder hint)
|
|
2746
3762
|
if (e.key === '/') {
|
|
2747
3763
|
e.preventDefault();
|
|
2748
|
-
const inp = document.querySelector('.
|
|
3764
|
+
const inp = document.querySelector('.st-search input');
|
|
2749
3765
|
if (inp) inp.focus();
|
|
2750
3766
|
return;
|
|
2751
3767
|
}
|
|
@@ -2753,7 +3769,7 @@ function App() {
|
|
|
2753
3769
|
e.preventDefault();
|
|
2754
3770
|
if (!sidebarOpen) setSidebarOpen(true);
|
|
2755
3771
|
setTimeout(() => {
|
|
2756
|
-
const inp = document.querySelector('.
|
|
3772
|
+
const inp = document.querySelector('.st-search input');
|
|
2757
3773
|
if (inp) inp.focus();
|
|
2758
3774
|
}, 0);
|
|
2759
3775
|
return;
|
|
@@ -2790,6 +3806,12 @@ function App() {
|
|
|
2790
3806
|
}
|
|
2791
3807
|
return;
|
|
2792
3808
|
}
|
|
3809
|
+
// I — toggle Inspector panel (T6, Plan C)
|
|
3810
|
+
if ((e.key === 'i' || e.key === 'I') && !meta && !e.shiftKey) {
|
|
3811
|
+
e.preventDefault();
|
|
3812
|
+
setInspectorOpen((v) => !v);
|
|
3813
|
+
return;
|
|
3814
|
+
}
|
|
2793
3815
|
// ? or F1 — open Help modal
|
|
2794
3816
|
if (e.key === '?' || e.key === 'F1') {
|
|
2795
3817
|
e.preventDefault();
|
|
@@ -2822,8 +3844,6 @@ function App() {
|
|
|
2822
3844
|
if (el) iframesRef.current.set(path, el);
|
|
2823
3845
|
}, []);
|
|
2824
3846
|
|
|
2825
|
-
const activeFileComments =
|
|
2826
|
-
activePath && activePath !== SYSTEM_TAB ? commentsByFile[activePath] || [] : [];
|
|
2827
3847
|
const totalOpen = totalCounts(commentsByFile).open;
|
|
2828
3848
|
|
|
2829
3849
|
// Suppress the native browser context menu across the shell — the canvas
|
|
@@ -2840,88 +3860,142 @@ function App() {
|
|
|
2840
3860
|
e.preventDefault();
|
|
2841
3861
|
}, []);
|
|
2842
3862
|
|
|
3863
|
+
// ⌘K palette actions — shell-doable only (in-canvas export lives in the iframe).
|
|
3864
|
+
// T4 (Plan C) — grouped command set per `.design/ui/Studio.tsx` AB-D.
|
|
3865
|
+
// `group` drives the section headers; the list stays a flat array so keyboard
|
|
3866
|
+
// nav indexes straight across groups.
|
|
3867
|
+
const paletteActions = useMemo(
|
|
3868
|
+
() => [
|
|
3869
|
+
// ── Canvas ──────────────────────────────────────────────────────────
|
|
3870
|
+
{
|
|
3871
|
+
id: 'new',
|
|
3872
|
+
group: 'Canvas',
|
|
3873
|
+
label: 'New canvas…',
|
|
3874
|
+
icon: 'plus',
|
|
3875
|
+
kbd: '⌘N',
|
|
3876
|
+
run: () => {
|
|
3877
|
+
setSidebarOpen(true);
|
|
3878
|
+
setTimeout(
|
|
3879
|
+
() => document.querySelector('[aria-label="New blank brief board"]')?.click(),
|
|
3880
|
+
60
|
|
3881
|
+
);
|
|
3882
|
+
},
|
|
3883
|
+
},
|
|
3884
|
+
{
|
|
3885
|
+
id: 'export',
|
|
3886
|
+
group: 'Canvas',
|
|
3887
|
+
label: 'Export…',
|
|
3888
|
+
icon: 'download',
|
|
3889
|
+
kbd: '⇧⌘E',
|
|
3890
|
+
run: () => setExportDialog({ mode: 'export' }),
|
|
3891
|
+
},
|
|
3892
|
+
{
|
|
3893
|
+
id: 'handoff',
|
|
3894
|
+
group: 'Canvas',
|
|
3895
|
+
label: 'Handoff to production',
|
|
3896
|
+
icon: 'share',
|
|
3897
|
+
kbd: '⇧⌘H',
|
|
3898
|
+
run: () => setExportDialog({ mode: 'handoff' }),
|
|
3899
|
+
},
|
|
3900
|
+
// ── View ────────────────────────────────────────────────────────────
|
|
3901
|
+
{
|
|
3902
|
+
id: 'system',
|
|
3903
|
+
group: 'View',
|
|
3904
|
+
label: 'Open design system view',
|
|
3905
|
+
icon: 'sliders',
|
|
3906
|
+
kbd: 'S',
|
|
3907
|
+
run: () => openSystem(),
|
|
3908
|
+
},
|
|
3909
|
+
{
|
|
3910
|
+
id: 'comments',
|
|
3911
|
+
group: 'View',
|
|
3912
|
+
label: 'Toggle comments panel',
|
|
3913
|
+
icon: 'resolve',
|
|
3914
|
+
kbd: '⌘⇧M',
|
|
3915
|
+
run: () => setCommentsPanelOpen((v) => !v),
|
|
3916
|
+
},
|
|
3917
|
+
{
|
|
3918
|
+
id: 'inspector',
|
|
3919
|
+
group: 'View',
|
|
3920
|
+
label: 'Open inspector',
|
|
3921
|
+
icon: 'sliders',
|
|
3922
|
+
kbd: 'I',
|
|
3923
|
+
run: () => setInspectorOpen(true),
|
|
3924
|
+
},
|
|
3925
|
+
{
|
|
3926
|
+
id: 'reload',
|
|
3927
|
+
group: 'View',
|
|
3928
|
+
label: 'Reload active canvas',
|
|
3929
|
+
icon: 'reload',
|
|
3930
|
+
kbd: '⌘R',
|
|
3931
|
+
run: () => reloadActive(),
|
|
3932
|
+
},
|
|
3933
|
+
// ── Tools ───────────────────────────────────────────────────────────
|
|
3934
|
+
{
|
|
3935
|
+
id: 'draw',
|
|
3936
|
+
group: 'Tools',
|
|
3937
|
+
label: 'Draw a mark with the SVG agent',
|
|
3938
|
+
icon: 'pen',
|
|
3939
|
+
run: () => {
|
|
3940
|
+
// The shell can't invoke Claude — surface the command for the user to
|
|
3941
|
+
// paste into Claude Code (clipboard is the honest, useful affordance).
|
|
3942
|
+
try {
|
|
3943
|
+
navigator.clipboard?.writeText('/design:draw ');
|
|
3944
|
+
} catch {}
|
|
3945
|
+
},
|
|
3946
|
+
},
|
|
3947
|
+
{
|
|
3948
|
+
id: 'theme',
|
|
3949
|
+
group: 'Tools',
|
|
3950
|
+
label: 'Toggle light / dark theme',
|
|
3951
|
+
icon: 'sun',
|
|
3952
|
+
run: () => toggleTheme(),
|
|
3953
|
+
},
|
|
3954
|
+
// ── Help ────────────────────────────────────────────────────────────
|
|
3955
|
+
{
|
|
3956
|
+
id: 'whatsnew',
|
|
3957
|
+
group: 'Help',
|
|
3958
|
+
label: "What's new in maude",
|
|
3959
|
+
icon: 'sparkle',
|
|
3960
|
+
run: () => whatsNew.openPanel(),
|
|
3961
|
+
},
|
|
3962
|
+
{
|
|
3963
|
+
id: 'help',
|
|
3964
|
+
group: 'Help',
|
|
3965
|
+
label: 'Help · shortcuts & commands',
|
|
3966
|
+
icon: 'help',
|
|
3967
|
+
kbd: '?',
|
|
3968
|
+
run: () => setHelpOpen(true),
|
|
3969
|
+
},
|
|
3970
|
+
],
|
|
3971
|
+
[openSystem, toggleTheme, reloadActive, whatsNew]
|
|
3972
|
+
);
|
|
3973
|
+
|
|
2843
3974
|
return (
|
|
2844
|
-
<div
|
|
2845
|
-
className={
|
|
2846
|
-
'app' + (commentsPanelOpen ? ' with-rsidebar' : '') + (sidebarOpen ? '' : ' no-sidebar')
|
|
2847
|
-
}
|
|
2848
|
-
onContextMenu={onShellContextMenu}
|
|
2849
|
-
>
|
|
3975
|
+
<div className="maude" data-theme={theme} onContextMenu={onShellContextMenu}>
|
|
2850
3976
|
<SyncBanner status={syncStatus} />
|
|
3977
|
+
{!usageNudge && !tourSteps && <WhatsNewToast wn={whatsNew} />}
|
|
2851
3978
|
{gitLifecycle && (
|
|
2852
|
-
<div
|
|
2853
|
-
|
|
2854
|
-
aria-live="polite"
|
|
2855
|
-
style={{
|
|
2856
|
-
position: 'fixed',
|
|
2857
|
-
top: 12,
|
|
2858
|
-
left: '50%',
|
|
2859
|
-
transform: 'translateX(-50%)',
|
|
2860
|
-
zIndex: 10002,
|
|
2861
|
-
display: 'flex',
|
|
2862
|
-
alignItems: 'center',
|
|
2863
|
-
gap: 12,
|
|
2864
|
-
padding: '8px 14px',
|
|
2865
|
-
background: '#dbeafe',
|
|
2866
|
-
color: '#1e40af',
|
|
2867
|
-
border: '1px solid #93c5fd',
|
|
2868
|
-
borderRadius: 999,
|
|
2869
|
-
boxShadow: '0 2px 8px rgba(0,0,0,0.12)',
|
|
2870
|
-
font: '500 13px/1.2 system-ui, -apple-system, sans-serif',
|
|
2871
|
-
}}
|
|
2872
|
-
>
|
|
3979
|
+
<div role="status" aria-live="polite" className="st-banner st-banner--info">
|
|
3980
|
+
<span className="st-banner-dot" aria-hidden="true" />
|
|
2873
3981
|
<span>Repo state changed — reload to sync?</span>
|
|
2874
3982
|
<button
|
|
2875
3983
|
type="button"
|
|
3984
|
+
className="btn btn--primary btn--sm"
|
|
2876
3985
|
onClick={() => {
|
|
2877
3986
|
try {
|
|
2878
3987
|
window.location.reload();
|
|
2879
3988
|
} catch {}
|
|
2880
3989
|
}}
|
|
2881
|
-
style={{
|
|
2882
|
-
padding: '3px 10px',
|
|
2883
|
-
background: '#2563eb',
|
|
2884
|
-
color: '#fff',
|
|
2885
|
-
border: 'none',
|
|
2886
|
-
borderRadius: 4,
|
|
2887
|
-
font: '500 11px/1.2 system-ui',
|
|
2888
|
-
cursor: 'pointer',
|
|
2889
|
-
}}
|
|
2890
3990
|
>
|
|
2891
3991
|
Reload
|
|
2892
3992
|
</button>
|
|
2893
|
-
<button
|
|
2894
|
-
type="button"
|
|
2895
|
-
onClick={() => setGitLifecycle(null)}
|
|
2896
|
-
style={{
|
|
2897
|
-
padding: '3px 10px',
|
|
2898
|
-
background: 'transparent',
|
|
2899
|
-
color: '#1e40af',
|
|
2900
|
-
border: '1px solid #93c5fd',
|
|
2901
|
-
borderRadius: 4,
|
|
2902
|
-
font: '500 11px/1.2 system-ui',
|
|
2903
|
-
cursor: 'pointer',
|
|
2904
|
-
}}
|
|
2905
|
-
>
|
|
3993
|
+
<button type="button" className="btn btn--ghost btn--sm" onClick={() => setGitLifecycle(null)}>
|
|
2906
3994
|
Dismiss
|
|
2907
3995
|
</button>
|
|
2908
3996
|
</div>
|
|
2909
3997
|
)}
|
|
2910
|
-
<
|
|
2911
|
-
groups={groups}
|
|
2912
|
-
activePath={activePath}
|
|
2913
|
-
activeDsName={activePath === SYSTEM_TAB ? (systemData?.ds?.name ?? null) : null}
|
|
2914
|
-
onOpen={openTab}
|
|
2915
|
-
onOpenSystem={openSystem}
|
|
2916
|
-
wsConnected={wsConnected}
|
|
2917
|
-
search={search}
|
|
2918
|
-
setSearch={setSearch}
|
|
2919
|
-
commentsByFile={commentsByFile}
|
|
2920
|
-
showHidden={showHidden}
|
|
2921
|
-
sectionsExpanded={sectionsExpanded}
|
|
2922
|
-
onToggleSection={toggleSection}
|
|
2923
|
-
/>
|
|
2924
|
-
<div className="main">
|
|
3998
|
+
<div className="st-shell">
|
|
2925
3999
|
<Menubar
|
|
2926
4000
|
activePath={activePath}
|
|
2927
4001
|
project={project}
|
|
@@ -2939,20 +4013,90 @@ function App() {
|
|
|
2939
4013
|
annotationsVisible={annotationsVisible}
|
|
2940
4014
|
onToggleAnnotations={toggleAnnotations}
|
|
2941
4015
|
postToActiveCanvas={postToActiveCanvas}
|
|
4016
|
+
onOpenWhatsNew={whatsNew.openPanel}
|
|
4017
|
+
whatsNewCount={whatsNew.unseen.length}
|
|
4018
|
+
artboardCount={activeArtboards}
|
|
4019
|
+
inspectorOpen={inspectorOpen}
|
|
4020
|
+
onToggleInspector={() => setInspectorOpen((v) => !v)}
|
|
4021
|
+
onNewCanvas={() => {
|
|
4022
|
+
setSidebarOpen(true);
|
|
4023
|
+
setTimeout(
|
|
4024
|
+
() => document.querySelector('[aria-label="New blank brief board"]')?.click(),
|
|
4025
|
+
60
|
|
4026
|
+
);
|
|
4027
|
+
}}
|
|
4028
|
+
onOpenExport={(mode) => setExportDialog({ mode })}
|
|
4029
|
+
onReload={reloadActive}
|
|
4030
|
+
presence={
|
|
4031
|
+
<>
|
|
4032
|
+
<StAvatar
|
|
4033
|
+
initials={initialsOf(gitUser || 'you')}
|
|
4034
|
+
hue="var(--accent)"
|
|
4035
|
+
title={gitUser ? `${gitUser} (you)` : 'You'}
|
|
4036
|
+
/>
|
|
4037
|
+
{agentActive && (
|
|
4038
|
+
<StAvatar initials="C" hue="var(--presence-agent)" title="Claude · editing" />
|
|
4039
|
+
)}
|
|
4040
|
+
</>
|
|
4041
|
+
}
|
|
2942
4042
|
/>
|
|
2943
|
-
<
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
4043
|
+
<div className="st-body">
|
|
4044
|
+
<CollapsedRail
|
|
4045
|
+
shown={!sidebarOpen}
|
|
4046
|
+
onExpand={() => setSidebarOpen(true)}
|
|
4047
|
+
onSearch={() => {
|
|
4048
|
+
setSidebarOpen(true);
|
|
4049
|
+
setTimeout(() => document.querySelector('.st-search input')?.focus(), 60);
|
|
4050
|
+
}}
|
|
4051
|
+
/>
|
|
4052
|
+
<Sidebar
|
|
4053
|
+
groups={groups}
|
|
4054
|
+
activePath={activePath}
|
|
4055
|
+
activeDsName={activePath === SYSTEM_TAB ? (systemData?.ds?.name ?? null) : null}
|
|
4056
|
+
onOpen={openTab}
|
|
4057
|
+
onOpenSystem={openSystem}
|
|
4058
|
+
wsConnected={wsConnected}
|
|
4059
|
+
search={search}
|
|
4060
|
+
setSearch={setSearch}
|
|
4061
|
+
commentsByFile={commentsByFile}
|
|
4062
|
+
showHidden={showHidden}
|
|
4063
|
+
sectionsExpanded={sectionsExpanded}
|
|
4064
|
+
onToggleSection={toggleSection}
|
|
4065
|
+
onNewBoard={createBoard}
|
|
4066
|
+
onDeleteBoard={deleteBoard}
|
|
4067
|
+
collapsed={!sidebarOpen}
|
|
4068
|
+
onCollapse={() => setSidebarOpen(false)}
|
|
4069
|
+
/>
|
|
4070
|
+
<div className="main">
|
|
4071
|
+
<Viewport
|
|
4072
|
+
tabs={tabs}
|
|
4073
|
+
activePath={activePath}
|
|
4074
|
+
registerIframe={registerIframe}
|
|
4075
|
+
systemData={systemData}
|
|
4076
|
+
onOpenFromSystem={openTab}
|
|
4077
|
+
onSelectDs={loadSystemData}
|
|
4078
|
+
project={project}
|
|
4079
|
+
cfg={cfg}
|
|
4080
|
+
/>
|
|
4081
|
+
</div>
|
|
4082
|
+
{/* Right dock — one panel at a time. Inspector takes precedence when
|
|
4083
|
+
open (T6); else the comments panel. */}
|
|
4084
|
+
{inspectorOpen ? (
|
|
4085
|
+
<InspectorPanel selected={selected} onClose={() => setInspectorOpen(false)} />
|
|
4086
|
+
) : commentsPanelOpen ? (
|
|
4087
|
+
<CommentsPanel
|
|
4088
|
+
commentsByFile={commentsByFile}
|
|
4089
|
+
filter={commentsFilter}
|
|
4090
|
+
setFilter={setCommentsFilter}
|
|
4091
|
+
activePath={activePath}
|
|
4092
|
+
focusedId={focusedCommentId}
|
|
4093
|
+
onJump={jumpToComment}
|
|
4094
|
+
onResolve={resolveComment}
|
|
4095
|
+
onReopen={reopenComment}
|
|
4096
|
+
onDelete={deleteComment}
|
|
4097
|
+
/>
|
|
4098
|
+
) : null}
|
|
4099
|
+
</div>
|
|
2956
4100
|
<StatusBar
|
|
2957
4101
|
activePath={activePath}
|
|
2958
4102
|
selected={selected}
|
|
@@ -2964,20 +4108,59 @@ function App() {
|
|
|
2964
4108
|
syncStatus={syncStatus}
|
|
2965
4109
|
/>
|
|
2966
4110
|
</div>
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
4111
|
+
<CommandPalette
|
|
4112
|
+
open={paletteOpen}
|
|
4113
|
+
onClose={() => setPaletteOpen(false)}
|
|
4114
|
+
actions={paletteActions}
|
|
4115
|
+
/>
|
|
4116
|
+
{exportDialog && (
|
|
4117
|
+
<ExportDialog
|
|
4118
|
+
mode={exportDialog.mode}
|
|
4119
|
+
initialScope={exportDialog.scope}
|
|
2972
4120
|
activePath={activePath}
|
|
2973
|
-
|
|
2974
|
-
onJump={jumpToComment}
|
|
2975
|
-
onResolve={resolveComment}
|
|
2976
|
-
onReopen={reopenComment}
|
|
2977
|
-
onDelete={deleteComment}
|
|
4121
|
+
onClose={() => setExportDialog(null)}
|
|
2978
4122
|
/>
|
|
2979
4123
|
)}
|
|
2980
|
-
<HelpModal
|
|
4124
|
+
<HelpModal
|
|
4125
|
+
open={helpOpen}
|
|
4126
|
+
onClose={() => setHelpOpen(false)}
|
|
4127
|
+
onStartTour={() => {
|
|
4128
|
+
setHelpOpen(false);
|
|
4129
|
+
startTour(USAGE_TOUR);
|
|
4130
|
+
}}
|
|
4131
|
+
/>
|
|
4132
|
+
<WhatsNewPanel wn={whatsNew} onStartTour={startTour} />
|
|
4133
|
+
{usageNudge && !tourSteps && (
|
|
4134
|
+
<div className="mdcc-tour-nudge" role="status" aria-live="polite">
|
|
4135
|
+
<div className="mdcc-tour-nudge__body">
|
|
4136
|
+
New here? Take a 60-second tour of the canvas browser.
|
|
4137
|
+
</div>
|
|
4138
|
+
<button
|
|
4139
|
+
type="button"
|
|
4140
|
+
className="mdcc-tour-nudge__cta"
|
|
4141
|
+
onClick={() => {
|
|
4142
|
+
markUsageSeen();
|
|
4143
|
+
startTour(USAGE_TOUR);
|
|
4144
|
+
}}
|
|
4145
|
+
>
|
|
4146
|
+
Start
|
|
4147
|
+
</button>
|
|
4148
|
+
<button
|
|
4149
|
+
type="button"
|
|
4150
|
+
className="mdcc-tour-nudge__skip"
|
|
4151
|
+
aria-label="Dismiss"
|
|
4152
|
+
onClick={markUsageSeen}
|
|
4153
|
+
>
|
|
4154
|
+
×
|
|
4155
|
+
</button>
|
|
4156
|
+
</div>
|
|
4157
|
+
)}
|
|
4158
|
+
<TourOverlay
|
|
4159
|
+
steps={tourSteps ?? []}
|
|
4160
|
+
open={!!tourSteps}
|
|
4161
|
+
onClose={() => setTourSteps(null)}
|
|
4162
|
+
onComplete={markUsageSeen}
|
|
4163
|
+
/>
|
|
2981
4164
|
</div>
|
|
2982
4165
|
);
|
|
2983
4166
|
}
|