@elabs-ai/components-ui 4.0.0 → 4.2.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 (339) hide show
  1. package/README.md +10 -8
  2. package/dist/chunk-FCH4ZN6G.js +57 -0
  3. package/dist/chunk-FCH4ZN6G.js.map +1 -0
  4. package/dist/form.d.ts +28 -0
  5. package/dist/form.js +123 -0
  6. package/dist/form.js.map +1 -0
  7. package/dist/index.d.ts +2597 -188
  8. package/dist/index.js +12855 -3578
  9. package/dist/index.js.map +1 -1
  10. package/dist/lib/cn.js +10 -1
  11. package/dist/lib/cn.js.map +1 -1
  12. package/package.json +21 -7
  13. package/src/__contract__/attribution-panel.contract.test.tsx +49 -0
  14. package/src/__contract__/badge.contract.test.tsx +49 -0
  15. package/src/__contract__/bounded-number.contract.test.tsx +49 -0
  16. package/src/__contract__/button.contract.test.tsx +49 -0
  17. package/src/__contract__/color-picker.contract.test.tsx +49 -0
  18. package/src/__contract__/combobox.contract.test.tsx +49 -0
  19. package/src/__contract__/command-trigger.contract.test.tsx +49 -0
  20. package/src/__contract__/copyable-value.contract.test.tsx +49 -0
  21. package/src/__contract__/date-picker.contract.test.tsx +49 -0
  22. package/src/__contract__/date-range-picker.contract.test.tsx +49 -0
  23. package/src/__contract__/empty-state.contract.test.tsx +49 -0
  24. package/src/__contract__/error-state.contract.test.tsx +49 -0
  25. package/src/__contract__/icon-button.contract.test.tsx +49 -0
  26. package/src/__contract__/input.contract.test.tsx +49 -0
  27. package/src/__contract__/key-value-editor.contract.test.tsx +49 -0
  28. package/src/__contract__/list-editor.contract.test.tsx +49 -0
  29. package/src/__contract__/loading-state.contract.test.tsx +49 -0
  30. package/src/__contract__/metric-card.contract.test.tsx +49 -0
  31. package/src/__contract__/model-picker.contract.test.tsx +49 -0
  32. package/src/__contract__/nav-notifications.contract.test.tsx +49 -0
  33. package/src/__contract__/number-input.contract.test.tsx +49 -0
  34. package/src/__contract__/rating.contract.test.tsx +49 -0
  35. package/src/__contract__/schema-form.contract.test.tsx +49 -0
  36. package/src/__contract__/section-header.contract.test.tsx +49 -0
  37. package/src/__contract__/segmented-field.contract.test.tsx +49 -0
  38. package/src/__contract__/slider-number.contract.test.tsx +49 -0
  39. package/src/__contract__/status-badge.contract.test.tsx +49 -0
  40. package/src/__contract__/tag-input.contract.test.tsx +49 -0
  41. package/src/__contract__/text.contract.test.tsx +49 -0
  42. package/src/__contract__/textarea.contract.test.tsx +49 -0
  43. package/src/__contract__/timeline.contract.test.tsx +49 -0
  44. package/src/__contract__/toggle.contract.test.tsx +49 -0
  45. package/src/__contract__/transfer.contract.test.tsx +49 -0
  46. package/src/__contract__/tree-select.contract.test.tsx +49 -0
  47. package/src/__contract__/tree.contract.test.tsx +49 -0
  48. package/src/__contract__/virtual-select.contract.test.tsx +49 -0
  49. package/src/__contract__/workspace-picker.contract.test.tsx +49 -0
  50. package/src/_peer-deps.test.ts +59 -0
  51. package/src/blocks-comparison-table.stories.tsx +2 -2
  52. package/src/components/accordion/accordion.tsx +13 -4
  53. package/src/components/advanced-group/advanced-group.tsx +1 -1
  54. package/src/components/alert/alert.tsx +15 -4
  55. package/src/components/alert-dialog/alert-dialog.test.tsx +32 -0
  56. package/src/components/alert-dialog/alert-dialog.tsx +28 -6
  57. package/src/components/app-shell/app-shell.stories.tsx +44 -2
  58. package/src/components/app-shell/app-shell.test.tsx +99 -0
  59. package/src/components/app-shell/app-shell.tsx +56 -4
  60. package/src/components/app-sidebar/app-sidebar.stories.tsx +9 -1
  61. package/src/components/attribution-panel/attribution-panel.tsx +3 -1
  62. package/src/components/attribution-panel/attributions.generated.ts +83 -79
  63. package/src/components/avatar/avatar.tsx +4 -1
  64. package/src/components/badge/badge.stories.tsx +8 -0
  65. package/src/components/badge/badge.test.tsx +8 -0
  66. package/src/components/badge/badge.tsx +15 -5
  67. package/src/components/bento-grid/bento-grid.test.tsx +24 -3
  68. package/src/components/bento-grid/bento-grid.tsx +11 -1
  69. package/src/components/bounded-number/bounded-number.test.tsx +1 -1
  70. package/src/components/bounded-number/bounded-number.tsx +78 -75
  71. package/src/components/breadcrumb/breadcrumb.stories.tsx +38 -0
  72. package/src/components/breadcrumb/breadcrumb.test.tsx +24 -1
  73. package/src/components/breadcrumb/breadcrumb.tsx +10 -4
  74. package/src/components/button/button.stories.tsx +116 -8
  75. package/src/components/button/button.tsx +18 -12
  76. package/src/components/button-group/button-group.tsx +1 -1
  77. package/src/components/calendar/calendar.tsx +3 -2
  78. package/src/components/card/card.stories.tsx +11 -6
  79. package/src/components/card/card.test.tsx +24 -2
  80. package/src/components/card/card.tsx +48 -7
  81. package/src/components/carousel/carousel.test.tsx +169 -0
  82. package/src/components/carousel/carousel.tsx +93 -32
  83. package/src/components/change-review/change-review.stories.tsx +145 -1
  84. package/src/components/change-review/change-review.test.tsx +143 -0
  85. package/src/components/change-review/change-review.tsx +214 -24
  86. package/src/components/checkbox/checkbox.tsx +2 -1
  87. package/src/components/collapsible-panel/use-collapsible-panel.ts +0 -3
  88. package/src/components/color-picker/color-picker.tsx +22 -15
  89. package/src/components/combobox/combobox.stories.tsx +10 -0
  90. package/src/components/combobox/combobox.test.tsx +54 -0
  91. package/src/components/combobox/combobox.tsx +25 -11
  92. package/src/components/command/command.stories.tsx +16 -6
  93. package/src/components/command/command.test.tsx +88 -0
  94. package/src/components/command/command.tsx +38 -13
  95. package/src/components/command/index.ts +1 -0
  96. package/src/components/command-trigger/command-trigger.stories.tsx +39 -0
  97. package/src/components/command-trigger/command-trigger.test.tsx +18 -0
  98. package/src/components/command-trigger/command-trigger.tsx +70 -0
  99. package/src/components/command-trigger/index.ts +1 -0
  100. package/src/components/context-menu/context-menu.tsx +16 -7
  101. package/src/components/context-rail/context-rail.stories.tsx +285 -0
  102. package/src/components/context-rail/context-rail.test.tsx +263 -0
  103. package/src/components/context-rail/context-rail.tsx +678 -0
  104. package/src/components/context-rail/index.ts +1 -0
  105. package/src/components/copyable-value/copyable-value.stories.tsx +10 -0
  106. package/src/components/copyable-value/copyable-value.test.tsx +28 -1
  107. package/src/components/copyable-value/copyable-value.tsx +13 -2
  108. package/src/components/date-picker/date-picker.stories.tsx +9 -0
  109. package/src/components/date-picker/date-picker.test.tsx +52 -0
  110. package/src/components/date-picker/date-picker.tsx +13 -15
  111. package/src/components/date-range-picker/date-range-picker.stories.tsx +9 -0
  112. package/src/components/date-range-picker/date-range-picker.test.tsx +76 -1
  113. package/src/components/date-range-picker/date-range-picker.tsx +39 -28
  114. package/src/components/descriptions/descriptions.tsx +1 -1
  115. package/src/components/dialog/dialog.test.tsx +8 -3
  116. package/src/components/dialog/dialog.tsx +14 -6
  117. package/src/components/drawer/drawer.tsx +23 -6
  118. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +1 -1
  119. package/src/components/dropdown-menu/dropdown-menu.tsx +4 -4
  120. package/src/components/empty-state/empty-state.test.tsx +26 -0
  121. package/src/components/empty-state/empty-state.tsx +6 -1
  122. package/src/components/expand-dialog/expand-dialog.tsx +1 -2
  123. package/src/components/field/field-context.ts +59 -0
  124. package/src/components/field/field.stories.tsx +142 -0
  125. package/src/components/field/field.test.tsx +415 -0
  126. package/src/components/field/field.tsx +342 -0
  127. package/src/components/field/index.ts +9 -0
  128. package/src/components/field-row/field-row.stories.tsx +85 -1
  129. package/src/components/field-row/field-row.test.tsx +145 -0
  130. package/src/components/field-row/field-row.tsx +50 -6
  131. package/src/components/file-upload/file-upload.stories.tsx +16 -0
  132. package/src/components/file-upload/file-upload.test.tsx +90 -12
  133. package/src/components/file-upload/file-upload.tsx +164 -31
  134. package/src/components/form/form.tsx +8 -3
  135. package/src/components/hover-card/hover-card.tsx +1 -0
  136. package/src/components/icon-button/icon-button.test.tsx +1 -1
  137. package/src/components/icon-button/icon-button.tsx +3 -3
  138. package/src/components/input/input.stories.tsx +121 -0
  139. package/src/components/input/input.test.tsx +13 -0
  140. package/src/components/input/input.tsx +8 -4
  141. package/src/components/input-group/input-group.stories.tsx +74 -0
  142. package/src/components/input-group/input-group.tsx +18 -9
  143. package/src/components/input-otp/input-otp.tsx +8 -2
  144. package/src/components/keyboard-shortcuts/index.ts +6 -0
  145. package/src/components/keyboard-shortcuts/keyboard-shortcuts.stories.tsx +177 -0
  146. package/src/components/keyboard-shortcuts/keyboard-shortcuts.test.tsx +73 -0
  147. package/src/components/keyboard-shortcuts/keyboard-shortcuts.tsx +203 -0
  148. package/src/components/label/label.tsx +1 -1
  149. package/src/components/link-preview/link-preview.tsx +1 -1
  150. package/src/components/list-editor/list-editor.test.tsx +24 -0
  151. package/src/components/list-editor/list-editor.tsx +126 -52
  152. package/src/components/locale-provider/index.ts +6 -1
  153. package/src/components/locale-provider/locale-keys.test.ts +134 -0
  154. package/src/components/locale-provider/locale-provider.stories.tsx +89 -1
  155. package/src/components/locale-provider/locale-provider.test.tsx +257 -1
  156. package/src/components/locale-provider/locale-provider.tsx +63 -8
  157. package/src/components/locale-provider/messages.ts +822 -2
  158. package/src/components/match-highlight/match-highlight.stories.tsx +1 -1
  159. package/src/components/mention-input/mention-input-mirror.stories.tsx +9 -1
  160. package/src/components/mention-input/mention-input.stories.tsx +3 -3
  161. package/src/components/mention-input/mention-input.test.tsx +51 -1
  162. package/src/components/mention-input/mention-input.tsx +66 -46
  163. package/src/components/mention-input/mention-value.ts +8 -2
  164. package/src/components/menubar/menubar.tsx +16 -6
  165. package/src/components/metric-card/metric-card.stories.tsx +5 -3
  166. package/src/components/metric-card/metric-card.tsx +21 -2
  167. package/src/components/model-picker/model-picker.stories.tsx +63 -3
  168. package/src/components/model-picker/model-picker.test.tsx +63 -0
  169. package/src/components/model-picker/model-picker.tsx +62 -36
  170. package/src/components/nav-main/nav-main.tsx +8 -4
  171. package/src/components/nav-notifications/nav-notifications.stories.tsx +110 -0
  172. package/src/components/nav-notifications/nav-notifications.test.tsx +11 -0
  173. package/src/components/nav-notifications/nav-notifications.tsx +67 -9
  174. package/src/components/nav-user/nav-user.tsx +5 -5
  175. package/src/components/navigation-menu/navigation-menu.stories.tsx +22 -0
  176. package/src/components/navigation-menu/navigation-menu.test.tsx +174 -0
  177. package/src/components/navigation-menu/navigation-menu.tsx +204 -15
  178. package/src/components/number-input/number-input.stories.tsx +7 -3
  179. package/src/components/number-input/number-input.test.tsx +47 -0
  180. package/src/components/number-input/number-input.tsx +65 -24
  181. package/src/components/page-shell/page-shell.stories.tsx +163 -0
  182. package/src/components/page-shell/page-shell.test.tsx +229 -0
  183. package/src/components/page-shell/page-shell.tsx +108 -25
  184. package/src/components/pagination/pagination.test.tsx +109 -0
  185. package/src/components/pagination/pagination.tsx +27 -19
  186. package/src/components/popover/popover.tsx +1 -0
  187. package/src/components/progress/progress.tsx +2 -0
  188. package/src/components/radio-group/radio-group.tsx +1 -1
  189. package/src/components/rating/rating.tsx +3 -1
  190. package/src/components/resizable/resizable.stories.tsx +114 -2
  191. package/src/components/resizable/resizable.test.tsx +76 -0
  192. package/src/components/resizable/resizable.tsx +32 -6
  193. package/src/components/revision-timeline/revision-timeline.stories.tsx +8 -1
  194. package/src/components/revision-timeline/revision-timeline.test.tsx +26 -0
  195. package/src/components/revision-timeline/revision-timeline.tsx +53 -5
  196. package/src/components/schema-form/from-json-schema.test.ts +390 -0
  197. package/src/components/schema-form/from-json-schema.ts +375 -0
  198. package/src/components/schema-form/index.ts +81 -0
  199. package/src/components/schema-form/schema-form-spec.ts +698 -0
  200. package/src/components/schema-form/schema-form-store.ts +506 -0
  201. package/src/components/schema-form/schema-form.stories.tsx +566 -0
  202. package/src/components/schema-form/schema-form.test.tsx +1021 -0
  203. package/src/components/schema-form/schema-form.tsx +1381 -0
  204. package/src/components/section-header/section-header.stories.tsx +37 -0
  205. package/src/components/section-header/section-header.tsx +14 -3
  206. package/src/components/select/select.tsx +13 -7
  207. package/src/components/separator/separator.tsx +1 -0
  208. package/src/components/sheet/sheet.test.tsx +39 -0
  209. package/src/components/sheet/sheet.tsx +40 -9
  210. package/src/components/side-dock/index.ts +1 -0
  211. package/src/components/side-dock/side-dock.stories.tsx +137 -0
  212. package/src/components/side-dock/side-dock.test.tsx +299 -0
  213. package/src/components/side-dock/side-dock.tsx +515 -0
  214. package/src/components/sidebar/index.ts +1 -0
  215. package/src/components/sidebar/sidebar-frame.test.tsx +162 -0
  216. package/src/components/sidebar/sidebar.stories.tsx +421 -1
  217. package/src/components/sidebar/sidebar.test.tsx +71 -2
  218. package/src/components/sidebar/sidebar.tsx +264 -38
  219. package/src/components/skeleton/skeleton.tsx +1 -0
  220. package/src/components/skip-link/index.ts +1 -0
  221. package/src/components/skip-link/skip-link.stories.tsx +106 -0
  222. package/src/components/skip-link/skip-link.test.tsx +20 -0
  223. package/src/components/skip-link/skip-link.tsx +34 -0
  224. package/src/components/slider/slider.tsx +1 -1
  225. package/src/components/slider-number/slider-number.stories.tsx +4 -1
  226. package/src/components/slider-number/slider-number.test.tsx +1 -1
  227. package/src/components/sonner/sonner.tsx +2 -0
  228. package/src/components/spinner/spinner.test.tsx +18 -0
  229. package/src/components/spinner/spinner.tsx +14 -4
  230. package/src/components/split-panel/index.ts +6 -1
  231. package/src/components/split-panel/split-panel.stories.tsx +18 -2
  232. package/src/components/split-panel/split-panel.test.tsx +20 -0
  233. package/src/components/split-panel/split-panel.tsx +28 -8
  234. package/src/components/state-panel/state-panel.stories.tsx +52 -0
  235. package/src/components/state-panel/state-panel.test.tsx +125 -0
  236. package/src/components/state-panel/state-panel.tsx +110 -24
  237. package/src/components/status-badge/status-badge.stories.tsx +8 -0
  238. package/src/components/switch/switch.tsx +3 -1
  239. package/src/components/table/table.stories.tsx +64 -0
  240. package/src/components/table/table.test.tsx +149 -1
  241. package/src/components/table/table.tsx +71 -2
  242. package/src/components/tabs/index.ts +10 -1
  243. package/src/components/tabs/tabs.stories.tsx +48 -0
  244. package/src/components/tabs/tabs.test.tsx +67 -0
  245. package/src/components/tabs/tabs.tsx +176 -72
  246. package/src/components/tag-input/tag-input.tsx +6 -3
  247. package/src/components/team-switcher/team-switcher.tsx +35 -6
  248. package/src/components/textarea/textarea.tsx +2 -2
  249. package/src/components/theme-switcher/theme-switcher.stories.tsx +76 -0
  250. package/src/components/theme-switcher/theme-switcher.test.tsx +182 -0
  251. package/src/components/theme-switcher/theme-switcher.tsx +140 -12
  252. package/src/components/theme-switcher/use-theme-transition.ts +2 -2
  253. package/src/components/timeline/timeline.stories.tsx +27 -0
  254. package/src/components/timeline/timeline.tsx +10 -3
  255. package/src/components/toggle/toggle.tsx +7 -2
  256. package/src/components/toggle-group/toggle-group.tsx +2 -0
  257. package/src/components/toolbar/toolbar.stories.tsx +10 -2
  258. package/src/components/tooltip/tooltip.tsx +2 -1
  259. package/src/components/top-nav/top-nav.tsx +1 -1
  260. package/src/components/transfer/transfer.tsx +35 -30
  261. package/src/components/tree/tree.stories.tsx +2 -2
  262. package/src/components/tree/tree.test.tsx +41 -0
  263. package/src/components/tree/tree.tsx +91 -31
  264. package/src/components/tree/use-tree-keyboard.ts +37 -14
  265. package/src/components/tree-select/tree-select.stories.tsx +9 -0
  266. package/src/components/tree-select/tree-select.test.tsx +5 -0
  267. package/src/components/tree-select/tree-select.tsx +11 -14
  268. package/src/components/typography/prose.test.tsx +3 -2
  269. package/src/components/typography/prose.tsx +3 -3
  270. package/src/components/typography/typography.stories.tsx +228 -0
  271. package/src/components/view-toolbar/view-toolbar.stories.tsx +41 -1
  272. package/src/components/view-toolbar/view-toolbar.test.tsx +33 -0
  273. package/src/components/view-toolbar/view-toolbar.tsx +37 -4
  274. package/src/components/virtual-select/virtual-select.stories.tsx +9 -0
  275. package/src/components/virtual-select/virtual-select.test.tsx +29 -3
  276. package/src/components/virtual-select/virtual-select.tsx +56 -18
  277. package/src/components/wizard/wizard.test.tsx +9 -0
  278. package/src/components/wizard/wizard.tsx +13 -5
  279. package/src/components/workspace-picker/index.ts +3 -0
  280. package/src/components/workspace-picker/workspace-picker-state.ts +50 -0
  281. package/src/components/workspace-picker/workspace-picker.stories.tsx +126 -0
  282. package/src/components/workspace-picker/workspace-picker.test.tsx +207 -0
  283. package/src/components/workspace-picker/workspace-picker.tsx +172 -0
  284. package/src/illustrations/empty-list-illustration.tsx +26 -0
  285. package/src/illustrations/error-illustration.tsx +60 -0
  286. package/src/illustrations/first-run-illustration.tsx +27 -0
  287. package/src/illustrations/illustration-base.tsx +108 -0
  288. package/src/illustrations/illustrations.stories.tsx +94 -0
  289. package/src/illustrations/illustrations.test.tsx +98 -0
  290. package/src/illustrations/index.ts +16 -0
  291. package/src/illustrations/no-access-illustration.tsx +45 -0
  292. package/src/illustrations/no-results-illustration.tsx +46 -0
  293. package/src/illustrations/offline-illustration.tsx +37 -0
  294. package/src/illustrations/success-illustration.tsx +40 -0
  295. package/src/index.ts +80 -2
  296. package/src/lib/agent-event-model.test.ts +12 -0
  297. package/src/lib/agent-event-model.ts +42 -0
  298. package/src/lib/approval-option.test.ts +17 -0
  299. package/src/lib/approval-option.ts +53 -0
  300. package/src/lib/check-result.ts +33 -0
  301. package/src/lib/cn.ts +9 -0
  302. package/src/lib/csv.ts +48 -0
  303. package/src/lib/diff-line.test.ts +36 -0
  304. package/src/lib/diff-line.ts +63 -0
  305. package/src/lib/diff-rows.test.ts +76 -0
  306. package/src/lib/diff-rows.ts +94 -0
  307. package/src/lib/format-duration.test.ts +22 -0
  308. package/src/lib/format-duration.ts +30 -0
  309. package/src/lib/has-renderable-content.test.ts +49 -0
  310. package/src/lib/has-renderable-content.ts +32 -0
  311. package/src/lib/merge-refs.ts +2 -0
  312. package/src/lib/operating-mode.test.ts +36 -0
  313. package/src/lib/operating-mode.ts +81 -0
  314. package/src/lib/optional-peer.ts +59 -0
  315. package/src/lib/session-launch.ts +37 -0
  316. package/src/lib/slash-command.test.ts +44 -0
  317. package/src/lib/slash-command.ts +45 -0
  318. package/src/lib/trigger-query.test.ts +94 -0
  319. package/src/lib/trigger-query.ts +88 -0
  320. package/src/lib/use-controllable-state.ts +47 -0
  321. package/src/lib/use-mobile.test.tsx +47 -0
  322. package/src/lib/use-mobile.ts +34 -12
  323. package/src/motion.stories.tsx +6 -0
  324. package/src/templates-object-detail-hub.stories.tsx +2 -2
  325. package/src/templates-screen-states.stories.tsx +5 -5
  326. package/src/templates-settings.stories.tsx +1 -1
  327. package/src/blocks/sidebar-02/app-sidebar.tsx +0 -160
  328. package/src/blocks/sidebar-02/logo.tsx +0 -18
  329. package/src/blocks/sidebar-02/nav-main.tsx +0 -13
  330. package/src/blocks/sidebar-02/nav-notifications.tsx +0 -7
  331. package/src/blocks/sidebar-02/sidebar-02.stories.tsx +0 -23
  332. package/src/blocks/sidebar-02/team-switcher.tsx +0 -7
  333. package/src/blocks/sidebar-04/app-sidebar.tsx +0 -236
  334. package/src/blocks/sidebar-04/mail-context.tsx +0 -29
  335. package/src/blocks/sidebar-04/nav-user.tsx +0 -6
  336. package/src/blocks/sidebar-04/sidebar-04.stories.tsx +0 -150
  337. package/src/blocks/sidebar-05/app-sidebar.tsx +0 -375
  338. package/src/blocks/sidebar-05/sidebar-05.stories.tsx +0 -18
  339. package/src/blocks/sidebar-05/team-switcher.tsx +0 -6
@@ -0,0 +1,46 @@
1
+ import { forwardRef } from "react";
2
+ import {
3
+ illustrationSvgProps,
4
+ illustrationAccent,
5
+ type IllustrationProps,
6
+ } from "./illustration-base";
7
+
8
+ /**
9
+ * Search / filter with zero matches. A line-art magnifying glass with a
10
+ * `--primary-text` "×" inside the lens standing in for "nothing matched".
11
+ * The "×" is meaning-bearing (per the accent-semantics convention in
12
+ * `illustration-base.tsx`, #48 finding 1) — a bare magnifying glass alone
13
+ * reads as "search", not "no results".
14
+ */
15
+ export const NoResultsIllustration = forwardRef<SVGSVGElement, IllustrationProps>(
16
+ function NoResultsIllustration({ size, className, ...props }, ref) {
17
+ return (
18
+ <svg
19
+ ref={ref}
20
+ {...illustrationSvgProps(size, className)}
21
+ data-slot="no-results-illustration"
22
+ {...props}
23
+ >
24
+ <circle cx="68" cy="66" r="30" />
25
+ <line x1="89.2" y1="87.2" x2="124" y2="122" />
26
+ <line
27
+ x1="56"
28
+ y1="54"
29
+ x2="80"
30
+ y2="78"
31
+ style={{ stroke: illustrationAccent("var(--primary-text)") }}
32
+ strokeWidth={3}
33
+ />
34
+ <line
35
+ x1="80"
36
+ y1="54"
37
+ x2="56"
38
+ y2="78"
39
+ style={{ stroke: illustrationAccent("var(--primary-text)") }}
40
+ strokeWidth={3}
41
+ />
42
+ </svg>
43
+ );
44
+ },
45
+ );
46
+ NoResultsIllustration.displayName = "NoResultsIllustration";
@@ -0,0 +1,37 @@
1
+ import { forwardRef } from "react";
2
+ import { illustrationSvgProps, type IllustrationProps } from "./illustration-base";
3
+
4
+ /**
5
+ * Disconnected / no-network state. A line-art cloud crossed by a slash — the
6
+ * universal "disabled/no signal" idiom — reads as "offline" on its own
7
+ * silhouette. No accent mark, per the accent-semantics convention in
8
+ * `illustration-base.tsx` (#48 finding 1).
9
+ *
10
+ * Redrawn for #24 fix round 1 (P1-4) — the previous cloud path's closing curve
11
+ * left a visible hook/spur, and the dashed tether + falling node read as a
12
+ * raindrop rather than a severed connection while pulling the ink centre 11.5
13
+ * units below the shared stage centre.
14
+ *
15
+ * Redrawn again for #48 finding 4 — the two small rounded-rect "chain link"
16
+ * fragments below the slash read as loose debris/dashes rather than a
17
+ * "broken link" gesture. Removed rather than redrawn: the cloud+slash
18
+ * silhouette already fully conveys "offline" without them (the same
19
+ * reasoning that dropped the decorative accents from `EmptyListIllustration`/
20
+ * `FirstRunIllustration`).
21
+ */
22
+ export const OfflineIllustration = forwardRef<SVGSVGElement, IllustrationProps>(
23
+ function OfflineIllustration({ size, className, ...props }, ref) {
24
+ return (
25
+ <svg
26
+ ref={ref}
27
+ {...illustrationSvgProps(size, className)}
28
+ data-slot="offline-illustration"
29
+ {...props}
30
+ >
31
+ <path d="M44.9,100.4 C44.9,83.6 56.4,72.4 67.9,75.2 C70.2,58.4 86.3,50 97.8,61.2 C111.6,55.6 125.4,69.6 120.8,86.4 C127.7,92 125.4,106 116.2,106 L54.1,106 C49.5,106 44.9,104.6 44.9,100.4 Z" />
32
+ <line x1="36" y1="112" x2="124" y2="36" strokeWidth={4} />
33
+ </svg>
34
+ );
35
+ },
36
+ );
37
+ OfflineIllustration.displayName = "OfflineIllustration";
@@ -0,0 +1,40 @@
1
+ import { forwardRef } from "react";
2
+ import {
3
+ illustrationSvgProps,
4
+ illustrationAccent,
5
+ type IllustrationProps,
6
+ } from "./illustration-base";
7
+
8
+ /**
9
+ * Completed / positive-outcome state. A line-art ring with a `--success-text`
10
+ * checkmark — a ring + check needs no rays. The checkmark is meaning-bearing
11
+ * (per the accent-semantics convention in `illustration-base.tsx`, #48
12
+ * finding 1) — a bare ring alone carries no state at all.
13
+ *
14
+ * Redrawn for #24 fix round 1 (P0-1, P1-4) — the checkmark previously used
15
+ * `--primary` (re-creating the "success chips read as primary" confusion #334
16
+ * removed from the tokens) at `strokeWidth={6}` (off the shared accent-weight
17
+ * rule), and the three radiating ticks straddled the now-removed dashed stage
18
+ * (two crossed it, one didn't), reading as a clock/sun rather than a
19
+ * celebration.
20
+ */
21
+ export const SuccessIllustration = forwardRef<SVGSVGElement, IllustrationProps>(
22
+ function SuccessIllustration({ size, className, ...props }, ref) {
23
+ return (
24
+ <svg
25
+ ref={ref}
26
+ {...illustrationSvgProps(size, className)}
27
+ data-slot="success-illustration"
28
+ {...props}
29
+ >
30
+ <circle cx="80" cy="78" r="44" />
31
+ <path
32
+ d="M59,81 L75,96 L103,60"
33
+ style={{ stroke: illustrationAccent("var(--success-text)") }}
34
+ strokeWidth={3}
35
+ />
36
+ </svg>
37
+ );
38
+ },
39
+ );
40
+ SuccessIllustration.displayName = "SuccessIllustration";
package/src/index.ts CHANGED
@@ -9,7 +9,8 @@
9
9
  * components require their own peer libs (declared in package.json), e.g.
10
10
  * Drawer (vaul), Command (cmdk), Toast (sonner), Calendar (react-day-picker),
11
11
  * Carousel (embla-carousel-react), Resizable (react-resizable-panels),
12
- * Form (react-hook-form + zod), InputOTP (input-otp).
12
+ * InputOTP (input-otp). Form (react-hook-form + zod) is NOT on this barrel —
13
+ * it lives on the "@elabs-ai/components-ui/form" subpath (issue #26).
13
14
  */
14
15
 
15
16
  // Utilities
@@ -20,6 +21,11 @@ export { useIsMobile } from "./lib/use-mobile";
20
21
  // Browser file-download mechanics — one home for the Blob/URL → <a download> dance
21
22
  // (shared by ConversationDownload, downloadCsv, and Gallery). SSR-guarded.
22
23
  export { downloadBlob, downloadUrl } from "./lib/download";
24
+ // Shared CSV (RFC 4180) value stringification + injection-guarded field
25
+ // quoting — one home for logic `@elabs-ai/components-data`'s `toCsv` and
26
+ // `@elabs-ai/components-charts`'s ChartFrame serializer both need (charts may
27
+ // not depend on data, so both build on this instead).
28
+ export { csvStringifyValue, csvQuoteField } from "./lib/csv";
23
29
  // Clipboard write + transient "copied" flag — one implementation for the
24
30
  // editor's copy button and `CopyableValue`, so the timing and the
25
31
  // "no clipboard in this context" answer are defined once.
@@ -64,6 +70,63 @@ export {
64
70
  type StreamdownTranslationKey,
65
71
  type StreamdownTranslationMap,
66
72
  } from "./lib/streamdown-translations";
73
+ // A verification verdict (linter/type-check/test run/policy hook) — shared
74
+ // vocabulary between `ChangeReview` (this package) and `ai`'s `AgentEvent`
75
+ // (docs/decisions/2026-09-01-brainless-adoption-architecture.md § 6). `ai`
76
+ // imports this rather than declaring its own.
77
+ export { type CheckResult, type CheckSummary } from "./lib/check-result";
78
+ // One elapsed-time formatter for every "how long did this take" surface —
79
+ // `TurnStatus` (ai), `ChangeReview`'s check rows, `AgentEvent`'s hook
80
+ // durations (§ 6 / § 6.1 of the doc above).
81
+ export { formatElapsed } from "./lib/format-duration";
82
+ // Optional-peer detection, promoted from `@elabs-ai/components-ai` (§ 2 of the
83
+ // doc above) — `ui` is upstream of every layer-2 package, so this is the one
84
+ // legal home for a helper shared by DAG siblings.
85
+ export { isModuleNotFoundMessage, isOptionalPeerMissing } from "./lib/optional-peer";
86
+ // The generic trigger-driven-query algorithm behind `MentionInput`'s popup,
87
+ // generalised (§ 5 of the doc above) so a second trigger surface (a
88
+ // slash-command palette) reuses it instead of re-implementing it.
89
+ export {
90
+ findTriggerQuery,
91
+ replaceTriggerRun,
92
+ type FindTriggerQueryOptions,
93
+ type TriggerBoundary,
94
+ type TriggerQuery,
95
+ } from "./lib/trigger-query";
96
+ // The T0 promotion wave (issue #117 — see
97
+ // docs/decisions/2026-09-01-brainless-adoption-architecture.md § 4/§ 10(b)):
98
+ // headless models/helpers behind several `@elabs-ai/components-ai` composer
99
+ // and transcript surfaces, promoted here (never duplicated) because
100
+ // `@elabs-ai/components-ai` and `@elabs-ai/components-terminal` are layer-2
101
+ // DAG siblings that may not import each other — `ui` is upstream of both.
102
+ export { defaultSlashCommandFilter, stepIndex, type SlashCommand } from "./lib/slash-command";
103
+ export {
104
+ APPROVAL_SCOPE_DESCRIPTION_KEYS,
105
+ type ApprovalOption,
106
+ type ApprovalScope,
107
+ } from "./lib/approval-option";
108
+ export {
109
+ diffLineAccessibleLabel,
110
+ diffLineMarker,
111
+ type DiffLine,
112
+ type DiffLineType,
113
+ } from "./lib/diff-line";
114
+ export { collapseDiffRows, useDiffRows, type DiffRow } from "./lib/diff-rows";
115
+ export { effortRungForIndex, type EffortLevel, type OperatingMode } from "./lib/operating-mode";
116
+ export {
117
+ agentEventOutcomeStatus,
118
+ type AgentEventOutcome,
119
+ type AgentEventPhase,
120
+ } from "./lib/agent-event-model";
121
+ export {
122
+ type SessionCapability,
123
+ type SessionQuickAction,
124
+ type SessionWhatsNewItem,
125
+ } from "./lib/session-launch";
126
+ // Empty/error/success/etc. state illustrations for StatePanel's `illustration`
127
+ // slot (#24) — token-driven line art, kept alongside the utilities above
128
+ // because it lives outside the folder-per-component `./components/` tree.
129
+ export * from "./illustrations";
67
130
 
68
131
  // Components
69
132
  export * from "./components/accordion";
@@ -91,8 +154,10 @@ export * from "./components/collapsible-panel";
91
154
  export * from "./components/color-picker";
92
155
  export * from "./components/combobox";
93
156
  export * from "./components/command";
157
+ export * from "./components/command-trigger";
94
158
  export * from "./components/confirm-dialog";
95
159
  export * from "./components/context-menu";
160
+ export * from "./components/context-rail";
96
161
  export * from "./components/copyable-value";
97
162
  export * from "./components/date-picker";
98
163
  export * from "./components/date-range-picker";
@@ -103,15 +168,24 @@ export * from "./components/dropdown-menu";
103
168
  export * from "./components/empty-state";
104
169
  export * from "./components/error-state";
105
170
  export * from "./components/expand-dialog";
171
+ export * from "./components/field";
106
172
  export * from "./components/field-row";
107
173
  export * from "./components/file-upload";
108
- export * from "./components/form";
174
+ // `Form`/`FormField`/… are NOT re-exported from the main barrel — they are the
175
+ // ONLY module in this package that imports `react-hook-form`/`@hookform/resolvers`
176
+ // (peer, optional per issue #26). Re-exporting them here would pull that static
177
+ // import into every consumer's bundled `dist/index.js` (bundlers must resolve a
178
+ // top-level ESM import to build the module graph, even if the binding is never
179
+ // used), forcing react-hook-form on every consumer regardless of whether they
180
+ // use Form. Import the RHF-bound family from the dedicated subpath instead:
181
+ // `import { Form, FormField, … } from "@elabs-ai/components-ui/form"`.
109
182
  export * from "./components/hover-card";
110
183
  export * from "./components/icon-button";
111
184
  export * from "./components/input";
112
185
  export * from "./components/input-group";
113
186
  export * from "./components/input-otp";
114
187
  export * from "./components/kbd";
188
+ export * from "./components/keyboard-shortcuts";
115
189
  export * from "./components/key-value-editor";
116
190
  export * from "./components/label";
117
191
  export * from "./components/link-preview";
@@ -136,14 +210,17 @@ export * from "./components/radio-group";
136
210
  export * from "./components/rating";
137
211
  export * from "./components/resizable";
138
212
  export * from "./components/reveal";
213
+ export * from "./components/schema-form";
139
214
  export * from "./components/scroll-area";
140
215
  export * from "./components/section-header";
141
216
  export * from "./components/segmented-field";
142
217
  export * from "./components/select";
143
218
  export * from "./components/separator";
144
219
  export * from "./components/sheet";
220
+ export * from "./components/side-dock";
145
221
  export * from "./components/sidebar";
146
222
  export * from "./components/skeleton";
223
+ export * from "./components/skip-link";
147
224
  export * from "./components/slider";
148
225
  export * from "./components/slider-number";
149
226
  export * from "./components/sonner";
@@ -171,4 +248,5 @@ export * from "./components/tree-select";
171
248
  export * from "./components/typography";
172
249
  export * from "./components/view-toolbar";
173
250
  export * from "./components/virtual-select";
251
+ export * from "./components/workspace-picker";
174
252
  export * from "./components/wizard";
@@ -0,0 +1,12 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { agentEventOutcomeStatus, type AgentEventOutcome } from "./agent-event-model";
3
+
4
+ describe("agentEventOutcomeStatus", () => {
5
+ it.each([
6
+ ["ok", "complete"],
7
+ ["blocked", "denied"],
8
+ ["failed", "failed"],
9
+ ] as [AgentEventOutcome, string][])("maps %s to the %s status", (outcome, expected) => {
10
+ expect(agentEventOutcomeStatus(outcome)).toBe(expected);
11
+ });
12
+ });
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The lifecycle/hook-event model behind `AgentEvent` (`@elabs-ai/components-ai`,
3
+ * #109) and the terminal CLI look-alike family's own event line (issue #117).
4
+ *
5
+ * Promoted here (not duplicated) because `@elabs-ai/components-ai` and
6
+ * `@elabs-ai/components-terminal` are layer-2 DAG SIBLINGS and may not import
7
+ * each other — `ui` is upstream of both
8
+ * (docs/decisions/2026-09-01-brainless-adoption-architecture.md § 4).
9
+ * `agentEventOutcomeStatus` maps onto the existing closed 7-state `Status`
10
+ * (`components/status-badge/status-badge.tsx`), which already lives in
11
+ * `ui` — no eighth status is introduced.
12
+ */
13
+ import type { Status } from "../components/status-badge/status-badge";
14
+
15
+ /**
16
+ * When an event fired relative to the action it gates. A separate, WIDER
17
+ * vocabulary than `CheckResult.phase` (`"before" | "after"`, per-check): this
18
+ * one also admits `"lifecycle"` for events with no gated action (a turn's
19
+ * `user_prompt_submit`/`stop`). The two `phase` concepts answer different
20
+ * questions — "when did this event fire" vs "when did this check run" — and
21
+ * are never unified (§ 6.1 of the architecture decision above).
22
+ */
23
+ export type AgentEventPhase = "before" | "after" | "lifecycle";
24
+
25
+ /** An event's verdict. Maps onto the existing closed `Status` — see `agentEventOutcomeStatus`. */
26
+ export type AgentEventOutcome = "ok" | "blocked" | "failed";
27
+
28
+ /**
29
+ * `outcome` → the canonical 7-state `Status`. No new status value is added
30
+ * (issue #109 acceptance criteria); `blocked` maps to `denied` — a runtime
31
+ * hook refusing an action is the same shape as a human denying one.
32
+ */
33
+ export function agentEventOutcomeStatus(outcome: AgentEventOutcome): Status {
34
+ switch (outcome) {
35
+ case "ok":
36
+ return "complete";
37
+ case "blocked":
38
+ return "denied";
39
+ case "failed":
40
+ return "failed";
41
+ }
42
+ }
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { APPROVAL_SCOPE_DESCRIPTION_KEYS, type ApprovalScope } from "./approval-option";
3
+
4
+ describe("APPROVAL_SCOPE_DESCRIPTION_KEYS", () => {
5
+ it("has one message key per scope", () => {
6
+ const scopes: ApprovalScope[] = ["once", "session", "always", "deny"];
7
+ for (const scope of scopes) {
8
+ expect(typeof APPROVAL_SCOPE_DESCRIPTION_KEYS[scope]).toBe("string");
9
+ expect(APPROVAL_SCOPE_DESCRIPTION_KEYS[scope].length).toBeGreaterThan(0);
10
+ }
11
+ });
12
+
13
+ it("keys are distinct per scope", () => {
14
+ const values = Object.values(APPROVAL_SCOPE_DESCRIPTION_KEYS);
15
+ expect(new Set(values).size).toBe(values.length);
16
+ });
17
+ });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The N-option, scoped-approval model behind `ApprovalCardOptions`
3
+ * (`@elabs-ai/components-ai`'s `confirmation.tsx`, #103) and the terminal CLI
4
+ * look-alike family's own permission row (issue #117) — a real coding-agent
5
+ * permission prompt is rarely a plain yes/no; it asks an N-option question
6
+ * whose options encode SCOPE ("Yes", "Yes, and don't ask again this
7
+ * session", "No, and tell the agent what to do instead").
8
+ *
9
+ * Promoted here (not duplicated) because `@elabs-ai/components-ai` and
10
+ * `@elabs-ai/components-terminal` are layer-2 DAG SIBLINGS and may not import
11
+ * each other — `ui` is upstream of both
12
+ * (docs/decisions/2026-09-01-brainless-adoption-architecture.md § 4). The
13
+ * SYMBOLS have zero SDK coupling; only their previous host module
14
+ * (`confirmation.tsx`) imports `ai`'s `ToolUIPart`, so only the vocabulary —
15
+ * never the component — moves.
16
+ */
17
+
18
+ /**
19
+ * How far a chosen option's permission reaches. Deliberately a closed,
20
+ * app-agnostic vocabulary (not a free string) so a consumer can derive an
21
+ * accessible fallback description per option (see
22
+ * `APPROVAL_SCOPE_DESCRIPTION_KEYS` below) — the one thing every option of a
23
+ * given scope always means, regardless of the agent-specific label wording.
24
+ */
25
+ export type ApprovalScope = "once" | "session" | "always" | "deny";
26
+
27
+ export interface ApprovalOption {
28
+ id: string;
29
+ label: string;
30
+ /**
31
+ * What choosing this option actually does. Optional: when omitted, the
32
+ * consumer renders a scope-derived sentence instead, so scope always
33
+ * reaches assistive tech through real text — never only through colour or
34
+ * a `data-*` attribute (WCAG 1.4.1).
35
+ */
36
+ description?: string;
37
+ scope: ApprovalScope;
38
+ /** Optional key hint shown beside the option, e.g. "1". */
39
+ keyHint?: string;
40
+ }
41
+
42
+ /**
43
+ * The locale-message KEY (not the translated text — `ui`'s locale catalogue,
44
+ * `components/locale-provider/messages.ts`, holds the English fallback and
45
+ * every other locale's translation) for the scope-derived sentence used only
46
+ * when `option.description` is omitted.
47
+ */
48
+ export const APPROVAL_SCOPE_DESCRIPTION_KEYS: Record<ApprovalScope, string> = {
49
+ once: "ai.approvalCard.scopeOnceDescription",
50
+ session: "ai.approvalCard.scopeSessionDescription",
51
+ always: "ai.approvalCard.scopeAlwaysDescription",
52
+ deny: "ai.approvalCard.scopeDenyDescription",
53
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * One verification verdict about an action — a linter, a type check, a test run,
3
+ * a policy hook. Shared vocabulary across packages: `ChangeReview` (#112) shows the
4
+ * checks a proposed edit already passed, `AgentEvent` (#109) shows the checks a
5
+ * runtime hook ran around a tool call. One runtime concept, one shape.
6
+ *
7
+ * Presentational only (D5): this type records a verdict someone else computed.
8
+ * Nothing in brand-ui ever runs a check.
9
+ *
10
+ * Rendering contract, binding on every consumer: `ok` must reach the user through
11
+ * an ICON and ACCESSIBLE TEXT, never through colour alone — the greyscale test in
12
+ * `.claude/rules/accessibility.md`.
13
+ */
14
+ export interface CheckResult {
15
+ /** What ran, verbatim as the runtime names it: "eslint", "tsc", "pre_tool_use". */
16
+ label: string;
17
+ /** Did it pass? The verdict; the only required field besides `label`. */
18
+ ok: boolean;
19
+ /** One line of explanation. Renders as a secondary line, collapsed when long. */
20
+ detail?: string;
21
+ /** Wall-clock duration in milliseconds. Format with `formatElapsed`. */
22
+ durationMs?: number;
23
+ /** When it ran relative to the action it gates. Groups results into sections. */
24
+ phase?: "before" | "after";
25
+ }
26
+
27
+ /** A count summary, when the individual verdicts are not available. */
28
+ export interface CheckSummary {
29
+ /** How many checks ran. */
30
+ ran: number;
31
+ /** How many of them passed. `passed <= ran`. */
32
+ passed: number;
33
+ }
package/src/lib/cn.ts CHANGED
@@ -17,6 +17,15 @@ const twMerge = extendTailwindMerge({
17
17
  "font-size": [{ text: TEXT_ROLES }],
18
18
  // `font-display` is the display-face seam (sibling of font-mono, #187).
19
19
  "font-family": [{ font: ["display"] }],
20
+ // Theme-driven control/table knobs (themes.css § CONTROL UTILITIES and the
21
+ // `@theme inline` bridge) — so a caller's `h-8` / `rounded-none` /
22
+ // `font-bold` / `shadow-none` still replaces the token default.
23
+ "font-weight": [{ font: ["control", "table-header"] }],
24
+ rounded: [{ rounded: ["control"] }],
25
+ shadow: [{ shadow: ["input"] }],
26
+ h: [{ h: ["control", "control-sm", "control-lg", "header"] }],
27
+ size: [{ size: ["control", "control-sm", "control-lg"] }],
28
+ "min-w": [{ "min-w": ["control", "control-sm", "control-lg"] }],
20
29
  },
21
30
  },
22
31
  });
package/src/lib/csv.ts ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Shared CSV (RFC 4180) primitives — value stringification + injection-guarded
3
+ * field quoting. Pure, no DOM, SSR-safe.
4
+ *
5
+ * The single home for logic previously duplicated between `@elabs-ai/components-data`'s
6
+ * `toCsv` and `@elabs-ai/components-charts`'s `ChartFrame` local serializer (charts may not
7
+ * depend on data per the one-way package rule, so both import this instead of
8
+ * one importing the other).
9
+ */
10
+
11
+ /** Leading characters a spreadsheet may interpret as a formula trigger. */
12
+ const INJECTION_TRIGGER = /^[=+\-@]/;
13
+
14
+ /**
15
+ * True when `field` parses as a plain finite number, e.g. "-5" or "+12.3".
16
+ * A negative/positive number is not a formula — only guard fields that
17
+ * START with a trigger character AND are not actually numeric.
18
+ */
19
+ function isNumericLiteral(field: string): boolean {
20
+ const trimmed = field.trim();
21
+ if (trimmed === "") return false;
22
+ return Number.isFinite(Number(trimmed));
23
+ }
24
+
25
+ /** Stringify a cell value for CSV: dates → ISO, objects → JSON, else `String()`. */
26
+ export function csvStringifyValue(value: unknown): string {
27
+ if (value === null || value === undefined) return "";
28
+ if (value instanceof Date) return value.toISOString();
29
+ if (typeof value === "object") return JSON.stringify(value);
30
+ return String(value);
31
+ }
32
+
33
+ /**
34
+ * Quote/escape one CSV field: prefixes a leading `'` when the field starts
35
+ * with a formula-trigger character and is not a plain number (CSV-injection
36
+ * guard), then RFC-4180-quotes when it contains the delimiter, a double
37
+ * quote, or a line break.
38
+ */
39
+ export function csvQuoteField(field: string, delimiter: string): string {
40
+ let out = field;
41
+ if (INJECTION_TRIGGER.test(out) && !isNumericLiteral(out)) {
42
+ out = "'" + out;
43
+ }
44
+ if (out.includes(delimiter) || out.includes('"') || out.includes("\n") || out.includes("\r")) {
45
+ return '"' + out.replaceAll('"', '""') + '"';
46
+ }
47
+ return out;
48
+ }
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { diffLineAccessibleLabel, diffLineMarker, type DiffLineType } from "./diff-line";
3
+
4
+ describe("diffLineMarker", () => {
5
+ it.each([
6
+ ["add", "+"],
7
+ ["del", "−"],
8
+ ["context", " "],
9
+ ["hunk", ""],
10
+ ["meta", ""],
11
+ ] as const)("returns %s's marker as %s", (type, expected) => {
12
+ expect(diffLineMarker(type)).toBe(expected);
13
+ });
14
+
15
+ it("uses a NO-BREAK SPACE (U+00A0) for context, not a plain space — must survive JSX/HTML whitespace collapsing", () => {
16
+ expect(diffLineMarker("context")).toBe(" ");
17
+ expect(diffLineMarker("context").charCodeAt(0)).toBe(0xa0);
18
+ });
19
+ });
20
+
21
+ describe("diffLineAccessibleLabel", () => {
22
+ it("returns the added-line key for an add row", () => {
23
+ expect(diffLineAccessibleLabel("add")).toBe("ai.diffView.addedLine");
24
+ });
25
+
26
+ it("returns the removed-line key for a del row", () => {
27
+ expect(diffLineAccessibleLabel("del")).toBe("ai.diffView.removedLine");
28
+ });
29
+
30
+ it.each(["context", "hunk", "meta"] as DiffLineType[])(
31
+ "returns undefined for %s (no polarity)",
32
+ (type) => {
33
+ expect(diffLineAccessibleLabel(type)).toBeUndefined();
34
+ },
35
+ );
36
+ });
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The unified-diff line model behind `DiffView` (`@elabs-ai/components-ai`,
3
+ * #102) and the terminal CLI look-alike family's own diff hunk (issue #117).
4
+ *
5
+ * Promoted here (not duplicated) because `@elabs-ai/components-ai` and
6
+ * `@elabs-ai/components-terminal` are layer-2 DAG SIBLINGS and may not import
7
+ * each other — `ui` is upstream of both
8
+ * (docs/decisions/2026-09-01-brainless-adoption-architecture.md § 4). This
9
+ * module is the shiki-free half: the line shape, the marker glyph and the
10
+ * screen-reader polarity label. Intra-line syntax highlighting (Shiki) and
11
+ * the diff's presentational chrome stay in `@elabs-ai/components-ai`'s
12
+ * `diff-view.tsx` — `ui` does not depend on Shiki and never will for one
13
+ * component (see the decision doc § 3).
14
+ *
15
+ * The `DiffLine` model and the polarity-prefix technique are adapted from
16
+ * `theswerd/brainless` (MIT) — see `ATTRIBUTION.md`.
17
+ */
18
+
19
+ /** What role a line plays in the diff. */
20
+ export type DiffLineType = "add" | "del" | "context" | "hunk" | "meta";
21
+
22
+ /** One line of a unified diff. Absent gutter numbers are load-bearing (see below). */
23
+ export interface DiffLine {
24
+ type: DiffLineType;
25
+ /** Line number in the original file. Absent on `add` — there is no old line. */
26
+ oldNumber?: number;
27
+ /** Line number in the new file. Absent on `del` — there is no new line. */
28
+ newNumber?: number;
29
+ /** The line's text, without its leading `+` / `-` / ` ` marker. */
30
+ text: string;
31
+ }
32
+
33
+ /**
34
+ * The single-character marker glyph for a real source line (`add`/`del`/
35
+ * `context`). `hunk`/`meta` lines are diff headers, not source, and render
36
+ * through a full-width row instead — this returns `""` for them.
37
+ */
38
+ export function diffLineMarker(type: DiffLineType): string {
39
+ switch (type) {
40
+ case "add":
41
+ return "+";
42
+ case "del":
43
+ return "−";
44
+ case "context":
45
+ return " ";
46
+ default:
47
+ return "";
48
+ }
49
+ }
50
+
51
+ /**
52
+ * The `ui` locale-message KEY (not the translated text) for the `sr-only`
53
+ * screen-reader polarity word an `add`/`del` row carries ALONGSIDE its marker
54
+ * glyph and its fill tint — colour is never the only channel (WCAG 1.4.1).
55
+ * `undefined` for `context`/`hunk`/`meta`, which carry no polarity.
56
+ */
57
+ export function diffLineAccessibleLabel(
58
+ type: DiffLineType,
59
+ ): "ai.diffView.addedLine" | "ai.diffView.removedLine" | undefined {
60
+ if (type === "add") return "ai.diffView.addedLine";
61
+ if (type === "del") return "ai.diffView.removedLine";
62
+ return undefined;
63
+ }