zabi-components 8.1.0 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1011 -0
- package/README.md +1 -1
- package/THEME.md +18 -5
- package/THEMING.md +308 -74
- package/dist/atoms/ActionPanel.svelte +1 -1
- package/dist/atoms/Avatar.svelte +19 -3
- package/dist/atoms/Avatar.svelte.d.ts +12 -0
- package/dist/atoms/Badge.svelte +37 -8
- package/dist/atoms/Badge.svelte.d.ts +6 -0
- package/dist/atoms/Block.svelte +74 -0
- package/dist/atoms/Block.svelte.d.ts +29 -0
- package/dist/atoms/Button.svelte +52 -15
- package/dist/atoms/Button.svelte.d.ts +3 -2
- package/dist/atoms/Card.svelte +181 -25
- package/dist/atoms/Card.svelte.d.ts +55 -3
- package/dist/atoms/Checkbox.svelte +33 -0
- package/dist/atoms/Checkbox.svelte.d.ts +27 -0
- package/dist/atoms/Chip.svelte +351 -0
- package/dist/atoms/Chip.svelte.d.ts +57 -0
- package/dist/atoms/CodeField.svelte +385 -0
- package/dist/atoms/CodeField.svelte.d.ts +61 -0
- package/dist/atoms/ColorPicker.svelte +1 -0
- package/dist/atoms/DateField.svelte +41 -3
- package/dist/atoms/DateField.svelte.d.ts +31 -3
- package/dist/atoms/FloatingActionButton.svelte +5 -3
- package/dist/atoms/FloatingActionButton.svelte.d.ts +1 -1
- package/dist/atoms/Heading.svelte +27 -1
- package/dist/atoms/Heading.svelte.d.ts +7 -0
- package/dist/atoms/IconButton.svelte +81 -9
- package/dist/atoms/IconButton.svelte.d.ts +13 -0
- package/dist/atoms/Input.svelte +10 -5
- package/dist/atoms/Input.svelte.d.ts +3 -2
- package/dist/atoms/List.svelte +3 -2
- package/dist/atoms/ListItem.svelte +31 -7
- package/dist/atoms/ListItem.svelte.d.ts +6 -0
- package/dist/atoms/PickerField.svelte +239 -0
- package/dist/atoms/PickerField.svelte.d.ts +67 -0
- package/dist/atoms/Progress.svelte +78 -17
- package/dist/atoms/Progress.svelte.d.ts +11 -1
- package/dist/atoms/Radio.svelte +32 -0
- package/dist/atoms/Radio.svelte.d.ts +26 -0
- package/dist/atoms/Rating.svelte +132 -6
- package/dist/atoms/Rating.svelte.d.ts +24 -3
- package/dist/atoms/Select.svelte +24 -26
- package/dist/atoms/Select.svelte.d.ts +12 -0
- package/dist/atoms/SelectionControl.svelte +99 -6
- package/dist/atoms/SelectionControl.svelte.d.ts +25 -0
- package/dist/atoms/Skeleton.svelte +114 -18
- package/dist/atoms/Skeleton.svelte.d.ts +30 -3
- package/dist/atoms/SkeletonGroup.svelte +43 -0
- package/dist/atoms/SkeletonGroup.svelte.d.ts +20 -0
- package/dist/atoms/Slider.svelte +35 -0
- package/dist/atoms/Sparkline.svelte +109 -0
- package/dist/atoms/Sparkline.svelte.d.ts +27 -0
- package/dist/atoms/SpeechBubble.svelte +187 -0
- package/dist/atoms/SpeechBubble.svelte.d.ts +33 -0
- package/dist/atoms/Stat.svelte +72 -0
- package/dist/atoms/Stat.svelte.d.ts +19 -0
- package/dist/atoms/TemporalField.svelte +342 -10
- package/dist/atoms/TemporalField.svelte.d.ts +20 -4
- package/dist/atoms/Textarea.svelte +100 -5
- package/dist/atoms/Textarea.svelte.d.ts +21 -0
- package/dist/atoms/TimeColumn.svelte +150 -0
- package/dist/atoms/TimeColumn.svelte.d.ts +25 -0
- package/dist/atoms/TimeField.svelte +46 -3
- package/dist/atoms/TimeField.svelte.d.ts +34 -3
- package/dist/atoms/Toast.svelte +6 -5
- package/dist/atoms/Toggle.svelte +61 -10
- package/dist/atoms/Toggle.svelte.d.ts +15 -0
- package/dist/atoms/Tooltip.svelte +20 -1
- package/dist/atoms/Tooltip.svelte.d.ts +7 -0
- package/dist/atoms/index.d.ts +21 -0
- package/dist/atoms/index.js +17 -0
- package/dist/atoms/selection-control.styles.d.ts +15 -4
- package/dist/atoms/selection-control.styles.js +16 -5
- package/dist/components/atoms/index.d.ts +21 -0
- package/dist/components/atoms/index.d.ts.map +1 -1
- package/dist/components/atoms/selection-control.styles.d.ts +15 -4
- package/dist/components/atoms/selection-control.styles.d.ts.map +1 -1
- package/dist/components/index.d.ts +27 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/map.d.ts +11 -0
- package/dist/components/map.d.ts.map +1 -0
- package/dist/components/molecules/index.d.ts +7 -2
- package/dist/components/molecules/index.d.ts.map +1 -1
- package/dist/components/molecules/toast-store.d.ts +30 -0
- package/dist/components/molecules/toast-store.d.ts.map +1 -1
- package/dist/components/organisms/index.d.ts +6 -1
- package/dist/components/organisms/index.d.ts.map +1 -1
- package/dist/components/types/variants.d.ts +12 -3
- package/dist/components/types/variants.d.ts.map +1 -1
- package/dist/components/util/app-shell.d.ts +55 -4
- package/dist/components/util/app-shell.d.ts.map +1 -1
- package/dist/components/util/avatar.d.ts +3 -0
- package/dist/components/util/avatar.d.ts.map +1 -1
- package/dist/components/util/bottom-tab-bar.d.ts +6 -0
- package/dist/components/util/bottom-tab-bar.d.ts.map +1 -1
- package/dist/components/util/chart.d.ts +140 -0
- package/dist/components/util/chart.d.ts.map +1 -0
- package/dist/components/util/chip.d.ts +66 -0
- package/dist/components/util/chip.d.ts.map +1 -0
- package/dist/components/util/cn.d.ts.map +1 -1
- package/dist/components/util/code-field.d.ts +63 -0
- package/dist/components/util/code-field.d.ts.map +1 -0
- package/dist/components/util/collapsible.d.ts +6 -0
- package/dist/components/util/collapsible.d.ts.map +1 -1
- package/dist/components/util/display-voice.d.ts +10 -0
- package/dist/components/util/display-voice.d.ts.map +1 -0
- package/dist/components/util/field-control.d.ts +30 -0
- package/dist/components/util/field-control.d.ts.map +1 -0
- package/dist/components/util/field.d.ts +6 -0
- package/dist/components/util/field.d.ts.map +1 -1
- package/dist/components/util/focus-utils.d.ts +2 -0
- package/dist/components/util/focus-utils.d.ts.map +1 -1
- package/dist/components/util/map-view.d.ts +144 -0
- package/dist/components/util/map-view.d.ts.map +1 -0
- package/dist/components/util/photo.d.ts +19 -0
- package/dist/components/util/photo.d.ts.map +1 -1
- package/dist/components/util/progress.d.ts +19 -0
- package/dist/components/util/progress.d.ts.map +1 -0
- package/dist/components/util/scroll-edge.d.ts +33 -0
- package/dist/components/util/scroll-edge.d.ts.map +1 -0
- package/dist/components/util/sheet-drag.d.ts +25 -1
- package/dist/components/util/sheet-drag.d.ts.map +1 -1
- package/dist/components/util/skeleton.d.ts +15 -0
- package/dist/components/util/skeleton.d.ts.map +1 -0
- package/dist/components/util/sliding-indicator.d.ts +25 -0
- package/dist/components/util/sliding-indicator.d.ts.map +1 -0
- package/dist/components/util/temporal-field.d.ts +89 -0
- package/dist/components/util/temporal-field.d.ts.map +1 -0
- package/dist/components/util/toaster.d.ts +40 -0
- package/dist/components/util/toaster.d.ts.map +1 -1
- package/dist/components/util/tone-surface.d.ts +34 -0
- package/dist/components/util/tone-surface.d.ts.map +1 -0
- package/dist/components/util/top-navbar.d.ts +26 -2
- package/dist/components/util/top-navbar.d.ts.map +1 -1
- package/dist/components/util/zabi-strings.d.ts +9 -0
- package/dist/components/util/zabi-strings.d.ts.map +1 -1
- package/dist/create-theme/cli.js +6 -2
- package/dist/create-theme/index.d.ts +17 -3
- package/dist/create-theme/index.js +252 -35
- package/dist/create-theme/lib/resolve.js +97 -0
- package/dist/create-theme/theme-data.js +1618 -90
- package/dist/index.d.ts +27 -3
- package/dist/index.js +18 -0
- package/dist/map.d.ts +10 -0
- package/dist/map.js +9 -0
- package/dist/molecules/Alert.svelte +31 -12
- package/dist/molecules/Alert.svelte.d.ts +12 -1
- package/dist/molecules/AppBar.svelte +287 -28
- package/dist/molecules/AppBar.svelte.d.ts +62 -0
- package/dist/molecules/AvatarGroup.svelte +1 -1
- package/dist/molecules/BarChart.svelte +381 -0
- package/dist/molecules/BarChart.svelte.d.ts +45 -0
- package/dist/molecules/BottomSheet.svelte +88 -20
- package/dist/molecules/BottomSheet.svelte.d.ts +17 -1
- package/dist/molecules/BottomTabBar.svelte +134 -28
- package/dist/molecules/BottomTabBar.svelte.d.ts +36 -2
- package/dist/molecules/Calendar.svelte.d.ts +1 -1
- package/dist/molecules/ChipGroup.svelte +325 -0
- package/dist/molecules/ChipGroup.svelte.d.ts +41 -0
- package/dist/molecules/Collapsible.svelte +158 -54
- package/dist/molecules/Collapsible.svelte.d.ts +23 -6
- package/dist/molecules/CollapsibleGroup.svelte +16 -6
- package/dist/molecules/ConfirmDialog.svelte +5 -1
- package/dist/molecules/ConfirmDialog.svelte.d.ts +3 -0
- package/dist/molecules/Drawer.svelte +33 -12
- package/dist/molecules/Dropdown.svelte +9 -1
- package/dist/molecules/Dropdown.svelte.d.ts +2 -0
- package/dist/molecules/DropdownItem.svelte +1 -1
- package/dist/molecules/ImageUpload.svelte +230 -120
- package/dist/molecules/ImageUpload.svelte.d.ts +38 -3
- package/dist/molecules/LineChart.svelte +459 -0
- package/dist/molecules/LineChart.svelte.d.ts +53 -0
- package/dist/molecules/MapView.svelte +565 -0
- package/dist/molecules/MapView.svelte.d.ts +88 -0
- package/dist/molecules/Modal.svelte +11 -6
- package/dist/molecules/PhotoGrid.svelte +100 -10
- package/dist/molecules/PhotoGrid.svelte.d.ts +11 -0
- package/dist/molecules/PhotoViewer.svelte +121 -24
- package/dist/molecules/PhotoViewer.svelte.d.ts +18 -0
- package/dist/molecules/PieChart.svelte +421 -0
- package/dist/molecules/PieChart.svelte.d.ts +51 -0
- package/dist/molecules/RadioGroup.svelte +20 -4
- package/dist/molecules/RadioGroup.svelte.d.ts +5 -0
- package/dist/molecules/SegmentedControl.svelte +136 -3
- package/dist/molecules/SegmentedControl.svelte.d.ts +6 -0
- package/dist/molecules/SidebarFooter.svelte +1 -1
- package/dist/molecules/SidebarNavSection.svelte +1 -1
- package/dist/molecules/SortableList.svelte +1 -0
- package/dist/molecules/Stepper.svelte +21 -7
- package/dist/molecules/StickyActionBar.svelte +67 -8
- package/dist/molecules/StickyActionBar.svelte.d.ts +14 -1
- package/dist/molecules/Tabs.svelte +79 -2
- package/dist/molecules/Toaster.svelte +171 -7
- package/dist/molecules/Toaster.svelte.d.ts +60 -2
- package/dist/molecules/ToasterToast.svelte +292 -60
- package/dist/molecules/ToasterToast.svelte.d.ts +6 -0
- package/dist/molecules/UnsavedChangesBar.svelte +13 -2
- package/dist/molecules/index.d.ts +7 -2
- package/dist/molecules/index.js +4 -0
- package/dist/molecules/toast-store.d.ts +30 -0
- package/dist/molecules/toast-store.js +4 -1
- package/dist/organisms/AppNavigation.svelte +440 -0
- package/dist/organisms/AppNavigation.svelte.d.ts +72 -0
- package/dist/organisms/AppShell.svelte +539 -39
- package/dist/organisms/AppShell.svelte.d.ts +102 -6
- package/dist/organisms/AppShellHeaderPart.svelte +163 -0
- package/dist/organisms/AppShellHeaderPart.svelte.d.ts +57 -0
- package/dist/organisms/SidebarNavigation.svelte +23 -36
- package/dist/organisms/SidebarPanel.svelte +10 -14
- package/dist/organisms/SidebarPanel.svelte.d.ts +1 -0
- package/dist/organisms/SidebarShell.svelte +2 -3
- package/dist/organisms/TopNavbar.svelte +117 -28
- package/dist/organisms/TopNavbar.svelte.d.ts +22 -1
- package/dist/organisms/TopNavbarSheetContent.svelte +145 -0
- package/dist/organisms/TopNavbarSheetContent.svelte.d.ts +24 -0
- package/dist/organisms/index.d.ts +6 -1
- package/dist/organisms/index.js +4 -0
- package/dist/types/components.d.ts +17 -1
- package/dist/types/components.d.ts.map +1 -1
- package/dist/types/index.d.ts +497 -10
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/variants.ts +14 -3
- package/dist/util/app-shell.d.ts +55 -4
- package/dist/util/app-shell.js +32 -7
- package/dist/util/avatar.d.ts +3 -0
- package/dist/util/bottom-tab-bar.d.ts +6 -0
- package/dist/util/chart.d.ts +139 -0
- package/dist/util/chart.js +305 -0
- package/dist/util/chip.d.ts +65 -0
- package/dist/util/chip.js +58 -0
- package/dist/util/cn.js +28 -2
- package/dist/util/code-field.d.ts +62 -0
- package/dist/util/code-field.js +112 -0
- package/dist/util/collapsible.d.ts +6 -0
- package/dist/util/display-voice.d.ts +9 -0
- package/dist/util/display-voice.js +9 -0
- package/dist/util/field-control.d.ts +29 -0
- package/dist/util/field-control.js +32 -0
- package/dist/util/field.d.ts +6 -0
- package/dist/util/focus-utils.d.ts +2 -0
- package/dist/util/focus-utils.js +4 -0
- package/dist/util/map-view.d.ts +143 -0
- package/dist/util/map-view.js +121 -0
- package/dist/util/photo.d.ts +19 -0
- package/dist/util/photo.js +3 -0
- package/dist/util/progress.d.ts +18 -0
- package/dist/util/progress.js +17 -0
- package/dist/util/scroll-edge.d.ts +32 -0
- package/dist/util/scroll-edge.js +87 -0
- package/dist/util/sheet-drag.d.ts +25 -1
- package/dist/util/sheet-drag.js +71 -1
- package/dist/util/skeleton.d.ts +14 -0
- package/dist/util/skeleton.js +22 -0
- package/dist/util/sliding-indicator.d.ts +24 -0
- package/dist/util/sliding-indicator.js +44 -0
- package/dist/util/temporal-field.d.ts +88 -0
- package/dist/util/temporal-field.js +136 -0
- package/dist/util/toaster.d.ts +40 -0
- package/dist/util/toaster.js +55 -0
- package/dist/util/tone-surface.d.ts +33 -0
- package/dist/util/tone-surface.js +35 -0
- package/dist/util/top-navbar.d.ts +26 -2
- package/dist/util/top-navbar.js +23 -0
- package/dist/util/zabi-strings.d.ts +9 -0
- package/dist/zabi-components-colors.css +9 -3
- package/dist/zabi-components-theme-dark-only.css +474 -164
- package/dist/zabi-components-theme-dark.css +474 -164
- package/dist/zabi-components-theme-only.css +933 -99
- package/dist/zabi-components-theme.css +933 -99
- package/dist/zabi-components.css +2275 -321
- package/package.json +11 -1
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLInputAttributes } from "svelte/elements";
|
|
3
|
+
import type { SemanticVariant } from "../types/variants.js";
|
|
4
|
+
import { cn } from "../util/cn.js";
|
|
5
|
+
import type { SizeVariant } from "../types/variants.js";
|
|
6
|
+
import {
|
|
7
|
+
activePlace,
|
|
8
|
+
caretAfterSanitise,
|
|
9
|
+
CODE_FIELD_GAP_REM,
|
|
10
|
+
CODE_FIELD_GROUP_GAP_REM,
|
|
11
|
+
CODE_FIELD_SIZES,
|
|
12
|
+
codeGroups,
|
|
13
|
+
codeLength,
|
|
14
|
+
DEFAULT_CODE_FIELD_STRINGS,
|
|
15
|
+
isCodeChar,
|
|
16
|
+
nearestBox,
|
|
17
|
+
pasteInto,
|
|
18
|
+
replaceAt,
|
|
19
|
+
sanitiseCode,
|
|
20
|
+
type CodeFieldMode,
|
|
21
|
+
type CodeFieldStrings,
|
|
22
|
+
} from "../util/code-field.js";
|
|
23
|
+
import { fieldDescribedBy, fieldMessageState } from "../util/field.js";
|
|
24
|
+
import { mergeStrings } from "../util/ready-made-strings.js";
|
|
25
|
+
import { generateId } from "../util/ssr-safe.js";
|
|
26
|
+
import { zabiStringsFor } from "../util/zabi-strings.js";
|
|
27
|
+
import FieldMessages from "./FieldMessages.svelte";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A field for a one-time or verification code. One real `<input>` holds
|
|
31
|
+
* the code, so autofill from a message, paste, the numeric keyboard, form
|
|
32
|
+
* posting by `name` and a single accessible name all work as on `Input`;
|
|
33
|
+
* the boxes are drawn under it and hidden from assistive technology.
|
|
34
|
+
* Other attributes (`autocomplete`, `inputmode`, `data-*`, ...) land on
|
|
35
|
+
* the `<input>`.
|
|
36
|
+
*/
|
|
37
|
+
type Props = Omit<
|
|
38
|
+
HTMLInputAttributes,
|
|
39
|
+
| "class"
|
|
40
|
+
| "size"
|
|
41
|
+
| "value"
|
|
42
|
+
| "type"
|
|
43
|
+
| "id"
|
|
44
|
+
| "name"
|
|
45
|
+
| "required"
|
|
46
|
+
| "disabled"
|
|
47
|
+
| "readonly"
|
|
48
|
+
| "maxlength"
|
|
49
|
+
| "oninput"
|
|
50
|
+
| "onblur"
|
|
51
|
+
| "aria-label"
|
|
52
|
+
| "aria-describedby"
|
|
53
|
+
> & {
|
|
54
|
+
/** Omit to auto-generate. */
|
|
55
|
+
id?: string;
|
|
56
|
+
/** The code typed so far. Bindable. */
|
|
57
|
+
value?: string;
|
|
58
|
+
/** The name the code is posted under, with or without scripts. */
|
|
59
|
+
name?: string;
|
|
60
|
+
class?: string;
|
|
61
|
+
/** Places in the code. A whole number from 1 to 12. */
|
|
62
|
+
length?: number;
|
|
63
|
+
/** `numeric` (default): digits only. `alphanumeric` also takes letters. */
|
|
64
|
+
mode?: CodeFieldMode;
|
|
65
|
+
/** Box size: `sm` (44px tall, a compact row), `md` (default), `lg`. */
|
|
66
|
+
size?: SizeVariant;
|
|
67
|
+
/**
|
|
68
|
+
* Draws the boxes in groups with a wider gap between them: a number is
|
|
69
|
+
* the size of each group (`4` makes 4 and 4 of 8), a list gives each
|
|
70
|
+
* group's size. Only the drawing changes: one input, one value, and a
|
|
71
|
+
* paste with spaces or dashes still fills it. A row that cannot fit at
|
|
72
|
+
* large text or a narrow width wraps between groups.
|
|
73
|
+
*/
|
|
74
|
+
groups?: number | number[];
|
|
75
|
+
/** Names the field. Defaults to the provider's `codeField.label`, then "Verification code". */
|
|
76
|
+
label?: string;
|
|
77
|
+
/** Keeps the label for assistive technology and takes it off the screen. */
|
|
78
|
+
labelHidden?: boolean;
|
|
79
|
+
required?: boolean;
|
|
80
|
+
disabled?: boolean;
|
|
81
|
+
readonly?: boolean;
|
|
82
|
+
variant?: SemanticVariant;
|
|
83
|
+
/** Status text under the field. Shown with a `success`, `warning` or `error` variant; for neutral help, use `hint`. */
|
|
84
|
+
message?: string;
|
|
85
|
+
/** Help text under the field, read out with it. */
|
|
86
|
+
hint?: string;
|
|
87
|
+
/** An error under the field. It marks the field invalid and is announced; it wins over `variant` and `message`. */
|
|
88
|
+
error?: string;
|
|
89
|
+
/** Called with the code when the last place is filled by typing, pasting or autofill. It does not submit anything. */
|
|
90
|
+
oncomplete?: (value: string) => void;
|
|
91
|
+
/** Overrides for the built-in strings: label, description. */
|
|
92
|
+
strings?: Partial<CodeFieldStrings>;
|
|
93
|
+
oninput?: (event: Event) => void;
|
|
94
|
+
onblur?: (event: Event) => void;
|
|
95
|
+
"aria-label"?: string;
|
|
96
|
+
/** Ids of other elements that describe the field. The length, the hint and the message are added after them. */
|
|
97
|
+
"aria-describedby"?: string | null;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** An event on the input, as Svelte types the handlers of an element. */
|
|
101
|
+
type OnInput<E extends Event> = E & { currentTarget: EventTarget & HTMLInputElement };
|
|
102
|
+
|
|
103
|
+
let {
|
|
104
|
+
id: idProp,
|
|
105
|
+
value = $bindable<Exclude<Props["value"], undefined>>(),
|
|
106
|
+
name = "",
|
|
107
|
+
class: className = "",
|
|
108
|
+
length: lengthProp = 6,
|
|
109
|
+
mode = "numeric",
|
|
110
|
+
size = "md",
|
|
111
|
+
groups,
|
|
112
|
+
label,
|
|
113
|
+
labelHidden = false,
|
|
114
|
+
required = false,
|
|
115
|
+
disabled = false,
|
|
116
|
+
readonly = false,
|
|
117
|
+
variant = "default",
|
|
118
|
+
message = "",
|
|
119
|
+
hint = "",
|
|
120
|
+
error = "",
|
|
121
|
+
oncomplete,
|
|
122
|
+
strings,
|
|
123
|
+
oninput,
|
|
124
|
+
onblur,
|
|
125
|
+
"aria-label": ariaLabel,
|
|
126
|
+
"aria-describedby": describedBy,
|
|
127
|
+
...restProps
|
|
128
|
+
}: Props = $props();
|
|
129
|
+
|
|
130
|
+
// No fallback on a bindable prop (see Input): the default is applied here,
|
|
131
|
+
// at once and whenever a parent hands back `undefined`.
|
|
132
|
+
const applyDefaults = () => {
|
|
133
|
+
if (value === undefined) value = "";
|
|
134
|
+
};
|
|
135
|
+
applyDefaults();
|
|
136
|
+
$effect.pre(applyDefaults);
|
|
137
|
+
|
|
138
|
+
const length = $derived(codeLength(lengthProp));
|
|
139
|
+
const sizing = $derived(CODE_FIELD_SIZES[size] ?? CODE_FIELD_SIZES.md);
|
|
140
|
+
const groupSizes = $derived(codeGroups(groups, length));
|
|
141
|
+
const grouped = $derived(groupSizes.length > 1);
|
|
142
|
+
/** Which group each box is in, for the drawing, and the first box of each. */
|
|
143
|
+
const groupStarts = $derived(groupSizes.map((_, index) => groupSizes.slice(0, index).reduce((sum, n) => sum + n, 0)));
|
|
144
|
+
const provided = zabiStringsFor("codeField");
|
|
145
|
+
const text = $derived(mergeStrings(DEFAULT_CODE_FIELD_STRINGS, provided(), strings));
|
|
146
|
+
const shownLabel = $derived(label || text.label);
|
|
147
|
+
|
|
148
|
+
const fallbackId = generateId("code");
|
|
149
|
+
const inputId = $derived(idProp ?? fallbackId);
|
|
150
|
+
const lengthId = $derived(`${inputId}-length`);
|
|
151
|
+
const status = $derived(fieldMessageState({ variant, message, hint, error }));
|
|
152
|
+
|
|
153
|
+
let input: HTMLInputElement | undefined = $state();
|
|
154
|
+
let cells: HTMLElement | undefined = $state();
|
|
155
|
+
let focused = $state(false);
|
|
156
|
+
let caret = $state(0);
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Without scripts the input is a plain bordered field showing what is
|
|
160
|
+
* typed, which posts like any other. Once the page is interactive it
|
|
161
|
+
* becomes the invisible layer over the boxes.
|
|
162
|
+
*/
|
|
163
|
+
let enhanced = $state(false);
|
|
164
|
+
$effect(() => {
|
|
165
|
+
enhanced = true;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const places = $derived(Array.from({ length }, (_, index) => value?.[index] ?? ""));
|
|
169
|
+
const active = $derived(enhanced && focused && !disabled && !readonly ? activePlace(caret, length) : -1);
|
|
170
|
+
|
|
171
|
+
const variantClass = $derived(
|
|
172
|
+
status.variant === "success"
|
|
173
|
+
? "border-success"
|
|
174
|
+
: status.variant === "warning"
|
|
175
|
+
? "border-warning"
|
|
176
|
+
: status.variant === "error"
|
|
177
|
+
? "border-error"
|
|
178
|
+
: "border-input-border",
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
/** The focus ring of `.focus-ring`, drawn on the box the caret is in. */
|
|
182
|
+
const RING = "shadow-[0_0_0_2px_var(--color-focus-ring-offset),0_0_0_4px_var(--color-focus-ring)]";
|
|
183
|
+
|
|
184
|
+
let complete = "";
|
|
185
|
+
|
|
186
|
+
/** Sets the code, and calls `oncomplete` when this makes it whole. */
|
|
187
|
+
function commit(next: string, caretAt: number) {
|
|
188
|
+
const previous = value ?? "";
|
|
189
|
+
if (next !== previous) value = next;
|
|
190
|
+
if (input) {
|
|
191
|
+
if (input.value !== next) input.value = next;
|
|
192
|
+
try {
|
|
193
|
+
input.setSelectionRange(caretAt, caretAt);
|
|
194
|
+
} catch {
|
|
195
|
+
// A browser that cannot place the caret: typing still works.
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
caret = caretAt;
|
|
199
|
+
if (next.length === length) {
|
|
200
|
+
if (next !== complete) {
|
|
201
|
+
complete = next;
|
|
202
|
+
oncomplete?.(next);
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
complete = "";
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function syncCaret() {
|
|
210
|
+
if (input) caret = input.selectionStart ?? input.value.length;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function handleInput(event: OnInput<Event>) {
|
|
214
|
+
oninput?.(event);
|
|
215
|
+
const field = event.currentTarget;
|
|
216
|
+
const raw = field.value;
|
|
217
|
+
const clean = sanitiseCode(raw, mode, length);
|
|
218
|
+
const at = caretAfterSanitise(raw, field.selectionStart ?? raw.length, mode, length);
|
|
219
|
+
commit(clean, Math.min(at, clean.length));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* A character typed over a filled place replaces it; one typed at the end
|
|
224
|
+
* is added. A character that does not belong never gets in.
|
|
225
|
+
*/
|
|
226
|
+
function handleKeydown(event: OnInput<KeyboardEvent>) {
|
|
227
|
+
restProps.onkeydown?.(event);
|
|
228
|
+
if (event.defaultPrevented || readonly || disabled) return;
|
|
229
|
+
if (event.ctrlKey || event.metaKey || event.altKey || event.key.length !== 1) return;
|
|
230
|
+
const field = event.currentTarget;
|
|
231
|
+
if (!isCodeChar(event.key, mode)) {
|
|
232
|
+
event.preventDefault();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const start = field.selectionStart ?? 0;
|
|
236
|
+
const end = field.selectionEnd ?? start;
|
|
237
|
+
if (start === end && start < field.value.length) {
|
|
238
|
+
event.preventDefault();
|
|
239
|
+
commit(replaceAt(field.value, start, event.key), start + 1);
|
|
240
|
+
} else if (start === end && field.value.length >= length) {
|
|
241
|
+
event.preventDefault();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Takes the whole code from the clipboard, wherever the caret is; "123 456" and "123-456" are 123456. */
|
|
246
|
+
function handlePaste(event: OnInput<ClipboardEvent>) {
|
|
247
|
+
restProps.onpaste?.(event);
|
|
248
|
+
if (event.defaultPrevented || readonly || disabled) return;
|
|
249
|
+
const field = event.currentTarget;
|
|
250
|
+
event.preventDefault();
|
|
251
|
+
const pasted = event.clipboardData?.getData("text") ?? "";
|
|
252
|
+
const start = field.selectionStart ?? field.value.length;
|
|
253
|
+
const end = field.selectionEnd ?? start;
|
|
254
|
+
const next = pasteInto(field.value, start, end, pasted, mode, length);
|
|
255
|
+
if (next === field.value && sanitiseCode(pasted, mode, length) === "") return;
|
|
256
|
+
commit(next, next.length === length ? length : Math.min(next.length, start + sanitiseCode(pasted, mode, length).length));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** A tap on a box puts the caret there, or at the end of the code when that box is still empty. */
|
|
260
|
+
function handleClick(event: OnInput<MouseEvent>) {
|
|
261
|
+
restProps.onclick?.(event);
|
|
262
|
+
if (!input || !cells) return;
|
|
263
|
+
const boxes = Array.from(cells.querySelectorAll<HTMLElement>("[data-code-cell]"));
|
|
264
|
+
let index = nearestBox(
|
|
265
|
+
boxes.map((box) => box.getBoundingClientRect()),
|
|
266
|
+
event.clientX,
|
|
267
|
+
event.clientY,
|
|
268
|
+
);
|
|
269
|
+
if (index < 0) return syncCaret();
|
|
270
|
+
index = Math.min(index, input.value.length);
|
|
271
|
+
try {
|
|
272
|
+
input.setSelectionRange(index, index);
|
|
273
|
+
} catch {
|
|
274
|
+
// Nothing to place.
|
|
275
|
+
}
|
|
276
|
+
syncCaret();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const fieldClass = $derived(
|
|
280
|
+
enhanced
|
|
281
|
+
? "absolute inset-0 size-full min-w-0 cursor-text border-0 bg-transparent p-0 text-base text-transparent caret-transparent outline-none selection:bg-transparent disabled:cursor-not-allowed"
|
|
282
|
+
: cn(
|
|
283
|
+
sizing.box,
|
|
284
|
+
"w-full rounded-control border bg-input px-3 text-center text-xl font-medium tracking-[0.35em] text-body tabular-nums hover:bg-input-hover disabled:cursor-not-allowed disabled:bg-input-disabled disabled:text-action-disabled-text focus-ring",
|
|
285
|
+
variantClass,
|
|
286
|
+
),
|
|
287
|
+
);
|
|
288
|
+
</script>
|
|
289
|
+
|
|
290
|
+
{#snippet cell(place: string, index: number)}
|
|
291
|
+
<span
|
|
292
|
+
data-code-cell
|
|
293
|
+
data-active={index === active ? "" : undefined}
|
|
294
|
+
style:max-inline-size="{sizing.maxRem}rem"
|
|
295
|
+
style:min-inline-size="{sizing.minEm}em"
|
|
296
|
+
class={cn(
|
|
297
|
+
"flex flex-1 items-center justify-center rounded-control border bg-input font-medium text-body tabular-nums transition-colors duration-150",
|
|
298
|
+
sizing.box,
|
|
299
|
+
sizing.text,
|
|
300
|
+
variantClass,
|
|
301
|
+
disabled && "cursor-not-allowed bg-input-disabled text-action-disabled-text",
|
|
302
|
+
index === active && RING,
|
|
303
|
+
)}
|
|
304
|
+
>
|
|
305
|
+
{place}
|
|
306
|
+
</span>
|
|
307
|
+
{/snippet}
|
|
308
|
+
|
|
309
|
+
<div class={cn("w-full", className)}>
|
|
310
|
+
<label for={inputId} class={cn("mb-2 block text-sm font-medium text-label", labelHidden && "sr-only")}>
|
|
311
|
+
{shownLabel}
|
|
312
|
+
</label>
|
|
313
|
+
<div
|
|
314
|
+
class="relative"
|
|
315
|
+
style:max-inline-size="calc({length} * {sizing.maxRem}rem + {length - 1 - Math.max(groupSizes.length - 1, 0)} * {CODE_FIELD_GAP_REM}rem + {Math.max(groupSizes.length - 1, 0)} * {CODE_FIELD_GROUP_GAP_REM}rem)"
|
|
316
|
+
bind:this={cells}
|
|
317
|
+
>
|
|
318
|
+
{#if enhanced}
|
|
319
|
+
<div
|
|
320
|
+
class={cn("flex flex-wrap", grouped ? "gap-x-4 gap-y-2" : "gap-2")}
|
|
321
|
+
aria-hidden="true"
|
|
322
|
+
data-code-cells
|
|
323
|
+
data-grouped={grouped ? "" : undefined}
|
|
324
|
+
>
|
|
325
|
+
{#if grouped}
|
|
326
|
+
{#each groupSizes as groupSize, group (group)}
|
|
327
|
+
<div
|
|
328
|
+
data-code-group
|
|
329
|
+
class="flex gap-2"
|
|
330
|
+
style:flex="{groupSize} 1 0"
|
|
331
|
+
style:min-inline-size="calc({groupSize} * {sizing.minEm * sizing.digitRem}rem + {groupSize - 1} * {CODE_FIELD_GAP_REM}rem)"
|
|
332
|
+
style:max-inline-size="calc({groupSize} * {sizing.maxRem}rem + {groupSize - 1} * {CODE_FIELD_GAP_REM}rem)"
|
|
333
|
+
>
|
|
334
|
+
{#each places.slice(groupStarts[group], groupStarts[group] + groupSize) as place, offset (offset)}
|
|
335
|
+
{@render cell(place, groupStarts[group] + offset)}
|
|
336
|
+
{/each}
|
|
337
|
+
</div>
|
|
338
|
+
{/each}
|
|
339
|
+
{:else}
|
|
340
|
+
{#each places as place, index (index)}
|
|
341
|
+
{@render cell(place, index)}
|
|
342
|
+
{/each}
|
|
343
|
+
{/if}
|
|
344
|
+
</div>
|
|
345
|
+
{/if}
|
|
346
|
+
<input
|
|
347
|
+
bind:this={input}
|
|
348
|
+
id={inputId}
|
|
349
|
+
type="text"
|
|
350
|
+
{name}
|
|
351
|
+
bind:value
|
|
352
|
+
{required}
|
|
353
|
+
{disabled}
|
|
354
|
+
{readonly}
|
|
355
|
+
inputmode={mode === "numeric" ? "numeric" : "text"}
|
|
356
|
+
autocomplete="one-time-code"
|
|
357
|
+
autocapitalize="off"
|
|
358
|
+
autocorrect="off"
|
|
359
|
+
spellcheck={false}
|
|
360
|
+
class={fieldClass}
|
|
361
|
+
aria-label={ariaLabel}
|
|
362
|
+
aria-invalid={status.variant === "error" ? "true" : undefined}
|
|
363
|
+
aria-required={required ? "true" : undefined}
|
|
364
|
+
aria-describedby={fieldDescribedBy(inputId, status, [describedBy, lengthId].filter(Boolean).join(" "))}
|
|
365
|
+
{...restProps}
|
|
366
|
+
oninput={handleInput}
|
|
367
|
+
onkeydown={handleKeydown}
|
|
368
|
+
onpaste={handlePaste}
|
|
369
|
+
onclick={handleClick}
|
|
370
|
+
onkeyup={syncCaret}
|
|
371
|
+
onselect={syncCaret}
|
|
372
|
+
onfocus={(event) => {
|
|
373
|
+
focused = true;
|
|
374
|
+
syncCaret();
|
|
375
|
+
restProps.onfocus?.(event);
|
|
376
|
+
}}
|
|
377
|
+
onblur={(event) => {
|
|
378
|
+
focused = false;
|
|
379
|
+
onblur?.(event);
|
|
380
|
+
}}
|
|
381
|
+
/>
|
|
382
|
+
</div>
|
|
383
|
+
<span id={lengthId} class="sr-only">{text.description(length, mode)}</span>
|
|
384
|
+
<FieldMessages fieldId={inputId} {status} {hint} />
|
|
385
|
+
</div>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { HTMLInputAttributes } from "svelte/elements";
|
|
2
|
+
import type { SemanticVariant } from "../types/variants.js";
|
|
3
|
+
import type { SizeVariant } from "../types/variants.js";
|
|
4
|
+
import { type CodeFieldMode, type CodeFieldStrings } from "../util/code-field.js";
|
|
5
|
+
/**
|
|
6
|
+
* A field for a one-time or verification code. One real `<input>` holds
|
|
7
|
+
* the code, so autofill from a message, paste, the numeric keyboard, form
|
|
8
|
+
* posting by `name` and a single accessible name all work as on `Input`;
|
|
9
|
+
* the boxes are drawn under it and hidden from assistive technology.
|
|
10
|
+
* Other attributes (`autocomplete`, `inputmode`, `data-*`, ...) land on
|
|
11
|
+
* the `<input>`.
|
|
12
|
+
*/
|
|
13
|
+
type Props = Omit<HTMLInputAttributes, "class" | "size" | "value" | "type" | "id" | "name" | "required" | "disabled" | "readonly" | "maxlength" | "oninput" | "onblur" | "aria-label" | "aria-describedby"> & {
|
|
14
|
+
/** Omit to auto-generate. */
|
|
15
|
+
id?: string;
|
|
16
|
+
/** The code typed so far. Bindable. */
|
|
17
|
+
value?: string;
|
|
18
|
+
/** The name the code is posted under, with or without scripts. */
|
|
19
|
+
name?: string;
|
|
20
|
+
class?: string;
|
|
21
|
+
/** Places in the code. A whole number from 1 to 12. */
|
|
22
|
+
length?: number;
|
|
23
|
+
/** `numeric` (default): digits only. `alphanumeric` also takes letters. */
|
|
24
|
+
mode?: CodeFieldMode;
|
|
25
|
+
/** Box size: `sm` (44px tall, a compact row), `md` (default), `lg`. */
|
|
26
|
+
size?: SizeVariant;
|
|
27
|
+
/**
|
|
28
|
+
* Draws the boxes in groups with a wider gap between them: a number is
|
|
29
|
+
* the size of each group (`4` makes 4 and 4 of 8), a list gives each
|
|
30
|
+
* group's size. Only the drawing changes: one input, one value, and a
|
|
31
|
+
* paste with spaces or dashes still fills it. A row that cannot fit at
|
|
32
|
+
* large text or a narrow width wraps between groups.
|
|
33
|
+
*/
|
|
34
|
+
groups?: number | number[];
|
|
35
|
+
/** Names the field. Defaults to the provider's `codeField.label`, then "Verification code". */
|
|
36
|
+
label?: string;
|
|
37
|
+
/** Keeps the label for assistive technology and takes it off the screen. */
|
|
38
|
+
labelHidden?: boolean;
|
|
39
|
+
required?: boolean;
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
readonly?: boolean;
|
|
42
|
+
variant?: SemanticVariant;
|
|
43
|
+
/** Status text under the field. Shown with a `success`, `warning` or `error` variant; for neutral help, use `hint`. */
|
|
44
|
+
message?: string;
|
|
45
|
+
/** Help text under the field, read out with it. */
|
|
46
|
+
hint?: string;
|
|
47
|
+
/** An error under the field. It marks the field invalid and is announced; it wins over `variant` and `message`. */
|
|
48
|
+
error?: string;
|
|
49
|
+
/** Called with the code when the last place is filled by typing, pasting or autofill. It does not submit anything. */
|
|
50
|
+
oncomplete?: (value: string) => void;
|
|
51
|
+
/** Overrides for the built-in strings: label, description. */
|
|
52
|
+
strings?: Partial<CodeFieldStrings>;
|
|
53
|
+
oninput?: (event: Event) => void;
|
|
54
|
+
onblur?: (event: Event) => void;
|
|
55
|
+
"aria-label"?: string;
|
|
56
|
+
/** Ids of other elements that describe the field. The length, the hint and the message are added after them. */
|
|
57
|
+
"aria-describedby"?: string | null;
|
|
58
|
+
};
|
|
59
|
+
declare const CodeField: import("svelte").Component<Props, {}, "value">;
|
|
60
|
+
type CodeField = ReturnType<typeof CodeField>;
|
|
61
|
+
export default CodeField;
|
|
@@ -488,6 +488,7 @@
|
|
|
488
488
|
use:setColorMapContainer
|
|
489
489
|
class="relative w-full h-48 rounded-container cursor-crosshair touch-none select-none {RING_WITHIN}"
|
|
490
490
|
role="group"
|
|
491
|
+
data-sheet-no-drag
|
|
491
492
|
aria-label={text.area}
|
|
492
493
|
onpointerdown={handleMapPointerDown}
|
|
493
494
|
onpointermove={handleMapPointerMove}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { HTMLInputAttributes } from "svelte/elements";
|
|
3
3
|
import type { SizeVariant } from "../types/variants.js";
|
|
4
|
+
import type { DateFieldStrings } from "../util/temporal-field.js";
|
|
4
5
|
import TemporalField from "./TemporalField.svelte";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -10,15 +11,28 @@
|
|
|
10
11
|
* calendar, Android its calendar, a desktop browser its own. `value` is
|
|
11
12
|
* always `YYYY-MM-DD`, or `""` while empty, whatever the field shows.
|
|
12
13
|
*
|
|
13
|
-
* What the field shows is the browser's decision
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* What the field shows is the browser's decision, unless it has a
|
|
15
|
+
* `locale`: without one, the date is in the format of the browser or
|
|
16
|
+
* device, not of the page, and no attribute changes that. To show a date
|
|
17
|
+
* as text in the page's language, use `formatDate`:
|
|
16
18
|
*
|
|
17
19
|
* ```svelte
|
|
18
20
|
* <DateField label="Date" bind:value={date} min="2026-01-01" />
|
|
19
21
|
* <p>{formatDate(date, "sv")}</p> <!-- 6 okt. 2026 -->
|
|
20
22
|
* ```
|
|
21
23
|
*
|
|
24
|
+
* With `locale`, the field shows the value in that language and its own
|
|
25
|
+
* picker opens: a Calendar in a BottomSheet (at every width), with Clear
|
|
26
|
+
* unless the field is `required`. Until the page has run, and without
|
|
27
|
+
* scripts, it is the native input as above and submits as before; once
|
|
28
|
+
* it has, the native input stays as the form control, out of sight, and a
|
|
29
|
+
* button in its place shows the date. `picker="native"` opens the
|
|
30
|
+
* platform's own picker instead.
|
|
31
|
+
*
|
|
32
|
+
* ```svelte
|
|
33
|
+
* <DateField label="Datum" locale="sv" bind:value format={{ weekday: "short", day: "numeric", month: "short" }} />
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
22
36
|
* Other attributes (`autocomplete`, `data-*`, `onchange`, ...) land on the
|
|
23
37
|
* `<input>`.
|
|
24
38
|
*/
|
|
@@ -53,6 +67,20 @@
|
|
|
53
67
|
size?: SizeVariant;
|
|
54
68
|
/** Extra classes for the `<input>`. */
|
|
55
69
|
class?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Language of the field, as a BCP 47 tag (`sv`, `en-GB`) or a list of them. With it the
|
|
72
|
+
* field shows the value in that language, whatever the browser's is, and opens the
|
|
73
|
+
* library's own picker; see above. Left out, the field is the native input.
|
|
74
|
+
*/
|
|
75
|
+
locale?: string | string[];
|
|
76
|
+
/** With `locale`: how the value is shown, as options of `Intl.DateTimeFormat`. Default: what formatDate gives for the locale. */
|
|
77
|
+
format?: Intl.DateTimeFormatOptions;
|
|
78
|
+
/** With `locale`: shown in the placeholder colour while the field is empty. */
|
|
79
|
+
placeholder?: string;
|
|
80
|
+
/** With `locale`: `library` (default) opens the library's calendar in a sheet; `native` opens the platform's own picker. */
|
|
81
|
+
picker?: "library" | "native";
|
|
82
|
+
/** With `locale`: the words the field says; replace any of them to translate. */
|
|
83
|
+
strings?: Partial<DateFieldStrings>;
|
|
56
84
|
};
|
|
57
85
|
|
|
58
86
|
let {
|
|
@@ -71,6 +99,11 @@
|
|
|
71
99
|
readonly = false,
|
|
72
100
|
size = "md",
|
|
73
101
|
class: className = "",
|
|
102
|
+
locale,
|
|
103
|
+
format,
|
|
104
|
+
placeholder,
|
|
105
|
+
picker = "library",
|
|
106
|
+
strings,
|
|
74
107
|
...restProps
|
|
75
108
|
}: Props = $props();
|
|
76
109
|
|
|
@@ -103,5 +136,10 @@
|
|
|
103
136
|
{readonly}
|
|
104
137
|
{size}
|
|
105
138
|
class={className}
|
|
139
|
+
{locale}
|
|
140
|
+
{format}
|
|
141
|
+
{placeholder}
|
|
142
|
+
{picker}
|
|
143
|
+
{strings}
|
|
106
144
|
{...restProps}
|
|
107
145
|
/>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { HTMLInputAttributes } from "svelte/elements";
|
|
2
2
|
import type { SizeVariant } from "../types/variants.js";
|
|
3
|
+
import type { DateFieldStrings } from "../util/temporal-field.js";
|
|
3
4
|
/**
|
|
4
5
|
* A date, picked with the device's own date picker and styled like Input.
|
|
5
6
|
*
|
|
@@ -7,15 +8,28 @@ import type { SizeVariant } from "../types/variants.js";
|
|
|
7
8
|
* calendar, Android its calendar, a desktop browser its own. `value` is
|
|
8
9
|
* always `YYYY-MM-DD`, or `""` while empty, whatever the field shows.
|
|
9
10
|
*
|
|
10
|
-
* What the field shows is the browser's decision
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* What the field shows is the browser's decision, unless it has a
|
|
12
|
+
* `locale`: without one, the date is in the format of the browser or
|
|
13
|
+
* device, not of the page, and no attribute changes that. To show a date
|
|
14
|
+
* as text in the page's language, use `formatDate`:
|
|
13
15
|
*
|
|
14
16
|
* ```svelte
|
|
15
17
|
* <DateField label="Date" bind:value={date} min="2026-01-01" />
|
|
16
18
|
* <p>{formatDate(date, "sv")}</p> <!-- 6 okt. 2026 -->
|
|
17
19
|
* ```
|
|
18
20
|
*
|
|
21
|
+
* With `locale`, the field shows the value in that language and its own
|
|
22
|
+
* picker opens: a Calendar in a BottomSheet (at every width), with Clear
|
|
23
|
+
* unless the field is `required`. Until the page has run, and without
|
|
24
|
+
* scripts, it is the native input as above and submits as before; once
|
|
25
|
+
* it has, the native input stays as the form control, out of sight, and a
|
|
26
|
+
* button in its place shows the date. `picker="native"` opens the
|
|
27
|
+
* platform's own picker instead.
|
|
28
|
+
*
|
|
29
|
+
* ```svelte
|
|
30
|
+
* <DateField label="Datum" locale="sv" bind:value format={{ weekday: "short", day: "numeric", month: "short" }} />
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
19
33
|
* Other attributes (`autocomplete`, `data-*`, `onchange`, ...) land on the
|
|
20
34
|
* `<input>`.
|
|
21
35
|
*/
|
|
@@ -47,6 +61,20 @@ type Props = Omit<HTMLInputAttributes, "class" | "type" | "value" | "size" | "mi
|
|
|
47
61
|
size?: SizeVariant;
|
|
48
62
|
/** Extra classes for the `<input>`. */
|
|
49
63
|
class?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Language of the field, as a BCP 47 tag (`sv`, `en-GB`) or a list of them. With it the
|
|
66
|
+
* field shows the value in that language, whatever the browser's is, and opens the
|
|
67
|
+
* library's own picker; see above. Left out, the field is the native input.
|
|
68
|
+
*/
|
|
69
|
+
locale?: string | string[];
|
|
70
|
+
/** With `locale`: how the value is shown, as options of `Intl.DateTimeFormat`. Default: what formatDate gives for the locale. */
|
|
71
|
+
format?: Intl.DateTimeFormatOptions;
|
|
72
|
+
/** With `locale`: shown in the placeholder colour while the field is empty. */
|
|
73
|
+
placeholder?: string;
|
|
74
|
+
/** With `locale`: `library` (default) opens the library's calendar in a sheet; `native` opens the platform's own picker. */
|
|
75
|
+
picker?: "library" | "native";
|
|
76
|
+
/** With `locale`: the words the field says; replace any of them to translate. */
|
|
77
|
+
strings?: Partial<DateFieldStrings>;
|
|
50
78
|
};
|
|
51
79
|
declare const DateField: import("svelte").Component<Props, {}, "value">;
|
|
52
80
|
type DateField = ReturnType<typeof DateField>;
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* <FloatingActionButton
|
|
21
21
|
* label="New quiz"
|
|
22
22
|
* onclick={create}
|
|
23
|
-
* style="--fab-bottom-offset:
|
|
23
|
+
* style="--fab-bottom-offset: 64px"
|
|
24
24
|
* />
|
|
25
25
|
* ```
|
|
26
26
|
*
|
|
@@ -125,8 +125,8 @@
|
|
|
125
125
|
extended ? "min-h-14 min-w-14" : "size-[56px]",
|
|
126
126
|
// Transparent until forced-colors mode draws it: the fill is gone there.
|
|
127
127
|
"border border-transparent",
|
|
128
|
-
"bg-action-primary text-action-primary",
|
|
129
|
-
"transition-[background-color,scale] duration-
|
|
128
|
+
"bg-action-primary bg-control-gradient text-action-primary",
|
|
129
|
+
"transition-[background-color,scale] duration-(--duration-base) active:scale-[0.96] motion-reduce:transition-none motion-reduce:active:scale-100",
|
|
130
130
|
extended ? "px-5 py-2 text-sm font-medium" : "p-0",
|
|
131
131
|
// Never wider than the screen it floats on, margins included.
|
|
132
132
|
"[max-inline-size:calc(100%_-_32px)]",
|
|
@@ -156,6 +156,7 @@
|
|
|
156
156
|
class={classes}
|
|
157
157
|
aria-label={extended ? undefined : label}
|
|
158
158
|
data-position={position}
|
|
159
|
+
data-toast-clear
|
|
159
160
|
{...restProps}
|
|
160
161
|
>
|
|
161
162
|
{@render content()}
|
|
@@ -167,6 +168,7 @@
|
|
|
167
168
|
class={classes}
|
|
168
169
|
aria-label={extended ? undefined : label}
|
|
169
170
|
data-position={position}
|
|
171
|
+
data-toast-clear
|
|
170
172
|
{...restProps}
|
|
171
173
|
>
|
|
172
174
|
{@render content()}
|