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,325 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getContext, onMount, setContext, type Snippet } from "svelte";
|
|
3
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
4
|
+
import { cn } from "../util/cn.js";
|
|
5
|
+
import { generateId } from "../util/ssr-safe.js";
|
|
6
|
+
import {
|
|
7
|
+
CHIP_GAP,
|
|
8
|
+
CHIP_GROUP_CONTEXT_KEY,
|
|
9
|
+
SELECTED_CHIP_SELECTOR,
|
|
10
|
+
chipBox,
|
|
11
|
+
measureRowOverflow,
|
|
12
|
+
scrollLeftToReveal,
|
|
13
|
+
toggleValue,
|
|
14
|
+
type ChipGroupContext,
|
|
15
|
+
type RowMetrics,
|
|
16
|
+
} from "../util/chip.js";
|
|
17
|
+
|
|
18
|
+
/** Other attributes (`id`, `data-*`, `aria-label`, `aria-labelledby`, ...) land on the group element. */
|
|
19
|
+
type Props = Omit<HTMLAttributes<HTMLDivElement>, "class" | "onchange" | "children"> & {
|
|
20
|
+
class?: string;
|
|
21
|
+
/**
|
|
22
|
+
* `wrap` breaks onto as many lines as it needs; `row` is one line that
|
|
23
|
+
* scrolls sideways, with the chosen chip brought into view on load.
|
|
24
|
+
* A group inside a group takes the layout of the one around it.
|
|
25
|
+
*/
|
|
26
|
+
layout?: "wrap" | "row";
|
|
27
|
+
/** The group's accessible name. Not shown unless `showLabel`. */
|
|
28
|
+
label?: string;
|
|
29
|
+
/** Show `label` as text above the group (beside it, in a group inside a group). */
|
|
30
|
+
showLabel?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* `radio` or `checkbox`: the chips are inputs that share this group's
|
|
33
|
+
* `name` and `value`. Without it the group only lays out its chips,
|
|
34
|
+
* which are links or toggle buttons.
|
|
35
|
+
*/
|
|
36
|
+
type?: "radio" | "checkbox";
|
|
37
|
+
/** Name the inputs submit under. */
|
|
38
|
+
name?: string;
|
|
39
|
+
/**
|
|
40
|
+
* What is chosen. Bindable: a string, or `undefined` for none, for
|
|
41
|
+
* `radio`; an array of strings for `checkbox`.
|
|
42
|
+
*/
|
|
43
|
+
value?: string | string[] | undefined;
|
|
44
|
+
/** Disables every chip in the group. */
|
|
45
|
+
disabled?: boolean;
|
|
46
|
+
/** With `layout="row"`: `fade` dims the edge that has more chips beyond it; `none` never does. */
|
|
47
|
+
edge?: "fade" | "none";
|
|
48
|
+
/** With `layout="row"`: bring the chosen chip into view when the row appears. */
|
|
49
|
+
scrollSelectedIntoView?: boolean;
|
|
50
|
+
/** Called with the new `value` when a person changes it. */
|
|
51
|
+
onchange?: (value: string | string[] | undefined) => void;
|
|
52
|
+
children?: Snippet;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
let {
|
|
56
|
+
class: className = "",
|
|
57
|
+
layout = "wrap",
|
|
58
|
+
label = "",
|
|
59
|
+
showLabel = false,
|
|
60
|
+
type,
|
|
61
|
+
name,
|
|
62
|
+
value = $bindable<Props["value"]>(),
|
|
63
|
+
disabled = false,
|
|
64
|
+
edge = "fade",
|
|
65
|
+
scrollSelectedIntoView = true,
|
|
66
|
+
onchange,
|
|
67
|
+
children,
|
|
68
|
+
...restProps
|
|
69
|
+
}: Props = $props();
|
|
70
|
+
|
|
71
|
+
// No fallback on a bindable prop (see Rating): a checkbox group starts
|
|
72
|
+
// empty, here, so the server renders it and `bind:value={undefined}` works.
|
|
73
|
+
const applyDefaults = () => {
|
|
74
|
+
if (type === "checkbox" && value === undefined) value = [];
|
|
75
|
+
};
|
|
76
|
+
applyDefaults();
|
|
77
|
+
$effect.pre(applyDefaults);
|
|
78
|
+
|
|
79
|
+
/** A group inside a group is a sub-group: it names its chips and does not scroll or wrap on its own. */
|
|
80
|
+
const parent = getContext<ChipGroupContext | undefined>(CHIP_GROUP_CONTEXT_KEY);
|
|
81
|
+
const nested = parent !== undefined;
|
|
82
|
+
const effectiveLayout = $derived(parent ? parent.layout : layout);
|
|
83
|
+
|
|
84
|
+
function isSelected(chip: string): boolean {
|
|
85
|
+
if (type === "radio") return value === chip;
|
|
86
|
+
if (type === "checkbox") return Array.isArray(value) && value.includes(chip);
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
setContext<ChipGroupContext>(CHIP_GROUP_CONTEXT_KEY, {
|
|
91
|
+
get type() {
|
|
92
|
+
return type;
|
|
93
|
+
},
|
|
94
|
+
get name() {
|
|
95
|
+
return name;
|
|
96
|
+
},
|
|
97
|
+
get layout() {
|
|
98
|
+
return effectiveLayout;
|
|
99
|
+
},
|
|
100
|
+
get disabled() {
|
|
101
|
+
return disabled;
|
|
102
|
+
},
|
|
103
|
+
get radioValue() {
|
|
104
|
+
return type === "radio" && typeof value === "string" ? value : undefined;
|
|
105
|
+
},
|
|
106
|
+
isSelected,
|
|
107
|
+
change(chip, checked, report) {
|
|
108
|
+
if (type === "radio") {
|
|
109
|
+
// Single selection never clears: unchecking is not a thing a radio does.
|
|
110
|
+
if (!checked) return;
|
|
111
|
+
value = chip;
|
|
112
|
+
} else if (type === "checkbox") {
|
|
113
|
+
value = toggleValue(Array.isArray(value) ? value : [], chip, checked);
|
|
114
|
+
} else {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (report) onchange?.(value);
|
|
118
|
+
},
|
|
119
|
+
restore() {
|
|
120
|
+
if (type === "radio") value = undefined;
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const labelId = generateId("chip-group-label");
|
|
125
|
+
const role = $derived(type === "radio" ? "radiogroup" : "group");
|
|
126
|
+
const isRow = $derived(effectiveLayout === "row");
|
|
127
|
+
/** Only the outermost row scrolls; a sub-group is just inline. */
|
|
128
|
+
const scrolls = $derived(isRow && !nested);
|
|
129
|
+
|
|
130
|
+
// ---- The one-row layout: the chosen chip in view, and the edge cue. ----
|
|
131
|
+
|
|
132
|
+
let scroller = $state<HTMLDivElement | null>(null);
|
|
133
|
+
let overflowStart = $state(false);
|
|
134
|
+
let overflowEnd = $state(false);
|
|
135
|
+
|
|
136
|
+
function metricsOf(element: HTMLElement): RowMetrics {
|
|
137
|
+
return {
|
|
138
|
+
scrollLeft: element.scrollLeft,
|
|
139
|
+
scrollWidth: element.scrollWidth,
|
|
140
|
+
clientWidth: element.clientWidth,
|
|
141
|
+
rtl: getComputedStyle(element).direction === "rtl",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function measure() {
|
|
146
|
+
if (!scroller) return;
|
|
147
|
+
const overflow = measureRowOverflow(metricsOf(scroller));
|
|
148
|
+
overflowStart = overflow.start;
|
|
149
|
+
overflowEnd = overflow.end;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Room kept between the revealed chip and the edge: one gap, and the fade when there is one. */
|
|
153
|
+
const FADE_WIDTH = 24;
|
|
154
|
+
|
|
155
|
+
/** Scrolls the row, and only the row (it sets `scrollLeft`, so the page cannot move). Not animated. */
|
|
156
|
+
function revealSelected() {
|
|
157
|
+
const row = scroller;
|
|
158
|
+
if (!row) return;
|
|
159
|
+
const selected = row.querySelector(SELECTED_CHIP_SELECTOR);
|
|
160
|
+
if (!selected) return;
|
|
161
|
+
const box = row.getBoundingClientRect();
|
|
162
|
+
const item = chipBox(selected).getBoundingClientRect();
|
|
163
|
+
row.scrollLeft = scrollLeftToReveal(
|
|
164
|
+
metricsOf(row),
|
|
165
|
+
{ left: box.left, right: box.right },
|
|
166
|
+
{ left: item.left, right: item.right },
|
|
167
|
+
edge === "fade" ? CHIP_GAP + FADE_WIDTH : CHIP_GAP,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let revealed = false;
|
|
172
|
+
onMount(() => {
|
|
173
|
+
if (!revealed && scroller && scrolls && scrollSelectedIntoView) revealSelected();
|
|
174
|
+
revealed = true;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
$effect(() => {
|
|
178
|
+
const row = scroller;
|
|
179
|
+
if (!row || !scrolls) {
|
|
180
|
+
overflowStart = false;
|
|
181
|
+
overflowEnd = false;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
measure();
|
|
185
|
+
row.addEventListener("scroll", measure, { passive: true });
|
|
186
|
+
let sizes: ResizeObserver | undefined;
|
|
187
|
+
let nodes: MutationObserver | undefined;
|
|
188
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
189
|
+
const sized = new ResizeObserver(measure);
|
|
190
|
+
sizes = sized;
|
|
191
|
+
const watch = () => {
|
|
192
|
+
sized.disconnect();
|
|
193
|
+
sized.observe(row);
|
|
194
|
+
for (const child of row.children) sized.observe(child);
|
|
195
|
+
};
|
|
196
|
+
watch();
|
|
197
|
+
if (typeof MutationObserver !== "undefined") {
|
|
198
|
+
nodes = new MutationObserver(() => {
|
|
199
|
+
watch();
|
|
200
|
+
measure();
|
|
201
|
+
});
|
|
202
|
+
nodes.observe(row, { childList: true });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return () => {
|
|
206
|
+
row.removeEventListener("scroll", measure);
|
|
207
|
+
sizes?.disconnect();
|
|
208
|
+
nodes?.disconnect();
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const hostClasses = $derived.by(() => {
|
|
213
|
+
if (nested) {
|
|
214
|
+
return cn(isRow ? "flex shrink-0 items-center gap-2" : "flex flex-wrap items-center gap-2", className);
|
|
215
|
+
}
|
|
216
|
+
if (isRow) {
|
|
217
|
+
// 4px of room on every side for the focus ring, which the scrolling box
|
|
218
|
+
// would clip, taken back by the negative margin (as Tabs does).
|
|
219
|
+
return cn(
|
|
220
|
+
"chip-row -m-1 flex flex-nowrap items-center gap-2 overflow-x-auto overflow-y-hidden overscroll-x-contain p-1 scroll-px-8 has-[>[data-chip-subgroup]]:gap-6",
|
|
221
|
+
className,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return cn("flex flex-wrap items-center gap-2 has-[>[data-chip-subgroup]]:gap-x-6", className);
|
|
225
|
+
});
|
|
226
|
+
</script>
|
|
227
|
+
|
|
228
|
+
{#snippet group(labelledBy: string | undefined)}
|
|
229
|
+
<div
|
|
230
|
+
bind:this={scroller}
|
|
231
|
+
{role}
|
|
232
|
+
class={hostClasses}
|
|
233
|
+
aria-label={labelledBy ? undefined : label || undefined}
|
|
234
|
+
aria-labelledby={labelledBy}
|
|
235
|
+
data-chip-subgroup={nested ? "" : undefined}
|
|
236
|
+
data-layout={nested ? undefined : effectiveLayout}
|
|
237
|
+
data-overflow-start={scrolls && edge === "fade" && overflowStart ? "" : undefined}
|
|
238
|
+
data-overflow-end={scrolls && edge === "fade" && overflowEnd ? "" : undefined}
|
|
239
|
+
{...restProps}
|
|
240
|
+
>
|
|
241
|
+
{#if nested && showLabel && label}
|
|
242
|
+
<span id={labelId} class="shrink-0 text-sm font-medium whitespace-nowrap text-label">{label}</span>
|
|
243
|
+
{/if}
|
|
244
|
+
{@render children?.()}
|
|
245
|
+
</div>
|
|
246
|
+
{/snippet}
|
|
247
|
+
|
|
248
|
+
{#if !nested && showLabel && label}
|
|
249
|
+
<div class="flex flex-col gap-2">
|
|
250
|
+
<span id={labelId} class="text-sm font-medium text-label">{label}</span>
|
|
251
|
+
{@render group(labelId)}
|
|
252
|
+
</div>
|
|
253
|
+
{:else if nested && showLabel && label}
|
|
254
|
+
{@render group(labelId)}
|
|
255
|
+
{:else}
|
|
256
|
+
{@render group(undefined)}
|
|
257
|
+
{/if}
|
|
258
|
+
|
|
259
|
+
<style>
|
|
260
|
+
/*
|
|
261
|
+
* One line that scrolls sideways, with no scrollbar. The edge cue is a
|
|
262
|
+
* mask, not a gradient laid over the chips, so it fades them into any
|
|
263
|
+
* background, a brand block included, and nothing sits on a chip or its
|
|
264
|
+
* focus ring. Only a side with more chips beyond it fades (the component
|
|
265
|
+
* writes data-overflow-start and -end after it has measured), and there
|
|
266
|
+
* is none on the server. The mask is physical; start is the right edge
|
|
267
|
+
* in a right-to-left row.
|
|
268
|
+
*/
|
|
269
|
+
:global(.chip-row) {
|
|
270
|
+
scrollbar-width: none;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
:global(.chip-row::-webkit-scrollbar) {
|
|
274
|
+
display: none;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
:global(.chip-row[data-overflow-start]),
|
|
278
|
+
:global(.chip-row[data-overflow-end]) {
|
|
279
|
+
--chip-fade-start: 0px;
|
|
280
|
+
--chip-fade-end: 0px;
|
|
281
|
+
--chip-fade-left: var(--chip-fade-start);
|
|
282
|
+
--chip-fade-right: var(--chip-fade-end);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
:global(.chip-row[data-overflow-start]) {
|
|
286
|
+
--chip-fade-start: 1.5rem;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
:global(.chip-row[data-overflow-end]) {
|
|
290
|
+
--chip-fade-end: 1.5rem;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
:global(.chip-row[data-overflow-start]:dir(rtl)),
|
|
294
|
+
:global(.chip-row[data-overflow-end]:dir(rtl)) {
|
|
295
|
+
--chip-fade-left: var(--chip-fade-end);
|
|
296
|
+
--chip-fade-right: var(--chip-fade-start);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
:global(.chip-row[data-overflow-start]),
|
|
300
|
+
:global(.chip-row[data-overflow-end]) {
|
|
301
|
+
-webkit-mask-image: linear-gradient(
|
|
302
|
+
to right,
|
|
303
|
+
transparent,
|
|
304
|
+
black var(--chip-fade-left),
|
|
305
|
+
black calc(100% - var(--chip-fade-right)),
|
|
306
|
+
transparent
|
|
307
|
+
);
|
|
308
|
+
mask-image: linear-gradient(
|
|
309
|
+
to right,
|
|
310
|
+
transparent,
|
|
311
|
+
black var(--chip-fade-left),
|
|
312
|
+
black calc(100% - var(--chip-fade-right)),
|
|
313
|
+
transparent
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* Forced colours: a half-transparent label is harder to read than a cut-off one. */
|
|
318
|
+
@media (forced-colors: active) {
|
|
319
|
+
:global(.chip-row[data-overflow-start]),
|
|
320
|
+
:global(.chip-row[data-overflow-end]) {
|
|
321
|
+
-webkit-mask-image: none;
|
|
322
|
+
mask-image: none;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
</style>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type Snippet } from "svelte";
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
/** Other attributes (`id`, `data-*`, `aria-label`, `aria-labelledby`, ...) land on the group element. */
|
|
4
|
+
type Props = Omit<HTMLAttributes<HTMLDivElement>, "class" | "onchange" | "children"> & {
|
|
5
|
+
class?: string;
|
|
6
|
+
/**
|
|
7
|
+
* `wrap` breaks onto as many lines as it needs; `row` is one line that
|
|
8
|
+
* scrolls sideways, with the chosen chip brought into view on load.
|
|
9
|
+
* A group inside a group takes the layout of the one around it.
|
|
10
|
+
*/
|
|
11
|
+
layout?: "wrap" | "row";
|
|
12
|
+
/** The group's accessible name. Not shown unless `showLabel`. */
|
|
13
|
+
label?: string;
|
|
14
|
+
/** Show `label` as text above the group (beside it, in a group inside a group). */
|
|
15
|
+
showLabel?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* `radio` or `checkbox`: the chips are inputs that share this group's
|
|
18
|
+
* `name` and `value`. Without it the group only lays out its chips,
|
|
19
|
+
* which are links or toggle buttons.
|
|
20
|
+
*/
|
|
21
|
+
type?: "radio" | "checkbox";
|
|
22
|
+
/** Name the inputs submit under. */
|
|
23
|
+
name?: string;
|
|
24
|
+
/**
|
|
25
|
+
* What is chosen. Bindable: a string, or `undefined` for none, for
|
|
26
|
+
* `radio`; an array of strings for `checkbox`.
|
|
27
|
+
*/
|
|
28
|
+
value?: string | string[] | undefined;
|
|
29
|
+
/** Disables every chip in the group. */
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
/** With `layout="row"`: `fade` dims the edge that has more chips beyond it; `none` never does. */
|
|
32
|
+
edge?: "fade" | "none";
|
|
33
|
+
/** With `layout="row"`: bring the chosen chip into view when the row appears. */
|
|
34
|
+
scrollSelectedIntoView?: boolean;
|
|
35
|
+
/** Called with the new `value` when a person changes it. */
|
|
36
|
+
onchange?: (value: string | string[] | undefined) => void;
|
|
37
|
+
children?: Snippet;
|
|
38
|
+
};
|
|
39
|
+
declare const ChipGroup: import("svelte").Component<Props, {}, "value">;
|
|
40
|
+
type ChipGroup = ReturnType<typeof ChipGroup>;
|
|
41
|
+
export default ChipGroup;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { HTMLAttributes } from "svelte/elements";
|
|
4
4
|
import ChevronDown from "@lucide/svelte/icons/chevron-down";
|
|
5
5
|
import { cn } from "../util/cn.js";
|
|
6
|
-
import { generateId } from "../util/ssr-safe.js";
|
|
6
|
+
import { generateId, isBrowser } from "../util/ssr-safe.js";
|
|
7
7
|
import {
|
|
8
8
|
COLLAPSIBLE_GROUP,
|
|
9
9
|
type CollapsibleGroupContext,
|
|
@@ -13,11 +13,19 @@
|
|
|
13
13
|
type CollapsibleTriggerState,
|
|
14
14
|
} from "../util/collapsible.js";
|
|
15
15
|
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* A trigger, then a panel of its own that it opens. It is not for a "show
|
|
18
|
+
* all" under a list: there the control comes after the content, and what
|
|
19
|
+
* it shows is more items of a list that is already on the screen. For
|
|
20
|
+
* that, put a `Button` with `aria-expanded` and `aria-controls` under the
|
|
21
|
+
* list and let the list draw the rest (Z-127).
|
|
22
|
+
*
|
|
23
|
+
* Other attributes (`id`, `data-*`, ...) land on the host `<div>`.
|
|
24
|
+
*/
|
|
17
25
|
type Props = Omit<HTMLAttributes<HTMLDivElement>, "class" | "title"> & {
|
|
18
26
|
/** Extra classes for the host element. */
|
|
19
27
|
class?: string;
|
|
20
|
-
/** Extra classes for the default trigger
|
|
28
|
+
/** Extra classes for the default trigger (a `<summary>`). */
|
|
21
29
|
triggerClass?: string;
|
|
22
30
|
/** Extra classes for the panel. */
|
|
23
31
|
panelClass?: string;
|
|
@@ -30,18 +38,27 @@
|
|
|
30
38
|
* a group does not close a disabled panel either.
|
|
31
39
|
*/
|
|
32
40
|
disabled?: boolean;
|
|
33
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Text of the default trigger, a full-width header with a chevron. It is
|
|
43
|
+
* a native `<details>` / `<summary>`, so it opens and closes without
|
|
44
|
+
* scripts: before hydration, and for a visitor who has none.
|
|
45
|
+
*/
|
|
34
46
|
title?: string;
|
|
35
47
|
/**
|
|
36
48
|
* Your own header, in place of the default trigger. Spread the first
|
|
37
|
-
* argument on a `<button>`; the second carries the open state.
|
|
49
|
+
* argument on a `<button>`; the second carries the open state. This
|
|
50
|
+
* path needs scripts: the button and the `hidden` panel are yours to
|
|
51
|
+
* wire, and nothing opens until the page has hydrated. Use `title` for
|
|
52
|
+
* content that must open without scripts.
|
|
38
53
|
*/
|
|
39
54
|
trigger?: Snippet<[CollapsibleTriggerProps, CollapsibleTriggerState]>;
|
|
40
55
|
/** Wraps the default trigger in `<h1>`–`<h6>`. No heading when omitted. */
|
|
41
56
|
headingLevel?: CollapsibleHeadingLevel;
|
|
42
57
|
/**
|
|
43
|
-
* Closed content
|
|
44
|
-
* keeps its values
|
|
58
|
+
* Closed content stays in the DOM, inside the closed `<details>`, so a
|
|
59
|
+
* form inside it keeps its values and still submits them: closed
|
|
60
|
+
* details content is part of the form. Set this to remove the content
|
|
61
|
+
* while closed instead.
|
|
45
62
|
*/
|
|
46
63
|
unmountOnClose?: boolean;
|
|
47
64
|
/**
|
|
@@ -167,6 +184,67 @@
|
|
|
167
184
|
"data-collapsible-trigger": "",
|
|
168
185
|
} satisfies CollapsibleTriggerProps);
|
|
169
186
|
|
|
187
|
+
let detailsElement: HTMLDetailsElement | undefined = $state();
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* What only the server writes on the `<details>`: the `open` attribute and
|
|
191
|
+
* a mark that says the element came from the server. The client never
|
|
192
|
+
* writes `open` through the template: hydration would set it from the
|
|
193
|
+
* state and undo a toggle the visitor made before the scripts arrived.
|
|
194
|
+
* The effects below adopt that choice, then keep `open` in line.
|
|
195
|
+
*/
|
|
196
|
+
const SERVER_MARK = "data-collapsible-ssr";
|
|
197
|
+
const serverAttributes = () =>
|
|
198
|
+
isBrowser() ? {} : { open: isOpen, [SERVER_MARK]: "" };
|
|
199
|
+
|
|
200
|
+
// Once, after mount: a panel toggled before hydration keeps that state.
|
|
201
|
+
$effect(() => {
|
|
202
|
+
const element = detailsElement;
|
|
203
|
+
if (!element?.hasAttribute(SERVER_MARK)) return;
|
|
204
|
+
element.removeAttribute(SERVER_MARK);
|
|
205
|
+
untrack(() => {
|
|
206
|
+
if (!disabled && element.open !== isOpen) setOpen(element.open);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// `open` follows the state: an `open` bound from outside, a group closing
|
|
211
|
+
// a sibling. Runs after the adoption above on the first pass.
|
|
212
|
+
$effect(() => {
|
|
213
|
+
const element = detailsElement;
|
|
214
|
+
if (element && element.open !== isOpen) element.open = isOpen;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The browser's `toggle` event is where a change made on the `<details>`
|
|
219
|
+
* enters: a click on the summary, Enter or Space, find-in-page opening it,
|
|
220
|
+
* or the native exclusive group closing it. It also fires, a task later,
|
|
221
|
+
* for a change this component made itself; `setOpen` then sees no change.
|
|
222
|
+
*/
|
|
223
|
+
function handleToggle() {
|
|
224
|
+
const element = detailsElement;
|
|
225
|
+
if (!element) return;
|
|
226
|
+
if (disabled) {
|
|
227
|
+
// Find-in-page can still open a disabled panel: undo it.
|
|
228
|
+
if (element.open !== isOpen) element.open = isOpen;
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
setOpen(element.open);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** A summary cannot be disabled, so its click is cancelled instead. */
|
|
235
|
+
function handleSummaryClick(event: MouseEvent) {
|
|
236
|
+
if (disabled) event.preventDefault();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Native exclusivity (the `name` attribute) for a single-open group. A
|
|
241
|
+
* disabled panel stays out of it: the group neither closes nor counts it.
|
|
242
|
+
*/
|
|
243
|
+
const detailsName = $derived(disabled ? undefined : group?.exclusiveName);
|
|
244
|
+
|
|
245
|
+
const rowClass =
|
|
246
|
+
"m-0 flex items-center justify-between gap-2 px-3 py-2 text-sm font-medium pointer-coarse:min-h-11";
|
|
247
|
+
|
|
170
248
|
/**
|
|
171
249
|
* A region is a landmark, and a page of them is noise (the WAI-ARIA
|
|
172
250
|
* accordion pattern warns against it). In a single-open group at most one
|
|
@@ -178,66 +256,92 @@
|
|
|
178
256
|
);
|
|
179
257
|
</script>
|
|
180
258
|
|
|
181
|
-
{#snippet
|
|
182
|
-
<
|
|
183
|
-
|
|
259
|
+
{#snippet defaultRow()}
|
|
260
|
+
<span class="min-w-0 flex-1">{title}</span>
|
|
261
|
+
<ChevronDown
|
|
262
|
+
size={16}
|
|
263
|
+
aria-hidden="true"
|
|
184
264
|
class={cn(
|
|
185
|
-
"
|
|
186
|
-
|
|
265
|
+
"shrink-0 text-description transition-transform duration-150 motion-reduce:transition-none",
|
|
266
|
+
isOpen && "rotate-180",
|
|
187
267
|
)}
|
|
188
|
-
|
|
189
|
-
<span class="min-w-0 flex-1">{title}</span>
|
|
190
|
-
<ChevronDown
|
|
191
|
-
size={16}
|
|
192
|
-
aria-hidden="true"
|
|
193
|
-
class={cn(
|
|
194
|
-
"shrink-0 text-description transition-transform duration-150 motion-reduce:transition-none",
|
|
195
|
-
isOpen && "rotate-180",
|
|
196
|
-
)}
|
|
197
|
-
/>
|
|
198
|
-
</button>
|
|
268
|
+
/>
|
|
199
269
|
{/snippet}
|
|
200
270
|
|
|
201
|
-
|
|
202
|
-
class={cn(className)}
|
|
203
|
-
data-state={isOpen ? "open" : "closed"}
|
|
204
|
-
data-disabled={disabled ? "" : undefined}
|
|
205
|
-
{...restProps}
|
|
206
|
-
>
|
|
207
|
-
{#if trigger}
|
|
208
|
-
{@render trigger(triggerProps, { open: isOpen, disabled })}
|
|
209
|
-
{:else if headingLevel === 1}
|
|
210
|
-
<!-- One branch per level, not `<svelte:element>`: hydration takes a
|
|
211
|
-
dynamic element out and puts it back, which blurs the trigger inside
|
|
212
|
-
it if the user had already tabbed to it. -->
|
|
213
|
-
<h1 class="m-0">{@render defaultTrigger()}</h1>
|
|
214
|
-
{:else if headingLevel === 2}
|
|
215
|
-
<h2 class="m-0">{@render defaultTrigger()}</h2>
|
|
216
|
-
{:else if headingLevel === 3}
|
|
217
|
-
<h3 class="m-0">{@render defaultTrigger()}</h3>
|
|
218
|
-
{:else if headingLevel === 4}
|
|
219
|
-
<h4 class="m-0">{@render defaultTrigger()}</h4>
|
|
220
|
-
{:else if headingLevel === 5}
|
|
221
|
-
<h5 class="m-0">{@render defaultTrigger()}</h5>
|
|
222
|
-
{:else if headingLevel === 6}
|
|
223
|
-
<h6 class="m-0">{@render defaultTrigger()}</h6>
|
|
224
|
-
{:else}
|
|
225
|
-
{@render defaultTrigger()}
|
|
226
|
-
{/if}
|
|
227
|
-
|
|
228
|
-
<!-- `hidden` is `display: none`: closed content takes no focus and is not
|
|
229
|
-
in the accessibility tree. The element itself stays, so `aria-controls`
|
|
230
|
-
always points at something. -->
|
|
271
|
+
{#snippet panel()}
|
|
231
272
|
<div
|
|
232
273
|
bind:this={panelElement}
|
|
233
274
|
id={panelId}
|
|
234
275
|
role={panelRole}
|
|
235
276
|
aria-labelledby={triggerId}
|
|
236
|
-
hidden={!isOpen}
|
|
277
|
+
hidden={trigger ? !isOpen : undefined}
|
|
237
278
|
class={cn(!trigger && "px-3 pb-3 pt-1", panelClass)}
|
|
238
279
|
>
|
|
239
280
|
{#if isOpen || !unmountOnClose}
|
|
240
281
|
{@render children?.()}
|
|
241
282
|
{/if}
|
|
242
283
|
</div>
|
|
284
|
+
{/snippet}
|
|
285
|
+
|
|
286
|
+
<div
|
|
287
|
+
class={cn(className)}
|
|
288
|
+
data-state={isOpen ? "open" : "closed"}
|
|
289
|
+
data-disabled={disabled ? "" : undefined}
|
|
290
|
+
{...restProps}
|
|
291
|
+
>
|
|
292
|
+
{#if trigger}
|
|
293
|
+
{@render trigger(triggerProps, { open: isOpen, disabled })}
|
|
294
|
+
<!-- `hidden` is `display: none`: closed content takes no focus and is not
|
|
295
|
+
in the accessibility tree. The element itself stays, so `aria-controls`
|
|
296
|
+
always points at something. -->
|
|
297
|
+
{@render panel()}
|
|
298
|
+
{:else}
|
|
299
|
+
<!-- A native disclosure: the browser opens and closes it, so it works
|
|
300
|
+
before hydration and without scripts. `open` follows `isOpen` (see
|
|
301
|
+
`serverAttributes`); user changes come back through `toggle`. -->
|
|
302
|
+
<details
|
|
303
|
+
bind:this={detailsElement}
|
|
304
|
+
{...serverAttributes()}
|
|
305
|
+
name={detailsName}
|
|
306
|
+
ontoggle={handleToggle}
|
|
307
|
+
>
|
|
308
|
+
<summary
|
|
309
|
+
id={triggerId}
|
|
310
|
+
aria-controls={panelId}
|
|
311
|
+
aria-disabled={disabled ? "true" : undefined}
|
|
312
|
+
tabindex={disabled ? -1 : undefined}
|
|
313
|
+
data-collapsible-trigger=""
|
|
314
|
+
onclick={handleSummaryClick}
|
|
315
|
+
class={cn(
|
|
316
|
+
"focus-ring block w-full cursor-pointer list-none rounded-control text-start text-sm font-medium text-headline transition-colors duration-150 hover:bg-surface-hover active:bg-surface-active motion-reduce:transition-none [&::-webkit-details-marker]:hidden",
|
|
317
|
+
disabled &&
|
|
318
|
+
"pointer-events-none cursor-not-allowed opacity-50 hover:bg-transparent active:bg-transparent",
|
|
319
|
+
triggerClass,
|
|
320
|
+
)}
|
|
321
|
+
>
|
|
322
|
+
<!-- One branch per level, not `<svelte:element>`: hydration takes
|
|
323
|
+
a dynamic element out and puts it back, which blurs the trigger
|
|
324
|
+
inside it if the user had already tabbed to it. The heading goes
|
|
325
|
+
inside the summary: a summary inside a heading is not valid. The
|
|
326
|
+
flex row is on the inner element, not on the summary, which
|
|
327
|
+
older Safari does not lay out as flex. -->
|
|
328
|
+
{#if headingLevel === 1}
|
|
329
|
+
<h1 class={rowClass}>{@render defaultRow()}</h1>
|
|
330
|
+
{:else if headingLevel === 2}
|
|
331
|
+
<h2 class={rowClass}>{@render defaultRow()}</h2>
|
|
332
|
+
{:else if headingLevel === 3}
|
|
333
|
+
<h3 class={rowClass}>{@render defaultRow()}</h3>
|
|
334
|
+
{:else if headingLevel === 4}
|
|
335
|
+
<h4 class={rowClass}>{@render defaultRow()}</h4>
|
|
336
|
+
{:else if headingLevel === 5}
|
|
337
|
+
<h5 class={rowClass}>{@render defaultRow()}</h5>
|
|
338
|
+
{:else if headingLevel === 6}
|
|
339
|
+
<h6 class={rowClass}>{@render defaultRow()}</h6>
|
|
340
|
+
{:else}
|
|
341
|
+
<span class={rowClass}>{@render defaultRow()}</span>
|
|
342
|
+
{/if}
|
|
343
|
+
</summary>
|
|
344
|
+
{@render panel()}
|
|
345
|
+
</details>
|
|
346
|
+
{/if}
|
|
243
347
|
</div>
|