@ggui-ai/ui-gen 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (308) hide show
  1. package/dist/{adapter-h4_V69LI.d.ts → adapter-DLQ-5COm.d.ts} +1 -1
  2. package/dist/adapters/base.js +2 -27
  3. package/dist/adapters/base.js.map +1 -1
  4. package/dist/adapters/generation-dispatch.d.ts +14 -6
  5. package/dist/adapters/generation-dispatch.js +40 -19260
  6. package/dist/adapters/generation-dispatch.js.map +1 -1
  7. package/dist/adapters/index.d.ts +1 -1
  8. package/dist/adapters/index.js +25 -3561
  9. package/dist/adapters/index.js.map +1 -1
  10. package/dist/adapters/types.js +2 -9
  11. package/dist/adapters/types.js.map +1 -1
  12. package/dist/advanced/index.d.ts +13 -12
  13. package/dist/advanced/index.js +63 -21954
  14. package/dist/advanced/index.js.map +1 -1
  15. package/dist/{bank-Ctdp3FUA.d.ts → bank-LWaiAtaA.d.ts} +1 -1
  16. package/dist/blueprint-validator.js +6 -2301
  17. package/dist/blueprint-validator.js.map +1 -1
  18. package/dist/boilerplate.d.ts +3 -2
  19. package/dist/boilerplate.js +6 -1867
  20. package/dist/boilerplate.js.map +1 -1
  21. package/dist/check/index.d.ts +3 -2
  22. package/dist/check/index.js +6 -1923
  23. package/dist/check/index.js.map +1 -1
  24. package/dist/chunk-33B3BAWS.js +24 -0
  25. package/dist/chunk-33B3BAWS.js.map +1 -0
  26. package/dist/chunk-3HSGA6NI.js +591 -0
  27. package/dist/chunk-3HSGA6NI.js.map +1 -0
  28. package/dist/chunk-3NNNGNTO.js +1707 -0
  29. package/dist/chunk-3NNNGNTO.js.map +1 -0
  30. package/dist/chunk-3T2HMTON.js +79 -0
  31. package/dist/chunk-3T2HMTON.js.map +1 -0
  32. package/dist/chunk-3ZSSDV3L.js +123 -0
  33. package/dist/chunk-3ZSSDV3L.js.map +1 -0
  34. package/dist/chunk-4MBR5GA3.js +99 -0
  35. package/dist/chunk-4MBR5GA3.js.map +1 -0
  36. package/dist/chunk-56NJFD55.js +337 -0
  37. package/dist/chunk-56NJFD55.js.map +1 -0
  38. package/dist/chunk-5AGTY555.js +26 -0
  39. package/dist/chunk-5AGTY555.js.map +1 -0
  40. package/dist/chunk-5EJM7EGY.js +115 -0
  41. package/dist/chunk-5EJM7EGY.js.map +1 -0
  42. package/dist/chunk-5KXCQLLR.js +529 -0
  43. package/dist/chunk-5KXCQLLR.js.map +1 -0
  44. package/dist/chunk-5VBXHVEH.js +171 -0
  45. package/dist/chunk-5VBXHVEH.js.map +1 -0
  46. package/dist/chunk-6KDDEING.js +515 -0
  47. package/dist/chunk-6KDDEING.js.map +1 -0
  48. package/dist/chunk-6VDRWAP6.js +123 -0
  49. package/dist/chunk-6VDRWAP6.js.map +1 -0
  50. package/dist/chunk-732IBFEJ.js +2400 -0
  51. package/dist/chunk-732IBFEJ.js.map +1 -0
  52. package/dist/chunk-73AURS3I.js +46 -0
  53. package/dist/chunk-73AURS3I.js.map +1 -0
  54. package/dist/chunk-77H5SKLX.js +8 -0
  55. package/dist/chunk-77H5SKLX.js.map +1 -0
  56. package/dist/chunk-7QE3AHLE.js +35 -0
  57. package/dist/chunk-7QE3AHLE.js.map +1 -0
  58. package/dist/chunk-A375NNWN.js +11 -0
  59. package/dist/chunk-A375NNWN.js.map +1 -0
  60. package/dist/chunk-AOU33EFS.js +140 -0
  61. package/dist/chunk-AOU33EFS.js.map +1 -0
  62. package/dist/chunk-BA4QYA6E.js +461 -0
  63. package/dist/chunk-BA4QYA6E.js.map +1 -0
  64. package/dist/chunk-DBP764GL.js +3 -0
  65. package/dist/chunk-DBP764GL.js.map +1 -0
  66. package/dist/chunk-DUU3ZJDQ.js +41 -0
  67. package/dist/chunk-DUU3ZJDQ.js.map +1 -0
  68. package/dist/chunk-DXKRBXNU.js +30 -0
  69. package/dist/chunk-DXKRBXNU.js.map +1 -0
  70. package/dist/chunk-FB7OEEAH.js +1467 -0
  71. package/dist/chunk-FB7OEEAH.js.map +1 -0
  72. package/dist/chunk-FLRNMZV2.js +18 -0
  73. package/dist/chunk-FLRNMZV2.js.map +1 -0
  74. package/dist/chunk-FWJIMKFZ.js +2032 -0
  75. package/dist/chunk-FWJIMKFZ.js.map +1 -0
  76. package/dist/chunk-FXRHWYIQ.js +163 -0
  77. package/dist/chunk-FXRHWYIQ.js.map +1 -0
  78. package/dist/chunk-G3PMV62Z.js +33 -0
  79. package/dist/chunk-G3PMV62Z.js.map +1 -0
  80. package/dist/chunk-GAJI7Y3P.js +13 -0
  81. package/dist/chunk-GAJI7Y3P.js.map +1 -0
  82. package/dist/chunk-GYINWMPR.js +2052 -0
  83. package/dist/chunk-GYINWMPR.js.map +1 -0
  84. package/dist/chunk-HD6ABIK7.js +484 -0
  85. package/dist/chunk-HD6ABIK7.js.map +1 -0
  86. package/dist/chunk-HDQVHYO6.js +100 -0
  87. package/dist/chunk-HDQVHYO6.js.map +1 -0
  88. package/dist/chunk-IHUT3SIW.js +7 -0
  89. package/dist/chunk-IHUT3SIW.js.map +1 -0
  90. package/dist/chunk-JDH5EGNZ.js +126 -0
  91. package/dist/chunk-JDH5EGNZ.js.map +1 -0
  92. package/dist/chunk-JMD4RGSV.js +37 -0
  93. package/dist/chunk-JMD4RGSV.js.map +1 -0
  94. package/dist/chunk-KJWGZL5B.js +33 -0
  95. package/dist/chunk-KJWGZL5B.js.map +1 -0
  96. package/dist/chunk-MKMDQFZ7.js +86 -0
  97. package/dist/chunk-MKMDQFZ7.js.map +1 -0
  98. package/dist/chunk-NF6JUZWF.js +85 -0
  99. package/dist/chunk-NF6JUZWF.js.map +1 -0
  100. package/dist/chunk-O2YC6XDC.js +1933 -0
  101. package/dist/chunk-O2YC6XDC.js.map +1 -0
  102. package/dist/chunk-OVLH5ICV.js +29 -0
  103. package/dist/chunk-OVLH5ICV.js.map +1 -0
  104. package/dist/chunk-PBOYHGSD.js +106 -0
  105. package/dist/chunk-PBOYHGSD.js.map +1 -0
  106. package/dist/chunk-PJFH7QWA.js +81 -0
  107. package/dist/chunk-PJFH7QWA.js.map +1 -0
  108. package/dist/chunk-PUH4CXGZ.js +284 -0
  109. package/dist/chunk-PUH4CXGZ.js.map +1 -0
  110. package/dist/chunk-QLXXPGTV.js +18 -0
  111. package/dist/chunk-QLXXPGTV.js.map +1 -0
  112. package/dist/chunk-R5GIGPW2.js +2845 -0
  113. package/dist/chunk-R5GIGPW2.js.map +1 -0
  114. package/dist/chunk-RH4IUSTO.js +83 -0
  115. package/dist/chunk-RH4IUSTO.js.map +1 -0
  116. package/dist/chunk-S2GVXKKY.js +153 -0
  117. package/dist/chunk-S2GVXKKY.js.map +1 -0
  118. package/dist/chunk-TVHGKFJS.js +58 -0
  119. package/dist/chunk-TVHGKFJS.js.map +1 -0
  120. package/dist/chunk-U7GALWLG.js +66 -0
  121. package/dist/chunk-U7GALWLG.js.map +1 -0
  122. package/dist/chunk-VTC263LT.js +256 -0
  123. package/dist/chunk-VTC263LT.js.map +1 -0
  124. package/dist/chunk-WEOXFEVE.js +739 -0
  125. package/dist/chunk-WEOXFEVE.js.map +1 -0
  126. package/dist/chunk-WYHP46LT.js +28 -0
  127. package/dist/chunk-WYHP46LT.js.map +1 -0
  128. package/dist/chunk-XGLO5LGG.js +274 -0
  129. package/dist/chunk-XGLO5LGG.js.map +1 -0
  130. package/dist/chunk-XRBCRKA2.js +29 -0
  131. package/dist/chunk-XRBCRKA2.js.map +1 -0
  132. package/dist/chunk-XW7ADFFV.js +340 -0
  133. package/dist/chunk-XW7ADFFV.js.map +1 -0
  134. package/dist/chunk-YIQ42IUX.js +34 -0
  135. package/dist/chunk-YIQ42IUX.js.map +1 -0
  136. package/dist/chunk-YJZTUY7B.js +101 -0
  137. package/dist/chunk-YJZTUY7B.js.map +1 -0
  138. package/dist/chunk-Z3CXME3K.js +107 -0
  139. package/dist/chunk-Z3CXME3K.js.map +1 -0
  140. package/dist/classifier/index.js +2 -527
  141. package/dist/classifier/index.js.map +1 -1
  142. package/dist/client-JBPCXU7W.js +4 -0
  143. package/dist/client-JBPCXU7W.js.map +1 -0
  144. package/dist/compose.js +3 -367
  145. package/dist/compose.js.map +1 -1
  146. package/dist/cost-tracker-HHGQ4FTW.js +67 -0
  147. package/dist/cost-tracker-HHGQ4FTW.js.map +1 -0
  148. package/dist/{create-ui-generator-BrZHF0cf.d.ts → create-ui-generator-BY-WeaPi.d.ts} +3 -3
  149. package/dist/{design-mode-DGWN_Gkm.d.ts → design-mode-XyOQmzE0.d.ts} +4 -3
  150. package/dist/{dispatch-B07TI30d.d.ts → dispatch-8LzTddKw.d.ts} +3 -2
  151. package/dist/dist-UALN6ZVB.js +543 -0
  152. package/dist/dist-UALN6ZVB.js.map +1 -0
  153. package/dist/dupe-break-X6K7QFIS.js +4 -0
  154. package/dist/dupe-break-X6K7QFIS.js.map +1 -0
  155. package/dist/evaluation/axis-checks/index.d.ts +5 -4
  156. package/dist/evaluation/axis-checks/index.js +6 -1914
  157. package/dist/evaluation/axis-checks/index.js.map +1 -1
  158. package/dist/evaluation/axis-checks/registry.d.ts +3 -2
  159. package/dist/evaluation/axis-checks/registry.js +3 -1844
  160. package/dist/evaluation/axis-checks/registry.js.map +1 -1
  161. package/dist/evaluation/index.d.ts +49 -132
  162. package/dist/evaluation/index.js +302 -6466
  163. package/dist/evaluation/index.js.map +1 -1
  164. package/dist/evaluation/types.d.ts +3 -2
  165. package/dist/evaluation/types.js +2 -5
  166. package/dist/evaluation/types.js.map +1 -1
  167. package/dist/fragments/index.js +2 -338
  168. package/dist/fragments/index.js.map +1 -1
  169. package/dist/{generate-tSO_shQ0.d.ts → generate-DQ4ci90s.d.ts} +1 -1
  170. package/dist/harness/check/runtime-render/index.d.ts +35 -15
  171. package/dist/harness/check/runtime-render/index.js +6 -2390
  172. package/dist/harness/check/runtime-render/index.js.map +1 -1
  173. package/dist/harness/check/runtime-render/render-check-worker.js +4 -1739
  174. package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
  175. package/dist/harness/index.d.ts +3 -2
  176. package/dist/harness/index.js +23 -5126
  177. package/dist/harness/index.js.map +1 -1
  178. package/dist/harness/llm-trace-sink.js +2 -24
  179. package/dist/harness/llm-trace-sink.js.map +1 -1
  180. package/dist/harness/prompts.d.ts +2 -2
  181. package/dist/harness/prompts.js +8 -324
  182. package/dist/harness/prompts.js.map +1 -1
  183. package/dist/harness/result-types.d.ts +6 -5
  184. package/dist/harness/result-types.js +2 -0
  185. package/dist/harness/result-types.js.map +1 -1
  186. package/dist/harness/runtime.d.ts +22 -5
  187. package/dist/harness/runtime.js +10 -2313
  188. package/dist/harness/runtime.js.map +1 -1
  189. package/dist/harness/types-public.d.ts +3 -2
  190. package/dist/harness/types-public.js +16 -2954
  191. package/dist/harness/types-public.js.map +1 -1
  192. package/dist/harness/validator-trace-sink.d.ts +3 -2
  193. package/dist/harness/validator-trace-sink.js +2 -26
  194. package/dist/harness/validator-trace-sink.js.map +1 -1
  195. package/dist/hash.js +2 -39
  196. package/dist/hash.js.map +1 -1
  197. package/dist/hashline-5OKREIRM.js +4 -0
  198. package/dist/hashline-5OKREIRM.js.map +1 -0
  199. package/dist/index.d.ts +50 -11
  200. package/dist/index.js +57 -22015
  201. package/dist/index.js.map +1 -1
  202. package/dist/llm-evaluator-PN3WFCS4.js +653 -0
  203. package/dist/llm-evaluator-PN3WFCS4.js.map +1 -0
  204. package/dist/magic-string.es-2NZTC4GZ.js +1307 -0
  205. package/dist/magic-string.es-2NZTC4GZ.js.map +1 -0
  206. package/dist/patch.d.ts +3 -2
  207. package/dist/patch.js +2 -56
  208. package/dist/patch.js.map +1 -1
  209. package/dist/policy-VCYKGSAC.js +5 -0
  210. package/dist/policy-VCYKGSAC.js.map +1 -0
  211. package/dist/policy.js +2 -35
  212. package/dist/policy.js.map +1 -1
  213. package/dist/primitive-index-2OF6F4TZ.js +131 -0
  214. package/dist/primitive-index-2OF6F4TZ.js.map +1 -0
  215. package/dist/primitive-slice-WTMBLYFJ.js +235 -0
  216. package/dist/primitive-slice-WTMBLYFJ.js.map +1 -0
  217. package/dist/{prompts-BhNUuRoZ.d.ts → prompts-CLZGywMP.d.ts} +1 -1
  218. package/dist/provider-adapter-contract.js +10407 -14204
  219. package/dist/provider-adapter-contract.js.map +1 -1
  220. package/dist/provider-adapter.js +2 -44
  221. package/dist/provider-adapter.js.map +1 -1
  222. package/dist/providers/index.d.ts +89 -1
  223. package/dist/providers/index.js +115 -44
  224. package/dist/providers/index.js.map +1 -1
  225. package/dist/raw-C6KBH5I3.js +141 -0
  226. package/dist/raw-C6KBH5I3.js.map +1 -0
  227. package/dist/raw-FXSBXQTZ.js +7 -0
  228. package/dist/raw-FXSBXQTZ.js.map +1 -0
  229. package/dist/raw-MURDUPGK.js +8 -0
  230. package/dist/raw-MURDUPGK.js.map +1 -0
  231. package/dist/raw-T3GW3UVX.js +8 -0
  232. package/dist/raw-T3GW3UVX.js.map +1 -0
  233. package/dist/render-check-host-ZNIS6BJ5.js +5 -0
  234. package/dist/render-check-host-ZNIS6BJ5.js.map +1 -0
  235. package/dist/sdk-4JNSDNSR.js +6 -0
  236. package/dist/sdk-4JNSDNSR.js.map +1 -0
  237. package/dist/sdk-DIVJMHCX.js +7 -0
  238. package/dist/sdk-DIVJMHCX.js.map +1 -0
  239. package/dist/sdk-QRJOCRTS.js +7 -0
  240. package/dist/sdk-QRJOCRTS.js.map +1 -0
  241. package/dist/tag-balance-CTHCTRJ7.js +123 -0
  242. package/dist/tag-balance-CTHCTRJ7.js.map +1 -0
  243. package/dist/tools/get-primitives-ts.d.ts +1 -1
  244. package/dist/tools/get-primitives-ts.js +3 -1
  245. package/dist/tools/get-primitives-ts.js.map +1 -1
  246. package/dist/tools/render-check-worker.js +3 -40
  247. package/dist/tools/render-check-worker.js.map +1 -1
  248. package/dist/{types-public-h8IMnYuh.d.ts → types-public-hYlrerZh.d.ts} +35 -2
  249. package/dist/validation/index.d.ts +2 -2
  250. package/dist/validation/index.js +3 -498
  251. package/dist/validation/index.js.map +1 -1
  252. package/dist/{visual-evaluator-C2strxqD.d.ts → visual-evaluator-KskajxD5.d.ts} +14 -4
  253. package/dist/visual-evaluator-TGEXYQTP.js +12 -0
  254. package/dist/visual-evaluator-TGEXYQTP.js.map +1 -0
  255. package/dist/workflows.d.ts +3 -2
  256. package/dist/workflows.js +2 -151
  257. package/dist/workflows.js.map +1 -1
  258. package/package.json +20 -16
  259. package/src/adapters/claude/sdk.ts +4 -2
  260. package/src/adapters/claude/tool-bridge.ts +2 -1
  261. package/src/adapters/generation-dispatch.ts +9 -0
  262. package/src/adapters/openai/raw.ts +4 -0
  263. package/src/adapters/openai/sdk.ts +6 -1
  264. package/src/adapters/openai/tokens.ts +21 -7
  265. package/src/adapters/tool-bridge.ts +1 -1
  266. package/src/adapters/tools.ts +1 -1
  267. package/src/advanced/feedback.ts +1 -2
  268. package/src/advanced/generator.ts +4 -3
  269. package/src/boilerplate/free-design-prompt.ts +7 -0
  270. package/src/boilerplate/generate.ts +18 -5
  271. package/src/boilerplate/hard-sections.ts +8 -3
  272. package/src/boilerplate/react-hooks.ts +56 -0
  273. package/src/boilerplate/system-prompt.ts +12 -3
  274. package/src/check/run-tier0.ts +233 -8
  275. package/src/check/type-checker.ts +128 -16
  276. package/src/coding-agent/tools.ts +26 -3
  277. package/src/design-mode.ts +4 -9
  278. package/src/evaluation/axis-checks/checks/state-merge.ts +2 -4
  279. package/src/evaluation/axis-checks/checks/state-payload.ts +1 -28
  280. package/src/evaluation/axis-checks/checks/tint.ts +197 -0
  281. package/src/evaluation/axis-checks/helpers.ts +121 -0
  282. package/src/evaluation/axis-checks/registry.ts +2 -0
  283. package/src/evaluation/criteria/__fixtures__/edge-v2-draw2.json +4629 -0
  284. package/src/evaluation/criteria/bank.ts +1 -1
  285. package/src/evaluation/criteria/edge-instruments.ts +14 -0
  286. package/src/evaluation/criteria/edge-probe-v2.ts +352 -0
  287. package/src/evaluation/criteria/edge-v2.ts +242 -0
  288. package/src/evaluation/criteria/resolve.ts +24 -5
  289. package/src/evaluation/index.ts +3 -5
  290. package/src/evaluation/mcp-server.ts +0 -50
  291. package/src/evaluation/types-public.ts +6 -1
  292. package/src/evaluation/visual-evaluator.ts +161 -30
  293. package/src/harness/check/runtime-render/index.ts +28 -4
  294. package/src/harness/check/runtime-render/render-check.ts +20 -21
  295. package/src/harness/claude-agent-sdk-loader.ts +93 -0
  296. package/src/harness/claude-code-login-query.ts +1 -1
  297. package/src/harness/coding/generate-task-runner.ts +1 -0
  298. package/src/harness/coding/run-coding-turn.ts +3 -0
  299. package/src/harness/coding/run-eval-round.ts +9 -0
  300. package/src/harness/llm-router.ts +50 -2
  301. package/src/harness/runtime.ts +17 -0
  302. package/src/index.ts +9 -0
  303. package/src/providers/anthropic-tool-call.ts +263 -0
  304. package/src/providers/index.ts +13 -0
  305. package/src/tools/get-primitives-ts.ts +1 -1
  306. package/src/tools/render-check.ts +4 -4
  307. package/src/validation/primitives.ts +2 -1
  308. package/src/evaluation/loop.ts +0 -218
@@ -445,9 +445,10 @@ EXACT primitive prop values (other values are silently ignored — the design sy
445
445
  - \`<Icon name="..." tone="...">\` / \`<Spinner tone="...">\` / \`<Link href="..." tone="...">\` / \`<Divider tone="...">\` — same \`tone\` vocabulary as Text. Default = \`currentColor\` (Icon), primary-tinted (Spinner / Link), outlineVariant (Divider). Use \`tone="inherit"\` when you want the element to track the parent's foreground color (e.g. an Icon next to muted text).
446
446
  - \`<Button variant="...">\` — \`primary | secondary | outline | ghost | danger\`. Sizes \`xs | sm | md | lg\`. Use \`primary\` for the main action — renders in the brand color automatically.
447
447
  - \`<Card padding="lg" shadow="md" radius="lg" surface="default">\` — shadow \`none|sm|md|lg|xl\`, radius \`none|sm|md|lg|xl\`. \`surface\` slot picks the fill: \`default | elevated | sunken | accent | inverted | hero | transparent\`. Use \`inverted\` for dark testimonial-style cards on a light theme; \`accent\` for branded fills; \`hero\` for the one hero / greeting panel (the theme's hero ground: brand-tinted on a light host, the ink pair on a dark one). \`inverted\` and \`hero\` OWN their ground — never set \`background\` in their \`style\` (tier-0 fails it); the surface paints it.
448
- - \`<Box surface="...">\` — same surface slots as Card. \`surface\` is the ONLY theme-tracking background prop; the legacy \`background="..."\` prop has been removed. For non-theme-mapped brand colors (a partner's exact brand hex like Stripe purple), use the typed escape \`<Box assetColor="#635BFF" assetSemantic="stripe-brand-purple">\` — both props are required, and \`assetSemantic\` MUST be a non-empty human-readable label. Tier-0 self-check rejects every other hex / rgba on Box.
448
+ - \`<Box surface="...">\` — same surface slots as Card. \`surface\` (layout slots) and \`tint\` (data categories) are the ONLY theme-tracking background props; the legacy \`background="..."\` prop has been removed. To colour by a DATA CATEGORY (series, lanes, kinds, tags, element groups), use \`tint="tint-1"\`…\`"tint-8"\` (see "Categorical tint" below). For non-theme-mapped brand colors (a partner's exact brand hex like Stripe purple), use the typed escape \`<Box assetColor="#635BFF" assetSemantic="stripe-brand-purple">\` — both props are required, and \`assetSemantic\` MUST be a non-empty human-readable label. Tier-0 self-check rejects every other hex / rgba on Box.
449
449
  - \`<Stack gap="...">\` / \`<Row gap="...">\` — \`gap\` takes the **spacing scale** (next bullet). \`align\` (cross-axis) is ONLY \`start | center | end | stretch\` and \`justify\` (main-axis) is ONLY \`start | center | end | between | around | evenly\` — NEVER the raw CSS values \`flex-start\` / \`flex-end\` / \`space-between\`, which are type errors.
450
450
  - **Spacing scale** — \`gap\` (Stack / Row / Grid) and \`padding\` (Card / Box / Container) take a t-shirt size: \`none | xs | sm | md | lg | xl | 2xl\`. Each resolves to a \`--ggui-spacing-*\` token (xs≈4px, sm≈8px, md≈16px, lg≈24px, xl≈32px, 2xl≈48px). A bare number is treated as pixels. NEVER pass a raw CSS length such as \`gap="8px"\` — it is silently dropped by the browser and the gap collapses to 0; use the scale name (\`gap="sm"\`).
451
+ - **One content edge per card** — on \`Card\` and \`Box\`, \`padding\` sets the VERTICAL rhythm; the card owns the horizontal inset. Every block's content sits on the card's one left edge, and a filled or bordered surface steps its own content in by exactly one inset. So never use \`paddingX\` to push content sideways or a padded unstyled wrapper to indent a list. A badge, date or action on the right of a line belongs in a \`<Row justify="between">\` with content on its left, never alone on its own line.
451
452
  - \`<Grid columns={N} gap="md">\` — 2-D layout (rows AND columns). Reach for it for card galleries, stat grids and dashboards — NEVER hand-roll \`style={{ display: 'grid' }}\`. When the request names exact per-breakpoint counts ("3 per row on desktop, 1 on mobile"), pass a map: \`<Grid columns={{ base: 1, md: 3 }}>\` (breakpoints \`sm\`/\`md\`/\`lg\`/\`xl\`; the design system emits the media queries). For an open-ended gallery where any column count is fine, use \`<Grid minColumnWidth={220}>\` — it fits as many equal columns as the width allows. \`radius\` (Card / Box / Image) takes the scale \`none | sm | md | lg | xl\`.
452
453
  - \`<Stat label="…" value="…" delta="+12%" trend="up">\` — KPI display (label + big value + trend-coloured delta + optional \`icon\`). \`trend\` is \`up | down | neutral\` (delta renders green / red / muted). Reach for it for any "show a number" UI; drop several into a \`<Grid>\` for a stat grid instead of hand-building label+value pairs.
453
454
  - \`<Stepper steps={STEPS} current={step} />\` — display-only step indicator for wizards/checkouts. \`steps\` is a top-level \`const\` array of labels; \`current\` is YOUR zero-indexed \`useState\` value; \`orientation\` is \`horizontal | vertical\`; optional \`onStepClick={(i) => …}\` makes steps clickable. Stepper never owns navigation state — your Next/Back handlers move \`current\`.
@@ -457,6 +458,14 @@ EXACT primitive prop values (other values are silently ignored — the design sy
457
458
 
458
459
  **Asset-color escape (Box only).** When you genuinely need a non-theme color — a partner's exact brand hex (Stripe purple \`#635BFF\`, Slack aubergine \`#4A154B\`), a fixed product surface — use \`<Box assetColor="#635BFF" assetSemantic="stripe-brand-purple">…</Box>\`. The \`assetSemantic\` is REQUIRED and MUST be a non-empty human-readable label that documents intent. Tier-0 allows hex inside this typed pair; one without the other fails the check. Reach for \`surface\` first — \`assetColor\` is rare.
459
460
 
461
+ **Categorical tint (data categories).** When cells, rows or swatches must be told apart by a data category — element groups in a periodic table, lanes on a board, series in a chart legend, tags — set \`tint="tint-1"\` … \`tint="tint-8"\` on the Box or Card. The eight steps are theme colours: they follow the operator's theme in light and dark, and the tinted surface sets its own readable text colour. Rules:
462
+ - Tints are for **data categories only** — never for state (a status takes live, draft or signal, not a tint) and never as decoration.
463
+ - The category is **always also in words** — a label in the cell or a legend on the card. Colour is never the only way to tell categories apart.
464
+ - **Text on a tint is the theme's ink only, one short line** — a label, a symbol or a value, never \`tone="subtle"\` or muted text. Secondary or muted text sits beside the cell or in the legend, never on the tint.
465
+ - **A category keeps its tint for the life of the card.** Assign tints deterministically from the data, in the order the categories are first listed or sorted, never by render order, so an update or repaint never recolours a category.
466
+ - **More than eight categories:** group them to eight before assigning, or show the category in words only.
467
+ - Never put a theme colour through \`assetColor\` — that escape is only for an exact external brand hex.
468
+
460
469
  ## Accessibility (REQUIRED)
461
470
 
462
471
  The design-system primitives are accessible by construction — they emit their own roles, labels, keyboard handlers, and error wiring. Your job is to USE them correctly, NOT to re-declare ARIA on top of them.
@@ -481,7 +490,7 @@ MANDATORY:
481
490
  2. NEVER use CSS gradients with custom colors. If you need a gradient: \`linear-gradient(to bottom, var(--ggui-color-primary-500), var(--ggui-color-primary-700))\`.
482
491
  3. NEVER invent your own palette. The system provides primary, neutral, success, warning, error, and info — use ONLY these.
483
492
  4. NEVER include literal fallback values in ANY token reference — colors, spacing, typography, radius, shadows alike. Write \`var(--ggui-spacing-4)\` bare, never \`var(--ggui-spacing-4, 16px)\`; \`var(--ggui-color-primary-600)\` bare, never \`var(--ggui-color-primary-600, #0284c7)\`. The runtime injects every token; a literal fallback paints the wrong value exactly when the operator's theme matters most.
484
- 5. NEVER set an inline \`border\`, \`borderColor\`, \`borderWidth\` or \`borderStyle\` on a kit control — \`Button\`, \`Input\`, \`Select\`, \`TextArea\`, \`Checkbox\`, \`RadioGroup\`, \`Toggle\`, \`SearchField\`. The control draws its own boundary, and it clears 3:1 on its surface in every theme: an outline chip is \`<Button variant="outline">\` exactly as it comes (its edge is \`controlAccentOutline\`). Where you draw a control-like edge on your own element, use \`var(--ggui-color-controlOutline)\`. A ramp step such as \`primary-200\` as a control's border is nearly invisible (about 1.1:1).
493
+ 5. NEVER set an inline \`border\`, \`borderColor\`, \`borderWidth\` or \`borderStyle\` on a kit control — \`Button\`, \`Input\`, \`Select\`, \`TextArea\`, \`Checkbox\`, \`RadioGroup\`, \`Toggle\`, \`SearchField\`. The control draws its own boundary, and it clears 3:1 on its surface in every theme: an outline chip is \`<Button variant="outline">\` exactly as it comes (its edge is \`controlAccentOutline\`). Where you draw a control-like edge on your own element, use \`var(--ggui-color-controlOutline)\`. A ramp step such as \`primary-200\` as a control's border is nearly invisible (about 1.1:1). Its ink and fill are the variant's too: NEVER set an inline \`color\`, \`background\` or \`backgroundColor\` on a kit control, a token as much as a literal — pick the variant that carries the look.
485
494
 
486
495
  Token categories:
487
496
  - Brand fills: \`var(--ggui-color-primary-600)\`, \`var(--ggui-color-primary-50)\`
@@ -584,7 +593,7 @@ If your component is 100% purple text on purple backgrounds, you've lost the eye
584
593
  ### Visual rhythm — vary your card treatments
585
594
 
586
595
  A row of identical flat tiles feels monotone. Use card-treatment variation to create rhythm:
587
- - **Hero card**: \`<Card surface="hero" padding="xl" shadow="lg" radius="xl">\` — the theme's hero ground (brand-tinted on a light host, the ink pair on a dark one); anchors the eye
596
+ - **Hero card**: \`<Card surface="hero" padding="xl" shadow="lg" radius="xl">\` — the theme's hero ground (brand-tinted on a light host, the ink pair on a dark one); anchors the eye.
588
597
  - **Stat tiles**: \`<Card padding="md" shadow="sm" radius="md">\` with surface bg — secondary
589
598
  - **Inline rows / list items**: no card chrome at all, just \`<Stack gap="sm">\` with dividers — tertiary
590
599
 
@@ -571,6 +571,171 @@ function detectComponentNeverReturns(
571
571
  * Wraps the same logic as runSelfChecks (adapters/tools.ts) but emits EvalIssue[].
572
572
  * Also runs contract validation when contract are provided.
573
573
  */
574
+ /**
575
+ * ggui#1977 / #1981: an opening band that lost its edge. The card keeps one content edge in every fit; only a hero
576
+ * Box that is the root's FIRST child, a hero Card that is its ONLY child (the runtime's legacy inference) or a root
577
+ * child that declares `bleed` meets the card's edge (design's `EXPANDED_FRAME_BLEEDS`). The shapes that miss it (the
578
+ * #1981 corpus analyzer's version of `hero-not-first` read 14 of 14 true on dev + staging, by the CDO; the cases
579
+ * added since, and `hero-card-first`, are pinned by unit tests and were not counted on the corpus, which is compiled
580
+ * JS this source reader does not parse):
581
+ * - `hero-not-first`: a band (a `surface="hero"` Box with no edge of its own: no `radius` but `none`, no style
582
+ * radius, shadow or border; a Card is a tile, never a band) that is a root child but not the first and does not
583
+ * declare `bleed`, OR the first child of a plain wrapper (a Stack, Box, Container or div that paints no ground
584
+ * and is not a horizontal Stack) that opens the root, `bleed` or not: the runtime honours `bleed` on root
585
+ * children only. It renders inside the inset, a box floating in the card.
586
+ * - `hero-card-first` (#1981 c.6314477145): a `surface="hero"` Card without `bleed` that is the root's first child
587
+ * AMONG siblings. A Card there is a tile and keeps the inset; if it is meant as the opening band, the band is a
588
+ * Box. Never a sole child: a Card that is the root's only child is the card's body and keeps its bleed.
589
+ * A root that already opens on a band is left alone (a second band is not this shape). Conditional children are read
590
+ * where they render: `a && <El/>`, `a || <El/>` and every ternary branch. When the band renders conditionally, or
591
+ * every slot before it does, the warning says so. The component is `export default function`, or
592
+ * `export default C` naming a function or an arrow in the file. Read from the source, never from a helper's JSX.
593
+ * A `warn`, not a `fail`.
594
+ * Retired (#1981, the CDO's read): `painted-band`, 7 of 8 served hits were contained Cards (tiles), not bands.
595
+ */
596
+ function detectBandEdgeLost(
597
+ sourceCode: string,
598
+ ): { readonly shape: 'hero-not-first' | 'hero-card-first'; readonly line: number; readonly conditional: boolean } | null {
599
+ const sf = ts.createSourceFile('source.tsx', sourceCode, ts.ScriptTarget.Latest, /* setParentNodes */ true, ts.ScriptKind.TSX);
600
+ type El = ts.JsxElement | ts.JsxSelfClosingElement;
601
+ const unwrap = (e: ts.Expression): ts.Expression => (ts.isParenthesizedExpression(e) ? unwrap(e.expression) : e);
602
+ const isEl = (n: ts.Node): n is El => ts.isJsxElement(n) || ts.isJsxSelfClosingElement(n);
603
+ const opening = (el: El): ts.JsxOpeningElement | ts.JsxSelfClosingElement => (ts.isJsxElement(el) ? el.openingElement : el);
604
+ const tag = (el: El): string => opening(el).tagName.getText(sf);
605
+ const attr = (el: El, name: string): ts.JsxAttribute | undefined =>
606
+ opening(el).attributes.properties.find((a): a is ts.JsxAttribute => ts.isJsxAttribute(a) && a.name.getText(sf) === name);
607
+ const stringAttr = (el: El, name: string): string | undefined => {
608
+ const a = attr(el, name);
609
+ if (a?.initializer === undefined) return undefined;
610
+ if (ts.isStringLiteral(a.initializer)) return a.initializer.text;
611
+ if (ts.isJsxExpression(a.initializer) && a.initializer.expression !== undefined && ts.isStringLiteral(a.initializer.expression)) {
612
+ return a.initializer.expression.text;
613
+ }
614
+ return undefined;
615
+ };
616
+ const flagOn = (el: El, name: string): boolean => {
617
+ const a = attr(el, name);
618
+ if (a === undefined) return false;
619
+ if (a.initializer === undefined) return true;
620
+ return !(ts.isJsxExpression(a.initializer) && a.initializer.expression?.kind === ts.SyntaxKind.FalseKeyword);
621
+ };
622
+ type Slot = { readonly alts: El[]; readonly conditional: boolean };
623
+ /**
624
+ * The elements a JSX child can render: itself, or every element branch of `a && <El/>`, `a || <El/>` and
625
+ * `a ? <El/> : <Other/>` (nested ternaries included).
626
+ */
627
+ const branches = (x: ts.Expression): El[] => {
628
+ const e = unwrap(x);
629
+ if (isEl(e)) return [e];
630
+ const guard = ts.isBinaryExpression(e) && (e.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || e.operatorToken.kind === ts.SyntaxKind.BarBarToken);
631
+ if (guard) return branches(e.right);
632
+ if (ts.isConditionalExpression(e)) return [...branches(e.whenTrue), ...branches(e.whenFalse)];
633
+ return [];
634
+ };
635
+ /** One rendered slot per JSX child; an expression child is conditional and lists the elements it may render. */
636
+ const slots = (el: El): Slot[] => {
637
+ if (!ts.isJsxElement(el)) return [];
638
+ const out: Slot[] = [];
639
+ for (const n of el.children) {
640
+ if (isEl(n)) out.push({ alts: [n], conditional: false });
641
+ else if (ts.isJsxExpression(n) && n.expression !== undefined) {
642
+ const alts = branches(n.expression);
643
+ if (alts.length > 0) out.push({ alts, conditional: true });
644
+ }
645
+ }
646
+ return out;
647
+ };
648
+ const lineOf = (el: El): number => sf.getLineAndCharacterOfPosition(el.getStart(sf)).line + 1;
649
+
650
+ const styleKeys = (el: El): string[] => {
651
+ const style = attr(el, 'style')?.initializer;
652
+ if (style === undefined || !ts.isJsxExpression(style) || style.expression === undefined) return [];
653
+ const obj = unwrap(style.expression);
654
+ if (!ts.isObjectLiteralExpression(obj)) return [];
655
+ return obj.properties.filter(ts.isPropertyAssignment).map((pr) => pr.name.getText(sf).replace(/['"]/g, ''));
656
+ };
657
+ const paints = (el: El): boolean => {
658
+ const surface = stringAttr(el, 'surface');
659
+ if (surface !== undefined && surface !== 'transparent') return true;
660
+ if (attr(el, 'tint') !== undefined) return true;
661
+ return styleKeys(el).some((k) => k === 'background' || k === 'backgroundColor');
662
+ };
663
+ /** A Box with an edge of its own is a tile: a `radius` other than `none`, or a style radius, shadow or border. */
664
+ const hasOwnEdge = (el: El): boolean => {
665
+ if (attr(el, 'radius') !== undefined && stringAttr(el, 'radius') !== 'none') return true;
666
+ return styleKeys(el).some((k) => k === 'borderRadius' || k === 'boxShadow' || /^border(Top|Bottom|Left|Right)?(Width|Style|Color)?$/.test(k));
667
+ };
668
+ /** A hero Box with no edge of its own: a band. A Card is a tile, never a band. */
669
+ const isHeroBand = (el: El): boolean => tag(el) === 'Box' && stringAttr(el, 'surface') === 'hero' && !hasOwnEdge(el);
670
+ const isHeroCard = (el: El): boolean => tag(el) === 'Card' && stringAttr(el, 'surface') === 'hero' && !flagOn(el, 'bleed');
671
+ /** A wrapper that only stacks its children: it paints nothing and does not lay them out side by side. */
672
+ const isPlainWrapper = (el: El): boolean =>
673
+ ['Stack', 'Box', 'Container', 'div'].includes(tag(el)) && !paints(el) && !(tag(el) === 'Stack' && stringAttr(el, 'direction') === 'horizontal');
674
+
675
+ // The default export's component: `export default function`, or `export default C` naming a function or a
676
+ // `const C = (…) => …` in this file, or an inline arrow / function expression.
677
+ type Fn = ts.FunctionDeclaration | ts.ArrowFunction | ts.FunctionExpression;
678
+ const byName = new Map<string, Fn>();
679
+ let component: Fn | undefined;
680
+ for (const st of sf.statements) {
681
+ if (ts.isFunctionDeclaration(st)) {
682
+ if (st.name !== undefined) byName.set(st.name.text, st);
683
+ if (st.modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword)) component = st;
684
+ } else if (ts.isVariableStatement(st)) {
685
+ for (const d of st.declarationList.declarations) {
686
+ const init = d.initializer === undefined ? undefined : unwrap(d.initializer);
687
+ if (ts.isIdentifier(d.name) && init !== undefined && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) byName.set(d.name.text, init);
688
+ }
689
+ }
690
+ }
691
+ for (const st of sf.statements) {
692
+ if (component !== undefined || !ts.isExportAssignment(st) || st.isExportEquals === true) continue;
693
+ const e = unwrap(st.expression);
694
+ if (ts.isIdentifier(e)) component = byName.get(e.text);
695
+ else if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) component = e;
696
+ }
697
+ if (component?.body === undefined) return null;
698
+ let root: El | undefined;
699
+ if (!ts.isBlock(component.body)) {
700
+ const e = unwrap(component.body);
701
+ if (isEl(e)) root = e;
702
+ } else {
703
+ for (const st of component.body.statements) {
704
+ if (ts.isReturnStatement(st) && st.expression !== undefined) {
705
+ const e = unwrap(st.expression);
706
+ if (isEl(e)) root = e;
707
+ }
708
+ }
709
+ }
710
+ if (root === undefined) return null;
711
+
712
+ const kids = slots(root);
713
+ const first = kids[0];
714
+ if (first === undefined) return null;
715
+ // A root that already opens on a band (always rendered) has its band; a second one is not this shape.
716
+ if (!first.conditional && first.alts.some(isHeroBand)) return null;
717
+ // A hero Card first AMONG siblings is a tile: it keeps the inset. Never a sole child (that Card is the card's body).
718
+ const heroCard = kids.length > 1 ? first.alts.find(isHeroCard) : undefined;
719
+ if (heroCard !== undefined) return { shape: 'hero-card-first', line: lineOf(heroCard), conditional: first.conditional };
720
+ // A band that is a later root child: the root's inset holds it unless it declares `bleed` (honoured on a root
721
+ // child). Conditional when it, or every slot before it, renders conditionally.
722
+ for (let i = 1; i < kids.length; i++) {
723
+ const k = kids[i]!;
724
+ const band = k.alts.find((a) => isHeroBand(a) && !flagOn(a, 'bleed'));
725
+ if (band !== undefined) {
726
+ return { shape: 'hero-not-first', line: lineOf(band), conditional: k.conditional || kids.slice(0, i).every((s) => s.conditional) };
727
+ }
728
+ }
729
+ // A band first inside a plain wrapper that opens the root: `bleed` is not honoured that deep, so it warns either way.
730
+ const opener = first.alts.find(isPlainWrapper);
731
+ const inner = opener === undefined ? undefined : slots(opener)[0];
732
+ const wrapped = inner?.alts.find(isHeroBand);
733
+ if (wrapped !== undefined && inner !== undefined) {
734
+ return { shape: 'hero-not-first', line: lineOf(wrapped), conditional: first.conditional || inner.conditional };
735
+ }
736
+ return null;
737
+ }
738
+
574
739
  /** The kit controls whose boundary the design system draws (ggui#1697). */
575
740
  const KIT_CONTROLS = ['Button', 'Input', 'Select', 'TextArea', 'Checkbox', 'RadioGroup', 'Toggle', 'SearchField'] as const;
576
741
 
@@ -600,18 +765,22 @@ function kitControlTags(source: string): { name: string; attrs: string; line: nu
600
765
  return out;
601
766
  }
602
767
 
603
- /** A border key (not a radius) in the tag's inline style, or in a same-file object its `style={name}` points at. */
604
- function inlineBorderKey(attrs: string, source: string): string | null {
605
- const BORDER = /\b(border(?:Top|Right|Bottom|Left)?(?:Color|Width|Style)?)\s*:/;
768
+ /** A border key, never a radius (ggui#1697). */
769
+ const CONTROL_BORDER_KEY = /\b(border(?:Top|Right|Bottom|Left)?(?:Color|Width|Style)?)\s*:/;
770
+ /** An ink or fill key (ggui#1966): a kit control's variant carries its ink and its fill, token or not. */
771
+ const CONTROL_COLOR_KEY = /\b(color|background|backgroundColor)\s*:/;
772
+
773
+ /** A matching key in the tag's inline style, or in a same-file object its `style={name}` points at. */
774
+ function inlineStyleKey(attrs: string, source: string, key: RegExp): string | null {
606
775
  const style = attrs.match(/\bstyle=\{\{([\s\S]*)\}\}/);
607
776
  if (style !== null) {
608
- const hit = (style[1] ?? '').match(BORDER);
777
+ const hit = (style[1] ?? '').match(key);
609
778
  if (hit !== null) return hit[1] ?? null;
610
779
  }
611
780
  const named = attrs.match(/\bstyle=\{\s*([A-Za-z_$][\w$]*)\s*\}/);
612
781
  if (named === null) return null;
613
782
  const decl = source.match(new RegExp(`\\b(?:const|let)\\s+${named[1]}\\b[^=]*=\\s*\\{([^{}]*)\\}`));
614
- const hit = decl !== null ? (decl[1] ?? '').match(BORDER) : null;
783
+ const hit = decl !== null ? (decl[1] ?? '').match(key) : null;
615
784
  return hit !== null ? hit[1] ?? null : null;
616
785
  }
617
786
 
@@ -703,6 +872,27 @@ export async function runTier0Checks(
703
872
  const assetSemanticAttr = tagAttrs.match(/\bassetSemantic\s*=\s*["']([^"']*)["']/);
704
873
  const startLine = sourceCode.slice(0, match.index ?? 0).split('\n').length;
705
874
  const tagLineCount = match[0].split('\n').length;
875
+ // ggui#1846 — the escape is for an exact EXTERNAL brand colour. A theme
876
+ // token through it (`assetColor="var(--ggui-…)"`, or an expression whose
877
+ // text carries one) bypasses the door the theme already has: a categorical
878
+ // fill is `tint`, a theme-tracking fill is `surface`. Deterministic from the
879
+ // tag's own text, and always satisfiable, so it fails.
880
+ if (/var\(\s*--ggui-/.test(assetColorAttr[0])) {
881
+ issues.push({
882
+ tier: 0,
883
+ result: 'fail',
884
+ category: 'tokens',
885
+ subcategory: 'asset-color-theme-token',
886
+ severity: 'critical',
887
+ description:
888
+ 'Box `assetColor` carries a theme token (`var(--ggui-…)`) — the brand-color escape is only for an exact external brand hex, and the theme already has its own doors.',
889
+ fix:
890
+ 'To colour by a data category, use `tint="tint-1"` … `tint="tint-8"` (one step per category, in the data\'s order). ' +
891
+ 'For a theme-tracking fill, use `surface="..."`. Drop `assetColor` and `assetSemantic`.',
892
+ line: startLine,
893
+ });
894
+ continue;
895
+ }
706
896
  // Both attributes must be present, AND assetSemantic must be a
707
897
  // non-empty string literal. A `{expr}` value for assetSemantic is
708
898
  // treated as not-statically-verifiable; the escape is opt-in
@@ -722,8 +912,8 @@ export async function runTier0Checks(
722
912
  '`assetSemantic` is a human-readable label that documents why this color bypasses the theme.',
723
913
  fix:
724
914
  'Pair the `assetColor` with a non-empty `assetSemantic` literal, e.g. ' +
725
- '`<Box assetColor="#635BFF" assetSemantic="stripe-brand-purple">`. Reach for `surface="..."` first ' +
726
- 'whenever the color SHOULD track the operator\'s theme.',
915
+ '`<Box assetColor="#635BFF" assetSemantic="stripe-brand-purple">`. The escape is only for an exact external brand hex: ' +
916
+ 'to colour by a data category use `tint="tint-1"` … `tint="tint-8"`, and use `surface="..."` whenever the color SHOULD track the operator\'s theme.',
727
917
  line: startLine,
728
918
  });
729
919
  continue;
@@ -1220,7 +1410,7 @@ export async function runTier0Checks(
1220
1410
  // this check enforces what the model was told. The description starts with the check's id so the
1221
1411
  // generator's logged violation lines (`✗ …`) count its fires per generation, in-turn repairs included.
1222
1412
  for (const tag of enforceDesignVocabulary ? kitControlTags(sourceCode) : []) {
1223
- const key = inlineBorderKey(tag.attrs, sourceCode);
1413
+ const key = inlineStyleKey(tag.attrs, sourceCode, CONTROL_BORDER_KEY);
1224
1414
  if (key === null) continue;
1225
1415
  issues.push({
1226
1416
  tier: 0, result: 'fail', category: 'tokens', subcategory: 'control-inline-border', severity: 'critical',
@@ -1230,6 +1420,41 @@ export async function runTier0Checks(
1230
1420
  });
1231
1421
  }
1232
1422
 
1423
+ // ── A kit control's ink and fill are the kit's (ggui#1966) ──
1424
+ // The variant carries a control's ink and fill, derived to read on its surface in every theme; an inline
1425
+ // `color` or `background` paints over it, a token as much as a literal (`var(--ggui-color-onContainer)` on an
1426
+ // outline Button is still a restyle). The prompt's rule 5 says the same.
1427
+ for (const tag of enforceDesignVocabulary ? kitControlTags(sourceCode) : []) {
1428
+ const key = inlineStyleKey(tag.attrs, sourceCode, CONTROL_COLOR_KEY);
1429
+ if (key === null) continue;
1430
+ issues.push({
1431
+ tier: 0, result: 'fail', category: 'tokens', subcategory: 'control-inline-color', severity: 'critical',
1432
+ description: `control-inline-color: <${tag.name}> sets an inline \`${key}\` — a kit control's variant carries its ink and fill, and an inline one paints over it`,
1433
+ fix: `Remove \`${key}\` from <${tag.name}>'s style and pick the variant that carries the look (e.g. <Button variant="primary"> for the main action, variant="outline" for a chip). Colour your own element, not the kit's control.`,
1434
+ line: tag.line,
1435
+ });
1436
+ }
1437
+
1438
+ // ── An opening band that lost its edge (ggui#1977 / #1981) ──
1439
+ // Only a first-child hero Box, a sole-child hero Card or a root-child `bleed` element meets the card's edge. The
1440
+ // prompt (FRAME_SIZING) teaches the band as a hero Box, the root's first child; this names a hero band placed
1441
+ // anywhere else, and a hero Card opening the root among siblings. A warn.
1442
+ const bandEdge = enforceDesignVocabulary ? detectBandEdgeLost(sourceCode) : null;
1443
+ if (bandEdge !== null) {
1444
+ issues.push({
1445
+ tier: 0, result: 'warn', category: 'layout', subcategory: `band-edge-lost:${bandEdge.shape}`, severity: 'major',
1446
+ description:
1447
+ bandEdge.shape === 'hero-card-first'
1448
+ ? `band-edge-lost: a surface="hero" Card opens the card among other children; a Card is a tile and keeps the card's inset (read from the source${bandEdge.conditional ? '; it renders conditionally' : ''})`
1449
+ : `band-edge-lost: a surface="hero" band that is not the root's first child floats inside the card's inset (read from the source${bandEdge.conditional ? '; it, or everything before it, renders conditionally, and it was read where it renders' : ''})`,
1450
+ fix:
1451
+ bandEdge.shape === 'hero-card-first'
1452
+ ? 'If this is the opening band, make it <Box surface="hero" bleed> (a Box, no radius, shadow or border of its own). A hero tile belongs among other tiles, in a grid or a row, not opening the card.'
1453
+ : 'Make the opening band the root\'s FIRST child as <Box surface="hero" bleed> (not wrapped in a Stack, not after other content). A band is a Box with no radius, shadow or border of its own; a hero Card is a tile and stays where it is. Put a conditional notice after the band, not before it.',
1454
+ line: bandEdge.line,
1455
+ });
1456
+ }
1457
+
1233
1458
  // ── Missing Props interface ───────────────────────────────
1234
1459
  if (!sourceCode.includes('interface Props') && !sourceCode.includes('type Props')) {
1235
1460
  issues.push({
@@ -59,7 +59,9 @@ import ts from 'typescript';
59
59
  import fs from 'fs';
60
60
  import path from 'path';
61
61
  import { createRequire } from 'module';
62
+ import { TINT_STEPS } from '@ggui-ai/design';
62
63
  import { describeAllowedImports } from '../validation/allowed-imports.js';
64
+ import { BOILERPLATE_REACT_HOOKS, BOILERPLATE_REACT_IMPORT } from '../boilerplate/react-hooks.js';
63
65
 
64
66
  // ---------------------------------------------------------------------------
65
67
  // Types
@@ -83,11 +85,12 @@ export interface TypeCheckResult {
83
85
 
84
86
  const BLOCKING_CODES = new Set([
85
87
  2304, // Cannot find name
86
- // 2307 (Cannot find module) is NOT blocking — Lambda bundles code without
87
- // type declarations, so the VFS can't resolve react/@ggui-ai/design.
88
- // Forbidden imports are caught by runSelfChecks regex instead.
88
+ // 2307 (Cannot find module) is NOT blocking: a third-party gadget package
89
+ // with no `.d.ts` overlay can't be resolved by design, and forbidden imports
90
+ // are caught by runSelfChecks regex instead.
89
91
  2305, // Module has no exported member
90
92
  2322, // Type not assignable
93
+ 2820, // Type not assignable — the "did you mean '…'?" form of 2322, for a near-miss literal (ggui#1849)
91
94
  2339, // Property does not exist on type
92
95
  2741, // Missing required property
93
96
  2769, // No overload matches this call
@@ -100,6 +103,50 @@ const BLOCKING_CODES = new Set([
100
103
  // Fix suggestions per error code
101
104
  // ---------------------------------------------------------------------------
102
105
 
106
+ /**
107
+ * ggui#1933 — props a model reaches for from other kits, mapped to the design primitive's own name for the
108
+ * same thing. Applied only when the component's props type (read from the checker) actually has the target.
109
+ */
110
+ const PROP_INTENT_ALIASES: Readonly<Record<string, string>> = { spacing: 'gap', space: 'gap', gutter: 'gap', color: 'tone' };
111
+
112
+ /**
113
+ * ggui#1933 — fix text for a prop the component does not take: TypeScript says the prop does not exist, never
114
+ * what the component accepts, so the generic text sent the model guessing (a fast arm looped on `<Stack spacing>`).
115
+ * `accepted` is the component's own props, read from its props type.
116
+ */
117
+ export function unknownPropFix(component: string, prop: string, accepted: readonly string[], sourceHint: string, native = false): string {
118
+ const meant = PROP_INTENT_ALIASES[prop];
119
+ const named = meant !== undefined && accepted.includes(meant) ? ` For what \`${prop}\` does in other kits, \`<${component}>\` takes \`${meant}\`.` : '';
120
+ return `\`<${component}>\` takes no \`${prop}\` prop.${named} Its props: ${accepted.map((a) => `\`${a}\``).join(', ')}${native ? ', plus the native HTML attributes' : ''}. Use one of these or drop the prop.${sourceHint}`;
121
+ }
122
+
123
+ /**
124
+ * The JSX element a diagnostic sits in, and the prop names its component declares; `undefined` when unreadable.
125
+ * Props inherited from React's own attribute types (a control extending `InputHTMLAttributes`) are not listed one
126
+ * by one, so a native-extending control's list stays its own props; `native` says the native set is also accepted.
127
+ */
128
+ function acceptedPropsAt(program: ts.Program, file: ts.SourceFile, pos: number): { component: string; accepted: string[]; native: boolean } | undefined {
129
+ const find = (node: ts.Node): ts.Node | undefined =>
130
+ pos >= node.getStart(file) && pos < node.getEnd() ? (ts.forEachChild(node, find) ?? node) : undefined;
131
+ let node = find(file);
132
+ while (node !== undefined && !ts.isJsxOpeningElement(node) && !ts.isJsxSelfClosingElement(node)) node = node.parent;
133
+ if (node === undefined) return undefined;
134
+ const checker = program.getTypeChecker();
135
+ const tagType = checker.getTypeAtLocation(node.tagName);
136
+ const signature = [...tagType.getCallSignatures(), ...tagType.getConstructSignatures()][0];
137
+ const param = signature?.getParameters()[0];
138
+ if (param === undefined) return undefined;
139
+ const propsType = checker.getTypeOfSymbolAtLocation(param, node);
140
+ const listed = checker
141
+ .getPropertiesOfType(propsType)
142
+ .filter((p) => !['children', 'key', 'ref'].includes(p.getName()) && !p.getName().startsWith('aria-') && !p.getName().startsWith('data-'));
143
+ const fromReact = (p: ts.Symbol): boolean =>
144
+ (p.getDeclarations() ?? []).every((d) => /[\\/]@types[\\/]react[\\/]/.test(d.getSourceFile().fileName));
145
+ const own = listed.filter((p) => !fromReact(p));
146
+ const accepted = own.map((p) => p.getName()).sort();
147
+ return accepted.length > 0 ? { component: node.tagName.getText(file), accepted, native: own.length < listed.length } : undefined;
148
+ }
149
+
103
150
  function generateFix(code: number, message?: string, sourceLine?: string): string {
104
151
  // Source-line hint: when the failing line is short and self-contained,
105
152
  // appending it gives the LLM a precise target. Skip for very long lines
@@ -122,11 +169,32 @@ function generateFix(code: number, message?: string, sourceLine?: string): strin
122
169
  return `You renamed a prop with a leading underscore to silence \`no-unused-vars\`, but the prop on \`Props\` doesn't have that prefix. Restore the original name; better, don't destructure props you won't render — access them via \`props.fieldName\` only when needed.${sourceHint}`;
123
170
  }
124
171
 
172
+ // A React hook the boilerplate always imports is missing: the DO NOT EDIT React import line was
173
+ // dropped or rewritten. The generic text below (props / a removed helper) points the model away
174
+ // from the one line that fixes it, and it loops (google-fast × chat-interface reached 30 turns).
175
+ const missing = code === 2304 && message !== undefined ? /Cannot find name '([^']+)'/.exec(message)?.[1] : undefined;
176
+ if (missing !== undefined && BOILERPLATE_REACT_HOOKS.some((h) => h === missing)) {
177
+ return `\`${missing}\` is a React hook, and the boilerplate's React import lists it: \`${BOILERPLATE_REACT_IMPORT}\` is a DO NOT EDIT line. This patch dropped or rewrote it. Restore that line exactly as the boilerplate wrote it, at the top of the file; do not import the hook a second way.${sourceHint}`;
178
+ }
179
+
125
180
  // Detect "Cannot find name" for destructured props
126
181
  if (code === 2304 && message && !message.includes('module') && !message.includes('import')) {
127
182
  return `This name is not defined in scope. Either you destructured props (use \`props.fieldName\` directly instead) OR you removed a helper declaration in this patch but kept a JSX/expression reference to it. Read your full patch and either restore the declaration or remove the reference.${sourceHint}`;
128
183
  }
129
184
 
185
+ // ggui#1846 — a categorical tint passed to `surface`. `surface` takes a layout slot; a data
186
+ // category takes `tint`. TypeScript names the slot union but never the prop, and the line it
187
+ // reports is the attribute's own (`surface={tint}`), so the key is that line plus a
188
+ // string-literal target type. A wrong prop on the same line reports the props object instead
189
+ // (`Type '{ … }' is not assignable to …`) and keeps the generic text. With both colour props on
190
+ // one line, the target tells them apart: the `tint` prop's union is the tint steps, the surface
191
+ // union never contains one. Only the TARGET is read, because the rejected value may itself be a
192
+ // tint (`Type '"tint-3"' is not assignable to type '"default" | …'`).
193
+ const target = message === undefined ? undefined : /is not assignable to type '(.*)/.exec(message)?.[1];
194
+ if ((code === 2322 || code === 2820) && target !== undefined && target.startsWith('"') && sourceLine && /(?<![\w-])surface=/.test(sourceLine) && !TINT_STEPS.some((s) => target.includes(`"${s}"`))) {
195
+ return `\`surface\` takes a layout slot only, one of the values the message lists. To colour by a data category (series, lanes, kinds, element groups), set \`tint\` instead, on the same Box or Card: ${TINT_STEPS.map((s) => `tint="${s}"`).join(', ')}. When the value comes from a map or a helper, type it as the tint union (${TINT_STEPS.map((s) => `'${s}'`).join(' | ')}), not \`string\`. Never cast it.${sourceHint}`;
196
+ }
197
+
130
198
  // Detect "unknown is not assignable to ReactNode"
131
199
  if ((code === 2322 || code === 2769) && message && /unknown.*ReactNode|ReactNode.*unknown/.test(message)) {
132
200
  return `This value has type 'unknown' and cannot be rendered in JSX. Cast it: String(value) or add a type annotation.${sourceHint}`;
@@ -136,6 +204,7 @@ function generateFix(code: number, message?: string, sourceLine?: string): strin
136
204
  case 2307:
137
205
  return `Only these imports are allowed: ${describeAllowedImports()}${sourceHint}`;
138
206
  case 2322:
207
+ case 2820:
139
208
  case 2769:
140
209
  return `Type mismatch on this expression. Check the offending line below — the prop name in JSX is what TypeScript is rejecting; the message tells you the expected type.${sourceHint}`;
141
210
  case 2339:
@@ -163,6 +232,12 @@ interface VfsEntry {
163
232
 
164
233
  let vfsCache: Map<string, VfsEntry> | null = null;
165
234
 
235
+ /**
236
+ * Where `csstype`'s declarations sit in the sandbox (ggui#1871). `@types/react`'s
237
+ * `import … from "csstype"` resolves here through the generic bare-specifier lookup.
238
+ */
239
+ const CSSTYPE_VFS_PATH = 'node_modules/csstype/index.d.ts';
240
+
166
241
  function parseAndStore(
167
242
  vfs: Map<string, VfsEntry>,
168
243
  virtualPath: string,
@@ -204,14 +279,41 @@ function walkDir(dir: string, filter: (f: string) => boolean): string[] {
204
279
  * walk lands in the wrong place).
205
280
  */
206
281
  function findReactTypesDir(): string | null {
207
- try {
208
- // `@types/react` doesn't expose `package.json` as a public subpath,
209
- // so resolve via the always-present `index.d.ts`.
210
- const indexDts = createRequire(import.meta.url).resolve('@types/react/index.d.ts');
211
- return path.dirname(indexDts);
212
- } catch {
213
- return null;
282
+ // `@types/react` (19.2+) declares an `exports` map whose entries carry only
283
+ // `types` conditions, so `require.resolve` refuses every subpath of it —
284
+ // `@types/react/index.d.ts` included. That made this function return null
285
+ // and the sandbox ran WITHOUT React's types (ggui#1849). Plain components
286
+ // kept their own props through the global JSX shim, but every prop typed
287
+ // through React (`ref`, `style`, handlers) degraded to `any` (`style` needs
288
+ // `csstype` as well — {@link findCsstypeDir}, ggui#1871), and so did every
289
+ // prop of the trait-composed primitives (Box, Card, Grid, Row, Stack): their
290
+ // `WithTrait` union has an arm built from React's types, and a union with an
291
+ // `any` arm is `any`. Walk Node's own
292
+ // lookup paths for the package directory instead; `resolve.paths` applies no
293
+ // `exports` restriction, and the index file is checked on disk.
294
+ const lookup = createRequire(import.meta.url).resolve.paths('@types/react') ?? [];
295
+ for (const dir of lookup) {
296
+ const candidate = path.join(dir, '@types', 'react');
297
+ if (fs.existsSync(path.join(candidate, 'index.d.ts'))) return candidate;
298
+ }
299
+ return null;
300
+ }
301
+
302
+ /**
303
+ * Resolve `csstype`'s root directory (ggui#1871). `@types/react/index.d.ts`
304
+ * opens with `import * as CSS from "csstype"`; without it `CSS` is unresolved,
305
+ * `CSSProperties` has no members and every `style={{…}}` goes unchecked.
306
+ * `csstype` is `@types/react`'s own dependency, so it is looked up from that
307
+ * package's real directory — the same place Node would find it for React's
308
+ * types, nested or hoisted.
309
+ */
310
+ function findCsstypeDir(reactDir: string): string | null {
311
+ const lookup = createRequire(path.join(fs.realpathSync(reactDir), 'index.d.ts')).resolve.paths('csstype') ?? [];
312
+ for (const dir of lookup) {
313
+ const candidate = path.join(dir, 'csstype');
314
+ if (fs.existsSync(path.join(candidate, 'index.d.ts'))) return candidate;
214
315
  }
316
+ return null;
215
317
  }
216
318
 
217
319
  /**
@@ -249,11 +351,13 @@ async function loadVfs(): Promise<Map<string, VfsEntry>> {
249
351
  }
250
352
 
251
353
  // ---- 2. React types ----
252
- // Resolved through `createRequire(import.meta.url).resolve('@types/
253
- // react/index.d.ts')` — position-independent. Works the same in
254
- // monorepo (hoisted to root) and in standalone `packages/`-only
255
- // installs (the projected public OSS repo strips the `packages/`
256
- // prefix, so a hardcoded `../../../..` walk would miss).
354
+ // Found by `findReactTypesDir` (Node's lookup paths, no `exports`
355
+ // restriction) — position-independent: the same in the monorepo
356
+ // (hoisted to root), in a deployed closure, and in standalone
357
+ // `packages/`-only installs. `@types/react` is a runtime DEPENDENCY of
358
+ // this package, not a dev one (ggui#1849): a deployed closure never
359
+ // installs devDependencies, and without these types every JSX prop
360
+ // degrades to `any`.
257
361
  const reactDir = findReactTypesDir();
258
362
  if (reactDir) {
259
363
  const reactDts = fs.readdirSync(reactDir).filter((f) => f.endsWith('.d.ts'));
@@ -261,6 +365,10 @@ async function loadVfs(): Promise<Map<string, VfsEntry>> {
261
365
  const content = fs.readFileSync(path.join(reactDir, file), 'utf-8');
262
366
  parseAndStore(vfs, `node_modules/@types/react/${file}`, content);
263
367
  }
368
+ const csstypeDir = findCsstypeDir(reactDir);
369
+ if (csstypeDir) {
370
+ parseAndStore(vfs, CSSTYPE_VFS_PATH, fs.readFileSync(path.join(csstypeDir, 'index.d.ts'), 'utf-8'));
371
+ }
264
372
  }
265
373
 
266
374
  // ---- 3. Design system types ----
@@ -664,11 +772,15 @@ export async function typecheck(
664
772
  ? sourceLines[rawLine - 1]?.trim() ?? ''
665
773
  : '';
666
774
 
775
+ // ggui#1933 — a prop the component does not take: name the props it does take, from its own props type.
776
+ const unknownProp = code_ === 2322 ? /Property '([^']+)' does not exist on type '[^']*Props[^']*'/.exec(message)?.[1] : undefined;
777
+ const props = unknownProp !== undefined ? acceptedPropsAt(program, diag.file, diag.start ?? 0) : undefined;
778
+ const hint = sourceLine.length > 0 && sourceLine.length <= 140 ? ` Offending line: \`${sourceLine}\`` : '';
667
779
  const entry: TypeCheckDiagnostic = {
668
780
  code: code_,
669
781
  line,
670
782
  message,
671
- fix: generateFix(code_, message, sourceLine),
783
+ fix: unknownProp !== undefined && props !== undefined ? unknownPropFix(props.component, unknownProp, props.accepted, hint, props.native) : generateFix(code_, message, sourceLine),
672
784
  };
673
785
 
674
786
  if (BLOCKING_CODES.has(code_)) {
@@ -11,9 +11,10 @@
11
11
  import * as esbuild from 'esbuild';
12
12
  import { AgentWorkspace } from './workspace';
13
13
  import { getSoftWarnings } from './self-check';
14
+ import { restoreReactImport } from '../boilerplate/react-hooks.js';
14
15
  import { runTier0Checks } from '../check/index.js';
15
16
  import { runGatedAxisChecks, traceAxisChecksSkipped } from '../evaluation/axis-checks/dispatch.js';
16
- import type { AxisCheck, AxisCheckInput } from '../evaluation/types-public.js';
17
+ import type { AxisCheck, AxisCheckInput, EvalIssue } from '../evaluation/types-public.js';
17
18
  import type { Classification } from '../classifier/axes.js';
18
19
  import type { CanvasClass } from '../design-mode.js';
19
20
  import { PRIMITIVES_DOCUMENTATION } from '../validation/index.js';
@@ -24,6 +25,21 @@ import type {
24
25
  } from './types';
25
26
  import { DEFAULT_DESIGN_MODE, type DesignMode } from '../design-mode.js';
26
27
 
28
+ /**
29
+ * ggui#1876 — the auto-commit line's axis field: the F/W counts, then the check ids behind them, so a
30
+ * WARN the model saw at a turn is attributable in the log (the `[WARN]` text otherwise appears only at
31
+ * the final evaluation). `0F/1W[W:universal.tint_category_in_words]`; nothing appended at `0F/0W`.
32
+ * The `<n>F/<n>W` prefix is unchanged, so anything that reads the counts reads them as before. An
33
+ * issue with no `subcategory` (the axis checks always set one, as their id) is named by its category.
34
+ */
35
+ export function axisCountsWithIds(issues: readonly EvalIssue[]): string {
36
+ const idOf = (i: EvalIssue): string => i.subcategory ?? i.category;
37
+ const warns = issues.filter((i) => i.result === 'warn').map(idOf);
38
+ const fails = issues.filter((i) => i.result !== 'warn').map(idOf);
39
+ const groups = [...(fails.length > 0 ? [`F:${fails.join(',')}`] : []), ...(warns.length > 0 ? [`W:${warns.join(',')}`] : [])];
40
+ return `${fails.length}F/${warns.length}W${groups.length > 0 ? `[${groups.join(' ')}]` : ''}`;
41
+ }
42
+
27
43
  /**
28
44
  * Extract documentation for a single component from the full primitives docs.
29
45
  * Returns ~200-500 tokens instead of 43K.
@@ -106,7 +122,14 @@ async function autoCommit(
106
122
  // No post-compile formatting — the generated TSX is built as-is.
107
123
  // (Prettier auto-format was removed; it added a dependency + repair
108
124
  // churn without a quality gain.)
109
- const formatted = raw;
125
+ // ggui#1932: the boilerplate's DO NOT EDIT React import is code's to keep — restored here, before
126
+ // the build and tier-0, so a dropped hook never becomes a refusal the model spends a turn on.
127
+ const restore = restoreReactImport(raw);
128
+ if (restore.added.length > 0) {
129
+ workspace.write(restore.source);
130
+ console.log(`[coding-agent] react-import restored: ${restore.added.map((h) => `+${h}`).join(',')}`);
131
+ }
132
+ const formatted = restore.source;
110
133
 
111
134
  // Build with esbuild — keep all imports (react, @ggui-ai/design)
112
135
  // esbuild.transform() passes them through as-is, the runtime resolves them
@@ -197,7 +220,7 @@ async function autoCommit(
197
220
 
198
221
  const status = buildSuccess && selfCheckPassed ? 'PASS' : 'FAIL';
199
222
  console.log(
200
- `[coding-agent] auto-commit: ${status} | build=${buildMs}ms self-check=${selfCheckMs}ms git=${gitMs}ms total=${Date.now() - commitStart}ms | violations=${violations.length} | axis=${axisRun === undefined ? 'off' : `${axisIssues.length - axisWarnings.length}F/${axisWarnings.length}W`}`,
223
+ `[coding-agent] auto-commit: ${status} | build=${buildMs}ms self-check=${selfCheckMs}ms git=${gitMs}ms total=${Date.now() - commitStart}ms | violations=${violations.length} | axis=${axisRun === undefined ? 'off' : axisCountsWithIds(axisIssues)}`,
201
224
  );
202
225
  if (violations.length > 0) {
203
226
  for (const v of violations) {