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
|
@@ -2,55 +2,121 @@
|
|
|
2
2
|
import { onMount } from 'svelte';
|
|
3
3
|
import { cubicOut } from 'svelte/easing';
|
|
4
4
|
import { slide, fade, fly } from 'svelte/transition';
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
X,
|
|
12
|
-
} from '@lucide/svelte';
|
|
5
|
+
import AlertCircle from '@lucide/svelte/icons/circle-alert';
|
|
6
|
+
import AlertTriangle from '@lucide/svelte/icons/triangle-alert';
|
|
7
|
+
import CheckCircle from '@lucide/svelte/icons/circle-check-big';
|
|
8
|
+
import ChevronDown from '@lucide/svelte/icons/chevron-down';
|
|
9
|
+
import Info from '@lucide/svelte/icons/info';
|
|
10
|
+
import X from '@lucide/svelte/icons/x';
|
|
13
11
|
import Button from '../atoms/Button.svelte';
|
|
14
12
|
import { toastStore, type ToastItem } from './toast-store.js';
|
|
13
|
+
import { cn } from "../util/cn.js";
|
|
14
|
+
import {
|
|
15
|
+
DEFAULT_TOASTER_STRINGS,
|
|
16
|
+
resolveToastDuration,
|
|
17
|
+
TOAST_SECONDS_AFTER_HOLD,
|
|
18
|
+
type ToastDuration,
|
|
19
|
+
type ToasterStrings,
|
|
20
|
+
type ToastPauseChange,
|
|
21
|
+
} from "../util/toaster.js";
|
|
15
22
|
|
|
16
23
|
interface Props {
|
|
24
|
+
/** Extra classes for the host element. */
|
|
25
|
+
class?: string;
|
|
17
26
|
toast: ToastItem;
|
|
27
|
+
/** Every word the toast says by itself; the Toaster passes its own. */
|
|
28
|
+
strings?: ToasterStrings;
|
|
29
|
+
/** Shows the time left as a sentence, with a button that stops the timer. */
|
|
30
|
+
showCountdown?: boolean;
|
|
31
|
+
/** What a toast pushed without a duration gets, in place of medium; the Toaster passes its own. */
|
|
32
|
+
defaultDuration?: ToastDuration;
|
|
33
|
+
/** Called when a pointer, a finger or focus starts or stops holding the timer. */
|
|
34
|
+
onpausechange?: (detail: ToastPauseChange) => void;
|
|
18
35
|
}
|
|
19
36
|
|
|
20
|
-
let {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const maxSeconds = $derived(autoDismissSeconds(toast.duration));
|
|
37
|
+
let {
|
|
38
|
+
class: className = "",
|
|
39
|
+
toast,
|
|
40
|
+
strings = DEFAULT_TOASTER_STRINGS,
|
|
41
|
+
showCountdown = false,
|
|
42
|
+
defaultDuration,
|
|
43
|
+
onpausechange,
|
|
44
|
+
}: Props = $props();
|
|
30
45
|
|
|
31
46
|
let count = $state(0);
|
|
32
47
|
let timerActive = $state(false);
|
|
33
48
|
let isExpanded = $state(false);
|
|
49
|
+
/**
|
|
50
|
+
* What holds the timer (WCAG 2.2.1): a mouse or a stylus over the toast,
|
|
51
|
+
* a finger on it, keyboard focus inside it.
|
|
52
|
+
*/
|
|
53
|
+
let hovered = $state(false);
|
|
54
|
+
let held = $state(false);
|
|
55
|
+
let focusWithin = $state(false);
|
|
56
|
+
const paused = $derived(hovered || held || focusWithin);
|
|
34
57
|
|
|
35
58
|
let intervalRef: ReturnType<typeof setInterval> | undefined;
|
|
36
59
|
|
|
37
|
-
const
|
|
60
|
+
const title = $derived(toast.title?.trim() ?? "");
|
|
61
|
+
const message = $derived(toast.message.trim());
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* What a toast pushed with neither a title nor a message says: a word
|
|
65
|
+
* for its type. A toast with a message says the message.
|
|
66
|
+
*/
|
|
67
|
+
const fallbackTitle = $derived(
|
|
68
|
+
toast.type === 'success'
|
|
69
|
+
? strings.successTitle
|
|
70
|
+
: toast.type === 'error'
|
|
71
|
+
? strings.errorTitle
|
|
72
|
+
: toast.type === 'warning'
|
|
73
|
+
? strings.warningTitle
|
|
74
|
+
: strings.infoTitle,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
/** The first line: the title, or the message when there is no title. */
|
|
78
|
+
const headerTitle = $derived(title || message || fallbackTitle);
|
|
79
|
+
/** The second line: the message, under a title that says something else. */
|
|
80
|
+
const secondLine = $derived(title && message && message !== title ? message : "");
|
|
38
81
|
|
|
82
|
+
/**
|
|
83
|
+
* The part that opens and closes is `detail`, when the toast was given
|
|
84
|
+
* one: the long version. The message is never folded away.
|
|
85
|
+
*/
|
|
86
|
+
const expandableText = $derived((toast.detail ?? "").trim());
|
|
39
87
|
const hasExpandable = $derived(expandableText.length > 0);
|
|
40
88
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Seconds until the toast closes by itself; `0` is never. What the app
|
|
91
|
+
* said for this toast wins. Otherwise it goes by what the toast is and by
|
|
92
|
+
* how much there is to read in it: the two lines that are shown, not the
|
|
93
|
+
* detail behind its button.
|
|
94
|
+
*/
|
|
95
|
+
const maxSeconds = $derived.by(() => {
|
|
96
|
+
const ms = resolveToastDuration(
|
|
97
|
+
{
|
|
98
|
+
duration: toast.duration,
|
|
99
|
+
type: toast.type,
|
|
100
|
+
hasAction: !!toast.action,
|
|
101
|
+
textLength: headerTitle.length + secondLine.length,
|
|
102
|
+
},
|
|
103
|
+
defaultDuration,
|
|
104
|
+
);
|
|
105
|
+
return ms <= 0 ? 0 : Math.max(1, Math.ceil(ms / 1000));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
/** The time left as words: for the sentence, or for a screen reader to find. */
|
|
109
|
+
const countdownText = $derived(
|
|
110
|
+
paused ? strings.pausedClosesIn(count) : strings.closesIn(count),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Said to the app as well: `data-paused` is the same thing in the markup.
|
|
114
|
+
let reportedPause = false;
|
|
115
|
+
$effect(() => {
|
|
116
|
+
const now = paused;
|
|
117
|
+
if (now === reportedPause) return;
|
|
118
|
+
reportedPause = now;
|
|
119
|
+
onpausechange?.({ id: toast.id, paused: now });
|
|
54
120
|
});
|
|
55
121
|
|
|
56
122
|
const statusIconClass = $derived(
|
|
@@ -90,6 +156,10 @@
|
|
|
90
156
|
timerActive = true;
|
|
91
157
|
|
|
92
158
|
intervalRef = setInterval(() => {
|
|
159
|
+
// The state, not the `paused` derived: a tick can land while the
|
|
160
|
+
// toast is animating out, and reading a derived then warns
|
|
161
|
+
// (`derived_inert`).
|
|
162
|
+
if (hovered || held || focusWithin) return;
|
|
93
163
|
count -= 1;
|
|
94
164
|
if (count <= 0) {
|
|
95
165
|
stopTimer();
|
|
@@ -109,44 +179,171 @@
|
|
|
109
179
|
toastStore.dismiss(toast.id);
|
|
110
180
|
}
|
|
111
181
|
|
|
182
|
+
/** The toast closes even if the handler throws, unless the action opted out. */
|
|
183
|
+
function handleAction() {
|
|
184
|
+
const action = toast.action;
|
|
185
|
+
if (!action) return;
|
|
186
|
+
try {
|
|
187
|
+
action.onclick();
|
|
188
|
+
} finally {
|
|
189
|
+
if (action.dismissOnClick !== false) {
|
|
190
|
+
toastStore.dismiss(toast.id);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let expandButton = $state<HTMLButtonElement>();
|
|
196
|
+
let dismissButton = $state<HTMLButtonElement>();
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Okay closes the details and goes with them. Focus moves to the button
|
|
200
|
+
* that opens them again, or it would fall to the page and a keyboard
|
|
201
|
+
* user would be at the top of it.
|
|
202
|
+
*/
|
|
112
203
|
function handleOkay() {
|
|
113
204
|
isExpanded = false;
|
|
205
|
+
expandButton?.focus();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The stop button goes with the sentence it is in: focus stays in the toast. */
|
|
209
|
+
function handleStop(event: MouseEvent) {
|
|
210
|
+
const held = event.currentTarget === document.activeElement;
|
|
211
|
+
stopTimer();
|
|
212
|
+
if (held) dismissButton?.focus();
|
|
114
213
|
}
|
|
115
214
|
|
|
116
|
-
|
|
117
|
-
|
|
215
|
+
/**
|
|
216
|
+
* The kind of pointer last seen on the toast. After a tap a browser makes
|
|
217
|
+
* up mouse events for it, `mouseenter` among them and never a
|
|
218
|
+
* `mouseleave`: taken for a mouse, a tap held the timer for good.
|
|
219
|
+
*/
|
|
220
|
+
let lastPointer: string | undefined;
|
|
221
|
+
|
|
222
|
+
function handlePointerEnter(event: PointerEvent) {
|
|
223
|
+
lastPointer = event.pointerType;
|
|
224
|
+
// A mouse, or a stylus in range. A finger is over the toast only while it is on it.
|
|
225
|
+
if (event.pointerType !== "touch") hovered = true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function handlePointerLeave(event: PointerEvent) {
|
|
229
|
+
if (event.pointerType !== "touch") hovered = false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function handleMouseEnter() {
|
|
233
|
+
if (lastPointer !== "touch") hovered = true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function handlePointerDown(event: PointerEvent) {
|
|
237
|
+
lastPointer = event.pointerType;
|
|
238
|
+
if (event.pointerType === "touch") held = true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The finger lifts, or the browser takes the touch for a scroll. The
|
|
243
|
+
* timer runs again, with time left to do something about the toast: it
|
|
244
|
+
* was held because someone was reading it.
|
|
245
|
+
*/
|
|
246
|
+
function handlePointerRelease(event: PointerEvent) {
|
|
247
|
+
if (event.pointerType !== "touch" || !held) return;
|
|
248
|
+
held = false;
|
|
249
|
+
if (timerActive) count = Math.max(count, Math.min(TOAST_SECONDS_AFTER_HOLD, maxSeconds));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function handleFocusOut(event: FocusEvent) {
|
|
253
|
+
const next = event.relatedTarget as Node | null;
|
|
254
|
+
if (!next || !(event.currentTarget as HTMLElement).contains(next)) {
|
|
255
|
+
focusWithin = false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** No movement where motion is unwanted: the toast appears and goes at once. */
|
|
260
|
+
const reducedMotion =
|
|
261
|
+
typeof window !== 'undefined' &&
|
|
262
|
+
typeof window.matchMedia === 'function' &&
|
|
263
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
264
|
+
const motion = (duration: number) => (reducedMotion ? 0 : duration);
|
|
265
|
+
|
|
266
|
+
const toastEnter = { y: 18, duration: motion(260), opacity: 0, easing: cubicOut };
|
|
267
|
+
const toastLeave = { y: 14, duration: motion(300), opacity: 0, easing: cubicOut };
|
|
118
268
|
</script>
|
|
119
269
|
|
|
120
270
|
<div
|
|
121
|
-
class="pointer-events-auto relative w-full min-w-[18rem] overflow-hidden rounded-
|
|
271
|
+
class={cn("pointer-events-auto relative w-full min-w-[min(18rem,100%)] shrink-0 overflow-hidden rounded-overlay border border-border-overlay bg-surface-overlay shadow-lg", className)}
|
|
122
272
|
in:fly={toastEnter}
|
|
123
273
|
out:fly={toastLeave}
|
|
124
|
-
|
|
274
|
+
onpointerenter={handlePointerEnter}
|
|
275
|
+
onpointerleave={handlePointerLeave}
|
|
276
|
+
onpointerdown={handlePointerDown}
|
|
277
|
+
onpointerup={handlePointerRelease}
|
|
278
|
+
onpointercancel={handlePointerRelease}
|
|
279
|
+
onmouseenter={handleMouseEnter}
|
|
280
|
+
onmouseleave={() => (hovered = false)}
|
|
281
|
+
onfocusin={() => (focusWithin = true)}
|
|
282
|
+
onfocusout={handleFocusOut}
|
|
283
|
+
role="group"
|
|
284
|
+
aria-label={headerTitle}
|
|
285
|
+
data-toast-id={toast.id}
|
|
286
|
+
data-paused={paused}
|
|
125
287
|
>
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
288
|
+
<!-- Padding and gaps in px: at 200% text on a phone, room that grew with
|
|
289
|
+
the text left the text itself a column too narrow for one long word. -->
|
|
290
|
+
<div class="p-[16px]">
|
|
291
|
+
<!-- The title keeps 8rem at least, icon included. When the toast has
|
|
292
|
+
less beside its buttons (320px with enlarged text), the buttons go to a
|
|
293
|
+
line of their own under it, at the end. -->
|
|
294
|
+
<div class="flex flex-wrap items-start gap-x-[12px] gap-y-[4px]">
|
|
295
|
+
<!-- Live region holds only title + message, so the countdown and buttons are never re-announced. -->
|
|
296
|
+
<div
|
|
297
|
+
class="flex min-w-0 flex-[1_1_8rem] items-start gap-[12px]"
|
|
298
|
+
role={toast.type === 'error' ? 'alert' : 'status'}
|
|
299
|
+
aria-atomic="true"
|
|
300
|
+
>
|
|
301
|
+
<!-- 20px at every text size, as the padding and the gap beside it
|
|
302
|
+
are px: an icon that grew with the text took the room the text
|
|
303
|
+
needed. Lowered so it stays at the middle of the first line. -->
|
|
304
|
+
<div class="shrink-0 mt-[calc((1.5rem_-_20px)_/_2)]">
|
|
129
305
|
{#if toast.type === 'success'}
|
|
130
|
-
<CheckCircle class="size-
|
|
306
|
+
<CheckCircle class="size-[20px] {statusIconClass}" aria-hidden="true" />
|
|
131
307
|
{:else if toast.type === 'error'}
|
|
132
|
-
<AlertCircle class="size-
|
|
308
|
+
<AlertCircle class="size-[20px] {statusIconClass}" aria-hidden="true" />
|
|
133
309
|
{:else if toast.type === 'warning'}
|
|
134
|
-
<AlertTriangle class="size-
|
|
310
|
+
<AlertTriangle class="size-[20px] {statusIconClass}" aria-hidden="true" />
|
|
135
311
|
{:else}
|
|
136
|
-
<Info class="size-
|
|
312
|
+
<Info class="size-[20px] {statusIconClass}" aria-hidden="true" />
|
|
137
313
|
{/if}
|
|
138
314
|
</div>
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
315
|
+
<div class="min-w-0 flex-1">
|
|
316
|
+
<!-- A heading when the app gave the toast a title (or nothing
|
|
317
|
+
at all); a message on its own is a sentence, not a heading. -->
|
|
318
|
+
{#if title || !message}
|
|
319
|
+
<h4 class="text-base font-semibold text-headline [overflow-wrap:anywhere]">
|
|
320
|
+
{headerTitle}
|
|
321
|
+
</h4>
|
|
322
|
+
{:else}
|
|
323
|
+
<p class="text-base font-medium text-headline [overflow-wrap:anywhere]" data-toast-message>
|
|
324
|
+
{headerTitle}
|
|
325
|
+
</p>
|
|
326
|
+
{/if}
|
|
327
|
+
{#if secondLine}
|
|
328
|
+
<p class="mt-1 text-sm text-description [overflow-wrap:anywhere]" data-toast-message>
|
|
329
|
+
{secondLine}
|
|
330
|
+
</p>
|
|
331
|
+
{/if}
|
|
332
|
+
{#if toast.action}
|
|
333
|
+
<!-- The button sits outside the live region, so this is how a screen reader user hears that there is one. -->
|
|
334
|
+
<span class="sr-only">{strings.actionAvailable(toast.action.label)}</span>
|
|
335
|
+
{/if}
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
<div class="ms-auto flex shrink-0 items-center gap-1">
|
|
143
339
|
{#if hasExpandable}
|
|
144
340
|
<button
|
|
145
341
|
type="button"
|
|
146
|
-
class="focus-ring cursor-pointer rounded-
|
|
342
|
+
class="focus-ring cursor-pointer inline-flex items-center justify-center rounded-control p-1 pointer-coarse:min-h-[44px] pointer-coarse:min-w-[44px] text-description transition-colors hover:bg-surface-overlay-hover hover:text-headline active:bg-surface-active focus:outline-none"
|
|
147
343
|
aria-expanded={isExpanded}
|
|
148
|
-
aria-controls=
|
|
149
|
-
aria-label={isExpanded ?
|
|
344
|
+
aria-controls={isExpanded ? `toaster-expand-${toast.id}` : undefined}
|
|
345
|
+
aria-label={isExpanded ? strings.collapse : strings.expand}
|
|
346
|
+
bind:this={expandButton}
|
|
150
347
|
onclick={() => (isExpanded = !isExpanded)}
|
|
151
348
|
>
|
|
152
349
|
<ChevronDown
|
|
@@ -159,26 +356,41 @@
|
|
|
159
356
|
{/if}
|
|
160
357
|
<button
|
|
161
358
|
type="button"
|
|
162
|
-
class="focus-ring cursor-pointer rounded-
|
|
359
|
+
class="focus-ring cursor-pointer inline-flex items-center justify-center rounded-control p-1 pointer-coarse:min-h-[44px] pointer-coarse:min-w-[44px] text-description transition-colors hover:bg-surface-overlay-hover hover:text-headline active:bg-surface-active focus:outline-none"
|
|
163
360
|
onclick={handleDismiss}
|
|
164
|
-
aria-label=
|
|
361
|
+
aria-label={strings.dismiss}
|
|
362
|
+
bind:this={dismissButton}
|
|
165
363
|
>
|
|
166
364
|
<X class="size-5" aria-hidden="true" />
|
|
167
365
|
</button>
|
|
168
366
|
</div>
|
|
169
367
|
</div>
|
|
170
368
|
|
|
369
|
+
{#if toast.action}
|
|
370
|
+
<!-- Indented to the title: past the 20px status icon and its 12px gap. -->
|
|
371
|
+
<div class="mt-3 pl-[32px]">
|
|
372
|
+
<Button
|
|
373
|
+
variant="secondary"
|
|
374
|
+
size="sm"
|
|
375
|
+
text={toast.action.label}
|
|
376
|
+
type="button"
|
|
377
|
+
data-toast-action
|
|
378
|
+
onclick={handleAction}
|
|
379
|
+
/>
|
|
380
|
+
</div>
|
|
381
|
+
{/if}
|
|
382
|
+
|
|
171
383
|
{#if hasExpandable && isExpanded}
|
|
172
384
|
<div
|
|
173
385
|
id="toaster-expand-{toast.id}"
|
|
174
386
|
class="mt-3"
|
|
175
|
-
transition:slide={{ duration: 200 }}
|
|
387
|
+
transition:slide={{ duration: motion(200) }}
|
|
176
388
|
>
|
|
177
389
|
<p class="text-sm text-description">{expandableText}</p>
|
|
178
390
|
<Button
|
|
179
391
|
variant="primary"
|
|
180
392
|
size="sm"
|
|
181
|
-
text=
|
|
393
|
+
text={strings.okay}
|
|
182
394
|
type="button"
|
|
183
395
|
class="mt-3"
|
|
184
396
|
onclick={handleOkay}
|
|
@@ -187,25 +399,30 @@
|
|
|
187
399
|
{/if}
|
|
188
400
|
</div>
|
|
189
401
|
|
|
190
|
-
{#if timerActive}
|
|
191
|
-
<div class="px-
|
|
192
|
-
<p class="text-xs text-description">
|
|
193
|
-
|
|
402
|
+
{#if timerActive && showCountdown}
|
|
403
|
+
<div class="px-[16px] pb-3" transition:fade={{ duration: motion(180) }}>
|
|
404
|
+
<p class="text-xs text-description" data-toast-countdown>
|
|
405
|
+
{countdownText}
|
|
194
406
|
<button
|
|
195
407
|
type="button"
|
|
196
|
-
class="focus-ring cursor-pointer rounded-
|
|
197
|
-
onclick={
|
|
408
|
+
class="focus-ring cursor-pointer rounded-control text-link underline-offset-2 hover:underline active:underline focus:outline-none pointer-coarse:inline-flex pointer-coarse:items-center pointer-coarse:min-h-[44px]"
|
|
409
|
+
onclick={handleStop}
|
|
198
410
|
>
|
|
199
|
-
|
|
411
|
+
{strings.stop}
|
|
200
412
|
</button>
|
|
201
413
|
</p>
|
|
202
414
|
</div>
|
|
415
|
+
{:else if timerActive}
|
|
416
|
+
<!-- The bar below shows the time left to the eye. For a screen reader
|
|
417
|
+
it is here as words: outside the live region, so it is found when the
|
|
418
|
+
toast is read and is not spoken again every second. -->
|
|
419
|
+
<p class="sr-only" data-toast-countdown>{countdownText}</p>
|
|
203
420
|
{/if}
|
|
204
421
|
|
|
205
422
|
{#if timerActive}
|
|
206
423
|
<div
|
|
207
424
|
class="pointer-events-none absolute bottom-0 left-0 right-0 h-1 bg-border/70"
|
|
208
|
-
transition:fade={{ duration: 180 }}
|
|
425
|
+
transition:fade={{ duration: motion(180) }}
|
|
209
426
|
aria-hidden="true"
|
|
210
427
|
>
|
|
211
428
|
<div
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { type ToastItem } from './toast-store.js';
|
|
2
|
+
import { type ToastDuration, type ToasterStrings, type ToastPauseChange } from "../util/toaster.js";
|
|
2
3
|
interface Props {
|
|
4
|
+
/** Extra classes for the host element. */
|
|
5
|
+
class?: string;
|
|
3
6
|
toast: ToastItem;
|
|
7
|
+
/** Every word the toast says by itself; the Toaster passes its own. */
|
|
8
|
+
strings?: ToasterStrings;
|
|
9
|
+
/** Shows the time left as a sentence, with a button that stops the timer. */
|
|
10
|
+
showCountdown?: boolean;
|
|
11
|
+
/** What a toast pushed without a duration gets, in place of medium; the Toaster passes its own. */
|
|
12
|
+
defaultDuration?: ToastDuration;
|
|
13
|
+
/** Called when a pointer, a finger or focus starts or stops holding the timer. */
|
|
14
|
+
onpausechange?: (detail: ToastPauseChange) => void;
|
|
4
15
|
}
|
|
5
16
|
declare const ToasterToast: import("svelte").Component<Props, {}, "">;
|
|
6
17
|
type ToasterToast = ReturnType<typeof ToasterToast>;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { DEFAULT_UNSAVED_CHANGES_BAR_TEXTS, zabiStringsFor } from "../util/zabi-strings.js";
|
|
3
|
+
import { tick, untrack, type Snippet } from "svelte";
|
|
4
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
5
|
+
import Button from "../atoms/Button.svelte";
|
|
6
|
+
import { cn } from "../util/cn.js";
|
|
7
|
+
|
|
8
|
+
type Props = Omit<HTMLAttributes<HTMLDivElement>, "class" | "role"> & {
|
|
9
|
+
/** Shows the bar. Set it while the form differs from what is saved. */
|
|
10
|
+
dirty?: boolean;
|
|
11
|
+
/** Shown in the bar, and announced once when it appears. */
|
|
12
|
+
message?: string;
|
|
13
|
+
/** Accessible name of the bar as a landmark. */
|
|
14
|
+
label?: string;
|
|
15
|
+
saveLabel?: string;
|
|
16
|
+
discardLabel?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Save shows its loading state and Discard is disabled. Set by the
|
|
19
|
+
* bar itself while a promise from `onsave` is pending.
|
|
20
|
+
*/
|
|
21
|
+
saving?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Called when Save is activated. Return a promise to keep the bar in
|
|
24
|
+
* its saving state until it settles. The bar does not hide itself:
|
|
25
|
+
* clear `dirty` once the save has gone through.
|
|
26
|
+
*/
|
|
27
|
+
onsave?: () => void | Promise<unknown>;
|
|
28
|
+
/** Called when Discard is activated. */
|
|
29
|
+
ondiscard?: () => void;
|
|
30
|
+
/**
|
|
31
|
+
* Called when the promise from `onsave` rejects; the bar stays.
|
|
32
|
+
* Without it the error goes to the global error handler.
|
|
33
|
+
*/
|
|
34
|
+
onerror?: (error: unknown) => void;
|
|
35
|
+
/**
|
|
36
|
+
* Which edge of its scroll container the bar sticks to. It is sticky,
|
|
37
|
+
* not fixed, so it keeps its place in the flow: put it after the
|
|
38
|
+
* form's last field (`bottom`) or before its first (`top`).
|
|
39
|
+
*/
|
|
40
|
+
position?: "bottom" | "top";
|
|
41
|
+
/** Extra buttons, placed before Discard. */
|
|
42
|
+
actions?: Snippet;
|
|
43
|
+
/** Extra classes for the bar. */
|
|
44
|
+
class?: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
let {
|
|
48
|
+
dirty = false,
|
|
49
|
+
message: messageGiven,
|
|
50
|
+
label: labelGiven,
|
|
51
|
+
saveLabel: saveLabelGiven,
|
|
52
|
+
discardLabel: discardLabelGiven,
|
|
53
|
+
saving = false,
|
|
54
|
+
onsave,
|
|
55
|
+
ondiscard,
|
|
56
|
+
onerror,
|
|
57
|
+
position = "bottom",
|
|
58
|
+
actions,
|
|
59
|
+
class: className = "",
|
|
60
|
+
...restProps
|
|
61
|
+
}: Props = $props();
|
|
62
|
+
|
|
63
|
+
/** Each text: the prop, else the app-wide word from a `ZabiStringsProvider` above this one, else English. */
|
|
64
|
+
const provided = zabiStringsFor("unsavedChangesBar");
|
|
65
|
+
const message = $derived(messageGiven ?? provided()?.message ?? DEFAULT_UNSAVED_CHANGES_BAR_TEXTS.message);
|
|
66
|
+
const label = $derived(labelGiven ?? provided()?.label ?? DEFAULT_UNSAVED_CHANGES_BAR_TEXTS.label);
|
|
67
|
+
const saveLabel = $derived(saveLabelGiven ?? provided()?.saveLabel ?? DEFAULT_UNSAVED_CHANGES_BAR_TEXTS.saveLabel);
|
|
68
|
+
const discardLabel = $derived(discardLabelGiven ?? provided()?.discardLabel ?? DEFAULT_UNSAVED_CHANGES_BAR_TEXTS.discardLabel);
|
|
69
|
+
|
|
70
|
+
/** True while a promise returned by `onsave` is pending. */
|
|
71
|
+
let pending = $state(false);
|
|
72
|
+
const busy = $derived(saving || pending);
|
|
73
|
+
|
|
74
|
+
let host: HTMLDivElement | undefined = $state();
|
|
75
|
+
/** Where focus was before it came into the bar; it goes back there when the bar leaves. */
|
|
76
|
+
let cameFrom: HTMLElement | null = null;
|
|
77
|
+
|
|
78
|
+
async function save() {
|
|
79
|
+
if (busy) return;
|
|
80
|
+
// A synchronous throw is the caller's bug: it propagates.
|
|
81
|
+
const result = onsave?.();
|
|
82
|
+
if (!(result instanceof Promise)) return;
|
|
83
|
+
pending = true;
|
|
84
|
+
try {
|
|
85
|
+
await result;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
// The bar stays, so the caller can show what went wrong. Never
|
|
88
|
+
// swallowed: with no `onerror` it is reported like any other
|
|
89
|
+
// uncaught error.
|
|
90
|
+
if (onerror) onerror(error);
|
|
91
|
+
else if (typeof reportError === "function") reportError(error);
|
|
92
|
+
else console.error(error);
|
|
93
|
+
} finally {
|
|
94
|
+
pending = false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function discard() {
|
|
99
|
+
if (busy) return;
|
|
100
|
+
ondiscard?.();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function handleFocusIn(event: FocusEvent) {
|
|
104
|
+
const from = event.relatedTarget;
|
|
105
|
+
if (from instanceof HTMLElement && !host?.contains(from)) cameFrom = from;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function canTakeFocus(element: HTMLElement | null): element is HTMLElement {
|
|
109
|
+
return (
|
|
110
|
+
!!element &&
|
|
111
|
+
element.isConnected &&
|
|
112
|
+
!(element as HTMLButtonElement).disabled &&
|
|
113
|
+
!host?.contains(element)
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* While busy, Save is loading and stays focusable, so focus that is on
|
|
119
|
+
* it is left there. Discard is disabled, and a disabled button drops
|
|
120
|
+
* focus on `<body>`: focus that was on it (or on an action that is gone)
|
|
121
|
+
* is held on the bar instead, and handed to Save if the bar is still
|
|
122
|
+
* there afterwards (a rejected save).
|
|
123
|
+
*/
|
|
124
|
+
let wasBusy = false;
|
|
125
|
+
$effect.pre(() => {
|
|
126
|
+
const now = busy;
|
|
127
|
+
untrack(() => {
|
|
128
|
+
const changed = now !== wasBusy;
|
|
129
|
+
wasBusy = now;
|
|
130
|
+
// Read before the buttons change: afterwards focus may be gone.
|
|
131
|
+
if (!changed || !host?.contains(document.activeElement)) return;
|
|
132
|
+
void tick().then(() => {
|
|
133
|
+
if (now) {
|
|
134
|
+
const active = document.activeElement;
|
|
135
|
+
const usable =
|
|
136
|
+
!!active &&
|
|
137
|
+
active !== host &&
|
|
138
|
+
!!host?.contains(active) &&
|
|
139
|
+
!(active as HTMLButtonElement).disabled;
|
|
140
|
+
if (!usable) host?.focus();
|
|
141
|
+
} else if (dirty && document.activeElement === host) {
|
|
142
|
+
host?.querySelector<HTMLElement>("[data-unsaved-changes-save]")?.focus();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* When the bar leaves, its buttons go with it. If focus was on one, send
|
|
150
|
+
* it back to where it came from, usually the field the user was editing;
|
|
151
|
+
* failing that it stays on the (now empty) host, which keeps the user's
|
|
152
|
+
* place in the page instead of dropping them at the top.
|
|
153
|
+
*/
|
|
154
|
+
let wasDirty = false;
|
|
155
|
+
$effect.pre(() => {
|
|
156
|
+
const now = dirty;
|
|
157
|
+
untrack(() => {
|
|
158
|
+
const leaving = wasDirty && !now;
|
|
159
|
+
wasDirty = now;
|
|
160
|
+
if (!leaving || !host?.contains(document.activeElement)) return;
|
|
161
|
+
const target = canTakeFocus(cameFrom) ? cameFrom : host;
|
|
162
|
+
cameFrom = null;
|
|
163
|
+
void tick().then(() => {
|
|
164
|
+
const active = document.activeElement;
|
|
165
|
+
// Never from an element that has taken focus meanwhile.
|
|
166
|
+
if (active && active !== document.body && !host?.contains(active)) return;
|
|
167
|
+
target.focus();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
</script>
|
|
172
|
+
|
|
173
|
+
<!--
|
|
174
|
+
The host is always rendered and empty until `dirty`. The message is a live
|
|
175
|
+
region, and a live region is only announced when its text changes inside an
|
|
176
|
+
element that was already there. The status is on the message alone, so the
|
|
177
|
+
buttons are not read out with it, and nothing here takes focus on appearing.
|
|
178
|
+
-->
|
|
179
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
180
|
+
<div
|
|
181
|
+
bind:this={host}
|
|
182
|
+
tabindex="-1"
|
|
183
|
+
role={dirty ? "region" : undefined}
|
|
184
|
+
aria-label={dirty ? label : undefined}
|
|
185
|
+
aria-busy={dirty && busy ? "true" : undefined}
|
|
186
|
+
data-position={position}
|
|
187
|
+
class={cn(
|
|
188
|
+
"focus-ring",
|
|
189
|
+
dirty &&
|
|
190
|
+
"sticky z-sticky flex flex-wrap items-center justify-between gap-x-4 gap-y-2 rounded-container border border-border-overlay bg-surface-overlay px-4 py-3 shadow-lg",
|
|
191
|
+
// Clear of the home indicator on a phone.
|
|
192
|
+
dirty && (position === "top" ? "top-2" : "bottom-[max(0.5rem,env(safe-area-inset-bottom))]"),
|
|
193
|
+
dirty && className,
|
|
194
|
+
)}
|
|
195
|
+
onfocusin={handleFocusIn}
|
|
196
|
+
{...restProps}
|
|
197
|
+
>
|
|
198
|
+
<p role="status" class={dirty ? "min-w-0 text-sm font-medium text-body" : "sr-only"}>
|
|
199
|
+
{dirty ? message : ""}
|
|
200
|
+
</p>
|
|
201
|
+
{#if dirty}
|
|
202
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
203
|
+
{@render actions?.()}
|
|
204
|
+
<Button variant="ghost" onclick={discard} disabled={busy}>
|
|
205
|
+
{discardLabel}
|
|
206
|
+
</Button>
|
|
207
|
+
<Button onclick={save} loading={busy} data-unsaved-changes-save>
|
|
208
|
+
{saveLabel}
|
|
209
|
+
</Button>
|
|
210
|
+
</div>
|
|
211
|
+
{/if}
|
|
212
|
+
</div>
|