@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.
- package/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/index.d.ts +4497 -0
- package/dist/index.js +15192 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/cn.d.ts +10 -0
- package/dist/lib/cn.js +20 -0
- package/dist/lib/cn.js.map +1 -0
- package/package.json +106 -0
- package/src/blocks/sidebar-02/app-sidebar.tsx +160 -0
- package/src/blocks/sidebar-02/logo.tsx +18 -0
- package/src/blocks/sidebar-02/nav-main.tsx +13 -0
- package/src/blocks/sidebar-02/nav-notifications.tsx +7 -0
- package/src/blocks/sidebar-02/sidebar-02.stories.tsx +23 -0
- package/src/blocks/sidebar-02/team-switcher.tsx +7 -0
- package/src/blocks/sidebar-04/app-sidebar.tsx +236 -0
- package/src/blocks/sidebar-04/mail-context.tsx +29 -0
- package/src/blocks/sidebar-04/nav-user.tsx +6 -0
- package/src/blocks/sidebar-04/sidebar-04.stories.tsx +150 -0
- package/src/blocks/sidebar-05/app-sidebar.tsx +375 -0
- package/src/blocks/sidebar-05/sidebar-05.stories.tsx +18 -0
- package/src/blocks/sidebar-05/team-switcher.tsx +6 -0
- package/src/blocks-comparison-table.stories.tsx +165 -0
- package/src/components/accordion/accordion.stories.tsx +71 -0
- package/src/components/accordion/accordion.test.tsx +18 -0
- package/src/components/accordion/accordion.tsx +49 -0
- package/src/components/accordion/index.ts +1 -0
- package/src/components/advanced-group/advanced-group.stories.tsx +143 -0
- package/src/components/advanced-group/advanced-group.test.tsx +81 -0
- package/src/components/advanced-group/advanced-group.tsx +111 -0
- package/src/components/advanced-group/index.ts +2 -0
- package/src/components/alert/alert.stories.tsx +156 -0
- package/src/components/alert/alert.test.tsx +57 -0
- package/src/components/alert/alert.tsx +91 -0
- package/src/components/alert/index.ts +8 -0
- package/src/components/alert-dialog/alert-dialog.stories.tsx +73 -0
- package/src/components/alert-dialog/alert-dialog.tsx +104 -0
- package/src/components/alert-dialog/index.ts +12 -0
- package/src/components/app-shell/app-shell.stories.tsx +61 -0
- package/src/components/app-shell/app-shell.tsx +31 -0
- package/src/components/app-shell/index.ts +1 -0
- package/src/components/app-sidebar/app-sidebar.stories.tsx +121 -0
- package/src/components/app-sidebar/app-sidebar.test.tsx +55 -0
- package/src/components/app-sidebar/app-sidebar.tsx +53 -0
- package/src/components/app-sidebar/index.ts +1 -0
- package/src/components/aspect-ratio/aspect-ratio.stories.tsx +46 -0
- package/src/components/aspect-ratio/aspect-ratio.tsx +3 -0
- package/src/components/aspect-ratio/index.ts +1 -0
- package/src/components/attribution-panel/attribution-panel.stories.tsx +83 -0
- package/src/components/attribution-panel/attribution-panel.test.tsx +123 -0
- package/src/components/attribution-panel/attribution-panel.tsx +191 -0
- package/src/components/attribution-panel/attribution-types.ts +44 -0
- package/src/components/attribution-panel/attributions.generated.ts +1422 -0
- package/src/components/attribution-panel/index.ts +3 -0
- package/src/components/avatar/avatar.stories.tsx +45 -0
- package/src/components/avatar/avatar.test.tsx +13 -0
- package/src/components/avatar/avatar.tsx +46 -0
- package/src/components/avatar/index.ts +1 -0
- package/src/components/badge/badge.stories.tsx +44 -0
- package/src/components/badge/badge.test.tsx +10 -0
- package/src/components/badge/badge.tsx +28 -0
- package/src/components/badge/index.ts +1 -0
- package/src/components/bento-grid/bento-grid.stories.tsx +165 -0
- package/src/components/bento-grid/bento-grid.test.tsx +119 -0
- package/src/components/bento-grid/bento-grid.tsx +339 -0
- package/src/components/bento-grid/index.ts +9 -0
- package/src/components/bounded-number/bounded-number.stories.tsx +118 -0
- package/src/components/bounded-number/bounded-number.test.tsx +73 -0
- package/src/components/bounded-number/bounded-number.tsx +110 -0
- package/src/components/bounded-number/index.ts +1 -0
- package/src/components/breadcrumb/breadcrumb.stories.tsx +47 -0
- package/src/components/breadcrumb/breadcrumb.test.tsx +17 -0
- package/src/components/breadcrumb/breadcrumb.tsx +86 -0
- package/src/components/breadcrumb/index.ts +9 -0
- package/src/components/button/button.stories.tsx +70 -0
- package/src/components/button/button.test.tsx +50 -0
- package/src/components/button/button.tsx +63 -0
- package/src/components/button/index.ts +1 -0
- package/src/components/button-group/button-group.stories.tsx +45 -0
- package/src/components/button-group/button-group.tsx +70 -0
- package/src/components/button-group/index.ts +6 -0
- package/src/components/calendar/calendar.stories.tsx +95 -0
- package/src/components/calendar/calendar.test.tsx +140 -0
- package/src/components/calendar/calendar.tsx +70 -0
- package/src/components/calendar/index.ts +1 -0
- package/src/components/card/card.stories.tsx +438 -0
- package/src/components/card/card.test.tsx +153 -0
- package/src/components/card/card.tsx +332 -0
- package/src/components/card/index.ts +13 -0
- package/src/components/carousel/carousel.stories.tsx +102 -0
- package/src/components/carousel/carousel.tsx +225 -0
- package/src/components/carousel/index.ts +8 -0
- package/src/components/change-review/change-review.stories.tsx +288 -0
- package/src/components/change-review/change-review.test.tsx +261 -0
- package/src/components/change-review/change-review.tsx +637 -0
- package/src/components/change-review/index.ts +23 -0
- package/src/components/checkbox/checkbox.stories.tsx +119 -0
- package/src/components/checkbox/checkbox.test.tsx +40 -0
- package/src/components/checkbox/checkbox.tsx +34 -0
- package/src/components/checkbox/index.ts +1 -0
- package/src/components/collapsible/collapsible.stories.tsx +62 -0
- package/src/components/collapsible/collapsible.tsx +22 -0
- package/src/components/collapsible/index.ts +1 -0
- package/src/components/collapsible-panel/index.ts +8 -0
- package/src/components/collapsible-panel/use-collapsible-panel.test.tsx +93 -0
- package/src/components/collapsible-panel/use-collapsible-panel.ts +159 -0
- package/src/components/color-picker/color-picker.stories.tsx +93 -0
- package/src/components/color-picker/color-picker.test.tsx +79 -0
- package/src/components/color-picker/color-picker.tsx +341 -0
- package/src/components/color-picker/index.ts +1 -0
- package/src/components/combobox/combobox.stories.tsx +154 -0
- package/src/components/combobox/combobox.test.tsx +246 -0
- package/src/components/combobox/combobox.tsx +210 -0
- package/src/components/combobox/index.ts +1 -0
- package/src/components/command/command.stories.tsx +371 -0
- package/src/components/command/command.test.tsx +280 -0
- package/src/components/command/command.tsx +301 -0
- package/src/components/command/index.ts +13 -0
- package/src/components/confirm-dialog/confirm-dialog.stories.tsx +183 -0
- package/src/components/confirm-dialog/confirm-dialog.test.tsx +147 -0
- package/src/components/confirm-dialog/confirm-dialog.tsx +146 -0
- package/src/components/confirm-dialog/index.ts +2 -0
- package/src/components/context-menu/context-menu.stories.tsx +70 -0
- package/src/components/context-menu/context-menu.tsx +172 -0
- package/src/components/context-menu/index.ts +16 -0
- package/src/components/copyable-value/copyable-value.stories.tsx +80 -0
- package/src/components/copyable-value/copyable-value.test.tsx +128 -0
- package/src/components/copyable-value/copyable-value.tsx +101 -0
- package/src/components/copyable-value/index.ts +1 -0
- package/src/components/date-picker/date-picker.stories.tsx +42 -0
- package/src/components/date-picker/date-picker.tsx +49 -0
- package/src/components/date-picker/index.ts +1 -0
- package/src/components/date-range-picker/date-range-picker.stories.tsx +151 -0
- package/src/components/date-range-picker/date-range-picker.test.tsx +62 -0
- package/src/components/date-range-picker/date-range-picker.tsx +126 -0
- package/src/components/date-range-picker/index.ts +6 -0
- package/src/components/descriptions/descriptions.stories.tsx +43 -0
- package/src/components/descriptions/descriptions.test.tsx +41 -0
- package/src/components/descriptions/descriptions.tsx +95 -0
- package/src/components/descriptions/index.ts +6 -0
- package/src/components/dialog/dialog.stories.tsx +728 -0
- package/src/components/dialog/dialog.test.tsx +285 -0
- package/src/components/dialog/dialog.tsx +340 -0
- package/src/components/dialog/index.ts +18 -0
- package/src/components/dialog/use-dialog-dismiss-guard.test.tsx +80 -0
- package/src/components/dialog/use-dialog-dismiss-guard.ts +109 -0
- package/src/components/drawer/drawer.stories.tsx +76 -0
- package/src/components/drawer/drawer.tsx +85 -0
- package/src/components/drawer/index.ts +11 -0
- package/src/components/dropdown-menu/dropdown-menu.stories.tsx +176 -0
- package/src/components/dropdown-menu/dropdown-menu.test.tsx +109 -0
- package/src/components/dropdown-menu/dropdown-menu.tsx +179 -0
- package/src/components/dropdown-menu/index.ts +17 -0
- package/src/components/empty-state/empty-state.stories.tsx +54 -0
- package/src/components/empty-state/empty-state.test.tsx +11 -0
- package/src/components/empty-state/empty-state.tsx +30 -0
- package/src/components/empty-state/index.ts +1 -0
- package/src/components/error-state/error-state.stories.tsx +51 -0
- package/src/components/error-state/error-state.tsx +27 -0
- package/src/components/error-state/index.ts +1 -0
- package/src/components/expand-dialog/expand-dialog.stories.tsx +246 -0
- package/src/components/expand-dialog/expand-dialog.test.tsx +185 -0
- package/src/components/expand-dialog/expand-dialog.tsx +387 -0
- package/src/components/expand-dialog/index.ts +11 -0
- package/src/components/field-row/field-row.stories.tsx +114 -0
- package/src/components/field-row/field-row.test.tsx +188 -0
- package/src/components/field-row/field-row.tsx +115 -0
- package/src/components/field-row/index.ts +1 -0
- package/src/components/file-upload/file-upload.stories.tsx +183 -0
- package/src/components/file-upload/file-upload.test.tsx +169 -0
- package/src/components/file-upload/file-upload.tsx +459 -0
- package/src/components/file-upload/index.ts +13 -0
- package/src/components/form/form.stories.tsx +71 -0
- package/src/components/form/form.tsx +141 -0
- package/src/components/form/index.ts +10 -0
- package/src/components/hover-card/hover-card.stories.tsx +57 -0
- package/src/components/hover-card/hover-card.tsx +25 -0
- package/src/components/hover-card/index.ts +1 -0
- package/src/components/icon-button/icon-button.stories.tsx +108 -0
- package/src/components/icon-button/icon-button.test.tsx +93 -0
- package/src/components/icon-button/icon-button.tsx +139 -0
- package/src/components/icon-button/index.ts +1 -0
- package/src/components/input/index.ts +1 -0
- package/src/components/input/input.stories.tsx +44 -0
- package/src/components/input/input.test.tsx +19 -0
- package/src/components/input/input.tsx +26 -0
- package/src/components/input-group/index.ts +11 -0
- package/src/components/input-group/input-group.stories.tsx +87 -0
- package/src/components/input-group/input-group.test.tsx +48 -0
- package/src/components/input-group/input-group.tsx +170 -0
- package/src/components/input-otp/index.ts +1 -0
- package/src/components/input-otp/input-otp.stories.tsx +66 -0
- package/src/components/input-otp/input-otp.tsx +68 -0
- package/src/components/kbd/index.ts +1 -0
- package/src/components/kbd/kbd.stories.tsx +45 -0
- package/src/components/kbd/kbd.tsx +20 -0
- package/src/components/key-value-editor/index.ts +1 -0
- package/src/components/key-value-editor/key-value-editor.stories.tsx +130 -0
- package/src/components/key-value-editor/key-value-editor.test.tsx +231 -0
- package/src/components/key-value-editor/key-value-editor.tsx +263 -0
- package/src/components/label/index.ts +1 -0
- package/src/components/label/label.stories.tsx +35 -0
- package/src/components/label/label.tsx +19 -0
- package/src/components/link-preview/index.ts +2 -0
- package/src/components/link-preview/link-preview.stories.tsx +178 -0
- package/src/components/link-preview/link-preview.test.tsx +123 -0
- package/src/components/link-preview/link-preview.tsx +194 -0
- package/src/components/list-editor/index.ts +1 -0
- package/src/components/list-editor/list-editor.stories.tsx +119 -0
- package/src/components/list-editor/list-editor.test.tsx +96 -0
- package/src/components/list-editor/list-editor.tsx +192 -0
- package/src/components/loading-state/index.ts +1 -0
- package/src/components/loading-state/loading-state.stories.tsx +38 -0
- package/src/components/loading-state/loading-state.tsx +26 -0
- package/src/components/locale-provider/index.ts +8 -0
- package/src/components/locale-provider/locale-provider.stories.tsx +83 -0
- package/src/components/locale-provider/locale-provider.test.tsx +238 -0
- package/src/components/locale-provider/locale-provider.tsx +187 -0
- package/src/components/locale-provider/messages.ts +300 -0
- package/src/components/match-highlight/index.ts +7 -0
- package/src/components/match-highlight/match-highlight.stories.tsx +146 -0
- package/src/components/match-highlight/match-highlight.test.tsx +202 -0
- package/src/components/match-highlight/match-highlight.tsx +145 -0
- package/src/components/mention-input/index.ts +28 -0
- package/src/components/mention-input/mention-input-mirror.stories.tsx +171 -0
- package/src/components/mention-input/mention-input.stories.tsx +463 -0
- package/src/components/mention-input/mention-input.test.tsx +617 -0
- package/src/components/mention-input/mention-input.tsx +1102 -0
- package/src/components/mention-input/mention-value.test.ts +253 -0
- package/src/components/mention-input/mention-value.ts +193 -0
- package/src/components/menubar/index.ts +17 -0
- package/src/components/menubar/menubar.stories.tsx +67 -0
- package/src/components/menubar/menubar.tsx +198 -0
- package/src/components/metric-card/index.ts +1 -0
- package/src/components/metric-card/metric-card.stories.tsx +224 -0
- package/src/components/metric-card/metric-card.test.tsx +198 -0
- package/src/components/metric-card/metric-card.tsx +229 -0
- package/src/components/model-picker/index.ts +11 -0
- package/src/components/model-picker/model-picker-state.ts +85 -0
- package/src/components/model-picker/model-picker.stories.tsx +128 -0
- package/src/components/model-picker/model-picker.test.tsx +241 -0
- package/src/components/model-picker/model-picker.tsx +270 -0
- package/src/components/nav-main/index.ts +1 -0
- package/src/components/nav-main/nav-main.tsx +147 -0
- package/src/components/nav-notifications/index.ts +5 -0
- package/src/components/nav-notifications/nav-notifications.test.tsx +15 -0
- package/src/components/nav-notifications/nav-notifications.tsx +70 -0
- package/src/components/nav-user/index.ts +1 -0
- package/src/components/nav-user/nav-user.test.tsx +29 -0
- package/src/components/nav-user/nav-user.tsx +114 -0
- package/src/components/navigation-menu/index.ts +10 -0
- package/src/components/navigation-menu/navigation-menu.stories.tsx +92 -0
- package/src/components/navigation-menu/navigation-menu.tsx +99 -0
- package/src/components/number-input/index.ts +1 -0
- package/src/components/number-input/number-input.stories.tsx +120 -0
- package/src/components/number-input/number-input.test.tsx +96 -0
- package/src/components/number-input/number-input.tsx +219 -0
- package/src/components/page-shell/index.ts +1 -0
- package/src/components/page-shell/page-shell.stories.tsx +105 -0
- package/src/components/page-shell/page-shell.test.tsx +57 -0
- package/src/components/page-shell/page-shell.tsx +62 -0
- package/src/components/pagination/index.ts +9 -0
- package/src/components/pagination/pagination.stories.tsx +57 -0
- package/src/components/pagination/pagination.tsx +77 -0
- package/src/components/popover/index.ts +1 -0
- package/src/components/popover/popover.stories.tsx +28 -0
- package/src/components/popover/popover.test.tsx +16 -0
- package/src/components/popover/popover.tsx +29 -0
- package/src/components/progress/index.ts +1 -0
- package/src/components/progress/progress.stories.tsx +80 -0
- package/src/components/progress/progress.test.tsx +35 -0
- package/src/components/progress/progress.tsx +50 -0
- package/src/components/radio-group/index.ts +1 -0
- package/src/components/radio-group/radio-group.stories.tsx +75 -0
- package/src/components/radio-group/radio-group.test.tsx +16 -0
- package/src/components/radio-group/radio-group.tsx +33 -0
- package/src/components/rating/index.ts +1 -0
- package/src/components/rating/rating.stories.tsx +111 -0
- package/src/components/rating/rating.test.tsx +41 -0
- package/src/components/rating/rating.tsx +226 -0
- package/src/components/resizable/index.ts +1 -0
- package/src/components/resizable/resizable.stories.tsx +25 -0
- package/src/components/resizable/resizable.tsx +40 -0
- package/src/components/reveal/index.ts +7 -0
- package/src/components/reveal/reveal.stories.tsx +90 -0
- package/src/components/reveal/reveal.test.tsx +43 -0
- package/src/components/reveal/reveal.tsx +95 -0
- package/src/components/revision-timeline/index.ts +10 -0
- package/src/components/revision-timeline/revision-timeline.stories.tsx +785 -0
- package/src/components/revision-timeline/revision-timeline.test.tsx +509 -0
- package/src/components/revision-timeline/revision-timeline.tsx +1107 -0
- package/src/components/scroll-area/index.ts +1 -0
- package/src/components/scroll-area/scroll-area.stories.tsx +45 -0
- package/src/components/scroll-area/scroll-area.tsx +43 -0
- package/src/components/section-header/index.ts +1 -0
- package/src/components/section-header/section-header.stories.tsx +20 -0
- package/src/components/section-header/section-header.tsx +36 -0
- package/src/components/segmented-field/index.ts +5 -0
- package/src/components/segmented-field/segmented-field.stories.tsx +121 -0
- package/src/components/segmented-field/segmented-field.test.tsx +163 -0
- package/src/components/segmented-field/segmented-field.tsx +235 -0
- package/src/components/select/index.ts +10 -0
- package/src/components/select/select.stories.tsx +106 -0
- package/src/components/select/select.test.tsx +98 -0
- package/src/components/select/select.tsx +230 -0
- package/src/components/separator/index.ts +2 -0
- package/src/components/separator/separator.stories.tsx +87 -0
- package/src/components/separator/separator.tsx +51 -0
- package/src/components/sheet/index.ts +12 -0
- package/src/components/sheet/sheet.stories.tsx +65 -0
- package/src/components/sheet/sheet.tsx +109 -0
- package/src/components/sidebar/index.ts +27 -0
- package/src/components/sidebar/sidebar.stories.tsx +69 -0
- package/src/components/sidebar/sidebar.test.tsx +43 -0
- package/src/components/sidebar/sidebar.tsx +688 -0
- package/src/components/skeleton/index.ts +1 -0
- package/src/components/skeleton/skeleton.stories.tsx +28 -0
- package/src/components/skeleton/skeleton.tsx +15 -0
- package/src/components/slider/index.ts +1 -0
- package/src/components/slider/slider.stories.tsx +211 -0
- package/src/components/slider/slider.test.tsx +168 -0
- package/src/components/slider/slider.tsx +136 -0
- package/src/components/slider-number/index.ts +1 -0
- package/src/components/slider-number/slider-number.stories.tsx +123 -0
- package/src/components/slider-number/slider-number.test.tsx +165 -0
- package/src/components/slider-number/slider-number.tsx +191 -0
- package/src/components/sonner/index.ts +1 -0
- package/src/components/sonner/sonner.stories.tsx +124 -0
- package/src/components/sonner/sonner.test.tsx +76 -0
- package/src/components/sonner/sonner.tsx +101 -0
- package/src/components/spinner/index.ts +1 -0
- package/src/components/spinner/spinner.stories.tsx +27 -0
- package/src/components/spinner/spinner.tsx +19 -0
- package/src/components/split-panel/index.ts +1 -0
- package/src/components/split-panel/split-panel.stories.tsx +60 -0
- package/src/components/split-panel/split-panel.test.tsx +38 -0
- package/src/components/split-panel/split-panel.tsx +95 -0
- package/src/components/state-panel/index.ts +1 -0
- package/src/components/state-panel/state-panel.stories.tsx +80 -0
- package/src/components/state-panel/state-panel.test.tsx +64 -0
- package/src/components/state-panel/state-panel.tsx +167 -0
- package/src/components/status-badge/index.ts +18 -0
- package/src/components/status-badge/status-badge.stories.tsx +132 -0
- package/src/components/status-badge/status-badge.test.tsx +189 -0
- package/src/components/status-badge/status-badge.tsx +330 -0
- package/src/components/switch/index.ts +1 -0
- package/src/components/switch/switch.stories.tsx +58 -0
- package/src/components/switch/switch.test.tsx +12 -0
- package/src/components/switch/switch.tsx +29 -0
- package/src/components/table/index.ts +10 -0
- package/src/components/table/table.stories.tsx +53 -0
- package/src/components/table/table.test.tsx +17 -0
- package/src/components/table/table.tsx +106 -0
- package/src/components/tabs/index.ts +1 -0
- package/src/components/tabs/tabs.stories.tsx +267 -0
- package/src/components/tabs/tabs.test.tsx +94 -0
- package/src/components/tabs/tabs.tsx +170 -0
- package/src/components/tag-input/index.ts +1 -0
- package/src/components/tag-input/tag-input.stories.tsx +120 -0
- package/src/components/tag-input/tag-input.test.tsx +97 -0
- package/src/components/tag-input/tag-input.tsx +253 -0
- package/src/components/team-switcher/index.ts +1 -0
- package/src/components/team-switcher/team-switcher.test.tsx +39 -0
- package/src/components/team-switcher/team-switcher.tsx +100 -0
- package/src/components/textarea/index.ts +1 -0
- package/src/components/textarea/textarea.stories.tsx +42 -0
- package/src/components/textarea/textarea.tsx +24 -0
- package/src/components/theme-switcher/index.ts +2 -0
- package/src/components/theme-switcher/theme-switcher.stories.tsx +146 -0
- package/src/components/theme-switcher/theme-switcher.test.tsx +253 -0
- package/src/components/theme-switcher/theme-switcher.tsx +280 -0
- package/src/components/theme-switcher/use-theme-transition.ts +62 -0
- package/src/components/timeline/index.ts +13 -0
- package/src/components/timeline/timeline.stories.tsx +99 -0
- package/src/components/timeline/timeline.test.tsx +196 -0
- package/src/components/timeline/timeline.tsx +188 -0
- package/src/components/toggle/index.ts +1 -0
- package/src/components/toggle/toggle.stories.tsx +116 -0
- package/src/components/toggle/toggle.test.tsx +119 -0
- package/src/components/toggle/toggle.tsx +64 -0
- package/src/components/toggle-group/index.ts +1 -0
- package/src/components/toggle-group/toggle-group.stories.tsx +110 -0
- package/src/components/toggle-group/toggle-group.tsx +59 -0
- package/src/components/toolbar/index.ts +14 -0
- package/src/components/toolbar/toolbar.stories.tsx +126 -0
- package/src/components/toolbar/toolbar.test.tsx +73 -0
- package/src/components/toolbar/toolbar.tsx +177 -0
- package/src/components/tooltip/index.ts +1 -0
- package/src/components/tooltip/tooltip.stories.tsx +67 -0
- package/src/components/tooltip/tooltip.tsx +27 -0
- package/src/components/top-nav/index.ts +1 -0
- package/src/components/top-nav/top-nav.stories.tsx +18 -0
- package/src/components/top-nav/top-nav.tsx +34 -0
- package/src/components/transfer/index.ts +2 -0
- package/src/components/transfer/transfer.stories.tsx +162 -0
- package/src/components/transfer/transfer.test.tsx +177 -0
- package/src/components/transfer/transfer.tsx +481 -0
- package/src/components/tree/index.ts +6 -0
- package/src/components/tree/tree.stories.tsx +618 -0
- package/src/components/tree/tree.test.tsx +699 -0
- package/src/components/tree/tree.tsx +1149 -0
- package/src/components/tree/use-tree-keyboard.ts +314 -0
- package/src/components/tree-select/index.ts +1 -0
- package/src/components/tree-select/tree-select.stories.tsx +127 -0
- package/src/components/tree-select/tree-select.test.tsx +40 -0
- package/src/components/tree-select/tree-select.tsx +137 -0
- package/src/components/typography/index.ts +31 -0
- package/src/components/typography/prose.test.tsx +48 -0
- package/src/components/typography/prose.tsx +194 -0
- package/src/components/typography/typography.stories.tsx +244 -0
- package/src/components/typography/typography.test.tsx +140 -0
- package/src/components/typography/typography.tsx +126 -0
- package/src/components/view-toolbar/index.ts +10 -0
- package/src/components/view-toolbar/view-toolbar.stories.tsx +342 -0
- package/src/components/view-toolbar/view-toolbar.test.tsx +262 -0
- package/src/components/view-toolbar/view-toolbar.tsx +320 -0
- package/src/components/virtual-select/index.ts +2 -0
- package/src/components/virtual-select/use-virtual-listbox.ts +132 -0
- package/src/components/virtual-select/virtual-select.stories.tsx +203 -0
- package/src/components/virtual-select/virtual-select.test.tsx +106 -0
- package/src/components/virtual-select/virtual-select.tsx +361 -0
- package/src/components/wizard/index.ts +12 -0
- package/src/components/wizard/wizard.stories.tsx +148 -0
- package/src/components/wizard/wizard.test.tsx +67 -0
- package/src/components/wizard/wizard.tsx +354 -0
- package/src/decoration.stories.tsx +236 -0
- package/src/index.ts +174 -0
- package/src/lib/cn.test.ts +39 -0
- package/src/lib/cn.ts +31 -0
- package/src/lib/compact-number.ts +61 -0
- package/src/lib/document-address.test.ts +75 -0
- package/src/lib/document-address.ts +265 -0
- package/src/lib/download.ts +93 -0
- package/src/lib/file-icon.test.ts +45 -0
- package/src/lib/file-icon.ts +58 -0
- package/src/lib/file-kind.test.ts +98 -0
- package/src/lib/file-kind.ts +303 -0
- package/src/lib/file-source.test.ts +217 -0
- package/src/lib/file-source.ts +269 -0
- package/src/lib/merge-refs.test.ts +37 -0
- package/src/lib/merge-refs.ts +17 -0
- package/src/lib/streamdown-translations.ts +99 -0
- package/src/lib/use-copy-to-clipboard.ts +75 -0
- package/src/lib/use-mobile.ts +16 -0
- package/src/motion.stories.tsx +266 -0
- package/src/taste-profile.test.tsx +252 -0
- package/src/templates-object-detail-hub.stories.tsx +466 -0
- package/src/templates-screen-states.stories.tsx +306 -0
- 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
|
+
});
|