zabi-components 8.0.0 → 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 +243 -10
- package/THEME.md +149 -332
- package/THEMING.md +1192 -266
- package/dist/atoms/ActionPanel.svelte +15 -5
- package/dist/atoms/Avatar.svelte +104 -0
- package/dist/atoms/Avatar.svelte.d.ts +34 -0
- package/dist/atoms/Badge.svelte +36 -7
- package/dist/atoms/Badge.svelte.d.ts +2 -2
- package/dist/atoms/Button.svelte +177 -27
- package/dist/atoms/Button.svelte.d.ts +30 -2
- package/dist/atoms/Card.svelte +4 -4
- package/dist/atoms/CardHeader.svelte +16 -4
- package/dist/atoms/Checkbox.svelte +48 -21
- package/dist/atoms/Checkbox.svelte.d.ts +4 -2
- package/dist/atoms/CodeBlock.svelte +20 -2
- package/dist/atoms/CodeBlock.svelte.d.ts +4 -0
- package/dist/atoms/ColorPicker.svelte +259 -49
- package/dist/atoms/ColorPicker.svelte.d.ts +3 -0
- package/dist/atoms/Container.svelte +16 -8
- package/dist/atoms/DateField.svelte +107 -0
- package/dist/atoms/DateField.svelte.d.ts +53 -0
- 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 +37 -5
- package/dist/atoms/Heading.svelte.d.ts +7 -0
- package/dist/atoms/IconButton.svelte +220 -24
- package/dist/atoms/IconButton.svelte.d.ts +49 -4
- package/dist/atoms/Input.svelte +253 -52
- package/dist/atoms/Input.svelte.d.ts +32 -2
- package/dist/atoms/List.svelte +4 -1
- package/dist/atoms/ListItem.svelte +44 -11
- package/dist/atoms/ListItem.svelte.d.ts +5 -0
- package/dist/atoms/Progress.svelte +27 -8
- package/dist/atoms/Progress.svelte.d.ts +4 -0
- package/dist/atoms/Radio.svelte +19 -3
- package/dist/atoms/Radio.svelte.d.ts +4 -2
- package/dist/atoms/Rating.svelte +494 -0
- package/dist/atoms/Rating.svelte.d.ts +56 -0
- package/dist/atoms/Select.svelte +433 -91
- package/dist/atoms/Select.svelte.d.ts +49 -3
- package/dist/atoms/SelectionControl.svelte +94 -17
- package/dist/atoms/SelectionControl.svelte.d.ts +4 -3
- package/dist/atoms/Skeleton.svelte +1 -1
- 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 +151 -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 +23 -11
- package/dist/atoms/Text.svelte.d.ts +6 -3
- package/dist/atoms/Textarea.svelte +55 -44
- package/dist/atoms/Textarea.svelte.d.ts +14 -2
- package/dist/atoms/ThemeToggle.svelte +223 -44
- package/dist/atoms/ThemeToggle.svelte.d.ts +26 -3
- package/dist/atoms/TimeField.svelte +108 -0
- package/dist/atoms/TimeField.svelte.d.ts +54 -0
- package/dist/atoms/Toast.svelte +17 -4
- package/dist/atoms/Toast.svelte.d.ts +2 -0
- package/dist/atoms/Toggle.svelte +41 -8
- package/dist/atoms/Toggle.svelte.d.ts +11 -2
- package/dist/atoms/Tooltip.svelte +508 -23
- package/dist/atoms/Tooltip.svelte.d.ts +11 -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 +80 -2
- 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 +5 -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/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/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 +80 -2
- package/dist/index.js +57 -2
- package/dist/molecules/Alert.svelte +33 -5
- package/dist/molecules/Alert.svelte.d.ts +2 -0
- 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 +16 -4
- package/dist/molecules/ComponentDemo.svelte.d.ts +3 -0
- package/dist/molecules/ConfirmDialog.svelte +253 -0
- package/dist/molecules/ConfirmDialog.svelte.d.ts +55 -0
- package/dist/molecules/ContactForm.svelte +34 -17
- package/dist/molecules/ContactForm.svelte.d.ts +8 -0
- package/dist/molecules/Drawer.svelte +394 -0
- package/dist/molecules/Drawer.svelte.d.ts +55 -0
- package/dist/molecules/Dropdown.svelte +509 -48
- package/dist/molecules/Dropdown.svelte.d.ts +45 -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 +383 -70
- package/dist/molecules/ImageUpload.svelte.d.ts +47 -2
- package/dist/molecules/MediaGrid.svelte +527 -0
- package/dist/molecules/MediaGrid.svelte.d.ts +77 -0
- package/dist/molecules/Modal.svelte +288 -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 +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 +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 +52 -14
- package/dist/molecules/RadioGroup.svelte.d.ts +14 -3
- package/dist/molecules/Section.svelte +1 -1
- package/dist/molecules/SegmentedControl.svelte +323 -0
- package/dist/molecules/SegmentedControl.svelte.d.ts +34 -0
- package/dist/molecules/SidebarBrandHeader.svelte +11 -1
- package/dist/molecules/SidebarBrandHeader.svelte.d.ts +3 -0
- package/dist/molecules/SidebarFooter.svelte +29 -6
- package/dist/molecules/SidebarFooter.svelte.d.ts +3 -0
- package/dist/molecules/SlideUp.svelte +220 -61
- package/dist/molecules/SlideUp.svelte.d.ts +26 -0
- 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 +233 -9
- package/dist/molecules/Tabs.svelte.d.ts +7 -0
- package/dist/molecules/Toaster.svelte +350 -5
- package/dist/molecules/Toaster.svelte.d.ts +95 -2
- package/dist/molecules/ToasterToast.svelte +253 -67
- package/dist/molecules/ToasterToast.svelte.d.ts +9 -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 +107 -12
- package/dist/organisms/SidebarAccountPanel.svelte.d.ts +19 -0
- package/dist/organisms/SidebarNavigation.svelte +96 -15
- package/dist/organisms/SidebarNavigation.svelte.d.ts +29 -1
- package/dist/organisms/SidebarPanel.svelte +31 -10
- package/dist/organisms/SidebarShell.svelte +162 -11
- package/dist/organisms/SidebarShell.svelte.d.ts +56 -2
- package/dist/organisms/TopNavbar.svelte +256 -31
- package/dist/organisms/TopNavbar.svelte.d.ts +26 -0
- package/dist/organisms/index.d.ts +5 -0
- package/dist/organisms/index.js +3 -0
- package/dist/types/components.d.ts +4 -0
- package/dist/types/components.d.ts.map +1 -1
- package/dist/types/index.d.ts +605 -25
- package/dist/types/index.d.ts.map +1 -1
- 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/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/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 +543 -241
- package/dist/zabi-components-theme-dark.css +543 -241
- package/dist/zabi-components-theme-only.css +1584 -367
- package/dist/zabi-components-theme.css +1584 -367
- package/dist/zabi-components.css +3885 -518
- package/docs/lucide-icons.md +40 -17
- package/docs/theme-imports.md +81 -11
- package/package.json +35 -8
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import type { HTMLTextareaAttributes } from 'svelte/elements';
|
|
1
2
|
import type { SemanticVariant, SizeVariant } from '../types/variants.js';
|
|
2
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Other attributes (`autocomplete`, `inputmode`, `maxlength`,
|
|
5
|
+
* `enterkeyhint`, `data-*`, `onchange`, ...) land on the `<textarea>`.
|
|
6
|
+
*/
|
|
7
|
+
type Props = Omit<HTMLTextareaAttributes, 'class' | 'value' | 'id' | 'name' | 'placeholder' | 'required' | 'disabled' | 'rows' | 'oninput' | 'aria-describedby'> & {
|
|
3
8
|
id?: string;
|
|
4
9
|
value?: string;
|
|
5
10
|
name?: string;
|
|
@@ -13,9 +18,16 @@ interface Props {
|
|
|
13
18
|
rows?: number;
|
|
14
19
|
size?: SizeVariant;
|
|
15
20
|
variant?: SemanticVariant;
|
|
21
|
+
/** Status text under the field. Shown with a `success`, `warning` or `error` variant; for neutral help, use `hint`. */
|
|
16
22
|
message?: string;
|
|
23
|
+
/** Help text under the field, read out with it. */
|
|
24
|
+
hint?: string;
|
|
25
|
+
/** An error under the field. It marks the field invalid and is announced; it wins over `variant` and `message`. */
|
|
26
|
+
error?: string;
|
|
17
27
|
oninput?: (event: Event) => void;
|
|
18
|
-
|
|
28
|
+
/** Ids of other elements that describe the field. The hint and the message are added after them. */
|
|
29
|
+
"aria-describedby"?: string | null;
|
|
30
|
+
};
|
|
19
31
|
declare const Textarea: import("svelte").Component<Props, {}, "value">;
|
|
20
32
|
type Textarea = ReturnType<typeof Textarea>;
|
|
21
33
|
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}
|
|
@@ -1,11 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
import type { HTMLButtonAttributes } from "svelte/elements";
|
|
2
|
+
import { type ThemeMode, type ThemeToggleLabels } from "../util/theme-mode.js";
|
|
3
|
+
/** Other attributes (`data-*`, `aria-*`, `id`, ...) land on the `<button>`. */
|
|
4
|
+
type Props = Omit<HTMLButtonAttributes, "class" | "disabled" | "type" | "onclick" | "children"> & {
|
|
2
5
|
/** Extra classes for the host element. */
|
|
3
6
|
class?: string;
|
|
4
7
|
size?: "sm" | "md" | "lg";
|
|
5
8
|
variant?: "default" | "ghost" | "outline";
|
|
6
9
|
disabled?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* `"two"` (the default) flips light and dark. `"three"` steps through
|
|
12
|
+
* system, light and dark, and always writes `data-theme` on `<html>`.
|
|
13
|
+
*/
|
|
14
|
+
modes?: "two" | "three";
|
|
15
|
+
/**
|
|
16
|
+
* The page's mode: `"auto"` (follow the system), `"light"` or `"dark"`.
|
|
17
|
+
* Bindable. It is read from `<html>` when the button mounts and follows
|
|
18
|
+
* the page from then on; assigning it switches the page.
|
|
19
|
+
*/
|
|
20
|
+
mode?: ThemeMode;
|
|
21
|
+
/** Called with the new mode when a press changes it. */
|
|
22
|
+
onmodechange?: (mode: ThemeMode) => void;
|
|
23
|
+
/**
|
|
24
|
+
* The `localStorage` key the choice is kept under and restored from when
|
|
25
|
+
* the button mounts. `null` keeps nothing.
|
|
26
|
+
*/
|
|
27
|
+
storageKey?: string | null;
|
|
28
|
+
/** Replaces the texts of the accessible name, for another language. */
|
|
29
|
+
labels?: Partial<ThemeToggleLabels>;
|
|
7
30
|
onclick?: (event: Event) => void;
|
|
8
|
-
}
|
|
9
|
-
declare const ThemeToggle: import("svelte").Component<Props, {}, "">;
|
|
31
|
+
};
|
|
32
|
+
declare const ThemeToggle: import("svelte").Component<Props, {}, "mode">;
|
|
10
33
|
type ThemeToggle = ReturnType<typeof ThemeToggle>;
|
|
11
34
|
export default ThemeToggle;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLInputAttributes } from "svelte/elements";
|
|
3
|
+
import type { SizeVariant } from "../types/variants.js";
|
|
4
|
+
import TemporalField from "./TemporalField.svelte";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A time of day, picked with the device's own time picker and styled
|
|
8
|
+
* like Input.
|
|
9
|
+
*
|
|
10
|
+
* It is a native `<input type="time">`. `value` is always 24-hour
|
|
11
|
+
* `HH:mm` (`HH:mm:ss` when `step` asks for seconds), or `""` while empty,
|
|
12
|
+
* whatever the field shows.
|
|
13
|
+
*
|
|
14
|
+
* What the field shows is the browser's decision: 24-hour or 12-hour
|
|
15
|
+
* with AM and PM, by the settings of the browser or device, not of the
|
|
16
|
+
* page. To show a time as text in the page's language, use `formatTime`:
|
|
17
|
+
*
|
|
18
|
+
* ```svelte
|
|
19
|
+
* <TimeField label="Starts" bind:value={time} step={300} />
|
|
20
|
+
* <p>{formatTime(time, "sv")}</p> <!-- 19:00 -->
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Other attributes (`autocomplete`, `data-*`, `onchange`, ...) land on the
|
|
24
|
+
* `<input>`.
|
|
25
|
+
*/
|
|
26
|
+
type Props = Omit<
|
|
27
|
+
HTMLInputAttributes,
|
|
28
|
+
"class" | "type" | "value" | "size" | "min" | "max" | "step" | "required" | "disabled" | "readonly" | "id" | "name"
|
|
29
|
+
> & {
|
|
30
|
+
/** Omit to auto-generate; pair with FormField's `id` when used inside FormField. */
|
|
31
|
+
id?: string;
|
|
32
|
+
/** The time as 24-hour `HH:mm`, or `""` while empty. Supports `bind:value`. */
|
|
33
|
+
value?: string;
|
|
34
|
+
/** Name the value is submitted under in a form. */
|
|
35
|
+
name?: string;
|
|
36
|
+
label?: string;
|
|
37
|
+
/** True when FormField supplies the visible `<label>`. */
|
|
38
|
+
hideLabel?: boolean;
|
|
39
|
+
/** Help text under the field, read out with it. */
|
|
40
|
+
hint?: string;
|
|
41
|
+
/** An error under the field. It marks the field invalid and is announced. */
|
|
42
|
+
error?: string;
|
|
43
|
+
/** Earliest time that can be picked, as `HH:mm`. */
|
|
44
|
+
min?: string;
|
|
45
|
+
/** Latest time that can be picked, as `HH:mm`. */
|
|
46
|
+
max?: string;
|
|
47
|
+
/** Steps between times that can be picked, in seconds: 300 for five minutes. */
|
|
48
|
+
step?: number | "any";
|
|
49
|
+
required?: boolean;
|
|
50
|
+
disabled?: boolean;
|
|
51
|
+
/** Shows the value without letting it be changed; it is still submitted. */
|
|
52
|
+
readonly?: boolean;
|
|
53
|
+
/** Height, on the scale Input and Button use. At least 44px on a touch screen. */
|
|
54
|
+
size?: SizeVariant;
|
|
55
|
+
/** Extra classes for the `<input>`. */
|
|
56
|
+
class?: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
let {
|
|
60
|
+
id,
|
|
61
|
+
value = $bindable<Exclude<Props["value"], undefined>>(),
|
|
62
|
+
name = "",
|
|
63
|
+
label = "",
|
|
64
|
+
hideLabel = false,
|
|
65
|
+
hint = "",
|
|
66
|
+
error = "",
|
|
67
|
+
min,
|
|
68
|
+
max,
|
|
69
|
+
step,
|
|
70
|
+
required = false,
|
|
71
|
+
disabled = false,
|
|
72
|
+
readonly = false,
|
|
73
|
+
size = "md",
|
|
74
|
+
class: className = "",
|
|
75
|
+
...restProps
|
|
76
|
+
}: Props = $props();
|
|
77
|
+
|
|
78
|
+
// No fallback on a bindable prop: Svelte refuses `bind:…={undefined}` on
|
|
79
|
+
// one that has a fallback (`props_invalid_value`), and a page that throws
|
|
80
|
+
// while it hydrates never becomes interactive. The default is applied
|
|
81
|
+
// here instead: at once, for the server and the first render, and again
|
|
82
|
+
// whenever a parent hands back `undefined`.
|
|
83
|
+
const applyDefaults = () => {
|
|
84
|
+
if (value === undefined) value = "";
|
|
85
|
+
};
|
|
86
|
+
applyDefaults();
|
|
87
|
+
$effect.pre(applyDefaults);
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<TemporalField
|
|
91
|
+
kind="time"
|
|
92
|
+
{id}
|
|
93
|
+
bind:value
|
|
94
|
+
{name}
|
|
95
|
+
{label}
|
|
96
|
+
{hideLabel}
|
|
97
|
+
{hint}
|
|
98
|
+
{error}
|
|
99
|
+
{min}
|
|
100
|
+
{max}
|
|
101
|
+
{step}
|
|
102
|
+
{required}
|
|
103
|
+
{disabled}
|
|
104
|
+
{readonly}
|
|
105
|
+
{size}
|
|
106
|
+
class={className}
|
|
107
|
+
{...restProps}
|
|
108
|
+
/>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { HTMLInputAttributes } from "svelte/elements";
|
|
2
|
+
import type { SizeVariant } from "../types/variants.js";
|
|
3
|
+
/**
|
|
4
|
+
* A time of day, picked with the device's own time picker and styled
|
|
5
|
+
* like Input.
|
|
6
|
+
*
|
|
7
|
+
* It is a native `<input type="time">`. `value` is always 24-hour
|
|
8
|
+
* `HH:mm` (`HH:mm:ss` when `step` asks for seconds), or `""` while empty,
|
|
9
|
+
* whatever the field shows.
|
|
10
|
+
*
|
|
11
|
+
* What the field shows is the browser's decision: 24-hour or 12-hour
|
|
12
|
+
* with AM and PM, by the settings of the browser or device, not of the
|
|
13
|
+
* page. To show a time as text in the page's language, use `formatTime`:
|
|
14
|
+
*
|
|
15
|
+
* ```svelte
|
|
16
|
+
* <TimeField label="Starts" bind:value={time} step={300} />
|
|
17
|
+
* <p>{formatTime(time, "sv")}</p> <!-- 19:00 -->
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Other attributes (`autocomplete`, `data-*`, `onchange`, ...) land on the
|
|
21
|
+
* `<input>`.
|
|
22
|
+
*/
|
|
23
|
+
type Props = Omit<HTMLInputAttributes, "class" | "type" | "value" | "size" | "min" | "max" | "step" | "required" | "disabled" | "readonly" | "id" | "name"> & {
|
|
24
|
+
/** Omit to auto-generate; pair with FormField's `id` when used inside FormField. */
|
|
25
|
+
id?: string;
|
|
26
|
+
/** The time as 24-hour `HH:mm`, or `""` while empty. Supports `bind:value`. */
|
|
27
|
+
value?: string;
|
|
28
|
+
/** Name the value is submitted under in a form. */
|
|
29
|
+
name?: string;
|
|
30
|
+
label?: string;
|
|
31
|
+
/** True when FormField supplies the visible `<label>`. */
|
|
32
|
+
hideLabel?: boolean;
|
|
33
|
+
/** Help text under the field, read out with it. */
|
|
34
|
+
hint?: string;
|
|
35
|
+
/** An error under the field. It marks the field invalid and is announced. */
|
|
36
|
+
error?: string;
|
|
37
|
+
/** Earliest time that can be picked, as `HH:mm`. */
|
|
38
|
+
min?: string;
|
|
39
|
+
/** Latest time that can be picked, as `HH:mm`. */
|
|
40
|
+
max?: string;
|
|
41
|
+
/** Steps between times that can be picked, in seconds: 300 for five minutes. */
|
|
42
|
+
step?: number | "any";
|
|
43
|
+
required?: boolean;
|
|
44
|
+
disabled?: boolean;
|
|
45
|
+
/** Shows the value without letting it be changed; it is still submitted. */
|
|
46
|
+
readonly?: boolean;
|
|
47
|
+
/** Height, on the scale Input and Button use. At least 44px on a touch screen. */
|
|
48
|
+
size?: SizeVariant;
|
|
49
|
+
/** Extra classes for the `<input>`. */
|
|
50
|
+
class?: string;
|
|
51
|
+
};
|
|
52
|
+
declare const TimeField: import("svelte").Component<Props, {}, "value">;
|
|
53
|
+
type TimeField = ReturnType<typeof TimeField>;
|
|
54
|
+
export default TimeField;
|
package/dist/atoms/Toast.svelte
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { DEFAULT_TOAST_TEXTS, zabiStringsFor } from "../util/zabi-strings.js";
|
|
2
3
|
import { cn } from "../util/cn.js";
|
|
4
|
+
import { TOUCH_HIT_AREA } from "../util/touch-target.js";
|
|
3
5
|
|
|
4
6
|
interface Props {
|
|
5
7
|
message?: string;
|
|
6
8
|
type?: 'success' | 'error' | 'warning' | 'info';
|
|
7
9
|
closable?: boolean;
|
|
10
|
+
/** Accessible name of the close button. */
|
|
11
|
+
closeLabel?: string;
|
|
8
12
|
onclick?: (event: Event) => void;
|
|
9
13
|
class?: string;
|
|
10
14
|
/** `viewport`: fixed corner; `inline`: block in flow (e.g. demos). */
|
|
@@ -15,12 +19,17 @@
|
|
|
15
19
|
message = '',
|
|
16
20
|
type = 'info',
|
|
17
21
|
closable = true,
|
|
22
|
+
closeLabel: closeLabelGiven,
|
|
18
23
|
onclick,
|
|
19
24
|
class: className = '',
|
|
20
25
|
layout = 'viewport',
|
|
21
26
|
...restProps
|
|
22
27
|
}: Props = $props();
|
|
23
28
|
|
|
29
|
+
/** Each text: the prop, else the app-wide word from a `ZabiStringsProvider` above this one, else English. */
|
|
30
|
+
const provided = zabiStringsFor("toast");
|
|
31
|
+
const closeLabel = $derived(closeLabelGiven ?? provided()?.closeLabel ?? DEFAULT_TOAST_TEXTS.closeLabel);
|
|
32
|
+
|
|
24
33
|
let isVisible = $state(true);
|
|
25
34
|
|
|
26
35
|
const typeClasses: Record<NonNullable<Props['type']>, string> = {
|
|
@@ -30,8 +39,9 @@
|
|
|
30
39
|
info: 'border-border bg-surface-overlay text-body',
|
|
31
40
|
};
|
|
32
41
|
|
|
42
|
+
// `min(18rem, 100%)`: 18rem is wider than a 320px screen once the text is enlarged.
|
|
33
43
|
const cardClasses =
|
|
34
|
-
'box-border w-full min-w-[18rem] max-w-[min(24rem,calc(100vw-2rem))] overflow-hidden rounded-control border p-4 shadow-lg';
|
|
44
|
+
'box-border w-full min-w-[min(18rem,100%)] max-w-[min(24rem,calc(100vw-2rem))] overflow-hidden rounded-control border p-4 shadow-lg';
|
|
35
45
|
|
|
36
46
|
function closeToast(event: Event) {
|
|
37
47
|
isVisible = false;
|
|
@@ -46,11 +56,14 @@
|
|
|
46
56
|
</div>
|
|
47
57
|
|
|
48
58
|
{#if closable}
|
|
59
|
+
<!-- `px-1` on a touch screen: the 44px layer is centred on the
|
|
60
|
+
button, and around a button as narrow as the glyph it reached a
|
|
61
|
+
pixel past what the card clips. -->
|
|
49
62
|
<button
|
|
50
63
|
type="button"
|
|
51
|
-
class="focus-ring shrink-0 cursor-pointer rounded-control text-description hover:text-headline focus:outline-none"
|
|
64
|
+
class="focus-ring pointer-coarse:relative shrink-0 cursor-pointer rounded-control text-description hover:text-headline active:text-headline focus:outline-none pointer-coarse:px-1 {TOUCH_HIT_AREA}"
|
|
52
65
|
onclick={closeToast}
|
|
53
|
-
aria-label=
|
|
66
|
+
aria-label={closeLabel}
|
|
54
67
|
>
|
|
55
68
|
×
|
|
56
69
|
</button>
|
|
@@ -61,7 +74,7 @@
|
|
|
61
74
|
{#if isVisible}
|
|
62
75
|
{#if layout === 'viewport'}
|
|
63
76
|
<div
|
|
64
|
-
class="pointer-events-none fixed top-
|
|
77
|
+
class="pointer-events-none fixed top-[calc(max(var(--app-shell-top-inset,0px),env(safe-area-inset-top,0px))_+_1rem)] right-[calc(env(safe-area-inset-right,0px)_+_1rem)] left-[calc(env(safe-area-inset-left,0px)_+_1rem)] z-toast flex justify-end sm:left-auto"
|
|
65
78
|
>
|
|
66
79
|
<div
|
|
67
80
|
class={cn("pointer-events-auto", cardClasses, typeClasses[type], className)}
|
|
@@ -2,6 +2,8 @@ interface Props {
|
|
|
2
2
|
message?: string;
|
|
3
3
|
type?: 'success' | 'error' | 'warning' | 'info';
|
|
4
4
|
closable?: boolean;
|
|
5
|
+
/** Accessible name of the close button. */
|
|
6
|
+
closeLabel?: string;
|
|
5
7
|
onclick?: (event: Event) => void;
|
|
6
8
|
class?: string;
|
|
7
9
|
/** `viewport`: fixed corner; `inline`: block in flow (e.g. demos). */
|