zabi-components 7.0.2 → 8.1.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/CHANGELOG.md +1924 -0
- package/README.md +287 -11
- package/THEME.md +149 -332
- package/THEMING.md +1329 -163
- package/dist/atoms/ActionPanel.svelte +24 -9
- package/dist/atoms/ActionPanel.svelte.d.ts +2 -0
- package/dist/atoms/Avatar.svelte +104 -0
- package/dist/atoms/Avatar.svelte.d.ts +34 -0
- package/dist/atoms/Badge.svelte +124 -58
- package/dist/atoms/Badge.svelte.d.ts +17 -5
- package/dist/atoms/Button.svelte +212 -62
- package/dist/atoms/Button.svelte.d.ts +33 -2
- package/dist/atoms/Card.svelte +31 -28
- package/dist/atoms/Card.svelte.d.ts +5 -2
- package/dist/atoms/CardContent.svelte +10 -2
- package/dist/atoms/CardContent.svelte.d.ts +2 -0
- package/dist/atoms/CardFooter.svelte +10 -2
- package/dist/atoms/CardFooter.svelte.d.ts +2 -0
- package/dist/atoms/CardHeader.svelte +26 -6
- package/dist/atoms/CardHeader.svelte.d.ts +2 -0
- package/dist/atoms/Checkbox.svelte +52 -21
- package/dist/atoms/Checkbox.svelte.d.ts +6 -2
- package/dist/atoms/CodeBlock.svelte +35 -9
- package/dist/atoms/CodeBlock.svelte.d.ts +13 -3
- package/dist/atoms/ColorPicker.svelte +272 -52
- package/dist/atoms/ColorPicker.svelte.d.ts +6 -1
- package/dist/atoms/Container.svelte +17 -10
- package/dist/atoms/DateField.svelte +107 -0
- package/dist/atoms/DateField.svelte.d.ts +53 -0
- package/dist/atoms/Divider.svelte +4 -4
- package/dist/atoms/FeatureCard.svelte +4 -3
- package/dist/atoms/FieldMessages.svelte +69 -0
- package/dist/atoms/FieldMessages.svelte.d.ts +23 -0
- package/dist/atoms/FloatingActionButton.svelte +174 -0
- package/dist/atoms/FloatingActionButton.svelte.d.ts +51 -0
- package/dist/atoms/Heading.svelte +93 -32
- package/dist/atoms/Heading.svelte.d.ts +24 -4
- package/dist/atoms/IconButton.svelte +242 -53
- package/dist/atoms/IconButton.svelte.d.ts +49 -4
- package/dist/atoms/Input.svelte +267 -74
- package/dist/atoms/Input.svelte.d.ts +32 -2
- package/dist/atoms/List.svelte +13 -2
- package/dist/atoms/List.svelte.d.ts +2 -0
- package/dist/atoms/ListItem.svelte +44 -11
- package/dist/atoms/ListItem.svelte.d.ts +5 -0
- package/dist/atoms/ListItemLeading.svelte +4 -2
- package/dist/atoms/ListItemLeading.svelte.d.ts +1 -1
- package/dist/atoms/OptimizedImage.svelte +11 -2
- package/dist/atoms/OptimizedImage.svelte.d.ts +2 -0
- package/dist/atoms/Progress.svelte +31 -9
- package/dist/atoms/Progress.svelte.d.ts +6 -0
- package/dist/atoms/Radio.svelte +23 -3
- package/dist/atoms/Radio.svelte.d.ts +6 -2
- package/dist/atoms/Rating.svelte +494 -0
- package/dist/atoms/Rating.svelte.d.ts +56 -0
- package/dist/atoms/Select.svelte +475 -135
- package/dist/atoms/Select.svelte.d.ts +52 -2
- package/dist/atoms/SelectionControl.svelte +99 -18
- package/dist/atoms/SelectionControl.svelte.d.ts +6 -3
- package/dist/atoms/Skeleton.svelte +5 -3
- package/dist/atoms/Slider.svelte +309 -0
- package/dist/atoms/Slider.svelte.d.ts +40 -0
- package/dist/atoms/Spinner.svelte +57 -0
- package/dist/atoms/Spinner.svelte.d.ts +19 -0
- package/dist/atoms/Table.svelte +152 -6
- package/dist/atoms/Table.svelte.d.ts +15 -3
- package/dist/atoms/TemporalField.svelte +140 -0
- package/dist/atoms/TemporalField.svelte.d.ts +32 -0
- package/dist/atoms/Text.svelte +56 -15
- package/dist/atoms/Text.svelte.d.ts +13 -4
- package/dist/atoms/Textarea.svelte +57 -45
- package/dist/atoms/Textarea.svelte.d.ts +14 -2
- package/dist/atoms/ThemeToggle.svelte +238 -56
- package/dist/atoms/ThemeToggle.svelte.d.ts +28 -3
- package/dist/atoms/TimeField.svelte +108 -0
- package/dist/atoms/TimeField.svelte.d.ts +54 -0
- package/dist/atoms/Toast.svelte +25 -10
- package/dist/atoms/Toast.svelte.d.ts +2 -0
- package/dist/atoms/Toggle.svelte +60 -19
- package/dist/atoms/Toggle.svelte.d.ts +18 -2
- package/dist/atoms/Tooltip.svelte +583 -40
- package/dist/atoms/Tooltip.svelte.d.ts +26 -0
- package/dist/atoms/ZabiStringsProvider.svelte +45 -0
- package/dist/atoms/ZabiStringsProvider.svelte.d.ts +27 -0
- package/dist/atoms/index.d.ts +17 -0
- package/dist/atoms/index.js +12 -0
- package/dist/atoms/selection-control.styles.d.ts +36 -5
- package/dist/atoms/selection-control.styles.js +40 -9
- package/dist/components/atoms/index.d.ts +17 -0
- package/dist/components/atoms/index.d.ts.map +1 -1
- package/dist/components/atoms/selection-control.styles.d.ts +36 -5
- package/dist/components/atoms/selection-control.styles.d.ts.map +1 -1
- package/dist/components/index.d.ts +83 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/molecules/index.d.ts +46 -0
- package/dist/components/molecules/index.d.ts.map +1 -1
- package/dist/components/molecules/toast-store.d.ts +53 -7
- package/dist/components/molecules/toast-store.d.ts.map +1 -1
- package/dist/components/organisms/index.d.ts +6 -0
- package/dist/components/organisms/index.d.ts.map +1 -1
- package/dist/components/types/variants.d.ts +15 -1
- package/dist/components/types/variants.d.ts.map +1 -1
- package/dist/components/util/app-shell.d.ts +28 -0
- package/dist/components/util/app-shell.d.ts.map +1 -0
- package/dist/components/util/avatar.d.ts +36 -0
- package/dist/components/util/avatar.d.ts.map +1 -0
- package/dist/components/util/bottom-sheet.d.ts +30 -0
- package/dist/components/util/bottom-sheet.d.ts.map +1 -0
- package/dist/components/util/bottom-tab-bar.d.ts +35 -0
- package/dist/components/util/bottom-tab-bar.d.ts.map +1 -0
- package/dist/components/util/calendar.d.ts +56 -0
- package/dist/components/util/calendar.d.ts.map +1 -0
- package/dist/components/util/cn.d.ts +8 -0
- package/dist/components/util/cn.d.ts.map +1 -0
- package/dist/components/util/collapsible.d.ts +58 -0
- package/dist/components/util/collapsible.d.ts.map +1 -0
- package/dist/components/util/confirm-dialog.d.ts +12 -0
- package/dist/components/util/confirm-dialog.d.ts.map +1 -0
- package/dist/components/util/date.d.ts +96 -0
- package/dist/components/util/date.d.ts.map +1 -0
- package/dist/components/util/drawer.d.ts +13 -0
- package/dist/components/util/drawer.d.ts.map +1 -0
- package/dist/components/util/dropdown.d.ts +47 -0
- package/dist/components/util/dropdown.d.ts.map +1 -0
- package/dist/components/util/field.d.ts +39 -0
- package/dist/components/util/field.d.ts.map +1 -0
- package/dist/components/util/fit-in-viewport.d.ts +201 -0
- package/dist/components/util/fit-in-viewport.d.ts.map +1 -0
- package/dist/components/util/focus-utils.d.ts +75 -2
- package/dist/components/util/focus-utils.d.ts.map +1 -1
- package/dist/components/util/hydration.d.ts +45 -0
- package/dist/components/util/hydration.d.ts.map +1 -0
- package/dist/components/util/keyboard-inset.d.ts +18 -0
- package/dist/components/util/keyboard-inset.d.ts.map +1 -0
- package/dist/components/util/media-grid.d.ts +51 -0
- package/dist/components/util/media-grid.d.ts.map +1 -0
- package/dist/components/util/media.d.ts +6 -0
- package/dist/components/util/media.d.ts.map +1 -0
- package/dist/components/util/overlay-chrome.d.ts +53 -0
- package/dist/components/util/overlay-chrome.d.ts.map +1 -0
- package/dist/components/util/overlay.d.ts +72 -0
- package/dist/components/util/overlay.d.ts.map +1 -0
- package/dist/components/util/photo.d.ts +215 -0
- package/dist/components/util/photo.d.ts.map +1 -0
- package/dist/components/util/portal.d.ts +23 -0
- package/dist/components/util/portal.d.ts.map +1 -0
- package/dist/components/util/pull-to-refresh.d.ts +27 -0
- package/dist/components/util/pull-to-refresh.d.ts.map +1 -0
- package/dist/components/util/radio-keys.d.ts +25 -0
- package/dist/components/util/radio-keys.d.ts.map +1 -0
- package/dist/components/util/rating.d.ts +23 -0
- package/dist/components/util/rating.d.ts.map +1 -0
- package/dist/components/util/ready-made-strings.d.ts +77 -0
- package/dist/components/util/ready-made-strings.d.ts.map +1 -0
- package/dist/components/util/scroll-padding.d.ts +20 -0
- package/dist/components/util/scroll-padding.d.ts.map +1 -0
- package/dist/components/util/segmented-control.d.ts +16 -0
- package/dist/components/util/segmented-control.d.ts.map +1 -0
- package/dist/components/util/select.d.ts +41 -0
- package/dist/components/util/select.d.ts.map +1 -0
- package/dist/components/util/sheet-drag.d.ts +60 -0
- package/dist/components/util/sheet-drag.d.ts.map +1 -0
- package/dist/components/util/sidebar.d.ts +63 -0
- package/dist/components/util/sidebar.d.ts.map +1 -0
- package/dist/components/util/sortable-list.d.ts +65 -0
- package/dist/components/util/sortable-list.d.ts.map +1 -0
- package/dist/components/util/ssr-safe.d.ts +4 -4
- package/dist/components/util/ssr-safe.d.ts.map +1 -1
- package/dist/components/util/stepper.d.ts +40 -0
- package/dist/components/util/stepper.d.ts.map +1 -0
- package/dist/components/util/swipeable-list-item.d.ts +36 -0
- package/dist/components/util/swipeable-list-item.d.ts.map +1 -0
- package/dist/components/util/theme-mode.d.ts +80 -0
- package/dist/components/util/theme-mode.d.ts.map +1 -0
- package/dist/components/util/toaster.d.ts +93 -0
- package/dist/components/util/toaster.d.ts.map +1 -0
- package/dist/components/util/tooltip.d.ts +10 -0
- package/dist/components/util/tooltip.d.ts.map +1 -0
- package/dist/components/util/top-navbar.d.ts +11 -0
- package/dist/components/util/top-navbar.d.ts.map +1 -0
- package/dist/components/util/touch-target.d.ts +31 -0
- package/dist/components/util/touch-target.d.ts.map +1 -0
- package/dist/components/util/zabi-strings.d.ts +133 -0
- package/dist/components/util/zabi-strings.d.ts.map +1 -0
- package/dist/create-theme/cli.js +169 -0
- package/dist/create-theme/index.d.ts +176 -0
- package/dist/create-theme/index.js +817 -0
- package/dist/create-theme/lib/ramp-math.js +128 -0
- package/dist/create-theme/lib/resolve.js +141 -0
- package/dist/create-theme/theme-data.js +1116 -0
- package/dist/index.d.ts +83 -3
- package/dist/index.js +60 -8
- package/dist/molecules/Alert.svelte +73 -66
- package/dist/molecules/Alert.svelte.d.ts +9 -1
- package/dist/molecules/AppBar.svelte +415 -0
- package/dist/molecules/AppBar.svelte.d.ts +69 -0
- package/dist/molecules/AvatarGroup.svelte +116 -0
- package/dist/molecules/AvatarGroup.svelte.d.ts +40 -0
- package/dist/molecules/BottomSheet.svelte +540 -0
- package/dist/molecules/BottomSheet.svelte.d.ts +93 -0
- package/dist/molecules/BottomTabBar.svelte +297 -0
- package/dist/molecules/BottomTabBar.svelte.d.ts +64 -0
- package/dist/molecules/Calendar.svelte +597 -0
- package/dist/molecules/Calendar.svelte.d.ts +72 -0
- package/dist/molecules/Collapsible.svelte +243 -0
- package/dist/molecules/Collapsible.svelte.d.ts +44 -0
- package/dist/molecules/CollapsibleGroup.svelte +126 -0
- package/dist/molecules/CollapsibleGroup.svelte.d.ts +14 -0
- package/dist/molecules/ComponentDemo.svelte +25 -5
- package/dist/molecules/ComponentDemo.svelte.d.ts +5 -0
- package/dist/molecules/ConfirmDialog.svelte +253 -0
- package/dist/molecules/ConfirmDialog.svelte.d.ts +55 -0
- package/dist/molecules/ContactForm.svelte +46 -19
- package/dist/molecules/ContactForm.svelte.d.ts +10 -0
- package/dist/molecules/Drawer.svelte +394 -0
- package/dist/molecules/Drawer.svelte.d.ts +55 -0
- package/dist/molecules/Dropdown.svelte +540 -50
- package/dist/molecules/Dropdown.svelte.d.ts +49 -6
- package/dist/molecules/DropdownItem.svelte +122 -0
- package/dist/molecules/DropdownItem.svelte.d.ts +28 -0
- package/dist/molecules/EmptyState.svelte +56 -10
- package/dist/molecules/EmptyState.svelte.d.ts +8 -0
- package/dist/molecules/Form.svelte +25 -14
- package/dist/molecules/Form.svelte.d.ts +13 -5
- package/dist/molecules/FormField.svelte +11 -2
- package/dist/molecules/FormField.svelte.d.ts +2 -0
- package/dist/molecules/Header.svelte +13 -3
- package/dist/molecules/Header.svelte.d.ts +2 -0
- package/dist/molecules/ImageUpload.svelte +422 -70
- package/dist/molecules/ImageUpload.svelte.d.ts +61 -5
- package/dist/molecules/MediaGrid.svelte +527 -0
- package/dist/molecules/MediaGrid.svelte.d.ts +77 -0
- package/dist/molecules/Modal.svelte +340 -45
- package/dist/molecules/Modal.svelte.d.ts +59 -7
- package/dist/molecules/NavigationMenu.svelte +15 -3
- package/dist/molecules/NavigationMenu.svelte.d.ts +4 -0
- package/dist/molecules/NavigationMenuContent.svelte +91 -36
- package/dist/molecules/NavigationMenuContent.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuItem.svelte +10 -3
- package/dist/molecules/NavigationMenuItem.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuLink.svelte +10 -2
- package/dist/molecules/NavigationMenuLink.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuList.svelte +19 -4
- package/dist/molecules/NavigationMenuList.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuTrigger.svelte +16 -3
- package/dist/molecules/NavigationMenuTrigger.svelte.d.ts +2 -0
- package/dist/molecules/Page.svelte +38 -2
- package/dist/molecules/Page.svelte.d.ts +13 -0
- package/dist/molecules/PhotoGrid.svelte +433 -0
- package/dist/molecules/PhotoGrid.svelte.d.ts +62 -0
- package/dist/molecules/PhotoViewer.svelte +1116 -0
- package/dist/molecules/PhotoViewer.svelte.d.ts +56 -0
- package/dist/molecules/PropsTable.svelte +26 -11
- package/dist/molecules/PropsTable.svelte.d.ts +5 -0
- package/dist/molecules/PullToRefresh.svelte +263 -0
- package/dist/molecules/PullToRefresh.svelte.d.ts +52 -0
- package/dist/molecules/RadioGroup.svelte +57 -15
- package/dist/molecules/RadioGroup.svelte.d.ts +16 -3
- package/dist/molecules/Section.svelte +13 -5
- package/dist/molecules/Section.svelte.d.ts +3 -1
- package/dist/molecules/SegmentedControl.svelte +323 -0
- package/dist/molecules/SegmentedControl.svelte.d.ts +34 -0
- package/dist/molecules/SidebarBrandHeader.svelte +24 -5
- package/dist/molecules/SidebarBrandHeader.svelte.d.ts +5 -0
- package/dist/molecules/SidebarFooter.svelte +39 -8
- package/dist/molecules/SidebarFooter.svelte.d.ts +5 -0
- package/dist/molecules/SidebarNavSection.svelte +12 -4
- package/dist/molecules/SidebarNavSection.svelte.d.ts +2 -0
- package/dist/molecules/SlideUp.svelte +253 -30
- package/dist/molecules/SlideUp.svelte.d.ts +31 -4
- package/dist/molecules/SortableList.svelte +632 -0
- package/dist/molecules/SortableList.svelte.d.ts +69 -0
- package/dist/molecules/Stepper.svelte +548 -0
- package/dist/molecules/Stepper.svelte.d.ts +39 -0
- package/dist/molecules/StickyActionBar.svelte +191 -0
- package/dist/molecules/StickyActionBar.svelte.d.ts +65 -0
- package/dist/molecules/SwipeableListItem.svelte +334 -0
- package/dist/molecules/SwipeableListItem.svelte.d.ts +54 -0
- package/dist/molecules/Tabs.svelte +254 -24
- package/dist/molecules/Tabs.svelte.d.ts +15 -4
- package/dist/molecules/Toaster.svelte +352 -6
- package/dist/molecules/Toaster.svelte.d.ts +95 -2
- package/dist/molecules/ToasterToast.svelte +279 -62
- package/dist/molecules/ToasterToast.svelte.d.ts +11 -0
- package/dist/molecules/UnsavedChangesBar.svelte +212 -0
- package/dist/molecules/UnsavedChangesBar.svelte.d.ts +43 -0
- package/dist/molecules/index.d.ts +46 -0
- package/dist/molecules/index.js +27 -0
- package/dist/molecules/toast-store.d.ts +53 -7
- package/dist/molecules/toast-store.js +21 -0
- package/dist/organisms/AppShell.svelte +196 -0
- package/dist/organisms/AppShell.svelte.d.ts +61 -0
- package/dist/organisms/SidebarAccountPanel.svelte +111 -12
- package/dist/organisms/SidebarAccountPanel.svelte.d.ts +21 -0
- package/dist/organisms/SidebarNavigation.svelte +193 -69
- package/dist/organisms/SidebarNavigation.svelte.d.ts +31 -1
- package/dist/organisms/SidebarPanel.svelte +45 -16
- package/dist/organisms/SidebarPanel.svelte.d.ts +2 -0
- package/dist/organisms/SidebarShell.svelte +263 -0
- package/dist/organisms/SidebarShell.svelte.d.ts +106 -0
- package/dist/organisms/TopNavbar.svelte +329 -33
- package/dist/organisms/TopNavbar.svelte.d.ts +28 -0
- package/dist/organisms/index.d.ts +6 -0
- package/dist/organisms/index.js +4 -0
- package/dist/routes/lib/variant-utils.d.ts +26 -0
- package/dist/routes/lib/{variant-utils.ts → variant-utils.js} +15 -46
- package/dist/types/components.d.ts +4 -0
- package/dist/types/components.d.ts.map +1 -1
- package/dist/types/events.js +1 -0
- package/dist/types/index.d.ts +605 -25
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +3 -0
- package/dist/types/variants.ts +17 -1
- package/dist/util/app-shell.d.ts +27 -0
- package/dist/util/app-shell.js +72 -0
- package/dist/util/avatar.d.ts +35 -0
- package/dist/util/avatar.js +52 -0
- package/dist/util/bottom-sheet.d.ts +29 -0
- package/dist/util/bottom-sheet.js +43 -0
- package/dist/util/bottom-tab-bar.d.ts +34 -0
- package/dist/util/bottom-tab-bar.js +54 -0
- package/dist/util/calendar.d.ts +55 -0
- package/dist/util/calendar.js +74 -0
- package/dist/util/cn.d.ts +7 -0
- package/dist/util/cn.js +48 -0
- package/dist/util/collapsible.d.ts +57 -0
- package/dist/util/collapsible.js +23 -0
- package/dist/util/confirm-dialog.d.ts +11 -0
- package/dist/util/confirm-dialog.js +2 -0
- package/dist/util/date.d.ts +95 -0
- package/dist/util/date.js +237 -0
- package/dist/util/drawer.d.ts +12 -0
- package/dist/util/drawer.js +7 -0
- package/dist/util/dropdown.d.ts +46 -0
- package/dist/util/dropdown.js +21 -0
- package/dist/util/field.d.ts +38 -0
- package/dist/util/field.js +38 -0
- package/dist/util/fit-in-viewport.d.ts +200 -0
- package/dist/util/fit-in-viewport.js +399 -0
- package/dist/util/focus-utils.d.ts +75 -2
- package/dist/util/focus-utils.js +182 -9
- package/dist/util/hydration.d.ts +44 -0
- package/dist/util/hydration.js +58 -0
- package/dist/util/keyboard-inset.d.ts +17 -0
- package/dist/util/keyboard-inset.js +53 -0
- package/dist/util/media-grid.d.ts +50 -0
- package/dist/util/media-grid.js +81 -0
- package/dist/util/media.d.ts +5 -0
- package/dist/util/media.js +10 -0
- package/dist/util/overlay-chrome.d.ts +52 -0
- package/dist/util/overlay-chrome.js +56 -0
- package/dist/util/overlay.d.ts +71 -0
- package/dist/util/overlay.js +240 -0
- package/dist/util/photo.d.ts +214 -0
- package/dist/util/photo.js +206 -0
- package/dist/util/portal.d.ts +22 -0
- package/dist/util/portal.js +50 -0
- package/dist/util/pull-to-refresh.d.ts +26 -0
- package/dist/util/pull-to-refresh.js +30 -0
- package/dist/util/radio-keys.d.ts +24 -0
- package/dist/util/radio-keys.js +49 -0
- package/dist/util/rating.d.ts +22 -0
- package/dist/util/rating.js +26 -0
- package/dist/util/ready-made-strings.d.ts +76 -0
- package/dist/util/ready-made-strings.js +67 -0
- package/dist/util/scroll-padding.d.ts +19 -0
- package/dist/util/scroll-padding.js +44 -0
- package/dist/util/segmented-control.d.ts +15 -0
- package/dist/util/segmented-control.js +1 -0
- package/dist/util/select.d.ts +40 -0
- package/dist/util/select.js +63 -0
- package/dist/util/sheet-drag.d.ts +59 -0
- package/dist/util/sheet-drag.js +241 -0
- package/dist/util/sidebar.d.ts +62 -0
- package/dist/util/sidebar.js +38 -0
- package/dist/util/sortable-list.d.ts +64 -0
- package/dist/util/sortable-list.js +53 -0
- package/dist/util/ssr-safe.d.ts +4 -4
- package/dist/util/stepper.d.ts +39 -0
- package/dist/util/stepper.js +38 -0
- package/dist/util/swipeable-list-item.d.ts +35 -0
- package/dist/util/swipeable-list-item.js +32 -0
- package/dist/util/theme-mode.d.ts +79 -0
- package/dist/util/theme-mode.js +121 -0
- package/dist/util/toaster.d.ts +92 -0
- package/dist/util/toaster.js +75 -0
- package/dist/util/tooltip.d.ts +9 -0
- package/dist/util/tooltip.js +18 -0
- package/dist/util/top-navbar.d.ts +10 -0
- package/dist/util/top-navbar.js +5 -0
- package/dist/util/touch-target.d.ts +30 -0
- package/dist/util/touch-target.js +30 -0
- package/dist/util/zabi-strings.d.ts +132 -0
- package/dist/util/zabi-strings.js +105 -0
- package/dist/zabi-components-colors.css +5 -2
- package/dist/zabi-components-theme-dark-only.css +574 -225
- package/dist/zabi-components-theme-dark.css +574 -225
- package/dist/zabi-components-theme-only.css +1680 -317
- package/dist/zabi-components-theme.css +1680 -317
- package/dist/zabi-components.css +4473 -1158
- package/docs/lucide-icons.md +41 -16
- package/docs/theme-imports.md +81 -11
- package/package.json +51 -29
- package/dist/lib/layout-width-tokens.d.ts +0 -17
- package/dist/lib/layout-width-tokens.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Button.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Button.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Input.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Input.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/List.d.ts +0 -3
- package/dist/lib/showcase/component-docs/List.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Modal.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Modal.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Radio.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Radio.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts +0 -3
- package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Skeleton.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Skeleton.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/_shared.d.ts +0 -12
- package/dist/lib/showcase/component-docs/_shared.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/index.d.ts +0 -4
- package/dist/lib/showcase/component-docs/index.d.ts.map +0 -1
- package/dist/lib/showcase/components-catalog.d.ts +0 -3
- package/dist/lib/showcase/components-catalog.d.ts.map +0 -1
- package/dist/lib/showcase/components-showcase-constants.d.ts +0 -89
- package/dist/lib/showcase/components-showcase-constants.d.ts.map +0 -1
- package/dist/lib/showcase/docs-sidebar-helpers.d.ts +0 -15
- package/dist/lib/showcase/docs-sidebar-helpers.d.ts.map +0 -1
- package/dist/routes/lib/focus-utils.d.ts +0 -12
- package/dist/routes/lib/focus-utils.ts +0 -110
- package/dist/routes/lib/ssr-safe.ts +0 -58
- package/scripts/fix-lucide-svelte-icon-dts.js +0 -35
package/dist/atoms/Select.svelte
CHANGED
|
@@ -1,15 +1,54 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { mergeStrings } from "../util/ready-made-strings.js";
|
|
3
|
+
import { zabiCommonStrings, zabiStringsFor } from "../util/zabi-strings.js";
|
|
2
4
|
import Dropdown from "../molecules/Dropdown.svelte";
|
|
3
5
|
import Input from "./Input.svelte";
|
|
6
|
+
import Check from "@lucide/svelte/icons/check";
|
|
7
|
+
import ChevronDown from "@lucide/svelte/icons/chevron-down";
|
|
8
|
+
import { onMount, tick, untrack } from "svelte";
|
|
9
|
+
import type { HTMLButtonAttributes } from "svelte/elements";
|
|
4
10
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
DEFAULT_SELECT_STRINGS,
|
|
12
|
+
NATIVE_SELECT_ATTRIBUTE,
|
|
13
|
+
choiceBeforeMount,
|
|
14
|
+
type SelectPresentation,
|
|
15
|
+
type SelectStrings,
|
|
16
|
+
} from "../util/select.js";
|
|
10
17
|
import { generateId } from "../util/ssr-safe.js";
|
|
18
|
+
import { isInsideToastRegion } from "../util/focus-utils.js";
|
|
19
|
+
import { cn } from "../util/cn.js";
|
|
20
|
+
import { fieldDescribedBy, fieldMessageState } from "../util/field.js";
|
|
21
|
+
import FieldMessages from "./FieldMessages.svelte";
|
|
11
22
|
|
|
12
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Other attributes (`data-*`, `aria-*`, `onfocus`, ...) land on the
|
|
25
|
+
* trigger, which is a `<button>`: it is the control that takes focus and
|
|
26
|
+
* that the label names.
|
|
27
|
+
*
|
|
28
|
+
* The server's HTML also carries a native `<select>` with the same
|
|
29
|
+
* options. Until the component has mounted, and for good where there are
|
|
30
|
+
* no scripts, that is the control: it is what is seen, pressed and
|
|
31
|
+
* submitted. Once mounted the trigger and the list take over, and the
|
|
32
|
+
* native select stays, out of sight and out of reach, as the form control
|
|
33
|
+
* (`name`, `required`), kept at the same value. With
|
|
34
|
+
* `presentation="native"` it is the control throughout.
|
|
35
|
+
*/
|
|
36
|
+
type Props = Omit<
|
|
37
|
+
HTMLButtonAttributes,
|
|
38
|
+
| "class"
|
|
39
|
+
| "value"
|
|
40
|
+
| "name"
|
|
41
|
+
| "disabled"
|
|
42
|
+
| "type"
|
|
43
|
+
| "onchange"
|
|
44
|
+
| "onclick"
|
|
45
|
+
| "aria-describedby"
|
|
46
|
+
| "children"
|
|
47
|
+
> & {
|
|
48
|
+
/** Extra classes for the host element. */
|
|
49
|
+
class?: string;
|
|
50
|
+
/** Id of the trigger. Omit to auto-generate. */
|
|
51
|
+
id?: string;
|
|
13
52
|
value?: string | number | undefined;
|
|
14
53
|
options?: Array<{
|
|
15
54
|
value: string | number;
|
|
@@ -17,9 +56,26 @@
|
|
|
17
56
|
disabled?: boolean;
|
|
18
57
|
}>;
|
|
19
58
|
searchable?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* The component's own texts, for another language. Whatever is left
|
|
61
|
+
* out keeps its English default. The six older props for single texts
|
|
62
|
+
* (`placeholder`, `searchPlaceholder`, `noResultsText`,
|
|
63
|
+
* `loadingText`, `emptyStateTitle`, `emptyStateDescription`) win
|
|
64
|
+
* over it.
|
|
65
|
+
*/
|
|
66
|
+
strings?: Partial<SelectStrings>;
|
|
20
67
|
searchPlaceholder?: string;
|
|
21
68
|
maxMenuHeight?: string;
|
|
22
69
|
menuWidth?: string;
|
|
70
|
+
/**
|
|
71
|
+
* How the list is shown. `auto`: in a BottomSheet on a phone (a touch
|
|
72
|
+
* screen narrower than 640px), under the trigger everywhere else.
|
|
73
|
+
* `popover` and `sheet` are always the one or the other. `native`:
|
|
74
|
+
* only the browser's own `<select>`, styled as the field, with the
|
|
75
|
+
* platform's picker. It shows the options' labels and nothing else:
|
|
76
|
+
* no search field, and none of the loading or empty states.
|
|
77
|
+
*/
|
|
78
|
+
presentation?: SelectPresentation;
|
|
23
79
|
noResultsText?: string;
|
|
24
80
|
isLoading?: boolean;
|
|
25
81
|
loadingText?: string;
|
|
@@ -28,127 +84,187 @@
|
|
|
28
84
|
emptyStateActionLabel?: string;
|
|
29
85
|
placeholder?: string;
|
|
30
86
|
label?: string;
|
|
87
|
+
/** The name the chosen value is submitted under, by the native `<select>`. */
|
|
88
|
+
name?: string;
|
|
89
|
+
/**
|
|
90
|
+
* The form is not sent while nothing is chosen. Checked by the
|
|
91
|
+
* browser on the native `<select>`; once mounted, the browser's own
|
|
92
|
+
* message is shown under the field and focus goes to the trigger.
|
|
93
|
+
*/
|
|
31
94
|
required?: boolean;
|
|
32
95
|
disabled?: boolean;
|
|
33
96
|
size?: "sm" | "md" | "lg";
|
|
34
97
|
variant?: "default" | "success" | "warning" | "error";
|
|
98
|
+
/** Status text under the field. Shown with a `success`, `warning` or `error` variant; for neutral help, use `hint`. */
|
|
35
99
|
message?: string;
|
|
100
|
+
/** Help text under the field, read out with it. */
|
|
101
|
+
hint?: string;
|
|
102
|
+
/** An error under the field. It marks the field invalid and is announced; it wins over `variant` and `message`. */
|
|
103
|
+
error?: string;
|
|
36
104
|
onchange?: (event: Event) => void;
|
|
37
105
|
onEmptyStateAction?: () => void;
|
|
38
|
-
|
|
106
|
+
/** Ids of other elements that describe the field. The hint and the message are added after them. */
|
|
107
|
+
"aria-describedby"?: string | null;
|
|
108
|
+
};
|
|
39
109
|
|
|
40
110
|
let {
|
|
111
|
+
class: className = "",
|
|
112
|
+
id: idProp,
|
|
41
113
|
value = $bindable(undefined),
|
|
42
114
|
options = [],
|
|
43
115
|
searchable = true,
|
|
44
|
-
|
|
45
|
-
|
|
116
|
+
strings,
|
|
117
|
+
searchPlaceholder,
|
|
118
|
+
maxMenuHeight = "60dvh",
|
|
46
119
|
menuWidth = "100%",
|
|
47
|
-
|
|
120
|
+
presentation = "auto",
|
|
121
|
+
noResultsText,
|
|
48
122
|
isLoading = false,
|
|
49
|
-
loadingText
|
|
50
|
-
emptyStateTitle
|
|
51
|
-
emptyStateDescription
|
|
123
|
+
loadingText,
|
|
124
|
+
emptyStateTitle,
|
|
125
|
+
emptyStateDescription,
|
|
52
126
|
emptyStateActionLabel = "",
|
|
53
|
-
placeholder
|
|
127
|
+
placeholder,
|
|
54
128
|
label = "",
|
|
129
|
+
name = "",
|
|
55
130
|
required = false,
|
|
56
131
|
disabled = false,
|
|
57
132
|
size = "md",
|
|
58
133
|
variant = "default",
|
|
59
134
|
message = "",
|
|
135
|
+
hint = "",
|
|
136
|
+
error = "",
|
|
60
137
|
onchange,
|
|
61
138
|
onEmptyStateAction,
|
|
139
|
+
"aria-describedby": describedBy,
|
|
62
140
|
...restProps
|
|
63
141
|
}: Props = $props();
|
|
64
142
|
|
|
143
|
+
/** The defaults, then a provider's words, then `strings`, then the older props where they are set. */
|
|
144
|
+
const provided = zabiStringsFor("select");
|
|
145
|
+
const common = zabiCommonStrings();
|
|
146
|
+
const text = $derived<SelectStrings>({
|
|
147
|
+
...mergeStrings(
|
|
148
|
+
{
|
|
149
|
+
...DEFAULT_SELECT_STRINGS,
|
|
150
|
+
// The sheet's three buttons say what every sheet's do.
|
|
151
|
+
closeLabel: common().close,
|
|
152
|
+
expandLabel: common().expand,
|
|
153
|
+
collapseLabel: common().collapse,
|
|
154
|
+
},
|
|
155
|
+
provided(),
|
|
156
|
+
strings,
|
|
157
|
+
),
|
|
158
|
+
...(placeholder !== undefined && { placeholder }),
|
|
159
|
+
...(searchPlaceholder !== undefined && { searchPlaceholder }),
|
|
160
|
+
...(noResultsText !== undefined && { noResults: noResultsText }),
|
|
161
|
+
...(loadingText !== undefined && { loading: loadingText }),
|
|
162
|
+
...(emptyStateTitle !== undefined && { emptyTitle: emptyStateTitle }),
|
|
163
|
+
...(emptyStateDescription !== undefined && { emptyDescription: emptyStateDescription }),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const isNative = $derived(presentation === "native");
|
|
167
|
+
/**
|
|
168
|
+
* False on the server and until the component has mounted: the native
|
|
169
|
+
* select is the control. True after: the trigger is. The two have the
|
|
170
|
+
* same box, so nothing moves when one replaces the other.
|
|
171
|
+
*/
|
|
172
|
+
let mounted = $state(false);
|
|
173
|
+
/** Whether the native select is what is seen and pressed. */
|
|
174
|
+
const nativeShown = $derived(isNative || !mounted);
|
|
175
|
+
|
|
176
|
+
let nativeElement = $state<HTMLSelectElement | null>(null);
|
|
177
|
+
/** What the browser said was wrong with the hidden native select (`required`), in its own words. */
|
|
178
|
+
let nativeError = $state("");
|
|
179
|
+
|
|
180
|
+
const status = $derived(
|
|
181
|
+
fieldMessageState({ variant, message, hint, error: error || nativeError }),
|
|
182
|
+
);
|
|
183
|
+
|
|
65
184
|
let isOpen = $state(false);
|
|
66
185
|
let selectContainer: HTMLDivElement;
|
|
67
186
|
let searchQuery = $state("");
|
|
68
187
|
|
|
188
|
+
// Same fixed height scale as Button, IconButton and Input (32 / 40 / 48),
|
|
189
|
+
// and the same 16px text below `sm`, so a Select beside an Input matches it.
|
|
69
190
|
const sizeClass = $derived(() => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
padding: "px-4 py-3",
|
|
79
|
-
text: "text-base",
|
|
80
|
-
leading: "leading-6",
|
|
81
|
-
};
|
|
82
|
-
} else {
|
|
83
|
-
return {
|
|
84
|
-
padding: "px-4 py-2.5",
|
|
85
|
-
text: "text-base",
|
|
86
|
-
leading: "leading-6",
|
|
87
|
-
};
|
|
88
|
-
}
|
|
191
|
+
// A minimum height, so a label that wraps makes the trigger taller
|
|
192
|
+
// instead of being cut. One line is exactly 32, 40 or 48px: the
|
|
193
|
+
// padding leaves room for the line and the border inside it, and is
|
|
194
|
+
// a step smaller below `sm`, where the text is 16px on a 24px line. `native` is the same box for the native
|
|
195
|
+
// select, which is always one line.
|
|
196
|
+
if (size === "sm") return { box: "min-h-8 px-3 py-1 max-sm:py-0 pointer-coarse:min-h-11", text: "text-sm max-sm:text-base", native: "h-8 ps-3 pe-9 pointer-coarse:h-11", chevron: "end-3" };
|
|
197
|
+
if (size === "lg") return { box: "min-h-12 px-4 py-2", text: "text-base", native: "h-12 ps-4 pe-10", chevron: "end-4" };
|
|
198
|
+
return { box: "min-h-10 px-3 py-2 max-sm:py-1 pointer-coarse:min-h-11", text: "text-sm max-sm:text-base", native: "h-10 ps-3 pe-9 pointer-coarse:h-11", chevron: "end-3" };
|
|
89
199
|
});
|
|
90
200
|
|
|
91
201
|
const variantClass = $derived(() => {
|
|
92
|
-
return variant === "success"
|
|
202
|
+
return status.variant === "success"
|
|
93
203
|
? "border-success focus-visible:border-success"
|
|
94
|
-
: variant === "warning"
|
|
204
|
+
: status.variant === "warning"
|
|
95
205
|
? "border-warning focus-visible:border-warning"
|
|
96
|
-
: variant === "error"
|
|
206
|
+
: status.variant === "error"
|
|
97
207
|
? "border-error focus-visible:border-error"
|
|
98
|
-
: "border-input-border";
|
|
208
|
+
: "border-input-border enabled:hover:border-input-border-hover";
|
|
99
209
|
});
|
|
100
210
|
|
|
211
|
+
/** What the trigger and the visible native select share: the field's surface, edge and states. */
|
|
212
|
+
const fieldSurface =
|
|
213
|
+
"focus-ring w-full min-w-0 cursor-pointer rounded-control border bg-input text-body transition-colors duration-150 hover:bg-input-hover active:bg-input-active focus-visible:bg-input-focus focus:outline-none focus-visible:outline-none disabled:cursor-not-allowed disabled:bg-input-disabled disabled:text-action-disabled-text";
|
|
214
|
+
|
|
101
215
|
const triggerClasses = $derived(() => {
|
|
102
216
|
const sizeStyles = sizeClass();
|
|
103
|
-
const baseClasses =
|
|
104
|
-
"focus-ring flex w-full cursor-pointer items-center justify-between rounded-lg border bg-input text-body transition-all duration-200 hover:bg-input-hover active:bg-input-focus focus-visible:bg-input-focus focus:outline-none focus-visible:outline-none disabled:cursor-not-allowed disabled:bg-input-disabled disabled:opacity-50";
|
|
217
|
+
const baseClasses = `${fieldSurface} flex items-center justify-between gap-2`;
|
|
105
218
|
|
|
106
|
-
return
|
|
219
|
+
return cn(
|
|
220
|
+
`${baseClasses} ${sizeStyles.box} ${sizeStyles.text} ${variantClass()}`,
|
|
221
|
+
// Until it has mounted the native select stands in its place.
|
|
222
|
+
!mounted && "hidden",
|
|
223
|
+
);
|
|
107
224
|
});
|
|
108
225
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return "text-warning text-sm mt-1 flex items-center gap-1.5 w-full";
|
|
226
|
+
const nativeClasses = $derived.by(() => {
|
|
227
|
+
const sizeStyles = sizeClass();
|
|
228
|
+
if (!nativeShown) {
|
|
229
|
+
// Mounted, with a list of our own: still the form control, laid
|
|
230
|
+
// over the trigger so the browser's validation has a place to
|
|
231
|
+
// point at, but not seen, pressed or reached. `invisible`
|
|
232
|
+
// (`visibility: hidden`) is what takes it out of the tab order
|
|
233
|
+
// and the accessibility tree for certain; a form still sends and
|
|
234
|
+
// validates it.
|
|
235
|
+
return "pointer-events-none invisible absolute inset-0 h-full w-full";
|
|
120
236
|
}
|
|
121
|
-
return
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (variant === "error") return AlertCircle;
|
|
126
|
-
if (variant === "success") return CheckCircle;
|
|
127
|
-
if (variant === "warning") return AlertTriangle;
|
|
128
|
-
return null;
|
|
237
|
+
return cn(
|
|
238
|
+
`${fieldSurface} block appearance-none truncate ${sizeStyles.native} ${sizeStyles.text} ${variantClass()}`,
|
|
239
|
+
!disabled && isEmpty() && "text-input-placeholder",
|
|
240
|
+
);
|
|
129
241
|
});
|
|
130
242
|
|
|
131
|
-
const
|
|
243
|
+
const labelClasses = $derived(
|
|
244
|
+
() => "block text-sm font-medium text-label mb-2",
|
|
245
|
+
);
|
|
132
246
|
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
return String(placeholder || "Select an option");
|
|
136
|
-
}
|
|
137
|
-
const selected = options.find((opt) => opt.value === value);
|
|
138
|
-
return selected?.label
|
|
139
|
-
? String(selected.label)
|
|
140
|
-
: String(placeholder || "Select an option");
|
|
141
|
-
});
|
|
247
|
+
const fallbackId = generateId("select");
|
|
248
|
+
const selectId = $derived(idProp ?? fallbackId);
|
|
142
249
|
|
|
143
250
|
const isEmpty = $derived(() => {
|
|
144
251
|
return value === undefined || value === null || value === "";
|
|
145
252
|
});
|
|
146
253
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
254
|
+
/** String comparison so `"2"` and `2` match — same rule as Dropdown's `selectedValue`. */
|
|
255
|
+
function isSameValue(
|
|
256
|
+
a: string | number | null | undefined,
|
|
257
|
+
b: string | number | null | undefined,
|
|
258
|
+
): boolean {
|
|
259
|
+
if (a === undefined || a === null || b === undefined || b === null) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
return String(a) === String(b);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const selectedOption = $derived.by(() =>
|
|
266
|
+
options.find((opt) => isSameValue(opt.value, value)),
|
|
267
|
+
);
|
|
152
268
|
|
|
153
269
|
const hasSearchQuery = $derived(() => {
|
|
154
270
|
return searchQuery.trim().length > 0;
|
|
@@ -156,7 +272,7 @@
|
|
|
156
272
|
|
|
157
273
|
const filteredOptions = $derived(() => {
|
|
158
274
|
const query = searchQuery.trim().toLowerCase();
|
|
159
|
-
const availableOptions =
|
|
275
|
+
const availableOptions = options;
|
|
160
276
|
if (!searchable || query.length === 0) {
|
|
161
277
|
return availableOptions;
|
|
162
278
|
}
|
|
@@ -173,8 +289,14 @@
|
|
|
173
289
|
|
|
174
290
|
function handleOptionClick(optionValue: string | number) {
|
|
175
291
|
if (disabled || isLoading) return;
|
|
176
|
-
value
|
|
292
|
+
if (options.find((option) => option.value === optionValue)?.disabled) return;
|
|
177
293
|
isOpen = false;
|
|
294
|
+
choose(optionValue);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Sets the value and tells the app, as a change of a form control does. */
|
|
298
|
+
function choose(optionValue: string | number | undefined) {
|
|
299
|
+
value = optionValue;
|
|
178
300
|
|
|
179
301
|
if (onchange) {
|
|
180
302
|
const syntheticEvent = new Event("change", { bubbles: true });
|
|
@@ -186,6 +308,124 @@
|
|
|
186
308
|
}
|
|
187
309
|
}
|
|
188
310
|
|
|
311
|
+
/**
|
|
312
|
+
* The native select's value as one of the options' own: a number stays a
|
|
313
|
+
* number. The empty first option is "nothing chosen".
|
|
314
|
+
*/
|
|
315
|
+
function fromNative(raw: string): string | number | undefined {
|
|
316
|
+
if (raw === "") return undefined;
|
|
317
|
+
return options.find((option) => String(option.value) === raw)?.value ?? raw;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** The native select was changed: by the visitor, in `native`, or by the browser restoring a form. */
|
|
321
|
+
function handleNativeChange(event: Event) {
|
|
322
|
+
const raw = (event.currentTarget as HTMLSelectElement).value;
|
|
323
|
+
if (raw === (isEmpty() ? "" : String(value))) return;
|
|
324
|
+
choose(fromNative(raw));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* The browser found the hidden native select invalid (`required`, and
|
|
329
|
+
* nothing chosen). Its own bubble would point at a control nobody can
|
|
330
|
+
* see, and focus would go into it: the message is shown under the field
|
|
331
|
+
* instead, in the browser's words, and focus goes to the trigger.
|
|
332
|
+
*/
|
|
333
|
+
function handleNativeInvalid(event: Event) {
|
|
334
|
+
if (nativeShown) return;
|
|
335
|
+
event.preventDefault();
|
|
336
|
+
const element = event.currentTarget as HTMLSelectElement;
|
|
337
|
+
nativeError = element.validationMessage;
|
|
338
|
+
// The browser tells every invalid control, and would focus the first.
|
|
339
|
+
// So does this: each Select shows its message, and only the first
|
|
340
|
+
// invalid control of the form takes focus.
|
|
341
|
+
const controls = [...(element.form?.elements ?? [])] as (Element & Partial<HTMLSelectElement>)[];
|
|
342
|
+
const first = controls.find((control) => control.willValidate && control.validity?.valid === false);
|
|
343
|
+
if (!first || first === element) document.getElementById(selectId)?.focus();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
$effect(() => {
|
|
347
|
+
// A choice answers it.
|
|
348
|
+
void value;
|
|
349
|
+
nativeError = "";
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* The trigger takes over from the native select. If the keyboard was in
|
|
354
|
+
* the native select, which now leaves the tab order, focus goes to the
|
|
355
|
+
* trigger and does not fall to the page.
|
|
356
|
+
*/
|
|
357
|
+
function handOver() {
|
|
358
|
+
const hadFocus = !isNative && nativeElement !== null && document.activeElement === nativeElement;
|
|
359
|
+
mounted = true;
|
|
360
|
+
if (hadFocus) void tick().then(() => document.getElementById(selectId)?.focus());
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** The value the Select was rendered with: what a form reset goes back to. */
|
|
364
|
+
const initialValue = untrack(() => value);
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The form around the Select was reset. The native select goes back to
|
|
368
|
+
* what its markup says by itself, which the component would not know of:
|
|
369
|
+
* the value is put back to the one it was rendered with, and that is
|
|
370
|
+
* reported once if it is a change. After the browser has done its own
|
|
371
|
+
* resetting, which is after the event.
|
|
372
|
+
*/
|
|
373
|
+
function handleFormReset(event: Event) {
|
|
374
|
+
setTimeout(() => {
|
|
375
|
+
if (event.defaultPrevented) return;
|
|
376
|
+
const initiallyEmpty = initialValue === undefined || initialValue === null || initialValue === "";
|
|
377
|
+
const unchanged = initiallyEmpty ? isEmpty() : isSameValue(value, initialValue);
|
|
378
|
+
if (!unchanged) choose(initiallyEmpty ? undefined : initialValue);
|
|
379
|
+
void tick().then(() => {
|
|
380
|
+
if (nativeElement) nativeElement.value = initiallyEmpty ? "" : String(initialValue);
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Whether the browser's own list of the native select is open, where the browser can say (`:open`). */
|
|
386
|
+
function nativePickerOpen(element: HTMLSelectElement): boolean {
|
|
387
|
+
try {
|
|
388
|
+
return element.matches(":open");
|
|
389
|
+
} catch {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
onMount(() => {
|
|
395
|
+
// A choice made in the native select before the scripts arrived is
|
|
396
|
+
// the visitor's: it becomes the value, and is reported once. Only a
|
|
397
|
+
// choice: a select nobody touched keeps the value it was given.
|
|
398
|
+
const before = nativeElement ? choiceBeforeMount(nativeElement) : undefined;
|
|
399
|
+
if (before !== undefined && before !== (isEmpty() ? "" : String(value))) {
|
|
400
|
+
choose(fromNative(before));
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const element = nativeElement;
|
|
404
|
+
const form = element?.form ?? null;
|
|
405
|
+
form?.addEventListener("reset", handleFormReset);
|
|
406
|
+
const stopListening = () => form?.removeEventListener("reset", handleFormReset);
|
|
407
|
+
|
|
408
|
+
// Someone is choosing in the browser's own list right now: taking the
|
|
409
|
+
// select away would close it under their finger. They keep it until
|
|
410
|
+
// they have chosen or left, and the trigger takes over then.
|
|
411
|
+
if (!isNative && element && document.activeElement === element && nativePickerOpen(element)) {
|
|
412
|
+
const finish = () => {
|
|
413
|
+
element.removeEventListener("change", finish);
|
|
414
|
+
element.removeEventListener("blur", finish);
|
|
415
|
+
handOver();
|
|
416
|
+
};
|
|
417
|
+
element.addEventListener("change", finish);
|
|
418
|
+
element.addEventListener("blur", finish);
|
|
419
|
+
return () => {
|
|
420
|
+
element.removeEventListener("change", finish);
|
|
421
|
+
element.removeEventListener("blur", finish);
|
|
422
|
+
stopListening();
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
handOver();
|
|
426
|
+
return stopListening;
|
|
427
|
+
});
|
|
428
|
+
|
|
189
429
|
function handleTriggerClick(event: MouseEvent) {
|
|
190
430
|
if (disabled || isLoading) return;
|
|
191
431
|
event.stopPropagation();
|
|
@@ -196,6 +436,10 @@
|
|
|
196
436
|
if (!isOpen) return;
|
|
197
437
|
|
|
198
438
|
const target = event.target as HTMLElement;
|
|
439
|
+
if (isInsideToastRegion(target)) return;
|
|
440
|
+
// The list in a sheet is in `<body>`, not in this container: a press
|
|
441
|
+
// in that sheet (an option, the grip, the search field) is not outside.
|
|
442
|
+
if (target.closest('[role="dialog"]')?.querySelector("[data-dropdown-sheet]")) return;
|
|
199
443
|
const clickedSelectContainer = target.closest(".select-container");
|
|
200
444
|
|
|
201
445
|
if (
|
|
@@ -215,115 +459,222 @@
|
|
|
215
459
|
|
|
216
460
|
<svelte:window onclick={handleClickOutside} onkeydown={handleKeydown} />
|
|
217
461
|
|
|
218
|
-
<div bind:this={selectContainer} class="w-full select-container">
|
|
462
|
+
<div bind:this={selectContainer} class={cn("w-full min-w-0 select-container", className)}>
|
|
219
463
|
{#if label}
|
|
220
464
|
<label for={selectId} class={labelClasses()}>{label}</label>
|
|
221
465
|
{/if}
|
|
222
466
|
|
|
467
|
+
<div class="relative w-full min-w-0">
|
|
468
|
+
<!-- The native select. While it is the control it has the field's id, so
|
|
469
|
+
the label and the messages are its; once the trigger has taken over it is
|
|
470
|
+
hidden from assistive technology and out of the tab order, and only the
|
|
471
|
+
form talks to it. A disabled Select has always sent its value, so the
|
|
472
|
+
hidden one is not disabled; what is not sent is its `required`. -->
|
|
473
|
+
<select
|
|
474
|
+
bind:this={nativeElement}
|
|
475
|
+
id={nativeShown ? selectId : `${selectId}-native`}
|
|
476
|
+
name={name || undefined}
|
|
477
|
+
class={nativeClasses}
|
|
478
|
+
required={required && (nativeShown || !(disabled || isLoading))}
|
|
479
|
+
disabled={nativeShown && (disabled || isLoading)}
|
|
480
|
+
tabindex={nativeShown ? undefined : -1}
|
|
481
|
+
aria-hidden={nativeShown ? undefined : "true"}
|
|
482
|
+
aria-invalid={nativeShown && status.variant === "error" ? "true" : undefined}
|
|
483
|
+
aria-describedby={nativeShown ? fieldDescribedBy(selectId, status, describedBy) : undefined}
|
|
484
|
+
onchange={handleNativeChange}
|
|
485
|
+
oninvalid={handleNativeInvalid}
|
|
486
|
+
aria-label={nativeShown ? restProps["aria-label"] : undefined}
|
|
487
|
+
aria-labelledby={nativeShown ? restProps["aria-labelledby"] : undefined}
|
|
488
|
+
{...{ [NATIVE_SELECT_ATTRIBUTE]: "" }}
|
|
489
|
+
{...isNative ? (restProps as Record<string, unknown>) : {}}
|
|
490
|
+
>
|
|
491
|
+
{#if isEmpty()}
|
|
492
|
+
<option value="" selected>{isLoading ? text.loading : text.placeholder}</option>
|
|
493
|
+
{:else if !selectedOption}
|
|
494
|
+
<!-- The value has no option of its own: its options are still on
|
|
495
|
+
their way, or it is not among them. It is still the value, so the
|
|
496
|
+
select holds it and a form sends it; it is not offered in the list
|
|
497
|
+
where the browser lets an option be hidden. -->
|
|
498
|
+
<option value={String(value)} selected hidden>{isLoading ? text.loading : String(value)}</option>
|
|
499
|
+
{/if}
|
|
500
|
+
{#each options as option (option.value)}
|
|
501
|
+
<option
|
|
502
|
+
value={String(option.value)}
|
|
503
|
+
disabled={option.disabled}
|
|
504
|
+
selected={isSameValue(value, option.value)}
|
|
505
|
+
>
|
|
506
|
+
{option.label}
|
|
507
|
+
</option>
|
|
508
|
+
{/each}
|
|
509
|
+
</select>
|
|
510
|
+
{#if nativeShown}
|
|
511
|
+
<!-- `appearance-none` takes the browser's arrow away with its chrome. -->
|
|
512
|
+
<span
|
|
513
|
+
class={cn(
|
|
514
|
+
"pointer-events-none absolute top-1/2 me-px flex -translate-y-1/2 text-description",
|
|
515
|
+
sizeClass().chevron,
|
|
516
|
+
)}
|
|
517
|
+
aria-hidden="true"
|
|
518
|
+
>
|
|
519
|
+
<ChevronDown size={20} />
|
|
520
|
+
</span>
|
|
521
|
+
{/if}
|
|
522
|
+
|
|
523
|
+
{#if !isNative}
|
|
223
524
|
<Dropdown
|
|
224
525
|
bind:isOpen={isOpen}
|
|
225
526
|
placement="bottom-start"
|
|
226
527
|
selectedValue={value}
|
|
227
528
|
onOptionClick={handleOptionClick}
|
|
228
|
-
ariaLabel="
|
|
529
|
+
ariaLabel={strings?.listLabel || label || (restProps["aria-label"] as string | undefined) || text.listLabel}
|
|
530
|
+
ariaLabelledby={strings?.listLabel || label ? undefined : (restProps["aria-labelledby"] as string | undefined)}
|
|
229
531
|
menuRole="listbox"
|
|
532
|
+
fullWidth
|
|
533
|
+
presentation={presentation === "native" ? "popover" : presentation}
|
|
534
|
+
sheetTitle={label || text.placeholder}
|
|
535
|
+
sheetCloseLabel={text.closeLabel}
|
|
536
|
+
sheetExpandLabel={text.expandLabel}
|
|
537
|
+
sheetCollapseLabel={text.collapseLabel}
|
|
538
|
+
sheetSnap={searchable && options.length > 6 ? "full" : "half"}
|
|
230
539
|
>
|
|
231
540
|
{#snippet trigger(aria)}
|
|
541
|
+
<!-- A select-only combobox, as the ARIA practices describe it:
|
|
542
|
+
a button could not say that it is invalid or required
|
|
543
|
+
(`aria-invalid` and `aria-required` are not a button's), so a
|
|
544
|
+
Select with an error was announced like one without. The keys,
|
|
545
|
+
`aria-haspopup="listbox"`, `aria-expanded` and `aria-controls` are
|
|
546
|
+
as they were. -->
|
|
232
547
|
<button
|
|
233
548
|
type="button"
|
|
234
|
-
id={selectId}
|
|
549
|
+
id={mounted ? selectId : `${selectId}-trigger`}
|
|
235
550
|
class={triggerClasses()}
|
|
236
551
|
{disabled}
|
|
237
552
|
onclick={handleTriggerClick}
|
|
553
|
+
{...restProps}
|
|
238
554
|
{...aria}
|
|
239
|
-
|
|
555
|
+
role="combobox"
|
|
556
|
+
aria-invalid={status.variant === "error" ? "true" : undefined}
|
|
557
|
+
aria-required={required ? "true" : undefined}
|
|
558
|
+
aria-busy={isLoading ? "true" : undefined}
|
|
559
|
+
aria-describedby={fieldDescribedBy(selectId, status, describedBy)}
|
|
240
560
|
>
|
|
561
|
+
<!-- An empty trigger shows its placeholder in the placeholder
|
|
562
|
+
colour, as Input does. A value has no colour class of its own,
|
|
563
|
+
and a disabled trigger none at all: the button's `text-body`
|
|
564
|
+
and its disabled colour are inherited. A colour class here
|
|
565
|
+
beat the button's `disabled:` one, and a disabled Select read
|
|
566
|
+
as enabled. -->
|
|
567
|
+
<!-- The label wraps: cut off, there was nowhere on the closed
|
|
568
|
+
control to read the rest. The chevron stays in the middle of
|
|
569
|
+
the box, however many lines there are. -->
|
|
241
570
|
<span
|
|
242
|
-
class="
|
|
243
|
-
? '
|
|
244
|
-
:
|
|
245
|
-
|
|
246
|
-
|
|
571
|
+
class="min-w-0 flex-1 text-start [overflow-wrap:anywhere] {disabled
|
|
572
|
+
? ''
|
|
573
|
+
: isLoading
|
|
574
|
+
? 'text-description'
|
|
575
|
+
: isEmpty()
|
|
576
|
+
? 'text-input-placeholder'
|
|
577
|
+
: ''}"
|
|
247
578
|
>
|
|
248
579
|
{#if isLoading}
|
|
249
|
-
{
|
|
580
|
+
{text.loading}
|
|
250
581
|
{:else}
|
|
251
582
|
{isEmpty()
|
|
252
|
-
? placeholder
|
|
253
|
-
:
|
|
254
|
-
placeholder}
|
|
583
|
+
? text.placeholder
|
|
584
|
+
: selectedOption?.label || text.placeholder}
|
|
255
585
|
{/if}
|
|
256
586
|
</span>
|
|
257
587
|
<ChevronDown
|
|
258
588
|
size={20}
|
|
259
|
-
class="text-description transition-transform duration-200 {isOpen
|
|
589
|
+
class="shrink-0 text-description transition-transform duration-200 motion-reduce:transition-none {isOpen
|
|
260
590
|
? 'rotate-180'
|
|
261
591
|
: ''}"
|
|
262
592
|
/>
|
|
263
593
|
</button>
|
|
264
594
|
{/snippet}
|
|
595
|
+
{#snippet header()}
|
|
596
|
+
{#if searchable && !isLoading}
|
|
597
|
+
<!-- Outside role="listbox": a textbox is not a valid listbox child. -->
|
|
598
|
+
<!-- 0.5rem and the panel's 1px border from the corner, as the
|
|
599
|
+
options below and a Dropdown's items are: an 8px field 9px
|
|
600
|
+
inside the panel's 16px corner is concentric with it. -->
|
|
601
|
+
<div class="px-2 pb-2 pt-1" style:width={menuWidth} data-menu-width>
|
|
602
|
+
<Input
|
|
603
|
+
type="text"
|
|
604
|
+
size="sm"
|
|
605
|
+
class="min-w-0"
|
|
606
|
+
placeholder={text.searchPlaceholder}
|
|
607
|
+
bind:value={searchQuery}
|
|
608
|
+
aria-label={text.searchPlaceholder}
|
|
609
|
+
{disabled}
|
|
610
|
+
/>
|
|
611
|
+
</div>
|
|
612
|
+
{/if}
|
|
613
|
+
{/snippet}
|
|
265
614
|
{#snippet children()}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
type="text"
|
|
271
|
-
size="sm"
|
|
272
|
-
class="min-w-0"
|
|
273
|
-
placeholder={searchPlaceholder}
|
|
274
|
-
bind:value={searchQuery}
|
|
275
|
-
aria-label={searchPlaceholder}
|
|
276
|
-
{disabled}
|
|
277
|
-
/>
|
|
278
|
-
</div>
|
|
279
|
-
{/if}
|
|
615
|
+
<!-- `px-1` twice: the inner one is room for an option's focus
|
|
616
|
+
ring inside the scrolling box, and together with the panel's
|
|
617
|
+
border they put the options 9px from its corner. -->
|
|
618
|
+
<div class="px-1 pb-2 pt-1" style:width={menuWidth} data-menu-width>
|
|
280
619
|
<div
|
|
281
620
|
class="overflow-y-auto px-1"
|
|
621
|
+
data-menu-scroll
|
|
282
622
|
style:max-height={maxMenuHeight}
|
|
283
623
|
>
|
|
284
624
|
{#if isLoading}
|
|
285
625
|
<div class="space-y-2 px-2 py-2" role="status" aria-live="polite">
|
|
286
|
-
<div class="h-8 w-full animate-pulse rounded-
|
|
287
|
-
<div class="h-8 w-full animate-pulse rounded-
|
|
288
|
-
<p class="text-xs text-description">{
|
|
626
|
+
<div class="h-8 w-full animate-pulse rounded-control bg-base-200"></div>
|
|
627
|
+
<div class="h-8 w-full animate-pulse rounded-control bg-base-200"></div>
|
|
628
|
+
<p class="text-xs text-description">{text.loading}</p>
|
|
289
629
|
</div>
|
|
290
630
|
{:else if filteredOptions().length > 0}
|
|
291
631
|
{#each filteredOptions() as option (option.value)}
|
|
292
632
|
{@const buttonRestProps = {
|
|
293
633
|
"data-value": String(option.value),
|
|
294
634
|
} as Record<string, string>}
|
|
295
|
-
<div class="w-full my-
|
|
635
|
+
<div class="w-full my-1">
|
|
636
|
+
<!-- `aria-disabled`, not `disabled`: a natively disabled button cannot take focus, so the arrow keys stopped at the option before it. The click is refused in `handleOptionClick`. -->
|
|
296
637
|
<button
|
|
297
638
|
type="button"
|
|
298
639
|
role="option"
|
|
299
|
-
aria-selected={value
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
? 'border-
|
|
305
|
-
:
|
|
306
|
-
|
|
640
|
+
aria-selected={isSameValue(value, option.value)}
|
|
641
|
+
class="focus-ring flex w-full items-center justify-start gap-2 rounded-control border-2 px-3 py-2 pointer-coarse:py-3 text-start text-sm font-medium transition-colors focus:outline-none aria-disabled:cursor-not-allowed aria-disabled:opacity-50 {isSameValue(
|
|
642
|
+
value,
|
|
643
|
+
option.value,
|
|
644
|
+
)
|
|
645
|
+
? 'border-transparent bg-surface-overlay-hover text-headline active:bg-surface-active'
|
|
646
|
+
: option.disabled
|
|
647
|
+
? 'border-transparent bg-transparent text-body'
|
|
648
|
+
: 'border-transparent bg-transparent text-body hover:bg-surface-overlay-hover active:bg-surface-active'}"
|
|
649
|
+
aria-disabled={option.disabled ? "true" : undefined}
|
|
307
650
|
{...buttonRestProps}
|
|
308
651
|
onclick={() =>
|
|
309
652
|
handleOptionClick(option.value)}
|
|
310
653
|
>
|
|
311
|
-
{option.label}
|
|
654
|
+
<span class="min-w-0 flex-1">{option.label}</span>
|
|
655
|
+
<!-- The chosen option's mark: a check and a
|
|
656
|
+
fill. It used to be an edge in the action
|
|
657
|
+
colour, which is what the focus ring draws:
|
|
658
|
+
the chosen and the focused option could
|
|
659
|
+
not be told apart. -->
|
|
660
|
+
{#if isSameValue(value, option.value)}
|
|
661
|
+
<Check size={16} class="shrink-0" aria-hidden="true" />
|
|
662
|
+
{/if}
|
|
312
663
|
</button>
|
|
313
664
|
</div>
|
|
314
665
|
{/each}
|
|
315
666
|
{:else if hasSearchQuery()}
|
|
316
667
|
<div class="px-3 py-2 text-sm text-description">
|
|
317
|
-
{
|
|
668
|
+
{text.noResults}
|
|
318
669
|
</div>
|
|
319
670
|
{:else}
|
|
320
|
-
<div class="rounded-
|
|
321
|
-
<p class="font-medium text-headline">{
|
|
322
|
-
<p class="mt-1 text-description">{
|
|
671
|
+
<div class="rounded-control border border-border bg-surface-overlay-hover px-3 py-3 text-sm">
|
|
672
|
+
<p class="font-medium text-headline">{text.emptyTitle}</p>
|
|
673
|
+
<p class="mt-1 text-description">{text.emptyDescription}</p>
|
|
323
674
|
{#if emptyStateActionLabel && onEmptyStateAction}
|
|
324
675
|
<button
|
|
325
676
|
type="button"
|
|
326
|
-
class="mt-3 inline-flex min-h-11 cursor-pointer items-center rounded-
|
|
677
|
+
class="mt-3 inline-flex min-h-11 cursor-pointer items-center rounded-control bg-action-primary px-3 py-2 text-sm text-action-primary"
|
|
327
678
|
onclick={onEmptyStateAction}
|
|
328
679
|
>
|
|
329
680
|
{emptyStateActionLabel}
|
|
@@ -335,18 +686,7 @@
|
|
|
335
686
|
</div>
|
|
336
687
|
{/snippet}
|
|
337
688
|
</Dropdown>
|
|
338
|
-
{#if message && variant !== "default"}
|
|
339
|
-
<p
|
|
340
|
-
id={`${selectId}-message`}
|
|
341
|
-
class={messageClasses()}
|
|
342
|
-
role={variant === "error" ? "alert" : "status"}
|
|
343
|
-
aria-live={variant === "error" ? "assertive" : "polite"}
|
|
344
|
-
>
|
|
345
|
-
{#if getIcon()}
|
|
346
|
-
{@const Icon = getIcon()}
|
|
347
|
-
<Icon size={14} class="shrink-0" />
|
|
348
|
-
{/if}
|
|
349
|
-
<span>{message}</span>
|
|
350
|
-
</p>
|
|
351
689
|
{/if}
|
|
690
|
+
</div>
|
|
691
|
+
<FieldMessages fieldId={selectId} {status} {hint} gap="mt-1" messageClass="w-full" />
|
|
352
692
|
</div>
|