@1agh/maude 0.28.1 → 0.30.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 +3 -3
- package/{plugins/design/dev-server → apps/studio}/ai-banner.tsx +1 -2
- package/apps/studio/annotations-align.ts +149 -0
- package/apps/studio/annotations-bindings.ts +197 -0
- package/{plugins/design/dev-server → apps/studio}/annotations-context-toolbar.tsx +681 -151
- package/apps/studio/annotations-groups.ts +270 -0
- package/apps/studio/annotations-layer.tsx +4537 -0
- package/apps/studio/annotations-model.ts +2077 -0
- package/apps/studio/annotations-snap.ts +125 -0
- package/{plugins/design/dev-server → apps/studio}/api.ts +616 -70
- 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/_png-playwright.mjs +1 -1
- package/apps/studio/bin/annotate.mjs +732 -0
- package/apps/studio/bin/annotate.sh +17 -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 +573 -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 +6 -4
- package/{plugins/design/dev-server → apps/studio}/canvas-arrowheads.ts +79 -10
- 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 +48 -12
- package/{plugins/design/dev-server → apps/studio}/canvas-edit.ts +257 -7
- package/{plugins/design/dev-server → apps/studio}/canvas-icons.tsx +165 -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 +360 -48
- package/apps/studio/client/app.jsx +6742 -0
- package/apps/studio/client/canvas-url.js +81 -0
- package/{plugins/design/dev-server → apps/studio}/client/comments-overlay.css +41 -27
- package/apps/studio/client/index.html +16 -0
- package/apps/studio/client/styles/1-tokens-maude.css +296 -0
- package/apps/studio/client/styles/3-shell-maude.css +1161 -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 +396 -34
- package/apps/studio/client/styles/5-maude-overrides.css +137 -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 +279 -0
- package/apps/studio/client/tour/usage-tour.js +48 -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 +2 -2
- 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 +2 -2
- package/{plugins/design/dev-server → apps/studio}/comments-overlay.tsx +32 -11
- package/{plugins/design/dev-server → apps/studio}/context-menu.tsx +5 -4
- package/{plugins/design/dev-server → apps/studio}/contextual-toolbar.tsx +18 -7
- package/{plugins/design/dev-server → apps/studio}/cursors-overlay.tsx +21 -21
- package/apps/studio/dist/client.bundle.js +25545 -0
- package/apps/studio/dist/comment-mount.js +2046 -0
- package/apps/studio/dist/styles.css +8018 -0
- package/apps/studio/dom-selection.ts +342 -0
- 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 +16 -4
- package/{plugins/design/dev-server → apps/studio}/history.ts +20 -3
- package/{plugins/design/dev-server → apps/studio}/http.ts +226 -6
- package/{plugins/design/dev-server → apps/studio}/input-router.tsx +15 -1
- package/{plugins/design/dev-server → apps/studio}/inspect.ts +50 -4
- package/{plugins/design/dev-server → apps/studio}/marquee-overlay.tsx +5 -2
- package/{plugins/design/dev-server → apps/studio}/participants-chrome.tsx +2 -8
- package/{plugins/design/dev-server → apps/studio}/paths.ts +15 -11
- package/{plugins/design/dev-server → apps/studio}/server.ts +23 -2
- package/{plugins/design/dev-server → apps/studio}/sync/agent.ts +231 -57
- package/{plugins/design/dev-server → apps/studio}/sync/codec.ts +45 -0
- package/apps/studio/sync/cold-start.ts +158 -0
- package/{plugins/design/dev-server → apps/studio}/sync/connection-state.ts +58 -2
- package/{plugins/design/dev-server → apps/studio}/sync/index.ts +563 -221
- package/apps/studio/sync/journal.ts +190 -0
- package/apps/studio/sync/migrate-seed.ts +321 -0
- package/{plugins/design/dev-server → apps/studio}/sync/projection.ts +22 -3
- package/{plugins/design/dev-server → apps/studio}/sync/status.ts +15 -4
- package/{plugins/design/dev-server → apps/studio}/test/activity.test.ts +1 -7
- package/apps/studio/test/annotate-write.test.ts +184 -0
- package/apps/studio/test/annotations-align.test.ts +88 -0
- package/apps/studio/test/annotations-bindings.test.ts +124 -0
- package/apps/studio/test/annotations-groups.test.ts +231 -0
- 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-snap.test.ts +79 -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/apps/studio/test/canvas-edit.test.ts +319 -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 +25 -1
- package/apps/studio/test/canvas-url.test.ts +99 -0
- package/{plugins/design/dev-server → apps/studio}/test/collab-bridge.test.ts +0 -2
- package/{plugins/design/dev-server → apps/studio}/test/collab-room.test.ts +2 -7
- package/{plugins/design/dev-server → apps/studio}/test/csp-canvas-shell.test.ts +5 -0
- package/apps/studio/test/csrf-write-guard.test.ts +52 -0
- package/apps/studio/test/figjam-v3-model.test.ts +342 -0
- package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +1 -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/shared-doc-migrate.test.ts +369 -0
- package/apps/studio/test/sync-agent.test.ts +575 -0
- package/{plugins/design/dev-server → apps/studio}/test/sync-codec.test.ts +65 -0
- package/apps/studio/test/sync-cold-start.test.ts +244 -0
- package/{plugins/design/dev-server → apps/studio}/test/sync-connection-state.test.ts +68 -0
- package/{plugins/design/dev-server → apps/studio}/test/sync-hardening.test.ts +0 -8
- package/{plugins/design/dev-server → apps/studio}/test/sync-hubs-config.test.ts +0 -1
- package/apps/studio/test/sync-incident-replay.test.ts +211 -0
- package/apps/studio/test/sync-journal.test.ts +176 -0
- package/{plugins/design/dev-server → apps/studio}/test/sync-runtime.test.ts +360 -5
- package/{plugins/design/dev-server → apps/studio}/test/sync-status.test.ts +66 -0
- package/apps/studio/test/tour-overlay.test.tsx +56 -0
- package/apps/studio/test/use-annotation-resize.test.ts +402 -0
- package/{plugins/design/dev-server → apps/studio}/test/use-artboard-drag.test.ts +0 -1
- package/{plugins/design/dev-server → apps/studio}/test/use-collab.test.ts +0 -0
- package/{plugins/design/dev-server → apps/studio}/test/use-tool-mode.test.tsx +8 -2
- 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 +15 -70
- package/{plugins/design/dev-server → apps/studio}/undo-hud.tsx +1 -2
- package/{plugins/design/dev-server → apps/studio}/undo-stack.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-agent-presence.tsx +5 -3
- package/apps/studio/use-annotation-resize.tsx +912 -0
- package/{plugins/design/dev-server → apps/studio}/use-annotation-selection.tsx +17 -5
- 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 +3 -3
- package/apps/studio/use-canvas-media-drop.tsx +343 -0
- package/{plugins/design/dev-server → apps/studio}/use-collab.tsx +21 -16
- 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 +18 -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 +8 -2
- package/{plugins/design/dev-server → apps/studio}/use-undo-stack.tsx +1 -1
- package/apps/studio/whats-new.json +264 -0
- package/apps/studio/whats-new.schema.json +86 -0
- package/apps/studio/whats-new.ts +87 -0
- package/cli/bin/maude.mjs +10 -0
- package/cli/commands/design-link.test.mjs +84 -0
- package/cli/commands/design.mjs +16 -7
- package/cli/commands/design.test.mjs +40 -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/design-link.mjs +51 -1
- 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/_shell.html +28 -4
- 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/annotations-layer.tsx +0 -3402
- package/plugins/design/dev-server/client/app.jsx +0 -2985
- package/plugins/design/dev-server/client/index.html +0 -15
- 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/dom-selection.ts +0 -156
- package/plugins/design/dev-server/sync/migrate-seed.ts +0 -163
- package/plugins/design/dev-server/test/canvas-edit.test.ts +0 -139
- package/plugins/design/dev-server/test/shared-doc-migrate.test.ts +0 -159
- package/plugins/design/dev-server/test/sync-agent.test.ts +0 -305
- package/plugins/design/dev-server/test/use-annotation-resize.test.ts +0 -200
- package/plugins/design/dev-server/use-annotation-resize.tsx +0 -449
- /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/_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-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}/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/atomic-write.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/origins.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/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-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-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-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/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-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-atomic-write.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-meta-codec.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-canvas-activity.test.tsx +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
|
@@ -1,2985 +0,0 @@
|
|
|
1
|
-
// Design plugin local browser — React UI.
|
|
2
|
-
// Bundled via Bun.build (DDR-009/012) — IIFE, tree-shaken, React 19 from npm.
|
|
3
|
-
// Renders: file tree, tabs, viewport (iframes), status bar, design-system view, comments.
|
|
4
|
-
// Universal — no project tokens needed; styling lives in client/styles/.
|
|
5
|
-
|
|
6
|
-
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
7
|
-
import { createRoot } from 'react-dom/client';
|
|
8
|
-
|
|
9
|
-
// Trusted tool→cursor resolver (shares the single TOOL_CURSORS source with the
|
|
10
|
-
// canvas runtime). canvas-cursors.ts is dependency-free (a type-only Tool
|
|
11
|
-
// import that Bun erases), so this pulls only string constants into the client
|
|
12
|
-
// bundle — no React, no input-router. See the tool-cursor handler below.
|
|
13
|
-
import { resolveToolCursor } from '../canvas-cursors.ts';
|
|
14
|
-
|
|
15
|
-
const SYSTEM_TAB = '__system__';
|
|
16
|
-
const THEME_STORE = 'mdcc-theme';
|
|
17
|
-
const SHOW_HIDDEN_STORE = 'mdcc-show-hidden';
|
|
18
|
-
const SECTIONS_STORE = 'mdcc-sections-expanded';
|
|
19
|
-
const SIDEBAR_STORE = 'mdcc-sidebar-open';
|
|
20
|
-
const CANVAS_EXT_RE = /\.(tsx|html?)$/i;
|
|
21
|
-
// Bun's `define` substitutes this at build time (see build.ts); falls back when
|
|
22
|
-
// the bundle is consumed in a context that hasn't run the build.
|
|
23
|
-
const MDCC_VERSION = typeof __MDCC_VERSION__ !== 'undefined' ? __MDCC_VERSION__ : 'dev';
|
|
24
|
-
|
|
25
|
-
function readInitialTheme() {
|
|
26
|
-
if (typeof window === 'undefined') return 'dark';
|
|
27
|
-
try {
|
|
28
|
-
const stored = localStorage.getItem(THEME_STORE);
|
|
29
|
-
if (stored === 'light' || stored === 'dark') return stored;
|
|
30
|
-
} catch {}
|
|
31
|
-
// Match the data-theme attribute index.html ships with (dark).
|
|
32
|
-
return 'dark';
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function readBoolStore(key, fallback) {
|
|
36
|
-
if (typeof window === 'undefined') return fallback;
|
|
37
|
-
try {
|
|
38
|
-
const v = localStorage.getItem(key);
|
|
39
|
-
if (v === '1') return true;
|
|
40
|
-
if (v === '0') return false;
|
|
41
|
-
} catch {}
|
|
42
|
-
return fallback;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function readJsonStore(key, fallback) {
|
|
46
|
-
if (typeof window === 'undefined') return fallback;
|
|
47
|
-
try {
|
|
48
|
-
const v = localStorage.getItem(key);
|
|
49
|
-
return v ? JSON.parse(v) : fallback;
|
|
50
|
-
} catch {
|
|
51
|
-
return fallback;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Section default-open: working sections (project + non-DS canvas groups)
|
|
56
|
-
// open; meta sections (DS + runtime) collapsed. Users can override per-section
|
|
57
|
-
// via the chevron; overrides persist in localStorage.
|
|
58
|
-
function sectionDefaultOpen(g) {
|
|
59
|
-
if (g.kind === 'runtime') return false;
|
|
60
|
-
if (g.label === 'Design system') return false;
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ---------- Utility ----------
|
|
65
|
-
|
|
66
|
-
function urlOf(p) {
|
|
67
|
-
return '/' + p.split('/').map(encodeURIComponent).join('/');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Iframe src for a canvas path. TSX canvases go through _canvas-shell.html so
|
|
71
|
-
// the bundled React 19 runtime + importmap can mount the default export. HTML
|
|
72
|
-
// canvases keep the legacy "serve the file with inspector + Babel injected"
|
|
73
|
-
// path. Phase 3.6 contract; the path argument is repo-root-relative
|
|
74
|
-
// (e.g. ".design/ui/Foo.tsx").
|
|
75
|
-
function canvasUrl(p, cfg, opts) {
|
|
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
|
-
}
|
|
133
|
-
|
|
134
|
-
function basename(p) {
|
|
135
|
-
return p.split('/').pop();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Strip canvas extensions for display. `Canvas Viewport.tsx` → `Canvas Viewport`.
|
|
139
|
-
// Sidecars (`.meta.json`, `.css`, `.registry.json`) keep their extensions so
|
|
140
|
-
// the file type stays unambiguous.
|
|
141
|
-
function displayName(name) {
|
|
142
|
-
return name.replace(CANVAS_EXT_RE, '');
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Primary base = name with the canvas extension stripped. `Canvas Viewport.tsx`
|
|
146
|
-
// → `Canvas Viewport`. A sidecar belongs to that primary when its name starts
|
|
147
|
-
// with `<base>.` — so `Canvas Viewport.meta.json` and `Canvas Viewport.css`
|
|
148
|
-
// both nest under `Canvas Viewport.tsx`. Naïve single-extension stripping
|
|
149
|
-
// breaks for multi-dot sidecars like `*.meta.json`.
|
|
150
|
-
function canvasBase(name) {
|
|
151
|
-
return name.replace(CANVAS_EXT_RE, '');
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Group flat file list into { primary: canvas, sidecars: [...] }. Sidecars
|
|
155
|
-
// share the primary base + `.` prefix and don't themselves match the canvas
|
|
156
|
-
// extension regex. Orphans (no canvas peer at this dir level) come back as
|
|
157
|
-
// `{ primary: orphan, sidecars: [], orphan: true }` so the caller can gate
|
|
158
|
-
// them on `showHidden`.
|
|
159
|
-
function groupBySidecar(files) {
|
|
160
|
-
// Pass 1 — claim primaries; prefer .tsx over .html on tie.
|
|
161
|
-
const primaryByBase = new Map();
|
|
162
|
-
for (const f of files) {
|
|
163
|
-
if (!CANVAS_EXT_RE.test(f.name)) continue;
|
|
164
|
-
const base = canvasBase(f.name);
|
|
165
|
-
if (!primaryByBase.has(base) || /\.tsx$/i.test(f.name)) primaryByBase.set(base, f);
|
|
166
|
-
}
|
|
167
|
-
// Pass 2 — match non-canvas files to the longest primary base they prefix.
|
|
168
|
-
const sidecarsByBase = new Map();
|
|
169
|
-
const orphans = [];
|
|
170
|
-
for (const f of files) {
|
|
171
|
-
if (CANVAS_EXT_RE.test(f.name)) continue;
|
|
172
|
-
let matched = null;
|
|
173
|
-
for (const base of primaryByBase.keys()) {
|
|
174
|
-
if (f.name === base) continue;
|
|
175
|
-
if (f.name.startsWith(`${base}.`)) {
|
|
176
|
-
if (!matched || base.length > matched.length) matched = base;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
if (matched) {
|
|
180
|
-
const list = sidecarsByBase.get(matched) || [];
|
|
181
|
-
list.push(f);
|
|
182
|
-
sidecarsByBase.set(matched, list);
|
|
183
|
-
} else {
|
|
184
|
-
orphans.push(f);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
const canvases = [];
|
|
188
|
-
for (const [base, primary] of primaryByBase) {
|
|
189
|
-
const sidecars = (sidecarsByBase.get(base) || []).sort((a, b) => a.name.localeCompare(b.name));
|
|
190
|
-
canvases.push({ primary, sidecars, orphan: false });
|
|
191
|
-
}
|
|
192
|
-
canvases.sort((a, b) => a.primary.name.localeCompare(b.primary.name));
|
|
193
|
-
orphans.sort((a, b) => a.name.localeCompare(b.name));
|
|
194
|
-
return {
|
|
195
|
-
canvases,
|
|
196
|
-
orphans: orphans.map((f) => ({ primary: f, sidecars: [], orphan: true })),
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function buildTree(paths, stripPrefix) {
|
|
201
|
-
const root = {};
|
|
202
|
-
for (const p of paths) {
|
|
203
|
-
const stripped = p.startsWith(stripPrefix)
|
|
204
|
-
? p.slice(stripPrefix.length).replace(/^\/+/, '')
|
|
205
|
-
: p;
|
|
206
|
-
const parts = stripped.split('/');
|
|
207
|
-
let node = root;
|
|
208
|
-
for (let i = 0; i < parts.length; i++) {
|
|
209
|
-
const key = parts[i];
|
|
210
|
-
const isFile = i === parts.length - 1;
|
|
211
|
-
if (isFile) {
|
|
212
|
-
node._files = node._files || [];
|
|
213
|
-
node._files.push({ name: key, path: p });
|
|
214
|
-
} else {
|
|
215
|
-
node[key] = node[key] || {};
|
|
216
|
-
node = node[key];
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
return root;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function filterTree(node, query) {
|
|
224
|
-
if (!query) return node;
|
|
225
|
-
const q = query.toLowerCase();
|
|
226
|
-
const out = {};
|
|
227
|
-
let any = false;
|
|
228
|
-
const dirs = Object.keys(node).filter((k) => k !== '_files');
|
|
229
|
-
for (const d of dirs) {
|
|
230
|
-
const filtered = filterTree(node[d], query);
|
|
231
|
-
if (filtered) {
|
|
232
|
-
out[d] = filtered;
|
|
233
|
-
any = true;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
if (node._files) {
|
|
237
|
-
const files = node._files.filter(
|
|
238
|
-
(f) => f.name.toLowerCase().includes(q) || f.path.toLowerCase().includes(q)
|
|
239
|
-
);
|
|
240
|
-
if (files.length) {
|
|
241
|
-
out._files = files;
|
|
242
|
-
any = true;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
return any ? out : null;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function openCount(comments) {
|
|
249
|
-
return (comments || []).filter((c) => c.status !== 'resolved').length;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function timeAgo(iso) {
|
|
253
|
-
if (!iso) return '';
|
|
254
|
-
const t = new Date(iso).getTime();
|
|
255
|
-
if (!t) return '';
|
|
256
|
-
const s = Math.max(0, Math.floor((Date.now() - t) / 1000));
|
|
257
|
-
if (s < 60) return s + 's';
|
|
258
|
-
const m = Math.floor(s / 60);
|
|
259
|
-
if (m < 60) return m + 'm';
|
|
260
|
-
const h = Math.floor(m / 60);
|
|
261
|
-
if (h < 24) return h + 'h';
|
|
262
|
-
const d = Math.floor(h / 24);
|
|
263
|
-
if (d < 7) return d + 'd';
|
|
264
|
-
return new Date(iso).toLocaleDateString();
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function totalCounts(commentsByFile) {
|
|
268
|
-
let all = 0,
|
|
269
|
-
open = 0,
|
|
270
|
-
resolved = 0;
|
|
271
|
-
for (const list of Object.values(commentsByFile || {})) {
|
|
272
|
-
for (const c of list || []) {
|
|
273
|
-
all++;
|
|
274
|
-
if (c.status === 'resolved') resolved++;
|
|
275
|
-
else open++;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
return { all, open, resolved };
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// ---------- Components ----------
|
|
282
|
-
|
|
283
|
-
function Icon({ d, size = 14, color }) {
|
|
284
|
-
return (
|
|
285
|
-
<svg
|
|
286
|
-
width={size}
|
|
287
|
-
height={size}
|
|
288
|
-
viewBox="0 0 24 24"
|
|
289
|
-
fill="none"
|
|
290
|
-
stroke={color || 'currentColor'}
|
|
291
|
-
strokeWidth="1.5"
|
|
292
|
-
strokeLinecap="round"
|
|
293
|
-
strokeLinejoin="round"
|
|
294
|
-
style={{ flex: 'none' }}
|
|
295
|
-
>
|
|
296
|
-
<path d={d} />
|
|
297
|
-
</svg>
|
|
298
|
-
);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// ───── Tree (CV-08 spec) ─────
|
|
302
|
-
// File rows use `.tp-row` with optional .dir / .sel / .star / .modified
|
|
303
|
-
// modifiers + a leading `.glyph` (▾ open dir, ▸ closed dir / selected file,
|
|
304
|
-
// · file). Section headers use `.tp-section-hd` with a `.pill` counter.
|
|
305
|
-
// The flat-row model (vs the old nested <details>) mirrors the mock and
|
|
306
|
-
// keeps padding-left under explicit control per depth level.
|
|
307
|
-
|
|
308
|
-
const TREE_INDENT_BASE = 12;
|
|
309
|
-
const TREE_INDENT_STEP = 16;
|
|
310
|
-
|
|
311
|
-
function DirRow({ name, depth, defaultOpen, children }) {
|
|
312
|
-
const [open, setOpen] = useState(defaultOpen);
|
|
313
|
-
return (
|
|
314
|
-
<Fragment>
|
|
315
|
-
<button
|
|
316
|
-
type="button"
|
|
317
|
-
role="treeitem"
|
|
318
|
-
aria-expanded={open}
|
|
319
|
-
tabIndex={-1}
|
|
320
|
-
className="tp-row dir"
|
|
321
|
-
style={{ paddingLeft: TREE_INDENT_BASE + depth * TREE_INDENT_STEP + 'px' }}
|
|
322
|
-
onClick={() => setOpen((v) => !v)}
|
|
323
|
-
>
|
|
324
|
-
<span className="glyph" aria-hidden="true">
|
|
325
|
-
{open ? '▾' : '▸'}
|
|
326
|
-
</span>
|
|
327
|
-
<span className="name">{name}</span>
|
|
328
|
-
</button>
|
|
329
|
-
{open && children}
|
|
330
|
-
</Fragment>
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// DsFolderRow — a per-DS folder inside the DESIGN SYSTEM section.
|
|
335
|
-
// Split target: chevron toggles disclosure of the folder's contents; clicking
|
|
336
|
-
// the folder name opens the SystemView focused on that DS (single SystemView
|
|
337
|
-
// for now; the dsName is plumbed through so a future per-DS view can use it).
|
|
338
|
-
function DsFolderRow({ name, dsName, depth, defaultOpen, active, onOpenSystem, children }) {
|
|
339
|
-
const [open, setOpen] = useState(defaultOpen);
|
|
340
|
-
return (
|
|
341
|
-
<Fragment>
|
|
342
|
-
<div
|
|
343
|
-
className={'tp-row ds-folder' + (active ? ' sel' : '')}
|
|
344
|
-
style={{ paddingLeft: TREE_INDENT_BASE + depth * TREE_INDENT_STEP + 'px' }}
|
|
345
|
-
role="treeitem"
|
|
346
|
-
aria-expanded={open}
|
|
347
|
-
>
|
|
348
|
-
<button
|
|
349
|
-
type="button"
|
|
350
|
-
className="ds-folder-chev"
|
|
351
|
-
onClick={() => setOpen((v) => !v)}
|
|
352
|
-
aria-label={open ? 'Collapse design system' : 'Expand design system'}
|
|
353
|
-
title={open ? 'Collapse' : 'Expand'}
|
|
354
|
-
>
|
|
355
|
-
<span className="glyph" aria-hidden="true">
|
|
356
|
-
{open ? '▾' : '▸'}
|
|
357
|
-
</span>
|
|
358
|
-
</button>
|
|
359
|
-
<button
|
|
360
|
-
type="button"
|
|
361
|
-
className="ds-folder-open"
|
|
362
|
-
onClick={() => onOpenSystem(dsName)}
|
|
363
|
-
aria-label={`Open ${dsName} design system view`}
|
|
364
|
-
title="Open the design system view"
|
|
365
|
-
>
|
|
366
|
-
<span className="name">{name}</span>
|
|
367
|
-
</button>
|
|
368
|
-
</div>
|
|
369
|
-
{open && children}
|
|
370
|
-
</Fragment>
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function FileRow({ file, activePath, onOpen, openCount: oc, depth, kind, sidecar }) {
|
|
375
|
-
const isSel = file.path === activePath;
|
|
376
|
-
const isCanvas = CANVAS_EXT_RE.test(file.name);
|
|
377
|
-
// Non-canvas rows (PROJECT *.md, RUNTIME _active.json, ...) are display-only —
|
|
378
|
-
// clicking them doesn't open an iframe; we leave the click as no-op + cursor
|
|
379
|
-
// hint via `aria-disabled`.
|
|
380
|
-
const inert = !isCanvas;
|
|
381
|
-
const label = isCanvas ? displayName(file.name) : file.name;
|
|
382
|
-
return (
|
|
383
|
-
<button
|
|
384
|
-
type="button"
|
|
385
|
-
role="treeitem"
|
|
386
|
-
aria-selected={isSel}
|
|
387
|
-
aria-disabled={inert ? 'true' : undefined}
|
|
388
|
-
tabIndex={isSel ? 0 : -1}
|
|
389
|
-
className={
|
|
390
|
-
'tp-row' +
|
|
391
|
-
(isSel ? ' sel' : '') +
|
|
392
|
-
(kind === 'runtime' ? ' muted' : '') +
|
|
393
|
-
(sidecar ? ' sidecar' : '')
|
|
394
|
-
}
|
|
395
|
-
style={{ paddingLeft: TREE_INDENT_BASE + depth * TREE_INDENT_STEP + 'px' }}
|
|
396
|
-
title={file.path + (oc ? ` — ${oc} open` : inert ? ' (file index only)' : '')}
|
|
397
|
-
onClick={() => {
|
|
398
|
-
if (!inert) onOpen(file.path);
|
|
399
|
-
}}
|
|
400
|
-
>
|
|
401
|
-
<span className="glyph" aria-hidden="true">
|
|
402
|
-
{isSel ? '▸' : '·'}
|
|
403
|
-
</span>
|
|
404
|
-
<span className="name">{label}</span>
|
|
405
|
-
{oc > 0 && <span className="badge">{oc}</span>}
|
|
406
|
-
</button>
|
|
407
|
-
);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function CanvasRow({
|
|
411
|
-
primary,
|
|
412
|
-
sidecars,
|
|
413
|
-
depth,
|
|
414
|
-
kind,
|
|
415
|
-
activePath,
|
|
416
|
-
onOpen,
|
|
417
|
-
openCount: oc,
|
|
418
|
-
showHidden,
|
|
419
|
-
forceOpen,
|
|
420
|
-
}) {
|
|
421
|
-
const hasSidecars = sidecars.length > 0;
|
|
422
|
-
const [openState, setOpenState] = useState(false);
|
|
423
|
-
// Sidecars are only revealed when the user opts in via `showHidden` — the
|
|
424
|
-
// chevron itself only appears in that mode. When `forceOpen` is true (search
|
|
425
|
-
// match in a sidecar), override local state so the user sees the hit.
|
|
426
|
-
const open = forceOpen || openState;
|
|
427
|
-
const isSel = primary.path === activePath;
|
|
428
|
-
const showChevron = hasSidecars && showHidden;
|
|
429
|
-
if (!showChevron) {
|
|
430
|
-
return (
|
|
431
|
-
<FileRow
|
|
432
|
-
file={primary}
|
|
433
|
-
activePath={activePath}
|
|
434
|
-
onOpen={onOpen}
|
|
435
|
-
openCount={oc}
|
|
436
|
-
depth={depth}
|
|
437
|
-
kind={kind}
|
|
438
|
-
/>
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
|
-
return (
|
|
442
|
-
<Fragment>
|
|
443
|
-
<button
|
|
444
|
-
type="button"
|
|
445
|
-
role="treeitem"
|
|
446
|
-
aria-selected={isSel}
|
|
447
|
-
aria-expanded={open}
|
|
448
|
-
tabIndex={isSel ? 0 : -1}
|
|
449
|
-
className={'tp-row canvas-row' + (isSel ? ' sel' : '')}
|
|
450
|
-
style={{ paddingLeft: TREE_INDENT_BASE + depth * TREE_INDENT_STEP + 'px' }}
|
|
451
|
-
title={primary.path}
|
|
452
|
-
onClick={(e) => {
|
|
453
|
-
// Click the chevron region → toggle disclosure. Click anywhere else → open canvas.
|
|
454
|
-
if (e.target.closest('.canvas-chev')) {
|
|
455
|
-
setOpenState((v) => !v);
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
onOpen(primary.path);
|
|
459
|
-
}}
|
|
460
|
-
>
|
|
461
|
-
<span
|
|
462
|
-
className="glyph canvas-chev"
|
|
463
|
-
aria-hidden="true"
|
|
464
|
-
onClick={(e) => {
|
|
465
|
-
e.stopPropagation();
|
|
466
|
-
setOpenState((v) => !v);
|
|
467
|
-
}}
|
|
468
|
-
>
|
|
469
|
-
{open ? '▾' : '▸'}
|
|
470
|
-
</span>
|
|
471
|
-
<span className="name">{displayName(primary.name)}</span>
|
|
472
|
-
{oc > 0 && <span className="badge">{oc}</span>}
|
|
473
|
-
</button>
|
|
474
|
-
{open &&
|
|
475
|
-
sidecars.map((sc) => (
|
|
476
|
-
<FileRow
|
|
477
|
-
key={sc.path}
|
|
478
|
-
file={sc}
|
|
479
|
-
activePath={activePath}
|
|
480
|
-
onOpen={onOpen}
|
|
481
|
-
openCount={0}
|
|
482
|
-
depth={depth + 1}
|
|
483
|
-
kind={kind}
|
|
484
|
-
sidecar
|
|
485
|
-
/>
|
|
486
|
-
))}
|
|
487
|
-
</Fragment>
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
function Tree({
|
|
492
|
-
node,
|
|
493
|
-
activePath,
|
|
494
|
-
onOpen,
|
|
495
|
-
commentsByFile,
|
|
496
|
-
depth = 1,
|
|
497
|
-
kind,
|
|
498
|
-
showHidden,
|
|
499
|
-
search,
|
|
500
|
-
dsFolders,
|
|
501
|
-
activeDsName,
|
|
502
|
-
onOpenSystem,
|
|
503
|
-
}) {
|
|
504
|
-
const dirs = Object.keys(node)
|
|
505
|
-
.filter((k) => k !== '_files')
|
|
506
|
-
.sort();
|
|
507
|
-
const files = node._files || [];
|
|
508
|
-
// VS Code-style sidecar grouping. Canvas (`.tsx`/`.html`) becomes the primary
|
|
509
|
-
// row; same-basename non-canvas files (`.meta.json`, `.css`, …) collapse
|
|
510
|
-
// under it. Orphans surface only when `showHidden` is on.
|
|
511
|
-
const { canvases, orphans } = useMemo(() => groupBySidecar(files), [files]);
|
|
512
|
-
const hasSearch = !!(search && search.trim());
|
|
513
|
-
// DS-folder lookup: only meaningful at the top level of a DS group. The
|
|
514
|
-
// server emits `dsFolders: [{name, folder}, ...]` so the client knows which
|
|
515
|
-
// dir at depth=1 corresponds to a DS root (click → open SystemView).
|
|
516
|
-
const dsFolderByName = useMemo(() => {
|
|
517
|
-
if (!dsFolders || depth !== 1) return null;
|
|
518
|
-
const m = new Map();
|
|
519
|
-
for (const f of dsFolders) m.set(f.folder, f);
|
|
520
|
-
return m;
|
|
521
|
-
}, [dsFolders, depth]);
|
|
522
|
-
return (
|
|
523
|
-
<Fragment>
|
|
524
|
-
{canvases.map((entry) => {
|
|
525
|
-
const forceOpen =
|
|
526
|
-
hasSearch &&
|
|
527
|
-
entry.sidecars.some((sc) => {
|
|
528
|
-
const q = search.toLowerCase();
|
|
529
|
-
return sc.name.toLowerCase().includes(q) || sc.path.toLowerCase().includes(q);
|
|
530
|
-
});
|
|
531
|
-
return (
|
|
532
|
-
<CanvasRow
|
|
533
|
-
key={entry.primary.path}
|
|
534
|
-
primary={entry.primary}
|
|
535
|
-
sidecars={entry.sidecars}
|
|
536
|
-
activePath={activePath}
|
|
537
|
-
onOpen={onOpen}
|
|
538
|
-
openCount={openCount(commentsByFile[entry.primary.path])}
|
|
539
|
-
depth={depth}
|
|
540
|
-
kind={kind}
|
|
541
|
-
showHidden={showHidden}
|
|
542
|
-
forceOpen={forceOpen}
|
|
543
|
-
/>
|
|
544
|
-
);
|
|
545
|
-
})}
|
|
546
|
-
{showHidden &&
|
|
547
|
-
orphans.map((entry) => (
|
|
548
|
-
<FileRow
|
|
549
|
-
key={entry.primary.path}
|
|
550
|
-
file={entry.primary}
|
|
551
|
-
activePath={activePath}
|
|
552
|
-
onOpen={onOpen}
|
|
553
|
-
openCount={openCount(commentsByFile[entry.primary.path])}
|
|
554
|
-
depth={depth}
|
|
555
|
-
kind={kind}
|
|
556
|
-
/>
|
|
557
|
-
))}
|
|
558
|
-
{dirs.map((d) => {
|
|
559
|
-
const dsMatch = dsFolderByName?.get(d);
|
|
560
|
-
const childTree = (
|
|
561
|
-
<Tree
|
|
562
|
-
node={node[d]}
|
|
563
|
-
activePath={activePath}
|
|
564
|
-
onOpen={onOpen}
|
|
565
|
-
commentsByFile={commentsByFile}
|
|
566
|
-
depth={depth + 1}
|
|
567
|
-
kind={kind}
|
|
568
|
-
showHidden={showHidden}
|
|
569
|
-
search={search}
|
|
570
|
-
activeDsName={activeDsName}
|
|
571
|
-
onOpenSystem={onOpenSystem}
|
|
572
|
-
/>
|
|
573
|
-
);
|
|
574
|
-
if (dsMatch && onOpenSystem) {
|
|
575
|
-
return (
|
|
576
|
-
<DsFolderRow
|
|
577
|
-
key={d}
|
|
578
|
-
name={d}
|
|
579
|
-
dsName={dsMatch.name}
|
|
580
|
-
depth={depth}
|
|
581
|
-
defaultOpen={true}
|
|
582
|
-
active={activePath === SYSTEM_TAB && dsMatch.name === activeDsName}
|
|
583
|
-
onOpenSystem={onOpenSystem}
|
|
584
|
-
>
|
|
585
|
-
{childTree}
|
|
586
|
-
</DsFolderRow>
|
|
587
|
-
);
|
|
588
|
-
}
|
|
589
|
-
return (
|
|
590
|
-
<DirRow key={d} name={d} depth={depth} defaultOpen={true}>
|
|
591
|
-
{childTree}
|
|
592
|
-
</DirRow>
|
|
593
|
-
);
|
|
594
|
-
})}
|
|
595
|
-
</Fragment>
|
|
596
|
-
);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// CV-08 section labels — title + optional SKU pill. The pill carries
|
|
600
|
-
// project / DS identity; the mock keeps these tight (1 line). Labels are
|
|
601
|
-
// keyed by the server-provided `kind` (PROJECT / DS / UI / RUNTIME).
|
|
602
|
-
const SECTION_META = {
|
|
603
|
-
project: { title: 'PROJECT', pillFromCount: false },
|
|
604
|
-
// Design-system group: pill shows the number of DSes (one row per DS folder
|
|
605
|
-
// inside). Computed in Sidebar from `g.dsFolders.length`.
|
|
606
|
-
ds: { title: 'DESIGN SYSTEM', pillFromDsCount: true },
|
|
607
|
-
canvas: { title: 'UI CANVASES', pillFromCount: true },
|
|
608
|
-
runtime: { title: 'RUNTIME · GITIGNORED', pillFromCount: true },
|
|
609
|
-
};
|
|
610
|
-
|
|
611
|
-
function sectionMetaFor(g) {
|
|
612
|
-
if (g.kind === 'project') return SECTION_META.project;
|
|
613
|
-
if (g.kind === 'runtime') return SECTION_META.runtime;
|
|
614
|
-
// canvas-kind groups: "Design system" → ds, anything else → canvas label
|
|
615
|
-
if (g.label === 'Design system') return SECTION_META.ds;
|
|
616
|
-
if (g.label === 'UI kit') return SECTION_META.canvas;
|
|
617
|
-
return { title: g.label.toUpperCase(), pillFromCount: true };
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
function Sidebar({
|
|
621
|
-
groups,
|
|
622
|
-
activePath,
|
|
623
|
-
activeDsName,
|
|
624
|
-
onOpen,
|
|
625
|
-
onOpenSystem,
|
|
626
|
-
wsConnected,
|
|
627
|
-
search,
|
|
628
|
-
setSearch,
|
|
629
|
-
commentsByFile,
|
|
630
|
-
showHidden,
|
|
631
|
-
sectionsExpanded,
|
|
632
|
-
onToggleSection,
|
|
633
|
-
}) {
|
|
634
|
-
const filteredGroups = useMemo(() => {
|
|
635
|
-
if (!search) return groups;
|
|
636
|
-
return groups.map((g) => ({ ...g, tree: filterTree(g.tree, search), filtered: !!search }));
|
|
637
|
-
}, [groups, search]);
|
|
638
|
-
|
|
639
|
-
// Mock uses `42 / 42` — total openable canvases, not every listed file.
|
|
640
|
-
// We count canvas files (TSX Phase 3.6+ default, HTML legacy) so the counter
|
|
641
|
-
// matches "canvases you can mount".
|
|
642
|
-
const htmlCount = useMemo(() => {
|
|
643
|
-
let total = 0;
|
|
644
|
-
for (const g of groups) for (const p of g.paths || []) if (CANVAS_EXT_RE.test(p)) total++;
|
|
645
|
-
return total;
|
|
646
|
-
}, [groups]);
|
|
647
|
-
const htmlShown = useMemo(() => {
|
|
648
|
-
let total = 0;
|
|
649
|
-
for (const g of filteredGroups)
|
|
650
|
-
for (const p of g.paths || []) if (CANVAS_EXT_RE.test(p)) total++;
|
|
651
|
-
return total;
|
|
652
|
-
}, [filteredGroups]);
|
|
653
|
-
|
|
654
|
-
return (
|
|
655
|
-
<nav className="sidebar">
|
|
656
|
-
<div className="tree-panel-hd">
|
|
657
|
-
<span>FILES</span>
|
|
658
|
-
<span className="ct" title={wsConnected ? 'live · file index synced' : 'reconnecting…'}>
|
|
659
|
-
<span className={'live-dot' + (wsConnected ? ' connected' : '')} aria-hidden="true" />
|
|
660
|
-
{htmlShown} / {htmlCount}
|
|
661
|
-
</span>
|
|
662
|
-
</div>
|
|
663
|
-
|
|
664
|
-
<div className="tree-panel-search">
|
|
665
|
-
<Icon d="M21 21l-4.35-4.35 M11 19a8 8 0 100-16 8 8 0 000 16z" size={12} />
|
|
666
|
-
<input
|
|
667
|
-
type="search"
|
|
668
|
-
placeholder="filter (⌘F)"
|
|
669
|
-
value={search}
|
|
670
|
-
onChange={(e) => setSearch(e.target.value)}
|
|
671
|
-
aria-label="Filter files"
|
|
672
|
-
/>
|
|
673
|
-
{search ? (
|
|
674
|
-
<button
|
|
675
|
-
className="search-clear"
|
|
676
|
-
onClick={() => setSearch('')}
|
|
677
|
-
title="Clear (Esc)"
|
|
678
|
-
aria-label="Clear search"
|
|
679
|
-
>
|
|
680
|
-
×
|
|
681
|
-
</button>
|
|
682
|
-
) : (
|
|
683
|
-
<span className="search-kbd" aria-hidden="true">
|
|
684
|
-
/
|
|
685
|
-
</span>
|
|
686
|
-
)}
|
|
687
|
-
</div>
|
|
688
|
-
|
|
689
|
-
<div className="tree-panel-body" role="tree" aria-label="Project file tree">
|
|
690
|
-
{filteredGroups.map((g) => {
|
|
691
|
-
// Hide gitignored runtime / orphan-only project sections by default.
|
|
692
|
-
// Active search overrides — if the user typed a query, they want hits
|
|
693
|
-
// wherever they live.
|
|
694
|
-
if (!showHidden && !search && g.kind === 'runtime') return null;
|
|
695
|
-
const meta = sectionMetaFor(g);
|
|
696
|
-
// Counter pill counts canvases only — sidecars + orphans inflate the
|
|
697
|
-
// raw `paths.length` and the FILES header already filters this way.
|
|
698
|
-
const canvasCount = (g.paths || []).filter((p) => CANVAS_EXT_RE.test(p)).length;
|
|
699
|
-
const pill =
|
|
700
|
-
meta.pill ||
|
|
701
|
-
(meta.pillFromDsCount ? String(g.dsFolders?.length || 0) : null) ||
|
|
702
|
-
(meta.pillFromCount ? String(canvasCount || g.paths?.length || 0) : null);
|
|
703
|
-
const hasItems = g.tree && Object.keys(g.tree).length > 0;
|
|
704
|
-
const isDs = g.label === 'Design system';
|
|
705
|
-
const isProject = g.kind === 'project';
|
|
706
|
-
// Project section: when showHidden is off, every row inside is an
|
|
707
|
-
// orphan (.md / .json / .css) → empty body. Skip the header in that
|
|
708
|
-
// case so the sidebar doesn't show "PROJECT" with nothing under it.
|
|
709
|
-
if (!showHidden && !search && isProject && canvasCount === 0) return null;
|
|
710
|
-
const defaultOpen = sectionDefaultOpen(g);
|
|
711
|
-
const explicit = sectionsExpanded[g.label];
|
|
712
|
-
// Active search forces every section open so hits aren't hidden.
|
|
713
|
-
const sectionOpen = !!search || (explicit === undefined ? defaultOpen : explicit);
|
|
714
|
-
const chev = sectionOpen ? '▾' : '▸';
|
|
715
|
-
return (
|
|
716
|
-
<Fragment key={g.label}>
|
|
717
|
-
<button
|
|
718
|
-
type="button"
|
|
719
|
-
className="tp-section-hd clickable section-toggle"
|
|
720
|
-
onClick={() => onToggleSection(g.label, defaultOpen)}
|
|
721
|
-
aria-expanded={sectionOpen}
|
|
722
|
-
title={sectionOpen ? 'Collapse section' : 'Expand section'}
|
|
723
|
-
>
|
|
724
|
-
<span className="chev" aria-hidden="true">
|
|
725
|
-
{chev}
|
|
726
|
-
</span>
|
|
727
|
-
<span className="section-label">{meta.title}</span>
|
|
728
|
-
{pill && <span className="pill">{pill}</span>}
|
|
729
|
-
</button>
|
|
730
|
-
{sectionOpen &&
|
|
731
|
-
(hasItems ? (
|
|
732
|
-
<Tree
|
|
733
|
-
node={g.tree}
|
|
734
|
-
activePath={activePath}
|
|
735
|
-
onOpen={onOpen}
|
|
736
|
-
commentsByFile={commentsByFile}
|
|
737
|
-
depth={1}
|
|
738
|
-
kind={g.kind}
|
|
739
|
-
showHidden={showHidden}
|
|
740
|
-
search={search}
|
|
741
|
-
dsFolders={g.dsFolders}
|
|
742
|
-
activeDsName={activeDsName}
|
|
743
|
-
onOpenSystem={isDs ? onOpenSystem : undefined}
|
|
744
|
-
/>
|
|
745
|
-
) : (
|
|
746
|
-
<div className="tp-empty">{search ? 'No matches.' : 'Empty.'}</div>
|
|
747
|
-
))}
|
|
748
|
-
</Fragment>
|
|
749
|
-
);
|
|
750
|
-
})}
|
|
751
|
-
</div>
|
|
752
|
-
</nav>
|
|
753
|
-
);
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// Help modal — hosts the cheatsheet that used to live in the left sidebar.
|
|
757
|
-
// Triggered from the menubar's Help item. Esc + backdrop click close it.
|
|
758
|
-
function HelpModal({ open, onClose }) {
|
|
759
|
-
useEffect(() => {
|
|
760
|
-
if (!open) return;
|
|
761
|
-
function onKey(e) {
|
|
762
|
-
if (e.key === 'Escape') onClose();
|
|
763
|
-
}
|
|
764
|
-
window.addEventListener('keydown', onKey);
|
|
765
|
-
return () => window.removeEventListener('keydown', onKey);
|
|
766
|
-
}, [open, onClose]);
|
|
767
|
-
if (!open) return null;
|
|
768
|
-
return (
|
|
769
|
-
<div
|
|
770
|
-
className="help-modal-backdrop"
|
|
771
|
-
role="presentation"
|
|
772
|
-
onMouseDown={(e) => {
|
|
773
|
-
if (e.target === e.currentTarget) onClose();
|
|
774
|
-
}}
|
|
775
|
-
>
|
|
776
|
-
<div
|
|
777
|
-
className="help-modal"
|
|
778
|
-
role="dialog"
|
|
779
|
-
aria-modal="true"
|
|
780
|
-
aria-labelledby="help-modal-title"
|
|
781
|
-
>
|
|
782
|
-
<header className="help-modal-hd">
|
|
783
|
-
<span className="title" id="help-modal-title">
|
|
784
|
-
Help · shortcuts & commands
|
|
785
|
-
</span>
|
|
786
|
-
<span className="sku">MDCC-DEV-SRV / v{MDCC_VERSION}</span>
|
|
787
|
-
<button
|
|
788
|
-
type="button"
|
|
789
|
-
className="help-modal-close"
|
|
790
|
-
aria-label="Close (Esc)"
|
|
791
|
-
onClick={onClose}
|
|
792
|
-
>
|
|
793
|
-
×
|
|
794
|
-
</button>
|
|
795
|
-
</header>
|
|
796
|
-
<div className="help-modal-body">
|
|
797
|
-
<details open>
|
|
798
|
-
<summary>Canvas selection & tools</summary>
|
|
799
|
-
<ul>
|
|
800
|
-
<li>
|
|
801
|
-
<kbd>V</kbd> <span>move tool — Cmd+click to select, Cmd+Shift to multi</span>
|
|
802
|
-
</li>
|
|
803
|
-
<li>
|
|
804
|
-
<kbd>H</kbd> <span>hand tool — bare drag pans (no Space needed)</span>
|
|
805
|
-
</li>
|
|
806
|
-
<li>
|
|
807
|
-
<kbd>C</kbd> <span>comment tool — hover paints, click drops a pin</span>
|
|
808
|
-
</li>
|
|
809
|
-
<li>
|
|
810
|
-
<kbd>⌘</kbd> + hover <span>preview deepest element under cursor</span>
|
|
811
|
-
</li>
|
|
812
|
-
<li>
|
|
813
|
-
<kbd>⌘</kbd> + click <span>select that element (replace)</span>
|
|
814
|
-
</li>
|
|
815
|
-
<li>
|
|
816
|
-
<kbd>⌘⇧</kbd> + click <span>add deepest to selection (multi)</span>
|
|
817
|
-
</li>
|
|
818
|
-
<li>
|
|
819
|
-
right-click <span>context menu (Copy CSS / Fit / Reset...)</span>
|
|
820
|
-
</li>
|
|
821
|
-
<li>
|
|
822
|
-
<kbd>Esc</kbd> in canvas <span>clear selection + close menu</span>
|
|
823
|
-
</li>
|
|
824
|
-
</ul>
|
|
825
|
-
</details>
|
|
826
|
-
<details>
|
|
827
|
-
<summary>Annotation tools</summary>
|
|
828
|
-
<ul>
|
|
829
|
-
<li>
|
|
830
|
-
<kbd>B</kbd> <span>pen — freehand stroke</span>
|
|
831
|
-
</li>
|
|
832
|
-
<li>
|
|
833
|
-
<kbd>R</kbd> <span>rectangle — drag to define corners</span>
|
|
834
|
-
</li>
|
|
835
|
-
<li>
|
|
836
|
-
<kbd>O</kbd> <span>ellipse — drag from center outward</span>
|
|
837
|
-
</li>
|
|
838
|
-
<li>
|
|
839
|
-
<kbd>A</kbd> <span>arrow — drag tail → tip</span>
|
|
840
|
-
</li>
|
|
841
|
-
<li>
|
|
842
|
-
<kbd>E</kbd> <span>eraser — click or drag over strokes to remove</span>
|
|
843
|
-
</li>
|
|
844
|
-
<li>
|
|
845
|
-
<kbd>V</kbd> + click stroke <span>select annotation (Shift+click to multi)</span>
|
|
846
|
-
</li>
|
|
847
|
-
<li>
|
|
848
|
-
<kbd>V</kbd> + drag empty <span>marquee-select strokes that overlap</span>
|
|
849
|
-
</li>
|
|
850
|
-
<li>
|
|
851
|
-
double-click rect/ellipse <span>add text inside the shape</span>
|
|
852
|
-
</li>
|
|
853
|
-
<li>
|
|
854
|
-
arrow keys <span>nudge selected annotation 1 unit (Shift = 10)</span>
|
|
855
|
-
</li>
|
|
856
|
-
<li>
|
|
857
|
-
<kbd>Backspace</kbd> <span>delete selected annotations</span>
|
|
858
|
-
</li>
|
|
859
|
-
<li>
|
|
860
|
-
<kbd>⇧P</kbd> <span>presentation — hide annotations for clean screenshot</span>
|
|
861
|
-
</li>
|
|
862
|
-
</ul>
|
|
863
|
-
</details>
|
|
864
|
-
<details>
|
|
865
|
-
<summary>Tabs & canvas</summary>
|
|
866
|
-
<ul>
|
|
867
|
-
<li>
|
|
868
|
-
click in tree <span>open tab</span>
|
|
869
|
-
</li>
|
|
870
|
-
<li>
|
|
871
|
-
<kbd>×</kbd> on tab <span>close tab</span>
|
|
872
|
-
</li>
|
|
873
|
-
<li>
|
|
874
|
-
<kbd>⌘R</kbd> <span>reload iframe</span>
|
|
875
|
-
</li>
|
|
876
|
-
<li>
|
|
877
|
-
<kbd>/</kbd> <span>focus search</span>
|
|
878
|
-
</li>
|
|
879
|
-
<li>
|
|
880
|
-
<kbd>⌘⇧M</kbd> <span>toggle comments panel</span>
|
|
881
|
-
</li>
|
|
882
|
-
</ul>
|
|
883
|
-
</details>
|
|
884
|
-
<details>
|
|
885
|
-
<summary>Slash commands</summary>
|
|
886
|
-
<ul className="cmds">
|
|
887
|
-
<li>
|
|
888
|
-
<code>
|
|
889
|
-
/design:edit "<i>feedback</i>"
|
|
890
|
-
</code>
|
|
891
|
-
<span>edit + 4-iter multi-axis loop</span>
|
|
892
|
-
</li>
|
|
893
|
-
<li>
|
|
894
|
-
<code>
|
|
895
|
-
/design:edit "<i>…</i>" --perfect
|
|
896
|
-
</code>
|
|
897
|
-
<span>8-iter polish (4.5/5 aspiration)</span>
|
|
898
|
-
</li>
|
|
899
|
-
<li>
|
|
900
|
-
<code>
|
|
901
|
-
/design:edit "<i>…</i>" --no-critic
|
|
902
|
-
</code>
|
|
903
|
-
<span>raw edit, skip loop</span>
|
|
904
|
-
</li>
|
|
905
|
-
<li>
|
|
906
|
-
<code>
|
|
907
|
-
/design:edit "<i>…</i>" --opt-out=<i>scope</i>
|
|
908
|
-
</code>
|
|
909
|
-
<span>override DS scope (palette/aesthetic/full)</span>
|
|
910
|
-
</li>
|
|
911
|
-
<li>
|
|
912
|
-
<code>
|
|
913
|
-
/design:new "<i>Name</i>" "<i>brief</i>"
|
|
914
|
-
</code>
|
|
915
|
-
<span>scaffold canvas</span>
|
|
916
|
-
</li>
|
|
917
|
-
<li>
|
|
918
|
-
<code>
|
|
919
|
-
/design:new "<i>…</i>" --opt-out=aesthetic
|
|
920
|
-
</code>
|
|
921
|
-
<span>scaffold off-system canvas (gradients/radii/type free)</span>
|
|
922
|
-
</li>
|
|
923
|
-
<li>
|
|
924
|
-
<code>/design:critic</code>
|
|
925
|
-
<span>review panel (routed)</span>
|
|
926
|
-
</li>
|
|
927
|
-
<li>
|
|
928
|
-
<code>/design:critic --all</code>
|
|
929
|
-
<span>10-critic sweep</span>
|
|
930
|
-
</li>
|
|
931
|
-
<li>
|
|
932
|
-
<code>/design:critic --agent signature-moment-critic</code>
|
|
933
|
-
<span>aspiration axis only</span>
|
|
934
|
-
</li>
|
|
935
|
-
<li>
|
|
936
|
-
<code>/design:rollback</code>
|
|
937
|
-
<span>undo last edit</span>
|
|
938
|
-
</li>
|
|
939
|
-
<li>
|
|
940
|
-
<code>/design:screenshot</code>
|
|
941
|
-
<span>capture canvas</span>
|
|
942
|
-
</li>
|
|
943
|
-
<li>
|
|
944
|
-
<code>/design:setup-docs</code>
|
|
945
|
-
<span>refresh README + INDEX</span>
|
|
946
|
-
</li>
|
|
947
|
-
<li>
|
|
948
|
-
<code>/design:handoff</code>
|
|
949
|
-
<span>migrate to apps/</span>
|
|
950
|
-
</li>
|
|
951
|
-
</ul>
|
|
952
|
-
</details>
|
|
953
|
-
<details>
|
|
954
|
-
<summary>Opt-out scope</summary>
|
|
955
|
-
<ul>
|
|
956
|
-
<li>
|
|
957
|
-
<strong>palette</strong>{' '}
|
|
958
|
-
<span>
|
|
959
|
-
default — tokens + rootClass kept; local namespace overrides colors only. DS
|
|
960
|
-
aesthetic still enforced.
|
|
961
|
-
</span>
|
|
962
|
-
</li>
|
|
963
|
-
<li>
|
|
964
|
-
<strong>aesthetic</strong>{' '}
|
|
965
|
-
<span>
|
|
966
|
-
palette + gradients/off-ladder radii/alt type/decorative SVG flags allowed.
|
|
967
|
-
</span>
|
|
968
|
-
</li>
|
|
969
|
-
<li>
|
|
970
|
-
<strong>full</strong>{' '}
|
|
971
|
-
<span>DS treated as advisory. Type/radii/aesthetic up to canvas.</span>
|
|
972
|
-
</li>
|
|
973
|
-
<li>
|
|
974
|
-
<em>A11y enforced at every scope</em>{' '}
|
|
975
|
-
<span>contrast, focus, semantics, motion, touch targets — never relaxed.</span>
|
|
976
|
-
</li>
|
|
977
|
-
<li>
|
|
978
|
-
Persisted on canvas's <code>.meta.json</code> <code>opt_out_scope</code> field —
|
|
979
|
-
subsequent <code>/design:edit</code> iterations inherit.
|
|
980
|
-
</li>
|
|
981
|
-
<li>
|
|
982
|
-
Inferred from brief ("modern", "vibrant", "off-system") with one-shot
|
|
983
|
-
AskUserQuestion before iter-1 critics fire.
|
|
984
|
-
</li>
|
|
985
|
-
</ul>
|
|
986
|
-
</details>
|
|
987
|
-
<details>
|
|
988
|
-
<summary>Auto-critic loop</summary>
|
|
989
|
-
<ul>
|
|
990
|
-
<li>
|
|
991
|
-
<strong>Default</strong>{' '}
|
|
992
|
-
<span>4 iter · aspiration ≥ 4.0 · stable-but-bland exit</span>
|
|
993
|
-
</li>
|
|
994
|
-
<li>
|
|
995
|
-
<strong>--perfect</strong>{' '}
|
|
996
|
-
<span>8 iter · aspiration ≥ 4.5 · broader divergence tolerance</span>
|
|
997
|
-
</li>
|
|
998
|
-
<li>
|
|
999
|
-
<strong>--perfect --all</strong>{' '}
|
|
1000
|
-
<span>every critic incl. aspiration · portfolio-grade</span>
|
|
1001
|
-
</li>
|
|
1002
|
-
<li>
|
|
1003
|
-
Exit: <code>solid</code> · <code>stable-but-bland</code> · <code>max-reached</code>{' '}
|
|
1004
|
-
· <code>divergent</code>
|
|
1005
|
-
</li>
|
|
1006
|
-
<li>
|
|
1007
|
-
<em>stable-but-bland</em> = correctness clean, aspiration plateau — surface for
|
|
1008
|
-
review with lowest 2 axes named
|
|
1009
|
-
</li>
|
|
1010
|
-
<li>
|
|
1011
|
-
When <code>opt_out_scope ∈ {aesthetic, full}</code>: iter-1 checkpoint
|
|
1012
|
-
fires — pick (a) run loop, (b) skip auto-loop and review iter 1, (c) a11y-only
|
|
1013
|
-
check.
|
|
1014
|
-
</li>
|
|
1015
|
-
</ul>
|
|
1016
|
-
</details>
|
|
1017
|
-
<details>
|
|
1018
|
-
<summary>Pin-to-element flow</summary>
|
|
1019
|
-
<ol>
|
|
1020
|
-
<li>Open canvas tab</li>
|
|
1021
|
-
<li>
|
|
1022
|
-
<kbd>⌘</kbd>+click element
|
|
1023
|
-
</li>
|
|
1024
|
-
<li>Status bar shows ● selector</li>
|
|
1025
|
-
<li>
|
|
1026
|
-
Run{' '}
|
|
1027
|
-
<code>
|
|
1028
|
-
/design:edit "<i>change just this</i>"
|
|
1029
|
-
</code>
|
|
1030
|
-
</li>
|
|
1031
|
-
<li>
|
|
1032
|
-
Reload iframe (<kbd>⌘R</kbd>)
|
|
1033
|
-
</li>
|
|
1034
|
-
</ol>
|
|
1035
|
-
</details>
|
|
1036
|
-
<details>
|
|
1037
|
-
<summary>Comments</summary>
|
|
1038
|
-
<ol>
|
|
1039
|
-
<li>
|
|
1040
|
-
<kbd>⌘</kbd>+click element, then <kbd>⌘C</kbd> <span>or ⌘⇧+click</span>
|
|
1041
|
-
</li>
|
|
1042
|
-
<li>Numbered pin appears on canvas</li>
|
|
1043
|
-
<li>
|
|
1044
|
-
<kbd>⌘⇧M</kbd> <span>opens panel — All / Open / Resolved</span>
|
|
1045
|
-
</li>
|
|
1046
|
-
<li>
|
|
1047
|
-
Click row in panel <span>jumps to that file + pin</span>
|
|
1048
|
-
</li>
|
|
1049
|
-
<li>
|
|
1050
|
-
Claude reads <code>_comments/<slug>.json</code> on next <code>/design</code>
|
|
1051
|
-
</li>
|
|
1052
|
-
</ol>
|
|
1053
|
-
</details>
|
|
1054
|
-
</div>
|
|
1055
|
-
</div>
|
|
1056
|
-
</div>
|
|
1057
|
-
);
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
// ───────── Menubar (CV-01/CV-08 top chrome) ─────────
|
|
1061
|
-
//
|
|
1062
|
-
// Replaces the legacy `.header` action-button toolbar. Mirrors the shared
|
|
1063
|
-
// Menubar component from .design/ui/Canvas Viewport.html — brand · menus ·
|
|
1064
|
-
// status. View dropdown is wired to the only toggleable panel today (the
|
|
1065
|
-
// Comments sidebar); the rest is inert with a phase-tag explaining when it
|
|
1066
|
-
// lands.
|
|
1067
|
-
|
|
1068
|
-
const MENU_NAMES = ['File', 'Edit', 'View', 'Selection', 'Tools', 'Help'];
|
|
1069
|
-
|
|
1070
|
-
function ViewDropdown({ panels, onToggle, onClose }) {
|
|
1071
|
-
useEffect(() => {
|
|
1072
|
-
function onKey(e) {
|
|
1073
|
-
if (e.key === 'Escape') onClose();
|
|
1074
|
-
}
|
|
1075
|
-
function onDocClick(e) {
|
|
1076
|
-
if (!e.target.closest('.mb-dropdown, .mb-menu')) onClose();
|
|
1077
|
-
}
|
|
1078
|
-
window.addEventListener('keydown', onKey);
|
|
1079
|
-
window.addEventListener('mousedown', onDocClick);
|
|
1080
|
-
return () => {
|
|
1081
|
-
window.removeEventListener('keydown', onKey);
|
|
1082
|
-
window.removeEventListener('mousedown', onDocClick);
|
|
1083
|
-
};
|
|
1084
|
-
}, [onClose]);
|
|
1085
|
-
|
|
1086
|
-
return (
|
|
1087
|
-
<div className="mb-dropdown" role="menu" aria-label="View" style={{ left: '146px' }}>
|
|
1088
|
-
<div className="mb-dd-hd">Panels</div>
|
|
1089
|
-
{panels.map((p) => (
|
|
1090
|
-
<button
|
|
1091
|
-
key={p.id}
|
|
1092
|
-
type="button"
|
|
1093
|
-
role="menuitem"
|
|
1094
|
-
className={'mb-dd-item' + (p.checked ? ' active' : '')}
|
|
1095
|
-
aria-disabled={p.disabled ? 'true' : undefined}
|
|
1096
|
-
onClick={() => {
|
|
1097
|
-
if (!p.disabled) {
|
|
1098
|
-
onToggle(p.id);
|
|
1099
|
-
onClose();
|
|
1100
|
-
}
|
|
1101
|
-
}}
|
|
1102
|
-
>
|
|
1103
|
-
<span className="lbl">
|
|
1104
|
-
<span className="check">{p.checked ? '✓' : ''}</span>
|
|
1105
|
-
<span>{p.label}</span>
|
|
1106
|
-
</span>
|
|
1107
|
-
{p.phase ? (
|
|
1108
|
-
<span className="phase-tag">{p.phase}</span>
|
|
1109
|
-
) : (
|
|
1110
|
-
<span className="shortcut">{p.shortcut || ''}</span>
|
|
1111
|
-
)}
|
|
1112
|
-
</button>
|
|
1113
|
-
))}
|
|
1114
|
-
<div className="mb-dd-sep" />
|
|
1115
|
-
<div className="mb-dd-hd">Zoom</div>
|
|
1116
|
-
{[
|
|
1117
|
-
{ label: 'Zoom In', shortcut: '⌘ +' },
|
|
1118
|
-
{ label: 'Zoom Out', shortcut: '⌘ −' },
|
|
1119
|
-
{ label: 'Fit to Screen', shortcut: '⌘ 0' },
|
|
1120
|
-
{ label: 'Actual Size · 100 %', shortcut: '⌥ ⌘ 0' },
|
|
1121
|
-
].map((z) => (
|
|
1122
|
-
<button
|
|
1123
|
-
key={z.label}
|
|
1124
|
-
type="button"
|
|
1125
|
-
role="menuitem"
|
|
1126
|
-
className="mb-dd-item"
|
|
1127
|
-
aria-disabled="true"
|
|
1128
|
-
>
|
|
1129
|
-
<span className="lbl">
|
|
1130
|
-
<span className="check" />
|
|
1131
|
-
<span>{z.label}</span>
|
|
1132
|
-
</span>
|
|
1133
|
-
<span className="phase-tag">Phase 4</span>
|
|
1134
|
-
</button>
|
|
1135
|
-
))}
|
|
1136
|
-
</div>
|
|
1137
|
-
);
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1141
|
-
// Phase 5.1 — Selection + Tools dropdowns (mirror ViewDropdown shape).
|
|
1142
|
-
|
|
1143
|
-
function SelectionDropdown({ onAction, onClose }) {
|
|
1144
|
-
useEffect(() => {
|
|
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]);
|
|
1158
|
-
const items = [
|
|
1159
|
-
{ id: 'deselect-all', label: 'Deselect all', shortcut: 'Esc' },
|
|
1160
|
-
{ id: 'select-all-annotations', label: 'Select all annotations', shortcut: '⌘ ⇧ A' },
|
|
1161
|
-
];
|
|
1162
|
-
return (
|
|
1163
|
-
<div className="mb-dropdown" role="menu" aria-label="Selection" style={{ left: '195px' }}>
|
|
1164
|
-
{items.map((it) => (
|
|
1165
|
-
<button
|
|
1166
|
-
key={it.id}
|
|
1167
|
-
type="button"
|
|
1168
|
-
role="menuitem"
|
|
1169
|
-
className="mb-dd-item"
|
|
1170
|
-
onClick={() => {
|
|
1171
|
-
onAction(it.id);
|
|
1172
|
-
onClose();
|
|
1173
|
-
}}
|
|
1174
|
-
>
|
|
1175
|
-
<span className="lbl">
|
|
1176
|
-
<span className="check" />
|
|
1177
|
-
<span>{it.label}</span>
|
|
1178
|
-
</span>
|
|
1179
|
-
<span className="shortcut">{it.shortcut}</span>
|
|
1180
|
-
</button>
|
|
1181
|
-
))}
|
|
1182
|
-
</div>
|
|
1183
|
-
);
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
function ToolsDropdown({ onAction, onClose }) {
|
|
1187
|
-
useEffect(() => {
|
|
1188
|
-
function onKey(e) {
|
|
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 —
|
|
1202
|
-
// kept in sync by hand because the menubar lives in the dev-server shell
|
|
1203
|
-
// (no shared bundle with the canvas iframes).
|
|
1204
|
-
const tools = [
|
|
1205
|
-
{ id: 'move', label: 'Move', shortcut: 'V' },
|
|
1206
|
-
{ id: 'hand', label: 'Hand', shortcut: 'H' },
|
|
1207
|
-
{ id: 'comment', label: 'Comment', shortcut: 'C' },
|
|
1208
|
-
{ id: 'pen', label: 'Pen', shortcut: 'B' },
|
|
1209
|
-
{ id: 'rect', label: 'Rect', shortcut: 'R' },
|
|
1210
|
-
{ id: 'ellipse', label: 'Ellipse', shortcut: 'O' },
|
|
1211
|
-
{ id: 'sticky', label: 'Sticky', shortcut: 'N' },
|
|
1212
|
-
{ id: 'arrow', label: 'Arrow', shortcut: 'A' },
|
|
1213
|
-
{ id: 'text', label: 'Text', shortcut: 'T' },
|
|
1214
|
-
{ id: 'eraser', label: 'Eraser', shortcut: 'E' },
|
|
1215
|
-
];
|
|
1216
|
-
return (
|
|
1217
|
-
<div className="mb-dropdown" role="menu" aria-label="Tools" style={{ left: '253px' }}>
|
|
1218
|
-
{tools.map((t) => (
|
|
1219
|
-
<button
|
|
1220
|
-
key={t.id}
|
|
1221
|
-
type="button"
|
|
1222
|
-
role="menuitem"
|
|
1223
|
-
className="mb-dd-item"
|
|
1224
|
-
onClick={() => {
|
|
1225
|
-
onAction(t.id);
|
|
1226
|
-
onClose();
|
|
1227
|
-
}}
|
|
1228
|
-
>
|
|
1229
|
-
<span className="lbl">
|
|
1230
|
-
<span className="check" />
|
|
1231
|
-
<span>{t.label}</span>
|
|
1232
|
-
</span>
|
|
1233
|
-
<span className="shortcut">{t.shortcut}</span>
|
|
1234
|
-
</button>
|
|
1235
|
-
))}
|
|
1236
|
-
</div>
|
|
1237
|
-
);
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
function Menubar({
|
|
1241
|
-
activePath,
|
|
1242
|
-
project,
|
|
1243
|
-
tabsCount,
|
|
1244
|
-
openMenu,
|
|
1245
|
-
setOpenMenu,
|
|
1246
|
-
commentsPanelOpen,
|
|
1247
|
-
onToggleComments,
|
|
1248
|
-
onOpenSystem,
|
|
1249
|
-
sidebarOpen,
|
|
1250
|
-
onToggleSidebar,
|
|
1251
|
-
showHidden,
|
|
1252
|
-
onToggleShowHidden,
|
|
1253
|
-
onOpenHelp,
|
|
1254
|
-
annotationsVisible,
|
|
1255
|
-
onToggleAnnotations,
|
|
1256
|
-
postToActiveCanvas,
|
|
1257
|
-
}) {
|
|
1258
|
-
const isSystem = activePath === SYSTEM_TAB;
|
|
1259
|
-
const stamp = isSystem ? 'SYSTEM' : activePath ? 'CANVAS' : 'IDLE';
|
|
1260
|
-
const fileLabel = isSystem ? (
|
|
1261
|
-
<b>design system</b>
|
|
1262
|
-
) : activePath ? (
|
|
1263
|
-
<>
|
|
1264
|
-
{activePath.split('/').slice(0, -1).join('/')}/<b>{displayName(basename(activePath))}</b>
|
|
1265
|
-
</>
|
|
1266
|
-
) : (
|
|
1267
|
-
<span style={{ color: 'var(--u-fg-3)' }}>no canvas open</span>
|
|
1268
|
-
);
|
|
1269
|
-
|
|
1270
|
-
const panels = [
|
|
1271
|
-
{ id: 'tree', label: 'Project Tree', shortcut: 'T', checked: sidebarOpen, disabled: false },
|
|
1272
|
-
{
|
|
1273
|
-
id: 'comments',
|
|
1274
|
-
label: 'Comments Sidebar',
|
|
1275
|
-
shortcut: '⌘ ⇧ M',
|
|
1276
|
-
checked: commentsPanelOpen,
|
|
1277
|
-
disabled: false,
|
|
1278
|
-
},
|
|
1279
|
-
{
|
|
1280
|
-
id: 'hidden',
|
|
1281
|
-
label: 'Show hidden files',
|
|
1282
|
-
shortcut: 'H',
|
|
1283
|
-
checked: showHidden,
|
|
1284
|
-
disabled: false,
|
|
1285
|
-
},
|
|
1286
|
-
{ id: 'layers', label: 'Layers Panel', phase: 'Phase 12', disabled: true },
|
|
1287
|
-
{ id: 'inspector', label: 'Inspector', phase: 'Phase 12', disabled: true },
|
|
1288
|
-
{
|
|
1289
|
-
id: 'annotate',
|
|
1290
|
-
label: 'Annotations',
|
|
1291
|
-
shortcut: '⇧ P',
|
|
1292
|
-
checked: annotationsVisible,
|
|
1293
|
-
disabled: false,
|
|
1294
|
-
},
|
|
1295
|
-
{ id: 'present', label: 'Presentation Mode', phase: 'Phase 6', disabled: true },
|
|
1296
|
-
];
|
|
1297
|
-
|
|
1298
|
-
function onMenuClick(key) {
|
|
1299
|
-
if (key === 'view' || key === 'selection' || key === 'tools') {
|
|
1300
|
-
setOpenMenu(openMenu === key ? null : key);
|
|
1301
|
-
} else if (key === 'help') {
|
|
1302
|
-
setOpenMenu(null);
|
|
1303
|
-
onOpenHelp();
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
return (
|
|
1308
|
-
<header className="mb" role="menubar" aria-label="Application menubar">
|
|
1309
|
-
<span className="mb-brand">
|
|
1310
|
-
<span className="dot" aria-hidden="true" />
|
|
1311
|
-
<span>maude</span>
|
|
1312
|
-
</span>
|
|
1313
|
-
<nav className="mb-menus" aria-label="Application menus">
|
|
1314
|
-
{MENU_NAMES.map((name) => {
|
|
1315
|
-
const key = name.toLowerCase();
|
|
1316
|
-
const hasDropdown = key === 'view' || key === 'selection' || key === 'tools';
|
|
1317
|
-
const interactive = hasDropdown || key === 'help';
|
|
1318
|
-
const open = openMenu === key;
|
|
1319
|
-
return (
|
|
1320
|
-
<button
|
|
1321
|
-
key={key}
|
|
1322
|
-
type="button"
|
|
1323
|
-
className="mb-menu"
|
|
1324
|
-
role="menuitem"
|
|
1325
|
-
aria-haspopup={hasDropdown ? 'menu' : undefined}
|
|
1326
|
-
aria-expanded={hasDropdown ? open : undefined}
|
|
1327
|
-
aria-disabled={interactive ? undefined : 'true'}
|
|
1328
|
-
title={interactive ? '' : 'Coming in a later phase'}
|
|
1329
|
-
onClick={() => onMenuClick(key)}
|
|
1330
|
-
>
|
|
1331
|
-
{name}
|
|
1332
|
-
</button>
|
|
1333
|
-
);
|
|
1334
|
-
})}
|
|
1335
|
-
</nav>
|
|
1336
|
-
{openMenu === 'view' && (
|
|
1337
|
-
<ViewDropdown
|
|
1338
|
-
panels={panels}
|
|
1339
|
-
onToggle={(id) => {
|
|
1340
|
-
if (id === 'tree') onToggleSidebar();
|
|
1341
|
-
else if (id === 'comments') onToggleComments();
|
|
1342
|
-
else if (id === 'hidden') onToggleShowHidden();
|
|
1343
|
-
else if (id === 'annotate') onToggleAnnotations();
|
|
1344
|
-
}}
|
|
1345
|
-
onClose={() => setOpenMenu(null)}
|
|
1346
|
-
/>
|
|
1347
|
-
)}
|
|
1348
|
-
{openMenu === 'selection' && (
|
|
1349
|
-
<SelectionDropdown
|
|
1350
|
-
onAction={(id) => {
|
|
1351
|
-
if (id === 'deselect-all') postToActiveCanvas({ dgn: 'selection-clear' });
|
|
1352
|
-
else if (id === 'select-all-annotations')
|
|
1353
|
-
postToActiveCanvas({ dgn: 'annotation-select-all' });
|
|
1354
|
-
}}
|
|
1355
|
-
onClose={() => setOpenMenu(null)}
|
|
1356
|
-
/>
|
|
1357
|
-
)}
|
|
1358
|
-
{openMenu === 'tools' && (
|
|
1359
|
-
<ToolsDropdown
|
|
1360
|
-
onAction={(tool) => postToActiveCanvas({ dgn: 'tool-set', tool })}
|
|
1361
|
-
onClose={() => setOpenMenu(null)}
|
|
1362
|
-
/>
|
|
1363
|
-
)}
|
|
1364
|
-
<div className="mb-spacer" />
|
|
1365
|
-
<div className="mb-status">
|
|
1366
|
-
<span className="cv-stamp">{stamp}</span>
|
|
1367
|
-
<span className="file" title={activePath || ''}>
|
|
1368
|
-
{fileLabel}
|
|
1369
|
-
</span>
|
|
1370
|
-
<span className="sep" />
|
|
1371
|
-
<span>
|
|
1372
|
-
<span className="accent-dot">●</span> <b>{tabsCount}</b> ARTBOARDS
|
|
1373
|
-
</span>
|
|
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>
|
|
1381
|
-
</span>
|
|
1382
|
-
</div>
|
|
1383
|
-
</header>
|
|
1384
|
-
);
|
|
1385
|
-
}
|
|
1386
|
-
|
|
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
|
-
function Viewport({
|
|
1424
|
-
tabs,
|
|
1425
|
-
activePath,
|
|
1426
|
-
registerIframe,
|
|
1427
|
-
systemData,
|
|
1428
|
-
onOpenFromSystem,
|
|
1429
|
-
onSelectDs,
|
|
1430
|
-
project,
|
|
1431
|
-
cfg,
|
|
1432
|
-
}) {
|
|
1433
|
-
return (
|
|
1434
|
-
<div className="viewport">
|
|
1435
|
-
{tabs.length === 0 && (
|
|
1436
|
-
<>
|
|
1437
|
-
<Wordmark
|
|
1438
|
-
project={project}
|
|
1439
|
-
port={typeof window !== 'undefined' ? window.location.port : ''}
|
|
1440
|
-
version={MDCC_VERSION}
|
|
1441
|
-
/>
|
|
1442
|
-
<div className="empty-state">
|
|
1443
|
-
<div className="big">No mock open</div>
|
|
1444
|
-
<div className="small">
|
|
1445
|
-
← Click a <code>.tsx</code> (or legacy <code>.html</code>) file in the tree, or open
|
|
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>
|
|
1461
|
-
</div>
|
|
1462
|
-
</>
|
|
1463
|
-
)}
|
|
1464
|
-
{tabs.map((t) => {
|
|
1465
|
-
if (t.path === SYSTEM_TAB) {
|
|
1466
|
-
return (
|
|
1467
|
-
<div key={t.path} className={'system-view' + (t.path === activePath ? ' active' : '')}>
|
|
1468
|
-
<SystemView
|
|
1469
|
-
data={systemData}
|
|
1470
|
-
onOpen={onOpenFromSystem}
|
|
1471
|
-
cfg={cfg}
|
|
1472
|
-
onSelectDs={onSelectDs}
|
|
1473
|
-
/>
|
|
1474
|
-
</div>
|
|
1475
|
-
);
|
|
1476
|
-
}
|
|
1477
|
-
return (
|
|
1478
|
-
<iframe
|
|
1479
|
-
key={t.path}
|
|
1480
|
-
ref={(el) => registerIframe(t.path, el)}
|
|
1481
|
-
src={canvasUrl(t.path, cfg)}
|
|
1482
|
-
className={t.path === activePath ? 'active' : ''}
|
|
1483
|
-
data-path={t.path}
|
|
1484
|
-
// T2 (9.1-A) — only sandbox + delegate clipboard when the canvas is
|
|
1485
|
-
// served cross-origin (canvasOrigin present = the split is on). In
|
|
1486
|
-
// the default same-origin mode these attrs are omitted so behavior
|
|
1487
|
-
// is identical to pre-9.1. allow-same-origin gives the cross-origin
|
|
1488
|
-
// frame its OWN origin (own WS/fetch/storage), NOT the parent's.
|
|
1489
|
-
{...(cfg?.canvasOrigin
|
|
1490
|
-
? { sandbox: 'allow-scripts allow-same-origin', allow: 'clipboard-write' }
|
|
1491
|
-
: {})}
|
|
1492
|
-
/>
|
|
1493
|
-
);
|
|
1494
|
-
})}
|
|
1495
|
-
</div>
|
|
1496
|
-
);
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
// ---------- SystemView ----------
|
|
1500
|
-
//
|
|
1501
|
-
// DDR-048 — the System view renders the USER's design-system tokens. It does
|
|
1502
|
-
// NOT read from `document.documentElement` (that would surface the dev-server
|
|
1503
|
-
// shell's amber-rust chrome theme from styles/1-tokens.css, which is NOT a
|
|
1504
|
-
// user template) and it does NOT assume any canonical token-name contract.
|
|
1505
|
-
// Whatever the user's `colors_and_type.css` declared — names, values, theme
|
|
1506
|
-
// blocks — is what shows up here.
|
|
1507
|
-
|
|
1508
|
-
// Order kinds match the typical reading flow of a tokens file. Unknown kinds
|
|
1509
|
-
// fall through to `other` so a custom token group still renders, just last.
|
|
1510
|
-
const TOKEN_GROUP_ORDER = [
|
|
1511
|
-
'color',
|
|
1512
|
-
'space',
|
|
1513
|
-
'radius',
|
|
1514
|
-
'shadow',
|
|
1515
|
-
'leading',
|
|
1516
|
-
'weight',
|
|
1517
|
-
'motion',
|
|
1518
|
-
'font',
|
|
1519
|
-
'other',
|
|
1520
|
-
];
|
|
1521
|
-
const TOKEN_GROUP_LABELS = {
|
|
1522
|
-
color: 'colors',
|
|
1523
|
-
space: 'spacing',
|
|
1524
|
-
radius: 'radii',
|
|
1525
|
-
shadow: 'shadows',
|
|
1526
|
-
leading: 'leading',
|
|
1527
|
-
weight: 'weights',
|
|
1528
|
-
motion: 'motion',
|
|
1529
|
-
font: 'font stacks',
|
|
1530
|
-
other: 'other',
|
|
1531
|
-
};
|
|
1532
|
-
|
|
1533
|
-
function isSwatchKind(kind) {
|
|
1534
|
-
return kind === 'color';
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
function TokenLadder({ tokens, tokenGroups, tokensPath }) {
|
|
1538
|
-
if (!tokens || tokens.length === 0) {
|
|
1539
|
-
return (
|
|
1540
|
-
<section className="sv-section sv-section-tokens">
|
|
1541
|
-
<h2>
|
|
1542
|
-
tokens<span className="sv-h-num">0</span>
|
|
1543
|
-
</h2>
|
|
1544
|
-
<div className="sv-empty">
|
|
1545
|
-
<p>
|
|
1546
|
-
No tokens parsed from{' '}
|
|
1547
|
-
{tokensPath ? <code>{tokensPath}</code> : 'the configured tokens file'}. Does the file
|
|
1548
|
-
exist and contain CSS custom properties (<code>--name: value;</code>)?
|
|
1549
|
-
</p>
|
|
1550
|
-
</div>
|
|
1551
|
-
</section>
|
|
1552
|
-
);
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
const groups = tokenGroups || {};
|
|
1556
|
-
const kinds = Array.from(new Set([...TOKEN_GROUP_ORDER, ...Object.keys(groups)])).filter(
|
|
1557
|
-
(k) => groups[k]?.length
|
|
1558
|
-
);
|
|
1559
|
-
|
|
1560
|
-
return (
|
|
1561
|
-
<>
|
|
1562
|
-
{kinds.map((kind) => {
|
|
1563
|
-
const list = groups[kind];
|
|
1564
|
-
const swatch = isSwatchKind(kind);
|
|
1565
|
-
return (
|
|
1566
|
-
<section className={'sv-section sv-section-tokens sv-section-' + kind} key={kind}>
|
|
1567
|
-
<h2>
|
|
1568
|
-
tokens · {TOKEN_GROUP_LABELS[kind] || kind}
|
|
1569
|
-
<span className="sv-h-num">{list.length}</span>
|
|
1570
|
-
</h2>
|
|
1571
|
-
<div className="sv-tokens-ladder">
|
|
1572
|
-
{list.map((t) => (
|
|
1573
|
-
<div className="sv-tok-cell" key={t.name + '|' + t.value}>
|
|
1574
|
-
{swatch ? (
|
|
1575
|
-
<div className="sv-tok-swatch" style={{ background: t.value }} />
|
|
1576
|
-
) : null}
|
|
1577
|
-
<div className="sv-tok-meta">
|
|
1578
|
-
<code className="sv-tok-name">{t.name}</code>
|
|
1579
|
-
<span className="sv-tok-value">{t.value || '—'}</span>
|
|
1580
|
-
</div>
|
|
1581
|
-
</div>
|
|
1582
|
-
))}
|
|
1583
|
-
</div>
|
|
1584
|
-
</section>
|
|
1585
|
-
);
|
|
1586
|
-
})}
|
|
1587
|
-
</>
|
|
1588
|
-
);
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
// Find a leading partner by name suffix: --fs-base → --lh-base, --type-xl → --lh-xl.
|
|
1592
|
-
// Returns null when no convention match exists; caller omits the lineHeight style.
|
|
1593
|
-
function findLeadingFor(typeToken, leadingTokens) {
|
|
1594
|
-
const m = typeToken.name.match(/^--(?:type|fs|text)-(.+)$/);
|
|
1595
|
-
if (!m) return null;
|
|
1596
|
-
const suffix = m[1];
|
|
1597
|
-
return (
|
|
1598
|
-
(leadingTokens || []).find(
|
|
1599
|
-
(t) => /^--(?:lh|leading|line-height)-/.test(t.name) && t.name.endsWith('-' + suffix)
|
|
1600
|
-
)?.value ?? null
|
|
1601
|
-
);
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
// Best-effort sample font: prefer body / sans / display tokens, fall back to
|
|
1605
|
-
// the first font-kind token, fall back to system-ui. Avoids the shell's
|
|
1606
|
-
// Berkeley Mono leaking into user-facing previews.
|
|
1607
|
-
function sampleFontFamily(fontTokens) {
|
|
1608
|
-
if (!fontTokens?.length) return undefined;
|
|
1609
|
-
const prefer = ['body', 'sans', 'display', 'text', 'family'];
|
|
1610
|
-
for (const tag of prefer) {
|
|
1611
|
-
const hit = fontTokens.find((t) => t.name.includes(tag));
|
|
1612
|
-
if (hit) return hit.value;
|
|
1613
|
-
}
|
|
1614
|
-
return fontTokens[0].value;
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
function TypeLadder({ tokenGroups }) {
|
|
1618
|
-
const typeTokens = tokenGroups?.fontsize || [];
|
|
1619
|
-
if (typeTokens.length === 0) return null;
|
|
1620
|
-
const leadingTokens = tokenGroups?.leading || [];
|
|
1621
|
-
const sampleFont = sampleFontFamily(tokenGroups?.font);
|
|
1622
|
-
|
|
1623
|
-
return (
|
|
1624
|
-
<section className="sv-section sv-section-type">
|
|
1625
|
-
<h2>
|
|
1626
|
-
type · ladder<span className="sv-h-num">{typeTokens.length}</span>
|
|
1627
|
-
</h2>
|
|
1628
|
-
<div className="sv-type-list">
|
|
1629
|
-
{typeTokens.map((t) => {
|
|
1630
|
-
const lh = findLeadingFor(t, leadingTokens);
|
|
1631
|
-
const style = { fontSize: t.value };
|
|
1632
|
-
if (lh) style.lineHeight = lh;
|
|
1633
|
-
if (sampleFont) style.fontFamily = sampleFont;
|
|
1634
|
-
return (
|
|
1635
|
-
<div className="sv-type-row" key={t.name}>
|
|
1636
|
-
<code className="sv-type-tok">{t.name}</code>
|
|
1637
|
-
<span className="sv-type-sample" style={style}>
|
|
1638
|
-
The catalog is the system.
|
|
1639
|
-
</span>
|
|
1640
|
-
</div>
|
|
1641
|
-
);
|
|
1642
|
-
})}
|
|
1643
|
-
</div>
|
|
1644
|
-
</section>
|
|
1645
|
-
);
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
|
-
function SystemView({ data, onOpen, cfg, onSelectDs }) {
|
|
1649
|
-
if (!data) {
|
|
1650
|
-
return (
|
|
1651
|
-
<div className="sv-empty">
|
|
1652
|
-
<p>Loading design system…</p>
|
|
1653
|
-
</div>
|
|
1654
|
-
);
|
|
1655
|
-
}
|
|
1656
|
-
const {
|
|
1657
|
-
previewGallery,
|
|
1658
|
-
uiKitsGallery,
|
|
1659
|
-
systemDir,
|
|
1660
|
-
tokens,
|
|
1661
|
-
tokenGroups,
|
|
1662
|
-
tokensPath,
|
|
1663
|
-
ds,
|
|
1664
|
-
availableDesignSystems,
|
|
1665
|
-
} = data;
|
|
1666
|
-
const empty =
|
|
1667
|
-
(!previewGallery || !previewGallery.length) && (!uiKitsGallery || !uiKitsGallery.length);
|
|
1668
|
-
const hasPicker = Array.isArray(availableDesignSystems) && availableDesignSystems.length > 1;
|
|
1669
|
-
const selectedName = ds?.name ?? availableDesignSystems?.[0]?.name ?? '';
|
|
1670
|
-
|
|
1671
|
-
return (
|
|
1672
|
-
<div className="sv">
|
|
1673
|
-
<header className="sv-header">
|
|
1674
|
-
<span className="sv-sku">MDCC-DSN/01</span>
|
|
1675
|
-
<span className="sv-title">design system view</span>
|
|
1676
|
-
{hasPicker ? (
|
|
1677
|
-
<label className="sv-ds-picker">
|
|
1678
|
-
<span className="sv-ds-picker-label">DS</span>
|
|
1679
|
-
<select value={selectedName} onChange={(e) => onSelectDs && onSelectDs(e.target.value)}>
|
|
1680
|
-
{availableDesignSystems.map((d) => (
|
|
1681
|
-
<option key={d.name} value={d.name}>
|
|
1682
|
-
{d.name}
|
|
1683
|
-
</option>
|
|
1684
|
-
))}
|
|
1685
|
-
</select>
|
|
1686
|
-
</label>
|
|
1687
|
-
) : null}
|
|
1688
|
-
<span className="sv-loc">
|
|
1689
|
-
<code>{systemDir}</code>
|
|
1690
|
-
</span>
|
|
1691
|
-
</header>
|
|
1692
|
-
|
|
1693
|
-
{ds?.description ? <p className="sv-ds-description">{ds.description}</p> : null}
|
|
1694
|
-
|
|
1695
|
-
<TokenLadder tokens={tokens} tokenGroups={tokenGroups} tokensPath={tokensPath} />
|
|
1696
|
-
<TypeLadder tokenGroups={tokenGroups} />
|
|
1697
|
-
|
|
1698
|
-
{empty ? (
|
|
1699
|
-
<div className="sv-empty">
|
|
1700
|
-
<p>
|
|
1701
|
-
No <code>preview/</code> or <code>ui_kits/</code> folders found under{' '}
|
|
1702
|
-
<code>{systemDir}</code>.
|
|
1703
|
-
</p>
|
|
1704
|
-
</div>
|
|
1705
|
-
) : (
|
|
1706
|
-
<>
|
|
1707
|
-
<Gallery
|
|
1708
|
-
title="preview"
|
|
1709
|
-
items={previewGallery}
|
|
1710
|
-
onOpen={onOpen}
|
|
1711
|
-
kind="preview"
|
|
1712
|
-
cfg={cfg}
|
|
1713
|
-
/>
|
|
1714
|
-
<Gallery title="ui kits" items={uiKitsGallery} onOpen={onOpen} kind="ui_kits" cfg={cfg} />
|
|
1715
|
-
</>
|
|
1716
|
-
)}
|
|
1717
|
-
</div>
|
|
1718
|
-
);
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
function Gallery({ title, items, onOpen, kind, cfg }) {
|
|
1722
|
-
if (!items || items.length === 0) return null;
|
|
1723
|
-
return (
|
|
1724
|
-
<section className="sv-section">
|
|
1725
|
-
<h2>
|
|
1726
|
-
{title} <span className="sv-count">{items.length}</span>
|
|
1727
|
-
</h2>
|
|
1728
|
-
<div className={'sv-previews sv-previews-' + kind}>
|
|
1729
|
-
{items.map((p) => (
|
|
1730
|
-
<article key={p.path} className="sv-preview-card" onClick={() => onOpen(p.path)}>
|
|
1731
|
-
<div className="sv-preview-frame">
|
|
1732
|
-
<iframe
|
|
1733
|
-
src={canvasUrl(p.path, cfg, { thumbnail: true })}
|
|
1734
|
-
title={p.label}
|
|
1735
|
-
scrolling="no"
|
|
1736
|
-
{...(cfg?.canvasOrigin ? { sandbox: 'allow-scripts allow-same-origin' } : {})}
|
|
1737
|
-
/>
|
|
1738
|
-
</div>
|
|
1739
|
-
<div className="sv-preview-foot">
|
|
1740
|
-
<strong>{p.label}</strong>
|
|
1741
|
-
<code>{p.path}</code>
|
|
1742
|
-
</div>
|
|
1743
|
-
</article>
|
|
1744
|
-
))}
|
|
1745
|
-
</div>
|
|
1746
|
-
</section>
|
|
1747
|
-
);
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
// ---------- Comment composer / viewer ----------
|
|
1751
|
-
|
|
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
|
-
function StatusBar({
|
|
1780
|
-
activePath,
|
|
1781
|
-
selected,
|
|
1782
|
-
wsConnected,
|
|
1783
|
-
openCount,
|
|
1784
|
-
theme,
|
|
1785
|
-
onToggleTheme,
|
|
1786
|
-
onClearSelected,
|
|
1787
|
-
syncStatus,
|
|
1788
|
-
}) {
|
|
1789
|
-
const isSystem = activePath === SYSTEM_TAB;
|
|
1790
|
-
const text =
|
|
1791
|
-
selected && selected.selector
|
|
1792
|
-
? selected.selector + (selected.text ? ` — "${selected.text.slice(0, 60)}"` : '')
|
|
1793
|
-
: '';
|
|
1794
|
-
const title =
|
|
1795
|
-
selected && selected.dom_path
|
|
1796
|
-
? selected.dom_path.join(' > ')
|
|
1797
|
-
: selected
|
|
1798
|
-
? selected.selector
|
|
1799
|
-
: '';
|
|
1800
|
-
return (
|
|
1801
|
-
<div className="statusbar" role="contentinfo">
|
|
1802
|
-
<StatusBarSlot label="Active file" className="sb-active">
|
|
1803
|
-
<span className="sb-key">active</span>
|
|
1804
|
-
<span className="sb-file" title={activePath || ''}>
|
|
1805
|
-
{isSystem ? '▦ design system' : activePath || '—'}
|
|
1806
|
-
</span>
|
|
1807
|
-
</StatusBarSlot>
|
|
1808
|
-
|
|
1809
|
-
{selected && selected.selector && !isSystem && (
|
|
1810
|
-
<StatusBarSlot label="Selected element" className="sb-selected">
|
|
1811
|
-
<span className="sb-dot" aria-hidden="true">
|
|
1812
|
-
●
|
|
1813
|
-
</span>
|
|
1814
|
-
<span className="sb-sel-text" title={title}>
|
|
1815
|
-
{text}
|
|
1816
|
-
</span>
|
|
1817
|
-
<button
|
|
1818
|
-
type="button"
|
|
1819
|
-
className="sb-clear-sel"
|
|
1820
|
-
onClick={onClearSelected}
|
|
1821
|
-
title="Clear (Esc inside iframe)"
|
|
1822
|
-
aria-label="Clear selection"
|
|
1823
|
-
>
|
|
1824
|
-
×
|
|
1825
|
-
</button>
|
|
1826
|
-
</StatusBarSlot>
|
|
1827
|
-
)}
|
|
1828
|
-
|
|
1829
|
-
<StatusBarSlot label="Open comments" className="sb-unread">
|
|
1830
|
-
<span className="sb-key">comments</span>
|
|
1831
|
-
<span className="sb-count">{openCount}</span>
|
|
1832
|
-
</StatusBarSlot>
|
|
1833
|
-
|
|
1834
|
-
<StatusBarSlot label="Connection" className="sb-live">
|
|
1835
|
-
<span className={'sb-live-dot' + (wsConnected ? ' connected' : '')} aria-hidden="true" />
|
|
1836
|
-
<span className="sb-key">{wsConnected ? 'live' : 'reconnecting'}</span>
|
|
1837
|
-
</StatusBarSlot>
|
|
1838
|
-
|
|
1839
|
-
{/* DDR-060 / 9.1-D — linked-to-hub-but-nothing-syncs state lives here in the
|
|
1840
|
-
status bar (DS-styled, hover for detail) instead of a floating off-brand
|
|
1841
|
-
pill. Only shown when the project is linked + has 0 syncable canvases. */}
|
|
1842
|
-
{syncStatus?.notSyncable && (
|
|
1843
|
-
<StatusBarSlot label="Hub sync" className="sb-sync">
|
|
1844
|
-
<span className="sb-sync-dot" aria-hidden="true" />
|
|
1845
|
-
<span
|
|
1846
|
-
className="sb-key"
|
|
1847
|
-
title={syncStatus.reason || 'Linked to a hub, but no canvases are syncable.'}
|
|
1848
|
-
>
|
|
1849
|
-
0 syncable{syncStatus.tsxCount > 0 ? ` · ${syncStatus.tsxCount} tsx` : ''}
|
|
1850
|
-
</span>
|
|
1851
|
-
</StatusBarSlot>
|
|
1852
|
-
)}
|
|
1853
|
-
|
|
1854
|
-
<span className="sb-spacer" />
|
|
1855
|
-
|
|
1856
|
-
<StatusBarSlot label="Theme" className="sb-theme">
|
|
1857
|
-
<ThemeToggle theme={theme} onToggle={onToggleTheme} />
|
|
1858
|
-
</StatusBarSlot>
|
|
1859
|
-
</div>
|
|
1860
|
-
);
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
// ---------- Right sidebar — Comments panel ----------
|
|
1864
|
-
|
|
1865
|
-
function CommentsPanel({
|
|
1866
|
-
commentsByFile,
|
|
1867
|
-
filter,
|
|
1868
|
-
setFilter,
|
|
1869
|
-
activePath,
|
|
1870
|
-
focusedId,
|
|
1871
|
-
onJump,
|
|
1872
|
-
onResolve,
|
|
1873
|
-
onReopen,
|
|
1874
|
-
onDelete,
|
|
1875
|
-
}) {
|
|
1876
|
-
const counts = totalCounts(commentsByFile);
|
|
1877
|
-
// Build groups: [{ file, comments: filtered }]
|
|
1878
|
-
const files = Object.keys(commentsByFile || {}).sort();
|
|
1879
|
-
const groups = [];
|
|
1880
|
-
for (const f of files) {
|
|
1881
|
-
const all = commentsByFile[f] || [];
|
|
1882
|
-
const filtered = all.filter((c) => {
|
|
1883
|
-
if (filter === 'open') return c.status !== 'resolved';
|
|
1884
|
-
if (filter === 'resolved') return c.status === 'resolved';
|
|
1885
|
-
return true;
|
|
1886
|
-
});
|
|
1887
|
-
if (filtered.length === 0) continue;
|
|
1888
|
-
// Number is fixed by all-list order so it matches pin numbers (which are based on position in the array of selector-having comments)
|
|
1889
|
-
const numberedAll = all.filter((c) => c.selector);
|
|
1890
|
-
groups.push({
|
|
1891
|
-
file: f,
|
|
1892
|
-
comments: filtered.map((c) => ({
|
|
1893
|
-
...c,
|
|
1894
|
-
n: numberedAll.findIndex((x) => x.id === c.id) + 1,
|
|
1895
|
-
})),
|
|
1896
|
-
});
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
return (
|
|
1900
|
-
<aside className="rsidebar">
|
|
1901
|
-
<div className="rsidebar-header">
|
|
1902
|
-
<h2>
|
|
1903
|
-
<span>Comments</span>
|
|
1904
|
-
<span className="total">{counts.all}</span>
|
|
1905
|
-
</h2>
|
|
1906
|
-
<div className="rsidebar-filters" role="tablist">
|
|
1907
|
-
<button
|
|
1908
|
-
className={'rsidebar-filter' + (filter === 'all' ? ' active' : '')}
|
|
1909
|
-
role="tab"
|
|
1910
|
-
aria-selected={filter === 'all'}
|
|
1911
|
-
onClick={() => setFilter('all')}
|
|
1912
|
-
>
|
|
1913
|
-
All <span className="fc">{counts.all}</span>
|
|
1914
|
-
</button>
|
|
1915
|
-
<button
|
|
1916
|
-
className={'rsidebar-filter' + (filter === 'open' ? ' active' : '')}
|
|
1917
|
-
role="tab"
|
|
1918
|
-
aria-selected={filter === 'open'}
|
|
1919
|
-
onClick={() => setFilter('open')}
|
|
1920
|
-
>
|
|
1921
|
-
Open <span className="fc">{counts.open}</span>
|
|
1922
|
-
</button>
|
|
1923
|
-
<button
|
|
1924
|
-
className={'rsidebar-filter' + (filter === 'resolved' ? ' active' : '')}
|
|
1925
|
-
role="tab"
|
|
1926
|
-
aria-selected={filter === 'resolved'}
|
|
1927
|
-
onClick={() => setFilter('resolved')}
|
|
1928
|
-
>
|
|
1929
|
-
Resolved <span className="fc">{counts.resolved}</span>
|
|
1930
|
-
</button>
|
|
1931
|
-
</div>
|
|
1932
|
-
</div>
|
|
1933
|
-
<div className="rsidebar-body">
|
|
1934
|
-
{groups.length === 0 ? (
|
|
1935
|
-
<div className="rsidebar-empty">
|
|
1936
|
-
<p>No comments {filter !== 'all' ? `with status “${filter}”` : 'yet'}.</p>
|
|
1937
|
-
<p style={{ marginTop: 12 }}>
|
|
1938
|
-
Open a canvas, hold <kbd>⌘</kbd> and click an element, then press <kbd>C</kbd> — or
|
|
1939
|
-
hold <kbd>⌘⇧</kbd> and click directly.
|
|
1940
|
-
</p>
|
|
1941
|
-
</div>
|
|
1942
|
-
) : (
|
|
1943
|
-
groups.map((g) => (
|
|
1944
|
-
<div key={g.file} className="rs-group">
|
|
1945
|
-
<button className="rs-group-h" onClick={() => onJump(g.file, null)} title={g.file}>
|
|
1946
|
-
<span className="rs-group-name">{displayName(basename(g.file))}</span>
|
|
1947
|
-
<span className="rs-group-count">{g.comments.length}</span>
|
|
1948
|
-
</button>
|
|
1949
|
-
{g.comments.map((c) => (
|
|
1950
|
-
<div
|
|
1951
|
-
key={c.id}
|
|
1952
|
-
className={
|
|
1953
|
-
'rs-comment' +
|
|
1954
|
-
(c.status === 'resolved' ? ' resolved' : '') +
|
|
1955
|
-
(c.id === focusedId ? ' active-pin' : '')
|
|
1956
|
-
}
|
|
1957
|
-
onClick={() => onJump(g.file, c.id)}
|
|
1958
|
-
>
|
|
1959
|
-
<div className="rs-comment-head">
|
|
1960
|
-
<span className="rs-num">{c.n || '·'}</span>
|
|
1961
|
-
<span className="rs-time">{timeAgo(c.created)}</span>
|
|
1962
|
-
</div>
|
|
1963
|
-
<div className="rs-comment-text">{c.text}</div>
|
|
1964
|
-
<div className="rs-comment-foot">
|
|
1965
|
-
<code title={(c.dom_path || []).join(' > ')}>{c.selector || '—'}</code>
|
|
1966
|
-
<div className="rs-comment-actions">
|
|
1967
|
-
{c.status === 'resolved' ? (
|
|
1968
|
-
<button
|
|
1969
|
-
className="rs-act"
|
|
1970
|
-
onClick={(e) => {
|
|
1971
|
-
e.stopPropagation();
|
|
1972
|
-
onReopen(c.id);
|
|
1973
|
-
}}
|
|
1974
|
-
>
|
|
1975
|
-
↺
|
|
1976
|
-
</button>
|
|
1977
|
-
) : (
|
|
1978
|
-
<button
|
|
1979
|
-
className="rs-act"
|
|
1980
|
-
onClick={(e) => {
|
|
1981
|
-
e.stopPropagation();
|
|
1982
|
-
onResolve(c.id);
|
|
1983
|
-
}}
|
|
1984
|
-
>
|
|
1985
|
-
✓
|
|
1986
|
-
</button>
|
|
1987
|
-
)}
|
|
1988
|
-
<button
|
|
1989
|
-
className="rs-act danger"
|
|
1990
|
-
onClick={(e) => {
|
|
1991
|
-
e.stopPropagation();
|
|
1992
|
-
onDelete(c.id);
|
|
1993
|
-
}}
|
|
1994
|
-
>
|
|
1995
|
-
×
|
|
1996
|
-
</button>
|
|
1997
|
-
</div>
|
|
1998
|
-
</div>
|
|
1999
|
-
</div>
|
|
2000
|
-
))}
|
|
2001
|
-
</div>
|
|
2002
|
-
))
|
|
2003
|
-
)}
|
|
2004
|
-
</div>
|
|
2005
|
-
</aside>
|
|
2006
|
-
);
|
|
2007
|
-
}
|
|
2008
|
-
|
|
2009
|
-
// ---------- Sync banner (Phase 9 Task 8 — hub-down offline mode) ----------
|
|
2010
|
-
|
|
2011
|
-
// Renders nothing when online with no flash (the common case). Yellow strip
|
|
2012
|
-
// while offline (with queued-edit count), red when offline > 24h, green flash
|
|
2013
|
-
// for 3s right after a reconnect. Driven entirely by the 'sync:status' payload
|
|
2014
|
-
// the dev-server's linked-mode sync runtime broadcasts.
|
|
2015
|
-
function SyncBanner({ status }) {
|
|
2016
|
-
if (!status || status.linked === false) return null;
|
|
2017
|
-
// DDR-060 / 9.1-D — the "linked but 0 syncable" state is surfaced in the
|
|
2018
|
-
// status bar (sb-sync slot), NOT as a floating banner. This component owns
|
|
2019
|
-
// only the transient offline / reconnect-flash banner (Task 8).
|
|
2020
|
-
if (status.notSyncable) return null;
|
|
2021
|
-
const { state, queuedOps, flash, conflicts } = status;
|
|
2022
|
-
const showFlash = flash === 'synced';
|
|
2023
|
-
const offline = state === 'offline' || state === 'offline-long';
|
|
2024
|
-
if (!offline && !showFlash) return null;
|
|
2025
|
-
|
|
2026
|
-
let bg;
|
|
2027
|
-
let fg;
|
|
2028
|
-
let border;
|
|
2029
|
-
let text;
|
|
2030
|
-
if (showFlash) {
|
|
2031
|
-
bg = '#dcfce7';
|
|
2032
|
-
fg = '#166534';
|
|
2033
|
-
border = '#86efac';
|
|
2034
|
-
text = 'Synced with hub';
|
|
2035
|
-
} else if (state === 'offline-long') {
|
|
2036
|
-
bg = '#fee2e2';
|
|
2037
|
-
fg = '#991b1b';
|
|
2038
|
-
border = '#fca5a5';
|
|
2039
|
-
text = `Long offline — ${queuedOps} edit(s) queued. Consider \`git commit && git push\` as backup.`;
|
|
2040
|
-
} else {
|
|
2041
|
-
bg = '#fef9c3';
|
|
2042
|
-
fg = '#854d0e';
|
|
2043
|
-
border = '#fde047';
|
|
2044
|
-
text = `Working offline · ${queuedOps} edit(s) queued · will sync when the hub reconnects.`;
|
|
2045
|
-
}
|
|
2046
|
-
const conflictNote =
|
|
2047
|
-
conflicts && conflicts.length > 0 ? ` (${conflicts.length} conflict notice(s))` : '';
|
|
2048
|
-
|
|
2049
|
-
return (
|
|
2050
|
-
<div
|
|
2051
|
-
role="status"
|
|
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
|
-
>
|
|
2072
|
-
<span>
|
|
2073
|
-
{text}
|
|
2074
|
-
{conflictNote}
|
|
2075
|
-
</span>
|
|
2076
|
-
</div>
|
|
2077
|
-
);
|
|
2078
|
-
}
|
|
2079
|
-
|
|
2080
|
-
// ---------- App ----------
|
|
2081
|
-
|
|
2082
|
-
function App() {
|
|
2083
|
-
const [groups, setGroups] = useState([]);
|
|
2084
|
-
const [project, setProject] = useState('Design');
|
|
2085
|
-
const [tabs, setTabs] = useState([]);
|
|
2086
|
-
const [activePath, setActivePath] = useState(null);
|
|
2087
|
-
const [selected, setSelected] = useState(null);
|
|
2088
|
-
const [wsConnected, setWsConnected] = useState(false);
|
|
2089
|
-
// Phase 8 Task 7 — git lifecycle reload prompt. Server has already flushed
|
|
2090
|
-
// every dirty Y.Doc to disk by the time this state populates, so accepting
|
|
2091
|
-
// the reload is data-loss-safe (DDR-051 §3).
|
|
2092
|
-
const [gitLifecycle, setGitLifecycle] = useState(null);
|
|
2093
|
-
// Phase 9 Task 8 — hub-down offline mode banner. Driven by the 'sync:status'
|
|
2094
|
-
// WS message the linked-mode sync runtime emits. null in solo mode.
|
|
2095
|
-
const [syncStatus, setSyncStatus] = useState(null);
|
|
2096
|
-
const [search, setSearch] = useState('');
|
|
2097
|
-
const [systemData, setSystemData] = useState(null);
|
|
2098
|
-
// Loaded once at boot from /_config — informs canvasUrl() so TSX iframes
|
|
2099
|
-
// can pass the right ?designRel + ?tokens query to the canvas mount shell.
|
|
2100
|
-
const [cfg, setCfg] = useState({ designRel: '.design' });
|
|
2101
|
-
useEffect(() => {
|
|
2102
|
-
let cancelled = false;
|
|
2103
|
-
fetch('/_config')
|
|
2104
|
-
.then((r) => r.json())
|
|
2105
|
-
.then((data) => {
|
|
2106
|
-
if (cancelled) return;
|
|
2107
|
-
const designRel = (data.designRoot || '.design').replace(/^\/+|\/+$/g, '');
|
|
2108
|
-
setCfg({
|
|
2109
|
-
designRel,
|
|
2110
|
-
tokensCssRel: data.tokensCssRel,
|
|
2111
|
-
// Pass through designSystems so canvasUrl can resolve the right
|
|
2112
|
-
// tokens/components paths per-DS. Top-level tokensCssRel is the
|
|
2113
|
-
// legacy default; designSystems[0].tokensCssRel is the project's
|
|
2114
|
-
// authoritative value (post DS-bootstrap).
|
|
2115
|
-
designSystems: data.designSystems,
|
|
2116
|
-
// T2 (9.1-A) — segregated canvas-content origin. canvasUrl() prepends
|
|
2117
|
-
// it so iframes load cross-origin (hub-pushed JSX is then walled off
|
|
2118
|
-
// from the main origin's /_api). Absent on older servers → relative
|
|
2119
|
-
// URL fallback keeps same-origin behavior.
|
|
2120
|
-
canvasOrigin: data.canvasOrigin,
|
|
2121
|
-
});
|
|
2122
|
-
})
|
|
2123
|
-
.catch(() => {});
|
|
2124
|
-
return () => {
|
|
2125
|
-
cancelled = true;
|
|
2126
|
-
};
|
|
2127
|
-
}, []);
|
|
2128
|
-
// Backfill the sync banner on mount from /_sync-status. The 'sync:status' WS
|
|
2129
|
-
// broadcast is one-shot for the zero-syncable case (DDR-060 / 9.1-D), so a
|
|
2130
|
-
// tab that connects after boot would otherwise miss it. {linked:false} (solo)
|
|
2131
|
-
// leaves the banner null.
|
|
2132
|
-
useEffect(() => {
|
|
2133
|
-
let cancelled = false;
|
|
2134
|
-
fetch('/_sync-status')
|
|
2135
|
-
.then((r) => r.json())
|
|
2136
|
-
.then((data) => {
|
|
2137
|
-
if (cancelled || !data || data.linked === false) return;
|
|
2138
|
-
setSyncStatus(data);
|
|
2139
|
-
})
|
|
2140
|
-
.catch(() => {});
|
|
2141
|
-
return () => {
|
|
2142
|
-
cancelled = true;
|
|
2143
|
-
};
|
|
2144
|
-
}, []);
|
|
2145
|
-
const [commentsByFile, setCommentsByFile] = useState({}); // { file: [Comment] }
|
|
2146
|
-
// Phase 6 — the in-iframe composer owns drafting; the shell no longer holds
|
|
2147
|
-
// a `draft` state. Mutations route through postMessage → WS instead.
|
|
2148
|
-
const [focusedCommentId, setFocusedCommentId] = useState(null);
|
|
2149
|
-
const [commentsPanelOpen, setCommentsPanelOpen] = useState(false);
|
|
2150
|
-
const [commentsFilter, setCommentsFilter] = useState('open'); // 'all' | 'open' | 'resolved'
|
|
2151
|
-
const [theme, setTheme] = useState(readInitialTheme);
|
|
2152
|
-
const [openMenu, setOpenMenu] = useState(null);
|
|
2153
|
-
const [sidebarOpen, setSidebarOpen] = useState(() => readBoolStore(SIDEBAR_STORE, true));
|
|
2154
|
-
const [showHidden, setShowHidden] = useState(() => readBoolStore(SHOW_HIDDEN_STORE, false));
|
|
2155
|
-
const [sectionsExpanded, setSectionsExpanded] = useState(() => readJsonStore(SECTIONS_STORE, {}));
|
|
2156
|
-
const [helpOpen, setHelpOpen] = useState(false);
|
|
2157
|
-
const [annotationsVisible, setAnnotationsVisible] = useState(true);
|
|
2158
|
-
const wsRef = useRef(null);
|
|
2159
|
-
const iframesRef = useRef(new Map());
|
|
2160
|
-
|
|
2161
|
-
// Phase 5.1 — postMessage bridge from menubar dropdowns to the canvas iframe.
|
|
2162
|
-
// The canvas-shell listens for these `dgn:*` messages and dispatches into the
|
|
2163
|
-
// matching local provider (annotations visibility / both selection stores /
|
|
2164
|
-
// tool mode). Mirrors the existing `force-clear` / `select-clear` channel.
|
|
2165
|
-
const postToActiveCanvas = useCallback(
|
|
2166
|
-
(payload) => {
|
|
2167
|
-
const el = activePath ? iframesRef.current.get(activePath) : null;
|
|
2168
|
-
if (!el || !el.contentWindow) return;
|
|
2169
|
-
try {
|
|
2170
|
-
el.contentWindow.postMessage(payload, '*');
|
|
2171
|
-
} catch {}
|
|
2172
|
-
},
|
|
2173
|
-
[activePath]
|
|
2174
|
-
);
|
|
2175
|
-
|
|
2176
|
-
const toggleAnnotations = useCallback(() => {
|
|
2177
|
-
setAnnotationsVisible((v) => {
|
|
2178
|
-
const next = !v;
|
|
2179
|
-
const el = activePath ? iframesRef.current.get(activePath) : null;
|
|
2180
|
-
if (el && el.contentWindow) {
|
|
2181
|
-
try {
|
|
2182
|
-
el.contentWindow.postMessage({ dgn: 'view-annotations', visible: next }, '*');
|
|
2183
|
-
} catch {}
|
|
2184
|
-
}
|
|
2185
|
-
return next;
|
|
2186
|
-
});
|
|
2187
|
-
}, [activePath]);
|
|
2188
|
-
|
|
2189
|
-
// Sync theme to <html data-theme> + localStorage on every change.
|
|
2190
|
-
useEffect(() => {
|
|
2191
|
-
try {
|
|
2192
|
-
document.documentElement.setAttribute('data-theme', theme);
|
|
2193
|
-
localStorage.setItem(THEME_STORE, theme);
|
|
2194
|
-
} catch {}
|
|
2195
|
-
// System-review D9 — the canvas-shell chrome (workspace plane, floating
|
|
2196
|
-
// toolbar, minimap, zoom HUD, halos) follows the Maude theme. Broadcast to
|
|
2197
|
-
// EVERY open canvas iframe (not just activePath — several may be open); the
|
|
2198
|
-
// iframe's canvas-shell sets `data-maude-theme` and re-themes its floating
|
|
2199
|
-
// chrome via the --maude-chrome-* family. Artboards keep their DS theme.
|
|
2200
|
-
// Mirrors the git-lifecycle broadcast-to-all loop below. On the initial
|
|
2201
|
-
// mount run iframesRef is empty (no canvas open yet) — a freshly-loaded
|
|
2202
|
-
// iframe instead gets the current theme from the `dgn:'loaded'` handler.
|
|
2203
|
-
for (const el of iframesRef.current.values()) {
|
|
2204
|
-
try {
|
|
2205
|
-
el.contentWindow.postMessage({ dgn: 'theme', theme }, '*');
|
|
2206
|
-
} catch {}
|
|
2207
|
-
}
|
|
2208
|
-
}, [theme]);
|
|
2209
|
-
|
|
2210
|
-
// Persist sidebar / hidden-files / DS-body toggles. Mirror theme pattern.
|
|
2211
|
-
useEffect(() => {
|
|
2212
|
-
try {
|
|
2213
|
-
localStorage.setItem(SIDEBAR_STORE, sidebarOpen ? '1' : '0');
|
|
2214
|
-
} catch {}
|
|
2215
|
-
}, [sidebarOpen]);
|
|
2216
|
-
useEffect(() => {
|
|
2217
|
-
try {
|
|
2218
|
-
localStorage.setItem(SHOW_HIDDEN_STORE, showHidden ? '1' : '0');
|
|
2219
|
-
} catch {}
|
|
2220
|
-
}, [showHidden]);
|
|
2221
|
-
useEffect(() => {
|
|
2222
|
-
try {
|
|
2223
|
-
localStorage.setItem(SECTIONS_STORE, JSON.stringify(sectionsExpanded));
|
|
2224
|
-
} catch {}
|
|
2225
|
-
}, [sectionsExpanded]);
|
|
2226
|
-
|
|
2227
|
-
const toggleSection = useCallback((label, defaultOpen) => {
|
|
2228
|
-
setSectionsExpanded((prev) => {
|
|
2229
|
-
const cur = prev[label];
|
|
2230
|
-
const isOpen = cur === undefined ? defaultOpen : cur;
|
|
2231
|
-
return { ...prev, [label]: !isOpen };
|
|
2232
|
-
});
|
|
2233
|
-
}, []);
|
|
2234
|
-
|
|
2235
|
-
const toggleTheme = useCallback(() => {
|
|
2236
|
-
setTheme((t) => (t === 'dark' ? 'light' : 'dark'));
|
|
2237
|
-
}, []);
|
|
2238
|
-
|
|
2239
|
-
// ----- Tree -----
|
|
2240
|
-
const loadTree = useCallback(async () => {
|
|
2241
|
-
try {
|
|
2242
|
-
const r = await fetch('/_index-data');
|
|
2243
|
-
const data = await r.json();
|
|
2244
|
-
setProject(data.project || 'Design');
|
|
2245
|
-
const built = data.groups.map((g) => ({
|
|
2246
|
-
...g,
|
|
2247
|
-
tree: buildTree(g.paths, g.stripPrefix),
|
|
2248
|
-
}));
|
|
2249
|
-
setGroups(built);
|
|
2250
|
-
} catch (e) {
|
|
2251
|
-
console.error('failed to load tree', e);
|
|
2252
|
-
}
|
|
2253
|
-
}, []);
|
|
2254
|
-
|
|
2255
|
-
useEffect(() => {
|
|
2256
|
-
loadTree();
|
|
2257
|
-
}, [loadTree]);
|
|
2258
|
-
|
|
2259
|
-
// ----- System data (lazy) -----
|
|
2260
|
-
// `dsName` scopes to a single design-system entry (DDR-048). The initial
|
|
2261
|
-
// call is unscoped — server returns `availableDesignSystems[]` + a default
|
|
2262
|
-
// — so the picker can render without a probe round-trip. Subsequent calls
|
|
2263
|
-
// (e.g. picker change) pass the chosen DS name and we replace systemData
|
|
2264
|
-
// wholesale (tokens + previews + ds metadata all shift together).
|
|
2265
|
-
const loadSystemData = useCallback(async (dsName) => {
|
|
2266
|
-
try {
|
|
2267
|
-
const url = dsName ? `/_system-data?ds=${encodeURIComponent(dsName)}` : '/_system-data';
|
|
2268
|
-
const r = await fetch(url);
|
|
2269
|
-
if (!r.ok) {
|
|
2270
|
-
console.error('failed to load system-data', r.status);
|
|
2271
|
-
return;
|
|
2272
|
-
}
|
|
2273
|
-
const data = await r.json();
|
|
2274
|
-
// If the initial unscoped fetch has a defaultDesignSystem but no `ds`
|
|
2275
|
-
// attached (multi-DS project), kick off a scoped fetch so the visible
|
|
2276
|
-
// tokens + previews match the default DS, not the union root scan.
|
|
2277
|
-
if (!dsName && data?.defaultDesignSystem && !data.ds) {
|
|
2278
|
-
setSystemData(data);
|
|
2279
|
-
const r2 = await fetch(`/_system-data?ds=${encodeURIComponent(data.defaultDesignSystem)}`);
|
|
2280
|
-
if (r2.ok) setSystemData(await r2.json());
|
|
2281
|
-
return;
|
|
2282
|
-
}
|
|
2283
|
-
setSystemData(data);
|
|
2284
|
-
} catch (e) {
|
|
2285
|
-
console.error('failed to load system-data', e);
|
|
2286
|
-
}
|
|
2287
|
-
}, []);
|
|
2288
|
-
|
|
2289
|
-
// ----- Comments — initial load of all files -----
|
|
2290
|
-
const loadAllComments = useCallback(async () => {
|
|
2291
|
-
try {
|
|
2292
|
-
const r = await fetch('/_comments-all');
|
|
2293
|
-
const data = await r.json();
|
|
2294
|
-
setCommentsByFile(data || {});
|
|
2295
|
-
} catch (e) {
|
|
2296
|
-
console.error('failed to load comments', e);
|
|
2297
|
-
}
|
|
2298
|
-
}, []);
|
|
2299
|
-
|
|
2300
|
-
useEffect(() => {
|
|
2301
|
-
loadAllComments();
|
|
2302
|
-
}, [loadAllComments]);
|
|
2303
|
-
|
|
2304
|
-
// ----- WebSocket -----
|
|
2305
|
-
useEffect(() => {
|
|
2306
|
-
function connect() {
|
|
2307
|
-
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
2308
|
-
const ws = new WebSocket(proto + '//' + location.host + '/_ws');
|
|
2309
|
-
wsRef.current = ws;
|
|
2310
|
-
ws.addEventListener('open', () => setWsConnected(true));
|
|
2311
|
-
ws.addEventListener('close', () => {
|
|
2312
|
-
setWsConnected(false);
|
|
2313
|
-
setTimeout(connect, 1000);
|
|
2314
|
-
});
|
|
2315
|
-
ws.addEventListener('error', () => {});
|
|
2316
|
-
ws.addEventListener('message', (e) => {
|
|
2317
|
-
try {
|
|
2318
|
-
const m = JSON.parse(e.data);
|
|
2319
|
-
if (m.type === 'snapshot' && m.state) {
|
|
2320
|
-
setSelected(m.state.selected);
|
|
2321
|
-
} else if (m.type === 'selected') {
|
|
2322
|
-
setSelected(m.selected);
|
|
2323
|
-
} else if (m.type === 'comments' && typeof m.file === 'string') {
|
|
2324
|
-
setCommentsByFile((prev) => ({ ...prev, [m.file]: m.comments || [] }));
|
|
2325
|
-
} else if (m.type === 'ai-activity' && typeof m.file === 'string') {
|
|
2326
|
-
// Phase 8 Task 4 — relay to every open iframe; each canvas's
|
|
2327
|
-
// AiBanner filters by its own file path. Lightweight broadcast
|
|
2328
|
-
// (one envelope per change, not per iframe count).
|
|
2329
|
-
for (const el of iframesRef.current.values()) {
|
|
2330
|
-
try {
|
|
2331
|
-
el.contentWindow.postMessage(
|
|
2332
|
-
{ dgn: 'ai-activity', file: m.file, entry: m.entry },
|
|
2333
|
-
'*'
|
|
2334
|
-
);
|
|
2335
|
-
} catch {}
|
|
2336
|
-
}
|
|
2337
|
-
} else if (m.type === 'sync:status' && m.payload) {
|
|
2338
|
-
// Phase 9 Task 8 — hub connection state for the offline banner.
|
|
2339
|
-
setSyncStatus(m.payload);
|
|
2340
|
-
} else if (m.type === 'git-lifecycle' && m.payload) {
|
|
2341
|
-
// Phase 8 Task 7 — branch switch / pull mid-session. Server has
|
|
2342
|
-
// already flushed every dirty Y.Doc to JSON; just prompt the user.
|
|
2343
|
-
// Single confirm covers all open iframes — reload reseeds them all.
|
|
2344
|
-
setGitLifecycle(m.payload);
|
|
2345
|
-
// Also relay to iframes so canvas-level "Reload?" UI (if any)
|
|
2346
|
-
// can react. Outer banner is the primary prompt.
|
|
2347
|
-
for (const el of iframesRef.current.values()) {
|
|
2348
|
-
try {
|
|
2349
|
-
el.contentWindow.postMessage({ dgn: 'git-lifecycle', payload: m.payload }, '*');
|
|
2350
|
-
} catch {}
|
|
2351
|
-
}
|
|
2352
|
-
}
|
|
2353
|
-
} catch {}
|
|
2354
|
-
});
|
|
2355
|
-
}
|
|
2356
|
-
connect();
|
|
2357
|
-
return () => wsRef.current && wsRef.current.close();
|
|
2358
|
-
}, []);
|
|
2359
|
-
|
|
2360
|
-
function wsSend(obj) {
|
|
2361
|
-
const ws = wsRef.current;
|
|
2362
|
-
try {
|
|
2363
|
-
if (ws && ws.readyState === 1) ws.send(JSON.stringify(obj));
|
|
2364
|
-
} catch {}
|
|
2365
|
-
}
|
|
2366
|
-
|
|
2367
|
-
// ----- Tab management (single-canvas) -----
|
|
2368
|
-
// Single-canvas model: opening a file REPLACES the active one (no tab strip).
|
|
2369
|
-
// The `tabs` state stays as a 0-or-1 array so the rest of the plumbing
|
|
2370
|
-
// (iframesRef, comments push, WS `tabs` message) doesn't need refactoring.
|
|
2371
|
-
// ARTBOARDS slot in the menubar reads `tabs.length` and reports 0 or 1.
|
|
2372
|
-
const openTab = useCallback((path) => {
|
|
2373
|
-
setTabs((prev) => {
|
|
2374
|
-
// Drop the previously-open iframe so we don't leak DOM nodes.
|
|
2375
|
-
for (const t of prev) if (t.path !== path) iframesRef.current.delete(t.path);
|
|
2376
|
-
return [{ path }];
|
|
2377
|
-
});
|
|
2378
|
-
setActivePath(path);
|
|
2379
|
-
setFocusedCommentId(null);
|
|
2380
|
-
}, []);
|
|
2381
|
-
|
|
2382
|
-
const openSystem = useCallback(
|
|
2383
|
-
(dsName) => {
|
|
2384
|
-
// DsFolderRow passes the clicked DS name → scope the System view to it so
|
|
2385
|
-
// each folder shows its own tokens + previews. The no-arg callers (menubar,
|
|
2386
|
-
// keyboard reopen) only load default data on first open.
|
|
2387
|
-
const ds = typeof dsName === 'string' ? dsName : undefined;
|
|
2388
|
-
if (ds) loadSystemData(ds);
|
|
2389
|
-
else if (!systemData) loadSystemData();
|
|
2390
|
-
openTab(SYSTEM_TAB);
|
|
2391
|
-
},
|
|
2392
|
-
[systemData, loadSystemData, openTab]
|
|
2393
|
-
);
|
|
2394
|
-
|
|
2395
|
-
useEffect(() => {
|
|
2396
|
-
wsSend({ type: 'tabs', tabs: tabs.map((t) => t.path).filter((p) => p !== SYSTEM_TAB) });
|
|
2397
|
-
}, [tabs]);
|
|
2398
|
-
|
|
2399
|
-
useEffect(() => {
|
|
2400
|
-
if (activePath && activePath !== SYSTEM_TAB) wsSend({ type: 'active', file: activePath });
|
|
2401
|
-
else if (activePath === SYSTEM_TAB) wsSend({ type: 'active', file: '' });
|
|
2402
|
-
else wsSend({ type: 'active', file: '' });
|
|
2403
|
-
}, [activePath]);
|
|
2404
|
-
|
|
2405
|
-
const closeTab = useCallback(
|
|
2406
|
-
(path) => {
|
|
2407
|
-
setTabs((prev) => {
|
|
2408
|
-
const idx = prev.findIndex((t) => t.path === path);
|
|
2409
|
-
if (idx < 0) return prev;
|
|
2410
|
-
const next = prev.filter((t) => t.path !== path);
|
|
2411
|
-
if (path === activePath) {
|
|
2412
|
-
if (next.length === 0) setActivePath(null);
|
|
2413
|
-
else setActivePath(next[Math.max(0, idx - 1)].path);
|
|
2414
|
-
}
|
|
2415
|
-
return next;
|
|
2416
|
-
});
|
|
2417
|
-
iframesRef.current.delete(path);
|
|
2418
|
-
},
|
|
2419
|
-
[activePath]
|
|
2420
|
-
);
|
|
2421
|
-
|
|
2422
|
-
const reloadActive = useCallback(() => {
|
|
2423
|
-
if (!activePath || activePath === SYSTEM_TAB) {
|
|
2424
|
-
if (activePath === SYSTEM_TAB) loadSystemData();
|
|
2425
|
-
return;
|
|
2426
|
-
}
|
|
2427
|
-
const el = iframesRef.current.get(activePath);
|
|
2428
|
-
if (el) el.src = el.src;
|
|
2429
|
-
}, [activePath, loadSystemData]);
|
|
2430
|
-
|
|
2431
|
-
const reloadTree = useCallback(() => loadTree(), [loadTree]);
|
|
2432
|
-
|
|
2433
|
-
const clearSelected = useCallback(() => {
|
|
2434
|
-
wsSend({ type: 'clear-select' });
|
|
2435
|
-
setSelected(null);
|
|
2436
|
-
if (activePath && activePath !== SYSTEM_TAB) {
|
|
2437
|
-
const el = iframesRef.current.get(activePath);
|
|
2438
|
-
if (el && el.contentWindow) {
|
|
2439
|
-
try {
|
|
2440
|
-
el.contentWindow.postMessage({ dgn: 'force-clear' }, '*');
|
|
2441
|
-
} catch {}
|
|
2442
|
-
}
|
|
2443
|
-
}
|
|
2444
|
-
}, [activePath]);
|
|
2445
|
-
|
|
2446
|
-
// ----- Push comments to iframe whenever they change for active file -----
|
|
2447
|
-
useEffect(() => {
|
|
2448
|
-
if (!activePath || activePath === SYSTEM_TAB) return;
|
|
2449
|
-
const el = iframesRef.current.get(activePath);
|
|
2450
|
-
if (!el || !el.contentWindow) return;
|
|
2451
|
-
const list = commentsByFile[activePath] || [];
|
|
2452
|
-
try {
|
|
2453
|
-
el.contentWindow.postMessage({ dgn: 'comments-set', comments: list }, '*');
|
|
2454
|
-
} catch {}
|
|
2455
|
-
}, [activePath, commentsByFile]);
|
|
2456
|
-
|
|
2457
|
-
// ----- Inbound messages from iframes -----
|
|
2458
|
-
useEffect(() => {
|
|
2459
|
-
function onMessage(e) {
|
|
2460
|
-
// Cross-origin hardening (DDR-054): only accept dgn control messages from
|
|
2461
|
-
// the canvas-content origin — the split origin when on, else our own origin
|
|
2462
|
-
// for the same-origin iframe. Drops spoofed messages from any other window.
|
|
2463
|
-
// The handlers below relay to inert stores (comments / selection — the
|
|
2464
|
-
// "safe to sync" set), so the blast radius was small, but unchecked inbound
|
|
2465
|
-
// postMessage is a confused-deputy seam the F1 hardening should close.
|
|
2466
|
-
const expectedOrigin = cfg?.canvasOrigin || window.location.origin;
|
|
2467
|
-
if (e.origin !== expectedOrigin) return;
|
|
2468
|
-
const m = e.data;
|
|
2469
|
-
if (!m || typeof m !== 'object' || !m.dgn) return;
|
|
2470
|
-
if (m.dgn === 'tool-cursor') {
|
|
2471
|
-
// Phase 24 — show the active canvas tool's cursor across the WHOLE app
|
|
2472
|
-
// shell (sidebar, top bar, everything) so the custom cursor is visible
|
|
2473
|
-
// everywhere in maude, not just inside the canvas iframe. The canvas
|
|
2474
|
-
// sends only a tool TOKEN; we resolve it to a cursor string from our own
|
|
2475
|
-
// trusted map (resolveToolCursor) and apply THAT — never a raw
|
|
2476
|
-
// canvas-supplied value. A malicious synced canvas (DDR-054) can thus
|
|
2477
|
-
// only pick a known, always-visible glyph; it cannot inject an
|
|
2478
|
-
// invisible/displaced SVG cursor as a clickjacking aid over the un-CSP'd
|
|
2479
|
-
// shell (phase-24 ethical-hacker Finding 2; DDR-067).
|
|
2480
|
-
const cursor = resolveToolCursor(m.tool);
|
|
2481
|
-
if (cursor) {
|
|
2482
|
-
document.body.style.cursor = cursor;
|
|
2483
|
-
let el = document.getElementById('dc-app-cursor');
|
|
2484
|
-
if (!el) {
|
|
2485
|
-
el = document.createElement('style');
|
|
2486
|
-
el.id = 'dc-app-cursor';
|
|
2487
|
-
document.head.appendChild(el);
|
|
2488
|
-
}
|
|
2489
|
-
el.textContent = `* { cursor: ${cursor} !important; }`;
|
|
2490
|
-
}
|
|
2491
|
-
return;
|
|
2492
|
-
}
|
|
2493
|
-
if (m.dgn === 'select' && m.selection) {
|
|
2494
|
-
wsSend({ type: 'select', selection: m.selection });
|
|
2495
|
-
setSelected(m.selection);
|
|
2496
|
-
} else if (m.dgn === 'select-set') {
|
|
2497
|
-
// Canvas multi-select. Payload shape:
|
|
2498
|
-
// null → empty selection
|
|
2499
|
-
// Selection → length-1 (back-compat with legacy single-element shape)
|
|
2500
|
-
// Selection[] → N > 1
|
|
2501
|
-
// For shell purposes we track the focused entry (head of array, or
|
|
2502
|
-
// the bare object) — comments + halo only act on one element at a
|
|
2503
|
-
// time today. Multi-target editing is an explicit Phase-4.1 non-goal.
|
|
2504
|
-
const payload = m.selection;
|
|
2505
|
-
if (payload == null) {
|
|
2506
|
-
wsSend({ type: 'clear-select' });
|
|
2507
|
-
setSelected(null);
|
|
2508
|
-
} else if (Array.isArray(payload)) {
|
|
2509
|
-
const head = payload[0] ?? null;
|
|
2510
|
-
if (head) wsSend({ type: 'select', selection: head });
|
|
2511
|
-
setSelected(head);
|
|
2512
|
-
} else {
|
|
2513
|
-
wsSend({ type: 'select', selection: payload });
|
|
2514
|
-
setSelected(payload);
|
|
2515
|
-
}
|
|
2516
|
-
} else if (m.dgn === 'clear-select') {
|
|
2517
|
-
wsSend({ type: 'clear-select' });
|
|
2518
|
-
setSelected(null);
|
|
2519
|
-
} else if (m.dgn === 'comment-compose' && m.selection) {
|
|
2520
|
-
// Phase 6 — the iframe overlay owns the composer surface now. The
|
|
2521
|
-
// shell just mirrors `selected` so the StatusBar / sidebar still
|
|
2522
|
-
// reflect the target, and skips the legacy `startDraftFor` path that
|
|
2523
|
-
// opened the shell-side composer. Legacy `.html` mocks (no
|
|
2524
|
-
// canvas-shell mount) fall through to the same path; they lose the
|
|
2525
|
-
// shell composer in this phase. Acceptable per Phase 6 scope.
|
|
2526
|
-
setSelected(m.selection);
|
|
2527
|
-
} else if (m.dgn === 'comment-submit' && m.payload && typeof m.payload.text === 'string') {
|
|
2528
|
-
// Phase 6 — iframe overlay finished composing. Relay through the
|
|
2529
|
-
// existing WS `comments-add` channel; server-side persistence +
|
|
2530
|
-
// broadcast back are identical to the legacy shell-composer flow.
|
|
2531
|
-
const p = m.payload;
|
|
2532
|
-
const txt = String(p.text).trim();
|
|
2533
|
-
if (txt) {
|
|
2534
|
-
wsSend({
|
|
2535
|
-
type: 'comments-add',
|
|
2536
|
-
payload: {
|
|
2537
|
-
file: p.file,
|
|
2538
|
-
selector: p.selector,
|
|
2539
|
-
dom_path: p.dom_path,
|
|
2540
|
-
tag: p.tag,
|
|
2541
|
-
classes: p.classes,
|
|
2542
|
-
bounds: p.bounds,
|
|
2543
|
-
html_excerpt: p.html_excerpt,
|
|
2544
|
-
text: txt,
|
|
2545
|
-
},
|
|
2546
|
-
});
|
|
2547
|
-
}
|
|
2548
|
-
} else if (m.dgn === 'comment-patch' && m.id && m.patch && typeof m.patch === 'object') {
|
|
2549
|
-
// Phase 6 — thread popover routes resolve / reopen through here.
|
|
2550
|
-
wsSend({ type: 'comments-patch', id: m.id, patch: m.patch });
|
|
2551
|
-
} else if (m.dgn === 'comment-delete' && m.id) {
|
|
2552
|
-
wsSend({ type: 'comments-delete', id: m.id });
|
|
2553
|
-
setFocusedCommentId((prev) => (prev === m.id ? null : prev));
|
|
2554
|
-
} else if (m.dgn === 'comment-click' && m.id) {
|
|
2555
|
-
setFocusedCommentId(m.id);
|
|
2556
|
-
} else if (m.dgn === 'loaded' && m.file) {
|
|
2557
|
-
// iframe finished loading — push current comments + carry over focused pin if any
|
|
2558
|
-
const list = commentsByFile[m.file] || [];
|
|
2559
|
-
const el = [...iframesRef.current.entries()].find(([k]) => k === m.file)?.[1];
|
|
2560
|
-
if (el && el.contentWindow) {
|
|
2561
|
-
try {
|
|
2562
|
-
el.contentWindow.postMessage({ dgn: 'comments-set', comments: list }, '*');
|
|
2563
|
-
} catch {}
|
|
2564
|
-
// System-review D9 — seed the just-loaded canvas with the current
|
|
2565
|
-
// chrome theme so a canvas opened AFTER a theme toggle starts
|
|
2566
|
-
// correct (no flash from the dark default).
|
|
2567
|
-
try {
|
|
2568
|
-
el.contentWindow.postMessage({ dgn: 'theme', theme }, '*');
|
|
2569
|
-
} catch {}
|
|
2570
|
-
if (focusedCommentId && list.some((c) => c.id === focusedCommentId)) {
|
|
2571
|
-
try {
|
|
2572
|
-
el.contentWindow.postMessage({ dgn: 'comment-focus', id: focusedCommentId }, '*');
|
|
2573
|
-
} catch {}
|
|
2574
|
-
}
|
|
2575
|
-
}
|
|
2576
|
-
} else if (m.dgn === 'export-request' && m.id && m.payload) {
|
|
2577
|
-
// The export dialog renders inside the canvas iframe (canvas origin),
|
|
2578
|
-
// but /_api/export is a privileged MAIN-origin endpoint deliberately
|
|
2579
|
-
// kept off the canvas allowlist (DDR-060). A direct in-iframe fetch
|
|
2580
|
-
// therefore 403s ("Forbidden (canvas origin)"). Bridge it: run the
|
|
2581
|
-
// export here on the trusted main origin, stream the download, and
|
|
2582
|
-
// report status back to the iframe. Origin is already validated
|
|
2583
|
-
// (e.origin === expectedOrigin) above, so only the real canvas iframe
|
|
2584
|
-
// can ask — this is NOT a generic fetch proxy.
|
|
2585
|
-
void runBridgedExport(e.source, m.id, m.payload);
|
|
2586
|
-
} else if (m.dgn === 'export-history-request' && m.id) {
|
|
2587
|
-
// Same bridge for the dialog's Recent tab (/_api/export-history is
|
|
2588
|
-
// also main-origin-only).
|
|
2589
|
-
void runBridgedHistory(e.source, m.id);
|
|
2590
|
-
}
|
|
2591
|
-
}
|
|
2592
|
-
// Reply target for the export bridge: the canvas iframe's own origin.
|
|
2593
|
-
const replyOrigin = cfg?.canvasOrigin || window.location.origin;
|
|
2594
|
-
async function runBridgedExport(source, id, payload) {
|
|
2595
|
-
const reply = (msg) => {
|
|
2596
|
-
try {
|
|
2597
|
-
if (source) source.postMessage({ dgn: 'export-result', id, ...msg }, replyOrigin);
|
|
2598
|
-
} catch {}
|
|
2599
|
-
};
|
|
2600
|
-
try {
|
|
2601
|
-
const r = await fetch('/_api/export', {
|
|
2602
|
-
method: 'POST',
|
|
2603
|
-
headers: { 'content-type': 'application/json' },
|
|
2604
|
-
body: JSON.stringify(payload),
|
|
2605
|
-
});
|
|
2606
|
-
if (!r.ok) {
|
|
2607
|
-
reply({ ok: false, error: (await r.text()) || String(r.status) });
|
|
2608
|
-
return;
|
|
2609
|
-
}
|
|
2610
|
-
const disp = r.headers.get('Content-Disposition') || '';
|
|
2611
|
-
const fn = /filename="([^"]+)"/.exec(disp);
|
|
2612
|
-
const filename = (fn && fn[1]) || 'export';
|
|
2613
|
-
const blob = await r.blob();
|
|
2614
|
-
const url = URL.createObjectURL(blob);
|
|
2615
|
-
const a = document.createElement('a');
|
|
2616
|
-
a.href = url;
|
|
2617
|
-
a.download = filename;
|
|
2618
|
-
document.body.appendChild(a);
|
|
2619
|
-
a.click();
|
|
2620
|
-
a.remove();
|
|
2621
|
-
URL.revokeObjectURL(url);
|
|
2622
|
-
reply({ ok: true, filename });
|
|
2623
|
-
} catch (err) {
|
|
2624
|
-
reply({ ok: false, error: err && err.message ? err.message : String(err) });
|
|
2625
|
-
}
|
|
2626
|
-
}
|
|
2627
|
-
async function runBridgedHistory(source, id) {
|
|
2628
|
-
let history = [];
|
|
2629
|
-
try {
|
|
2630
|
-
const r = await fetch('/_api/export-history');
|
|
2631
|
-
if (r.ok) {
|
|
2632
|
-
const data = await r.json();
|
|
2633
|
-
if (Array.isArray(data.history)) history = data.history;
|
|
2634
|
-
}
|
|
2635
|
-
} catch {
|
|
2636
|
-
/* best-effort — empty list */
|
|
2637
|
-
}
|
|
2638
|
-
try {
|
|
2639
|
-
if (source) source.postMessage({ dgn: 'export-history-result', id, history }, replyOrigin);
|
|
2640
|
-
} catch {}
|
|
2641
|
-
}
|
|
2642
|
-
window.addEventListener('message', onMessage);
|
|
2643
|
-
return () => window.removeEventListener('message', onMessage);
|
|
2644
|
-
}, [commentsByFile, focusedCommentId, cfg, theme]);
|
|
2645
|
-
|
|
2646
|
-
// Tell the active canvas iframe to drop any persistent selection (canvas
|
|
2647
|
-
// SelectionSet) — used when the comment composer closes via submit /
|
|
2648
|
-
// cancel / Esc. canvas-shell listens for `force-clear` on the window
|
|
2649
|
-
// message channel and calls selSet.clear().
|
|
2650
|
-
const clearActiveCanvasSelection = useCallback(() => {
|
|
2651
|
-
if (!activePath || activePath === SYSTEM_TAB) return;
|
|
2652
|
-
const el = iframesRef.current.get(activePath);
|
|
2653
|
-
if (el && el.contentWindow) {
|
|
2654
|
-
try {
|
|
2655
|
-
el.contentWindow.postMessage({ dgn: 'force-clear' }, '*');
|
|
2656
|
-
} catch {}
|
|
2657
|
-
}
|
|
2658
|
-
}, [activePath]);
|
|
2659
|
-
|
|
2660
|
-
const resolveComment = useCallback((id) => {
|
|
2661
|
-
wsSend({ type: 'comments-patch', id, patch: { status: 'resolved' } });
|
|
2662
|
-
}, []);
|
|
2663
|
-
const reopenComment = useCallback((id) => {
|
|
2664
|
-
wsSend({ type: 'comments-patch', id, patch: { status: 'open' } });
|
|
2665
|
-
}, []);
|
|
2666
|
-
const deleteComment = useCallback((id) => {
|
|
2667
|
-
wsSend({ type: 'comments-delete', id });
|
|
2668
|
-
setFocusedCommentId((prev) => (prev === id ? null : prev));
|
|
2669
|
-
}, []);
|
|
2670
|
-
|
|
2671
|
-
// Jump from right-sidebar list to a comment: open file tab if needed, focus pin.
|
|
2672
|
-
// The iframe may be freshly mounted; the loaded handler also re-sends focus if focusedCommentId matches.
|
|
2673
|
-
const jumpToComment = useCallback(
|
|
2674
|
-
(file, id) => {
|
|
2675
|
-
if (file && file !== activePath) {
|
|
2676
|
-
setTabs((prev) => (prev.find((t) => t.path === file) ? prev : [...prev, { path: file }]));
|
|
2677
|
-
setActivePath(file);
|
|
2678
|
-
}
|
|
2679
|
-
if (id == null) {
|
|
2680
|
-
setFocusedCommentId(null);
|
|
2681
|
-
return;
|
|
2682
|
-
}
|
|
2683
|
-
setFocusedCommentId(id);
|
|
2684
|
-
// Try sending focus immediately (existing iframe) and again after a short delay (newly opened tab).
|
|
2685
|
-
const send = () => {
|
|
2686
|
-
const el = iframesRef.current.get(file);
|
|
2687
|
-
if (el && el.contentWindow) {
|
|
2688
|
-
try {
|
|
2689
|
-
el.contentWindow.postMessage({ dgn: 'comment-focus', id }, '*');
|
|
2690
|
-
} catch {}
|
|
2691
|
-
}
|
|
2692
|
-
};
|
|
2693
|
-
send();
|
|
2694
|
-
setTimeout(send, 200);
|
|
2695
|
-
},
|
|
2696
|
-
[activePath]
|
|
2697
|
-
);
|
|
2698
|
-
|
|
2699
|
-
// ----- Keyboard shortcuts (no Cmd+W — let browser close the tab) -----
|
|
2700
|
-
useEffect(() => {
|
|
2701
|
-
function onKey(e) {
|
|
2702
|
-
const meta = e.metaKey || e.ctrlKey;
|
|
2703
|
-
const inEditable =
|
|
2704
|
-
['INPUT', 'TEXTAREA'].includes(document.activeElement?.tagName) ||
|
|
2705
|
-
document.activeElement?.isContentEditable;
|
|
2706
|
-
// Phase 4.1: shell-side letter shortcuts (H/T/S) must not double-fire
|
|
2707
|
-
// inside a focused canvas iframe — the canvas input router owns those
|
|
2708
|
-
// letters as tool-mode keys (V/H/C). Cmd-modified shortcuts (⌘R, ⌘⇧M,
|
|
2709
|
-
// ⌘F) still fire regardless of focus, mirroring browser convention.
|
|
2710
|
-
const inCanvasIframe = document.activeElement?.tagName === 'IFRAME';
|
|
2711
|
-
|
|
2712
|
-
// Cmd+R — reload active iframe (override browser reload)
|
|
2713
|
-
if (meta && (e.key === 'r' || e.key === 'R')) {
|
|
2714
|
-
e.preventDefault();
|
|
2715
|
-
reloadActive();
|
|
2716
|
-
return;
|
|
2717
|
-
}
|
|
2718
|
-
// Cmd+Shift+M / Ctrl+Shift+M — toggle right "Comments" panel
|
|
2719
|
-
if (meta && e.shiftKey && (e.key === 'm' || e.key === 'M')) {
|
|
2720
|
-
e.preventDefault();
|
|
2721
|
-
setCommentsPanelOpen((v) => !v);
|
|
2722
|
-
return;
|
|
2723
|
-
}
|
|
2724
|
-
// Cmd+C / Ctrl+C — Phase 4.1 removed the shell-side comment-drop chord.
|
|
2725
|
-
// Canvas comment-drop is the `C` tool letter (press C in the canvas,
|
|
2726
|
-
// then click the element) or right-click "Add comment". Cmd+C now
|
|
2727
|
-
// reverts to native browser copy.
|
|
2728
|
-
if (meta && !e.shiftKey && !e.altKey && (e.key === 'c' || e.key === 'C')) {
|
|
2729
|
-
if (
|
|
2730
|
-
selected &&
|
|
2731
|
-
selected.selector &&
|
|
2732
|
-
activePath &&
|
|
2733
|
-
activePath !== SYSTEM_TAB &&
|
|
2734
|
-
!inEditable &&
|
|
2735
|
-
console &&
|
|
2736
|
-
console.warn
|
|
2737
|
-
) {
|
|
2738
|
-
console.warn(
|
|
2739
|
-
'Cmd+C comment-drop deprecated — press C inside the canvas to enter Comment tool, then click the element.'
|
|
2740
|
-
);
|
|
2741
|
-
}
|
|
2742
|
-
// Fall through to native copy.
|
|
2743
|
-
}
|
|
2744
|
-
if (inEditable) return;
|
|
2745
|
-
// / — focus search (or ⌘F per CV-08 placeholder hint)
|
|
2746
|
-
if (e.key === '/') {
|
|
2747
|
-
e.preventDefault();
|
|
2748
|
-
const inp = document.querySelector('.tree-panel-search input');
|
|
2749
|
-
if (inp) inp.focus();
|
|
2750
|
-
return;
|
|
2751
|
-
}
|
|
2752
|
-
if (meta && (e.key === 'f' || e.key === 'F')) {
|
|
2753
|
-
e.preventDefault();
|
|
2754
|
-
if (!sidebarOpen) setSidebarOpen(true);
|
|
2755
|
-
setTimeout(() => {
|
|
2756
|
-
const inp = document.querySelector('.tree-panel-search input');
|
|
2757
|
-
if (inp) inp.focus();
|
|
2758
|
-
}, 0);
|
|
2759
|
-
return;
|
|
2760
|
-
}
|
|
2761
|
-
// T / H / S are bare-letter shell shortcuts. When focus is inside a
|
|
2762
|
-
// canvas iframe, the canvas input router claims V/H/C — bail out
|
|
2763
|
-
// here so the canvas owns the key and the sidebar/system view don't
|
|
2764
|
-
// double-fire on focused-canvas keypresses.
|
|
2765
|
-
if (inCanvasIframe) {
|
|
2766
|
-
// Esc still bubbles below (composer / focused-pin clear).
|
|
2767
|
-
if (e.key !== 'Escape') return;
|
|
2768
|
-
}
|
|
2769
|
-
// T — toggle Project Tree (sidebar)
|
|
2770
|
-
if (e.key === 't' || e.key === 'T') {
|
|
2771
|
-
if (e.shiftKey || meta) return;
|
|
2772
|
-
e.preventDefault();
|
|
2773
|
-
setSidebarOpen((v) => !v);
|
|
2774
|
-
return;
|
|
2775
|
-
}
|
|
2776
|
-
// H — toggle show-hidden (sidecars + project/runtime orphans)
|
|
2777
|
-
if (e.key === 'h' || e.key === 'H') {
|
|
2778
|
-
if (e.shiftKey || meta) return;
|
|
2779
|
-
e.preventDefault();
|
|
2780
|
-
setShowHidden((v) => !v);
|
|
2781
|
-
return;
|
|
2782
|
-
}
|
|
2783
|
-
// S — toggle Design system view
|
|
2784
|
-
if ((e.key === 's' || e.key === 'S') && !meta && !e.shiftKey) {
|
|
2785
|
-
e.preventDefault();
|
|
2786
|
-
if (activePath === SYSTEM_TAB) {
|
|
2787
|
-
closeTab(SYSTEM_TAB);
|
|
2788
|
-
} else {
|
|
2789
|
-
openSystem();
|
|
2790
|
-
}
|
|
2791
|
-
return;
|
|
2792
|
-
}
|
|
2793
|
-
// ? or F1 — open Help modal
|
|
2794
|
-
if (e.key === '?' || e.key === 'F1') {
|
|
2795
|
-
e.preventDefault();
|
|
2796
|
-
setHelpOpen(true);
|
|
2797
|
-
return;
|
|
2798
|
-
}
|
|
2799
|
-
// Esc — clear focused pin. The in-place composer (Phase 6) and thread
|
|
2800
|
-
// popover handle their own Esc inside the iframe.
|
|
2801
|
-
if (e.key === 'Escape') {
|
|
2802
|
-
if (focusedCommentId) {
|
|
2803
|
-
setFocusedCommentId(null);
|
|
2804
|
-
return;
|
|
2805
|
-
}
|
|
2806
|
-
}
|
|
2807
|
-
}
|
|
2808
|
-
window.addEventListener('keydown', onKey);
|
|
2809
|
-
return () => window.removeEventListener('keydown', onKey);
|
|
2810
|
-
}, [
|
|
2811
|
-
reloadActive,
|
|
2812
|
-
selected,
|
|
2813
|
-
activePath,
|
|
2814
|
-
focusedCommentId,
|
|
2815
|
-
sidebarOpen,
|
|
2816
|
-
openSystem,
|
|
2817
|
-
closeTab,
|
|
2818
|
-
clearActiveCanvasSelection,
|
|
2819
|
-
]);
|
|
2820
|
-
|
|
2821
|
-
const registerIframe = useCallback((path, el) => {
|
|
2822
|
-
if (el) iframesRef.current.set(path, el);
|
|
2823
|
-
}, []);
|
|
2824
|
-
|
|
2825
|
-
const activeFileComments =
|
|
2826
|
-
activePath && activePath !== SYSTEM_TAB ? commentsByFile[activePath] || [] : [];
|
|
2827
|
-
const totalOpen = totalCounts(commentsByFile).open;
|
|
2828
|
-
|
|
2829
|
-
// Suppress the native browser context menu across the shell — the canvas
|
|
2830
|
-
// input-router already handles right-click inside the canvas host, but
|
|
2831
|
-
// sidebar / menubar / statusbar / floating chrome would otherwise leak the
|
|
2832
|
-
// native menu on top of our `.dc-context-menu` (or alone, outside canvas).
|
|
2833
|
-
// Editable fields (search box, future text inputs) keep the native menu so
|
|
2834
|
-
// copy/paste still works.
|
|
2835
|
-
const onShellContextMenu = useCallback((e) => {
|
|
2836
|
-
const t = e.target;
|
|
2837
|
-
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) {
|
|
2838
|
-
return;
|
|
2839
|
-
}
|
|
2840
|
-
e.preventDefault();
|
|
2841
|
-
}, []);
|
|
2842
|
-
|
|
2843
|
-
return (
|
|
2844
|
-
<div
|
|
2845
|
-
className={
|
|
2846
|
-
'app' + (commentsPanelOpen ? ' with-rsidebar' : '') + (sidebarOpen ? '' : ' no-sidebar')
|
|
2847
|
-
}
|
|
2848
|
-
onContextMenu={onShellContextMenu}
|
|
2849
|
-
>
|
|
2850
|
-
<SyncBanner status={syncStatus} />
|
|
2851
|
-
{gitLifecycle && (
|
|
2852
|
-
<div
|
|
2853
|
-
role="status"
|
|
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
|
-
>
|
|
2873
|
-
<span>Repo state changed — reload to sync?</span>
|
|
2874
|
-
<button
|
|
2875
|
-
type="button"
|
|
2876
|
-
onClick={() => {
|
|
2877
|
-
try {
|
|
2878
|
-
window.location.reload();
|
|
2879
|
-
} catch {}
|
|
2880
|
-
}}
|
|
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
|
-
>
|
|
2891
|
-
Reload
|
|
2892
|
-
</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
|
-
>
|
|
2906
|
-
Dismiss
|
|
2907
|
-
</button>
|
|
2908
|
-
</div>
|
|
2909
|
-
)}
|
|
2910
|
-
<Sidebar
|
|
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">
|
|
2925
|
-
<Menubar
|
|
2926
|
-
activePath={activePath}
|
|
2927
|
-
project={project}
|
|
2928
|
-
tabsCount={tabs.length}
|
|
2929
|
-
openMenu={openMenu}
|
|
2930
|
-
setOpenMenu={setOpenMenu}
|
|
2931
|
-
commentsPanelOpen={commentsPanelOpen}
|
|
2932
|
-
onToggleComments={() => setCommentsPanelOpen((v) => !v)}
|
|
2933
|
-
onOpenSystem={openSystem}
|
|
2934
|
-
sidebarOpen={sidebarOpen}
|
|
2935
|
-
onToggleSidebar={() => setSidebarOpen((v) => !v)}
|
|
2936
|
-
showHidden={showHidden}
|
|
2937
|
-
onToggleShowHidden={() => setShowHidden((v) => !v)}
|
|
2938
|
-
onOpenHelp={() => setHelpOpen(true)}
|
|
2939
|
-
annotationsVisible={annotationsVisible}
|
|
2940
|
-
onToggleAnnotations={toggleAnnotations}
|
|
2941
|
-
postToActiveCanvas={postToActiveCanvas}
|
|
2942
|
-
/>
|
|
2943
|
-
<Viewport
|
|
2944
|
-
tabs={tabs}
|
|
2945
|
-
activePath={activePath}
|
|
2946
|
-
registerIframe={registerIframe}
|
|
2947
|
-
systemData={systemData}
|
|
2948
|
-
onOpenFromSystem={openTab}
|
|
2949
|
-
onSelectDs={loadSystemData}
|
|
2950
|
-
project={project}
|
|
2951
|
-
cfg={cfg}
|
|
2952
|
-
/>
|
|
2953
|
-
{activePath && activePath !== SYSTEM_TAB && (
|
|
2954
|
-
<CommentBar activePath={activePath} comments={activeFileComments} />
|
|
2955
|
-
)}
|
|
2956
|
-
<StatusBar
|
|
2957
|
-
activePath={activePath}
|
|
2958
|
-
selected={selected}
|
|
2959
|
-
wsConnected={wsConnected}
|
|
2960
|
-
openCount={totalOpen}
|
|
2961
|
-
theme={theme}
|
|
2962
|
-
onToggleTheme={toggleTheme}
|
|
2963
|
-
onClearSelected={clearSelected}
|
|
2964
|
-
syncStatus={syncStatus}
|
|
2965
|
-
/>
|
|
2966
|
-
</div>
|
|
2967
|
-
{commentsPanelOpen && (
|
|
2968
|
-
<CommentsPanel
|
|
2969
|
-
commentsByFile={commentsByFile}
|
|
2970
|
-
filter={commentsFilter}
|
|
2971
|
-
setFilter={setCommentsFilter}
|
|
2972
|
-
activePath={activePath}
|
|
2973
|
-
focusedId={focusedCommentId}
|
|
2974
|
-
onJump={jumpToComment}
|
|
2975
|
-
onResolve={resolveComment}
|
|
2976
|
-
onReopen={reopenComment}
|
|
2977
|
-
onDelete={deleteComment}
|
|
2978
|
-
/>
|
|
2979
|
-
)}
|
|
2980
|
-
<HelpModal open={helpOpen} onClose={() => setHelpOpen(false)} />
|
|
2981
|
-
</div>
|
|
2982
|
-
);
|
|
2983
|
-
}
|
|
2984
|
-
|
|
2985
|
-
createRoot(document.getElementById('root')).render(<App />);
|