zabi-components 8.0.0 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2935 -0
- package/README.md +243 -10
- package/THEME.md +162 -332
- package/THEMING.md +1419 -259
- package/dist/atoms/ActionPanel.svelte +15 -5
- package/dist/atoms/Avatar.svelte +120 -0
- package/dist/atoms/Avatar.svelte.d.ts +46 -0
- package/dist/atoms/Badge.svelte +71 -13
- package/dist/atoms/Badge.svelte.d.ts +8 -2
- package/dist/atoms/Block.svelte +74 -0
- package/dist/atoms/Block.svelte.d.ts +29 -0
- package/dist/atoms/Button.svelte +217 -30
- package/dist/atoms/Button.svelte.d.ts +33 -4
- package/dist/atoms/Card.svelte +182 -26
- package/dist/atoms/Card.svelte.d.ts +55 -3
- package/dist/atoms/CardHeader.svelte +16 -4
- package/dist/atoms/Checkbox.svelte +81 -21
- package/dist/atoms/Checkbox.svelte.d.ts +31 -2
- package/dist/atoms/Chip.svelte +351 -0
- package/dist/atoms/Chip.svelte.d.ts +57 -0
- package/dist/atoms/CodeBlock.svelte +20 -2
- package/dist/atoms/CodeBlock.svelte.d.ts +4 -0
- package/dist/atoms/CodeField.svelte +385 -0
- package/dist/atoms/CodeField.svelte.d.ts +61 -0
- package/dist/atoms/ColorPicker.svelte +260 -49
- package/dist/atoms/ColorPicker.svelte.d.ts +3 -0
- package/dist/atoms/Container.svelte +16 -8
- package/dist/atoms/DateField.svelte +145 -0
- package/dist/atoms/DateField.svelte.d.ts +81 -0
- package/dist/atoms/FieldMessages.svelte +69 -0
- package/dist/atoms/FieldMessages.svelte.d.ts +23 -0
- package/dist/atoms/FloatingActionButton.svelte +176 -0
- package/dist/atoms/FloatingActionButton.svelte.d.ts +51 -0
- package/dist/atoms/Heading.svelte +63 -5
- package/dist/atoms/Heading.svelte.d.ts +14 -0
- package/dist/atoms/IconButton.svelte +294 -26
- package/dist/atoms/IconButton.svelte.d.ts +62 -4
- package/dist/atoms/Input.svelte +259 -53
- package/dist/atoms/Input.svelte.d.ts +33 -2
- package/dist/atoms/List.svelte +5 -1
- package/dist/atoms/ListItem.svelte +70 -13
- package/dist/atoms/ListItem.svelte.d.ts +11 -0
- package/dist/atoms/PickerField.svelte +239 -0
- package/dist/atoms/PickerField.svelte.d.ts +67 -0
- package/dist/atoms/Progress.svelte +99 -19
- package/dist/atoms/Progress.svelte.d.ts +15 -1
- package/dist/atoms/Radio.svelte +51 -3
- package/dist/atoms/Radio.svelte.d.ts +30 -2
- package/dist/atoms/Rating.svelte +620 -0
- package/dist/atoms/Rating.svelte.d.ts +77 -0
- package/dist/atoms/Select.svelte +438 -98
- package/dist/atoms/Select.svelte.d.ts +61 -3
- package/dist/atoms/SelectionControl.svelte +193 -23
- package/dist/atoms/SelectionControl.svelte.d.ts +29 -3
- package/dist/atoms/Skeleton.svelte +114 -18
- package/dist/atoms/Skeleton.svelte.d.ts +30 -3
- package/dist/atoms/SkeletonGroup.svelte +43 -0
- package/dist/atoms/SkeletonGroup.svelte.d.ts +20 -0
- package/dist/atoms/Slider.svelte +344 -0
- package/dist/atoms/Slider.svelte.d.ts +40 -0
- package/dist/atoms/Sparkline.svelte +109 -0
- package/dist/atoms/Sparkline.svelte.d.ts +27 -0
- package/dist/atoms/SpeechBubble.svelte +187 -0
- package/dist/atoms/SpeechBubble.svelte.d.ts +33 -0
- package/dist/atoms/Spinner.svelte +57 -0
- package/dist/atoms/Spinner.svelte.d.ts +19 -0
- package/dist/atoms/Stat.svelte +72 -0
- package/dist/atoms/Stat.svelte.d.ts +19 -0
- package/dist/atoms/Table.svelte +151 -6
- package/dist/atoms/Table.svelte.d.ts +15 -3
- package/dist/atoms/TemporalField.svelte +472 -0
- package/dist/atoms/TemporalField.svelte.d.ts +48 -0
- package/dist/atoms/Text.svelte +23 -11
- package/dist/atoms/Text.svelte.d.ts +6 -3
- package/dist/atoms/Textarea.svelte +150 -44
- package/dist/atoms/Textarea.svelte.d.ts +35 -2
- package/dist/atoms/ThemeToggle.svelte +223 -44
- package/dist/atoms/ThemeToggle.svelte.d.ts +26 -3
- package/dist/atoms/TimeColumn.svelte +150 -0
- package/dist/atoms/TimeColumn.svelte.d.ts +25 -0
- package/dist/atoms/TimeField.svelte +151 -0
- package/dist/atoms/TimeField.svelte.d.ts +85 -0
- package/dist/atoms/Toast.svelte +22 -8
- package/dist/atoms/Toast.svelte.d.ts +2 -0
- package/dist/atoms/Toggle.svelte +98 -14
- package/dist/atoms/Toggle.svelte.d.ts +26 -2
- package/dist/atoms/Tooltip.svelte +528 -24
- package/dist/atoms/Tooltip.svelte.d.ts +18 -0
- package/dist/atoms/ZabiStringsProvider.svelte +45 -0
- package/dist/atoms/ZabiStringsProvider.svelte.d.ts +27 -0
- package/dist/atoms/index.d.ts +38 -0
- package/dist/atoms/index.js +29 -0
- package/dist/atoms/selection-control.styles.d.ts +47 -5
- package/dist/atoms/selection-control.styles.js +52 -10
- package/dist/components/atoms/index.d.ts +38 -0
- package/dist/components/atoms/index.d.ts.map +1 -1
- package/dist/components/atoms/selection-control.styles.d.ts +47 -5
- package/dist/components/atoms/selection-control.styles.d.ts.map +1 -1
- package/dist/components/index.d.ts +104 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/map.d.ts +11 -0
- package/dist/components/map.d.ts.map +1 -0
- package/dist/components/molecules/index.d.ts +51 -0
- package/dist/components/molecules/index.d.ts.map +1 -1
- package/dist/components/molecules/toast-store.d.ts +83 -7
- package/dist/components/molecules/toast-store.d.ts.map +1 -1
- package/dist/components/organisms/index.d.ts +10 -0
- package/dist/components/organisms/index.d.ts.map +1 -1
- package/dist/components/types/variants.d.ts +24 -1
- package/dist/components/types/variants.d.ts.map +1 -1
- package/dist/components/util/app-shell.d.ts +79 -0
- package/dist/components/util/app-shell.d.ts.map +1 -0
- package/dist/components/util/avatar.d.ts +39 -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 +41 -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/chart.d.ts +140 -0
- package/dist/components/util/chart.d.ts.map +1 -0
- package/dist/components/util/chip.d.ts +66 -0
- package/dist/components/util/chip.d.ts.map +1 -0
- package/dist/components/util/cn.d.ts.map +1 -1
- package/dist/components/util/code-field.d.ts +63 -0
- package/dist/components/util/code-field.d.ts.map +1 -0
- package/dist/components/util/collapsible.d.ts +64 -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/display-voice.d.ts +10 -0
- package/dist/components/util/display-voice.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-control.d.ts +30 -0
- package/dist/components/util/field-control.d.ts.map +1 -0
- package/dist/components/util/field.d.ts +45 -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 +77 -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/map-view.d.ts +144 -0
- package/dist/components/util/map-view.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 +234 -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/progress.d.ts +19 -0
- package/dist/components/util/progress.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-edge.d.ts +33 -0
- package/dist/components/util/scroll-edge.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 +84 -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/skeleton.d.ts +15 -0
- package/dist/components/util/skeleton.d.ts.map +1 -0
- package/dist/components/util/sliding-indicator.d.ts +25 -0
- package/dist/components/util/sliding-indicator.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/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/temporal-field.d.ts +89 -0
- package/dist/components/util/temporal-field.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 +133 -0
- package/dist/components/util/toaster.d.ts.map +1 -0
- package/dist/components/util/tone-surface.d.ts +34 -0
- package/dist/components/util/tone-surface.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 +35 -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 +142 -0
- package/dist/components/util/zabi-strings.d.ts.map +1 -0
- package/dist/create-theme/cli.js +173 -0
- package/dist/create-theme/index.d.ts +190 -0
- package/dist/create-theme/index.js +1034 -0
- package/dist/create-theme/lib/ramp-math.js +128 -0
- package/dist/create-theme/lib/resolve.js +238 -0
- package/dist/create-theme/theme-data.js +2644 -0
- package/dist/index.d.ts +104 -2
- package/dist/index.js +75 -2
- package/dist/map.d.ts +10 -0
- package/dist/map.js +9 -0
- package/dist/molecules/Alert.svelte +63 -16
- package/dist/molecules/Alert.svelte.d.ts +14 -1
- package/dist/molecules/AppBar.svelte +674 -0
- package/dist/molecules/AppBar.svelte.d.ts +131 -0
- package/dist/molecules/AvatarGroup.svelte +116 -0
- package/dist/molecules/AvatarGroup.svelte.d.ts +40 -0
- package/dist/molecules/BarChart.svelte +381 -0
- package/dist/molecules/BarChart.svelte.d.ts +45 -0
- package/dist/molecules/BottomSheet.svelte +608 -0
- package/dist/molecules/BottomSheet.svelte.d.ts +109 -0
- package/dist/molecules/BottomTabBar.svelte +403 -0
- package/dist/molecules/BottomTabBar.svelte.d.ts +98 -0
- package/dist/molecules/Calendar.svelte +597 -0
- package/dist/molecules/Calendar.svelte.d.ts +72 -0
- package/dist/molecules/ChipGroup.svelte +325 -0
- package/dist/molecules/ChipGroup.svelte.d.ts +41 -0
- package/dist/molecules/Collapsible.svelte +347 -0
- package/dist/molecules/Collapsible.svelte.d.ts +61 -0
- package/dist/molecules/CollapsibleGroup.svelte +136 -0
- package/dist/molecules/CollapsibleGroup.svelte.d.ts +14 -0
- package/dist/molecules/ComponentDemo.svelte +16 -4
- package/dist/molecules/ComponentDemo.svelte.d.ts +3 -0
- package/dist/molecules/ConfirmDialog.svelte +257 -0
- package/dist/molecules/ConfirmDialog.svelte.d.ts +58 -0
- package/dist/molecules/ContactForm.svelte +34 -17
- package/dist/molecules/ContactForm.svelte.d.ts +8 -0
- package/dist/molecules/Drawer.svelte +415 -0
- package/dist/molecules/Drawer.svelte.d.ts +55 -0
- package/dist/molecules/Dropdown.svelte +517 -48
- package/dist/molecules/Dropdown.svelte.d.ts +47 -6
- package/dist/molecules/DropdownItem.svelte +122 -0
- package/dist/molecules/DropdownItem.svelte.d.ts +28 -0
- package/dist/molecules/EmptyState.svelte +55 -10
- package/dist/molecules/EmptyState.svelte.d.ts +8 -0
- package/dist/molecules/FormField.svelte +9 -1
- package/dist/molecules/FormField.svelte.d.ts +2 -0
- package/dist/molecules/Header.svelte +1 -1
- package/dist/molecules/ImageUpload.svelte +492 -69
- package/dist/molecules/ImageUpload.svelte.d.ts +82 -2
- package/dist/molecules/LineChart.svelte +459 -0
- package/dist/molecules/LineChart.svelte.d.ts +53 -0
- package/dist/molecules/MapView.svelte +565 -0
- package/dist/molecules/MapView.svelte.d.ts +88 -0
- package/dist/molecules/MediaGrid.svelte +527 -0
- package/dist/molecules/MediaGrid.svelte.d.ts +77 -0
- package/dist/molecules/Modal.svelte +293 -72
- package/dist/molecules/Modal.svelte.d.ts +51 -2
- package/dist/molecules/NavigationMenuContent.svelte +58 -1
- package/dist/molecules/NavigationMenuItem.svelte +0 -1
- package/dist/molecules/NavigationMenuLink.svelte +1 -1
- package/dist/molecules/NavigationMenuList.svelte +10 -3
- package/dist/molecules/NavigationMenuTrigger.svelte +2 -2
- package/dist/molecules/Page.svelte +32 -2
- package/dist/molecules/Page.svelte.d.ts +11 -0
- package/dist/molecules/PhotoGrid.svelte +523 -0
- package/dist/molecules/PhotoGrid.svelte.d.ts +73 -0
- package/dist/molecules/PhotoViewer.svelte +1213 -0
- package/dist/molecules/PhotoViewer.svelte.d.ts +74 -0
- package/dist/molecules/PieChart.svelte +421 -0
- package/dist/molecules/PieChart.svelte.d.ts +51 -0
- package/dist/molecules/PropsTable.svelte +22 -10
- package/dist/molecules/PropsTable.svelte.d.ts +3 -0
- package/dist/molecules/PullToRefresh.svelte +263 -0
- package/dist/molecules/PullToRefresh.svelte.d.ts +52 -0
- package/dist/molecules/RadioGroup.svelte +72 -18
- package/dist/molecules/RadioGroup.svelte.d.ts +19 -3
- package/dist/molecules/Section.svelte +1 -1
- package/dist/molecules/SegmentedControl.svelte +456 -0
- package/dist/molecules/SegmentedControl.svelte.d.ts +40 -0
- package/dist/molecules/SidebarBrandHeader.svelte +11 -1
- package/dist/molecules/SidebarBrandHeader.svelte.d.ts +3 -0
- package/dist/molecules/SidebarFooter.svelte +30 -7
- package/dist/molecules/SidebarFooter.svelte.d.ts +3 -0
- package/dist/molecules/SidebarNavSection.svelte +1 -1
- package/dist/molecules/SlideUp.svelte +220 -61
- package/dist/molecules/SlideUp.svelte.d.ts +26 -0
- package/dist/molecules/SortableList.svelte +633 -0
- package/dist/molecules/SortableList.svelte.d.ts +69 -0
- package/dist/molecules/Stepper.svelte +562 -0
- package/dist/molecules/Stepper.svelte.d.ts +39 -0
- package/dist/molecules/StickyActionBar.svelte +250 -0
- package/dist/molecules/StickyActionBar.svelte.d.ts +78 -0
- package/dist/molecules/SwipeableListItem.svelte +334 -0
- package/dist/molecules/SwipeableListItem.svelte.d.ts +54 -0
- package/dist/molecules/Tabs.svelte +310 -9
- package/dist/molecules/Tabs.svelte.d.ts +7 -0
- package/dist/molecules/Toaster.svelte +514 -5
- package/dist/molecules/Toaster.svelte.d.ts +153 -2
- package/dist/molecules/ToasterToast.svelte +495 -77
- package/dist/molecules/ToasterToast.svelte.d.ts +15 -0
- package/dist/molecules/UnsavedChangesBar.svelte +223 -0
- package/dist/molecules/UnsavedChangesBar.svelte.d.ts +43 -0
- package/dist/molecules/index.d.ts +51 -0
- package/dist/molecules/index.js +31 -0
- package/dist/molecules/toast-store.d.ts +83 -7
- package/dist/molecules/toast-store.js +25 -1
- package/dist/organisms/AppNavigation.svelte +440 -0
- package/dist/organisms/AppNavigation.svelte.d.ts +72 -0
- package/dist/organisms/AppShell.svelte +696 -0
- package/dist/organisms/AppShell.svelte.d.ts +157 -0
- package/dist/organisms/AppShellHeaderPart.svelte +163 -0
- package/dist/organisms/AppShellHeaderPart.svelte.d.ts +57 -0
- package/dist/organisms/SidebarAccountPanel.svelte +107 -12
- package/dist/organisms/SidebarAccountPanel.svelte.d.ts +19 -0
- package/dist/organisms/SidebarNavigation.svelte +111 -43
- package/dist/organisms/SidebarNavigation.svelte.d.ts +29 -1
- package/dist/organisms/SidebarPanel.svelte +34 -17
- package/dist/organisms/SidebarPanel.svelte.d.ts +1 -0
- package/dist/organisms/SidebarShell.svelte +164 -14
- package/dist/organisms/SidebarShell.svelte.d.ts +56 -2
- package/dist/organisms/TopNavbar.svelte +353 -39
- package/dist/organisms/TopNavbar.svelte.d.ts +47 -0
- package/dist/organisms/TopNavbarSheetContent.svelte +145 -0
- package/dist/organisms/TopNavbarSheetContent.svelte.d.ts +24 -0
- package/dist/organisms/index.d.ts +10 -0
- package/dist/organisms/index.js +7 -0
- package/dist/types/components.d.ts +21 -1
- package/dist/types/components.d.ts.map +1 -1
- package/dist/types/index.d.ts +1093 -26
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/variants.ts +28 -1
- package/dist/util/app-shell.d.ts +78 -0
- package/dist/util/app-shell.js +97 -0
- package/dist/util/avatar.d.ts +38 -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 +40 -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/chart.d.ts +139 -0
- package/dist/util/chart.js +305 -0
- package/dist/util/chip.d.ts +65 -0
- package/dist/util/chip.js +58 -0
- package/dist/util/cn.js +28 -2
- package/dist/util/code-field.d.ts +62 -0
- package/dist/util/code-field.js +112 -0
- package/dist/util/collapsible.d.ts +63 -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/display-voice.d.ts +9 -0
- package/dist/util/display-voice.js +9 -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-control.d.ts +29 -0
- package/dist/util/field-control.js +32 -0
- package/dist/util/field.d.ts +44 -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 +77 -2
- package/dist/util/focus-utils.js +186 -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/map-view.d.ts +143 -0
- package/dist/util/map-view.js +121 -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 +233 -0
- package/dist/util/photo.js +209 -0
- package/dist/util/portal.d.ts +22 -0
- package/dist/util/portal.js +50 -0
- package/dist/util/progress.d.ts +18 -0
- package/dist/util/progress.js +17 -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-edge.d.ts +32 -0
- package/dist/util/scroll-edge.js +87 -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 +83 -0
- package/dist/util/sheet-drag.js +311 -0
- package/dist/util/sidebar.d.ts +62 -0
- package/dist/util/sidebar.js +38 -0
- package/dist/util/skeleton.d.ts +14 -0
- package/dist/util/skeleton.js +22 -0
- package/dist/util/sliding-indicator.d.ts +24 -0
- package/dist/util/sliding-indicator.js +44 -0
- package/dist/util/sortable-list.d.ts +64 -0
- package/dist/util/sortable-list.js +53 -0
- 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/temporal-field.d.ts +88 -0
- package/dist/util/temporal-field.js +136 -0
- package/dist/util/theme-mode.d.ts +79 -0
- package/dist/util/theme-mode.js +121 -0
- package/dist/util/toaster.d.ts +132 -0
- package/dist/util/toaster.js +130 -0
- package/dist/util/tone-surface.d.ts +33 -0
- package/dist/util/tone-surface.js +35 -0
- package/dist/util/tooltip.d.ts +9 -0
- package/dist/util/tooltip.js +18 -0
- package/dist/util/top-navbar.d.ts +34 -0
- package/dist/util/top-navbar.js +28 -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 +141 -0
- package/dist/util/zabi-strings.js +105 -0
- package/dist/zabi-components-colors.css +11 -2
- package/dist/zabi-components-theme-dark-only.css +870 -258
- package/dist/zabi-components-theme-dark.css +870 -258
- package/dist/zabi-components-theme-only.css +2419 -368
- package/dist/zabi-components-theme.css +2419 -368
- package/dist/zabi-components.css +6032 -711
- package/docs/lucide-icons.md +40 -17
- package/docs/theme-imports.md +81 -11
- package/package.json +45 -8
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import type { HTMLTextareaAttributes } from 'svelte/elements';
|
|
3
3
|
import type { SemanticVariant, SizeVariant } from '../types/variants.js';
|
|
4
4
|
import { generateId } from "../util/ssr-safe.js";
|
|
5
5
|
import { cn } from "../util/cn.js";
|
|
6
|
+
import { fieldDescribedBy, fieldMessageState, type TextareaCount } from "../util/field.js";
|
|
7
|
+
import FieldMessages from "./FieldMessages.svelte";
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Other attributes (`autocomplete`, `inputmode`, `maxlength`,
|
|
11
|
+
* `enterkeyhint`, `data-*`, `onchange`, ...) land on the `<textarea>`.
|
|
12
|
+
*/
|
|
13
|
+
type Props = Omit<
|
|
14
|
+
HTMLTextareaAttributes,
|
|
15
|
+
| 'class'
|
|
16
|
+
| 'value'
|
|
17
|
+
| 'id'
|
|
18
|
+
| 'name'
|
|
19
|
+
| 'placeholder'
|
|
20
|
+
| 'required'
|
|
21
|
+
| 'disabled'
|
|
22
|
+
| 'rows'
|
|
23
|
+
| 'oninput'
|
|
24
|
+
| 'aria-describedby'
|
|
25
|
+
> & {
|
|
8
26
|
id?: string;
|
|
9
27
|
value?: string;
|
|
10
28
|
name?: string;
|
|
@@ -16,15 +34,42 @@
|
|
|
16
34
|
disabled?: boolean;
|
|
17
35
|
loading?: boolean;
|
|
18
36
|
rows?: number;
|
|
37
|
+
/**
|
|
38
|
+
* The resize grip. `auto` (default): the browser's vertical grip with a mouse or
|
|
39
|
+
* trackpad, none on a touch screen, where dragging it scrolls the page. `vertical`
|
|
40
|
+
* shows it everywhere, `none` never. With `autogrow` there is no grip.
|
|
41
|
+
*/
|
|
42
|
+
resize?: 'auto' | 'none' | 'vertical';
|
|
43
|
+
/** The field grows with its content, from `rows` up to `maxRows`, and scrolls after that. */
|
|
44
|
+
autogrow?: boolean;
|
|
45
|
+
/** The tallest an `autogrow` field gets, in rows. Without it the field has no limit. */
|
|
46
|
+
maxRows?: number;
|
|
19
47
|
size?: SizeVariant;
|
|
20
48
|
variant?: SemanticVariant;
|
|
49
|
+
/** Status text under the field. Shown with a `success`, `warning` or `error` variant; for neutral help, use `hint`. */
|
|
21
50
|
message?: string;
|
|
51
|
+
/** Help text under the field, read out with it. */
|
|
52
|
+
hint?: string;
|
|
53
|
+
/** An error under the field. It marks the field invalid and is announced; it wins over `variant` and `message`. */
|
|
54
|
+
error?: string;
|
|
55
|
+
/**
|
|
56
|
+
* A character count under the field, at the end of the line the hint is on. It needs
|
|
57
|
+
* `maxlength`; without one nothing is shown. It is part of the field's description
|
|
58
|
+
* (`aria-describedby`) and is not a live region, so it is not read out at every key.
|
|
59
|
+
*/
|
|
60
|
+
count?: boolean;
|
|
61
|
+
/** The count is shown from this many characters on. `0` (default): always. */
|
|
62
|
+
countFrom?: number;
|
|
63
|
+
/** The words of the count, for the page's language. Default: "12 / 2000". */
|
|
64
|
+
countText?: (count: TextareaCount) => string;
|
|
22
65
|
oninput?: (event: Event) => void;
|
|
23
|
-
|
|
66
|
+
/** Ids of other elements that describe the field. The hint and the message are added after them. */
|
|
67
|
+
"aria-describedby"?: string | null;
|
|
68
|
+
};
|
|
24
69
|
|
|
25
70
|
let {
|
|
26
71
|
id: idProp,
|
|
27
|
-
value = $bindable(
|
|
72
|
+
value = $bindable<Exclude<Props["value"], undefined>>(),
|
|
28
73
|
name = '',
|
|
29
74
|
class: className = '',
|
|
30
75
|
label = '',
|
|
@@ -34,56 +79,113 @@
|
|
|
34
79
|
disabled = false,
|
|
35
80
|
loading = false,
|
|
36
81
|
rows = 4,
|
|
82
|
+
resize = 'auto',
|
|
83
|
+
autogrow = false,
|
|
84
|
+
maxRows,
|
|
37
85
|
size = 'md',
|
|
38
86
|
variant = 'default',
|
|
39
87
|
message = '',
|
|
88
|
+
hint = '',
|
|
89
|
+
error = '',
|
|
90
|
+
count = false,
|
|
91
|
+
countFrom = 0,
|
|
92
|
+
countText,
|
|
40
93
|
oninput,
|
|
94
|
+
'aria-describedby': describedBy,
|
|
41
95
|
...restProps
|
|
42
96
|
}: Props = $props();
|
|
43
97
|
|
|
98
|
+
// No fallback on a bindable prop: Svelte refuses `bind:…={undefined}` on
|
|
99
|
+
// one that has a fallback (`props_invalid_value`), and a page that throws
|
|
100
|
+
// while it hydrates never becomes interactive. The default is applied
|
|
101
|
+
// here instead: at once, for the server and the first render, and again
|
|
102
|
+
// whenever a parent hands back `undefined`.
|
|
103
|
+
const applyDefaults = () => {
|
|
104
|
+
if (value === undefined) value = '';
|
|
105
|
+
};
|
|
106
|
+
applyDefaults();
|
|
107
|
+
$effect.pre(applyDefaults);
|
|
108
|
+
|
|
44
109
|
const fallbackId = generateId('textarea');
|
|
45
110
|
const textareaId = $derived(idProp ?? fallbackId);
|
|
46
111
|
const isDisabled = $derived(disabled || loading);
|
|
47
112
|
|
|
113
|
+
const status = $derived(fieldMessageState({ variant, message, hint, error }));
|
|
114
|
+
|
|
115
|
+
// The count: only with a `maxlength`, and only once `countFrom` is reached.
|
|
116
|
+
// The length is the one `maxlength` counts (UTF-16 units).
|
|
117
|
+
const countMax = $derived(Number(restProps.maxlength));
|
|
118
|
+
const countLength = $derived((value ?? '').length);
|
|
119
|
+
const showCount = $derived(
|
|
120
|
+
count && Number.isFinite(countMax) && countMax > 0 && countLength >= countFrom,
|
|
121
|
+
);
|
|
122
|
+
const countLabel = $derived.by(() => {
|
|
123
|
+
if (!showCount) return '';
|
|
124
|
+
const state = { length: countLength, max: countMax, remaining: Math.max(0, countMax - countLength) };
|
|
125
|
+
return countText ? countText(state) : `${state.length} / ${state.max}`;
|
|
126
|
+
});
|
|
127
|
+
const countId = $derived(`${textareaId}-count`);
|
|
128
|
+
// The caller's ids, the hint and the message, then the count.
|
|
129
|
+
const fieldDescription = $derived(
|
|
130
|
+
[fieldDescribedBy(textareaId, status, describedBy), showCount ? countId : '']
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
.join(' ') || undefined,
|
|
133
|
+
);
|
|
134
|
+
|
|
48
135
|
const variantClass = $derived(() => {
|
|
49
|
-
return variant === 'success'
|
|
136
|
+
return status.variant === 'success'
|
|
50
137
|
? 'border-success focus-visible:border-success'
|
|
51
|
-
: variant === 'warning'
|
|
138
|
+
: status.variant === 'warning'
|
|
52
139
|
? 'border-warning focus-visible:border-warning'
|
|
53
|
-
: variant === 'error'
|
|
140
|
+
: status.variant === 'error'
|
|
54
141
|
? 'border-error focus-visible:border-error'
|
|
55
|
-
: 'border-input-border';
|
|
142
|
+
: 'border-input-border enabled:hover:border-input-border-hover';
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const resizeClass = $derived(
|
|
146
|
+
autogrow || resize === 'none'
|
|
147
|
+
? 'resize-none'
|
|
148
|
+
: resize === 'vertical'
|
|
149
|
+
? 'resize-y'
|
|
150
|
+
: 'resize-y pointer-coarse:resize-none',
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// Autogrow: `field-sizing: content` where the browser has it, held between
|
|
154
|
+
// `rows` and `maxRows` lines (`leading-6`, plus `py-2` and the two borders,
|
|
155
|
+
// since the box is border-box). Elsewhere the height is measured from the
|
|
156
|
+
// content after each change; the same two limits clamp it.
|
|
157
|
+
const linesHeight = (lines: number) => `calc(${lines * 1.5 + 1}rem + 2px)`;
|
|
158
|
+
const minHeight = $derived(autogrow ? linesHeight(Math.max(1, rows)) : undefined);
|
|
159
|
+
const maxHeight = $derived(
|
|
160
|
+
autogrow && maxRows !== undefined ? linesHeight(Math.max(rows, maxRows)) : undefined,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
let element: HTMLTextAreaElement | undefined = $state();
|
|
164
|
+
|
|
165
|
+
$effect(() => {
|
|
166
|
+
void value;
|
|
167
|
+
if (!autogrow || !element) return;
|
|
168
|
+
if (typeof CSS !== 'undefined' && CSS.supports?.('field-sizing', 'content')) return;
|
|
169
|
+
const field = element;
|
|
170
|
+
field.style.height = 'auto';
|
|
171
|
+
field.style.height = `${field.scrollHeight + field.offsetHeight - field.clientHeight}px`;
|
|
172
|
+
return () => {
|
|
173
|
+
field.style.height = '';
|
|
174
|
+
};
|
|
56
175
|
});
|
|
57
176
|
|
|
177
|
+
// 16px below `sm`: iOS Safari zooms the page when a focused field is smaller.
|
|
58
178
|
const textareaClasses = $derived(() => {
|
|
59
179
|
const baseClasses =
|
|
60
|
-
'focus-ring w-full border bg-input hover:bg-input-hover focus-visible:bg-input-focus disabled:bg-input-disabled rounded-control transition-colors duration-150 placeholder:text-input-placeholder text-body focus:outline-none focus-visible:outline-none disabled:text-action-disabled-text disabled:cursor-not-allowed
|
|
180
|
+
'focus-ring w-full border bg-input hover:bg-input-hover focus-visible:bg-input-focus disabled:bg-input-disabled rounded-control transition-colors duration-150 placeholder:text-input-placeholder text-body focus:outline-none focus-visible:outline-none disabled:text-action-disabled-text disabled:cursor-not-allowed px-3 py-2 text-sm max-sm:text-base leading-6';
|
|
61
181
|
|
|
62
|
-
return cn(`${baseClasses} ${variantClass()} ${className}`);
|
|
182
|
+
return cn(`${baseClasses} ${resizeClass} ${variantClass()} ${className}`);
|
|
63
183
|
});
|
|
64
184
|
|
|
65
185
|
const labelClasses = $derived(
|
|
66
186
|
() => 'mb-1 block text-sm font-medium text-label',
|
|
67
187
|
);
|
|
68
188
|
|
|
69
|
-
const messageClasses = $derived(() => {
|
|
70
|
-
if (variant === 'error') {
|
|
71
|
-
return 'mt-1 flex items-center gap-2 text-sm text-error';
|
|
72
|
-
} else if (variant === 'success') {
|
|
73
|
-
return 'mt-1 flex items-center gap-2 text-sm text-success';
|
|
74
|
-
} else if (variant === 'warning') {
|
|
75
|
-
return 'mt-1 flex items-center gap-2 text-sm text-warning';
|
|
76
|
-
}
|
|
77
|
-
return 'mt-1 flex items-center gap-2 text-sm text-description';
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
const getIcon = $derived(() => {
|
|
81
|
-
if (variant === 'error') return AlertCircle;
|
|
82
|
-
if (variant === 'success') return CheckCircle;
|
|
83
|
-
if (variant === 'warning') return AlertTriangle;
|
|
84
|
-
return null;
|
|
85
|
-
});
|
|
86
|
-
|
|
87
189
|
function handleInput(event: Event) {
|
|
88
190
|
const target = event.target as HTMLTextAreaElement;
|
|
89
191
|
value = target.value;
|
|
@@ -97,6 +199,7 @@
|
|
|
97
199
|
{/if}
|
|
98
200
|
<div class="relative">
|
|
99
201
|
<textarea
|
|
202
|
+
bind:this={element}
|
|
100
203
|
id={textareaId}
|
|
101
204
|
{name}
|
|
102
205
|
bind:value
|
|
@@ -105,11 +208,15 @@
|
|
|
105
208
|
disabled={isDisabled}
|
|
106
209
|
{rows}
|
|
107
210
|
class={textareaClasses()}
|
|
211
|
+
style:field-sizing={autogrow ? 'content' : undefined}
|
|
212
|
+
style:min-height={minHeight}
|
|
213
|
+
style:max-height={maxHeight}
|
|
214
|
+
style:overflow-y={maxHeight ? 'auto' : undefined}
|
|
108
215
|
oninput={handleInput}
|
|
109
|
-
aria-invalid={variant === 'error' ? 'true' : undefined}
|
|
216
|
+
aria-invalid={status.variant === 'error' ? 'true' : undefined}
|
|
110
217
|
aria-required={required ? 'true' : undefined}
|
|
111
218
|
aria-busy={loading ? 'true' : undefined}
|
|
112
|
-
aria-describedby={
|
|
219
|
+
aria-describedby={fieldDescription}
|
|
113
220
|
{...restProps}
|
|
114
221
|
></textarea>
|
|
115
222
|
{#if loading}
|
|
@@ -118,23 +225,22 @@
|
|
|
118
225
|
aria-hidden="true"
|
|
119
226
|
>
|
|
120
227
|
<span
|
|
121
|
-
class="inline-block size-5 shrink-0 animate-spin rounded-full border-2 border-current border-t-transparent opacity-70"
|
|
228
|
+
class="inline-block size-5 shrink-0 animate-spin rounded-full border-2 border-current border-t-transparent opacity-70 motion-reduce:animate-pulse"
|
|
122
229
|
></span>
|
|
123
230
|
</span>
|
|
124
231
|
{/if}
|
|
125
232
|
</div>
|
|
126
|
-
{#if
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
class=
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
</p>
|
|
233
|
+
{#if showCount}
|
|
234
|
+
<!-- The count ends the line the hint starts. Not a live region: it is read with the field. -->
|
|
235
|
+
<div class="flex items-start gap-3">
|
|
236
|
+
<div class="min-w-0 flex-1">
|
|
237
|
+
<FieldMessages fieldId={textareaId} {status} {hint} gap="mt-1" />
|
|
238
|
+
</div>
|
|
239
|
+
<p id={countId} class="mt-1 shrink-0 text-sm text-description tabular-nums" data-textarea-count>
|
|
240
|
+
{countLabel}
|
|
241
|
+
</p>
|
|
242
|
+
</div>
|
|
243
|
+
{:else}
|
|
244
|
+
<FieldMessages fieldId={textareaId} {status} {hint} gap="mt-1" />
|
|
139
245
|
{/if}
|
|
140
246
|
</div>
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
import type { HTMLTextareaAttributes } from 'svelte/elements';
|
|
1
2
|
import type { SemanticVariant, SizeVariant } from '../types/variants.js';
|
|
2
|
-
|
|
3
|
+
import { type TextareaCount } from "../util/field.js";
|
|
4
|
+
/**
|
|
5
|
+
* Other attributes (`autocomplete`, `inputmode`, `maxlength`,
|
|
6
|
+
* `enterkeyhint`, `data-*`, `onchange`, ...) land on the `<textarea>`.
|
|
7
|
+
*/
|
|
8
|
+
type Props = Omit<HTMLTextareaAttributes, 'class' | 'value' | 'id' | 'name' | 'placeholder' | 'required' | 'disabled' | 'rows' | 'oninput' | 'aria-describedby'> & {
|
|
3
9
|
id?: string;
|
|
4
10
|
value?: string;
|
|
5
11
|
name?: string;
|
|
@@ -11,11 +17,38 @@ interface Props {
|
|
|
11
17
|
disabled?: boolean;
|
|
12
18
|
loading?: boolean;
|
|
13
19
|
rows?: number;
|
|
20
|
+
/**
|
|
21
|
+
* The resize grip. `auto` (default): the browser's vertical grip with a mouse or
|
|
22
|
+
* trackpad, none on a touch screen, where dragging it scrolls the page. `vertical`
|
|
23
|
+
* shows it everywhere, `none` never. With `autogrow` there is no grip.
|
|
24
|
+
*/
|
|
25
|
+
resize?: 'auto' | 'none' | 'vertical';
|
|
26
|
+
/** The field grows with its content, from `rows` up to `maxRows`, and scrolls after that. */
|
|
27
|
+
autogrow?: boolean;
|
|
28
|
+
/** The tallest an `autogrow` field gets, in rows. Without it the field has no limit. */
|
|
29
|
+
maxRows?: number;
|
|
14
30
|
size?: SizeVariant;
|
|
15
31
|
variant?: SemanticVariant;
|
|
32
|
+
/** Status text under the field. Shown with a `success`, `warning` or `error` variant; for neutral help, use `hint`. */
|
|
16
33
|
message?: string;
|
|
34
|
+
/** Help text under the field, read out with it. */
|
|
35
|
+
hint?: string;
|
|
36
|
+
/** An error under the field. It marks the field invalid and is announced; it wins over `variant` and `message`. */
|
|
37
|
+
error?: string;
|
|
38
|
+
/**
|
|
39
|
+
* A character count under the field, at the end of the line the hint is on. It needs
|
|
40
|
+
* `maxlength`; without one nothing is shown. It is part of the field's description
|
|
41
|
+
* (`aria-describedby`) and is not a live region, so it is not read out at every key.
|
|
42
|
+
*/
|
|
43
|
+
count?: boolean;
|
|
44
|
+
/** The count is shown from this many characters on. `0` (default): always. */
|
|
45
|
+
countFrom?: number;
|
|
46
|
+
/** The words of the count, for the page's language. Default: "12 / 2000". */
|
|
47
|
+
countText?: (count: TextareaCount) => string;
|
|
17
48
|
oninput?: (event: Event) => void;
|
|
18
|
-
|
|
49
|
+
/** Ids of other elements that describe the field. The hint and the message are added after them. */
|
|
50
|
+
"aria-describedby"?: string | null;
|
|
51
|
+
};
|
|
19
52
|
declare const Textarea: import("svelte").Component<Props, {}, "value">;
|
|
20
53
|
type Textarea = ReturnType<typeof Textarea>;
|
|
21
54
|
export default Textarea;
|
|
@@ -1,88 +1,251 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { mergeStrings } from "../util/ready-made-strings.js";
|
|
3
|
+
import { zabiStringsFor } from "../util/zabi-strings.js";
|
|
4
|
+
import { onMount, untrack } from "svelte";
|
|
5
|
+
import type { HTMLButtonAttributes } from "svelte/elements";
|
|
6
|
+
import Sun from "@lucide/svelte/icons/sun";
|
|
7
|
+
import Moon from "@lucide/svelte/icons/moon";
|
|
8
|
+
import Monitor from "@lucide/svelte/icons/monitor";
|
|
4
9
|
import { FOCUS_BRAND_CLASS } from "../util/focus-utils.js";
|
|
5
10
|
import { cn } from "../util/cn.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_THEME_STORAGE_KEY,
|
|
13
|
+
THEME_MODES,
|
|
14
|
+
getStoredThemeMode,
|
|
15
|
+
getThemeMode,
|
|
16
|
+
isThemeDark,
|
|
17
|
+
isThemeMode,
|
|
18
|
+
setThemeMode,
|
|
19
|
+
storeThemeMode,
|
|
20
|
+
type ThemeMode,
|
|
21
|
+
type ThemeToggleLabels,
|
|
22
|
+
} from "../util/theme-mode.js";
|
|
14
23
|
|
|
15
|
-
|
|
24
|
+
/** Other attributes (`data-*`, `aria-*`, `id`, ...) land on the `<button>`. */
|
|
25
|
+
type Props = Omit<
|
|
26
|
+
HTMLButtonAttributes,
|
|
27
|
+
"class" | "disabled" | "type" | "onclick" | "children"
|
|
28
|
+
> & {
|
|
16
29
|
/** Extra classes for the host element. */
|
|
17
30
|
class?: string;
|
|
18
31
|
size?: "sm" | "md" | "lg";
|
|
19
32
|
variant?: "default" | "ghost" | "outline";
|
|
20
33
|
disabled?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* `"two"` (the default) flips light and dark. `"three"` steps through
|
|
36
|
+
* system, light and dark, and always writes `data-theme` on `<html>`.
|
|
37
|
+
*/
|
|
38
|
+
modes?: "two" | "three";
|
|
39
|
+
/**
|
|
40
|
+
* The page's mode: `"auto"` (follow the system), `"light"` or `"dark"`.
|
|
41
|
+
* Bindable. It is read from `<html>` when the button mounts and follows
|
|
42
|
+
* the page from then on; assigning it switches the page.
|
|
43
|
+
*/
|
|
44
|
+
mode?: ThemeMode;
|
|
45
|
+
/** Called with the new mode when a press changes it. */
|
|
46
|
+
onmodechange?: (mode: ThemeMode) => void;
|
|
47
|
+
/**
|
|
48
|
+
* The `localStorage` key the choice is kept under and restored from when
|
|
49
|
+
* the button mounts. `null` keeps nothing.
|
|
50
|
+
*/
|
|
51
|
+
storageKey?: string | null;
|
|
52
|
+
/** Replaces the texts of the accessible name, for another language. */
|
|
53
|
+
labels?: Partial<ThemeToggleLabels>;
|
|
21
54
|
onclick?: (event: Event) => void;
|
|
22
|
-
}
|
|
55
|
+
};
|
|
23
56
|
|
|
24
57
|
let {
|
|
25
58
|
class: className = "",
|
|
26
59
|
size = "md",
|
|
27
60
|
variant = "default",
|
|
28
61
|
disabled = false,
|
|
62
|
+
modes = "two",
|
|
63
|
+
mode = $bindable(),
|
|
64
|
+
onmodechange,
|
|
65
|
+
storageKey = DEFAULT_THEME_STORAGE_KEY,
|
|
66
|
+
labels,
|
|
29
67
|
onclick,
|
|
30
68
|
...restProps
|
|
31
69
|
}: Props = $props();
|
|
32
70
|
|
|
71
|
+
const DEFAULT_LABELS: ThemeToggleLabels = {
|
|
72
|
+
auto: "system",
|
|
73
|
+
light: "light",
|
|
74
|
+
dark: "dark",
|
|
75
|
+
describe: (current, next) => `Theme: ${current}. Switch to ${next}`,
|
|
76
|
+
darkMode: "Dark mode",
|
|
77
|
+
beforeMount: "Theme toggle",
|
|
78
|
+
};
|
|
79
|
+
/** The app-wide words for this component, from a `ZabiStringsProvider` above it, if there is one. */
|
|
80
|
+
const provided = zabiStringsFor("themeToggle");
|
|
81
|
+
const text = $derived(mergeStrings(DEFAULT_LABELS, provided(), labels));
|
|
82
|
+
|
|
83
|
+
/** Whether the page is dark right now, whichever way it is switched. */
|
|
33
84
|
let isDark = $state(false);
|
|
85
|
+
/** The page's mode as last read from `<html>`. */
|
|
86
|
+
let pageMode = $state<ThemeMode>("light");
|
|
34
87
|
let mounted = $state(false);
|
|
35
88
|
|
|
89
|
+
const SYSTEM_DARK = "(prefers-color-scheme: dark)";
|
|
90
|
+
|
|
91
|
+
/** A page that carries `data-theme` on `<html>` is switched through it; any other page through the class. */
|
|
92
|
+
function usesAttribute(root: HTMLElement): boolean {
|
|
93
|
+
return root.hasAttribute("data-theme");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Reads the page into the button. The only place `mode` is set from the page. */
|
|
97
|
+
function readPage() {
|
|
98
|
+
isDark = isThemeDark();
|
|
99
|
+
pageMode = getThemeMode();
|
|
100
|
+
mode = pageMode;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Puts the page in `next`. With three modes that is always `data-theme`.
|
|
105
|
+
* With two, "auto" can only be said with the attribute, and light or dark
|
|
106
|
+
* go to whichever mechanism the page uses, as they always have.
|
|
107
|
+
*/
|
|
108
|
+
function applyMode(next: ThemeMode) {
|
|
109
|
+
if (modes === "three" || next === "auto") {
|
|
110
|
+
setThemeMode(next, { storageKey });
|
|
111
|
+
} else {
|
|
112
|
+
isDark = next === "dark";
|
|
113
|
+
updateTheme();
|
|
114
|
+
storeThemeMode(next, storageKey);
|
|
115
|
+
}
|
|
116
|
+
readPage();
|
|
117
|
+
}
|
|
118
|
+
|
|
36
119
|
onMount(() => {
|
|
37
120
|
mounted = true;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
121
|
+
if (typeof document === "undefined") return;
|
|
122
|
+
const root = document.documentElement;
|
|
123
|
+
|
|
124
|
+
clearInlineScheme(root);
|
|
125
|
+
|
|
126
|
+
// The choice made last time, unless a script in <head> has applied it
|
|
127
|
+
// already (`themeInitScript`) or the app passed a mode of its own.
|
|
128
|
+
const asked = isThemeMode(mode) ? mode : getStoredThemeMode(storageKey);
|
|
129
|
+
if (asked && asked !== getThemeMode() && (modes === "three" || asked !== "auto" || usesAttribute(root))) {
|
|
130
|
+
applyMode(asked);
|
|
131
|
+
} else {
|
|
132
|
+
readPage();
|
|
41
133
|
}
|
|
42
134
|
|
|
43
|
-
|
|
135
|
+
// The page can be switched by something else (another control bound to
|
|
136
|
+
// the same helpers, the app's own script): the icon and the name follow.
|
|
137
|
+
const observer = typeof MutationObserver === "undefined" ? undefined : new MutationObserver(readPage);
|
|
138
|
+
observer?.observe(root, { attributes: true, attributeFilter: ["class", "data-theme"] });
|
|
44
139
|
|
|
45
|
-
const media = window.matchMedia(
|
|
140
|
+
const media = window.matchMedia?.(SYSTEM_DARK);
|
|
46
141
|
const handleSystemChange = (event: MediaQueryListEvent) => {
|
|
47
|
-
|
|
48
|
-
|
|
142
|
+
if (modes === "three" || usesAttribute(root)) {
|
|
143
|
+
// `data-theme="auto"` follows the system in the stylesheet, and
|
|
144
|
+
// "light" or "dark" is a choice: either way only the reading changes.
|
|
145
|
+
readPage();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (getStoredThemeMode(storageKey)) return;
|
|
49
149
|
isDark = event.matches;
|
|
50
150
|
updateTheme();
|
|
151
|
+
readPage();
|
|
152
|
+
};
|
|
153
|
+
media?.addEventListener("change", handleSystemChange);
|
|
154
|
+
|
|
155
|
+
return () => {
|
|
156
|
+
observer?.disconnect();
|
|
157
|
+
media?.removeEventListener("change", handleSystemChange);
|
|
51
158
|
};
|
|
159
|
+
});
|
|
52
160
|
|
|
53
|
-
|
|
54
|
-
|
|
161
|
+
// `mode` assigned from outside (a bound value, the app's own control).
|
|
162
|
+
$effect(() => {
|
|
163
|
+
const wanted = mode;
|
|
164
|
+
if (!mounted || !isThemeMode(wanted)) return;
|
|
165
|
+
untrack(() => {
|
|
166
|
+
if (wanted !== pageMode) applyMode(wanted);
|
|
167
|
+
});
|
|
55
168
|
});
|
|
56
169
|
|
|
170
|
+
/** What a press leads to. */
|
|
171
|
+
const nextMode = $derived<ThemeMode>(
|
|
172
|
+
modes === "three"
|
|
173
|
+
? THEME_MODES[(THEME_MODES.indexOf(pageMode) + 1) % THEME_MODES.length]
|
|
174
|
+
: isDark
|
|
175
|
+
? "light"
|
|
176
|
+
: "dark",
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Two modes: a switch with one name, "Dark mode", and `aria-pressed` for its
|
|
181
|
+
* state. The name used to flip as well ("Switch to dark mode", then "Switch
|
|
182
|
+
* to light mode, pressed"), and "pressed" on "Switch to light mode" says
|
|
183
|
+
* two things at once. Three modes are steps, not on and off, so there the
|
|
184
|
+
* name carries the state and there is no `aria-pressed`.
|
|
185
|
+
*/
|
|
186
|
+
const accessibleName = $derived(modes === "three" ? text.describe(text[pageMode], text[nextMode]) : text.darkMode);
|
|
187
|
+
|
|
57
188
|
function toggleTheme(event: Event) {
|
|
58
189
|
if (disabled) return;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const storage = safeLocalStorage();
|
|
62
|
-
if (mounted && storage) {
|
|
63
|
-
storage.setItem("theme", isDark ? "dark" : "light");
|
|
64
|
-
}
|
|
65
|
-
|
|
190
|
+
applyMode(nextMode);
|
|
191
|
+
onmodechange?.(pageMode);
|
|
66
192
|
onclick?.(event);
|
|
67
193
|
}
|
|
68
194
|
|
|
195
|
+
/** Two modes, light or dark: the attribute where the page has it, the class otherwise. */
|
|
69
196
|
function updateTheme() {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
197
|
+
if (!mounted || typeof document === "undefined") return;
|
|
198
|
+
const root = document.documentElement;
|
|
199
|
+
if (usesAttribute(root)) {
|
|
200
|
+
// An explicit choice, also when the page was on "auto". The theme
|
|
201
|
+
// sets `color-scheme` for each value, so no inline style is written.
|
|
202
|
+
root.setAttribute("data-theme", isDark ? "dark" : "light");
|
|
203
|
+
// A leftover class would keep the page dark under data-theme="light".
|
|
204
|
+
if (!isDark) root.classList.remove("dark");
|
|
205
|
+
return;
|
|
79
206
|
}
|
|
207
|
+
// The class only. The theme sets `color-scheme` for it (dark under
|
|
208
|
+
// `.dark`, light on the root otherwise), so no inline value is written:
|
|
209
|
+
// one left behind would outrank the stylesheet, and the next time the
|
|
210
|
+
// app added or removed the class itself the tokens would change and
|
|
211
|
+
// the native controls would not.
|
|
212
|
+
root.classList.toggle("dark", isDark);
|
|
213
|
+
clearInlineScheme(root);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Removes an inline `color-scheme` of "light" or "dark" from `<html>`: what
|
|
218
|
+
* this button wrote before the theme set the scheme itself, and what a
|
|
219
|
+
* start-up script copied from the old docs writes. Any other inline value
|
|
220
|
+
* is the app's own and is left alone.
|
|
221
|
+
*/
|
|
222
|
+
function clearInlineScheme(root: HTMLElement) {
|
|
223
|
+
const inline = root.style.colorScheme;
|
|
224
|
+
if (inline === "light" || inline === "dark") root.style.removeProperty("color-scheme");
|
|
80
225
|
}
|
|
81
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Before mount every icon is rendered and CSS shows one, by the selectors
|
|
229
|
+
* the theme itself is switched with. Written out in full, because Tailwind
|
|
230
|
+
* only generates a class it can read in the source.
|
|
231
|
+
*
|
|
232
|
+
* Two modes: sun on a light page, moon on a dark one (the class,
|
|
233
|
+
* `data-theme="dark"`, or `data-theme="auto"` on a dark system).
|
|
234
|
+
* Three modes show the mode, not the result: the monitor for
|
|
235
|
+
* `data-theme="auto"` whatever the system is.
|
|
236
|
+
*/
|
|
237
|
+
const SUN_BEFORE_MOUNT =
|
|
238
|
+
"[.dark_&]:hidden [[data-theme=dark]_&]:hidden [@media(prefers-color-scheme:dark)]:[[data-theme=auto]_&]:hidden";
|
|
239
|
+
const MOON_BEFORE_MOUNT =
|
|
240
|
+
"hidden [.dark_&]:block [[data-theme=dark]_&]:block [@media(prefers-color-scheme:dark)]:[[data-theme=auto]_&]:block";
|
|
241
|
+
const SUN_BEFORE_MOUNT_THREE = "[.dark_&]:hidden [[data-theme=dark]_&]:hidden [[data-theme=auto]_&]:hidden";
|
|
242
|
+
const MOON_BEFORE_MOUNT_THREE = "hidden [.dark_&]:block [[data-theme=dark]_&]:block";
|
|
243
|
+
const SYSTEM_BEFORE_MOUNT_THREE = "hidden [[data-theme=auto]_&]:block";
|
|
244
|
+
|
|
82
245
|
const sizeClass = $derived(() => {
|
|
83
246
|
if (size === "sm") {
|
|
84
247
|
return {
|
|
85
|
-
button: "w-8 h-8",
|
|
248
|
+
button: "w-8 h-8 pointer-coarse:min-h-11 pointer-coarse:min-w-11",
|
|
86
249
|
icon: 16
|
|
87
250
|
};
|
|
88
251
|
} else if (size === "lg") {
|
|
@@ -92,7 +255,7 @@
|
|
|
92
255
|
};
|
|
93
256
|
} else {
|
|
94
257
|
return {
|
|
95
|
-
button: "w-10 h-10",
|
|
258
|
+
button: "w-10 h-10 pointer-coarse:min-h-11 pointer-coarse:min-w-11",
|
|
96
259
|
icon: 20
|
|
97
260
|
};
|
|
98
261
|
}
|
|
@@ -135,13 +298,16 @@
|
|
|
135
298
|
<button
|
|
136
299
|
onclick={toggleTheme}
|
|
137
300
|
class="{buttonClasses()} {className}"
|
|
138
|
-
aria-label={
|
|
139
|
-
aria-pressed={isDark}
|
|
301
|
+
aria-label={accessibleName}
|
|
302
|
+
aria-pressed={modes === "three" ? undefined : isDark}
|
|
303
|
+
data-theme-mode={modes === "three" ? pageMode : undefined}
|
|
140
304
|
type="button"
|
|
141
305
|
{disabled}
|
|
142
306
|
{...restProps}
|
|
143
307
|
>
|
|
144
|
-
{#if
|
|
308
|
+
{#if modes === "three" && pageMode === "auto"}
|
|
309
|
+
<Monitor size={sizeClass().icon} class="text-label" />
|
|
310
|
+
{:else if modes === "three" ? pageMode === "dark" : isDark}
|
|
145
311
|
<Moon size={sizeClass().icon} class="text-label" />
|
|
146
312
|
{:else}
|
|
147
313
|
<Sun size={sizeClass().icon} class="text-label" />
|
|
@@ -150,11 +316,24 @@
|
|
|
150
316
|
{:else}
|
|
151
317
|
<button
|
|
152
318
|
class="{sizeClass().button} {variantClass()} rounded-control flex items-center justify-center text-label cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
|
153
|
-
aria-label=
|
|
319
|
+
aria-label={text.beforeMount}
|
|
154
320
|
type="button"
|
|
155
321
|
{disabled}
|
|
156
322
|
{...restProps}
|
|
157
323
|
>
|
|
158
|
-
|
|
324
|
+
<!-- Before mount there is no state to read: the server cannot know the
|
|
325
|
+
theme, and hydration has not run yet. Every icon is in the markup and
|
|
326
|
+
the stylesheet picks one, so a dark page never shows the Sun first:
|
|
327
|
+
by the `dark` class, by `data-theme="dark"`, or by `data-theme="auto"`
|
|
328
|
+
(the moon on a dark system with two modes, the monitor with three).
|
|
329
|
+
The icon matches the mounted button. -->
|
|
330
|
+
{#if modes === "three"}
|
|
331
|
+
<Sun size={sizeClass().icon} class="text-label {SUN_BEFORE_MOUNT_THREE}" />
|
|
332
|
+
<Moon size={sizeClass().icon} class="text-label {MOON_BEFORE_MOUNT_THREE}" />
|
|
333
|
+
<Monitor size={sizeClass().icon} class="text-label {SYSTEM_BEFORE_MOUNT_THREE}" />
|
|
334
|
+
{:else}
|
|
335
|
+
<Sun size={sizeClass().icon} class="text-label {SUN_BEFORE_MOUNT}" />
|
|
336
|
+
<Moon size={sizeClass().icon} class="text-label {MOON_BEFORE_MOUNT}" />
|
|
337
|
+
{/if}
|
|
159
338
|
</button>
|
|
160
339
|
{/if}
|