@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
|
@@ -3,9 +3,16 @@
|
|
|
3
3
|
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
5
|
import type { Dirent } from 'node:fs';
|
|
6
|
-
import { readdir, readFile, stat as statp } from 'node:fs/promises';
|
|
6
|
+
import { mkdir, readdir, readFile, rename, stat as statp } from 'node:fs/promises';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
|
|
9
|
+
import { renderBriefBoard, validateCanvasName } from './canvas-create.ts';
|
|
10
|
+
import {
|
|
11
|
+
CanvasEditError,
|
|
12
|
+
editAttribute,
|
|
13
|
+
removeAttribute,
|
|
14
|
+
editText as runEditText,
|
|
15
|
+
} from './canvas-edit.ts';
|
|
9
16
|
import type { Context } from './context.ts';
|
|
10
17
|
|
|
11
18
|
const SKIP_DIRS = new Set([
|
|
@@ -95,6 +102,9 @@ export interface Comment {
|
|
|
95
102
|
id: string;
|
|
96
103
|
file: string;
|
|
97
104
|
selector: string;
|
|
105
|
+
/** Occurrence index among `querySelectorAll(selector)` — disambiguates a
|
|
106
|
+
* component repeated within one artboard. Absent on legacy comments. */
|
|
107
|
+
index?: number;
|
|
98
108
|
dom_path: string[];
|
|
99
109
|
tag: string;
|
|
100
110
|
classes: string;
|
|
@@ -129,6 +139,19 @@ export interface ExportHistoryEntry {
|
|
|
129
139
|
at: string;
|
|
130
140
|
}
|
|
131
141
|
|
|
142
|
+
export type CreateCanvasResult =
|
|
143
|
+
| { ok: true; file: string; rel: string; slug: string }
|
|
144
|
+
| { ok: false; status: number; error: string };
|
|
145
|
+
|
|
146
|
+
export type DeleteCanvasResult =
|
|
147
|
+
| { ok: true; rel: string; slug: string; trashed: string[]; trashDir: string }
|
|
148
|
+
| { ok: false; status: number; error: string };
|
|
149
|
+
|
|
150
|
+
/** Phase 12 — result of an in-canvas direct edit (`editCss` / `editText`). */
|
|
151
|
+
export type EditOpResult =
|
|
152
|
+
| { ok: true; delta: number }
|
|
153
|
+
| { ok: false; status: number; error: string };
|
|
154
|
+
|
|
132
155
|
export interface Api {
|
|
133
156
|
// File tree
|
|
134
157
|
fileSlug(file: string): string;
|
|
@@ -168,6 +191,35 @@ export interface Api {
|
|
|
168
191
|
// Annotations sidecar (Phase 5 — .design/<slug>.annotations.svg)
|
|
169
192
|
loadAnnotations(file: string): Promise<string | null>;
|
|
170
193
|
saveAnnotations(file: string, svg: string): Promise<boolean>;
|
|
194
|
+
// Phase 23 — content-addressed binary image write (drag-drop / paste / picker)
|
|
195
|
+
saveAsset(bytes: Uint8Array): Promise<SaveAssetResult>;
|
|
196
|
+
// Create a blank brief board from the browser (Phase 22 — POST /_api/canvas)
|
|
197
|
+
createCanvas(input: {
|
|
198
|
+
name?: unknown;
|
|
199
|
+
kind?: unknown;
|
|
200
|
+
group?: unknown;
|
|
201
|
+
}): Promise<CreateCanvasResult>;
|
|
202
|
+
// Soft-delete a canvas from the browser (Phase 22 — DELETE /_api/canvas)
|
|
203
|
+
deleteCanvas(input: { file?: unknown }): Promise<DeleteCanvasResult>;
|
|
204
|
+
// Phase 12 (DDR-103) — single-property inline CSS edit (POST /_api/edit-css).
|
|
205
|
+
// Main-origin only: writes one key into the element's inline `style={{}}` object.
|
|
206
|
+
editCss(input: {
|
|
207
|
+
canvas?: unknown;
|
|
208
|
+
id?: unknown;
|
|
209
|
+
property?: unknown;
|
|
210
|
+
value?: unknown;
|
|
211
|
+
}): Promise<EditOpResult>;
|
|
212
|
+
// Phase 12 (DDR-103) — inline text-content edit (POST /_api/edit-text). Main-origin only.
|
|
213
|
+
editText(input: { canvas?: unknown; id?: unknown; text?: unknown }): Promise<EditOpResult>;
|
|
214
|
+
// Phase 12.2 (DDR-104) — custom HTML attribute edit (POST /_api/edit-attr). Main-origin
|
|
215
|
+
// only. The CSS panel's "custom HTML attribute" escape hatch (data-*, aria-*, role, …);
|
|
216
|
+
// writes a plain JSX attribute via editAttribute's non-`style.` path.
|
|
217
|
+
editAttr(input: {
|
|
218
|
+
canvas?: unknown;
|
|
219
|
+
id?: unknown;
|
|
220
|
+
attr?: unknown;
|
|
221
|
+
value?: unknown;
|
|
222
|
+
}): Promise<EditOpResult>;
|
|
171
223
|
// Aggregate data
|
|
172
224
|
buildIndexData(): Promise<unknown>;
|
|
173
225
|
buildSystemData(dsName?: string | null): Promise<unknown>;
|
|
@@ -182,80 +234,92 @@ export interface ApiHooks {
|
|
|
182
234
|
onAnnotationsChanged?: (file: string, svg: string) => void;
|
|
183
235
|
}
|
|
184
236
|
|
|
237
|
+
// FigJam v3 — the annotation sanitizer moved to annotations-model.ts (the
|
|
238
|
+
// schema owner; the allowlist guards exactly that vocabulary, and the
|
|
239
|
+
// headless `maude design annotate` write verb needs it without pulling the
|
|
240
|
+
// server modules). Re-exported here so every existing `from './api.ts'`
|
|
241
|
+
// import keeps working unchanged.
|
|
242
|
+
export { ASSET_IMAGE_HREF_RE, sanitizeAnnotationSvg } from './annotations-model.ts';
|
|
243
|
+
|
|
244
|
+
import { sanitizeAnnotationSvg } from './annotations-model.ts';
|
|
245
|
+
|
|
246
|
+
/** Phase 23 — hard ceiling on a single uploaded asset (10 MB). */
|
|
247
|
+
export const ASSET_MAX_BYTES = 10 * 1024 * 1024;
|
|
248
|
+
|
|
185
249
|
/**
|
|
186
|
-
*
|
|
187
|
-
*
|
|
250
|
+
* Phase 23 security review (DDR-088 follow-up) — aggregate per-server-instance
|
|
251
|
+
* write budget for `/_api/asset`. Content-addressing dedupes IDENTICAL bytes,
|
|
252
|
+
* but a one-byte mutation (a PNG `tEXt` chunk / a single pixel) yields a fresh
|
|
253
|
+
* sha8 each time, so dedup is NOT a disk-fill defense. This caps total bytes a
|
|
254
|
+
* single dev-server instance will ever write to `assets/` — generous for real
|
|
255
|
+
* reference material, but bounds a scripted loop from the untrusted canvas
|
|
256
|
+
* origin. Overridable via `MAUDE_ASSET_SESSION_BUDGET` (bytes) for power users.
|
|
188
257
|
*/
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
'rect',
|
|
194
|
-
'ellipse',
|
|
195
|
-
'line',
|
|
196
|
-
'polyline',
|
|
197
|
-
// Phase 24 — `polygon` (diamond / triangle shape primitives + closed arrowhead
|
|
198
|
-
// outlines + diamond heads) and `circle` (circle arrowhead). Inert shape
|
|
199
|
-
// elements with no script capability — same safety class as the rest.
|
|
200
|
-
'polygon',
|
|
201
|
-
'circle',
|
|
202
|
-
'text',
|
|
203
|
-
]);
|
|
258
|
+
export const ASSET_SESSION_BUDGET = (() => {
|
|
259
|
+
const env = Number(process.env.MAUDE_ASSET_SESSION_BUDGET);
|
|
260
|
+
return Number.isFinite(env) && env > 0 ? env : 256 * 1024 * 1024;
|
|
261
|
+
})();
|
|
204
262
|
|
|
205
263
|
/**
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
* vocabulary so they're zero-regression on real annotations:
|
|
212
|
-
*
|
|
213
|
-
* 1. Element allowlist — drop the markup of any tag whose LOCAL name (namespace
|
|
214
|
-
* stripped, so `svg:script` → `script`) isn't in ANNOTATION_SVG_ELEMENTS.
|
|
215
|
-
* Dropped-tag text content survives as inert text (not in a script/style
|
|
216
|
-
* context), which is harmless.
|
|
217
|
-
* 2. Attribute denylist on survivors — the legit vocabulary uses none of
|
|
218
|
-
* `on*=`, `style=`, or any `href`, so stripping them closes inline handlers,
|
|
219
|
-
* CSS `url(javascript:)`, and entity-encoded `xlink:href` in one pass.
|
|
220
|
-
*
|
|
221
|
-
* The current consumer (`svgToStrokes` → DOMParser image/svg+xml → structured
|
|
222
|
-
* strokes → React) never raw-renders this string, so the live XSS risk is already
|
|
223
|
-
* nil; this keeps "inert" TRUE for any future raw-render consumer and for the
|
|
224
|
-
* synced file a peer / Claude-context ingests (defense-in-depth, DDR-054 §3).
|
|
264
|
+
* Phase 23 — content-type sniff from the first bytes (magic numbers). The
|
|
265
|
+
* declared name / extension / Content-Type is NEVER trusted — the bytes decide
|
|
266
|
+
* the stored extension (a `.png` name carrying GIF bytes is stored as `.gif`).
|
|
267
|
+
* SVG (XML/text) matches nothing here → returns null → rejected, so a
|
|
268
|
+
* script-bearing vector can't ride in through the image route. See DDR (Task 9).
|
|
225
269
|
*/
|
|
226
|
-
export function
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
270
|
+
export function sniffImageType(bytes: Uint8Array): 'png' | 'jpg' | 'gif' | 'webp' | null {
|
|
271
|
+
const b = bytes;
|
|
272
|
+
// PNG — 89 50 4E 47 0D 0A 1A 0A
|
|
273
|
+
if (
|
|
274
|
+
b.length >= 8 &&
|
|
275
|
+
b[0] === 0x89 &&
|
|
276
|
+
b[1] === 0x50 &&
|
|
277
|
+
b[2] === 0x4e &&
|
|
278
|
+
b[3] === 0x47 &&
|
|
279
|
+
b[4] === 0x0d &&
|
|
280
|
+
b[5] === 0x0a &&
|
|
281
|
+
b[6] === 0x1a &&
|
|
282
|
+
b[7] === 0x0a
|
|
283
|
+
) {
|
|
284
|
+
return 'png';
|
|
285
|
+
}
|
|
286
|
+
// JPEG — FF D8 FF
|
|
287
|
+
if (b.length >= 3 && b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff) return 'jpg';
|
|
288
|
+
// GIF — "GIF87a" / "GIF89a"
|
|
289
|
+
if (
|
|
290
|
+
b.length >= 6 &&
|
|
291
|
+
b[0] === 0x47 &&
|
|
292
|
+
b[1] === 0x49 &&
|
|
293
|
+
b[2] === 0x46 &&
|
|
294
|
+
b[3] === 0x38 &&
|
|
295
|
+
(b[4] === 0x37 || b[4] === 0x39) &&
|
|
296
|
+
b[5] === 0x61
|
|
297
|
+
) {
|
|
298
|
+
return 'gif';
|
|
299
|
+
}
|
|
300
|
+
// WEBP — "RIFF"????"WEBP"
|
|
301
|
+
if (
|
|
302
|
+
b.length >= 12 &&
|
|
303
|
+
b[0] === 0x52 &&
|
|
304
|
+
b[1] === 0x49 &&
|
|
305
|
+
b[2] === 0x46 &&
|
|
306
|
+
b[3] === 0x46 &&
|
|
307
|
+
b[8] === 0x57 &&
|
|
308
|
+
b[9] === 0x45 &&
|
|
309
|
+
b[10] === 0x42 &&
|
|
310
|
+
b[11] === 0x50
|
|
311
|
+
) {
|
|
312
|
+
return 'webp';
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export interface SaveAssetResult {
|
|
318
|
+
ok: boolean;
|
|
319
|
+
status?: number;
|
|
320
|
+
error?: string;
|
|
321
|
+
/** Relative `assets/<sha8>.<ext>` path on success. */
|
|
322
|
+
path?: string;
|
|
259
323
|
}
|
|
260
324
|
|
|
261
325
|
export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
@@ -471,6 +535,7 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
471
535
|
id: newCommentId(),
|
|
472
536
|
file: payload.file,
|
|
473
537
|
selector: String(payload.selector || ''),
|
|
538
|
+
index: typeof payload.index === 'number' ? payload.index : undefined,
|
|
474
539
|
dom_path: Array.isArray(payload.dom_path) ? payload.dom_path.slice(0, 16) : [],
|
|
475
540
|
tag: String(payload.tag || ''),
|
|
476
541
|
classes: String(payload.classes || ''),
|
|
@@ -712,6 +777,444 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
712
777
|
return true;
|
|
713
778
|
}
|
|
714
779
|
|
|
780
|
+
// Phase 23 — content-addressed asset write. Reachable from the (potentially
|
|
781
|
+
// untrusted, DDR-054) canvas origin, so every cap is load-bearing, NOT
|
|
782
|
+
// optional (DDR Task 9):
|
|
783
|
+
// • magic-byte sniff → true type ∈ {png,jpg,gif,webp}; a header lie or an
|
|
784
|
+
// SVG (script-bearing vector) is rejected — bytes decide, name is ignored.
|
|
785
|
+
// • ≤ 10 MB ceiling (assets get their OWN cap; never routed through the 1 MB
|
|
786
|
+
// SVG-text gate in saveAnnotations).
|
|
787
|
+
// • content-addressed name `assets/<sha8-of-bytes>.<ext>` → identical drops
|
|
788
|
+
// dedupe → a malicious canvas can't fill the disk with N copies of one
|
|
789
|
+
// image, and orphan-on-delete is safe (shared content survives).
|
|
790
|
+
// • resolved-path containment assert (defense-in-depth; the name carries no
|
|
791
|
+
// user input, but a poisoned designRoot must still not escape).
|
|
792
|
+
// Running total of bytes this server instance has actually written (post-dedupe).
|
|
793
|
+
let assetBytesWritten = 0;
|
|
794
|
+
async function saveAsset(bytes: Uint8Array): Promise<SaveAssetResult> {
|
|
795
|
+
if (!bytes || bytes.length === 0) return { ok: false, status: 400, error: 'empty body' };
|
|
796
|
+
if (bytes.length > ASSET_MAX_BYTES) {
|
|
797
|
+
return { ok: false, status: 413, error: 'asset exceeds the 10 MB cap' };
|
|
798
|
+
}
|
|
799
|
+
const kind = sniffImageType(bytes);
|
|
800
|
+
if (!kind) {
|
|
801
|
+
return {
|
|
802
|
+
ok: false,
|
|
803
|
+
status: 415,
|
|
804
|
+
error: 'unsupported image type — png/jpeg/gif/webp only (SVG rejected)',
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
const sha8 = crypto.createHash('sha256').update(bytes).digest('hex').slice(0, 8);
|
|
808
|
+
const name = `${sha8}.${kind}`;
|
|
809
|
+
const assetsDir = path.join(paths.designRoot, 'assets');
|
|
810
|
+
const fileAbs = path.join(assetsDir, name);
|
|
811
|
+
// Containment backstop — the name is content-addressed (sha8 hex + sniffed
|
|
812
|
+
// ext), so there is no user-controlled path segment, but assert anyway.
|
|
813
|
+
const resolved = path.resolve(fileAbs);
|
|
814
|
+
const assetsResolved = path.resolve(assetsDir);
|
|
815
|
+
if (resolved !== path.join(assetsResolved, name)) {
|
|
816
|
+
return { ok: false, status: 400, error: 'resolved asset path escapes assets dir' };
|
|
817
|
+
}
|
|
818
|
+
try {
|
|
819
|
+
// Dedupe — identical bytes hash to the same name; skip the write if present.
|
|
820
|
+
if (!(await Bun.file(fileAbs).exists())) {
|
|
821
|
+
// Aggregate write budget (DDR-088 follow-up) — bounds a scripted
|
|
822
|
+
// one-byte-mutation disk-fill loop from the untrusted canvas origin.
|
|
823
|
+
// Only a genuinely NEW file counts (a dedupe hit is free).
|
|
824
|
+
if (assetBytesWritten + bytes.length > ASSET_SESSION_BUDGET) {
|
|
825
|
+
return {
|
|
826
|
+
ok: false,
|
|
827
|
+
status: 429,
|
|
828
|
+
error: 'asset write budget exceeded for this server session',
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
await mkdir(assetsDir, { recursive: true });
|
|
832
|
+
await Bun.write(fileAbs, bytes);
|
|
833
|
+
assetBytesWritten += bytes.length;
|
|
834
|
+
}
|
|
835
|
+
} catch (err) {
|
|
836
|
+
return { ok: false, status: 500, error: err instanceof Error ? err.message : 'write failed' };
|
|
837
|
+
}
|
|
838
|
+
return { ok: true, path: `assets/${name}` };
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Phase 22 — create a blank brief board from the browser file tree. Wired ONLY
|
|
842
|
+
// on the main origin (server.ts startMainServer); the segregated canvas origin
|
|
843
|
+
// (DDR-054) never exposes this — an untrusted canvas iframe must not be able to
|
|
844
|
+
// write arbitrary `.tsx` files. The single user-controlled value (`name`) is
|
|
845
|
+
// gated by `validateCanvasName` (path + JSX + JSON injection boundary); `group`
|
|
846
|
+
// is allowlisted to the configured canvas groups; a `resolve()` containment
|
|
847
|
+
// assert is the defense-in-depth backstop.
|
|
848
|
+
async function createCanvas(input: {
|
|
849
|
+
name?: unknown;
|
|
850
|
+
kind?: unknown;
|
|
851
|
+
group?: unknown;
|
|
852
|
+
}): Promise<CreateCanvasResult> {
|
|
853
|
+
// v1 only stamps blank boards — generation stays with `/design:new` (Claude).
|
|
854
|
+
const kind = input.kind == null || input.kind === '' ? 'brief-board' : input.kind;
|
|
855
|
+
if (kind !== 'brief-board') {
|
|
856
|
+
return { ok: false, status: 400, error: 'only kind "brief-board" is supported' };
|
|
857
|
+
}
|
|
858
|
+
const v = validateCanvasName(input.name);
|
|
859
|
+
if (!v.ok || !v.name || !v.componentName) {
|
|
860
|
+
return { ok: false, status: 400, error: v.error ?? 'invalid name' };
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Group must be one of the configured canvas groups (+ the new-canvas dir).
|
|
864
|
+
const allowed = new Set<string>(cfg.canvasGroups.map((g) => g.path));
|
|
865
|
+
allowed.add(cfg.newCanvasDir || 'ui');
|
|
866
|
+
const group =
|
|
867
|
+
input.group == null || input.group === '' ? cfg.newCanvasDir || 'ui' : String(input.group);
|
|
868
|
+
if (!allowed.has(group)) {
|
|
869
|
+
return { ok: false, status: 400, error: `group must be one of: ${[...allowed].join(', ')}` };
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const groupAbs = path.join(paths.designRoot, group);
|
|
873
|
+
const fileAbs = path.join(groupAbs, `${v.name}.tsx`);
|
|
874
|
+
// Containment backstop, two layers (Phase 22 security review F2). The name
|
|
875
|
+
// regex already forbids traversal, but `group` comes from config
|
|
876
|
+
// (canvasGroups[].path / newCanvasDir), which `normalizeConfig` does NOT
|
|
877
|
+
// validate for `..`. So assert (1) the GROUP resolves inside designRoot —
|
|
878
|
+
// a config-supplied `../../etc` must not relocate the write out of the
|
|
879
|
+
// project — and (2) the file resolves inside that group. Without (1) a
|
|
880
|
+
// poisoned config would be an arbitrary-directory `.tsx` write (latent today
|
|
881
|
+
// because config isn't sync-scoped; closed now so a future config-sync can't
|
|
882
|
+
// re-open it).
|
|
883
|
+
const resolvedDesignRoot = path.resolve(paths.designRoot);
|
|
884
|
+
const resolvedGroup = path.resolve(groupAbs);
|
|
885
|
+
if (
|
|
886
|
+
resolvedGroup !== resolvedDesignRoot &&
|
|
887
|
+
!resolvedGroup.startsWith(`${resolvedDesignRoot}${path.sep}`)
|
|
888
|
+
) {
|
|
889
|
+
return { ok: false, status: 400, error: 'canvas group resolves outside the design root' };
|
|
890
|
+
}
|
|
891
|
+
if (
|
|
892
|
+
path.resolve(fileAbs) !== path.join(resolvedGroup, `${v.name}.tsx`) ||
|
|
893
|
+
!path.resolve(fileAbs).startsWith(`${resolvedGroup}${path.sep}`)
|
|
894
|
+
) {
|
|
895
|
+
return { ok: false, status: 400, error: 'resolved path escapes the canvas group' };
|
|
896
|
+
}
|
|
897
|
+
if (await Bun.file(fileAbs).exists()) {
|
|
898
|
+
return {
|
|
899
|
+
ok: false,
|
|
900
|
+
status: 409,
|
|
901
|
+
error: `a canvas named "${v.name}" already exists in ${group}`,
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const rel = path.posix.join(group, `${v.name}.tsx`);
|
|
906
|
+
const slug = fileSlug(rel);
|
|
907
|
+
const dsName = cfg.defaultDesignSystem || 'project';
|
|
908
|
+
const platform = 'desktop';
|
|
909
|
+
const tsx = renderBriefBoard({
|
|
910
|
+
name: v.name,
|
|
911
|
+
componentName: v.componentName,
|
|
912
|
+
dsName,
|
|
913
|
+
platform,
|
|
914
|
+
seedHint: 'Empty brief board — annotate me',
|
|
915
|
+
historyDir: path.posix.join(paths.designRel, '_history', slug),
|
|
916
|
+
});
|
|
917
|
+
const now = new Date().toISOString();
|
|
918
|
+
const meta = {
|
|
919
|
+
kind: 'brief-board',
|
|
920
|
+
title: v.name,
|
|
921
|
+
subtitle: 'brief board',
|
|
922
|
+
brief: v.name,
|
|
923
|
+
designSystem: dsName,
|
|
924
|
+
platform,
|
|
925
|
+
created: now,
|
|
926
|
+
last_modified: now,
|
|
927
|
+
};
|
|
928
|
+
await Bun.write(fileAbs, tsx);
|
|
929
|
+
await Bun.write(
|
|
930
|
+
path.join(groupAbs, `${v.name}.meta.json`),
|
|
931
|
+
`${JSON.stringify(meta, null, 2)}\n`
|
|
932
|
+
);
|
|
933
|
+
// designRel-prefixed path — matches the file-tree `file.path` shape so the
|
|
934
|
+
// client can open it directly after reloadTree().
|
|
935
|
+
return { ok: true, file: path.posix.join(paths.designRel, rel), rel, slug };
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Phase 22 — SOFT-delete a canvas (DELETE /_api/canvas). Same trust boundary as
|
|
939
|
+
// createCanvas: main-origin-only, never the untrusted canvas iframe origin
|
|
940
|
+
// (DDR-054). Destructive, so it MOVES the whole sidecar set to
|
|
941
|
+
// `<designRoot>/_trash/<stamp>__<slug>/` (recoverable locally) instead of a hard
|
|
942
|
+
// rm — pairs with the existing `_history/` + /design:rollback model. Hub
|
|
943
|
+
// propagation of the delete is deliberately out of scope (Phase 26 consent).
|
|
944
|
+
async function deleteCanvas(input: { file?: unknown }): Promise<DeleteCanvasResult> {
|
|
945
|
+
const raw = input?.file;
|
|
946
|
+
if (typeof raw !== 'string' || !raw.trim()) {
|
|
947
|
+
return { ok: false, status: 400, error: 'file is required' };
|
|
948
|
+
}
|
|
949
|
+
let rel = raw.trim();
|
|
950
|
+
try {
|
|
951
|
+
rel = decodeURIComponent(rel);
|
|
952
|
+
} catch {
|
|
953
|
+
/* leave as-is */
|
|
954
|
+
}
|
|
955
|
+
rel = rel.replace(/^\/+/, '');
|
|
956
|
+
const prefix = `${paths.designRel.replace(/^\/+|\/+$/g, '')}/`;
|
|
957
|
+
if (rel.startsWith(prefix)) rel = rel.slice(prefix.length);
|
|
958
|
+
|
|
959
|
+
// Only a real `.tsx` canvas, no traversal.
|
|
960
|
+
if (rel.includes('..')) return { ok: false, status: 400, error: 'invalid path' };
|
|
961
|
+
if (!/\.tsx$/i.test(rel)) {
|
|
962
|
+
return { ok: false, status: 400, error: 'only .tsx canvases can be deleted' };
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const fileAbs = path.join(paths.designRoot, rel);
|
|
966
|
+
const resolvedDesignRoot = path.resolve(paths.designRoot);
|
|
967
|
+
const resolvedFile = path.resolve(fileAbs);
|
|
968
|
+
if (
|
|
969
|
+
resolvedFile !== resolvedDesignRoot &&
|
|
970
|
+
!resolvedFile.startsWith(`${resolvedDesignRoot}${path.sep}`)
|
|
971
|
+
) {
|
|
972
|
+
return { ok: false, status: 400, error: 'path escapes the design root' };
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Must live under a NON-DS canvas group — never the design system (`system`),
|
|
976
|
+
// never a loose root file (config.json, README). Deleting DS sources is not a
|
|
977
|
+
// canvas operation.
|
|
978
|
+
const deletable = cfg.canvasGroups.filter(
|
|
979
|
+
(g) => g.label !== 'Design system' && !/^system(\/|$)/.test(g.path)
|
|
980
|
+
);
|
|
981
|
+
const inGroup = deletable.some((g) => {
|
|
982
|
+
const gAbs = path.resolve(path.join(paths.designRoot, g.path));
|
|
983
|
+
return resolvedFile.startsWith(`${gAbs}${path.sep}`);
|
|
984
|
+
});
|
|
985
|
+
if (!inGroup) {
|
|
986
|
+
return {
|
|
987
|
+
ok: false,
|
|
988
|
+
status: 400,
|
|
989
|
+
error: 'only canvases under a managed canvas group can be deleted',
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (!(await Bun.file(fileAbs).exists())) {
|
|
994
|
+
return { ok: false, status: 404, error: 'canvas not found' };
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
const slug = fileSlug(rel);
|
|
998
|
+
const base = path.basename(rel).replace(/\.tsx$/i, '');
|
|
999
|
+
const groupDir = path.dirname(fileAbs);
|
|
1000
|
+
// Filesystem-safe wall-clock stamp (no `:`) so repeated deletes of the same
|
|
1001
|
+
// canvas don't collide in _trash.
|
|
1002
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1003
|
+
const trashDir = path.join(paths.designRoot, '_trash', `${stamp}__${slug}`);
|
|
1004
|
+
await mkdir(trashDir, { recursive: true });
|
|
1005
|
+
|
|
1006
|
+
const trashed: string[] = [];
|
|
1007
|
+
const moveIfExists = async (src: string, destName: string) => {
|
|
1008
|
+
try {
|
|
1009
|
+
await statp(src); // throws if absent (works for files AND dirs)
|
|
1010
|
+
} catch {
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
try {
|
|
1014
|
+
await rename(src, path.join(trashDir, destName));
|
|
1015
|
+
trashed.push(path.relative(paths.designRoot, src));
|
|
1016
|
+
} catch {
|
|
1017
|
+
/* best-effort — a sidecar that can't move shouldn't abort the delete */
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
// Primary + the full sidecar set (annotations, meta, history, canvas-state,
|
|
1022
|
+
// comments). Flattened names so the trash dir is a self-contained bundle.
|
|
1023
|
+
await moveIfExists(fileAbs, `${base}.tsx`);
|
|
1024
|
+
await moveIfExists(path.join(groupDir, `${base}.meta.json`), `${base}.meta.json`);
|
|
1025
|
+
await moveIfExists(
|
|
1026
|
+
path.join(paths.designRoot, `${slug}.annotations.svg`),
|
|
1027
|
+
`${slug}.annotations.svg`
|
|
1028
|
+
);
|
|
1029
|
+
await moveIfExists(path.join(paths.designRoot, '_history', slug), `_history__${slug}`);
|
|
1030
|
+
await moveIfExists(
|
|
1031
|
+
path.join(paths.canvasStateDir, `${slug}.json`),
|
|
1032
|
+
`_canvas-state__${slug}.json`
|
|
1033
|
+
);
|
|
1034
|
+
await moveIfExists(path.join(paths.commentsDir, `${slug}.json`), `_comments__${slug}.json`);
|
|
1035
|
+
|
|
1036
|
+
await Bun.write(
|
|
1037
|
+
path.join(trashDir, '_trash-manifest.json'),
|
|
1038
|
+
`${JSON.stringify({ canvas: rel, slug, deletedAt: new Date().toISOString(), trashed }, null, 2)}\n`
|
|
1039
|
+
);
|
|
1040
|
+
|
|
1041
|
+
return {
|
|
1042
|
+
ok: true,
|
|
1043
|
+
rel,
|
|
1044
|
+
slug,
|
|
1045
|
+
trashed,
|
|
1046
|
+
trashDir: path.relative(paths.repoRoot, trashDir),
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// Phase 12 (DDR-103) — resolve a v2 canvas slug (`selected.canvas`: POSIX,
|
|
1051
|
+
// extension-less, designRoot-relative — matches `_locator.json` keys + the
|
|
1052
|
+
// `canvasSlug()` shape) to its absolute `.tsx` path, with a containment
|
|
1053
|
+
// backstop. Same main-origin-only trust boundary as createCanvas: the
|
|
1054
|
+
// untrusted canvas iframe origin never reaches a source-write endpoint.
|
|
1055
|
+
function resolveCanvasAbs(
|
|
1056
|
+
slugRaw: unknown
|
|
1057
|
+
): { ok: true; abs: string } | { ok: false; status: number; error: string } {
|
|
1058
|
+
if (typeof slugRaw !== 'string' || !slugRaw.trim()) {
|
|
1059
|
+
return { ok: false, status: 400, error: 'canvas (slug) required' };
|
|
1060
|
+
}
|
|
1061
|
+
let slug = slugRaw.replace(/^\/+|\/+$/g, '').replace(/\.(tsx|html)$/i, '');
|
|
1062
|
+
// Accept either the bare slug (`ui/Foo`) or the designRel-prefixed file path
|
|
1063
|
+
// the client `selected.file` carries (`.design/ui/Foo.tsx`) — strip a leading
|
|
1064
|
+
// designRel so both shapes resolve to the same canvas (mirrors deriveCanvasSlug).
|
|
1065
|
+
const dr = paths.designRel.replace(/^\.\//, '').replace(/^\/+|\/+$/g, '');
|
|
1066
|
+
if (dr && slug.startsWith(`${dr}/`)) slug = slug.slice(dr.length + 1);
|
|
1067
|
+
if (
|
|
1068
|
+
!slug ||
|
|
1069
|
+
path.isAbsolute(slug) ||
|
|
1070
|
+
slug.split('/').some((seg) => seg === '..' || seg === '.' || seg === '')
|
|
1071
|
+
) {
|
|
1072
|
+
return { ok: false, status: 400, error: 'invalid canvas slug' };
|
|
1073
|
+
}
|
|
1074
|
+
const abs = `${path.join(paths.designRoot, ...slug.split('/'))}.tsx`;
|
|
1075
|
+
const resolvedRoot = path.resolve(paths.designRoot);
|
|
1076
|
+
const resolved = path.resolve(abs);
|
|
1077
|
+
if (!resolved.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
1078
|
+
return { ok: false, status: 400, error: 'canvas resolves outside the design root' };
|
|
1079
|
+
}
|
|
1080
|
+
return { ok: true, abs };
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// 8-hex lowercase, the shape `computeId` (canvas-edit.ts) stamps on data-cd-id.
|
|
1084
|
+
const CD_ID_RE = /^[0-9a-f]{8}$/;
|
|
1085
|
+
|
|
1086
|
+
async function editCss(input: {
|
|
1087
|
+
canvas?: unknown;
|
|
1088
|
+
id?: unknown;
|
|
1089
|
+
property?: unknown;
|
|
1090
|
+
value?: unknown;
|
|
1091
|
+
reset?: unknown;
|
|
1092
|
+
}): Promise<EditOpResult> {
|
|
1093
|
+
const r = resolveCanvasAbs(input.canvas);
|
|
1094
|
+
if (!r.ok) return r;
|
|
1095
|
+
const id = typeof input.id === 'string' ? input.id.trim() : '';
|
|
1096
|
+
if (!CD_ID_RE.test(id)) return { ok: false, status: 400, error: 'invalid data-cd-id' };
|
|
1097
|
+
const property = typeof input.property === 'string' ? input.property.trim() : '';
|
|
1098
|
+
// CSS property names are ASCII letters + hyphens only (optionally a leading
|
|
1099
|
+
// `-` for vendor prefixes) — reject anything that could smuggle a second key
|
|
1100
|
+
// or an expression into the inline style object.
|
|
1101
|
+
if (!property || !/^-?[a-z][a-z-]*$/.test(property)) {
|
|
1102
|
+
return { ok: false, status: 400, error: 'invalid css property' };
|
|
1103
|
+
}
|
|
1104
|
+
// kebab → camelCase JSX style key.
|
|
1105
|
+
const camel = property.replace(/-([a-z])/g, (_m, c: string) => c.toUpperCase());
|
|
1106
|
+
// Phase 12.3 — `reset: true` REMOVES the inline property (back to class /
|
|
1107
|
+
// inherited). No value needed; a missing key is a no-op (delta 0).
|
|
1108
|
+
if (input.reset === true) {
|
|
1109
|
+
try {
|
|
1110
|
+
const res = await removeAttribute(r.abs, id, `style.${camel}`);
|
|
1111
|
+
return { ok: true, delta: res.delta };
|
|
1112
|
+
} catch (err) {
|
|
1113
|
+
return {
|
|
1114
|
+
ok: false,
|
|
1115
|
+
status: 422,
|
|
1116
|
+
error: err instanceof CanvasEditError ? err.message : 'reset failed',
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
const value = typeof input.value === 'string' ? input.value : '';
|
|
1121
|
+
if (!value.trim()) return { ok: false, status: 400, error: 'value required' };
|
|
1122
|
+
if (value.length > 256) return { ok: false, status: 413, error: 'value too long' };
|
|
1123
|
+
// The value is always written as a JS STRING literal: JSON.stringify escapes
|
|
1124
|
+
// quotes/backslashes/newlines so it can never break out of the string, and
|
|
1125
|
+
// React accepts string values for every style prop — so `var(--accent)`,
|
|
1126
|
+
// `#fff`, `8px`, `700`, `1.5` all ride verbatim.
|
|
1127
|
+
try {
|
|
1128
|
+
const res = await editAttribute(r.abs, id, `style.${camel}`, JSON.stringify(value));
|
|
1129
|
+
return { ok: true, delta: res.delta };
|
|
1130
|
+
} catch (err) {
|
|
1131
|
+
return {
|
|
1132
|
+
ok: false,
|
|
1133
|
+
status: 422,
|
|
1134
|
+
error: err instanceof CanvasEditError ? err.message : 'edit failed',
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
async function editText(input: {
|
|
1140
|
+
canvas?: unknown;
|
|
1141
|
+
id?: unknown;
|
|
1142
|
+
text?: unknown;
|
|
1143
|
+
}): Promise<EditOpResult> {
|
|
1144
|
+
const r = resolveCanvasAbs(input.canvas);
|
|
1145
|
+
if (!r.ok) return r;
|
|
1146
|
+
const id = typeof input.id === 'string' ? input.id.trim() : '';
|
|
1147
|
+
if (!CD_ID_RE.test(id)) return { ok: false, status: 400, error: 'invalid data-cd-id' };
|
|
1148
|
+
if (typeof input.text !== 'string') return { ok: false, status: 400, error: 'text required' };
|
|
1149
|
+
if (input.text.length > 5000) return { ok: false, status: 413, error: 'text too long' };
|
|
1150
|
+
try {
|
|
1151
|
+
const res = await runEditText(r.abs, id, input.text);
|
|
1152
|
+
return { ok: true, delta: res.delta };
|
|
1153
|
+
} catch (err) {
|
|
1154
|
+
return {
|
|
1155
|
+
ok: false,
|
|
1156
|
+
status: 422,
|
|
1157
|
+
error: err instanceof CanvasEditError ? err.message : 'edit failed',
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
async function editAttr(input: {
|
|
1163
|
+
canvas?: unknown;
|
|
1164
|
+
id?: unknown;
|
|
1165
|
+
attr?: unknown;
|
|
1166
|
+
value?: unknown;
|
|
1167
|
+
reset?: unknown;
|
|
1168
|
+
}): Promise<EditOpResult> {
|
|
1169
|
+
const r = resolveCanvasAbs(input.canvas);
|
|
1170
|
+
if (!r.ok) return r;
|
|
1171
|
+
const id = typeof input.id === 'string' ? input.id.trim() : '';
|
|
1172
|
+
if (!CD_ID_RE.test(id)) return { ok: false, status: 400, error: 'invalid data-cd-id' };
|
|
1173
|
+
const attr = typeof input.attr === 'string' ? input.attr.trim() : '';
|
|
1174
|
+
// Plain HTML attributes only — data-*, aria-*, role, title, id, lang, dir…
|
|
1175
|
+
// Reject `style*` (that's /_api/edit-css), `data-cd-id` (pipeline-owned, also
|
|
1176
|
+
// refused downstream by editAttribute), and anything that isn't a bare html
|
|
1177
|
+
// attribute name. Digits allowed mid-name (e.g. data-2x).
|
|
1178
|
+
if (
|
|
1179
|
+
!attr ||
|
|
1180
|
+
!/^[a-z][a-z0-9-]*$/.test(attr) ||
|
|
1181
|
+
attr === 'data-cd-id' ||
|
|
1182
|
+
attr.startsWith('style')
|
|
1183
|
+
) {
|
|
1184
|
+
return { ok: false, status: 400, error: 'invalid attribute' };
|
|
1185
|
+
}
|
|
1186
|
+
// Phase 12.3 — `reset: true` REMOVES the custom attribute. No-op if absent.
|
|
1187
|
+
if (input.reset === true) {
|
|
1188
|
+
try {
|
|
1189
|
+
const res = await removeAttribute(r.abs, id, attr);
|
|
1190
|
+
return { ok: true, delta: res.delta };
|
|
1191
|
+
} catch (err) {
|
|
1192
|
+
return {
|
|
1193
|
+
ok: false,
|
|
1194
|
+
status: 422,
|
|
1195
|
+
error: err instanceof CanvasEditError ? err.message : 'reset failed',
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
const value = typeof input.value === 'string' ? input.value : '';
|
|
1200
|
+
if (!value.trim()) return { ok: false, status: 400, error: 'value required' };
|
|
1201
|
+
if (value.length > 256) return { ok: false, status: 413, error: 'value too long' };
|
|
1202
|
+
try {
|
|
1203
|
+
// Non-`style.` attr name → editAttribute writes a plain quoted JSX attribute.
|
|
1204
|
+
// Pass the value RAW: editStringAttr quotes/escapes it itself (JSON.stringify
|
|
1205
|
+
// on replace, escapeAttr on insert) — pre-stringifying here double-encoded
|
|
1206
|
+
// the value (`data-x="\"ok\""`; knob-smoke finding, 2026-06-12).
|
|
1207
|
+
const res = await editAttribute(r.abs, id, attr, value);
|
|
1208
|
+
return { ok: true, delta: res.delta };
|
|
1209
|
+
} catch (err) {
|
|
1210
|
+
return {
|
|
1211
|
+
ok: false,
|
|
1212
|
+
status: 422,
|
|
1213
|
+
error: err instanceof CanvasEditError ? err.message : 'edit failed',
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
715
1218
|
async function saveCanvasState(file: string, state: Record<string, unknown>) {
|
|
716
1219
|
if (!state || typeof state !== 'object') return;
|
|
717
1220
|
const safe: Record<string, unknown> = {};
|
|
@@ -732,6 +1235,28 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
732
1235
|
async function buildIndexData() {
|
|
733
1236
|
const groups = [];
|
|
734
1237
|
|
|
1238
|
+
// DDR-093 — per-canvas design-system map (repo-relative canvas path → DS
|
|
1239
|
+
// name) so the client's canvasUrl() injects each UI canvas's OWN tokens
|
|
1240
|
+
// instead of unconditionally designSystems[0]. Populated from the canvas
|
|
1241
|
+
// groups below; returned on the payload and folded into the client cfg.
|
|
1242
|
+
const canvasDesignSystems: Record<string, string> = {};
|
|
1243
|
+
const defaultDs = cfg.defaultDesignSystem || cfg.designSystems?.[0]?.name || 'project';
|
|
1244
|
+
// A file under `system/<folder>/` belongs to the DS that owns that folder —
|
|
1245
|
+
// path-authoritative, because specimens/ui_kits rarely carry a sidecar
|
|
1246
|
+
// `designSystem`. Returns null for `ui/` canvases (resolved via sidecar).
|
|
1247
|
+
const ownerDsName = (repoRelPath: string): string | null => {
|
|
1248
|
+
let r = repoRelPath;
|
|
1249
|
+
const prefix = `${paths.designRel}/`;
|
|
1250
|
+
if (r.startsWith(prefix)) r = r.slice(prefix.length);
|
|
1251
|
+
const m = r.match(/^system\/([^/]+)\//);
|
|
1252
|
+
if (!m) return null;
|
|
1253
|
+
const folder = m[1];
|
|
1254
|
+
const owner = (cfg.designSystems ?? []).find(
|
|
1255
|
+
(d) => d.path === `system/${folder}` || d.path.endsWith(`/${folder}`)
|
|
1256
|
+
);
|
|
1257
|
+
return owner?.name ?? null;
|
|
1258
|
+
};
|
|
1259
|
+
|
|
735
1260
|
// PROJECT — top-level .design/ files (README.md, INDEX.md, config.json, …).
|
|
736
1261
|
// These are non-HTML so they're listed but not openable in the iframe; the
|
|
737
1262
|
// sidebar can still display them as context (mirrors CV-08 mock).
|
|
@@ -777,6 +1302,20 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
777
1302
|
const filePaths = isDs
|
|
778
1303
|
? await findFiles(groupAbs, groupRel, ['.tsx', '.html', '.md', '.css', '.json'])
|
|
779
1304
|
: await findFiles(groupAbs, groupRel, ['.tsx', '.html', '.css', '.json']);
|
|
1305
|
+
// DDR-093 — record each `.tsx` canvas's design system. canvasUrl() only
|
|
1306
|
+
// injects tokens for `.tsx`, so skip everything else. Path-owned DS wins
|
|
1307
|
+
// (system/<ds>/…); otherwise the sidecar's `meta.designSystem`, defaulting
|
|
1308
|
+
// to the project default when the canvas declares none.
|
|
1309
|
+
for (const fp of filePaths) {
|
|
1310
|
+
if (!fp.endsWith('.tsx')) continue;
|
|
1311
|
+
let dsName = ownerDsName(fp);
|
|
1312
|
+
if (!dsName) {
|
|
1313
|
+
const meta = await loadCanvasMeta(fp);
|
|
1314
|
+
const declared = meta?.designSystem;
|
|
1315
|
+
dsName = typeof declared === 'string' && declared.trim() ? declared : defaultDs;
|
|
1316
|
+
}
|
|
1317
|
+
canvasDesignSystems[fp] = dsName;
|
|
1318
|
+
}
|
|
780
1319
|
// Strip down to `g.path` so per-DS folders (`project`, `beta`, …) show
|
|
781
1320
|
// up as the top-level dirs inside the DS section. Single-DS configs get
|
|
782
1321
|
// a wrapper folder too — slightly more verbose, but consistent with
|
|
@@ -837,6 +1376,7 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
837
1376
|
projectLabel: ctx.projectLabel,
|
|
838
1377
|
designRoot: paths.designRel,
|
|
839
1378
|
groups,
|
|
1379
|
+
canvasDesignSystems,
|
|
840
1380
|
};
|
|
841
1381
|
}
|
|
842
1382
|
|
|
@@ -1078,6 +1618,12 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1078
1618
|
patchCanvasMeta,
|
|
1079
1619
|
loadAnnotations,
|
|
1080
1620
|
saveAnnotations,
|
|
1621
|
+
saveAsset,
|
|
1622
|
+
createCanvas,
|
|
1623
|
+
deleteCanvas,
|
|
1624
|
+
editCss,
|
|
1625
|
+
editText,
|
|
1626
|
+
editAttr,
|
|
1081
1627
|
buildIndexData,
|
|
1082
1628
|
buildSystemData,
|
|
1083
1629
|
loadExportHistory,
|