@noxlovette/material 0.9.4 → 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.
Files changed (34) hide show
  1. package/dist/components/forms/index.d.ts +1 -0
  2. package/dist/components/forms/index.js +1 -0
  3. package/dist/components/forms/slider/Slider.svelte +8 -0
  4. package/dist/components/forms/slider/types.d.ts +13 -0
  5. package/dist/components/forms/textarea/Textarea.mdx +63 -0
  6. package/dist/components/forms/textarea/Textarea.stories.svelte +99 -0
  7. package/dist/components/forms/textarea/Textarea.stories.svelte.d.ts +19 -0
  8. package/dist/components/forms/textarea/Textarea.svelte +142 -0
  9. package/dist/components/forms/textarea/Textarea.svelte.d.ts +13 -0
  10. package/dist/components/forms/textarea/index.d.ts +3 -0
  11. package/dist/components/forms/textarea/index.js +6 -0
  12. package/dist/components/forms/textarea/theme.d.ts +150 -0
  13. package/dist/components/forms/textarea/theme.js +155 -0
  14. package/dist/components/forms/textarea/types.d.ts +59 -0
  15. package/dist/components/forms/textarea/types.js +1 -0
  16. package/dist/components/misc/ThemeSettings.svelte +1 -1
  17. package/dist/components/misc/materialAvatarPalette.js +1 -1
  18. package/dist/components/progress/CircularProgress.svelte +5 -3
  19. package/dist/components/progress/CircularProgress.svelte.d.ts +2 -1
  20. package/dist/components/progress/LinearProgress.svelte +5 -2
  21. package/dist/components/progress/LinearProgress.svelte.d.ts +2 -1
  22. package/dist/components/progress/WavyLinearProgress.svelte +5 -3
  23. package/dist/components/progress/WavyLinearProgress.svelte.d.ts +2 -1
  24. package/dist/components/progress/index.d.ts +1 -0
  25. package/dist/components/progress/index.js +1 -0
  26. package/dist/components/progress/types.d.ts +16 -0
  27. package/dist/components/progress/types.js +1 -0
  28. package/dist/utils/Theme.svelte +1 -1
  29. package/dist/utils/ThemeScript.svelte +1 -1
  30. package/dist/utils/index.d.ts +2 -2
  31. package/dist/utils/index.js +1 -1
  32. package/package.json +1 -1
  33. /package/dist/utils/{theme.svelte.d.ts → themeState.svelte.d.ts} +0 -0
  34. /package/dist/utils/{theme.svelte.js → themeState.svelte.js} +0 -0
@@ -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';
@@ -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"
@@ -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,3 @@
1
+ export * from './theme.js';
2
+ export * from './types.js';
3
+ export { default as Textarea } from './Textarea.svelte';
@@ -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';
@@ -0,0 +1,150 @@
1
+ import { type VariantProps } from '../../../utils/tv.js';
2
+ export type TextareaVariants = VariantProps<typeof textarea>;
3
+ /**
4
+ * M3 defines a multi-line text field but no separate textarea spec. This keeps the text field's
5
+ * containers, label, notch and indicator, and changes only what a growing field has to:
6
+ * the container has a 56dp minimum instead of a fixed height, and the label and icons line up
7
+ * with the first line instead of the vertical center.
8
+ */
9
+ export declare const textarea: import("tailwind-variants").TVReturnType<{
10
+ variant: {
11
+ filled: {
12
+ base: string;
13
+ inputWrapper: string;
14
+ leadingIcon: string;
15
+ trailingIcon: string;
16
+ label: string;
17
+ };
18
+ outlined: {
19
+ inputWrapper: string;
20
+ label: string;
21
+ };
22
+ };
23
+ error: {
24
+ true: {
25
+ base: string;
26
+ label: string;
27
+ supportingText: string;
28
+ leadingIcon: string;
29
+ trailingIcon: string;
30
+ fieldset: string;
31
+ };
32
+ };
33
+ leadingIcon: {
34
+ true: {};
35
+ };
36
+ disabled: {
37
+ true: {
38
+ base: string;
39
+ inputWrapper: string;
40
+ fieldset: string;
41
+ };
42
+ };
43
+ }, {
44
+ base: string;
45
+ inputWrapper: string;
46
+ input: string;
47
+ label: string;
48
+ requiredAsterisk: string;
49
+ leadingIcon: string;
50
+ trailingIcon: string;
51
+ supportingText: string;
52
+ counter: string;
53
+ fieldset: string;
54
+ legend: string;
55
+ legendLabel: string;
56
+ }, undefined, {
57
+ variant: {
58
+ filled: {
59
+ base: string;
60
+ inputWrapper: string;
61
+ leadingIcon: string;
62
+ trailingIcon: string;
63
+ label: string;
64
+ };
65
+ outlined: {
66
+ inputWrapper: string;
67
+ label: string;
68
+ };
69
+ };
70
+ error: {
71
+ true: {
72
+ base: string;
73
+ label: string;
74
+ supportingText: string;
75
+ leadingIcon: string;
76
+ trailingIcon: string;
77
+ fieldset: string;
78
+ };
79
+ };
80
+ leadingIcon: {
81
+ true: {};
82
+ };
83
+ disabled: {
84
+ true: {
85
+ base: string;
86
+ inputWrapper: string;
87
+ fieldset: string;
88
+ };
89
+ };
90
+ }, {
91
+ base: string;
92
+ inputWrapper: string;
93
+ input: string;
94
+ label: string;
95
+ requiredAsterisk: string;
96
+ leadingIcon: string;
97
+ trailingIcon: string;
98
+ supportingText: string;
99
+ counter: string;
100
+ fieldset: string;
101
+ legend: string;
102
+ legendLabel: string;
103
+ }, import("tailwind-variants").TVReturnTypeLike<{
104
+ variant: {
105
+ filled: {
106
+ base: string;
107
+ inputWrapper: string;
108
+ leadingIcon: string;
109
+ trailingIcon: string;
110
+ label: string;
111
+ };
112
+ outlined: {
113
+ inputWrapper: string;
114
+ label: string;
115
+ };
116
+ };
117
+ error: {
118
+ true: {
119
+ base: string;
120
+ label: string;
121
+ supportingText: string;
122
+ leadingIcon: string;
123
+ trailingIcon: string;
124
+ fieldset: string;
125
+ };
126
+ };
127
+ leadingIcon: {
128
+ true: {};
129
+ };
130
+ disabled: {
131
+ true: {
132
+ base: string;
133
+ inputWrapper: string;
134
+ fieldset: string;
135
+ };
136
+ };
137
+ }, {
138
+ base: string;
139
+ inputWrapper: string;
140
+ input: string;
141
+ label: string;
142
+ requiredAsterisk: string;
143
+ leadingIcon: string;
144
+ trailingIcon: string;
145
+ supportingText: string;
146
+ counter: string;
147
+ fieldset: string;
148
+ legend: string;
149
+ legendLabel: string;
150
+ }>>;
@@ -0,0 +1,155 @@
1
+ import { tv } from '../../../utils/tv.js';
2
+ /**
3
+ * M3 defines a multi-line text field but no separate textarea spec. This keeps the text field's
4
+ * containers, label, notch and indicator, and changes only what a growing field has to:
5
+ * the container has a 56dp minimum instead of a fixed height, and the label and icons line up
6
+ * with the first line instead of the vertical center.
7
+ */
8
+ export const textarea = tv({
9
+ slots: {
10
+ base: `
11
+ group relative w-full min-h-spacing-700 flex items-start
12
+ `,
13
+ /* The wrapper holds the vertical padding, not the textarea, so text scrolled past `maxRows`
14
+ is clipped at the textarea's edge instead of sliding under the floated label. */
15
+ inputWrapper: `
16
+ relative flex-1 self-stretch px-spacing-200 cursor-text
17
+ `,
18
+ input: `
19
+ peer block w-full bg-transparent outline-none resize-none field-sizing-content
20
+ md-sys-typescale-body-large text-md-sys-color-on-surface
21
+ disabled:text-md-sys-color-on-surface/38 disabled:cursor-not-allowed
22
+ `,
23
+ label: `
24
+ absolute left-spacing-200 top-spacing-200
25
+ md-sys-typescale-body-large
26
+ text-md-sys-color-on-surface-variant
27
+ pointer-events-none
28
+ transition-[top,left,font-size,line-height,color,transform] md-sys-motion-fast-spatial
29
+
30
+ peer-focus:text-md-sys-color-primary
31
+ `,
32
+ requiredAsterisk: `
33
+ ml-spacing-25
34
+ text-md-sys-color-error
35
+ group-focus-within:text-md-sys-color-primary
36
+ transition-colors md-sys-motion-effects
37
+ `,
38
+ /* On the first line of text: 16dp from the top when outlined, 24dp when filled. */
39
+ leadingIcon: `
40
+ text-md-sys-color-on-surface-variant size-(--text-icon-24)
41
+ group-focus-within:text-md-sys-color-primary
42
+ ml-spacing-150 mt-spacing-200 text-icon-24
43
+ `,
44
+ /* Sized to the 24dp glyph like Textfield's, so it takes the same offset as the leading icon. */
45
+ trailingIcon: `
46
+ mr-spacing-150 mt-spacing-200 text-md-sys-color-on-surface-variant size-(--text-icon-24)
47
+ group-focus-within:text-md-sys-color-primary
48
+ z-20 text-icon-24
49
+ `,
50
+ supportingText: `
51
+ px-spacing-200 pt-spacing-50 flex justify-between gap-spacing-200
52
+ md-sys-typescale-body-small
53
+ text-md-sys-color-on-surface-variant
54
+ `,
55
+ counter: `
56
+ ms-auto shrink-0
57
+ `,
58
+ fieldset: `
59
+ absolute -top-spacing-100 left-spacing-0 right-spacing-0 bottom-spacing-0 m-spacing-0 px-spacing-150
60
+ pointer-events-none rounded-xs border border-md-sys-color-outline
61
+ transition-colors md-sys-motion-fast-effects
62
+
63
+ group-hover:border-md-sys-color-on-surface
64
+ group-focus-within:border-2 group-focus-within:border-md-sys-color-primary
65
+ `,
66
+ legend: `
67
+ invisible float-none block h-auto max-w-[0.01px] overflow-hidden
68
+ whitespace-nowrap md-sys-typescale-body-small
69
+ transition-[max-width] md-sys-motion-fast-spatial
70
+
71
+ group-focus-within:max-w-full
72
+ group-has-[textarea:not(:placeholder-shown)]:max-w-full
73
+ `,
74
+ legendLabel: `
75
+ px-spacing-50
76
+ `
77
+ },
78
+ variants: {
79
+ variant: {
80
+ filled: {
81
+ base: `
82
+ px-spacing-0 bg-md-sys-color-surface-container-highest
83
+ rounded-t-xs state-layer before:rounded-xs hover:before:bg-md-sys-color-on-surface/8
84
+ after:absolute after:bottom-spacing-0 after:left-spacing-0 after:right-spacing-0 after:h-px after:bg-md-sys-color-on-surface-variant
85
+ after:transition-[height,background-color] after:md-sys-motion-fast-spatial
86
+ hover:after:bg-md-sys-color-on-surface
87
+ focus-within:after:bg-md-sys-color-primary
88
+ focus-within:after:h-[2px]
89
+ `,
90
+ /* 24dp above the first line leaves room for the label floated inside the container;
91
+ 24 + 24 + 8 = the 56dp single-line field. The resting label and the icons sit on that
92
+ first line too, so the icons stay beside the text once the label floats to 8dp. */
93
+ inputWrapper: 'pt-spacing-300 pb-spacing-100',
94
+ leadingIcon: 'mt-spacing-300',
95
+ trailingIcon: 'mt-spacing-300',
96
+ label: `
97
+ top-spacing-300
98
+ peer-focus:top-spacing-100 peer-focus:md-sys-typescale-body-small
99
+ peer-not-placeholder-shown:top-spacing-100 peer-not-placeholder-shown:md-sys-typescale-body-small
100
+ `
101
+ },
102
+ outlined: {
103
+ inputWrapper: 'py-spacing-200',
104
+ label: `
105
+ peer-focus:top-spacing-0 peer-focus:md-sys-typescale-body-small peer-focus:-translate-y-1/2
106
+ peer-not-placeholder-shown:top-spacing-0 peer-not-placeholder-shown:md-sys-typescale-body-small peer-not-placeholder-shown:-translate-y-1/2
107
+ `
108
+ }
109
+ },
110
+ error: {
111
+ true: {
112
+ base: 'after:bg-md-sys-color-error focus-within:after:bg-md-sys-color-error',
113
+ label: 'text-md-sys-color-error peer-focus:text-md-sys-color-error',
114
+ supportingText: 'text-md-sys-color-error',
115
+ leadingIcon: 'text-md-sys-color-error',
116
+ trailingIcon: 'text-md-sys-color-error',
117
+ fieldset: `
118
+ border-md-sys-color-error
119
+ group-hover:border-md-sys-color-error
120
+ group-focus-within:border-md-sys-color-error
121
+ `
122
+ }
123
+ },
124
+ leadingIcon: {
125
+ true: {}
126
+ },
127
+ disabled: {
128
+ true: {
129
+ base: 'cursor-not-allowed opacity-60',
130
+ inputWrapper: 'cursor-not-allowed',
131
+ fieldset: 'border-md-sys-color-on-surface/12 group-hover:border-md-sys-color-on-surface/12'
132
+ }
133
+ }
134
+ },
135
+ compoundVariants: [
136
+ {
137
+ /* Same as Textfield: the outlined label floats back over the leading icon into the notch. */
138
+ variant: 'outlined',
139
+ leadingIcon: true,
140
+ class: {
141
+ label: 'peer-focus:-left-spacing-250 peer-not-placeholder-shown:-left-spacing-250'
142
+ }
143
+ },
144
+ {
145
+ variant: 'filled',
146
+ disabled: true,
147
+ class: {
148
+ base: 'bg-md-sys-color-on-surface/4 after:bg-md-sys-color-on-surface/12'
149
+ }
150
+ }
151
+ ],
152
+ defaultVariants: {
153
+ variant: 'outlined'
154
+ }
155
+ });
@@ -0,0 +1,59 @@
1
+ import type { IconProps } from '../../../utils/index.js';
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLTextareaAttributes } from 'svelte/elements';
4
+ import type { TextareaVariants } from './theme.js';
5
+ /**
6
+ * Props for the Textarea component.
7
+ */
8
+ export type TextareaProps = TextareaVariants & Omit<HTMLTextareaAttributes, 'children' | 'value' | 'rows'> & {
9
+ /**
10
+ * The current value of the textarea.
11
+ */
12
+ value?: string | null;
13
+ /**
14
+ * The label text for the textarea.
15
+ */
16
+ label: string;
17
+ /**
18
+ * The ID for the textarea element. Auto-generated but can be replaced.
19
+ */
20
+ id?: string;
21
+ /**
22
+ * The placeholder text. Not used in M3: the label takes its place.
23
+ */
24
+ placeholder?: '';
25
+ /**
26
+ * Minimum height, in lines. The field grows with its content from here. Defaults to 3.
27
+ */
28
+ rows?: number;
29
+ /**
30
+ * Maximum height, in lines, before the content scrolls. Omit to grow without limit; set it
31
+ * to `rows` for a fixed-height field.
32
+ */
33
+ maxRows?: number;
34
+ /**
35
+ * Snippet for supporting text (helper or error text) displayed below the field.
36
+ */
37
+ supportingText?: Snippet;
38
+ /**
39
+ * Props for the leading icon, aligned with the first line.
40
+ */
41
+ leadingIconProps?: IconProps;
42
+ /**
43
+ * Props for the trailing icon button, aligned with the first line.
44
+ */
45
+ trailingIconProps?: IconProps;
46
+ /**
47
+ * Shows a character counter below the field. Going over it shows the error state; it does
48
+ * not stop typing (pass `maxlength` for a hard limit).
49
+ */
50
+ characterLimit?: number;
51
+ /**
52
+ * Callback function when the trailing icon is clicked.
53
+ */
54
+ trailingOnClick?: () => void;
55
+ /**
56
+ * Optional snippet for trailing icon override.
57
+ */
58
+ trailingIcon?: Snippet;
59
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { RadioGroup } from '../forms/radio-group/index.js';
3
- import { themeState, extractColorFromImage } from '../../utils/theme.svelte.js';
3
+ import { themeState, extractColorFromImage } from '../../utils/themeState.svelte.js';
4
4
  import { Title } from '../typography/title/index.js';
5
5
  import { Body } from '../typography/body/index.js';
6
6
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Container-tier M3 color roles (see `styles/theme/*.css`) used as a generated avatar's
3
3
  * background. Deliberately NOT a fixed hex list: every consuming app picks its own source
4
- * color (`ThemeConfig.sourceColor` in `theme.svelte.ts`), which the M3 dynamic color
4
+ * color (`ThemeConfig.sourceColor` in `themeState.svelte.ts`), which the M3 dynamic color
5
5
  * algorithm expands into these tokens as real `--color-md-sys-color-*` custom properties on
6
6
  * `:root` — reading them live is what makes the avatar match that app's actual palette
7
7
  * instead of this library's own hardcoded pastel.
@@ -14,12 +14,14 @@ travels around the circle.
14
14
  import { animate, type AnimationPlaybackControls } from 'motion';
15
15
  import { springTokens, springTransition } from '../../animation/spring.js';
16
16
  import { SpringValue } from '../../animation/springValue.svelte.js';
17
+ import type { ProgressA11yProps } from './types.js';
17
18
 
18
19
  let {
19
20
  percent,
20
21
  size = 48,
21
- thickness = 4
22
- }: {
22
+ thickness = 4,
23
+ ...a11y
24
+ }: ProgressA11yProps & {
23
25
  /** The current progress percentage (0–100). Omit or pass null for indeterminate. */
24
26
  percent?: number | null;
25
27
  /** Diameter of the indicator in pixels. Defaults to 48. */
@@ -93,7 +95,7 @@ travels around the circle.
93
95
  );
94
96
  </script>
95
97
 
96
- <Progress.Root value={percent ?? null} max={100} data-cy="m3-circular-progress">
98
+ <Progress.Root {...a11y} value={percent ?? null} max={100} data-cy="m3-circular-progress">
97
99
  {#snippet child({ props })}
98
100
  <svg
99
101
  {...props}
@@ -1,4 +1,5 @@
1
- type $$ComponentProps = {
1
+ import type { ProgressA11yProps } from './types.js';
2
+ type $$ComponentProps = ProgressA11yProps & {
2
3
  /** The current progress percentage (0–100). Omit or pass null for indeterminate. */
3
4
  percent?: number | null;
4
5
  /** Diameter of the indicator in pixels. Defaults to 48. */
@@ -16,12 +16,14 @@ The track always keeps a gap either side of the bar.
16
16
  import { animate, type AnimationPlaybackControls } from 'motion';
17
17
  import { springTokens, springTransition } from '../../animation/spring.js';
18
18
  import { SpringValue } from '../../animation/springValue.svelte.js';
19
+ import type { ProgressA11yProps } from './types.js';
19
20
 
20
21
  let {
21
22
  percent,
22
23
  height = 4,
23
- class: className
24
- }: {
24
+ class: className,
25
+ ...a11y
26
+ }: ProgressA11yProps & {
25
27
  /** The current progress percentage (0–100). Omit or pass null for indeterminate. */
26
28
  percent?: number | null;
27
29
  /** Height of the bar in pixels. Defaults to 4. */
@@ -84,6 +86,7 @@ The track always keeps a gap either side of the bar.
84
86
  </script>
85
87
 
86
88
  <Progress.Root
89
+ {...a11y}
87
90
  value={percent ?? null}
88
91
  max={100}
89
92
  class={container({ class: clsx(className) })}
@@ -1,4 +1,5 @@
1
- type $$ComponentProps = {
1
+ import type { ProgressA11yProps } from './types.js';
2
+ type $$ComponentProps = ProgressA11yProps & {
2
3
  /** The current progress percentage (0–100). Omit or pass null for indeterminate. */
3
4
  percent?: number | null;
4
5
  /** Height of the bar in pixels. Defaults to 4. */
@@ -13,14 +13,16 @@ effects spring (critically damped, so progress never visibly runs backwards).
13
13
  import { springTokens } from '../../animation/spring.js';
14
14
  import { SpringValue } from '../../animation/springValue.svelte.js';
15
15
  import { linear, trackOpacity, WAVE_PERIOD_MS } from './_wavy.js';
16
+ import type { ProgressA11yProps } from './types.js';
16
17
 
17
18
  let {
18
19
  width = 600,
19
20
  height = 10,
20
21
  thickness = 4,
21
22
  percent,
22
- class: className
23
- }: {
23
+ class: className,
24
+ ...a11y
25
+ }: ProgressA11yProps & {
24
26
  /** Width of the SVG viewBox. */
25
27
  width?: number;
26
28
  /** Height of the SVG viewBox. */
@@ -56,7 +58,7 @@ effects spring (critically damped, so progress never visibly runs backwards).
56
58
  let wavePath = $derived(linear(height / 2 - thickness / 2, height / 2, left, percentX, time));
57
59
  </script>
58
60
 
59
- <Progress.Root value={percent ?? null} max={100}>
61
+ <Progress.Root {...a11y} value={percent ?? null} max={100}>
60
62
  {#snippet child({ props })}
61
63
  <svg {...props} viewBox="0 0 {width} {height}" class={className}>
62
64
  <path
@@ -1,4 +1,5 @@
1
- type $$ComponentProps = {
1
+ import type { ProgressA11yProps } from './types.js';
2
+ type $$ComponentProps = ProgressA11yProps & {
2
3
  /** Width of the SVG viewBox. */
3
4
  width?: number;
4
5
  /** Height of the SVG viewBox. */
@@ -1,4 +1,5 @@
1
1
  export * from './_wavy.js';
2
+ export * from './types.js';
2
3
  export { default as CircularProgress } from './CircularProgress.svelte';
3
4
  export { default as LinearProgress } from './LinearProgress.svelte';
4
5
  export { default as WavyLinearProgress } from './WavyLinearProgress.svelte';
@@ -1,6 +1,7 @@
1
1
  // THIS FILE IS AUTO-GENERATED BY scripts/generate-components-index.ts - DO NOT EDIT MANUALLY
2
2
  // re-export TS/JS modules from this folder
3
3
  export * from './_wavy.js';
4
+ export * from './types.js';
4
5
  // default exports for components in this folder
5
6
  export { default as CircularProgress } from './CircularProgress.svelte';
6
7
  export { default as LinearProgress } from './LinearProgress.svelte';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Accessible-name attributes shared by every progress indicator, forwarded to the element with
3
+ * `role="progressbar"`. Give each indicator a name when more than one is on screen, or when the
4
+ * visible label is not already associated with it.
5
+ */
6
+ export type ProgressA11yProps = {
7
+ id?: string;
8
+ /** Accessible name, e.g. `"Reading comprehension"`. */
9
+ 'aria-label'?: string;
10
+ /** Id of the element whose text names the indicator. */
11
+ 'aria-labelledby'?: string;
12
+ /** Id of the element that describes the indicator. */
13
+ 'aria-describedby'?: string;
14
+ /** Human-readable value, e.g. `"3 of 5 lessons"`. Defaults to the percentage. */
15
+ 'aria-valuetext'?: string;
16
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { MediaQuery } from 'svelte/reactivity';
3
- import { generateThemeCSS, themeState, STORAGE_KEY } from './theme.svelte.js';
3
+ import { generateThemeCSS, themeState, STORAGE_KEY } from './themeState.svelte.js';
4
4
 
5
5
  /**
6
6
  * Whether to generate and inject the M3 dynamic-color CSS custom properties
@@ -9,7 +9,7 @@ Add this to your root +layout.svelte (or any layout that wraps the whole app):
9
9
  <App>...</App>
10
10
  -->
11
11
  <script lang="ts">
12
- import { THEME_INIT_SCRIPT } from './theme.svelte.js';
12
+ import { THEME_INIT_SCRIPT } from './themeState.svelte.js';
13
13
  </script>
14
14
 
15
15
  <svelte:head>
@@ -1,9 +1,9 @@
1
1
  export * from './icon/index.js';
2
2
  export * from './shortcut.js';
3
- export * from './theme.svelte.js';
3
+ export * from './themeState.svelte.js';
4
4
  export * from './types.js';
5
5
  export * from './ui.js';
6
- export type { ColorScheme, ContrastMode, ThemeConfig, ThemeVariant } from './theme.svelte.js';
6
+ export type { ColorScheme, ContrastMode, ThemeConfig, ThemeVariant } from './themeState.svelte.js';
7
7
  export { default as Layer } from './Layer.svelte';
8
8
  export { default as Theme } from './Theme.svelte';
9
9
  export { default as ThemeScript } from './ThemeScript.svelte';
@@ -1,6 +1,6 @@
1
1
  export * from './icon/index.js';
2
2
  export * from './shortcut.js';
3
- export * from './theme.svelte.js';
3
+ export * from './themeState.svelte.js';
4
4
  export * from './types.js';
5
5
  export * from './ui.js';
6
6
  export { default as Layer } from './Layer.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noxlovette/material",
3
- "version": "0.9.4",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "description": "A Material Design 3 component library for Svelte, built on Bits UI.",
6
6
  "keywords": [