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,351 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getContext, onMount, tick, type Snippet } from "svelte";
|
|
3
|
+
import type { HTMLAnchorAttributes, HTMLButtonAttributes, HTMLInputAttributes } from "svelte/elements";
|
|
4
|
+
import Check from "@lucide/svelte/icons/check";
|
|
5
|
+
import { cn } from "../util/cn.js";
|
|
6
|
+
import { groupBinding } from "../util/hydration.js";
|
|
7
|
+
import { CHIP_GROUP_CONTEXT_KEY, type ChipGroupContext } from "../util/chip.js";
|
|
8
|
+
import { TOUCH_HIT_AREA } from "../util/touch-target.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* One type for the four forms, as Button has one for the button and the
|
|
12
|
+
* link: a union of the elements' attributes is too large to carry through
|
|
13
|
+
* a wrapper. Other attributes land on the element that is the control: the
|
|
14
|
+
* `<a>`, the `<button>` or the `<input>`; `class` is always the chip's.
|
|
15
|
+
*/
|
|
16
|
+
type Props = Omit<HTMLButtonAttributes, "class" | "type" | "value" | "onchange" | "children"> & {
|
|
17
|
+
/** Makes the chip a link to this address, which works without scripts. */
|
|
18
|
+
href?: string;
|
|
19
|
+
target?: HTMLAnchorAttributes["target"];
|
|
20
|
+
rel?: HTMLAnchorAttributes["rel"];
|
|
21
|
+
download?: HTMLAnchorAttributes["download"];
|
|
22
|
+
hreflang?: HTMLAnchorAttributes["hreflang"];
|
|
23
|
+
/** With `href`: what `aria-current` says while `selected`. */
|
|
24
|
+
current?: "page" | "true" | "step" | "location" | "date" | "time";
|
|
25
|
+
/**
|
|
26
|
+
* `checkbox` or `radio`: a real input under the chip, which submits in
|
|
27
|
+
* a form without scripts. Without it (and without `href`) the chip is a
|
|
28
|
+
* toggle button. Inside a ChipGroup with a `type`, the group's.
|
|
29
|
+
* `submit` or `button`: a button that does not toggle. A press posts
|
|
30
|
+
* the form (`submit`, with `name` and `value`) or only calls `onclick`;
|
|
31
|
+
* `selected` is drawn as on a toggle and said with `aria-pressed`, and
|
|
32
|
+
* the app decides it, as a server's answer does.
|
|
33
|
+
*/
|
|
34
|
+
type?: "checkbox" | "radio" | "submit" | "button";
|
|
35
|
+
/** Name the input is submitted under. Inside a ChipGroup with a `name`, the group's. */
|
|
36
|
+
name?: string;
|
|
37
|
+
/** What the input submits, and what a ChipGroup's `value` holds when it is chosen. */
|
|
38
|
+
value?: string;
|
|
39
|
+
/** The chip is chosen. Bindable; the one prop for `checked`, `aria-pressed` and `aria-current`. */
|
|
40
|
+
selected?: boolean;
|
|
41
|
+
required?: boolean;
|
|
42
|
+
form?: string;
|
|
43
|
+
/** 28, 32 or 40px tall. On a coarse pointer the touch target reaches 44px; the box does not grow. */
|
|
44
|
+
size?: "sm" | "md" | "lg";
|
|
45
|
+
/** An icon or an avatar before the label. */
|
|
46
|
+
leading?: Snippet;
|
|
47
|
+
/** A check icon before the label while selected. On for a checkbox and a toggle button, off for a link and a radio. */
|
|
48
|
+
checkmark?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* A button chip (`type="submit"`, `"button"` or a toggle) while its
|
|
51
|
+
* action is on its way, as Button's: `aria-busy` and `aria-disabled`,
|
|
52
|
+
* a spinner over the label, the press swallowed. The chip keeps its
|
|
53
|
+
* width and stays a Tab stop, so focus is not lost. Ignored by a link
|
|
54
|
+
* and by `checkbox` / `radio`, which are not actions.
|
|
55
|
+
*/
|
|
56
|
+
loading?: boolean;
|
|
57
|
+
/** Called with the new `selected` when a person changes it. */
|
|
58
|
+
onchange?: (selected: boolean) => void;
|
|
59
|
+
class?: string;
|
|
60
|
+
children?: Snippet;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
let {
|
|
64
|
+
href,
|
|
65
|
+
target,
|
|
66
|
+
rel,
|
|
67
|
+
download,
|
|
68
|
+
hreflang,
|
|
69
|
+
current = "true",
|
|
70
|
+
type: typeProp,
|
|
71
|
+
name: nameProp,
|
|
72
|
+
value,
|
|
73
|
+
selected = $bindable<Exclude<Props["selected"], undefined>>(),
|
|
74
|
+
disabled = false,
|
|
75
|
+
required,
|
|
76
|
+
form,
|
|
77
|
+
size = "md",
|
|
78
|
+
leading,
|
|
79
|
+
checkmark,
|
|
80
|
+
loading = false,
|
|
81
|
+
onchange,
|
|
82
|
+
onclick,
|
|
83
|
+
class: className = "",
|
|
84
|
+
children,
|
|
85
|
+
...restProps
|
|
86
|
+
}: Props = $props();
|
|
87
|
+
|
|
88
|
+
// No fallback on a bindable prop: Svelte refuses `bind:…={undefined}` on
|
|
89
|
+
// one that has a fallback (`props_invalid_value`), and a page that throws
|
|
90
|
+
// while it hydrates never becomes interactive. The default is applied
|
|
91
|
+
// here instead: at once, for the server and the first render, and again
|
|
92
|
+
// whenever a parent hands back `undefined`.
|
|
93
|
+
const applyDefaults = () => {
|
|
94
|
+
if (selected === undefined) selected = false;
|
|
95
|
+
};
|
|
96
|
+
applyDefaults();
|
|
97
|
+
$effect.pre(applyDefaults);
|
|
98
|
+
|
|
99
|
+
const group = getContext<ChipGroupContext | undefined>(CHIP_GROUP_CONTEXT_KEY);
|
|
100
|
+
/** Inside a group that has a `type`, the group decides what is chosen. */
|
|
101
|
+
const grouped = $derived(group?.type !== undefined && typeProp === undefined && href === undefined);
|
|
102
|
+
const type = $derived(grouped ? group?.type : typeProp);
|
|
103
|
+
const name = $derived(nameProp ?? (grouped ? group?.name : undefined));
|
|
104
|
+
const isLink = $derived(href !== undefined && href !== null);
|
|
105
|
+
const isInput = $derived(!isLink && (type === "checkbox" || type === "radio"));
|
|
106
|
+
/** A button that does not toggle: the app says what is selected. */
|
|
107
|
+
const isFixedButton = $derived(!isLink && (typeProp === "submit" || typeProp === "button"));
|
|
108
|
+
const isDisabled = $derived(!!disabled || (grouped && !!group?.disabled));
|
|
109
|
+
const chosen = $derived(grouped ? !!group?.isSelected(value ?? "") : !!selected);
|
|
110
|
+
const showCheckmark = $derived(checkmark ?? (!isLink && type !== "radio"));
|
|
111
|
+
|
|
112
|
+
function setChosen(next: boolean, report: boolean) {
|
|
113
|
+
if (grouped) group?.change(value ?? "", next, report);
|
|
114
|
+
else selected = next;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let input: HTMLInputElement | undefined = $state();
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The input is bound, so a choice made before the page hydrated is kept:
|
|
121
|
+
* Svelte's binding finds the input changed and hands its state over here
|
|
122
|
+
* instead of overwriting it (see util/hydration.ts). A choice adopted that
|
|
123
|
+
* way came without a `change` event that anything heard, so it is reported
|
|
124
|
+
* once, with a real `change` on the input, when the component has settled:
|
|
125
|
+
* the callbacks and a form around it hear it as they hear a press.
|
|
126
|
+
*/
|
|
127
|
+
let settling = true;
|
|
128
|
+
let adopted = false;
|
|
129
|
+
|
|
130
|
+
function take(next: boolean) {
|
|
131
|
+
if (next === chosen) return;
|
|
132
|
+
setChosen(next, false);
|
|
133
|
+
if (settling) adopted = true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const NOT_CHOSEN = {};
|
|
137
|
+
/** A radio on its own: its own value while it is checked. */
|
|
138
|
+
const aloneBinding = groupBinding<unknown>(
|
|
139
|
+
() => (selected ? value : NOT_CHOSEN),
|
|
140
|
+
(picked) => take(picked === value),
|
|
141
|
+
() => take(false),
|
|
142
|
+
);
|
|
143
|
+
/** A radio in a group: the group's value. */
|
|
144
|
+
const groupedBinding = groupBinding<string>(
|
|
145
|
+
() => group?.radioValue,
|
|
146
|
+
(picked) => {
|
|
147
|
+
if (group?.radioValue === picked) return;
|
|
148
|
+
group?.change(picked, true, false);
|
|
149
|
+
if (settling) adopted = true;
|
|
150
|
+
},
|
|
151
|
+
() => group?.restore(),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
onMount(() => {
|
|
155
|
+
// The bindings adopt in a microtask of their own, queued before this one.
|
|
156
|
+
void tick().then(() => {
|
|
157
|
+
settling = false;
|
|
158
|
+
if (adopted) input?.dispatchEvent(new Event("change", { bubbles: true }));
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
function handleInputChange(event: Event) {
|
|
163
|
+
if (isDisabled) return;
|
|
164
|
+
// A real change is its own report: nothing is left to tell.
|
|
165
|
+
adopted = false;
|
|
166
|
+
const checked = (event.currentTarget as HTMLInputElement).checked;
|
|
167
|
+
setChosen(checked, true);
|
|
168
|
+
onchange?.(checked);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Loading without `disabled`: unavailable, and still where focus is (see Button). */
|
|
172
|
+
const isBusy = $derived(loading && !disabled);
|
|
173
|
+
|
|
174
|
+
function handleButtonClick(event: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) {
|
|
175
|
+
if (isBusy) {
|
|
176
|
+
// The press ends here: it does not submit, toggle or reach `onclick`.
|
|
177
|
+
event.preventDefault();
|
|
178
|
+
event.stopPropagation();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
onclick?.(event);
|
|
182
|
+
if (event.defaultPrevented || isDisabled) return;
|
|
183
|
+
if (isFixedButton) return;
|
|
184
|
+
selected = !selected;
|
|
185
|
+
onchange?.(selected);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** A disabled link has no `href`, but a pointer can still press it. */
|
|
189
|
+
function handleLinkClick(event: MouseEvent) {
|
|
190
|
+
if (isDisabled) {
|
|
191
|
+
event.preventDefault();
|
|
192
|
+
event.stopPropagation();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
(onclick as ((event: MouseEvent) => void) | null | undefined)?.(event);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 28 / 32 / 40px, a fixed height: a chip never wraps its label, so one
|
|
200
|
+
* line is all it ever holds. The padding is 10 / 12 / 16px.
|
|
201
|
+
*/
|
|
202
|
+
const SIZE = { sm: "h-7 px-[10px]", md: "h-8 px-3", lg: "h-10 px-4" } as const;
|
|
203
|
+
const FORCED = "forced-colors:outline-2 forced-colors:-outline-offset-2 forced-colors:outline-[color:Highlight]";
|
|
204
|
+
|
|
205
|
+
const base = $derived(
|
|
206
|
+
`group/chip relative inline-flex shrink-0 cursor-pointer select-none items-center justify-center gap-[6px] whitespace-nowrap rounded-pill text-sm transition-colors duration-(--duration-base) motion-reduce:transition-none ${SIZE[size] ?? SIZE.md} ${TOUCH_HIT_AREA} ${isDisabled ? "pointer-events-none cursor-not-allowed opacity-50" : ""}`,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* A link and a button read `selected`. The hand-written chip colours are
|
|
211
|
+
* outside every layer, so they cannot be overridden by a variant of
|
|
212
|
+
* another colour: the two states are two class lists, never both.
|
|
213
|
+
*/
|
|
214
|
+
const elementClasses = $derived(
|
|
215
|
+
cn(
|
|
216
|
+
base,
|
|
217
|
+
"focus-ring",
|
|
218
|
+
chosen
|
|
219
|
+
? `bg-chip-selected hover:bg-chip-selected-hover active:bg-chip-selected-active text-chip-selected-text font-semibold ${FORCED}`
|
|
220
|
+
: "bg-chip hover:bg-chip-hover active:bg-chip-active text-chip-text font-medium",
|
|
221
|
+
isBusy && "cursor-progress",
|
|
222
|
+
className,
|
|
223
|
+
),
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* An input is chosen by the browser, and the look follows its `:checked`
|
|
228
|
+
* (not `selected`), so a choice made before hydration shows at once.
|
|
229
|
+
* Every colour is a `has-` or `not-has-` variant: with the plain
|
|
230
|
+
* hand-written class on the label, the unlayered rule would win over the
|
|
231
|
+
* selected variant.
|
|
232
|
+
*/
|
|
233
|
+
const labelClasses = $derived(
|
|
234
|
+
cn(
|
|
235
|
+
base,
|
|
236
|
+
"focus-ring font-medium not-has-[:checked]:bg-chip not-has-[:checked]:text-chip-text not-has-[:checked]:hover:bg-chip-hover not-has-[:checked]:active:bg-chip-active has-[:checked]:bg-chip-selected has-[:checked]:text-chip-selected-text has-[:checked]:font-semibold has-[:checked]:hover:bg-chip-selected-hover has-[:checked]:active:bg-chip-selected-active",
|
|
237
|
+
"has-[:checked]:forced-colors:outline-2 has-[:checked]:forced-colors:-outline-offset-2 has-[:checked]:forced-colors:outline-[color:Highlight]",
|
|
238
|
+
"has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-focus-ring",
|
|
239
|
+
className,
|
|
240
|
+
),
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
/** The real input, stretched over the chip and transparent, so a press lands on it. */
|
|
244
|
+
const INPUT_CLASSES = "absolute inset-0 m-0 size-full cursor-pointer appearance-none rounded-pill opacity-0 outline-none disabled:cursor-not-allowed";
|
|
245
|
+
</script>
|
|
246
|
+
|
|
247
|
+
{#snippet inner(checkClass: string)}
|
|
248
|
+
{#if showCheckmark}
|
|
249
|
+
<Check class="size-3.5 shrink-0 {checkClass}" aria-hidden="true" />
|
|
250
|
+
{/if}
|
|
251
|
+
{#if leading}
|
|
252
|
+
<span class="inline-flex shrink-0 items-center">{@render leading()}</span>
|
|
253
|
+
{/if}
|
|
254
|
+
{#if children}
|
|
255
|
+
<span class="min-w-0 truncate">{@render children()}</span>
|
|
256
|
+
{/if}
|
|
257
|
+
{/snippet}
|
|
258
|
+
|
|
259
|
+
{#if isLink}
|
|
260
|
+
<!-- A link that cannot be followed keeps its role and says it is unavailable, as Button's does. -->
|
|
261
|
+
<a
|
|
262
|
+
href={isDisabled ? undefined : href}
|
|
263
|
+
{target}
|
|
264
|
+
{rel}
|
|
265
|
+
{download}
|
|
266
|
+
{hreflang}
|
|
267
|
+
class={elementClasses}
|
|
268
|
+
role={isDisabled ? "link" : undefined}
|
|
269
|
+
aria-disabled={isDisabled ? "true" : undefined}
|
|
270
|
+
aria-current={chosen ? current : undefined}
|
|
271
|
+
{...restProps as unknown as HTMLAnchorAttributes}
|
|
272
|
+
onclick={handleLinkClick}
|
|
273
|
+
>
|
|
274
|
+
{@render inner(chosen ? "" : "hidden")}
|
|
275
|
+
</a>
|
|
276
|
+
{:else if isInput}
|
|
277
|
+
<label class={labelClasses} data-chip-form={type}>
|
|
278
|
+
<!-- One branch per type: a binding needs a type it can read. -->
|
|
279
|
+
{#if type === "checkbox"}
|
|
280
|
+
<input
|
|
281
|
+
bind:this={input}
|
|
282
|
+
type="checkbox"
|
|
283
|
+
{name}
|
|
284
|
+
{value}
|
|
285
|
+
{required}
|
|
286
|
+
{form}
|
|
287
|
+
bind:checked={() => chosen, take}
|
|
288
|
+
disabled={isDisabled}
|
|
289
|
+
class={INPUT_CLASSES}
|
|
290
|
+
onchange={handleInputChange}
|
|
291
|
+
{...restProps as unknown as HTMLInputAttributes}
|
|
292
|
+
/>
|
|
293
|
+
{:else if grouped}
|
|
294
|
+
<input
|
|
295
|
+
bind:this={input}
|
|
296
|
+
type="radio"
|
|
297
|
+
{name}
|
|
298
|
+
{value}
|
|
299
|
+
{required}
|
|
300
|
+
{form}
|
|
301
|
+
bind:group={groupedBinding.value}
|
|
302
|
+
disabled={isDisabled}
|
|
303
|
+
class={INPUT_CLASSES}
|
|
304
|
+
onchange={handleInputChange}
|
|
305
|
+
{...restProps as unknown as HTMLInputAttributes}
|
|
306
|
+
/>
|
|
307
|
+
{:else}
|
|
308
|
+
<input
|
|
309
|
+
bind:this={input}
|
|
310
|
+
type="radio"
|
|
311
|
+
{name}
|
|
312
|
+
{value}
|
|
313
|
+
{required}
|
|
314
|
+
{form}
|
|
315
|
+
bind:group={aloneBinding.value}
|
|
316
|
+
disabled={isDisabled}
|
|
317
|
+
class={INPUT_CLASSES}
|
|
318
|
+
onchange={handleInputChange}
|
|
319
|
+
{...restProps as unknown as HTMLInputAttributes}
|
|
320
|
+
/>
|
|
321
|
+
{/if}
|
|
322
|
+
{@render inner("hidden group-has-checked/chip:block")}
|
|
323
|
+
</label>
|
|
324
|
+
{:else}
|
|
325
|
+
<button
|
|
326
|
+
type={typeProp === "submit" ? "submit" : "button"}
|
|
327
|
+
{name}
|
|
328
|
+
{value}
|
|
329
|
+
{form}
|
|
330
|
+
class={elementClasses}
|
|
331
|
+
{disabled}
|
|
332
|
+
aria-pressed={chosen}
|
|
333
|
+
aria-busy={isBusy ? "true" : undefined}
|
|
334
|
+
aria-disabled={isBusy ? "true" : undefined}
|
|
335
|
+
{...restProps}
|
|
336
|
+
onclick={handleButtonClick}
|
|
337
|
+
>
|
|
338
|
+
{#if isBusy}
|
|
339
|
+
<!-- The label stays in the chip, only unseen, so the width and the name do not change. -->
|
|
340
|
+
<span class="inline-flex min-w-0 items-center justify-center gap-[6px] opacity-0">
|
|
341
|
+
{@render inner(chosen ? "" : "hidden")}
|
|
342
|
+
</span>
|
|
343
|
+
<span
|
|
344
|
+
class="pointer-events-none absolute inline-block size-3.5 animate-spin rounded-full border-2 border-current border-t-transparent motion-reduce:animate-pulse"
|
|
345
|
+
aria-hidden="true"
|
|
346
|
+
></span>
|
|
347
|
+
{:else}
|
|
348
|
+
{@render inner(chosen ? "" : "hidden")}
|
|
349
|
+
{/if}
|
|
350
|
+
</button>
|
|
351
|
+
{/if}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type Snippet } from "svelte";
|
|
2
|
+
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
|
3
|
+
/**
|
|
4
|
+
* One type for the four forms, as Button has one for the button and the
|
|
5
|
+
* link: a union of the elements' attributes is too large to carry through
|
|
6
|
+
* a wrapper. Other attributes land on the element that is the control: the
|
|
7
|
+
* `<a>`, the `<button>` or the `<input>`; `class` is always the chip's.
|
|
8
|
+
*/
|
|
9
|
+
type Props = Omit<HTMLButtonAttributes, "class" | "type" | "value" | "onchange" | "children"> & {
|
|
10
|
+
/** Makes the chip a link to this address, which works without scripts. */
|
|
11
|
+
href?: string;
|
|
12
|
+
target?: HTMLAnchorAttributes["target"];
|
|
13
|
+
rel?: HTMLAnchorAttributes["rel"];
|
|
14
|
+
download?: HTMLAnchorAttributes["download"];
|
|
15
|
+
hreflang?: HTMLAnchorAttributes["hreflang"];
|
|
16
|
+
/** With `href`: what `aria-current` says while `selected`. */
|
|
17
|
+
current?: "page" | "true" | "step" | "location" | "date" | "time";
|
|
18
|
+
/**
|
|
19
|
+
* `checkbox` or `radio`: a real input under the chip, which submits in
|
|
20
|
+
* a form without scripts. Without it (and without `href`) the chip is a
|
|
21
|
+
* toggle button. Inside a ChipGroup with a `type`, the group's.
|
|
22
|
+
* `submit` or `button`: a button that does not toggle. A press posts
|
|
23
|
+
* the form (`submit`, with `name` and `value`) or only calls `onclick`;
|
|
24
|
+
* `selected` is drawn as on a toggle and said with `aria-pressed`, and
|
|
25
|
+
* the app decides it, as a server's answer does.
|
|
26
|
+
*/
|
|
27
|
+
type?: "checkbox" | "radio" | "submit" | "button";
|
|
28
|
+
/** Name the input is submitted under. Inside a ChipGroup with a `name`, the group's. */
|
|
29
|
+
name?: string;
|
|
30
|
+
/** What the input submits, and what a ChipGroup's `value` holds when it is chosen. */
|
|
31
|
+
value?: string;
|
|
32
|
+
/** The chip is chosen. Bindable; the one prop for `checked`, `aria-pressed` and `aria-current`. */
|
|
33
|
+
selected?: boolean;
|
|
34
|
+
required?: boolean;
|
|
35
|
+
form?: string;
|
|
36
|
+
/** 28, 32 or 40px tall. On a coarse pointer the touch target reaches 44px; the box does not grow. */
|
|
37
|
+
size?: "sm" | "md" | "lg";
|
|
38
|
+
/** An icon or an avatar before the label. */
|
|
39
|
+
leading?: Snippet;
|
|
40
|
+
/** A check icon before the label while selected. On for a checkbox and a toggle button, off for a link and a radio. */
|
|
41
|
+
checkmark?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* A button chip (`type="submit"`, `"button"` or a toggle) while its
|
|
44
|
+
* action is on its way, as Button's: `aria-busy` and `aria-disabled`,
|
|
45
|
+
* a spinner over the label, the press swallowed. The chip keeps its
|
|
46
|
+
* width and stays a Tab stop, so focus is not lost. Ignored by a link
|
|
47
|
+
* and by `checkbox` / `radio`, which are not actions.
|
|
48
|
+
*/
|
|
49
|
+
loading?: boolean;
|
|
50
|
+
/** Called with the new `selected` when a person changes it. */
|
|
51
|
+
onchange?: (selected: boolean) => void;
|
|
52
|
+
class?: string;
|
|
53
|
+
children?: Snippet;
|
|
54
|
+
};
|
|
55
|
+
declare const Chip: import("svelte").Component<Props, {}, "selected">;
|
|
56
|
+
type Chip = ReturnType<typeof Chip>;
|
|
57
|
+
export default Chip;
|