@elabs-ai/components-ui 4.0.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 (449) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +78 -0
  3. package/dist/index.d.ts +4497 -0
  4. package/dist/index.js +15192 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/lib/cn.d.ts +10 -0
  7. package/dist/lib/cn.js +20 -0
  8. package/dist/lib/cn.js.map +1 -0
  9. package/package.json +106 -0
  10. package/src/blocks/sidebar-02/app-sidebar.tsx +160 -0
  11. package/src/blocks/sidebar-02/logo.tsx +18 -0
  12. package/src/blocks/sidebar-02/nav-main.tsx +13 -0
  13. package/src/blocks/sidebar-02/nav-notifications.tsx +7 -0
  14. package/src/blocks/sidebar-02/sidebar-02.stories.tsx +23 -0
  15. package/src/blocks/sidebar-02/team-switcher.tsx +7 -0
  16. package/src/blocks/sidebar-04/app-sidebar.tsx +236 -0
  17. package/src/blocks/sidebar-04/mail-context.tsx +29 -0
  18. package/src/blocks/sidebar-04/nav-user.tsx +6 -0
  19. package/src/blocks/sidebar-04/sidebar-04.stories.tsx +150 -0
  20. package/src/blocks/sidebar-05/app-sidebar.tsx +375 -0
  21. package/src/blocks/sidebar-05/sidebar-05.stories.tsx +18 -0
  22. package/src/blocks/sidebar-05/team-switcher.tsx +6 -0
  23. package/src/blocks-comparison-table.stories.tsx +165 -0
  24. package/src/components/accordion/accordion.stories.tsx +71 -0
  25. package/src/components/accordion/accordion.test.tsx +18 -0
  26. package/src/components/accordion/accordion.tsx +49 -0
  27. package/src/components/accordion/index.ts +1 -0
  28. package/src/components/advanced-group/advanced-group.stories.tsx +143 -0
  29. package/src/components/advanced-group/advanced-group.test.tsx +81 -0
  30. package/src/components/advanced-group/advanced-group.tsx +111 -0
  31. package/src/components/advanced-group/index.ts +2 -0
  32. package/src/components/alert/alert.stories.tsx +156 -0
  33. package/src/components/alert/alert.test.tsx +57 -0
  34. package/src/components/alert/alert.tsx +91 -0
  35. package/src/components/alert/index.ts +8 -0
  36. package/src/components/alert-dialog/alert-dialog.stories.tsx +73 -0
  37. package/src/components/alert-dialog/alert-dialog.tsx +104 -0
  38. package/src/components/alert-dialog/index.ts +12 -0
  39. package/src/components/app-shell/app-shell.stories.tsx +61 -0
  40. package/src/components/app-shell/app-shell.tsx +31 -0
  41. package/src/components/app-shell/index.ts +1 -0
  42. package/src/components/app-sidebar/app-sidebar.stories.tsx +121 -0
  43. package/src/components/app-sidebar/app-sidebar.test.tsx +55 -0
  44. package/src/components/app-sidebar/app-sidebar.tsx +53 -0
  45. package/src/components/app-sidebar/index.ts +1 -0
  46. package/src/components/aspect-ratio/aspect-ratio.stories.tsx +46 -0
  47. package/src/components/aspect-ratio/aspect-ratio.tsx +3 -0
  48. package/src/components/aspect-ratio/index.ts +1 -0
  49. package/src/components/attribution-panel/attribution-panel.stories.tsx +83 -0
  50. package/src/components/attribution-panel/attribution-panel.test.tsx +123 -0
  51. package/src/components/attribution-panel/attribution-panel.tsx +191 -0
  52. package/src/components/attribution-panel/attribution-types.ts +44 -0
  53. package/src/components/attribution-panel/attributions.generated.ts +1422 -0
  54. package/src/components/attribution-panel/index.ts +3 -0
  55. package/src/components/avatar/avatar.stories.tsx +45 -0
  56. package/src/components/avatar/avatar.test.tsx +13 -0
  57. package/src/components/avatar/avatar.tsx +46 -0
  58. package/src/components/avatar/index.ts +1 -0
  59. package/src/components/badge/badge.stories.tsx +44 -0
  60. package/src/components/badge/badge.test.tsx +10 -0
  61. package/src/components/badge/badge.tsx +28 -0
  62. package/src/components/badge/index.ts +1 -0
  63. package/src/components/bento-grid/bento-grid.stories.tsx +165 -0
  64. package/src/components/bento-grid/bento-grid.test.tsx +119 -0
  65. package/src/components/bento-grid/bento-grid.tsx +339 -0
  66. package/src/components/bento-grid/index.ts +9 -0
  67. package/src/components/bounded-number/bounded-number.stories.tsx +118 -0
  68. package/src/components/bounded-number/bounded-number.test.tsx +73 -0
  69. package/src/components/bounded-number/bounded-number.tsx +110 -0
  70. package/src/components/bounded-number/index.ts +1 -0
  71. package/src/components/breadcrumb/breadcrumb.stories.tsx +47 -0
  72. package/src/components/breadcrumb/breadcrumb.test.tsx +17 -0
  73. package/src/components/breadcrumb/breadcrumb.tsx +86 -0
  74. package/src/components/breadcrumb/index.ts +9 -0
  75. package/src/components/button/button.stories.tsx +70 -0
  76. package/src/components/button/button.test.tsx +50 -0
  77. package/src/components/button/button.tsx +63 -0
  78. package/src/components/button/index.ts +1 -0
  79. package/src/components/button-group/button-group.stories.tsx +45 -0
  80. package/src/components/button-group/button-group.tsx +70 -0
  81. package/src/components/button-group/index.ts +6 -0
  82. package/src/components/calendar/calendar.stories.tsx +95 -0
  83. package/src/components/calendar/calendar.test.tsx +140 -0
  84. package/src/components/calendar/calendar.tsx +70 -0
  85. package/src/components/calendar/index.ts +1 -0
  86. package/src/components/card/card.stories.tsx +438 -0
  87. package/src/components/card/card.test.tsx +153 -0
  88. package/src/components/card/card.tsx +332 -0
  89. package/src/components/card/index.ts +13 -0
  90. package/src/components/carousel/carousel.stories.tsx +102 -0
  91. package/src/components/carousel/carousel.tsx +225 -0
  92. package/src/components/carousel/index.ts +8 -0
  93. package/src/components/change-review/change-review.stories.tsx +288 -0
  94. package/src/components/change-review/change-review.test.tsx +261 -0
  95. package/src/components/change-review/change-review.tsx +637 -0
  96. package/src/components/change-review/index.ts +23 -0
  97. package/src/components/checkbox/checkbox.stories.tsx +119 -0
  98. package/src/components/checkbox/checkbox.test.tsx +40 -0
  99. package/src/components/checkbox/checkbox.tsx +34 -0
  100. package/src/components/checkbox/index.ts +1 -0
  101. package/src/components/collapsible/collapsible.stories.tsx +62 -0
  102. package/src/components/collapsible/collapsible.tsx +22 -0
  103. package/src/components/collapsible/index.ts +1 -0
  104. package/src/components/collapsible-panel/index.ts +8 -0
  105. package/src/components/collapsible-panel/use-collapsible-panel.test.tsx +93 -0
  106. package/src/components/collapsible-panel/use-collapsible-panel.ts +159 -0
  107. package/src/components/color-picker/color-picker.stories.tsx +93 -0
  108. package/src/components/color-picker/color-picker.test.tsx +79 -0
  109. package/src/components/color-picker/color-picker.tsx +341 -0
  110. package/src/components/color-picker/index.ts +1 -0
  111. package/src/components/combobox/combobox.stories.tsx +154 -0
  112. package/src/components/combobox/combobox.test.tsx +246 -0
  113. package/src/components/combobox/combobox.tsx +210 -0
  114. package/src/components/combobox/index.ts +1 -0
  115. package/src/components/command/command.stories.tsx +371 -0
  116. package/src/components/command/command.test.tsx +280 -0
  117. package/src/components/command/command.tsx +301 -0
  118. package/src/components/command/index.ts +13 -0
  119. package/src/components/confirm-dialog/confirm-dialog.stories.tsx +183 -0
  120. package/src/components/confirm-dialog/confirm-dialog.test.tsx +147 -0
  121. package/src/components/confirm-dialog/confirm-dialog.tsx +146 -0
  122. package/src/components/confirm-dialog/index.ts +2 -0
  123. package/src/components/context-menu/context-menu.stories.tsx +70 -0
  124. package/src/components/context-menu/context-menu.tsx +172 -0
  125. package/src/components/context-menu/index.ts +16 -0
  126. package/src/components/copyable-value/copyable-value.stories.tsx +80 -0
  127. package/src/components/copyable-value/copyable-value.test.tsx +128 -0
  128. package/src/components/copyable-value/copyable-value.tsx +101 -0
  129. package/src/components/copyable-value/index.ts +1 -0
  130. package/src/components/date-picker/date-picker.stories.tsx +42 -0
  131. package/src/components/date-picker/date-picker.tsx +49 -0
  132. package/src/components/date-picker/index.ts +1 -0
  133. package/src/components/date-range-picker/date-range-picker.stories.tsx +151 -0
  134. package/src/components/date-range-picker/date-range-picker.test.tsx +62 -0
  135. package/src/components/date-range-picker/date-range-picker.tsx +126 -0
  136. package/src/components/date-range-picker/index.ts +6 -0
  137. package/src/components/descriptions/descriptions.stories.tsx +43 -0
  138. package/src/components/descriptions/descriptions.test.tsx +41 -0
  139. package/src/components/descriptions/descriptions.tsx +95 -0
  140. package/src/components/descriptions/index.ts +6 -0
  141. package/src/components/dialog/dialog.stories.tsx +728 -0
  142. package/src/components/dialog/dialog.test.tsx +285 -0
  143. package/src/components/dialog/dialog.tsx +340 -0
  144. package/src/components/dialog/index.ts +18 -0
  145. package/src/components/dialog/use-dialog-dismiss-guard.test.tsx +80 -0
  146. package/src/components/dialog/use-dialog-dismiss-guard.ts +109 -0
  147. package/src/components/drawer/drawer.stories.tsx +76 -0
  148. package/src/components/drawer/drawer.tsx +85 -0
  149. package/src/components/drawer/index.ts +11 -0
  150. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +176 -0
  151. package/src/components/dropdown-menu/dropdown-menu.test.tsx +109 -0
  152. package/src/components/dropdown-menu/dropdown-menu.tsx +179 -0
  153. package/src/components/dropdown-menu/index.ts +17 -0
  154. package/src/components/empty-state/empty-state.stories.tsx +54 -0
  155. package/src/components/empty-state/empty-state.test.tsx +11 -0
  156. package/src/components/empty-state/empty-state.tsx +30 -0
  157. package/src/components/empty-state/index.ts +1 -0
  158. package/src/components/error-state/error-state.stories.tsx +51 -0
  159. package/src/components/error-state/error-state.tsx +27 -0
  160. package/src/components/error-state/index.ts +1 -0
  161. package/src/components/expand-dialog/expand-dialog.stories.tsx +246 -0
  162. package/src/components/expand-dialog/expand-dialog.test.tsx +185 -0
  163. package/src/components/expand-dialog/expand-dialog.tsx +387 -0
  164. package/src/components/expand-dialog/index.ts +11 -0
  165. package/src/components/field-row/field-row.stories.tsx +114 -0
  166. package/src/components/field-row/field-row.test.tsx +188 -0
  167. package/src/components/field-row/field-row.tsx +115 -0
  168. package/src/components/field-row/index.ts +1 -0
  169. package/src/components/file-upload/file-upload.stories.tsx +183 -0
  170. package/src/components/file-upload/file-upload.test.tsx +169 -0
  171. package/src/components/file-upload/file-upload.tsx +459 -0
  172. package/src/components/file-upload/index.ts +13 -0
  173. package/src/components/form/form.stories.tsx +71 -0
  174. package/src/components/form/form.tsx +141 -0
  175. package/src/components/form/index.ts +10 -0
  176. package/src/components/hover-card/hover-card.stories.tsx +57 -0
  177. package/src/components/hover-card/hover-card.tsx +25 -0
  178. package/src/components/hover-card/index.ts +1 -0
  179. package/src/components/icon-button/icon-button.stories.tsx +108 -0
  180. package/src/components/icon-button/icon-button.test.tsx +93 -0
  181. package/src/components/icon-button/icon-button.tsx +139 -0
  182. package/src/components/icon-button/index.ts +1 -0
  183. package/src/components/input/index.ts +1 -0
  184. package/src/components/input/input.stories.tsx +44 -0
  185. package/src/components/input/input.test.tsx +19 -0
  186. package/src/components/input/input.tsx +26 -0
  187. package/src/components/input-group/index.ts +11 -0
  188. package/src/components/input-group/input-group.stories.tsx +87 -0
  189. package/src/components/input-group/input-group.test.tsx +48 -0
  190. package/src/components/input-group/input-group.tsx +170 -0
  191. package/src/components/input-otp/index.ts +1 -0
  192. package/src/components/input-otp/input-otp.stories.tsx +66 -0
  193. package/src/components/input-otp/input-otp.tsx +68 -0
  194. package/src/components/kbd/index.ts +1 -0
  195. package/src/components/kbd/kbd.stories.tsx +45 -0
  196. package/src/components/kbd/kbd.tsx +20 -0
  197. package/src/components/key-value-editor/index.ts +1 -0
  198. package/src/components/key-value-editor/key-value-editor.stories.tsx +130 -0
  199. package/src/components/key-value-editor/key-value-editor.test.tsx +231 -0
  200. package/src/components/key-value-editor/key-value-editor.tsx +263 -0
  201. package/src/components/label/index.ts +1 -0
  202. package/src/components/label/label.stories.tsx +35 -0
  203. package/src/components/label/label.tsx +19 -0
  204. package/src/components/link-preview/index.ts +2 -0
  205. package/src/components/link-preview/link-preview.stories.tsx +178 -0
  206. package/src/components/link-preview/link-preview.test.tsx +123 -0
  207. package/src/components/link-preview/link-preview.tsx +194 -0
  208. package/src/components/list-editor/index.ts +1 -0
  209. package/src/components/list-editor/list-editor.stories.tsx +119 -0
  210. package/src/components/list-editor/list-editor.test.tsx +96 -0
  211. package/src/components/list-editor/list-editor.tsx +192 -0
  212. package/src/components/loading-state/index.ts +1 -0
  213. package/src/components/loading-state/loading-state.stories.tsx +38 -0
  214. package/src/components/loading-state/loading-state.tsx +26 -0
  215. package/src/components/locale-provider/index.ts +8 -0
  216. package/src/components/locale-provider/locale-provider.stories.tsx +83 -0
  217. package/src/components/locale-provider/locale-provider.test.tsx +238 -0
  218. package/src/components/locale-provider/locale-provider.tsx +187 -0
  219. package/src/components/locale-provider/messages.ts +300 -0
  220. package/src/components/match-highlight/index.ts +7 -0
  221. package/src/components/match-highlight/match-highlight.stories.tsx +146 -0
  222. package/src/components/match-highlight/match-highlight.test.tsx +202 -0
  223. package/src/components/match-highlight/match-highlight.tsx +145 -0
  224. package/src/components/mention-input/index.ts +28 -0
  225. package/src/components/mention-input/mention-input-mirror.stories.tsx +171 -0
  226. package/src/components/mention-input/mention-input.stories.tsx +463 -0
  227. package/src/components/mention-input/mention-input.test.tsx +617 -0
  228. package/src/components/mention-input/mention-input.tsx +1102 -0
  229. package/src/components/mention-input/mention-value.test.ts +253 -0
  230. package/src/components/mention-input/mention-value.ts +193 -0
  231. package/src/components/menubar/index.ts +17 -0
  232. package/src/components/menubar/menubar.stories.tsx +67 -0
  233. package/src/components/menubar/menubar.tsx +198 -0
  234. package/src/components/metric-card/index.ts +1 -0
  235. package/src/components/metric-card/metric-card.stories.tsx +224 -0
  236. package/src/components/metric-card/metric-card.test.tsx +198 -0
  237. package/src/components/metric-card/metric-card.tsx +229 -0
  238. package/src/components/model-picker/index.ts +11 -0
  239. package/src/components/model-picker/model-picker-state.ts +85 -0
  240. package/src/components/model-picker/model-picker.stories.tsx +128 -0
  241. package/src/components/model-picker/model-picker.test.tsx +241 -0
  242. package/src/components/model-picker/model-picker.tsx +270 -0
  243. package/src/components/nav-main/index.ts +1 -0
  244. package/src/components/nav-main/nav-main.tsx +147 -0
  245. package/src/components/nav-notifications/index.ts +5 -0
  246. package/src/components/nav-notifications/nav-notifications.test.tsx +15 -0
  247. package/src/components/nav-notifications/nav-notifications.tsx +70 -0
  248. package/src/components/nav-user/index.ts +1 -0
  249. package/src/components/nav-user/nav-user.test.tsx +29 -0
  250. package/src/components/nav-user/nav-user.tsx +114 -0
  251. package/src/components/navigation-menu/index.ts +10 -0
  252. package/src/components/navigation-menu/navigation-menu.stories.tsx +92 -0
  253. package/src/components/navigation-menu/navigation-menu.tsx +99 -0
  254. package/src/components/number-input/index.ts +1 -0
  255. package/src/components/number-input/number-input.stories.tsx +120 -0
  256. package/src/components/number-input/number-input.test.tsx +96 -0
  257. package/src/components/number-input/number-input.tsx +219 -0
  258. package/src/components/page-shell/index.ts +1 -0
  259. package/src/components/page-shell/page-shell.stories.tsx +105 -0
  260. package/src/components/page-shell/page-shell.test.tsx +57 -0
  261. package/src/components/page-shell/page-shell.tsx +62 -0
  262. package/src/components/pagination/index.ts +9 -0
  263. package/src/components/pagination/pagination.stories.tsx +57 -0
  264. package/src/components/pagination/pagination.tsx +77 -0
  265. package/src/components/popover/index.ts +1 -0
  266. package/src/components/popover/popover.stories.tsx +28 -0
  267. package/src/components/popover/popover.test.tsx +16 -0
  268. package/src/components/popover/popover.tsx +29 -0
  269. package/src/components/progress/index.ts +1 -0
  270. package/src/components/progress/progress.stories.tsx +80 -0
  271. package/src/components/progress/progress.test.tsx +35 -0
  272. package/src/components/progress/progress.tsx +50 -0
  273. package/src/components/radio-group/index.ts +1 -0
  274. package/src/components/radio-group/radio-group.stories.tsx +75 -0
  275. package/src/components/radio-group/radio-group.test.tsx +16 -0
  276. package/src/components/radio-group/radio-group.tsx +33 -0
  277. package/src/components/rating/index.ts +1 -0
  278. package/src/components/rating/rating.stories.tsx +111 -0
  279. package/src/components/rating/rating.test.tsx +41 -0
  280. package/src/components/rating/rating.tsx +226 -0
  281. package/src/components/resizable/index.ts +1 -0
  282. package/src/components/resizable/resizable.stories.tsx +25 -0
  283. package/src/components/resizable/resizable.tsx +40 -0
  284. package/src/components/reveal/index.ts +7 -0
  285. package/src/components/reveal/reveal.stories.tsx +90 -0
  286. package/src/components/reveal/reveal.test.tsx +43 -0
  287. package/src/components/reveal/reveal.tsx +95 -0
  288. package/src/components/revision-timeline/index.ts +10 -0
  289. package/src/components/revision-timeline/revision-timeline.stories.tsx +785 -0
  290. package/src/components/revision-timeline/revision-timeline.test.tsx +509 -0
  291. package/src/components/revision-timeline/revision-timeline.tsx +1107 -0
  292. package/src/components/scroll-area/index.ts +1 -0
  293. package/src/components/scroll-area/scroll-area.stories.tsx +45 -0
  294. package/src/components/scroll-area/scroll-area.tsx +43 -0
  295. package/src/components/section-header/index.ts +1 -0
  296. package/src/components/section-header/section-header.stories.tsx +20 -0
  297. package/src/components/section-header/section-header.tsx +36 -0
  298. package/src/components/segmented-field/index.ts +5 -0
  299. package/src/components/segmented-field/segmented-field.stories.tsx +121 -0
  300. package/src/components/segmented-field/segmented-field.test.tsx +163 -0
  301. package/src/components/segmented-field/segmented-field.tsx +235 -0
  302. package/src/components/select/index.ts +10 -0
  303. package/src/components/select/select.stories.tsx +106 -0
  304. package/src/components/select/select.test.tsx +98 -0
  305. package/src/components/select/select.tsx +230 -0
  306. package/src/components/separator/index.ts +2 -0
  307. package/src/components/separator/separator.stories.tsx +87 -0
  308. package/src/components/separator/separator.tsx +51 -0
  309. package/src/components/sheet/index.ts +12 -0
  310. package/src/components/sheet/sheet.stories.tsx +65 -0
  311. package/src/components/sheet/sheet.tsx +109 -0
  312. package/src/components/sidebar/index.ts +27 -0
  313. package/src/components/sidebar/sidebar.stories.tsx +69 -0
  314. package/src/components/sidebar/sidebar.test.tsx +43 -0
  315. package/src/components/sidebar/sidebar.tsx +688 -0
  316. package/src/components/skeleton/index.ts +1 -0
  317. package/src/components/skeleton/skeleton.stories.tsx +28 -0
  318. package/src/components/skeleton/skeleton.tsx +15 -0
  319. package/src/components/slider/index.ts +1 -0
  320. package/src/components/slider/slider.stories.tsx +211 -0
  321. package/src/components/slider/slider.test.tsx +168 -0
  322. package/src/components/slider/slider.tsx +136 -0
  323. package/src/components/slider-number/index.ts +1 -0
  324. package/src/components/slider-number/slider-number.stories.tsx +123 -0
  325. package/src/components/slider-number/slider-number.test.tsx +165 -0
  326. package/src/components/slider-number/slider-number.tsx +191 -0
  327. package/src/components/sonner/index.ts +1 -0
  328. package/src/components/sonner/sonner.stories.tsx +124 -0
  329. package/src/components/sonner/sonner.test.tsx +76 -0
  330. package/src/components/sonner/sonner.tsx +101 -0
  331. package/src/components/spinner/index.ts +1 -0
  332. package/src/components/spinner/spinner.stories.tsx +27 -0
  333. package/src/components/spinner/spinner.tsx +19 -0
  334. package/src/components/split-panel/index.ts +1 -0
  335. package/src/components/split-panel/split-panel.stories.tsx +60 -0
  336. package/src/components/split-panel/split-panel.test.tsx +38 -0
  337. package/src/components/split-panel/split-panel.tsx +95 -0
  338. package/src/components/state-panel/index.ts +1 -0
  339. package/src/components/state-panel/state-panel.stories.tsx +80 -0
  340. package/src/components/state-panel/state-panel.test.tsx +64 -0
  341. package/src/components/state-panel/state-panel.tsx +167 -0
  342. package/src/components/status-badge/index.ts +18 -0
  343. package/src/components/status-badge/status-badge.stories.tsx +132 -0
  344. package/src/components/status-badge/status-badge.test.tsx +189 -0
  345. package/src/components/status-badge/status-badge.tsx +330 -0
  346. package/src/components/switch/index.ts +1 -0
  347. package/src/components/switch/switch.stories.tsx +58 -0
  348. package/src/components/switch/switch.test.tsx +12 -0
  349. package/src/components/switch/switch.tsx +29 -0
  350. package/src/components/table/index.ts +10 -0
  351. package/src/components/table/table.stories.tsx +53 -0
  352. package/src/components/table/table.test.tsx +17 -0
  353. package/src/components/table/table.tsx +106 -0
  354. package/src/components/tabs/index.ts +1 -0
  355. package/src/components/tabs/tabs.stories.tsx +267 -0
  356. package/src/components/tabs/tabs.test.tsx +94 -0
  357. package/src/components/tabs/tabs.tsx +170 -0
  358. package/src/components/tag-input/index.ts +1 -0
  359. package/src/components/tag-input/tag-input.stories.tsx +120 -0
  360. package/src/components/tag-input/tag-input.test.tsx +97 -0
  361. package/src/components/tag-input/tag-input.tsx +253 -0
  362. package/src/components/team-switcher/index.ts +1 -0
  363. package/src/components/team-switcher/team-switcher.test.tsx +39 -0
  364. package/src/components/team-switcher/team-switcher.tsx +100 -0
  365. package/src/components/textarea/index.ts +1 -0
  366. package/src/components/textarea/textarea.stories.tsx +42 -0
  367. package/src/components/textarea/textarea.tsx +24 -0
  368. package/src/components/theme-switcher/index.ts +2 -0
  369. package/src/components/theme-switcher/theme-switcher.stories.tsx +146 -0
  370. package/src/components/theme-switcher/theme-switcher.test.tsx +253 -0
  371. package/src/components/theme-switcher/theme-switcher.tsx +280 -0
  372. package/src/components/theme-switcher/use-theme-transition.ts +62 -0
  373. package/src/components/timeline/index.ts +13 -0
  374. package/src/components/timeline/timeline.stories.tsx +99 -0
  375. package/src/components/timeline/timeline.test.tsx +196 -0
  376. package/src/components/timeline/timeline.tsx +188 -0
  377. package/src/components/toggle/index.ts +1 -0
  378. package/src/components/toggle/toggle.stories.tsx +116 -0
  379. package/src/components/toggle/toggle.test.tsx +119 -0
  380. package/src/components/toggle/toggle.tsx +64 -0
  381. package/src/components/toggle-group/index.ts +1 -0
  382. package/src/components/toggle-group/toggle-group.stories.tsx +110 -0
  383. package/src/components/toggle-group/toggle-group.tsx +59 -0
  384. package/src/components/toolbar/index.ts +14 -0
  385. package/src/components/toolbar/toolbar.stories.tsx +126 -0
  386. package/src/components/toolbar/toolbar.test.tsx +73 -0
  387. package/src/components/toolbar/toolbar.tsx +177 -0
  388. package/src/components/tooltip/index.ts +1 -0
  389. package/src/components/tooltip/tooltip.stories.tsx +67 -0
  390. package/src/components/tooltip/tooltip.tsx +27 -0
  391. package/src/components/top-nav/index.ts +1 -0
  392. package/src/components/top-nav/top-nav.stories.tsx +18 -0
  393. package/src/components/top-nav/top-nav.tsx +34 -0
  394. package/src/components/transfer/index.ts +2 -0
  395. package/src/components/transfer/transfer.stories.tsx +162 -0
  396. package/src/components/transfer/transfer.test.tsx +177 -0
  397. package/src/components/transfer/transfer.tsx +481 -0
  398. package/src/components/tree/index.ts +6 -0
  399. package/src/components/tree/tree.stories.tsx +618 -0
  400. package/src/components/tree/tree.test.tsx +699 -0
  401. package/src/components/tree/tree.tsx +1149 -0
  402. package/src/components/tree/use-tree-keyboard.ts +314 -0
  403. package/src/components/tree-select/index.ts +1 -0
  404. package/src/components/tree-select/tree-select.stories.tsx +127 -0
  405. package/src/components/tree-select/tree-select.test.tsx +40 -0
  406. package/src/components/tree-select/tree-select.tsx +137 -0
  407. package/src/components/typography/index.ts +31 -0
  408. package/src/components/typography/prose.test.tsx +48 -0
  409. package/src/components/typography/prose.tsx +194 -0
  410. package/src/components/typography/typography.stories.tsx +244 -0
  411. package/src/components/typography/typography.test.tsx +140 -0
  412. package/src/components/typography/typography.tsx +126 -0
  413. package/src/components/view-toolbar/index.ts +10 -0
  414. package/src/components/view-toolbar/view-toolbar.stories.tsx +342 -0
  415. package/src/components/view-toolbar/view-toolbar.test.tsx +262 -0
  416. package/src/components/view-toolbar/view-toolbar.tsx +320 -0
  417. package/src/components/virtual-select/index.ts +2 -0
  418. package/src/components/virtual-select/use-virtual-listbox.ts +132 -0
  419. package/src/components/virtual-select/virtual-select.stories.tsx +203 -0
  420. package/src/components/virtual-select/virtual-select.test.tsx +106 -0
  421. package/src/components/virtual-select/virtual-select.tsx +361 -0
  422. package/src/components/wizard/index.ts +12 -0
  423. package/src/components/wizard/wizard.stories.tsx +148 -0
  424. package/src/components/wizard/wizard.test.tsx +67 -0
  425. package/src/components/wizard/wizard.tsx +354 -0
  426. package/src/decoration.stories.tsx +236 -0
  427. package/src/index.ts +174 -0
  428. package/src/lib/cn.test.ts +39 -0
  429. package/src/lib/cn.ts +31 -0
  430. package/src/lib/compact-number.ts +61 -0
  431. package/src/lib/document-address.test.ts +75 -0
  432. package/src/lib/document-address.ts +265 -0
  433. package/src/lib/download.ts +93 -0
  434. package/src/lib/file-icon.test.ts +45 -0
  435. package/src/lib/file-icon.ts +58 -0
  436. package/src/lib/file-kind.test.ts +98 -0
  437. package/src/lib/file-kind.ts +303 -0
  438. package/src/lib/file-source.test.ts +217 -0
  439. package/src/lib/file-source.ts +269 -0
  440. package/src/lib/merge-refs.test.ts +37 -0
  441. package/src/lib/merge-refs.ts +17 -0
  442. package/src/lib/streamdown-translations.ts +99 -0
  443. package/src/lib/use-copy-to-clipboard.ts +75 -0
  444. package/src/lib/use-mobile.ts +16 -0
  445. package/src/motion.stories.tsx +266 -0
  446. package/src/taste-profile.test.tsx +252 -0
  447. package/src/templates-object-detail-hub.stories.tsx +466 -0
  448. package/src/templates-screen-states.stories.tsx +306 -0
  449. package/src/templates-settings.stories.tsx +245 -0
package/src/index.ts ADDED
@@ -0,0 +1,174 @@
1
+ /**
2
+ * @elabs-ai/components-ui — foundation + app UI components.
3
+ *
4
+ * Source-owned shadcn-style components built on Radix primitives and the
5
+ * @elabs-ai/components-tokens semantic theme. Import the token stylesheet once at the app
6
+ * root ("import \"@elabs-ai/components-tokens/styles.css\"") and wrap in <ThemeProvider>.
7
+ *
8
+ * NOTE: this barrel is generated to include every component folder. Some
9
+ * components require their own peer libs (declared in package.json), e.g.
10
+ * Drawer (vaul), Command (cmdk), Toast (sonner), Calendar (react-day-picker),
11
+ * Carousel (embla-carousel-react), Resizable (react-resizable-panels),
12
+ * Form (react-hook-form + zod), InputOTP (input-otp).
13
+ */
14
+
15
+ // Utilities
16
+ export { cn } from "./lib/cn";
17
+ // The canonical mobile-breakpoint check (Sidebar / ContextPanel Sheet fallback
18
+ // pattern, research 09 §B.4) — exported so siblings don't fork a matchMedia hook.
19
+ export { useIsMobile } from "./lib/use-mobile";
20
+ // Browser file-download mechanics — one home for the Blob/URL → <a download> dance
21
+ // (shared by ConversationDownload, downloadCsv, and Gallery). SSR-guarded.
22
+ export { downloadBlob, downloadUrl } from "./lib/download";
23
+ // Clipboard write + transient "copied" flag — one implementation for the
24
+ // editor's copy button and `CopyableValue`, so the timing and the
25
+ // "no clipboard in this context" answer are defined once.
26
+ export {
27
+ COPY_FEEDBACK_MS,
28
+ useCopyToClipboard,
29
+ type CopyToClipboard,
30
+ type UseCopyToClipboardOptions,
31
+ } from "./lib/use-copy-to-clipboard";
32
+ // The shared file model (ADR 0024) — one input union + one coarse, CLOSED
33
+ // category, so `ai`, `viewer` and future surfaces stop each inventing their own.
34
+ // Fine-grained format matching belongs to the adapter manifests, not here.
35
+ export { type FileSource, type ResolvedFileSource, normalizeFileSource } from "./lib/file-source";
36
+ export {
37
+ type FileCategory,
38
+ type FileKind,
39
+ extensionOf,
40
+ mediaTypeFromName,
41
+ resolveFileKind,
42
+ } from "./lib/file-kind";
43
+ export { FILE_CATEGORY_ICONS, fileIconFor } from "./lib/file-icon";
44
+ // How one PART of a document is addressed (ADR 0025). Lives here for the same
45
+ // reason `FileSource` does: the producer of a citation (`ai`) and its consumer
46
+ // (`viewer`) are Layer-2 siblings that may not import each other.
47
+ export {
48
+ DOCUMENT_ADDRESS_KINDS,
49
+ type DocumentAddress,
50
+ type DocumentAddressKind,
51
+ type DocumentRect,
52
+ type NormalizedText,
53
+ type QuoteAddress,
54
+ type RangeAddress,
55
+ type RectAddress,
56
+ normalizeQuoteText,
57
+ normalizeQuoteTextWithOffsets,
58
+ } from "./lib/document-address";
59
+ // Streamdown's in-markdown chrome, resolved through `t()` (#310). Shared here
60
+ // because both `ai` and `viewer` render Streamdown and may not import each other.
61
+ export {
62
+ STREAMDOWN_TRANSLATION_KEYS,
63
+ useStreamdownTranslations,
64
+ type StreamdownTranslationKey,
65
+ type StreamdownTranslationMap,
66
+ } from "./lib/streamdown-translations";
67
+
68
+ // Components
69
+ export * from "./components/accordion";
70
+ export * from "./components/advanced-group";
71
+ export * from "./components/app-sidebar";
72
+ export * from "./components/alert";
73
+ export * from "./components/alert-dialog";
74
+ export * from "./components/app-shell";
75
+ export * from "./components/aspect-ratio";
76
+ export * from "./components/attribution-panel";
77
+ export * from "./components/avatar";
78
+ export * from "./components/badge";
79
+ export * from "./components/bento-grid";
80
+ export * from "./components/bounded-number";
81
+ export * from "./components/breadcrumb";
82
+ export * from "./components/button";
83
+ export * from "./components/button-group";
84
+ export * from "./components/calendar";
85
+ export * from "./components/card";
86
+ export * from "./components/carousel";
87
+ export * from "./components/change-review";
88
+ export * from "./components/checkbox";
89
+ export * from "./components/collapsible";
90
+ export * from "./components/collapsible-panel";
91
+ export * from "./components/color-picker";
92
+ export * from "./components/combobox";
93
+ export * from "./components/command";
94
+ export * from "./components/confirm-dialog";
95
+ export * from "./components/context-menu";
96
+ export * from "./components/copyable-value";
97
+ export * from "./components/date-picker";
98
+ export * from "./components/date-range-picker";
99
+ export * from "./components/descriptions";
100
+ export * from "./components/dialog";
101
+ export * from "./components/drawer";
102
+ export * from "./components/dropdown-menu";
103
+ export * from "./components/empty-state";
104
+ export * from "./components/error-state";
105
+ export * from "./components/expand-dialog";
106
+ export * from "./components/field-row";
107
+ export * from "./components/file-upload";
108
+ export * from "./components/form";
109
+ export * from "./components/hover-card";
110
+ export * from "./components/icon-button";
111
+ export * from "./components/input";
112
+ export * from "./components/input-group";
113
+ export * from "./components/input-otp";
114
+ export * from "./components/kbd";
115
+ export * from "./components/key-value-editor";
116
+ export * from "./components/label";
117
+ export * from "./components/link-preview";
118
+ export * from "./components/list-editor";
119
+ export * from "./components/locale-provider";
120
+ export * from "./components/loading-state";
121
+ export * from "./components/match-highlight";
122
+ export * from "./components/mention-input";
123
+ export * from "./components/menubar";
124
+ export * from "./components/model-picker";
125
+ export * from "./components/metric-card";
126
+ export * from "./components/nav-main";
127
+ export * from "./components/nav-notifications";
128
+ export * from "./components/nav-user";
129
+ export * from "./components/navigation-menu";
130
+ export * from "./components/number-input";
131
+ export * from "./components/page-shell";
132
+ export * from "./components/pagination";
133
+ export * from "./components/popover";
134
+ export * from "./components/progress";
135
+ export * from "./components/radio-group";
136
+ export * from "./components/rating";
137
+ export * from "./components/resizable";
138
+ export * from "./components/reveal";
139
+ export * from "./components/scroll-area";
140
+ export * from "./components/section-header";
141
+ export * from "./components/segmented-field";
142
+ export * from "./components/select";
143
+ export * from "./components/separator";
144
+ export * from "./components/sheet";
145
+ export * from "./components/sidebar";
146
+ export * from "./components/skeleton";
147
+ export * from "./components/slider";
148
+ export * from "./components/slider-number";
149
+ export * from "./components/sonner";
150
+ export * from "./components/spinner";
151
+ export * from "./components/state-panel";
152
+ export * from "./components/status-badge";
153
+ export * from "./components/split-panel";
154
+ export * from "./components/switch";
155
+ export * from "./components/table";
156
+ export * from "./components/tabs";
157
+ export * from "./components/tag-input";
158
+ export * from "./components/team-switcher";
159
+ export * from "./components/textarea";
160
+ export * from "./components/theme-switcher";
161
+ export * from "./components/revision-timeline";
162
+ export * from "./components/timeline";
163
+ export * from "./components/toggle";
164
+ export * from "./components/toggle-group";
165
+ export * from "./components/toolbar";
166
+ export * from "./components/tooltip";
167
+ export * from "./components/top-nav";
168
+ export * from "./components/transfer";
169
+ export * from "./components/tree";
170
+ export * from "./components/tree-select";
171
+ export * from "./components/typography";
172
+ export * from "./components/view-toolbar";
173
+ export * from "./components/virtual-select";
174
+ export * from "./components/wizard";
@@ -0,0 +1,39 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { cn } from "./cn";
3
+
4
+ describe("cn", () => {
5
+ it("later utilities win for a genuine conflict (e.g. padding)", () => {
6
+ expect(cn("p-2", "p-4")).toBe("p-4");
7
+ });
8
+
9
+ it("keeps semantic type-role and text-color independent (#187)", () => {
10
+ expect(cn("text-kpi", "text-foreground")).toContain("text-kpi");
11
+ expect(cn("text-kpi", "text-foreground")).toContain("text-foreground");
12
+ });
13
+
14
+ // #336: the CardDescription regression was NOT a tailwind-merge conflict — it
15
+ // was an invalid, misspelled utility name that isn't a real Tailwind class,
16
+ // which tailwind-merge's default `text-*` color group happily classified as
17
+ // an unrecognized color and dropped in favor of whichever `text-<color>`
18
+ // utility came later. `text-balance` is a real, already-registered Tailwind
19
+ // utility that tailwind-merge groups under its own `text-wrap` conflict
20
+ // group by default — no `cn.ts` registration is (or ever was) needed to
21
+ // keep it independent from `text-<color>`.
22
+ it("keeps text-balance and text-<color> independent regardless of order (#336)", () => {
23
+ const colorFirst = cn("text-sm", "text-muted-foreground", "text-balance");
24
+ expect(colorFirst).toContain("text-muted-foreground");
25
+ expect(colorFirst).toContain("text-balance");
26
+
27
+ const wrapFirst = cn("text-sm", "text-balance", "text-muted-foreground");
28
+ expect(wrapFirst).toContain("text-muted-foreground");
29
+ expect(wrapFirst).toContain("text-balance");
30
+ });
31
+
32
+ it("still dedupes conflicting text-wrap utilities against each other", () => {
33
+ expect(cn("text-balance", "text-pretty")).toBe("text-pretty");
34
+ });
35
+
36
+ it("still dedupes conflicting text-<color> utilities against each other", () => {
37
+ expect(cn("text-red-500", "text-blue-500")).toBe("text-blue-500");
38
+ });
39
+ });
package/src/lib/cn.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { extendTailwindMerge } from "tailwind-merge";
3
+
4
+ /**
5
+ * The semantic type roles (`--text-<role>` tokens → `text-<role>` utilities, #187).
6
+ * tailwind-merge doesn't know custom theme keys, so without registration it
7
+ * classifies `text-title` as a text COLOR — and `cn("text-kpi", "text-foreground")`
8
+ * would silently drop the role. Registering the roles in the `font-size` class
9
+ * group makes role-vs-role and role-vs-raw-size conflicts resolve correctly while
10
+ * keeping role + color independent.
11
+ */
12
+ const TEXT_ROLES = ["display", "title", "subtitle", "body", "caption", "meta", "kpi", "code"];
13
+
14
+ const twMerge = extendTailwindMerge({
15
+ extend: {
16
+ classGroups: {
17
+ "font-size": [{ text: TEXT_ROLES }],
18
+ // `font-display` is the display-face seam (sibling of font-mono, #187).
19
+ "font-family": [{ font: ["display"] }],
20
+ },
21
+ },
22
+ });
23
+
24
+ /**
25
+ * Merge class names with conflict resolution. `clsx` handles conditionals;
26
+ * `tailwind-merge` ensures later Tailwind utilities win (e.g. `p-2 p-4` -> `p-4`).
27
+ * Use this in every component that accepts a `className` prop.
28
+ */
29
+ export function cn(...inputs: ClassValue[]): string {
30
+ return twMerge(clsx(inputs));
31
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * compact-number.ts — the same "shown short, copied exact" number rule the
3
+ * charts package applies to axis ticks and tooltips, available to `ui` surfaces
4
+ * that display a figure (today: `MetricCard`).
5
+ *
6
+ * DELIBERATE TWIN of `packages/charts/src/charts/value-format.ts`. Dependencies
7
+ * flow one way — `tokens → ui → charts` — so `ui` cannot import the charts
8
+ * module, and a chart-domain union does not belong in the `ui` public API just
9
+ * to be shared. The duplication is ~20 lines and the two must stay in step:
10
+ * **change one, change the other**, and keep {@link COMPACT_THRESHOLD} equal in
11
+ * both. `metric-card.test.tsx` and `value-format.test.ts` pin the same cases on
12
+ * each side so a drift shows up as a failing test rather than as two surfaces
13
+ * disagreeing about what `1500` looks like.
14
+ */
15
+
16
+ /** How a numeric value is rendered. Mirrors the charts `ChartValueFormat`. */
17
+ export type NumberFormatKind = "number" | "compact" | "currency" | "percent";
18
+
19
+ /** Magnitude at or above which `compact`/`currency` switch to compact notation. */
20
+ export const COMPACT_THRESHOLD = 1000;
21
+
22
+ /** Used when a caller asks for currency without naming one. */
23
+ export const DEFAULT_CURRENCY = "USD";
24
+
25
+ /** One decimal keeps `1.5M` readable without implying precision it lacks. */
26
+ export const DEFAULT_MAX_FRACTION_DIGITS = 1;
27
+
28
+ /** `Intl.NumberFormatOptions` for `format` at this magnitude. */
29
+ export function numberFormatOptions(
30
+ format: NumberFormatKind,
31
+ value: number,
32
+ currency: string = DEFAULT_CURRENCY,
33
+ maxFractionDigits?: number,
34
+ ): Intl.NumberFormatOptions {
35
+ const compact = Number.isFinite(value) && Math.abs(value) >= COMPACT_THRESHOLD;
36
+ // `"number"` is the exact-value escape hatch and keeps `Intl`'s own default
37
+ // (3 digits) unless the caller states one — capping it at the compact rung's
38
+ // single decimal would render 0.0512 as "0.1".
39
+ const digits = maxFractionDigits ?? DEFAULT_MAX_FRACTION_DIGITS;
40
+ switch (format) {
41
+ case "number":
42
+ return maxFractionDigits === undefined ? {} : { maximumFractionDigits: maxFractionDigits };
43
+ case "percent":
44
+ return { style: "percent", maximumFractionDigits: digits };
45
+ case "currency":
46
+ return compact
47
+ ? {
48
+ style: "currency",
49
+ currency,
50
+ notation: "compact",
51
+ compactDisplay: "short",
52
+ maximumFractionDigits: digits,
53
+ }
54
+ : { style: "currency", currency, maximumFractionDigits: digits };
55
+ case "compact":
56
+ default:
57
+ return compact
58
+ ? { notation: "compact", compactDisplay: "short", maximumFractionDigits: digits }
59
+ : { maximumFractionDigits: digits };
60
+ }
61
+ }
@@ -0,0 +1,75 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { normalizeQuoteText, normalizeQuoteTextWithOffsets } from "./document-address";
4
+
5
+ /** Map a normalized range back and read the slice it names in the original. */
6
+ function sliceVia(original: string, from: number, to: number): string {
7
+ const { offsets } = normalizeQuoteTextWithOffsets(original);
8
+ return original.slice(offsets[from], offsets[to]);
9
+ }
10
+
11
+ describe("normalizeQuoteText", () => {
12
+ it("collapses every whitespace run to one space, and trims", () => {
13
+ expect(normalizeQuoteText(" the\n\n quick\tbrown \r\n fox ")).toBe("the quick brown fox");
14
+ });
15
+
16
+ it("straightens the quotes and dashes an extractor disagrees about", () => {
17
+ // The same sentence out of a PDF and out of a model. Neither side should
18
+ // have to know which glyph the other used.
19
+ expect(normalizeQuoteText("“Don’t”—she said")).toBe(normalizeQuoteText('"Don\'t"-she said'));
20
+ });
21
+
22
+ it("folds case, so a citation matches without a caller-set flag", () => {
23
+ expect(normalizeQuoteText("Total Revenue")).toBe(normalizeQuoteText("total revenue"));
24
+ });
25
+
26
+ it("drops the invisibles a line break leaves behind", () => {
27
+ // A soft hyphen at a PDF's line wrap, and a zero-width space.
28
+ expect(normalizeQuoteText("re­venue​")).toBe("revenue");
29
+ });
30
+
31
+ it("leaves a ligature alone — folding it would be a 1:many expansion", () => {
32
+ // The offset map is only trustworthy while every rule is a collapse, a drop
33
+ // or a 1:1 swap. A fuzzier match is not worth losing that.
34
+ expect(normalizeQuoteText("final")).toBe("final");
35
+ });
36
+ });
37
+
38
+ describe("normalizeQuoteTextWithOffsets", () => {
39
+ it("ends one past the last SURVIVING character, not at the input's length", () => {
40
+ // Ending at the input's length would drag the trailing space into every
41
+ // range that runs to the end of the document.
42
+ const input = " hello world ";
43
+ const { text, offsets } = normalizeQuoteTextWithOffsets(input);
44
+ expect(text).toBe("hello world");
45
+ expect(offsets).toHaveLength(text.length + 1);
46
+ expect(offsets[offsets.length - 1]).toBe(input.lastIndexOf("d") + 1);
47
+ });
48
+
49
+ it("maps a normalized range back onto the ORIGINAL characters", () => {
50
+ // "world" in the normalized string is at [6, 11); in the original it sits
51
+ // past a three-space run and a leading pair, at completely different indices.
52
+ const original = " Hello World ";
53
+ expect(sliceVia(original, 6, 11)).toBe("World");
54
+ });
55
+
56
+ it("points a collapsed space at where its run began", () => {
57
+ // Selecting across the gap must include the whole run, not just its last
58
+ // character — otherwise a highlight starts mid-whitespace.
59
+ const original = "a \n\t b";
60
+ expect(sliceVia(original, 0, 3)).toBe("a \n\t b");
61
+ });
62
+
63
+ it("keeps the map monotone across folds, drops and case changes", () => {
64
+ const { offsets } = normalizeQuoteTextWithOffsets(" “Süß”­ — DONE ");
65
+ for (let i = 1; i < offsets.length; i += 1) {
66
+ expect(offsets[i]).toBeGreaterThan(offsets[i - 1] as number);
67
+ }
68
+ });
69
+
70
+ it("survives a string that is nothing but whitespace", () => {
71
+ const { text, offsets } = normalizeQuoteTextWithOffsets(" \n ");
72
+ expect(text).toBe("");
73
+ expect(offsets).toEqual([0]);
74
+ });
75
+ });
@@ -0,0 +1,265 @@
1
+ /**
2
+ * How one part of a document is addressed.
3
+ *
4
+ * A viewer that can only open a file answers "here is the document". A viewer
5
+ * that can be pointed at a PART of one answers "here is the sentence that
6
+ * claim came from" — the RAG-citation case, and the same machinery behind
7
+ * find-in-document.
8
+ *
9
+ * ## Why this lives in `@elabs-ai/components-ui`
10
+ *
11
+ * The producer of an address (a chat answer's citation, in
12
+ * `@elabs-ai/components-ai`) and its consumer (the file viewer, in
13
+ * `@elabs-ai/components-viewer`) are Layer-2 siblings that may not
14
+ * import each other. `ui` is the only layer both reach, which is exactly why
15
+ * `FileSource` and `MatchRange` already live here. This module is a
16
+ * dependency-free leaf: types plus pure string functions, no React.
17
+ *
18
+ * ## Three kinds, one union
19
+ *
20
+ * Real producers disagree about what they can emit, so the union carries all
21
+ * three rather than forcing everyone through the weakest:
22
+ *
23
+ * - **`quote`** — the snippet text. The only kind that survives crossing a tool
24
+ * boundary: an indexing pipeline's character offsets are computed against ITS
25
+ * extraction of the PDF, never against ours, so its numbers are meaningless
26
+ * here while its text still is. Lossy (a repeated phrase is ambiguous) and the
27
+ * most useful.
28
+ * - **`range`** — exact character offsets, and exact only against the SAME text
29
+ * projection: `AdapterDocument.text` as this build's adapter produced it. Use
30
+ * it when both ends are ours (find-in-document, a second pass over a document
31
+ * already open).
32
+ * - **`rect`** — page-relative boxes, `0..1` fractions of the page. Needs no
33
+ * text at all, which is what makes it the answer for a scanned page, a chart,
34
+ * a photo region, or any layout-analysis model whose output is geometry.
35
+ *
36
+ * ## The rule that lets this union grow
37
+ *
38
+ * A consumer matches only the kinds it declared it honours and reports the rest
39
+ * as unsupported — it never writes an exhaustive `switch` with a `never`
40
+ * fallthrough. That is what lets a fourth kind (a spreadsheet `cell` address,
41
+ * say) be added later without breaking every consumer that predates it.
42
+ */
43
+
44
+ /** The address kinds, as a value — for capability declarations and iteration. */
45
+ export const DOCUMENT_ADDRESS_KINDS = ["quote", "range", "rect"] as const;
46
+
47
+ /** Which way a part of a document is addressed. */
48
+ export type DocumentAddressKind = (typeof DOCUMENT_ADDRESS_KINDS)[number];
49
+
50
+ /**
51
+ * A box on a page, as fractions of that page's own box.
52
+ *
53
+ * Fractions, not pixels, because the page is not a fixed size: it rescales with
54
+ * zoom, with the pane's width and with the device's pixel ratio. A consumer
55
+ * multiplies by the page element's live measured size at paint time, so the box
56
+ * stays put through every one of those. Origin is top-left (CSS), so `y` grows
57
+ * downward — matching the DOM rather than the PDF's own bottom-left space.
58
+ */
59
+ export interface DocumentRect {
60
+ /** Distance from the page's left edge, `0..1`. */
61
+ x: number;
62
+ /** Distance from the page's TOP edge, `0..1`. */
63
+ y: number;
64
+ /** Fraction of the page's width. */
65
+ width: number;
66
+ /** Fraction of the page's height. */
67
+ height: number;
68
+ }
69
+
70
+ /**
71
+ * Address by the text itself — the interoperable kind.
72
+ *
73
+ * Matching is done on the normalized form of both sides
74
+ * ({@link normalizeQuoteText}), so a quote survives the whitespace, quote-glyph
75
+ * and casing differences that every extractor introduces.
76
+ */
77
+ export interface QuoteAddress {
78
+ kind: "quote";
79
+ /** The passage to find. Whitespace and quote glyphs need not match exactly. */
80
+ text: string;
81
+ /**
82
+ * Which occurrence to take when the passage appears more than once, 1-based.
83
+ * Omitted means the first — or the one nearest {@link QuoteAddress.near},
84
+ * when that is given.
85
+ */
86
+ occurrence?: number;
87
+ /**
88
+ * A hint about where to look. It disambiguates repeats rather than restricting
89
+ * the search: the occurrence closest to it wins, and a passage that turns out
90
+ * to be elsewhere is still found.
91
+ */
92
+ near?: {
93
+ /**
94
+ * 1-based page (or slide) the passage is expected on.
95
+ *
96
+ * Advisory, and deliberately NOT used to navigate: the viewer turns to the
97
+ * page the passage was actually located on, so a stale hint cannot send a
98
+ * reader to a blank page. Only {@link QuoteAddress.near.offset} takes part in
99
+ * tie-breaking today; a renderer may read this off the address if it wants
100
+ * more.
101
+ */
102
+ page?: number;
103
+ /** Approximate character offset the passage is expected near. */
104
+ offset?: number;
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Address by character offsets into `AdapterDocument.text`, half-open
110
+ * `[start, end)` — the same convention as `MatchRange`.
111
+ *
112
+ * Only meaningful against the text projection THIS build produced. An offset
113
+ * from a foreign pipeline is not this kind; it is a {@link QuoteAddress}.
114
+ */
115
+ export interface RangeAddress {
116
+ kind: "range";
117
+ start: number;
118
+ end: number;
119
+ /**
120
+ * 1-based page hint, when the producer knows it. Advisory only — the viewer
121
+ * derives the page from the offsets against its own index, so a hint that
122
+ * disagrees cannot misdirect the pager.
123
+ */
124
+ page?: number;
125
+ }
126
+
127
+ /** Address by geometry — one or more boxes on one page. */
128
+ export interface RectAddress {
129
+ kind: "rect";
130
+ /** 1-based page (or slide) number. */
131
+ page: number;
132
+ /**
133
+ * The boxes making up this one addressed part. An array, not a single box,
134
+ * because a real passage wraps: a sentence spanning three lines is three
135
+ * boxes, and a two-column paragraph is more. Painting them as one shape each
136
+ * is what avoids stitching DOM ranges together.
137
+ */
138
+ rects: readonly DocumentRect[];
139
+ }
140
+
141
+ /** Which part of a document something refers to. */
142
+ export type DocumentAddress = QuoteAddress | RangeAddress | RectAddress;
143
+
144
+ /* -------------------------------------------------------------------------- */
145
+ /* Quote normalization */
146
+ /* -------------------------------------------------------------------------- */
147
+
148
+ /** Straightened 1:1 — every one of these is one code unit in and one out. */
149
+ const CHARACTER_FOLDS: Readonly<Record<string, string>> = {
150
+ // Curly quotes and primes. A model quoting a PDF re-types these constantly;
151
+ // the file has the other one.
152
+ "‘": "'",
153
+ "’": "'",
154
+ "‚": "'",
155
+ "‛": "'",
156
+ "′": "'",
157
+ "“": '"',
158
+ "”": '"',
159
+ "„": '"',
160
+ "‟": '"',
161
+ "″": '"',
162
+ // Dashes. PDF extraction yields the typographic one, prose yields a hyphen.
163
+ "‐": "-",
164
+ "‑": "-",
165
+ "‒": "-",
166
+ "–": "-",
167
+ "—": "-",
168
+ "―": "-",
169
+ "−": "-",
170
+ };
171
+
172
+ /**
173
+ * Invisible, and typically present in exactly one of the two strings being
174
+ * compared: soft hyphen (a PDF's line-break artefact), zero-width space, ZWNJ,
175
+ * ZWJ, word joiner. Written as escapes because a literal here is unreviewable —
176
+ * it renders as nothing.
177
+ */
178
+ const DROPPED = new Set(["­", "​", "‌", "‍", "⁠"]);
179
+
180
+ /**
181
+ * Anything that collapses into one space. Plain `\s` is already the full set in
182
+ * JavaScript — it covers NBSP, the `U+2000`–`U+200A` spaces, the line and
183
+ * paragraph separators, narrow NBSP, ideographic space and the BOM.
184
+ */
185
+ const WHITESPACE = /\s/u;
186
+
187
+ /**
188
+ * The result of normalizing, with the way back.
189
+ *
190
+ * `offsets` has one more entry than `text` has characters: `offsets[i]` is the
191
+ * index in the ORIGINAL string where normalized character `i` came from, and
192
+ * the final entry is one past the last character that survived — NOT the
193
+ * original's length, which would drag trailing whitespace into every range that
194
+ * runs to the end. So a normalized range `[a, b)` maps back to
195
+ * `[offsets[a], offsets[b])` with no arithmetic, which is the whole point of
196
+ * building the map rather than searching twice.
197
+ */
198
+ export interface NormalizedText {
199
+ text: string;
200
+ offsets: readonly number[];
201
+ }
202
+
203
+ /**
204
+ * Fold a string to its canonical match key, keeping a map back to the original.
205
+ *
206
+ * Every rule is a collapse, a drop or a 1:1 swap — never an expansion — so the
207
+ * map stays monotone and a range maps back exactly. That constraint is why
208
+ * ligatures (`fi`) and the ellipsis character are deliberately left alone: both
209
+ * would be 1:many, and a fuzzier match is not worth an offset map that can no
210
+ * longer be trusted.
211
+ *
212
+ * What it does: collapses every whitespace run to one space and trims; drops
213
+ * zero-width characters and soft hyphens; straightens curly quotes and dashes;
214
+ * folds case — a citation is matched case-insensitively, and that is a property
215
+ * of the match key rather than a flag every caller has to remember.
216
+ */
217
+ export function normalizeQuoteTextWithOffsets(input: string): NormalizedText {
218
+ const out: string[] = [];
219
+ const offsets: number[] = [];
220
+ let pendingSpaceAt: number | undefined;
221
+ /** One past the last surviving character — the map's terminal entry. */
222
+ let end = 0;
223
+
224
+ for (let i = 0; i < input.length; i += 1) {
225
+ const character = input[i] as string;
226
+
227
+ if (WHITESPACE.test(character)) {
228
+ // Held rather than emitted, so a trailing run never reaches the output
229
+ // and a leading one is dropped by the `out.length` test below.
230
+ pendingSpaceAt ??= i;
231
+ continue;
232
+ }
233
+ if (DROPPED.has(character)) continue;
234
+
235
+ if (pendingSpaceAt !== undefined) {
236
+ if (out.length > 0) {
237
+ out.push(" ");
238
+ // The single space stands for the whole run, so it points at where the
239
+ // run began — not at the character that ended it.
240
+ offsets.push(pendingSpaceAt);
241
+ }
242
+ pendingSpaceAt = undefined;
243
+ }
244
+
245
+ const folded = CHARACTER_FOLDS[character] ?? character;
246
+ const lowered = folded.toLowerCase();
247
+ // Guard the 1:1 invariant: a few code points grow when lowercased
248
+ // (`İ` → `i̇`). Keeping the original is a worse match and a correct map; the
249
+ // reverse trade would silently corrupt every offset after it.
250
+ out.push(lowered.length === folded.length ? lowered : folded);
251
+ offsets.push(i);
252
+ end = i + 1;
253
+ }
254
+
255
+ offsets.push(end);
256
+ return { text: out.join(""), offsets };
257
+ }
258
+
259
+ /**
260
+ * The canonical match key for a quote — {@link normalizeQuoteTextWithOffsets}
261
+ * without the map, for comparing or de-duplicating two quotes.
262
+ */
263
+ export function normalizeQuoteText(input: string): string {
264
+ return normalizeQuoteTextWithOffsets(input).text;
265
+ }