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.
- package/CHANGELOG.md +39 -0
- package/LICENSE +21 -0
- package/README.md +286 -0
- package/dist/index.cjs +7014 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2068 -0
- package/dist/index.d.ts +2068 -0
- package/dist/index.js +6941 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +2 -0
- package/dist/theme.css +187 -0
- package/docs/components/Amount.md +48 -0
- package/docs/components/Avatar.md +69 -0
- package/docs/components/AvatarStack.md +50 -0
- package/docs/components/Badge.md +50 -0
- package/docs/components/Blob.md +44 -0
- package/docs/components/Button.md +79 -0
- package/docs/components/ButtonGroup.md +46 -0
- package/docs/components/ButtonGroupConnected.md +62 -0
- package/docs/components/Card.md +52 -0
- package/docs/components/Checkbox.md +45 -0
- package/docs/components/Chips.md +77 -0
- package/docs/components/DatePicker.md +112 -0
- package/docs/components/Dialog.md +83 -0
- package/docs/components/Divider.md +48 -0
- package/docs/components/Dropdown.md +79 -0
- package/docs/components/FAB.md +63 -0
- package/docs/components/FABMenu.md +76 -0
- package/docs/components/Gallery.md +35 -0
- package/docs/components/Icon.md +36 -0
- package/docs/components/IconButton.md +69 -0
- package/docs/components/Img.md +52 -0
- package/docs/components/Layers.md +43 -0
- package/docs/components/Link.md +43 -0
- package/docs/components/List.md +87 -0
- package/docs/components/Loading.md +67 -0
- package/docs/components/LoadingIndicator.md +64 -0
- package/docs/components/MaterialSymbol.md +48 -0
- package/docs/components/MediaFrame.md +46 -0
- package/docs/components/Menu.md +149 -0
- package/docs/components/NavigationBar.md +78 -0
- package/docs/components/NavigationRail.md +105 -0
- package/docs/components/OverflowMenu.md +65 -0
- package/docs/components/Perspective.md +45 -0
- package/docs/components/Progress.md +83 -0
- package/docs/components/Radio.md +39 -0
- package/docs/components/Search.md +100 -0
- package/docs/components/Select.md +76 -0
- package/docs/components/Sheets.md +62 -0
- package/docs/components/Slider.md +89 -0
- package/docs/components/Snackbar.md +73 -0
- package/docs/components/SplitButton.md +75 -0
- package/docs/components/Stories.md +71 -0
- package/docs/components/Switch.md +40 -0
- package/docs/components/Table.md +67 -0
- package/docs/components/Tabs.md +67 -0
- package/docs/components/TextElement.md +37 -0
- package/docs/components/TextField.md +70 -0
- package/docs/components/TimePicker.md +83 -0
- package/docs/components/ToggleTheme.md +71 -0
- package/docs/components/Toolbar.md +102 -0
- package/docs/components/Tooltip.md +63 -0
- package/docs/components/TopAppBar.md +84 -0
- package/docs/components/Video.md +35 -0
- package/llms.txt +90 -0
- 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.
|