@noxlovette/material 0.9.3 → 0.10.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/dist/components/buttons/connected/theme.d.ts +15 -15
- package/dist/components/buttons/connected/theme.js +20 -5
- package/dist/components/buttons/split-button/theme.d.ts +15 -15
- package/dist/components/buttons/split-button/theme.js +21 -15
- package/dist/components/buttons/theme.d.ts +40 -40
- package/dist/components/buttons/theme.js +29 -29
- package/dist/components/cards/theme.js +1 -1
- package/dist/components/chips/Chip.svelte +1 -1
- package/dist/components/chips/theme.js +1 -1
- package/dist/components/containers/bottom-sheet/theme.js +1 -1
- package/dist/components/containers/carousel/theme.js +1 -1
- package/dist/components/containers/context-menu/theme.js +2 -2
- package/dist/components/containers/dialogue/theme.js +1 -1
- package/dist/components/containers/list/theme.d.ts +3 -3
- package/dist/components/containers/list/theme.js +8 -8
- package/dist/components/containers/menu/theme.js +3 -3
- package/dist/components/date/theme.js +2 -2
- package/dist/components/forms/checkbox/theme.js +1 -1
- package/dist/components/forms/command/theme.js +1 -1
- package/dist/components/forms/index.d.ts +1 -0
- package/dist/components/forms/index.js +1 -0
- package/dist/components/forms/search/theme.js +1 -1
- package/dist/components/forms/select/theme.js +2 -2
- package/dist/components/forms/slider/Slider.svelte +8 -0
- package/dist/components/forms/slider/theme.js +7 -7
- package/dist/components/forms/slider/types.d.ts +13 -0
- package/dist/components/forms/textarea/Textarea.mdx +63 -0
- package/dist/components/forms/textarea/Textarea.stories.svelte +99 -0
- package/dist/components/forms/textarea/Textarea.stories.svelte.d.ts +19 -0
- package/dist/components/forms/textarea/Textarea.svelte +142 -0
- package/dist/components/forms/textarea/Textarea.svelte.d.ts +13 -0
- package/dist/components/forms/textarea/index.d.ts +3 -0
- package/dist/components/forms/textarea/index.js +6 -0
- package/dist/components/forms/textarea/theme.d.ts +150 -0
- package/dist/components/forms/textarea/theme.js +155 -0
- package/dist/components/forms/textarea/types.d.ts +59 -0
- package/dist/components/forms/textarea/types.js +1 -0
- package/dist/components/forms/textfield/theme.js +4 -4
- package/dist/components/forms/toggle-group/theme.js +2 -2
- package/dist/components/misc/ThemeSettings.svelte +1 -1
- package/dist/components/misc/materialAvatarPalette.js +1 -1
- package/dist/components/nav/appbar/theme.js +22 -6
- package/dist/components/nav/navbar/theme.js +1 -1
- package/dist/components/nav/tabs/theme.js +1 -1
- package/dist/components/progress/CircularProgress.svelte +5 -3
- package/dist/components/progress/CircularProgress.svelte.d.ts +2 -1
- package/dist/components/progress/LinearProgress.svelte +5 -2
- package/dist/components/progress/LinearProgress.svelte.d.ts +2 -1
- package/dist/components/progress/WavyLinearProgress.svelte +5 -3
- package/dist/components/progress/WavyLinearProgress.svelte.d.ts +2 -1
- package/dist/components/progress/index.d.ts +1 -0
- package/dist/components/progress/index.js +1 -0
- package/dist/components/progress/types.d.ts +16 -0
- package/dist/components/progress/types.js +1 -0
- package/dist/components/snackbar/theme.js +1 -1
- package/dist/components/table/TableHeader.svelte +1 -1
- package/dist/components/toolbar/theme.js +2 -2
- package/dist/styles/components.css +7 -2
- package/dist/styles/icon.css +19 -0
- package/dist/styles/rounding.css +4 -0
- package/dist/utils/Theme.svelte +1 -1
- package/dist/utils/ThemeScript.svelte +1 -1
- package/dist/utils/icon/Icon.svelte +5 -5
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +1 -1
- package/package.json +1 -1
- /package/dist/utils/{theme.svelte.d.ts → themeState.svelte.d.ts} +0 -0
- /package/dist/utils/{theme.svelte.js → themeState.svelte.js} +0 -0
|
@@ -13,7 +13,7 @@ export const carousel = tv({
|
|
|
13
13
|
snapPoint: 'pointer-events-none absolute top-spacing-0 h-px w-px snap-start',
|
|
14
14
|
viewport: 'sticky left-spacing-0 h-full overflow-clip',
|
|
15
15
|
item: [
|
|
16
|
-
'text-md-sys-color-on-surface bg-md-sys-color-surface relative block shrink-0 overflow-hidden rounded-
|
|
16
|
+
'text-md-sys-color-on-surface bg-md-sys-color-surface relative block shrink-0 overflow-hidden rounded-xl text-start',
|
|
17
17
|
'md-sys-state-focus-indicator transition-shadow md-sys-motion-effects'
|
|
18
18
|
],
|
|
19
19
|
content: 'h-full'
|
|
@@ -24,9 +24,9 @@ export const contextMenu = tv({
|
|
|
24
24
|
itemContent: 'flex flex-1 flex-col gap-spacing-25 min-w-spacing-0',
|
|
25
25
|
label: 'md-sys-typescale-body-large text-md-sys-color-on-surface',
|
|
26
26
|
helper: 'md-sys-typescale-body-medium text-md-sys-color-on-surface-variant',
|
|
27
|
-
icon: 'size-
|
|
27
|
+
icon: 'size-(--text-icon-20) text-icon-20 text-md-sys-color-on-surface-variant shrink-0',
|
|
28
28
|
separator: 'my-spacing-50 h-px bg-md-sys-color-outline-variant',
|
|
29
|
-
rightSlot: 'ml-auto size-
|
|
29
|
+
rightSlot: 'ml-auto size-(--text-icon-20) text-icon-20 shrink-0 text-md-sys-color-on-surface-variant'
|
|
30
30
|
},
|
|
31
31
|
variants: {
|
|
32
32
|
selected: {
|
|
@@ -2,7 +2,7 @@ import { tv } from '../../../utils/tv.js';
|
|
|
2
2
|
export const dialogue = tv({
|
|
3
3
|
slots: {
|
|
4
4
|
base: 'fixed inset-spacing-0 z-layer-modal bg-md-sys-color-scrim/32',
|
|
5
|
-
inner: 'fixed inset-spacing-0 z-layer-modal m-auto h-max w-full outline-none bg-md-sys-color-surface-container-high shadow-elevation-3 flex flex-col rounded-
|
|
5
|
+
inner: 'fixed inset-spacing-0 z-layer-modal m-auto h-max w-full outline-none bg-md-sys-color-surface-container-high shadow-elevation-3 flex flex-col rounded-xl p-spacing-300 gap-spacing-200',
|
|
6
6
|
headlineContainer: 'md-sys-typescale-headline-small text-md-sys-color-on-surface',
|
|
7
7
|
supportingTextContainer: 'md-sys-typescale-body-medium text-md-sys-color-on-surface-variant',
|
|
8
8
|
buttonContainer: 'mt-spacing-100 flex items-center justify-end gap-spacing-100'
|
|
@@ -4,17 +4,17 @@ export type ListMediaVariants = VariantProps<typeof listMedia>;
|
|
|
4
4
|
export declare const list: import("tailwind-variants").TVReturnType<{
|
|
5
5
|
variant: {
|
|
6
6
|
standard: "";
|
|
7
|
-
segmented: "gap-spacing-25 [&>:first-child]:[--li-top:
|
|
7
|
+
segmented: "gap-spacing-25 [&>:first-child]:[--li-top:var(--radius-lg)] [&>:last-child]:[--li-bottom:var(--radius-lg)]";
|
|
8
8
|
};
|
|
9
9
|
}, undefined, "flex w-full flex-col [--li-bottom:initial] [--li-top:initial]", {
|
|
10
10
|
variant: {
|
|
11
11
|
standard: "";
|
|
12
|
-
segmented: "gap-spacing-25 [&>:first-child]:[--li-top:
|
|
12
|
+
segmented: "gap-spacing-25 [&>:first-child]:[--li-top:var(--radius-lg)] [&>:last-child]:[--li-bottom:var(--radius-lg)]";
|
|
13
13
|
};
|
|
14
14
|
}, undefined, import("tailwind-variants").TVReturnTypeLike<{
|
|
15
15
|
variant: {
|
|
16
16
|
standard: "";
|
|
17
|
-
segmented: "gap-spacing-25 [&>:first-child]:[--li-top:
|
|
17
|
+
segmented: "gap-spacing-25 [&>:first-child]:[--li-top:var(--radius-lg)] [&>:last-child]:[--li-bottom:var(--radius-lg)]";
|
|
18
18
|
};
|
|
19
19
|
}, undefined>>;
|
|
20
20
|
export declare const listItem: import("tailwind-variants").TVReturnType<{
|
|
@@ -19,7 +19,7 @@ export const list = tv({
|
|
|
19
19
|
variant: {
|
|
20
20
|
standard: '',
|
|
21
21
|
// 2dp between segments; outer corners 16dp (md.comp.list.container.shape).
|
|
22
|
-
segmented: 'gap-spacing-25 [&>:first-child]:[--li-top:
|
|
22
|
+
segmented: 'gap-spacing-25 [&>:first-child]:[--li-top:var(--radius-lg)] [&>:last-child]:[--li-bottom:var(--radius-lg)]'
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
25
|
defaultVariants: { variant: 'standard' }
|
|
@@ -28,7 +28,7 @@ export const listItem = tv({
|
|
|
28
28
|
slots: {
|
|
29
29
|
base: [
|
|
30
30
|
'relative isolate flex w-full min-w-spacing-0 gap-spacing-150 ps-spacing-200 pe-spacing-200 pt-spacing-125 pb-spacing-125 text-start',
|
|
31
|
-
'[--li-shape:
|
|
31
|
+
'[--li-shape:var(--radius-xs)] rounded-t-[var(--li-top,var(--li-shape))] rounded-b-[var(--li-bottom,var(--li-shape))]',
|
|
32
32
|
'transition-[border-radius,background-color,color] md-sys-motion-fast-spatial',
|
|
33
33
|
'text-md-sys-color-on-surface-variant'
|
|
34
34
|
],
|
|
@@ -69,17 +69,17 @@ export const listItem = tv({
|
|
|
69
69
|
true: {
|
|
70
70
|
base: [
|
|
71
71
|
'cursor-pointer outline-none',
|
|
72
|
-
'hover:[--li-shape:
|
|
73
|
-
'focus-visible:[--li-shape:
|
|
72
|
+
'hover:[--li-shape:var(--radius-md)]',
|
|
73
|
+
'focus-visible:[--li-shape:var(--radius-lg)]',
|
|
74
74
|
'focus-visible:outline-solid focus-visible:outline-3 focus-visible:-outline-offset-3 focus-visible:outline-md-sys-color-secondary',
|
|
75
|
-
'active:[--li-shape:
|
|
75
|
+
'active:[--li-shape:var(--radius-lg)]'
|
|
76
76
|
]
|
|
77
77
|
},
|
|
78
78
|
false: ''
|
|
79
79
|
},
|
|
80
80
|
selected: {
|
|
81
81
|
true: {
|
|
82
|
-
base: 'text-md-sys-color-on-secondary-container [--li-shape:
|
|
82
|
+
base: 'text-md-sys-color-on-secondary-container [--li-shape:var(--radius-lg)] hover:[--li-shape:var(--radius-lg)]',
|
|
83
83
|
leading: 'text-md-sys-color-on-secondary-container',
|
|
84
84
|
overline: 'text-md-sys-color-on-secondary-container',
|
|
85
85
|
headline: 'text-md-sys-color-on-secondary-container',
|
|
@@ -104,10 +104,10 @@ export const listItem = tv({
|
|
|
104
104
|
expanded: {
|
|
105
105
|
true: {
|
|
106
106
|
// The expanded group is one 16dp container: header on top, nested items below.
|
|
107
|
-
base: '[--li-top:
|
|
107
|
+
base: '[--li-top:var(--radius-lg)] [--li-bottom:var(--li-shape)]',
|
|
108
108
|
expandIcon: 'rotate-180 bg-md-sys-color-surface-container',
|
|
109
109
|
region: 'grid-rows-[1fr]',
|
|
110
|
-
nested: '[&>:last-child]:[--li-bottom:
|
|
110
|
+
nested: '[&>:last-child]:[--li-bottom:var(--radius-lg)]'
|
|
111
111
|
},
|
|
112
112
|
false: {
|
|
113
113
|
expandIcon: 'bg-md-sys-color-surface',
|
|
@@ -16,11 +16,11 @@ export const menu = tv({
|
|
|
16
16
|
focus-visible:outline-md-sys-color-primary
|
|
17
17
|
data-[disabled]:cursor-not-allowed data-[disabled]:opacity-38
|
|
18
18
|
`,
|
|
19
|
-
icon: 'text-md-sys-color-on-surface-variant size-
|
|
20
|
-
indicator: 'size-
|
|
19
|
+
icon: 'text-md-sys-color-on-surface-variant size-(--text-icon-20) text-icon-20 shrink-0',
|
|
20
|
+
indicator: 'size-(--text-icon-20) text-icon-20 shrink-0 text-md-sys-color-on-surface-variant',
|
|
21
21
|
itemContent: 'flex flex-1 flex-col gap-spacing-50 text-left min-w-spacing-0',
|
|
22
22
|
helper: 'md-sys-typescale-body-medium text-md-sys-color-on-surface-variant',
|
|
23
|
-
rightSlot: 'text-md-sys-color-on-surface-variant size-
|
|
23
|
+
rightSlot: 'text-md-sys-color-on-surface-variant size-(--text-icon-20) text-icon-20 shrink-0',
|
|
24
24
|
groupHeading: 'px-spacing-150 pt-spacing-100 pb-spacing-50 md-sys-typescale-label-medium text-md-sys-color-primary'
|
|
25
25
|
},
|
|
26
26
|
variants: {
|
|
@@ -15,7 +15,7 @@ export const dateRangeField = tv({
|
|
|
15
15
|
leadingIcon: `
|
|
16
16
|
text-md-sys-color-on-surface-variant size-spacing-300
|
|
17
17
|
group-focus-within:text-md-sys-color-primary
|
|
18
|
-
group-data-[invalid]:text-md-sys-color-error ml-spacing-150 text-
|
|
18
|
+
group-data-[invalid]:text-md-sys-color-error ml-spacing-150 text-icon-24
|
|
19
19
|
`,
|
|
20
20
|
trailingIcon: `
|
|
21
21
|
mr-spacing-50 shrink-0 text-md-sys-color-on-surface-variant
|
|
@@ -124,7 +124,7 @@ export const dateField = tv({
|
|
|
124
124
|
leadingIcon: `
|
|
125
125
|
text-md-sys-color-on-surface-variant size-spacing-300
|
|
126
126
|
group-focus-within:text-md-sys-color-primary
|
|
127
|
-
group-data-[invalid]:text-md-sys-color-error ml-spacing-150 text-
|
|
127
|
+
group-data-[invalid]:text-md-sys-color-error ml-spacing-150 text-icon-24
|
|
128
128
|
`,
|
|
129
129
|
trailingIcon: `
|
|
130
130
|
mr-spacing-50 shrink-0 text-md-sys-color-on-surface-variant
|
|
@@ -4,7 +4,7 @@ export const checkbox = tv({
|
|
|
4
4
|
root: 'group inline-flex size-spacing-500 shrink-0 select-none items-center justify-center cursor-pointer',
|
|
5
5
|
container: 'relative inline-flex size-[18px] shrink-0',
|
|
6
6
|
control: 'layer-container absolute -inset-[11px] rounded-full text-md-sys-color-on-surface-variant state-layer before:rounded-full group-focus-visible:outline group-focus-visible:outline-3 group-focus-visible:outline-offset-2 group-focus-visible:outline-md-sys-color-secondary transition-colors md-sys-motion-fast-effects',
|
|
7
|
-
box: 'absolute inset-[11px] rounded-
|
|
7
|
+
box: 'absolute inset-[11px] rounded-xs border-2 border-current bg-md-sys-color-surface transition-colors md-sys-motion-fast-effects',
|
|
8
8
|
checkIcon: 'pointer-events-none absolute inset-spacing-0 m-auto size-spacing-175 text-md-sys-color-on-primary opacity-0 scale-75',
|
|
9
9
|
indeterminateIcon: 'pointer-events-none absolute inset-spacing-0 m-auto block h-spacing-25 w-spacing-150 rounded-full bg-md-sys-color-on-primary opacity-0 scale-75',
|
|
10
10
|
label: 'md-sys-typescale-body-large leading-5 text-md-sys-color-on-surface cursor-pointer'
|
|
@@ -25,7 +25,7 @@ export const commandItem = tv({
|
|
|
25
25
|
slots: {
|
|
26
26
|
base: [
|
|
27
27
|
'cursor-default select-none outline-none',
|
|
28
|
-
'data-selected:[--li-shape:
|
|
28
|
+
'data-selected:[--li-shape:var(--radius-md)] data-selected:bg-md-sys-color-on-surface/10'
|
|
29
29
|
]
|
|
30
30
|
},
|
|
31
31
|
// The list's own surface fill is a variant, so it's replaced here, not in the base.
|
|
@@ -6,6 +6,7 @@ export * from './search/index.js';
|
|
|
6
6
|
export * from './select/index.js';
|
|
7
7
|
export * from './slider/index.js';
|
|
8
8
|
export * from './switch/index.js';
|
|
9
|
+
export * from './textarea/index.js';
|
|
9
10
|
export * from './textfield/index.js';
|
|
10
11
|
export * from './toggle-group/index.js';
|
|
11
12
|
export * from './tooltip/index.js';
|
|
@@ -8,6 +8,7 @@ export * from './search/index.js';
|
|
|
8
8
|
export * from './select/index.js';
|
|
9
9
|
export * from './slider/index.js';
|
|
10
10
|
export * from './switch/index.js';
|
|
11
|
+
export * from './textarea/index.js';
|
|
11
12
|
export * from './textfield/index.js';
|
|
12
13
|
export * from './toggle-group/index.js';
|
|
13
14
|
export * from './tooltip/index.js';
|
|
@@ -38,7 +38,7 @@ export const searchView = tv({
|
|
|
38
38
|
// keys are on gets the M3 focus indicator, drawn inside the item, and a focused item's shape.
|
|
39
39
|
// `outline-solid` is load-bearing: ListItem's `outline-none` sets --tw-outline-style to none,
|
|
40
40
|
// which `outline-3` reads, so without it the ring has a width and colour but no style.
|
|
41
|
-
results: 'min-h-spacing-0 overflow-y-auto overscroll-contain [&_[role=option]:not([aria-disabled=true])]:bg-transparent [&_[data-highlighted]]:outline-solid [&_[data-highlighted]]:outline-3 [&_[data-highlighted]]:-outline-offset-3 [&_[data-highlighted]]:outline-md-sys-color-secondary [&_[data-highlighted]]:[--li-shape:
|
|
41
|
+
results: 'min-h-spacing-0 overflow-y-auto overscroll-contain [&_[role=option]:not([aria-disabled=true])]:bg-transparent [&_[data-highlighted]]:outline-solid [&_[data-highlighted]]:outline-3 [&_[data-highlighted]]:-outline-offset-3 [&_[data-highlighted]]:outline-md-sys-color-secondary [&_[data-highlighted]]:[--li-shape:var(--radius-lg)]'
|
|
42
42
|
},
|
|
43
43
|
variants: {
|
|
44
44
|
hasTrailing: { true: '', false: { input: 'me-spacing-150' } }
|
|
@@ -22,12 +22,12 @@ export const select = tv({
|
|
|
22
22
|
leadingIcon: `
|
|
23
23
|
text-md-sys-color-on-surface-variant size-spacing-300
|
|
24
24
|
group-has-[[data-state=open]]:text-md-sys-color-primary
|
|
25
|
-
group-data-[invalid]:text-md-sys-color-error ml-spacing-150 text-
|
|
25
|
+
group-data-[invalid]:text-md-sys-color-error ml-spacing-150 text-icon-24
|
|
26
26
|
`,
|
|
27
27
|
dropdownIcon: `
|
|
28
28
|
mr-spacing-150 text-md-sys-color-on-surface-variant size-spacing-300
|
|
29
29
|
group-data-[state=open]:text-md-sys-color-primary
|
|
30
|
-
group-data-[invalid]:text-md-sys-color-error z-20 text-
|
|
30
|
+
group-data-[invalid]:text-md-sys-color-error z-20 text-icon-24
|
|
31
31
|
transition-transform md-sys-motion-fast-spatial
|
|
32
32
|
group-data-[state=open]:rotate-180
|
|
33
33
|
`,
|
|
@@ -36,6 +36,9 @@ the track, a new `value` from outside) springs to it on the fast spatial spring.
|
|
|
36
36
|
class: className,
|
|
37
37
|
dir,
|
|
38
38
|
id,
|
|
39
|
+
'aria-label': ariaLabel,
|
|
40
|
+
'aria-labelledby': ariaLabelledby,
|
|
41
|
+
'aria-describedby': ariaDescribedby,
|
|
39
42
|
...extra
|
|
40
43
|
}: SliderProps = $props();
|
|
41
44
|
|
|
@@ -216,8 +219,13 @@ the track, a new `value` from outside) springs to it on the fast spatial spring.
|
|
|
216
219
|
|
|
217
220
|
<Slider.Thumb index={0}>
|
|
218
221
|
{#snippet child({ props: thumbProps, active })}
|
|
222
|
+
<!-- The thumb is the focusable role="slider" element, so it carries the accessible name. -->
|
|
219
223
|
<div
|
|
220
224
|
{...thumbProps}
|
|
225
|
+
aria-label={ariaLabel}
|
|
226
|
+
aria-labelledby={ariaLabelledby}
|
|
227
|
+
aria-describedby={ariaDescribedby}
|
|
228
|
+
aria-valuetext={format(value)}
|
|
221
229
|
class={cls.handle()}
|
|
222
230
|
data-active={active || undefined}
|
|
223
231
|
style="{start}: {center(pos)}; {extent}: {handlePx}px"
|
|
@@ -40,23 +40,23 @@ export const slider = tv({
|
|
|
40
40
|
s: { root: '[--track:1.5rem] [--handle:2.75rem] [--outer:var(--radius-sm)]' },
|
|
41
41
|
m: {
|
|
42
42
|
root: '[--track:2.5rem] [--handle:3.25rem] [--outer:var(--radius-md)]',
|
|
43
|
-
iconGlyph: 'size-
|
|
43
|
+
iconGlyph: 'size-(--text-icon-24) text-icon-24'
|
|
44
44
|
},
|
|
45
45
|
l: {
|
|
46
46
|
root: '[--track:3.5rem] [--handle:4.25rem] [--outer:var(--radius-lg)]',
|
|
47
|
-
iconGlyph: 'size-
|
|
47
|
+
iconGlyph: 'size-(--text-icon-24) text-icon-24'
|
|
48
48
|
},
|
|
49
49
|
xl: {
|
|
50
50
|
root: '[--track:6rem] [--handle:6.75rem] [--outer:var(--radius-xl)]',
|
|
51
|
-
iconGlyph: 'size-
|
|
51
|
+
iconGlyph: 'size-(--text-icon-32) text-icon-32'
|
|
52
52
|
}
|
|
53
53
|
},
|
|
54
54
|
vertical: {
|
|
55
55
|
false: {
|
|
56
56
|
root: 'h-[var(--handle)] w-full min-w-40',
|
|
57
57
|
track: 'top-1/2 h-[var(--track)] -translate-y-1/2',
|
|
58
|
-
activeTrack: 'left-spacing-0 rounded-l-
|
|
59
|
-
inactiveTrack: 'right-spacing-0 rounded-l-
|
|
58
|
+
activeTrack: 'left-spacing-0 rounded-l-(--outer) rounded-r-(--md-comp-slider-inner-corner)',
|
|
59
|
+
inactiveTrack: 'right-spacing-0 rounded-l-(--md-comp-slider-inner-corner) rounded-r-(--outer)',
|
|
60
60
|
stop: 'top-1/2 -translate-x-1/2 -translate-y-1/2',
|
|
61
61
|
icon: 'top-1/2 -translate-y-1/2',
|
|
62
62
|
handle: 'top-spacing-0 h-full -translate-x-1/2',
|
|
@@ -65,8 +65,8 @@ export const slider = tv({
|
|
|
65
65
|
true: {
|
|
66
66
|
root: 'h-full min-h-40 w-[var(--handle)]',
|
|
67
67
|
track: 'left-1/2 w-[var(--track)] -translate-x-1/2',
|
|
68
|
-
activeTrack: 'bottom-spacing-0 rounded-t-
|
|
69
|
-
inactiveTrack: 'top-spacing-0 rounded-t-
|
|
68
|
+
activeTrack: 'bottom-spacing-0 rounded-t-(--md-comp-slider-inner-corner) rounded-b-(--outer)',
|
|
69
|
+
inactiveTrack: 'top-spacing-0 rounded-t-(--outer) rounded-b-(--md-comp-slider-inner-corner)',
|
|
70
70
|
stop: 'left-1/2 -translate-x-1/2 translate-y-1/2',
|
|
71
71
|
icon: 'left-1/2 -translate-x-1/2',
|
|
72
72
|
handle: 'left-spacing-0 w-full translate-y-1/2',
|
|
@@ -54,4 +54,17 @@ export type SliderProps = Omit<SliderVariants, 'active'> & Omit<HTMLAttributes<H
|
|
|
54
54
|
* Orientation of the slider.
|
|
55
55
|
*/
|
|
56
56
|
vertical?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Accessible name for the slider. Applied to the focusable thumb (`role="slider"`), not the
|
|
59
|
+
* root, so screen readers announce it. Required unless `aria-labelledby` is set.
|
|
60
|
+
*/
|
|
61
|
+
'aria-label'?: string | null;
|
|
62
|
+
/**
|
|
63
|
+
* Id of the element that labels the slider. Applied to the thumb, like `aria-label`.
|
|
64
|
+
*/
|
|
65
|
+
'aria-labelledby'?: string | null;
|
|
66
|
+
/**
|
|
67
|
+
* Id of the element that describes the slider. Applied to the thumb.
|
|
68
|
+
*/
|
|
69
|
+
'aria-describedby'?: string | null;
|
|
57
70
|
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Meta, Title, Subtitle, Canvas, Controls } from '@storybook/addon-docs/blocks';
|
|
2
|
+
import * as TextareaStories from './Textarea.stories.svelte';
|
|
3
|
+
|
|
4
|
+
<Meta of={TextareaStories} />
|
|
5
|
+
|
|
6
|
+
<Title />
|
|
7
|
+
|
|
8
|
+
<Subtitle>A text field for longer, multi-line input.</Subtitle>
|
|
9
|
+
|
|
10
|
+
[M3 text field guidelines](https://m3.material.io/components/text-fields/guidelines) ·
|
|
11
|
+
`import { Textarea } from '@noxlovette/material';`
|
|
12
|
+
|
|
13
|
+
<Canvas of={TextareaStories.Playground} />
|
|
14
|
+
|
|
15
|
+
<Controls of={TextareaStories.Playground} />
|
|
16
|
+
|
|
17
|
+
## Not an M3 component
|
|
18
|
+
|
|
19
|
+
M3 has no textarea. It has a _multi-line text field_, and that is what this is: the
|
|
20
|
+
[`Textfield`](?path=/docs/forms-textfield--docs) with a `<textarea>` inside. Everything a text
|
|
21
|
+
field does carries over (filled and outlined containers, the floating label and notch, icons,
|
|
22
|
+
supporting text, required, error, disabled). Only these change:
|
|
23
|
+
|
|
24
|
+
| Part | Textfield | Textarea |
|
|
25
|
+
| ------------- | ------------------------- | ------------------------------------------------------- |
|
|
26
|
+
| Height | Fixed 56dp | 56dp minimum, then grows with the content |
|
|
27
|
+
| Resting label | Centered vertically | On the first line of text |
|
|
28
|
+
| Icons | Centered vertically | Beside the first line of text |
|
|
29
|
+
| Filled input | Centered, 16dp top offset | 24dp above the first line, 8dp below the last |
|
|
30
|
+
| Counter | Only with supporting text | Whenever `characterLimit` is set; over the limit errors |
|
|
31
|
+
|
|
32
|
+
Use `Textfield` for anything that fits on one line (names, emails, search). Use `Textarea` for
|
|
33
|
+
free text: messages, descriptions, notes, feedback.
|
|
34
|
+
|
|
35
|
+
<Canvas of={TextareaStories.FilledAndOutlined} />
|
|
36
|
+
|
|
37
|
+
## Height
|
|
38
|
+
|
|
39
|
+
`rows` is the minimum height in lines (default 3). The field grows as the user types, up to
|
|
40
|
+
`maxRows`, and scrolls after that. Leave `maxRows` out to grow without limit, or set it equal to
|
|
41
|
+
`rows` for a fixed-height field. There's no resize handle: the field sizes itself.
|
|
42
|
+
|
|
43
|
+
Growing uses CSS `field-sizing: content`, with a script fallback for browsers without it.
|
|
44
|
+
|
|
45
|
+
<Canvas of={TextareaStories.Growing} />
|
|
46
|
+
|
|
47
|
+
## Character limit
|
|
48
|
+
|
|
49
|
+
`characterLimit` shows a `12 / 160` counter below the field. Going over the limit puts the field
|
|
50
|
+
in its error state but doesn't block typing, so pasted text isn't silently cut. Pass the native
|
|
51
|
+
`maxlength` too if the limit is hard.
|
|
52
|
+
|
|
53
|
+
<Canvas of={TextareaStories.CharacterLimit} />
|
|
54
|
+
|
|
55
|
+
## Accessibility
|
|
56
|
+
|
|
57
|
+
- **Label.** The floating label is a real `<label for>` tied to the textarea's id.
|
|
58
|
+
- **Descriptions.** The supporting text and the counter are linked with `aria-describedby`, added
|
|
59
|
+
to any `aria-describedby` you pass.
|
|
60
|
+
- **Errors.** `error`, or going over `characterLimit`, sets `aria-invalid`. Say what's wrong in
|
|
61
|
+
`supportingText`, not just with color.
|
|
62
|
+
- **Required.** `required` sets `required` and `aria-required`, and shows an asterisk that screen
|
|
63
|
+
readers skip.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import { defineMeta } from '@storybook/addon-svelte-csf';
|
|
3
|
+
import Textarea from './Textarea.svelte';
|
|
4
|
+
|
|
5
|
+
const { Story } = defineMeta({
|
|
6
|
+
title: 'Forms/Textarea',
|
|
7
|
+
component: Textarea,
|
|
8
|
+
argTypes: {
|
|
9
|
+
label: { control: 'text' },
|
|
10
|
+
variant: { control: 'select', options: ['filled', 'outlined'] },
|
|
11
|
+
rows: { control: 'number' },
|
|
12
|
+
maxRows: { control: 'number' },
|
|
13
|
+
disabled: { control: 'boolean' },
|
|
14
|
+
error: { control: 'boolean' },
|
|
15
|
+
required: { control: 'boolean' },
|
|
16
|
+
characterLimit: { control: 'number' }
|
|
17
|
+
},
|
|
18
|
+
args: {
|
|
19
|
+
label: 'Message',
|
|
20
|
+
variant: 'outlined',
|
|
21
|
+
rows: 3,
|
|
22
|
+
disabled: false,
|
|
23
|
+
error: false,
|
|
24
|
+
required: false
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<Story name="Playground">
|
|
30
|
+
{#snippet template(args)}
|
|
31
|
+
<div class="p-spacing-300 max-w-md">
|
|
32
|
+
<Textarea {...args} />
|
|
33
|
+
</div>
|
|
34
|
+
{/snippet}
|
|
35
|
+
</Story>
|
|
36
|
+
|
|
37
|
+
<Story name="Filled and outlined" asChild>
|
|
38
|
+
<div class="gap-spacing-300 p-spacing-300 flex max-w-md flex-col">
|
|
39
|
+
<Textarea variant="filled" label="Filled" />
|
|
40
|
+
<Textarea variant="outlined" label="Outlined" />
|
|
41
|
+
<Textarea
|
|
42
|
+
variant="filled"
|
|
43
|
+
label="Filled with value"
|
|
44
|
+
value={'Line one\nLine two\nLine three\nLine four grows the field.'}
|
|
45
|
+
/>
|
|
46
|
+
<Textarea
|
|
47
|
+
variant="outlined"
|
|
48
|
+
label="Outlined with value"
|
|
49
|
+
value={'Line one\nLine two\nLine three\nLine four grows the field.'}
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
</Story>
|
|
53
|
+
|
|
54
|
+
<Story name="Growing" asChild>
|
|
55
|
+
<div class="gap-spacing-300 p-spacing-300 flex max-w-md flex-col">
|
|
56
|
+
<Textarea label="Grows without limit" rows={2} />
|
|
57
|
+
<Textarea label="Grows to 5 lines, then scrolls" rows={2} maxRows={5} />
|
|
58
|
+
<Textarea label="Fixed at 4 lines" rows={4} maxRows={4} />
|
|
59
|
+
</div>
|
|
60
|
+
</Story>
|
|
61
|
+
|
|
62
|
+
<Story name="Character Limit" asChild>
|
|
63
|
+
<div class="p-spacing-300 max-w-md">
|
|
64
|
+
<Textarea label="Bio" characterLimit={160} value="">
|
|
65
|
+
{#snippet supportingText()}
|
|
66
|
+
Tell people a little about yourself.
|
|
67
|
+
{/snippet}
|
|
68
|
+
</Textarea>
|
|
69
|
+
</div>
|
|
70
|
+
</Story>
|
|
71
|
+
|
|
72
|
+
<Story name="With Icons" asChild>
|
|
73
|
+
<div class="gap-spacing-300 p-spacing-300 flex max-w-md flex-col">
|
|
74
|
+
<Textarea
|
|
75
|
+
label="Notes"
|
|
76
|
+
leadingIconProps={{ name: 'edit_note' }}
|
|
77
|
+
trailingIconProps={{ name: 'close' }}
|
|
78
|
+
/>
|
|
79
|
+
<Textarea
|
|
80
|
+
variant="filled"
|
|
81
|
+
label="Notes"
|
|
82
|
+
leadingIconProps={{ name: 'edit_note' }}
|
|
83
|
+
trailingIconProps={{ name: 'close' }}
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
</Story>
|
|
87
|
+
|
|
88
|
+
<Story name="States" asChild>
|
|
89
|
+
<div class="gap-spacing-300 p-spacing-300 flex max-w-md flex-col">
|
|
90
|
+
<Textarea label="Required" required />
|
|
91
|
+
<Textarea label="Error" error value="Not quite right">
|
|
92
|
+
{#snippet supportingText()}
|
|
93
|
+
Describe the problem in at least 20 characters.
|
|
94
|
+
{/snippet}
|
|
95
|
+
</Textarea>
|
|
96
|
+
<Textarea label="Disabled" disabled value="Can't edit" />
|
|
97
|
+
<Textarea variant="filled" label="Disabled filled" disabled value="Can't edit" />
|
|
98
|
+
</div>
|
|
99
|
+
</Story>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import Textarea from './Textarea.svelte';
|
|
2
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
3
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
4
|
+
$$bindings?: Bindings;
|
|
5
|
+
} & Exports;
|
|
6
|
+
(internal: unknown, props: {
|
|
7
|
+
$$events?: Events;
|
|
8
|
+
$$slots?: Slots;
|
|
9
|
+
}): Exports & {
|
|
10
|
+
$set?: any;
|
|
11
|
+
$on?: any;
|
|
12
|
+
};
|
|
13
|
+
z_$$bindings?: Bindings;
|
|
14
|
+
}
|
|
15
|
+
declare const Textarea: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
16
|
+
[evt: string]: CustomEvent<any>;
|
|
17
|
+
}, {}, {}, string>;
|
|
18
|
+
type Textarea = InstanceType<typeof Textarea>;
|
|
19
|
+
export default Textarea;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
Multi-line text field.
|
|
4
|
+
|
|
5
|
+
M3 has no textarea spec, only a multi-line text field, so this is the `Textfield` with a
|
|
6
|
+
`<textarea>` inside: the same filled and outlined containers, floating label and supporting
|
|
7
|
+
text. It grows with its content from `rows` to `maxRows` lines, then scrolls.
|
|
8
|
+
|
|
9
|
+
@see https://m3.material.io/components/text-fields/guidelines
|
|
10
|
+
-->
|
|
11
|
+
<script lang="ts">
|
|
12
|
+
import clsx from 'clsx';
|
|
13
|
+
import type { Attachment } from 'svelte/attachments';
|
|
14
|
+
import { useId } from 'bits-ui';
|
|
15
|
+
import { Icon } from '../../../utils/index.js';
|
|
16
|
+
import { ButtonIcon } from '../../buttons/index.js';
|
|
17
|
+
import { textarea } from './theme.js';
|
|
18
|
+
import type { TextareaProps } from './types.js';
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
value = $bindable(),
|
|
22
|
+
supportingText,
|
|
23
|
+
leadingIconProps,
|
|
24
|
+
trailingIconProps,
|
|
25
|
+
placeholder = '',
|
|
26
|
+
label,
|
|
27
|
+
id = useId(),
|
|
28
|
+
class: className,
|
|
29
|
+
characterLimit,
|
|
30
|
+
disabled = false,
|
|
31
|
+
error = false,
|
|
32
|
+
required = false,
|
|
33
|
+
variant = 'outlined',
|
|
34
|
+
rows = 3,
|
|
35
|
+
maxRows,
|
|
36
|
+
trailingOnClick,
|
|
37
|
+
trailingIcon,
|
|
38
|
+
...restProps
|
|
39
|
+
}: TextareaProps = $props();
|
|
40
|
+
|
|
41
|
+
const length = $derived(value?.length ?? 0);
|
|
42
|
+
const overLimit = $derived(characterLimit != null && length > characterLimit);
|
|
43
|
+
const invalid = $derived(!!error || overLimit);
|
|
44
|
+
|
|
45
|
+
const cls = $derived(
|
|
46
|
+
textarea({ disabled, error: invalid, variant, leadingIcon: !!leadingIconProps })
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const supportingId = $derived(`${id}-supporting`);
|
|
50
|
+
const counterId = $derived(`${id}-counter`);
|
|
51
|
+
const describedBy = $derived(
|
|
52
|
+
clsx(
|
|
53
|
+
restProps['aria-describedby'],
|
|
54
|
+
supportingText && supportingId,
|
|
55
|
+
characterLimit != null && counterId
|
|
56
|
+
) || undefined
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const sizeStyle = $derived(
|
|
60
|
+
`min-height: ${rows}lh;` + (maxRows != null ? ` max-height: ${Math.max(rows, maxRows)}lh;` : '')
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
/* `field-sizing: content` does the growing where it's supported; elsewhere, fit the height to
|
|
64
|
+
the content on every change, including a `value` set from outside. */
|
|
65
|
+
let textareaEl = $state<HTMLTextAreaElement>();
|
|
66
|
+
const fieldSizing = typeof CSS !== 'undefined' && CSS.supports('field-sizing', 'content');
|
|
67
|
+
|
|
68
|
+
const autoGrow: Attachment<HTMLTextAreaElement> = (node) => {
|
|
69
|
+
if (fieldSizing) return;
|
|
70
|
+
void value;
|
|
71
|
+
node.style.height = 'auto';
|
|
72
|
+
node.style.height = `${node.scrollHeight}px`;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/* The wrapper's padding sits outside the textarea; a press there still focuses it. */
|
|
76
|
+
function focusTextarea(event: PointerEvent) {
|
|
77
|
+
if (event.target !== event.currentTarget || disabled) return;
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
textareaEl?.focus();
|
|
80
|
+
}
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<div class={clsx('relative w-full', className)}>
|
|
84
|
+
<div class={cls.base()}>
|
|
85
|
+
{#if leadingIconProps}
|
|
86
|
+
<Icon class={cls.leadingIcon()} {...leadingIconProps} />
|
|
87
|
+
{/if}
|
|
88
|
+
|
|
89
|
+
<div class={cls.inputWrapper()} onpointerdown={focusTextarea} role="presentation">
|
|
90
|
+
<textarea
|
|
91
|
+
bind:this={textareaEl}
|
|
92
|
+
bind:value
|
|
93
|
+
{id}
|
|
94
|
+
class={cls.input()}
|
|
95
|
+
style={sizeStyle}
|
|
96
|
+
rows={1}
|
|
97
|
+
aria-invalid={invalid}
|
|
98
|
+
aria-required={required}
|
|
99
|
+
{required}
|
|
100
|
+
{disabled}
|
|
101
|
+
{placeholder}
|
|
102
|
+
{...restProps}
|
|
103
|
+
aria-describedby={describedBy}
|
|
104
|
+
{@attach autoGrow}></textarea>
|
|
105
|
+
|
|
106
|
+
<label class={cls.label()} for={id}>
|
|
107
|
+
{label}{#if required}<span class={cls.requiredAsterisk()} aria-hidden="true">*</span>{/if}
|
|
108
|
+
</label>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{#if trailingIcon}
|
|
112
|
+
{@render trailingIcon()}
|
|
113
|
+
{:else if trailingIconProps}
|
|
114
|
+
<ButtonIcon
|
|
115
|
+
variant="standard"
|
|
116
|
+
type="button"
|
|
117
|
+
onclick={trailingOnClick}
|
|
118
|
+
class={cls.trailingIcon()}
|
|
119
|
+
iconProps={trailingIconProps}
|
|
120
|
+
></ButtonIcon>
|
|
121
|
+
{/if}
|
|
122
|
+
|
|
123
|
+
{#if variant === 'outlined'}
|
|
124
|
+
<fieldset class={cls.fieldset()} aria-hidden="true">
|
|
125
|
+
<legend class={cls.legend()}>
|
|
126
|
+
<span class={cls.legendLabel()}>{label}{required ? '*' : ''}</span>
|
|
127
|
+
</legend>
|
|
128
|
+
</fieldset>
|
|
129
|
+
{/if}
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{#if supportingText || characterLimit != null}
|
|
133
|
+
<div class={cls.supportingText()}>
|
|
134
|
+
{#if supportingText}
|
|
135
|
+
<p id={supportingId}>{@render supportingText()}</p>
|
|
136
|
+
{/if}
|
|
137
|
+
{#if characterLimit != null}
|
|
138
|
+
<p id={counterId} class={cls.counter()}>{length} / {characterLimit}</p>
|
|
139
|
+
{/if}
|
|
140
|
+
</div>
|
|
141
|
+
{/if}
|
|
142
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TextareaProps } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Multi-line text field.
|
|
4
|
+
*
|
|
5
|
+
* M3 has no textarea spec, only a multi-line text field, so this is the `Textfield` with a
|
|
6
|
+
* `<textarea>` inside: the same filled and outlined containers, floating label and supporting
|
|
7
|
+
* text. It grows with its content from `rows` to `maxRows` lines, then scrolls.
|
|
8
|
+
*
|
|
9
|
+
* @see https://m3.material.io/components/text-fields/guidelines
|
|
10
|
+
*/
|
|
11
|
+
declare const Textarea: import("svelte").Component<TextareaProps, {}, "value">;
|
|
12
|
+
type Textarea = ReturnType<typeof Textarea>;
|
|
13
|
+
export default Textarea;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// THIS FILE IS AUTO-GENERATED BY scripts/generate-components-index.ts - DO NOT EDIT MANUALLY
|
|
2
|
+
// re-export TS/JS modules from this folder
|
|
3
|
+
export * from './theme.js';
|
|
4
|
+
export * from './types.js';
|
|
5
|
+
// default exports for components in this folder
|
|
6
|
+
export { default as Textarea } from './Textarea.svelte';
|