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,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).