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
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
2
|
+
type Props = Omit<HTMLAttributes<HTMLSpanElement>, "class"> & {
|
|
3
|
+
/**
|
|
4
|
+
* Diameter, on the scale the built-in loading states use: 12, 14, 16
|
|
5
|
+
* and 20px. For another size pass a `size-*` utility through `class`.
|
|
6
|
+
*/
|
|
7
|
+
size?: "xs" | "sm" | "md" | "lg";
|
|
8
|
+
/**
|
|
9
|
+
* What is loading. With a label the spinner is a `status` and the
|
|
10
|
+
* label is read out; without one it is decorative and hidden from
|
|
11
|
+
* assistive technology, for use beside text that already says so.
|
|
12
|
+
*/
|
|
13
|
+
label?: string;
|
|
14
|
+
/** Extra classes for the ring: a size, a colour, an opacity. */
|
|
15
|
+
class?: string;
|
|
16
|
+
};
|
|
17
|
+
declare const Spinner: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type Spinner = ReturnType<typeof Spinner>;
|
|
19
|
+
export default Spinner;
|
package/dist/atoms/Table.svelte
CHANGED
|
@@ -1,23 +1,168 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLTableAttributes } from 'svelte/elements';
|
|
3
4
|
import { cn } from "../util/cn.js";
|
|
5
|
+
import { generateId } from "../util/ssr-safe.js";
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
/** `true` stacks at every width; a breakpoint name stacks below it. */
|
|
8
|
+
export type TableStacked = boolean | "sm" | "md" | "lg";
|
|
9
|
+
|
|
10
|
+
/** Other attributes (`id`, `data-*`, `aria-*`, ...) land on the `<table>`. */
|
|
11
|
+
type Props = Omit<HTMLTableAttributes, 'class' | 'children'> & {
|
|
12
|
+
/** `<caption>`, and the table's accessible name, when set. */
|
|
7
13
|
caption?: string;
|
|
14
|
+
/** Keeps the caption as the accessible name but hides it from view. */
|
|
15
|
+
captionHidden?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Lays each row out as a block of label and value pairs instead of
|
|
18
|
+
* scrolling sideways. The label of a cell is its `data-label`
|
|
19
|
+
* attribute, which the caller sets on every `<td>`.
|
|
20
|
+
*/
|
|
21
|
+
stacked?: TableStacked;
|
|
8
22
|
class?: string;
|
|
9
23
|
children?: Snippet;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let {
|
|
27
|
+
caption,
|
|
28
|
+
captionHidden = false,
|
|
29
|
+
stacked = false,
|
|
30
|
+
class: className = '',
|
|
31
|
+
children,
|
|
32
|
+
...restProps
|
|
33
|
+
}: Props = $props();
|
|
34
|
+
|
|
35
|
+
const captionId = generateId("table-caption");
|
|
36
|
+
let tableElement = $state<HTMLTableElement>();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The rows arrive as `children`, so the stacked layout is written from the
|
|
40
|
+
* table down. Tailwind only generates classes it can read whole, which is
|
|
41
|
+
* why each breakpoint repeats the list rather than building it.
|
|
42
|
+
*
|
|
43
|
+
* The header row is hidden from view, not removed, so cells keep their
|
|
44
|
+
* column header. The visible label is the cell's `data-label` drawn in
|
|
45
|
+
* `::before`; where the browser supports alternative text for generated
|
|
46
|
+
* content it is given an empty one, so a screen reader reads the column
|
|
47
|
+
* header once instead of the header and then the label.
|
|
48
|
+
*
|
|
49
|
+
* The label takes the start of the line with an auto margin after it, and
|
|
50
|
+
* the cell packs everything else at the end. So a cell's own children stay
|
|
51
|
+
* together however many there are, and a cell with no label still has its
|
|
52
|
+
* value on the value side. An empty cell collapses to nothing but is not
|
|
53
|
+
* hidden: taking it out of the tree would shift the cells after it under
|
|
54
|
+
* the wrong column headers.
|
|
55
|
+
*/
|
|
56
|
+
const STACKED_CLASS = {
|
|
57
|
+
always:
|
|
58
|
+
"block min-w-0 [&>:not(thead)]:block [&>thead]:sr-only [&>*>tr]:block [&>*>tr]:py-2 " +
|
|
59
|
+
"[&>*>tr>*]:flex [&>*>tr>*]:items-baseline [&>*>tr>*]:justify-end " +
|
|
60
|
+
"[&>*>tr>*]:gap-4 [&>*>tr>*]:py-1 [&>*>tr>*]:text-end " +
|
|
61
|
+
"[&>*>tr>:empty]:py-0 [&>*>tr>[data-label]:not(:empty)]:before:me-auto " +
|
|
62
|
+
"[&>*>tr>[data-label]:not(:empty)]:before:shrink-0 " +
|
|
63
|
+
"[&>*>tr>[data-label]:not(:empty)]:before:text-start " +
|
|
64
|
+
"[&>*>tr>[data-label]:not(:empty)]:before:font-medium " +
|
|
65
|
+
"[&>*>tr>[data-label]:not(:empty)]:before:text-headline " +
|
|
66
|
+
"[&>*>tr>[data-label]:not(:empty)]:before:content-[attr(data-label)] " +
|
|
67
|
+
"supports-[content:'x'/'']:[&>*>tr>[data-label]:not(:empty)]:before:content-[attr(data-label)/'']",
|
|
68
|
+
sm:
|
|
69
|
+
"max-sm:block max-sm:min-w-0 max-sm:[&>:not(thead)]:block max-sm:[&>thead]:sr-only " +
|
|
70
|
+
"max-sm:[&>*>tr]:block max-sm:[&>*>tr]:py-2 max-sm:[&>*>tr>*]:flex " +
|
|
71
|
+
"max-sm:[&>*>tr>*]:items-baseline " +
|
|
72
|
+
"max-sm:[&>*>tr>*]:justify-end max-sm:[&>*>tr>*]:gap-4 " +
|
|
73
|
+
"max-sm:[&>*>tr>*]:py-1 max-sm:[&>*>tr>*]:text-end max-sm:[&>*>tr>:empty]:py-0 " +
|
|
74
|
+
"max-sm:[&>*>tr>[data-label]:not(:empty)]:before:me-auto " +
|
|
75
|
+
"max-sm:[&>*>tr>[data-label]:not(:empty)]:before:shrink-0 " +
|
|
76
|
+
"max-sm:[&>*>tr>[data-label]:not(:empty)]:before:text-start " +
|
|
77
|
+
"max-sm:[&>*>tr>[data-label]:not(:empty)]:before:font-medium " +
|
|
78
|
+
"max-sm:[&>*>tr>[data-label]:not(:empty)]:before:text-headline " +
|
|
79
|
+
"max-sm:[&>*>tr>[data-label]:not(:empty)]:before:content-[attr(data-label)] " +
|
|
80
|
+
"max-sm:supports-[content:'x'/'']:[&>*>tr>[data-label]:not(:empty)]:before:content-[attr(data-label)/'']",
|
|
81
|
+
md:
|
|
82
|
+
"max-md:block max-md:min-w-0 max-md:[&>:not(thead)]:block max-md:[&>thead]:sr-only " +
|
|
83
|
+
"max-md:[&>*>tr]:block max-md:[&>*>tr]:py-2 max-md:[&>*>tr>*]:flex " +
|
|
84
|
+
"max-md:[&>*>tr>*]:items-baseline " +
|
|
85
|
+
"max-md:[&>*>tr>*]:justify-end max-md:[&>*>tr>*]:gap-4 " +
|
|
86
|
+
"max-md:[&>*>tr>*]:py-1 max-md:[&>*>tr>*]:text-end max-md:[&>*>tr>:empty]:py-0 " +
|
|
87
|
+
"max-md:[&>*>tr>[data-label]:not(:empty)]:before:me-auto " +
|
|
88
|
+
"max-md:[&>*>tr>[data-label]:not(:empty)]:before:shrink-0 " +
|
|
89
|
+
"max-md:[&>*>tr>[data-label]:not(:empty)]:before:text-start " +
|
|
90
|
+
"max-md:[&>*>tr>[data-label]:not(:empty)]:before:font-medium " +
|
|
91
|
+
"max-md:[&>*>tr>[data-label]:not(:empty)]:before:text-headline " +
|
|
92
|
+
"max-md:[&>*>tr>[data-label]:not(:empty)]:before:content-[attr(data-label)] " +
|
|
93
|
+
"max-md:supports-[content:'x'/'']:[&>*>tr>[data-label]:not(:empty)]:before:content-[attr(data-label)/'']",
|
|
94
|
+
lg:
|
|
95
|
+
"max-lg:block max-lg:min-w-0 max-lg:[&>:not(thead)]:block max-lg:[&>thead]:sr-only " +
|
|
96
|
+
"max-lg:[&>*>tr]:block max-lg:[&>*>tr]:py-2 max-lg:[&>*>tr>*]:flex " +
|
|
97
|
+
"max-lg:[&>*>tr>*]:items-baseline " +
|
|
98
|
+
"max-lg:[&>*>tr>*]:justify-end max-lg:[&>*>tr>*]:gap-4 " +
|
|
99
|
+
"max-lg:[&>*>tr>*]:py-1 max-lg:[&>*>tr>*]:text-end max-lg:[&>*>tr>:empty]:py-0 " +
|
|
100
|
+
"max-lg:[&>*>tr>[data-label]:not(:empty)]:before:me-auto " +
|
|
101
|
+
"max-lg:[&>*>tr>[data-label]:not(:empty)]:before:shrink-0 " +
|
|
102
|
+
"max-lg:[&>*>tr>[data-label]:not(:empty)]:before:text-start " +
|
|
103
|
+
"max-lg:[&>*>tr>[data-label]:not(:empty)]:before:font-medium " +
|
|
104
|
+
"max-lg:[&>*>tr>[data-label]:not(:empty)]:before:text-headline " +
|
|
105
|
+
"max-lg:[&>*>tr>[data-label]:not(:empty)]:before:content-[attr(data-label)] " +
|
|
106
|
+
"max-lg:supports-[content:'x'/'']:[&>*>tr>[data-label]:not(:empty)]:before:content-[attr(data-label)/'']",
|
|
107
|
+
} as const;
|
|
108
|
+
|
|
109
|
+
const stackedClass = $derived(
|
|
110
|
+
stacked === true ? STACKED_CLASS.always : stacked ? STACKED_CLASS[stacked] : "",
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Some browsers stop treating a table as one once its boxes are no longer
|
|
115
|
+
* table boxes, and the rows then reach a screen reader as loose text.
|
|
116
|
+
* Explicit roles keep the structure whatever `display` says. The rows are
|
|
117
|
+
* the caller's markup, so the roles are set on the rendered elements and
|
|
118
|
+
* kept up as rows come and go; a role the caller set is left alone.
|
|
119
|
+
*/
|
|
120
|
+
function restoreTableRoles(table: HTMLTableElement) {
|
|
121
|
+
const set = (element: Element, role: string) => {
|
|
122
|
+
if (!element.hasAttribute("role")) element.setAttribute("role", role);
|
|
123
|
+
};
|
|
124
|
+
for (const group of table.querySelectorAll(":scope > :is(thead, tbody, tfoot)")) {
|
|
125
|
+
set(group, "rowgroup");
|
|
126
|
+
for (const row of group.querySelectorAll(":scope > tr")) {
|
|
127
|
+
set(row, "row");
|
|
128
|
+
for (const cell of row.querySelectorAll(":scope > :is(td, th)")) {
|
|
129
|
+
if (cell.tagName === "TD") set(cell, "cell");
|
|
130
|
+
else if (cell.getAttribute("scope") === "row") set(cell, "rowheader");
|
|
131
|
+
else if (cell.getAttribute("scope") === "col") set(cell, "columnheader");
|
|
132
|
+
else set(cell, group.tagName === "THEAD" ? "columnheader" : "rowheader");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
10
136
|
}
|
|
11
137
|
|
|
12
|
-
|
|
138
|
+
$effect(() => {
|
|
139
|
+
const table = tableElement;
|
|
140
|
+
if (!table || !stacked) return;
|
|
141
|
+
|
|
142
|
+
restoreTableRoles(table);
|
|
143
|
+
const observer = new MutationObserver(() => restoreTableRoles(table));
|
|
144
|
+
observer.observe(table, { childList: true, subtree: true });
|
|
145
|
+
return () => observer.disconnect();
|
|
146
|
+
});
|
|
13
147
|
</script>
|
|
14
148
|
|
|
15
149
|
<div
|
|
16
|
-
class={cn("overflow-x-auto rounded-control border border-border shadow-sm", className)}
|
|
150
|
+
class={cn("overflow-x-auto rounded-control border border-border bg-card shadow-sm", className)}
|
|
17
151
|
>
|
|
18
|
-
<table
|
|
152
|
+
<table
|
|
153
|
+
bind:this={tableElement}
|
|
154
|
+
class={cn("w-full min-w-[20rem] border-collapse text-left text-sm text-body", stackedClass)}
|
|
155
|
+
role={stacked ? "table" : undefined}
|
|
156
|
+
aria-labelledby={stacked && caption ? captionId : undefined}
|
|
157
|
+
{...restProps}
|
|
158
|
+
>
|
|
19
159
|
{#if caption}
|
|
20
|
-
<caption
|
|
160
|
+
<caption
|
|
161
|
+
id={captionId}
|
|
162
|
+
class={captionHidden
|
|
163
|
+
? "sr-only"
|
|
164
|
+
: "border-b border-border bg-surface-elevated px-4 py-3 text-left text-headline"}
|
|
165
|
+
>
|
|
21
166
|
{caption}
|
|
22
167
|
</caption>
|
|
23
168
|
{/if}
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import type { HTMLTableAttributes } from 'svelte/elements';
|
|
3
|
+
/** `true` stacks at every width; a breakpoint name stacks below it. */
|
|
4
|
+
export type TableStacked = boolean | "sm" | "md" | "lg";
|
|
5
|
+
/** Other attributes (`id`, `data-*`, `aria-*`, ...) land on the `<table>`. */
|
|
6
|
+
type Props = Omit<HTMLTableAttributes, 'class' | 'children'> & {
|
|
7
|
+
/** `<caption>`, and the table's accessible name, when set. */
|
|
4
8
|
caption?: string;
|
|
9
|
+
/** Keeps the caption as the accessible name but hides it from view. */
|
|
10
|
+
captionHidden?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Lays each row out as a block of label and value pairs instead of
|
|
13
|
+
* scrolling sideways. The label of a cell is its `data-label`
|
|
14
|
+
* attribute, which the caller sets on every `<td>`.
|
|
15
|
+
*/
|
|
16
|
+
stacked?: TableStacked;
|
|
5
17
|
class?: string;
|
|
6
18
|
children?: Snippet;
|
|
7
|
-
}
|
|
19
|
+
};
|
|
8
20
|
declare const Table: import("svelte").Component<Props, {}, "">;
|
|
9
21
|
type Table = ReturnType<typeof Table>;
|
|
10
22
|
export default Table;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { SizeVariant } from "../types/variants.js";
|
|
3
|
+
import { cn } from "../util/cn.js";
|
|
4
|
+
import Input from "./Input.svelte";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What DateField and TimeField share. Internal: it is not exported from
|
|
8
|
+
* the package, and its props are those of the two fields.
|
|
9
|
+
*
|
|
10
|
+
* It is an `Input` with a native `type`, so the box, the sizes, the label,
|
|
11
|
+
* the hint and the error message are Input's own and cannot drift from it.
|
|
12
|
+
* What is added here is only what a native date or time input needs to
|
|
13
|
+
* look like one: see the classes and the style block below.
|
|
14
|
+
*/
|
|
15
|
+
interface Props {
|
|
16
|
+
kind: "date" | "time";
|
|
17
|
+
id?: string;
|
|
18
|
+
value?: string;
|
|
19
|
+
name?: string;
|
|
20
|
+
label?: string;
|
|
21
|
+
hideLabel?: boolean;
|
|
22
|
+
hint?: string;
|
|
23
|
+
error?: string;
|
|
24
|
+
min?: string;
|
|
25
|
+
max?: string;
|
|
26
|
+
step?: number | "any";
|
|
27
|
+
required?: boolean;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
readonly?: boolean;
|
|
30
|
+
size?: SizeVariant;
|
|
31
|
+
class?: string;
|
|
32
|
+
"aria-describedby"?: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let {
|
|
36
|
+
kind,
|
|
37
|
+
id: idProp,
|
|
38
|
+
value = $bindable<Exclude<Props["value"], undefined>>(),
|
|
39
|
+
name = "",
|
|
40
|
+
label = "",
|
|
41
|
+
hideLabel = false,
|
|
42
|
+
hint = "",
|
|
43
|
+
error = "",
|
|
44
|
+
min,
|
|
45
|
+
max,
|
|
46
|
+
step,
|
|
47
|
+
required = false,
|
|
48
|
+
disabled = false,
|
|
49
|
+
readonly = false,
|
|
50
|
+
size = "md",
|
|
51
|
+
class: className = "",
|
|
52
|
+
"aria-describedby": describedBy,
|
|
53
|
+
...restProps
|
|
54
|
+
}: Props = $props();
|
|
55
|
+
|
|
56
|
+
// No fallback on a bindable prop: Svelte refuses `bind:…={undefined}` on
|
|
57
|
+
// one that has a fallback (`props_invalid_value`), and a page that throws
|
|
58
|
+
// while it hydrates never becomes interactive. The default is applied
|
|
59
|
+
// here instead: at once, for the server and the first render, and again
|
|
60
|
+
// whenever a parent hands back `undefined`.
|
|
61
|
+
const applyDefaults = () => {
|
|
62
|
+
if (value === undefined) value = "";
|
|
63
|
+
};
|
|
64
|
+
applyDefaults();
|
|
65
|
+
$effect.pre(applyDefaults);
|
|
66
|
+
|
|
67
|
+
/** What Input does not declare, but passes on to the `<input>`. */
|
|
68
|
+
const native = $derived({
|
|
69
|
+
min,
|
|
70
|
+
max,
|
|
71
|
+
step,
|
|
72
|
+
readonly: readonly || undefined,
|
|
73
|
+
"data-empty": value ? undefined : "",
|
|
74
|
+
...restProps,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const fieldClasses = $derived(
|
|
78
|
+
cn(
|
|
79
|
+
"zabi-temporal-field",
|
|
80
|
+
// Its own look off: WebKit on iOS otherwise draws a date input as
|
|
81
|
+
// a button, with its own height and centred text. `block` and
|
|
82
|
+
// `min-w-0`: an empty one there has no intrinsic width.
|
|
83
|
+
"block min-w-0 appearance-none",
|
|
84
|
+
// The format the browser shows in an empty field ("mm/dd/yyyy")
|
|
85
|
+
// is a hint, not a value: in the placeholder colour.
|
|
86
|
+
!value && "text-input-placeholder",
|
|
87
|
+
className,
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<div>
|
|
93
|
+
<Input
|
|
94
|
+
id={idProp}
|
|
95
|
+
type={kind}
|
|
96
|
+
bind:value
|
|
97
|
+
{name}
|
|
98
|
+
{label}
|
|
99
|
+
{hideLabel}
|
|
100
|
+
{required}
|
|
101
|
+
{disabled}
|
|
102
|
+
{size}
|
|
103
|
+
{hint}
|
|
104
|
+
{error}
|
|
105
|
+
aria-describedby={describedBy}
|
|
106
|
+
class={fieldClasses}
|
|
107
|
+
{...native}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<style>
|
|
112
|
+
/*
|
|
113
|
+
* The parts of a native date or time input that utilities cannot reach.
|
|
114
|
+
* Global selectors under one class: the `<input>` is rendered by Input,
|
|
115
|
+
* so a scoped selector would not match it.
|
|
116
|
+
*/
|
|
117
|
+
|
|
118
|
+
/* WebKit on iOS centres the value, and gives an empty one no height. */
|
|
119
|
+
:global(.zabi-temporal-field::-webkit-date-and-time-value) {
|
|
120
|
+
min-height: 1.5em;
|
|
121
|
+
text-align: start;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* The editable segments in Chromium and desktop WebKit: no extra box
|
|
125
|
+
inside the field, so the text sits where an Input's does. */
|
|
126
|
+
:global(.zabi-temporal-field::-webkit-datetime-edit) {
|
|
127
|
+
padding: 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* The browser's calendar or clock button. It follows `color-scheme`,
|
|
131
|
+
which the theme's dark block sets (see app.css); nothing here has to. */
|
|
132
|
+
:global(.zabi-temporal-field::-webkit-calendar-picker-indicator) {
|
|
133
|
+
cursor: pointer;
|
|
134
|
+
}
|
|
135
|
+
:global(.zabi-temporal-field:disabled::-webkit-calendar-picker-indicator),
|
|
136
|
+
:global(.zabi-temporal-field:read-only::-webkit-calendar-picker-indicator) {
|
|
137
|
+
cursor: default;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
</style>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { SizeVariant } from "../types/variants.js";
|
|
2
|
+
/**
|
|
3
|
+
* What DateField and TimeField share. Internal: it is not exported from
|
|
4
|
+
* the package, and its props are those of the two fields.
|
|
5
|
+
*
|
|
6
|
+
* It is an `Input` with a native `type`, so the box, the sizes, the label,
|
|
7
|
+
* the hint and the error message are Input's own and cannot drift from it.
|
|
8
|
+
* What is added here is only what a native date or time input needs to
|
|
9
|
+
* look like one: see the classes and the style block below.
|
|
10
|
+
*/
|
|
11
|
+
interface Props {
|
|
12
|
+
kind: "date" | "time";
|
|
13
|
+
id?: string;
|
|
14
|
+
value?: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
label?: string;
|
|
17
|
+
hideLabel?: boolean;
|
|
18
|
+
hint?: string;
|
|
19
|
+
error?: string;
|
|
20
|
+
min?: string;
|
|
21
|
+
max?: string;
|
|
22
|
+
step?: number | "any";
|
|
23
|
+
required?: boolean;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
readonly?: boolean;
|
|
26
|
+
size?: SizeVariant;
|
|
27
|
+
class?: string;
|
|
28
|
+
"aria-describedby"?: string | null;
|
|
29
|
+
}
|
|
30
|
+
declare const TemporalField: import("svelte").Component<Props, {}, "value">;
|
|
31
|
+
type TemporalField = ReturnType<typeof TemporalField>;
|
|
32
|
+
export default TemporalField;
|
package/dist/atoms/Text.svelte
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
4
|
|
|
4
5
|
import { cn } from "../util/cn.js";
|
|
5
|
-
type
|
|
6
|
+
import type { OnFillTone } from "../types/variants.js";
|
|
7
|
+
type Tone = 'body' | 'description' | 'caption' | 'headline' | 'label' | 'error' | OnFillTone;
|
|
6
8
|
type Size = 'xs' | 'sm' | 'md' | 'lg';
|
|
7
9
|
type Weight = 'normal' | 'medium' | 'semibold' | 'bold';
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
/** Other attributes (`id`, `data-*`, `aria-*`, ...) land on the element. */
|
|
12
|
+
type Props = Omit<HTMLAttributes<HTMLElement>, 'class'> & {
|
|
10
13
|
as?: 'p' | 'span' | 'div';
|
|
11
14
|
tone?: Tone;
|
|
12
15
|
size?: Size;
|
|
@@ -17,7 +20,7 @@
|
|
|
17
20
|
weight?: Weight;
|
|
18
21
|
class?: string;
|
|
19
22
|
children?: Snippet;
|
|
20
|
-
}
|
|
23
|
+
};
|
|
21
24
|
|
|
22
25
|
let {
|
|
23
26
|
as: Tag = 'p',
|
|
@@ -26,6 +29,7 @@
|
|
|
26
29
|
weight,
|
|
27
30
|
class: className = '',
|
|
28
31
|
children,
|
|
32
|
+
...restProps
|
|
29
33
|
}: Props = $props();
|
|
30
34
|
|
|
31
35
|
const toneClass: Record<Tone, string> = {
|
|
@@ -35,6 +39,9 @@
|
|
|
35
39
|
headline: 'text-headline',
|
|
36
40
|
label: 'text-label',
|
|
37
41
|
error: 'text-error',
|
|
42
|
+
inherit: 'text-inherit',
|
|
43
|
+
'on-brand': 'text-on-brand',
|
|
44
|
+
'on-accent': 'text-on-accent',
|
|
38
45
|
};
|
|
39
46
|
|
|
40
47
|
/**
|
|
@@ -63,13 +70,18 @@
|
|
|
63
70
|
/** Labels read as labels by default; callers can still override. */
|
|
64
71
|
const defaultWeight: Weight = $derived(tone === 'label' ? 'medium' : 'normal');
|
|
65
72
|
const resolvedWeight = $derived(weight ?? defaultWeight);
|
|
73
|
+
const classes = $derived(
|
|
74
|
+
cn(toneClass[tone], sizeClass[size], weightClass[resolvedWeight], className),
|
|
75
|
+
);
|
|
66
76
|
</script>
|
|
67
77
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
{
|
|
75
|
-
|
|
78
|
+
<!-- One branch per tag, not a dynamic element: hydration takes a dynamic
|
|
79
|
+
element out and puts it back, which blurs a link or a field inside it that the
|
|
80
|
+
user had already tabbed to. -->
|
|
81
|
+
{#if Tag === 'span'}
|
|
82
|
+
<span class={classes} {...restProps}>{@render children?.()}</span>
|
|
83
|
+
{:else if Tag === 'div'}
|
|
84
|
+
<div class={classes} {...restProps}>{@render children?.()}</div>
|
|
85
|
+
{:else}
|
|
86
|
+
<p class={classes} {...restProps}>{@render children?.()}</p>
|
|
87
|
+
{/if}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
type
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import type { OnFillTone } from "../types/variants.js";
|
|
4
|
+
type Tone = 'body' | 'description' | 'caption' | 'headline' | 'label' | 'error' | OnFillTone;
|
|
3
5
|
type Size = 'xs' | 'sm' | 'md' | 'lg';
|
|
4
6
|
type Weight = 'normal' | 'medium' | 'semibold' | 'bold';
|
|
5
|
-
|
|
7
|
+
/** Other attributes (`id`, `data-*`, `aria-*`, ...) land on the element. */
|
|
8
|
+
type Props = Omit<HTMLAttributes<HTMLElement>, 'class'> & {
|
|
6
9
|
as?: 'p' | 'span' | 'div';
|
|
7
10
|
tone?: Tone;
|
|
8
11
|
size?: Size;
|
|
@@ -13,7 +16,7 @@ interface Props {
|
|
|
13
16
|
weight?: Weight;
|
|
14
17
|
class?: string;
|
|
15
18
|
children?: Snippet;
|
|
16
|
-
}
|
|
19
|
+
};
|
|
17
20
|
declare const Text: import("svelte").Component<Props, {}, "">;
|
|
18
21
|
type Text = ReturnType<typeof Text>;
|
|
19
22
|
export default Text;
|
|
@@ -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 } 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;
|
|
@@ -18,13 +36,20 @@
|
|
|
18
36
|
rows?: number;
|
|
19
37
|
size?: SizeVariant;
|
|
20
38
|
variant?: SemanticVariant;
|
|
39
|
+
/** Status text under the field. Shown with a `success`, `warning` or `error` variant; for neutral help, use `hint`. */
|
|
21
40
|
message?: string;
|
|
41
|
+
/** Help text under the field, read out with it. */
|
|
42
|
+
hint?: string;
|
|
43
|
+
/** An error under the field. It marks the field invalid and is announced; it wins over `variant` and `message`. */
|
|
44
|
+
error?: string;
|
|
22
45
|
oninput?: (event: Event) => void;
|
|
23
|
-
|
|
46
|
+
/** Ids of other elements that describe the field. The hint and the message are added after them. */
|
|
47
|
+
"aria-describedby"?: string | null;
|
|
48
|
+
};
|
|
24
49
|
|
|
25
50
|
let {
|
|
26
51
|
id: idProp,
|
|
27
|
-
value = $bindable(
|
|
52
|
+
value = $bindable<Exclude<Props["value"], undefined>>(),
|
|
28
53
|
name = '',
|
|
29
54
|
class: className = '',
|
|
30
55
|
label = '',
|
|
@@ -37,27 +62,44 @@
|
|
|
37
62
|
size = 'md',
|
|
38
63
|
variant = 'default',
|
|
39
64
|
message = '',
|
|
65
|
+
hint = '',
|
|
66
|
+
error = '',
|
|
40
67
|
oninput,
|
|
68
|
+
'aria-describedby': describedBy,
|
|
41
69
|
...restProps
|
|
42
70
|
}: Props = $props();
|
|
43
71
|
|
|
72
|
+
// No fallback on a bindable prop: Svelte refuses `bind:…={undefined}` on
|
|
73
|
+
// one that has a fallback (`props_invalid_value`), and a page that throws
|
|
74
|
+
// while it hydrates never becomes interactive. The default is applied
|
|
75
|
+
// here instead: at once, for the server and the first render, and again
|
|
76
|
+
// whenever a parent hands back `undefined`.
|
|
77
|
+
const applyDefaults = () => {
|
|
78
|
+
if (value === undefined) value = '';
|
|
79
|
+
};
|
|
80
|
+
applyDefaults();
|
|
81
|
+
$effect.pre(applyDefaults);
|
|
82
|
+
|
|
44
83
|
const fallbackId = generateId('textarea');
|
|
45
84
|
const textareaId = $derived(idProp ?? fallbackId);
|
|
46
85
|
const isDisabled = $derived(disabled || loading);
|
|
47
86
|
|
|
87
|
+
const status = $derived(fieldMessageState({ variant, message, hint, error }));
|
|
88
|
+
|
|
48
89
|
const variantClass = $derived(() => {
|
|
49
|
-
return variant === 'success'
|
|
90
|
+
return status.variant === 'success'
|
|
50
91
|
? 'border-success focus-visible:border-success'
|
|
51
|
-
: variant === 'warning'
|
|
92
|
+
: status.variant === 'warning'
|
|
52
93
|
? 'border-warning focus-visible:border-warning'
|
|
53
|
-
: variant === 'error'
|
|
94
|
+
: status.variant === 'error'
|
|
54
95
|
? 'border-error focus-visible:border-error'
|
|
55
|
-
: 'border-input-border';
|
|
96
|
+
: 'border-input-border enabled:hover:border-input-border-hover';
|
|
56
97
|
});
|
|
57
98
|
|
|
99
|
+
// 16px below `sm`: iOS Safari zooms the page when a focused field is smaller.
|
|
58
100
|
const textareaClasses = $derived(() => {
|
|
59
101
|
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 resize-y px-3 py-2 text-sm leading-6';
|
|
102
|
+
'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 resize-y px-3 py-2 text-sm max-sm:text-base leading-6';
|
|
61
103
|
|
|
62
104
|
return cn(`${baseClasses} ${variantClass()} ${className}`);
|
|
63
105
|
});
|
|
@@ -66,24 +108,6 @@
|
|
|
66
108
|
() => 'mb-1 block text-sm font-medium text-label',
|
|
67
109
|
);
|
|
68
110
|
|
|
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
111
|
function handleInput(event: Event) {
|
|
88
112
|
const target = event.target as HTMLTextAreaElement;
|
|
89
113
|
value = target.value;
|
|
@@ -106,10 +130,10 @@
|
|
|
106
130
|
{rows}
|
|
107
131
|
class={textareaClasses()}
|
|
108
132
|
oninput={handleInput}
|
|
109
|
-
aria-invalid={variant === 'error' ? 'true' : undefined}
|
|
133
|
+
aria-invalid={status.variant === 'error' ? 'true' : undefined}
|
|
110
134
|
aria-required={required ? 'true' : undefined}
|
|
111
135
|
aria-busy={loading ? 'true' : undefined}
|
|
112
|
-
aria-describedby={
|
|
136
|
+
aria-describedby={fieldDescribedBy(textareaId, status, describedBy)}
|
|
113
137
|
{...restProps}
|
|
114
138
|
></textarea>
|
|
115
139
|
{#if loading}
|
|
@@ -118,23 +142,10 @@
|
|
|
118
142
|
aria-hidden="true"
|
|
119
143
|
>
|
|
120
144
|
<span
|
|
121
|
-
class="inline-block size-5 shrink-0 animate-spin rounded-full border-2 border-current border-t-transparent opacity-70"
|
|
145
|
+
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
146
|
></span>
|
|
123
147
|
</span>
|
|
124
148
|
{/if}
|
|
125
149
|
</div>
|
|
126
|
-
{
|
|
127
|
-
<p
|
|
128
|
-
id={`${textareaId}-message`}
|
|
129
|
-
class={messageClasses()}
|
|
130
|
-
role={variant === 'error' ? 'alert' : 'status'}
|
|
131
|
-
aria-live={variant === 'error' ? 'assertive' : 'polite'}
|
|
132
|
-
>
|
|
133
|
-
{#if getIcon()}
|
|
134
|
-
{@const Icon = getIcon()}
|
|
135
|
-
<Icon size={14} class="shrink-0" />
|
|
136
|
-
{/if}
|
|
137
|
-
<span>{message}</span>
|
|
138
|
-
</p>
|
|
139
|
-
{/if}
|
|
150
|
+
<FieldMessages fieldId={textareaId} {status} {hint} gap="mt-1" />
|
|
140
151
|
</div>
|