@makolabs/ripple 3.0.0 → 3.0.1
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/dist/elements/combobox/ComboBox.svelte +9 -12
- package/dist/elements/dropdown/select.d.ts +3 -108
- package/dist/elements/dropdown/select.js +37 -46
- package/dist/elements/popover/Popover.svelte +20 -0
- package/dist/forms/Checkbox.svelte +24 -9
- package/dist/forms/DateRange.svelte +19 -4
- package/dist/forms/Input.svelte +18 -18
- package/dist/forms/MarketSelector.svelte +1 -1
- package/dist/forms/NumberInput.svelte +12 -16
- package/dist/forms/RadioGroup.svelte +6 -2
- package/dist/forms/Tags.svelte +32 -11
- package/dist/forms/Textarea.svelte +8 -13
- package/dist/forms/Toggle.svelte +25 -17
- package/dist/forms/calendar/Calendar.svelte +105 -8
- package/dist/forms/calendar/calendar-types.d.ts +8 -0
- package/dist/forms/date-picker/DatePicker.svelte +11 -14
- package/dist/forms/form-size.d.ts +37 -0
- package/dist/forms/form-size.js +67 -0
- package/dist/forms/form-types.d.ts +5 -0
- package/dist/forms/segmented-control.js +12 -11
- package/dist/forms/slider.js +35 -28
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { cn } from '../../helper/cls.js';
|
|
3
3
|
import { buildTestId } from '../../helper/testid.js';
|
|
4
4
|
import { Size } from '../../variants.js';
|
|
5
|
+
import { formSizeTokens } from '../../forms/form-size.js';
|
|
5
6
|
import Popover from '../popover/Popover.svelte';
|
|
6
7
|
import type { ComboBoxProps, ComboBoxItem } from './combobox-types.js';
|
|
7
8
|
|
|
@@ -57,16 +58,7 @@
|
|
|
57
58
|
if (highlightedIndex >= filteredItems.length) highlightedIndex = 0;
|
|
58
59
|
});
|
|
59
60
|
|
|
60
|
-
const
|
|
61
|
-
{
|
|
62
|
-
[Size.XS]: 'h-7 text-xs',
|
|
63
|
-
[Size.SM]: 'h-8 text-sm',
|
|
64
|
-
[Size.MD]: 'h-10 text-sm',
|
|
65
|
-
[Size.LG]: 'h-12 text-base',
|
|
66
|
-
[Size.XL]: 'h-14 text-lg',
|
|
67
|
-
[Size.XXL]: 'h-16 text-lg'
|
|
68
|
-
}[size]
|
|
69
|
-
);
|
|
61
|
+
const tokens = $derived(formSizeTokens[size]);
|
|
70
62
|
|
|
71
63
|
function openMenu() {
|
|
72
64
|
if (disabled) return;
|
|
@@ -139,9 +131,14 @@
|
|
|
139
131
|
<Popover trigger="manual" bind:open panelClass="w-[var(--cbx-w,20rem)] p-0">
|
|
140
132
|
<div
|
|
141
133
|
class={cn(
|
|
142
|
-
'flex w-full items-center
|
|
134
|
+
'flex w-full items-center border bg-white transition-colors',
|
|
143
135
|
'focus-within:ring-2 focus-within:ring-offset-2 focus-within:outline-none',
|
|
144
|
-
|
|
136
|
+
tokens.height,
|
|
137
|
+
tokens.padX,
|
|
138
|
+
tokens.text,
|
|
139
|
+
tokens.gap,
|
|
140
|
+
tokens.radius,
|
|
141
|
+
tokens.shadow,
|
|
145
142
|
hasErrors
|
|
146
143
|
? 'border-danger-300 focus-within:border-danger-500 focus-within:ring-danger-500'
|
|
147
144
|
: 'border-default-300 focus-within:border-primary-500 focus-within:ring-primary-500',
|
|
@@ -1,45 +1,10 @@
|
|
|
1
1
|
export declare const selectTV: import("tailwind-variants").TVReturnType<{
|
|
2
2
|
size: {
|
|
3
|
-
|
|
3
|
+
[k: string]: {
|
|
4
4
|
trigger: string;
|
|
5
5
|
triggerIcon: string;
|
|
6
|
-
container: string;
|
|
7
|
-
item: string;
|
|
8
|
-
base: string;
|
|
9
|
-
};
|
|
10
|
-
sm: {
|
|
11
|
-
trigger: string;
|
|
12
|
-
triggerIcon: string;
|
|
13
|
-
container: string;
|
|
14
6
|
item: string;
|
|
15
|
-
base: string;
|
|
16
|
-
};
|
|
17
|
-
md: {
|
|
18
|
-
trigger: string;
|
|
19
|
-
triggerIcon: string;
|
|
20
7
|
container: string;
|
|
21
|
-
item: string;
|
|
22
|
-
base: string;
|
|
23
|
-
};
|
|
24
|
-
lg: {
|
|
25
|
-
trigger: string;
|
|
26
|
-
triggerIcon: string;
|
|
27
|
-
container: string;
|
|
28
|
-
item: string;
|
|
29
|
-
base: string;
|
|
30
|
-
};
|
|
31
|
-
xl: {
|
|
32
|
-
trigger: string;
|
|
33
|
-
triggerIcon: string;
|
|
34
|
-
container: string;
|
|
35
|
-
item: string;
|
|
36
|
-
base: string;
|
|
37
|
-
};
|
|
38
|
-
"2xl": {
|
|
39
|
-
trigger: string;
|
|
40
|
-
triggerIcon: string;
|
|
41
|
-
container: string;
|
|
42
|
-
item: string;
|
|
43
8
|
base: string;
|
|
44
9
|
};
|
|
45
10
|
};
|
|
@@ -71,46 +36,11 @@ export declare const selectTV: import("tailwind-variants").TVReturnType<{
|
|
|
71
36
|
emptyMessage: string;
|
|
72
37
|
}, undefined, {
|
|
73
38
|
size: {
|
|
74
|
-
|
|
75
|
-
trigger: string;
|
|
76
|
-
triggerIcon: string;
|
|
77
|
-
container: string;
|
|
78
|
-
item: string;
|
|
79
|
-
base: string;
|
|
80
|
-
};
|
|
81
|
-
sm: {
|
|
82
|
-
trigger: string;
|
|
83
|
-
triggerIcon: string;
|
|
84
|
-
container: string;
|
|
85
|
-
item: string;
|
|
86
|
-
base: string;
|
|
87
|
-
};
|
|
88
|
-
md: {
|
|
89
|
-
trigger: string;
|
|
90
|
-
triggerIcon: string;
|
|
91
|
-
container: string;
|
|
92
|
-
item: string;
|
|
93
|
-
base: string;
|
|
94
|
-
};
|
|
95
|
-
lg: {
|
|
39
|
+
[k: string]: {
|
|
96
40
|
trigger: string;
|
|
97
41
|
triggerIcon: string;
|
|
98
|
-
container: string;
|
|
99
42
|
item: string;
|
|
100
|
-
base: string;
|
|
101
|
-
};
|
|
102
|
-
xl: {
|
|
103
|
-
trigger: string;
|
|
104
|
-
triggerIcon: string;
|
|
105
43
|
container: string;
|
|
106
|
-
item: string;
|
|
107
|
-
base: string;
|
|
108
|
-
};
|
|
109
|
-
"2xl": {
|
|
110
|
-
trigger: string;
|
|
111
|
-
triggerIcon: string;
|
|
112
|
-
container: string;
|
|
113
|
-
item: string;
|
|
114
44
|
base: string;
|
|
115
45
|
};
|
|
116
46
|
};
|
|
@@ -142,46 +72,11 @@ export declare const selectTV: import("tailwind-variants").TVReturnType<{
|
|
|
142
72
|
emptyMessage: string;
|
|
143
73
|
}, import("tailwind-variants").TVReturnType<{
|
|
144
74
|
size: {
|
|
145
|
-
|
|
75
|
+
[k: string]: {
|
|
146
76
|
trigger: string;
|
|
147
77
|
triggerIcon: string;
|
|
148
|
-
container: string;
|
|
149
78
|
item: string;
|
|
150
|
-
base: string;
|
|
151
|
-
};
|
|
152
|
-
sm: {
|
|
153
|
-
trigger: string;
|
|
154
|
-
triggerIcon: string;
|
|
155
79
|
container: string;
|
|
156
|
-
item: string;
|
|
157
|
-
base: string;
|
|
158
|
-
};
|
|
159
|
-
md: {
|
|
160
|
-
trigger: string;
|
|
161
|
-
triggerIcon: string;
|
|
162
|
-
container: string;
|
|
163
|
-
item: string;
|
|
164
|
-
base: string;
|
|
165
|
-
};
|
|
166
|
-
lg: {
|
|
167
|
-
trigger: string;
|
|
168
|
-
triggerIcon: string;
|
|
169
|
-
container: string;
|
|
170
|
-
item: string;
|
|
171
|
-
base: string;
|
|
172
|
-
};
|
|
173
|
-
xl: {
|
|
174
|
-
trigger: string;
|
|
175
|
-
triggerIcon: string;
|
|
176
|
-
container: string;
|
|
177
|
-
item: string;
|
|
178
|
-
base: string;
|
|
179
|
-
};
|
|
180
|
-
"2xl": {
|
|
181
|
-
trigger: string;
|
|
182
|
-
triggerIcon: string;
|
|
183
|
-
container: string;
|
|
184
|
-
item: string;
|
|
185
80
|
base: string;
|
|
186
81
|
};
|
|
187
82
|
};
|
|
@@ -1,15 +1,49 @@
|
|
|
1
1
|
import { tv } from 'tailwind-variants';
|
|
2
2
|
import { Size } from '../../variants.js';
|
|
3
|
+
import { formSizeTokens } from '../../forms/form-size.js';
|
|
4
|
+
// Build the per-size slot overrides from the canonical form-size tokens
|
|
5
|
+
// so Select coordinates visually with Input/Textarea/NumberInput at the
|
|
6
|
+
// same `size` prop. Container max-heights and base min-widths stay here
|
|
7
|
+
// because they're Select-specific (not tied to field sizing).
|
|
8
|
+
const containerHeights = {
|
|
9
|
+
[Size.XS]: 'max-h-32',
|
|
10
|
+
[Size.SM]: 'max-h-40',
|
|
11
|
+
[Size.MD]: 'max-h-56',
|
|
12
|
+
[Size.LG]: 'max-h-64',
|
|
13
|
+
[Size.XL]: 'max-h-72',
|
|
14
|
+
[Size.XXL]: 'max-h-80'
|
|
15
|
+
};
|
|
16
|
+
const minWidths = {
|
|
17
|
+
[Size.XS]: 'min-w-20',
|
|
18
|
+
[Size.SM]: 'min-w-28',
|
|
19
|
+
[Size.MD]: 'min-w-32',
|
|
20
|
+
[Size.LG]: 'min-w-40',
|
|
21
|
+
[Size.XL]: 'min-w-48',
|
|
22
|
+
[Size.XXL]: 'min-w-56'
|
|
23
|
+
};
|
|
24
|
+
const sizeVariants = Object.fromEntries(Object.keys(formSizeTokens).map((key) => {
|
|
25
|
+
const t = formSizeTokens[key];
|
|
26
|
+
return [
|
|
27
|
+
key,
|
|
28
|
+
{
|
|
29
|
+
trigger: `${t.height} ${t.padX} ${t.padY} ${t.text} ${t.gap} ${t.radius} ${t.shadow}`,
|
|
30
|
+
triggerIcon: t.iconSize,
|
|
31
|
+
item: `${t.padX} ${t.padY} ${t.text}`,
|
|
32
|
+
container: containerHeights[key],
|
|
33
|
+
base: minWidths[key]
|
|
34
|
+
}
|
|
35
|
+
];
|
|
36
|
+
}));
|
|
3
37
|
export const selectTV = tv({
|
|
4
38
|
slots: {
|
|
5
39
|
base: 'w-full',
|
|
6
40
|
trigger: `relative flex items-center justify-between w-full text-left bg-white border
|
|
7
|
-
border-default-300
|
|
41
|
+
border-default-300 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:border-primary-500 focus-within:ring-primary-500 cursor-pointer transition-colors hover:border-default-400`,
|
|
8
42
|
triggerIcon: 'transition-transform duration-200 text-default-500',
|
|
9
43
|
container: 'absolute z-50 w-full mt-1 bg-white overflow-clip border border-default-200 rounded-md shadow-sm origin-top-left top-full left-0 mt-2',
|
|
10
44
|
searchInput: 'flex items-center gap-x-3 w-full outline-none px-2 h-10 border-b border-b-default-200',
|
|
11
45
|
list: 'py-1 max-h-60 overflow-x-clip overflow-y-auto h-full',
|
|
12
|
-
item: `w-full
|
|
46
|
+
item: `w-full text-left
|
|
13
47
|
data-[highlighted=true]:bg-default-100 data-[highlighted=true]:text-default-700
|
|
14
48
|
data-[selected=true]:bg-info-100 data-[selected=true]:text-info-500 data-[selected=true]:font-medium
|
|
15
49
|
data-[selected=true]:data-[highlighted=true]:bg-info-200 data-[selected=true]:data-[highlighted=true]:text-info-500
|
|
@@ -17,50 +51,7 @@ export const selectTV = tv({
|
|
|
17
51
|
emptyMessage: 'px-3 py-2 text-sm text-default-500'
|
|
18
52
|
},
|
|
19
53
|
variants: {
|
|
20
|
-
size:
|
|
21
|
-
[Size.XS]: {
|
|
22
|
-
trigger: 'h-6 px-2 py-1 text-xs gap-1',
|
|
23
|
-
triggerIcon: 'h-3 w-3',
|
|
24
|
-
container: 'max-h-40',
|
|
25
|
-
item: 'px-2 py-1 text-xs',
|
|
26
|
-
base: 'min-w-24'
|
|
27
|
-
},
|
|
28
|
-
[Size.SM]: {
|
|
29
|
-
trigger: 'h-8 px-3 py-2 text-sm gap-1.5',
|
|
30
|
-
triggerIcon: 'h-3.5 w-3.5',
|
|
31
|
-
container: 'max-h-48',
|
|
32
|
-
item: 'px-2.5 py-1.5 text-xs',
|
|
33
|
-
base: 'min-w-32'
|
|
34
|
-
},
|
|
35
|
-
[Size.MD]: {
|
|
36
|
-
trigger: 'h-10 px-3 py-2 text-base gap-2',
|
|
37
|
-
triggerIcon: 'h-4 w-4',
|
|
38
|
-
container: 'max-h-60',
|
|
39
|
-
item: 'px-3 py-2 text-sm',
|
|
40
|
-
base: 'min-w-40'
|
|
41
|
-
},
|
|
42
|
-
[Size.LG]: {
|
|
43
|
-
trigger: 'h-12 px-3 py-2 text-lg gap-2.5',
|
|
44
|
-
triggerIcon: 'h-5 w-5',
|
|
45
|
-
container: 'max-h-72',
|
|
46
|
-
item: 'px-4 py-2.5 text-base',
|
|
47
|
-
base: 'min-w-48'
|
|
48
|
-
},
|
|
49
|
-
[Size.XL]: {
|
|
50
|
-
trigger: 'h-12 px-5 py-3 text-lg gap-3',
|
|
51
|
-
triggerIcon: 'h-6 w-6',
|
|
52
|
-
container: 'max-h-80',
|
|
53
|
-
item: 'px-5 py-3 text-lg',
|
|
54
|
-
base: 'min-w-56'
|
|
55
|
-
},
|
|
56
|
-
[Size.XXL]: {
|
|
57
|
-
trigger: 'h-14 px-6 py-3.5 text-xl gap-4',
|
|
58
|
-
triggerIcon: 'h-7 w-7',
|
|
59
|
-
container: 'max-h-96',
|
|
60
|
-
item: 'px-6 py-3.5 text-xl',
|
|
61
|
-
base: 'min-w-64'
|
|
62
|
-
}
|
|
63
|
-
},
|
|
54
|
+
size: sizeVariants,
|
|
64
55
|
disabled: {
|
|
65
56
|
true: {
|
|
66
57
|
trigger: 'opacity-50 cursor-not-allowed hover:border-default-300',
|
|
@@ -162,6 +162,26 @@
|
|
|
162
162
|
};
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Portal the panel element to `document.body` once it mounts, so any
|
|
167
|
+
* ancestor with `transform` / `filter` / `will-change` (Storybook's
|
|
168
|
+
* docs container, a scaled preview, a card with transform-based
|
|
169
|
+
* hover, etc.) doesn't re-parent our `position: fixed` panel and
|
|
170
|
+
* clip / mis-position it. On unmount, we simply remove the node —
|
|
171
|
+
* Svelte's own `{#if}` teardown will still work because it tracks
|
|
172
|
+
* the element by reference, not by DOM path.
|
|
173
|
+
*/
|
|
174
|
+
$effect(() => {
|
|
175
|
+
if (!panelEl) return;
|
|
176
|
+
const el = panelEl;
|
|
177
|
+
document.body.appendChild(el);
|
|
178
|
+
return () => {
|
|
179
|
+
if (el.parentNode === document.body) {
|
|
180
|
+
document.body.removeChild(el);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
|
|
165
185
|
const arrowClass = $derived(
|
|
166
186
|
{
|
|
167
187
|
top: 'top-full left-1/2 -translate-x-1/2 border-t-white border-l-transparent border-r-transparent border-b-transparent',
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { cn } from '../helper/cls.js';
|
|
3
3
|
import { buildTestId } from '../helper/testid.js';
|
|
4
|
-
import
|
|
4
|
+
import { Size } from '../variants.js';
|
|
5
|
+
import { formSizeTokens } from './form-size.js';
|
|
6
|
+
import type { CheckboxProps, VariantSizes } from '../index.js';
|
|
5
7
|
|
|
6
8
|
let {
|
|
7
9
|
name,
|
|
@@ -10,21 +12,34 @@
|
|
|
10
12
|
disabled = false,
|
|
11
13
|
errors = [],
|
|
12
14
|
required = false,
|
|
15
|
+
size = Size.MD,
|
|
13
16
|
testId
|
|
14
17
|
}: CheckboxProps = $props();
|
|
15
18
|
|
|
19
|
+
// Checkbox box dimension uses Tailwind's `size-*` shorthand, scaling
|
|
20
|
+
// from size-3 (XS) up to size-7 (XXL) so it stays visually proportional
|
|
21
|
+
// to adjacent form controls at the same size.
|
|
22
|
+
const boxSize: Record<VariantSizes, string> = {
|
|
23
|
+
[Size.XS]: 'size-3',
|
|
24
|
+
[Size.SM]: 'size-3.5',
|
|
25
|
+
[Size.MD]: 'size-4',
|
|
26
|
+
[Size.LG]: 'size-5',
|
|
27
|
+
[Size.XL]: 'size-6',
|
|
28
|
+
[Size.XXL]: 'size-7'
|
|
29
|
+
};
|
|
30
|
+
const tokens = $derived(formSizeTokens[size]);
|
|
31
|
+
|
|
16
32
|
const checkboxClass = $derived(
|
|
17
|
-
cn(
|
|
18
|
-
'
|
|
19
|
-
|
|
20
|
-
|
|
33
|
+
cn(
|
|
34
|
+
'rounded text-primary-600 border-default-300 focus:ring-primary-500',
|
|
35
|
+
boxSize[size],
|
|
36
|
+
disabled && 'opacity-50 cursor-not-allowed',
|
|
37
|
+
errors.length && 'accent-danger-500'
|
|
38
|
+
)
|
|
21
39
|
);
|
|
22
40
|
|
|
23
41
|
const labelClass = $derived(
|
|
24
|
-
cn('
|
|
25
|
-
'text-default-700': !errors.length,
|
|
26
|
-
'text-danger-600': errors.length
|
|
27
|
-
})
|
|
42
|
+
cn('font-medium', tokens.text, errors.length ? 'text-danger-600' : 'text-default-700')
|
|
28
43
|
);
|
|
29
44
|
</script>
|
|
30
45
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { cn } from '../helper/cls.js';
|
|
3
|
+
import { Size } from '../variants.js';
|
|
4
|
+
import { formSizeTokens } from './form-size.js';
|
|
3
5
|
import type { DateRangeProps } from '../index.js';
|
|
4
6
|
import Portal from '../utils/Portal.svelte';
|
|
5
7
|
import { fly } from 'svelte/transition';
|
|
@@ -17,11 +19,14 @@
|
|
|
17
19
|
endLabel = 'End date',
|
|
18
20
|
format = 'MM/dd/yyyy',
|
|
19
21
|
errors = [],
|
|
22
|
+
size = Size.MD,
|
|
20
23
|
id,
|
|
21
24
|
name,
|
|
22
25
|
onselect
|
|
23
26
|
}: DateRangeProps = $props();
|
|
24
27
|
|
|
28
|
+
const tokens = $derived(formSizeTokens[size]);
|
|
29
|
+
|
|
25
30
|
let isOpen = $state(false);
|
|
26
31
|
let hoveredDate = $state<Date | null>(null);
|
|
27
32
|
let datePickerRef = $state<HTMLDivElement | null>(null);
|
|
@@ -239,7 +244,12 @@
|
|
|
239
244
|
{id}
|
|
240
245
|
type="button"
|
|
241
246
|
class={cn(
|
|
242
|
-
'border-default-300 flex w-full items-center justify-between
|
|
247
|
+
'border-default-300 flex w-full items-center justify-between border bg-white',
|
|
248
|
+
tokens.height,
|
|
249
|
+
tokens.padX,
|
|
250
|
+
tokens.text,
|
|
251
|
+
tokens.radius,
|
|
252
|
+
tokens.shadow,
|
|
243
253
|
disabled
|
|
244
254
|
? 'bg-default-100 text-default-400 cursor-not-allowed opacity-50'
|
|
245
255
|
: errors?.length
|
|
@@ -259,7 +269,7 @@
|
|
|
259
269
|
? `${formatDate(startDate)} - Select end date`
|
|
260
270
|
: placeholder}
|
|
261
271
|
</span>
|
|
262
|
-
<svg class=
|
|
272
|
+
<svg class={cn('text-default-400', tokens.iconSize)} viewBox="0 0 20 20" fill="currentColor">
|
|
263
273
|
<path
|
|
264
274
|
fill-rule="evenodd"
|
|
265
275
|
d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z"
|
|
@@ -271,11 +281,16 @@
|
|
|
271
281
|
{#if startDate || endDate}
|
|
272
282
|
<button
|
|
273
283
|
type="button"
|
|
274
|
-
class=
|
|
284
|
+
class={cn(
|
|
285
|
+
'text-default-400 hover:text-default-500 absolute top-1/2 -translate-y-1/2',
|
|
286
|
+
// Sit just left of the calendar icon; use the token gap
|
|
287
|
+
// so the clear button stays visually tied to the icon.
|
|
288
|
+
'right-8'
|
|
289
|
+
)}
|
|
275
290
|
onclick={clearDates}
|
|
276
291
|
aria-label="Clear dates"
|
|
277
292
|
>
|
|
278
|
-
<svg class=
|
|
293
|
+
<svg class={cn(tokens.iconSize)} viewBox="0 0 20 20" fill="currentColor">
|
|
279
294
|
<path
|
|
280
295
|
fill-rule="evenodd"
|
|
281
296
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
|
package/dist/forms/Input.svelte
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { cn } from '../helper/cls.js';
|
|
3
3
|
import { buildTestId } from '../helper/testid.js';
|
|
4
4
|
import { Size } from '../variants.js';
|
|
5
|
+
import { formSizeTokens } from './form-size.js';
|
|
5
6
|
import type { InputProps } from '../index.js';
|
|
6
7
|
|
|
7
8
|
let {
|
|
@@ -20,26 +21,25 @@
|
|
|
20
21
|
}: InputProps = $props();
|
|
21
22
|
|
|
22
23
|
const BASIC_TYPES = ['text', 'email', 'password', 'number', 'tel', 'url', 'date', 'textarea'];
|
|
24
|
+
const tokens = $derived(formSizeTokens[size]);
|
|
23
25
|
const inputClasses = $derived(
|
|
24
26
|
cn(
|
|
25
|
-
'transition-colors placeholder:text-default-400',
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
'border-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
!errors.length
|
|
42
|
-
},
|
|
27
|
+
'w-full bg-white transition-colors placeholder:text-default-400',
|
|
28
|
+
tokens.padX,
|
|
29
|
+
tokens.text,
|
|
30
|
+
// All basic types (including textarea) get the bordered look.
|
|
31
|
+
BASIC_TYPES.includes(type) && [
|
|
32
|
+
'border focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2',
|
|
33
|
+
tokens.radius,
|
|
34
|
+
tokens.shadow
|
|
35
|
+
],
|
|
36
|
+
// Single-line types get a fixed height; textarea is content-driven.
|
|
37
|
+
type !== 'textarea' && tokens.height,
|
|
38
|
+
type === 'textarea' && ['resize-y min-h-[100px]', tokens.padY],
|
|
39
|
+
errors.length
|
|
40
|
+
? 'border-danger-300 focus-within:border-danger-500 focus-within:ring-danger-500'
|
|
41
|
+
: 'border-default-300 focus-within:border-primary-500 focus-within:ring-primary-500',
|
|
42
|
+
disabled && 'opacity-50 cursor-not-allowed',
|
|
43
43
|
className
|
|
44
44
|
)
|
|
45
45
|
);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { cn } from '../helper/cls.js';
|
|
3
3
|
import { buildTestId } from '../helper/testid.js';
|
|
4
4
|
import { Size } from '../variants.js';
|
|
5
|
+
import { formSizeTokens } from './form-size.js';
|
|
5
6
|
import type { NumberInputProps } from '../index.js';
|
|
6
7
|
|
|
7
8
|
let {
|
|
@@ -26,18 +27,16 @@
|
|
|
26
27
|
|
|
27
28
|
const selectedOption = $derived(units.find((u) => u.value === unit));
|
|
28
29
|
|
|
30
|
+
const tokens = $derived(formSizeTokens[size]);
|
|
31
|
+
|
|
29
32
|
const containerClass = $derived(
|
|
30
33
|
cn(
|
|
31
|
-
'relative flex items-center gap-1
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
'h-8': size === Size.SM,
|
|
38
|
-
'h-10': size === Size.MD,
|
|
39
|
-
'h-12': size === Size.LG
|
|
40
|
-
},
|
|
34
|
+
'relative flex items-center gap-1 border bg-white',
|
|
35
|
+
tokens.radius,
|
|
36
|
+
tokens.shadow,
|
|
37
|
+
tokens.height,
|
|
38
|
+
errors?.length && 'border-danger-300',
|
|
39
|
+
disabled && 'cursor-not-allowed opacity-50',
|
|
41
40
|
'border-default-300 focus-within:border-primary-500 focus-within:ring-2 focus-within:ring-primary-500 focus-within:ring-offset-2',
|
|
42
41
|
className
|
|
43
42
|
)
|
|
@@ -45,12 +44,9 @@
|
|
|
45
44
|
|
|
46
45
|
const inputClass = $derived(
|
|
47
46
|
cn(
|
|
48
|
-
'w-full bg-transparent outline-none disabled:cursor-not-allowed
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
'text-base': size === Size.MD,
|
|
52
|
-
'text-lg': size === Size.LG
|
|
53
|
-
}
|
|
47
|
+
'w-full bg-transparent outline-none disabled:cursor-not-allowed placeholder:text-default-400',
|
|
48
|
+
tokens.padX,
|
|
49
|
+
tokens.text
|
|
54
50
|
)
|
|
55
51
|
);
|
|
56
52
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { cn } from '../helper/cls.js';
|
|
3
3
|
import { buildTestId } from '../helper/testid.js';
|
|
4
4
|
import { Color, Size } from '../variants.js';
|
|
5
|
+
import { formSizeTokens } from './form-size.js';
|
|
5
6
|
import type { RadioGroupProps } from '../index.js';
|
|
6
7
|
|
|
7
8
|
let {
|
|
@@ -21,7 +22,10 @@
|
|
|
21
22
|
}: RadioGroupProps = $props();
|
|
22
23
|
|
|
23
24
|
const hasErrors = $derived(errors.length > 0);
|
|
25
|
+
const tokens = $derived(formSizeTokens[size]);
|
|
24
26
|
|
|
27
|
+
// Radio circle scales with the form ladder — `size-3` at xs through
|
|
28
|
+
// `size-6` at xl. 2xl aliases xl.
|
|
25
29
|
const dotSize = $derived(
|
|
26
30
|
{
|
|
27
31
|
[Size.XS]: 'size-3',
|
|
@@ -29,7 +33,7 @@
|
|
|
29
33
|
[Size.MD]: 'size-4',
|
|
30
34
|
[Size.LG]: 'size-5',
|
|
31
35
|
[Size.XL]: 'size-6',
|
|
32
|
-
[Size.XXL]: 'size-
|
|
36
|
+
[Size.XXL]: 'size-6'
|
|
33
37
|
}[size]
|
|
34
38
|
);
|
|
35
39
|
|
|
@@ -104,7 +108,7 @@
|
|
|
104
108
|
{/if}
|
|
105
109
|
</span>
|
|
106
110
|
<span class="flex flex-col">
|
|
107
|
-
<span class=
|
|
111
|
+
<span class={cn('text-default-800', tokens.text)}>{option.label}</span>
|
|
108
112
|
{#if option.description}
|
|
109
113
|
<span class="text-default-500 text-xs">{option.description}</span>
|
|
110
114
|
{/if}
|
package/dist/forms/Tags.svelte
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import Badge from '../elements/badge/Badge.svelte';
|
|
3
3
|
import { Size } from '../variants.js';
|
|
4
4
|
import { cn } from '../helper/cls.js';
|
|
5
|
+
import { formSizeTokens } from './form-size.js';
|
|
5
6
|
import { fade } from 'svelte/transition';
|
|
6
7
|
import { flip } from 'svelte/animate';
|
|
7
8
|
import { quintOut } from 'svelte/easing';
|
|
@@ -34,6 +35,25 @@
|
|
|
34
35
|
showSuggestions ? suggestions.filter(isUnselected).filter(matchesInput).slice(0, 5) : []
|
|
35
36
|
);
|
|
36
37
|
|
|
38
|
+
const tokens = $derived(formSizeTokens[size]);
|
|
39
|
+
|
|
40
|
+
// Chip size shifted one tier down from the Tags container so chip
|
|
41
|
+
// text matches the container text (e.g. Tags `md` uses `text-xs`, so
|
|
42
|
+
// Badge `sm` — which is also `text-xs` — lines up, instead of Badge
|
|
43
|
+
// `md` which jumps to `text-sm`).
|
|
44
|
+
const chipSize = $derived(
|
|
45
|
+
(
|
|
46
|
+
{
|
|
47
|
+
[Size.XS]: Size.XS,
|
|
48
|
+
[Size.SM]: Size.XS,
|
|
49
|
+
[Size.MD]: Size.SM,
|
|
50
|
+
[Size.LG]: Size.MD,
|
|
51
|
+
[Size.XL]: Size.LG,
|
|
52
|
+
[Size.XXL]: Size.LG
|
|
53
|
+
} as const
|
|
54
|
+
)[size]
|
|
55
|
+
);
|
|
56
|
+
|
|
37
57
|
function handleKeydown(event: KeyboardEvent) {
|
|
38
58
|
if (event.key === 'Enter') {
|
|
39
59
|
event.preventDefault();
|
|
@@ -108,12 +128,14 @@
|
|
|
108
128
|
|
|
109
129
|
const containerClass = $derived(
|
|
110
130
|
cn(
|
|
111
|
-
'relative flex flex-wrap gap-2
|
|
131
|
+
'relative flex flex-wrap gap-2 border bg-white',
|
|
132
|
+
tokens.radius,
|
|
133
|
+
tokens.shadow,
|
|
134
|
+
tokens.padX,
|
|
135
|
+
tokens.padY,
|
|
112
136
|
'border-default-300 focus-within:border-primary-500 focus-within:ring-2 focus-within:ring-primary-500 focus-within:ring-offset-2',
|
|
113
|
-
|
|
114
|
-
'border-danger-300 focus-within:border-danger-500 focus-within:ring-danger-500'
|
|
115
|
-
errors?.length
|
|
116
|
-
},
|
|
137
|
+
errors?.length &&
|
|
138
|
+
'border-danger-300 focus-within:border-danger-500 focus-within:ring-danger-500',
|
|
117
139
|
className
|
|
118
140
|
)
|
|
119
141
|
);
|
|
@@ -143,7 +165,7 @@
|
|
|
143
165
|
transition:fade={{ duration: 250, easing: quintOut }}
|
|
144
166
|
animate:flip={{ duration: 300, easing: quintOut }}
|
|
145
167
|
>
|
|
146
|
-
<Badge {
|
|
168
|
+
<Badge size={chipSize} color="info" onclose={() => handleTagRemoval(tag)} class="shadow-xs">
|
|
147
169
|
{tag}
|
|
148
170
|
</Badge>
|
|
149
171
|
</div>
|
|
@@ -154,11 +176,10 @@
|
|
|
154
176
|
{name}
|
|
155
177
|
id={name}
|
|
156
178
|
{placeholder}
|
|
157
|
-
class={cn(
|
|
158
|
-
'text-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
})}
|
|
179
|
+
class={cn(
|
|
180
|
+
'placeholder:text-default-400 min-w-[120px] flex-1 bg-transparent outline-none',
|
|
181
|
+
tokens.text
|
|
182
|
+
)}
|
|
162
183
|
type="text"
|
|
163
184
|
autocomplete="off"
|
|
164
185
|
onkeydown={handleKeydown}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { cn } from '../helper/cls.js';
|
|
3
3
|
import { buildTestId } from '../helper/testid.js';
|
|
4
4
|
import { Size } from '../variants.js';
|
|
5
|
+
import { formSizeTokens } from './form-size.js';
|
|
5
6
|
import type { TextareaProps } from '../index.js';
|
|
6
7
|
|
|
7
8
|
let {
|
|
@@ -27,25 +28,19 @@
|
|
|
27
28
|
|
|
28
29
|
let el = $state<HTMLTextAreaElement | undefined>();
|
|
29
30
|
|
|
30
|
-
const
|
|
31
|
-
{
|
|
32
|
-
[Size.XS]: 'text-xs',
|
|
33
|
-
[Size.SM]: 'text-sm',
|
|
34
|
-
[Size.MD]: 'text-sm',
|
|
35
|
-
[Size.LG]: 'text-base',
|
|
36
|
-
[Size.XL]: 'text-lg',
|
|
37
|
-
[Size.XXL]: 'text-lg'
|
|
38
|
-
}[size]
|
|
39
|
-
);
|
|
40
|
-
|
|
31
|
+
const tokens = $derived(formSizeTokens[size]);
|
|
41
32
|
const hasErrors = $derived(errors.length > 0);
|
|
42
33
|
|
|
43
34
|
const textareaClasses = $derived(
|
|
44
35
|
cn(
|
|
45
|
-
'w-full
|
|
36
|
+
'w-full border bg-white transition-colors',
|
|
46
37
|
'placeholder:text-default-400',
|
|
47
38
|
'focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2',
|
|
48
|
-
|
|
39
|
+
tokens.radius,
|
|
40
|
+
tokens.shadow,
|
|
41
|
+
tokens.padX,
|
|
42
|
+
tokens.padY,
|
|
43
|
+
tokens.text,
|
|
49
44
|
hasErrors
|
|
50
45
|
? 'border-danger-300 focus-within:border-danger-500 focus-within:ring-danger-500'
|
|
51
46
|
: 'border-default-300 focus-within:border-primary-500 focus-within:ring-primary-500',
|
package/dist/forms/Toggle.svelte
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { cn } from '../helper/cls.js';
|
|
3
3
|
import { Color, Size } from '../variants.js';
|
|
4
4
|
import { buildTestId } from '../helper/testid.js';
|
|
5
|
+
import { formSizeTokens } from './form-size.js';
|
|
5
6
|
import type { ToggleProps, VariantColors, VariantSizes } from '../index.js';
|
|
6
7
|
|
|
7
8
|
let {
|
|
@@ -35,13 +36,19 @@
|
|
|
35
36
|
)[color]
|
|
36
37
|
);
|
|
37
38
|
|
|
39
|
+
const tokens = $derived(formSizeTokens[size]);
|
|
40
|
+
|
|
41
|
+
// Track + thumb + on-state offset ladder. Chosen to roughly match the
|
|
42
|
+
// height of an Input at the same `size` (20/24/28/36/44px) so a row
|
|
43
|
+
// with a Toggle and an Input reads evenly. 2xl aliases xl — form
|
|
44
|
+
// controls cap at xl.
|
|
38
45
|
const toggleSize = $derived(
|
|
39
46
|
(
|
|
40
47
|
{
|
|
41
|
-
[Size.XS]: 'w-
|
|
42
|
-
[Size.SM]: 'w-
|
|
43
|
-
[Size.MD]: 'w-
|
|
44
|
-
[Size.LG]: 'w-
|
|
48
|
+
[Size.XS]: 'w-6 h-3',
|
|
49
|
+
[Size.SM]: 'w-7 h-3.5',
|
|
50
|
+
[Size.MD]: 'w-8 h-4',
|
|
51
|
+
[Size.LG]: 'w-10 h-5',
|
|
45
52
|
[Size.XL]: 'w-12 h-6',
|
|
46
53
|
[Size.XXL]: 'w-12 h-6'
|
|
47
54
|
} satisfies Record<VariantSizes, string>
|
|
@@ -51,10 +58,10 @@
|
|
|
51
58
|
const thumbSize = $derived(
|
|
52
59
|
(
|
|
53
60
|
{
|
|
54
|
-
[Size.XS]: 'h-
|
|
55
|
-
[Size.SM]: 'h-
|
|
56
|
-
[Size.MD]: 'h-
|
|
57
|
-
[Size.LG]: 'h-
|
|
61
|
+
[Size.XS]: 'h-2 w-2',
|
|
62
|
+
[Size.SM]: 'h-2.5 w-2.5',
|
|
63
|
+
[Size.MD]: 'h-3 w-3',
|
|
64
|
+
[Size.LG]: 'h-4 w-4',
|
|
58
65
|
[Size.XL]: 'h-5 w-5',
|
|
59
66
|
[Size.XXL]: 'h-5 w-5'
|
|
60
67
|
} satisfies Record<VariantSizes, string>
|
|
@@ -64,10 +71,10 @@
|
|
|
64
71
|
const thumbPosition = $derived(
|
|
65
72
|
(
|
|
66
73
|
{
|
|
67
|
-
[Size.XS]: value ? 'translate-x-
|
|
68
|
-
[Size.SM]: value ? 'translate-x-
|
|
69
|
-
[Size.MD]: value ? 'translate-x-
|
|
70
|
-
[Size.LG]: value ? 'translate-x-
|
|
74
|
+
[Size.XS]: value ? 'translate-x-3' : 'translate-x-0.5',
|
|
75
|
+
[Size.SM]: value ? 'translate-x-3.5' : 'translate-x-0.5',
|
|
76
|
+
[Size.MD]: value ? 'translate-x-4' : 'translate-x-0.5',
|
|
77
|
+
[Size.LG]: value ? 'translate-x-5' : 'translate-x-0.5',
|
|
71
78
|
[Size.XL]: value ? 'translate-x-6' : 'translate-x-0.5',
|
|
72
79
|
[Size.XXL]: value ? 'translate-x-6' : 'translate-x-0.5'
|
|
73
80
|
} satisfies Record<VariantSizes, string>
|
|
@@ -112,11 +119,12 @@
|
|
|
112
119
|
);
|
|
113
120
|
|
|
114
121
|
const labelClasses = $derived(
|
|
115
|
-
cn(
|
|
116
|
-
'
|
|
117
|
-
|
|
118
|
-
'
|
|
119
|
-
|
|
122
|
+
cn(
|
|
123
|
+
'font-medium',
|
|
124
|
+
tokens.text,
|
|
125
|
+
errors.length ? 'text-danger-600' : 'text-default-700',
|
|
126
|
+
disabled && 'opacity-50'
|
|
127
|
+
)
|
|
120
128
|
);
|
|
121
129
|
|
|
122
130
|
function handleKeyDown(event: KeyboardEvent) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { cn } from '../../helper/cls.js';
|
|
3
3
|
import { buildTestId } from '../../helper/testid.js';
|
|
4
|
+
import { Size } from '../../variants.js';
|
|
5
|
+
import type { VariantSizes } from '../../index.js';
|
|
4
6
|
import type { CalendarProps } from './calendar-types.js';
|
|
5
7
|
|
|
6
8
|
let {
|
|
@@ -14,11 +16,91 @@
|
|
|
14
16
|
weekStartsOn = 1,
|
|
15
17
|
hideHeader = false,
|
|
16
18
|
disabled = false,
|
|
19
|
+
size = Size.MD,
|
|
17
20
|
class: className = '',
|
|
18
21
|
onselect,
|
|
19
22
|
testId
|
|
20
23
|
}: CalendarProps = $props();
|
|
21
24
|
|
|
25
|
+
// Calendar dimensions don't map cleanly onto form-size tokens — a
|
|
26
|
+
// date grid's "size" means cell + panel width, not input height. So
|
|
27
|
+
// we maintain a dedicated ladder here, tuned so a Calendar at the
|
|
28
|
+
// same `size` as its enclosing DatePicker feels proportional.
|
|
29
|
+
type CalendarDensity = {
|
|
30
|
+
panel: string;
|
|
31
|
+
padding: string;
|
|
32
|
+
cell: string;
|
|
33
|
+
navBtn: string;
|
|
34
|
+
navIcon: string;
|
|
35
|
+
monthText: string;
|
|
36
|
+
dayHeaderText: string;
|
|
37
|
+
cellText: string;
|
|
38
|
+
};
|
|
39
|
+
const calendarSize: Record<VariantSizes, CalendarDensity> = {
|
|
40
|
+
[Size.XS]: {
|
|
41
|
+
panel: 'w-48',
|
|
42
|
+
padding: 'p-2',
|
|
43
|
+
cell: 'size-6',
|
|
44
|
+
navBtn: 'size-5',
|
|
45
|
+
navIcon: 'size-3',
|
|
46
|
+
monthText: 'text-xs',
|
|
47
|
+
dayHeaderText: 'text-[9px]',
|
|
48
|
+
cellText: 'text-[10px]'
|
|
49
|
+
},
|
|
50
|
+
[Size.SM]: {
|
|
51
|
+
panel: 'w-56',
|
|
52
|
+
padding: 'p-2.5',
|
|
53
|
+
cell: 'size-7',
|
|
54
|
+
navBtn: 'size-6',
|
|
55
|
+
navIcon: 'size-3.5',
|
|
56
|
+
monthText: 'text-xs',
|
|
57
|
+
dayHeaderText: 'text-[10px]',
|
|
58
|
+
cellText: 'text-xs'
|
|
59
|
+
},
|
|
60
|
+
[Size.MD]: {
|
|
61
|
+
panel: 'w-64',
|
|
62
|
+
padding: 'p-3',
|
|
63
|
+
cell: 'size-8',
|
|
64
|
+
navBtn: 'size-7',
|
|
65
|
+
navIcon: 'size-4',
|
|
66
|
+
monthText: 'text-sm',
|
|
67
|
+
dayHeaderText: 'text-[10px]',
|
|
68
|
+
cellText: 'text-xs'
|
|
69
|
+
},
|
|
70
|
+
[Size.LG]: {
|
|
71
|
+
panel: 'w-72',
|
|
72
|
+
padding: 'p-3.5',
|
|
73
|
+
cell: 'size-9',
|
|
74
|
+
navBtn: 'size-8',
|
|
75
|
+
navIcon: 'size-4',
|
|
76
|
+
monthText: 'text-base',
|
|
77
|
+
dayHeaderText: 'text-xs',
|
|
78
|
+
cellText: 'text-sm'
|
|
79
|
+
},
|
|
80
|
+
[Size.XL]: {
|
|
81
|
+
panel: 'w-80',
|
|
82
|
+
padding: 'p-4',
|
|
83
|
+
cell: 'size-10',
|
|
84
|
+
navBtn: 'size-9',
|
|
85
|
+
navIcon: 'size-5',
|
|
86
|
+
monthText: 'text-lg',
|
|
87
|
+
dayHeaderText: 'text-xs',
|
|
88
|
+
cellText: 'text-sm'
|
|
89
|
+
},
|
|
90
|
+
// Form controls cap at xl — see `form-size.ts`.
|
|
91
|
+
[Size.XXL]: {
|
|
92
|
+
panel: 'w-80',
|
|
93
|
+
padding: 'p-4',
|
|
94
|
+
cell: 'size-10',
|
|
95
|
+
navBtn: 'size-9',
|
|
96
|
+
navIcon: 'size-5',
|
|
97
|
+
monthText: 'text-lg',
|
|
98
|
+
dayHeaderText: 'text-xs',
|
|
99
|
+
cellText: 'text-sm'
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const density = $derived(calendarSize[size]);
|
|
103
|
+
|
|
22
104
|
const anchor = $derived(initialMonth ?? value ?? valueStart ?? new Date());
|
|
23
105
|
|
|
24
106
|
let viewYear = $state(anchor.getFullYear());
|
|
@@ -146,7 +228,9 @@
|
|
|
146
228
|
|
|
147
229
|
<div
|
|
148
230
|
class={cn(
|
|
149
|
-
'border-default-200 inline-block
|
|
231
|
+
'border-default-200 inline-block rounded-lg border bg-white shadow-xs select-none',
|
|
232
|
+
density.panel,
|
|
233
|
+
density.padding,
|
|
150
234
|
className
|
|
151
235
|
)}
|
|
152
236
|
data-testid={buildTestId('calendar', undefined, testId)}
|
|
@@ -156,11 +240,14 @@
|
|
|
156
240
|
<button
|
|
157
241
|
type="button"
|
|
158
242
|
onclick={prevMonth}
|
|
159
|
-
class=
|
|
243
|
+
class={cn(
|
|
244
|
+
'text-default-500 hover:bg-default-100 hover:text-default-800 flex cursor-pointer items-center justify-center rounded',
|
|
245
|
+
density.navBtn
|
|
246
|
+
)}
|
|
160
247
|
aria-label="Previous month"
|
|
161
248
|
{disabled}
|
|
162
249
|
>
|
|
163
|
-
<svg class=
|
|
250
|
+
<svg class={density.navIcon} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
164
251
|
<path
|
|
165
252
|
fill-rule="evenodd"
|
|
166
253
|
d="M12.78 5.22a.75.75 0 0 1 0 1.06L9.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0z"
|
|
@@ -168,15 +255,18 @@
|
|
|
168
255
|
/>
|
|
169
256
|
</svg>
|
|
170
257
|
</button>
|
|
171
|
-
<span class=
|
|
258
|
+
<span class={cn('text-default-800 font-semibold', density.monthText)}>{monthLabel}</span>
|
|
172
259
|
<button
|
|
173
260
|
type="button"
|
|
174
261
|
onclick={nextMonth}
|
|
175
|
-
class=
|
|
262
|
+
class={cn(
|
|
263
|
+
'text-default-500 hover:bg-default-100 hover:text-default-800 flex cursor-pointer items-center justify-center rounded',
|
|
264
|
+
density.navBtn
|
|
265
|
+
)}
|
|
176
266
|
aria-label="Next month"
|
|
177
267
|
{disabled}
|
|
178
268
|
>
|
|
179
|
-
<svg class=
|
|
269
|
+
<svg class={density.navIcon} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
180
270
|
<path
|
|
181
271
|
fill-rule="evenodd"
|
|
182
272
|
d="M7.22 14.78a.75.75 0 0 1 0-1.06L10.94 10 7.22 6.28a.75.75 0 0 1 1.06-1.06l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0z"
|
|
@@ -187,7 +277,12 @@
|
|
|
187
277
|
</div>
|
|
188
278
|
{/if}
|
|
189
279
|
|
|
190
|
-
<div
|
|
280
|
+
<div
|
|
281
|
+
class={cn(
|
|
282
|
+
'text-default-400 mb-1 grid grid-cols-7 gap-0.5 text-center font-medium',
|
|
283
|
+
density.dayHeaderText
|
|
284
|
+
)}
|
|
285
|
+
>
|
|
191
286
|
{#each dayHeaders() as d (d)}
|
|
192
287
|
<div>{d}</div>
|
|
193
288
|
{/each}
|
|
@@ -202,7 +297,9 @@
|
|
|
202
297
|
aria-pressed={cell.isSelected}
|
|
203
298
|
aria-label={cell.date.toLocaleDateString()}
|
|
204
299
|
class={cn(
|
|
205
|
-
'relative flex
|
|
300
|
+
'relative flex items-center justify-center rounded transition-colors',
|
|
301
|
+
density.cell,
|
|
302
|
+
density.cellText,
|
|
206
303
|
!cell.inMonth && 'text-default-300',
|
|
207
304
|
cell.inMonth && !cell.disabled && 'text-default-700 hover:bg-default-100 cursor-pointer',
|
|
208
305
|
cell.disabled && 'text-default-200 cursor-not-allowed',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ClassValue } from 'tailwind-variants';
|
|
2
|
+
import type { VariantSizes } from '../../index.js';
|
|
2
3
|
/**
|
|
3
4
|
* Calendar selection mode.
|
|
4
5
|
* - `'single'`: one date. Bind to `value`.
|
|
@@ -31,6 +32,13 @@ export type CalendarProps = {
|
|
|
31
32
|
hideHeader?: boolean;
|
|
32
33
|
/** Disable all interaction. */
|
|
33
34
|
disabled?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Density preset. Scales day cells, headers, and overall panel width
|
|
37
|
+
* to match the tight form-size ladder so a `size="sm"` Calendar fits
|
|
38
|
+
* inside a `size="sm"` DatePicker / DateRange popover without feeling
|
|
39
|
+
* oversized. `2xl` aliases `xl`. @default 'md'
|
|
40
|
+
*/
|
|
41
|
+
size?: VariantSizes;
|
|
34
42
|
/** Wrapper class. */
|
|
35
43
|
class?: ClassValue;
|
|
36
44
|
/**
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { cn } from '../../helper/cls.js';
|
|
3
3
|
import { buildTestId } from '../../helper/testid.js';
|
|
4
4
|
import { Size } from '../../variants.js';
|
|
5
|
+
import { formSizeTokens } from '../form-size.js';
|
|
5
6
|
import Popover from '../../elements/popover/Popover.svelte';
|
|
6
7
|
import Calendar from '../calendar/Calendar.svelte';
|
|
7
8
|
import type { DatePickerProps } from './date-picker-types.js';
|
|
@@ -38,16 +39,7 @@
|
|
|
38
39
|
const display = $derived(value ? formatDate(value) : '');
|
|
39
40
|
const hasErrors = $derived(errors.length > 0);
|
|
40
41
|
|
|
41
|
-
const
|
|
42
|
-
{
|
|
43
|
-
[Size.XS]: 'h-7 text-xs',
|
|
44
|
-
[Size.SM]: 'h-8 text-sm',
|
|
45
|
-
[Size.MD]: 'h-10 text-sm',
|
|
46
|
-
[Size.LG]: 'h-12 text-base',
|
|
47
|
-
[Size.XL]: 'h-14 text-lg',
|
|
48
|
-
[Size.XXL]: 'h-16 text-lg'
|
|
49
|
-
}[size]
|
|
50
|
-
);
|
|
42
|
+
const tokens = $derived(formSizeTokens[size]);
|
|
51
43
|
|
|
52
44
|
function clear(e: MouseEvent) {
|
|
53
45
|
e.stopPropagation();
|
|
@@ -83,9 +75,14 @@
|
|
|
83
75
|
aria-expanded={open}
|
|
84
76
|
aria-invalid={hasErrors}
|
|
85
77
|
class={cn(
|
|
86
|
-
'flex w-full items-center justify-between
|
|
78
|
+
'flex w-full items-center justify-between border bg-white transition-colors',
|
|
87
79
|
'focus-within:ring-2 focus-within:ring-offset-2 focus-within:outline-none',
|
|
88
|
-
|
|
80
|
+
tokens.height,
|
|
81
|
+
tokens.padX,
|
|
82
|
+
tokens.text,
|
|
83
|
+
tokens.gap,
|
|
84
|
+
tokens.radius,
|
|
85
|
+
tokens.shadow,
|
|
89
86
|
hasErrors
|
|
90
87
|
? 'border-danger-300 focus-within:border-danger-500 focus-within:ring-danger-500'
|
|
91
88
|
: 'border-default-300 focus-within:border-primary-500 focus-within:ring-primary-500',
|
|
@@ -115,7 +112,7 @@
|
|
|
115
112
|
</button>
|
|
116
113
|
{:else}
|
|
117
114
|
<svg
|
|
118
|
-
class=
|
|
115
|
+
class={cn('text-default-400', tokens.iconSize)}
|
|
119
116
|
viewBox="0 0 20 20"
|
|
120
117
|
fill="currentColor"
|
|
121
118
|
aria-hidden="true"
|
|
@@ -130,7 +127,7 @@
|
|
|
130
127
|
</button>
|
|
131
128
|
|
|
132
129
|
{#snippet content()}
|
|
133
|
-
<Calendar {value} {minDate} {maxDate} onselect={(d) => handleSelect(d as Date)} />
|
|
130
|
+
<Calendar {value} {minDate} {maxDate} {size} onselect={(d) => handleSelect(d as Date)} />
|
|
134
131
|
{/snippet}
|
|
135
132
|
</Popover>
|
|
136
133
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { VariantSizes } from '../index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Canonical size tokens used by every form control — `Input`, `Textarea`,
|
|
4
|
+
* `NumberInput`, `Tags`, `Checkbox`, and the select-based controls.
|
|
5
|
+
*
|
|
6
|
+
* The goal is that a form using `size="sm"` across mixed controls looks
|
|
7
|
+
* vertically aligned and typographically coordinated. Before this map,
|
|
8
|
+
* each component had its own inline size map that disagreed with the
|
|
9
|
+
* others (e.g. `Input` only supported SM/MD/LG while `Textarea` supported
|
|
10
|
+
* all six, and `MD` meant `text-base` in `Input` but `text-sm` in
|
|
11
|
+
* `Textarea`).
|
|
12
|
+
*
|
|
13
|
+
* Multi-line controls (Textarea) read only `padX` / `padY` / `text` —
|
|
14
|
+
* height is content-driven. Single-line controls also read `height`.
|
|
15
|
+
*/
|
|
16
|
+
export type FormSizeTokens = {
|
|
17
|
+
/** Control height for single-line inputs (e.g. `h-10`). */
|
|
18
|
+
height: string;
|
|
19
|
+
/** Horizontal padding token (e.g. `px-3`). */
|
|
20
|
+
padX: string;
|
|
21
|
+
/** Vertical padding, used by multi-line controls and chip rows. */
|
|
22
|
+
padY: string;
|
|
23
|
+
/** Font size token (e.g. `text-base`). */
|
|
24
|
+
text: string;
|
|
25
|
+
/** Inline gap between leading icon and text (e.g. `gap-2`). */
|
|
26
|
+
gap: string;
|
|
27
|
+
/** Inline icon dimension (e.g. `size-4`). */
|
|
28
|
+
iconSize: string;
|
|
29
|
+
/** Rounded corner radius that scales with the control (e.g. `rounded-lg`). */
|
|
30
|
+
radius: string;
|
|
31
|
+
/**
|
|
32
|
+
* Drop shadow. `shadow-none` at the smallest sizes so `xs` / inline
|
|
33
|
+
* table cells don't stand out against their container.
|
|
34
|
+
*/
|
|
35
|
+
shadow: string;
|
|
36
|
+
};
|
|
37
|
+
export declare const formSizeTokens: Record<VariantSizes, FormSizeTokens>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Size } from '../variants.js';
|
|
2
|
+
// Tight ladder by design — we prefer compact, focused form controls
|
|
3
|
+
// over bulky ones. `md` (the default) sits at `h-7` / `text-xs` so a
|
|
4
|
+
// stock form reads dense; consumers who want roomier controls opt up to
|
|
5
|
+
// `lg` / `xl`. `xs` is unmistakably inline-text-in-a-table-cell — 20px
|
|
6
|
+
// tall, no shadow, barely-there rounded corners.
|
|
7
|
+
//
|
|
8
|
+
// `2xl` (`Size.XXL`) intentionally aliases `xl` for form controls: form
|
|
9
|
+
// fields don't need a sixth, jumbo tier — anything larger reads as a
|
|
10
|
+
// display element rather than an input. We still accept the token (the
|
|
11
|
+
// `Size` enum exposes it library-wide) and quietly fall back to `xl`
|
|
12
|
+
// rather than requiring every component to exclude it from its size type.
|
|
13
|
+
const xl = {
|
|
14
|
+
height: 'h-11',
|
|
15
|
+
padX: 'px-3.5',
|
|
16
|
+
padY: 'py-2.5',
|
|
17
|
+
text: 'text-base',
|
|
18
|
+
gap: 'gap-2.5',
|
|
19
|
+
iconSize: 'size-5',
|
|
20
|
+
radius: 'rounded-lg',
|
|
21
|
+
shadow: 'shadow-xs'
|
|
22
|
+
};
|
|
23
|
+
export const formSizeTokens = {
|
|
24
|
+
[Size.XS]: {
|
|
25
|
+
height: 'h-5',
|
|
26
|
+
padX: 'px-1',
|
|
27
|
+
padY: 'py-0',
|
|
28
|
+
text: 'text-xs',
|
|
29
|
+
gap: 'gap-1',
|
|
30
|
+
iconSize: 'size-3',
|
|
31
|
+
radius: 'rounded-sm',
|
|
32
|
+
shadow: 'shadow-none'
|
|
33
|
+
},
|
|
34
|
+
[Size.SM]: {
|
|
35
|
+
height: 'h-6',
|
|
36
|
+
padX: 'px-1.5',
|
|
37
|
+
padY: 'py-0.5',
|
|
38
|
+
text: 'text-xs',
|
|
39
|
+
gap: 'gap-1',
|
|
40
|
+
iconSize: 'size-3',
|
|
41
|
+
radius: 'rounded',
|
|
42
|
+
shadow: 'shadow-xs'
|
|
43
|
+
},
|
|
44
|
+
[Size.MD]: {
|
|
45
|
+
height: 'h-7',
|
|
46
|
+
padX: 'px-2',
|
|
47
|
+
padY: 'py-1',
|
|
48
|
+
text: 'text-xs',
|
|
49
|
+
gap: 'gap-1.5',
|
|
50
|
+
iconSize: 'size-3.5',
|
|
51
|
+
radius: 'rounded-md',
|
|
52
|
+
shadow: 'shadow-xs'
|
|
53
|
+
},
|
|
54
|
+
[Size.LG]: {
|
|
55
|
+
height: 'h-9',
|
|
56
|
+
padX: 'px-2.5',
|
|
57
|
+
padY: 'py-1.5',
|
|
58
|
+
text: 'text-sm',
|
|
59
|
+
gap: 'gap-2',
|
|
60
|
+
iconSize: 'size-4',
|
|
61
|
+
radius: 'rounded-md',
|
|
62
|
+
shadow: 'shadow-xs'
|
|
63
|
+
},
|
|
64
|
+
[Size.XL]: xl,
|
|
65
|
+
// Form controls cap at xl visually — see comment above.
|
|
66
|
+
[Size.XXL]: xl
|
|
67
|
+
};
|
|
@@ -291,6 +291,11 @@ export interface DateRangeProps {
|
|
|
291
291
|
*/
|
|
292
292
|
format?: string;
|
|
293
293
|
errors?: string[];
|
|
294
|
+
/**
|
|
295
|
+
* Trigger control size — shares the form-size ladder with `Input`,
|
|
296
|
+
* `Select`, etc. so a row of mixed controls lines up. @default 'md'
|
|
297
|
+
*/
|
|
298
|
+
size?: VariantSizes;
|
|
294
299
|
/**
|
|
295
300
|
* Fires when the user completes a selection. Both dates may be
|
|
296
301
|
* undefined (cleared).
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { tv } from 'tailwind-variants';
|
|
2
2
|
import { cn } from '../helper/cls.js';
|
|
3
|
-
import { Color
|
|
3
|
+
import { Color } from '../variants.js';
|
|
4
|
+
import { formSizeTokens } from './form-size.js';
|
|
4
5
|
export const segmentedTrack = tv({
|
|
5
6
|
// `max-w-full overflow-x-auto` + hidden scrollbar lets a long segment
|
|
6
7
|
// row swipe on narrow viewports instead of bleeding past the parent.
|
|
@@ -36,18 +37,18 @@ const selectedByColor = {
|
|
|
36
37
|
[Color.WARNING]: 'bg-warning-600 text-white shadow-sm',
|
|
37
38
|
[Color.DANGER]: 'bg-danger-600 text-white shadow-sm'
|
|
38
39
|
};
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
40
|
+
// Pull padding/text/gap from the shared form-size tokens so a
|
|
41
|
+
// SegmentedControl at `size="md"` coordinates with Input / Select /
|
|
42
|
+
// DateRange / etc. at the same tier. Segments don't use `height` —
|
|
43
|
+
// their height falls out of text + padding.
|
|
44
|
+
function segmentSize(size) {
|
|
45
|
+
const t = formSizeTokens[size];
|
|
46
|
+
return `${t.gap} ${t.padX} ${t.padY} ${t.text}`;
|
|
47
|
+
}
|
|
47
48
|
export function segmentClasses(args) {
|
|
48
49
|
const { selected, disabled, appearance, color, size } = args;
|
|
49
50
|
if (appearance === 'pills') {
|
|
50
|
-
const pillBase = cn('flex cursor-pointer items-center justify-center rounded-full font-medium transition-colors duration-150 shrink-0 whitespace-nowrap', 'focus-visible:ring-primary-500 focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-offset-2', segmentSize
|
|
51
|
+
const pillBase = cn('flex cursor-pointer items-center justify-center rounded-full font-medium transition-colors duration-150 shrink-0 whitespace-nowrap', 'focus-visible:ring-primary-500 focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-offset-2', segmentSize(size), disabled && 'cursor-not-allowed opacity-50');
|
|
51
52
|
if (disabled)
|
|
52
53
|
return cn(pillBase, 'text-default-400');
|
|
53
54
|
if (selected)
|
|
@@ -57,7 +58,7 @@ export function segmentClasses(args) {
|
|
|
57
58
|
const base = cn('flex cursor-pointer items-center justify-center font-medium transition-colors duration-150 shrink-0 whitespace-nowrap', 'focus-visible:ring-primary-500 focus-visible:ring-2 focus-visible:outline-none', appearance === 'inverted' ? 'focus-visible:ring-offset-0' : 'focus-visible:ring-offset-2',
|
|
58
59
|
// Inverted track has p-0.5 padding around segments — give them inner radius
|
|
59
60
|
// so selected segments don't render as square boxes (matches surface look).
|
|
60
|
-
appearance === 'inverted' && 'rounded-md', segmentSize
|
|
61
|
+
appearance === 'inverted' && 'rounded-md', segmentSize(size), disabled && 'cursor-not-allowed opacity-50');
|
|
61
62
|
if (disabled) {
|
|
62
63
|
return cn(base, 'text-default-400');
|
|
63
64
|
}
|
package/dist/forms/slider.js
CHANGED
|
@@ -1,62 +1,69 @@
|
|
|
1
1
|
import { tv } from 'tailwind-variants';
|
|
2
2
|
import { Size } from '../variants.js';
|
|
3
|
+
import { formSizeTokens } from './form-size.js';
|
|
4
|
+
// Track/thumb dimensions are Slider-specific — a "size" for a slider
|
|
5
|
+
// means the physical rail, not an input height. We keep these tuned per
|
|
6
|
+
// tier but pull `text` classes from the shared form-size tokens so a
|
|
7
|
+
// Slider's label reads the same size as an adjacent Input's text.
|
|
8
|
+
const textFor = (size) => formSizeTokens[size].text;
|
|
3
9
|
export const slider = tv({
|
|
4
10
|
slots: {
|
|
5
11
|
base: 'relative w-full',
|
|
6
|
-
track: 'absolute
|
|
12
|
+
track: 'absolute w-full rounded-full bg-default-200 cursor-pointer',
|
|
7
13
|
range: 'absolute h-full rounded-full bg-primary-500',
|
|
8
14
|
thumb: [
|
|
9
15
|
'absolute top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white border-2 border-primary-500',
|
|
10
16
|
'focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
|
11
17
|
'hover:scale-110 transition-transform cursor-pointer'
|
|
12
18
|
],
|
|
13
|
-
mark: 'absolute text-
|
|
14
|
-
label: 'mb-2 block
|
|
15
|
-
value: 'mt-1 text-
|
|
19
|
+
mark: 'absolute text-default-500 -translate-x-1/2',
|
|
20
|
+
label: 'mb-2 block font-medium text-default-700',
|
|
21
|
+
value: 'mt-1 text-default-500'
|
|
16
22
|
},
|
|
17
23
|
variants: {
|
|
18
24
|
size: {
|
|
19
25
|
[Size.XS]: {
|
|
20
|
-
track: 'h-
|
|
21
|
-
thumb: 'w-
|
|
22
|
-
mark:
|
|
23
|
-
label:
|
|
24
|
-
value:
|
|
26
|
+
track: 'h-0.5',
|
|
27
|
+
thumb: 'w-2.5 h-2.5',
|
|
28
|
+
mark: `${textFor(Size.XS)} top-3`,
|
|
29
|
+
label: textFor(Size.XS),
|
|
30
|
+
value: textFor(Size.XS)
|
|
25
31
|
},
|
|
26
32
|
[Size.SM]: {
|
|
27
33
|
track: 'h-1',
|
|
28
34
|
thumb: 'w-3 h-3',
|
|
29
|
-
mark:
|
|
30
|
-
label:
|
|
31
|
-
value:
|
|
35
|
+
mark: `${textFor(Size.SM)} top-4`,
|
|
36
|
+
label: textFor(Size.SM),
|
|
37
|
+
value: textFor(Size.SM)
|
|
32
38
|
},
|
|
33
39
|
[Size.MD]: {
|
|
34
|
-
track: 'h-
|
|
35
|
-
thumb: 'w-
|
|
36
|
-
mark:
|
|
37
|
-
label:
|
|
38
|
-
value:
|
|
40
|
+
track: 'h-1.5',
|
|
41
|
+
thumb: 'w-3.5 h-3.5',
|
|
42
|
+
mark: `${textFor(Size.MD)} top-5`,
|
|
43
|
+
label: textFor(Size.MD),
|
|
44
|
+
value: textFor(Size.MD)
|
|
39
45
|
},
|
|
40
46
|
[Size.LG]: {
|
|
41
|
-
track: 'h-
|
|
42
|
-
thumb: 'w-
|
|
43
|
-
mark:
|
|
44
|
-
label:
|
|
45
|
-
value:
|
|
47
|
+
track: 'h-2',
|
|
48
|
+
thumb: 'w-4 h-4',
|
|
49
|
+
mark: `${textFor(Size.LG)} top-6`,
|
|
50
|
+
label: textFor(Size.LG),
|
|
51
|
+
value: textFor(Size.LG)
|
|
46
52
|
},
|
|
47
53
|
[Size.XL]: {
|
|
48
54
|
track: 'h-3',
|
|
49
55
|
thumb: 'w-5 h-5',
|
|
50
|
-
mark:
|
|
51
|
-
label:
|
|
52
|
-
value:
|
|
56
|
+
mark: `${textFor(Size.XL)} top-7`,
|
|
57
|
+
label: textFor(Size.XL),
|
|
58
|
+
value: textFor(Size.XL)
|
|
53
59
|
},
|
|
60
|
+
// Form controls cap at xl — see `form-size.ts`.
|
|
54
61
|
[Size.XXL]: {
|
|
55
62
|
track: 'h-3',
|
|
56
63
|
thumb: 'w-5 h-5',
|
|
57
|
-
mark:
|
|
58
|
-
label:
|
|
59
|
-
value:
|
|
64
|
+
mark: `${textFor(Size.XXL)} top-7`,
|
|
65
|
+
label: textFor(Size.XXL),
|
|
66
|
+
value: textFor(Size.XXL)
|
|
60
67
|
}
|
|
61
68
|
},
|
|
62
69
|
disabled: {
|