@cueplusplus/ui 0.3.0 → 0.6.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 (541) hide show
  1. package/CHANGELOG.md +2263 -0
  2. package/dist/agent-runtime/_glyphs.js +43 -0
  3. package/dist/agent-runtime/branch-picker.d.ts +45 -0
  4. package/dist/agent-runtime/branch-picker.js +87 -0
  5. package/dist/agent-runtime/index.d.ts +4 -0
  6. package/dist/agent-runtime/index.js +4 -0
  7. package/dist/agent-runtime/tool-call-card.d.ts +69 -0
  8. package/dist/agent-runtime/tool-call-card.js +187 -0
  9. package/dist/agent-runtime/use-agent-thread.d.ts +164 -0
  10. package/dist/agent-runtime/use-agent-thread.js +251 -0
  11. package/dist/chat/index.d.ts +2 -2
  12. package/dist/chat/message-list.d.ts +6 -0
  13. package/dist/chat/message-list.js +35 -0
  14. package/dist/chrome/_drag.js +59 -0
  15. package/dist/chrome/_status-bar-item.js +85 -0
  16. package/dist/chrome/_status-bar.d.ts +15 -0
  17. package/dist/chrome/_status-bar.js +171 -0
  18. package/dist/chrome/app-shell.d.ts +120 -2
  19. package/dist/chrome/app-shell.js +107 -8
  20. package/dist/chrome/index.d.ts +4 -3
  21. package/dist/chrome/status-bar.d.ts +87 -4
  22. package/dist/chrome/status-bar.js +80 -16
  23. package/dist/chrome/title-bar.d.ts +11 -1
  24. package/dist/chrome/title-bar.js +1 -3
  25. package/dist/configurator/_export.js +3 -2
  26. package/dist/configurator/_overrides.d.ts +1 -1
  27. package/dist/configurator/token-editor.js +4 -1
  28. package/dist/date/_segments.js +52 -6
  29. package/dist/elements/activity-graph.d.ts +40 -0
  30. package/dist/elements/activity-graph.js +81 -0
  31. package/dist/elements/agent-card.d.ts +49 -0
  32. package/dist/elements/agent-card.js +91 -0
  33. package/dist/elements/agent-handoff.d.ts +24 -0
  34. package/dist/elements/agent-handoff.js +53 -0
  35. package/dist/elements/agent-mode-badge.d.ts +72 -0
  36. package/dist/elements/agent-mode-badge.js +110 -0
  37. package/dist/elements/agent-plan.d.ts +24 -0
  38. package/dist/elements/agent-plan.js +74 -0
  39. package/dist/elements/agent-status.d.ts +29 -0
  40. package/dist/elements/agent-status.js +46 -0
  41. package/dist/elements/agent-surface.d.ts +65 -0
  42. package/dist/elements/agent-surface.js +68 -0
  43. package/dist/elements/animated-number.d.ts +86 -0
  44. package/dist/elements/animated-number.js +118 -0
  45. package/dist/elements/approval-card.d.ts +40 -0
  46. package/dist/elements/approval-card.js +78 -0
  47. package/dist/elements/artifact-card.d.ts +30 -0
  48. package/dist/elements/artifact-card.js +58 -0
  49. package/dist/elements/background-inbox.d.ts +39 -0
  50. package/dist/elements/background-inbox.js +60 -0
  51. package/dist/elements/canvas-split.d.ts +77 -0
  52. package/dist/elements/canvas-split.js +138 -0
  53. package/dist/elements/chart.d.ts +41 -0
  54. package/dist/elements/chart.js +117 -0
  55. package/dist/elements/chat-panel.d.ts +46 -0
  56. package/dist/elements/chat-panel.js +104 -0
  57. package/dist/elements/checkpoint-history.d.ts +31 -0
  58. package/dist/elements/checkpoint-history.js +65 -0
  59. package/dist/elements/clamp.d.ts +90 -0
  60. package/dist/elements/clamp.js +109 -0
  61. package/dist/elements/code-diff.d.ts +42 -0
  62. package/dist/elements/code-diff.js +69 -0
  63. package/dist/elements/code-runner.d.ts +38 -0
  64. package/dist/elements/code-runner.js +84 -0
  65. package/dist/elements/command-palette.d.ts +48 -0
  66. package/dist/elements/command-palette.js +129 -0
  67. package/dist/elements/compaction-row.d.ts +63 -0
  68. package/dist/elements/compaction-row.js +110 -0
  69. package/dist/elements/comparison-card.d.ts +47 -0
  70. package/dist/elements/comparison-card.js +69 -0
  71. package/dist/elements/composer.d.ts +274 -0
  72. package/dist/elements/composer.js +505 -0
  73. package/dist/elements/computer-use.d.ts +40 -0
  74. package/dist/elements/computer-use.js +92 -0
  75. package/dist/elements/confidence-marker.d.ts +39 -0
  76. package/dist/elements/confidence-marker.js +64 -0
  77. package/dist/elements/connection-state.d.ts +29 -0
  78. package/dist/elements/connection-state.js +66 -0
  79. package/dist/elements/context-breakdown.d.ts +30 -0
  80. package/dist/elements/context-breakdown.js +86 -0
  81. package/dist/elements/context-usage.d.ts +99 -0
  82. package/dist/elements/context-usage.js +225 -0
  83. package/dist/elements/conversation-search.d.ts +48 -0
  84. package/dist/elements/conversation-search.js +88 -0
  85. package/dist/elements/cost-meter.d.ts +33 -0
  86. package/dist/elements/cost-meter.js +75 -0
  87. package/dist/elements/data-table.d.ts +31 -0
  88. package/dist/elements/data-table.js +67 -0
  89. package/dist/elements/day-separator.d.ts +34 -0
  90. package/dist/elements/day-separator.js +50 -0
  91. package/dist/elements/diagram.d.ts +34 -0
  92. package/dist/elements/diagram.js +75 -0
  93. package/dist/elements/document-reference.d.ts +37 -0
  94. package/dist/elements/document-reference.js +66 -0
  95. package/dist/elements/draft-restore.d.ts +27 -0
  96. package/dist/elements/draft-restore.js +54 -0
  97. package/dist/elements/edit-message.d.ts +36 -0
  98. package/dist/elements/edit-message.js +77 -0
  99. package/dist/elements/elements.css +459 -0
  100. package/dist/elements/elicitation-form.d.ts +48 -0
  101. package/dist/elements/elicitation-form.js +106 -0
  102. package/dist/elements/empty-state.d.ts +52 -0
  103. package/dist/elements/empty-state.js +98 -0
  104. package/dist/elements/end-of-turn-summary.d.ts +60 -0
  105. package/dist/elements/end-of-turn-summary.js +75 -0
  106. package/dist/elements/error-state.d.ts +29 -0
  107. package/dist/elements/error-state.js +55 -0
  108. package/dist/elements/feedback-dialog.d.ts +36 -0
  109. package/dist/elements/feedback-dialog.js +81 -0
  110. package/dist/elements/file-tree.d.ts +45 -0
  111. package/dist/elements/file-tree.js +84 -0
  112. package/dist/elements/flow-graph.d.ts +45 -0
  113. package/dist/elements/flow-graph.js +78 -0
  114. package/dist/elements/generative/chart.js +200 -0
  115. package/dist/elements/generative/faults.js +111 -0
  116. package/dist/elements/generative/icons.d.ts +41 -0
  117. package/dist/elements/generative/icons.js +190 -0
  118. package/dist/elements/generative/intrinsics.js +483 -0
  119. package/dist/elements/generative/spec.d.ts +110 -0
  120. package/dist/elements/generative/spec.js +366 -0
  121. package/dist/elements/generative.css +1145 -0
  122. package/dist/elements/generative.d.ts +144 -0
  123. package/dist/elements/generative.js +126 -0
  124. package/dist/elements/guardrail-notice.d.ts +29 -0
  125. package/dist/elements/guardrail-notice.js +63 -0
  126. package/dist/elements/image-generation.d.ts +18 -0
  127. package/dist/elements/image-generation.js +64 -0
  128. package/dist/elements/index.d.ts +116 -0
  129. package/dist/elements/index.js +115 -0
  130. package/dist/elements/inline-citation.d.ts +43 -0
  131. package/dist/elements/inline-citation.js +93 -0
  132. package/dist/elements/job-progress.d.ts +49 -0
  133. package/dist/elements/job-progress.js +74 -0
  134. package/dist/elements/launcher-bubble.d.ts +34 -0
  135. package/dist/elements/launcher-bubble.js +72 -0
  136. package/dist/elements/live-region-announcer.d.ts +130 -0
  137. package/dist/elements/live-region-announcer.js +230 -0
  138. package/dist/elements/loading-state.d.ts +27 -0
  139. package/dist/elements/loading-state.js +36 -0
  140. package/dist/elements/map-answer.d.ts +38 -0
  141. package/dist/elements/map-answer.js +93 -0
  142. package/dist/elements/markdown.d.ts +131 -0
  143. package/dist/elements/markdown.js +528 -0
  144. package/dist/elements/math-block.d.ts +51 -0
  145. package/dist/elements/math-block.js +70 -0
  146. package/dist/elements/mcp-server-panel.d.ts +41 -0
  147. package/dist/elements/mcp-server-panel.js +115 -0
  148. package/dist/elements/memory-chips.d.ts +30 -0
  149. package/dist/elements/memory-chips.js +43 -0
  150. package/dist/elements/message-actions.d.ts +38 -0
  151. package/dist/elements/message-actions.js +71 -0
  152. package/dist/elements/message-attachment.d.ts +48 -0
  153. package/dist/elements/message-attachment.js +63 -0
  154. package/dist/elements/message-branches.d.ts +25 -0
  155. package/dist/elements/message-branches.js +65 -0
  156. package/dist/elements/message-pair.d.ts +33 -0
  157. package/dist/elements/message-pair.js +65 -0
  158. package/dist/elements/message-queue.d.ts +31 -0
  159. package/dist/elements/message-queue.js +80 -0
  160. package/dist/elements/message-timing.d.ts +33 -0
  161. package/dist/elements/message-timing.js +37 -0
  162. package/dist/elements/mobile-composer.d.ts +44 -0
  163. package/dist/elements/mobile-composer.js +88 -0
  164. package/dist/elements/model-picker.d.ts +38 -0
  165. package/dist/elements/model-picker.js +69 -0
  166. package/dist/elements/number-ticker.d.ts +18 -0
  167. package/dist/elements/number-ticker.js +47 -0
  168. package/dist/elements/onboarding.d.ts +30 -0
  169. package/dist/elements/onboarding.js +75 -0
  170. package/dist/elements/permission-grant.d.ts +35 -0
  171. package/dist/elements/permission-grant.js +86 -0
  172. package/dist/elements/permission-scopes.d.ts +215 -0
  173. package/dist/elements/permission-scopes.js +249 -0
  174. package/dist/elements/prompt-library.d.ts +43 -0
  175. package/dist/elements/prompt-library.js +117 -0
  176. package/dist/elements/queue-dock.d.ts +101 -0
  177. package/dist/elements/queue-dock.js +132 -0
  178. package/dist/elements/quota-banner.d.ts +26 -0
  179. package/dist/elements/quota-banner.js +66 -0
  180. package/dist/elements/quote-reply.d.ts +44 -0
  181. package/dist/elements/quote-reply.js +79 -0
  182. package/dist/elements/range.d.ts +29 -0
  183. package/dist/elements/range.js +45 -0
  184. package/dist/elements/read-aloud.d.ts +45 -0
  185. package/dist/elements/read-aloud.js +75 -0
  186. package/dist/elements/reasoning-effort.d.ts +36 -0
  187. package/dist/elements/reasoning-effort.js +67 -0
  188. package/dist/elements/reasoning-panel.d.ts +46 -0
  189. package/dist/elements/reasoning-panel.js +75 -0
  190. package/dist/elements/recommendation-card.d.ts +45 -0
  191. package/dist/elements/recommendation-card.js +83 -0
  192. package/dist/elements/regenerate-menu.d.ts +45 -0
  193. package/dist/elements/regenerate-menu.js +51 -0
  194. package/dist/elements/replay.d.ts +217 -0
  195. package/dist/elements/replay.js +498 -0
  196. package/dist/elements/research-report.d.ts +38 -0
  197. package/dist/elements/research-report.js +69 -0
  198. package/dist/elements/retrieval-chunks.d.ts +43 -0
  199. package/dist/elements/retrieval-chunks.js +81 -0
  200. package/dist/elements/revert-dock.d.ts +159 -0
  201. package/dist/elements/revert-dock.js +327 -0
  202. package/dist/elements/reviewable-diff.d.ts +45 -0
  203. package/dist/elements/reviewable-diff.js +110 -0
  204. package/dist/elements/risk-badge.d.ts +52 -0
  205. package/dist/elements/risk-badge.js +91 -0
  206. package/dist/elements/schedule-card.d.ts +42 -0
  207. package/dist/elements/schedule-card.js +90 -0
  208. package/dist/elements/score-breakdown.d.ts +47 -0
  209. package/dist/elements/score-breakdown.js +82 -0
  210. package/dist/elements/scroll-anchor.d.ts +32 -0
  211. package/dist/elements/scroll-anchor.js +121 -0
  212. package/dist/elements/settings-panel.d.ts +52 -0
  213. package/dist/elements/settings-panel.js +107 -0
  214. package/dist/elements/shared-conversation.d.ts +39 -0
  215. package/dist/elements/shared-conversation.js +68 -0
  216. package/dist/elements/sources.d.ts +33 -0
  217. package/dist/elements/sources.js +61 -0
  218. package/dist/elements/speaker-identity.d.ts +32 -0
  219. package/dist/elements/speaker-identity.js +51 -0
  220. package/dist/elements/spec-sheet.d.ts +31 -0
  221. package/dist/elements/spec-sheet.js +45 -0
  222. package/dist/elements/stopped-run.d.ts +22 -0
  223. package/dist/elements/stopped-run.js +50 -0
  224. package/dist/elements/streaming-text.d.ts +31 -0
  225. package/dist/elements/streaming-text.js +45 -0
  226. package/dist/elements/subagent-list.d.ts +37 -0
  227. package/dist/elements/subagent-list.js +79 -0
  228. package/dist/elements/suggestions.d.ts +30 -0
  229. package/dist/elements/suggestions.js +36 -0
  230. package/dist/elements/surfaces.d.ts +85 -0
  231. package/dist/elements/surfaces.js +107 -0
  232. package/dist/elements/tail-status.d.ts +131 -0
  233. package/dist/elements/tail-status.js +94 -0
  234. package/dist/elements/terminal-block.d.ts +29 -0
  235. package/dist/elements/terminal-block.js +54 -0
  236. package/dist/elements/thinking-indicator.d.ts +18 -0
  237. package/dist/elements/thinking-indicator.js +36 -0
  238. package/dist/elements/thread-list.d.ts +35 -0
  239. package/dist/elements/thread-list.js +68 -0
  240. package/dist/elements/thread-search.d.ts +42 -0
  241. package/dist/elements/thread-search.js +99 -0
  242. package/dist/elements/timeline.d.ts +37 -0
  243. package/dist/elements/timeline.js +46 -0
  244. package/dist/elements/todo-list.d.ts +42 -0
  245. package/dist/elements/todo-list.js +59 -0
  246. package/dist/elements/tool-call.d.ts +41 -0
  247. package/dist/elements/tool-call.js +85 -0
  248. package/dist/elements/tool-error.d.ts +39 -0
  249. package/dist/elements/tool-error.js +76 -0
  250. package/dist/elements/tool-group.d.ts +45 -0
  251. package/dist/elements/tool-group.js +71 -0
  252. package/dist/elements/tool-timeline.d.ts +65 -0
  253. package/dist/elements/tool-timeline.js +88 -0
  254. package/dist/elements/trace-waterfall.d.ts +37 -0
  255. package/dist/elements/trace-waterfall.js +69 -0
  256. package/dist/elements/transcript-rules.d.ts +67 -0
  257. package/dist/elements/transcript-rules.js +95 -0
  258. package/dist/elements/turn-footer.d.ts +74 -0
  259. package/dist/elements/turn-footer.js +101 -0
  260. package/dist/elements/typing-indicator.d.ts +16 -0
  261. package/dist/elements/typing-indicator.js +51 -0
  262. package/dist/elements/verdict-row.d.ts +89 -0
  263. package/dist/elements/verdict-row.js +150 -0
  264. package/dist/elements/vocabulary.d.ts +612 -0
  265. package/dist/elements/vocabulary.js +611 -0
  266. package/dist/elements/voice-conversation.d.ts +49 -0
  267. package/dist/elements/voice-conversation.js +123 -0
  268. package/dist/elements/web-preview.d.ts +32 -0
  269. package/dist/elements/web-preview.js +66 -0
  270. package/dist/elements/web-search.d.ts +37 -0
  271. package/dist/elements/web-search.js +66 -0
  272. package/dist/elements/work-collapse.d.ts +103 -0
  273. package/dist/elements/work-collapse.js +131 -0
  274. package/dist/forms/_chassis.d.ts +104 -4
  275. package/dist/forms/_chassis.js +104 -4
  276. package/dist/forms/input-group.d.ts +8 -3
  277. package/dist/forms/input-group.js +8 -3
  278. package/dist/forms/number-field.d.ts +3 -2
  279. package/dist/forms/number-field.js +3 -2
  280. package/dist/icons/containers.d.ts +89 -0
  281. package/dist/icons/containers.js +295 -0
  282. package/dist/icons/index.d.ts +2 -0
  283. package/dist/index.d.ts +12 -6
  284. package/dist/index.js +5 -1
  285. package/dist/instruments/_data-row.js +118 -0
  286. package/dist/instruments/data-row.d.ts +94 -0
  287. package/dist/instruments/data-row.js +151 -0
  288. package/dist/instruments/data-tree.d.ts +126 -0
  289. package/dist/instruments/data-tree.js +303 -0
  290. package/dist/instruments/index.d.ts +5 -2
  291. package/dist/instruments/index.js +4 -1
  292. package/dist/instruments/ledger.d.ts +105 -0
  293. package/dist/instruments/ledger.js +114 -0
  294. package/dist/instruments/table.d.ts +70 -2
  295. package/dist/instruments/table.js +133 -39
  296. package/dist/instruments/two-step-button.js +25 -6
  297. package/dist/layout/sidebar.d.ts +19 -1
  298. package/dist/layout/sidebar.js +19 -1
  299. package/dist/primitives/button.js +11 -1
  300. package/dist/primitives/chip.d.ts +35 -9
  301. package/dist/primitives/chip.js +97 -11
  302. package/dist/system/agent-skin.d.ts +27 -0
  303. package/dist/system/agent-skin.js +20 -0
  304. package/dist/system/portal.d.ts +5 -3
  305. package/dist/system/portal.js +19 -5
  306. package/dist/system/theme-provider.js +54 -9
  307. package/dist/system/use-density.d.ts +9 -0
  308. package/dist/system/use-density.js +9 -0
  309. package/dist/system/use-isomorphic-layout-effect.js +19 -1
  310. package/dist/theming/_presets.d.ts +1 -1
  311. package/dist/theming/_presets.js +74 -11
  312. package/dist/theming/contrast.d.ts +16 -4
  313. package/dist/theming/contrast.js +33 -6
  314. package/dist/theming/create-theme.d.ts +6 -2
  315. package/dist/theming/create-theme.js +32 -5
  316. package/dist/theming/serialize.d.ts +10 -1
  317. package/dist/theming/serialize.js +62 -8
  318. package/manifest/components/activity-graph.json +88 -0
  319. package/manifest/components/agent-card.json +120 -0
  320. package/manifest/components/agent-handoff.json +87 -0
  321. package/manifest/components/agent-mode-badge.json +84 -0
  322. package/manifest/components/agent-plan.json +67 -0
  323. package/manifest/components/agent-status.json +73 -0
  324. package/manifest/components/agent-surface.json +78 -0
  325. package/manifest/components/alert-dialog.json +1 -1
  326. package/manifest/components/animated-number.json +67 -0
  327. package/manifest/components/app-shell.json +142 -9
  328. package/manifest/components/approval-card.json +106 -0
  329. package/manifest/components/artifact-card.json +81 -0
  330. package/manifest/components/audience-icon.json +52 -0
  331. package/manifest/components/autocomplete.json +2 -0
  332. package/manifest/components/background-inbox.json +71 -0
  333. package/manifest/components/branch-picker.json +124 -0
  334. package/manifest/components/button.json +1 -0
  335. package/manifest/components/canvas-split-body.json +62 -0
  336. package/manifest/components/canvas-split-document.json +54 -0
  337. package/manifest/components/canvas-split-header.json +83 -0
  338. package/manifest/components/canvas-split-line.json +62 -0
  339. package/manifest/components/canvas-split-message.json +62 -0
  340. package/manifest/components/canvas-split-thread.json +54 -0
  341. package/manifest/components/canvas-split.json +60 -0
  342. package/manifest/components/catalogue-icon.json +52 -0
  343. package/manifest/components/channel-beta-icon.json +52 -0
  344. package/manifest/components/channel-released-icon.json +52 -0
  345. package/manifest/components/chart.json +93 -0
  346. package/manifest/components/chat-empty-state.json +55 -0
  347. package/manifest/components/chat-panel-assistant-message.json +51 -0
  348. package/manifest/components/chat-panel-composer.json +66 -0
  349. package/manifest/components/chat-panel-messages.json +51 -0
  350. package/manifest/components/chat-panel-typing.json +51 -0
  351. package/manifest/components/chat-panel-user-message.json +51 -0
  352. package/manifest/components/chat-panel.json +57 -0
  353. package/manifest/components/checkpoint-history.json +80 -0
  354. package/manifest/components/chip.json +24 -12
  355. package/manifest/components/clamp.json +102 -0
  356. package/manifest/components/cli-tool-icon.json +52 -0
  357. package/manifest/components/code-diff.json +87 -0
  358. package/manifest/components/code-runner.json +97 -0
  359. package/manifest/components/color-field.json +2 -0
  360. package/manifest/components/color-picker.json +2 -0
  361. package/manifest/components/combobox.json +2 -0
  362. package/manifest/components/compaction-row.json +100 -0
  363. package/manifest/components/comparison-card.json +83 -0
  364. package/manifest/components/composer-actions.json +65 -0
  365. package/manifest/components/composer-attach-button.json +65 -0
  366. package/manifest/components/composer-attachment-chip.json +75 -0
  367. package/manifest/components/composer-attachments.json +60 -0
  368. package/manifest/components/composer-bar.json +73 -0
  369. package/manifest/components/composer-command-item.json +79 -0
  370. package/manifest/components/composer-context.json +67 -0
  371. package/manifest/components/composer-input.json +73 -0
  372. package/manifest/components/composer-menu-item.json +72 -0
  373. package/manifest/components/composer-menu.json +79 -0
  374. package/manifest/components/composer-model-item.json +75 -0
  375. package/manifest/components/composer-model-trigger.json +75 -0
  376. package/manifest/components/composer-person-item.json +79 -0
  377. package/manifest/components/composer-send.json +80 -0
  378. package/manifest/components/composer-toolbar.json +65 -0
  379. package/manifest/components/composer-voice-button.json +68 -0
  380. package/manifest/components/composer-voice.json +75 -0
  381. package/manifest/components/composer.json +2 -0
  382. package/manifest/components/computer-use.json +84 -0
  383. package/manifest/components/confidence-marker.json +74 -0
  384. package/manifest/components/connection-state.json +84 -0
  385. package/manifest/components/context-breakdown.json +65 -0
  386. package/manifest/components/context-usage.json +85 -0
  387. package/manifest/components/conversation-search.json +92 -0
  388. package/manifest/components/cost-meter.json +74 -0
  389. package/manifest/components/cue-portal-frame.json +1 -1
  390. package/manifest/components/data-row.json +203 -0
  391. package/manifest/components/data-tree.json +151 -0
  392. package/manifest/components/date-field.json +2 -0
  393. package/manifest/components/date-picker.json +2 -0
  394. package/manifest/components/date-range-picker.json +2 -0
  395. package/manifest/components/day-separator.json +58 -0
  396. package/manifest/components/delegation-card.json +1 -1
  397. package/manifest/components/density.json +1 -1
  398. package/manifest/components/diagram.json +100 -0
  399. package/manifest/components/document-reference.json +90 -0
  400. package/manifest/components/draft-restore.json +82 -0
  401. package/manifest/components/drawer.json +1 -1
  402. package/manifest/components/dropdown-menu.json +1 -1
  403. package/manifest/components/edit-message.json +105 -0
  404. package/manifest/components/elements-command-palette.json +100 -0
  405. package/manifest/components/elements-composer.json +71 -0
  406. package/manifest/components/elements-data-table.json +68 -0
  407. package/manifest/components/elements-timeline.json +69 -0
  408. package/manifest/components/elicitation-form.json +103 -0
  409. package/manifest/components/empty-state-composer.json +64 -0
  410. package/manifest/components/empty-state-greeting.json +49 -0
  411. package/manifest/components/empty-state-suggestion.json +57 -0
  412. package/manifest/components/empty-state-suggestions.json +49 -0
  413. package/manifest/components/end-of-turn-summary.json +77 -0
  414. package/manifest/components/env-var-input.json +2 -0
  415. package/manifest/components/error-state.json +80 -0
  416. package/manifest/components/feedback-dialog.json +107 -0
  417. package/manifest/components/file-tree.json +83 -0
  418. package/manifest/components/flow-graph.json +75 -0
  419. package/manifest/components/folder-icon.json +52 -0
  420. package/manifest/components/frac.json +59 -0
  421. package/manifest/components/generation-loader.json +64 -0
  422. package/manifest/components/generative-ui.json +93 -0
  423. package/manifest/components/guardrail-notice.json +91 -0
  424. package/manifest/components/hover-card.json +1 -1
  425. package/manifest/components/image-generation.json +67 -0
  426. package/manifest/components/info-tip.json +1 -1
  427. package/manifest/components/inline-citation.json +77 -0
  428. package/manifest/components/input-group.json +4 -2
  429. package/manifest/components/input.json +3 -1
  430. package/manifest/components/job-progress.json +96 -0
  431. package/manifest/components/launcher-bubble.json +105 -0
  432. package/manifest/components/ledger.json +187 -0
  433. package/manifest/components/live-region-announcer.json +79 -0
  434. package/manifest/components/map-answer.json +83 -0
  435. package/manifest/components/markdown-text.json +94 -0
  436. package/manifest/components/math-block.json +74 -0
  437. package/manifest/components/mcp-server-icon.json +52 -0
  438. package/manifest/components/mcp-server-panel.json +87 -0
  439. package/manifest/components/memory-chips.json +65 -0
  440. package/manifest/components/message-actions.json +99 -0
  441. package/manifest/components/message-attachments.json +63 -0
  442. package/manifest/components/message-branches.json +71 -0
  443. package/manifest/components/message-list.json +1 -1
  444. package/manifest/components/message-pair.json +86 -0
  445. package/manifest/components/message-queue.json +74 -0
  446. package/manifest/components/message-timing.json +62 -0
  447. package/manifest/components/mobile-composer.json +126 -0
  448. package/manifest/components/model-picker.json +76 -0
  449. package/manifest/components/multi-select.json +2 -0
  450. package/manifest/components/musical-time-input.json +2 -0
  451. package/manifest/components/node-card.json +1 -1
  452. package/manifest/components/node-handle.json +1 -1
  453. package/manifest/components/number-field.json +3 -1
  454. package/manifest/components/number-ticker.json +61 -0
  455. package/manifest/components/onboarding.json +84 -0
  456. package/manifest/components/otp-field.json +9 -7
  457. package/manifest/components/password-input.json +2 -0
  458. package/manifest/components/permission-grant.json +92 -0
  459. package/manifest/components/permission-scopes.json +139 -0
  460. package/manifest/components/popover.json +1 -1
  461. package/manifest/components/prompt-library.json +99 -0
  462. package/manifest/components/queue-dock.json +110 -0
  463. package/manifest/components/quota-banner.json +94 -0
  464. package/manifest/components/quote-reply.json +105 -0
  465. package/manifest/components/read-aloud.json +111 -0
  466. package/manifest/components/reasoning-effort.json +81 -0
  467. package/manifest/components/reasoning-panel.json +108 -0
  468. package/manifest/components/recommendation-card.json +107 -0
  469. package/manifest/components/regenerate-menu.json +89 -0
  470. package/manifest/components/replay-player.json +102 -0
  471. package/manifest/components/research-report.json +76 -0
  472. package/manifest/components/retrieval-chunks.json +84 -0
  473. package/manifest/components/revert-dock.json +150 -0
  474. package/manifest/components/reviewable-diff.json +93 -0
  475. package/manifest/components/risk-badge.json +74 -0
  476. package/manifest/components/schedule-card.json +101 -0
  477. package/manifest/components/score-breakdown.json +91 -0
  478. package/manifest/components/scroll-anchor.json +72 -0
  479. package/manifest/components/scroll-area.json +7 -7
  480. package/manifest/components/scrub-input.json +2 -0
  481. package/manifest/components/search-input.json +9 -7
  482. package/manifest/components/segmented-control.json +21 -21
  483. package/manifest/components/select.json +1 -0
  484. package/manifest/components/settings-panel.json +118 -0
  485. package/manifest/components/shared-conversation.json +89 -0
  486. package/manifest/components/shimmer-label.json +59 -0
  487. package/manifest/components/skill-icon.json +52 -0
  488. package/manifest/components/sources.json +83 -0
  489. package/manifest/components/speaker-identity.json +61 -0
  490. package/manifest/components/spec-sheet.json +80 -0
  491. package/manifest/components/stack-icon.json +52 -0
  492. package/manifest/components/stacks-matrix-icon.json +52 -0
  493. package/manifest/components/stat.json +1 -1
  494. package/manifest/components/status-bar.json +146 -12
  495. package/manifest/components/status-dot.json +1 -1
  496. package/manifest/components/stopped-run.json +81 -0
  497. package/manifest/components/streaming-text.json +71 -0
  498. package/manifest/components/sub.json +44 -0
  499. package/manifest/components/subagent-list.json +88 -0
  500. package/manifest/components/suggestions.json +84 -0
  501. package/manifest/components/sup.json +44 -0
  502. package/manifest/components/swap-label.json +71 -0
  503. package/manifest/components/table-scroll-region.json +6 -0
  504. package/manifest/components/table.json +88 -4
  505. package/manifest/components/tags-input.json +2 -0
  506. package/manifest/components/tail-status.json +109 -0
  507. package/manifest/components/terminal-block.json +91 -0
  508. package/manifest/components/textarea.json +3 -1
  509. package/manifest/components/theme-provider.json +2 -2
  510. package/manifest/components/thinking-indicator.json +63 -0
  511. package/manifest/components/thread-list.json +76 -0
  512. package/manifest/components/thread-search.json +90 -0
  513. package/manifest/components/threshold-rail.json +2 -2
  514. package/manifest/components/time-boundary.json +62 -0
  515. package/manifest/components/time-field.json +2 -0
  516. package/manifest/components/timeline.json +1 -1
  517. package/manifest/components/title-bar.json +1 -1
  518. package/manifest/components/todo-list.json +69 -0
  519. package/manifest/components/toggle-group.json +9 -7
  520. package/manifest/components/toggle.json +2 -0
  521. package/manifest/components/token-editor.json +1 -1
  522. package/manifest/components/tool-call-card.json +200 -0
  523. package/manifest/components/tool-call.json +119 -0
  524. package/manifest/components/tool-error.json +114 -0
  525. package/manifest/components/tool-group.json +83 -0
  526. package/manifest/components/tool-timeline.json +119 -0
  527. package/manifest/components/toolbar.json +7 -7
  528. package/manifest/components/tooltip.json +1 -1
  529. package/manifest/components/trace-waterfall.json +74 -0
  530. package/manifest/components/turn-footer.json +105 -0
  531. package/manifest/components/typing-indicator.json +53 -0
  532. package/manifest/components/unread-divider.json +55 -0
  533. package/manifest/components/usage-chart.json +1 -1
  534. package/manifest/components/verdict-row.json +91 -0
  535. package/manifest/components/voice-conversation.json +109 -0
  536. package/manifest/components/web-preview.json +87 -0
  537. package/manifest/components/web-search.json +92 -0
  538. package/manifest/components/work-collapse.json +102 -0
  539. package/manifest/manifest.json +2736 -60
  540. package/manifest/tokens.json +22 -1
  541. package/package.json +44 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,2268 @@
1
1
  # @cueplusplus/ui
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - bf116b5: `AppShell.Bar` gains the desktop drag contract `TitleBar` already had, plus a truly bar-centred `center` slot and a trailing `actions` cluster, so a Tauri or Electron console spends one chrome row on window furniture instead of two.
8
+
9
+ `StatusBar.Item` gains `interactive`, `prominence` and `render`, so a reading can become a real button or link with hover, press and inset keyboard-focus states, and the new `StatusBar.Group` fuses adjacent items into one compact control. The published manifest now lists `StatusBar.Item` and `StatusBar.Group` with their props, so a props table or lookup tool reads them the way it already reads `AppShell`'s parts.
10
+
11
+ ## Migrating
12
+
13
+ Nothing to change. Both additions are opt-in: without `platformDrag`, `center` or `actions` the bar emits no drag attributes at all, and a `StatusBar.Item` with none of the new props is the same inert `<div>` with the same classes it always was.
14
+
15
+ Two shapes moved underneath, in case your own CSS reaches into them. `AppShell.Bar` now wraps its brand, its leading children and its actions in `[data-slot="app-shell-brand-content"]`, `[data-slot="app-shell-bar-leading"]` and `[data-slot="app-shell-bar-actions"]` respectively — each a `<div>` laid out as a flex row with the bar's own gap, so the rhythm is unchanged and `brandClassName` still lands on the cell. Under `platformDrag` those clusters take their content's width rather than filling their cell, which is what leaves the bar's dead space to the window manager; an `ms-auto` that needs to reach the cell's rim belongs on a box inside the cluster there. And `StatusBar`'s root is now `overflow-x-clip`: the bar has always promised to be one row tall, and now it keeps that promise when it is overfilled.
16
+
17
+ - fb20c71: Dense data surfaces: `DataRow` is the shared row anatomy (marker, label, description, tags, figure and an action track hidden until hover, focus or selection, with a touch fallback and no reflow when it appears); `Ledger` groups rows under sticky tier headings and gutter slugs with no disclosure semantics; `DataTree` draws the same rows as a hierarchy under `role="tree"` with the APG keyboard map and CSS depth guides; and `Table` gains `Table.GroupRow` (a `<th scope="rowgroup">` divider so a grouped list stays one table with its columns named once), `sticky` on `Table.Head`, and `interactive`/`selected` on `Table.Row`. Existing tables are untouched: every addition is opt-in.
18
+
19
+ ### Patch Changes
20
+
21
+ - 778374d: The keyboard focus ring is now visible in the luma, venu and hivehub light
22
+ themes. Each set `--cue-focus` to its brand accent, and on paper that accent
23
+ read as low as 2.45:1 against the recessed fill a focused field paints it
24
+ over — under the 3:1 WCAG 1.4.11 asks of a focus indicator. Each light block
25
+ now carries its own focus colour: the same accent hue, stepped down in
26
+ lightness until it clears every surface a field can sit on. `--cue-accent` is
27
+ unchanged, so no button, chip or link moves.
28
+ - 013960d: Form controls now show one quiet focus signal instead of two. A focused field strengthens its rim by one rung (`--cue-border` → `--cue-border-strong`) and draws a single 2px ring in the theme's own `--cue-focus`, pulled flush against the rim rather than floating outside it on a gap. Grouped controls — `InputGroup`, `SearchInput`, `Combobox`, `MultiSelect`, `Autocomplete`, `TagsInput`, `NumberField`, `ScrubInput`, `Composer`, `MusicalTimeInput` and the segmented date and time fields — strengthen their rim for any focus inside but wait for a keyboard-visible descendant before drawing the ring, so a mouse click no longer lights the whole cluster up. An invalid field stays red while it is focused: because a flush ring is drawn over the rim it hugs, the ring itself turns `--cue-danger` rather than leaving a border nobody can see underneath it — so focus keeps its own indicator and invalid keeps its colour, on the plain controls and on the grouped ones alike.
29
+
30
+ ## Migrating
31
+
32
+ No API changed, and no consumer edit is required. Anyone who added local overrides to mute the old accent-rim-plus-outer-ring treatment can delete them: the defaults are now the quiet version, and a local override will fight the theme's focus colour rather than help it.
33
+
34
+ - Updated dependencies [778374d]
35
+ - @cueplusplus/tokens@0.6.0
36
+
37
+ ## 0.5.0
38
+
39
+ ### Minor Changes
40
+
41
+ - f71a189: ## The hooks are documented API
42
+
43
+ `useAgentThread` had no page. Neither did `useTheme`, `useToast`, `useCueTable`
44
+ or the eight others — not because they are private, but because the generator
45
+ that writes this library's documentation only ever looked for components. A
46
+ symbol you are told to import, whose shape decides how you write the call, was
47
+ reaching you as a name in a changelog and a type in your editor and nothing
48
+ else. An agent reading the manifest could not answer "what does `useCueTable`
49
+ take" at all.
50
+
51
+ `manifest.json` now carries a `hooks` array beside `components`, with every
52
+ `use*` an entry subpath exports:
53
+
54
+ ```
55
+ useAgentThread @cueplusplus/ui/agent-runtime
56
+ useCarousel @cueplusplus/ui/layout/carousel
57
+ useControlHeight @cueplusplus/ui
58
+ useCuePortalProps @cueplusplus/ui
59
+ useCueTable @cueplusplus/ui/instruments/data-table
60
+ useDensity @cueplusplus/ui
61
+ useFlowTheme @cueplusplus/ui/flow
62
+ useSidebar @cueplusplus/ui
63
+ useTheme @cueplusplus/ui
64
+ useToast @cueplusplus/ui
65
+ useZoom @cueplusplus/ui/configurator
66
+ ```
67
+
68
+ Each entry carries what the source already knew and had nowhere to put: the
69
+ `signature` TypeScript resolves for it (`(size?: ControlSize): number`, not a
70
+ prop table with nothing in it), the description and summary, `@param`,
71
+ `@returns` and `@throws` where the JSDoc has them, every `@example` — all of
72
+ them, so `useFlowTheme`'s second one survives — plus `group`, `importPath`,
73
+ `peerDependencies`, `clientOnly` and the page it lives on.
74
+
75
+ The array is **inline**. A component's row points at a companion file because
76
+ its props, parts, variants and tokens do not fit; a hook's whole documentation
77
+ does, so `manifest.json` answers about a hook with no second fetch. That is why
78
+ a hook row has `url` and `mdUrl` but no `jsonUrl` — there is no companion to
79
+ name.
80
+
81
+ Every hook is `clientOnly: true`, and every one of them says so on its page. The
82
+ seven internal look-alikes — `useComposedRefs`, `useDebouncedValue`,
83
+ `useEdgeScroll` and the rest — are a component's own machinery, no entry exports
84
+ them, and none of them appears. A new one showing up in this array is a
85
+ decision, not a diff.
86
+
87
+ ## Two hooks that could not have carried a page
88
+
89
+ **`useDensity` now shows the read it exists for.** Its description always said
90
+ this is the JS read for code that cannot ask CSS — virtualised row heights,
91
+ canvas drawing, layout math — and then showed you none of it. It carries the
92
+ virtualiser idiom now: the level, the measured row height beside it, and the
93
+ overscan that has to move with the level because shorter rows mean more of them
94
+ on screen.
95
+
96
+ **`useSidebar` was one sentence long.** It said it reads the collapse state and
97
+ stopped, which left the two things a caller actually needs undocumented: that it
98
+ hands back `toggle` as well as `collapsed` — the same valve `Sidebar.Rail`
99
+ pulls, so a collapse control does not have to live on the rail — and what
100
+ happens outside a `Sidebar.Root`. It does not throw there. It reports
101
+ `collapsed: false` and a `toggle` that does nothing, because a row rendered on
102
+ its own in a test or a docs page is a legitimate thing and the honest answer for
103
+ it is that nothing is collapsed. Its example swaps a label for an icon and keeps
104
+ the `aria-label` across both.
105
+
106
+ Behaviour is unchanged in both. This is JSDoc.
107
+
108
+ ## Finding one
109
+
110
+ Hooks share the components namespace on the docs site — `/docs/components/use-agent-thread`,
111
+ and the markdown twin at `…/use-agent-thread.md` — so a link to a hook looks
112
+ like a link to anything else, the sidebar lists them under their group, and
113
+ `llms.txt` and `llms-full.txt` carry them. `useAgentThread`'s page mounts the
114
+ live scripted thread that already ran under **Agent runtime**, because the bench
115
+ now names the hook it was built to show.
116
+
117
+ The skills references list them per group, and the lookup script answers:
118
+
119
+ ```
120
+ node scripts/lookup.mjs --props useAgentThread # signature, params, returns, example
121
+ node scripts/lookup.mjs --hooks # all eleven, with their import paths
122
+ ```
123
+
124
+ ## What did not change
125
+
126
+ `schemaVersion` is still `1` — this is an addition, and additions never bump it.
127
+ `components` still means components: every count, badge, table and `--list` that
128
+ said "155 components" says it still, and none of them quietly grew by eleven. No
129
+ component's documentation moved, no runtime code changed, and nothing new is
130
+ required of a consumer who never reads the manifest.
131
+
132
+ - 8991fa3: ## Every state watchable
133
+
134
+ An agent UI is mostly states nobody can get to. A tool call that is _running_
135
+ holds that shape for four hundred milliseconds; a run _waiting on a person_
136
+ holds it until somebody answers; a _cancelled_ turn is a picture you have to
137
+ break something to see. This library has ninety-five components for those
138
+ states and, until now, two ways of showing them: a still, or a script that plays
139
+ once and cannot be stopped, rewound or linked to.
140
+
141
+ Now every state a run passes through is watchable — the run has a transport, the
142
+ transport has a scrubber, and the frame you stopped on has an address. `t=8` in
143
+ a gallery link opens the run paused on the approval, in your theme, at your
144
+ density, on the device you picked.
145
+
146
+ **`ReplayPlayer`** ships on its own subpath, `@cueplusplus/ui/elements/replay`,
147
+ with `useReplay` beside it for a console that wants the transport and not the
148
+ chrome.
149
+
150
+ ```tsx
151
+ import {
152
+ ReplayPlayer,
153
+ type ReplayScript,
154
+ } from "@cueplusplus/ui/elements/replay";
155
+
156
+ const SCRIPT: ReplayScript = {
157
+ title: "Patching universe 3",
158
+ frames: [
159
+ {
160
+ scene: (
161
+ <ToolCall
162
+ label="Read"
163
+ activeLabel="Reading the plot"
164
+ query="plot.md"
165
+ request='{"path":"plot.md"}'
166
+ result=""
167
+ running
168
+ open={false}
169
+ onOpenChange={() => {}}
170
+ />
171
+ ),
172
+ hold: 600,
173
+ label: "tool-call",
174
+ },
175
+ {
176
+ scene: (
177
+ <ApprovalCard
178
+ state="request"
179
+ command="cue write --cue 1"
180
+ title="Write the first cue"
181
+ subtitle="Universe 3, channel 1"
182
+ />
183
+ ),
184
+ hold: 1600,
185
+ label: "awaiting-approval",
186
+ },
187
+ { scene: <MarkdownText>{answer}</MarkdownText>, hold: 0, label: "done" },
188
+ ],
189
+ };
190
+
191
+ <ReplayPlayer script={SCRIPT} />;
192
+ <ReplayPlayer script={SCRIPT} startAt={17} autoPlay={false} rate={0.5} />;
193
+ ```
194
+
195
+ ### A controller over a filmstrip, not an animation
196
+
197
+ Every frame carries the **whole scene** rather than the part that changed, so
198
+ the component's entire rendering rule is `frames[index].scene`. That is what
199
+ makes the position controls honest: frame 12 reached by scrubbing is the same
200
+ DOM as frame 12 reached by watching, because there is no accumulated state for
201
+ the two paths to disagree about. A player whose frames accumulated — the way a
202
+ simulated terminal accumulates printed lines inside its own closure — can be
203
+ played and it can be restarted, but it can never be scrubbed.
204
+
205
+ ### One clock, so pause is exact
206
+
207
+ The two older players in this library pace themselves with a chain of
208
+ `setTimeout`s, which makes pause impossible: the time already spent inside the
209
+ in-flight timeout is unrecoverable, so a resume can only start the hold again.
210
+ Here the playhead is a number of _script_ milliseconds and wall time reaches it
211
+ through one monotonic origin — `base + (now() - origin) * rate` — so pausing is
212
+ "write down the playhead", resuming is "take a new origin", and the unspent
213
+ 380 ms of a 400 ms hold is still 380 ms on the other side of the pause. The same
214
+ identity is what makes the rate multiplier exact rather than approximate, and
215
+ what makes a seek and a play land on the same frame.
216
+
217
+ The transport is play, pause, step either way, scrub, restart and rate, built
218
+ from shipped cue components — a `Slider` for the scrubber, `IconButton`s, a
219
+ `Chip` for the state — and painted in cue's own metrics even when the scene
220
+ inside it is an `AgentSurface` island at upstream fidelity. The chrome belongs
221
+ to the console showing the run, not to the run.
222
+
223
+ ### Reduced motion is a mode, not an ending
224
+
225
+ Under `prefers-reduced-motion: reduce` **nothing is ever scheduled** — not
226
+ scheduled and hidden, not scheduled at speed, not scheduled at all — and the
227
+ player opens on the resting frame. Every control except play and rate keeps
228
+ working, so a reader who asked for stillness can still step through the run one
229
+ frame at a time and read every state it passes through. That is strictly more
230
+ than "show the end and stop", which is what this library offered before.
231
+
232
+ ### Legible from outside
233
+
234
+ The root publishes `data-replay-state` (the current frame's authored label),
235
+ `data-replay-playing` and `data-replay-frame`, the way `TerminalFrame` publishes
236
+ `data-simulating` — enough for a stylesheet, a screenshot harness or a test to
237
+ say "show me the awaiting-approval frame" without reaching into React.
238
+
239
+ State labels are **authored, not derived**: nothing in this tree emits a
240
+ run-state event and a scripted thread has no clock, so each frame carries its
241
+ own word from a set of seven — `queued`, `thinking`, `tool-call`,
242
+ `awaiting-approval`, `streaming`, `cancelled`, `done`. Two kinds of waiting, two
243
+ kinds of work, the one state that is waiting on a _human_, and the two endings a
244
+ transcript really has.
245
+
246
+ ### No peer, and no icon set
247
+
248
+ `./elements/replay` declares an empty peer row in the dist contract, and that is
249
+ a claim being checked rather than an omission: the five transport glyphs are
250
+ drawn in the file at 16px. A player is chrome, and chrome that drags an icon set
251
+ in behind it is a dependency nobody chose.
252
+
253
+ ## One run, written down once
254
+
255
+ A player needs something to play, and eight example pages hand-authoring their
256
+ own transcripts would have produced eight different fictions — eight inventions
257
+ of what a `grep` returns, eight decisions about what a cancelled call says, and
258
+ a reader who cannot compare two of them because they are not looking at the same
259
+ run.
260
+
261
+ So the run is data, in the corpus (`@repo/specimens`) rather than in the
262
+ package: fourteen tool payloads — read, glob, grep, bash, edit, write, patch,
263
+ plan, todowrite, task, webfetch, websearch, question, mcp — each readable at any
264
+ of six statuses through one total function, `withStatus(fixture, status)`, whose
265
+ totality the suite pins at all eighty-four pairs. Beside them, fourteen message
266
+ samples: turns with mentions and attachments, three reasoning blocks, five
267
+ answers including the one with a table and the one with sources, and `streamed`
268
+ for the part arriving a word at a time.
269
+
270
+ `CANONICAL_TRANSCRIPT` is what they compose into — the dark-mode-toggle run the
271
+ Cursor evidence recorded, in twenty-three beats over two turns: read, grep,
272
+ glob, plan, edit, shell, web search, a markdown answer, then a follow-up whose
273
+ edit streams in. A transcript is not a script; it is the _content_ of one, with
274
+ no opinion about how a beat is drawn. `transcriptScript(transcript, scene)` is
275
+ the join: hand it the way your surface draws a beat and it hands back a
276
+ `ReplayScript` with the frame count, the state labels and the captions already
277
+ right.
278
+
279
+ ## Eight studies, at fidelity, named
280
+
281
+ The gallery gains eight pages, all playing that same twenty-three-beat run, so
282
+ the only thing that differs between any two of them is the chrome.
283
+
284
+ Five are **fidelity studies of named consumer products** — ChatGPT, Claude,
285
+ Gemini, Grok and Perplexity — drawn to the trade dress the research recorded:
286
+ ChatGPT's always-visible action bar against Claude's hover-and-focus one,
287
+ Gemini's gradient-carried surface with no avatars, Grok's near-monochrome fold
288
+ of consecutive calls into one group, Perplexity's answer-engine shape where the
289
+ question becomes a heading over a strip of sources. Three are **studies of
290
+ IDE-style consoles** — Claude Code's timeline rail and number-keyed permission
291
+ card, Cursor's five-level conversation-density switch, Hermes's composer status
292
+ stack with approval answered inline.
293
+
294
+ These are internal reference and they say so on their own faces: every one of
295
+ the eight carries a header note naming the product and its maker, stating that
296
+ the page is a fidelity study drawn from the elements family, that it is **not a
297
+ cue product surface**, and that it is not affiliated with the maker — beside a
298
+ chip that says whether the palette was quoted from the record or matched to it.
299
+ Nothing here is a template to ship a product on. They exist so the family can be
300
+ judged against the surfaces it will be asked to imitate.
301
+
302
+ Two gallery axes make them linkable: `surface` (`cue` or `aui`, the value-set
303
+ the page's `AgentSurface` island paints in) and `t`, the frame the replay opens
304
+ on. Both compose with theme, density, mode and device rather than overriding
305
+ any, and `t` is optional rather than defaulted — a stamped `t=0` pauses the run
306
+ on its first frame, while no `t` at all lets it play, and defaulting would have
307
+ turned every copied link into the paused one.
308
+
309
+ The corpus and the site are `private: true` and none of this reaches the
310
+ tarball. It is here because it is the evidence for the minor above: a transport
311
+ nobody has driven through twenty-three states on eight different chromes is a
312
+ transport nobody has tested.
313
+
314
+ ## What did not change
315
+
316
+ The root barrel and `@cueplusplus/ui/elements` gained nothing — no export, no
317
+ peer, no bytes. The transport is chrome _around_ the elements family rather than
318
+ a member of it, so the group's import graph stays exactly as the vendor sync
319
+ emitted it. No component's props, structure or rendering moved; `elements.css`,
320
+ the island, the tokens and the sync pin are untouched.
321
+
322
+ - 7fb3004: ## Model output, rendered
323
+
324
+ Every component in this library has been able to say one thing about a string a
325
+ model wrote: it goes into the DOM as characters. `Message` places it,
326
+ `ToolCallCard` places it, all ninety-four elements place it, and the `Chat`
327
+ group's doctrine header said so in a line — **text is text**. That line was
328
+ doing real work. A component that quietly parsed model output would put every
329
+ consumer of this library one prompt away from an injection, because the
330
+ practical shape of prompt injection is not "the model turned evil", it is "the
331
+ model faithfully relayed bytes an attacker left on a page it fetched".
332
+
333
+ Verbatim is still the default, everywhere, unchanged. What this release adds is
334
+ the one exception, and everything about how it ships is an argument that it
335
+ should stay one: **`MarkdownText`**, on its own subpath
336
+ `@cueplusplus/ui/elements/markdown`, with its own two peers, not re-exported
337
+ from `@cueplusplus/ui/elements` and never going to be.
338
+
339
+ ```tsx
340
+ import { Message } from "@cueplusplus/ui";
341
+ import { MarkdownText } from "@cueplusplus/ui/elements/markdown";
342
+
343
+ <Message agent="researcher">
344
+ <MarkdownText streaming={turn.status === "running"}>
345
+ {turn.text}
346
+ </MarkdownText>
347
+ </Message>;
348
+ ```
349
+
350
+ `Message` did not change to make that work. It places what it is given, and what
351
+ it is given is a string until a caller writes the line above — which is the
352
+ whole design: rendering markup is a line of code somebody wrote, not a behaviour
353
+ they inherited.
354
+
355
+ ### What renders
356
+
357
+ Twenty tags, written out in the source rather than inherited from
358
+ `hast-util-sanitize`'s `defaultSchema`, because a spread schema means the set of
359
+ tags this library vouches for is whatever a transitive dependency decided this
360
+ week.
361
+
362
+ | | |
363
+ | ------ | ---------------------------------------------------------------------------------------------------------------------- |
364
+ | blocks | paragraphs, ATX and setext headings, bullet and ordered lists, block quotes, thematic breaks, fenced and indented code |
365
+ | inline | emphasis, strong, strikethrough, inline code, links, images, hard breaks |
366
+
367
+ Headings start at `h2` and clamp at `h6` — model output is a passage inside a
368
+ page that already owns its `h1` — and `headingLevel` moves the base. Fenced
369
+ blocks are `<pre><code>` containing the model's characters and nothing else
370
+ until `renderCodeBlock` is passed: a highlighter is a second parser over the
371
+ same untrusted string, usually one that emits HTML, so opting into it is
372
+ deliberate. Every part carries a `data-slot`, so an app restyles a heading or a
373
+ quote from the outside without a prop for it.
374
+
375
+ ### What is dropped
376
+
377
+ **Raw HTML has no path to the DOM, and the first reason is an absence rather
378
+ than a setting.** `react-markdown` spreads its own `allowDangerousHtml: true`
379
+ over whatever `remarkRehypeOptions` it is given, so that option cannot be turned
380
+ off from here and `<img onerror=…>` does become a `raw` node. A `raw` node is a
381
+ span of source text rather than an element, and the one thing that turns it back
382
+ into markup is `rehype-raw`. Nothing in this package depends on that plugin, and
383
+ a test asserts it never will — a `pnpm add rehype-raw` is a failing build, not a
384
+ quiet demotion of defence-in-depth to defence. `rehype-sanitize` then drops the
385
+ `raw` node type outright, and `skipHtml` removes whatever survived. Three gates,
386
+ each of them disabled in turn in the suite to prove they are redundant rather
387
+ than load-bearing one at a time.
388
+
389
+ None of the three is a deny-list of dangerous tags. A `<script>`, an `<iframe>`,
390
+ an `<svg>`, a `<form>` and an `onclick=` are the same non-event: there is
391
+ nothing in the tree for them to be.
392
+
393
+ **Attributes are enumerated, not filtered.** An element outside the subset
394
+ renders nothing; an element inside it renders only the attributes named beside
395
+ it — `href`/`title` on a link, `src`/`alt`/`title` on an image, `start` on an
396
+ ordered list, a single `language-…` class on a `code`. Everything else gets the
397
+ empty list. No element in the subset accepts `id`, `style`, `srcset`,
398
+ `formaction` or any `on*` handler, so none of them has an attribute to arrive
399
+ on. Comments and doctypes are refused, and DOM clobbering has no attribute to
400
+ travel on.
401
+
402
+ **URLs are allow-listed twice, and to three protocols.** `http`, `https` and
403
+ `mailto`, on both `href` and `src`. A relative URL stays relative; a
404
+ scheme-relative `//host` does not. A `javascript:` link, a `data:text/html`
405
+ image, `vbscript:`, `blob:`, `file:` and the `irc:`/`xmpp:` handlers React's
406
+ default pipeline permits all lose the attribute and leave an inert element
407
+ behind — an anchor with no destination, not an anchor to the page you are on.
408
+
409
+ **Every link carries `rel="noopener noreferrer nofollow"`, and that is not a
410
+ prop.** Reverse tabnabbing, a private console URL travelling in a `Referer`
411
+ header, and SEO laundering through somebody's transcript are not things a caller
412
+ should be able to turn off by accident. `target` is configurable (`_blank` by
413
+ default, because a transcript that navigates away loses the run); `rel` is not.
414
+ Images are `loading="lazy"`, `decoding="async"`, `referrerPolicy="no-referrer"`.
415
+
416
+ No GFM plugin is enabled, so there are no tables, task lists, footnotes or
417
+ autolink literals — an explicit `<https://…>` autolink still works. Task lists
418
+ are absent on purpose as much as by omission: a checkbox inside a transcript is
419
+ a control a model drew.
420
+
421
+ The threat model above is on the component, in its JSDoc, which means it is on
422
+ the docs page, in the manifest and in a consumer's hover — not in a wiki that
423
+ drifts from the code. It is held by 122 adversarial assertions run three ways:
424
+ settled, streaming, and at every prefix of a hostile stream.
425
+
426
+ ### Streaming
427
+
428
+ Pass `streaming` while the turn is arriving and the token the model is halfway
429
+ through typing is hidden rather than rendered as punctuation. Streaming markdown
430
+ is not malformed markdown — CommonMark has an answer for every truncation, and
431
+ that answer is the problem: `**bo` is a paragraph containing two literal
432
+ asterisks, and three frames later it is bold text and the asterisks are gone.
433
+ The visible result is punctuation flickering through every emphasis, link and
434
+ code span as it arrives, with a reflow behind it.
435
+
436
+ Six rules, covering unclosed fences, odd inline-code runs, half-written links
437
+ and autolinks, and unmatched emphasis openers — the words appear the moment the
438
+ model writes them and only their formatting waits for the closing token. None of
439
+ it is a second parser and none of it can widen what renders: every rule only
440
+ ever _deletes_ characters before the source reaches the pipeline, so a sealed
441
+ stream passes the same three gates as a settled document.
442
+
443
+ ### The line in `Chat` that changed
444
+
445
+ One doctrine bullet, from "**Text is text.** Nothing here parses markdown or
446
+ HTML" to "**Text is text, unless somebody says otherwise.**" — the same default,
447
+ now named as a default rather than as the only option, pointing at the renderer
448
+ for when a consumer wants it. No component in that group changed.
449
+
450
+ ## The vocabulary a model composes
451
+
452
+ The second half of this release is the other end of the same problem. A model
453
+ that can only answer in prose answers a flight query in prose. `GenerativeUI`
454
+ takes a tree of JSON a model emitted — 27 intrinsic names, nested — and draws
455
+ it.
456
+
457
+ ```tsx
458
+ import { GenerativeUI } from "@cueplusplus/ui/elements/generative";
459
+ import "@cueplusplus/ui/elements/generative.css";
460
+
461
+ // A `GenerativeAction` carries the intrinsic that fired it as `type`, so it is
462
+ // named rather than spread over a message discriminator of the same name.
463
+ <GenerativeUI
464
+ spec={JSON.parse(toolResult)}
465
+ onAction={({ action, type, value }) =>
466
+ send({ type: "ui-action", action, intrinsic: type, value })
467
+ }
468
+ onFault={(fault) => telemetry.warn("generative", fault)}
469
+ />;
470
+ ```
471
+
472
+ | | the 27 |
473
+ | -------------- | --------------------------------------------------------------------- |
474
+ | layout (7) | `Card` `Row` `Col` `Box` `Spacer` `Divider` `Carousel` |
475
+ | typography (4) | `Header` `Text` `Caption` `Markdown` |
476
+ | data (4) | `Fact` `Table` `Chart` `Badge` |
477
+ | media (2) | `Image` `Icon` |
478
+ | controls (7) | `Button` `Select` `Input` `DatePicker` `Checkbox` `RadioGroup` `Form` |
479
+ | lists (2) | `ListView` `ListViewItem` |
480
+ | feedback (1) | `Alert` |
481
+
482
+ Every other component here is handed props by a programmer, and a wrong one
483
+ costs a type error at the call site. This one is handed a tree by a language
484
+ model over a network, so `spec` is typed `unknown` — a signature demanding a
485
+ node would push every consumer into the same cast, which is a claim about the
486
+ bytes nobody is in a position to make — and the interesting question is not what
487
+ a correct spec renders as. It is what an incorrect one does, and the answer is
488
+ never an exception:
489
+
490
+ - a `$type` from a newer catalogue draws an in-place notice and its siblings
491
+ still render;
492
+ - a spec that is not a component tree draws a legible error state, never a blank
493
+ box that looks like a message still streaming;
494
+ - a tree deeper than 32 levels stops and says so rather than exhausting the
495
+ stack and taking the console with it;
496
+ - a component that throws anyway is caught by a boundary the renderer mounts
497
+ above its own walk.
498
+
499
+ All four also arrive as a stream on `onFault` — `not-a-node`, `unknown-type`,
500
+ `missing-prop`, `too-deep`, each with a dotted path — because the person who
501
+ needs to know this console is a version behind the model driving it is usually
502
+ not the person looking at it.
503
+
504
+ Two gates keep a widget from becoming a beacon. `Box` and `Card` take a
505
+ `background` and a size the model writes freely, and those land in an inline
506
+ style: colours are allow-listed by _shape_ (a hex triple, a numeric colour
507
+ function, a bare keyword) so `url(…)` and `image-set(…)` cannot reach a
508
+ `background`, and lengths must be a plain number-and-unit, which is enough for
509
+ `50%`, `12rem` and `320px` and not enough for `calc(…)` or `var(…)`.
510
+
511
+ ### It needs no Tailwind
512
+
513
+ The stylesheet is plain `[data-aui]` CSS and the tree it draws carries no
514
+ `class` attribute anywhere. That is upstream's arrangement and it is worth
515
+ keeping precisely: a widget a model composed has to draw inside whatever console
516
+ it was sent to, and a console is very often somebody else's app with no utility
517
+ framework, no shadcn and no design system of its own. Every hook is an attribute
518
+ rather than a class, because a class is a claim on a name in a host's one global
519
+ namespace and `[data-aui="card"]` collides with nothing.
520
+
521
+ The consequence runs all the way down. The 24 icons are hand-drawn SVG rather
522
+ than `lucide-react`; the chart is hand-drawn SVG rather than `recharts`; the
523
+ carousel snaps with CSS rather than a script. **`./elements/generative` declares
524
+ no optional peer at all** — an empty row in the dist-contract's peer map, which
525
+ is that claim checked against the built package — so there is no package in the
526
+ tree whose absence turns a weather card into a stack trace.
527
+
528
+ The sheet has two honest modes, and one seam between them. Exactly one rule in
529
+ 1,100 lines reads `--cue-*` tokens, and every reference in it carries a literal
530
+ fallback: with `@cueplusplus/ui/styles.css` present the vocabulary is cue's
531
+ palette, type ladder and density; without it, it is a plain legible widget that
532
+ a `<link>` away works in a host that has never heard of cue. Every other rule
533
+ spends `--aui-*` and nothing else, which is what makes a re-skin forty
534
+ declarations in one place rather than a hunt through two hundred rules.
535
+
536
+ ### Both skins reach it, two ways
537
+
538
+ The vocabulary answers the same two axes `AgentSurface` publishes, and it
539
+ answers them whether they arrive as props or by inheritance:
540
+
541
+ ```tsx
542
+ <GenerativeUI spec={spec} skin="aui" fidelity="upstream" />;
543
+
544
+ <AgentSurface skin="aui">
545
+ <GenerativeUI spec={spec} /> {/* no props: the stamp is on an ancestor */}
546
+ </AgentSurface>;
547
+ ```
548
+
549
+ Every island rule that lands on the root is written with both selectors — the
550
+ stamp on an ancestor, and the stamp on the root itself — so a tree that is its
551
+ own island and a tree inside a transcript paint identically.
552
+
553
+ - **`skin`** — `cue` by default. `aui` is upstream's own
554
+ `data-aui-theme="elements"` layer, transcribed: a 20px card, a 12px borderless
555
+ field on a filled ground, a pill button, a shadow instead of a hairline, and
556
+ labels that stop shouting. Geometry only — the colour re-pointing arrives
557
+ through `elements.css`, so an `aui`-skinned widget still tracks the app's
558
+ light and dark rather than pinning a palette of its own.
559
+ - **`fidelity`** — `cue-metrics` by default, which drives the five density knobs
560
+ upstream names (`--aui-control-height`, `--aui-control-font-size`,
561
+ `--aui-button-padding-x`, `--aui-field-padding-x`, `--aui-card-padding`) and
562
+ the five type steps off cue's ladder. `upstream` pins assistant-ui's own
563
+ numbers instead — a 36px control, 14px control type, `px-4`, `px-3`, `p-5` —
564
+ so density stops at the island edge. The two blocks are line for line, and a
565
+ test keeps them that way: a property one pins and the other does not restore
566
+ is a value that survives a nesting, and the subtree would draw at neither
567
+ fidelity.
568
+
569
+ ### Where the two halves meet
570
+
571
+ Once, through one prop. The `Markdown` intrinsic renders its source verbatim —
572
+ as characters — unless `renderMarkdown` is given, because the generative subpath
573
+ ships no parser and will not acquire one:
574
+
575
+ ```tsx
576
+ <GenerativeUI
577
+ spec={spec}
578
+ renderMarkdown={(md) => <MarkdownText>{md}</MarkdownText>}
579
+ />
580
+ ```
581
+
582
+ Neither subpath depends on the other. A console drawing weather cards installs
583
+ no sanitizer; a console rendering markdown installs no vocabulary.
584
+
585
+ ## Where to look
586
+
587
+ Benched at the states that matter rather than at the happy path. `MarkdownText`
588
+ on `elements-thread`: a settled document exercising the whole subset, whose last
589
+ line is a real injection shape and renders as nothing; a turn cut mid-token; a
590
+ fence still filling. `GenerativeUI` on `elements-knowledge`: a composed
591
+ dashboard, a form with all six control types, a chart, an unknown component, and
592
+ a spec that is not a tree at all. Pages are at `/docs/components/markdown-text`
593
+ and `/docs/components/generative-ui`.
594
+
595
+ ## What did not change
596
+
597
+ The root barrel, and `@cueplusplus/ui/elements`. Both entries gained nothing —
598
+ no export, no peer, no bytes. `react-markdown` and `rehype-sanitize` are
599
+ optional peers of `./elements/markdown` alone, externalised rather than inlined
600
+ for the usual reason and one more: bundling a sanitizer would freeze a security
601
+ boundary at whatever version was in the store the day this package was built,
602
+ where a consumer's `pnpm update` could never reach it.
603
+
604
+ No component's props, structure or rendering moved. The `Chat` group is the same
605
+ six components it was; `Message`, `AskBox`, `ToolCallCard` and all ninety-four
606
+ elements still render what they are given as characters. `elements.css`, the
607
+ island, the tokens and the sync pin are untouched.
608
+
609
+ One accessibility fix, found by running axe over the specs a model can emit
610
+ rather than over the benches: a `ListViewItem` a model put outside a `ListView`
611
+ drew as `role="listitem"`, which requires a `list` parent it did not have. A
612
+ stray row now draws as a row and says nothing about where it is, which is true.
613
+
614
+ `schemaVersion` is still `1`.
615
+
616
+ - 42e0672: ## One voice at the foot of the transcript
617
+
618
+ Two elements in this family are live regions. `TailStatus` is a `role="status"`,
619
+ because that is what makes the row at the foot of a transcript reach a reader
620
+ who cannot see the spinner. `LiveRegionAnnouncer` is a polite region with a
621
+ queue, and the nine transitions in `ANNOUNCEMENTS` are the same states that row
622
+ narrates.
623
+
624
+ A console that mounted both — the ordinary shape of an agent console, and the
625
+ one the benches show — had every state read out twice, out of step, because the
626
+ two regions update at different moments. Neither file mentioned the other.
627
+
628
+ They compose now, and the rule is that **the announcer speaks and the row
629
+ draws**:
630
+
631
+ ```tsx
632
+ <TailStatus action={TAIL_ACTIONS.searchingCode} detail="composer.tsx" />;
633
+ <LiveRegionAnnouncer messages={log} />;
634
+ ```
635
+
636
+ Mounting an announcer takes the speech off the row automatically. The words stay
637
+ on the screen, the `role="status"` goes, and `data-announces="false"` says which
638
+ of the two it is doing. Nothing to wire: the row notices on its own, wherever in
639
+ the tree either of them sits.
640
+
641
+ The row is the half that defers because the announcer is the better speaker. It
642
+ covers transitions the row can never show — an error, a permission request, a
643
+ finished answer. It is mounted empty and stays mounted, which is the only way a
644
+ live region is announced reliably, while a tail row comes and goes with the run.
645
+ And its whole mechanism — 400ms of spacing, five deep, paused while the page is
646
+ hidden or a modal has the reader — exists because narration that changes several
647
+ times a second is unusable when it is spoken, which is exactly what the row is.
648
+
649
+ Two ways to decide it by hand:
650
+
651
+ - `<TailStatus announce />` — the row speaks even with an announcer mounted, for
652
+ a console whose announcer is fed something other than these actions.
653
+ - `<TailStatus announce={false} />` — the row never speaks, for a console that
654
+ narrates the foot of the transcript some other way.
655
+
656
+ And `useAnnouncerPresent()` is exported, so a console's own status row can ask
657
+ the same question before becoming a second live region.
658
+
659
+ - 51e5046: ## The amber carries its own ink too
660
+
661
+ This is the amber half of the argument `the-red-carries-its-own-ink` already
662
+ made, and it is the same argument: the tone a control is _filled_ with decides
663
+ what can be read on it, and no other token in the palette knows that.
664
+
665
+ `TwoStepButton` is the one control in the library whose whole reason for
666
+ existing is that the press is consequential — arm, then confirm. Its `warn`
667
+ confirm painted `bg-warn text-black`. Black on an amber is a good guess, and on
668
+ the ten dark blocks it is right by a distance: 7.11:1 to 12.41:1. It is wrong on
669
+ the light ones, because a light theme darkens its amber to clear the paper and a
670
+ darkened amber wants paper back. **3.58:1** on requestport, **3.74:1** on dusk,
671
+ **4.22:1** on cue's own light block — three of the twenty preset x mode blocks
672
+ under AA, on the half of the control that does the thing.
673
+
674
+ It could not be closed before: there was no `--cue-warn-fg` to close it with,
675
+ and the queued note for the red said so in as many words. There is one now.
676
+
677
+ ### `--cue-warn-fg`
678
+
679
+ A new authored anchor, per preset, per mode, in the colour contract beside the
680
+ tone it is chosen against, with `text-warn-fg` / `bg-warn-fg` bridged into
681
+ Tailwind like every other colour token:
682
+
683
+ ```css
684
+ [data-theme="cue"] {
685
+ --cue-warn: oklch(0.8 0.17 75);
686
+ --cue-warn-fg: #000000;
687
+ }
688
+ [data-theme="cue"][data-mode="light"] {
689
+ --cue-warn: oklch(0.55 0.115 70);
690
+ --cue-warn-fg: #fcfcfc;
691
+ }
692
+ ```
693
+
694
+ Not one value is hand-picked. Each is the same `pickInk` `createTheme()` already
695
+ uses for `accent-fg` and `danger-fg`, run over the extremes the theme itself
696
+ owns — its page, its deepest well, its type — falling to pure black or white
697
+ only where none of the three reaches AA. Run over the reds instead, it
698
+ reproduces all twenty shipped `danger-fg` values exactly, spelling included,
699
+ which is the check that this is the same derivation rather than a second one
700
+ wearing its name.
701
+
702
+ The amber does not answer the way the red did, and that is the argument for
703
+ deriving against each tone instead of writing one rule about ink on a status
704
+ colour. All ten dark blocks take their deepest ground. The light ten split three
705
+ ways: cue, dusk and requestport take their paper; hivehub, luma, signal and
706
+ terminal take the theme's own type; quotamate, snuffle and venu take plain black
707
+ because nothing they own reaches AA on their amber. The worst amber pair in the
708
+ system is **4.77:1** (luma, light). The worst pair anywhere is still the red's
709
+ 4.59:1.
710
+
711
+ `CONTRAST_REQUIREMENTS` gains an eleventh pair, `warn-fg/warn` at 4.5:1, so a
712
+ generated palette is judged on it too, and `createTheme()` derives it for one.
713
+ The configurator shows it beside the amber in the Status group. The assistant-ui
714
+ island declares its own, because that block re-tints `--cue-warn` and an ink
715
+ chosen against the app's amber is an ink chosen against a colour nobody is
716
+ looking at in there.
717
+
718
+ ### The sweep covers the amber now
719
+
720
+ `test/destructive-ink.test.ts` reads the source and measures what the components
721
+ _spend_ rather than what the palette _offers_. It swept the red and said in its
722
+ own prose that it did not sweep the amber, because sweeping a defect nobody
723
+ could fix turns a missing token into a red suite instead of into a decision. The
724
+ token exists, so the amber is in: every class string that makes `bg-warn` a
725
+ ground and puts an ink on it, every rule in the package's own stylesheets that
726
+ does the same, measured on all twenty blocks at 4.5:1, with the fills pinned by
727
+ name per tone so a new one has to choose its ink in the commit that paints it. A
728
+ tint keeps its exemption — `bg-warn/10 text-warn` and the search highlight's
729
+ `bg-warn/35 text-fg` are read against the page, and the report already owns
730
+ those pairs — and the file says out loud what that exemption does not cover.
731
+
732
+ One number beside it got more honest. Both that sweep and
733
+ `everywhere-the-red-is-spent` said the right ink "falls to 3.95:1 on quotamate's
734
+ dark" under `bg-danger/90` without naming the ground the alpha let through. It
735
+ is **3.94:1** over `--cue-sunken` and **3.97:1** over `--cue-bg`. That the two
736
+ grounds disagree at all is the reason an alpha is refused rather than measured.
737
+
738
+ ### And the amber's pair is swept on its own, the way the red's is
739
+
740
+ `destructive-ink.test.ts` covers `warn-fg` on `warn` across all twenty blocks
741
+ for exactly as long as `TwoStepButton` keeps painting that string, because it
742
+ measures the call site. The red does not depend on that: it has a second,
743
+ unconditional sweep of the token pair beside `RevertDock` in
744
+ `elements/canon.test.tsx`. The amber had no equivalent, and the gap is not
745
+ theoretical — with the confirm moved off the amber in the way a later refactor
746
+ legitimately might, and that file's pinned list updated to match, a `warn-fg`
747
+ re-tinted down to **1.62:1** passed every contrast guard in the package.
748
+
749
+ So `instruments/two-step-button.test.tsx` gains the sweep the red has, asking
750
+ nothing about what any component currently paints, plus the spot check that says
751
+ this confirm takes the amber at full strength in the amber's own ink rather than
752
+ the `text-black` it used to.
753
+
754
+ ### If you ship your own theme block
755
+
756
+ A hand-written `[data-theme="acme"]` block should declare `--cue-warn-fg`
757
+ alongside `--cue-warn`, the same way it now declares `--cue-danger-fg` alongside
758
+ `--cue-danger`. Without it, `text-warn-fg` resolves to nothing on your theme.
759
+ Themes generated by `createTheme()` and the configurator's exports already carry
760
+ it.
761
+
762
+ - 704f9e5: ## The canon
763
+
764
+ The elements family this library spent four releases adopting is ninety-five
765
+ files deep and it is one product's answer to what a transcript needs. A very
766
+ good one — and one. So before closing the adoption, four teardowns went reading
767
+ the others: Cursor and Trae's local bundles, Claude Code's editor panel, the
768
+ Codex extension, and the open-source field — opencode, Cline, Roo, Continue,
769
+ Void, Zed, OpenHands, Hermes. One question each. **Which rows do three or more
770
+ shipping agent UIs have, that no component catalogue ships at all?**
771
+
772
+ Seventeen answers, and they are in this release. They live in
773
+ `@cueplusplus/ui/elements` beside the vendored family, in the same barrel,
774
+ inside the same island, on the same `data-slot` conventions — but they are
775
+ cue's, hand-written in house style rather than fetched and rewritten, and the
776
+ sync codemod never sees them, so there is nothing here for a re-sync to revert.
777
+ Every file opens with a comment naming the products that justify it and the
778
+ report section that recorded it.
779
+
780
+ ```tsx
781
+ import {
782
+ CompactionRow,
783
+ TailStatus,
784
+ TurnFooter,
785
+ WorkCollapse,
786
+ verbFor,
787
+ } from "@cueplusplus/ui/elements";
788
+
789
+ <WorkCollapse
790
+ durationMs={252_000}
791
+ toolCalls={14}
792
+ open={open}
793
+ onOpenChange={setOpen}
794
+ >
795
+ {rows}
796
+ </WorkCollapse>;
797
+
798
+ <CompactionRow
799
+ state="completed"
800
+ tokensBefore={128_000}
801
+ tokensAfter={24_000}
802
+ messagesBefore={214}
803
+ messagesAfter={31}
804
+ />;
805
+
806
+ <TurnFooter
807
+ onCopy={copy}
808
+ onFork={fork}
809
+ timestamp={{
810
+ label: "2m ago",
811
+ title: "Today at 2:14 PM",
812
+ subtitle: "Worked for 2m 15s",
813
+ }}
814
+ />;
815
+
816
+ <TailStatus
817
+ action={verbFor("shell", "active")}
818
+ detail="pnpm test"
819
+ onCancel={stop}
820
+ />;
821
+ ```
822
+
823
+ ### The seventeen, and who else ships them
824
+
825
+ | | | |
826
+ | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
827
+ | `TailStatus` | the live row at the foot: an action, a detail that truncates on its own, an inline Cancel that exists only when there is something to cancel | Cursor's `AgentTranscriptTailStatus`, opencode's action vocabulary |
828
+ | `WorkCollapse` | `Worked for 4m 12s and made 14 tool calls`, expanding back to the rows it folded | Cline's `WorkActivity`, Hermes's run collapsing |
829
+ | `TimeBoundary` / `UnreadDivider` | the two transcript rules nobody documents — one row each, placed by the caller | Cursor's row union (the only exhaustive one in any shipping product) |
830
+ | `TurnFooter` / `EndOfTurnSummary` | copy / reply / fork with a three-level timestamp, and `3 files changed +48 −12` | Cursor; the diff-stat half also Zed, Roo, Cline |
831
+ | `CompactionRow` | `128k → 24k tokens · 214 → 31 messages`, in all five states | Cline, Zed, Roo, Continue |
832
+ | `QueueDock` | what you typed while it was working — foldable, with edit / send-now / remove on every entry | opencode's `followupDock`, Cursor, Cline, Roo, Zed |
833
+ | `RevertDock` | restore, on the files-versus-conversation axis, behind a second press | Roo's `CheckpointMenu`, Cline, Cursor, opencode |
834
+ | `ContextUsage` | a segmented bar, a `Category / Tokens / Usage` table, and the stat rows that turn a gauge into a bill | Cursor's tray, opencode's context tab, Claude Code's modal |
835
+ | `PermissionScopes` | approval whose "always allow" writes its own label and names the file it writes to | Claude Code's scope cycler, Zed's granularity dropdown, Roo, Cline, Void |
836
+ | `AgentModeBadge` | how much authority the run is holding, as a chip and as `data-agent-mode` | Claude Code's mode-tinted chrome, Cursor's mode tokens |
837
+ | `RiskBadge` | an explicit `unknown` / `low` / `medium` / `high` on the call, with the qualifier beside it | OpenHands' `SecurityRisk`; the qualifier from Cline, Roo, Zed |
838
+ | `VerdictRow` | a scored verification result on a finished turn — rating, score, and what is most likely wrong | OpenHands' critic display, generalised past the critic |
839
+ | `AnimatedNumber` | a token, cost or duration readout that counts to its new value in tabular numerals | Claude Code's live thinking-token counter |
840
+ | `Clamp` | the truncation trio — a height, a fixed-pixel dissolve, a way back | Claude Code's five separate truncations |
841
+ | `LiveRegionAnnouncer` | one polite atomic region and the queue that makes it audible | Claude Code's announcement queue, Codex's announcement strings |
842
+
843
+ Nothing here shipped on taste. The rule for the phase was that a component
844
+ which could not point at a report section did not ship, and the two that are
845
+ gaps rather than crowds say so in their own files: `RiskBadge` exists because
846
+ six of the seven open-source UIs encode risk _implicitly_ — you reconstruct how
847
+ dangerous a call is from which approval bucket its tool happened to land in —
848
+ and one, OpenHands, makes it a property of the action. `VerdictRow` exists
849
+ because every agent runtime now scores its own work somewhere and, in six of
850
+ seven, that judgement reaches the reader as prose or not at all.
851
+
852
+ ### Copy is data, not markup
853
+
854
+ The tense triad is the single most repeated pattern in the whole study.
855
+ `Run` / `Running` / `Ran`. Cursor keeps it in `tool-action-labels.js`, Void as
856
+ `{proposed, running, done}`, Continue as `{wouldLikeTo, isCurrently,
857
+ hasAlready}`, Hermes as five category verbs, opencode inside an i18n dictionary
858
+ its own `AGENTS.md` forbids anyone to bypass. Six products, the same three
859
+ strings, private to the app every single time.
860
+
861
+ Here they are tables. `VERB_TRIADS` with `verbFor()` over it — which falls back
862
+ to `Use` / `Using` / `Used` for a kind it does not know, because an MCP server
863
+ names its own tools and a row forced to choose between `undefined` and the raw
864
+ wire name will render the wire name. Beside it: `TAIL_ACTIONS` (opencode's ten
865
+ status phrases), `TURN_ACTION_LABELS`, `COMPACTION_LABELS`, `QUEUE_LABELS`, the
866
+ revert triple, the context set, the authority ladder, the permission pair, the
867
+ two judgements, `CLAMP_LABELS` and the nine `ANNOUNCEMENTS`.
868
+
869
+ ```tsx
870
+ import { VERB_TRIADS, verbFor } from "@cueplusplus/ui/elements";
871
+
872
+ // Your product's agent renders rather than uses.
873
+ const VERBS = {
874
+ ...VERB_TRIADS,
875
+ render: { proposed: "Render", active: "Rendering", past: "Rendered" },
876
+ };
877
+ ```
878
+
879
+ Every component that draws one of these takes the copy as a prop and defaults
880
+ to the table. Localising a console, or writing for a product whose agent
881
+ "edits" where this one "patches", is spreading a table — not forking a
882
+ component to change a word.
883
+
884
+ ### The mode ladder is a token contract, not a colour prop
885
+
886
+ The most distinctive systemic idea in the study is not a component. Claude
887
+ Code's panel puts one attribute on the composer, the send button and the
888
+ spinner, and each re-derives its accent from it — six modes painted in four
889
+ colours, because what a mode _means_ is how much authority the run is holding,
890
+ and that ladder has four rungs everywhere. Cursor reaches the same place from
891
+ the token side: a quadruple per mode that the chip, the composer border and the
892
+ transcript accent all read.
893
+
894
+ So `@cueplusplus/ui/elements.css` gains sixteen properties — four per rung,
895
+ declared under `[data-agent-mode]` — and four aliases pointing at whichever
896
+ rung the attribute names:
897
+
898
+ ```css
899
+ .composer:focus-within {
900
+ border-color: var(--cue-elements-mode-icon);
901
+ }
902
+ .send-button {
903
+ background: var(--cue-elements-mode-icon);
904
+ color: var(--cue-elements-mode-background);
905
+ }
906
+ .turn-rail {
907
+ background: var(--cue-elements-mode-border);
908
+ }
909
+ ```
910
+
911
+ Stamp `data-agent-mode="unsafe"` on the composer and all three follow, with no
912
+ prop threading and no branch in any component. `AgentModeBadge` is the readout
913
+ over the same four properties, not the API.
914
+
915
+ They are declared under the attribute rather than at `:root` on purpose: a
916
+ custom property's `var()` references resolve where they are declared, so a rung
917
+ declared at `:root` would paint a badge inside an `<AgentSurface skin="aui">`
918
+ in the console's palette while everything around it painted in the island's.
919
+ The default rung is the family's neutral, not `manual`, because an unmapped
920
+ mode painted brand-orange would be claiming the run asks before it acts, and
921
+ that is the one claim nobody should make on a guess. Those same four neutrals
922
+ are the badge's inline fallbacks, so a consumer who never imports
923
+ `elements.css` gets a legible chip that makes no claim rather than an invisible
924
+ one — and the unpainted and the unmapped chip look alike deliberately.
925
+
926
+ ### The controls that are not allowed to lie
927
+
928
+ Three of the seventeen sit where a control that misstates what it is about to
929
+ do is a defect rather than a polish item.
930
+
931
+ **`PermissionScopes`** is the one control in an agent UI whose label has to be
932
+ computed, because what it grants is different every time it appears:
933
+ `Yes, allow Bash(cue patch:*) for this project (just you)`, with the file that
934
+ grant is saved to in a sentence underneath. Two departures from the reference,
935
+ both in the same direction. Nothing is elided — the reference shortens a rule
936
+ past twenty characters and strips the `:*` off a prefix rule, and both cuts
937
+ land on exactly the part that says how far the grant reaches, so this label
938
+ wraps instead — inside the word where it has to, since a rule and a directory
939
+ are one word to a line breaker and an underscored path gives it nowhere else
940
+ to go, and so does every other string the caller hands it: the subject beside
941
+ the question and the sentence naming the file the grant is written into break
942
+ the same way, because the account of where a permission lands is no more
943
+ allowed off the edge of the card than the account of what it grants. And when
944
+ there are more rules than the sentence can name, every one of them is listed
945
+ underneath, because `and 3 more` is a count and a count is not an account. No scope means no always-allow answer at all: a standing
946
+ grant that cannot say where it is saved is not one this element will offer.
947
+ The scope table ships the four reaches in words and a file path for none of
948
+ them — where a grant lands is the product's answer, and a design system that
949
+ guessed at it would commit the very defect this element exists to prevent.
950
+
951
+ **`RevertDock`** is the family's one destructive control and is built like one.
952
+ The axis first, because a run changes the files and the conversation about them
953
+ and putting one back is not putting the other back. Then the gate: choosing a
954
+ scope only arms the dock, `onRestore` can fire from the second separate press
955
+ alone, and `This action cannot be undone.` is on the screen the whole time the
956
+ dock is open rather than in a props table somebody reads afterwards. Then the
957
+ geometry, which is the half a gate cannot do on its own: arming does not
958
+ replace the scope list — it marks the chosen scope, disables the list where it
959
+ stands and appends the question _underneath_ it, so the destructive control is
960
+ never drawn over the control that armed it and a double click cannot walk
961
+ through both steps. Its confirm also refuses presses for 400 ms after it
962
+ arrives, and fires once per arm. The way out is `TwoStepButton`'s ruling rather
963
+ than a second one: focus lands on `Keep everything` and never on the
964
+ destructive half, that button comes first in the DOM as well as on the screen,
965
+ `Escape` disarms and stops there rather than closing whatever the dock is
966
+ inside, and folding the dock away disarms it — an armed state nobody can see is
967
+ one somebody re-opens into.
968
+
969
+ **`RiskBadge`** treats `unknown` as a first-class value drawn in the neutral,
970
+ never in the low-risk green: a runtime with no scorer must not be made to look
971
+ like one that scored everything safe. And the qualifier renders beside the
972
+ label rather than hiding in a `title`, because `High risk` alone is a
973
+ temperature while `High risk · outside your workspace` is a fact somebody can
974
+ act on.
975
+
976
+ ### Three that are contracts more than components
977
+
978
+ **`AnimatedNumber`** counts to its new value instead of jumping to it, in
979
+ tabular numerals — both halves lifted from one line of a shipping bundle, and
980
+ the second is not cosmetic: a proportional `1` is narrower than a `0`, so a
981
+ stepping counter nudges everything to its right on every update, which is what
982
+ makes a busy transcript feel unstable. The value stays a prop and every tween
983
+ lands exactly on it. Mid-tween the digits are `aria-hidden` with the settled
984
+ value carried beside them, so a screen reader hears what the run reported and
985
+ never a frame of the animation. Reduced motion is answered by _arriving_: no
986
+ frame is ever scheduled, because a readout that stopped where it was would be
987
+ wrong rather than merely still.
988
+
989
+ **`Clamp`** is a height, a dissolve of a fixed number of pixels, and a way
990
+ back. The fixed depth is the part worth copying — ten pixels of dissolve is the
991
+ same cut over a two-line row and a forty-line one, which is why the reference's
992
+ five separate truncations read as one idea. The fade is a **mask**, not a
993
+ gradient to the surface colour, which is the bug the reference shipped: its
994
+ diff variant hard-codes `#1e1e1e` and smears on a light theme, while a mask
995
+ fades the content itself and has no colour to get wrong in any theme, either
996
+ mode, or inside an island. The trio is published as
997
+ `--cue-elements-clamp-height` / `-fade`, so a height overridden in a stylesheet
998
+ moves the dissolve with it. The reveal is always in the DOM and always
999
+ focusable — a `Show more` chip that appears on hover only is unreachable for
1000
+ anyone not holding a pointer.
1001
+
1002
+ **`LiveRegionAnnouncer`** is the accessibility surface, and it is a queue
1003
+ rather than a live region wired to state, because the naive version announces
1004
+ almost nothing: polite announcements replace each other, so a region bound to a
1005
+ transcript that changes four times a second speaks once and swallows the rest.
1006
+ Four rules make it audible — 400 ms of spacing, five deep with the _oldest_
1007
+ dropped (behind a busy run the newest is the true one), held while the page is
1008
+ hidden or a modal has the reader, and an identical sentence re-announced by
1009
+ appending a space, which changes the string without changing a word of it. The
1010
+ hold is in force from the first render rather than the first effect, so an
1011
+ announcer that mounts into a background tab holding a backlog — a restored
1012
+ session, which is the whole reason `announceOnMount` exists — does not spend a
1013
+ line of it finding out where it is. The
1014
+ region mounts empty and stays mounted, since a live region added at the same
1015
+ moment as its text is one most assistive technologies never announce. One
1016
+ decision moves to the caller: `paused` is a prop rather than an internal
1017
+ ref-count of open overlays, because this library has its own portal contract
1018
+ and an announcer holding a second opinion about what is on screen would be
1019
+ wrong exactly when it mattered.
1020
+
1021
+ ### What did not change
1022
+
1023
+ No new subpath, no new peer, no new group. The seventeen are exports of
1024
+ `@cueplusplus/ui/elements`, drawn with the icon set that subpath already
1025
+ declares, and the root barrel gained nothing. The ninety-five vendored files
1026
+ are untouched and the sync pin, the codemod and `elements-sync.lock.json` are
1027
+ exactly where P0 left them — a re-sync at a newer upstream SHA rewrites the
1028
+ vendored tree and cannot touch a line of this. The tokens are untouched:
1029
+ everything here paints from the contract the flagship release already shipped,
1030
+ and `elements.css` gained one section rather than a new dependency for anyone
1031
+ in cue mode.
1032
+
1033
+ ***
1034
+
1035
+ **This closes the adoption.** `@cueplusplus/ui/elements` is now the ninety-five
1036
+ vendored components, the island that can paint them in either value-set, the
1037
+ sanitized markdown surface and generative vocabulary, the replay transport with
1038
+ its fixture catalogue and eight gallery studies — and the seventeen rows the
1039
+ field study found that nobody was shipping.
1040
+
1041
+ - 06eb4ac: ## The flagship answers in colour
1042
+
1043
+ `cue` is a monochrome brand, and until this release it carried the monochrome
1044
+ all the way through to the one vocabulary that exists to be told apart at a
1045
+ glance. On the default theme `--cue-ok`, `--cue-busy`, `--cue-warn` and
1046
+ `--cue-info` were all `#ffffff` on dark and all `#0a0a0a` on light: four
1047
+ different meanings, one colour, distinguishable only by the glyph or the word
1048
+ beside them. It read as restraint on a settings page. It reads as a defect on a
1049
+ transcript where three tool calls are in three different states and the only
1050
+ question is which of them is still moving.
1051
+
1052
+ They are chromatic now.
1053
+
1054
+ | token | before (dark / light) | after (dark / light) |
1055
+ | -------------- | --------------------- | ----------------------------------------------- |
1056
+ | `--cue-ok` | `#ffffff` / `#0a0a0a` | `oklch(0.72 0.17 149)` / `oklch(0.53 0.14 149)` |
1057
+ | `--cue-busy` | `#ffffff` / `#0a0a0a` | `oklch(0.78 0.13 220)` / `oklch(0.54 0.1 225)` |
1058
+ | `--cue-warn` | `#ffffff` / `#0a0a0a` | `oklch(0.8 0.17 75)` / `oklch(0.55 0.115 70)` |
1059
+ | `--cue-info` | `#ffffff` / `#0a0a0a` | `oklch(0.75 0.12 252)` / `oklch(0.55 0.16 258)` |
1060
+ | `--cue-danger` | `#ff5f57` / `#d03030` | unchanged |
1061
+
1062
+ `ok` and `warn` are assistant-ui's own semantic pair; `busy` is a console cyan
1063
+ for in-flight and `info` a calmer blue for news. `danger` does not move — it was
1064
+ the one tone already chromatic, and it is the one nobody should have to
1065
+ re-learn.
1066
+
1067
+ The light row is darker than these hues usually run, and that is the point: a
1068
+ tone is not only a dot here. `Chip`, `StatusDot`, `Toast` and the rest of the
1069
+ tone family draw `text-<tone>` at 11px uppercase — text, judged at 4.5:1, not at
1070
+ the 3:1 a signal is held to. Every light value above clears 4.5:1 on `--cue-bg`
1071
+ (4.79 to 4.85, beside `danger`'s 4.94) and every one of them is inside sRGB, so
1072
+ the measured ratio is the painted one. Dark was never in question: it runs 8.5
1073
+ to 10.3:1.
1074
+
1075
+ The chrome is untouched. Ink, ground, surfaces, borders, `accent`, `focus` and
1076
+ `selection` are the same monochrome they have always been, on both modes. This
1077
+ change is the status vocabulary and the live hue below it, and nothing else.
1078
+
1079
+ The other nine presets do not move at all: `terminal`, `signal`, `venu`,
1080
+ `hivehub`, `dusk`, `luma`, `snuffle`, `quotamate` and `requestport` were already
1081
+ chromatic here, and an app on any of them sees nothing new in this section.
1082
+
1083
+ ### Migrating
1084
+
1085
+ **A console on the default theme that relied on monochrome statuses will now
1086
+ see colour.** No API moved and nothing needs editing to keep working, but
1087
+ pixels change, and they change in more places than a status pill: `Chip`,
1088
+ `StatusDot`, `Meter`, `Toast`, `Stat`, `StatusBar`, `LogViewer`, `Sparkline`,
1089
+ `GroupBar`, `TwoStepButton`, `Tree`, `TerminalFrame`, `AppWindowFrame`,
1090
+ `UsageChart`, `FileUpload`, `EnvVarInput`, `AskBox`, `DelegationCard` and
1091
+ `ToolCallCard` all spend these four tokens, along with the MIDI and DMX
1092
+ instruments. Anywhere a `tone` prop was chosen and then quietly rendered white,
1093
+ it now renders the tone. Screenshot tests on `data-theme="cue"` will need new
1094
+ baselines; a design that used `tone="info"` as a way of saying "plain" will now
1095
+ say "blue".
1096
+
1097
+ If a product deliberately wants the monochrome back, it is four declarations —
1098
+ the soft fill and the rim derive from the base with `color-mix`, so overriding
1099
+ the base moves the whole family with it:
1100
+
1101
+ ```css
1102
+ @import "tailwindcss";
1103
+ @import "@cueplusplus/ui/styles.css";
1104
+
1105
+ [data-theme="cue"] {
1106
+ --cue-ok: #ffffff;
1107
+ --cue-busy: #ffffff;
1108
+ --cue-warn: #ffffff;
1109
+ --cue-info: #ffffff;
1110
+ }
1111
+ [data-theme="cue"][data-mode="light"] {
1112
+ --cue-ok: #0a0a0a;
1113
+ --cue-busy: #0a0a0a;
1114
+ --cue-warn: #0a0a0a;
1115
+ --cue-info: #0a0a0a;
1116
+ }
1117
+ ```
1118
+
1119
+ Two selectors, not one, and the block goes _after_ the import — the preset's own
1120
+ rules are `[data-theme="cue"]` and `[data-theme="cue"][data-mode="light"]`, so
1121
+ an override has to match their specificity and win on source order. An app that
1122
+ runs `mode="system"` mirrors these onto
1123
+ `[data-theme="cue"][data-mode="system"]` inside the two `prefers-color-scheme`
1124
+ queries, exactly as the generated sheet does.
1125
+
1126
+ ## `--cue-stream`, the live hue
1127
+
1128
+ The colour contract gains one token and its two derivations —
1129
+ `--cue-stream`, `--cue-stream-soft` and `--cue-stream-border` — on all ten
1130
+ presets, in both modes, at the same 12% and 35% mixes the statuses use. The
1131
+ colour contract is 42 custom properties where it was 39.
1132
+
1133
+ It is the colour of something still arriving: a token mid-stream, a tool still
1134
+ running, a cursor that has not stopped. It is deliberately **not** a sixth
1135
+ status. A status is something a `Chip` or a `StatusDot` can be _told_ to be, and
1136
+ "streaming" is not a tone a caller picks — it is a property of a surface while
1137
+ the model is talking. There is no `tone="stream"` and there is not going to be
1138
+ one. What it shares with the statuses is the shape of its family, which is why
1139
+ it is derived beside them and bridged beside them:
1140
+
1141
+ ```
1142
+ --color-stream → text-stream bg-stream/15 border-stream/30
1143
+ ```
1144
+
1145
+ The bridge carries the tone at full strength and lets Tailwind's alpha modifier
1146
+ do the washes, the way `Chip` already writes `bg-busy/10`.
1147
+
1148
+ The flagship pair is assistant-ui's `--aui-live` — `blue-400` on ink verbatim,
1149
+ and on paper `blue-500`'s hue taken down to `oklch(0.55 0.21 259.815)`, because
1150
+ the elements draw the live hue as text and `blue-500` itself lands at 3.6:1
1151
+ there. Every other preset answers in its own blue, except `terminal`, which
1152
+ already owned a live hue and keeps it.
1153
+
1154
+ Three things that hold a second copy of the contract moved with it, so nothing
1155
+ needs to be told about the new token twice: `createTheme()` resolves and derives
1156
+ `stream` like any other tone and darkens an invented light one to the non-text
1157
+ AA floor; the contrast report checks `stream/bg` against that same floor — every
1158
+ preset clears it with room to spare, the tightest being `terminal`'s light block
1159
+ at 4.89:1; and the configurator edits and exports it beside the statuses rather
1160
+ than in the miscellany. A theme somebody generated before this release and pasted into their
1161
+ own CSS keeps working: `stream` falls back through the same resolution order
1162
+ every other invented token does.
1163
+
1164
+ ## `@cueplusplus/ui/elements`
1165
+
1166
+ A new group, and the largest one this library has added at once: the agent's own
1167
+ surface. Fifteen elements, plus the island and the two label primitives the
1168
+ family is built out of, at `/docs/components/elements`.
1169
+
1170
+ | element | what it is |
1171
+ | ----------------- | ----------------------------------------------------------- |
1172
+ | `ToolCall` | one invocation, request and result behind a disclosure |
1173
+ | `ToolGroup` | calls that went out together, collapsed to one row |
1174
+ | `ToolError` | one call failed, with the error legible |
1175
+ | `TerminalBlock` | output streaming line by line, ending in an exit status |
1176
+ | `CodeDiff` | a unified diff, tinted, sized for a chat column |
1177
+ | `ReviewableDiff` | the same diff where each hunk is a decision |
1178
+ | `FileTree` | everything a run touched, with the churn per file |
1179
+ | `ApprovalCard` | the agent asking before it does something with side effects |
1180
+ | `PermissionGrant` | granting a capability rather than approving an action |
1181
+ | `AgentPlan` | a checklist the agent works through |
1182
+ | `TodoList` | the agent's working list, rewritten mid-run |
1183
+ | `AgentStatus` | one pill: what it is doing, and for how long |
1184
+ | `SubagentList` | parallel workers, their models and their progress |
1185
+ | `TraceWaterfall` | every span in a run on one nested time axis |
1186
+ | `ChatPanel` | the whole family working together, with its five parts |
1187
+
1188
+ Beside them: `ShimmerLabel` and `SwapLabel`, the eighteen surface recipes the
1189
+ family shares as plain class strings (`paper`, `floating`, `field`, `mono`,
1190
+ `live`, `codeSurface`, `collapsePanel`, `pressable`, the swap pairs and the
1191
+ rest), and `range`'s six functions — `at`, `clamp`, `indexIn`, `pct`,
1192
+ `progressOf`, `take` — which are the arithmetic a replayed transcript is made
1193
+ of.
1194
+
1195
+ Two conventions run through the group and both are worth knowing before you read
1196
+ the source:
1197
+
1198
+ **State is a prop, and time is the caller's.** Nothing here holds a run.
1199
+ `ToolCall` is _told_ `running` and `open`; `TerminalBlock` is told how many of
1200
+ its lines are visible. Replaying a conversation is arithmetic on props, which is
1201
+ what makes every one of these benchable at every state.
1202
+
1203
+ **These components are written upstream's way, not this library's.** Plain
1204
+ exported functions with no `forwardRef`, a `data-slot` attribute on every root,
1205
+ `className` merged last through `cn`. That is deliberate rather than sloppy: it
1206
+ keeps a re-synced diff readable, and `data-slot` is what lets an app restyle a
1207
+ part from the outside without a prop for it. `AgentSurface` — cue's own — is the
1208
+ one component in the group written in the house style.
1209
+
1210
+ `lucide-react` is an **optional peer** of this entry, which is why the group is
1211
+ its own subpath instead of part of the root barrel: the family draws its own
1212
+ icons rather than taking them from a prop. Import nothing from `/elements` and
1213
+ it costs nothing — no install, no bundle, no resolution error.
1214
+
1215
+ `@cueplusplus/ui/elements.css` ships beside it and is **optional even for this
1216
+ subpath**, because the family is painted by the token layer already:
1217
+
1218
+ ```css
1219
+ @import "tailwindcss";
1220
+ @import "@cueplusplus/ui/styles.css";
1221
+ @import "@cueplusplus/ui/elements.css"; /* motion, and the supersede island */
1222
+ ```
1223
+
1224
+ It carries the two vocabularies upstream never shipped as anything but
1225
+ dependencies — the shimmer sweep and the nine entrance utilities, both stilled
1226
+ under `prefers-reduced-motion` — and the island values below. Skip it and the
1227
+ elements still render in cue's palette; you lose the motion and `skin="aui"`
1228
+ becomes a no-op.
1229
+
1230
+ **Every class it declares is namespaced `cue-elements-`**, and that is a
1231
+ compatibility promise rather than a house style. The entrance utilities are
1232
+ `tw-animate-css`'s by name — `animate-in`, `fade-in`, `slide-in-from-*` — and
1233
+ `tw-animate-css` is what shadcn installs by default, so a stylesheet declaring
1234
+ them again would give such an app two `.animate-in` rules with import order
1235
+ picking the winner, silently. The vendoring codemod renames them at emit, the
1236
+ lockfile records the table as `classRenames`, and a test fails on any class in
1237
+ this stylesheet that a third-party utility package could also own.
1238
+
1239
+ ## The island: one DOM, two value-sets
1240
+
1241
+ ```tsx
1242
+ import { AgentSurface, ToolCall } from "@cueplusplus/ui/elements";
1243
+
1244
+ <AgentSurface skin="aui" fidelity="upstream">
1245
+ <ToolCall
1246
+ label="Searched"
1247
+ activeLabel="Searching"
1248
+ query="composer"
1249
+ request={request}
1250
+ result={result}
1251
+ running
1252
+ open={open}
1253
+ onOpenChange={setOpen}
1254
+ />
1255
+ </AgentSurface>;
1256
+ ```
1257
+
1258
+ `AgentSurface` renders a plain `div` and stamps `data-cue-skin` and
1259
+ `data-cue-fidelity` on it. `elements.css` re-declares, under those attributes,
1260
+ the `--cue-*` tokens the family reads, and custom properties inherit — so the
1261
+ whole subtree repaints with no prop threading and no branch inside any
1262
+ component.
1263
+
1264
+ The two axes are independent, which is the switchable fidelity the spec asked
1265
+ for:
1266
+
1267
+ - **`skin`** — `cue` (the default) or `aui`, which installs assistant-ui's four
1268
+ shadcn variables and four literal hues as cue tokens, in light and dark.
1269
+ - **`fidelity`** — `cue-metrics` (the default) keeps cue's type scale and
1270
+ declares exactly one metric of its own:
1271
+ `--spacing: calc(0.25rem * var(--cue-density, 1))`. That is the lever that
1272
+ carries the density ladder into five hundred-odd vendored geometry utilities,
1273
+ none of which names a `--cue-space-*` token. `upstream` pins assistant-ui's
1274
+ radii, their 13.5/12/11px type and a flat `--spacing: 0.25rem` instead, so
1275
+ **density stops at the island edge** and the subtree keeps upstream's
1276
+ proportions whatever the console around it is set to.
1277
+
1278
+ It is not a theme. `data-theme`, `data-mode` and `data-density` are untouched,
1279
+ and a light app gets a light island.
1280
+
1281
+ Overlays follow. The portal stamp gains both axes, so a popover or dialog opened
1282
+ from inside an island — which mounts on `<body>`, far from the attributes —
1283
+ paints like the transcript it came from rather than like the page. Outside an
1284
+ island the two attributes are _absent_ rather than defaulted, so nothing that
1285
+ does not use this pays for it.
1286
+
1287
+ ## Where this code came from
1288
+
1289
+ Elements are not a package. `@assistant-ui/ui` is private at `0.0.0` and never
1290
+ published; the only channel is a shadcn registry whose index carries no version,
1291
+ no date and no hash, rebuilt from upstream `main` on every deploy. Copy-in is
1292
+ the intended use — so this release also ships the only thing that can answer
1293
+ "which Elements do we have".
1294
+
1295
+ `scripts/elements-sync/` is pinned to upstream commit
1296
+ `31a049fcfa846a76da7c8e2c0bcd62960825dbd4` (resolved 2026-08-22). It fetches
1297
+ all 96 `elements-*` registry items, verifies every allowlisted file against
1298
+ `raw.githubusercontent.com` at that pin, records every byte it read in
1299
+ `elements-sync.lock.json`, and runs a committed codemod that rewrites the fetched
1300
+ TSX before it lands: the ink ramp onto cue's four stops, the four literal
1301
+ Tailwind hues onto `stream`/`ok`/`danger`/`warn` with their alphas intact,
1302
+ literal pixel sizes onto the type scale, radii onto the three rungs, and every
1303
+ `dark:` variant resolved away because cue's mode axis is `[data-mode]` rather
1304
+ than `prefers-color-scheme`. Geometry is deliberately left alone: it all compiles
1305
+ through `--spacing`, which is the island's lever. Upstream's own 679 documented
1306
+ prop rows are merged into JSDoc on the way out, so the props table on each page
1307
+ is upstream's description of upstream's prop.
1308
+
1309
+ What that buys a consumer: the elements arrive as source this library owns and
1310
+ can fix, not as a dependency that can move underneath you; the "version" you have
1311
+ is a SHA you can read in `NOTICE.md` and diff against; and the licence obligation
1312
+ travels with the package. Upstream's MIT notice — Copyright (c) 2025 AgentbaseAI
1313
+ Inc. — is preserved verbatim in `packages/ui/src/elements/NOTICE.md` and named in
1314
+ the group's own documentation.
1315
+
1316
+ Two consequences worth stating plainly. **The directory is generated.** Every
1317
+ file in `src/elements/` except `surfaces.tsx` and `agent-surface.tsx` is output;
1318
+ a bug there is fixed by changing a codemod rule and re-running the sync, never by
1319
+ editing the file, because the next re-sync deletes a hand edit without telling
1320
+ anyone. And **six upstream export names are renamed at emit**, because docgen
1321
+ slugs are globally unique and the `Chat` family stays authoritative:
1322
+ `Composer → ElementsComposer`, `CommandPalette → ElementsCommandPalette`,
1323
+ `DataTable → ElementsDataTable`, `EmptyState → ChatEmptyState`,
1324
+ `Timeline → ElementsTimeline`, and `Source → CitationSource` (that one collides
1325
+ inside upstream's own family). None of the six is in this release — they land
1326
+ with the rest of the catalogue — but the table is generated by the sync, ships in
1327
+ the group barrel, and is the answer to "why is the import name not the one on
1328
+ assistant-ui's site".
1329
+
1330
+ ## What did not change
1331
+
1332
+ Outside the `cue` preset's four status values and its light `stream`, no colour
1333
+ moved. No component's
1334
+ props, structure or rendering changed, with two additions that are worth naming
1335
+ because they are additions rather than nothing: the portal stamp — both
1336
+ `useCuePortalProps` and `CuePortalFrame` — now carries `data-cue-skin` and
1337
+ `data-cue-fidelity` alongside the four axes it already carried, and both are
1338
+ _absent_ outside an island rather than defaulted; and the configurator's
1339
+ `TokenEditor` lists `stream` in its Status group, so a generated theme edits it
1340
+ where you would look for it.
1341
+
1342
+ The root barrel is untouched, so an app that never imports `/elements` gets no
1343
+ new peer, no new stylesheet and no new bytes, and `tw-shimmer` and
1344
+ `tw-animate-css` were reimplemented as CSS in this package rather than taken as
1345
+ dependencies. The `Chat` group is unchanged and stays authoritative on every
1346
+ name the two families both wanted.
1347
+
1348
+ Text is still text. Nothing in this group parses model output as markdown or
1349
+ HTML; the elements render what they are given, for the same reason `Message`
1350
+ does. Sanitized markdown is a documented, opt-in surface arriving later, with its
1351
+ own peer and its own entry.
1352
+
1353
+ `schemaVersion` is still `1`.
1354
+
1355
+ - 4e510e7: ## The red carries its own ink
1356
+
1357
+ `RevertDock`'s confirm is the one solid destructive fill in the elements family
1358
+ and the one control in it that does something irreversible. It painted
1359
+ `bg-danger/90 text-accent-fg`, and both halves of that were wrong.
1360
+
1361
+ `--cue-accent-fg` is the ink authored against `--cue-accent`. It has no
1362
+ relationship to `--cue-danger`, and on the ten reds this system ships it lands
1363
+ anywhere between **3.58:1** (venu, dark) and **6.67:1** (requestport, light).
1364
+ The `/90` then let the paper through and took roughly another 0.65 off the
1365
+ ratio. In cue's own light theme the label on that button measured **4.29:1** at
1366
+ the 12px this row is set in — under AA, on the press that cannot be undone.
1367
+ Inside an `<AgentSurface skin="aui">` in light mode it was **3.82:1**.
1368
+
1369
+ There is no existing token that fixes it. Every candidate the palette already
1370
+ owns fails somewhere: `accent-fg` fails terminal and venu in dark, `bg` fails
1371
+ terminal and hivehub in dark, the theme's own `fg` fails every light block. The
1372
+ ink a red can carry is a property of _that red_, not of the mode — so the red
1373
+ now has one.
1374
+
1375
+ ### `--cue-danger-fg`
1376
+
1377
+ A new authored anchor, per preset, per mode, in the colour contract beside the
1378
+ tone it is chosen against:
1379
+
1380
+ ```css
1381
+ [data-theme="cue"] {
1382
+ --cue-danger: #ff5f57;
1383
+ --cue-danger-fg: #000000;
1384
+ }
1385
+ [data-theme="cue"][data-mode="light"] {
1386
+ --cue-danger: #d03030;
1387
+ --cue-danger-fg: #fcfcfc;
1388
+ }
1389
+ ```
1390
+
1391
+ with `text-danger-fg` / `bg-danger-fg` bridged into Tailwind like every other
1392
+ colour token. `ok`, `busy`, `info` and `stream` have no such ink, and that is
1393
+ deliberate: they are read as signals — a dot, a rim, a 12% wash — and the text
1394
+ near them sits on a surface. The red is a tone this system fills a control with,
1395
+ which is what makes text on it text. (The amber turns out to be the other one —
1396
+ _The amber carries its own ink too_, later in this same release.)
1397
+
1398
+ Each preset's value is the extreme it already owns, measured: every light block
1399
+ takes its paper, seven dark blocks take their deepest ground, and the two whose
1400
+ red sits in the middle of the range (terminal, hivehub) take white, because
1401
+ nothing else reaches AA on it. The worst pair in the system is now **4.59:1**;
1402
+ it was **3.58:1**.
1403
+
1404
+ `createTheme()` derives the same token for a generated theme the way it derives
1405
+ `accent-fg` — best of the theme's ground, its deepest well and its ink, with
1406
+ pure black or white only when none of the three can be read — and
1407
+ `CONTRAST_REQUIREMENTS` gained a tenth pair, `danger-fg/danger` at 4.5:1, so a
1408
+ generated palette is judged on it too. The configurator shows it next to the red
1409
+ in the Status group.
1410
+
1411
+ A sweep in `@cueplusplus/ui`'s own suite now measures the shipped pairing across
1412
+ all ten presets in both modes and fails under 4.5:1, and a second one measures
1413
+ it inside the assistant-ui island, where the two halves legitimately disagree
1414
+ about which ink their red wants.
1415
+
1416
+ ### If you ship your own theme block
1417
+
1418
+ A hand-written `[data-theme="acme"]` block should declare `--cue-danger-fg`
1419
+ alongside `--cue-danger`. Without it, `text-danger-fg` resolves to nothing on
1420
+ your theme. Themes generated by `createTheme()` and the configurator's exports
1421
+ already carry it.
1422
+
1423
+ - b9ab713: ## The rest of the family
1424
+
1425
+ P0 shipped fifteen elements and the machinery that makes more of them. This
1426
+ release finishes the catalogue. **All 96 of assistant-ui's `elements-*` registry
1427
+ items now emit** — 94 elements, the eighteen shared surface recipes, and
1428
+ `range`'s six functions — through the same pinned sync, the same codemod and the
1429
+ same island. `@cueplusplus/ui/elements` goes from 23 exported components to
1430
+ **132**, plus two hooks, and is now the largest group in the library.
1431
+
1432
+ None of it was typed. The 79 new elements arrived the way the first fifteen did:
1433
+ `scripts/elements-sync/` fetches the registry at upstream
1434
+ `31a049fcfa846a76da7c8e2c0bcd62960825dbd4`, verifies every file against
1435
+ `raw.githubusercontent.com` at that pin, hashes what it read into
1436
+ `elements-sync.lock.json`, and runs a committed codemod that rewrites the fetched
1437
+ TSX before it lands. Adding an element is a line in an allowlist plus whatever
1438
+ rule its source needed; the emitted files are never edited by hand, because the
1439
+ next re-sync would delete the edit without telling anyone.
1440
+
1441
+ What that means for a consumer is the sentence the whole phase exists for: **the
1442
+ entire assistant-ui Elements catalogue is now cue-owned source.** Not a
1443
+ dependency that can move underneath you — there is no package to depend on;
1444
+ `@assistant-ui/ui` is private at `0.0.0` and the registry carries no version
1445
+ field. Every one of the 94 is painted by `--cue-*` tokens and answers to
1446
+ `data-theme`, `data-mode` and `data-density` like everything else here, and every
1447
+ one of them re-skins to assistant-ui's own values inside
1448
+ `<AgentSurface skin="aui">` without a prop being threaded anywhere.
1449
+
1450
+ | what landed | elements | what it covers |
1451
+ | ----------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1452
+ | thread & reasoning | 20 | reasoning revealed a beat at a time, text arriving a token at a time, and everything that hangs off one turn — actions, attachments, branches, timing, the queue, editing, quoting, regenerating, reading aloud |
1453
+ | knowledge & structured output | 21 | where an answer came from and how far to trust it — sources, inline citations, retrieved passages, the document it quotes, a confidence marker — and what it arrives as when prose is not enough: tables, charts, diagrams, spec sheets, maps, maths, canvases |
1454
+ | agents & observability | 18 | who is doing the work and what it costs — agent cards, handoffs, background runs, checkpoints, job progress, spend, context, quota — plus a session's failure modes and the four drawings it is legible in afterwards |
1455
+ | composer, thread list & shell | 20 | the application around the conversation — the composer and its twenty-one facets, the palette, the model picker, the prompt library, the thread list and its searches, settings, onboarding, the blank screen, the launcher bubble, the call |
1456
+
1457
+ Six upstream names are renamed at emit, because docgen slugs are globally unique
1458
+ and the `Chat` family stays authoritative: `Composer → ElementsComposer`,
1459
+ `CommandPalette → ElementsCommandPalette`, `DataTable → ElementsDataTable`,
1460
+ `EmptyState → ChatEmptyState`, `Timeline → ElementsTimeline`, and
1461
+ `Source → CitationSource`, which collides inside upstream's own family. The table
1462
+ is generated by the sync, ships in the group barrel's JSDoc, and is the answer to
1463
+ "why is the import name not the one on assistant-ui's site".
1464
+
1465
+ `composer` is the family's one compound: twenty-one exports out of a single file,
1466
+ all of them published, because upstream's own examples compose the facets rather
1467
+ than configuring a root — and a composer exported without `useSlashMatches` and
1468
+ `useMentionMatches` is a composer whose two menus nobody can open.
1469
+
1470
+ ## Four new kitchen-sink sections
1471
+
1472
+ The corpus is split the way the batches were, not lumped into one page:
1473
+ `elements-thread`, `elements-knowledge`, `elements-agents` and
1474
+ `elements-composer` join `elements` on `/kitchen-sink`, each with its own
1475
+ heading and its own axe case. One section holding a hundred and thirty benches
1476
+ is one unreadable file and one slow accessibility run; five sections are five of
1477
+ each. Every element is benched at its real states — a streaming element shows
1478
+ streaming _and_ settled, an error element shows the error — which is what makes
1479
+ the props table on each component page evidence rather than a claim.
1480
+
1481
+ ## `heat-graph`, a second optional peer
1482
+
1483
+ `ActivityGraph` draws a calendar of runs, and it draws it with `heat-graph` —
1484
+ the headless grid assistant-ui publishes beside the elements (MIT, same author).
1485
+ It ships as an **optional peer** at `^0.0.15`, the shape `recharts` and
1486
+ `@xyflow/react` already have: declared in `peerDependencies`, marked optional,
1487
+ externalised by the bundler, named in this entry's expected-peer list, and
1488
+ installed here only as a devDependency. One component of 132 reaches it, so an
1489
+ app that never renders a heat calendar installs nothing for it and sees no
1490
+ resolution error.
1491
+
1492
+ **This is a deliberate departure from the spec.** §B of
1493
+ `docs/specs/2026-08-22-agent-elements.md` says `heat-graph` is "vendored as a
1494
+ helper, not a peer". That line was written before the pipeline existed. Vendoring
1495
+ it now would mean hand-writing eight React components into a directory whose
1496
+ entire contract is that every file in it is generated — a hand-written tree
1497
+ inside a generated one, invisible to the lockfile, unreachable by the codemod,
1498
+ and the first thing a re-sync would trip over. The plan's decision 3, which asks
1499
+ for a peer with its four coordinated edits, is what was followed instead. The
1500
+ reason is recorded in `rules.mjs` beside the batch it belongs to, so the next
1501
+ person to read the spec finds the disagreement where the code is.
1502
+
1503
+ ## Two elements carry cue runtime, and that is the point
1504
+
1505
+ Two of the 96 needed more than a class rewrite, and both were held back through
1506
+ their own batch for a decision rather than improvised inside one. The decision:
1507
+ **the codemod may write cue's own code into a vendored file.**
1508
+
1509
+ - **`InlineCitation`** is the family's one overlay — a Base UI preview card
1510
+ behind each numbered marker — and its `Portal` now spreads
1511
+ `useCuePortalProps()`. A portaled subtree mounts on `<body>`, where the token
1512
+ layer resolves the document's values instead of the island's, so without the
1513
+ stamp a hover card opened inside an `<AgentSurface skin="aui">` would paint in
1514
+ your console's palette while the sentence behind it painted in assistant-ui's.
1515
+ That is precisely the bug P0 gave `PortalStamp` its `skin` / `fidelity`
1516
+ carriage to prevent. `@base-ui/react` was already a required peer, so this
1517
+ costs nothing new to install.
1518
+ - **`ScrollAnchor`** is the one element in all 96 that drives its own clock — a
1519
+ `setInterval` appending a turn every 1.3s — and the one whose viewport scrolls
1520
+ itself. Its effect now returns early under `usePrefersReducedMotion()`, after
1521
+ painting the still frame: the _finished_ transcript, pinned to the bottom,
1522
+ rather than a scene frozen three messages in. Its two imperative smooth
1523
+ scrolls lost their `behavior: "smooth"` option, which no browser will still for
1524
+ `prefers-reduced-motion`; the element already carries `scroll-smooth` on the
1525
+ same viewport, so the scroll stays smooth from CSS and one
1526
+ `motion-reduce:scroll-auto` twin now turns all three of them off.
1527
+
1528
+ Read as a leak, four lines of cue runtime inside vendored source look like the
1529
+ vendoring failing. They are the vendoring working. These files are cue-owned
1530
+ source — that is the whole premise, since Elements ship as copy-in behind a
1531
+ registry with no version — so `packages/ui/src/elements/` is this repository's
1532
+ code, generated rather than typed. The codemod already rewrites every class in it
1533
+ onto cue's tokens; making a vendored element obey cue's _contracts_ is the same
1534
+ act performed one layer up. An element that portals without the island stamp, or
1535
+ re-arms a callback without asking whether the reader wants motion, is broken in
1536
+ this system's terms whatever it does upstream.
1537
+
1538
+ And it is a rule, never a hand edit. `RUNTIME_INJECTIONS` in `rules.mjs` names
1539
+ the file, the hook, the module, the binding, the component and the exact lines
1540
+ that change, each with its reason; the codemod throws on every anchor it cannot
1541
+ find, `sync.test.mjs` fails on an injection that stopped reaching the emitted
1542
+ tree, and both files carry the reasoning in a comment where a reader meets the
1543
+ import.
1544
+
1545
+ ## Six text inputs get a focus ring back
1546
+
1547
+ `ElementsComposer`'s input, `ElementsCommandPalette`'s, `PromptLibrary`'s,
1548
+ `ThreadSearch`'s, `ConversationSearch`'s and `MobileComposer`'s are bare
1549
+ `<input>`s whose entire upstream class string is a placeholder colour, a size,
1550
+ `bg-transparent` and `outline-none`. They removed the browser's focus outline and
1551
+ put nothing in its place — a keyboard user tabbing into a command palette had no
1552
+ idea they had arrived. It is not a ring the codemod dropped; upstream never wrote
1553
+ one.
1554
+
1555
+ The rewrite restores cue's ring only where something in the class string says the
1556
+ element is focusable, and its signal was interaction-state variants and
1557
+ `group`/`peer` labels — none of which these six carry. `::placeholder` and
1558
+ `caret-color` apply to `input` and `textarea` and to nothing else in CSS, so
1559
+ either one is proof the element takes a caret, and an element that takes a caret
1560
+ is focusable. Both now count, and the six come back with the same
1561
+ `focus-visible:outline-2 / solid / offset-1 / accent` recipe every other control
1562
+ in this package uses.
1563
+
1564
+ **This is a WCAG 2.4.7 (Focus Visible, AA) fix you inherit by upgrading.** No
1565
+ prop moved and nothing needs editing; if you render any of those six, a visible
1566
+ focus indicator appears where there was none. The fix is in `rules.mjs`, so a
1567
+ re-sync reproduces it byte for byte rather than losing it.
1568
+
1569
+ ## `llms.txt` is an index again
1570
+
1571
+ The machine surfaces on `ui.cueplusplus.com` re-tier, because one of them was a
1572
+ release away from failing its own cap: `/llms.txt` was 386 lines against a
1573
+ ceiling of 400, and 308 of those lines were a component's name, its URL and a
1574
+ one-liner. What grows that file is the catalogue, and the catalogue is what the
1575
+ library is for, so raising the cap would have bought exactly one release.
1576
+
1577
+ `/llms.txt` now lists **groups** — nineteen lines, each with the group's page,
1578
+ its component and hook counts, the specifier it imports from and what it is for —
1579
+ and points at the sheet for everything inside them. 61 non-blank lines, from 386,
1580
+ with the same facts still coming off the manifest.
1581
+
1582
+ Nothing lost information. `/llms-components.txt` was already exhaustive and stays
1583
+ exhaustive: one line per component with its full props signature and its import
1584
+ specifier, one per hook with its call signature, and a header stating the rule for
1585
+ reaching any component's page
1586
+ (`https://ui.cueplusplus.com/docs/components/<slug>.md`). The index stopped
1587
+ duplicating the sheet, which is the only thing it was doing with 80% of its lines.
1588
+
1589
+ If you point an agent at these files, point it at both, in this order: `/llms.txt`
1590
+ first — what the library is, the rules a props table cannot show, and which group
1591
+ a thing lives in — then `/llms-components.txt` before it writes a call, for the
1592
+ specifier and the full props literal. `/llms-full.txt` and `/kitchen-sink.md` are
1593
+ unchanged.
1594
+
1595
+ ## What did not change
1596
+
1597
+ No token moved. `@cueplusplus/tokens` is untouched by this release — the colour
1598
+ contract is the same 42 custom properties the last one shipped — and no existing
1599
+ component's props, structure or rendering changed. The root barrel is still
1600
+ untouched, so an app that never imports `/elements` gets no new peer, no new
1601
+ stylesheet and no new bytes; `heat-graph` and `lucide-react` are both optional
1602
+ and both reached only from this subpath.
1603
+
1604
+ `@cueplusplus/ui/elements.css` gains exactly one declaration — a
1605
+ `cue-elements-zoom-in-95` entrance class, namespaced like the other nine so that
1606
+ an app which also installs `tw-animate-css` never has two stylesheets quietly
1607
+ arguing over `.animate-in` by import order. It remains optional even for this
1608
+ subpath: skip it and the elements still render in cue's palette, you lose the
1609
+ motion and `skin="aui"` becomes a no-op.
1610
+
1611
+ Text is still text. Nothing in this group parses model output as markdown or
1612
+ HTML — 132 components and not one of them decides your markup for you. Sanitized
1613
+ markdown remains a documented, opt-in surface arriving later, with its own peer
1614
+ and its own entry.
1615
+
1616
+ `schemaVersion` is still `1`.
1617
+
1618
+ - 854ad96: ## A streaming layer under the Chat group
1619
+
1620
+ The Chat group renders a turn that has finished arriving. It has never had a
1621
+ story for one that is _still_ arriving: no token assembly, no cancelling a run
1622
+ mid-sentence, no regenerating a turn beside the one it replaces, and nowhere to
1623
+ put the model's own tool call while the rest of the answer is still coming.
1624
+
1625
+ `@cueplusplus/ui/agent-runtime` is that story, and it is one hook wide.
1626
+ `useAgentThread(runtime)` takes any `@assistant-ui/react` runtime — the one you
1627
+ build with `useExternalStoreRuntime(adapter)`, or the AI-SDK or LangGraph ones —
1628
+ and gives back the props `Message`, `MessageList` and `Composer` already take.
1629
+ Every field is named for the prop it feeds: `busy`, `disabled`, `onSend` and
1630
+ `onStop` to `Composer`; `role`, `agent`, `pending` and `text` to `Message`;
1631
+ `toolCalls[]` onto `ToolCallCard`; `branch` and `switchBranch` to
1632
+ `BranchPicker`.
1633
+
1634
+ Two components ship in the entry with it. **`ToolCallCard`** is the model's own
1635
+ call: the tool name in mono, an optional risk chip in `AskBox`'s exact
1636
+ vocabulary, the status spelled in words beside a spinner rather than in colour
1637
+ alone, and the arguments and result behind a disclosure that mounts and unmounts
1638
+ instead of animating a height. It is neither of its neighbours — `AskBox` is a
1639
+ question put to a human, `DelegationCard` is a sub-agent's work, and a tool call
1640
+ is the model's own action. **`BranchPicker`** is a `‹ 2 / 3 ›` stepper for an
1641
+ agent turn's footer: two ghost icon buttons around a mono readout that reads
1642
+ itself aloud as "branch 2 of 3", clamped so it can never point past either end,
1643
+ and drawing nothing at all while there is only one branch.
1644
+
1645
+ `@assistant-ui/react@^0.15.16` is an **optional** peer, declared and
1646
+ externalised the way `@xyflow/react` is under `/flow`. Import nothing from
1647
+ `/agent-runtime` and it costs you nothing: no install, no bundle, no resolution
1648
+ error — the root barrel's import graph never reaches it, and neither does
1649
+ `/chat`'s. Import the entry and you do need it. Nothing in the shipped modules
1650
+ imports `@assistant-ui/react` at runtime — the hook's own file names only its
1651
+ types — but you cannot build the `AssistantRuntime` the hook takes without it,
1652
+ so install it. Installing it also installs its own dependencies — `radix-ui`
1653
+ and several `@radix-ui/*` parts, `zustand`, `zod`, `assistant-stream`,
1654
+ `@assistant-ui/core` — about 16 MB on disk; the subpath boundary keeps all of
1655
+ it out of an app that never imports the entry.
1656
+
1657
+ ```tsx
1658
+ import { Composer, Message, MessageList } from "@cueplusplus/ui";
1659
+ import {
1660
+ BranchPicker,
1661
+ ToolCallCard,
1662
+ useAgentThread,
1663
+ } from "@cueplusplus/ui/agent-runtime";
1664
+ import {
1665
+ useExternalStoreRuntime,
1666
+ type ThreadMessageLike,
1667
+ } from "@assistant-ui/react";
1668
+
1669
+ // Outside the component: the runtime caches the conversion against this
1670
+ // function, and a new one every render is an update loop.
1671
+ const convertMessage = (m: ThreadMessageLike) => m;
1672
+
1673
+ const runtime = useExternalStoreRuntime({
1674
+ messages,
1675
+ isRunning,
1676
+ convertMessage,
1677
+ onNew,
1678
+ onCancel,
1679
+ onReload,
1680
+ setMessages,
1681
+ });
1682
+ const thread = useAgentThread(runtime, { agent: "planner" });
1683
+
1684
+ <MessageList aria-label="Thread">
1685
+ {thread.turns.map((t) => (
1686
+ <Message
1687
+ key={t.id}
1688
+ role={t.role}
1689
+ agent={t.agent}
1690
+ pending={t.pending}
1691
+ footer={
1692
+ t.branch.count > 1 && thread.switchBranch ? (
1693
+ <BranchPicker
1694
+ index={t.branch.index}
1695
+ count={t.branch.count}
1696
+ onNavigate={(d) => thread.switchBranch?.(t.id, d)}
1697
+ disabled={thread.busy}
1698
+ />
1699
+ ) : undefined
1700
+ }
1701
+ >
1702
+ {t.text}
1703
+ {t.toolCalls.map((c) => (
1704
+ <ToolCallCard
1705
+ key={c.id}
1706
+ tool={c.tool}
1707
+ args={c.args}
1708
+ result={c.result}
1709
+ status={c.status}
1710
+ />
1711
+ ))}
1712
+ </Message>
1713
+ ))}
1714
+ </MessageList>;
1715
+ <Composer
1716
+ onSend={thread.onSend}
1717
+ busy={thread.busy}
1718
+ disabled={thread.disabled || thread.busy}
1719
+ onStop={thread.onStop}
1720
+ />;
1721
+ ```
1722
+
1723
+ ## What did not change
1724
+
1725
+ The six Chat components — `AgentPile`, `AskBox`, `Composer`, `DelegationCard`,
1726
+ `Message`, `MessageList` — render what they rendered before, with the one
1727
+ exception below. Nothing is restyled and no prop moved.
1728
+
1729
+ Text is still text. `ToolCallCard` renders arguments and results verbatim, never
1730
+ as markdown and never as HTML, for the same reason `Message` does: an argument
1731
+ string is whatever a model decided to emit, and the moment a component parses it
1732
+ the model chooses your markup. The hook joins text parts into strings rather
1733
+ than handing you a node tree to trust. No markdown package is added, opt-in or
1734
+ otherwise.
1735
+
1736
+ Nothing of assistant-ui's rendering is adopted. The hook is a
1737
+ `useSyncExternalStore` over the runtime's thread and nothing else — no
1738
+ `AssistantRuntimeProvider` anywhere in your tree, no primitive mounted, no Radix
1739
+ DOM, no borrowed context. Its `asChild` composition model is not adapted and not
1740
+ exposed: this library's public API is still 100% `render`-prop. assistant-ui
1741
+ owns the state machine, this library owns 100% of the render.
1742
+
1743
+ ## The one fix inside Chat
1744
+
1745
+ **`MessageList` now follows a turn that is still growing.** Its re-pin ran on the
1746
+ number of children changing, so a _new_ turn kept the view at the bottom but
1747
+ tokens streaming into a bubble that was already there did not — the list quietly
1748
+ stopped following mid-sentence, exactly when following matters most. It now also
1749
+ watches the items container and re-pins **when that container grows**, while
1750
+ following and pinned.
1751
+
1752
+ Only growth moves the view. A tool-call card the reader folds away, an image
1753
+ that failed to load, a footer that disappears — anything that makes the list
1754
+ shorter leaves the scroll position exactly where it was. Scrolling up still lets
1755
+ go, and a turn that grows while you are up there offers the "jump to latest"
1756
+ pill rather than pulling you down. `follow={false}` switches the whole mode off,
1757
+ growth included.
1758
+
1759
+ This is new behaviour on a component you already ship, so it is worth a moment
1760
+ if you have a `MessageList` whose children change height in place: it will now
1761
+ stay at the bottom where before it drifted up. There is no new prop and no API
1762
+ change, and an engine without `ResizeObserver` behaves exactly as it did.
1763
+
1764
+ ## Things worth knowing
1765
+
1766
+ **A branch switch is host-owned under an ExternalStore runtime.** `switchBranch`
1767
+ does not move the thread itself. It arrives at your adapter's `setMessages` with
1768
+ the newly visible list, and your host writes it back into whatever holds the
1769
+ messages. A host that ignores `setMessages` gets a picker that clicks and does
1770
+ nothing — which looks like a bug in the picker and is not one.
1771
+
1772
+ **`onReload` takes two arguments**, `(parentId, config)`. Regenerating re-runs
1773
+ the turn's _parent_, which is what puts the new answer beside the old one rather
1774
+ than after it — and is what gives `BranchPicker` a second branch to show at all.
1775
+ The picker only moves between branches that already exist, so the control that
1776
+ asks for a new one is yours to draw: a ghost `Button` in the turn's footer
1777
+ calling `regenerate(turn.id)`, as the docs demo does.
1778
+
1779
+ **Pass `convertMessage`, and declare it outside the component.** Without it your
1780
+ messages have to already be complete runtime messages, and a missing `metadata`
1781
+ throws; `convertMessage: (m) => m` lets the runtime fill the defaults in around
1782
+ the messages you already have. Give it a stable identity, though: the runtime
1783
+ caches the conversion against that function, so an arrow written inline in the
1784
+ adapter re-converts the whole thread on every render — and the next time your
1785
+ message list changes, that becomes React's "Maximum update depth exceeded"
1786
+ rather than a slow render.
1787
+
1788
+ **`onStop`, `regenerate` and `switchBranch` are `undefined`** until the adapter
1789
+ has the matching callback — `onCancel`, `onReload`, `setMessages`. That is
1790
+ deliberate rather than defensive: `Composer` keeps drawing Send while `onStop`
1791
+ is missing, so a control that could not work is never drawn.
1792
+
1793
+ **The peer range is pinned on purpose.** `^0.15.16`, not `^0.15.0`: 0.15.9
1794
+ through 0.15.16 shipped inside twelve days. What this entry reads are the stable
1795
+ members of the runtime object — `thread.getState()`, `subscribe`, `append`,
1796
+ `cancelRun`, `getMessageById`, `switchToBranch` — and never an `unstable_*` one,
1797
+ but a floor that recent is worth saying out loud rather than discovering.
1798
+
1799
+ The live thread, the four tool-call states and the branch stepper are on the
1800
+ docs site under **Agent runtime**, at `/docs/components/agent-runtime`.
1801
+
1802
+ ### Patch Changes
1803
+
1804
+ - e5fc161: ## Everywhere the red is spent
1805
+
1806
+ `--cue-danger-fg` arrived with `RevertDock`'s confirm and stopped there. The two
1807
+ destructive controls the rest of the library is actually built out of never got
1808
+ it, so the token shipped alongside the defect it was authored to close.
1809
+
1810
+ Three fills change, and all three are contrast fixes rather than restyles:
1811
+
1812
+ - **`Button` `variant="danger"`** — `bg-danger text-white` becomes
1813
+ `bg-danger text-danger-fg`.
1814
+ - **`TwoStepButton`'s armed `danger` confirm** — the same swap, on the control
1815
+ that exists because the action is irreversible.
1816
+ - **`VoiceConversation`'s end-call button** — `bg-danger/90 text-accent-fg`
1817
+ becomes `bg-danger text-danger-fg`. That element is vendored from
1818
+ assistant-ui, so the fix is a rule in `scripts/elements-sync/rules.mjs` and
1819
+ the file is re-emitted by the sync; a hand edit would have lasted until the
1820
+ next SHA bump.
1821
+
1822
+ `text-white` on the red measures **2.99:1** on cue's dark block — the default
1823
+ theme, under AA text and under the 3:1 floor a non-text control gets — and it is
1824
+ under AA on eight of the ten dark blocks this system ships. `text-danger-fg`
1825
+ measures **7.03:1** in the same place and clears 4.5:1 on all twenty
1826
+ preset x mode blocks. `text-accent-fg` is the ink authored against
1827
+ `--cue-accent`, and on the red it lands anywhere from 3.58:1 to 6.67:1; the
1828
+ `/90` then let the surface through and took the pair down with it.
1829
+
1830
+ Nothing you pass changes. A `danger` button, an armed confirm and an end-call
1831
+ button are the same elements with the same props; the label on them is legible
1832
+ now on every theme in both modes.
1833
+
1834
+ ### The reason all three stayed green
1835
+
1836
+ The sweep that arrived with the token measures **the token pair** — twenty
1837
+ blocks of `danger-fg` on `danger` — and a token pair is only the answer for a
1838
+ control that asks for it. These three asked for something else and sat outside
1839
+ the measurement.
1840
+
1841
+ `test/destructive-ink.test.ts` reads the source instead. It finds every class
1842
+ string in the package that makes the red a ground and puts an ink on it, takes
1843
+ the ink out of that string, and measures it on that theme's red across all
1844
+ twenty blocks at 4.5:1. It refuses an alpha on the fill, because
1845
+ `--cue-danger-fg` on `bg-danger/90` falls to 3.94:1 on quotamate's dark over
1846
+ `--cue-sunken` and 3.97:1 over `--cue-bg` — and that the two grounds disagree at
1847
+ all is why the alpha is refused rather than measured. It fails on an ink whose
1848
+ colour it cannot read rather than skipping it. The list of fills is pinned by
1849
+ name: a new destructive fill has to choose its ink in the commit that paints
1850
+ it.
1851
+
1852
+ `TwoStepButton`'s `warn` confirm keeps the `text-black` literal through this
1853
+ change, and its JSDoc stops claiming more than it can. At this point there was
1854
+ no `--cue-warn-fg` to give it, that pairing floored at 3.58:1 on requestport's
1855
+ light block, and the sweep said in writing that the amber was the one it did not
1856
+ cover. _The amber carries its own ink too_, in this same release, is where that
1857
+ gets closed.
1858
+
1859
+ - 58eb41a: ## The red the sheets draw
1860
+
1861
+ The generative vocabulary fills a control with the red too. `GenerativeUI`
1862
+ renders `[data-aui="button"][data-aui-style="danger"]` from a model's spec, and
1863
+ that button painted `--aui-bg` — the page under the widget — on a solid
1864
+ `--aui-danger`. It reads **4.03:1** on terminal's dark block and **4.12:1** on
1865
+ hivehub's, both under AA, and **3.82:1** on the `aui` skin's light half, which
1866
+ is the number `elements.css` already writes down beside `--cue-danger-fg` as
1867
+ the reason that token exists.
1868
+
1869
+ It takes `--aui-danger-fg` now, one more line on the seam
1870
+ (`var(--cue-danger-fg, #000000)`, a literal fallback like every other reference
1871
+ in that block), and it clears 4.5:1 on all twenty shipped preset x mode blocks
1872
+ and on both halves of the island. Nothing a spec can say changes: a `danger`
1873
+ button is the same intrinsic with the same props.
1874
+
1875
+ ### Why the sweep beside it did not see this one
1876
+
1877
+ `test/destructive-ink.test.ts` reads class strings, and this sheet has none.
1878
+ `generative.css` is plain `[data-aui]` CSS on purpose — no Tailwind, no utility
1879
+ to grep — so a fill drawn there sat outside a guard whose own name said it
1880
+ found every one.
1881
+
1882
+ It reads the package's stylesheets too now. For every rule that makes
1883
+ `--cue-danger` a whole-value ground and puts a `color` on it, it resolves both
1884
+ through the `--aui-*` seam and measures the pair on the same twenty blocks at
1885
+ the same 4.5:1, refuses an ink it cannot resolve to a theme colour rather than
1886
+ skipping it, and pins the rules it found by selector. A `color-mix()` does not
1887
+ resolve, which is the same exemption the class sweep gives `bg-danger/10`.
1888
+
1889
+ One hole in the class half closed with it: `text-[#7f1d1d]` was read as "not a
1890
+ colour" and took its whole call site out of the corpus in silence. Only lengths
1891
+ are excluded now — the six this package actually writes — so an arbitrary ink
1892
+ on the red fails by name and asks to be taught.
1893
+
1894
+ - Updated dependencies [51e5046]
1895
+ - Updated dependencies [06eb4ac]
1896
+ - Updated dependencies [4e510e7]
1897
+ - @cueplusplus/tokens@0.5.0
1898
+
1899
+ ## 0.4.0
1900
+
1901
+ ### Minor Changes
1902
+
1903
+ - e558992: Ten container icons, for the navigation column that just learned to rail
1904
+
1905
+ A navigation column is a list of places, and a place is a word until the column
1906
+ narrows. `AppShell` can rail now — the column drops to `3.5rem` and every row
1907
+ keeps its glyph and loses its name — which turns "does this row have an icon"
1908
+ from a styling question into whether the row still exists. This library had a
1909
+ public component for the marks (`CueMark`, `CueLogotype`, `Plussie`) and nine
1910
+ private `_glyphs` modules for its own chevrons and carets, and nothing at all
1911
+ for the things a catalogue actually sorts into.
1912
+
1913
+ ```tsx
1914
+ import { CatalogueIcon, ChannelBetaIcon, SkillIcon } from "@cueplusplus/ui";
1915
+
1916
+ <Sidebar.Item icon={CatalogueIcon}>All skills</Sidebar.Item>
1917
+ <Sidebar.Item icon={ChannelBetaIcon}>Our skills · beta</Sidebar.Item>
1918
+ ```
1919
+
1920
+ | Icon | The place it marks |
1921
+ | --------------------- | ------------------------------------------------------ |
1922
+ | `CatalogueIcon` | everything there is, before anything has been narrowed |
1923
+ | `StackIcon` | one person's stack |
1924
+ | `StacksMatrixIcon` | every stack at once, one column each |
1925
+ | `AudienceIcon` | a named group of people something is published to |
1926
+ | `ChannelReleasedIcon` | the released channel |
1927
+ | `ChannelBetaIcon` | the beta channel |
1928
+ | `FolderIcon` | a shelf somebody filed by hand |
1929
+ | `McpServerIcon` | an MCP server in the registry |
1930
+ | `CliToolIcon` | a CLI tool in the registry |
1931
+ | `SkillIcon` | a skill — the thing all the others hold |
1932
+
1933
+ ## It is a set, which is a stronger claim than ten icons
1934
+
1935
+ They are read in one column, one after another, so a grid or a weight that
1936
+ drifts on any one of them is read as a mistake on _that row_ rather than as a
1937
+ style. Every one is drawn from a single shared chassis and the suite holds all
1938
+ three of its rules: one grid (`0 0 16 16` at `stroke-width` 1.5), no size of
1939
+ its own, no colour of its own. There is also a test that no two of them draw
1940
+ the same thing, because a set that ships one mark twice has a hole in it where
1941
+ a reader will look for a distinction.
1942
+
1943
+ Two pairs carry the weight of that. `ChannelReleasedIcon` and `ChannelBetaIcon`
1944
+ are **one ring told twice** — the same `r="5.5"` circle, filled solid and
1945
+ filled halfway — so nobody has to learn which of two unrelated pictures means
1946
+ "finished". And `StackIcon` is plates seen edge-on while `StacksMatrixIcon` is
1947
+ a table with a head rule and a name column, because the question the matrix
1948
+ answers has two axes and the question a stack answers has one.
1949
+
1950
+ ## Sized and inked by the caller, on purpose
1951
+
1952
+ No `size` prop, no `tone` prop. `className="size-icon-md"` reads
1953
+ `--cue-icon-md`, so a glyph tracks the density island it is in — an icon that
1954
+ measured itself would be the one thing in a compact subtree that did not move.
1955
+ Ink is `currentColor` throughout, so a glyph in a muted row is muted and the
1956
+ same glyph in the current row is not, with nothing to pass down.
1957
+
1958
+ They are decorative by default — `aria-hidden` — because they mark a row that
1959
+ already carries its name, and a second announcement of the same word is noise.
1960
+ Props spread last, so `aria-hidden={false} role="img"` with a `<title>` turns
1961
+ one back into a picture where a glyph really is the only name for something.
1962
+
1963
+ ## Why 16 and not 24
1964
+
1965
+ `lucide-react` draws on a 24 grid, and it remains the answer for everything
1966
+ outside this vocabulary — it is still an optional peer, every `icon` slot here
1967
+ still takes a component, and the two mix freely in one column. But `1.5` on a
1968
+ 24 grid renders at two-thirds the weight of `1.5` on a 16 grid in the same box,
1969
+ and every glyph this library already draws is on 16. Matching lucide's number
1970
+ would have meant not matching its own line, and the line is the thing a reader
1971
+ sees. An app that wants these to sit at exactly lucide's weight passes
1972
+ `strokeWidth` — it overrides, like every other attribute.
1973
+
1974
+ ## Ten exports rather than one `Icons` object
1975
+
1976
+ Deliberate, and it costs ten manifest entries instead of one with ten parts. A
1977
+ frozen namespace object does not tree-shake per member: a bundler cannot drop
1978
+ an unused property of an exported object literal, so a console that used two of
1979
+ these would have shipped all ten. Named exports of one module shake to exactly
1980
+ what is imported.
1981
+
1982
+ They ship in the root barrel with no subpath of their own, on the same ruling
1983
+ `brand/` records: the group pulls no optional peer and weighs two files.
1984
+ **Nothing here changes an existing component.** `packages/ui/manifest/**` is
1985
+ regenerated, not hand-edited.
1986
+
1987
+ - 85448d8: ## Three hairlines, derived from the foreground
1988
+
1989
+ A list is not a stack of boxes, and until now the token vocabulary had no way to
1990
+ say the difference.
1991
+
1992
+ `--cue-border` is an authored anchor: every preset picks one, and what every
1993
+ preset picks is a **box edge** — the line around a card, an input, a panel. It is
1994
+ the right weight for exactly that. Repeat it down thirty rows of a table and the
1995
+ page stops reading as a list and starts reading as thirty boxes stacked on each
1996
+ other. The line _between_ things wants to be lighter than the line _around_ a
1997
+ thing, and there was no token for it, so every surface that needed one either
1998
+ borrowed `--cue-border` and looked heavy, or invented a `color-mix()` of its own
1999
+ and stopped being themeable.
2000
+
2001
+ Three new tokens, in every preset, in both modes:
2002
+
2003
+ | Token | Value | What it is |
2004
+ | ------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------- |
2005
+ | `--cue-hair` | `color-mix(in oklch, var(--cue-fg) 6%, transparent)` | **Hairline rule** — between rows, between sections, under a caption |
2006
+ | `--cue-hair-strong` | `color-mix(in oklch, var(--cue-fg) 12%, transparent)` | **Strong hairline** — the same line where it carries weight: a table head, a quiet tag's edge |
2007
+ | `--cue-row-hover` | `color-mix(in oklch, var(--cue-fg) 3.5%, transparent)` | **Row hover wash** — the faintest fill that still reads as one row picked out of a list |
2008
+
2009
+ They are mixed from `--cue-fg`, not authored per preset, and that is the whole
2010
+ point of them. A share of the foreground keeps its relationship to the type it
2011
+ separates whatever the palette turned out to be — the rules lighten with the text
2012
+ in a light block, they hold at `terminal`'s cyan-on-black and at `luma`'s warm
2013
+ paper, and a preset added tomorrow gets all three for free without authoring a
2014
+ thing. Asking ten presets to hand-pick a hairline would have produced ten
2015
+ slightly different answers to a question nobody should have to answer.
2016
+
2017
+ They are emitted **inside** every theme block rather than once on `:root`, like
2018
+ every other derived colour here: a custom property's `var()` references are
2019
+ substituted at the element that declares it, so a `:root`-level mix would freeze
2020
+ the default preset's foreground into every nested theme.
2021
+
2022
+ ## Spending them
2023
+
2024
+ They are not Tailwind utilities, on the same ruling as `--cue-accent-soft`'s
2025
+ status siblings — arbitrary-property syntax reads the token directly:
2026
+
2027
+ ```tsx
2028
+ <div className="border-t border-(--cue-hair) hover:bg-(--cue-row-hover)" />
2029
+ ```
2030
+
2031
+ ## `--cue-row-hover` does not replace the surface ramp
2032
+
2033
+ Worth being exact, because there are now two answers to "how does a row show
2034
+ hover" and only one of them is new. A row that sits **on a surface** still lifts
2035
+ to the next rung — `hover:bg-surface-2` — and that is what this library's own
2036
+ rows do: `Sidebar.Item`, `Tree`, `Pagination`, `Toggle`, `NumberField`. None of
2037
+ them change, and none of them should.
2038
+
2039
+ `--cue-row-hover` is for rows drawn **directly on `--cue-bg`**, where the next
2040
+ rung of the ramp is a step you can see from across the room and the row you are
2041
+ pointing at ends up looking selected rather than hovered. If the ramp fits your
2042
+ row, use the ramp.
2043
+
2044
+ ## `createTheme()` derives them too — and now it cannot forget to
2045
+
2046
+ `@cueplusplus/ui` carries its own copy of the derivation table, because
2047
+ `createTheme()` runs in a browser over a palette somebody just typed while the
2048
+ token build runs at publish time over DTCG files under Style Dictionary, and
2049
+ neither package can import the other's. So a preset you generate gets the three
2050
+ hairlines exactly as a shipped preset does.
2051
+
2052
+ The copy was the real hazard here, and it was already sitting in the tree: **no
2053
+ test compared the two lists.** A derivation added to the build alone would have
2054
+ been missing from every theme a consumer generated, and the only symptom would
2055
+ have been an unstyled rule in somebody else's app weeks later. There is now a
2056
+ test that compares `DERIVED_TOKEN_TEMPLATES` against the compiled stylesheet in
2057
+ both directions, so a one-sided edit fails in this repository instead.
2058
+
2059
+ **Nothing changes for existing consumers.** Both packages gain tokens and neither
2060
+ loses or moves one; no component's rendered output differs.
2061
+
2062
+ - 0cf94cb: Chip takes a third variant, `tag` — a hairline and micro type, no fill at rest
2063
+
2064
+ A list row's name is usually followed by two or three flat facts about it: what
2065
+ kind of thing it is, which channel it came from, what scope it lives in, whether
2066
+ it needs an account before it will do anything. They are not states and nobody
2067
+ is going to click them. Until now the only shape this system offered for them
2068
+ was a `Chip`, and a chip is a container — a tone tint at low alpha inside a tone
2069
+ rim, sized for a status readout that has to be seen from across the room. Put
2070
+ three on a row and repeat the row thirty times and the list stops reading as a
2071
+ list. It reads as ninety small buttons, and the names they were annotating come
2072
+ second to them.
2073
+
2074
+ `variant="tag"` is the same recipe read quietly. No fill. A `--cue-hair-strong`
2075
+ rim instead of `--cue-border` — the token that landed for exactly this, the rule
2076
+ _between_ things rather than the edge _around_ one. Micro type, and the chip's
2077
+ `0.15em` tracking closed to `0.06em`, because tracking cut for label-size type
2078
+ holds a four-letter word at micro apart far enough to read as four letters.
2079
+
2080
+ ```tsx
2081
+ <Chip variant="tag">mcp</Chip>
2082
+ <Chip tone="accent" variant="tag">beta</Chip>
2083
+ <Chip tone="danger" variant="tag">needs auth</Chip>
2084
+ ```
2085
+
2086
+ ## What it is not
2087
+
2088
+ It is not a small chip. The height is still `h-chip` and the radius is still
2089
+ `--cue-radius-control`, so all three variants ride the density ladder together
2090
+ and a row that mixes a tag with a chip has one baseline rather than two. What
2091
+ changed is the volume, not the size — and that is the decision to make when
2092
+ choosing between them. A tag whose job is to be seen is a chip.
2093
+
2094
+ The six toned rims are unchanged at `/35` across all three variants, so a
2095
+ `danger` tag and a `danger` outline chip agree on what danger looks like, which
2096
+ is the whole point of a shared tone vocabulary. Only `neutral` moves, because
2097
+ the neutral line is the one that has two weights now.
2098
+
2099
+ An interactive tag brightens its rim on hover and never grows a fill. The
2100
+ existing tone hover rows are now keyed on the variant so that stays true; a
2101
+ `tinted` or `outline` chip hovers exactly as it did.
2102
+
2103
+ ## The invariant that made room for it
2104
+
2105
+ The recipe already refused to emit a fill and then cancel it — that is why the
2106
+ tinted fills live in `compoundVariants`. Adding a third variant extended the
2107
+ same rule to everything else that varies: the rim, the ink, the gutter, the
2108
+ tracking and the type size are each emitted from exactly one place now, so no
2109
+ call to `chipVariants` ever produces two utilities competing for one property.
2110
+
2111
+ That matters more than it looks. `chipVariants` is exported, and a caller who
2112
+ uses it without `cn` gets a raw class string — where the winner is decided by
2113
+ the order the rules happen to sit in the stylesheet, not by which utility was
2114
+ written last. `tailwind-merge` would not have saved that string either: it
2115
+ cannot see `h-chip h-4` as a conflict at all, because `chip` is a token name and
2116
+ not a scale step. So the base string gave up `px`, the type size and the
2117
+ tracking to the `variant` map, and `neutral` gave up its rim to a compound —
2118
+ `--cue-border` for the two chip variants, `--cue-hair-strong` for the tag. The
2119
+ suite holds the invariant over the whole 7 × 3 × 2 matrix.
2120
+
2121
+ **Nothing here changes an existing chip.** Every tinted and outline chip emits
2122
+ the same set of utilities it did before; only which line of the recipe they come
2123
+ from moved. `packages/ui/manifest/**` is regenerated, not hand-edited.
2124
+
2125
+ - 2a21229: AppShell rails its navigation column, and the header cell over it moves with it
2126
+
2127
+ A console's navigation is set once and then wanted out of the way, and the shape
2128
+ that answers that is an icon rail: the column narrows to a strip of glyphs and
2129
+ the work area takes the 10rem back. `Sidebar` has been able to do the narrowing
2130
+ since it shipped — controlled `collapsed`, `data-collapsed`, an `sr-only` label
2131
+ so an icon-only row keeps its accessible name, a width transition that switches
2132
+ off under `prefers-reduced-motion`. What it could not do was tell anything
2133
+ outside itself, and the thing that needs telling is the cell in the bar directly
2134
+ above it.
2135
+
2136
+ That cell did not exist here at all. `TitleBar` and `AppBar` are full-width
2137
+ strips with leading, centre and trailing slots; neither knows the column's
2138
+ width, and a console with an app mark over its navigation has been hand-rolling
2139
+ one. Hand-rolling it is where the defect lives: the cell is a child of the bar
2140
+ and the column is a child of the body, two different branches of the tree, and
2141
+ two boxes that are meant to read as one column will drift the first time one of
2142
+ them changes width. A rail changes it on every click.
2143
+
2144
+ ## One length, written once
2145
+
2146
+ `AppShell.Root` now writes `--cue-nav-width` on the frame — `navWidth`
2147
+ (`"14rem"`) while expanded, `railWidth` (`"3.5rem"`) while railed — and both
2148
+ boxes read it. Neither holds a width of its own, so railing rewrites one length
2149
+ and the cell and the column arrive together rather than a frame apart. It is
2150
+ also the handle a consumer's own CSS aligns to: a sticky rule, a toolbar offset,
2151
+ a drop shadow that should stop where the column does.
2152
+
2153
+ `3.5rem` is a `--cue-icon-md` glyph centred in a `--cue-control-md` row with a
2154
+ gutter either side that still reads as a column rather than a strip. It is half
2155
+ a rem wider than the `3rem` `Sidebar.Root` collapses to on its own, which is the
2156
+ one visible change to an existing console: a shell whose column is collapsed
2157
+ from its hairline rail is now 8px wider there. Set `railWidth="3rem"` on
2158
+ `AppShell.Root` to keep the old measure exactly.
2159
+
2160
+ ## The new surface
2161
+ - **`AppShell.Root`** takes `rail` / `defaultRail` / `onRailChange` — the same
2162
+ controlled-prop bargain `navOpen` already makes — plus `navWidth` and
2163
+ `railWidth`. It stamps `data-rail` on itself and declares `group/app-shell`
2164
+ beside it, so anything in the frame can answer in plain Tailwind:
2165
+ `group-data-[rail]/app-shell:hidden` on a wordmark, the way `Sidebar` already
2166
+ publishes `data-collapsed` for its own parts.
2167
+ - **`AppShell.Bar`** is the bar divided by the navigation column: a `brand` slot
2168
+ exactly the column's width, wearing the column's rim, and the rest of the bar
2169
+ beside it. Omit `brand` and it is one undivided strip — an empty bordered cell
2170
+ over the column is worse than no cell. It stays a `<div>`, like `TitleBar`
2171
+ beside it, so a frame that also carries an `AppBar` does not end up with two
2172
+ `banner` landmarks; a page that wants one says `role="banner"`.
2173
+ - **`AppShell.Sidebar`** hands the frame's rail down to `Sidebar.Root` as its
2174
+ controlled `collapsed` and writes it back whenever the column's own hairline
2175
+ rail is pressed, so the two states cannot disagree and there is nothing to
2176
+ keep in sync.
2177
+
2178
+ The rail is off by default and every part of this is additive. A `collapsed` or
2179
+ `defaultCollapsed` passed to `AppShell.Sidebar` is the pre-rail spelling and
2180
+ still means exactly what it did — the column keeps its own state and its own two
2181
+ lengths, and the frame stays out of it rather than half-driving a column it
2182
+ could not have seeded, because a child cannot seed its parent's state. Reach for
2183
+ `defaultRail` on the frame when both boxes should move. A `width` passed to
2184
+ `AppShell.Sidebar` likewise still wins, for the column alone, which is a
2185
+ misalignment worth knowing about before writing it.
2186
+
2187
+ ## What is deliberately not here
2188
+
2189
+ **The toggle.** `Sidebar.Rail` — the 4px hit strip the shell already draws on
2190
+ the column's edge — rails the frame now, and any other control is one you render
2191
+ and wire, because where a rail button lives is a decision about your bar. The
2192
+ guidance ships the pattern: an `IconButton` with `aria-pressed` rather than
2193
+ `aria-expanded` (the column is still there either way, so it is a two-state
2194
+ toggle and not a disclosure) and `aria-controls` pointed at the `<nav>`.
2195
+
2196
+ **Persistence.** A rail is set once and expected to be remembered, and _where_
2197
+ it is remembered is a decision about hydration and privacy that a layout
2198
+ component has no business making. Read it from a cookie on the server and the
2199
+ first painted frame is already railed; read it from `localStorage` in an effect
2200
+ and the column jumps once after hydration. That is the whole reason the choice
2201
+ is yours and not the shell's.
2202
+
2203
+ The `md` breakpoint now wears a third face — the header cell stops measuring
2204
+ itself from a column that below 48rem is not in the layout at all, which keeps
2205
+ 14rem of a 390px bar from being walled off for nothing. It is the same
2206
+ navigation-shape decision the column and the drawer trigger already make, and
2207
+ the library's breakpoint budget is still that one decision and nothing else.
2208
+
2209
+ ### Patch Changes
2210
+
2211
+ - ad9a0e9: ## One space in a `TimeField` was throwing away the server render
2212
+
2213
+ A segmented date or time field prints text that `Intl.DateTimeFormat` produced,
2214
+ and `Intl` answers from the ICU data of whatever is running it. Today, `en-US` at
2215
+ half past seven in the evening is `7:30 PM` from Node 24 (CLDR 48) and
2216
+ `7:30 PM` from Chrome 151 — a narrow no-break space against an ordinary one.
2217
+ Identical on screen. Not identical to React.
2218
+
2219
+ A hydration mismatch is not a warning. React discards the server's HTML and
2220
+ re-renders the whole document on the client, and that re-mounts `<html>` — which
2221
+ React treats as a _singleton_ and therefore strips every attribute off before
2222
+ re-applying the ones the server rendered. `data-theme`, `data-density`,
2223
+ `data-font`, `data-mode` and `color-scheme` are not among them: they are written
2224
+ by `prepaintScript()`, client-side, before React exists. So the document spends
2225
+ the whole client render wearing bare `:root` — the library's default preset, no
2226
+ density, no pairing — and then snaps back.
2227
+
2228
+ Measured on this design system's own kitchen sink, in headless Chrome with the
2229
+ cache disabled: **208 ms** with no axis on the document, 6,423 elements resolving
2230
+ to a monospace nobody had selected, and **40 KB** of webfont fetched for it that
2231
+ then painted zero glyphs — on _every_ pairing, including the two that download
2232
+ nothing at all.
2233
+
2234
+ The literal separators now carry their text in an element of their own, marked
2235
+ `suppressHydrationWarning`. React only honours that flag on the element directly
2236
+ containing the differing text, and React Aria's `filterDOMProps` drops it before
2237
+ the segment's own element sees it, which is why the extra element exists. Only
2238
+ literal segments are wrapped — they are `aria-hidden` and not focusable. The
2239
+ value segments React Aria makes `contentEditable` are untouched, and digits and
2240
+ `AM`/`PM` are spelled the same by every ICU there has been.
2241
+
2242
+ **If you style literal separators**, `[data-type="literal"]` still selects them
2243
+ and still carries the text; a child-element selector under one now finds a
2244
+ `<span>`.
2245
+
2246
+ ## `ThemeProvider` stops waiting a commit to dress the document
2247
+
2248
+ Related, and found on the way. The `<html>` stamp used to wait for the provider's
2249
+ persisted preferences to be restored — a commit later — because on the first one
2250
+ its state is still the props, and writing those would put the app's defaults on a
2251
+ document the visitor had already chosen for. Waiting closed that hole by leaving
2252
+ another one: a subtree could be measured before the stamp landed.
2253
+
2254
+ It now writes on the first commit, from the same storage `prepaintScript()` read,
2255
+ and it writes in an _insertion_ effect, which React runs before any layout effect
2256
+ in the tree — so nothing measures the document before it is dressed. A value that
2257
+ is already correct is not rewritten, so the usual case is no DOM mutation at all.
2258
+
2259
+ No API moved. An app that ships the pre-paint script and the provider together,
2260
+ as the getting-started page has always said to, sees only that the first frame is
2261
+ the right one more often.
2262
+
2263
+ - Updated dependencies [85448d8]
2264
+ - @cueplusplus/tokens@0.4.0
2265
+
3
2266
  ## 0.3.0
4
2267
 
5
2268
  ### Minor Changes