@1agh/maude 1.0.11 → 1.3.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 -1
- package/apps/studio/acp/bridge.ts +18 -13
- package/apps/studio/acp/index.ts +43 -7
- package/apps/studio/acp/transcript-io.ts +394 -0
- package/apps/studio/acp/transcript.ts +103 -38
- package/apps/studio/annotations-layer.tsx +191 -116
- package/apps/studio/annotations-model.ts +39 -0
- package/apps/studio/annotations-sync.ts +50 -0
- package/apps/studio/api.ts +821 -55
- package/apps/studio/bin/_import-figma.mjs +57 -11
- package/apps/studio/bin/_seed-fixture.mjs +163 -0
- package/apps/studio/bin/annotate.mjs +3 -1
- package/apps/studio/bin/server-up.sh +17 -2
- package/apps/studio/canvas-edit.ts +391 -7
- package/apps/studio/canvas-imports.ts +89 -0
- package/apps/studio/canvas-lib.tsx +82 -18
- package/apps/studio/canvas-notice-message.ts +16 -0
- package/apps/studio/canvas-notifications.tsx +19 -0
- package/apps/studio/canvas-shell.tsx +57 -6
- package/apps/studio/client/app.jsx +943 -248
- package/apps/studio/client/apply-edit-request.ts +55 -0
- package/apps/studio/client/export-center.jsx +125 -67
- package/apps/studio/client/github.js +38 -1
- package/apps/studio/client/index-loader.ts +76 -0
- package/apps/studio/client/panels/CloudBar.jsx +52 -6
- package/apps/studio/client/panels/GitPanel.jsx +103 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +36 -7
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +92 -27
- package/apps/studio/client/panels/SourceConflictPanel.jsx +166 -0
- package/apps/studio/client/panels/SyncPanel.jsx +238 -4
- package/apps/studio/client/panels/TeamProjects.jsx +376 -0
- package/apps/studio/client/panels/file-deep-link-dialog.jsx +54 -0
- package/apps/studio/client/photo-knobs.jsx +3 -0
- package/apps/studio/client/report-bug.jsx +19 -1
- package/apps/studio/client/share-dialog.jsx +91 -0
- package/apps/studio/client/share-link.js +94 -0
- package/apps/studio/client/styles/3-shell-maude.css +69 -18
- package/apps/studio/client/styles/4-components.css +44 -1
- package/apps/studio/client/tree-row-menu.jsx +48 -3
- package/apps/studio/client/whats-new.jsx +26 -32
- package/apps/studio/cloud/endpoints.ts +132 -0
- package/apps/studio/collab/index.ts +4 -0
- package/apps/studio/collab/persistence.ts +137 -17
- package/apps/studio/collab/registry.ts +19 -21
- package/apps/studio/collab/room.ts +13 -2
- package/apps/studio/context.ts +59 -0
- package/apps/studio/debug-bundle.ts +13 -0
- package/apps/studio/dist/client.bundle.js +1911 -1879
- package/apps/studio/dist/runtime/.min-sizes.json +1 -0
- package/apps/studio/dist/runtime/sonner.js +1 -0
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/export-dialog.tsx +54 -0
- package/apps/studio/exporters/degraded.ts +19 -3
- package/apps/studio/exporters/ghostscript.ts +194 -0
- package/apps/studio/exporters/pdf-fonts.ts +261 -0
- package/apps/studio/exporters/pdf.ts +152 -3
- package/apps/studio/exporters/remote.ts +36 -1
- package/apps/studio/fs-watch.ts +45 -1
- package/apps/studio/http.ts +296 -19
- package/apps/studio/inspect.ts +19 -0
- package/apps/studio/managed-projects.ts +150 -0
- package/apps/studio/mem.ts +45 -9
- package/apps/studio/notifications.tsx +292 -0
- package/apps/studio/runtime-bundle.ts +2 -0
- package/apps/studio/server.ts +69 -6
- package/apps/studio/sync/accepted-cold-start.ts +225 -0
- package/apps/studio/sync/accepted-link.ts +320 -0
- package/apps/studio/sync/action-stage.ts +343 -0
- package/apps/studio/sync/agent.ts +104 -45
- package/apps/studio/sync/asset-push.ts +1 -47
- package/apps/studio/sync/cell-file-events.ts +3 -0
- package/apps/studio/sync/codec.ts +192 -34
- package/apps/studio/sync/cold-start-apply.ts +7 -0
- package/apps/studio/sync/cold-start.ts +118 -22
- package/apps/studio/sync/comment-identity.ts +68 -0
- package/apps/studio/sync/connection-state.ts +15 -1
- package/apps/studio/sync/ctl-provider.ts +15 -2
- package/apps/studio/sync/document-discovery.ts +81 -0
- package/apps/studio/sync/file-ledger.ts +117 -0
- package/apps/studio/sync/file-membership.ts +10 -2
- package/apps/studio/sync/file-plane.ts +926 -47
- package/apps/studio/sync/index.ts +1786 -36
- package/apps/studio/sync/journal-client.ts +13 -1
- package/apps/studio/sync/migrate-seed.ts +150 -22
- package/apps/studio/sync/poke.ts +4 -2
- package/apps/studio/sync/presentation.ts +311 -5
- package/apps/studio/sync/projection.ts +820 -23
- package/apps/studio/sync/repeated-module.ts +73 -0
- package/apps/studio/sync/retry-after.ts +169 -0
- package/apps/studio/sync/revision-barrier.ts +129 -0
- package/apps/studio/sync/seed-progress.ts +218 -0
- package/apps/studio/sync/seed-repair.ts +46 -0
- package/apps/studio/sync/source-merge.ts +100 -0
- package/apps/studio/sync/source-ops.ts +289 -0
- package/apps/studio/sync/source-recovery.ts +70 -0
- package/apps/studio/sync/source-validation.ts +56 -0
- package/apps/studio/sync/status.ts +201 -6
- package/apps/studio/sync/transaction-client.ts +460 -0
- package/apps/studio/sync/transport-error.ts +111 -0
- package/apps/studio/sync/writer-registry.ts +236 -0
- package/apps/studio/text-caret.ts +35 -0
- package/apps/studio/undo-hud.tsx +9 -87
- package/apps/studio/use-canvas-media-drop.tsx +4 -39
- package/apps/studio/whats-new.json +173 -0
- package/cli/bin/maude.mjs +2 -0
- package/cli/commands/codex.mjs +191 -0
- package/cli/commands/harness.mjs +1051 -0
- package/cli/commands/help.mjs +13 -0
- package/cli/lib/design-link.mjs +190 -2
- package/cli/lib/harness/capabilities.mjs +302 -0
- package/cli/lib/harness/codex-runtime.mjs +892 -0
- package/cli/lib/harness/compatibility.mjs +27 -0
- package/cli/lib/harness/discover-claude.mjs +1393 -0
- package/cli/lib/harness/managed-state.mjs +1296 -0
- package/cli/lib/harness/model.mjs +86 -0
- package/cli/lib/harness/secrets.mjs +390 -0
- package/cli/lib/harness/targets/codex.mjs +1141 -0
- package/cli/lib/harness/targets/opencode.mjs +807 -0
- package/cli/lib/harness/transaction.mjs +1099 -0
- package/cli/lib/hubs-config.mjs +35 -8
- package/cli/templates/harness/opencode/maude-projector.ts +58 -0
- package/docs/harness-capability-matrix.md +125 -0
- package/docs/harness-environment-projection.md +95 -0
- package/package.json +33 -9
- package/plugins/design/dependencies.json +3 -3
- package/plugins/design/templates/_shell.html +47 -0
- package/plugins/flow/.claude-plugin/config.schema.json +8 -0
- package/plugins/flow/dependencies.json +3 -3
- package/apps/studio/.ai/cache/_stats.json +0 -7
- package/apps/studio/bin/_agent-browser-safe.test.mjs +0 -165
- package/apps/studio/bin/_curl-local.test.mjs +0 -280
- package/apps/studio/bin/_pw-launch.test.mjs +0 -90
- package/apps/studio/bin/_smart-frames.test.mjs +0 -195
- package/apps/studio/bin/_transcribe.test.mjs +0 -80
- package/apps/studio/bun.lock +0 -928
- package/apps/studio/draw/test/animate.test.ts +0 -172
- package/apps/studio/draw/test/brush.test.ts +0 -213
- package/apps/studio/draw/test/composition.test.ts +0 -141
- package/apps/studio/draw/test/geometry.test.ts +0 -199
- package/apps/studio/draw/test/gradient.test.ts +0 -167
- package/apps/studio/draw/test/layout.test.ts +0 -120
- package/apps/studio/draw/test/morph.test.ts +0 -107
- package/apps/studio/draw/test/optimize.test.ts +0 -40
- package/apps/studio/draw/test/palette.test.ts +0 -123
- package/apps/studio/draw/test/primitives.test.ts +0 -129
- package/apps/studio/draw/test/serialize-animate.test.ts +0 -142
- package/apps/studio/draw/test/serialize.test.ts +0 -105
- package/apps/studio/figma/assets.test.ts +0 -464
- package/apps/studio/figma/client.test.ts +0 -395
- package/apps/studio/figma/codegen-client.test.ts +0 -276
- package/apps/studio/figma/codegen-fonts.test.ts +0 -103
- package/apps/studio/figma/codegen-values.test.ts +0 -179
- package/apps/studio/figma/comments-to-strokes.test.ts +0 -194
- package/apps/studio/figma/fig-decode.test.ts +0 -788
- package/apps/studio/figma/fig-differential.test.ts +0 -182
- package/apps/studio/figma/fig-translator.test.ts +0 -192
- package/apps/studio/figma/fig-vector.test.ts +0 -113
- package/apps/studio/figma/from-codegen.test.ts +0 -408
- package/apps/studio/figma/sanitize.test.ts +0 -325
- package/apps/studio/figma/tailwind-map.test.ts +0 -142
- package/apps/studio/figma/to-artboard.test.ts +0 -808
- package/apps/studio/figma/to-render.test.ts +0 -180
- package/apps/studio/figma/to-strokes-roundtrip.test.ts +0 -152
- package/apps/studio/figma/to-strokes.test.ts +0 -705
- package/apps/studio/figma/to-tokens.test.ts +0 -321
- package/apps/studio/figma/url.test.ts +0 -167
- package/apps/studio/footage/schema.test.ts +0 -287
- package/apps/studio/generation/adapters/groq.test.ts +0 -80
- package/apps/studio/generation/audio-library.test.ts +0 -95
- package/apps/studio/generation/captions.test.ts +0 -78
- package/apps/studio/generation/elevenlabs.test.ts +0 -223
- package/apps/studio/generation/gemini.test.ts +0 -350
- package/apps/studio/generation/jobs.test.ts +0 -128
- package/apps/studio/generation/keys.test.ts +0 -75
- package/apps/studio/generation/prefs.test.ts +0 -95
- package/apps/studio/generation/types.test.ts +0 -123
- package/apps/studio/generation/whisper-models.test.ts +0 -179
- package/apps/studio/test/_helpers.ts +0 -108
- package/apps/studio/test/acp-activity-endpoint.test.ts +0 -183
- package/apps/studio/test/acp-activity.test.ts +0 -154
- package/apps/studio/test/acp-ai-activity.test.ts +0 -182
- package/apps/studio/test/acp-attachment-serve.test.ts +0 -74
- package/apps/studio/test/acp-bootstrap-brief.test.ts +0 -178
- package/apps/studio/test/acp-branch-guard.test.ts +0 -123
- package/apps/studio/test/acp-bridge-lifetime.test.ts +0 -475
- package/apps/studio/test/acp-bridge.test.ts +0 -545
- package/apps/studio/test/acp-capabilities.test.ts +0 -123
- package/apps/studio/test/acp-caps-bridge.test.ts +0 -279
- package/apps/studio/test/acp-commands.test.ts +0 -119
- package/apps/studio/test/acp-elicitation-bridge.test.ts +0 -480
- package/apps/studio/test/acp-elicitation.test.ts +0 -251
- package/apps/studio/test/acp-env.test.ts +0 -95
- package/apps/studio/test/acp-mode-banner.test.ts +0 -45
- package/apps/studio/test/acp-origin-gate.test.ts +0 -160
- package/apps/studio/test/acp-permission-prompt.test.ts +0 -251
- package/apps/studio/test/acp-permission.test.ts +0 -278
- package/apps/studio/test/acp-plugin-bootstrap.test.ts +0 -89
- package/apps/studio/test/acp-session-allowed-tools.test.ts +0 -235
- package/apps/studio/test/acp-session-plugins.test.ts +0 -209
- package/apps/studio/test/acp-toolgroup.test.ts +0 -76
- package/apps/studio/test/acp-transcript-view.test.ts +0 -71
- package/apps/studio/test/acp-transcript.test.ts +0 -238
- package/apps/studio/test/acp-usage-bridge.test.ts +0 -141
- package/apps/studio/test/acp-usage.test.ts +0 -143
- package/apps/studio/test/acp-write-gate.test.ts +0 -323
- package/apps/studio/test/acp-write-scope.test.ts +0 -533
- package/apps/studio/test/active-state.test.ts +0 -186
- package/apps/studio/test/activity.test.ts +0 -189
- package/apps/studio/test/ai-activity.test.ts +0 -113
- package/apps/studio/test/annotate-write.test.ts +0 -1074
- package/apps/studio/test/annotation-strokes-command.test.ts +0 -166
- package/apps/studio/test/annotations-align.test.ts +0 -88
- package/apps/studio/test/annotations-api.test.ts +0 -145
- package/apps/studio/test/annotations-bindings.test.ts +0 -262
- package/apps/studio/test/annotations-draw-modifiers.test.ts +0 -206
- package/apps/studio/test/annotations-groups.test.ts +0 -231
- package/apps/studio/test/annotations-layer.test.ts +0 -1011
- package/apps/studio/test/annotations-roundtrip.test.ts +0 -663
- package/apps/studio/test/annotations-snap.test.ts +0 -135
- package/apps/studio/test/annotations-text-format.test.ts +0 -396
- package/apps/studio/test/artboard-activity-overlay.test.tsx +0 -56
- package/apps/studio/test/artboard-guides-overlay.test.tsx +0 -152
- package/apps/studio/test/artboard-hug-commit.test.ts +0 -36
- package/apps/studio/test/artboard-kinds.test.tsx +0 -83
- package/apps/studio/test/artboard-selection-attrs.test.ts +0 -69
- package/apps/studio/test/asset-api.test.ts +0 -149
- package/apps/studio/test/assets-s3.test.ts +0 -249
- package/apps/studio/test/binary-smoke.test.ts +0 -47
- package/apps/studio/test/boot-self-heal.test.ts +0 -87
- package/apps/studio/test/browse-posture.test.tsx +0 -107
- package/apps/studio/test/bundle-smoke.test.ts +0 -46
- package/apps/studio/test/camera-reveal.test.tsx +0 -173
- package/apps/studio/test/canvas-artifacts.test.ts +0 -125
- package/apps/studio/test/canvas-build-sandbox.test.ts +0 -69
- package/apps/studio/test/canvas-build.test.ts +0 -141
- package/apps/studio/test/canvas-create-api.test.ts +0 -676
- package/apps/studio/test/canvas-cursors.test.ts +0 -164
- package/apps/studio/test/canvas-edit.test.ts +0 -470
- package/apps/studio/test/canvas-freshness-deps.test.ts +0 -64
- package/apps/studio/test/canvas-header.test.ts +0 -127
- package/apps/studio/test/canvas-hide-chrome.test.ts +0 -88
- package/apps/studio/test/canvas-hmr-runtime.test.tsx +0 -114
- package/apps/studio/test/canvas-lib-inline.test.ts +0 -146
- package/apps/studio/test/canvas-lib-motion.test.ts +0 -131
- package/apps/studio/test/canvas-lib-resolver.test.ts +0 -111
- package/apps/studio/test/canvas-list-watch.test.ts +0 -388
- package/apps/studio/test/canvas-media-drop.test.ts +0 -187
- package/apps/studio/test/canvas-meta-api.test.ts +0 -707
- package/apps/studio/test/canvas-move-api.test.ts +0 -587
- package/apps/studio/test/canvas-origin-gate.test.ts +0 -417
- package/apps/studio/test/canvas-pipeline.test.ts +0 -180
- package/apps/studio/test/canvas-rects.test.ts +0 -198
- package/apps/studio/test/canvas-reorder.test.ts +0 -211
- package/apps/studio/test/canvas-route.test.ts +0 -179
- package/apps/studio/test/canvas-shell-base.test.ts +0 -107
- package/apps/studio/test/canvas-shell-build-error.test.ts +0 -52
- package/apps/studio/test/canvas-url.test.ts +0 -138
- package/apps/studio/test/canvas-wheel-delta.test.ts +0 -42
- package/apps/studio/test/canvas-zoom-floor.test.ts +0 -73
- package/apps/studio/test/capture-core.test.ts +0 -32
- package/apps/studio/test/capture-determinism-shape.test.ts +0 -135
- package/apps/studio/test/chat-attachments.test.ts +0 -117
- package/apps/studio/test/chat-context.test.ts +0 -129
- package/apps/studio/test/chat-markdown.test.tsx +0 -58
- package/apps/studio/test/clip-addressing.test.ts +0 -738
- package/apps/studio/test/clip-ops.test.ts +0 -922
- package/apps/studio/test/cloud-build.test.ts +0 -95
- package/apps/studio/test/cloud-connect-note.test.ts +0 -135
- package/apps/studio/test/cloud-endpoints.test.ts +0 -590
- package/apps/studio/test/cloud-history-hardening.test.ts +0 -165
- package/apps/studio/test/cloud-history-posture.test.ts +0 -230
- package/apps/studio/test/cloud-managed-save-surfaces.test.ts +0 -90
- package/apps/studio/test/cloud-renew.test.ts +0 -205
- package/apps/studio/test/cloud-session-role.test.ts +0 -161
- package/apps/studio/test/cloud-shell-surfaces.test.ts +0 -170
- package/apps/studio/test/cold-start-apply.test.ts +0 -303
- package/apps/studio/test/collab-annotations-bridge.test.ts +0 -55
- package/apps/studio/test/collab-awareness-bridge.test.ts +0 -223
- package/apps/studio/test/collab-bridge.test.ts +0 -79
- package/apps/studio/test/collab-loopback.test.ts +0 -63
- package/apps/studio/test/collab-origin-gate.test.ts +0 -323
- package/apps/studio/test/collab-protocol.test.ts +0 -146
- package/apps/studio/test/collab-readonly-gate.test.ts +0 -194
- package/apps/studio/test/collab-reseed-guard.test.ts +0 -78
- package/apps/studio/test/collab-room.test.ts +0 -188
- package/apps/studio/test/collab-session-survival.test.tsx +0 -176
- package/apps/studio/test/collab-stress.test.ts +0 -129
- package/apps/studio/test/comment-mount.test.ts +0 -97
- package/apps/studio/test/comment-relay-origin-gate.test.ts +0 -117
- package/apps/studio/test/comments-api.test.ts +0 -277
- package/apps/studio/test/comments-fs-rebroadcast.test.ts +0 -155
- package/apps/studio/test/comments-overlay.test.ts +0 -117
- package/apps/studio/test/compile-entry.test.ts +0 -133
- package/apps/studio/test/component-map.test.ts +0 -48
- package/apps/studio/test/config-projection.test.ts +0 -104
- package/apps/studio/test/config-reload.test.ts +0 -230
- package/apps/studio/test/config-version.test.ts +0 -88
- package/apps/studio/test/context-resolve-tokens.test.ts +0 -97
- package/apps/studio/test/convert-to-absolute.test.ts +0 -333
- package/apps/studio/test/credential-grammar.test.ts +0 -107
- package/apps/studio/test/csp-canvas-shell.test.ts +0 -62
- package/apps/studio/test/csrf-write-guard.test.ts +0 -141
- package/apps/studio/test/debug-bundle.test.ts +0 -105
- package/apps/studio/test/design-setup-readiness.test.ts +0 -155
- package/apps/studio/test/detach-component.test.ts +0 -94
- package/apps/studio/test/dns-rebinding-guard.test.ts +0 -101
- package/apps/studio/test/dom-selection.test.ts +0 -198
- package/apps/studio/test/dynamic-text-edit.test.ts +0 -162
- package/apps/studio/test/edit-css-occurrence.test.ts +0 -81
- package/apps/studio/test/edit-persistence.test.ts +0 -91
- package/apps/studio/test/edit-scope-api.test.ts +0 -119
- package/apps/studio/test/edit-source-command.test.ts +0 -142
- package/apps/studio/test/edit-suppress.test.ts +0 -170
- package/apps/studio/test/editing-presence.test.ts +0 -103
- package/apps/studio/test/element-resize.test.ts +0 -136
- package/apps/studio/test/element-structural-api.test.ts +0 -434
- package/apps/studio/test/element-structural-edit.test.ts +0 -750
- package/apps/studio/test/ensure-browser.test.ts +0 -92
- package/apps/studio/test/equal-spacing-detector.test.ts +0 -257
- package/apps/studio/test/export-assemble.test.ts +0 -53
- package/apps/studio/test/export-browser-lane.test.ts +0 -126
- package/apps/studio/test/export-capture-fidelity.test.ts +0 -146
- package/apps/studio/test/export-capture-hygiene.test.ts +0 -675
- package/apps/studio/test/export-center.test.tsx +0 -287
- package/apps/studio/test/export-e2e-lanes.test.ts +0 -857
- package/apps/studio/test/export-format-scope-coherence.test.ts +0 -89
- package/apps/studio/test/export-lane.test.ts +0 -245
- package/apps/studio/test/export-shim-multi-capture.test.ts +0 -83
- package/apps/studio/test/exporters/canva.test.ts +0 -63
- package/apps/studio/test/exporters/degraded-propagation.test.ts +0 -123
- package/apps/studio/test/exporters/endpoint.test.ts +0 -121
- package/apps/studio/test/exporters/history.test.ts +0 -107
- package/apps/studio/test/exporters/html.test.ts +0 -26
- package/apps/studio/test/exporters/jobs.test.ts +0 -342
- package/apps/studio/test/exporters/pdf.test.ts +0 -85
- package/apps/studio/test/exporters/png.test.ts +0 -103
- package/apps/studio/test/exporters/pptx-deck.test.ts +0 -112
- package/apps/studio/test/exporters/pptx.test.ts +0 -31
- package/apps/studio/test/exporters/pw-launch.test.ts +0 -49
- package/apps/studio/test/exporters/runtime.test.ts +0 -59
- package/apps/studio/test/exporters/scope.test.ts +0 -292
- package/apps/studio/test/exporters/svg.test.ts +0 -29
- package/apps/studio/test/exporters/unsupported-media.test.ts +0 -123
- package/apps/studio/test/exporters/zip.test.ts +0 -104
- package/apps/studio/test/fetch-asset-gate.test.ts +0 -189
- package/apps/studio/test/figjam-v3-model.test.ts +0 -342
- package/apps/studio/test/figma-explode.test.ts +0 -438
- package/apps/studio/test/figma-provenance.test.ts +0 -108
- package/apps/studio/test/figma-routes.test.ts +0 -294
- package/apps/studio/test/file-lock.test.ts +0 -84
- package/apps/studio/test/fixtures/backgrounding-claude-auth.sh +0 -12
- package/apps/studio/test/fixtures/fake-claude-auth.mjs +0 -23
- package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +0 -1
- package/apps/studio/test/fixtures/mock-acp-agent-caps.mjs +0 -158
- package/apps/studio/test/fixtures/mock-acp-agent-commands.mjs +0 -44
- package/apps/studio/test/fixtures/mock-acp-agent-elicit-flood.mjs +0 -46
- package/apps/studio/test/fixtures/mock-acp-agent-elicit-url.mjs +0 -42
- package/apps/studio/test/fixtures/mock-acp-agent-elicit.mjs +0 -63
- package/apps/studio/test/fixtures/mock-acp-agent-permission-flood.mjs +0 -51
- package/apps/studio/test/fixtures/mock-acp-agent-permission.mjs +0 -50
- package/apps/studio/test/fixtures/mock-acp-agent-slow.mjs +0 -45
- package/apps/studio/test/fixtures/mock-acp-agent-usage.mjs +0 -56
- package/apps/studio/test/fixtures/mock-acp-agent-wedged.mjs +0 -37
- package/apps/studio/test/fixtures/mock-acp-agent-write.mjs +0 -118
- package/apps/studio/test/fixtures/mock-acp-agent.mjs +0 -69
- package/apps/studio/test/fixtures/noisy-claude-auth.mjs +0 -27
- package/apps/studio/test/fixtures/perf-canvas.mjs +0 -201
- package/apps/studio/test/fixtures/phase-20-annotations.svg +0 -1
- package/apps/studio/test/fixtures/phase-21-annotations.svg +0 -1
- package/apps/studio/test/fixtures/tiny-subset.woff2 +0 -0
- package/apps/studio/test/fixtures/video-comp-fixture.tsx +0 -82
- package/apps/studio/test/fixtures/wedged-claude-auth.sh +0 -11
- package/apps/studio/test/footage-store.test.ts +0 -100
- package/apps/studio/test/fs-mkdir-api.test.ts +0 -248
- package/apps/studio/test/fs-watch.test.ts +0 -41
- package/apps/studio/test/gemma-models.test.ts +0 -245
- package/apps/studio/test/generate-route.test.ts +0 -106
- package/apps/studio/test/git-api.test.ts +0 -0
- package/apps/studio/test/git-branches.test.ts +0 -341
- package/apps/studio/test/git-cloud-posture.test.ts +0 -59
- package/apps/studio/test/git-grouping.test.ts +0 -106
- package/apps/studio/test/git-lifecycle.test.ts +0 -100
- package/apps/studio/test/git-log-format.test.ts +0 -95
- package/apps/studio/test/git-safe-rel.test.ts +0 -132
- package/apps/studio/test/git-system-engine.test.ts +0 -135
- package/apps/studio/test/git-watch.test.ts +0 -97
- package/apps/studio/test/github-api.test.ts +0 -577
- package/apps/studio/test/grid-track-handles.test.ts +0 -160
- package/apps/studio/test/handoff-static-frames.test.ts +0 -215
- package/apps/studio/test/handoff.test.ts +0 -329
- package/apps/studio/test/history-rollback.test.ts +0 -88
- package/apps/studio/test/hmr-broadcast.test.ts +0 -236
- package/apps/studio/test/hmr-classify.test.ts +0 -87
- package/apps/studio/test/hub-link.test.ts +0 -80
- package/apps/studio/test/identity-cache.test.ts +0 -83
- package/apps/studio/test/import-asset-browser.test.ts +0 -64
- package/apps/studio/test/import-asset-route.test.ts +0 -71
- package/apps/studio/test/import-asset.test.ts +0 -427
- package/apps/studio/test/import-brand-browser.test.ts +0 -88
- package/apps/studio/test/import-brand-route.test.ts +0 -120
- package/apps/studio/test/import-brand.test.ts +0 -206
- package/apps/studio/test/import-figma.test.ts +0 -667
- package/apps/studio/test/import-tokens.test.ts +0 -722
- package/apps/studio/test/index-data-canvas-ds.test.ts +0 -97
- package/apps/studio/test/input-router.test.ts +0 -757
- package/apps/studio/test/inspect-script-syntax.test.ts +0 -53
- package/apps/studio/test/inspect-selections.test.ts +0 -219
- package/apps/studio/test/knob-props-authored.test.ts +0 -87
- package/apps/studio/test/layers-synthetic-groups.test.ts +0 -96
- package/apps/studio/test/locator.test.ts +0 -214
- package/apps/studio/test/marquee-overlay.test.ts +0 -94
- package/apps/studio/test/media-commit-chain.test.ts +0 -210
- package/apps/studio/test/mode-toggle.test.tsx +0 -226
- package/apps/studio/test/motion-sample.test.ts +0 -35
- package/apps/studio/test/move-artboards-command.test.ts +0 -108
- package/apps/studio/test/participants-chrome.test.ts +0 -65
- package/apps/studio/test/paths.test.ts +0 -88
- package/apps/studio/test/pdf-print-boxes.test.ts +0 -359
- package/apps/studio/test/peer-selection-follows-camera.test.tsx +0 -131
- package/apps/studio/test/perf-harness.ts +0 -193
- package/apps/studio/test/phase-3.6-smoke.test.ts +0 -79
- package/apps/studio/test/photo-bg-remove-validation.test.ts +0 -71
- package/apps/studio/test/photo-canvas-bundle.test.ts +0 -79
- package/apps/studio/test/photo-edit-api.test.ts +0 -201
- package/apps/studio/test/photo-filters.test.ts +0 -149
- package/apps/studio/test/photo-pipeline.test.ts +0 -106
- package/apps/studio/test/photo-taxonomy.test.ts +0 -96
- package/apps/studio/test/print-marks.test.ts +0 -113
- package/apps/studio/test/print-units.test.ts +0 -173
- package/apps/studio/test/read-annotations.test.ts +0 -727
- package/apps/studio/test/read-only-gate.test.ts +0 -188
- package/apps/studio/test/read-only-role.test.ts +0 -95
- package/apps/studio/test/readiness.test.ts +0 -337
- package/apps/studio/test/remote-ahead-behind-cache.test.ts +0 -89
- package/apps/studio/test/reorder-api.test.ts +0 -210
- package/apps/studio/test/repo-concurrency.test.ts +0 -146
- package/apps/studio/test/repo-lock.test.ts +0 -350
- package/apps/studio/test/report-proxy.test.ts +0 -125
- package/apps/studio/test/ripple.test.ts +0 -197
- package/apps/studio/test/runtime-bundle-error-mapping.test.ts +0 -43
- package/apps/studio/test/runtime-bundle.test.ts +0 -69
- package/apps/studio/test/sanitize-annotation-svg.test.ts +0 -238
- package/apps/studio/test/scaffold-design.test.ts +0 -122
- package/apps/studio/test/server-lifecycle.test.ts +0 -38
- package/apps/studio/test/session-runtime-state.test.ts +0 -226
- package/apps/studio/test/session-scope.test.ts +0 -100
- package/apps/studio/test/shared-doc-cell-pairing.test.ts +0 -642
- package/apps/studio/test/shared-doc-convergence.test.ts +0 -265
- package/apps/studio/test/shared-doc-foundation.test.ts +0 -63
- package/apps/studio/test/shared-doc-migrate.test.ts +0 -369
- package/apps/studio/test/shared-doc-projection.test.ts +0 -238
- package/apps/studio/test/shell-importmap.test.ts +0 -56
- package/apps/studio/test/sizing-mode.test.ts +0 -102
- package/apps/studio/test/slash-commands.test.ts +0 -117
- package/apps/studio/test/snap-distance-pill.test.ts +0 -68
- package/apps/studio/test/spacing-handles.test.ts +0 -138
- package/apps/studio/test/specimen-select.test.ts +0 -88
- package/apps/studio/test/sync-agent.test.ts +0 -660
- package/apps/studio/test/sync-annotations-cold-start.test.ts +0 -302
- package/apps/studio/test/sync-asset-push.test.ts +0 -931
- package/apps/studio/test/sync-atomic-write.test.ts +0 -62
- package/apps/studio/test/sync-attach-incremental.test.ts +0 -527
- package/apps/studio/test/sync-autocommit.test.ts +0 -537
- package/apps/studio/test/sync-canvas-path.test.ts +0 -200
- package/apps/studio/test/sync-cell-write-nudge.test.ts +0 -346
- package/apps/studio/test/sync-codec.test.ts +0 -230
- package/apps/studio/test/sync-cold-seed-dedup.test.ts +0 -187
- package/apps/studio/test/sync-cold-start.test.ts +0 -304
- package/apps/studio/test/sync-connect-honesty.test.ts +0 -114
- package/apps/studio/test/sync-connection-state.test.ts +0 -337
- package/apps/studio/test/sync-ctl-channel.test.ts +0 -508
- package/apps/studio/test/sync-decide-file.test.ts +0 -420
- package/apps/studio/test/sync-doc-name.test.ts +0 -281
- package/apps/studio/test/sync-echo-guard.test.ts +0 -96
- package/apps/studio/test/sync-file-ledger.test.ts +0 -334
- package/apps/studio/test/sync-file-membership.test.ts +0 -347
- package/apps/studio/test/sync-file-plane.test.ts +0 -976
- package/apps/studio/test/sync-file-pull.test.ts +0 -333
- package/apps/studio/test/sync-fresh-link-parity.test.ts +0 -267
- package/apps/studio/test/sync-fs-mirror.test.ts +0 -181
- package/apps/studio/test/sync-hardening.test.ts +0 -586
- package/apps/studio/test/sync-hub-listing.test.ts +0 -46
- package/apps/studio/test/sync-hubs-config.test.ts +0 -133
- package/apps/studio/test/sync-incident-replay.test.ts +0 -211
- package/apps/studio/test/sync-journal.test.ts +0 -176
- package/apps/studio/test/sync-meta-codec.test.ts +0 -199
- package/apps/studio/test/sync-migrate-flat-fallback.test.ts +0 -98
- package/apps/studio/test/sync-move-retirement.test.ts +0 -231
- package/apps/studio/test/sync-panel-surface.test.ts +0 -153
- package/apps/studio/test/sync-path-pull.test.ts +0 -531
- package/apps/studio/test/sync-presentation.test.ts +0 -262
- package/apps/studio/test/sync-pull-budget.test.ts +0 -169
- package/apps/studio/test/sync-remote-docs.test.ts +0 -333
- package/apps/studio/test/sync-resync-routes.test.ts +0 -168
- package/apps/studio/test/sync-runtime.test.ts +0 -1675
- package/apps/studio/test/sync-seed-defers-to-hub.test.ts +0 -83
- package/apps/studio/test/sync-settings-routes.test.ts +0 -195
- package/apps/studio/test/sync-settings.test.ts +0 -151
- package/apps/studio/test/sync-status.test.ts +0 -261
- package/apps/studio/test/sync-supervisor.test.ts +0 -262
- package/apps/studio/test/sync-tombstone-apply.test.ts +0 -111
- package/apps/studio/test/sync-trash.test.ts +0 -132
- package/apps/studio/test/sync-two-peer-discovery.test.ts +0 -343
- package/apps/studio/test/sync-untrusted.test.ts +0 -103
- package/apps/studio/test/system-endpoint.test.ts +0 -144
- package/apps/studio/test/text-editability-stamp.test.ts +0 -131
- package/apps/studio/test/timeline-comp-target.test.ts +0 -139
- package/apps/studio/test/timeline-media-cache.test.ts +0 -41
- package/apps/studio/test/timeline-parse.test.ts +0 -184
- package/apps/studio/test/timeline-scale.test.ts +0 -65
- package/apps/studio/test/timeline-snap.test.ts +0 -85
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +0 -86
- package/apps/studio/test/tool-palette.test.tsx +0 -55
- package/apps/studio/test/tour-overlay.test.tsx +0 -90
- package/apps/studio/test/trusted-request-host.test.ts +0 -66
- package/apps/studio/test/undo-sequence-byte-compare.test.ts +0 -211
- package/apps/studio/test/undo-stack.test.ts +0 -244
- package/apps/studio/test/use-agent-presence.test.tsx +0 -117
- package/apps/studio/test/use-annotation-resize.test.ts +0 -402
- package/apps/studio/test/use-annotation-selection.test.tsx +0 -77
- package/apps/studio/test/use-artboard-drag.test.ts +0 -324
- package/apps/studio/test/use-canvas-activity.test.tsx +0 -206
- package/apps/studio/test/use-collab.test.ts +0 -0
- package/apps/studio/test/use-cursor-modifiers.test.ts +0 -59
- package/apps/studio/test/use-keyboard-discipline.test.ts +0 -27
- package/apps/studio/test/use-selection-set.test.tsx +0 -166
- package/apps/studio/test/use-snap-guides.test.ts +0 -271
- package/apps/studio/test/use-tool-mode.test.tsx +0 -186
- package/apps/studio/test/use-undo-stack.test.tsx +0 -223
- package/apps/studio/test/video-asset.test.ts +0 -163
- package/apps/studio/test/video-comp-fixture.test.ts +0 -50
- package/apps/studio/test/video-comp.test.ts +0 -270
- package/apps/studio/test/video-encode-lib.test.ts +0 -63
- package/apps/studio/test/video-render-bridge.test.ts +0 -171
- package/apps/studio/test/whats-new.test.ts +0 -113
- package/apps/studio/test/whisper-setup.test.ts +0 -97
- package/apps/studio/test/workspace-containment.test.ts +0 -382
- package/apps/studio/test/workspace-signin.test.ts +0 -292
- package/apps/studio/test/ws-handshake.test.ts +0 -33
- package/cli/commands/cache.test.mjs +0 -166
- package/cli/commands/design-link.test.mjs +0 -419
- package/cli/commands/design.test.mjs +0 -240
- package/cli/commands/doctor.test.mjs +0 -237
- package/cli/commands/hub-workspace.test.mjs +0 -275
- package/cli/commands/hub.test.mjs +0 -225
- package/cli/commands/kg.test.mjs +0 -142
- package/cli/commands/preflight.test.mjs +0 -46
- package/cli/lib/argv.test.mjs +0 -46
- package/cli/lib/cache.test.mjs +0 -303
- package/cli/lib/cell-plan.test.mjs +0 -225
- package/cli/lib/config-lint.test.mjs +0 -117
- package/cli/lib/ddr-to-kgai.test.mjs +0 -118
- package/cli/lib/design-ownership.test.mjs +0 -329
- package/cli/lib/fetch-asset.test.mjs +0 -213
- package/cli/lib/figma-codegen-reachability.test.mjs +0 -104
- package/cli/lib/figma-import-controls.test.mjs +0 -70
- package/cli/lib/flow-design-integration.test.mjs +0 -165
- package/cli/lib/gitignore-block.test.mjs +0 -136
- package/cli/lib/gitignore-drift.test.mjs +0 -156
- package/cli/lib/hubs-config.test.mjs +0 -165
- package/cli/lib/pkg-root.test.mjs +0 -111
- package/cli/lib/plugin-cli-reachability.test.mjs +0 -57
- package/cli/lib/plugin-name-namespace.test.mjs +0 -71
- package/cli/lib/reconstruct-toolset.test.mjs +0 -194
- package/cli/lib/stack-detect.test.mjs +0 -121
- package/cli/lib/update-check.test.mjs +0 -32
- package/cli/lib/workspace-plan.test.mjs +0 -699
|
@@ -33,6 +33,8 @@ import { sizingModeOf, sizingModePatch } from '../sizing-mode.ts';
|
|
|
33
33
|
// above (a type-only SyncStatusSnapshot import that Bun erases).
|
|
34
34
|
import { syncPresentation } from '../sync/presentation.ts';
|
|
35
35
|
import { canvasUrl } from './canvas-url.js';
|
|
36
|
+
import { applyEditRequest } from './apply-edit-request.ts';
|
|
37
|
+
import { createIndexLoader } from './index-loader.ts';
|
|
36
38
|
import {
|
|
37
39
|
BROWSER_CAPTURE_FORMATS,
|
|
38
40
|
BROWSER_SERVABLE_FORMATS,
|
|
@@ -50,6 +52,8 @@ import GitPanel from './panels/GitPanel.jsx';
|
|
|
50
52
|
import SyncConsentDialog from './panels/SyncConsentDialog.jsx';
|
|
51
53
|
import SyncPanel from './panels/SyncPanel.jsx';
|
|
52
54
|
import CloudBar from './panels/CloudBar.jsx';
|
|
55
|
+
import { buildShareLinks, normalizeOpenPath, readOpenParam, withOpenParam } from './share-link.js';
|
|
56
|
+
import { ShareDialog, copyShareLink } from './share-dialog.jsx';
|
|
53
57
|
import IdentityBar from './panels/IdentityBar.jsx';
|
|
54
58
|
import OnboardingWizard from './panels/OnboardingWizard.jsx';
|
|
55
59
|
import { ReadinessDialog } from './panels/ReadinessList.jsx';
|
|
@@ -119,6 +123,8 @@ import { COLLAB_TOUR } from './tour/collab-tour.js';
|
|
|
119
123
|
import { TourOverlay } from './tour/overlay.jsx';
|
|
120
124
|
import { QUICK_SETUP_TOUR } from './tour/quick-setup-tour.js';
|
|
121
125
|
import { USAGE_TOUR } from './tour/usage-tour.js';
|
|
126
|
+
import { dismissNotice, NotificationHost, notify, notifyCanvasText } from '../notifications.tsx';
|
|
127
|
+
import { acceptCanvasNotice } from '../canvas-notice-message.ts';
|
|
122
128
|
import { ExportBadge, ExportPanel, ExportToast, useExportCenter } from './export-center.jsx';
|
|
123
129
|
import { ReportBugDialog } from './report-bug.jsx';
|
|
124
130
|
import { useWhatsNew, WhatsNewPanel, WhatsNewToast } from './whats-new.jsx';
|
|
@@ -202,6 +208,7 @@ function DockSlot({ side, width, open, ids, activeId, onPick, children, labels =
|
|
|
202
208
|
key={id}
|
|
203
209
|
type="button"
|
|
204
210
|
role="tab"
|
|
211
|
+
data-testid={`dock-tab-${id}`}
|
|
205
212
|
aria-selected={activeId === id}
|
|
206
213
|
className={'st-docktab' + (activeId === id ? ' is-active' : '')}
|
|
207
214
|
onClick={() => onPick(id)}
|
|
@@ -245,6 +252,18 @@ function pathTestIdSlug(p) {
|
|
|
245
252
|
.toLowerCase()
|
|
246
253
|
.replace(/^-+|-+$/g, '');
|
|
247
254
|
}
|
|
255
|
+
// A layers tree's identity: its element ids in document order. Two trees with
|
|
256
|
+
// the same signature are the same DOM as far as positional ids go.
|
|
257
|
+
function layersTreeSig(nodes) {
|
|
258
|
+
const ids = [];
|
|
259
|
+
(function walk(list) {
|
|
260
|
+
for (const n of list || []) {
|
|
261
|
+
ids.push(n?.id ?? '');
|
|
262
|
+
walk(n?.children);
|
|
263
|
+
}
|
|
264
|
+
})(nodes);
|
|
265
|
+
return ids.join(',');
|
|
266
|
+
}
|
|
248
267
|
// Bun's `define` substitutes this at build time (see build.ts); falls back when
|
|
249
268
|
// the bundle is consumed in a context that hasn't run the build.
|
|
250
269
|
const MDCC_VERSION = typeof __MDCC_VERSION__ !== 'undefined' ? __MDCC_VERSION__ : 'dev';
|
|
@@ -331,58 +350,9 @@ function basename(p) {
|
|
|
331
350
|
return p.split('/').pop();
|
|
332
351
|
}
|
|
333
352
|
|
|
334
|
-
//
|
|
335
|
-
// The clip ops used to console.warn their failures (a 422 "series clip can't
|
|
336
|
-
// move" looked like "the button does nothing"). Self-contained DOM (no React
|
|
337
|
-
// state), mirrors the canvas-side showCanvasToast.
|
|
338
|
-
// `action` (feature-file-tree-drag-drop-folders, Task 10) — optional
|
|
339
|
-
// `{ label, onClick }` for an inline undo affordance (the move toast). Absent
|
|
340
|
-
// for every other caller, so their toast stays the plain auto-dismissing text
|
|
341
|
-
// bubble this always was.
|
|
353
|
+
// Keep call sites (including Undo actions) on the shared notification stack.
|
|
342
354
|
function shellToast(message, ok = false, action) {
|
|
343
|
-
|
|
344
|
-
let el = document.getElementById('st-op-toast');
|
|
345
|
-
if (!el) {
|
|
346
|
-
el = document.createElement('div');
|
|
347
|
-
el.id = 'st-op-toast';
|
|
348
|
-
el.setAttribute('role', 'status');
|
|
349
|
-
el.style.cssText =
|
|
350
|
-
'position:fixed;left:50%;bottom:64px;transform:translateX(-50%);z-index:80;' +
|
|
351
|
-
'display:flex;align-items:center;gap:10px;max-width:440px;padding:8px 14px;' +
|
|
352
|
-
'border-radius:8px;font:12px/1.45 var(--font-mono,monospace);' +
|
|
353
|
-
'box-shadow:0 8px 28px rgba(0,0,0,.34);pointer-events:none;opacity:0;transition:opacity 140ms ease;';
|
|
354
|
-
document.body.appendChild(el);
|
|
355
|
-
}
|
|
356
|
-
el.style.background = ok ? '#1d3524' : '#3a1d1d';
|
|
357
|
-
el.style.color = ok ? '#b7e4c0' : '#f1b8b8';
|
|
358
|
-
el.textContent = '';
|
|
359
|
-
const text = document.createElement('span');
|
|
360
|
-
text.textContent = message;
|
|
361
|
-
el.appendChild(text);
|
|
362
|
-
if (action) {
|
|
363
|
-
const btn = document.createElement('button');
|
|
364
|
-
btn.type = 'button';
|
|
365
|
-
btn.textContent = action.label;
|
|
366
|
-
btn.style.cssText =
|
|
367
|
-
'pointer-events:auto;background:none;border:0;text-decoration:underline;' +
|
|
368
|
-
'color:inherit;font:inherit;cursor:pointer;padding:0;';
|
|
369
|
-
btn.onclick = () => {
|
|
370
|
-
action.onClick();
|
|
371
|
-
el.style.opacity = '0';
|
|
372
|
-
el.style.pointerEvents = 'none';
|
|
373
|
-
};
|
|
374
|
-
el.appendChild(btn);
|
|
375
|
-
}
|
|
376
|
-
el.style.opacity = '1';
|
|
377
|
-
el.style.pointerEvents = action ? 'auto' : 'none';
|
|
378
|
-
clearTimeout(shellToast._t);
|
|
379
|
-
shellToast._t = setTimeout(
|
|
380
|
-
() => {
|
|
381
|
-
el.style.opacity = '0';
|
|
382
|
-
el.style.pointerEvents = 'none';
|
|
383
|
-
},
|
|
384
|
-
action ? 6000 : 3200
|
|
385
|
-
);
|
|
355
|
+
notify({ title: message, kind: ok ? 'success' : 'error', action });
|
|
386
356
|
}
|
|
387
357
|
|
|
388
358
|
// DDR-223 (issue #93, supersedes DDR-187's boot half) — one-time first-run
|
|
@@ -402,48 +372,24 @@ function browseFirstRunHint(readOnly = false) {
|
|
|
402
372
|
} catch {
|
|
403
373
|
return;
|
|
404
374
|
}
|
|
405
|
-
if (
|
|
406
|
-
|
|
407
|
-
el.id = 'st-browse-hint';
|
|
408
|
-
el.setAttribute('role', 'status');
|
|
409
|
-
el.style.cssText =
|
|
410
|
-
'position:fixed;left:50%;bottom:64px;transform:translateX(-50%);z-index:81;' +
|
|
411
|
-
'display:flex;align-items:center;gap:10px;max-width:520px;padding:10px 14px;border-radius:10px;' +
|
|
412
|
-
'font:12px/1.5 var(--font-ui,system-ui,sans-serif);background:var(--surface-2,#1b1e24);' +
|
|
413
|
-
'color:var(--text-1,#e7eaf0);border:1px solid var(--border-1,#333a45);' +
|
|
414
|
-
'box-shadow:0 10px 34px rgba(0,0,0,.42);opacity:0;transition:opacity 160ms ease;';
|
|
415
|
-
const kbd =
|
|
416
|
-
'<kbd style="padding:1px 6px;border-radius:5px;border:1px solid var(--border-1,#333a45);' +
|
|
417
|
-
'background:var(--surface-3,#262b33);font-family:var(--font-mono,monospace)">V</kbd>';
|
|
418
|
-
el.innerHTML =
|
|
419
|
-
// Cloud Phase 25 C2 — a viewer selects to inspect, never to edit; the
|
|
420
|
-
// hint must not promise an editor the role doesn't have.
|
|
421
|
-
(readOnly
|
|
422
|
-
? `<span>Your mock is <strong>live</strong> — click things to try it. Press ${kbd} to select & inspect.</span>`
|
|
423
|
-
: '<span>You’re in <strong>Edit</strong> — click selects, like Figma. ' +
|
|
424
|
-
'Switch to <strong>Preview</strong> in the toolbar to use the live mock.</span>') +
|
|
425
|
-
'<button type="button" aria-label="Dismiss" style="background:none;border:none;color:inherit;' +
|
|
426
|
-
'cursor:pointer;font-size:15px;line-height:1;opacity:.65;padding:2px">×</button>';
|
|
375
|
+
if (browseFirstRunHint.active) return;
|
|
376
|
+
browseFirstRunHint.active = true;
|
|
427
377
|
const dismiss = () => {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
} catch {
|
|
431
|
-
/* private mode */
|
|
432
|
-
}
|
|
433
|
-
el.style.opacity = '0';
|
|
434
|
-
setTimeout(() => el.remove(), 200);
|
|
378
|
+
browseFirstRunHint.active = false;
|
|
379
|
+
try { localStorage.setItem(MODE_HINT_SEEN, '1'); } catch {}
|
|
435
380
|
document.removeEventListener('keydown', onV, true);
|
|
436
381
|
};
|
|
437
382
|
const onV = (e) => {
|
|
438
|
-
if ((e.key === 'v' || e.key === 'V') && !e.metaKey && !e.ctrlKey && !e.altKey)
|
|
383
|
+
if ((e.key === 'v' || e.key === 'V') && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
|
384
|
+
dismissNotice('mode-hint');
|
|
385
|
+
dismiss();
|
|
386
|
+
}
|
|
439
387
|
};
|
|
440
|
-
el.querySelector('button')?.addEventListener('click', dismiss);
|
|
441
388
|
if (readOnly) document.addEventListener('keydown', onV, true);
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
setTimeout(dismiss, 9000);
|
|
389
|
+
notify({ id: 'mode-hint', title: readOnly ? 'Your mock is live' : 'You’re in Edit',
|
|
390
|
+
description: readOnly ? 'Click things to try it. Press V to select & inspect.'
|
|
391
|
+
: 'Click selects, like Figma. Switch to Preview in the toolbar to use the live mock.',
|
|
392
|
+
onDismiss: dismiss });
|
|
447
393
|
}
|
|
448
394
|
|
|
449
395
|
// Strip canvas extensions for display. `Canvas Viewport.tsx` → `Canvas Viewport`.
|
|
@@ -1218,6 +1164,37 @@ const PDF_DPI_OPTIONS = [
|
|
|
1218
1164
|
];
|
|
1219
1165
|
const PDF_DPI_DEFAULT = 'auto';
|
|
1220
1166
|
|
|
1167
|
+
// issue #116 — how the export treats text. Mirrors export-dialog.tsx's
|
|
1168
|
+
// PDF_TEXT_OPTIONS (same mirror obligation as PDF_DPI_OPTIONS above).
|
|
1169
|
+
//
|
|
1170
|
+
// The list is FIXED regardless of whether Ghostscript is present on this
|
|
1171
|
+
// machine. Hiding "Convert to outlines" when gs is missing would need a new
|
|
1172
|
+
// capability route, and would answer the user's question ("can I send this to
|
|
1173
|
+
// a printer?") by making the answer invisible; a refusal that names the
|
|
1174
|
+
// one-line install is more honest and reaches them at the moment they care.
|
|
1175
|
+
const PDF_TEXT_OPTIONS = [
|
|
1176
|
+
{
|
|
1177
|
+
id: 'keep',
|
|
1178
|
+
label: 'Keep selectable (default)',
|
|
1179
|
+
description:
|
|
1180
|
+
'Text stays selectable and searchable. You are warned if a font came out unprintable.',
|
|
1181
|
+
},
|
|
1182
|
+
{
|
|
1183
|
+
id: 'embed',
|
|
1184
|
+
label: 'Verify fonts embedded',
|
|
1185
|
+
description:
|
|
1186
|
+
'Same file, but the export fails instead of shipping a font a print shop would reject.',
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
id: 'outline',
|
|
1190
|
+
label: 'Convert to outlines (print-safe)',
|
|
1191
|
+
description:
|
|
1192
|
+
'Every glyph becomes a vector path — no fonts left to break. Text is no longer selectable ' +
|
|
1193
|
+
'and the file can grow a lot. Needs Ghostscript installed locally; cloud workspaces have it.',
|
|
1194
|
+
},
|
|
1195
|
+
];
|
|
1196
|
+
const PDF_TEXT_DEFAULT = 'keep';
|
|
1197
|
+
|
|
1221
1198
|
// feature-bulk-media-insert — best-effort MIME guess from a filename extension.
|
|
1222
1199
|
// Only used to classify a natively-picked file (Rust returns `{name, bytes}`,
|
|
1223
1200
|
// no type) for the destination-toggle's image/video/audio split; the actual
|
|
@@ -1602,6 +1579,7 @@ function ExportDialog({
|
|
|
1602
1579
|
const [pdfMarksCrop, setPdfMarksCrop] = useState(false);
|
|
1603
1580
|
const [pdfMarksRegistration, setPdfMarksRegistration] = useState(false);
|
|
1604
1581
|
const [pdfDpiId, setPdfDpiId] = useState(PDF_DPI_DEFAULT);
|
|
1582
|
+
const [pdfTextId, setPdfTextId] = useState(PDF_TEXT_DEFAULT);
|
|
1605
1583
|
// DDR-148 addendum — mp4/webm of a registered video-comp render through
|
|
1606
1584
|
// renderMediaOnWeb, which produces real audio (Remotion owns the
|
|
1607
1585
|
// TransitionSeries/volume-closure timeline). gif has no audio track at all
|
|
@@ -1714,6 +1692,10 @@ function ExportDialog({
|
|
|
1714
1692
|
// itself stays vector regardless of this.
|
|
1715
1693
|
const pdfDpi = PDF_DPI_OPTIONS.find((d) => d.id === pdfDpiId)?.value;
|
|
1716
1694
|
if (pdfDpi !== undefined) options.dpi = pdfDpi;
|
|
1695
|
+
// issue #116 — omitted for `keep` rather than sent explicitly: the
|
|
1696
|
+
// adapter's default IS keep, so an untouched dialog produces the exact
|
|
1697
|
+
// request body it did before this feature existed.
|
|
1698
|
+
if (pdfTextId !== 'keep') options.text = pdfTextId;
|
|
1717
1699
|
}
|
|
1718
1700
|
// Scope targeting hints (resolveScope reads these): `artboardId` makes
|
|
1719
1701
|
// "Active artboard" export the right screen instead of `:first-of-type`;
|
|
@@ -2022,6 +2004,27 @@ function ExportDialog({
|
|
|
2022
2004
|
The page stays vector; this only sets the capture density for raster content on it
|
|
2023
2005
|
(dropped photos, large-format art).
|
|
2024
2006
|
</div>
|
|
2007
|
+
<div className="st-dialog-row">
|
|
2008
|
+
<label className="st-dialog-lbl" htmlFor="st-export-pdf-text">
|
|
2009
|
+
Text
|
|
2010
|
+
</label>
|
|
2011
|
+
<select
|
|
2012
|
+
id="st-export-pdf-text"
|
|
2013
|
+
className="st-select"
|
|
2014
|
+
data-testid="export-pdf-text"
|
|
2015
|
+
value={pdfTextId}
|
|
2016
|
+
onChange={(e) => setPdfTextId(e.target.value)}
|
|
2017
|
+
>
|
|
2018
|
+
{PDF_TEXT_OPTIONS.map((t) => (
|
|
2019
|
+
<option key={t.id} value={t.id}>
|
|
2020
|
+
{t.label}
|
|
2021
|
+
</option>
|
|
2022
|
+
))}
|
|
2023
|
+
</select>
|
|
2024
|
+
</div>
|
|
2025
|
+
<div className="st-mono" style={{ fontSize: 11, color: 'var(--fg-3)' }}>
|
|
2026
|
+
{PDF_TEXT_OPTIONS.find((t) => t.id === pdfTextId)?.description}
|
|
2027
|
+
</div>
|
|
2025
2028
|
<div className="st-dialog-row">
|
|
2026
2029
|
<label
|
|
2027
2030
|
className="st-dialog-lbl"
|
|
@@ -2311,6 +2314,10 @@ function FileRow({
|
|
|
2311
2314
|
// feature-file-tree-drag-drop-folders (Task 9) — the KEYBOARD path for
|
|
2312
2315
|
// "Move to…" (drag-only fails WCAG 2.1.1). Same eligibility as dragging.
|
|
2313
2316
|
const canMove = draggableRow && typeof menu !== 'undefined';
|
|
2317
|
+
// Plan T17/L03 — a supporting file (notes, styles, images, media) in a
|
|
2318
|
+
// canvas folder gets the same ⋯ menu: rename, move, delete.
|
|
2319
|
+
const supporting = !isCanvas && !!pKind && !sidecar && typeof menu !== 'undefined';
|
|
2320
|
+
const canShare = typeof menu !== 'undefined' && kind !== 'runtime';
|
|
2314
2321
|
const fileDir = file.path.split('/').slice(0, -1).join('/');
|
|
2315
2322
|
// Stable hook for the desktop E2E harness (data-testid convention — see the
|
|
2316
2323
|
// `desktop-e2e` skill): canvas rows only, slug derived from the relative path
|
|
@@ -2328,7 +2335,7 @@ function FileRow({
|
|
|
2328
2335
|
<button
|
|
2329
2336
|
type="button"
|
|
2330
2337
|
role="treeitem"
|
|
2331
|
-
data-testid={testId}
|
|
2338
|
+
data-testid={testId ?? (supporting ? `file-row-${pathTestIdSlug(file.path)}` : undefined)}
|
|
2332
2339
|
aria-selected={isSel}
|
|
2333
2340
|
aria-disabled={inert ? 'true' : undefined}
|
|
2334
2341
|
aria-busy={isBusy || undefined}
|
|
@@ -2347,7 +2354,17 @@ function FileRow({
|
|
|
2347
2354
|
else if (pKind) onPreview?.(file.path);
|
|
2348
2355
|
}}
|
|
2349
2356
|
onContextMenu={
|
|
2350
|
-
|
|
2357
|
+
canShare
|
|
2358
|
+
? (e) =>
|
|
2359
|
+
menu.openAt(e, {
|
|
2360
|
+
kind: 'file',
|
|
2361
|
+
path: file.path,
|
|
2362
|
+
dir: fileDir,
|
|
2363
|
+
canMove: canMove || supporting,
|
|
2364
|
+
supporting,
|
|
2365
|
+
name: file.name,
|
|
2366
|
+
})
|
|
2367
|
+
: undefined
|
|
2351
2368
|
}
|
|
2352
2369
|
{...dragHandlers}
|
|
2353
2370
|
>
|
|
@@ -2381,21 +2398,30 @@ function FileRow({
|
|
|
2381
2398
|
{oc > 0 && <span className="st-row-badge">{oc}</span>}
|
|
2382
2399
|
</button>
|
|
2383
2400
|
);
|
|
2384
|
-
if (!canDelete && !
|
|
2401
|
+
if (!canDelete && !canShare) return row;
|
|
2385
2402
|
// Sibling menu/delete buttons (can't nest a button in the row button). The
|
|
2386
2403
|
// wrapper is presentational so the treeitem stays the tree's child for a11y.
|
|
2387
2404
|
return (
|
|
2388
2405
|
<div className="st-row-wrap" role="none">
|
|
2389
2406
|
{row}
|
|
2390
|
-
{
|
|
2407
|
+
{canShare && (
|
|
2391
2408
|
<button
|
|
2392
2409
|
type="button"
|
|
2393
2410
|
className={'st-row-menu-btn' + (canDelete ? ' has-delete-sibling' : '')}
|
|
2394
2411
|
data-testid={`tree-row-menu-${pathTestIdSlug(file.path)}`}
|
|
2395
|
-
title={`
|
|
2412
|
+
title={`Actions for ${label}`}
|
|
2396
2413
|
aria-label={`Actions for ${label}`}
|
|
2397
2414
|
aria-haspopup="menu"
|
|
2398
|
-
onClick={(e) =>
|
|
2415
|
+
onClick={(e) =>
|
|
2416
|
+
menu.openAt(e, {
|
|
2417
|
+
kind: 'file',
|
|
2418
|
+
path: file.path,
|
|
2419
|
+
dir: fileDir,
|
|
2420
|
+
canMove: canMove || supporting,
|
|
2421
|
+
supporting,
|
|
2422
|
+
name: file.name,
|
|
2423
|
+
})
|
|
2424
|
+
}
|
|
2399
2425
|
>
|
|
2400
2426
|
<Icon d="M12 6a1 1 0 100-2 1 1 0 000 2zM12 13a1 1 0 100-2 1 1 0 000 2zM12 20a1 1 0 100-2 1 1 0 000 2z" size={12} />
|
|
2401
2427
|
</button>
|
|
@@ -2469,6 +2495,7 @@ function CanvasRow({
|
|
|
2469
2495
|
const isDragging = drag?.draggedPath === primary.path;
|
|
2470
2496
|
const isBusy = drag?.busyPath === primary.path;
|
|
2471
2497
|
const canMove = draggableRow && typeof menu !== 'undefined';
|
|
2498
|
+
const canShare = typeof menu !== 'undefined' && kind !== 'runtime';
|
|
2472
2499
|
const primaryDir = primary.path.split('/').slice(0, -1).join('/');
|
|
2473
2500
|
const row = (
|
|
2474
2501
|
<button
|
|
@@ -2495,7 +2522,7 @@ function CanvasRow({
|
|
|
2495
2522
|
onOpen(primary.path);
|
|
2496
2523
|
}}
|
|
2497
2524
|
onContextMenu={
|
|
2498
|
-
|
|
2525
|
+
canShare ? (e) => menu.openAt(e, { kind: 'file', path: primary.path, dir: primaryDir, canMove }) : undefined
|
|
2499
2526
|
}
|
|
2500
2527
|
{...dragHandlers}
|
|
2501
2528
|
>
|
|
@@ -2537,17 +2564,17 @@ function CanvasRow({
|
|
|
2537
2564
|
);
|
|
2538
2565
|
return (
|
|
2539
2566
|
<Fragment>
|
|
2540
|
-
{
|
|
2567
|
+
{canShare ? (
|
|
2541
2568
|
<div className="st-row-wrap" role="none">
|
|
2542
2569
|
{row}
|
|
2543
2570
|
<button
|
|
2544
2571
|
type="button"
|
|
2545
2572
|
className="st-row-menu-btn"
|
|
2546
2573
|
data-testid={`tree-row-menu-${pathTestIdSlug(primary.path)}`}
|
|
2547
|
-
title={`
|
|
2574
|
+
title={`Actions for ${displayName(primary.name)}`}
|
|
2548
2575
|
aria-label={`Actions for ${displayName(primary.name)}`}
|
|
2549
2576
|
aria-haspopup="menu"
|
|
2550
|
-
onClick={(e) => menu.openAt(e, { kind: 'file', path: primary.path, dir: primaryDir })}
|
|
2577
|
+
onClick={(e) => menu.openAt(e, { kind: 'file', path: primary.path, dir: primaryDir, canMove })}
|
|
2551
2578
|
>
|
|
2552
2579
|
<Icon d="M12 6a1 1 0 100-2 1 1 0 000 2zM12 13a1 1 0 100-2 1 1 0 000 2zM12 20a1 1 0 100-2 1 1 0 000 2z" size={12} />
|
|
2553
2580
|
</button>
|
|
@@ -2560,6 +2587,7 @@ function CanvasRow({
|
|
|
2560
2587
|
<FileRow
|
|
2561
2588
|
key={sc.path}
|
|
2562
2589
|
file={sc}
|
|
2590
|
+
menu={menu}
|
|
2563
2591
|
activePath={activePath}
|
|
2564
2592
|
previewPath={previewPath}
|
|
2565
2593
|
onOpen={onOpen}
|
|
@@ -2666,6 +2694,7 @@ function Tree({
|
|
|
2666
2694
|
<FileRow
|
|
2667
2695
|
key={entry.primary.path}
|
|
2668
2696
|
file={entry.primary}
|
|
2697
|
+
menu={menu}
|
|
2669
2698
|
activePath={activePath}
|
|
2670
2699
|
previewPath={previewPath}
|
|
2671
2700
|
onOpen={onOpen}
|
|
@@ -2680,6 +2709,7 @@ function Tree({
|
|
|
2680
2709
|
<FileRow
|
|
2681
2710
|
key={entry.primary.path}
|
|
2682
2711
|
file={entry.primary}
|
|
2712
|
+
menu={menu}
|
|
2683
2713
|
activePath={activePath}
|
|
2684
2714
|
previewPath={previewPath}
|
|
2685
2715
|
onOpen={onOpen}
|
|
@@ -2738,7 +2768,7 @@ function Tree({
|
|
|
2738
2768
|
defaultOpen={true}
|
|
2739
2769
|
dirPath={childPath}
|
|
2740
2770
|
drag={drag}
|
|
2741
|
-
menu={menu}
|
|
2771
|
+
menu={drag ? menu : undefined}
|
|
2742
2772
|
>
|
|
2743
2773
|
{childTree}
|
|
2744
2774
|
</DirRow>
|
|
@@ -2811,12 +2841,20 @@ function Sidebar({
|
|
|
2811
2841
|
onMoveCanvas,
|
|
2812
2842
|
onNewFolder,
|
|
2813
2843
|
onDeleteFolder,
|
|
2844
|
+
onRenameFolder,
|
|
2845
|
+
onRenameCanvas,
|
|
2846
|
+
onDuplicateCanvas,
|
|
2847
|
+
onDeleteFile,
|
|
2814
2848
|
// Passed through to CloudBar only — the live `sync:status` payload that makes
|
|
2815
2849
|
// the connect note follow the link instead of freezing at attach time.
|
|
2816
2850
|
syncStatus,
|
|
2817
2851
|
// Passed through to CloudBar only — lifts linkedHub changes to the app shell
|
|
2818
2852
|
// so the GitPanel's cloud-managed posture (DDR-218) reacts live.
|
|
2819
2853
|
onLinkedHub,
|
|
2854
|
+
onLocalProject,
|
|
2855
|
+
onOpenLinkedFile,
|
|
2856
|
+
filesReady,
|
|
2857
|
+
onShare,
|
|
2820
2858
|
// feature-cloud-managed-git-posture — the widened DDR-218 gate, resolved once
|
|
2821
2859
|
// in App and handed down. Withdraws the drafts switcher: a local branch
|
|
2822
2860
|
// switch moves a HEAD the cell knows nothing about.
|
|
@@ -2852,7 +2890,67 @@ function Sidebar({
|
|
|
2852
2890
|
const menuExtra = rowMenu.state?.extra;
|
|
2853
2891
|
const rowMenuRootItems =
|
|
2854
2892
|
menuExtra?.kind === 'file'
|
|
2855
|
-
? [
|
|
2893
|
+
? [
|
|
2894
|
+
{ id: 'share', label: 'Share…', onSelect: () => { rowMenu.close(); onShare(menuExtra.path); } },
|
|
2895
|
+
...(onRenameCanvas && /\.tsx$/i.test(menuExtra.path)
|
|
2896
|
+
? [
|
|
2897
|
+
{
|
|
2898
|
+
id: 'rename-canvas',
|
|
2899
|
+
label: 'Rename…',
|
|
2900
|
+
onSelect: () => {
|
|
2901
|
+
rowMenu.close();
|
|
2902
|
+
const current = displayName(menuExtra.path.split('/').pop());
|
|
2903
|
+
const name = window.prompt('Rename canvas to:', current);
|
|
2904
|
+
if (name?.trim() && name.trim() !== current) onRenameCanvas(menuExtra.path, name.trim());
|
|
2905
|
+
},
|
|
2906
|
+
},
|
|
2907
|
+
]
|
|
2908
|
+
: []),
|
|
2909
|
+
...(onDuplicateCanvas && /\.tsx$/i.test(menuExtra.path)
|
|
2910
|
+
? [
|
|
2911
|
+
{
|
|
2912
|
+
id: 'duplicate-canvas',
|
|
2913
|
+
label: 'Duplicate',
|
|
2914
|
+
onSelect: () => {
|
|
2915
|
+
rowMenu.close();
|
|
2916
|
+
onDuplicateCanvas(menuExtra.path);
|
|
2917
|
+
},
|
|
2918
|
+
},
|
|
2919
|
+
]
|
|
2920
|
+
: []),
|
|
2921
|
+
// Plan T17/L03 — a supporting file renames as itself (the extension
|
|
2922
|
+
// stays: a rename is not a conversion).
|
|
2923
|
+
...(menuExtra.supporting && onRenameCanvas
|
|
2924
|
+
? [
|
|
2925
|
+
{
|
|
2926
|
+
id: 'rename-file',
|
|
2927
|
+
label: 'Rename…',
|
|
2928
|
+
onSelect: () => {
|
|
2929
|
+
rowMenu.close();
|
|
2930
|
+
const n = menuExtra.name ?? menuExtra.path.split('/').pop();
|
|
2931
|
+
const ext = n.includes('.') ? n.slice(n.lastIndexOf('.')) : '';
|
|
2932
|
+
const current = ext ? n.slice(0, -ext.length) : n;
|
|
2933
|
+
const name = window.prompt(`Rename ${n} to:`, current);
|
|
2934
|
+
if (name?.trim() && name.trim() !== current) onRenameCanvas(menuExtra.path, name.trim());
|
|
2935
|
+
},
|
|
2936
|
+
},
|
|
2937
|
+
]
|
|
2938
|
+
: []),
|
|
2939
|
+
...(menuExtra.canMove ? [{ id: 'move-to', label: 'Move to…', onSelect: () => rowMenu.showMoveTo() }] : []),
|
|
2940
|
+
...(menuExtra.supporting && onDeleteFile
|
|
2941
|
+
? [
|
|
2942
|
+
{
|
|
2943
|
+
id: 'delete-file',
|
|
2944
|
+
label: 'Delete',
|
|
2945
|
+
destructive: true,
|
|
2946
|
+
onSelect: () => {
|
|
2947
|
+
rowMenu.close();
|
|
2948
|
+
onDeleteFile(menuExtra.path, menuExtra.name ?? menuExtra.path.split('/').pop());
|
|
2949
|
+
},
|
|
2950
|
+
},
|
|
2951
|
+
]
|
|
2952
|
+
: []),
|
|
2953
|
+
]
|
|
2856
2954
|
: menuExtra?.kind === 'dir'
|
|
2857
2955
|
? [
|
|
2858
2956
|
{
|
|
@@ -2864,6 +2962,16 @@ function Sidebar({
|
|
|
2864
2962
|
if (name?.trim()) onNewFolder(menuExtra.dirPath, name.trim());
|
|
2865
2963
|
},
|
|
2866
2964
|
},
|
|
2965
|
+
{
|
|
2966
|
+
id: 'rename-folder',
|
|
2967
|
+
label: 'Rename folder',
|
|
2968
|
+
onSelect: () => {
|
|
2969
|
+
rowMenu.close();
|
|
2970
|
+
const current = menuExtra.dirPath.split('/').pop();
|
|
2971
|
+
const name = window.prompt('Rename folder to:', current);
|
|
2972
|
+
if (name?.trim() && name.trim() !== current) onRenameFolder(menuExtra.dirPath, name.trim());
|
|
2973
|
+
},
|
|
2974
|
+
},
|
|
2867
2975
|
{
|
|
2868
2976
|
id: 'delete-folder',
|
|
2869
2977
|
label: 'Delete folder',
|
|
@@ -2876,7 +2984,7 @@ function Sidebar({
|
|
|
2876
2984
|
]
|
|
2877
2985
|
: [];
|
|
2878
2986
|
const rowMenuDestinations =
|
|
2879
|
-
menuExtra?.kind === 'file'
|
|
2987
|
+
menuExtra?.kind === 'file' || menuExtra?.kind === 'supporting'
|
|
2880
2988
|
? destinations.filter((d) => d.path !== menuExtra.dir)
|
|
2881
2989
|
: [];
|
|
2882
2990
|
|
|
@@ -3168,7 +3276,7 @@ function Sidebar({
|
|
|
3168
3276
|
canvasKinds={canvasKinds}
|
|
3169
3277
|
dirPath={g.fullPath}
|
|
3170
3278
|
drag={!isDs && g.kind === 'canvas' && !readOnly ? treeDrag : undefined}
|
|
3171
|
-
menu={
|
|
3279
|
+
menu={rowMenu}
|
|
3172
3280
|
/>
|
|
3173
3281
|
) : (
|
|
3174
3282
|
<div className="st-tree-empty">{search ? 'No matches.' : 'Empty.'}</div>
|
|
@@ -3196,9 +3304,9 @@ function Sidebar({
|
|
|
3196
3304
|
and `/_api/git/checkout` keep exactly their old gates, and a terminal
|
|
3197
3305
|
`git checkout` still works (and still flushes into Yjs via DDR-051's
|
|
3198
3306
|
watcher). Absent, not disabled — there is nothing to explain here. */}
|
|
3199
|
-
{
|
|
3200
|
-
|
|
3201
|
-
|
|
3307
|
+
{/* Plan T22 — withdrawn means the BRANCH half. A managed project still
|
|
3308
|
+
needs the way to another project, so the dock stays, project-only. */}
|
|
3309
|
+
<RepoBranchSwitcher project={project} liveBranch={gitBranch} remoteSync={remoteSync} onGetLatest={onGetLatest} projectOnly={savingIsManaged} />
|
|
3202
3310
|
{/* Cloud Phase 23 C3 — Maude Cloud sign-in + remote-project attach, docked
|
|
3203
3311
|
above the GitHub identity. Dev-server-backed, so it works in the desktop
|
|
3204
3312
|
shell AND a plain browser. */}
|
|
@@ -3207,7 +3315,7 @@ function Sidebar({
|
|
|
3207
3315
|
Absent rather than disabled, because unlike the agent chat there is
|
|
3208
3316
|
nothing here to explain — the capability is not missing, it is
|
|
3209
3317
|
already satisfied. */}
|
|
3210
|
-
{cloud === null ? <CloudBar syncStatus={syncStatus} onLinkedHub={onLinkedHub} /> : null}
|
|
3318
|
+
{cloud === null ? <CloudBar syncStatus={syncStatus} onLinkedHub={onLinkedHub} onLocalProject={onLocalProject} onOpenFile={onOpenLinkedFile} filesReady={filesReady} /> : null}
|
|
3211
3319
|
{/* Phase 28 (E3) — GitHub identity as a compact avatar docked at the BOTTOM:
|
|
3212
3320
|
sign in, connected account + New/Pull/Share, sign out. Self-contained
|
|
3213
3321
|
(owns its device-code + CreateProject dialogs). Renders nothing in browser. */}
|
|
@@ -3916,12 +4024,13 @@ function ToolsDropdown({ onAction, onClose, readOnly = false }) {
|
|
|
3916
4024
|
|
|
3917
4025
|
// Plan C follow-up — File + Edit menus, previously inert. Both dispatch to real
|
|
3918
4026
|
// shell flows (File) or the in-canvas undo stack / selection bridges (Edit).
|
|
3919
|
-
function FileDropdown({ onAction, onClose, hasCanvas, readOnly = false }) {
|
|
4027
|
+
function FileDropdown({ onAction, onClose, hasCanvas, hasSharePath, readOnly = false }) {
|
|
3920
4028
|
// Cloud Phase 25 C2 — a viewer keeps the reads (export, handoff, reload,
|
|
3921
4029
|
// close); create / assemble / generate / settings are absent.
|
|
3922
4030
|
const items = readOnly
|
|
3923
4031
|
? [
|
|
3924
4032
|
{ id: 'export', label: 'Export…', shortcut: '⇧⌘E' },
|
|
4033
|
+
{ id: 'share', label: 'Share link…', disabled: !hasSharePath },
|
|
3925
4034
|
{ id: 'handoff', label: 'Handoff to production', shortcut: '⇧⌘H' },
|
|
3926
4035
|
{ sep: true },
|
|
3927
4036
|
{ id: 'reload', label: 'Reload canvas', shortcut: '⌘R', disabled: !hasCanvas },
|
|
@@ -3934,6 +4043,7 @@ function FileDropdown({ onAction, onClose, hasCanvas, readOnly = false }) {
|
|
|
3934
4043
|
// dropped as reference chips on the active canvas.
|
|
3935
4044
|
{ id: 'assemble', label: 'Assemble dropped clips → video', disabled: !hasCanvas },
|
|
3936
4045
|
{ id: 'export', label: 'Export…', shortcut: '⇧⌘E' },
|
|
4046
|
+
{ id: 'share', label: 'Share link…', disabled: !hasSharePath },
|
|
3937
4047
|
{ id: 'handoff', label: 'Handoff to production', shortcut: '⇧⌘H' },
|
|
3938
4048
|
{ sep: true },
|
|
3939
4049
|
// feature-ai-media-generation (DDR-16x) — BYOK generate action + settings.
|
|
@@ -3995,6 +4105,8 @@ function EditDropdown({ onAction, onClose, hasCanvas, readOnly = false }) {
|
|
|
3995
4105
|
|
|
3996
4106
|
function Menubar({
|
|
3997
4107
|
activePath,
|
|
4108
|
+
sharePath,
|
|
4109
|
+
onShare,
|
|
3998
4110
|
project,
|
|
3999
4111
|
/** `{ dashboardUrl?, projectName }` when this is a cloud tab, else null. */
|
|
4000
4112
|
// Tri-state (see the note where this value is created): `undefined` until
|
|
@@ -4333,6 +4445,7 @@ function Menubar({
|
|
|
4333
4445
|
type="button"
|
|
4334
4446
|
className="st-menu"
|
|
4335
4447
|
role="menuitem"
|
|
4448
|
+
data-testid={`menu-${key}`}
|
|
4336
4449
|
data-tour={key === 'help' ? 'help' : undefined}
|
|
4337
4450
|
aria-haspopup={hasDropdown ? 'menu' : undefined}
|
|
4338
4451
|
aria-expanded={hasDropdown ? open : undefined}
|
|
@@ -4352,10 +4465,12 @@ function Menubar({
|
|
|
4352
4465
|
<FileDropdown
|
|
4353
4466
|
readOnly={readOnly}
|
|
4354
4467
|
hasCanvas={!!activePath}
|
|
4468
|
+
hasSharePath={!!sharePath}
|
|
4355
4469
|
onAction={(id) => {
|
|
4356
4470
|
if (id === 'new') onNewCanvas?.();
|
|
4357
4471
|
else if (id === 'assemble') onAssembleVideo?.();
|
|
4358
4472
|
else if (id === 'export') onOpenExport?.('export');
|
|
4473
|
+
else if (id === 'share') { document.querySelector('[data-testid="menu-file"]')?.focus(); onShare?.(); }
|
|
4359
4474
|
else if (id === 'handoff') onOpenExport?.('handoff');
|
|
4360
4475
|
else if (id === 'generate') onOpenGenerate?.();
|
|
4361
4476
|
else if (id === 'settings') onOpenSettings?.();
|
|
@@ -4499,6 +4614,7 @@ function Menubar({
|
|
|
4499
4614
|
<StIcon name="sparkle" size={15} />
|
|
4500
4615
|
</button>
|
|
4501
4616
|
)}
|
|
4617
|
+
<button type="button" className="st-mb-icon st-share-btn" data-testid="share-btn" data-tip={sharePath ? 'Share link' : 'Open a canvas to share it'} aria-label="Share link" disabled={!sharePath} onClick={onShare}><StIcon name="share" size={15} /></button>
|
|
4502
4618
|
{exportCenter && <ExportBadge center={exportCenter} />}
|
|
4503
4619
|
<button
|
|
4504
4620
|
type="button"
|
|
@@ -4548,6 +4664,10 @@ function Viewport({
|
|
|
4548
4664
|
cfg,
|
|
4549
4665
|
loadingPath,
|
|
4550
4666
|
onIframeLoad,
|
|
4667
|
+
canvasError,
|
|
4668
|
+
canvasReloadNonce,
|
|
4669
|
+
onRetryCanvasLoad,
|
|
4670
|
+
loadedPath,
|
|
4551
4671
|
showQuickSetup,
|
|
4552
4672
|
onStartQuickSetup,
|
|
4553
4673
|
previewPath,
|
|
@@ -4564,8 +4684,21 @@ function Viewport({
|
|
|
4564
4684
|
useEffect(() => {
|
|
4565
4685
|
if (previewPath) previewRef.current?.focus();
|
|
4566
4686
|
}, [previewPath]);
|
|
4687
|
+
// One observable word for "what is the canvas pane actually doing" — the
|
|
4688
|
+
// top-frame signal the #115 E2E scenario waits on, and the only place these
|
|
4689
|
+
// three states are named together. `ready` is `dgn:'loaded'`-backed, so it
|
|
4690
|
+
// cannot be faked by a timer expiring (which is exactly how the bug used to
|
|
4691
|
+
// look successful: skeleton gone, pane white).
|
|
4692
|
+
const canvasState =
|
|
4693
|
+
canvasError && canvasError.path === activePath
|
|
4694
|
+
? 'error'
|
|
4695
|
+
: activePath && activePath !== SYSTEM_TAB && loadedPath === activePath
|
|
4696
|
+
? 'ready'
|
|
4697
|
+
: loadingPath && loadingPath === activePath
|
|
4698
|
+
? 'loading'
|
|
4699
|
+
: 'idle';
|
|
4567
4700
|
return (
|
|
4568
|
-
<div className="viewport st-stage" data-tour="viewport">
|
|
4701
|
+
<div className="viewport st-stage" data-tour="viewport" data-canvas-state={canvasState}>
|
|
4569
4702
|
{previewPath && (
|
|
4570
4703
|
// feature-studio-file-preview — an overlay, not a tab: the canvas
|
|
4571
4704
|
// iframe (if any) stays mounted underneath so switching back to it
|
|
@@ -4638,7 +4771,10 @@ function Viewport({
|
|
|
4638
4771
|
}
|
|
4639
4772
|
return (
|
|
4640
4773
|
<iframe
|
|
4641
|
-
key
|
|
4774
|
+
// The nonce is part of the key ONLY so the #115 Retry can force a
|
|
4775
|
+
// remount; it is otherwise constant, so normal switching keys on the
|
|
4776
|
+
// path exactly as before.
|
|
4777
|
+
key={`${t.path}#${canvasReloadNonce}`}
|
|
4642
4778
|
ref={(el) => registerIframe(t.path, el)}
|
|
4643
4779
|
src={canvasUrl(t.path, cfg)}
|
|
4644
4780
|
className={t.path === activePath ? 'active' : ''}
|
|
@@ -4669,6 +4805,45 @@ function Viewport({
|
|
|
4669
4805
|
</div>
|
|
4670
4806
|
</div>
|
|
4671
4807
|
)}
|
|
4808
|
+
{canvasError && canvasError.path === activePath && (
|
|
4809
|
+
// issue #115 — what the blank pane used to be. Names which of the two
|
|
4810
|
+
// failures happened and offers the recovery that used to require
|
|
4811
|
+
// quitting the app (or knowing that switching projects and back
|
|
4812
|
+
// re-reads /_config).
|
|
4813
|
+
<div className="st-canvas-error" role="alert" data-testid="canvas-load-error">
|
|
4814
|
+
<div className="st-canvas-error-card">
|
|
4815
|
+
<div className="st-canvas-error-title">
|
|
4816
|
+
{canvasError.kind === 'server'
|
|
4817
|
+
? "Maude's server isn't responding"
|
|
4818
|
+
: "This canvas didn't finish loading"}
|
|
4819
|
+
</div>
|
|
4820
|
+
<div className="st-canvas-error-body">
|
|
4821
|
+
{canvasError.kind === 'server' ? (
|
|
4822
|
+
<>
|
|
4823
|
+
The canvas couldn't be reached. This usually means Maude's
|
|
4824
|
+
server restarted underneath this window — your files on disk
|
|
4825
|
+
are untouched.
|
|
4826
|
+
</>
|
|
4827
|
+
) : (
|
|
4828
|
+
<>
|
|
4829
|
+
The server is up, but{' '}
|
|
4830
|
+
<code>{sanitizeDisplayText(basename(canvasError.path))}</code>{' '}
|
|
4831
|
+
never finished compiling. Its own error, if it has one, is in
|
|
4832
|
+
the canvas frame.
|
|
4833
|
+
</>
|
|
4834
|
+
)}
|
|
4835
|
+
</div>
|
|
4836
|
+
<button
|
|
4837
|
+
type="button"
|
|
4838
|
+
className="btn btn--primary"
|
|
4839
|
+
data-testid="canvas-load-retry"
|
|
4840
|
+
onClick={() => onRetryCanvasLoad?.(canvasError.path)}
|
|
4841
|
+
>
|
|
4842
|
+
Reload canvas
|
|
4843
|
+
</button>
|
|
4844
|
+
</div>
|
|
4845
|
+
</div>
|
|
4846
|
+
)}
|
|
4672
4847
|
</div>
|
|
4673
4848
|
);
|
|
4674
4849
|
}
|
|
@@ -5504,6 +5679,11 @@ const CSS_JUSTIFY = [
|
|
|
5504
5679
|
'space-evenly',
|
|
5505
5680
|
];
|
|
5506
5681
|
const CSS_WEIGHTS = ['300', '400', '500', '600', '700', '800'];
|
|
5682
|
+
/** What an edit route says the write replaced (`previous`, read under the file
|
|
5683
|
+
* lock) — the value an undo must restore. `fallback` only when the route could
|
|
5684
|
+
* not name it (an expression, or an older server). */
|
|
5685
|
+
const replacedValue = (j, fallback) =>
|
|
5686
|
+
j && Object.hasOwn(j, 'previous') ? j.previous : fallback;
|
|
5507
5687
|
const CSS_FONTS = [
|
|
5508
5688
|
'inherit',
|
|
5509
5689
|
'system-ui',
|
|
@@ -6396,12 +6576,16 @@ function CssKnobs({ el, cfg, onOptimistic, onRecordEdit, onReplaceMedia, onUndoR
|
|
|
6396
6576
|
// no longer triggers a reselect that would re-post fresh `authored` values — so
|
|
6397
6577
|
// the panel must reflect its own commits immediately or it shows the stale
|
|
6398
6578
|
// pre-edit value until the user re-selects. Each commit/reset writes here;
|
|
6399
|
-
// `null` marks a removed key. Cleared when a different element is selected
|
|
6579
|
+
// `null` marks a removed key. Cleared when a different element is selected —
|
|
6580
|
+
// and whenever the canvas re-posts this selection's maps. A re-post carries
|
|
6581
|
+
// the source as it is NOW (a teammate's change reloads the canvas); keeping
|
|
6582
|
+
// our older commit over it showed a stale value and recorded it as the next
|
|
6583
|
+
// undo's `before`, so Cmd+Z restored a value nobody had on screen.
|
|
6400
6584
|
const [overlay, setOverlay] = useState({ a: {}, c: {}, t: {} });
|
|
6401
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: clear
|
|
6585
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: clear on a fresh selection payload only.
|
|
6402
6586
|
useEffect(() => {
|
|
6403
6587
|
setOverlay({ a: {}, c: {}, t: {} });
|
|
6404
|
-
}, [el.id]);
|
|
6588
|
+
}, [el.id, el.authored, el.customStyles, el.attrs]);
|
|
6405
6589
|
const mergeOverlay = (base, ov) => {
|
|
6406
6590
|
const out = { ...(base || {}) };
|
|
6407
6591
|
for (const [k, v] of Object.entries(ov)) {
|
|
@@ -6486,10 +6670,27 @@ function CssKnobs({ el, cfg, onOptimistic, onRecordEdit, onReplaceMedia, onUndoR
|
|
|
6486
6670
|
...s,
|
|
6487
6671
|
[key]: !res.ok || !j.ok ? `err:${(j && j.error) || `HTTP ${res.status}`}` : 'saved',
|
|
6488
6672
|
}));
|
|
6673
|
+
return res.ok ? j : null;
|
|
6489
6674
|
} catch (err) {
|
|
6490
6675
|
setStatus((s) => ({ ...s, [key]: `err:${err && err.message ? err.message : String(err)}` }));
|
|
6676
|
+
return null;
|
|
6491
6677
|
}
|
|
6492
6678
|
}
|
|
6679
|
+
// Write, THEN record the undo entry from what the write actually replaced
|
|
6680
|
+
// (`previous`, read server-side under the file lock). The panel's own value
|
|
6681
|
+
// can be a teammate's edit old — it is not re-posted when a peer changes the
|
|
6682
|
+
// source — and recording it made Cmd+Z restore a value nobody had on screen.
|
|
6683
|
+
// Serialized so the stack keeps edit order; a failed write records nothing.
|
|
6684
|
+
const writeChainRef = useRef(Promise.resolve());
|
|
6685
|
+
const writeAndRecord = (url, payload, key, op, prop, fallbackBefore, after) => {
|
|
6686
|
+
writeChainRef.current = writeChainRef.current.then(async () => {
|
|
6687
|
+
const j = await post(url, payload, key);
|
|
6688
|
+
if (!j?.ok) return;
|
|
6689
|
+
const before = Object.hasOwn(j, 'previous') ? j.previous : fallbackBefore;
|
|
6690
|
+
if ((before ?? null) === (after ?? null)) return; // nothing changed to undo
|
|
6691
|
+
record(op, prop, before, after);
|
|
6692
|
+
});
|
|
6693
|
+
};
|
|
6493
6694
|
// Optimistic preview: nudge the live element so the change shows before the
|
|
6494
6695
|
// edit → HMR reload lands. `value` null = remove (reset path). No-op when the
|
|
6495
6696
|
// selection has no stable id (can't be resolved in the canvas).
|
|
@@ -6523,8 +6724,15 @@ function CssKnobs({ el, cfg, onOptimistic, onRecordEdit, onReplaceMedia, onUndoR
|
|
|
6523
6724
|
if (value === (before ?? '').trim()) return; // no-op
|
|
6524
6725
|
optimistic(property, value);
|
|
6525
6726
|
setA(property, value); // reflect in the panel immediately (no reload → no reselect)
|
|
6526
|
-
|
|
6527
|
-
|
|
6727
|
+
writeAndRecord(
|
|
6728
|
+
'/_api/edit-css',
|
|
6729
|
+
{ canvas: el.file, id: el.id, property, value },
|
|
6730
|
+
property,
|
|
6731
|
+
'css',
|
|
6732
|
+
property,
|
|
6733
|
+
before,
|
|
6734
|
+
value
|
|
6735
|
+
);
|
|
6528
6736
|
};
|
|
6529
6737
|
// A custom CSS property (Advanced) — same write, but the panel surfaces it from
|
|
6530
6738
|
// the customStyles map, so overlay THERE.
|
|
@@ -6535,8 +6743,15 @@ function CssKnobs({ el, cfg, onOptimistic, onRecordEdit, onReplaceMedia, onUndoR
|
|
|
6535
6743
|
const before = customStyles[prop] ?? null;
|
|
6536
6744
|
optimistic(prop, value);
|
|
6537
6745
|
setC(prop, value);
|
|
6538
|
-
|
|
6539
|
-
|
|
6746
|
+
writeAndRecord(
|
|
6747
|
+
'/_api/edit-css',
|
|
6748
|
+
{ canvas: el.file, id: el.id, property: prop, value },
|
|
6749
|
+
prop,
|
|
6750
|
+
'css',
|
|
6751
|
+
prop,
|
|
6752
|
+
before,
|
|
6753
|
+
value
|
|
6754
|
+
);
|
|
6540
6755
|
};
|
|
6541
6756
|
const commitAttr = (attr, raw) => {
|
|
6542
6757
|
const a = (attr || '').trim();
|
|
@@ -6544,8 +6759,15 @@ function CssKnobs({ el, cfg, onOptimistic, onRecordEdit, onReplaceMedia, onUndoR
|
|
|
6544
6759
|
if (!editable || !a || !value) return;
|
|
6545
6760
|
const before = attrs[a] ?? null;
|
|
6546
6761
|
setT(a, value);
|
|
6547
|
-
|
|
6548
|
-
|
|
6762
|
+
writeAndRecord(
|
|
6763
|
+
'/_api/edit-attr',
|
|
6764
|
+
{ canvas: el.file, id: el.id, attr: a, value },
|
|
6765
|
+
`@${a}`,
|
|
6766
|
+
'attr',
|
|
6767
|
+
a,
|
|
6768
|
+
before,
|
|
6769
|
+
value
|
|
6770
|
+
);
|
|
6549
6771
|
};
|
|
6550
6772
|
// Phase 12.3 — reset (remove the inline prop / attr → back to class/inherited).
|
|
6551
6773
|
const reset = (property) => {
|
|
@@ -6553,23 +6775,44 @@ function CssKnobs({ el, cfg, onOptimistic, onRecordEdit, onReplaceMedia, onUndoR
|
|
|
6553
6775
|
const before = authored[property] ?? null;
|
|
6554
6776
|
optimistic(property, null);
|
|
6555
6777
|
setA(property, null);
|
|
6556
|
-
|
|
6557
|
-
|
|
6778
|
+
writeAndRecord(
|
|
6779
|
+
'/_api/edit-css',
|
|
6780
|
+
{ canvas: el.file, id: el.id, property, reset: true },
|
|
6781
|
+
property,
|
|
6782
|
+
'css',
|
|
6783
|
+
property,
|
|
6784
|
+
before,
|
|
6785
|
+
null
|
|
6786
|
+
);
|
|
6558
6787
|
};
|
|
6559
6788
|
const resetCustom = (property) => {
|
|
6560
6789
|
if (!editable) return;
|
|
6561
6790
|
const before = customStyles[property] ?? null;
|
|
6562
6791
|
optimistic(property, null);
|
|
6563
6792
|
setC(property, null);
|
|
6564
|
-
|
|
6565
|
-
|
|
6793
|
+
writeAndRecord(
|
|
6794
|
+
'/_api/edit-css',
|
|
6795
|
+
{ canvas: el.file, id: el.id, property, reset: true },
|
|
6796
|
+
property,
|
|
6797
|
+
'css',
|
|
6798
|
+
property,
|
|
6799
|
+
before,
|
|
6800
|
+
null
|
|
6801
|
+
);
|
|
6566
6802
|
};
|
|
6567
6803
|
const resetAttr = (attr) => {
|
|
6568
6804
|
if (!editable) return;
|
|
6569
6805
|
const before = attrs[attr] ?? null;
|
|
6570
6806
|
setT(attr, null);
|
|
6571
|
-
|
|
6572
|
-
|
|
6807
|
+
writeAndRecord(
|
|
6808
|
+
'/_api/edit-attr',
|
|
6809
|
+
{ canvas: el.file, id: el.id, attr, reset: true },
|
|
6810
|
+
`@${attr}`,
|
|
6811
|
+
'attr',
|
|
6812
|
+
attr,
|
|
6813
|
+
before,
|
|
6814
|
+
null
|
|
6815
|
+
);
|
|
6573
6816
|
};
|
|
6574
6817
|
// Stage M1 — apply a Fixed / Hug / Fill sizing mode to one axis. The pure
|
|
6575
6818
|
// `sizingModePatch` returns the exact writes (context-aware Fill: flex main axis
|
|
@@ -9598,6 +9841,9 @@ function InspectorPanel({
|
|
|
9598
9841
|
|
|
9599
9842
|
function App() {
|
|
9600
9843
|
const [groups, setGroups] = useState([]);
|
|
9844
|
+
const [treeLoaded, setTreeLoaded] = useState(false);
|
|
9845
|
+
const addressMode = useRef('push');
|
|
9846
|
+
const previousAddressPath = useRef(null);
|
|
9601
9847
|
const [project, setProject] = useState('Design');
|
|
9602
9848
|
const [tabs, setTabs] = useState([]);
|
|
9603
9849
|
const [activePath, setActivePath] = useState(null);
|
|
@@ -9607,6 +9853,7 @@ function App() {
|
|
|
9607
9853
|
// state, or iframe registration that openTab()/openSystem() drive.
|
|
9608
9854
|
const [previewPath, setPreviewPath] = useState(null);
|
|
9609
9855
|
const onPreview = useCallback((path) => {
|
|
9856
|
+
addressMode.current = 'push';
|
|
9610
9857
|
setPreviewPath(path);
|
|
9611
9858
|
}, []);
|
|
9612
9859
|
const [selected, setSelected] = useState(null);
|
|
@@ -9713,6 +9960,30 @@ function App() {
|
|
|
9713
9960
|
// after it) renumbers. Stash the re-settle target { file, movedId, artboardId }
|
|
9714
9961
|
// so the dgn:'loaded' handler re-selects the moved element by its NEW id.
|
|
9715
9962
|
const pendingReorderRef = useRef(null);
|
|
9963
|
+
// The last layers tree the canvas posted, and its id signature. A duplicate
|
|
9964
|
+
// or insert renumbers every later positional id, so the copy's NEW id already
|
|
9965
|
+
// names another element (the next sibling) in the pre-write tree: settling
|
|
9966
|
+
// on the first tree that merely contains it selected the wrong node. The
|
|
9967
|
+
// pending entry records the signature at request time and waits for a tree
|
|
9968
|
+
// that differs from it.
|
|
9969
|
+
const lastLayersTreeRef = useRef(null);
|
|
9970
|
+
const settlePendingSelectionRef = useRef(null);
|
|
9971
|
+
settlePendingSelectionRef.current = (tree, sig, artboardId) => {
|
|
9972
|
+
const pend = pendingReorderRef.current;
|
|
9973
|
+
if (!pend || !pend.movedId) return;
|
|
9974
|
+
if (pend.staleSig != null && sig === pend.staleSig) return; // still the pre-write DOM
|
|
9975
|
+
const has = (function find(nodes) {
|
|
9976
|
+
return (nodes || []).some((n) => n.id === pend.movedId || find(n.children));
|
|
9977
|
+
})(tree);
|
|
9978
|
+
if (!has) return;
|
|
9979
|
+
pendingReorderRef.current = null;
|
|
9980
|
+
const win = activePath ? iframesRef.current.get(activePath)?.contentWindow : null;
|
|
9981
|
+
if (win) {
|
|
9982
|
+
try {
|
|
9983
|
+
win.postMessage({ dgn: 'select-by-id', id: pend.movedId, artboardId, index: 0 }, '*');
|
|
9984
|
+
} catch {}
|
|
9985
|
+
}
|
|
9986
|
+
};
|
|
9716
9987
|
// Latest `reorderLayer` (defined far below), read from the stale-closure
|
|
9717
9988
|
// onMessage handler when the in-canvas grip posts dgn:'reorder-request'
|
|
9718
9989
|
// (same freshness idiom as selectedRef; avoids a render-time TDZ on the
|
|
@@ -9763,10 +10034,52 @@ function App() {
|
|
|
9763
10034
|
// Phase 9 Task 8 — hub-down offline mode banner. Driven by the 'sync:status'
|
|
9764
10035
|
// WS message the linked-mode sync runtime emits. null in solo mode.
|
|
9765
10036
|
const [syncStatus, setSyncStatus] = useState(null);
|
|
10037
|
+
// feature-large-project-seed — the NATIVE half of seed progress.
|
|
10038
|
+
//
|
|
10039
|
+
// A seed against a large project is minutes to hours of work, and the target
|
|
10040
|
+
// user (DDR-177) never opens a terminal. The Sync panel is the detail view;
|
|
10041
|
+
// this is what reaches them when the panel is not on screen: the window
|
|
10042
|
+
// title while it runs, and ONE notification per phase transition.
|
|
10043
|
+
//
|
|
10044
|
+
// Per TRANSITION, never per file. A 2 961-file seed must produce two
|
|
10045
|
+
// notifications, not 2 961 — `send_notification` throttles on the Rust side
|
|
10046
|
+
// too, but a client that leans on that is a client that would spam a plain
|
|
10047
|
+
// browser tab through the Web fallback.
|
|
10048
|
+
const seedPhaseRef = useRef(null);
|
|
10049
|
+
useEffect(() => {
|
|
10050
|
+
const p = syncStatus?.files?.progress;
|
|
10051
|
+
if (!p) return;
|
|
10052
|
+
const prev = seedPhaseRef.current;
|
|
10053
|
+
seedPhaseRef.current = p.phase;
|
|
10054
|
+
if (prev === null || prev === p.phase) return;
|
|
10055
|
+
if (p.phase === 'converged' && (prev === 'seeding' || prev === 'paused')) {
|
|
10056
|
+
notifyDesktop('Project synced', `${p.delivered} files are up to date.`);
|
|
10057
|
+
} else if (p.phase === 'blocked') {
|
|
10058
|
+
const total = (p.blocked ?? []).reduce((n, b) => n + b.count, 0);
|
|
10059
|
+
notifyDesktop(
|
|
10060
|
+
'Sync needs a decision',
|
|
10061
|
+
`${total} file${total === 1 ? '' : 's'} could not be sent. Open the Sync panel for the reason.`
|
|
10062
|
+
);
|
|
10063
|
+
}
|
|
10064
|
+
}, [syncStatus?.files?.progress]);
|
|
10065
|
+
// The title carries the count while a seed runs, and is restored afterwards —
|
|
10066
|
+
// it is the one surface visible without opening anything.
|
|
10067
|
+
useEffect(() => {
|
|
10068
|
+
if (typeof document === 'undefined') return undefined;
|
|
10069
|
+
const p = syncStatus?.files?.progress;
|
|
10070
|
+
const base = document.title.replace(/\s+—\s+syncing\s+\d+\/\d+$/, '');
|
|
10071
|
+
if (p && (p.phase === 'seeding' || p.phase === 'paused') && p.tracked > 0) {
|
|
10072
|
+
document.title = `${base} — syncing ${p.delivered}/${p.tracked}`;
|
|
10073
|
+
} else {
|
|
10074
|
+
document.title = base;
|
|
10075
|
+
}
|
|
10076
|
+
return undefined;
|
|
10077
|
+
}, [syncStatus?.files?.progress]);
|
|
9766
10078
|
// DDR-218 — the folder's cloud link ({url, credentialed} | null), lifted from
|
|
9767
10079
|
// CloudBar (status resolve / attach / detach). Gates the GitPanel's
|
|
9768
10080
|
// cloud-managed posture; linked-but-uncredentialed keeps the full panel.
|
|
9769
10081
|
const [cloudLinkedHub, setCloudLinkedHub] = useState(null);
|
|
10082
|
+
const [localProjectName, setLocalProjectName] = useState(null);
|
|
9770
10083
|
// Phase 27 (E2) — in-UI git layer. `gitStatus` is the live dirty-state the
|
|
9771
10084
|
// server broadcasts on `git-status`; `changesOpen` toggles the Changes panel;
|
|
9772
10085
|
// `diffTarget` opens the before/after DiffView ({ file, conflict }).
|
|
@@ -9790,6 +10103,24 @@ function App() {
|
|
|
9790
10103
|
// Canvas-compile skeleton (single-canvas model → one path at a time).
|
|
9791
10104
|
const [loadingPath, setLoadingPath] = useState(null);
|
|
9792
10105
|
const loadFallbackTimer = useRef(null);
|
|
10106
|
+
// issue #115 — a canvas that never reported `loaded`. `{ path, kind }`, where
|
|
10107
|
+
// kind is 'server' (the canvas origin did not answer /_health — a dead or
|
|
10108
|
+
// respawned sidecar) or 'compile' (the origin is up, the canvas itself never
|
|
10109
|
+
// mounted). Rendered instead of the blank pane the cap used to leave behind.
|
|
10110
|
+
const [canvasError, setCanvasError] = useState(null);
|
|
10111
|
+
// Bumped by the error panel's Retry. Folded into the iframe `key` so a retry
|
|
10112
|
+
// REMOUNTS the frame — re-setting `src` to the identical URL would not
|
|
10113
|
+
// re-navigate, and after a respawn the URL is identical in every case except
|
|
10114
|
+
// the port that `loadServerConfig()` is about to correct.
|
|
10115
|
+
const [canvasReloadNonce, setCanvasReloadNonce] = useState(0);
|
|
10116
|
+
// The canvas whose SHELL DOCUMENT has actually loaded, per its `dgn:'loaded'`
|
|
10117
|
+
// post. Distinct from `!loadingPath`, which the 2.5 s onLoad fallback and the
|
|
10118
|
+
// 15 s cap also clear — this one only ever means "the shell ran". That makes
|
|
10119
|
+
// it the honest signal for `data-canvas-state` below, and the one the #115 E2E
|
|
10120
|
+
// scenario asserts: the shell's inline script posts it whether or not the
|
|
10121
|
+
// canvas TSX went on to build, so it isolates "the origin was reachable" from
|
|
10122
|
+
// "the canvas compiled" — exactly the axis #115 is about.
|
|
10123
|
+
const [loadedPath, setLoadedPath] = useState(null);
|
|
9793
10124
|
// Resizable side panels (DS components-resize-panels) + the active drag side.
|
|
9794
10125
|
const sbSize = usePanelSize('maude-sb-w', { min: 200, max: 420, def: 252 });
|
|
9795
10126
|
const rpSize = usePanelSize('maude-rp-w', { min: 260, max: 480, def: 304 });
|
|
@@ -9825,11 +10156,6 @@ function App() {
|
|
|
9825
10156
|
setLoadingPath((p) => (p === path ? null : p));
|
|
9826
10157
|
}, 2500);
|
|
9827
10158
|
}, []);
|
|
9828
|
-
useEffect(() => {
|
|
9829
|
-
if (!loadingPath) return;
|
|
9830
|
-
const cap = setTimeout(() => setLoadingPath(null), 15000);
|
|
9831
|
-
return () => clearTimeout(cap);
|
|
9832
|
-
}, [loadingPath]);
|
|
9833
10159
|
// Loaded at boot from /_config and re-fetched on the server's
|
|
9834
10160
|
// `config-updated` push (config.json hot-reload — /design:setup-ds rewrites
|
|
9835
10161
|
// it mid-session) — informs canvasUrl() so TSX iframes can pass the right
|
|
@@ -9846,6 +10172,44 @@ function App() {
|
|
|
9846
10172
|
// for one frame, each fired the request its shell exists to refuse, and then
|
|
9847
10173
|
// unmounted. Nothing was broken and everything looked broken.
|
|
9848
10174
|
const [cfg, setCfg] = useState({ designRel: '.design', cloud: undefined });
|
|
10175
|
+
// THE CAP MUST NOT CLEAR TO WHITE — issue #115. This used to be a bare
|
|
10176
|
+
// `setLoadingPath(null)`, which is the most useless thing it could do: when
|
|
10177
|
+
// the canvas origin is unreachable the shell HTML never loads, so the shell's
|
|
10178
|
+
// own `#canvas-mount-error` surface (templates/_shell.html) does not exist to
|
|
10179
|
+
// report anything, and dropping the skeleton left a blank pane with no error,
|
|
10180
|
+
// no reason and no way back inside the app. Probe the canvas origin to tell
|
|
10181
|
+
// the two failures apart — a dead server (the #115 stale-origin case, and any
|
|
10182
|
+
// sidecar crash) versus a canvas that genuinely never finished compiling —
|
|
10183
|
+
// and hand the user the recovery in both cases.
|
|
10184
|
+
//
|
|
10185
|
+
// DECLARED AFTER `cfg`, ON PURPOSE — the same rule the block below states for
|
|
10186
|
+
// the git flags, and this effect is what taught us it is not decorative. Its
|
|
10187
|
+
// dependency array reads `cfg?.canvasOrigin`, and A DEPENDENCY ARRAY IS
|
|
10188
|
+
// EVALUATED DURING RENDER: sitting above the `useState` above, it hit `cfg` in
|
|
10189
|
+
// its temporal dead zone, so `App` threw `Cannot access 'cfg' before
|
|
10190
|
+
// initialization` on its very first render and the WHOLE STUDIO mounted
|
|
10191
|
+
// nothing — a white page for every user, not only the desktop shell the fix
|
|
10192
|
+
// was written for. It reached the branch because the PR carried a merge
|
|
10193
|
+
// conflict, which stops GitHub from building a merge ref, which means the
|
|
10194
|
+
// client-boot gate never ran on it (issue #112 close-out).
|
|
10195
|
+
useEffect(() => {
|
|
10196
|
+
if (!loadingPath) return;
|
|
10197
|
+
const path = loadingPath;
|
|
10198
|
+
const cap = setTimeout(async () => {
|
|
10199
|
+
let kind = 'compile';
|
|
10200
|
+
try {
|
|
10201
|
+
const r = await fetch(`${cfg?.canvasOrigin || ''}/_health`, {
|
|
10202
|
+
cache: 'no-store',
|
|
10203
|
+
});
|
|
10204
|
+
if (!r.ok) kind = 'server';
|
|
10205
|
+
} catch {
|
|
10206
|
+
kind = 'server';
|
|
10207
|
+
}
|
|
10208
|
+
setCanvasError({ path, kind });
|
|
10209
|
+
setLoadingPath((p) => (p === path ? null : p));
|
|
10210
|
+
}, 15000);
|
|
10211
|
+
return () => clearTimeout(cap);
|
|
10212
|
+
}, [loadingPath, cfg?.canvasOrigin]);
|
|
9849
10213
|
// WHO IS SAVING THIS PROJECT — one expression, read by every surface that
|
|
9850
10214
|
// would otherwise offer to save it, poll for it, or badge it (DDR-218, widened
|
|
9851
10215
|
// by feature-cloud-managed-git-posture).
|
|
@@ -10039,6 +10403,7 @@ function App() {
|
|
|
10039
10403
|
const [paletteOpen, setPaletteOpen] = useState(false);
|
|
10040
10404
|
// T5/T6 (Plan C) — shell-level export/handoff dialog + inspector panel state.
|
|
10041
10405
|
// The palette (T4) drives them; the dialog (T5) + panel (T6) consume them.
|
|
10406
|
+
const [shareDialog, setShareDialog] = useState(null);
|
|
10042
10407
|
const [exportDialog, setExportDialog] = useState(null); // null | { mode: 'export'|'handoff', scope? }
|
|
10043
10408
|
// feature-ai-media-generation (DDR-16x) — BYOK provider-key Settings modal +
|
|
10044
10409
|
// the AI generate action.
|
|
@@ -11381,37 +11746,47 @@ function App() {
|
|
|
11381
11746
|
}, []);
|
|
11382
11747
|
|
|
11383
11748
|
// ----- Tree -----
|
|
11384
|
-
const
|
|
11385
|
-
|
|
11386
|
-
const r = await fetch('/_index-data');
|
|
11387
|
-
const data = await r.json();
|
|
11388
|
-
setProject(data.project || 'Design');
|
|
11389
|
-
const built = data.groups.map((g) => ({
|
|
11390
|
-
...g,
|
|
11391
|
-
tree: buildTree(g.paths, g.stripPrefix, g.dirs),
|
|
11392
|
-
}));
|
|
11393
|
-
setGroups(built);
|
|
11394
|
-
// DDR-093 — fold the server-resolved per-canvas DS map into cfg so
|
|
11395
|
-
// canvasUrl() injects each UI canvas's OWN design-system tokens instead of
|
|
11396
|
-
// always designSystems[0]. Functional merge to coexist with the /_config
|
|
11397
|
-
// fetch (either may land first). `?? {}` keeps older servers (no map) on
|
|
11398
|
-
// the ds0 fallback. Re-runs on every tree reload, so adding/retargeting a
|
|
11399
|
-
// canvas refreshes the map.
|
|
11400
|
-
setCfg((prev) => ({
|
|
11401
|
-
...prev,
|
|
11402
|
-
canvasDesignSystems: data.canvasDesignSystems ?? {},
|
|
11403
|
-
// DDR-174 (T15) — per-canvas notable `.meta.json` `kind` values (today:
|
|
11404
|
-
// only `reconstructed-experimental`), folded in the same way + for the
|
|
11405
|
-
// same reason as canvasDesignSystems above.
|
|
11406
|
-
canvasKinds: data.canvasKinds ?? {},
|
|
11407
|
-
}));
|
|
11408
|
-
} catch (e) {
|
|
11409
|
-
console.error('failed to load tree', e);
|
|
11410
|
-
}
|
|
11411
|
-
}, []);
|
|
11412
|
-
|
|
11749
|
+
const treeLoaderRef = useRef(null);
|
|
11750
|
+
const loadTree = useCallback(() => treeLoaderRef.current?.reload() ?? Promise.resolve(), []);
|
|
11413
11751
|
useEffect(() => {
|
|
11752
|
+
const loader = createIndexLoader({
|
|
11753
|
+
read: async (signal) => {
|
|
11754
|
+
const r = await fetch('/_index-data', { signal, cache: 'no-store' });
|
|
11755
|
+
if (!r.ok) throw Object.assign(new Error(`Project index request failed: ${r.status}`), { status: r.status });
|
|
11756
|
+
const data = await r.json();
|
|
11757
|
+
const built = data.groups.map((g) => ({
|
|
11758
|
+
...g,
|
|
11759
|
+
tree: buildTree(g.paths, g.stripPrefix, g.dirs),
|
|
11760
|
+
}));
|
|
11761
|
+
return { data, built };
|
|
11762
|
+
},
|
|
11763
|
+
apply: ({ data, built }) => {
|
|
11764
|
+
setProject(data.project || 'Design');
|
|
11765
|
+
setGroups(built);
|
|
11766
|
+
setTreeLoaded(true);
|
|
11767
|
+
// DDR-093 — fold the server-resolved per-canvas DS map into cfg so
|
|
11768
|
+
// canvasUrl() injects each UI canvas's OWN design-system tokens instead of
|
|
11769
|
+
// always designSystems[0]. Functional merge to coexist with the /_config
|
|
11770
|
+
// fetch (either may land first). `?? {}` keeps older servers (no map) on
|
|
11771
|
+
// the ds0 fallback. Re-runs on every tree reload, so adding/retargeting a
|
|
11772
|
+
// canvas refreshes the map.
|
|
11773
|
+
setCfg((prev) => ({
|
|
11774
|
+
...prev,
|
|
11775
|
+
canvasDesignSystems: data.canvasDesignSystems ?? {},
|
|
11776
|
+
// DDR-174 (T15) — per-canvas notable `.meta.json` `kind` values (today:
|
|
11777
|
+
// only `reconstructed-experimental`), folded in the same way + for the
|
|
11778
|
+
// same reason as canvasDesignSystems above.
|
|
11779
|
+
canvasKinds: data.canvasKinds ?? {},
|
|
11780
|
+
}));
|
|
11781
|
+
},
|
|
11782
|
+
onError: (error) => console.error('failed to load tree', error),
|
|
11783
|
+
});
|
|
11784
|
+
treeLoaderRef.current = loader;
|
|
11414
11785
|
loadTree();
|
|
11786
|
+
return () => {
|
|
11787
|
+
treeLoaderRef.current = null;
|
|
11788
|
+
loader.dispose();
|
|
11789
|
+
};
|
|
11415
11790
|
}, [loadTree]);
|
|
11416
11791
|
|
|
11417
11792
|
// ----- System data (lazy) -----
|
|
@@ -11460,6 +11835,7 @@ function App() {
|
|
|
11460
11835
|
}, [loadAllComments]);
|
|
11461
11836
|
|
|
11462
11837
|
// ----- WebSocket -----
|
|
11838
|
+
const canvasListChangeRef = useRef(() => {});
|
|
11463
11839
|
useEffect(() => {
|
|
11464
11840
|
// KEEPALIVE. The inspector feed only pushes on events (a comment, a
|
|
11465
11841
|
// selection, sync:status), so an idle designer's socket exchanges nothing
|
|
@@ -11471,12 +11847,30 @@ function App() {
|
|
|
11471
11847
|
// 25 s keeps the socket active (Bun resets the idle timer on any frame,
|
|
11472
11848
|
// inbound OR outbound); the server ignores an unknown message type.
|
|
11473
11849
|
let pingTimer = null;
|
|
11850
|
+
// A RECONNECT MEANS THE PEER MAY BE A DIFFERENT PROCESS — issue #115.
|
|
11851
|
+
// The desktop supervisor respawns the sidecar when it dies (DDR-235's Bun
|
|
11852
|
+
// fault is a recurring one), and everything this page cached from `/_config`
|
|
11853
|
+
// was published by the process that just went away. `canvasOrigin` is the
|
|
11854
|
+
// one that bites: it is an OS-assigned ephemeral port (server.ts's
|
|
11855
|
+
// `startCanvasServer(0)`), so it is DIFFERENT in every process, while the
|
|
11856
|
+
// main origin walks a deterministic ladder from 4399 and usually reclaims
|
|
11857
|
+
// its old port. The result was a half-live page — socket back up, status bar
|
|
11858
|
+
// `live`, tree and panels fine — whose every canvas iframe navigated to a
|
|
11859
|
+
// dead port. Since a canvas switch mounts a FRESH iframe (single-canvas
|
|
11860
|
+
// model, keyed by path), the already-open canvas kept rendering and only
|
|
11861
|
+
// switching went white, silently and permanently.
|
|
11862
|
+
//
|
|
11863
|
+
// So: re-derive the config from whoever is answering now. Only on a
|
|
11864
|
+
// RE-connect — the boot effect above already did the first fetch.
|
|
11865
|
+
let everConnected = false;
|
|
11474
11866
|
function connect() {
|
|
11475
11867
|
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
11476
11868
|
const ws = new WebSocket(proto + '//' + location.host + '/_ws');
|
|
11477
11869
|
wsRef.current = ws;
|
|
11478
11870
|
ws.addEventListener('open', () => {
|
|
11479
11871
|
setWsConnected(true);
|
|
11872
|
+
if (everConnected) loadServerConfig();
|
|
11873
|
+
everConnected = true;
|
|
11480
11874
|
clearInterval(pingTimer);
|
|
11481
11875
|
pingTimer = setInterval(() => {
|
|
11482
11876
|
if (ws.readyState === WebSocket.OPEN) ws.send('{"type":"ping"}');
|
|
@@ -11553,26 +11947,8 @@ function App() {
|
|
|
11553
11947
|
// Phase 9 Task 8 — hub connection state for the offline banner.
|
|
11554
11948
|
setSyncStatus(m.payload);
|
|
11555
11949
|
} else if (m.type === 'canvas-list-update') {
|
|
11556
|
-
// Phase 30 — a canvas was created/deleted on THIS dev-server; re-read
|
|
11557
|
-
// the branch-scoped tree so other open tabs reflect it without a
|
|
11558
|
-
// reload. Cross-machine peers get a new canvas via git "Get latest".
|
|
11559
11950
|
loadTree();
|
|
11560
|
-
|
|
11561
|
-
// moveCanvasReq already retargets the INITIATING tab locally (no
|
|
11562
|
-
// need to wait for this broadcast to round-trip); this branch is
|
|
11563
|
-
// for every OTHER open tab on the same dev-server, so a canvas
|
|
11564
|
-
// that was open there doesn't go dead pointing at a path that no
|
|
11565
|
-
// longer exists.
|
|
11566
|
-
if (m.payload?.action === 'moved' && m.payload.fromRel && m.payload.rel) {
|
|
11567
|
-
const designRel = (cfg?.designRel || cfg?.designRoot || '.design').replace(
|
|
11568
|
-
/^\/+|\/+$/g,
|
|
11569
|
-
''
|
|
11570
|
-
);
|
|
11571
|
-
const fromFile = `${designRel}/${m.payload.fromRel}`;
|
|
11572
|
-
const toFile = `${designRel}/${m.payload.rel}`;
|
|
11573
|
-
setTabs((prev) => prev.map((t) => (t.path === fromFile ? { path: toFile } : t)));
|
|
11574
|
-
setActivePath((prev) => (prev === fromFile ? toFile : prev));
|
|
11575
|
-
}
|
|
11951
|
+
canvasListChangeRef.current(m.payload);
|
|
11576
11952
|
} else if (m.type === 'config-updated') {
|
|
11577
11953
|
// Server hot-reloaded .design/config.json (/design:setup-ds rewrote
|
|
11578
11954
|
// it) — refetch /_config so designSystems / tokensCssRel / groups
|
|
@@ -11775,6 +12151,39 @@ function App() {
|
|
|
11775
12151
|
}
|
|
11776
12152
|
}, []);
|
|
11777
12153
|
|
|
12154
|
+
// ACCEPTED REVISIONS — the project's own history (DDR-241, T27). When the
|
|
12155
|
+
// project saves through accepted revisions the History tab lists logical
|
|
12156
|
+
// actions (who did what, when), not Git commits; a row previews as `r<rev>`
|
|
12157
|
+
// through the same version preview. `'legacy'` means "use Git history".
|
|
12158
|
+
const [projectHistoryOn, setProjectHistoryOn] = useState(false);
|
|
12159
|
+
const loadAcceptedLog = useCallback(async (path) => {
|
|
12160
|
+
try {
|
|
12161
|
+
const qs =
|
|
12162
|
+
'/_api/project/history?limit=40' + (path ? `&path=${encodeURIComponent(path)}` : '');
|
|
12163
|
+
const r = await fetch(qs);
|
|
12164
|
+
if (!r.ok) return 'legacy';
|
|
12165
|
+
const data = await r.json();
|
|
12166
|
+
if (!data?.ok) return data?.reason === 'legacy' ? 'legacy' : null;
|
|
12167
|
+
return (data.history || []).map((a) => {
|
|
12168
|
+
const where = path ? '' : (a.canvases || []).map((c) => c.split('/').pop()).join(', ');
|
|
12169
|
+
return {
|
|
12170
|
+
sha: `r${a.revision}`,
|
|
12171
|
+
message: [a.label || a.kind, where].filter(Boolean).join(' · '),
|
|
12172
|
+
author: a.actor,
|
|
12173
|
+
date: new Date(a.committedAt).toISOString(),
|
|
12174
|
+
accepted: {
|
|
12175
|
+
revision: a.revision,
|
|
12176
|
+
actionId: a.actionId,
|
|
12177
|
+
mine: !!a.mine,
|
|
12178
|
+
undo: a.kind === 'undo' || a.kind === 'redo',
|
|
12179
|
+
},
|
|
12180
|
+
};
|
|
12181
|
+
});
|
|
12182
|
+
} catch {
|
|
12183
|
+
return null;
|
|
12184
|
+
}
|
|
12185
|
+
}, []);
|
|
12186
|
+
|
|
11778
12187
|
// Repo-relative path → M/A/D/U badge for the tree (paths match: both the tree
|
|
11779
12188
|
// and gitStatus use `.design/ui/Foo.tsx`). Keyed off gitStatus so it updates
|
|
11780
12189
|
// live with the WS broadcast.
|
|
@@ -11818,18 +12227,99 @@ function App() {
|
|
|
11818
12227
|
// (iframesRef, comments push, WS `tabs` message) doesn't need refactoring.
|
|
11819
12228
|
// ARTBOARDS slot in the menubar reads `tabs.length` and reports 0 or 1.
|
|
11820
12229
|
const openTab = useCallback((path) => {
|
|
12230
|
+
addressMode.current = 'push';
|
|
12231
|
+
setFocusedCommentId(null);
|
|
12232
|
+
setPreviewPath(null);
|
|
12233
|
+
// The same path keeps its mounted iframe. It will not emit a new loaded
|
|
12234
|
+
// event, so keep its current loading/error state until an actual retry.
|
|
12235
|
+
if (path === activePath) return;
|
|
11821
12236
|
setTabs((prev) => {
|
|
11822
12237
|
// Drop the previously-open iframe so we don't leak DOM nodes.
|
|
11823
12238
|
for (const t of prev) if (t.path !== path) iframesRef.current.delete(t.path);
|
|
11824
12239
|
return [{ path }];
|
|
11825
12240
|
});
|
|
11826
12241
|
setActivePath(path);
|
|
11827
|
-
|
|
11828
|
-
|
|
12242
|
+
setCanvasError(null);
|
|
12243
|
+
setLoadedPath(null);
|
|
11829
12244
|
// Canvas-compile skeleton — cleared by the iframe's dgn:'loaded' message,
|
|
11830
12245
|
// the onLoad fallback timer (legacy .html), or a hard 15s cap.
|
|
11831
12246
|
if (path !== SYSTEM_TAB) setLoadingPath(path);
|
|
11832
|
-
}, []);
|
|
12247
|
+
}, [activePath]);
|
|
12248
|
+
|
|
12249
|
+
// Resolve URL identities against the loaded tree, including non-canvas previews.
|
|
12250
|
+
const openLinkedFile = useCallback((rel, mode = 'push') => {
|
|
12251
|
+
const path = groups.flatMap((g) => g.paths || []).find((p) => normalizeOpenPath(p, cfg.designRel) === rel);
|
|
12252
|
+
if (path && CANVAS_EXT_RE.test(path)) openTab(path);
|
|
12253
|
+
else if (path && previewKind(basename(path))) onPreview(path);
|
|
12254
|
+
else {
|
|
12255
|
+
setTabs([]);
|
|
12256
|
+
setActivePath(null);
|
|
12257
|
+
setPreviewPath(null);
|
|
12258
|
+
notify({ title: 'Not here yet', description: `${rel} is not in this project (not synced yet?)`, kind: 'info' });
|
|
12259
|
+
}
|
|
12260
|
+
addressMode.current = mode;
|
|
12261
|
+
}, [groups, cfg.designRel, openTab, onPreview]);
|
|
12262
|
+
|
|
12263
|
+
const addressBooted = useRef(false);
|
|
12264
|
+
useEffect(() => {
|
|
12265
|
+
if (!treeLoaded || cfg.cloud === undefined || addressBooted.current) return;
|
|
12266
|
+
addressBooted.current = true;
|
|
12267
|
+
const rel = readOpenParam(location, cfg.designRel);
|
|
12268
|
+
if (rel) openLinkedFile(rel, 'none');
|
|
12269
|
+
}, [treeLoaded, cfg.cloud, cfg.designRel, openLinkedFile]);
|
|
12270
|
+
|
|
12271
|
+
useEffect(() => {
|
|
12272
|
+
if (!treeLoaded || cfg.cloud === undefined) return;
|
|
12273
|
+
const navigate = () => {
|
|
12274
|
+
const rel = readOpenParam(location, cfg.designRel);
|
|
12275
|
+
if (rel) openLinkedFile(rel, 'none');
|
|
12276
|
+
else {
|
|
12277
|
+
addressMode.current = 'none';
|
|
12278
|
+
setTabs([]);
|
|
12279
|
+
setActivePath(null);
|
|
12280
|
+
setPreviewPath(null);
|
|
12281
|
+
}
|
|
12282
|
+
};
|
|
12283
|
+
window.addEventListener('popstate', navigate);
|
|
12284
|
+
return () => window.removeEventListener('popstate', navigate);
|
|
12285
|
+
}, [treeLoaded, cfg.cloud, cfg.designRel, openLinkedFile]);
|
|
12286
|
+
|
|
12287
|
+
useEffect(() => {
|
|
12288
|
+
const visible = previewPath || (activePath === SYSTEM_TAB ? null : activePath);
|
|
12289
|
+
if (visible === previousAddressPath.current) return;
|
|
12290
|
+
previousAddressPath.current = visible;
|
|
12291
|
+
const rel = normalizeOpenPath(visible, cfg.designRel);
|
|
12292
|
+
if (addressMode.current !== 'none' && readOpenParam(location, cfg.designRel) !== rel) {
|
|
12293
|
+
history[rel ? 'pushState' : 'replaceState'](rel ? { open: rel } : null, '', withOpenParam(location, rel));
|
|
12294
|
+
}
|
|
12295
|
+
addressMode.current = 'push';
|
|
12296
|
+
}, [activePath, previewPath, cfg.designRel]);
|
|
12297
|
+
|
|
12298
|
+
const sharePath = previewPath || (activePath === SYSTEM_TAB ? null : activePath);
|
|
12299
|
+
const shareShell = cfg.cloud ? 'cloud' : isNativeApp() ? 'native' : 'local';
|
|
12300
|
+
const shareLinksFor = (path) => buildShareLinks({
|
|
12301
|
+
rel: normalizeOpenPath(path, cfg.designRel), shell: shareShell, location,
|
|
12302
|
+
linkedHubUrl: cloudLinkedHub?.url, project: cfg.cloud ? project : localProjectName,
|
|
12303
|
+
localUrl: location.origin,
|
|
12304
|
+
});
|
|
12305
|
+
const showShare = (path = sharePath) => {
|
|
12306
|
+
const rel = normalizeOpenPath(path, cfg.designRel);
|
|
12307
|
+
if (rel) setShareDialog({ path, rel, label: basename(path) });
|
|
12308
|
+
};
|
|
12309
|
+
|
|
12310
|
+
// Retry from the #115 error panel: re-read /_config FIRST (the stale
|
|
12311
|
+
// `canvasOrigin` is the likeliest reason we are here at all), then remount the
|
|
12312
|
+
// iframe via the nonce so it navigates against the corrected origin.
|
|
12313
|
+
const retryCanvasLoad = useCallback(
|
|
12314
|
+
(path) => {
|
|
12315
|
+
setCanvasError(null);
|
|
12316
|
+
setLoadedPath(null);
|
|
12317
|
+
loadServerConfig();
|
|
12318
|
+
setCanvasReloadNonce((n) => n + 1);
|
|
12319
|
+
if (path && path !== SYSTEM_TAB) setLoadingPath(path);
|
|
12320
|
+
},
|
|
12321
|
+
[loadServerConfig]
|
|
12322
|
+
);
|
|
11833
12323
|
|
|
11834
12324
|
/**
|
|
11835
12325
|
* First open lands on a rendered canvas — Cloud Phase 27 C3.
|
|
@@ -11847,6 +12337,7 @@ function App() {
|
|
|
11847
12337
|
useEffect(() => {
|
|
11848
12338
|
if (autoOpened.current) return;
|
|
11849
12339
|
if (!cfg.cloud) return; // desktop / unknown-yet
|
|
12340
|
+
if (new URLSearchParams(location.search).has('open')) { autoOpened.current = true; return; }
|
|
11850
12341
|
if (tabs.length > 0) return;
|
|
11851
12342
|
if (!groups.length) return; // tree not loaded
|
|
11852
12343
|
// The first canvas that is somebody's WORK — a design-system specimen is a
|
|
@@ -11907,6 +12398,23 @@ function App() {
|
|
|
11907
12398
|
[activePath]
|
|
11908
12399
|
);
|
|
11909
12400
|
|
|
12401
|
+
// The socket stays connected while this ref follows the current config and
|
|
12402
|
+
// active tab. Structural events apply to local and remote users alike.
|
|
12403
|
+
canvasListChangeRef.current = (change) => {
|
|
12404
|
+
if (!change?.rel) return;
|
|
12405
|
+
const designRel = (cfg?.designRel || cfg?.designRoot || '.design').replace(/^\/+|\/+$/g, '');
|
|
12406
|
+
const file = `${designRel}/${change.rel}`;
|
|
12407
|
+
if (change.action === 'removed') {
|
|
12408
|
+
closeTab(file);
|
|
12409
|
+
} else if (change.action === 'moved' && change.fromRel) {
|
|
12410
|
+
const fromFile = `${designRel}/${change.fromRel}`;
|
|
12411
|
+
setTabs((prev) => prev.map((t) => t.path === fromFile ? { ...t, path: file } : t));
|
|
12412
|
+
setActivePath((prev) => prev === fromFile ? file : prev);
|
|
12413
|
+
setLoadingPath((prev) => prev === fromFile ? null : prev);
|
|
12414
|
+
iframesRef.current.delete(fromFile);
|
|
12415
|
+
}
|
|
12416
|
+
};
|
|
12417
|
+
|
|
11910
12418
|
const reloadActive = useCallback(() => {
|
|
11911
12419
|
if (!activePath || activePath === SYSTEM_TAB) {
|
|
11912
12420
|
if (activePath === SYSTEM_TAB) loadSystemData();
|
|
@@ -12291,6 +12799,100 @@ function App() {
|
|
|
12291
12799
|
[loadTree]
|
|
12292
12800
|
);
|
|
12293
12801
|
|
|
12802
|
+
// Plan T17 — rename a folder: a move to a new name in the same parent (one
|
|
12803
|
+
// dir.move action in accepted mode, carrying every canvas inside).
|
|
12804
|
+
const renameFolderReq = useCallback(
|
|
12805
|
+
async (dirPath, name) => {
|
|
12806
|
+
const parent = dirPath.split('/').slice(0, -1).join('/');
|
|
12807
|
+
try {
|
|
12808
|
+
const r = await fetch('/_api/fs-move', {
|
|
12809
|
+
method: 'POST',
|
|
12810
|
+
headers: { 'Content-Type': 'application/json' },
|
|
12811
|
+
body: JSON.stringify({ file: dirPath, toDir: parent, toName: name }),
|
|
12812
|
+
});
|
|
12813
|
+
const j = await r.json().catch(() => ({}));
|
|
12814
|
+
if (!r.ok || !j.ok) {
|
|
12815
|
+
shellToast(`Could not rename folder: ${j.error || `error ${r.status}`}`);
|
|
12816
|
+
return;
|
|
12817
|
+
}
|
|
12818
|
+
await loadTree();
|
|
12819
|
+
} catch (e) {
|
|
12820
|
+
shellToast(`Rename failed: ${e instanceof Error ? e.message : 'network error'}`);
|
|
12821
|
+
}
|
|
12822
|
+
},
|
|
12823
|
+
[loadTree]
|
|
12824
|
+
);
|
|
12825
|
+
|
|
12826
|
+
// Plan T25/L04 — rename a canvas in place (its sidecars follow; an open tab
|
|
12827
|
+
// follows the new path) and duplicate one beside itself.
|
|
12828
|
+
const renameCanvasReq = useCallback(
|
|
12829
|
+
async (filePath, name) => {
|
|
12830
|
+
try {
|
|
12831
|
+
const r = await fetch('/_api/fs-move', {
|
|
12832
|
+
method: 'POST',
|
|
12833
|
+
headers: { 'Content-Type': 'application/json' },
|
|
12834
|
+
body: JSON.stringify({ file: filePath, toName: name }),
|
|
12835
|
+
});
|
|
12836
|
+
const j = await r.json().catch(() => ({}));
|
|
12837
|
+
if (!r.ok || !j.ok) {
|
|
12838
|
+
shellToast(`Could not rename: ${j.error || `error ${r.status}`}`);
|
|
12839
|
+
return;
|
|
12840
|
+
}
|
|
12841
|
+
const designRel = (cfg?.designRel || cfg?.designRoot || '.design').replace(
|
|
12842
|
+
/^\/+|\/+$/g,
|
|
12843
|
+
''
|
|
12844
|
+
);
|
|
12845
|
+
const fromFile = `${designRel}/${j.fromRel}`;
|
|
12846
|
+
const toFile = `${designRel}/${j.toRel}`;
|
|
12847
|
+
await loadTree();
|
|
12848
|
+
setTabs((prev) => prev.map((t) => (t.path === fromFile ? { path: toFile } : t)));
|
|
12849
|
+
setActivePath((prev) => (prev === fromFile ? toFile : prev));
|
|
12850
|
+
} catch (e) {
|
|
12851
|
+
shellToast(`Rename failed: ${e instanceof Error ? e.message : 'network error'}`);
|
|
12852
|
+
}
|
|
12853
|
+
},
|
|
12854
|
+
[loadTree, cfg]
|
|
12855
|
+
);
|
|
12856
|
+
const duplicateCanvasReq = useCallback(
|
|
12857
|
+
async (filePath) => {
|
|
12858
|
+
try {
|
|
12859
|
+
const r = await fetch('/_api/canvas', {
|
|
12860
|
+
method: 'POST',
|
|
12861
|
+
headers: { 'Content-Type': 'application/json' },
|
|
12862
|
+
body: JSON.stringify({ duplicateOf: filePath }),
|
|
12863
|
+
});
|
|
12864
|
+
const j = await r.json().catch(() => ({}));
|
|
12865
|
+
if (!r.ok || !j.ok) {
|
|
12866
|
+
shellToast(`Could not duplicate: ${j.error || `error ${r.status}`}`);
|
|
12867
|
+
return;
|
|
12868
|
+
}
|
|
12869
|
+
await loadTree();
|
|
12870
|
+
shellToast(`Duplicated as ${j.rel.split('/').pop().replace(/\.tsx$/i, '')}`, true);
|
|
12871
|
+
} catch (e) {
|
|
12872
|
+
shellToast(`Duplicate failed: ${e instanceof Error ? e.message : 'network error'}`);
|
|
12873
|
+
}
|
|
12874
|
+
},
|
|
12875
|
+
[loadTree]
|
|
12876
|
+
);
|
|
12877
|
+
|
|
12878
|
+
const deleteFileReq = useCallback(
|
|
12879
|
+
async (filePath, name) => {
|
|
12880
|
+
if (!window.confirm(`Move “${name}” to trash?\n\nYou can restore it from .design/_trash/.`)) return;
|
|
12881
|
+
try {
|
|
12882
|
+
const r = await fetch(`/_api/canvas?file=${encodeURIComponent(filePath)}`, { method: 'DELETE' });
|
|
12883
|
+
const j = await r.json().catch(() => ({}));
|
|
12884
|
+
if (!r.ok || !j.ok) {
|
|
12885
|
+
shellToast(`Could not delete: ${j.error || `error ${r.status}`}`);
|
|
12886
|
+
return;
|
|
12887
|
+
}
|
|
12888
|
+
await loadTree();
|
|
12889
|
+
} catch (e) {
|
|
12890
|
+
shellToast(`Delete failed: ${e instanceof Error ? e.message : 'network error'}`);
|
|
12891
|
+
}
|
|
12892
|
+
},
|
|
12893
|
+
[loadTree]
|
|
12894
|
+
);
|
|
12895
|
+
|
|
12294
12896
|
const clearSelected = useCallback(() => {
|
|
12295
12897
|
wsSend({ type: 'clear-select' });
|
|
12296
12898
|
setSelected(null);
|
|
@@ -12340,6 +12942,13 @@ function App() {
|
|
|
12340
12942
|
if (e.origin !== expectedOrigin) return;
|
|
12341
12943
|
const m = e.data;
|
|
12342
12944
|
if (!m || typeof m !== 'object' || !m.dgn) return;
|
|
12945
|
+
if (m.dgn === 'canvas-notice') {
|
|
12946
|
+
const activeWin = activePath && activePath !== SYSTEM_TAB
|
|
12947
|
+
? iframesRef.current.get(activePath)?.contentWindow : null;
|
|
12948
|
+
const notice = acceptCanvasNotice(e, expectedOrigin, activeWin);
|
|
12949
|
+
if (notice) notifyCanvasText(notice.title, notice.kind);
|
|
12950
|
+
return;
|
|
12951
|
+
}
|
|
12343
12952
|
if (m.dgn === 'tool-cursor') {
|
|
12344
12953
|
// Phase 24 — show the active canvas tool's cursor across the WHOLE app
|
|
12345
12954
|
// shell (sidebar, top bar, everything) so the custom cursor is visible
|
|
@@ -12481,55 +13090,48 @@ function App() {
|
|
|
12481
13090
|
// Inline-edit undo/redo (DDR-103/104 follow-up). The canvas iframe's
|
|
12482
13091
|
// `edit-source` command can't call the main-origin-only `/_api/edit-*`
|
|
12483
13092
|
// routes (DDR-054), so it asks us to re-apply the before/after value.
|
|
12484
|
-
// `value` null = reset (remove the inline prop / attr).
|
|
12485
|
-
//
|
|
12486
|
-
|
|
13093
|
+
// `value` null = reset (remove the inline prop / attr). The css/attr
|
|
13094
|
+
// request carries the command's expected current value, so an undo can
|
|
13095
|
+
// never overwrite a teammate's newer value (audit 2026-09-13 P1 #5);
|
|
13096
|
+
// the outcome goes back to the canvas so a refused undo/redo does not
|
|
13097
|
+
// advance its stack.
|
|
13098
|
+
const req = applyEditRequest(m);
|
|
13099
|
+
if (!req) return;
|
|
13100
|
+
const replyTo = e.source;
|
|
13101
|
+
const reply = (result) => {
|
|
13102
|
+
if (typeof m.requestId !== 'string') return;
|
|
13103
|
+
try {
|
|
13104
|
+
replyTo?.postMessage({ dgn: 'apply-edit-result', requestId: m.requestId, ...result }, '*');
|
|
13105
|
+
} catch {}
|
|
13106
|
+
};
|
|
12487
13107
|
const value = typeof m.value === 'string' ? m.value : null;
|
|
12488
|
-
let url;
|
|
12489
|
-
let body;
|
|
12490
|
-
if (op === 'css') {
|
|
12491
|
-
url = '/_api/edit-css';
|
|
12492
|
-
body =
|
|
12493
|
-
value == null
|
|
12494
|
-
? { canvas: m.canvas, id: m.id, property: m.key, reset: true }
|
|
12495
|
-
: { canvas: m.canvas, id: m.id, property: m.key, value };
|
|
12496
|
-
applyOptimisticStyle({ id: m.id, prop: m.key, value });
|
|
12497
|
-
} else if (op === 'attr') {
|
|
12498
|
-
url = '/_api/edit-attr';
|
|
12499
|
-
body =
|
|
12500
|
-
value == null
|
|
12501
|
-
? { canvas: m.canvas, id: m.id, attr: m.key, reset: true }
|
|
12502
|
-
: { canvas: m.canvas, id: m.id, attr: m.key, value };
|
|
12503
|
-
} else {
|
|
12504
|
-
url = '/_api/edit-text';
|
|
12505
|
-
body = { canvas: m.canvas, id: m.id, text: value ?? '' };
|
|
12506
|
-
// Undo/redo of a `{variable}` text edit needs to re-target the source
|
|
12507
|
-
// string: the occurrence + the value currently on disk (`from` — the
|
|
12508
|
-
// side we're replacing FROM). Harmless for literal text.
|
|
12509
|
-
if (typeof m.occurrence === 'number') body.occurrence = m.occurrence;
|
|
12510
|
-
if (typeof m.from === 'string') body.before = m.from;
|
|
12511
|
-
}
|
|
12512
13108
|
editApplyChainRef.current = editApplyChainRef.current
|
|
12513
13109
|
.catch(() => {})
|
|
12514
13110
|
.then(() =>
|
|
12515
|
-
fetch(url, {
|
|
13111
|
+
fetch(req.url, {
|
|
12516
13112
|
method: 'POST',
|
|
12517
13113
|
headers: { 'content-type': 'application/json' },
|
|
12518
|
-
body: JSON.stringify(body),
|
|
13114
|
+
body: JSON.stringify(req.body),
|
|
12519
13115
|
})
|
|
12520
13116
|
.then((r) => r.json().catch(() => ({})))
|
|
12521
13117
|
.then((j) => {
|
|
12522
|
-
|
|
12523
|
-
|
|
12524
|
-
|
|
12525
|
-
|
|
12526
|
-
|
|
13118
|
+
if (j.ok) {
|
|
13119
|
+
// Repaint only once the source accepted it: a refused undo
|
|
13120
|
+
// must not show a value the file does not hold.
|
|
13121
|
+
if (req.op === 'css') applyOptimisticStyle({ id: m.id, prop: m.key, value });
|
|
13122
|
+
reply({ ok: true });
|
|
13123
|
+
return;
|
|
13124
|
+
}
|
|
13125
|
+
console.warn('[apply-edit]', req.op, j.error || 'failed');
|
|
13126
|
+
reply({ ok: false, error: j.error || 'failed', conflict: j.conflict === true });
|
|
12527
13127
|
})
|
|
12528
|
-
.catch(() => {})
|
|
13128
|
+
.catch(() => reply({ ok: false, error: 'network error' }))
|
|
12529
13129
|
);
|
|
12530
13130
|
} else if (m.dgn === 'layers-tree') {
|
|
12531
13131
|
// Phase 12 Task 4 — browsable layers tree for the active artboard.
|
|
12532
13132
|
setLayersTree({ artboardId: m.artboardId, nodes: Array.isArray(m.tree) ? m.tree : [] });
|
|
13133
|
+
const sig = layersTreeSig(m.tree);
|
|
13134
|
+
lastLayersTreeRef.current = { tree: m.tree, sig, artboardId: m.artboardId ?? null };
|
|
12533
13135
|
// fresh tree (correct ids) landed — drags OK again
|
|
12534
13136
|
layersBusyRef.current = false;
|
|
12535
13137
|
if (layersBusyTimerRef.current) {
|
|
@@ -12545,24 +13147,7 @@ function App() {
|
|
|
12545
13147
|
// actually CONTAINS movedId — an in-canvas drag posts a PREVIEW tree
|
|
12546
13148
|
// first (old ids, pre-write); consuming then would re-select nothing and
|
|
12547
13149
|
// burn the pending ref before the real post-HMR tree lands.
|
|
12548
|
-
|
|
12549
|
-
if (pend && pend.movedId) {
|
|
12550
|
-
const has = (function find(nodes) {
|
|
12551
|
-
return (nodes || []).some((n) => n.id === pend.movedId || find(n.children));
|
|
12552
|
-
})(m.tree);
|
|
12553
|
-
if (has) {
|
|
12554
|
-
pendingReorderRef.current = null;
|
|
12555
|
-
const win = activePath ? iframesRef.current.get(activePath)?.contentWindow : null;
|
|
12556
|
-
if (win) {
|
|
12557
|
-
try {
|
|
12558
|
-
win.postMessage(
|
|
12559
|
-
{ dgn: 'select-by-id', id: pend.movedId, artboardId: m.artboardId ?? null, index: 0 },
|
|
12560
|
-
'*'
|
|
12561
|
-
);
|
|
12562
|
-
} catch {}
|
|
12563
|
-
}
|
|
12564
|
-
}
|
|
12565
|
-
}
|
|
13150
|
+
settlePendingSelectionRef.current?.(m.tree, sig, m.artboardId ?? null);
|
|
12566
13151
|
} else if (m.dgn === 'reorder-revert') {
|
|
12567
13152
|
// Phase 12.1 follow-up — Cmd+Z/Cmd+Shift+Z on a reorder. The canvas undo
|
|
12568
13153
|
// stack (untrusted iframe) can't reach the main-origin-only
|
|
@@ -12886,6 +13471,23 @@ function App() {
|
|
|
12886
13471
|
if (e.source === activeWin && typeof m.artboardId === 'string') {
|
|
12887
13472
|
deleteArtboardShellRef.current?.(m.artboardId);
|
|
12888
13473
|
}
|
|
13474
|
+
} else if (m.dgn === 'rename-artboard-request') {
|
|
13475
|
+
// Plan T25/L08 — double-click an artboard's name to rename it. Same
|
|
13476
|
+
// confused-deputy guard: the canvas asks, the shell writes the active
|
|
13477
|
+
// canvas only.
|
|
13478
|
+
const activeWin = activePath ? iframesRef.current.get(activePath)?.contentWindow : null;
|
|
13479
|
+
if (
|
|
13480
|
+
e.source === activeWin &&
|
|
13481
|
+
typeof m.artboardId === 'string' &&
|
|
13482
|
+
typeof m.label === 'string' &&
|
|
13483
|
+
m.label.trim()
|
|
13484
|
+
) {
|
|
13485
|
+
structuralWriteRef.current?.(
|
|
13486
|
+
'/_api/set-artboard-label',
|
|
13487
|
+
{ artboardId: m.artboardId, label: m.label.slice(0, 80) },
|
|
13488
|
+
{ label: 'rename artboard' }
|
|
13489
|
+
);
|
|
13490
|
+
}
|
|
12889
13491
|
} else if (m.dgn === 'set-artboard-kind-request') {
|
|
12890
13492
|
// feature-1-artboard-kinds-foundation, T8 — context-menu "Artboard
|
|
12891
13493
|
// kind" submenu (inside the untrusted iframe). `kind: null` clears
|
|
@@ -13132,6 +13734,10 @@ function App() {
|
|
|
13132
13734
|
// iframe finished loading — drop the compile skeleton, push current
|
|
13133
13735
|
// comments + carry over focused pin if any
|
|
13134
13736
|
setLoadingPath((p) => (p === m.file ? null : p));
|
|
13737
|
+
setLoadedPath(m.file);
|
|
13738
|
+
// …and retire any #115 error panel for this canvas: it just proved it
|
|
13739
|
+
// can load (a late load, or a successful Retry).
|
|
13740
|
+
setCanvasError((e) => (e && e.path === m.file ? null : e));
|
|
13135
13741
|
// Presentation Mode suppresses comment pins (same gate as the push
|
|
13136
13742
|
// effect above), so a canvas opened while presenting starts pin-free.
|
|
13137
13743
|
const list = presentMode ? [] : commentsByFile[m.file] || [];
|
|
@@ -13528,6 +14134,7 @@ function App() {
|
|
|
13528
14134
|
// fix as resizeElement; also covers the keyboard nudge (L1) which reuses this.
|
|
13529
14135
|
applyOptimisticStyle({ id, prop: 'left', value: `${left}px` });
|
|
13530
14136
|
applyOptimisticStyle({ id, prop: 'top', value: `${top}px` });
|
|
14137
|
+
let j1ref = null;
|
|
13531
14138
|
const writeProp = (property, value) =>
|
|
13532
14139
|
fetch('/_api/edit-css', {
|
|
13533
14140
|
method: 'POST',
|
|
@@ -13539,6 +14146,7 @@ function App() {
|
|
|
13539
14146
|
.then(() => writeProp('left', left))
|
|
13540
14147
|
.then((j1) => {
|
|
13541
14148
|
if (!j1.ok) throw new Error(j1.error || 'left write failed');
|
|
14149
|
+
j1ref = j1;
|
|
13542
14150
|
return writeProp('top', top);
|
|
13543
14151
|
})
|
|
13544
14152
|
.then((j2) => {
|
|
@@ -13550,7 +14158,7 @@ function App() {
|
|
|
13550
14158
|
canvas,
|
|
13551
14159
|
id,
|
|
13552
14160
|
key: 'left',
|
|
13553
|
-
before: `${beforeLeft}px
|
|
14161
|
+
before: replacedValue(j1ref, `${beforeLeft}px`),
|
|
13554
14162
|
after: `${left}px`,
|
|
13555
14163
|
});
|
|
13556
14164
|
recordSourceEdit({
|
|
@@ -13558,7 +14166,7 @@ function App() {
|
|
|
13558
14166
|
canvas,
|
|
13559
14167
|
id,
|
|
13560
14168
|
key: 'top',
|
|
13561
|
-
before: `${beforeTop}px
|
|
14169
|
+
before: replacedValue(j2, `${beforeTop}px`),
|
|
13562
14170
|
after: `${top}px`,
|
|
13563
14171
|
});
|
|
13564
14172
|
})
|
|
@@ -13646,11 +14254,15 @@ function App() {
|
|
|
13646
14254
|
headers: { 'content-type': 'application/json' },
|
|
13647
14255
|
body: JSON.stringify({ canvas, id, property, value, idIndex: occ }),
|
|
13648
14256
|
}).then((r) => r.json().catch(() => ({})));
|
|
14257
|
+
const replies = {};
|
|
13649
14258
|
let chain = editApplyChainRef.current.catch(() => {});
|
|
13650
14259
|
for (const p of props) {
|
|
13651
14260
|
chain = chain.then((prev) => {
|
|
13652
14261
|
if (prev && prev.ok === false) throw new Error(prev.error || `${p} write failed`);
|
|
13653
|
-
return writeProp(p, patch[p])
|
|
14262
|
+
return writeProp(p, patch[p]).then((j) => {
|
|
14263
|
+
replies[p] = j;
|
|
14264
|
+
return j;
|
|
14265
|
+
});
|
|
13654
14266
|
});
|
|
13655
14267
|
}
|
|
13656
14268
|
editApplyChainRef.current = chain
|
|
@@ -13658,7 +14270,14 @@ function App() {
|
|
|
13658
14270
|
if (last && last.ok === false) throw new Error(last.error || 'resize write failed');
|
|
13659
14271
|
// Record one undo entry per written property (each Cmd+Z reverts one).
|
|
13660
14272
|
for (const p of props) {
|
|
13661
|
-
recordSourceEdit({
|
|
14273
|
+
recordSourceEdit({
|
|
14274
|
+
op: 'css',
|
|
14275
|
+
canvas,
|
|
14276
|
+
id,
|
|
14277
|
+
key: p,
|
|
14278
|
+
before: replacedValue(replies[p], b[p] ?? null),
|
|
14279
|
+
after: patch[p],
|
|
14280
|
+
});
|
|
13662
14281
|
}
|
|
13663
14282
|
// Dogfood 2026-07-07 — belt-and-suspenders reselect, mirroring the
|
|
13664
14283
|
// structural ops' `pendingReorderRef` re-settle. The DDR-105 suppression
|
|
@@ -13690,9 +14309,18 @@ function App() {
|
|
|
13690
14309
|
// inverse descriptor goes stale (same reason reorder uses the server seq log).
|
|
13691
14310
|
const structuralWriteRef = useRef(null);
|
|
13692
14311
|
const structuralWrite = useCallback(
|
|
13693
|
-
(route, body, { label, onOk, onFail } = {}) => {
|
|
14312
|
+
(route, body, { label, onOk, onFail: failed } = {}) => {
|
|
13694
14313
|
if (!activePath) return;
|
|
13695
14314
|
const canvas = activePath;
|
|
14315
|
+
// A refused structural edit used to vanish into the console: the
|
|
14316
|
+
// designer pressed Delete and nothing happened. Say so in the canvas.
|
|
14317
|
+
const onFail =
|
|
14318
|
+
failed ??
|
|
14319
|
+
((j) =>
|
|
14320
|
+
postToActiveCanvas({
|
|
14321
|
+
dgn: 'op-toast',
|
|
14322
|
+
message: `Couldn't ${label || 'apply the edit'}${j?.error ? ` — ${j.error}` : ''}.`,
|
|
14323
|
+
}));
|
|
13696
14324
|
editApplyChainRef.current = editApplyChainRef.current
|
|
13697
14325
|
.catch(() => {})
|
|
13698
14326
|
.then(() =>
|
|
@@ -13739,6 +14367,7 @@ function App() {
|
|
|
13739
14367
|
|
|
13740
14368
|
const insertElementShell = useCallback(
|
|
13741
14369
|
(refId, position, kind, opts = {}) => {
|
|
14370
|
+
const staleSig = lastLayersTreeRef.current?.sig ?? null;
|
|
13742
14371
|
structuralWrite(
|
|
13743
14372
|
'/_api/insert-element',
|
|
13744
14373
|
{
|
|
@@ -13754,7 +14383,10 @@ function App() {
|
|
|
13754
14383
|
// Select the new element once the HMR reload lands (its id is stamped
|
|
13755
14384
|
// on transpile — best-effort, like reorder's pendingReorderRef).
|
|
13756
14385
|
onOk: (j, canvas) => {
|
|
13757
|
-
if (j.newId)
|
|
14386
|
+
if (!j.newId) return;
|
|
14387
|
+
pendingReorderRef.current = { file: canvas, movedId: j.newId, artboardId: null, staleSig };
|
|
14388
|
+
const last = lastLayersTreeRef.current;
|
|
14389
|
+
if (last) settlePendingSelectionRef.current?.(last.tree, last.sig, last.artboardId);
|
|
13758
14390
|
},
|
|
13759
14391
|
}
|
|
13760
14392
|
);
|
|
@@ -13818,14 +14450,19 @@ function App() {
|
|
|
13818
14450
|
|
|
13819
14451
|
const duplicateElementShell = useCallback(
|
|
13820
14452
|
(id, idIndex) => {
|
|
14453
|
+
const staleSig = lastLayersTreeRef.current?.sig ?? null;
|
|
13821
14454
|
structuralWrite(
|
|
13822
14455
|
'/_api/duplicate-element',
|
|
13823
14456
|
{ id, idIndex: Number.isInteger(idIndex) ? idIndex : undefined },
|
|
13824
14457
|
{
|
|
13825
14458
|
label: 'duplicate element',
|
|
13826
|
-
// Select the copy once the
|
|
14459
|
+
// Select the copy once the post-write tree lands — not the pre-write
|
|
14460
|
+
// one, where the copy's id still names the next sibling.
|
|
13827
14461
|
onOk: (j, canvas) => {
|
|
13828
|
-
if (j.newId)
|
|
14462
|
+
if (!j.newId) return;
|
|
14463
|
+
pendingReorderRef.current = { file: canvas, movedId: j.newId, artboardId: null, staleSig };
|
|
14464
|
+
const last = lastLayersTreeRef.current;
|
|
14465
|
+
if (last) settlePendingSelectionRef.current?.(last.tree, last.sig, last.artboardId);
|
|
13829
14466
|
},
|
|
13830
14467
|
}
|
|
13831
14468
|
);
|
|
@@ -13895,7 +14532,14 @@ function App() {
|
|
|
13895
14532
|
.then((r) => r.json().catch(() => ({})))
|
|
13896
14533
|
.then((j) => {
|
|
13897
14534
|
if (j.ok)
|
|
13898
|
-
recordSourceEdit({
|
|
14535
|
+
recordSourceEdit({
|
|
14536
|
+
op: 'css',
|
|
14537
|
+
canvas,
|
|
14538
|
+
id,
|
|
14539
|
+
key: property,
|
|
14540
|
+
before: replacedValue(j, null),
|
|
14541
|
+
after: value,
|
|
14542
|
+
});
|
|
13899
14543
|
})
|
|
13900
14544
|
.catch(() => {})
|
|
13901
14545
|
);
|
|
@@ -14235,7 +14879,7 @@ function App() {
|
|
|
14235
14879
|
canvas,
|
|
14236
14880
|
id: req.id,
|
|
14237
14881
|
key: 'src',
|
|
14238
|
-
before: req.before ?? null,
|
|
14882
|
+
before: replacedValue(j, req.before ?? null),
|
|
14239
14883
|
after: pickedPath,
|
|
14240
14884
|
});
|
|
14241
14885
|
} else {
|
|
@@ -14699,11 +15343,18 @@ function App() {
|
|
|
14699
15343
|
kbd: '⇧⌘E',
|
|
14700
15344
|
run: () => setExportDialog({ mode: 'export' }),
|
|
14701
15345
|
},
|
|
15346
|
+
{
|
|
15347
|
+
id: 'share-link',
|
|
15348
|
+
group: 'Canvas',
|
|
15349
|
+
label: 'Copy share link',
|
|
15350
|
+
icon: 'link',
|
|
15351
|
+
run: () => { const links = shareLinksFor(sharePath); const link = links.web ?? links.app ?? links.local; if (link) copyShareLink(link); },
|
|
15352
|
+
},
|
|
14702
15353
|
{
|
|
14703
15354
|
id: 'handoff',
|
|
14704
15355
|
group: 'Canvas',
|
|
14705
15356
|
label: 'Handoff to production',
|
|
14706
|
-
icon: '
|
|
15357
|
+
icon: 'external',
|
|
14707
15358
|
kbd: '⇧⌘H',
|
|
14708
15359
|
run: () => setExportDialog({ mode: 'handoff' }),
|
|
14709
15360
|
},
|
|
@@ -14808,7 +15459,7 @@ function App() {
|
|
|
14808
15459
|
run: () => setReportBugOpen(true),
|
|
14809
15460
|
},
|
|
14810
15461
|
],
|
|
14811
|
-
[openSystem, toggleTheme, reloadActive, whatsNew, createVideo]
|
|
15462
|
+
[openSystem, toggleTheme, reloadActive, whatsNew, createVideo, sharePath, shareShell, cloudLinkedHub, localProjectName, cfg.designRel]
|
|
14812
15463
|
);
|
|
14813
15464
|
|
|
14814
15465
|
// feature-configurable-panel-docking — resolve, for each slot, the panels
|
|
@@ -14887,6 +15538,9 @@ function App() {
|
|
|
14887
15538
|
activeDsName={activePath === SYSTEM_TAB ? (systemData?.ds?.name ?? null) : null}
|
|
14888
15539
|
onOpen={openTab}
|
|
14889
15540
|
onPreview={onPreview}
|
|
15541
|
+
onOpenLinkedFile={openLinkedFile}
|
|
15542
|
+
filesReady={treeLoaded}
|
|
15543
|
+
onShare={showShare}
|
|
14890
15544
|
onOpenSystem={openSystem}
|
|
14891
15545
|
wsConnected={wsConnected}
|
|
14892
15546
|
search={search}
|
|
@@ -14900,6 +15554,10 @@ function App() {
|
|
|
14900
15554
|
onMoveCanvas={moveCanvasReq}
|
|
14901
15555
|
onNewFolder={newFolderReq}
|
|
14902
15556
|
onDeleteFolder={deleteFolderReq}
|
|
15557
|
+
onRenameFolder={renameFolderReq}
|
|
15558
|
+
onRenameCanvas={renameCanvasReq}
|
|
15559
|
+
onDuplicateCanvas={duplicateCanvasReq}
|
|
15560
|
+
onDeleteFile={deleteFileReq}
|
|
14903
15561
|
onRefresh={refreshTree}
|
|
14904
15562
|
refreshing={treeRefreshing}
|
|
14905
15563
|
collapsed={false}
|
|
@@ -14913,6 +15571,7 @@ function App() {
|
|
|
14913
15571
|
canvasKinds={cfg?.canvasKinds}
|
|
14914
15572
|
syncStatus={syncStatus}
|
|
14915
15573
|
onLinkedHub={setCloudLinkedHub}
|
|
15574
|
+
onLocalProject={setLocalProjectName}
|
|
14916
15575
|
savingIsManaged={savingIsManaged}
|
|
14917
15576
|
/>
|
|
14918
15577
|
);
|
|
@@ -14953,8 +15612,35 @@ function App() {
|
|
|
14953
15612
|
// WHICH HISTORY, decided at the ONE place the posture is named — not
|
|
14954
15613
|
// inside the panel, which would be a second derivation of the rule
|
|
14955
15614
|
// `cloud-managed-save-surfaces.test.ts` exists to keep singular.
|
|
14956
|
-
loadLog={
|
|
14957
|
-
|
|
15615
|
+
loadLog={async (path) => {
|
|
15616
|
+
// The project's accepted history when it has one; Git otherwise.
|
|
15617
|
+
const accepted = await loadAcceptedLog(path);
|
|
15618
|
+
setProjectHistoryOn(accepted !== 'legacy');
|
|
15619
|
+
if (accepted !== 'legacy') return accepted;
|
|
15620
|
+
return (cloudManaged ? gitLoadCloudLog : gitLoadLog)(path);
|
|
15621
|
+
}}
|
|
15622
|
+
historySource={projectHistoryOn ? 'project' : cloudManaged ? 'cloud' : 'local'}
|
|
15623
|
+
onRestoreVersion={async (revision) => {
|
|
15624
|
+
const r = await fetch('/_api/project/restore', {
|
|
15625
|
+
method: 'POST',
|
|
15626
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15627
|
+
body: JSON.stringify({ path: activePath, revision }),
|
|
15628
|
+
}).catch(() => null);
|
|
15629
|
+
const j = r ? await r.json().catch(() => null) : null;
|
|
15630
|
+
return !!j?.ok;
|
|
15631
|
+
}}
|
|
15632
|
+
// Not through a cell's door: every browser editor proposes under the
|
|
15633
|
+
// studio's one credential there, so "your own action" is not
|
|
15634
|
+
// knowable and the hub refuses the route (studio-manifest.mjs).
|
|
15635
|
+
onUndoAction={cellManaged ? undefined : async (actionId) => {
|
|
15636
|
+
const r = await fetch('/_api/project/undo', {
|
|
15637
|
+
method: 'POST',
|
|
15638
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15639
|
+
body: JSON.stringify({ actionId }),
|
|
15640
|
+
}).catch(() => null);
|
|
15641
|
+
const j = r ? await r.json().catch(() => null) : null;
|
|
15642
|
+
return !!j?.ok;
|
|
15643
|
+
}}
|
|
14958
15644
|
// What the cloud half of the header names. The cell reports its own
|
|
14959
15645
|
// project and branch with the log; the hub host is the fallback,
|
|
14960
15646
|
// because a header that names the LOCAL folder while listing the
|
|
@@ -15071,8 +15757,10 @@ function App() {
|
|
|
15071
15757
|
only appears if you happen to open the Sync panel is not consent.
|
|
15072
15758
|
Skipped during first-run onboarding so two flows don't stack. */}
|
|
15073
15759
|
{!firstRun && <SyncConsentDialog status={syncStatus} cloud={cfg.cloud} />}
|
|
15074
|
-
{
|
|
15075
|
-
|
|
15760
|
+
<NotificationHost paused={!!usageNudge || !!tourSteps}
|
|
15761
|
+
hiddenGroups={exportCenter.panelOpen ? ['exports'] : []} />
|
|
15762
|
+
<WhatsNewToast wn={whatsNew} />
|
|
15763
|
+
<ExportToast center={exportCenter} />
|
|
15076
15764
|
{gitLifecycle && (
|
|
15077
15765
|
<div role="status" aria-live="polite" className="st-banner st-banner--info">
|
|
15078
15766
|
<span className="st-banner-dot" aria-hidden="true" />
|
|
@@ -15095,6 +15783,8 @@ function App() {
|
|
|
15095
15783
|
)}
|
|
15096
15784
|
<div className="st-shell">
|
|
15097
15785
|
<Menubar
|
|
15786
|
+
sharePath={sharePath}
|
|
15787
|
+
onShare={() => showShare()}
|
|
15098
15788
|
readOnly={viewerMode}
|
|
15099
15789
|
// Cloud Phase 27 C2/C4 — the ONE cloud-only input the shared chrome
|
|
15100
15790
|
// takes. A prop, not the whole `cfg`: the Menubar needs to know it is
|
|
@@ -15259,6 +15949,10 @@ function App() {
|
|
|
15259
15949
|
cfg={cfg}
|
|
15260
15950
|
loadingPath={loadingPath}
|
|
15261
15951
|
onIframeLoad={onIframeLoad}
|
|
15952
|
+
canvasError={canvasError}
|
|
15953
|
+
canvasReloadNonce={canvasReloadNonce}
|
|
15954
|
+
onRetryCanvasLoad={retryCanvasLoad}
|
|
15955
|
+
loadedPath={loadedPath}
|
|
15262
15956
|
showQuickSetup={
|
|
15263
15957
|
isNativeApp() && !viewerMode && !!setupReadiness && !setupReadiness.ready
|
|
15264
15958
|
}
|
|
@@ -16103,6 +16797,7 @@ function App() {
|
|
|
16103
16797
|
</div>
|
|
16104
16798
|
</div>
|
|
16105
16799
|
)}
|
|
16800
|
+
{shareDialog && <ShareDialog target={shareDialog} links={shareLinksFor(shareDialog.path)} shell={shareShell} onClose={() => setShareDialog(null)} Icon={StIcon} />}
|
|
16106
16801
|
{exportDialog && (
|
|
16107
16802
|
<ExportDialog
|
|
16108
16803
|
mode={exportDialog.mode}
|