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
package/dist/atoms/Rating.svelte
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import Star from "@lucide/svelte/icons/star";
|
|
5
5
|
import X from "@lucide/svelte/icons/x";
|
|
6
6
|
import type { HTMLAttributes } from "svelte/elements";
|
|
7
|
-
import
|
|
7
|
+
import { isDevBuild } from "../util/app-shell.js";
|
|
8
8
|
import { cn } from "../util/cn.js";
|
|
9
9
|
import { resetValueOf, groupBinding } from "../util/hydration.js";
|
|
10
10
|
import { isRtl, radioKeyIndex } from "../util/radio-keys.js";
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
} from "../util/rating.js";
|
|
19
19
|
import { generateId } from "../util/ssr-safe.js";
|
|
20
20
|
|
|
21
|
+
/** Sizes of a rating. `xl` and `display` are for a large input or a compact score. */
|
|
22
|
+
type RatingSize = "sm" | "md" | "lg" | "xl" | "display";
|
|
23
|
+
|
|
21
24
|
/** Other attributes (`data-*`, `id`, ...) land on the host element. */
|
|
22
25
|
type Props = Omit<
|
|
23
26
|
HTMLAttributes<HTMLDivElement>,
|
|
@@ -37,8 +40,12 @@
|
|
|
37
40
|
"aria-label"?: string;
|
|
38
41
|
/** Id of the element that names the rating, when there is no `label`. */
|
|
39
42
|
"aria-labelledby"?: string;
|
|
40
|
-
/**
|
|
41
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Size of a star. An interactive star is a 44px target at every size.
|
|
45
|
+
* `xl` is 40px stars. `display` is a display-size score with `compact`
|
|
46
|
+
* (a 40px star and a 48px number); as a row of stars it is `xl`.
|
|
47
|
+
*/
|
|
48
|
+
size?: RatingSize;
|
|
42
49
|
/**
|
|
43
50
|
* Colour of a filled star: the primary action colour, or the app's
|
|
44
51
|
* accent (`--color-accent`). An app can also set the colours itself:
|
|
@@ -62,6 +69,22 @@
|
|
|
62
69
|
* around it: it submits nothing and a form reset leaves it alone.
|
|
63
70
|
*/
|
|
64
71
|
name?: string;
|
|
72
|
+
/**
|
|
73
|
+
* A score for a list, not a control: one filled star and the number
|
|
74
|
+
* ("4,0"), from the real `value` and `max`, with one accessible name
|
|
75
|
+
* ("4 of 5 stars"). Implies `readonly`: `clearable`, `name` and
|
|
76
|
+
* `onchange` are ignored, and `showValue` too (the number is the point).
|
|
77
|
+
*/
|
|
78
|
+
compact?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* What an empty star looks like: a hollow `outline` (default), or a
|
|
81
|
+
* `soft` fill (no outline) in `--zabi-rating-off-fill`, falling back to
|
|
82
|
+
* `--color-control-track`. The soft fill is below 3:1 against the page
|
|
83
|
+
* by default, so it suits a rating that also has a visible label and
|
|
84
|
+
* value. `--zabi-rating-off-fill` sets the colour; it is declared
|
|
85
|
+
* nowhere, like `--zabi-rating-on`. In forced colours it is an outline.
|
|
86
|
+
*/
|
|
87
|
+
empty?: "outline" | "soft";
|
|
65
88
|
/** Shows the number beside the stars. On by default when `readonly`. */
|
|
66
89
|
showValue?: boolean;
|
|
67
90
|
/** Turns the value into text for a locale ("3,5"). At most one decimal by default. */
|
|
@@ -86,6 +109,8 @@
|
|
|
86
109
|
clearable = false,
|
|
87
110
|
disabled = false,
|
|
88
111
|
name = "",
|
|
112
|
+
compact = false,
|
|
113
|
+
empty = "outline",
|
|
89
114
|
showValue,
|
|
90
115
|
formatValue,
|
|
91
116
|
strings,
|
|
@@ -104,6 +129,13 @@
|
|
|
104
129
|
applyDefaults();
|
|
105
130
|
$effect.pre(applyDefaults);
|
|
106
131
|
|
|
132
|
+
$effect(() => {
|
|
133
|
+
if (!isDevBuild() || !compact || !(clearable || name)) return;
|
|
134
|
+
console.warn(
|
|
135
|
+
"[zabi-components] Rating: `compact` is a read-only score; `clearable` and `name` are ignored.",
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
107
139
|
const baseId = generateId("rating");
|
|
108
140
|
const labelId = `${baseId}-label`;
|
|
109
141
|
const valueId = `${baseId}-value`;
|
|
@@ -127,6 +159,9 @@
|
|
|
127
159
|
const shown = $derived(clampRating(value, count));
|
|
128
160
|
const valueShown = $derived(showValue ?? readonly);
|
|
129
161
|
const labelVisible = $derived(!!label && !hideLabel);
|
|
162
|
+
/** A row of stars has no `display` size: it is `xl`. */
|
|
163
|
+
const starSize = $derived(size === "display" ? "xl" : size);
|
|
164
|
+
const softEmpty = $derived(empty === "soft" ? "soft" : undefined);
|
|
130
165
|
|
|
131
166
|
function format(current: number): string {
|
|
132
167
|
return formatValue ? formatValue(current) : defaultRatingFormat(current);
|
|
@@ -214,13 +249,48 @@
|
|
|
214
249
|
</span>
|
|
215
250
|
{/snippet}
|
|
216
251
|
|
|
217
|
-
{#if
|
|
252
|
+
{#if compact}
|
|
253
|
+
<!-- A score: one star and the number, one image with one name as `readonly`. -->
|
|
254
|
+
<div
|
|
255
|
+
class={cn("rating inline-flex items-center", className)}
|
|
256
|
+
data-size={size}
|
|
257
|
+
data-tone={tone}
|
|
258
|
+
data-empty={softEmpty}
|
|
259
|
+
data-compact
|
|
260
|
+
data-readonly
|
|
261
|
+
role="img"
|
|
262
|
+
aria-label={ariaLabelledby
|
|
263
|
+
? undefined
|
|
264
|
+
: [label || ariaLabel, valueLabel].filter(Boolean).join(", ")}
|
|
265
|
+
aria-labelledby={ariaLabelledby ? `${ariaLabelledby} ${valueId}` : undefined}
|
|
266
|
+
{...restProps}
|
|
267
|
+
>
|
|
268
|
+
{#if labelVisible}
|
|
269
|
+
<span class="me-2 text-sm font-medium text-label">{label}</span>
|
|
270
|
+
{/if}
|
|
271
|
+
{@render glyph(shown === null ? 0 : 1)}
|
|
272
|
+
<span
|
|
273
|
+
class={cn(
|
|
274
|
+
"rating-compact-value font-semibold tabular-nums",
|
|
275
|
+
size === "display" && "font-display font-bold",
|
|
276
|
+
shown === null ? "text-description" : "text-headline",
|
|
277
|
+
)}
|
|
278
|
+
data-rating-value
|
|
279
|
+
>
|
|
280
|
+
{shown === null ? text.noRating : valueText}
|
|
281
|
+
</span>
|
|
282
|
+
{#if ariaLabelledby}
|
|
283
|
+
<span id={valueId} class="sr-only">{valueLabel}</span>
|
|
284
|
+
{/if}
|
|
285
|
+
</div>
|
|
286
|
+
{:else if readonly}
|
|
218
287
|
<!-- One image with one name. Its children, the visible label and number
|
|
219
288
|
included, are presentational, so nothing is read twice. -->
|
|
220
289
|
<div
|
|
221
290
|
class={cn("rating", className)}
|
|
222
|
-
data-size={
|
|
291
|
+
data-size={starSize}
|
|
223
292
|
data-tone={tone}
|
|
293
|
+
data-empty={softEmpty}
|
|
224
294
|
data-readonly
|
|
225
295
|
role="img"
|
|
226
296
|
aria-label={ariaLabelledby
|
|
@@ -249,8 +319,9 @@
|
|
|
249
319
|
{:else}
|
|
250
320
|
<div
|
|
251
321
|
class={cn("rating", disabled && "opacity-50", className)}
|
|
252
|
-
data-size={
|
|
322
|
+
data-size={starSize}
|
|
253
323
|
data-tone={tone}
|
|
324
|
+
data-empty={softEmpty}
|
|
254
325
|
{...restProps}
|
|
255
326
|
>
|
|
256
327
|
{#if labelVisible}
|
|
@@ -371,6 +442,49 @@
|
|
|
371
442
|
--zabi-rating-target: max(48px, var(--zabi-rating-star) + 8px);
|
|
372
443
|
}
|
|
373
444
|
|
|
445
|
+
.rating[data-size="xl"] {
|
|
446
|
+
--zabi-rating-star: 2.5rem;
|
|
447
|
+
--zabi-rating-target: max(48px, var(--zabi-rating-star) + 8px);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/* A compact score: the star and the number share one size per step. */
|
|
451
|
+
.rating[data-compact] {
|
|
452
|
+
--zabi-rating-text: 1.5rem;
|
|
453
|
+
gap: 4px;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.rating[data-compact][data-size="sm"] {
|
|
457
|
+
--zabi-rating-star: 14px;
|
|
458
|
+
--zabi-rating-text: 14px;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.rating[data-compact][data-size="md"] {
|
|
462
|
+
--zabi-rating-star: 16px;
|
|
463
|
+
--zabi-rating-text: 16px;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.rating[data-compact][data-size="lg"] {
|
|
467
|
+
--zabi-rating-star: 20px;
|
|
468
|
+
--zabi-rating-text: 20px;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.rating[data-compact][data-size="xl"] {
|
|
472
|
+
--zabi-rating-star: 28px;
|
|
473
|
+
--zabi-rating-text: 30px;
|
|
474
|
+
gap: 6px;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.rating[data-compact][data-size="display"] {
|
|
478
|
+
--zabi-rating-star: 40px;
|
|
479
|
+
--zabi-rating-text: 48px;
|
|
480
|
+
gap: 6px;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.rating-compact-value {
|
|
484
|
+
font-size: var(--zabi-rating-text);
|
|
485
|
+
line-height: 1;
|
|
486
|
+
}
|
|
487
|
+
|
|
374
488
|
.rating-stars {
|
|
375
489
|
display: inline-flex;
|
|
376
490
|
flex-wrap: wrap;
|
|
@@ -424,6 +538,12 @@
|
|
|
424
538
|
color: var(--zabi-rating-off, var(--_rating-off));
|
|
425
539
|
}
|
|
426
540
|
|
|
541
|
+
/* A soft empty star is a filled shape with no outline. */
|
|
542
|
+
.rating[data-empty="soft"] .rating-glyph :global(.rating-glyph-empty) {
|
|
543
|
+
fill: var(--zabi-rating-off-fill, var(--color-control-track));
|
|
544
|
+
stroke: none;
|
|
545
|
+
}
|
|
546
|
+
|
|
427
547
|
/* Anchored at the start, so the filled part is on the right in a right-to-left layout. */
|
|
428
548
|
.rating-glyph-fill {
|
|
429
549
|
position: absolute;
|
|
@@ -484,6 +604,12 @@
|
|
|
484
604
|
.rating-glyph :global(.rating-glyph-empty) {
|
|
485
605
|
color: CanvasText;
|
|
486
606
|
}
|
|
607
|
+
|
|
608
|
+
/* A soft star is hollow here, so it still differs from a filled one. */
|
|
609
|
+
.rating[data-empty="soft"] .rating-glyph :global(.rating-glyph-empty) {
|
|
610
|
+
fill: none;
|
|
611
|
+
stroke: CanvasText;
|
|
612
|
+
}
|
|
487
613
|
}
|
|
488
614
|
|
|
489
615
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { HTMLAttributes } from "svelte/elements";
|
|
2
|
-
import type { SizeVariant } from "../types/variants.js";
|
|
3
2
|
import { type RatingStrings } from "../util/rating.js";
|
|
3
|
+
/** Sizes of a rating. `xl` and `display` are for a large input or a compact score. */
|
|
4
|
+
type RatingSize = "sm" | "md" | "lg" | "xl" | "display";
|
|
4
5
|
/** Other attributes (`data-*`, `id`, ...) land on the host element. */
|
|
5
6
|
type Props = Omit<HTMLAttributes<HTMLDivElement>, "class" | "onchange" | "aria-label" | "aria-labelledby"> & {
|
|
6
7
|
/** Extra classes for the host element. */
|
|
@@ -17,8 +18,12 @@ type Props = Omit<HTMLAttributes<HTMLDivElement>, "class" | "onchange" | "aria-l
|
|
|
17
18
|
"aria-label"?: string;
|
|
18
19
|
/** Id of the element that names the rating, when there is no `label`. */
|
|
19
20
|
"aria-labelledby"?: string;
|
|
20
|
-
/**
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Size of a star. An interactive star is a 44px target at every size.
|
|
23
|
+
* `xl` is 40px stars. `display` is a display-size score with `compact`
|
|
24
|
+
* (a 40px star and a 48px number); as a row of stars it is `xl`.
|
|
25
|
+
*/
|
|
26
|
+
size?: RatingSize;
|
|
22
27
|
/**
|
|
23
28
|
* Colour of a filled star: the primary action colour, or the app's
|
|
24
29
|
* accent (`--color-accent`). An app can also set the colours itself:
|
|
@@ -42,6 +47,22 @@ type Props = Omit<HTMLAttributes<HTMLDivElement>, "class" | "onchange" | "aria-l
|
|
|
42
47
|
* around it: it submits nothing and a form reset leaves it alone.
|
|
43
48
|
*/
|
|
44
49
|
name?: string;
|
|
50
|
+
/**
|
|
51
|
+
* A score for a list, not a control: one filled star and the number
|
|
52
|
+
* ("4,0"), from the real `value` and `max`, with one accessible name
|
|
53
|
+
* ("4 of 5 stars"). Implies `readonly`: `clearable`, `name` and
|
|
54
|
+
* `onchange` are ignored, and `showValue` too (the number is the point).
|
|
55
|
+
*/
|
|
56
|
+
compact?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* What an empty star looks like: a hollow `outline` (default), or a
|
|
59
|
+
* `soft` fill (no outline) in `--zabi-rating-off-fill`, falling back to
|
|
60
|
+
* `--color-control-track`. The soft fill is below 3:1 against the page
|
|
61
|
+
* by default, so it suits a rating that also has a visible label and
|
|
62
|
+
* value. `--zabi-rating-off-fill` sets the colour; it is declared
|
|
63
|
+
* nowhere, like `--zabi-rating-on`. In forced colours it is an outline.
|
|
64
|
+
*/
|
|
65
|
+
empty?: "outline" | "soft";
|
|
45
66
|
/** Shows the number beside the stars. On by default when `readonly`. */
|
|
46
67
|
showValue?: boolean;
|
|
47
68
|
/** Turns the value into text for a locale ("3,5"). At most one decimal by default. */
|
package/dist/atoms/Select.svelte
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { generateId } from "../util/ssr-safe.js";
|
|
18
18
|
import { isInsideToastRegion } from "../util/focus-utils.js";
|
|
19
19
|
import { cn } from "../util/cn.js";
|
|
20
|
+
import { FIELD_CONTROL_SURFACE, fieldControlEdge, fieldControlSize } from "../util/field-control.js";
|
|
20
21
|
import { fieldDescribedBy, fieldMessageState } from "../util/field.js";
|
|
21
22
|
import FieldMessages from "./FieldMessages.svelte";
|
|
22
23
|
|
|
@@ -54,6 +55,12 @@
|
|
|
54
55
|
value: string | number;
|
|
55
56
|
label: string;
|
|
56
57
|
disabled?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* The language of the label, as a BCP 47 tag (`"en"`), when it is
|
|
60
|
+
* not the page's (WCAG 3.1.2). Set on the option's row, on the
|
|
61
|
+
* trigger's text while it is chosen, and on the native `<option>`.
|
|
62
|
+
*/
|
|
63
|
+
lang?: string;
|
|
57
64
|
}>;
|
|
58
65
|
searchable?: boolean;
|
|
59
66
|
/**
|
|
@@ -76,6 +83,12 @@
|
|
|
76
83
|
* no search field, and none of the loading or empty states.
|
|
77
84
|
*/
|
|
78
85
|
presentation?: SelectPresentation;
|
|
86
|
+
/**
|
|
87
|
+
* In a sheet, a short list (up to six options) without a search field
|
|
88
|
+
* gets a sheet as tall as the list, not half the screen. `false`
|
|
89
|
+
* keeps the half-height sheet.
|
|
90
|
+
*/
|
|
91
|
+
sheetFit?: boolean;
|
|
79
92
|
noResultsText?: string;
|
|
80
93
|
isLoading?: boolean;
|
|
81
94
|
loadingText?: string;
|
|
@@ -118,6 +131,7 @@
|
|
|
118
131
|
maxMenuHeight = "60dvh",
|
|
119
132
|
menuWidth = "100%",
|
|
120
133
|
presentation = "auto",
|
|
134
|
+
sheetFit = true,
|
|
121
135
|
noResultsText,
|
|
122
136
|
isLoading = false,
|
|
123
137
|
loadingText,
|
|
@@ -185,32 +199,12 @@
|
|
|
185
199
|
let selectContainer: HTMLDivElement;
|
|
186
200
|
let searchQuery = $state("");
|
|
187
201
|
|
|
188
|
-
|
|
189
|
-
// and the same 16px text below `sm`, so a Select beside an Input matches it.
|
|
190
|
-
const sizeClass = $derived(() => {
|
|
191
|
-
// A minimum height, so a label that wraps makes the trigger taller
|
|
192
|
-
// instead of being cut. One line is exactly 32, 40 or 48px: the
|
|
193
|
-
// padding leaves room for the line and the border inside it, and is
|
|
194
|
-
// a step smaller below `sm`, where the text is 16px on a 24px line. `native` is the same box for the native
|
|
195
|
-
// select, which is always one line.
|
|
196
|
-
if (size === "sm") return { box: "min-h-8 px-3 py-1 max-sm:py-0 pointer-coarse:min-h-11", text: "text-sm max-sm:text-base", native: "h-8 ps-3 pe-9 pointer-coarse:h-11", chevron: "end-3" };
|
|
197
|
-
if (size === "lg") return { box: "min-h-12 px-4 py-2", text: "text-base", native: "h-12 ps-4 pe-10", chevron: "end-4" };
|
|
198
|
-
return { box: "min-h-10 px-3 py-2 max-sm:py-1 pointer-coarse:min-h-11", text: "text-sm max-sm:text-base", native: "h-10 ps-3 pe-9 pointer-coarse:h-11", chevron: "end-3" };
|
|
199
|
-
});
|
|
202
|
+
const sizeClass = $derived(() => fieldControlSize(size));
|
|
200
203
|
|
|
201
|
-
const variantClass = $derived(() =>
|
|
202
|
-
return status.variant === "success"
|
|
203
|
-
? "border-success focus-visible:border-success"
|
|
204
|
-
: status.variant === "warning"
|
|
205
|
-
? "border-warning focus-visible:border-warning"
|
|
206
|
-
: status.variant === "error"
|
|
207
|
-
? "border-error focus-visible:border-error"
|
|
208
|
-
: "border-input-border enabled:hover:border-input-border-hover";
|
|
209
|
-
});
|
|
204
|
+
const variantClass = $derived(() => fieldControlEdge(status.variant));
|
|
210
205
|
|
|
211
206
|
/** What the trigger and the visible native select share: the field's surface, edge and states. */
|
|
212
|
-
const fieldSurface =
|
|
213
|
-
"focus-ring w-full min-w-0 cursor-pointer rounded-control border bg-input text-body transition-colors duration-150 hover:bg-input-hover active:bg-input-active focus-visible:bg-input-focus focus:outline-none focus-visible:outline-none disabled:cursor-not-allowed disabled:bg-input-disabled disabled:text-action-disabled-text";
|
|
207
|
+
const fieldSurface = FIELD_CONTROL_SURFACE;
|
|
214
208
|
|
|
215
209
|
const triggerClasses = $derived(() => {
|
|
216
210
|
const sizeStyles = sizeClass();
|
|
@@ -501,6 +495,7 @@
|
|
|
501
495
|
<option
|
|
502
496
|
value={String(option.value)}
|
|
503
497
|
disabled={option.disabled}
|
|
498
|
+
lang={option.lang}
|
|
504
499
|
selected={isSameValue(value, option.value)}
|
|
505
500
|
>
|
|
506
501
|
{option.label}
|
|
@@ -536,6 +531,7 @@
|
|
|
536
531
|
sheetExpandLabel={text.expandLabel}
|
|
537
532
|
sheetCollapseLabel={text.collapseLabel}
|
|
538
533
|
sheetSnap={searchable && options.length > 6 ? "full" : "half"}
|
|
534
|
+
sheetFit={sheetFit && !searchable && options.length <= 6}
|
|
539
535
|
>
|
|
540
536
|
{#snippet trigger(aria)}
|
|
541
537
|
<!-- A select-only combobox, as the ARIA practices describe it:
|
|
@@ -568,6 +564,7 @@
|
|
|
568
564
|
control to read the rest. The chevron stays in the middle of
|
|
569
565
|
the box, however many lines there are. -->
|
|
570
566
|
<span
|
|
567
|
+
lang={isLoading || isEmpty() ? undefined : selectedOption?.lang}
|
|
571
568
|
class="min-w-0 flex-1 text-start [overflow-wrap:anywhere] {disabled
|
|
572
569
|
? ''
|
|
573
570
|
: isLoading
|
|
@@ -586,7 +583,7 @@
|
|
|
586
583
|
</span>
|
|
587
584
|
<ChevronDown
|
|
588
585
|
size={20}
|
|
589
|
-
class="shrink-0 text-description transition-transform duration-
|
|
586
|
+
class="shrink-0 text-description transition-transform duration-(--duration-moderate) motion-reduce:transition-none {isOpen
|
|
590
587
|
? 'rotate-180'
|
|
591
588
|
: ''}"
|
|
592
589
|
/>
|
|
@@ -614,7 +611,7 @@
|
|
|
614
611
|
{#snippet children()}
|
|
615
612
|
<!-- `px-1` twice: the inner one is room for an option's focus
|
|
616
613
|
ring inside the scrolling box, and together with the panel's
|
|
617
|
-
|
|
614
|
+
1px rim they put the options 9px from its corner. -->
|
|
618
615
|
<div class="px-1 pb-2 pt-1" style:width={menuWidth} data-menu-width>
|
|
619
616
|
<div
|
|
620
617
|
class="overflow-y-auto px-1"
|
|
@@ -638,7 +635,7 @@
|
|
|
638
635
|
type="button"
|
|
639
636
|
role="option"
|
|
640
637
|
aria-selected={isSameValue(value, option.value)}
|
|
641
|
-
class="focus-ring flex w-full items-center justify-start gap-2 rounded-control border-2 px-3 py-2 pointer-coarse:py-3 text-start text-sm font-medium transition-colors focus:outline-none aria-disabled:cursor-not-allowed aria-disabled:opacity-50 {isSameValue(
|
|
638
|
+
class="focus-ring flex w-full items-center justify-start gap-2 rounded-control border-2 px-3 py-2 pointer-coarse:py-3 text-start text-sm font-medium transition-colors focus:outline-none cursor-pointer aria-disabled:cursor-not-allowed aria-disabled:opacity-50 {isSameValue(
|
|
642
639
|
value,
|
|
643
640
|
option.value,
|
|
644
641
|
)
|
|
@@ -647,6 +644,7 @@
|
|
|
647
644
|
? 'border-transparent bg-transparent text-body'
|
|
648
645
|
: 'border-transparent bg-transparent text-body hover:bg-surface-overlay-hover active:bg-surface-active'}"
|
|
649
646
|
aria-disabled={option.disabled ? "true" : undefined}
|
|
647
|
+
lang={option.lang}
|
|
650
648
|
{...buttonRestProps}
|
|
651
649
|
onclick={() =>
|
|
652
650
|
handleOptionClick(option.value)}
|
|
@@ -23,6 +23,12 @@ type Props = Omit<HTMLButtonAttributes, "class" | "value" | "name" | "disabled"
|
|
|
23
23
|
value: string | number;
|
|
24
24
|
label: string;
|
|
25
25
|
disabled?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* The language of the label, as a BCP 47 tag (`"en"`), when it is
|
|
28
|
+
* not the page's (WCAG 3.1.2). Set on the option's row, on the
|
|
29
|
+
* trigger's text while it is chosen, and on the native `<option>`.
|
|
30
|
+
*/
|
|
31
|
+
lang?: string;
|
|
26
32
|
}>;
|
|
27
33
|
searchable?: boolean;
|
|
28
34
|
/**
|
|
@@ -45,6 +51,12 @@ type Props = Omit<HTMLButtonAttributes, "class" | "value" | "name" | "disabled"
|
|
|
45
51
|
* no search field, and none of the loading or empty states.
|
|
46
52
|
*/
|
|
47
53
|
presentation?: SelectPresentation;
|
|
54
|
+
/**
|
|
55
|
+
* In a sheet, a short list (up to six options) without a search field
|
|
56
|
+
* gets a sheet as tall as the list, not half the screen. `false`
|
|
57
|
+
* keeps the half-height sheet.
|
|
58
|
+
*/
|
|
59
|
+
sheetFit?: boolean;
|
|
48
60
|
noResultsText?: string;
|
|
49
61
|
isLoading?: boolean;
|
|
50
62
|
loadingText?: string;
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
import { generateId } from "../util/ssr-safe.js";
|
|
5
5
|
import { cn } from "../util/cn.js";
|
|
6
6
|
import { groupBinding } from "../util/hydration.js";
|
|
7
|
+
import { fieldDescribedBy, fieldMessageState } from "../util/field.js";
|
|
8
|
+
import type { SemanticVariant } from "../types/variants.js";
|
|
9
|
+
import FieldMessages from "./FieldMessages.svelte";
|
|
7
10
|
import {
|
|
8
11
|
SELECTION_CONTROL_INPUT,
|
|
9
12
|
SELECTION_CONTROL_LABEL_ROW,
|
|
@@ -39,6 +42,30 @@
|
|
|
39
42
|
onChange?: (event: Event) => void;
|
|
40
43
|
onchange?: (event: Event) => void;
|
|
41
44
|
mark?: Snippet<[SelectionControlMarkProps]>;
|
|
45
|
+
/**
|
|
46
|
+
* Drawn inside the one label, between the control and the label text,
|
|
47
|
+
* as decoration for the name `label` already gives. A leading image
|
|
48
|
+
* should have an empty alt: the label names the row.
|
|
49
|
+
*/
|
|
50
|
+
leading?: Snippet;
|
|
51
|
+
/**
|
|
52
|
+
* A line under the label, tied to the input with `aria-describedby`.
|
|
53
|
+
* It is not part of the name: the label names the control.
|
|
54
|
+
*/
|
|
55
|
+
description?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Where the control sits in the row. `end` puts it after the leading,
|
|
58
|
+
* the label and the description, at the far edge; the row is still
|
|
59
|
+
* one label and fills the width it is given.
|
|
60
|
+
*/
|
|
61
|
+
controlPosition?: "start" | "end";
|
|
62
|
+
variant?: SemanticVariant;
|
|
63
|
+
/** Status text under the control. Shown with a `success`, `warning` or `error` variant; for neutral help, use `hint`. */
|
|
64
|
+
message?: string;
|
|
65
|
+
/** Help text under the control, read out with it. */
|
|
66
|
+
hint?: string;
|
|
67
|
+
/** An error under the control. It marks the input invalid and is announced; it wins over `variant` and `message`. */
|
|
68
|
+
error?: string;
|
|
42
69
|
};
|
|
43
70
|
|
|
44
71
|
let {
|
|
@@ -56,6 +83,13 @@
|
|
|
56
83
|
onChange,
|
|
57
84
|
onchange,
|
|
58
85
|
mark,
|
|
86
|
+
leading,
|
|
87
|
+
description,
|
|
88
|
+
controlPosition = "start",
|
|
89
|
+
variant = "default",
|
|
90
|
+
message = "",
|
|
91
|
+
hint = "",
|
|
92
|
+
error = "",
|
|
59
93
|
...restProps
|
|
60
94
|
}: Props = $props();
|
|
61
95
|
|
|
@@ -75,9 +109,34 @@
|
|
|
75
109
|
const controlId = $derived(idProp ?? fallbackId);
|
|
76
110
|
const isDisabled = $derived(disabled || loading);
|
|
77
111
|
|
|
78
|
-
const
|
|
112
|
+
const descriptionId = $derived(`${controlId}-description`);
|
|
113
|
+
const status = $derived(fieldMessageState({ variant, message, hint, error }));
|
|
114
|
+
const hasFieldText = $derived(status.showMessage || status.showHint);
|
|
115
|
+
// The caller's ids, then the description, the hint and the message.
|
|
116
|
+
const describedBy = $derived(
|
|
117
|
+
fieldDescribedBy(
|
|
118
|
+
controlId,
|
|
119
|
+
status,
|
|
120
|
+
[restProps["aria-describedby"], description ? descriptionId : undefined]
|
|
121
|
+
.filter(Boolean)
|
|
122
|
+
.join(" "),
|
|
123
|
+
),
|
|
124
|
+
);
|
|
125
|
+
// `aria-invalid` is not supported by the radio role: a radio's error is said by its description.
|
|
126
|
+
const invalid = $derived(status.variant === "error" ? "true" : undefined);
|
|
127
|
+
|
|
128
|
+
// With a description the row is two lines: the box sits at the top, as in RadioGroup.
|
|
129
|
+
// At the end the control is moved by `order`, so the markup and what a
|
|
130
|
+
// screen reader meets first are the same in both positions.
|
|
131
|
+
const controlAtEnd = $derived(controlPosition === "end");
|
|
132
|
+
const labelWrapperClasses = $derived(
|
|
133
|
+
cn(SELECTION_CONTROL_LABEL_ROW, description && "items-start", controlAtEnd && "min-w-0 flex-1"),
|
|
134
|
+
);
|
|
79
135
|
|
|
80
|
-
const controlContainerClasses = $derived(
|
|
136
|
+
const controlContainerClasses = $derived(
|
|
137
|
+
cn(selectionControlShellClasses(shape), description && "mt-0.5", controlAtEnd && "order-last"),
|
|
138
|
+
);
|
|
139
|
+
const labelTextClasses = $derived(controlAtEnd ? "min-w-0 flex-1" : "");
|
|
81
140
|
|
|
82
141
|
const controlRingClasses = $derived(selectionControlRingOverlayClasses(shape));
|
|
83
142
|
|
|
@@ -132,7 +191,7 @@
|
|
|
132
191
|
}
|
|
133
192
|
</script>
|
|
134
193
|
|
|
135
|
-
|
|
194
|
+
{#snippet row()}
|
|
136
195
|
<label for={controlId} class={labelWrapperClasses}>
|
|
137
196
|
<span class={controlContainerClasses}>
|
|
138
197
|
<!-- One branch per type: a binding needs a type it can read. -->
|
|
@@ -149,6 +208,8 @@
|
|
|
149
208
|
aria-busy={loading ? "true" : undefined}
|
|
150
209
|
onchange={handleChange}
|
|
151
210
|
{...restProps}
|
|
211
|
+
aria-describedby={describedBy}
|
|
212
|
+
aria-invalid={invalid ?? restProps["aria-invalid"]}
|
|
152
213
|
/>
|
|
153
214
|
{:else}
|
|
154
215
|
<input
|
|
@@ -163,6 +224,7 @@
|
|
|
163
224
|
aria-busy={loading ? "true" : undefined}
|
|
164
225
|
onchange={handleChange}
|
|
165
226
|
{...restProps}
|
|
227
|
+
aria-describedby={describedBy}
|
|
166
228
|
/>
|
|
167
229
|
{/if}
|
|
168
230
|
<span class={controlRingClasses}></span>
|
|
@@ -170,10 +232,41 @@
|
|
|
170
232
|
{@render mark({ checked: !!checked, loading, disabled: isDisabled })}
|
|
171
233
|
{/if}
|
|
172
234
|
</span>
|
|
173
|
-
{#if
|
|
174
|
-
<span class="
|
|
235
|
+
{#if leading}
|
|
236
|
+
<span class="inline-flex shrink-0 items-center">
|
|
237
|
+
{@render leading()}
|
|
238
|
+
</span>
|
|
239
|
+
{/if}
|
|
240
|
+
{#if description}
|
|
241
|
+
<span class={cn("flex flex-col gap-1", labelTextClasses)}>
|
|
242
|
+
<span class="text-sm font-medium text-label">
|
|
243
|
+
{label}
|
|
244
|
+
</span>
|
|
245
|
+
<!-- aria-hidden keeps it out of the name; aria-describedby still reads it. -->
|
|
246
|
+
<span id={descriptionId} class="text-sm text-description" aria-hidden="true">
|
|
247
|
+
{description}
|
|
248
|
+
</span>
|
|
249
|
+
</span>
|
|
250
|
+
{:else if label}
|
|
251
|
+
<span class={cn("text-sm font-medium text-label", labelTextClasses)}>
|
|
175
252
|
{label}
|
|
176
253
|
</span>
|
|
177
254
|
{/if}
|
|
178
255
|
</label>
|
|
179
|
-
|
|
256
|
+
{/snippet}
|
|
257
|
+
|
|
258
|
+
{#if hasFieldText}
|
|
259
|
+
<div class={cn("flex flex-col", className)}>
|
|
260
|
+
<div class="flex items-center gap-2">
|
|
261
|
+
{@render row()}
|
|
262
|
+
</div>
|
|
263
|
+
<!-- Under the label text: the box is 20px and the gap 8px. With the box at the end, from the row's edge. -->
|
|
264
|
+
<div class={controlAtEnd ? "" : "ps-7"}>
|
|
265
|
+
<FieldMessages fieldId={controlId} {status} {hint} gap="mt-1" />
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
{:else}
|
|
269
|
+
<div class={cn("flex items-center gap-2", className)}>
|
|
270
|
+
{@render row()}
|
|
271
|
+
</div>
|
|
272
|
+
{/if}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Snippet } from "svelte";
|
|
2
2
|
import type { HTMLInputAttributes } from "svelte/elements";
|
|
3
|
+
import type { SemanticVariant } from "../types/variants.js";
|
|
3
4
|
import { type SelectionControlShape } from "./selection-control.styles";
|
|
4
5
|
type SelectionControlType = "checkbox" | "radio";
|
|
5
6
|
export type SelectionControlMarkProps = {
|
|
@@ -23,6 +24,30 @@ type Props = Omit<HTMLInputAttributes, "class" | "id" | "name" | "value" | "disa
|
|
|
23
24
|
onChange?: (event: Event) => void;
|
|
24
25
|
onchange?: (event: Event) => void;
|
|
25
26
|
mark?: Snippet<[SelectionControlMarkProps]>;
|
|
27
|
+
/**
|
|
28
|
+
* Drawn inside the one label, between the control and the label text,
|
|
29
|
+
* as decoration for the name `label` already gives. A leading image
|
|
30
|
+
* should have an empty alt: the label names the row.
|
|
31
|
+
*/
|
|
32
|
+
leading?: Snippet;
|
|
33
|
+
/**
|
|
34
|
+
* A line under the label, tied to the input with `aria-describedby`.
|
|
35
|
+
* It is not part of the name: the label names the control.
|
|
36
|
+
*/
|
|
37
|
+
description?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Where the control sits in the row. `end` puts it after the leading,
|
|
40
|
+
* the label and the description, at the far edge; the row is still
|
|
41
|
+
* one label and fills the width it is given.
|
|
42
|
+
*/
|
|
43
|
+
controlPosition?: "start" | "end";
|
|
44
|
+
variant?: SemanticVariant;
|
|
45
|
+
/** Status text under the control. Shown with a `success`, `warning` or `error` variant; for neutral help, use `hint`. */
|
|
46
|
+
message?: string;
|
|
47
|
+
/** Help text under the control, read out with it. */
|
|
48
|
+
hint?: string;
|
|
49
|
+
/** An error under the control. It marks the input invalid and is announced; it wins over `variant` and `message`. */
|
|
50
|
+
error?: string;
|
|
26
51
|
};
|
|
27
52
|
declare const SelectionControl: import("svelte").Component<Props, {}, "checked">;
|
|
28
53
|
type SelectionControl = ReturnType<typeof SelectionControl>;
|