@hypoth-ui/cli 0.0.1 → 0.1.1
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 +19 -115
- package/dist/{add-PDBC4JTE.js → add-V5PW73GC.js} +29 -17
- package/dist/{chunk-5LTQ2XVL.js → chunk-27CLUUVC.js} +0 -2
- package/dist/{chunk-YPKFYE45.js → chunk-NWIRSZUQ.js} +6 -13
- package/dist/{chunk-GJ6JOQ3Q.js → chunk-PBK72SJJ.js} +1 -1
- package/dist/{diff-BQEXG7HU.js → diff-776UATCA.js} +2 -2
- package/dist/index.js +5 -5
- package/dist/{init-7AZXYAPJ.js → init-GDU2PW7K.js} +10 -13
- package/dist/{list-X6ZLM2NQ.js → list-XDP5I537.js} +3 -3
- package/package.json +16 -12
- package/registry/components.json +1820 -206
- package/templates/accordion/index.tsx +266 -0
- package/templates/accordion/wc/accordion-content.ts +113 -0
- package/templates/accordion/wc/accordion-item.ts +111 -0
- package/templates/accordion/wc/accordion-trigger.ts +105 -0
- package/templates/accordion/wc/accordion.ts +213 -0
- package/templates/accordion/wc/index.ts +12 -0
- package/templates/alert/index.tsx +177 -0
- package/templates/alert/wc/alert.ts +167 -0
- package/templates/alert/wc/index.ts +1 -0
- package/templates/alert-dialog/index.tsx +360 -0
- package/templates/alert-dialog/wc/alert-dialog-action.ts +43 -0
- package/templates/alert-dialog/wc/alert-dialog-cancel.ts +43 -0
- package/templates/alert-dialog/wc/alert-dialog-content.ts +42 -0
- package/templates/alert-dialog/wc/alert-dialog-description.ts +34 -0
- package/templates/alert-dialog/wc/alert-dialog-footer.ts +25 -0
- package/templates/alert-dialog/wc/alert-dialog-header.ts +25 -0
- package/templates/alert-dialog/wc/alert-dialog-title.ts +34 -0
- package/templates/alert-dialog/wc/alert-dialog-trigger.ts +46 -0
- package/templates/alert-dialog/wc/alert-dialog.ts +302 -0
- package/templates/alert-dialog/wc/index.ts +13 -0
- package/templates/aspect-ratio/index.tsx +50 -0
- package/templates/aspect-ratio/wc/aspect-ratio.ts +78 -0
- package/templates/aspect-ratio/wc/index.ts +5 -0
- package/templates/avatar/avatar-group.tsx +88 -0
- package/templates/avatar/avatar.tsx +124 -0
- package/templates/avatar/index.tsx +33 -0
- package/templates/avatar/wc/avatar-group.ts +112 -0
- package/templates/avatar/wc/avatar.ts +184 -0
- package/templates/avatar/wc/index.ts +5 -0
- package/templates/badge/index.tsx +140 -0
- package/templates/badge/wc/badge.ts +119 -0
- package/templates/badge/wc/index.ts +9 -0
- package/templates/breadcrumb/index.tsx +157 -0
- package/templates/breadcrumb/wc/breadcrumb-item.ts +30 -0
- package/templates/breadcrumb/wc/breadcrumb-link.ts +70 -0
- package/templates/breadcrumb/wc/breadcrumb-list.ts +30 -0
- package/templates/breadcrumb/wc/breadcrumb-page.ts +32 -0
- package/templates/breadcrumb/wc/breadcrumb-separator.ts +31 -0
- package/templates/breadcrumb/wc/breadcrumb.ts +55 -0
- package/templates/breadcrumb/wc/index.ts +10 -0
- package/templates/button/button.tsx +119 -0
- package/templates/button/index.ts +1 -0
- package/templates/button/wc/button.ts +169 -0
- package/templates/calendar/index.tsx +149 -0
- package/templates/calendar/wc/calendar.ts +316 -0
- package/templates/calendar/wc/index.ts +4 -0
- package/templates/card/index.tsx +108 -0
- package/templates/card/wc/card-content.ts +25 -0
- package/templates/card/wc/card-footer.ts +25 -0
- package/templates/card/wc/card-header.ts +25 -0
- package/templates/card/wc/card.ts +43 -0
- package/templates/card/wc/index.ts +8 -0
- package/templates/checkbox/checkbox.tsx +85 -0
- package/templates/checkbox/wc/checkbox.ts +247 -0
- package/templates/collapsible/index.tsx +172 -0
- package/templates/collapsible/wc/collapsible-content.ts +97 -0
- package/templates/collapsible/wc/collapsible-trigger.ts +39 -0
- package/templates/collapsible/wc/collapsible.ts +143 -0
- package/templates/collapsible/wc/index.ts +7 -0
- package/templates/combobox/combobox-content.tsx +141 -0
- package/templates/combobox/combobox-context.ts +36 -0
- package/templates/combobox/combobox-empty.tsx +38 -0
- package/templates/combobox/combobox-input.tsx +159 -0
- package/templates/combobox/combobox-loading.tsx +38 -0
- package/templates/combobox/combobox-option.tsx +99 -0
- package/templates/combobox/combobox-root.tsx +207 -0
- package/templates/combobox/combobox-tag.tsx +62 -0
- package/templates/combobox/index.ts +62 -0
- package/templates/combobox/wc/combobox-content.ts +97 -0
- package/templates/combobox/wc/combobox-input.ts +134 -0
- package/templates/combobox/wc/combobox-option.ts +111 -0
- package/templates/combobox/wc/combobox-tag.ts +103 -0
- package/templates/combobox/wc/combobox.ts +981 -0
- package/templates/combobox/wc/index.ts +5 -0
- package/templates/command/index.tsx +279 -0
- package/templates/command/wc/command-empty.ts +24 -0
- package/templates/command/wc/command-group.ts +60 -0
- package/templates/command/wc/command-input.ts +136 -0
- package/templates/command/wc/command-item.ts +78 -0
- package/templates/command/wc/command-list.ts +103 -0
- package/templates/command/wc/command-loading.ts +24 -0
- package/templates/command/wc/command-separator.ts +23 -0
- package/templates/command/wc/command.ts +176 -0
- package/templates/context-menu/index.tsx +262 -0
- package/templates/context-menu/wc/context-menu-content.ts +41 -0
- package/templates/context-menu/wc/context-menu-item.ts +83 -0
- package/templates/context-menu/wc/context-menu-label.ts +30 -0
- package/templates/context-menu/wc/context-menu-separator.ts +28 -0
- package/templates/context-menu/wc/context-menu.ts +324 -0
- package/templates/context-menu/wc/index.ts +9 -0
- package/templates/data-table/index.tsx +263 -0
- package/templates/data-table/wc/data-table.ts +405 -0
- package/templates/data-table/wc/index.ts +10 -0
- package/templates/date-picker/date-picker-calendar.tsx +352 -0
- package/templates/date-picker/date-picker-content.tsx +121 -0
- package/templates/date-picker/date-picker-context.ts +46 -0
- package/templates/date-picker/date-picker-root.tsx +201 -0
- package/templates/date-picker/date-picker-trigger.tsx +95 -0
- package/templates/date-picker/index.ts +44 -0
- package/templates/date-picker/wc/date-picker-calendar.ts +457 -0
- package/templates/date-picker/wc/date-picker.ts +592 -0
- package/templates/date-picker/wc/date-utils.ts +467 -0
- package/templates/date-picker/wc/index.ts +3 -0
- package/templates/dialog/dialog-close.tsx +57 -0
- package/templates/dialog/dialog-content.tsx +106 -0
- package/templates/dialog/dialog-context.ts +24 -0
- package/templates/dialog/dialog-description.tsx +51 -0
- package/templates/dialog/dialog-root.tsx +104 -0
- package/templates/dialog/dialog-title.tsx +38 -0
- package/templates/dialog/dialog-trigger.tsx +94 -0
- package/templates/dialog/index.ts +52 -0
- package/templates/dialog/wc/dialog-content.ts +59 -0
- package/templates/dialog/wc/dialog-description.ts +58 -0
- package/templates/dialog/wc/dialog-title.ts +56 -0
- package/templates/dialog/wc/dialog.ts +411 -0
- package/templates/drawer/index.tsx +263 -0
- package/templates/drawer/wc/drawer-content.ts +150 -0
- package/templates/drawer/wc/drawer-description.ts +34 -0
- package/templates/drawer/wc/drawer-footer.ts +25 -0
- package/templates/drawer/wc/drawer-header.ts +25 -0
- package/templates/drawer/wc/drawer-title.ts +34 -0
- package/templates/drawer/wc/drawer.ts +348 -0
- package/templates/drawer/wc/index.ts +10 -0
- package/templates/dropdown-menu/index.tsx +454 -0
- package/templates/dropdown-menu/wc/dropdown-menu-checkbox-item.ts +93 -0
- package/templates/dropdown-menu/wc/dropdown-menu-content.ts +43 -0
- package/templates/dropdown-menu/wc/dropdown-menu-item.ts +85 -0
- package/templates/dropdown-menu/wc/dropdown-menu-label.ts +31 -0
- package/templates/dropdown-menu/wc/dropdown-menu-radio-group.ts +80 -0
- package/templates/dropdown-menu/wc/dropdown-menu-radio-item.ts +101 -0
- package/templates/dropdown-menu/wc/dropdown-menu-separator.ts +28 -0
- package/templates/dropdown-menu/wc/dropdown-menu.ts +358 -0
- package/templates/dropdown-menu/wc/index.ts +12 -0
- package/templates/field/field-description.tsx +39 -0
- package/templates/field/field-error.tsx +37 -0
- package/templates/field/field.tsx +46 -0
- package/templates/field/index.ts +4 -0
- package/templates/field/label.tsx +40 -0
- package/templates/field/wc/field-description.ts +42 -0
- package/templates/field/wc/field-error.ts +46 -0
- package/templates/field/wc/field.ts +210 -0
- package/templates/field/wc/label.ts +54 -0
- package/templates/file-upload/file-upload-context.ts +26 -0
- package/templates/file-upload/file-upload-dropzone.tsx +111 -0
- package/templates/file-upload/file-upload-input.tsx +86 -0
- package/templates/file-upload/file-upload-item.tsx +105 -0
- package/templates/file-upload/file-upload-root.tsx +115 -0
- package/templates/file-upload/index.ts +50 -0
- package/templates/file-upload/wc/file-upload.ts +380 -0
- package/templates/file-upload/wc/index.ts +1 -0
- package/templates/hover-card/index.tsx +203 -0
- package/templates/hover-card/wc/hover-card-content.ts +50 -0
- package/templates/hover-card/wc/hover-card.ts +382 -0
- package/templates/hover-card/wc/index.ts +6 -0
- package/templates/icon/icon.tsx +76 -0
- package/templates/icon/wc/icon-adapter.ts +108 -0
- package/templates/icon/wc/icon.ts +161 -0
- package/templates/input/input.tsx +130 -0
- package/templates/input/wc/input.ts +216 -0
- package/templates/layout/app-shell.tsx +177 -0
- package/templates/layout/box.tsx +53 -0
- package/templates/layout/center.tsx +42 -0
- package/templates/layout/container.tsx +43 -0
- package/templates/layout/flow.tsx +83 -0
- package/templates/layout/grid.tsx +79 -0
- package/templates/layout/index.ts +33 -0
- package/templates/layout/inline.tsx +16 -0
- package/templates/layout/page.tsx +43 -0
- package/templates/layout/section.tsx +39 -0
- package/templates/layout/spacer.tsx +30 -0
- package/templates/layout/split.tsx +47 -0
- package/templates/layout/stack.tsx +16 -0
- package/templates/layout/wc/app-shell.ts +58 -0
- package/templates/layout/wc/box.ts +117 -0
- package/templates/layout/wc/center.ts +78 -0
- package/templates/layout/wc/container.ts +77 -0
- package/templates/layout/wc/flow.ts +149 -0
- package/templates/layout/wc/footer.ts +57 -0
- package/templates/layout/wc/grid.ts +142 -0
- package/templates/layout/wc/header.ts +57 -0
- package/templates/layout/wc/index.ts +41 -0
- package/templates/layout/wc/main.ts +46 -0
- package/templates/layout/wc/page.ts +81 -0
- package/templates/layout/wc/section.ts +65 -0
- package/templates/layout/wc/spacer.ts +77 -0
- package/templates/layout/wc/split.ts +94 -0
- package/templates/layout/wc/wrap.ts +93 -0
- package/templates/layout/wrap.tsx +46 -0
- package/templates/link/link.tsx +109 -0
- package/templates/link/wc/link.ts +124 -0
- package/templates/list/index.tsx +55 -0
- package/templates/list/list-item.tsx +117 -0
- package/templates/list/list.tsx +115 -0
- package/templates/list/wc/index.ts +5 -0
- package/templates/list/wc/list-item.ts +127 -0
- package/templates/list/wc/list.ts +114 -0
- package/templates/menu/index.ts +49 -0
- package/templates/menu/menu-content.tsx +109 -0
- package/templates/menu/menu-context.ts +17 -0
- package/templates/menu/menu-item.tsx +108 -0
- package/templates/menu/menu-label.tsx +32 -0
- package/templates/menu/menu-root.tsx +108 -0
- package/templates/menu/menu-separator.tsx +24 -0
- package/templates/menu/menu-trigger.tsx +104 -0
- package/templates/menu/wc/menu-content.ts +67 -0
- package/templates/menu/wc/menu-item.ts +109 -0
- package/templates/menu/wc/menu.ts +449 -0
- package/templates/navigation-menu/index.tsx +328 -0
- package/templates/navigation-menu/wc/index.ts +12 -0
- package/templates/navigation-menu/wc/navigation-menu-content.ts +30 -0
- package/templates/navigation-menu/wc/navigation-menu-indicator.ts +30 -0
- package/templates/navigation-menu/wc/navigation-menu-item.ts +60 -0
- package/templates/navigation-menu/wc/navigation-menu-link.ts +97 -0
- package/templates/navigation-menu/wc/navigation-menu-list.ts +30 -0
- package/templates/navigation-menu/wc/navigation-menu-trigger.ts +110 -0
- package/templates/navigation-menu/wc/navigation-menu-viewport.ts +85 -0
- package/templates/navigation-menu/wc/navigation-menu.ts +272 -0
- package/templates/number-input/index.ts +46 -0
- package/templates/number-input/number-input-context.ts +38 -0
- package/templates/number-input/number-input-decrement.tsx +53 -0
- package/templates/number-input/number-input-field.tsx +93 -0
- package/templates/number-input/number-input-increment.tsx +53 -0
- package/templates/number-input/number-input-root.tsx +137 -0
- package/templates/number-input/wc/index.ts +1 -0
- package/templates/number-input/wc/number-input.ts +283 -0
- package/templates/pagination/index.tsx +198 -0
- package/templates/pagination/wc/index.ts +11 -0
- package/templates/pagination/wc/pagination-content.ts +30 -0
- package/templates/pagination/wc/pagination-ellipsis.ts +28 -0
- package/templates/pagination/wc/pagination-item.ts +30 -0
- package/templates/pagination/wc/pagination-link.ts +76 -0
- package/templates/pagination/wc/pagination-next.ts +69 -0
- package/templates/pagination/wc/pagination-previous.ts +69 -0
- package/templates/pagination/wc/pagination.ts +156 -0
- package/templates/pin-input/index.ts +39 -0
- package/templates/pin-input/pin-input-context.ts +30 -0
- package/templates/pin-input/pin-input-field.tsx +186 -0
- package/templates/pin-input/pin-input-root.tsx +120 -0
- package/templates/pin-input/wc/index.ts +1 -0
- package/templates/pin-input/wc/pin-input.ts +259 -0
- package/templates/popover/popover.tsx +121 -0
- package/templates/popover/wc/popover-content.ts +66 -0
- package/templates/popover/wc/popover.ts +343 -0
- package/templates/progress/index.tsx +117 -0
- package/templates/progress/wc/index.ts +4 -0
- package/templates/progress/wc/progress.ts +174 -0
- package/templates/radio/radio.tsx +43 -0
- package/templates/radio/wc/radio-group.ts +261 -0
- package/templates/radio/wc/radio.ts +145 -0
- package/templates/scroll-area/index.tsx +144 -0
- package/templates/scroll-area/wc/index.ts +8 -0
- package/templates/scroll-area/wc/scroll-area-scrollbar.ts +143 -0
- package/templates/scroll-area/wc/scroll-area-thumb.ts +225 -0
- package/templates/scroll-area/wc/scroll-area-viewport.ts +120 -0
- package/templates/scroll-area/wc/scroll-area.ts +63 -0
- package/templates/select/index.ts +57 -0
- package/templates/select/select-content.tsx +243 -0
- package/templates/select/select-context.ts +30 -0
- package/templates/select/select-group.tsx +53 -0
- package/templates/select/select-label.tsx +34 -0
- package/templates/select/select-option.tsx +97 -0
- package/templates/select/select-root.tsx +153 -0
- package/templates/select/select-separator.tsx +27 -0
- package/templates/select/select-trigger.tsx +112 -0
- package/templates/select/select-value.tsx +48 -0
- package/templates/select/wc/index.ts +6 -0
- package/templates/select/wc/select-content.ts +89 -0
- package/templates/select/wc/select-group.ts +82 -0
- package/templates/select/wc/select-label.ts +49 -0
- package/templates/select/wc/select-option.ts +111 -0
- package/templates/select/wc/select-trigger.ts +101 -0
- package/templates/select/wc/select.ts +840 -0
- package/templates/separator/index.tsx +49 -0
- package/templates/separator/wc/index.ts +5 -0
- package/templates/separator/wc/separator.ts +60 -0
- package/templates/sheet/index.tsx +291 -0
- package/templates/sheet/wc/index.ts +12 -0
- package/templates/sheet/wc/sheet-close.ts +43 -0
- package/templates/sheet/wc/sheet-content.ts +47 -0
- package/templates/sheet/wc/sheet-description.ts +34 -0
- package/templates/sheet/wc/sheet-footer.ts +25 -0
- package/templates/sheet/wc/sheet-header.ts +25 -0
- package/templates/sheet/wc/sheet-overlay.ts +23 -0
- package/templates/sheet/wc/sheet-title.ts +34 -0
- package/templates/sheet/wc/sheet.ts +336 -0
- package/templates/skeleton/index.tsx +131 -0
- package/templates/skeleton/wc/index.ts +10 -0
- package/templates/skeleton/wc/skeleton.ts +107 -0
- package/templates/slider/index.ts +41 -0
- package/templates/slider/slider-context.ts +36 -0
- package/templates/slider/slider-range.tsx +59 -0
- package/templates/slider/slider-root.tsx +166 -0
- package/templates/slider/slider-thumb.tsx +213 -0
- package/templates/slider/slider-track.tsx +113 -0
- package/templates/slider/wc/index.ts +1 -0
- package/templates/slider/wc/slider.ts +465 -0
- package/templates/spinner/spinner.tsx +64 -0
- package/templates/spinner/wc/spinner.ts +70 -0
- package/templates/stepper/index.tsx +230 -0
- package/templates/stepper/wc/index.ts +12 -0
- package/templates/stepper/wc/stepper-content.ts +30 -0
- package/templates/stepper/wc/stepper-description.ts +25 -0
- package/templates/stepper/wc/stepper-indicator.ts +30 -0
- package/templates/stepper/wc/stepper-item.ts +55 -0
- package/templates/stepper/wc/stepper-separator.ts +29 -0
- package/templates/stepper/wc/stepper-title.ts +25 -0
- package/templates/stepper/wc/stepper-trigger.ts +67 -0
- package/templates/stepper/wc/stepper.ts +164 -0
- package/templates/switch/switch.tsx +90 -0
- package/templates/switch/wc/switch.ts +228 -0
- package/templates/table/body.tsx +21 -0
- package/templates/table/cell.tsx +44 -0
- package/templates/table/head.tsx +112 -0
- package/templates/table/header.tsx +21 -0
- package/templates/table/index.tsx +93 -0
- package/templates/table/root.tsx +82 -0
- package/templates/table/row.tsx +36 -0
- package/templates/table/wc/index.ts +9 -0
- package/templates/table/wc/table-body.ts +32 -0
- package/templates/table/wc/table-cell.ts +58 -0
- package/templates/table/wc/table-head.ts +129 -0
- package/templates/table/wc/table-header.ts +32 -0
- package/templates/table/wc/table-row.ts +50 -0
- package/templates/table/wc/table.ts +93 -0
- package/templates/tabs/index.tsx +222 -0
- package/templates/tabs/wc/index.ts +8 -0
- package/templates/tabs/wc/tabs-content.ts +82 -0
- package/templates/tabs/wc/tabs-list.ts +56 -0
- package/templates/tabs/wc/tabs-trigger.ts +136 -0
- package/templates/tabs/wc/tabs.ts +202 -0
- package/templates/tag/index.tsx +186 -0
- package/templates/tag/wc/index.ts +4 -0
- package/templates/tag/wc/tag.ts +166 -0
- package/templates/text/text.tsx +100 -0
- package/templates/text/wc/text.ts +94 -0
- package/templates/textarea/textarea.tsx +134 -0
- package/templates/textarea/wc/textarea.ts +280 -0
- package/templates/time-picker/index.ts +42 -0
- package/templates/time-picker/time-picker-context.ts +28 -0
- package/templates/time-picker/time-picker-root.tsx +113 -0
- package/templates/time-picker/time-picker-segment.tsx +91 -0
- package/templates/time-picker/wc/index.ts +1 -0
- package/templates/time-picker/wc/time-picker.ts +221 -0
- package/templates/toast/index.tsx +71 -0
- package/templates/toast/provider.tsx +228 -0
- package/templates/toast/toast.tsx +142 -0
- package/templates/toast/use-toast.ts +89 -0
- package/templates/toast/wc/index.ts +15 -0
- package/templates/toast/wc/toast-controller.ts +282 -0
- package/templates/toast/wc/toast-provider.ts +161 -0
- package/templates/toast/wc/toast.ts +165 -0
- package/templates/tooltip/tooltip.tsx +62 -0
- package/templates/tooltip/wc/tooltip-content.ts +64 -0
- package/templates/tooltip/wc/tooltip.ts +289 -0
- package/templates/tree/index.tsx +60 -0
- package/templates/tree/tree-item.tsx +131 -0
- package/templates/tree/tree.tsx +138 -0
- package/templates/tree/wc/index.ts +11 -0
- package/templates/tree/wc/tree-item.ts +273 -0
- package/templates/tree/wc/tree-utils.ts +143 -0
- package/templates/tree/wc/tree.ts +139 -0
- package/templates/visually-hidden/visually-hidden.tsx +45 -0
- package/templates/visually-hidden/wc/visually-hidden.ts +64 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collapsible compound component exports.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```tsx
|
|
6
|
+
* import { Collapsible } from "@/components/ui";
|
|
7
|
+
*
|
|
8
|
+
* <Collapsible.Root>
|
|
9
|
+
* <Collapsible.Trigger>Toggle</Collapsible.Trigger>
|
|
10
|
+
* <Collapsible.Content>
|
|
11
|
+
* <p>Collapsible content here.</p>
|
|
12
|
+
* </Collapsible.Content>
|
|
13
|
+
* </Collapsible.Root>
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
type HTMLAttributes,
|
|
19
|
+
type ReactNode,
|
|
20
|
+
createElement,
|
|
21
|
+
forwardRef,
|
|
22
|
+
useCallback,
|
|
23
|
+
useEffect,
|
|
24
|
+
useRef,
|
|
25
|
+
useState,
|
|
26
|
+
} from "react";
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Types
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
export interface CollapsibleRootProps extends HTMLAttributes<HTMLElement> {
|
|
33
|
+
/** Content */
|
|
34
|
+
children?: ReactNode;
|
|
35
|
+
/** Controlled open state */
|
|
36
|
+
open?: boolean;
|
|
37
|
+
/** Default open state (uncontrolled) */
|
|
38
|
+
defaultOpen?: boolean;
|
|
39
|
+
/** Called when open state changes */
|
|
40
|
+
onOpenChange?: (open: boolean) => void;
|
|
41
|
+
/** Disable the collapsible */
|
|
42
|
+
disabled?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CollapsibleTriggerProps extends HTMLAttributes<HTMLElement> {
|
|
46
|
+
/** Trigger content */
|
|
47
|
+
children?: ReactNode;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface CollapsibleContentProps extends HTMLAttributes<HTMLElement> {
|
|
51
|
+
/** Content */
|
|
52
|
+
children?: ReactNode;
|
|
53
|
+
/** Keep mounted when collapsed */
|
|
54
|
+
forceMount?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Components
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Collapsible root component.
|
|
63
|
+
*/
|
|
64
|
+
const CollapsibleRoot = forwardRef<HTMLElement, CollapsibleRootProps>(function CollapsibleRoot(
|
|
65
|
+
{
|
|
66
|
+
children,
|
|
67
|
+
className,
|
|
68
|
+
open: controlledOpen,
|
|
69
|
+
defaultOpen = false,
|
|
70
|
+
onOpenChange,
|
|
71
|
+
disabled = false,
|
|
72
|
+
...props
|
|
73
|
+
},
|
|
74
|
+
ref
|
|
75
|
+
) {
|
|
76
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
77
|
+
const isControlled = controlledOpen !== undefined;
|
|
78
|
+
const open = isControlled ? controlledOpen : internalOpen;
|
|
79
|
+
const elementRef = useRef<HTMLElement>(null);
|
|
80
|
+
|
|
81
|
+
// Combine refs
|
|
82
|
+
const combinedRef = (node: HTMLElement | null) => {
|
|
83
|
+
(elementRef as React.MutableRefObject<HTMLElement | null>).current = node;
|
|
84
|
+
if (typeof ref === "function") {
|
|
85
|
+
ref(node);
|
|
86
|
+
} else if (ref) {
|
|
87
|
+
(ref as React.MutableRefObject<HTMLElement | null>).current = node;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const handleOpenChange = useCallback(
|
|
92
|
+
(event: Event) => {
|
|
93
|
+
const customEvent = event as CustomEvent;
|
|
94
|
+
const isOpen = customEvent.type === "ds:open";
|
|
95
|
+
|
|
96
|
+
if (!isControlled) {
|
|
97
|
+
setInternalOpen(isOpen);
|
|
98
|
+
}
|
|
99
|
+
onOpenChange?.(isOpen);
|
|
100
|
+
},
|
|
101
|
+
[isControlled, onOpenChange]
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Attach event listeners
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
const element = elementRef.current;
|
|
107
|
+
if (!element) return;
|
|
108
|
+
|
|
109
|
+
element.addEventListener("ds:open", handleOpenChange);
|
|
110
|
+
element.addEventListener("ds:close", handleOpenChange);
|
|
111
|
+
|
|
112
|
+
return () => {
|
|
113
|
+
element.removeEventListener("ds:open", handleOpenChange);
|
|
114
|
+
element.removeEventListener("ds:close", handleOpenChange);
|
|
115
|
+
};
|
|
116
|
+
}, [handleOpenChange]);
|
|
117
|
+
|
|
118
|
+
return createElement(
|
|
119
|
+
"ds-collapsible",
|
|
120
|
+
{
|
|
121
|
+
ref: combinedRef,
|
|
122
|
+
class: className,
|
|
123
|
+
open: open || undefined,
|
|
124
|
+
disabled: disabled || undefined,
|
|
125
|
+
...props,
|
|
126
|
+
},
|
|
127
|
+
children
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
CollapsibleRoot.displayName = "Collapsible.Root";
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Collapsible trigger component.
|
|
134
|
+
*/
|
|
135
|
+
const CollapsibleTrigger = forwardRef<HTMLElement, CollapsibleTriggerProps>(
|
|
136
|
+
function CollapsibleTrigger({ children, className, ...props }, ref) {
|
|
137
|
+
return createElement("ds-collapsible-trigger", { ref, class: className, ...props }, children);
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
CollapsibleTrigger.displayName = "Collapsible.Trigger";
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Collapsible content component.
|
|
144
|
+
*/
|
|
145
|
+
const CollapsibleContent = forwardRef<HTMLElement, CollapsibleContentProps>(
|
|
146
|
+
function CollapsibleContent({ children, className, forceMount = false, ...props }, ref) {
|
|
147
|
+
return createElement(
|
|
148
|
+
"ds-collapsible-content",
|
|
149
|
+
{
|
|
150
|
+
ref,
|
|
151
|
+
class: className,
|
|
152
|
+
"force-mount": forceMount || undefined,
|
|
153
|
+
...props,
|
|
154
|
+
},
|
|
155
|
+
children
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
CollapsibleContent.displayName = "Collapsible.Content";
|
|
160
|
+
|
|
161
|
+
// ============================================================================
|
|
162
|
+
// Compound Component
|
|
163
|
+
// ============================================================================
|
|
164
|
+
|
|
165
|
+
export const Collapsible = {
|
|
166
|
+
Root: CollapsibleRoot,
|
|
167
|
+
Trigger: CollapsibleTrigger,
|
|
168
|
+
Content: CollapsibleContent,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// Also export individual components
|
|
172
|
+
export { CollapsibleRoot, CollapsibleTrigger, CollapsibleContent };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collapsible Content component - content section that expands/collapses.
|
|
3
|
+
*
|
|
4
|
+
* @element ds-collapsible-content
|
|
5
|
+
*
|
|
6
|
+
* @slot - Content to show/hide
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { type Presence, createPresence, prefersReducedMotion } from "@hypoth-ui/primitives-dom";
|
|
10
|
+
import { html } from "lit";
|
|
11
|
+
import { property, state } from "lit/decorators.js";
|
|
12
|
+
import { DSElement } from "../../base/ds-element.js";
|
|
13
|
+
import { define } from "../../registry/define.js";
|
|
14
|
+
|
|
15
|
+
export class DsCollapsibleContent extends DSElement {
|
|
16
|
+
/** Keep mounted when collapsed (for animations) */
|
|
17
|
+
@property({ type: Boolean, attribute: "force-mount" })
|
|
18
|
+
forceMount = false;
|
|
19
|
+
|
|
20
|
+
/** Current state for animations */
|
|
21
|
+
@state()
|
|
22
|
+
dataState: "open" | "closed" = "closed";
|
|
23
|
+
|
|
24
|
+
/** Whether we're currently animating out */
|
|
25
|
+
@state()
|
|
26
|
+
private isAnimatingOut = false;
|
|
27
|
+
|
|
28
|
+
private presence: Presence | null = null;
|
|
29
|
+
|
|
30
|
+
override connectedCallback(): void {
|
|
31
|
+
super.connectedCallback();
|
|
32
|
+
this.setAttribute("role", "region");
|
|
33
|
+
this.updateVisibility();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
override disconnectedCallback(): void {
|
|
37
|
+
super.disconnectedCallback();
|
|
38
|
+
this.presence?.destroy();
|
|
39
|
+
this.presence = null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
override updated(changedProperties: Map<string, unknown>): void {
|
|
43
|
+
if (changedProperties.has("dataState")) {
|
|
44
|
+
this.handleStateChange();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private handleStateChange(): void {
|
|
49
|
+
this.setAttribute("data-state", this.dataState);
|
|
50
|
+
|
|
51
|
+
if (this.dataState === "open") {
|
|
52
|
+
this.isAnimatingOut = false;
|
|
53
|
+
this.updateVisibility();
|
|
54
|
+
} else if (!this.forceMount && !prefersReducedMotion()) {
|
|
55
|
+
// Animate out before hiding
|
|
56
|
+
this.isAnimatingOut = true;
|
|
57
|
+
|
|
58
|
+
this.presence?.destroy();
|
|
59
|
+
this.presence = createPresence({
|
|
60
|
+
onExitComplete: () => {
|
|
61
|
+
this.isAnimatingOut = false;
|
|
62
|
+
this.updateVisibility();
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
this.presence.hide(this);
|
|
66
|
+
} else {
|
|
67
|
+
this.updateVisibility();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private updateVisibility(): void {
|
|
72
|
+
if (this.forceMount) {
|
|
73
|
+
this.hidden = false;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Stay visible during exit animation
|
|
78
|
+
if (this.isAnimatingOut) {
|
|
79
|
+
this.hidden = false;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.hidden = this.dataState === "closed";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
override render() {
|
|
87
|
+
return html`<slot></slot>`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
define("ds-collapsible-content", DsCollapsibleContent);
|
|
92
|
+
|
|
93
|
+
declare global {
|
|
94
|
+
interface HTMLElementTagNameMap {
|
|
95
|
+
"ds-collapsible-content": DsCollapsibleContent;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collapsible Trigger component - button that toggles collapsible.
|
|
3
|
+
*
|
|
4
|
+
* @element ds-collapsible-trigger
|
|
5
|
+
*
|
|
6
|
+
* @slot - Trigger content
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { html } from "lit";
|
|
10
|
+
import { DSElement } from "../../base/ds-element.js";
|
|
11
|
+
import { define } from "../../registry/define.js";
|
|
12
|
+
|
|
13
|
+
export class DsCollapsibleTrigger extends DSElement {
|
|
14
|
+
override connectedCallback(): void {
|
|
15
|
+
super.connectedCallback();
|
|
16
|
+
|
|
17
|
+
// Ensure focusability
|
|
18
|
+
if (!this.hasAttribute("tabindex")) {
|
|
19
|
+
this.setAttribute("tabindex", "0");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Set button role if not already set
|
|
23
|
+
if (!this.hasAttribute("role")) {
|
|
24
|
+
this.setAttribute("role", "button");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
override render() {
|
|
29
|
+
return html`<slot></slot>`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
define("ds-collapsible-trigger", DsCollapsibleTrigger);
|
|
34
|
+
|
|
35
|
+
declare global {
|
|
36
|
+
interface HTMLElementTagNameMap {
|
|
37
|
+
"ds-collapsible-trigger": DsCollapsibleTrigger;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collapsible component - toggleable content visibility.
|
|
3
|
+
*
|
|
4
|
+
* Provides an expandable/collapsible content section with animation support.
|
|
5
|
+
*
|
|
6
|
+
* @element ds-collapsible
|
|
7
|
+
*
|
|
8
|
+
* @slot trigger - Button or element that toggles the collapsible
|
|
9
|
+
* @slot - Collapsible content (ds-collapsible-content)
|
|
10
|
+
*
|
|
11
|
+
* @fires ds:open - Fired when collapsible opens
|
|
12
|
+
* @fires ds:close - Fired when collapsible closes
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```html
|
|
16
|
+
* <ds-collapsible>
|
|
17
|
+
* <button slot="trigger">Toggle Content</button>
|
|
18
|
+
* <ds-collapsible-content>
|
|
19
|
+
* <p>Collapsible content here.</p>
|
|
20
|
+
* </ds-collapsible-content>
|
|
21
|
+
* </ds-collapsible>
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { html } from "lit";
|
|
26
|
+
import { property, state } from "lit/decorators.js";
|
|
27
|
+
import { DSElement } from "../../base/ds-element.js";
|
|
28
|
+
import { StandardEvents, emitEvent } from "../../events/emit.js";
|
|
29
|
+
import { define } from "../../registry/define.js";
|
|
30
|
+
|
|
31
|
+
// Import child components
|
|
32
|
+
import type { DsCollapsibleContent } from "./collapsible-content.js";
|
|
33
|
+
import "./collapsible-content.js";
|
|
34
|
+
import "./collapsible-trigger.js";
|
|
35
|
+
|
|
36
|
+
export class DsCollapsible extends DSElement {
|
|
37
|
+
/** Whether the collapsible is open */
|
|
38
|
+
@property({ type: Boolean, reflect: true })
|
|
39
|
+
open = false;
|
|
40
|
+
|
|
41
|
+
/** Disable the collapsible */
|
|
42
|
+
@property({ type: Boolean, reflect: true })
|
|
43
|
+
disabled = false;
|
|
44
|
+
|
|
45
|
+
/** Internal ID for ARIA relationships */
|
|
46
|
+
@state()
|
|
47
|
+
private contentId = `collapsible-${crypto.randomUUID().slice(0, 8)}`;
|
|
48
|
+
|
|
49
|
+
override connectedCallback(): void {
|
|
50
|
+
super.connectedCallback();
|
|
51
|
+
this.addEventListener("click", this.handleTriggerClick);
|
|
52
|
+
this.addEventListener("keydown", this.handleKeyDown);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
override disconnectedCallback(): void {
|
|
56
|
+
super.disconnectedCallback();
|
|
57
|
+
this.removeEventListener("click", this.handleTriggerClick);
|
|
58
|
+
this.removeEventListener("keydown", this.handleKeyDown);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Toggle the open state.
|
|
63
|
+
*/
|
|
64
|
+
public toggle(): void {
|
|
65
|
+
if (this.disabled) return;
|
|
66
|
+
this.setOpen(!this.open);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Set the open state.
|
|
71
|
+
*/
|
|
72
|
+
public setOpen(open: boolean): void {
|
|
73
|
+
if (this.disabled || this.open === open) return;
|
|
74
|
+
|
|
75
|
+
this.open = open;
|
|
76
|
+
emitEvent(this, open ? StandardEvents.OPEN : StandardEvents.CLOSE);
|
|
77
|
+
this.updateContent();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private handleTriggerClick = (event: Event): void => {
|
|
81
|
+
const target = event.target as HTMLElement;
|
|
82
|
+
const trigger = target.closest('[slot="trigger"], ds-collapsible-trigger');
|
|
83
|
+
|
|
84
|
+
if (trigger && this.contains(trigger)) {
|
|
85
|
+
this.toggle();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
private handleKeyDown = (event: KeyboardEvent): void => {
|
|
90
|
+
const target = event.target as HTMLElement;
|
|
91
|
+
const trigger = target.closest('[slot="trigger"], ds-collapsible-trigger');
|
|
92
|
+
|
|
93
|
+
if (trigger && this.contains(trigger)) {
|
|
94
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
95
|
+
event.preventDefault();
|
|
96
|
+
this.toggle();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
override updated(): void {
|
|
102
|
+
this.updateContent();
|
|
103
|
+
this.updateTrigger();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private updateContent(): void {
|
|
107
|
+
const content = this.querySelector("ds-collapsible-content") as DsCollapsibleContent | null;
|
|
108
|
+
if (content) {
|
|
109
|
+
content.id = this.contentId;
|
|
110
|
+
content.dataState = this.open ? "open" : "closed";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private updateTrigger(): void {
|
|
115
|
+
const trigger =
|
|
116
|
+
this.querySelector('[slot="trigger"]') || this.querySelector("ds-collapsible-trigger");
|
|
117
|
+
|
|
118
|
+
if (trigger) {
|
|
119
|
+
trigger.setAttribute("aria-expanded", String(this.open));
|
|
120
|
+
trigger.setAttribute("aria-controls", this.contentId);
|
|
121
|
+
if (this.disabled) {
|
|
122
|
+
trigger.setAttribute("aria-disabled", "true");
|
|
123
|
+
} else {
|
|
124
|
+
trigger.removeAttribute("aria-disabled");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
override render() {
|
|
130
|
+
return html`
|
|
131
|
+
<slot name="trigger"></slot>
|
|
132
|
+
<slot></slot>
|
|
133
|
+
`;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
define("ds-collapsible", DsCollapsible);
|
|
138
|
+
|
|
139
|
+
declare global {
|
|
140
|
+
interface HTMLElementTagNameMap {
|
|
141
|
+
"ds-collapsible": DsCollapsible;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Combobox Content component - container for combobox options.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type AnchorPosition,
|
|
7
|
+
type DismissableLayer,
|
|
8
|
+
type RovingFocus,
|
|
9
|
+
createAnchorPosition,
|
|
10
|
+
createDismissableLayer,
|
|
11
|
+
createRovingFocus,
|
|
12
|
+
} from "@hypoth-ui/primitives-dom";
|
|
13
|
+
import {
|
|
14
|
+
type HTMLAttributes,
|
|
15
|
+
type ReactNode,
|
|
16
|
+
forwardRef,
|
|
17
|
+
useCallback,
|
|
18
|
+
useEffect,
|
|
19
|
+
useRef,
|
|
20
|
+
} from "react";
|
|
21
|
+
import { useComboboxContext } from "./combobox-context.js";
|
|
22
|
+
|
|
23
|
+
export interface ComboboxContentProps extends HTMLAttributes<HTMLDivElement> {
|
|
24
|
+
/** Content container */
|
|
25
|
+
children?: ReactNode;
|
|
26
|
+
/** Optional label for accessibility */
|
|
27
|
+
"aria-label"?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Container for combobox options.
|
|
32
|
+
* Handles positioning, dismiss behavior, and keyboard navigation.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```tsx
|
|
36
|
+
* <Combobox.Content>
|
|
37
|
+
* <Combobox.Option value="apple">Apple</Combobox.Option>
|
|
38
|
+
* <Combobox.Option value="banana">Banana</Combobox.Option>
|
|
39
|
+
* </Combobox.Content>
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export const ComboboxContent = forwardRef<HTMLDivElement, ComboboxContentProps>(
|
|
43
|
+
({ children, className, ...restProps }, ref) => {
|
|
44
|
+
const { behavior, open, setOpen, loading } = useComboboxContext("Combobox.Content");
|
|
45
|
+
const internalRef = useRef<HTMLDivElement>(null);
|
|
46
|
+
|
|
47
|
+
// Behavior instances
|
|
48
|
+
const anchorPositionRef = useRef<AnchorPosition | null>(null);
|
|
49
|
+
const dismissLayerRef = useRef<DismissableLayer | null>(null);
|
|
50
|
+
const rovingFocusRef = useRef<RovingFocus | null>(null);
|
|
51
|
+
const inputRef = useRef<HTMLElement | null>(null);
|
|
52
|
+
|
|
53
|
+
// Merge refs
|
|
54
|
+
const mergedRef = useCallback(
|
|
55
|
+
(element: HTMLDivElement | null) => {
|
|
56
|
+
(internalRef as React.MutableRefObject<HTMLDivElement | null>).current = element;
|
|
57
|
+
if (typeof ref === "function") {
|
|
58
|
+
ref(element);
|
|
59
|
+
} else if (ref) {
|
|
60
|
+
(ref as React.MutableRefObject<HTMLDivElement | null>).current = element;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
[ref]
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Setup and cleanup behaviors when open changes
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const content = internalRef.current;
|
|
69
|
+
if (!content || !open) return;
|
|
70
|
+
|
|
71
|
+
// Find input element (previous sibling or parent context)
|
|
72
|
+
const input = content.previousElementSibling as HTMLElement;
|
|
73
|
+
inputRef.current = input;
|
|
74
|
+
|
|
75
|
+
// Setup anchor positioning
|
|
76
|
+
if (input) {
|
|
77
|
+
anchorPositionRef.current = createAnchorPosition({
|
|
78
|
+
anchor: input,
|
|
79
|
+
floating: content,
|
|
80
|
+
placement: "bottom-start",
|
|
81
|
+
offset: 4,
|
|
82
|
+
flip: true,
|
|
83
|
+
onPositionChange: (pos) => {
|
|
84
|
+
content.setAttribute("data-placement", pos.placement);
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Setup dismiss layer
|
|
90
|
+
dismissLayerRef.current = createDismissableLayer({
|
|
91
|
+
container: content,
|
|
92
|
+
excludeElements: input ? [input] : [],
|
|
93
|
+
onDismiss: () => setOpen(false),
|
|
94
|
+
closeOnEscape: true,
|
|
95
|
+
closeOnOutsideClick: true,
|
|
96
|
+
});
|
|
97
|
+
dismissLayerRef.current.activate();
|
|
98
|
+
|
|
99
|
+
// Setup roving focus
|
|
100
|
+
rovingFocusRef.current = createRovingFocus({
|
|
101
|
+
container: content,
|
|
102
|
+
selector: '[role="option"]:not([aria-disabled="true"])',
|
|
103
|
+
direction: "vertical",
|
|
104
|
+
loop: true,
|
|
105
|
+
skipDisabled: true,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Cleanup
|
|
109
|
+
return () => {
|
|
110
|
+
anchorPositionRef.current?.destroy();
|
|
111
|
+
anchorPositionRef.current = null;
|
|
112
|
+
dismissLayerRef.current?.deactivate();
|
|
113
|
+
dismissLayerRef.current = null;
|
|
114
|
+
rovingFocusRef.current?.destroy();
|
|
115
|
+
rovingFocusRef.current = null;
|
|
116
|
+
};
|
|
117
|
+
}, [open, setOpen]);
|
|
118
|
+
|
|
119
|
+
// Don't render if not open
|
|
120
|
+
if (!open) return null;
|
|
121
|
+
|
|
122
|
+
const listboxProps = behavior.getListboxProps();
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div
|
|
126
|
+
ref={mergedRef}
|
|
127
|
+
role={listboxProps.role}
|
|
128
|
+
id={listboxProps.id}
|
|
129
|
+
aria-multiselectable={listboxProps["aria-multiselectable"]}
|
|
130
|
+
aria-busy={loading || undefined}
|
|
131
|
+
className={className}
|
|
132
|
+
data-state="open"
|
|
133
|
+
{...restProps}
|
|
134
|
+
>
|
|
135
|
+
{children}
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
ComboboxContent.displayName = "Combobox.Content";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Combobox context for compound component pattern.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ComboboxBehavior, Option } from "@hypoth-ui/primitives-dom";
|
|
6
|
+
import { createCompoundContext } from "../../utils/create-context.js";
|
|
7
|
+
|
|
8
|
+
export interface ComboboxContextValue<Multi extends boolean = false> {
|
|
9
|
+
/** Combobox behavior instance */
|
|
10
|
+
behavior: ComboboxBehavior<string, Multi>;
|
|
11
|
+
/** Whether combobox is open */
|
|
12
|
+
open: boolean;
|
|
13
|
+
/** Set open state */
|
|
14
|
+
setOpen: (open: boolean) => void;
|
|
15
|
+
/** Current value(s) */
|
|
16
|
+
value: Multi extends true ? string[] : string | null;
|
|
17
|
+
/** Set value(s) */
|
|
18
|
+
setValue: (value: Multi extends true ? string[] : string | null) => void;
|
|
19
|
+
/** Input value */
|
|
20
|
+
inputValue: string;
|
|
21
|
+
/** Set input value */
|
|
22
|
+
setInputValue: (value: string) => void;
|
|
23
|
+
/** Highlighted value for keyboard navigation */
|
|
24
|
+
highlightedValue: string | null;
|
|
25
|
+
/** Set highlighted value */
|
|
26
|
+
setHighlightedValue: (value: string | null) => void;
|
|
27
|
+
/** Whether multi-select mode */
|
|
28
|
+
multiple: Multi;
|
|
29
|
+
/** Filtered options */
|
|
30
|
+
filteredOptions: Option<string>[];
|
|
31
|
+
/** Loading state */
|
|
32
|
+
loading: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const [ComboboxProvider, useComboboxContext] =
|
|
36
|
+
createCompoundContext<ComboboxContextValue<boolean>>("Combobox");
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Combobox Empty component - shown when no options match.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { type HTMLAttributes, type ReactNode, forwardRef } from "react";
|
|
6
|
+
import { useComboboxContext } from "./combobox-context.js";
|
|
7
|
+
|
|
8
|
+
export interface ComboboxEmptyProps extends HTMLAttributes<HTMLOutputElement> {
|
|
9
|
+
/** Empty state content */
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Empty state shown when no options match the search.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* <Combobox.Empty>No results found</Combobox.Empty>
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export const ComboboxEmpty = forwardRef<HTMLOutputElement, ComboboxEmptyProps>(
|
|
22
|
+
({ children, className, ...restProps }, ref) => {
|
|
23
|
+
const { filteredOptions, loading, inputValue } = useComboboxContext("Combobox.Empty");
|
|
24
|
+
|
|
25
|
+
// Only show if no options and not loading and has input
|
|
26
|
+
if (filteredOptions.length > 0 || loading || !inputValue) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<output ref={ref} aria-live="polite" className={className} {...restProps}>
|
|
32
|
+
{children}
|
|
33
|
+
</output>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
ComboboxEmpty.displayName = "Combobox.Empty";
|