@bastani/atomic 0.9.13 → 0.9.14-alpha.2
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 +58 -0
- package/README.md +3 -3
- package/dist/builtin/intercom/package.json +2 -2
- package/dist/builtin/mcp/package.json +3 -3
- package/dist/builtin/subagents/CHANGELOG.md +14 -0
- package/dist/builtin/subagents/README.md +6 -4
- package/dist/builtin/subagents/agents/code-simplifier.md +1 -1
- package/dist/builtin/subagents/agents/codebase-analyzer.md +1 -1
- package/dist/builtin/subagents/agents/codebase-locator.md +1 -1
- package/dist/builtin/subagents/agents/codebase-online-researcher.md +1 -1
- package/dist/builtin/subagents/agents/codebase-pattern-finder.md +1 -1
- package/dist/builtin/subagents/agents/codebase-research-analyzer.md +1 -1
- package/dist/builtin/subagents/agents/codebase-research-locator.md +1 -1
- package/dist/builtin/subagents/agents/debugger.md +1 -1
- package/dist/builtin/subagents/agents/worker.md +1 -1
- package/dist/builtin/subagents/package.json +6 -5
- package/dist/builtin/subagents/src/agents/agent-discovery.ts +17 -11
- package/dist/builtin/subagents/src/agents/agent-loaders.ts +115 -43
- package/dist/builtin/subagents/src/agents/agent-management-helpers.ts +10 -8
- package/dist/builtin/subagents/src/agents/agent-management.ts +8 -7
- package/dist/builtin/subagents/src/agents/agent-overrides.ts +2 -2
- package/dist/builtin/subagents/src/agents/agent-scope.ts +1 -1
- package/dist/builtin/subagents/src/agents/agent-selection.ts +1 -1
- package/dist/builtin/subagents/src/agents/agent-serializer.ts +24 -3
- package/dist/builtin/subagents/src/agents/agent-types.ts +3 -1
- package/dist/builtin/subagents/src/agents/agents.ts +7 -5
- package/dist/builtin/subagents/src/agents/frontmatter.ts +67 -22
- package/dist/builtin/subagents/src/agents/identity.ts +1 -1
- package/dist/builtin/subagents/src/agents/skills-paths.ts +1 -1
- package/dist/builtin/subagents/src/agents/skills.ts +64 -6
- package/dist/builtin/subagents/src/extension/config.ts +1 -1
- package/dist/builtin/subagents/src/extension/control-notices.ts +2 -2
- package/dist/builtin/subagents/src/extension/doctor.ts +15 -5
- package/dist/builtin/subagents/src/extension/fanout-child.ts +12 -12
- package/dist/builtin/subagents/src/extension/index.ts +23 -23
- package/dist/builtin/subagents/src/extension/notification-content.ts +1 -1
- package/dist/builtin/subagents/src/extension/schemas.ts +1 -1
- package/dist/builtin/subagents/src/extension/startup-maintenance.ts +3 -3
- package/dist/builtin/subagents/src/intercom/intercom-bridge.ts +3 -3
- package/dist/builtin/subagents/src/intercom/result-intercom.ts +1 -1
- package/dist/builtin/subagents/src/intercom/supervisor-authorization.ts +1 -1
- package/dist/builtin/subagents/src/runs/foreground/completion-notification.ts +2 -2
- package/dist/builtin/subagents/src/runs/foreground/execution-detach-reservations.ts +1 -1
- package/dist/builtin/subagents/src/runs/foreground/execution-intercom-detach.ts +3 -3
- package/dist/builtin/subagents/src/runs/foreground/execution-run-sync.ts +1 -1
- package/dist/builtin/subagents/src/runs/foreground/execution-updates.ts +2 -2
- package/dist/builtin/subagents/src/runs/foreground/execution-utils.ts +1 -1
- package/dist/builtin/subagents/src/runs/foreground/execution.ts +2 -2
- package/dist/builtin/subagents/src/runs/foreground/inprocess-run-sync.ts +27 -11
- package/dist/builtin/subagents/src/runs/foreground/notify.ts +3 -3
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-context.ts +11 -11
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-input.ts +3 -3
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel-task.ts +15 -14
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel.ts +19 -19
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-runtime.ts +2 -2
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-single.ts +14 -13
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-status.ts +9 -9
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-types.ts +7 -7
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-worktree.ts +6 -6
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +24 -23
- package/dist/builtin/subagents/src/runs/inprocess/control-registry.ts +1 -1
- package/dist/builtin/subagents/src/runs/inprocess/control-status.ts +3 -3
- package/dist/builtin/subagents/src/runs/inprocess/index.ts +3 -3
- package/dist/builtin/subagents/src/runs/inprocess/prompt-behavior.ts +17 -8
- package/dist/builtin/subagents/src/runs/inprocess/runner.ts +60 -12
- package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-api.ts +8 -8
- package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-control.ts +3 -3
- package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-core.ts +4 -4
- package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-projection.ts +4 -4
- package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-registry.ts +5 -5
- package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-rendering.ts +3 -3
- package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-sanitize.ts +3 -3
- package/dist/builtin/subagents/src/runs/inprocess.ts +1 -1
- package/dist/builtin/subagents/src/runs/shared/long-running-guard.ts +1 -1
- package/dist/builtin/subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +1 -1
- package/dist/builtin/subagents/src/runs/shared/model-candidate-filter.ts +3 -3
- package/dist/builtin/subagents/src/runs/shared/model-fallback.ts +2 -2
- package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +1 -1
- package/dist/builtin/subagents/src/runs/shared/single-output.ts +1 -1
- package/dist/builtin/subagents/src/runs/shared/subagent-control.ts +1 -1
- package/dist/builtin/subagents/src/shared/artifacts.ts +2 -2
- package/dist/builtin/subagents/src/shared/event-jsonl-writer.ts +1 -1
- package/dist/builtin/subagents/src/shared/fast-mode.ts +1 -1
- package/dist/builtin/subagents/src/shared/formatters.ts +2 -2
- package/dist/builtin/subagents/src/shared/jsonl-writer.ts +1 -1
- package/dist/builtin/subagents/src/shared/model-resolution.ts +1 -1
- package/dist/builtin/subagents/src/shared/session-tokens.ts +1 -1
- package/dist/builtin/subagents/src/shared/settings.ts +4 -4
- package/dist/builtin/subagents/src/shared/status-format.ts +1 -1
- package/dist/builtin/subagents/src/shared/types-config.ts +16 -4
- package/dist/builtin/subagents/src/shared/types-depth.ts +1 -1
- package/dist/builtin/subagents/src/shared/types-nested.ts +1 -1
- package/dist/builtin/subagents/src/shared/types-output.ts +1 -1
- package/dist/builtin/subagents/src/shared/types-results.ts +1 -1
- package/dist/builtin/subagents/src/shared/types-runtime.ts +1 -1
- package/dist/builtin/subagents/src/shared/types.ts +6 -6
- package/dist/builtin/subagents/src/shared/utils.ts +3 -3
- package/dist/builtin/subagents/src/slash/prompt-template-bridge.ts +1 -1
- package/dist/builtin/subagents/src/slash/slash-bridge.ts +3 -3
- package/dist/builtin/subagents/src/slash/slash-commands.ts +6 -6
- package/dist/builtin/subagents/src/slash/slash-live-state.ts +3 -3
- package/dist/builtin/subagents/src/tui/render-event-formatting.ts +2 -2
- package/dist/builtin/subagents/src/tui/render-layout.ts +1 -1
- package/dist/builtin/subagents/src/tui/render-progress.ts +2 -2
- package/dist/builtin/subagents/src/tui/render-result-compact.ts +7 -7
- package/dist/builtin/subagents/src/tui/render-result.ts +10 -10
- package/dist/builtin/subagents/src/tui/render-stable-output.ts +1 -1
- package/dist/builtin/subagents/src/tui/render-status-progress.ts +5 -5
- package/dist/builtin/subagents/src/tui/render.ts +4 -4
- package/dist/builtin/web-access/package.json +2 -2
- package/dist/builtin/workflows/CHANGELOG.md +57 -0
- package/dist/builtin/workflows/README.md +39 -8
- package/dist/builtin/workflows/builtin/adversarial-verification-runner.ts +16 -5
- package/dist/builtin/workflows/builtin/adversarial-verification.ts +4 -0
- package/dist/builtin/workflows/builtin/classify-and-act.ts +4 -0
- package/dist/builtin/workflows/builtin/fan-out-and-synthesize.ts +4 -0
- package/dist/builtin/workflows/builtin/generate-and-filter-runner.ts +12 -4
- package/dist/builtin/workflows/builtin/generate-and-filter.ts +4 -0
- package/dist/builtin/workflows/builtin/goal-models.ts +8 -10
- package/dist/builtin/workflows/builtin/goal.ts +4 -0
- package/dist/builtin/workflows/builtin/index.d.ts +0 -4
- package/dist/builtin/workflows/builtin/loop-until-done-runner.ts +4 -2
- package/dist/builtin/workflows/builtin/loop-until-done.ts +4 -0
- package/dist/builtin/workflows/builtin/open-claude-design-live-protocol.ts +305 -0
- package/dist/builtin/workflows/builtin/open-claude-design-phases.ts +115 -171
- package/dist/builtin/workflows/builtin/open-claude-design-runner.ts +9 -25
- package/dist/builtin/workflows/builtin/open-claude-design-setup.ts +19 -86
- package/dist/builtin/workflows/builtin/open-claude-design-utils.ts +0 -6
- package/dist/builtin/workflows/builtin/open-claude-design.d.ts +0 -5
- package/dist/builtin/workflows/builtin/open-claude-design.ts +11 -15
- package/dist/builtin/workflows/builtin/ralph-models.ts +24 -29
- package/dist/builtin/workflows/builtin/ralph.ts +4 -0
- package/dist/builtin/workflows/builtin/tournament-runner.ts +6 -3
- package/dist/builtin/workflows/builtin/tournament.ts +4 -0
- package/dist/builtin/workflows/package.json +2 -2
- package/dist/builtin/workflows/skills/impeccable/SKILL.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_asset_producer.toml +25 -31
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_documenter.toml +1 -0
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_finish_reviewer.toml +16 -10
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_manual_edit_applier.toml +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/android.md +6 -0
- package/dist/builtin/workflows/skills/impeccable/reference/animate.md +3 -0
- package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +3 -1
- package/dist/builtin/workflows/skills/impeccable/reference/craft-floor.md +6 -1
- package/dist/builtin/workflows/skills/impeccable/reference/critique.md +18 -2
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/asset-producer.md +24 -30
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/documenter.md +1 -0
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/finish-reviewer.md +16 -10
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/manual-edit-applier.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/distill.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/doctor.md +1 -0
- package/dist/builtin/workflows/skills/impeccable/reference/document.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/extract.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/hooks.md +12 -6
- package/dist/builtin/workflows/skills/impeccable/reference/init.md +12 -3
- package/dist/builtin/workflows/skills/impeccable/reference/ios.md +6 -0
- package/dist/builtin/workflows/skills/impeccable/reference/live-setup.md +102 -0
- package/dist/builtin/workflows/skills/impeccable/reference/live.md +113 -521
- package/dist/builtin/workflows/skills/impeccable/reference/new-work.md +39 -26
- package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/polish.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/visualize.md +25 -11
- package/dist/builtin/workflows/skills/impeccable/scripts/concept-seed.mjs +332 -221
- package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +10 -19
- package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +91 -38
- package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +19 -10
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +56 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +10 -16
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/design-system.mjs +140 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +1174 -694
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns.mjs +1 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +5 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +431 -45
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +10 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +47 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +23 -22
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +0 -10
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +384 -384
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/color.mjs +466 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/doctor.mjs +2 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/embed-prompt.mjs +270 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/generate-image.mjs +51 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/hook-admin.mjs +70 -10
- package/dist/builtin/workflows/skills/impeccable/scripts/hook-lib.mjs +336 -90
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/composition-catalog.mjs +38 -3
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/concept-catalog.mjs +77 -11
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/design-parser.mjs +102 -19
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-config.mjs +44 -59
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/is-generated.mjs +3 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/open-system-browser.mjs +57 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/roll-selection.mjs +369 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-deep.mjs +31 -3
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness.mjs +73 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/live/accept-css.mjs +617 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/accept-verify.mjs +71 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/browser-script-parts.mjs +29 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/live/event-validation.mjs +54 -7
- package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/astro.mjs +47 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/detect-utils.mjs +157 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/index.mjs +143 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/journal.mjs +205 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/nextjs.mjs +49 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/nuxt.mjs +170 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/script-src.mjs +26 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/static-html.mjs +26 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/sveltekit.mjs +71 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/tag-strategy.mjs +247 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/tanstack-start.mjs +70 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/vite-generic.mjs +42 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/instructions.mjs +169 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/roots.mjs +670 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/session-store.mjs +232 -35
- package/dist/builtin/workflows/skills/impeccable/scripts/live/svelte-ast.mjs +969 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/svelte-component.mjs +621 -74
- package/dist/builtin/workflows/skills/impeccable/scripts/live/sveltekit-adapter.mjs +66 -28
- package/dist/builtin/workflows/skills/impeccable/scripts/live/tanstack-adapter.mjs +12 -12
- package/dist/builtin/workflows/skills/impeccable/scripts/live/ui-surfaces.mjs +75 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/vocabulary.mjs +135 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +23 -37
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +945 -115
- package/dist/builtin/workflows/skills/impeccable/scripts/live-complete.mjs +33 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live-copy-edit-agent.mjs +132 -15
- package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +173 -425
- package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +2 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +16 -3
- package/dist/builtin/workflows/skills/impeccable/scripts/live-resume.mjs +39 -10
- package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +194 -27
- package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +10 -4
- package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +50 -31
- package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +91 -28
- package/dist/builtin/workflows/skills/impeccable/scripts/pin.mjs +8 -5
- package/dist/builtin/workflows/skills/impeccable/scripts/serve-question.mjs +759 -109
- package/dist/builtin/workflows/src/authoring/workflow.ts +9 -0
- package/dist/builtin/workflows/src/authoring.d.ts +3 -0
- package/dist/builtin/workflows/src/durable/completed-catalog.ts +23 -2
- package/dist/builtin/workflows/src/durable/dbos-backend.ts +30 -17
- package/dist/builtin/workflows/src/durable/dbos-envelope.ts +14 -0
- package/dist/builtin/workflows/src/durable/dbos-lifecycle.ts +60 -57
- package/dist/builtin/workflows/src/durable/dbos-process-owner.ts +87 -0
- package/dist/builtin/workflows/src/durable/dbos-registration-diagnostics.ts +113 -0
- package/dist/builtin/workflows/src/durable/factory.ts +11 -8
- package/dist/builtin/workflows/src/durable/tool-failure-checkpoint.ts +5 -0
- package/dist/builtin/workflows/src/durable/tool-primitive.ts +186 -20
- package/dist/builtin/workflows/src/durable/types.ts +4 -0
- package/dist/builtin/workflows/src/durable/workflow-heartbeat-anchor.ts +153 -0
- package/dist/builtin/workflows/src/engine/run-durable-stage-session.ts +28 -0
- package/dist/builtin/workflows/src/engine/run-tool-control-registry.ts +9 -1
- package/dist/builtin/workflows/src/engine/run.ts +1 -0
- package/dist/builtin/workflows/src/extension/adopt-session-run-state.ts +20 -0
- package/dist/builtin/workflows/src/extension/background-ui-adapter.ts +9 -30
- package/dist/builtin/workflows/src/extension/extension-factory.ts +3 -0
- package/dist/builtin/workflows/src/extension/extension-lifecycle.ts +28 -10
- package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +138 -0
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +3365 -1341
- package/dist/builtin/workflows/src/extension/workflow-heartbeat-delivery.ts +197 -0
- package/dist/builtin/workflows/src/extension/workflow-heartbeat-notice.ts +115 -0
- package/dist/builtin/workflows/src/extension/workflow-heartbeat-scheduler.ts +962 -0
- package/dist/builtin/workflows/src/extension/workflow-prompts.ts +4 -1
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +29 -5
- package/dist/builtin/workflows/src/extension/workflow-tool-send.ts +32 -1
- package/dist/builtin/workflows/src/runs/background/cancellation-registry.ts +10 -1
- package/dist/builtin/workflows/src/runs/background/job-tracker.ts +10 -1
- package/dist/builtin/workflows/src/runs/foreground/executor-prompt-nodes.ts +16 -11
- package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +31 -1
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +41 -10
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-messages.ts +48 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-output.ts +4 -1
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-structured-output.ts +45 -12
- package/dist/builtin/workflows/src/sdk-surface.ts +9 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +10 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +10 -0
- package/dist/builtin/workflows/src/shared/graph-store-snapshot.ts +14 -2
- package/dist/builtin/workflows/src/shared/persistence-restore.ts +12 -0
- package/dist/builtin/workflows/src/shared/prompt-answer.ts +130 -0
- package/dist/builtin/workflows/src/shared/session-scoped-singleton.ts +98 -0
- package/dist/builtin/workflows/src/shared/stage-ui-broker.ts +9 -1
- package/dist/builtin/workflows/src/shared/store-factory.ts +9 -1
- package/dist/builtin/workflows/src/shared/store-public-types.ts +2 -1
- package/dist/builtin/workflows/src/shared/store-tool-node-methods.ts +7 -0
- package/dist/builtin/workflows/src/shared/store-types.ts +18 -1
- package/dist/builtin/workflows/src/shared/tool-payload-bounds.ts +576 -0
- package/dist/builtin/workflows/src/shared/workflow-authoring-types.d.ts +1 -0
- package/dist/builtin/workflows/src/shared/workflow-authoring-types.ts +1 -0
- package/dist/builtin/workflows/src/shared/workflow-heartbeat-contract.d.ts +21 -0
- package/dist/builtin/workflows/src/shared/workflow-heartbeat-contract.ts +25 -0
- package/dist/builtin/workflows/src/tui/graph-theme.ts +8 -1
- package/dist/builtin/workflows/src/tui/graph-view-constants.ts +11 -0
- package/dist/builtin/workflows/src/tui/graph-view-input.ts +100 -5
- package/dist/builtin/workflows/src/tui/graph-view-layout.ts +42 -11
- package/dist/builtin/workflows/src/tui/graph-view-render-helpers.ts +55 -6
- package/dist/builtin/workflows/src/tui/graph-view-render.ts +71 -6
- package/dist/builtin/workflows/src/tui/graph-view-state.ts +43 -0
- package/dist/builtin/workflows/src/tui/keybindings-adapter.ts +2 -0
- package/dist/builtin/workflows/src/tui/node-card.ts +5 -1
- package/dist/builtin/workflows/src/tui/stage-chat-view-archive-history.ts +11 -2
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +36 -10
- package/dist/builtin/workflows/src/tui/switcher.ts +11 -4
- package/dist/builtin/workflows/src/tui/tool-detail.ts +348 -0
- package/dist/builtin/workflows/src/tui/workflow-status.ts +5 -2
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +11 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/auth-check.d.ts +1 -1
- package/dist/cli/auth-check.js.map +1 -1
- package/dist/cli/credential-print.d.ts +1 -1
- package/dist/cli/credential-print.js.map +1 -1
- package/dist/cli/list-models.d.ts +1 -1
- package/dist/cli/list-models.js.map +1 -1
- package/dist/cli/startup-ui.d.ts +9 -0
- package/dist/cli/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +7 -1
- package/dist/cli/startup-ui.js.map +1 -1
- package/dist/core/agent-session-custom-message-commit.js +2 -2
- package/dist/core/agent-session-custom-message-commit.js.map +1 -1
- package/dist/core/agent-session-export.d.ts +6 -1
- package/dist/core/agent-session-export.d.ts.map +1 -1
- package/dist/core/agent-session-export.js +6 -3
- package/dist/core/agent-session-export.js.map +1 -1
- package/dist/core/agent-session-extension-bindings.d.ts.map +1 -1
- package/dist/core/agent-session-extension-bindings.js +6 -4
- package/dist/core/agent-session-extension-bindings.js.map +1 -1
- package/dist/core/agent-session-methods.d.ts +5 -2
- package/dist/core/agent-session-methods.d.ts.map +1 -1
- package/dist/core/agent-session-methods.js.map +1 -1
- package/dist/core/agent-session-prompt.d.ts +12 -1
- package/dist/core/agent-session-prompt.d.ts.map +1 -1
- package/dist/core/agent-session-prompt.js +31 -10
- package/dist/core/agent-session-prompt.js.map +1 -1
- package/dist/core/agent-session-runtime-auth.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +1 -1
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session-skill-block.d.ts +1 -0
- package/dist/core/agent-session-skill-block.d.ts.map +1 -1
- package/dist/core/agent-session-skill-block.js +18 -2
- package/dist/core/agent-session-skill-block.js.map +1 -1
- package/dist/core/agent-session-state.d.ts.map +1 -1
- package/dist/core/agent-session-state.js +2 -1
- package/dist/core/agent-session-state.js.map +1 -1
- package/dist/core/agent-session-types.d.ts +2 -1
- package/dist/core/agent-session-types.d.ts.map +1 -1
- package/dist/core/agent-session-types.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/atomic-guide-command.js +2 -2
- package/dist/core/atomic-guide-command.js.map +1 -1
- package/dist/core/builtin-packages.d.ts.map +1 -1
- package/dist/core/builtin-packages.js +0 -6
- package/dist/core/builtin-packages.js.map +1 -1
- package/dist/core/cloudflare-gateway-binding.d.ts +11 -0
- package/dist/core/cloudflare-gateway-binding.d.ts.map +1 -0
- package/dist/core/cloudflare-gateway-binding.js +11 -0
- package/dist/core/cloudflare-gateway-binding.js.map +1 -0
- package/dist/core/codex-fast-mode-transport.d.ts +12 -0
- package/dist/core/codex-fast-mode-transport.d.ts.map +1 -0
- package/dist/core/codex-fast-mode-transport.js +130 -0
- package/dist/core/codex-fast-mode-transport.js.map +1 -0
- package/dist/core/codex-fast-mode.d.ts +20 -6
- package/dist/core/codex-fast-mode.d.ts.map +1 -1
- package/dist/core/codex-fast-mode.js +111 -3
- package/dist/core/codex-fast-mode.js.map +1 -1
- package/dist/core/diagnostics.d.ts +4 -0
- package/dist/core/diagnostics.d.ts.map +1 -1
- package/dist/core/diagnostics.js.map +1 -1
- package/dist/core/event-bus.d.ts +12 -0
- package/dist/core/event-bus.d.ts.map +1 -1
- package/dist/core/event-bus.js +22 -0
- package/dist/core/event-bus.js.map +1 -1
- package/dist/core/experimental.d.ts +25 -0
- package/dist/core/experimental.d.ts.map +1 -1
- package/dist/core/experimental.js +24 -0
- package/dist/core/experimental.js.map +1 -1
- package/dist/core/export-html/template-js/tree-filter-render.js +4 -3
- package/dist/core/export-html/template.js +4 -3
- package/dist/core/extension-session-state.d.ts +28 -0
- package/dist/core/extension-session-state.d.ts.map +1 -0
- package/dist/core/extension-session-state.js +55 -0
- package/dist/core/extension-session-state.js.map +1 -0
- package/dist/core/extensions/api-types.d.ts +6 -0
- package/dist/core/extensions/api-types.d.ts.map +1 -1
- package/dist/core/extensions/api-types.js.map +1 -1
- package/dist/core/extensions/context-types.d.ts +4 -0
- package/dist/core/extensions/context-types.d.ts.map +1 -1
- package/dist/core/extensions/context-types.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -0
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js +1 -0
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader-api.d.ts +1 -1
- package/dist/core/extensions/loader-api.d.ts.map +1 -1
- package/dist/core/extensions/loader-api.js +16 -10
- package/dist/core/extensions/loader-api.js.map +1 -1
- package/dist/core/extensions/loader-virtual-modules.d.ts.map +1 -1
- package/dist/core/extensions/loader-virtual-modules.js +12 -1
- package/dist/core/extensions/loader-virtual-modules.js.map +1 -1
- package/dist/core/extensions/runner-context.d.ts +2 -0
- package/dist/core/extensions/runner-context.d.ts.map +1 -1
- package/dist/core/extensions/runner-context.js +8 -0
- package/dist/core/extensions/runner-context.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +1 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +3 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/runtime-types.d.ts +3 -0
- package/dist/core/extensions/runtime-types.d.ts.map +1 -1
- package/dist/core/extensions/runtime-types.js.map +1 -1
- package/dist/core/extensions/ui-types.d.ts +26 -0
- package/dist/core/extensions/ui-types.d.ts.map +1 -1
- package/dist/core/extensions/ui-types.js +17 -1
- package/dist/core/extensions/ui-types.js.map +1 -1
- package/dist/core/http-dispatcher.d.ts.map +1 -1
- package/dist/core/http-dispatcher.js +5 -0
- package/dist/core/http-dispatcher.js.map +1 -1
- package/dist/core/keybindings.d.ts +24 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +6 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/model-config.d.ts +5 -0
- package/dist/core/model-config.d.ts.map +1 -1
- package/dist/core/model-config.js +1 -0
- package/dist/core/model-config.js.map +1 -1
- package/dist/core/model-registry.d.ts +1 -1
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver-cli.d.ts +1 -1
- package/dist/core/model-resolver-cli.js.map +1 -1
- package/dist/core/model-resolver-defaults.js +3 -3
- package/dist/core/model-resolver-defaults.js.map +1 -1
- package/dist/core/model-resolver-initial.d.ts +1 -1
- package/dist/core/model-resolver-initial.js.map +1 -1
- package/dist/core/model-resolver-scope.d.ts +1 -1
- package/dist/core/model-resolver-scope.js.map +1 -1
- package/dist/core/model-runtime.d.ts +54 -0
- package/dist/core/model-runtime.d.ts.map +1 -1
- package/dist/core/model-runtime.js +96 -13
- package/dist/core/model-runtime.js.map +1 -1
- package/dist/core/oauth-login.js.map +1 -1
- package/dist/core/project-trust.d.ts +10 -0
- package/dist/core/project-trust.d.ts.map +1 -1
- package/dist/core/project-trust.js +12 -0
- package/dist/core/project-trust.js.map +1 -1
- package/dist/core/resource-loader-assets.d.ts.map +1 -1
- package/dist/core/resource-loader-assets.js +14 -5
- package/dist/core/resource-loader-assets.js.map +1 -1
- package/dist/core/resource-loader-core.d.ts +3 -0
- package/dist/core/resource-loader-core.d.ts.map +1 -1
- package/dist/core/resource-loader-core.js +5 -0
- package/dist/core/resource-loader-core.js.map +1 -1
- package/dist/core/resource-loader-internals.d.ts +2 -0
- package/dist/core/resource-loader-internals.d.ts.map +1 -1
- package/dist/core/resource-loader-internals.js.map +1 -1
- package/dist/core/resource-loader-reload.d.ts.map +1 -1
- package/dist/core/resource-loader-reload.js +2 -0
- package/dist/core/resource-loader-reload.js.map +1 -1
- package/dist/core/resource-loader-types.d.ts +2 -0
- package/dist/core/resource-loader-types.d.ts.map +1 -1
- package/dist/core/resource-loader-types.js.map +1 -1
- package/dist/core/resource-loader.d.ts +2 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +1 -0
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk-types.d.ts +7 -6
- package/dist/core/sdk-types.d.ts.map +1 -1
- package/dist/core/sdk-types.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +17 -6
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager-core.d.ts +7 -1
- package/dist/core/session-manager-core.d.ts.map +1 -1
- package/dist/core/session-manager-core.js +11 -2
- package/dist/core/session-manager-core.js.map +1 -1
- package/dist/core/session-manager-entries.d.ts +2 -2
- package/dist/core/session-manager-entries.d.ts.map +1 -1
- package/dist/core/session-manager-entries.js +6 -4
- package/dist/core/session-manager-entries.js.map +1 -1
- package/dist/core/session-manager-list.d.ts.map +1 -1
- package/dist/core/session-manager-list.js +5 -1
- package/dist/core/session-manager-list.js.map +1 -1
- package/dist/core/session-manager-types.d.ts +25 -2
- package/dist/core/session-manager-types.d.ts.map +1 -1
- package/dist/core/session-manager-types.js.map +1 -1
- package/dist/core/session-manager.d.ts +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager-basic-accessors.d.ts +1 -0
- package/dist/core/settings-manager-basic-accessors.d.ts.map +1 -1
- package/dist/core/settings-manager-basic-accessors.js +8 -0
- package/dist/core/settings-manager-basic-accessors.js.map +1 -1
- package/dist/core/settings-manager-ui-accessors.d.ts +3 -1
- package/dist/core/settings-manager-ui-accessors.d.ts.map +1 -1
- package/dist/core/settings-manager-ui-accessors.js +9 -0
- package/dist/core/settings-manager-ui-accessors.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/settings-types.d.ts +4 -0
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/skill-catalog.d.ts +35 -0
- package/dist/core/skill-catalog.d.ts.map +1 -0
- package/dist/core/skill-catalog.js +264 -0
- package/dist/core/skill-catalog.js.map +1 -0
- package/dist/core/skills-async.d.ts.map +1 -1
- package/dist/core/skills-async.js +12 -6
- package/dist/core/skills-async.js.map +1 -1
- package/dist/core/skills.d.ts +2 -0
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +8 -5
- package/dist/core/skills.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -5
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.js +2 -2
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/ask-user-question/ask-user-question.d.ts +25 -2
- package/dist/core/tools/ask-user-question/ask-user-question.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/ask-user-question.js +47 -18
- package/dist/core/tools/ask-user-question/ask-user-question.js.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/multi-select-view.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/multi-select-view.js +5 -2
- package/dist/core/tools/ask-user-question/view/components/multi-select-view.js.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/submit-picker.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/submit-picker.js +3 -1
- package/dist/core/tools/ask-user-question/view/components/submit-picker.js.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/wrapping-select.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/wrapping-select.js +8 -1
- package/dist/core/tools/ask-user-question/view/components/wrapping-select.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +2 -0
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +2 -0
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +3 -1
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.js +1 -1
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/hashline-engine/format.d.ts +2 -0
- package/dist/core/tools/hashline-engine/format.d.ts.map +1 -1
- package/dist/core/tools/hashline-engine/format.js +4 -2
- package/dist/core/tools/hashline-engine/format.js.map +1 -1
- package/dist/core/tools/hashline-engine/messages.d.ts +2 -0
- package/dist/core/tools/hashline-engine/messages.d.ts.map +1 -1
- package/dist/core/tools/hashline-engine/messages.js +4 -2
- package/dist/core/tools/hashline-engine/messages.js.map +1 -1
- package/dist/core/tools/hashline-engine/parser.d.ts.map +1 -1
- package/dist/core/tools/hashline-engine/parser.js +16 -4
- package/dist/core/tools/hashline-engine/parser.js.map +1 -1
- package/dist/core/tools/hashline-engine/tokenizer.d.ts.map +1 -1
- package/dist/core/tools/hashline-engine/tokenizer.js +37 -27
- package/dist/core/tools/hashline-engine/tokenizer.js.map +1 -1
- package/dist/core/tools/hashline.d.ts.map +1 -1
- package/dist/core/tools/hashline.js +8 -2
- package/dist/core/tools/hashline.js.map +1 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +2 -0
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +8 -1
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/search.d.ts.map +1 -1
- package/dist/core/tools/search.js +2 -0
- package/dist/core/tools/search.js.map +1 -1
- package/dist/core/tools/todos.d.ts +3 -12
- package/dist/core/tools/todos.d.ts.map +1 -1
- package/dist/core/tools/todos.js +19 -4
- package/dist/core/tools/todos.js.map +1 -1
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +2 -0
- package/dist/core/tools/write.js.map +1 -1
- package/dist/index-extensions.d.ts +1 -1
- package/dist/index-extensions.d.ts.map +1 -1
- package/dist/index-extensions.js +1 -1
- package/dist/index-extensions.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/main-runtime-api-key.d.ts +1 -1
- package/dist/main-runtime-api-key.js.map +1 -1
- package/dist/main-session-options.d.ts +1 -1
- package/dist/main-session-options.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +10 -2
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-rendering.js +1 -1
- package/dist/modes/interactive/components/chat-session-host-rendering.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-state.d.ts +8 -0
- package/dist/modes/interactive/components/chat-session-host-state.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-state.js +8 -0
- package/dist/modes/interactive/components/chat-session-host-state.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.d.ts +26 -0
- package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.js +38 -0
- package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
- package/dist/modes/interactive/components/chat-transcript.d.ts +37 -0
- package/dist/modes/interactive/components/chat-transcript.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-transcript.js +54 -1
- package/dist/modes/interactive/components/chat-transcript.js.map +1 -1
- package/dist/modes/interactive/components/fast-mode-selector.js +1 -1
- package/dist/modes/interactive/components/fast-mode-selector.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +2 -1
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts +1 -1
- package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
- package/dist/modes/interactive/components/reserved-bottom-overlay.d.ts +95 -0
- package/dist/modes/interactive/components/reserved-bottom-overlay.d.ts.map +1 -0
- package/dist/modes/interactive/components/reserved-bottom-overlay.js +234 -0
- package/dist/modes/interactive/components/reserved-bottom-overlay.js.map +1 -0
- package/dist/modes/interactive/components/session-selector-search.d.ts +4 -0
- package/dist/modes/interactive/components/session-selector-search.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector-search.js +4 -0
- package/dist/modes/interactive/components/session-selector-search.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector-handlers.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector-handlers.js +3 -0
- package/dist/modes/interactive/components/settings-selector-handlers.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector-items.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector-items.js +7 -0
- package/dist/modes/interactive/components/settings-selector-items.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector-types.d.ts +3 -1
- package/dist/modes/interactive/components/settings-selector-types.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector-types.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +12 -1
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-auth-login.js.map +1 -1
- package/dist/modes/interactive/interactive-auth-routing.js.map +1 -1
- package/dist/modes/interactive/interactive-autocomplete.js +5 -4
- package/dist/modes/interactive/interactive-autocomplete.js.map +1 -1
- package/dist/modes/interactive/interactive-bash-compact.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-bash-compact.js +2 -2
- package/dist/modes/interactive/interactive-bash-compact.js.map +1 -1
- package/dist/modes/interactive/interactive-extension-custom-ui.js +94 -6
- package/dist/modes/interactive/interactive-extension-custom-ui.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-base.d.ts +41 -3
- package/dist/modes/interactive/interactive-mode-base.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-base.js +80 -11
- package/dist/modes/interactive/interactive-mode-base.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-surface.d.ts +9 -1
- package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-types.d.ts +2 -0
- package/dist/modes/interactive/interactive-mode-types.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-types.js.map +1 -1
- package/dist/modes/interactive/interactive-model-catalog-startup.d.ts +8 -1
- package/dist/modes/interactive/interactive-model-catalog-startup.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-model-catalog-startup.js +13 -3
- package/dist/modes/interactive/interactive-model-catalog-startup.js.map +1 -1
- package/dist/modes/interactive/interactive-model-routing.js +3 -4
- package/dist/modes/interactive/interactive-model-routing.js.map +1 -1
- package/dist/modes/interactive/interactive-render-chat.js +6 -0
- package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
- package/dist/modes/interactive/interactive-resource-disclosure.js +2 -2
- package/dist/modes/interactive/interactive-resource-disclosure.js.map +1 -1
- package/dist/modes/interactive/interactive-selectors.js +7 -3
- package/dist/modes/interactive/interactive-selectors.js.map +1 -1
- package/dist/modes/interactive/interactive-session-runtime.js +7 -1
- package/dist/modes/interactive/interactive-session-runtime.js.map +1 -1
- package/dist/modes/interactive/interactive-slash-commands.js +2 -2
- package/dist/modes/interactive/interactive-slash-commands.js.map +1 -1
- package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-startup.js +34 -9
- package/dist/modes/interactive/interactive-startup.js.map +1 -1
- package/dist/modes/interactive/interactive-tui.d.ts +21 -1
- package/dist/modes/interactive/interactive-tui.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-tui.js +140 -38
- package/dist/modes/interactive/interactive-tui.js.map +1 -1
- package/dist/modes/interactive/model-catalog-refresh.d.ts +17 -0
- package/dist/modes/interactive/model-catalog-refresh.d.ts.map +1 -0
- package/dist/modes/interactive/model-catalog-refresh.js +84 -0
- package/dist/modes/interactive/model-catalog-refresh.js.map +1 -0
- package/dist/modes/interactive/theme/dark.json +2 -0
- package/dist/modes/interactive/theme/light.json +2 -0
- package/dist/modes/interactive/theme/theme-class.d.ts +12 -3
- package/dist/modes/interactive/theme/theme-class.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme-class.js +6 -1
- package/dist/modes/interactive/theme/theme-class.js.map +1 -1
- package/dist/modes/interactive/theme/theme-controller.d.ts +10 -2
- package/dist/modes/interactive/theme/theme-controller.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme-controller.js +23 -10
- package/dist/modes/interactive/theme/theme-controller.js.map +1 -1
- package/dist/modes/interactive/theme/theme-loading.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme-loading.js +1 -0
- package/dist/modes/interactive/theme/theme-loading.js.map +1 -1
- package/dist/modes/interactive/theme/theme-schema.d.ts +8 -0
- package/dist/modes/interactive/theme/theme-schema.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme-schema.js +3 -1
- package/dist/modes/interactive/theme/theme-schema.js.map +1 -1
- package/dist/modes/interactive/theme/theme-schema.json +9 -1
- package/dist/modes/interactive-engine/engine-custom-ui.d.ts +2 -0
- package/dist/modes/interactive-engine/engine-custom-ui.d.ts.map +1 -1
- package/dist/modes/interactive-engine/engine-custom-ui.js +1 -0
- package/dist/modes/interactive-engine/engine-custom-ui.js.map +1 -1
- package/dist/modes/interactive-engine/protocol.d.ts +1 -0
- package/dist/modes/interactive-engine/protocol.d.ts.map +1 -1
- package/dist/modes/interactive-engine/protocol.js +1 -0
- package/dist/modes/interactive-engine/protocol.js.map +1 -1
- package/dist/modes/interactive-engine/remote-component.d.ts +3 -1
- package/dist/modes/interactive-engine/remote-component.d.ts.map +1 -1
- package/dist/modes/interactive-engine/remote-component.js +3 -2
- package/dist/modes/interactive-engine/remote-component.js.map +1 -1
- package/dist/modes/json-event.d.ts +6 -1
- package/dist/modes/json-event.d.ts.map +1 -1
- package/dist/modes/json-event.js +17 -2
- package/dist/modes/json-event.js.map +1 -1
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-command-handler.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-command-handler.js +5 -4
- package/dist/modes/rpc/rpc-command-handler.js.map +1 -1
- package/dist/modes/rpc/rpc-provider-auth.js.map +1 -1
- package/dist/modes/rpc/rpc-responses.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/server/create-harness.d.ts.map +1 -1
- package/dist/server/create-harness.js +11 -0
- package/dist/server/create-harness.js.map +1 -1
- package/dist/utils/abort.d.ts +9 -0
- package/dist/utils/abort.d.ts.map +1 -0
- package/dist/utils/abort.js +19 -0
- package/dist/utils/abort.js.map +1 -0
- package/dist/utils/tools-manager.d.ts +16 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +19 -19
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/custom-provider.md +9 -8
- package/docs/development.md +0 -1
- package/docs/docs.json +0 -1
- package/docs/environment-variables.md +3 -0
- package/docs/extensions.md +100 -2
- package/docs/json.md +3 -1
- package/docs/keybindings.md +8 -3
- package/docs/models/artificial-analysis-index.md +2 -2
- package/docs/models/model-selection.md +32 -23
- package/docs/models/pareto-efficiency.md +25 -24
- package/docs/providers.md +95 -2
- package/docs/quickstart.md +4 -2
- package/docs/rpc.md +21 -7
- package/docs/sdk.md +26 -1
- package/docs/session-format.md +5 -2
- package/docs/settings.md +34 -3
- package/docs/skills.md +16 -1
- package/docs/terminal-setup.md +15 -2
- package/docs/themes.md +22 -4
- package/docs/tools.md +3 -1
- package/docs/tui.md +2 -0
- package/docs/usage.md +5 -4
- package/docs/workflows.md +96 -19
- package/npm-shrinkwrap.json +211 -232
- package/package.json +8 -8
- package/dist/builtin/i-have-adhd/CHANGELOG.md +0 -30
- package/dist/builtin/i-have-adhd/LICENSE +0 -21
- package/dist/builtin/i-have-adhd/README.md +0 -18
- package/dist/builtin/i-have-adhd/index.ts +0 -238
- package/dist/builtin/i-have-adhd/package.json +0 -56
- package/dist/builtin/i-have-adhd/skills/i-have-adhd/SKILL.md +0 -140
- package/dist/builtin/workflows/builtin/open-claude-design-feedback.ts +0 -359
- package/dist/builtin/workflows/skills/impeccable/scripts/live/ui-core.mjs +0 -180
- package/docs/i-have-adhd.md +0 -36
package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js
CHANGED
|
@@ -139,16 +139,6 @@ const ANTIPATTERNS = [
|
|
|
139
139
|
skillSection: 'Typography',
|
|
140
140
|
skillGuideline: 'overused fonts like Inter',
|
|
141
141
|
},
|
|
142
|
-
{
|
|
143
|
-
id: 'single-font',
|
|
144
|
-
category: 'slop',
|
|
145
|
-
scopes: ['type'],
|
|
146
|
-
name: 'Single font without hierarchy',
|
|
147
|
-
description:
|
|
148
|
-
'Only one font family is used for the entire page. A single family can work when weight and size contrast carry the hierarchy; otherwise pair a distinctive display font with a refined body font.',
|
|
149
|
-
skillSection: 'Typography',
|
|
150
|
-
skillGuideline: 'only one font family for the entire page',
|
|
151
|
-
},
|
|
152
142
|
{
|
|
153
143
|
id: 'flat-type-hierarchy',
|
|
154
144
|
category: 'slop',
|
|
@@ -751,11 +741,43 @@ function contrastRatio(c1, c2) {
|
|
|
751
741
|
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
|
752
742
|
}
|
|
753
743
|
|
|
744
|
+
// The CSS color functions worth pulling out of a longer declaration. The set
|
|
745
|
+
// is deliberately closed: `linear-gradient(` and `url(` also look like
|
|
746
|
+
// `name(` and must not be read as colors.
|
|
747
|
+
const COLOR_FUNCTION_NAMES = new Set([
|
|
748
|
+
'rgb', 'rgba', 'hsl', 'hsla', 'hwb', 'oklch', 'oklab', 'lch', 'lab', 'color', 'color-mix',
|
|
749
|
+
]);
|
|
750
|
+
|
|
751
|
+
// Pull every color-function token out of a value, with balanced-paren capture
|
|
752
|
+
// so nested forms (`color-mix(in oklab, oklch(...) 20%, transparent)`) survive
|
|
753
|
+
// whole. Returns the raw substrings in source order.
|
|
754
|
+
function extractColorFunctionTokens(value) {
|
|
755
|
+
const str = String(value || '');
|
|
756
|
+
const tokens = [];
|
|
757
|
+
const re = /([a-z][a-z-]*)\(/gi;
|
|
758
|
+
let m;
|
|
759
|
+
while ((m = re.exec(str)) !== null) {
|
|
760
|
+
if (!COLOR_FUNCTION_NAMES.has(m[1].toLowerCase())) continue;
|
|
761
|
+
let depth = 0, end = -1;
|
|
762
|
+
for (let i = m.index + m[0].length - 1; i < str.length; i++) {
|
|
763
|
+
if (str[i] === '(') depth++;
|
|
764
|
+
else if (str[i] === ')') { depth--; if (depth === 0) { end = i; break; } }
|
|
765
|
+
}
|
|
766
|
+
if (end < 0) break;
|
|
767
|
+
tokens.push(str.slice(m.index, end + 1));
|
|
768
|
+
re.lastIndex = end + 1;
|
|
769
|
+
}
|
|
770
|
+
return tokens;
|
|
771
|
+
}
|
|
772
|
+
|
|
754
773
|
function parseGradientColors(bgImage) {
|
|
755
774
|
if (!bgImage || !bgImage.includes('gradient')) return [];
|
|
756
775
|
const colors = [];
|
|
757
|
-
|
|
758
|
-
|
|
776
|
+
// Stops arrive in whatever syntax the author wrote and the browser kept.
|
|
777
|
+
// A dark ground painted as `linear-gradient(oklch(...), oklch(...))` used
|
|
778
|
+
// to read as a gradient with no stops at all.
|
|
779
|
+
for (const token of extractColorFunctionTokens(bgImage)) {
|
|
780
|
+
const c = parseAnyColor(token);
|
|
759
781
|
if (c) colors.push(c);
|
|
760
782
|
}
|
|
761
783
|
for (const m of bgImage.matchAll(/#([0-9a-f]{6}|[0-9a-f]{3})\b/gi)) {
|
|
@@ -792,141 +814,587 @@ function colorToHex(c) {
|
|
|
792
814
|
return '#' + [c.r, c.g, c.b].map(v => v.toString(16).padStart(2, '0')).join('');
|
|
793
815
|
}
|
|
794
816
|
|
|
795
|
-
//
|
|
796
|
-
|
|
817
|
+
// ─── Color-space conversions ────────────────────────────────────────────────
|
|
818
|
+
//
|
|
819
|
+
// Every function here lands on 8-bit sRGB, clamped to gamut. Chrome, Safari,
|
|
820
|
+
// and Firefox all keep the authored color space in getComputedStyle output
|
|
821
|
+
// (`oklch(0.84 0.19 80.46)`, `lch(20 5 60)`, `color(srgb 1.04 0.72 -0.21)`),
|
|
822
|
+
// so a detector that only reads rgb() is blind on any modern palette. The
|
|
823
|
+
// expected outputs are pinned in tests/detect-antipatterns.test.js against
|
|
824
|
+
// what Chrome itself paints for the same strings.
|
|
797
825
|
|
|
798
|
-
function
|
|
799
|
-
return
|
|
800
|
-
.split('|')
|
|
801
|
-
.map(part => part.split(':')[0].trim().toLowerCase())
|
|
802
|
-
.filter(Boolean);
|
|
826
|
+
function clamp01(x) {
|
|
827
|
+
return Number.isFinite(x) ? Math.max(0, Math.min(1, x)) : 0;
|
|
803
828
|
}
|
|
804
829
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
830
|
+
// Linear-light sRGB channel to the encoded 0-255 value.
|
|
831
|
+
function encodeSrgbChannel(x) {
|
|
832
|
+
const c = clamp01(x);
|
|
833
|
+
return Math.round((c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055) * 255);
|
|
834
|
+
}
|
|
808
835
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
836
|
+
function decodeSrgbChannel(x) {
|
|
837
|
+
const c = Number.isFinite(x) ? x : 0;
|
|
838
|
+
const sign = c < 0 ? -1 : 1;
|
|
839
|
+
const abs = Math.abs(c);
|
|
840
|
+
return sign * (abs <= 0.04045 ? abs / 12.92 : Math.pow((abs + 0.055) / 1.055, 2.4));
|
|
841
|
+
}
|
|
815
842
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
}
|
|
820
|
-
}
|
|
843
|
+
function linearSrgbToColor(r, g, b, a = 1) {
|
|
844
|
+
return { r: encodeSrgbChannel(r), g: encodeSrgbChannel(g), b: encodeSrgbChannel(b), a };
|
|
845
|
+
}
|
|
821
846
|
|
|
822
|
-
|
|
847
|
+
// OKLab to sRGB (Björn Ottosson's matrices). L in 0..1, a/b are signed axes.
|
|
848
|
+
function oklabToRgb(L, a, b) {
|
|
849
|
+
const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
|
|
850
|
+
const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
|
|
851
|
+
const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
|
|
852
|
+
const lc = l_ * l_ * l_, mc = m_ * m_ * m_, sc = s_ * s_ * s_;
|
|
853
|
+
return linearSrgbToColor(
|
|
854
|
+
4.0767416621 * lc - 3.3077115913 * mc + 0.2309699292 * sc,
|
|
855
|
+
-1.2684380046 * lc + 2.6097574011 * mc - 0.3413193965 * sc,
|
|
856
|
+
-0.0041960863 * lc - 0.7034186147 * mc + 1.7076147010 * sc,
|
|
857
|
+
);
|
|
823
858
|
}
|
|
824
859
|
|
|
825
|
-
//
|
|
826
|
-
|
|
860
|
+
// OKLCH to sRGB. L in 0..1, C in 0..~0.4 typical, H in degrees. Chroma past
|
|
861
|
+
// the sRGB gamut clamps per channel rather than producing NaN.
|
|
862
|
+
function oklchToRgb(L, C, H) {
|
|
863
|
+
const hRad = (H * Math.PI) / 180;
|
|
864
|
+
return oklabToRgb(L, C * Math.cos(hRad), C * Math.sin(hRad));
|
|
865
|
+
}
|
|
827
866
|
|
|
828
|
-
//
|
|
867
|
+
// CIE Lab to sRGB. CSS lab()/lch() use the D50 white point; the matrix below
|
|
868
|
+
// is the Bradford-adapted XYZ-D50 to linear-sRGB transform from CSS Color 4.
|
|
869
|
+
function labToRgb(L, a, b) {
|
|
870
|
+
const kappa = 24389 / 27, epsilon = 216 / 24389;
|
|
871
|
+
const fy = (L + 16) / 116, fx = fy + a / 500, fz = fy - b / 200;
|
|
872
|
+
const invert = (t) => (t * t * t > epsilon ? t * t * t : (116 * t - 16) / kappa);
|
|
873
|
+
const yr = L > kappa * epsilon ? Math.pow((L + 16) / 116, 3) : L / kappa;
|
|
874
|
+
const Xn = 0.3457 / 0.3585, Zn = (1 - 0.3457 - 0.3585) / 0.3585;
|
|
875
|
+
const x = invert(fx) * Xn, y = yr, z = invert(fz) * Zn;
|
|
876
|
+
return linearSrgbToColor(
|
|
877
|
+
3.1341359569958707 * x - 1.6173863321612538 * y - 0.4906619460083532 * z,
|
|
878
|
+
-0.9787955029120890 * x + 1.9162545672595240 * y + 0.0334427311613195 * z,
|
|
879
|
+
0.0719553798841168 * x - 0.2289768264158322 * y + 1.4053860583241250 * z,
|
|
880
|
+
);
|
|
881
|
+
}
|
|
829
882
|
|
|
830
|
-
function
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
883
|
+
function lchToRgb(L, C, H) {
|
|
884
|
+
const hRad = (H * Math.PI) / 180;
|
|
885
|
+
return labToRgb(L, C * Math.cos(hRad), C * Math.sin(hRad));
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// color(<space> c1 c2 c3) for the spaces that turn up in real stylesheets.
|
|
889
|
+
// `srgb` is what Chrome serializes most color-mix() results into, routinely
|
|
890
|
+
// with channels outside 0..1. Spaces we do not model return null so callers
|
|
891
|
+
// abstain instead of measuring against a color we invented.
|
|
892
|
+
function colorFunctionToRgb(space, c1, c2, c3) {
|
|
893
|
+
switch (space) {
|
|
894
|
+
case 'srgb':
|
|
895
|
+
return { r: Math.round(clamp01(c1) * 255), g: Math.round(clamp01(c2) * 255), b: Math.round(clamp01(c3) * 255), a: 1 };
|
|
896
|
+
case 'srgb-linear':
|
|
897
|
+
return linearSrgbToColor(c1, c2, c3);
|
|
898
|
+
case 'display-p3': {
|
|
899
|
+
const [R, G, B] = [decodeSrgbChannel(c1), decodeSrgbChannel(c2), decodeSrgbChannel(c3)];
|
|
900
|
+
return linearSrgbToColor(
|
|
901
|
+
1.2249401762805587 * R - 0.2249404646817506 * G + 0.0000002884022551 * B,
|
|
902
|
+
-0.0420569547096138 * R + 1.0420571661298634 * G - 0.0000002113202247 * B,
|
|
903
|
+
-0.0196375587040044 * R - 0.0786360772174755 * G + 1.0982736359214800 * B,
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
default:
|
|
907
|
+
return null;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
841
910
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
911
|
+
function hslToRgb(h, s, l) {
|
|
912
|
+
h = ((h % 360) + 360) % 360;
|
|
913
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
914
|
+
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
915
|
+
const m0 = l - c / 2;
|
|
916
|
+
const [r, g, b] =
|
|
917
|
+
h < 60 ? [c, x, 0] :
|
|
918
|
+
h < 120 ? [x, c, 0] :
|
|
919
|
+
h < 180 ? [0, c, x] :
|
|
920
|
+
h < 240 ? [0, x, c] :
|
|
921
|
+
h < 300 ? [x, 0, c] : [c, 0, x];
|
|
922
|
+
return {
|
|
923
|
+
r: Math.round((r + m0) * 255),
|
|
924
|
+
g: Math.round((g + m0) * 255),
|
|
925
|
+
b: Math.round((b + m0) * 255),
|
|
926
|
+
a: 1,
|
|
927
|
+
};
|
|
928
|
+
}
|
|
845
929
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
930
|
+
function hwbToRgb(h, w, bl) {
|
|
931
|
+
if (w + bl >= 1) {
|
|
932
|
+
const g = Math.round((w / (w + bl)) * 255);
|
|
933
|
+
return { r: g, g, b: g, a: 1 };
|
|
934
|
+
}
|
|
935
|
+
const base = hslToRgb(h, 1, 0.5);
|
|
936
|
+
const mix = (c) => Math.round(((c / 255) * (1 - w - bl) + w) * 255);
|
|
937
|
+
return { r: mix(base.r), g: mix(base.g), b: mix(base.b), a: 1 };
|
|
938
|
+
}
|
|
849
939
|
|
|
850
|
-
|
|
851
|
-
|
|
940
|
+
// Common CSS named colors — the handful that actually show up in generated
|
|
941
|
+
// UIs, not the full 148-name spec list. Includes the achromatic names so a
|
|
942
|
+
// named gray parses (and correctly reads as no-chroma) instead of being
|
|
943
|
+
// treated as an unknown color.
|
|
944
|
+
const CSS_NAMED_COLORS = {
|
|
945
|
+
black: { r: 0, g: 0, b: 0 },
|
|
946
|
+
white: { r: 255, g: 255, b: 255 },
|
|
947
|
+
gray: { r: 128, g: 128, b: 128 },
|
|
948
|
+
grey: { r: 128, g: 128, b: 128 },
|
|
949
|
+
silver: { r: 192, g: 192, b: 192 },
|
|
950
|
+
dimgray: { r: 105, g: 105, b: 105 },
|
|
951
|
+
darkgray: { r: 169, g: 169, b: 169 },
|
|
952
|
+
lightgray: { r: 211, g: 211, b: 211 },
|
|
953
|
+
gainsboro: { r: 220, g: 220, b: 220 },
|
|
954
|
+
whitesmoke: { r: 245, g: 245, b: 245 },
|
|
955
|
+
red: { r: 255, g: 0, b: 0 },
|
|
956
|
+
crimson: { r: 220, g: 20, b: 60 },
|
|
957
|
+
tomato: { r: 255, g: 99, b: 71 },
|
|
958
|
+
coral: { r: 255, g: 127, b: 80 },
|
|
959
|
+
salmon: { r: 250, g: 128, b: 114 },
|
|
960
|
+
orange: { r: 255, g: 165, b: 0 },
|
|
961
|
+
gold: { r: 255, g: 215, b: 0 },
|
|
962
|
+
yellow: { r: 255, g: 255, b: 0 },
|
|
963
|
+
olive: { r: 128, g: 128, b: 0 },
|
|
964
|
+
lime: { r: 0, g: 255, b: 0 },
|
|
965
|
+
green: { r: 0, g: 128, b: 0 },
|
|
966
|
+
teal: { r: 0, g: 128, b: 128 },
|
|
967
|
+
turquoise: { r: 64, g: 224, b: 208 },
|
|
968
|
+
cyan: { r: 0, g: 255, b: 255 },
|
|
969
|
+
aqua: { r: 0, g: 255, b: 255 },
|
|
970
|
+
skyblue: { r: 135, g: 206, b: 235 },
|
|
971
|
+
dodgerblue: { r: 30, g: 144, b: 255 },
|
|
972
|
+
blue: { r: 0, g: 0, b: 255 },
|
|
973
|
+
navy: { r: 0, g: 0, b: 128 },
|
|
974
|
+
indigo: { r: 75, g: 0, b: 130 },
|
|
975
|
+
rebeccapurple: { r: 102, g: 51, b: 153 },
|
|
976
|
+
purple: { r: 128, g: 0, b: 128 },
|
|
977
|
+
violet: { r: 238, g: 130, b: 238 },
|
|
978
|
+
orchid: { r: 218, g: 112, b: 214 },
|
|
979
|
+
magenta: { r: 255, g: 0, b: 255 },
|
|
980
|
+
fuchsia: { r: 255, g: 0, b: 255 },
|
|
981
|
+
hotpink: { r: 255, g: 105, b: 180 },
|
|
982
|
+
pink: { r: 255, g: 192, b: 203 },
|
|
983
|
+
maroon: { r: 128, g: 0, b: 0 },
|
|
984
|
+
};
|
|
852
985
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
// table cells, and <hr> never reach here (BORDER_SAFE_TAGS).
|
|
865
|
-
else if (!opts.tabContext && w >= 3 && w <= 12) {
|
|
866
|
-
findings.push({ id: 'side-tab', snippet: `border-${sn}: ${w}px` });
|
|
867
|
-
}
|
|
986
|
+
// Split a string on top-level commas (ignoring commas nested in parens).
|
|
987
|
+
function splitTopLevelCommas(str) {
|
|
988
|
+
const parts = [];
|
|
989
|
+
let depth = 0, start = 0;
|
|
990
|
+
for (let i = 0; i < str.length; i++) {
|
|
991
|
+
const ch = str[i];
|
|
992
|
+
if (ch === '(') depth++;
|
|
993
|
+
else if (ch === ')') depth = Math.max(0, depth - 1);
|
|
994
|
+
else if (ch === ',' && depth === 0) {
|
|
995
|
+
parts.push(str.slice(start, i).trim());
|
|
996
|
+
start = i + 1;
|
|
868
997
|
}
|
|
869
998
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
// Returns true if the given text is composed entirely of emoji characters
|
|
875
|
-
// (plus whitespace / variation selectors). Emojis render as multicolor glyphs
|
|
876
|
-
// regardless of CSS `color`, so contrast checks against the element's text
|
|
877
|
-
// color are meaningless for these nodes.
|
|
878
|
-
const EMOJI_CHAR_RE = /[\u{1F1E6}-\u{1F1FF}\u{1F300}-\u{1F9FF}\u{1FA00}-\u{1FAFF}\u{2600}-\u{27BF}\u{2300}-\u{23FF}\u{FE0F}\u{200D}\u{1F3FB}-\u{1F3FF}]/u;
|
|
879
|
-
const EMOJI_CHARS_GLOBAL = /[\u{1F1E6}-\u{1F1FF}\u{1F300}-\u{1F9FF}\u{1FA00}-\u{1FAFF}\u{2600}-\u{27BF}\u{2300}-\u{23FF}\u{FE0F}\u{200D}\u{1F3FB}-\u{1F3FF}]/gu;
|
|
880
|
-
function isEmojiOnlyText(text) {
|
|
881
|
-
if (!text) return false;
|
|
882
|
-
if (!EMOJI_CHAR_RE.test(text)) return false;
|
|
883
|
-
return text.replace(EMOJI_CHARS_GLOBAL, '').trim() === '';
|
|
999
|
+
const tail = str.slice(start).trim();
|
|
1000
|
+
if (tail) parts.push(tail);
|
|
1001
|
+
return parts;
|
|
884
1002
|
}
|
|
885
1003
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
// color that fails against every stop sails through on the
|
|
905
|
-
// SAFE_TAGS suppression (the shipped escape).
|
|
906
|
-
|| (bgImage && /gradient/i.test(bgImage)))
|
|
907
|
-
&& fontSize >= 9;
|
|
908
|
-
if (!isStyledControl) return [];
|
|
1004
|
+
// Evaluate a CSS color-mix() expression to {r,g,b,a}. Returns null when
|
|
1005
|
+
// the expression can't be resolved (unresolved var(), unknown colors).
|
|
1006
|
+
//
|
|
1007
|
+
// Mixing is done with premultiplied alpha in sRGB regardless of the
|
|
1008
|
+
// declared interpolation space. That is exact for the dominant generated-UI
|
|
1009
|
+
// pattern — `color-mix(in oklab, <color> N%, transparent)` — where the
|
|
1010
|
+
// result is simply <color> at alpha N% in ANY rectangular space, and a
|
|
1011
|
+
// close-enough approximation for opaque-opaque mixes (the detector only
|
|
1012
|
+
// consumes these values for contrast/chroma thresholds, not for display).
|
|
1013
|
+
function parseColorMix(str) {
|
|
1014
|
+
const m = String(str).trim().match(/^color-mix\(/i);
|
|
1015
|
+
if (!m) return null;
|
|
1016
|
+
// Balanced-paren capture of the arguments.
|
|
1017
|
+
let depth = 0, end = -1;
|
|
1018
|
+
const open = str.indexOf('(');
|
|
1019
|
+
for (let i = open; i < str.length; i++) {
|
|
1020
|
+
if (str[i] === '(') depth++;
|
|
1021
|
+
else if (str[i] === ')') { depth--; if (depth === 0) { end = i; break; } }
|
|
909
1022
|
}
|
|
910
|
-
|
|
1023
|
+
if (end < 0) return null;
|
|
1024
|
+
const args = splitTopLevelCommas(str.slice(open + 1, end));
|
|
1025
|
+
if (args.length !== 3 || !/^in\s/i.test(args[0])) return null;
|
|
911
1026
|
|
|
912
|
-
|
|
913
|
-
//
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1027
|
+
const parseComponent = (component) => {
|
|
1028
|
+
// Percentage may lead or trail the color per spec.
|
|
1029
|
+
let pct = null;
|
|
1030
|
+
let colorStr = component;
|
|
1031
|
+
const trail = component.match(/\s+([\d.]+)%$/);
|
|
1032
|
+
const lead = component.match(/^([\d.]+)%\s+/);
|
|
1033
|
+
if (trail) { pct = parseFloat(trail[1]); colorStr = component.slice(0, trail.index).trim(); }
|
|
1034
|
+
else if (lead) { pct = parseFloat(lead[1]); colorStr = component.slice(lead[0].length).trim(); }
|
|
1035
|
+
let color;
|
|
1036
|
+
if (/^transparent$/i.test(colorStr)) color = { r: 0, g: 0, b: 0, a: 0 };
|
|
1037
|
+
else color = parseAnyColor(colorStr);
|
|
1038
|
+
if (!color) return null;
|
|
1039
|
+
return { color, pct };
|
|
1040
|
+
};
|
|
1041
|
+
|
|
1042
|
+
const c1 = parseComponent(args[1]);
|
|
1043
|
+
const c2 = parseComponent(args[2]);
|
|
1044
|
+
if (!c1 || !c2) return null;
|
|
1045
|
+
let p1 = c1.pct, p2 = c2.pct;
|
|
1046
|
+
if (p1 == null && p2 == null) { p1 = 50; p2 = 50; }
|
|
1047
|
+
else if (p1 == null) p1 = 100 - p2;
|
|
1048
|
+
else if (p2 == null) p2 = 100 - p1;
|
|
1049
|
+
const sum = p1 + p2;
|
|
1050
|
+
if (sum <= 0) return null;
|
|
1051
|
+
// Per spec: weights normalize to sum; when sum < 100 the result alpha is
|
|
1052
|
+
// additionally scaled by sum/100.
|
|
1053
|
+
const w1 = p1 / sum, w2 = p2 / sum;
|
|
1054
|
+
const alphaScale = sum < 100 ? sum / 100 : 1;
|
|
1055
|
+
const a1 = c1.color.a ?? 1, a2 = c2.color.a ?? 1;
|
|
1056
|
+
const a = (a1 * w1 + a2 * w2) * alphaScale;
|
|
1057
|
+
if (a <= 0) return { r: 0, g: 0, b: 0, a: 0 };
|
|
1058
|
+
const mix = (ch) => Math.round((c1.color[ch] * a1 * w1 + c2.color[ch] * a2 * w2) / (a1 * w1 + a2 * w2));
|
|
1059
|
+
return { r: mix('r'), g: mix('g'), b: mix('b'), a: Math.min(1, a) };
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// Composite a translucent color over an opaque(ish) base (simple
|
|
1063
|
+
// source-over in sRGB). Returns an opaque {r,g,b,a:1}.
|
|
1064
|
+
function compositeColorOver(top, base) {
|
|
1065
|
+
const a = top.a ?? 1;
|
|
1066
|
+
return {
|
|
1067
|
+
r: Math.round(top.r * a + base.r * (1 - a)),
|
|
1068
|
+
g: Math.round(top.g * a + base.g * (1 - a)),
|
|
1069
|
+
b: Math.round(top.b * a + base.b * (1 - a)),
|
|
1070
|
+
a: 1,
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// A color() / lab() / lch() component: a bare number, a percentage against
|
|
1075
|
+
// `scale`, or the `none` keyword (which resolves to zero for our purposes).
|
|
1076
|
+
function parseColorComponent(token, scale = 1) {
|
|
1077
|
+
if (token == null) return null;
|
|
1078
|
+
const t = String(token).trim();
|
|
1079
|
+
if (/^none$/i.test(t)) return 0;
|
|
1080
|
+
const num = parseFloat(t);
|
|
1081
|
+
if (!Number.isFinite(num)) return null;
|
|
1082
|
+
return t.endsWith('%') ? (num / 100) * scale : num;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function parseAlphaToken(token) {
|
|
1086
|
+
if (token == null) return 1;
|
|
1087
|
+
const t = String(token).trim();
|
|
1088
|
+
if (/^none$/i.test(t)) return 1;
|
|
1089
|
+
const num = parseFloat(t);
|
|
1090
|
+
if (!Number.isFinite(num)) return 1;
|
|
1091
|
+
return t.endsWith('%') ? num / 100 : num;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// Extended color parser: rgb/rgba/hex/oklch/oklab/lch/lab/hsl/hwb/color()/
|
|
1095
|
+
// color-mix/common named colors. Returns null on no match. Use this when the
|
|
1096
|
+
// input might be any CSS color form; use plain parseRgb when you only expect
|
|
1097
|
+
// computed rgb() values from real browsers.
|
|
1098
|
+
function parseAnyColor(s) {
|
|
1099
|
+
if (!s || typeof s !== 'string') return null;
|
|
1100
|
+
const str = s.trim();
|
|
1101
|
+
if (str === 'transparent' || str === 'currentcolor' || str === 'inherit') return null;
|
|
1102
|
+
if (/^color-mix\(/i.test(str)) return parseColorMix(str);
|
|
1103
|
+
let m;
|
|
1104
|
+
m = str.match(/rgba?\(\s*(\d+(?:\.\d+)?)\s*,?\s*(\d+(?:\.\d+)?)\s*,?\s*(\d+(?:\.\d+)?)(?:\s*[,/]\s*([\d.]+)(%)?)?\s*\)/);
|
|
1105
|
+
if (m) {
|
|
1106
|
+
const c = { r: Math.round(+m[1]), g: Math.round(+m[2]), b: Math.round(+m[3]), a: 1 };
|
|
1107
|
+
if (m[4] !== undefined) c.a = m[5] === '%' ? parseFloat(m[4]) / 100 : +m[4];
|
|
1108
|
+
return c;
|
|
1109
|
+
}
|
|
1110
|
+
m = str.match(/^#([0-9a-f]{3,8})$/i);
|
|
1111
|
+
if (m) {
|
|
1112
|
+
const h = m[1];
|
|
1113
|
+
if (h.length === 3 || h.length === 4) {
|
|
1114
|
+
return {
|
|
1115
|
+
r: parseInt(h[0] + h[0], 16),
|
|
1116
|
+
g: parseInt(h[1] + h[1], 16),
|
|
1117
|
+
b: parseInt(h[2] + h[2], 16),
|
|
1118
|
+
a: h.length === 4 ? parseInt(h[3] + h[3], 16) / 255 : 1,
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
if (h.length === 6 || h.length === 8) {
|
|
1122
|
+
return {
|
|
1123
|
+
r: parseInt(h.slice(0, 2), 16),
|
|
1124
|
+
g: parseInt(h.slice(2, 4), 16),
|
|
1125
|
+
b: parseInt(h.slice(4, 6), 16),
|
|
1126
|
+
a: h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1,
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
// OKLCH parser. Tailwind v4's CSS minifier squishes the space after
|
|
1131
|
+
// `%` ("21.5%.02 50"), so the separator between L and C may be absent.
|
|
1132
|
+
// Match L (with optional %), then C and H separated permissively.
|
|
1133
|
+
m = str.match(/oklch\(\s*([\d.]+)(%?)\s*[\s,]*\s*([\d.]+)\s*[\s,]+\s*([-\d.]+)(?:deg)?(?:\s*\/\s*([\d.]+)(%)?)?\s*\)/i);
|
|
1134
|
+
if (m) {
|
|
1135
|
+
const Lnum = parseFloat(m[1]);
|
|
1136
|
+
const L = m[2] === '%' ? Lnum / 100 : Lnum;
|
|
1137
|
+
const rgb = oklchToRgb(L, parseFloat(m[3]), parseFloat(m[4]));
|
|
1138
|
+
if (m[5] !== undefined) {
|
|
1139
|
+
const alpha = parseFloat(m[5]);
|
|
1140
|
+
rgb.a = m[6] === '%' ? alpha / 100 : alpha;
|
|
1141
|
+
}
|
|
1142
|
+
return rgb;
|
|
1143
|
+
}
|
|
1144
|
+
// OKLAB — a/b are signed axes; percentages map 100% → 0.4.
|
|
1145
|
+
m = str.match(/oklab\(\s*([\d.]+)(%?)\s+(-?[\d.]+)(%?)\s+(-?[\d.]+)(%?)(?:\s*\/\s*([\d.]+)(%)?)?\s*\)/i);
|
|
1146
|
+
if (m) {
|
|
1147
|
+
const L = m[2] === '%' ? parseFloat(m[1]) / 100 : parseFloat(m[1]);
|
|
1148
|
+
const a = m[4] === '%' ? parseFloat(m[3]) * 0.004 : parseFloat(m[3]);
|
|
1149
|
+
const b = m[6] === '%' ? parseFloat(m[5]) * 0.004 : parseFloat(m[5]);
|
|
1150
|
+
const rgb = oklabToRgb(L, a, b);
|
|
1151
|
+
if (m[7] !== undefined) {
|
|
1152
|
+
const alpha = parseFloat(m[7]);
|
|
1153
|
+
rgb.a = m[8] === '%' ? alpha / 100 : alpha;
|
|
1154
|
+
}
|
|
1155
|
+
return rgb;
|
|
1156
|
+
}
|
|
1157
|
+
// LCH / LAB — CIE, D50 white point. Chrome serializes lch(20% 5 60) as
|
|
1158
|
+
// `lch(20 5 60)`, so L arrives with or without its percent sign. In both
|
|
1159
|
+
// spaces L runs 0..100 and 100% means 100.
|
|
1160
|
+
m = str.match(/^lch\(\s*([\d.]+%?|none)\s+([\d.]+%?|none)\s+(-?[\d.]+)(?:deg)?(?:\s*\/\s*([\d.]+%?|none))?\s*\)$/i);
|
|
1161
|
+
if (m) {
|
|
1162
|
+
const L = parseColorComponent(m[1], 100);
|
|
1163
|
+
const C = parseColorComponent(m[2], 150);
|
|
1164
|
+
const H = parseFloat(m[3]);
|
|
1165
|
+
if (L == null || C == null || !Number.isFinite(H)) return null;
|
|
1166
|
+
const rgb = lchToRgb(L, C, H);
|
|
1167
|
+
rgb.a = parseAlphaToken(m[4]);
|
|
1168
|
+
return rgb;
|
|
1169
|
+
}
|
|
1170
|
+
m = str.match(/^lab\(\s*([\d.]+%?|none)\s+(-?[\d.]+%?|none)\s+(-?[\d.]+%?|none)(?:\s*\/\s*([\d.]+%?|none))?\s*\)$/i);
|
|
1171
|
+
if (m) {
|
|
1172
|
+
const L = parseColorComponent(m[1], 100);
|
|
1173
|
+
const a = parseColorComponent(m[2], 125);
|
|
1174
|
+
const b = parseColorComponent(m[3], 125);
|
|
1175
|
+
if (L == null || a == null || b == null) return null;
|
|
1176
|
+
const rgb = labToRgb(L, a, b);
|
|
1177
|
+
rgb.a = parseAlphaToken(m[4]);
|
|
1178
|
+
return rgb;
|
|
1179
|
+
}
|
|
1180
|
+
// color(<space> c1 c2 c3 [/ alpha]) — what Chrome hands back for most
|
|
1181
|
+
// color-mix() results and for any wide-gamut color an author wrote.
|
|
1182
|
+
m = str.match(/^color\(\s*([a-z0-9-]+)\s+(-?[\d.eE+-]+%?|none)\s+(-?[\d.eE+-]+%?|none)\s+(-?[\d.eE+-]+%?|none)(?:\s*\/\s*([\d.]+%?|none))?\s*\)$/i);
|
|
1183
|
+
if (m) {
|
|
1184
|
+
const c1 = parseColorComponent(m[2]);
|
|
1185
|
+
const c2 = parseColorComponent(m[3]);
|
|
1186
|
+
const c3 = parseColorComponent(m[4]);
|
|
1187
|
+
if (c1 == null || c2 == null || c3 == null) return null;
|
|
1188
|
+
const rgb = colorFunctionToRgb(m[1].toLowerCase(), c1, c2, c3);
|
|
1189
|
+
if (!rgb) return null;
|
|
1190
|
+
rgb.a = parseAlphaToken(m[5]);
|
|
1191
|
+
return rgb;
|
|
1192
|
+
}
|
|
1193
|
+
// HSL/HSLA — comma or space syntax, optional deg on hue.
|
|
1194
|
+
m = str.match(/hsla?\(\s*(-?[\d.]+)(?:deg)?\s*[,\s]\s*([\d.]+)%\s*[,\s]\s*([\d.]+)%(?:\s*[,/]\s*([\d.]+)(%)?)?\s*\)/i);
|
|
1195
|
+
if (m) {
|
|
1196
|
+
const rgb = hslToRgb(parseFloat(m[1]), parseFloat(m[2]) / 100, parseFloat(m[3]) / 100);
|
|
1197
|
+
if (m[4] !== undefined) {
|
|
1198
|
+
const alpha = parseFloat(m[4]);
|
|
1199
|
+
rgb.a = m[5] === '%' ? alpha / 100 : alpha;
|
|
1200
|
+
}
|
|
1201
|
+
return rgb;
|
|
1202
|
+
}
|
|
1203
|
+
// HWB — hue whiteness% blackness%.
|
|
1204
|
+
m = str.match(/hwb\(\s*(-?[\d.]+)(?:deg)?\s+([\d.]+)%\s+([\d.]+)%(?:\s*\/\s*([\d.]+)(%)?)?\s*\)/i);
|
|
1205
|
+
if (m) {
|
|
1206
|
+
const rgb = hwbToRgb(parseFloat(m[1]), parseFloat(m[2]) / 100, parseFloat(m[3]) / 100);
|
|
1207
|
+
if (m[4] !== undefined) {
|
|
1208
|
+
const alpha = parseFloat(m[4]);
|
|
1209
|
+
rgb.a = m[5] === '%' ? alpha / 100 : alpha;
|
|
1210
|
+
}
|
|
1211
|
+
return rgb;
|
|
1212
|
+
}
|
|
1213
|
+
const named = CSS_NAMED_COLORS[str.toLowerCase()];
|
|
1214
|
+
if (named) return { ...named, a: 1 };
|
|
1215
|
+
return null;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// True when a computed background-color string names no paint at all. Used to
|
|
1219
|
+
// tell "this layer is see-through" (walk on to the ancestor) apart from "this
|
|
1220
|
+
// layer has a color we could not read" (stop and abstain).
|
|
1221
|
+
//
|
|
1222
|
+
// `inherit` belongs here even though it is not literally see-through: it means
|
|
1223
|
+
// "paint with the parent's background-color", and walking on to the parent IS
|
|
1224
|
+
// that resolution. Real browsers resolve the keyword before getComputedStyle
|
|
1225
|
+
// output; only jsdom's partial cascade hands it through verbatim, and treating
|
|
1226
|
+
// it as unreadable would make the walk abstain on a surface it can know.
|
|
1227
|
+
// (`currentcolor` is NOT here — it is real paint in the element's own text
|
|
1228
|
+
// color; resolveBackgroundInfo substitutes the computed color for it.)
|
|
1229
|
+
function isNoPaintColorValue(value) {
|
|
1230
|
+
const v = String(value || '').trim().toLowerCase();
|
|
1231
|
+
if (!v) return true;
|
|
1232
|
+
return v === 'transparent' || v === 'none' || v === 'initial' || v === 'inherit' || v === 'unset' || v === 'revert' || v === 'revert-layer';
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// --- cli/engine/shared/fonts.mjs ---
|
|
1236
|
+
const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi;
|
|
1237
|
+
|
|
1238
|
+
function normalizeGoogleFontFamilyParam(value) {
|
|
1239
|
+
return String(value || '')
|
|
1240
|
+
.split('|')
|
|
1241
|
+
.map(part => part.split(':')[0].trim().toLowerCase())
|
|
1242
|
+
.filter(Boolean);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function extractGoogleFontFamilies(text) {
|
|
1246
|
+
const families = [];
|
|
1247
|
+
if (!text) return families;
|
|
1248
|
+
|
|
1249
|
+
GOOGLE_FONTS_URL_RE.lastIndex = 0;
|
|
1250
|
+
let urlMatch;
|
|
1251
|
+
while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) {
|
|
1252
|
+
const url = urlMatch[0];
|
|
1253
|
+
const queryStart = url.indexOf('?');
|
|
1254
|
+
if (queryStart === -1) continue;
|
|
1255
|
+
|
|
1256
|
+
const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&'));
|
|
1257
|
+
for (const value of params.getAll('family')) {
|
|
1258
|
+
families.push(...normalizeGoogleFontFamilyParam(value));
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
return families;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// --- cli/engine/rules/checks.mjs ---
|
|
1266
|
+
const DETECTOR_IS_BROWSER = typeof window !== 'undefined';
|
|
1267
|
+
|
|
1268
|
+
// ─── Section 3: Pure Detection ──────────────────────────────────────────────
|
|
1269
|
+
|
|
1270
|
+
function checkBorders(tag, widths, colors, radius, opts = {}) {
|
|
1271
|
+
// Badge-shaped <span>s (own visible background) are a real stripe target
|
|
1272
|
+
// for the top/bottom variant — the inline-tag exemption exists to quiet
|
|
1273
|
+
// text-level borders, not chips. They skip the left/right arms below.
|
|
1274
|
+
const spanBadge = tag === 'span' && !!opts.badgeLike;
|
|
1275
|
+
if (BORDER_SAFE_TAGS.has(tag) && !spanBadge) return [];
|
|
1276
|
+
// A live status/alert region wears a colored single-edge border as a
|
|
1277
|
+
// severity accent (toast, snackbar, callout), not as the side-tab tell.
|
|
1278
|
+
if (opts.statusContext) return [];
|
|
1279
|
+
const findings = [];
|
|
1280
|
+
const sides = ['Top', 'Right', 'Bottom', 'Left'];
|
|
1281
|
+
|
|
1282
|
+
for (const side of sides) {
|
|
1283
|
+
const w = widths[side];
|
|
1284
|
+
if (w < 1 || isNeutralColor(colors[side])) continue;
|
|
1285
|
+
|
|
1286
|
+
const otherSides = sides.filter(s => s !== side);
|
|
1287
|
+
const maxOther = Math.max(...otherSides.map(s => widths[s]));
|
|
1288
|
+
if (!(w >= 2 && (maxOther <= 1 || w >= maxOther * 2))) continue;
|
|
1289
|
+
|
|
1290
|
+
const sn = side.toLowerCase();
|
|
1291
|
+
const isSide = side === 'Left' || side === 'Right';
|
|
1292
|
+
|
|
1293
|
+
if (isSide) {
|
|
1294
|
+
if (spanBadge) continue;
|
|
1295
|
+
if (radius > 0) findings.push({ id: 'side-tab', snippet: `border-${sn}: ${w}px + border-radius: ${radius}px` });
|
|
1296
|
+
else if (w >= 3) findings.push({ id: 'side-tab', snippet: `border-${sn}: ${w}px` });
|
|
1297
|
+
} else {
|
|
1298
|
+
if (radius > 0 && w >= 2) findings.push({ id: 'border-accent-on-rounded', snippet: `border-${sn}: ${w}px + border-radius: ${radius}px` });
|
|
1299
|
+
// Horizontal variant of the side-tab stripe: a thick chromatic accent
|
|
1300
|
+
// riding the top or bottom edge of a card/badge/container. Same
|
|
1301
|
+
// dominant-edge + chroma gates as left/right, 3-12px band. Selected-
|
|
1302
|
+
// tab underlines are exempt via opts.tabContext (adapters look for
|
|
1303
|
+
// tablist/nav/tab ancestors and aria-selected); links, buttons,
|
|
1304
|
+
// table cells, and <hr> never reach here (BORDER_SAFE_TAGS).
|
|
1305
|
+
else if (!opts.tabContext && w >= 3 && w <= 12) {
|
|
1306
|
+
findings.push({ id: 'side-tab', snippet: `border-${sn}: ${w}px` });
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
return findings;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// ─── Scoped ignores: data-impeccable-ignore ─────────────────────────────────
|
|
1315
|
+
//
|
|
1316
|
+
// An element-scoped waiver that travels with the markup: any element carrying
|
|
1317
|
+
// `data-impeccable-ignore="rule-a rule-b"` (or `*`, or an empty value, for
|
|
1318
|
+
// every rule) suppresses matching findings from itself and its entire subtree,
|
|
1319
|
+
// in every engine that walks elements — the browser overlay, the extension,
|
|
1320
|
+
// and the static scan. This is the DOM twin of the line-based
|
|
1321
|
+
// `impeccable-disable` comment directives, which the browser cannot apply (a
|
|
1322
|
+
// live DOM has no line numbers), and the generalization of the one-off
|
|
1323
|
+
// `data-impeccable-allow-kickers` opt-out.
|
|
1324
|
+
//
|
|
1325
|
+
// The intended use is curated exhibits: a page that documents anti-patterns by
|
|
1326
|
+
// example, or renders a deliberate "before" specimen, marks the container once
|
|
1327
|
+
// and every engine skips it while still scanning the page around it.
|
|
1328
|
+
function scopedIgnoreActive(el, ruleId) {
|
|
1329
|
+
const rule = String(ruleId || '').toLowerCase();
|
|
1330
|
+
let cur = el;
|
|
1331
|
+
while (cur && cur.nodeType === 1) {
|
|
1332
|
+
const attr = typeof cur.getAttribute === 'function' ? cur.getAttribute('data-impeccable-ignore') : null;
|
|
1333
|
+
if (attr != null) {
|
|
1334
|
+
const rules = String(attr).trim().toLowerCase().split(/[\s,]+/).filter(Boolean);
|
|
1335
|
+
if (rules.length === 0 || rules.includes('*') || rules.includes(rule)) return true;
|
|
1336
|
+
}
|
|
1337
|
+
cur = cur.parentElement;
|
|
1338
|
+
}
|
|
1339
|
+
return false;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// Returns true if the given text is composed entirely of emoji characters
|
|
1343
|
+
// (plus whitespace / variation selectors). Emojis render as multicolor glyphs
|
|
1344
|
+
// regardless of CSS `color`, so contrast checks against the element's text
|
|
1345
|
+
// color are meaningless for these nodes.
|
|
1346
|
+
const EMOJI_CHAR_RE = /[\u{1F1E6}-\u{1F1FF}\u{1F300}-\u{1F9FF}\u{1FA00}-\u{1FAFF}\u{2600}-\u{27BF}\u{2300}-\u{23FF}\u{FE0F}\u{200D}\u{1F3FB}-\u{1F3FF}]/u;
|
|
1347
|
+
const EMOJI_CHARS_GLOBAL = /[\u{1F1E6}-\u{1F1FF}\u{1F300}-\u{1F9FF}\u{1FA00}-\u{1FAFF}\u{2600}-\u{27BF}\u{2300}-\u{23FF}\u{FE0F}\u{200D}\u{1F3FB}-\u{1F3FF}]/gu;
|
|
1348
|
+
function isEmojiOnlyText(text) {
|
|
1349
|
+
if (!text) return false;
|
|
1350
|
+
if (!EMOJI_CHAR_RE.test(text)) return false;
|
|
1351
|
+
return text.replace(EMOJI_CHARS_GLOBAL, '').trim() === '';
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
function checkColors(opts) {
|
|
1355
|
+
const { tag, textColor, bgColor, effectiveBg, effectiveBgStops, fontSize, fontWeight, hasDirectText, isEmojiOnly, bgClip, bgImage, classList } = opts;
|
|
1356
|
+
if (SAFE_TAGS.has(tag)) {
|
|
1357
|
+
// Exception for elements styled as controls or chips. SAFE_TAGS exists to
|
|
1358
|
+
// suppress contrast noise on inline links and unstyled spans, where the
|
|
1359
|
+
// element has no own background and the contrast against the ancestor
|
|
1360
|
+
// surface is already the intended visual. When the element paints its own
|
|
1361
|
+
// opaque background under direct text, it is a styled button, chip, or
|
|
1362
|
+
// badge regardless of tag, and contrast on its own surface is a real,
|
|
1363
|
+
// frequent bug worth flagging. (The shipped miss: a <span> severity chip
|
|
1364
|
+
// whose white text lost a specificity fight and rendered muted-on-red at
|
|
1365
|
+
// 1.2:1; the old a/button-only exception never looked at it.) The 9px
|
|
1366
|
+
// font floor keeps sub-text decorations out.
|
|
1367
|
+
const isStyledControl = hasDirectText
|
|
1368
|
+
&& ((bgColor && bgColor.a > 0.5)
|
|
1369
|
+
// A gradient painted on the element itself is an own surface the
|
|
1370
|
+
// same way a solid background is. Without this branch a nav CTA
|
|
1371
|
+
// built as `<a>` with `background: linear-gradient(…)` and a text
|
|
1372
|
+
// color that fails against every stop sails through on the
|
|
1373
|
+
// SAFE_TAGS suppression (the shipped escape).
|
|
1374
|
+
|| (bgImage && /gradient/i.test(bgImage)))
|
|
1375
|
+
&& fontSize >= 9;
|
|
1376
|
+
if (!isStyledControl) return [];
|
|
1377
|
+
}
|
|
1378
|
+
const findings = [];
|
|
1379
|
+
|
|
1380
|
+
if (hasDirectText && textColor && !isEmojiOnly) {
|
|
1381
|
+
// Gradient-clipped text (`background-clip: text`, typically with a
|
|
1382
|
+
// transparent text-fill) paints its glyphs *with* the element's own
|
|
1383
|
+
// gradient. The `color` value the cascade still reports is never painted,
|
|
1384
|
+
// and the gradient is the fill, not a backdrop — so measuring `color`
|
|
1385
|
+
// against that gradient (which resolveGradientStops picks up as the
|
|
1386
|
+
// element's own background-image) is a guaranteed false positive
|
|
1387
|
+
// (issue #409 Case A). Skip the backdrop-contrast checks; the gradient-text
|
|
1388
|
+
// rule below still flags the pattern itself. Skipping a rule beats a false
|
|
1389
|
+
// positive here — the true painted contrast can't be measured from `color`.
|
|
1390
|
+
const isGradientClippedText = bgClip === 'text';
|
|
1391
|
+
// Run background-dependent checks against either a solid bg or, if the
|
|
1392
|
+
// ancestor is a gradient, against every gradient stop (use the worst case).
|
|
1393
|
+
const bgs = isGradientClippedText
|
|
1394
|
+
? null
|
|
1395
|
+
: (effectiveBg ? [effectiveBg] : (effectiveBgStops && effectiveBgStops.length ? effectiveBgStops : null));
|
|
1396
|
+
if (bgs) {
|
|
1397
|
+
// Gray on colored background — flag if every stop is chromatic
|
|
930
1398
|
const textLum = relativeLuminance(textColor);
|
|
931
1399
|
const isGray = !hasChroma(textColor, 20) && textLum > 0.05 && textLum < 0.85;
|
|
932
1400
|
if (isGray && bgs.every(b => hasChroma(b, 40))) {
|
|
@@ -1438,6 +1906,26 @@ function cssTextHasDarkRootBg(content, customProps) {
|
|
|
1438
1906
|
return false;
|
|
1439
1907
|
}
|
|
1440
1908
|
|
|
1909
|
+
// Best-effort extraction of the CSS selector whose declaration block contains
|
|
1910
|
+
// the given index in raw CSS text. Lets CSS-text findings carry a live-DOM
|
|
1911
|
+
// anchor, so the browser pass can resolve scoped ignores against the actual
|
|
1912
|
+
// element and drop patterns that render nowhere on the page. Returns null for
|
|
1913
|
+
// @-rule preludes, keyframe steps, nested blocks, and anything that does not
|
|
1914
|
+
// read as a selector; those findings stay page-level.
|
|
1915
|
+
function enclosingCssSelector(cssText, index) {
|
|
1916
|
+
if (!cssText || !Number.isFinite(index)) return null;
|
|
1917
|
+
const open = cssText.lastIndexOf('{', index);
|
|
1918
|
+
if (open === -1) return null;
|
|
1919
|
+
const prevClose = Math.max(cssText.lastIndexOf('}', open - 1), cssText.lastIndexOf(';', open - 1));
|
|
1920
|
+
const raw = cssText.slice(prevClose + 1, open).trim().replace(/\s+/g, ' ');
|
|
1921
|
+
if (!raw || raw.startsWith('@') || /^\d/.test(raw) || /[{}<]/.test(raw)) return null;
|
|
1922
|
+
// Keyframe steps: percentage steps fail the digit test above, but `from`
|
|
1923
|
+
// and `to` would read as (never-matching) type selectors and get a valid
|
|
1924
|
+
// finding wrongly dropped by the zero-match rule downstream.
|
|
1925
|
+
if (/^(?:from|to)(?:\s*,\s*(?:from|to))*$/i.test(raw)) return null;
|
|
1926
|
+
return raw;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1441
1929
|
function scanCssTextForGlow(content) {
|
|
1442
1930
|
const customProps = collectCssCustomProps(content);
|
|
1443
1931
|
const hasDarkBg = cssTextHasDarkRootBg(content, customProps);
|
|
@@ -1749,6 +2237,7 @@ function scanCssTextForPseudoStripe(rawContent) {
|
|
|
1749
2237
|
id: 'side-tab',
|
|
1750
2238
|
snippet: `${selector} — absolute ${thicknessPx}px pseudo-element stripe (${edge}: 0)`,
|
|
1751
2239
|
index: selectorStart,
|
|
2240
|
+
selector,
|
|
1752
2241
|
});
|
|
1753
2242
|
}
|
|
1754
2243
|
return findings;
|
|
@@ -1811,6 +2300,7 @@ function scanCssTextForInsetStripe(content) {
|
|
|
1811
2300
|
findings.push({
|
|
1812
2301
|
id: 'side-tab',
|
|
1813
2302
|
snippet: `${selector} — inset box-shadow ${ay === 0 ? ax : ay}px stripe (${edge})`,
|
|
2303
|
+
selector,
|
|
1814
2304
|
});
|
|
1815
2305
|
break;
|
|
1816
2306
|
}
|
|
@@ -1863,10 +2353,12 @@ function collectMarqueeKeyframes(content) {
|
|
|
1863
2353
|
// bound to a keyframe loop that travels a large horizontal distance.
|
|
1864
2354
|
// Rotation/opacity animations never qualify (no X travel); JS-driven
|
|
1865
2355
|
// carousels with user controls have no infinite CSS X-loop to match.
|
|
1866
|
-
|
|
2356
|
+
// `content` is CSS-bearing text; `markup` (defaulting to the same string
|
|
2357
|
+
// for single-corpus callers) is where the <marquee> tag itself lives.
|
|
2358
|
+
function scanCssTextForMarquee(content, markup = content) {
|
|
1867
2359
|
const findings = [];
|
|
1868
|
-
if (/<marquee\b/i.test(
|
|
1869
|
-
findings.push({ id: 'marquee', snippet: '<marquee> element' });
|
|
2360
|
+
if (/<marquee\b/i.test(markup)) {
|
|
2361
|
+
findings.push({ id: 'marquee', snippet: '<marquee> element', selector: 'marquee' });
|
|
1870
2362
|
}
|
|
1871
2363
|
const marqueeKeyframes = collectMarqueeKeyframes(content);
|
|
1872
2364
|
if (marqueeKeyframes.size === 0) return findings;
|
|
@@ -1881,7 +2373,7 @@ function scanCssTextForMarquee(content) {
|
|
|
1881
2373
|
const key = `${selector} ${name}`;
|
|
1882
2374
|
if (seen.has(key)) continue;
|
|
1883
2375
|
seen.add(key);
|
|
1884
|
-
findings.push({ id: 'marquee', snippet: `${selector} — infinite horizontal loop animation "${name}"
|
|
2376
|
+
findings.push({ id: 'marquee', snippet: `${selector} — infinite horizontal loop animation "${name}"`, selector });
|
|
1885
2377
|
}
|
|
1886
2378
|
}
|
|
1887
2379
|
return findings;
|
|
@@ -2044,10 +2536,14 @@ function selectorHitsLandmark(content, selector, ranges) {
|
|
|
2044
2536
|
// element sits inside a header/nav landmark is the hero liveness cliché
|
|
2045
2537
|
// and is promoted to error severity; occurrences elsewhere keep the
|
|
2046
2538
|
// registry default severity.
|
|
2047
|
-
|
|
2539
|
+
//
|
|
2540
|
+
// `content` is CSS-bearing text (rules and keyframes); `markup` — defaulting
|
|
2541
|
+
// to the same string for single-corpus callers like the regex source
|
|
2542
|
+
// engine — is where landmark ranges and Tailwind class attributes live.
|
|
2543
|
+
function scanCssTextForPulsingDot(content, markup = content) {
|
|
2048
2544
|
const customProps = collectCssCustomProps(content);
|
|
2049
2545
|
const keyframes = collectPulseKeyframes(content);
|
|
2050
|
-
const heroRanges = landmarkSourceRanges(
|
|
2546
|
+
const heroRanges = landmarkSourceRanges(markup);
|
|
2051
2547
|
const findings = [];
|
|
2052
2548
|
const seen = new Set();
|
|
2053
2549
|
|
|
@@ -2096,7 +2592,7 @@ function scanCssTextForPulsingDot(content) {
|
|
|
2096
2592
|
|
|
2097
2593
|
if (seen.has(selector)) continue;
|
|
2098
2594
|
seen.add(selector);
|
|
2099
|
-
const inLandmark = selectorHitsLandmark(
|
|
2595
|
+
const inLandmark = selectorHitsLandmark(markup, selector, heroRanges);
|
|
2100
2596
|
findings.push({
|
|
2101
2597
|
id: 'pulsing-dot',
|
|
2102
2598
|
snippet: `${selector} — ${w}x${h}px dot with infinite "${pulseName}" animation${inLandmark ? ' in header/nav' : ''}`,
|
|
@@ -2106,10 +2602,11 @@ function scanCssTextForPulsingDot(content) {
|
|
|
2106
2602
|
}
|
|
2107
2603
|
|
|
2108
2604
|
// Tailwind utilities: animate-ping / animate-pulse on a tiny rounded-full
|
|
2109
|
-
// element declared entirely in the class attribute.
|
|
2605
|
+
// element declared entirely in the class attribute. Scanned in the markup
|
|
2606
|
+
// corpus so the match index lines up with the landmark ranges.
|
|
2110
2607
|
const classRe = /class\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
|
|
2111
2608
|
let cm;
|
|
2112
|
-
while ((cm = classRe.exec(
|
|
2609
|
+
while ((cm = classRe.exec(markup)) !== null) {
|
|
2113
2610
|
const cls = cm[1] || cm[2] || '';
|
|
2114
2611
|
const anim = cls.match(/\banimate-(ping|pulse)\b/);
|
|
2115
2612
|
if (!anim) continue;
|
|
@@ -2190,36 +2687,82 @@ function scanHtmlForShapeAssembledIllustration(html) {
|
|
|
2190
2687
|
return findings;
|
|
2191
2688
|
}
|
|
2192
2689
|
|
|
2690
|
+
// Scoped scan corpora for the page-level pattern checks. CSS-property
|
|
2691
|
+
// regexes run over the whole source string fire on documentation ABOUT
|
|
2692
|
+
// css — `<code>background-clip: text</code>` prose, <pre> samples, HTML
|
|
2693
|
+
// comments — so the checks scan only the strings that actually style the
|
|
2694
|
+
// page:
|
|
2695
|
+
// styleText — <style> block contents plus style="…" attribute values.
|
|
2696
|
+
// Attribute values keep their `style="…"` form so block-scoped
|
|
2697
|
+
// scanners (grid background) keep treating each attribute as one
|
|
2698
|
+
// declaration block, exactly as they did against raw source. Engines
|
|
2699
|
+
// that already read more CSS (linked stylesheets) prepend it.
|
|
2700
|
+
// classText — class attribute values, for utility-class scans.
|
|
2701
|
+
// Markup-shaped checks (inline <svg> scenes, <img> tags, <marquee>,
|
|
2702
|
+
// landmark ranges) and rendered-text checks (theater phrases) keep the
|
|
2703
|
+
// full source. This extraction serves callers without a parsed document
|
|
2704
|
+
// (the browser bundle scanning outerHTML); attribute reads are tag-scoped
|
|
2705
|
+
// so escaped code samples (<div style="…">) never contribute. The
|
|
2706
|
+
// static engine passes richer corpora built from its parsed document.
|
|
2707
|
+
// Bare CSS input (no markup at all) is its own style text, which keeps
|
|
2708
|
+
// direct checkHtmlPatterns(css) callers behaving as before.
|
|
2709
|
+
function buildHtmlPatternCorpora(html) {
|
|
2710
|
+
const source = String(html || '');
|
|
2711
|
+
if (!/<[a-zA-Z!/]/.test(source)) {
|
|
2712
|
+
return { styleText: source, classText: source };
|
|
2713
|
+
}
|
|
2714
|
+
const styleParts = [];
|
|
2715
|
+
const classParts = [];
|
|
2716
|
+
const styleBlockRe = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
|
|
2717
|
+
let m;
|
|
2718
|
+
while ((m = styleBlockRe.exec(source)) !== null) styleParts.push(m[1]);
|
|
2719
|
+
const tagRe = /<[a-zA-Z][^>]*>/g;
|
|
2720
|
+
while ((m = tagRe.exec(source)) !== null) {
|
|
2721
|
+
const tag = m[0];
|
|
2722
|
+
const sm = tag.match(/\bstyle\s*=\s*("[^"]*"|'[^']*')/i);
|
|
2723
|
+
if (sm) styleParts.push(`style=${sm[1]}`);
|
|
2724
|
+
const cm = tag.match(/\bclass\s*=\s*(?:"([^"]*)"|'([^']*)')/i);
|
|
2725
|
+
if (cm) classParts.push(cm[1] ?? cm[2] ?? '');
|
|
2726
|
+
}
|
|
2727
|
+
return { styleText: styleParts.join('\n'), classText: classParts.join('\n') };
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2193
2730
|
/**
|
|
2194
2731
|
* Regex-on-HTML checks shared between browser and Node page-level detection.
|
|
2195
|
-
* These don't need DOM access, just the raw HTML string.
|
|
2732
|
+
* These don't need DOM access, just the raw HTML string. CSS-property and
|
|
2733
|
+
* utility-class patterns scan the scoped corpora (styleText / classText —
|
|
2734
|
+
* see buildHtmlPatternCorpora) so prose about css never flags; only the
|
|
2735
|
+
* markup-shaped and rendered-text checks read the full source.
|
|
2196
2736
|
*/
|
|
2197
|
-
function checkHtmlPatterns(html) {
|
|
2737
|
+
function checkHtmlPatterns(html, corpora) {
|
|
2738
|
+
const { styleText, classText } = corpora || buildHtmlPatternCorpora(html);
|
|
2198
2739
|
const findings = [];
|
|
2199
2740
|
|
|
2200
2741
|
// --- Color ---
|
|
2201
2742
|
|
|
2202
2743
|
// AI color palette: purple/violet
|
|
2203
2744
|
const purpleHexRe = /#(?:7c3aed|8b5cf6|a855f7|9333ea|7e22ce|6d28d9|6366f1|764ba2|667eea)\b/gi;
|
|
2204
|
-
if (purpleHexRe.test(
|
|
2745
|
+
if (purpleHexRe.test(styleText)) {
|
|
2205
2746
|
const purpleTextRe = /(?:(?:^|;)\s*color\s*:\s*(?:.*?)(?:#(?:7c3aed|8b5cf6|a855f7|9333ea|7e22ce|6d28d9))|gradient.*?#(?:7c3aed|8b5cf6|a855f7|764ba2|667eea))/gi;
|
|
2206
|
-
|
|
2207
|
-
|
|
2747
|
+
purpleTextRe.lastIndex = 0;
|
|
2748
|
+
const purpleMatch = purpleTextRe.exec(styleText);
|
|
2749
|
+
if (purpleMatch) {
|
|
2750
|
+
findings.push({ id: 'ai-color-palette', snippet: 'Purple/violet accent colors detected', selector: enclosingCssSelector(styleText, purpleMatch.index + 1) || undefined });
|
|
2208
2751
|
}
|
|
2209
2752
|
}
|
|
2210
2753
|
|
|
2211
2754
|
// Gradient text (background-clip: text + gradient)
|
|
2212
2755
|
const gradientRe = /(?:-webkit-)?background-clip\s*:\s*text/gi;
|
|
2213
2756
|
let gm;
|
|
2214
|
-
while ((gm = gradientRe.exec(
|
|
2757
|
+
while ((gm = gradientRe.exec(styleText)) !== null) {
|
|
2215
2758
|
const start = Math.max(0, gm.index - 200);
|
|
2216
|
-
const context =
|
|
2759
|
+
const context = styleText.substring(start, gm.index + gm[0].length + 200);
|
|
2217
2760
|
if (/gradient/i.test(context)) {
|
|
2218
|
-
findings.push({ id: 'gradient-text', snippet: 'background-clip: text + gradient' });
|
|
2761
|
+
findings.push({ id: 'gradient-text', snippet: 'background-clip: text + gradient', selector: enclosingCssSelector(styleText, gm.index) || undefined });
|
|
2219
2762
|
break;
|
|
2220
2763
|
}
|
|
2221
2764
|
}
|
|
2222
|
-
if (/\bbg-clip-text\b/.test(
|
|
2765
|
+
if (/\bbg-clip-text\b/.test(classText) && /\bbg-gradient-to-/.test(classText)) {
|
|
2223
2766
|
findings.push({ id: 'gradient-text', snippet: 'bg-clip-text + bg-gradient (Tailwind)' });
|
|
2224
2767
|
}
|
|
2225
2768
|
|
|
@@ -2228,10 +2771,10 @@ function checkHtmlPatterns(html) {
|
|
|
2228
2771
|
// Side-tab accent stripe drawn as an absolutely-positioned pseudo-element
|
|
2229
2772
|
// (no border property involved, so the element-level border checks and
|
|
2230
2773
|
// the border-left regexes never see it).
|
|
2231
|
-
findings.push(...scanCssTextForPseudoStripe(
|
|
2774
|
+
findings.push(...scanCssTextForPseudoStripe(styleText));
|
|
2232
2775
|
|
|
2233
2776
|
// Side-tab accent stripe drawn as a single-edge inset box-shadow.
|
|
2234
|
-
findings.push(...scanCssTextForInsetStripe(
|
|
2777
|
+
findings.push(...scanCssTextForInsetStripe(styleText));
|
|
2235
2778
|
|
|
2236
2779
|
// --- Layout ---
|
|
2237
2780
|
|
|
@@ -2239,20 +2782,20 @@ function checkHtmlPatterns(html) {
|
|
|
2239
2782
|
const spacingValues = [];
|
|
2240
2783
|
const spacingRe = /(?:padding|margin)(?:-(?:top|right|bottom|left))?\s*:\s*(\d+)px/gi;
|
|
2241
2784
|
let sm;
|
|
2242
|
-
while ((sm = spacingRe.exec(
|
|
2785
|
+
while ((sm = spacingRe.exec(styleText)) !== null) {
|
|
2243
2786
|
const v = parseInt(sm[1], 10);
|
|
2244
2787
|
if (v > 0 && v < 200) spacingValues.push(v);
|
|
2245
2788
|
}
|
|
2246
2789
|
const gapRe = /gap\s*:\s*(\d+)px/gi;
|
|
2247
|
-
while ((sm = gapRe.exec(
|
|
2790
|
+
while ((sm = gapRe.exec(styleText)) !== null) {
|
|
2248
2791
|
spacingValues.push(parseInt(sm[1], 10));
|
|
2249
2792
|
}
|
|
2250
2793
|
const twSpaceRe = /\b(?:p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|gap)-(\d+)\b/g;
|
|
2251
|
-
while ((sm = twSpaceRe.exec(
|
|
2794
|
+
while ((sm = twSpaceRe.exec(classText)) !== null) {
|
|
2252
2795
|
spacingValues.push(parseInt(sm[1], 10) * 4);
|
|
2253
2796
|
}
|
|
2254
2797
|
const remSpacingRe = /(?:padding|margin)(?:-(?:top|right|bottom|left))?\s*:\s*([\d.]+)rem/gi;
|
|
2255
|
-
while ((sm = remSpacingRe.exec(
|
|
2798
|
+
while ((sm = remSpacingRe.exec(styleText)) !== null) {
|
|
2256
2799
|
const v = Math.round(parseFloat(sm[1]) * 16);
|
|
2257
2800
|
if (v > 0 && v < 200) spacingValues.push(v);
|
|
2258
2801
|
}
|
|
@@ -2276,21 +2819,21 @@ function checkHtmlPatterns(html) {
|
|
|
2276
2819
|
|
|
2277
2820
|
// Bounce/elastic animation names
|
|
2278
2821
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
|
2279
|
-
const bounceMatch = bounceRe.exec(
|
|
2822
|
+
const bounceMatch = bounceRe.exec(styleText);
|
|
2280
2823
|
if (bounceMatch) {
|
|
2281
2824
|
const animationToken = bounceMatch[1]
|
|
2282
2825
|
.split(/[,\s]+/)
|
|
2283
2826
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
|
2284
|
-
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}
|
|
2827
|
+
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}`, selector: enclosingCssSelector(styleText, bounceMatch.index) || undefined });
|
|
2285
2828
|
}
|
|
2286
2829
|
|
|
2287
2830
|
// Overshoot cubic-bezier
|
|
2288
2831
|
const bezierRe = /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g;
|
|
2289
2832
|
let bm;
|
|
2290
|
-
while ((bm = bezierRe.exec(
|
|
2833
|
+
while ((bm = bezierRe.exec(styleText)) !== null) {
|
|
2291
2834
|
const y1 = parseFloat(bm[2]), y2 = parseFloat(bm[4]);
|
|
2292
2835
|
if (y1 < -0.1 || y1 > 1.1 || y2 < -0.1 || y2 > 1.1) {
|
|
2293
|
-
findings.push({ id: 'bounce-easing', snippet: `cubic-bezier(${bm[1]}, ${bm[2]}, ${bm[3]}, ${bm[4]})
|
|
2836
|
+
findings.push({ id: 'bounce-easing', snippet: `cubic-bezier(${bm[1]}, ${bm[2]}, ${bm[3]}, ${bm[4]})`, selector: enclosingCssSelector(styleText, bm.index) || undefined });
|
|
2294
2837
|
break;
|
|
2295
2838
|
}
|
|
2296
2839
|
}
|
|
@@ -2298,7 +2841,7 @@ function checkHtmlPatterns(html) {
|
|
|
2298
2841
|
// Layout property transitions
|
|
2299
2842
|
const transRe = /transition(?:-property)?\s*:\s*([^;{}]+)/gi;
|
|
2300
2843
|
let tm;
|
|
2301
|
-
while ((tm = transRe.exec(
|
|
2844
|
+
while ((tm = transRe.exec(styleText)) !== null) {
|
|
2302
2845
|
const val = tm[1].toLowerCase();
|
|
2303
2846
|
if (/\ball\b/.test(val)) continue;
|
|
2304
2847
|
const found = val.match(/\b(?:(?:max|min)-)?(?:width|height)\b|\bpadding(?:-(?:top|right|bottom|left))?\b|\bmargin(?:-(?:top|right|bottom|left))?\b/gi);
|
|
@@ -2308,31 +2851,36 @@ function checkHtmlPatterns(html) {
|
|
|
2308
2851
|
}
|
|
2309
2852
|
}
|
|
2310
2853
|
|
|
2311
|
-
// Pulsing status dots (tiny circular elements on infinite pulse animations)
|
|
2312
|
-
|
|
2854
|
+
// Pulsing status dots (tiny circular elements on infinite pulse animations).
|
|
2855
|
+
// The CSS rules come from styleText; the markup carries the landmark
|
|
2856
|
+
// ranges and Tailwind class attributes.
|
|
2857
|
+
findings.push(...scanCssTextForPulsingDot(styleText, html));
|
|
2313
2858
|
|
|
2314
2859
|
// Shape-assembled illustrations (large pictorial SVGs built from primitives)
|
|
2315
2860
|
findings.push(...scanHtmlForShapeAssembledIllustration(html));
|
|
2316
2861
|
|
|
2317
2862
|
// Auto-scrolling marquees (<marquee> or infinite horizontal loop animations)
|
|
2318
|
-
findings.push(...scanCssTextForMarquee(html));
|
|
2863
|
+
findings.push(...scanCssTextForMarquee(styleText, html));
|
|
2319
2864
|
|
|
2320
2865
|
// --- Dark glow / chromatic halo shadows ---
|
|
2321
2866
|
|
|
2322
|
-
const glowHits = scanCssTextForGlow(
|
|
2867
|
+
const glowHits = scanCssTextForGlow(styleText);
|
|
2323
2868
|
if (glowHits.length > 0) {
|
|
2324
|
-
findings.push({ id: 'dark-glow', snippet: glowHits[0].snippet });
|
|
2869
|
+
findings.push({ id: 'dark-glow', snippet: glowHits[0].snippet, selector: enclosingCssSelector(styleText, glowHits[0].index) || undefined });
|
|
2325
2870
|
}
|
|
2326
2871
|
|
|
2327
2872
|
// Radial-gradient background halo (gradient-drawn sibling of dark-glow)
|
|
2328
|
-
const haloHits = scanCssTextForRadialHalo(
|
|
2873
|
+
const haloHits = scanCssTextForRadialHalo(styleText);
|
|
2329
2874
|
if (haloHits.length > 0) {
|
|
2330
|
-
findings.push({ id: 'radial-halo', snippet: haloHits[0].snippet });
|
|
2875
|
+
findings.push({ id: 'radial-halo', snippet: haloHits[0].snippet, selector: enclosingCssSelector(styleText, haloHits[0].index) || undefined });
|
|
2331
2876
|
}
|
|
2332
2877
|
|
|
2333
2878
|
// --- Generated-UI tells: repeating-gradient stripes ---
|
|
2334
|
-
|
|
2335
|
-
|
|
2879
|
+
{
|
|
2880
|
+
const stripesMatch = /repeating-(?:linear|radial|conic)-gradient\s*\(/i.exec(styleText);
|
|
2881
|
+
if (stripesMatch) {
|
|
2882
|
+
findings.push({ id: 'repeating-stripes-gradient', snippet: 'repeating-gradient decorative stripes', selector: enclosingCssSelector(styleText, stripesMatch.index) || undefined });
|
|
2883
|
+
}
|
|
2336
2884
|
}
|
|
2337
2885
|
|
|
2338
2886
|
// --- Generated-UI tells: two-axis grid-line background ---
|
|
@@ -2348,9 +2896,9 @@ function checkHtmlPatterns(html) {
|
|
|
2348
2896
|
// in for the second axis. Colors like `oklch(96% 0.012 82 / 0.055)` carry
|
|
2349
2897
|
// nested parens, so match the hairline stop directly rather than parsing
|
|
2350
2898
|
// whole gradient layers.
|
|
2351
|
-
const gridHits = scanCssTextForGridBackground(
|
|
2899
|
+
const gridHits = scanCssTextForGridBackground(styleText);
|
|
2352
2900
|
if (gridHits.length > 0) {
|
|
2353
|
-
findings.push({ id: 'codex-grid-background', snippet: gridHits[0].snippet });
|
|
2901
|
+
findings.push({ id: 'codex-grid-background', snippet: gridHits[0].snippet, selector: enclosingCssSelector(styleText, gridHits[0].index) || undefined });
|
|
2354
2902
|
}
|
|
2355
2903
|
|
|
2356
2904
|
function stripHtmlBlocks(content, tagNames) {
|
|
@@ -2386,8 +2934,11 @@ function checkHtmlPatterns(html) {
|
|
|
2386
2934
|
// hover:rotate / hover:translate utility on an <img>. Each distinct
|
|
2387
2935
|
// mechanism is its own finding.
|
|
2388
2936
|
const imgHoverCss = /\bimg\b[^,{}]*:hover\b[^{}]*\{[^}]*\btransform\s*:\s*(?:scale|rotate|translate|matrix|skew)/i;
|
|
2389
|
-
|
|
2390
|
-
|
|
2937
|
+
{
|
|
2938
|
+
const imgHoverMatch = imgHoverCss.exec(styleText);
|
|
2939
|
+
if (imgHoverMatch) {
|
|
2940
|
+
findings.push({ id: 'image-hover-transform', snippet: 'img:hover { transform } rule', selector: enclosingCssSelector(styleText, imgHoverMatch.index + imgHoverMatch[0].indexOf('{') + 1) || undefined });
|
|
2941
|
+
}
|
|
2391
2942
|
}
|
|
2392
2943
|
const imgTagRe = /<img\b[^>]*\bclass\s*=\s*"([^"]*)"/gi;
|
|
2393
2944
|
let im;
|
|
@@ -2434,7 +2985,46 @@ function readOwnBackgroundColor(el, computedStyle) {
|
|
|
2434
2985
|
return bg;
|
|
2435
2986
|
}
|
|
2436
2987
|
|
|
2437
|
-
|
|
2988
|
+
// One element's background-color as the cascade walk sees it: computed style
|
|
2989
|
+
// first (with the modern-color fallback), then, in static mode only,
|
|
2990
|
+
// custom-prop resolution and the inline-shorthand peek. Shared by
|
|
2991
|
+
// resolveBackgroundInfo and resolveGradientStops so both walks read the same
|
|
2992
|
+
// surfaces.
|
|
2993
|
+
function readCascadeBackgroundColor(current, style, customPropMap) {
|
|
2994
|
+
let bg = parseRgb(style.backgroundColor) || parseAnyColor(style.backgroundColor);
|
|
2995
|
+
if (!DETECTOR_IS_BROWSER && (!bg || bg.a < 0.1)) {
|
|
2996
|
+
// The static engine can return literal "var(--X)" / "oklch(...)" strings.
|
|
2997
|
+
// Resolve through customPropMap so Tailwind v4 color tokens become RGB.
|
|
2998
|
+
if (customPropMap) {
|
|
2999
|
+
bg = parseColorResolved(style.backgroundColor, customPropMap);
|
|
3000
|
+
}
|
|
3001
|
+
if (!bg || bg.a < 0.1) {
|
|
3002
|
+
// Inline-style fallback for colors the static cascade did not surface
|
|
3003
|
+
// on backgroundColor.
|
|
3004
|
+
const rawStyle = current.getAttribute?.('style') || '';
|
|
3005
|
+
const bgMatch = rawStyle.match(/background(?:-color)?\s*:\s*([^;]+)/i);
|
|
3006
|
+
const inlineBg = bgMatch ? bgMatch[1].trim() : '';
|
|
3007
|
+
if (inlineBg && !/gradient/i.test(inlineBg) && !/url\s*\(/i.test(inlineBg)) {
|
|
3008
|
+
bg = parseColorResolved(inlineBg, customPropMap) || parseAnyColor(inlineBg);
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
return bg;
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
// Walk up for the surface the element's text is painted on.
|
|
3016
|
+
//
|
|
3017
|
+
// Returns { color, unresolved }:
|
|
3018
|
+
// • color set — the effective surface, overlays composited in.
|
|
3019
|
+
// • unresolved: true — a layer on the way up paints a color this parser
|
|
3020
|
+
// cannot read, so the surface is unknown. Callers
|
|
3021
|
+
// must SKIP their contrast checks. Guessing white
|
|
3022
|
+
// here is what flooded dark themes with false
|
|
3023
|
+
// "on #ffffff" findings: one abstention costs a
|
|
3024
|
+
// single finding, one wrong guess costs a hundred.
|
|
3025
|
+
// • both null/false — no solid color, but a gradient or image is in
|
|
3026
|
+
// play; callers fall back to its color stops.
|
|
3027
|
+
function resolveBackgroundInfo(el, win, customPropMap) {
|
|
2438
3028
|
let current = el;
|
|
2439
3029
|
// Translucent layers (0.1 < a < 1) found on the way down to an opaque
|
|
2440
3030
|
// base. A browser composites these over the base; the old behavior
|
|
@@ -2462,67 +3052,114 @@ function resolveBackground(el, win, customPropMap) {
|
|
|
2462
3052
|
// body backgrounds.
|
|
2463
3053
|
// Real browsers serialize wide-gamut computed values as oklab()/oklch()
|
|
2464
3054
|
// (e.g. any color-mix() result), which plain parseRgb misses.
|
|
2465
|
-
let bg =
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
bg = parseColorResolved(inlineBg, customPropMap) || parseAnyColor(inlineBg);
|
|
2480
|
-
}
|
|
2481
|
-
}
|
|
3055
|
+
let bg = readCascadeBackgroundColor(current, style, customPropMap);
|
|
3056
|
+
|
|
3057
|
+
// `background-color: currentcolor` paints with the element's own text
|
|
3058
|
+
// color — real paint whose value we know. Real browsers resolve the
|
|
3059
|
+
// keyword before getComputedStyle output; jsdom hands it through
|
|
3060
|
+
// verbatim, and without this substitution the layer would read as
|
|
3061
|
+
// unparseable and force a needless abstention.
|
|
3062
|
+
if ((!bg || bg.a < 0.1) && /^currentcolor$/i.test(String(style.backgroundColor || '').trim())) {
|
|
3063
|
+
// The static cascade resolves var() text tokens before checks run, so
|
|
3064
|
+
// style.color is normally already an rgb string here; parseColorResolved
|
|
3065
|
+
// is defense in depth for any future caller that passes a live
|
|
3066
|
+
// customPropMap (it matches the text-color path in checkElementColors
|
|
3067
|
+
// and reduces to parseAnyColor when the map is null or absent).
|
|
3068
|
+
bg = parseRgb(style.color) || parseColorResolved(style.color, customPropMap);
|
|
2482
3069
|
}
|
|
2483
3070
|
|
|
2484
3071
|
if (bg && bg.a > 0.1) {
|
|
2485
|
-
if (bg.a >= 0.99) return flatten(bg);
|
|
3072
|
+
if (bg.a >= 0.99) return { color: flatten(bg), unresolved: false };
|
|
2486
3073
|
overlays.push(bg);
|
|
2487
|
-
}
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
//
|
|
2496
|
-
//
|
|
2497
|
-
//
|
|
2498
|
-
//
|
|
2499
|
-
//
|
|
3074
|
+
} else if (!bg && !isNoPaintColorValue(style.backgroundColor)) {
|
|
3075
|
+
// This layer names a color we could not parse (a color space we do not
|
|
3076
|
+
// model, an unresolved var(), a syntax newer than the parser). It may
|
|
3077
|
+
// well be opaque, which would make every ancestor below it invisible —
|
|
3078
|
+
// so the surface is unknown and the walk stops here rather than
|
|
3079
|
+
// reporting an ancestor the visitor never sees.
|
|
3080
|
+
return { color: null, unresolved: true };
|
|
3081
|
+
}
|
|
3082
|
+
// No solid bg-color at this level, but this level paints an image. CSS
|
|
3083
|
+
// stacks background-image layers first-on-top, so which layer leads
|
|
3084
|
+
// decides what the visitor sees:
|
|
3085
|
+
// • gradient on top — the gradient is the surface. Hand the caller a
|
|
3086
|
+
// null color so it falls back to the gradient's own stops (body
|
|
3087
|
+
// grounds, gradient buttons, hero sections).
|
|
3088
|
+
// • url() on top — the surface is an image whose pixels this engine
|
|
3089
|
+
// cannot read, and it may fully cover every layer and ancestor
|
|
3090
|
+
// beneath it. Same contract as an unparseable color: abstain, so
|
|
3091
|
+
// the gradient-stop fallback never measures a gradient the image
|
|
3092
|
+
// hides (the shipped miss: `url(photo), linear-gradient(...)`
|
|
3093
|
+
// reported low-contrast against the invisible gradient's stops).
|
|
2500
3094
|
if (hasGradientOrUrl) {
|
|
2501
|
-
|
|
2502
|
-
|
|
3095
|
+
const layers = splitTopLevelCommas(bgImage);
|
|
3096
|
+
const topPaintLayer = layers.find(
|
|
3097
|
+
(layer) => /gradient\s*\(/i.test(layer) || /url\s*\(/i.test(layer),
|
|
3098
|
+
);
|
|
3099
|
+
const gradientOnTop = !!topPaintLayer
|
|
3100
|
+
&& /gradient\s*\(/i.test(topPaintLayer)
|
|
3101
|
+
&& !/^\s*url\s*\(/i.test(topPaintLayer);
|
|
3102
|
+
if (!gradientOnTop) return { color: null, unresolved: true };
|
|
3103
|
+
// Gradient on top of a url() layer: the image shows through wherever
|
|
3104
|
+
// the gradient is not fully opaque, so a translucent wash like
|
|
3105
|
+
// `linear-gradient(rgba(0,0,0,.2), rgba(0,0,0,.2)), url(photo)` paints
|
|
3106
|
+
// a blend with pixels this engine cannot read. Only a gradient whose
|
|
3107
|
+
// every readable stop is opaque provably covers the image; otherwise
|
|
3108
|
+
// the surface is unknown — abstain rather than hand callers gradient
|
|
3109
|
+
// stops (or a stop average) the visitor never sees unmixed.
|
|
3110
|
+
const urlBeneath = layers.some(
|
|
3111
|
+
(layer) => layer !== topPaintLayer && /url\s*\(/i.test(layer),
|
|
3112
|
+
);
|
|
3113
|
+
if (urlBeneath) {
|
|
3114
|
+
const topStops = parseGradientColors(topPaintLayer);
|
|
3115
|
+
const provablyOpaque = topStops.length > 0 && topStops.every((s) => (s.a ?? 1) >= 0.99);
|
|
3116
|
+
if (!provablyOpaque) return { color: null, unresolved: true };
|
|
2503
3117
|
}
|
|
2504
|
-
return null;
|
|
3118
|
+
return { color: null, unresolved: false };
|
|
2505
3119
|
}
|
|
2506
3120
|
current = current.parentElement;
|
|
2507
3121
|
}
|
|
2508
|
-
|
|
3122
|
+
// Every layer up to the document root was genuinely see-through, so the
|
|
3123
|
+
// browser paints its default canvas. This is the ONLY case that earns the
|
|
3124
|
+
// white assumption.
|
|
3125
|
+
return { color: flatten({ r: 255, g: 255, b: 255, a: 1 }), unresolved: false };
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
function resolveBackground(el, win, customPropMap) {
|
|
3129
|
+
return resolveBackgroundInfo(el, win, customPropMap).color;
|
|
2509
3130
|
}
|
|
2510
3131
|
|
|
2511
3132
|
// Walk parents looking for a gradient background and return its color stops.
|
|
2512
3133
|
// Used as a fallback when resolveBackground() returns null because the
|
|
2513
3134
|
// effective background is a gradient (no single solid color to compare against).
|
|
3135
|
+
// Translucent solid layers found between the element and the gradient (frosted
|
|
3136
|
+
// panels, glass washes) are composited over every stop, the same way
|
|
3137
|
+
// resolveBackground flattens them over a solid base — raw stops alone would
|
|
3138
|
+
// false-flag dark text on a light frosted wash over a dark gradient, and miss
|
|
3139
|
+
// the inverse.
|
|
2514
3140
|
function resolveGradientStops(el, win, customPropMap) {
|
|
2515
3141
|
let current = el;
|
|
3142
|
+
const overlays = [];
|
|
2516
3143
|
while (current && current.nodeType === 1) {
|
|
2517
3144
|
const style = DETECTOR_IS_BROWSER ? getComputedStyle(current) : win.getComputedStyle(current);
|
|
2518
3145
|
const bgImage = style.backgroundImage || '';
|
|
3146
|
+
// A url() layer anywhere in the stack — alone, or alongside a gradient in
|
|
3147
|
+
// the same declaration (a translucent wash over a texture photo) — paints
|
|
3148
|
+
// pixels the analytic walk cannot know. Measuring the gradient stops over
|
|
3149
|
+
// the wrong base flagged dark ink sitting on a bright gold-leaf image at
|
|
3150
|
+
// 2.6:1; skipping beats a wrong ratio, and the screenshot subsystem owns
|
|
3151
|
+
// image-backed text.
|
|
3152
|
+
if (bgImage && bgImage !== 'none' && /url\s*\(/i.test(bgImage)) return null;
|
|
2519
3153
|
let stops = null;
|
|
2520
3154
|
if (bgImage && bgImage !== 'none' && /gradient/i.test(bgImage)) {
|
|
3155
|
+
// parseGradientColors (shared) reads modern-space stops too — oklch,
|
|
3156
|
+
// color-mix and friends via balanced-paren token capture — so browser
|
|
3157
|
+
// computed values that keep the authored syntax stay measurable.
|
|
2521
3158
|
const parsed = parseGradientColors(bgImage);
|
|
2522
3159
|
if (parsed.length > 0) stops = parsed;
|
|
2523
3160
|
}
|
|
2524
3161
|
if (!stops && !DETECTOR_IS_BROWSER) {
|
|
2525
|
-
//
|
|
3162
|
+
// Static mode: peek at the raw inline style for gradients the cascade did not surface
|
|
2526
3163
|
const rawStyle = current.getAttribute?.('style') || '';
|
|
2527
3164
|
const bgMatch = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i);
|
|
2528
3165
|
if (bgMatch && /gradient/i.test(bgMatch[1])) {
|
|
@@ -2530,7 +3167,23 @@ function resolveGradientStops(el, win, customPropMap) {
|
|
|
2530
3167
|
if (parsed.length > 0) stops = parsed;
|
|
2531
3168
|
}
|
|
2532
3169
|
}
|
|
2533
|
-
if (stops)
|
|
3170
|
+
if (stops) {
|
|
3171
|
+
const composited = compositeGradientStops(stops, current, win, customPropMap);
|
|
3172
|
+
if (!composited || overlays.length === 0) return composited;
|
|
3173
|
+
return composited.map(stop => {
|
|
3174
|
+
let acc = stop;
|
|
3175
|
+
for (let i = overlays.length - 1; i >= 0; i--) acc = compositeColorOver(overlays[i], acc);
|
|
3176
|
+
return acc;
|
|
3177
|
+
});
|
|
3178
|
+
}
|
|
3179
|
+
const bg = readCascadeBackgroundColor(current, style, customPropMap);
|
|
3180
|
+
if (bg && bg.a > 0.1) {
|
|
3181
|
+
// An opaque surface above the gradient means the gradient never shows
|
|
3182
|
+
// through here; resolveBackground would have returned it, so reaching
|
|
3183
|
+
// this is defensive — bail rather than measure the wrong layer.
|
|
3184
|
+
if (bg.a >= 0.99) return null;
|
|
3185
|
+
overlays.push(bg);
|
|
3186
|
+
}
|
|
2534
3187
|
current = current.parentElement;
|
|
2535
3188
|
}
|
|
2536
3189
|
return null;
|
|
@@ -2714,491 +3367,224 @@ function checkElementPseudoStripeDOM(el) {
|
|
|
2714
3367
|
|
|
2715
3368
|
findings.push({
|
|
2716
3369
|
id: 'side-tab',
|
|
2717
|
-
snippet: `${classSelector(el)}${which} — absolute ${thickness}px pseudo-element stripe (${edge})`,
|
|
2718
|
-
});
|
|
2719
|
-
}
|
|
2720
|
-
return findings;
|
|
2721
|
-
}
|
|
2722
|
-
|
|
2723
|
-
// Full-cover surface pseudo (browser): a ::before/::after positioned
|
|
2724
|
-
// absolute/fixed whose box covers (nearly) the whole host and carries an
|
|
2725
|
-
// opaque background. That pseudo is the element's visible surface even
|
|
2726
|
-
// though the element's own background-color reads transparent — the nav-CTA
|
|
2727
|
-
// construction that otherwise escapes every own-background contrast gate.
|
|
2728
|
-
function readPseudoSurfaceDOM(el, rect) {
|
|
2729
|
-
for (const which of ['::before', '::after']) {
|
|
2730
|
-
let ps;
|
|
2731
|
-
try { ps = getComputedStyle(el, which); } catch { continue; }
|
|
2732
|
-
if (!ps || ps.content === 'none' || ps.content === '') continue;
|
|
2733
|
-
if (ps.position !== 'absolute' && ps.position !== 'fixed') continue;
|
|
2734
|
-
if (ps.display === 'none' || (parseFloat(ps.opacity) || 1) < 0.9) continue;
|
|
2735
|
-
const w = parseFloat(ps.width) || 0;
|
|
2736
|
-
const h = parseFloat(ps.height) || 0;
|
|
2737
|
-
if (w < rect.width - 4 || h < rect.height - 4) continue;
|
|
2738
|
-
const bg = parseRgb(ps.backgroundColor) || parseAnyColor(ps.backgroundColor);
|
|
2739
|
-
if (!bg || (bg.a ?? 1) < 0.9) continue;
|
|
2740
|
-
return bg;
|
|
2741
|
-
}
|
|
2742
|
-
return null;
|
|
2743
|
-
}
|
|
2744
|
-
|
|
2745
|
-
function checkElementColorsDOM(el) {
|
|
2746
|
-
const tag = el.tagName.toLowerCase();
|
|
2747
|
-
// No early SAFE_TAGS bail here — checkColors() does its own gating that
|
|
2748
|
-
// includes the styled-button exception for <a> / <button> with their own
|
|
2749
|
-
// opaque background. Bailing here would prevent that exception from firing.
|
|
2750
|
-
const rect = el.getBoundingClientRect();
|
|
2751
|
-
if (rect.width < 10 || rect.height < 10) return [];
|
|
2752
|
-
const style = getComputedStyle(el);
|
|
2753
|
-
const directText = [...el.childNodes].filter(n => n.nodeType === 3).map(n => n.textContent).join('');
|
|
2754
|
-
const hasDirectText = directText.trim().length > 0;
|
|
2755
|
-
let effectiveBg = resolveBackground(el);
|
|
2756
|
-
let ownBg = readOwnBackgroundColor(el, style);
|
|
2757
|
-
if (!ownBg || (ownBg.a ?? 1) <= 0.5) {
|
|
2758
|
-
const pseudoSurface = readPseudoSurfaceDOM(el, rect);
|
|
2759
|
-
if (pseudoSurface) {
|
|
2760
|
-
ownBg = pseudoSurface;
|
|
2761
|
-
effectiveBg = pseudoSurface;
|
|
2762
|
-
}
|
|
2763
|
-
}
|
|
2764
|
-
return checkColors({
|
|
2765
|
-
tag,
|
|
2766
|
-
// Chrome serializes computed colors specified in modern spaces as
|
|
2767
|
-
// oklch()/oklab() strings; without the parseAnyColor fallback the text
|
|
2768
|
-
// color comes back null and the low-contrast / gray-on-color checks
|
|
2769
|
-
// silently never run (the shipped miss: a nav CTA whose text color was
|
|
2770
|
-
// an oklch token near its own oklch background).
|
|
2771
|
-
textColor: parseRgb(style.color) || parseAnyColor(style.color),
|
|
2772
|
-
bgColor: ownBg,
|
|
2773
|
-
effectiveBg,
|
|
2774
|
-
effectiveBgStops: effectiveBg ? null : resolveGradientStops(el),
|
|
2775
|
-
fontSize: parseFloat(style.fontSize) || 16,
|
|
2776
|
-
fontWeight: parseInt(style.fontWeight) || 400,
|
|
2777
|
-
hasDirectText,
|
|
2778
|
-
isEmojiOnly: isEmojiOnlyText(directText),
|
|
2779
|
-
bgClip: style.webkitBackgroundClip || style.backgroundClip || '',
|
|
2780
|
-
bgImage: style.backgroundImage || '',
|
|
2781
|
-
classList: el.getAttribute('class') || '',
|
|
2782
|
-
});
|
|
2783
|
-
}
|
|
2784
|
-
|
|
2785
|
-
function checkElementIconTileDOM(el) {
|
|
2786
|
-
const tag = el.tagName.toLowerCase();
|
|
2787
|
-
if (!HEADING_TAGS.has(tag)) return [];
|
|
2788
|
-
const sibling = el.previousElementSibling;
|
|
2789
|
-
if (!sibling) return [];
|
|
2790
|
-
|
|
2791
|
-
const sibRect = sibling.getBoundingClientRect();
|
|
2792
|
-
const headRect = el.getBoundingClientRect();
|
|
2793
|
-
const sibStyle = getComputedStyle(sibling);
|
|
2794
|
-
|
|
2795
|
-
// The tile may either contain an <svg>/<i> icon child, OR the tile itself
|
|
2796
|
-
// may contain an emoji/symbol character directly as its only text content
|
|
2797
|
-
// (the "card-icon" pattern from many AI-generated demos).
|
|
2798
|
-
const iconChild = sibling.querySelector('svg, i[data-lucide], i[class*="fa-"], i[class*="icon"]');
|
|
2799
|
-
const iconRect = iconChild?.getBoundingClientRect();
|
|
2800
|
-
const sibDirectText = [...sibling.childNodes].filter(n => n.nodeType === 3).map(n => n.textContent).join('');
|
|
2801
|
-
const hasInlineEmojiIcon = sibling.children.length === 0 && isEmojiOnlyText(sibDirectText);
|
|
2802
|
-
|
|
2803
|
-
return checkIconTile({
|
|
2804
|
-
headingTag: tag,
|
|
2805
|
-
headingText: el.textContent || '',
|
|
2806
|
-
headingTop: headRect.top,
|
|
2807
|
-
siblingTag: sibling.tagName.toLowerCase(),
|
|
2808
|
-
siblingWidth: sibRect.width,
|
|
2809
|
-
siblingHeight: sibRect.height,
|
|
2810
|
-
siblingBottom: sibRect.bottom,
|
|
2811
|
-
siblingBgColor: parseRgb(sibStyle.backgroundColor),
|
|
2812
|
-
siblingBgImage: sibStyle.backgroundImage || '',
|
|
2813
|
-
siblingBorderWidth: parseFloat(sibStyle.borderTopWidth) || 0,
|
|
2814
|
-
siblingBorderRadius: parseFloat(sibStyle.borderRadius) || 0,
|
|
2815
|
-
hasIconChild: !!iconChild || hasInlineEmojiIcon,
|
|
2816
|
-
iconChildWidth: iconRect?.width || 0,
|
|
2817
|
-
});
|
|
2818
|
-
}
|
|
2819
|
-
|
|
2820
|
-
function checkElementItalicSerifDOM(el) {
|
|
2821
|
-
const tag = el.tagName.toLowerCase();
|
|
2822
|
-
if (tag !== 'h1' && tag !== 'h2') return [];
|
|
2823
|
-
const style = getComputedStyle(el);
|
|
2824
|
-
return checkItalicSerif({
|
|
2825
|
-
tag,
|
|
2826
|
-
fontStyle: style.fontStyle || '',
|
|
2827
|
-
fontFamily: style.fontFamily || '',
|
|
2828
|
-
fontSize: parseFloat(style.fontSize) || 0,
|
|
2829
|
-
headingText: el.textContent || '',
|
|
2830
|
-
});
|
|
3370
|
+
snippet: `${classSelector(el)}${which} — absolute ${thickness}px pseudo-element stripe (${edge})`,
|
|
3371
|
+
});
|
|
3372
|
+
}
|
|
3373
|
+
return findings;
|
|
2831
3374
|
}
|
|
2832
3375
|
|
|
2833
|
-
|
|
3376
|
+
// Full-cover surface pseudo (browser): a ::before/::after positioned
|
|
3377
|
+
// absolute/fixed whose box covers (nearly) the whole host and carries an
|
|
3378
|
+
// opaque background. That pseudo is the element's visible surface even
|
|
3379
|
+
// though the element's own background-color reads transparent — the nav-CTA
|
|
3380
|
+
// construction that otherwise escapes every own-background contrast gate.
|
|
3381
|
+
function readPseudoSurfaceDOM(el, rect) {
|
|
2834
3382
|
for (const which of ['::before', '::after']) {
|
|
2835
3383
|
let ps;
|
|
2836
3384
|
try { ps = getComputedStyle(el, which); } catch { continue; }
|
|
2837
3385
|
if (!ps || ps.content === 'none' || ps.content === '') continue;
|
|
3386
|
+
if (ps.position !== 'absolute' && ps.position !== 'fixed') continue;
|
|
3387
|
+
if (ps.display === 'none' || (parseFloat(ps.opacity) || 1) < 0.9) continue;
|
|
2838
3388
|
const w = parseFloat(ps.width) || 0;
|
|
2839
3389
|
const h = parseFloat(ps.height) || 0;
|
|
2840
|
-
if (
|
|
3390
|
+
if (w < rect.width - 4 || h < rect.height - 4) continue;
|
|
2841
3391
|
const bg = parseRgb(ps.backgroundColor) || parseAnyColor(ps.backgroundColor);
|
|
2842
|
-
if (!bg || (bg.a ?? 1) < 0.
|
|
2843
|
-
|
|
3392
|
+
if (!bg || (bg.a ?? 1) < 0.9) continue;
|
|
3393
|
+
return bg;
|
|
2844
3394
|
}
|
|
2845
|
-
return
|
|
3395
|
+
return null;
|
|
2846
3396
|
}
|
|
2847
3397
|
|
|
2848
|
-
function
|
|
3398
|
+
function checkElementColorsDOM(el) {
|
|
2849
3399
|
const tag = el.tagName.toLowerCase();
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
const
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
// var(--text-xs), letter-spacing: var(--tracking-widest)), so without
|
|
2876
|
-
// resolution every style-based check silently fails on Tailwind v4
|
|
2877
|
-
// builds — the values come back as literal "var(--font-weight-bold)"
|
|
2878
|
-
// strings and parseFloat returns NaN.
|
|
2879
|
-
function buildCustomPropMap(document) {
|
|
2880
|
-
const map = new Map();
|
|
2881
|
-
let sheets;
|
|
2882
|
-
try { sheets = Array.from(document.styleSheets || []); }
|
|
2883
|
-
catch { return map; }
|
|
2884
|
-
for (const sheet of sheets) {
|
|
2885
|
-
let rules;
|
|
2886
|
-
try { rules = Array.from(sheet.cssRules || []); }
|
|
2887
|
-
catch { continue; }
|
|
2888
|
-
for (const rule of rules) {
|
|
2889
|
-
// Style rules only (type 1). Walk @media / @supports if present.
|
|
2890
|
-
if (rule.type === 4 /* MEDIA_RULE */ || rule.type === 12 /* SUPPORTS_RULE */) {
|
|
2891
|
-
try { rules.push(...Array.from(rule.cssRules || [])); } catch { /* ignore */ }
|
|
2892
|
-
continue;
|
|
2893
|
-
}
|
|
2894
|
-
if (rule.type !== 1 /* STYLE_RULE */) continue;
|
|
2895
|
-
const sel = rule.selectorText || '';
|
|
2896
|
-
if (!/(^|,\s*)(:root|html|:host)\b/i.test(sel)) continue;
|
|
2897
|
-
const style = rule.style;
|
|
2898
|
-
if (!style) continue;
|
|
2899
|
-
for (let i = 0; i < style.length; i++) {
|
|
2900
|
-
const prop = style[i];
|
|
2901
|
-
if (!prop || !prop.startsWith('--')) continue;
|
|
2902
|
-
const val = style.getPropertyValue(prop).trim();
|
|
2903
|
-
if (val) map.set(prop, val);
|
|
2904
|
-
}
|
|
3400
|
+
// No early SAFE_TAGS bail here — checkColors() does its own gating that
|
|
3401
|
+
// includes the styled-button exception for <a> / <button> with their own
|
|
3402
|
+
// opaque background. Bailing here would prevent that exception from firing.
|
|
3403
|
+
const rect = el.getBoundingClientRect();
|
|
3404
|
+
if (rect.width < 10 || rect.height < 10) return [];
|
|
3405
|
+
const style = getComputedStyle(el);
|
|
3406
|
+
// Invisible at rest: hidden scene variants (opacity-0 carousels, swap
|
|
3407
|
+
// decks) are not user-visible, and measuring their inherited colors against
|
|
3408
|
+
// whatever surface happens to sit behind the stack is noise, not audit.
|
|
3409
|
+
if (style.visibility === 'hidden' || effectiveOpacityDOM(el) <= 0.02) return [];
|
|
3410
|
+
const directText = [...el.childNodes].filter(n => n.nodeType === 3).map(n => n.textContent).join('');
|
|
3411
|
+
const hasDirectText = directText.trim().length > 0;
|
|
3412
|
+
const bgInfo = resolveBackgroundInfo(el);
|
|
3413
|
+
let effectiveBg = bgInfo.color;
|
|
3414
|
+
// An unreadable surface anywhere up the chain: skip the gradient-stop
|
|
3415
|
+
// fallback too, so nothing downstream measures against a ground we never
|
|
3416
|
+
// resolved.
|
|
3417
|
+
let surfaceUnresolved = bgInfo.unresolved;
|
|
3418
|
+
let ownBg = readOwnBackgroundColor(el, style);
|
|
3419
|
+
if (!ownBg || (ownBg.a ?? 1) <= 0.5) {
|
|
3420
|
+
const pseudoSurface = readPseudoSurfaceDOM(el, rect);
|
|
3421
|
+
if (pseudoSurface) {
|
|
3422
|
+
ownBg = pseudoSurface;
|
|
3423
|
+
effectiveBg = pseudoSurface;
|
|
3424
|
+
surfaceUnresolved = false;
|
|
2905
3425
|
}
|
|
2906
3426
|
}
|
|
2907
|
-
return
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
//
|
|
2911
|
-
//
|
|
2912
|
-
//
|
|
2913
|
-
//
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
3427
|
+
return checkColors({
|
|
3428
|
+
tag,
|
|
3429
|
+
// Chrome serializes computed colors specified in modern spaces as
|
|
3430
|
+
// oklch()/oklab() strings; without the parseAnyColor fallback the text
|
|
3431
|
+
// color comes back null and the low-contrast / gray-on-color checks
|
|
3432
|
+
// silently never run (the shipped miss: a nav CTA whose text color was
|
|
3433
|
+
// an oklch token near its own oklch background).
|
|
3434
|
+
textColor: parseRgb(style.color) || parseAnyColor(style.color),
|
|
3435
|
+
bgColor: ownBg,
|
|
3436
|
+
effectiveBg: surfaceUnresolved ? null : effectiveBg,
|
|
3437
|
+
effectiveBgStops: surfaceUnresolved || effectiveBg ? null : resolveGradientStops(el),
|
|
3438
|
+
fontSize: parseFloat(style.fontSize) || 16,
|
|
3439
|
+
fontWeight: parseInt(style.fontWeight) || 400,
|
|
3440
|
+
hasDirectText,
|
|
3441
|
+
isEmojiOnly: isEmojiOnlyText(directText),
|
|
3442
|
+
bgClip: style.webkitBackgroundClip || style.backgroundClip || '',
|
|
3443
|
+
bgImage: style.backgroundImage || '',
|
|
3444
|
+
classList: el.getAttribute('class') || '',
|
|
2921
3445
|
});
|
|
2922
3446
|
}
|
|
2923
3447
|
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
// detector's contrast / color checks.
|
|
2930
|
-
function oklchToRgb(L, C, H) {
|
|
2931
|
-
const hRad = (H * Math.PI) / 180;
|
|
2932
|
-
return oklabToRgb(L, C * Math.cos(hRad), C * Math.sin(hRad));
|
|
2933
|
-
}
|
|
2934
|
-
|
|
2935
|
-
function oklabToRgb(L, a, b) {
|
|
2936
|
-
const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
|
|
2937
|
-
const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
|
|
2938
|
-
const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
|
|
2939
|
-
const lc = l_ * l_ * l_, mc = m_ * m_ * m_, sc = s_ * s_ * s_;
|
|
2940
|
-
const rLin = 4.0767416621 * lc - 3.3077115913 * mc + 0.2309699292 * sc;
|
|
2941
|
-
const gLin = -1.2684380046 * lc + 2.6097574011 * mc - 0.3413193965 * sc;
|
|
2942
|
-
const bLin = -0.0041960863 * lc - 0.7034186147 * mc + 1.7076147010 * sc;
|
|
2943
|
-
const enc = (x) => {
|
|
2944
|
-
const c = Math.max(0, Math.min(1, x));
|
|
2945
|
-
return c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
|
|
2946
|
-
};
|
|
2947
|
-
return {
|
|
2948
|
-
r: Math.round(enc(rLin) * 255),
|
|
2949
|
-
g: Math.round(enc(gLin) * 255),
|
|
2950
|
-
b: Math.round(enc(bLin) * 255),
|
|
2951
|
-
a: 1,
|
|
2952
|
-
};
|
|
2953
|
-
}
|
|
2954
|
-
|
|
2955
|
-
function hslToRgb(h, s, l) {
|
|
2956
|
-
h = ((h % 360) + 360) % 360;
|
|
2957
|
-
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
2958
|
-
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
2959
|
-
const m0 = l - c / 2;
|
|
2960
|
-
const [r, g, b] =
|
|
2961
|
-
h < 60 ? [c, x, 0] :
|
|
2962
|
-
h < 120 ? [x, c, 0] :
|
|
2963
|
-
h < 180 ? [0, c, x] :
|
|
2964
|
-
h < 240 ? [0, x, c] :
|
|
2965
|
-
h < 300 ? [x, 0, c] : [c, 0, x];
|
|
2966
|
-
return {
|
|
2967
|
-
r: Math.round((r + m0) * 255),
|
|
2968
|
-
g: Math.round((g + m0) * 255),
|
|
2969
|
-
b: Math.round((b + m0) * 255),
|
|
2970
|
-
a: 1,
|
|
2971
|
-
};
|
|
2972
|
-
}
|
|
2973
|
-
|
|
2974
|
-
function hwbToRgb(h, w, bl) {
|
|
2975
|
-
if (w + bl >= 1) {
|
|
2976
|
-
const g = Math.round((w / (w + bl)) * 255);
|
|
2977
|
-
return { r: g, g, b: g, a: 1 };
|
|
2978
|
-
}
|
|
2979
|
-
const base = hslToRgb(h, 1, 0.5);
|
|
2980
|
-
const mix = (c) => Math.round(((c / 255) * (1 - w - bl) + w) * 255);
|
|
2981
|
-
return { r: mix(base.r), g: mix(base.g), b: mix(base.b), a: 1 };
|
|
2982
|
-
}
|
|
2983
|
-
|
|
2984
|
-
// Common CSS named colors — the handful that actually show up in generated
|
|
2985
|
-
// UIs, not the full 148-name spec list. Includes the achromatic names so a
|
|
2986
|
-
// named gray parses (and correctly reads as no-chroma) instead of being
|
|
2987
|
-
// treated as an unknown color.
|
|
2988
|
-
const CSS_NAMED_COLORS = {
|
|
2989
|
-
black: { r: 0, g: 0, b: 0 },
|
|
2990
|
-
white: { r: 255, g: 255, b: 255 },
|
|
2991
|
-
gray: { r: 128, g: 128, b: 128 },
|
|
2992
|
-
grey: { r: 128, g: 128, b: 128 },
|
|
2993
|
-
silver: { r: 192, g: 192, b: 192 },
|
|
2994
|
-
dimgray: { r: 105, g: 105, b: 105 },
|
|
2995
|
-
darkgray: { r: 169, g: 169, b: 169 },
|
|
2996
|
-
lightgray: { r: 211, g: 211, b: 211 },
|
|
2997
|
-
gainsboro: { r: 220, g: 220, b: 220 },
|
|
2998
|
-
whitesmoke: { r: 245, g: 245, b: 245 },
|
|
2999
|
-
red: { r: 255, g: 0, b: 0 },
|
|
3000
|
-
crimson: { r: 220, g: 20, b: 60 },
|
|
3001
|
-
tomato: { r: 255, g: 99, b: 71 },
|
|
3002
|
-
coral: { r: 255, g: 127, b: 80 },
|
|
3003
|
-
salmon: { r: 250, g: 128, b: 114 },
|
|
3004
|
-
orange: { r: 255, g: 165, b: 0 },
|
|
3005
|
-
gold: { r: 255, g: 215, b: 0 },
|
|
3006
|
-
yellow: { r: 255, g: 255, b: 0 },
|
|
3007
|
-
olive: { r: 128, g: 128, b: 0 },
|
|
3008
|
-
lime: { r: 0, g: 255, b: 0 },
|
|
3009
|
-
green: { r: 0, g: 128, b: 0 },
|
|
3010
|
-
teal: { r: 0, g: 128, b: 128 },
|
|
3011
|
-
turquoise: { r: 64, g: 224, b: 208 },
|
|
3012
|
-
cyan: { r: 0, g: 255, b: 255 },
|
|
3013
|
-
aqua: { r: 0, g: 255, b: 255 },
|
|
3014
|
-
skyblue: { r: 135, g: 206, b: 235 },
|
|
3015
|
-
dodgerblue: { r: 30, g: 144, b: 255 },
|
|
3016
|
-
blue: { r: 0, g: 0, b: 255 },
|
|
3017
|
-
navy: { r: 0, g: 0, b: 128 },
|
|
3018
|
-
indigo: { r: 75, g: 0, b: 130 },
|
|
3019
|
-
rebeccapurple: { r: 102, g: 51, b: 153 },
|
|
3020
|
-
purple: { r: 128, g: 0, b: 128 },
|
|
3021
|
-
violet: { r: 238, g: 130, b: 238 },
|
|
3022
|
-
orchid: { r: 218, g: 112, b: 214 },
|
|
3023
|
-
magenta: { r: 255, g: 0, b: 255 },
|
|
3024
|
-
fuchsia: { r: 255, g: 0, b: 255 },
|
|
3025
|
-
hotpink: { r: 255, g: 105, b: 180 },
|
|
3026
|
-
pink: { r: 255, g: 192, b: 203 },
|
|
3027
|
-
maroon: { r: 128, g: 0, b: 0 },
|
|
3028
|
-
};
|
|
3029
|
-
|
|
3030
|
-
// Split a string on top-level commas (ignoring commas nested in parens).
|
|
3031
|
-
function splitTopLevelCommas(str) {
|
|
3032
|
-
const parts = [];
|
|
3033
|
-
let depth = 0, start = 0;
|
|
3034
|
-
for (let i = 0; i < str.length; i++) {
|
|
3035
|
-
const ch = str[i];
|
|
3036
|
-
if (ch === '(') depth++;
|
|
3037
|
-
else if (ch === ')') depth = Math.max(0, depth - 1);
|
|
3038
|
-
else if (ch === ',' && depth === 0) {
|
|
3039
|
-
parts.push(str.slice(start, i).trim());
|
|
3040
|
-
start = i + 1;
|
|
3041
|
-
}
|
|
3042
|
-
}
|
|
3043
|
-
const tail = str.slice(start).trim();
|
|
3044
|
-
if (tail) parts.push(tail);
|
|
3045
|
-
return parts;
|
|
3046
|
-
}
|
|
3047
|
-
|
|
3048
|
-
// Evaluate a CSS color-mix() expression to {r,g,b,a}. Returns null when
|
|
3049
|
-
// the expression can't be resolved (unresolved var(), unknown colors).
|
|
3050
|
-
//
|
|
3051
|
-
// Mixing is done with premultiplied alpha in sRGB regardless of the
|
|
3052
|
-
// declared interpolation space. That is exact for the dominant generated-UI
|
|
3053
|
-
// pattern — `color-mix(in oklab, <color> N%, transparent)` — where the
|
|
3054
|
-
// result is simply <color> at alpha N% in ANY rectangular space, and a
|
|
3055
|
-
// close-enough approximation for opaque-opaque mixes (the detector only
|
|
3056
|
-
// consumes these values for contrast/chroma thresholds, not for display).
|
|
3057
|
-
function parseColorMix(str) {
|
|
3058
|
-
const m = String(str).trim().match(/^color-mix\(/i);
|
|
3059
|
-
if (!m) return null;
|
|
3060
|
-
// Balanced-paren capture of the arguments.
|
|
3061
|
-
let depth = 0, end = -1;
|
|
3062
|
-
const open = str.indexOf('(');
|
|
3063
|
-
for (let i = open; i < str.length; i++) {
|
|
3064
|
-
if (str[i] === '(') depth++;
|
|
3065
|
-
else if (str[i] === ')') { depth--; if (depth === 0) { end = i; break; } }
|
|
3066
|
-
}
|
|
3067
|
-
if (end < 0) return null;
|
|
3068
|
-
const args = splitTopLevelCommas(str.slice(open + 1, end));
|
|
3069
|
-
if (args.length !== 3 || !/^in\s/i.test(args[0])) return null;
|
|
3448
|
+
function checkElementIconTileDOM(el) {
|
|
3449
|
+
const tag = el.tagName.toLowerCase();
|
|
3450
|
+
if (!HEADING_TAGS.has(tag)) return [];
|
|
3451
|
+
const sibling = el.previousElementSibling;
|
|
3452
|
+
if (!sibling) return [];
|
|
3070
3453
|
|
|
3071
|
-
const
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
let colorStr = component;
|
|
3075
|
-
const trail = component.match(/\s+([\d.]+)%$/);
|
|
3076
|
-
const lead = component.match(/^([\d.]+)%\s+/);
|
|
3077
|
-
if (trail) { pct = parseFloat(trail[1]); colorStr = component.slice(0, trail.index).trim(); }
|
|
3078
|
-
else if (lead) { pct = parseFloat(lead[1]); colorStr = component.slice(lead[0].length).trim(); }
|
|
3079
|
-
let color;
|
|
3080
|
-
if (/^transparent$/i.test(colorStr)) color = { r: 0, g: 0, b: 0, a: 0 };
|
|
3081
|
-
else color = parseAnyColor(colorStr);
|
|
3082
|
-
if (!color) return null;
|
|
3083
|
-
return { color, pct };
|
|
3084
|
-
};
|
|
3454
|
+
const sibRect = sibling.getBoundingClientRect();
|
|
3455
|
+
const headRect = el.getBoundingClientRect();
|
|
3456
|
+
const sibStyle = getComputedStyle(sibling);
|
|
3085
3457
|
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3458
|
+
// The tile may either contain an <svg>/<i> icon child, OR the tile itself
|
|
3459
|
+
// may contain an emoji/symbol character directly as its only text content
|
|
3460
|
+
// (the "card-icon" pattern from many AI-generated demos).
|
|
3461
|
+
const iconChild = sibling.querySelector('svg, i[data-lucide], i[class*="fa-"], i[class*="icon"]');
|
|
3462
|
+
const iconRect = iconChild?.getBoundingClientRect();
|
|
3463
|
+
const sibDirectText = [...sibling.childNodes].filter(n => n.nodeType === 3).map(n => n.textContent).join('');
|
|
3464
|
+
const hasInlineEmojiIcon = sibling.children.length === 0 && isEmojiOnlyText(sibDirectText);
|
|
3465
|
+
|
|
3466
|
+
return checkIconTile({
|
|
3467
|
+
headingTag: tag,
|
|
3468
|
+
headingText: el.textContent || '',
|
|
3469
|
+
headingTop: headRect.top,
|
|
3470
|
+
siblingTag: sibling.tagName.toLowerCase(),
|
|
3471
|
+
siblingWidth: sibRect.width,
|
|
3472
|
+
siblingHeight: sibRect.height,
|
|
3473
|
+
siblingBottom: sibRect.bottom,
|
|
3474
|
+
siblingBgColor: parseRgb(sibStyle.backgroundColor),
|
|
3475
|
+
siblingBgImage: sibStyle.backgroundImage || '',
|
|
3476
|
+
siblingBorderWidth: parseFloat(sibStyle.borderTopWidth) || 0,
|
|
3477
|
+
siblingBorderRadius: parseFloat(sibStyle.borderRadius) || 0,
|
|
3478
|
+
hasIconChild: !!iconChild || hasInlineEmojiIcon,
|
|
3479
|
+
iconChildWidth: iconRect?.width || 0,
|
|
3480
|
+
});
|
|
3104
3481
|
}
|
|
3105
3482
|
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
const
|
|
3110
|
-
return {
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3483
|
+
function checkElementItalicSerifDOM(el) {
|
|
3484
|
+
const tag = el.tagName.toLowerCase();
|
|
3485
|
+
if (tag !== 'h1' && tag !== 'h2') return [];
|
|
3486
|
+
const style = getComputedStyle(el);
|
|
3487
|
+
return checkItalicSerif({
|
|
3488
|
+
tag,
|
|
3489
|
+
fontStyle: style.fontStyle || '',
|
|
3490
|
+
fontFamily: style.fontFamily || '',
|
|
3491
|
+
fontSize: parseFloat(style.fontSize) || 0,
|
|
3492
|
+
headingText: el.textContent || '',
|
|
3493
|
+
});
|
|
3116
3494
|
}
|
|
3117
3495
|
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
if (m) return { r: Math.round(+m[1]), g: Math.round(+m[2]), b: Math.round(+m[3]), a: m[4] !== undefined ? +m[4] : 1 };
|
|
3130
|
-
m = str.match(/^#([0-9a-f]{3,8})$/i);
|
|
3131
|
-
if (m) {
|
|
3132
|
-
const h = m[1];
|
|
3133
|
-
if (h.length === 3 || h.length === 4) {
|
|
3134
|
-
return {
|
|
3135
|
-
r: parseInt(h[0] + h[0], 16),
|
|
3136
|
-
g: parseInt(h[1] + h[1], 16),
|
|
3137
|
-
b: parseInt(h[2] + h[2], 16),
|
|
3138
|
-
a: h.length === 4 ? parseInt(h[3] + h[3], 16) / 255 : 1,
|
|
3139
|
-
};
|
|
3140
|
-
}
|
|
3141
|
-
if (h.length === 6 || h.length === 8) {
|
|
3142
|
-
return {
|
|
3143
|
-
r: parseInt(h.slice(0, 2), 16),
|
|
3144
|
-
g: parseInt(h.slice(2, 4), 16),
|
|
3145
|
-
b: parseInt(h.slice(4, 6), 16),
|
|
3146
|
-
a: h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1,
|
|
3147
|
-
};
|
|
3148
|
-
}
|
|
3149
|
-
}
|
|
3150
|
-
// OKLCH parser. Tailwind v4's CSS minifier squishes the space after
|
|
3151
|
-
// `%` ("21.5%.02 50"), so the separator between L and C may be absent.
|
|
3152
|
-
// Match L (with optional %), then C and H separated permissively.
|
|
3153
|
-
m = str.match(/oklch\(\s*([\d.]+)(%?)\s*[\s,]*\s*([\d.]+)\s*[\s,]+\s*([-\d.]+)(?:deg)?(?:\s*\/\s*([\d.]+)(%)?)?\s*\)/i);
|
|
3154
|
-
if (m) {
|
|
3155
|
-
const Lnum = parseFloat(m[1]);
|
|
3156
|
-
const L = m[2] === '%' ? Lnum / 100 : Lnum;
|
|
3157
|
-
const rgb = oklchToRgb(L, parseFloat(m[3]), parseFloat(m[4]));
|
|
3158
|
-
if (m[5] !== undefined) {
|
|
3159
|
-
const alpha = parseFloat(m[5]);
|
|
3160
|
-
rgb.a = m[6] === '%' ? alpha / 100 : alpha;
|
|
3161
|
-
}
|
|
3162
|
-
return rgb;
|
|
3163
|
-
}
|
|
3164
|
-
// OKLAB — a/b are signed axes; percentages map 100% → 0.4.
|
|
3165
|
-
m = str.match(/oklab\(\s*([\d.]+)(%?)\s+(-?[\d.]+)(%?)\s+(-?[\d.]+)(%?)(?:\s*\/\s*([\d.]+)(%)?)?\s*\)/i);
|
|
3166
|
-
if (m) {
|
|
3167
|
-
const L = m[2] === '%' ? parseFloat(m[1]) / 100 : parseFloat(m[1]);
|
|
3168
|
-
const a = m[4] === '%' ? parseFloat(m[3]) * 0.004 : parseFloat(m[3]);
|
|
3169
|
-
const b = m[6] === '%' ? parseFloat(m[5]) * 0.004 : parseFloat(m[5]);
|
|
3170
|
-
const rgb = oklabToRgb(L, a, b);
|
|
3171
|
-
if (m[7] !== undefined) {
|
|
3172
|
-
const alpha = parseFloat(m[7]);
|
|
3173
|
-
rgb.a = m[8] === '%' ? alpha / 100 : alpha;
|
|
3174
|
-
}
|
|
3175
|
-
return rgb;
|
|
3176
|
-
}
|
|
3177
|
-
// HSL/HSLA — comma or space syntax, optional deg on hue.
|
|
3178
|
-
m = str.match(/hsla?\(\s*(-?[\d.]+)(?:deg)?\s*[,\s]\s*([\d.]+)%\s*[,\s]\s*([\d.]+)%(?:\s*[,/]\s*([\d.]+)(%)?)?\s*\)/i);
|
|
3179
|
-
if (m) {
|
|
3180
|
-
const rgb = hslToRgb(parseFloat(m[1]), parseFloat(m[2]) / 100, parseFloat(m[3]) / 100);
|
|
3181
|
-
if (m[4] !== undefined) {
|
|
3182
|
-
const alpha = parseFloat(m[4]);
|
|
3183
|
-
rgb.a = m[5] === '%' ? alpha / 100 : alpha;
|
|
3184
|
-
}
|
|
3185
|
-
return rgb;
|
|
3496
|
+
function domAccentDashPseudo(el) {
|
|
3497
|
+
for (const which of ['::before', '::after']) {
|
|
3498
|
+
let ps;
|
|
3499
|
+
try { ps = getComputedStyle(el, which); } catch { continue; }
|
|
3500
|
+
if (!ps || ps.content === 'none' || ps.content === '') continue;
|
|
3501
|
+
const w = parseFloat(ps.width) || 0;
|
|
3502
|
+
const h = parseFloat(ps.height) || 0;
|
|
3503
|
+
if (!(w >= 8 && w <= 80 && h >= 1 && h <= 6)) continue;
|
|
3504
|
+
const bg = parseRgb(ps.backgroundColor) || parseAnyColor(ps.backgroundColor);
|
|
3505
|
+
if (!bg || (bg.a ?? 1) < 0.1) continue;
|
|
3506
|
+
if (Math.max(bg.r, bg.g, bg.b) - Math.min(bg.r, bg.g, bg.b) >= 30) return true;
|
|
3186
3507
|
}
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3508
|
+
return false;
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
function checkElementHeroEyebrowDOM(el) {
|
|
3512
|
+
const tag = el.tagName.toLowerCase();
|
|
3513
|
+
if (tag !== 'h1') return [];
|
|
3514
|
+
const sibling = el.previousElementSibling;
|
|
3515
|
+
if (!sibling) return [];
|
|
3516
|
+
const headStyle = getComputedStyle(el);
|
|
3517
|
+
const sibStyle = getComputedStyle(sibling);
|
|
3518
|
+
return checkHeroEyebrow({
|
|
3519
|
+
headingTag: tag,
|
|
3520
|
+
headingText: el.textContent || '',
|
|
3521
|
+
headingFontSize: parseFloat(headStyle.fontSize) || 0,
|
|
3522
|
+
headingInApplicationContext: !!el.closest('[role="tabpanel"], [role="dialog"], [role="application"], dialog'),
|
|
3523
|
+
siblingTag: sibling.tagName.toLowerCase(),
|
|
3524
|
+
siblingText: sibling.textContent || '',
|
|
3525
|
+
siblingTextTransform: sibStyle.textTransform || '',
|
|
3526
|
+
siblingFontSize: parseFloat(sibStyle.fontSize) || 0,
|
|
3527
|
+
siblingLetterSpacing: parseFloat(sibStyle.letterSpacing) || 0,
|
|
3528
|
+
siblingFontWeight: sibStyle.fontWeight || '',
|
|
3529
|
+
siblingColor: sibStyle.color || '',
|
|
3530
|
+
siblingHasAccentDashPseudo: domAccentDashPseudo(sibling),
|
|
3531
|
+
});
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
// Build a map of CSS custom properties declared on :root / :host / html.
|
|
3535
|
+
// Used to resolve var(--X) refs that jsdom returns verbatim in
|
|
3536
|
+
// getComputedStyle. Tailwind v4 routes every utility class through
|
|
3537
|
+
// CSS vars (font-weight: var(--font-weight-bold), font-size:
|
|
3538
|
+
// var(--text-xs), letter-spacing: var(--tracking-widest)), so without
|
|
3539
|
+
// resolution every style-based check silently fails on Tailwind v4
|
|
3540
|
+
// builds — the values come back as literal "var(--font-weight-bold)"
|
|
3541
|
+
// strings and parseFloat returns NaN.
|
|
3542
|
+
function buildCustomPropMap(document) {
|
|
3543
|
+
const map = new Map();
|
|
3544
|
+
let sheets;
|
|
3545
|
+
try { sheets = Array.from(document.styleSheets || []); }
|
|
3546
|
+
catch { return map; }
|
|
3547
|
+
for (const sheet of sheets) {
|
|
3548
|
+
let rules;
|
|
3549
|
+
try { rules = Array.from(sheet.cssRules || []); }
|
|
3550
|
+
catch { continue; }
|
|
3551
|
+
for (const rule of rules) {
|
|
3552
|
+
// Style rules only (type 1). Walk @media / @supports if present.
|
|
3553
|
+
if (rule.type === 4 /* MEDIA_RULE */ || rule.type === 12 /* SUPPORTS_RULE */) {
|
|
3554
|
+
try { rules.push(...Array.from(rule.cssRules || [])); } catch { /* ignore */ }
|
|
3555
|
+
continue;
|
|
3556
|
+
}
|
|
3557
|
+
if (rule.type !== 1 /* STYLE_RULE */) continue;
|
|
3558
|
+
const sel = rule.selectorText || '';
|
|
3559
|
+
if (!/(^|,\s*)(:root|html|:host)\b/i.test(sel)) continue;
|
|
3560
|
+
const style = rule.style;
|
|
3561
|
+
if (!style) continue;
|
|
3562
|
+
for (let i = 0; i < style.length; i++) {
|
|
3563
|
+
const prop = style[i];
|
|
3564
|
+
if (!prop || !prop.startsWith('--')) continue;
|
|
3565
|
+
const val = style.getPropertyValue(prop).trim();
|
|
3566
|
+
if (val) map.set(prop, val);
|
|
3567
|
+
}
|
|
3194
3568
|
}
|
|
3195
|
-
return rgb;
|
|
3196
3569
|
}
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3570
|
+
return map;
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
// Resolve var(--X[, fallback]) refs in a computed-style value string.
|
|
3574
|
+
// Recurses up to 8 levels for chained refs (--a: var(--b)). Returns
|
|
3575
|
+
// the original string when no refs are present or the chain doesn't
|
|
3576
|
+
// resolve. Safe to call on already-resolved values.
|
|
3577
|
+
function resolveVarRefs(raw, customPropMap, depth = 0) {
|
|
3578
|
+
if (typeof raw !== 'string' || !raw.includes('var(')) return raw;
|
|
3579
|
+
if (depth > 8) return raw;
|
|
3580
|
+
return raw.replace(/var\(\s*(--[a-zA-Z0-9_-]+)\s*(?:,\s*([^)]+))?\)/g, (_m, name, fallback) => {
|
|
3581
|
+
const v = customPropMap.get(name);
|
|
3582
|
+
if (v != null) return resolveVarRefs(v, customPropMap, depth + 1);
|
|
3583
|
+
return fallback ? resolveVarRefs(fallback.trim(), customPropMap, depth + 1) : _m;
|
|
3584
|
+
});
|
|
3200
3585
|
}
|
|
3201
3586
|
|
|
3587
|
+
|
|
3202
3588
|
// Resolve var() refs in a color string (via customPropMap), then parse.
|
|
3203
3589
|
// Returns null on any failure. Used in jsdom-mode paths where
|
|
3204
3590
|
// getComputedStyle returns literal "var(--X)" or "oklch(...)" strings.
|
|
@@ -3287,7 +3673,11 @@ function isKickerCandidate(opts) {
|
|
|
3287
3673
|
|| isSmallCaps;
|
|
3288
3674
|
if (!isUppercased) return false;
|
|
3289
3675
|
if (!(kickerFontSize > 0 && kickerFontSize <= 14)) return false;
|
|
3290
|
-
|
|
3676
|
+
// Proportional only, no absolute floor: the wild's most common recipe is
|
|
3677
|
+
// 0.08em at a sub-13px size, which computes to under 1px and sailed past
|
|
3678
|
+
// the old Math.max(1, ...) floor (observed live: a page whose kickers were
|
|
3679
|
+
// literally class="kicker" produced zero findings).
|
|
3680
|
+
const minTrackedSpacing = kickerFontSize * 0.06;
|
|
3291
3681
|
if (!(kickerLetterSpacing >= minTrackedSpacing)) return false;
|
|
3292
3682
|
return true;
|
|
3293
3683
|
}
|
|
@@ -3556,9 +3946,20 @@ function checkElementGlowDOM(el) {
|
|
|
3556
3946
|
if (!boxShadow && !textShadow) return [];
|
|
3557
3947
|
// Use parent's background — glow radiates outward, so the surrounding context matters
|
|
3558
3948
|
// If resolveBackground returns null (gradient), try to infer from the gradient colors
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3949
|
+
const parentBgInfo = resolveBackgroundInfo(el.parentElement || el);
|
|
3950
|
+
// Unknown surface (an unreadable layer on the way up): skip only the
|
|
3951
|
+
// gradient hunt below, which would walk PAST that layer and score the
|
|
3952
|
+
// glow against a background the visitor never sees. checkGlow still runs
|
|
3953
|
+
// with a null surface: the zero-offset chromatic halo tell holds on ANY
|
|
3954
|
+
// background, and the static loop already passes the unresolved walk's
|
|
3955
|
+
// null color straight through (detect-html.mjs uses resolveBackground).
|
|
3956
|
+
let parentBg = parentBgInfo.color;
|
|
3957
|
+
if (!parentBg && !parentBgInfo.unresolved) {
|
|
3958
|
+
// Gradient background — sample its colors to determine if it's dark.
|
|
3959
|
+
// Modern-syntax parsing matters here: body-level gradients now reach this
|
|
3960
|
+
// fallback in browser mode, and their stops usually serialize as oklch —
|
|
3961
|
+
// which the shared parseGradientColors reads via its color-function
|
|
3962
|
+
// token capture.
|
|
3562
3963
|
let cur = el.parentElement;
|
|
3563
3964
|
while (cur && cur.nodeType === 1) {
|
|
3564
3965
|
const bgImage = getComputedStyle(cur).backgroundImage || '';
|
|
@@ -3606,10 +4007,13 @@ function checkElementAIPaletteDOM(el) {
|
|
|
3606
4007
|
const hue = getHue(textColor);
|
|
3607
4008
|
const isAIPalette = (hue >= 160 && hue <= 200) || (hue >= 260 && hue <= 310);
|
|
3608
4009
|
if (isAIPalette) {
|
|
3609
|
-
const
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
4010
|
+
const parentBgInfo = el.parentElement
|
|
4011
|
+
? resolveBackgroundInfo(el.parentElement)
|
|
4012
|
+
: { color: null, unresolved: false };
|
|
4013
|
+
// Unknown surface: leave effectiveBg null (no finding) rather than
|
|
4014
|
+
// hunting gradient ancestors past a layer we could not read.
|
|
4015
|
+
let effectiveBg = parentBgInfo.color;
|
|
4016
|
+
if (!effectiveBg && !parentBgInfo.unresolved) {
|
|
3613
4017
|
let cur = el.parentElement;
|
|
3614
4018
|
while (cur && cur.nodeType === 1) {
|
|
3615
4019
|
const gi = getComputedStyle(cur).backgroundImage || '';
|
|
@@ -4404,10 +4808,19 @@ function checkElementBorders(tag, style, overrides, resolvedRadius, el = null) {
|
|
|
4404
4808
|
}
|
|
4405
4809
|
|
|
4406
4810
|
function checkElementColors(el, style, tag, window, customPropMap, hasAnchorInheritRule) {
|
|
4811
|
+
// Invisible at rest, static twin of the browser walk's skip: opacity does
|
|
4812
|
+
// not inherit, so walk ancestors multiplying declared opacity down.
|
|
4813
|
+
if (style.visibility === 'hidden') return [];
|
|
4814
|
+
let effOpacity = 1;
|
|
4815
|
+
for (let cur = el; cur && cur.nodeType === 1 && effOpacity > 0.02; cur = cur.parentElement) {
|
|
4816
|
+
effOpacity *= parseFloat(window.getComputedStyle(cur).opacity || '1');
|
|
4817
|
+
}
|
|
4818
|
+
if (effOpacity <= 0.02) return [];
|
|
4407
4819
|
const directText = [...el.childNodes].filter(n => n.nodeType === 3).map(n => n.textContent).join('');
|
|
4408
4820
|
const hasDirectText = directText.trim().length > 0;
|
|
4409
4821
|
|
|
4410
|
-
const
|
|
4822
|
+
const bgInfo = resolveBackgroundInfo(el, window, customPropMap);
|
|
4823
|
+
const effectiveBg = bgInfo.color;
|
|
4411
4824
|
// jsdom returns literal "var(--X)" / "oklch(...)" for color, so plain
|
|
4412
4825
|
// parseRgb misses Tailwind-tokenized text colors. Resolve through the
|
|
4413
4826
|
// customPropMap first; fall back to parseRgb for vanilla rgb() pages.
|
|
@@ -4453,11 +4866,13 @@ function checkElementColors(el, style, tag, window, customPropMap, hasAnchorInhe
|
|
|
4453
4866
|
// element itself has no usable own background, that pseudo is the real
|
|
4454
4867
|
// surface for contrast purposes.
|
|
4455
4868
|
let finalEffectiveBg = effectiveBg;
|
|
4869
|
+
let surfaceUnresolved = bgInfo.unresolved;
|
|
4456
4870
|
if ((!ownBg || (ownBg.a ?? 1) <= 0.5) && typeof window.getPseudoSurface === 'function') {
|
|
4457
4871
|
const pseudoSurface = window.getPseudoSurface(el);
|
|
4458
4872
|
if (pseudoSurface) {
|
|
4459
4873
|
ownBg = pseudoSurface;
|
|
4460
4874
|
finalEffectiveBg = pseudoSurface;
|
|
4875
|
+
surfaceUnresolved = false;
|
|
4461
4876
|
}
|
|
4462
4877
|
}
|
|
4463
4878
|
|
|
@@ -4465,8 +4880,9 @@ function checkElementColors(el, style, tag, window, customPropMap, hasAnchorInhe
|
|
|
4465
4880
|
tag,
|
|
4466
4881
|
textColor,
|
|
4467
4882
|
bgColor: ownBg,
|
|
4468
|
-
|
|
4469
|
-
|
|
4883
|
+
// Unknown surface: hand the checks nothing rather than a guess.
|
|
4884
|
+
effectiveBg: surfaceUnresolved ? null : finalEffectiveBg,
|
|
4885
|
+
effectiveBgStops: surfaceUnresolved || finalEffectiveBg ? null : resolveGradientStops(el, window, customPropMap),
|
|
4470
4886
|
fontSize: parseFloat(style.fontSize) || 16,
|
|
4471
4887
|
fontWeight: parseInt(style.fontWeight) || 400,
|
|
4472
4888
|
hasDirectText,
|
|
@@ -4671,12 +5087,6 @@ function checkTypography() {
|
|
|
4671
5087
|
if (isBrandFontOnOwnDomain(font)) continue;
|
|
4672
5088
|
findings.push({ type: 'overused-font', detail: `Primary font: ${font} (${Math.round(share * 100)}% of text)` });
|
|
4673
5089
|
}
|
|
4674
|
-
|
|
4675
|
-
// Single-font check: only one distinct primary font across all text
|
|
4676
|
-
if (fontUsage.size === 1) {
|
|
4677
|
-
const only = [...fontUsage.keys()][0];
|
|
4678
|
-
findings.push({ type: 'single-font', detail: `only font used is ${only}` });
|
|
4679
|
-
}
|
|
4680
5090
|
}
|
|
4681
5091
|
|
|
4682
5092
|
const sizes = new Set();
|
|
@@ -4938,14 +5348,6 @@ function checkPageTypography(doc, win) {
|
|
|
4938
5348
|
findings.push({ id: 'overused-font', snippet: `Primary font: ${font}` });
|
|
4939
5349
|
}
|
|
4940
5350
|
|
|
4941
|
-
// Single font
|
|
4942
|
-
if (fonts.size === 1) {
|
|
4943
|
-
const els = doc.querySelectorAll('*');
|
|
4944
|
-
if (els.length >= 20) {
|
|
4945
|
-
findings.push({ id: 'single-font', snippet: `only font used is ${[...fonts][0]}` });
|
|
4946
|
-
}
|
|
4947
|
-
}
|
|
4948
|
-
|
|
4949
5351
|
// Flat type hierarchy
|
|
4950
5352
|
const sizes = new Set();
|
|
4951
5353
|
const textEls = doc.querySelectorAll('h1, h2, h3, h4, h5, h6, p, span, a, li, td, th, label, button, div');
|
|
@@ -5576,6 +5978,11 @@ function isRenderedForBrowserRule(el) {
|
|
|
5576
5978
|
function checkElementTextOverflowDOM(el) {
|
|
5577
5979
|
const tag = el.tagName.toLowerCase();
|
|
5578
5980
|
if (TEXT_OVERFLOW_SKIP_TAGS.has(tag)) return [];
|
|
5981
|
+
// scrollWidth/clientWidth are CSS box-model metrics; on SVG content Chrome
|
|
5982
|
+
// returns arbitrary non-zero values for both (a <text> reported 78/48 while
|
|
5983
|
+
// its rendered length sat comfortably inside its box), so the delta is
|
|
5984
|
+
// noise, not overflow. SVG clips to its own viewport anyway.
|
|
5985
|
+
if (el.namespaceURI === 'http://www.w3.org/2000/svg') return [];
|
|
5579
5986
|
if (!isRenderedForBrowserRule(el)) return [];
|
|
5580
5987
|
// Only the element that actually owns overflowing text — not its ancestors,
|
|
5581
5988
|
// which inherit a wider scrollWidth from the spilling descendant.
|
|
@@ -5960,6 +6367,22 @@ function isPaintedForOcclusion(el) {
|
|
|
5960
6367
|
// path is pure geometry and runs anywhere on the page.
|
|
5961
6368
|
const OCCLUSION_TEXT_SKIP_TAGS = new Set(['script', 'style', 'noscript', 'template', 'title']);
|
|
5962
6369
|
|
|
6370
|
+
// An element whose effective opacity multiplies out to ~0 paints nothing at
|
|
6371
|
+
// rest: it is not user-visible, so visual findings on it (contrast, occlusion)
|
|
6372
|
+
// measure a state nobody sees. Browser-only — the walk needs live computed
|
|
6373
|
+
// styles. Cycling scenes that fade such elements in later are the screenshot
|
|
6374
|
+
// subsystem's territory, not the analytic walk's.
|
|
6375
|
+
function effectiveOpacityDOM(el) {
|
|
6376
|
+
let o = 1;
|
|
6377
|
+
// Walk all the way through body and html: `body { opacity: 0 }` page-fade
|
|
6378
|
+
// wrappers hide every descendant just as thoroughly as a local wrapper.
|
|
6379
|
+
for (let cur = el; cur && cur.nodeType === 1; cur = cur.parentElement) {
|
|
6380
|
+
o *= parseFloat(getComputedStyle(cur).opacity || '1');
|
|
6381
|
+
if (o <= 0.02) return 0;
|
|
6382
|
+
}
|
|
6383
|
+
return o;
|
|
6384
|
+
}
|
|
6385
|
+
|
|
5963
6386
|
function checkTextOcclusionDOM() {
|
|
5964
6387
|
const findings = [];
|
|
5965
6388
|
const seenVictims = new Set();
|
|
@@ -5987,6 +6410,11 @@ function checkTextOcclusionDOM() {
|
|
|
5987
6410
|
}
|
|
5988
6411
|
return false;
|
|
5989
6412
|
};
|
|
6413
|
+
// The classic occluder shape this rules out is an opacity-0 interaction
|
|
6414
|
+
// layer — a range scrubber stretched over a before/after comparison — which
|
|
6415
|
+
// elementFromPoint still returns and whose UA background-color otherwise
|
|
6416
|
+
// reads as an opaque box.
|
|
6417
|
+
const effectiveOpacity = effectiveOpacityDOM;
|
|
5990
6418
|
|
|
5991
6419
|
// Collect renderable text owners in / near the first viewport for the
|
|
5992
6420
|
// elementFromPoint probe. SVG <text> counts too.
|
|
@@ -5999,6 +6427,7 @@ function checkTextOcclusionDOM() {
|
|
|
5999
6427
|
const text = inSvg ? (el.textContent || '').trim() : elementDirectText(el);
|
|
6000
6428
|
if (text.length < 2) continue;
|
|
6001
6429
|
if (!isPaintedForOcclusion(el)) continue;
|
|
6430
|
+
if (effectiveOpacity(el) <= 0.02) continue;
|
|
6002
6431
|
let rect; try { rect = el.getBoundingClientRect(); } catch { continue; }
|
|
6003
6432
|
if (rect.width < 6 || rect.height < 6) continue;
|
|
6004
6433
|
// Viewport-bound probe: keep text whose box overlaps the live viewport.
|
|
@@ -6032,6 +6461,7 @@ function checkTextOcclusionDOM() {
|
|
|
6032
6461
|
if (top === el || el.contains(top) || top.contains(el)) continue;
|
|
6033
6462
|
const topCs = getComputedStyle(top);
|
|
6034
6463
|
if (isFloated(topCs) || isMarqueeish(top, topCs) || isPinnedOverlay(top)) continue;
|
|
6464
|
+
if (effectiveOpacity(top) <= 0.02) continue;
|
|
6035
6465
|
const topTag = top.tagName.toLowerCase();
|
|
6036
6466
|
// Text sitting under a raw image/video is contrast territory (deduped
|
|
6037
6467
|
// against the pixel low-contrast rule); leave those alone here.
|
|
@@ -6924,6 +7354,10 @@ if (IS_BROWSER) {
|
|
|
6924
7354
|
|
|
6925
7355
|
const reasons = collectVisualContrastReasons(el, style);
|
|
6926
7356
|
if (reasons.length === 0) continue;
|
|
7357
|
+
// Image-only mode filters here, inside the cap: gradient/opacity/filter
|
|
7358
|
+
// candidates earlier in DOM order must not consume the budget and
|
|
7359
|
+
// starve the url()-backed texts this mode exists to sample.
|
|
7360
|
+
if (options.imageOnly && !reasons.includes('image background')) continue;
|
|
6927
7361
|
|
|
6928
7362
|
const textColor = parseRgb(style.color);
|
|
6929
7363
|
const fontSize = parseFloat(style.fontSize) || 16;
|
|
@@ -7416,6 +7850,7 @@ if (IS_BROWSER) {
|
|
|
7416
7850
|
}
|
|
7417
7851
|
|
|
7418
7852
|
async function analyzeVisualContrast(options = {}) {
|
|
7853
|
+
// imageOnly is enforced inside the collector, before the candidate cap.
|
|
7419
7854
|
const candidates = collectVisualContrastCandidates(options);
|
|
7420
7855
|
const results = [];
|
|
7421
7856
|
const shouldScrollOffscreen = options.scrollOffscreen === true;
|
|
@@ -7501,9 +7936,16 @@ if (IS_BROWSER) {
|
|
|
7501
7936
|
|
|
7502
7937
|
function addBrowserFindings(groupMap, el, findings) {
|
|
7503
7938
|
if (!findings || findings.length === 0) return;
|
|
7939
|
+
// Element-scoped waivers: a data-impeccable-ignore ancestor suppresses
|
|
7940
|
+
// matching findings for its whole subtree. Applied at this choke point so
|
|
7941
|
+
// every per-element attribution (checks, layout, occlusion, rhythm)
|
|
7942
|
+
// honors it; page-level findings attributed to <body> pass through
|
|
7943
|
+
// untouched, since body has no ignoring ancestor.
|
|
7944
|
+
const kept = findings.filter(f => !scopedIgnoreActive(el, f.type));
|
|
7945
|
+
if (kept.length === 0) return;
|
|
7504
7946
|
const existing = groupMap.get(el);
|
|
7505
|
-
if (existing) existing.push(...
|
|
7506
|
-
else groupMap.set(el, [...
|
|
7947
|
+
if (existing) existing.push(...kept);
|
|
7948
|
+
else groupMap.set(el, [...kept]);
|
|
7507
7949
|
}
|
|
7508
7950
|
|
|
7509
7951
|
function browserFindingsFromMap(groupMap) {
|
|
@@ -7861,9 +8303,27 @@ if (IS_BROWSER) {
|
|
|
7861
8303
|
for (const node of docClone.querySelectorAll('[id^="impeccable-live-"]')) {
|
|
7862
8304
|
node.remove();
|
|
7863
8305
|
}
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
8306
|
+
// Regex findings that name a live selector resolve against the real DOM:
|
|
8307
|
+
// pseudo-element/class segments are stripped (the host element is the
|
|
8308
|
+
// anchor), a selector that matches nothing on this page drops the finding
|
|
8309
|
+
// (the CSS ships here, but the pattern never renders — the live DOM is
|
|
8310
|
+
// ground truth in the browser), and a match under a data-impeccable-ignore
|
|
8311
|
+
// ancestor is waived. Selector-less findings stay page-level.
|
|
8312
|
+
const scopedHtmlFindings = checkHtmlPatterns(docClone.outerHTML).filter(f => {
|
|
8313
|
+
if (!f.selector) return true;
|
|
8314
|
+
const query = String(f.selector).replace(/::?[a-zA-Z-]+(\([^)]*\))?/g, '').trim().replace(/,\s*(?=,|$)/g, '');
|
|
8315
|
+
if (!query || /^[,\s]*$/.test(query)) return true;
|
|
8316
|
+
let matches;
|
|
8317
|
+
try {
|
|
8318
|
+
matches = document.querySelectorAll(query);
|
|
8319
|
+
} catch {
|
|
8320
|
+
return true;
|
|
8321
|
+
}
|
|
8322
|
+
if (matches.length === 0) return false;
|
|
8323
|
+
return [...matches].some(el => !scopedIgnoreActive(el, f.id));
|
|
8324
|
+
});
|
|
8325
|
+
if (scopedHtmlFindings.length > 0) {
|
|
8326
|
+
const mapped = scopedHtmlFindings.map(f => {
|
|
7867
8327
|
const item = { type: f.id, detail: f.snippet };
|
|
7868
8328
|
if (f.severity) {
|
|
7869
8329
|
item.severity = f.severity;
|
|
@@ -7893,8 +8353,27 @@ if (IS_BROWSER) {
|
|
|
7893
8353
|
};
|
|
7894
8354
|
}
|
|
7895
8355
|
|
|
8356
|
+
// Visual contrast has three modes. Explicit true runs the full sampled
|
|
8357
|
+
// pass; explicit false disables it entirely (the deterministic-only mode
|
|
8358
|
+
// the test suites use). Unset — the default overlay run — samples ONLY
|
|
8359
|
+
// image-backed text: the one class the analytic walk deliberately skips,
|
|
8360
|
+
// because a url() layer's pixels are unknowable without looking. In-page
|
|
8361
|
+
// sampling draws the source image alone to a canvas (glyph ink never
|
|
8362
|
+
// pollutes it), and a cross-origin image without CORS reports unresolved
|
|
8363
|
+
// instead of guessing.
|
|
8364
|
+
function visualContrastMode(options = {}) {
|
|
8365
|
+
const explicit = typeof options.visualContrast === 'boolean'
|
|
8366
|
+
? options.visualContrast
|
|
8367
|
+
: typeof window.__IMPECCABLE_CONFIG__?.visualContrast === 'boolean'
|
|
8368
|
+
? window.__IMPECCABLE_CONFIG__.visualContrast
|
|
8369
|
+
: null;
|
|
8370
|
+
if (explicit === true) return 'full';
|
|
8371
|
+
if (explicit === false) return false;
|
|
8372
|
+
return 'image-only';
|
|
8373
|
+
}
|
|
8374
|
+
|
|
7896
8375
|
function shouldRunVisualContrast(options = {}) {
|
|
7897
|
-
return options
|
|
8376
|
+
return visualContrastMode(options) !== false;
|
|
7898
8377
|
}
|
|
7899
8378
|
|
|
7900
8379
|
function visualContrastOptions(options = {}) {
|
|
@@ -8071,6 +8550,7 @@ if (IS_BROWSER) {
|
|
|
8071
8550
|
return [];
|
|
8072
8551
|
}
|
|
8073
8552
|
const resolvedOptions = visualContrastOptions(options);
|
|
8553
|
+
if (visualContrastMode(options) === 'image-only') resolvedOptions.imageOnly = true;
|
|
8074
8554
|
const analyses = await analyzeVisualContrast(resolvedOptions);
|
|
8075
8555
|
if (runtime.generation && runtime.generation !== scanGeneration) return analyses;
|
|
8076
8556
|
lastVisualContrastAnalyses = analyses;
|