react-material-expressive 1.0.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 (66) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/LICENSE +21 -0
  3. package/README.md +286 -0
  4. package/dist/index.cjs +7014 -0
  5. package/dist/index.cjs.map +1 -0
  6. package/dist/index.d.cts +2068 -0
  7. package/dist/index.d.ts +2068 -0
  8. package/dist/index.js +6941 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/styles.css +2 -0
  11. package/dist/theme.css +187 -0
  12. package/docs/components/Amount.md +48 -0
  13. package/docs/components/Avatar.md +69 -0
  14. package/docs/components/AvatarStack.md +50 -0
  15. package/docs/components/Badge.md +50 -0
  16. package/docs/components/Blob.md +44 -0
  17. package/docs/components/Button.md +79 -0
  18. package/docs/components/ButtonGroup.md +46 -0
  19. package/docs/components/ButtonGroupConnected.md +62 -0
  20. package/docs/components/Card.md +52 -0
  21. package/docs/components/Checkbox.md +45 -0
  22. package/docs/components/Chips.md +77 -0
  23. package/docs/components/DatePicker.md +112 -0
  24. package/docs/components/Dialog.md +83 -0
  25. package/docs/components/Divider.md +48 -0
  26. package/docs/components/Dropdown.md +79 -0
  27. package/docs/components/FAB.md +63 -0
  28. package/docs/components/FABMenu.md +76 -0
  29. package/docs/components/Gallery.md +35 -0
  30. package/docs/components/Icon.md +36 -0
  31. package/docs/components/IconButton.md +69 -0
  32. package/docs/components/Img.md +52 -0
  33. package/docs/components/Layers.md +43 -0
  34. package/docs/components/Link.md +43 -0
  35. package/docs/components/List.md +87 -0
  36. package/docs/components/Loading.md +67 -0
  37. package/docs/components/LoadingIndicator.md +64 -0
  38. package/docs/components/MaterialSymbol.md +48 -0
  39. package/docs/components/MediaFrame.md +46 -0
  40. package/docs/components/Menu.md +149 -0
  41. package/docs/components/NavigationBar.md +78 -0
  42. package/docs/components/NavigationRail.md +105 -0
  43. package/docs/components/OverflowMenu.md +65 -0
  44. package/docs/components/Perspective.md +45 -0
  45. package/docs/components/Progress.md +83 -0
  46. package/docs/components/Radio.md +39 -0
  47. package/docs/components/Search.md +100 -0
  48. package/docs/components/Select.md +76 -0
  49. package/docs/components/Sheets.md +62 -0
  50. package/docs/components/Slider.md +89 -0
  51. package/docs/components/Snackbar.md +73 -0
  52. package/docs/components/SplitButton.md +75 -0
  53. package/docs/components/Stories.md +71 -0
  54. package/docs/components/Switch.md +40 -0
  55. package/docs/components/Table.md +67 -0
  56. package/docs/components/Tabs.md +67 -0
  57. package/docs/components/TextElement.md +37 -0
  58. package/docs/components/TextField.md +70 -0
  59. package/docs/components/TimePicker.md +83 -0
  60. package/docs/components/ToggleTheme.md +71 -0
  61. package/docs/components/Toolbar.md +102 -0
  62. package/docs/components/Tooltip.md +63 -0
  63. package/docs/components/TopAppBar.md +84 -0
  64. package/docs/components/Video.md +35 -0
  65. package/llms.txt +90 -0
  66. package/package.json +101 -0
@@ -0,0 +1,83 @@
1
+ # Progress / Circle
2
+
3
+ M3 Expressive progress indicators. `Progress` is linear (primary
4
+ indicator over a secondary-container track with a 4px gap and a stop
5
+ indicator; optional `wavy` sine variant); `Circle` is circular (4px
6
+ stroke, 4px track gap, optional center content). Both support determinate
7
+ (`value` 0–100) and `indeterminate` modes with proper `progressbar`
8
+ semantics; the linear indeterminate is the @material/web two-bar (a primary +
9
+ secondary bar translate/scale across; `wavy` runs the same choreography with
10
+ each segment drawn as a traveling sine), and the circular indeterminate is an
11
+ SVG arc with round caps that grows ~10°↔270° while it spins (`wavy` ripples
12
+ that spinning arc).
13
+
14
+ ## Import
15
+
16
+ ```tsx
17
+ import {Circle, Progress} from "react-material-expressive";
18
+ ```
19
+
20
+ ## API
21
+
22
+ ```ts
23
+ interface ProgressProps {
24
+ className?: string;
25
+ indeterminate?: boolean;
26
+ labels?: ProgressLabels; // accessible names
27
+ value?: number; // 0–100
28
+ wavy?: boolean; // M3 Expressive sine active indicator (indeterminate: two sine segments sweep across)
29
+ }
30
+
31
+ interface ProgressLabels {
32
+ label?: string; // aria-label (default "Progress")
33
+ }
34
+
35
+ interface CircleProps {
36
+ children?: ReactNode; // center content (e.g. "60%")
37
+ className?: string;
38
+ indeterminate?: boolean;
39
+ labels?: CircleLabels; // accessible names
40
+ size?: number; // px, defaults: 40 flat / 48 wavy (M3E baselines)
41
+ strokeWidth?: number; // px, default 4 (the spec's configurable thickness)
42
+ value?: number; // 0–100
43
+ wavy?: boolean; // rippled active arc — determinate (flat outside 10-95% like Compose) and the indeterminate spinner
44
+ }
45
+
46
+ interface CircleLabels {
47
+ label?: string; // aria-label (default "Progress")
48
+ }
49
+ ```
50
+
51
+ ## Examples
52
+
53
+ ```tsx
54
+ <Progress value={60} />
55
+ <Progress value={60} wavy />
56
+ <Progress indeterminate />
57
+ <Circle value={60}>60%</Circle>
58
+ <Circle value={60} wavy />
59
+ <Circle indeterminate size={32} />
60
+ ```
61
+
62
+ ## Gotchas
63
+
64
+ - Determinate values transition like @material/web (linear 250ms,
65
+ circular stroke 500ms).
66
+ - The linear indeterminate runs @material/web's two-bar choreography (a
67
+ primary + secondary bar translate/scale, 2s). The flat variant draws solid
68
+ bars; `wavy` reuses that exact timing — two primary segments sweep sideways
69
+ (their head/tail `clip-path` is derived from the flat translate/scale) and
70
+ each shows the same traveling sine as the determinate active indicator
71
+ (wavelength 40), 10px tall (the flat bar is 4px). The circular indeterminate
72
+ is an SVG arc with round caps that grows ~10°↔270° while it spins (flat
73
+ reuses `_Spinner`); `wavy` keeps that spin + grow/shrink dash but draws a
74
+ rippled ring (amplitude 1.6, wavelength 15) whose wave travels.
75
+ - The flat `Circle indeterminate` and [`Loading`](Loading.md) render the
76
+ **same** spinner (`_Spinner`) — pick by intent: `Circle` is the **progress
77
+ indicator**
78
+ (`role="progressbar"`, fixed `primary`, can hold a center label);
79
+ `Loading` is the inline **status** spinner (`role="status"`, recolorable
80
+ via `currentColor`, lives in `elements`) for a busy indicator inside a
81
+ button or text.
82
+ - **BREAKING:** the `aria-label` prop was replaced by `labels={{label}}` on
83
+ `Progress`, `Circle` and [`LoadingIndicator`](LoadingIndicator.md).
@@ -0,0 +1,39 @@
1
+ # Radio
2
+
3
+ M3 radio button: custom-rendered 20px ring + 10px dot with a 40px circular
4
+ state layer. The native input is the source of truth, so uncontrolled
5
+ groups (shared `name`) keep native semantics; pass `checked` + `onChange`
6
+ for controlled usage.
7
+
8
+ ## Import
9
+
10
+ ```tsx
11
+ import {Radio} from "react-material-expressive";
12
+ ```
13
+
14
+ ## API
15
+
16
+ ```ts
17
+ interface RadioProps extends Omit<
18
+ ComponentProps<"input">,
19
+ "type" | "size" | "children"
20
+ > {
21
+ className?: string; // wrapping <label>
22
+ label?: ReactNode;
23
+ }
24
+ ```
25
+
26
+ ## Example
27
+
28
+ ```tsx
29
+ <div role="radiogroup" aria-label="Size">
30
+ <Radio defaultChecked label="Small" name="size" value="s" />
31
+ <Radio label="Medium" name="size" value="m" />
32
+ <Radio disabled label="Large" name="size" value="l" />
33
+ </div>
34
+ ```
35
+
36
+ ## Gotchas
37
+
38
+ - Group radios with the same `name` inside a `role="radiogroup"` container.
39
+ - Disabled follows M3 (38% ring/dot/label).
@@ -0,0 +1,100 @@
1
+ # Search / SearchInput / SearchItem
2
+
3
+ M3 search bar + docked search view. Default = the M3 Expressive **contained**
4
+ style: a persistent full-pill bar on `surface-container-high` that, on
5
+ click/focus, opens a **separate** suggestions card below — `corner-medium`
6
+ (12) all around, a 2dp gap, no divider, elevation 3. The bar leading icon is
7
+ `on-surface` (primary affordance), the trailing icon/avatar `on-surface-variant`.
8
+ Items are 56dp `body-large` rows. Closes on outside click or Escape.
9
+
10
+ > **M3 Expressive note:** the legacy **divided** style (`divided`) joins the
11
+ > bar and results with an `outline` divider and squares off the bar's bottom
12
+ > corners (extra-large). M3 Expressive marks it _"not recommended — use
13
+ > contained"_; it stays for baseline parity.
14
+
15
+ ## Import
16
+
17
+ ```tsx
18
+ import {Search, SearchInput} from "react-material-expressive";
19
+ ```
20
+
21
+ ## API
22
+
23
+ ```ts
24
+ interface SearchProps {
25
+ children: ReactNode; // the bar content (SearchInput)
26
+ className?: string;
27
+ divided?: boolean; // baseline joined-with-divider style (legacy)
28
+ result?: ReactNode; // Search.Item list shown while open
29
+ resultClassName?: string;
30
+ }
31
+
32
+ interface SearchInputProps extends Omit<
33
+ ComponentProps<"input">,
34
+ "placeholder" | "size"
35
+ > {
36
+ className?: string;
37
+ inputClassName?: string;
38
+ labels?: SearchInputLabels; // accessible names
39
+ leftElement?: ReactNode; // 56px slot (search icon / back)
40
+ rightElement?: ReactNode; // 56px slot (avatar / mic)
41
+ }
42
+
43
+ interface SearchInputLabels {
44
+ placeholder?: string; // input placeholder (default "Search")
45
+ }
46
+
47
+ interface SearchItemProps {
48
+ children?;
49
+ className?;
50
+ label?: ReactNode;
51
+ leftElement?: ReactNode; // 24px box
52
+ onClick?: MouseEventHandler<HTMLButtonElement>;
53
+ rightElement?: ReactNode;
54
+ }
55
+ ```
56
+
57
+ `Search.Input` and `Search.Item` are also exposed as statics.
58
+
59
+ ## Example
60
+
61
+ ```tsx
62
+ <Search
63
+ result={
64
+ <>
65
+ <Search.Item
66
+ label="Recent query"
67
+ leftElement={<MaterialSymbol name="history" />}
68
+ onClick={pick}
69
+ />
70
+ <Search.Item
71
+ label="Trending"
72
+ leftElement={<MaterialSymbol name="trending_up" />}
73
+ />
74
+ </>
75
+ }>
76
+ <SearchInput
77
+ labels={{placeholder: "Search"}}
78
+ leftElement={<MaterialSymbol name="search" />}
79
+ onChange={onQuery}
80
+ />
81
+ </Search>
82
+ ```
83
+
84
+ ## Gotchas
85
+
86
+ - `SearchInput` is transparent — the `Search` wrapper owns the container
87
+ color/shape. Standalone bars: wrap in `Search` without `result`.
88
+ - Search bars don't float labels (M3); the placeholder stays visible.
89
+ - The suggestions card opens with the menu choreography (500ms emphasized
90
+ clip reveal with staggered items, 150ms accelerate exit). In the default
91
+ **contained** style the bar keeps its 28px pill shape and the card floats
92
+ 2dp below (corner-medium, level-3 shadow). With `divided` the bar's bottom
93
+ corners square off in step with the reveal (real 28px pill — half the 56dp
94
+ bar) and the card joins it via the `outline` divider.
95
+ - The bar is flat at rest (tonal-first, matching Compose's `Level0` default);
96
+ the search-bar token nominally specifies elevation 3.
97
+ - Like the lib's menus, the docked card is an inline overlay (no full-page
98
+ scrim).
99
+ - **BREAKING:** the native `placeholder` prop was replaced by
100
+ `labels={{placeholder}}`.
@@ -0,0 +1,76 @@
1
+ # Select
2
+
3
+ M3 selects: the text field anatomy (height 56, floating label, supporting
4
+ text, error states — filled or outlined) opening a 48dp-item options menu
5
+ with the @material/web animation (500ms emphasized reveal, 150ms
6
+ accelerate close). The caret cross-fades to an up arrow while open (75ms
7
+ linear delayed 75ms, like `md-select`). Keyboard navigation
8
+ (arrows/Home/End/Enter/Esc) and 200ms typeahead like `md-select`; the
9
+ selected option uses secondary-container. Designed to sit next to the text fields in forms —
10
+ same metrics, label behavior and supporting text.
11
+
12
+ ## Import
13
+
14
+ ```tsx
15
+ import {SelectFilled, SelectOutlined} from "react-material-expressive";
16
+ ```
17
+
18
+ ## API
19
+
20
+ ```ts
21
+ interface SelectOption {
22
+ disabled?: boolean;
23
+ label?: ReactNode; // display content; defaults to the value
24
+ value: string;
25
+ }
26
+
27
+ interface SelectFilledProps {
28
+ className?: string;
29
+ defaultValue?: string;
30
+ disabled?: boolean;
31
+ error?: boolean;
32
+ errorText?: ReactNode;
33
+ id?: string;
34
+ label?: string; // floating label
35
+ leftElement?: ReactNode; // leading icon (24px box)
36
+ name?: string; // posts the value via a hidden input
37
+ onChange?: (value: string) => void;
38
+ options: SelectOption[];
39
+ supportingText?: ReactNode;
40
+ value?: string; // controlled
41
+ }
42
+ // SelectOutlinedProps is identical.
43
+ ```
44
+
45
+ ## Examples
46
+
47
+ ```tsx
48
+ <SelectFilled
49
+ label="Fruit"
50
+ options={[
51
+ {label: "Apple", value: "apple"},
52
+ {label: "Banana", value: "banana"},
53
+ ]}
54
+ onChange={setFruit}
55
+ />
56
+ <SelectOutlined label="Quantity" name="qty" options={options} />
57
+ ```
58
+
59
+ ## Keyboard
60
+
61
+ Closed: `ArrowDown`/`ArrowUp`/`Enter`/`Space` open the menu (highlighting
62
+ the selected option); typing selects the first match directly. Open:
63
+ arrows move the highlight (skipping disabled options, wrapping),
64
+ `Home`/`End` jump, `Enter`/`Space` select, `Escape`/`Tab` close, typing
65
+ highlights by prefix (200ms buffer).
66
+
67
+ ## Gotchas
68
+
69
+ - Trigger is a `role="combobox"` button with `aria-activedescendant`
70
+ (focus never leaves the trigger) over a `role="listbox"` menu.
71
+ - `name` renders a hidden input so the value posts in native forms;
72
+ native `required` validation is not wired — validate in `onSubmit`.
73
+ - The menu matches the field width and clips after ~280px with scroll.
74
+ - The label float shares the text fields' animation: a transform-only
75
+ tween between two label copies (@material/web technique) — font-size
76
+ never interpolates.
@@ -0,0 +1,62 @@
1
+ # BottomSheet / SideSheet
2
+
3
+ M3 modal sheets over a 32% scrim, on surface-container-low at elevation 1.
4
+ Both stay mounted during their exit animation (slide down / slide right)
5
+ and lock body scroll while open.
6
+
7
+ ## Import
8
+
9
+ ```tsx
10
+ import {BottomSheet, SideSheet} from "react-material-expressive";
11
+ ```
12
+
13
+ ## API
14
+
15
+ ```ts
16
+ interface BottomSheetProps {
17
+ children?: ReactNode;
18
+ className?: string;
19
+ dragHandle?: boolean; // M3 32x4 handle (default true; clicking it closes)
20
+ isVisible?: boolean;
21
+ onClose?: () => void;
22
+ }
23
+
24
+ interface SideSheetProps {
25
+ children?: ReactNode;
26
+ className?: string;
27
+ closeButton?: boolean | ReactNode; // true = default X icon button
28
+ isVisible?: boolean;
29
+ labels?: SideSheetLabels; // accessible names
30
+ onClose?: () => void;
31
+ title?: ReactNode; // title-large header
32
+ }
33
+
34
+ interface SideSheetLabels {
35
+ close?: string; // default close button aria-label (default "Close")
36
+ }
37
+ ```
38
+
39
+ - BottomSheet: extra-large top corners, max width 640, 72dp top margin,
40
+ slides from the bottom. The 32×4 drag handle is `on-surface-variant`
41
+ (full opacity, per token) with 22dp padding above/below.
42
+ - SideSheet: docked right, 360px (400 on ≥sm; max-width 400), large start
43
+ corners, slides from the right. The `title` headline is `on-surface-variant`
44
+ (M3 side-sheet color role), not on-surface.
45
+
46
+ ## Example
47
+
48
+ ```tsx
49
+ <BottomSheet isVisible={open} onClose={() => setOpen(false)}>
50
+ <List>…</List>
51
+ </BottomSheet>
52
+
53
+ <SideSheet closeButton isVisible={open} onClose={close} title="Filters">
54
+
55
+ </SideSheet>
56
+ ```
57
+
58
+ ## Gotchas
59
+
60
+ - Controlled-only (`isVisible` + `onClose`); Escape and scrim click close.
61
+ - Content scrolls inside the sheet; the bottom sheet caps at
62
+ `100vh − 72px`.
@@ -0,0 +1,89 @@
1
+ # Slider / SliderDual
2
+
3
+ M3 Expressive sliders: 16px inset track (primary active /
4
+ secondary-container inactive, full outer corners, 2px inner), 4×44 pill
5
+ handle with a 6px gap on each side (narrows to 2px while pressed),
6
+ `on-secondary-container` stop indicator dots (4dp, 6dp from the end) and a
7
+ 44×48 `inverse-surface` value indicator (`label-large`, 12dp above the
8
+ handle) on hover/drag. `size` selects the M3E track scale — `xs` (16dp,
9
+ default), `s` (24), `m` (40), `l` (56), `xl` (96), with handle heights
10
+ 44/44/52/68/108 and corners 8/8/12/16/28; the thick sizes (`m`/`l`/`xl`)
11
+ host an optional inset `icon` at the leading edge of the track, two-toned
12
+ to follow it (`on-primary` over the active track, `on-secondary-container`
13
+ over the inactive).
14
+ Built over native `input[type=range]` — keyboard and a11y come for free.
15
+
16
+ ## Import
17
+
18
+ ```tsx
19
+ import {Slider, SliderDual} from "react-material-expressive";
20
+ ```
21
+
22
+ ## API
23
+
24
+ ```ts
25
+ interface SliderProps {
26
+ className?: string;
27
+ defaultValue?: number;
28
+ disabled?: boolean;
29
+ icon?: ReactNode; // inset leading icon (m/l/xl), two-toned by track
30
+ labels?: SliderLabels; // accessible names
31
+ max?: number;
32
+ min?: number;
33
+ size?: "xs" | "s" | "m" | "l" | "xl"; // M3E track scale (xs default)
34
+ step?: number; // 100 / 0 / browser default
35
+ onChange?: (value: number) => void;
36
+ showLabel?: boolean; // value indicator (default true)
37
+ tooltipChildren?: ReactNode; // suffix inside the indicator (e.g. "%")
38
+ value?: number; // controlled
39
+ }
40
+
41
+ interface SliderLabels {
42
+ label?: string; // aria-label (default "Slider")
43
+ }
44
+
45
+ interface SliderDualValue {
46
+ max: number;
47
+ min: number;
48
+ }
49
+ interface SliderDualProps {
50
+ // same options (incl. `size`, no `icon`), with:
51
+ defaultValue?: SliderDualValue;
52
+ labels?: SliderDualLabels; // accessible names
53
+ onChange?: (value: SliderDualValue) => void;
54
+ value?: SliderDualValue;
55
+ }
56
+
57
+ interface SliderDualLabels {
58
+ label?: string; // base aria-label (default "Range slider")
59
+ minimum?: string; // min handle suffix (default "minimum")
60
+ maximum?: string; // max handle suffix (default "maximum")
61
+ // each handle's aria-label = `${label} ${minimum|maximum}`
62
+ }
63
+ ```
64
+
65
+ ## Examples
66
+
67
+ ```tsx
68
+ <Slider defaultValue={40} onChange={setVolume} />
69
+ <Slider min={0} max={100} step={10} tooltipChildren="%" />
70
+ <SliderDual value={range} onChange={setRange} />
71
+ ```
72
+
73
+ ## Gotchas
74
+
75
+ - `Slider.onChange` receives a plain number; `SliderDual.onChange` a
76
+ `{min, max}` pair (handles clamp to ±1 step of each other).
77
+ - Range click-to-jump works on `Slider`; on `SliderDual` only the handles
78
+ are draggable.
79
+ - Sizes `xs`–`xl` are supported on both `Slider` and `SliderDual`; the
80
+ inset `icon` is single-`Slider` only (the range's active segment sits
81
+ between the handles, so there's no fixed leading edge for it).
82
+ - The inset `icon` is two-toned like the stop indicators — `on-primary`
83
+ over the active track, `on-secondary-container` over the inactive one —
84
+ so it changes colour (rather than vanishing) as the handle crosses it.
85
+ - The M3E "discrete/stops" rendering (a 4dp stop indicator at every step,
86
+ on-primary over the active track / on-secondary-container over the
87
+ inactive one) is not drawn — `step` still snaps the value.
88
+ - **BREAKING:** the `aria-label` prop was replaced by `labels={{label}}`
89
+ (`SliderDual` handle suffixes are `labels.minimum`/`labels.maximum`).
@@ -0,0 +1,73 @@
1
+ # Snackbar / SnackbarWrapper
2
+
3
+ M3 snackbar: shape extra-small (4), inverse-surface, min height 48, width
4
+ 344–600, `body-medium` text. Action and close render as real M3 buttons
5
+ recolored to inverse roles. `SnackbarWrapper` is the fixed bottom-centered
6
+ stacking area.
7
+
8
+ ## Import
9
+
10
+ ```tsx
11
+ import {Snackbar, SnackbarWrapper} from "react-material-expressive";
12
+ ```
13
+
14
+ ## API
15
+
16
+ ```ts
17
+ interface SnackbarProps {
18
+ actionLabel?: string; // optional text action
19
+ autoHideDuration?: number; // ms; auto-calls onClose, pauses on hover/focus
20
+ button?: ReactNode; // extra trailing content
21
+ className?: string;
22
+ closeIcon?: ReactNode; // custom X
23
+ isVisible: boolean;
24
+ labels?: SnackbarLabels; // accessible names
25
+ onAction?: () => void;
26
+ onClose?: () => void;
27
+ showClose?: boolean; // optional close icon button
28
+ text?: ReactNode;
29
+ }
30
+
31
+ interface SnackbarLabels {
32
+ dismiss?: string; // close button aria-label (default "Dismiss")
33
+ }
34
+
35
+ interface SnackbarWrapperProps {
36
+ children: ReactNode;
37
+ className?: string;
38
+ }
39
+ ```
40
+
41
+ Everything but `isVisible` is optional — a message-only snackbar is valid.
42
+
43
+ ## Example
44
+
45
+ ```tsx
46
+ <SnackbarWrapper>
47
+ <Snackbar
48
+ actionLabel="Undo"
49
+ autoHideDuration={4000}
50
+ isVisible={open}
51
+ onAction={undo}
52
+ onClose={() => setOpen(false)}
53
+ showClose
54
+ text="Photo archived"
55
+ />
56
+ </SnackbarWrapper>
57
+ ```
58
+
59
+ ## Gotchas
60
+
61
+ - Auto-hide pauses while hovered or focused (M3 a11y) and resumes with a
62
+ full interval on leave.
63
+ - Enter/exit are a fade + scale (0.8↔1) — the M3 Expressive snackbar motion
64
+ (Compose `FadeInFadeOutWithScale`), not a slide; the component unmounts
65
+ ~200ms after `isVisible` goes false.
66
+ - Single-line is 48dp, two-line 68dp; elevation level 3.
67
+ - `role="status"` announces politely to screen readers.
68
+ - The dismiss button aria-label is customizable via `labels.dismiss`
69
+ (default "Dismiss").
70
+ - `SnackbarWrapper` portals to `document.body`, so it always paints above app
71
+ content no matter where it is mounted — a positioned, z-indexed ancestor
72
+ can't trap it in a lower stacking context. It renders only on the client
73
+ (SSR-safe).
@@ -0,0 +1,75 @@
1
+ # SplitButton
2
+
3
+ M3 Expressive split button: a leading action button plus a trailing menu
4
+ button separated by a 2dp gap. Outer corners are full; the inner corners
5
+ morph on hover/focus/press (4/4/4/8/12dp at rest → 8/12/12/20/20dp per
6
+ size). While the menu is open the trailing button rounds to full, centers
7
+ its caret (rotated 180°) and keeps a pressed-opacity state layer — the
8
+ container colors never change on selection (spec). Colors, state layers,
9
+ elevation and disabled states are the standard button ones, so themes
10
+ apply via the same tokens.
11
+
12
+ ## Import
13
+
14
+ ```tsx
15
+ import {SplitButton} from "react-material-expressive";
16
+ ```
17
+
18
+ ## API
19
+
20
+ ```ts
21
+ interface SplitButtonProps {
22
+ children?: ReactNode; // leading button label
23
+ className?: string;
24
+ disabled?: boolean; // disables both buttons
25
+ iconLeft?: ReactNode; // leading icon (sized per size like Button)
26
+ labels?: SplitButtonLabels; // trailing menu aria-label
27
+ menu?: ReactNode; // SplitButton.Item list (anchored bottom right)
28
+ menuClassName?: string;
29
+ onClick?: MouseEventHandler; // leading button action
30
+ size?: "xs" | "s" | "m" | "l" | "xl"; // default "s" (40dp)
31
+ text?: string; // label fallback when no children
32
+ variant?: "elevated" | "filled" | "tonal" | "outlined"; // default "filled"
33
+ }
34
+
35
+ interface SplitButtonLabels {
36
+ menu?: string; // trailing button aria-label (default "More options")
37
+ }
38
+ ```
39
+
40
+ The menu is the shared M3E vertical [`Menu`](Menu.md) (corner-large surface,
41
+ `surface-container-low`, elevation 3) anchored to the trailing button's
42
+ bottom-right; focus returns to that button when it closes. `SplitButton.Item`
43
+ is the shared `Menu.Item` (same as `OverflowMenu.Item`).
44
+
45
+ ## Sizes
46
+
47
+ Heights match the Expressive button sizes (32/40/56/96/136). The trailing
48
+ button uses the spec menu icon (22/22/26/38/50dp) with the optical offset
49
+ (−1/−1/−2/−3/−6dp) that disappears when open. Both buttons keep the
50
+ Compose 48dp minimum width — it only shows on an icon-only XS leading
51
+ button (which would otherwise be 42dp wide).
52
+
53
+ ## Example
54
+
55
+ ```tsx
56
+ <SplitButton
57
+ iconLeft={<MaterialSymbol name="send" size={20} />}
58
+ onClick={send}
59
+ text="Send"
60
+ menu={
61
+ <>
62
+ <SplitButton.Item label="Schedule send" onClick={schedule} />
63
+ <SplitButton.Item label="Save draft" onClick={saveDraft} />
64
+ </>
65
+ }
66
+ />
67
+ ```
68
+
69
+ ## Gotchas
70
+
71
+ - The leading button never opens the menu — only the trailing one does
72
+ (clicks on items close it; Escape and outside clicks too).
73
+ - There is no `text` color variant: the spec only defines elevated,
74
+ filled, tonal and outlined.
75
+ - **BREAKING:** `menuLabel` was replaced by `labels={{menu}}`.
@@ -0,0 +1,71 @@
1
+ # Stories
2
+
3
+ Horizontal scrollable story strip with two item types: `Stories.User`
4
+ (circular, ring/badges/live, name below) and `Stories.Business` (rounded
5
+ media with primary ring and start-aligned label). Media is injectable.
6
+
7
+ ## Import
8
+
9
+ ```tsx
10
+ import {Stories} from "react-material-expressive";
11
+ ```
12
+
13
+ ## API
14
+
15
+ ```ts
16
+ interface StoriesProps {
17
+ children: ReactNode;
18
+ className?: string;
19
+ }
20
+
21
+ interface UserItemProps extends MediaInjectionProps {
22
+ alt?: string;
23
+ badge?: boolean;
24
+ badgeColor?: string;
25
+ badgeIcon?: ReactNode;
26
+ badgeText?: string;
27
+ className?: string;
28
+ live?: boolean;
29
+ liveColor?: string;
30
+ liveIcon?: ReactNode;
31
+ liveText?: string; // default "LIVE"
32
+ name?: string;
33
+ onClick?: MouseEventHandler<HTMLDivElement>;
34
+ radius?: number;
35
+ size?: number; // default 64
36
+ ring?: boolean; // animated token-gradient ring
37
+ src?: string | StaticImageData;
38
+ }
39
+
40
+ interface BusinessItemProps extends MediaInjectionProps {
41
+ alt?: string;
42
+ className?: string;
43
+ height?: number;
44
+ width?: number; // default 64
45
+ onClick?: MouseEventHandler<HTMLDivElement>;
46
+ radius?: number;
47
+ ring?: boolean; // static primary ring
48
+ src?: string | StaticImageData;
49
+ text?: string;
50
+ }
51
+ ```
52
+
53
+ ## Example
54
+
55
+ ```tsx
56
+ <Stories>
57
+ <Stories.User name="Ada" ring src="/ada.jpg" />
58
+ <Stories.User live name="Grace" src="/grace.jpg" />
59
+ <Stories.Business src="/brand.png" text="Coffee & Co" />
60
+ </Stories>
61
+ ```
62
+
63
+ ## Gotchas
64
+
65
+ - Pure presentation: wire `onClick` for your story viewer. `onClick` lands
66
+ on a `<div>` (no button semantics) — wrap in a `<button>` for keyboard
67
+ users, like `Avatar`.
68
+ - All imagery falls back per the media-injection contract (render > image
69
+ > children > native `<img>`).
70
+ - `Stories.User`'s animated ring stops under `prefers-reduced-motion`;
71
+ `Stories.Business`'s ring is static.