@bastani/atomic 0.6.8-0 → 0.7.0-1

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 (765) hide show
  1. package/bin/atomic +65 -0
  2. package/package.json +17 -82
  3. package/postinstall.mjs +47 -0
  4. package/.agents/skills/ado-commit/SKILL.md +0 -94
  5. package/.agents/skills/ado-create-pr/SKILL.md +0 -211
  6. package/.agents/skills/advanced-evaluation/SKILL.md +0 -404
  7. package/.agents/skills/advanced-evaluation/references/bias-mitigation.md +0 -288
  8. package/.agents/skills/advanced-evaluation/references/evaluation-pipeline.md +0 -43
  9. package/.agents/skills/advanced-evaluation/references/implementation-patterns.md +0 -315
  10. package/.agents/skills/advanced-evaluation/references/metrics-guide.md +0 -331
  11. package/.agents/skills/advanced-evaluation/scripts/evaluation_example.py +0 -392
  12. package/.agents/skills/ast-grep/SKILL.md +0 -325
  13. package/.agents/skills/ast-grep/references/rule_reference.md +0 -297
  14. package/.agents/skills/bdi-mental-states/SKILL.md +0 -313
  15. package/.agents/skills/bdi-mental-states/references/bdi-ontology-core.md +0 -207
  16. package/.agents/skills/bdi-mental-states/references/framework-integration.md +0 -582
  17. package/.agents/skills/bdi-mental-states/references/rdf-examples.md +0 -315
  18. package/.agents/skills/bdi-mental-states/references/sparql-competency.md +0 -420
  19. package/.agents/skills/bun/SKILL.md +0 -233
  20. package/.agents/skills/context-compression/SKILL.md +0 -274
  21. package/.agents/skills/context-compression/references/evaluation-framework.md +0 -213
  22. package/.agents/skills/context-compression/scripts/compression_evaluator.py +0 -862
  23. package/.agents/skills/context-compression/tests/test_compression_evaluator.py +0 -56
  24. package/.agents/skills/context-degradation/SKILL.md +0 -208
  25. package/.agents/skills/context-degradation/references/patterns.md +0 -314
  26. package/.agents/skills/context-degradation/scripts/degradation_detector.py +0 -614
  27. package/.agents/skills/context-fundamentals/SKILL.md +0 -203
  28. package/.agents/skills/context-fundamentals/references/context-components.md +0 -283
  29. package/.agents/skills/context-fundamentals/scripts/context_manager.py +0 -533
  30. package/.agents/skills/context-optimization/SKILL.md +0 -197
  31. package/.agents/skills/context-optimization/references/optimization_techniques.md +0 -272
  32. package/.agents/skills/context-optimization/scripts/compaction.py +0 -562
  33. package/.agents/skills/create-spec/SKILL.md +0 -249
  34. package/.agents/skills/docx/LICENSE.txt +0 -30
  35. package/.agents/skills/docx/SKILL.md +0 -592
  36. package/.agents/skills/docx/scripts/__init__.py +0 -1
  37. package/.agents/skills/docx/scripts/accept_changes.py +0 -135
  38. package/.agents/skills/docx/scripts/comment.py +0 -318
  39. package/.agents/skills/docx/scripts/office/helpers/__init__.py +0 -0
  40. package/.agents/skills/docx/scripts/office/helpers/merge_runs.py +0 -199
  41. package/.agents/skills/docx/scripts/office/helpers/simplify_redlines.py +0 -197
  42. package/.agents/skills/docx/scripts/office/pack.py +0 -159
  43. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +0 -1499
  44. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +0 -146
  45. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +0 -1085
  46. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +0 -11
  47. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +0 -3081
  48. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +0 -23
  49. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +0 -185
  50. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +0 -287
  51. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +0 -1676
  52. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +0 -28
  53. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +0 -144
  54. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +0 -174
  55. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +0 -25
  56. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +0 -18
  57. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +0 -59
  58. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +0 -56
  59. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +0 -195
  60. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +0 -582
  61. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +0 -25
  62. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +0 -4439
  63. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +0 -570
  64. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +0 -509
  65. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +0 -12
  66. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +0 -108
  67. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +0 -96
  68. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +0 -3646
  69. package/.agents/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +0 -116
  70. package/.agents/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +0 -42
  71. package/.agents/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +0 -50
  72. package/.agents/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +0 -49
  73. package/.agents/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +0 -33
  74. package/.agents/skills/docx/scripts/office/schemas/mce/mc.xsd +0 -75
  75. package/.agents/skills/docx/scripts/office/schemas/microsoft/wml-2010.xsd +0 -560
  76. package/.agents/skills/docx/scripts/office/schemas/microsoft/wml-2012.xsd +0 -67
  77. package/.agents/skills/docx/scripts/office/schemas/microsoft/wml-2018.xsd +0 -14
  78. package/.agents/skills/docx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +0 -20
  79. package/.agents/skills/docx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +0 -13
  80. package/.agents/skills/docx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +0 -4
  81. package/.agents/skills/docx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +0 -8
  82. package/.agents/skills/docx/scripts/office/soffice.py +0 -183
  83. package/.agents/skills/docx/scripts/office/unpack.py +0 -132
  84. package/.agents/skills/docx/scripts/office/validate.py +0 -111
  85. package/.agents/skills/docx/scripts/office/validators/__init__.py +0 -15
  86. package/.agents/skills/docx/scripts/office/validators/base.py +0 -847
  87. package/.agents/skills/docx/scripts/office/validators/docx.py +0 -446
  88. package/.agents/skills/docx/scripts/office/validators/pptx.py +0 -275
  89. package/.agents/skills/docx/scripts/office/validators/redlining.py +0 -247
  90. package/.agents/skills/docx/scripts/templates/comments.xml +0 -3
  91. package/.agents/skills/docx/scripts/templates/commentsExtended.xml +0 -3
  92. package/.agents/skills/docx/scripts/templates/commentsExtensible.xml +0 -3
  93. package/.agents/skills/docx/scripts/templates/commentsIds.xml +0 -3
  94. package/.agents/skills/docx/scripts/templates/people.xml +0 -3
  95. package/.agents/skills/evaluation/SKILL.md +0 -253
  96. package/.agents/skills/evaluation/references/metrics.md +0 -339
  97. package/.agents/skills/evaluation/scripts/evaluator.py +0 -627
  98. package/.agents/skills/explain-code/SKILL.md +0 -232
  99. package/.agents/skills/filesystem-context/SKILL.md +0 -289
  100. package/.agents/skills/filesystem-context/references/implementation-patterns.md +0 -549
  101. package/.agents/skills/filesystem-context/scripts/filesystem_context.py +0 -425
  102. package/.agents/skills/find-skills/SKILL.md +0 -144
  103. package/.agents/skills/gh-commit/SKILL.md +0 -245
  104. package/.agents/skills/gh-create-pr/SKILL.md +0 -95
  105. package/.agents/skills/hosted-agents/SKILL.md +0 -262
  106. package/.agents/skills/hosted-agents/references/infrastructure-patterns.md +0 -700
  107. package/.agents/skills/hosted-agents/scripts/sandbox_manager.py +0 -590
  108. package/.agents/skills/impeccable/SKILL.md +0 -178
  109. package/.agents/skills/impeccable/agents/openai.yaml +0 -4
  110. package/.agents/skills/impeccable/reference/adapt.md +0 -190
  111. package/.agents/skills/impeccable/reference/animate.md +0 -175
  112. package/.agents/skills/impeccable/reference/audit.md +0 -134
  113. package/.agents/skills/impeccable/reference/bolder.md +0 -113
  114. package/.agents/skills/impeccable/reference/brand.md +0 -114
  115. package/.agents/skills/impeccable/reference/clarify.md +0 -174
  116. package/.agents/skills/impeccable/reference/cognitive-load.md +0 -106
  117. package/.agents/skills/impeccable/reference/color-and-contrast.md +0 -105
  118. package/.agents/skills/impeccable/reference/colorize.md +0 -154
  119. package/.agents/skills/impeccable/reference/craft.md +0 -193
  120. package/.agents/skills/impeccable/reference/critique.md +0 -213
  121. package/.agents/skills/impeccable/reference/delight.md +0 -302
  122. package/.agents/skills/impeccable/reference/distill.md +0 -111
  123. package/.agents/skills/impeccable/reference/document.md +0 -427
  124. package/.agents/skills/impeccable/reference/extract.md +0 -70
  125. package/.agents/skills/impeccable/reference/harden.md +0 -347
  126. package/.agents/skills/impeccable/reference/heuristics-scoring.md +0 -234
  127. package/.agents/skills/impeccable/reference/interaction-design.md +0 -195
  128. package/.agents/skills/impeccable/reference/layout.md +0 -141
  129. package/.agents/skills/impeccable/reference/live.md +0 -594
  130. package/.agents/skills/impeccable/reference/motion-design.md +0 -109
  131. package/.agents/skills/impeccable/reference/onboard.md +0 -234
  132. package/.agents/skills/impeccable/reference/optimize.md +0 -258
  133. package/.agents/skills/impeccable/reference/overdrive.md +0 -130
  134. package/.agents/skills/impeccable/reference/personas.md +0 -178
  135. package/.agents/skills/impeccable/reference/polish.md +0 -232
  136. package/.agents/skills/impeccable/reference/product.md +0 -62
  137. package/.agents/skills/impeccable/reference/quieter.md +0 -99
  138. package/.agents/skills/impeccable/reference/responsive-design.md +0 -114
  139. package/.agents/skills/impeccable/reference/shape.md +0 -151
  140. package/.agents/skills/impeccable/reference/spatial-design.md +0 -100
  141. package/.agents/skills/impeccable/reference/teach.md +0 -156
  142. package/.agents/skills/impeccable/reference/typeset.md +0 -124
  143. package/.agents/skills/impeccable/reference/typography.md +0 -159
  144. package/.agents/skills/impeccable/reference/ux-writing.md +0 -107
  145. package/.agents/skills/impeccable/scripts/cleanup-deprecated.mjs +0 -284
  146. package/.agents/skills/impeccable/scripts/command-metadata.json +0 -94
  147. package/.agents/skills/impeccable/scripts/design-parser.mjs +0 -820
  148. package/.agents/skills/impeccable/scripts/detect-csp.mjs +0 -198
  149. package/.agents/skills/impeccable/scripts/is-generated.mjs +0 -69
  150. package/.agents/skills/impeccable/scripts/live-accept.mjs +0 -595
  151. package/.agents/skills/impeccable/scripts/live-browser.js +0 -4781
  152. package/.agents/skills/impeccable/scripts/live-inject.mjs +0 -445
  153. package/.agents/skills/impeccable/scripts/live-poll.mjs +0 -186
  154. package/.agents/skills/impeccable/scripts/live-server.mjs +0 -694
  155. package/.agents/skills/impeccable/scripts/live-wrap.mjs +0 -571
  156. package/.agents/skills/impeccable/scripts/live.mjs +0 -247
  157. package/.agents/skills/impeccable/scripts/load-context.mjs +0 -141
  158. package/.agents/skills/impeccable/scripts/modern-screenshot.umd.js +0 -14
  159. package/.agents/skills/impeccable/scripts/pin.mjs +0 -214
  160. package/.agents/skills/init/SKILL.md +0 -140
  161. package/.agents/skills/liteparse/SKILL.md +0 -223
  162. package/.agents/skills/memory-systems/SKILL.md +0 -221
  163. package/.agents/skills/memory-systems/references/implementation.md +0 -551
  164. package/.agents/skills/memory-systems/scripts/memory_store.py +0 -616
  165. package/.agents/skills/multi-agent-patterns/SKILL.md +0 -259
  166. package/.agents/skills/multi-agent-patterns/references/frameworks.md +0 -433
  167. package/.agents/skills/multi-agent-patterns/scripts/coordination.py +0 -613
  168. package/.agents/skills/opentui/SKILL.md +0 -202
  169. package/.agents/skills/opentui/references/animation/REFERENCE.md +0 -431
  170. package/.agents/skills/opentui/references/components/REFERENCE.md +0 -144
  171. package/.agents/skills/opentui/references/components/code-diff.md +0 -672
  172. package/.agents/skills/opentui/references/components/containers.md +0 -417
  173. package/.agents/skills/opentui/references/components/inputs.md +0 -531
  174. package/.agents/skills/opentui/references/components/text-display.md +0 -386
  175. package/.agents/skills/opentui/references/core/REFERENCE.md +0 -145
  176. package/.agents/skills/opentui/references/core/api.md +0 -543
  177. package/.agents/skills/opentui/references/core/configuration.md +0 -168
  178. package/.agents/skills/opentui/references/core/gotchas.md +0 -393
  179. package/.agents/skills/opentui/references/core/patterns.md +0 -449
  180. package/.agents/skills/opentui/references/keyboard/REFERENCE.md +0 -617
  181. package/.agents/skills/opentui/references/layout/REFERENCE.md +0 -337
  182. package/.agents/skills/opentui/references/layout/patterns.md +0 -444
  183. package/.agents/skills/opentui/references/react/REFERENCE.md +0 -174
  184. package/.agents/skills/opentui/references/react/api.md +0 -436
  185. package/.agents/skills/opentui/references/react/configuration.md +0 -302
  186. package/.agents/skills/opentui/references/react/gotchas.md +0 -443
  187. package/.agents/skills/opentui/references/react/patterns.md +0 -501
  188. package/.agents/skills/opentui/references/solid/REFERENCE.md +0 -201
  189. package/.agents/skills/opentui/references/solid/api.md +0 -564
  190. package/.agents/skills/opentui/references/solid/configuration.md +0 -316
  191. package/.agents/skills/opentui/references/solid/gotchas.md +0 -427
  192. package/.agents/skills/opentui/references/solid/patterns.md +0 -560
  193. package/.agents/skills/opentui/references/testing/REFERENCE.md +0 -614
  194. package/.agents/skills/pdf/LICENSE.txt +0 -30
  195. package/.agents/skills/pdf/SKILL.md +0 -316
  196. package/.agents/skills/pdf/forms.md +0 -294
  197. package/.agents/skills/pdf/reference.md +0 -612
  198. package/.agents/skills/pdf/scripts/check_bounding_boxes.py +0 -65
  199. package/.agents/skills/pdf/scripts/check_fillable_fields.py +0 -11
  200. package/.agents/skills/pdf/scripts/convert_pdf_to_images.py +0 -33
  201. package/.agents/skills/pdf/scripts/create_validation_image.py +0 -37
  202. package/.agents/skills/pdf/scripts/extract_form_field_info.py +0 -122
  203. package/.agents/skills/pdf/scripts/extract_form_structure.py +0 -115
  204. package/.agents/skills/pdf/scripts/fill_fillable_fields.py +0 -98
  205. package/.agents/skills/pdf/scripts/fill_pdf_form_with_annotations.py +0 -107
  206. package/.agents/skills/playwright-cli/SKILL.md +0 -390
  207. package/.agents/skills/playwright-cli/references/element-attributes.md +0 -23
  208. package/.agents/skills/playwright-cli/references/playwright-tests.md +0 -39
  209. package/.agents/skills/playwright-cli/references/request-mocking.md +0 -87
  210. package/.agents/skills/playwright-cli/references/running-code.md +0 -241
  211. package/.agents/skills/playwright-cli/references/session-management.md +0 -225
  212. package/.agents/skills/playwright-cli/references/spec-driven-testing.md +0 -305
  213. package/.agents/skills/playwright-cli/references/storage-state.md +0 -275
  214. package/.agents/skills/playwright-cli/references/test-generation.md +0 -134
  215. package/.agents/skills/playwright-cli/references/tracing.md +0 -139
  216. package/.agents/skills/playwright-cli/references/video-recording.md +0 -143
  217. package/.agents/skills/pptx/LICENSE.txt +0 -30
  218. package/.agents/skills/pptx/SKILL.md +0 -234
  219. package/.agents/skills/pptx/editing.md +0 -205
  220. package/.agents/skills/pptx/pptxgenjs.md +0 -420
  221. package/.agents/skills/pptx/scripts/__init__.py +0 -0
  222. package/.agents/skills/pptx/scripts/add_slide.py +0 -195
  223. package/.agents/skills/pptx/scripts/clean.py +0 -286
  224. package/.agents/skills/pptx/scripts/office/helpers/__init__.py +0 -0
  225. package/.agents/skills/pptx/scripts/office/helpers/merge_runs.py +0 -199
  226. package/.agents/skills/pptx/scripts/office/helpers/simplify_redlines.py +0 -197
  227. package/.agents/skills/pptx/scripts/office/pack.py +0 -159
  228. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +0 -1499
  229. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +0 -146
  230. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +0 -1085
  231. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +0 -11
  232. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +0 -3081
  233. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +0 -23
  234. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +0 -185
  235. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +0 -287
  236. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +0 -1676
  237. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +0 -28
  238. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +0 -144
  239. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +0 -174
  240. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +0 -25
  241. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +0 -18
  242. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +0 -59
  243. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +0 -56
  244. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +0 -195
  245. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +0 -582
  246. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +0 -25
  247. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +0 -4439
  248. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +0 -570
  249. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +0 -509
  250. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +0 -12
  251. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +0 -108
  252. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +0 -96
  253. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +0 -3646
  254. package/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +0 -116
  255. package/.agents/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +0 -42
  256. package/.agents/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +0 -50
  257. package/.agents/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +0 -49
  258. package/.agents/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +0 -33
  259. package/.agents/skills/pptx/scripts/office/schemas/mce/mc.xsd +0 -75
  260. package/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-2010.xsd +0 -560
  261. package/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-2012.xsd +0 -67
  262. package/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-2018.xsd +0 -14
  263. package/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +0 -20
  264. package/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +0 -13
  265. package/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +0 -4
  266. package/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +0 -8
  267. package/.agents/skills/pptx/scripts/office/soffice.py +0 -183
  268. package/.agents/skills/pptx/scripts/office/unpack.py +0 -132
  269. package/.agents/skills/pptx/scripts/office/validate.py +0 -111
  270. package/.agents/skills/pptx/scripts/office/validators/__init__.py +0 -15
  271. package/.agents/skills/pptx/scripts/office/validators/base.py +0 -847
  272. package/.agents/skills/pptx/scripts/office/validators/docx.py +0 -446
  273. package/.agents/skills/pptx/scripts/office/validators/pptx.py +0 -275
  274. package/.agents/skills/pptx/scripts/office/validators/redlining.py +0 -247
  275. package/.agents/skills/pptx/scripts/thumbnail.py +0 -289
  276. package/.agents/skills/project-development/SKILL.md +0 -293
  277. package/.agents/skills/project-development/references/case-studies.md +0 -388
  278. package/.agents/skills/project-development/references/pipeline-patterns.md +0 -610
  279. package/.agents/skills/project-development/scripts/pipeline_template.py +0 -796
  280. package/.agents/skills/prompt-engineer/SKILL.md +0 -265
  281. package/.agents/skills/prompt-engineer/references/advanced_patterns.md +0 -271
  282. package/.agents/skills/prompt-engineer/references/core_prompting.md +0 -137
  283. package/.agents/skills/prompt-engineer/references/quality_improvement.md +0 -193
  284. package/.agents/skills/research-codebase/SKILL.md +0 -229
  285. package/.agents/skills/ripgrep/SKILL.md +0 -384
  286. package/.agents/skills/skill-creator/LICENSE.txt +0 -202
  287. package/.agents/skills/skill-creator/SKILL.md +0 -487
  288. package/.agents/skills/skill-creator/agents/analyzer.md +0 -274
  289. package/.agents/skills/skill-creator/agents/comparator.md +0 -202
  290. package/.agents/skills/skill-creator/agents/grader.md +0 -223
  291. package/.agents/skills/skill-creator/assets/eval_review.html +0 -146
  292. package/.agents/skills/skill-creator/eval-viewer/generate_review.py +0 -471
  293. package/.agents/skills/skill-creator/eval-viewer/viewer.html +0 -1325
  294. package/.agents/skills/skill-creator/references/schemas.md +0 -430
  295. package/.agents/skills/skill-creator/scripts/__init__.py +0 -0
  296. package/.agents/skills/skill-creator/scripts/aggregate_benchmark.py +0 -401
  297. package/.agents/skills/skill-creator/scripts/generate_report.py +0 -326
  298. package/.agents/skills/skill-creator/scripts/improve_description.py +0 -247
  299. package/.agents/skills/skill-creator/scripts/package_skill.py +0 -136
  300. package/.agents/skills/skill-creator/scripts/quick_validate.py +0 -103
  301. package/.agents/skills/skill-creator/scripts/run_eval.py +0 -310
  302. package/.agents/skills/skill-creator/scripts/run_loop.py +0 -328
  303. package/.agents/skills/skill-creator/scripts/utils.py +0 -47
  304. package/.agents/skills/sl-commit/SKILL.md +0 -53
  305. package/.agents/skills/sl-submit-diff/SKILL.md +0 -57
  306. package/.agents/skills/tdd/SKILL.md +0 -111
  307. package/.agents/skills/tdd/deep-modules.md +0 -33
  308. package/.agents/skills/tdd/interface-design.md +0 -31
  309. package/.agents/skills/tdd/mocking.md +0 -59
  310. package/.agents/skills/tdd/refactoring.md +0 -10
  311. package/.agents/skills/tdd/tests.md +0 -61
  312. package/.agents/skills/tool-design/SKILL.md +0 -273
  313. package/.agents/skills/tool-design/references/architectural_reduction.md +0 -210
  314. package/.agents/skills/tool-design/references/best_practices.md +0 -176
  315. package/.agents/skills/tool-design/scripts/description_generator.py +0 -528
  316. package/.agents/skills/typescript-advanced-types/SKILL.md +0 -720
  317. package/.agents/skills/typescript-expert/SKILL.md +0 -434
  318. package/.agents/skills/typescript-expert/references/tsconfig-strict.json +0 -92
  319. package/.agents/skills/typescript-expert/references/typescript-cheatsheet.md +0 -383
  320. package/.agents/skills/typescript-expert/references/utility-types.ts +0 -335
  321. package/.agents/skills/typescript-expert/scripts/ts_diagnostic.py +0 -203
  322. package/.agents/skills/typescript-react-reviewer/SKILL.md +0 -201
  323. package/.agents/skills/typescript-react-reviewer/references/antipatterns.md +0 -510
  324. package/.agents/skills/typescript-react-reviewer/references/checklist.md +0 -267
  325. package/.agents/skills/typescript-react-reviewer/references/react19-patterns.md +0 -305
  326. package/.agents/skills/workflow-creator/SKILL.md +0 -553
  327. package/.agents/skills/workflow-creator/references/agent-sessions.md +0 -891
  328. package/.agents/skills/workflow-creator/references/agent-setup-recipe.md +0 -266
  329. package/.agents/skills/workflow-creator/references/computation-and-validation.md +0 -201
  330. package/.agents/skills/workflow-creator/references/control-flow.md +0 -470
  331. package/.agents/skills/workflow-creator/references/failure-modes.md +0 -1014
  332. package/.agents/skills/workflow-creator/references/getting-started.md +0 -392
  333. package/.agents/skills/workflow-creator/references/registry-and-validation.md +0 -141
  334. package/.agents/skills/workflow-creator/references/running-workflows.md +0 -418
  335. package/.agents/skills/workflow-creator/references/session-config.md +0 -431
  336. package/.agents/skills/workflow-creator/references/state-and-data-flow.md +0 -356
  337. package/.agents/skills/workflow-creator/references/user-input.md +0 -234
  338. package/.agents/skills/workflow-creator/references/workflow-inputs.md +0 -392
  339. package/.agents/skills/xlsx/LICENSE.txt +0 -30
  340. package/.agents/skills/xlsx/SKILL.md +0 -294
  341. package/.agents/skills/xlsx/scripts/office/helpers/__init__.py +0 -0
  342. package/.agents/skills/xlsx/scripts/office/helpers/merge_runs.py +0 -199
  343. package/.agents/skills/xlsx/scripts/office/helpers/simplify_redlines.py +0 -197
  344. package/.agents/skills/xlsx/scripts/office/pack.py +0 -159
  345. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +0 -1499
  346. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +0 -146
  347. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +0 -1085
  348. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +0 -11
  349. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +0 -3081
  350. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +0 -23
  351. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +0 -185
  352. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +0 -287
  353. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +0 -1676
  354. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +0 -28
  355. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +0 -144
  356. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +0 -174
  357. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +0 -25
  358. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +0 -18
  359. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +0 -59
  360. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +0 -56
  361. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +0 -195
  362. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +0 -582
  363. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +0 -25
  364. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +0 -4439
  365. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +0 -570
  366. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +0 -509
  367. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +0 -12
  368. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +0 -108
  369. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +0 -96
  370. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +0 -3646
  371. package/.agents/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +0 -116
  372. package/.agents/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +0 -42
  373. package/.agents/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +0 -50
  374. package/.agents/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +0 -49
  375. package/.agents/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +0 -33
  376. package/.agents/skills/xlsx/scripts/office/schemas/mce/mc.xsd +0 -75
  377. package/.agents/skills/xlsx/scripts/office/schemas/microsoft/wml-2010.xsd +0 -560
  378. package/.agents/skills/xlsx/scripts/office/schemas/microsoft/wml-2012.xsd +0 -67
  379. package/.agents/skills/xlsx/scripts/office/schemas/microsoft/wml-2018.xsd +0 -14
  380. package/.agents/skills/xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +0 -20
  381. package/.agents/skills/xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +0 -13
  382. package/.agents/skills/xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +0 -4
  383. package/.agents/skills/xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +0 -8
  384. package/.agents/skills/xlsx/scripts/office/soffice.py +0 -183
  385. package/.agents/skills/xlsx/scripts/office/unpack.py +0 -132
  386. package/.agents/skills/xlsx/scripts/office/validate.py +0 -111
  387. package/.agents/skills/xlsx/scripts/office/validators/__init__.py +0 -15
  388. package/.agents/skills/xlsx/scripts/office/validators/base.py +0 -847
  389. package/.agents/skills/xlsx/scripts/office/validators/docx.py +0 -446
  390. package/.agents/skills/xlsx/scripts/office/validators/pptx.py +0 -275
  391. package/.agents/skills/xlsx/scripts/office/validators/redlining.py +0 -247
  392. package/.agents/skills/xlsx/scripts/recalc.py +0 -184
  393. package/.claude/agents/code-simplifier.md +0 -52
  394. package/.claude/agents/codebase-analyzer.md +0 -166
  395. package/.claude/agents/codebase-locator.md +0 -122
  396. package/.claude/agents/codebase-online-researcher.md +0 -148
  397. package/.claude/agents/codebase-pattern-finder.md +0 -247
  398. package/.claude/agents/codebase-research-analyzer.md +0 -179
  399. package/.claude/agents/codebase-research-locator.md +0 -145
  400. package/.claude/agents/debugger.md +0 -91
  401. package/.claude/agents/orchestrator.md +0 -19
  402. package/.claude/agents/planner.md +0 -295
  403. package/.claude/agents/reviewer.md +0 -98
  404. package/.claude/agents/worker.md +0 -165
  405. package/.claude/settings.json +0 -27
  406. package/.github/agents/code-simplifier.md +0 -52
  407. package/.github/agents/codebase-analyzer.md +0 -166
  408. package/.github/agents/codebase-locator.md +0 -122
  409. package/.github/agents/codebase-online-researcher.md +0 -146
  410. package/.github/agents/codebase-pattern-finder.md +0 -247
  411. package/.github/agents/codebase-research-analyzer.md +0 -179
  412. package/.github/agents/codebase-research-locator.md +0 -145
  413. package/.github/agents/debugger.md +0 -98
  414. package/.github/agents/orchestrator.md +0 -27
  415. package/.github/agents/planner.md +0 -305
  416. package/.github/agents/reviewer.md +0 -95
  417. package/.github/agents/worker.md +0 -237
  418. package/.github/lsp.json +0 -93
  419. package/.mcp.json +0 -20
  420. package/.opencode/agents/code-simplifier.md +0 -62
  421. package/.opencode/agents/codebase-analyzer.md +0 -171
  422. package/.opencode/agents/codebase-locator.md +0 -127
  423. package/.opencode/agents/codebase-online-researcher.md +0 -152
  424. package/.opencode/agents/codebase-pattern-finder.md +0 -252
  425. package/.opencode/agents/codebase-research-analyzer.md +0 -183
  426. package/.opencode/agents/codebase-research-locator.md +0 -149
  427. package/.opencode/agents/debugger.md +0 -99
  428. package/.opencode/agents/orchestrator.md +0 -27
  429. package/.opencode/agents/planner.md +0 -309
  430. package/.opencode/agents/reviewer.md +0 -103
  431. package/.opencode/agents/worker.md +0 -165
  432. package/.opencode/opencode.json +0 -25
  433. package/README.md +0 -1624
  434. package/assets/settings.schema.json +0 -51
  435. package/dist/commands/cli/claude-inflight-hook.d.ts +0 -100
  436. package/dist/commands/cli/claude-inflight-hook.d.ts.map +0 -1
  437. package/dist/commands/cli/claude-stop-hook.d.ts +0 -80
  438. package/dist/commands/cli/claude-stop-hook.d.ts.map +0 -1
  439. package/dist/lib/atomic-temp.d.ts +0 -8
  440. package/dist/lib/atomic-temp.d.ts.map +0 -1
  441. package/dist/lib/path-root-guard.d.ts +0 -4
  442. package/dist/lib/path-root-guard.d.ts.map +0 -1
  443. package/dist/lib/spawn.d.ts +0 -102
  444. package/dist/lib/spawn.d.ts.map +0 -1
  445. package/dist/lib/terminal-env.d.ts +0 -9
  446. package/dist/lib/terminal-env.d.ts.map +0 -1
  447. package/dist/sdk/components/attached-statusline.d.ts +0 -26
  448. package/dist/sdk/components/attached-statusline.d.ts.map +0 -1
  449. package/dist/sdk/components/color-utils.d.ts +0 -4
  450. package/dist/sdk/components/color-utils.d.ts.map +0 -1
  451. package/dist/sdk/components/compact-switcher.d.ts +0 -10
  452. package/dist/sdk/components/compact-switcher.d.ts.map +0 -1
  453. package/dist/sdk/components/connectors.d.ts +0 -16
  454. package/dist/sdk/components/connectors.d.ts.map +0 -1
  455. package/dist/sdk/components/edge.d.ts +0 -4
  456. package/dist/sdk/components/edge.d.ts.map +0 -1
  457. package/dist/sdk/components/error-boundary.d.ts +0 -23
  458. package/dist/sdk/components/error-boundary.d.ts.map +0 -1
  459. package/dist/sdk/components/graph-theme.d.ts +0 -18
  460. package/dist/sdk/components/graph-theme.d.ts.map +0 -1
  461. package/dist/sdk/components/header.d.ts +0 -3
  462. package/dist/sdk/components/header.d.ts.map +0 -1
  463. package/dist/sdk/components/hooks.d.ts +0 -15
  464. package/dist/sdk/components/hooks.d.ts.map +0 -1
  465. package/dist/sdk/components/layout.d.ts +0 -27
  466. package/dist/sdk/components/layout.d.ts.map +0 -1
  467. package/dist/sdk/components/node-card.d.ts +0 -10
  468. package/dist/sdk/components/node-card.d.ts.map +0 -1
  469. package/dist/sdk/components/orchestrator-panel-contexts.d.ts +0 -16
  470. package/dist/sdk/components/orchestrator-panel-contexts.d.ts.map +0 -1
  471. package/dist/sdk/components/orchestrator-panel-store.d.ts +0 -52
  472. package/dist/sdk/components/orchestrator-panel-store.d.ts.map +0 -1
  473. package/dist/sdk/components/orchestrator-panel-types.d.ts +0 -18
  474. package/dist/sdk/components/orchestrator-panel-types.d.ts.map +0 -1
  475. package/dist/sdk/components/orchestrator-panel.d.ts +0 -86
  476. package/dist/sdk/components/orchestrator-panel.d.ts.map +0 -1
  477. package/dist/sdk/components/renderer-background.d.ts +0 -9
  478. package/dist/sdk/components/renderer-background.d.ts.map +0 -1
  479. package/dist/sdk/components/session-graph-panel.d.ts +0 -7
  480. package/dist/sdk/components/session-graph-panel.d.ts.map +0 -1
  481. package/dist/sdk/components/status-helpers.d.ts +0 -6
  482. package/dist/sdk/components/status-helpers.d.ts.map +0 -1
  483. package/dist/sdk/components/statusline.d.ts +0 -5
  484. package/dist/sdk/components/statusline.d.ts.map +0 -1
  485. package/dist/sdk/components/tui-diagnostics.d.ts +0 -56
  486. package/dist/sdk/components/tui-diagnostics.d.ts.map +0 -1
  487. package/dist/sdk/components/workflow-picker-panel.d.ts +0 -126
  488. package/dist/sdk/components/workflow-picker-panel.d.ts.map +0 -1
  489. package/dist/sdk/define-workflow.d.ts +0 -107
  490. package/dist/sdk/define-workflow.d.ts.map +0 -1
  491. package/dist/sdk/errors.d.ts +0 -46
  492. package/dist/sdk/errors.d.ts.map +0 -1
  493. package/dist/sdk/index.d.ts +0 -26
  494. package/dist/sdk/index.d.ts.map +0 -1
  495. package/dist/sdk/primitives/inputs.d.ts +0 -36
  496. package/dist/sdk/primitives/inputs.d.ts.map +0 -1
  497. package/dist/sdk/primitives/metadata.d.ts +0 -40
  498. package/dist/sdk/primitives/metadata.d.ts.map +0 -1
  499. package/dist/sdk/primitives/run.d.ts +0 -57
  500. package/dist/sdk/primitives/run.d.ts.map +0 -1
  501. package/dist/sdk/primitives/sessions.d.ts +0 -128
  502. package/dist/sdk/primitives/sessions.d.ts.map +0 -1
  503. package/dist/sdk/providers/claude.d.ts +0 -392
  504. package/dist/sdk/providers/claude.d.ts.map +0 -1
  505. package/dist/sdk/providers/copilot.d.ts +0 -55
  506. package/dist/sdk/providers/copilot.d.ts.map +0 -1
  507. package/dist/sdk/providers/opencode.d.ts +0 -27
  508. package/dist/sdk/providers/opencode.d.ts.map +0 -1
  509. package/dist/sdk/registry.d.ts +0 -27
  510. package/dist/sdk/registry.d.ts.map +0 -1
  511. package/dist/sdk/runtime/attached-footer.d.ts +0 -31
  512. package/dist/sdk/runtime/attached-footer.d.ts.map +0 -1
  513. package/dist/sdk/runtime/cc-debounce.d.ts +0 -29
  514. package/dist/sdk/runtime/cc-debounce.d.ts.map +0 -1
  515. package/dist/sdk/runtime/executor-env.d.ts +0 -20
  516. package/dist/sdk/runtime/executor-env.d.ts.map +0 -1
  517. package/dist/sdk/runtime/executor.d.ts +0 -265
  518. package/dist/sdk/runtime/executor.d.ts.map +0 -1
  519. package/dist/sdk/runtime/graph-inference.d.ts +0 -35
  520. package/dist/sdk/runtime/graph-inference.d.ts.map +0 -1
  521. package/dist/sdk/runtime/orchestrator-entry.d.ts +0 -26
  522. package/dist/sdk/runtime/orchestrator-entry.d.ts.map +0 -1
  523. package/dist/sdk/runtime/panel.d.ts +0 -9
  524. package/dist/sdk/runtime/panel.d.ts.map +0 -1
  525. package/dist/sdk/runtime/port-discovery.d.ts +0 -71
  526. package/dist/sdk/runtime/port-discovery.d.ts.map +0 -1
  527. package/dist/sdk/runtime/status-writer.d.ts +0 -101
  528. package/dist/sdk/runtime/status-writer.d.ts.map +0 -1
  529. package/dist/sdk/runtime/theme.d.ts +0 -33
  530. package/dist/sdk/runtime/theme.d.ts.map +0 -1
  531. package/dist/sdk/runtime/tmux.d.ts +0 -307
  532. package/dist/sdk/runtime/tmux.d.ts.map +0 -1
  533. package/dist/sdk/runtime/version-compat.d.ts +0 -28
  534. package/dist/sdk/runtime/version-compat.d.ts.map +0 -1
  535. package/dist/sdk/types.d.ts +0 -435
  536. package/dist/sdk/types.d.ts.map +0 -1
  537. package/dist/sdk/worker-shared.d.ts +0 -42
  538. package/dist/sdk/worker-shared.d.ts.map +0 -1
  539. package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts +0 -81
  540. package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts.map +0 -1
  541. package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts +0 -37
  542. package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts.map +0 -1
  543. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/batching.d.ts +0 -43
  544. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/batching.d.ts.map +0 -1
  545. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.d.ts +0 -14
  546. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.d.ts.map +0 -1
  547. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts +0 -136
  548. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts.map +0 -1
  549. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scout.d.ts +0 -58
  550. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scout.d.ts.map +0 -1
  551. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scratch.d.ts +0 -43
  552. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scratch.d.ts.map +0 -1
  553. package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts +0 -37
  554. package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +0 -1
  555. package/dist/sdk/workflows/builtin/open-claude-design/claude/index.d.ts +0 -68
  556. package/dist/sdk/workflows/builtin/open-claude-design/claude/index.d.ts.map +0 -1
  557. package/dist/sdk/workflows/builtin/open-claude-design/copilot/index.d.ts +0 -56
  558. package/dist/sdk/workflows/builtin/open-claude-design/copilot/index.d.ts.map +0 -1
  559. package/dist/sdk/workflows/builtin/open-claude-design/helpers/constants.d.ts +0 -72
  560. package/dist/sdk/workflows/builtin/open-claude-design/helpers/constants.d.ts.map +0 -1
  561. package/dist/sdk/workflows/builtin/open-claude-design/helpers/design-system.d.ts +0 -46
  562. package/dist/sdk/workflows/builtin/open-claude-design/helpers/design-system.d.ts.map +0 -1
  563. package/dist/sdk/workflows/builtin/open-claude-design/helpers/export.d.ts +0 -32
  564. package/dist/sdk/workflows/builtin/open-claude-design/helpers/export.d.ts.map +0 -1
  565. package/dist/sdk/workflows/builtin/open-claude-design/helpers/import.d.ts +0 -33
  566. package/dist/sdk/workflows/builtin/open-claude-design/helpers/import.d.ts.map +0 -1
  567. package/dist/sdk/workflows/builtin/open-claude-design/helpers/prompts.d.ts +0 -106
  568. package/dist/sdk/workflows/builtin/open-claude-design/helpers/prompts.d.ts.map +0 -1
  569. package/dist/sdk/workflows/builtin/open-claude-design/helpers/scan.d.ts +0 -50
  570. package/dist/sdk/workflows/builtin/open-claude-design/helpers/scan.d.ts.map +0 -1
  571. package/dist/sdk/workflows/builtin/open-claude-design/helpers/validation.d.ts +0 -12
  572. package/dist/sdk/workflows/builtin/open-claude-design/helpers/validation.d.ts.map +0 -1
  573. package/dist/sdk/workflows/builtin/open-claude-design/opencode/index.d.ts +0 -58
  574. package/dist/sdk/workflows/builtin/open-claude-design/opencode/index.d.ts.map +0 -1
  575. package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts +0 -37
  576. package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts.map +0 -1
  577. package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts +0 -34
  578. package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts.map +0 -1
  579. package/dist/sdk/workflows/builtin/ralph/helpers/copilot-reviewer.d.ts +0 -25
  580. package/dist/sdk/workflows/builtin/ralph/helpers/copilot-reviewer.d.ts.map +0 -1
  581. package/dist/sdk/workflows/builtin/ralph/helpers/git.d.ts +0 -69
  582. package/dist/sdk/workflows/builtin/ralph/helpers/git.d.ts.map +0 -1
  583. package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts +0 -266
  584. package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts.map +0 -1
  585. package/dist/sdk/workflows/builtin/ralph/helpers/review.d.ts +0 -24
  586. package/dist/sdk/workflows/builtin/ralph/helpers/review.d.ts.map +0 -1
  587. package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts +0 -33
  588. package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts.map +0 -1
  589. package/dist/sdk/workflows/index.d.ts +0 -32
  590. package/dist/sdk/workflows/index.d.ts.map +0 -1
  591. package/dist/services/config/additional-instructions.d.ts +0 -67
  592. package/dist/services/config/additional-instructions.d.ts.map +0 -1
  593. package/dist/services/config/atomic-config.d.ts +0 -42
  594. package/dist/services/config/atomic-config.d.ts.map +0 -1
  595. package/dist/services/config/definitions.d.ts +0 -52
  596. package/dist/services/config/definitions.d.ts.map +0 -1
  597. package/dist/services/config/index.d.ts +0 -7
  598. package/dist/services/config/index.d.ts.map +0 -1
  599. package/dist/services/config/scm-sync.d.ts +0 -37
  600. package/dist/services/config/scm-sync.d.ts.map +0 -1
  601. package/dist/services/config/settings-schema.d.ts +0 -2
  602. package/dist/services/config/settings-schema.d.ts.map +0 -1
  603. package/dist/services/system/copy.d.ts +0 -84
  604. package/dist/services/system/copy.d.ts.map +0 -1
  605. package/dist/services/system/detect.d.ts +0 -75
  606. package/dist/services/system/detect.d.ts.map +0 -1
  607. package/dist/theme/colors.d.ts +0 -35
  608. package/dist/theme/colors.d.ts.map +0 -1
  609. package/src/cli.ts +0 -397
  610. package/src/commands/builtin-registry.ts +0 -37
  611. package/src/commands/cli/chat/index.test.ts +0 -252
  612. package/src/commands/cli/chat/index.ts +0 -430
  613. package/src/commands/cli/chat.ts +0 -8
  614. package/src/commands/cli/claude-ask-hook.test.ts +0 -128
  615. package/src/commands/cli/claude-ask-hook.ts +0 -84
  616. package/src/commands/cli/claude-inflight-hook.test.ts +0 -598
  617. package/src/commands/cli/claude-inflight-hook.ts +0 -359
  618. package/src/commands/cli/claude-session-start-hook.ts +0 -61
  619. package/src/commands/cli/claude-stop-hook.test.ts +0 -317
  620. package/src/commands/cli/claude-stop-hook.ts +0 -441
  621. package/src/commands/cli/completions.ts +0 -24
  622. package/src/commands/cli/config.ts +0 -80
  623. package/src/commands/cli/footer.tsx +0 -248
  624. package/src/commands/cli/init/index.ts +0 -41
  625. package/src/commands/cli/init/onboarding.ts +0 -61
  626. package/src/commands/cli/init.ts +0 -8
  627. package/src/commands/cli/management-commands.ts +0 -112
  628. package/src/commands/cli/session.test.ts +0 -830
  629. package/src/commands/cli/session.ts +0 -447
  630. package/src/commands/cli/workflow-command.test.ts +0 -618
  631. package/src/commands/cli/workflow-inputs.test.ts +0 -353
  632. package/src/commands/cli/workflow-inputs.ts +0 -266
  633. package/src/commands/cli/workflow-list.test.ts +0 -235
  634. package/src/commands/cli/workflow-list.ts +0 -0
  635. package/src/commands/cli/workflow-status.test.ts +0 -451
  636. package/src/commands/cli/workflow-status.ts +0 -330
  637. package/src/commands/cli/workflow.ts +0 -196
  638. package/src/completions/bash.ts +0 -102
  639. package/src/completions/fish.ts +0 -136
  640. package/src/completions/index.ts +0 -7
  641. package/src/completions/powershell.ts +0 -195
  642. package/src/completions/zsh.ts +0 -150
  643. package/src/lib/atomic-temp.test.ts +0 -86
  644. package/src/lib/atomic-temp.ts +0 -62
  645. package/src/lib/common-ignore.ts +0 -46
  646. package/src/lib/merge.ts +0 -103
  647. package/src/lib/path-root-guard.ts +0 -38
  648. package/src/lib/spawn.test.ts +0 -109
  649. package/src/lib/spawn.ts +0 -678
  650. package/src/lib/terminal-env.test.ts +0 -343
  651. package/src/lib/terminal-env.ts +0 -100
  652. package/src/scripts/bump-version.ts +0 -94
  653. package/src/scripts/bundle-configs.ts +0 -116
  654. package/src/scripts/clean-dist.test.ts +0 -53
  655. package/src/scripts/clean-dist.ts +0 -37
  656. package/src/scripts/constants-base.ts +0 -14
  657. package/src/scripts/constants.ts +0 -35
  658. package/src/sdk/components/attached-statusline.tsx +0 -86
  659. package/src/sdk/components/color-utils.ts +0 -20
  660. package/src/sdk/components/compact-switcher.tsx +0 -78
  661. package/src/sdk/components/connectors.test.ts +0 -707
  662. package/src/sdk/components/connectors.ts +0 -160
  663. package/src/sdk/components/edge.tsx +0 -13
  664. package/src/sdk/components/error-boundary.tsx +0 -38
  665. package/src/sdk/components/graph-theme.ts +0 -37
  666. package/src/sdk/components/header.tsx +0 -85
  667. package/src/sdk/components/hooks.ts +0 -21
  668. package/src/sdk/components/layout.test.ts +0 -1245
  669. package/src/sdk/components/layout.ts +0 -223
  670. package/src/sdk/components/node-card.tsx +0 -91
  671. package/src/sdk/components/orchestrator-panel-contexts.ts +0 -35
  672. package/src/sdk/components/orchestrator-panel-store.test.ts +0 -847
  673. package/src/sdk/components/orchestrator-panel-store.ts +0 -187
  674. package/src/sdk/components/orchestrator-panel-types.ts +0 -23
  675. package/src/sdk/components/orchestrator-panel.tsx +0 -262
  676. package/src/sdk/components/renderer-background.ts +0 -49
  677. package/src/sdk/components/session-graph-panel.tsx +0 -471
  678. package/src/sdk/components/status-helpers.ts +0 -33
  679. package/src/sdk/components/statusline.tsx +0 -68
  680. package/src/sdk/components/tui-diagnostics.ts +0 -273
  681. package/src/sdk/components/workflow-picker-panel.tsx +0 -1613
  682. package/src/sdk/define-workflow.test.ts +0 -354
  683. package/src/sdk/define-workflow.ts +0 -275
  684. package/src/sdk/errors.test.ts +0 -83
  685. package/src/sdk/errors.ts +0 -77
  686. package/src/sdk/index.test.ts +0 -92
  687. package/src/sdk/index.ts +0 -101
  688. package/src/sdk/primitives/inputs.ts +0 -48
  689. package/src/sdk/primitives/metadata.ts +0 -63
  690. package/src/sdk/primitives/run.ts +0 -81
  691. package/src/sdk/primitives/sessions.test.ts +0 -594
  692. package/src/sdk/primitives/sessions.ts +0 -328
  693. package/src/sdk/providers/claude.ts +0 -1450
  694. package/src/sdk/providers/copilot.test.ts +0 -365
  695. package/src/sdk/providers/copilot.ts +0 -185
  696. package/src/sdk/providers/headless-hil-policy.test.ts +0 -211
  697. package/src/sdk/providers/opencode.ts +0 -88
  698. package/src/sdk/registry.ts +0 -132
  699. package/src/sdk/runtime/attached-footer.ts +0 -155
  700. package/src/sdk/runtime/cc-debounce.ts +0 -104
  701. package/src/sdk/runtime/executor-env.ts +0 -45
  702. package/src/sdk/runtime/executor.test.ts +0 -1321
  703. package/src/sdk/runtime/executor.ts +0 -2136
  704. package/src/sdk/runtime/graph-inference.ts +0 -50
  705. package/src/sdk/runtime/orchestrator-entry.ts +0 -110
  706. package/src/sdk/runtime/panel.tsx +0 -9
  707. package/src/sdk/runtime/port-discovery.test.ts +0 -573
  708. package/src/sdk/runtime/port-discovery.ts +0 -496
  709. package/src/sdk/runtime/status-writer.test.ts +0 -245
  710. package/src/sdk/runtime/status-writer.ts +0 -201
  711. package/src/sdk/runtime/theme.ts +0 -71
  712. package/src/sdk/runtime/tmux.conf +0 -112
  713. package/src/sdk/runtime/tmux.ts +0 -785
  714. package/src/sdk/runtime/version-compat.ts +0 -68
  715. package/src/sdk/types.ts +0 -548
  716. package/src/sdk/worker-shared.test.ts +0 -163
  717. package/src/sdk/worker-shared.ts +0 -155
  718. package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +0 -569
  719. package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +0 -481
  720. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/batching.ts +0 -65
  721. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.ts +0 -24
  722. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/ignore-by-default.d.ts +0 -8
  723. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.ts +0 -958
  724. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/scout.ts +0 -505
  725. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/scratch.ts +0 -115
  726. package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +0 -530
  727. package/src/sdk/workflows/builtin/open-claude-design/claude/index.ts +0 -500
  728. package/src/sdk/workflows/builtin/open-claude-design/copilot/index.ts +0 -508
  729. package/src/sdk/workflows/builtin/open-claude-design/helpers/constants.ts +0 -159
  730. package/src/sdk/workflows/builtin/open-claude-design/helpers/design-system.ts +0 -88
  731. package/src/sdk/workflows/builtin/open-claude-design/helpers/export.ts +0 -193
  732. package/src/sdk/workflows/builtin/open-claude-design/helpers/import.ts +0 -52
  733. package/src/sdk/workflows/builtin/open-claude-design/helpers/prompts.ts +0 -1110
  734. package/src/sdk/workflows/builtin/open-claude-design/helpers/scan.ts +0 -117
  735. package/src/sdk/workflows/builtin/open-claude-design/helpers/validation.ts +0 -38
  736. package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +0 -610
  737. package/src/sdk/workflows/builtin/ralph/claude/index.ts +0 -272
  738. package/src/sdk/workflows/builtin/ralph/copilot/index.ts +0 -298
  739. package/src/sdk/workflows/builtin/ralph/helpers/copilot-reviewer.ts +0 -105
  740. package/src/sdk/workflows/builtin/ralph/helpers/git.ts +0 -201
  741. package/src/sdk/workflows/builtin/ralph/helpers/prompts.ts +0 -1108
  742. package/src/sdk/workflows/builtin/ralph/helpers/review.ts +0 -33
  743. package/src/sdk/workflows/builtin/ralph/opencode/index.ts +0 -290
  744. package/src/sdk/workflows/index.ts +0 -116
  745. package/src/services/config/additional-instructions.ts +0 -273
  746. package/src/services/config/atomic-config.ts +0 -210
  747. package/src/services/config/atomic-global-config.ts +0 -348
  748. package/src/services/config/config-path.ts +0 -19
  749. package/src/services/config/definitions.ts +0 -125
  750. package/src/services/config/index.ts +0 -7
  751. package/src/services/config/scm-sync.ts +0 -185
  752. package/src/services/config/settings-schema.ts +0 -2
  753. package/src/services/config/settings.ts +0 -144
  754. package/src/services/system/agents.ts +0 -95
  755. package/src/services/system/auth.test.ts +0 -343
  756. package/src/services/system/auth.ts +0 -140
  757. package/src/services/system/auto-sync.ts +0 -128
  758. package/src/services/system/copy.ts +0 -392
  759. package/src/services/system/detect.ts +0 -161
  760. package/src/services/system/file-lock.ts +0 -289
  761. package/src/services/system/install-ui.ts +0 -296
  762. package/src/services/system/skills.ts +0 -58
  763. package/src/theme/colors.ts +0 -96
  764. package/src/theme/logo.ts +0 -123
  765. package/src/version.ts +0 -7
@@ -1,4781 +0,0 @@
1
- /**
2
- * Impeccable Live Variant Mode — Browser Script
3
- *
4
- * Injected into the user's page via <script src="http://localhost:PORT/live.js">.
5
- * The server prepends window.__IMPECCABLE_TOKEN__ and window.__IMPECCABLE_PORT__
6
- * before this code.
7
- *
8
- * UI: a single floating bar that morphs between three states —
9
- * configure (pick action + go), generating (progressive dots), and cycling
10
- * (prev/next + accept/discard). Feels like Spotlight, not a modal.
11
- */
12
- (function () {
13
- 'use strict';
14
- if (typeof window === 'undefined') return;
15
-
16
- // Guard against double-init. Bun's HTML loader may process the <script> tag
17
- // and create a bundled copy alongside the external load, or HMR may re-execute.
18
- // Check BEFORE reading token/port to catch all cases.
19
- if (window.__IMPECCABLE_LIVE_INIT__) return;
20
- window.__IMPECCABLE_LIVE_INIT__ = true;
21
-
22
- const TOKEN = window.__IMPECCABLE_TOKEN__;
23
- const PORT = window.__IMPECCABLE_PORT__;
24
- if (!TOKEN || !PORT) {
25
- window.__IMPECCABLE_LIVE_INIT__ = false; // reset so the real load can init
26
- return;
27
- }
28
-
29
- // ---------------------------------------------------------------------------
30
- // Design tokens
31
- // ---------------------------------------------------------------------------
32
-
33
- // Brand magenta is pinned to the site token (--color-accent in main.css)
34
- // so Accept / knobs / cycle-dots match the site's accent, not a washed
35
- // theme-adjusted one.
36
- const C = {
37
- brand: 'oklch(60% 0.25 350)',
38
- brandHov: 'oklch(52% 0.25 350)',
39
- brandSoft: 'oklch(60% 0.25 350 / 0.15)',
40
- ink: 'oklch(15% 0.01 350)',
41
- ash: 'oklch(55% 0 0)',
42
- paper: 'oklch(98% 0.005 350 / 0.92)',
43
- paperSolid:'oklch(98% 0.005 350)',
44
- mist: 'oklch(90% 0.01 350 / 0.6)',
45
- white: 'oklch(99% 0 0)',
46
- };
47
- const FONT = 'system-ui, -apple-system, sans-serif';
48
- const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
49
- // z-index: detect overlays use 99999, so our UI must be above them
50
- const Z = { highlight: 100001, bar: 100005, picker: 100007, toast: 100010 };
51
- const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; // ease-out-quint
52
- const PREFIX = 'impeccable-live';
53
- const HIGHLIGHT_TRANSITION =
54
- 'top 140ms ' + EASE +
55
- ', left 140ms ' + EASE +
56
- ', width 140ms ' + EASE +
57
- ', height 140ms ' + EASE +
58
- ', opacity 150ms ease';
59
- const TOOLTIP_TRANSITION =
60
- 'top 140ms ' + EASE + ', left 140ms ' + EASE + ', opacity 150ms ease';
61
-
62
- const SKIP_TAGS = new Set([
63
- 'html', 'head', 'body', 'script', 'style', 'link', 'meta', 'noscript', 'br', 'wbr',
64
- ]);
65
-
66
- // SVG icons stack above each chip label. All strokes use currentColor so the
67
- // icon recolors to C.brand when its chip is selected. 20x20 render, 24-viewBox,
68
- // 1.5 stroke — visually consistent with the Foundation grid on the homepage.
69
- const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
70
- const ICONS = {
71
- impeccable: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>`,
72
- bolder: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>`,
73
- quieter: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>`,
74
- distill: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>`,
75
- polish: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>`,
76
- typeset: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>`,
77
- colorize: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>`,
78
- layout: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>`,
79
- adapt: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>`,
80
- animate: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>`,
81
- delight: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>`,
82
- overdrive: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>`,
83
- };
84
-
85
- const ACTIONS = [
86
- { value: 'impeccable', label: 'Freeform' },
87
- { value: 'bolder', label: 'Bolder' },
88
- { value: 'quieter', label: 'Quieter' },
89
- { value: 'distill', label: 'Distill' },
90
- { value: 'polish', label: 'Polish' },
91
- { value: 'typeset', label: 'Typeset' },
92
- { value: 'colorize', label: 'Colorize' },
93
- { value: 'layout', label: 'Layout' },
94
- { value: 'adapt', label: 'Adapt' },
95
- { value: 'animate', label: 'Animate' },
96
- { value: 'delight', label: 'Delight' },
97
- { value: 'overdrive', label: 'Overdrive' },
98
- ];
99
-
100
- // ---------------------------------------------------------------------------
101
- // State
102
- // ---------------------------------------------------------------------------
103
-
104
- let state = 'IDLE';
105
- let hoveredElement = null;
106
- let selectedElement = null;
107
- let currentSessionId = null;
108
- let expectedVariants = 0;
109
- let arrivedVariants = 0;
110
- let visibleVariant = 0;
111
- let variantObserver = null;
112
- let hasProjectContext = false;
113
- let selectedAction = 'impeccable';
114
- let selectedCount = 3;
115
-
116
- // Scroll lock — holds window.scrollY at a fixed value while the session is
117
- // active, so HMR DOM patches and variant swaps can't drift the page. See
118
- // startScrollLock / stopScrollLock below.
119
- let scrollLockObserver = null;
120
- let scrollLockTargetY = null;
121
- let scrollLockRaf = null;
122
- let scrollLockAbort = null;
123
-
124
- // Dedicated key for scroll position — SEPARATE from LS_KEY so that
125
- // saveSession's state updates don't clobber a carefully-captured scrollY.
126
- // (Previously: saveSession wrote scrollY alongside state, so every call
127
- // during resume overwrote the pre-reload value with whatever the browser
128
- // had landed on, typically 0.)
129
- const SCROLL_KEY_SUFFIX = '-scroll';
130
- function writeScrollY(y) {
131
- try { localStorage.setItem(LS_KEY + SCROLL_KEY_SUFFIX, String(y)); } catch {}
132
- }
133
- function readScrollY() {
134
- try {
135
- const raw = localStorage.getItem(LS_KEY + SCROLL_KEY_SUFFIX);
136
- if (raw == null) return null;
137
- const n = parseFloat(raw);
138
- return isFinite(n) ? n : null;
139
- } catch { return null; }
140
- }
141
- function clearScrollY() {
142
- try { localStorage.removeItem(LS_KEY + SCROLL_KEY_SUFFIX); } catch {}
143
- }
144
-
145
- // Pre-empt the browser: apply manual scroll restoration and jump to the
146
- // saved scrollY at script-parse time. Retries on fonts.ready and load
147
- // are essential: scrollTo(y) clamps to the current document.scrollHeight,
148
- // which is often hundreds of pixels short of the final value until
149
- // async-loaded fonts swap in and reflow.
150
- try {
151
- history.scrollRestoration = 'manual';
152
- const savedY = readScrollY();
153
- if (savedY != null) {
154
- const apply = () => {
155
- if (Math.abs(window.scrollY - savedY) > 0.5) {
156
- console.log('[impeccable.scroll] early restore', { from: window.scrollY, to: savedY });
157
- window.scrollTo(0, savedY);
158
- }
159
- };
160
- apply();
161
- if (document.fonts?.ready) document.fonts.ready.then(apply).catch(() => {});
162
- window.addEventListener('load', apply, { once: true });
163
- }
164
- } catch {}
165
-
166
- // UI refs
167
- let highlightEl = null;
168
- let tooltipEl = null;
169
- let barEl = null;
170
- let pickerEl = null;
171
- let toastEl = null;
172
- let scrollRaf = null;
173
-
174
- // ---------------------------------------------------------------------------
175
- // Helpers
176
- // ---------------------------------------------------------------------------
177
-
178
- function own(el) {
179
- return el && (el.id?.startsWith(PREFIX) || el.closest?.('[id^="' + PREFIX + '"]'));
180
- }
181
-
182
- function pickable(el) {
183
- if (!el || el.nodeType !== 1) return false;
184
- if (SKIP_TAGS.has(el.tagName.toLowerCase())) return false;
185
- if (own(el)) return false;
186
- const r = el.getBoundingClientRect();
187
- return r.width >= 20 && r.height >= 20;
188
- }
189
-
190
- function desc(el) {
191
- if (!el) return '';
192
- let s = el.tagName.toLowerCase();
193
- if (el.id) s += '#' + el.id;
194
- else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.');
195
- return s;
196
- }
197
-
198
- function id8() { return crypto.randomUUID().replace(/-/g, '').slice(0, 8); }
199
-
200
- // Modal-aware chrome: keep our floating UI clickable inside Radix /
201
- // Headless UI / vaul portals.
202
- //
203
- // Two host-page behaviors break us when the picked element lives inside a
204
- // modal dialog:
205
- //
206
- // 1. Modal scroll-lock disables outside pointer events. Radix's
207
- // `DismissableLayer` sets `document.body.style.pointerEvents = 'none'`
208
- // while a modal is open and only restores `auto` on the layer. Our
209
- // chrome inherits `none` from <body> and becomes unclickable.
210
- // 2. The dialog's outside-interaction handler (Radix's
211
- // `usePointerDownOutside`) listens at document level and dismisses
212
- // the dialog whenever a `pointerdown` lands outside the layer node.
213
- // Our chrome is a sibling of <body>, so Radix classifies our clicks
214
- // as outside and tears the dialog down mid-task.
215
- //
216
- // We can't reliably re-parent our chrome into the dialog subtree (z-index
217
- // stacking, scroll containers, theming all become host-page concerns), so
218
- // we defang both behaviors at our root:
219
- //
220
- // - `pointer-events: auto !important` overrides the inherited `none`.
221
- // - Stop `pointerdown` / `mousedown` propagation so the document-level
222
- // dismiss listener never fires for our clicks.
223
- // - Stop `focusin` propagation so any focus shifts inside our chrome
224
- // don't read as "focus moved outside the dialog" to focus traps.
225
- //
226
- // Click events still bubble normally — only the early pointer/focus
227
- // signals that drive outside-interaction detection are silenced.
228
- function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) {
229
- if (!rootEl) return;
230
- if (setPointerEvents) {
231
- rootEl.style.setProperty('pointer-events', 'auto', 'important');
232
- }
233
- const stop = (e) => e.stopPropagation();
234
- rootEl.addEventListener('pointerdown', stop);
235
- rootEl.addEventListener('mousedown', stop);
236
- rootEl.addEventListener('focusin', stop);
237
- }
238
-
239
- // ---------------------------------------------------------------------------
240
- // Highlight overlay
241
- // ---------------------------------------------------------------------------
242
-
243
- function initHighlight() {
244
- highlightEl = document.createElement('div');
245
- highlightEl.id = PREFIX + '-highlight';
246
- Object.assign(highlightEl.style, {
247
- position: 'fixed', top: '0', left: '0', width: '0', height: '0',
248
- border: '2px solid ' + C.brand, borderRadius: '3px',
249
- pointerEvents: 'none', zIndex: Z.highlight, boxSizing: 'border-box',
250
- transition: HIGHLIGHT_TRANSITION,
251
- display: 'none', opacity: '0',
252
- });
253
- document.body.appendChild(highlightEl);
254
-
255
- tooltipEl = document.createElement('div');
256
- tooltipEl.id = PREFIX + '-tooltip';
257
- Object.assign(tooltipEl.style, {
258
- position: 'fixed',
259
- background: C.ink, color: C.white,
260
- fontFamily: MONO, fontSize: '10px', fontWeight: '500',
261
- padding: '2px 6px', borderRadius: '3px',
262
- zIndex: Z.highlight + 1, pointerEvents: 'none',
263
- whiteSpace: 'nowrap', display: 'none',
264
- letterSpacing: '0.02em',
265
- transition: TOOLTIP_TRANSITION,
266
- });
267
- document.body.appendChild(tooltipEl);
268
- }
269
-
270
- function showHighlight(el) {
271
- if (!el || !highlightEl) return;
272
- const r = el.getBoundingClientRect();
273
- const top = (r.top - 2) + 'px', left = (r.left - 2) + 'px';
274
- const width = (r.width + 4) + 'px', height = (r.height + 4) + 'px';
275
- const tipTop = r.top - 20;
276
- const tipY = (tipTop < 4 ? r.bottom + 4 : tipTop) + 'px';
277
- const tipX = Math.max(4, r.left) + 'px';
278
- tooltipEl.textContent = desc(el);
279
-
280
- const hiWasHidden = highlightEl.style.display === 'none' || highlightEl.style.opacity === '0';
281
- if (hiWasHidden) {
282
- // Snap to first target without animating from (0,0), then fade in.
283
- highlightEl.style.transition = 'none';
284
- Object.assign(highlightEl.style, { top, left, width, height, display: 'block' });
285
- tooltipEl.style.transition = 'none';
286
- Object.assign(tooltipEl.style, { top: tipY, left: tipX, display: 'block' });
287
- void highlightEl.offsetWidth;
288
- highlightEl.style.transition = HIGHLIGHT_TRANSITION;
289
- highlightEl.style.opacity = '1';
290
- tooltipEl.style.transition = TOOLTIP_TRANSITION;
291
- tooltipEl.style.opacity = '1';
292
- } else {
293
- Object.assign(highlightEl.style, { top, left, width, height, display: 'block', opacity: '1' });
294
- Object.assign(tooltipEl.style, { top: tipY, left: tipX, display: 'block', opacity: '1' });
295
- }
296
- }
297
-
298
- function hideHighlight() {
299
- if (highlightEl) { highlightEl.style.opacity = '0'; highlightEl.style.display = 'none'; }
300
- if (tooltipEl) { tooltipEl.style.opacity = '0'; tooltipEl.style.display = 'none'; }
301
- }
302
-
303
- // ---------------------------------------------------------------------------
304
- // Annotation overlay (comment pins + magenta strokes)
305
- //
306
- // Active while state === 'CONFIGURING'. The overlay is a fixed-positioned
307
- // sibling of <body> mirroring selectedElement's bounding rect. Click (no
308
- // drag) drops a comment pin; drag paints a magenta SVG stroke. All coords
309
- // are stored in element-local CSS px so they survive scroll / resize and
310
- // correlate directly with the captured PNG.
311
- // ---------------------------------------------------------------------------
312
-
313
- const DRAG_THRESHOLD = 5; // px — below this, treat pointerup as a click
314
- const PIN_DBL_CLICK_MS = 300; // two clicks on the same pin within this delete it
315
- let annotOverlayEl = null;
316
- let annotSvgEl = null;
317
- let annotPinsEl = null;
318
- let annotClearChipEl = null;
319
- let annotState = { comments: [], strokes: [] };
320
- let annotActive = false;
321
- // `annotPointer` is either:
322
- // { kind: 'new', x0, y0, moved, strokeEl, strokePoints } creating a stroke/pin
323
- // { kind: 'pin', idx, startPointer, startPin, moved } dragging an existing pin
324
- let annotPointer = null;
325
- let annotEditing = null; // { idx, input, wrapEl }
326
- let annotLastPinClick = { idx: -1, time: 0 }; // for click-click-to-delete
327
-
328
- function initAnnotOverlay() {
329
- annotOverlayEl = document.createElement('div');
330
- annotOverlayEl.id = PREFIX + '-annot';
331
- Object.assign(annotOverlayEl.style, {
332
- position: 'fixed', top: '0', left: '0', width: '0', height: '0',
333
- pointerEvents: 'auto', zIndex: Z.highlight + 2,
334
- display: 'none', overflow: 'visible',
335
- cursor: 'crosshair', touchAction: 'none',
336
- });
337
-
338
- annotSvgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
339
- annotSvgEl.id = PREFIX + '-annot-svg';
340
- Object.assign(annotSvgEl.style, {
341
- position: 'absolute', top: '0', left: '0',
342
- width: '100%', height: '100%',
343
- // The SVG itself doesn't absorb clicks; individual hit-paths opt-in via
344
- // pointer-events=stroke so gaps still fall through to the overlay.
345
- pointerEvents: 'none', overflow: 'visible',
346
- });
347
- annotOverlayEl.appendChild(annotSvgEl);
348
-
349
- annotPinsEl = document.createElement('div');
350
- annotPinsEl.id = PREFIX + '-annot-pins';
351
- Object.assign(annotPinsEl.style, {
352
- position: 'absolute', inset: '0',
353
- pointerEvents: 'none',
354
- });
355
- annotOverlayEl.appendChild(annotPinsEl);
356
-
357
- annotClearChipEl = document.createElement('div');
358
- annotClearChipEl.id = PREFIX + '-annot-clear';
359
- annotClearChipEl.dataset.annotClear = 'true';
360
- annotClearChipEl.textContent = 'Clear';
361
- Object.assign(annotClearChipEl.style, {
362
- position: 'absolute', top: '8px', right: '8px',
363
- background: C.ink, color: C.white,
364
- fontFamily: FONT, fontSize: '10px', fontWeight: '500',
365
- letterSpacing: '0.08em', textTransform: 'uppercase',
366
- padding: '5px 12px', borderRadius: '999px',
367
- cursor: 'pointer', pointerEvents: 'auto',
368
- display: 'none', userSelect: 'none',
369
- boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
370
- });
371
- annotOverlayEl.appendChild(annotClearChipEl);
372
-
373
- annotOverlayEl.addEventListener('pointerdown', onAnnotDown);
374
- annotOverlayEl.addEventListener('pointermove', onAnnotMove);
375
- annotOverlayEl.addEventListener('pointerup', onAnnotUp);
376
- annotOverlayEl.addEventListener('pointercancel', onAnnotUp);
377
- document.body.appendChild(annotOverlayEl);
378
- // Modal-host friendliness: pointer-events is already 'auto' on this
379
- // overlay; we only need to silence the host's outside-interaction
380
- // listeners. Don't override pointer-events here (the overlay toggles
381
- // visibility via display:none, which is fine).
382
- defangOutsideHandlers(annotOverlayEl, { setPointerEvents: false });
383
- }
384
-
385
- function updateClearChip() {
386
- if (!annotClearChipEl) return;
387
- const hasAny = annotState.comments.length > 0 || annotState.strokes.length > 0;
388
- annotClearChipEl.style.display = hasAny ? 'block' : 'none';
389
- }
390
-
391
- function showAnnotOverlay(el) {
392
- if (!annotOverlayEl || !el) return;
393
- annotActive = true;
394
- positionAnnotOverlay(el);
395
- annotOverlayEl.style.display = 'block';
396
- }
397
-
398
- function hideAnnotOverlay() {
399
- annotActive = false;
400
- if (annotOverlayEl) annotOverlayEl.style.display = 'none';
401
- // Drop any in-progress edit without touching annotState — clearAnnotations
402
- // (if the caller is exiting configure mode) handles state reset.
403
- annotEditing = null;
404
- }
405
-
406
- function positionAnnotOverlay(el) {
407
- if (!annotOverlayEl || !el) return;
408
- const r = el.getBoundingClientRect();
409
- Object.assign(annotOverlayEl.style, {
410
- top: r.top + 'px', left: r.left + 'px',
411
- width: r.width + 'px', height: r.height + 'px',
412
- });
413
- annotSvgEl.setAttribute('viewBox', '0 0 ' + r.width + ' ' + r.height);
414
- }
415
-
416
- function clearAnnotations() {
417
- annotState.comments = [];
418
- annotState.strokes = [];
419
- if (annotSvgEl) while (annotSvgEl.firstChild) annotSvgEl.removeChild(annotSvgEl.firstChild);
420
- if (annotPinsEl) annotPinsEl.innerHTML = '';
421
- annotPointer = null;
422
- annotEditing = null;
423
- annotLastPinClick = { idx: -1, time: 0 };
424
- updateClearChip();
425
- }
426
-
427
- // Rebuild the SVG layer. Each stroke gets a wider invisible hit path
428
- // beneath the visible magenta path so clicks register on thin lines.
429
- function redrawStrokes() {
430
- while (annotSvgEl.firstChild) annotSvgEl.removeChild(annotSvgEl.firstChild);
431
- annotState.strokes.forEach((s, idx) => {
432
- const d = pointsToPath(s.points);
433
- const hit = document.createElementNS('http://www.w3.org/2000/svg', 'path');
434
- hit.setAttribute('d', d);
435
- hit.setAttribute('stroke', 'transparent');
436
- hit.setAttribute('stroke-width', '16');
437
- hit.setAttribute('stroke-linecap', 'round');
438
- hit.setAttribute('stroke-linejoin', 'round');
439
- hit.setAttribute('fill', 'none');
440
- hit.setAttribute('pointer-events', 'stroke');
441
- hit.style.cursor = 'pointer';
442
- hit.dataset.annotStroke = String(idx);
443
- annotSvgEl.appendChild(hit);
444
- const visible = document.createElementNS('http://www.w3.org/2000/svg', 'path');
445
- visible.setAttribute('d', d);
446
- visible.setAttribute('stroke', C.brand);
447
- visible.setAttribute('stroke-width', '3');
448
- visible.setAttribute('stroke-linecap', 'round');
449
- visible.setAttribute('stroke-linejoin', 'round');
450
- visible.setAttribute('fill', 'none');
451
- visible.setAttribute('pointer-events', 'none');
452
- annotSvgEl.appendChild(visible);
453
- });
454
- updateClearChip();
455
- }
456
-
457
- function localCoords(e) {
458
- const rect = annotOverlayEl.getBoundingClientRect();
459
- return { x: e.clientX - rect.left, y: e.clientY - rect.top };
460
- }
461
-
462
- function onAnnotDown(e) {
463
- if (!annotActive) return;
464
-
465
- // 1) Clear chip → wipe all annotations
466
- if (e.target.closest?.('[data-annot-clear]')) {
467
- if (annotEditing) annotEditing = null;
468
- clearAnnotations();
469
- renderAllPins();
470
- redrawStrokes();
471
- e.stopPropagation(); e.preventDefault();
472
- return;
473
- }
474
-
475
- // 2) Stroke hit path → delete that stroke
476
- const strokeHit = e.target.closest?.('[data-annot-stroke]');
477
- if (strokeHit) {
478
- const idx = parseInt(strokeHit.dataset.annotStroke, 10);
479
- if (Number.isInteger(idx)) {
480
- annotState.strokes.splice(idx, 1);
481
- redrawStrokes();
482
- }
483
- e.stopPropagation(); e.preventDefault();
484
- return;
485
- }
486
-
487
- // 3) Pin → drag, edit, or delete-on-double-click
488
- const pinWrap = e.target.closest?.('[data-annot-pin]');
489
- if (pinWrap) {
490
- const idx = parseInt(pinWrap.dataset.annotPin, 10);
491
- if (!Number.isInteger(idx)) return;
492
- // Double-click (two pointerdowns on the same pin within window) → delete.
493
- const now = Date.now();
494
- if (annotLastPinClick.idx === idx && now - annotLastPinClick.time < PIN_DBL_CLICK_MS) {
495
- if (annotEditing && annotEditing.idx === idx) annotEditing = null;
496
- annotState.comments.splice(idx, 1);
497
- annotLastPinClick = { idx: -1, time: 0 };
498
- renderAllPins();
499
- e.stopPropagation(); e.preventDefault();
500
- return;
501
- }
502
- annotLastPinClick = { idx, time: now };
503
- // If editing a different pin, commit that edit before starting here.
504
- if (annotEditing && annotEditing.idx !== idx) finalizeEditingPin();
505
- // If already editing THIS pin and the user clicked the dot, let the
506
- // input keep focus (don't start a drag — the click wasn't meant as one).
507
- if (annotEditing && annotEditing.idx === idx) return;
508
- const p = localCoords(e);
509
- const pin = annotState.comments[idx];
510
- annotPointer = {
511
- kind: 'pin', idx,
512
- startPointer: p,
513
- startPin: { x: pin.x, y: pin.y },
514
- moved: false,
515
- };
516
- try { annotOverlayEl.setPointerCapture(e.pointerId); } catch {}
517
- e.stopPropagation(); e.preventDefault();
518
- return;
519
- }
520
-
521
- // 4) Empty area → commit any open edit, then start new annotation
522
- if (annotEditing) {
523
- finalizeEditingPin();
524
- e.stopPropagation(); e.preventDefault();
525
- return;
526
- }
527
- const p = localCoords(e);
528
- annotPointer = { kind: 'new', x0: p.x, y0: p.y, moved: false, strokeEl: null, strokePoints: null };
529
- try { annotOverlayEl.setPointerCapture(e.pointerId); } catch {}
530
- e.stopPropagation(); e.preventDefault();
531
- }
532
-
533
- function onAnnotMove(e) {
534
- if (!annotActive || !annotPointer) return;
535
- const p = localCoords(e);
536
-
537
- if (annotPointer.kind === 'pin') {
538
- const dx = p.x - annotPointer.startPointer.x;
539
- const dy = p.y - annotPointer.startPointer.y;
540
- if (!annotPointer.moved) {
541
- if (Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
542
- annotPointer.moved = true;
543
- }
544
- const pin = annotState.comments[annotPointer.idx];
545
- if (!pin) { annotPointer = null; return; }
546
- pin.x = annotPointer.startPin.x + dx;
547
- pin.y = annotPointer.startPin.y + dy;
548
- renderAllPins();
549
- e.stopPropagation();
550
- return;
551
- }
552
-
553
- // kind === 'new'
554
- const dx = p.x - annotPointer.x0, dy = p.y - annotPointer.y0;
555
- if (!annotPointer.moved) {
556
- if (Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
557
- annotPointer.moved = true;
558
- const strokeEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
559
- strokeEl.setAttribute('stroke', C.brand);
560
- strokeEl.setAttribute('stroke-width', '3');
561
- strokeEl.setAttribute('stroke-linecap', 'round');
562
- strokeEl.setAttribute('stroke-linejoin', 'round');
563
- strokeEl.setAttribute('fill', 'none');
564
- strokeEl.setAttribute('pointer-events', 'none');
565
- annotSvgEl.appendChild(strokeEl);
566
- annotPointer.strokeEl = strokeEl;
567
- annotPointer.strokePoints = [[annotPointer.x0, annotPointer.y0]];
568
- }
569
- annotPointer.strokePoints.push([p.x, p.y]);
570
- annotPointer.strokeEl.setAttribute('d', pointsToPath(annotPointer.strokePoints));
571
- e.stopPropagation();
572
- }
573
-
574
- function onAnnotUp(e) {
575
- if (!annotActive || !annotPointer) return;
576
-
577
- if (annotPointer.kind === 'pin') {
578
- const wasDrag = annotPointer.moved;
579
- const idx = annotPointer.idx;
580
- try { annotOverlayEl.releasePointerCapture(e.pointerId); } catch {}
581
- annotPointer = null;
582
- if (wasDrag) {
583
- // A drag is an intentional reposition; a follow-up click shouldn't be
584
- // interpreted as a double-click-to-delete.
585
- annotLastPinClick = { idx: -1, time: 0 };
586
- } else {
587
- beginEditPin(idx);
588
- }
589
- e.stopPropagation();
590
- return;
591
- }
592
-
593
- // kind === 'new'
594
- const wasDrag = annotPointer.moved;
595
- if (wasDrag) {
596
- annotState.strokes.push({ points: annotPointer.strokePoints });
597
- // Swap the temporary preview SVG path for the full render with hit paths.
598
- redrawStrokes();
599
- } else {
600
- const idx = annotState.comments.length;
601
- annotState.comments.push({ x: annotPointer.x0, y: annotPointer.y0, text: '' });
602
- renderAllPins();
603
- beginEditPin(idx);
604
- }
605
- try { annotOverlayEl.releasePointerCapture(e.pointerId); } catch {}
606
- annotPointer = null;
607
- e.stopPropagation();
608
- }
609
-
610
- function pointsToPath(points) {
611
- if (!points || points.length === 0) return '';
612
- let d = 'M' + points[0][0].toFixed(1) + ' ' + points[0][1].toFixed(1);
613
- for (let i = 1; i < points.length; i++) {
614
- d += ' L' + points[i][0].toFixed(1) + ' ' + points[i][1].toFixed(1);
615
- }
616
- return d;
617
- }
618
-
619
- function renderAllPins() {
620
- annotPinsEl.innerHTML = '';
621
- annotState.comments.forEach((c, idx) => {
622
- annotPinsEl.appendChild(buildPinElement(c, idx));
623
- });
624
- updateClearChip();
625
- }
626
-
627
- function buildPinElement(comment, idx) {
628
- const interactive = idx >= 0;
629
- const wrap = document.createElement('div');
630
- if (interactive) wrap.dataset.annotPin = String(idx);
631
- Object.assign(wrap.style, {
632
- position: 'absolute',
633
- left: (comment.x - 7) + 'px', top: (comment.y - 7) + 'px',
634
- pointerEvents: interactive ? 'auto' : 'none',
635
- display: 'flex', alignItems: 'flex-start', gap: '6px',
636
- cursor: interactive ? 'grab' : 'default',
637
- touchAction: 'none',
638
- });
639
- const dot = document.createElement('div');
640
- Object.assign(dot.style, {
641
- width: '14px', height: '14px', borderRadius: '50%',
642
- background: C.brand, border: '2px solid ' + C.white,
643
- boxShadow: '0 1px 3px rgba(0,0,0,0.25)',
644
- flexShrink: '0',
645
- });
646
- wrap.appendChild(dot);
647
-
648
- if (comment.text) {
649
- const bubble = document.createElement('div');
650
- bubble.textContent = comment.text;
651
- Object.assign(bubble.style, {
652
- background: C.ink, color: C.white,
653
- fontFamily: FONT, fontSize: '12px', lineHeight: '1.4',
654
- padding: '4px 8px', borderRadius: '3px',
655
- marginTop: '-2px', maxWidth: '220px',
656
- pointerEvents: 'none', whiteSpace: 'pre-wrap',
657
- wordBreak: 'break-word',
658
- });
659
- wrap.appendChild(bubble);
660
- }
661
- return wrap;
662
- }
663
-
664
- function beginEditPin(idx) {
665
- const wrapEl = annotPinsEl.querySelector('[data-annot-pin="' + idx + '"]');
666
- if (!wrapEl) return;
667
- // Strip any existing bubble (but keep the dot)
668
- wrapEl.querySelectorAll('div:not(:first-child)').forEach(n => n.remove());
669
- const input = document.createElement('input');
670
- input.type = 'text';
671
- input.placeholder = 'Note…';
672
- Object.assign(input.style, {
673
- background: C.ink, color: C.white,
674
- fontFamily: FONT, fontSize: '12px', lineHeight: '1.4',
675
- padding: '4px 8px', borderRadius: '3px',
676
- border: '1px solid ' + C.brand,
677
- outline: 'none', marginTop: '-2px',
678
- width: '220px', pointerEvents: 'auto',
679
- });
680
- const originalText = annotState.comments[idx].text || '';
681
- input.value = originalText;
682
- wrapEl.appendChild(input);
683
- annotEditing = { idx, input, wrapEl, originalText };
684
- input.addEventListener('keydown', onAnnotInputKey, true);
685
- input.addEventListener('blur', () => {
686
- // Fires on both focus-loss and programmatic blur; commit unless we
687
- // already handled it.
688
- if (annotEditing && annotEditing.input === input) finalizeEditingPin();
689
- });
690
- // Stop clicks/pointerdowns inside the input from bubbling to the overlay
691
- ['pointerdown', 'click'].forEach(ev => {
692
- input.addEventListener(ev, e => e.stopPropagation());
693
- });
694
- setTimeout(() => input.focus(), 0);
695
- }
696
-
697
- function onAnnotInputKey(e) {
698
- if (e.key === 'Enter') {
699
- e.preventDefault(); e.stopPropagation();
700
- finalizeEditingPin();
701
- } else if (e.key === 'Escape') {
702
- e.preventDefault(); e.stopPropagation();
703
- cancelEditingPin();
704
- } else {
705
- // Keep arrows / backspace from hitting global handlers
706
- e.stopPropagation();
707
- }
708
- }
709
-
710
- function finalizeEditingPin() {
711
- if (!annotEditing) return;
712
- const { idx, input } = annotEditing;
713
- const text = input.value.trim();
714
- annotEditing = null;
715
- if (text) annotState.comments[idx].text = text;
716
- else annotState.comments.splice(idx, 1);
717
- renderAllPins();
718
- }
719
-
720
- function cancelEditingPin() {
721
- if (!annotEditing) return;
722
- const { idx, originalText } = annotEditing;
723
- annotEditing = null;
724
- // If the pin had text before this edit, revert to it. If it was a
725
- // just-created empty pin, Escape removes it.
726
- if (originalText) {
727
- annotState.comments[idx].text = originalText;
728
- } else {
729
- annotState.comments.splice(idx, 1);
730
- }
731
- renderAllPins();
732
- }
733
-
734
- // Build a detached annotation subtree suitable for injection into the clone
735
- // modern-screenshot creates. Coordinates are element-local so this slots
736
- // straight into an element that's been made position:relative. Takes an
737
- // explicit snapshot so it works after annotState has been cleared.
738
- function buildAnnotationsForCapture(rect, snapshot) {
739
- const comments = snapshot ? snapshot.comments : annotState.comments;
740
- const strokes = snapshot ? snapshot.strokes : annotState.strokes;
741
- if (comments.length === 0 && strokes.length === 0) return null;
742
- const wrap = document.createElement('div');
743
- Object.assign(wrap.style, {
744
- position: 'absolute', top: '0', left: '0',
745
- width: rect.width + 'px', height: rect.height + 'px',
746
- pointerEvents: 'none', overflow: 'visible',
747
- });
748
- if (strokes.length > 0) {
749
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
750
- svg.setAttribute('viewBox', '0 0 ' + rect.width + ' ' + rect.height);
751
- Object.assign(svg.style, {
752
- position: 'absolute', top: '0', left: '0',
753
- width: '100%', height: '100%', overflow: 'visible',
754
- });
755
- for (const s of strokes) {
756
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
757
- path.setAttribute('stroke', C.brand);
758
- path.setAttribute('stroke-width', '3');
759
- path.setAttribute('stroke-linecap', 'round');
760
- path.setAttribute('stroke-linejoin', 'round');
761
- path.setAttribute('fill', 'none');
762
- path.setAttribute('d', pointsToPath(s.points));
763
- svg.appendChild(path);
764
- }
765
- wrap.appendChild(svg);
766
- }
767
- for (const c of comments) {
768
- // idx=-1 means non-interactive; pointerEvents stay off in the clone
769
- wrap.appendChild(buildPinElement(c, -1));
770
- }
771
- return wrap;
772
- }
773
-
774
- // ---------------------------------------------------------------------------
775
- // Element context extraction
776
- // ---------------------------------------------------------------------------
777
-
778
- function extractContext(el) {
779
- const cs = getComputedStyle(el);
780
- const r = el.getBoundingClientRect();
781
- const props = {};
782
- for (const sheet of document.styleSheets) {
783
- try {
784
- for (const rule of sheet.cssRules) {
785
- if (rule.style) for (let i = 0; i < rule.style.length; i++) {
786
- const p = rule.style[i];
787
- if (p.startsWith('--') && !props[p]) {
788
- const v = cs.getPropertyValue(p).trim();
789
- if (v) props[p] = v;
790
- }
791
- }
792
- }
793
- } catch { /* cross-origin */ }
794
- }
795
- return {
796
- tagName: el.tagName.toLowerCase(), id: el.id || null,
797
- classes: [...el.classList],
798
- textContent: (el.textContent || '').slice(0, 500),
799
- outerHTML: el.outerHTML.slice(0, 10000),
800
- computedStyles: {
801
- 'font-family': cs.fontFamily, 'font-size': cs.fontSize,
802
- 'font-weight': cs.fontWeight, 'line-height': cs.lineHeight,
803
- 'color': cs.color, 'background': cs.background,
804
- 'background-color': cs.backgroundColor,
805
- 'padding': cs.padding, 'margin': cs.margin,
806
- 'display': cs.display, 'position': cs.position,
807
- 'gap': cs.gap, 'border-radius': cs.borderRadius,
808
- 'box-shadow': cs.boxShadow,
809
- },
810
- cssCustomProperties: props,
811
- parentContext: el.parentElement
812
- ? '<' + el.parentElement.tagName.toLowerCase()
813
- + (el.parentElement.id ? ' id="' + el.parentElement.id + '"' : '')
814
- + (el.parentElement.className ? ' class="' + el.parentElement.className + '"' : '')
815
- + '>'
816
- : null,
817
- boundingRect: { width: Math.round(r.width), height: Math.round(r.height) },
818
- };
819
- }
820
-
821
- // ---------------------------------------------------------------------------
822
- // The Bar — one floating element, three modes
823
- // ---------------------------------------------------------------------------
824
-
825
- // Contextual-bar palette. Cached at init so every build*Row reads a
826
- // consistent set of colors; detectPageTheme runs once rather than on every
827
- // phase transition.
828
- let BP = null;
829
-
830
- // Bar shadow variants. The default projects down + subtle around. When
831
- // the Tune popover opens below the bar, a downward shadow lands on the
832
- // dark popover and reads as a bright ghost line. We swap to UP-only while
833
- // tune is open below so the popover's top edge is clean.
834
- const BAR_SHADOW_DEFAULT = '0 4px 20px oklch(0% 0 0 / 0.08), 0 1px 3px oklch(0% 0 0 / 0.06)';
835
- const BAR_SHADOW_UP = '0 -4px 20px oklch(0% 0 0 / 0.08), 0 -1px 3px oklch(0% 0 0 / 0.06)';
836
- const BAR_SHADOW_DOWN = BAR_SHADOW_DEFAULT;
837
-
838
- function initBar() {
839
- BP = barPaletteForTheme(detectPageTheme());
840
- barEl = document.createElement('div');
841
- barEl.id = PREFIX + '-bar';
842
- Object.assign(barEl.style, {
843
- position: 'fixed', zIndex: Z.bar,
844
- display: 'none', opacity: '0',
845
- transform: 'translateY(6px)',
846
- transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
847
- background: BP.surface,
848
- backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)',
849
- border: '1px solid ' + BP.hairline,
850
- borderRadius: '10px',
851
- boxShadow: BAR_SHADOW_DEFAULT,
852
- transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
853
- fontFamily: FONT, fontSize: '13px', color: BP.text,
854
- padding: '6px',
855
- maxWidth: '520px', minWidth: '320px',
856
- });
857
- document.body.appendChild(barEl);
858
- defangOutsideHandlers(barEl);
859
- }
860
-
861
- function positionBar() {
862
- if (!barEl || !selectedElement) return;
863
- const r = selectedElement.getBoundingClientRect();
864
- const barH = barEl.offsetHeight || 44;
865
- const barW = barEl.offsetWidth || 380;
866
- const GLOBAL_BAR_RESERVE = 64; // global bar height + bottom margin + breathing room
867
- const GAP = 8;
868
-
869
- // Prefer below the element; fall back to above; if neither fits (element
870
- // taller than viewport), pin to a stable viewport anchor so the bar
871
- // doesn't teleport between top and bottom as the user scrolls.
872
- let top;
873
- const belowTop = r.bottom + GAP;
874
- const aboveTop = r.top - barH - GAP;
875
- if (belowTop + barH + GAP <= window.innerHeight - GLOBAL_BAR_RESERVE) {
876
- top = belowTop;
877
- } else if (aboveTop >= GAP) {
878
- top = aboveTop;
879
- } else {
880
- top = window.innerHeight - barH - GLOBAL_BAR_RESERVE;
881
- }
882
-
883
- let left = r.left + (r.width - barW) / 2;
884
- if (left < GAP) left = GAP;
885
- if (left + barW > window.innerWidth - GAP) left = window.innerWidth - barW - GAP;
886
- Object.assign(barEl.style, { top: top + 'px', left: left + 'px' });
887
- }
888
-
889
- function showBar(mode) {
890
- barEl.innerHTML = '';
891
- if (mode === 'configure') barEl.appendChild(buildConfigureRow());
892
- else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
893
- else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
894
- barEl.style.display = 'block';
895
- positionBar();
896
- requestAnimationFrame(() => {
897
- barEl.style.opacity = '1';
898
- barEl.style.transform = 'translateY(0)';
899
- });
900
- }
901
-
902
- function hideBar() {
903
- if (!barEl) return;
904
- barEl.style.opacity = '0';
905
- barEl.style.transform = 'translateY(6px)';
906
- setTimeout(() => { if (barEl) barEl.style.display = 'none'; }, 250);
907
- hideActionPicker();
908
- closeTunePopover();
909
- }
910
-
911
- function updateBarContent(mode) {
912
- if (!barEl || barEl.style.display === 'none') return;
913
- barEl.innerHTML = '';
914
- // Reset bar styling to the theme-aware palette
915
- barEl.style.background = BP.surface;
916
- barEl.style.border = '1px solid ' + BP.hairline;
917
- if (mode === 'configure') barEl.appendChild(buildConfigureRow());
918
- else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
919
- else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
920
- else if (mode === 'saving') barEl.appendChild(buildSavingRow());
921
- else if (mode === 'confirmed') {
922
- barEl.appendChild(buildConfirmedRow());
923
- barEl.style.background = 'oklch(95% 0.05 145)';
924
- barEl.style.border = '1px solid oklch(75% 0.12 145 / 0.4)';
925
- }
926
- }
927
-
928
- // --- Configure row ---
929
-
930
- function buildConfigureRow() {
931
- const row = el('div', {
932
- display: 'flex', alignItems: 'center', gap: '4px',
933
- });
934
-
935
- // Action pill
936
- const pill = el('button', {
937
- display: 'inline-flex', alignItems: 'center', gap: '4px',
938
- padding: '5px 10px', borderRadius: '6px',
939
- background: BP.mark, color: BP.markText,
940
- fontFamily: FONT, fontSize: '12px', fontWeight: '500',
941
- border: 'none', cursor: 'pointer',
942
- transition: 'background 0.12s ease, transform 0.1s ease',
943
- whiteSpace: 'nowrap', flexShrink: '0',
944
- });
945
- pill.textContent = actionLabel() + ' \u25BE';
946
- pill.addEventListener('mouseenter', () => pill.style.background = BP.accent);
947
- pill.addEventListener('mouseleave', () => pill.style.background = BP.mark);
948
- pill.addEventListener('mousedown', () => pill.style.transform = 'scale(0.97)');
949
- pill.addEventListener('mouseup', () => pill.style.transform = 'scale(1)');
950
- pill.addEventListener('click', (e) => { e.stopPropagation(); toggleActionPicker(); });
951
- row.appendChild(pill);
952
-
953
- // Freeform input. Focus state shows an accent-colored border only —
954
- // an earlier version tinted the background with `BP.accentSoft`, which
955
- // composited against the dark bar surface to a murky purple where the
956
- // browser's default placeholder gray was unreadable. Placeholder color
957
- // is set explicitly via a one-shot stylesheet keyed off this input's id
958
- // so it picks up the bar's `textDim` token in both themes.
959
- const input = document.createElement('input');
960
- input.id = PREFIX + '-input';
961
- input.type = 'text';
962
- input.placeholder = selectedAction === 'impeccable' ? 'describe what you want...' : 'refine further (optional)...';
963
- Object.assign(input.style, {
964
- flex: '1', minWidth: '0',
965
- padding: '5px 8px', borderRadius: '6px',
966
- border: '1px solid transparent', background: 'transparent',
967
- fontFamily: FONT, fontSize: '12px', color: BP.text,
968
- outline: 'none',
969
- transition: 'border-color 0.15s ease',
970
- });
971
- if (!document.getElementById(PREFIX + '-input-style')) {
972
- const s = document.createElement('style');
973
- s.id = PREFIX + '-input-style';
974
- s.textContent =
975
- '#' + PREFIX + '-input::placeholder { color: ' + BP.textDim + '; opacity: 1; }';
976
- document.head.appendChild(s);
977
- }
978
- input.addEventListener('focus', () => {
979
- input.style.borderColor = BP.accent;
980
- });
981
- input.addEventListener('blur', () => {
982
- input.style.borderColor = 'transparent';
983
- });
984
- input.addEventListener('keydown', (e) => {
985
- if (e.key === 'Enter') { e.stopPropagation(); e.preventDefault(); handleGo(); return; }
986
- if (e.key === 'Escape') { e.stopPropagation(); e.preventDefault(); input.blur(); hideBar(); state = 'PICKING'; return; }
987
- // Let arrow keys pass through to the element picker when the input is empty
988
- if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && !input.value) return;
989
- e.stopPropagation();
990
- });
991
- row.appendChild(input);
992
-
993
- // Variant count toggle
994
- const count = el('button', {
995
- padding: '4px 6px', borderRadius: '5px',
996
- border: '1px solid ' + BP.hairline, background: 'transparent',
997
- fontFamily: MONO, fontSize: '11px', fontWeight: '600',
998
- color: BP.textDim, cursor: 'pointer',
999
- transition: 'color 0.12s ease, border-color 0.12s ease',
1000
- flexShrink: '0', whiteSpace: 'nowrap',
1001
- });
1002
- count.textContent = '\u00D7' + selectedCount;
1003
- count.title = 'Variants: click to change';
1004
- count.addEventListener('mouseenter', () => { count.style.color = BP.text; count.style.borderColor = BP.text; });
1005
- count.addEventListener('mouseleave', () => { count.style.color = BP.textDim; count.style.borderColor = BP.hairline; });
1006
- count.addEventListener('click', (e) => {
1007
- e.stopPropagation();
1008
- selectedCount = selectedCount >= 4 ? 2 : selectedCount + 1;
1009
- count.textContent = '\u00D7' + selectedCount;
1010
- });
1011
- row.appendChild(count);
1012
-
1013
- // Go button
1014
- const go = el('button', {
1015
- padding: '5px 12px', borderRadius: '6px',
1016
- border: 'none', background: BP.accent, color: BP.mark,
1017
- fontFamily: FONT, fontSize: '12px', fontWeight: '600',
1018
- cursor: 'pointer',
1019
- transition: 'filter 0.12s ease, transform 0.1s ease',
1020
- flexShrink: '0', whiteSpace: 'nowrap',
1021
- });
1022
- go.textContent = 'Go \u2192';
1023
- go.addEventListener('mouseenter', () => go.style.filter = 'brightness(1.1)');
1024
- go.addEventListener('mouseleave', () => go.style.filter = 'none');
1025
- go.addEventListener('mousedown', () => go.style.transform = 'scale(0.97)');
1026
- go.addEventListener('mouseup', () => go.style.transform = 'scale(1)');
1027
- go.addEventListener('click', (e) => { e.stopPropagation(); handleGo(); });
1028
- row.appendChild(go);
1029
-
1030
- // Auto-focus input after a beat
1031
- setTimeout(() => input.focus(), 60);
1032
- return row;
1033
- }
1034
-
1035
- // --- Generating row ---
1036
-
1037
- function buildGeneratingRow() {
1038
- const row = el('div', {
1039
- display: 'flex', alignItems: 'center', gap: '8px',
1040
- padding: '2px 4px',
1041
- });
1042
-
1043
- // Action label
1044
- const label = el('span', {
1045
- fontWeight: '600', fontSize: '12px', color: BP.text,
1046
- flexShrink: '0', whiteSpace: 'nowrap',
1047
- });
1048
- label.textContent = actionLabel();
1049
- row.appendChild(label);
1050
-
1051
- // Dots
1052
- row.appendChild(buildDots(false));
1053
-
1054
- // Status
1055
- const status = el('span', {
1056
- fontSize: '11px', color: BP.textDim, whiteSpace: 'nowrap',
1057
- marginLeft: 'auto',
1058
- });
1059
- // Variants currently arrive atomically in a single file edit, so a
1060
- // per-variant counter would lie. Say what's true.
1061
- status.textContent = arrivedVariants < expectedVariants
1062
- ? 'Generating ' + expectedVariants + ' variants...'
1063
- : 'Done';
1064
- row.appendChild(status);
1065
-
1066
- return row;
1067
- }
1068
-
1069
- // --- Cycling row ---
1070
-
1071
- const TUNE_ICON_SVG = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" style="flex-shrink:0"><line x1="4" y1="8" x2="20" y2="8"/><circle cx="14" cy="8" r="2.4" fill="currentColor" stroke="none"/><line x1="4" y1="16" x2="20" y2="16"/><circle cx="10" cy="16" r="2.4" fill="currentColor" stroke="none"/></svg>';
1072
-
1073
- function buildCyclingRow() {
1074
- const row = el('div', {
1075
- display: 'flex', alignItems: 'center', gap: '6px',
1076
- padding: '1px 2px',
1077
- });
1078
-
1079
- // Prev
1080
- const prev = navBtn('\u2190');
1081
- prev.addEventListener('click', (e) => { e.stopPropagation(); cycleVariant(-1); });
1082
- if (visibleVariant <= 1) prev.style.opacity = '0.3';
1083
- row.appendChild(prev);
1084
-
1085
- // Dots (clickable)
1086
- row.appendChild(buildDots(true));
1087
-
1088
- // Counter
1089
- const counter = el('span', {
1090
- fontFamily: MONO, fontSize: '11px', fontWeight: '500',
1091
- color: BP.textDim, minWidth: '24px', textAlign: 'center',
1092
- });
1093
- counter.textContent = visibleVariant + '/' + arrivedVariants;
1094
- row.appendChild(counter);
1095
-
1096
- // Next
1097
- const next = navBtn('\u2192');
1098
- next.addEventListener('click', (e) => { e.stopPropagation(); cycleVariant(1); });
1099
- if (visibleVariant >= arrivedVariants) next.style.opacity = '0.3';
1100
- row.appendChild(next);
1101
-
1102
- // Tune chip — only when the visible variant exposes params
1103
- const visParams = parseVariantParams(getVisibleVariantEl());
1104
- const hasParams = visParams.length > 0;
1105
- if (hasParams) {
1106
- const tune = el('button', {
1107
- display: 'inline-flex', alignItems: 'center', gap: '6px',
1108
- padding: '4px 10px', borderRadius: '5px',
1109
- border: '1px solid transparent',
1110
- background: tuneOpen ? BP.accentSoft : 'transparent',
1111
- color: tuneOpen ? BP.accent : BP.text,
1112
- fontFamily: FONT, fontSize: '11px', fontWeight: '500',
1113
- cursor: 'pointer',
1114
- transition: 'color 0.12s ease, background 0.12s ease',
1115
- whiteSpace: 'nowrap',
1116
- });
1117
- tune.innerHTML = TUNE_ICON_SVG;
1118
- const tuneLabel = document.createElement('span');
1119
- tuneLabel.textContent = 'Tune';
1120
- tune.appendChild(tuneLabel);
1121
- const tuneBadge = document.createElement('span');
1122
- Object.assign(tuneBadge.style, {
1123
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
1124
- minWidth: '16px', height: '16px', padding: '0 4px',
1125
- borderRadius: '999px',
1126
- background: tuneOpen ? C.brand : BP.hairline,
1127
- color: tuneOpen ? 'oklch(98% 0 0)' : 'inherit',
1128
- fontFamily: MONO, fontSize: '9.5px', fontWeight: '600',
1129
- lineHeight: '1',
1130
- boxSizing: 'border-box',
1131
- });
1132
- tuneBadge.textContent = String(visParams.length);
1133
- tune.appendChild(tuneBadge);
1134
- tune.title = 'Tune this variant (' + visParams.length + ' knob' + (visParams.length === 1 ? '' : 's') + ')';
1135
- tune.addEventListener('mouseenter', () => {
1136
- if (!tuneOpen) tune.style.background = BP.accentSoft;
1137
- });
1138
- tune.addEventListener('mouseleave', () => {
1139
- if (!tuneOpen) tune.style.background = 'transparent';
1140
- });
1141
- tune.addEventListener('click', (e) => { e.stopPropagation(); toggleTunePopover(); });
1142
- tune.dataset.iceqTune = '1';
1143
- row.appendChild(tune);
1144
- }
1145
-
1146
- // Spacer
1147
- row.appendChild(el('div', { flex: '1' }));
1148
-
1149
- // Accept — primary action, uses the site's saturated brand magenta
1150
- // with paper-white text, not the theme-muted BP.accent.
1151
- const accept = el('button', {
1152
- padding: '5px 14px', borderRadius: '5px',
1153
- border: 'none', background: C.brand, color: 'oklch(98% 0 0)',
1154
- fontFamily: FONT, fontSize: '11px', fontWeight: '600',
1155
- cursor: 'pointer', transition: 'filter 0.12s ease, transform 0.1s ease',
1156
- whiteSpace: 'nowrap',
1157
- });
1158
- accept.textContent = '\u2713 Accept';
1159
- accept.addEventListener('mouseenter', () => accept.style.filter = 'brightness(1.08)');
1160
- accept.addEventListener('mouseleave', () => accept.style.filter = 'none');
1161
- accept.addEventListener('mousedown', () => accept.style.transform = 'scale(0.97)');
1162
- accept.addEventListener('mouseup', () => accept.style.transform = 'scale(1)');
1163
- accept.addEventListener('click', (e) => { e.stopPropagation(); handleAccept(); });
1164
- if (arrivedVariants === 0) { accept.style.opacity = '0.3'; accept.style.pointerEvents = 'none'; }
1165
- row.appendChild(accept);
1166
-
1167
- // Discard
1168
- const discard = el('button', {
1169
- padding: '4px 6px', borderRadius: '5px',
1170
- border: '1px solid ' + BP.hairline, background: 'transparent',
1171
- fontFamily: FONT, fontSize: '11px', color: BP.textDim,
1172
- cursor: 'pointer', transition: 'color 0.12s ease, border-color 0.12s ease',
1173
- });
1174
- discard.textContent = '\u2715';
1175
- discard.title = 'Discard all variants';
1176
- discard.addEventListener('mouseenter', () => { discard.style.color = BP.text; discard.style.borderColor = BP.text; });
1177
- discard.addEventListener('mouseleave', () => { discard.style.color = BP.textDim; discard.style.borderColor = BP.hairline; });
1178
- discard.addEventListener('click', (e) => { e.stopPropagation(); handleDiscard(); });
1179
- row.appendChild(discard);
1180
-
1181
- return row;
1182
- }
1183
-
1184
- // --- Shared UI builders ---
1185
-
1186
- // --- Saving row (waiting for agent to process accept/discard) ---
1187
-
1188
- function buildSavingRow() {
1189
- const row = el('div', {
1190
- display: 'flex', alignItems: 'center', gap: '8px',
1191
- padding: '2px 8px',
1192
- });
1193
- const spinner = el('div', {
1194
- width: '14px', height: '14px', borderRadius: '50%',
1195
- border: '2px solid ' + BP.hairline,
1196
- borderTopColor: BP.accent,
1197
- animation: 'impeccable-spin 0.6s linear infinite',
1198
- flexShrink: '0',
1199
- });
1200
- row.appendChild(spinner);
1201
- const label = el('span', {
1202
- fontSize: '12px', color: BP.textDim, fontWeight: '500',
1203
- });
1204
- label.textContent = 'Applying variant...';
1205
- row.appendChild(label);
1206
-
1207
- // Inject the keyframes if not already present
1208
- if (!document.getElementById(PREFIX + '-keyframes')) {
1209
- const style = document.createElement('style');
1210
- style.id = PREFIX + '-keyframes';
1211
- style.textContent = '@keyframes impeccable-spin { to { transform: rotate(360deg); } }';
1212
- document.head.appendChild(style);
1213
- }
1214
- return row;
1215
- }
1216
-
1217
- // --- Confirmed row (green success, auto-dismisses) ---
1218
-
1219
- function buildConfirmedRow() {
1220
- const row = el('div', {
1221
- display: 'flex', alignItems: 'center', gap: '8px',
1222
- padding: '2px 8px',
1223
- });
1224
- const check = el('span', {
1225
- fontSize: '15px', lineHeight: '1', flexShrink: '0',
1226
- color: 'oklch(45% 0.15 145)',
1227
- });
1228
- check.textContent = '\u2713';
1229
- row.appendChild(check);
1230
- const label = el('span', {
1231
- fontSize: '12px', color: 'oklch(35% 0.1 145)', fontWeight: '600',
1232
- });
1233
- label.textContent = 'Variant applied';
1234
- row.appendChild(label);
1235
- return row;
1236
- }
1237
-
1238
- // --- Shared UI builders ---
1239
-
1240
- function buildDots(clickable) {
1241
- const container = el('div', {
1242
- display: 'flex', alignItems: 'center', gap: '4px',
1243
- });
1244
- for (let i = 1; i <= expectedVariants; i++) {
1245
- const arrived = i <= arrivedVariants;
1246
- const active = i === visibleVariant;
1247
- // active: solid site-brand magenta dot. arrived+inactive: muted neutral.
1248
- // pending (not yet arrived): faint outline ring. No borders on arrived
1249
- // dots — the previous "accent ring + ash fill" combo read as noisy
1250
- // magenta chips, especially when all variants had arrived and every
1251
- // dot wore an accent ring.
1252
- const dotBg = active ? C.brand
1253
- : arrived ? BP.textDim
1254
- : 'transparent';
1255
- const dotBorder = arrived ? 'none' : '1.5px solid ' + BP.hairline;
1256
- const dot = el('div', {
1257
- width: active ? '8px' : '6px',
1258
- height: active ? '8px' : '6px',
1259
- borderRadius: '50%',
1260
- background: dotBg,
1261
- border: dotBorder,
1262
- boxSizing: 'border-box',
1263
- transition: 'all 0.2s ' + EASE,
1264
- cursor: (clickable && arrived) ? 'pointer' : 'default',
1265
- transform: arrived ? 'scale(1)' : 'scale(0.85)',
1266
- opacity: arrived ? (active ? '1' : '0.6') : '0.4',
1267
- });
1268
- if (clickable && arrived) {
1269
- const idx = i;
1270
- dot.addEventListener('click', (e) => {
1271
- e.stopPropagation();
1272
- visibleVariant = idx;
1273
- showVariantInDOM(currentSessionId, idx);
1274
- updateSelectedElement();
1275
- updateBarContent('cycling');
1276
- });
1277
- }
1278
- container.appendChild(dot);
1279
- }
1280
- return container;
1281
- }
1282
-
1283
- function navBtn(text) {
1284
- const b = el('button', {
1285
- width: '26px', height: '26px', borderRadius: '5px',
1286
- border: '1px solid ' + BP.hairline, background: 'transparent',
1287
- color: BP.text, fontFamily: FONT, fontSize: '13px',
1288
- cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
1289
- transition: 'border-color 0.12s ease, background 0.12s ease',
1290
- padding: '0', lineHeight: '1',
1291
- });
1292
- b.textContent = text;
1293
- b.addEventListener('mouseenter', () => { b.style.borderColor = BP.text; });
1294
- b.addEventListener('mouseleave', () => { b.style.borderColor = BP.hairline; });
1295
- return b;
1296
- }
1297
-
1298
- function actionLabel() {
1299
- const a = ACTIONS.find(a => a.value === selectedAction);
1300
- return a ? a.label : 'Freeform';
1301
- }
1302
-
1303
- function el(tag, styles) {
1304
- const e = document.createElement(tag);
1305
- if (styles) Object.assign(e.style, styles);
1306
- return e;
1307
- }
1308
-
1309
- // ---------------------------------------------------------------------------
1310
- // Action picker popover
1311
- // ---------------------------------------------------------------------------
1312
-
1313
- function initActionPicker() {
1314
- const P = barPaletteForTheme(detectPageTheme());
1315
- pickerEl = document.createElement('div');
1316
- pickerEl.id = PREFIX + '-picker';
1317
- Object.assign(pickerEl.style, {
1318
- position: 'fixed', zIndex: Z.picker,
1319
- display: 'none', opacity: '0',
1320
- transform: 'scale(0.96) translateY(4px)',
1321
- transformOrigin: 'bottom left',
1322
- transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
1323
- background: P.surface,
1324
- border: '1px solid ' + P.hairline,
1325
- borderRadius: '10px',
1326
- boxShadow: '0 8px 30px oklch(0% 0 0 / 0.10), 0 2px 6px oklch(0% 0 0 / 0.06)',
1327
- padding: '6px',
1328
- fontFamily: FONT,
1329
- backdropFilter: 'blur(10px)',
1330
- WebkitBackdropFilter: 'blur(10px)',
1331
- });
1332
-
1333
- // Build the chip grid
1334
- const grid = el('div', {
1335
- display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '3px',
1336
- });
1337
-
1338
- ACTIONS.forEach(action => {
1339
- const chip = el('button', {
1340
- display: 'flex', flexDirection: 'column', alignItems: 'center',
1341
- gap: '4px',
1342
- padding: '8px 6px', borderRadius: '6px',
1343
- border: 'none',
1344
- background: action.value === selectedAction ? P.accentSoft : 'transparent',
1345
- color: action.value === selectedAction ? P.accent : P.text,
1346
- fontFamily: FONT, fontSize: '11px', fontWeight: '500',
1347
- cursor: 'pointer',
1348
- transition: 'background 0.1s ease, color 0.1s ease',
1349
- textAlign: 'center', whiteSpace: 'nowrap',
1350
- });
1351
- const iconWrap = el('span', {
1352
- display: 'flex', alignItems: 'center', justifyContent: 'center',
1353
- height: '20px', opacity: '0.9',
1354
- });
1355
- iconWrap.innerHTML = ICONS[action.value] || '';
1356
- const labelEl = el('span', { lineHeight: '1' });
1357
- labelEl.textContent = action.label;
1358
- chip.appendChild(iconWrap);
1359
- chip.appendChild(labelEl);
1360
- chip.dataset.action = action.value;
1361
- chip.addEventListener('mouseenter', () => {
1362
- if (action.value !== selectedAction) chip.style.background = P.accentSoft;
1363
- });
1364
- chip.addEventListener('mouseleave', () => {
1365
- chip.style.background = action.value === selectedAction ? P.accentSoft : 'transparent';
1366
- });
1367
- chip.addEventListener('click', (e) => {
1368
- e.stopPropagation();
1369
- selectedAction = action.value;
1370
- hideActionPicker();
1371
- updateBarContent('configure');
1372
- });
1373
- grid.appendChild(chip);
1374
- });
1375
-
1376
- pickerEl.appendChild(grid);
1377
- document.body.appendChild(pickerEl);
1378
- defangOutsideHandlers(pickerEl);
1379
-
1380
- // Cache the palette on the picker so toggleActionPicker's state refresh
1381
- // uses the same theme-aware colors when it repaints chips.
1382
- pickerEl.__iceq_palette = P;
1383
- }
1384
-
1385
- function toggleActionPicker() {
1386
- if (pickerEl.style.display !== 'none') { hideActionPicker(); return; }
1387
- // Rebuild chips to reflect current selection
1388
- const P = pickerEl.__iceq_palette || barPaletteForTheme(detectPageTheme());
1389
- pickerEl.querySelectorAll('button').forEach(chip => {
1390
- const isActive = chip.dataset.action === selectedAction;
1391
- chip.style.background = isActive ? P.accentSoft : 'transparent';
1392
- chip.style.color = isActive ? P.accent : P.text;
1393
- });
1394
- // Position above the bar
1395
- const barRect = barEl.getBoundingClientRect();
1396
- const pickerH = 170; // approximate; grows with icon + label rows
1397
- let top = barRect.top - pickerH - 6;
1398
- if (top < 8) top = barRect.bottom + 6;
1399
- Object.assign(pickerEl.style, {
1400
- top: top + 'px', left: barRect.left + 'px',
1401
- display: 'block',
1402
- });
1403
- requestAnimationFrame(() => {
1404
- pickerEl.style.opacity = '1';
1405
- pickerEl.style.transform = 'scale(1) translateY(0)';
1406
- });
1407
- }
1408
-
1409
- function hideActionPicker() {
1410
- if (!pickerEl) return;
1411
- pickerEl.style.opacity = '0';
1412
- pickerEl.style.transform = 'scale(0.96) translateY(4px)';
1413
- setTimeout(() => { if (pickerEl) pickerEl.style.display = 'none'; }, 180);
1414
- }
1415
-
1416
- // ---------------------------------------------------------------------------
1417
- // Params panel (per-variant coarse controls)
1418
- //
1419
- // Variants may declare a parameter manifest via a JSON attribute on the
1420
- // variant wrapper:
1421
- //
1422
- // <div data-impeccable-variant="1"
1423
- // data-impeccable-params='[{"id":"density","kind":"steps",...}]'>
1424
- //
1425
- // The panel docks to the right edge of the outline during CYCLING and
1426
- // exposes 2-5 coarse knobs. Values apply to the variant wrapper so scoped
1427
- // CSS can respond instantly without regeneration:
1428
- //
1429
- // range / numeric toggle → CSS var (`--p-<id>`) used via var(--p-foo, N)
1430
- // steps / boolean toggle → data-p-<id> attribute used via :scope[data-p-foo="..."]
1431
- //
1432
- // On variant switch, values reset to that variant's declared defaults.
1433
- // On accept, current values are sent in the event payload so the agent
1434
- // can bake them into the source-file write.
1435
- // ---------------------------------------------------------------------------
1436
-
1437
- let paramsPanelEl = null; // outer wrapper (overflow:hidden, clips the slide)
1438
- let paramsPanelInner = null; // translating content (carries bg, padding, knobs)
1439
- let paramsPanelBody = null; // grid holding the knob cells
1440
- let paramsCurrentValues = {}; // {paramId: value} — mirror of the visible variant's live values
1441
- let tuneOpen = false; // whether the Tune popover is open right now
1442
-
1443
- // Theme-aware Tune popover. Appears as a drawer that slides out from the
1444
- // contextual bar's bar-facing edge (below if the bar sits below the
1445
- // element, above otherwise). Same width as the bar. Auto-wraps to extra
1446
- // rows when the knobs exceed one row. The bar's border-radius on the
1447
- // popover side goes flat while open so the two shapes read as one.
1448
- let paramsPanelPalette = null;
1449
-
1450
- function initParamsPanel() {
1451
- paramsPanelPalette = barPaletteForTheme(detectPageTheme());
1452
- const P = paramsPanelPalette;
1453
-
1454
- // Single element, always in the DOM. The slide animation is a CSS mask
1455
- // with mask-size growing from 0% to 100% along the bar-facing axis — no
1456
- // display toggle, no opacity toggle, no transform trickery. The mask
1457
- // hides everything initially; as it grows, content is revealed from
1458
- // the bar edge outward.
1459
- paramsPanelEl = document.createElement('div');
1460
- paramsPanelEl.id = PREFIX + '-params-panel';
1461
- Object.assign(paramsPanelEl.style, {
1462
- position: 'fixed', zIndex: String(Z.bar - 1),
1463
- background: P.surfaceDeep,
1464
- color: P.text,
1465
- fontFamily: FONT,
1466
- padding: '14px 18px',
1467
- boxSizing: 'border-box',
1468
- borderRadius: '0 0 10px 10px',
1469
- pointerEvents: 'none',
1470
- backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)',
1471
-
1472
- // clip-path is the same conceptual reveal as mask but with rock-solid
1473
- // transition support across engines. Closed state clips from the far
1474
- // edge; open = inset(0) shows everything.
1475
- clipPath: 'inset(0 0 100% 0)',
1476
- transition: 'clip-path 0.44s ' + EASE,
1477
-
1478
- // Park off-screen until positionParamsPanel places it. These are NOT
1479
- // in the transition list, so they snap instantly — no fly-in from the
1480
- // top-left when first shown.
1481
- top: '-9999px', left: '-9999px', width: '0',
1482
- });
1483
-
1484
- paramsPanelBody = el('div', {
1485
- display: 'grid',
1486
- gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))',
1487
- gap: '12px 16px',
1488
- });
1489
-
1490
- paramsPanelEl.appendChild(paramsPanelBody);
1491
- document.body.appendChild(paramsPanelEl);
1492
- // Don't override pointer-events: the panel toggles between 'none' (closed,
1493
- // click-through) and 'auto' (open) on its own. Just silence the host's
1494
- // outside-interaction listeners while the panel is open.
1495
- defangOutsideHandlers(paramsPanelEl, { setPointerEvents: false });
1496
- paramsPanelInner = paramsPanelEl; // compatibility alias for the rest of the code
1497
- }
1498
-
1499
- function getVisibleVariantEl() {
1500
- if (!currentSessionId) return null;
1501
- const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
1502
- if (!wrapper) return null;
1503
- return wrapper.querySelector('[data-impeccable-variant="' + visibleVariant + '"]');
1504
- }
1505
-
1506
- function parseVariantParams(variantEl) {
1507
- if (!variantEl) return [];
1508
- const raw = variantEl.getAttribute('data-impeccable-params');
1509
- if (!raw) return [];
1510
- try {
1511
- const parsed = JSON.parse(raw);
1512
- return Array.isArray(parsed) ? parsed : [];
1513
- } catch (err) {
1514
- console.warn('[impeccable] Invalid data-impeccable-params JSON:', err.message);
1515
- return [];
1516
- }
1517
- }
1518
-
1519
- function applyParamValue(variantEl, param, value) {
1520
- if (!variantEl) return;
1521
- const attr = 'data-p-' + param.id;
1522
- if (param.kind === 'range') {
1523
- variantEl.style.setProperty('--p-' + param.id, String(value));
1524
- } else if (param.kind === 'toggle') {
1525
- const on = !!value;
1526
- variantEl.style.setProperty('--p-' + param.id, on ? '1' : '0');
1527
- if (on) variantEl.setAttribute(attr, 'on');
1528
- else variantEl.removeAttribute(attr);
1529
- } else if (param.kind === 'steps') {
1530
- variantEl.setAttribute(attr, String(value));
1531
- }
1532
- }
1533
-
1534
- function applyParamDefaults(variantEl, params) {
1535
- paramsCurrentValues = {};
1536
- for (const p of params) {
1537
- paramsCurrentValues[p.id] = p.default;
1538
- applyParamValue(variantEl, p, p.default);
1539
- }
1540
- }
1541
-
1542
- function formatRangeValue(input) {
1543
- const max = parseFloat(input.max), min = parseFloat(input.min);
1544
- const v = parseFloat(input.value);
1545
- if (!isFinite(v)) return input.value;
1546
- return (max - min) <= 2 ? v.toFixed(2) : String(Math.round(v));
1547
- }
1548
-
1549
- function buildParamsPanel(variantEl, params) {
1550
- const P = paramsPanelPalette || barPaletteForTheme(detectPageTheme());
1551
- paramsPanelBody.innerHTML = '';
1552
- for (const p of params) {
1553
- const row = el('div', { display: 'flex', flexDirection: 'column', gap: '6px' });
1554
- const labelRow = el('div', {
1555
- display: 'flex', justifyContent: 'space-between',
1556
- alignItems: 'baseline', gap: '8px',
1557
- });
1558
- const lbl = el('span', {
1559
- fontSize: '10.5px', fontWeight: '600', color: P.text,
1560
- letterSpacing: '0.03em',
1561
- });
1562
- lbl.textContent = p.label || p.id;
1563
- labelRow.appendChild(lbl);
1564
- const readout = el('span', {
1565
- fontSize: '10.5px', color: P.textDim,
1566
- fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
1567
- });
1568
- labelRow.appendChild(readout);
1569
- row.appendChild(labelRow);
1570
-
1571
- if (p.kind === 'range') {
1572
- const input = document.createElement('input');
1573
- input.type = 'range';
1574
- input.min = String(p.min != null ? p.min : 0);
1575
- input.max = String(p.max != null ? p.max : 1);
1576
- input.step = String(p.step != null ? p.step : 0.05);
1577
- input.value = String(p.default);
1578
- Object.assign(input.style, {
1579
- width: '100%', accentColor: C.brand, cursor: 'pointer',
1580
- });
1581
- readout.textContent = formatRangeValue(input);
1582
- input.addEventListener('input', (e) => {
1583
- e.stopPropagation();
1584
- const v = parseFloat(input.value);
1585
- paramsCurrentValues[p.id] = v;
1586
- readout.textContent = formatRangeValue(input);
1587
- applyParamValue(variantEl, p, v);
1588
- });
1589
- row.appendChild(input);
1590
- } else if (p.kind === 'toggle') {
1591
- const initial = !!p.default;
1592
- readout.textContent = initial ? 'On' : 'Off';
1593
- const track = el('button', {
1594
- position: 'relative', width: '36px', height: '20px',
1595
- borderRadius: '10px', border: 'none', padding: '0',
1596
- cursor: 'pointer',
1597
- background: initial ? C.brand : P.hairline,
1598
- transition: 'background 0.15s ease',
1599
- alignSelf: 'flex-start',
1600
- });
1601
- const knob = el('span', {
1602
- position: 'absolute', top: '2px',
1603
- left: initial ? '18px' : '2px',
1604
- width: '16px', height: '16px', borderRadius: '50%',
1605
- background: 'oklch(98% 0 0)',
1606
- transition: 'left 0.18s ' + EASE,
1607
- boxShadow: '0 1px 2px oklch(0% 0 0 / 0.2)',
1608
- });
1609
- track.appendChild(knob);
1610
- track.addEventListener('click', (e) => {
1611
- e.stopPropagation();
1612
- const next = !paramsCurrentValues[p.id];
1613
- paramsCurrentValues[p.id] = next;
1614
- track.style.background = next ? C.brand : P.hairline;
1615
- knob.style.left = next ? '18px' : '2px';
1616
- readout.textContent = next ? 'On' : 'Off';
1617
- applyParamValue(variantEl, p, next);
1618
- });
1619
- row.appendChild(track);
1620
- } else if (p.kind === 'steps') {
1621
- const opts = (p.options || []).map(o =>
1622
- typeof o === 'string' ? { value: o, label: o } : o
1623
- );
1624
- const activeOpt = opts.find(o => o.value === p.default) || opts[0];
1625
- readout.textContent = activeOpt ? activeOpt.label : String(p.default);
1626
- const segRow = el('div', {
1627
- display: 'grid',
1628
- gridTemplateColumns: 'repeat(' + opts.length + ', 1fr)',
1629
- gap: '1px', padding: '2px',
1630
- background: P.hairline, borderRadius: '5px',
1631
- });
1632
- const segBtns = [];
1633
- opts.forEach(o => {
1634
- const active = o.value === p.default;
1635
- const b = el('button', {
1636
- padding: '5px 4px', border: 'none', borderRadius: '3px',
1637
- background: active ? C.brand : 'transparent',
1638
- color: active ? 'oklch(98% 0 0)' : P.text,
1639
- fontFamily: FONT, fontSize: '10.5px', fontWeight: '500',
1640
- cursor: 'pointer', whiteSpace: 'nowrap',
1641
- transition: 'background 0.1s ease, color 0.1s ease',
1642
- });
1643
- b.textContent = o.label;
1644
- b.addEventListener('click', (e) => {
1645
- e.stopPropagation();
1646
- paramsCurrentValues[p.id] = o.value;
1647
- readout.textContent = o.label;
1648
- segBtns.forEach(({ btn, val }) => {
1649
- const on = val === o.value;
1650
- btn.style.background = on ? C.brand : 'transparent';
1651
- btn.style.color = on ? 'oklch(98% 0 0)' : P.text;
1652
- });
1653
- applyParamValue(variantEl, p, o.value);
1654
- });
1655
- segRow.appendChild(b);
1656
- segBtns.push({ btn: b, val: o.value });
1657
- });
1658
- row.appendChild(segRow);
1659
- }
1660
-
1661
- paramsPanelBody.appendChild(row);
1662
- }
1663
- }
1664
-
1665
- // Decide which way the popover opens: away from the picked element. If the
1666
- // bar landed below the element, popover slides DOWN from the bar's bottom.
1667
- // If the bar landed above, popover slides UP from the bar's top.
1668
- function popoverDirection() {
1669
- if (!barEl || !selectedElement) return 'below';
1670
- const br = barEl.getBoundingClientRect();
1671
- const er = selectedElement.getBoundingClientRect();
1672
- return br.top >= er.bottom - 4 ? 'below' : 'above';
1673
- }
1674
-
1675
- // The popover overlaps the bar by OVERLAP px on the bar-facing side. With
1676
- // popover z-index below bar, that overlap sits behind bar (invisible) and
1677
- // reinforces the "tucked behind" feel. Padding compensates so the real
1678
- // content starts flush with bar's outer edge.
1679
- const TUNE_OVERLAP = 6;
1680
-
1681
- // Closed clip-path depends on direction: for 'below' clip from the far
1682
- // (bottom) edge so the reveal grows downward from the bar; for 'above'
1683
- // clip from the top edge so the reveal grows upward from the bar.
1684
- function closedClipPath(direction) {
1685
- return direction === 'below' ? 'inset(0 0 100% 0)' : 'inset(100% 0 0 0)';
1686
- }
1687
-
1688
- function setClipPath(value, withTransition) {
1689
- const saved = paramsPanelEl.style.transition;
1690
- if (!withTransition) paramsPanelEl.style.transition = 'none';
1691
- paramsPanelEl.style.clipPath = value;
1692
- if (!withTransition) {
1693
- void paramsPanelEl.offsetHeight;
1694
- paramsPanelEl.style.transition = saved;
1695
- }
1696
- }
1697
-
1698
- function positionParamsPanel() {
1699
- if (!paramsPanelEl || !barEl || barEl.style.display === 'none') return;
1700
- const br = barEl.getBoundingClientRect();
1701
- const direction = popoverDirection();
1702
- const prevDirection = paramsPanelEl.dataset.tuneDirection;
1703
-
1704
- // top/left/width are NOT in the transition list, so they snap instantly.
1705
- paramsPanelEl.style.left = br.left + 'px';
1706
- paramsPanelEl.style.width = br.width + 'px';
1707
-
1708
- if (direction === 'below') {
1709
- paramsPanelEl.style.top = (br.bottom - TUNE_OVERLAP) + 'px';
1710
- paramsPanelEl.style.borderRadius = '0 0 10px 10px';
1711
- paramsPanelEl.style.paddingTop = (14 + TUNE_OVERLAP) + 'px';
1712
- paramsPanelEl.style.paddingBottom = '14px';
1713
- } else {
1714
- const ih = paramsPanelEl.offsetHeight || 80;
1715
- paramsPanelEl.style.top = (br.top - ih + TUNE_OVERLAP) + 'px';
1716
- paramsPanelEl.style.borderRadius = '10px 10px 0 0';
1717
- paramsPanelEl.style.paddingTop = '14px';
1718
- paramsPanelEl.style.paddingBottom = (14 + TUNE_OVERLAP) + 'px';
1719
- }
1720
- paramsPanelEl.dataset.tuneDirection = direction;
1721
-
1722
- // If currently closed and direction flipped (or first-time setup),
1723
- // snap the clip-path to the new direction's closed pose without
1724
- // transitioning (so the clip doesn't slide across the element).
1725
- if (!tuneOpen && (!prevDirection || prevDirection !== direction)) {
1726
- setClipPath(closedClipPath(direction), false);
1727
- }
1728
- }
1729
-
1730
- function showParamsPanel() {
1731
- if (!paramsPanelEl) return;
1732
- positionParamsPanel();
1733
- paramsPanelEl.style.pointerEvents = 'auto';
1734
- // rAF so the positioning paint commits before the transition fires.
1735
- requestAnimationFrame(() => {
1736
- setClipPath('inset(0 0 0 0)', true);
1737
- });
1738
- }
1739
-
1740
- function hideParamsPanel() {
1741
- if (!paramsPanelEl) return;
1742
- paramsPanelEl.style.pointerEvents = 'none';
1743
- const direction = paramsPanelEl.dataset.tuneDirection || 'below';
1744
- setClipPath(closedClipPath(direction), true);
1745
- }
1746
-
1747
- // Build/rebuild the panel's contents for the current variant AND apply
1748
- // its defaults to the variant wrapper (so scoped CSS responds even before
1749
- // the user opens the popover). Visibility is governed by tuneOpen.
1750
- function refreshParamsPanel() {
1751
- if (state !== 'CYCLING') {
1752
- paramsCurrentValues = {};
1753
- tuneOpen = false;
1754
- hideParamsPanel();
1755
- return;
1756
- }
1757
- const variantEl = getVisibleVariantEl();
1758
- const params = parseVariantParams(variantEl);
1759
- if (!variantEl || params.length === 0) {
1760
- paramsCurrentValues = {};
1761
- tuneOpen = false;
1762
- hideParamsPanel();
1763
- return;
1764
- }
1765
- applyParamDefaults(variantEl, params);
1766
- buildParamsPanel(variantEl, params);
1767
- if (tuneOpen) {
1768
- // If already visible (variant cycled while open), refresh in place
1769
- // instead of re-running the clip-path animation.
1770
- const alreadyVisible = paramsPanelEl.style.display === 'block'
1771
- && paramsPanelEl.style.opacity === '1';
1772
- if (alreadyVisible) positionParamsPanel();
1773
- else showParamsPanel();
1774
- } else {
1775
- hideParamsPanel();
1776
- }
1777
- }
1778
-
1779
- function toggleTunePopover() {
1780
- if (tuneOpen) { closeTunePopover(); return; }
1781
- openTunePopover();
1782
- }
1783
-
1784
- function openTunePopover() {
1785
- if (state !== 'CYCLING') return;
1786
- const variantEl = getVisibleVariantEl();
1787
- const params = parseVariantParams(variantEl);
1788
- if (!variantEl || params.length === 0) return;
1789
- // Build fresh to ensure the current variant's controls are shown.
1790
- applyParamDefaults(variantEl, params);
1791
- buildParamsPanel(variantEl, params);
1792
- tuneOpen = true;
1793
- showParamsPanel();
1794
- // Kill the bar's shadow on the popover-facing side so the dark popover
1795
- // doesn't pick up a bright glow line.
1796
- if (barEl) {
1797
- const direction = paramsPanelEl?.dataset.tuneDirection || 'below';
1798
- barEl.style.boxShadow = direction === 'below' ? BAR_SHADOW_UP : BAR_SHADOW_DOWN;
1799
- }
1800
- // Re-render the bar so the Tune chip picks up the active styling.
1801
- updateBarContent('cycling');
1802
- }
1803
-
1804
- function closeTunePopover() {
1805
- tuneOpen = false;
1806
- hideParamsPanel();
1807
- if (barEl) barEl.style.boxShadow = BAR_SHADOW_DEFAULT;
1808
- if (barEl && barEl.style.display !== 'none' && state === 'CYCLING') {
1809
- updateBarContent('cycling');
1810
- }
1811
- }
1812
-
1813
- // ---------------------------------------------------------------------------
1814
- // Variant cycling in DOM
1815
- // ---------------------------------------------------------------------------
1816
-
1817
- function showVariantInDOM(sessionId, num) {
1818
- const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
1819
- if (!wrapper) return;
1820
- for (const child of wrapper.children) {
1821
- const v = child.dataset ? child.dataset.impeccableVariant : null;
1822
- if (!v) continue;
1823
- child.style.display = (v === String(num)) ? '' : 'none';
1824
- }
1825
- // Unconditional refresh — covers first-reveal (no-op if state isn't
1826
- // CYCLING yet, the subsequent CYCLING transition triggers its own
1827
- // refresh) and every cycle step.
1828
- refreshParamsPanel();
1829
- }
1830
-
1831
- /**
1832
- * No-HMR fallback: fetch the raw source file from the live server,
1833
- * parse it, extract the variant wrapper, and inject it into the live DOM.
1834
- * This works even when the dev server caches HTML (Bun, static servers).
1835
- */
1836
- function injectVariantsFromSource(filePath, sessionId) {
1837
- const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath);
1838
- fetch(url)
1839
- .then(r => { if (!r.ok) throw new Error(r.status); return r.text(); })
1840
- .then(html => {
1841
- // Parse the raw source HTML
1842
- const parser = new DOMParser();
1843
- const doc = parser.parseFromString(html, 'text/html');
1844
- const srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]');
1845
- if (!srcWrapper) {
1846
- console.error('[impeccable] Variant wrapper not found in source file.');
1847
- return;
1848
- }
1849
-
1850
- // Find the original element in the live DOM.
1851
- // The original is inside the wrapper in the source. We find the
1852
- // corresponding element in the live DOM by matching the first child's
1853
- // tag + classes from the original snapshot.
1854
- const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
1855
- if (!origContent) return;
1856
-
1857
- const tag = origContent.tagName.toLowerCase();
1858
- const cls = origContent.className;
1859
- let liveEl = null;
1860
- if (origContent.id) {
1861
- liveEl = document.getElementById(origContent.id);
1862
- } else if (cls) {
1863
- // Find by tag + exact class match
1864
- const candidates = document.querySelectorAll(tag + '.' + cls.split(' ')[0]);
1865
- for (const c of candidates) {
1866
- if (c.className === cls && !own(c)) { liveEl = c; break; }
1867
- }
1868
- }
1869
-
1870
- if (!liveEl) {
1871
- console.error('[impeccable] Could not find original element in live DOM.');
1872
- return;
1873
- }
1874
-
1875
- // Replace the live element with the full wrapper from source
1876
- const wrapper = srcWrapper.cloneNode(true);
1877
- liveEl.parentElement.replaceChild(wrapper, liveEl);
1878
-
1879
- // Update state: count variants, show the first one
1880
- const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
1881
- arrivedVariants = variants.length;
1882
- expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants);
1883
- visibleVariant = 1;
1884
- showVariantInDOM(sessionId, 1);
1885
-
1886
- // Update selectedElement to the visible variant's content
1887
- selectedElement = pickVariantContent(wrapper, 1) || wrapper.parentElement;
1888
-
1889
- state = 'CYCLING';
1890
- hideShaderOverlay();
1891
- updateBarContent('cycling');
1892
- refreshParamsPanel();
1893
- saveSession();
1894
- console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.');
1895
- })
1896
- .catch(err => {
1897
- console.error('[impeccable] Failed to fetch source:', err);
1898
- showToast('Could not load variants. Try refreshing the page.', 5000);
1899
- });
1900
- }
1901
-
1902
- function cycleVariant(dir) {
1903
- const next = visibleVariant + dir;
1904
- if (next < 1 || next > arrivedVariants) return;
1905
- visibleVariant = next;
1906
- showVariantInDOM(currentSessionId, next); // calls refreshParamsPanel itself
1907
- updateSelectedElement();
1908
- updateBarContent('cycling');
1909
- saveSession();
1910
- }
1911
-
1912
- function updateSelectedElement() {
1913
- if (!currentSessionId) return;
1914
- const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
1915
- if (!wrapper) return;
1916
- const visEl = pickVariantContent(wrapper, visibleVariant);
1917
- if (visEl) selectedElement = visEl;
1918
- }
1919
-
1920
- // Resolve the element that represents the variant's visible content.
1921
- // Contract: each variant div should contain exactly one top-level element
1922
- // (the full replacement). In practice a model may ship loose siblings or
1923
- // lead with <style>/<script>. Be defensive: skip non-visual elements, and
1924
- // if the variant has multiple element children, use the variant div itself
1925
- // (it wraps all of them and gets correct bounds).
1926
- function pickVariantContent(wrapper, index) {
1927
- if (!wrapper) return null;
1928
- const variantDiv = wrapper.querySelector('[data-impeccable-variant="' + index + '"]');
1929
- if (!variantDiv) return null;
1930
- const NON_VISUAL = new Set(['STYLE', 'SCRIPT', 'LINK', 'META', 'TEMPLATE']);
1931
- const visual = [];
1932
- for (const child of variantDiv.children) {
1933
- if (!NON_VISUAL.has(child.tagName)) visual.push(child);
1934
- }
1935
- if (visual.length === 1) return visual[0];
1936
- return variantDiv;
1937
- }
1938
-
1939
- // Hold window.scrollY at a fixed value across DOM mutations inside the
1940
- // session's wrapper (HMR patches, variant inserts, cycle swaps).
1941
- function startScrollLock(sessionId, initialTargetY) {
1942
- stopScrollLock();
1943
- scrollLockTargetY = typeof initialTargetY === 'number' && isFinite(initialTargetY)
1944
- ? initialTargetY
1945
- : window.scrollY;
1946
- console.log('[impeccable.scroll] startScrollLock', { sessionId, scrollY: window.scrollY, targetY: scrollLockTargetY, initialOverride: initialTargetY });
1947
-
1948
- try { history.scrollRestoration = 'manual'; } catch {}
1949
-
1950
- const prevHtmlAnchor = document.documentElement.style.overflowAnchor;
1951
- const prevBodyAnchor = document.body.style.overflowAnchor;
1952
- document.documentElement.style.overflowAnchor = 'none';
1953
- document.body.style.overflowAnchor = 'none';
1954
-
1955
- const correct = (why) => {
1956
- scrollLockRaf = null;
1957
- if (scrollLockTargetY == null) return;
1958
- const before = window.scrollY;
1959
- const delta = before - scrollLockTargetY;
1960
- if (Math.abs(delta) < 0.5) {
1961
- console.log('[impeccable.scroll] correct noop', { why, scrollY: before, targetY: scrollLockTargetY });
1962
- return;
1963
- }
1964
- window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
1965
- console.log('[impeccable.scroll] corrected', { why, from: before, to: scrollLockTargetY, delta, nowAt: window.scrollY });
1966
- };
1967
- const schedule = (why) => {
1968
- if (scrollLockRaf != null) return;
1969
- scrollLockRaf = requestAnimationFrame(() => correct(why));
1970
- };
1971
-
1972
- scrollLockObserver = new MutationObserver((mutations) => {
1973
- for (const m of mutations) {
1974
- if (m.target?.closest?.('[data-impeccable-variants="' + sessionId + '"]')) {
1975
- const childAdds = Array.from(m.addedNodes).map(n => n.nodeType === 1 ? (n.tagName + (n.dataset?.impeccableVariant ? ('[variant=' + n.dataset.impeccableVariant + ']') : '')) : n.nodeType).join(',');
1976
- console.log('[impeccable.scroll] mutation inside wrapper', { type: m.type, target: m.target?.tagName, adds: childAdds, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
1977
- schedule('mutation-in-wrapper');
1978
- return;
1979
- }
1980
- for (const n of m.addedNodes) {
1981
- if (n.nodeType === 1 && (n.matches?.('[data-impeccable-variants="' + sessionId + '"]') || n.querySelector?.('[data-impeccable-variants="' + sessionId + '"]'))) {
1982
- console.log('[impeccable.scroll] wrapper node added', { tag: n.tagName, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
1983
- schedule('wrapper-added');
1984
- return;
1985
- }
1986
- }
1987
- }
1988
- });
1989
- scrollLockObserver.observe(document.body, { childList: true, subtree: true });
1990
-
1991
- scrollLockAbort = new AbortController();
1992
- scrollLockAbort.signal.addEventListener('abort', () => {
1993
- document.documentElement.style.overflowAnchor = prevHtmlAnchor;
1994
- document.body.style.overflowAnchor = prevBodyAnchor;
1995
- }, { once: true });
1996
- const sig = { signal: scrollLockAbort.signal };
1997
- // Track whether the most recent scroll came from a user gesture. We
1998
- // gate user-scroll re-anchoring on this flag so programmatic smooth
1999
- // scrolls (browser reload-restore, scrollIntoView from other scripts)
2000
- // don't accidentally update our target.
2001
- let userGestureAt = 0;
2002
- const USER_GESTURE_WINDOW_MS = 250;
2003
-
2004
- const reanchor = (why) => {
2005
- if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
2006
- const prevTarget = scrollLockTargetY;
2007
- scrollLockTargetY = window.scrollY;
2008
- writeScrollY(scrollLockTargetY);
2009
- console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
2010
- };
2011
- const markGesture = (why) => {
2012
- userGestureAt = performance.now();
2013
- reanchor(why);
2014
- };
2015
- window.addEventListener('wheel', () => markGesture('wheel'), { passive: true, ...sig });
2016
- window.addEventListener('touchstart', () => markGesture('touchstart'), { passive: true, ...sig });
2017
- window.addEventListener('touchmove', () => markGesture('touchmove'), { passive: true, ...sig });
2018
- window.addEventListener('keydown', (e) => {
2019
- if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) markGesture('key:' + e.key);
2020
- }, sig);
2021
-
2022
- // Correct on EVERY scroll event: whether it's the browser's
2023
- // post-reload animated restore or some other script calling
2024
- // scrollIntoView, we want to snap back immediately. Only skip if a
2025
- // user gesture fired in the last 250ms.
2026
- let lastLoggedScrollY = window.scrollY;
2027
- window.addEventListener('scroll', () => {
2028
- const now = window.scrollY;
2029
- if (Math.abs(now - lastLoggedScrollY) > 5) {
2030
- console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
2031
- lastLoggedScrollY = now;
2032
- }
2033
- if (scrollLockTargetY == null) return;
2034
- if (performance.now() - userGestureAt < USER_GESTURE_WINDOW_MS) return;
2035
- if (Math.abs(now - scrollLockTargetY) < 0.5) return;
2036
- console.log('[impeccable.scroll] scroll-event snap', { from: now, to: scrollLockTargetY });
2037
- window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
2038
- }, { passive: true, ...sig });
2039
-
2040
- // Apply target synchronously, not via rAF — racing the browser's
2041
- // restore or a smooth-scroll animation means we want to win now.
2042
- if (Math.abs(window.scrollY - scrollLockTargetY) > 0.5) {
2043
- window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
2044
- console.log('[impeccable.scroll] startScrollLock initial apply', { to: scrollLockTargetY });
2045
- }
2046
- }
2047
-
2048
- function stopScrollLock() {
2049
- if (scrollLockObserver) { scrollLockObserver.disconnect(); scrollLockObserver = null; }
2050
- if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
2051
- if (scrollLockAbort) { scrollLockAbort.abort(); scrollLockAbort = null; }
2052
- scrollLockTargetY = null;
2053
- // NOTE: do NOT clear the persistent scroll key here. startScrollLock
2054
- // calls us as a reset, and clearing the key would nuke the Go-time
2055
- // scrollY that the next resume needs to read.
2056
- }
2057
-
2058
- // ---------------------------------------------------------------------------
2059
- // MutationObserver for progressive variant reveal
2060
- // ---------------------------------------------------------------------------
2061
-
2062
- function startVariantObserver(sessionId) {
2063
- let updating = false; // re-entrancy guard
2064
-
2065
- const obs = new MutationObserver((mutations) => {
2066
- if (updating) return;
2067
-
2068
- // Only react to mutations that add nodes with data-impeccable-variant,
2069
- // or mutations inside the variant wrapper. Ignore our own bar/UI changes.
2070
- let dominated = false;
2071
- for (const m of mutations) {
2072
- if (m.target.closest?.('[data-impeccable-variants]')) { dominated = true; break; }
2073
- for (const n of m.addedNodes) {
2074
- if (n.nodeType !== 1) continue;
2075
- // Direct hit: the added node itself is the wrapper or a variant.
2076
- if (n.dataset?.impeccableVariants || n.dataset?.impeccableVariant) {
2077
- dominated = true; break;
2078
- }
2079
- // Subtree hit: framework HMR (notably SvelteKit) sometimes replaces
2080
- // a whole subtree where the wrapper is a descendant of the added
2081
- // node. Without this check, the observer ignores those mutations
2082
- // and the session stays in GENERATING forever.
2083
- if (n.querySelector?.('[data-impeccable-variants],[data-impeccable-variant]')) {
2084
- dominated = true; break;
2085
- }
2086
- }
2087
- if (dominated) break;
2088
- }
2089
- if (!dominated) return;
2090
-
2091
- const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
2092
- if (!wrapper) return;
2093
-
2094
- // Re-anchor selectedElement if it was detached by live-wrap's HMR swap.
2095
- // Without this, the shader / highlight / bar track a zero-rect phantom
2096
- // and the overlay appears frozen.
2097
- if (selectedElement && !document.body.contains(selectedElement)) {
2098
- selectedElement = pickVariantContent(wrapper, 'original') || wrapper;
2099
- }
2100
-
2101
- const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
2102
- const count = variants.length;
2103
-
2104
- // Nothing new
2105
- if (count <= arrivedVariants) return;
2106
-
2107
- updating = true;
2108
- arrivedVariants = count;
2109
- if (visibleVariant === 0 && arrivedVariants > 0) {
2110
- visibleVariant = 1;
2111
- showVariantInDOM(sessionId, 1);
2112
- // showVariantInDOM hid the original (display:none); if we were still
2113
- // anchored to the original's content, its boundingRect is now zero
2114
- // and the bar snaps to (0,0). Re-point at the visible variant instead.
2115
- const visEl = pickVariantContent(wrapper, visibleVariant);
2116
- if (visEl) selectedElement = visEl;
2117
- }
2118
-
2119
- const expected = parseInt(wrapper.dataset.impeccableVariantCount || '0');
2120
- if (expected > 0) expectedVariants = expected;
2121
-
2122
- if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
2123
- state = 'CYCLING';
2124
- hideShaderOverlay();
2125
- updateBarContent('cycling');
2126
- refreshParamsPanel();
2127
- } else if (state === 'GENERATING') {
2128
- updateBarContent('generating');
2129
- }
2130
- saveSession();
2131
- updating = false;
2132
- });
2133
-
2134
- obs.observe(document.body, { childList: true, subtree: true });
2135
- return obs;
2136
- }
2137
-
2138
- // ---------------------------------------------------------------------------
2139
- // Bar scroll tracking
2140
- // ---------------------------------------------------------------------------
2141
-
2142
- function startScrollTracking() {
2143
- function tick() {
2144
- if (state === 'CONFIGURING' || state === 'GENERATING' || state === 'CYCLING') {
2145
- positionBar();
2146
- showHighlight(selectedElement);
2147
- if (tuneOpen) positionParamsPanel();
2148
- }
2149
- if (annotActive) positionAnnotOverlay(selectedElement);
2150
- // Shader overlay (via debug P toggle or generation) is repositioned
2151
- // by its own branch below; debug no longer has a separate overlay.
2152
- if (shaderState) positionShaderOverlay();
2153
- scrollRaf = requestAnimationFrame(tick);
2154
- }
2155
- scrollRaf = requestAnimationFrame(tick);
2156
- }
2157
-
2158
- function stopScrollTracking() {
2159
- if (scrollRaf) { cancelAnimationFrame(scrollRaf); scrollRaf = null; }
2160
- }
2161
-
2162
- // ---------------------------------------------------------------------------
2163
- // SSE (server→browser) + fetch POST (browser→server)
2164
- // Zero-dependency replacement for WebSocket.
2165
- // ---------------------------------------------------------------------------
2166
-
2167
- let evtSource = null;
2168
- let sseRetries = 0;
2169
- const SSE_MAX_RETRIES = 20; // generous: heartbeats keep the connection alive, so retries mean real trouble
2170
-
2171
- function connectSSE() {
2172
- evtSource = new EventSource('http://localhost:' + PORT + '/events?token=' + TOKEN);
2173
-
2174
- evtSource.onopen = () => {
2175
- sseRetries = 0; // reset on successful (re)connect
2176
- };
2177
-
2178
- evtSource.onmessage = (e) => {
2179
- sseRetries = 0; // reset on any successful message
2180
- let msg; try { msg = JSON.parse(e.data); } catch { return; }
2181
- switch (msg.type) {
2182
- case 'connected':
2183
- hasProjectContext = !!msg.hasProjectContext;
2184
- if (!hasProjectContext) showToast('No PRODUCT.md found. Variants will be brand-agnostic. Run /impeccable teach to generate one.', 7000);
2185
- console.log('[impeccable] Live mode connected.');
2186
- if (state === 'IDLE') state = 'PICKING';
2187
- break;
2188
- case 'done':
2189
- // Variants already arrived via HMR → normal transition.
2190
- if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
2191
- if (state === 'GENERATING') {
2192
- state = 'CYCLING';
2193
- updateBarContent('cycling');
2194
- refreshParamsPanel();
2195
- }
2196
- break;
2197
- }
2198
- // Variants are in source but not in the DOM yet. Common when the
2199
- // picked element lived inside conditional render (closed modal,
2200
- // hidden tab, a route the user navigated away from). The variant
2201
- // MutationObserver stays armed and auto-transitions to CYCLING
2202
- // the moment the wrapper actually mounts. Nudge the user toward
2203
- // that path with a toast — better than the prior force-reload
2204
- // which reset framework state and left the session stuck.
2205
- setTimeout(() => {
2206
- if (arrivedVariants >= expectedVariants && expectedVariants > 0) return;
2207
- if (state !== 'GENERATING') return;
2208
- showToast(
2209
- "Variants ready. If the picked element isn't visible, retrace the path that revealed it — they'll appear automatically.",
2210
- 15000,
2211
- );
2212
- }, 2000);
2213
- break;
2214
- case 'error':
2215
- console.error('[impeccable] Error:', msg.message);
2216
- showToast('Error: ' + msg.message, 5000);
2217
- hideBar();
2218
- state = 'PICKING';
2219
- break;
2220
- }
2221
- };
2222
-
2223
- evtSource.onerror = () => {
2224
- sseRetries++;
2225
- if (sseRetries <= SSE_MAX_RETRIES) {
2226
- console.log('[impeccable] SSE connection lost. Retry ' + sseRetries + '/' + SSE_MAX_RETRIES + '...');
2227
- return; // EventSource auto-reconnects
2228
- }
2229
- // Server is gone. Clean up gracefully.
2230
- console.log('[impeccable] Live server unreachable. Cleaning up UI.');
2231
- evtSource.close();
2232
- evtSource = null;
2233
- handleServerLost();
2234
- };
2235
- }
2236
-
2237
- /** Server died or became unreachable. Reset UI to a clean state. */
2238
- function handleServerLost() {
2239
- if (state === 'GENERATING' || state === 'CYCLING' || state === 'SAVING') {
2240
- showToast('Live server disconnected. Session ended.', 5000);
2241
- }
2242
- hideBar();
2243
- hideHighlight();
2244
- hideShaderOverlay();
2245
- hideAnnotOverlay();
2246
- stopScrollTracking();
2247
- if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
2248
- stopScrollLock();
2249
- clearScrollY();
2250
- clearSession();
2251
- selectedElement = null;
2252
- currentSessionId = null;
2253
- selectedAction = 'impeccable';
2254
- state = 'IDLE';
2255
- }
2256
-
2257
- function sendEvent(msg) {
2258
- msg.token = TOKEN;
2259
- fetch('http://localhost:' + PORT + '/events', {
2260
- method: 'POST',
2261
- headers: { 'Content-Type': 'application/json' },
2262
- body: JSON.stringify(msg),
2263
- }).catch(err => console.error('[impeccable] Failed to send event:', err));
2264
- }
2265
-
2266
- // ---------------------------------------------------------------------------
2267
- // Event handlers
2268
- // ---------------------------------------------------------------------------
2269
-
2270
- function handleMouseMove(e) {
2271
- if (state !== 'PICKING' || !pickActive) return;
2272
- const target = document.elementFromPoint(e.clientX, e.clientY);
2273
- if (!target || !pickable(target) || target === hoveredElement) return;
2274
- hoveredElement = target;
2275
- showHighlight(target);
2276
- }
2277
-
2278
- function handleClick(e) {
2279
- // Close action picker on any outside click
2280
- if (pickerEl?.style.display !== 'none' && !own(e.target)) {
2281
- hideActionPicker();
2282
- }
2283
- // Close Tune popover on outside click (anything outside panel + bar)
2284
- if (tuneOpen && paramsPanelEl && !paramsPanelEl.contains(e.target) && barEl && !barEl.contains(e.target)) {
2285
- closeTunePopover();
2286
- }
2287
- // In CONFIGURING: click outside the bar and selected element returns to PICKING
2288
- if (state === 'CONFIGURING' && !own(e.target) && selectedElement && !selectedElement.contains(e.target)) {
2289
- hideBar();
2290
- stopScrollTracking();
2291
- hideAnnotOverlay();
2292
- clearAnnotations();
2293
- state = 'PICKING';
2294
- hoveredElement = null;
2295
- hideHighlight();
2296
- return;
2297
- }
2298
- if (state !== 'PICKING' || !pickActive) return;
2299
- if (own(e.target)) return;
2300
- if (!hoveredElement || !pickable(hoveredElement)) return;
2301
- e.preventDefault();
2302
- e.stopPropagation();
2303
- selectedElement = hoveredElement;
2304
- state = 'CONFIGURING';
2305
- showHighlight(selectedElement);
2306
- clearAnnotations();
2307
- showAnnotOverlay(selectedElement);
2308
- showBar('configure');
2309
- startScrollTracking();
2310
- maybePrefetchPage();
2311
- maybeWarnConditionalAncestor(selectedElement);
2312
- }
2313
-
2314
- /**
2315
- * Surface a brief, non-blocking heads-up when the picked element lives
2316
- * inside a container whose visibility is gated by ephemeral state — modals,
2317
- * collapsible panels, popovers, off-screen tab panels. If HMR remounts the
2318
- * parent during generation (Vite Fast Refresh, SvelteKit page reload), the
2319
- * variants land in source but stay invisible until the user re-opens the
2320
- * container. Telling the user upfront is much friendlier than the silent
2321
- * timeout-then-toast that they'd otherwise hit.
2322
- *
2323
- * Heuristic, intentionally narrow — only fires for unambiguous cases so
2324
- * we don't cry wolf on every nested element.
2325
- */
2326
- function maybeWarnConditionalAncestor(el) {
2327
- let node = el?.parentElement;
2328
- let depth = 0;
2329
- while (node && depth < 12) {
2330
- // 1. Active dialog / modal
2331
- if (node.getAttribute && node.getAttribute('role') === 'dialog'
2332
- && node.getAttribute('aria-modal') === 'true') {
2333
- showToast('Heads up: this element lives inside a dialog. If state resets during generation, you may need to re-open it.', 6000);
2334
- return;
2335
- }
2336
- // 2. Common Radix / shadcn / headless-ui open-state attribute
2337
- if (node.dataset && node.dataset.state === 'open') {
2338
- showToast('Heads up: this element lives inside an open panel. If state resets during generation, you may need to re-open it.', 6000);
2339
- return;
2340
- }
2341
- // 3. Tab panel — only meaningful when the page also shows ANOTHER
2342
- // tab as selected. A single tabpanel with no tablist is just a static
2343
- // section in disguise and isn't conditional.
2344
- if (node.getAttribute && node.getAttribute('role') === 'tabpanel') {
2345
- const list = document.querySelector('[role="tablist"]');
2346
- if (list) {
2347
- const tabs = list.querySelectorAll('[role="tab"]');
2348
- if (tabs.length > 1) {
2349
- showToast('Heads up: this element lives in a tab panel. If state resets during generation, switch back to this tab.', 6000);
2350
- return;
2351
- }
2352
- }
2353
- }
2354
- // 4. Collapsible: aria-expanded sibling. Look for the trigger button.
2355
- if (node.id) {
2356
- const trigger = document.querySelector(`[aria-controls="${CSS.escape(node.id)}"][aria-expanded="true"]`);
2357
- if (trigger) {
2358
- showToast('Heads up: this element lives inside an expandable section. If state resets during generation, re-expand it.', 6000);
2359
- return;
2360
- }
2361
- }
2362
- node = node.parentElement;
2363
- depth++;
2364
- }
2365
- }
2366
-
2367
- // Fire a lightweight prefetch event the first time the user selects an
2368
- // element on a given route. The agent uses this to Read the underlying file
2369
- // into context before Go is hit, shaving the read off the critical path.
2370
- // Dedupe per session by pathname — clicking around on the same page doesn't
2371
- // re-fire.
2372
- //
2373
- // DISABLED: quick-Go workflows pay an extra harness round trip because
2374
- // prefetch + generate arrive as two events instead of one. Re-enable with
2375
- // a browser-side debounce (~800–1000ms, cancelled on Go) if we want to
2376
- // resurrect this. Server validator and skill dispatch remain in place so
2377
- // flipping this flag is the only change needed.
2378
- const PREFETCH_ENABLED = false;
2379
- const prefetchedPaths = new Set();
2380
- function maybePrefetchPage() {
2381
- if (!PREFETCH_ENABLED) return;
2382
- const path = location.pathname;
2383
- if (prefetchedPaths.has(path)) return;
2384
- prefetchedPaths.add(path);
2385
- sendEvent({ type: 'prefetch', pageUrl: path });
2386
- }
2387
-
2388
- function handleKeyDown(e) {
2389
- // When the annotation input is focused, let it handle its own keys.
2390
- if (annotEditing && annotEditing.input && e.target === annotEditing.input) return;
2391
- if (e.key === 'Escape') {
2392
- e.preventDefault();
2393
- if (pickerEl?.style.display !== 'none') { hideActionPicker(); return; }
2394
- if (state === 'CONFIGURING') { hideBar(); stopScrollTracking(); hideAnnotOverlay(); clearAnnotations(); state = 'PICKING'; return; }
2395
- if (state === 'CYCLING') { handleDiscard(); return; }
2396
- if (state === 'SAVING' || state === 'CONFIRMED') return; // don't interrupt
2397
- if (state === 'PICKING') {
2398
- // Use togglePick so the "Pick" button in the global bar also flips
2399
- // off, otherwise the bar stays lit while nothing else is active.
2400
- if (pickActive) togglePick();
2401
- else { hideHighlight(); state = 'IDLE'; }
2402
- return;
2403
- }
2404
- }
2405
-
2406
- // Arrow/Enter nav works in PICKING (hover) and CONFIGURING (selected, input empty)
2407
- var navEl = (state === 'PICKING') ? hoveredElement : (state === 'CONFIGURING') ? selectedElement : null;
2408
- if (navEl && (e.key === 'ArrowUp' || e.key === 'ArrowDown' || (e.key === 'Enter' && state === 'PICKING'))) {
2409
- let next = null;
2410
- if (e.key === 'ArrowDown' && !e.shiftKey) {
2411
- next = navEl.nextElementSibling;
2412
- while (next && !pickable(next)) next = next.nextElementSibling;
2413
- } else if (e.key === 'ArrowUp' && !e.shiftKey) {
2414
- next = navEl.previousElementSibling;
2415
- while (next && !pickable(next)) next = next.previousElementSibling;
2416
- } else if (e.key === 'ArrowUp' && e.shiftKey) {
2417
- next = navEl.parentElement;
2418
- if (next && !pickable(next)) next = null;
2419
- } else if (e.key === 'ArrowDown' && e.shiftKey) {
2420
- next = navEl.firstElementChild;
2421
- while (next && !pickable(next)) next = next.nextElementSibling;
2422
- } else if (e.key === 'Enter') {
2423
- e.preventDefault();
2424
- selectedElement = hoveredElement;
2425
- state = 'CONFIGURING';
2426
- showHighlight(selectedElement);
2427
- clearAnnotations();
2428
- showAnnotOverlay(selectedElement);
2429
- showBar('configure');
2430
- startScrollTracking();
2431
- return;
2432
- }
2433
- if (next) {
2434
- e.preventDefault();
2435
- if (state === 'PICKING') {
2436
- hoveredElement = next;
2437
- } else {
2438
- // CONFIGURING: re-select the new element and refresh the bar
2439
- selectedElement = next;
2440
- clearAnnotations();
2441
- showAnnotOverlay(next);
2442
- showBar('configure');
2443
- startScrollTracking();
2444
- }
2445
- showHighlight(next);
2446
- next.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
2447
- }
2448
- return;
2449
- }
2450
-
2451
- if (state === 'CYCLING') {
2452
- if (e.key === 'ArrowLeft') { e.preventDefault(); cycleVariant(-1); }
2453
- if (e.key === 'ArrowRight') { e.preventDefault(); cycleVariant(1); }
2454
- if (e.key === 'Enter') { e.preventDefault(); handleAccept(); }
2455
- }
2456
- }
2457
-
2458
- function handleGo() {
2459
- if (!selectedElement || state !== 'CONFIGURING') return;
2460
- const input = document.getElementById(PREFIX + '-input');
2461
- const prompt = input ? input.value.trim() : '';
2462
-
2463
- // Commit any pending pin edit BEFORE we snapshot annotations.
2464
- if (annotEditing) finalizeEditingPin();
2465
-
2466
- currentSessionId = id8();
2467
- expectedVariants = selectedCount;
2468
- arrivedVariants = 0;
2469
- visibleVariant = 0;
2470
-
2471
- // Flip to GENERATING immediately so the bar morphs without waiting on
2472
- // capture + upload. The event is emitted from captureAndEmit() once the
2473
- // screenshot is uploaded (or capture fails — we still emit, just without
2474
- // screenshotPath).
2475
- const elForCapture = selectedElement;
2476
- const captureRect = elForCapture.getBoundingClientRect();
2477
- const snapshot = {
2478
- comments: annotState.comments.map(c => ({ x: c.x, y: c.y, text: c.text })),
2479
- strokes: annotState.strokes.map(s => ({ points: s.points.map(p => [p[0], p[1]]) })),
2480
- };
2481
- const basePayload = {
2482
- type: 'generate', id: currentSessionId,
2483
- action: selectedAction,
2484
- freeformPrompt: prompt || undefined,
2485
- count: selectedCount,
2486
- pageUrl: location.pathname,
2487
- element: extractContext(elForCapture),
2488
- };
2489
- if (snapshot.comments.length > 0) basePayload.comments = snapshot.comments;
2490
- if (snapshot.strokes.length > 0) basePayload.strokes = snapshot.strokes;
2491
-
2492
- // Hide the interactive overlay so it doesn't linger during generation.
2493
- hideAnnotOverlay();
2494
- clearAnnotations();
2495
-
2496
- state = 'GENERATING';
2497
- showBar('generating');
2498
- saveSession();
2499
- writeScrollY(window.scrollY);
2500
- if (variantObserver) variantObserver.disconnect();
2501
- variantObserver = startVariantObserver(currentSessionId);
2502
- console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
2503
- startScrollLock(currentSessionId);
2504
-
2505
- captureAndEmit(elForCapture, basePayload, snapshot, captureRect);
2506
- }
2507
-
2508
- // ---------------------------------------------------------------------------
2509
- // Screenshot capture + upload
2510
- // ---------------------------------------------------------------------------
2511
-
2512
- let msLoadPromise = null;
2513
- function loadModernScreenshot() {
2514
- if (window.modernScreenshot) return Promise.resolve(window.modernScreenshot);
2515
- if (msLoadPromise) return msLoadPromise;
2516
- msLoadPromise = new Promise((resolve, reject) => {
2517
- const s = document.createElement('script');
2518
- s.src = 'http://localhost:' + PORT + '/modern-screenshot.js';
2519
- s.onload = () => resolve(window.modernScreenshot);
2520
- s.onerror = () => { msLoadPromise = null; reject(new Error('modern-screenshot failed to load')); };
2521
- document.head.appendChild(s);
2522
- });
2523
- return msLoadPromise;
2524
- }
2525
-
2526
- // Collect @font-face rules from every stylesheet on the page. Cross-origin
2527
- // sheets (Google Fonts, Typekit, etc.) throw SecurityError on .cssRules
2528
- // access, so modern-screenshot can't embed them on its own — the resulting
2529
- // SVG falls back to system fonts and text re-wraps + renders with different
2530
- // weight. We fetch the raw CSS text (CORS-permitted for these providers),
2531
- // extract @font-face blocks, inline the referenced font files as base64
2532
- // data URIs (SVGs rasterized via canvas can't fetch external resources,
2533
- // so URLs inside the SVG silently fail without this), and pass the result
2534
- // to modern-screenshot as font.cssText.
2535
- const FONT_EXT_RE = /\.(woff2?|ttf|otf|eot)(\?.*)?$/i;
2536
- const FONT_MIME = {
2537
- woff2: 'font/woff2', woff: 'font/woff', ttf: 'font/ttf', otf: 'font/otf', eot: 'application/vnd.ms-fontobject',
2538
- };
2539
- function bufferToBase64(buf) {
2540
- const bytes = new Uint8Array(buf);
2541
- let binary = '';
2542
- const CHUNK = 0x8000;
2543
- for (let i = 0; i < bytes.length; i += CHUNK) {
2544
- binary += String.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK));
2545
- }
2546
- return btoa(binary);
2547
- }
2548
- async function inlineFontUrls(cssText) {
2549
- const urlRe = /url\((['"]?)(https?:\/\/[^'")\s]+)\1\)/g;
2550
- const urls = new Set();
2551
- let m;
2552
- while ((m = urlRe.exec(cssText))) {
2553
- if (FONT_EXT_RE.test(m[2])) urls.add(m[2]);
2554
- }
2555
- const map = new Map();
2556
- await Promise.all([...urls].map(async (url) => {
2557
- try {
2558
- const res = await fetch(url);
2559
- if (!res.ok) return;
2560
- const buf = await res.arrayBuffer();
2561
- const ext = url.toLowerCase().match(FONT_EXT_RE)?.[1] || 'woff2';
2562
- const mime = FONT_MIME[ext] || 'application/octet-stream';
2563
- map.set(url, 'data:' + mime + ';base64,' + bufferToBase64(buf));
2564
- } catch { /* skip; fall through to URL */ }
2565
- }));
2566
- return cssText.replace(urlRe, (orig, q, url) => {
2567
- const data = map.get(url);
2568
- return data ? 'url(' + q + data + q + ')' : orig;
2569
- });
2570
- }
2571
- async function collectFontCssText() {
2572
- const chunks = [];
2573
- const fontFaceRe = /@font-face\s*\{[^}]*\}/g;
2574
- for (const sheet of document.styleSheets) {
2575
- try {
2576
- const rules = sheet.cssRules;
2577
- for (const rule of rules) {
2578
- if (rule.constructor.name === 'CSSFontFaceRule' || rule.cssText?.startsWith('@font-face')) {
2579
- chunks.push(rule.cssText);
2580
- }
2581
- }
2582
- } catch {
2583
- if (!sheet.href) continue;
2584
- try {
2585
- const res = await fetch(sheet.href);
2586
- if (!res.ok) continue;
2587
- const text = await res.text();
2588
- let m2;
2589
- while ((m2 = fontFaceRe.exec(text))) chunks.push(m2[0]);
2590
- } catch { /* ignore; capture is best-effort */ }
2591
- }
2592
- }
2593
- if (chunks.length === 0) return '';
2594
- return inlineFontUrls(chunks.join('\n'));
2595
- }
2596
-
2597
- // True if `s` is a computed color string that renders as nothing
2598
- // (explicit `transparent`, or `rgba(...)` with alpha 0).
2599
- function isTransparentColor(s) {
2600
- if (!s) return true;
2601
- if (s === 'transparent') return true;
2602
- const m = /rgba?\(([^)]+)\)/.exec(s);
2603
- if (!m) return false;
2604
- const parts = m[1].split(',').map((p) => p.trim());
2605
- if (parts.length === 4) return parseFloat(parts[3]) === 0;
2606
- return false;
2607
- }
2608
-
2609
- // modern-screenshot force-sets `background-color: X !important` on the
2610
- // cloned root whenever `backgroundColor` is passed, clobbering the
2611
- // element's own background. So we only pass it when the element is
2612
- // genuinely transparent (no own color, no own image) — in that case
2613
- // we resolve up the DOM to the nearest opaque ancestor so the capture
2614
- // sits on the page's real background instead of rendering black.
2615
- function resolveCanvasBackground(el) {
2616
- const own = getComputedStyle(el);
2617
- if (!isTransparentColor(own.backgroundColor)) return null;
2618
- if (own.backgroundImage && own.backgroundImage !== 'none') return null;
2619
- let node = el.parentElement;
2620
- while (node) {
2621
- const cs = getComputedStyle(node);
2622
- if (!isTransparentColor(cs.backgroundColor)) return cs.backgroundColor;
2623
- node = node.parentElement;
2624
- }
2625
- // The walk already passed through <body> and <html>; if they had been
2626
- // opaque we would have returned. Falling through with the previous
2627
- // `getComputedStyle(body).backgroundColor || …` chain is a trap: that
2628
- // call returns the literal string `"rgba(0, 0, 0, 0)"` for a page that
2629
- // never set its own bg, which is truthy and short-circuits the chain to
2630
- // transparent-black — modern-screenshot then renders the capture on a
2631
- // black canvas and the shader overlay flashes solid black during load.
2632
- // The browser canvas defaults to white, so we do too.
2633
- return '#ffffff';
2634
- }
2635
-
2636
- // Capture the element (with current annotations baked in) and return a PNG
2637
- // Blob. Shared between the Go flow (uploads it to the server) and the
2638
- // debug toggle (displays it as an overlay for side-by-side comparison).
2639
- async function captureElementToBlob(el, snapshot, rect) {
2640
- try { if (document.fonts?.ready) await document.fonts.ready; } catch {}
2641
- const hasAnnotations = snapshot && (snapshot.comments.length > 0 || snapshot.strokes.length > 0);
2642
- let annotNode = null;
2643
- let savedPosition = null;
2644
- if (hasAnnotations) {
2645
- const pos = getComputedStyle(el).position;
2646
- if (pos === 'static') {
2647
- savedPosition = el.style.position;
2648
- el.style.position = 'relative';
2649
- }
2650
- annotNode = buildAnnotationsForCapture(rect, snapshot);
2651
- el.appendChild(annotNode);
2652
- }
2653
- try {
2654
- const ms = await loadModernScreenshot();
2655
- const fontCssText = await collectFontCssText();
2656
- const backgroundColor = resolveCanvasBackground(el);
2657
- return await ms.domToBlob(el, {
2658
- scale: Math.min(window.devicePixelRatio || 1, 2),
2659
- font: fontCssText ? { cssText: fontCssText } : undefined,
2660
- ...(backgroundColor ? { backgroundColor } : {}),
2661
- });
2662
- } finally {
2663
- if (annotNode) annotNode.remove();
2664
- if (savedPosition !== null) el.style.position = savedPosition;
2665
- }
2666
- }
2667
-
2668
- async function captureAndEmit(el, basePayload, snapshot, rect) {
2669
- let screenshotPath;
2670
- let blob;
2671
- try {
2672
- blob = await captureElementToBlob(el, snapshot, rect);
2673
- } catch (err) {
2674
- console.warn('[impeccable] capture failed, proceeding without screenshot:', err);
2675
- }
2676
- // Light up the shader overlay the moment capture is ready — no reason to
2677
- // wait for the upload to complete before the user sees something alive.
2678
- if (blob && state === 'GENERATING') {
2679
- showShaderOverlay(el, blob, rect);
2680
- }
2681
- // Only upload + forward the screenshot when annotations (comments/strokes)
2682
- // are present. Without annotations the image is pure visual anchoring —
2683
- // it biases the model toward the current rendering and works against the
2684
- // three-distinct-directions brief.
2685
- const hasAnnotations = snapshot && (snapshot.comments.length > 0 || snapshot.strokes.length > 0);
2686
- if (blob && hasAnnotations) {
2687
- try {
2688
- const uploadRes = await fetch(
2689
- 'http://localhost:' + PORT + '/annotation?token=' + encodeURIComponent(TOKEN) +
2690
- '&eventId=' + encodeURIComponent(basePayload.id),
2691
- { method: 'POST', headers: { 'Content-Type': 'image/png' }, body: blob },
2692
- );
2693
- if (uploadRes.ok) {
2694
- const { path: p } = await uploadRes.json();
2695
- screenshotPath = p;
2696
- } else {
2697
- console.warn('[impeccable] annotation upload failed:', uploadRes.status);
2698
- }
2699
- } catch (err) {
2700
- console.warn('[impeccable] annotation upload failed:', err);
2701
- }
2702
- }
2703
- sendEvent(screenshotPath ? { ...basePayload, screenshotPath } : basePayload);
2704
- }
2705
-
2706
- // ---------------------------------------------------------------------------
2707
- // Shader overlay — renders the captured screenshot as a WebGL texture and
2708
- // runs an editorial "ink-wash" fragment shader over it during generation.
2709
- // A single rolling band sweeps top-to-bottom, desaturating + tinting magenta
2710
- // and leaving a soft trail. Makes the wait feel like a letterpress scan
2711
- // instead of a dead spinner.
2712
- // ---------------------------------------------------------------------------
2713
-
2714
- const SHADER_VS = `attribute vec2 a_position;
2715
- attribute vec2 a_uv;
2716
- varying vec2 v_uv;
2717
- void main() {
2718
- v_uv = a_uv;
2719
- gl_Position = vec4(a_position, 0.0, 1.0);
2720
- }`;
2721
-
2722
- const SHADER_FS = `precision highp float;
2723
- uniform sampler2D u_texture;
2724
- uniform float u_time;
2725
- uniform vec2 u_resolution;
2726
- uniform vec3 u_accent;
2727
- varying vec2 v_uv;
2728
-
2729
- // Asymmetric roller band. Product of two one-sided smoothsteps — peaks at
2730
- // d=0 with a short sharp leading ramp and a longer soft trailing tail. Clean
2731
- // outside the [-leadW, trailW] range (no rogue "trail=1 everywhere below"
2732
- // failure that reversed-edge smoothstep would give).
2733
- float bandAt(float d, float leadW, float trailW) {
2734
- float above = smoothstep(-leadW, 0.0, d);
2735
- float below = 1.0 - smoothstep(0.0, trailW, d);
2736
- return above * below;
2737
- }
2738
-
2739
- void main() {
2740
- vec2 uv = v_uv;
2741
- // Roller sweeps top-to-bottom with small overshoot so each cycle enters
2742
- // and exits the element cleanly.
2743
- float phase = fract(u_time / 3.4);
2744
- float y = phase * 1.25 - 0.12;
2745
- float band = bandAt(uv.y - y, 0.05, 0.32);
2746
-
2747
- // Halftone cell grid (fixed ~10 px pitch).
2748
- float cellPx = 10.0;
2749
- vec2 gridUv = uv * u_resolution / cellPx;
2750
- vec2 cellId = floor(gridUv);
2751
- vec2 cellUv = fract(gridUv) - 0.5;
2752
- vec2 sampleCenter = (cellId + 0.5) * cellPx / u_resolution;
2753
- vec3 cellImg = texture2D(u_texture, sampleCenter).rgb;
2754
- float luma = dot(cellImg, vec3(0.299, 0.587, 0.114));
2755
- // Darker cells → bigger magenta dots (classic risograph halftone curve).
2756
- float radius = sqrt(clamp(1.0 - luma, 0.0, 1.0)) * 0.56;
2757
- float dotMask = smoothstep(radius + 0.06, radius, length(cellUv));
2758
- vec3 paper = vec3(0.975, 0.965, 0.955);
2759
- vec3 dotLayer = mix(paper, u_accent, dotMask);
2760
-
2761
- // Blend the halftone layer in where the roller is passing; leave the
2762
- // element pristine elsewhere.
2763
- vec3 base = texture2D(u_texture, uv).rgb;
2764
- gl_FragColor = vec4(mix(base, dotLayer, band), 1.0);
2765
- }`;
2766
-
2767
- // Editorial Magenta converted to approximate sRGB 0-1 (matches oklch(60% 0.25 350))
2768
- const SHADER_ACCENT = [0.82, 0.16, 0.47];
2769
- let shaderState = null; // { canvas, gl, program, texture, rafId, startTime }
2770
-
2771
- function compileShader(gl, type, source) {
2772
- const sh = gl.createShader(type);
2773
- gl.shaderSource(sh, source);
2774
- gl.compileShader(sh);
2775
- if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
2776
- const info = gl.getShaderInfoLog(sh);
2777
- gl.deleteShader(sh);
2778
- throw new Error('shader compile failed: ' + info);
2779
- }
2780
- return sh;
2781
- }
2782
-
2783
- function positionShaderOverlay() {
2784
- if (!shaderState || !selectedElement) return;
2785
- const r = selectedElement.getBoundingClientRect();
2786
- Object.assign(shaderState.canvas.style, {
2787
- top: r.top + 'px', left: r.left + 'px',
2788
- width: r.width + 'px', height: r.height + 'px',
2789
- });
2790
- }
2791
-
2792
- function hideShaderOverlay() {
2793
- if (!shaderState) return;
2794
- if (shaderState.rafId) cancelAnimationFrame(shaderState.rafId);
2795
- if (shaderState.canvas) shaderState.canvas.remove();
2796
- const lose = shaderState.gl?.getExtension?.('WEBGL_lose_context');
2797
- try { lose?.loseContext(); } catch {}
2798
- shaderState = null;
2799
- }
2800
-
2801
- async function showShaderOverlay(el, blob, rect) {
2802
- hideShaderOverlay();
2803
- if (!blob || !el) return;
2804
- const canvas = document.createElement('canvas');
2805
- canvas.id = PREFIX + '-shader';
2806
- const dpr = Math.min(window.devicePixelRatio || 1, 2);
2807
- canvas.width = Math.max(1, Math.floor(rect.width * dpr));
2808
- canvas.height = Math.max(1, Math.floor(rect.height * dpr));
2809
- Object.assign(canvas.style, {
2810
- position: 'fixed',
2811
- top: rect.top + 'px', left: rect.left + 'px',
2812
- width: rect.width + 'px', height: rect.height + 'px',
2813
- pointerEvents: 'none',
2814
- zIndex: Z.bar - 1,
2815
- });
2816
- document.body.appendChild(canvas);
2817
-
2818
- const gl = canvas.getContext('webgl', { premultipliedAlpha: false, preserveDrawingBuffer: false })
2819
- || canvas.getContext('experimental-webgl');
2820
- if (!gl) {
2821
- // WebGL unavailable — fall back to a plain <img> overlay so the user
2822
- // still sees something meaningful during generation.
2823
- canvas.remove();
2824
- const img = document.createElement('img');
2825
- img.src = URL.createObjectURL(blob);
2826
- img.id = PREFIX + '-shader';
2827
- // Copy positioning via cssText. Object.assign across CSSStyleDeclaration
2828
- // throws in modern Chromium because the source's indexed properties
2829
- // (style[0], [1], ...) are read-only and the engine forbids writing
2830
- // them on the destination.
2831
- img.style.cssText = canvas.style.cssText;
2832
- img.style.outline = '2px dashed ' + C.brand;
2833
- img.style.outlineOffset = '-2px';
2834
- document.body.appendChild(img);
2835
- shaderState = { canvas: img, gl: null, program: null, texture: null, rafId: 0, startTime: 0 };
2836
- return;
2837
- }
2838
-
2839
- let program, texture;
2840
- try {
2841
- const vs = compileShader(gl, gl.VERTEX_SHADER, SHADER_VS);
2842
- const fs = compileShader(gl, gl.FRAGMENT_SHADER, SHADER_FS);
2843
- program = gl.createProgram();
2844
- gl.attachShader(program, vs);
2845
- gl.attachShader(program, fs);
2846
- gl.linkProgram(program);
2847
- if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
2848
- throw new Error('program link failed: ' + gl.getProgramInfoLog(program));
2849
- }
2850
- // Full-screen quad
2851
- const buf = gl.createBuffer();
2852
- gl.bindBuffer(gl.ARRAY_BUFFER, buf);
2853
- gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
2854
- -1, -1, 0, 1,
2855
- 1, -1, 1, 1,
2856
- -1, 1, 0, 0,
2857
- -1, 1, 0, 0,
2858
- 1, -1, 1, 1,
2859
- 1, 1, 1, 0,
2860
- ]), gl.STATIC_DRAW);
2861
- const posLoc = gl.getAttribLocation(program, 'a_position');
2862
- const uvLoc = gl.getAttribLocation(program, 'a_uv');
2863
- gl.enableVertexAttribArray(posLoc);
2864
- gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 16, 0);
2865
- gl.enableVertexAttribArray(uvLoc);
2866
- gl.vertexAttribPointer(uvLoc, 2, gl.FLOAT, false, 16, 8);
2867
- } catch (err) {
2868
- console.warn('[impeccable] shader setup failed:', err);
2869
- canvas.remove();
2870
- return;
2871
- }
2872
-
2873
- // Upload the screenshot as a texture
2874
- let bitmap;
2875
- try {
2876
- bitmap = await createImageBitmap(blob);
2877
- } catch {
2878
- // Safari fallback: go via a regular Image
2879
- const imgUrl = URL.createObjectURL(blob);
2880
- const img = new Image();
2881
- img.src = imgUrl;
2882
- await new Promise((r, rej) => { img.onload = r; img.onerror = rej; });
2883
- bitmap = img;
2884
- URL.revokeObjectURL(imgUrl);
2885
- }
2886
- texture = gl.createTexture();
2887
- gl.bindTexture(gl.TEXTURE_2D, texture);
2888
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
2889
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
2890
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
2891
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
2892
- gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
2893
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, bitmap);
2894
- if (bitmap.close) bitmap.close();
2895
-
2896
- const uTime = gl.getUniformLocation(program, 'u_time');
2897
- const uRes = gl.getUniformLocation(program, 'u_resolution');
2898
- const uAccent = gl.getUniformLocation(program, 'u_accent');
2899
- const uTex = gl.getUniformLocation(program, 'u_texture');
2900
- const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
2901
-
2902
- shaderState = { canvas, gl, program, texture, rafId: 0, startTime: performance.now(), reduced };
2903
- function frame() {
2904
- if (!shaderState) return;
2905
- const elapsed = (performance.now() - shaderState.startTime) / 1000;
2906
- const t = shaderState.reduced ? 0.0 : elapsed;
2907
- gl.viewport(0, 0, canvas.width, canvas.height);
2908
- gl.useProgram(program);
2909
- gl.activeTexture(gl.TEXTURE0);
2910
- gl.bindTexture(gl.TEXTURE_2D, texture);
2911
- gl.uniform1i(uTex, 0);
2912
- gl.uniform1f(uTime, t);
2913
- gl.uniform2f(uRes, canvas.width, canvas.height);
2914
- gl.uniform3f(uAccent, SHADER_ACCENT[0], SHADER_ACCENT[1], SHADER_ACCENT[2]);
2915
- gl.drawArrays(gl.TRIANGLES, 0, 6);
2916
- shaderState.rafId = requestAnimationFrame(frame);
2917
- }
2918
- frame();
2919
- }
2920
-
2921
- function handleAccept() {
2922
- if (!currentSessionId || arrivedVariants === 0) return;
2923
- const acceptPayload = { type: 'accept', id: currentSessionId, variantId: String(visibleVariant) };
2924
- if (Object.keys(paramsCurrentValues).length > 0) {
2925
- acceptPayload.paramValues = { ...paramsCurrentValues };
2926
- }
2927
- sendEvent(acceptPayload);
2928
- markSessionHandled();
2929
-
2930
- // The accepted variant is already the only visible child of the wrapper
2931
- // (all other variants are display:none). HMR from the source rewrite will
2932
- // replace the wrapper imminently. Don't eagerly replaceChild here — React
2933
- // reconciliation races with our mutation and throws NotFoundError in Next
2934
- // 16 / Turbopack. Schedule a fallback that runs the manual swap only if
2935
- // HMR hasn't cleaned up by then (keeps static-server flows working).
2936
- const acceptedSessionId = currentSessionId;
2937
- const acceptedVariant = visibleVariant;
2938
-
2939
- state = 'CONFIRMED';
2940
- updateBarContent('confirmed');
2941
- setTimeout(function() {
2942
- hideBar();
2943
- hideHighlight();
2944
- stopScrollTracking();
2945
- if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
2946
- stopScrollLock();
2947
- clearScrollY();
2948
- clearSession();
2949
- selectedElement = null;
2950
- currentSessionId = null;
2951
- selectedAction = 'impeccable';
2952
- state = 'PICKING';
2953
- }, 1800);
2954
-
2955
- // Static-server / no-HMR fallback: if the wrapper is still around 2s after
2956
- // the cleanup above, swap it out manually. By now React has either moved
2957
- // on or the app isn't React at all. Preserve the `data-impeccable-variant="N"`
2958
- // div (with display:contents) so @scope rules anchored to the variant
2959
- // attribute keep matching until reload replaces it with the carbonize block.
2960
- setTimeout(function() {
2961
- const wrapper = document.querySelector('[data-impeccable-variants="' + acceptedSessionId + '"]');
2962
- if (!wrapper) return;
2963
- const accepted = wrapper.querySelector('[data-impeccable-variant="' + acceptedVariant + '"]');
2964
- if (accepted && accepted.firstElementChild) {
2965
- const parent = wrapper.parentElement;
2966
- if (!parent) return;
2967
- accepted.style.display = 'contents';
2968
- parent.replaceChild(accepted, wrapper);
2969
- }
2970
- }, 2000);
2971
- }
2972
-
2973
- function handleDiscard() {
2974
- if (!currentSessionId) return;
2975
- sendEvent({ type: 'discard', id: currentSessionId });
2976
- markSessionHandled();
2977
- // Instant DOM restore + fire-and-forget (script handles file cleanup)
2978
- cleanup();
2979
- }
2980
-
2981
- // ---------------------------------------------------------------------------
2982
- // Session persistence via localStorage
2983
- // ---------------------------------------------------------------------------
2984
- // Survives page reloads, browser close/reopen, HMR, and accidental refreshes.
2985
-
2986
- const LS_KEY = PREFIX + '-session';
2987
-
2988
- function saveSession() {
2989
- if (!currentSessionId) return;
2990
- // NOTE: scrollY is stored under a separate key (writeScrollY). Storing
2991
- // it here would overwrite the Go-time value every time state changes.
2992
- try {
2993
- localStorage.setItem(LS_KEY, JSON.stringify({
2994
- id: currentSessionId,
2995
- state: state,
2996
- action: selectedAction,
2997
- count: selectedCount,
2998
- expected: expectedVariants,
2999
- arrived: arrivedVariants,
3000
- visible: visibleVariant,
3001
- }));
3002
- } catch { /* quota exceeded or private mode */ }
3003
- }
3004
-
3005
- function loadSession() {
3006
- try {
3007
- const raw = localStorage.getItem(LS_KEY);
3008
- return raw ? JSON.parse(raw) : null;
3009
- } catch { return null; }
3010
- }
3011
-
3012
- function clearSession() {
3013
- try { localStorage.removeItem(LS_KEY); } catch {}
3014
- }
3015
-
3016
- /** Mark session as handled (accepted/discarded). The agent will clean up
3017
- * the source, but until it does the wrapper is still in the HTML. This
3018
- * prevents resumeSession from picking it up again after reload. */
3019
- function markSessionHandled() {
3020
- if (!currentSessionId) return;
3021
- try {
3022
- localStorage.setItem(LS_KEY + '-handled', currentSessionId);
3023
- } catch {}
3024
- }
3025
-
3026
- function isSessionHandled(id) {
3027
- try {
3028
- return localStorage.getItem(LS_KEY + '-handled') === id;
3029
- } catch { return false; }
3030
- }
3031
-
3032
- function clearHandled() {
3033
- try { localStorage.removeItem(LS_KEY + '-handled'); } catch {}
3034
- }
3035
-
3036
- function cleanup() {
3037
- // Hide the wrapper immediately so variants disappear. DON'T structurally
3038
- // mutate the DOM yet — HMR from the agent's source rewrite is on its way,
3039
- // and a manual replaceChild under React causes NotFoundError when the
3040
- // reconciler later tries to remove a wrapper we already removed.
3041
- // Schedule a 2s fallback that does the manual swap only if HMR hasn't
3042
- // replaced the wrapper by then (keeps static-server / no-HMR flows alive).
3043
- const cleanupSessionId = currentSessionId;
3044
- if (cleanupSessionId) {
3045
- const wrapper = document.querySelector('[data-impeccable-variants="' + cleanupSessionId + '"]');
3046
- if (wrapper) wrapper.style.display = 'none';
3047
- }
3048
- setTimeout(function() {
3049
- if (!cleanupSessionId) return;
3050
- const wrapper = document.querySelector('[data-impeccable-variants="' + cleanupSessionId + '"]');
3051
- if (!wrapper) return;
3052
- const orig = wrapper.querySelector('[data-impeccable-variant="original"]');
3053
- if (orig) {
3054
- const content = orig.firstElementChild;
3055
- if (content) {
3056
- wrapper.parentElement.replaceChild(content, wrapper);
3057
- return;
3058
- }
3059
- }
3060
- wrapper.remove();
3061
- }, 2000);
3062
- hideBar();
3063
- hideHighlight();
3064
- stopScrollTracking();
3065
- if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
3066
- stopScrollLock();
3067
- clearScrollY();
3068
- clearSession();
3069
- selectedElement = null;
3070
- currentSessionId = null;
3071
- selectedAction = 'impeccable';
3072
- state = 'PICKING';
3073
- }
3074
-
3075
- // ---------------------------------------------------------------------------
3076
- // Toast
3077
- // ---------------------------------------------------------------------------
3078
-
3079
- function showToast(message, duration) {
3080
- if (toastEl) toastEl.remove();
3081
- // Stack the toast above the global bar (which sits at bottom:14px) so
3082
- // the two never overlap. Read the bar's actual rect — its height varies
3083
- // with hover-expanded labels — and fall back to a sensible default
3084
- // when the bar isn't mounted yet.
3085
- const barRect = globalBarEl?.getBoundingClientRect();
3086
- const barTopFromBottom = barRect && barRect.height > 0
3087
- ? Math.max(16, window.innerHeight - barRect.top + 12)
3088
- : 16;
3089
- toastEl = el('div', {
3090
- position: 'fixed', bottom: barTopFromBottom + 'px', left: '50%',
3091
- transform: 'translateX(-50%) translateY(8px)',
3092
- background: C.ink, color: C.white,
3093
- fontFamily: FONT, fontSize: '12px',
3094
- padding: '8px 16px', borderRadius: '8px',
3095
- zIndex: Z.toast, opacity: '0',
3096
- transition: 'opacity 0.25s ' + EASE + ', transform 0.25s ' + EASE,
3097
- pointerEvents: 'none', maxWidth: '420px', textAlign: 'center',
3098
- });
3099
- toastEl.id = PREFIX + '-toast';
3100
- toastEl.textContent = message;
3101
- document.body.appendChild(toastEl);
3102
- requestAnimationFrame(() => {
3103
- toastEl.style.opacity = '1';
3104
- toastEl.style.transform = 'translateX(-50%) translateY(0)';
3105
- });
3106
- setTimeout(() => {
3107
- if (toastEl) {
3108
- toastEl.style.opacity = '0';
3109
- toastEl.style.transform = 'translateX(-50%) translateY(8px)';
3110
- setTimeout(() => { if (toastEl) { toastEl.remove(); toastEl = null; } }, 250);
3111
- }
3112
- }, duration);
3113
- }
3114
-
3115
- // ---------------------------------------------------------------------------
3116
- // Init
3117
- // ---------------------------------------------------------------------------
3118
-
3119
- // Resume an active variant session after HMR/page reload.
3120
- // If a [data-impeccable-variants] wrapper exists in the DOM, the agent wrote
3121
- // variants before HMR fired. Pick up where we left off.
3122
- function resumeSession() {
3123
- const wrapper = document.querySelector('[data-impeccable-variants]');
3124
- if (!wrapper) { clearSession(); clearHandled(); return false; }
3125
-
3126
- const sessionId = wrapper.dataset.impeccableVariants;
3127
-
3128
- // Don't resume if this session was already accepted/discarded
3129
- if (isSessionHandled(sessionId)) return false;
3130
-
3131
- currentSessionId = sessionId;
3132
- expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || '0');
3133
- const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
3134
- arrivedVariants = variants.length;
3135
-
3136
- // Restore state from localStorage if available
3137
- const saved = loadSession();
3138
- if (saved && saved.id === sessionId) {
3139
- visibleVariant = (saved.visible > 0 && saved.visible <= arrivedVariants) ? saved.visible : (arrivedVariants > 0 ? 1 : 0);
3140
- if (saved.action) selectedAction = saved.action;
3141
- if (saved.count) selectedCount = saved.count;
3142
- } else {
3143
- visibleVariant = arrivedVariants > 0 ? 1 : 0;
3144
- }
3145
-
3146
- // Find the visible variant's content element for highlight positioning.
3147
- // Try the visible variant first, fall back to the original's content.
3148
- const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null;
3149
- const origEl = pickVariantContent(wrapper, 'original');
3150
- selectedElement = visEl || origEl || wrapper.parentElement;
3151
-
3152
- // Set display state BEFORE starting observer (avoid triggering it)
3153
- if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant);
3154
-
3155
- state = arrivedVariants >= expectedVariants ? 'CYCLING' : 'GENERATING';
3156
- showBar(state === 'CYCLING' ? 'cycling' : 'generating');
3157
- startScrollTracking();
3158
- // Build the params panel for the restored visible variant. Previously
3159
- // this was missed on page-reload resume: showVariantInDOM above fires
3160
- // refreshParamsPanel, but state was still IDLE at that moment so it
3161
- // hid. Now that state is CYCLING, re-fire.
3162
- if (state === 'CYCLING') refreshParamsPanel();
3163
- saveSession();
3164
-
3165
- // Start observing for more variants AFTER initial setup
3166
- if (variantObserver) variantObserver.disconnect();
3167
- variantObserver = startVariantObserver(currentSessionId);
3168
-
3169
- // Hold the target at its saved viewport top through any subsequent
3170
- // HMR patches, variant inserts, or cycle swaps.
3171
- startScrollLock(currentSessionId, readScrollY());
3172
-
3173
- // If we reloaded mid-generation (Bun's HTML HMR destroys the shader
3174
- // canvas), re-capture the original's content and restart the shader so
3175
- // the wait doesn't go dead.
3176
- if (state === 'GENERATING' && origEl) {
3177
- (async () => {
3178
- try {
3179
- const rect = origEl.getBoundingClientRect();
3180
- if (rect.width === 0 || rect.height === 0) return;
3181
- const blob = await captureElementToBlob(origEl, null, rect);
3182
- if (blob && state === 'GENERATING') {
3183
- showShaderOverlay(origEl, blob, rect);
3184
- }
3185
- } catch (err) {
3186
- console.warn('[impeccable] shader resume failed:', err);
3187
- }
3188
- })();
3189
- }
3190
- return true;
3191
- }
3192
-
3193
- // ---------------------------------------------------------------------------
3194
- // Global bar (always visible at bottom)
3195
- // ---------------------------------------------------------------------------
3196
-
3197
- let globalBarEl = null;
3198
- let detectActive = false;
3199
- let pickActive = true;
3200
- let detectCount = 0;
3201
- let detectScriptLoaded = false;
3202
-
3203
- // Theme-aware color palette for the global bar. We detect the page's
3204
- // ambient background and invert — dark bar on light pages, light bar on
3205
- // dark pages. This keeps the bar from fighting with the host design.
3206
- function detectPageTheme() {
3207
- try {
3208
- // Dev override: set localStorage 'impeccable-dev-theme' to 'light' or
3209
- // 'dark' to preview the opposite palette without actually changing the
3210
- // page bg. Used for screenshots and theme QA.
3211
- const override = localStorage.getItem('impeccable-dev-theme');
3212
- if (override === 'light' || override === 'dark') return override;
3213
-
3214
- // Walk body → html, taking the first opaque background. The browser's
3215
- // default body / html background is `rgba(0, 0, 0, 0)`, which a naive
3216
- // regex would read as black and mislabel a perfectly white page as
3217
- // dark. Honoring alpha avoids that — and falling through to <html>
3218
- // catches the common pattern of a bg only on <html> (or only on body).
3219
- function readOpaque(el) {
3220
- if (!el) return null;
3221
- const bg = getComputedStyle(el).backgroundColor;
3222
- const m = bg.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/);
3223
- if (!m) return null;
3224
- const alpha = m[4] == null ? 1 : parseFloat(m[4]);
3225
- if (alpha < 0.5) return null; // transparent / nearly transparent → skip
3226
- return [+m[1], +m[2], +m[3]];
3227
- }
3228
-
3229
- const rgb = readOpaque(document.body) || readOpaque(document.documentElement);
3230
- // Both transparent → fall back to the browser's effective canvas color.
3231
- // White is the universal default; only one in a thousand sites swaps it
3232
- // via `color-scheme: dark` on <html>, and `prefers-color-scheme` lets
3233
- // us catch that case.
3234
- if (!rgb) {
3235
- return matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
3236
- }
3237
- const [r, g, b] = rgb;
3238
- // Perceptual luminance (Rec. 709)
3239
- const L = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
3240
- return L > 0.55 ? 'light' : 'dark';
3241
- } catch { return 'light'; }
3242
- }
3243
-
3244
- function barPaletteForTheme(theme) {
3245
- if (theme === 'dark') {
3246
- // Light bar on dark page
3247
- return {
3248
- surface: 'oklch(98% 0 0 / 0.92)',
3249
- surfaceDeep: 'oklch(92% 0.005 60 / 0.96)', // slightly deeper, faint warm
3250
- hairline: 'oklch(70% 0 0 / 0.35)',
3251
- text: 'oklch(15% 0 0)',
3252
- textDim: 'oklch(45% 0 0)',
3253
- accent: 'oklch(60% 0.25 350)',
3254
- accentSoft: 'oklch(60% 0.25 350 / 0.18)',
3255
- mark: 'oklch(98% 0 0)', // logo mark fill
3256
- markText: 'oklch(15% 0 0)', // logo "/" color
3257
- exitHover: 'oklch(85% 0 0 / 0.5)',
3258
- };
3259
- }
3260
- // Dark bar on light page. Bar is a warm charcoal, logo slab is much
3261
- // deeper so the rounded-right shape reads as a clear sculpted mark.
3262
- return {
3263
- surface: 'oklch(26% 0 0 / 0.94)',
3264
- surfaceDeep: 'oklch(18% 0 0 / 0.96)', // darker sand for Tune popover
3265
- hairline: 'oklch(42% 0 0 / 0.5)',
3266
- text: 'oklch(96% 0 0)',
3267
- textDim: 'oklch(72% 0 0)',
3268
- accent: 'oklch(72% 0.22 350)',
3269
- accentSoft: 'oklch(72% 0.22 350 / 0.22)',
3270
- mark: 'oklch(8% 0 0)',
3271
- markText: 'oklch(96% 0 0)',
3272
- exitHover: 'oklch(36% 0 0 / 0.6)',
3273
- };
3274
- }
3275
-
3276
- // Impeccable logo mark — matches the site-header SVG (rounded square + "/").
3277
- function brandMarkSvg(fill, ink, size = 18) {
3278
- return `<svg width="${size}" height="${size}" viewBox="0 0 32 32" aria-hidden="true">
3279
- <rect width="32" height="32" rx="7" fill="${fill}"/>
3280
- <text x="16" y="24" font-family="system-ui, -apple-system, sans-serif" font-size="22" font-weight="500" fill="${ink}" text-anchor="middle">/</text>
3281
- </svg>`;
3282
- }
3283
-
3284
- function initGlobalBar() {
3285
- const theme = detectPageTheme();
3286
- const P = barPaletteForTheme(theme);
3287
-
3288
- // Custom focus-visible for bar buttons. Browser default is a heavy
3289
- // blue ring that looks jarring on the dark capsule. Replace with a
3290
- // soft accent-tinted inner ring that respects the bar's palette.
3291
- if (!document.getElementById(PREFIX + '-bar-focus-style')) {
3292
- const s = document.createElement('style');
3293
- s.id = PREFIX + '-bar-focus-style';
3294
- s.textContent =
3295
- '#' + PREFIX + '-global-bar button:focus { outline: none; }' +
3296
- '#' + PREFIX + '-global-bar button:focus-visible {' +
3297
- ' outline: none;' +
3298
- ' box-shadow: 0 0 0 2px ' + P.accentSoft + ', 0 0 0 3px ' + P.accent + ';' +
3299
- '}';
3300
- document.head.appendChild(s);
3301
- }
3302
-
3303
- globalBarEl = el('div', {
3304
- position: 'fixed', bottom: '14px', left: '50%',
3305
- transform: 'translateX(-50%) translateY(20px)',
3306
- zIndex: Z.bar + 5,
3307
- display: 'flex', alignItems: 'stretch',
3308
- gap: '2px',
3309
- background: P.surface,
3310
- backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)',
3311
- border: '1px solid ' + P.hairline,
3312
- borderRadius: '10px',
3313
- boxShadow: '0 4px 20px oklch(0% 0 0 / 0.12), 0 1px 3px oklch(0% 0 0 / 0.08)',
3314
- fontFamily: FONT, fontSize: '12px', lineHeight: '1',
3315
- opacity: '0',
3316
- overflow: 'hidden', // clip the full-bleed brand mark to the bar radius
3317
- transition: 'opacity 0.3s ' + EASE + ', transform 0.3s ' + EASE,
3318
- });
3319
- globalBarEl.id = PREFIX + '-global-bar';
3320
- globalBarEl.dataset.theme = theme;
3321
-
3322
- // Brand mark — fills bar height on the left. Left side inherits the bar's
3323
- // rounded corner via overflow:hidden; right side is a clean hard edge since
3324
- // the near-black/charcoal contrast does the shape-defining work.
3325
- const brand = el('span', {
3326
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
3327
- alignSelf: 'stretch',
3328
- padding: '0 12px 0 14px',
3329
- background: P.mark,
3330
- color: P.markText,
3331
- fontFamily: 'system-ui, -apple-system, sans-serif',
3332
- fontWeight: '500',
3333
- fontSize: '18px', lineHeight: '1',
3334
- });
3335
- brand.textContent = '/';
3336
- brand.title = 'Impeccable';
3337
- globalBarEl.appendChild(brand);
3338
-
3339
- // Inner wrapper: holds the toggles with normal bar padding.
3340
- const inner = el('div', {
3341
- display: 'flex', alignItems: 'center',
3342
- padding: '4px 5px', gap: '2px',
3343
- });
3344
- inner.id = PREFIX + '-global-bar-inner';
3345
- globalBarEl.appendChild(inner);
3346
-
3347
- // --- button factory: icon-only at rest, label slides in on hover/active ---
3348
- function makeIconBtn({ id, svg, label, ariaLabel, labelFont, onClick }) {
3349
- const b = el('button', {
3350
- position: 'relative',
3351
- display: 'inline-flex', alignItems: 'center',
3352
- padding: '6px 8px', borderRadius: '7px',
3353
- border: 'none', background: 'transparent',
3354
- color: P.textDim, fontFamily: FONT, fontSize: '11.5px', fontWeight: '500',
3355
- cursor: 'pointer',
3356
- transition: 'background 0.15s ease, color 0.15s ease',
3357
- whiteSpace: 'nowrap', overflow: 'hidden',
3358
- });
3359
- b.id = id;
3360
- b.title = ariaLabel || label || '';
3361
- b.setAttribute('aria-label', ariaLabel || label || '');
3362
- b.innerHTML = svg + (label
3363
- ? `<span class="icon-btn-label" style="display:inline-block;max-width:0;opacity:0;margin-left:0;overflow:hidden;font-family:${labelFont || FONT};transition:max-width 0.25s ${EASE}, opacity 0.2s ease, margin-left 0.25s ${EASE};">${label}</span>`
3364
- : '');
3365
- const labelEl = b.querySelector('.icon-btn-label');
3366
- const expand = () => {
3367
- if (!labelEl) return;
3368
- labelEl.style.maxWidth = '120px'; labelEl.style.opacity = '1'; labelEl.style.marginLeft = '6px';
3369
- };
3370
- const collapse = () => {
3371
- if (!labelEl || b.dataset.active === 'true') return;
3372
- labelEl.style.maxWidth = '0'; labelEl.style.opacity = '0'; labelEl.style.marginLeft = '0';
3373
- };
3374
- // Per-button hover only changes color (no layout). The label expand/
3375
- // collapse is driven by the bar-level mouseenter/mouseleave so moving
3376
- // the mouse between adjacent buttons doesn't trigger per-button width
3377
- // thrashing — the whole bar grows once and shrinks once.
3378
- b.addEventListener('mouseenter', () => { if (b.dataset.active !== 'true') b.style.color = P.text; });
3379
- b.addEventListener('mouseleave', () => { if (b.dataset.active !== 'true') b.style.color = P.textDim; });
3380
- b.addEventListener('click', onClick);
3381
- b._expandLabel = expand;
3382
- b._collapseLabel = collapse;
3383
- return b;
3384
- }
3385
-
3386
- // Pick toggle — starts active (primary intent when entering live mode).
3387
- const pickBtn = makeIconBtn({
3388
- id: PREFIX + '-pick-toggle',
3389
- svg: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><circle cx="12" cy="12" r="10"/><line x1="22" y1="12" x2="18" y2="12"/><line x1="6" y1="12" x2="2" y2="12"/><line x1="12" y1="6" x2="12" y2="2"/><line x1="12" y1="22" x2="12" y2="18"/></svg>',
3390
- label: 'Pick',
3391
- ariaLabel: 'Pick element',
3392
- onClick: () => togglePick(),
3393
- });
3394
- pickBtn.style.background = P.accentSoft;
3395
- pickBtn.style.color = P.accent;
3396
- pickBtn.dataset.active = 'true';
3397
- pickBtn._expandLabel();
3398
- inner.appendChild(pickBtn);
3399
-
3400
- // Detect toggle
3401
- const detectBtn = makeIconBtn({
3402
- id: PREFIX + '-detect-toggle',
3403
- svg: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>',
3404
- label: 'Detect',
3405
- ariaLabel: 'Detect anti-patterns',
3406
- onClick: () => toggleDetect(),
3407
- });
3408
- const detectBadge = el('span', {
3409
- fontSize: '10px', fontWeight: '600',
3410
- padding: '0px 5px', borderRadius: '7px', lineHeight: '16px',
3411
- background: P.accent, color: P.surface.includes('18%') ? 'oklch(18% 0 0)' : 'oklch(98% 0 0)',
3412
- display: 'none', fontFamily: MONO, marginLeft: '4px',
3413
- });
3414
- detectBadge.id = PREFIX + '-detect-badge';
3415
- detectBtn.appendChild(detectBadge);
3416
- inner.appendChild(detectBtn);
3417
-
3418
- // DESIGN.md panel toggle — quartet of color squares as the mark.
3419
- const designBtn = makeIconBtn({
3420
- id: PREFIX + '-design-toggle',
3421
- svg: `<span style="display:inline-grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;width:14px;height:14px;border-radius:3px;overflow:hidden;box-shadow:inset 0 0 0 1px ${P.hairline};flex-shrink:0">
3422
- <span style="background:oklch(60% 0.25 350)"></span>
3423
- <span style="background:oklch(60% 0.15 45)"></span>
3424
- <span style="background:oklch(55% 0.12 250)"></span>
3425
- <span style="background:oklch(30% 0 0)"></span>
3426
- </span>`,
3427
- label: 'DESIGN.md',
3428
- ariaLabel: 'Toggle DESIGN.md panel',
3429
- labelFont: MONO,
3430
- onClick: () => toggleDesignPanel(),
3431
- });
3432
- inner.appendChild(designBtn);
3433
-
3434
- // Thin divider before the exit button
3435
- const divider = el('span', {
3436
- width: '1px', height: '18px',
3437
- background: P.hairline,
3438
- margin: '0 4px 0 2px',
3439
- });
3440
- inner.appendChild(divider);
3441
-
3442
- // Exit × on the right — intentionally subtle (textDim at rest, text on
3443
- // hover) so it sits behind the active toggles in visual hierarchy.
3444
- //
3445
- // Explicit padding + box-sizing here is load-bearing: a host page like
3446
- // `button { padding: 0.5rem 1rem; }` (very common in resets) would
3447
- // otherwise inflate this 24x24 button into 56x40 and push the SVG out
3448
- // of the visible bar — the X stays invisible even though the styles in
3449
- // DevTools look fine. Every other chrome button sets padding inline;
3450
- // this one needed it too.
3451
- const exitBtn = el('button', {
3452
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
3453
- padding: '0', boxSizing: 'border-box',
3454
- width: '24px', height: '24px', borderRadius: '6px',
3455
- border: 'none', background: 'transparent',
3456
- color: P.textDim, fontFamily: FONT, fontSize: '0', lineHeight: '0',
3457
- cursor: 'pointer', transition: 'color 0.12s ease, background 0.12s ease',
3458
- });
3459
- exitBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="3" y1="3" x2="11" y2="11"/><line x1="11" y1="3" x2="3" y2="11"/></svg>';
3460
- exitBtn.title = 'Exit live mode';
3461
- exitBtn.addEventListener('mouseenter', () => { exitBtn.style.color = P.text; exitBtn.style.background = P.exitHover; });
3462
- exitBtn.addEventListener('mouseleave', () => { exitBtn.style.color = P.textDim; exitBtn.style.background = 'transparent'; });
3463
- exitBtn.addEventListener('click', () => { sendEvent({ type: 'exit' }); teardown(); });
3464
- inner.appendChild(exitBtn);
3465
-
3466
- // Bar-level hover: expand every toggle's label at once; collapse on leave.
3467
- // Buttons with dataset.active="true" ignore collapse (their label stays).
3468
- const toggles = [pickBtn, detectBtn, designBtn];
3469
- globalBarEl.addEventListener('mouseenter', () => {
3470
- toggles.forEach((t) => t._expandLabel && t._expandLabel());
3471
- });
3472
- globalBarEl.addEventListener('mouseleave', () => {
3473
- toggles.forEach((t) => t._collapseLabel && t._collapseLabel());
3474
- });
3475
-
3476
- document.body.appendChild(globalBarEl);
3477
- defangOutsideHandlers(globalBarEl);
3478
-
3479
- requestAnimationFrame(() => {
3480
- globalBarEl.style.opacity = '1';
3481
- globalBarEl.style.transform = 'translateX(-50%) translateY(0)';
3482
- });
3483
-
3484
- // Listen for detection results AND ready signal
3485
- window.addEventListener('message', onDetectMessage);
3486
- }
3487
-
3488
- function updateGlobalBarState() {
3489
- const detectToggle = document.getElementById(PREFIX + '-detect-toggle');
3490
- const detectBadge = document.getElementById(PREFIX + '-detect-badge');
3491
- const pickToggle = document.getElementById(PREFIX + '-pick-toggle');
3492
- const designToggle = document.getElementById(PREFIX + '-design-toggle');
3493
- const theme = globalBarEl?.dataset.theme || 'light';
3494
- const P = barPaletteForTheme(theme);
3495
-
3496
- // Sync one toggle's active state, colors, and slide-label visibility.
3497
- function sync(btn, active) {
3498
- if (!btn) return;
3499
- btn.style.background = active ? P.accentSoft : 'transparent';
3500
- btn.style.color = active ? P.accent : P.textDim;
3501
- btn.dataset.active = active ? 'true' : 'false';
3502
- if (active && btn._expandLabel) btn._expandLabel();
3503
- else if (!active && btn._collapseLabel) btn._collapseLabel();
3504
- }
3505
- sync(pickToggle, pickActive);
3506
- sync(detectToggle, detectActive);
3507
- sync(designToggle, designState.open);
3508
-
3509
- // If the bar is currently under the cursor, keep all labels expanded —
3510
- // otherwise clicking a toggle that deactivates (e.g. closing DESIGN.md)
3511
- // would collapse its label while the user's mouse is still on the bar.
3512
- if (globalBarEl && globalBarEl.matches(':hover')) {
3513
- [pickToggle, detectToggle, designToggle].forEach((t) => t?._expandLabel?.());
3514
- }
3515
-
3516
- if (detectBadge) {
3517
- detectBadge.style.display = (detectActive && detectCount > 0) ? 'inline' : 'none';
3518
- detectBadge.textContent = detectCount;
3519
- }
3520
-
3521
- // When pick is active, make detect overlays click-through so the picker works
3522
- document.querySelectorAll('.impeccable-overlay').forEach(o => {
3523
- o.style.pointerEvents = pickActive ? 'none' : '';
3524
- });
3525
- }
3526
-
3527
- let detectReady = false; // true once detect script posts 'impeccable-ready'
3528
- let detectPendingScan = false; // scan requested before script was ready
3529
-
3530
- function toggleDetect() {
3531
- detectActive = !detectActive;
3532
- updateGlobalBarState();
3533
-
3534
- if (detectActive) {
3535
- if (!detectScriptLoaded) {
3536
- detectPendingScan = true;
3537
- loadDetectScript();
3538
- } else if (detectReady) {
3539
- window.postMessage({ source: 'impeccable-command', action: 'scan' }, '*');
3540
- } else {
3541
- detectPendingScan = true;
3542
- }
3543
- } else {
3544
- window.postMessage({ source: 'impeccable-command', action: 'remove' }, '*');
3545
- detectCount = 0;
3546
- updateGlobalBarState();
3547
- }
3548
- }
3549
-
3550
- function togglePick() {
3551
- pickActive = !pickActive;
3552
- updateGlobalBarState();
3553
-
3554
- if (!pickActive) {
3555
- // Disabling pick clears any in-flight selection and UI: highlight,
3556
- // contextual bar, selectedElement. Otherwise a stale selection sits
3557
- // on screen with no obvious way to dismiss.
3558
- hideHighlight();
3559
- hideBar();
3560
- hideActionPicker();
3561
- selectedElement = null;
3562
- if (state === 'PICKING' || state === 'CONFIGURING') state = 'IDLE';
3563
- } else {
3564
- if (state === 'IDLE') state = 'PICKING';
3565
- }
3566
- }
3567
-
3568
- function loadDetectScript() {
3569
- if (detectScriptLoaded) return;
3570
- detectScriptLoaded = true;
3571
- const s = document.createElement('script');
3572
- s.src = 'http://localhost:' + PORT + '/detect.js';
3573
- s.dataset.impeccableExtension = 'true';
3574
- document.head.appendChild(s);
3575
- }
3576
-
3577
- function onDetectMessage(e) {
3578
- if (!e.data || typeof e.data.source !== 'string') return;
3579
- // Detection script is loaded and ready
3580
- if (e.data.source === 'impeccable-ready') {
3581
- detectReady = true;
3582
- if (detectPendingScan && detectActive) {
3583
- detectPendingScan = false;
3584
- window.postMessage({ source: 'impeccable-command', action: 'scan' }, '*');
3585
- }
3586
- }
3587
- // Scan results arrived
3588
- if (e.data.source === 'impeccable-results') {
3589
- detectCount = e.data.count || 0;
3590
- updateGlobalBarState();
3591
- }
3592
- }
3593
-
3594
- /** Full teardown: remove all UI, disconnect SSE, clean up. */
3595
- function teardown() {
3596
- cleanup();
3597
- hideBar();
3598
- if (globalBarEl) {
3599
- globalBarEl.style.transform = 'translateY(100%)';
3600
- setTimeout(() => { if (globalBarEl) globalBarEl.remove(); globalBarEl = null; }, 300);
3601
- }
3602
- if (highlightEl) { highlightEl.remove(); highlightEl = null; }
3603
- if (tooltipEl) { tooltipEl.remove(); tooltipEl = null; }
3604
- if (barEl) { barEl.remove(); barEl = null; }
3605
- if (pickerEl) { pickerEl.remove(); pickerEl = null; }
3606
- if (paramsPanelEl) { paramsPanelEl.remove(); paramsPanelEl = null; paramsPanelInner = null; paramsPanelBody = null; }
3607
- if (evtSource) { evtSource.close(); evtSource = null; }
3608
- document.removeEventListener('mousemove', handleMouseMove, true);
3609
- document.removeEventListener('click', handleClick, true);
3610
- document.removeEventListener('keydown', handleKeyDown, true);
3611
- window.removeEventListener('message', onDetectMessage);
3612
- // Remove detection overlays
3613
- window.postMessage({ source: 'impeccable-command', action: 'remove' }, '*');
3614
- state = 'IDLE';
3615
- window.__IMPECCABLE_LIVE_INIT__ = false;
3616
- console.log('[impeccable] Live mode exited.');
3617
- }
3618
-
3619
- // ---------------------------------------------------------------------------
3620
- // Design System Panel — visualizes the project's DESIGN.json sidecar
3621
- // ---------------------------------------------------------------------------
3622
-
3623
- const DESIGN_PREFS_KEY = 'impeccable-live-design-panel';
3624
- const DESIGN_PANEL_WIDTH = 440;
3625
-
3626
- let designHost = null;
3627
- let designShadow = null;
3628
- let designState = {
3629
- open: false,
3630
- tab: 'visual', // 'visual' | 'raw'
3631
- parsed: null, // parseDesignMd output (frontmatter + body sections)
3632
- sidecar: null, // DESIGN.json v2 payload (extensions + components + narrative)
3633
- hasMd: false,
3634
- hasSidecar: false,
3635
- present: null, // true/false once fetch resolves
3636
- raw: null, // raw DESIGN.md for the raw tab
3637
- mdNewerThanJson: false, // stale-hint flag
3638
- loading: false,
3639
- error: null,
3640
- collapsed: { // narrative-section accordion state
3641
- rules: true, dosdonts: true, overview: true,
3642
- },
3643
- };
3644
-
3645
- function loadDesignPrefs() {
3646
- // `open` is intentionally NOT persisted — the panel always starts closed
3647
- // so live mode doesn't auto-slide a big panel over the page on startup.
3648
- try {
3649
- const raw = localStorage.getItem(DESIGN_PREFS_KEY);
3650
- if (!raw) return;
3651
- const prefs = JSON.parse(raw);
3652
- if (prefs.tab === 'visual' || prefs.tab === 'raw') designState.tab = prefs.tab;
3653
- if (prefs.collapsed && typeof prefs.collapsed === 'object') {
3654
- Object.assign(designState.collapsed, prefs.collapsed);
3655
- }
3656
- } catch { /* ignore */ }
3657
- }
3658
-
3659
- function saveDesignPrefs() {
3660
- try {
3661
- localStorage.setItem(DESIGN_PREFS_KEY, JSON.stringify({
3662
- tab: designState.tab,
3663
- collapsed: designState.collapsed,
3664
- }));
3665
- } catch { /* ignore */ }
3666
- }
3667
-
3668
- function initDesignPanel() {
3669
- designHost = document.createElement('div');
3670
- designHost.id = PREFIX + '-design-host';
3671
- Object.assign(designHost.style, {
3672
- position: 'fixed', top: '0', left: '0',
3673
- width: '0', height: '0',
3674
- zIndex: String(Z.bar + 10),
3675
- pointerEvents: 'none',
3676
- });
3677
- designShadow = designHost.attachShadow({ mode: 'open' });
3678
-
3679
- const style = document.createElement('style');
3680
- // Theme-match the bar: dark chrome on light pages, light chrome on dark pages.
3681
- const theme = detectPageTheme();
3682
- style.textContent = designPanelCss(barPaletteForTheme(theme));
3683
- designShadow.appendChild(style);
3684
-
3685
- const root = document.createElement('div');
3686
- root.className = 'root';
3687
- designShadow.appendChild(root);
3688
-
3689
- document.body.appendChild(designHost);
3690
- // The host is pointer-events: none; the panel inside the shadow DOM
3691
- // manages its own auto/none. Events bubble through the shadow boundary,
3692
- // so attaching here silences host-page outside-interaction handlers
3693
- // without touching the host's click-through behavior.
3694
- defangOutsideHandlers(designHost, { setPointerEvents: false });
3695
-
3696
- loadDesignPrefs();
3697
- renderDesignChrome();
3698
- if (designState.open) {
3699
- fetchDesignSystem();
3700
- }
3701
- }
3702
-
3703
- // Neutral panel palette — deliberately NOT Impeccable-branded. The panel is
3704
- // a viewer of the project's design system, not an Impeccable surface.
3705
- const DP = {
3706
- canvas: 'oklch(94% 0 0)', // panel background
3707
- tile: 'oklch(98.5% 0 0)', // card-on-canvas
3708
- tileAlt: 'oklch(96% 0 0)', // subtler tile for inner surfaces
3709
- ink: 'oklch(15% 0 0)',
3710
- ink2: 'oklch(35% 0 0)',
3711
- meta: 'oklch(55% 0 0)',
3712
- hairline: 'oklch(88% 0 0)',
3713
- hairlineSoft: 'oklch(92% 0 0)',
3714
- amber: 'oklch(70% 0.13 65)', // stale-hint accent
3715
- amberBg: 'oklch(95% 0.05 80)',
3716
- };
3717
-
3718
- function designPanelCss(BP) {
3719
- // BP = bar palette (theme-aware, matches the global bar).
3720
- // DP = internal content palette (neutral, so tiles render colors true).
3721
- return `
3722
- :host, .root { all: initial; }
3723
- .root {
3724
- font-family: ${FONT};
3725
- color: ${DP.ink};
3726
- pointer-events: none;
3727
- }
3728
- .root * { box-sizing: border-box; }
3729
- button { font: inherit; color: inherit; }
3730
-
3731
- /* --- Panel shell: chrome matches the bar; body canvas stays neutral --- */
3732
- .panel {
3733
- position: fixed; top: 12px; bottom: 72px; right: 12px;
3734
- width: ${DESIGN_PANEL_WIDTH}px; max-width: calc(100vw - 24px);
3735
- background: ${BP.surface};
3736
- border: 1px solid ${BP.hairline};
3737
- border-radius: 14px;
3738
- backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
3739
- box-shadow: 0 20px 60px oklch(0% 0 0 / 0.18), 0 4px 12px oklch(0% 0 0 / 0.08);
3740
- display: flex; flex-direction: column;
3741
- transform: translateX(calc(100% + 24px));
3742
- opacity: 0;
3743
- transition: transform 0.35s ${EASE}, opacity 0.25s ${EASE};
3744
- pointer-events: none;
3745
- overflow: hidden;
3746
- }
3747
- .panel[data-open="true"] { transform: translateX(0); opacity: 1; pointer-events: auto; }
3748
-
3749
- .panel-header {
3750
- display: flex; align-items: center; gap: 10px;
3751
- padding: 10px 10px 10px 14px;
3752
- background: transparent;
3753
- border-bottom: 1px solid ${BP.hairline};
3754
- }
3755
- .panel-title {
3756
- flex: 1; min-width: 0;
3757
- font-family: ${MONO};
3758
- font-size: 11.5px; font-weight: 600;
3759
- letter-spacing: 0.02em;
3760
- color: ${BP.text};
3761
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
3762
- }
3763
- .panel-close {
3764
- border: none; background: transparent; color: ${BP.textDim};
3765
- width: 26px; height: 26px; border-radius: 7px;
3766
- display: inline-flex; align-items: center; justify-content: center;
3767
- cursor: pointer; transition: background 0.15s ease, color 0.15s ease;
3768
- }
3769
- .panel-close:hover { background: ${BP.hairline}; color: ${BP.text}; }
3770
-
3771
- .tabs {
3772
- display: inline-flex; padding: 2px;
3773
- background: ${BP.hairline};
3774
- border-radius: 7px;
3775
- gap: 2px;
3776
- }
3777
- .tab {
3778
- border: none; background: transparent;
3779
- padding: 4px 10px; border-radius: 5px;
3780
- font-family: ${MONO};
3781
- font-size: 10px; font-weight: 600; letter-spacing: 0.08em;
3782
- text-transform: uppercase;
3783
- color: ${BP.textDim}; cursor: pointer;
3784
- transition: background 0.15s ease, color 0.15s ease;
3785
- }
3786
- .tab[data-active="true"] { background: ${BP.surface}; color: ${BP.text}; }
3787
-
3788
- .panel-body {
3789
- flex: 1; overflow-y: auto;
3790
- padding: 12px 12px 20px;
3791
- background: ${DP.canvas};
3792
- scrollbar-width: thin;
3793
- scrollbar-color: ${DP.hairline} transparent;
3794
- }
3795
- .panel-body::-webkit-scrollbar { width: 8px; }
3796
- .panel-body::-webkit-scrollbar-thumb { background: ${DP.hairline}; border-radius: 8px; border: 2px solid transparent; background-clip: padding-box; }
3797
-
3798
- /* --- States --- */
3799
- .empty, .loading, .error {
3800
- margin: 16px 4px;
3801
- padding: 28px 20px; text-align: center;
3802
- background: ${DP.tile}; border-radius: 14px;
3803
- color: ${DP.ink2}; font-size: 13px; line-height: 1.55;
3804
- }
3805
- .empty strong { color: ${DP.ink}; display: block; margin-bottom: 6px; font-size: 14px; }
3806
- .empty code { font-family: ${MONO}; background: ${DP.canvas}; padding: 1px 6px; border-radius: 4px; font-size: 12px; color: ${DP.ink}; }
3807
- .error { color: oklch(45% 0.15 25); }
3808
-
3809
- /* --- Stale hint --- */
3810
- .stale {
3811
- display: flex; align-items: center; gap: 8px;
3812
- margin: 8px 4px 12px;
3813
- padding: 8px 12px;
3814
- background: ${DP.amberBg};
3815
- border-radius: 10px;
3816
- font-size: 11.5px; color: ${DP.ink2};
3817
- }
3818
- .stale-dot { width: 8px; height: 8px; border-radius: 50%; background: ${DP.amber}; flex-shrink: 0; }
3819
- .stale-text { flex: 1; min-width: 0; }
3820
- .stale-text strong { color: ${DP.ink}; font-weight: 600; }
3821
-
3822
- /* --- Parsed-md fallback banner --- */
3823
- .parsed-md-cta {
3824
- margin: 8px 4px 14px;
3825
- padding: 14px 16px;
3826
- background: ${DP.tile};
3827
- border: 1px dashed ${DP.hairline};
3828
- border-radius: 12px;
3829
- font-size: 12px; color: ${DP.ink2}; line-height: 1.55;
3830
- }
3831
- .parsed-md-cta strong { color: ${DP.ink}; display: block; margin-bottom: 4px; font-size: 13px; font-weight: 600; }
3832
- .parsed-md-cta code { font-family: ${MONO}; background: ${DP.canvas}; padding: 1px 5px; border-radius: 4px; font-size: 11.5px; color: ${DP.ink}; }
3833
-
3834
- /* --- Tile primitives --- */
3835
- .tile {
3836
- position: relative;
3837
- background: ${DP.tile};
3838
- border-radius: 16px;
3839
- padding: 16px;
3840
- margin: 0 4px 10px;
3841
- }
3842
- .tile-row { margin: 0 4px 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
3843
- .tile-row .tile { margin: 0; }
3844
- .tile-meta {
3845
- display: flex; align-items: baseline; justify-content: space-between;
3846
- gap: 10px;
3847
- font-family: ${MONO};
3848
- font-size: 10px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase;
3849
- color: ${DP.meta};
3850
- }
3851
- .tile-meta .name { color: ${DP.ink}; font-weight: 600; letter-spacing: 0.05em; text-transform: none; font-family: ${FONT}; font-size: 12.5px; }
3852
-
3853
- /* --- Color tile --- */
3854
- .c-tile { cursor: pointer; transition: transform 0.2s ${EASE}; }
3855
- .c-tile:hover { transform: translateY(-1px); }
3856
- .c-hero {
3857
- height: 72px; border-radius: 10px; margin-top: 10px;
3858
- box-shadow: inset 0 0 0 1px oklch(0% 0 0 / 0.05);
3859
- }
3860
- .c-ramp {
3861
- display: flex; gap: 0; height: 14px; border-radius: 4px; overflow: hidden;
3862
- margin-top: 8px;
3863
- box-shadow: inset 0 0 0 1px oklch(0% 0 0 / 0.04);
3864
- }
3865
- .c-ramp > span { flex: 1; }
3866
- .c-desc { margin-top: 8px; font-size: 11.5px; line-height: 1.45; color: ${DP.ink2}; }
3867
-
3868
- /* --- Type tile --- */
3869
- .t-tile { }
3870
- .t-specimen {
3871
- margin: 4px 0 6px;
3872
- color: ${DP.ink};
3873
- line-height: 0.9;
3874
- }
3875
- .t-family { margin-top: 4px; font-size: 12px; font-weight: 600; color: ${DP.ink}; }
3876
- .t-purpose { margin-top: 4px; font-size: 11px; line-height: 1.45; color: ${DP.ink2}; }
3877
-
3878
- /* --- Shadow tile --- */
3879
- .s-tile { }
3880
- .s-surface {
3881
- height: 60px; margin: 8px 2px 10px;
3882
- background: ${DP.tile};
3883
- border-radius: 10px;
3884
- }
3885
- .s-value { font-family: ${MONO}; font-size: 10px; color: ${DP.meta}; word-break: break-all; line-height: 1.4; }
3886
- .s-purpose { margin-top: 4px; font-size: 11px; color: ${DP.ink2}; line-height: 1.45; }
3887
-
3888
- /* --- Radii strip --- */
3889
- .r-strip { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 10px; }
3890
- .r-item { display: flex; flex-direction: column; align-items: center; gap: 4px; flex: 1; min-width: 60px; }
3891
- .r-sample { width: 44px; height: 44px; background: ${DP.canvas}; box-shadow: inset 0 0 0 1px oklch(0% 0 0 / 0.08); }
3892
- .r-label { font-family: ${MONO}; font-size: 10px; color: ${DP.meta}; letter-spacing: 0.05em; text-transform: uppercase; }
3893
- .r-val { font-family: ${MONO}; font-size: 10px; color: ${DP.ink}; }
3894
-
3895
- /* --- Component tile (hosts live primitives) --- */
3896
- .cmp-tile { }
3897
- .cmp-stage {
3898
- margin: 12px -4px 0;
3899
- padding: 18px 16px 10px;
3900
- border-top: 1px solid ${DP.hairlineSoft};
3901
- display: flex; flex-direction: column; align-items: center; justify-content: center;
3902
- gap: 14px;
3903
- min-height: 68px;
3904
- }
3905
- .cmp-stage + .cmp-stage { border-top: 1px dashed ${DP.hairlineSoft}; }
3906
- .cmp-sublabel { font-family: ${MONO}; font-size: 10px; color: ${DP.meta}; letter-spacing: 0.06em; }
3907
- .cmp-kind { font-family: ${MONO}; font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: ${DP.meta}; }
3908
-
3909
- /* --- Collapsible --- */
3910
- .coll {
3911
- margin: 0 4px 8px;
3912
- background: ${DP.tile};
3913
- border-radius: 12px;
3914
- overflow: hidden;
3915
- }
3916
- .coll-head {
3917
- display: flex; align-items: center; gap: 10px;
3918
- width: 100%;
3919
- padding: 12px 14px;
3920
- background: transparent; border: none;
3921
- cursor: pointer; text-align: left;
3922
- font-family: ${FONT}; font-size: 12.5px; font-weight: 600; color: ${DP.ink};
3923
- transition: background 0.12s ease;
3924
- }
3925
- .coll-head:hover { background: ${DP.tileAlt}; }
3926
- .coll-chev {
3927
- width: 12px; height: 12px; flex-shrink: 0;
3928
- color: ${DP.meta};
3929
- transition: transform 0.2s ${EASE};
3930
- }
3931
- .coll[data-open="true"] .coll-chev { transform: rotate(90deg); }
3932
- .coll-count { margin-left: auto; font-family: ${MONO}; font-size: 10px; color: ${DP.meta}; letter-spacing: 0.05em; }
3933
- .coll-body { padding: 0 14px 14px; display: none; }
3934
- .coll[data-open="true"] .coll-body { display: block; }
3935
-
3936
- .rule-card {
3937
- padding: 10px 0;
3938
- border-top: 1px solid ${DP.hairlineSoft};
3939
- }
3940
- .rule-card:first-child { border-top: none; padding-top: 2px; }
3941
- .rule-card .name { font-size: 11.5px; font-weight: 700; color: ${DP.ink}; margin-bottom: 3px; }
3942
- .rule-card .name .section { font-family: ${MONO}; font-size: 9px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; color: ${DP.meta}; margin-left: 8px; }
3943
- .rule-card .body { font-size: 11.5px; color: ${DP.ink2}; line-height: 1.5; }
3944
-
3945
- .coll .dos { display: grid; gap: 0; margin-top: 2px; }
3946
- .coll .do, .coll .dont {
3947
- position: relative;
3948
- padding: 8px 0 8px 22px;
3949
- font-size: 11.5px; line-height: 1.5; color: ${DP.ink2};
3950
- border-top: 1px solid ${DP.hairlineSoft};
3951
- }
3952
- .coll .do:first-child, .coll .dont:first-child,
3953
- .coll .do:first-of-type { border-top: none; }
3954
- .coll .do + .dont { border-top: 1px solid ${DP.hairlineSoft}; }
3955
- .coll .do::before, .coll .dont::before {
3956
- content: ''; position: absolute; left: 4px; top: 13px;
3957
- width: 8px; height: 8px; border-radius: 50%;
3958
- }
3959
- .coll .do::before { background: oklch(62% 0.16 145); }
3960
- .coll .dont::before { background: oklch(58% 0.22 25); }
3961
-
3962
- .coll .overview-body {
3963
- font-size: 12px; line-height: 1.55; color: ${DP.ink2};
3964
- }
3965
- .coll .overview-body .north-star {
3966
- display: block; font-family: ${FONT}; font-style: italic;
3967
- font-size: 15px; line-height: 1.3; color: ${DP.ink};
3968
- margin-bottom: 8px;
3969
- }
3970
- .coll .overview-body p { margin: 0 0 8px; }
3971
- .coll .overview-body ul { margin: 6px 0 0; padding-left: 16px; font-size: 11.5px; }
3972
- .coll .overview-body li { margin-bottom: 3px; }
3973
-
3974
- /* --- raw tab markdown (unchanged layout, neutralized palette) --- */
3975
- .md { padding: 4px 10px 20px; font-size: 13px; line-height: 1.6; color: ${DP.ink}; }
3976
- .md h1, .md h2, .md h3, .md h4 { margin: 20px 0 8px; color: ${DP.ink}; font-weight: 600; }
3977
- .md h1 { font-size: 18px; }
3978
- .md h2 { font-size: 15px; padding-bottom: 4px; border-bottom: 1px solid ${DP.hairlineSoft}; }
3979
- .md h3 { font-size: 13px; }
3980
- .md h4 { font-size: 12px; color: ${DP.meta}; }
3981
- .md p { margin: 0 0 10px; }
3982
- .md ul, .md ol { margin: 0 0 10px; padding-left: 20px; }
3983
- .md li { margin-bottom: 4px; }
3984
- .md code { font-family: ${MONO}; font-size: 12px; background: ${DP.canvas}; padding: 1px 5px; border-radius: 4px; }
3985
- .md pre { font-family: ${MONO}; font-size: 12px; background: ${DP.canvas}; padding: 10px 12px; border-radius: 8px; overflow-x: auto; margin: 0 0 10px; }
3986
- .md pre code { background: none; padding: 0; }
3987
- .md strong { font-weight: 700; }
3988
- .md em { font-style: italic; }
3989
- .md a { color: ${DP.ink}; text-decoration: underline; }
3990
- .md hr { border: none; border-top: 1px solid ${DP.hairlineSoft}; margin: 16px 0; }
3991
- `;
3992
- }
3993
-
3994
- function renderDesignChrome() {
3995
- const root = designShadow.querySelector('.root');
3996
- root.innerHTML = '';
3997
-
3998
- // (Panel toggle lives in the global bar — no floating FAB.)
3999
- // Panel
4000
- const panel = document.createElement('aside');
4001
- panel.className = 'panel';
4002
- panel.setAttribute('data-open', designState.open ? 'true' : 'false');
4003
- panel.appendChild(buildDesignHeader());
4004
- const body = document.createElement('div');
4005
- body.className = 'panel-body';
4006
- body.id = 'panel-body';
4007
- panel.appendChild(body);
4008
- root.appendChild(panel);
4009
-
4010
- renderDesignBody();
4011
- }
4012
-
4013
- function buildDesignHeader() {
4014
- const header = document.createElement('div');
4015
- header.className = 'panel-header';
4016
-
4017
- const title = document.createElement('div');
4018
- title.className = 'panel-title';
4019
- title.textContent = 'DESIGN.md';
4020
- header.appendChild(title);
4021
-
4022
- const tabs = document.createElement('div');
4023
- tabs.className = 'tabs';
4024
- for (const t of [['visual', 'Visual'], ['raw', 'Raw']]) {
4025
- const btn = document.createElement('button');
4026
- btn.className = 'tab';
4027
- btn.textContent = t[1];
4028
- btn.setAttribute('data-active', designState.tab === t[0] ? 'true' : 'false');
4029
- btn.addEventListener('click', () => {
4030
- if (designState.tab === t[0]) return;
4031
- designState.tab = t[0];
4032
- saveDesignPrefs();
4033
- renderDesignChrome();
4034
- if (t[0] === 'raw' && designState.raw === null && !designState.loading) {
4035
- fetchDesignSystem(); // raw is part of the same fetch pair
4036
- }
4037
- });
4038
- tabs.appendChild(btn);
4039
- }
4040
- header.appendChild(tabs);
4041
-
4042
- const close = document.createElement('button');
4043
- close.className = 'panel-close';
4044
- close.innerHTML = '&#x2715;';
4045
- close.setAttribute('aria-label', 'Close panel');
4046
- close.addEventListener('click', toggleDesignPanel);
4047
- header.appendChild(close);
4048
-
4049
- return header;
4050
- }
4051
-
4052
- function toggleDesignPanel() {
4053
- designState.open = !designState.open;
4054
- renderDesignChrome();
4055
- updateGlobalBarState();
4056
- if (designState.open && designState.present === null && !designState.loading) {
4057
- fetchDesignSystem();
4058
- }
4059
- }
4060
-
4061
- async function fetchDesignSystem() {
4062
- designState.loading = true;
4063
- designState.error = null;
4064
- renderDesignBody();
4065
- try {
4066
- const [jsonRes, rawRes] = await Promise.all([
4067
- fetch(`http://localhost:${PORT}/design-system.json?token=${TOKEN}`, { cache: 'no-store' }),
4068
- fetch(`http://localhost:${PORT}/design-system/raw?token=${TOKEN}`, { cache: 'no-store' }),
4069
- ]);
4070
- const jsonData = await jsonRes.json();
4071
- designState.present = jsonData.present === true;
4072
- designState.parsed = jsonData.parsed || null;
4073
- designState.sidecar = jsonData.sidecar || null;
4074
- designState.hasMd = !!jsonData.hasMd;
4075
- designState.hasSidecar = !!jsonData.hasSidecar;
4076
- designState.mdNewerThanJson = !!jsonData.mdNewerThanJson;
4077
- designState.raw = designState.present && rawRes.ok ? await rawRes.text() : null;
4078
- designState.error = jsonData.parseError || jsonData.sidecarError || null;
4079
- } catch (err) {
4080
- designState.error = err?.message || 'Failed to load design system.';
4081
- } finally {
4082
- designState.loading = false;
4083
- renderDesignChrome(); // refresh title from data
4084
- }
4085
- }
4086
-
4087
- function renderDesignBody() {
4088
- const body = designShadow.querySelector('#panel-body');
4089
- if (!body) return;
4090
- body.innerHTML = '';
4091
-
4092
- if (designState.loading) {
4093
- body.appendChild(msgDiv('loading', 'Loading design system…'));
4094
- return;
4095
- }
4096
- if (designState.error) {
4097
- body.appendChild(msgDiv('error', designState.error));
4098
- return;
4099
- }
4100
- if (designState.present === false) {
4101
- const empty = document.createElement('div');
4102
- empty.className = 'empty';
4103
- empty.innerHTML = `<strong>No DESIGN.md yet</strong>Create one by running <code>/impeccable document</code> in your terminal, then re-open this panel.`;
4104
- body.appendChild(empty);
4105
- return;
4106
- }
4107
-
4108
- if (designState.tab === 'raw') {
4109
- renderRawTab(body, designState.raw || '');
4110
- return;
4111
- }
4112
-
4113
- // Visual tab — single unified render path.
4114
- if (designState.mdNewerThanJson) body.appendChild(renderStaleHint());
4115
- if (designState.hasMd && !designState.hasSidecar) {
4116
- body.appendChild(renderParsedMdCta());
4117
- }
4118
- renderDesignVisual(body, designState.parsed, designState.sidecar);
4119
- }
4120
-
4121
- function msgDiv(cls, text) {
4122
- const d = document.createElement('div');
4123
- d.className = cls;
4124
- d.textContent = text;
4125
- return d;
4126
- }
4127
-
4128
- function renderStaleHint() {
4129
- const box = document.createElement('div');
4130
- box.className = 'stale';
4131
- box.innerHTML = `
4132
- <span class="stale-dot"></span>
4133
- <span class="stale-text"><strong>DESIGN.md is newer than DESIGN.json.</strong> Run <code>/impeccable document</code> to refresh the sidecar.</span>
4134
- `;
4135
- return box;
4136
- }
4137
-
4138
- function renderParsedMdCta() {
4139
- const box = document.createElement('div');
4140
- box.className = 'parsed-md-cta';
4141
- box.innerHTML = `<strong>Basic view</strong>This panel reads the tokens in your <code>DESIGN.md</code> frontmatter. Running <code>/impeccable document</code> also generates a <code>DESIGN.json</code> sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`;
4142
- return box;
4143
- }
4144
-
4145
- // --- Unified render: merge parsed DESIGN.md frontmatter with sidecar v2 ---
4146
-
4147
- function renderDesignVisual(body, parsed, sidecar) {
4148
- const frontmatter = parsed?.frontmatter || {};
4149
- const extensions = sidecar?.extensions || {};
4150
- const proseColors = parsed?.colors || null;
4151
-
4152
- const colors = buildColorModels(frontmatter.colors, extensions.colorMeta, proseColors);
4153
- if (colors.length) renderColorTiles(body, colors);
4154
-
4155
- const types = buildTypographyModels(frontmatter.typography, extensions.typographyMeta);
4156
- if (types.length) renderTypeTiles(body, types);
4157
-
4158
- const radii = buildRadiiModels(frontmatter.rounded);
4159
- if (radii.length) renderRadiiTile(body, radii);
4160
-
4161
- if (extensions.shadows?.length) renderShadowTiles(body, extensions.shadows);
4162
-
4163
- const components = sidecar?.components || [];
4164
- if (components.length) renderComponentTiles(body, components);
4165
-
4166
- // Narrative: sidecar wins if present (richer, agent-curated). Otherwise
4167
- // synthesize from prose sections.
4168
- const narrative = sidecar?.narrative || synthesizeNarrative(parsed);
4169
- if (narrative.rules?.length) body.appendChild(renderRulesCollapsible(narrative.rules));
4170
- if ((narrative.dos?.length || narrative.donts?.length)) body.appendChild(renderDosDontsCollapsible(narrative));
4171
- if (narrative.overview || narrative.northStar || narrative.keyCharacteristics?.length) {
4172
- body.appendChild(renderOverviewCollapsible(narrative));
4173
- }
4174
-
4175
- if (body.childElementCount === 0) {
4176
- body.appendChild(msgDiv('empty', 'No design system data available.'));
4177
- }
4178
- }
4179
-
4180
- // Frontmatter primitives + sidecar colorMeta → tile-ready color models.
4181
- // A matching prose bullet (when the slug sits in the bullet text) supplies
4182
- // description as a last-resort fallback.
4183
- function buildColorModels(fmColors, colorMeta, proseColors) {
4184
- if (!fmColors) return [];
4185
- const meta = colorMeta || {};
4186
- return Object.entries(fmColors).map(([key, value]) => {
4187
- const m = meta[key] || {};
4188
- return {
4189
- role: m.role || humanizeKey(key),
4190
- name: m.displayName || humanizeKey(key),
4191
- value: value,
4192
- canonical: m.canonical || null,
4193
- description: m.description || findProseDescription(proseColors, key, m.displayName),
4194
- tonalRamp: m.tonalRamp || null,
4195
- };
4196
- });
4197
- }
4198
-
4199
- function buildTypographyModels(fmTypography, typographyMeta) {
4200
- if (!fmTypography) return [];
4201
- const meta = typographyMeta || {};
4202
- return Object.entries(fmTypography).map(([key, spec]) => {
4203
- const m = meta[key] || {};
4204
- const { family, fallback } = splitFontFamily(spec?.fontFamily);
4205
- return {
4206
- role: key,
4207
- name: m.displayName || humanizeKey(key),
4208
- family,
4209
- fallback,
4210
- weight: spec?.fontWeight ?? 400,
4211
- // fontStyle isn't in Stitch's frontmatter schema; the sidecar carries
4212
- // it when a role is rendered in italic (e.g. display italic).
4213
- style: m.style || 'normal',
4214
- sampleSize: spec?.fontSize || '1rem',
4215
- lineHeight: spec?.lineHeight != null ? String(spec.lineHeight) : '',
4216
- letterSpacing: spec?.letterSpacing,
4217
- purpose: m.purpose,
4218
- };
4219
- });
4220
- }
4221
-
4222
- function buildRadiiModels(fmRounded) {
4223
- if (!fmRounded) return [];
4224
- return Object.entries(fmRounded).map(([name, value]) => ({ name, value }));
4225
- }
4226
-
4227
- function splitFontFamily(stack) {
4228
- if (!stack || typeof stack !== 'string') return { family: '', fallback: '' };
4229
- const parts = stack.split(',').map((s) => s.trim().replace(/^['"]|['"]$/g, ''));
4230
- return { family: parts[0] || '', fallback: parts.slice(1).join(', ') };
4231
- }
4232
-
4233
- function humanizeKey(k) {
4234
- return String(k || '').replace(/[-_]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
4235
- }
4236
-
4237
- function findProseDescription(proseColors, key, displayName) {
4238
- if (!proseColors || !proseColors.groups) return null;
4239
- const needles = [key, displayName].filter(Boolean).map((s) => s.toLowerCase());
4240
- for (const g of proseColors.groups) {
4241
- for (const c of g.colors || []) {
4242
- const hay = String(c.name || '').toLowerCase();
4243
- if (hay && needles.some((n) => hay.includes(n) || n.includes(hay))) {
4244
- return c.description || null;
4245
- }
4246
- }
4247
- }
4248
- return null;
4249
- }
4250
-
4251
- function synthesizeNarrative(parsed) {
4252
- if (!parsed) return {};
4253
- const md = parsed;
4254
- return {
4255
- northStar: md.overview?.creativeNorthStar,
4256
- overview: (md.overview?.philosophy || []).join(' '),
4257
- keyCharacteristics: md.overview?.keyCharacteristics || [],
4258
- rules: [
4259
- ...(md.colors?.rules || []).map((r) => ({ ...r, section: 'colors' })),
4260
- ...(md.typography?.rules || []).map((r) => ({ ...r, section: 'typography' })),
4261
- ...(md.elevation?.rules || []).map((r) => ({ ...r, section: 'elevation' })),
4262
- ],
4263
- dos: md.dosDonts?.dos || [],
4264
- donts: md.dosDonts?.donts || [],
4265
- };
4266
- }
4267
-
4268
- function renderColorTiles(body, colors) {
4269
- for (const c of colors) {
4270
- const tile = document.createElement('div');
4271
- tile.className = 'tile c-tile';
4272
- tile.title = 'Click to copy';
4273
- tile.addEventListener('click', () => copyToClipboard(c.value));
4274
-
4275
- const meta = document.createElement('div');
4276
- meta.className = 'tile-meta';
4277
- meta.innerHTML = `<span class="name">${escapeHtml(c.name || c.role || 'Color')}</span><span>${escapeHtml(c.value || '')}</span>`;
4278
- tile.appendChild(meta);
4279
-
4280
- const hero = document.createElement('div');
4281
- hero.className = 'c-hero';
4282
- hero.style.background = c.value;
4283
- tile.appendChild(hero);
4284
-
4285
- const ramp = synthesizeRamp(c);
4286
- if (ramp.length) {
4287
- const r = document.createElement('div');
4288
- r.className = 'c-ramp';
4289
- r.innerHTML = ramp.map((v) => `<span style="background:${cssSafe(v)}"></span>`).join('');
4290
- tile.appendChild(r);
4291
- }
4292
-
4293
- if (c.description) {
4294
- const d = document.createElement('div');
4295
- d.className = 'c-desc';
4296
- d.textContent = c.description;
4297
- tile.appendChild(d);
4298
- }
4299
- body.appendChild(tile);
4300
- }
4301
- }
4302
-
4303
- function synthesizeRamp(c) {
4304
- if (c.tonalRamp?.length) return c.tonalRamp;
4305
- // If base value is OKLCH, synthesize an 8-step ramp across lightness.
4306
- const m = typeof c.value === 'string' && c.value.match(/^oklch\(\s*([\d.]+)%\s+([\d.]+)\s+([\d.]+)\s*(?:\/\s*([\d.]+))?\s*\)$/i);
4307
- if (!m) return [];
4308
- const [, , chroma, hue] = m;
4309
- const steps = [20, 32, 44, 56, 68, 80, 90, 96];
4310
- return steps.map((l) => `oklch(${l}% ${chroma} ${hue})`);
4311
- }
4312
-
4313
- function renderTypeTiles(body, types) {
4314
- for (const t of types) {
4315
- const tile = document.createElement('div');
4316
- tile.className = 'tile t-tile';
4317
-
4318
- const meta = document.createElement('div');
4319
- meta.className = 'tile-meta';
4320
- meta.innerHTML = `<span>${escapeHtml(t.role || '')}</span><span>${escapeHtml(t.weight || '')} ${escapeHtml(t.style === 'italic' ? 'italic' : '')}</span>`;
4321
- tile.appendChild(meta);
4322
-
4323
- const specimen = document.createElement('div');
4324
- specimen.className = 't-specimen';
4325
- specimen.textContent = 'Aa';
4326
- specimen.style.fontFamily = fontStack(t);
4327
- specimen.style.fontWeight = String(t.weight || 400);
4328
- specimen.style.fontStyle = t.style || 'normal';
4329
- specimen.style.fontSize = '56px'; // Fixed specimen size — compare faces, not scales.
4330
- specimen.style.letterSpacing = 'normal';
4331
- specimen.style.textTransform = 'none';
4332
- tile.appendChild(specimen);
4333
-
4334
- // The system's actual sample size for this role, shown as small mono meta below.
4335
- if (t.sampleSize) {
4336
- const scale = document.createElement('div');
4337
- scale.style.cssText = 'font-family:' + MONO + '; font-size: 10px; color:' + DP.meta + '; margin-top: 2px;';
4338
- scale.textContent = t.sampleSize;
4339
- tile.appendChild(scale);
4340
- }
4341
-
4342
- const family = document.createElement('div');
4343
- family.className = 't-family';
4344
- family.textContent = t.family || t.name || '';
4345
- tile.appendChild(family);
4346
-
4347
- if (t.purpose) {
4348
- const p = document.createElement('div');
4349
- p.className = 't-purpose';
4350
- p.textContent = t.purpose;
4351
- tile.appendChild(p);
4352
- }
4353
- body.appendChild(tile);
4354
- }
4355
- }
4356
-
4357
- function fontStack(t) {
4358
- const fam = t.family || '';
4359
- const fb = t.fallback || '';
4360
- if (fam && /[,\s]/.test(fam) && !fam.includes("'") && !fam.includes('"')) {
4361
- return `"${fam}", ${fb}`;
4362
- }
4363
- return fam && fb ? `"${fam}", ${fb}` : (fam || fb);
4364
- }
4365
-
4366
- function renderRadiiTile(body, radii) {
4367
- const tile = document.createElement('div');
4368
- tile.className = 'tile';
4369
- const meta = document.createElement('div');
4370
- meta.className = 'tile-meta';
4371
- meta.innerHTML = `<span class="name">Corner Radii</span><span>${radii.length}</span>`;
4372
- tile.appendChild(meta);
4373
-
4374
- const strip = document.createElement('div');
4375
- strip.className = 'r-strip';
4376
- for (const r of radii) {
4377
- const item = document.createElement('div');
4378
- item.className = 'r-item';
4379
- const s = document.createElement('div');
4380
- s.className = 'r-sample';
4381
- s.style.borderRadius = r.value || '0';
4382
- item.appendChild(s);
4383
- const lbl = document.createElement('div');
4384
- lbl.className = 'r-label';
4385
- lbl.textContent = r.name || '';
4386
- item.appendChild(lbl);
4387
- const val = document.createElement('div');
4388
- val.className = 'r-val';
4389
- val.textContent = r.value || '';
4390
- item.appendChild(val);
4391
- strip.appendChild(item);
4392
- }
4393
- tile.appendChild(strip);
4394
- body.appendChild(tile);
4395
- }
4396
-
4397
- function renderShadowTiles(body, shadows) {
4398
- for (const sh of shadows) {
4399
- const tile = document.createElement('div');
4400
- tile.className = 'tile s-tile';
4401
-
4402
- const meta = document.createElement('div');
4403
- meta.className = 'tile-meta';
4404
- meta.innerHTML = `<span class="name">${escapeHtml(sh.name || 'Shadow')}</span><span>Elevation</span>`;
4405
- tile.appendChild(meta);
4406
-
4407
- const surface = document.createElement('div');
4408
- surface.className = 's-surface';
4409
- surface.style.boxShadow = sh.value || 'none';
4410
- tile.appendChild(surface);
4411
-
4412
- const val = document.createElement('div');
4413
- val.className = 's-value';
4414
- val.textContent = sh.value || '';
4415
- tile.appendChild(val);
4416
-
4417
- if (sh.purpose) {
4418
- const p = document.createElement('div');
4419
- p.className = 's-purpose';
4420
- p.textContent = sh.purpose;
4421
- tile.appendChild(p);
4422
- }
4423
- body.appendChild(tile);
4424
- }
4425
- }
4426
-
4427
- function renderComponentTiles(body, components) {
4428
- // Group consecutive components that share a kind into one tile. This avoids
4429
- // a pile of one-component tiles (e.g., three button variants = three tiles)
4430
- // and reads more like a proper category.
4431
- const groups = groupByKind(components);
4432
-
4433
- for (const group of groups) {
4434
- const tile = document.createElement('div');
4435
- tile.className = 'tile cmp-tile';
4436
-
4437
- const meta = document.createElement('div');
4438
- meta.className = 'tile-meta';
4439
- const groupTitle = group.length === 1
4440
- ? (group[0].name || group[0].kind || 'Component')
4441
- : titleForKind(group[0].kind, group.length);
4442
- meta.innerHTML = `<span class="name">${escapeHtml(groupTitle)}</span><span class="cmp-kind">${escapeHtml(group[0].kind || '')}</span>`;
4443
- tile.appendChild(meta);
4444
-
4445
- for (const c of group) {
4446
- const stage = document.createElement('div');
4447
- stage.className = 'cmp-stage';
4448
-
4449
- // Render the component in its own shadow root so its CSS can't bleed.
4450
- const host = document.createElement('div');
4451
- const sub = host.attachShadow({ mode: 'open' });
4452
- const style = document.createElement('style');
4453
- style.textContent = c.css || '';
4454
- sub.appendChild(style);
4455
- const container = document.createElement('div');
4456
- container.innerHTML = c.html || '';
4457
- sub.appendChild(container);
4458
- stage.appendChild(host);
4459
-
4460
- // Show component name as a sublabel only when the tile groups >1 item,
4461
- // or when the component's display name differs from its kind.
4462
- const showSublabel = group.length > 1;
4463
- if (showSublabel) {
4464
- const lbl = document.createElement('div');
4465
- lbl.className = 'cmp-sublabel';
4466
- lbl.textContent = c.name || '';
4467
- stage.appendChild(lbl);
4468
- }
4469
- tile.appendChild(stage);
4470
- }
4471
-
4472
- // Single shared description if all items carry the same one; otherwise
4473
- // skip — per-item descriptions clutter a grouped tile.
4474
- if (group.length === 1 && group[0].description) {
4475
- const d = document.createElement('div');
4476
- d.className = 'c-desc';
4477
- d.textContent = group[0].description;
4478
- tile.appendChild(d);
4479
- }
4480
- body.appendChild(tile);
4481
- }
4482
- }
4483
-
4484
- function groupByKind(components) {
4485
- const groups = [];
4486
- for (const c of components) {
4487
- const last = groups[groups.length - 1];
4488
- if (last && last[0].kind && c.kind === last[0].kind) {
4489
- last.push(c);
4490
- } else {
4491
- groups.push([c]);
4492
- }
4493
- }
4494
- return groups;
4495
- }
4496
-
4497
- function titleForKind(kind, count) {
4498
- const labels = {
4499
- button: 'Buttons',
4500
- input: 'Inputs',
4501
- nav: 'Navigation',
4502
- chip: 'Chips',
4503
- card: 'Cards',
4504
- custom: 'Components',
4505
- };
4506
- return labels[kind] || (kind ? kind.charAt(0).toUpperCase() + kind.slice(1) + 's' : 'Components');
4507
- }
4508
-
4509
- // --- Collapsibles ---------------------------------------------------------
4510
-
4511
- function buildCollapsible(key, label, count) {
4512
- const wrap = document.createElement('div');
4513
- wrap.className = 'coll';
4514
- wrap.setAttribute('data-open', designState.collapsed[key] ? 'false' : 'true');
4515
-
4516
- const head = document.createElement('button');
4517
- head.className = 'coll-head';
4518
- head.innerHTML = `
4519
- <svg class="coll-chev" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 2.5L8 6 4 9.5"/></svg>
4520
- <span>${escapeHtml(label)}</span>
4521
- ${count != null ? `<span class="coll-count">${escapeHtml(String(count))}</span>` : ''}
4522
- `;
4523
- head.addEventListener('click', () => {
4524
- designState.collapsed[key] = !designState.collapsed[key];
4525
- saveDesignPrefs();
4526
- renderDesignBody();
4527
- });
4528
- wrap.appendChild(head);
4529
-
4530
- const body = document.createElement('div');
4531
- body.className = 'coll-body';
4532
- wrap.appendChild(body);
4533
- return { wrap, body };
4534
- }
4535
-
4536
- function renderRulesCollapsible(rules) {
4537
- const { wrap, body } = buildCollapsible('rules', 'Named Rules', rules.length);
4538
- for (const r of rules) {
4539
- const card = document.createElement('div');
4540
- card.className = 'rule-card';
4541
- const name = document.createElement('div');
4542
- name.className = 'name';
4543
- name.innerHTML = `${escapeHtml(r.name)}${r.section ? `<span class="section">${escapeHtml(r.section)}</span>` : ''}`;
4544
- card.appendChild(name);
4545
- const b = document.createElement('div');
4546
- b.className = 'body';
4547
- b.textContent = r.body || '';
4548
- card.appendChild(b);
4549
- body.appendChild(card);
4550
- }
4551
- return wrap;
4552
- }
4553
-
4554
- function renderDosDontsCollapsible(n) {
4555
- const total = (n.dos?.length || 0) + (n.donts?.length || 0);
4556
- const { wrap, body } = buildCollapsible('dosdonts', "Do's and Don'ts", total);
4557
- const grid = document.createElement('div');
4558
- grid.className = 'dos';
4559
- for (const d of n.dos || []) {
4560
- const el = document.createElement('div');
4561
- el.className = 'do';
4562
- el.innerHTML = inlineMd(d);
4563
- grid.appendChild(el);
4564
- }
4565
- for (const d of n.donts || []) {
4566
- const el = document.createElement('div');
4567
- el.className = 'dont';
4568
- el.innerHTML = inlineMd(d);
4569
- grid.appendChild(el);
4570
- }
4571
- body.appendChild(grid);
4572
- return wrap;
4573
- }
4574
-
4575
- function renderOverviewCollapsible(n) {
4576
- const { wrap, body } = buildCollapsible('overview', 'Overview', null);
4577
- const ov = document.createElement('div');
4578
- ov.className = 'overview-body';
4579
- if (n.northStar) {
4580
- const star = document.createElement('span');
4581
- star.className = 'north-star';
4582
- star.textContent = '“' + n.northStar + '”';
4583
- ov.appendChild(star);
4584
- }
4585
- if (n.overview) {
4586
- const p = document.createElement('p');
4587
- p.innerHTML = inlineMd(n.overview);
4588
- ov.appendChild(p);
4589
- }
4590
- if (n.keyCharacteristics?.length) {
4591
- const ul = document.createElement('ul');
4592
- ul.innerHTML = n.keyCharacteristics.map((k) => `<li>${inlineMd(k)}</li>`).join('');
4593
- ov.appendChild(ul);
4594
- }
4595
- body.appendChild(ov);
4596
- return wrap;
4597
- }
4598
-
4599
- function cssSafe(v) {
4600
- // Strip anything outside valid CSS value chars to prevent injection via
4601
- // DESIGN.json values rendered into inline style strings.
4602
- return String(v).replace(/[<>"'`\n]/g, '');
4603
- }
4604
-
4605
- // --- Raw tab: minimal markdown renderer (subset) --------------------------
4606
-
4607
- function renderRawTab(body, md) {
4608
- const wrap = document.createElement('div');
4609
- wrap.className = 'md';
4610
- wrap.innerHTML = renderMarkdown(md);
4611
- body.appendChild(wrap);
4612
- }
4613
-
4614
- function renderMarkdown(md) {
4615
- const lines = md.split(/\r?\n/);
4616
- const out = [];
4617
- let i = 0;
4618
- let inCode = false;
4619
- let codeBuf = [];
4620
- let paraBuf = [];
4621
- let listBuf = []; // array of { indent, html }
4622
- let listType = null; // 'ul' | 'ol'
4623
-
4624
- const flushPara = () => {
4625
- if (paraBuf.length) {
4626
- out.push(`<p>${inlineMd(paraBuf.join(' '))}</p>`);
4627
- paraBuf = [];
4628
- }
4629
- };
4630
- const flushList = () => {
4631
- if (listBuf.length) {
4632
- out.push(buildListHtml(listBuf, listType));
4633
- listBuf = [];
4634
- listType = null;
4635
- }
4636
- };
4637
- const flushAll = () => { flushPara(); flushList(); };
4638
-
4639
- for (; i < lines.length; i++) {
4640
- const line = lines[i];
4641
-
4642
- // Code fence
4643
- const fence = line.match(/^```(\w*)\s*$/);
4644
- if (fence) {
4645
- if (!inCode) { flushAll(); inCode = true; codeBuf = []; }
4646
- else {
4647
- out.push(`<pre><code>${escapeHtml(codeBuf.join('\n'))}</code></pre>`);
4648
- inCode = false;
4649
- }
4650
- continue;
4651
- }
4652
- if (inCode) { codeBuf.push(line); continue; }
4653
-
4654
- if (line.trim() === '') { flushAll(); continue; }
4655
-
4656
- const hr = line.match(/^\s*(?:---+|\*\*\*+)\s*$/);
4657
- if (hr) { flushAll(); out.push('<hr />'); continue; }
4658
-
4659
- const heading = line.match(/^(#{1,4})\s+(.+)$/);
4660
- if (heading) {
4661
- flushAll();
4662
- const lvl = heading[1].length;
4663
- out.push(`<h${lvl}>${inlineMd(heading[2])}</h${lvl}>`);
4664
- continue;
4665
- }
4666
-
4667
- const bullet = line.match(/^(\s*)([-*])\s+(.+)$/);
4668
- const ordered = line.match(/^(\s*)(\d+)\.\s+(.+)$/);
4669
- if (bullet || ordered) {
4670
- flushPara();
4671
- const m = bullet || ordered;
4672
- const indent = Math.floor(m[1].length / 2);
4673
- const t = bullet ? 'ul' : 'ol';
4674
- if (listType && listType !== t) flushList();
4675
- listType = t;
4676
- listBuf.push({ indent, html: inlineMd(m[3]) });
4677
- continue;
4678
- }
4679
-
4680
- paraBuf.push(line);
4681
- }
4682
- flushAll();
4683
- if (inCode && codeBuf.length) {
4684
- out.push(`<pre><code>${escapeHtml(codeBuf.join('\n'))}</code></pre>`);
4685
- }
4686
- return out.join('\n');
4687
- }
4688
-
4689
- function buildListHtml(items, type) {
4690
- // Nest by indent (one level deep is plenty for DESIGN.md).
4691
- let html = `<${type}>`;
4692
- let lastIndent = 0;
4693
- for (const it of items) {
4694
- if (it.indent > lastIndent) html += `<${type}>`;
4695
- else if (it.indent < lastIndent) html += `</${type}>`.repeat(lastIndent - it.indent);
4696
- html += `<li>${it.html}</li>`;
4697
- lastIndent = it.indent;
4698
- }
4699
- html += `</${type}>`.repeat(lastIndent + 1);
4700
- return html;
4701
- }
4702
-
4703
- function inlineMd(text) {
4704
- // Order matters: escape first, then re-inject tags.
4705
- let s = escapeHtml(text);
4706
- // Code spans
4707
- s = s.replace(/`([^`]+)`/g, (_, code) => `<code>${code}</code>`);
4708
- // Links [text](url)
4709
- s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, t, u) => `<a href="${u}" target="_blank" rel="noopener noreferrer">${t}</a>`);
4710
- // Bold
4711
- s = s.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
4712
- // Italic (only single *…*, skip if inside bold already handled)
4713
- s = s.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, '$1<em>$2</em>');
4714
- return s;
4715
- }
4716
-
4717
- function highlightBold(text) {
4718
- return inlineMd(text);
4719
- }
4720
-
4721
- function escapeHtml(s) {
4722
- return String(s)
4723
- .replace(/&/g, '&amp;')
4724
- .replace(/</g, '&lt;')
4725
- .replace(/>/g, '&gt;')
4726
- .replace(/"/g, '&quot;')
4727
- .replace(/'/g, '&#39;');
4728
- }
4729
-
4730
- function copyToClipboard(text) {
4731
- if (!text) return;
4732
- try {
4733
- navigator.clipboard.writeText(text);
4734
- showToast('Copied: ' + text);
4735
- } catch { /* ignore */ }
4736
- }
4737
-
4738
- // ---------------------------------------------------------------------------
4739
- // Init
4740
- // ---------------------------------------------------------------------------
4741
-
4742
- function init() {
4743
- try { history.scrollRestoration = 'manual'; } catch {}
4744
- initHighlight();
4745
- initAnnotOverlay();
4746
- initBar();
4747
- initActionPicker();
4748
- initParamsPanel();
4749
- initGlobalBar();
4750
- initDesignPanel();
4751
- document.addEventListener('mousemove', handleMouseMove, true);
4752
- document.addEventListener('click', handleClick, true);
4753
- document.addEventListener('keydown', handleKeyDown, true);
4754
- connectSSE();
4755
-
4756
- // Check for an active session to resume (variant wrapper already in DOM after HMR)
4757
- if (!resumeSession()) {
4758
- console.log('[impeccable] Live variant mode ready. Hover over elements to pick one.');
4759
- // SvelteKit (and any framework that hydrates after HTML parse) may add
4760
- // the variant wrapper AFTER init runs. Watch for it and retry resume
4761
- // once it appears. Disconnect on first hit.
4762
- const scout = new MutationObserver(() => {
4763
- const wrapper = document.querySelector('[data-impeccable-variants]');
4764
- if (!wrapper) return;
4765
- scout.disconnect();
4766
- if (resumeSession()) {
4767
- console.log('[impeccable] Resumed deferred session ' + currentSessionId + ' (post-hydration).');
4768
- }
4769
- });
4770
- scout.observe(document.body, { childList: true, subtree: true });
4771
- } else {
4772
- console.log('[impeccable] Resumed active variant session ' + currentSessionId + ' (' + arrivedVariants + '/' + expectedVariants + ' variants).');
4773
- }
4774
- }
4775
-
4776
- if (document.readyState === 'loading') {
4777
- document.addEventListener('DOMContentLoaded', init);
4778
- } else {
4779
- init();
4780
- }
4781
- })();