@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.
Files changed (68) hide show
  1. package/dist/components/buttons/connected/theme.d.ts +15 -15
  2. package/dist/components/buttons/connected/theme.js +20 -5
  3. package/dist/components/buttons/split-button/theme.d.ts +15 -15
  4. package/dist/components/buttons/split-button/theme.js +21 -15
  5. package/dist/components/buttons/theme.d.ts +40 -40
  6. package/dist/components/buttons/theme.js +29 -29
  7. package/dist/components/cards/theme.js +1 -1
  8. package/dist/components/chips/Chip.svelte +1 -1
  9. package/dist/components/chips/theme.js +1 -1
  10. package/dist/components/containers/bottom-sheet/theme.js +1 -1
  11. package/dist/components/containers/carousel/theme.js +1 -1
  12. package/dist/components/containers/context-menu/theme.js +2 -2
  13. package/dist/components/containers/dialogue/theme.js +1 -1
  14. package/dist/components/containers/list/theme.d.ts +3 -3
  15. package/dist/components/containers/list/theme.js +8 -8
  16. package/dist/components/containers/menu/theme.js +3 -3
  17. package/dist/components/date/theme.js +2 -2
  18. package/dist/components/forms/checkbox/theme.js +1 -1
  19. package/dist/components/forms/command/theme.js +1 -1
  20. package/dist/components/forms/index.d.ts +1 -0
  21. package/dist/components/forms/index.js +1 -0
  22. package/dist/components/forms/search/theme.js +1 -1
  23. package/dist/components/forms/select/theme.js +2 -2
  24. package/dist/components/forms/slider/Slider.svelte +8 -0
  25. package/dist/components/forms/slider/theme.js +7 -7
  26. package/dist/components/forms/slider/types.d.ts +13 -0
  27. package/dist/components/forms/textarea/Textarea.mdx +63 -0
  28. package/dist/components/forms/textarea/Textarea.stories.svelte +99 -0
  29. package/dist/components/forms/textarea/Textarea.stories.svelte.d.ts +19 -0
  30. package/dist/components/forms/textarea/Textarea.svelte +142 -0
  31. package/dist/components/forms/textarea/Textarea.svelte.d.ts +13 -0
  32. package/dist/components/forms/textarea/index.d.ts +3 -0
  33. package/dist/components/forms/textarea/index.js +6 -0
  34. package/dist/components/forms/textarea/theme.d.ts +150 -0
  35. package/dist/components/forms/textarea/theme.js +155 -0
  36. package/dist/components/forms/textarea/types.d.ts +59 -0
  37. package/dist/components/forms/textarea/types.js +1 -0
  38. package/dist/components/forms/textfield/theme.js +4 -4
  39. package/dist/components/forms/toggle-group/theme.js +2 -2
  40. package/dist/components/misc/ThemeSettings.svelte +1 -1
  41. package/dist/components/misc/materialAvatarPalette.js +1 -1
  42. package/dist/components/nav/appbar/theme.js +22 -6
  43. package/dist/components/nav/navbar/theme.js +1 -1
  44. package/dist/components/nav/tabs/theme.js +1 -1
  45. package/dist/components/progress/CircularProgress.svelte +5 -3
  46. package/dist/components/progress/CircularProgress.svelte.d.ts +2 -1
  47. package/dist/components/progress/LinearProgress.svelte +5 -2
  48. package/dist/components/progress/LinearProgress.svelte.d.ts +2 -1
  49. package/dist/components/progress/WavyLinearProgress.svelte +5 -3
  50. package/dist/components/progress/WavyLinearProgress.svelte.d.ts +2 -1
  51. package/dist/components/progress/index.d.ts +1 -0
  52. package/dist/components/progress/index.js +1 -0
  53. package/dist/components/progress/types.d.ts +16 -0
  54. package/dist/components/progress/types.js +1 -0
  55. package/dist/components/snackbar/theme.js +1 -1
  56. package/dist/components/table/TableHeader.svelte +1 -1
  57. package/dist/components/toolbar/theme.js +2 -2
  58. package/dist/styles/components.css +7 -2
  59. package/dist/styles/icon.css +19 -0
  60. package/dist/styles/rounding.css +4 -0
  61. package/dist/utils/Theme.svelte +1 -1
  62. package/dist/utils/ThemeScript.svelte +1 -1
  63. package/dist/utils/icon/Icon.svelte +5 -5
  64. package/dist/utils/index.d.ts +2 -2
  65. package/dist/utils/index.js +1 -1
  66. package/package.json +1 -1
  67. /package/dist/utils/{theme.svelte.d.ts → themeState.svelte.d.ts} +0 -0
  68. /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-[28px] text-start',
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-spacing-250 text-[20px] text-md-sys-color-on-surface-variant shrink-0',
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-spacing-250 text-[20px] shrink-0 text-md-sys-color-on-surface-variant'
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-[28px] p-spacing-300 gap-spacing-200',
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:1rem] [&>:last-child]:[--li-bottom:1rem]";
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:1rem] [&>:last-child]:[--li-bottom:1rem]";
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:1rem] [&>:last-child]:[--li-bottom:1rem]";
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:1rem] [&>:last-child]:[--li-bottom:1rem]'
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:0.25rem] rounded-t-[var(--li-top,var(--li-shape))] rounded-b-[var(--li-bottom,var(--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:0.75rem]',
73
- 'focus-visible:[--li-shape:1rem]',
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:1rem]'
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:1rem] hover:[--li-shape:1rem]',
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:1rem] [--li-bottom:var(--li-shape)]',
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:1rem]'
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-spacing-250 text-[20px] shrink-0',
20
- indicator: 'size-spacing-250 text-[20px] shrink-0 text-md-sys-color-on-surface-variant',
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-spacing-250 text-[20px] shrink-0',
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-[24px]
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-[24px]
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-[4px] border-2 border-current bg-md-sys-color-surface transition-colors md-sys-motion-fast-effects',
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:0.75rem] data-selected:bg-md-sys-color-on-surface/10'
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:1rem]'
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-[24px]
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-[24px]
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-spacing-300 text-[24px]'
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-spacing-300 text-[24px]'
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-spacing-400 text-[32px]'
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-[var(--outer)] rounded-r-[2px]',
59
- inactiveTrack: 'right-spacing-0 rounded-l-[2px] rounded-r-[var(--outer)]',
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-[2px] rounded-b-[var(--outer)]',
69
- inactiveTrack: 'top-spacing-0 rounded-t-[var(--outer)] rounded-b-[2px]',
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,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';