pm-workflow-studio 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (592) hide show
  1. package/.claude/CLAUDE.md +31 -0
  2. package/.claude/agents/demand-analyst.md +26 -0
  3. package/.claude/agents/dev-planner.md +22 -0
  4. package/.claude/agents/product-manager.md +25 -0
  5. package/.claude/agents/quality-reviewer.md +31 -0
  6. package/.claude/agents/tech-architect.md +22 -0
  7. package/.claude/agents/ui-designer.md +39 -0
  8. package/.claude/commands/pm-workflow/analyze.md +6 -0
  9. package/.claude/commands/pm-workflow/architect.md +6 -0
  10. package/.claude/commands/pm-workflow/deliver.md +6 -0
  11. package/.claude/commands/pm-workflow/design.md +6 -0
  12. package/.claude/commands/pm-workflow/help.md +6 -0
  13. package/.claude/commands/pm-workflow/init.md +8 -0
  14. package/.claude/commands/pm-workflow/plan.md +6 -0
  15. package/.claude/commands/pm-workflow/review.md +6 -0
  16. package/.claude/commands/pm-workflow/status.md +6 -0
  17. package/.claude/commands/pm-workflow.md +13 -0
  18. package/.claude/settings.json +15 -0
  19. package/.claude/skills/demand-analysis/SKILL.md +50 -0
  20. package/.claude/skills/demand-analysis/templates/handoff-prd.md +39 -0
  21. package/.claude/skills/demand-analysis/templates/prd.md +85 -0
  22. package/.claude/skills/dev-task-planning/SKILL.md +37 -0
  23. package/.claude/skills/dev-task-planning/templates/dev-tasks.md +54 -0
  24. package/.claude/skills/impeccable/SKILL.md +169 -0
  25. package/.claude/skills/impeccable/reference/adapt.md +190 -0
  26. package/.claude/skills/impeccable/reference/animate.md +175 -0
  27. package/.claude/skills/impeccable/reference/audit.md +133 -0
  28. package/.claude/skills/impeccable/reference/bolder.md +113 -0
  29. package/.claude/skills/impeccable/reference/brand.md +118 -0
  30. package/.claude/skills/impeccable/reference/clarify.md +174 -0
  31. package/.claude/skills/impeccable/reference/codex.md +105 -0
  32. package/.claude/skills/impeccable/reference/cognitive-load.md +106 -0
  33. package/.claude/skills/impeccable/reference/color-and-contrast.md +105 -0
  34. package/.claude/skills/impeccable/reference/colorize.md +154 -0
  35. package/.claude/skills/impeccable/reference/craft.md +123 -0
  36. package/.claude/skills/impeccable/reference/critique.md +236 -0
  37. package/.claude/skills/impeccable/reference/delight.md +302 -0
  38. package/.claude/skills/impeccable/reference/distill.md +111 -0
  39. package/.claude/skills/impeccable/reference/document.md +427 -0
  40. package/.claude/skills/impeccable/reference/extract.md +69 -0
  41. package/.claude/skills/impeccable/reference/harden.md +347 -0
  42. package/.claude/skills/impeccable/reference/heuristics-scoring.md +234 -0
  43. package/.claude/skills/impeccable/reference/interaction-design.md +195 -0
  44. package/.claude/skills/impeccable/reference/layout.md +141 -0
  45. package/.claude/skills/impeccable/reference/live.md +622 -0
  46. package/.claude/skills/impeccable/reference/motion-design.md +109 -0
  47. package/.claude/skills/impeccable/reference/onboard.md +234 -0
  48. package/.claude/skills/impeccable/reference/optimize.md +258 -0
  49. package/.claude/skills/impeccable/reference/overdrive.md +130 -0
  50. package/.claude/skills/impeccable/reference/personas.md +179 -0
  51. package/.claude/skills/impeccable/reference/polish.md +242 -0
  52. package/.claude/skills/impeccable/reference/product.md +62 -0
  53. package/.claude/skills/impeccable/reference/quieter.md +99 -0
  54. package/.claude/skills/impeccable/reference/responsive-design.md +114 -0
  55. package/.claude/skills/impeccable/reference/shape.md +165 -0
  56. package/.claude/skills/impeccable/reference/spatial-design.md +100 -0
  57. package/.claude/skills/impeccable/reference/teach.md +156 -0
  58. package/.claude/skills/impeccable/reference/typeset.md +124 -0
  59. package/.claude/skills/impeccable/reference/typography.md +159 -0
  60. package/.claude/skills/impeccable/reference/ux-writing.md +107 -0
  61. package/.claude/skills/impeccable/scripts/cleanup-deprecated.mjs +284 -0
  62. package/.claude/skills/impeccable/scripts/command-metadata.json +94 -0
  63. package/.claude/skills/impeccable/scripts/critique-storage.mjs +242 -0
  64. package/.claude/skills/impeccable/scripts/design-parser.mjs +820 -0
  65. package/.claude/skills/impeccable/scripts/detect-csp.mjs +198 -0
  66. package/.claude/skills/impeccable/scripts/detect.mjs +21 -0
  67. package/.claude/skills/impeccable/scripts/detector/browser/injected/index.mjs +1688 -0
  68. package/.claude/skills/impeccable/scripts/detector/cli/main.mjs +232 -0
  69. package/.claude/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +4030 -0
  70. package/.claude/skills/impeccable/scripts/detector/detect-antipatterns.mjs +43 -0
  71. package/.claude/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +251 -0
  72. package/.claude/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +420 -0
  73. package/.claude/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +954 -0
  74. package/.claude/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +174 -0
  75. package/.claude/skills/impeccable/scripts/detector/engines/visual/screenshot-contrast.mjs +189 -0
  76. package/.claude/skills/impeccable/scripts/detector/findings.mjs +12 -0
  77. package/.claude/skills/impeccable/scripts/detector/node/file-system.mjs +198 -0
  78. package/.claude/skills/impeccable/scripts/detector/profile/profiler.mjs +166 -0
  79. package/.claude/skills/impeccable/scripts/detector/registry/antipatterns.mjs +278 -0
  80. package/.claude/skills/impeccable/scripts/detector/rules/checks.mjs +1948 -0
  81. package/.claude/skills/impeccable/scripts/detector/shared/color.mjs +124 -0
  82. package/.claude/skills/impeccable/scripts/detector/shared/constants.mjs +101 -0
  83. package/.claude/skills/impeccable/scripts/detector/shared/page.mjs +7 -0
  84. package/.claude/skills/impeccable/scripts/impeccable-paths.mjs +110 -0
  85. package/.claude/skills/impeccable/scripts/is-generated.mjs +69 -0
  86. package/.claude/skills/impeccable/scripts/live-accept.mjs +595 -0
  87. package/.claude/skills/impeccable/scripts/live-browser-session.js +123 -0
  88. package/.claude/skills/impeccable/scripts/live-browser.js +4860 -0
  89. package/.claude/skills/impeccable/scripts/live-complete.mjs +75 -0
  90. package/.claude/skills/impeccable/scripts/live-completion.mjs +18 -0
  91. package/.claude/skills/impeccable/scripts/live-inject.mjs +446 -0
  92. package/.claude/skills/impeccable/scripts/live-poll.mjs +200 -0
  93. package/.claude/skills/impeccable/scripts/live-resume.mjs +48 -0
  94. package/.claude/skills/impeccable/scripts/live-server.mjs +838 -0
  95. package/.claude/skills/impeccable/scripts/live-session-store.mjs +254 -0
  96. package/.claude/skills/impeccable/scripts/live-status.mjs +47 -0
  97. package/.claude/skills/impeccable/scripts/live-wrap.mjs +632 -0
  98. package/.claude/skills/impeccable/scripts/live.mjs +247 -0
  99. package/.claude/skills/impeccable/scripts/load-context.mjs +141 -0
  100. package/.claude/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
  101. package/.claude/skills/impeccable/scripts/pin.mjs +214 -0
  102. package/.claude/skills/pm-workflow/SKILL.md +371 -0
  103. package/.claude/skills/pm-workflow/assets/design-themes/README.md +56 -0
  104. package/.claude/skills/pm-workflow/assets/design-themes/open-design/OPEN_DESIGN_COMMIT +1 -0
  105. package/.claude/skills/pm-workflow/assets/design-themes/open-design/OPEN_DESIGN_IMPORT.md +28 -0
  106. package/.claude/skills/pm-workflow/assets/design-themes/open-design/OPEN_DESIGN_LICENSE +201 -0
  107. package/.claude/skills/pm-workflow/assets/design-themes/open-design/README.md +103 -0
  108. package/.claude/skills/pm-workflow/assets/design-themes/open-design/agentic/DESIGN.md +71 -0
  109. package/.claude/skills/pm-workflow/assets/design-themes/open-design/airbnb/DESIGN.md +393 -0
  110. package/.claude/skills/pm-workflow/assets/design-themes/open-design/airbnb/examples.html +23 -0
  111. package/.claude/skills/pm-workflow/assets/design-themes/open-design/airtable/DESIGN.md +92 -0
  112. package/.claude/skills/pm-workflow/assets/design-themes/open-design/apple/DESIGN.md +250 -0
  113. package/.claude/skills/pm-workflow/assets/design-themes/open-design/apple/examples.html +23 -0
  114. package/.claude/skills/pm-workflow/assets/design-themes/open-design/application/DESIGN.md +71 -0
  115. package/.claude/skills/pm-workflow/assets/design-themes/open-design/arc/DESIGN.md +152 -0
  116. package/.claude/skills/pm-workflow/assets/design-themes/open-design/artistic/DESIGN.md +71 -0
  117. package/.claude/skills/pm-workflow/assets/design-themes/open-design/atelier-zero/DESIGN.md +316 -0
  118. package/.claude/skills/pm-workflow/assets/design-themes/open-design/bento/DESIGN.md +71 -0
  119. package/.claude/skills/pm-workflow/assets/design-themes/open-design/binance/DESIGN.md +348 -0
  120. package/.claude/skills/pm-workflow/assets/design-themes/open-design/bmw/DESIGN.md +183 -0
  121. package/.claude/skills/pm-workflow/assets/design-themes/open-design/bmw-m/DESIGN.md +246 -0
  122. package/.claude/skills/pm-workflow/assets/design-themes/open-design/bold/DESIGN.md +71 -0
  123. package/.claude/skills/pm-workflow/assets/design-themes/open-design/brutalism/DESIGN.md +71 -0
  124. package/.claude/skills/pm-workflow/assets/design-themes/open-design/bugatti/DESIGN.md +271 -0
  125. package/.claude/skills/pm-workflow/assets/design-themes/open-design/cafe/DESIGN.md +71 -0
  126. package/.claude/skills/pm-workflow/assets/design-themes/open-design/cal/DESIGN.md +262 -0
  127. package/.claude/skills/pm-workflow/assets/design-themes/open-design/canva/DESIGN.md +157 -0
  128. package/.claude/skills/pm-workflow/assets/design-themes/open-design/cisco/DESIGN.md +201 -0
  129. package/.claude/skills/pm-workflow/assets/design-themes/open-design/claude/DESIGN.md +315 -0
  130. package/.claude/skills/pm-workflow/assets/design-themes/open-design/clay/DESIGN.md +307 -0
  131. package/.claude/skills/pm-workflow/assets/design-themes/open-design/claymorphism/DESIGN.md +71 -0
  132. package/.claude/skills/pm-workflow/assets/design-themes/open-design/clean/DESIGN.md +71 -0
  133. package/.claude/skills/pm-workflow/assets/design-themes/open-design/clickhouse/DESIGN.md +284 -0
  134. package/.claude/skills/pm-workflow/assets/design-themes/open-design/cohere/DESIGN.md +269 -0
  135. package/.claude/skills/pm-workflow/assets/design-themes/open-design/coinbase/DESIGN.md +132 -0
  136. package/.claude/skills/pm-workflow/assets/design-themes/open-design/colorful/DESIGN.md +71 -0
  137. package/.claude/skills/pm-workflow/assets/design-themes/open-design/composio/DESIGN.md +310 -0
  138. package/.claude/skills/pm-workflow/assets/design-themes/open-design/contemporary/DESIGN.md +71 -0
  139. package/.claude/skills/pm-workflow/assets/design-themes/open-design/corporate/DESIGN.md +71 -0
  140. package/.claude/skills/pm-workflow/assets/design-themes/open-design/cosmic/DESIGN.md +71 -0
  141. package/.claude/skills/pm-workflow/assets/design-themes/open-design/creative/DESIGN.md +71 -0
  142. package/.claude/skills/pm-workflow/assets/design-themes/open-design/cursor/DESIGN.md +312 -0
  143. package/.claude/skills/pm-workflow/assets/design-themes/open-design/dashboard/DESIGN.md +71 -0
  144. package/.claude/skills/pm-workflow/assets/design-themes/open-design/default/DESIGN.md +62 -0
  145. package/.claude/skills/pm-workflow/assets/design-themes/open-design/discord/DESIGN.md +162 -0
  146. package/.claude/skills/pm-workflow/assets/design-themes/open-design/dithered/DESIGN.md +71 -0
  147. package/.claude/skills/pm-workflow/assets/design-themes/open-design/doodle/DESIGN.md +71 -0
  148. package/.claude/skills/pm-workflow/assets/design-themes/open-design/dramatic/DESIGN.md +71 -0
  149. package/.claude/skills/pm-workflow/assets/design-themes/open-design/duolingo/DESIGN.md +154 -0
  150. package/.claude/skills/pm-workflow/assets/design-themes/open-design/editorial/DESIGN.md +71 -0
  151. package/.claude/skills/pm-workflow/assets/design-themes/open-design/elegant/DESIGN.md +71 -0
  152. package/.claude/skills/pm-workflow/assets/design-themes/open-design/elevenlabs/DESIGN.md +268 -0
  153. package/.claude/skills/pm-workflow/assets/design-themes/open-design/energetic/DESIGN.md +72 -0
  154. package/.claude/skills/pm-workflow/assets/design-themes/open-design/enterprise/DESIGN.md +71 -0
  155. package/.claude/skills/pm-workflow/assets/design-themes/open-design/expo/DESIGN.md +284 -0
  156. package/.claude/skills/pm-workflow/assets/design-themes/open-design/expressive/DESIGN.md +71 -0
  157. package/.claude/skills/pm-workflow/assets/design-themes/open-design/fantasy/DESIGN.md +71 -0
  158. package/.claude/skills/pm-workflow/assets/design-themes/open-design/ferrari/DESIGN.md +317 -0
  159. package/.claude/skills/pm-workflow/assets/design-themes/open-design/figma/DESIGN.md +223 -0
  160. package/.claude/skills/pm-workflow/assets/design-themes/open-design/flat/DESIGN.md +71 -0
  161. package/.claude/skills/pm-workflow/assets/design-themes/open-design/framer/DESIGN.md +249 -0
  162. package/.claude/skills/pm-workflow/assets/design-themes/open-design/friendly/DESIGN.md +71 -0
  163. package/.claude/skills/pm-workflow/assets/design-themes/open-design/futuristic/DESIGN.md +71 -0
  164. package/.claude/skills/pm-workflow/assets/design-themes/open-design/glassmorphism/DESIGN.md +71 -0
  165. package/.claude/skills/pm-workflow/assets/design-themes/open-design/gradient/DESIGN.md +71 -0
  166. package/.claude/skills/pm-workflow/assets/design-themes/open-design/hashicorp/DESIGN.md +281 -0
  167. package/.claude/skills/pm-workflow/assets/design-themes/open-design/hud/DESIGN.md +173 -0
  168. package/.claude/skills/pm-workflow/assets/design-themes/open-design/huggingface/DESIGN.md +149 -0
  169. package/.claude/skills/pm-workflow/assets/design-themes/open-design/huggingface/examples.html +11 -0
  170. package/.claude/skills/pm-workflow/assets/design-themes/open-design/intercom/DESIGN.md +149 -0
  171. package/.claude/skills/pm-workflow/assets/design-themes/open-design/kami/DESIGN.md +410 -0
  172. package/.claude/skills/pm-workflow/assets/design-themes/open-design/kraken/DESIGN.md +128 -0
  173. package/.claude/skills/pm-workflow/assets/design-themes/open-design/lamborghini/DESIGN.md +291 -0
  174. package/.claude/skills/pm-workflow/assets/design-themes/open-design/levels/DESIGN.md +71 -0
  175. package/.claude/skills/pm-workflow/assets/design-themes/open-design/linear-app/DESIGN.md +370 -0
  176. package/.claude/skills/pm-workflow/assets/design-themes/open-design/linear-app/examples.html +56 -0
  177. package/.claude/skills/pm-workflow/assets/design-themes/open-design/lingo/DESIGN.md +71 -0
  178. package/.claude/skills/pm-workflow/assets/design-themes/open-design/loom/DESIGN.md +201 -0
  179. package/.claude/skills/pm-workflow/assets/design-themes/open-design/lovable/DESIGN.md +301 -0
  180. package/.claude/skills/pm-workflow/assets/design-themes/open-design/luxury/DESIGN.md +71 -0
  181. package/.claude/skills/pm-workflow/assets/design-themes/open-design/mastercard/DESIGN.md +368 -0
  182. package/.claude/skills/pm-workflow/assets/design-themes/open-design/material/DESIGN.md +71 -0
  183. package/.claude/skills/pm-workflow/assets/design-themes/open-design/material/examples.html +11 -0
  184. package/.claude/skills/pm-workflow/assets/design-themes/open-design/meta/DESIGN.md +369 -0
  185. package/.claude/skills/pm-workflow/assets/design-themes/open-design/minimal/DESIGN.md +71 -0
  186. package/.claude/skills/pm-workflow/assets/design-themes/open-design/minimax/DESIGN.md +260 -0
  187. package/.claude/skills/pm-workflow/assets/design-themes/open-design/mintlify/DESIGN.md +329 -0
  188. package/.claude/skills/pm-workflow/assets/design-themes/open-design/miro/DESIGN.md +111 -0
  189. package/.claude/skills/pm-workflow/assets/design-themes/open-design/mission-control/DESIGN.md +474 -0
  190. package/.claude/skills/pm-workflow/assets/design-themes/open-design/mistral-ai/DESIGN.md +264 -0
  191. package/.claude/skills/pm-workflow/assets/design-themes/open-design/modern/DESIGN.md +71 -0
  192. package/.claude/skills/pm-workflow/assets/design-themes/open-design/mongodb/DESIGN.md +269 -0
  193. package/.claude/skills/pm-workflow/assets/design-themes/open-design/mongodb/examples.html +14 -0
  194. package/.claude/skills/pm-workflow/assets/design-themes/open-design/mono/DESIGN.md +71 -0
  195. package/.claude/skills/pm-workflow/assets/design-themes/open-design/neobrutalism/DESIGN.md +71 -0
  196. package/.claude/skills/pm-workflow/assets/design-themes/open-design/neon/DESIGN.md +71 -0
  197. package/.claude/skills/pm-workflow/assets/design-themes/open-design/neumorphism/DESIGN.md +71 -0
  198. package/.claude/skills/pm-workflow/assets/design-themes/open-design/nike/DESIGN.md +366 -0
  199. package/.claude/skills/pm-workflow/assets/design-themes/open-design/notion/DESIGN.md +312 -0
  200. package/.claude/skills/pm-workflow/assets/design-themes/open-design/notion/examples.html +64 -0
  201. package/.claude/skills/pm-workflow/assets/design-themes/open-design/nvidia/DESIGN.md +296 -0
  202. package/.claude/skills/pm-workflow/assets/design-themes/open-design/ollama/DESIGN.md +270 -0
  203. package/.claude/skills/pm-workflow/assets/design-themes/open-design/openai/DESIGN.md +140 -0
  204. package/.claude/skills/pm-workflow/assets/design-themes/open-design/openai/examples.html +14 -0
  205. package/.claude/skills/pm-workflow/assets/design-themes/open-design/opencode-ai/DESIGN.md +284 -0
  206. package/.claude/skills/pm-workflow/assets/design-themes/open-design/pacman/DESIGN.md +71 -0
  207. package/.claude/skills/pm-workflow/assets/design-themes/open-design/paper/DESIGN.md +71 -0
  208. package/.claude/skills/pm-workflow/assets/design-themes/open-design/perspective/DESIGN.md +71 -0
  209. package/.claude/skills/pm-workflow/assets/design-themes/open-design/pinterest/DESIGN.md +233 -0
  210. package/.claude/skills/pm-workflow/assets/design-themes/open-design/playstation/DESIGN.md +367 -0
  211. package/.claude/skills/pm-workflow/assets/design-themes/open-design/posthog/DESIGN.md +259 -0
  212. package/.claude/skills/pm-workflow/assets/design-themes/open-design/premium/DESIGN.md +71 -0
  213. package/.claude/skills/pm-workflow/assets/design-themes/open-design/professional/DESIGN.md +71 -0
  214. package/.claude/skills/pm-workflow/assets/design-themes/open-design/publication/DESIGN.md +71 -0
  215. package/.claude/skills/pm-workflow/assets/design-themes/open-design/raycast/DESIGN.md +271 -0
  216. package/.claude/skills/pm-workflow/assets/design-themes/open-design/raycast/examples.html +11 -0
  217. package/.claude/skills/pm-workflow/assets/design-themes/open-design/refined/DESIGN.md +71 -0
  218. package/.claude/skills/pm-workflow/assets/design-themes/open-design/renault/DESIGN.md +314 -0
  219. package/.claude/skills/pm-workflow/assets/design-themes/open-design/replicate/DESIGN.md +264 -0
  220. package/.claude/skills/pm-workflow/assets/design-themes/open-design/resend/DESIGN.md +306 -0
  221. package/.claude/skills/pm-workflow/assets/design-themes/open-design/retro/DESIGN.md +71 -0
  222. package/.claude/skills/pm-workflow/assets/design-themes/open-design/revolut/DESIGN.md +188 -0
  223. package/.claude/skills/pm-workflow/assets/design-themes/open-design/runwayml/DESIGN.md +247 -0
  224. package/.claude/skills/pm-workflow/assets/design-themes/open-design/sanity/DESIGN.md +360 -0
  225. package/.claude/skills/pm-workflow/assets/design-themes/open-design/sentry/DESIGN.md +265 -0
  226. package/.claude/skills/pm-workflow/assets/design-themes/open-design/shadcn/DESIGN.md +71 -0
  227. package/.claude/skills/pm-workflow/assets/design-themes/open-design/shadcn/examples.html +24 -0
  228. package/.claude/skills/pm-workflow/assets/design-themes/open-design/shopify/DESIGN.md +353 -0
  229. package/.claude/skills/pm-workflow/assets/design-themes/open-design/shopify/examples.html +11 -0
  230. package/.claude/skills/pm-workflow/assets/design-themes/open-design/simple/DESIGN.md +71 -0
  231. package/.claude/skills/pm-workflow/assets/design-themes/open-design/skeumorphism/DESIGN.md +71 -0
  232. package/.claude/skills/pm-workflow/assets/design-themes/open-design/sleek/DESIGN.md +71 -0
  233. package/.claude/skills/pm-workflow/assets/design-themes/open-design/spacex/DESIGN.md +197 -0
  234. package/.claude/skills/pm-workflow/assets/design-themes/open-design/spacious/DESIGN.md +71 -0
  235. package/.claude/skills/pm-workflow/assets/design-themes/open-design/spotify/DESIGN.md +249 -0
  236. package/.claude/skills/pm-workflow/assets/design-themes/open-design/starbucks/DESIGN.md +583 -0
  237. package/.claude/skills/pm-workflow/assets/design-themes/open-design/storytelling/DESIGN.md +71 -0
  238. package/.claude/skills/pm-workflow/assets/design-themes/open-design/stripe/DESIGN.md +325 -0
  239. package/.claude/skills/pm-workflow/assets/design-themes/open-design/stripe/examples.html +58 -0
  240. package/.claude/skills/pm-workflow/assets/design-themes/open-design/supabase/DESIGN.md +258 -0
  241. package/.claude/skills/pm-workflow/assets/design-themes/open-design/supabase/examples.html +26 -0
  242. package/.claude/skills/pm-workflow/assets/design-themes/open-design/superhuman/DESIGN.md +255 -0
  243. package/.claude/skills/pm-workflow/assets/design-themes/open-design/tesla/DESIGN.md +289 -0
  244. package/.claude/skills/pm-workflow/assets/design-themes/open-design/tetris/DESIGN.md +71 -0
  245. package/.claude/skills/pm-workflow/assets/design-themes/open-design/theverge/DESIGN.md +342 -0
  246. package/.claude/skills/pm-workflow/assets/design-themes/open-design/together-ai/DESIGN.md +266 -0
  247. package/.claude/skills/pm-workflow/assets/design-themes/open-design/totality-festival/DESIGN.md +206 -0
  248. package/.claude/skills/pm-workflow/assets/design-themes/open-design/trading-terminal/DESIGN.md +178 -0
  249. package/.claude/skills/pm-workflow/assets/design-themes/open-design/uber/DESIGN.md +298 -0
  250. package/.claude/skills/pm-workflow/assets/design-themes/open-design/urdu/DESIGN.md +1002 -0
  251. package/.claude/skills/pm-workflow/assets/design-themes/open-design/vercel/DESIGN.md +313 -0
  252. package/.claude/skills/pm-workflow/assets/design-themes/open-design/vercel/examples.html +55 -0
  253. package/.claude/skills/pm-workflow/assets/design-themes/open-design/vibrant/DESIGN.md +71 -0
  254. package/.claude/skills/pm-workflow/assets/design-themes/open-design/vintage/DESIGN.md +71 -0
  255. package/.claude/skills/pm-workflow/assets/design-themes/open-design/vodafone/DESIGN.md +426 -0
  256. package/.claude/skills/pm-workflow/assets/design-themes/open-design/voltagent/DESIGN.md +326 -0
  257. package/.claude/skills/pm-workflow/assets/design-themes/open-design/warm-editorial/DESIGN.md +65 -0
  258. package/.claude/skills/pm-workflow/assets/design-themes/open-design/warp/DESIGN.md +256 -0
  259. package/.claude/skills/pm-workflow/assets/design-themes/open-design/webex/DESIGN.md +207 -0
  260. package/.claude/skills/pm-workflow/assets/design-themes/open-design/webflow/DESIGN.md +95 -0
  261. package/.claude/skills/pm-workflow/assets/design-themes/open-design/webflow/examples.html +11 -0
  262. package/.claude/skills/pm-workflow/assets/design-themes/open-design/wired/DESIGN.md +281 -0
  263. package/.claude/skills/pm-workflow/assets/design-themes/open-design/wise/DESIGN.md +176 -0
  264. package/.claude/skills/pm-workflow/assets/design-themes/open-design/x-ai/DESIGN.md +260 -0
  265. package/.claude/skills/pm-workflow/assets/design-themes/open-design/x-ai/examples.html +12 -0
  266. package/.claude/skills/pm-workflow/assets/design-themes/open-design/xiaohongshu/DESIGN.md +402 -0
  267. package/.claude/skills/pm-workflow/assets/design-themes/open-design/zapier/DESIGN.md +331 -0
  268. package/.claude/skills/pm-workflow/assets/design-themes/revenuecat/DESIGN.md +209 -0
  269. package/.claude/skills/pm-workflow/assets/design-themes/revenuecat/examples.html +122 -0
  270. package/.claude/skills/pm-workflow/assets/design-themes/vben/DESIGN.md +685 -0
  271. package/.claude/skills/pm-workflow/assets/design-themes/vben/examples.html +155 -0
  272. package/.claude/skills/pm-workflow/assets/vendor/flow-viewer/MERMAID_LICENSE +21 -0
  273. package/.claude/skills/pm-workflow/assets/vendor/flow-viewer/SVG_PAN_ZOOM_LICENSE +23 -0
  274. package/.claude/skills/pm-workflow/assets/vendor/flow-viewer/THIRD_PARTY_LICENSES.md +21 -0
  275. package/.claude/skills/pm-workflow/assets/vendor/flow-viewer/mermaid.min.js +3298 -0
  276. package/.claude/skills/pm-workflow/assets/vendor/flow-viewer/svg-pan-zoom.min.js +3 -0
  277. package/.claude/skills/pm-workflow/references/commands/analyze.md +39 -0
  278. package/.claude/skills/pm-workflow/references/commands/architect.md +41 -0
  279. package/.claude/skills/pm-workflow/references/commands/deliver.md +29 -0
  280. package/.claude/skills/pm-workflow/references/commands/design.md +92 -0
  281. package/.claude/skills/pm-workflow/references/commands/help.md +24 -0
  282. package/.claude/skills/pm-workflow/references/commands/init.md +40 -0
  283. package/.claude/skills/pm-workflow/references/commands/plan.md +41 -0
  284. package/.claude/skills/pm-workflow/references/commands/review.md +33 -0
  285. package/.claude/skills/pm-workflow/references/commands/status.md +20 -0
  286. package/.claude/skills/pm-workflow/scripts/package_delivery.js +195 -0
  287. package/.claude/skills/pm-workflow/scripts/review_stage.js +622 -0
  288. package/.claude/skills/quality-review/SKILL.md +49 -0
  289. package/.claude/skills/quality-review/templates/review-stage.md +39 -0
  290. package/.claude/skills/tech-architecture/SKILL.md +49 -0
  291. package/.claude/skills/tech-architecture/templates/handoff-architecture.md +28 -0
  292. package/.claude/skills/tech-architecture/templates/tech-architecture.md +54 -0
  293. package/.claude/skills/ui-prototype-design/SKILL.md +125 -0
  294. package/.claude/skills/ui-prototype-design/templates/handoff-ui.md +40 -0
  295. package/.claude/skills/ui-prototype-design/templates/prototype-review.md +57 -0
  296. package/.claude/skills/ui-prototype-design/templates/ui-design.md +142 -0
  297. package/.codex/SKILL.md +374 -0
  298. package/.codex/agents/demand-analyst.toml +87 -0
  299. package/.codex/agents/dev-planner.toml +47 -0
  300. package/.codex/agents/openai.yaml +4 -0
  301. package/.codex/agents/product-manager.toml +81 -0
  302. package/.codex/agents/quality-reviewer.toml +76 -0
  303. package/.codex/agents/tech-architect.toml +57 -0
  304. package/.codex/agents/ui-designer.toml +132 -0
  305. package/.codex/assets/design-themes/README.md +56 -0
  306. package/.codex/assets/design-themes/open-design/OPEN_DESIGN_COMMIT +1 -0
  307. package/.codex/assets/design-themes/open-design/OPEN_DESIGN_IMPORT.md +28 -0
  308. package/.codex/assets/design-themes/open-design/OPEN_DESIGN_LICENSE +201 -0
  309. package/.codex/assets/design-themes/open-design/README.md +103 -0
  310. package/.codex/assets/design-themes/open-design/agentic/DESIGN.md +71 -0
  311. package/.codex/assets/design-themes/open-design/airbnb/DESIGN.md +393 -0
  312. package/.codex/assets/design-themes/open-design/airbnb/examples.html +23 -0
  313. package/.codex/assets/design-themes/open-design/airtable/DESIGN.md +92 -0
  314. package/.codex/assets/design-themes/open-design/apple/DESIGN.md +250 -0
  315. package/.codex/assets/design-themes/open-design/apple/examples.html +23 -0
  316. package/.codex/assets/design-themes/open-design/application/DESIGN.md +71 -0
  317. package/.codex/assets/design-themes/open-design/arc/DESIGN.md +152 -0
  318. package/.codex/assets/design-themes/open-design/artistic/DESIGN.md +71 -0
  319. package/.codex/assets/design-themes/open-design/atelier-zero/DESIGN.md +316 -0
  320. package/.codex/assets/design-themes/open-design/bento/DESIGN.md +71 -0
  321. package/.codex/assets/design-themes/open-design/binance/DESIGN.md +348 -0
  322. package/.codex/assets/design-themes/open-design/bmw/DESIGN.md +183 -0
  323. package/.codex/assets/design-themes/open-design/bmw-m/DESIGN.md +246 -0
  324. package/.codex/assets/design-themes/open-design/bold/DESIGN.md +71 -0
  325. package/.codex/assets/design-themes/open-design/brutalism/DESIGN.md +71 -0
  326. package/.codex/assets/design-themes/open-design/bugatti/DESIGN.md +271 -0
  327. package/.codex/assets/design-themes/open-design/cafe/DESIGN.md +71 -0
  328. package/.codex/assets/design-themes/open-design/cal/DESIGN.md +262 -0
  329. package/.codex/assets/design-themes/open-design/canva/DESIGN.md +157 -0
  330. package/.codex/assets/design-themes/open-design/cisco/DESIGN.md +201 -0
  331. package/.codex/assets/design-themes/open-design/claude/DESIGN.md +315 -0
  332. package/.codex/assets/design-themes/open-design/clay/DESIGN.md +307 -0
  333. package/.codex/assets/design-themes/open-design/claymorphism/DESIGN.md +71 -0
  334. package/.codex/assets/design-themes/open-design/clean/DESIGN.md +71 -0
  335. package/.codex/assets/design-themes/open-design/clickhouse/DESIGN.md +284 -0
  336. package/.codex/assets/design-themes/open-design/cohere/DESIGN.md +269 -0
  337. package/.codex/assets/design-themes/open-design/coinbase/DESIGN.md +132 -0
  338. package/.codex/assets/design-themes/open-design/colorful/DESIGN.md +71 -0
  339. package/.codex/assets/design-themes/open-design/composio/DESIGN.md +310 -0
  340. package/.codex/assets/design-themes/open-design/contemporary/DESIGN.md +71 -0
  341. package/.codex/assets/design-themes/open-design/corporate/DESIGN.md +71 -0
  342. package/.codex/assets/design-themes/open-design/cosmic/DESIGN.md +71 -0
  343. package/.codex/assets/design-themes/open-design/creative/DESIGN.md +71 -0
  344. package/.codex/assets/design-themes/open-design/cursor/DESIGN.md +312 -0
  345. package/.codex/assets/design-themes/open-design/dashboard/DESIGN.md +71 -0
  346. package/.codex/assets/design-themes/open-design/default/DESIGN.md +62 -0
  347. package/.codex/assets/design-themes/open-design/discord/DESIGN.md +162 -0
  348. package/.codex/assets/design-themes/open-design/dithered/DESIGN.md +71 -0
  349. package/.codex/assets/design-themes/open-design/doodle/DESIGN.md +71 -0
  350. package/.codex/assets/design-themes/open-design/dramatic/DESIGN.md +71 -0
  351. package/.codex/assets/design-themes/open-design/duolingo/DESIGN.md +154 -0
  352. package/.codex/assets/design-themes/open-design/editorial/DESIGN.md +71 -0
  353. package/.codex/assets/design-themes/open-design/elegant/DESIGN.md +71 -0
  354. package/.codex/assets/design-themes/open-design/elevenlabs/DESIGN.md +268 -0
  355. package/.codex/assets/design-themes/open-design/energetic/DESIGN.md +72 -0
  356. package/.codex/assets/design-themes/open-design/enterprise/DESIGN.md +71 -0
  357. package/.codex/assets/design-themes/open-design/expo/DESIGN.md +284 -0
  358. package/.codex/assets/design-themes/open-design/expressive/DESIGN.md +71 -0
  359. package/.codex/assets/design-themes/open-design/fantasy/DESIGN.md +71 -0
  360. package/.codex/assets/design-themes/open-design/ferrari/DESIGN.md +317 -0
  361. package/.codex/assets/design-themes/open-design/figma/DESIGN.md +223 -0
  362. package/.codex/assets/design-themes/open-design/flat/DESIGN.md +71 -0
  363. package/.codex/assets/design-themes/open-design/framer/DESIGN.md +249 -0
  364. package/.codex/assets/design-themes/open-design/friendly/DESIGN.md +71 -0
  365. package/.codex/assets/design-themes/open-design/futuristic/DESIGN.md +71 -0
  366. package/.codex/assets/design-themes/open-design/glassmorphism/DESIGN.md +71 -0
  367. package/.codex/assets/design-themes/open-design/gradient/DESIGN.md +71 -0
  368. package/.codex/assets/design-themes/open-design/hashicorp/DESIGN.md +281 -0
  369. package/.codex/assets/design-themes/open-design/hud/DESIGN.md +173 -0
  370. package/.codex/assets/design-themes/open-design/huggingface/DESIGN.md +149 -0
  371. package/.codex/assets/design-themes/open-design/huggingface/examples.html +11 -0
  372. package/.codex/assets/design-themes/open-design/intercom/DESIGN.md +149 -0
  373. package/.codex/assets/design-themes/open-design/kami/DESIGN.md +410 -0
  374. package/.codex/assets/design-themes/open-design/kraken/DESIGN.md +128 -0
  375. package/.codex/assets/design-themes/open-design/lamborghini/DESIGN.md +291 -0
  376. package/.codex/assets/design-themes/open-design/levels/DESIGN.md +71 -0
  377. package/.codex/assets/design-themes/open-design/linear-app/DESIGN.md +370 -0
  378. package/.codex/assets/design-themes/open-design/linear-app/examples.html +56 -0
  379. package/.codex/assets/design-themes/open-design/lingo/DESIGN.md +71 -0
  380. package/.codex/assets/design-themes/open-design/loom/DESIGN.md +201 -0
  381. package/.codex/assets/design-themes/open-design/lovable/DESIGN.md +301 -0
  382. package/.codex/assets/design-themes/open-design/luxury/DESIGN.md +71 -0
  383. package/.codex/assets/design-themes/open-design/mastercard/DESIGN.md +368 -0
  384. package/.codex/assets/design-themes/open-design/material/DESIGN.md +71 -0
  385. package/.codex/assets/design-themes/open-design/material/examples.html +11 -0
  386. package/.codex/assets/design-themes/open-design/meta/DESIGN.md +369 -0
  387. package/.codex/assets/design-themes/open-design/minimal/DESIGN.md +71 -0
  388. package/.codex/assets/design-themes/open-design/minimax/DESIGN.md +260 -0
  389. package/.codex/assets/design-themes/open-design/mintlify/DESIGN.md +329 -0
  390. package/.codex/assets/design-themes/open-design/miro/DESIGN.md +111 -0
  391. package/.codex/assets/design-themes/open-design/mission-control/DESIGN.md +474 -0
  392. package/.codex/assets/design-themes/open-design/mistral-ai/DESIGN.md +264 -0
  393. package/.codex/assets/design-themes/open-design/modern/DESIGN.md +71 -0
  394. package/.codex/assets/design-themes/open-design/mongodb/DESIGN.md +269 -0
  395. package/.codex/assets/design-themes/open-design/mongodb/examples.html +14 -0
  396. package/.codex/assets/design-themes/open-design/mono/DESIGN.md +71 -0
  397. package/.codex/assets/design-themes/open-design/neobrutalism/DESIGN.md +71 -0
  398. package/.codex/assets/design-themes/open-design/neon/DESIGN.md +71 -0
  399. package/.codex/assets/design-themes/open-design/neumorphism/DESIGN.md +71 -0
  400. package/.codex/assets/design-themes/open-design/nike/DESIGN.md +366 -0
  401. package/.codex/assets/design-themes/open-design/notion/DESIGN.md +312 -0
  402. package/.codex/assets/design-themes/open-design/notion/examples.html +64 -0
  403. package/.codex/assets/design-themes/open-design/nvidia/DESIGN.md +296 -0
  404. package/.codex/assets/design-themes/open-design/ollama/DESIGN.md +270 -0
  405. package/.codex/assets/design-themes/open-design/openai/DESIGN.md +140 -0
  406. package/.codex/assets/design-themes/open-design/openai/examples.html +14 -0
  407. package/.codex/assets/design-themes/open-design/opencode-ai/DESIGN.md +284 -0
  408. package/.codex/assets/design-themes/open-design/pacman/DESIGN.md +71 -0
  409. package/.codex/assets/design-themes/open-design/paper/DESIGN.md +71 -0
  410. package/.codex/assets/design-themes/open-design/perspective/DESIGN.md +71 -0
  411. package/.codex/assets/design-themes/open-design/pinterest/DESIGN.md +233 -0
  412. package/.codex/assets/design-themes/open-design/playstation/DESIGN.md +367 -0
  413. package/.codex/assets/design-themes/open-design/posthog/DESIGN.md +259 -0
  414. package/.codex/assets/design-themes/open-design/premium/DESIGN.md +71 -0
  415. package/.codex/assets/design-themes/open-design/professional/DESIGN.md +71 -0
  416. package/.codex/assets/design-themes/open-design/publication/DESIGN.md +71 -0
  417. package/.codex/assets/design-themes/open-design/raycast/DESIGN.md +271 -0
  418. package/.codex/assets/design-themes/open-design/raycast/examples.html +11 -0
  419. package/.codex/assets/design-themes/open-design/refined/DESIGN.md +71 -0
  420. package/.codex/assets/design-themes/open-design/renault/DESIGN.md +314 -0
  421. package/.codex/assets/design-themes/open-design/replicate/DESIGN.md +264 -0
  422. package/.codex/assets/design-themes/open-design/resend/DESIGN.md +306 -0
  423. package/.codex/assets/design-themes/open-design/retro/DESIGN.md +71 -0
  424. package/.codex/assets/design-themes/open-design/revolut/DESIGN.md +188 -0
  425. package/.codex/assets/design-themes/open-design/runwayml/DESIGN.md +247 -0
  426. package/.codex/assets/design-themes/open-design/sanity/DESIGN.md +360 -0
  427. package/.codex/assets/design-themes/open-design/sentry/DESIGN.md +265 -0
  428. package/.codex/assets/design-themes/open-design/shadcn/DESIGN.md +71 -0
  429. package/.codex/assets/design-themes/open-design/shadcn/examples.html +24 -0
  430. package/.codex/assets/design-themes/open-design/shopify/DESIGN.md +353 -0
  431. package/.codex/assets/design-themes/open-design/shopify/examples.html +11 -0
  432. package/.codex/assets/design-themes/open-design/simple/DESIGN.md +71 -0
  433. package/.codex/assets/design-themes/open-design/skeumorphism/DESIGN.md +71 -0
  434. package/.codex/assets/design-themes/open-design/sleek/DESIGN.md +71 -0
  435. package/.codex/assets/design-themes/open-design/spacex/DESIGN.md +197 -0
  436. package/.codex/assets/design-themes/open-design/spacious/DESIGN.md +71 -0
  437. package/.codex/assets/design-themes/open-design/spotify/DESIGN.md +249 -0
  438. package/.codex/assets/design-themes/open-design/starbucks/DESIGN.md +583 -0
  439. package/.codex/assets/design-themes/open-design/storytelling/DESIGN.md +71 -0
  440. package/.codex/assets/design-themes/open-design/stripe/DESIGN.md +325 -0
  441. package/.codex/assets/design-themes/open-design/stripe/examples.html +58 -0
  442. package/.codex/assets/design-themes/open-design/supabase/DESIGN.md +258 -0
  443. package/.codex/assets/design-themes/open-design/supabase/examples.html +26 -0
  444. package/.codex/assets/design-themes/open-design/superhuman/DESIGN.md +255 -0
  445. package/.codex/assets/design-themes/open-design/tesla/DESIGN.md +289 -0
  446. package/.codex/assets/design-themes/open-design/tetris/DESIGN.md +71 -0
  447. package/.codex/assets/design-themes/open-design/theverge/DESIGN.md +342 -0
  448. package/.codex/assets/design-themes/open-design/together-ai/DESIGN.md +266 -0
  449. package/.codex/assets/design-themes/open-design/totality-festival/DESIGN.md +206 -0
  450. package/.codex/assets/design-themes/open-design/trading-terminal/DESIGN.md +178 -0
  451. package/.codex/assets/design-themes/open-design/uber/DESIGN.md +298 -0
  452. package/.codex/assets/design-themes/open-design/urdu/DESIGN.md +1002 -0
  453. package/.codex/assets/design-themes/open-design/vercel/DESIGN.md +313 -0
  454. package/.codex/assets/design-themes/open-design/vercel/examples.html +55 -0
  455. package/.codex/assets/design-themes/open-design/vibrant/DESIGN.md +71 -0
  456. package/.codex/assets/design-themes/open-design/vintage/DESIGN.md +71 -0
  457. package/.codex/assets/design-themes/open-design/vodafone/DESIGN.md +426 -0
  458. package/.codex/assets/design-themes/open-design/voltagent/DESIGN.md +326 -0
  459. package/.codex/assets/design-themes/open-design/warm-editorial/DESIGN.md +65 -0
  460. package/.codex/assets/design-themes/open-design/warp/DESIGN.md +256 -0
  461. package/.codex/assets/design-themes/open-design/webex/DESIGN.md +207 -0
  462. package/.codex/assets/design-themes/open-design/webflow/DESIGN.md +95 -0
  463. package/.codex/assets/design-themes/open-design/webflow/examples.html +11 -0
  464. package/.codex/assets/design-themes/open-design/wired/DESIGN.md +281 -0
  465. package/.codex/assets/design-themes/open-design/wise/DESIGN.md +176 -0
  466. package/.codex/assets/design-themes/open-design/x-ai/DESIGN.md +260 -0
  467. package/.codex/assets/design-themes/open-design/x-ai/examples.html +12 -0
  468. package/.codex/assets/design-themes/open-design/xiaohongshu/DESIGN.md +402 -0
  469. package/.codex/assets/design-themes/open-design/zapier/DESIGN.md +331 -0
  470. package/.codex/assets/design-themes/revenuecat/DESIGN.md +209 -0
  471. package/.codex/assets/design-themes/revenuecat/examples.html +122 -0
  472. package/.codex/assets/design-themes/vben/DESIGN.md +685 -0
  473. package/.codex/assets/design-themes/vben/examples.html +155 -0
  474. package/.codex/assets/vendor/flow-viewer/MERMAID_LICENSE +21 -0
  475. package/.codex/assets/vendor/flow-viewer/SVG_PAN_ZOOM_LICENSE +23 -0
  476. package/.codex/assets/vendor/flow-viewer/THIRD_PARTY_LICENSES.md +21 -0
  477. package/.codex/assets/vendor/flow-viewer/mermaid.min.js +3298 -0
  478. package/.codex/assets/vendor/flow-viewer/svg-pan-zoom.min.js +3 -0
  479. package/.codex/bundled-skills/impeccable/SKILL.md +163 -0
  480. package/.codex/bundled-skills/impeccable/agents/openai.yaml +4 -0
  481. package/.codex/bundled-skills/impeccable/reference/adapt.md +190 -0
  482. package/.codex/bundled-skills/impeccable/reference/animate.md +175 -0
  483. package/.codex/bundled-skills/impeccable/reference/audit.md +132 -0
  484. package/.codex/bundled-skills/impeccable/reference/bolder.md +113 -0
  485. package/.codex/bundled-skills/impeccable/reference/brand.md +118 -0
  486. package/.codex/bundled-skills/impeccable/reference/clarify.md +174 -0
  487. package/.codex/bundled-skills/impeccable/reference/codex.md +105 -0
  488. package/.codex/bundled-skills/impeccable/reference/cognitive-load.md +106 -0
  489. package/.codex/bundled-skills/impeccable/reference/color-and-contrast.md +105 -0
  490. package/.codex/bundled-skills/impeccable/reference/colorize.md +154 -0
  491. package/.codex/bundled-skills/impeccable/reference/craft.md +123 -0
  492. package/.codex/bundled-skills/impeccable/reference/critique.md +259 -0
  493. package/.codex/bundled-skills/impeccable/reference/delight.md +302 -0
  494. package/.codex/bundled-skills/impeccable/reference/distill.md +111 -0
  495. package/.codex/bundled-skills/impeccable/reference/document.md +427 -0
  496. package/.codex/bundled-skills/impeccable/reference/extract.md +68 -0
  497. package/.codex/bundled-skills/impeccable/reference/harden.md +347 -0
  498. package/.codex/bundled-skills/impeccable/reference/heuristics-scoring.md +234 -0
  499. package/.codex/bundled-skills/impeccable/reference/interaction-design.md +195 -0
  500. package/.codex/bundled-skills/impeccable/reference/layout.md +141 -0
  501. package/.codex/bundled-skills/impeccable/reference/live.md +622 -0
  502. package/.codex/bundled-skills/impeccable/reference/motion-design.md +109 -0
  503. package/.codex/bundled-skills/impeccable/reference/onboard.md +234 -0
  504. package/.codex/bundled-skills/impeccable/reference/optimize.md +258 -0
  505. package/.codex/bundled-skills/impeccable/reference/overdrive.md +130 -0
  506. package/.codex/bundled-skills/impeccable/reference/personas.md +179 -0
  507. package/.codex/bundled-skills/impeccable/reference/polish.md +242 -0
  508. package/.codex/bundled-skills/impeccable/reference/product.md +62 -0
  509. package/.codex/bundled-skills/impeccable/reference/quieter.md +99 -0
  510. package/.codex/bundled-skills/impeccable/reference/responsive-design.md +114 -0
  511. package/.codex/bundled-skills/impeccable/reference/shape.md +165 -0
  512. package/.codex/bundled-skills/impeccable/reference/spatial-design.md +100 -0
  513. package/.codex/bundled-skills/impeccable/reference/teach.md +156 -0
  514. package/.codex/bundled-skills/impeccable/reference/typeset.md +124 -0
  515. package/.codex/bundled-skills/impeccable/reference/typography.md +159 -0
  516. package/.codex/bundled-skills/impeccable/reference/ux-writing.md +107 -0
  517. package/.codex/bundled-skills/impeccable/scripts/cleanup-deprecated.mjs +284 -0
  518. package/.codex/bundled-skills/impeccable/scripts/command-metadata.json +94 -0
  519. package/.codex/bundled-skills/impeccable/scripts/critique-storage.mjs +242 -0
  520. package/.codex/bundled-skills/impeccable/scripts/design-parser.mjs +820 -0
  521. package/.codex/bundled-skills/impeccable/scripts/detect-csp.mjs +198 -0
  522. package/.codex/bundled-skills/impeccable/scripts/detect.mjs +21 -0
  523. package/.codex/bundled-skills/impeccable/scripts/detector/browser/injected/index.mjs +1688 -0
  524. package/.codex/bundled-skills/impeccable/scripts/detector/cli/main.mjs +232 -0
  525. package/.codex/bundled-skills/impeccable/scripts/detector/detect-antipatterns-browser.js +4030 -0
  526. package/.codex/bundled-skills/impeccable/scripts/detector/detect-antipatterns.mjs +43 -0
  527. package/.codex/bundled-skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +251 -0
  528. package/.codex/bundled-skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +420 -0
  529. package/.codex/bundled-skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +954 -0
  530. package/.codex/bundled-skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +174 -0
  531. package/.codex/bundled-skills/impeccable/scripts/detector/engines/visual/screenshot-contrast.mjs +189 -0
  532. package/.codex/bundled-skills/impeccable/scripts/detector/findings.mjs +12 -0
  533. package/.codex/bundled-skills/impeccable/scripts/detector/node/file-system.mjs +198 -0
  534. package/.codex/bundled-skills/impeccable/scripts/detector/profile/profiler.mjs +166 -0
  535. package/.codex/bundled-skills/impeccable/scripts/detector/registry/antipatterns.mjs +278 -0
  536. package/.codex/bundled-skills/impeccable/scripts/detector/rules/checks.mjs +1948 -0
  537. package/.codex/bundled-skills/impeccable/scripts/detector/shared/color.mjs +124 -0
  538. package/.codex/bundled-skills/impeccable/scripts/detector/shared/constants.mjs +101 -0
  539. package/.codex/bundled-skills/impeccable/scripts/detector/shared/page.mjs +7 -0
  540. package/.codex/bundled-skills/impeccable/scripts/impeccable-paths.mjs +110 -0
  541. package/.codex/bundled-skills/impeccable/scripts/is-generated.mjs +69 -0
  542. package/.codex/bundled-skills/impeccable/scripts/live-accept.mjs +595 -0
  543. package/.codex/bundled-skills/impeccable/scripts/live-browser-session.js +123 -0
  544. package/.codex/bundled-skills/impeccable/scripts/live-browser.js +4860 -0
  545. package/.codex/bundled-skills/impeccable/scripts/live-complete.mjs +75 -0
  546. package/.codex/bundled-skills/impeccable/scripts/live-completion.mjs +18 -0
  547. package/.codex/bundled-skills/impeccable/scripts/live-inject.mjs +446 -0
  548. package/.codex/bundled-skills/impeccable/scripts/live-poll.mjs +200 -0
  549. package/.codex/bundled-skills/impeccable/scripts/live-resume.mjs +48 -0
  550. package/.codex/bundled-skills/impeccable/scripts/live-server.mjs +838 -0
  551. package/.codex/bundled-skills/impeccable/scripts/live-session-store.mjs +254 -0
  552. package/.codex/bundled-skills/impeccable/scripts/live-status.mjs +47 -0
  553. package/.codex/bundled-skills/impeccable/scripts/live-wrap.mjs +632 -0
  554. package/.codex/bundled-skills/impeccable/scripts/live.mjs +247 -0
  555. package/.codex/bundled-skills/impeccable/scripts/load-context.mjs +141 -0
  556. package/.codex/bundled-skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
  557. package/.codex/bundled-skills/impeccable/scripts/pin.mjs +214 -0
  558. package/.codex/references/commands/analyze.md +39 -0
  559. package/.codex/references/commands/architect.md +41 -0
  560. package/.codex/references/commands/deliver.md +29 -0
  561. package/.codex/references/commands/design.md +92 -0
  562. package/.codex/references/commands/help.md +24 -0
  563. package/.codex/references/commands/init.md +40 -0
  564. package/.codex/references/commands/plan.md +41 -0
  565. package/.codex/references/commands/review.md +33 -0
  566. package/.codex/references/commands/status.md +20 -0
  567. package/.codex/role-skills/demand-analysis/SKILL.md +50 -0
  568. package/.codex/role-skills/demand-analysis/templates/handoff-prd.md +39 -0
  569. package/.codex/role-skills/demand-analysis/templates/prd.md +85 -0
  570. package/.codex/role-skills/dev-task-planning/SKILL.md +37 -0
  571. package/.codex/role-skills/dev-task-planning/templates/dev-tasks.md +54 -0
  572. package/.codex/role-skills/quality-review/SKILL.md +49 -0
  573. package/.codex/role-skills/quality-review/templates/review-stage.md +39 -0
  574. package/.codex/role-skills/tech-architecture/SKILL.md +49 -0
  575. package/.codex/role-skills/tech-architecture/templates/handoff-architecture.md +28 -0
  576. package/.codex/role-skills/tech-architecture/templates/tech-architecture.md +54 -0
  577. package/.codex/role-skills/ui-prototype-design/SKILL.md +125 -0
  578. package/.codex/role-skills/ui-prototype-design/templates/handoff-ui.md +40 -0
  579. package/.codex/role-skills/ui-prototype-design/templates/prototype-review.md +57 -0
  580. package/.codex/role-skills/ui-prototype-design/templates/ui-design.md +142 -0
  581. package/.codex/scripts/package_delivery.js +195 -0
  582. package/.codex/scripts/review_stage.js +622 -0
  583. package/.codex/templates/AGENTS.md +44 -0
  584. package/.codex/templates/delivery-README.md +37 -0
  585. package/.codex/templates/framework-AGENTS.md +74 -0
  586. package/.codex/templates/framework-README.md +65 -0
  587. package/.codex/templates/project-config.md +117 -0
  588. package/.codex/templates/prototype-README.md +45 -0
  589. package/.codex/templates/workflow-state.json +47 -0
  590. package/README.md +28 -0
  591. package/bin/pmflow.js +463 -0
  592. package/package.json +30 -0
@@ -0,0 +1,1688 @@
1
+ const IS_BROWSER = typeof window !== 'undefined';
2
+
3
+ // ─── Section 7: Browser UI (IS_BROWSER only) ────────────────────────────────
4
+
5
+ if (IS_BROWSER) {
6
+ // Detect extension mode via the script tag's data attribute or the document element fallback.
7
+ // currentScript is reliable for synchronously-executing scripts (which our IIFE is).
8
+ const _myScript = document.currentScript;
9
+ const EXTENSION_MODE = (_myScript && _myScript.dataset.impeccableExtension === 'true')
10
+ || document.documentElement.dataset.impeccableExtension === 'true';
11
+
12
+ const BRAND_COLOR = 'oklch(55% 0.25 350)';
13
+ const BRAND_COLOR_HOVER = 'oklch(45% 0.25 350)';
14
+ const LABEL_BG = BRAND_COLOR;
15
+ const OUTLINE_COLOR = BRAND_COLOR;
16
+
17
+ // Inject hover styles via CSS (more reliable than JS event listeners)
18
+ const styleEl = document.createElement('style');
19
+ styleEl.textContent = `
20
+ @keyframes impeccable-reveal {
21
+ from { opacity: 0; }
22
+ to { opacity: 1; }
23
+ }
24
+ .impeccable-overlay:not(.impeccable-banner) {
25
+ pointer-events: none;
26
+ outline: 2px solid ${OUTLINE_COLOR};
27
+ border-radius: 4px;
28
+ transition: outline-color 0.15s ease;
29
+ animation: impeccable-reveal 0.4s cubic-bezier(0.16, 1, 0.3, 1) both;
30
+ animation-play-state: paused;
31
+ border-top-left-radius: 0;
32
+ }
33
+ .impeccable-overlay.impeccable-visible {
34
+ animation-play-state: running;
35
+ }
36
+ .impeccable-overlay.impeccable-hover {
37
+ outline-color: ${BRAND_COLOR_HOVER};
38
+ z-index: 100001 !important;
39
+ }
40
+ .impeccable-overlay.impeccable-hover .impeccable-label {
41
+ background: ${BRAND_COLOR_HOVER};
42
+ }
43
+ .impeccable-overlay.impeccable-spotlight {
44
+ z-index: 100002 !important;
45
+ }
46
+ .impeccable-overlay.impeccable-spotlight-dimmed {
47
+ opacity: 0.15 !important;
48
+ animation: none !important;
49
+ filter: blur(3px);
50
+ }
51
+ .impeccable-spotlight-backdrop {
52
+ position: fixed;
53
+ top: 0; left: 0; right: 0; bottom: 0;
54
+ backdrop-filter: blur(3px) brightness(0.6);
55
+ -webkit-backdrop-filter: blur(3px) brightness(0.6);
56
+ pointer-events: none;
57
+ z-index: 99998;
58
+ opacity: 0;
59
+ outline: none !important;
60
+ animation: none !important;
61
+ }
62
+ .impeccable-spotlight-backdrop.impeccable-visible {
63
+ opacity: 1;
64
+ }
65
+ .impeccable-hidden .impeccable-overlay${EXTENSION_MODE ? '' : ':not(.impeccable-banner)'} {
66
+ display: none !important;
67
+ }
68
+ `;
69
+ (document.head || document.documentElement).appendChild(styleEl);
70
+
71
+ // Spotlight backdrop element (created lazily on first use)
72
+ let spotlightBackdrop = null;
73
+ let spotlightTarget = null;
74
+
75
+ function getSpotlightBackdrop() {
76
+ if (!spotlightBackdrop) {
77
+ spotlightBackdrop = document.createElement('div');
78
+ spotlightBackdrop.className = 'impeccable-spotlight-backdrop';
79
+ document.body.appendChild(spotlightBackdrop);
80
+ }
81
+ return spotlightBackdrop;
82
+ }
83
+
84
+ function updateSpotlightClipPath() {
85
+ if (!spotlightBackdrop || !spotlightTarget) return;
86
+ const r = spotlightTarget.getBoundingClientRect();
87
+ // Match the overlay's outer edge: element rect + 4px (2px overlay offset + 2px outline width)
88
+ const inset = 4;
89
+ const radius = 6; // outline border-radius (4) + outline width (2)
90
+ const x1 = r.left - inset;
91
+ const y1 = r.top - inset;
92
+ const x2 = r.right + inset;
93
+ const y2 = r.bottom + inset;
94
+ const vw = window.innerWidth;
95
+ const vh = window.innerHeight;
96
+ // Outer rect + rounded inner rect (evenodd creates a hole)
97
+ const path = `M0 0H${vw}V${vh}H0Z M${x1 + radius} ${y1}H${x2 - radius}A${radius} ${radius} 0 0 1 ${x2} ${y1 + radius}V${y2 - radius}A${radius} ${radius} 0 0 1 ${x2 - radius} ${y2}H${x1 + radius}A${radius} ${radius} 0 0 1 ${x1} ${y2 - radius}V${y1 + radius}A${radius} ${radius} 0 0 1 ${x1 + radius} ${y1}Z`;
98
+ spotlightBackdrop.style.clipPath = `path(evenodd, "${path}")`;
99
+ }
100
+
101
+ function showSpotlight(target) {
102
+ if (!target || !target.getBoundingClientRect) return;
103
+ // Respect the spotlightBlur setting: if disabled, don't show the backdrop
104
+ if (window.__IMPECCABLE_CONFIG__?.spotlightBlur === false) {
105
+ spotlightTarget = target;
106
+ return;
107
+ }
108
+ spotlightTarget = target;
109
+ const bd = getSpotlightBackdrop();
110
+ updateSpotlightClipPath();
111
+ bd.classList.add('impeccable-visible');
112
+ }
113
+
114
+ function hideSpotlight() {
115
+ spotlightTarget = null;
116
+ if (spotlightBackdrop) spotlightBackdrop.classList.remove('impeccable-visible');
117
+ }
118
+
119
+ function isInViewport(el) {
120
+ const r = el.getBoundingClientRect();
121
+ return r.top >= 0 && r.left >= 0 && r.bottom <= window.innerHeight && r.right <= window.innerWidth;
122
+ }
123
+
124
+ // Reposition spotlight on scroll/resize
125
+ window.addEventListener('scroll', () => {
126
+ if (spotlightTarget) updateSpotlightClipPath();
127
+ }, { passive: true });
128
+ window.addEventListener('resize', () => {
129
+ if (spotlightTarget) updateSpotlightClipPath();
130
+ });
131
+
132
+ const overlays = [];
133
+ const TYPE_LABELS = {};
134
+ const RULE_CATEGORY = {};
135
+ for (const ap of ANTIPATTERNS) {
136
+ TYPE_LABELS[ap.id] = ap.name.toLowerCase();
137
+ RULE_CATEGORY[ap.id] = ap.category || 'quality';
138
+ }
139
+
140
+ function isInFixedContext(el) {
141
+ let p = el;
142
+ while (p && p !== document.body) {
143
+ if (getComputedStyle(p).position === 'fixed') return true;
144
+ p = p.parentElement;
145
+ }
146
+ return false;
147
+ }
148
+
149
+ function positionOverlay(overlay) {
150
+ const el = overlay._targetEl;
151
+ if (!el) return;
152
+ const rect = el.getBoundingClientRect();
153
+ if (overlay._isFixed) {
154
+ // Viewport-relative coords for fixed targets
155
+ overlay.style.top = `${rect.top - 2}px`;
156
+ overlay.style.left = `${rect.left - 2}px`;
157
+ } else {
158
+ // Document-relative coords for normal targets
159
+ overlay.style.top = `${rect.top + scrollY - 2}px`;
160
+ overlay.style.left = `${rect.left + scrollX - 2}px`;
161
+ }
162
+ overlay.style.width = `${rect.width + 4}px`;
163
+ overlay.style.height = `${rect.height + 4}px`;
164
+ }
165
+
166
+ function repositionOverlays() {
167
+ for (const o of overlays) {
168
+ if (!o._targetEl || o.classList.contains('impeccable-banner')) continue;
169
+ // Skip overlays whose target is currently hidden (display: none on the overlay)
170
+ if (o.style.display === 'none') continue;
171
+ positionOverlay(o);
172
+ }
173
+ }
174
+
175
+ let resizeRAF;
176
+ const onResize = () => {
177
+ cancelAnimationFrame(resizeRAF);
178
+ resizeRAF = requestAnimationFrame(repositionOverlays);
179
+ };
180
+ window.addEventListener('resize', onResize);
181
+ // Reposition on scroll too -- catches sticky/parallax shifts
182
+ window.addEventListener('scroll', onResize, { passive: true });
183
+ // Reposition when body resizes (lazy-loaded images, dynamic content, fonts loading)
184
+ if (typeof ResizeObserver !== 'undefined') {
185
+ const bodyResizeObserver = new ResizeObserver(onResize);
186
+ bodyResizeObserver.observe(document.body);
187
+ }
188
+
189
+ // Track target element visibility via IntersectionObserver.
190
+ // Uses a huge rootMargin so all *rendered* elements count as intersecting,
191
+ // while display:none / closed <details> / hidden modals etc. do not.
192
+ // This is event-driven -- no polling needed.
193
+ let overlayIndex = 0;
194
+ const visibilityObserver = new IntersectionObserver((entries) => {
195
+ for (const entry of entries) {
196
+ const overlay = entry.target._impeccableOverlay;
197
+ if (!overlay) continue;
198
+ if (entry.isIntersecting) {
199
+ overlay.style.display = '';
200
+ positionOverlay(overlay);
201
+ if (!overlay._revealed) {
202
+ overlay._revealed = true;
203
+ if (firstScanDone) {
204
+ // Subsequent reveals (re-scans, scroll-into-view): instant, no animation
205
+ overlay.style.animation = 'none';
206
+ } else {
207
+ // Initial scan: staggered cascade reveal
208
+ overlay.style.animationDelay = `${Math.min((overlay._staggerIndex || 0) * 60, 600)}ms`;
209
+ }
210
+ requestAnimationFrame(() => {
211
+ overlay.classList.add('impeccable-visible');
212
+ if (overlay._checkLabel) overlay._checkLabel();
213
+ });
214
+ }
215
+ } else {
216
+ overlay.style.display = 'none';
217
+ }
218
+ }
219
+ }, { rootMargin: '99999px' });
220
+
221
+ function detachOverlay(overlay) {
222
+ if (!overlay) return;
223
+ if (typeof overlay._cleanup === 'function') {
224
+ try { overlay._cleanup(); } catch { /* best effort overlay teardown */ }
225
+ }
226
+ if (overlay._targetEl && overlay._targetEl._impeccableOverlay === overlay) {
227
+ visibilityObserver.unobserve(overlay._targetEl);
228
+ delete overlay._targetEl._impeccableOverlay;
229
+ }
230
+ const idx = overlays.indexOf(overlay);
231
+ if (idx >= 0) overlays.splice(idx, 1);
232
+ overlay.remove();
233
+ }
234
+
235
+ // Reposition overlays after CSS transitions end (e.g. reveal animations).
236
+ // Listens at document level so it catches transitions on ancestor elements
237
+ // (the transform may be on a parent, not the flagged element itself).
238
+ document.addEventListener('transitionend', (e) => {
239
+ if (e.propertyName !== 'transform') return;
240
+ for (const o of overlays) {
241
+ if (!o._targetEl || o.classList.contains('impeccable-banner') || o.style.display === 'none') continue;
242
+ if (e.target === o._targetEl || e.target.contains(o._targetEl)) {
243
+ positionOverlay(o);
244
+ }
245
+ }
246
+ });
247
+
248
+ const highlight = function(el, findings) {
249
+ if (el._impeccableOverlay) detachOverlay(el._impeccableOverlay);
250
+ const hasSlop = findings.some(f => RULE_CATEGORY[f.type || f.id] === 'slop');
251
+
252
+ const fixed = isInFixedContext(el);
253
+ const rect = el.getBoundingClientRect();
254
+ const outline = document.createElement('div');
255
+ outline.className = 'impeccable-overlay';
256
+ outline._targetEl = el;
257
+ outline._isFixed = fixed;
258
+ Object.assign(outline.style, {
259
+ position: fixed ? 'fixed' : 'absolute',
260
+ top: fixed ? `${rect.top - 2}px` : `${rect.top + scrollY - 2}px`,
261
+ left: fixed ? `${rect.left - 2}px` : `${rect.left + scrollX - 2}px`,
262
+ width: `${rect.width + 4}px`, height: `${rect.height + 4}px`,
263
+ zIndex: '99999', boxSizing: 'border-box',
264
+ });
265
+
266
+ // Build per-finding label entries: ✦ prefix for slop
267
+ const entries = findings.map(f => {
268
+ const name = TYPE_LABELS[f.type || f.id] || f.type || f.id;
269
+ const prefix = RULE_CATEGORY[f.type || f.id] === 'slop' ? '\u2726 ' : '';
270
+ return { name: prefix + name, detail: f.detail || f.snippet };
271
+ });
272
+ const allText = entries.map(e => e.name).join(', ');
273
+
274
+ const label = document.createElement('div');
275
+ label.className = 'impeccable-label';
276
+ Object.assign(label.style, {
277
+ position: 'absolute', bottom: '100%', left: '-2px',
278
+ display: 'flex', alignItems: 'center',
279
+ whiteSpace: 'nowrap',
280
+ fontSize: '11px', fontWeight: '600', letterSpacing: '0.02em',
281
+ color: 'white', lineHeight: '14px',
282
+ background: LABEL_BG,
283
+ fontFamily: 'system-ui, sans-serif',
284
+ borderRadius: '4px 4px 0 0',
285
+ });
286
+
287
+ const textSpan = document.createElement('span');
288
+ textSpan.style.padding = '3px 8px';
289
+ textSpan.textContent = allText;
290
+ label.appendChild(textSpan);
291
+
292
+ // State for cycling mode
293
+ let cycleMode = false;
294
+ let cycleIndex = 0;
295
+ let isHovered = false;
296
+ let prevBtn, nextBtn;
297
+
298
+ function updateCycleText() {
299
+ const e = entries[cycleIndex];
300
+ textSpan.textContent = isHovered ? e.detail : e.name;
301
+ }
302
+
303
+ function enableCycleMode() {
304
+ if (cycleMode || entries.length < 2) return;
305
+ cycleMode = true;
306
+
307
+ const btnStyle = {
308
+ background: 'none', border: 'none', color: 'rgba(255,255,255,0.7)',
309
+ fontSize: '11px', cursor: 'pointer', padding: '3px 4px',
310
+ fontFamily: 'system-ui, sans-serif', lineHeight: '14px',
311
+ pointerEvents: 'auto',
312
+ };
313
+
314
+ const navGroup = document.createElement('span');
315
+ Object.assign(navGroup.style, {
316
+ display: 'inline-flex', alignItems: 'center', flexShrink: '0',
317
+ });
318
+
319
+ prevBtn = document.createElement('button');
320
+ prevBtn.textContent = '\u2039';
321
+ Object.assign(prevBtn.style, btnStyle);
322
+ prevBtn.style.paddingLeft = '6px';
323
+ prevBtn.addEventListener('click', (e) => {
324
+ e.stopPropagation();
325
+ cycleIndex = (cycleIndex - 1 + entries.length) % entries.length;
326
+ updateCycleText();
327
+ });
328
+
329
+ nextBtn = document.createElement('button');
330
+ nextBtn.textContent = '\u203A';
331
+ Object.assign(nextBtn.style, btnStyle);
332
+ nextBtn.style.paddingRight = '2px';
333
+ nextBtn.addEventListener('click', (e) => {
334
+ e.stopPropagation();
335
+ cycleIndex = (cycleIndex + 1) % entries.length;
336
+ updateCycleText();
337
+ });
338
+
339
+ navGroup.appendChild(prevBtn);
340
+ navGroup.appendChild(nextBtn);
341
+ label.insertBefore(navGroup, textSpan);
342
+ textSpan.style.padding = '3px 8px 3px 4px';
343
+ updateCycleText();
344
+ }
345
+
346
+ outline.appendChild(label);
347
+
348
+ // Start hidden; the IntersectionObserver will show it once the target is rendered
349
+ outline.style.display = 'none';
350
+ outline._staggerIndex = overlayIndex++;
351
+ el._impeccableOverlay = outline;
352
+ visibilityObserver.observe(el);
353
+
354
+ // After first paint, check label width vs outline
355
+ outline._checkLabel = () => {
356
+ if (entries.length > 1 && label.offsetWidth > outline.offsetWidth) {
357
+ enableCycleMode();
358
+ }
359
+ };
360
+
361
+ // Hover: show detail text, darken
362
+ const onMouseEnter = () => {
363
+ isHovered = true;
364
+ outline.classList.add('impeccable-hover');
365
+ outline.style.outlineColor = BRAND_COLOR_HOVER;
366
+ label.style.background = BRAND_COLOR_HOVER;
367
+ if (cycleMode) {
368
+ updateCycleText();
369
+ } else {
370
+ textSpan.textContent = entries.map(e => e.detail).join(' | ');
371
+ }
372
+ };
373
+ const onMouseLeave = () => {
374
+ isHovered = false;
375
+ outline.classList.remove('impeccable-hover');
376
+ outline.style.outlineColor = '';
377
+ label.style.background = LABEL_BG;
378
+ if (cycleMode) {
379
+ updateCycleText();
380
+ } else {
381
+ textSpan.textContent = allText;
382
+ }
383
+ };
384
+ el.addEventListener('mouseenter', onMouseEnter);
385
+ el.addEventListener('mouseleave', onMouseLeave);
386
+ outline._cleanup = () => {
387
+ el.removeEventListener('mouseenter', onMouseEnter);
388
+ el.removeEventListener('mouseleave', onMouseLeave);
389
+ };
390
+
391
+ document.body.appendChild(outline);
392
+ overlays.push(outline);
393
+ };
394
+
395
+ const showPageBanner = function(findings) {
396
+ if (!findings.length) return;
397
+ const banner = document.createElement('div');
398
+ banner.className = 'impeccable-overlay impeccable-banner';
399
+ Object.assign(banner.style, {
400
+ position: 'fixed', top: '0', left: '0', right: '0', zIndex: '100000',
401
+ background: LABEL_BG, color: 'white',
402
+ fontFamily: 'system-ui, sans-serif', fontSize: '13px',
403
+ display: 'flex', alignItems: 'center', pointerEvents: 'auto',
404
+ height: '36px', overflow: 'hidden', maxWidth: '100vw',
405
+ transform: 'translateY(-100%)',
406
+ transition: 'transform 0.4s cubic-bezier(0.16, 1, 0.3, 1)',
407
+ });
408
+ requestAnimationFrame(() => requestAnimationFrame(() => {
409
+ banner.style.transform = 'translateY(0)';
410
+ }));
411
+
412
+ // Scrollable findings area
413
+ const scrollArea = document.createElement('div');
414
+ Object.assign(scrollArea.style, {
415
+ flex: '1', minWidth: '0', overflowX: 'auto', overflowY: 'hidden',
416
+ display: 'flex', gap: '8px', alignItems: 'center',
417
+ padding: '0 12px', scrollSnapType: 'x mandatory',
418
+ scrollbarWidth: 'none',
419
+ });
420
+ for (const f of findings) {
421
+ const prefix = RULE_CATEGORY[f.type] === 'slop' ? '\u2726 ' : '';
422
+ const tag = document.createElement('span');
423
+ tag.textContent = `${prefix}${TYPE_LABELS[f.type] || f.type}: ${f.detail}`;
424
+ Object.assign(tag.style, {
425
+ background: 'rgba(255,255,255,0.15)', padding: '2px 8px',
426
+ borderRadius: '3px', fontSize: '12px', fontFamily: 'ui-monospace, monospace',
427
+ whiteSpace: 'nowrap', flexShrink: '0', scrollSnapAlign: 'start',
428
+ });
429
+ scrollArea.appendChild(tag);
430
+ }
431
+ banner.appendChild(scrollArea);
432
+
433
+ // Controls area (only in standalone mode, not extension)
434
+ if (!EXTENSION_MODE) {
435
+ const controls = document.createElement('div');
436
+ Object.assign(controls.style, {
437
+ display: 'flex', alignItems: 'center', gap: '2px',
438
+ padding: '0 8px', flexShrink: '0',
439
+ });
440
+
441
+ // Toggle visibility button
442
+ const toggle = document.createElement('button');
443
+ toggle.textContent = '\u25C9'; // circle with dot (visible state)
444
+ toggle.title = 'Toggle overlay visibility';
445
+ Object.assign(toggle.style, {
446
+ background: 'none', border: 'none',
447
+ color: 'white', fontSize: '16px', cursor: 'pointer', padding: '0 4px',
448
+ opacity: '0.85', transition: 'opacity 0.15s',
449
+ });
450
+ let overlaysVisible = true;
451
+ toggle.addEventListener('click', () => {
452
+ overlaysVisible = !overlaysVisible;
453
+ document.body.classList.toggle('impeccable-hidden', !overlaysVisible);
454
+ toggle.textContent = overlaysVisible ? '\u25C9' : '\u25CB'; // filled vs empty circle
455
+ toggle.style.opacity = overlaysVisible ? '0.85' : '0.5';
456
+ });
457
+ controls.appendChild(toggle);
458
+
459
+ // Close button
460
+ const close = document.createElement('button');
461
+ close.textContent = '\u00d7';
462
+ close.title = 'Dismiss banner';
463
+ Object.assign(close.style, {
464
+ background: 'none', border: 'none',
465
+ color: 'white', fontSize: '18px', cursor: 'pointer', padding: '0 4px',
466
+ });
467
+ close.addEventListener('click', () => banner.remove());
468
+ controls.appendChild(close);
469
+
470
+ banner.appendChild(controls);
471
+ }
472
+ document.body.appendChild(banner);
473
+ overlays.push(banner);
474
+ };
475
+
476
+ // Heuristic for skipping CSS-in-JS hashed class names like "css-1a2b3c" or "_2x4hG_".
477
+ // These change between builds and produce brittle, ugly selectors.
478
+ function isLikelyHashedClass(c) {
479
+ if (!c) return true;
480
+ if (/^(css|sc|emotion|jsx|module)-[\w-]{4,}$/i.test(c)) return true;
481
+ if (/^_[\w-]{5,}$/.test(c)) return true;
482
+ if (/^[a-z0-9]{6,}$/i.test(c) && /\d/.test(c)) return true;
483
+ return false;
484
+ }
485
+
486
+ function buildSelectorSegment(el) {
487
+ const tag = el.tagName.toLowerCase();
488
+ let sel = tag;
489
+
490
+ if (el.classList && el.classList.length > 0) {
491
+ const classes = [...el.classList]
492
+ .filter(c => !c.startsWith('impeccable-') && !isLikelyHashedClass(c))
493
+ .slice(0, 2);
494
+ if (classes.length > 0) {
495
+ sel += '.' + classes.map(c => CSS.escape(c)).join('.');
496
+ }
497
+ }
498
+
499
+ // Disambiguate among siblings only if the parent has multiple matches
500
+ const parent = el.parentElement;
501
+ if (parent) {
502
+ try {
503
+ const matching = parent.querySelectorAll(':scope > ' + sel);
504
+ if (matching.length > 1) {
505
+ const sameType = [...parent.children].filter(c => c.tagName === el.tagName);
506
+ const idx = sameType.indexOf(el) + 1;
507
+ sel += `:nth-of-type(${idx})`;
508
+ }
509
+ } catch {
510
+ const idx = [...parent.children].indexOf(el) + 1;
511
+ sel = `${tag}:nth-child(${idx})`;
512
+ }
513
+ }
514
+ return sel;
515
+ }
516
+
517
+ function generateSelector(el) {
518
+ if (el === document.body) return 'body';
519
+ if (el === document.documentElement) return 'html';
520
+ if (el.id) return '#' + CSS.escape(el.id);
521
+
522
+ const parts = [];
523
+ let current = el;
524
+ let depth = 0;
525
+ const MAX_DEPTH = 10;
526
+
527
+ while (current && current !== document.body && current !== document.documentElement && depth < MAX_DEPTH) {
528
+ parts.unshift(buildSelectorSegment(current));
529
+
530
+ // Anchor on an ancestor's ID and stop walking up
531
+ if (current.id) {
532
+ parts[0] = '#' + CSS.escape(current.id);
533
+ break;
534
+ }
535
+
536
+ // Stop as soon as the partial selector uniquely identifies the target
537
+ const trySelector = parts.join(' > ');
538
+ try {
539
+ const matches = document.querySelectorAll(trySelector);
540
+ if (matches.length === 1 && matches[0] === el) {
541
+ return trySelector;
542
+ }
543
+ } catch { /* invalid selector — keep walking */ }
544
+
545
+ current = current.parentElement;
546
+ depth++;
547
+ }
548
+
549
+ return parts.join(' > ');
550
+ }
551
+
552
+ function getDirectText(el) {
553
+ return [...el.childNodes]
554
+ .filter(n => n.nodeType === 3)
555
+ .map(n => n.textContent || '')
556
+ .join('');
557
+ }
558
+
559
+ function getDirectTextRect(el) {
560
+ const rects = [];
561
+ for (const node of el.childNodes) {
562
+ if (node.nodeType !== 3 || !(node.textContent || '').trim()) continue;
563
+ const range = document.createRange();
564
+ range.selectNodeContents(node);
565
+ for (const rect of range.getClientRects()) {
566
+ if (rect.width >= 1 && rect.height >= 1) rects.push(rect);
567
+ }
568
+ range.detach?.();
569
+ }
570
+ if (rects.length === 0) return null;
571
+ const left = Math.min(...rects.map(r => r.left));
572
+ const top = Math.min(...rects.map(r => r.top));
573
+ const right = Math.max(...rects.map(r => r.right));
574
+ const bottom = Math.max(...rects.map(r => r.bottom));
575
+ return {
576
+ left,
577
+ top,
578
+ right,
579
+ bottom,
580
+ width: right - left,
581
+ height: bottom - top,
582
+ x: left,
583
+ y: top,
584
+ };
585
+ }
586
+
587
+ function collectVisualContrastReasons(el, style) {
588
+ const reasons = new Set();
589
+ const bgClip = style.webkitBackgroundClip || style.backgroundClip || '';
590
+ const ownBgImage = style.backgroundImage || '';
591
+ if (bgClip === 'text' && ownBgImage && ownBgImage !== 'none') {
592
+ reasons.add('background-clip text');
593
+ }
594
+ if (style.textShadow && style.textShadow !== 'none') reasons.add('text shadow');
595
+
596
+ let current = el;
597
+ while (current && current.nodeType === 1) {
598
+ const tag = current.tagName?.toLowerCase();
599
+ const currentStyle = getComputedStyle(current);
600
+ const bgImage = currentStyle.backgroundImage || '';
601
+ const isDocumentSurface = tag === 'body' || tag === 'html';
602
+
603
+ if (!isDocumentSurface && bgImage && bgImage !== 'none') {
604
+ if (/url\s*\(/i.test(bgImage)) reasons.add('image background');
605
+ if (/gradient/i.test(bgImage)) reasons.add('gradient background');
606
+ }
607
+ if (parseFloat(currentStyle.opacity) < 0.99) reasons.add('opacity stack');
608
+ if (currentStyle.mixBlendMode && currentStyle.mixBlendMode !== 'normal') reasons.add('blend mode');
609
+ if (currentStyle.filter && currentStyle.filter !== 'none') reasons.add('filter');
610
+ if (currentStyle.backdropFilter && currentStyle.backdropFilter !== 'none') reasons.add('backdrop filter');
611
+
612
+ const solidBg = parseRgb(currentStyle.backgroundColor);
613
+ if (solidBg && solidBg.a >= 0.95 && (!bgImage || bgImage === 'none')) break;
614
+ current = current.parentElement;
615
+ }
616
+
617
+ const sampleRect = getDirectTextRect(el) || el.getBoundingClientRect();
618
+ if (sampleRect && document.elementsFromPoint) {
619
+ const points = [
620
+ [sampleRect.left + sampleRect.width / 2, sampleRect.top + sampleRect.height / 2],
621
+ [sampleRect.left + Math.min(sampleRect.width - 1, Math.max(1, sampleRect.width * 0.25)), sampleRect.top + sampleRect.height / 2],
622
+ [sampleRect.left + Math.min(sampleRect.width - 1, Math.max(1, sampleRect.width * 0.75)), sampleRect.top + sampleRect.height / 2],
623
+ ];
624
+ for (const [x, y] of points) {
625
+ if (x < 0 || y < 0 || x > window.innerWidth || y > window.innerHeight) continue;
626
+ const stack = document.elementsFromPoint(x, y);
627
+ const selfIndex = stack.findIndex(node => node === el || el.contains(node) || node.contains?.(el));
628
+ if (selfIndex < 0) continue;
629
+ for (const node of stack.slice(selfIndex + 1)) {
630
+ const nodeTag = node.tagName?.toLowerCase();
631
+ if (nodeTag === 'img' || nodeTag === 'picture' || nodeTag === 'video' || nodeTag === 'canvas' || nodeTag === 'svg') {
632
+ reasons.add(`${nodeTag} underlay`);
633
+ break;
634
+ }
635
+ }
636
+ }
637
+ }
638
+
639
+ return [...reasons];
640
+ }
641
+
642
+ function collectVisualContrastCandidates(options = {}) {
643
+ const maxCandidates = Number.isFinite(options.maxCandidates) ? options.maxCandidates : 12;
644
+ const candidates = [];
645
+ for (const el of document.querySelectorAll('*')) {
646
+ if (candidates.length >= maxCandidates) break;
647
+ if (el.closest('.impeccable-overlay, .impeccable-label, .impeccable-banner, .impeccable-tooltip')) continue;
648
+ if (el.closest('[id^="impeccable-live-"]')) continue;
649
+ if (el === document.body || el === document.documentElement) continue;
650
+
651
+ const tag = el.tagName.toLowerCase();
652
+ const style = getComputedStyle(el);
653
+ if (style.display === 'none' || style.visibility === 'hidden') continue;
654
+ const directText = getDirectText(el);
655
+ const hasDirectText = directText.trim().length > 0;
656
+ if (!hasDirectText || isEmojiOnlyText(directText)) continue;
657
+
658
+ const bgColor = readOwnBackgroundColor(el, style);
659
+ const isStyledButton = (tag === 'a' || tag === 'button')
660
+ && bgColor && bgColor.a > 0.5;
661
+ if (SAFE_TAGS.has(tag) && !isStyledButton) continue;
662
+
663
+ const rect = getDirectTextRect(el) || el.getBoundingClientRect();
664
+ if (!rect || rect.width < 4 || rect.height < 4) continue;
665
+
666
+ const reasons = collectVisualContrastReasons(el, style);
667
+ if (reasons.length === 0) continue;
668
+
669
+ const textColor = parseRgb(style.color);
670
+ const fontSize = parseFloat(style.fontSize) || 16;
671
+ const fontWeight = parseInt(style.fontWeight) || 400;
672
+ const isLargeText = fontSize >= WCAG_LARGE_TEXT_PX || (fontSize >= WCAG_LARGE_BOLD_TEXT_PX && fontWeight >= 700);
673
+ const threshold = isLargeText ? 3.0 : 4.5;
674
+ const clip = {
675
+ x: Math.max(0, Math.floor(rect.left + window.scrollX - 2)),
676
+ y: Math.max(0, Math.floor(rect.top + window.scrollY - 2)),
677
+ width: Math.max(1, Math.ceil(rect.width + 4)),
678
+ height: Math.max(1, Math.ceil(rect.height + 4)),
679
+ };
680
+
681
+ candidates.push({
682
+ selector: generateSelector(el),
683
+ tagName: tag,
684
+ text: directText.trim().replace(/\s+/g, ' ').slice(0, 80),
685
+ threshold,
686
+ reasons,
687
+ clip,
688
+ textColor,
689
+ preferRenderedForeground: !textColor || textColor.a < 0.99 || reasons.some(reason =>
690
+ reason === 'opacity stack' ||
691
+ reason === 'blend mode' ||
692
+ reason === 'filter' ||
693
+ reason === 'backdrop filter' ||
694
+ reason === 'background-clip text'
695
+ ),
696
+ backgroundClipText: reasons.includes('background-clip text'),
697
+ });
698
+ }
699
+ return candidates;
700
+ }
701
+
702
+ const visualContrastImageCache = new Map();
703
+ const visualContrastRasterCache = new WeakMap();
704
+
705
+ function clampByte(value) {
706
+ return Math.max(0, Math.min(255, Math.round(value)));
707
+ }
708
+
709
+ function blendRgba(fg, bg) {
710
+ if (!fg) return bg || null;
711
+ if (!bg || fg.a == null || fg.a >= 0.999) {
712
+ return { r: clampByte(fg.r), g: clampByte(fg.g), b: clampByte(fg.b), a: fg.a == null ? 1 : fg.a };
713
+ }
714
+ const alpha = Math.max(0, Math.min(1, fg.a));
715
+ return {
716
+ r: clampByte(fg.r * alpha + bg.r * (1 - alpha)),
717
+ g: clampByte(fg.g * alpha + bg.g * (1 - alpha)),
718
+ b: clampByte(fg.b * alpha + bg.b * (1 - alpha)),
719
+ a: 1,
720
+ };
721
+ }
722
+
723
+ function pickWorstContrastColor(textColor, colors) {
724
+ const usable = (colors || []).filter(Boolean);
725
+ if (!usable.length) return null;
726
+ let worst = usable[0];
727
+ let worstRatio = contrastRatio(textColor, worst);
728
+ for (const color of usable.slice(1)) {
729
+ const ratio = contrastRatio(textColor, color);
730
+ if (ratio < worstRatio) {
731
+ worst = color;
732
+ worstRatio = ratio;
733
+ }
734
+ }
735
+ return worst;
736
+ }
737
+
738
+ function firstCssUrl(value) {
739
+ const match = String(value || '').match(/url\((?:"([^"]+)"|'([^']+)'|([^)]*))\)/i);
740
+ if (!match) return '';
741
+ return (match[1] || match[2] || match[3] || '').trim();
742
+ }
743
+
744
+ function getLayerValue(value, index = 0) {
745
+ return String(value || '').split(',')[index]?.trim() || '';
746
+ }
747
+
748
+ function parsePositionToken(token, container, painted) {
749
+ if (!token || token === 'center') return (container - painted) / 2;
750
+ if (token === 'left' || token === 'top') return 0;
751
+ if (token === 'right' || token === 'bottom') return container - painted;
752
+ if (/%$/.test(token)) {
753
+ const pct = parseFloat(token) / 100;
754
+ return (container - painted) * pct;
755
+ }
756
+ if (/px$/.test(token)) return parseFloat(token) || 0;
757
+ return (container - painted) / 2;
758
+ }
759
+
760
+ function parsePositionPair(positionValue) {
761
+ const tokens = String(positionValue || '50% 50%').trim().split(/\s+/).filter(Boolean);
762
+ const first = tokens[0] || '50%';
763
+ if (tokens.length < 2) {
764
+ if (first === 'top' || first === 'bottom') return ['50%', first];
765
+ return [first, '50%'];
766
+ }
767
+ return [first, tokens[1] || '50%'];
768
+ }
769
+
770
+ function resolvePaintedImageRect(containerRect, image, sizeValue, positionValue) {
771
+ const intrinsicWidth = image.naturalWidth || image.videoWidth || image.width || 1;
772
+ const intrinsicHeight = image.naturalHeight || image.videoHeight || image.height || 1;
773
+ let paintedWidth = intrinsicWidth;
774
+ let paintedHeight = intrinsicHeight;
775
+ const size = String(sizeValue || 'auto').trim();
776
+
777
+ if (size === 'cover' || size === 'contain') {
778
+ const scale = size === 'cover'
779
+ ? Math.max(containerRect.width / intrinsicWidth, containerRect.height / intrinsicHeight)
780
+ : Math.min(containerRect.width / intrinsicWidth, containerRect.height / intrinsicHeight);
781
+ paintedWidth = intrinsicWidth * scale;
782
+ paintedHeight = intrinsicHeight * scale;
783
+ } else if (size && size !== 'auto') {
784
+ const parts = size.split(/\s+/);
785
+ const widthToken = parts[0];
786
+ const heightToken = parts[1] || 'auto';
787
+ if (/%$/.test(widthToken)) paintedWidth = containerRect.width * (parseFloat(widthToken) / 100);
788
+ else if (/px$/.test(widthToken)) paintedWidth = parseFloat(widthToken) || paintedWidth;
789
+ if (heightToken === 'auto') paintedHeight = paintedWidth * (intrinsicHeight / intrinsicWidth);
790
+ else if (/%$/.test(heightToken)) paintedHeight = containerRect.height * (parseFloat(heightToken) / 100);
791
+ else if (/px$/.test(heightToken)) paintedHeight = parseFloat(heightToken) || paintedHeight;
792
+ }
793
+
794
+ const [xToken, yToken] = parsePositionPair(positionValue);
795
+ const positionX = parsePositionToken(xToken, containerRect.width, paintedWidth);
796
+ const positionY = parsePositionToken(yToken, containerRect.height, paintedHeight);
797
+ return {
798
+ left: containerRect.left + positionX,
799
+ top: containerRect.top + positionY,
800
+ width: paintedWidth,
801
+ height: paintedHeight,
802
+ intrinsicWidth,
803
+ intrinsicHeight,
804
+ };
805
+ }
806
+
807
+ function parseObjectPosition(positionValue) {
808
+ return parsePositionPair(positionValue);
809
+ }
810
+
811
+ function resolveObjectImageRect(containerRect, image, style) {
812
+ const intrinsicWidth = image.naturalWidth || image.videoWidth || image.width || 1;
813
+ const intrinsicHeight = image.naturalHeight || image.videoHeight || image.height || 1;
814
+ const fit = style.objectFit || 'fill';
815
+ let paintedWidth = containerRect.width;
816
+ let paintedHeight = containerRect.height;
817
+ if (fit === 'contain' || fit === 'cover') {
818
+ const scale = fit === 'cover'
819
+ ? Math.max(containerRect.width / intrinsicWidth, containerRect.height / intrinsicHeight)
820
+ : Math.min(containerRect.width / intrinsicWidth, containerRect.height / intrinsicHeight);
821
+ paintedWidth = intrinsicWidth * scale;
822
+ paintedHeight = intrinsicHeight * scale;
823
+ } else if (fit === 'none') {
824
+ paintedWidth = intrinsicWidth;
825
+ paintedHeight = intrinsicHeight;
826
+ } else if (fit === 'scale-down') {
827
+ const containScale = Math.min(containerRect.width / intrinsicWidth, containerRect.height / intrinsicHeight, 1);
828
+ paintedWidth = intrinsicWidth * containScale;
829
+ paintedHeight = intrinsicHeight * containScale;
830
+ }
831
+ const [xToken, yToken] = parseObjectPosition(style.objectPosition);
832
+ return {
833
+ left: containerRect.left + parsePositionToken(xToken, containerRect.width, paintedWidth),
834
+ top: containerRect.top + parsePositionToken(yToken, containerRect.height, paintedHeight),
835
+ width: paintedWidth,
836
+ height: paintedHeight,
837
+ intrinsicWidth,
838
+ intrinsicHeight,
839
+ };
840
+ }
841
+
842
+ function pointToImageSource(point, paintedRect) {
843
+ if (
844
+ point.x < paintedRect.left ||
845
+ point.y < paintedRect.top ||
846
+ point.x > paintedRect.left + paintedRect.width ||
847
+ point.y > paintedRect.top + paintedRect.height
848
+ ) {
849
+ return null;
850
+ }
851
+ return {
852
+ x: Math.max(0, Math.min(paintedRect.intrinsicWidth - 1, ((point.x - paintedRect.left) / paintedRect.width) * paintedRect.intrinsicWidth)),
853
+ y: Math.max(0, Math.min(paintedRect.intrinsicHeight - 1, ((point.y - paintedRect.top) / paintedRect.height) * paintedRect.intrinsicHeight)),
854
+ };
855
+ }
856
+
857
+ async function loadVisualContrastImage(src) {
858
+ if (!src) return null;
859
+ if (visualContrastImageCache.has(src)) return visualContrastImageCache.get(src);
860
+ const promise = new Promise(resolve => {
861
+ const img = new Image();
862
+ let settled = false;
863
+ const finish = value => {
864
+ if (settled) return;
865
+ settled = true;
866
+ clearTimeout(timer);
867
+ resolve(value);
868
+ };
869
+ const timer = setTimeout(() => finish(null), 800);
870
+ try {
871
+ const absolute = new URL(src, location.href);
872
+ if (absolute.origin !== location.origin && absolute.protocol !== 'data:' && absolute.protocol !== 'blob:') {
873
+ img.crossOrigin = 'anonymous';
874
+ }
875
+ } catch {
876
+ // Let the browser resolve unusual URLs itself.
877
+ }
878
+ img.onload = () => finish(img);
879
+ img.onerror = () => finish(null);
880
+ img.src = src;
881
+ });
882
+ visualContrastImageCache.set(src, promise);
883
+ return promise;
884
+ }
885
+
886
+ function sampleDrawablePixel(drawable, sourcePoint) {
887
+ if (visualContrastRasterCache.has(drawable)) {
888
+ const cached = visualContrastRasterCache.get(drawable);
889
+ if (!cached || !cached.ctx) return { status: 'unresolved', reason: cached?.reason || 'image sample failed' };
890
+ try {
891
+ const x = Math.max(0, Math.min(cached.width - 1, Math.floor(sourcePoint.x * cached.scaleX)));
892
+ const y = Math.max(0, Math.min(cached.height - 1, Math.floor(sourcePoint.y * cached.scaleY)));
893
+ const data = cached.ctx.getImageData(x, y, 1, 1).data;
894
+ return {
895
+ status: 'sampled',
896
+ color: { r: data[0], g: data[1], b: data[2], a: data[3] / 255 },
897
+ };
898
+ } catch (err) {
899
+ return {
900
+ status: 'unresolved',
901
+ reason: /taint|cross-origin|Security/i.test(err?.message || '') ? 'tainted image' : 'image sample failed',
902
+ };
903
+ }
904
+ }
905
+
906
+ const canvas = document.createElement('canvas');
907
+ const intrinsicWidth = drawable.naturalWidth || drawable.videoWidth || drawable.width || 1;
908
+ const intrinsicHeight = drawable.naturalHeight || drawable.videoHeight || drawable.height || 1;
909
+ const maxRasterSide = 640;
910
+ const scale = Math.min(1, maxRasterSide / Math.max(intrinsicWidth, intrinsicHeight));
911
+ canvas.width = Math.max(1, Math.round(intrinsicWidth * scale));
912
+ canvas.height = Math.max(1, Math.round(intrinsicHeight * scale));
913
+ const ctx = canvas.getContext('2d', { willReadFrequently: true });
914
+ if (!ctx) return { status: 'unresolved', reason: 'canvas unavailable' };
915
+ try {
916
+ ctx.drawImage(drawable, 0, 0, canvas.width, canvas.height);
917
+ const cached = {
918
+ ctx,
919
+ width: canvas.width,
920
+ height: canvas.height,
921
+ scaleX: canvas.width / intrinsicWidth,
922
+ scaleY: canvas.height / intrinsicHeight,
923
+ };
924
+ visualContrastRasterCache.set(drawable, cached);
925
+ const x = Math.max(0, Math.min(cached.width - 1, Math.floor(sourcePoint.x * cached.scaleX)));
926
+ const y = Math.max(0, Math.min(cached.height - 1, Math.floor(sourcePoint.y * cached.scaleY)));
927
+ const data = ctx.getImageData(x, y, 1, 1).data;
928
+ return {
929
+ status: 'sampled',
930
+ color: { r: data[0], g: data[1], b: data[2], a: data[3] / 255 },
931
+ };
932
+ } catch (err) {
933
+ const reason = /taint|cross-origin|Security/i.test(err?.message || '') ? 'tainted image' : 'image sample failed';
934
+ visualContrastRasterCache.set(drawable, { ctx: null, reason });
935
+ return {
936
+ status: 'unresolved',
937
+ reason,
938
+ };
939
+ }
940
+ }
941
+
942
+ async function sampleCssBackground(el, style, point, textColor) {
943
+ const rect = el.getBoundingClientRect();
944
+ const bgImage = style.backgroundImage || '';
945
+ if (bgImage && bgImage !== 'none') {
946
+ if (/gradient/i.test(bgImage)) {
947
+ const color = pickWorstContrastColor(textColor, parseGradientColors(bgImage));
948
+ if (color) return { status: 'sampled', color, method: 'analytic-gradient' };
949
+ }
950
+ if (/url\s*\(/i.test(bgImage)) {
951
+ const img = await loadVisualContrastImage(firstCssUrl(bgImage));
952
+ if (!img) return { status: 'unresolved', reason: 'image unavailable' };
953
+ const paintedRect = resolvePaintedImageRect(
954
+ rect,
955
+ img,
956
+ getLayerValue(style.backgroundSize) || 'auto',
957
+ getLayerValue(style.backgroundPosition) || '50% 50%',
958
+ );
959
+ const sourcePoint = pointToImageSource(point, paintedRect);
960
+ if (!sourcePoint) return { status: 'unresolved', reason: 'point outside background image' };
961
+ const sample = sampleDrawablePixel(img, sourcePoint);
962
+ if (sample.status === 'sampled') return { ...sample, method: 'canvas-background-image' };
963
+ return sample;
964
+ }
965
+ }
966
+ const bg = parseRgb(style.backgroundColor);
967
+ if (bg && bg.a > 0.05) return { status: 'sampled', color: bg, method: 'solid-background' };
968
+ return { status: 'unresolved', reason: 'no readable background' };
969
+ }
970
+
971
+ async function sampleImageElement(img, point) {
972
+ const rect = img.getBoundingClientRect();
973
+ const style = getComputedStyle(img);
974
+ const paintedRect = resolveObjectImageRect(rect, img, style);
975
+ const sourcePoint = pointToImageSource(point, paintedRect);
976
+ if (!sourcePoint) return { status: 'unresolved', reason: 'point outside image' };
977
+ const sample = sampleDrawablePixel(img, sourcePoint);
978
+ if (sample.status === 'sampled') return { ...sample, method: 'canvas-img-underlay' };
979
+
980
+ if (img.currentSrc || img.src) {
981
+ const loaded = await loadVisualContrastImage(img.currentSrc || img.src);
982
+ if (loaded) {
983
+ const loadedRect = { ...paintedRect, intrinsicWidth: loaded.naturalWidth || loaded.width || paintedRect.intrinsicWidth, intrinsicHeight: loaded.naturalHeight || loaded.height || paintedRect.intrinsicHeight };
984
+ const loadedPoint = pointToImageSource(point, loadedRect);
985
+ if (loadedPoint) {
986
+ const loadedSample = sampleDrawablePixel(loaded, loadedPoint);
987
+ if (loadedSample.status === 'sampled') return { ...loadedSample, method: 'canvas-img-underlay' };
988
+ }
989
+ }
990
+ }
991
+ return sample;
992
+ }
993
+
994
+ function textSamplePoints(rect) {
995
+ const insetX = Math.min(12, Math.max(1, rect.width * 0.12));
996
+ const insetY = Math.min(8, Math.max(1, rect.height * 0.22));
997
+ const xs = rect.width < 28
998
+ ? [rect.left + rect.width / 2]
999
+ : [rect.left + insetX, rect.left + rect.width / 2, rect.right - insetX];
1000
+ const ys = rect.height < 22
1001
+ ? [rect.top + rect.height / 2]
1002
+ : [rect.top + insetY, rect.top + rect.height / 2, rect.bottom - insetY];
1003
+ const points = [];
1004
+ for (const y of ys) {
1005
+ for (const x of xs) {
1006
+ if (x >= 0 && y >= 0 && x <= window.innerWidth && y <= window.innerHeight) points.push({ x, y });
1007
+ }
1008
+ }
1009
+ return points;
1010
+ }
1011
+
1012
+ async function sampleVisualBackgroundAtPoint(el, point, textColor, depth = 0) {
1013
+ if (depth > 8) {
1014
+ return { status: 'unresolved', reason: 'background stack too deep' };
1015
+ }
1016
+ const stack = typeof document.elementsFromPoint === 'function'
1017
+ ? document.elementsFromPoint(point.x, point.y)
1018
+ : [];
1019
+ const selfIndex = stack.findIndex(node => node === el || el.contains(node));
1020
+ const nodes = selfIndex >= 0 ? stack.slice(selfIndex) : [el, ...stack];
1021
+ const unresolved = [];
1022
+
1023
+ for (const node of nodes) {
1024
+ if (!node || node.nodeType !== 1) continue;
1025
+ if (node.closest?.('.impeccable-overlay, .impeccable-label, .impeccable-banner, .impeccable-tooltip')) continue;
1026
+ const tag = node.tagName?.toLowerCase();
1027
+ if (tag === 'img') {
1028
+ const sample = await sampleImageElement(node, point);
1029
+ if (sample.status === 'sampled') return sample;
1030
+ unresolved.push(sample.reason);
1031
+ continue;
1032
+ }
1033
+ if (tag === 'canvas' || tag === 'video') {
1034
+ const rect = node.getBoundingClientRect();
1035
+ const sourcePoint = pointToImageSource(point, {
1036
+ left: rect.left,
1037
+ top: rect.top,
1038
+ width: rect.width,
1039
+ height: rect.height,
1040
+ intrinsicWidth: node.width || node.videoWidth || rect.width,
1041
+ intrinsicHeight: node.height || node.videoHeight || rect.height,
1042
+ });
1043
+ if (sourcePoint) {
1044
+ const sample = sampleDrawablePixel(node, sourcePoint);
1045
+ if (sample.status === 'sampled') return { ...sample, method: `canvas-${tag}-underlay` };
1046
+ unresolved.push(sample.reason);
1047
+ }
1048
+ continue;
1049
+ }
1050
+ const style = getComputedStyle(node);
1051
+ const sample = await sampleCssBackground(node, style, point, textColor);
1052
+ if (sample.status === 'sampled') {
1053
+ if (!sample.color || sample.color.a == null || sample.color.a >= 0.95) return sample;
1054
+ const under = await sampleVisualBackgroundAtPoint(node.parentElement || document.body, point, textColor, depth + 1);
1055
+ if (under.status === 'sampled') {
1056
+ return {
1057
+ status: 'sampled',
1058
+ color: blendRgba(sample.color, under.color),
1059
+ method: `${sample.method}+alpha`,
1060
+ };
1061
+ }
1062
+ return sample;
1063
+ }
1064
+ unresolved.push(sample.reason);
1065
+ }
1066
+
1067
+ return {
1068
+ status: 'unresolved',
1069
+ reason: [...new Set(unresolved.filter(Boolean))].slice(0, 3).join(', ') || 'no readable visual background',
1070
+ };
1071
+ }
1072
+
1073
+ async function analyzeVisualContrastCandidate(candidate) {
1074
+ let el;
1075
+ try {
1076
+ el = document.querySelector(candidate.selector);
1077
+ } catch {
1078
+ return { ...candidate, status: 'unresolved', confidence: 'none', reason: 'stale selector' };
1079
+ }
1080
+ if (!el) return { ...candidate, status: 'unresolved', confidence: 'none', reason: 'missing element' };
1081
+
1082
+ const blockingReason = (candidate.reasons || []).find(reason =>
1083
+ reason === 'background-clip text' ||
1084
+ reason === 'blend mode' ||
1085
+ reason === 'filter' ||
1086
+ reason === 'backdrop filter' ||
1087
+ reason === 'opacity stack' ||
1088
+ reason === 'text shadow'
1089
+ );
1090
+ if (blockingReason) {
1091
+ return { ...candidate, status: 'unresolved', confidence: 'none', reason: `${blockingReason} needs screenshot pixels` };
1092
+ }
1093
+
1094
+ const style = getComputedStyle(el);
1095
+ const textColor = parseRgb(style.color) || candidate.textColor;
1096
+ if (!textColor) return { ...candidate, status: 'unresolved', confidence: 'none', reason: 'unreadable text color' };
1097
+
1098
+ const rect = getDirectTextRect(el) || el.getBoundingClientRect();
1099
+ if (!rect || rect.width < 4 || rect.height < 4) {
1100
+ return { ...candidate, status: 'unresolved', confidence: 'none', reason: 'missing text rect' };
1101
+ }
1102
+
1103
+ const points = textSamplePoints(rect);
1104
+ if (points.length === 0) {
1105
+ return { ...candidate, status: 'unresolved', confidence: 'none', reason: 'text outside viewport' };
1106
+ }
1107
+
1108
+ const ratios = [];
1109
+ const methods = new Set();
1110
+ const unresolved = [];
1111
+ for (const point of points) {
1112
+ const sample = await sampleVisualBackgroundAtPoint(el, point, textColor);
1113
+ if (sample.status !== 'sampled' || !sample.color) {
1114
+ unresolved.push(sample.reason);
1115
+ continue;
1116
+ }
1117
+ const fg = blendRgba(textColor, sample.color);
1118
+ ratios.push(contrastRatio(fg, sample.color));
1119
+ if (sample.method) methods.add(sample.method);
1120
+ }
1121
+
1122
+ if (ratios.length < Math.min(3, points.length)) {
1123
+ return {
1124
+ ...candidate,
1125
+ status: 'unresolved',
1126
+ confidence: 'none',
1127
+ samples: ratios.length,
1128
+ reason: [...new Set(unresolved.filter(Boolean))].slice(0, 3).join(', ') || 'not enough readable samples',
1129
+ };
1130
+ }
1131
+
1132
+ ratios.sort((a, b) => a - b);
1133
+ const pick = pct => ratios[Math.min(ratios.length - 1, Math.max(0, Math.floor((pct / 100) * ratios.length)))];
1134
+ const measuredRatio = pick(10);
1135
+ const medianRatio = pick(50);
1136
+ const status = measuredRatio < candidate.threshold ? 'fail' : 'pass';
1137
+ const method = [...methods].sort().join(', ') || 'browser-visual';
1138
+ const textLabel = candidate.text ? ` "${candidate.text}"` : '';
1139
+ const detail = `browser contrast ${measuredRatio.toFixed(1)}:1 median ${medianRatio.toFixed(1)}:1 (need ${candidate.threshold}:1) via ${method}${textLabel}`;
1140
+ return {
1141
+ ...candidate,
1142
+ status,
1143
+ confidence: method.includes('canvas-') ? 'high' : 'medium',
1144
+ method,
1145
+ ratio: measuredRatio,
1146
+ medianRatio,
1147
+ samples: ratios.length,
1148
+ finding: status === 'fail' ? { id: 'low-contrast', snippet: detail } : null,
1149
+ };
1150
+ }
1151
+
1152
+ function waitForVisualPaint() {
1153
+ return new Promise(resolve => {
1154
+ requestAnimationFrame(() => requestAnimationFrame(resolve));
1155
+ });
1156
+ }
1157
+
1158
+ async function analyzeVisualContrast(options = {}) {
1159
+ const candidates = collectVisualContrastCandidates(options);
1160
+ const results = [];
1161
+ const shouldScrollOffscreen = options.scrollOffscreen === true;
1162
+ const restoreScroll = { x: window.scrollX, y: window.scrollY };
1163
+ for (const candidate of candidates) {
1164
+ if (shouldScrollOffscreen && (window.scrollX !== restoreScroll.x || window.scrollY !== restoreScroll.y)) {
1165
+ window.scrollTo(restoreScroll.x, restoreScroll.y);
1166
+ await waitForVisualPaint();
1167
+ }
1168
+ let result = await analyzeVisualContrastCandidate(candidate);
1169
+ if (shouldScrollOffscreen && result.status === 'unresolved' && result.reason === 'text outside viewport') {
1170
+ let el = null;
1171
+ try {
1172
+ el = document.querySelector(candidate.selector);
1173
+ } catch {
1174
+ el = null;
1175
+ }
1176
+ if (el && typeof el.scrollIntoView === 'function') {
1177
+ el.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
1178
+ await waitForVisualPaint();
1179
+ result = await analyzeVisualContrastCandidate(candidate);
1180
+ }
1181
+ }
1182
+ results.push(result);
1183
+ }
1184
+ if (shouldScrollOffscreen && (window.scrollX !== restoreScroll.x || window.scrollY !== restoreScroll.y)) {
1185
+ window.scrollTo(restoreScroll.x, restoreScroll.y);
1186
+ }
1187
+ return results;
1188
+ }
1189
+
1190
+ function isElementHidden(el) {
1191
+ if (!el || el === document.body || el === document.documentElement) return false;
1192
+ if (typeof el.checkVisibility === 'function') return !el.checkVisibility({ checkOpacity: false, checkVisibilityCSS: true });
1193
+ // Fallback: zero size or no offsetParent (covers display:none and detached subtrees)
1194
+ return el.offsetWidth === 0 && el.offsetHeight === 0;
1195
+ }
1196
+
1197
+ function serializeFindings(allFindings) {
1198
+ return allFindings.map(({ el, findings }) => ({
1199
+ selector: generateSelector(el),
1200
+ tagName: el.tagName?.toLowerCase() || 'unknown',
1201
+ rect: (el !== document.body && el !== document.documentElement && el.getBoundingClientRect)
1202
+ ? el.getBoundingClientRect().toJSON() : null,
1203
+ isPageLevel: el === document.body || el === document.documentElement,
1204
+ isHidden: isElementHidden(el),
1205
+ findings: findings.map(f => {
1206
+ const ap = ANTIPATTERNS.find(a => a.id === (f.type || f.id));
1207
+ return {
1208
+ type: f.type || f.id,
1209
+ category: ap ? ap.category : 'quality',
1210
+ severity: ap?.severity || 'warning',
1211
+ detail: f.detail || f.snippet,
1212
+ name: ap ? ap.name : (f.type || f.id),
1213
+ description: ap ? ap.description : '',
1214
+ };
1215
+ }),
1216
+ }));
1217
+ }
1218
+
1219
+ const printSummary = function(allFindings) {
1220
+ if (allFindings.length === 0) {
1221
+ console.log('%c[impeccable] No anti-patterns found.', 'color: #22c55e; font-weight: bold');
1222
+ return;
1223
+ }
1224
+ console.group(
1225
+ `%c[impeccable] ${allFindings.length} anti-pattern${allFindings.length === 1 ? '' : 's'} found`,
1226
+ 'color: oklch(60% 0.25 350); font-weight: bold'
1227
+ );
1228
+ for (const { el, findings } of allFindings) {
1229
+ for (const f of findings) {
1230
+ console.log(`%c${f.type || f.id}%c ${f.detail || f.snippet}`,
1231
+ 'color: oklch(55% 0.25 350); font-weight: bold', 'color: inherit', el);
1232
+ }
1233
+ }
1234
+ console.groupEnd();
1235
+ };
1236
+
1237
+ function addBrowserFindings(groupMap, el, findings) {
1238
+ if (!findings || findings.length === 0) return;
1239
+ const existing = groupMap.get(el);
1240
+ if (existing) existing.push(...findings);
1241
+ else groupMap.set(el, [...findings]);
1242
+ }
1243
+
1244
+ function browserFindingsFromMap(groupMap) {
1245
+ return [...groupMap.entries()].map(([el, findings]) => ({ el, findings }));
1246
+ }
1247
+
1248
+ function collectBrowserFindings() {
1249
+ const groupMap = new Map();
1250
+ const _disabled = EXTENSION_MODE ? (window.__IMPECCABLE_CONFIG__?.disabledRules || []) : [];
1251
+ const _ruleOk = (id) => !_disabled.length || !_disabled.includes(id);
1252
+
1253
+ for (const el of document.querySelectorAll('*')) {
1254
+ // Skip impeccable's own elements and any descendants (overlays, labels, banner, nav buttons)
1255
+ if (el.closest('.impeccable-overlay, .impeccable-label, .impeccable-banner, .impeccable-tooltip')) continue;
1256
+ // Skip browser extension elements (Claude, etc.)
1257
+ const elId = el.id || '';
1258
+ if (elId.startsWith('claude-') || elId.startsWith('cic-')) continue;
1259
+ // Skip the impeccable live-mode overlay (highlight, tooltip, bar, picker, toast).
1260
+ // These are inspector chrome, not part of the user's design.
1261
+ if (el.closest('[id^="impeccable-live-"]')) continue;
1262
+ // Skip html/body -- page-level findings go in the banner, not a full-page overlay
1263
+ if (el === document.body || el === document.documentElement) continue;
1264
+
1265
+ const findings = [
1266
+ ...checkElementBordersDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
1267
+ ...checkElementColorsDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
1268
+ ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
1269
+ ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
1270
+ ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
1271
+ ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
1272
+ ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
1273
+ ...checkElementHeroEyebrowDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
1274
+ ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
1275
+ ].filter(f => _ruleOk(f.type));
1276
+
1277
+ addBrowserFindings(groupMap, el, findings);
1278
+ }
1279
+
1280
+ const pageLevelFindings = [];
1281
+
1282
+ const typoFindings = checkTypography().filter(f => _ruleOk(f.type));
1283
+ if (typoFindings.length > 0) {
1284
+ pageLevelFindings.push(...typoFindings);
1285
+ addBrowserFindings(groupMap, document.body, typoFindings);
1286
+ }
1287
+
1288
+ const sectionKickerFindings = checkRepeatedSectionKickersDOM()
1289
+ .map(f => ({ type: f.id, detail: f.snippet }))
1290
+ .filter(f => _ruleOk(f.type));
1291
+ if (sectionKickerFindings.length > 0) {
1292
+ pageLevelFindings.push(...sectionKickerFindings);
1293
+ addBrowserFindings(groupMap, document.body, sectionKickerFindings);
1294
+ }
1295
+
1296
+ const layoutFindings = checkLayout().filter(f => _ruleOk(f.type));
1297
+ for (const f of layoutFindings) {
1298
+ const el = f.el || document.body;
1299
+ addBrowserFindings(groupMap, el, [{ type: f.type, detail: f.detail || f.snippet }]);
1300
+ }
1301
+
1302
+ // Page-level quality checks (headings, etc.)
1303
+ const qualityFindings = checkPageQualityDOM().filter(f => _ruleOk(f.type));
1304
+ if (qualityFindings.length > 0) {
1305
+ pageLevelFindings.push(...qualityFindings);
1306
+ addBrowserFindings(groupMap, document.body, qualityFindings);
1307
+ }
1308
+
1309
+ // Regex-on-HTML checks (shared with Node)
1310
+ // Clone the document and strip impeccable-live overlay nodes before the
1311
+ // regex scan, so the inspector's own inline styles (transitions on top/
1312
+ // left/width/height, etc.) don't register as page anti-patterns.
1313
+ const docClone = document.documentElement.cloneNode(true);
1314
+ for (const node of docClone.querySelectorAll('[id^="impeccable-live-"]')) {
1315
+ node.remove();
1316
+ }
1317
+ const htmlPatternFindings = checkHtmlPatterns(docClone.outerHTML);
1318
+ if (htmlPatternFindings.length > 0) {
1319
+ const mapped = htmlPatternFindings.map(f => ({ type: f.id, detail: f.snippet })).filter(f => _ruleOk(f.type));
1320
+ pageLevelFindings.push(...mapped);
1321
+ addBrowserFindings(groupMap, document.body, mapped);
1322
+ }
1323
+
1324
+ return {
1325
+ groupMap,
1326
+ allFindings: browserFindingsFromMap(groupMap),
1327
+ pageLevelFindings,
1328
+ };
1329
+ }
1330
+
1331
+ function shouldRunVisualContrast(options = {}) {
1332
+ return options.visualContrast === true || window.__IMPECCABLE_CONFIG__?.visualContrast === true;
1333
+ }
1334
+
1335
+ function visualContrastOptions(options = {}) {
1336
+ const config = window.__IMPECCABLE_CONFIG__ || {};
1337
+ const scrollOffscreen = typeof options.scrollOffscreen === 'boolean'
1338
+ ? options.scrollOffscreen
1339
+ : typeof options.visualContrastScrollOffscreen === 'boolean'
1340
+ ? options.visualContrastScrollOffscreen
1341
+ : typeof config.visualContrastScrollOffscreen === 'boolean'
1342
+ ? config.visualContrastScrollOffscreen
1343
+ : false;
1344
+ return {
1345
+ ...options,
1346
+ maxCandidates: Number.isFinite(options.visualContrastMaxCandidates)
1347
+ ? options.visualContrastMaxCandidates
1348
+ : Number.isFinite(options.maxCandidates)
1349
+ ? options.maxCandidates
1350
+ : Number.isFinite(config.visualContrastMaxCandidates)
1351
+ ? config.visualContrastMaxCandidates
1352
+ : undefined,
1353
+ scrollOffscreen,
1354
+ };
1355
+ }
1356
+
1357
+ let lastVisualContrastAnalyses = [];
1358
+ let lazyVisualContrastObserver = null;
1359
+ let lazyVisualContrastPending = new WeakMap();
1360
+ const lazyVisualContrastResolving = new WeakSet();
1361
+ let scanGeneration = 0;
1362
+
1363
+ function rememberVisualContrastAnalysis(result) {
1364
+ if (!result?.selector) {
1365
+ lastVisualContrastAnalyses.push(result);
1366
+ return;
1367
+ }
1368
+ const idx = lastVisualContrastAnalyses.findIndex(item => item.selector === result.selector);
1369
+ if (idx >= 0) lastVisualContrastAnalyses[idx] = result;
1370
+ else lastVisualContrastAnalyses.push(result);
1371
+ }
1372
+
1373
+ function disconnectLazyVisualContrastObserver() {
1374
+ if (lazyVisualContrastObserver) {
1375
+ lazyVisualContrastObserver.disconnect();
1376
+ lazyVisualContrastObserver = null;
1377
+ }
1378
+ lazyVisualContrastPending = new WeakMap();
1379
+ }
1380
+
1381
+ function addVisualContrastResult(groupMap, result, options = {}) {
1382
+ if (result.status !== 'fail' || !result.finding || !result.selector) return false;
1383
+ let el = null;
1384
+ try {
1385
+ el = document.querySelector(result.selector);
1386
+ } catch {
1387
+ el = null;
1388
+ }
1389
+ if (!el) return false;
1390
+ const findingType = result.finding.type || result.finding.id || 'low-contrast';
1391
+ const existing = groupMap.get(el) || [];
1392
+ if (existing.some(f => (f.type || f.id) === findingType)) return false;
1393
+ addBrowserFindings(groupMap, el, [{
1394
+ type: findingType,
1395
+ detail: result.finding.detail || result.finding.snippet,
1396
+ }]);
1397
+ if (options.decorate && el !== document.body && el !== document.documentElement) {
1398
+ highlight(el, groupMap.get(el) || []);
1399
+ }
1400
+ return true;
1401
+ }
1402
+
1403
+ function postSerializedFindings(groupMap) {
1404
+ if (!EXTENSION_MODE) return;
1405
+ const allFindings = browserFindingsFromMap(groupMap);
1406
+ window.postMessage({
1407
+ source: 'impeccable-results',
1408
+ findings: serializeFindings(allFindings),
1409
+ count: allFindings.length,
1410
+ }, '*');
1411
+ }
1412
+
1413
+ function postExtensionError(err) {
1414
+ if (!EXTENSION_MODE) return;
1415
+ window.postMessage({
1416
+ source: 'impeccable-error',
1417
+ message: err?.message || String(err),
1418
+ }, '*');
1419
+ }
1420
+
1421
+ function reportVisualContrastError(err, detail = {}) {
1422
+ window.dispatchEvent(new CustomEvent('impeccable-visual-contrast-error', {
1423
+ detail: {
1424
+ ...detail,
1425
+ message: err?.message || String(err),
1426
+ },
1427
+ }));
1428
+ if (EXTENSION_MODE) {
1429
+ postExtensionError(err);
1430
+ } else {
1431
+ console.warn('[impeccable] visual contrast scan failed', err);
1432
+ }
1433
+ }
1434
+
1435
+ function scheduleLazyVisualContrast(groupMap, analyses, options = {}, runtime = {}) {
1436
+ disconnectLazyVisualContrastObserver();
1437
+ if (options.visualContrastLazy === false || options.scrollOffscreen !== false) return;
1438
+ if (typeof IntersectionObserver === 'undefined') return;
1439
+ const unresolved = (analyses || []).filter(result =>
1440
+ result?.status === 'unresolved' &&
1441
+ result.reason === 'text outside viewport' &&
1442
+ result.selector
1443
+ );
1444
+ if (unresolved.length === 0) return;
1445
+ const generation = runtime.generation || scanGeneration;
1446
+
1447
+ lazyVisualContrastObserver = new IntersectionObserver((entries) => {
1448
+ for (const entry of entries) {
1449
+ if (!entry.isIntersecting) continue;
1450
+ const el = entry.target;
1451
+ const candidate = lazyVisualContrastPending.get(el);
1452
+ if (!candidate || lazyVisualContrastResolving.has(el)) continue;
1453
+ lazyVisualContrastObserver?.unobserve(el);
1454
+ lazyVisualContrastPending.delete(el);
1455
+ lazyVisualContrastResolving.add(el);
1456
+ waitForVisualPaint()
1457
+ .then(() => analyzeVisualContrastCandidate(candidate))
1458
+ .then(result => {
1459
+ if (generation !== scanGeneration) return;
1460
+ rememberVisualContrastAnalysis(result);
1461
+ const added = addVisualContrastResult(groupMap, result, { decorate: true });
1462
+ if (added) {
1463
+ postSerializedFindings(groupMap);
1464
+ window.dispatchEvent(new CustomEvent('impeccable-visual-contrast-resolved', {
1465
+ detail: {
1466
+ selector: result.selector,
1467
+ status: result.status,
1468
+ finding: result.finding || null,
1469
+ },
1470
+ }));
1471
+ }
1472
+ })
1473
+ .catch(err => {
1474
+ reportVisualContrastError(err, { selector: candidate.selector });
1475
+ })
1476
+ .finally(() => {
1477
+ lazyVisualContrastResolving.delete(el);
1478
+ });
1479
+ }
1480
+ }, { threshold: 0.5 });
1481
+
1482
+ for (const candidate of unresolved) {
1483
+ let el = null;
1484
+ try {
1485
+ el = document.querySelector(candidate.selector);
1486
+ } catch {
1487
+ el = null;
1488
+ }
1489
+ if (!el) continue;
1490
+ lazyVisualContrastPending.set(el, candidate);
1491
+ lazyVisualContrastObserver.observe(el);
1492
+ }
1493
+ }
1494
+
1495
+ async function addVisualContrastFindings(groupMap, options = {}, runtime = {}) {
1496
+ if (!shouldRunVisualContrast(options)) {
1497
+ lastVisualContrastAnalyses = [];
1498
+ disconnectLazyVisualContrastObserver();
1499
+ return [];
1500
+ }
1501
+ const resolvedOptions = visualContrastOptions(options);
1502
+ const analyses = await analyzeVisualContrast(resolvedOptions);
1503
+ if (runtime.generation && runtime.generation !== scanGeneration) return analyses;
1504
+ lastVisualContrastAnalyses = analyses;
1505
+ for (const result of analyses) {
1506
+ addVisualContrastResult(groupMap, result, { decorate: runtime.decorate });
1507
+ }
1508
+ if (runtime.decorate || runtime.scheduleLazy) scheduleLazyVisualContrast(groupMap, analyses, resolvedOptions, runtime);
1509
+ return analyses;
1510
+ }
1511
+
1512
+ async function collectBrowserFindingsAsync(options = {}, runtime = {}) {
1513
+ const collected = collectBrowserFindings();
1514
+ await addVisualContrastFindings(collected.groupMap, options, runtime);
1515
+ return {
1516
+ ...collected,
1517
+ allFindings: browserFindingsFromMap(collected.groupMap),
1518
+ visualContrastAnalyses: lastVisualContrastAnalyses,
1519
+ };
1520
+ }
1521
+
1522
+ function clearOverlays() {
1523
+ scanGeneration += 1;
1524
+ disconnectLazyVisualContrastObserver();
1525
+ for (const o of [...overlays]) detachOverlay(o);
1526
+ overlays.length = 0;
1527
+ visibilityObserver.disconnect();
1528
+ overlayIndex = 0;
1529
+ }
1530
+
1531
+ function renderBrowserFindings(collected) {
1532
+ const { allFindings, pageLevelFindings } = collected;
1533
+
1534
+ for (const { el, findings } of allFindings) {
1535
+ if (el === document.body || el === document.documentElement) continue;
1536
+ highlight(el, findings);
1537
+ }
1538
+
1539
+ if (pageLevelFindings.length > 0) {
1540
+ showPageBanner(pageLevelFindings);
1541
+ }
1542
+
1543
+ if (!EXTENSION_MODE) printSummary(allFindings);
1544
+
1545
+ // In extension mode, post serialized results for the DevTools panel
1546
+ if (EXTENSION_MODE) {
1547
+ window.postMessage({
1548
+ source: 'impeccable-results',
1549
+ findings: serializeFindings(allFindings),
1550
+ count: allFindings.length,
1551
+ }, '*');
1552
+ }
1553
+
1554
+ // After this scan completes, all subsequent reveals are instant (no stagger, no animation)
1555
+ setTimeout(() => { firstScanDone = true; }, 1000);
1556
+
1557
+ return allFindings;
1558
+ }
1559
+
1560
+ let firstScanDone = false;
1561
+ const scan = function(options = {}) {
1562
+ clearOverlays();
1563
+ const generation = scanGeneration;
1564
+ const collected = collectBrowserFindings();
1565
+ const allFindings = renderBrowserFindings(collected);
1566
+ if (shouldRunVisualContrast(options)) {
1567
+ addVisualContrastFindings(collected.groupMap, options, { decorate: true, generation })
1568
+ .then(() => {
1569
+ if (generation === scanGeneration) postSerializedFindings(collected.groupMap);
1570
+ })
1571
+ .catch(err => {
1572
+ reportVisualContrastError(err);
1573
+ });
1574
+ }
1575
+ return allFindings;
1576
+ };
1577
+
1578
+ const scanAsync = async function(options = {}) {
1579
+ clearOverlays();
1580
+ const generation = scanGeneration;
1581
+ if (shouldRunVisualContrast(options)) {
1582
+ const collected = await collectBrowserFindingsAsync(options, { generation, scheduleLazy: true });
1583
+ if (generation !== scanGeneration) return [];
1584
+ return renderBrowserFindings(collected);
1585
+ }
1586
+ lastVisualContrastAnalyses = [];
1587
+ return renderBrowserFindings(collectBrowserFindings());
1588
+ };
1589
+
1590
+ const detect = function(options = {}) {
1591
+ lastVisualContrastAnalyses = [];
1592
+ const { allFindings } = collectBrowserFindings();
1593
+ return options.serialize === false ? allFindings : serializeFindings(allFindings);
1594
+ };
1595
+
1596
+ const detectAsync = async function(options = {}) {
1597
+ if (shouldRunVisualContrast(options)) {
1598
+ const { allFindings } = await collectBrowserFindingsAsync(options);
1599
+ return options.serialize === false ? allFindings : serializeFindings(allFindings);
1600
+ }
1601
+ lastVisualContrastAnalyses = [];
1602
+ const { allFindings } = collectBrowserFindings();
1603
+ return options.serialize === false ? allFindings : serializeFindings(allFindings);
1604
+ };
1605
+
1606
+ if (EXTENSION_MODE) {
1607
+ // Extension mode: listen for commands, don't auto-scan
1608
+ window.addEventListener('message', (e) => {
1609
+ if (e.source !== window || !e.data || e.data.source !== 'impeccable-command') return;
1610
+ if (e.data.action === 'scan') {
1611
+ if (e.data.config) window.__IMPECCABLE_CONFIG__ = e.data.config;
1612
+ try {
1613
+ scan(e.data.config || {});
1614
+ } catch (err) {
1615
+ postExtensionError(err);
1616
+ }
1617
+ }
1618
+ if (e.data.action === 'toggle-overlays') {
1619
+ const visible = !document.body.classList.contains('impeccable-hidden');
1620
+ document.body.classList.toggle('impeccable-hidden', visible);
1621
+ window.postMessage({ source: 'impeccable-overlays-toggled', visible: !visible }, '*');
1622
+ }
1623
+ if (e.data.action === 'remove') {
1624
+ clearOverlays();
1625
+ styleEl.remove();
1626
+ if (spotlightBackdrop) { spotlightBackdrop.remove(); spotlightBackdrop = null; }
1627
+ document.body.classList.remove('impeccable-hidden');
1628
+ }
1629
+ if (e.data.action === 'highlight') {
1630
+ try {
1631
+ const target = e.data.selector ? document.querySelector(e.data.selector) : null;
1632
+ if (target) {
1633
+ // Scroll first so positionOverlay reads the post-scroll rect
1634
+ if (!isInViewport(target) && target.scrollIntoView) {
1635
+ target.scrollIntoView({ behavior: 'instant', block: 'center' });
1636
+ }
1637
+ for (const o of overlays) {
1638
+ if (o.classList.contains('impeccable-banner')) continue;
1639
+ const isMatch = o._targetEl === target;
1640
+ o.classList.toggle('impeccable-spotlight', isMatch);
1641
+ o.classList.toggle('impeccable-spotlight-dimmed', !isMatch);
1642
+ if (isMatch) {
1643
+ // Force the matching overlay visible immediately, don't wait for IntersectionObserver
1644
+ o.style.display = '';
1645
+ o.style.animation = 'none';
1646
+ o.classList.add('impeccable-visible');
1647
+ o._revealed = true;
1648
+ positionOverlay(o);
1649
+ }
1650
+ }
1651
+ showSpotlight(target);
1652
+ }
1653
+ } catch { /* invalid selector */ }
1654
+ }
1655
+ if (e.data.action === 'unhighlight') {
1656
+ hideSpotlight();
1657
+ for (const o of overlays) {
1658
+ o.classList.remove('impeccable-spotlight');
1659
+ o.classList.remove('impeccable-spotlight-dimmed');
1660
+ }
1661
+ }
1662
+ });
1663
+ window.postMessage({ source: 'impeccable-ready' }, '*');
1664
+ } else {
1665
+ if (window.__IMPECCABLE_CONFIG__?.autoScan !== false) {
1666
+ const runAutoScan = () => {
1667
+ try {
1668
+ scan();
1669
+ } catch (err) {
1670
+ console.warn('[impeccable] scan failed', err);
1671
+ }
1672
+ };
1673
+ if (document.readyState === 'loading') {
1674
+ document.addEventListener('DOMContentLoaded', () => setTimeout(runAutoScan, 100));
1675
+ } else {
1676
+ setTimeout(runAutoScan, 100);
1677
+ }
1678
+ }
1679
+ }
1680
+
1681
+ window.impeccableDetect = detect;
1682
+ window.impeccableDetectAsync = detectAsync;
1683
+ window.impeccableScan = scan;
1684
+ window.impeccableScanAsync = scanAsync;
1685
+ window.impeccableCollectVisualContrastCandidates = collectVisualContrastCandidates;
1686
+ window.impeccableAnalyzeVisualContrast = analyzeVisualContrast;
1687
+ window.impeccableGetLastVisualContrastAnalyses = () => lastVisualContrastAnalyses.slice();
1688
+ }