openplanr 3.2.0 → 3.4.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 +73 -0
- package/dist/cli/commands/artifact-design-share.d.ts +9 -0
- package/dist/cli/commands/artifact-design-share.d.ts.map +1 -0
- package/dist/cli/commands/artifact-design-share.js +200 -0
- package/dist/cli/commands/artifact-design-share.js.map +1 -0
- package/dist/cli/commands/artifact.d.ts.map +1 -1
- package/dist/cli/commands/artifact.js +5 -18
- package/dist/cli/commands/artifact.js.map +1 -1
- package/dist/cli/commands/ship.d.ts.map +1 -1
- package/dist/cli/commands/ship.js +2 -0
- package/dist/cli/commands/ship.js.map +1 -1
- package/dist/cli/commands/spec.d.ts +2 -1
- package/dist/cli/commands/spec.d.ts.map +1 -1
- package/dist/cli/commands/spec.js +45 -5
- package/dist/cli/commands/spec.js.map +1 -1
- package/dist/dashboard/.vite/manifest.json +2 -2
- package/dist/dashboard/assets/index-Cv4Vwhuw.css +2 -0
- package/dist/dashboard/assets/index-CvQAlBy3.js +75 -0
- package/dist/dashboard/assets/index-CvQAlBy3.js.map +1 -0
- package/dist/dashboard/dashboard-manifest.json +14 -14
- package/dist/dashboard/index.html +2 -2
- package/dist/services/claude-plugin-service.d.ts +5 -0
- package/dist/services/claude-plugin-service.d.ts.map +1 -1
- package/dist/services/claude-plugin-service.js +18 -2
- package/dist/services/claude-plugin-service.js.map +1 -1
- package/dist/services/codex-plugin-service.d.ts.map +1 -1
- package/dist/services/codex-plugin-service.js +11 -3
- package/dist/services/codex-plugin-service.js.map +1 -1
- package/dist/services/design-artifact-service.d.ts +20 -1
- package/dist/services/design-artifact-service.d.ts.map +1 -1
- package/dist/services/design-artifact-service.js +137 -18
- package/dist/services/design-artifact-service.js.map +1 -1
- package/dist/services/runtime-manager/codex-discovery.d.ts +1 -3
- package/dist/services/runtime-manager/codex-discovery.d.ts.map +1 -1
- package/dist/services/runtime-manager/codex-discovery.js +3 -16
- package/dist/services/runtime-manager/codex-discovery.js.map +1 -1
- package/dist/services/runtime-manager/doctor.d.ts +25 -0
- package/dist/services/runtime-manager/doctor.d.ts.map +1 -1
- package/dist/services/runtime-manager/doctor.js +480 -0
- package/dist/services/runtime-manager/doctor.js.map +1 -1
- package/dist/services/runtime-manager/errors.d.ts +12 -0
- package/dist/services/runtime-manager/errors.d.ts.map +1 -0
- package/dist/services/runtime-manager/errors.js +14 -0
- package/dist/services/runtime-manager/errors.js.map +1 -0
- package/dist/services/runtime-manager/global-state.d.ts +7 -0
- package/dist/services/runtime-manager/global-state.d.ts.map +1 -1
- package/dist/services/runtime-manager/global-state.js +116 -0
- package/dist/services/runtime-manager/global-state.js.map +1 -1
- package/dist/services/runtime-manager/host-version-probe.d.ts +8 -0
- package/dist/services/runtime-manager/host-version-probe.d.ts.map +1 -0
- package/dist/services/runtime-manager/host-version-probe.js +14 -0
- package/dist/services/runtime-manager/host-version-probe.js.map +1 -0
- package/dist/services/runtime-manager/install-plan.d.ts +25 -0
- package/dist/services/runtime-manager/install-plan.d.ts.map +1 -1
- package/dist/services/runtime-manager/install-plan.js +305 -0
- package/dist/services/runtime-manager/install-plan.js.map +1 -1
- package/dist/services/runtime-manager/inventory.d.ts +6 -0
- package/dist/services/runtime-manager/inventory.d.ts.map +1 -1
- package/dist/services/runtime-manager/inventory.js +16 -1
- package/dist/services/runtime-manager/inventory.js.map +1 -1
- package/dist/services/runtime-manager/legacy-adapters.d.ts +9 -0
- package/dist/services/runtime-manager/legacy-adapters.d.ts.map +1 -0
- package/dist/services/runtime-manager/legacy-adapters.js +187 -0
- package/dist/services/runtime-manager/legacy-adapters.js.map +1 -0
- package/dist/services/runtime-manager/paths.d.ts +18 -0
- package/dist/services/runtime-manager/paths.d.ts.map +1 -0
- package/dist/services/runtime-manager/paths.js +83 -0
- package/dist/services/runtime-manager/paths.js.map +1 -0
- package/dist/services/runtime-manager/runtime-package.d.ts +21 -0
- package/dist/services/runtime-manager/runtime-package.d.ts.map +1 -1
- package/dist/services/runtime-manager/runtime-package.js +77 -0
- package/dist/services/runtime-manager/runtime-package.js.map +1 -1
- package/dist/services/runtime-manager/storage.d.ts +47 -0
- package/dist/services/runtime-manager/storage.d.ts.map +1 -0
- package/dist/services/runtime-manager/storage.js +604 -0
- package/dist/services/runtime-manager/storage.js.map +1 -0
- package/dist/services/runtime-manager-service.d.ts +7 -17
- package/dist/services/runtime-manager-service.d.ts.map +1 -1
- package/dist/services/runtime-manager-service.js +32 -1695
- package/dist/services/runtime-manager-service.js.map +1 -1
- package/dist/services/ship-checks/contract.d.ts +15 -1
- package/dist/services/ship-checks/contract.d.ts.map +1 -1
- package/dist/services/ship-checks/contract.js +1 -1
- package/dist/services/ship-checks/contract.js.map +1 -1
- package/dist/services/ship-checks/coverage.d.ts +4 -0
- package/dist/services/ship-checks/coverage.d.ts.map +1 -0
- package/dist/services/ship-checks/coverage.js +113 -0
- package/dist/services/ship-checks/coverage.js.map +1 -0
- package/dist/services/ship-checks/discover.d.ts.map +1 -1
- package/dist/services/ship-checks/discover.js +5 -1
- package/dist/services/ship-checks/discover.js.map +1 -1
- package/dist/services/ship-checks/package-contents.d.ts +4 -0
- package/dist/services/ship-checks/package-contents.d.ts.map +1 -0
- package/dist/services/ship-checks/package-contents.js +91 -0
- package/dist/services/ship-checks/package-contents.js.map +1 -0
- package/dist/services/ship-checks/packages.d.ts +2 -0
- package/dist/services/ship-checks/packages.d.ts.map +1 -1
- package/dist/services/ship-checks/packages.js +4 -0
- package/dist/services/ship-checks/packages.js.map +1 -1
- package/dist/services/ship-checks/scope.js +2 -1
- package/dist/services/ship-checks/scope.js.map +1 -1
- package/dist/services/spec-service.d.ts +18 -0
- package/dist/services/spec-service.d.ts.map +1 -1
- package/dist/services/spec-service.js +174 -9
- package/dist/services/spec-service.js.map +1 -1
- package/dist/templates/spec/spec-shaped.md.hbs +1 -1
- package/dist/templates/spec/spec.md.hbs +2 -2
- package/dist/utils/splice-managed-block.js +1 -1
- package/dist/utils/splice-managed-block.js.map +1 -1
- package/docs/CLI.md +86 -2
- package/lib/host-packages/adapter-registry.json +5 -5
- package/lib/host-packages/capability-map.json +3 -3
- package/lib/host-packages/claude/.claude-plugin/marketplace.json +3 -3
- package/lib/host-packages/claude/openplanr/.claude-plugin/plugin.json +6 -6
- package/lib/host-packages/claude/openplanr/.openplanr-content.json +94 -146
- package/lib/host-packages/claude/openplanr/README.md +12 -12
- package/lib/host-packages/claude/openplanr/agents/planr-qa.md +5 -3
- package/lib/host-packages/claude/openplanr/references/agents/shared/modes/shared/task-context-resolution.md +4 -4
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-artifact-shell.mjs +2 -4
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-document.mjs +110 -25
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-handoff.mjs +851 -57
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-review-state-store.mjs +45 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-review.mjs +65 -74
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-runtime-home.mjs +0 -1
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-share-cli-bridge.mjs +321 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-shared-artifact-support-dependencies-design-support-protocol-contracts-d707e49b.mjs +1652 -0
- package/lib/host-packages/{openai/openplanr/runtime/design/scripts/design-shared-artifact-support-protocol-contracts-ea2cd15e.mjs → claude/openplanr/runtime/design/scripts/design-shared-artifact-support-protocol-contracts-9f6918b0.mjs} +537 -5
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-shared-design-support-0bac378d.mjs +137 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design.mjs +374 -61
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/notices/THIRD_PARTY_NOTICES.txt +32 -59
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/02-artifact-review-ui.js +138 -133
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/03-protocol-contracts.js +1 -5608
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/04-artifact-review-ui.js +2568 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources.json +10 -74
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/package.json +4 -4
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/share.js +8 -2
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/02-react.js +2 -2
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/09-react-dom-client-public-roots.js +5 -5
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/12-protocol-contracts.js +72 -2
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/13-design-review-experience.js +14 -7
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/15-canvas-engine.js +71 -34
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/21-design-studio.js +28 -14
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources.json +12 -12
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/package.json +6 -2
- package/lib/host-packages/{openai/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.20.0 → claude/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.22.0}/design-document.schema.json +131 -4
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.22.0/design-review-bundle.schema.json +822 -0
- package/lib/host-packages/claude/openplanr/runtime/plan/scripts/design-protocol-contracts.mjs +616 -4775
- package/lib/host-packages/claude/openplanr/runtime/plan/scripts/design.mjs +219 -424
- package/lib/host-packages/claude/openplanr/runtime/plan/scripts/runtime/packages/protocol/package.json +6 -2
- package/lib/host-packages/claude/openplanr/skills/delegate/SKILL.md +2 -1
- package/lib/host-packages/claude/openplanr/skills/delegate/references/integration-review.md +26 -20
- package/lib/host-packages/claude/openplanr/skills/delegate/references/operator-guide.md +25 -1
- package/lib/host-packages/claude/openplanr/skills/delegate/references/run-handoff.md +2 -2
- package/lib/host-packages/claude/openplanr/skills/delegate/references/worktree-custody.md +17 -9
- package/lib/host-packages/claude/openplanr/skills/delegate/scripts/adapters/claude.mjs +15 -3
- package/lib/host-packages/claude/openplanr/skills/delegate/scripts/adapters/codex.mjs +38 -2
- package/lib/host-packages/claude/openplanr/skills/delegate/scripts/adapters/native.mjs +5 -0
- package/lib/host-packages/claude/openplanr/skills/delegate/scripts/custody.mjs +217 -10
- package/lib/host-packages/claude/openplanr/skills/delegate/scripts/handoff.mjs +1080 -0
- package/lib/host-packages/claude/openplanr/skills/delegate/scripts/integrate.mjs +96 -201
- package/lib/host-packages/claude/openplanr/skills/delegate/scripts/integration-files.mjs +3 -382
- package/lib/host-packages/claude/openplanr/skills/delegate/scripts/presentation.mjs +29 -12
- package/lib/host-packages/claude/openplanr/skills/delegate/scripts/run-contract.mjs +11 -0
- package/lib/host-packages/claude/openplanr/skills/delegate/scripts/run-execution.mjs +1 -1
- package/lib/host-packages/claude/openplanr/skills/delegate/scripts/run-lifecycle.mjs +14 -10
- package/lib/host-packages/claude/openplanr/skills/delegate/scripts/run-preparation.mjs +4 -0
- package/lib/host-packages/claude/openplanr/skills/delegate/scripts/run-record.mjs +2 -2
- package/lib/host-packages/claude/openplanr/skills/design/references/design-system.md +17 -2
- package/lib/host-packages/claude/openplanr/skills/design/references/generation.md +60 -2
- package/lib/host-packages/claude/openplanr/skills/design/references/utilities.md +23 -7
- package/lib/host-packages/claude/openplanr/skills/design/schemas/design-document.schema.json +131 -4
- package/lib/host-packages/claude/openplanr/skills/design-loop/references/design-system.md +17 -2
- package/lib/host-packages/claude/openplanr/skills/design-loop/references/generation.md +60 -2
- package/lib/host-packages/claude/openplanr/skills/design-loop/references/utilities.md +23 -7
- package/lib/host-packages/claude/openplanr/skills/design-loop/schemas/design-document.schema.json +131 -4
- package/lib/host-packages/claude/openplanr/skills/design-review/references/design-system.md +17 -2
- package/lib/host-packages/claude/openplanr/skills/design-review/references/generation.md +60 -2
- package/lib/host-packages/claude/openplanr/skills/design-review/references/utilities.md +23 -7
- package/lib/host-packages/claude/openplanr/skills/design-review/schemas/design-document.schema.json +131 -4
- package/lib/host-packages/claude/openplanr/skills/investigate/SKILL.md +2 -1
- package/lib/host-packages/claude/openplanr/skills/investigate/references/fail-first.md +40 -0
- package/lib/host-packages/claude/openplanr/skills/plan/SKILL.md +21 -11
- package/lib/host-packages/claude/openplanr/skills/plan/schemas/design-document.schema.json +131 -4
- package/lib/host-packages/claude/openplanr/skills/ship/SKILL.md +51 -18
- package/lib/host-packages/claude/openplanr/skills/ship/references/fail-first.md +40 -0
- package/lib/host-packages/claude/openplanr/skills/ship/references/result-contract.md +7 -3
- package/lib/host-packages/claude/openplanr/skills/spec/SKILL.md +6 -0
- package/lib/host-packages/claude/openplanr/skills/spec/references/specification-contract.md +3 -1
- package/lib/host-packages/cursor/openplanr/.openplanr-content.json +91 -143
- package/lib/host-packages/cursor/openplanr/manifest.json +1 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-delegate/references/integration-review.md +26 -20
- package/lib/host-packages/cursor/openplanr/rules/planr-delegate/references/operator-guide.md +25 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-delegate/references/run-handoff.md +2 -2
- package/lib/host-packages/cursor/openplanr/rules/planr-delegate/references/worktree-custody.md +17 -9
- package/lib/host-packages/cursor/openplanr/rules/planr-delegate/scripts/adapters/claude.mjs +15 -3
- package/lib/host-packages/cursor/openplanr/rules/planr-delegate/scripts/adapters/codex.mjs +38 -2
- package/lib/host-packages/cursor/openplanr/rules/planr-delegate/scripts/adapters/native.mjs +5 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-delegate/scripts/custody.mjs +217 -10
- package/lib/host-packages/cursor/openplanr/rules/planr-delegate/scripts/handoff.mjs +1080 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-delegate/scripts/integrate.mjs +96 -201
- package/lib/host-packages/cursor/openplanr/rules/planr-delegate/scripts/integration-files.mjs +3 -382
- package/lib/host-packages/cursor/openplanr/rules/planr-delegate/scripts/presentation.mjs +29 -12
- package/lib/host-packages/cursor/openplanr/rules/planr-delegate/scripts/run-contract.mjs +11 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-delegate/scripts/run-execution.mjs +1 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-delegate/scripts/run-lifecycle.mjs +14 -10
- package/lib/host-packages/cursor/openplanr/rules/planr-delegate/scripts/run-preparation.mjs +4 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-delegate/scripts/run-record.mjs +2 -2
- package/lib/host-packages/cursor/openplanr/rules/planr-delegate.mdc +2 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-design/references/design-system.md +17 -2
- package/lib/host-packages/cursor/openplanr/rules/planr-design/references/generation.md +60 -2
- package/lib/host-packages/cursor/openplanr/rules/planr-design/references/utilities.md +23 -7
- package/lib/host-packages/cursor/openplanr/rules/planr-design/schemas/design-document.schema.json +131 -4
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/references/design-system.md +17 -2
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/references/generation.md +60 -2
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/references/utilities.md +23 -7
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/schemas/design-document.schema.json +131 -4
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/references/design-system.md +17 -2
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/references/generation.md +60 -2
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/references/utilities.md +23 -7
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/schemas/design-document.schema.json +131 -4
- package/lib/host-packages/cursor/openplanr/rules/planr-investigate/references/fail-first.md +40 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-investigate.mdc +2 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/schemas/design-document.schema.json +131 -4
- package/lib/host-packages/cursor/openplanr/rules/planr-plan.mdc +21 -11
- package/lib/host-packages/cursor/openplanr/rules/planr-ship/references/fail-first.md +40 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-ship/references/result-contract.md +7 -3
- package/lib/host-packages/cursor/openplanr/rules/planr-ship.mdc +51 -18
- package/lib/host-packages/cursor/openplanr/rules/planr-spec/references/specification-contract.md +3 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-spec.mdc +6 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-artifact-shell.mjs +2 -4
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-document.mjs +110 -25
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-handoff.mjs +851 -57
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-review-state-store.mjs +45 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-review.mjs +65 -74
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-runtime-home.mjs +0 -1
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-share-cli-bridge.mjs +321 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-shared-artifact-support-dependencies-design-support-protocol-contracts-d707e49b.mjs +1652 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/{design-shared-artifact-support-protocol-contracts-ea2cd15e.mjs → design-shared-artifact-support-protocol-contracts-9f6918b0.mjs} +537 -5
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-shared-design-support-0bac378d.mjs +137 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design.mjs +374 -61
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/notices/THIRD_PARTY_NOTICES.txt +32 -59
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/02-artifact-review-ui.js +138 -133
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/03-protocol-contracts.js +1 -5608
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/04-artifact-review-ui.js +2568 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources.json +10 -74
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/package.json +4 -4
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/share.js +8 -2
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/02-react.js +2 -2
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/09-react-dom-client-public-roots.js +5 -5
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/12-protocol-contracts.js +72 -2
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/13-design-review-experience.js +14 -7
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/15-canvas-engine.js +71 -34
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/21-design-studio.js +28 -14
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources.json +12 -12
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/package.json +6 -2
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/{v1.20.0 → v1.22.0}/design-document.schema.json +131 -4
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.22.0/design-review-bundle.schema.json +822 -0
- package/lib/host-packages/cursor/openplanr/runtime/plan/scripts/design-protocol-contracts.mjs +616 -4775
- package/lib/host-packages/cursor/openplanr/runtime/plan/scripts/design.mjs +219 -424
- package/lib/host-packages/cursor/openplanr/runtime/plan/scripts/runtime/packages/protocol/package.json +6 -2
- package/lib/host-packages/openai/.claude-plugin/marketplace.json +3 -3
- package/lib/host-packages/openai/openplanr/.codex-plugin/plugin.json +2 -2
- package/lib/host-packages/openai/openplanr/.openplanr-content.json +98 -150
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-artifact-shell.mjs +2 -4
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-document.mjs +110 -25
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-handoff.mjs +851 -57
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-review-state-store.mjs +45 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-review.mjs +65 -74
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-runtime-home.mjs +0 -1
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-share-cli-bridge.mjs +321 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-shared-artifact-support-dependencies-design-support-protocol-contracts-d707e49b.mjs +1652 -0
- package/lib/host-packages/{claude/openplanr/runtime/design/scripts/design-shared-artifact-support-protocol-contracts-ea2cd15e.mjs → openai/openplanr/runtime/design/scripts/design-shared-artifact-support-protocol-contracts-9f6918b0.mjs} +537 -5
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-shared-design-support-0bac378d.mjs +137 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design.mjs +374 -61
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/notices/THIRD_PARTY_NOTICES.txt +32 -59
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/02-artifact-review-ui.js +138 -133
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/03-protocol-contracts.js +1 -5608
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/04-artifact-review-ui.js +2568 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources.json +10 -74
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/package.json +4 -4
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/share.js +8 -2
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/02-react.js +2 -2
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/09-react-dom-client-public-roots.js +5 -5
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/12-protocol-contracts.js +72 -2
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/13-design-review-experience.js +14 -7
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/15-canvas-engine.js +71 -34
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/21-design-studio.js +28 -14
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources.json +12 -12
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/package.json +6 -2
- package/lib/host-packages/{claude/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.20.0 → openai/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.22.0}/design-document.schema.json +131 -4
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.22.0/design-review-bundle.schema.json +822 -0
- package/lib/host-packages/openai/openplanr/runtime/plan/scripts/design-protocol-contracts.mjs +616 -4775
- package/lib/host-packages/openai/openplanr/runtime/plan/scripts/design.mjs +219 -424
- package/lib/host-packages/openai/openplanr/runtime/plan/scripts/runtime/packages/protocol/package.json +6 -2
- package/lib/host-packages/openai/openplanr/skills/delegate/SKILL.md +2 -1
- package/lib/host-packages/openai/openplanr/skills/delegate/openplanr.skill.json +1 -1
- package/lib/host-packages/openai/openplanr/skills/delegate/references/integration-review.md +26 -20
- package/lib/host-packages/openai/openplanr/skills/delegate/references/operator-guide.md +25 -1
- package/lib/host-packages/openai/openplanr/skills/delegate/references/run-handoff.md +2 -2
- package/lib/host-packages/openai/openplanr/skills/delegate/references/worktree-custody.md +17 -9
- package/lib/host-packages/openai/openplanr/skills/delegate/scripts/adapters/claude.mjs +15 -3
- package/lib/host-packages/openai/openplanr/skills/delegate/scripts/adapters/codex.mjs +38 -2
- package/lib/host-packages/openai/openplanr/skills/delegate/scripts/adapters/native.mjs +5 -0
- package/lib/host-packages/openai/openplanr/skills/delegate/scripts/custody.mjs +217 -10
- package/lib/host-packages/openai/openplanr/skills/delegate/scripts/handoff.mjs +1080 -0
- package/lib/host-packages/openai/openplanr/skills/delegate/scripts/integrate.mjs +96 -201
- package/lib/host-packages/openai/openplanr/skills/delegate/scripts/integration-files.mjs +3 -382
- package/lib/host-packages/openai/openplanr/skills/delegate/scripts/presentation.mjs +29 -12
- package/lib/host-packages/openai/openplanr/skills/delegate/scripts/run-contract.mjs +11 -0
- package/lib/host-packages/openai/openplanr/skills/delegate/scripts/run-execution.mjs +1 -1
- package/lib/host-packages/openai/openplanr/skills/delegate/scripts/run-lifecycle.mjs +14 -10
- package/lib/host-packages/openai/openplanr/skills/delegate/scripts/run-preparation.mjs +4 -0
- package/lib/host-packages/openai/openplanr/skills/delegate/scripts/run-record.mjs +2 -2
- package/lib/host-packages/openai/openplanr/skills/design/references/design-system.md +17 -2
- package/lib/host-packages/openai/openplanr/skills/design/references/generation.md +60 -2
- package/lib/host-packages/openai/openplanr/skills/design/references/utilities.md +23 -7
- package/lib/host-packages/openai/openplanr/skills/design/schemas/design-document.schema.json +131 -4
- package/lib/host-packages/openai/openplanr/skills/design-loop/references/design-system.md +17 -2
- package/lib/host-packages/openai/openplanr/skills/design-loop/references/generation.md +60 -2
- package/lib/host-packages/openai/openplanr/skills/design-loop/references/utilities.md +23 -7
- package/lib/host-packages/openai/openplanr/skills/design-loop/schemas/design-document.schema.json +131 -4
- package/lib/host-packages/openai/openplanr/skills/design-review/references/design-system.md +17 -2
- package/lib/host-packages/openai/openplanr/skills/design-review/references/generation.md +60 -2
- package/lib/host-packages/openai/openplanr/skills/design-review/references/utilities.md +23 -7
- package/lib/host-packages/openai/openplanr/skills/design-review/schemas/design-document.schema.json +131 -4
- package/lib/host-packages/openai/openplanr/skills/investigate/SKILL.md +2 -1
- package/lib/host-packages/openai/openplanr/skills/investigate/agents/openai.yaml +1 -1
- package/lib/host-packages/openai/openplanr/skills/investigate/openplanr.skill.json +12 -1
- package/lib/host-packages/openai/openplanr/skills/investigate/references/fail-first.md +40 -0
- package/lib/host-packages/openai/openplanr/skills/plan/SKILL.md +21 -11
- package/lib/host-packages/openai/openplanr/skills/plan/openplanr.skill.json +1 -1
- package/lib/host-packages/openai/openplanr/skills/plan/schemas/design-document.schema.json +131 -4
- package/lib/host-packages/openai/openplanr/skills/ship/SKILL.md +51 -18
- package/lib/host-packages/openai/openplanr/skills/ship/agents/openai.yaml +1 -1
- package/lib/host-packages/openai/openplanr/skills/ship/openplanr.skill.json +12 -1
- package/lib/host-packages/openai/openplanr/skills/ship/references/fail-first.md +40 -0
- package/lib/host-packages/openai/openplanr/skills/ship/references/result-contract.md +7 -3
- package/lib/host-packages/openai/openplanr/skills/spec/SKILL.md +6 -0
- package/lib/host-packages/openai/openplanr/skills/spec/openplanr.skill.json +1 -1
- package/lib/host-packages/openai/openplanr/skills/spec/references/specification-contract.md +3 -1
- package/lib/json-schema.mjs +4 -0
- package/package.json +16 -16
- package/dist/dashboard/assets/index-BU5fzjyz.css +0 -1
- package/dist/dashboard/assets/index-L8qtwqv6.js +0 -572
- package/dist/dashboard/assets/index-L8qtwqv6.js.map +0 -1
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-chunked-workspace-client.mjs +0 -555
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-compression.mjs +0 -4180
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-encrypted-workspace-client.mjs +0 -647
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-owner-custody.mjs +0 -129
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-resource-pack.mjs +0 -698
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-runtime-support-6774fd3d5d02.mjs +0 -24
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-share.mjs +0 -1993
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-shared-artifact-support-dependencies-design-support-protocol-contracts-3d9b1a86.mjs +0 -1020
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-shared-protocol-contracts-75a938cc.mjs +0 -222
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-shared-protocol-contracts-b5f28f94.mjs +0 -5778
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-upload-spool.mjs +0 -198
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/04-compression.js +0 -4175
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/05-artifact-sharing.js +0 -65
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/06-artifact-review-ui.js +0 -1755
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/07-artifact-sharing.js +0 -62
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/08-artifact-review-ui.js +0 -2633
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.1.0/artifact-paste.schema.json +0 -49
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.9.0/design-review-workspace.schema.json +0 -108
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.9.0/design-workspace-create.schema.json +0 -170
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.9.0/design-workspace-event.schema.json +0 -97
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.9.0/design-workspace-revision.schema.json +0 -56
- package/lib/host-packages/claude/openplanr/skills/delegate/scripts/integration-transaction.mjs +0 -958
- package/lib/host-packages/cursor/openplanr/rules/planr-delegate/scripts/integration-transaction.mjs +0 -958
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-chunked-workspace-client.mjs +0 -555
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-compression.mjs +0 -4180
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-encrypted-workspace-client.mjs +0 -647
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-owner-custody.mjs +0 -129
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-resource-pack.mjs +0 -698
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-runtime-support-6774fd3d5d02.mjs +0 -24
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-share.mjs +0 -1993
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-shared-artifact-support-dependencies-design-support-protocol-contracts-3d9b1a86.mjs +0 -1020
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-shared-protocol-contracts-75a938cc.mjs +0 -222
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-shared-protocol-contracts-b5f28f94.mjs +0 -5778
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-upload-spool.mjs +0 -198
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/04-compression.js +0 -4175
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/05-artifact-sharing.js +0 -65
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/06-artifact-review-ui.js +0 -1755
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/07-artifact-sharing.js +0 -62
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/08-artifact-review-ui.js +0 -2633
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.1.0/artifact-paste.schema.json +0 -49
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.9.0/design-review-workspace.schema.json +0 -108
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.9.0/design-workspace-create.schema.json +0 -170
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.9.0/design-workspace-event.schema.json +0 -97
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.9.0/design-workspace-revision.schema.json +0 -56
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-chunked-workspace-client.mjs +0 -555
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-compression.mjs +0 -4180
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-encrypted-workspace-client.mjs +0 -647
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-owner-custody.mjs +0 -129
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-resource-pack.mjs +0 -698
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-runtime-support-6774fd3d5d02.mjs +0 -24
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-share.mjs +0 -1993
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-shared-artifact-support-dependencies-design-support-protocol-contracts-3d9b1a86.mjs +0 -1020
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-shared-protocol-contracts-75a938cc.mjs +0 -222
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-shared-protocol-contracts-b5f28f94.mjs +0 -5778
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-upload-spool.mjs +0 -198
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/04-compression.js +0 -4175
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/05-artifact-sharing.js +0 -65
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/06-artifact-review-ui.js +0 -1755
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/07-artifact-sharing.js +0 -62
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/08-artifact-review-ui.js +0 -2633
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.1.0/artifact-paste.schema.json +0 -49
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.9.0/design-review-workspace.schema.json +0 -108
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.9.0/design-workspace-create.schema.json +0 -170
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.9.0/design-workspace-event.schema.json +0 -97
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.9.0/design-workspace-revision.schema.json +0 -56
- package/lib/host-packages/openai/openplanr/skills/delegate/scripts/integration-transaction.mjs +0 -958
|
@@ -0,0 +1,2568 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
// lib/artifact/ui/feedback-rail.mjs
|
|
4
|
+
var ARTIFACT_REVIEW_CHANGE_EVENT = "planr:artifact-review-change";
|
|
5
|
+
var ARTIFACT_REVIEW_SELECT_EVENT = "planr:artifact-review-select";
|
|
6
|
+
var ARTIFACT_REVIEW_DRAFT_CHANGE_EVENT = "planr:artifact-review-draft-change";
|
|
7
|
+
var ARTIFACT_REVIEW_LIMITS = Object.freeze({
|
|
8
|
+
id: 128,
|
|
9
|
+
authorName: 256,
|
|
10
|
+
artifactId: 128,
|
|
11
|
+
variant: 128,
|
|
12
|
+
anchor: 512,
|
|
13
|
+
screen: 128,
|
|
14
|
+
text: 65536,
|
|
15
|
+
pins: 1e4,
|
|
16
|
+
replies: 1e4,
|
|
17
|
+
viewport: 16384
|
|
18
|
+
});
|
|
19
|
+
var ARTIFACT_REVIEW_DECISIONS = Object.freeze([
|
|
20
|
+
"pending",
|
|
21
|
+
"approved",
|
|
22
|
+
"changes_requested"
|
|
23
|
+
]);
|
|
24
|
+
var ARTIFACT_REVIEW_INTENTS = Object.freeze(["fix", "improve", "question"]);
|
|
25
|
+
var ARTIFACT_REVIEW_STATUSES = Object.freeze(["open", "addressed", "resolved"]);
|
|
26
|
+
var REVIEW_OF_RE = /^[a-f0-9]{64}$/;
|
|
27
|
+
var ArtifactReviewStateError = class extends Error {
|
|
28
|
+
constructor(code, message) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "ArtifactReviewStateError";
|
|
31
|
+
this.code = code;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
function invalid(message) {
|
|
35
|
+
throw new ArtifactReviewStateError("E_ARTIFACT_REVIEW_INVALID", message);
|
|
36
|
+
}
|
|
37
|
+
function identityRequired() {
|
|
38
|
+
throw new ArtifactReviewStateError(
|
|
39
|
+
"E_ARTIFACT_REVIEW_IDENTITY_REQUIRED",
|
|
40
|
+
"Enter your name before adding a comment."
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
function clonePlain(value) {
|
|
44
|
+
if (Array.isArray(value)) return value.map(clonePlain);
|
|
45
|
+
if (!value || typeof value !== "object") return value;
|
|
46
|
+
const clone = {};
|
|
47
|
+
for (const [key, entry] of Object.entries(value)) clone[key] = clonePlain(entry);
|
|
48
|
+
return clone;
|
|
49
|
+
}
|
|
50
|
+
function cloneFrozenArtifactReview(review) {
|
|
51
|
+
return deepFreeze(clonePlain(review));
|
|
52
|
+
}
|
|
53
|
+
function boundedString(value, label, { min = 0, max, trim = false, pattern } = {}) {
|
|
54
|
+
if (typeof value !== "string") invalid(`${label} must be a string.`);
|
|
55
|
+
const normalized3 = trim ? value.trim() : value;
|
|
56
|
+
if (normalized3.length < min || max !== void 0 && normalized3.length > max) {
|
|
57
|
+
invalid(`${label} must contain ${min} through ${max ?? "unlimited"} characters.`);
|
|
58
|
+
}
|
|
59
|
+
if (pattern && !pattern.test(normalized3)) invalid(`${label} has an invalid format.`);
|
|
60
|
+
return normalized3;
|
|
61
|
+
}
|
|
62
|
+
function optionalString(value, label, options) {
|
|
63
|
+
if (value === void 0) return void 0;
|
|
64
|
+
return boundedString(value, label, options);
|
|
65
|
+
}
|
|
66
|
+
function enumValue(value, values, label) {
|
|
67
|
+
if (!values.includes(value)) invalid(`${label} must be one of: ${values.join(", ")}.`);
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
function isoTimestamp(value, label) {
|
|
71
|
+
const timestamp = value instanceof Date ? value.toISOString() : value;
|
|
72
|
+
if (typeof timestamp !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(timestamp) || !Number.isFinite(Date.parse(timestamp))) {
|
|
73
|
+
invalid(`${label} must be an ISO-8601 date-time.`);
|
|
74
|
+
}
|
|
75
|
+
return timestamp;
|
|
76
|
+
}
|
|
77
|
+
function dependencyTimestamp(now) {
|
|
78
|
+
return isoTimestamp(now(), "now()");
|
|
79
|
+
}
|
|
80
|
+
function defaultNow() {
|
|
81
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
82
|
+
}
|
|
83
|
+
function createSecureArtifactReviewId(cryptoProvider = globalThis.crypto) {
|
|
84
|
+
const randomUuid = cryptoProvider?.randomUUID?.();
|
|
85
|
+
if (randomUuid) return randomUuid;
|
|
86
|
+
const bytes = new Uint8Array(16);
|
|
87
|
+
if (typeof cryptoProvider?.getRandomValues !== "function") {
|
|
88
|
+
throw new ArtifactReviewStateError(
|
|
89
|
+
"E_ARTIFACT_REVIEW_UUID_UNAVAILABLE",
|
|
90
|
+
"Secure UUID generation is unavailable in this browser."
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
cryptoProvider.getRandomValues(bytes);
|
|
94
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
95
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
96
|
+
const hex = [...bytes].map((value) => value.toString(16).padStart(2, "0")).join("");
|
|
97
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
98
|
+
}
|
|
99
|
+
function defaultCreateId() {
|
|
100
|
+
return createSecureArtifactReviewId();
|
|
101
|
+
}
|
|
102
|
+
function dependencyId(createId, kind) {
|
|
103
|
+
return boundedString(createId(kind), `${kind} id`, {
|
|
104
|
+
min: 1,
|
|
105
|
+
max: ARTIFACT_REVIEW_LIMITS.id,
|
|
106
|
+
trim: true
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function uniqueDependencyId(createId, kind, existingIds) {
|
|
110
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
111
|
+
const candidate = dependencyId(createId, kind);
|
|
112
|
+
if (!existingIds.has(candidate)) return candidate;
|
|
113
|
+
}
|
|
114
|
+
throw new ArtifactReviewStateError(
|
|
115
|
+
"E_ARTIFACT_REVIEW_ID_COLLISION",
|
|
116
|
+
`Could not create a unique ${kind} id.`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
function normalizeArtifactReviewIdentity(value, { allowEmpty = false } = {}) {
|
|
120
|
+
if (value === null || value === void 0 || value === "") {
|
|
121
|
+
if (allowEmpty) return null;
|
|
122
|
+
identityRequired();
|
|
123
|
+
}
|
|
124
|
+
const source = typeof value === "string" ? { name: value } : value;
|
|
125
|
+
if (!source || typeof source !== "object" || Array.isArray(source)) identityRequired();
|
|
126
|
+
const name = boundedString(source.name, "author.name", {
|
|
127
|
+
min: 1,
|
|
128
|
+
max: ARTIFACT_REVIEW_LIMITS.authorName,
|
|
129
|
+
trim: true
|
|
130
|
+
});
|
|
131
|
+
const id = optionalString(source.id, "author.id", {
|
|
132
|
+
min: 1,
|
|
133
|
+
max: ARTIFACT_REVIEW_LIMITS.id,
|
|
134
|
+
trim: true
|
|
135
|
+
});
|
|
136
|
+
return deepFreeze(id === void 0 ? { name } : { id, name });
|
|
137
|
+
}
|
|
138
|
+
function normalizeRegion(region) {
|
|
139
|
+
if (!region || typeof region !== "object" || Array.isArray(region)) {
|
|
140
|
+
invalid("pin.region must be an object.");
|
|
141
|
+
}
|
|
142
|
+
const finite3 = (value, label) => {
|
|
143
|
+
if (typeof value !== "number" || !Number.isFinite(value)) invalid(`${label} must be finite.`);
|
|
144
|
+
return value;
|
|
145
|
+
};
|
|
146
|
+
const x = finite3(region.x, "pin.region.x");
|
|
147
|
+
const y = finite3(region.y, "pin.region.y");
|
|
148
|
+
const w = finite3(region.w, "pin.region.w");
|
|
149
|
+
const h = finite3(region.h, "pin.region.h");
|
|
150
|
+
if (x >= 0 && y >= 0 && w >= 0 && h >= 0 && x + w <= 1 && y + h <= 1) {
|
|
151
|
+
return { x, y, w, h };
|
|
152
|
+
}
|
|
153
|
+
const unit = (value) => Math.round(Math.min(1, Math.max(0, value)) * 1e6) / 1e6;
|
|
154
|
+
const boundedX = unit(x);
|
|
155
|
+
const boundedY = unit(y);
|
|
156
|
+
return {
|
|
157
|
+
x: boundedX,
|
|
158
|
+
y: boundedY,
|
|
159
|
+
w: Math.round(Math.min(unit(w), 1 - boundedX) * 1e6) / 1e6,
|
|
160
|
+
h: Math.round(Math.min(unit(h), 1 - boundedY) * 1e6) / 1e6
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function normalizeViewport(viewport) {
|
|
164
|
+
if (!viewport || typeof viewport !== "object" || Array.isArray(viewport)) {
|
|
165
|
+
invalid("pin.viewport must be an object.");
|
|
166
|
+
}
|
|
167
|
+
const dimension = (value, label) => {
|
|
168
|
+
if (!Number.isInteger(value) || value < 1 || value > ARTIFACT_REVIEW_LIMITS.viewport) {
|
|
169
|
+
invalid(`${label} must be an integer from 1 through ${ARTIFACT_REVIEW_LIMITS.viewport}.`);
|
|
170
|
+
}
|
|
171
|
+
return value;
|
|
172
|
+
};
|
|
173
|
+
return {
|
|
174
|
+
width: dimension(viewport.width, "pin.viewport.width"),
|
|
175
|
+
height: dimension(viewport.height, "pin.viewport.height")
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function normalizeAnchor(anchor) {
|
|
179
|
+
if (anchor === void 0 || anchor === null) return void 0;
|
|
180
|
+
if (typeof anchor !== "object" || Array.isArray(anchor)) invalid("pin.anchor must be an object.");
|
|
181
|
+
const planrId = boundedString(anchor.planrId, "pin.anchor.planrId", {
|
|
182
|
+
min: 1,
|
|
183
|
+
max: ARTIFACT_REVIEW_LIMITS.anchor,
|
|
184
|
+
trim: true
|
|
185
|
+
});
|
|
186
|
+
const screen = optionalString(anchor.screen, "pin.anchor.screen", {
|
|
187
|
+
min: 1,
|
|
188
|
+
max: ARTIFACT_REVIEW_LIMITS.screen,
|
|
189
|
+
trim: true
|
|
190
|
+
});
|
|
191
|
+
return screen === void 0 ? { planrId } : { planrId, screen };
|
|
192
|
+
}
|
|
193
|
+
function normalizeReply(reply, label = "reply") {
|
|
194
|
+
if (!reply || typeof reply !== "object" || Array.isArray(reply))
|
|
195
|
+
invalid(`${label} must be an object.`);
|
|
196
|
+
return {
|
|
197
|
+
id: boundedString(reply.id, `${label}.id`, {
|
|
198
|
+
min: 1,
|
|
199
|
+
max: ARTIFACT_REVIEW_LIMITS.id,
|
|
200
|
+
trim: true
|
|
201
|
+
}),
|
|
202
|
+
author: normalizeArtifactReviewIdentity(reply.author),
|
|
203
|
+
comment: boundedString(reply.comment, `${label}.comment`, {
|
|
204
|
+
min: 1,
|
|
205
|
+
max: ARTIFACT_REVIEW_LIMITS.text,
|
|
206
|
+
trim: true
|
|
207
|
+
}),
|
|
208
|
+
createdAt: isoTimestamp(reply.createdAt, `${label}.createdAt`)
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function compareTimestampThenId(left, right) {
|
|
212
|
+
const byTime = left.createdAt.localeCompare(right.createdAt);
|
|
213
|
+
return byTime === 0 ? left.id.localeCompare(right.id) : byTime;
|
|
214
|
+
}
|
|
215
|
+
function normalizePin(pin, label = "pin") {
|
|
216
|
+
if (!pin || typeof pin !== "object" || Array.isArray(pin)) invalid(`${label} must be an object.`);
|
|
217
|
+
if (!Array.isArray(pin.replies) || pin.replies.length > ARTIFACT_REVIEW_LIMITS.replies) {
|
|
218
|
+
invalid(`${label}.replies must contain no more than ${ARTIFACT_REVIEW_LIMITS.replies} items.`);
|
|
219
|
+
}
|
|
220
|
+
const replies = pin.replies.map(
|
|
221
|
+
(reply, index) => normalizeReply(reply, `${label}.replies[${index}]`)
|
|
222
|
+
);
|
|
223
|
+
const replyIds = new Set(replies.map(({ id }) => id));
|
|
224
|
+
if (replyIds.size !== replies.length) invalid(`${label}.replies must have unique ids.`);
|
|
225
|
+
const normalized3 = {
|
|
226
|
+
id: boundedString(pin.id, `${label}.id`, {
|
|
227
|
+
min: 1,
|
|
228
|
+
max: ARTIFACT_REVIEW_LIMITS.id,
|
|
229
|
+
trim: true
|
|
230
|
+
}),
|
|
231
|
+
author: normalizeArtifactReviewIdentity(pin.author),
|
|
232
|
+
artifactId: boundedString(pin.artifactId, `${label}.artifactId`, {
|
|
233
|
+
min: 1,
|
|
234
|
+
max: ARTIFACT_REVIEW_LIMITS.artifactId,
|
|
235
|
+
trim: true
|
|
236
|
+
}),
|
|
237
|
+
region: normalizeRegion(pin.region),
|
|
238
|
+
viewport: normalizeViewport(pin.viewport),
|
|
239
|
+
intent: enumValue(pin.intent, ARTIFACT_REVIEW_INTENTS, `${label}.intent`),
|
|
240
|
+
status: enumValue(pin.status, ARTIFACT_REVIEW_STATUSES, `${label}.status`),
|
|
241
|
+
comment: boundedString(pin.comment, `${label}.comment`, {
|
|
242
|
+
min: 1,
|
|
243
|
+
max: ARTIFACT_REVIEW_LIMITS.text,
|
|
244
|
+
trim: true
|
|
245
|
+
}),
|
|
246
|
+
replies: replies.sort(compareTimestampThenId),
|
|
247
|
+
createdAt: isoTimestamp(pin.createdAt, `${label}.createdAt`),
|
|
248
|
+
updatedAt: isoTimestamp(pin.updatedAt, `${label}.updatedAt`)
|
|
249
|
+
};
|
|
250
|
+
const variant = optionalString(pin.variant, `${label}.variant`, {
|
|
251
|
+
min: 1,
|
|
252
|
+
max: ARTIFACT_REVIEW_LIMITS.variant,
|
|
253
|
+
trim: true
|
|
254
|
+
});
|
|
255
|
+
const anchor = normalizeAnchor(pin.anchor);
|
|
256
|
+
if (variant !== void 0) normalized3.variant = variant;
|
|
257
|
+
if (anchor !== void 0) normalized3.anchor = anchor;
|
|
258
|
+
return normalized3;
|
|
259
|
+
}
|
|
260
|
+
function normalizeArtifactReview(review) {
|
|
261
|
+
if (!review || typeof review !== "object" || Array.isArray(review)) {
|
|
262
|
+
invalid("Artifact review must be an object.");
|
|
263
|
+
}
|
|
264
|
+
if (!Array.isArray(review.pins) || review.pins.length > ARTIFACT_REVIEW_LIMITS.pins) {
|
|
265
|
+
invalid(`review.pins must contain no more than ${ARTIFACT_REVIEW_LIMITS.pins} items.`);
|
|
266
|
+
}
|
|
267
|
+
const pins = review.pins.map((pin, index) => normalizePin(pin, `review.pins[${index}]`));
|
|
268
|
+
const pinIds = new Set(pins.map(({ id }) => id));
|
|
269
|
+
if (pinIds.size !== pins.length) invalid("review.pins must have unique ids.");
|
|
270
|
+
const normalized3 = {
|
|
271
|
+
schemaVersion: enumValue(review.schemaVersion, ["1.0.0"], "review.schemaVersion"),
|
|
272
|
+
reviewId: boundedString(review.reviewId, "review.reviewId", {
|
|
273
|
+
min: 1,
|
|
274
|
+
max: ARTIFACT_REVIEW_LIMITS.id,
|
|
275
|
+
trim: true
|
|
276
|
+
}),
|
|
277
|
+
reviewOf: boundedString(review.reviewOf, "review.reviewOf", {
|
|
278
|
+
min: 64,
|
|
279
|
+
max: 64,
|
|
280
|
+
pattern: REVIEW_OF_RE
|
|
281
|
+
}),
|
|
282
|
+
decision: enumValue(review.decision, ARTIFACT_REVIEW_DECISIONS, "review.decision"),
|
|
283
|
+
overall: boundedString(review.overall, "review.overall", {
|
|
284
|
+
max: ARTIFACT_REVIEW_LIMITS.text
|
|
285
|
+
}),
|
|
286
|
+
pins: pins.sort(compareTimestampThenId)
|
|
287
|
+
};
|
|
288
|
+
if (review.createdAt !== void 0)
|
|
289
|
+
normalized3.createdAt = isoTimestamp(review.createdAt, "review.createdAt");
|
|
290
|
+
if (review.updatedAt !== void 0)
|
|
291
|
+
normalized3.updatedAt = isoTimestamp(review.updatedAt, "review.updatedAt");
|
|
292
|
+
return deepFreeze(normalized3);
|
|
293
|
+
}
|
|
294
|
+
function createArtifactReview({ reviewId, reviewOf, createId = defaultCreateId } = {}) {
|
|
295
|
+
const normalizedReviewId = reviewId === void 0 ? dependencyId(createId, "review") : boundedString(reviewId, "reviewId", {
|
|
296
|
+
min: 1,
|
|
297
|
+
max: ARTIFACT_REVIEW_LIMITS.id,
|
|
298
|
+
trim: true
|
|
299
|
+
});
|
|
300
|
+
return normalizeArtifactReview({
|
|
301
|
+
schemaVersion: "1.0.0",
|
|
302
|
+
reviewId: normalizedReviewId,
|
|
303
|
+
reviewOf: boundedString(reviewOf, "reviewOf", {
|
|
304
|
+
min: 64,
|
|
305
|
+
max: 64,
|
|
306
|
+
pattern: REVIEW_OF_RE
|
|
307
|
+
}),
|
|
308
|
+
decision: "pending",
|
|
309
|
+
overall: "",
|
|
310
|
+
pins: []
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
function replacePin(review, pin) {
|
|
314
|
+
return review.pins.map((candidate) => candidate.id === pin.id ? pin : candidate);
|
|
315
|
+
}
|
|
316
|
+
function findPin(review, pinId) {
|
|
317
|
+
const normalizedId = boundedString(pinId, "pinId", {
|
|
318
|
+
min: 1,
|
|
319
|
+
max: ARTIFACT_REVIEW_LIMITS.id,
|
|
320
|
+
trim: true
|
|
321
|
+
});
|
|
322
|
+
const pin = review.pins.find(({ id }) => id === normalizedId);
|
|
323
|
+
if (!pin) {
|
|
324
|
+
throw new ArtifactReviewStateError(
|
|
325
|
+
"E_ARTIFACT_REVIEW_PIN_NOT_FOUND",
|
|
326
|
+
`Unknown feedback pin: ${normalizedId}`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
return pin;
|
|
330
|
+
}
|
|
331
|
+
function preserveOptionalReviewTimestamps(review, next, timestamp) {
|
|
332
|
+
if (review.createdAt !== void 0) next.createdAt = review.createdAt;
|
|
333
|
+
if (review.updatedAt !== void 0) next.updatedAt = timestamp;
|
|
334
|
+
return next;
|
|
335
|
+
}
|
|
336
|
+
function reduceArtifactReview(review, action, { createId = defaultCreateId, now = defaultNow } = {}) {
|
|
337
|
+
const current = normalizeArtifactReview(review);
|
|
338
|
+
if (!action || typeof action !== "object" || Array.isArray(action))
|
|
339
|
+
invalid("Review action must be an object.");
|
|
340
|
+
const timestamp = dependencyTimestamp(now);
|
|
341
|
+
let next;
|
|
342
|
+
switch (action.type) {
|
|
343
|
+
case "add-pin": {
|
|
344
|
+
if (current.pins.length >= ARTIFACT_REVIEW_LIMITS.pins) {
|
|
345
|
+
invalid(`A review can contain at most ${ARTIFACT_REVIEW_LIMITS.pins} pins.`);
|
|
346
|
+
}
|
|
347
|
+
const author = normalizeArtifactReviewIdentity(action.author);
|
|
348
|
+
const pinInput = action.pin && typeof action.pin === "object" ? action.pin : {};
|
|
349
|
+
const pinId = pinInput.id === void 0 ? uniqueDependencyId(createId, "pin", new Set(current.pins.map(({ id }) => id))) : boundedString(pinInput.id, "pin.id", {
|
|
350
|
+
min: 1,
|
|
351
|
+
max: ARTIFACT_REVIEW_LIMITS.id,
|
|
352
|
+
trim: true
|
|
353
|
+
});
|
|
354
|
+
if (current.pins.some(({ id }) => id === pinId)) {
|
|
355
|
+
throw new ArtifactReviewStateError(
|
|
356
|
+
"E_ARTIFACT_REVIEW_ID_COLLISION",
|
|
357
|
+
`Duplicate pin id: ${pinId}`
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
const pin = normalizePin({
|
|
361
|
+
...pinInput,
|
|
362
|
+
id: pinId,
|
|
363
|
+
author,
|
|
364
|
+
status: pinInput.status ?? "open",
|
|
365
|
+
replies: [],
|
|
366
|
+
createdAt: pinInput.createdAt ?? timestamp,
|
|
367
|
+
updatedAt: pinInput.updatedAt ?? timestamp
|
|
368
|
+
});
|
|
369
|
+
next = preserveOptionalReviewTimestamps(
|
|
370
|
+
current,
|
|
371
|
+
{
|
|
372
|
+
...current,
|
|
373
|
+
pins: [...current.pins, pin]
|
|
374
|
+
},
|
|
375
|
+
timestamp
|
|
376
|
+
);
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
case "add-reply": {
|
|
380
|
+
const pin = findPin(current, action.pinId);
|
|
381
|
+
if (pin.replies.length >= ARTIFACT_REVIEW_LIMITS.replies) {
|
|
382
|
+
invalid(`A feedback thread can contain at most ${ARTIFACT_REVIEW_LIMITS.replies} replies.`);
|
|
383
|
+
}
|
|
384
|
+
const replyId = action.id === void 0 ? uniqueDependencyId(createId, "reply", new Set(pin.replies.map(({ id }) => id))) : boundedString(action.id, "reply.id", {
|
|
385
|
+
min: 1,
|
|
386
|
+
max: ARTIFACT_REVIEW_LIMITS.id,
|
|
387
|
+
trim: true
|
|
388
|
+
});
|
|
389
|
+
if (pin.replies.some(({ id }) => id === replyId)) {
|
|
390
|
+
throw new ArtifactReviewStateError(
|
|
391
|
+
"E_ARTIFACT_REVIEW_ID_COLLISION",
|
|
392
|
+
`Duplicate reply id: ${replyId}`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
const reply = normalizeReply({
|
|
396
|
+
id: replyId,
|
|
397
|
+
author: normalizeArtifactReviewIdentity(action.author),
|
|
398
|
+
comment: action.comment,
|
|
399
|
+
createdAt: action.createdAt ?? timestamp
|
|
400
|
+
});
|
|
401
|
+
const updatedPin = normalizePin({
|
|
402
|
+
...pin,
|
|
403
|
+
replies: [...pin.replies, reply],
|
|
404
|
+
updatedAt: timestamp
|
|
405
|
+
});
|
|
406
|
+
next = preserveOptionalReviewTimestamps(
|
|
407
|
+
current,
|
|
408
|
+
{
|
|
409
|
+
...current,
|
|
410
|
+
pins: replacePin(current, updatedPin)
|
|
411
|
+
},
|
|
412
|
+
timestamp
|
|
413
|
+
);
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
case "set-status": {
|
|
417
|
+
const pin = findPin(current, action.pinId);
|
|
418
|
+
const updatedPin = normalizePin({
|
|
419
|
+
...pin,
|
|
420
|
+
status: enumValue(action.status, ARTIFACT_REVIEW_STATUSES, "status"),
|
|
421
|
+
updatedAt: timestamp
|
|
422
|
+
});
|
|
423
|
+
next = preserveOptionalReviewTimestamps(
|
|
424
|
+
current,
|
|
425
|
+
{
|
|
426
|
+
...current,
|
|
427
|
+
pins: replacePin(current, updatedPin)
|
|
428
|
+
},
|
|
429
|
+
timestamp
|
|
430
|
+
);
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
case "set-overall":
|
|
434
|
+
next = preserveOptionalReviewTimestamps(
|
|
435
|
+
current,
|
|
436
|
+
{
|
|
437
|
+
...current,
|
|
438
|
+
overall: boundedString(action.overall, "overall", { max: ARTIFACT_REVIEW_LIMITS.text })
|
|
439
|
+
},
|
|
440
|
+
timestamp
|
|
441
|
+
);
|
|
442
|
+
break;
|
|
443
|
+
case "set-decision":
|
|
444
|
+
next = preserveOptionalReviewTimestamps(
|
|
445
|
+
current,
|
|
446
|
+
{
|
|
447
|
+
...current,
|
|
448
|
+
decision: enumValue(action.decision, ARTIFACT_REVIEW_DECISIONS, "decision")
|
|
449
|
+
},
|
|
450
|
+
timestamp
|
|
451
|
+
);
|
|
452
|
+
break;
|
|
453
|
+
default:
|
|
454
|
+
throw new ArtifactReviewStateError(
|
|
455
|
+
"E_ARTIFACT_REVIEW_ACTION_UNKNOWN",
|
|
456
|
+
`Unknown artifact review action: ${String(action.type)}`
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
return normalizeArtifactReview(next);
|
|
460
|
+
}
|
|
461
|
+
function createArtifactReviewController({
|
|
462
|
+
initialReview = null,
|
|
463
|
+
reviewOf,
|
|
464
|
+
reviewId,
|
|
465
|
+
identity = null,
|
|
466
|
+
createId = defaultCreateId,
|
|
467
|
+
now = defaultNow
|
|
468
|
+
} = {}) {
|
|
469
|
+
let review = initialReview === null || initialReview === void 0 ? null : normalizeArtifactReview(initialReview);
|
|
470
|
+
if (review && reviewOf !== void 0 && review.reviewOf !== reviewOf) {
|
|
471
|
+
throw new ArtifactReviewStateError(
|
|
472
|
+
"E_ARTIFACT_REVIEW_DIGEST_MISMATCH",
|
|
473
|
+
"The initial review does not match the artifact envelope digest."
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
let localIdentity = normalizeArtifactReviewIdentity(identity, { allowEmpty: true });
|
|
477
|
+
let activePinId = null;
|
|
478
|
+
let destroyed = false;
|
|
479
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
480
|
+
const assertAlive = () => {
|
|
481
|
+
if (destroyed) {
|
|
482
|
+
throw new ArtifactReviewStateError(
|
|
483
|
+
"E_ARTIFACT_REVIEW_DESTROYED",
|
|
484
|
+
"Artifact review controller is destroyed."
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
const ensureReview = () => {
|
|
489
|
+
review ??= createArtifactReview({ reviewId, reviewOf, createId });
|
|
490
|
+
return review;
|
|
491
|
+
};
|
|
492
|
+
const getState = () => deepFreeze({
|
|
493
|
+
review,
|
|
494
|
+
identity: localIdentity,
|
|
495
|
+
activePinId
|
|
496
|
+
});
|
|
497
|
+
const notify = (change) => {
|
|
498
|
+
const state = getState();
|
|
499
|
+
for (const listener of [...listeners]) listener(state, deepFreeze({ ...change }));
|
|
500
|
+
};
|
|
501
|
+
const controller = {
|
|
502
|
+
getReview() {
|
|
503
|
+
return review;
|
|
504
|
+
},
|
|
505
|
+
getState,
|
|
506
|
+
getIdentity() {
|
|
507
|
+
return localIdentity;
|
|
508
|
+
},
|
|
509
|
+
setIdentity(value) {
|
|
510
|
+
assertAlive();
|
|
511
|
+
localIdentity = normalizeArtifactReviewIdentity(value, { allowEmpty: true });
|
|
512
|
+
notify({ type: "identity" });
|
|
513
|
+
return localIdentity;
|
|
514
|
+
},
|
|
515
|
+
dispatch(action) {
|
|
516
|
+
assertAlive();
|
|
517
|
+
const authored = action?.type === "add-pin" || action?.type === "add-reply";
|
|
518
|
+
const nextAction = authored && action.author === void 0 ? { ...action, author: localIdentity ?? identityRequired() } : action;
|
|
519
|
+
const previousPinIds = nextAction?.type === "add-pin" ? new Set(review?.pins.map(({ id }) => id) ?? []) : null;
|
|
520
|
+
review = reduceArtifactReview(ensureReview(), nextAction, { createId, now });
|
|
521
|
+
if (previousPinIds) {
|
|
522
|
+
activePinId = review.pins.find(({ id }) => !previousPinIds.has(id))?.id ?? activePinId;
|
|
523
|
+
}
|
|
524
|
+
notify({ type: "review", action: nextAction.type });
|
|
525
|
+
return review;
|
|
526
|
+
},
|
|
527
|
+
replaceReview(value) {
|
|
528
|
+
assertAlive();
|
|
529
|
+
const next = value === null || value === void 0 ? null : normalizeArtifactReview(value);
|
|
530
|
+
if (next && reviewOf !== void 0 && next.reviewOf !== reviewOf) {
|
|
531
|
+
throw new ArtifactReviewStateError(
|
|
532
|
+
"E_ARTIFACT_REVIEW_DIGEST_MISMATCH",
|
|
533
|
+
"The replacement review does not match the artifact envelope digest."
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
review = next;
|
|
537
|
+
if (activePinId && !review?.pins.some(({ id }) => id === activePinId)) activePinId = null;
|
|
538
|
+
notify({ type: "review-replaced" });
|
|
539
|
+
return review;
|
|
540
|
+
},
|
|
541
|
+
selectPin(pinId) {
|
|
542
|
+
assertAlive();
|
|
543
|
+
if (pinId === null || pinId === void 0) {
|
|
544
|
+
activePinId = null;
|
|
545
|
+
} else {
|
|
546
|
+
activePinId = findPin(ensureReview(), pinId).id;
|
|
547
|
+
}
|
|
548
|
+
notify({ type: "selection" });
|
|
549
|
+
return activePinId;
|
|
550
|
+
},
|
|
551
|
+
subscribe(listener) {
|
|
552
|
+
assertAlive();
|
|
553
|
+
if (typeof listener !== "function") invalid("Review subscriber must be a function.");
|
|
554
|
+
listeners.add(listener);
|
|
555
|
+
return () => listeners.delete(listener);
|
|
556
|
+
},
|
|
557
|
+
destroy() {
|
|
558
|
+
if (destroyed) return;
|
|
559
|
+
destroyed = true;
|
|
560
|
+
listeners.clear();
|
|
561
|
+
review = null;
|
|
562
|
+
localIdentity = null;
|
|
563
|
+
activePinId = null;
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
return Object.freeze(controller);
|
|
567
|
+
}
|
|
568
|
+
function createElement(document2, tagName, { className, text: text2, attributes = {} } = {}) {
|
|
569
|
+
const element = document2.createElement(tagName);
|
|
570
|
+
if (className) element.className = className;
|
|
571
|
+
if (text2 !== void 0) element.textContent = text2;
|
|
572
|
+
for (const [name, value] of Object.entries(attributes)) {
|
|
573
|
+
if (value !== void 0 && value !== null) element.setAttribute(name, String(value));
|
|
574
|
+
}
|
|
575
|
+
return element;
|
|
576
|
+
}
|
|
577
|
+
function artifactReviewThreadDomId(pinId) {
|
|
578
|
+
return annotationDomIds(pinId).thread;
|
|
579
|
+
}
|
|
580
|
+
function displayTimestamp(timestamp) {
|
|
581
|
+
const canonical = new Date(timestamp).toISOString();
|
|
582
|
+
return `${canonical.slice(0, 10)} ${canonical.slice(11, 16)} UTC`;
|
|
583
|
+
}
|
|
584
|
+
function renderReply(document2, reply) {
|
|
585
|
+
const item = createElement(document2, "li", {
|
|
586
|
+
className: "planr-reply",
|
|
587
|
+
attributes: { "data-planr-reply-id": reply.id }
|
|
588
|
+
});
|
|
589
|
+
const heading = createElement(document2, "header");
|
|
590
|
+
heading.append(createElement(document2, "strong", { text: reply.author.name }));
|
|
591
|
+
const time = createElement(document2, "time", {
|
|
592
|
+
text: displayTimestamp(reply.createdAt),
|
|
593
|
+
attributes: { datetime: reply.createdAt }
|
|
594
|
+
});
|
|
595
|
+
heading.append(time);
|
|
596
|
+
item.append(heading, createElement(document2, "p", { text: reply.comment }));
|
|
597
|
+
return item;
|
|
598
|
+
}
|
|
599
|
+
function renderReplyForm(document2, pin, expanded = false) {
|
|
600
|
+
const fieldId = `${annotationDomIds(pin.id).thread}-reply`;
|
|
601
|
+
const wrapper = createElement(document2, "div", { className: "planr-reply-editor" });
|
|
602
|
+
const toggle = createElement(document2, "button", {
|
|
603
|
+
className: "planr-reply-toggle",
|
|
604
|
+
text: expanded ? "− Reply" : "+ Reply",
|
|
605
|
+
attributes: {
|
|
606
|
+
type: "button",
|
|
607
|
+
id: `${fieldId}-toggle`,
|
|
608
|
+
"data-planr-reply-toggle": pin.id,
|
|
609
|
+
"aria-expanded": String(expanded),
|
|
610
|
+
"aria-controls": `${fieldId}-form`,
|
|
611
|
+
title: expanded ? "Collapse reply" : "Reply to this comment"
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
const form = createElement(document2, "form", {
|
|
615
|
+
className: "planr-reply-form",
|
|
616
|
+
attributes: {
|
|
617
|
+
"data-planr-reply-form": pin.id,
|
|
618
|
+
id: `${fieldId}-form`,
|
|
619
|
+
...expanded ? {} : { hidden: "" }
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
const label = createElement(document2, "label", {
|
|
623
|
+
className: "planr-reply-label",
|
|
624
|
+
text: "Reply to thread",
|
|
625
|
+
attributes: { for: fieldId }
|
|
626
|
+
});
|
|
627
|
+
const textarea = createElement(document2, "textarea", {
|
|
628
|
+
attributes: {
|
|
629
|
+
id: fieldId,
|
|
630
|
+
name: "reply",
|
|
631
|
+
maxlength: ARTIFACT_REVIEW_LIMITS.text,
|
|
632
|
+
rows: 2,
|
|
633
|
+
placeholder: "Write a reply…",
|
|
634
|
+
required: "",
|
|
635
|
+
"aria-describedby": "planr-review-error"
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
const controls = createElement(document2, "div", { className: "planr-reply-controls" });
|
|
639
|
+
const hint = createElement(document2, "span", {
|
|
640
|
+
className: "planr-reply-hint",
|
|
641
|
+
text: "Ctrl/⌘ + Enter to send"
|
|
642
|
+
});
|
|
643
|
+
const submit = createElement(document2, "button", {
|
|
644
|
+
className: "planr-reply-send",
|
|
645
|
+
attributes: {
|
|
646
|
+
type: "submit",
|
|
647
|
+
disabled: "",
|
|
648
|
+
"aria-label": "Send reply",
|
|
649
|
+
title: "Send reply (Ctrl/⌘ + Enter)"
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
const svg = document2.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
653
|
+
for (const [name, value] of Object.entries({
|
|
654
|
+
viewBox: "0 0 24 24",
|
|
655
|
+
width: "16",
|
|
656
|
+
height: "16",
|
|
657
|
+
fill: "none",
|
|
658
|
+
stroke: "currentColor",
|
|
659
|
+
"stroke-width": "1.8",
|
|
660
|
+
"stroke-linecap": "round",
|
|
661
|
+
"stroke-linejoin": "round",
|
|
662
|
+
"aria-hidden": "true",
|
|
663
|
+
focusable: "false"
|
|
664
|
+
}))
|
|
665
|
+
svg.setAttribute(name, value);
|
|
666
|
+
const path = document2.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
667
|
+
path.setAttribute("d", "M12 19V5m-6 6 6-6 6 6");
|
|
668
|
+
svg.append(path);
|
|
669
|
+
submit.append(svg);
|
|
670
|
+
controls.append(hint, submit);
|
|
671
|
+
form.append(label, textarea, controls);
|
|
672
|
+
wrapper.append(toggle, form);
|
|
673
|
+
return wrapper;
|
|
674
|
+
}
|
|
675
|
+
function renderThread(document2, pin, active, description = {}) {
|
|
676
|
+
const intentLabel = typeof description.intentLabel === "string" ? description.intentLabel.slice(0, 128) : pin.intent;
|
|
677
|
+
const article = createElement(document2, "article", {
|
|
678
|
+
className: `planr-thread${active ? " is-active" : ""}`,
|
|
679
|
+
attributes: {
|
|
680
|
+
id: artifactReviewThreadDomId(pin.id),
|
|
681
|
+
tabindex: "-1",
|
|
682
|
+
"data-planr-pin-id": pin.id,
|
|
683
|
+
"data-planr-intent": pin.intent,
|
|
684
|
+
"data-planr-status": pin.status,
|
|
685
|
+
"aria-controls": annotationDomIds(pin.id).pin,
|
|
686
|
+
"aria-label": `${intentLabel} comment by ${pin.author.name}`,
|
|
687
|
+
"data-planr-unread": description.unread === true ? "true" : "false",
|
|
688
|
+
...description.compact ? { "data-planr-compact": "true" } : {}
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
const header = createElement(document2, "header");
|
|
692
|
+
const byline = createElement(document2, "div", { className: "planr-review-byline" });
|
|
693
|
+
byline.append(
|
|
694
|
+
createElement(document2, "strong", { text: pin.author.name }),
|
|
695
|
+
createElement(document2, "span", { className: "planr-intent", text: intentLabel })
|
|
696
|
+
);
|
|
697
|
+
const time = createElement(document2, "time", {
|
|
698
|
+
text: description.compact ? `${new Date(pin.createdAt).toISOString().slice(11, 16)} UTC` : displayTimestamp(pin.createdAt),
|
|
699
|
+
attributes: { datetime: pin.createdAt, title: displayTimestamp(pin.createdAt) }
|
|
700
|
+
});
|
|
701
|
+
header.append(byline, time);
|
|
702
|
+
const comment = createElement(document2, "p", {
|
|
703
|
+
className: "planr-thread-comment",
|
|
704
|
+
text: pin.comment
|
|
705
|
+
});
|
|
706
|
+
const lifecycle = createElement(document2, "div", { className: "planr-thread-actions" });
|
|
707
|
+
lifecycle.append(
|
|
708
|
+
createElement(document2, "span", {
|
|
709
|
+
className: "planr-thread-status",
|
|
710
|
+
text: pin.status,
|
|
711
|
+
attributes: { title: `Status: ${pin.status}` }
|
|
712
|
+
}),
|
|
713
|
+
createElement(document2, "button", {
|
|
714
|
+
text: pin.status === "resolved" ? "Reopen" : "Resolve",
|
|
715
|
+
attributes: {
|
|
716
|
+
type: "button",
|
|
717
|
+
"data-planr-thread-action": pin.status === "resolved" ? "reopen" : "resolve",
|
|
718
|
+
"data-planr-pin-id": pin.id
|
|
719
|
+
}
|
|
720
|
+
}),
|
|
721
|
+
createElement(document2, "button", {
|
|
722
|
+
text: "Show pin",
|
|
723
|
+
attributes: {
|
|
724
|
+
type: "button",
|
|
725
|
+
"data-planr-thread-focus": pin.id,
|
|
726
|
+
"aria-controls": annotationDomIds(pin.id).pin
|
|
727
|
+
}
|
|
728
|
+
})
|
|
729
|
+
);
|
|
730
|
+
const replies = createElement(document2, "ol", {
|
|
731
|
+
className: "planr-replies",
|
|
732
|
+
attributes: { "aria-label": "Replies" }
|
|
733
|
+
});
|
|
734
|
+
const earlierReplies = description.compact ? Math.max(0, pin.replies.length - (description.replyLimit || 2)) : 0;
|
|
735
|
+
for (const reply of pin.replies.slice(earlierReplies))
|
|
736
|
+
replies.append(renderReply(document2, reply));
|
|
737
|
+
const editor = renderReplyForm(document2, pin, description.replyExpanded);
|
|
738
|
+
article.append(header, comment);
|
|
739
|
+
if (description.compact && pin.comment.length > 240) {
|
|
740
|
+
comment.dataset.planrCommentCollapsed = String(!description.commentExpanded);
|
|
741
|
+
article.append(
|
|
742
|
+
createElement(document2, "button", {
|
|
743
|
+
className: "planr-comment-expand",
|
|
744
|
+
text: description.commentExpanded ? "Show less" : "Read full comment",
|
|
745
|
+
attributes: {
|
|
746
|
+
type: "button",
|
|
747
|
+
id: `${annotationDomIds(pin.id).thread}-expand`,
|
|
748
|
+
"data-planr-comment-expand": pin.id,
|
|
749
|
+
"aria-expanded": String(Boolean(description.commentExpanded))
|
|
750
|
+
}
|
|
751
|
+
})
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
if (earlierReplies)
|
|
755
|
+
article.append(
|
|
756
|
+
createElement(document2, "button", {
|
|
757
|
+
className: "planr-history-expand",
|
|
758
|
+
text: `Show ${earlierReplies} earlier ${earlierReplies === 1 ? "reply" : "replies"}`,
|
|
759
|
+
attributes: {
|
|
760
|
+
type: "button",
|
|
761
|
+
id: `${annotationDomIds(pin.id).thread}-history`,
|
|
762
|
+
"data-planr-history-expand": pin.id,
|
|
763
|
+
"aria-expanded": "false"
|
|
764
|
+
}
|
|
765
|
+
})
|
|
766
|
+
);
|
|
767
|
+
if (pin.replies.length) article.append(replies);
|
|
768
|
+
if (description.compact) {
|
|
769
|
+
lifecycle.append(editor.querySelector("[data-planr-reply-toggle]"));
|
|
770
|
+
article.append(lifecycle, editor);
|
|
771
|
+
} else article.append(lifecycle, editor);
|
|
772
|
+
return article;
|
|
773
|
+
}
|
|
774
|
+
function decisionCopy(decision) {
|
|
775
|
+
if (decision === "approved") return "Review approved";
|
|
776
|
+
if (decision === "changes_requested") return "Changes requested";
|
|
777
|
+
return "Decision pending";
|
|
778
|
+
}
|
|
779
|
+
function mountArtifactFeedbackRail({
|
|
780
|
+
root,
|
|
781
|
+
document: document2 = root?.ownerDocument,
|
|
782
|
+
window = document2?.defaultView,
|
|
783
|
+
controller: providedController,
|
|
784
|
+
initialReview = null,
|
|
785
|
+
reviewOf,
|
|
786
|
+
reviewId,
|
|
787
|
+
identity = null,
|
|
788
|
+
createId = defaultCreateId,
|
|
789
|
+
now = defaultNow,
|
|
790
|
+
onSelectPin,
|
|
791
|
+
presentation: initialPresentation = {}
|
|
792
|
+
} = {}) {
|
|
793
|
+
if (!root || !document2 || !window) invalid("A browser root, document, and window are required.");
|
|
794
|
+
const slot = root.querySelector('[data-planr-slot="feedback-rail"]');
|
|
795
|
+
const identityInput = root.querySelector("[data-planr-reviewer-name]");
|
|
796
|
+
const identityStatus = root.querySelector("[data-planr-identity-status]");
|
|
797
|
+
const overall = root.querySelector("#planr-overall-note");
|
|
798
|
+
const decisionStatus = root.querySelector('[data-planr-slot="decision-status"]');
|
|
799
|
+
const decisionButtons = [...root.querySelectorAll("[data-planr-decision]")];
|
|
800
|
+
if (!slot || !identityInput || !overall || !decisionStatus || decisionButtons.length === 0) {
|
|
801
|
+
invalid("Artifact feedback renderer slots are missing.");
|
|
802
|
+
}
|
|
803
|
+
const ownsController = !providedController;
|
|
804
|
+
const controller = providedController ?? createArtifactReviewController({
|
|
805
|
+
initialReview,
|
|
806
|
+
reviewOf,
|
|
807
|
+
reviewId,
|
|
808
|
+
identity,
|
|
809
|
+
createId,
|
|
810
|
+
now
|
|
811
|
+
});
|
|
812
|
+
let destroyed = false;
|
|
813
|
+
let presentation = { ...initialPresentation };
|
|
814
|
+
const replyDrafts = /* @__PURE__ */ new Map();
|
|
815
|
+
const expandedReplies = /* @__PURE__ */ new Set();
|
|
816
|
+
const expandedHistory = /* @__PURE__ */ new Map(), expandedComments = /* @__PURE__ */ new Set();
|
|
817
|
+
let visibleLimit = Number.isInteger(initialPresentation.pageSize) ? initialPresentation.pageSize : Infinity;
|
|
818
|
+
const extraDrafts = /* @__PURE__ */ new Map();
|
|
819
|
+
let overallDirty = false;
|
|
820
|
+
let composing = false;
|
|
821
|
+
let renderQueued = false;
|
|
822
|
+
const captureDrafts = () => {
|
|
823
|
+
for (const form of slot.querySelectorAll("[data-planr-reply-form]")) {
|
|
824
|
+
const field = form.elements.namedItem("reply");
|
|
825
|
+
if (field) replyDrafts.set(form.dataset.planrReplyForm, field.value);
|
|
826
|
+
}
|
|
827
|
+
for (const field of slot.querySelectorAll("[data-planr-draft-key]")) {
|
|
828
|
+
if (typeof field.value === "string")
|
|
829
|
+
extraDrafts.set(field.dataset.planrDraftKey, field.value);
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
const snapshotDrafts = () => {
|
|
833
|
+
captureDrafts();
|
|
834
|
+
return cloneFrozenArtifactReview({
|
|
835
|
+
reviewOf: controller.getReview()?.reviewOf ?? reviewOf,
|
|
836
|
+
replies: Object.fromEntries(replyDrafts),
|
|
837
|
+
expandedReplies: [...expandedReplies],
|
|
838
|
+
fields: Object.fromEntries(extraDrafts),
|
|
839
|
+
overall: { value: overall.value, dirty: overallDirty },
|
|
840
|
+
identity: identityInput.value
|
|
841
|
+
});
|
|
842
|
+
};
|
|
843
|
+
const emitDraftChange = () => root.dispatchEvent(
|
|
844
|
+
new window.CustomEvent(ARTIFACT_REVIEW_DRAFT_CHANGE_EVENT, {
|
|
845
|
+
bubbles: true,
|
|
846
|
+
detail: snapshotDrafts()
|
|
847
|
+
})
|
|
848
|
+
);
|
|
849
|
+
const focusDescriptor = () => {
|
|
850
|
+
const element = document2.activeElement;
|
|
851
|
+
if (!element || !slot.contains(element)) return null;
|
|
852
|
+
const thread = element.closest("[data-planr-pin-id]");
|
|
853
|
+
return {
|
|
854
|
+
id: element.id,
|
|
855
|
+
pinId: thread?.dataset.planrPinId,
|
|
856
|
+
draftKey: element.dataset.planrDraftKey,
|
|
857
|
+
action: element.dataset.planrThreadAction,
|
|
858
|
+
showPin: element.dataset.planrThreadFocus,
|
|
859
|
+
tag: element.tagName,
|
|
860
|
+
name: element.name,
|
|
861
|
+
start: element.selectionStart,
|
|
862
|
+
end: element.selectionEnd,
|
|
863
|
+
direction: element.selectionDirection
|
|
864
|
+
};
|
|
865
|
+
};
|
|
866
|
+
const restoreFocus = (descriptor) => {
|
|
867
|
+
if (!descriptor) return;
|
|
868
|
+
const thread = descriptor.pinId && document2.getElementById(artifactReviewThreadDomId(descriptor.pinId));
|
|
869
|
+
const target = descriptor.id && document2.getElementById(descriptor.id) || descriptor.draftKey && [...slot.querySelectorAll("[data-planr-draft-key]")].find(
|
|
870
|
+
(field) => field.dataset.planrDraftKey === descriptor.draftKey
|
|
871
|
+
) || descriptor.action && [...thread?.querySelectorAll("[data-planr-thread-action]") ?? []].find(
|
|
872
|
+
(button) => button.dataset.planrThreadAction === descriptor.action
|
|
873
|
+
) || descriptor.showPin && thread?.querySelector("[data-planr-thread-focus]") || [...thread?.querySelectorAll("button,input,select,textarea") ?? []].find(
|
|
874
|
+
(field) => field.tagName === descriptor.tag && field.name === descriptor.name
|
|
875
|
+
);
|
|
876
|
+
target?.focus?.({ preventScroll: true });
|
|
877
|
+
if (Number.isInteger(descriptor.start))
|
|
878
|
+
target?.setSelectionRange?.(descriptor.start, descriptor.end, descriptor.direction);
|
|
879
|
+
};
|
|
880
|
+
const showError = (error) => {
|
|
881
|
+
const target = root.querySelector("#planr-review-error");
|
|
882
|
+
if (!target) return;
|
|
883
|
+
target.textContent = error?.message ?? String(error);
|
|
884
|
+
target.hidden = false;
|
|
885
|
+
};
|
|
886
|
+
const clearError = () => {
|
|
887
|
+
const target = root.querySelector("#planr-review-error");
|
|
888
|
+
if (!target) return;
|
|
889
|
+
target.textContent = "";
|
|
890
|
+
target.hidden = true;
|
|
891
|
+
};
|
|
892
|
+
const announce2 = (message) => {
|
|
893
|
+
decisionStatus.textContent = message;
|
|
894
|
+
const live = root.parentElement?.querySelector('[data-planr-slot="review-announcer"]') ?? document2.querySelector('[data-planr-slot="review-announcer"]');
|
|
895
|
+
if (live) live.textContent = message;
|
|
896
|
+
};
|
|
897
|
+
const updateCounts = (pins) => {
|
|
898
|
+
const label = `${pins.length} ${pins.length === 1 ? "comment" : "comments"}`;
|
|
899
|
+
for (const count of root.querySelectorAll(
|
|
900
|
+
'[data-planr-action="feedback"] .planr-count, .planr-review-rail > header .planr-count'
|
|
901
|
+
)) {
|
|
902
|
+
count.textContent = String(pins.length);
|
|
903
|
+
count.setAttribute("aria-label", label);
|
|
904
|
+
}
|
|
905
|
+
const commentsButton = root.querySelector('[data-planr-action="feedback"]');
|
|
906
|
+
commentsButton?.setAttribute("aria-label", commentsButton.dataset.planrReviewLabel || label);
|
|
907
|
+
if (commentsButton?.dataset.planrReviewLabel)
|
|
908
|
+
commentsButton.setAttribute("aria-description", label);
|
|
909
|
+
};
|
|
910
|
+
const render = ({ capture = true } = {}) => {
|
|
911
|
+
if (composing) {
|
|
912
|
+
renderQueued = true;
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
if (capture) captureDrafts();
|
|
916
|
+
const focus = focusDescriptor();
|
|
917
|
+
const scroll = [];
|
|
918
|
+
for (let node = slot; node && root.contains(node); node = node.parentElement)
|
|
919
|
+
scroll.push([node, node.scrollTop, node.scrollLeft]);
|
|
920
|
+
const state = controller.getState();
|
|
921
|
+
const { review, activePinId } = state;
|
|
922
|
+
const allPins = review?.pins ?? [];
|
|
923
|
+
const pins = typeof presentation.filterPin === "function" ? allPins.filter((pin) => presentation.filterPin(pin, state)) : allPins;
|
|
924
|
+
const activeIndex = pins.findIndex((pin) => pin.id === activePinId);
|
|
925
|
+
const visiblePins = pins.slice(0, visibleLimit);
|
|
926
|
+
if (activeIndex >= visibleLimit) visiblePins.push(pins[activeIndex]);
|
|
927
|
+
const fragment = document2.createDocumentFragment();
|
|
928
|
+
const list = createElement(document2, "div", {
|
|
929
|
+
className: "planr-thread-list",
|
|
930
|
+
attributes: { "aria-label": "Comment threads" }
|
|
931
|
+
});
|
|
932
|
+
if (pins.length === 0) {
|
|
933
|
+
list.append(
|
|
934
|
+
createElement(document2, "p", {
|
|
935
|
+
className: "planr-review-empty",
|
|
936
|
+
text: typeof presentation.emptyMessage === "string" ? presentation.emptyMessage : allPins.length ? "No comments match these filters." : "No comments yet. Choose Add comment, then select a point or region in the artifact."
|
|
937
|
+
})
|
|
938
|
+
);
|
|
939
|
+
} else {
|
|
940
|
+
for (const pin of visiblePins) {
|
|
941
|
+
const thread = renderThread(document2, pin, pin.id === activePinId, {
|
|
942
|
+
...presentation.describePin?.(pin, state),
|
|
943
|
+
compact: presentation.compact === true,
|
|
944
|
+
replyLimit: expandedHistory.get(pin.id) || 2,
|
|
945
|
+
commentExpanded: expandedComments.has(pin.id),
|
|
946
|
+
replyExpanded: expandedReplies.has(pin.id)
|
|
947
|
+
});
|
|
948
|
+
presentation.decorateThread?.({ element: thread, pin, state, document: document2 });
|
|
949
|
+
const reply = thread.querySelector('[name="reply"]');
|
|
950
|
+
if (replyDrafts.has(pin.id)) reply.value = replyDrafts.get(pin.id);
|
|
951
|
+
thread.querySelector(".planr-reply-send").disabled = !reply.value.trim();
|
|
952
|
+
for (const field of thread.querySelectorAll("[data-planr-draft-key]")) {
|
|
953
|
+
if (extraDrafts.has(field.dataset.planrDraftKey))
|
|
954
|
+
field.value = extraDrafts.get(field.dataset.planrDraftKey);
|
|
955
|
+
}
|
|
956
|
+
list.append(thread);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
if (pins.length > visibleLimit)
|
|
960
|
+
list.append(
|
|
961
|
+
createElement(document2, "button", {
|
|
962
|
+
className: "planr-threads-more",
|
|
963
|
+
text: `Show more comments · ${Math.min(visibleLimit, pins.length)} of ${pins.length}`,
|
|
964
|
+
attributes: { type: "button", "data-planr-threads-more": "" }
|
|
965
|
+
})
|
|
966
|
+
);
|
|
967
|
+
fragment.append(list);
|
|
968
|
+
slot.replaceChildren(fragment);
|
|
969
|
+
const identityName2 = controller.getIdentity()?.name ?? "";
|
|
970
|
+
if (document2.activeElement !== identityInput) identityInput.value = identityName2;
|
|
971
|
+
if (identityStatus) {
|
|
972
|
+
identityStatus.dataset.planrIdentityReady = String(Boolean(identityName2));
|
|
973
|
+
identityStatus.textContent = identityName2 ? `Comments will appear as ${identityName2}.` : "Used to sign your comments.";
|
|
974
|
+
}
|
|
975
|
+
overall.maxLength = ARTIFACT_REVIEW_LIMITS.text;
|
|
976
|
+
if (!overallDirty && document2.activeElement !== overall) overall.value = review?.overall ?? "";
|
|
977
|
+
for (const button of decisionButtons) {
|
|
978
|
+
button.setAttribute(
|
|
979
|
+
"aria-pressed",
|
|
980
|
+
String(button.dataset.planrDecision === (review?.decision ?? "pending"))
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
decisionStatus.textContent = decisionCopy(review?.decision ?? "pending");
|
|
984
|
+
updateCounts(allPins);
|
|
985
|
+
restoreFocus(focus);
|
|
986
|
+
for (const [node, top, left] of scroll) {
|
|
987
|
+
node.scrollTop = top;
|
|
988
|
+
node.scrollLeft = left;
|
|
989
|
+
}
|
|
990
|
+
const openMetric = root.querySelector('[data-planr-metric="open"]');
|
|
991
|
+
if (openMetric)
|
|
992
|
+
openMetric.textContent = `${allPins.filter(({ status }) => status !== "resolved").length} open`;
|
|
993
|
+
};
|
|
994
|
+
const focusThread = (pinId) => {
|
|
995
|
+
const thread = document2.getElementById(artifactReviewThreadDomId(pinId));
|
|
996
|
+
thread?.focus({ preventScroll: true });
|
|
997
|
+
thread?.scrollIntoView?.({ block: "nearest" });
|
|
998
|
+
};
|
|
999
|
+
const emitReview = (review) => {
|
|
1000
|
+
const detail = cloneFrozenArtifactReview(review);
|
|
1001
|
+
root.dispatchEvent(
|
|
1002
|
+
new window.CustomEvent(ARTIFACT_REVIEW_CHANGE_EVENT, {
|
|
1003
|
+
detail,
|
|
1004
|
+
bubbles: true
|
|
1005
|
+
})
|
|
1006
|
+
);
|
|
1007
|
+
};
|
|
1008
|
+
const unsubscribe = controller.subscribe((state, change) => {
|
|
1009
|
+
if (destroyed) return;
|
|
1010
|
+
if (["review", "review-replaced", "selection"].includes(change.type)) render();
|
|
1011
|
+
if (["review", "review-replaced"].includes(change.type) && state.review)
|
|
1012
|
+
emitReview(state.review);
|
|
1013
|
+
});
|
|
1014
|
+
const onInput = (event) => {
|
|
1015
|
+
if (event.target !== identityInput) return;
|
|
1016
|
+
try {
|
|
1017
|
+
controller.setIdentity(event.target.value ? { name: event.target.value } : null);
|
|
1018
|
+
const name = controller.getIdentity()?.name ?? "";
|
|
1019
|
+
if (identityStatus) {
|
|
1020
|
+
identityStatus.dataset.planrIdentityReady = String(Boolean(name));
|
|
1021
|
+
identityStatus.textContent = name ? `Comments will appear as ${name}.` : "Used to sign your comments.";
|
|
1022
|
+
}
|
|
1023
|
+
clearError();
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
showError(error);
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
const setReplyExpanded = (pinId, expanded, { focus = false } = {}) => {
|
|
1029
|
+
const thread = document2.getElementById(artifactReviewThreadDomId(pinId));
|
|
1030
|
+
const form = thread?.querySelector("[data-planr-reply-form]");
|
|
1031
|
+
const toggle = thread?.querySelector("[data-planr-reply-toggle]");
|
|
1032
|
+
if (!form || !toggle) return;
|
|
1033
|
+
if (expanded) expandedReplies.add(pinId);
|
|
1034
|
+
else expandedReplies.delete(pinId);
|
|
1035
|
+
form.hidden = !expanded;
|
|
1036
|
+
toggle.setAttribute("aria-expanded", String(expanded));
|
|
1037
|
+
toggle.textContent = expanded ? "− Reply" : "+ Reply";
|
|
1038
|
+
toggle.title = expanded ? "Collapse reply" : "Reply to this comment";
|
|
1039
|
+
if (focus)
|
|
1040
|
+
(expanded ? form.elements.namedItem("reply") : toggle).focus({ preventScroll: true });
|
|
1041
|
+
emitDraftChange();
|
|
1042
|
+
};
|
|
1043
|
+
const onKeyDown = (event) => {
|
|
1044
|
+
const form = event.target?.closest?.("[data-planr-reply-form]");
|
|
1045
|
+
if (!form || event.isComposing) return;
|
|
1046
|
+
if (event.key === "Escape") {
|
|
1047
|
+
event.preventDefault();
|
|
1048
|
+
event.stopPropagation();
|
|
1049
|
+
setReplyExpanded(form.dataset.planrReplyForm, false, { focus: true });
|
|
1050
|
+
} else if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
|
|
1051
|
+
event.preventDefault();
|
|
1052
|
+
if (form.elements.namedItem("reply").value.trim()) form.requestSubmit();
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
const emitSelection = (pinId) => {
|
|
1056
|
+
controller.selectPin(pinId);
|
|
1057
|
+
root.dispatchEvent(
|
|
1058
|
+
new window.CustomEvent(ARTIFACT_REVIEW_SELECT_EVENT, {
|
|
1059
|
+
detail: deepFreeze({ pinId, source: "thread" }),
|
|
1060
|
+
bubbles: true
|
|
1061
|
+
})
|
|
1062
|
+
);
|
|
1063
|
+
onSelectPin?.(pinId);
|
|
1064
|
+
};
|
|
1065
|
+
const onClick = (event) => {
|
|
1066
|
+
if (event.target.closest?.("[data-planr-threads-more]")) {
|
|
1067
|
+
visibleLimit += presentation.pageSize || 40;
|
|
1068
|
+
render();
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
const historyToggle = event.target.closest?.("[data-planr-history-expand]");
|
|
1072
|
+
if (historyToggle) {
|
|
1073
|
+
const id = historyToggle.dataset.planrHistoryExpand;
|
|
1074
|
+
expandedHistory.set(id, (expandedHistory.get(id) || 2) + 20);
|
|
1075
|
+
render();
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
const commentToggle = event.target.closest?.("[data-planr-comment-expand]");
|
|
1079
|
+
if (commentToggle) {
|
|
1080
|
+
const id = commentToggle.dataset.planrCommentExpand;
|
|
1081
|
+
if (expandedComments.has(id)) expandedComments.delete(id);
|
|
1082
|
+
else expandedComments.add(id);
|
|
1083
|
+
render();
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
const replyToggle = event.target?.closest?.("[data-planr-reply-toggle]");
|
|
1087
|
+
if (replyToggle) {
|
|
1088
|
+
setReplyExpanded(
|
|
1089
|
+
replyToggle.dataset.planrReplyToggle,
|
|
1090
|
+
replyToggle.getAttribute("aria-expanded") !== "true",
|
|
1091
|
+
{ focus: true }
|
|
1092
|
+
);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
const action = event.target?.closest?.("[data-planr-thread-action]");
|
|
1096
|
+
const focus = event.target?.closest?.("[data-planr-thread-focus]");
|
|
1097
|
+
if (action) {
|
|
1098
|
+
try {
|
|
1099
|
+
const pinId = action.dataset.planrPinId;
|
|
1100
|
+
controller.dispatch({
|
|
1101
|
+
type: "set-status",
|
|
1102
|
+
pinId,
|
|
1103
|
+
status: action.dataset.planrThreadAction === "resolve" ? "resolved" : "open"
|
|
1104
|
+
});
|
|
1105
|
+
announce2(
|
|
1106
|
+
action.dataset.planrThreadAction === "resolve" ? "Comment resolved" : "Comment reopened"
|
|
1107
|
+
);
|
|
1108
|
+
focusThread(pinId);
|
|
1109
|
+
clearError();
|
|
1110
|
+
} catch (error) {
|
|
1111
|
+
showError(error);
|
|
1112
|
+
}
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
if (focus) emitSelection(focus.dataset.planrThreadFocus);
|
|
1116
|
+
};
|
|
1117
|
+
const onSubmit = (event) => {
|
|
1118
|
+
const form = event.target?.closest?.("[data-planr-reply-form]");
|
|
1119
|
+
if (!form) return;
|
|
1120
|
+
event.preventDefault();
|
|
1121
|
+
const textarea = form.elements.namedItem("reply");
|
|
1122
|
+
const comment = textarea.value;
|
|
1123
|
+
if (!comment.trim()) return;
|
|
1124
|
+
const wasExpanded = expandedReplies.has(form.dataset.planrReplyForm);
|
|
1125
|
+
try {
|
|
1126
|
+
textarea.setAttribute("aria-invalid", "false");
|
|
1127
|
+
textarea.value = "";
|
|
1128
|
+
replyDrafts.delete(form.dataset.planrReplyForm);
|
|
1129
|
+
expandedReplies.delete(form.dataset.planrReplyForm);
|
|
1130
|
+
controller.dispatch({
|
|
1131
|
+
type: "add-reply",
|
|
1132
|
+
pinId: form.dataset.planrReplyForm,
|
|
1133
|
+
comment
|
|
1134
|
+
});
|
|
1135
|
+
emitDraftChange();
|
|
1136
|
+
announce2("Reply added");
|
|
1137
|
+
focusThread(form.dataset.planrReplyForm);
|
|
1138
|
+
clearError();
|
|
1139
|
+
} catch (error) {
|
|
1140
|
+
textarea.value = comment;
|
|
1141
|
+
replyDrafts.set(form.dataset.planrReplyForm, comment);
|
|
1142
|
+
if (wasExpanded) expandedReplies.add(form.dataset.planrReplyForm);
|
|
1143
|
+
textarea?.setAttribute("aria-invalid", "true");
|
|
1144
|
+
showError(error);
|
|
1145
|
+
textarea?.focus();
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
const onOverallChange = () => {
|
|
1149
|
+
try {
|
|
1150
|
+
const value = overall.value;
|
|
1151
|
+
controller.dispatch({ type: "set-overall", overall: value });
|
|
1152
|
+
overallDirty = false;
|
|
1153
|
+
announce2("Overall note updated");
|
|
1154
|
+
clearError();
|
|
1155
|
+
} catch (error) {
|
|
1156
|
+
showError(error);
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
const onDecision = (event) => {
|
|
1160
|
+
try {
|
|
1161
|
+
const selected = event.currentTarget.dataset.planrDecision;
|
|
1162
|
+
const decision = controller.getReview()?.decision === selected ? "pending" : selected;
|
|
1163
|
+
controller.dispatch({ type: "set-decision", decision });
|
|
1164
|
+
announce2(decisionCopy(decision));
|
|
1165
|
+
clearError();
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
showError(error);
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
const onSelect = (event) => {
|
|
1171
|
+
if (event.detail?.source === "thread" || typeof event.detail?.pinId !== "string") return;
|
|
1172
|
+
try {
|
|
1173
|
+
controller.selectPin(event.detail.pinId);
|
|
1174
|
+
focusThread(event.detail.pinId);
|
|
1175
|
+
} catch (error) {
|
|
1176
|
+
showError(error);
|
|
1177
|
+
}
|
|
1178
|
+
};
|
|
1179
|
+
let lastOpened = null;
|
|
1180
|
+
const onThreadOpen = (event) => {
|
|
1181
|
+
const thread = event.target.closest?.(".planr-thread[data-planr-pin-id]");
|
|
1182
|
+
if (!thread || !slot.contains(thread)) return;
|
|
1183
|
+
const pin = controller.getReview()?.pins.find((entry) => entry.id === thread.dataset.planrPinId);
|
|
1184
|
+
const key = pin && `${pin.id}:${pin.replies.map((reply) => reply.id).join(",")}`;
|
|
1185
|
+
if (!key || lastOpened === key) return;
|
|
1186
|
+
lastOpened = key;
|
|
1187
|
+
presentation.onThreadOpen?.(pin.id, controller.getState());
|
|
1188
|
+
root.dispatchEvent(
|
|
1189
|
+
new window.CustomEvent("planr:artifact-thread-open", {
|
|
1190
|
+
bubbles: true,
|
|
1191
|
+
detail: Object.freeze({ pinId: pin.id })
|
|
1192
|
+
})
|
|
1193
|
+
);
|
|
1194
|
+
};
|
|
1195
|
+
const onDraftInput = (event) => {
|
|
1196
|
+
const form = event.target.closest?.("[data-planr-reply-form]");
|
|
1197
|
+
if (form)
|
|
1198
|
+
form.querySelector(".planr-reply-send").disabled = !form.elements.namedItem("reply").value.trim();
|
|
1199
|
+
emitDraftChange();
|
|
1200
|
+
};
|
|
1201
|
+
const onOverallInput = () => {
|
|
1202
|
+
overallDirty = true;
|
|
1203
|
+
emitDraftChange();
|
|
1204
|
+
};
|
|
1205
|
+
const onCompositionStart = () => {
|
|
1206
|
+
composing = true;
|
|
1207
|
+
};
|
|
1208
|
+
const onCompositionEnd = () => {
|
|
1209
|
+
composing = false;
|
|
1210
|
+
if (renderQueued) {
|
|
1211
|
+
renderQueued = false;
|
|
1212
|
+
render();
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
slot.addEventListener("input", onDraftInput);
|
|
1216
|
+
slot.addEventListener("focusin", onThreadOpen);
|
|
1217
|
+
slot.addEventListener("click", onThreadOpen);
|
|
1218
|
+
slot.addEventListener("compositionstart", onCompositionStart);
|
|
1219
|
+
slot.addEventListener("compositionend", onCompositionEnd);
|
|
1220
|
+
overall.addEventListener("input", onOverallInput);
|
|
1221
|
+
identityInput.addEventListener("input", onInput);
|
|
1222
|
+
slot.addEventListener("click", onClick);
|
|
1223
|
+
slot.addEventListener("submit", onSubmit);
|
|
1224
|
+
slot.addEventListener("keydown", onKeyDown);
|
|
1225
|
+
overall.addEventListener("change", onOverallChange);
|
|
1226
|
+
for (const button of decisionButtons) button.addEventListener("click", onDecision);
|
|
1227
|
+
root.addEventListener(ARTIFACT_REVIEW_SELECT_EVENT, onSelect);
|
|
1228
|
+
render();
|
|
1229
|
+
return Object.freeze({
|
|
1230
|
+
controller,
|
|
1231
|
+
getReview: () => controller.getReview(),
|
|
1232
|
+
getState: () => controller.getState(),
|
|
1233
|
+
getIdentity: () => controller.getIdentity(),
|
|
1234
|
+
setIdentity: (value) => controller.setIdentity(value),
|
|
1235
|
+
dispatch: (action) => controller.dispatch(action),
|
|
1236
|
+
replaceReview: (review) => controller.replaceReview(review),
|
|
1237
|
+
selectPin: (pinId) => controller.selectPin(pinId),
|
|
1238
|
+
render,
|
|
1239
|
+
setPresentation(options = {}) {
|
|
1240
|
+
if (options.filterKey !== presentation.filterKey || options.pageSize !== void 0 && options.pageSize !== presentation.pageSize)
|
|
1241
|
+
visibleLimit = options.pageSize || presentation.pageSize || Infinity;
|
|
1242
|
+
presentation = { ...presentation, ...options };
|
|
1243
|
+
render();
|
|
1244
|
+
},
|
|
1245
|
+
snapshotDrafts,
|
|
1246
|
+
getReviewOf: () => controller.getReview()?.reviewOf ?? reviewOf,
|
|
1247
|
+
restoreDrafts(snapshot) {
|
|
1248
|
+
const digest = controller.getReview()?.reviewOf ?? reviewOf;
|
|
1249
|
+
if (!snapshot || snapshot.reviewOf !== digest) return false;
|
|
1250
|
+
if (typeof snapshot.identity === "string") {
|
|
1251
|
+
const name = snapshot.identity.slice(0, ARTIFACT_REVIEW_LIMITS.authorName);
|
|
1252
|
+
controller.setIdentity(name.trim() ? { ...controller.getIdentity(), name } : null);
|
|
1253
|
+
}
|
|
1254
|
+
for (const [id, value] of Object.entries(snapshot.replies ?? {}).slice(
|
|
1255
|
+
0,
|
|
1256
|
+
ARTIFACT_REVIEW_LIMITS.pins
|
|
1257
|
+
)) {
|
|
1258
|
+
if (id.length <= ARTIFACT_REVIEW_LIMITS.id && typeof value === "string")
|
|
1259
|
+
replyDrafts.set(id, value.slice(0, ARTIFACT_REVIEW_LIMITS.text));
|
|
1260
|
+
}
|
|
1261
|
+
expandedReplies.clear();
|
|
1262
|
+
const expanded = Array.isArray(snapshot.expandedReplies) ? snapshot.expandedReplies : [...replyDrafts.keys()];
|
|
1263
|
+
for (const id of expanded.slice(0, ARTIFACT_REVIEW_LIMITS.pins))
|
|
1264
|
+
if (typeof id === "string" && id.length <= ARTIFACT_REVIEW_LIMITS.id)
|
|
1265
|
+
expandedReplies.add(id);
|
|
1266
|
+
for (const [key, value] of Object.entries(snapshot.fields ?? {}).slice(
|
|
1267
|
+
0,
|
|
1268
|
+
ARTIFACT_REVIEW_LIMITS.pins
|
|
1269
|
+
)) {
|
|
1270
|
+
if (key.length <= 512 && typeof value === "string")
|
|
1271
|
+
extraDrafts.set(key, value.slice(0, ARTIFACT_REVIEW_LIMITS.text));
|
|
1272
|
+
}
|
|
1273
|
+
overallDirty = snapshot.overall?.dirty === true;
|
|
1274
|
+
if (overallDirty && typeof snapshot.overall?.value === "string")
|
|
1275
|
+
overall.value = snapshot.overall.value.slice(0, ARTIFACT_REVIEW_LIMITS.text);
|
|
1276
|
+
render({ capture: false });
|
|
1277
|
+
return true;
|
|
1278
|
+
},
|
|
1279
|
+
focusThread,
|
|
1280
|
+
destroy() {
|
|
1281
|
+
if (destroyed) return;
|
|
1282
|
+
destroyed = true;
|
|
1283
|
+
unsubscribe();
|
|
1284
|
+
slot.removeEventListener("input", onDraftInput);
|
|
1285
|
+
slot.removeEventListener("focusin", onThreadOpen);
|
|
1286
|
+
slot.removeEventListener("click", onThreadOpen);
|
|
1287
|
+
slot.removeEventListener("compositionstart", onCompositionStart);
|
|
1288
|
+
slot.removeEventListener("compositionend", onCompositionEnd);
|
|
1289
|
+
overall.removeEventListener("input", onOverallInput);
|
|
1290
|
+
identityInput.removeEventListener("input", onInput);
|
|
1291
|
+
slot.removeEventListener("click", onClick);
|
|
1292
|
+
slot.removeEventListener("submit", onSubmit);
|
|
1293
|
+
slot.removeEventListener("keydown", onKeyDown);
|
|
1294
|
+
overall.removeEventListener("change", onOverallChange);
|
|
1295
|
+
for (const button of decisionButtons) button.removeEventListener("click", onDecision);
|
|
1296
|
+
root.removeEventListener(ARTIFACT_REVIEW_SELECT_EVENT, onSelect);
|
|
1297
|
+
if (ownsController) controller.destroy();
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// local-stage-stub:./hosted-viewer.mjs
|
|
1303
|
+
function mountHostedArtifactViewer() {
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// local-stage-stub:./share-dialog.mjs
|
|
1308
|
+
function mountArtifactShareDialog({ root } = {}) {
|
|
1309
|
+
const trigger = root?.querySelector?.('[data-planr-action="share"]');
|
|
1310
|
+
if (trigger && !trigger.hasAttribute("data-planr-share-adapter")) trigger.hidden = true;
|
|
1311
|
+
return null;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// lib/artifact/ui/stage-mount.mjs
|
|
1315
|
+
var ARTIFACT_STAGE_MOUNT_EVENT = "planr:artifact-stage-mount";
|
|
1316
|
+
function publishArtifactStage(window, stage) {
|
|
1317
|
+
window.__openPlanrArtifactStage = stage;
|
|
1318
|
+
window.dispatchEvent(new window.CustomEvent(ARTIFACT_STAGE_MOUNT_EVENT, { detail: stage }));
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// lib/artifact/ui/stage-payload.mjs
|
|
1322
|
+
var PRESENTATIONS = Object.freeze(["document", "canvas"]);
|
|
1323
|
+
function positiveInteger(value, fallback) {
|
|
1324
|
+
return Number.isInteger(value) && value > 0 ? value : fallback;
|
|
1325
|
+
}
|
|
1326
|
+
function freezeArtifactMetadata(artifact, index) {
|
|
1327
|
+
if (!artifact || typeof artifact !== "object") {
|
|
1328
|
+
throw new TypeError(`Artifact ${index + 1} must be an object.`);
|
|
1329
|
+
}
|
|
1330
|
+
if (typeof artifact.id !== "string" || artifact.id.length === 0) {
|
|
1331
|
+
throw new TypeError(`Artifact ${index + 1} requires an id.`);
|
|
1332
|
+
}
|
|
1333
|
+
return Object.freeze({
|
|
1334
|
+
id: artifact.id,
|
|
1335
|
+
title: typeof artifact.title === "string" && artifact.title.length > 0 ? artifact.title : `Artifact ${index + 1}`,
|
|
1336
|
+
sha256: typeof artifact.sha256 === "string" ? artifact.sha256 : "",
|
|
1337
|
+
viewport: Object.freeze({
|
|
1338
|
+
width: positiveInteger(artifact.viewport?.width, 1440),
|
|
1339
|
+
height: positiveInteger(artifact.viewport?.height, 900)
|
|
1340
|
+
}),
|
|
1341
|
+
colorScheme: ["light", "dark"].includes(artifact.colorScheme) ? artifact.colorScheme : "light"
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
function requestedArtifactId(value) {
|
|
1345
|
+
if (typeof value === "string") return value;
|
|
1346
|
+
return typeof value?.id === "string" ? value.id : "";
|
|
1347
|
+
}
|
|
1348
|
+
function availableId(artifacts, requested, fallback = "") {
|
|
1349
|
+
return artifacts.some(({ id }) => id === requested) ? requested : fallback;
|
|
1350
|
+
}
|
|
1351
|
+
function normalizeViewMode(value, artifactCount) {
|
|
1352
|
+
if (artifactCount < 2) return "single";
|
|
1353
|
+
return ["single", "variants", "split"].includes(value) ? value : "variants";
|
|
1354
|
+
}
|
|
1355
|
+
function createArtifactStagePayload(envelope = {}, { viewer } = {}) {
|
|
1356
|
+
const artifacts = Object.freeze(
|
|
1357
|
+
(Array.isArray(envelope?.artifacts) ? envelope.artifacts : []).map(freezeArtifactMetadata)
|
|
1358
|
+
);
|
|
1359
|
+
const sourceViewer = viewer && typeof viewer === "object" ? viewer : envelope?.viewer && typeof envelope.viewer === "object" ? envelope.viewer : {};
|
|
1360
|
+
const firstId = artifacts[0]?.id ?? "";
|
|
1361
|
+
const activeArtifactId = availableId(
|
|
1362
|
+
artifacts,
|
|
1363
|
+
requestedArtifactId(sourceViewer.activeArtifactId),
|
|
1364
|
+
firstId
|
|
1365
|
+
);
|
|
1366
|
+
const mode = normalizeViewMode(sourceViewer.mode, artifacts.length);
|
|
1367
|
+
return Object.freeze({
|
|
1368
|
+
schemaVersion: "1.0.0",
|
|
1369
|
+
artifacts,
|
|
1370
|
+
viewer: Object.freeze({
|
|
1371
|
+
mode,
|
|
1372
|
+
activeArtifactId,
|
|
1373
|
+
...PRESENTATIONS.includes(sourceViewer.presentation) ? { presentation: sourceViewer.presentation } : {}
|
|
1374
|
+
})
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// lib/artifact/ui/stage.mjs
|
|
1379
|
+
var ARTIFACT_STAGE_EVENTS = Object.freeze({
|
|
1380
|
+
change: "planr:stage-change",
|
|
1381
|
+
point: "planr:artifact-point",
|
|
1382
|
+
region: "planr:artifact-region",
|
|
1383
|
+
layout: "planr:artifact-layout",
|
|
1384
|
+
frameState: "planr:artifact-frame-state"
|
|
1385
|
+
});
|
|
1386
|
+
var ARTIFACT_STAGE_LIMITS = Object.freeze({
|
|
1387
|
+
defaultZoom: 72,
|
|
1388
|
+
minZoom: 25,
|
|
1389
|
+
maxZoom: 200,
|
|
1390
|
+
zoomStep: 10,
|
|
1391
|
+
maxDocumentWidth: 16384,
|
|
1392
|
+
maxDocumentHeight: 262144,
|
|
1393
|
+
defaultFrameBudget: 3,
|
|
1394
|
+
frameLoadTimeoutMs: 15e3
|
|
1395
|
+
});
|
|
1396
|
+
var VIEW_MODES = Object.freeze(["single", "variants", "split"]);
|
|
1397
|
+
var REVIEW_MODES = Object.freeze(["interact", "comment"]);
|
|
1398
|
+
var THEMES = Object.freeze(["auto", "light", "dark"]);
|
|
1399
|
+
var PRESENTATIONS2 = Object.freeze(["document", "canvas"]);
|
|
1400
|
+
var STATUSES = Object.freeze([
|
|
1401
|
+
"ready",
|
|
1402
|
+
"empty",
|
|
1403
|
+
"bundling",
|
|
1404
|
+
"loading",
|
|
1405
|
+
"invalid",
|
|
1406
|
+
"expired",
|
|
1407
|
+
"decryption-failed",
|
|
1408
|
+
"unsupported-browser"
|
|
1409
|
+
]);
|
|
1410
|
+
var STATUS_COPY = Object.freeze({
|
|
1411
|
+
empty: Object.freeze({
|
|
1412
|
+
title: "No artifact content",
|
|
1413
|
+
detail: "Choose a bundled HTML artifact to begin this review."
|
|
1414
|
+
}),
|
|
1415
|
+
bundling: Object.freeze({
|
|
1416
|
+
title: "Bundling artifact",
|
|
1417
|
+
detail: "Packaging local scripts, styles, fonts, and images without network access."
|
|
1418
|
+
}),
|
|
1419
|
+
loading: Object.freeze({
|
|
1420
|
+
title: "Loading private review",
|
|
1421
|
+
detail: "Validating the envelope and frozen artifact viewport."
|
|
1422
|
+
}),
|
|
1423
|
+
invalid: Object.freeze({
|
|
1424
|
+
title: "This review is invalid",
|
|
1425
|
+
detail: "The artifact envelope could not be decoded or validated."
|
|
1426
|
+
}),
|
|
1427
|
+
expired: Object.freeze({
|
|
1428
|
+
title: "This encrypted review expired",
|
|
1429
|
+
detail: "Ask the sender for a new immutable review link."
|
|
1430
|
+
}),
|
|
1431
|
+
"decryption-failed": Object.freeze({
|
|
1432
|
+
title: "This key cannot decrypt the review",
|
|
1433
|
+
detail: "Use the complete link, including its private fragment key."
|
|
1434
|
+
}),
|
|
1435
|
+
"unsupported-browser": Object.freeze({
|
|
1436
|
+
title: "Browser support is required",
|
|
1437
|
+
detail: "Use a current browser with Blob URL support to review this artifact."
|
|
1438
|
+
})
|
|
1439
|
+
});
|
|
1440
|
+
function member(value, allowed, fallback) {
|
|
1441
|
+
return allowed.includes(value) ? value : fallback;
|
|
1442
|
+
}
|
|
1443
|
+
function finite2(value, fallback = 0) {
|
|
1444
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
1445
|
+
}
|
|
1446
|
+
function clamp2(value, min, max) {
|
|
1447
|
+
return Math.min(max, Math.max(min, value));
|
|
1448
|
+
}
|
|
1449
|
+
function normalized2(value) {
|
|
1450
|
+
return Math.round(clamp2(finite2(value), 0, 1) * 1e6) / 1e6;
|
|
1451
|
+
}
|
|
1452
|
+
function artifactMetadata(artifact) {
|
|
1453
|
+
return Object.freeze({
|
|
1454
|
+
id: artifact.id,
|
|
1455
|
+
title: artifact.title,
|
|
1456
|
+
sha256: artifact.sha256,
|
|
1457
|
+
viewport: artifact.viewport,
|
|
1458
|
+
colorScheme: artifact.colorScheme
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
function requestedArtifactId2(value) {
|
|
1462
|
+
if (typeof value === "string") return value;
|
|
1463
|
+
return typeof value?.id === "string" ? value.id : "";
|
|
1464
|
+
}
|
|
1465
|
+
function availableId2(artifacts, requested, fallback = "") {
|
|
1466
|
+
return artifacts.some(({ id }) => id === requested) ? requested : fallback;
|
|
1467
|
+
}
|
|
1468
|
+
function comparisonIdFor(artifacts, activeArtifactId, requested = "") {
|
|
1469
|
+
if (requested !== activeArtifactId && artifacts.some(({ id }) => id === requested))
|
|
1470
|
+
return requested;
|
|
1471
|
+
return artifacts.find(({ id }) => id !== activeArtifactId)?.id ?? "";
|
|
1472
|
+
}
|
|
1473
|
+
function normalizeViewMode2(value, artifactCount) {
|
|
1474
|
+
if (artifactCount < 2) return "single";
|
|
1475
|
+
return member(value, VIEW_MODES, "variants");
|
|
1476
|
+
}
|
|
1477
|
+
function createArtifactStageState(payload = {}, shellModel = {}) {
|
|
1478
|
+
const artifacts = Object.freeze(
|
|
1479
|
+
(Array.isArray(payload?.artifacts) ? payload.artifacts : []).map(artifactMetadata)
|
|
1480
|
+
);
|
|
1481
|
+
const firstId = artifacts[0]?.id ?? "";
|
|
1482
|
+
const activeArtifactId = availableId2(
|
|
1483
|
+
artifacts,
|
|
1484
|
+
requestedArtifactId2(shellModel.activeArtifact ?? shellModel.activeArtifactId) || requestedArtifactId2(payload?.viewer?.activeArtifactId),
|
|
1485
|
+
firstId
|
|
1486
|
+
);
|
|
1487
|
+
const comparisonArtifactId = comparisonIdFor(
|
|
1488
|
+
artifacts,
|
|
1489
|
+
activeArtifactId,
|
|
1490
|
+
requestedArtifactId2(shellModel.comparisonArtifact ?? shellModel.comparisonArtifactId)
|
|
1491
|
+
);
|
|
1492
|
+
const statusFallback = artifacts.length === 0 ? "empty" : "ready";
|
|
1493
|
+
let status = member(shellModel.status, STATUSES, statusFallback);
|
|
1494
|
+
if (artifacts.length === 0 && status === "ready") status = "empty";
|
|
1495
|
+
const viewMode = normalizeViewMode2(
|
|
1496
|
+
shellModel.viewMode ?? payload?.viewer?.mode,
|
|
1497
|
+
artifacts.length
|
|
1498
|
+
);
|
|
1499
|
+
const presentation = resolveArtifactPresentation(
|
|
1500
|
+
shellModel.presentation ?? payload?.viewer?.presentation,
|
|
1501
|
+
{ viewMode, artifactCount: artifacts.length }
|
|
1502
|
+
);
|
|
1503
|
+
return Object.freeze({
|
|
1504
|
+
schemaVersion: "1.0.0",
|
|
1505
|
+
artifacts,
|
|
1506
|
+
activeArtifactId,
|
|
1507
|
+
comparisonArtifactId,
|
|
1508
|
+
viewMode,
|
|
1509
|
+
presentation,
|
|
1510
|
+
reviewMode: member(shellModel.reviewMode, REVIEW_MODES, "interact"),
|
|
1511
|
+
zoom: clamp2(
|
|
1512
|
+
Number.isInteger(shellModel.zoom) ? shellModel.zoom : ARTIFACT_STAGE_LIMITS.defaultZoom,
|
|
1513
|
+
ARTIFACT_STAGE_LIMITS.minZoom,
|
|
1514
|
+
ARTIFACT_STAGE_LIMITS.maxZoom
|
|
1515
|
+
),
|
|
1516
|
+
railOpen: shellModel.railOpen === void 0 ? presentation === "canvas" : Boolean(shellModel.railOpen),
|
|
1517
|
+
theme: member(shellModel.theme, THEMES, "auto"),
|
|
1518
|
+
status
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
function nextState(state, changes) {
|
|
1522
|
+
return Object.freeze({ ...state, ...changes });
|
|
1523
|
+
}
|
|
1524
|
+
function reduceArtifactStageState(state, action = {}) {
|
|
1525
|
+
switch (action.type) {
|
|
1526
|
+
case "set-active": {
|
|
1527
|
+
const id = availableId2(state.artifacts, action.artifactId);
|
|
1528
|
+
if (!id || id === state.activeArtifactId) return state;
|
|
1529
|
+
const comparisonArtifactId = id === state.comparisonArtifactId ? state.activeArtifactId : comparisonIdFor(state.artifacts, id, state.comparisonArtifactId);
|
|
1530
|
+
return nextState(state, { activeArtifactId: id, comparisonArtifactId });
|
|
1531
|
+
}
|
|
1532
|
+
case "set-comparison": {
|
|
1533
|
+
const id = comparisonIdFor(state.artifacts, state.activeArtifactId, action.artifactId);
|
|
1534
|
+
return id === state.comparisonArtifactId ? state : nextState(state, { comparisonArtifactId: id });
|
|
1535
|
+
}
|
|
1536
|
+
case "set-view-mode": {
|
|
1537
|
+
const viewMode = normalizeViewMode2(action.viewMode, state.artifacts.length);
|
|
1538
|
+
return viewMode === state.viewMode ? state : nextState(state, { viewMode });
|
|
1539
|
+
}
|
|
1540
|
+
case "set-review-mode": {
|
|
1541
|
+
const reviewMode = member(action.reviewMode, REVIEW_MODES, state.reviewMode);
|
|
1542
|
+
return reviewMode === state.reviewMode ? state : nextState(state, { reviewMode });
|
|
1543
|
+
}
|
|
1544
|
+
case "set-zoom": {
|
|
1545
|
+
const zoom = clamp2(
|
|
1546
|
+
Math.round(finite2(action.zoom, state.zoom)),
|
|
1547
|
+
ARTIFACT_STAGE_LIMITS.minZoom,
|
|
1548
|
+
ARTIFACT_STAGE_LIMITS.maxZoom
|
|
1549
|
+
);
|
|
1550
|
+
return zoom === state.zoom ? state : nextState(state, { zoom });
|
|
1551
|
+
}
|
|
1552
|
+
case "zoom-by":
|
|
1553
|
+
return reduceArtifactStageState(state, {
|
|
1554
|
+
type: "set-zoom",
|
|
1555
|
+
zoom: state.zoom + finite2(action.delta)
|
|
1556
|
+
});
|
|
1557
|
+
case "set-rail-open": {
|
|
1558
|
+
const railOpen = Boolean(action.railOpen);
|
|
1559
|
+
return railOpen === state.railOpen ? state : nextState(state, { railOpen });
|
|
1560
|
+
}
|
|
1561
|
+
case "toggle-rail":
|
|
1562
|
+
return nextState(state, { railOpen: !state.railOpen });
|
|
1563
|
+
case "set-theme": {
|
|
1564
|
+
const theme = member(action.theme, THEMES, state.theme);
|
|
1565
|
+
return theme === state.theme ? state : nextState(state, { theme });
|
|
1566
|
+
}
|
|
1567
|
+
case "cycle-theme": {
|
|
1568
|
+
const index = THEMES.indexOf(state.theme);
|
|
1569
|
+
return nextState(state, { theme: THEMES[(index + 1) % THEMES.length] });
|
|
1570
|
+
}
|
|
1571
|
+
case "set-status": {
|
|
1572
|
+
const status = member(action.status, STATUSES, state.status);
|
|
1573
|
+
return status === state.status ? state : nextState(state, { status });
|
|
1574
|
+
}
|
|
1575
|
+
default:
|
|
1576
|
+
return state;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
function visibleArtifactIds(state) {
|
|
1580
|
+
if (!state.activeArtifactId) return Object.freeze([]);
|
|
1581
|
+
if (state.viewMode === "split" && state.comparisonArtifactId) {
|
|
1582
|
+
return Object.freeze([state.activeArtifactId, state.comparisonArtifactId]);
|
|
1583
|
+
}
|
|
1584
|
+
return Object.freeze([state.activeArtifactId]);
|
|
1585
|
+
}
|
|
1586
|
+
function assertRect2(rect) {
|
|
1587
|
+
if (!rect || !Number.isFinite(rect.left) || !Number.isFinite(rect.top) || !Number.isFinite(rect.width) || !Number.isFinite(rect.height) || rect.width <= 0 || rect.height <= 0) {
|
|
1588
|
+
throw new RangeError("Artifact bounds must have positive finite dimensions.");
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
function clientPointToNormalized(rect, point) {
|
|
1592
|
+
assertRect2(rect);
|
|
1593
|
+
return Object.freeze({
|
|
1594
|
+
x: normalized2((finite2(point?.x ?? point?.clientX) - rect.left) / rect.width),
|
|
1595
|
+
y: normalized2((finite2(point?.y ?? point?.clientY) - rect.top) / rect.height)
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
function normalizedPointToClient(rect, point) {
|
|
1599
|
+
assertRect2(rect);
|
|
1600
|
+
return Object.freeze({
|
|
1601
|
+
x: rect.left + normalized2(point?.x) * rect.width,
|
|
1602
|
+
y: rect.top + normalized2(point?.y) * rect.height
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
function parseDataScript(document2, id) {
|
|
1606
|
+
const node = document2.getElementById(id);
|
|
1607
|
+
if (!node) throw new Error(`Missing artifact shell data: ${id}`);
|
|
1608
|
+
return JSON.parse(node.textContent ?? "null");
|
|
1609
|
+
}
|
|
1610
|
+
function isEditableTarget(target) {
|
|
1611
|
+
const HTMLElement = target?.ownerDocument?.defaultView?.HTMLElement;
|
|
1612
|
+
return Boolean(
|
|
1613
|
+
HTMLElement && target instanceof HTMLElement && (target.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName))
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
function stageArtifactById(state, id) {
|
|
1617
|
+
return state.artifacts.find((artifact) => artifact.id === id) ?? null;
|
|
1618
|
+
}
|
|
1619
|
+
function updateStatus(document2, state) {
|
|
1620
|
+
const statusPanel = document2.querySelector(".planr-stage-status");
|
|
1621
|
+
const surface = document2.querySelector(".planr-stage-surface");
|
|
1622
|
+
if (!statusPanel || !surface) return;
|
|
1623
|
+
const ready = state.status === "ready";
|
|
1624
|
+
statusPanel.hidden = ready;
|
|
1625
|
+
surface.toggleAttribute("inert", !ready);
|
|
1626
|
+
surface.setAttribute("aria-hidden", String(!ready));
|
|
1627
|
+
if (ready) surface.removeAttribute("aria-hidden");
|
|
1628
|
+
const copy = STATUS_COPY[state.status];
|
|
1629
|
+
if (copy) {
|
|
1630
|
+
const title = statusPanel.querySelector("strong");
|
|
1631
|
+
const detail = statusPanel.querySelector("p");
|
|
1632
|
+
if (title) title.textContent = copy.title;
|
|
1633
|
+
if (detail) detail.textContent = copy.detail;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
function emit(root, window, type, detail) {
|
|
1637
|
+
root.dispatchEvent(new window.CustomEvent(type, { detail, bubbles: true }));
|
|
1638
|
+
}
|
|
1639
|
+
async function htmlForSource(window, source) {
|
|
1640
|
+
if (typeof source === "string" && source.trimStart().startsWith("<")) return source;
|
|
1641
|
+
if (source && typeof source === "object" && typeof source.html === "string") return source.html;
|
|
1642
|
+
if (source instanceof window.Blob) return source.text();
|
|
1643
|
+
if (source instanceof window.ArrayBuffer) return new window.TextDecoder().decode(source);
|
|
1644
|
+
if (window.ArrayBuffer.isView(source)) {
|
|
1645
|
+
return new window.TextDecoder().decode(
|
|
1646
|
+
new window.Uint8Array(source.buffer, source.byteOffset, source.byteLength)
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
return null;
|
|
1650
|
+
}
|
|
1651
|
+
function mountArtifactStage({
|
|
1652
|
+
document: document2 = globalThis.document,
|
|
1653
|
+
window = document2?.defaultView,
|
|
1654
|
+
resolveArtifactSource,
|
|
1655
|
+
sourceTransport = "blob",
|
|
1656
|
+
frameBudget: requestedFrameBudget,
|
|
1657
|
+
frameLoadTimeoutMs = ARTIFACT_STAGE_LIMITS.frameLoadTimeoutMs,
|
|
1658
|
+
bridgeClient,
|
|
1659
|
+
onState,
|
|
1660
|
+
review: reviewOptions = {},
|
|
1661
|
+
share: shareOptions = {},
|
|
1662
|
+
hosted: hostedOptions = {}
|
|
1663
|
+
} = {}) {
|
|
1664
|
+
if (!document2 || !window) return null;
|
|
1665
|
+
const root = document2.querySelector(".planr-shell");
|
|
1666
|
+
if (!root) return null;
|
|
1667
|
+
const initializationError = globalThis.__OPENPLANR_ARTIFACT_STAGE_INITIALIZATION_ERROR__;
|
|
1668
|
+
if (initializationError) {
|
|
1669
|
+
const status = root.querySelector('[data-planr-slot="status"]');
|
|
1670
|
+
if (status) {
|
|
1671
|
+
status.textContent = initializationError;
|
|
1672
|
+
status.setAttribute("data-error", "");
|
|
1673
|
+
status.setAttribute("role", "alert");
|
|
1674
|
+
}
|
|
1675
|
+
root.setAttribute("data-planr-initialization-error", "");
|
|
1676
|
+
return null;
|
|
1677
|
+
}
|
|
1678
|
+
if (!["blob", "srcdoc"].includes(sourceTransport)) {
|
|
1679
|
+
throw new TypeError("Artifact source transport must be blob or srcdoc.");
|
|
1680
|
+
}
|
|
1681
|
+
let payload;
|
|
1682
|
+
let shellModel;
|
|
1683
|
+
let reviewConfig;
|
|
1684
|
+
try {
|
|
1685
|
+
payload = parseDataScript(document2, "planr-artifact-stage-payload");
|
|
1686
|
+
shellModel = parseDataScript(document2, "planr-artifact-shell-model");
|
|
1687
|
+
reviewConfig = parseDataScript(document2, "planr-artifact-review-state");
|
|
1688
|
+
} catch {
|
|
1689
|
+
payload = { artifacts: [], viewer: { mode: "single", activeArtifactId: "" } };
|
|
1690
|
+
shellModel = { status: "invalid" };
|
|
1691
|
+
reviewConfig = { reviewOf: "0".repeat(64), review: null };
|
|
1692
|
+
}
|
|
1693
|
+
let state;
|
|
1694
|
+
try {
|
|
1695
|
+
state = createArtifactStageState(payload, shellModel);
|
|
1696
|
+
} catch {
|
|
1697
|
+
state = createArtifactStageState({}, { status: "invalid" });
|
|
1698
|
+
}
|
|
1699
|
+
const frames = new Map(
|
|
1700
|
+
[...document2.querySelectorAll("[data-planr-artifact-frame]")].map((frame) => [
|
|
1701
|
+
frame.dataset.planrArtifactFrame,
|
|
1702
|
+
frame
|
|
1703
|
+
])
|
|
1704
|
+
);
|
|
1705
|
+
const panels = new Map(
|
|
1706
|
+
[...document2.querySelectorAll(".planr-artifact-panel[data-artifact-id]")].map((panel) => [
|
|
1707
|
+
panel.dataset.artifactId,
|
|
1708
|
+
panel
|
|
1709
|
+
])
|
|
1710
|
+
);
|
|
1711
|
+
const documentLayouts = /* @__PURE__ */ new Map();
|
|
1712
|
+
const cleanup = [];
|
|
1713
|
+
const frameBudget = requestedFrameBudget === void 0 ? Number(root.dataset.planrFrameBudget || ARTIFACT_STAGE_LIMITS.defaultFrameBudget) : requestedFrameBudget;
|
|
1714
|
+
if (frameBudget !== null && (!Number.isInteger(frameBudget) || frameBudget < 1 || frameBudget > 8)) {
|
|
1715
|
+
throw new RangeError(
|
|
1716
|
+
"Artifact frame budget must be between 1 and 8, or null for eager loading."
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
if (!Number.isInteger(frameLoadTimeoutMs) || frameLoadTimeoutMs < 1 || frameLoadTimeoutMs > 12e4) {
|
|
1720
|
+
throw new RangeError("Artifact frame load timeout must be between 1 and 120000 milliseconds.");
|
|
1721
|
+
}
|
|
1722
|
+
const frameDiagnostics = /* @__PURE__ */ new Map();
|
|
1723
|
+
const frameLoads = /* @__PURE__ */ new Map();
|
|
1724
|
+
const frameHolds = /* @__PURE__ */ new Map();
|
|
1725
|
+
const frameHoldWaiters = /* @__PURE__ */ new Set();
|
|
1726
|
+
const frameSlots = /* @__PURE__ */ new Map();
|
|
1727
|
+
if (frameBudget !== null) {
|
|
1728
|
+
for (const [id, frame] of frames) {
|
|
1729
|
+
const host = frame.parentNode;
|
|
1730
|
+
if (!host || !root.contains(host) || frame.hasAttribute("src") || frame.hasAttribute("srcdoc"))
|
|
1731
|
+
continue;
|
|
1732
|
+
const placeholder = document2.createComment("Artifact preview");
|
|
1733
|
+
frameSlots.set(id, { host, placeholder });
|
|
1734
|
+
host.replaceChild(placeholder, frame);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
let frameUse = 0;
|
|
1738
|
+
let frameQueue = Promise.resolve();
|
|
1739
|
+
let activationGeneration = 0;
|
|
1740
|
+
let disposed = false;
|
|
1741
|
+
for (const frame of frames.values()) frame.dataset.planrFrameState = "unloaded";
|
|
1742
|
+
function listen(target, type, handler, options) {
|
|
1743
|
+
target.addEventListener(type, handler, options);
|
|
1744
|
+
cleanup.push(() => target.removeEventListener(type, handler, options));
|
|
1745
|
+
}
|
|
1746
|
+
function render({ announce: announce2 = false } = {}) {
|
|
1747
|
+
const visible = new Set(visibleArtifactIds(state));
|
|
1748
|
+
root.dataset.planrView = state.viewMode;
|
|
1749
|
+
root.dataset.planrReviewMode = state.reviewMode;
|
|
1750
|
+
root.dataset.planrState = state.status;
|
|
1751
|
+
root.dataset.planrRailOpen = String(state.railOpen);
|
|
1752
|
+
root.dataset.planrPresentation = state.presentation;
|
|
1753
|
+
document2.documentElement.dataset.planrPresentation = state.presentation;
|
|
1754
|
+
document2.documentElement.dataset.planrTheme = state.theme;
|
|
1755
|
+
const grid = document2.querySelector(".planr-frame-grid");
|
|
1756
|
+
const surface = document2.querySelector(".planr-stage-surface");
|
|
1757
|
+
const tablist = document2.querySelector(".planr-variants");
|
|
1758
|
+
const rail = document2.getElementById("planr-review-rail");
|
|
1759
|
+
const feedbackButton = document2.querySelector('[data-planr-action="feedback"]');
|
|
1760
|
+
const addCommentButton = document2.querySelector('[data-planr-action="add-comment"]');
|
|
1761
|
+
const themeButton = document2.querySelector('[data-planr-action="theme"]');
|
|
1762
|
+
const statusSlot = document2.querySelector('[data-planr-slot="status"]');
|
|
1763
|
+
const metadata = document2.querySelector(
|
|
1764
|
+
".planr-toolbar:not([data-studio-react-chrome]) .planr-title-block > span"
|
|
1765
|
+
);
|
|
1766
|
+
const breadcrumb = document2.querySelector(".planr-stage-heading > span:first-child");
|
|
1767
|
+
const activeArtifact = stageArtifactById(state, state.activeArtifactId);
|
|
1768
|
+
if (grid) grid.dataset.planrLayout = state.viewMode;
|
|
1769
|
+
const visualOrder = state.viewMode === "split" ? [...visible, ...state.artifacts.map(({ id }) => id).filter((id) => !visible.has(id))] : state.artifacts.map(({ id }) => id);
|
|
1770
|
+
if (surface) surface.style.setProperty("--planr-shell-zoom", String(state.zoom / 100));
|
|
1771
|
+
if (tablist) tablist.hidden = state.viewMode === "single" || state.artifacts.length < 2;
|
|
1772
|
+
if (rail) {
|
|
1773
|
+
rail.toggleAttribute("inert", !state.railOpen);
|
|
1774
|
+
rail.setAttribute("aria-hidden", String(!state.railOpen));
|
|
1775
|
+
if (state.railOpen) rail.removeAttribute("aria-hidden");
|
|
1776
|
+
}
|
|
1777
|
+
if (feedbackButton) feedbackButton.setAttribute("aria-expanded", String(state.railOpen));
|
|
1778
|
+
if (addCommentButton) {
|
|
1779
|
+
const paused = root.hasAttribute("data-planr-room-comments-paused");
|
|
1780
|
+
addCommentButton.disabled = paused;
|
|
1781
|
+
addCommentButton.setAttribute("aria-pressed", String(state.reviewMode === "comment"));
|
|
1782
|
+
addCommentButton.setAttribute(
|
|
1783
|
+
"aria-label",
|
|
1784
|
+
paused ? "Add comment unavailable: comments are paused" : "Add comment"
|
|
1785
|
+
);
|
|
1786
|
+
addCommentButton.dataset.planrTooltip = paused ? "Comments are paused" : "Add comment (C)";
|
|
1787
|
+
}
|
|
1788
|
+
if (themeButton) {
|
|
1789
|
+
themeButton.textContent = state.theme;
|
|
1790
|
+
themeButton.setAttribute("aria-label", `Shell theme ${state.theme}`);
|
|
1791
|
+
}
|
|
1792
|
+
if (statusSlot)
|
|
1793
|
+
statusSlot.textContent = state.reviewMode === "comment" ? "Comment mode" : "Interactions enabled";
|
|
1794
|
+
if (metadata && activeArtifact) {
|
|
1795
|
+
metadata.textContent = `HTML · ${activeArtifact.viewport.width}×${activeArtifact.viewport.height}`;
|
|
1796
|
+
}
|
|
1797
|
+
if (breadcrumb)
|
|
1798
|
+
breadcrumb.textContent = `ARTIFACT / ${(activeArtifact?.title ?? "Artifact").toUpperCase()}`;
|
|
1799
|
+
for (const button of document2.querySelectorAll("[data-planr-view]")) {
|
|
1800
|
+
const mode = button.dataset.planrView;
|
|
1801
|
+
button.setAttribute("aria-pressed", String(mode === state.viewMode));
|
|
1802
|
+
button.disabled = state.artifacts.length < 2 && mode !== "single";
|
|
1803
|
+
}
|
|
1804
|
+
for (const button of document2.querySelectorAll("[data-planr-mode]")) {
|
|
1805
|
+
button.setAttribute("aria-pressed", String(button.dataset.planrMode === state.reviewMode));
|
|
1806
|
+
}
|
|
1807
|
+
for (const button of document2.querySelectorAll('[data-planr-action="zoom-reset"]')) {
|
|
1808
|
+
button.textContent = `${state.zoom}%`;
|
|
1809
|
+
}
|
|
1810
|
+
for (const tab of document2.querySelectorAll('[role="tab"][data-artifact-id]')) {
|
|
1811
|
+
const selected = tab.dataset.artifactId === state.activeArtifactId;
|
|
1812
|
+
tab.setAttribute("aria-selected", String(selected));
|
|
1813
|
+
tab.tabIndex = selected ? 0 : -1;
|
|
1814
|
+
}
|
|
1815
|
+
for (const [id, panel] of panels) {
|
|
1816
|
+
const isVisible = visible.has(id);
|
|
1817
|
+
const isPrimary = id === state.activeArtifactId;
|
|
1818
|
+
panel.hidden = !isVisible;
|
|
1819
|
+
panel.style.order = String(visualOrder.indexOf(id));
|
|
1820
|
+
const artifact = stageArtifactById(state, id);
|
|
1821
|
+
panel.setAttribute(
|
|
1822
|
+
"aria-label",
|
|
1823
|
+
`${isPrimary ? "Primary" : "Comparison"} artifact: ${artifact?.title ?? id}`
|
|
1824
|
+
);
|
|
1825
|
+
const frame = frames.get(id);
|
|
1826
|
+
const frameReady = frame?.dataset.planrFrameState === "ready";
|
|
1827
|
+
const annotationLayer = panel.querySelector("[data-planr-annotation-layer]");
|
|
1828
|
+
if (frame) {
|
|
1829
|
+
frame.tabIndex = isVisible && frameReady && state.status === "ready" && state.reviewMode === "interact" ? 0 : -1;
|
|
1830
|
+
if (state.presentation === "document") {
|
|
1831
|
+
frame.setAttribute("scrolling", "no");
|
|
1832
|
+
frame.style.overflow = "hidden";
|
|
1833
|
+
} else {
|
|
1834
|
+
frame.removeAttribute("scrolling");
|
|
1835
|
+
frame.style.removeProperty("overflow");
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
if (annotationLayer) {
|
|
1839
|
+
const enabled = isVisible && frameReady && state.status === "ready" && state.reviewMode === "comment";
|
|
1840
|
+
const hasComposer = Boolean(
|
|
1841
|
+
annotationLayer.querySelector("[data-planr-annotation-composer]")
|
|
1842
|
+
);
|
|
1843
|
+
annotationLayer.tabIndex = enabled ? 0 : -1;
|
|
1844
|
+
annotationLayer.setAttribute("aria-disabled", String(!enabled && !hasComposer));
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
updateStatus(document2, state);
|
|
1848
|
+
renderFrameLoadingStatus();
|
|
1849
|
+
if (typeof onState === "function") {
|
|
1850
|
+
try {
|
|
1851
|
+
onState(state);
|
|
1852
|
+
} catch {
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
if (announce2) emit(root, window, ARTIFACT_STAGE_EVENTS.change, state);
|
|
1856
|
+
}
|
|
1857
|
+
function renderFrameLoadingStatus() {
|
|
1858
|
+
const activeDiagnostic = frameDiagnostics.get(state.activeArtifactId);
|
|
1859
|
+
const statusPanel = document2.querySelector(".planr-stage-status");
|
|
1860
|
+
if (!statusPanel || !activeDiagnostic) return;
|
|
1861
|
+
const detail = statusPanel.querySelector("p");
|
|
1862
|
+
if (detail && state.status === "loading") {
|
|
1863
|
+
const copy = {
|
|
1864
|
+
source: "Preparing the selected screen.",
|
|
1865
|
+
document: "Loading the selected screen.",
|
|
1866
|
+
bridge: "Connecting the selected screen to the review."
|
|
1867
|
+
};
|
|
1868
|
+
detail.textContent = copy[activeDiagnostic.phase] ?? "Loading the selected screen.";
|
|
1869
|
+
}
|
|
1870
|
+
if (detail && state.status === "invalid" && activeDiagnostic.status === "error") {
|
|
1871
|
+
detail.textContent = `The selected screen did not finish its ${activeDiagnostic.failedPhase ?? "preview"} step. Retry to load it again; saved feedback remains available.`;
|
|
1872
|
+
}
|
|
1873
|
+
let retry = statusPanel.querySelector('[data-planr-action="retry-frame"]');
|
|
1874
|
+
if (!retry) {
|
|
1875
|
+
retry = document2.createElement("button");
|
|
1876
|
+
retry.className = "planr-toolbar-action";
|
|
1877
|
+
retry.dataset.planrAction = "retry-frame";
|
|
1878
|
+
retry.textContent = "Retry screen";
|
|
1879
|
+
statusPanel.querySelector("div")?.append(retry);
|
|
1880
|
+
}
|
|
1881
|
+
retry.hidden = state.status !== "invalid" || activeDiagnostic.status !== "error";
|
|
1882
|
+
}
|
|
1883
|
+
function dispatch(action, { announce: announce2 = true } = {}) {
|
|
1884
|
+
const previous = state;
|
|
1885
|
+
state = reduceArtifactStageState(state, action);
|
|
1886
|
+
if (state !== previous) {
|
|
1887
|
+
render({ announce: announce2 });
|
|
1888
|
+
if (["set-active", "set-comparison", "set-view-mode"].includes(action.type) && typeof resolveArtifactSource === "function") {
|
|
1889
|
+
readyPromise = activateVisibleFrames();
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
return state;
|
|
1893
|
+
}
|
|
1894
|
+
function setActiveFromTab(tab, { focus = false } = {}) {
|
|
1895
|
+
dispatch({ type: "set-active", artifactId: tab.dataset.artifactId });
|
|
1896
|
+
if (focus) tab.focus();
|
|
1897
|
+
}
|
|
1898
|
+
function onClick(event) {
|
|
1899
|
+
const target = event.target.closest?.("button");
|
|
1900
|
+
if (!target) return;
|
|
1901
|
+
if (target.hasAttribute("data-planr-close-feedback")) {
|
|
1902
|
+
dispatch({ type: "set-rail-open", railOpen: false });
|
|
1903
|
+
document2.querySelector('[data-planr-action="feedback"]')?.focus();
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
if (target.dataset.planrView) {
|
|
1907
|
+
dispatch({ type: "set-view-mode", viewMode: target.dataset.planrView });
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
if (target.dataset.artifactId && target.getAttribute("role") === "tab") {
|
|
1911
|
+
setActiveFromTab(target);
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
if (target.dataset.planrMode) {
|
|
1915
|
+
dispatch({ type: "set-review-mode", reviewMode: target.dataset.planrMode });
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
switch (target.dataset.planrAction) {
|
|
1919
|
+
case "add-comment":
|
|
1920
|
+
if (!root.hasAttribute("data-planr-room-comments-paused")) {
|
|
1921
|
+
dispatch({ type: "set-review-mode", reviewMode: "comment" });
|
|
1922
|
+
}
|
|
1923
|
+
break;
|
|
1924
|
+
case "zoom-out":
|
|
1925
|
+
dispatch({ type: "zoom-by", delta: -ARTIFACT_STAGE_LIMITS.zoomStep });
|
|
1926
|
+
break;
|
|
1927
|
+
case "zoom-reset":
|
|
1928
|
+
dispatch({ type: "set-zoom", zoom: ARTIFACT_STAGE_LIMITS.defaultZoom });
|
|
1929
|
+
break;
|
|
1930
|
+
case "zoom-in":
|
|
1931
|
+
dispatch({ type: "zoom-by", delta: ARTIFACT_STAGE_LIMITS.zoomStep });
|
|
1932
|
+
break;
|
|
1933
|
+
case "feedback": {
|
|
1934
|
+
const rail = document2.getElementById("planr-review-rail");
|
|
1935
|
+
if (state.railOpen && rail?.contains(document2.activeElement)) target.focus();
|
|
1936
|
+
dispatch({ type: "toggle-rail" });
|
|
1937
|
+
break;
|
|
1938
|
+
}
|
|
1939
|
+
case "more": {
|
|
1940
|
+
const menu = target.closest(".planr-more")?.querySelector(".planr-more-menu");
|
|
1941
|
+
if (!menu) break;
|
|
1942
|
+
const open = menu.hidden;
|
|
1943
|
+
menu.hidden = !open;
|
|
1944
|
+
target.setAttribute("aria-expanded", String(open));
|
|
1945
|
+
if (open) menu.querySelector('[role="menuitem"]')?.focus();
|
|
1946
|
+
break;
|
|
1947
|
+
}
|
|
1948
|
+
case "sharing-options":
|
|
1949
|
+
target.closest(".planr-more-menu").hidden = true;
|
|
1950
|
+
document2.querySelector('[data-planr-action="more"]')?.setAttribute("aria-expanded", "false");
|
|
1951
|
+
shareController?.open?.();
|
|
1952
|
+
break;
|
|
1953
|
+
case "retry-frame":
|
|
1954
|
+
void retryFrames([state.activeArtifactId]);
|
|
1955
|
+
break;
|
|
1956
|
+
case "theme":
|
|
1957
|
+
dispatch({ type: "cycle-theme" });
|
|
1958
|
+
break;
|
|
1959
|
+
default:
|
|
1960
|
+
break;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
function onKeyDown(event) {
|
|
1964
|
+
if (event.defaultPrevented) return;
|
|
1965
|
+
if (!event.altKey && !event.ctrlKey && !event.metaKey && !isEditableTarget(event.target)) {
|
|
1966
|
+
if (event.key.toLowerCase() === "i") {
|
|
1967
|
+
dispatch({ type: "set-review-mode", reviewMode: "interact" });
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
if (event.key.toLowerCase() === "c") {
|
|
1971
|
+
if (!root.hasAttribute("data-planr-room-comments-paused")) {
|
|
1972
|
+
dispatch({ type: "set-review-mode", reviewMode: "comment" });
|
|
1973
|
+
document2.querySelector('[data-planr-action="add-comment"]')?.focus();
|
|
1974
|
+
}
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
if (event.key === "Escape") {
|
|
1978
|
+
const more = document2.querySelector(".planr-more-menu:not([hidden])");
|
|
1979
|
+
if (more) {
|
|
1980
|
+
more.hidden = true;
|
|
1981
|
+
const trigger = document2.querySelector('[data-planr-action="more"]');
|
|
1982
|
+
trigger?.setAttribute("aria-expanded", "false");
|
|
1983
|
+
trigger?.focus();
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
if (state.presentation === "document" && state.reviewMode === "comment") {
|
|
1987
|
+
dispatch({ type: "set-review-mode", reviewMode: "interact" });
|
|
1988
|
+
document2.querySelector('[data-planr-action="add-comment"]')?.focus();
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1991
|
+
if (state.railOpen) {
|
|
1992
|
+
dispatch({ type: "set-rail-open", railOpen: false });
|
|
1993
|
+
document2.querySelector('[data-planr-action="feedback"]')?.focus();
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
const tab = event.target.closest?.('[role="tab"][data-artifact-id]');
|
|
1999
|
+
if (!tab || !["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
|
|
2000
|
+
const tabs = [...document2.querySelectorAll('[role="tab"][data-artifact-id]')];
|
|
2001
|
+
const index = tabs.indexOf(tab);
|
|
2002
|
+
if (index < 0) return;
|
|
2003
|
+
event.preventDefault();
|
|
2004
|
+
const nextIndex = event.key === "Home" ? 0 : event.key === "End" ? tabs.length - 1 : (index + (event.key === "ArrowRight" ? 1 : -1) + tabs.length) % tabs.length;
|
|
2005
|
+
setActiveFromTab(tabs[nextIndex], { focus: true });
|
|
2006
|
+
}
|
|
2007
|
+
function emitSelection(layer, start, end = start) {
|
|
2008
|
+
if (state.reviewMode !== "comment" || state.status !== "ready") return;
|
|
2009
|
+
const artifactId = layer.dataset.planrAnnotationLayer;
|
|
2010
|
+
if (!visibleArtifactIds(state).includes(artifactId)) return;
|
|
2011
|
+
const artifact = stageArtifactById(state, artifactId);
|
|
2012
|
+
if (!artifact) return;
|
|
2013
|
+
const region = clientSelectionToNormalized(layer.getBoundingClientRect(), start, end);
|
|
2014
|
+
const measured = state.presentation === "document" ? documentLayouts.get(artifactId) : null;
|
|
2015
|
+
const detail = Object.freeze({
|
|
2016
|
+
schemaVersion: "1.0.0",
|
|
2017
|
+
artifactId,
|
|
2018
|
+
region,
|
|
2019
|
+
viewport: measured ?? artifact.viewport
|
|
2020
|
+
});
|
|
2021
|
+
emit(root, window, ARTIFACT_STAGE_EVENTS.region, detail);
|
|
2022
|
+
emit(root, window, ARTIFACT_STAGE_EVENTS.point, detail);
|
|
2023
|
+
if (state.presentation === "document") {
|
|
2024
|
+
dispatch({ type: "set-review-mode", reviewMode: "interact" });
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
for (const layer of document2.querySelectorAll("[data-planr-annotation-layer]")) {
|
|
2028
|
+
let selection = null;
|
|
2029
|
+
let selectionPreview = null;
|
|
2030
|
+
listen(layer, "pointerdown", (event) => {
|
|
2031
|
+
if (event.button !== 0 || state.reviewMode !== "comment" || state.status !== "ready") return;
|
|
2032
|
+
if (event.target !== layer) return;
|
|
2033
|
+
event.preventDefault();
|
|
2034
|
+
selection = { pointerId: event.pointerId, start: { x: event.clientX, y: event.clientY } };
|
|
2035
|
+
layer.setPointerCapture?.(event.pointerId);
|
|
2036
|
+
selectionPreview = document2.createElement("span");
|
|
2037
|
+
selectionPreview.className = "planr-region-selection";
|
|
2038
|
+
selectionPreview.setAttribute("aria-hidden", "true");
|
|
2039
|
+
layer.append(selectionPreview);
|
|
2040
|
+
});
|
|
2041
|
+
listen(layer, "pointermove", (event) => {
|
|
2042
|
+
if (!selection || selection.pointerId !== event.pointerId || !selectionPreview) return;
|
|
2043
|
+
const region = clientSelectionToNormalized(layer.getBoundingClientRect(), selection.start, {
|
|
2044
|
+
x: event.clientX,
|
|
2045
|
+
y: event.clientY
|
|
2046
|
+
});
|
|
2047
|
+
selectionPreview.style.left = `${region.x * 100}%`;
|
|
2048
|
+
selectionPreview.style.top = `${region.y * 100}%`;
|
|
2049
|
+
selectionPreview.style.width = `${region.w * 100}%`;
|
|
2050
|
+
selectionPreview.style.height = `${region.h * 100}%`;
|
|
2051
|
+
});
|
|
2052
|
+
listen(layer, "pointercancel", (event) => {
|
|
2053
|
+
if (!selection || selection.pointerId !== event.pointerId) return;
|
|
2054
|
+
layer.releasePointerCapture?.(event.pointerId);
|
|
2055
|
+
selection = null;
|
|
2056
|
+
selectionPreview?.remove();
|
|
2057
|
+
selectionPreview = null;
|
|
2058
|
+
});
|
|
2059
|
+
listen(layer, "pointerup", (event) => {
|
|
2060
|
+
if (!selection || selection.pointerId !== event.pointerId) return;
|
|
2061
|
+
const start = selection.start;
|
|
2062
|
+
layer.releasePointerCapture?.(event.pointerId);
|
|
2063
|
+
selection = null;
|
|
2064
|
+
selectionPreview?.remove();
|
|
2065
|
+
selectionPreview = null;
|
|
2066
|
+
emitSelection(layer, start, { x: event.clientX, y: event.clientY });
|
|
2067
|
+
});
|
|
2068
|
+
listen(layer, "keydown", (event) => {
|
|
2069
|
+
if (!["Enter", " "].includes(event.key)) return;
|
|
2070
|
+
if (event.target !== layer) return;
|
|
2071
|
+
event.preventDefault();
|
|
2072
|
+
const bounds = layer.getBoundingClientRect();
|
|
2073
|
+
emitSelection(layer, {
|
|
2074
|
+
x: bounds.left + bounds.width / 2,
|
|
2075
|
+
y: bounds.top + bounds.height / 2
|
|
2076
|
+
});
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2079
|
+
for (const [artifactId, frame] of frames) {
|
|
2080
|
+
listen(frame, ARTIFACT_STAGE_EVENTS.layout, (event) => {
|
|
2081
|
+
if (state.presentation !== "document") return;
|
|
2082
|
+
const width = event.detail?.width;
|
|
2083
|
+
const height = event.detail?.height;
|
|
2084
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || width > ARTIFACT_STAGE_LIMITS.maxDocumentWidth || height < 1 || height > ARTIFACT_STAGE_LIMITS.maxDocumentHeight)
|
|
2085
|
+
return;
|
|
2086
|
+
const layout = Object.freeze({ width, height });
|
|
2087
|
+
documentLayouts.set(artifactId, layout);
|
|
2088
|
+
const panel = panels.get(artifactId);
|
|
2089
|
+
if (!panel) return;
|
|
2090
|
+
panel.dataset.planrLayoutMeasured = "true";
|
|
2091
|
+
panel.style.setProperty("--planr-document-width", `${width}px`);
|
|
2092
|
+
panel.style.setProperty("--planr-document-height", `${height}px`);
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
listen(root, "click", onClick);
|
|
2096
|
+
listen(document2, "keydown", onKeyDown);
|
|
2097
|
+
listen(document2, "click", (event) => {
|
|
2098
|
+
const more = document2.querySelector(".planr-more-menu:not([hidden])");
|
|
2099
|
+
if (!more || event.target.closest?.(".planr-more")) return;
|
|
2100
|
+
more.hidden = true;
|
|
2101
|
+
document2.querySelector('[data-planr-action="more"]')?.setAttribute("aria-expanded", "false");
|
|
2102
|
+
});
|
|
2103
|
+
const roomStateObserver = typeof window.MutationObserver === "function" ? new window.MutationObserver(() => {
|
|
2104
|
+
if (root.hasAttribute("data-planr-room-comments-paused") && state.reviewMode === "comment") {
|
|
2105
|
+
dispatch({ type: "set-review-mode", reviewMode: "interact" });
|
|
2106
|
+
} else {
|
|
2107
|
+
render();
|
|
2108
|
+
}
|
|
2109
|
+
}) : null;
|
|
2110
|
+
roomStateObserver?.observe(root, {
|
|
2111
|
+
attributes: true,
|
|
2112
|
+
attributeFilter: ["data-planr-room-comments-paused"]
|
|
2113
|
+
});
|
|
2114
|
+
if (roomStateObserver) cleanup.push(() => roomStateObserver.disconnect());
|
|
2115
|
+
render();
|
|
2116
|
+
let readyPromise = Promise.resolve(state);
|
|
2117
|
+
let feedbackController = null;
|
|
2118
|
+
let annotationController = null;
|
|
2119
|
+
let shareController = null;
|
|
2120
|
+
let hostedController = null;
|
|
2121
|
+
function disposeStage() {
|
|
2122
|
+
if (disposed) return true;
|
|
2123
|
+
disposed = true;
|
|
2124
|
+
wakeFrameDemand();
|
|
2125
|
+
for (const id of [...frameLoads.keys()]) releaseFrame(id);
|
|
2126
|
+
for (const remove of cleanup.splice(0)) remove();
|
|
2127
|
+
return true;
|
|
2128
|
+
}
|
|
2129
|
+
const controller = Object.freeze({
|
|
2130
|
+
frameBudget,
|
|
2131
|
+
holdFrames,
|
|
2132
|
+
ensureFrames,
|
|
2133
|
+
retryFrames,
|
|
2134
|
+
getFrameDiagnostics: () => [...frameDiagnostics.values()],
|
|
2135
|
+
getLoadedArtifactIds: () => [...frameLoads].filter(([, record]) => record.status === "ready").map(([id]) => id),
|
|
2136
|
+
getState: () => state,
|
|
2137
|
+
getFrame: (artifactId) => frames.get(artifactId) ?? null,
|
|
2138
|
+
getPanel: (artifactId) => panels.get(artifactId) ?? null,
|
|
2139
|
+
get review() {
|
|
2140
|
+
return feedbackController;
|
|
2141
|
+
},
|
|
2142
|
+
get annotations() {
|
|
2143
|
+
return annotationController;
|
|
2144
|
+
},
|
|
2145
|
+
get share() {
|
|
2146
|
+
return shareController;
|
|
2147
|
+
},
|
|
2148
|
+
get hosted() {
|
|
2149
|
+
return hostedController;
|
|
2150
|
+
},
|
|
2151
|
+
get ready() {
|
|
2152
|
+
return readyPromise;
|
|
2153
|
+
},
|
|
2154
|
+
dispatch,
|
|
2155
|
+
destroy() {
|
|
2156
|
+
if (["creating", "ambiguous"].includes(shareController?.getState?.().phase)) {
|
|
2157
|
+
shareController.destroy?.();
|
|
2158
|
+
return false;
|
|
2159
|
+
}
|
|
2160
|
+
return disposeStage();
|
|
2161
|
+
}
|
|
2162
|
+
});
|
|
2163
|
+
publishArtifactStage(window, controller);
|
|
2164
|
+
listen(window, "pagehide", (event) => {
|
|
2165
|
+
if (!event.persisted) disposeStage();
|
|
2166
|
+
});
|
|
2167
|
+
feedbackController = mountArtifactFeedbackRail({
|
|
2168
|
+
document: document2,
|
|
2169
|
+
window,
|
|
2170
|
+
root,
|
|
2171
|
+
stageController: controller,
|
|
2172
|
+
reviewOf: reviewConfig?.reviewOf,
|
|
2173
|
+
initialReview: reviewConfig?.review ?? null,
|
|
2174
|
+
artifacts: state.artifacts,
|
|
2175
|
+
...reviewOptions
|
|
2176
|
+
});
|
|
2177
|
+
annotationController = mountArtifactAnnotations({
|
|
2178
|
+
document: document2,
|
|
2179
|
+
window,
|
|
2180
|
+
root,
|
|
2181
|
+
stageController: controller,
|
|
2182
|
+
reviewController: feedbackController
|
|
2183
|
+
});
|
|
2184
|
+
const durablePasteShare = shareOptions.createShare && shareOptions.resumePreparedShare ? createDurablePasteShare({
|
|
2185
|
+
scope: () => ({
|
|
2186
|
+
workspaceId: window.location.pathname,
|
|
2187
|
+
revisionId: reviewConfig.reviewOf ?? "unknown-revision",
|
|
2188
|
+
actorId: "local-paste-owner"
|
|
2189
|
+
}),
|
|
2190
|
+
create: (request) => shareOptions.createShare(request),
|
|
2191
|
+
commit: shareOptions.resumePreparedShare,
|
|
2192
|
+
indexedDB: window.indexedDB
|
|
2193
|
+
}) : null;
|
|
2194
|
+
if (durablePasteShare) cleanup.push(() => durablePasteShare.close());
|
|
2195
|
+
shareController = mountArtifactShareDialog({
|
|
2196
|
+
document: document2,
|
|
2197
|
+
window,
|
|
2198
|
+
root,
|
|
2199
|
+
stageController: controller,
|
|
2200
|
+
...shareOptions,
|
|
2201
|
+
...durablePasteShare ? {
|
|
2202
|
+
createShare: (request) => durablePasteShare.run(request)
|
|
2203
|
+
} : {}
|
|
2204
|
+
});
|
|
2205
|
+
hostedController = mountHostedArtifactViewer({
|
|
2206
|
+
document: document2,
|
|
2207
|
+
window,
|
|
2208
|
+
...hostedOptions
|
|
2209
|
+
});
|
|
2210
|
+
if (feedbackController?.destroy) cleanup.push(() => feedbackController.destroy());
|
|
2211
|
+
if (annotationController?.destroy) cleanup.push(() => annotationController.destroy());
|
|
2212
|
+
if (shareController?.destroy) cleanup.push(() => shareController.destroy());
|
|
2213
|
+
if (hostedController?.destroy) cleanup.push(() => hostedController.destroy());
|
|
2214
|
+
function cancelledFrame() {
|
|
2215
|
+
const error = new Error("Artifact frame loading was cancelled.");
|
|
2216
|
+
error.name = "AbortError";
|
|
2217
|
+
return error;
|
|
2218
|
+
}
|
|
2219
|
+
function frameStatus(artifactId, status, phase, failure) {
|
|
2220
|
+
const frame = frames.get(artifactId);
|
|
2221
|
+
if (frame) {
|
|
2222
|
+
frame.dataset.planrFrameState = status;
|
|
2223
|
+
frame.dataset.planrFramePhase = phase;
|
|
2224
|
+
}
|
|
2225
|
+
const previous = frameDiagnostics.get(artifactId);
|
|
2226
|
+
const now = window.performance.now();
|
|
2227
|
+
const starting = status === "loading" && phase === "source";
|
|
2228
|
+
const startedAt = starting ? now : previous?.startedAt ?? now;
|
|
2229
|
+
const diagnostic = Object.freeze({
|
|
2230
|
+
artifactId,
|
|
2231
|
+
status,
|
|
2232
|
+
phase,
|
|
2233
|
+
startedAt,
|
|
2234
|
+
elapsedMs: Math.max(0, Math.round(now - startedAt)),
|
|
2235
|
+
attempt: (previous?.attempt ?? 0) + (starting ? 1 : 0),
|
|
2236
|
+
transport: sourceTransport,
|
|
2237
|
+
...failure
|
|
2238
|
+
});
|
|
2239
|
+
frameDiagnostics.set(artifactId, diagnostic);
|
|
2240
|
+
if (!disposed) {
|
|
2241
|
+
emit(root, window, ARTIFACT_STAGE_EVENTS.frameState, diagnostic);
|
|
2242
|
+
if (artifactId === state.activeArtifactId) render();
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
function releaseFrame(artifactId, {
|
|
2246
|
+
status = "unloaded",
|
|
2247
|
+
error = cancelledFrame(),
|
|
2248
|
+
failure
|
|
2249
|
+
} = {}) {
|
|
2250
|
+
const record = frameLoads.get(artifactId);
|
|
2251
|
+
if (!record) return;
|
|
2252
|
+
frameLoads.delete(artifactId);
|
|
2253
|
+
wakeFrameDemand();
|
|
2254
|
+
record.cancelled = true;
|
|
2255
|
+
record.abort.abort();
|
|
2256
|
+
record.unlisten?.();
|
|
2257
|
+
record.detach?.();
|
|
2258
|
+
record.reject(error);
|
|
2259
|
+
const frame = frames.get(artifactId);
|
|
2260
|
+
const slot = frameSlots.get(artifactId);
|
|
2261
|
+
if (slot && frame.parentNode === slot.host && !slot.placeholder.parentNode) {
|
|
2262
|
+
slot.host.replaceChild(slot.placeholder, frame);
|
|
2263
|
+
}
|
|
2264
|
+
frame.removeAttribute("srcdoc");
|
|
2265
|
+
frame.removeAttribute("src");
|
|
2266
|
+
delete frame.dataset.planrArtifactDigest;
|
|
2267
|
+
delete frame.dataset.planrBridgeTrusted;
|
|
2268
|
+
try {
|
|
2269
|
+
delete frame.__openPlanrBridge;
|
|
2270
|
+
} catch {
|
|
2271
|
+
}
|
|
2272
|
+
if (record.sourceUrl) window.URL.revokeObjectURL(record.sourceUrl);
|
|
2273
|
+
documentLayouts.delete(artifactId);
|
|
2274
|
+
const panel = panels.get(artifactId);
|
|
2275
|
+
if (panel) {
|
|
2276
|
+
delete panel.dataset.planrLayoutMeasured;
|
|
2277
|
+
panel.style.removeProperty("--planr-document-width");
|
|
2278
|
+
panel.style.removeProperty("--planr-document-height");
|
|
2279
|
+
}
|
|
2280
|
+
frameStatus(artifactId, status, status, failure);
|
|
2281
|
+
}
|
|
2282
|
+
function assignArtifactSource(artifact) {
|
|
2283
|
+
if (disposed) return Promise.reject(cancelledFrame());
|
|
2284
|
+
const frame = frames.get(artifact.id);
|
|
2285
|
+
if (!frame) return Promise.reject(new Error(`Missing artifact frame: ${artifact.id}`));
|
|
2286
|
+
const slot = frameSlots.get(artifact.id);
|
|
2287
|
+
const existing = frameLoads.get(artifact.id);
|
|
2288
|
+
if (slot && (!slot.host.isConnected || !root.contains(slot.host) || existing?.status === "ready" && frame.parentNode !== slot.host)) {
|
|
2289
|
+
releaseFrame(artifact.id);
|
|
2290
|
+
return Promise.reject(cancelledFrame());
|
|
2291
|
+
}
|
|
2292
|
+
if (existing) {
|
|
2293
|
+
if (existing.status === "ready" && existing.requireTrust && frames.get(artifact.id)?.dataset.planrBridgeTrusted !== "true") {
|
|
2294
|
+
releaseFrame(artifact.id);
|
|
2295
|
+
} else {
|
|
2296
|
+
existing.used = ++frameUse;
|
|
2297
|
+
return existing.promise;
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
const record = {
|
|
2301
|
+
status: "loading",
|
|
2302
|
+
used: ++frameUse,
|
|
2303
|
+
cancelled: false,
|
|
2304
|
+
abort: typeof window.AbortController === "function" ? new window.AbortController() : { signal: void 0, abort() {
|
|
2305
|
+
} }
|
|
2306
|
+
};
|
|
2307
|
+
record.promise = new Promise((resolve, reject) => {
|
|
2308
|
+
record.resolve = resolve;
|
|
2309
|
+
record.reject = reject;
|
|
2310
|
+
});
|
|
2311
|
+
frameLoads.set(artifact.id, record);
|
|
2312
|
+
const current = () => !disposed && !record.cancelled && frameLoads.get(artifact.id) === record;
|
|
2313
|
+
const fail = (error) => {
|
|
2314
|
+
if (current())
|
|
2315
|
+
releaseFrame(artifact.id, {
|
|
2316
|
+
status: "error",
|
|
2317
|
+
error,
|
|
2318
|
+
failure: {
|
|
2319
|
+
failedPhase: frameDiagnostics.get(artifact.id)?.phase ?? "source",
|
|
2320
|
+
code: error?.code ?? "E_ARTIFACT_FRAME_LOAD"
|
|
2321
|
+
}
|
|
2322
|
+
});
|
|
2323
|
+
};
|
|
2324
|
+
const timer = window.setTimeout(() => {
|
|
2325
|
+
const error = new Error(`Artifact frame did not finish loading: ${artifact.id}`);
|
|
2326
|
+
error.code = "E_ARTIFACT_FRAME_TIMEOUT";
|
|
2327
|
+
fail(error);
|
|
2328
|
+
}, frameLoadTimeoutMs);
|
|
2329
|
+
let loaded = false;
|
|
2330
|
+
const ready = () => {
|
|
2331
|
+
if (current() && slot && (!slot.host.isConnected || !root.contains(slot.host) || frame.parentNode !== slot.host)) {
|
|
2332
|
+
fail(cancelledFrame());
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
if (!current() || !loaded || record.requireTrust && frame.dataset.planrBridgeTrusted !== "true")
|
|
2336
|
+
return;
|
|
2337
|
+
record.status = "ready";
|
|
2338
|
+
record.unlisten();
|
|
2339
|
+
frameStatus(artifact.id, "ready", "ready");
|
|
2340
|
+
record.resolve(artifact.id);
|
|
2341
|
+
};
|
|
2342
|
+
const onLoad = () => {
|
|
2343
|
+
loaded = true;
|
|
2344
|
+
if (record.requireTrust && frame.dataset.planrBridgeTrusted !== "true") {
|
|
2345
|
+
frameStatus(artifact.id, "loading", "bridge");
|
|
2346
|
+
}
|
|
2347
|
+
ready();
|
|
2348
|
+
};
|
|
2349
|
+
const onError = () => fail(new Error(`Artifact frame failed: ${artifact.id}`));
|
|
2350
|
+
record.unlisten = () => {
|
|
2351
|
+
window.clearTimeout(timer);
|
|
2352
|
+
frame.removeEventListener("load", onLoad);
|
|
2353
|
+
frame.removeEventListener("error", onError);
|
|
2354
|
+
frame.removeEventListener("planr:artifact-bridge-ready", ready);
|
|
2355
|
+
};
|
|
2356
|
+
frameStatus(artifact.id, "loading", "source");
|
|
2357
|
+
if (!current()) return record.promise;
|
|
2358
|
+
void (async () => {
|
|
2359
|
+
const source = await resolveArtifactSource(artifact, {
|
|
2360
|
+
frame,
|
|
2361
|
+
getState: () => state,
|
|
2362
|
+
signal: record.abort.signal
|
|
2363
|
+
});
|
|
2364
|
+
if (!current()) return;
|
|
2365
|
+
if (typeof window.TextDecoder !== "function") {
|
|
2366
|
+
const error = new Error("UTF-8 decoding support is required for artifact sources.");
|
|
2367
|
+
error.code = "E_ARTIFACT_BROWSER_UNSUPPORTED";
|
|
2368
|
+
throw error;
|
|
2369
|
+
}
|
|
2370
|
+
const html = await htmlForSource(window, source);
|
|
2371
|
+
if (!current()) return;
|
|
2372
|
+
if (!html) {
|
|
2373
|
+
throw new TypeError(
|
|
2374
|
+
`Artifact source resolver must return HTML bytes or a Blob for ${artifact.id}.`
|
|
2375
|
+
);
|
|
2376
|
+
}
|
|
2377
|
+
if (slot) {
|
|
2378
|
+
if (!slot.host.isConnected || !root.contains(slot.host) || slot.placeholder.parentNode !== slot.host || frame.parentNode) {
|
|
2379
|
+
throw cancelledFrame();
|
|
2380
|
+
}
|
|
2381
|
+
slot.host.replaceChild(frame, slot.placeholder);
|
|
2382
|
+
}
|
|
2383
|
+
if (typeof bridgeClient?.attach === "function") {
|
|
2384
|
+
const detach = bridgeClient.attach({
|
|
2385
|
+
artifact,
|
|
2386
|
+
frame,
|
|
2387
|
+
getState: () => state
|
|
2388
|
+
});
|
|
2389
|
+
if (typeof detach === "function") record.detach = detach;
|
|
2390
|
+
if (!current()) {
|
|
2391
|
+
record.detach?.();
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
record.requireTrust = true;
|
|
2395
|
+
}
|
|
2396
|
+
frame.addEventListener("load", onLoad);
|
|
2397
|
+
frame.addEventListener("error", onError);
|
|
2398
|
+
frame.addEventListener("planr:artifact-bridge-ready", ready);
|
|
2399
|
+
frame.dataset.planrArtifactDigest = artifact.sha256;
|
|
2400
|
+
frameStatus(artifact.id, "loading", "document");
|
|
2401
|
+
if (!current()) return;
|
|
2402
|
+
if (slot && (!slot.host.isConnected || !root.contains(slot.host) || frame.parentNode !== slot.host))
|
|
2403
|
+
throw cancelledFrame();
|
|
2404
|
+
if (sourceTransport === "srcdoc") {
|
|
2405
|
+
frame.removeAttribute("src");
|
|
2406
|
+
frame.srcdoc = html;
|
|
2407
|
+
} else {
|
|
2408
|
+
if (typeof window.URL?.createObjectURL !== "function" || typeof window.Blob !== "function") {
|
|
2409
|
+
const error = new Error("Blob URL support is required for artifact sources.");
|
|
2410
|
+
error.code = "E_ARTIFACT_BROWSER_UNSUPPORTED";
|
|
2411
|
+
throw error;
|
|
2412
|
+
}
|
|
2413
|
+
const sourceUrl = window.URL.createObjectURL(
|
|
2414
|
+
new window.Blob([html], {
|
|
2415
|
+
type: "text/html;charset=utf-8"
|
|
2416
|
+
})
|
|
2417
|
+
);
|
|
2418
|
+
record.sourceUrl = sourceUrl;
|
|
2419
|
+
frame.removeAttribute("srcdoc");
|
|
2420
|
+
frame.src = sourceUrl;
|
|
2421
|
+
}
|
|
2422
|
+
})().catch(fail);
|
|
2423
|
+
return record.promise;
|
|
2424
|
+
}
|
|
2425
|
+
function wakeFrameDemand() {
|
|
2426
|
+
for (const resolve of frameHoldWaiters) resolve();
|
|
2427
|
+
frameHoldWaiters.clear();
|
|
2428
|
+
}
|
|
2429
|
+
function holdFrames(artifactIds) {
|
|
2430
|
+
if (disposed) throw cancelledFrame();
|
|
2431
|
+
if (!Array.isArray(artifactIds) || artifactIds.some((id) => !frames.has(id))) {
|
|
2432
|
+
throw new TypeError("Requested artifact frames must be known artifact IDs.");
|
|
2433
|
+
}
|
|
2434
|
+
const ids = [...new Set(artifactIds)];
|
|
2435
|
+
for (const id of ids) frameHolds.set(id, (frameHolds.get(id) ?? 0) + 1);
|
|
2436
|
+
let released = false;
|
|
2437
|
+
return () => {
|
|
2438
|
+
if (released) return;
|
|
2439
|
+
released = true;
|
|
2440
|
+
for (const id of ids) {
|
|
2441
|
+
const count = frameHolds.get(id) ?? 0;
|
|
2442
|
+
if (count > 1) frameHolds.set(id, count - 1);
|
|
2443
|
+
else frameHolds.delete(id);
|
|
2444
|
+
}
|
|
2445
|
+
wakeFrameDemand();
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
function ensureFrames(artifactIds, isCurrent = () => true) {
|
|
2449
|
+
if (disposed || !isCurrent()) return Promise.reject(cancelledFrame());
|
|
2450
|
+
if (!Array.isArray(artifactIds) || artifactIds.some((id) => typeof id !== "string" || !frames.has(id))) {
|
|
2451
|
+
return Promise.reject(new TypeError("Requested artifact frames must be known artifact IDs."));
|
|
2452
|
+
}
|
|
2453
|
+
const requested = [...new Set(artifactIds)];
|
|
2454
|
+
if (frameBudget !== null && requested.length > frameBudget) {
|
|
2455
|
+
return Promise.reject(
|
|
2456
|
+
new RangeError(`At most ${frameBudget} artifact frames may be requested together.`)
|
|
2457
|
+
);
|
|
2458
|
+
}
|
|
2459
|
+
if (frameBudget === null || requested.every((id) => frameLoads.has(id)))
|
|
2460
|
+
return Promise.all(requested.map((id) => assignArtifactSource(stageArtifactById(state, id))));
|
|
2461
|
+
const run = async () => {
|
|
2462
|
+
if (disposed || !isCurrent()) throw cancelledFrame();
|
|
2463
|
+
for (const id of requested) {
|
|
2464
|
+
if (disposed || !isCurrent()) throw cancelledFrame();
|
|
2465
|
+
if (!frameLoads.has(id)) {
|
|
2466
|
+
while (frameLoads.size >= frameBudget) {
|
|
2467
|
+
const candidates = [...frameLoads].filter(([loadedId]) => !requested.includes(loadedId) && !frameHolds.has(loadedId)).sort((a, b) => a[1].used - b[1].used);
|
|
2468
|
+
if (!candidates.length) {
|
|
2469
|
+
if (!frameHolds.size) throw new Error("The artifact frame budget is exhausted.");
|
|
2470
|
+
await new Promise((resolve) => frameHoldWaiters.add(resolve));
|
|
2471
|
+
if (disposed || !isCurrent()) throw cancelledFrame();
|
|
2472
|
+
continue;
|
|
2473
|
+
}
|
|
2474
|
+
releaseFrame(candidates[0][0]);
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
await assignArtifactSource(stageArtifactById(state, id));
|
|
2478
|
+
}
|
|
2479
|
+
return requested;
|
|
2480
|
+
};
|
|
2481
|
+
const result = frameQueue.then(run);
|
|
2482
|
+
frameQueue = result.catch(() => {
|
|
2483
|
+
});
|
|
2484
|
+
return result;
|
|
2485
|
+
}
|
|
2486
|
+
function frameIsReady(id) {
|
|
2487
|
+
const frame = frames.get(id);
|
|
2488
|
+
return frame?.dataset.planrFrameState === "ready" && (!frame.__openPlanrBridge || frame.dataset.planrBridgeTrusted === "true");
|
|
2489
|
+
}
|
|
2490
|
+
function setSelectionStatus(status, selection, generation) {
|
|
2491
|
+
if (disposed || generation !== activationGeneration || visibleArtifactIds(state).join("|") !== selection)
|
|
2492
|
+
return;
|
|
2493
|
+
state = reduceArtifactStageState(state, { type: "set-status", status });
|
|
2494
|
+
render({ announce: true });
|
|
2495
|
+
}
|
|
2496
|
+
function cancelInactiveLoads(ids) {
|
|
2497
|
+
for (const [id, record] of frameLoads) {
|
|
2498
|
+
if (record.status === "loading" && !ids.includes(id) && !frameHolds.has(id)) releaseFrame(id);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
async function activateVisibleFrames() {
|
|
2502
|
+
const generation = ++activationGeneration;
|
|
2503
|
+
const isCurrent = () => generation === activationGeneration;
|
|
2504
|
+
const ids = visibleArtifactIds(state);
|
|
2505
|
+
const selection = ids.join("|");
|
|
2506
|
+
if (ids.length === 0) return state;
|
|
2507
|
+
cancelInactiveLoads(ids);
|
|
2508
|
+
if (ids.every(frameIsReady)) {
|
|
2509
|
+
setSelectionStatus("ready", selection, generation);
|
|
2510
|
+
return state;
|
|
2511
|
+
}
|
|
2512
|
+
setSelectionStatus("loading", selection, generation);
|
|
2513
|
+
try {
|
|
2514
|
+
await ensureFrames(ids, isCurrent);
|
|
2515
|
+
setSelectionStatus("ready", selection, generation);
|
|
2516
|
+
} catch (error) {
|
|
2517
|
+
const status = error?.code === "E_ARTIFACT_BROWSER_UNSUPPORTED" ? "unsupported-browser" : "invalid";
|
|
2518
|
+
setSelectionStatus(status, selection, generation);
|
|
2519
|
+
}
|
|
2520
|
+
return state;
|
|
2521
|
+
}
|
|
2522
|
+
function retryFrames(artifactIds = visibleArtifactIds(state)) {
|
|
2523
|
+
if (!Array.isArray(artifactIds) || artifactIds.some((id) => !frames.has(id))) {
|
|
2524
|
+
return Promise.reject(new TypeError("Requested artifact frames must be known artifact IDs."));
|
|
2525
|
+
}
|
|
2526
|
+
for (const id of artifactIds) releaseFrame(id);
|
|
2527
|
+
if (artifactIds.some((id) => visibleArtifactIds(state).includes(id))) {
|
|
2528
|
+
readyPromise = activateVisibleFrames();
|
|
2529
|
+
return readyPromise;
|
|
2530
|
+
}
|
|
2531
|
+
return ensureFrames(artifactIds);
|
|
2532
|
+
}
|
|
2533
|
+
if (state.status === "ready" && state.artifacts.length > 0) {
|
|
2534
|
+
if (typeof resolveArtifactSource !== "function") {
|
|
2535
|
+
frameStatus(state.activeArtifactId, "loading", "source");
|
|
2536
|
+
frameStatus(state.activeArtifactId, "error", "error", {
|
|
2537
|
+
failedPhase: "source",
|
|
2538
|
+
code: "E_ARTIFACT_SOURCE_UNAVAILABLE"
|
|
2539
|
+
});
|
|
2540
|
+
state = reduceArtifactStageState(state, { type: "set-status", status: "invalid" });
|
|
2541
|
+
render();
|
|
2542
|
+
readyPromise = Promise.resolve(state);
|
|
2543
|
+
} else {
|
|
2544
|
+
state = reduceArtifactStageState(state, { type: "set-status", status: "loading" });
|
|
2545
|
+
render();
|
|
2546
|
+
readyPromise = activateVisibleFrames();
|
|
2547
|
+
if (frameBudget === null) {
|
|
2548
|
+
void Promise.allSettled(
|
|
2549
|
+
state.artifacts.filter(({ id }) => id !== state.activeArtifactId).map(assignArtifactSource)
|
|
2550
|
+
);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
return controller;
|
|
2555
|
+
}
|
|
2556
|
+
function bootstrapArtifactStage(document2 = globalThis.document, options = {}) {
|
|
2557
|
+
return mountArtifactStage({ ...options, document: document2, window: document2?.defaultView });
|
|
2558
|
+
}
|
|
2559
|
+
if (typeof document !== "undefined") {
|
|
2560
|
+
const options = globalThis.__OPENPLANR_ARTIFACT_STAGE_OPTIONS__ ?? {};
|
|
2561
|
+
if (document.readyState === "loading") {
|
|
2562
|
+
document.addEventListener("DOMContentLoaded", () => bootstrapArtifactStage(document, options), {
|
|
2563
|
+
once: true
|
|
2564
|
+
});
|
|
2565
|
+
} else {
|
|
2566
|
+
queueMicrotask(() => bootstrapArtifactStage(document, options));
|
|
2567
|
+
}
|
|
2568
|
+
}
|