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,50 @@
|
|
|
1
|
+
# Badge / DotBadge / OnIconBadge
|
|
2
|
+
|
|
3
|
+
M3 badges: full shape on the error container. `Badge` is the large badge
|
|
4
|
+
(min 16px, `label-small` count), `DotBadge` the 6px small badge,
|
|
5
|
+
`OnIconBadge` a count badge pre-positioned on an icon's top-right corner.
|
|
6
|
+
|
|
7
|
+
## Import
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import {Badge, DotBadge, OnIconBadge} from "react-material-expressive";
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## API
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
interface BadgeProps extends ComponentProps<"div"> {
|
|
17
|
+
icon?: ReactNode;
|
|
18
|
+
iconLeft?: ReactNode;
|
|
19
|
+
iconRight?: ReactNode; // 12px boxes
|
|
20
|
+
text?: ReactNode; // content fallback when no children
|
|
21
|
+
}
|
|
22
|
+
type DotBadgeProps = ComponentProps<"div">;
|
|
23
|
+
interface OnIconBadgeProps extends ComponentProps<"div"> {
|
|
24
|
+
count?: ReactNode;
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Examples
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
<Badge text="3" />
|
|
32
|
+
<Badge className="bg-tertiary text-on-tertiary" text="new" /> // recolor via className
|
|
33
|
+
|
|
34
|
+
<span className="relative inline-flex">
|
|
35
|
+
<MaterialSymbol name="notifications" size={32} />
|
|
36
|
+
<OnIconBadge count="12" />
|
|
37
|
+
</span>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Gotchas
|
|
41
|
+
|
|
42
|
+
- `OnIconBadge` positions absolutely — the icon wrapper needs
|
|
43
|
+
`position: relative`.
|
|
44
|
+
- `OnIconBadge` follows the M3 placement spec (14×12dp from the icon's
|
|
45
|
+
top-trailing corner): top −2, leading edge fixed 12px from the trailing
|
|
46
|
+
edge, growing outward (trailing) with the count.
|
|
47
|
+
- Keep counts short ("999+" — the spec caps the badge at 4 characters /
|
|
48
|
+
16×34dp); the badge grows with content.
|
|
49
|
+
- Dot on an icon goes flush in the corner (`top-0 right-0`), per the
|
|
50
|
+
spec's 6×6dp corner measurement.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Blob
|
|
2
|
+
|
|
3
|
+
Decorative blurred blob with organic border-radius morphing
|
|
4
|
+
(`animate-blob`, 10s loop). Token-colored (`bg-primary/10` by default) and
|
|
5
|
+
non-interactive.
|
|
6
|
+
|
|
7
|
+
## Import
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import {Blob} from "react-material-expressive";
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## API
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
interface BlobProps {
|
|
17
|
+
children?: ReactNode;
|
|
18
|
+
className?: string;
|
|
19
|
+
color?: string; // background class, e.g. "bg-tertiary/15"
|
|
20
|
+
delay?: number; // ms phase offset into the loop, stagger multiple blobs
|
|
21
|
+
height?: number; // default 300
|
|
22
|
+
position?: string; // position classes (default centered)
|
|
23
|
+
width?: number; // default 300
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Example
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
<section className="relative overflow-hidden">
|
|
31
|
+
<Blob color="bg-primary/15" position="top-10 left-1/4" />
|
|
32
|
+
<Blob color="bg-tertiary/15" delay={3000} width={220} height={220} />
|
|
33
|
+
<h1 className="relative z-10 text-display-small">Hero</h1>
|
|
34
|
+
</section>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Gotchas
|
|
38
|
+
|
|
39
|
+
- Absolutely positioned: the parent needs `position: relative` and usually
|
|
40
|
+
`overflow-hidden`.
|
|
41
|
+
- It's `aria-hidden` and `pointer-events-none` by design.
|
|
42
|
+
- `delay` is applied as a **negative** `animation-delay` (phase offset):
|
|
43
|
+
staggered blobs morph from the first frame instead of sitting frozen as
|
|
44
|
+
perfect circles until the delay elapses.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Button
|
|
2
|
+
|
|
3
|
+
M3 Expressive button. `size` defaults to `s` (small): height 40,
|
|
4
|
+
`label-large`, full shape, 20px icons with 8px icon-label gap and
|
|
5
|
+
symmetric 16dp padding. Five sizes (`xs`/`s`/`m`/`l`/`xl`), `shape`
|
|
6
|
+
(round/square + pressed morph) and a `selected` toggle.
|
|
7
|
+
|
|
8
|
+
## Import
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
import {Button} from "react-material-expressive";
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## API
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
interface ButtonProps extends ComponentProps<"button"> {
|
|
18
|
+
iconLeft?: ReactNode; // 20/20/24/32/40px box per size
|
|
19
|
+
iconRight?: ReactNode;
|
|
20
|
+
selected?: boolean; // M3 Expressive toggle (adds aria-pressed)
|
|
21
|
+
shape?: "round" | "square"; // M3 Expressive; default "round"
|
|
22
|
+
size?: "xs" | "s" | "m" | "l" | "xl"; // M3 Expressive: 32/40/56/96/136; default "s"
|
|
23
|
+
text?: string; // label fallback when no children
|
|
24
|
+
variant?: "filled" | "tonal" | "outlined" | "elevated" | "text"; // default "filled"
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Variants
|
|
29
|
+
|
|
30
|
+
- `filled` — primary/on-primary, highest emphasis.
|
|
31
|
+
- `tonal` — secondary-container/on-secondary-container.
|
|
32
|
+
- `elevated` — surface-container-low + level 1 shadow (level 2 on hover).
|
|
33
|
+
- `outlined` — outline border, primary label.
|
|
34
|
+
- `text` — lowest emphasis, primary label, no container.
|
|
35
|
+
|
|
36
|
+
## Expressive sizes and shapes
|
|
37
|
+
|
|
38
|
+
All five sizes (`xs`/`s`/`m`/`l`/`xl`, default `s`) use symmetric padding
|
|
39
|
+
12/16/24/48/64, icons 20/20/24/32/40, labels label-large → title-medium →
|
|
40
|
+
headline-small → headline-large, and per-size square radii (12/12/16/28/28).
|
|
41
|
+
Pressing morphs both shapes to the per-size pressed radius (8/8/12/16/16,
|
|
42
|
+
the DSDB PressedContainerShape tokens; border-radius transition,
|
|
43
|
+
emphasized). The asymmetric 24dp padding of the old common button is no
|
|
44
|
+
longer recommended in M3E.
|
|
45
|
+
|
|
46
|
+
## Toggle
|
|
47
|
+
|
|
48
|
+
With `selected` the button becomes a toggle: colors follow the M3
|
|
49
|
+
Expressive toggle tokens per variant (elevated: surface-container-low ↔
|
|
50
|
+
primary; filled: surface-container ↔ primary; tonal: secondary-container
|
|
51
|
+
↔ secondary; outlined: outline ↔ inverse-surface — the text variant has
|
|
52
|
+
no toggle) and the shape follows the selection (unselected round,
|
|
53
|
+
selected square). Emits `aria-pressed`, so it works inside
|
|
54
|
+
`ButtonGroupConnected` out of the box.
|
|
55
|
+
|
|
56
|
+
## States
|
|
57
|
+
|
|
58
|
+
Hover/focus/pressed use M3 state layers (8/12/12% currentColor); the
|
|
59
|
+
press ripple grows from the pointer like @material/web. Filled/tonal gain
|
|
60
|
+
a level 1 shadow on hover. `disabled` renders container `on-surface/12` +
|
|
61
|
+
label `on-surface/38` (outlined: 12% border; no container on text), and
|
|
62
|
+
removes elevation.
|
|
63
|
+
|
|
64
|
+
## Examples
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
<Button>Accept</Button>
|
|
68
|
+
<Button variant="tonal" iconLeft={<MaterialSymbol name="add" size={18} />}>
|
|
69
|
+
New item
|
|
70
|
+
</Button>
|
|
71
|
+
<Button size="xl" shape="square">Get started</Button>
|
|
72
|
+
<Button variant="text" onClick={close}>Cancel</Button>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Gotchas
|
|
76
|
+
|
|
77
|
+
- `type` defaults to `"button"` (no accidental form submits).
|
|
78
|
+
- Icons are `ReactNode`; size them at 18px for spec fidelity (or at the
|
|
79
|
+
per-size icon box when using `size`).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# ButtonGroup
|
|
2
|
+
|
|
3
|
+
M3 Expressive standard button group: an invisible container that spaces
|
|
4
|
+
its buttons per size (18/12/8/8/8dp — wider on small sizes to preserve
|
|
5
|
+
48dp touch targets) and adds the press interaction between them: the
|
|
6
|
+
pressed button widens while its direct neighbors compress, animated by
|
|
7
|
+
the shared 200ms emphasized morph (web approximation of the Compose
|
|
8
|
+
~15% width spring).
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import {ButtonGroup} from "react-material-expressive";
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## API
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
interface ButtonGroupProps {
|
|
20
|
+
children?: ReactNode; // Buttons / IconButtons with the same `size`
|
|
21
|
+
className?: string;
|
|
22
|
+
label?: string; // aria-label for the group
|
|
23
|
+
size?: "xs" | "s" | "m" | "l" | "xl"; // default "s"
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Example
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
<ButtonGroup label="Playback controls" size="s">
|
|
31
|
+
<Button size="s" text="Back" variant="tonal" />
|
|
32
|
+
<Button size="s" text="Pause" variant="filled" />
|
|
33
|
+
<Button size="s" text="Next" variant="tonal" />
|
|
34
|
+
</ButtonGroup>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Gotchas
|
|
38
|
+
|
|
39
|
+
- Pass the same `size` to the group and every button inside — the press
|
|
40
|
+
width-morph assumes the size's symmetric padding.
|
|
41
|
+
- Per spec, avoid text buttons and standard icon buttons inside groups
|
|
42
|
+
(they have no container treatment). Use filled/tonal/outlined/elevated.
|
|
43
|
+
- The width morph applies to label `Button`s (padding-driven width); icon
|
|
44
|
+
buttons keep their fixed square size.
|
|
45
|
+
- The group has no colors of its own — theming comes entirely from the
|
|
46
|
+
buttons' tokens.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# ButtonGroupConnected
|
|
2
|
+
|
|
3
|
+
M3 Expressive connected button group: buttons joined by 2dp gaps where
|
|
4
|
+
the group controls the corners. Outer edges keep the group shape — full
|
|
5
|
+
(round) or the per-size square value (4/8/8/16/20dp) — while inner
|
|
6
|
+
corners stay small (8/8/8/16/20dp), sharpen while pressed (4/4/4/12/16dp)
|
|
7
|
+
and round to full when a toggle button is selected. Unlike the standard
|
|
8
|
+
`ButtonGroup` there is no interaction between neighbors. It is the
|
|
9
|
+
M3 Expressive successor of the segmented buttons.
|
|
10
|
+
|
|
11
|
+
## Import
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import {ButtonGroupConnected} from "react-material-expressive";
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## API
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
interface ButtonGroupConnectedProps {
|
|
21
|
+
children?: ReactNode; // Buttons / IconButtons with the same `size`
|
|
22
|
+
className?: string;
|
|
23
|
+
label?: string; // aria-label for the group
|
|
24
|
+
shape?: "round" | "square"; // outer corners (default "round")
|
|
25
|
+
size?: "xs" | "s" | "m" | "l" | "xl"; // default "s"
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Selection
|
|
30
|
+
|
|
31
|
+
The group reads selection from the children's `aria-pressed` — toggle
|
|
32
|
+
`IconButton`s (`selected`) work out of the box; manage single or
|
|
33
|
+
multi-select state in your app. The selected button rounds to full and
|
|
34
|
+
its colors follow the button toggle specs.
|
|
35
|
+
|
|
36
|
+
## Example
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
const [active, setActive] = useState(0);
|
|
40
|
+
|
|
41
|
+
<ButtonGroupConnected label="Transport mode">
|
|
42
|
+
{modes.map((mode, index) => (
|
|
43
|
+
<IconButton
|
|
44
|
+
icon={<MaterialSymbol name={mode.icon} size={24} />}
|
|
45
|
+
key={mode.id}
|
|
46
|
+
onClick={() => setActive(index)}
|
|
47
|
+
selected={active === index}
|
|
48
|
+
size="s"
|
|
49
|
+
variant="tonal"
|
|
50
|
+
/>
|
|
51
|
+
))}
|
|
52
|
+
</ButtonGroupConnected>;
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Gotchas
|
|
56
|
+
|
|
57
|
+
- Pass the same `size` to the group and every button inside.
|
|
58
|
+
- The group corner system overrides the children's own shape (including
|
|
59
|
+
their standalone pressed morphs) on purpose — it must, per spec.
|
|
60
|
+
- XS/S groups enforce a 48dp minimum button width for touch targets.
|
|
61
|
+
- The group has no colors of its own — theming comes entirely from the
|
|
62
|
+
buttons' tokens.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Card
|
|
2
|
+
|
|
3
|
+
M3 card (shape medium, 16dp padding) with composable sub-parts:
|
|
4
|
+
`Card.Header`, `Card.Body`, `Card.Footer`.
|
|
5
|
+
|
|
6
|
+
## Import
|
|
7
|
+
|
|
8
|
+
```tsx
|
|
9
|
+
import {Card} from "react-material-expressive";
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## API
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
interface CardProps extends ComponentProps<"div"> {
|
|
16
|
+
variant?: "elevated" | "filled" | "outlined"; // default "filled"
|
|
17
|
+
}
|
|
18
|
+
// Card.Header / Card.Body / Card.Footer: ComponentProps<"div">
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Variants (M3 spec)
|
|
22
|
+
|
|
23
|
+
- `elevated` — surface-container-low + elevation level 1.
|
|
24
|
+
- `filled` — surface-container-highest.
|
|
25
|
+
- `outlined` — surface + outline-variant border.
|
|
26
|
+
|
|
27
|
+
## Example
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
<Card variant="elevated" className="max-w-sm">
|
|
31
|
+
<Card.Header>
|
|
32
|
+
<Avatar name="MX" />
|
|
33
|
+
<TextElement title="Headline" body="Subhead" />
|
|
34
|
+
</Card.Header>
|
|
35
|
+
<Card.Body>
|
|
36
|
+
<p className="text-body-medium text-on-surface-variant">Content…</p>
|
|
37
|
+
</Card.Body>
|
|
38
|
+
<Card.Footer className="justify-end">
|
|
39
|
+
<Button variant="text">Dismiss</Button>
|
|
40
|
+
<Button>Action</Button>
|
|
41
|
+
</Card.Footer>
|
|
42
|
+
</Card>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Gotchas
|
|
46
|
+
|
|
47
|
+
- For edge-to-edge media use `className="p-0"` and pad inner content
|
|
48
|
+
manually (see the WithMedia story).
|
|
49
|
+
- Cards are non-interactive by default; wrap with `LinkContainer` for a
|
|
50
|
+
clickable card. The hover/pressed/dragged tokens in the M3 card sets
|
|
51
|
+
apply only to actionable cards, so the base component ships none.
|
|
52
|
+
- Spec guidance: keep at most 8dp between sibling cards.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Checkbox
|
|
2
|
+
|
|
3
|
+
M3 checkbox: custom-rendered 18×18 box (2dp shape, 2dp outline, checkmark
|
|
4
|
+
in currentColor) with a 40px circular state layer. The native input is the
|
|
5
|
+
source of truth — controlled (`checked` + `onChange`) and uncontrolled
|
|
6
|
+
(`defaultChecked`) both keep platform semantics.
|
|
7
|
+
|
|
8
|
+
## Import
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
import {Checkbox} from "react-material-expressive";
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## API
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
interface CheckboxProps extends Omit<
|
|
18
|
+
ComponentProps<"input">,
|
|
19
|
+
"type" | "size" | "children"
|
|
20
|
+
> {
|
|
21
|
+
className?: string; // wrapping <label>
|
|
22
|
+
inputClassName?: string; // visual box
|
|
23
|
+
label?: ReactNode;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## States
|
|
28
|
+
|
|
29
|
+
Unchecked (on-surface-variant outline) / checked (primary container,
|
|
30
|
+
on-primary check) / disabled (38% outline; selected disabled: 38%
|
|
31
|
+
container). Hover/focus/pressed show the 40px state layer (on-surface
|
|
32
|
+
unchecked, primary checked).
|
|
33
|
+
|
|
34
|
+
## Examples
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
<Checkbox label="Accept terms" onChange={(e) => setOk(e.target.checked)} />
|
|
38
|
+
<Checkbox checked={value} onChange={(e) => setValue(e.target.checked)} label="Controlled" />
|
|
39
|
+
<Checkbox defaultChecked disabled label="Locked" />
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Gotchas
|
|
43
|
+
|
|
44
|
+
- The label is part of the clickable area (wrapped in `<label>`).
|
|
45
|
+
- For groups just repeat with a shared `name`.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Chips
|
|
2
|
+
|
|
3
|
+
M3 chip: height 32, small shape (8), `label-large`, 18px icons. Padding is
|
|
4
|
+
16dp, reduced to 8dp on the icon/remove side automatically.
|
|
5
|
+
|
|
6
|
+
## Import
|
|
7
|
+
|
|
8
|
+
```tsx
|
|
9
|
+
import {Chips} from "react-material-expressive";
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## API
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
interface ChipsProps extends ComponentProps<"button"> {
|
|
16
|
+
avatar?: boolean; // input chips: 24dp leading avatar (4dp edge padding)
|
|
17
|
+
elevated?: boolean; // surface-container-low + level 1 instead of outline
|
|
18
|
+
labels?: ChipsLabels; // accessible names
|
|
19
|
+
leftElement?: ReactNode; // 18px box
|
|
20
|
+
onRemove?: (e) => void; // input chips: trailing remove affordance
|
|
21
|
+
rightElement?: ReactNode; // 18px box
|
|
22
|
+
selected?: boolean; // filter/input visual state (controlled by you)
|
|
23
|
+
text?: string; // label fallback when no children
|
|
24
|
+
variant?: "assist" | "filter" | "input" | "suggestion"; // default "assist"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ChipsLabels {
|
|
28
|
+
remove?: string; // trailing remove affordance aria-label (default "Remove")
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Variants
|
|
33
|
+
|
|
34
|
+
- `assist` — on-surface label; leading icon tinted primary.
|
|
35
|
+
- `suggestion` — on-surface-variant label; leading icon tinted primary.
|
|
36
|
+
- `filter` / `input` — on-surface-variant label; when `selected`, the chip
|
|
37
|
+
turns secondary-container without border. Both expose `aria-pressed`.
|
|
38
|
+
The leading icon tints primary on unselected filter chips and on
|
|
39
|
+
selected input chips (per the M3 chip token sets). Selected filter
|
|
40
|
+
chips grow a leading checkmark (the chip widens ~150ms while the check
|
|
41
|
+
draws in); a custom `leftElement` crossfades with the check instead.
|
|
42
|
+
- `elevated` — swaps the outline for `surface-container-low` at elevation
|
|
43
|
+
1 (2 on hover, 1 pressed). Not available on input chips (ignored), like
|
|
44
|
+
`@material/web`.
|
|
45
|
+
|
|
46
|
+
The flat outline is `outline-variant` (current M3 tokens) and darkens on
|
|
47
|
+
keyboard focus (on-surface for assist, on-surface-variant otherwise).
|
|
48
|
+
|
|
49
|
+
## States
|
|
50
|
+
|
|
51
|
+
State layer on hover/focus/pressed. A selected flat filter chip raises to
|
|
52
|
+
elevation 1 on hover (snap, like every chip elevation). `disabled`: border
|
|
53
|
+
`on-surface/12`, label `on-surface/38` (selected or elevated disabled:
|
|
54
|
+
container `on-surface/12`, no shadow). The input remove affordance carries
|
|
55
|
+
a 48dp-tall touch target that bleeds past the 32dp container (spec: min
|
|
56
|
+
48dp for the close icon).
|
|
57
|
+
|
|
58
|
+
## Examples
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
<Chips leftElement={<MaterialSymbol name="event" size={18} />} text="Add to calendar" />
|
|
62
|
+
<Chips variant="filter" selected={active} onClick={toggle} text="Vegan" />
|
|
63
|
+
<Chips variant="input" text="ada@example.com" onRemove={() => remove(id)} />
|
|
64
|
+
<Chips
|
|
65
|
+
avatar
|
|
66
|
+
leftElement={<Avatar height={24} name="AL" width={24} />}
|
|
67
|
+
onRemove={() => remove(id)}
|
|
68
|
+
text="Ada Lovelace"
|
|
69
|
+
variant="input"
|
|
70
|
+
/>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Gotchas
|
|
74
|
+
|
|
75
|
+
- `selected` is a display prop — manage the selection set in your app
|
|
76
|
+
(chips are presentational).
|
|
77
|
+
- `onRemove` stops propagation, so chip `onClick` doesn't fire on remove.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# DatePicker
|
|
2
|
+
|
|
3
|
+
M3 date pickers built over the library's `Dialog` / `InputOutlined`
|
|
4
|
+
primitives, controllable and presentational (native `Date`, no date
|
|
5
|
+
dependency, locale via `Intl`). Three forms:
|
|
6
|
+
|
|
7
|
+
- **`DatePicker`** — modal calendar (360×568 dialog, surface-container-high,
|
|
8
|
+
level 3, extra-large 28) with a 120dp headline header, month/year
|
|
9
|
+
navigation, a year grid, and a keyboard-input toggle. Selection is drafted
|
|
10
|
+
and committed on **OK**.
|
|
11
|
+
- **`DatePicker.Docked`** — an outlined field with a calendar icon that
|
|
12
|
+
opens a dropdown calendar; selection commits immediately.
|
|
13
|
+
- **`DateRangePicker`** — modal range picker (first tap sets the start, the
|
|
14
|
+
next the end; the days between highlight in secondary-container).
|
|
15
|
+
|
|
16
|
+
## Import
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import {DatePicker, DateRangePicker} from "react-material-expressive";
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## API
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
interface DatePickerProps {
|
|
26
|
+
input?: boolean; // start in keyboard-input mode
|
|
27
|
+
isVisible: boolean;
|
|
28
|
+
labels?: DatePickerLabels; // every user-facing string (see below)
|
|
29
|
+
locale?: string; // Intl locale; default system
|
|
30
|
+
max?: Date | null;
|
|
31
|
+
min?: Date | null;
|
|
32
|
+
onClose: () => void;
|
|
33
|
+
onConfirm?: (date: Date | null) => void; // OK
|
|
34
|
+
value?: Date | null;
|
|
35
|
+
weekStartsOn?: number; // 0 = Sunday … 6 = Saturday (default 0)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// All text lives in `labels` — each key optional, English defaults shown.
|
|
39
|
+
interface DatePickerLabels {
|
|
40
|
+
supporting?: ReactNode; // "Select date"
|
|
41
|
+
cancel?: ReactNode; // "Cancel"
|
|
42
|
+
confirm?: ReactNode; // "OK"
|
|
43
|
+
inputLabel?: string; // "Date"
|
|
44
|
+
inputPlaceholder?: string; // "mm/dd/yyyy"
|
|
45
|
+
switchToCalendar?: string; // aria (input→calendar toggle)
|
|
46
|
+
switchToInput?: string; // aria (calendar→input toggle)
|
|
47
|
+
previousMonth?: string; // aria "Previous month"
|
|
48
|
+
nextMonth?: string; // aria "Next month"
|
|
49
|
+
selectYear?: string; // aria "Select year"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface DatePickerDockedProps {
|
|
53
|
+
className?: string;
|
|
54
|
+
labels?: DatePickerDockedLabels; // { field, openCalendar, previousMonth, nextMonth, selectYear }
|
|
55
|
+
locale?: string;
|
|
56
|
+
max?: Date | null;
|
|
57
|
+
min?: Date | null;
|
|
58
|
+
onChange?: (date: Date | null) => void;
|
|
59
|
+
value?: Date | null;
|
|
60
|
+
weekStartsOn?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type DateRange = [Date | null, Date | null];
|
|
64
|
+
interface DateRangePickerProps {
|
|
65
|
+
isVisible: boolean;
|
|
66
|
+
// labels: { supporting, cancel, confirm, start, end, previousMonth, nextMonth, selectYear }
|
|
67
|
+
labels?: DateRangePickerLabels;
|
|
68
|
+
locale?: string;
|
|
69
|
+
max?: Date | null;
|
|
70
|
+
min?: Date | null;
|
|
71
|
+
onClose: () => void;
|
|
72
|
+
onConfirm?: (range: DateRange) => void;
|
|
73
|
+
value?: DateRange;
|
|
74
|
+
weekStartsOn?: number;
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Example
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
const [open, setOpen] = useState(false);
|
|
82
|
+
const [date, setDate] = useState<Date | null>(null);
|
|
83
|
+
|
|
84
|
+
<Button onClick={() => setOpen(true)}>Pick a date</Button>
|
|
85
|
+
<DatePicker
|
|
86
|
+
isVisible={open}
|
|
87
|
+
onClose={() => setOpen(false)}
|
|
88
|
+
onConfirm={setDate}
|
|
89
|
+
value={date}
|
|
90
|
+
/>;
|
|
91
|
+
|
|
92
|
+
// Docked
|
|
93
|
+
<DatePicker.Docked value={date} onChange={setDate} />;
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Gotchas
|
|
97
|
+
|
|
98
|
+
- The modal drafts the selection internally and only calls `onConfirm` on
|
|
99
|
+
**OK** — Cancel/scrim discards. The docked variant commits on click.
|
|
100
|
+
- Weekday labels and the headline are localized via `Intl`; set `locale`
|
|
101
|
+
for a fixed locale, `weekStartsOn` for the first column.
|
|
102
|
+
- Every user-facing string (visible text **and** the control aria-labels)
|
|
103
|
+
is in `labels`, e.g. `labels={{cancel: "Cancelar", confirm: "Aceptar"}}`;
|
|
104
|
+
unset keys keep the English defaults.
|
|
105
|
+
- `min`/`max` disable out-of-range days.
|
|
106
|
+
|
|
107
|
+
## Accepted gaps (criterion)
|
|
108
|
+
|
|
109
|
+
- The range picker uses the standard navigable calendar with in-range
|
|
110
|
+
highlighting rather than the Android full-screen scrolling-months layout.
|
|
111
|
+
- Day-grid arrow-key navigation is not implemented (days are buttons; Tab
|
|
112
|
+
reaches them). The input mode covers fast keyboard entry.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Dialog
|
|
2
|
+
|
|
3
|
+
M3 modal dialog: shape extra-large (28), 280–560dp wide,
|
|
4
|
+
surface-container-high container, 32% scrim, elevation 3. Mounts/unmounts
|
|
5
|
+
with fade enter/exit animations and locks body scroll while open.
|
|
6
|
+
|
|
7
|
+
## Import
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import {Dialog} from "react-material-expressive";
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## API
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
interface DialogProps {
|
|
17
|
+
children?: ReactNode;
|
|
18
|
+
className?: string;
|
|
19
|
+
dismissable?: boolean; // scrim click + Escape close (default true)
|
|
20
|
+
isVisible: boolean;
|
|
21
|
+
onClose: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Sub-parts
|
|
25
|
+
Dialog.Header: {headline?: ReactNode; icon?: ReactNode; text?: ReactNode; className?}
|
|
26
|
+
Dialog.Body: ComponentProps<"div"> // body-medium, on-surface-variant
|
|
27
|
+
Dialog.Footer: ComponentProps<"div"> // right-aligned action row
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`Dialog.Header` renders an optional 24px hero icon (secondary color, which
|
|
31
|
+
centers the header per M3), an `headline-small` headline and
|
|
32
|
+
`body-medium` supporting text. The headline also gives the dialog its
|
|
33
|
+
accessible name: `Dialog` wires `aria-labelledby` to the `Dialog.Header`
|
|
34
|
+
headline through an internal context.
|
|
35
|
+
|
|
36
|
+
## Accessibility & keyboard
|
|
37
|
+
|
|
38
|
+
The panel is a `div` (not the native `<dialog>`), so it replicates
|
|
39
|
+
`showModal()` behaviour manually:
|
|
40
|
+
|
|
41
|
+
- `role="dialog"` + `aria-modal="true"`, named by the `Dialog.Header`
|
|
42
|
+
headline via `aria-labelledby`. Provide a headline for an accessible
|
|
43
|
+
name; if you compose a custom header instead, name it yourself.
|
|
44
|
+
- On open, focus moves into the dialog — to a child carrying the
|
|
45
|
+
`autofocus` attribute if present, otherwise the panel itself — and is
|
|
46
|
+
restored to the previously focused element on close.
|
|
47
|
+
- `Tab` / `Shift+Tab` are trapped inside the panel and wrap around.
|
|
48
|
+
- `Escape` closes the dialog when `dismissable` (default), same as a
|
|
49
|
+
scrim click.
|
|
50
|
+
|
|
51
|
+
## Example
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const [open, setOpen] = useState(false);
|
|
55
|
+
|
|
56
|
+
<Dialog isVisible={open} onClose={() => setOpen(false)}>
|
|
57
|
+
<Dialog.Header
|
|
58
|
+
headline="Reset settings?"
|
|
59
|
+
icon={<MaterialSymbol name="restart_alt" />}
|
|
60
|
+
text="This cannot be undone."
|
|
61
|
+
/>
|
|
62
|
+
<Dialog.Footer>
|
|
63
|
+
<Button variant="text" onClick={() => setOpen(false)}>
|
|
64
|
+
Cancel
|
|
65
|
+
</Button>
|
|
66
|
+
<Button variant="text" onClick={confirm}>
|
|
67
|
+
Accept
|
|
68
|
+
</Button>
|
|
69
|
+
</Dialog.Footer>
|
|
70
|
+
</Dialog>;
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Gotchas
|
|
74
|
+
|
|
75
|
+
- Controlled-only: there is no internal open state.
|
|
76
|
+
- The exit animation runs 150ms after `isVisible` goes false; the
|
|
77
|
+
component stays mounted until it finishes (`useDismissable`).
|
|
78
|
+
- Long content scrolls inside the panel as a whole (max-height
|
|
79
|
+
100vh − 48). This is the only modeled variant: there are no per-section
|
|
80
|
+
scroll dividers, no full-screen dialog and no `alertdialog` type — the
|
|
81
|
+
M3 spec lists those as optional/separate, and the basic dialog is
|
|
82
|
+
faithful to it value-for-value (verified 2026-06-12 against
|
|
83
|
+
m3.material.io and @material/web).
|