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
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { zabiCommonStrings } from "../util/zabi-strings.js";
|
|
2
3
|
import { generateId } from "../util/ssr-safe.js";
|
|
3
|
-
import type
|
|
4
|
+
import { onMount, setContext, type Snippet } from 'svelte';
|
|
5
|
+
import { cn } from "../util/cn.js";
|
|
6
|
+
import {
|
|
7
|
+
DROPDOWN_CONTEXT_KEY,
|
|
8
|
+
opensAsSheet,
|
|
9
|
+
type DropdownContext,
|
|
10
|
+
type DropdownOption,
|
|
11
|
+
type DropdownPresentation,
|
|
12
|
+
} from "../util/dropdown.js";
|
|
13
|
+
import type { BottomSheetSnap } from "../util/bottom-sheet.js";
|
|
14
|
+
import {
|
|
15
|
+
measurePlacement,
|
|
16
|
+
unfitted,
|
|
17
|
+
watchViewport,
|
|
18
|
+
type PanelPlacement,
|
|
19
|
+
} from "../util/fit-in-viewport.js";
|
|
20
|
+
import { isInsideToastRegion } from "../util/focus-utils.js";
|
|
21
|
+
import BottomSheet from './BottomSheet.svelte';
|
|
22
|
+
import DropdownItem from './DropdownItem.svelte';
|
|
4
23
|
|
|
5
24
|
export type DropdownTriggerProps = {
|
|
6
25
|
'aria-expanded': boolean;
|
|
@@ -9,38 +28,128 @@
|
|
|
9
28
|
};
|
|
10
29
|
|
|
11
30
|
interface Props {
|
|
31
|
+
/** Extra classes for the host element. */
|
|
32
|
+
class?: string;
|
|
12
33
|
isOpen?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The side the menu opens on. It is where the menu goes whenever it
|
|
36
|
+
* fits there; near an edge of the screen it flips to the other side,
|
|
37
|
+
* and is narrowed or given its own scroll when neither side has room.
|
|
38
|
+
*/
|
|
13
39
|
placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
|
|
40
|
+
/**
|
|
41
|
+
* How the menu is shown. `popover`: under its trigger, as always.
|
|
42
|
+
* `sheet`: in a BottomSheet, with the same items, roles and keys; this
|
|
43
|
+
* is the action sheet ("Edit, Share, Delete" from the bottom of the
|
|
44
|
+
* screen). `auto`: a sheet on a phone (a touch screen narrower than
|
|
45
|
+
* 640px), the pop-over everywhere else. Decided each time the menu
|
|
46
|
+
* opens, in the browser.
|
|
47
|
+
*/
|
|
48
|
+
presentation?: DropdownPresentation;
|
|
49
|
+
/**
|
|
50
|
+
* The host fills its container instead of being as wide as its
|
|
51
|
+
* trigger's content, and the menu is at least as wide as the host.
|
|
52
|
+
* For a trigger that is a field (Select); a menu on an icon button
|
|
53
|
+
* stays as it is.
|
|
54
|
+
*/
|
|
55
|
+
fullWidth?: boolean;
|
|
56
|
+
/** Heading of the sheet. Without it, `ariaLabel` is. */
|
|
57
|
+
sheetTitle?: string;
|
|
58
|
+
/**
|
|
59
|
+
* The height the sheet opens at. Without it: half the screen for up
|
|
60
|
+
* to six `options`, all of it for more. The grip moves it either way.
|
|
61
|
+
*/
|
|
62
|
+
sheetSnap?: BottomSheetSnap;
|
|
63
|
+
/** Accessible name of the sheet's close button. */
|
|
64
|
+
sheetCloseLabel?: string;
|
|
65
|
+
/** Accessible name of the sheet's grip while its button takes the sheet up a step. */
|
|
66
|
+
sheetExpandLabel?: string;
|
|
67
|
+
/** The same, while it takes the sheet down a step. */
|
|
68
|
+
sheetCollapseLabel?: string;
|
|
14
69
|
ariaLabel?: string;
|
|
70
|
+
/** Id of an element that names the list; wins over `ariaLabel` for the pop-over list. */
|
|
71
|
+
ariaLabelledby?: string;
|
|
15
72
|
/** `listbox` for Select-style; `menu` for actions. */
|
|
16
73
|
menuRole?: 'menu' | 'listbox';
|
|
17
74
|
selectedValue?: string | number | null;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Menu items, when not supplying children. Each takes an optional
|
|
77
|
+
* `icon`, `tone` (`danger`) and `description`; a disabled item stays
|
|
78
|
+
* focusable so its description can be read.
|
|
79
|
+
*/
|
|
80
|
+
options?: DropdownOption[];
|
|
23
81
|
onOptionClick?: (value: string | number) => void;
|
|
24
82
|
trigger: Snippet<[DropdownTriggerProps]>;
|
|
83
|
+
/** Rendered in the popup above the menu/listbox, outside its role (e.g. a search field). */
|
|
84
|
+
header?: Snippet;
|
|
25
85
|
children?: Snippet;
|
|
26
86
|
}
|
|
27
87
|
|
|
28
88
|
let {
|
|
29
|
-
|
|
89
|
+
class: className = "",
|
|
90
|
+
isOpen = $bindable<Exclude<Props["isOpen"], undefined>>(),
|
|
30
91
|
placement = 'bottom-start',
|
|
92
|
+
presentation = 'popover',
|
|
93
|
+
fullWidth = false,
|
|
94
|
+
sheetTitle = undefined,
|
|
95
|
+
sheetSnap = undefined,
|
|
96
|
+
sheetCloseLabel: sheetCloseLabelGiven,
|
|
97
|
+
sheetExpandLabel: sheetExpandLabelGiven,
|
|
98
|
+
sheetCollapseLabel: sheetCollapseLabelGiven,
|
|
31
99
|
ariaLabel = 'Menu',
|
|
100
|
+
ariaLabelledby = undefined,
|
|
32
101
|
menuRole = 'menu',
|
|
33
102
|
selectedValue = null,
|
|
34
103
|
options = [],
|
|
35
104
|
onOptionClick,
|
|
36
105
|
trigger,
|
|
106
|
+
header,
|
|
37
107
|
children,
|
|
38
108
|
...restProps
|
|
39
109
|
}: Props = $props();
|
|
40
110
|
|
|
111
|
+
// No fallback on a bindable prop: Svelte refuses `bind:…={undefined}` on
|
|
112
|
+
// one that has a fallback (`props_invalid_value`), and a page that throws
|
|
113
|
+
// while it hydrates never becomes interactive. The default is applied
|
|
114
|
+
// here instead: at once, for the server and the first render, and again
|
|
115
|
+
// whenever a parent hands back `undefined`.
|
|
116
|
+
const applyDefaults = () => {
|
|
117
|
+
if (isOpen === undefined) isOpen = false;
|
|
118
|
+
};
|
|
119
|
+
applyDefaults();
|
|
120
|
+
$effect.pre(applyDefaults);
|
|
121
|
+
|
|
122
|
+
/** Words many components share: a `ZabiStringsProvider` above this one may give them; else English. */
|
|
123
|
+
const common = zabiCommonStrings();
|
|
124
|
+
const sheetCloseLabel = $derived(sheetCloseLabelGiven ?? common().close);
|
|
125
|
+
const sheetExpandLabel = $derived(sheetExpandLabelGiven ?? common().expand);
|
|
126
|
+
const sheetCollapseLabel = $derived(sheetCollapseLabelGiven ?? common().collapse);
|
|
127
|
+
|
|
128
|
+
// `DropdownItem` reads its role from here, in `options` and in `children` alike.
|
|
129
|
+
setContext<DropdownContext>(DROPDOWN_CONTEXT_KEY, {
|
|
130
|
+
get itemRole() {
|
|
131
|
+
return menuRole === 'listbox' ? ('option' as const) : ('menuitem' as const);
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
41
135
|
const menuId = generateId('dropdown-menu');
|
|
42
136
|
let rootEl = $state<HTMLDivElement | null>(null);
|
|
43
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Whether the open menu is in a sheet. Worked out when `isOpen` turns
|
|
140
|
+
* true and kept while it is open, so turning the phone does not swap one
|
|
141
|
+
* for the other under the finger. Never before the component has mounted:
|
|
142
|
+
* a menu rendered open on the server is a pop-over, and stays one through
|
|
143
|
+
* hydration.
|
|
144
|
+
*/
|
|
145
|
+
let mounted = $state(false);
|
|
146
|
+
onMount(() => {
|
|
147
|
+
mounted = true;
|
|
148
|
+
});
|
|
149
|
+
const asSheet = $derived(isOpen && mounted && opensAsSheet(presentation));
|
|
150
|
+
/** The sheet's content, when the menu is in one: it is outside `rootEl`, in `<body>`. */
|
|
151
|
+
let sheetElement = $state<HTMLElement | null>(null);
|
|
152
|
+
|
|
44
153
|
let openedViaKeyboard = $state(false);
|
|
45
154
|
|
|
46
155
|
const triggerAria = $derived({
|
|
@@ -49,8 +158,22 @@
|
|
|
49
158
|
'aria-controls': menuId,
|
|
50
159
|
} satisfies DropdownTriggerProps);
|
|
51
160
|
|
|
161
|
+
function isTextEntryTarget(target: EventTarget | null): boolean {
|
|
162
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
163
|
+
if (target.isContentEditable || target instanceof HTMLTextAreaElement) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
return (
|
|
167
|
+
target instanceof HTMLInputElement &&
|
|
168
|
+
!['checkbox', 'radio', 'button', 'submit', 'reset'].includes(target.type)
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
52
172
|
function handleKeydown(event: KeyboardEvent) {
|
|
53
173
|
if (!isOpen) {
|
|
174
|
+
if (event.key === ' ' && isTextEntryTarget(event.target)) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
54
177
|
if (
|
|
55
178
|
event.key === 'Enter' ||
|
|
56
179
|
event.key === ' ' ||
|
|
@@ -63,6 +186,12 @@
|
|
|
63
186
|
return;
|
|
64
187
|
}
|
|
65
188
|
|
|
189
|
+
// In a sheet, Escape and Tab are the sheet's: it closes itself and
|
|
190
|
+
// keeps Tab inside, on its grip and close button too.
|
|
191
|
+
if (asSheet && (event.key === 'Escape' || event.key === 'Tab')) return;
|
|
192
|
+
|
|
193
|
+
if (typedIntoList(event)) return;
|
|
194
|
+
|
|
66
195
|
switch (event.key) {
|
|
67
196
|
case 'Escape':
|
|
68
197
|
event.preventDefault();
|
|
@@ -77,26 +206,146 @@
|
|
|
77
206
|
focusPreviousItem();
|
|
78
207
|
break;
|
|
79
208
|
case 'Home':
|
|
209
|
+
// Let text fields (e.g. Select search) keep native caret movement.
|
|
210
|
+
if (isTextEntryTarget(event.target)) break;
|
|
80
211
|
event.preventDefault();
|
|
81
212
|
focusFirstItem();
|
|
82
213
|
break;
|
|
83
214
|
case 'End':
|
|
215
|
+
if (isTextEntryTarget(event.target)) break;
|
|
84
216
|
event.preventDefault();
|
|
85
217
|
focusLastItem();
|
|
86
218
|
break;
|
|
87
|
-
case 'Tab':
|
|
88
|
-
|
|
219
|
+
case 'Tab': {
|
|
220
|
+
// The list is one stop and a field above it (Select's search)
|
|
221
|
+
// another: Shift+Tab from an item goes to the field, Tab from
|
|
222
|
+
// the field to the list. Any other Tab leaves, and closes.
|
|
223
|
+
const field = headerField();
|
|
224
|
+
const onItem = getMenuItems().includes(event.target as HTMLElement);
|
|
225
|
+
if (field && onItem && event.shiftKey) {
|
|
226
|
+
event.preventDefault();
|
|
227
|
+
field.focus();
|
|
228
|
+
} else if (field && event.target === field && !event.shiftKey && getMenuItems().length > 0) {
|
|
229
|
+
event.preventDefault();
|
|
230
|
+
tabStop()?.focus();
|
|
231
|
+
} else {
|
|
232
|
+
isOpen = false;
|
|
233
|
+
}
|
|
89
234
|
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** The text field in the popup's header, when there is one: Select's search. */
|
|
240
|
+
function headerField(): HTMLElement | null {
|
|
241
|
+
const popup = sheetElement ?? popoverElement();
|
|
242
|
+
if (!popup) return null;
|
|
243
|
+
return (
|
|
244
|
+
[...popup.querySelectorAll<HTMLElement>('input, textarea')].find(
|
|
245
|
+
(candidate) => isTextEntryTarget(candidate) && !(candidate as HTMLInputElement).disabled,
|
|
246
|
+
) ?? null
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** The item that is the list's one Tab stop. */
|
|
251
|
+
function tabStop(): HTMLElement | undefined {
|
|
252
|
+
const items = getMenuItems();
|
|
253
|
+
return items.find((item) => item.tabIndex === 0) ?? items[0];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** How long a pause ends a word typed on the list, in ms. */
|
|
257
|
+
const TYPEAHEAD_PAUSE = 600;
|
|
258
|
+
let typed = '';
|
|
259
|
+
let typedAt = 0;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* A printable character typed while focus is on an item. With a text
|
|
263
|
+
* field above the list it is meant for that field: focus moves there and
|
|
264
|
+
* the character is typed into it. Without one it is typeahead: focus goes
|
|
265
|
+
* to the next item that starts with what has been typed. Returns true
|
|
266
|
+
* when the key was taken.
|
|
267
|
+
*/
|
|
268
|
+
function typedIntoList(event: KeyboardEvent): boolean {
|
|
269
|
+
if (event.key.length !== 1 || event.ctrlKey || event.metaKey || event.altKey) return false;
|
|
270
|
+
const items = getMenuItems();
|
|
271
|
+
const at = items.indexOf(event.target as HTMLElement);
|
|
272
|
+
if (at === -1) return false;
|
|
273
|
+
const now = event.timeStamp || Date.now();
|
|
274
|
+
const word = now - typedAt > TYPEAHEAD_PAUSE ? '' : typed;
|
|
275
|
+
// A space activates the item, unless it is in the middle of a word.
|
|
276
|
+
if (event.key === ' ' && word === '') return false;
|
|
277
|
+
|
|
278
|
+
const field = headerField();
|
|
279
|
+
if (field) {
|
|
280
|
+
// Focus moves during keydown, so the character lands in the field.
|
|
281
|
+
field.focus();
|
|
282
|
+
return true;
|
|
90
283
|
}
|
|
284
|
+
|
|
285
|
+
event.preventDefault();
|
|
286
|
+
typed = word + event.key.toLocaleLowerCase();
|
|
287
|
+
typedAt = now;
|
|
288
|
+
const starts = (item: HTMLElement) =>
|
|
289
|
+
(item.textContent ?? '').trim().toLocaleLowerCase().startsWith(typed);
|
|
290
|
+
// One letter again and again steps through the items that start with it.
|
|
291
|
+
const from = typed.length === 1 ? at + 1 : at;
|
|
292
|
+
const order = [...items.slice(from), ...items.slice(0, from)];
|
|
293
|
+
order.find(starts)?.focus();
|
|
294
|
+
return true;
|
|
91
295
|
}
|
|
92
296
|
|
|
297
|
+
/**
|
|
298
|
+
* One Tab stop for the whole list, on the item that has focus, or the
|
|
299
|
+
* chosen one, or the first: the roving tabindex of a listbox or a menu.
|
|
300
|
+
* With every item a stop, a field above twelve options was twelve
|
|
301
|
+
* Shift+Tabs away. Kept as the items change (a search filters them).
|
|
302
|
+
*/
|
|
303
|
+
$effect(() => {
|
|
304
|
+
const popup = sheetElement ?? menuElement;
|
|
305
|
+
if (!isOpen || !popup) return;
|
|
306
|
+
const rove = (current?: HTMLElement) => {
|
|
307
|
+
const items = getMenuItems();
|
|
308
|
+
const stop =
|
|
309
|
+
(current && items.includes(current) ? current : undefined) ??
|
|
310
|
+
items.find((item) => item === document.activeElement) ??
|
|
311
|
+
items.find((item) => item.tabIndex === 0 && item.hasAttribute('data-roving')) ??
|
|
312
|
+
items.find(
|
|
313
|
+
(item) =>
|
|
314
|
+
item.getAttribute('aria-selected') === 'true' ||
|
|
315
|
+
item.getAttribute('aria-checked') === 'true',
|
|
316
|
+
) ??
|
|
317
|
+
items[0];
|
|
318
|
+
for (const item of items) {
|
|
319
|
+
item.tabIndex = item === stop ? 0 : -1;
|
|
320
|
+
item.setAttribute('data-roving', '');
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
rove();
|
|
324
|
+
const onFocusIn = (event: FocusEvent) => {
|
|
325
|
+
if (getMenuItems().includes(event.target as HTMLElement)) rove(event.target as HTMLElement);
|
|
326
|
+
};
|
|
327
|
+
popup.addEventListener('focusin', onFocusIn);
|
|
328
|
+
const changes = new MutationObserver(() => rove());
|
|
329
|
+
changes.observe(popup, { childList: true, subtree: true });
|
|
330
|
+
return () => {
|
|
331
|
+
popup.removeEventListener('focusin', onFocusIn);
|
|
332
|
+
changes.disconnect();
|
|
333
|
+
};
|
|
334
|
+
});
|
|
335
|
+
|
|
93
336
|
let menuElement = $state<HTMLElement | null>(null);
|
|
337
|
+
const popoverElement = () => menuElement;
|
|
94
338
|
|
|
95
339
|
function getMenuItems(): HTMLElement[] {
|
|
340
|
+
const menuElement = sheetElement ?? popoverElement();
|
|
96
341
|
if (!menuElement) return [];
|
|
342
|
+
// `menuitemradio` and `menuitemcheckbox` are menu items too. Matching
|
|
343
|
+
// only `menuitem` left a menu built from them with nothing to focus:
|
|
344
|
+
// arrows and Home/End did nothing, opening by keyboard focused nothing,
|
|
345
|
+
// and Tab closed the menu, so its items could not be reached at all.
|
|
97
346
|
return Array.from(
|
|
98
347
|
menuElement.querySelectorAll<HTMLElement>(
|
|
99
|
-
'[role="menuitem"], [role="option"]',
|
|
348
|
+
'[role="menuitem"], [role="menuitemradio"], [role="menuitemcheckbox"], [role="option"]',
|
|
100
349
|
),
|
|
101
350
|
);
|
|
102
351
|
}
|
|
@@ -143,10 +392,81 @@
|
|
|
143
392
|
}
|
|
144
393
|
});
|
|
145
394
|
|
|
395
|
+
/**
|
|
396
|
+
* Closing removes the popup, and with it the focused item: the browser
|
|
397
|
+
* then drops focus on `<body>` and a keyboard user loses their place.
|
|
398
|
+
* Whether focus was in the popup has to be read before the DOM changes.
|
|
399
|
+
*/
|
|
400
|
+
let focusWasInPopup = false;
|
|
401
|
+
$effect.pre(() => {
|
|
402
|
+
if (!isOpen) {
|
|
403
|
+
focusWasInPopup =
|
|
404
|
+
!!menuElement && menuElement.contains(document.activeElement);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* In a sheet. Focus goes to the chosen item, or the first one, as soon as
|
|
410
|
+
* the sheet is in place: the sheet itself would start on its grip. (It
|
|
411
|
+
* leaves focus alone when something inside already has it.) Focusing also
|
|
412
|
+
* scrolls the chosen item into view.
|
|
413
|
+
*/
|
|
414
|
+
let closedFromSheet = false;
|
|
146
415
|
$effect(() => {
|
|
147
|
-
if (!
|
|
416
|
+
if (!asSheet || !sheetElement) return;
|
|
417
|
+
closedFromSheet = true;
|
|
418
|
+
queueMicrotask(() => {
|
|
419
|
+
const items = getMenuItems();
|
|
420
|
+
const chosen = items.find(
|
|
421
|
+
(item) =>
|
|
422
|
+
item.getAttribute('aria-selected') === 'true' ||
|
|
423
|
+
item.getAttribute('aria-checked') === 'true',
|
|
424
|
+
);
|
|
425
|
+
(chosen ?? items[0])?.focus();
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Closed from a sheet, focus goes back to the trigger. The sheet returns
|
|
430
|
+
// it to whatever had focus when it opened, but a tap on a button does not
|
|
431
|
+
// focus it in every browser, and then that is `<body>`.
|
|
432
|
+
$effect(() => {
|
|
433
|
+
if (isOpen || !closedFromSheet) return;
|
|
434
|
+
closedFromSheet = false;
|
|
435
|
+
const timer = setTimeout(() => {
|
|
436
|
+
const active = document.activeElement;
|
|
437
|
+
if (!active || active === document.body) triggerElement()?.focus();
|
|
438
|
+
}, 0);
|
|
439
|
+
return () => clearTimeout(timer);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
/** The control that opened the menu: what `aria-controls` is spread on, else the first control. */
|
|
443
|
+
function triggerElement(): HTMLElement | null {
|
|
444
|
+
if (!rootEl) return null;
|
|
445
|
+
return (
|
|
446
|
+
rootEl.querySelector<HTMLElement>(`[aria-controls="${menuId}"]`) ??
|
|
447
|
+
rootEl.querySelector<HTMLElement>(
|
|
448
|
+
'button:not([disabled]), [href], [tabindex]:not([tabindex="-1"])',
|
|
449
|
+
)
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
$effect(() => {
|
|
454
|
+
if (isOpen || !focusWasInPopup) return;
|
|
455
|
+
focusWasInPopup = false;
|
|
456
|
+
// Only when the close left focus nowhere. If the handler that closed
|
|
457
|
+
// the menu moved focus itself (it opened a dialog, say), that wins.
|
|
458
|
+
const active = document.activeElement;
|
|
459
|
+
if (!active || active === document.body) {
|
|
460
|
+
triggerElement()?.focus();
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
$effect(() => {
|
|
465
|
+
// A sheet has a backdrop of its own, and is not inside `rootEl`.
|
|
466
|
+
if (!isOpen || asSheet) return;
|
|
148
467
|
function onDocMouseDown(e: MouseEvent) {
|
|
149
468
|
const t = e.target as Node;
|
|
469
|
+
if (isInsideToastRegion(t)) return;
|
|
150
470
|
if (rootEl && !rootEl.contains(t)) {
|
|
151
471
|
isOpen = false;
|
|
152
472
|
}
|
|
@@ -155,15 +475,71 @@
|
|
|
155
475
|
return () => document.removeEventListener('mousedown', onDocMouseDown);
|
|
156
476
|
});
|
|
157
477
|
|
|
478
|
+
/** Room kept between the menu and the edge of the screen. */
|
|
479
|
+
const VIEWPORT_MARGIN = 8;
|
|
480
|
+
|
|
481
|
+
/** The room between the menu and its trigger: `mt-2` or `mb-2`, so it grows with the text size. */
|
|
482
|
+
function triggerGap(panel: HTMLElement): number {
|
|
483
|
+
// A menu placed with `position: fixed` has its margin taken away inline:
|
|
484
|
+
// read the one its classes give it, or the gap would be 0 from then on.
|
|
485
|
+
const inline = panel.style.margin;
|
|
486
|
+
panel.style.margin = '';
|
|
487
|
+
const style = getComputedStyle(panel);
|
|
488
|
+
const gap = Math.max(parseFloat(style.marginTop), parseFloat(style.marginBottom));
|
|
489
|
+
panel.style.margin = inline;
|
|
490
|
+
return Number.isFinite(gap) ? gap : 8;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const preferred = $derived({
|
|
494
|
+
block: placement.startsWith('top') ? ('top' as const) : ('bottom' as const),
|
|
495
|
+
inline: placement.endsWith('end') ? ('end' as const) : ('start' as const),
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Where the menu actually is. It starts, and stays, on the side asked for
|
|
500
|
+
* whenever the menu fits there; `measurePlacement` only moves it when it
|
|
501
|
+
* would leave the screen, or be cut off by a box that scrolls around it
|
|
502
|
+
* (a Modal's body, say). In the second case it may be given
|
|
503
|
+
* `position: fixed` (`fit.fixed`), which such a box does not clip. It
|
|
504
|
+
* never leaves its place in the DOM, so the keys, focus and a dialog's
|
|
505
|
+
* focus trap all still see it as part of this Dropdown.
|
|
506
|
+
*/
|
|
507
|
+
let measured = $state<PanelPlacement | null>(null);
|
|
508
|
+
const fit = $derived<PanelPlacement>(
|
|
509
|
+
measured ?? { ...unfitted(preferred.block, preferred.inline), fixed: null },
|
|
510
|
+
);
|
|
511
|
+
const resolvedPlacement = $derived(`${fit.block}-${fit.inline}` as typeof placement);
|
|
512
|
+
|
|
513
|
+
// Runs after the menu is in the DOM and before the browser paints it, so
|
|
514
|
+
// it is never seen on the wrong side; then again when the screen changes.
|
|
515
|
+
$effect(() => {
|
|
516
|
+
const side = preferred;
|
|
517
|
+
if (!isOpen || !menuElement || !rootEl) {
|
|
518
|
+
measured = null;
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const anchor = rootEl;
|
|
522
|
+
const panel = menuElement;
|
|
523
|
+
const update = () => {
|
|
524
|
+
measured = measurePlacement(anchor, panel, side, {
|
|
525
|
+
margin: VIEWPORT_MARGIN,
|
|
526
|
+
gap: triggerGap(panel),
|
|
527
|
+
});
|
|
528
|
+
};
|
|
529
|
+
update();
|
|
530
|
+
return watchViewport(update, panel);
|
|
531
|
+
});
|
|
532
|
+
|
|
158
533
|
const placementClasses = $derived(() => {
|
|
159
534
|
const base = 'absolute z-dropdown min-w-[12rem]';
|
|
160
535
|
const positioning: Record<typeof placement, string> = {
|
|
161
|
-
|
|
162
|
-
'bottom-
|
|
163
|
-
'
|
|
164
|
-
'top-
|
|
536
|
+
// Logical: `start` is the left edge, and the right one in a right-to-left page.
|
|
537
|
+
'bottom-start': 'top-full start-0 mt-2',
|
|
538
|
+
'bottom-end': 'top-full end-0 mt-2',
|
|
539
|
+
'top-start': 'bottom-full start-0 mb-2',
|
|
540
|
+
'top-end': 'bottom-full end-0 mb-2',
|
|
165
541
|
};
|
|
166
|
-
return `${base} ${positioning[
|
|
542
|
+
return `${base} ${positioning[resolvedPlacement]}`;
|
|
167
543
|
});
|
|
168
544
|
|
|
169
545
|
const transformClasses = $derived(() => {
|
|
@@ -182,60 +558,174 @@
|
|
|
182
558
|
const dropdownContentClasses = $derived(() => {
|
|
183
559
|
return [
|
|
184
560
|
placementClasses(),
|
|
185
|
-
|
|
561
|
+
// A menu is an overlay (16px). Its items are 8px, 9px inside
|
|
562
|
+
// the corner: within a pixel of concentric.
|
|
563
|
+
// A column, so that the list below can be the part that scrolls
|
|
564
|
+
// when the menu is limited in height.
|
|
565
|
+
'flex flex-col rounded-overlay border border-border-overlay bg-surface-overlay py-2 shadow-lg transition-[opacity,translate] duration-200 ease-in-out motion-reduce:transition-none',
|
|
186
566
|
transformClasses(),
|
|
187
567
|
]
|
|
188
568
|
.join(' ')
|
|
189
569
|
.replace(/\s+/g, ' ')
|
|
190
570
|
.trim();
|
|
191
571
|
});
|
|
192
|
-
|
|
193
|
-
const itemRole = $derived(menuRole === 'listbox' ? 'option' : 'menuitem');
|
|
194
|
-
|
|
195
|
-
const optionClasses =
|
|
196
|
-
'focus-ring flex w-full items-center justify-start rounded-md px-3 py-2 text-left text-sm text-body transition-colors hover:bg-base-100 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50';
|
|
197
572
|
</script>
|
|
198
573
|
|
|
199
574
|
<div
|
|
200
575
|
bind:this={rootEl}
|
|
201
|
-
class="relative inline-block"
|
|
576
|
+
class={cn("relative", fullWidth ? "block w-full min-w-0" : "inline-block", className)}
|
|
202
577
|
data-placement={placement}
|
|
203
578
|
onkeydown={handleKeydown}
|
|
204
579
|
{...restProps}
|
|
205
580
|
>
|
|
206
581
|
{@render trigger(triggerAria)}
|
|
207
582
|
|
|
208
|
-
{#if
|
|
583
|
+
{#if asSheet}
|
|
584
|
+
<!-- The same menu in a BottomSheet: the element with the menu or
|
|
585
|
+
listbox role, its items and the header are the ones the pop-over has,
|
|
586
|
+
so roles, names and the arrow keys are too. The sheet brings the focus
|
|
587
|
+
trap, Escape, the backdrop, the swipe and its place in the stack of
|
|
588
|
+
overlays. It is rendered in `<body>`, outside this component's root, so
|
|
589
|
+
the keys are listened to here as well. -->
|
|
590
|
+
<BottomSheet
|
|
591
|
+
bind:isOpen
|
|
592
|
+
snap={sheetSnap ?? (options.length > 6 ? 'full' : 'half')}
|
|
593
|
+
title={sheetTitle || ariaLabel}
|
|
594
|
+
closeLabel={sheetCloseLabel}
|
|
595
|
+
expandLabel={sheetExpandLabel}
|
|
596
|
+
collapseLabel={sheetCollapseLabel}
|
|
597
|
+
>
|
|
598
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
599
|
+
<div
|
|
600
|
+
bind:this={sheetElement}
|
|
601
|
+
class="dropdown-sheet"
|
|
602
|
+
data-dropdown-sheet
|
|
603
|
+
onkeydown={handleKeydown}
|
|
604
|
+
>
|
|
605
|
+
{#if header}
|
|
606
|
+
<div class="dropdown-sheet-header">
|
|
607
|
+
{@render header()}
|
|
608
|
+
</div>
|
|
609
|
+
{/if}
|
|
610
|
+
<div
|
|
611
|
+
id={menuId}
|
|
612
|
+
role={menuRole === 'listbox' ? 'listbox' : 'menu'}
|
|
613
|
+
aria-label={ariaLabel}
|
|
614
|
+
>
|
|
615
|
+
{@render items()}
|
|
616
|
+
</div>
|
|
617
|
+
</div>
|
|
618
|
+
</BottomSheet>
|
|
619
|
+
{:else if isOpen}
|
|
620
|
+
<!-- The inline styles are only set when the menu would leave the screen,
|
|
621
|
+
or be cut off by a scrolling box around it (`fit.fixed`: placed against
|
|
622
|
+
the viewport instead, with the classes' own offsets switched off).
|
|
623
|
+
`min-width`: `min-w-[12rem]` is wider than a 320px screen once the text
|
|
624
|
+
is at 200%, and a minimum beats `max-width`; a narrowed menu is exactly
|
|
625
|
+
as wide as the room there is.
|
|
626
|
+
A menu limited in height does not scroll itself: the list inside it
|
|
627
|
+
does. A scrollbar on the panel was drawn in the panel's square box, on
|
|
628
|
+
and outside its 16px corners; the list is 9px inside them (the panel's
|
|
629
|
+
padding and border), and a header (Select's search field) stays put. -->
|
|
209
630
|
<div
|
|
210
631
|
bind:this={menuElement}
|
|
211
|
-
id={menuId}
|
|
212
632
|
class={dropdownContentClasses()}
|
|
213
|
-
|
|
214
|
-
|
|
633
|
+
data-resolved-placement={resolvedPlacement}
|
|
634
|
+
style:inset-inline-start={!fit.fixed && fit.inline === 'start' && fit.inlineOffset !== 0
|
|
635
|
+
? `${fit.inlineOffset}px`
|
|
636
|
+
: undefined}
|
|
637
|
+
style:inset-inline-end={!fit.fixed && fit.inline === 'end' && fit.inlineOffset !== 0
|
|
638
|
+
? `${fit.inlineOffset}px`
|
|
639
|
+
: undefined}
|
|
640
|
+
style:position={fit.fixed ? 'fixed' : undefined}
|
|
641
|
+
style:top={fit.fixed ? `${fit.fixed.top}px` : undefined}
|
|
642
|
+
style:left={fit.fixed ? `${fit.fixed.left}px` : undefined}
|
|
643
|
+
style:right={fit.fixed ? 'auto' : undefined}
|
|
644
|
+
style:bottom={fit.fixed ? 'auto' : undefined}
|
|
645
|
+
style:margin={fit.fixed ? '0' : undefined}
|
|
646
|
+
style:width={fit.fixed ? `${fit.fixed.width}px` : undefined}
|
|
647
|
+
style:max-width={fit.maxWidth !== null ? `${fit.maxWidth}px` : undefined}
|
|
648
|
+
style:min-width={fit.maxWidth !== null
|
|
649
|
+
? `${fit.maxWidth}px`
|
|
650
|
+
: fullWidth
|
|
651
|
+
? 'max(12rem, 100%)'
|
|
652
|
+
: undefined}
|
|
653
|
+
style:max-height={fit.maxHeight !== null ? `${fit.maxHeight}px` : undefined}
|
|
654
|
+
style:overflow={fit.maxWidth !== null ? 'hidden' : undefined}
|
|
215
655
|
>
|
|
216
|
-
{
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
</button>
|
|
234
|
-
{/each}
|
|
235
|
-
</div>
|
|
236
|
-
{:else if children}
|
|
237
|
-
{@render children?.()}
|
|
238
|
-
{/if}
|
|
656
|
+
{@render header?.()}
|
|
657
|
+
<!-- `mx-1` and `px-1` are the 0.5rem the items always had from the
|
|
658
|
+
panel's edge, split so that the scrolling box is 4px in from it:
|
|
659
|
+
room for an item's focus ring inside the box, and a scrollbar
|
|
660
|
+
clear of the corners. -->
|
|
661
|
+
<div
|
|
662
|
+
id={menuId}
|
|
663
|
+
role={menuRole === 'listbox' ? 'listbox' : 'menu'}
|
|
664
|
+
aria-label={ariaLabelledby ? undefined : ariaLabel}
|
|
665
|
+
aria-labelledby={ariaLabelledby}
|
|
666
|
+
class={options.length > 0 ? 'mx-1 min-h-0' : 'min-h-0'}
|
|
667
|
+
data-dropdown-scroller
|
|
668
|
+
style:overflow={fit.maxWidth !== null || fit.maxHeight !== null ? 'auto' : undefined}
|
|
669
|
+
style:overscroll-behavior={fit.maxHeight !== null ? 'contain' : undefined}
|
|
670
|
+
>
|
|
671
|
+
{@render items()}
|
|
672
|
+
</div>
|
|
239
673
|
</div>
|
|
240
674
|
{/if}
|
|
241
675
|
</div>
|
|
676
|
+
|
|
677
|
+
{#snippet items()}
|
|
678
|
+
{#if options.length > 0}
|
|
679
|
+
<div class="px-1 py-1">
|
|
680
|
+
{#each options as option (option.value)}
|
|
681
|
+
<DropdownItem
|
|
682
|
+
label={option.label}
|
|
683
|
+
description={option.description}
|
|
684
|
+
icon={option.icon}
|
|
685
|
+
tone={option.tone}
|
|
686
|
+
disabled={option.disabled}
|
|
687
|
+
selected={selectedValue !== null &&
|
|
688
|
+
String(selectedValue) === String(option.value)}
|
|
689
|
+
data-value={String(option.value)}
|
|
690
|
+
onclick={() => onOptionClick?.(option.value)}
|
|
691
|
+
/>
|
|
692
|
+
{/each}
|
|
693
|
+
</div>
|
|
694
|
+
{:else if children}
|
|
695
|
+
{@render children?.()}
|
|
696
|
+
{/if}
|
|
697
|
+
{/snippet}
|
|
698
|
+
|
|
699
|
+
<style>
|
|
700
|
+
/*
|
|
701
|
+
* In a sheet the rows are 48px, for a thumb, and as wide as the sheet.
|
|
702
|
+
* px, not rem: a row that grows with the text grows anyway, by its text.
|
|
703
|
+
*/
|
|
704
|
+
.dropdown-sheet :global(:is([role="menuitem"], [role="menuitemradio"], [role="menuitemcheckbox"], [role="option"])) {
|
|
705
|
+
min-height: 48px;
|
|
706
|
+
align-items: center;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/* The sheet scrolls, so a list with a height limit and a scrollbar of its
|
|
710
|
+
own (Select's) has neither here, and a width set for the pop-over
|
|
711
|
+
(`menuWidth`) is the sheet's. */
|
|
712
|
+
.dropdown-sheet :global([data-menu-scroll]) {
|
|
713
|
+
max-height: none !important;
|
|
714
|
+
overflow: visible !important;
|
|
715
|
+
}
|
|
716
|
+
.dropdown-sheet :global([data-menu-width]) {
|
|
717
|
+
width: 100% !important;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/* A header (Select's search field) stays under the sheet's own header
|
|
721
|
+
while the list scrolls beneath it. Wider than the list by the room a
|
|
722
|
+
focus ring takes, so no ring shows beside it. */
|
|
723
|
+
.dropdown-sheet-header {
|
|
724
|
+
position: sticky;
|
|
725
|
+
top: 0;
|
|
726
|
+
z-index: 1;
|
|
727
|
+
margin-inline: -4px;
|
|
728
|
+
padding-inline: 4px;
|
|
729
|
+
background-color: var(--color-surface-overlay);
|
|
730
|
+
}
|
|
731
|
+
</style>
|