@sveltier/ui 0.0.0-stage → 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +82 -2
- package/dist/components/Alert/Alert.svelte +88 -0
- package/dist/components/Alert/Alert.svelte.d.ts +4 -0
- package/dist/components/Alert/alert.css +78 -0
- package/dist/components/Alert/index.d.ts +2 -0
- package/dist/components/Alert/index.js +1 -0
- package/dist/components/Alert/types.d.ts +67 -0
- package/dist/components/Alert/types.js +1 -0
- package/dist/components/Avatar/Avatar.svelte +47 -0
- package/dist/components/Avatar/Avatar.svelte.d.ts +4 -0
- package/dist/components/Avatar/avatar.css +34 -0
- package/dist/components/Avatar/index.d.ts +2 -0
- package/dist/components/Avatar/index.js +1 -0
- package/dist/components/Avatar/types.d.ts +47 -0
- package/dist/components/Avatar/types.js +1 -0
- package/dist/components/Badge/Badge.svelte +53 -0
- package/dist/components/Badge/Badge.svelte.d.ts +4 -0
- package/dist/components/Badge/badge.css +57 -0
- package/dist/components/Badge/index.d.ts +2 -0
- package/dist/components/Badge/index.js +1 -0
- package/dist/components/Badge/types.d.ts +43 -0
- package/dist/components/Badge/types.js +1 -0
- package/dist/components/Button/Button.svelte +85 -0
- package/dist/components/Button/Button.svelte.d.ts +4 -0
- package/dist/components/Button/button.css +132 -0
- package/dist/components/Button/index.d.ts +2 -0
- package/dist/components/Button/index.js +1 -0
- package/dist/components/Button/types.d.ts +76 -0
- package/dist/components/Button/types.js +1 -0
- package/dist/components/ButtonGroup/ButtonGroup.svelte +61 -0
- package/dist/components/ButtonGroup/ButtonGroup.svelte.d.ts +4 -0
- package/dist/components/ButtonGroup/button-group.css +39 -0
- package/dist/components/ButtonGroup/index.d.ts +2 -0
- package/dist/components/ButtonGroup/index.js +1 -0
- package/dist/components/ButtonGroup/types.d.ts +48 -0
- package/dist/components/ButtonGroup/types.js +1 -0
- package/dist/components/Card/Card.svelte +53 -0
- package/dist/components/Card/Card.svelte.d.ts +4 -0
- package/dist/components/Card/card.css +62 -0
- package/dist/components/Card/index.d.ts +2 -0
- package/dist/components/Card/index.js +1 -0
- package/dist/components/Card/types.d.ts +56 -0
- package/dist/components/Card/types.js +1 -0
- package/dist/components/Checkbox/Checkbox.svelte +105 -0
- package/dist/components/Checkbox/Checkbox.svelte.d.ts +4 -0
- package/dist/components/Checkbox/checkbox.css +138 -0
- package/dist/components/Checkbox/index.d.ts +2 -0
- package/dist/components/Checkbox/index.js +1 -0
- package/dist/components/Checkbox/types.d.ts +76 -0
- package/dist/components/Checkbox/types.js +1 -0
- package/dist/components/Chip/Chip.svelte +109 -0
- package/dist/components/Chip/Chip.svelte.d.ts +4 -0
- package/dist/components/Chip/chip.css +101 -0
- package/dist/components/Chip/index.d.ts +2 -0
- package/dist/components/Chip/index.js +1 -0
- package/dist/components/Chip/types.d.ts +69 -0
- package/dist/components/Chip/types.js +1 -0
- package/dist/components/Choice/Choice.svelte +60 -0
- package/dist/components/Choice/Choice.svelte.d.ts +4 -0
- package/dist/components/Choice/choice.css +75 -0
- package/dist/components/Choice/types.d.ts +43 -0
- package/dist/components/Choice/types.js +1 -0
- package/dist/components/Divider/Divider.svelte +19 -0
- package/dist/components/Divider/Divider.svelte.d.ts +4 -0
- package/dist/components/Divider/divider.css +23 -0
- package/dist/components/Divider/index.d.ts +2 -0
- package/dist/components/Divider/index.js +1 -0
- package/dist/components/Divider/types.d.ts +10 -0
- package/dist/components/Divider/types.js +1 -0
- package/dist/components/Field/Field.svelte +95 -0
- package/dist/components/Field/Field.svelte.d.ts +4 -0
- package/dist/components/Field/field.css +210 -0
- package/dist/components/Field/index.d.ts +3 -0
- package/dist/components/Field/index.js +2 -0
- package/dist/components/Field/types.d.ts +109 -0
- package/dist/components/Field/types.js +2 -0
- package/dist/components/Form/Form.svelte +93 -0
- package/dist/components/Form/Form.svelte.d.ts +8 -0
- package/dist/components/Form/form.css +16 -0
- package/dist/components/Form/index.d.ts +2 -0
- package/dist/components/Form/index.js +1 -0
- package/dist/components/Form/types.d.ts +38 -0
- package/dist/components/Form/types.js +1 -0
- package/dist/components/Icon/Icon.svelte +38 -0
- package/dist/components/Icon/Icon.svelte.d.ts +4 -0
- package/dist/components/Icon/icon.css +33 -0
- package/dist/components/Icon/index.d.ts +2 -0
- package/dist/components/Icon/index.js +1 -0
- package/dist/components/Icon/types.d.ts +34 -0
- package/dist/components/Icon/types.js +1 -0
- package/dist/components/List/List.svelte +39 -0
- package/dist/components/List/List.svelte.d.ts +4 -0
- package/dist/components/List/ListItem.svelte +57 -0
- package/dist/components/List/ListItem.svelte.d.ts +4 -0
- package/dist/components/List/index.d.ts +3 -0
- package/dist/components/List/index.js +2 -0
- package/dist/components/List/list.css +127 -0
- package/dist/components/List/types.d.ts +67 -0
- package/dist/components/List/types.js +1 -0
- package/dist/components/NativeSelect/NativeSelect.svelte +110 -0
- package/dist/components/NativeSelect/NativeSelect.svelte.d.ts +25 -0
- package/dist/components/NativeSelect/index.d.ts +2 -0
- package/dist/components/NativeSelect/index.js +1 -0
- package/dist/components/NativeSelect/native-select.css +45 -0
- package/dist/components/NativeSelect/types.d.ts +58 -0
- package/dist/components/NativeSelect/types.js +1 -0
- package/dist/components/NumberField/NumberField.svelte +246 -0
- package/dist/components/NumberField/NumberField.svelte.d.ts +4 -0
- package/dist/components/NumberField/index.d.ts +2 -0
- package/dist/components/NumberField/index.js +1 -0
- package/dist/components/NumberField/number-field.css +74 -0
- package/dist/components/NumberField/types.d.ts +73 -0
- package/dist/components/NumberField/types.js +1 -0
- package/dist/components/Progress/ProgressCircular.svelte +61 -0
- package/dist/components/Progress/ProgressCircular.svelte.d.ts +4 -0
- package/dist/components/Progress/ProgressLinear.svelte +44 -0
- package/dist/components/Progress/ProgressLinear.svelte.d.ts +4 -0
- package/dist/components/Progress/index.d.ts +3 -0
- package/dist/components/Progress/index.js +2 -0
- package/dist/components/Progress/progress-circular.css +61 -0
- package/dist/components/Progress/progress-linear.css +67 -0
- package/dist/components/Progress/types.d.ts +53 -0
- package/dist/components/Progress/types.js +1 -0
- package/dist/components/RadioGroup/RadioGroup.svelte +149 -0
- package/dist/components/RadioGroup/RadioGroup.svelte.d.ts +25 -0
- package/dist/components/RadioGroup/index.d.ts +2 -0
- package/dist/components/RadioGroup/index.js +1 -0
- package/dist/components/RadioGroup/radio-group.css +139 -0
- package/dist/components/RadioGroup/types.d.ts +93 -0
- package/dist/components/RadioGroup/types.js +1 -0
- package/dist/components/SegmentedControl/SegmentedControl.svelte +70 -0
- package/dist/components/SegmentedControl/SegmentedControl.svelte.d.ts +25 -0
- package/dist/components/SegmentedControl/index.d.ts +2 -0
- package/dist/components/SegmentedControl/index.js +1 -0
- package/dist/components/SegmentedControl/segmented-control.css +68 -0
- package/dist/components/SegmentedControl/types.d.ts +60 -0
- package/dist/components/SegmentedControl/types.js +1 -0
- package/dist/components/Skeleton/Skeleton.svelte +33 -0
- package/dist/components/Skeleton/Skeleton.svelte.d.ts +4 -0
- package/dist/components/Skeleton/index.d.ts +2 -0
- package/dist/components/Skeleton/index.js +1 -0
- package/dist/components/Skeleton/skeleton.css +70 -0
- package/dist/components/Skeleton/types.d.ts +25 -0
- package/dist/components/Skeleton/types.js +1 -0
- package/dist/components/Slider/Slider.svelte +153 -0
- package/dist/components/Slider/Slider.svelte.d.ts +4 -0
- package/dist/components/Slider/index.d.ts +2 -0
- package/dist/components/Slider/index.js +1 -0
- package/dist/components/Slider/slider.css +206 -0
- package/dist/components/Slider/types.d.ts +102 -0
- package/dist/components/Slider/types.js +1 -0
- package/dist/components/Switch/Switch.svelte +94 -0
- package/dist/components/Switch/Switch.svelte.d.ts +4 -0
- package/dist/components/Switch/index.d.ts +2 -0
- package/dist/components/Switch/index.js +1 -0
- package/dist/components/Switch/switch.css +118 -0
- package/dist/components/Switch/types.d.ts +67 -0
- package/dist/components/Switch/types.js +1 -0
- package/dist/components/Tabs/Tabs.svelte +93 -0
- package/dist/components/Tabs/Tabs.svelte.d.ts +25 -0
- package/dist/components/Tabs/index.d.ts +2 -0
- package/dist/components/Tabs/index.js +1 -0
- package/dist/components/Tabs/tabs.css +73 -0
- package/dist/components/Tabs/types.d.ts +59 -0
- package/dist/components/Tabs/types.js +1 -0
- package/dist/components/TextField/TextField.svelte +115 -0
- package/dist/components/TextField/TextField.svelte.d.ts +4 -0
- package/dist/components/TextField/index.d.ts +2 -0
- package/dist/components/TextField/index.js +1 -0
- package/dist/components/TextField/types.d.ts +57 -0
- package/dist/components/TextField/types.js +1 -0
- package/dist/components/Textarea/Textarea.svelte +116 -0
- package/dist/components/Textarea/Textarea.svelte.d.ts +4 -0
- package/dist/components/Textarea/index.d.ts +2 -0
- package/dist/components/Textarea/index.js +1 -0
- package/dist/components/Textarea/textarea.css +35 -0
- package/dist/components/Textarea/types.d.ts +60 -0
- package/dist/components/Textarea/types.js +1 -0
- package/dist/core/defaults/DefaultsProvider.svelte +30 -0
- package/dist/core/defaults/DefaultsProvider.svelte.d.ts +13 -0
- package/dist/core/defaults/context.d.ts +45 -0
- package/dist/core/defaults/context.js +38 -0
- package/dist/core/display/display.d.ts +39 -0
- package/dist/core/display/display.js +53 -0
- package/dist/core/errors/codes.d.ts +112 -0
- package/dist/core/errors/codes.js +96 -0
- package/dist/core/errors/errors.d.ts +8 -0
- package/dist/core/errors/errors.js +7 -0
- package/dist/core/form/constraints.d.ts +15 -0
- package/dist/core/form/constraints.js +42 -0
- package/dist/core/form/context.d.ts +19 -0
- package/dist/core/form/context.js +4 -0
- package/dist/core/form/field.svelte.d.ts +34 -0
- package/dist/core/form/field.svelte.js +66 -0
- package/dist/core/form/number.d.ts +24 -0
- package/dist/core/form/number.js +71 -0
- package/dist/core/form/types.d.ts +22 -0
- package/dist/core/form/types.js +1 -0
- package/dist/core/locale/locale.d.ts +55 -0
- package/dist/core/locale/locale.js +80 -0
- package/dist/core/props/classes.d.ts +21 -0
- package/dist/core/props/classes.js +44 -0
- package/dist/core/props/element.d.ts +18 -0
- package/dist/core/props/element.js +1 -0
- package/dist/core/props/types.d.ts +47 -0
- package/dist/core/props/types.js +16 -0
- package/dist/core/provider/UIProvider.svelte +60 -0
- package/dist/core/provider/UIProvider.svelte.d.ts +29 -0
- package/dist/core/theme/ThemeProvider.svelte +178 -0
- package/dist/core/theme/ThemeProvider.svelte.d.ts +25 -0
- package/dist/core/theme/color.d.ts +23 -0
- package/dist/core/theme/color.js +54 -0
- package/dist/core/theme/context.d.ts +25 -0
- package/dist/core/theme/context.js +14 -0
- package/dist/core/theme/css.d.ts +25 -0
- package/dist/core/theme/css.js +69 -0
- package/dist/core/theme/define.d.ts +11 -0
- package/dist/core/theme/define.js +21 -0
- package/dist/core/theme/resolve.d.ts +6 -0
- package/dist/core/theme/resolve.js +18 -0
- package/dist/core/theme/theme-provider.css +13 -0
- package/dist/core/theme/types.d.ts +35 -0
- package/dist/core/theme/types.js +61 -0
- package/dist/dev/off.d.ts +4 -0
- package/dist/dev/off.js +3 -0
- package/dist/dev/on.d.ts +8 -0
- package/dist/dev/on.js +33 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +72 -0
- package/dist/primitives/anchored/anchored.d.ts +39 -0
- package/dist/primitives/anchored/anchored.js +104 -0
- package/dist/primitives/anchored/fallback.d.ts +31 -0
- package/dist/primitives/anchored/fallback.js +73 -0
- package/dist/primitives/focus/focus-trap.d.ts +25 -0
- package/dist/primitives/focus/focus-trap.js +86 -0
- package/dist/primitives/focus/roving-focus.d.ts +22 -0
- package/dist/primitives/focus/roving-focus.js +86 -0
- package/dist/primitives/focus/tabbable.d.ts +9 -0
- package/dist/primitives/focus/tabbable.js +75 -0
- package/dist/primitives/inert/inert-others.d.ts +12 -0
- package/dist/primitives/inert/inert-others.js +44 -0
- package/dist/primitives/keyboard/keymap.d.ts +17 -0
- package/dist/primitives/keyboard/keymap.js +39 -0
- package/dist/primitives/keyboard/typeahead.d.ts +18 -0
- package/dist/primitives/keyboard/typeahead.js +50 -0
- package/dist/primitives/layer/dismissable.d.ts +26 -0
- package/dist/primitives/layer/dismissable.js +84 -0
- package/dist/primitives/motion/easing.d.ts +9 -0
- package/dist/primitives/motion/easing.js +46 -0
- package/dist/primitives/motion/motion.d.ts +59 -0
- package/dist/primitives/motion/motion.js +80 -0
- package/dist/primitives/portal/Portal.svelte +37 -0
- package/dist/primitives/portal/Portal.svelte.d.ts +9 -0
- package/dist/primitives/portal/PortalContent.svelte +15 -0
- package/dist/primitives/portal/PortalContent.svelte.d.ts +9 -0
- package/dist/primitives/portal/portal.css +8 -0
- package/dist/primitives/scroll-lock/scroll-lock.d.ts +10 -0
- package/dist/primitives/scroll-lock/scroll-lock.js +33 -0
- package/dist/primitives/virtual/virtual-list.svelte.d.ts +65 -0
- package/dist/primitives/virtual/virtual-list.svelte.js +291 -0
- package/dist/styles/base.css +44 -0
- package/dist/styles/props.css +137 -0
- package/dist/styles/tokens.css +84 -0
- package/dist/styles/visually-hidden.css +16 -0
- package/dist/themes/aurora.d.ts +13 -0
- package/dist/themes/aurora.js +103 -0
- package/dist/themes/heritage.d.ts +13 -0
- package/dist/themes/heritage.js +102 -0
- package/dist/themes/maison.d.ts +13 -0
- package/dist/themes/maison.js +99 -0
- package/dist/themes/milano.d.ts +13 -0
- package/dist/themes/milano.js +101 -0
- package/dist/themes/monolith.d.ts +13 -0
- package/dist/themes/monolith.js +101 -0
- package/package.json +87 -5
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
@layer sv.tokens, sv.base, sv.components, sv.utilities;
|
|
2
|
+
|
|
3
|
+
@layer sv.components {
|
|
4
|
+
/*
|
|
5
|
+
* A label row with the value at its end, the platform's range input, ticks and a hint or an
|
|
6
|
+
* error. The thumb is φ⁻¹·⁵ of the control height and the track a quarter of the thumb. The
|
|
7
|
+
* filled part ends under the thumb's centre, which travels the track less one thumb.
|
|
8
|
+
*/
|
|
9
|
+
.sv-slider {
|
|
10
|
+
--sv-slider-thumb: calc(var(--sv-size-height) * 0.472);
|
|
11
|
+
--sv-slider-track: calc(var(--sv-slider-thumb) * 0.236);
|
|
12
|
+
--sv-slider-fill: calc(
|
|
13
|
+
var(--sv-slider-thumb) / 2 + var(--sv-slider-fraction) * (100% - var(--sv-slider-thumb))
|
|
14
|
+
);
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
min-inline-size: 0;
|
|
18
|
+
color: var(--sv-color-text);
|
|
19
|
+
font-family: var(--sv-font-ui);
|
|
20
|
+
font-size: var(--sv-size-text);
|
|
21
|
+
line-height: var(--sv-leading-snug);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.sv-slider--invalid {
|
|
25
|
+
--sv-color: var(--sv-color-error);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.sv-slider__header {
|
|
29
|
+
display: flex;
|
|
30
|
+
gap: var(--sv-space-8);
|
|
31
|
+
align-items: baseline;
|
|
32
|
+
justify-content: space-between;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* The theme's label voice, as a field's label. */
|
|
36
|
+
.sv-slider__label {
|
|
37
|
+
font-size: var(--sv-label-size);
|
|
38
|
+
font-weight: var(--sv-weight-medium);
|
|
39
|
+
letter-spacing: var(--sv-label-tracking);
|
|
40
|
+
text-transform: var(--sv-label-transform);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.sv-slider__value {
|
|
44
|
+
font-weight: var(--sv-weight-medium);
|
|
45
|
+
font-variant-numeric: tabular-nums;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* The input is the press target, at least 24px tall. */
|
|
49
|
+
.sv-slider__input {
|
|
50
|
+
box-sizing: border-box;
|
|
51
|
+
inline-size: 100%;
|
|
52
|
+
block-size: max(1.5rem, var(--sv-size-height) * var(--sv-density) * 0.618);
|
|
53
|
+
margin: 0;
|
|
54
|
+
padding: 0;
|
|
55
|
+
border-radius: var(--sv-radius-sm);
|
|
56
|
+
background: transparent;
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
appearance: none;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.sv-slider__input:focus-visible {
|
|
62
|
+
outline-color: var(--sv-color);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.sv-slider__input:disabled {
|
|
66
|
+
cursor: not-allowed;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Chromium and Safari: the filled part is a gradient on the track. */
|
|
70
|
+
.sv-slider__input::-webkit-slider-runnable-track {
|
|
71
|
+
block-size: var(--sv-slider-track);
|
|
72
|
+
border-radius: var(--sv-radius-full);
|
|
73
|
+
background: linear-gradient(
|
|
74
|
+
to right,
|
|
75
|
+
var(--sv-color) var(--sv-slider-fill),
|
|
76
|
+
var(--sv-color-border-strong) var(--sv-slider-fill)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* An attribute selector, as minifiers rewrite :dir() into language guesses. */
|
|
81
|
+
:where([dir='rtl']) .sv-slider__input::-webkit-slider-runnable-track {
|
|
82
|
+
background: linear-gradient(
|
|
83
|
+
to left,
|
|
84
|
+
var(--sv-color) var(--sv-slider-fill),
|
|
85
|
+
var(--sv-color-border-strong) var(--sv-slider-fill)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.sv-slider__input::-webkit-slider-thumb {
|
|
90
|
+
box-sizing: border-box;
|
|
91
|
+
inline-size: var(--sv-slider-thumb);
|
|
92
|
+
block-size: var(--sv-slider-thumb);
|
|
93
|
+
margin-block-start: calc((var(--sv-slider-track) - var(--sv-slider-thumb)) / 2);
|
|
94
|
+
border: calc(var(--sv-hairline) * 2) solid var(--sv-color);
|
|
95
|
+
border-radius: var(--sv-radius-full);
|
|
96
|
+
background: var(--sv-color-surface);
|
|
97
|
+
appearance: none;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* Firefox draws the filled part itself. */
|
|
101
|
+
.sv-slider__input::-moz-range-track {
|
|
102
|
+
block-size: var(--sv-slider-track);
|
|
103
|
+
border-radius: var(--sv-radius-full);
|
|
104
|
+
background: var(--sv-color-border-strong);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.sv-slider__input::-moz-range-progress {
|
|
108
|
+
block-size: var(--sv-slider-track);
|
|
109
|
+
border-radius: var(--sv-radius-full);
|
|
110
|
+
background: var(--sv-color);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.sv-slider__input::-moz-range-thumb {
|
|
114
|
+
box-sizing: border-box;
|
|
115
|
+
inline-size: var(--sv-slider-thumb);
|
|
116
|
+
block-size: var(--sv-slider-thumb);
|
|
117
|
+
border: calc(var(--sv-hairline) * 2) solid var(--sv-color);
|
|
118
|
+
border-radius: var(--sv-radius-full);
|
|
119
|
+
background: var(--sv-color-surface);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* Hover and press: a halo in the accent around the thumb. */
|
|
123
|
+
@media (hover: hover) {
|
|
124
|
+
.sv-slider__input:enabled:hover::-webkit-slider-thumb {
|
|
125
|
+
box-shadow: 0 0 0 calc(var(--sv-slider-thumb) * 0.382)
|
|
126
|
+
color-mix(in oklch, var(--sv-color) var(--sv-state-hover), transparent);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.sv-slider__input:enabled:hover::-moz-range-thumb {
|
|
130
|
+
box-shadow: 0 0 0 calc(var(--sv-slider-thumb) * 0.382)
|
|
131
|
+
color-mix(in oklch, var(--sv-color) var(--sv-state-hover), transparent);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.sv-slider__input:enabled:active::-webkit-slider-thumb {
|
|
136
|
+
box-shadow: 0 0 0 calc(var(--sv-slider-thumb) * 0.382)
|
|
137
|
+
color-mix(in oklch, var(--sv-color) var(--sv-state-press), transparent);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.sv-slider__input:enabled:active::-moz-range-thumb {
|
|
141
|
+
box-shadow: 0 0 0 calc(var(--sv-slider-thumb) * 0.382)
|
|
142
|
+
color-mix(in oklch, var(--sv-color) var(--sv-state-press), transparent);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/*
|
|
146
|
+
* Ticks share one grid cell, each pushed along by the same distance as the thumb's centre.
|
|
147
|
+
* They are as wide as nothing, so a label centres on its tick; the end labels lean inwards.
|
|
148
|
+
*/
|
|
149
|
+
.sv-slider__ticks {
|
|
150
|
+
display: grid;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.sv-slider__tick {
|
|
154
|
+
display: flex;
|
|
155
|
+
flex-direction: column;
|
|
156
|
+
grid-area: 1 / 1;
|
|
157
|
+
align-items: center;
|
|
158
|
+
inline-size: 0;
|
|
159
|
+
margin-inline-start: calc(
|
|
160
|
+
var(--sv-slider-thumb) / 2 + var(--sv-slider-at) * (100% - var(--sv-slider-thumb))
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.sv-slider__tick--start {
|
|
165
|
+
align-items: flex-start;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.sv-slider__tick--end {
|
|
169
|
+
align-items: flex-end;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.sv-slider__tick::before {
|
|
173
|
+
content: '';
|
|
174
|
+
inline-size: var(--sv-hairline);
|
|
175
|
+
block-size: 0.382em;
|
|
176
|
+
background-color: var(--sv-color-border-strong);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.sv-slider__tick-label {
|
|
180
|
+
margin-block-start: var(--sv-space-3);
|
|
181
|
+
color: var(--sv-color-text-muted);
|
|
182
|
+
font-size: var(--sv-text-xs);
|
|
183
|
+
white-space: nowrap;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* Padding, not margin: an app's paragraph reset (p { margin: 0 }) would win over a layer. */
|
|
187
|
+
.sv-slider__hint,
|
|
188
|
+
.sv-slider__error {
|
|
189
|
+
margin: 0;
|
|
190
|
+
padding-block-start: var(--sv-space-5);
|
|
191
|
+
font-size: var(--sv-text-xs);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.sv-slider__hint {
|
|
195
|
+
color: var(--sv-color-text-muted);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.sv-slider__error {
|
|
199
|
+
color: var(--sv-color-error);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* The hint stays legible when the slider is off: it often says why. */
|
|
203
|
+
.sv-slider--disabled > :is(.sv-slider__header, .sv-slider__input, .sv-slider__ticks) {
|
|
204
|
+
opacity: 0.382;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
2
|
+
import type { FieldError, Rule, ValidateOn } from '../../core/form/types.js';
|
|
3
|
+
import type { ElementProps } from '../../core/props/element.js';
|
|
4
|
+
import type { Color, Density, Size } from '../../core/props/types.js';
|
|
5
|
+
/** The Slider props that `DefaultsProvider` and `UIProvider` can set for a subtree. */
|
|
6
|
+
export interface SliderDefaults {
|
|
7
|
+
/**
|
|
8
|
+
* The colour of the filled part of the track and of the thumb's ring. An invalid slider uses
|
|
9
|
+
* `error`.
|
|
10
|
+
* @default 'primary'
|
|
11
|
+
*/
|
|
12
|
+
color: Color;
|
|
13
|
+
/**
|
|
14
|
+
* The size of the thumb, the track and the text.
|
|
15
|
+
* @default 'md'
|
|
16
|
+
*/
|
|
17
|
+
size: Size;
|
|
18
|
+
/**
|
|
19
|
+
* Multiplies the height of the slider's row, never the text.
|
|
20
|
+
* @default 'default'
|
|
21
|
+
*/
|
|
22
|
+
density: Density;
|
|
23
|
+
/**
|
|
24
|
+
* Mutes the slider and disables its input.
|
|
25
|
+
* @default false
|
|
26
|
+
*/
|
|
27
|
+
disabled: boolean;
|
|
28
|
+
}
|
|
29
|
+
declare module '../../core/defaults/context.js' {
|
|
30
|
+
interface ComponentDefaults {
|
|
31
|
+
Slider: SliderDefaults;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** A labelled tick under a Slider's track. */
|
|
35
|
+
export interface SliderTick {
|
|
36
|
+
value: number;
|
|
37
|
+
/** What the tick says. Read out as the slider's value while it sits on the tick. */
|
|
38
|
+
label: string;
|
|
39
|
+
}
|
|
40
|
+
type SliderOwnProps = Partial<SliderDefaults> & {
|
|
41
|
+
/**
|
|
42
|
+
* The value, between `min` and `max`, on a step. Bindable.
|
|
43
|
+
* @default min
|
|
44
|
+
*/
|
|
45
|
+
value?: number;
|
|
46
|
+
/** @default 0 */
|
|
47
|
+
min?: number;
|
|
48
|
+
/** @default 100 */
|
|
49
|
+
max?: number;
|
|
50
|
+
/**
|
|
51
|
+
* The distance between the values the slider stops at.
|
|
52
|
+
* @default 1
|
|
53
|
+
*/
|
|
54
|
+
step?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Marks under the track: `true` for every step, a list of values, or values with labels such
|
|
57
|
+
* as `{ value: 50, label: 'Balanced' }`.
|
|
58
|
+
*/
|
|
59
|
+
ticks?: boolean | readonly number[] | readonly SliderTick[];
|
|
60
|
+
/**
|
|
61
|
+
* How the value reads, beside the label and to screen readers, for example
|
|
62
|
+
* `(value) => value + '%'`. Defaults to the number in the locale's format, or the label of the
|
|
63
|
+
* tick the slider sits on.
|
|
64
|
+
*/
|
|
65
|
+
format?: (value: number) => string;
|
|
66
|
+
/** The visible label, linked to the input. Without it, name the slider with `aria-label`. */
|
|
67
|
+
label?: string;
|
|
68
|
+
/** Help under the slider. An error replaces it. */
|
|
69
|
+
hint?: string;
|
|
70
|
+
/**
|
|
71
|
+
* An error from outside the slider, such as the server's answer: a message, a list of which
|
|
72
|
+
* the first is shown, or `true` for an invalid state described elsewhere.
|
|
73
|
+
*/
|
|
74
|
+
error?: FieldError;
|
|
75
|
+
/** Checks run in order on the value. The first message is shown. */
|
|
76
|
+
rules?: readonly Rule<number>[];
|
|
77
|
+
/** When the message shows. Defaults to the form's setting, else `'blur'`. */
|
|
78
|
+
validateOn?: ValidateOn;
|
|
79
|
+
/** Inline style of the slider's root element. */
|
|
80
|
+
style?: string | null;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* A number picked by dragging a thumb along a track: the platform's own range input, so the
|
|
84
|
+
* keyboard, touch, forms and right-to-left pages work as they always do. The value shows beside
|
|
85
|
+
* the label. A range always holds a value, so it takes no `required`. `class` and `style` apply
|
|
86
|
+
* to the root; `id`, `ref`, `name` and every other attribute go to the `<input type="range">`.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* <Slider
|
|
90
|
+
* label="Risk"
|
|
91
|
+
* name="risk"
|
|
92
|
+
* step={25}
|
|
93
|
+
* ticks={[
|
|
94
|
+
* { value: 0, label: 'Careful' },
|
|
95
|
+
* { value: 50, label: 'Balanced' },
|
|
96
|
+
* { value: 100, label: 'Bold' }
|
|
97
|
+
* ]}
|
|
98
|
+
* bind:value={risk}
|
|
99
|
+
* />
|
|
100
|
+
*/
|
|
101
|
+
export type SliderProps = ElementProps<HTMLInputElement, Omit<HTMLInputAttributes, 'type' | 'children' | 'required'>, SliderOwnProps>;
|
|
102
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// A setting that is on or off, with its label beside it and validation. See SwitchProps.
|
|
3
|
+
import { untrack } from 'svelte';
|
|
4
|
+
import { DEV, warnUnnamed } from '#dev';
|
|
5
|
+
import { useDefaults } from '../../core/defaults/context.js';
|
|
6
|
+
import { createField } from '../../core/form/field.svelte.js';
|
|
7
|
+
import { useLocale } from '../../core/locale/locale.js';
|
|
8
|
+
import { sharedClasses } from '../../core/props/classes.js';
|
|
9
|
+
import Choice from '../Choice/Choice.svelte';
|
|
10
|
+
import './switch.css';
|
|
11
|
+
import type { SwitchDefaults, SwitchProps } from './types.js';
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
color,
|
|
15
|
+
size,
|
|
16
|
+
density,
|
|
17
|
+
disabled,
|
|
18
|
+
checked = $bindable(false),
|
|
19
|
+
label,
|
|
20
|
+
hint,
|
|
21
|
+
error,
|
|
22
|
+
rules,
|
|
23
|
+
validateOn,
|
|
24
|
+
required = false,
|
|
25
|
+
id,
|
|
26
|
+
'aria-describedby': describedBy,
|
|
27
|
+
class: className,
|
|
28
|
+
style,
|
|
29
|
+
ref = $bindable(null),
|
|
30
|
+
...rest
|
|
31
|
+
}: SwitchProps = $props();
|
|
32
|
+
|
|
33
|
+
const resolve = useDefaults<SwitchDefaults>('Switch', {
|
|
34
|
+
color: 'primary',
|
|
35
|
+
size: 'md',
|
|
36
|
+
density: 'default',
|
|
37
|
+
disabled: false
|
|
38
|
+
});
|
|
39
|
+
const p = $derived(resolve({ color, size, density, disabled }));
|
|
40
|
+
const locale = useLocale();
|
|
41
|
+
// A form reset goes back to the state the switch started in.
|
|
42
|
+
const initial = untrack(() => checked ?? false);
|
|
43
|
+
|
|
44
|
+
const field = createField<boolean>({
|
|
45
|
+
value: () => checked ?? false,
|
|
46
|
+
// Rules run on a switch that is on only, as they run on a filled field only.
|
|
47
|
+
empty: (on) => !on,
|
|
48
|
+
check: (on) => (!on && required ? locale.t('required') : undefined),
|
|
49
|
+
rules: () => rules,
|
|
50
|
+
error: () => error,
|
|
51
|
+
validateOn: () => validateOn,
|
|
52
|
+
control: () => ref
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (DEV) $effect(() => warnUnnamed('Switch', ref));
|
|
56
|
+
|
|
57
|
+
// A toggle is a whole edit, so it also counts as leaving: the message shows at once, even in
|
|
58
|
+
// Safari, where a click does not focus the input.
|
|
59
|
+
function toggled() {
|
|
60
|
+
field.changed();
|
|
61
|
+
field.left();
|
|
62
|
+
}
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<Choice
|
|
66
|
+
{id}
|
|
67
|
+
{label}
|
|
68
|
+
{hint}
|
|
69
|
+
error={field.error}
|
|
70
|
+
{required}
|
|
71
|
+
disabled={p.disabled}
|
|
72
|
+
class={['sv-switch', sharedClasses(p, 'Switch'), className]}
|
|
73
|
+
{style}
|
|
74
|
+
onchange={toggled}
|
|
75
|
+
>
|
|
76
|
+
{#snippet children(attributes)}
|
|
77
|
+
<span class="sv-switch__control">
|
|
78
|
+
<input
|
|
79
|
+
bind:this={ref}
|
|
80
|
+
defaultChecked={initial}
|
|
81
|
+
{...rest}
|
|
82
|
+
{...attributes}
|
|
83
|
+
aria-describedby={[describedBy, attributes['aria-describedby']].filter(Boolean).join(' ') ||
|
|
84
|
+
undefined}
|
|
85
|
+
type="checkbox"
|
|
86
|
+
role="switch"
|
|
87
|
+
class="sv-switch__input"
|
|
88
|
+
bind:checked
|
|
89
|
+
/>
|
|
90
|
+
<span class="sv-switch__track" aria-hidden="true"><span class="sv-switch__thumb"></span></span
|
|
91
|
+
>
|
|
92
|
+
</span>
|
|
93
|
+
{/snippet}
|
|
94
|
+
</Choice>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Switch } from './Switch.svelte';
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
@layer sv.tokens, sv.base, sv.components, sv.utilities;
|
|
2
|
+
|
|
3
|
+
@layer sv.components {
|
|
4
|
+
/*
|
|
5
|
+
* A track as tall as a line of the label and φ times as wide, with the theme's round radius
|
|
6
|
+
* (square in a square theme). The thumb travels the difference.
|
|
7
|
+
*/
|
|
8
|
+
.sv-switch {
|
|
9
|
+
--sv-switch-block: 1.272em;
|
|
10
|
+
--sv-switch-inline: calc(var(--sv-switch-block) * 1.618);
|
|
11
|
+
--sv-switch-pad: calc(var(--sv-switch-block) * 0.146);
|
|
12
|
+
--sv-switch-travel: calc(var(--sv-switch-inline) - var(--sv-switch-block));
|
|
13
|
+
--sv-choice-control: var(--sv-switch-inline);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
* The native input lies over the drawn track, invisible, so focus, keys and forms stay native.
|
|
18
|
+
* The track sits on the first line of the label.
|
|
19
|
+
*/
|
|
20
|
+
.sv-switch__control {
|
|
21
|
+
position: relative;
|
|
22
|
+
display: inline-flex;
|
|
23
|
+
flex: none;
|
|
24
|
+
margin-block-start: calc((1lh - var(--sv-switch-block)) / 2);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.sv-switch__input {
|
|
28
|
+
position: absolute;
|
|
29
|
+
inset: 0;
|
|
30
|
+
inline-size: 100%;
|
|
31
|
+
block-size: 100%;
|
|
32
|
+
margin: 0;
|
|
33
|
+
opacity: 0;
|
|
34
|
+
cursor: inherit;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Transparent borders turn visible in forced colours, where backgrounds are dropped. */
|
|
38
|
+
.sv-switch__track {
|
|
39
|
+
position: relative;
|
|
40
|
+
box-sizing: border-box;
|
|
41
|
+
inline-size: var(--sv-switch-inline);
|
|
42
|
+
block-size: var(--sv-switch-block);
|
|
43
|
+
border: var(--sv-hairline) solid transparent;
|
|
44
|
+
border-radius: var(--sv-radius-full);
|
|
45
|
+
background-color: var(--sv-color-border-strong);
|
|
46
|
+
pointer-events: none;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* The fill (before) fades in under the thumb; the state layer (after) sits over both. */
|
|
50
|
+
.sv-switch__track::before,
|
|
51
|
+
.sv-switch__track::after {
|
|
52
|
+
content: '';
|
|
53
|
+
position: absolute;
|
|
54
|
+
inset: calc(var(--sv-hairline) * -1);
|
|
55
|
+
border-radius: inherit;
|
|
56
|
+
opacity: 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.sv-switch__track::before {
|
|
60
|
+
background-color: var(--sv-color);
|
|
61
|
+
transition: opacity var(--sv-duration-fast) var(--sv-ease);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.sv-switch__track::after {
|
|
65
|
+
background-color: var(--sv-color-text);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.sv-switch__thumb {
|
|
69
|
+
position: absolute;
|
|
70
|
+
inset-block-start: calc(var(--sv-switch-pad) - var(--sv-hairline));
|
|
71
|
+
inset-inline-start: calc(var(--sv-switch-pad) - var(--sv-hairline));
|
|
72
|
+
box-sizing: border-box;
|
|
73
|
+
inline-size: calc(var(--sv-switch-block) - 2 * var(--sv-switch-pad));
|
|
74
|
+
block-size: calc(var(--sv-switch-block) - 2 * var(--sv-switch-pad));
|
|
75
|
+
border: var(--sv-hairline) solid transparent;
|
|
76
|
+
border-radius: var(--sv-radius-full);
|
|
77
|
+
background-color: var(--sv-color-surface);
|
|
78
|
+
transition: translate var(--sv-duration-fast) var(--sv-ease);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.sv-switch__input:checked + .sv-switch__track::before {
|
|
82
|
+
opacity: 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.sv-switch__input:checked + .sv-switch__track::after {
|
|
86
|
+
background-color: var(--sv-on-color);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.sv-switch__input:checked + .sv-switch__track .sv-switch__thumb {
|
|
90
|
+
translate: var(--sv-switch-travel) 0;
|
|
91
|
+
background-color: var(--sv-on-color);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* An attribute selector, as minifiers rewrite :dir() into language guesses. */
|
|
95
|
+
:where([dir='rtl']) .sv-switch__input:checked + .sv-switch__track .sv-switch__thumb {
|
|
96
|
+
translate: calc(var(--sv-switch-travel) * -1) 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* Hover and press mix a layer over the track. The disabled guard adds no weight. */
|
|
100
|
+
@media (hover: hover) {
|
|
101
|
+
:where(.sv-switch:not(.sv-choice--disabled)) > .sv-choice__row:hover .sv-switch__track::after {
|
|
102
|
+
opacity: var(--sv-state-hover);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
:where(.sv-switch:not(.sv-choice--disabled)) > .sv-choice__row:active .sv-switch__track::after {
|
|
107
|
+
opacity: var(--sv-state-press);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.sv-switch__input:focus-visible + .sv-switch__track {
|
|
111
|
+
outline: var(--sv-focus-width) solid var(--sv-color);
|
|
112
|
+
outline-offset: var(--sv-focus-offset);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.sv-choice--invalid > .sv-choice__row .sv-switch__track {
|
|
116
|
+
background-color: var(--sv-color-error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
2
|
+
import type { Rule, ValidateOn } from '../../core/form/types.js';
|
|
3
|
+
import type { ElementProps } from '../../core/props/element.js';
|
|
4
|
+
import type { Color, Density, Size } from '../../core/props/types.js';
|
|
5
|
+
import type { ChoiceContentProps } from '../Choice/types.js';
|
|
6
|
+
/** The Switch props that `DefaultsProvider` and `UIProvider` can set for a subtree. */
|
|
7
|
+
export interface SwitchDefaults {
|
|
8
|
+
/**
|
|
9
|
+
* The colour of the track while on. An invalid switch uses `error`.
|
|
10
|
+
* @default 'primary'
|
|
11
|
+
*/
|
|
12
|
+
color: Color;
|
|
13
|
+
/**
|
|
14
|
+
* The size of the switch and the label text.
|
|
15
|
+
* @default 'md'
|
|
16
|
+
*/
|
|
17
|
+
size: Size;
|
|
18
|
+
/**
|
|
19
|
+
* Multiplies the height of the row, never the text.
|
|
20
|
+
* @default 'default'
|
|
21
|
+
*/
|
|
22
|
+
density: Density;
|
|
23
|
+
/**
|
|
24
|
+
* Mutes the switch and disables its input.
|
|
25
|
+
* @default false
|
|
26
|
+
*/
|
|
27
|
+
disabled: boolean;
|
|
28
|
+
}
|
|
29
|
+
declare module '../../core/defaults/context.js' {
|
|
30
|
+
interface ComponentDefaults {
|
|
31
|
+
Switch: SwitchDefaults;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
type SwitchOwnProps = Partial<SwitchDefaults> & ChoiceContentProps & {
|
|
35
|
+
/**
|
|
36
|
+
* Whether the switch is on. Bindable.
|
|
37
|
+
* @default false
|
|
38
|
+
*/
|
|
39
|
+
checked?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* What the form submits under `name` while the switch is on.
|
|
42
|
+
* @default 'on'
|
|
43
|
+
*/
|
|
44
|
+
value?: string;
|
|
45
|
+
/** Checks run in order after `required`, with the on state. The first message is shown. */
|
|
46
|
+
rules?: readonly Rule<boolean>[];
|
|
47
|
+
/** When the message shows. Defaults to the form's setting, else `'blur'`. */
|
|
48
|
+
validateOn?: ValidateOn;
|
|
49
|
+
/**
|
|
50
|
+
* The switch must be on. Adds an asterisk to the label.
|
|
51
|
+
* @default false
|
|
52
|
+
*/
|
|
53
|
+
required?: boolean;
|
|
54
|
+
/** Inline style of the switch's root element. */
|
|
55
|
+
style?: string | null;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* A setting that is on or off, with its label beside it: a native checkbox with
|
|
59
|
+
* `role="switch"`, drawn as a track and a thumb. `class` and `style` apply to the root; `id`,
|
|
60
|
+
* `ref`, `name` and every other attribute go to the `<input>`. Use it for a setting that takes
|
|
61
|
+
* effect at once; for a choice submitted with a form, use a Checkbox.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* <Switch label="Sign in with biometrics" bind:checked={biometrics} />
|
|
65
|
+
*/
|
|
66
|
+
export type SwitchProps = ElementProps<HTMLInputElement, Omit<HTMLInputAttributes, 'type' | 'role' | 'children'>, SwitchOwnProps>;
|
|
67
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends string | number">
|
|
2
|
+
// Tabs and the selected tab's panel. See TabsProps for the props.
|
|
3
|
+
import { useDefaults } from '../../core/defaults/context.js';
|
|
4
|
+
import { sharedClasses } from '../../core/props/classes.js';
|
|
5
|
+
import { rovingFocus } from '../../primitives/focus/roving-focus.js';
|
|
6
|
+
import './tabs.css';
|
|
7
|
+
import type { TabsDefaults, TabsItem, TabsProps } from './types.js';
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
color,
|
|
11
|
+
size,
|
|
12
|
+
density,
|
|
13
|
+
grow,
|
|
14
|
+
items,
|
|
15
|
+
value = $bindable(),
|
|
16
|
+
onchange,
|
|
17
|
+
activation = 'automatic',
|
|
18
|
+
panel,
|
|
19
|
+
id,
|
|
20
|
+
'aria-label': ariaLabel,
|
|
21
|
+
'aria-labelledby': ariaLabelledby,
|
|
22
|
+
class: className,
|
|
23
|
+
ref = $bindable(null),
|
|
24
|
+
...rest
|
|
25
|
+
}: TabsProps<T> = $props();
|
|
26
|
+
|
|
27
|
+
const resolve = useDefaults<TabsDefaults>('Tabs', {
|
|
28
|
+
color: 'primary',
|
|
29
|
+
size: 'md',
|
|
30
|
+
density: 'default',
|
|
31
|
+
grow: false
|
|
32
|
+
});
|
|
33
|
+
const p = $derived(resolve({ color, size, density, grow }));
|
|
34
|
+
const base = $props.id();
|
|
35
|
+
const prefix = $derived(id ?? base);
|
|
36
|
+
|
|
37
|
+
// Without a value, the first tab that can be chosen.
|
|
38
|
+
const index = $derived.by(() => {
|
|
39
|
+
const chosen = items.findIndex((item) => item.value === value);
|
|
40
|
+
return chosen >= 0 ? chosen : items.findIndex((item) => !item.disabled);
|
|
41
|
+
});
|
|
42
|
+
const selected = $derived(items[index]);
|
|
43
|
+
|
|
44
|
+
function select(item: TabsItem<T>) {
|
|
45
|
+
if (item.disabled || item.value === selected?.value) return;
|
|
46
|
+
value = item.value;
|
|
47
|
+
onchange?.(item.value);
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<div
|
|
52
|
+
bind:this={ref}
|
|
53
|
+
{...rest}
|
|
54
|
+
id={prefix}
|
|
55
|
+
class={['sv-tabs', sharedClasses(p, 'Tabs'), p.grow && 'sv-tabs--grow', className]}
|
|
56
|
+
>
|
|
57
|
+
<div
|
|
58
|
+
role="tablist"
|
|
59
|
+
aria-label={ariaLabel}
|
|
60
|
+
aria-labelledby={ariaLabelledby}
|
|
61
|
+
class="sv-tabs__list"
|
|
62
|
+
{@attach rovingFocus({ orientation: 'horizontal' })}
|
|
63
|
+
>
|
|
64
|
+
{#each items as item, i (item.value)}
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
role="tab"
|
|
68
|
+
id="{prefix}-tab-{i}"
|
|
69
|
+
class="sv-tabs__tab"
|
|
70
|
+
data-sv-item
|
|
71
|
+
aria-selected={i === index}
|
|
72
|
+
aria-controls={i === index && panel ? `${prefix}-panel` : undefined}
|
|
73
|
+
disabled={item.disabled}
|
|
74
|
+
onclick={() => select(item)}
|
|
75
|
+
onfocus={() => activation === 'automatic' && select(item)}
|
|
76
|
+
>
|
|
77
|
+
{item.label}
|
|
78
|
+
</button>
|
|
79
|
+
{/each}
|
|
80
|
+
</div>
|
|
81
|
+
{#if panel && selected}
|
|
82
|
+
<!-- Focusable, so keyboard users reach a panel with nothing focusable in it (APG). -->
|
|
83
|
+
<div
|
|
84
|
+
role="tabpanel"
|
|
85
|
+
id="{prefix}-panel"
|
|
86
|
+
aria-labelledby="{prefix}-tab-{index}"
|
|
87
|
+
tabindex="0"
|
|
88
|
+
class="sv-tabs__panel"
|
|
89
|
+
>
|
|
90
|
+
{@render panel(selected)}
|
|
91
|
+
</div>
|
|
92
|
+
{/if}
|
|
93
|
+
</div>
|