@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
@@ -0,0 +1,231 @@
1
+ import { useState } from "react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { render, screen } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+ import { KeyValueEditor, type KeyValueRow } from "./key-value-editor";
6
+
7
+ describe("KeyValueEditor", () => {
8
+ it("renders one key/value input pair per row", () => {
9
+ render(
10
+ <KeyValueEditor
11
+ defaultValue={[
12
+ { key: "A", value: "1" },
13
+ { key: "B", value: "2" },
14
+ ]}
15
+ />,
16
+ );
17
+ expect(screen.getByLabelText("Key 1")).toHaveValue("A");
18
+ expect(screen.getByLabelText("Value 1")).toHaveValue("1");
19
+ expect(screen.getByLabelText("Key 2")).toHaveValue("B");
20
+ expect(screen.getByLabelText("Value 2")).toHaveValue("2");
21
+ });
22
+
23
+ it("renders a real empty state for zero rows", () => {
24
+ render(<KeyValueEditor defaultValue={[]} />);
25
+ expect(screen.getByText("No entries yet.")).toBeInTheDocument();
26
+ });
27
+
28
+ it("adds a row via the Add row button", async () => {
29
+ const onValueChange = vi.fn();
30
+ render(<KeyValueEditor defaultValue={[]} onValueChange={onValueChange} />);
31
+ await userEvent.click(screen.getByRole("button", { name: /add row/i }));
32
+ expect(onValueChange).toHaveBeenCalledWith([{ key: "", value: "", secret: false }]);
33
+ });
34
+
35
+ it("edits a row's key and value", async () => {
36
+ const onValueChange = vi.fn();
37
+ render(
38
+ <KeyValueEditor defaultValue={[{ key: "", value: "" }]} onValueChange={onValueChange} />,
39
+ );
40
+ await userEvent.type(screen.getByLabelText("Key 1"), "X");
41
+ expect(onValueChange).toHaveBeenLastCalledWith([{ key: "X", value: "" }]);
42
+ });
43
+
44
+ it("removes a row via the remove button", async () => {
45
+ const onValueChange = vi.fn();
46
+ render(
47
+ <KeyValueEditor
48
+ defaultValue={[
49
+ { key: "A", value: "1" },
50
+ { key: "B", value: "2" },
51
+ ]}
52
+ onValueChange={onValueChange}
53
+ />,
54
+ );
55
+ await userEvent.click(screen.getByRole("button", { name: "Remove row 1" }));
56
+ expect(onValueChange).toHaveBeenCalledWith([{ key: "B", value: "2" }]);
57
+ });
58
+
59
+ // ── Security: masking ─────────────────────────────────────────────────
60
+
61
+ it("does not render a reveal toggle for a non-secret row", () => {
62
+ render(<KeyValueEditor defaultValue={[{ key: "REGION", value: "us-east-1" }]} />);
63
+ expect(screen.getByLabelText("Value 1")).toHaveAttribute("type", "text");
64
+ expect(screen.queryByRole("button", { name: /reveal/i })).not.toBeInTheDocument();
65
+ });
66
+
67
+ it("masks a secret row's value via type=password until revealed", () => {
68
+ render(
69
+ <KeyValueEditor
70
+ defaultValue={[{ key: "API_KEY", value: "sk-super-secret", secret: true }]}
71
+ />,
72
+ );
73
+ const valueInput = screen.getByLabelText("Value 1");
74
+ expect(valueInput).toHaveAttribute("type", "password");
75
+ });
76
+
77
+ it("reveals the value on toggle click, then hides it again", async () => {
78
+ render(<KeyValueEditor defaultValue={[{ key: "API_KEY", value: "sk-1234", secret: true }]} />);
79
+ const valueInput = screen.getByLabelText("Value 1");
80
+ await userEvent.click(screen.getByRole("button", { name: "Reveal value 1" }));
81
+ expect(valueInput).toHaveAttribute("type", "text");
82
+ expect(valueInput).toHaveValue("sk-1234");
83
+
84
+ await userEvent.click(screen.getByRole("button", { name: "Hide value 1" }));
85
+ expect(valueInput).toHaveAttribute("type", "password");
86
+ });
87
+
88
+ it("never exposes the raw secret via title/aria-label/aria-valuetext while masked", () => {
89
+ const secret = "sk-super-secret-value";
90
+ const { container } = render(
91
+ <KeyValueEditor defaultValue={[{ key: "API_KEY", value: secret, secret: true }]} />,
92
+ );
93
+ const html = container.innerHTML;
94
+ expect(html).not.toContain(`title="${secret}"`);
95
+ expect(html).not.toContain(`aria-label="${secret}"`);
96
+ expect(html).not.toContain(`aria-valuetext="${secret}"`);
97
+ // The reveal button's own accessible name never carries the secret text.
98
+ const revealBtn = screen.getByRole("button", { name: "Reveal value 1" });
99
+ expect(revealBtn).not.toHaveAccessibleName(secret);
100
+ });
101
+
102
+ it("preserves a row's own reveal state when an earlier row is removed", async () => {
103
+ render(
104
+ <KeyValueEditor
105
+ defaultValue={[
106
+ { key: "A", value: "one", secret: true },
107
+ { key: "B", value: "two", secret: true },
108
+ ]}
109
+ />,
110
+ );
111
+ // Reveal row 2's ("B") value.
112
+ await userEvent.click(screen.getByRole("button", { name: "Reveal value 2" }));
113
+ expect(screen.getByLabelText("Value 2")).toHaveAttribute("type", "text");
114
+
115
+ // Remove row 1 ("A") — row 2 ("B") shifts down to index 0 (label "Value 1").
116
+ await userEvent.click(screen.getByRole("button", { name: "Remove row 1" }));
117
+
118
+ // "B" is the SAME row the user explicitly revealed — its reveal state
119
+ // must follow it, not reset just because an earlier row was removed.
120
+ expect(screen.getByLabelText("Value 1")).toHaveAttribute("type", "text");
121
+ });
122
+
123
+ it("never leaves a NEW row at a shifted position incorrectly revealed", async () => {
124
+ render(
125
+ <KeyValueEditor
126
+ defaultValue={[
127
+ { key: "A", value: "one", secret: true },
128
+ { key: "B", value: "two", secret: true },
129
+ ]}
130
+ />,
131
+ );
132
+ // Reveal row 1's ("A") value, then remove row 1 itself.
133
+ await userEvent.click(screen.getByRole("button", { name: "Reveal value 1" }));
134
+ await userEvent.click(screen.getByRole("button", { name: "Remove row 1" }));
135
+
136
+ // "B" now occupies position 1 but was NEVER revealed by the user — it
137
+ // must render masked, not inherit the removed row's reveal state.
138
+ expect(screen.getByLabelText("Value 1")).toHaveAttribute("type", "password");
139
+ });
140
+
141
+ // The reveal stamp fails CLOSED for any row-list change this component did
142
+ // not itself perform — an index alone would unmask a DIFFERENT row's secret.
143
+
144
+ it("re-masks when a controlled parent PREPENDS a row under a revealed one", async () => {
145
+ function Harness() {
146
+ const [rows, setRows] = useState<KeyValueRow[]>([
147
+ { key: "A", value: "revealed-me", secret: true },
148
+ { key: "B", value: "sk-TOP-SECRET", secret: true },
149
+ ]);
150
+ return (
151
+ <>
152
+ <button onClick={() => setRows((r) => [{ key: "Z", value: "zzz", secret: true }, ...r])}>
153
+ prepend
154
+ </button>
155
+ <KeyValueEditor value={rows} onValueChange={setRows} />
156
+ </>
157
+ );
158
+ }
159
+ render(<Harness />);
160
+ await userEvent.click(screen.getByRole("button", { name: "Reveal value 1" }));
161
+ expect(screen.getByLabelText("Value 1")).toHaveAttribute("type", "text");
162
+
163
+ await userEvent.click(screen.getByRole("button", { name: "prepend" }));
164
+ // Index 1 is now "Z", a row the user never revealed.
165
+ expect(screen.getByLabelText("Key 1")).toHaveValue("Z");
166
+ expect(screen.getByLabelText("Value 1")).toHaveAttribute("type", "password");
167
+ });
168
+
169
+ it("re-masks when a controlled parent REORDERS the rows", async () => {
170
+ function Harness() {
171
+ const [rows, setRows] = useState<KeyValueRow[]>([
172
+ { key: "A", value: "aaa", secret: true },
173
+ { key: "B", value: "sk-TOP-SECRET", secret: true },
174
+ ]);
175
+ return (
176
+ <>
177
+ <button onClick={() => setRows((r) => [...r].reverse())}>reverse</button>
178
+ <KeyValueEditor value={rows} onValueChange={setRows} />
179
+ </>
180
+ );
181
+ }
182
+ render(<Harness />);
183
+ await userEvent.click(screen.getByRole("button", { name: "Reveal value 1" }));
184
+ await userEvent.click(screen.getByRole("button", { name: "reverse" }));
185
+ expect(screen.getByLabelText("Key 1")).toHaveValue("B");
186
+ expect(screen.getByLabelText("Value 1")).toHaveAttribute("type", "password");
187
+ });
188
+
189
+ it("never unmasks a row when a controlled parent REJECTS a removal", async () => {
190
+ const rows: KeyValueRow[] = [
191
+ { key: "A", value: "aaa", secret: true },
192
+ { key: "B", value: "sk-TOP-SECRET", secret: true },
193
+ ];
194
+ render(<KeyValueEditor value={rows} onValueChange={() => {}} />);
195
+ await userEvent.click(screen.getByRole("button", { name: "Reveal value 2" }));
196
+ // The parent ignores the change, so the rows do NOT shift — the re-indexed
197
+ // reveal must not land on row 1.
198
+ await userEvent.click(screen.getByRole("button", { name: "Remove row 1" }));
199
+ expect(screen.getByLabelText("Key 1")).toHaveValue("A");
200
+ expect(screen.getByLabelText("Value 1")).toHaveAttribute("type", "password");
201
+ expect(screen.getByLabelText("Value 2")).toHaveAttribute("type", "password");
202
+ });
203
+
204
+ it("keeps a revealed secret revealed while the user edits it", async () => {
205
+ render(<KeyValueEditor defaultValue={[{ key: "API_KEY", value: "sk-1", secret: true }]} />);
206
+ await userEvent.click(screen.getByRole("button", { name: "Reveal value 1" }));
207
+ await userEvent.type(screen.getByLabelText("Value 1"), "23");
208
+ expect(screen.getByLabelText("Value 1")).toHaveAttribute("type", "text");
209
+ expect(screen.getByLabelText("Value 1")).toHaveValue("sk-123");
210
+ });
211
+
212
+ it("supports the controlled mode via value/onValueChange", async () => {
213
+ const onValueChange = vi.fn();
214
+ render(<KeyValueEditor value={[]} onValueChange={onValueChange} />);
215
+ await userEvent.click(screen.getByRole("button", { name: /add row/i }));
216
+ expect(onValueChange).toHaveBeenCalledWith([{ key: "", value: "", secret: false }]);
217
+ });
218
+
219
+ it("is disabled when disabled prop is set", () => {
220
+ render(<KeyValueEditor defaultValue={[{ key: "A", value: "1" }]} disabled />);
221
+ expect(screen.getByLabelText("Key 1")).toBeDisabled();
222
+ expect(screen.getByLabelText("Value 1")).toBeDisabled();
223
+ expect(screen.getByRole("button", { name: /add row/i })).toBeDisabled();
224
+ });
225
+
226
+ it("never blocks paste into the value field", async () => {
227
+ render(<KeyValueEditor defaultValue={[{ key: "A", value: "" }]} />);
228
+ const valueInput = screen.getByLabelText("Value 1") as HTMLInputElement;
229
+ expect(valueInput.onpaste).toBeNull();
230
+ });
231
+ });
@@ -0,0 +1,263 @@
1
+ "use client";
2
+
3
+ import {
4
+ forwardRef,
5
+ useCallback,
6
+ useMemo,
7
+ useState,
8
+ type HTMLAttributes,
9
+ type ReactNode,
10
+ } from "react";
11
+ import { Eye, EyeOff, Plus, X } from "lucide-react";
12
+ import { cn } from "../../lib/cn";
13
+ import { Button } from "../button";
14
+ import { Input } from "../input";
15
+ import { useLocale } from "../locale-provider";
16
+
17
+ export interface KeyValueRow {
18
+ key: string;
19
+ value: string;
20
+ /**
21
+ * Marks this row's value as sensitive: it renders masked (`type="password"`)
22
+ * with a reveal toggle, and is never surfaced in plain text via any
23
+ * `title`/`aria-label`/`aria-valuetext`. Defaults to false (a plain,
24
+ * unmasked key/value pair) — the consumer opts a row in explicitly.
25
+ */
26
+ secret?: boolean;
27
+ }
28
+
29
+ export interface KeyValueEditorProps extends Omit<
30
+ HTMLAttributes<HTMLDivElement>,
31
+ "onChange" | "defaultValue"
32
+ > {
33
+ /**
34
+ * Controlled, ORDERED rows. An ordered `{ key, value, secret? }[]` is used
35
+ * instead of `Record<string,string>` because a plain record can't
36
+ * represent an in-progress duplicate key or preserve row order while the
37
+ * user is mid-edit — both happen constantly while typing an env-var map.
38
+ */
39
+ value?: KeyValueRow[];
40
+ /** Uncontrolled initial rows. */
41
+ defaultValue?: KeyValueRow[];
42
+ /** Called when the rows change (add, remove, or edit). */
43
+ onValueChange?: (rows: KeyValueRow[]) => void;
44
+ /** Placeholder for each row's key field. */
45
+ keyPlaceholder?: string;
46
+ /** Placeholder for each row's value field. */
47
+ valuePlaceholder?: string;
48
+ /** Label for the "add row" control. */
49
+ addLabel?: ReactNode;
50
+ /** Disables the whole editor. */
51
+ disabled?: boolean;
52
+ }
53
+
54
+ /**
55
+ * KeyValueEditor — key/value rows with per-row secret masking.
56
+ *
57
+ * Composes the existing `Input` + `Button` (icon vocabulary matches
58
+ * `packages/ai/src/environment-variables.tsx`'s `Eye`/`EyeOff` reveal icons
59
+ * for conceptual consistency, though this component lives in
60
+ * `@elabs-ai/components-ui` — an EDITABLE settings-form primitive
61
+ * is app UI, not the read-only agent tool-output display that
62
+ * `EnvironmentVariables` is).
63
+ *
64
+ * A masked row uses the native `type="password"` input (the correct
65
+ * primitive for an EDITABLE masked field — unlike a read-only display, the
66
+ * value must stay typable, and native masking is what keeps it that way
67
+ * without hand-rolling bullet substitution). The reveal toggle's
68
+ * `aria-label` never contains the secret text itself, and no `title`
69
+ * attribute is ever set from row content.
70
+ *
71
+ * Controlled via `value`/`onValueChange`; uncontrolled via `defaultValue`.
72
+ */
73
+ export const KeyValueEditor = forwardRef<HTMLDivElement, KeyValueEditorProps>(
74
+ function KeyValueEditor(
75
+ {
76
+ value: valueProp,
77
+ defaultValue,
78
+ onValueChange,
79
+ keyPlaceholder,
80
+ valuePlaceholder,
81
+ addLabel,
82
+ disabled,
83
+ className,
84
+ ...props
85
+ },
86
+ ref,
87
+ ) {
88
+ const { t } = useLocale();
89
+ const isControlled = valueProp !== undefined;
90
+ const [internalRows, setInternalRows] = useState<KeyValueRow[]>(defaultValue ?? []);
91
+ const rows = useMemo(
92
+ () => (isControlled ? (valueProp ?? []) : internalRows),
93
+ [isControlled, valueProp, internalRows],
94
+ );
95
+ // Reveal state is keyed by row index AND stamped with the exact value the
96
+ // user chose to reveal. The index alone is NOT safe: the row list can be
97
+ // re-ordered, prepended to, filtered, or left unchanged (a controlled
98
+ // parent that rejects a removal) entirely OUTSIDE this component, and a
99
+ // bare index would then point at a DIFFERENT row — unmasking a secret the
100
+ // user never asked to see. Comparing the stamp against the row's current
101
+ // value makes the check FAIL CLOSED: any row whose value is not exactly
102
+ // the one that was revealed renders masked again.
103
+ const [revealed, setRevealed] = useState<Map<number, string>>(() => new Map());
104
+
105
+ const isRevealed = useCallback(
106
+ (index: number, row: KeyValueRow) => revealed.get(index) === row.value,
107
+ [revealed],
108
+ );
109
+
110
+ const commit = useCallback(
111
+ (next: KeyValueRow[]) => {
112
+ if (!isControlled) setInternalRows(next);
113
+ onValueChange?.(next);
114
+ },
115
+ [isControlled, onValueChange],
116
+ );
117
+
118
+ const addRow = useCallback(() => {
119
+ commit([...rows, { key: "", value: "", secret: false }]);
120
+ }, [rows, commit]);
121
+
122
+ const updateRow = useCallback(
123
+ (index: number, patch: Partial<KeyValueRow>) => {
124
+ commit(rows.map((row, i) => (i === index ? { ...row, ...patch } : row)));
125
+ // Typing into a row the user deliberately revealed must not re-mask it
126
+ // mid-keystroke — re-stamp the snapshot with the new value, but ONLY
127
+ // when the old stamp still matched (i.e. it really was revealed).
128
+ if (patch.value !== undefined) {
129
+ const nextValue = patch.value;
130
+ setRevealed((prev) => {
131
+ if (prev.get(index) !== rows[index]?.value) return prev;
132
+ const next = new Map(prev);
133
+ next.set(index, nextValue);
134
+ return next;
135
+ });
136
+ }
137
+ },
138
+ [rows, commit],
139
+ );
140
+
141
+ const removeRow = useCallback(
142
+ (index: number) => {
143
+ commit(rows.filter((_, i) => i !== index));
144
+ // Re-index revealed state so a shifted row never inherits a stale
145
+ // reveal — dropping the removed index and shifting later ones down.
146
+ // The value stamp above is the second line of defence: if the parent
147
+ // rejects or rewrites the removal, the shifted stamps stop matching
148
+ // and every row falls back to masked.
149
+ setRevealed((prev) => {
150
+ const next = new Map<number, string>();
151
+ for (const [i, stamp] of prev) {
152
+ if (i === index) continue;
153
+ next.set(i > index ? i - 1 : i, stamp);
154
+ }
155
+ return next;
156
+ });
157
+ },
158
+ [rows, commit],
159
+ );
160
+
161
+ const toggleReveal = useCallback((index: number, row: KeyValueRow) => {
162
+ setRevealed((prev) => {
163
+ const next = new Map(prev);
164
+ if (next.get(index) === row.value) next.delete(index);
165
+ else next.set(index, row.value);
166
+ return next;
167
+ });
168
+ }, []);
169
+
170
+ return (
171
+ <div
172
+ ref={ref}
173
+ data-slot="key-value-editor"
174
+ className={cn("flex flex-col gap-2", className)}
175
+ {...props}
176
+ >
177
+ {rows.length === 0 ? (
178
+ <p data-slot="key-value-editor-empty" className="text-body text-muted-foreground">
179
+ {t("ui.keyValueEditor.empty")}
180
+ </p>
181
+ ) : (
182
+ <div className="flex flex-col gap-2">
183
+ {rows.map((row, index) => {
184
+ const isSecret = row.secret === true;
185
+ const rowRevealed = isRevealed(index, row);
186
+ const masked = isSecret && !rowRevealed;
187
+ return (
188
+ <div
189
+ key={index}
190
+ data-slot="key-value-editor-row"
191
+ className="flex items-center gap-1"
192
+ >
193
+ <Input
194
+ data-slot="key-value-editor-key"
195
+ value={row.key}
196
+ placeholder={keyPlaceholder}
197
+ disabled={disabled}
198
+ aria-label={t("ui.keyValueEditor.keyLabel", { n: index + 1 })}
199
+ onChange={(event) => updateRow(index, { key: event.target.value })}
200
+ autoComplete="off"
201
+ spellCheck={false}
202
+ className="flex-1"
203
+ />
204
+ <Input
205
+ data-slot="key-value-editor-value"
206
+ type={masked ? "password" : "text"}
207
+ value={row.value}
208
+ placeholder={valuePlaceholder}
209
+ disabled={disabled}
210
+ aria-label={t("ui.keyValueEditor.valueLabel", { n: index + 1 })}
211
+ onChange={(event) => updateRow(index, { value: event.target.value })}
212
+ autoComplete="off"
213
+ spellCheck={false}
214
+ className="flex-1"
215
+ />
216
+ {isSecret && (
217
+ <Button
218
+ data-slot="key-value-editor-reveal"
219
+ type="button"
220
+ variant="ghost"
221
+ size="icon-sm"
222
+ disabled={disabled}
223
+ aria-label={t(
224
+ rowRevealed ? "ui.keyValueEditor.hide" : "ui.keyValueEditor.reveal",
225
+ { n: index + 1 },
226
+ )}
227
+ onClick={() => toggleReveal(index, row)}
228
+ >
229
+ {rowRevealed ? <EyeOff aria-hidden="true" /> : <Eye aria-hidden="true" />}
230
+ </Button>
231
+ )}
232
+ <Button
233
+ data-slot="key-value-editor-remove"
234
+ type="button"
235
+ variant="ghost"
236
+ size="icon-sm"
237
+ disabled={disabled}
238
+ aria-label={t("ui.keyValueEditor.removeRow", { n: index + 1 })}
239
+ onClick={() => removeRow(index)}
240
+ >
241
+ <X aria-hidden="true" />
242
+ </Button>
243
+ </div>
244
+ );
245
+ })}
246
+ </div>
247
+ )}
248
+ <Button
249
+ data-slot="key-value-editor-add"
250
+ type="button"
251
+ variant="outline-subtle"
252
+ size="sm"
253
+ disabled={disabled}
254
+ onClick={addRow}
255
+ className="self-start"
256
+ >
257
+ <Plus aria-hidden="true" />
258
+ {addLabel ?? t("ui.keyValueEditor.addRow")}
259
+ </Button>
260
+ </div>
261
+ );
262
+ },
263
+ );
@@ -0,0 +1 @@
1
+ export { Label } from "./label";
@@ -0,0 +1,35 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Label } from "./label";
3
+ import { Input } from "../input";
4
+ const meta = {
5
+ title: "Forms/Label",
6
+ component: Label,
7
+ tags: ["autodocs"],
8
+ argTypes: {
9
+ htmlFor: {
10
+ description: "Associates the label with an input by its `id`.",
11
+ control: "text",
12
+ table: { category: "Behavior" },
13
+ },
14
+ children: {
15
+ description: "Label text content.",
16
+ control: false,
17
+ table: { category: "Content" },
18
+ },
19
+ className: {
20
+ description: "Additional CSS classes applied to the label element.",
21
+ control: "text",
22
+ table: { category: "Appearance" },
23
+ },
24
+ },
25
+ } satisfies Meta<typeof Label>;
26
+ export default meta;
27
+ type Story = StoryObj<typeof meta>;
28
+ export const Default: Story = {
29
+ render: () => (
30
+ <div className="grid gap-1.5">
31
+ <Label htmlFor="email">Email</Label>
32
+ <Input id="email" placeholder="you@example.com" />
33
+ </div>
34
+ ),
35
+ };
@@ -0,0 +1,19 @@
1
+ import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
2
+ import * as LabelPrimitive from "@radix-ui/react-label";
3
+ import { cn } from "../../lib/cn";
4
+
5
+ export const Label = forwardRef<
6
+ ElementRef<typeof LabelPrimitive.Root>,
7
+ ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
8
+ >(function Label({ className, ...props }, ref) {
9
+ return (
10
+ <LabelPrimitive.Root
11
+ ref={ref}
12
+ className={cn(
13
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
14
+ className,
15
+ )}
16
+ {...props}
17
+ />
18
+ );
19
+ });
@@ -0,0 +1,2 @@
1
+ export { LinkPreview, LinkPreviewCard } from "./link-preview";
2
+ export type { LinkPreviewCardProps } from "./link-preview";