@bastani/atomic 0.7.17-1 → 0.8.0-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 +4183 -0
- package/README.md +655 -0
- package/dist/builtin/intercom/CHANGELOG.md +195 -0
- package/dist/builtin/intercom/LICENSE +21 -0
- package/dist/builtin/intercom/README.md +484 -0
- package/dist/builtin/intercom/broker/broker.ts +346 -0
- package/dist/builtin/intercom/broker/client.ts +535 -0
- package/dist/builtin/intercom/broker/framing.ts +57 -0
- package/dist/builtin/intercom/broker/paths.ts +21 -0
- package/dist/builtin/intercom/broker/spawn.ts +308 -0
- package/dist/builtin/intercom/config.ts +109 -0
- package/dist/builtin/intercom/index.ts +1780 -0
- package/dist/builtin/intercom/package.json +59 -0
- package/dist/builtin/intercom/reply-tracker.ts +102 -0
- package/dist/builtin/intercom/skills/intercom/SKILL.md +513 -0
- package/dist/builtin/intercom/types.ts +46 -0
- package/dist/builtin/intercom/ui/compose.ts +139 -0
- package/dist/builtin/intercom/ui/inline-message.ts +76 -0
- package/dist/builtin/intercom/ui/session-list.ts +162 -0
- package/dist/builtin/mcp/CHANGELOG.md +346 -0
- package/dist/builtin/mcp/LICENSE +21 -0
- package/dist/builtin/mcp/OAUTH.md +324 -0
- package/dist/builtin/mcp/README.md +373 -0
- package/dist/builtin/mcp/agent-dir.ts +21 -0
- package/dist/builtin/mcp/app-bridge.bundle.js +67 -0
- package/dist/builtin/mcp/cli.js +186 -0
- package/dist/builtin/mcp/commands.ts +420 -0
- package/dist/builtin/mcp/config.ts +667 -0
- package/dist/builtin/mcp/consent-manager.ts +64 -0
- package/dist/builtin/mcp/direct-tools.ts +427 -0
- package/dist/builtin/mcp/errors.ts +219 -0
- package/dist/builtin/mcp/glimpse-ui.ts +80 -0
- package/dist/builtin/mcp/host-html-template.ts +427 -0
- package/dist/builtin/mcp/index.ts +334 -0
- package/dist/builtin/mcp/init.ts +336 -0
- package/dist/builtin/mcp/lifecycle.ts +93 -0
- package/dist/builtin/mcp/logger.ts +169 -0
- package/dist/builtin/mcp/mcp-auth-flow.ts +362 -0
- package/dist/builtin/mcp/mcp-auth.ts +297 -0
- package/dist/builtin/mcp/mcp-callback-server.ts +284 -0
- package/dist/builtin/mcp/mcp-oauth-provider.ts +302 -0
- package/dist/builtin/mcp/mcp-panel.ts +826 -0
- package/dist/builtin/mcp/mcp-setup-panel.ts +577 -0
- package/dist/builtin/mcp/metadata-cache.ts +201 -0
- package/dist/builtin/mcp/npx-resolver.ts +424 -0
- package/dist/builtin/mcp/oauth-handler.ts +60 -0
- package/dist/builtin/mcp/onboarding-state.ts +68 -0
- package/dist/builtin/mcp/package.json +61 -0
- package/dist/builtin/mcp/proxy-modes.ts +803 -0
- package/dist/builtin/mcp/resource-tools.ts +17 -0
- package/dist/builtin/mcp/sampling-handler.ts +268 -0
- package/dist/builtin/mcp/server-manager.ts +375 -0
- package/dist/builtin/mcp/state.ts +41 -0
- package/dist/builtin/mcp/tool-metadata.ts +152 -0
- package/dist/builtin/mcp/tool-registrar.ts +46 -0
- package/dist/builtin/mcp/tool-result-renderer.ts +65 -0
- package/dist/builtin/mcp/types.ts +441 -0
- package/dist/builtin/mcp/ui-resource-handler.ts +145 -0
- package/dist/builtin/mcp/ui-server.ts +623 -0
- package/dist/builtin/mcp/ui-session.ts +384 -0
- package/dist/builtin/mcp/ui-stream-types.ts +89 -0
- package/dist/builtin/mcp/utils.ts +129 -0
- package/dist/builtin/subagents/CHANGELOG.md +1019 -0
- package/dist/builtin/subagents/README.md +991 -0
- package/dist/builtin/subagents/agents/code-simplifier.md +84 -0
- package/dist/builtin/subagents/agents/codebase-analyzer.md +158 -0
- package/dist/builtin/subagents/agents/codebase-locator.md +113 -0
- package/dist/builtin/subagents/agents/codebase-online-researcher.md +317 -0
- package/dist/builtin/subagents/agents/codebase-pattern-finder.md +236 -0
- package/dist/builtin/subagents/agents/codebase-research-analyzer.md +181 -0
- package/dist/builtin/subagents/agents/codebase-research-locator.md +146 -0
- package/dist/builtin/subagents/agents/debugger.md +92 -0
- package/dist/builtin/subagents/package.json +67 -0
- package/dist/builtin/subagents/prompts/gather-context-and-clarify.md +20 -0
- package/dist/builtin/subagents/prompts/parallel-cleanup.md +60 -0
- package/dist/builtin/subagents/prompts/parallel-context-build.md +55 -0
- package/dist/builtin/subagents/prompts/parallel-handoff-plan.md +77 -0
- package/dist/builtin/subagents/prompts/parallel-research.md +58 -0
- package/dist/builtin/subagents/prompts/parallel-review.md +52 -0
- package/dist/builtin/subagents/prompts/review-loop.md +48 -0
- package/dist/builtin/subagents/skills/subagent/SKILL.md +734 -0
- package/dist/builtin/subagents/src/agents/agent-management.ts +644 -0
- package/dist/builtin/subagents/src/agents/agent-scope.ts +6 -0
- package/dist/builtin/subagents/src/agents/agent-selection.ts +23 -0
- package/dist/builtin/subagents/src/agents/agent-serializer.ts +84 -0
- package/dist/builtin/subagents/src/agents/agents.ts +809 -0
- package/dist/builtin/subagents/src/agents/chain-serializer.ts +137 -0
- package/dist/builtin/subagents/src/agents/frontmatter.ts +29 -0
- package/dist/builtin/subagents/src/agents/identity.ts +30 -0
- package/dist/builtin/subagents/src/agents/skills.ts +630 -0
- package/dist/builtin/subagents/src/extension/control-notices.ts +92 -0
- package/dist/builtin/subagents/src/extension/doctor.ts +199 -0
- package/dist/builtin/subagents/src/extension/index.ts +586 -0
- package/dist/builtin/subagents/src/extension/schemas.ts +168 -0
- package/dist/builtin/subagents/src/intercom/intercom-bridge.ts +378 -0
- package/dist/builtin/subagents/src/intercom/result-intercom.ts +269 -0
- package/dist/builtin/subagents/src/runs/background/async-execution.ts +612 -0
- package/dist/builtin/subagents/src/runs/background/async-job-tracker.ts +267 -0
- package/dist/builtin/subagents/src/runs/background/async-resume.ts +332 -0
- package/dist/builtin/subagents/src/runs/background/async-status.ts +295 -0
- package/dist/builtin/subagents/src/runs/background/completion-dedupe.ts +63 -0
- package/dist/builtin/subagents/src/runs/background/notify.ts +108 -0
- package/dist/builtin/subagents/src/runs/background/parallel-groups.ts +45 -0
- package/dist/builtin/subagents/src/runs/background/result-watcher.ts +250 -0
- package/dist/builtin/subagents/src/runs/background/run-status.ts +193 -0
- package/dist/builtin/subagents/src/runs/background/stale-run-reconciler.ts +291 -0
- package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +1760 -0
- package/dist/builtin/subagents/src/runs/background/top-level-async.ts +13 -0
- package/dist/builtin/subagents/src/runs/foreground/chain-clarify.ts +1333 -0
- package/dist/builtin/subagents/src/runs/foreground/chain-execution.ts +932 -0
- package/dist/builtin/subagents/src/runs/foreground/execution.ts +902 -0
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +2231 -0
- package/dist/builtin/subagents/src/runs/shared/completion-guard.ts +125 -0
- package/dist/builtin/subagents/src/runs/shared/long-running-guard.ts +175 -0
- package/dist/builtin/subagents/src/runs/shared/model-fallback.ts +103 -0
- package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +108 -0
- package/dist/builtin/subagents/src/runs/shared/pi-args.ts +163 -0
- package/dist/builtin/subagents/src/runs/shared/pi-spawn.ts +115 -0
- package/dist/builtin/subagents/src/runs/shared/run-history.ts +56 -0
- package/dist/builtin/subagents/src/runs/shared/single-output.ts +154 -0
- package/dist/builtin/subagents/src/runs/shared/subagent-control.ts +226 -0
- package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +152 -0
- package/dist/builtin/subagents/src/runs/shared/worktree.ts +577 -0
- package/dist/builtin/subagents/src/shared/artifacts.ts +99 -0
- package/dist/builtin/subagents/src/shared/atomic-json.ts +16 -0
- package/dist/builtin/subagents/src/shared/file-coalescer.ts +40 -0
- package/dist/builtin/subagents/src/shared/fork-context.ts +76 -0
- package/dist/builtin/subagents/src/shared/formatters.ts +133 -0
- package/dist/builtin/subagents/src/shared/jsonl-writer.ts +81 -0
- package/dist/builtin/subagents/src/shared/model-info.ts +78 -0
- package/dist/builtin/subagents/src/shared/post-exit-stdio-guard.ts +85 -0
- package/dist/builtin/subagents/src/shared/session-identity.ts +10 -0
- package/dist/builtin/subagents/src/shared/session-tokens.ts +44 -0
- package/dist/builtin/subagents/src/shared/settings.ts +397 -0
- package/dist/builtin/subagents/src/shared/status-format.ts +49 -0
- package/dist/builtin/subagents/src/shared/types.ts +732 -0
- package/dist/builtin/subagents/src/shared/utils.ts +440 -0
- package/dist/builtin/subagents/src/slash/prompt-template-bridge.ts +397 -0
- package/dist/builtin/subagents/src/slash/slash-bridge.ts +174 -0
- package/dist/builtin/subagents/src/slash/slash-commands.ts +528 -0
- package/dist/builtin/subagents/src/slash/slash-live-state.ts +292 -0
- package/dist/builtin/subagents/src/tui/render-helpers.ts +80 -0
- package/dist/builtin/subagents/src/tui/render.ts +1257 -0
- package/dist/builtin/web-access/CHANGELOG.md +387 -0
- package/dist/builtin/web-access/LICENSE +21 -0
- package/dist/builtin/web-access/README.md +346 -0
- package/dist/builtin/web-access/activity.ts +101 -0
- package/dist/builtin/web-access/chrome-cookies.ts +322 -0
- package/dist/builtin/web-access/code-search.ts +107 -0
- package/dist/builtin/web-access/curator-page.ts +3359 -0
- package/dist/builtin/web-access/curator-server.ts +605 -0
- package/dist/builtin/web-access/exa.ts +521 -0
- package/dist/builtin/web-access/extract.ts +701 -0
- package/dist/builtin/web-access/gemini-api.ts +113 -0
- package/dist/builtin/web-access/gemini-search.ts +362 -0
- package/dist/builtin/web-access/gemini-url-context.ts +126 -0
- package/dist/builtin/web-access/gemini-web-config.ts +54 -0
- package/dist/builtin/web-access/gemini-web.ts +396 -0
- package/dist/builtin/web-access/github-api.ts +196 -0
- package/dist/builtin/web-access/github-extract.ts +635 -0
- package/dist/builtin/web-access/index.ts +2347 -0
- package/dist/builtin/web-access/package.json +54 -0
- package/dist/builtin/web-access/pdf-extract.ts +192 -0
- package/dist/builtin/web-access/perplexity.ts +196 -0
- package/dist/builtin/web-access/rsc-extract.ts +338 -0
- package/dist/builtin/web-access/storage.ts +72 -0
- package/dist/builtin/web-access/summary-review.ts +276 -0
- package/dist/builtin/web-access/utils.ts +44 -0
- package/dist/builtin/web-access/video-extract.ts +379 -0
- package/dist/builtin/web-access/youtube-extract.ts +311 -0
- package/dist/builtin/workflows/CHANGELOG.md +20 -0
- package/dist/builtin/workflows/README.md +323 -0
- package/dist/builtin/workflows/builtin/deep-research-codebase.ts +567 -0
- package/dist/builtin/workflows/builtin/index.ts +10 -0
- package/dist/builtin/workflows/builtin/open-claude-design.ts +985 -0
- package/dist/builtin/workflows/builtin/ralph.ts +613 -0
- package/dist/builtin/workflows/package.json +89 -0
- package/dist/builtin/workflows/skills/create-spec/SKILL.md +247 -0
- package/dist/builtin/workflows/skills/impeccable/SKILL.md +173 -0
- package/dist/builtin/workflows/skills/impeccable/reference/adapt.md +190 -0
- package/dist/builtin/workflows/skills/impeccable/reference/animate.md +175 -0
- package/dist/builtin/workflows/skills/impeccable/reference/audit.md +133 -0
- package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +113 -0
- package/dist/builtin/workflows/skills/impeccable/reference/brand.md +118 -0
- package/dist/builtin/workflows/skills/impeccable/reference/clarify.md +174 -0
- package/dist/builtin/workflows/skills/impeccable/reference/codex.md +105 -0
- package/dist/builtin/workflows/skills/impeccable/reference/cognitive-load.md +106 -0
- package/dist/builtin/workflows/skills/impeccable/reference/color-and-contrast.md +105 -0
- package/dist/builtin/workflows/skills/impeccable/reference/colorize.md +154 -0
- package/dist/builtin/workflows/skills/impeccable/reference/craft.md +123 -0
- package/dist/builtin/workflows/skills/impeccable/reference/critique.md +261 -0
- package/dist/builtin/workflows/skills/impeccable/reference/delight.md +302 -0
- package/dist/builtin/workflows/skills/impeccable/reference/distill.md +111 -0
- package/dist/builtin/workflows/skills/impeccable/reference/document.md +427 -0
- package/dist/builtin/workflows/skills/impeccable/reference/extract.md +69 -0
- package/dist/builtin/workflows/skills/impeccable/reference/harden.md +347 -0
- package/dist/builtin/workflows/skills/impeccable/reference/heuristics-scoring.md +234 -0
- package/dist/builtin/workflows/skills/impeccable/reference/interaction-design.md +195 -0
- package/dist/builtin/workflows/skills/impeccable/reference/layout.md +141 -0
- package/dist/builtin/workflows/skills/impeccable/reference/live.md +622 -0
- package/dist/builtin/workflows/skills/impeccable/reference/motion-design.md +109 -0
- package/dist/builtin/workflows/skills/impeccable/reference/onboard.md +234 -0
- package/dist/builtin/workflows/skills/impeccable/reference/optimize.md +258 -0
- package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +130 -0
- package/dist/builtin/workflows/skills/impeccable/reference/personas.md +179 -0
- package/dist/builtin/workflows/skills/impeccable/reference/polish.md +242 -0
- package/dist/builtin/workflows/skills/impeccable/reference/product.md +62 -0
- package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +99 -0
- package/dist/builtin/workflows/skills/impeccable/reference/responsive-design.md +114 -0
- package/dist/builtin/workflows/skills/impeccable/reference/shape.md +165 -0
- package/dist/builtin/workflows/skills/impeccable/reference/spatial-design.md +100 -0
- package/dist/builtin/workflows/skills/impeccable/reference/teach.md +156 -0
- package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +124 -0
- package/dist/builtin/workflows/skills/impeccable/reference/typography.md +159 -0
- package/dist/builtin/workflows/skills/impeccable/reference/ux-writing.md +107 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/cleanup-deprecated.mjs +284 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/command-metadata.json +94 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +226 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/design-parser.mjs +820 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detect-csp.mjs +198 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/impeccable-paths.mjs +110 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/is-generated.mjs +69 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +646 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser-session.js +123 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +4865 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-complete.mjs +75 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-completion.mjs +18 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +446 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +200 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-resume.mjs +48 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +847 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-session-store.mjs +254 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +47 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +632 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +247 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/load-context.mjs +141 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/pin.mjs +214 -0
- package/dist/builtin/workflows/skills/playwright-cli/SKILL.md +392 -0
- package/dist/builtin/workflows/skills/playwright-cli/references/element-attributes.md +23 -0
- package/dist/builtin/workflows/skills/playwright-cli/references/playwright-tests.md +39 -0
- package/dist/builtin/workflows/skills/playwright-cli/references/request-mocking.md +87 -0
- package/dist/builtin/workflows/skills/playwright-cli/references/running-code.md +241 -0
- package/dist/builtin/workflows/skills/playwright-cli/references/session-management.md +225 -0
- package/dist/builtin/workflows/skills/playwright-cli/references/spec-driven-testing.md +305 -0
- package/dist/builtin/workflows/skills/playwright-cli/references/storage-state.md +275 -0
- package/dist/builtin/workflows/skills/playwright-cli/references/test-generation.md +134 -0
- package/dist/builtin/workflows/skills/playwright-cli/references/tracing.md +139 -0
- package/dist/builtin/workflows/skills/playwright-cli/references/video-recording.md +143 -0
- package/dist/builtin/workflows/skills/prompt-engineer/SKILL.md +263 -0
- package/dist/builtin/workflows/skills/prompt-engineer/references/advanced_patterns.md +271 -0
- package/dist/builtin/workflows/skills/prompt-engineer/references/core_prompting.md +137 -0
- package/dist/builtin/workflows/skills/prompt-engineer/references/quality_improvement.md +193 -0
- package/dist/builtin/workflows/skills/research-codebase/SKILL.md +226 -0
- package/dist/builtin/workflows/skills/tdd/SKILL.md +109 -0
- package/dist/builtin/workflows/skills/tdd/deep-modules.md +33 -0
- package/dist/builtin/workflows/skills/tdd/interface-design.md +31 -0
- package/dist/builtin/workflows/skills/tdd/mocking.md +59 -0
- package/dist/builtin/workflows/skills/tdd/refactoring.md +10 -0
- package/dist/builtin/workflows/skills/tdd/tests.md +61 -0
- package/dist/builtin/workflows/skills/workflow/SKILL.md +255 -0
- package/dist/builtin/workflows/skills/workflow/references/context-engineering/advanced-evaluation.md +404 -0
- package/dist/builtin/workflows/skills/workflow/references/context-engineering/bdi-mental-states.md +313 -0
- package/dist/builtin/workflows/skills/workflow/references/context-engineering/context-compression.md +274 -0
- package/dist/builtin/workflows/skills/workflow/references/context-engineering/context-degradation.md +208 -0
- package/dist/builtin/workflows/skills/workflow/references/context-engineering/context-fundamentals.md +203 -0
- package/dist/builtin/workflows/skills/workflow/references/context-engineering/context-optimization.md +197 -0
- package/dist/builtin/workflows/skills/workflow/references/context-engineering/evaluation.md +253 -0
- package/dist/builtin/workflows/skills/workflow/references/context-engineering/filesystem-context.md +289 -0
- package/dist/builtin/workflows/skills/workflow/references/context-engineering/hosted-agents.md +262 -0
- package/dist/builtin/workflows/skills/workflow/references/context-engineering/memory-systems.md +221 -0
- package/dist/builtin/workflows/skills/workflow/references/context-engineering/multi-agent-patterns.md +259 -0
- package/dist/builtin/workflows/skills/workflow/references/context-engineering/project-development.md +293 -0
- package/dist/builtin/workflows/skills/workflow/references/context-engineering/tool-design.md +273 -0
- package/dist/builtin/workflows/skills/workflow/references/context-engineering.md +23 -0
- package/dist/builtin/workflows/skills/workflow/references/design-checklist.md +79 -0
- package/dist/builtin/workflows/skills/workflow/references/running-workflows.md +107 -0
- package/dist/builtin/workflows/skills/workflow/references/sdk-authoring.md +140 -0
- package/dist/builtin/workflows/src/extension/background-ui-adapter.ts +168 -0
- package/dist/builtin/workflows/src/extension/companions.ts +210 -0
- package/dist/builtin/workflows/src/extension/config-loader.ts +493 -0
- package/dist/builtin/workflows/src/extension/discovery.ts +501 -0
- package/dist/builtin/workflows/src/extension/dispatcher.ts +173 -0
- package/dist/builtin/workflows/src/extension/index.ts +2143 -0
- package/dist/builtin/workflows/src/extension/mcp.ts +110 -0
- package/dist/builtin/workflows/src/extension/render-call.ts +39 -0
- package/dist/builtin/workflows/src/extension/render-result.ts +214 -0
- package/dist/builtin/workflows/src/extension/renderers.ts +87 -0
- package/dist/builtin/workflows/src/extension/runtime.ts +360 -0
- package/dist/builtin/workflows/src/extension/status-writer.ts +167 -0
- package/dist/builtin/workflows/src/extension/wiring.ts +555 -0
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +102 -0
- package/dist/builtin/workflows/src/index.ts +25 -0
- package/dist/builtin/workflows/src/intercom/intercom-bridge.ts +93 -0
- package/dist/builtin/workflows/src/intercom/intercom-routing.ts +125 -0
- package/dist/builtin/workflows/src/intercom/result-intercom.ts +240 -0
- package/dist/builtin/workflows/src/runs/background/cancellation-registry.ts +113 -0
- package/dist/builtin/workflows/src/runs/background/job-tracker.ts +81 -0
- package/dist/builtin/workflows/src/runs/background/runner.ts +152 -0
- package/dist/builtin/workflows/src/runs/background/status.ts +354 -0
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +1522 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +233 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +712 -0
- package/dist/builtin/workflows/src/runs/shared/concurrency.ts +76 -0
- package/dist/builtin/workflows/src/runs/shared/graph-inference.ts +69 -0
- package/dist/builtin/workflows/src/runs/shared/model-fallback.ts +293 -0
- package/dist/builtin/workflows/src/runs/shared/validate-inputs.ts +83 -0
- package/dist/builtin/workflows/src/runs/shared/workflow-runner.ts +170 -0
- package/dist/builtin/workflows/src/runs/shared/worktree.ts +577 -0
- package/dist/builtin/workflows/src/shared/persistence-compaction-policy.ts +72 -0
- package/dist/builtin/workflows/src/shared/persistence-restore.ts +257 -0
- package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +145 -0
- package/dist/builtin/workflows/src/shared/render-inputs-schema.ts +196 -0
- package/dist/builtin/workflows/src/shared/store-types.ts +160 -0
- package/dist/builtin/workflows/src/shared/store.ts +579 -0
- package/dist/builtin/workflows/src/shared/types.ts +566 -0
- package/dist/builtin/workflows/src/tui/chat-surface-message.ts +224 -0
- package/dist/builtin/workflows/src/tui/chat-surface.ts +511 -0
- package/dist/builtin/workflows/src/tui/color-utils.ts +64 -0
- package/dist/builtin/workflows/src/tui/connectors.ts +88 -0
- package/dist/builtin/workflows/src/tui/dispatch-confirm.ts +307 -0
- package/dist/builtin/workflows/src/tui/edge.ts +24 -0
- package/dist/builtin/workflows/src/tui/graph-canvas.ts +108 -0
- package/dist/builtin/workflows/src/tui/graph-theme.ts +283 -0
- package/dist/builtin/workflows/src/tui/graph-view.ts +1217 -0
- package/dist/builtin/workflows/src/tui/header.ts +172 -0
- package/dist/builtin/workflows/src/tui/inline-form-card.ts +421 -0
- package/dist/builtin/workflows/src/tui/inline-form-editor.ts +638 -0
- package/dist/builtin/workflows/src/tui/inline-form-overlay.ts +326 -0
- package/dist/builtin/workflows/src/tui/inline-form-store.ts +78 -0
- package/dist/builtin/workflows/src/tui/inputs-overlay.ts +163 -0
- package/dist/builtin/workflows/src/tui/inputs-picker.ts +888 -0
- package/dist/builtin/workflows/src/tui/keybindings-adapter.ts +154 -0
- package/dist/builtin/workflows/src/tui/layout.ts +153 -0
- package/dist/builtin/workflows/src/tui/node-card.ts +274 -0
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +277 -0
- package/dist/builtin/workflows/src/tui/prompt-card.ts +501 -0
- package/dist/builtin/workflows/src/tui/renderers.ts +15 -0
- package/dist/builtin/workflows/src/tui/run-detail.ts +339 -0
- package/dist/builtin/workflows/src/tui/session-confirm.ts +202 -0
- package/dist/builtin/workflows/src/tui/session-list.ts +32 -0
- package/dist/builtin/workflows/src/tui/session-overlays.ts +239 -0
- package/dist/builtin/workflows/src/tui/session-picker.ts +399 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +1873 -0
- package/dist/builtin/workflows/src/tui/status-helpers.ts +73 -0
- package/dist/builtin/workflows/src/tui/status-list.ts +361 -0
- package/dist/builtin/workflows/src/tui/store-widget-installer.ts +206 -0
- package/dist/builtin/workflows/src/tui/switcher.ts +121 -0
- package/dist/builtin/workflows/src/tui/text-helpers.ts +31 -0
- package/dist/builtin/workflows/src/tui/toast.ts +106 -0
- package/dist/builtin/workflows/src/tui/widget.ts +348 -0
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +285 -0
- package/dist/builtin/workflows/src/tui/workflow-list.ts +224 -0
- package/dist/builtin/workflows/src/workflows/define-workflow.ts +150 -0
- package/dist/builtin/workflows/src/workflows/identity.ts +39 -0
- package/dist/builtin/workflows/src/workflows/registry.ts +113 -0
- package/dist/bun/cli.d.ts +3 -0
- package/dist/bun/cli.d.ts.map +1 -0
- package/dist/bun/cli.js +9 -0
- package/dist/bun/cli.js.map +1 -0
- package/dist/bun/register-bedrock.d.ts +2 -0
- package/dist/bun/register-bedrock.d.ts.map +1 -0
- package/dist/bun/register-bedrock.js +4 -0
- package/dist/bun/register-bedrock.js.map +1 -0
- package/dist/bun/restore-sandbox-env.d.ts +13 -0
- package/dist/bun/restore-sandbox-env.d.ts.map +1 -0
- package/dist/bun/restore-sandbox-env.js +32 -0
- package/dist/bun/restore-sandbox-env.js.map +1 -0
- package/dist/cli/args.d.ts +53 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +341 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/config-selector.d.ts +14 -0
- package/dist/cli/config-selector.d.ts.map +1 -0
- package/dist/cli/config-selector.js +31 -0
- package/dist/cli/config-selector.js.map +1 -0
- package/dist/cli/file-processor.d.ts +15 -0
- package/dist/cli/file-processor.d.ts.map +1 -0
- package/dist/cli/file-processor.js +83 -0
- package/dist/cli/file-processor.js.map +1 -0
- package/dist/cli/initial-message.d.ts +18 -0
- package/dist/cli/initial-message.d.ts.map +1 -0
- package/dist/cli/initial-message.js +22 -0
- package/dist/cli/initial-message.js.map +1 -0
- package/dist/cli/list-models.d.ts +9 -0
- package/dist/cli/list-models.d.ts.map +1 -0
- package/dist/cli/list-models.js +98 -0
- package/dist/cli/list-models.js.map +1 -0
- package/dist/cli/session-picker.d.ts +9 -0
- package/dist/cli/session-picker.d.ts.map +1 -0
- package/dist/cli/session-picker.js +35 -0
- package/dist/cli/session-picker.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +20 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +102 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +411 -0
- package/dist/config.js.map +1 -0
- package/dist/core/agent-session-runtime.d.ts +117 -0
- package/dist/core/agent-session-runtime.d.ts.map +1 -0
- package/dist/core/agent-session-runtime.js +292 -0
- package/dist/core/agent-session-runtime.js.map +1 -0
- package/dist/core/agent-session-services.d.ts +86 -0
- package/dist/core/agent-session-services.d.ts.map +1 -0
- package/dist/core/agent-session-services.js +117 -0
- package/dist/core/agent-session-services.js.map +1 -0
- package/dist/core/agent-session.d.ts +595 -0
- package/dist/core/agent-session.d.ts.map +1 -0
- package/dist/core/agent-session.js +2518 -0
- package/dist/core/agent-session.js.map +1 -0
- package/dist/core/auth-guidance.d.ts +5 -0
- package/dist/core/auth-guidance.d.ts.map +1 -0
- package/dist/core/auth-guidance.js +21 -0
- package/dist/core/auth-guidance.js.map +1 -0
- package/dist/core/auth-storage.d.ts +141 -0
- package/dist/core/auth-storage.d.ts.map +1 -0
- package/dist/core/auth-storage.js +437 -0
- package/dist/core/auth-storage.js.map +1 -0
- package/dist/core/bash-executor.d.ts +32 -0
- package/dist/core/bash-executor.d.ts.map +1 -0
- package/dist/core/bash-executor.js +111 -0
- package/dist/core/bash-executor.js.map +1 -0
- package/dist/core/builtin-packages.d.ts +14 -0
- package/dist/core/builtin-packages.d.ts.map +1 -0
- package/dist/core/builtin-packages.js +113 -0
- package/dist/core/builtin-packages.js.map +1 -0
- package/dist/core/compaction/branch-summarization.d.ts +88 -0
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -0
- package/dist/core/compaction/branch-summarization.js +243 -0
- package/dist/core/compaction/branch-summarization.js.map +1 -0
- package/dist/core/compaction/compaction.d.ts +121 -0
- package/dist/core/compaction/compaction.d.ts.map +1 -0
- package/dist/core/compaction/compaction.js +615 -0
- package/dist/core/compaction/compaction.js.map +1 -0
- package/dist/core/compaction/index.d.ts +7 -0
- package/dist/core/compaction/index.d.ts.map +1 -0
- package/dist/core/compaction/index.js +7 -0
- package/dist/core/compaction/index.js.map +1 -0
- package/dist/core/compaction/utils.d.ts +38 -0
- package/dist/core/compaction/utils.d.ts.map +1 -0
- package/dist/core/compaction/utils.js +153 -0
- package/dist/core/compaction/utils.js.map +1 -0
- package/dist/core/defaults.d.ts +3 -0
- package/dist/core/defaults.d.ts.map +1 -0
- package/dist/core/defaults.js +2 -0
- package/dist/core/defaults.js.map +1 -0
- package/dist/core/diagnostics.d.ts +15 -0
- package/dist/core/diagnostics.d.ts.map +1 -0
- package/dist/core/diagnostics.js +2 -0
- package/dist/core/diagnostics.js.map +1 -0
- package/dist/core/event-bus.d.ts +9 -0
- package/dist/core/event-bus.d.ts.map +1 -0
- package/dist/core/event-bus.js +25 -0
- package/dist/core/event-bus.js.map +1 -0
- package/dist/core/exec.d.ts +29 -0
- package/dist/core/exec.d.ts.map +1 -0
- package/dist/core/exec.js +75 -0
- package/dist/core/exec.js.map +1 -0
- package/dist/core/export-html/ansi-to-html.d.ts +22 -0
- package/dist/core/export-html/ansi-to-html.d.ts.map +1 -0
- package/dist/core/export-html/ansi-to-html.js +249 -0
- package/dist/core/export-html/ansi-to-html.js.map +1 -0
- package/dist/core/export-html/index.d.ts +37 -0
- package/dist/core/export-html/index.d.ts.map +1 -0
- package/dist/core/export-html/index.js +224 -0
- package/dist/core/export-html/index.js.map +1 -0
- package/dist/core/export-html/template.css +1066 -0
- package/dist/core/export-html/template.html +55 -0
- package/dist/core/export-html/template.js +1834 -0
- package/dist/core/export-html/tool-renderer.d.ts +34 -0
- package/dist/core/export-html/tool-renderer.d.ts.map +1 -0
- package/dist/core/export-html/tool-renderer.js +108 -0
- package/dist/core/export-html/tool-renderer.js.map +1 -0
- package/dist/core/export-html/vendor/highlight.min.js +1213 -0
- package/dist/core/export-html/vendor/marked.min.js +6 -0
- package/dist/core/extensions/index.d.ts +12 -0
- package/dist/core/extensions/index.d.ts.map +1 -0
- package/dist/core/extensions/index.js +9 -0
- package/dist/core/extensions/index.js.map +1 -0
- package/dist/core/extensions/loader.d.ts +24 -0
- package/dist/core/extensions/loader.d.ts.map +1 -0
- package/dist/core/extensions/loader.js +501 -0
- package/dist/core/extensions/loader.js.map +1 -0
- package/dist/core/extensions/runner.d.ts +159 -0
- package/dist/core/extensions/runner.d.ts.map +1 -0
- package/dist/core/extensions/runner.js +817 -0
- package/dist/core/extensions/runner.js.map +1 -0
- package/dist/core/extensions/types.d.ts +1173 -0
- package/dist/core/extensions/types.d.ts.map +1 -0
- package/dist/core/extensions/types.js +45 -0
- package/dist/core/extensions/types.js.map +1 -0
- package/dist/core/extensions/wrapper.d.ts +20 -0
- package/dist/core/extensions/wrapper.d.ts.map +1 -0
- package/dist/core/extensions/wrapper.js +22 -0
- package/dist/core/extensions/wrapper.js.map +1 -0
- package/dist/core/footer-data-provider.d.ts +52 -0
- package/dist/core/footer-data-provider.d.ts.map +1 -0
- package/dist/core/footer-data-provider.js +309 -0
- package/dist/core/footer-data-provider.js.map +1 -0
- package/dist/core/index.d.ts +12 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +12 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/keybindings.d.ts +353 -0
- package/dist/core/keybindings.d.ts.map +1 -0
- package/dist/core/keybindings.js +294 -0
- package/dist/core/keybindings.js.map +1 -0
- package/dist/core/messages.d.ts +77 -0
- package/dist/core/messages.d.ts.map +1 -0
- package/dist/core/messages.js +123 -0
- package/dist/core/messages.js.map +1 -0
- package/dist/core/model-registry.d.ts +150 -0
- package/dist/core/model-registry.d.ts.map +1 -0
- package/dist/core/model-registry.js +726 -0
- package/dist/core/model-registry.js.map +1 -0
- package/dist/core/model-resolver.d.ts +110 -0
- package/dist/core/model-resolver.d.ts.map +1 -0
- package/dist/core/model-resolver.js +493 -0
- package/dist/core/model-resolver.js.map +1 -0
- package/dist/core/output-guard.d.ts +6 -0
- package/dist/core/output-guard.d.ts.map +1 -0
- package/dist/core/output-guard.js +59 -0
- package/dist/core/output-guard.js.map +1 -0
- package/dist/core/package-manager.d.ts +198 -0
- package/dist/core/package-manager.d.ts.map +1 -0
- package/dist/core/package-manager.js +1970 -0
- package/dist/core/package-manager.js.map +1 -0
- package/dist/core/prompt-templates.d.ts +52 -0
- package/dist/core/prompt-templates.d.ts.map +1 -0
- package/dist/core/prompt-templates.js +250 -0
- package/dist/core/prompt-templates.js.map +1 -0
- package/dist/core/provider-display-names.d.ts +2 -0
- package/dist/core/provider-display-names.d.ts.map +1 -0
- package/dist/core/provider-display-names.js +33 -0
- package/dist/core/provider-display-names.js.map +1 -0
- package/dist/core/resolve-config-value.d.ts +23 -0
- package/dist/core/resolve-config-value.d.ts.map +1 -0
- package/dist/core/resolve-config-value.js +126 -0
- package/dist/core/resolve-config-value.js.map +1 -0
- package/dist/core/resource-loader.d.ts +196 -0
- package/dist/core/resource-loader.d.ts.map +1 -0
- package/dist/core/resource-loader.js +698 -0
- package/dist/core/resource-loader.js.map +1 -0
- package/dist/core/sdk.d.ts +107 -0
- package/dist/core/sdk.d.ts.map +1 -0
- package/dist/core/sdk.js +291 -0
- package/dist/core/sdk.js.map +1 -0
- package/dist/core/session-cwd.d.ts +19 -0
- package/dist/core/session-cwd.d.ts.map +1 -0
- package/dist/core/session-cwd.js +37 -0
- package/dist/core/session-cwd.js.map +1 -0
- package/dist/core/session-manager.d.ts +333 -0
- package/dist/core/session-manager.d.ts.map +1 -0
- package/dist/core/session-manager.js +1118 -0
- package/dist/core/session-manager.js.map +1 -0
- package/dist/core/settings-manager.d.ts +261 -0
- package/dist/core/settings-manager.d.ts.map +1 -0
- package/dist/core/settings-manager.js +773 -0
- package/dist/core/settings-manager.js.map +1 -0
- package/dist/core/skills.d.ts +60 -0
- package/dist/core/skills.d.ts.map +1 -0
- package/dist/core/skills.js +404 -0
- package/dist/core/skills.js.map +1 -0
- package/dist/core/slash-commands.d.ts +14 -0
- package/dist/core/slash-commands.d.ts.map +1 -0
- package/dist/core/slash-commands.js +25 -0
- package/dist/core/slash-commands.js.map +1 -0
- package/dist/core/source-info.d.ts +18 -0
- package/dist/core/source-info.d.ts.map +1 -0
- package/dist/core/source-info.js +19 -0
- package/dist/core/source-info.js.map +1 -0
- package/dist/core/system-prompt.d.ts +28 -0
- package/dist/core/system-prompt.d.ts.map +1 -0
- package/dist/core/system-prompt.js +120 -0
- package/dist/core/system-prompt.js.map +1 -0
- package/dist/core/telemetry.d.ts +3 -0
- package/dist/core/telemetry.d.ts.map +1 -0
- package/dist/core/telemetry.js +10 -0
- package/dist/core/telemetry.js.map +1 -0
- package/dist/core/timings.d.ts +8 -0
- package/dist/core/timings.d.ts.map +1 -0
- package/dist/core/timings.js +32 -0
- package/dist/core/timings.js.map +1 -0
- package/dist/core/tools/ask-user-question/ask-user-question.d.ts +10 -0
- package/dist/core/tools/ask-user-question/ask-user-question.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/ask-user-question.js +82 -0
- package/dist/core/tools/ask-user-question/ask-user-question.js.map +1 -0
- package/dist/core/tools/ask-user-question/config.d.ts +11 -0
- package/dist/core/tools/ask-user-question/config.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/config.js +34 -0
- package/dist/core/tools/ask-user-question/config.js.map +1 -0
- package/dist/core/tools/ask-user-question/index.d.ts +19 -0
- package/dist/core/tools/ask-user-question/index.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/index.js +19 -0
- package/dist/core/tools/ask-user-question/index.js.map +1 -0
- package/dist/core/tools/ask-user-question/state/build-questionnaire.d.ts +36 -0
- package/dist/core/tools/ask-user-question/state/build-questionnaire.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/state/build-questionnaire.js +213 -0
- package/dist/core/tools/ask-user-question/state/build-questionnaire.js.map +1 -0
- package/dist/core/tools/ask-user-question/state/key-router.d.ts +53 -0
- package/dist/core/tools/ask-user-question/state/key-router.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/state/key-router.js +240 -0
- package/dist/core/tools/ask-user-question/state/key-router.js.map +1 -0
- package/dist/core/tools/ask-user-question/state/questionnaire-session.d.ts +61 -0
- package/dist/core/tools/ask-user-question/state/questionnaire-session.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/state/questionnaire-session.js +147 -0
- package/dist/core/tools/ask-user-question/state/questionnaire-session.js.map +1 -0
- package/dist/core/tools/ask-user-question/state/row-intent.d.ts +91 -0
- package/dist/core/tools/ask-user-question/state/row-intent.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/state/row-intent.js +91 -0
- package/dist/core/tools/ask-user-question/state/row-intent.js.map +1 -0
- package/dist/core/tools/ask-user-question/state/selectors/contract.d.ts +21 -0
- package/dist/core/tools/ask-user-question/state/selectors/contract.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/state/selectors/contract.js +2 -0
- package/dist/core/tools/ask-user-question/state/selectors/contract.js.map +1 -0
- package/dist/core/tools/ask-user-question/state/selectors/derivations.d.ts +44 -0
- package/dist/core/tools/ask-user-question/state/selectors/derivations.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/state/selectors/derivations.js +76 -0
- package/dist/core/tools/ask-user-question/state/selectors/derivations.js.map +1 -0
- package/dist/core/tools/ask-user-question/state/selectors/focus.d.ts +15 -0
- package/dist/core/tools/ask-user-question/state/selectors/focus.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/state/selectors/focus.js +18 -0
- package/dist/core/tools/ask-user-question/state/selectors/focus.js.map +1 -0
- package/dist/core/tools/ask-user-question/state/selectors/projections.d.ts +16 -0
- package/dist/core/tools/ask-user-question/state/selectors/projections.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/state/selectors/projections.js +71 -0
- package/dist/core/tools/ask-user-question/state/selectors/projections.js.map +1 -0
- package/dist/core/tools/ask-user-question/state/state-reducer.d.ts +44 -0
- package/dist/core/tools/ask-user-question/state/state-reducer.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/state/state-reducer.js +239 -0
- package/dist/core/tools/ask-user-question/state/state-reducer.js.map +1 -0
- package/dist/core/tools/ask-user-question/state/state.d.ts +42 -0
- package/dist/core/tools/ask-user-question/state/state.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/state/state.js +2 -0
- package/dist/core/tools/ask-user-question/state/state.js.map +1 -0
- package/dist/core/tools/ask-user-question/tool/format-answer.d.ts +30 -0
- package/dist/core/tools/ask-user-question/tool/format-answer.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/tool/format-answer.js +39 -0
- package/dist/core/tools/ask-user-question/tool/format-answer.js.map +1 -0
- package/dist/core/tools/ask-user-question/tool/response-envelope.d.ts +29 -0
- package/dist/core/tools/ask-user-question/tool/response-envelope.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/tool/response-envelope.js +46 -0
- package/dist/core/tools/ask-user-question/tool/response-envelope.js.map +1 -0
- package/dist/core/tools/ask-user-question/tool/types.d.ts +113 -0
- package/dist/core/tools/ask-user-question/tool/types.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/tool/types.js +81 -0
- package/dist/core/tools/ask-user-question/tool/types.js.map +1 -0
- package/dist/core/tools/ask-user-question/tool/validate-questionnaire.d.ts +21 -0
- package/dist/core/tools/ask-user-question/tool/validate-questionnaire.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/tool/validate-questionnaire.js +49 -0
- package/dist/core/tools/ask-user-question/tool/validate-questionnaire.js.map +1 -0
- package/dist/core/tools/ask-user-question/view/body-residual-spacer.d.ts +18 -0
- package/dist/core/tools/ask-user-question/view/body-residual-spacer.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/view/body-residual-spacer.js +21 -0
- package/dist/core/tools/ask-user-question/view/body-residual-spacer.js.map +1 -0
- package/dist/core/tools/ask-user-question/view/component-binding.d.ts +23 -0
- package/dist/core/tools/ask-user-question/view/component-binding.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/view/component-binding.js +16 -0
- package/dist/core/tools/ask-user-question/view/component-binding.js.map +1 -0
- package/dist/core/tools/ask-user-question/view/components/chat-row-view.d.ts +40 -0
- package/dist/core/tools/ask-user-question/view/components/chat-row-view.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/view/components/chat-row-view.js +31 -0
- package/dist/core/tools/ask-user-question/view/components/chat-row-view.js.map +1 -0
- package/dist/core/tools/ask-user-question/view/components/multi-select-view.d.ts +35 -0
- package/dist/core/tools/ask-user-question/view/components/multi-select-view.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/view/components/multi-select-view.js +91 -0
- package/dist/core/tools/ask-user-question/view/components/multi-select-view.js.map +1 -0
- package/dist/core/tools/ask-user-question/view/components/option-list-view.d.ts +43 -0
- package/dist/core/tools/ask-user-question/view/components/option-list-view.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/view/components/option-list-view.js +37 -0
- package/dist/core/tools/ask-user-question/view/components/option-list-view.js.map +1 -0
- package/dist/core/tools/ask-user-question/view/components/preview/markdown-content-cache.d.ts +36 -0
- package/dist/core/tools/ask-user-question/view/components/preview/markdown-content-cache.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/view/components/preview/markdown-content-cache.js +66 -0
- package/dist/core/tools/ask-user-question/view/components/preview/markdown-content-cache.js.map +1 -0
- package/dist/core/tools/ask-user-question/view/components/preview/preview-block-renderer.d.ts +46 -0
- package/dist/core/tools/ask-user-question/view/components/preview/preview-block-renderer.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/view/components/preview/preview-block-renderer.js +69 -0
- package/dist/core/tools/ask-user-question/view/components/preview/preview-block-renderer.js.map +1 -0
- package/dist/core/tools/ask-user-question/view/components/preview/preview-box-renderer.d.ts +39 -0
- package/dist/core/tools/ask-user-question/view/components/preview/preview-box-renderer.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/view/components/preview/preview-box-renderer.js +76 -0
- package/dist/core/tools/ask-user-question/view/components/preview/preview-box-renderer.js.map +1 -0
- package/dist/core/tools/ask-user-question/view/components/preview/preview-layout-decider.d.ts +116 -0
- package/dist/core/tools/ask-user-question/view/components/preview/preview-layout-decider.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/view/components/preview/preview-layout-decider.js +173 -0
- package/dist/core/tools/ask-user-question/view/components/preview/preview-layout-decider.js.map +1 -0
- package/dist/core/tools/ask-user-question/view/components/preview/preview-pane.d.ts +66 -0
- package/dist/core/tools/ask-user-question/view/components/preview/preview-pane.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/view/components/preview/preview-pane.js +124 -0
- package/dist/core/tools/ask-user-question/view/components/preview/preview-pane.js.map +1 -0
- package/dist/core/tools/ask-user-question/view/components/submit-picker.d.ts +37 -0
- package/dist/core/tools/ask-user-question/view/components/submit-picker.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/view/components/submit-picker.js +44 -0
- package/dist/core/tools/ask-user-question/view/components/submit-picker.js.map +1 -0
- package/dist/core/tools/ask-user-question/view/components/tab-bar.d.ts +32 -0
- package/dist/core/tools/ask-user-question/view/components/tab-bar.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/view/components/tab-bar.js +33 -0
- package/dist/core/tools/ask-user-question/view/components/tab-bar.js.map +1 -0
- package/dist/core/tools/ask-user-question/view/components/wrapping-select.d.ts +122 -0
- package/dist/core/tools/ask-user-question/view/components/wrapping-select.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/view/components/wrapping-select.js +161 -0
- package/dist/core/tools/ask-user-question/view/components/wrapping-select.js.map +1 -0
- package/dist/core/tools/ask-user-question/view/dialog-builder.d.ts +66 -0
- package/dist/core/tools/ask-user-question/view/dialog-builder.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/view/dialog-builder.js +85 -0
- package/dist/core/tools/ask-user-question/view/dialog-builder.js.map +1 -0
- package/dist/core/tools/ask-user-question/view/props-adapter.d.ts +58 -0
- package/dist/core/tools/ask-user-question/view/props-adapter.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/view/props-adapter.js +67 -0
- package/dist/core/tools/ask-user-question/view/props-adapter.js.map +1 -0
- package/dist/core/tools/ask-user-question/view/stateful-view.d.ts +24 -0
- package/dist/core/tools/ask-user-question/view/stateful-view.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/view/stateful-view.js +2 -0
- package/dist/core/tools/ask-user-question/view/stateful-view.js.map +1 -0
- package/dist/core/tools/ask-user-question/view/tab-components.d.ts +15 -0
- package/dist/core/tools/ask-user-question/view/tab-components.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/view/tab-components.js +2 -0
- package/dist/core/tools/ask-user-question/view/tab-components.js.map +1 -0
- package/dist/core/tools/ask-user-question/view/tab-content-strategy.d.ts +71 -0
- package/dist/core/tools/ask-user-question/view/tab-content-strategy.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/view/tab-content-strategy.js +129 -0
- package/dist/core/tools/ask-user-question/view/tab-content-strategy.js.map +1 -0
- package/dist/core/tools/bash.d.ts +68 -0
- package/dist/core/tools/bash.d.ts.map +1 -0
- package/dist/core/tools/bash.js +338 -0
- package/dist/core/tools/bash.js.map +1 -0
- package/dist/core/tools/edit-diff.d.ts +85 -0
- package/dist/core/tools/edit-diff.d.ts.map +1 -0
- package/dist/core/tools/edit-diff.js +338 -0
- package/dist/core/tools/edit-diff.js.map +1 -0
- package/dist/core/tools/edit.d.ts +49 -0
- package/dist/core/tools/edit.d.ts.map +1 -0
- package/dist/core/tools/edit.js +324 -0
- package/dist/core/tools/edit.js.map +1 -0
- package/dist/core/tools/file-mutation-queue.d.ts +6 -0
- package/dist/core/tools/file-mutation-queue.d.ts.map +1 -0
- package/dist/core/tools/file-mutation-queue.js +37 -0
- package/dist/core/tools/file-mutation-queue.js.map +1 -0
- package/dist/core/tools/find.d.ts +35 -0
- package/dist/core/tools/find.d.ts.map +1 -0
- package/dist/core/tools/find.js +298 -0
- package/dist/core/tools/find.js.map +1 -0
- package/dist/core/tools/grep.d.ts +37 -0
- package/dist/core/tools/grep.d.ts.map +1 -0
- package/dist/core/tools/grep.js +304 -0
- package/dist/core/tools/grep.js.map +1 -0
- package/dist/core/tools/index.d.ts +42 -0
- package/dist/core/tools/index.d.ts.map +1 -0
- package/dist/core/tools/index.js +139 -0
- package/dist/core/tools/index.js.map +1 -0
- package/dist/core/tools/ls.d.ts +37 -0
- package/dist/core/tools/ls.d.ts.map +1 -0
- package/dist/core/tools/ls.js +169 -0
- package/dist/core/tools/ls.js.map +1 -0
- package/dist/core/tools/output-accumulator.d.ts +50 -0
- package/dist/core/tools/output-accumulator.d.ts.map +1 -0
- package/dist/core/tools/output-accumulator.js +172 -0
- package/dist/core/tools/output-accumulator.js.map +1 -0
- package/dist/core/tools/path-utils.d.ts +8 -0
- package/dist/core/tools/path-utils.d.ts.map +1 -0
- package/dist/core/tools/path-utils.js +81 -0
- package/dist/core/tools/path-utils.js.map +1 -0
- package/dist/core/tools/read.d.ts +35 -0
- package/dist/core/tools/read.d.ts.map +1 -0
- package/dist/core/tools/read.js +289 -0
- package/dist/core/tools/read.js.map +1 -0
- package/dist/core/tools/render-utils.d.ts +21 -0
- package/dist/core/tools/render-utils.d.ts.map +1 -0
- package/dist/core/tools/render-utils.js +49 -0
- package/dist/core/tools/render-utils.js.map +1 -0
- package/dist/core/tools/todos.d.ts +35 -0
- package/dist/core/tools/todos.d.ts.map +1 -0
- package/dist/core/tools/todos.js +906 -0
- package/dist/core/tools/todos.js.map +1 -0
- package/dist/core/tools/tool-definition-wrapper.d.ts +14 -0
- package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -0
- package/dist/core/tools/tool-definition-wrapper.js +34 -0
- package/dist/core/tools/tool-definition-wrapper.js.map +1 -0
- package/dist/core/tools/truncate.d.ts +70 -0
- package/dist/core/tools/truncate.d.ts.map +1 -0
- package/dist/core/tools/truncate.js +205 -0
- package/dist/core/tools/truncate.js.map +1 -0
- package/dist/core/tools/write.d.ts +26 -0
- package/dist/core/tools/write.d.ts.map +1 -0
- package/dist/core/tools/write.js +212 -0
- package/dist/core/tools/write.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/main.d.ts +13 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +586 -0
- package/dist/main.js.map +1 -0
- package/dist/migrations.d.ts +33 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +281 -0
- package/dist/migrations.js.map +1 -0
- package/dist/modes/index.d.ts +9 -0
- package/dist/modes/index.d.ts.map +1 -0
- package/dist/modes/index.js +8 -0
- package/dist/modes/index.js.map +1 -0
- package/dist/modes/interactive/assets/clankolas.png +3 -0
- package/dist/modes/interactive/components/armin.d.ts +34 -0
- package/dist/modes/interactive/components/armin.d.ts.map +1 -0
- package/dist/modes/interactive/components/armin.js +329 -0
- package/dist/modes/interactive/components/armin.js.map +1 -0
- package/dist/modes/interactive/components/assistant-message.d.ts +20 -0
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -0
- package/dist/modes/interactive/components/assistant-message.js +116 -0
- package/dist/modes/interactive/components/assistant-message.js.map +1 -0
- package/dist/modes/interactive/components/bash-execution.d.ts +34 -0
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -0
- package/dist/modes/interactive/components/bash-execution.js +170 -0
- package/dist/modes/interactive/components/bash-execution.js.map +1 -0
- package/dist/modes/interactive/components/bordered-loader.d.ts +16 -0
- package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -0
- package/dist/modes/interactive/components/bordered-loader.js +51 -0
- package/dist/modes/interactive/components/bordered-loader.js.map +1 -0
- package/dist/modes/interactive/components/branch-summary-message.d.ts +16 -0
- package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -0
- package/dist/modes/interactive/components/branch-summary-message.js +42 -0
- package/dist/modes/interactive/components/branch-summary-message.js.map +1 -0
- package/dist/modes/interactive/components/compaction-summary-message.d.ts +16 -0
- package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -0
- package/dist/modes/interactive/components/compaction-summary-message.js +43 -0
- package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -0
- package/dist/modes/interactive/components/config-selector.d.ts +71 -0
- package/dist/modes/interactive/components/config-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/config-selector.js +496 -0
- package/dist/modes/interactive/components/config-selector.js.map +1 -0
- package/dist/modes/interactive/components/countdown-timer.d.ts +14 -0
- package/dist/modes/interactive/components/countdown-timer.d.ts.map +1 -0
- package/dist/modes/interactive/components/countdown-timer.js +28 -0
- package/dist/modes/interactive/components/countdown-timer.js.map +1 -0
- package/dist/modes/interactive/components/custom-editor.d.ts +29 -0
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -0
- package/dist/modes/interactive/components/custom-editor.js +113 -0
- package/dist/modes/interactive/components/custom-editor.js.map +1 -0
- package/dist/modes/interactive/components/custom-message.d.ts +20 -0
- package/dist/modes/interactive/components/custom-message.d.ts.map +1 -0
- package/dist/modes/interactive/components/custom-message.js +74 -0
- package/dist/modes/interactive/components/custom-message.js.map +1 -0
- package/dist/modes/interactive/components/daxnuts.d.ts +23 -0
- package/dist/modes/interactive/components/daxnuts.d.ts.map +1 -0
- package/dist/modes/interactive/components/daxnuts.js +138 -0
- package/dist/modes/interactive/components/daxnuts.js.map +1 -0
- package/dist/modes/interactive/components/diff.d.ts +12 -0
- package/dist/modes/interactive/components/diff.d.ts.map +1 -0
- package/dist/modes/interactive/components/diff.js +151 -0
- package/dist/modes/interactive/components/diff.js.map +1 -0
- package/dist/modes/interactive/components/dynamic-border.d.ts +15 -0
- package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -0
- package/dist/modes/interactive/components/dynamic-border.js +20 -0
- package/dist/modes/interactive/components/dynamic-border.js.map +1 -0
- package/dist/modes/interactive/components/earendil-announcement.d.ts +5 -0
- package/dist/modes/interactive/components/earendil-announcement.d.ts.map +1 -0
- package/dist/modes/interactive/components/earendil-announcement.js +40 -0
- package/dist/modes/interactive/components/earendil-announcement.js.map +1 -0
- package/dist/modes/interactive/components/extension-editor.d.ts +20 -0
- package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -0
- package/dist/modes/interactive/components/extension-editor.js +106 -0
- package/dist/modes/interactive/components/extension-editor.js.map +1 -0
- package/dist/modes/interactive/components/extension-input.d.ts +23 -0
- package/dist/modes/interactive/components/extension-input.d.ts.map +1 -0
- package/dist/modes/interactive/components/extension-input.js +55 -0
- package/dist/modes/interactive/components/extension-input.js.map +1 -0
- package/dist/modes/interactive/components/extension-selector.d.ts +26 -0
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/extension-selector.js +75 -0
- package/dist/modes/interactive/components/extension-selector.js.map +1 -0
- package/dist/modes/interactive/components/footer.d.ts +27 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -0
- package/dist/modes/interactive/components/footer.js +199 -0
- package/dist/modes/interactive/components/footer.js.map +1 -0
- package/dist/modes/interactive/components/index.d.ts +32 -0
- package/dist/modes/interactive/components/index.d.ts.map +1 -0
- package/dist/modes/interactive/components/index.js +33 -0
- package/dist/modes/interactive/components/index.js.map +1 -0
- package/dist/modes/interactive/components/keybinding-hints.d.ts +13 -0
- package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -0
- package/dist/modes/interactive/components/keybinding-hints.js +36 -0
- package/dist/modes/interactive/components/keybinding-hints.js.map +1 -0
- package/dist/modes/interactive/components/login-dialog.d.ts +46 -0
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -0
- package/dist/modes/interactive/components/login-dialog.js +158 -0
- package/dist/modes/interactive/components/login-dialog.js.map +1 -0
- package/dist/modes/interactive/components/model-selector.d.ts +47 -0
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/model-selector.js +266 -0
- package/dist/modes/interactive/components/model-selector.js.map +1 -0
- package/dist/modes/interactive/components/oauth-selector.d.ts +31 -0
- package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/oauth-selector.js +156 -0
- package/dist/modes/interactive/components/oauth-selector.js.map +1 -0
- package/dist/modes/interactive/components/scoped-models-selector.d.ts +42 -0
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/scoped-models-selector.js +286 -0
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -0
- package/dist/modes/interactive/components/session-selector-search.d.ts +23 -0
- package/dist/modes/interactive/components/session-selector-search.d.ts.map +1 -0
- package/dist/modes/interactive/components/session-selector-search.js +155 -0
- package/dist/modes/interactive/components/session-selector-search.js.map +1 -0
- package/dist/modes/interactive/components/session-selector.d.ts +96 -0
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/session-selector.js +836 -0
- package/dist/modes/interactive/components/session-selector.js.map +1 -0
- package/dist/modes/interactive/components/settings-selector.d.ts +67 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/settings-selector.js +371 -0
- package/dist/modes/interactive/components/settings-selector.js.map +1 -0
- package/dist/modes/interactive/components/show-images-selector.d.ts +10 -0
- package/dist/modes/interactive/components/show-images-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/show-images-selector.js +38 -0
- package/dist/modes/interactive/components/show-images-selector.js.map +1 -0
- package/dist/modes/interactive/components/skill-invocation-message.d.ts +17 -0
- package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -0
- package/dist/modes/interactive/components/skill-invocation-message.js +45 -0
- package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -0
- package/dist/modes/interactive/components/theme-selector.d.ts +11 -0
- package/dist/modes/interactive/components/theme-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/theme-selector.js +48 -0
- package/dist/modes/interactive/components/theme-selector.js.map +1 -0
- package/dist/modes/interactive/components/thinking-selector.d.ts +11 -0
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/thinking-selector.js +50 -0
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -0
- package/dist/modes/interactive/components/tool-execution.d.ts +63 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -0
- package/dist/modes/interactive/components/tool-execution.js +280 -0
- package/dist/modes/interactive/components/tool-execution.js.map +1 -0
- package/dist/modes/interactive/components/tree-selector.d.ts +89 -0
- package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/tree-selector.js +1079 -0
- package/dist/modes/interactive/components/tree-selector.js.map +1 -0
- package/dist/modes/interactive/components/user-message-selector.d.ts +30 -0
- package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/user-message-selector.js +111 -0
- package/dist/modes/interactive/components/user-message-selector.js.map +1 -0
- package/dist/modes/interactive/components/user-message.d.ts +10 -0
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -0
- package/dist/modes/interactive/components/user-message.js +28 -0
- package/dist/modes/interactive/components/user-message.js.map +1 -0
- package/dist/modes/interactive/components/visual-truncate.d.ts +24 -0
- package/dist/modes/interactive/components/visual-truncate.d.ts.map +1 -0
- package/dist/modes/interactive/components/visual-truncate.js +33 -0
- package/dist/modes/interactive/components/visual-truncate.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +369 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -0
- package/dist/modes/interactive/interactive-mode.js +4709 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -0
- package/dist/modes/interactive/theme/catppuccin-frappe.json +90 -0
- package/dist/modes/interactive/theme/catppuccin-latte.json +90 -0
- package/dist/modes/interactive/theme/catppuccin-macchiato.json +90 -0
- package/dist/modes/interactive/theme/catppuccin-mocha.json +90 -0
- package/dist/modes/interactive/theme/dark.json +85 -0
- package/dist/modes/interactive/theme/light.json +84 -0
- package/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/dist/modes/interactive/theme/theme.d.ts +81 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/dist/modes/interactive/theme/theme.js +970 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -0
- package/dist/modes/interactive/whimsical-messages.d.ts +5 -0
- package/dist/modes/interactive/whimsical-messages.d.ts.map +1 -0
- package/dist/modes/interactive/whimsical-messages.js +464 -0
- package/dist/modes/interactive/whimsical-messages.js.map +1 -0
- package/dist/modes/print-mode.d.ts +28 -0
- package/dist/modes/print-mode.d.ts.map +1 -0
- package/dist/modes/print-mode.js +131 -0
- package/dist/modes/print-mode.js.map +1 -0
- package/dist/modes/rpc/jsonl.d.ts +17 -0
- package/dist/modes/rpc/jsonl.d.ts.map +1 -0
- package/dist/modes/rpc/jsonl.js +49 -0
- package/dist/modes/rpc/jsonl.js.map +1 -0
- package/dist/modes/rpc/rpc-client.d.ts +224 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -0
- package/dist/modes/rpc/rpc-client.js +409 -0
- package/dist/modes/rpc/rpc-client.js.map +1 -0
- package/dist/modes/rpc/rpc-mode.d.ts +20 -0
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -0
- package/dist/modes/rpc/rpc-mode.js +601 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -0
- package/dist/modes/rpc/rpc-types.d.ts +419 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -0
- package/dist/modes/rpc/rpc-types.js +8 -0
- package/dist/modes/rpc/rpc-types.js.map +1 -0
- package/dist/package-manager-cli.d.ts +4 -0
- package/dist/package-manager-cli.d.ts.map +1 -0
- package/dist/package-manager-cli.js +460 -0
- package/dist/package-manager-cli.js.map +1 -0
- package/dist/utils/ansi.d.ts +2 -0
- package/dist/utils/ansi.d.ts.map +1 -0
- package/dist/utils/ansi.js +52 -0
- package/dist/utils/ansi.js.map +1 -0
- package/dist/utils/changelog.d.ts +21 -0
- package/dist/utils/changelog.d.ts.map +1 -0
- package/dist/utils/changelog.js +87 -0
- package/dist/utils/changelog.js.map +1 -0
- package/dist/utils/child-process.d.ts +12 -0
- package/dist/utils/child-process.d.ts.map +1 -0
- package/dist/utils/child-process.js +86 -0
- package/dist/utils/child-process.js.map +1 -0
- package/dist/utils/clipboard-image.d.ts +11 -0
- package/dist/utils/clipboard-image.d.ts.map +1 -0
- package/dist/utils/clipboard-image.js +245 -0
- package/dist/utils/clipboard-image.js.map +1 -0
- package/dist/utils/clipboard-native.d.ts +8 -0
- package/dist/utils/clipboard-native.d.ts.map +1 -0
- package/dist/utils/clipboard-native.js +14 -0
- package/dist/utils/clipboard-native.js.map +1 -0
- package/dist/utils/clipboard.d.ts +2 -0
- package/dist/utils/clipboard.d.ts.map +1 -0
- package/dist/utils/clipboard.js +117 -0
- package/dist/utils/clipboard.js.map +1 -0
- package/dist/utils/exif-orientation.d.ts +5 -0
- package/dist/utils/exif-orientation.d.ts.map +1 -0
- package/dist/utils/exif-orientation.js +158 -0
- package/dist/utils/exif-orientation.js.map +1 -0
- package/dist/utils/frontmatter.d.ts +8 -0
- package/dist/utils/frontmatter.d.ts.map +1 -0
- package/dist/utils/frontmatter.js +26 -0
- package/dist/utils/frontmatter.js.map +1 -0
- package/dist/utils/fs-watch.d.ts +5 -0
- package/dist/utils/fs-watch.d.ts.map +1 -0
- package/dist/utils/fs-watch.js +25 -0
- package/dist/utils/fs-watch.js.map +1 -0
- package/dist/utils/git.d.ts +26 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +163 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/html.d.ts +7 -0
- package/dist/utils/html.d.ts.map +1 -0
- package/dist/utils/html.js +40 -0
- package/dist/utils/html.js.map +1 -0
- package/dist/utils/image-convert.d.ts +9 -0
- package/dist/utils/image-convert.d.ts.map +1 -0
- package/dist/utils/image-convert.js +39 -0
- package/dist/utils/image-convert.js.map +1 -0
- package/dist/utils/image-resize.d.ts +36 -0
- package/dist/utils/image-resize.d.ts.map +1 -0
- package/dist/utils/image-resize.js +137 -0
- package/dist/utils/image-resize.js.map +1 -0
- package/dist/utils/mime.d.ts +3 -0
- package/dist/utils/mime.d.ts.map +1 -0
- package/dist/utils/mime.js +69 -0
- package/dist/utils/mime.js.map +1 -0
- package/dist/utils/paths.d.ts +16 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +50 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/photon.d.ts +21 -0
- package/dist/utils/photon.d.ts.map +1 -0
- package/dist/utils/photon.js +121 -0
- package/dist/utils/photon.js.map +1 -0
- package/dist/utils/pi-user-agent.d.ts +2 -0
- package/dist/utils/pi-user-agent.d.ts.map +1 -0
- package/dist/utils/pi-user-agent.js +5 -0
- package/dist/utils/pi-user-agent.js.map +1 -0
- package/dist/utils/shell.d.ts +30 -0
- package/dist/utils/shell.d.ts.map +1 -0
- package/dist/utils/shell.js +190 -0
- package/dist/utils/shell.js.map +1 -0
- package/dist/utils/sleep.d.ts +5 -0
- package/dist/utils/sleep.d.ts.map +1 -0
- package/dist/utils/sleep.js +17 -0
- package/dist/utils/sleep.js.map +1 -0
- package/dist/utils/syntax-highlight.d.ts +12 -0
- package/dist/utils/syntax-highlight.d.ts.map +1 -0
- package/dist/utils/syntax-highlight.js +118 -0
- package/dist/utils/syntax-highlight.js.map +1 -0
- package/dist/utils/tools-manager.d.ts +3 -0
- package/dist/utils/tools-manager.d.ts.map +1 -0
- package/dist/utils/tools-manager.js +325 -0
- package/dist/utils/tools-manager.js.map +1 -0
- package/dist/utils/version-check.d.ts +14 -0
- package/dist/utils/version-check.d.ts.map +1 -0
- package/dist/utils/version-check.js +76 -0
- package/dist/utils/version-check.js.map +1 -0
- package/docs/compaction.md +394 -0
- package/docs/custom-provider.md +646 -0
- package/docs/development.md +71 -0
- package/docs/docs.json +148 -0
- package/docs/extensions.md +2596 -0
- package/docs/images/doom-extension.png +0 -0
- package/docs/images/exy.png +3 -0
- package/docs/images/interactive-mode.png +0 -0
- package/docs/images/tree-view.png +0 -0
- package/docs/index.md +70 -0
- package/docs/json.md +82 -0
- package/docs/keybindings.md +197 -0
- package/docs/models.md +474 -0
- package/docs/packages.md +223 -0
- package/docs/prompt-templates.md +88 -0
- package/docs/providers.md +243 -0
- package/docs/quickstart.md +142 -0
- package/docs/rpc.md +1407 -0
- package/docs/sdk.md +1129 -0
- package/docs/session-format.md +412 -0
- package/docs/sessions.md +137 -0
- package/docs/settings.md +279 -0
- package/docs/shell-aliases.md +13 -0
- package/docs/skills.md +232 -0
- package/docs/terminal-setup.md +106 -0
- package/docs/termux.md +127 -0
- package/docs/themes.md +299 -0
- package/docs/tmux.md +61 -0
- package/docs/tui.md +918 -0
- package/docs/usage.md +277 -0
- package/docs/windows.md +17 -0
- package/examples/README.md +25 -0
- package/examples/extensions/README.md +208 -0
- package/examples/extensions/auto-commit-on-exit.ts +49 -0
- package/examples/extensions/bash-spawn-hook.ts +30 -0
- package/examples/extensions/bookmark.ts +50 -0
- package/examples/extensions/border-status-editor.ts +150 -0
- package/examples/extensions/built-in-tool-renderer.ts +249 -0
- package/examples/extensions/claude-rules.ts +86 -0
- package/examples/extensions/commands.ts +72 -0
- package/examples/extensions/confirm-destructive.ts +59 -0
- package/examples/extensions/custom-compaction.ts +127 -0
- package/examples/extensions/custom-footer.ts +64 -0
- package/examples/extensions/custom-header.ts +73 -0
- package/examples/extensions/custom-provider-anthropic/index.ts +604 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +24 -0
- package/examples/extensions/custom-provider-anthropic/package.json +19 -0
- package/examples/extensions/custom-provider-gitlab-duo/index.ts +349 -0
- package/examples/extensions/custom-provider-gitlab-duo/package.json +16 -0
- package/examples/extensions/custom-provider-gitlab-duo/test.ts +82 -0
- package/examples/extensions/dirty-repo-guard.ts +56 -0
- package/examples/extensions/doom-overlay/README.md +46 -0
- package/examples/extensions/doom-overlay/doom/build.sh +152 -0
- package/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +72 -0
- package/examples/extensions/doom-overlay/doom-component.ts +132 -0
- package/examples/extensions/doom-overlay/doom-engine.ts +173 -0
- package/examples/extensions/doom-overlay/doom-keys.ts +104 -0
- package/examples/extensions/doom-overlay/index.ts +74 -0
- package/examples/extensions/doom-overlay/wad-finder.ts +51 -0
- package/examples/extensions/dynamic-resources/SKILL.md +8 -0
- package/examples/extensions/dynamic-resources/dynamic.json +79 -0
- package/examples/extensions/dynamic-resources/dynamic.md +5 -0
- package/examples/extensions/dynamic-resources/index.ts +15 -0
- package/examples/extensions/dynamic-tools.ts +74 -0
- package/examples/extensions/event-bus.ts +43 -0
- package/examples/extensions/file-trigger.ts +41 -0
- package/examples/extensions/git-checkpoint.ts +53 -0
- package/examples/extensions/github-issue-autocomplete.ts +185 -0
- package/examples/extensions/handoff.ts +191 -0
- package/examples/extensions/hello.ts +26 -0
- package/examples/extensions/hidden-thinking-label.ts +53 -0
- package/examples/extensions/inline-bash.ts +94 -0
- package/examples/extensions/input-transform.ts +43 -0
- package/examples/extensions/interactive-shell.ts +196 -0
- package/examples/extensions/mac-system-theme.ts +47 -0
- package/examples/extensions/message-renderer.ts +59 -0
- package/examples/extensions/minimal-mode.ts +426 -0
- package/examples/extensions/modal-editor.ts +85 -0
- package/examples/extensions/model-status.ts +31 -0
- package/examples/extensions/notify.ts +55 -0
- package/examples/extensions/overlay-qa-tests.ts +1348 -0
- package/examples/extensions/overlay-test.ts +150 -0
- package/examples/extensions/permission-gate.ts +34 -0
- package/examples/extensions/pirate.ts +47 -0
- package/examples/extensions/plan-mode/README.md +65 -0
- package/examples/extensions/plan-mode/index.ts +340 -0
- package/examples/extensions/plan-mode/utils.ts +168 -0
- package/examples/extensions/preset.ts +430 -0
- package/examples/extensions/prompt-customizer.ts +97 -0
- package/examples/extensions/protected-paths.ts +30 -0
- package/examples/extensions/provider-payload.ts +18 -0
- package/examples/extensions/qna.ts +122 -0
- package/examples/extensions/question.ts +264 -0
- package/examples/extensions/questionnaire.ts +427 -0
- package/examples/extensions/rainbow-editor.ts +88 -0
- package/examples/extensions/reload-runtime.ts +37 -0
- package/examples/extensions/rpc-demo.ts +118 -0
- package/examples/extensions/sandbox/index.ts +321 -0
- package/examples/extensions/sandbox/package-lock.json +92 -0
- package/examples/extensions/sandbox/package.json +19 -0
- package/examples/extensions/send-user-message.ts +97 -0
- package/examples/extensions/session-name.ts +27 -0
- package/examples/extensions/shutdown-command.ts +63 -0
- package/examples/extensions/snake.ts +343 -0
- package/examples/extensions/space-invaders.ts +560 -0
- package/examples/extensions/ssh.ts +220 -0
- package/examples/extensions/status-line.ts +32 -0
- package/examples/extensions/structured-output.ts +65 -0
- package/examples/extensions/subagent/README.md +172 -0
- package/examples/extensions/subagent/agents/planner.md +37 -0
- package/examples/extensions/subagent/agents/reviewer.md +35 -0
- package/examples/extensions/subagent/agents/scout.md +50 -0
- package/examples/extensions/subagent/agents/worker.md +24 -0
- package/examples/extensions/subagent/agents.ts +126 -0
- package/examples/extensions/subagent/index.ts +987 -0
- package/examples/extensions/subagent/prompts/implement-and-review.md +10 -0
- package/examples/extensions/subagent/prompts/implement.md +10 -0
- package/examples/extensions/subagent/prompts/scout-and-plan.md +9 -0
- package/examples/extensions/summarize.ts +206 -0
- package/examples/extensions/system-prompt-header.ts +17 -0
- package/examples/extensions/tic-tac-toe.ts +1008 -0
- package/examples/extensions/timed-confirm.ts +70 -0
- package/examples/extensions/titlebar-spinner.ts +58 -0
- package/examples/extensions/todo.ts +297 -0
- package/examples/extensions/tool-override.ts +144 -0
- package/examples/extensions/tools.ts +141 -0
- package/examples/extensions/trigger-compact.ts +50 -0
- package/examples/extensions/truncated-tool.ts +195 -0
- package/examples/extensions/widget-placement.ts +9 -0
- package/examples/extensions/with-deps/index.ts +32 -0
- package/examples/extensions/with-deps/package-lock.json +31 -0
- package/examples/extensions/with-deps/package.json +22 -0
- package/examples/extensions/working-indicator.ts +123 -0
- package/examples/extensions/working-message-test.ts +25 -0
- package/examples/rpc-extension-ui.ts +632 -0
- package/examples/sdk/01-minimal.ts +26 -0
- package/examples/sdk/02-custom-model.ts +53 -0
- package/examples/sdk/03-custom-prompt.ts +75 -0
- package/examples/sdk/04-skills.ts +55 -0
- package/examples/sdk/05-tools.ts +48 -0
- package/examples/sdk/06-extensions.ts +99 -0
- package/examples/sdk/07-context-files.ts +47 -0
- package/examples/sdk/08-prompt-templates.ts +51 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +52 -0
- package/examples/sdk/10-settings.ts +53 -0
- package/examples/sdk/11-sessions.ts +52 -0
- package/examples/sdk/12-full-control.ts +77 -0
- package/examples/sdk/13-session-runtime.ts +67 -0
- package/examples/sdk/README.md +144 -0
- package/package.json +94 -19
- package/bin/atomic +0 -82
- /package/{LICENSE → dist/builtin/workflows/LICENSE} +0 -0
|
@@ -0,0 +1,4865 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Impeccable Live Variant Mode — Browser Script
|
|
3
|
+
*
|
|
4
|
+
* Injected into the user's page via <script src="http://localhost:PORT/live.js">.
|
|
5
|
+
* The server prepends window.__IMPECCABLE_TOKEN__ and window.__IMPECCABLE_PORT__
|
|
6
|
+
* before this code.
|
|
7
|
+
*
|
|
8
|
+
* UI: a single floating bar that morphs between three states —
|
|
9
|
+
* configure (pick action + go), generating (progressive dots), and cycling
|
|
10
|
+
* (prev/next + accept/discard). Feels like Spotlight, not a modal.
|
|
11
|
+
*/
|
|
12
|
+
(function () {
|
|
13
|
+
'use strict';
|
|
14
|
+
if (typeof window === 'undefined') return;
|
|
15
|
+
|
|
16
|
+
// Guard against double-init. Bun's HTML loader may process the <script> tag
|
|
17
|
+
// and create a bundled copy alongside the external load, or HMR may re-execute.
|
|
18
|
+
// Check BEFORE reading token/port to catch all cases.
|
|
19
|
+
if (window.__IMPECCABLE_LIVE_INIT__) return;
|
|
20
|
+
window.__IMPECCABLE_LIVE_INIT__ = true;
|
|
21
|
+
|
|
22
|
+
const TOKEN = window.__IMPECCABLE_TOKEN__;
|
|
23
|
+
const PORT = window.__IMPECCABLE_PORT__;
|
|
24
|
+
const LIVE_ORIGIN = (() => {
|
|
25
|
+
const script = document.currentScript;
|
|
26
|
+
if (script && script.src) return new URL(script.src, window.location.href).origin;
|
|
27
|
+
return window.location.origin;
|
|
28
|
+
})();
|
|
29
|
+
if (!TOKEN || !PORT) {
|
|
30
|
+
window.__IMPECCABLE_LIVE_INIT__ = false; // reset so the real load can init
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Design tokens
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
// Brand magenta is pinned to the site token (--color-accent in main.css)
|
|
39
|
+
// so Accept / knobs / cycle-dots match the site's accent, not a washed
|
|
40
|
+
// theme-adjusted one.
|
|
41
|
+
const C = {
|
|
42
|
+
brand: 'oklch(60% 0.25 350)',
|
|
43
|
+
brandHov: 'oklch(52% 0.25 350)',
|
|
44
|
+
brandSoft: 'oklch(60% 0.25 350 / 0.15)',
|
|
45
|
+
ink: 'oklch(15% 0.01 350)',
|
|
46
|
+
ash: 'oklch(55% 0 0)',
|
|
47
|
+
paper: 'oklch(98% 0.005 350 / 0.92)',
|
|
48
|
+
paperSolid:'oklch(98% 0.005 350)',
|
|
49
|
+
mist: 'oklch(90% 0.01 350 / 0.6)',
|
|
50
|
+
white: 'oklch(99% 0 0)',
|
|
51
|
+
};
|
|
52
|
+
const FONT = 'system-ui, -apple-system, sans-serif';
|
|
53
|
+
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
|
|
54
|
+
// z-index: detect overlays use 99999, so our UI must be above them
|
|
55
|
+
const Z = { highlight: 100001, bar: 100005, picker: 100007, toast: 100010 };
|
|
56
|
+
const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; // ease-out-quint
|
|
57
|
+
const PREFIX = 'impeccable-live';
|
|
58
|
+
const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({
|
|
59
|
+
prefix: PREFIX,
|
|
60
|
+
storage: localStorage,
|
|
61
|
+
idFactory: () => crypto.randomUUID().replace(/-/g, '').slice(0, 8),
|
|
62
|
+
});
|
|
63
|
+
if (!sessionState) {
|
|
64
|
+
console.error('[impeccable] live-browser-session.js was not loaded. Live mode cannot start safely.');
|
|
65
|
+
window.__IMPECCABLE_LIVE_INIT__ = false;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const HIGHLIGHT_TRANSITION =
|
|
69
|
+
'top 140ms ' + EASE +
|
|
70
|
+
', left 140ms ' + EASE +
|
|
71
|
+
', width 140ms ' + EASE +
|
|
72
|
+
', height 140ms ' + EASE +
|
|
73
|
+
', opacity 150ms ease';
|
|
74
|
+
const TOOLTIP_TRANSITION =
|
|
75
|
+
'top 140ms ' + EASE + ', left 140ms ' + EASE + ', opacity 150ms ease';
|
|
76
|
+
|
|
77
|
+
const SKIP_TAGS = new Set([
|
|
78
|
+
'html', 'head', 'body', 'script', 'style', 'link', 'meta', 'noscript', 'br', 'wbr',
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
// SVG icons stack above each chip label. All strokes use currentColor so the
|
|
82
|
+
// icon recolors to C.brand when its chip is selected. 20x20 render, 24-viewBox,
|
|
83
|
+
// 1.5 stroke — visually consistent with the Foundation grid on the homepage.
|
|
84
|
+
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
|
85
|
+
const ICONS = {
|
|
86
|
+
impeccable: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>`,
|
|
87
|
+
bolder: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>`,
|
|
88
|
+
quieter: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>`,
|
|
89
|
+
distill: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>`,
|
|
90
|
+
polish: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>`,
|
|
91
|
+
typeset: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>`,
|
|
92
|
+
colorize: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>`,
|
|
93
|
+
layout: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>`,
|
|
94
|
+
adapt: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>`,
|
|
95
|
+
animate: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>`,
|
|
96
|
+
delight: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>`,
|
|
97
|
+
overdrive: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>`,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const ACTIONS = [
|
|
101
|
+
{ value: 'impeccable', label: 'Freeform' },
|
|
102
|
+
{ value: 'bolder', label: 'Bolder' },
|
|
103
|
+
{ value: 'quieter', label: 'Quieter' },
|
|
104
|
+
{ value: 'distill', label: 'Distill' },
|
|
105
|
+
{ value: 'polish', label: 'Polish' },
|
|
106
|
+
{ value: 'typeset', label: 'Typeset' },
|
|
107
|
+
{ value: 'colorize', label: 'Colorize' },
|
|
108
|
+
{ value: 'layout', label: 'Layout' },
|
|
109
|
+
{ value: 'adapt', label: 'Adapt' },
|
|
110
|
+
{ value: 'animate', label: 'Animate' },
|
|
111
|
+
{ value: 'delight', label: 'Delight' },
|
|
112
|
+
{ value: 'overdrive', label: 'Overdrive' },
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// State
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
let state = 'IDLE';
|
|
120
|
+
let hoveredElement = null;
|
|
121
|
+
let selectedElement = null;
|
|
122
|
+
let currentSessionId = null;
|
|
123
|
+
let expectedVariants = 0;
|
|
124
|
+
let arrivedVariants = 0;
|
|
125
|
+
let visibleVariant = 0;
|
|
126
|
+
let variantObserver = null;
|
|
127
|
+
let hasProjectContext = false;
|
|
128
|
+
let selectedAction = 'impeccable';
|
|
129
|
+
let selectedCount = 3;
|
|
130
|
+
const browserOwner = sessionState.owner;
|
|
131
|
+
let checkpointTimer = null;
|
|
132
|
+
|
|
133
|
+
// Scroll lock — holds window.scrollY at a fixed value while the session is
|
|
134
|
+
// active, so HMR DOM patches and variant swaps can't drift the page. See
|
|
135
|
+
// startScrollLock / stopScrollLock below.
|
|
136
|
+
let scrollLockObserver = null;
|
|
137
|
+
let scrollLockTargetY = null;
|
|
138
|
+
let scrollLockRaf = null;
|
|
139
|
+
let scrollLockAbort = null;
|
|
140
|
+
|
|
141
|
+
// Dedicated key for scroll position — SEPARATE from LS_KEY so that
|
|
142
|
+
// saveSession's state updates don't clobber a carefully-captured scrollY.
|
|
143
|
+
// (Previously: saveSession wrote scrollY alongside state, so every call
|
|
144
|
+
// during resume overwrote the pre-reload value with whatever the browser
|
|
145
|
+
// had landed on, typically 0.)
|
|
146
|
+
function writeScrollY(y) { sessionState.writeScrollY(y); }
|
|
147
|
+
function readScrollY() { return sessionState.readScrollY(); }
|
|
148
|
+
function clearScrollY() { sessionState.clearScrollY(); }
|
|
149
|
+
|
|
150
|
+
// Pre-empt the browser: apply manual scroll restoration and jump to the
|
|
151
|
+
// saved scrollY at script-parse time. Retries on fonts.ready and load
|
|
152
|
+
// are essential: scrollTo(y) clamps to the current document.scrollHeight,
|
|
153
|
+
// which is often hundreds of pixels short of the final value until
|
|
154
|
+
// async-loaded fonts swap in and reflow.
|
|
155
|
+
try {
|
|
156
|
+
history.scrollRestoration = 'manual';
|
|
157
|
+
const savedY = readScrollY();
|
|
158
|
+
if (savedY != null) {
|
|
159
|
+
const apply = () => {
|
|
160
|
+
if (Math.abs(window.scrollY - savedY) > 0.5) {
|
|
161
|
+
console.log('[impeccable.scroll] early restore', { from: window.scrollY, to: savedY });
|
|
162
|
+
window.scrollTo(0, savedY);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
apply();
|
|
166
|
+
if (document.fonts?.ready) document.fonts.ready.then(apply).catch(() => {});
|
|
167
|
+
window.addEventListener('load', apply, { once: true });
|
|
168
|
+
}
|
|
169
|
+
} catch {}
|
|
170
|
+
|
|
171
|
+
// UI refs
|
|
172
|
+
let highlightEl = null;
|
|
173
|
+
let tooltipEl = null;
|
|
174
|
+
let barEl = null;
|
|
175
|
+
let pickerEl = null;
|
|
176
|
+
let toastEl = null;
|
|
177
|
+
let scrollRaf = null;
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Helpers
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
function own(el) {
|
|
184
|
+
return el && (el.id?.startsWith(PREFIX) || el.closest?.('[id^="' + PREFIX + '"]'));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function pickable(el) {
|
|
188
|
+
if (!el || el.nodeType !== 1) return false;
|
|
189
|
+
if (SKIP_TAGS.has(el.tagName.toLowerCase())) return false;
|
|
190
|
+
if (own(el)) return false;
|
|
191
|
+
const r = el.getBoundingClientRect();
|
|
192
|
+
return r.width >= 20 && r.height >= 20;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function desc(el) {
|
|
196
|
+
if (!el) return '';
|
|
197
|
+
let s = el.tagName.toLowerCase();
|
|
198
|
+
if (el.id) s += '#' + el.id;
|
|
199
|
+
else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.');
|
|
200
|
+
return s;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function id8() { return crypto.randomUUID().replace(/-/g, '').slice(0, 8); }
|
|
204
|
+
|
|
205
|
+
// Modal-aware chrome: keep our floating UI clickable inside Radix /
|
|
206
|
+
// Headless UI / vaul portals.
|
|
207
|
+
//
|
|
208
|
+
// Two host-page behaviors break us when the picked element lives inside a
|
|
209
|
+
// modal dialog:
|
|
210
|
+
//
|
|
211
|
+
// 1. Modal scroll-lock disables outside pointer events. Radix's
|
|
212
|
+
// `DismissableLayer` sets `document.body.style.pointerEvents = 'none'`
|
|
213
|
+
// while a modal is open and only restores `auto` on the layer. Our
|
|
214
|
+
// chrome inherits `none` from <body> and becomes unclickable.
|
|
215
|
+
// 2. The dialog's outside-interaction handler (Radix's
|
|
216
|
+
// `usePointerDownOutside`) listens at document level and dismisses
|
|
217
|
+
// the dialog whenever a `pointerdown` lands outside the layer node.
|
|
218
|
+
// Our chrome is a sibling of <body>, so Radix classifies our clicks
|
|
219
|
+
// as outside and tears the dialog down mid-task.
|
|
220
|
+
//
|
|
221
|
+
// We can't reliably re-parent our chrome into the dialog subtree (z-index
|
|
222
|
+
// stacking, scroll containers, theming all become host-page concerns), so
|
|
223
|
+
// we defang both behaviors at our root:
|
|
224
|
+
//
|
|
225
|
+
// - `pointer-events: auto !important` overrides the inherited `none`.
|
|
226
|
+
// - Stop `pointerdown` / `mousedown` propagation so the document-level
|
|
227
|
+
// dismiss listener never fires for our clicks.
|
|
228
|
+
// - Stop `focusin` propagation so any focus shifts inside our chrome
|
|
229
|
+
// don't read as "focus moved outside the dialog" to focus traps.
|
|
230
|
+
//
|
|
231
|
+
// Click events still bubble normally — only the early pointer/focus
|
|
232
|
+
// signals that drive outside-interaction detection are silenced.
|
|
233
|
+
function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) {
|
|
234
|
+
if (!rootEl) return;
|
|
235
|
+
if (setPointerEvents) {
|
|
236
|
+
rootEl.style.setProperty('pointer-events', 'auto', 'important');
|
|
237
|
+
}
|
|
238
|
+
const stop = (e) => e.stopPropagation();
|
|
239
|
+
rootEl.addEventListener('pointerdown', stop);
|
|
240
|
+
rootEl.addEventListener('mousedown', stop);
|
|
241
|
+
rootEl.addEventListener('focusin', stop);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// Highlight overlay
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
function initHighlight() {
|
|
249
|
+
highlightEl = document.createElement('div');
|
|
250
|
+
highlightEl.id = PREFIX + '-highlight';
|
|
251
|
+
Object.assign(highlightEl.style, {
|
|
252
|
+
position: 'fixed', top: '0', left: '0', width: '0', height: '0',
|
|
253
|
+
border: '2px solid ' + C.brand, borderRadius: '3px',
|
|
254
|
+
pointerEvents: 'none', zIndex: Z.highlight, boxSizing: 'border-box',
|
|
255
|
+
transition: HIGHLIGHT_TRANSITION,
|
|
256
|
+
display: 'none', opacity: '0',
|
|
257
|
+
});
|
|
258
|
+
document.body.appendChild(highlightEl);
|
|
259
|
+
|
|
260
|
+
tooltipEl = document.createElement('div');
|
|
261
|
+
tooltipEl.id = PREFIX + '-tooltip';
|
|
262
|
+
Object.assign(tooltipEl.style, {
|
|
263
|
+
position: 'fixed',
|
|
264
|
+
background: C.ink, color: C.white,
|
|
265
|
+
fontFamily: MONO, fontSize: '10px', fontWeight: '500',
|
|
266
|
+
padding: '2px 6px', borderRadius: '3px',
|
|
267
|
+
zIndex: Z.highlight + 1, pointerEvents: 'none',
|
|
268
|
+
whiteSpace: 'nowrap', display: 'none',
|
|
269
|
+
letterSpacing: '0.02em',
|
|
270
|
+
transition: TOOLTIP_TRANSITION,
|
|
271
|
+
});
|
|
272
|
+
document.body.appendChild(tooltipEl);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function showHighlight(el) {
|
|
276
|
+
if (!el || !highlightEl) return;
|
|
277
|
+
const r = el.getBoundingClientRect();
|
|
278
|
+
const top = (r.top - 2) + 'px', left = (r.left - 2) + 'px';
|
|
279
|
+
const width = (r.width + 4) + 'px', height = (r.height + 4) + 'px';
|
|
280
|
+
const tipTop = r.top - 20;
|
|
281
|
+
const tipY = (tipTop < 4 ? r.bottom + 4 : tipTop) + 'px';
|
|
282
|
+
const tipX = Math.max(4, r.left) + 'px';
|
|
283
|
+
tooltipEl.textContent = desc(el);
|
|
284
|
+
|
|
285
|
+
const hiWasHidden = highlightEl.style.display === 'none' || highlightEl.style.opacity === '0';
|
|
286
|
+
if (hiWasHidden) {
|
|
287
|
+
// Snap to first target without animating from (0,0), then fade in.
|
|
288
|
+
highlightEl.style.transition = 'none';
|
|
289
|
+
Object.assign(highlightEl.style, { top, left, width, height, display: 'block' });
|
|
290
|
+
tooltipEl.style.transition = 'none';
|
|
291
|
+
Object.assign(tooltipEl.style, { top: tipY, left: tipX, display: 'block' });
|
|
292
|
+
void highlightEl.offsetWidth;
|
|
293
|
+
highlightEl.style.transition = HIGHLIGHT_TRANSITION;
|
|
294
|
+
highlightEl.style.opacity = '1';
|
|
295
|
+
tooltipEl.style.transition = TOOLTIP_TRANSITION;
|
|
296
|
+
tooltipEl.style.opacity = '1';
|
|
297
|
+
} else {
|
|
298
|
+
Object.assign(highlightEl.style, { top, left, width, height, display: 'block', opacity: '1' });
|
|
299
|
+
Object.assign(tooltipEl.style, { top: tipY, left: tipX, display: 'block', opacity: '1' });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function hideHighlight() {
|
|
304
|
+
if (highlightEl) { highlightEl.style.opacity = '0'; highlightEl.style.display = 'none'; }
|
|
305
|
+
if (tooltipEl) { tooltipEl.style.opacity = '0'; tooltipEl.style.display = 'none'; }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// Annotation overlay (comment pins + magenta strokes)
|
|
310
|
+
//
|
|
311
|
+
// Active while state === 'CONFIGURING'. The overlay is a fixed-positioned
|
|
312
|
+
// sibling of <body> mirroring selectedElement's bounding rect. Click (no
|
|
313
|
+
// drag) drops a comment pin; drag paints a magenta SVG stroke. All coords
|
|
314
|
+
// are stored in element-local CSS px so they survive scroll / resize and
|
|
315
|
+
// correlate directly with the captured PNG.
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
const DRAG_THRESHOLD = 5; // px — below this, treat pointerup as a click
|
|
319
|
+
const PIN_DBL_CLICK_MS = 300; // two clicks on the same pin within this delete it
|
|
320
|
+
let annotOverlayEl = null;
|
|
321
|
+
let annotSvgEl = null;
|
|
322
|
+
let annotPinsEl = null;
|
|
323
|
+
let annotClearChipEl = null;
|
|
324
|
+
let annotState = { comments: [], strokes: [] };
|
|
325
|
+
let annotActive = false;
|
|
326
|
+
// `annotPointer` is either:
|
|
327
|
+
// { kind: 'new', x0, y0, moved, strokeEl, strokePoints } creating a stroke/pin
|
|
328
|
+
// { kind: 'pin', idx, startPointer, startPin, moved } dragging an existing pin
|
|
329
|
+
let annotPointer = null;
|
|
330
|
+
let annotEditing = null; // { idx, input, wrapEl }
|
|
331
|
+
let annotLastPinClick = { idx: -1, time: 0 }; // for click-click-to-delete
|
|
332
|
+
|
|
333
|
+
function initAnnotOverlay() {
|
|
334
|
+
annotOverlayEl = document.createElement('div');
|
|
335
|
+
annotOverlayEl.id = PREFIX + '-annot';
|
|
336
|
+
Object.assign(annotOverlayEl.style, {
|
|
337
|
+
position: 'fixed', top: '0', left: '0', width: '0', height: '0',
|
|
338
|
+
pointerEvents: 'auto', zIndex: Z.highlight + 2,
|
|
339
|
+
display: 'none', overflow: 'visible',
|
|
340
|
+
cursor: 'crosshair', touchAction: 'none',
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
annotSvgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
344
|
+
annotSvgEl.id = PREFIX + '-annot-svg';
|
|
345
|
+
Object.assign(annotSvgEl.style, {
|
|
346
|
+
position: 'absolute', top: '0', left: '0',
|
|
347
|
+
width: '100%', height: '100%',
|
|
348
|
+
// The SVG itself doesn't absorb clicks; individual hit-paths opt-in via
|
|
349
|
+
// pointer-events=stroke so gaps still fall through to the overlay.
|
|
350
|
+
pointerEvents: 'none', overflow: 'visible',
|
|
351
|
+
});
|
|
352
|
+
annotOverlayEl.appendChild(annotSvgEl);
|
|
353
|
+
|
|
354
|
+
annotPinsEl = document.createElement('div');
|
|
355
|
+
annotPinsEl.id = PREFIX + '-annot-pins';
|
|
356
|
+
Object.assign(annotPinsEl.style, {
|
|
357
|
+
position: 'absolute', inset: '0',
|
|
358
|
+
pointerEvents: 'none',
|
|
359
|
+
});
|
|
360
|
+
annotOverlayEl.appendChild(annotPinsEl);
|
|
361
|
+
|
|
362
|
+
annotClearChipEl = document.createElement('div');
|
|
363
|
+
annotClearChipEl.id = PREFIX + '-annot-clear';
|
|
364
|
+
annotClearChipEl.dataset.annotClear = 'true';
|
|
365
|
+
annotClearChipEl.textContent = 'Clear';
|
|
366
|
+
Object.assign(annotClearChipEl.style, {
|
|
367
|
+
position: 'absolute', top: '8px', right: '8px',
|
|
368
|
+
background: C.ink, color: C.white,
|
|
369
|
+
fontFamily: FONT, fontSize: '10px', fontWeight: '500',
|
|
370
|
+
letterSpacing: '0.08em', textTransform: 'uppercase',
|
|
371
|
+
padding: '5px 12px', borderRadius: '999px',
|
|
372
|
+
cursor: 'pointer', pointerEvents: 'auto',
|
|
373
|
+
display: 'none', userSelect: 'none',
|
|
374
|
+
boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
|
|
375
|
+
});
|
|
376
|
+
annotOverlayEl.appendChild(annotClearChipEl);
|
|
377
|
+
|
|
378
|
+
annotOverlayEl.addEventListener('pointerdown', onAnnotDown);
|
|
379
|
+
annotOverlayEl.addEventListener('pointermove', onAnnotMove);
|
|
380
|
+
annotOverlayEl.addEventListener('pointerup', onAnnotUp);
|
|
381
|
+
annotOverlayEl.addEventListener('pointercancel', onAnnotUp);
|
|
382
|
+
document.body.appendChild(annotOverlayEl);
|
|
383
|
+
// Modal-host friendliness: pointer-events is already 'auto' on this
|
|
384
|
+
// overlay; we only need to silence the host's outside-interaction
|
|
385
|
+
// listeners. Don't override pointer-events here (the overlay toggles
|
|
386
|
+
// visibility via display:none, which is fine).
|
|
387
|
+
defangOutsideHandlers(annotOverlayEl, { setPointerEvents: false });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function updateClearChip() {
|
|
391
|
+
if (!annotClearChipEl) return;
|
|
392
|
+
const hasAny = annotState.comments.length > 0 || annotState.strokes.length > 0;
|
|
393
|
+
annotClearChipEl.style.display = hasAny ? 'block' : 'none';
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function showAnnotOverlay(el) {
|
|
397
|
+
if (!annotOverlayEl || !el) return;
|
|
398
|
+
annotActive = true;
|
|
399
|
+
positionAnnotOverlay(el);
|
|
400
|
+
annotOverlayEl.style.display = 'block';
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function hideAnnotOverlay() {
|
|
404
|
+
annotActive = false;
|
|
405
|
+
if (annotOverlayEl) annotOverlayEl.style.display = 'none';
|
|
406
|
+
// Drop any in-progress edit without touching annotState — clearAnnotations
|
|
407
|
+
// (if the caller is exiting configure mode) handles state reset.
|
|
408
|
+
annotEditing = null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function positionAnnotOverlay(el) {
|
|
412
|
+
if (!annotOverlayEl || !el) return;
|
|
413
|
+
const r = el.getBoundingClientRect();
|
|
414
|
+
Object.assign(annotOverlayEl.style, {
|
|
415
|
+
top: r.top + 'px', left: r.left + 'px',
|
|
416
|
+
width: r.width + 'px', height: r.height + 'px',
|
|
417
|
+
});
|
|
418
|
+
annotSvgEl.setAttribute('viewBox', '0 0 ' + r.width + ' ' + r.height);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function clearAnnotations() {
|
|
422
|
+
annotState.comments = [];
|
|
423
|
+
annotState.strokes = [];
|
|
424
|
+
if (annotSvgEl) while (annotSvgEl.firstChild) annotSvgEl.removeChild(annotSvgEl.firstChild);
|
|
425
|
+
if (annotPinsEl) annotPinsEl.innerHTML = '';
|
|
426
|
+
annotPointer = null;
|
|
427
|
+
annotEditing = null;
|
|
428
|
+
annotLastPinClick = { idx: -1, time: 0 };
|
|
429
|
+
updateClearChip();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Rebuild the SVG layer. Each stroke gets a wider invisible hit path
|
|
433
|
+
// beneath the visible magenta path so clicks register on thin lines.
|
|
434
|
+
function redrawStrokes() {
|
|
435
|
+
while (annotSvgEl.firstChild) annotSvgEl.removeChild(annotSvgEl.firstChild);
|
|
436
|
+
annotState.strokes.forEach((s, idx) => {
|
|
437
|
+
const d = pointsToPath(s.points);
|
|
438
|
+
const hit = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
439
|
+
hit.setAttribute('d', d);
|
|
440
|
+
hit.setAttribute('stroke', 'transparent');
|
|
441
|
+
hit.setAttribute('stroke-width', '16');
|
|
442
|
+
hit.setAttribute('stroke-linecap', 'round');
|
|
443
|
+
hit.setAttribute('stroke-linejoin', 'round');
|
|
444
|
+
hit.setAttribute('fill', 'none');
|
|
445
|
+
hit.setAttribute('pointer-events', 'stroke');
|
|
446
|
+
hit.style.cursor = 'pointer';
|
|
447
|
+
hit.dataset.annotStroke = String(idx);
|
|
448
|
+
annotSvgEl.appendChild(hit);
|
|
449
|
+
const visible = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
450
|
+
visible.setAttribute('d', d);
|
|
451
|
+
visible.setAttribute('stroke', C.brand);
|
|
452
|
+
visible.setAttribute('stroke-width', '3');
|
|
453
|
+
visible.setAttribute('stroke-linecap', 'round');
|
|
454
|
+
visible.setAttribute('stroke-linejoin', 'round');
|
|
455
|
+
visible.setAttribute('fill', 'none');
|
|
456
|
+
visible.setAttribute('pointer-events', 'none');
|
|
457
|
+
annotSvgEl.appendChild(visible);
|
|
458
|
+
});
|
|
459
|
+
updateClearChip();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function localCoords(e) {
|
|
463
|
+
const rect = annotOverlayEl.getBoundingClientRect();
|
|
464
|
+
return { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function onAnnotDown(e) {
|
|
468
|
+
if (!annotActive) return;
|
|
469
|
+
|
|
470
|
+
// 1) Clear chip → wipe all annotations
|
|
471
|
+
if (e.target.closest?.('[data-annot-clear]')) {
|
|
472
|
+
if (annotEditing) annotEditing = null;
|
|
473
|
+
clearAnnotations();
|
|
474
|
+
renderAllPins();
|
|
475
|
+
redrawStrokes();
|
|
476
|
+
e.stopPropagation(); e.preventDefault();
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// 2) Stroke hit path → delete that stroke
|
|
481
|
+
const strokeHit = e.target.closest?.('[data-annot-stroke]');
|
|
482
|
+
if (strokeHit) {
|
|
483
|
+
const idx = parseInt(strokeHit.dataset.annotStroke, 10);
|
|
484
|
+
if (Number.isInteger(idx)) {
|
|
485
|
+
annotState.strokes.splice(idx, 1);
|
|
486
|
+
redrawStrokes();
|
|
487
|
+
}
|
|
488
|
+
e.stopPropagation(); e.preventDefault();
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// 3) Pin → drag, edit, or delete-on-double-click
|
|
493
|
+
const pinWrap = e.target.closest?.('[data-annot-pin]');
|
|
494
|
+
if (pinWrap) {
|
|
495
|
+
const idx = parseInt(pinWrap.dataset.annotPin, 10);
|
|
496
|
+
if (!Number.isInteger(idx)) return;
|
|
497
|
+
// Double-click (two pointerdowns on the same pin within window) → delete.
|
|
498
|
+
const now = Date.now();
|
|
499
|
+
if (annotLastPinClick.idx === idx && now - annotLastPinClick.time < PIN_DBL_CLICK_MS) {
|
|
500
|
+
if (annotEditing && annotEditing.idx === idx) annotEditing = null;
|
|
501
|
+
annotState.comments.splice(idx, 1);
|
|
502
|
+
annotLastPinClick = { idx: -1, time: 0 };
|
|
503
|
+
renderAllPins();
|
|
504
|
+
e.stopPropagation(); e.preventDefault();
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
annotLastPinClick = { idx, time: now };
|
|
508
|
+
// If editing a different pin, commit that edit before starting here.
|
|
509
|
+
if (annotEditing && annotEditing.idx !== idx) finalizeEditingPin();
|
|
510
|
+
// If already editing THIS pin and the user clicked the dot, let the
|
|
511
|
+
// input keep focus (don't start a drag — the click wasn't meant as one).
|
|
512
|
+
if (annotEditing && annotEditing.idx === idx) return;
|
|
513
|
+
const p = localCoords(e);
|
|
514
|
+
const pin = annotState.comments[idx];
|
|
515
|
+
annotPointer = {
|
|
516
|
+
kind: 'pin', idx,
|
|
517
|
+
startPointer: p,
|
|
518
|
+
startPin: { x: pin.x, y: pin.y },
|
|
519
|
+
moved: false,
|
|
520
|
+
};
|
|
521
|
+
try { annotOverlayEl.setPointerCapture(e.pointerId); } catch {}
|
|
522
|
+
e.stopPropagation(); e.preventDefault();
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// 4) Empty area → commit any open edit, then start new annotation
|
|
527
|
+
if (annotEditing) {
|
|
528
|
+
finalizeEditingPin();
|
|
529
|
+
e.stopPropagation(); e.preventDefault();
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const p = localCoords(e);
|
|
533
|
+
annotPointer = { kind: 'new', x0: p.x, y0: p.y, moved: false, strokeEl: null, strokePoints: null };
|
|
534
|
+
try { annotOverlayEl.setPointerCapture(e.pointerId); } catch {}
|
|
535
|
+
e.stopPropagation(); e.preventDefault();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function onAnnotMove(e) {
|
|
539
|
+
if (!annotActive || !annotPointer) return;
|
|
540
|
+
const p = localCoords(e);
|
|
541
|
+
|
|
542
|
+
if (annotPointer.kind === 'pin') {
|
|
543
|
+
const dx = p.x - annotPointer.startPointer.x;
|
|
544
|
+
const dy = p.y - annotPointer.startPointer.y;
|
|
545
|
+
if (!annotPointer.moved) {
|
|
546
|
+
if (Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
|
|
547
|
+
annotPointer.moved = true;
|
|
548
|
+
}
|
|
549
|
+
const pin = annotState.comments[annotPointer.idx];
|
|
550
|
+
if (!pin) { annotPointer = null; return; }
|
|
551
|
+
pin.x = annotPointer.startPin.x + dx;
|
|
552
|
+
pin.y = annotPointer.startPin.y + dy;
|
|
553
|
+
renderAllPins();
|
|
554
|
+
e.stopPropagation();
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// kind === 'new'
|
|
559
|
+
const dx = p.x - annotPointer.x0, dy = p.y - annotPointer.y0;
|
|
560
|
+
if (!annotPointer.moved) {
|
|
561
|
+
if (Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
|
|
562
|
+
annotPointer.moved = true;
|
|
563
|
+
const strokeEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
564
|
+
strokeEl.setAttribute('stroke', C.brand);
|
|
565
|
+
strokeEl.setAttribute('stroke-width', '3');
|
|
566
|
+
strokeEl.setAttribute('stroke-linecap', 'round');
|
|
567
|
+
strokeEl.setAttribute('stroke-linejoin', 'round');
|
|
568
|
+
strokeEl.setAttribute('fill', 'none');
|
|
569
|
+
strokeEl.setAttribute('pointer-events', 'none');
|
|
570
|
+
annotSvgEl.appendChild(strokeEl);
|
|
571
|
+
annotPointer.strokeEl = strokeEl;
|
|
572
|
+
annotPointer.strokePoints = [[annotPointer.x0, annotPointer.y0]];
|
|
573
|
+
}
|
|
574
|
+
annotPointer.strokePoints.push([p.x, p.y]);
|
|
575
|
+
annotPointer.strokeEl.setAttribute('d', pointsToPath(annotPointer.strokePoints));
|
|
576
|
+
e.stopPropagation();
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function onAnnotUp(e) {
|
|
580
|
+
if (!annotActive || !annotPointer) return;
|
|
581
|
+
|
|
582
|
+
if (annotPointer.kind === 'pin') {
|
|
583
|
+
const wasDrag = annotPointer.moved;
|
|
584
|
+
const idx = annotPointer.idx;
|
|
585
|
+
try { annotOverlayEl.releasePointerCapture(e.pointerId); } catch {}
|
|
586
|
+
annotPointer = null;
|
|
587
|
+
if (wasDrag) {
|
|
588
|
+
// A drag is an intentional reposition; a follow-up click shouldn't be
|
|
589
|
+
// interpreted as a double-click-to-delete.
|
|
590
|
+
annotLastPinClick = { idx: -1, time: 0 };
|
|
591
|
+
} else {
|
|
592
|
+
beginEditPin(idx);
|
|
593
|
+
}
|
|
594
|
+
e.stopPropagation();
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// kind === 'new'
|
|
599
|
+
const wasDrag = annotPointer.moved;
|
|
600
|
+
if (wasDrag) {
|
|
601
|
+
annotState.strokes.push({ points: annotPointer.strokePoints });
|
|
602
|
+
// Swap the temporary preview SVG path for the full render with hit paths.
|
|
603
|
+
redrawStrokes();
|
|
604
|
+
} else {
|
|
605
|
+
const idx = annotState.comments.length;
|
|
606
|
+
annotState.comments.push({ x: annotPointer.x0, y: annotPointer.y0, text: '' });
|
|
607
|
+
renderAllPins();
|
|
608
|
+
beginEditPin(idx);
|
|
609
|
+
}
|
|
610
|
+
try { annotOverlayEl.releasePointerCapture(e.pointerId); } catch {}
|
|
611
|
+
annotPointer = null;
|
|
612
|
+
e.stopPropagation();
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function pointsToPath(points) {
|
|
616
|
+
if (!points || points.length === 0) return '';
|
|
617
|
+
let d = 'M' + points[0][0].toFixed(1) + ' ' + points[0][1].toFixed(1);
|
|
618
|
+
for (let i = 1; i < points.length; i++) {
|
|
619
|
+
d += ' L' + points[i][0].toFixed(1) + ' ' + points[i][1].toFixed(1);
|
|
620
|
+
}
|
|
621
|
+
return d;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function renderAllPins() {
|
|
625
|
+
annotPinsEl.innerHTML = '';
|
|
626
|
+
annotState.comments.forEach((c, idx) => {
|
|
627
|
+
annotPinsEl.appendChild(buildPinElement(c, idx));
|
|
628
|
+
});
|
|
629
|
+
updateClearChip();
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function buildPinElement(comment, idx) {
|
|
633
|
+
const interactive = idx >= 0;
|
|
634
|
+
const wrap = document.createElement('div');
|
|
635
|
+
if (interactive) wrap.dataset.annotPin = String(idx);
|
|
636
|
+
Object.assign(wrap.style, {
|
|
637
|
+
position: 'absolute',
|
|
638
|
+
left: (comment.x - 7) + 'px', top: (comment.y - 7) + 'px',
|
|
639
|
+
pointerEvents: interactive ? 'auto' : 'none',
|
|
640
|
+
display: 'flex', alignItems: 'flex-start', gap: '6px',
|
|
641
|
+
cursor: interactive ? 'grab' : 'default',
|
|
642
|
+
touchAction: 'none',
|
|
643
|
+
});
|
|
644
|
+
const dot = document.createElement('div');
|
|
645
|
+
Object.assign(dot.style, {
|
|
646
|
+
width: '14px', height: '14px', borderRadius: '50%',
|
|
647
|
+
background: C.brand, border: '2px solid ' + C.white,
|
|
648
|
+
boxShadow: '0 1px 3px rgba(0,0,0,0.25)',
|
|
649
|
+
flexShrink: '0',
|
|
650
|
+
});
|
|
651
|
+
wrap.appendChild(dot);
|
|
652
|
+
|
|
653
|
+
if (comment.text) {
|
|
654
|
+
const bubble = document.createElement('div');
|
|
655
|
+
bubble.textContent = comment.text;
|
|
656
|
+
Object.assign(bubble.style, {
|
|
657
|
+
background: C.ink, color: C.white,
|
|
658
|
+
fontFamily: FONT, fontSize: '12px', lineHeight: '1.4',
|
|
659
|
+
padding: '4px 8px', borderRadius: '3px',
|
|
660
|
+
marginTop: '-2px', maxWidth: '220px',
|
|
661
|
+
pointerEvents: 'none', whiteSpace: 'pre-wrap',
|
|
662
|
+
wordBreak: 'break-word',
|
|
663
|
+
});
|
|
664
|
+
wrap.appendChild(bubble);
|
|
665
|
+
}
|
|
666
|
+
return wrap;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function beginEditPin(idx) {
|
|
670
|
+
const wrapEl = annotPinsEl.querySelector('[data-annot-pin="' + idx + '"]');
|
|
671
|
+
if (!wrapEl) return;
|
|
672
|
+
// Strip any existing bubble (but keep the dot)
|
|
673
|
+
wrapEl.querySelectorAll('div:not(:first-child)').forEach(n => n.remove());
|
|
674
|
+
const input = document.createElement('input');
|
|
675
|
+
input.type = 'text';
|
|
676
|
+
input.placeholder = 'Note…';
|
|
677
|
+
Object.assign(input.style, {
|
|
678
|
+
background: C.ink, color: C.white,
|
|
679
|
+
fontFamily: FONT, fontSize: '12px', lineHeight: '1.4',
|
|
680
|
+
padding: '4px 8px', borderRadius: '3px',
|
|
681
|
+
border: '1px solid ' + C.brand,
|
|
682
|
+
outline: 'none', marginTop: '-2px',
|
|
683
|
+
width: '220px', pointerEvents: 'auto',
|
|
684
|
+
});
|
|
685
|
+
const originalText = annotState.comments[idx].text || '';
|
|
686
|
+
input.value = originalText;
|
|
687
|
+
wrapEl.appendChild(input);
|
|
688
|
+
annotEditing = { idx, input, wrapEl, originalText };
|
|
689
|
+
input.addEventListener('keydown', onAnnotInputKey, true);
|
|
690
|
+
input.addEventListener('blur', () => {
|
|
691
|
+
// Fires on both focus-loss and programmatic blur; commit unless we
|
|
692
|
+
// already handled it.
|
|
693
|
+
if (annotEditing && annotEditing.input === input) finalizeEditingPin();
|
|
694
|
+
});
|
|
695
|
+
// Stop clicks/pointerdowns inside the input from bubbling to the overlay
|
|
696
|
+
['pointerdown', 'click'].forEach(ev => {
|
|
697
|
+
input.addEventListener(ev, e => e.stopPropagation());
|
|
698
|
+
});
|
|
699
|
+
setTimeout(() => input.focus(), 0);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function onAnnotInputKey(e) {
|
|
703
|
+
if (e.key === 'Enter') {
|
|
704
|
+
e.preventDefault(); e.stopPropagation();
|
|
705
|
+
finalizeEditingPin();
|
|
706
|
+
} else if (e.key === 'Escape') {
|
|
707
|
+
e.preventDefault(); e.stopPropagation();
|
|
708
|
+
cancelEditingPin();
|
|
709
|
+
} else {
|
|
710
|
+
// Keep arrows / backspace from hitting global handlers
|
|
711
|
+
e.stopPropagation();
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function finalizeEditingPin() {
|
|
716
|
+
if (!annotEditing) return;
|
|
717
|
+
const { idx, input } = annotEditing;
|
|
718
|
+
const text = input.value.trim();
|
|
719
|
+
annotEditing = null;
|
|
720
|
+
if (text) annotState.comments[idx].text = text;
|
|
721
|
+
else annotState.comments.splice(idx, 1);
|
|
722
|
+
renderAllPins();
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function cancelEditingPin() {
|
|
726
|
+
if (!annotEditing) return;
|
|
727
|
+
const { idx, originalText } = annotEditing;
|
|
728
|
+
annotEditing = null;
|
|
729
|
+
// If the pin had text before this edit, revert to it. If it was a
|
|
730
|
+
// just-created empty pin, Escape removes it.
|
|
731
|
+
if (originalText) {
|
|
732
|
+
annotState.comments[idx].text = originalText;
|
|
733
|
+
} else {
|
|
734
|
+
annotState.comments.splice(idx, 1);
|
|
735
|
+
}
|
|
736
|
+
renderAllPins();
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Build a detached annotation subtree suitable for injection into the clone
|
|
740
|
+
// modern-screenshot creates. Coordinates are element-local so this slots
|
|
741
|
+
// straight into an element that's been made position:relative. Takes an
|
|
742
|
+
// explicit snapshot so it works after annotState has been cleared.
|
|
743
|
+
function buildAnnotationsForCapture(rect, snapshot) {
|
|
744
|
+
const comments = snapshot ? snapshot.comments : annotState.comments;
|
|
745
|
+
const strokes = snapshot ? snapshot.strokes : annotState.strokes;
|
|
746
|
+
if (comments.length === 0 && strokes.length === 0) return null;
|
|
747
|
+
const wrap = document.createElement('div');
|
|
748
|
+
Object.assign(wrap.style, {
|
|
749
|
+
position: 'absolute', top: '0', left: '0',
|
|
750
|
+
width: rect.width + 'px', height: rect.height + 'px',
|
|
751
|
+
pointerEvents: 'none', overflow: 'visible',
|
|
752
|
+
});
|
|
753
|
+
if (strokes.length > 0) {
|
|
754
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
755
|
+
svg.setAttribute('viewBox', '0 0 ' + rect.width + ' ' + rect.height);
|
|
756
|
+
Object.assign(svg.style, {
|
|
757
|
+
position: 'absolute', top: '0', left: '0',
|
|
758
|
+
width: '100%', height: '100%', overflow: 'visible',
|
|
759
|
+
});
|
|
760
|
+
for (const s of strokes) {
|
|
761
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
762
|
+
path.setAttribute('stroke', C.brand);
|
|
763
|
+
path.setAttribute('stroke-width', '3');
|
|
764
|
+
path.setAttribute('stroke-linecap', 'round');
|
|
765
|
+
path.setAttribute('stroke-linejoin', 'round');
|
|
766
|
+
path.setAttribute('fill', 'none');
|
|
767
|
+
path.setAttribute('d', pointsToPath(s.points));
|
|
768
|
+
svg.appendChild(path);
|
|
769
|
+
}
|
|
770
|
+
wrap.appendChild(svg);
|
|
771
|
+
}
|
|
772
|
+
for (const c of comments) {
|
|
773
|
+
// idx=-1 means non-interactive; pointerEvents stay off in the clone
|
|
774
|
+
wrap.appendChild(buildPinElement(c, -1));
|
|
775
|
+
}
|
|
776
|
+
return wrap;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// ---------------------------------------------------------------------------
|
|
780
|
+
// Element context extraction
|
|
781
|
+
// ---------------------------------------------------------------------------
|
|
782
|
+
|
|
783
|
+
function extractContext(el) {
|
|
784
|
+
const cs = getComputedStyle(el);
|
|
785
|
+
const r = el.getBoundingClientRect();
|
|
786
|
+
const props = {};
|
|
787
|
+
for (const sheet of document.styleSheets) {
|
|
788
|
+
try {
|
|
789
|
+
for (const rule of sheet.cssRules) {
|
|
790
|
+
if (rule.style) for (let i = 0; i < rule.style.length; i++) {
|
|
791
|
+
const p = rule.style[i];
|
|
792
|
+
if (p.startsWith('--') && !props[p]) {
|
|
793
|
+
const v = cs.getPropertyValue(p).trim();
|
|
794
|
+
if (v) props[p] = v;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
} catch { /* cross-origin */ }
|
|
799
|
+
}
|
|
800
|
+
return {
|
|
801
|
+
tagName: el.tagName.toLowerCase(), id: el.id || null,
|
|
802
|
+
classes: [...el.classList],
|
|
803
|
+
textContent: (el.textContent || '').slice(0, 500),
|
|
804
|
+
outerHTML: el.outerHTML.slice(0, 10000),
|
|
805
|
+
computedStyles: {
|
|
806
|
+
'font-family': cs.fontFamily, 'font-size': cs.fontSize,
|
|
807
|
+
'font-weight': cs.fontWeight, 'line-height': cs.lineHeight,
|
|
808
|
+
'color': cs.color, 'background': cs.background,
|
|
809
|
+
'background-color': cs.backgroundColor,
|
|
810
|
+
'padding': cs.padding, 'margin': cs.margin,
|
|
811
|
+
'display': cs.display, 'position': cs.position,
|
|
812
|
+
'gap': cs.gap, 'border-radius': cs.borderRadius,
|
|
813
|
+
'box-shadow': cs.boxShadow,
|
|
814
|
+
},
|
|
815
|
+
cssCustomProperties: props,
|
|
816
|
+
parentContext: el.parentElement
|
|
817
|
+
? '<' + el.parentElement.tagName.toLowerCase()
|
|
818
|
+
+ (el.parentElement.id ? ' id="' + el.parentElement.id + '"' : '')
|
|
819
|
+
+ (el.parentElement.className ? ' class="' + el.parentElement.className + '"' : '')
|
|
820
|
+
+ '>'
|
|
821
|
+
: null,
|
|
822
|
+
boundingRect: { width: Math.round(r.width), height: Math.round(r.height) },
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// ---------------------------------------------------------------------------
|
|
827
|
+
// The Bar — one floating element, three modes
|
|
828
|
+
// ---------------------------------------------------------------------------
|
|
829
|
+
|
|
830
|
+
// Contextual-bar palette. Cached at init so every build*Row reads a
|
|
831
|
+
// consistent set of colors; detectPageTheme runs once rather than on every
|
|
832
|
+
// phase transition.
|
|
833
|
+
let BP = null;
|
|
834
|
+
|
|
835
|
+
// Bar shadow variants. The default projects down + subtle around. When
|
|
836
|
+
// the Tune popover opens below the bar, a downward shadow lands on the
|
|
837
|
+
// dark popover and reads as a bright ghost line. We swap to UP-only while
|
|
838
|
+
// tune is open below so the popover's top edge is clean.
|
|
839
|
+
const BAR_SHADOW_DEFAULT = '0 4px 20px oklch(0% 0 0 / 0.08), 0 1px 3px oklch(0% 0 0 / 0.06)';
|
|
840
|
+
const BAR_SHADOW_UP = '0 -4px 20px oklch(0% 0 0 / 0.08), 0 -1px 3px oklch(0% 0 0 / 0.06)';
|
|
841
|
+
const BAR_SHADOW_DOWN = BAR_SHADOW_DEFAULT;
|
|
842
|
+
|
|
843
|
+
function initBar() {
|
|
844
|
+
BP = barPaletteForTheme(detectPageTheme());
|
|
845
|
+
barEl = document.createElement('div');
|
|
846
|
+
barEl.id = PREFIX + '-bar';
|
|
847
|
+
Object.assign(barEl.style, {
|
|
848
|
+
position: 'fixed', zIndex: Z.bar,
|
|
849
|
+
display: 'none', opacity: '0',
|
|
850
|
+
transform: 'translateY(6px)',
|
|
851
|
+
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
|
852
|
+
background: BP.surface,
|
|
853
|
+
backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)',
|
|
854
|
+
border: '1px solid ' + BP.hairline,
|
|
855
|
+
borderRadius: '10px',
|
|
856
|
+
boxShadow: BAR_SHADOW_DEFAULT,
|
|
857
|
+
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
|
858
|
+
fontFamily: FONT, fontSize: '13px', color: BP.text,
|
|
859
|
+
padding: '6px',
|
|
860
|
+
maxWidth: '520px', minWidth: '320px',
|
|
861
|
+
});
|
|
862
|
+
document.body.appendChild(barEl);
|
|
863
|
+
defangOutsideHandlers(barEl);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function positionBar() {
|
|
867
|
+
if (!barEl || !selectedElement) return;
|
|
868
|
+
const r = selectedElement.getBoundingClientRect();
|
|
869
|
+
const barH = barEl.offsetHeight || 44;
|
|
870
|
+
const barW = barEl.offsetWidth || 380;
|
|
871
|
+
const GLOBAL_BAR_RESERVE = 64; // global bar height + bottom margin + breathing room
|
|
872
|
+
const GAP = 8;
|
|
873
|
+
|
|
874
|
+
// Prefer below the element; fall back to above; if neither fits (element
|
|
875
|
+
// taller than viewport), pin to a stable viewport anchor so the bar
|
|
876
|
+
// doesn't teleport between top and bottom as the user scrolls.
|
|
877
|
+
let top;
|
|
878
|
+
const belowTop = r.bottom + GAP;
|
|
879
|
+
const aboveTop = r.top - barH - GAP;
|
|
880
|
+
if (belowTop + barH + GAP <= window.innerHeight - GLOBAL_BAR_RESERVE) {
|
|
881
|
+
top = belowTop;
|
|
882
|
+
} else if (aboveTop >= GAP) {
|
|
883
|
+
top = aboveTop;
|
|
884
|
+
} else {
|
|
885
|
+
top = window.innerHeight - barH - GLOBAL_BAR_RESERVE;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
let left = r.left + (r.width - barW) / 2;
|
|
889
|
+
if (left < GAP) left = GAP;
|
|
890
|
+
if (left + barW > window.innerWidth - GAP) left = window.innerWidth - barW - GAP;
|
|
891
|
+
Object.assign(barEl.style, { top: top + 'px', left: left + 'px' });
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function showBar(mode) {
|
|
895
|
+
barEl.innerHTML = '';
|
|
896
|
+
if (mode === 'configure') barEl.appendChild(buildConfigureRow());
|
|
897
|
+
else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
|
|
898
|
+
else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
|
|
899
|
+
barEl.style.display = 'block';
|
|
900
|
+
positionBar();
|
|
901
|
+
requestAnimationFrame(() => {
|
|
902
|
+
barEl.style.opacity = '1';
|
|
903
|
+
barEl.style.transform = 'translateY(0)';
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function hideBar() {
|
|
908
|
+
if (!barEl) return;
|
|
909
|
+
barEl.style.opacity = '0';
|
|
910
|
+
barEl.style.transform = 'translateY(6px)';
|
|
911
|
+
setTimeout(() => { if (barEl) barEl.style.display = 'none'; }, 250);
|
|
912
|
+
hideActionPicker();
|
|
913
|
+
closeTunePopover();
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function updateBarContent(mode) {
|
|
917
|
+
if (!barEl || barEl.style.display === 'none') return;
|
|
918
|
+
barEl.innerHTML = '';
|
|
919
|
+
// Reset bar styling to the theme-aware palette
|
|
920
|
+
barEl.style.background = BP.surface;
|
|
921
|
+
barEl.style.border = '1px solid ' + BP.hairline;
|
|
922
|
+
if (mode === 'configure') barEl.appendChild(buildConfigureRow());
|
|
923
|
+
else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
|
|
924
|
+
else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
|
|
925
|
+
else if (mode === 'saving') barEl.appendChild(buildSavingRow());
|
|
926
|
+
else if (mode === 'confirmed') {
|
|
927
|
+
barEl.appendChild(buildConfirmedRow());
|
|
928
|
+
barEl.style.background = 'oklch(95% 0.05 145)';
|
|
929
|
+
barEl.style.border = '1px solid oklch(75% 0.12 145 / 0.4)';
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// --- Configure row ---
|
|
934
|
+
|
|
935
|
+
function buildConfigureRow() {
|
|
936
|
+
const row = el('div', {
|
|
937
|
+
display: 'flex', alignItems: 'center', gap: '4px',
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
// Action pill
|
|
941
|
+
const pill = el('button', {
|
|
942
|
+
display: 'inline-flex', alignItems: 'center', gap: '4px',
|
|
943
|
+
padding: '5px 10px', borderRadius: '6px',
|
|
944
|
+
background: BP.mark, color: BP.markText,
|
|
945
|
+
fontFamily: FONT, fontSize: '12px', fontWeight: '500',
|
|
946
|
+
border: 'none', cursor: 'pointer',
|
|
947
|
+
transition: 'background 0.12s ease, transform 0.1s ease',
|
|
948
|
+
whiteSpace: 'nowrap', flexShrink: '0',
|
|
949
|
+
});
|
|
950
|
+
pill.textContent = actionLabel() + ' \u25BE';
|
|
951
|
+
pill.addEventListener('mouseenter', () => pill.style.background = BP.accent);
|
|
952
|
+
pill.addEventListener('mouseleave', () => pill.style.background = BP.mark);
|
|
953
|
+
pill.addEventListener('mousedown', () => pill.style.transform = 'scale(0.97)');
|
|
954
|
+
pill.addEventListener('mouseup', () => pill.style.transform = 'scale(1)');
|
|
955
|
+
pill.addEventListener('click', (e) => { e.stopPropagation(); toggleActionPicker(); });
|
|
956
|
+
row.appendChild(pill);
|
|
957
|
+
|
|
958
|
+
// Freeform input. Focus state shows an accent-colored border only —
|
|
959
|
+
// an earlier version tinted the background with `BP.accentSoft`, which
|
|
960
|
+
// composited against the dark bar surface to a murky purple where the
|
|
961
|
+
// browser's default placeholder gray was unreadable. Placeholder color
|
|
962
|
+
// is set explicitly via a one-shot stylesheet keyed off this input's id
|
|
963
|
+
// so it picks up the bar's `textDim` token in both themes.
|
|
964
|
+
const input = document.createElement('input');
|
|
965
|
+
input.id = PREFIX + '-input';
|
|
966
|
+
input.type = 'text';
|
|
967
|
+
input.placeholder = selectedAction === 'impeccable' ? 'describe what you want...' : 'refine further (optional)...';
|
|
968
|
+
Object.assign(input.style, {
|
|
969
|
+
flex: '1', minWidth: '0',
|
|
970
|
+
padding: '5px 8px', borderRadius: '6px',
|
|
971
|
+
border: '1px solid transparent', background: 'transparent',
|
|
972
|
+
fontFamily: FONT, fontSize: '12px', color: BP.text,
|
|
973
|
+
outline: 'none',
|
|
974
|
+
transition: 'border-color 0.15s ease',
|
|
975
|
+
});
|
|
976
|
+
if (!document.getElementById(PREFIX + '-input-style')) {
|
|
977
|
+
const s = document.createElement('style');
|
|
978
|
+
s.id = PREFIX + '-input-style';
|
|
979
|
+
s.textContent =
|
|
980
|
+
'#' + PREFIX + '-input::placeholder { color: ' + BP.textDim + '; opacity: 1; }';
|
|
981
|
+
document.head.appendChild(s);
|
|
982
|
+
}
|
|
983
|
+
input.addEventListener('focus', () => {
|
|
984
|
+
input.style.borderColor = BP.accent;
|
|
985
|
+
});
|
|
986
|
+
input.addEventListener('blur', () => {
|
|
987
|
+
input.style.borderColor = 'transparent';
|
|
988
|
+
});
|
|
989
|
+
input.addEventListener('keydown', (e) => {
|
|
990
|
+
if (e.key === 'Enter') { e.stopPropagation(); e.preventDefault(); handleGo(); return; }
|
|
991
|
+
if (e.key === 'Escape') { e.stopPropagation(); e.preventDefault(); input.blur(); hideBar(); state = 'PICKING'; return; }
|
|
992
|
+
// Let arrow keys pass through to the element picker when the input is empty
|
|
993
|
+
if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && !input.value) return;
|
|
994
|
+
e.stopPropagation();
|
|
995
|
+
});
|
|
996
|
+
row.appendChild(input);
|
|
997
|
+
|
|
998
|
+
// Variant count toggle
|
|
999
|
+
const count = el('button', {
|
|
1000
|
+
padding: '4px 6px', borderRadius: '5px',
|
|
1001
|
+
border: '1px solid ' + BP.hairline, background: 'transparent',
|
|
1002
|
+
fontFamily: MONO, fontSize: '11px', fontWeight: '600',
|
|
1003
|
+
color: BP.textDim, cursor: 'pointer',
|
|
1004
|
+
transition: 'color 0.12s ease, border-color 0.12s ease',
|
|
1005
|
+
flexShrink: '0', whiteSpace: 'nowrap',
|
|
1006
|
+
});
|
|
1007
|
+
count.textContent = '\u00D7' + selectedCount;
|
|
1008
|
+
count.title = 'Variants: click to change';
|
|
1009
|
+
count.addEventListener('mouseenter', () => { count.style.color = BP.text; count.style.borderColor = BP.text; });
|
|
1010
|
+
count.addEventListener('mouseleave', () => { count.style.color = BP.textDim; count.style.borderColor = BP.hairline; });
|
|
1011
|
+
count.addEventListener('click', (e) => {
|
|
1012
|
+
e.stopPropagation();
|
|
1013
|
+
selectedCount = selectedCount >= 4 ? 2 : selectedCount + 1;
|
|
1014
|
+
count.textContent = '\u00D7' + selectedCount;
|
|
1015
|
+
});
|
|
1016
|
+
row.appendChild(count);
|
|
1017
|
+
|
|
1018
|
+
// Go button
|
|
1019
|
+
const go = el('button', {
|
|
1020
|
+
padding: '5px 12px', borderRadius: '6px',
|
|
1021
|
+
border: 'none', background: BP.accent, color: BP.mark,
|
|
1022
|
+
fontFamily: FONT, fontSize: '12px', fontWeight: '600',
|
|
1023
|
+
cursor: 'pointer',
|
|
1024
|
+
transition: 'filter 0.12s ease, transform 0.1s ease',
|
|
1025
|
+
flexShrink: '0', whiteSpace: 'nowrap',
|
|
1026
|
+
});
|
|
1027
|
+
go.textContent = 'Go \u2192';
|
|
1028
|
+
go.addEventListener('mouseenter', () => go.style.filter = 'brightness(1.1)');
|
|
1029
|
+
go.addEventListener('mouseleave', () => go.style.filter = 'none');
|
|
1030
|
+
go.addEventListener('mousedown', () => go.style.transform = 'scale(0.97)');
|
|
1031
|
+
go.addEventListener('mouseup', () => go.style.transform = 'scale(1)');
|
|
1032
|
+
go.addEventListener('click', (e) => { e.stopPropagation(); handleGo(); });
|
|
1033
|
+
row.appendChild(go);
|
|
1034
|
+
|
|
1035
|
+
// Auto-focus input after a beat
|
|
1036
|
+
setTimeout(() => input.focus(), 60);
|
|
1037
|
+
return row;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// --- Generating row ---
|
|
1041
|
+
|
|
1042
|
+
function buildGeneratingRow() {
|
|
1043
|
+
const row = el('div', {
|
|
1044
|
+
display: 'flex', alignItems: 'center', gap: '8px',
|
|
1045
|
+
padding: '2px 4px',
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
// Action label
|
|
1049
|
+
const label = el('span', {
|
|
1050
|
+
fontWeight: '600', fontSize: '12px', color: BP.text,
|
|
1051
|
+
flexShrink: '0', whiteSpace: 'nowrap',
|
|
1052
|
+
});
|
|
1053
|
+
label.textContent = actionLabel();
|
|
1054
|
+
row.appendChild(label);
|
|
1055
|
+
|
|
1056
|
+
// Dots
|
|
1057
|
+
row.appendChild(buildDots(false));
|
|
1058
|
+
|
|
1059
|
+
// Status
|
|
1060
|
+
const status = el('span', {
|
|
1061
|
+
fontSize: '11px', color: BP.textDim, whiteSpace: 'nowrap',
|
|
1062
|
+
marginLeft: 'auto',
|
|
1063
|
+
});
|
|
1064
|
+
// Variants currently arrive atomically in a single file edit, so a
|
|
1065
|
+
// per-variant counter would lie. Say what's true.
|
|
1066
|
+
status.textContent = arrivedVariants < expectedVariants
|
|
1067
|
+
? 'Generating ' + expectedVariants + ' variants...'
|
|
1068
|
+
: 'Done';
|
|
1069
|
+
row.appendChild(status);
|
|
1070
|
+
|
|
1071
|
+
return row;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// --- Cycling row ---
|
|
1075
|
+
|
|
1076
|
+
const TUNE_ICON_SVG = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" style="flex-shrink:0"><line x1="4" y1="8" x2="20" y2="8"/><circle cx="14" cy="8" r="2.4" fill="currentColor" stroke="none"/><line x1="4" y1="16" x2="20" y2="16"/><circle cx="10" cy="16" r="2.4" fill="currentColor" stroke="none"/></svg>';
|
|
1077
|
+
|
|
1078
|
+
function buildCyclingRow() {
|
|
1079
|
+
const row = el('div', {
|
|
1080
|
+
display: 'flex', alignItems: 'center', gap: '6px',
|
|
1081
|
+
padding: '1px 2px',
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
// Prev
|
|
1085
|
+
const prev = navBtn('\u2190');
|
|
1086
|
+
prev.addEventListener('click', (e) => { e.stopPropagation(); cycleVariant(-1); });
|
|
1087
|
+
if (visibleVariant <= 1) prev.style.opacity = '0.3';
|
|
1088
|
+
row.appendChild(prev);
|
|
1089
|
+
|
|
1090
|
+
// Dots (clickable)
|
|
1091
|
+
row.appendChild(buildDots(true));
|
|
1092
|
+
|
|
1093
|
+
// Counter
|
|
1094
|
+
const counter = el('span', {
|
|
1095
|
+
fontFamily: MONO, fontSize: '11px', fontWeight: '500',
|
|
1096
|
+
color: BP.textDim, minWidth: '24px', textAlign: 'center',
|
|
1097
|
+
});
|
|
1098
|
+
counter.textContent = visibleVariant + '/' + arrivedVariants;
|
|
1099
|
+
row.appendChild(counter);
|
|
1100
|
+
|
|
1101
|
+
// Next
|
|
1102
|
+
const next = navBtn('\u2192');
|
|
1103
|
+
next.addEventListener('click', (e) => { e.stopPropagation(); cycleVariant(1); });
|
|
1104
|
+
if (visibleVariant >= arrivedVariants) next.style.opacity = '0.3';
|
|
1105
|
+
row.appendChild(next);
|
|
1106
|
+
|
|
1107
|
+
// Tune chip — only when the visible variant exposes params
|
|
1108
|
+
const visParams = parseVariantParams(getVisibleVariantEl());
|
|
1109
|
+
const hasParams = visParams.length > 0;
|
|
1110
|
+
if (hasParams) {
|
|
1111
|
+
const tune = el('button', {
|
|
1112
|
+
display: 'inline-flex', alignItems: 'center', gap: '6px',
|
|
1113
|
+
padding: '4px 10px', borderRadius: '5px',
|
|
1114
|
+
border: '1px solid transparent',
|
|
1115
|
+
background: tuneOpen ? BP.accentSoft : 'transparent',
|
|
1116
|
+
color: tuneOpen ? BP.accent : BP.text,
|
|
1117
|
+
fontFamily: FONT, fontSize: '11px', fontWeight: '500',
|
|
1118
|
+
cursor: 'pointer',
|
|
1119
|
+
transition: 'color 0.12s ease, background 0.12s ease',
|
|
1120
|
+
whiteSpace: 'nowrap',
|
|
1121
|
+
});
|
|
1122
|
+
tune.innerHTML = TUNE_ICON_SVG;
|
|
1123
|
+
const tuneLabel = document.createElement('span');
|
|
1124
|
+
tuneLabel.textContent = 'Tune';
|
|
1125
|
+
tune.appendChild(tuneLabel);
|
|
1126
|
+
const tuneBadge = document.createElement('span');
|
|
1127
|
+
Object.assign(tuneBadge.style, {
|
|
1128
|
+
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
1129
|
+
minWidth: '16px', height: '16px', padding: '0 4px',
|
|
1130
|
+
borderRadius: '999px',
|
|
1131
|
+
background: tuneOpen ? C.brand : BP.hairline,
|
|
1132
|
+
color: tuneOpen ? 'oklch(98% 0 0)' : 'inherit',
|
|
1133
|
+
fontFamily: MONO, fontSize: '9.5px', fontWeight: '600',
|
|
1134
|
+
lineHeight: '1',
|
|
1135
|
+
boxSizing: 'border-box',
|
|
1136
|
+
});
|
|
1137
|
+
tuneBadge.textContent = String(visParams.length);
|
|
1138
|
+
tune.appendChild(tuneBadge);
|
|
1139
|
+
tune.title = 'Tune this variant (' + visParams.length + ' knob' + (visParams.length === 1 ? '' : 's') + ')';
|
|
1140
|
+
tune.addEventListener('mouseenter', () => {
|
|
1141
|
+
if (!tuneOpen) tune.style.background = BP.accentSoft;
|
|
1142
|
+
});
|
|
1143
|
+
tune.addEventListener('mouseleave', () => {
|
|
1144
|
+
if (!tuneOpen) tune.style.background = 'transparent';
|
|
1145
|
+
});
|
|
1146
|
+
tune.addEventListener('click', (e) => { e.stopPropagation(); toggleTunePopover(); });
|
|
1147
|
+
tune.dataset.iceqTune = '1';
|
|
1148
|
+
row.appendChild(tune);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// Spacer
|
|
1152
|
+
row.appendChild(el('div', { flex: '1' }));
|
|
1153
|
+
|
|
1154
|
+
// Accept — primary action, uses the site's saturated brand magenta
|
|
1155
|
+
// with paper-white text, not the theme-muted BP.accent.
|
|
1156
|
+
const accept = el('button', {
|
|
1157
|
+
padding: '5px 14px', borderRadius: '5px',
|
|
1158
|
+
border: 'none', background: C.brand, color: 'oklch(98% 0 0)',
|
|
1159
|
+
fontFamily: FONT, fontSize: '11px', fontWeight: '600',
|
|
1160
|
+
cursor: 'pointer', transition: 'filter 0.12s ease, transform 0.1s ease',
|
|
1161
|
+
whiteSpace: 'nowrap',
|
|
1162
|
+
});
|
|
1163
|
+
accept.textContent = '\u2713 Accept';
|
|
1164
|
+
accept.addEventListener('mouseenter', () => accept.style.filter = 'brightness(1.08)');
|
|
1165
|
+
accept.addEventListener('mouseleave', () => accept.style.filter = 'none');
|
|
1166
|
+
accept.addEventListener('mousedown', () => accept.style.transform = 'scale(0.97)');
|
|
1167
|
+
accept.addEventListener('mouseup', () => accept.style.transform = 'scale(1)');
|
|
1168
|
+
accept.addEventListener('click', (e) => { e.stopPropagation(); handleAccept(); });
|
|
1169
|
+
if (arrivedVariants === 0) { accept.style.opacity = '0.3'; accept.style.pointerEvents = 'none'; }
|
|
1170
|
+
row.appendChild(accept);
|
|
1171
|
+
|
|
1172
|
+
// Discard
|
|
1173
|
+
const discard = el('button', {
|
|
1174
|
+
padding: '4px 6px', borderRadius: '5px',
|
|
1175
|
+
border: '1px solid ' + BP.hairline, background: 'transparent',
|
|
1176
|
+
fontFamily: FONT, fontSize: '11px', color: BP.textDim,
|
|
1177
|
+
cursor: 'pointer', transition: 'color 0.12s ease, border-color 0.12s ease',
|
|
1178
|
+
});
|
|
1179
|
+
discard.textContent = '\u2715';
|
|
1180
|
+
discard.title = 'Discard all variants';
|
|
1181
|
+
discard.addEventListener('mouseenter', () => { discard.style.color = BP.text; discard.style.borderColor = BP.text; });
|
|
1182
|
+
discard.addEventListener('mouseleave', () => { discard.style.color = BP.textDim; discard.style.borderColor = BP.hairline; });
|
|
1183
|
+
discard.addEventListener('click', (e) => { e.stopPropagation(); handleDiscard(); });
|
|
1184
|
+
row.appendChild(discard);
|
|
1185
|
+
|
|
1186
|
+
return row;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// --- Shared UI builders ---
|
|
1190
|
+
|
|
1191
|
+
// --- Saving row (waiting for agent to process accept/discard) ---
|
|
1192
|
+
|
|
1193
|
+
function buildSavingRow() {
|
|
1194
|
+
const row = el('div', {
|
|
1195
|
+
display: 'flex', alignItems: 'center', gap: '8px',
|
|
1196
|
+
padding: '2px 8px',
|
|
1197
|
+
});
|
|
1198
|
+
const spinner = el('div', {
|
|
1199
|
+
width: '14px', height: '14px', borderRadius: '50%',
|
|
1200
|
+
border: '2px solid ' + BP.hairline,
|
|
1201
|
+
borderTopColor: BP.accent,
|
|
1202
|
+
animation: 'impeccable-spin 0.6s linear infinite',
|
|
1203
|
+
flexShrink: '0',
|
|
1204
|
+
});
|
|
1205
|
+
row.appendChild(spinner);
|
|
1206
|
+
const label = el('span', {
|
|
1207
|
+
fontSize: '12px', color: BP.textDim, fontWeight: '500',
|
|
1208
|
+
});
|
|
1209
|
+
label.textContent = 'Applying variant...';
|
|
1210
|
+
row.appendChild(label);
|
|
1211
|
+
|
|
1212
|
+
// Inject the keyframes if not already present
|
|
1213
|
+
if (!document.getElementById(PREFIX + '-keyframes')) {
|
|
1214
|
+
const style = document.createElement('style');
|
|
1215
|
+
style.id = PREFIX + '-keyframes';
|
|
1216
|
+
style.textContent = '@keyframes impeccable-spin { to { transform: rotate(360deg); } }';
|
|
1217
|
+
document.head.appendChild(style);
|
|
1218
|
+
}
|
|
1219
|
+
return row;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// --- Confirmed row (green success, auto-dismisses) ---
|
|
1223
|
+
|
|
1224
|
+
function buildConfirmedRow() {
|
|
1225
|
+
const row = el('div', {
|
|
1226
|
+
display: 'flex', alignItems: 'center', gap: '8px',
|
|
1227
|
+
padding: '2px 8px',
|
|
1228
|
+
});
|
|
1229
|
+
const check = el('span', {
|
|
1230
|
+
fontSize: '15px', lineHeight: '1', flexShrink: '0',
|
|
1231
|
+
color: 'oklch(45% 0.15 145)',
|
|
1232
|
+
});
|
|
1233
|
+
check.textContent = '\u2713';
|
|
1234
|
+
row.appendChild(check);
|
|
1235
|
+
const label = el('span', {
|
|
1236
|
+
fontSize: '12px', color: 'oklch(35% 0.1 145)', fontWeight: '600',
|
|
1237
|
+
});
|
|
1238
|
+
label.textContent = 'Variant applied';
|
|
1239
|
+
row.appendChild(label);
|
|
1240
|
+
return row;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// --- Shared UI builders ---
|
|
1244
|
+
|
|
1245
|
+
function buildDots(clickable) {
|
|
1246
|
+
const container = el('div', {
|
|
1247
|
+
display: 'flex', alignItems: 'center', gap: '4px',
|
|
1248
|
+
});
|
|
1249
|
+
for (let i = 1; i <= expectedVariants; i++) {
|
|
1250
|
+
const arrived = i <= arrivedVariants;
|
|
1251
|
+
const active = i === visibleVariant;
|
|
1252
|
+
// active: solid site-brand magenta dot. arrived+inactive: muted neutral.
|
|
1253
|
+
// pending (not yet arrived): faint outline ring. No borders on arrived
|
|
1254
|
+
// dots — the previous "accent ring + ash fill" combo read as noisy
|
|
1255
|
+
// magenta chips, especially when all variants had arrived and every
|
|
1256
|
+
// dot wore an accent ring.
|
|
1257
|
+
const dotBg = active ? C.brand
|
|
1258
|
+
: arrived ? BP.textDim
|
|
1259
|
+
: 'transparent';
|
|
1260
|
+
const dotBorder = arrived ? 'none' : '1.5px solid ' + BP.hairline;
|
|
1261
|
+
const dot = el('div', {
|
|
1262
|
+
width: active ? '8px' : '6px',
|
|
1263
|
+
height: active ? '8px' : '6px',
|
|
1264
|
+
borderRadius: '50%',
|
|
1265
|
+
background: dotBg,
|
|
1266
|
+
border: dotBorder,
|
|
1267
|
+
boxSizing: 'border-box',
|
|
1268
|
+
transition: 'all 0.2s ' + EASE,
|
|
1269
|
+
cursor: (clickable && arrived) ? 'pointer' : 'default',
|
|
1270
|
+
transform: arrived ? 'scale(1)' : 'scale(0.85)',
|
|
1271
|
+
opacity: arrived ? (active ? '1' : '0.6') : '0.4',
|
|
1272
|
+
});
|
|
1273
|
+
if (clickable && arrived) {
|
|
1274
|
+
const idx = i;
|
|
1275
|
+
dot.addEventListener('click', (e) => {
|
|
1276
|
+
e.stopPropagation();
|
|
1277
|
+
visibleVariant = idx;
|
|
1278
|
+
showVariantInDOM(currentSessionId, idx);
|
|
1279
|
+
updateSelectedElement();
|
|
1280
|
+
updateBarContent('cycling');
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
container.appendChild(dot);
|
|
1284
|
+
}
|
|
1285
|
+
return container;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function navBtn(text) {
|
|
1289
|
+
const b = el('button', {
|
|
1290
|
+
width: '26px', height: '26px', borderRadius: '5px',
|
|
1291
|
+
border: '1px solid ' + BP.hairline, background: 'transparent',
|
|
1292
|
+
color: BP.text, fontFamily: FONT, fontSize: '13px',
|
|
1293
|
+
cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
1294
|
+
transition: 'border-color 0.12s ease, background 0.12s ease',
|
|
1295
|
+
padding: '0', lineHeight: '1',
|
|
1296
|
+
});
|
|
1297
|
+
b.textContent = text;
|
|
1298
|
+
b.addEventListener('mouseenter', () => { b.style.borderColor = BP.text; });
|
|
1299
|
+
b.addEventListener('mouseleave', () => { b.style.borderColor = BP.hairline; });
|
|
1300
|
+
return b;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function actionLabel() {
|
|
1304
|
+
const a = ACTIONS.find(a => a.value === selectedAction);
|
|
1305
|
+
return a ? a.label : 'Freeform';
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function el(tag, styles) {
|
|
1309
|
+
const e = document.createElement(tag);
|
|
1310
|
+
if (styles) Object.assign(e.style, styles);
|
|
1311
|
+
return e;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// ---------------------------------------------------------------------------
|
|
1315
|
+
// Action picker popover
|
|
1316
|
+
// ---------------------------------------------------------------------------
|
|
1317
|
+
|
|
1318
|
+
function initActionPicker() {
|
|
1319
|
+
const P = barPaletteForTheme(detectPageTheme());
|
|
1320
|
+
pickerEl = document.createElement('div');
|
|
1321
|
+
pickerEl.id = PREFIX + '-picker';
|
|
1322
|
+
Object.assign(pickerEl.style, {
|
|
1323
|
+
position: 'fixed', zIndex: Z.picker,
|
|
1324
|
+
display: 'none', opacity: '0',
|
|
1325
|
+
transform: 'scale(0.96) translateY(4px)',
|
|
1326
|
+
transformOrigin: 'bottom left',
|
|
1327
|
+
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
|
|
1328
|
+
background: P.surface,
|
|
1329
|
+
border: '1px solid ' + P.hairline,
|
|
1330
|
+
borderRadius: '10px',
|
|
1331
|
+
boxShadow: '0 8px 30px oklch(0% 0 0 / 0.10), 0 2px 6px oklch(0% 0 0 / 0.06)',
|
|
1332
|
+
padding: '6px',
|
|
1333
|
+
fontFamily: FONT,
|
|
1334
|
+
backdropFilter: 'blur(10px)',
|
|
1335
|
+
WebkitBackdropFilter: 'blur(10px)',
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
// Build the chip grid
|
|
1339
|
+
const grid = el('div', {
|
|
1340
|
+
display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '3px',
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
ACTIONS.forEach(action => {
|
|
1344
|
+
const chip = el('button', {
|
|
1345
|
+
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
|
1346
|
+
gap: '4px',
|
|
1347
|
+
padding: '8px 6px', borderRadius: '6px',
|
|
1348
|
+
border: 'none',
|
|
1349
|
+
background: action.value === selectedAction ? P.accentSoft : 'transparent',
|
|
1350
|
+
color: action.value === selectedAction ? P.accent : P.text,
|
|
1351
|
+
fontFamily: FONT, fontSize: '11px', fontWeight: '500',
|
|
1352
|
+
cursor: 'pointer',
|
|
1353
|
+
transition: 'background 0.1s ease, color 0.1s ease',
|
|
1354
|
+
textAlign: 'center', whiteSpace: 'nowrap',
|
|
1355
|
+
});
|
|
1356
|
+
const iconWrap = el('span', {
|
|
1357
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
1358
|
+
height: '20px', opacity: '0.9',
|
|
1359
|
+
});
|
|
1360
|
+
iconWrap.innerHTML = ICONS[action.value] || '';
|
|
1361
|
+
const labelEl = el('span', { lineHeight: '1' });
|
|
1362
|
+
labelEl.textContent = action.label;
|
|
1363
|
+
chip.appendChild(iconWrap);
|
|
1364
|
+
chip.appendChild(labelEl);
|
|
1365
|
+
chip.dataset.action = action.value;
|
|
1366
|
+
chip.addEventListener('mouseenter', () => {
|
|
1367
|
+
if (action.value !== selectedAction) chip.style.background = P.accentSoft;
|
|
1368
|
+
});
|
|
1369
|
+
chip.addEventListener('mouseleave', () => {
|
|
1370
|
+
chip.style.background = action.value === selectedAction ? P.accentSoft : 'transparent';
|
|
1371
|
+
});
|
|
1372
|
+
chip.addEventListener('click', (e) => {
|
|
1373
|
+
e.stopPropagation();
|
|
1374
|
+
selectedAction = action.value;
|
|
1375
|
+
hideActionPicker();
|
|
1376
|
+
updateBarContent('configure');
|
|
1377
|
+
});
|
|
1378
|
+
grid.appendChild(chip);
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
pickerEl.appendChild(grid);
|
|
1382
|
+
document.body.appendChild(pickerEl);
|
|
1383
|
+
defangOutsideHandlers(pickerEl);
|
|
1384
|
+
|
|
1385
|
+
// Cache the palette on the picker so toggleActionPicker's state refresh
|
|
1386
|
+
// uses the same theme-aware colors when it repaints chips.
|
|
1387
|
+
pickerEl.__iceq_palette = P;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function toggleActionPicker() {
|
|
1391
|
+
if (pickerEl.style.display !== 'none') { hideActionPicker(); return; }
|
|
1392
|
+
// Rebuild chips to reflect current selection
|
|
1393
|
+
const P = pickerEl.__iceq_palette || barPaletteForTheme(detectPageTheme());
|
|
1394
|
+
pickerEl.querySelectorAll('button').forEach(chip => {
|
|
1395
|
+
const isActive = chip.dataset.action === selectedAction;
|
|
1396
|
+
chip.style.background = isActive ? P.accentSoft : 'transparent';
|
|
1397
|
+
chip.style.color = isActive ? P.accent : P.text;
|
|
1398
|
+
});
|
|
1399
|
+
// Position above the bar
|
|
1400
|
+
const barRect = barEl.getBoundingClientRect();
|
|
1401
|
+
const pickerH = 170; // approximate; grows with icon + label rows
|
|
1402
|
+
let top = barRect.top - pickerH - 6;
|
|
1403
|
+
if (top < 8) top = barRect.bottom + 6;
|
|
1404
|
+
Object.assign(pickerEl.style, {
|
|
1405
|
+
top: top + 'px', left: barRect.left + 'px',
|
|
1406
|
+
display: 'block',
|
|
1407
|
+
});
|
|
1408
|
+
requestAnimationFrame(() => {
|
|
1409
|
+
pickerEl.style.opacity = '1';
|
|
1410
|
+
pickerEl.style.transform = 'scale(1) translateY(0)';
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
function hideActionPicker() {
|
|
1415
|
+
if (!pickerEl) return;
|
|
1416
|
+
pickerEl.style.opacity = '0';
|
|
1417
|
+
pickerEl.style.transform = 'scale(0.96) translateY(4px)';
|
|
1418
|
+
setTimeout(() => { if (pickerEl) pickerEl.style.display = 'none'; }, 180);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// ---------------------------------------------------------------------------
|
|
1422
|
+
// Params panel (per-variant coarse controls)
|
|
1423
|
+
//
|
|
1424
|
+
// Variants may declare a parameter manifest via a JSON attribute on the
|
|
1425
|
+
// variant wrapper:
|
|
1426
|
+
//
|
|
1427
|
+
// <div data-impeccable-variant="1"
|
|
1428
|
+
// data-impeccable-params='[{"id":"density","kind":"steps",...}]'>
|
|
1429
|
+
//
|
|
1430
|
+
// The panel docks to the right edge of the outline during CYCLING and
|
|
1431
|
+
// exposes 2-5 coarse knobs. Values apply to the variant wrapper so scoped
|
|
1432
|
+
// CSS can respond instantly without regeneration:
|
|
1433
|
+
//
|
|
1434
|
+
// range / numeric toggle → CSS var (`--p-<id>`) used via var(--p-foo, N)
|
|
1435
|
+
// steps / boolean toggle → data-p-<id> attribute used via :scope[data-p-foo="..."]
|
|
1436
|
+
//
|
|
1437
|
+
// On variant switch, values reset to that variant's declared defaults.
|
|
1438
|
+
// On accept, current values are sent in the event payload so the agent
|
|
1439
|
+
// can bake them into the source-file write.
|
|
1440
|
+
// ---------------------------------------------------------------------------
|
|
1441
|
+
|
|
1442
|
+
let paramsPanelEl = null; // outer wrapper (overflow:hidden, clips the slide)
|
|
1443
|
+
let paramsPanelInner = null; // translating content (carries bg, padding, knobs)
|
|
1444
|
+
let paramsPanelBody = null; // grid holding the knob cells
|
|
1445
|
+
let paramsCurrentValues = {}; // {paramId: value} — mirror of the visible variant's live values
|
|
1446
|
+
let tuneOpen = false; // whether the Tune popover is open right now
|
|
1447
|
+
|
|
1448
|
+
// Theme-aware Tune popover. Appears as a drawer that slides out from the
|
|
1449
|
+
// contextual bar's bar-facing edge (below if the bar sits below the
|
|
1450
|
+
// element, above otherwise). Same width as the bar. Auto-wraps to extra
|
|
1451
|
+
// rows when the knobs exceed one row. The bar's border-radius on the
|
|
1452
|
+
// popover side goes flat while open so the two shapes read as one.
|
|
1453
|
+
let paramsPanelPalette = null;
|
|
1454
|
+
|
|
1455
|
+
function initParamsPanel() {
|
|
1456
|
+
paramsPanelPalette = barPaletteForTheme(detectPageTheme());
|
|
1457
|
+
const P = paramsPanelPalette;
|
|
1458
|
+
|
|
1459
|
+
// Single element, always in the DOM. The slide animation is a CSS mask
|
|
1460
|
+
// with mask-size growing from 0% to 100% along the bar-facing axis — no
|
|
1461
|
+
// display toggle, no opacity toggle, no transform trickery. The mask
|
|
1462
|
+
// hides everything initially; as it grows, content is revealed from
|
|
1463
|
+
// the bar edge outward.
|
|
1464
|
+
paramsPanelEl = document.createElement('div');
|
|
1465
|
+
paramsPanelEl.id = PREFIX + '-params-panel';
|
|
1466
|
+
Object.assign(paramsPanelEl.style, {
|
|
1467
|
+
position: 'fixed', zIndex: String(Z.bar - 1),
|
|
1468
|
+
background: P.surfaceDeep,
|
|
1469
|
+
color: P.text,
|
|
1470
|
+
fontFamily: FONT,
|
|
1471
|
+
padding: '14px 18px',
|
|
1472
|
+
boxSizing: 'border-box',
|
|
1473
|
+
borderRadius: '0 0 10px 10px',
|
|
1474
|
+
pointerEvents: 'none',
|
|
1475
|
+
backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)',
|
|
1476
|
+
|
|
1477
|
+
// clip-path is the same conceptual reveal as mask but with rock-solid
|
|
1478
|
+
// transition support across engines. Closed state clips from the far
|
|
1479
|
+
// edge; open = inset(0) shows everything.
|
|
1480
|
+
clipPath: 'inset(0 0 100% 0)',
|
|
1481
|
+
transition: 'clip-path 0.44s ' + EASE,
|
|
1482
|
+
|
|
1483
|
+
// Park off-screen until positionParamsPanel places it. These are NOT
|
|
1484
|
+
// in the transition list, so they snap instantly — no fly-in from the
|
|
1485
|
+
// top-left when first shown.
|
|
1486
|
+
top: '-9999px', left: '-9999px', width: '0',
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
paramsPanelBody = el('div', {
|
|
1490
|
+
display: 'grid',
|
|
1491
|
+
gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))',
|
|
1492
|
+
gap: '12px 16px',
|
|
1493
|
+
});
|
|
1494
|
+
|
|
1495
|
+
paramsPanelEl.appendChild(paramsPanelBody);
|
|
1496
|
+
document.body.appendChild(paramsPanelEl);
|
|
1497
|
+
// Don't override pointer-events: the panel toggles between 'none' (closed,
|
|
1498
|
+
// click-through) and 'auto' (open) on its own. Just silence the host's
|
|
1499
|
+
// outside-interaction listeners while the panel is open.
|
|
1500
|
+
defangOutsideHandlers(paramsPanelEl, { setPointerEvents: false });
|
|
1501
|
+
paramsPanelInner = paramsPanelEl; // compatibility alias for the rest of the code
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
function getVisibleVariantEl() {
|
|
1505
|
+
if (!currentSessionId) return null;
|
|
1506
|
+
const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
|
|
1507
|
+
if (!wrapper) return null;
|
|
1508
|
+
return wrapper.querySelector('[data-impeccable-variant="' + visibleVariant + '"]');
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
function parseVariantParams(variantEl) {
|
|
1512
|
+
if (!variantEl) return [];
|
|
1513
|
+
const raw = variantEl.getAttribute('data-impeccable-params');
|
|
1514
|
+
if (!raw) return [];
|
|
1515
|
+
try {
|
|
1516
|
+
const parsed = JSON.parse(raw);
|
|
1517
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
1518
|
+
} catch (err) {
|
|
1519
|
+
console.warn('[impeccable] Invalid data-impeccable-params JSON:', err.message);
|
|
1520
|
+
return [];
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
function applyParamValue(variantEl, param, value) {
|
|
1525
|
+
if (!variantEl) return;
|
|
1526
|
+
const attr = 'data-p-' + param.id;
|
|
1527
|
+
if (param.kind === 'range') {
|
|
1528
|
+
variantEl.style.setProperty('--p-' + param.id, String(value));
|
|
1529
|
+
} else if (param.kind === 'toggle') {
|
|
1530
|
+
const on = !!value;
|
|
1531
|
+
variantEl.style.setProperty('--p-' + param.id, on ? '1' : '0');
|
|
1532
|
+
if (on) variantEl.setAttribute(attr, 'on');
|
|
1533
|
+
else variantEl.removeAttribute(attr);
|
|
1534
|
+
} else if (param.kind === 'steps') {
|
|
1535
|
+
variantEl.setAttribute(attr, String(value));
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
function applyParamDefaults(variantEl, params) {
|
|
1540
|
+
paramsCurrentValues = {};
|
|
1541
|
+
for (const p of params) {
|
|
1542
|
+
paramsCurrentValues[p.id] = p.default;
|
|
1543
|
+
applyParamValue(variantEl, p, p.default);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
function formatRangeValue(input) {
|
|
1548
|
+
const max = parseFloat(input.max), min = parseFloat(input.min);
|
|
1549
|
+
const v = parseFloat(input.value);
|
|
1550
|
+
if (!isFinite(v)) return input.value;
|
|
1551
|
+
return (max - min) <= 2 ? v.toFixed(2) : String(Math.round(v));
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
function buildParamsPanel(variantEl, params) {
|
|
1555
|
+
const P = paramsPanelPalette || barPaletteForTheme(detectPageTheme());
|
|
1556
|
+
paramsPanelBody.innerHTML = '';
|
|
1557
|
+
for (const p of params) {
|
|
1558
|
+
const row = el('div', { display: 'flex', flexDirection: 'column', gap: '6px' });
|
|
1559
|
+
const labelRow = el('div', {
|
|
1560
|
+
display: 'flex', justifyContent: 'space-between',
|
|
1561
|
+
alignItems: 'baseline', gap: '8px',
|
|
1562
|
+
});
|
|
1563
|
+
const lbl = el('span', {
|
|
1564
|
+
fontSize: '10.5px', fontWeight: '600', color: P.text,
|
|
1565
|
+
letterSpacing: '0.03em',
|
|
1566
|
+
});
|
|
1567
|
+
lbl.textContent = p.label || p.id;
|
|
1568
|
+
labelRow.appendChild(lbl);
|
|
1569
|
+
const readout = el('span', {
|
|
1570
|
+
fontSize: '10.5px', color: P.textDim,
|
|
1571
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
|
|
1572
|
+
});
|
|
1573
|
+
labelRow.appendChild(readout);
|
|
1574
|
+
row.appendChild(labelRow);
|
|
1575
|
+
|
|
1576
|
+
if (p.kind === 'range') {
|
|
1577
|
+
const input = document.createElement('input');
|
|
1578
|
+
input.type = 'range';
|
|
1579
|
+
input.min = String(p.min != null ? p.min : 0);
|
|
1580
|
+
input.max = String(p.max != null ? p.max : 1);
|
|
1581
|
+
input.step = String(p.step != null ? p.step : 0.05);
|
|
1582
|
+
input.value = String(p.default);
|
|
1583
|
+
Object.assign(input.style, {
|
|
1584
|
+
width: '100%', accentColor: C.brand, cursor: 'pointer',
|
|
1585
|
+
});
|
|
1586
|
+
readout.textContent = formatRangeValue(input);
|
|
1587
|
+
input.addEventListener('input', (e) => {
|
|
1588
|
+
e.stopPropagation();
|
|
1589
|
+
const v = parseFloat(input.value);
|
|
1590
|
+
paramsCurrentValues[p.id] = v;
|
|
1591
|
+
readout.textContent = formatRangeValue(input);
|
|
1592
|
+
applyParamValue(variantEl, p, v);
|
|
1593
|
+
queueCheckpoint('param_changed');
|
|
1594
|
+
});
|
|
1595
|
+
row.appendChild(input);
|
|
1596
|
+
} else if (p.kind === 'toggle') {
|
|
1597
|
+
const initial = !!p.default;
|
|
1598
|
+
readout.textContent = initial ? 'On' : 'Off';
|
|
1599
|
+
const track = el('button', {
|
|
1600
|
+
position: 'relative', width: '36px', height: '20px',
|
|
1601
|
+
borderRadius: '10px', border: 'none', padding: '0',
|
|
1602
|
+
cursor: 'pointer',
|
|
1603
|
+
background: initial ? C.brand : P.hairline,
|
|
1604
|
+
transition: 'background 0.15s ease',
|
|
1605
|
+
alignSelf: 'flex-start',
|
|
1606
|
+
});
|
|
1607
|
+
const knob = el('span', {
|
|
1608
|
+
position: 'absolute', top: '2px',
|
|
1609
|
+
left: initial ? '18px' : '2px',
|
|
1610
|
+
width: '16px', height: '16px', borderRadius: '50%',
|
|
1611
|
+
background: 'oklch(98% 0 0)',
|
|
1612
|
+
transition: 'left 0.18s ' + EASE,
|
|
1613
|
+
boxShadow: '0 1px 2px oklch(0% 0 0 / 0.2)',
|
|
1614
|
+
});
|
|
1615
|
+
track.appendChild(knob);
|
|
1616
|
+
track.addEventListener('click', (e) => {
|
|
1617
|
+
e.stopPropagation();
|
|
1618
|
+
const next = !paramsCurrentValues[p.id];
|
|
1619
|
+
paramsCurrentValues[p.id] = next;
|
|
1620
|
+
track.style.background = next ? C.brand : P.hairline;
|
|
1621
|
+
knob.style.left = next ? '18px' : '2px';
|
|
1622
|
+
readout.textContent = next ? 'On' : 'Off';
|
|
1623
|
+
applyParamValue(variantEl, p, next);
|
|
1624
|
+
queueCheckpoint('param_changed');
|
|
1625
|
+
});
|
|
1626
|
+
row.appendChild(track);
|
|
1627
|
+
} else if (p.kind === 'steps') {
|
|
1628
|
+
const opts = (p.options || []).map(o =>
|
|
1629
|
+
typeof o === 'string' ? { value: o, label: o } : o
|
|
1630
|
+
);
|
|
1631
|
+
const activeOpt = opts.find(o => o.value === p.default) || opts[0];
|
|
1632
|
+
readout.textContent = activeOpt ? activeOpt.label : String(p.default);
|
|
1633
|
+
const segRow = el('div', {
|
|
1634
|
+
display: 'grid',
|
|
1635
|
+
gridTemplateColumns: 'repeat(' + opts.length + ', 1fr)',
|
|
1636
|
+
gap: '1px', padding: '2px',
|
|
1637
|
+
background: P.hairline, borderRadius: '5px',
|
|
1638
|
+
});
|
|
1639
|
+
const segBtns = [];
|
|
1640
|
+
opts.forEach(o => {
|
|
1641
|
+
const active = o.value === p.default;
|
|
1642
|
+
const b = el('button', {
|
|
1643
|
+
padding: '5px 4px', border: 'none', borderRadius: '3px',
|
|
1644
|
+
background: active ? C.brand : 'transparent',
|
|
1645
|
+
color: active ? 'oklch(98% 0 0)' : P.text,
|
|
1646
|
+
fontFamily: FONT, fontSize: '10.5px', fontWeight: '500',
|
|
1647
|
+
cursor: 'pointer', whiteSpace: 'nowrap',
|
|
1648
|
+
transition: 'background 0.1s ease, color 0.1s ease',
|
|
1649
|
+
});
|
|
1650
|
+
b.textContent = o.label;
|
|
1651
|
+
b.addEventListener('click', (e) => {
|
|
1652
|
+
e.stopPropagation();
|
|
1653
|
+
paramsCurrentValues[p.id] = o.value;
|
|
1654
|
+
readout.textContent = o.label;
|
|
1655
|
+
segBtns.forEach(({ btn, val }) => {
|
|
1656
|
+
const on = val === o.value;
|
|
1657
|
+
btn.style.background = on ? C.brand : 'transparent';
|
|
1658
|
+
btn.style.color = on ? 'oklch(98% 0 0)' : P.text;
|
|
1659
|
+
});
|
|
1660
|
+
applyParamValue(variantEl, p, o.value);
|
|
1661
|
+
queueCheckpoint('param_changed');
|
|
1662
|
+
});
|
|
1663
|
+
segRow.appendChild(b);
|
|
1664
|
+
segBtns.push({ btn: b, val: o.value });
|
|
1665
|
+
});
|
|
1666
|
+
row.appendChild(segRow);
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
paramsPanelBody.appendChild(row);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// Decide which way the popover opens: away from the picked element. If the
|
|
1674
|
+
// bar landed below the element, popover slides DOWN from the bar's bottom.
|
|
1675
|
+
// If the bar landed above, popover slides UP from the bar's top.
|
|
1676
|
+
function popoverDirection() {
|
|
1677
|
+
if (!barEl || !selectedElement) return 'below';
|
|
1678
|
+
const br = barEl.getBoundingClientRect();
|
|
1679
|
+
const er = selectedElement.getBoundingClientRect();
|
|
1680
|
+
return br.top >= er.bottom - 4 ? 'below' : 'above';
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// The popover overlaps the bar by OVERLAP px on the bar-facing side. With
|
|
1684
|
+
// popover z-index below bar, that overlap sits behind bar (invisible) and
|
|
1685
|
+
// reinforces the "tucked behind" feel. Padding compensates so the real
|
|
1686
|
+
// content starts flush with bar's outer edge.
|
|
1687
|
+
const TUNE_OVERLAP = 6;
|
|
1688
|
+
|
|
1689
|
+
// Closed clip-path depends on direction: for 'below' clip from the far
|
|
1690
|
+
// (bottom) edge so the reveal grows downward from the bar; for 'above'
|
|
1691
|
+
// clip from the top edge so the reveal grows upward from the bar.
|
|
1692
|
+
function closedClipPath(direction) {
|
|
1693
|
+
return direction === 'below' ? 'inset(0 0 100% 0)' : 'inset(100% 0 0 0)';
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function setClipPath(value, withTransition) {
|
|
1697
|
+
const saved = paramsPanelEl.style.transition;
|
|
1698
|
+
if (!withTransition) paramsPanelEl.style.transition = 'none';
|
|
1699
|
+
paramsPanelEl.style.clipPath = value;
|
|
1700
|
+
if (!withTransition) {
|
|
1701
|
+
void paramsPanelEl.offsetHeight;
|
|
1702
|
+
paramsPanelEl.style.transition = saved;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
function positionParamsPanel() {
|
|
1707
|
+
if (!paramsPanelEl || !barEl || barEl.style.display === 'none') return;
|
|
1708
|
+
const br = barEl.getBoundingClientRect();
|
|
1709
|
+
const direction = popoverDirection();
|
|
1710
|
+
const prevDirection = paramsPanelEl.dataset.tuneDirection;
|
|
1711
|
+
|
|
1712
|
+
// top/left/width are NOT in the transition list, so they snap instantly.
|
|
1713
|
+
paramsPanelEl.style.left = br.left + 'px';
|
|
1714
|
+
paramsPanelEl.style.width = br.width + 'px';
|
|
1715
|
+
|
|
1716
|
+
if (direction === 'below') {
|
|
1717
|
+
paramsPanelEl.style.top = (br.bottom - TUNE_OVERLAP) + 'px';
|
|
1718
|
+
paramsPanelEl.style.borderRadius = '0 0 10px 10px';
|
|
1719
|
+
paramsPanelEl.style.paddingTop = (14 + TUNE_OVERLAP) + 'px';
|
|
1720
|
+
paramsPanelEl.style.paddingBottom = '14px';
|
|
1721
|
+
} else {
|
|
1722
|
+
const ih = paramsPanelEl.offsetHeight || 80;
|
|
1723
|
+
paramsPanelEl.style.top = (br.top - ih + TUNE_OVERLAP) + 'px';
|
|
1724
|
+
paramsPanelEl.style.borderRadius = '10px 10px 0 0';
|
|
1725
|
+
paramsPanelEl.style.paddingTop = '14px';
|
|
1726
|
+
paramsPanelEl.style.paddingBottom = (14 + TUNE_OVERLAP) + 'px';
|
|
1727
|
+
}
|
|
1728
|
+
paramsPanelEl.dataset.tuneDirection = direction;
|
|
1729
|
+
|
|
1730
|
+
// If currently closed and direction flipped (or first-time setup),
|
|
1731
|
+
// snap the clip-path to the new direction's closed pose without
|
|
1732
|
+
// transitioning (so the clip doesn't slide across the element).
|
|
1733
|
+
if (!tuneOpen && (!prevDirection || prevDirection !== direction)) {
|
|
1734
|
+
setClipPath(closedClipPath(direction), false);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
function showParamsPanel() {
|
|
1739
|
+
if (!paramsPanelEl) return;
|
|
1740
|
+
positionParamsPanel();
|
|
1741
|
+
paramsPanelEl.style.pointerEvents = 'auto';
|
|
1742
|
+
// rAF so the positioning paint commits before the transition fires.
|
|
1743
|
+
requestAnimationFrame(() => {
|
|
1744
|
+
setClipPath('inset(0 0 0 0)', true);
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
function hideParamsPanel() {
|
|
1749
|
+
if (!paramsPanelEl) return;
|
|
1750
|
+
paramsPanelEl.style.pointerEvents = 'none';
|
|
1751
|
+
const direction = paramsPanelEl.dataset.tuneDirection || 'below';
|
|
1752
|
+
setClipPath(closedClipPath(direction), true);
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// Build/rebuild the panel's contents for the current variant AND apply
|
|
1756
|
+
// its defaults to the variant wrapper (so scoped CSS responds even before
|
|
1757
|
+
// the user opens the popover). Visibility is governed by tuneOpen.
|
|
1758
|
+
function refreshParamsPanel() {
|
|
1759
|
+
if (state !== 'CYCLING') {
|
|
1760
|
+
paramsCurrentValues = {};
|
|
1761
|
+
tuneOpen = false;
|
|
1762
|
+
hideParamsPanel();
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
const variantEl = getVisibleVariantEl();
|
|
1766
|
+
const params = parseVariantParams(variantEl);
|
|
1767
|
+
if (!variantEl || params.length === 0) {
|
|
1768
|
+
paramsCurrentValues = {};
|
|
1769
|
+
tuneOpen = false;
|
|
1770
|
+
hideParamsPanel();
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
applyParamDefaults(variantEl, params);
|
|
1774
|
+
buildParamsPanel(variantEl, params);
|
|
1775
|
+
if (tuneOpen) {
|
|
1776
|
+
// If already visible (variant cycled while open), refresh in place
|
|
1777
|
+
// instead of re-running the clip-path animation.
|
|
1778
|
+
const alreadyVisible = paramsPanelEl.style.display === 'block'
|
|
1779
|
+
&& paramsPanelEl.style.opacity === '1';
|
|
1780
|
+
if (alreadyVisible) positionParamsPanel();
|
|
1781
|
+
else showParamsPanel();
|
|
1782
|
+
} else {
|
|
1783
|
+
hideParamsPanel();
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
function toggleTunePopover() {
|
|
1788
|
+
if (tuneOpen) { closeTunePopover(); return; }
|
|
1789
|
+
openTunePopover();
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
function openTunePopover() {
|
|
1793
|
+
if (state !== 'CYCLING') return;
|
|
1794
|
+
const variantEl = getVisibleVariantEl();
|
|
1795
|
+
const params = parseVariantParams(variantEl);
|
|
1796
|
+
if (!variantEl || params.length === 0) return;
|
|
1797
|
+
// Build fresh to ensure the current variant's controls are shown.
|
|
1798
|
+
applyParamDefaults(variantEl, params);
|
|
1799
|
+
buildParamsPanel(variantEl, params);
|
|
1800
|
+
tuneOpen = true;
|
|
1801
|
+
showParamsPanel();
|
|
1802
|
+
// Kill the bar's shadow on the popover-facing side so the dark popover
|
|
1803
|
+
// doesn't pick up a bright glow line.
|
|
1804
|
+
if (barEl) {
|
|
1805
|
+
const direction = paramsPanelEl?.dataset.tuneDirection || 'below';
|
|
1806
|
+
barEl.style.boxShadow = direction === 'below' ? BAR_SHADOW_UP : BAR_SHADOW_DOWN;
|
|
1807
|
+
}
|
|
1808
|
+
// Re-render the bar so the Tune chip picks up the active styling.
|
|
1809
|
+
updateBarContent('cycling');
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
function closeTunePopover() {
|
|
1813
|
+
tuneOpen = false;
|
|
1814
|
+
hideParamsPanel();
|
|
1815
|
+
if (barEl) barEl.style.boxShadow = BAR_SHADOW_DEFAULT;
|
|
1816
|
+
if (barEl && barEl.style.display !== 'none' && state === 'CYCLING') {
|
|
1817
|
+
updateBarContent('cycling');
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// ---------------------------------------------------------------------------
|
|
1822
|
+
// Variant cycling in DOM
|
|
1823
|
+
// ---------------------------------------------------------------------------
|
|
1824
|
+
|
|
1825
|
+
function showVariantInDOM(sessionId, num) {
|
|
1826
|
+
const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
|
|
1827
|
+
if (!wrapper) return;
|
|
1828
|
+
for (const child of wrapper.children) {
|
|
1829
|
+
const v = child.dataset ? child.dataset.impeccableVariant : null;
|
|
1830
|
+
if (!v) continue;
|
|
1831
|
+
child.style.display = (v === String(num)) ? '' : 'none';
|
|
1832
|
+
}
|
|
1833
|
+
// Unconditional refresh — covers first-reveal (no-op if state isn't
|
|
1834
|
+
// CYCLING yet, the subsequent CYCLING transition triggers its own
|
|
1835
|
+
// refresh) and every cycle step.
|
|
1836
|
+
refreshParamsPanel();
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
/**
|
|
1840
|
+
* No-HMR fallback: fetch the raw source file from the live server,
|
|
1841
|
+
* parse it, extract the variant wrapper, and inject it into the live DOM.
|
|
1842
|
+
* This works even when the dev server caches HTML (Bun, static servers).
|
|
1843
|
+
*/
|
|
1844
|
+
function injectVariantsFromSource(filePath, sessionId) {
|
|
1845
|
+
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath);
|
|
1846
|
+
fetch(url)
|
|
1847
|
+
.then(r => { if (!r.ok) throw new Error(r.status); return r.text(); })
|
|
1848
|
+
.then(html => {
|
|
1849
|
+
// Parse the raw source HTML
|
|
1850
|
+
const parser = new DOMParser();
|
|
1851
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
1852
|
+
const srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]');
|
|
1853
|
+
if (!srcWrapper) {
|
|
1854
|
+
console.error('[impeccable] Variant wrapper not found in source file.');
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
// Find the original element in the live DOM.
|
|
1859
|
+
// The original is inside the wrapper in the source. We find the
|
|
1860
|
+
// corresponding element in the live DOM by matching the first child's
|
|
1861
|
+
// tag + classes from the original snapshot.
|
|
1862
|
+
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
|
|
1863
|
+
if (!origContent) return;
|
|
1864
|
+
|
|
1865
|
+
const tag = origContent.tagName.toLowerCase();
|
|
1866
|
+
const cls = origContent.className;
|
|
1867
|
+
let liveEl = null;
|
|
1868
|
+
if (origContent.id) {
|
|
1869
|
+
liveEl = document.getElementById(origContent.id);
|
|
1870
|
+
} else if (cls) {
|
|
1871
|
+
// Find by tag + exact class match
|
|
1872
|
+
const candidates = document.querySelectorAll(tag + '.' + cls.split(' ')[0]);
|
|
1873
|
+
for (const c of candidates) {
|
|
1874
|
+
if (c.className === cls && !own(c)) { liveEl = c; break; }
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
if (!liveEl) {
|
|
1879
|
+
console.error('[impeccable] Could not find original element in live DOM.');
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0;
|
|
1884
|
+
|
|
1885
|
+
// Replace the live element with the full wrapper from source
|
|
1886
|
+
const wrapper = srcWrapper.cloneNode(true);
|
|
1887
|
+
liveEl.parentElement.replaceChild(wrapper, liveEl);
|
|
1888
|
+
|
|
1889
|
+
// Update state: count variants, preserving the user's current variant
|
|
1890
|
+
// when a late HMR/source reinjection lands after they have cycled.
|
|
1891
|
+
const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
|
|
1892
|
+
arrivedVariants = variants.length;
|
|
1893
|
+
expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants);
|
|
1894
|
+
const saved = loadSession();
|
|
1895
|
+
const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0;
|
|
1896
|
+
visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants
|
|
1897
|
+
? previousVisibleVariant
|
|
1898
|
+
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
|
|
1899
|
+
showVariantInDOM(sessionId, visibleVariant);
|
|
1900
|
+
|
|
1901
|
+
// Update selectedElement to the visible variant's content
|
|
1902
|
+
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
|
|
1903
|
+
|
|
1904
|
+
state = 'CYCLING';
|
|
1905
|
+
hideShaderOverlay();
|
|
1906
|
+
updateBarContent('cycling');
|
|
1907
|
+
refreshParamsPanel();
|
|
1908
|
+
saveSession();
|
|
1909
|
+
console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.');
|
|
1910
|
+
})
|
|
1911
|
+
.catch(err => {
|
|
1912
|
+
console.error('[impeccable] Failed to fetch source:', err);
|
|
1913
|
+
showToast('Could not load variants. Try refreshing the page.', 5000);
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
function cycleVariant(dir) {
|
|
1918
|
+
const next = visibleVariant + dir;
|
|
1919
|
+
if (next < 1 || next > arrivedVariants) return;
|
|
1920
|
+
visibleVariant = next;
|
|
1921
|
+
showVariantInDOM(currentSessionId, next); // calls refreshParamsPanel itself
|
|
1922
|
+
updateSelectedElement();
|
|
1923
|
+
updateBarContent('cycling');
|
|
1924
|
+
saveSession();
|
|
1925
|
+
queueCheckpoint('variant_changed');
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
function updateSelectedElement() {
|
|
1929
|
+
if (!currentSessionId) return;
|
|
1930
|
+
const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
|
|
1931
|
+
if (!wrapper) return;
|
|
1932
|
+
const visEl = pickVariantContent(wrapper, visibleVariant);
|
|
1933
|
+
if (visEl) selectedElement = visEl;
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
function readVisibleVariantFromDOM(sessionId) {
|
|
1937
|
+
const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
|
|
1938
|
+
if (!wrapper) return 0;
|
|
1939
|
+
const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
|
|
1940
|
+
for (const variant of variants) {
|
|
1941
|
+
if (variant.style.display === 'none') continue;
|
|
1942
|
+
const idx = parseInt(variant.dataset.impeccableVariant || '0', 10);
|
|
1943
|
+
if (idx > 0) return idx;
|
|
1944
|
+
}
|
|
1945
|
+
return 0;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
// Resolve the element that represents the variant's visible content.
|
|
1949
|
+
// Contract: each variant div should contain exactly one top-level element
|
|
1950
|
+
// (the full replacement). In practice a model may ship loose siblings or
|
|
1951
|
+
// lead with <style>/<script>. Be defensive: skip non-visual elements, and
|
|
1952
|
+
// if the variant has multiple element children, use the variant div itself
|
|
1953
|
+
// (it wraps all of them and gets correct bounds).
|
|
1954
|
+
function pickVariantContent(wrapper, index) {
|
|
1955
|
+
if (!wrapper) return null;
|
|
1956
|
+
const variantDiv = wrapper.querySelector('[data-impeccable-variant="' + index + '"]');
|
|
1957
|
+
if (!variantDiv) return null;
|
|
1958
|
+
const NON_VISUAL = new Set(['STYLE', 'SCRIPT', 'LINK', 'META', 'TEMPLATE']);
|
|
1959
|
+
const visual = [];
|
|
1960
|
+
for (const child of variantDiv.children) {
|
|
1961
|
+
if (!NON_VISUAL.has(child.tagName)) visual.push(child);
|
|
1962
|
+
}
|
|
1963
|
+
if (visual.length === 1) return visual[0];
|
|
1964
|
+
return variantDiv;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
// Hold window.scrollY at a fixed value across DOM mutations inside the
|
|
1968
|
+
// session's wrapper (HMR patches, variant inserts, cycle swaps).
|
|
1969
|
+
function startScrollLock(sessionId, initialTargetY) {
|
|
1970
|
+
stopScrollLock();
|
|
1971
|
+
scrollLockTargetY = typeof initialTargetY === 'number' && isFinite(initialTargetY)
|
|
1972
|
+
? initialTargetY
|
|
1973
|
+
: window.scrollY;
|
|
1974
|
+
console.log('[impeccable.scroll] startScrollLock', { sessionId, scrollY: window.scrollY, targetY: scrollLockTargetY, initialOverride: initialTargetY });
|
|
1975
|
+
|
|
1976
|
+
try { history.scrollRestoration = 'manual'; } catch {}
|
|
1977
|
+
|
|
1978
|
+
const prevHtmlAnchor = document.documentElement.style.overflowAnchor;
|
|
1979
|
+
const prevBodyAnchor = document.body.style.overflowAnchor;
|
|
1980
|
+
document.documentElement.style.overflowAnchor = 'none';
|
|
1981
|
+
document.body.style.overflowAnchor = 'none';
|
|
1982
|
+
|
|
1983
|
+
const correct = (why) => {
|
|
1984
|
+
scrollLockRaf = null;
|
|
1985
|
+
if (scrollLockTargetY == null) return;
|
|
1986
|
+
const before = window.scrollY;
|
|
1987
|
+
const delta = before - scrollLockTargetY;
|
|
1988
|
+
if (Math.abs(delta) < 0.5) {
|
|
1989
|
+
console.log('[impeccable.scroll] correct noop', { why, scrollY: before, targetY: scrollLockTargetY });
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1992
|
+
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
|
|
1993
|
+
console.log('[impeccable.scroll] corrected', { why, from: before, to: scrollLockTargetY, delta, nowAt: window.scrollY });
|
|
1994
|
+
};
|
|
1995
|
+
const schedule = (why) => {
|
|
1996
|
+
if (scrollLockRaf != null) return;
|
|
1997
|
+
scrollLockRaf = requestAnimationFrame(() => correct(why));
|
|
1998
|
+
};
|
|
1999
|
+
|
|
2000
|
+
scrollLockObserver = new MutationObserver((mutations) => {
|
|
2001
|
+
for (const m of mutations) {
|
|
2002
|
+
if (m.target?.closest?.('[data-impeccable-variants="' + sessionId + '"]')) {
|
|
2003
|
+
const childAdds = Array.from(m.addedNodes).map(n => n.nodeType === 1 ? (n.tagName + (n.dataset?.impeccableVariant ? ('[variant=' + n.dataset.impeccableVariant + ']') : '')) : n.nodeType).join(',');
|
|
2004
|
+
console.log('[impeccable.scroll] mutation inside wrapper', { type: m.type, target: m.target?.tagName, adds: childAdds, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
|
2005
|
+
schedule('mutation-in-wrapper');
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
for (const n of m.addedNodes) {
|
|
2009
|
+
if (n.nodeType === 1 && (n.matches?.('[data-impeccable-variants="' + sessionId + '"]') || n.querySelector?.('[data-impeccable-variants="' + sessionId + '"]'))) {
|
|
2010
|
+
console.log('[impeccable.scroll] wrapper node added', { tag: n.tagName, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
|
2011
|
+
schedule('wrapper-added');
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
});
|
|
2017
|
+
scrollLockObserver.observe(document.body, { childList: true, subtree: true });
|
|
2018
|
+
|
|
2019
|
+
scrollLockAbort = new AbortController();
|
|
2020
|
+
scrollLockAbort.signal.addEventListener('abort', () => {
|
|
2021
|
+
document.documentElement.style.overflowAnchor = prevHtmlAnchor;
|
|
2022
|
+
document.body.style.overflowAnchor = prevBodyAnchor;
|
|
2023
|
+
}, { once: true });
|
|
2024
|
+
const sig = { signal: scrollLockAbort.signal };
|
|
2025
|
+
// Track whether the most recent scroll came from a user gesture. We
|
|
2026
|
+
// gate user-scroll re-anchoring on this flag so programmatic smooth
|
|
2027
|
+
// scrolls (browser reload-restore, scrollIntoView from other scripts)
|
|
2028
|
+
// don't accidentally update our target.
|
|
2029
|
+
let userGestureAt = 0;
|
|
2030
|
+
const USER_GESTURE_WINDOW_MS = 250;
|
|
2031
|
+
|
|
2032
|
+
const reanchor = (why) => {
|
|
2033
|
+
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
|
|
2034
|
+
const prevTarget = scrollLockTargetY;
|
|
2035
|
+
scrollLockTargetY = window.scrollY;
|
|
2036
|
+
writeScrollY(scrollLockTargetY);
|
|
2037
|
+
console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
|
|
2038
|
+
};
|
|
2039
|
+
const markGesture = (why) => {
|
|
2040
|
+
userGestureAt = performance.now();
|
|
2041
|
+
reanchor(why);
|
|
2042
|
+
};
|
|
2043
|
+
window.addEventListener('wheel', () => markGesture('wheel'), { passive: true, ...sig });
|
|
2044
|
+
window.addEventListener('touchstart', () => markGesture('touchstart'), { passive: true, ...sig });
|
|
2045
|
+
window.addEventListener('touchmove', () => markGesture('touchmove'), { passive: true, ...sig });
|
|
2046
|
+
window.addEventListener('keydown', (e) => {
|
|
2047
|
+
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) markGesture('key:' + e.key);
|
|
2048
|
+
}, sig);
|
|
2049
|
+
|
|
2050
|
+
// Correct on EVERY scroll event: whether it's the browser's
|
|
2051
|
+
// post-reload animated restore or some other script calling
|
|
2052
|
+
// scrollIntoView, we want to snap back immediately. Only skip if a
|
|
2053
|
+
// user gesture fired in the last 250ms.
|
|
2054
|
+
let lastLoggedScrollY = window.scrollY;
|
|
2055
|
+
window.addEventListener('scroll', () => {
|
|
2056
|
+
const now = window.scrollY;
|
|
2057
|
+
if (Math.abs(now - lastLoggedScrollY) > 5) {
|
|
2058
|
+
console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
|
|
2059
|
+
lastLoggedScrollY = now;
|
|
2060
|
+
}
|
|
2061
|
+
if (scrollLockTargetY == null) return;
|
|
2062
|
+
if (performance.now() - userGestureAt < USER_GESTURE_WINDOW_MS) return;
|
|
2063
|
+
if (Math.abs(now - scrollLockTargetY) < 0.5) return;
|
|
2064
|
+
console.log('[impeccable.scroll] scroll-event snap', { from: now, to: scrollLockTargetY });
|
|
2065
|
+
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
|
|
2066
|
+
}, { passive: true, ...sig });
|
|
2067
|
+
|
|
2068
|
+
// Apply target synchronously, not via rAF — racing the browser's
|
|
2069
|
+
// restore or a smooth-scroll animation means we want to win now.
|
|
2070
|
+
if (Math.abs(window.scrollY - scrollLockTargetY) > 0.5) {
|
|
2071
|
+
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
|
|
2072
|
+
console.log('[impeccable.scroll] startScrollLock initial apply', { to: scrollLockTargetY });
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
function stopScrollLock() {
|
|
2077
|
+
if (scrollLockObserver) { scrollLockObserver.disconnect(); scrollLockObserver = null; }
|
|
2078
|
+
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
|
|
2079
|
+
if (scrollLockAbort) { scrollLockAbort.abort(); scrollLockAbort = null; }
|
|
2080
|
+
scrollLockTargetY = null;
|
|
2081
|
+
// NOTE: do NOT clear the persistent scroll key here. startScrollLock
|
|
2082
|
+
// calls us as a reset, and clearing the key would nuke the Go-time
|
|
2083
|
+
// scrollY that the next resume needs to read.
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
// ---------------------------------------------------------------------------
|
|
2087
|
+
// MutationObserver for progressive variant reveal
|
|
2088
|
+
// ---------------------------------------------------------------------------
|
|
2089
|
+
|
|
2090
|
+
function startVariantObserver(sessionId) {
|
|
2091
|
+
let updating = false; // re-entrancy guard
|
|
2092
|
+
|
|
2093
|
+
const obs = new MutationObserver((mutations) => {
|
|
2094
|
+
if (updating) return;
|
|
2095
|
+
|
|
2096
|
+
// Only react to mutations that add nodes with data-impeccable-variant,
|
|
2097
|
+
// or mutations inside the variant wrapper. Ignore our own bar/UI changes.
|
|
2098
|
+
let dominated = false;
|
|
2099
|
+
for (const m of mutations) {
|
|
2100
|
+
if (m.target.closest?.('[data-impeccable-variants]')) { dominated = true; break; }
|
|
2101
|
+
for (const n of m.addedNodes) {
|
|
2102
|
+
if (n.nodeType !== 1) continue;
|
|
2103
|
+
// Direct hit: the added node itself is the wrapper or a variant.
|
|
2104
|
+
if (n.dataset?.impeccableVariants || n.dataset?.impeccableVariant) {
|
|
2105
|
+
dominated = true; break;
|
|
2106
|
+
}
|
|
2107
|
+
// Subtree hit: framework HMR (notably SvelteKit) sometimes replaces
|
|
2108
|
+
// a whole subtree where the wrapper is a descendant of the added
|
|
2109
|
+
// node. Without this check, the observer ignores those mutations
|
|
2110
|
+
// and the session stays in GENERATING forever.
|
|
2111
|
+
if (n.querySelector?.('[data-impeccable-variants],[data-impeccable-variant]')) {
|
|
2112
|
+
dominated = true; break;
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
if (dominated) break;
|
|
2116
|
+
}
|
|
2117
|
+
if (!dominated) return;
|
|
2118
|
+
|
|
2119
|
+
const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
|
|
2120
|
+
if (!wrapper) return;
|
|
2121
|
+
|
|
2122
|
+
// Re-anchor selectedElement if it was detached by live-wrap's HMR swap.
|
|
2123
|
+
// Without this, the shader / highlight / bar track a zero-rect phantom
|
|
2124
|
+
// and the overlay appears frozen.
|
|
2125
|
+
if (selectedElement && !document.body.contains(selectedElement)) {
|
|
2126
|
+
selectedElement = pickVariantContent(wrapper, 'original') || wrapper;
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
|
|
2130
|
+
const count = variants.length;
|
|
2131
|
+
|
|
2132
|
+
// Nothing new
|
|
2133
|
+
if (count <= arrivedVariants) return;
|
|
2134
|
+
|
|
2135
|
+
updating = true;
|
|
2136
|
+
arrivedVariants = count;
|
|
2137
|
+
if (visibleVariant === 0 && arrivedVariants > 0) {
|
|
2138
|
+
const saved = loadSession();
|
|
2139
|
+
const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0;
|
|
2140
|
+
visibleVariant = savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1;
|
|
2141
|
+
showVariantInDOM(sessionId, visibleVariant);
|
|
2142
|
+
// showVariantInDOM hid the original (display:none); if we were still
|
|
2143
|
+
// anchored to the original's content, its boundingRect is now zero
|
|
2144
|
+
// and the bar snaps to (0,0). Re-point at the visible variant instead.
|
|
2145
|
+
const visEl = pickVariantContent(wrapper, visibleVariant);
|
|
2146
|
+
if (visEl) selectedElement = visEl;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
const expected = parseInt(wrapper.dataset.impeccableVariantCount || '0');
|
|
2150
|
+
if (expected > 0) expectedVariants = expected;
|
|
2151
|
+
|
|
2152
|
+
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
|
|
2153
|
+
state = 'CYCLING';
|
|
2154
|
+
hideShaderOverlay();
|
|
2155
|
+
updateBarContent('cycling');
|
|
2156
|
+
refreshParamsPanel();
|
|
2157
|
+
} else if (state === 'GENERATING') {
|
|
2158
|
+
updateBarContent('generating');
|
|
2159
|
+
}
|
|
2160
|
+
saveSession();
|
|
2161
|
+
queueCheckpoint(state === 'CYCLING' ? 'variants_ready' : 'variants_progress');
|
|
2162
|
+
updating = false;
|
|
2163
|
+
});
|
|
2164
|
+
|
|
2165
|
+
obs.observe(document.body, { childList: true, subtree: true });
|
|
2166
|
+
return obs;
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
// ---------------------------------------------------------------------------
|
|
2170
|
+
// Bar scroll tracking
|
|
2171
|
+
// ---------------------------------------------------------------------------
|
|
2172
|
+
|
|
2173
|
+
function startScrollTracking() {
|
|
2174
|
+
function tick() {
|
|
2175
|
+
if (state === 'CONFIGURING' || state === 'GENERATING' || state === 'CYCLING') {
|
|
2176
|
+
positionBar();
|
|
2177
|
+
showHighlight(selectedElement);
|
|
2178
|
+
if (tuneOpen) positionParamsPanel();
|
|
2179
|
+
}
|
|
2180
|
+
if (annotActive) positionAnnotOverlay(selectedElement);
|
|
2181
|
+
// Shader overlay (via debug P toggle or generation) is repositioned
|
|
2182
|
+
// by its own branch below; debug no longer has a separate overlay.
|
|
2183
|
+
if (shaderState) positionShaderOverlay();
|
|
2184
|
+
scrollRaf = requestAnimationFrame(tick);
|
|
2185
|
+
}
|
|
2186
|
+
scrollRaf = requestAnimationFrame(tick);
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
function stopScrollTracking() {
|
|
2190
|
+
if (scrollRaf) { cancelAnimationFrame(scrollRaf); scrollRaf = null; }
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
// ---------------------------------------------------------------------------
|
|
2194
|
+
// SSE (server→browser) + fetch POST (browser→server)
|
|
2195
|
+
// Zero-dependency replacement for WebSocket.
|
|
2196
|
+
// ---------------------------------------------------------------------------
|
|
2197
|
+
|
|
2198
|
+
let evtSource = null;
|
|
2199
|
+
let sseRetries = 0;
|
|
2200
|
+
const SSE_MAX_RETRIES = 20; // generous: heartbeats keep the connection alive, so retries mean real trouble
|
|
2201
|
+
|
|
2202
|
+
function connectSSE() {
|
|
2203
|
+
evtSource = new EventSource('http://localhost:' + PORT + '/events?token=' + TOKEN);
|
|
2204
|
+
|
|
2205
|
+
evtSource.onopen = () => {
|
|
2206
|
+
sseRetries = 0; // reset on successful (re)connect
|
|
2207
|
+
};
|
|
2208
|
+
|
|
2209
|
+
evtSource.onmessage = (e) => {
|
|
2210
|
+
sseRetries = 0; // reset on any successful message
|
|
2211
|
+
let msg; try { msg = JSON.parse(e.data); } catch { return; }
|
|
2212
|
+
switch (msg.type) {
|
|
2213
|
+
case 'connected':
|
|
2214
|
+
hasProjectContext = !!msg.hasProjectContext;
|
|
2215
|
+
if (!hasProjectContext) showToast('No PRODUCT.md found. Variants will be brand-agnostic. Run /impeccable teach to generate one.', 7000);
|
|
2216
|
+
console.log('[impeccable] Live mode connected.');
|
|
2217
|
+
if (state === 'IDLE') state = 'PICKING';
|
|
2218
|
+
break;
|
|
2219
|
+
case 'done':
|
|
2220
|
+
// Variants already arrived via HMR → normal transition.
|
|
2221
|
+
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
|
|
2222
|
+
if (state === 'GENERATING') {
|
|
2223
|
+
state = 'CYCLING';
|
|
2224
|
+
updateBarContent('cycling');
|
|
2225
|
+
refreshParamsPanel();
|
|
2226
|
+
}
|
|
2227
|
+
break;
|
|
2228
|
+
}
|
|
2229
|
+
// Variants are in source but not in the DOM yet. Common when the
|
|
2230
|
+
// picked element lived inside conditional render (closed modal,
|
|
2231
|
+
// hidden tab, a route the user navigated away from). The variant
|
|
2232
|
+
// MutationObserver stays armed and auto-transitions to CYCLING
|
|
2233
|
+
// the moment the wrapper actually mounts. Nudge the user toward
|
|
2234
|
+
// that path with a toast — better than the prior force-reload
|
|
2235
|
+
// which reset framework state and left the session stuck.
|
|
2236
|
+
setTimeout(() => {
|
|
2237
|
+
if (arrivedVariants >= expectedVariants && expectedVariants > 0) return;
|
|
2238
|
+
if (state !== 'GENERATING') return;
|
|
2239
|
+
showToast(
|
|
2240
|
+
"Variants ready. If the picked element isn't visible, retrace the path that revealed it — they'll appear automatically.",
|
|
2241
|
+
15000,
|
|
2242
|
+
);
|
|
2243
|
+
}, 2000);
|
|
2244
|
+
break;
|
|
2245
|
+
case 'error':
|
|
2246
|
+
console.error('[impeccable] Error:', msg.message);
|
|
2247
|
+
showToast('Error: ' + msg.message, 5000);
|
|
2248
|
+
hideBar();
|
|
2249
|
+
state = 'PICKING';
|
|
2250
|
+
break;
|
|
2251
|
+
}
|
|
2252
|
+
};
|
|
2253
|
+
|
|
2254
|
+
evtSource.onerror = () => {
|
|
2255
|
+
sseRetries++;
|
|
2256
|
+
if (sseRetries <= SSE_MAX_RETRIES) {
|
|
2257
|
+
console.log('[impeccable] SSE connection lost. Retry ' + sseRetries + '/' + SSE_MAX_RETRIES + '...');
|
|
2258
|
+
return; // EventSource auto-reconnects
|
|
2259
|
+
}
|
|
2260
|
+
// Server is gone. Clean up gracefully.
|
|
2261
|
+
console.log('[impeccable] Live server unreachable. Cleaning up UI.');
|
|
2262
|
+
evtSource.close();
|
|
2263
|
+
evtSource = null;
|
|
2264
|
+
handleServerLost();
|
|
2265
|
+
};
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
/** Server died or became unreachable. Reset UI to a clean state. */
|
|
2269
|
+
function handleServerLost() {
|
|
2270
|
+
const recoveryState = currentSessionId ? state : 'IDLE';
|
|
2271
|
+
if (state === 'GENERATING' || state === 'CYCLING' || state === 'SAVING') {
|
|
2272
|
+
showToast('Live server disconnected. Session ended.', 5000);
|
|
2273
|
+
}
|
|
2274
|
+
hideBar();
|
|
2275
|
+
hideHighlight();
|
|
2276
|
+
hideShaderOverlay();
|
|
2277
|
+
hideAnnotOverlay();
|
|
2278
|
+
stopScrollTracking();
|
|
2279
|
+
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
|
|
2280
|
+
stopScrollLock();
|
|
2281
|
+
// Preserve local session state on server loss. The durable journal is the
|
|
2282
|
+
// source of truth, but localStorage plus the variant wrapper lets the UI
|
|
2283
|
+
// resume after a helper restart or page reload instead of treating a
|
|
2284
|
+
// transient disconnect as an explicit discard.
|
|
2285
|
+
selectedElement = null;
|
|
2286
|
+
selectedAction = 'impeccable';
|
|
2287
|
+
state = recoveryState;
|
|
2288
|
+
if (currentSessionId) saveSession();
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
function sendEvent(msg, opts) {
|
|
2292
|
+
msg.token = TOKEN;
|
|
2293
|
+
function handleFailure(err) {
|
|
2294
|
+
console.error('[impeccable] Failed to send event:', err);
|
|
2295
|
+
if (opts && opts.throwOnError) throw err;
|
|
2296
|
+
return null;
|
|
2297
|
+
}
|
|
2298
|
+
return fetch('http://localhost:' + PORT + '/events', {
|
|
2299
|
+
method: 'POST',
|
|
2300
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2301
|
+
body: JSON.stringify(msg),
|
|
2302
|
+
}).then(res => {
|
|
2303
|
+
if (res.ok) return res;
|
|
2304
|
+
return handleFailure(new Error('HTTP ' + res.status + ' ' + res.statusText));
|
|
2305
|
+
}).catch(handleFailure);
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
function checkpointPayload(reason) {
|
|
2309
|
+
return {
|
|
2310
|
+
type: 'checkpoint',
|
|
2311
|
+
id: currentSessionId,
|
|
2312
|
+
revision: sessionState.nextCheckpointRevision(),
|
|
2313
|
+
owner: browserOwner,
|
|
2314
|
+
phase: String(state || '').toLowerCase(),
|
|
2315
|
+
reason,
|
|
2316
|
+
pageUrl: location.pathname,
|
|
2317
|
+
expectedVariants,
|
|
2318
|
+
arrivedVariants,
|
|
2319
|
+
visibleVariant,
|
|
2320
|
+
paramValues: { ...paramsCurrentValues },
|
|
2321
|
+
};
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
function sendCheckpoint(reason) {
|
|
2325
|
+
if (!currentSessionId) return Promise.resolve(null);
|
|
2326
|
+
return sendEvent(checkpointPayload(reason)).catch(() => null);
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
function queueCheckpoint(reason) {
|
|
2330
|
+
if (!currentSessionId) return;
|
|
2331
|
+
if (checkpointTimer) clearTimeout(checkpointTimer);
|
|
2332
|
+
checkpointTimer = setTimeout(() => {
|
|
2333
|
+
checkpointTimer = null;
|
|
2334
|
+
sendCheckpoint(reason);
|
|
2335
|
+
}, 120);
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
// ---------------------------------------------------------------------------
|
|
2339
|
+
// Event handlers
|
|
2340
|
+
// ---------------------------------------------------------------------------
|
|
2341
|
+
|
|
2342
|
+
function handleMouseMove(e) {
|
|
2343
|
+
if (state !== 'PICKING' || !pickActive) return;
|
|
2344
|
+
const target = document.elementFromPoint(e.clientX, e.clientY);
|
|
2345
|
+
if (!target || !pickable(target) || target === hoveredElement) return;
|
|
2346
|
+
hoveredElement = target;
|
|
2347
|
+
showHighlight(target);
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
function handleClick(e) {
|
|
2351
|
+
// Close action picker on any outside click
|
|
2352
|
+
if (pickerEl?.style.display !== 'none' && !own(e.target)) {
|
|
2353
|
+
hideActionPicker();
|
|
2354
|
+
}
|
|
2355
|
+
// Close Tune popover on outside click (anything outside panel + bar)
|
|
2356
|
+
if (tuneOpen && paramsPanelEl && !paramsPanelEl.contains(e.target) && barEl && !barEl.contains(e.target)) {
|
|
2357
|
+
closeTunePopover();
|
|
2358
|
+
}
|
|
2359
|
+
// In CONFIGURING: click outside the bar and selected element returns to PICKING
|
|
2360
|
+
if (state === 'CONFIGURING' && !own(e.target) && selectedElement && !selectedElement.contains(e.target)) {
|
|
2361
|
+
hideBar();
|
|
2362
|
+
stopScrollTracking();
|
|
2363
|
+
hideAnnotOverlay();
|
|
2364
|
+
clearAnnotations();
|
|
2365
|
+
state = 'PICKING';
|
|
2366
|
+
hoveredElement = null;
|
|
2367
|
+
hideHighlight();
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
if (state !== 'PICKING' || !pickActive) return;
|
|
2371
|
+
if (own(e.target)) return;
|
|
2372
|
+
if (!hoveredElement || !pickable(hoveredElement)) return;
|
|
2373
|
+
e.preventDefault();
|
|
2374
|
+
e.stopPropagation();
|
|
2375
|
+
selectedElement = hoveredElement;
|
|
2376
|
+
state = 'CONFIGURING';
|
|
2377
|
+
showHighlight(selectedElement);
|
|
2378
|
+
clearAnnotations();
|
|
2379
|
+
showAnnotOverlay(selectedElement);
|
|
2380
|
+
showBar('configure');
|
|
2381
|
+
startScrollTracking();
|
|
2382
|
+
maybePrefetchPage();
|
|
2383
|
+
maybeWarnConditionalAncestor(selectedElement);
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
/**
|
|
2387
|
+
* Surface a brief, non-blocking heads-up when the picked element lives
|
|
2388
|
+
* inside a container whose visibility is gated by ephemeral state — modals,
|
|
2389
|
+
* collapsible panels, popovers, off-screen tab panels. If HMR remounts the
|
|
2390
|
+
* parent during generation (Vite Fast Refresh, SvelteKit page reload), the
|
|
2391
|
+
* variants land in source but stay invisible until the user re-opens the
|
|
2392
|
+
* container. Telling the user upfront is much friendlier than the silent
|
|
2393
|
+
* timeout-then-toast that they'd otherwise hit.
|
|
2394
|
+
*
|
|
2395
|
+
* Heuristic, intentionally narrow — only fires for unambiguous cases so
|
|
2396
|
+
* we don't cry wolf on every nested element.
|
|
2397
|
+
*/
|
|
2398
|
+
function maybeWarnConditionalAncestor(el) {
|
|
2399
|
+
let node = el?.parentElement;
|
|
2400
|
+
let depth = 0;
|
|
2401
|
+
while (node && depth < 12) {
|
|
2402
|
+
// 1. Active dialog / modal
|
|
2403
|
+
if (node.getAttribute && node.getAttribute('role') === 'dialog'
|
|
2404
|
+
&& node.getAttribute('aria-modal') === 'true') {
|
|
2405
|
+
showToast('Heads up: this element lives inside a dialog. If state resets during generation, you may need to re-open it.', 6000);
|
|
2406
|
+
return;
|
|
2407
|
+
}
|
|
2408
|
+
// 2. Common Radix / shadcn / headless-ui open-state attribute
|
|
2409
|
+
if (node.dataset && node.dataset.state === 'open') {
|
|
2410
|
+
showToast('Heads up: this element lives inside an open panel. If state resets during generation, you may need to re-open it.', 6000);
|
|
2411
|
+
return;
|
|
2412
|
+
}
|
|
2413
|
+
// 3. Tab panel — only meaningful when the page also shows ANOTHER
|
|
2414
|
+
// tab as selected. A single tabpanel with no tablist is just a static
|
|
2415
|
+
// section in disguise and isn't conditional.
|
|
2416
|
+
if (node.getAttribute && node.getAttribute('role') === 'tabpanel') {
|
|
2417
|
+
const list = document.querySelector('[role="tablist"]');
|
|
2418
|
+
if (list) {
|
|
2419
|
+
const tabs = list.querySelectorAll('[role="tab"]');
|
|
2420
|
+
if (tabs.length > 1) {
|
|
2421
|
+
showToast('Heads up: this element lives in a tab panel. If state resets during generation, switch back to this tab.', 6000);
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
// 4. Collapsible: aria-expanded sibling. Look for the trigger button.
|
|
2427
|
+
if (node.id) {
|
|
2428
|
+
const trigger = document.querySelector(`[aria-controls="${CSS.escape(node.id)}"][aria-expanded="true"]`);
|
|
2429
|
+
if (trigger) {
|
|
2430
|
+
showToast('Heads up: this element lives inside an expandable section. If state resets during generation, re-expand it.', 6000);
|
|
2431
|
+
return;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
node = node.parentElement;
|
|
2435
|
+
depth++;
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
// Fire a lightweight prefetch event the first time the user selects an
|
|
2440
|
+
// element on a given route. The agent uses this to Read the underlying file
|
|
2441
|
+
// into context before Go is hit, shaving the read off the critical path.
|
|
2442
|
+
// Dedupe per session by pathname — clicking around on the same page doesn't
|
|
2443
|
+
// re-fire.
|
|
2444
|
+
//
|
|
2445
|
+
// DISABLED: quick-Go workflows pay an extra harness round trip because
|
|
2446
|
+
// prefetch + generate arrive as two events instead of one. Re-enable with
|
|
2447
|
+
// a browser-side debounce (~800–1000ms, cancelled on Go) if we want to
|
|
2448
|
+
// resurrect this. Server validator and skill dispatch remain in place so
|
|
2449
|
+
// flipping this flag is the only change needed.
|
|
2450
|
+
const PREFETCH_ENABLED = false;
|
|
2451
|
+
const prefetchedPaths = new Set();
|
|
2452
|
+
function maybePrefetchPage() {
|
|
2453
|
+
if (!PREFETCH_ENABLED) return;
|
|
2454
|
+
const path = location.pathname;
|
|
2455
|
+
if (prefetchedPaths.has(path)) return;
|
|
2456
|
+
prefetchedPaths.add(path);
|
|
2457
|
+
sendEvent({ type: 'prefetch', pageUrl: path });
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
function handleKeyDown(e) {
|
|
2461
|
+
// When the annotation input is focused, let it handle its own keys.
|
|
2462
|
+
if (annotEditing && annotEditing.input && e.target === annotEditing.input) return;
|
|
2463
|
+
if (e.key === 'Escape') {
|
|
2464
|
+
e.preventDefault();
|
|
2465
|
+
if (pickerEl?.style.display !== 'none') { hideActionPicker(); return; }
|
|
2466
|
+
if (state === 'CONFIGURING') { hideBar(); stopScrollTracking(); hideAnnotOverlay(); clearAnnotations(); state = 'PICKING'; return; }
|
|
2467
|
+
if (state === 'CYCLING') { handleDiscard(); return; }
|
|
2468
|
+
if (state === 'SAVING' || state === 'CONFIRMED') return; // don't interrupt
|
|
2469
|
+
if (state === 'PICKING') {
|
|
2470
|
+
// Use togglePick so the "Pick" button in the global bar also flips
|
|
2471
|
+
// off, otherwise the bar stays lit while nothing else is active.
|
|
2472
|
+
if (pickActive) togglePick();
|
|
2473
|
+
else { hideHighlight(); state = 'IDLE'; }
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
// Arrow/Enter nav works in PICKING (hover) and CONFIGURING (selected, input empty)
|
|
2479
|
+
var navEl = (state === 'PICKING') ? hoveredElement : (state === 'CONFIGURING') ? selectedElement : null;
|
|
2480
|
+
if (navEl && (e.key === 'ArrowUp' || e.key === 'ArrowDown' || (e.key === 'Enter' && state === 'PICKING'))) {
|
|
2481
|
+
let next = null;
|
|
2482
|
+
if (e.key === 'ArrowDown' && !e.shiftKey) {
|
|
2483
|
+
next = navEl.nextElementSibling;
|
|
2484
|
+
while (next && !pickable(next)) next = next.nextElementSibling;
|
|
2485
|
+
} else if (e.key === 'ArrowUp' && !e.shiftKey) {
|
|
2486
|
+
next = navEl.previousElementSibling;
|
|
2487
|
+
while (next && !pickable(next)) next = next.previousElementSibling;
|
|
2488
|
+
} else if (e.key === 'ArrowUp' && e.shiftKey) {
|
|
2489
|
+
next = navEl.parentElement;
|
|
2490
|
+
if (next && !pickable(next)) next = null;
|
|
2491
|
+
} else if (e.key === 'ArrowDown' && e.shiftKey) {
|
|
2492
|
+
next = navEl.firstElementChild;
|
|
2493
|
+
while (next && !pickable(next)) next = next.nextElementSibling;
|
|
2494
|
+
} else if (e.key === 'Enter') {
|
|
2495
|
+
e.preventDefault();
|
|
2496
|
+
selectedElement = hoveredElement;
|
|
2497
|
+
state = 'CONFIGURING';
|
|
2498
|
+
showHighlight(selectedElement);
|
|
2499
|
+
clearAnnotations();
|
|
2500
|
+
showAnnotOverlay(selectedElement);
|
|
2501
|
+
showBar('configure');
|
|
2502
|
+
startScrollTracking();
|
|
2503
|
+
return;
|
|
2504
|
+
}
|
|
2505
|
+
if (next) {
|
|
2506
|
+
e.preventDefault();
|
|
2507
|
+
if (state === 'PICKING') {
|
|
2508
|
+
hoveredElement = next;
|
|
2509
|
+
} else {
|
|
2510
|
+
// CONFIGURING: re-select the new element and refresh the bar
|
|
2511
|
+
selectedElement = next;
|
|
2512
|
+
clearAnnotations();
|
|
2513
|
+
showAnnotOverlay(next);
|
|
2514
|
+
showBar('configure');
|
|
2515
|
+
startScrollTracking();
|
|
2516
|
+
}
|
|
2517
|
+
showHighlight(next);
|
|
2518
|
+
next.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
2519
|
+
}
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
if (state === 'CYCLING') {
|
|
2524
|
+
if (e.key === 'ArrowLeft') { e.preventDefault(); cycleVariant(-1); }
|
|
2525
|
+
if (e.key === 'ArrowRight') { e.preventDefault(); cycleVariant(1); }
|
|
2526
|
+
if (e.key === 'Enter') { e.preventDefault(); handleAccept(); }
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
function handleGo() {
|
|
2531
|
+
if (!selectedElement || state !== 'CONFIGURING') return;
|
|
2532
|
+
const input = document.getElementById(PREFIX + '-input');
|
|
2533
|
+
const prompt = input ? input.value.trim() : '';
|
|
2534
|
+
|
|
2535
|
+
// Commit any pending pin edit BEFORE we snapshot annotations.
|
|
2536
|
+
if (annotEditing) finalizeEditingPin();
|
|
2537
|
+
|
|
2538
|
+
currentSessionId = id8();
|
|
2539
|
+
expectedVariants = selectedCount;
|
|
2540
|
+
arrivedVariants = 0;
|
|
2541
|
+
visibleVariant = 0;
|
|
2542
|
+
|
|
2543
|
+
// Flip to GENERATING immediately so the bar morphs without waiting on
|
|
2544
|
+
// capture + upload. The event is emitted from captureAndEmit() once the
|
|
2545
|
+
// screenshot is uploaded (or capture fails — we still emit, just without
|
|
2546
|
+
// screenshotPath).
|
|
2547
|
+
const elForCapture = selectedElement;
|
|
2548
|
+
const captureRect = elForCapture.getBoundingClientRect();
|
|
2549
|
+
const snapshot = {
|
|
2550
|
+
comments: annotState.comments.map(c => ({ x: c.x, y: c.y, text: c.text })),
|
|
2551
|
+
strokes: annotState.strokes.map(s => ({ points: s.points.map(p => [p[0], p[1]]) })),
|
|
2552
|
+
};
|
|
2553
|
+
const basePayload = {
|
|
2554
|
+
type: 'generate', id: currentSessionId,
|
|
2555
|
+
action: selectedAction,
|
|
2556
|
+
freeformPrompt: prompt || undefined,
|
|
2557
|
+
count: selectedCount,
|
|
2558
|
+
pageUrl: location.pathname,
|
|
2559
|
+
element: extractContext(elForCapture),
|
|
2560
|
+
};
|
|
2561
|
+
if (snapshot.comments.length > 0) basePayload.comments = snapshot.comments;
|
|
2562
|
+
if (snapshot.strokes.length > 0) basePayload.strokes = snapshot.strokes;
|
|
2563
|
+
|
|
2564
|
+
// Hide the interactive overlay so it doesn't linger during generation.
|
|
2565
|
+
hideAnnotOverlay();
|
|
2566
|
+
clearAnnotations();
|
|
2567
|
+
|
|
2568
|
+
state = 'GENERATING';
|
|
2569
|
+
showBar('generating');
|
|
2570
|
+
saveSession();
|
|
2571
|
+
sendCheckpoint('generate_started');
|
|
2572
|
+
writeScrollY(window.scrollY);
|
|
2573
|
+
if (variantObserver) variantObserver.disconnect();
|
|
2574
|
+
variantObserver = startVariantObserver(currentSessionId);
|
|
2575
|
+
console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
|
|
2576
|
+
startScrollLock(currentSessionId);
|
|
2577
|
+
|
|
2578
|
+
captureAndEmit(elForCapture, basePayload, snapshot, captureRect);
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
// ---------------------------------------------------------------------------
|
|
2582
|
+
// Screenshot capture + upload
|
|
2583
|
+
// ---------------------------------------------------------------------------
|
|
2584
|
+
|
|
2585
|
+
let msLoadPromise = null;
|
|
2586
|
+
function loadModernScreenshot() {
|
|
2587
|
+
if (window.modernScreenshot) return Promise.resolve(window.modernScreenshot);
|
|
2588
|
+
if (msLoadPromise) return msLoadPromise;
|
|
2589
|
+
msLoadPromise = new Promise((resolve, reject) => {
|
|
2590
|
+
const s = document.createElement('script');
|
|
2591
|
+
s.src = LIVE_ORIGIN + '/modern-screenshot.js';
|
|
2592
|
+
s.onload = () => resolve(window.modernScreenshot);
|
|
2593
|
+
s.onerror = () => { msLoadPromise = null; reject(new Error('modern-screenshot failed to load')); };
|
|
2594
|
+
document.head.appendChild(s);
|
|
2595
|
+
});
|
|
2596
|
+
return msLoadPromise;
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
// Collect @font-face rules from every stylesheet on the page. Cross-origin
|
|
2600
|
+
// sheets (Google Fonts, Typekit, etc.) throw SecurityError on .cssRules
|
|
2601
|
+
// access, so modern-screenshot can't embed them on its own — the resulting
|
|
2602
|
+
// SVG falls back to system fonts and text re-wraps + renders with different
|
|
2603
|
+
// weight. We fetch the raw CSS text (CORS-permitted for these providers),
|
|
2604
|
+
// extract @font-face blocks, inline the referenced font files as base64
|
|
2605
|
+
// data URIs (SVGs rasterized via canvas can't fetch external resources,
|
|
2606
|
+
// so URLs inside the SVG silently fail without this), and pass the result
|
|
2607
|
+
// to modern-screenshot as font.cssText.
|
|
2608
|
+
const FONT_EXT_RE = /\.(woff2?|ttf|otf|eot)(\?.*)?$/i;
|
|
2609
|
+
const FONT_MIME = {
|
|
2610
|
+
woff2: 'font/woff2', woff: 'font/woff', ttf: 'font/ttf', otf: 'font/otf', eot: 'application/vnd.ms-fontobject',
|
|
2611
|
+
};
|
|
2612
|
+
function bufferToBase64(buf) {
|
|
2613
|
+
const bytes = new Uint8Array(buf);
|
|
2614
|
+
let binary = '';
|
|
2615
|
+
const CHUNK = 0x8000;
|
|
2616
|
+
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
2617
|
+
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK));
|
|
2618
|
+
}
|
|
2619
|
+
return btoa(binary);
|
|
2620
|
+
}
|
|
2621
|
+
async function inlineFontUrls(cssText) {
|
|
2622
|
+
const urlRe = /url\((['"]?)(https?:\/\/[^'")\s]+)\1\)/g;
|
|
2623
|
+
const urls = new Set();
|
|
2624
|
+
let m;
|
|
2625
|
+
while ((m = urlRe.exec(cssText))) {
|
|
2626
|
+
if (FONT_EXT_RE.test(m[2])) urls.add(m[2]);
|
|
2627
|
+
}
|
|
2628
|
+
const map = new Map();
|
|
2629
|
+
await Promise.all([...urls].map(async (url) => {
|
|
2630
|
+
try {
|
|
2631
|
+
const res = await fetch(url);
|
|
2632
|
+
if (!res.ok) return;
|
|
2633
|
+
const buf = await res.arrayBuffer();
|
|
2634
|
+
const ext = url.toLowerCase().match(FONT_EXT_RE)?.[1] || 'woff2';
|
|
2635
|
+
const mime = FONT_MIME[ext] || 'application/octet-stream';
|
|
2636
|
+
map.set(url, 'data:' + mime + ';base64,' + bufferToBase64(buf));
|
|
2637
|
+
} catch { /* skip; fall through to URL */ }
|
|
2638
|
+
}));
|
|
2639
|
+
return cssText.replace(urlRe, (orig, q, url) => {
|
|
2640
|
+
const data = map.get(url);
|
|
2641
|
+
return data ? 'url(' + q + data + q + ')' : orig;
|
|
2642
|
+
});
|
|
2643
|
+
}
|
|
2644
|
+
async function collectFontCssText() {
|
|
2645
|
+
const chunks = [];
|
|
2646
|
+
const fontFaceRe = /@font-face\s*\{[^}]*\}/g;
|
|
2647
|
+
for (const sheet of document.styleSheets) {
|
|
2648
|
+
try {
|
|
2649
|
+
const rules = sheet.cssRules;
|
|
2650
|
+
for (const rule of rules) {
|
|
2651
|
+
if (rule.constructor.name === 'CSSFontFaceRule' || rule.cssText?.startsWith('@font-face')) {
|
|
2652
|
+
chunks.push(rule.cssText);
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
} catch {
|
|
2656
|
+
if (!sheet.href) continue;
|
|
2657
|
+
try {
|
|
2658
|
+
const res = await fetch(sheet.href);
|
|
2659
|
+
if (!res.ok) continue;
|
|
2660
|
+
const text = await res.text();
|
|
2661
|
+
let m2;
|
|
2662
|
+
while ((m2 = fontFaceRe.exec(text))) chunks.push(m2[0]);
|
|
2663
|
+
} catch { /* ignore; capture is best-effort */ }
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
if (chunks.length === 0) return '';
|
|
2667
|
+
return inlineFontUrls(chunks.join('\n'));
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
// True if `s` is a computed color string that renders as nothing
|
|
2671
|
+
// (explicit `transparent`, or `rgba(...)` with alpha 0).
|
|
2672
|
+
function isTransparentColor(s) {
|
|
2673
|
+
if (!s) return true;
|
|
2674
|
+
if (s === 'transparent') return true;
|
|
2675
|
+
const m = /rgba?\(([^)]+)\)/.exec(s);
|
|
2676
|
+
if (!m) return false;
|
|
2677
|
+
const parts = m[1].split(',').map((p) => p.trim());
|
|
2678
|
+
if (parts.length === 4) return parseFloat(parts[3]) === 0;
|
|
2679
|
+
return false;
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
// modern-screenshot force-sets `background-color: X !important` on the
|
|
2683
|
+
// cloned root whenever `backgroundColor` is passed, clobbering the
|
|
2684
|
+
// element's own background. So we only pass it when the element is
|
|
2685
|
+
// genuinely transparent (no own color, no own image) — in that case
|
|
2686
|
+
// we resolve up the DOM to the nearest opaque ancestor so the capture
|
|
2687
|
+
// sits on the page's real background instead of rendering black.
|
|
2688
|
+
function resolveCanvasBackground(el) {
|
|
2689
|
+
const own = getComputedStyle(el);
|
|
2690
|
+
if (!isTransparentColor(own.backgroundColor)) return null;
|
|
2691
|
+
if (own.backgroundImage && own.backgroundImage !== 'none') return null;
|
|
2692
|
+
let node = el.parentElement;
|
|
2693
|
+
while (node) {
|
|
2694
|
+
const cs = getComputedStyle(node);
|
|
2695
|
+
if (!isTransparentColor(cs.backgroundColor)) return cs.backgroundColor;
|
|
2696
|
+
node = node.parentElement;
|
|
2697
|
+
}
|
|
2698
|
+
// The walk already passed through <body> and <html>; if they had been
|
|
2699
|
+
// opaque we would have returned. Falling through with the previous
|
|
2700
|
+
// `getComputedStyle(body).backgroundColor || …` chain is a trap: that
|
|
2701
|
+
// call returns the literal string `"rgba(0, 0, 0, 0)"` for a page that
|
|
2702
|
+
// never set its own bg, which is truthy and short-circuits the chain to
|
|
2703
|
+
// transparent-black — modern-screenshot then renders the capture on a
|
|
2704
|
+
// black canvas and the shader overlay flashes solid black during load.
|
|
2705
|
+
// The browser canvas defaults to white, so we do too.
|
|
2706
|
+
return '#ffffff';
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
// Capture the element (with current annotations baked in) and return a PNG
|
|
2710
|
+
// Blob. Shared between the Go flow (uploads it to the server) and the
|
|
2711
|
+
// debug toggle (displays it as an overlay for side-by-side comparison).
|
|
2712
|
+
async function captureElementToBlob(el, snapshot, rect) {
|
|
2713
|
+
try { if (document.fonts?.ready) await document.fonts.ready; } catch {}
|
|
2714
|
+
const hasAnnotations = snapshot && (snapshot.comments.length > 0 || snapshot.strokes.length > 0);
|
|
2715
|
+
let annotNode = null;
|
|
2716
|
+
let savedPosition = null;
|
|
2717
|
+
if (hasAnnotations) {
|
|
2718
|
+
const pos = getComputedStyle(el).position;
|
|
2719
|
+
if (pos === 'static') {
|
|
2720
|
+
savedPosition = el.style.position;
|
|
2721
|
+
el.style.position = 'relative';
|
|
2722
|
+
}
|
|
2723
|
+
annotNode = buildAnnotationsForCapture(rect, snapshot);
|
|
2724
|
+
el.appendChild(annotNode);
|
|
2725
|
+
}
|
|
2726
|
+
try {
|
|
2727
|
+
const ms = await loadModernScreenshot();
|
|
2728
|
+
const fontCssText = await collectFontCssText();
|
|
2729
|
+
const backgroundColor = resolveCanvasBackground(el);
|
|
2730
|
+
return await ms.domToBlob(el, {
|
|
2731
|
+
scale: Math.min(window.devicePixelRatio || 1, 2),
|
|
2732
|
+
font: fontCssText ? { cssText: fontCssText } : undefined,
|
|
2733
|
+
...(backgroundColor ? { backgroundColor } : {}),
|
|
2734
|
+
});
|
|
2735
|
+
} finally {
|
|
2736
|
+
if (annotNode) annotNode.remove();
|
|
2737
|
+
if (savedPosition !== null) el.style.position = savedPosition;
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
async function captureAndEmit(el, basePayload, snapshot, rect) {
|
|
2742
|
+
let screenshotPath;
|
|
2743
|
+
let blob;
|
|
2744
|
+
try {
|
|
2745
|
+
blob = await captureElementToBlob(el, snapshot, rect);
|
|
2746
|
+
} catch (err) {
|
|
2747
|
+
console.warn('[impeccable] capture failed, proceeding without screenshot:', err);
|
|
2748
|
+
}
|
|
2749
|
+
// Light up the shader overlay the moment capture is ready — no reason to
|
|
2750
|
+
// wait for the upload to complete before the user sees something alive.
|
|
2751
|
+
if (blob && state === 'GENERATING') {
|
|
2752
|
+
showShaderOverlay(el, blob, rect);
|
|
2753
|
+
}
|
|
2754
|
+
// Only upload + forward the screenshot when annotations (comments/strokes)
|
|
2755
|
+
// are present. Without annotations the image is pure visual anchoring —
|
|
2756
|
+
// it biases the model toward the current rendering and works against the
|
|
2757
|
+
// three-distinct-directions brief.
|
|
2758
|
+
const hasAnnotations = snapshot && (snapshot.comments.length > 0 || snapshot.strokes.length > 0);
|
|
2759
|
+
if (blob && hasAnnotations) {
|
|
2760
|
+
try {
|
|
2761
|
+
const uploadRes = await fetch(
|
|
2762
|
+
'http://localhost:' + PORT + '/annotation?token=' + encodeURIComponent(TOKEN) +
|
|
2763
|
+
'&eventId=' + encodeURIComponent(basePayload.id),
|
|
2764
|
+
{ method: 'POST', headers: { 'Content-Type': 'image/png' }, body: blob },
|
|
2765
|
+
);
|
|
2766
|
+
if (uploadRes.ok) {
|
|
2767
|
+
const { path: p } = await uploadRes.json();
|
|
2768
|
+
screenshotPath = p;
|
|
2769
|
+
} else {
|
|
2770
|
+
console.warn('[impeccable] annotation upload failed:', uploadRes.status);
|
|
2771
|
+
}
|
|
2772
|
+
} catch (err) {
|
|
2773
|
+
console.warn('[impeccable] annotation upload failed:', err);
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
sendEvent(screenshotPath ? { ...basePayload, screenshotPath } : basePayload);
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
// ---------------------------------------------------------------------------
|
|
2780
|
+
// Shader overlay — renders the captured screenshot as a WebGL texture and
|
|
2781
|
+
// runs an editorial "ink-wash" fragment shader over it during generation.
|
|
2782
|
+
// A single rolling band sweeps top-to-bottom, desaturating + tinting magenta
|
|
2783
|
+
// and leaving a soft trail. Makes the wait feel like a letterpress scan
|
|
2784
|
+
// instead of a dead spinner.
|
|
2785
|
+
// ---------------------------------------------------------------------------
|
|
2786
|
+
|
|
2787
|
+
const SHADER_VS = `attribute vec2 a_position;
|
|
2788
|
+
attribute vec2 a_uv;
|
|
2789
|
+
varying vec2 v_uv;
|
|
2790
|
+
void main() {
|
|
2791
|
+
v_uv = a_uv;
|
|
2792
|
+
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
2793
|
+
}`;
|
|
2794
|
+
|
|
2795
|
+
const SHADER_FS = `precision highp float;
|
|
2796
|
+
uniform sampler2D u_texture;
|
|
2797
|
+
uniform float u_time;
|
|
2798
|
+
uniform vec2 u_resolution;
|
|
2799
|
+
uniform vec3 u_accent;
|
|
2800
|
+
varying vec2 v_uv;
|
|
2801
|
+
|
|
2802
|
+
// Asymmetric roller band. Product of two one-sided smoothsteps — peaks at
|
|
2803
|
+
// d=0 with a short sharp leading ramp and a longer soft trailing tail. Clean
|
|
2804
|
+
// outside the [-leadW, trailW] range (no rogue "trail=1 everywhere below"
|
|
2805
|
+
// failure that reversed-edge smoothstep would give).
|
|
2806
|
+
float bandAt(float d, float leadW, float trailW) {
|
|
2807
|
+
float above = smoothstep(-leadW, 0.0, d);
|
|
2808
|
+
float below = 1.0 - smoothstep(0.0, trailW, d);
|
|
2809
|
+
return above * below;
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
void main() {
|
|
2813
|
+
vec2 uv = v_uv;
|
|
2814
|
+
// Roller sweeps top-to-bottom with small overshoot so each cycle enters
|
|
2815
|
+
// and exits the element cleanly.
|
|
2816
|
+
float phase = fract(u_time / 3.4);
|
|
2817
|
+
float y = phase * 1.25 - 0.12;
|
|
2818
|
+
float band = bandAt(uv.y - y, 0.05, 0.32);
|
|
2819
|
+
|
|
2820
|
+
// Halftone cell grid (fixed ~10 px pitch).
|
|
2821
|
+
float cellPx = 10.0;
|
|
2822
|
+
vec2 gridUv = uv * u_resolution / cellPx;
|
|
2823
|
+
vec2 cellId = floor(gridUv);
|
|
2824
|
+
vec2 cellUv = fract(gridUv) - 0.5;
|
|
2825
|
+
vec2 sampleCenter = (cellId + 0.5) * cellPx / u_resolution;
|
|
2826
|
+
vec3 cellImg = texture2D(u_texture, sampleCenter).rgb;
|
|
2827
|
+
float luma = dot(cellImg, vec3(0.299, 0.587, 0.114));
|
|
2828
|
+
// Darker cells → bigger magenta dots (classic risograph halftone curve).
|
|
2829
|
+
float radius = sqrt(clamp(1.0 - luma, 0.0, 1.0)) * 0.56;
|
|
2830
|
+
float dotMask = smoothstep(radius + 0.06, radius, length(cellUv));
|
|
2831
|
+
vec3 paper = vec3(0.975, 0.965, 0.955);
|
|
2832
|
+
vec3 dotLayer = mix(paper, u_accent, dotMask);
|
|
2833
|
+
|
|
2834
|
+
// Blend the halftone layer in where the roller is passing; leave the
|
|
2835
|
+
// element pristine elsewhere.
|
|
2836
|
+
vec3 base = texture2D(u_texture, uv).rgb;
|
|
2837
|
+
gl_FragColor = vec4(mix(base, dotLayer, band), 1.0);
|
|
2838
|
+
}`;
|
|
2839
|
+
|
|
2840
|
+
// Editorial Magenta converted to approximate sRGB 0-1 (matches oklch(60% 0.25 350))
|
|
2841
|
+
const SHADER_ACCENT = [0.82, 0.16, 0.47];
|
|
2842
|
+
let shaderState = null; // { canvas, gl, program, texture, rafId, startTime }
|
|
2843
|
+
|
|
2844
|
+
function compileShader(gl, type, source) {
|
|
2845
|
+
const sh = gl.createShader(type);
|
|
2846
|
+
gl.shaderSource(sh, source);
|
|
2847
|
+
gl.compileShader(sh);
|
|
2848
|
+
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
|
|
2849
|
+
const info = gl.getShaderInfoLog(sh);
|
|
2850
|
+
gl.deleteShader(sh);
|
|
2851
|
+
throw new Error('shader compile failed: ' + info);
|
|
2852
|
+
}
|
|
2853
|
+
return sh;
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
function positionShaderOverlay() {
|
|
2857
|
+
if (!shaderState || !selectedElement) return;
|
|
2858
|
+
const r = selectedElement.getBoundingClientRect();
|
|
2859
|
+
Object.assign(shaderState.canvas.style, {
|
|
2860
|
+
top: r.top + 'px', left: r.left + 'px',
|
|
2861
|
+
width: r.width + 'px', height: r.height + 'px',
|
|
2862
|
+
});
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
function hideShaderOverlay() {
|
|
2866
|
+
if (!shaderState) return;
|
|
2867
|
+
if (shaderState.rafId) cancelAnimationFrame(shaderState.rafId);
|
|
2868
|
+
if (shaderState.canvas) shaderState.canvas.remove();
|
|
2869
|
+
const lose = shaderState.gl?.getExtension?.('WEBGL_lose_context');
|
|
2870
|
+
try { lose?.loseContext(); } catch {}
|
|
2871
|
+
shaderState = null;
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
async function showShaderOverlay(el, blob, rect) {
|
|
2875
|
+
hideShaderOverlay();
|
|
2876
|
+
if (!blob || !el) return;
|
|
2877
|
+
const canvas = document.createElement('canvas');
|
|
2878
|
+
canvas.id = PREFIX + '-shader';
|
|
2879
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
2880
|
+
canvas.width = Math.max(1, Math.floor(rect.width * dpr));
|
|
2881
|
+
canvas.height = Math.max(1, Math.floor(rect.height * dpr));
|
|
2882
|
+
Object.assign(canvas.style, {
|
|
2883
|
+
position: 'fixed',
|
|
2884
|
+
top: rect.top + 'px', left: rect.left + 'px',
|
|
2885
|
+
width: rect.width + 'px', height: rect.height + 'px',
|
|
2886
|
+
pointerEvents: 'none',
|
|
2887
|
+
zIndex: Z.bar - 1,
|
|
2888
|
+
});
|
|
2889
|
+
document.body.appendChild(canvas);
|
|
2890
|
+
|
|
2891
|
+
const gl = canvas.getContext('webgl', { premultipliedAlpha: false, preserveDrawingBuffer: false })
|
|
2892
|
+
|| canvas.getContext('experimental-webgl');
|
|
2893
|
+
if (!gl) {
|
|
2894
|
+
// WebGL unavailable — fall back to a plain <img> overlay so the user
|
|
2895
|
+
// still sees something meaningful during generation.
|
|
2896
|
+
canvas.remove();
|
|
2897
|
+
const img = document.createElement('img');
|
|
2898
|
+
img.src = URL.createObjectURL(blob);
|
|
2899
|
+
img.id = PREFIX + '-shader';
|
|
2900
|
+
// Copy positioning via cssText. Object.assign across CSSStyleDeclaration
|
|
2901
|
+
// throws in modern Chromium because the source's indexed properties
|
|
2902
|
+
// (style[0], [1], ...) are read-only and the engine forbids writing
|
|
2903
|
+
// them on the destination.
|
|
2904
|
+
img.style.cssText = canvas.style.cssText;
|
|
2905
|
+
img.style.outline = '2px dashed ' + C.brand;
|
|
2906
|
+
img.style.outlineOffset = '-2px';
|
|
2907
|
+
document.body.appendChild(img);
|
|
2908
|
+
shaderState = { canvas: img, gl: null, program: null, texture: null, rafId: 0, startTime: 0 };
|
|
2909
|
+
return;
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
let program, texture;
|
|
2913
|
+
try {
|
|
2914
|
+
const vs = compileShader(gl, gl.VERTEX_SHADER, SHADER_VS);
|
|
2915
|
+
const fs = compileShader(gl, gl.FRAGMENT_SHADER, SHADER_FS);
|
|
2916
|
+
program = gl.createProgram();
|
|
2917
|
+
gl.attachShader(program, vs);
|
|
2918
|
+
gl.attachShader(program, fs);
|
|
2919
|
+
gl.linkProgram(program);
|
|
2920
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
2921
|
+
throw new Error('program link failed: ' + gl.getProgramInfoLog(program));
|
|
2922
|
+
}
|
|
2923
|
+
// Full-screen quad
|
|
2924
|
+
const buf = gl.createBuffer();
|
|
2925
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
|
2926
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
|
|
2927
|
+
-1, -1, 0, 1,
|
|
2928
|
+
1, -1, 1, 1,
|
|
2929
|
+
-1, 1, 0, 0,
|
|
2930
|
+
-1, 1, 0, 0,
|
|
2931
|
+
1, -1, 1, 1,
|
|
2932
|
+
1, 1, 1, 0,
|
|
2933
|
+
]), gl.STATIC_DRAW);
|
|
2934
|
+
const posLoc = gl.getAttribLocation(program, 'a_position');
|
|
2935
|
+
const uvLoc = gl.getAttribLocation(program, 'a_uv');
|
|
2936
|
+
gl.enableVertexAttribArray(posLoc);
|
|
2937
|
+
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 16, 0);
|
|
2938
|
+
gl.enableVertexAttribArray(uvLoc);
|
|
2939
|
+
gl.vertexAttribPointer(uvLoc, 2, gl.FLOAT, false, 16, 8);
|
|
2940
|
+
} catch (err) {
|
|
2941
|
+
console.warn('[impeccable] shader setup failed:', err);
|
|
2942
|
+
canvas.remove();
|
|
2943
|
+
return;
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
// Upload the screenshot as a texture
|
|
2947
|
+
let bitmap;
|
|
2948
|
+
try {
|
|
2949
|
+
bitmap = await createImageBitmap(blob);
|
|
2950
|
+
} catch {
|
|
2951
|
+
// Safari fallback: go via a regular Image
|
|
2952
|
+
const imgUrl = URL.createObjectURL(blob);
|
|
2953
|
+
const img = new Image();
|
|
2954
|
+
img.src = imgUrl;
|
|
2955
|
+
await new Promise((r, rej) => { img.onload = r; img.onerror = rej; });
|
|
2956
|
+
bitmap = img;
|
|
2957
|
+
URL.revokeObjectURL(imgUrl);
|
|
2958
|
+
}
|
|
2959
|
+
texture = gl.createTexture();
|
|
2960
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
2961
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
2962
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
2963
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
2964
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
2965
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
2966
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, bitmap);
|
|
2967
|
+
if (bitmap.close) bitmap.close();
|
|
2968
|
+
|
|
2969
|
+
const uTime = gl.getUniformLocation(program, 'u_time');
|
|
2970
|
+
const uRes = gl.getUniformLocation(program, 'u_resolution');
|
|
2971
|
+
const uAccent = gl.getUniformLocation(program, 'u_accent');
|
|
2972
|
+
const uTex = gl.getUniformLocation(program, 'u_texture');
|
|
2973
|
+
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
2974
|
+
|
|
2975
|
+
shaderState = { canvas, gl, program, texture, rafId: 0, startTime: performance.now(), reduced };
|
|
2976
|
+
function frame() {
|
|
2977
|
+
if (!shaderState) return;
|
|
2978
|
+
const elapsed = (performance.now() - shaderState.startTime) / 1000;
|
|
2979
|
+
const t = shaderState.reduced ? 0.0 : elapsed;
|
|
2980
|
+
gl.viewport(0, 0, canvas.width, canvas.height);
|
|
2981
|
+
gl.useProgram(program);
|
|
2982
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
2983
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
2984
|
+
gl.uniform1i(uTex, 0);
|
|
2985
|
+
gl.uniform1f(uTime, t);
|
|
2986
|
+
gl.uniform2f(uRes, canvas.width, canvas.height);
|
|
2987
|
+
gl.uniform3f(uAccent, SHADER_ACCENT[0], SHADER_ACCENT[1], SHADER_ACCENT[2]);
|
|
2988
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
2989
|
+
shaderState.rafId = requestAnimationFrame(frame);
|
|
2990
|
+
}
|
|
2991
|
+
frame();
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
function handleAccept() {
|
|
2995
|
+
if (!currentSessionId || arrivedVariants === 0) return;
|
|
2996
|
+
const domVisibleVariant = readVisibleVariantFromDOM(currentSessionId);
|
|
2997
|
+
if (domVisibleVariant > 0) visibleVariant = domVisibleVariant;
|
|
2998
|
+
const acceptPayload = { type: 'accept', id: currentSessionId, variantId: String(visibleVariant) };
|
|
2999
|
+
if (Object.keys(paramsCurrentValues).length > 0) {
|
|
3000
|
+
acceptPayload.paramValues = { ...paramsCurrentValues };
|
|
3001
|
+
}
|
|
3002
|
+
// The accepted variant is already the only visible child of the wrapper
|
|
3003
|
+
// (all other variants are display:none). HMR from the source rewrite will
|
|
3004
|
+
// replace the wrapper imminently. Don't eagerly replaceChild here — React
|
|
3005
|
+
// reconciliation races with our mutation and throws NotFoundError in Next
|
|
3006
|
+
// 16 / Turbopack. Schedule a fallback that runs the manual swap only if
|
|
3007
|
+
// HMR hasn't cleaned up by then (keeps static-server flows working).
|
|
3008
|
+
const acceptedSessionId = currentSessionId;
|
|
3009
|
+
const acceptedVariant = visibleVariant;
|
|
3010
|
+
|
|
3011
|
+
state = 'SAVING';
|
|
3012
|
+
updateBarContent('saving');
|
|
3013
|
+
|
|
3014
|
+
sendEvent(acceptPayload, { throwOnError: true })
|
|
3015
|
+
.then(() => {
|
|
3016
|
+
markSessionHandled();
|
|
3017
|
+
confirmAcceptAfterReceipt();
|
|
3018
|
+
})
|
|
3019
|
+
.catch(() => {
|
|
3020
|
+
state = 'CYCLING';
|
|
3021
|
+
updateBarContent('cycling');
|
|
3022
|
+
showToast('Could not confirm accept with the live server. Session kept for recovery; try Accept again.', 5000);
|
|
3023
|
+
});
|
|
3024
|
+
|
|
3025
|
+
function confirmAcceptAfterReceipt() {
|
|
3026
|
+
state = 'CONFIRMED';
|
|
3027
|
+
updateBarContent('confirmed');
|
|
3028
|
+
scheduleAcceptCleanup();
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
function scheduleAcceptCleanup() {
|
|
3032
|
+
setTimeout(function() {
|
|
3033
|
+
hideBar();
|
|
3034
|
+
hideHighlight();
|
|
3035
|
+
stopScrollTracking();
|
|
3036
|
+
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
|
|
3037
|
+
stopScrollLock();
|
|
3038
|
+
clearScrollY();
|
|
3039
|
+
clearSession();
|
|
3040
|
+
selectedElement = null;
|
|
3041
|
+
currentSessionId = null;
|
|
3042
|
+
selectedAction = 'impeccable';
|
|
3043
|
+
state = 'PICKING';
|
|
3044
|
+
}, 1800);
|
|
3045
|
+
|
|
3046
|
+
// Static-server / no-HMR fallback: if the wrapper is still around 2s after
|
|
3047
|
+
// the cleanup above, swap it out manually. By now React has either moved
|
|
3048
|
+
// on or the app isn't React at all. Preserve the `data-impeccable-variant="N"`
|
|
3049
|
+
// div (with display:contents) so @scope rules anchored to the variant
|
|
3050
|
+
// attribute keep matching until reload replaces it with the carbonize block.
|
|
3051
|
+
setTimeout(function() {
|
|
3052
|
+
const wrapper = document.querySelector('[data-impeccable-variants="' + acceptedSessionId + '"]');
|
|
3053
|
+
if (!wrapper) return;
|
|
3054
|
+
const accepted = wrapper.querySelector('[data-impeccable-variant="' + acceptedVariant + '"]');
|
|
3055
|
+
if (accepted && accepted.firstElementChild) {
|
|
3056
|
+
const parent = wrapper.parentElement;
|
|
3057
|
+
if (!parent) return;
|
|
3058
|
+
accepted.style.display = 'contents';
|
|
3059
|
+
parent.replaceChild(accepted, wrapper);
|
|
3060
|
+
}
|
|
3061
|
+
}, 2000);
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
function handleDiscard() {
|
|
3066
|
+
if (!currentSessionId) return;
|
|
3067
|
+
sendEvent({ type: 'discard', id: currentSessionId }, { throwOnError: true })
|
|
3068
|
+
.then(() => {
|
|
3069
|
+
markSessionHandled();
|
|
3070
|
+
cleanup();
|
|
3071
|
+
})
|
|
3072
|
+
.catch(() => showToast('Could not confirm discard with the live server. Session kept for recovery.', 5000));
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
// ---------------------------------------------------------------------------
|
|
3076
|
+
// Session persistence via live-browser-session.js
|
|
3077
|
+
// ---------------------------------------------------------------------------
|
|
3078
|
+
// Survives page reloads, browser close/reopen, HMR, and accidental refreshes.
|
|
3079
|
+
|
|
3080
|
+
function saveSession() {
|
|
3081
|
+
if (!currentSessionId) return;
|
|
3082
|
+
// NOTE: scrollY is stored under a separate key (writeScrollY). Storing
|
|
3083
|
+
// it here would overwrite the Go-time value every time state changes.
|
|
3084
|
+
sessionState.saveSession({
|
|
3085
|
+
id: currentSessionId,
|
|
3086
|
+
state,
|
|
3087
|
+
action: selectedAction,
|
|
3088
|
+
count: selectedCount,
|
|
3089
|
+
expected: expectedVariants,
|
|
3090
|
+
arrived: arrivedVariants,
|
|
3091
|
+
visible: visibleVariant,
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
3094
|
+
|
|
3095
|
+
function loadSession() {
|
|
3096
|
+
return sessionState.loadSession();
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
function clearSession() {
|
|
3100
|
+
sessionState.clearSession();
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
/** Mark session as handled (accepted/discarded). The agent will clean up
|
|
3104
|
+
* the source, but until it does the wrapper is still in the HTML. This
|
|
3105
|
+
* prevents resumeSession from picking it up again after reload. */
|
|
3106
|
+
function markSessionHandled() {
|
|
3107
|
+
if (!currentSessionId) return;
|
|
3108
|
+
sessionState.markHandled(currentSessionId);
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
function isSessionHandled(id) {
|
|
3112
|
+
return sessionState.isHandled(id);
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
function clearHandled() {
|
|
3116
|
+
sessionState.clearHandled();
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
function cleanup() {
|
|
3120
|
+
// Hide the wrapper immediately so variants disappear. DON'T structurally
|
|
3121
|
+
// mutate the DOM yet — HMR from the agent's source rewrite is on its way,
|
|
3122
|
+
// and a manual replaceChild under React causes NotFoundError when the
|
|
3123
|
+
// reconciler later tries to remove a wrapper we already removed.
|
|
3124
|
+
// Schedule a 2s fallback that does the manual swap only if HMR hasn't
|
|
3125
|
+
// replaced the wrapper by then (keeps static-server / no-HMR flows alive).
|
|
3126
|
+
const cleanupSessionId = currentSessionId;
|
|
3127
|
+
if (cleanupSessionId) {
|
|
3128
|
+
const wrapper = document.querySelector('[data-impeccable-variants="' + cleanupSessionId + '"]');
|
|
3129
|
+
if (wrapper) wrapper.style.display = 'none';
|
|
3130
|
+
}
|
|
3131
|
+
setTimeout(function() {
|
|
3132
|
+
if (!cleanupSessionId) return;
|
|
3133
|
+
const wrapper = document.querySelector('[data-impeccable-variants="' + cleanupSessionId + '"]');
|
|
3134
|
+
if (!wrapper) return;
|
|
3135
|
+
const orig = wrapper.querySelector('[data-impeccable-variant="original"]');
|
|
3136
|
+
if (orig) {
|
|
3137
|
+
const content = orig.firstElementChild;
|
|
3138
|
+
if (content) {
|
|
3139
|
+
wrapper.parentElement.replaceChild(content, wrapper);
|
|
3140
|
+
return;
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
wrapper.remove();
|
|
3144
|
+
}, 2000);
|
|
3145
|
+
hideBar();
|
|
3146
|
+
hideHighlight();
|
|
3147
|
+
stopScrollTracking();
|
|
3148
|
+
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
|
|
3149
|
+
stopScrollLock();
|
|
3150
|
+
clearScrollY();
|
|
3151
|
+
clearSession();
|
|
3152
|
+
selectedElement = null;
|
|
3153
|
+
currentSessionId = null;
|
|
3154
|
+
selectedAction = 'impeccable';
|
|
3155
|
+
state = 'PICKING';
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3158
|
+
// ---------------------------------------------------------------------------
|
|
3159
|
+
// Toast
|
|
3160
|
+
// ---------------------------------------------------------------------------
|
|
3161
|
+
|
|
3162
|
+
function showToast(message, duration) {
|
|
3163
|
+
if (toastEl) toastEl.remove();
|
|
3164
|
+
// Stack the toast above the global bar (which sits at bottom:14px) so
|
|
3165
|
+
// the two never overlap. Read the bar's actual rect — its height varies
|
|
3166
|
+
// with hover-expanded labels — and fall back to a sensible default
|
|
3167
|
+
// when the bar isn't mounted yet.
|
|
3168
|
+
const barRect = globalBarEl?.getBoundingClientRect();
|
|
3169
|
+
const barTopFromBottom = barRect && barRect.height > 0
|
|
3170
|
+
? Math.max(16, window.innerHeight - barRect.top + 12)
|
|
3171
|
+
: 16;
|
|
3172
|
+
toastEl = el('div', {
|
|
3173
|
+
position: 'fixed', bottom: barTopFromBottom + 'px', left: '50%',
|
|
3174
|
+
transform: 'translateX(-50%) translateY(8px)',
|
|
3175
|
+
background: C.ink, color: C.white,
|
|
3176
|
+
fontFamily: FONT, fontSize: '12px',
|
|
3177
|
+
padding: '8px 16px', borderRadius: '8px',
|
|
3178
|
+
zIndex: Z.toast, opacity: '0',
|
|
3179
|
+
transition: 'opacity 0.25s ' + EASE + ', transform 0.25s ' + EASE,
|
|
3180
|
+
pointerEvents: 'none', maxWidth: '420px', textAlign: 'center',
|
|
3181
|
+
});
|
|
3182
|
+
toastEl.id = PREFIX + '-toast';
|
|
3183
|
+
toastEl.textContent = message;
|
|
3184
|
+
document.body.appendChild(toastEl);
|
|
3185
|
+
requestAnimationFrame(() => {
|
|
3186
|
+
toastEl.style.opacity = '1';
|
|
3187
|
+
toastEl.style.transform = 'translateX(-50%) translateY(0)';
|
|
3188
|
+
});
|
|
3189
|
+
setTimeout(() => {
|
|
3190
|
+
if (toastEl) {
|
|
3191
|
+
toastEl.style.opacity = '0';
|
|
3192
|
+
toastEl.style.transform = 'translateX(-50%) translateY(8px)';
|
|
3193
|
+
setTimeout(() => { if (toastEl) { toastEl.remove(); toastEl = null; } }, 250);
|
|
3194
|
+
}
|
|
3195
|
+
}, duration);
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
// ---------------------------------------------------------------------------
|
|
3199
|
+
// Init
|
|
3200
|
+
// ---------------------------------------------------------------------------
|
|
3201
|
+
|
|
3202
|
+
// Resume an active variant session after HMR/page reload.
|
|
3203
|
+
// If a [data-impeccable-variants] wrapper exists in the DOM, the agent wrote
|
|
3204
|
+
// variants before HMR fired. Pick up where we left off.
|
|
3205
|
+
function resumeSession() {
|
|
3206
|
+
const wrapper = document.querySelector('[data-impeccable-variants]');
|
|
3207
|
+
if (!wrapper) { clearSession(); clearHandled(); return false; }
|
|
3208
|
+
|
|
3209
|
+
const sessionId = wrapper.dataset.impeccableVariants;
|
|
3210
|
+
|
|
3211
|
+
// Don't resume if this session was already accepted/discarded
|
|
3212
|
+
if (isSessionHandled(sessionId)) return false;
|
|
3213
|
+
|
|
3214
|
+
currentSessionId = sessionId;
|
|
3215
|
+
expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || '0');
|
|
3216
|
+
const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
|
|
3217
|
+
arrivedVariants = variants.length;
|
|
3218
|
+
|
|
3219
|
+
// Restore state from localStorage if available
|
|
3220
|
+
const saved = loadSession();
|
|
3221
|
+
if (saved && saved.id === sessionId) {
|
|
3222
|
+
visibleVariant = (saved.visible > 0 && saved.visible <= arrivedVariants) ? saved.visible : (arrivedVariants > 0 ? 1 : 0);
|
|
3223
|
+
if (saved.action) selectedAction = saved.action;
|
|
3224
|
+
if (saved.count) selectedCount = saved.count;
|
|
3225
|
+
} else {
|
|
3226
|
+
visibleVariant = arrivedVariants > 0 ? 1 : 0;
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
// Find the visible variant's content element for highlight positioning.
|
|
3230
|
+
// Try the visible variant first, fall back to the original's content.
|
|
3231
|
+
const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null;
|
|
3232
|
+
const origEl = pickVariantContent(wrapper, 'original');
|
|
3233
|
+
selectedElement = visEl || origEl || wrapper.parentElement;
|
|
3234
|
+
|
|
3235
|
+
// Set display state BEFORE starting observer (avoid triggering it)
|
|
3236
|
+
if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant);
|
|
3237
|
+
|
|
3238
|
+
state = arrivedVariants >= expectedVariants ? 'CYCLING' : 'GENERATING';
|
|
3239
|
+
showBar(state === 'CYCLING' ? 'cycling' : 'generating');
|
|
3240
|
+
startScrollTracking();
|
|
3241
|
+
// Build the params panel for the restored visible variant. Previously
|
|
3242
|
+
// this was missed on page-reload resume: showVariantInDOM above fires
|
|
3243
|
+
// refreshParamsPanel, but state was still IDLE at that moment so it
|
|
3244
|
+
// hid. Now that state is CYCLING, re-fire.
|
|
3245
|
+
if (state === 'CYCLING') refreshParamsPanel();
|
|
3246
|
+
saveSession();
|
|
3247
|
+
queueCheckpoint('browser_resumed');
|
|
3248
|
+
|
|
3249
|
+
// Start observing for more variants AFTER initial setup
|
|
3250
|
+
if (variantObserver) variantObserver.disconnect();
|
|
3251
|
+
variantObserver = startVariantObserver(currentSessionId);
|
|
3252
|
+
|
|
3253
|
+
// Hold the target at its saved viewport top through any subsequent
|
|
3254
|
+
// HMR patches, variant inserts, or cycle swaps.
|
|
3255
|
+
startScrollLock(currentSessionId, readScrollY());
|
|
3256
|
+
|
|
3257
|
+
// If we reloaded mid-generation (Bun's HTML HMR destroys the shader
|
|
3258
|
+
// canvas), re-capture the original's content and restart the shader so
|
|
3259
|
+
// the wait doesn't go dead.
|
|
3260
|
+
if (state === 'GENERATING' && origEl) {
|
|
3261
|
+
(async () => {
|
|
3262
|
+
try {
|
|
3263
|
+
const rect = origEl.getBoundingClientRect();
|
|
3264
|
+
if (rect.width === 0 || rect.height === 0) return;
|
|
3265
|
+
const blob = await captureElementToBlob(origEl, null, rect);
|
|
3266
|
+
if (blob && state === 'GENERATING') {
|
|
3267
|
+
showShaderOverlay(origEl, blob, rect);
|
|
3268
|
+
}
|
|
3269
|
+
} catch (err) {
|
|
3270
|
+
console.warn('[impeccable] shader resume failed:', err);
|
|
3271
|
+
}
|
|
3272
|
+
})();
|
|
3273
|
+
}
|
|
3274
|
+
return true;
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
// ---------------------------------------------------------------------------
|
|
3278
|
+
// Global bar (always visible at bottom)
|
|
3279
|
+
// ---------------------------------------------------------------------------
|
|
3280
|
+
|
|
3281
|
+
let globalBarEl = null;
|
|
3282
|
+
let detectActive = false;
|
|
3283
|
+
let pickActive = true;
|
|
3284
|
+
let detectCount = 0;
|
|
3285
|
+
let detectScriptLoaded = false;
|
|
3286
|
+
|
|
3287
|
+
// Theme-aware color palette for the global bar. We detect the page's
|
|
3288
|
+
// ambient background and invert — dark bar on light pages, light bar on
|
|
3289
|
+
// dark pages. This keeps the bar from fighting with the host design.
|
|
3290
|
+
function detectPageTheme() {
|
|
3291
|
+
try {
|
|
3292
|
+
// Dev override: set localStorage 'impeccable-dev-theme' to 'light' or
|
|
3293
|
+
// 'dark' to preview the opposite palette without actually changing the
|
|
3294
|
+
// page bg. Used for screenshots and theme QA.
|
|
3295
|
+
const override = localStorage.getItem('impeccable-dev-theme');
|
|
3296
|
+
if (override === 'light' || override === 'dark') return override;
|
|
3297
|
+
|
|
3298
|
+
// Walk body → html, taking the first opaque background. The browser's
|
|
3299
|
+
// default body / html background is `rgba(0, 0, 0, 0)`, which a naive
|
|
3300
|
+
// regex would read as black and mislabel a perfectly white page as
|
|
3301
|
+
// dark. Honoring alpha avoids that — and falling through to <html>
|
|
3302
|
+
// catches the common pattern of a bg only on <html> (or only on body).
|
|
3303
|
+
function readOpaque(el) {
|
|
3304
|
+
if (!el) return null;
|
|
3305
|
+
const bg = getComputedStyle(el).backgroundColor;
|
|
3306
|
+
const m = bg.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/);
|
|
3307
|
+
if (!m) return null;
|
|
3308
|
+
const alpha = m[4] == null ? 1 : parseFloat(m[4]);
|
|
3309
|
+
if (alpha < 0.5) return null; // transparent / nearly transparent → skip
|
|
3310
|
+
return [+m[1], +m[2], +m[3]];
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
const rgb = readOpaque(document.body) || readOpaque(document.documentElement);
|
|
3314
|
+
// Both transparent → fall back to the browser's effective canvas color.
|
|
3315
|
+
// White is the universal default; only one in a thousand sites swaps it
|
|
3316
|
+
// via `color-scheme: dark` on <html>, and `prefers-color-scheme` lets
|
|
3317
|
+
// us catch that case.
|
|
3318
|
+
if (!rgb) {
|
|
3319
|
+
return matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
3320
|
+
}
|
|
3321
|
+
const [r, g, b] = rgb;
|
|
3322
|
+
// Perceptual luminance (Rec. 709)
|
|
3323
|
+
const L = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
|
|
3324
|
+
return L > 0.55 ? 'light' : 'dark';
|
|
3325
|
+
} catch { return 'light'; }
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
function barPaletteForTheme(theme) {
|
|
3329
|
+
if (theme === 'dark') {
|
|
3330
|
+
// Light bar on dark page
|
|
3331
|
+
return {
|
|
3332
|
+
surface: 'oklch(98% 0 0 / 0.92)',
|
|
3333
|
+
surfaceDeep: 'oklch(92% 0.005 60 / 0.96)', // slightly deeper, faint warm
|
|
3334
|
+
hairline: 'oklch(70% 0 0 / 0.35)',
|
|
3335
|
+
text: 'oklch(15% 0 0)',
|
|
3336
|
+
textDim: 'oklch(45% 0 0)',
|
|
3337
|
+
accent: 'oklch(60% 0.25 350)',
|
|
3338
|
+
accentSoft: 'oklch(60% 0.25 350 / 0.18)',
|
|
3339
|
+
mark: 'oklch(98% 0 0)', // logo mark fill
|
|
3340
|
+
markText: 'oklch(15% 0 0)', // logo "/" color
|
|
3341
|
+
exitHover: 'oklch(85% 0 0 / 0.5)',
|
|
3342
|
+
};
|
|
3343
|
+
}
|
|
3344
|
+
// Dark bar on light page. Bar is a warm charcoal, logo slab is much
|
|
3345
|
+
// deeper so the rounded-right shape reads as a clear sculpted mark.
|
|
3346
|
+
return {
|
|
3347
|
+
surface: 'oklch(26% 0 0 / 0.94)',
|
|
3348
|
+
surfaceDeep: 'oklch(18% 0 0 / 0.96)', // darker sand for Tune popover
|
|
3349
|
+
hairline: 'oklch(42% 0 0 / 0.5)',
|
|
3350
|
+
text: 'oklch(96% 0 0)',
|
|
3351
|
+
textDim: 'oklch(72% 0 0)',
|
|
3352
|
+
accent: 'oklch(72% 0.22 350)',
|
|
3353
|
+
accentSoft: 'oklch(72% 0.22 350 / 0.22)',
|
|
3354
|
+
mark: 'oklch(8% 0 0)',
|
|
3355
|
+
markText: 'oklch(96% 0 0)',
|
|
3356
|
+
exitHover: 'oklch(36% 0 0 / 0.6)',
|
|
3357
|
+
};
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
// Impeccable logo mark — matches the site-header SVG (rounded square + "/").
|
|
3361
|
+
function brandMarkSvg(fill, ink, size = 18) {
|
|
3362
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 32 32" aria-hidden="true">
|
|
3363
|
+
<rect width="32" height="32" rx="7" fill="${fill}"/>
|
|
3364
|
+
<text x="16" y="24" font-family="system-ui, -apple-system, sans-serif" font-size="22" font-weight="500" fill="${ink}" text-anchor="middle">/</text>
|
|
3365
|
+
</svg>`;
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
function initGlobalBar() {
|
|
3369
|
+
const theme = detectPageTheme();
|
|
3370
|
+
const P = barPaletteForTheme(theme);
|
|
3371
|
+
|
|
3372
|
+
// Custom focus-visible for bar buttons. Browser default is a heavy
|
|
3373
|
+
// blue ring that looks jarring on the dark capsule. Replace with a
|
|
3374
|
+
// soft accent-tinted inner ring that respects the bar's palette.
|
|
3375
|
+
if (!document.getElementById(PREFIX + '-bar-focus-style')) {
|
|
3376
|
+
const s = document.createElement('style');
|
|
3377
|
+
s.id = PREFIX + '-bar-focus-style';
|
|
3378
|
+
s.textContent =
|
|
3379
|
+
'#' + PREFIX + '-global-bar button:focus { outline: none; }' +
|
|
3380
|
+
'#' + PREFIX + '-global-bar button:focus-visible {' +
|
|
3381
|
+
' outline: none;' +
|
|
3382
|
+
' box-shadow: 0 0 0 2px ' + P.accentSoft + ', 0 0 0 3px ' + P.accent + ';' +
|
|
3383
|
+
'}';
|
|
3384
|
+
document.head.appendChild(s);
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
globalBarEl = el('div', {
|
|
3388
|
+
position: 'fixed', bottom: '14px', left: '50%',
|
|
3389
|
+
transform: 'translateX(-50%) translateY(20px)',
|
|
3390
|
+
zIndex: Z.bar + 5,
|
|
3391
|
+
display: 'flex', alignItems: 'stretch',
|
|
3392
|
+
gap: '2px',
|
|
3393
|
+
background: P.surface,
|
|
3394
|
+
backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)',
|
|
3395
|
+
border: '1px solid ' + P.hairline,
|
|
3396
|
+
borderRadius: '10px',
|
|
3397
|
+
boxShadow: '0 4px 20px oklch(0% 0 0 / 0.12), 0 1px 3px oklch(0% 0 0 / 0.08)',
|
|
3398
|
+
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
|
|
3399
|
+
opacity: '0',
|
|
3400
|
+
overflow: 'hidden', // clip the full-bleed brand mark to the bar radius
|
|
3401
|
+
transition: 'opacity 0.3s ' + EASE + ', transform 0.3s ' + EASE,
|
|
3402
|
+
});
|
|
3403
|
+
globalBarEl.id = PREFIX + '-global-bar';
|
|
3404
|
+
globalBarEl.dataset.theme = theme;
|
|
3405
|
+
|
|
3406
|
+
// Brand mark — fills bar height on the left. Left side inherits the bar's
|
|
3407
|
+
// rounded corner via overflow:hidden; right side is a clean hard edge since
|
|
3408
|
+
// the near-black/charcoal contrast does the shape-defining work.
|
|
3409
|
+
const brand = el('span', {
|
|
3410
|
+
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
3411
|
+
alignSelf: 'stretch',
|
|
3412
|
+
padding: '0 12px 0 14px',
|
|
3413
|
+
background: P.mark,
|
|
3414
|
+
color: P.markText,
|
|
3415
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
3416
|
+
fontWeight: '500',
|
|
3417
|
+
fontSize: '18px', lineHeight: '1',
|
|
3418
|
+
});
|
|
3419
|
+
brand.textContent = '/';
|
|
3420
|
+
brand.title = 'Impeccable';
|
|
3421
|
+
globalBarEl.appendChild(brand);
|
|
3422
|
+
|
|
3423
|
+
// Inner wrapper: holds the toggles with normal bar padding.
|
|
3424
|
+
const inner = el('div', {
|
|
3425
|
+
display: 'flex', alignItems: 'center',
|
|
3426
|
+
padding: '4px 5px', gap: '2px',
|
|
3427
|
+
});
|
|
3428
|
+
inner.id = PREFIX + '-global-bar-inner';
|
|
3429
|
+
globalBarEl.appendChild(inner);
|
|
3430
|
+
|
|
3431
|
+
// --- button factory: icon-only at rest, label slides in on hover/active ---
|
|
3432
|
+
function makeIconBtn({ id, svg, label, ariaLabel, labelFont, onClick }) {
|
|
3433
|
+
const b = el('button', {
|
|
3434
|
+
position: 'relative',
|
|
3435
|
+
display: 'inline-flex', alignItems: 'center',
|
|
3436
|
+
padding: '6px 8px', borderRadius: '7px',
|
|
3437
|
+
border: 'none', background: 'transparent',
|
|
3438
|
+
color: P.textDim, fontFamily: FONT, fontSize: '11.5px', fontWeight: '500',
|
|
3439
|
+
cursor: 'pointer',
|
|
3440
|
+
transition: 'background 0.15s ease, color 0.15s ease',
|
|
3441
|
+
whiteSpace: 'nowrap', overflow: 'hidden',
|
|
3442
|
+
});
|
|
3443
|
+
b.id = id;
|
|
3444
|
+
b.title = ariaLabel || label || '';
|
|
3445
|
+
b.setAttribute('aria-label', ariaLabel || label || '');
|
|
3446
|
+
b.innerHTML = svg + (label
|
|
3447
|
+
? `<span class="icon-btn-label" style="display:inline-block;max-width:0;opacity:0;margin-left:0;overflow:hidden;font-family:${labelFont || FONT};transition:max-width 0.25s ${EASE}, opacity 0.2s ease, margin-left 0.25s ${EASE};">${label}</span>`
|
|
3448
|
+
: '');
|
|
3449
|
+
const labelEl = b.querySelector('.icon-btn-label');
|
|
3450
|
+
const expand = () => {
|
|
3451
|
+
if (!labelEl) return;
|
|
3452
|
+
labelEl.style.maxWidth = '120px'; labelEl.style.opacity = '1'; labelEl.style.marginLeft = '6px';
|
|
3453
|
+
};
|
|
3454
|
+
const collapse = () => {
|
|
3455
|
+
if (!labelEl || b.dataset.active === 'true') return;
|
|
3456
|
+
labelEl.style.maxWidth = '0'; labelEl.style.opacity = '0'; labelEl.style.marginLeft = '0';
|
|
3457
|
+
};
|
|
3458
|
+
// Per-button hover only changes color (no layout). The label expand/
|
|
3459
|
+
// collapse is driven by the bar-level mouseenter/mouseleave so moving
|
|
3460
|
+
// the mouse between adjacent buttons doesn't trigger per-button width
|
|
3461
|
+
// thrashing — the whole bar grows once and shrinks once.
|
|
3462
|
+
b.addEventListener('mouseenter', () => { if (b.dataset.active !== 'true') b.style.color = P.text; });
|
|
3463
|
+
b.addEventListener('mouseleave', () => { if (b.dataset.active !== 'true') b.style.color = P.textDim; });
|
|
3464
|
+
b.addEventListener('click', onClick);
|
|
3465
|
+
b._expandLabel = expand;
|
|
3466
|
+
b._collapseLabel = collapse;
|
|
3467
|
+
return b;
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
// Pick toggle — starts active (primary intent when entering live mode).
|
|
3471
|
+
const pickBtn = makeIconBtn({
|
|
3472
|
+
id: PREFIX + '-pick-toggle',
|
|
3473
|
+
svg: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><circle cx="12" cy="12" r="10"/><line x1="22" y1="12" x2="18" y2="12"/><line x1="6" y1="12" x2="2" y2="12"/><line x1="12" y1="6" x2="12" y2="2"/><line x1="12" y1="22" x2="12" y2="18"/></svg>',
|
|
3474
|
+
label: 'Pick',
|
|
3475
|
+
ariaLabel: 'Pick element',
|
|
3476
|
+
onClick: () => togglePick(),
|
|
3477
|
+
});
|
|
3478
|
+
pickBtn.style.background = P.accentSoft;
|
|
3479
|
+
pickBtn.style.color = P.accent;
|
|
3480
|
+
pickBtn.dataset.active = 'true';
|
|
3481
|
+
pickBtn._expandLabel();
|
|
3482
|
+
inner.appendChild(pickBtn);
|
|
3483
|
+
|
|
3484
|
+
// Detect toggle
|
|
3485
|
+
const detectBtn = makeIconBtn({
|
|
3486
|
+
id: PREFIX + '-detect-toggle',
|
|
3487
|
+
svg: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>',
|
|
3488
|
+
label: 'Detect',
|
|
3489
|
+
ariaLabel: 'Detect anti-patterns',
|
|
3490
|
+
onClick: () => toggleDetect(),
|
|
3491
|
+
});
|
|
3492
|
+
const detectBadge = el('span', {
|
|
3493
|
+
fontSize: '10px', fontWeight: '600',
|
|
3494
|
+
padding: '0px 5px', borderRadius: '7px', lineHeight: '16px',
|
|
3495
|
+
background: P.accent, color: P.surface.includes('18%') ? 'oklch(18% 0 0)' : 'oklch(98% 0 0)',
|
|
3496
|
+
display: 'none', fontFamily: MONO, marginLeft: '4px',
|
|
3497
|
+
});
|
|
3498
|
+
detectBadge.id = PREFIX + '-detect-badge';
|
|
3499
|
+
detectBtn.appendChild(detectBadge);
|
|
3500
|
+
inner.appendChild(detectBtn);
|
|
3501
|
+
|
|
3502
|
+
// DESIGN.md panel toggle — quartet of color squares as the mark.
|
|
3503
|
+
const designBtn = makeIconBtn({
|
|
3504
|
+
id: PREFIX + '-design-toggle',
|
|
3505
|
+
svg: `<span style="display:inline-grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;width:14px;height:14px;border-radius:3px;overflow:hidden;box-shadow:inset 0 0 0 1px ${P.hairline};flex-shrink:0">
|
|
3506
|
+
<span style="background:oklch(60% 0.25 350)"></span>
|
|
3507
|
+
<span style="background:oklch(60% 0.15 45)"></span>
|
|
3508
|
+
<span style="background:oklch(55% 0.12 250)"></span>
|
|
3509
|
+
<span style="background:oklch(30% 0 0)"></span>
|
|
3510
|
+
</span>`,
|
|
3511
|
+
label: 'DESIGN.md',
|
|
3512
|
+
ariaLabel: 'Toggle DESIGN.md panel',
|
|
3513
|
+
labelFont: MONO,
|
|
3514
|
+
onClick: () => toggleDesignPanel(),
|
|
3515
|
+
});
|
|
3516
|
+
inner.appendChild(designBtn);
|
|
3517
|
+
|
|
3518
|
+
// Thin divider before the exit button
|
|
3519
|
+
const divider = el('span', {
|
|
3520
|
+
width: '1px', height: '18px',
|
|
3521
|
+
background: P.hairline,
|
|
3522
|
+
margin: '0 4px 0 2px',
|
|
3523
|
+
});
|
|
3524
|
+
inner.appendChild(divider);
|
|
3525
|
+
|
|
3526
|
+
// Exit × on the right — intentionally subtle (textDim at rest, text on
|
|
3527
|
+
// hover) so it sits behind the active toggles in visual hierarchy.
|
|
3528
|
+
//
|
|
3529
|
+
// Explicit padding + box-sizing here is load-bearing: a host page like
|
|
3530
|
+
// `button { padding: 0.5rem 1rem; }` (very common in resets) would
|
|
3531
|
+
// otherwise inflate this 24x24 button into 56x40 and push the SVG out
|
|
3532
|
+
// of the visible bar — the X stays invisible even though the styles in
|
|
3533
|
+
// DevTools look fine. Every other chrome button sets padding inline;
|
|
3534
|
+
// this one needed it too.
|
|
3535
|
+
const exitBtn = el('button', {
|
|
3536
|
+
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
3537
|
+
padding: '0', boxSizing: 'border-box',
|
|
3538
|
+
width: '24px', height: '24px', borderRadius: '6px',
|
|
3539
|
+
border: 'none', background: 'transparent',
|
|
3540
|
+
color: P.textDim, fontFamily: FONT, fontSize: '0', lineHeight: '0',
|
|
3541
|
+
cursor: 'pointer', transition: 'color 0.12s ease, background 0.12s ease',
|
|
3542
|
+
});
|
|
3543
|
+
exitBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="3" y1="3" x2="11" y2="11"/><line x1="11" y1="3" x2="3" y2="11"/></svg>';
|
|
3544
|
+
exitBtn.title = 'Exit live mode';
|
|
3545
|
+
exitBtn.addEventListener('mouseenter', () => { exitBtn.style.color = P.text; exitBtn.style.background = P.exitHover; });
|
|
3546
|
+
exitBtn.addEventListener('mouseleave', () => { exitBtn.style.color = P.textDim; exitBtn.style.background = 'transparent'; });
|
|
3547
|
+
exitBtn.addEventListener('click', () => { sendEvent({ type: 'exit' }); teardown(); });
|
|
3548
|
+
inner.appendChild(exitBtn);
|
|
3549
|
+
|
|
3550
|
+
// Bar-level hover: expand every toggle's label at once; collapse on leave.
|
|
3551
|
+
// Buttons with dataset.active="true" ignore collapse (their label stays).
|
|
3552
|
+
const toggles = [pickBtn, detectBtn, designBtn];
|
|
3553
|
+
globalBarEl.addEventListener('mouseenter', () => {
|
|
3554
|
+
toggles.forEach((t) => t._expandLabel && t._expandLabel());
|
|
3555
|
+
});
|
|
3556
|
+
globalBarEl.addEventListener('mouseleave', () => {
|
|
3557
|
+
toggles.forEach((t) => t._collapseLabel && t._collapseLabel());
|
|
3558
|
+
});
|
|
3559
|
+
|
|
3560
|
+
document.body.appendChild(globalBarEl);
|
|
3561
|
+
defangOutsideHandlers(globalBarEl);
|
|
3562
|
+
|
|
3563
|
+
requestAnimationFrame(() => {
|
|
3564
|
+
globalBarEl.style.opacity = '1';
|
|
3565
|
+
globalBarEl.style.transform = 'translateX(-50%) translateY(0)';
|
|
3566
|
+
});
|
|
3567
|
+
|
|
3568
|
+
// Listen for detection results AND ready signal
|
|
3569
|
+
window.addEventListener('message', onDetectMessage);
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
function updateGlobalBarState() {
|
|
3573
|
+
const detectToggle = document.getElementById(PREFIX + '-detect-toggle');
|
|
3574
|
+
const detectBadge = document.getElementById(PREFIX + '-detect-badge');
|
|
3575
|
+
const pickToggle = document.getElementById(PREFIX + '-pick-toggle');
|
|
3576
|
+
const designToggle = document.getElementById(PREFIX + '-design-toggle');
|
|
3577
|
+
const theme = globalBarEl?.dataset.theme || 'light';
|
|
3578
|
+
const P = barPaletteForTheme(theme);
|
|
3579
|
+
|
|
3580
|
+
// Sync one toggle's active state, colors, and slide-label visibility.
|
|
3581
|
+
function sync(btn, active) {
|
|
3582
|
+
if (!btn) return;
|
|
3583
|
+
btn.style.background = active ? P.accentSoft : 'transparent';
|
|
3584
|
+
btn.style.color = active ? P.accent : P.textDim;
|
|
3585
|
+
btn.dataset.active = active ? 'true' : 'false';
|
|
3586
|
+
if (active && btn._expandLabel) btn._expandLabel();
|
|
3587
|
+
else if (!active && btn._collapseLabel) btn._collapseLabel();
|
|
3588
|
+
}
|
|
3589
|
+
sync(pickToggle, pickActive);
|
|
3590
|
+
sync(detectToggle, detectActive);
|
|
3591
|
+
sync(designToggle, designState.open);
|
|
3592
|
+
|
|
3593
|
+
// If the bar is currently under the cursor, keep all labels expanded —
|
|
3594
|
+
// otherwise clicking a toggle that deactivates (e.g. closing DESIGN.md)
|
|
3595
|
+
// would collapse its label while the user's mouse is still on the bar.
|
|
3596
|
+
if (globalBarEl && globalBarEl.matches(':hover')) {
|
|
3597
|
+
[pickToggle, detectToggle, designToggle].forEach((t) => t?._expandLabel?.());
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3600
|
+
if (detectBadge) {
|
|
3601
|
+
detectBadge.style.display = (detectActive && detectCount > 0) ? 'inline' : 'none';
|
|
3602
|
+
detectBadge.textContent = detectCount;
|
|
3603
|
+
}
|
|
3604
|
+
|
|
3605
|
+
// When pick is active, make detect overlays click-through so the picker works
|
|
3606
|
+
document.querySelectorAll('.impeccable-overlay').forEach(o => {
|
|
3607
|
+
o.style.pointerEvents = pickActive ? 'none' : '';
|
|
3608
|
+
});
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
let detectReady = false; // true once detect script posts 'impeccable-ready'
|
|
3612
|
+
let detectPendingScan = false; // scan requested before script was ready
|
|
3613
|
+
|
|
3614
|
+
function toggleDetect() {
|
|
3615
|
+
detectActive = !detectActive;
|
|
3616
|
+
updateGlobalBarState();
|
|
3617
|
+
|
|
3618
|
+
if (detectActive) {
|
|
3619
|
+
if (!detectScriptLoaded) {
|
|
3620
|
+
detectPendingScan = true;
|
|
3621
|
+
loadDetectScript();
|
|
3622
|
+
} else if (detectReady) {
|
|
3623
|
+
window.postMessage({ source: 'impeccable-command', action: 'scan' }, '*');
|
|
3624
|
+
} else {
|
|
3625
|
+
detectPendingScan = true;
|
|
3626
|
+
}
|
|
3627
|
+
} else {
|
|
3628
|
+
window.postMessage({ source: 'impeccable-command', action: 'remove' }, '*');
|
|
3629
|
+
detectCount = 0;
|
|
3630
|
+
updateGlobalBarState();
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
|
|
3634
|
+
function togglePick() {
|
|
3635
|
+
pickActive = !pickActive;
|
|
3636
|
+
updateGlobalBarState();
|
|
3637
|
+
|
|
3638
|
+
if (!pickActive) {
|
|
3639
|
+
// Disabling pick clears any in-flight selection and UI: highlight,
|
|
3640
|
+
// contextual bar, selectedElement. Otherwise a stale selection sits
|
|
3641
|
+
// on screen with no obvious way to dismiss.
|
|
3642
|
+
hideHighlight();
|
|
3643
|
+
hideBar();
|
|
3644
|
+
hideActionPicker();
|
|
3645
|
+
selectedElement = null;
|
|
3646
|
+
if (state === 'PICKING' || state === 'CONFIGURING') state = 'IDLE';
|
|
3647
|
+
} else {
|
|
3648
|
+
if (state === 'IDLE') state = 'PICKING';
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
|
|
3652
|
+
function loadDetectScript() {
|
|
3653
|
+
if (detectScriptLoaded) return;
|
|
3654
|
+
detectScriptLoaded = true;
|
|
3655
|
+
const s = document.createElement('script');
|
|
3656
|
+
s.src = LIVE_ORIGIN + '/detect.js';
|
|
3657
|
+
s.dataset.impeccableExtension = 'true';
|
|
3658
|
+
document.head.appendChild(s);
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
function onDetectMessage(e) {
|
|
3662
|
+
if (!e.data || typeof e.data.source !== 'string') return;
|
|
3663
|
+
// Detection script is loaded and ready
|
|
3664
|
+
if (e.data.source === 'impeccable-ready') {
|
|
3665
|
+
detectReady = true;
|
|
3666
|
+
if (detectPendingScan && detectActive) {
|
|
3667
|
+
detectPendingScan = false;
|
|
3668
|
+
window.postMessage({ source: 'impeccable-command', action: 'scan' }, '*');
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
// Scan results arrived
|
|
3672
|
+
if (e.data.source === 'impeccable-results') {
|
|
3673
|
+
detectCount = e.data.count || 0;
|
|
3674
|
+
updateGlobalBarState();
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
|
|
3678
|
+
/** Full teardown: remove all UI, disconnect SSE, clean up. */
|
|
3679
|
+
function teardown() {
|
|
3680
|
+
cleanup();
|
|
3681
|
+
hideBar();
|
|
3682
|
+
if (globalBarEl) {
|
|
3683
|
+
globalBarEl.style.transform = 'translateY(100%)';
|
|
3684
|
+
setTimeout(() => { if (globalBarEl) globalBarEl.remove(); globalBarEl = null; }, 300);
|
|
3685
|
+
}
|
|
3686
|
+
if (highlightEl) { highlightEl.remove(); highlightEl = null; }
|
|
3687
|
+
if (tooltipEl) { tooltipEl.remove(); tooltipEl = null; }
|
|
3688
|
+
if (barEl) { barEl.remove(); barEl = null; }
|
|
3689
|
+
if (pickerEl) { pickerEl.remove(); pickerEl = null; }
|
|
3690
|
+
if (paramsPanelEl) { paramsPanelEl.remove(); paramsPanelEl = null; paramsPanelInner = null; paramsPanelBody = null; }
|
|
3691
|
+
if (evtSource) { evtSource.close(); evtSource = null; }
|
|
3692
|
+
document.removeEventListener('mousemove', handleMouseMove, true);
|
|
3693
|
+
document.removeEventListener('click', handleClick, true);
|
|
3694
|
+
document.removeEventListener('keydown', handleKeyDown, true);
|
|
3695
|
+
window.removeEventListener('message', onDetectMessage);
|
|
3696
|
+
// Remove detection overlays
|
|
3697
|
+
window.postMessage({ source: 'impeccable-command', action: 'remove' }, '*');
|
|
3698
|
+
state = 'IDLE';
|
|
3699
|
+
window.__IMPECCABLE_LIVE_INIT__ = false;
|
|
3700
|
+
console.log('[impeccable] Live mode exited.');
|
|
3701
|
+
}
|
|
3702
|
+
|
|
3703
|
+
// ---------------------------------------------------------------------------
|
|
3704
|
+
// Design System Panel — visualizes the project's .impeccable/design.json sidecar
|
|
3705
|
+
// ---------------------------------------------------------------------------
|
|
3706
|
+
|
|
3707
|
+
const DESIGN_PREFS_KEY = 'impeccable-live-design-panel';
|
|
3708
|
+
const DESIGN_PANEL_WIDTH = 440;
|
|
3709
|
+
|
|
3710
|
+
let designHost = null;
|
|
3711
|
+
let designShadow = null;
|
|
3712
|
+
let designState = {
|
|
3713
|
+
open: false,
|
|
3714
|
+
tab: 'visual', // 'visual' | 'raw'
|
|
3715
|
+
parsed: null, // parseDesignMd output (frontmatter + body sections)
|
|
3716
|
+
sidecar: null, // .impeccable/design.json v2 payload (extensions + components + narrative)
|
|
3717
|
+
hasMd: false,
|
|
3718
|
+
hasSidecar: false,
|
|
3719
|
+
present: null, // true/false once fetch resolves
|
|
3720
|
+
raw: null, // raw DESIGN.md for the raw tab
|
|
3721
|
+
mdNewerThanJson: false, // stale-hint flag
|
|
3722
|
+
loading: false,
|
|
3723
|
+
error: null,
|
|
3724
|
+
collapsed: { // narrative-section accordion state
|
|
3725
|
+
rules: true, dosdonts: true, overview: true,
|
|
3726
|
+
},
|
|
3727
|
+
};
|
|
3728
|
+
|
|
3729
|
+
function loadDesignPrefs() {
|
|
3730
|
+
// `open` is intentionally NOT persisted — the panel always starts closed
|
|
3731
|
+
// so live mode doesn't auto-slide a big panel over the page on startup.
|
|
3732
|
+
try {
|
|
3733
|
+
const raw = localStorage.getItem(DESIGN_PREFS_KEY);
|
|
3734
|
+
if (!raw) return;
|
|
3735
|
+
const prefs = JSON.parse(raw);
|
|
3736
|
+
if (prefs.tab === 'visual' || prefs.tab === 'raw') designState.tab = prefs.tab;
|
|
3737
|
+
if (prefs.collapsed && typeof prefs.collapsed === 'object') {
|
|
3738
|
+
Object.assign(designState.collapsed, prefs.collapsed);
|
|
3739
|
+
}
|
|
3740
|
+
} catch { /* ignore */ }
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3743
|
+
function saveDesignPrefs() {
|
|
3744
|
+
try {
|
|
3745
|
+
localStorage.setItem(DESIGN_PREFS_KEY, JSON.stringify({
|
|
3746
|
+
tab: designState.tab,
|
|
3747
|
+
collapsed: designState.collapsed,
|
|
3748
|
+
}));
|
|
3749
|
+
} catch { /* ignore */ }
|
|
3750
|
+
}
|
|
3751
|
+
|
|
3752
|
+
function initDesignPanel() {
|
|
3753
|
+
designHost = document.createElement('div');
|
|
3754
|
+
designHost.id = PREFIX + '-design-host';
|
|
3755
|
+
Object.assign(designHost.style, {
|
|
3756
|
+
position: 'fixed', top: '0', left: '0',
|
|
3757
|
+
width: '0', height: '0',
|
|
3758
|
+
zIndex: String(Z.bar + 10),
|
|
3759
|
+
pointerEvents: 'none',
|
|
3760
|
+
});
|
|
3761
|
+
designShadow = designHost.attachShadow({ mode: 'open' });
|
|
3762
|
+
|
|
3763
|
+
const style = document.createElement('style');
|
|
3764
|
+
// Theme-match the bar: dark chrome on light pages, light chrome on dark pages.
|
|
3765
|
+
const theme = detectPageTheme();
|
|
3766
|
+
style.textContent = designPanelCss(barPaletteForTheme(theme));
|
|
3767
|
+
designShadow.appendChild(style);
|
|
3768
|
+
|
|
3769
|
+
const root = document.createElement('div');
|
|
3770
|
+
root.className = 'root';
|
|
3771
|
+
designShadow.appendChild(root);
|
|
3772
|
+
|
|
3773
|
+
document.body.appendChild(designHost);
|
|
3774
|
+
// The host is pointer-events: none; the panel inside the shadow DOM
|
|
3775
|
+
// manages its own auto/none. Events bubble through the shadow boundary,
|
|
3776
|
+
// so attaching here silences host-page outside-interaction handlers
|
|
3777
|
+
// without touching the host's click-through behavior.
|
|
3778
|
+
defangOutsideHandlers(designHost, { setPointerEvents: false });
|
|
3779
|
+
|
|
3780
|
+
loadDesignPrefs();
|
|
3781
|
+
renderDesignChrome();
|
|
3782
|
+
if (designState.open) {
|
|
3783
|
+
fetchDesignSystem();
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3787
|
+
// Neutral panel palette — deliberately NOT Impeccable-branded. The panel is
|
|
3788
|
+
// a viewer of the project's design system, not an Impeccable surface.
|
|
3789
|
+
const DP = {
|
|
3790
|
+
canvas: 'oklch(94% 0 0)', // panel background
|
|
3791
|
+
tile: 'oklch(98.5% 0 0)', // card-on-canvas
|
|
3792
|
+
tileAlt: 'oklch(96% 0 0)', // subtler tile for inner surfaces
|
|
3793
|
+
ink: 'oklch(15% 0 0)',
|
|
3794
|
+
ink2: 'oklch(35% 0 0)',
|
|
3795
|
+
meta: 'oklch(55% 0 0)',
|
|
3796
|
+
hairline: 'oklch(88% 0 0)',
|
|
3797
|
+
hairlineSoft: 'oklch(92% 0 0)',
|
|
3798
|
+
amber: 'oklch(70% 0.13 65)', // stale-hint accent
|
|
3799
|
+
amberBg: 'oklch(95% 0.05 80)',
|
|
3800
|
+
};
|
|
3801
|
+
|
|
3802
|
+
function designPanelCss(BP) {
|
|
3803
|
+
// BP = bar palette (theme-aware, matches the global bar).
|
|
3804
|
+
// DP = internal content palette (neutral, so tiles render colors true).
|
|
3805
|
+
return `
|
|
3806
|
+
:host, .root { all: initial; }
|
|
3807
|
+
.root {
|
|
3808
|
+
font-family: ${FONT};
|
|
3809
|
+
color: ${DP.ink};
|
|
3810
|
+
pointer-events: none;
|
|
3811
|
+
}
|
|
3812
|
+
.root * { box-sizing: border-box; }
|
|
3813
|
+
button { font: inherit; color: inherit; }
|
|
3814
|
+
|
|
3815
|
+
/* --- Panel shell: chrome matches the bar; body canvas stays neutral --- */
|
|
3816
|
+
.panel {
|
|
3817
|
+
position: fixed; top: 12px; bottom: 72px; right: 12px;
|
|
3818
|
+
width: ${DESIGN_PANEL_WIDTH}px; max-width: calc(100vw - 24px);
|
|
3819
|
+
background: ${BP.surface};
|
|
3820
|
+
border: 1px solid ${BP.hairline};
|
|
3821
|
+
border-radius: 14px;
|
|
3822
|
+
backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
|
|
3823
|
+
box-shadow: 0 20px 60px oklch(0% 0 0 / 0.18), 0 4px 12px oklch(0% 0 0 / 0.08);
|
|
3824
|
+
display: flex; flex-direction: column;
|
|
3825
|
+
transform: translateX(calc(100% + 24px));
|
|
3826
|
+
opacity: 0;
|
|
3827
|
+
transition: transform 0.35s ${EASE}, opacity 0.25s ${EASE};
|
|
3828
|
+
pointer-events: none;
|
|
3829
|
+
overflow: hidden;
|
|
3830
|
+
}
|
|
3831
|
+
.panel[data-open="true"] { transform: translateX(0); opacity: 1; pointer-events: auto; }
|
|
3832
|
+
|
|
3833
|
+
.panel-header {
|
|
3834
|
+
display: flex; align-items: center; gap: 10px;
|
|
3835
|
+
padding: 10px 10px 10px 14px;
|
|
3836
|
+
background: transparent;
|
|
3837
|
+
border-bottom: 1px solid ${BP.hairline};
|
|
3838
|
+
}
|
|
3839
|
+
.panel-title {
|
|
3840
|
+
flex: 1; min-width: 0;
|
|
3841
|
+
font-family: ${MONO};
|
|
3842
|
+
font-size: 11.5px; font-weight: 600;
|
|
3843
|
+
letter-spacing: 0.02em;
|
|
3844
|
+
color: ${BP.text};
|
|
3845
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
3846
|
+
}
|
|
3847
|
+
.panel-close {
|
|
3848
|
+
border: none; background: transparent; color: ${BP.textDim};
|
|
3849
|
+
width: 26px; height: 26px; border-radius: 7px;
|
|
3850
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
3851
|
+
cursor: pointer; transition: background 0.15s ease, color 0.15s ease;
|
|
3852
|
+
}
|
|
3853
|
+
.panel-close:hover { background: ${BP.hairline}; color: ${BP.text}; }
|
|
3854
|
+
|
|
3855
|
+
.tabs {
|
|
3856
|
+
display: inline-flex; padding: 2px;
|
|
3857
|
+
background: ${BP.hairline};
|
|
3858
|
+
border-radius: 7px;
|
|
3859
|
+
gap: 2px;
|
|
3860
|
+
}
|
|
3861
|
+
.tab {
|
|
3862
|
+
border: none; background: transparent;
|
|
3863
|
+
padding: 4px 10px; border-radius: 5px;
|
|
3864
|
+
font-family: ${MONO};
|
|
3865
|
+
font-size: 10px; font-weight: 600; letter-spacing: 0.08em;
|
|
3866
|
+
text-transform: uppercase;
|
|
3867
|
+
color: ${BP.textDim}; cursor: pointer;
|
|
3868
|
+
transition: background 0.15s ease, color 0.15s ease;
|
|
3869
|
+
}
|
|
3870
|
+
.tab[data-active="true"] { background: ${BP.surface}; color: ${BP.text}; }
|
|
3871
|
+
|
|
3872
|
+
.panel-body {
|
|
3873
|
+
flex: 1; overflow-y: auto;
|
|
3874
|
+
padding: 12px 12px 20px;
|
|
3875
|
+
background: ${DP.canvas};
|
|
3876
|
+
scrollbar-width: thin;
|
|
3877
|
+
scrollbar-color: ${DP.hairline} transparent;
|
|
3878
|
+
}
|
|
3879
|
+
.panel-body::-webkit-scrollbar { width: 8px; }
|
|
3880
|
+
.panel-body::-webkit-scrollbar-thumb { background: ${DP.hairline}; border-radius: 8px; border: 2px solid transparent; background-clip: padding-box; }
|
|
3881
|
+
|
|
3882
|
+
/* --- States --- */
|
|
3883
|
+
.empty, .loading, .error {
|
|
3884
|
+
margin: 16px 4px;
|
|
3885
|
+
padding: 28px 20px; text-align: center;
|
|
3886
|
+
background: ${DP.tile}; border-radius: 14px;
|
|
3887
|
+
color: ${DP.ink2}; font-size: 13px; line-height: 1.55;
|
|
3888
|
+
}
|
|
3889
|
+
.empty strong { color: ${DP.ink}; display: block; margin-bottom: 6px; font-size: 14px; }
|
|
3890
|
+
.empty code { font-family: ${MONO}; background: ${DP.canvas}; padding: 1px 6px; border-radius: 4px; font-size: 12px; color: ${DP.ink}; }
|
|
3891
|
+
.error { color: oklch(45% 0.15 25); }
|
|
3892
|
+
|
|
3893
|
+
/* --- Stale hint --- */
|
|
3894
|
+
.stale {
|
|
3895
|
+
display: flex; align-items: center; gap: 8px;
|
|
3896
|
+
margin: 8px 4px 12px;
|
|
3897
|
+
padding: 8px 12px;
|
|
3898
|
+
background: ${DP.amberBg};
|
|
3899
|
+
border-radius: 10px;
|
|
3900
|
+
font-size: 11.5px; color: ${DP.ink2};
|
|
3901
|
+
}
|
|
3902
|
+
.stale-dot { width: 8px; height: 8px; border-radius: 50%; background: ${DP.amber}; flex-shrink: 0; }
|
|
3903
|
+
.stale-text { flex: 1; min-width: 0; }
|
|
3904
|
+
.stale-text strong { color: ${DP.ink}; font-weight: 600; }
|
|
3905
|
+
|
|
3906
|
+
/* --- Parsed-md fallback banner --- */
|
|
3907
|
+
.parsed-md-cta {
|
|
3908
|
+
margin: 8px 4px 14px;
|
|
3909
|
+
padding: 14px 16px;
|
|
3910
|
+
background: ${DP.tile};
|
|
3911
|
+
border: 1px dashed ${DP.hairline};
|
|
3912
|
+
border-radius: 12px;
|
|
3913
|
+
font-size: 12px; color: ${DP.ink2}; line-height: 1.55;
|
|
3914
|
+
}
|
|
3915
|
+
.parsed-md-cta strong { color: ${DP.ink}; display: block; margin-bottom: 4px; font-size: 13px; font-weight: 600; }
|
|
3916
|
+
.parsed-md-cta code { font-family: ${MONO}; background: ${DP.canvas}; padding: 1px 5px; border-radius: 4px; font-size: 11.5px; color: ${DP.ink}; }
|
|
3917
|
+
|
|
3918
|
+
/* --- Tile primitives --- */
|
|
3919
|
+
.tile {
|
|
3920
|
+
position: relative;
|
|
3921
|
+
background: ${DP.tile};
|
|
3922
|
+
border-radius: 16px;
|
|
3923
|
+
padding: 16px;
|
|
3924
|
+
margin: 0 4px 10px;
|
|
3925
|
+
}
|
|
3926
|
+
.tile-row { margin: 0 4px 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
3927
|
+
.tile-row .tile { margin: 0; }
|
|
3928
|
+
.tile-meta {
|
|
3929
|
+
display: flex; align-items: baseline; justify-content: space-between;
|
|
3930
|
+
gap: 10px;
|
|
3931
|
+
font-family: ${MONO};
|
|
3932
|
+
font-size: 10px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase;
|
|
3933
|
+
color: ${DP.meta};
|
|
3934
|
+
}
|
|
3935
|
+
.tile-meta .name { color: ${DP.ink}; font-weight: 600; letter-spacing: 0.05em; text-transform: none; font-family: ${FONT}; font-size: 12.5px; }
|
|
3936
|
+
|
|
3937
|
+
/* --- Color tile --- */
|
|
3938
|
+
.c-tile { cursor: pointer; transition: transform 0.2s ${EASE}; }
|
|
3939
|
+
.c-tile:hover { transform: translateY(-1px); }
|
|
3940
|
+
.c-hero {
|
|
3941
|
+
height: 72px; border-radius: 10px; margin-top: 10px;
|
|
3942
|
+
box-shadow: inset 0 0 0 1px oklch(0% 0 0 / 0.05);
|
|
3943
|
+
}
|
|
3944
|
+
.c-ramp {
|
|
3945
|
+
display: flex; gap: 0; height: 14px; border-radius: 4px; overflow: hidden;
|
|
3946
|
+
margin-top: 8px;
|
|
3947
|
+
box-shadow: inset 0 0 0 1px oklch(0% 0 0 / 0.04);
|
|
3948
|
+
}
|
|
3949
|
+
.c-ramp > span { flex: 1; }
|
|
3950
|
+
.c-desc { margin-top: 8px; font-size: 11.5px; line-height: 1.45; color: ${DP.ink2}; }
|
|
3951
|
+
|
|
3952
|
+
/* --- Type tile --- */
|
|
3953
|
+
.t-tile { }
|
|
3954
|
+
.t-specimen {
|
|
3955
|
+
margin: 4px 0 6px;
|
|
3956
|
+
color: ${DP.ink};
|
|
3957
|
+
line-height: 0.9;
|
|
3958
|
+
}
|
|
3959
|
+
.t-family { margin-top: 4px; font-size: 12px; font-weight: 600; color: ${DP.ink}; }
|
|
3960
|
+
.t-purpose { margin-top: 4px; font-size: 11px; line-height: 1.45; color: ${DP.ink2}; }
|
|
3961
|
+
|
|
3962
|
+
/* --- Shadow tile --- */
|
|
3963
|
+
.s-tile { }
|
|
3964
|
+
.s-surface {
|
|
3965
|
+
height: 60px; margin: 8px 2px 10px;
|
|
3966
|
+
background: ${DP.tile};
|
|
3967
|
+
border-radius: 10px;
|
|
3968
|
+
}
|
|
3969
|
+
.s-value { font-family: ${MONO}; font-size: 10px; color: ${DP.meta}; word-break: break-all; line-height: 1.4; }
|
|
3970
|
+
.s-purpose { margin-top: 4px; font-size: 11px; color: ${DP.ink2}; line-height: 1.45; }
|
|
3971
|
+
|
|
3972
|
+
/* --- Radii strip --- */
|
|
3973
|
+
.r-strip { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 10px; }
|
|
3974
|
+
.r-item { display: flex; flex-direction: column; align-items: center; gap: 4px; flex: 1; min-width: 60px; }
|
|
3975
|
+
.r-sample { width: 44px; height: 44px; background: ${DP.canvas}; box-shadow: inset 0 0 0 1px oklch(0% 0 0 / 0.08); }
|
|
3976
|
+
.r-label { font-family: ${MONO}; font-size: 10px; color: ${DP.meta}; letter-spacing: 0.05em; text-transform: uppercase; }
|
|
3977
|
+
.r-val { font-family: ${MONO}; font-size: 10px; color: ${DP.ink}; }
|
|
3978
|
+
|
|
3979
|
+
/* --- Component tile (hosts live primitives) --- */
|
|
3980
|
+
.cmp-tile { }
|
|
3981
|
+
.cmp-stage {
|
|
3982
|
+
margin: 12px -4px 0;
|
|
3983
|
+
padding: 18px 16px 10px;
|
|
3984
|
+
border-top: 1px solid ${DP.hairlineSoft};
|
|
3985
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
3986
|
+
gap: 14px;
|
|
3987
|
+
min-height: 68px;
|
|
3988
|
+
}
|
|
3989
|
+
.cmp-stage + .cmp-stage { border-top: 1px dashed ${DP.hairlineSoft}; }
|
|
3990
|
+
.cmp-sublabel { font-family: ${MONO}; font-size: 10px; color: ${DP.meta}; letter-spacing: 0.06em; }
|
|
3991
|
+
.cmp-kind { font-family: ${MONO}; font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: ${DP.meta}; }
|
|
3992
|
+
|
|
3993
|
+
/* --- Collapsible --- */
|
|
3994
|
+
.coll {
|
|
3995
|
+
margin: 0 4px 8px;
|
|
3996
|
+
background: ${DP.tile};
|
|
3997
|
+
border-radius: 12px;
|
|
3998
|
+
overflow: hidden;
|
|
3999
|
+
}
|
|
4000
|
+
.coll-head {
|
|
4001
|
+
display: flex; align-items: center; gap: 10px;
|
|
4002
|
+
width: 100%;
|
|
4003
|
+
padding: 12px 14px;
|
|
4004
|
+
background: transparent; border: none;
|
|
4005
|
+
cursor: pointer; text-align: left;
|
|
4006
|
+
font-family: ${FONT}; font-size: 12.5px; font-weight: 600; color: ${DP.ink};
|
|
4007
|
+
transition: background 0.12s ease;
|
|
4008
|
+
}
|
|
4009
|
+
.coll-head:hover { background: ${DP.tileAlt}; }
|
|
4010
|
+
.coll-chev {
|
|
4011
|
+
width: 12px; height: 12px; flex-shrink: 0;
|
|
4012
|
+
color: ${DP.meta};
|
|
4013
|
+
transition: transform 0.2s ${EASE};
|
|
4014
|
+
}
|
|
4015
|
+
.coll[data-open="true"] .coll-chev { transform: rotate(90deg); }
|
|
4016
|
+
.coll-count { margin-left: auto; font-family: ${MONO}; font-size: 10px; color: ${DP.meta}; letter-spacing: 0.05em; }
|
|
4017
|
+
.coll-body { padding: 0 14px 14px; display: none; }
|
|
4018
|
+
.coll[data-open="true"] .coll-body { display: block; }
|
|
4019
|
+
|
|
4020
|
+
.rule-card {
|
|
4021
|
+
padding: 10px 0;
|
|
4022
|
+
border-top: 1px solid ${DP.hairlineSoft};
|
|
4023
|
+
}
|
|
4024
|
+
.rule-card:first-child { border-top: none; padding-top: 2px; }
|
|
4025
|
+
.rule-card .name { font-size: 11.5px; font-weight: 700; color: ${DP.ink}; margin-bottom: 3px; }
|
|
4026
|
+
.rule-card .name .section { font-family: ${MONO}; font-size: 9px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; color: ${DP.meta}; margin-left: 8px; }
|
|
4027
|
+
.rule-card .body { font-size: 11.5px; color: ${DP.ink2}; line-height: 1.5; }
|
|
4028
|
+
|
|
4029
|
+
.coll .dos { display: grid; gap: 0; margin-top: 2px; }
|
|
4030
|
+
.coll .do, .coll .dont {
|
|
4031
|
+
position: relative;
|
|
4032
|
+
padding: 8px 0 8px 22px;
|
|
4033
|
+
font-size: 11.5px; line-height: 1.5; color: ${DP.ink2};
|
|
4034
|
+
border-top: 1px solid ${DP.hairlineSoft};
|
|
4035
|
+
}
|
|
4036
|
+
.coll .do:first-child, .coll .dont:first-child,
|
|
4037
|
+
.coll .do:first-of-type { border-top: none; }
|
|
4038
|
+
.coll .do + .dont { border-top: 1px solid ${DP.hairlineSoft}; }
|
|
4039
|
+
.coll .do::before, .coll .dont::before {
|
|
4040
|
+
content: ''; position: absolute; left: 4px; top: 13px;
|
|
4041
|
+
width: 8px; height: 8px; border-radius: 50%;
|
|
4042
|
+
}
|
|
4043
|
+
.coll .do::before { background: oklch(62% 0.16 145); }
|
|
4044
|
+
.coll .dont::before { background: oklch(58% 0.22 25); }
|
|
4045
|
+
|
|
4046
|
+
.coll .overview-body {
|
|
4047
|
+
font-size: 12px; line-height: 1.55; color: ${DP.ink2};
|
|
4048
|
+
}
|
|
4049
|
+
.coll .overview-body .north-star {
|
|
4050
|
+
display: block; font-family: ${FONT}; font-style: italic;
|
|
4051
|
+
font-size: 15px; line-height: 1.3; color: ${DP.ink};
|
|
4052
|
+
margin-bottom: 8px;
|
|
4053
|
+
}
|
|
4054
|
+
.coll .overview-body p { margin: 0 0 8px; }
|
|
4055
|
+
.coll .overview-body ul { margin: 6px 0 0; padding-left: 16px; font-size: 11.5px; }
|
|
4056
|
+
.coll .overview-body li { margin-bottom: 3px; }
|
|
4057
|
+
|
|
4058
|
+
/* --- raw tab markdown (unchanged layout, neutralized palette) --- */
|
|
4059
|
+
.md { padding: 4px 10px 20px; font-size: 13px; line-height: 1.6; color: ${DP.ink}; }
|
|
4060
|
+
.md h1, .md h2, .md h3, .md h4 { margin: 20px 0 8px; color: ${DP.ink}; font-weight: 600; }
|
|
4061
|
+
.md h1 { font-size: 18px; }
|
|
4062
|
+
.md h2 { font-size: 15px; padding-bottom: 4px; border-bottom: 1px solid ${DP.hairlineSoft}; }
|
|
4063
|
+
.md h3 { font-size: 13px; }
|
|
4064
|
+
.md h4 { font-size: 12px; color: ${DP.meta}; }
|
|
4065
|
+
.md p { margin: 0 0 10px; }
|
|
4066
|
+
.md ul, .md ol { margin: 0 0 10px; padding-left: 20px; }
|
|
4067
|
+
.md li { margin-bottom: 4px; }
|
|
4068
|
+
.md code { font-family: ${MONO}; font-size: 12px; background: ${DP.canvas}; padding: 1px 5px; border-radius: 4px; }
|
|
4069
|
+
.md pre { font-family: ${MONO}; font-size: 12px; background: ${DP.canvas}; padding: 10px 12px; border-radius: 8px; overflow-x: auto; margin: 0 0 10px; }
|
|
4070
|
+
.md pre code { background: none; padding: 0; }
|
|
4071
|
+
.md strong { font-weight: 700; }
|
|
4072
|
+
.md em { font-style: italic; }
|
|
4073
|
+
.md a { color: ${DP.ink}; text-decoration: underline; }
|
|
4074
|
+
.md hr { border: none; border-top: 1px solid ${DP.hairlineSoft}; margin: 16px 0; }
|
|
4075
|
+
`;
|
|
4076
|
+
}
|
|
4077
|
+
|
|
4078
|
+
function renderDesignChrome() {
|
|
4079
|
+
const root = designShadow.querySelector('.root');
|
|
4080
|
+
root.innerHTML = '';
|
|
4081
|
+
|
|
4082
|
+
// (Panel toggle lives in the global bar — no floating FAB.)
|
|
4083
|
+
// Panel
|
|
4084
|
+
const panel = document.createElement('aside');
|
|
4085
|
+
panel.className = 'panel';
|
|
4086
|
+
panel.setAttribute('data-open', designState.open ? 'true' : 'false');
|
|
4087
|
+
panel.appendChild(buildDesignHeader());
|
|
4088
|
+
const body = document.createElement('div');
|
|
4089
|
+
body.className = 'panel-body';
|
|
4090
|
+
body.id = 'panel-body';
|
|
4091
|
+
panel.appendChild(body);
|
|
4092
|
+
root.appendChild(panel);
|
|
4093
|
+
|
|
4094
|
+
renderDesignBody();
|
|
4095
|
+
}
|
|
4096
|
+
|
|
4097
|
+
function buildDesignHeader() {
|
|
4098
|
+
const header = document.createElement('div');
|
|
4099
|
+
header.className = 'panel-header';
|
|
4100
|
+
|
|
4101
|
+
const title = document.createElement('div');
|
|
4102
|
+
title.className = 'panel-title';
|
|
4103
|
+
title.textContent = 'DESIGN.md';
|
|
4104
|
+
header.appendChild(title);
|
|
4105
|
+
|
|
4106
|
+
const tabs = document.createElement('div');
|
|
4107
|
+
tabs.className = 'tabs';
|
|
4108
|
+
for (const t of [['visual', 'Visual'], ['raw', 'Raw']]) {
|
|
4109
|
+
const btn = document.createElement('button');
|
|
4110
|
+
btn.className = 'tab';
|
|
4111
|
+
btn.textContent = t[1];
|
|
4112
|
+
btn.setAttribute('data-active', designState.tab === t[0] ? 'true' : 'false');
|
|
4113
|
+
btn.addEventListener('click', () => {
|
|
4114
|
+
if (designState.tab === t[0]) return;
|
|
4115
|
+
designState.tab = t[0];
|
|
4116
|
+
saveDesignPrefs();
|
|
4117
|
+
renderDesignChrome();
|
|
4118
|
+
if (t[0] === 'raw' && designState.raw === null && !designState.loading) {
|
|
4119
|
+
fetchDesignSystem(); // raw is part of the same fetch pair
|
|
4120
|
+
}
|
|
4121
|
+
});
|
|
4122
|
+
tabs.appendChild(btn);
|
|
4123
|
+
}
|
|
4124
|
+
header.appendChild(tabs);
|
|
4125
|
+
|
|
4126
|
+
const close = document.createElement('button');
|
|
4127
|
+
close.className = 'panel-close';
|
|
4128
|
+
close.innerHTML = '✕';
|
|
4129
|
+
close.setAttribute('aria-label', 'Close panel');
|
|
4130
|
+
close.addEventListener('click', toggleDesignPanel);
|
|
4131
|
+
header.appendChild(close);
|
|
4132
|
+
|
|
4133
|
+
return header;
|
|
4134
|
+
}
|
|
4135
|
+
|
|
4136
|
+
function toggleDesignPanel() {
|
|
4137
|
+
designState.open = !designState.open;
|
|
4138
|
+
renderDesignChrome();
|
|
4139
|
+
updateGlobalBarState();
|
|
4140
|
+
if (designState.open && designState.present === null && !designState.loading) {
|
|
4141
|
+
fetchDesignSystem();
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4144
|
+
|
|
4145
|
+
async function fetchDesignSystem() {
|
|
4146
|
+
designState.loading = true;
|
|
4147
|
+
designState.error = null;
|
|
4148
|
+
renderDesignBody();
|
|
4149
|
+
try {
|
|
4150
|
+
const [jsonRes, rawRes] = await Promise.all([
|
|
4151
|
+
fetch(`http://localhost:${PORT}/design-system.json?token=${TOKEN}`, { cache: 'no-store' }),
|
|
4152
|
+
fetch(`http://localhost:${PORT}/design-system/raw?token=${TOKEN}`, { cache: 'no-store' }),
|
|
4153
|
+
]);
|
|
4154
|
+
const jsonData = await jsonRes.json();
|
|
4155
|
+
designState.present = jsonData.present === true;
|
|
4156
|
+
designState.parsed = jsonData.parsed || null;
|
|
4157
|
+
designState.sidecar = jsonData.sidecar || null;
|
|
4158
|
+
designState.hasMd = !!jsonData.hasMd;
|
|
4159
|
+
designState.hasSidecar = !!jsonData.hasSidecar;
|
|
4160
|
+
designState.mdNewerThanJson = !!jsonData.mdNewerThanJson;
|
|
4161
|
+
designState.raw = designState.present && rawRes.ok ? await rawRes.text() : null;
|
|
4162
|
+
designState.error = jsonData.parseError || jsonData.sidecarError || null;
|
|
4163
|
+
} catch (err) {
|
|
4164
|
+
designState.error = err?.message || 'Failed to load design system.';
|
|
4165
|
+
} finally {
|
|
4166
|
+
designState.loading = false;
|
|
4167
|
+
renderDesignChrome(); // refresh title from data
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
4170
|
+
|
|
4171
|
+
function renderDesignBody() {
|
|
4172
|
+
const body = designShadow.querySelector('#panel-body');
|
|
4173
|
+
if (!body) return;
|
|
4174
|
+
body.innerHTML = '';
|
|
4175
|
+
|
|
4176
|
+
if (designState.loading) {
|
|
4177
|
+
body.appendChild(msgDiv('loading', 'Loading design system…'));
|
|
4178
|
+
return;
|
|
4179
|
+
}
|
|
4180
|
+
if (designState.error) {
|
|
4181
|
+
body.appendChild(msgDiv('error', designState.error));
|
|
4182
|
+
return;
|
|
4183
|
+
}
|
|
4184
|
+
if (designState.present === false) {
|
|
4185
|
+
const empty = document.createElement('div');
|
|
4186
|
+
empty.className = 'empty';
|
|
4187
|
+
empty.innerHTML = `<strong>No DESIGN.md yet</strong>Create one by running <code>/impeccable document</code> in your terminal, then re-open this panel.`;
|
|
4188
|
+
body.appendChild(empty);
|
|
4189
|
+
return;
|
|
4190
|
+
}
|
|
4191
|
+
|
|
4192
|
+
if (designState.tab === 'raw') {
|
|
4193
|
+
renderRawTab(body, designState.raw || '');
|
|
4194
|
+
return;
|
|
4195
|
+
}
|
|
4196
|
+
|
|
4197
|
+
// Visual tab — single unified render path.
|
|
4198
|
+
if (designState.mdNewerThanJson) body.appendChild(renderStaleHint());
|
|
4199
|
+
if (designState.hasMd && !designState.hasSidecar) {
|
|
4200
|
+
body.appendChild(renderParsedMdCta());
|
|
4201
|
+
}
|
|
4202
|
+
renderDesignVisual(body, designState.parsed, designState.sidecar);
|
|
4203
|
+
}
|
|
4204
|
+
|
|
4205
|
+
function msgDiv(cls, text) {
|
|
4206
|
+
const d = document.createElement('div');
|
|
4207
|
+
d.className = cls;
|
|
4208
|
+
d.textContent = text;
|
|
4209
|
+
return d;
|
|
4210
|
+
}
|
|
4211
|
+
|
|
4212
|
+
function renderStaleHint() {
|
|
4213
|
+
const box = document.createElement('div');
|
|
4214
|
+
box.className = 'stale';
|
|
4215
|
+
box.innerHTML = `
|
|
4216
|
+
<span class="stale-dot"></span>
|
|
4217
|
+
<span class="stale-text"><strong>DESIGN.md is newer than .impeccable/design.json.</strong> Run <code>/impeccable document</code> to refresh the sidecar.</span>
|
|
4218
|
+
`;
|
|
4219
|
+
return box;
|
|
4220
|
+
}
|
|
4221
|
+
|
|
4222
|
+
function renderParsedMdCta() {
|
|
4223
|
+
const box = document.createElement('div');
|
|
4224
|
+
box.className = 'parsed-md-cta';
|
|
4225
|
+
box.innerHTML = `<strong>Basic view</strong>This panel reads the tokens in your <code>DESIGN.md</code> frontmatter. Running <code>/impeccable document</code> also generates a <code>.impeccable/design.json</code> sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`;
|
|
4226
|
+
return box;
|
|
4227
|
+
}
|
|
4228
|
+
|
|
4229
|
+
// --- Unified render: merge parsed DESIGN.md frontmatter with sidecar v2 ---
|
|
4230
|
+
|
|
4231
|
+
function renderDesignVisual(body, parsed, sidecar) {
|
|
4232
|
+
const frontmatter = parsed?.frontmatter || {};
|
|
4233
|
+
const extensions = sidecar?.extensions || {};
|
|
4234
|
+
const proseColors = parsed?.colors || null;
|
|
4235
|
+
|
|
4236
|
+
const colors = buildColorModels(frontmatter.colors, extensions.colorMeta, proseColors);
|
|
4237
|
+
if (colors.length) renderColorTiles(body, colors);
|
|
4238
|
+
|
|
4239
|
+
const types = buildTypographyModels(frontmatter.typography, extensions.typographyMeta);
|
|
4240
|
+
if (types.length) renderTypeTiles(body, types);
|
|
4241
|
+
|
|
4242
|
+
const radii = buildRadiiModels(frontmatter.rounded);
|
|
4243
|
+
if (radii.length) renderRadiiTile(body, radii);
|
|
4244
|
+
|
|
4245
|
+
if (extensions.shadows?.length) renderShadowTiles(body, extensions.shadows);
|
|
4246
|
+
|
|
4247
|
+
const components = sidecar?.components || [];
|
|
4248
|
+
if (components.length) renderComponentTiles(body, components);
|
|
4249
|
+
|
|
4250
|
+
// Narrative: sidecar wins if present (richer, agent-curated). Otherwise
|
|
4251
|
+
// synthesize from prose sections.
|
|
4252
|
+
const narrative = sidecar?.narrative || synthesizeNarrative(parsed);
|
|
4253
|
+
if (narrative.rules?.length) body.appendChild(renderRulesCollapsible(narrative.rules));
|
|
4254
|
+
if ((narrative.dos?.length || narrative.donts?.length)) body.appendChild(renderDosDontsCollapsible(narrative));
|
|
4255
|
+
if (narrative.overview || narrative.northStar || narrative.keyCharacteristics?.length) {
|
|
4256
|
+
body.appendChild(renderOverviewCollapsible(narrative));
|
|
4257
|
+
}
|
|
4258
|
+
|
|
4259
|
+
if (body.childElementCount === 0) {
|
|
4260
|
+
body.appendChild(msgDiv('empty', 'No design system data available.'));
|
|
4261
|
+
}
|
|
4262
|
+
}
|
|
4263
|
+
|
|
4264
|
+
// Frontmatter primitives + sidecar colorMeta → tile-ready color models.
|
|
4265
|
+
// A matching prose bullet (when the slug sits in the bullet text) supplies
|
|
4266
|
+
// description as a last-resort fallback.
|
|
4267
|
+
function buildColorModels(fmColors, colorMeta, proseColors) {
|
|
4268
|
+
if (!fmColors) return [];
|
|
4269
|
+
const meta = colorMeta || {};
|
|
4270
|
+
return Object.entries(fmColors).map(([key, value]) => {
|
|
4271
|
+
const m = meta[key] || {};
|
|
4272
|
+
return {
|
|
4273
|
+
role: m.role || humanizeKey(key),
|
|
4274
|
+
name: m.displayName || humanizeKey(key),
|
|
4275
|
+
value: value,
|
|
4276
|
+
canonical: m.canonical || null,
|
|
4277
|
+
description: m.description || findProseDescription(proseColors, key, m.displayName),
|
|
4278
|
+
tonalRamp: m.tonalRamp || null,
|
|
4279
|
+
};
|
|
4280
|
+
});
|
|
4281
|
+
}
|
|
4282
|
+
|
|
4283
|
+
function buildTypographyModels(fmTypography, typographyMeta) {
|
|
4284
|
+
if (!fmTypography) return [];
|
|
4285
|
+
const meta = typographyMeta || {};
|
|
4286
|
+
return Object.entries(fmTypography).map(([key, spec]) => {
|
|
4287
|
+
const m = meta[key] || {};
|
|
4288
|
+
const { family, fallback } = splitFontFamily(spec?.fontFamily);
|
|
4289
|
+
return {
|
|
4290
|
+
role: key,
|
|
4291
|
+
name: m.displayName || humanizeKey(key),
|
|
4292
|
+
family,
|
|
4293
|
+
fallback,
|
|
4294
|
+
weight: spec?.fontWeight ?? 400,
|
|
4295
|
+
// fontStyle isn't in Stitch's frontmatter schema; the sidecar carries
|
|
4296
|
+
// it when a role is rendered in italic (e.g. display italic).
|
|
4297
|
+
style: m.style || 'normal',
|
|
4298
|
+
sampleSize: spec?.fontSize || '1rem',
|
|
4299
|
+
lineHeight: spec?.lineHeight != null ? String(spec.lineHeight) : '',
|
|
4300
|
+
letterSpacing: spec?.letterSpacing,
|
|
4301
|
+
purpose: m.purpose,
|
|
4302
|
+
};
|
|
4303
|
+
});
|
|
4304
|
+
}
|
|
4305
|
+
|
|
4306
|
+
function buildRadiiModels(fmRounded) {
|
|
4307
|
+
if (!fmRounded) return [];
|
|
4308
|
+
return Object.entries(fmRounded).map(([name, value]) => ({ name, value }));
|
|
4309
|
+
}
|
|
4310
|
+
|
|
4311
|
+
function splitFontFamily(stack) {
|
|
4312
|
+
if (!stack || typeof stack !== 'string') return { family: '', fallback: '' };
|
|
4313
|
+
const parts = stack.split(',').map((s) => s.trim().replace(/^['"]|['"]$/g, ''));
|
|
4314
|
+
return { family: parts[0] || '', fallback: parts.slice(1).join(', ') };
|
|
4315
|
+
}
|
|
4316
|
+
|
|
4317
|
+
function humanizeKey(k) {
|
|
4318
|
+
return String(k || '').replace(/[-_]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
4319
|
+
}
|
|
4320
|
+
|
|
4321
|
+
function findProseDescription(proseColors, key, displayName) {
|
|
4322
|
+
if (!proseColors || !proseColors.groups) return null;
|
|
4323
|
+
const needles = [key, displayName].filter(Boolean).map((s) => s.toLowerCase());
|
|
4324
|
+
for (const g of proseColors.groups) {
|
|
4325
|
+
for (const c of g.colors || []) {
|
|
4326
|
+
const hay = String(c.name || '').toLowerCase();
|
|
4327
|
+
if (hay && needles.some((n) => hay.includes(n) || n.includes(hay))) {
|
|
4328
|
+
return c.description || null;
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
return null;
|
|
4333
|
+
}
|
|
4334
|
+
|
|
4335
|
+
function synthesizeNarrative(parsed) {
|
|
4336
|
+
if (!parsed) return {};
|
|
4337
|
+
const md = parsed;
|
|
4338
|
+
return {
|
|
4339
|
+
northStar: md.overview?.creativeNorthStar,
|
|
4340
|
+
overview: (md.overview?.philosophy || []).join(' '),
|
|
4341
|
+
keyCharacteristics: md.overview?.keyCharacteristics || [],
|
|
4342
|
+
rules: [
|
|
4343
|
+
...(md.colors?.rules || []).map((r) => ({ ...r, section: 'colors' })),
|
|
4344
|
+
...(md.typography?.rules || []).map((r) => ({ ...r, section: 'typography' })),
|
|
4345
|
+
...(md.elevation?.rules || []).map((r) => ({ ...r, section: 'elevation' })),
|
|
4346
|
+
],
|
|
4347
|
+
dos: md.dosDonts?.dos || [],
|
|
4348
|
+
donts: md.dosDonts?.donts || [],
|
|
4349
|
+
};
|
|
4350
|
+
}
|
|
4351
|
+
|
|
4352
|
+
function renderColorTiles(body, colors) {
|
|
4353
|
+
for (const c of colors) {
|
|
4354
|
+
const tile = document.createElement('div');
|
|
4355
|
+
tile.className = 'tile c-tile';
|
|
4356
|
+
tile.title = 'Click to copy';
|
|
4357
|
+
tile.addEventListener('click', () => copyToClipboard(c.value));
|
|
4358
|
+
|
|
4359
|
+
const meta = document.createElement('div');
|
|
4360
|
+
meta.className = 'tile-meta';
|
|
4361
|
+
meta.innerHTML = `<span class="name">${escapeHtml(c.name || c.role || 'Color')}</span><span>${escapeHtml(c.value || '')}</span>`;
|
|
4362
|
+
tile.appendChild(meta);
|
|
4363
|
+
|
|
4364
|
+
const hero = document.createElement('div');
|
|
4365
|
+
hero.className = 'c-hero';
|
|
4366
|
+
hero.style.background = c.value;
|
|
4367
|
+
tile.appendChild(hero);
|
|
4368
|
+
|
|
4369
|
+
const ramp = synthesizeRamp(c);
|
|
4370
|
+
if (ramp.length) {
|
|
4371
|
+
const r = document.createElement('div');
|
|
4372
|
+
r.className = 'c-ramp';
|
|
4373
|
+
r.innerHTML = ramp.map((v) => `<span style="background:${cssSafe(v)}"></span>`).join('');
|
|
4374
|
+
tile.appendChild(r);
|
|
4375
|
+
}
|
|
4376
|
+
|
|
4377
|
+
if (c.description) {
|
|
4378
|
+
const d = document.createElement('div');
|
|
4379
|
+
d.className = 'c-desc';
|
|
4380
|
+
d.textContent = c.description;
|
|
4381
|
+
tile.appendChild(d);
|
|
4382
|
+
}
|
|
4383
|
+
body.appendChild(tile);
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
|
|
4387
|
+
function synthesizeRamp(c) {
|
|
4388
|
+
if (c.tonalRamp?.length) return c.tonalRamp;
|
|
4389
|
+
// If base value is OKLCH, synthesize an 8-step ramp across lightness.
|
|
4390
|
+
const m = typeof c.value === 'string' && c.value.match(/^oklch\(\s*([\d.]+)%\s+([\d.]+)\s+([\d.]+)\s*(?:\/\s*([\d.]+))?\s*\)$/i);
|
|
4391
|
+
if (!m) return [];
|
|
4392
|
+
const [, , chroma, hue] = m;
|
|
4393
|
+
const steps = [20, 32, 44, 56, 68, 80, 90, 96];
|
|
4394
|
+
return steps.map((l) => `oklch(${l}% ${chroma} ${hue})`);
|
|
4395
|
+
}
|
|
4396
|
+
|
|
4397
|
+
function renderTypeTiles(body, types) {
|
|
4398
|
+
for (const t of types) {
|
|
4399
|
+
const tile = document.createElement('div');
|
|
4400
|
+
tile.className = 'tile t-tile';
|
|
4401
|
+
|
|
4402
|
+
const meta = document.createElement('div');
|
|
4403
|
+
meta.className = 'tile-meta';
|
|
4404
|
+
meta.innerHTML = `<span>${escapeHtml(t.role || '')}</span><span>${escapeHtml(t.weight || '')} ${escapeHtml(t.style === 'italic' ? 'italic' : '')}</span>`;
|
|
4405
|
+
tile.appendChild(meta);
|
|
4406
|
+
|
|
4407
|
+
const specimen = document.createElement('div');
|
|
4408
|
+
specimen.className = 't-specimen';
|
|
4409
|
+
specimen.textContent = 'Aa';
|
|
4410
|
+
specimen.style.fontFamily = fontStack(t);
|
|
4411
|
+
specimen.style.fontWeight = String(t.weight || 400);
|
|
4412
|
+
specimen.style.fontStyle = t.style || 'normal';
|
|
4413
|
+
specimen.style.fontSize = '56px'; // Fixed specimen size — compare faces, not scales.
|
|
4414
|
+
specimen.style.letterSpacing = 'normal';
|
|
4415
|
+
specimen.style.textTransform = 'none';
|
|
4416
|
+
tile.appendChild(specimen);
|
|
4417
|
+
|
|
4418
|
+
// The system's actual sample size for this role, shown as small mono meta below.
|
|
4419
|
+
if (t.sampleSize) {
|
|
4420
|
+
const scale = document.createElement('div');
|
|
4421
|
+
scale.style.cssText = 'font-family:' + MONO + '; font-size: 10px; color:' + DP.meta + '; margin-top: 2px;';
|
|
4422
|
+
scale.textContent = t.sampleSize;
|
|
4423
|
+
tile.appendChild(scale);
|
|
4424
|
+
}
|
|
4425
|
+
|
|
4426
|
+
const family = document.createElement('div');
|
|
4427
|
+
family.className = 't-family';
|
|
4428
|
+
family.textContent = t.family || t.name || '';
|
|
4429
|
+
tile.appendChild(family);
|
|
4430
|
+
|
|
4431
|
+
if (t.purpose) {
|
|
4432
|
+
const p = document.createElement('div');
|
|
4433
|
+
p.className = 't-purpose';
|
|
4434
|
+
p.textContent = t.purpose;
|
|
4435
|
+
tile.appendChild(p);
|
|
4436
|
+
}
|
|
4437
|
+
body.appendChild(tile);
|
|
4438
|
+
}
|
|
4439
|
+
}
|
|
4440
|
+
|
|
4441
|
+
function fontStack(t) {
|
|
4442
|
+
const fam = t.family || '';
|
|
4443
|
+
const fb = t.fallback || '';
|
|
4444
|
+
if (fam && /[,\s]/.test(fam) && !fam.includes("'") && !fam.includes('"')) {
|
|
4445
|
+
return `"${fam}", ${fb}`;
|
|
4446
|
+
}
|
|
4447
|
+
return fam && fb ? `"${fam}", ${fb}` : (fam || fb);
|
|
4448
|
+
}
|
|
4449
|
+
|
|
4450
|
+
function renderRadiiTile(body, radii) {
|
|
4451
|
+
const tile = document.createElement('div');
|
|
4452
|
+
tile.className = 'tile';
|
|
4453
|
+
const meta = document.createElement('div');
|
|
4454
|
+
meta.className = 'tile-meta';
|
|
4455
|
+
meta.innerHTML = `<span class="name">Corner Radii</span><span>${radii.length}</span>`;
|
|
4456
|
+
tile.appendChild(meta);
|
|
4457
|
+
|
|
4458
|
+
const strip = document.createElement('div');
|
|
4459
|
+
strip.className = 'r-strip';
|
|
4460
|
+
for (const r of radii) {
|
|
4461
|
+
const item = document.createElement('div');
|
|
4462
|
+
item.className = 'r-item';
|
|
4463
|
+
const s = document.createElement('div');
|
|
4464
|
+
s.className = 'r-sample';
|
|
4465
|
+
s.style.borderRadius = r.value || '0';
|
|
4466
|
+
item.appendChild(s);
|
|
4467
|
+
const lbl = document.createElement('div');
|
|
4468
|
+
lbl.className = 'r-label';
|
|
4469
|
+
lbl.textContent = r.name || '';
|
|
4470
|
+
item.appendChild(lbl);
|
|
4471
|
+
const val = document.createElement('div');
|
|
4472
|
+
val.className = 'r-val';
|
|
4473
|
+
val.textContent = r.value || '';
|
|
4474
|
+
item.appendChild(val);
|
|
4475
|
+
strip.appendChild(item);
|
|
4476
|
+
}
|
|
4477
|
+
tile.appendChild(strip);
|
|
4478
|
+
body.appendChild(tile);
|
|
4479
|
+
}
|
|
4480
|
+
|
|
4481
|
+
function renderShadowTiles(body, shadows) {
|
|
4482
|
+
for (const sh of shadows) {
|
|
4483
|
+
const tile = document.createElement('div');
|
|
4484
|
+
tile.className = 'tile s-tile';
|
|
4485
|
+
|
|
4486
|
+
const meta = document.createElement('div');
|
|
4487
|
+
meta.className = 'tile-meta';
|
|
4488
|
+
meta.innerHTML = `<span class="name">${escapeHtml(sh.name || 'Shadow')}</span><span>Elevation</span>`;
|
|
4489
|
+
tile.appendChild(meta);
|
|
4490
|
+
|
|
4491
|
+
const surface = document.createElement('div');
|
|
4492
|
+
surface.className = 's-surface';
|
|
4493
|
+
surface.style.boxShadow = sh.value || 'none';
|
|
4494
|
+
tile.appendChild(surface);
|
|
4495
|
+
|
|
4496
|
+
const val = document.createElement('div');
|
|
4497
|
+
val.className = 's-value';
|
|
4498
|
+
val.textContent = sh.value || '';
|
|
4499
|
+
tile.appendChild(val);
|
|
4500
|
+
|
|
4501
|
+
if (sh.purpose) {
|
|
4502
|
+
const p = document.createElement('div');
|
|
4503
|
+
p.className = 's-purpose';
|
|
4504
|
+
p.textContent = sh.purpose;
|
|
4505
|
+
tile.appendChild(p);
|
|
4506
|
+
}
|
|
4507
|
+
body.appendChild(tile);
|
|
4508
|
+
}
|
|
4509
|
+
}
|
|
4510
|
+
|
|
4511
|
+
function renderComponentTiles(body, components) {
|
|
4512
|
+
// Group consecutive components that share a kind into one tile. This avoids
|
|
4513
|
+
// a pile of one-component tiles (e.g., three button variants = three tiles)
|
|
4514
|
+
// and reads more like a proper category.
|
|
4515
|
+
const groups = groupByKind(components);
|
|
4516
|
+
|
|
4517
|
+
for (const group of groups) {
|
|
4518
|
+
const tile = document.createElement('div');
|
|
4519
|
+
tile.className = 'tile cmp-tile';
|
|
4520
|
+
|
|
4521
|
+
const meta = document.createElement('div');
|
|
4522
|
+
meta.className = 'tile-meta';
|
|
4523
|
+
const groupTitle = group.length === 1
|
|
4524
|
+
? (group[0].name || group[0].kind || 'Component')
|
|
4525
|
+
: titleForKind(group[0].kind, group.length);
|
|
4526
|
+
meta.innerHTML = `<span class="name">${escapeHtml(groupTitle)}</span><span class="cmp-kind">${escapeHtml(group[0].kind || '')}</span>`;
|
|
4527
|
+
tile.appendChild(meta);
|
|
4528
|
+
|
|
4529
|
+
for (const c of group) {
|
|
4530
|
+
const stage = document.createElement('div');
|
|
4531
|
+
stage.className = 'cmp-stage';
|
|
4532
|
+
|
|
4533
|
+
// Render the component in its own shadow root so its CSS can't bleed.
|
|
4534
|
+
const host = document.createElement('div');
|
|
4535
|
+
const sub = host.attachShadow({ mode: 'open' });
|
|
4536
|
+
const style = document.createElement('style');
|
|
4537
|
+
style.textContent = c.css || '';
|
|
4538
|
+
sub.appendChild(style);
|
|
4539
|
+
const container = document.createElement('div');
|
|
4540
|
+
container.innerHTML = c.html || '';
|
|
4541
|
+
sub.appendChild(container);
|
|
4542
|
+
stage.appendChild(host);
|
|
4543
|
+
|
|
4544
|
+
// Show component name as a sublabel only when the tile groups >1 item,
|
|
4545
|
+
// or when the component's display name differs from its kind.
|
|
4546
|
+
const showSublabel = group.length > 1;
|
|
4547
|
+
if (showSublabel) {
|
|
4548
|
+
const lbl = document.createElement('div');
|
|
4549
|
+
lbl.className = 'cmp-sublabel';
|
|
4550
|
+
lbl.textContent = c.name || '';
|
|
4551
|
+
stage.appendChild(lbl);
|
|
4552
|
+
}
|
|
4553
|
+
tile.appendChild(stage);
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4556
|
+
// Single shared description if all items carry the same one; otherwise
|
|
4557
|
+
// skip — per-item descriptions clutter a grouped tile.
|
|
4558
|
+
if (group.length === 1 && group[0].description) {
|
|
4559
|
+
const d = document.createElement('div');
|
|
4560
|
+
d.className = 'c-desc';
|
|
4561
|
+
d.textContent = group[0].description;
|
|
4562
|
+
tile.appendChild(d);
|
|
4563
|
+
}
|
|
4564
|
+
body.appendChild(tile);
|
|
4565
|
+
}
|
|
4566
|
+
}
|
|
4567
|
+
|
|
4568
|
+
function groupByKind(components) {
|
|
4569
|
+
const groups = [];
|
|
4570
|
+
for (const c of components) {
|
|
4571
|
+
const last = groups[groups.length - 1];
|
|
4572
|
+
if (last && last[0].kind && c.kind === last[0].kind) {
|
|
4573
|
+
last.push(c);
|
|
4574
|
+
} else {
|
|
4575
|
+
groups.push([c]);
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
return groups;
|
|
4579
|
+
}
|
|
4580
|
+
|
|
4581
|
+
function titleForKind(kind, count) {
|
|
4582
|
+
const labels = {
|
|
4583
|
+
button: 'Buttons',
|
|
4584
|
+
input: 'Inputs',
|
|
4585
|
+
nav: 'Navigation',
|
|
4586
|
+
chip: 'Chips',
|
|
4587
|
+
card: 'Cards',
|
|
4588
|
+
custom: 'Components',
|
|
4589
|
+
};
|
|
4590
|
+
return labels[kind] || (kind ? kind.charAt(0).toUpperCase() + kind.slice(1) + 's' : 'Components');
|
|
4591
|
+
}
|
|
4592
|
+
|
|
4593
|
+
// --- Collapsibles ---------------------------------------------------------
|
|
4594
|
+
|
|
4595
|
+
function buildCollapsible(key, label, count) {
|
|
4596
|
+
const wrap = document.createElement('div');
|
|
4597
|
+
wrap.className = 'coll';
|
|
4598
|
+
wrap.setAttribute('data-open', designState.collapsed[key] ? 'false' : 'true');
|
|
4599
|
+
|
|
4600
|
+
const head = document.createElement('button');
|
|
4601
|
+
head.className = 'coll-head';
|
|
4602
|
+
head.innerHTML = `
|
|
4603
|
+
<svg class="coll-chev" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 2.5L8 6 4 9.5"/></svg>
|
|
4604
|
+
<span>${escapeHtml(label)}</span>
|
|
4605
|
+
${count != null ? `<span class="coll-count">${escapeHtml(String(count))}</span>` : ''}
|
|
4606
|
+
`;
|
|
4607
|
+
head.addEventListener('click', () => {
|
|
4608
|
+
designState.collapsed[key] = !designState.collapsed[key];
|
|
4609
|
+
saveDesignPrefs();
|
|
4610
|
+
renderDesignBody();
|
|
4611
|
+
});
|
|
4612
|
+
wrap.appendChild(head);
|
|
4613
|
+
|
|
4614
|
+
const body = document.createElement('div');
|
|
4615
|
+
body.className = 'coll-body';
|
|
4616
|
+
wrap.appendChild(body);
|
|
4617
|
+
return { wrap, body };
|
|
4618
|
+
}
|
|
4619
|
+
|
|
4620
|
+
function renderRulesCollapsible(rules) {
|
|
4621
|
+
const { wrap, body } = buildCollapsible('rules', 'Named Rules', rules.length);
|
|
4622
|
+
for (const r of rules) {
|
|
4623
|
+
const card = document.createElement('div');
|
|
4624
|
+
card.className = 'rule-card';
|
|
4625
|
+
const name = document.createElement('div');
|
|
4626
|
+
name.className = 'name';
|
|
4627
|
+
name.innerHTML = `${escapeHtml(r.name)}${r.section ? `<span class="section">${escapeHtml(r.section)}</span>` : ''}`;
|
|
4628
|
+
card.appendChild(name);
|
|
4629
|
+
const b = document.createElement('div');
|
|
4630
|
+
b.className = 'body';
|
|
4631
|
+
b.textContent = r.body || '';
|
|
4632
|
+
card.appendChild(b);
|
|
4633
|
+
body.appendChild(card);
|
|
4634
|
+
}
|
|
4635
|
+
return wrap;
|
|
4636
|
+
}
|
|
4637
|
+
|
|
4638
|
+
function renderDosDontsCollapsible(n) {
|
|
4639
|
+
const total = (n.dos?.length || 0) + (n.donts?.length || 0);
|
|
4640
|
+
const { wrap, body } = buildCollapsible('dosdonts', "Do's and Don'ts", total);
|
|
4641
|
+
const grid = document.createElement('div');
|
|
4642
|
+
grid.className = 'dos';
|
|
4643
|
+
for (const d of n.dos || []) {
|
|
4644
|
+
const el = document.createElement('div');
|
|
4645
|
+
el.className = 'do';
|
|
4646
|
+
el.innerHTML = inlineMd(d);
|
|
4647
|
+
grid.appendChild(el);
|
|
4648
|
+
}
|
|
4649
|
+
for (const d of n.donts || []) {
|
|
4650
|
+
const el = document.createElement('div');
|
|
4651
|
+
el.className = 'dont';
|
|
4652
|
+
el.innerHTML = inlineMd(d);
|
|
4653
|
+
grid.appendChild(el);
|
|
4654
|
+
}
|
|
4655
|
+
body.appendChild(grid);
|
|
4656
|
+
return wrap;
|
|
4657
|
+
}
|
|
4658
|
+
|
|
4659
|
+
function renderOverviewCollapsible(n) {
|
|
4660
|
+
const { wrap, body } = buildCollapsible('overview', 'Overview', null);
|
|
4661
|
+
const ov = document.createElement('div');
|
|
4662
|
+
ov.className = 'overview-body';
|
|
4663
|
+
if (n.northStar) {
|
|
4664
|
+
const star = document.createElement('span');
|
|
4665
|
+
star.className = 'north-star';
|
|
4666
|
+
star.textContent = '“' + n.northStar + '”';
|
|
4667
|
+
ov.appendChild(star);
|
|
4668
|
+
}
|
|
4669
|
+
if (n.overview) {
|
|
4670
|
+
const p = document.createElement('p');
|
|
4671
|
+
p.innerHTML = inlineMd(n.overview);
|
|
4672
|
+
ov.appendChild(p);
|
|
4673
|
+
}
|
|
4674
|
+
if (n.keyCharacteristics?.length) {
|
|
4675
|
+
const ul = document.createElement('ul');
|
|
4676
|
+
ul.innerHTML = n.keyCharacteristics.map((k) => `<li>${inlineMd(k)}</li>`).join('');
|
|
4677
|
+
ov.appendChild(ul);
|
|
4678
|
+
}
|
|
4679
|
+
body.appendChild(ov);
|
|
4680
|
+
return wrap;
|
|
4681
|
+
}
|
|
4682
|
+
|
|
4683
|
+
function cssSafe(v) {
|
|
4684
|
+
// Strip anything outside valid CSS value chars to prevent injection via
|
|
4685
|
+
// .impeccable/design.json values rendered into inline style strings.
|
|
4686
|
+
return String(v).replace(/[<>"'`\n]/g, '');
|
|
4687
|
+
}
|
|
4688
|
+
|
|
4689
|
+
// --- Raw tab: minimal markdown renderer (subset) --------------------------
|
|
4690
|
+
|
|
4691
|
+
function renderRawTab(body, md) {
|
|
4692
|
+
const wrap = document.createElement('div');
|
|
4693
|
+
wrap.className = 'md';
|
|
4694
|
+
wrap.innerHTML = renderMarkdown(md);
|
|
4695
|
+
body.appendChild(wrap);
|
|
4696
|
+
}
|
|
4697
|
+
|
|
4698
|
+
function renderMarkdown(md) {
|
|
4699
|
+
const lines = md.split(/\r?\n/);
|
|
4700
|
+
const out = [];
|
|
4701
|
+
let i = 0;
|
|
4702
|
+
let inCode = false;
|
|
4703
|
+
let codeBuf = [];
|
|
4704
|
+
let paraBuf = [];
|
|
4705
|
+
let listBuf = []; // array of { indent, html }
|
|
4706
|
+
let listType = null; // 'ul' | 'ol'
|
|
4707
|
+
|
|
4708
|
+
const flushPara = () => {
|
|
4709
|
+
if (paraBuf.length) {
|
|
4710
|
+
out.push(`<p>${inlineMd(paraBuf.join(' '))}</p>`);
|
|
4711
|
+
paraBuf = [];
|
|
4712
|
+
}
|
|
4713
|
+
};
|
|
4714
|
+
const flushList = () => {
|
|
4715
|
+
if (listBuf.length) {
|
|
4716
|
+
out.push(buildListHtml(listBuf, listType));
|
|
4717
|
+
listBuf = [];
|
|
4718
|
+
listType = null;
|
|
4719
|
+
}
|
|
4720
|
+
};
|
|
4721
|
+
const flushAll = () => { flushPara(); flushList(); };
|
|
4722
|
+
|
|
4723
|
+
for (; i < lines.length; i++) {
|
|
4724
|
+
const line = lines[i];
|
|
4725
|
+
|
|
4726
|
+
// Code fence
|
|
4727
|
+
const fence = line.match(/^```(\w*)\s*$/);
|
|
4728
|
+
if (fence) {
|
|
4729
|
+
if (!inCode) { flushAll(); inCode = true; codeBuf = []; }
|
|
4730
|
+
else {
|
|
4731
|
+
out.push(`<pre><code>${escapeHtml(codeBuf.join('\n'))}</code></pre>`);
|
|
4732
|
+
inCode = false;
|
|
4733
|
+
}
|
|
4734
|
+
continue;
|
|
4735
|
+
}
|
|
4736
|
+
if (inCode) { codeBuf.push(line); continue; }
|
|
4737
|
+
|
|
4738
|
+
if (line.trim() === '') { flushAll(); continue; }
|
|
4739
|
+
|
|
4740
|
+
const hr = line.match(/^\s*(?:---+|\*\*\*+)\s*$/);
|
|
4741
|
+
if (hr) { flushAll(); out.push('<hr />'); continue; }
|
|
4742
|
+
|
|
4743
|
+
const heading = line.match(/^(#{1,4})\s+(.+)$/);
|
|
4744
|
+
if (heading) {
|
|
4745
|
+
flushAll();
|
|
4746
|
+
const lvl = heading[1].length;
|
|
4747
|
+
out.push(`<h${lvl}>${inlineMd(heading[2])}</h${lvl}>`);
|
|
4748
|
+
continue;
|
|
4749
|
+
}
|
|
4750
|
+
|
|
4751
|
+
const bullet = line.match(/^(\s*)([-*])\s+(.+)$/);
|
|
4752
|
+
const ordered = line.match(/^(\s*)(\d+)\.\s+(.+)$/);
|
|
4753
|
+
if (bullet || ordered) {
|
|
4754
|
+
flushPara();
|
|
4755
|
+
const m = bullet || ordered;
|
|
4756
|
+
const indent = Math.floor(m[1].length / 2);
|
|
4757
|
+
const t = bullet ? 'ul' : 'ol';
|
|
4758
|
+
if (listType && listType !== t) flushList();
|
|
4759
|
+
listType = t;
|
|
4760
|
+
listBuf.push({ indent, html: inlineMd(m[3]) });
|
|
4761
|
+
continue;
|
|
4762
|
+
}
|
|
4763
|
+
|
|
4764
|
+
paraBuf.push(line);
|
|
4765
|
+
}
|
|
4766
|
+
flushAll();
|
|
4767
|
+
if (inCode && codeBuf.length) {
|
|
4768
|
+
out.push(`<pre><code>${escapeHtml(codeBuf.join('\n'))}</code></pre>`);
|
|
4769
|
+
}
|
|
4770
|
+
return out.join('\n');
|
|
4771
|
+
}
|
|
4772
|
+
|
|
4773
|
+
function buildListHtml(items, type) {
|
|
4774
|
+
// Nest by indent (one level deep is plenty for DESIGN.md).
|
|
4775
|
+
let html = `<${type}>`;
|
|
4776
|
+
let lastIndent = 0;
|
|
4777
|
+
for (const it of items) {
|
|
4778
|
+
if (it.indent > lastIndent) html += `<${type}>`;
|
|
4779
|
+
else if (it.indent < lastIndent) html += `</${type}>`.repeat(lastIndent - it.indent);
|
|
4780
|
+
html += `<li>${it.html}</li>`;
|
|
4781
|
+
lastIndent = it.indent;
|
|
4782
|
+
}
|
|
4783
|
+
html += `</${type}>`.repeat(lastIndent + 1);
|
|
4784
|
+
return html;
|
|
4785
|
+
}
|
|
4786
|
+
|
|
4787
|
+
function inlineMd(text) {
|
|
4788
|
+
// Order matters: escape first, then re-inject tags.
|
|
4789
|
+
let s = escapeHtml(text);
|
|
4790
|
+
// Code spans
|
|
4791
|
+
s = s.replace(/`([^`]+)`/g, (_, code) => `<code>${code}</code>`);
|
|
4792
|
+
// Links [text](url)
|
|
4793
|
+
s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, t, u) => `<a href="${u}" target="_blank" rel="noopener noreferrer">${t}</a>`);
|
|
4794
|
+
// Bold
|
|
4795
|
+
s = s.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
4796
|
+
// Italic (only single *…*, skip if inside bold already handled)
|
|
4797
|
+
s = s.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, '$1<em>$2</em>');
|
|
4798
|
+
return s;
|
|
4799
|
+
}
|
|
4800
|
+
|
|
4801
|
+
function highlightBold(text) {
|
|
4802
|
+
return inlineMd(text);
|
|
4803
|
+
}
|
|
4804
|
+
|
|
4805
|
+
function escapeHtml(s) {
|
|
4806
|
+
return String(s)
|
|
4807
|
+
.replace(/&/g, '&')
|
|
4808
|
+
.replace(/</g, '<')
|
|
4809
|
+
.replace(/>/g, '>')
|
|
4810
|
+
.replace(/"/g, '"')
|
|
4811
|
+
.replace(/'/g, ''');
|
|
4812
|
+
}
|
|
4813
|
+
|
|
4814
|
+
function copyToClipboard(text) {
|
|
4815
|
+
if (!text) return;
|
|
4816
|
+
try {
|
|
4817
|
+
navigator.clipboard.writeText(text);
|
|
4818
|
+
showToast('Copied: ' + text);
|
|
4819
|
+
} catch { /* ignore */ }
|
|
4820
|
+
}
|
|
4821
|
+
|
|
4822
|
+
// ---------------------------------------------------------------------------
|
|
4823
|
+
// Init
|
|
4824
|
+
// ---------------------------------------------------------------------------
|
|
4825
|
+
|
|
4826
|
+
function init() {
|
|
4827
|
+
try { history.scrollRestoration = 'manual'; } catch {}
|
|
4828
|
+
initHighlight();
|
|
4829
|
+
initAnnotOverlay();
|
|
4830
|
+
initBar();
|
|
4831
|
+
initActionPicker();
|
|
4832
|
+
initParamsPanel();
|
|
4833
|
+
initGlobalBar();
|
|
4834
|
+
initDesignPanel();
|
|
4835
|
+
document.addEventListener('mousemove', handleMouseMove, true);
|
|
4836
|
+
document.addEventListener('click', handleClick, true);
|
|
4837
|
+
document.addEventListener('keydown', handleKeyDown, true);
|
|
4838
|
+
connectSSE();
|
|
4839
|
+
|
|
4840
|
+
// Check for an active session to resume (variant wrapper already in DOM after HMR)
|
|
4841
|
+
if (!resumeSession()) {
|
|
4842
|
+
console.log('[impeccable] Live variant mode ready. Hover over elements to pick one.');
|
|
4843
|
+
// SvelteKit (and any framework that hydrates after HTML parse) may add
|
|
4844
|
+
// the variant wrapper AFTER init runs. Watch for it and retry resume
|
|
4845
|
+
// once it appears. Disconnect on first hit.
|
|
4846
|
+
const scout = new MutationObserver(() => {
|
|
4847
|
+
const wrapper = document.querySelector('[data-impeccable-variants]');
|
|
4848
|
+
if (!wrapper) return;
|
|
4849
|
+
scout.disconnect();
|
|
4850
|
+
if (resumeSession()) {
|
|
4851
|
+
console.log('[impeccable] Resumed deferred session ' + currentSessionId + ' (post-hydration).');
|
|
4852
|
+
}
|
|
4853
|
+
});
|
|
4854
|
+
scout.observe(document.body, { childList: true, subtree: true });
|
|
4855
|
+
} else {
|
|
4856
|
+
console.log('[impeccable] Resumed active variant session ' + currentSessionId + ' (' + arrivedVariants + '/' + expectedVariants + ' variants).');
|
|
4857
|
+
}
|
|
4858
|
+
}
|
|
4859
|
+
|
|
4860
|
+
if (document.readyState === 'loading') {
|
|
4861
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
4862
|
+
} else {
|
|
4863
|
+
init();
|
|
4864
|
+
}
|
|
4865
|
+
})();
|