openplanr 2.2640.8 → 2.2641.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/CHANGELOG.md +14 -0
- package/dist/dashboard/.vite/manifest.json +1 -1
- package/dist/dashboard/assets/{index-COZ36pFn.js → index-D7R6tnlX.js} +2 -2
- package/dist/dashboard/assets/{index-COZ36pFn.js.map → index-D7R6tnlX.js.map} +1 -1
- package/dist/dashboard/dashboard-manifest.json +8 -8
- package/dist/dashboard/index.html +1 -1
- package/lib/host-packages/adapter-registry.json +5 -5
- package/lib/host-packages/capability-map.json +1 -1
- package/lib/host-packages/claude/.claude-plugin/marketplace.json +2 -2
- package/lib/host-packages/claude/openplanr/.claude-plugin/plugin.json +1 -1
- package/lib/host-packages/claude/openplanr/.openplanr-content.json +167 -35
- package/lib/host-packages/claude/openplanr/README.md +22 -1
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-artifact-shell.mjs +3 -11
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-chunked-workspace-client.mjs +555 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/{design-pako-esm.mjs → design-compression.mjs} +2 -1521
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-document.mjs +133 -12
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-encrypted-workspace-client.mjs +647 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-entities.mjs +17 -67
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-handoff.mjs +14 -16
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/{design-planr-home.mjs → design-loopback-server.mjs} +15 -36
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-owner-custody.mjs +121 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-resource-pack.mjs +698 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-review.mjs +798 -796
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-runtime-home.mjs +39 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-runtime-support-5cb7531bede8.mjs +24 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-share.mjs +138 -765
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/{design-escape.mjs → design-shared-artifact-support-dependencies-design-support-protocol-contracts-3d9b1a86.mjs} +158 -156
- package/lib/host-packages/{openai/openplanr/runtime/design/scripts/design-artifact-sources.mjs → claude/openplanr/runtime/design/scripts/design-shared-artifact-support-protocol-contracts-ea2cd15e.mjs} +1 -1
- package/lib/host-packages/{openai/openplanr/runtime/design/scripts/design-bounded-json-data.mjs → claude/openplanr/runtime/design/scripts/design-shared-protocol-contracts-31a760fc.mjs} +241 -454
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-shared-protocol-contracts-75a938cc.mjs +222 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-upload-spool.mjs +198 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design.mjs +12 -5
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/lib/artifact/ui/studio-shell.css +1 -1
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/01-module-runtime.js +18 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/02-artifact-review-ui.js +809 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/03-protocol-contracts.js +5193 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/04-compression.js +4175 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/05-artifact-sharing.js +65 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/06-artifact-review-ui.js +1755 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/07-artifact-sharing.js +62 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/08-artifact-review-ui.js +2600 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources.json +123 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/package.json +3 -3
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.css +3 -3
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/01-module-runtime.js +34 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/02-react.js +992 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/03-react-dom-client-fiber-foundations.js +2513 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/04-react-dom-client-reconciliation-hooks.js +1919 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/05-react-dom-client-render-phases.js +1759 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/06-react-dom-client-commit-effects.js +1551 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/07-react-dom-client-work-loop-scheduling.js +1121 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/08-react-dom-client-dom-events-properties-hydration.js +2183 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/09-react-dom-client-public-roots.js +705 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/10-react.js +27 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/11-artifact-review-ui.js +13 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/12-design-review-experience.js +3561 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/13-studio-shell.js +184 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/14-interface-primitives.js +5045 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/15-interface-primitives.js +1698 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/16-studio-shell.js +455 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/17-design-studio.js +1844 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources.json +257 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/package.json +1 -1
- package/lib/host-packages/claude/openplanr/runtime/plan/scripts/design-protocol-contracts.mjs +4966 -0
- package/lib/host-packages/claude/openplanr/runtime/plan/scripts/design.mjs +240 -5192
- package/lib/host-packages/claude/openplanr/runtime/plan/scripts/runtime/packages/protocol/package.json +1 -1
- package/lib/host-packages/claude/openplanr/skills/delegate/scripts/backend-diagnostics.mjs +6 -2
- package/lib/host-packages/cursor/openplanr/.openplanr-content.json +166 -34
- package/lib/host-packages/cursor/openplanr/manifest.json +1 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-delegate/scripts/backend-diagnostics.mjs +6 -2
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-artifact-shell.mjs +3 -11
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-chunked-workspace-client.mjs +555 -0
- package/lib/host-packages/{openai/openplanr/runtime/design/scripts/design-pako-esm.mjs → cursor/openplanr/runtime/design/scripts/design-compression.mjs} +2 -1521
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-document.mjs +133 -12
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-encrypted-workspace-client.mjs +647 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-entities.mjs +17 -67
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-handoff.mjs +14 -16
- package/lib/host-packages/{openai/openplanr/runtime/design/scripts/design-planr-home.mjs → cursor/openplanr/runtime/design/scripts/design-loopback-server.mjs} +15 -36
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-owner-custody.mjs +121 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-resource-pack.mjs +698 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-review.mjs +798 -796
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-runtime-home.mjs +39 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-runtime-support-5cb7531bede8.mjs +24 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-share.mjs +138 -765
- package/lib/host-packages/{openai/openplanr/runtime/design/scripts/design-escape.mjs → cursor/openplanr/runtime/design/scripts/design-shared-artifact-support-dependencies-design-support-protocol-contracts-3d9b1a86.mjs} +158 -156
- package/lib/host-packages/{claude/openplanr/runtime/design/scripts/design-artifact-sources.mjs → cursor/openplanr/runtime/design/scripts/design-shared-artifact-support-protocol-contracts-ea2cd15e.mjs} +1 -1
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/{design-bounded-json-data.mjs → design-shared-protocol-contracts-31a760fc.mjs} +241 -454
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-shared-protocol-contracts-75a938cc.mjs +222 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-upload-spool.mjs +198 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design.mjs +12 -5
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/lib/artifact/ui/studio-shell.css +1 -1
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/01-module-runtime.js +18 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/02-artifact-review-ui.js +809 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/03-protocol-contracts.js +5193 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/04-compression.js +4175 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/05-artifact-sharing.js +65 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/06-artifact-review-ui.js +1755 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/07-artifact-sharing.js +62 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/08-artifact-review-ui.js +2600 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources.json +123 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/package.json +3 -3
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.css +3 -3
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/01-module-runtime.js +34 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/02-react.js +992 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/03-react-dom-client-fiber-foundations.js +2513 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/04-react-dom-client-reconciliation-hooks.js +1919 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/05-react-dom-client-render-phases.js +1759 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/06-react-dom-client-commit-effects.js +1551 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/07-react-dom-client-work-loop-scheduling.js +1121 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/08-react-dom-client-dom-events-properties-hydration.js +2183 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/09-react-dom-client-public-roots.js +705 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/10-react.js +27 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/11-artifact-review-ui.js +13 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/12-design-review-experience.js +3561 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/13-studio-shell.js +184 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/14-interface-primitives.js +5045 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/15-interface-primitives.js +1698 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/16-studio-shell.js +455 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/17-design-studio.js +1844 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources.json +257 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/package.json +1 -1
- package/lib/host-packages/cursor/openplanr/runtime/plan/scripts/design-protocol-contracts.mjs +4966 -0
- package/lib/host-packages/cursor/openplanr/runtime/plan/scripts/design.mjs +240 -5192
- package/lib/host-packages/cursor/openplanr/runtime/plan/scripts/runtime/packages/protocol/package.json +1 -1
- package/lib/host-packages/openai/.claude-plugin/marketplace.json +2 -2
- package/lib/host-packages/openai/openplanr/.codex-plugin/plugin.json +1 -1
- package/lib/host-packages/openai/openplanr/.openplanr-content.json +166 -34
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-artifact-shell.mjs +3 -11
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-chunked-workspace-client.mjs +555 -0
- package/lib/host-packages/{cursor/openplanr/runtime/design/scripts/design-pako-esm.mjs → openai/openplanr/runtime/design/scripts/design-compression.mjs} +2 -1521
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-document.mjs +133 -12
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-encrypted-workspace-client.mjs +647 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-entities.mjs +17 -67
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-handoff.mjs +14 -16
- package/lib/host-packages/{cursor/openplanr/runtime/design/scripts/design-planr-home.mjs → openai/openplanr/runtime/design/scripts/design-loopback-server.mjs} +15 -36
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-owner-custody.mjs +121 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-resource-pack.mjs +698 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-review.mjs +798 -796
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-runtime-home.mjs +39 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-runtime-support-5cb7531bede8.mjs +24 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-share.mjs +138 -765
- package/lib/host-packages/{cursor/openplanr/runtime/design/scripts/design-escape.mjs → openai/openplanr/runtime/design/scripts/design-shared-artifact-support-dependencies-design-support-protocol-contracts-3d9b1a86.mjs} +158 -156
- package/lib/host-packages/{cursor/openplanr/runtime/design/scripts/design-artifact-sources.mjs → openai/openplanr/runtime/design/scripts/design-shared-artifact-support-protocol-contracts-ea2cd15e.mjs} +1 -1
- package/lib/host-packages/{claude/openplanr/runtime/design/scripts/design-bounded-json-data.mjs → openai/openplanr/runtime/design/scripts/design-shared-protocol-contracts-31a760fc.mjs} +241 -454
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-shared-protocol-contracts-75a938cc.mjs +222 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-upload-spool.mjs +198 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design.mjs +12 -5
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/lib/artifact/ui/studio-shell.css +1 -1
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/01-module-runtime.js +18 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/02-artifact-review-ui.js +809 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/03-protocol-contracts.js +5193 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/04-compression.js +4175 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/05-artifact-sharing.js +65 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/06-artifact-review-ui.js +1755 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/07-artifact-sharing.js +62 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/08-artifact-review-ui.js +2600 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources.json +123 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/package.json +3 -3
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.css +3 -3
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/01-module-runtime.js +34 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/02-react.js +992 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/03-react-dom-client-fiber-foundations.js +2513 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/04-react-dom-client-reconciliation-hooks.js +1919 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/05-react-dom-client-render-phases.js +1759 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/06-react-dom-client-commit-effects.js +1551 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/07-react-dom-client-work-loop-scheduling.js +1121 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/08-react-dom-client-dom-events-properties-hydration.js +2183 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/09-react-dom-client-public-roots.js +705 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/10-react.js +27 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/11-artifact-review-ui.js +13 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/12-design-review-experience.js +3561 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/13-studio-shell.js +184 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/14-interface-primitives.js +5045 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/15-interface-primitives.js +1698 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/16-studio-shell.js +455 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/17-design-studio.js +1844 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources.json +257 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/package.json +1 -1
- package/lib/host-packages/openai/openplanr/runtime/plan/scripts/design-protocol-contracts.mjs +4966 -0
- package/lib/host-packages/openai/openplanr/runtime/plan/scripts/design.mjs +240 -5192
- package/lib/host-packages/openai/openplanr/runtime/plan/scripts/runtime/packages/protocol/package.json +1 -1
- package/lib/host-packages/openai/openplanr/skills/delegate/scripts/backend-diagnostics.mjs +6 -2
- package/package.json +2 -2
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-generated-b0798298053515fd.mjs +0 -18
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js +0 -5
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js +0 -25597
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-generated-b0798298053515fd.mjs +0 -18
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js +0 -5
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js +0 -25597
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-generated-b0798298053515fd.mjs +0 -18
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js +0 -5
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js +0 -25597
|
@@ -0,0 +1,2600 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
// lib/artifact/ui/share-dialog.mjs
|
|
4
|
+
var ARTIFACT_SHARE_FRAGMENT_LIMIT = 8e3;
|
|
5
|
+
var ARTIFACT_SHARE_TTLS2 = Object.freeze({
|
|
6
|
+
"1d": Object.freeze({ label: "1 day", milliseconds: 864e5 }),
|
|
7
|
+
"7d": Object.freeze({ label: "7 days", milliseconds: 6048e5 }),
|
|
8
|
+
"30d": Object.freeze({ label: "30 days", milliseconds: 2592e6 })
|
|
9
|
+
});
|
|
10
|
+
var ARTIFACT_SHARE_TRANSPORTS = Object.freeze(["live", "fragment", "short"]);
|
|
11
|
+
var PHASES = Object.freeze([
|
|
12
|
+
"idle",
|
|
13
|
+
"previewing",
|
|
14
|
+
"ready",
|
|
15
|
+
"custody-preparing",
|
|
16
|
+
"custody-ready",
|
|
17
|
+
"creating",
|
|
18
|
+
"ambiguous",
|
|
19
|
+
"created",
|
|
20
|
+
"error"
|
|
21
|
+
]);
|
|
22
|
+
var LIVE_ROOM_ID_RE = /^[A-Za-z0-9_-]{16,128}$/;
|
|
23
|
+
var LIVE_ROOM_SECRET_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
24
|
+
var LIVE_ROOM_KEY_ID_RE = /^sha256:[a-f0-9]{64}$/;
|
|
25
|
+
var LIVE_ROOM_REVIEW_RE = /^[a-f0-9]{64}$/;
|
|
26
|
+
var BASE64URL_RE = /^[A-Za-z0-9_-]+$/;
|
|
27
|
+
var OWNER_SECRET_MAX_BYTES = 64 * 1024;
|
|
28
|
+
var ROOM_RECOVERY_MAX_BYTES = 8 * 1024 * 1024;
|
|
29
|
+
var ArtifactShareUiError = class extends Error {
|
|
30
|
+
constructor(code, message, details = {}) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = "ArtifactShareUiError";
|
|
33
|
+
this.code = code;
|
|
34
|
+
this.details = Object.freeze({ ...details });
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
function member(value, allowed, fallback) {
|
|
38
|
+
return allowed.includes(value) ? value : fallback;
|
|
39
|
+
}
|
|
40
|
+
function count(value, name) {
|
|
41
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
42
|
+
throw new ArtifactShareUiError(
|
|
43
|
+
"E_ARTIFACT_SHARE_PREVIEW_INVALID",
|
|
44
|
+
`${name} must be a non-negative integer.`,
|
|
45
|
+
{ field: name }
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function text4(value, fallback = "") {
|
|
51
|
+
return typeof value === "string" ? value : fallback;
|
|
52
|
+
}
|
|
53
|
+
function record(value) {
|
|
54
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
55
|
+
}
|
|
56
|
+
function exactKeys(value, expected) {
|
|
57
|
+
return record(value) && JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...expected].sort());
|
|
58
|
+
}
|
|
59
|
+
function exactLoopback(hostname) {
|
|
60
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1";
|
|
61
|
+
}
|
|
62
|
+
function parseLiveResultUrl(value, capability) {
|
|
63
|
+
let url;
|
|
64
|
+
try {
|
|
65
|
+
url = new URL(value);
|
|
66
|
+
} catch {
|
|
67
|
+
throw new ArtifactShareUiError(
|
|
68
|
+
"E_ARTIFACT_SHARE_RESULT_INVALID",
|
|
69
|
+
"Live room creation returned a malformed capability URL."
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const fields = new URLSearchParams(url.hash.slice(1));
|
|
73
|
+
const keys = [...fields.keys()];
|
|
74
|
+
const authenticatedRead = fields.has("r");
|
|
75
|
+
const expected = ["k", capability, ...authenticatedRead ? ["r"] : []];
|
|
76
|
+
const roomId = /^\/r\/([A-Za-z0-9_-]{16,128})\/?$/.exec(url.pathname)?.[1];
|
|
77
|
+
if (url.username || url.password || url.search || url.protocol !== "https:" && !(url.protocol === "http:" && exactLoopback(url.hostname)) || !roomId || !LIVE_ROOM_ID_RE.test(roomId) || keys.length !== expected.length || !keys.includes("k") || !keys.includes(capability) || keys.some((key) => !expected.includes(key) || fields.getAll(key).length !== 1) || authenticatedRead && !LIVE_ROOM_SECRET_RE.test(fields.get("r") ?? "") || !LIVE_ROOM_SECRET_RE.test(fields.get("k") ?? "") || !LIVE_ROOM_SECRET_RE.test(fields.get(capability) ?? "")) {
|
|
78
|
+
throw new ArtifactShareUiError(
|
|
79
|
+
"E_ARTIFACT_SHARE_RESULT_INVALID",
|
|
80
|
+
"Live room creation returned an invalid or mixed capability URL."
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return Object.freeze({
|
|
84
|
+
origin: url.origin,
|
|
85
|
+
pathname: url.pathname.replace(/\/$/, ""),
|
|
86
|
+
key: fields.get("k"),
|
|
87
|
+
capability: fields.get(capability),
|
|
88
|
+
readCapability: fields.get("r")
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function validateLiveResultUrls({ url, ownerUrl, manageUrl }) {
|
|
92
|
+
const reviewer = parseLiveResultUrl(url, "w");
|
|
93
|
+
const owner = parseLiveResultUrl(ownerUrl, "o");
|
|
94
|
+
const management = parseLiveResultUrl(manageUrl, "m");
|
|
95
|
+
if (reviewer.origin !== owner.origin || reviewer.origin !== management.origin || reviewer.pathname !== owner.pathname || reviewer.pathname !== management.pathname || reviewer.key !== owner.key || reviewer.key !== management.key || reviewer.readCapability !== owner.readCapability || reviewer.readCapability !== management.readCapability || (/* @__PURE__ */ new Set([reviewer.capability, owner.capability, management.capability])).size !== 3) {
|
|
96
|
+
throw new ArtifactShareUiError(
|
|
97
|
+
"E_ARTIFACT_SHARE_RESULT_INVALID",
|
|
98
|
+
"Live room creation must return separate reviewer, owner-verdict, and management capabilities for one room."
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function normalizeOwnerCustody(value) {
|
|
103
|
+
if (record(value?.prepared) && record(value?.recovery)) {
|
|
104
|
+
const { prepared, recovery } = value;
|
|
105
|
+
if (prepared.protocolVersion === "3.0.0") {
|
|
106
|
+
try {
|
|
107
|
+
assertLiveRoomV3RecoveryMatchesPreparation(prepared, recovery);
|
|
108
|
+
} catch {
|
|
109
|
+
throw new ArtifactShareUiError(
|
|
110
|
+
"E_ARTIFACT_SHARE_OWNER_CUSTODY_INVALID",
|
|
111
|
+
"Live room recovery custody does not match its prepared creation."
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
validateLiveResultUrls(prepared);
|
|
115
|
+
if (recovery.schemaVersion !== "2.0.0")
|
|
116
|
+
throw new ArtifactShareUiError(
|
|
117
|
+
"E_ARTIFACT_SHARE_OWNER_CUSTODY_INVALID",
|
|
118
|
+
"Live room recovery version is invalid."
|
|
119
|
+
);
|
|
120
|
+
const signerCustody2 = normalizeOwnerCustody({
|
|
121
|
+
signer: prepared.ownerSigner,
|
|
122
|
+
secret: recovery.ownerSigner
|
|
123
|
+
});
|
|
124
|
+
if (JSON.stringify(prepared.ownerKey) !== JSON.stringify({
|
|
125
|
+
algorithm: signerCustody2.signer.algorithm,
|
|
126
|
+
encoding: signerCustody2.signer.encoding,
|
|
127
|
+
keyId: signerCustody2.signer.keyId,
|
|
128
|
+
value: signerCustody2.signer.value ?? signerCustody2.signer.publicKey
|
|
129
|
+
}))
|
|
130
|
+
throw new ArtifactShareUiError(
|
|
131
|
+
"E_ARTIFACT_SHARE_OWNER_CUSTODY_INVALID",
|
|
132
|
+
"Live room recovery owner key does not match its prepared signer."
|
|
133
|
+
);
|
|
134
|
+
const serialized3 = `${JSON.stringify(recovery, null, 2)}
|
|
135
|
+
`;
|
|
136
|
+
if (new TextEncoder().encode(serialized3).byteLength > ROOM_RECOVERY_MAX_BYTES)
|
|
137
|
+
throw new ArtifactShareUiError(
|
|
138
|
+
"E_ARTIFACT_SHARE_OWNER_CUSTODY_INVALID",
|
|
139
|
+
"Live room recovery bundle exceeds its bounded export size."
|
|
140
|
+
);
|
|
141
|
+
return Object.freeze({
|
|
142
|
+
credential: prepared,
|
|
143
|
+
signer: signerCustody2.signer,
|
|
144
|
+
serialized: serialized3,
|
|
145
|
+
filename: `openplanr-live-room-recovery-${prepared.roomId.slice(0, 12)}.json`
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (recovery.schemaVersion !== "1.0.0")
|
|
149
|
+
throw new ArtifactShareUiError(
|
|
150
|
+
"E_ARTIFACT_SHARE_OWNER_CUSTODY_INVALID",
|
|
151
|
+
"Live room recovery version is invalid."
|
|
152
|
+
);
|
|
153
|
+
if (!exactKeys(prepared, [
|
|
154
|
+
"schemaVersion",
|
|
155
|
+
"kind",
|
|
156
|
+
"protocolVersion",
|
|
157
|
+
"id",
|
|
158
|
+
"roomId",
|
|
159
|
+
"reviewOf",
|
|
160
|
+
"ttl",
|
|
161
|
+
"ownerKey"
|
|
162
|
+
]) || prepared.schemaVersion !== "1.0.0" || prepared.kind !== "openplanr-live-room-preparation" || prepared.protocolVersion !== "2.0.0" || prepared.id !== prepared.roomId || !LIVE_ROOM_ID_RE.test(prepared.roomId ?? "") || !LIVE_ROOM_REVIEW_RE.test(prepared.reviewOf ?? "") || !Object.hasOwn(ARTIFACT_SHARE_TTLS2, prepared.ttl) || !record(prepared.ownerKey) || !exactKeys(prepared.ownerKey, ["algorithm", "encoding", "keyId", "value"]) || !record(prepared.ownerSigner) || !exactKeys(recovery, [
|
|
163
|
+
"schemaVersion",
|
|
164
|
+
"kind",
|
|
165
|
+
"protocolVersion",
|
|
166
|
+
"id",
|
|
167
|
+
"roomId",
|
|
168
|
+
"reviewOf",
|
|
169
|
+
"ttl",
|
|
170
|
+
"ownerKey",
|
|
171
|
+
"url",
|
|
172
|
+
"ownerUrl",
|
|
173
|
+
"manageUrl",
|
|
174
|
+
"ownerSigner"
|
|
175
|
+
]) || recovery.schemaVersion !== "1.0.0" || recovery.kind !== "openplanr-live-room-recovery" || recovery.protocolVersion !== "2.0.0" || recovery.id !== prepared.id || recovery.roomId !== prepared.roomId || recovery.reviewOf !== prepared.reviewOf || recovery.ttl !== prepared.ttl || JSON.stringify(recovery.ownerKey) !== JSON.stringify(prepared.ownerKey) || recovery.url !== prepared.url || recovery.ownerUrl !== prepared.ownerUrl || recovery.manageUrl !== prepared.manageUrl) {
|
|
176
|
+
throw new ArtifactShareUiError(
|
|
177
|
+
"E_ARTIFACT_SHARE_OWNER_CUSTODY_INVALID",
|
|
178
|
+
"Live room recovery custody does not match its prepared creation."
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
validateLiveResultUrls(recovery);
|
|
182
|
+
const signerCustody = normalizeOwnerCustody({
|
|
183
|
+
signer: prepared.ownerSigner,
|
|
184
|
+
secret: recovery.ownerSigner
|
|
185
|
+
});
|
|
186
|
+
if (prepared.ownerKey.algorithm !== signerCustody.signer.algorithm || prepared.ownerKey.encoding !== signerCustody.signer.encoding || prepared.ownerKey.keyId !== signerCustody.signer.keyId || prepared.ownerKey.value !== (signerCustody.signer.value ?? signerCustody.signer.publicKey)) {
|
|
187
|
+
throw new ArtifactShareUiError(
|
|
188
|
+
"E_ARTIFACT_SHARE_OWNER_CUSTODY_INVALID",
|
|
189
|
+
"Live room recovery owner key does not match its prepared signer."
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
const serialized2 = `${JSON.stringify(recovery, null, 2)}
|
|
193
|
+
`;
|
|
194
|
+
const byteLength2 = new TextEncoder().encode(serialized2).byteLength;
|
|
195
|
+
if (byteLength2 < 2 || byteLength2 > OWNER_SECRET_MAX_BYTES) {
|
|
196
|
+
throw new ArtifactShareUiError(
|
|
197
|
+
"E_ARTIFACT_SHARE_OWNER_CUSTODY_INVALID",
|
|
198
|
+
"Live room recovery bundle exceeds its bounded export size."
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return Object.freeze({
|
|
202
|
+
credential: prepared,
|
|
203
|
+
signer: signerCustody.signer,
|
|
204
|
+
serialized: serialized2,
|
|
205
|
+
filename: `openplanr-live-room-recovery-${prepared.roomId.slice(0, 12)}.json`
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (!record(value) || !record(value.signer) || !record(value.secret)) {
|
|
209
|
+
throw new ArtifactShareUiError(
|
|
210
|
+
"E_ARTIFACT_SHARE_OWNER_CUSTODY_INVALID",
|
|
211
|
+
"Live room owner custody could not be prepared safely."
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
const { signer, secret } = value;
|
|
215
|
+
if (signer.role !== "owner" || typeof signer.sign !== "function" || signer.algorithm !== "ECDSA-P256-SHA256" || signer.encoding !== "spki-base64url" || !LIVE_ROOM_KEY_ID_RE.test(signer.keyId ?? "") || !BASE64URL_RE.test(signer.value ?? signer.publicKey ?? "") || (signer.value ?? signer.publicKey).length < 64 || (signer.value ?? signer.publicKey).length > 512) {
|
|
216
|
+
throw new ArtifactShareUiError(
|
|
217
|
+
"E_ARTIFACT_SHARE_OWNER_CUSTODY_INVALID",
|
|
218
|
+
"Live room owner signer is invalid."
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (!exactKeys(secret, [
|
|
222
|
+
"schemaVersion",
|
|
223
|
+
"kind",
|
|
224
|
+
"role",
|
|
225
|
+
"algorithm",
|
|
226
|
+
"keyId",
|
|
227
|
+
"publicKey",
|
|
228
|
+
"privateKey"
|
|
229
|
+
]) || secret.schemaVersion !== "1.0.0" || secret.kind !== "openplanr-live-room-signer" || secret.role !== "owner" || secret.algorithm !== "ECDSA-P256-SHA256" || secret.keyId !== signer.keyId || secret.publicKey !== (signer.value ?? signer.publicKey) || !BASE64URL_RE.test(secret.publicKey ?? "") || !BASE64URL_RE.test(secret.privateKey ?? "") || secret.publicKey.length < 64 || secret.publicKey.length > 512 || secret.privateKey.length < 64 || secret.privateKey.length > 1024) {
|
|
230
|
+
throw new ArtifactShareUiError(
|
|
231
|
+
"E_ARTIFACT_SHARE_OWNER_CUSTODY_INVALID",
|
|
232
|
+
"Live room owner secret does not match its signer."
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
const serialized = `${JSON.stringify(secret, null, 2)}
|
|
236
|
+
`;
|
|
237
|
+
const byteLength = new TextEncoder().encode(serialized).byteLength;
|
|
238
|
+
if (byteLength < 2 || byteLength > OWNER_SECRET_MAX_BYTES) {
|
|
239
|
+
throw new ArtifactShareUiError(
|
|
240
|
+
"E_ARTIFACT_SHARE_OWNER_CUSTODY_INVALID",
|
|
241
|
+
"Live room owner secret exceeds its bounded export size."
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return Object.freeze({
|
|
245
|
+
credential: signer,
|
|
246
|
+
signer,
|
|
247
|
+
serialized,
|
|
248
|
+
filename: `openplanr-live-room-owner-${signer.keyId.slice(7, 19)}.json`
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
function defaultSaveOwnerCustody(document2, window, { filename, serialized }) {
|
|
252
|
+
if (typeof window?.Blob !== "function" || typeof window?.URL?.createObjectURL !== "function" || typeof window?.URL?.revokeObjectURL !== "function" || typeof document2?.createElement !== "function" || !document2.body) {
|
|
253
|
+
throw new ArtifactShareUiError(
|
|
254
|
+
"E_ARTIFACT_SHARE_OWNER_CUSTODY_UNAVAILABLE",
|
|
255
|
+
"This browser cannot save the private owner key. No room was created."
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
const blobUrl = window.URL.createObjectURL(
|
|
259
|
+
new window.Blob([serialized], { type: "application/json;charset=utf-8" })
|
|
260
|
+
);
|
|
261
|
+
try {
|
|
262
|
+
const anchor2 = document2.createElement("a");
|
|
263
|
+
anchor2.hidden = true;
|
|
264
|
+
anchor2.href = blobUrl;
|
|
265
|
+
anchor2.download = filename;
|
|
266
|
+
anchor2.rel = "noreferrer";
|
|
267
|
+
document2.body.append(anchor2);
|
|
268
|
+
anchor2.click();
|
|
269
|
+
anchor2.remove();
|
|
270
|
+
} catch (error) {
|
|
271
|
+
throw new ArtifactShareUiError(
|
|
272
|
+
"E_ARTIFACT_SHARE_OWNER_CUSTODY_UNAVAILABLE",
|
|
273
|
+
"The private owner key could not be handed to the browser download manager. No room was created.",
|
|
274
|
+
{ cause: error?.name }
|
|
275
|
+
);
|
|
276
|
+
} finally {
|
|
277
|
+
window.setTimeout(() => window.URL.revokeObjectURL(blobUrl), 0);
|
|
278
|
+
}
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
async function establishArtifactOwnerCustody({
|
|
282
|
+
prepareOwnerCustody,
|
|
283
|
+
saveOwnerCustody
|
|
284
|
+
} = {}) {
|
|
285
|
+
if (typeof prepareOwnerCustody !== "function" || typeof saveOwnerCustody !== "function") {
|
|
286
|
+
throw new ArtifactShareUiError(
|
|
287
|
+
"E_ARTIFACT_SHARE_OWNER_CUSTODY_REQUIRED",
|
|
288
|
+
"Live room creation requires explicit private owner-key custody. No room was created."
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
const custody = normalizeOwnerCustody(await prepareOwnerCustody());
|
|
292
|
+
const saved = await saveOwnerCustody(
|
|
293
|
+
Object.freeze({
|
|
294
|
+
filename: custody.filename,
|
|
295
|
+
serialized: custody.serialized
|
|
296
|
+
})
|
|
297
|
+
);
|
|
298
|
+
if (saved !== true && saved?.saved !== true) {
|
|
299
|
+
throw new ArtifactShareUiError(
|
|
300
|
+
"E_ARTIFACT_SHARE_OWNER_CUSTODY_UNAVAILABLE",
|
|
301
|
+
"Private owner-key custody was not confirmed. No room was created."
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
return custody.credential;
|
|
305
|
+
}
|
|
306
|
+
function frozenResult(value) {
|
|
307
|
+
if (!value || typeof value !== "object" || typeof value.url !== "string" || value.url.length === 0) {
|
|
308
|
+
throw new ArtifactShareUiError(
|
|
309
|
+
"E_ARTIFACT_SHARE_RESULT_INVALID",
|
|
310
|
+
"Share creation must return a non-empty review URL."
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (!ARTIFACT_SHARE_TRANSPORTS.includes(value.transport)) {
|
|
314
|
+
throw new ArtifactShareUiError(
|
|
315
|
+
"E_ARTIFACT_SHARE_RESULT_INVALID",
|
|
316
|
+
"Share creation must return a known transport.",
|
|
317
|
+
{ field: "transport" }
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
const deletionToken = text4(value.deletionToken);
|
|
321
|
+
const ownerUrl = text4(value.ownerUrl);
|
|
322
|
+
const manageUrl = text4(value.manageUrl);
|
|
323
|
+
if (deletionToken && value.url.includes(deletionToken)) {
|
|
324
|
+
throw new ArtifactShareUiError(
|
|
325
|
+
"E_ARTIFACT_SHARE_DELETION_TOKEN_LEAK",
|
|
326
|
+
"The deletion token must never be included in the review URL."
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
if (value.transport === "live") {
|
|
330
|
+
validateLiveResultUrls({ url: value.url, ownerUrl, manageUrl });
|
|
331
|
+
} else if (ownerUrl || manageUrl) {
|
|
332
|
+
throw new ArtifactShareUiError(
|
|
333
|
+
"E_ARTIFACT_SHARE_RESULT_INVALID",
|
|
334
|
+
"Snapshot creation cannot return live room capabilities."
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
return Object.freeze({
|
|
338
|
+
transport: value.transport,
|
|
339
|
+
url: value.url,
|
|
340
|
+
ownerUrl,
|
|
341
|
+
manageUrl,
|
|
342
|
+
deletionToken,
|
|
343
|
+
expiresAt: text4(value.expiresAt)
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
function normalizeArtifactSharePreview(value = {}) {
|
|
347
|
+
const preview = value && typeof value === "object" ? value : {};
|
|
348
|
+
const fragmentLength = count(preview.fragmentLength, "fragmentLength");
|
|
349
|
+
return Object.freeze({
|
|
350
|
+
fragmentLength,
|
|
351
|
+
compressedBytes: count(preview.compressedBytes, "compressedBytes"),
|
|
352
|
+
ciphertextBytes: count(preview.ciphertextBytes, "ciphertextBytes"),
|
|
353
|
+
fragmentEligible: fragmentLength <= ARTIFACT_SHARE_FRAGMENT_LIMIT
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
function freezeState(value) {
|
|
357
|
+
return Object.freeze({
|
|
358
|
+
open: Boolean(value.open),
|
|
359
|
+
phase: member(value.phase, PHASES, "idle"),
|
|
360
|
+
transport: member(value.transport, ARTIFACT_SHARE_TRANSPORTS, "live"),
|
|
361
|
+
ttl: Object.hasOwn(ARTIFACT_SHARE_TTLS2, value.ttl) ? value.ttl : "7d",
|
|
362
|
+
preview: value.preview ? normalizeArtifactSharePreview(value.preview) : null,
|
|
363
|
+
ownerCustodyEstablished: Boolean(value.ownerCustodyEstablished),
|
|
364
|
+
result: value.result ? frozenResult(value.result) : null,
|
|
365
|
+
error: text4(value.error)
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
function createArtifactShareDialogState({ preview, ttl = "7d" } = {}) {
|
|
369
|
+
const normalizedPreview = preview ? normalizeArtifactSharePreview(preview) : null;
|
|
370
|
+
return freezeState({
|
|
371
|
+
open: false,
|
|
372
|
+
phase: normalizedPreview ? "ready" : "idle",
|
|
373
|
+
transport: "live",
|
|
374
|
+
ttl,
|
|
375
|
+
preview: normalizedPreview,
|
|
376
|
+
ownerCustodyEstablished: false,
|
|
377
|
+
result: null,
|
|
378
|
+
error: ""
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
function reduceArtifactShareDialog(state, action = {}) {
|
|
382
|
+
const current = state ?? createArtifactShareDialogState();
|
|
383
|
+
if (current.phase === "created" && action.type !== "close") return current;
|
|
384
|
+
if (current.phase === "ambiguous" && !["create-start", "create-success", "failure"].includes(action.type))
|
|
385
|
+
return current;
|
|
386
|
+
if (current.phase === "custody-ready" && ["select-transport", "set-ttl"].includes(action.type))
|
|
387
|
+
return current;
|
|
388
|
+
switch (action.type) {
|
|
389
|
+
case "open":
|
|
390
|
+
return freezeState({
|
|
391
|
+
...current,
|
|
392
|
+
open: true,
|
|
393
|
+
ownerCustodyEstablished: false,
|
|
394
|
+
result: null,
|
|
395
|
+
error: ""
|
|
396
|
+
});
|
|
397
|
+
case "close":
|
|
398
|
+
return freezeState({
|
|
399
|
+
...current,
|
|
400
|
+
open: false,
|
|
401
|
+
phase: current.preview ? "ready" : "idle",
|
|
402
|
+
ownerCustodyEstablished: false,
|
|
403
|
+
error: ""
|
|
404
|
+
});
|
|
405
|
+
case "preview-start":
|
|
406
|
+
return freezeState({
|
|
407
|
+
...current,
|
|
408
|
+
open: true,
|
|
409
|
+
phase: "previewing",
|
|
410
|
+
preview: null,
|
|
411
|
+
ownerCustodyEstablished: false,
|
|
412
|
+
result: null,
|
|
413
|
+
error: ""
|
|
414
|
+
});
|
|
415
|
+
case "preview-ready": {
|
|
416
|
+
const preview = normalizeArtifactSharePreview(action.preview);
|
|
417
|
+
return freezeState({
|
|
418
|
+
...current,
|
|
419
|
+
phase: "ready",
|
|
420
|
+
preview,
|
|
421
|
+
ownerCustodyEstablished: false,
|
|
422
|
+
transport: current.transport === "fragment" && !preview.fragmentEligible ? "short" : current.transport,
|
|
423
|
+
result: null,
|
|
424
|
+
error: ""
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
case "select-transport": {
|
|
428
|
+
const transport = member(action.transport, ARTIFACT_SHARE_TRANSPORTS, current.transport);
|
|
429
|
+
if (transport === "fragment" && current.preview?.fragmentEligible === false) return current;
|
|
430
|
+
return transport === current.transport ? current : freezeState({
|
|
431
|
+
...current,
|
|
432
|
+
phase: current.preview ? "ready" : "idle",
|
|
433
|
+
transport,
|
|
434
|
+
ownerCustodyEstablished: false,
|
|
435
|
+
result: null,
|
|
436
|
+
error: ""
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
case "set-ttl": {
|
|
440
|
+
const ttl = Object.hasOwn(ARTIFACT_SHARE_TTLS2, action.ttl) ? action.ttl : current.ttl;
|
|
441
|
+
return ttl === current.ttl ? current : freezeState({ ...current, ttl, result: null, error: "" });
|
|
442
|
+
}
|
|
443
|
+
case "custody-start":
|
|
444
|
+
return freezeState({
|
|
445
|
+
...current,
|
|
446
|
+
phase: "custody-preparing",
|
|
447
|
+
ownerCustodyEstablished: false,
|
|
448
|
+
result: null,
|
|
449
|
+
error: ""
|
|
450
|
+
});
|
|
451
|
+
case "custody-ready":
|
|
452
|
+
return freezeState({
|
|
453
|
+
...current,
|
|
454
|
+
phase: "custody-ready",
|
|
455
|
+
ownerCustodyEstablished: true,
|
|
456
|
+
result: null,
|
|
457
|
+
error: ""
|
|
458
|
+
});
|
|
459
|
+
case "create-start":
|
|
460
|
+
return freezeState({ ...current, phase: "creating", result: null, error: "" });
|
|
461
|
+
case "create-success":
|
|
462
|
+
return freezeState({ ...current, phase: "created", result: action.result, error: "" });
|
|
463
|
+
case "failure":
|
|
464
|
+
return freezeState({
|
|
465
|
+
...current,
|
|
466
|
+
phase: action.ambiguous ? "ambiguous" : "error",
|
|
467
|
+
ownerCustodyEstablished: Boolean(action.ownerCustodyEstablished),
|
|
468
|
+
result: null,
|
|
469
|
+
error: text4(action.error, "Share creation failed.")
|
|
470
|
+
});
|
|
471
|
+
default:
|
|
472
|
+
return current;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function artifactShareExpiry(ttl, now = /* @__PURE__ */ new Date()) {
|
|
476
|
+
const choice = ARTIFACT_SHARE_TTLS2[ttl] ?? ARTIFACT_SHARE_TTLS2["7d"];
|
|
477
|
+
const base = now instanceof Date ? now.getTime() : new Date(now).getTime();
|
|
478
|
+
if (!Number.isFinite(base)) throw new TypeError("Share expiry requires a valid date.");
|
|
479
|
+
return new Date(base + choice.milliseconds).toISOString();
|
|
480
|
+
}
|
|
481
|
+
function formatArtifactShareBytes(value) {
|
|
482
|
+
const bytes = count(value, "bytes");
|
|
483
|
+
if (bytes < 1e3) return `${bytes} B`;
|
|
484
|
+
if (bytes < 1e6) return `${(bytes / 1e3).toFixed(bytes >= 1e4 ? 0 : 1)} KB`;
|
|
485
|
+
return `${(bytes / 1e6).toFixed(1)} MB`;
|
|
486
|
+
}
|
|
487
|
+
function focusableElements(dialog) {
|
|
488
|
+
return [
|
|
489
|
+
...dialog.querySelectorAll(
|
|
490
|
+
'button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [href], [tabindex]:not([tabindex="-1"])'
|
|
491
|
+
)
|
|
492
|
+
].filter((element) => !element.hidden && !element.closest("[hidden]"));
|
|
493
|
+
}
|
|
494
|
+
function defaultCopy(window, value) {
|
|
495
|
+
if (typeof window?.navigator?.clipboard?.writeText !== "function") {
|
|
496
|
+
throw new ArtifactShareUiError(
|
|
497
|
+
"E_ARTIFACT_SHARE_CLIPBOARD_UNAVAILABLE",
|
|
498
|
+
"Clipboard access is unavailable. Copy the value manually."
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
return window.navigator.clipboard.writeText(value);
|
|
502
|
+
}
|
|
503
|
+
function reviewForShare(stageController) {
|
|
504
|
+
return stageController?.review?.getReview?.() ?? null;
|
|
505
|
+
}
|
|
506
|
+
function artifactShareCapabilities({
|
|
507
|
+
prepareShare,
|
|
508
|
+
prepareOwnerCustody,
|
|
509
|
+
createShare,
|
|
510
|
+
supportedTransports
|
|
511
|
+
} = {}) {
|
|
512
|
+
const canCreate = typeof prepareShare === "function" && typeof createShare === "function";
|
|
513
|
+
return Object.freeze(
|
|
514
|
+
ARTIFACT_SHARE_TRANSPORTS.filter(
|
|
515
|
+
(transport) => canCreate && (transport !== "live" || typeof prepareOwnerCustody === "function") && (supportedTransports === void 0 || Array.isArray(supportedTransports) && supportedTransports.includes(transport))
|
|
516
|
+
)
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
function mountArtifactShareDialog({
|
|
520
|
+
document: document2 = globalThis.document,
|
|
521
|
+
window = document2?.defaultView,
|
|
522
|
+
root = document2?.querySelector?.(".planr-shell"),
|
|
523
|
+
stageController,
|
|
524
|
+
prepareShare,
|
|
525
|
+
prepareOwnerCustody,
|
|
526
|
+
saveOwnerCustody,
|
|
527
|
+
createShare,
|
|
528
|
+
supportedTransports,
|
|
529
|
+
unavailableReason,
|
|
530
|
+
copyText,
|
|
531
|
+
existingRoom = false,
|
|
532
|
+
existingShareUrl = null,
|
|
533
|
+
now = () => /* @__PURE__ */ new Date()
|
|
534
|
+
} = {}) {
|
|
535
|
+
if (!document2 || !window || !root) return null;
|
|
536
|
+
const backdrop = document2.querySelector("[data-planr-share-dialog]");
|
|
537
|
+
const dialog = backdrop?.querySelector('[role="dialog"]');
|
|
538
|
+
const trigger = root.querySelector('[data-planr-action="share"]');
|
|
539
|
+
if (!backdrop || !dialog || !trigger) return null;
|
|
540
|
+
let state = createArtifactShareDialogState();
|
|
541
|
+
const capabilities = artifactShareCapabilities({
|
|
542
|
+
prepareShare,
|
|
543
|
+
prepareOwnerCustody,
|
|
544
|
+
createShare,
|
|
545
|
+
supportedTransports
|
|
546
|
+
});
|
|
547
|
+
const unavailable = text4(unavailableReason) || "Sharing is not configured in this viewer. Open the review with an updated OpenPlanr installation that supports sharing.";
|
|
548
|
+
let returnFocus = null;
|
|
549
|
+
let generation = 0;
|
|
550
|
+
let pendingOwnerCustody = null;
|
|
551
|
+
const copyResetTimers = /* @__PURE__ */ new Map();
|
|
552
|
+
const cleanup = [];
|
|
553
|
+
const handlers = {
|
|
554
|
+
prepareShare: typeof prepareShare === "function" ? prepareShare : null,
|
|
555
|
+
prepareOwnerCustody: typeof prepareOwnerCustody === "function" ? prepareOwnerCustody : null,
|
|
556
|
+
saveOwnerCustody: typeof saveOwnerCustody === "function" ? saveOwnerCustody : (value) => defaultSaveOwnerCustody(document2, window, value),
|
|
557
|
+
createShare: typeof createShare === "function" ? createShare : null,
|
|
558
|
+
copyText: typeof copyText === "function" ? copyText : (value) => defaultCopy(window, value)
|
|
559
|
+
};
|
|
560
|
+
const stableShareUrl = typeof existingShareUrl === "function" ? existingShareUrl : () => existingShareUrl;
|
|
561
|
+
function supports(transport) {
|
|
562
|
+
return capabilities.includes(transport) && (transport !== "fragment" || state.preview?.fragmentEligible !== false);
|
|
563
|
+
}
|
|
564
|
+
function selectSupportedTransport() {
|
|
565
|
+
if (!supports(state.transport)) {
|
|
566
|
+
const transport = capabilities.find(supports);
|
|
567
|
+
if (transport)
|
|
568
|
+
state = reduceArtifactShareDialog(state, { type: "select-transport", transport });
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
function listen(target, type, handler, options) {
|
|
572
|
+
target.addEventListener(type, handler, options);
|
|
573
|
+
cleanup.push(() => target.removeEventListener(type, handler, options));
|
|
574
|
+
}
|
|
575
|
+
function announce2(message) {
|
|
576
|
+
const live = dialog.querySelector("[data-planr-share-status]");
|
|
577
|
+
if (live) live.textContent = message;
|
|
578
|
+
}
|
|
579
|
+
function resetCopyButton(button) {
|
|
580
|
+
const timer = copyResetTimers.get(button);
|
|
581
|
+
if (timer) window.clearTimeout(timer);
|
|
582
|
+
copyResetTimers.delete(button);
|
|
583
|
+
button.removeAttribute("data-planr-copy-state");
|
|
584
|
+
const label = button.querySelector?.(".planr-action-label");
|
|
585
|
+
if (label) label.textContent = button.dataset.planrCopyLabel ?? label.textContent;
|
|
586
|
+
else button.textContent = button.dataset.planrCopyLabel ?? button.textContent;
|
|
587
|
+
}
|
|
588
|
+
function showCopyState(button, stateValue) {
|
|
589
|
+
if (!button) return;
|
|
590
|
+
const label = button.querySelector?.(".planr-action-label");
|
|
591
|
+
if (!button.dataset.planrCopyLabel) {
|
|
592
|
+
button.dataset.planrCopyLabel = (label?.textContent ?? button.textContent).trim();
|
|
593
|
+
}
|
|
594
|
+
const prior = copyResetTimers.get(button);
|
|
595
|
+
if (prior) window.clearTimeout(prior);
|
|
596
|
+
button.dataset.planrCopyState = stateValue;
|
|
597
|
+
if (label) label.textContent = stateValue === "copied" ? "Copied" : "Try again";
|
|
598
|
+
else button.textContent = stateValue === "copied" ? "Copied" : "Try again";
|
|
599
|
+
copyResetTimers.set(
|
|
600
|
+
button,
|
|
601
|
+
window.setTimeout(() => resetCopyButton(button), 1800)
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
function resetCopyButtons() {
|
|
605
|
+
for (const button of dialog.querySelectorAll("[data-planr-copy-state]"))
|
|
606
|
+
resetCopyButton(button);
|
|
607
|
+
}
|
|
608
|
+
function clearPendingOwnerSigner() {
|
|
609
|
+
pendingOwnerCustody = null;
|
|
610
|
+
}
|
|
611
|
+
function pendingOwnerSigner() {
|
|
612
|
+
return pendingOwnerCustody?.ownerSigner ?? pendingOwnerCustody;
|
|
613
|
+
}
|
|
614
|
+
async function copyExistingRoom() {
|
|
615
|
+
const value = stableShareUrl();
|
|
616
|
+
if (!value) return;
|
|
617
|
+
try {
|
|
618
|
+
await handlers.copyText(value);
|
|
619
|
+
showCopyState(trigger, "copied");
|
|
620
|
+
announce2("Live review URL copied. This remains the same collaboration room.");
|
|
621
|
+
} catch (error) {
|
|
622
|
+
showCopyState(trigger, "error");
|
|
623
|
+
announce2(error?.message ?? "Review URL could not be copied.");
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function render() {
|
|
627
|
+
const preview = state.preview;
|
|
628
|
+
backdrop.hidden = !state.open;
|
|
629
|
+
backdrop.style.pointerEvents = state.open ? "auto" : "";
|
|
630
|
+
root.toggleAttribute("inert", state.open);
|
|
631
|
+
root.setAttribute("aria-hidden", String(state.open));
|
|
632
|
+
if (!state.open) root.removeAttribute("aria-hidden");
|
|
633
|
+
dialog.dataset.planrSharePhase = state.phase;
|
|
634
|
+
dialog.dataset.planrShareSelected = state.transport;
|
|
635
|
+
for (const button of dialog.querySelectorAll("[data-planr-share-transport]")) {
|
|
636
|
+
const transport = button.dataset.planrShareTransport;
|
|
637
|
+
const selected = transport === state.transport;
|
|
638
|
+
button.hidden = state.phase === "created";
|
|
639
|
+
button.setAttribute("aria-pressed", String(selected));
|
|
640
|
+
button.classList.toggle("is-selected", selected);
|
|
641
|
+
button.disabled = [
|
|
642
|
+
"previewing",
|
|
643
|
+
"custody-preparing",
|
|
644
|
+
"custody-ready",
|
|
645
|
+
"creating",
|
|
646
|
+
"ambiguous",
|
|
647
|
+
"created"
|
|
648
|
+
].includes(state.phase) || !supports(transport);
|
|
649
|
+
button.setAttribute("aria-disabled", String(button.disabled));
|
|
650
|
+
button.title = capabilities.includes(transport) ? "" : transport === "live" && capabilities.length ? "Live review requires this host to provide private owner-key custody. Choose an available snapshot option." : unavailable;
|
|
651
|
+
if (transport === "live")
|
|
652
|
+
button.querySelector(".planr-share-receipt-size").textContent = capabilities.includes(
|
|
653
|
+
"live"
|
|
654
|
+
) ? "Default" : "Unavailable";
|
|
655
|
+
}
|
|
656
|
+
const fragmentSize = dialog.querySelector("[data-planr-share-fragment-size]");
|
|
657
|
+
if (fragmentSize)
|
|
658
|
+
fragmentSize.textContent = !capabilities.includes("fragment") ? "Unavailable" : preview ? `${preview.fragmentLength.toLocaleString("en-US")} chars · ${formatArtifactShareBytes(preview.compressedBytes)}` : state.phase === "previewing" ? "Calculating…" : "Size unavailable";
|
|
659
|
+
const shortSize = dialog.querySelector("[data-planr-share-short-size]");
|
|
660
|
+
if (shortSize)
|
|
661
|
+
shortSize.textContent = !capabilities.includes("short") ? "Unavailable" : preview ? formatArtifactShareBytes(preview.ciphertextBytes) : state.phase === "previewing" ? "Calculating…" : "Size unavailable";
|
|
662
|
+
const threshold = dialog.querySelector("[data-planr-share-threshold]");
|
|
663
|
+
if (threshold) {
|
|
664
|
+
threshold.hidden = !capabilities.includes("fragment");
|
|
665
|
+
threshold.textContent = preview?.fragmentEligible === false ? `Private fragment snapshot unavailable (${preview.fragmentLength.toLocaleString("en-US")} characters; 8,000 limit). Choose an available sharing option.` : preview ? "Private fragment snapshot available for links up to 8,000 characters." : "Preparing the snapshot size before sharing.";
|
|
666
|
+
}
|
|
667
|
+
const ttlRow = dialog.querySelector("[data-planr-share-ttl-row]");
|
|
668
|
+
if (ttlRow)
|
|
669
|
+
ttlRow.hidden = state.phase === "created" || !supports(state.transport) || !["live", "short"].includes(state.transport);
|
|
670
|
+
const ttlSelect2 = dialog.querySelector("[data-planr-share-ttl]");
|
|
671
|
+
if (ttlSelect2) {
|
|
672
|
+
ttlSelect2.value = state.ttl;
|
|
673
|
+
ttlSelect2.disabled = [
|
|
674
|
+
"custody-preparing",
|
|
675
|
+
"custody-ready",
|
|
676
|
+
"creating",
|
|
677
|
+
"ambiguous",
|
|
678
|
+
"created"
|
|
679
|
+
].includes(state.phase);
|
|
680
|
+
}
|
|
681
|
+
const expiry = artifactShareExpiry(state.ttl, now());
|
|
682
|
+
const expiryNode = dialog.querySelector("[data-planr-share-expiry]");
|
|
683
|
+
if (expiryNode) {
|
|
684
|
+
expiryNode.dateTime = expiry;
|
|
685
|
+
expiryNode.textContent = new Intl.DateTimeFormat("en", {
|
|
686
|
+
year: "numeric",
|
|
687
|
+
month: "short",
|
|
688
|
+
day: "numeric",
|
|
689
|
+
timeZone: "UTC"
|
|
690
|
+
}).format(new Date(expiry));
|
|
691
|
+
}
|
|
692
|
+
const primary = dialog.querySelector("[data-planr-share-confirm]");
|
|
693
|
+
if (primary) {
|
|
694
|
+
primary.hidden = state.phase === "created";
|
|
695
|
+
primary.disabled = !supports(state.transport) || !preview && state.phase !== "error" || state.phase === "previewing" || state.phase === "custody-preparing" || state.phase === "creating" || state.phase === "created";
|
|
696
|
+
primary.textContent = !supports(state.transport) ? "Sharing unavailable" : state.phase === "error" && !preview ? "Retry preparation" : state.phase === "custody-preparing" ? "Preparing owner key…" : state.phase === "creating" ? "Creating…" : state.phase === "ambiguous" ? "Retry exact room creation" : state.transport === "live" ? state.ownerCustodyEstablished ? state.phase === "error" ? "Retry live review room" : "I saved it — create live room" : "Download recovery bundle" : state.transport === "short" ? "Create encrypted link" : "Copy private link";
|
|
697
|
+
}
|
|
698
|
+
for (const closeControl of dialog.querySelectorAll(
|
|
699
|
+
"[data-planr-share-close], [data-planr-share-cancel]"
|
|
700
|
+
)) {
|
|
701
|
+
const closeBlocked = state.phase === "creating" || state.phase === "ambiguous";
|
|
702
|
+
closeControl.disabled = closeBlocked;
|
|
703
|
+
closeControl.setAttribute("aria-disabled", String(closeBlocked));
|
|
704
|
+
}
|
|
705
|
+
const custody = dialog.querySelector("[data-planr-share-owner-custody]");
|
|
706
|
+
if (custody)
|
|
707
|
+
custody.hidden = state.transport !== "live" || !supports("live") || state.phase === "created";
|
|
708
|
+
const custodyStatus = dialog.querySelector("[data-planr-share-owner-custody-status]");
|
|
709
|
+
if (custodyStatus)
|
|
710
|
+
custodyStatus.textContent = state.ownerCustodyEstablished ? "Full recovery bundle handed to the browser. It contains the three scoped URLs and owner key; no room exists until you confirm creation." : "No room will be created until the full private recovery bundle is downloaded successfully.";
|
|
711
|
+
const receipt2 = dialog.querySelector("[data-planr-share-result]");
|
|
712
|
+
if (receipt2) receipt2.hidden = state.phase !== "created" || !state.result;
|
|
713
|
+
const resultUrl = dialog.querySelector("[data-planr-share-url]");
|
|
714
|
+
if (resultUrl) resultUrl.value = state.result?.url ?? "";
|
|
715
|
+
const owner = dialog.querySelector("[data-planr-share-owner]");
|
|
716
|
+
if (owner) owner.hidden = !state.result?.ownerUrl;
|
|
717
|
+
const ownerUrl = dialog.querySelector("[data-planr-share-owner-url]");
|
|
718
|
+
if (ownerUrl) ownerUrl.value = state.result?.ownerUrl ?? "";
|
|
719
|
+
const manage = dialog.querySelector("[data-planr-share-manage]");
|
|
720
|
+
if (manage) manage.hidden = !state.result?.manageUrl;
|
|
721
|
+
const manageUrl = dialog.querySelector("[data-planr-share-manage-url]");
|
|
722
|
+
if (manageUrl) manageUrl.value = state.result?.manageUrl ?? "";
|
|
723
|
+
const deletion = dialog.querySelector("[data-planr-share-deletion]");
|
|
724
|
+
if (deletion) deletion.hidden = !state.result?.deletionToken;
|
|
725
|
+
const deletionToken = dialog.querySelector("[data-planr-share-deletion-token]");
|
|
726
|
+
if (deletionToken) deletionToken.textContent = state.result?.deletionToken ?? "";
|
|
727
|
+
const error = dialog.querySelector("[data-planr-share-error]");
|
|
728
|
+
if (error) {
|
|
729
|
+
error.hidden = !state.error;
|
|
730
|
+
error.textContent = state.error;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
async function open() {
|
|
734
|
+
clearPendingOwnerSigner();
|
|
735
|
+
resetCopyButtons();
|
|
736
|
+
if (!state.open)
|
|
737
|
+
returnFocus = document2.activeElement instanceof window.HTMLElement ? document2.activeElement : trigger;
|
|
738
|
+
state = reduceArtifactShareDialog(state, { type: "open" });
|
|
739
|
+
state = reduceArtifactShareDialog(state, { type: "preview-start" });
|
|
740
|
+
selectSupportedTransport();
|
|
741
|
+
state = reduceArtifactShareDialog(state, { type: "preview-start" });
|
|
742
|
+
render();
|
|
743
|
+
dialog.querySelector("[data-planr-share-close]")?.focus();
|
|
744
|
+
const request = ++generation;
|
|
745
|
+
try {
|
|
746
|
+
if (!capabilities.length)
|
|
747
|
+
throw new ArtifactShareUiError("E_ARTIFACT_SHARE_HANDLER_REQUIRED", unavailable);
|
|
748
|
+
const preview = await handlers.prepareShare(
|
|
749
|
+
Object.freeze({
|
|
750
|
+
review: reviewForShare(stageController),
|
|
751
|
+
fragmentLimit: ARTIFACT_SHARE_FRAGMENT_LIMIT
|
|
752
|
+
})
|
|
753
|
+
);
|
|
754
|
+
if (request !== generation || !state.open) return state;
|
|
755
|
+
state = reduceArtifactShareDialog(state, { type: "preview-ready", preview });
|
|
756
|
+
selectSupportedTransport();
|
|
757
|
+
if (!supports(state.transport))
|
|
758
|
+
throw new ArtifactShareUiError(
|
|
759
|
+
"E_ARTIFACT_SHARE_TRANSPORT_UNAVAILABLE",
|
|
760
|
+
"This review is too large for the sharing options supported by this viewer. Open it with an updated OpenPlanr installation or export the review."
|
|
761
|
+
);
|
|
762
|
+
render();
|
|
763
|
+
announce2(
|
|
764
|
+
capabilities.includes("fragment") && state.preview.fragmentEligible ? "Private fragment is available. Nothing will be uploaded." : "Review prepared. Choose an available sharing option."
|
|
765
|
+
);
|
|
766
|
+
} catch (error) {
|
|
767
|
+
if (request !== generation || !state.open) return state;
|
|
768
|
+
state = reduceArtifactShareDialog(state, { type: "failure", error: error?.message });
|
|
769
|
+
render();
|
|
770
|
+
announce2(state.error);
|
|
771
|
+
}
|
|
772
|
+
return state;
|
|
773
|
+
}
|
|
774
|
+
function close() {
|
|
775
|
+
if (state.phase === "creating" || state.phase === "ambiguous") {
|
|
776
|
+
announce2(
|
|
777
|
+
state.phase === "ambiguous" ? "Room creation may have completed. Retry the exact attempt until its receipt is shown." : "Room creation is in progress. Keep this dialog open until its receipt is shown."
|
|
778
|
+
);
|
|
779
|
+
return state;
|
|
780
|
+
}
|
|
781
|
+
generation += 1;
|
|
782
|
+
clearPendingOwnerSigner();
|
|
783
|
+
state = reduceArtifactShareDialog(state, { type: "close" });
|
|
784
|
+
resetCopyButtons();
|
|
785
|
+
render();
|
|
786
|
+
returnFocus?.focus?.();
|
|
787
|
+
returnFocus = null;
|
|
788
|
+
return state;
|
|
789
|
+
}
|
|
790
|
+
async function confirm() {
|
|
791
|
+
if (!state.open || !supports(state.transport)) return state;
|
|
792
|
+
if (!state.preview && state.phase === "error") return open();
|
|
793
|
+
if (!state.preview || ["custody-preparing", "creating", "created"].includes(state.phase))
|
|
794
|
+
return state;
|
|
795
|
+
const transport = state.transport;
|
|
796
|
+
const request = generation;
|
|
797
|
+
if (transport === "live" && !state.ownerCustodyEstablished) {
|
|
798
|
+
state = reduceArtifactShareDialog(state, { type: "custody-start" });
|
|
799
|
+
render();
|
|
800
|
+
try {
|
|
801
|
+
const custody = await establishArtifactOwnerCustody({
|
|
802
|
+
prepareOwnerCustody: () => handlers.prepareOwnerCustody(
|
|
803
|
+
Object.freeze({
|
|
804
|
+
review: reviewForShare(stageController),
|
|
805
|
+
ttl: state.ttl
|
|
806
|
+
})
|
|
807
|
+
),
|
|
808
|
+
saveOwnerCustody: handlers.saveOwnerCustody
|
|
809
|
+
});
|
|
810
|
+
if (request !== generation || !state.open) return state;
|
|
811
|
+
pendingOwnerCustody = custody;
|
|
812
|
+
state = reduceArtifactShareDialog(state, { type: "custody-ready" });
|
|
813
|
+
render();
|
|
814
|
+
announce2(
|
|
815
|
+
"Private recovery-bundle download started. Verify the file is saved, then confirm room creation."
|
|
816
|
+
);
|
|
817
|
+
} catch (error) {
|
|
818
|
+
if (request !== generation || !state.open) return state;
|
|
819
|
+
clearPendingOwnerSigner();
|
|
820
|
+
state = reduceArtifactShareDialog(state, {
|
|
821
|
+
type: "failure",
|
|
822
|
+
ownerCustodyEstablished: false,
|
|
823
|
+
error: error?.message
|
|
824
|
+
});
|
|
825
|
+
render();
|
|
826
|
+
announce2(state.error);
|
|
827
|
+
}
|
|
828
|
+
return state;
|
|
829
|
+
}
|
|
830
|
+
if (transport === "live" && !pendingOwnerCustody) {
|
|
831
|
+
state = reduceArtifactShareDialog(state, {
|
|
832
|
+
type: "failure",
|
|
833
|
+
ownerCustodyEstablished: false,
|
|
834
|
+
error: "The prepared recovery attempt is no longer available. Download a new bundle before creating a room."
|
|
835
|
+
});
|
|
836
|
+
render();
|
|
837
|
+
announce2(state.error);
|
|
838
|
+
return state;
|
|
839
|
+
}
|
|
840
|
+
state = reduceArtifactShareDialog(state, { type: "create-start" });
|
|
841
|
+
render();
|
|
842
|
+
try {
|
|
843
|
+
const input = {
|
|
844
|
+
review: reviewForShare(stageController),
|
|
845
|
+
preview: state.preview,
|
|
846
|
+
transport,
|
|
847
|
+
ttl: ["live", "short"].includes(transport) ? state.ttl : void 0,
|
|
848
|
+
confirmed: ["live", "short"].includes(transport)
|
|
849
|
+
};
|
|
850
|
+
if (transport === "live") {
|
|
851
|
+
if (pendingOwnerCustody?.kind === "openplanr-live-room-preparation") {
|
|
852
|
+
Object.defineProperty(input, "prepared", {
|
|
853
|
+
enumerable: false,
|
|
854
|
+
value: pendingOwnerCustody
|
|
855
|
+
});
|
|
856
|
+
} else {
|
|
857
|
+
Object.defineProperty(input, "ownerSigner", {
|
|
858
|
+
enumerable: false,
|
|
859
|
+
value: pendingOwnerCustody
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
const result = await handlers.createShare(Object.freeze(input));
|
|
864
|
+
if (request !== generation || !state.open) return state;
|
|
865
|
+
if (transport === "live" && result?.ownerSigner !== pendingOwnerSigner()) {
|
|
866
|
+
throw new ArtifactShareUiError(
|
|
867
|
+
"E_ARTIFACT_SHARE_OWNER_CUSTODY_MISMATCH",
|
|
868
|
+
"The created room did not bind the prepared owner key. Retry the exact prepared attempt.",
|
|
869
|
+
{ effect: "ambiguous" }
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
state = reduceArtifactShareDialog(state, {
|
|
873
|
+
type: "create-success",
|
|
874
|
+
result: { ...result, transport }
|
|
875
|
+
});
|
|
876
|
+
clearPendingOwnerSigner();
|
|
877
|
+
render();
|
|
878
|
+
announce2(
|
|
879
|
+
transport === "live" ? "Live room created. Keep the downloaded owner key with the separate owner-verdict URL; management cannot set a verdict." : transport === "short" ? "Encrypted short link copied. Store the one-time deletion token now." : "Private fragment copied. Nothing was uploaded."
|
|
880
|
+
);
|
|
881
|
+
try {
|
|
882
|
+
await handlers.copyText(state.result.url);
|
|
883
|
+
announce2(
|
|
884
|
+
transport === "live" ? "Live review URL copied. Keep the downloaded owner key with the separate owner-verdict URL." : transport === "short" ? "Encrypted short link copied. Store the one-time deletion token now." : "Private fragment copied. Nothing was uploaded."
|
|
885
|
+
);
|
|
886
|
+
} catch {
|
|
887
|
+
announce2(
|
|
888
|
+
transport === "live" ? "Live room created. Copy the review and owner-verdict URLs manually; the room receipt remains visible." : "Share created. Copy the URL manually; the receipt remains visible."
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
} catch (error) {
|
|
892
|
+
if (request !== generation || !state.open) return state;
|
|
893
|
+
const ambiguous = transport === "live" && error?.details?.effect === "ambiguous";
|
|
894
|
+
if (transport === "live" && !ambiguous) clearPendingOwnerSigner();
|
|
895
|
+
state = reduceArtifactShareDialog(state, {
|
|
896
|
+
type: "failure",
|
|
897
|
+
ambiguous,
|
|
898
|
+
ownerCustodyEstablished: transport === "live" && Boolean(pendingOwnerCustody),
|
|
899
|
+
error: error?.message
|
|
900
|
+
});
|
|
901
|
+
render();
|
|
902
|
+
announce2(state.error);
|
|
903
|
+
}
|
|
904
|
+
return state;
|
|
905
|
+
}
|
|
906
|
+
async function copy(value, successMessage, button) {
|
|
907
|
+
if (!value) return;
|
|
908
|
+
try {
|
|
909
|
+
await handlers.copyText(value);
|
|
910
|
+
showCopyState(button, "copied");
|
|
911
|
+
announce2(successMessage);
|
|
912
|
+
} catch (error) {
|
|
913
|
+
showCopyState(button, "error");
|
|
914
|
+
announce2(
|
|
915
|
+
error?.message ?? "The value could not be copied. The share receipt remains visible."
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
if (existingRoom) {
|
|
920
|
+
const value = stableShareUrl();
|
|
921
|
+
if (value) {
|
|
922
|
+
const label = trigger.querySelector(".planr-action-label");
|
|
923
|
+
if (label) label.textContent = "Copy link";
|
|
924
|
+
else trigger.textContent = "Copy link";
|
|
925
|
+
trigger.dataset.planrCopyLabel = "Copy link";
|
|
926
|
+
trigger.dataset.planrTooltip = "Copy link";
|
|
927
|
+
trigger.removeAttribute("aria-haspopup");
|
|
928
|
+
trigger.setAttribute("aria-label", "Copy this live review room link");
|
|
929
|
+
listen(trigger, "click", () => {
|
|
930
|
+
void copyExistingRoom();
|
|
931
|
+
});
|
|
932
|
+
} else {
|
|
933
|
+
trigger.hidden = true;
|
|
934
|
+
}
|
|
935
|
+
} else {
|
|
936
|
+
listen(trigger, "click", open);
|
|
937
|
+
}
|
|
938
|
+
listen(backdrop, "click", (event) => {
|
|
939
|
+
const button = event.target.closest?.("button");
|
|
940
|
+
if (!button) return;
|
|
941
|
+
if (button.dataset.planrShareClose !== void 0 || button.dataset.planrShareCancel !== void 0) {
|
|
942
|
+
close();
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
if (button.dataset.planrShareTransport) {
|
|
946
|
+
if (!supports(button.dataset.planrShareTransport)) return;
|
|
947
|
+
if (["custody-ready", "creating", "ambiguous", "created"].includes(state.phase)) return;
|
|
948
|
+
if (button.dataset.planrShareTransport !== state.transport) clearPendingOwnerSigner();
|
|
949
|
+
state = reduceArtifactShareDialog(state, {
|
|
950
|
+
type: "select-transport",
|
|
951
|
+
transport: button.dataset.planrShareTransport
|
|
952
|
+
});
|
|
953
|
+
render();
|
|
954
|
+
announce2(
|
|
955
|
+
state.transport === "live" ? "Live encrypted review selected. Anyone with this link can comment." : state.transport === "short" ? "Encrypted short link selected. Creation requires confirmation." : "Private fragment selected. Nothing will be uploaded."
|
|
956
|
+
);
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
if (button.dataset.planrShareConfirm !== void 0) {
|
|
960
|
+
void confirm();
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
if (button.dataset.planrShareCopyUrl !== void 0) {
|
|
964
|
+
void copy(state.result?.url, "Review URL copied.", button);
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
if (button.dataset.planrShareCopyOwner !== void 0) {
|
|
968
|
+
void copy(
|
|
969
|
+
state.result?.ownerUrl,
|
|
970
|
+
"Private owner-verdict URL copied. It requires the matching downloaded owner key.",
|
|
971
|
+
button
|
|
972
|
+
);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if (button.dataset.planrShareCopyManage !== void 0) {
|
|
976
|
+
void copy(
|
|
977
|
+
state.result?.manageUrl,
|
|
978
|
+
"Private management URL copied. It can pause, reopen, or delete the room, but cannot set a verdict.",
|
|
979
|
+
button
|
|
980
|
+
);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
if (button.dataset.planrShareCopyDeletion !== void 0) {
|
|
984
|
+
void copy(state.result?.deletionToken, "One-time deletion token copied.", button);
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
const ttlSelect = dialog.querySelector("[data-planr-share-ttl]");
|
|
988
|
+
if (ttlSelect)
|
|
989
|
+
listen(ttlSelect, "change", () => {
|
|
990
|
+
if (["custody-ready", "creating", "ambiguous", "created"].includes(state.phase)) {
|
|
991
|
+
ttlSelect.value = state.ttl;
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
state = reduceArtifactShareDialog(state, { type: "set-ttl", ttl: ttlSelect.value });
|
|
995
|
+
render();
|
|
996
|
+
announce2(`Expiry set to ${ARTIFACT_SHARE_TTLS2[state.ttl].label}.`);
|
|
997
|
+
});
|
|
998
|
+
listen(
|
|
999
|
+
document2,
|
|
1000
|
+
"keydown",
|
|
1001
|
+
(event) => {
|
|
1002
|
+
if (!state.open) return;
|
|
1003
|
+
if (event.key === "Escape") {
|
|
1004
|
+
event.preventDefault();
|
|
1005
|
+
close();
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
if (event.key !== "Tab") return;
|
|
1009
|
+
const focusable = focusableElements(dialog);
|
|
1010
|
+
if (focusable.length === 0) return;
|
|
1011
|
+
const first = focusable[0];
|
|
1012
|
+
const last = focusable.at(-1);
|
|
1013
|
+
if (event.shiftKey && document2.activeElement === first) {
|
|
1014
|
+
event.preventDefault();
|
|
1015
|
+
last.focus();
|
|
1016
|
+
} else if (!event.shiftKey && document2.activeElement === last) {
|
|
1017
|
+
event.preventDefault();
|
|
1018
|
+
first.focus();
|
|
1019
|
+
}
|
|
1020
|
+
},
|
|
1021
|
+
true
|
|
1022
|
+
);
|
|
1023
|
+
render();
|
|
1024
|
+
const controller = Object.freeze({
|
|
1025
|
+
getState: () => state,
|
|
1026
|
+
open,
|
|
1027
|
+
close,
|
|
1028
|
+
confirm,
|
|
1029
|
+
dispatch(action) {
|
|
1030
|
+
if (action.type === "select-transport" && !supports(action.transport)) return state;
|
|
1031
|
+
state = reduceArtifactShareDialog(state, action);
|
|
1032
|
+
render();
|
|
1033
|
+
return state;
|
|
1034
|
+
},
|
|
1035
|
+
destroy() {
|
|
1036
|
+
if (state.phase === "creating" || state.phase === "ambiguous") {
|
|
1037
|
+
announce2(
|
|
1038
|
+
state.phase === "ambiguous" ? "Room creation may have completed. Retry the exact attempt until its receipt is shown." : "Room creation is in progress. Keep this review open until its receipt is shown."
|
|
1039
|
+
);
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1042
|
+
generation += 1;
|
|
1043
|
+
clearPendingOwnerSigner();
|
|
1044
|
+
for (const timer of copyResetTimers.values()) window.clearTimeout(timer);
|
|
1045
|
+
copyResetTimers.clear();
|
|
1046
|
+
for (const remove of cleanup.splice(0)) remove();
|
|
1047
|
+
if (state.open) {
|
|
1048
|
+
state = reduceArtifactShareDialog(state, { type: "close" });
|
|
1049
|
+
render();
|
|
1050
|
+
}
|
|
1051
|
+
return true;
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
1054
|
+
window.__openPlanrArtifactShare = controller;
|
|
1055
|
+
return controller;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// lib/artifact/ui/hosted-viewer.mjs
|
|
1059
|
+
var HOSTED_ARTIFACT_VIEWER_STATES = Object.freeze([
|
|
1060
|
+
"idle",
|
|
1061
|
+
"empty-hash",
|
|
1062
|
+
"loading",
|
|
1063
|
+
"ready",
|
|
1064
|
+
"invalid-version",
|
|
1065
|
+
"malformed-payload",
|
|
1066
|
+
"too-large",
|
|
1067
|
+
"paste-missing",
|
|
1068
|
+
"expired",
|
|
1069
|
+
"decryption-failed",
|
|
1070
|
+
"unsupported-browser",
|
|
1071
|
+
"network-error",
|
|
1072
|
+
"room-closed"
|
|
1073
|
+
]);
|
|
1074
|
+
var HOSTED_ARTIFACT_STATE_COPY = Object.freeze({
|
|
1075
|
+
"empty-hash": Object.freeze({
|
|
1076
|
+
title: "Open a private review link",
|
|
1077
|
+
detail: "This page needs a complete OpenPlanr fragment or encrypted short-link URL.",
|
|
1078
|
+
action: ""
|
|
1079
|
+
}),
|
|
1080
|
+
loading: Object.freeze({
|
|
1081
|
+
title: "Loading private review",
|
|
1082
|
+
detail: "Validating the immutable payload before opening the artifact.",
|
|
1083
|
+
action: ""
|
|
1084
|
+
}),
|
|
1085
|
+
"invalid-version": Object.freeze({
|
|
1086
|
+
title: "This review version is not supported",
|
|
1087
|
+
detail: "Ask the sender to create a new link with a compatible OpenPlanr release.",
|
|
1088
|
+
action: ""
|
|
1089
|
+
}),
|
|
1090
|
+
"malformed-payload": Object.freeze({
|
|
1091
|
+
title: "This review link is incomplete",
|
|
1092
|
+
detail: "Copy the complete URL again, including everything after the # character.",
|
|
1093
|
+
action: ""
|
|
1094
|
+
}),
|
|
1095
|
+
"too-large": Object.freeze({
|
|
1096
|
+
title: "This private fragment is too large",
|
|
1097
|
+
detail: "Ask the sender to create an encrypted expiring short link instead.",
|
|
1098
|
+
action: ""
|
|
1099
|
+
}),
|
|
1100
|
+
"paste-missing": Object.freeze({
|
|
1101
|
+
title: "This encrypted review is unavailable",
|
|
1102
|
+
detail: "It may have been deleted. Ask the sender for a new immutable review link.",
|
|
1103
|
+
action: ""
|
|
1104
|
+
}),
|
|
1105
|
+
expired: Object.freeze({
|
|
1106
|
+
title: "This encrypted review expired",
|
|
1107
|
+
detail: "Ask the sender for a new immutable review link.",
|
|
1108
|
+
action: ""
|
|
1109
|
+
}),
|
|
1110
|
+
"decryption-failed": Object.freeze({
|
|
1111
|
+
title: "This key cannot decrypt the review",
|
|
1112
|
+
detail: "Use the complete link, including its private fragment key. The payload may also have been changed.",
|
|
1113
|
+
action: ""
|
|
1114
|
+
}),
|
|
1115
|
+
"unsupported-browser": Object.freeze({
|
|
1116
|
+
title: "Browser support is required",
|
|
1117
|
+
detail: "Use a current browser with raw DEFLATE and Web Crypto support.",
|
|
1118
|
+
action: ""
|
|
1119
|
+
}),
|
|
1120
|
+
"network-error": Object.freeze({
|
|
1121
|
+
title: "The encrypted review could not be loaded",
|
|
1122
|
+
detail: "Your link remains unchanged. Check the connection and try again safely.",
|
|
1123
|
+
action: "Try again"
|
|
1124
|
+
}),
|
|
1125
|
+
"room-closed": Object.freeze({
|
|
1126
|
+
title: "Comments are paused",
|
|
1127
|
+
detail: "This review remains available to read, but the owner has paused new feedback.",
|
|
1128
|
+
action: ""
|
|
1129
|
+
})
|
|
1130
|
+
});
|
|
1131
|
+
var HostedArtifactViewerError = class extends Error {
|
|
1132
|
+
constructor(code, message, details = {}) {
|
|
1133
|
+
super(message);
|
|
1134
|
+
this.name = "HostedArtifactViewerError";
|
|
1135
|
+
this.code = code;
|
|
1136
|
+
this.details = Object.freeze({ ...details });
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
function freezeState2(value) {
|
|
1140
|
+
const status = HOSTED_ARTIFACT_VIEWER_STATES.includes(value.status) ? value.status : "idle";
|
|
1141
|
+
return Object.freeze({
|
|
1142
|
+
status,
|
|
1143
|
+
transport: ["fragment", "short", "room"].includes(value.transport) ? value.transport : null,
|
|
1144
|
+
request: value.request ? Object.freeze({ ...value.request }) : null,
|
|
1145
|
+
envelope: value.envelope ?? null,
|
|
1146
|
+
retryable: status === "network-error"
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
function locationParts(location) {
|
|
1150
|
+
if (typeof location === "string") {
|
|
1151
|
+
const parsed = new URL(location, "https://share.openplanr.dev/");
|
|
1152
|
+
return { pathname: parsed.pathname, hash: parsed.hash };
|
|
1153
|
+
}
|
|
1154
|
+
return {
|
|
1155
|
+
pathname: typeof location?.pathname === "string" ? location.pathname : "/",
|
|
1156
|
+
hash: typeof location?.hash === "string" ? location.hash : ""
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
function malformed(status, details = {}) {
|
|
1160
|
+
return Object.freeze({ ok: false, status, details: Object.freeze(details) });
|
|
1161
|
+
}
|
|
1162
|
+
function parseHostedArtifactLocation(location, { fragmentLimit = ARTIFACT_SHARE_FRAGMENT_LIMIT } = {}) {
|
|
1163
|
+
const { pathname, hash } = locationParts(location);
|
|
1164
|
+
const shortMatch = pathname.match(/^\/p\/([A-Za-z0-9_-]{1,128})\/?$/);
|
|
1165
|
+
if (shortMatch) {
|
|
1166
|
+
if (!hash.startsWith("#k=")) return malformed("malformed-payload", { transport: "short" });
|
|
1167
|
+
const key = hash.slice(3);
|
|
1168
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(key)) {
|
|
1169
|
+
return malformed("malformed-payload", { transport: "short" });
|
|
1170
|
+
}
|
|
1171
|
+
return Object.freeze({
|
|
1172
|
+
ok: true,
|
|
1173
|
+
transport: "short",
|
|
1174
|
+
id: shortMatch[1],
|
|
1175
|
+
key
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
const roomMatch = pathname.match(/^\/r\/([A-Za-z0-9_-]{16,128})\/?$/);
|
|
1179
|
+
if (roomMatch) {
|
|
1180
|
+
const params = new URLSearchParams(hash.slice(1));
|
|
1181
|
+
const key = params.get("k");
|
|
1182
|
+
const readCapability = params.get("r");
|
|
1183
|
+
const write = params.get("w");
|
|
1184
|
+
const owner = params.get("o");
|
|
1185
|
+
const manage = params.get("m");
|
|
1186
|
+
const authority = [write, owner, manage].filter(Boolean);
|
|
1187
|
+
if (!key || !/^[A-Za-z0-9_-]{43}$/.test(key) || authority.length > 1 || readCapability !== null && (!/^[A-Za-z0-9_-]{43}$/.test(readCapability) || params.size !== 2 + authority.length || [...params.keys()].some((name) => !["k", "r", "w", "o", "m"].includes(name)) || new Set(params.keys()).size !== params.size) || write && !/^[A-Za-z0-9_-]{43}$/.test(write) || owner && !/^[A-Za-z0-9_-]{43}$/.test(owner) || manage && !/^[A-Za-z0-9_-]{43}$/.test(manage)) {
|
|
1188
|
+
return malformed("malformed-payload", { transport: "room" });
|
|
1189
|
+
}
|
|
1190
|
+
return Object.freeze({
|
|
1191
|
+
ok: true,
|
|
1192
|
+
transport: "room",
|
|
1193
|
+
id: roomMatch[1],
|
|
1194
|
+
key,
|
|
1195
|
+
...readCapability ? { readCapability } : {},
|
|
1196
|
+
...write ? { write } : {},
|
|
1197
|
+
...owner ? { owner } : {},
|
|
1198
|
+
...manage ? { manage } : {}
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
if (!hash || hash === "#") return malformed("empty-hash");
|
|
1202
|
+
const fragment = hash.slice(1);
|
|
1203
|
+
if (fragment.length > fragmentLimit) {
|
|
1204
|
+
return malformed("too-large", { fragmentLength: fragment.length, fragmentLimit });
|
|
1205
|
+
}
|
|
1206
|
+
if (!fragment.startsWith("v1.")) {
|
|
1207
|
+
return malformed(/^v\d+\./.test(fragment) ? "invalid-version" : "malformed-payload");
|
|
1208
|
+
}
|
|
1209
|
+
const payload = fragment.slice(3);
|
|
1210
|
+
if (!payload || !/^[A-Za-z0-9_-]+$/.test(payload)) return malformed("malformed-payload");
|
|
1211
|
+
return Object.freeze({ ok: true, transport: "fragment", version: "v1", payload });
|
|
1212
|
+
}
|
|
1213
|
+
function hostedArtifactStateForError(error) {
|
|
1214
|
+
const code = typeof error?.code === "string" ? error.code : "";
|
|
1215
|
+
if (["E_ARTIFACT_BROWSER_UNSUPPORTED", "E_ARTIFACT_CODEC_UNSUPPORTED"].includes(code)) {
|
|
1216
|
+
return "unsupported-browser";
|
|
1217
|
+
}
|
|
1218
|
+
if ([
|
|
1219
|
+
"E_ARTIFACT_FRAGMENT_TOO_LARGE",
|
|
1220
|
+
"E_ARTIFACT_PAYLOAD_TOO_LARGE",
|
|
1221
|
+
"E_ARTIFACT_DECOMPRESSION_LIMIT"
|
|
1222
|
+
].includes(code)) {
|
|
1223
|
+
return "too-large";
|
|
1224
|
+
}
|
|
1225
|
+
if ([
|
|
1226
|
+
"E_ARTIFACT_PASTE_NOT_FOUND",
|
|
1227
|
+
"E_ARTIFACT_SHARE_NOT_FOUND",
|
|
1228
|
+
"E_ARTIFACT_PASTE_UNAVAILABLE"
|
|
1229
|
+
].includes(code)) {
|
|
1230
|
+
return "paste-missing";
|
|
1231
|
+
}
|
|
1232
|
+
if (["E_ARTIFACT_PASTE_EXPIRED", "E_ARTIFACT_SHARE_EXPIRED"].includes(code)) {
|
|
1233
|
+
return "expired";
|
|
1234
|
+
}
|
|
1235
|
+
if ([
|
|
1236
|
+
"E_ARTIFACT_DECRYPTION_FAILED",
|
|
1237
|
+
"E_ARTIFACT_AUTH_FAILED",
|
|
1238
|
+
"E_ARTIFACT_PAYLOAD_TAMPERED",
|
|
1239
|
+
"OperationError"
|
|
1240
|
+
].includes(code)) {
|
|
1241
|
+
return "decryption-failed";
|
|
1242
|
+
}
|
|
1243
|
+
if (["E_ARTIFACT_SHARE_NETWORK", "E_ARTIFACT_NETWORK", "E_ARTIFACT_FETCH_FAILED"].includes(code) || error?.name === "TypeError") {
|
|
1244
|
+
return "network-error";
|
|
1245
|
+
}
|
|
1246
|
+
if ([
|
|
1247
|
+
"E_ARTIFACT_VERSION_UNSUPPORTED",
|
|
1248
|
+
"E_ARTIFACT_FRAGMENT_VERSION",
|
|
1249
|
+
"E_ARTIFACT_FRAGMENT_VERSION_UNSUPPORTED"
|
|
1250
|
+
].includes(code)) {
|
|
1251
|
+
return "invalid-version";
|
|
1252
|
+
}
|
|
1253
|
+
if (code === "E_ARTIFACT_PASTE_INVALID") return "malformed-payload";
|
|
1254
|
+
return "malformed-payload";
|
|
1255
|
+
}
|
|
1256
|
+
function setCopy(document2, status) {
|
|
1257
|
+
const copy = HOSTED_ARTIFACT_STATE_COPY[status] ?? { title: "", detail: "", action: "" };
|
|
1258
|
+
const title2 = document2.querySelector("[data-planr-hosted-title]");
|
|
1259
|
+
const detail = document2.querySelector("[data-planr-hosted-detail]");
|
|
1260
|
+
const action = document2.querySelector("[data-planr-hosted-retry]");
|
|
1261
|
+
if (title2) title2.textContent = copy.title;
|
|
1262
|
+
if (detail) detail.textContent = copy.detail;
|
|
1263
|
+
if (action) {
|
|
1264
|
+
action.textContent = copy.action;
|
|
1265
|
+
action.hidden = !copy.action;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
function mountHostedArtifactViewer({
|
|
1269
|
+
document: document2 = globalThis.document,
|
|
1270
|
+
window = document2?.defaultView,
|
|
1271
|
+
enabled = false,
|
|
1272
|
+
location = window?.location,
|
|
1273
|
+
decodeFragment,
|
|
1274
|
+
loadShort,
|
|
1275
|
+
loadRoom,
|
|
1276
|
+
onEnvelope,
|
|
1277
|
+
supportsTransport = () => true,
|
|
1278
|
+
fragmentLimit = ARTIFACT_SHARE_FRAGMENT_LIMIT
|
|
1279
|
+
} = {}) {
|
|
1280
|
+
if (!enabled || !document2 || !window) return null;
|
|
1281
|
+
const slot = document2.querySelector("[data-planr-hosted-viewer]");
|
|
1282
|
+
if (!slot) return null;
|
|
1283
|
+
let state = freezeState2({ status: "idle" });
|
|
1284
|
+
let generation = 0;
|
|
1285
|
+
const cleanup = [];
|
|
1286
|
+
function render() {
|
|
1287
|
+
const visible = !["idle", "ready"].includes(state.status);
|
|
1288
|
+
slot.hidden = !visible;
|
|
1289
|
+
slot.dataset.planrHostedState = state.status;
|
|
1290
|
+
slot.setAttribute("aria-busy", String(state.status === "loading"));
|
|
1291
|
+
setCopy(document2, state.status);
|
|
1292
|
+
}
|
|
1293
|
+
function setState(next) {
|
|
1294
|
+
state = freezeState2(next);
|
|
1295
|
+
render();
|
|
1296
|
+
return state;
|
|
1297
|
+
}
|
|
1298
|
+
async function load() {
|
|
1299
|
+
const parsed = parseHostedArtifactLocation(location, { fragmentLimit });
|
|
1300
|
+
if (!parsed.ok) return setState({ status: parsed.status });
|
|
1301
|
+
const request = parsed.transport === "fragment" ? { transport: "fragment", version: parsed.version, payload: parsed.payload } : {
|
|
1302
|
+
transport: parsed.transport,
|
|
1303
|
+
id: parsed.id,
|
|
1304
|
+
key: parsed.key,
|
|
1305
|
+
...parsed.readCapability ? { readCapability: parsed.readCapability } : {},
|
|
1306
|
+
...parsed.write ? { write: parsed.write } : {},
|
|
1307
|
+
...parsed.owner ? { owner: parsed.owner } : {},
|
|
1308
|
+
...parsed.manage ? { manage: parsed.manage } : {}
|
|
1309
|
+
};
|
|
1310
|
+
if (!supportsTransport(parsed.transport)) {
|
|
1311
|
+
return setState({ status: "unsupported-browser", transport: parsed.transport, request });
|
|
1312
|
+
}
|
|
1313
|
+
const sequence = ++generation;
|
|
1314
|
+
setState({ status: "loading", transport: parsed.transport, request });
|
|
1315
|
+
try {
|
|
1316
|
+
const envelope2 = parsed.transport === "fragment" ? await (typeof decodeFragment === "function" ? decodeFragment(Object.freeze({ version: parsed.version, payload: parsed.payload })) : Promise.reject(
|
|
1317
|
+
new HostedArtifactViewerError(
|
|
1318
|
+
"E_ARTIFACT_CODEC_UNSUPPORTED",
|
|
1319
|
+
"No private-fragment decoder is installed."
|
|
1320
|
+
)
|
|
1321
|
+
)) : parsed.transport === "short" ? await (typeof loadShort === "function" ? loadShort(Object.freeze({ id: parsed.id, key: parsed.key })) : Promise.reject(
|
|
1322
|
+
new HostedArtifactViewerError(
|
|
1323
|
+
"E_ARTIFACT_BROWSER_UNSUPPORTED",
|
|
1324
|
+
"No encrypted short-link loader is installed."
|
|
1325
|
+
)
|
|
1326
|
+
)) : await (typeof loadRoom === "function" ? loadRoom(
|
|
1327
|
+
Object.freeze({
|
|
1328
|
+
id: parsed.id,
|
|
1329
|
+
key: parsed.key,
|
|
1330
|
+
...parsed.readCapability ? { readCapability: parsed.readCapability } : {},
|
|
1331
|
+
...parsed.write ? { write: parsed.write } : {},
|
|
1332
|
+
...parsed.owner ? { owner: parsed.owner } : {},
|
|
1333
|
+
...parsed.manage ? { manage: parsed.manage } : {}
|
|
1334
|
+
})
|
|
1335
|
+
) : Promise.reject(
|
|
1336
|
+
new HostedArtifactViewerError(
|
|
1337
|
+
"E_ARTIFACT_BROWSER_UNSUPPORTED",
|
|
1338
|
+
"No live review room loader is installed."
|
|
1339
|
+
)
|
|
1340
|
+
));
|
|
1341
|
+
if (sequence !== generation) return state;
|
|
1342
|
+
setState({ status: "ready", transport: parsed.transport, request, envelope: envelope2 });
|
|
1343
|
+
if (typeof onEnvelope === "function")
|
|
1344
|
+
await onEnvelope(envelope2, Object.freeze({ transport: parsed.transport }));
|
|
1345
|
+
} catch (error) {
|
|
1346
|
+
if (sequence !== generation) return state;
|
|
1347
|
+
setState({
|
|
1348
|
+
status: hostedArtifactStateForError(error),
|
|
1349
|
+
transport: parsed.transport,
|
|
1350
|
+
request
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
return state;
|
|
1354
|
+
}
|
|
1355
|
+
function onClick(event) {
|
|
1356
|
+
if (!event.target.closest?.("[data-planr-hosted-retry]") || !state.retryable) return;
|
|
1357
|
+
void load();
|
|
1358
|
+
}
|
|
1359
|
+
slot.addEventListener("click", onClick);
|
|
1360
|
+
cleanup.push(() => slot.removeEventListener("click", onClick));
|
|
1361
|
+
render();
|
|
1362
|
+
const ready = load();
|
|
1363
|
+
const controller = Object.freeze({
|
|
1364
|
+
getState: () => state,
|
|
1365
|
+
ready,
|
|
1366
|
+
retry() {
|
|
1367
|
+
return state.retryable ? load() : Promise.resolve(state);
|
|
1368
|
+
},
|
|
1369
|
+
load,
|
|
1370
|
+
destroy() {
|
|
1371
|
+
generation += 1;
|
|
1372
|
+
for (const remove of cleanup.splice(0)) remove();
|
|
1373
|
+
}
|
|
1374
|
+
});
|
|
1375
|
+
window.__openPlanrHostedArtifactViewer = controller;
|
|
1376
|
+
return controller;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// lib/artifact/ui/stage-mount.mjs
|
|
1380
|
+
var ARTIFACT_STAGE_MOUNT_EVENT = "planr:artifact-stage-mount";
|
|
1381
|
+
function publishArtifactStage(window, stage) {
|
|
1382
|
+
window.__openPlanrArtifactStage = stage;
|
|
1383
|
+
window.dispatchEvent(new window.CustomEvent(ARTIFACT_STAGE_MOUNT_EVENT, { detail: stage }));
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// lib/artifact/ui/stage-payload.mjs
|
|
1387
|
+
var PRESENTATIONS = Object.freeze(["document", "canvas"]);
|
|
1388
|
+
function positiveInteger(value, fallback) {
|
|
1389
|
+
return Number.isInteger(value) && value > 0 ? value : fallback;
|
|
1390
|
+
}
|
|
1391
|
+
function freezeArtifactMetadata(artifact, index2) {
|
|
1392
|
+
if (!artifact || typeof artifact !== "object") {
|
|
1393
|
+
throw new TypeError(`Artifact ${index2 + 1} must be an object.`);
|
|
1394
|
+
}
|
|
1395
|
+
if (typeof artifact.id !== "string" || artifact.id.length === 0) {
|
|
1396
|
+
throw new TypeError(`Artifact ${index2 + 1} requires an id.`);
|
|
1397
|
+
}
|
|
1398
|
+
return Object.freeze({
|
|
1399
|
+
id: artifact.id,
|
|
1400
|
+
title: typeof artifact.title === "string" && artifact.title.length > 0 ? artifact.title : `Artifact ${index2 + 1}`,
|
|
1401
|
+
sha256: typeof artifact.sha256 === "string" ? artifact.sha256 : "",
|
|
1402
|
+
viewport: Object.freeze({
|
|
1403
|
+
width: positiveInteger(artifact.viewport?.width, 1440),
|
|
1404
|
+
height: positiveInteger(artifact.viewport?.height, 900)
|
|
1405
|
+
}),
|
|
1406
|
+
colorScheme: ["light", "dark"].includes(artifact.colorScheme) ? artifact.colorScheme : "light"
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
function requestedArtifactId(value) {
|
|
1410
|
+
if (typeof value === "string") return value;
|
|
1411
|
+
return typeof value?.id === "string" ? value.id : "";
|
|
1412
|
+
}
|
|
1413
|
+
function availableId(artifacts, requested, fallback = "") {
|
|
1414
|
+
return artifacts.some(({ id: id7 }) => id7 === requested) ? requested : fallback;
|
|
1415
|
+
}
|
|
1416
|
+
function normalizeViewMode(value, artifactCount) {
|
|
1417
|
+
if (artifactCount < 2) return "single";
|
|
1418
|
+
return ["single", "variants", "split"].includes(value) ? value : "variants";
|
|
1419
|
+
}
|
|
1420
|
+
function createArtifactStagePayload(envelope2 = {}, { viewer } = {}) {
|
|
1421
|
+
const artifacts = Object.freeze(
|
|
1422
|
+
(Array.isArray(envelope2?.artifacts) ? envelope2.artifacts : []).map(freezeArtifactMetadata)
|
|
1423
|
+
);
|
|
1424
|
+
const sourceViewer = viewer && typeof viewer === "object" ? viewer : envelope2?.viewer && typeof envelope2.viewer === "object" ? envelope2.viewer : {};
|
|
1425
|
+
const firstId = artifacts[0]?.id ?? "";
|
|
1426
|
+
const activeArtifactId = availableId(
|
|
1427
|
+
artifacts,
|
|
1428
|
+
requestedArtifactId(sourceViewer.activeArtifactId),
|
|
1429
|
+
firstId
|
|
1430
|
+
);
|
|
1431
|
+
const mode = normalizeViewMode(sourceViewer.mode, artifacts.length);
|
|
1432
|
+
return Object.freeze({
|
|
1433
|
+
schemaVersion: "1.0.0",
|
|
1434
|
+
artifacts,
|
|
1435
|
+
viewer: Object.freeze({
|
|
1436
|
+
mode,
|
|
1437
|
+
activeArtifactId,
|
|
1438
|
+
...PRESENTATIONS.includes(sourceViewer.presentation) ? { presentation: sourceViewer.presentation } : {}
|
|
1439
|
+
})
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// lib/artifact/ui/stage.mjs
|
|
1444
|
+
var ARTIFACT_STAGE_EVENTS = Object.freeze({
|
|
1445
|
+
change: "planr:stage-change",
|
|
1446
|
+
point: "planr:artifact-point",
|
|
1447
|
+
region: "planr:artifact-region",
|
|
1448
|
+
layout: "planr:artifact-layout",
|
|
1449
|
+
frameState: "planr:artifact-frame-state"
|
|
1450
|
+
});
|
|
1451
|
+
var ARTIFACT_STAGE_LIMITS = Object.freeze({
|
|
1452
|
+
defaultZoom: 72,
|
|
1453
|
+
minZoom: 25,
|
|
1454
|
+
maxZoom: 200,
|
|
1455
|
+
zoomStep: 10,
|
|
1456
|
+
maxDocumentWidth: 16384,
|
|
1457
|
+
maxDocumentHeight: 262144,
|
|
1458
|
+
defaultFrameBudget: 3,
|
|
1459
|
+
frameLoadTimeoutMs: 15e3
|
|
1460
|
+
});
|
|
1461
|
+
var VIEW_MODES = Object.freeze(["single", "variants", "split"]);
|
|
1462
|
+
var REVIEW_MODES = Object.freeze(["interact", "comment"]);
|
|
1463
|
+
var THEMES = Object.freeze(["auto", "light", "dark"]);
|
|
1464
|
+
var PRESENTATIONS2 = Object.freeze(["document", "canvas"]);
|
|
1465
|
+
var STATUSES = Object.freeze([
|
|
1466
|
+
"ready",
|
|
1467
|
+
"empty",
|
|
1468
|
+
"bundling",
|
|
1469
|
+
"loading",
|
|
1470
|
+
"invalid",
|
|
1471
|
+
"expired",
|
|
1472
|
+
"decryption-failed",
|
|
1473
|
+
"unsupported-browser"
|
|
1474
|
+
]);
|
|
1475
|
+
var STATUS_COPY = Object.freeze({
|
|
1476
|
+
empty: Object.freeze({
|
|
1477
|
+
title: "No artifact content",
|
|
1478
|
+
detail: "Choose a bundled HTML artifact to begin this review."
|
|
1479
|
+
}),
|
|
1480
|
+
bundling: Object.freeze({
|
|
1481
|
+
title: "Bundling artifact",
|
|
1482
|
+
detail: "Packaging local scripts, styles, fonts, and images without network access."
|
|
1483
|
+
}),
|
|
1484
|
+
loading: Object.freeze({
|
|
1485
|
+
title: "Loading private review",
|
|
1486
|
+
detail: "Validating the envelope and frozen artifact viewport."
|
|
1487
|
+
}),
|
|
1488
|
+
invalid: Object.freeze({
|
|
1489
|
+
title: "This review is invalid",
|
|
1490
|
+
detail: "The artifact envelope could not be decoded or validated."
|
|
1491
|
+
}),
|
|
1492
|
+
expired: Object.freeze({
|
|
1493
|
+
title: "This encrypted review expired",
|
|
1494
|
+
detail: "Ask the sender for a new immutable review link."
|
|
1495
|
+
}),
|
|
1496
|
+
"decryption-failed": Object.freeze({
|
|
1497
|
+
title: "This key cannot decrypt the review",
|
|
1498
|
+
detail: "Use the complete link, including its private fragment key."
|
|
1499
|
+
}),
|
|
1500
|
+
"unsupported-browser": Object.freeze({
|
|
1501
|
+
title: "Browser support is required",
|
|
1502
|
+
detail: "Use a current browser with Blob URL support to review this artifact."
|
|
1503
|
+
})
|
|
1504
|
+
});
|
|
1505
|
+
function member2(value, allowed, fallback) {
|
|
1506
|
+
return allowed.includes(value) ? value : fallback;
|
|
1507
|
+
}
|
|
1508
|
+
function finite2(value, fallback = 0) {
|
|
1509
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
1510
|
+
}
|
|
1511
|
+
function clamp2(value, min, max) {
|
|
1512
|
+
return Math.min(max, Math.max(min, value));
|
|
1513
|
+
}
|
|
1514
|
+
function normalized2(value) {
|
|
1515
|
+
return Math.round(clamp2(finite2(value), 0, 1) * 1e6) / 1e6;
|
|
1516
|
+
}
|
|
1517
|
+
function artifactMetadata(artifact) {
|
|
1518
|
+
return Object.freeze({
|
|
1519
|
+
id: artifact.id,
|
|
1520
|
+
title: artifact.title,
|
|
1521
|
+
sha256: artifact.sha256,
|
|
1522
|
+
viewport: artifact.viewport,
|
|
1523
|
+
colorScheme: artifact.colorScheme
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
function requestedArtifactId2(value) {
|
|
1527
|
+
if (typeof value === "string") return value;
|
|
1528
|
+
return typeof value?.id === "string" ? value.id : "";
|
|
1529
|
+
}
|
|
1530
|
+
function availableId2(artifacts, requested, fallback = "") {
|
|
1531
|
+
return artifacts.some(({ id: id7 }) => id7 === requested) ? requested : fallback;
|
|
1532
|
+
}
|
|
1533
|
+
function comparisonIdFor(artifacts, activeArtifactId, requested = "") {
|
|
1534
|
+
if (requested !== activeArtifactId && artifacts.some(({ id: id7 }) => id7 === requested))
|
|
1535
|
+
return requested;
|
|
1536
|
+
return artifacts.find(({ id: id7 }) => id7 !== activeArtifactId)?.id ?? "";
|
|
1537
|
+
}
|
|
1538
|
+
function normalizeViewMode2(value, artifactCount) {
|
|
1539
|
+
if (artifactCount < 2) return "single";
|
|
1540
|
+
return member2(value, VIEW_MODES, "variants");
|
|
1541
|
+
}
|
|
1542
|
+
function createArtifactStageState(payload = {}, shellModel = {}) {
|
|
1543
|
+
const artifacts = Object.freeze(
|
|
1544
|
+
(Array.isArray(payload?.artifacts) ? payload.artifacts : []).map(artifactMetadata)
|
|
1545
|
+
);
|
|
1546
|
+
const firstId = artifacts[0]?.id ?? "";
|
|
1547
|
+
const activeArtifactId = availableId2(
|
|
1548
|
+
artifacts,
|
|
1549
|
+
requestedArtifactId2(shellModel.activeArtifact ?? shellModel.activeArtifactId) || requestedArtifactId2(payload?.viewer?.activeArtifactId),
|
|
1550
|
+
firstId
|
|
1551
|
+
);
|
|
1552
|
+
const comparisonArtifactId = comparisonIdFor(
|
|
1553
|
+
artifacts,
|
|
1554
|
+
activeArtifactId,
|
|
1555
|
+
requestedArtifactId2(shellModel.comparisonArtifact ?? shellModel.comparisonArtifactId)
|
|
1556
|
+
);
|
|
1557
|
+
const statusFallback = artifacts.length === 0 ? "empty" : "ready";
|
|
1558
|
+
let status = member2(shellModel.status, STATUSES, statusFallback);
|
|
1559
|
+
if (artifacts.length === 0 && status === "ready") status = "empty";
|
|
1560
|
+
const viewMode = normalizeViewMode2(
|
|
1561
|
+
shellModel.viewMode ?? payload?.viewer?.mode,
|
|
1562
|
+
artifacts.length
|
|
1563
|
+
);
|
|
1564
|
+
const presentation = resolveArtifactPresentation(
|
|
1565
|
+
shellModel.presentation ?? payload?.viewer?.presentation,
|
|
1566
|
+
{ viewMode, artifactCount: artifacts.length }
|
|
1567
|
+
);
|
|
1568
|
+
return Object.freeze({
|
|
1569
|
+
schemaVersion: "1.0.0",
|
|
1570
|
+
artifacts,
|
|
1571
|
+
activeArtifactId,
|
|
1572
|
+
comparisonArtifactId,
|
|
1573
|
+
viewMode,
|
|
1574
|
+
presentation,
|
|
1575
|
+
reviewMode: member2(shellModel.reviewMode, REVIEW_MODES, "interact"),
|
|
1576
|
+
zoom: clamp2(
|
|
1577
|
+
Number.isInteger(shellModel.zoom) ? shellModel.zoom : ARTIFACT_STAGE_LIMITS.defaultZoom,
|
|
1578
|
+
ARTIFACT_STAGE_LIMITS.minZoom,
|
|
1579
|
+
ARTIFACT_STAGE_LIMITS.maxZoom
|
|
1580
|
+
),
|
|
1581
|
+
railOpen: shellModel.railOpen === void 0 ? presentation === "canvas" : Boolean(shellModel.railOpen),
|
|
1582
|
+
theme: member2(shellModel.theme, THEMES, "auto"),
|
|
1583
|
+
status
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
function nextState(state, changes) {
|
|
1587
|
+
return Object.freeze({ ...state, ...changes });
|
|
1588
|
+
}
|
|
1589
|
+
function reduceArtifactStageState(state, action = {}) {
|
|
1590
|
+
switch (action.type) {
|
|
1591
|
+
case "set-active": {
|
|
1592
|
+
const id7 = availableId2(state.artifacts, action.artifactId);
|
|
1593
|
+
if (!id7 || id7 === state.activeArtifactId) return state;
|
|
1594
|
+
const comparisonArtifactId = id7 === state.comparisonArtifactId ? state.activeArtifactId : comparisonIdFor(state.artifacts, id7, state.comparisonArtifactId);
|
|
1595
|
+
return nextState(state, { activeArtifactId: id7, comparisonArtifactId });
|
|
1596
|
+
}
|
|
1597
|
+
case "set-comparison": {
|
|
1598
|
+
const id7 = comparisonIdFor(state.artifacts, state.activeArtifactId, action.artifactId);
|
|
1599
|
+
return id7 === state.comparisonArtifactId ? state : nextState(state, { comparisonArtifactId: id7 });
|
|
1600
|
+
}
|
|
1601
|
+
case "set-view-mode": {
|
|
1602
|
+
const viewMode = normalizeViewMode2(action.viewMode, state.artifacts.length);
|
|
1603
|
+
return viewMode === state.viewMode ? state : nextState(state, { viewMode });
|
|
1604
|
+
}
|
|
1605
|
+
case "set-review-mode": {
|
|
1606
|
+
const reviewMode = member2(action.reviewMode, REVIEW_MODES, state.reviewMode);
|
|
1607
|
+
return reviewMode === state.reviewMode ? state : nextState(state, { reviewMode });
|
|
1608
|
+
}
|
|
1609
|
+
case "set-zoom": {
|
|
1610
|
+
const zoom = clamp2(
|
|
1611
|
+
Math.round(finite2(action.zoom, state.zoom)),
|
|
1612
|
+
ARTIFACT_STAGE_LIMITS.minZoom,
|
|
1613
|
+
ARTIFACT_STAGE_LIMITS.maxZoom
|
|
1614
|
+
);
|
|
1615
|
+
return zoom === state.zoom ? state : nextState(state, { zoom });
|
|
1616
|
+
}
|
|
1617
|
+
case "zoom-by":
|
|
1618
|
+
return reduceArtifactStageState(state, {
|
|
1619
|
+
type: "set-zoom",
|
|
1620
|
+
zoom: state.zoom + finite2(action.delta)
|
|
1621
|
+
});
|
|
1622
|
+
case "set-rail-open": {
|
|
1623
|
+
const railOpen = Boolean(action.railOpen);
|
|
1624
|
+
return railOpen === state.railOpen ? state : nextState(state, { railOpen });
|
|
1625
|
+
}
|
|
1626
|
+
case "toggle-rail":
|
|
1627
|
+
return nextState(state, { railOpen: !state.railOpen });
|
|
1628
|
+
case "set-theme": {
|
|
1629
|
+
const theme = member2(action.theme, THEMES, state.theme);
|
|
1630
|
+
return theme === state.theme ? state : nextState(state, { theme });
|
|
1631
|
+
}
|
|
1632
|
+
case "cycle-theme": {
|
|
1633
|
+
const index2 = THEMES.indexOf(state.theme);
|
|
1634
|
+
return nextState(state, { theme: THEMES[(index2 + 1) % THEMES.length] });
|
|
1635
|
+
}
|
|
1636
|
+
case "set-status": {
|
|
1637
|
+
const status = member2(action.status, STATUSES, state.status);
|
|
1638
|
+
return status === state.status ? state : nextState(state, { status });
|
|
1639
|
+
}
|
|
1640
|
+
default:
|
|
1641
|
+
return state;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
function visibleArtifactIds(state) {
|
|
1645
|
+
if (!state.activeArtifactId) return Object.freeze([]);
|
|
1646
|
+
if (state.viewMode === "split" && state.comparisonArtifactId) {
|
|
1647
|
+
return Object.freeze([state.activeArtifactId, state.comparisonArtifactId]);
|
|
1648
|
+
}
|
|
1649
|
+
return Object.freeze([state.activeArtifactId]);
|
|
1650
|
+
}
|
|
1651
|
+
function assertRect2(rect) {
|
|
1652
|
+
if (!rect || !Number.isFinite(rect.left) || !Number.isFinite(rect.top) || !Number.isFinite(rect.width) || !Number.isFinite(rect.height) || rect.width <= 0 || rect.height <= 0) {
|
|
1653
|
+
throw new RangeError("Artifact bounds must have positive finite dimensions.");
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
function clientPointToNormalized(rect, point2) {
|
|
1657
|
+
assertRect2(rect);
|
|
1658
|
+
return Object.freeze({
|
|
1659
|
+
x: normalized2((finite2(point2?.x ?? point2?.clientX) - rect.left) / rect.width),
|
|
1660
|
+
y: normalized2((finite2(point2?.y ?? point2?.clientY) - rect.top) / rect.height)
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
function normalizedPointToClient(rect, point2) {
|
|
1664
|
+
assertRect2(rect);
|
|
1665
|
+
return Object.freeze({
|
|
1666
|
+
x: rect.left + normalized2(point2?.x) * rect.width,
|
|
1667
|
+
y: rect.top + normalized2(point2?.y) * rect.height
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
function parseDataScript(document2, id7) {
|
|
1671
|
+
const node2 = document2.getElementById(id7);
|
|
1672
|
+
if (!node2) throw new Error(`Missing artifact shell data: ${id7}`);
|
|
1673
|
+
return JSON.parse(node2.textContent ?? "null");
|
|
1674
|
+
}
|
|
1675
|
+
function isEditableTarget(target) {
|
|
1676
|
+
const HTMLElement = target?.ownerDocument?.defaultView?.HTMLElement;
|
|
1677
|
+
return Boolean(
|
|
1678
|
+
HTMLElement && target instanceof HTMLElement && (target.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName))
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
function stageArtifactById(state, id7) {
|
|
1682
|
+
return state.artifacts.find((artifact) => artifact.id === id7) ?? null;
|
|
1683
|
+
}
|
|
1684
|
+
function updateStatus(document2, state) {
|
|
1685
|
+
const statusPanel = document2.querySelector(".planr-stage-status");
|
|
1686
|
+
const surface = document2.querySelector(".planr-stage-surface");
|
|
1687
|
+
if (!statusPanel || !surface) return;
|
|
1688
|
+
const ready = state.status === "ready";
|
|
1689
|
+
statusPanel.hidden = ready;
|
|
1690
|
+
surface.toggleAttribute("inert", !ready);
|
|
1691
|
+
surface.setAttribute("aria-hidden", String(!ready));
|
|
1692
|
+
if (ready) surface.removeAttribute("aria-hidden");
|
|
1693
|
+
const copy = STATUS_COPY[state.status];
|
|
1694
|
+
if (copy) {
|
|
1695
|
+
const title2 = statusPanel.querySelector("strong");
|
|
1696
|
+
const detail = statusPanel.querySelector("p");
|
|
1697
|
+
if (title2) title2.textContent = copy.title;
|
|
1698
|
+
if (detail) detail.textContent = copy.detail;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
function emit(root, window, type, detail) {
|
|
1702
|
+
root.dispatchEvent(new window.CustomEvent(type, { detail, bubbles: true }));
|
|
1703
|
+
}
|
|
1704
|
+
async function htmlForSource(window, source) {
|
|
1705
|
+
if (typeof source === "string" && source.trimStart().startsWith("<")) return source;
|
|
1706
|
+
if (source && typeof source === "object" && typeof source.html === "string") return source.html;
|
|
1707
|
+
if (source instanceof window.Blob) return source.text();
|
|
1708
|
+
if (source instanceof window.ArrayBuffer) return new window.TextDecoder().decode(source);
|
|
1709
|
+
if (window.ArrayBuffer.isView(source)) {
|
|
1710
|
+
return new window.TextDecoder().decode(
|
|
1711
|
+
new window.Uint8Array(source.buffer, source.byteOffset, source.byteLength)
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
return null;
|
|
1715
|
+
}
|
|
1716
|
+
function mountArtifactStage({
|
|
1717
|
+
document: document2 = globalThis.document,
|
|
1718
|
+
window = document2?.defaultView,
|
|
1719
|
+
resolveArtifactSource,
|
|
1720
|
+
sourceTransport = "blob",
|
|
1721
|
+
frameBudget: requestedFrameBudget,
|
|
1722
|
+
frameLoadTimeoutMs = ARTIFACT_STAGE_LIMITS.frameLoadTimeoutMs,
|
|
1723
|
+
bridgeClient,
|
|
1724
|
+
onState,
|
|
1725
|
+
review: reviewOptions = {},
|
|
1726
|
+
share: shareOptions = {},
|
|
1727
|
+
hosted: hostedOptions = {}
|
|
1728
|
+
} = {}) {
|
|
1729
|
+
if (!document2 || !window) return null;
|
|
1730
|
+
const root = document2.querySelector(".planr-shell");
|
|
1731
|
+
if (!root) return null;
|
|
1732
|
+
const initializationError = globalThis.__OPENPLANR_ARTIFACT_STAGE_INITIALIZATION_ERROR__;
|
|
1733
|
+
if (initializationError) {
|
|
1734
|
+
const status = root.querySelector('[data-planr-slot="status"]');
|
|
1735
|
+
if (status) {
|
|
1736
|
+
status.textContent = initializationError;
|
|
1737
|
+
status.setAttribute("data-error", "");
|
|
1738
|
+
status.setAttribute("role", "alert");
|
|
1739
|
+
}
|
|
1740
|
+
root.setAttribute("data-planr-initialization-error", "");
|
|
1741
|
+
return null;
|
|
1742
|
+
}
|
|
1743
|
+
if (!["blob", "srcdoc"].includes(sourceTransport)) {
|
|
1744
|
+
throw new TypeError("Artifact source transport must be blob or srcdoc.");
|
|
1745
|
+
}
|
|
1746
|
+
let payload;
|
|
1747
|
+
let shellModel;
|
|
1748
|
+
let reviewConfig;
|
|
1749
|
+
try {
|
|
1750
|
+
payload = parseDataScript(document2, "planr-artifact-stage-payload");
|
|
1751
|
+
shellModel = parseDataScript(document2, "planr-artifact-shell-model");
|
|
1752
|
+
reviewConfig = parseDataScript(document2, "planr-artifact-review-state");
|
|
1753
|
+
} catch {
|
|
1754
|
+
payload = { artifacts: [], viewer: { mode: "single", activeArtifactId: "" } };
|
|
1755
|
+
shellModel = { status: "invalid" };
|
|
1756
|
+
reviewConfig = { reviewOf: "0".repeat(64), review: null };
|
|
1757
|
+
}
|
|
1758
|
+
let state;
|
|
1759
|
+
try {
|
|
1760
|
+
state = createArtifactStageState(payload, shellModel);
|
|
1761
|
+
} catch {
|
|
1762
|
+
state = createArtifactStageState({}, { status: "invalid" });
|
|
1763
|
+
}
|
|
1764
|
+
const frames = new Map(
|
|
1765
|
+
[...document2.querySelectorAll("[data-planr-artifact-frame]")].map((frame) => [
|
|
1766
|
+
frame.dataset.planrArtifactFrame,
|
|
1767
|
+
frame
|
|
1768
|
+
])
|
|
1769
|
+
);
|
|
1770
|
+
const panels = new Map(
|
|
1771
|
+
[...document2.querySelectorAll(".planr-artifact-panel[data-artifact-id]")].map((panel) => [
|
|
1772
|
+
panel.dataset.artifactId,
|
|
1773
|
+
panel
|
|
1774
|
+
])
|
|
1775
|
+
);
|
|
1776
|
+
const documentLayouts = /* @__PURE__ */ new Map();
|
|
1777
|
+
const cleanup = [];
|
|
1778
|
+
const frameBudget = requestedFrameBudget === void 0 ? Number(root.dataset.planrFrameBudget || ARTIFACT_STAGE_LIMITS.defaultFrameBudget) : requestedFrameBudget;
|
|
1779
|
+
if (frameBudget !== null && (!Number.isInteger(frameBudget) || frameBudget < 1 || frameBudget > 8)) {
|
|
1780
|
+
throw new RangeError(
|
|
1781
|
+
"Artifact frame budget must be between 1 and 8, or null for eager loading."
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
if (!Number.isInteger(frameLoadTimeoutMs) || frameLoadTimeoutMs < 1 || frameLoadTimeoutMs > 12e4) {
|
|
1785
|
+
throw new RangeError("Artifact frame load timeout must be between 1 and 120000 milliseconds.");
|
|
1786
|
+
}
|
|
1787
|
+
const frameDiagnostics = /* @__PURE__ */ new Map();
|
|
1788
|
+
const frameLoads = /* @__PURE__ */ new Map();
|
|
1789
|
+
const frameSlots = /* @__PURE__ */ new Map();
|
|
1790
|
+
if (frameBudget !== null) {
|
|
1791
|
+
for (const [id7, frame] of frames) {
|
|
1792
|
+
const host = frame.parentNode;
|
|
1793
|
+
if (!host || !root.contains(host) || frame.hasAttribute("src") || frame.hasAttribute("srcdoc"))
|
|
1794
|
+
continue;
|
|
1795
|
+
const placeholder = document2.createComment("Artifact preview");
|
|
1796
|
+
frameSlots.set(id7, { host, placeholder });
|
|
1797
|
+
host.replaceChild(placeholder, frame);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
let frameUse = 0;
|
|
1801
|
+
let frameQueue = Promise.resolve();
|
|
1802
|
+
let activationGeneration = 0;
|
|
1803
|
+
let disposed = false;
|
|
1804
|
+
for (const frame of frames.values()) frame.dataset.planrFrameState = "unloaded";
|
|
1805
|
+
function listen(target, type, handler, options) {
|
|
1806
|
+
target.addEventListener(type, handler, options);
|
|
1807
|
+
cleanup.push(() => target.removeEventListener(type, handler, options));
|
|
1808
|
+
}
|
|
1809
|
+
function render({ announce: announce2 = false } = {}) {
|
|
1810
|
+
const visible = new Set(visibleArtifactIds(state));
|
|
1811
|
+
root.dataset.planrView = state.viewMode;
|
|
1812
|
+
root.dataset.planrReviewMode = state.reviewMode;
|
|
1813
|
+
root.dataset.planrState = state.status;
|
|
1814
|
+
root.dataset.planrRailOpen = String(state.railOpen);
|
|
1815
|
+
root.dataset.planrPresentation = state.presentation;
|
|
1816
|
+
document2.documentElement.dataset.planrPresentation = state.presentation;
|
|
1817
|
+
document2.documentElement.dataset.planrTheme = state.theme;
|
|
1818
|
+
const grid = document2.querySelector(".planr-frame-grid");
|
|
1819
|
+
const surface = document2.querySelector(".planr-stage-surface");
|
|
1820
|
+
const tablist = document2.querySelector(".planr-variants");
|
|
1821
|
+
const rail = document2.getElementById("planr-review-rail");
|
|
1822
|
+
const feedbackButton = document2.querySelector('[data-planr-action="feedback"]');
|
|
1823
|
+
const addCommentButton = document2.querySelector('[data-planr-action="add-comment"]');
|
|
1824
|
+
const themeButton = document2.querySelector('[data-planr-action="theme"]');
|
|
1825
|
+
const statusSlot = document2.querySelector('[data-planr-slot="status"]');
|
|
1826
|
+
const metadata = document2.querySelector(
|
|
1827
|
+
".planr-toolbar:not([data-studio-react-chrome]) .planr-title-block > span"
|
|
1828
|
+
);
|
|
1829
|
+
const breadcrumb = document2.querySelector(".planr-stage-heading > span:first-child");
|
|
1830
|
+
const activeArtifact = stageArtifactById(state, state.activeArtifactId);
|
|
1831
|
+
if (grid) grid.dataset.planrLayout = state.viewMode;
|
|
1832
|
+
const visualOrder = state.viewMode === "split" ? [...visible, ...state.artifacts.map(({ id: id7 }) => id7).filter((id7) => !visible.has(id7))] : state.artifacts.map(({ id: id7 }) => id7);
|
|
1833
|
+
if (surface) surface.style.setProperty("--planr-shell-zoom", String(state.zoom / 100));
|
|
1834
|
+
if (tablist) tablist.hidden = state.viewMode === "single" || state.artifacts.length < 2;
|
|
1835
|
+
if (rail) {
|
|
1836
|
+
rail.toggleAttribute("inert", !state.railOpen);
|
|
1837
|
+
rail.setAttribute("aria-hidden", String(!state.railOpen));
|
|
1838
|
+
if (state.railOpen) rail.removeAttribute("aria-hidden");
|
|
1839
|
+
}
|
|
1840
|
+
if (feedbackButton) feedbackButton.setAttribute("aria-expanded", String(state.railOpen));
|
|
1841
|
+
if (addCommentButton) {
|
|
1842
|
+
const paused = root.hasAttribute("data-planr-room-comments-paused");
|
|
1843
|
+
addCommentButton.disabled = paused;
|
|
1844
|
+
addCommentButton.setAttribute("aria-pressed", String(state.reviewMode === "comment"));
|
|
1845
|
+
addCommentButton.setAttribute(
|
|
1846
|
+
"aria-label",
|
|
1847
|
+
paused ? "Add comment unavailable: comments are paused" : "Add comment"
|
|
1848
|
+
);
|
|
1849
|
+
addCommentButton.dataset.planrTooltip = paused ? "Comments are paused" : "Add comment (C)";
|
|
1850
|
+
}
|
|
1851
|
+
if (themeButton) {
|
|
1852
|
+
themeButton.textContent = state.theme;
|
|
1853
|
+
themeButton.setAttribute("aria-label", `Shell theme ${state.theme}`);
|
|
1854
|
+
}
|
|
1855
|
+
if (statusSlot)
|
|
1856
|
+
statusSlot.textContent = state.reviewMode === "comment" ? "Comment mode" : "Interactions enabled";
|
|
1857
|
+
if (metadata && activeArtifact) {
|
|
1858
|
+
metadata.textContent = `HTML · ${activeArtifact.viewport.width}×${activeArtifact.viewport.height}`;
|
|
1859
|
+
}
|
|
1860
|
+
if (breadcrumb)
|
|
1861
|
+
breadcrumb.textContent = `ARTIFACT / ${(activeArtifact?.title ?? "Artifact").toUpperCase()}`;
|
|
1862
|
+
for (const button of document2.querySelectorAll("[data-planr-view]")) {
|
|
1863
|
+
const mode = button.dataset.planrView;
|
|
1864
|
+
button.setAttribute("aria-pressed", String(mode === state.viewMode));
|
|
1865
|
+
button.disabled = state.artifacts.length < 2 && mode !== "single";
|
|
1866
|
+
}
|
|
1867
|
+
for (const button of document2.querySelectorAll("[data-planr-mode]")) {
|
|
1868
|
+
button.setAttribute("aria-pressed", String(button.dataset.planrMode === state.reviewMode));
|
|
1869
|
+
}
|
|
1870
|
+
for (const button of document2.querySelectorAll('[data-planr-action="zoom-reset"]')) {
|
|
1871
|
+
button.textContent = `${state.zoom}%`;
|
|
1872
|
+
}
|
|
1873
|
+
for (const tab of document2.querySelectorAll('[role="tab"][data-artifact-id]')) {
|
|
1874
|
+
const selected = tab.dataset.artifactId === state.activeArtifactId;
|
|
1875
|
+
tab.setAttribute("aria-selected", String(selected));
|
|
1876
|
+
tab.tabIndex = selected ? 0 : -1;
|
|
1877
|
+
}
|
|
1878
|
+
for (const [id7, panel] of panels) {
|
|
1879
|
+
const isVisible = visible.has(id7);
|
|
1880
|
+
const isPrimary = id7 === state.activeArtifactId;
|
|
1881
|
+
panel.hidden = !isVisible;
|
|
1882
|
+
panel.style.order = String(visualOrder.indexOf(id7));
|
|
1883
|
+
const artifact = stageArtifactById(state, id7);
|
|
1884
|
+
panel.setAttribute(
|
|
1885
|
+
"aria-label",
|
|
1886
|
+
`${isPrimary ? "Primary" : "Comparison"} artifact: ${artifact?.title ?? id7}`
|
|
1887
|
+
);
|
|
1888
|
+
const frame = frames.get(id7);
|
|
1889
|
+
const frameReady = frame?.dataset.planrFrameState === "ready";
|
|
1890
|
+
const annotationLayer = panel.querySelector("[data-planr-annotation-layer]");
|
|
1891
|
+
if (frame) {
|
|
1892
|
+
frame.tabIndex = isVisible && frameReady && state.status === "ready" && state.reviewMode === "interact" ? 0 : -1;
|
|
1893
|
+
if (state.presentation === "document") {
|
|
1894
|
+
frame.setAttribute("scrolling", "no");
|
|
1895
|
+
frame.style.overflow = "hidden";
|
|
1896
|
+
} else {
|
|
1897
|
+
frame.removeAttribute("scrolling");
|
|
1898
|
+
frame.style.removeProperty("overflow");
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
if (annotationLayer) {
|
|
1902
|
+
const enabled = isVisible && frameReady && state.status === "ready" && state.reviewMode === "comment";
|
|
1903
|
+
const hasComposer = Boolean(
|
|
1904
|
+
annotationLayer.querySelector("[data-planr-annotation-composer]")
|
|
1905
|
+
);
|
|
1906
|
+
annotationLayer.tabIndex = enabled ? 0 : -1;
|
|
1907
|
+
annotationLayer.setAttribute("aria-disabled", String(!enabled && !hasComposer));
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
updateStatus(document2, state);
|
|
1911
|
+
renderFrameLoadingStatus();
|
|
1912
|
+
if (typeof onState === "function") {
|
|
1913
|
+
try {
|
|
1914
|
+
onState(state);
|
|
1915
|
+
} catch {
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
if (announce2) emit(root, window, ARTIFACT_STAGE_EVENTS.change, state);
|
|
1919
|
+
}
|
|
1920
|
+
function renderFrameLoadingStatus() {
|
|
1921
|
+
const activeDiagnostic = frameDiagnostics.get(state.activeArtifactId);
|
|
1922
|
+
const statusPanel = document2.querySelector(".planr-stage-status");
|
|
1923
|
+
if (!statusPanel || !activeDiagnostic) return;
|
|
1924
|
+
const detail = statusPanel.querySelector("p");
|
|
1925
|
+
if (detail && state.status === "loading") {
|
|
1926
|
+
const copy = {
|
|
1927
|
+
source: "Preparing the selected screen.",
|
|
1928
|
+
document: "Loading the selected screen.",
|
|
1929
|
+
bridge: "Connecting the selected screen to the review."
|
|
1930
|
+
};
|
|
1931
|
+
detail.textContent = copy[activeDiagnostic.phase] ?? "Loading the selected screen.";
|
|
1932
|
+
}
|
|
1933
|
+
if (detail && state.status === "invalid" && activeDiagnostic.status === "error") {
|
|
1934
|
+
detail.textContent = `The selected screen did not finish its ${activeDiagnostic.failedPhase ?? "preview"} step. Retry to load it again; saved feedback remains available.`;
|
|
1935
|
+
}
|
|
1936
|
+
let retry = statusPanel.querySelector('[data-planr-action="retry-frame"]');
|
|
1937
|
+
if (!retry) {
|
|
1938
|
+
retry = document2.createElement("button");
|
|
1939
|
+
retry.className = "planr-toolbar-action";
|
|
1940
|
+
retry.dataset.planrAction = "retry-frame";
|
|
1941
|
+
retry.textContent = "Retry screen";
|
|
1942
|
+
statusPanel.querySelector("div")?.append(retry);
|
|
1943
|
+
}
|
|
1944
|
+
retry.hidden = state.status !== "invalid" || activeDiagnostic.status !== "error";
|
|
1945
|
+
}
|
|
1946
|
+
function dispatch(action, { announce: announce2 = true } = {}) {
|
|
1947
|
+
const previous = state;
|
|
1948
|
+
state = reduceArtifactStageState(state, action);
|
|
1949
|
+
if (state !== previous) {
|
|
1950
|
+
render({ announce: announce2 });
|
|
1951
|
+
if (["set-active", "set-comparison", "set-view-mode"].includes(action.type) && typeof resolveArtifactSource === "function") {
|
|
1952
|
+
readyPromise = activateVisibleFrames();
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
return state;
|
|
1956
|
+
}
|
|
1957
|
+
function setActiveFromTab(tab, { focus = false } = {}) {
|
|
1958
|
+
dispatch({ type: "set-active", artifactId: tab.dataset.artifactId });
|
|
1959
|
+
if (focus) tab.focus();
|
|
1960
|
+
}
|
|
1961
|
+
function onClick(event) {
|
|
1962
|
+
const target = event.target.closest?.("button");
|
|
1963
|
+
if (!target) return;
|
|
1964
|
+
if (target.hasAttribute("data-planr-close-feedback")) {
|
|
1965
|
+
dispatch({ type: "set-rail-open", railOpen: false });
|
|
1966
|
+
document2.querySelector('[data-planr-action="feedback"]')?.focus();
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
if (target.dataset.planrView) {
|
|
1970
|
+
dispatch({ type: "set-view-mode", viewMode: target.dataset.planrView });
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
if (target.dataset.artifactId && target.getAttribute("role") === "tab") {
|
|
1974
|
+
setActiveFromTab(target);
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
if (target.dataset.planrMode) {
|
|
1978
|
+
dispatch({ type: "set-review-mode", reviewMode: target.dataset.planrMode });
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
switch (target.dataset.planrAction) {
|
|
1982
|
+
case "add-comment":
|
|
1983
|
+
if (!root.hasAttribute("data-planr-room-comments-paused")) {
|
|
1984
|
+
dispatch({ type: "set-review-mode", reviewMode: "comment" });
|
|
1985
|
+
}
|
|
1986
|
+
break;
|
|
1987
|
+
case "zoom-out":
|
|
1988
|
+
dispatch({ type: "zoom-by", delta: -ARTIFACT_STAGE_LIMITS.zoomStep });
|
|
1989
|
+
break;
|
|
1990
|
+
case "zoom-reset":
|
|
1991
|
+
dispatch({ type: "set-zoom", zoom: ARTIFACT_STAGE_LIMITS.defaultZoom });
|
|
1992
|
+
break;
|
|
1993
|
+
case "zoom-in":
|
|
1994
|
+
dispatch({ type: "zoom-by", delta: ARTIFACT_STAGE_LIMITS.zoomStep });
|
|
1995
|
+
break;
|
|
1996
|
+
case "feedback": {
|
|
1997
|
+
const rail = document2.getElementById("planr-review-rail");
|
|
1998
|
+
if (state.railOpen && rail?.contains(document2.activeElement)) target.focus();
|
|
1999
|
+
dispatch({ type: "toggle-rail" });
|
|
2000
|
+
break;
|
|
2001
|
+
}
|
|
2002
|
+
case "more": {
|
|
2003
|
+
const menu = target.closest(".planr-more")?.querySelector(".planr-more-menu");
|
|
2004
|
+
if (!menu) break;
|
|
2005
|
+
const open = menu.hidden;
|
|
2006
|
+
menu.hidden = !open;
|
|
2007
|
+
target.setAttribute("aria-expanded", String(open));
|
|
2008
|
+
if (open) menu.querySelector('[role="menuitem"]')?.focus();
|
|
2009
|
+
break;
|
|
2010
|
+
}
|
|
2011
|
+
case "sharing-options":
|
|
2012
|
+
target.closest(".planr-more-menu").hidden = true;
|
|
2013
|
+
document2.querySelector('[data-planr-action="more"]')?.setAttribute("aria-expanded", "false");
|
|
2014
|
+
shareController?.open?.();
|
|
2015
|
+
break;
|
|
2016
|
+
case "retry-frame":
|
|
2017
|
+
void retryFrames([state.activeArtifactId]);
|
|
2018
|
+
break;
|
|
2019
|
+
case "theme":
|
|
2020
|
+
dispatch({ type: "cycle-theme" });
|
|
2021
|
+
break;
|
|
2022
|
+
default:
|
|
2023
|
+
break;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
function onKeyDown(event) {
|
|
2027
|
+
if (event.defaultPrevented) return;
|
|
2028
|
+
if (!event.altKey && !event.ctrlKey && !event.metaKey && !isEditableTarget(event.target)) {
|
|
2029
|
+
if (event.key.toLowerCase() === "i") {
|
|
2030
|
+
dispatch({ type: "set-review-mode", reviewMode: "interact" });
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
if (event.key.toLowerCase() === "c") {
|
|
2034
|
+
if (!root.hasAttribute("data-planr-room-comments-paused")) {
|
|
2035
|
+
dispatch({ type: "set-review-mode", reviewMode: "comment" });
|
|
2036
|
+
document2.querySelector('[data-planr-action="add-comment"]')?.focus();
|
|
2037
|
+
}
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
if (event.key === "Escape") {
|
|
2041
|
+
const more = document2.querySelector(".planr-more-menu:not([hidden])");
|
|
2042
|
+
if (more) {
|
|
2043
|
+
more.hidden = true;
|
|
2044
|
+
const trigger = document2.querySelector('[data-planr-action="more"]');
|
|
2045
|
+
trigger?.setAttribute("aria-expanded", "false");
|
|
2046
|
+
trigger?.focus();
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
if (state.presentation === "document" && state.reviewMode === "comment") {
|
|
2050
|
+
dispatch({ type: "set-review-mode", reviewMode: "interact" });
|
|
2051
|
+
document2.querySelector('[data-planr-action="add-comment"]')?.focus();
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
2054
|
+
if (state.railOpen) {
|
|
2055
|
+
dispatch({ type: "set-rail-open", railOpen: false });
|
|
2056
|
+
document2.querySelector('[data-planr-action="feedback"]')?.focus();
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
const tab = event.target.closest?.('[role="tab"][data-artifact-id]');
|
|
2062
|
+
if (!tab || !["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
|
|
2063
|
+
const tabs = [...document2.querySelectorAll('[role="tab"][data-artifact-id]')];
|
|
2064
|
+
const index2 = tabs.indexOf(tab);
|
|
2065
|
+
if (index2 < 0) return;
|
|
2066
|
+
event.preventDefault();
|
|
2067
|
+
const nextIndex = event.key === "Home" ? 0 : event.key === "End" ? tabs.length - 1 : (index2 + (event.key === "ArrowRight" ? 1 : -1) + tabs.length) % tabs.length;
|
|
2068
|
+
setActiveFromTab(tabs[nextIndex], { focus: true });
|
|
2069
|
+
}
|
|
2070
|
+
function emitSelection(layer, start, end = start) {
|
|
2071
|
+
if (state.reviewMode !== "comment" || state.status !== "ready") return;
|
|
2072
|
+
const artifactId = layer.dataset.planrAnnotationLayer;
|
|
2073
|
+
if (!visibleArtifactIds(state).includes(artifactId)) return;
|
|
2074
|
+
const artifact = stageArtifactById(state, artifactId);
|
|
2075
|
+
if (!artifact) return;
|
|
2076
|
+
const region = clientSelectionToNormalized(layer.getBoundingClientRect(), start, end);
|
|
2077
|
+
const measured = state.presentation === "document" ? documentLayouts.get(artifactId) : null;
|
|
2078
|
+
const detail = Object.freeze({
|
|
2079
|
+
schemaVersion: "1.0.0",
|
|
2080
|
+
artifactId,
|
|
2081
|
+
region,
|
|
2082
|
+
viewport: measured ?? artifact.viewport
|
|
2083
|
+
});
|
|
2084
|
+
emit(root, window, ARTIFACT_STAGE_EVENTS.region, detail);
|
|
2085
|
+
emit(root, window, ARTIFACT_STAGE_EVENTS.point, detail);
|
|
2086
|
+
if (state.presentation === "document") {
|
|
2087
|
+
dispatch({ type: "set-review-mode", reviewMode: "interact" });
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
for (const layer of document2.querySelectorAll("[data-planr-annotation-layer]")) {
|
|
2091
|
+
let selection = null;
|
|
2092
|
+
let selectionPreview = null;
|
|
2093
|
+
listen(layer, "pointerdown", (event) => {
|
|
2094
|
+
if (event.button !== 0 || state.reviewMode !== "comment" || state.status !== "ready") return;
|
|
2095
|
+
if (event.target !== layer) return;
|
|
2096
|
+
event.preventDefault();
|
|
2097
|
+
selection = { pointerId: event.pointerId, start: { x: event.clientX, y: event.clientY } };
|
|
2098
|
+
layer.setPointerCapture?.(event.pointerId);
|
|
2099
|
+
selectionPreview = document2.createElement("span");
|
|
2100
|
+
selectionPreview.className = "planr-region-selection";
|
|
2101
|
+
selectionPreview.setAttribute("aria-hidden", "true");
|
|
2102
|
+
layer.append(selectionPreview);
|
|
2103
|
+
});
|
|
2104
|
+
listen(layer, "pointermove", (event) => {
|
|
2105
|
+
if (!selection || selection.pointerId !== event.pointerId || !selectionPreview) return;
|
|
2106
|
+
const region = clientSelectionToNormalized(layer.getBoundingClientRect(), selection.start, {
|
|
2107
|
+
x: event.clientX,
|
|
2108
|
+
y: event.clientY
|
|
2109
|
+
});
|
|
2110
|
+
selectionPreview.style.left = `${region.x * 100}%`;
|
|
2111
|
+
selectionPreview.style.top = `${region.y * 100}%`;
|
|
2112
|
+
selectionPreview.style.width = `${region.w * 100}%`;
|
|
2113
|
+
selectionPreview.style.height = `${region.h * 100}%`;
|
|
2114
|
+
});
|
|
2115
|
+
listen(layer, "pointercancel", (event) => {
|
|
2116
|
+
if (!selection || selection.pointerId !== event.pointerId) return;
|
|
2117
|
+
layer.releasePointerCapture?.(event.pointerId);
|
|
2118
|
+
selection = null;
|
|
2119
|
+
selectionPreview?.remove();
|
|
2120
|
+
selectionPreview = null;
|
|
2121
|
+
});
|
|
2122
|
+
listen(layer, "pointerup", (event) => {
|
|
2123
|
+
if (!selection || selection.pointerId !== event.pointerId) return;
|
|
2124
|
+
const start = selection.start;
|
|
2125
|
+
layer.releasePointerCapture?.(event.pointerId);
|
|
2126
|
+
selection = null;
|
|
2127
|
+
selectionPreview?.remove();
|
|
2128
|
+
selectionPreview = null;
|
|
2129
|
+
emitSelection(layer, start, { x: event.clientX, y: event.clientY });
|
|
2130
|
+
});
|
|
2131
|
+
listen(layer, "keydown", (event) => {
|
|
2132
|
+
if (!["Enter", " "].includes(event.key)) return;
|
|
2133
|
+
if (event.target !== layer) return;
|
|
2134
|
+
event.preventDefault();
|
|
2135
|
+
const bounds2 = layer.getBoundingClientRect();
|
|
2136
|
+
emitSelection(layer, {
|
|
2137
|
+
x: bounds2.left + bounds2.width / 2,
|
|
2138
|
+
y: bounds2.top + bounds2.height / 2
|
|
2139
|
+
});
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
for (const [artifactId, frame] of frames) {
|
|
2143
|
+
listen(frame, ARTIFACT_STAGE_EVENTS.layout, (event) => {
|
|
2144
|
+
if (state.presentation !== "document") return;
|
|
2145
|
+
const width = event.detail?.width;
|
|
2146
|
+
const height = event.detail?.height;
|
|
2147
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || width > ARTIFACT_STAGE_LIMITS.maxDocumentWidth || height < 1 || height > ARTIFACT_STAGE_LIMITS.maxDocumentHeight)
|
|
2148
|
+
return;
|
|
2149
|
+
const layout = Object.freeze({ width, height });
|
|
2150
|
+
documentLayouts.set(artifactId, layout);
|
|
2151
|
+
const panel = panels.get(artifactId);
|
|
2152
|
+
if (!panel) return;
|
|
2153
|
+
panel.dataset.planrLayoutMeasured = "true";
|
|
2154
|
+
panel.style.setProperty("--planr-document-width", `${width}px`);
|
|
2155
|
+
panel.style.setProperty("--planr-document-height", `${height}px`);
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
listen(root, "click", onClick);
|
|
2159
|
+
listen(document2, "keydown", onKeyDown);
|
|
2160
|
+
listen(document2, "click", (event) => {
|
|
2161
|
+
const more = document2.querySelector(".planr-more-menu:not([hidden])");
|
|
2162
|
+
if (!more || event.target.closest?.(".planr-more")) return;
|
|
2163
|
+
more.hidden = true;
|
|
2164
|
+
document2.querySelector('[data-planr-action="more"]')?.setAttribute("aria-expanded", "false");
|
|
2165
|
+
});
|
|
2166
|
+
const roomStateObserver = typeof window.MutationObserver === "function" ? new window.MutationObserver(() => {
|
|
2167
|
+
if (root.hasAttribute("data-planr-room-comments-paused") && state.reviewMode === "comment") {
|
|
2168
|
+
dispatch({ type: "set-review-mode", reviewMode: "interact" });
|
|
2169
|
+
} else {
|
|
2170
|
+
render();
|
|
2171
|
+
}
|
|
2172
|
+
}) : null;
|
|
2173
|
+
roomStateObserver?.observe(root, {
|
|
2174
|
+
attributes: true,
|
|
2175
|
+
attributeFilter: ["data-planr-room-comments-paused"]
|
|
2176
|
+
});
|
|
2177
|
+
if (roomStateObserver) cleanup.push(() => roomStateObserver.disconnect());
|
|
2178
|
+
render();
|
|
2179
|
+
let readyPromise = Promise.resolve(state);
|
|
2180
|
+
let feedbackController = null;
|
|
2181
|
+
let annotationController = null;
|
|
2182
|
+
let shareController = null;
|
|
2183
|
+
let hostedController = null;
|
|
2184
|
+
function disposeStage() {
|
|
2185
|
+
if (disposed) return true;
|
|
2186
|
+
disposed = true;
|
|
2187
|
+
for (const id7 of [...frameLoads.keys()]) releaseFrame(id7);
|
|
2188
|
+
for (const remove of cleanup.splice(0)) remove();
|
|
2189
|
+
return true;
|
|
2190
|
+
}
|
|
2191
|
+
const controller = Object.freeze({
|
|
2192
|
+
frameBudget,
|
|
2193
|
+
ensureFrames,
|
|
2194
|
+
retryFrames,
|
|
2195
|
+
getFrameDiagnostics: () => [...frameDiagnostics.values()],
|
|
2196
|
+
getLoadedArtifactIds: () => [...frameLoads].filter(([, record2]) => record2.status === "ready").map(([id7]) => id7),
|
|
2197
|
+
getState: () => state,
|
|
2198
|
+
getFrame: (artifactId) => frames.get(artifactId) ?? null,
|
|
2199
|
+
getPanel: (artifactId) => panels.get(artifactId) ?? null,
|
|
2200
|
+
get review() {
|
|
2201
|
+
return feedbackController;
|
|
2202
|
+
},
|
|
2203
|
+
get annotations() {
|
|
2204
|
+
return annotationController;
|
|
2205
|
+
},
|
|
2206
|
+
get share() {
|
|
2207
|
+
return shareController;
|
|
2208
|
+
},
|
|
2209
|
+
get hosted() {
|
|
2210
|
+
return hostedController;
|
|
2211
|
+
},
|
|
2212
|
+
get ready() {
|
|
2213
|
+
return readyPromise;
|
|
2214
|
+
},
|
|
2215
|
+
dispatch,
|
|
2216
|
+
destroy() {
|
|
2217
|
+
if (["creating", "ambiguous"].includes(shareController?.getState?.().phase)) {
|
|
2218
|
+
shareController.destroy?.();
|
|
2219
|
+
return false;
|
|
2220
|
+
}
|
|
2221
|
+
return disposeStage();
|
|
2222
|
+
}
|
|
2223
|
+
});
|
|
2224
|
+
publishArtifactStage(window, controller);
|
|
2225
|
+
listen(window, "pagehide", (event) => {
|
|
2226
|
+
if (!event.persisted) disposeStage();
|
|
2227
|
+
});
|
|
2228
|
+
feedbackController = mountArtifactFeedbackRail({
|
|
2229
|
+
document: document2,
|
|
2230
|
+
window,
|
|
2231
|
+
root,
|
|
2232
|
+
stageController: controller,
|
|
2233
|
+
reviewOf: reviewConfig?.reviewOf,
|
|
2234
|
+
initialReview: reviewConfig?.review ?? null,
|
|
2235
|
+
artifacts: state.artifacts,
|
|
2236
|
+
...reviewOptions
|
|
2237
|
+
});
|
|
2238
|
+
annotationController = mountArtifactAnnotations({
|
|
2239
|
+
document: document2,
|
|
2240
|
+
window,
|
|
2241
|
+
root,
|
|
2242
|
+
stageController: controller,
|
|
2243
|
+
reviewController: feedbackController
|
|
2244
|
+
});
|
|
2245
|
+
const durablePasteShare = shareOptions.createShare && shareOptions.resumePreparedShare ? createDurablePasteShare({
|
|
2246
|
+
scope: () => ({
|
|
2247
|
+
workspaceId: window.location.pathname,
|
|
2248
|
+
revisionId: reviewConfig.reviewOf ?? "unknown-revision",
|
|
2249
|
+
actorId: "local-paste-owner"
|
|
2250
|
+
}),
|
|
2251
|
+
create: (request) => shareOptions.createShare(request),
|
|
2252
|
+
commit: shareOptions.resumePreparedShare,
|
|
2253
|
+
indexedDB: window.indexedDB
|
|
2254
|
+
}) : null;
|
|
2255
|
+
if (durablePasteShare) cleanup.push(() => durablePasteShare.close());
|
|
2256
|
+
shareController = mountArtifactShareDialog({
|
|
2257
|
+
document: document2,
|
|
2258
|
+
window,
|
|
2259
|
+
root,
|
|
2260
|
+
stageController: controller,
|
|
2261
|
+
...shareOptions,
|
|
2262
|
+
...durablePasteShare ? {
|
|
2263
|
+
createShare: (request) => durablePasteShare.run(request)
|
|
2264
|
+
} : {}
|
|
2265
|
+
});
|
|
2266
|
+
hostedController = mountHostedArtifactViewer({
|
|
2267
|
+
document: document2,
|
|
2268
|
+
window,
|
|
2269
|
+
...hostedOptions
|
|
2270
|
+
});
|
|
2271
|
+
if (feedbackController?.destroy) cleanup.push(() => feedbackController.destroy());
|
|
2272
|
+
if (annotationController?.destroy) cleanup.push(() => annotationController.destroy());
|
|
2273
|
+
if (shareController?.destroy) cleanup.push(() => shareController.destroy());
|
|
2274
|
+
if (hostedController?.destroy) cleanup.push(() => hostedController.destroy());
|
|
2275
|
+
function cancelledFrame() {
|
|
2276
|
+
const error = new Error("Artifact frame loading was cancelled.");
|
|
2277
|
+
error.name = "AbortError";
|
|
2278
|
+
return error;
|
|
2279
|
+
}
|
|
2280
|
+
function frameStatus(artifactId, status, phase, failure) {
|
|
2281
|
+
const frame = frames.get(artifactId);
|
|
2282
|
+
if (frame) {
|
|
2283
|
+
frame.dataset.planrFrameState = status;
|
|
2284
|
+
frame.dataset.planrFramePhase = phase;
|
|
2285
|
+
}
|
|
2286
|
+
const previous = frameDiagnostics.get(artifactId);
|
|
2287
|
+
const now = window.performance.now();
|
|
2288
|
+
const starting = status === "loading" && phase === "source";
|
|
2289
|
+
const startedAt = starting ? now : previous?.startedAt ?? now;
|
|
2290
|
+
const diagnostic = Object.freeze({
|
|
2291
|
+
artifactId,
|
|
2292
|
+
status,
|
|
2293
|
+
phase,
|
|
2294
|
+
startedAt,
|
|
2295
|
+
elapsedMs: Math.max(0, Math.round(now - startedAt)),
|
|
2296
|
+
attempt: (previous?.attempt ?? 0) + (starting ? 1 : 0),
|
|
2297
|
+
transport: sourceTransport,
|
|
2298
|
+
...failure
|
|
2299
|
+
});
|
|
2300
|
+
frameDiagnostics.set(artifactId, diagnostic);
|
|
2301
|
+
if (!disposed) {
|
|
2302
|
+
emit(root, window, ARTIFACT_STAGE_EVENTS.frameState, diagnostic);
|
|
2303
|
+
if (artifactId === state.activeArtifactId) render();
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
function releaseFrame(artifactId, {
|
|
2307
|
+
status = "unloaded",
|
|
2308
|
+
error = cancelledFrame(),
|
|
2309
|
+
failure
|
|
2310
|
+
} = {}) {
|
|
2311
|
+
const record2 = frameLoads.get(artifactId);
|
|
2312
|
+
if (!record2) return;
|
|
2313
|
+
frameLoads.delete(artifactId);
|
|
2314
|
+
record2.cancelled = true;
|
|
2315
|
+
record2.abort.abort();
|
|
2316
|
+
record2.unlisten?.();
|
|
2317
|
+
record2.detach?.();
|
|
2318
|
+
record2.reject(error);
|
|
2319
|
+
const frame = frames.get(artifactId);
|
|
2320
|
+
const slot = frameSlots.get(artifactId);
|
|
2321
|
+
if (slot && frame.parentNode === slot.host && !slot.placeholder.parentNode) {
|
|
2322
|
+
slot.host.replaceChild(slot.placeholder, frame);
|
|
2323
|
+
}
|
|
2324
|
+
frame.removeAttribute("srcdoc");
|
|
2325
|
+
frame.removeAttribute("src");
|
|
2326
|
+
delete frame.dataset.planrArtifactDigest;
|
|
2327
|
+
delete frame.dataset.planrBridgeTrusted;
|
|
2328
|
+
try {
|
|
2329
|
+
delete frame.__openPlanrBridge;
|
|
2330
|
+
} catch {
|
|
2331
|
+
}
|
|
2332
|
+
if (record2.sourceUrl) window.URL.revokeObjectURL(record2.sourceUrl);
|
|
2333
|
+
documentLayouts.delete(artifactId);
|
|
2334
|
+
const panel = panels.get(artifactId);
|
|
2335
|
+
if (panel) {
|
|
2336
|
+
delete panel.dataset.planrLayoutMeasured;
|
|
2337
|
+
panel.style.removeProperty("--planr-document-width");
|
|
2338
|
+
panel.style.removeProperty("--planr-document-height");
|
|
2339
|
+
}
|
|
2340
|
+
frameStatus(artifactId, status, status, failure);
|
|
2341
|
+
}
|
|
2342
|
+
function assignArtifactSource(artifact) {
|
|
2343
|
+
if (disposed) return Promise.reject(cancelledFrame());
|
|
2344
|
+
const frame = frames.get(artifact.id);
|
|
2345
|
+
if (!frame) return Promise.reject(new Error(`Missing artifact frame: ${artifact.id}`));
|
|
2346
|
+
const slot = frameSlots.get(artifact.id);
|
|
2347
|
+
const existing = frameLoads.get(artifact.id);
|
|
2348
|
+
if (slot && (!slot.host.isConnected || !root.contains(slot.host) || existing?.status === "ready" && frame.parentNode !== slot.host)) {
|
|
2349
|
+
releaseFrame(artifact.id);
|
|
2350
|
+
return Promise.reject(cancelledFrame());
|
|
2351
|
+
}
|
|
2352
|
+
if (existing) {
|
|
2353
|
+
if (existing.status === "ready" && existing.requireTrust && frames.get(artifact.id)?.dataset.planrBridgeTrusted !== "true") {
|
|
2354
|
+
releaseFrame(artifact.id);
|
|
2355
|
+
} else {
|
|
2356
|
+
existing.used = ++frameUse;
|
|
2357
|
+
return existing.promise;
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
const record2 = {
|
|
2361
|
+
status: "loading",
|
|
2362
|
+
used: ++frameUse,
|
|
2363
|
+
cancelled: false,
|
|
2364
|
+
abort: typeof window.AbortController === "function" ? new window.AbortController() : { signal: void 0, abort() {
|
|
2365
|
+
} }
|
|
2366
|
+
};
|
|
2367
|
+
record2.promise = new Promise((resolve, reject) => {
|
|
2368
|
+
record2.resolve = resolve;
|
|
2369
|
+
record2.reject = reject;
|
|
2370
|
+
});
|
|
2371
|
+
frameLoads.set(artifact.id, record2);
|
|
2372
|
+
const current = () => !disposed && !record2.cancelled && frameLoads.get(artifact.id) === record2;
|
|
2373
|
+
const fail = (error) => {
|
|
2374
|
+
if (current())
|
|
2375
|
+
releaseFrame(artifact.id, {
|
|
2376
|
+
status: "error",
|
|
2377
|
+
error,
|
|
2378
|
+
failure: {
|
|
2379
|
+
failedPhase: frameDiagnostics.get(artifact.id)?.phase ?? "source",
|
|
2380
|
+
code: error?.code ?? "E_ARTIFACT_FRAME_LOAD"
|
|
2381
|
+
}
|
|
2382
|
+
});
|
|
2383
|
+
};
|
|
2384
|
+
const timer = window.setTimeout(() => {
|
|
2385
|
+
const error = new Error(`Artifact frame did not finish loading: ${artifact.id}`);
|
|
2386
|
+
error.code = "E_ARTIFACT_FRAME_TIMEOUT";
|
|
2387
|
+
fail(error);
|
|
2388
|
+
}, frameLoadTimeoutMs);
|
|
2389
|
+
let loaded = false;
|
|
2390
|
+
const ready = () => {
|
|
2391
|
+
if (current() && slot && (!slot.host.isConnected || !root.contains(slot.host) || frame.parentNode !== slot.host)) {
|
|
2392
|
+
fail(cancelledFrame());
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
if (!current() || !loaded || record2.requireTrust && frame.dataset.planrBridgeTrusted !== "true")
|
|
2396
|
+
return;
|
|
2397
|
+
record2.status = "ready";
|
|
2398
|
+
record2.unlisten();
|
|
2399
|
+
frameStatus(artifact.id, "ready", "ready");
|
|
2400
|
+
record2.resolve(artifact.id);
|
|
2401
|
+
};
|
|
2402
|
+
const onLoad = () => {
|
|
2403
|
+
loaded = true;
|
|
2404
|
+
if (record2.requireTrust && frame.dataset.planrBridgeTrusted !== "true") {
|
|
2405
|
+
frameStatus(artifact.id, "loading", "bridge");
|
|
2406
|
+
}
|
|
2407
|
+
ready();
|
|
2408
|
+
};
|
|
2409
|
+
const onError = () => fail(new Error(`Artifact frame failed: ${artifact.id}`));
|
|
2410
|
+
record2.unlisten = () => {
|
|
2411
|
+
window.clearTimeout(timer);
|
|
2412
|
+
frame.removeEventListener("load", onLoad);
|
|
2413
|
+
frame.removeEventListener("error", onError);
|
|
2414
|
+
frame.removeEventListener("planr:artifact-bridge-ready", ready);
|
|
2415
|
+
};
|
|
2416
|
+
frameStatus(artifact.id, "loading", "source");
|
|
2417
|
+
if (!current()) return record2.promise;
|
|
2418
|
+
void (async () => {
|
|
2419
|
+
const source = await resolveArtifactSource(artifact, {
|
|
2420
|
+
frame,
|
|
2421
|
+
getState: () => state,
|
|
2422
|
+
signal: record2.abort.signal
|
|
2423
|
+
});
|
|
2424
|
+
if (!current()) return;
|
|
2425
|
+
if (typeof window.TextDecoder !== "function") {
|
|
2426
|
+
const error = new Error("UTF-8 decoding support is required for artifact sources.");
|
|
2427
|
+
error.code = "E_ARTIFACT_BROWSER_UNSUPPORTED";
|
|
2428
|
+
throw error;
|
|
2429
|
+
}
|
|
2430
|
+
const html = await htmlForSource(window, source);
|
|
2431
|
+
if (!current()) return;
|
|
2432
|
+
if (!html) {
|
|
2433
|
+
throw new TypeError(
|
|
2434
|
+
`Artifact source resolver must return HTML bytes or a Blob for ${artifact.id}.`
|
|
2435
|
+
);
|
|
2436
|
+
}
|
|
2437
|
+
if (slot) {
|
|
2438
|
+
if (!slot.host.isConnected || !root.contains(slot.host) || slot.placeholder.parentNode !== slot.host || frame.parentNode) {
|
|
2439
|
+
throw cancelledFrame();
|
|
2440
|
+
}
|
|
2441
|
+
slot.host.replaceChild(frame, slot.placeholder);
|
|
2442
|
+
}
|
|
2443
|
+
if (typeof bridgeClient?.attach === "function") {
|
|
2444
|
+
const detach = bridgeClient.attach({
|
|
2445
|
+
artifact,
|
|
2446
|
+
frame,
|
|
2447
|
+
getState: () => state
|
|
2448
|
+
});
|
|
2449
|
+
if (typeof detach === "function") record2.detach = detach;
|
|
2450
|
+
if (!current()) {
|
|
2451
|
+
record2.detach?.();
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
record2.requireTrust = true;
|
|
2455
|
+
}
|
|
2456
|
+
frame.addEventListener("load", onLoad);
|
|
2457
|
+
frame.addEventListener("error", onError);
|
|
2458
|
+
frame.addEventListener("planr:artifact-bridge-ready", ready);
|
|
2459
|
+
frame.dataset.planrArtifactDigest = artifact.sha256;
|
|
2460
|
+
frameStatus(artifact.id, "loading", "document");
|
|
2461
|
+
if (!current()) return;
|
|
2462
|
+
if (slot && (!slot.host.isConnected || !root.contains(slot.host) || frame.parentNode !== slot.host))
|
|
2463
|
+
throw cancelledFrame();
|
|
2464
|
+
if (sourceTransport === "srcdoc") {
|
|
2465
|
+
frame.removeAttribute("src");
|
|
2466
|
+
frame.srcdoc = html;
|
|
2467
|
+
} else {
|
|
2468
|
+
if (typeof window.URL?.createObjectURL !== "function" || typeof window.Blob !== "function") {
|
|
2469
|
+
const error = new Error("Blob URL support is required for artifact sources.");
|
|
2470
|
+
error.code = "E_ARTIFACT_BROWSER_UNSUPPORTED";
|
|
2471
|
+
throw error;
|
|
2472
|
+
}
|
|
2473
|
+
const sourceUrl = window.URL.createObjectURL(
|
|
2474
|
+
new window.Blob([html], {
|
|
2475
|
+
type: "text/html;charset=utf-8"
|
|
2476
|
+
})
|
|
2477
|
+
);
|
|
2478
|
+
record2.sourceUrl = sourceUrl;
|
|
2479
|
+
frame.removeAttribute("srcdoc");
|
|
2480
|
+
frame.src = sourceUrl;
|
|
2481
|
+
}
|
|
2482
|
+
})().catch(fail);
|
|
2483
|
+
return record2.promise;
|
|
2484
|
+
}
|
|
2485
|
+
function ensureFrames(artifactIds, isCurrent = () => true) {
|
|
2486
|
+
if (disposed || !isCurrent()) return Promise.reject(cancelledFrame());
|
|
2487
|
+
if (!Array.isArray(artifactIds) || artifactIds.some((id7) => typeof id7 !== "string" || !frames.has(id7))) {
|
|
2488
|
+
return Promise.reject(new TypeError("Requested artifact frames must be known artifact IDs."));
|
|
2489
|
+
}
|
|
2490
|
+
const requested = [...new Set(artifactIds)];
|
|
2491
|
+
if (frameBudget !== null && requested.length > frameBudget) {
|
|
2492
|
+
return Promise.reject(
|
|
2493
|
+
new RangeError(`At most ${frameBudget} artifact frames may be requested together.`)
|
|
2494
|
+
);
|
|
2495
|
+
}
|
|
2496
|
+
if (frameBudget === null)
|
|
2497
|
+
return Promise.all(requested.map((id7) => assignArtifactSource(stageArtifactById(state, id7))));
|
|
2498
|
+
const run = async () => {
|
|
2499
|
+
if (disposed || !isCurrent()) throw cancelledFrame();
|
|
2500
|
+
for (const id7 of requested) {
|
|
2501
|
+
if (disposed || !isCurrent()) throw cancelledFrame();
|
|
2502
|
+
if (!frameLoads.has(id7)) {
|
|
2503
|
+
while (frameLoads.size >= frameBudget) {
|
|
2504
|
+
const candidates = [...frameLoads].filter(([loadedId]) => !requested.includes(loadedId)).sort((a, b) => a[1].used - b[1].used);
|
|
2505
|
+
if (!candidates.length) throw new Error("The artifact frame budget is exhausted.");
|
|
2506
|
+
releaseFrame(candidates[0][0]);
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
await assignArtifactSource(stageArtifactById(state, id7));
|
|
2510
|
+
}
|
|
2511
|
+
return requested;
|
|
2512
|
+
};
|
|
2513
|
+
const result = frameQueue.then(run);
|
|
2514
|
+
frameQueue = result.catch(() => {
|
|
2515
|
+
});
|
|
2516
|
+
return result;
|
|
2517
|
+
}
|
|
2518
|
+
function frameIsReady(id7) {
|
|
2519
|
+
const frame = frames.get(id7);
|
|
2520
|
+
return frame?.dataset.planrFrameState === "ready" && (!frame.__openPlanrBridge || frame.dataset.planrBridgeTrusted === "true");
|
|
2521
|
+
}
|
|
2522
|
+
function setSelectionStatus(status, selection, generation) {
|
|
2523
|
+
if (disposed || generation !== activationGeneration || visibleArtifactIds(state).join("|") !== selection)
|
|
2524
|
+
return;
|
|
2525
|
+
state = reduceArtifactStageState(state, { type: "set-status", status });
|
|
2526
|
+
render({ announce: true });
|
|
2527
|
+
}
|
|
2528
|
+
function cancelInactiveLoads(ids2) {
|
|
2529
|
+
for (const [id7, record2] of frameLoads) {
|
|
2530
|
+
if (record2.status === "loading" && !ids2.includes(id7)) releaseFrame(id7);
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
async function activateVisibleFrames() {
|
|
2534
|
+
const generation = ++activationGeneration;
|
|
2535
|
+
const isCurrent = () => generation === activationGeneration;
|
|
2536
|
+
const ids2 = visibleArtifactIds(state);
|
|
2537
|
+
const selection = ids2.join("|");
|
|
2538
|
+
if (ids2.length === 0) return state;
|
|
2539
|
+
cancelInactiveLoads(ids2);
|
|
2540
|
+
if (ids2.every(frameIsReady)) {
|
|
2541
|
+
setSelectionStatus("ready", selection, generation);
|
|
2542
|
+
return state;
|
|
2543
|
+
}
|
|
2544
|
+
setSelectionStatus("loading", selection, generation);
|
|
2545
|
+
try {
|
|
2546
|
+
await ensureFrames(ids2, isCurrent);
|
|
2547
|
+
setSelectionStatus("ready", selection, generation);
|
|
2548
|
+
} catch (error) {
|
|
2549
|
+
const status = error?.code === "E_ARTIFACT_BROWSER_UNSUPPORTED" ? "unsupported-browser" : "invalid";
|
|
2550
|
+
setSelectionStatus(status, selection, generation);
|
|
2551
|
+
}
|
|
2552
|
+
return state;
|
|
2553
|
+
}
|
|
2554
|
+
function retryFrames(artifactIds = visibleArtifactIds(state)) {
|
|
2555
|
+
if (!Array.isArray(artifactIds) || artifactIds.some((id7) => !frames.has(id7))) {
|
|
2556
|
+
return Promise.reject(new TypeError("Requested artifact frames must be known artifact IDs."));
|
|
2557
|
+
}
|
|
2558
|
+
for (const id7 of artifactIds) releaseFrame(id7);
|
|
2559
|
+
if (artifactIds.some((id7) => visibleArtifactIds(state).includes(id7))) {
|
|
2560
|
+
readyPromise = activateVisibleFrames();
|
|
2561
|
+
return readyPromise;
|
|
2562
|
+
}
|
|
2563
|
+
return ensureFrames(artifactIds);
|
|
2564
|
+
}
|
|
2565
|
+
if (state.status === "ready" && state.artifacts.length > 0) {
|
|
2566
|
+
if (typeof resolveArtifactSource !== "function") {
|
|
2567
|
+
frameStatus(state.activeArtifactId, "loading", "source");
|
|
2568
|
+
frameStatus(state.activeArtifactId, "error", "error", {
|
|
2569
|
+
failedPhase: "source",
|
|
2570
|
+
code: "E_ARTIFACT_SOURCE_UNAVAILABLE"
|
|
2571
|
+
});
|
|
2572
|
+
state = reduceArtifactStageState(state, { type: "set-status", status: "invalid" });
|
|
2573
|
+
render();
|
|
2574
|
+
readyPromise = Promise.resolve(state);
|
|
2575
|
+
} else {
|
|
2576
|
+
state = reduceArtifactStageState(state, { type: "set-status", status: "loading" });
|
|
2577
|
+
render();
|
|
2578
|
+
readyPromise = activateVisibleFrames();
|
|
2579
|
+
if (frameBudget === null) {
|
|
2580
|
+
void Promise.allSettled(
|
|
2581
|
+
state.artifacts.filter(({ id: id7 }) => id7 !== state.activeArtifactId).map(assignArtifactSource)
|
|
2582
|
+
);
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
return controller;
|
|
2587
|
+
}
|
|
2588
|
+
function bootstrapArtifactStage(document2 = globalThis.document, options = {}) {
|
|
2589
|
+
return mountArtifactStage({ ...options, document: document2, window: document2?.defaultView });
|
|
2590
|
+
}
|
|
2591
|
+
if (typeof document !== "undefined") {
|
|
2592
|
+
const options = globalThis.__OPENPLANR_ARTIFACT_STAGE_OPTIONS__ ?? {};
|
|
2593
|
+
if (document.readyState === "loading") {
|
|
2594
|
+
document.addEventListener("DOMContentLoaded", () => bootstrapArtifactStage(document, options), {
|
|
2595
|
+
once: true
|
|
2596
|
+
});
|
|
2597
|
+
} else {
|
|
2598
|
+
queueMicrotask(() => bootstrapArtifactStage(document, options));
|
|
2599
|
+
}
|
|
2600
|
+
}
|