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,67 @@
1
+ # Loading
2
+
3
+ Icon-sized circular **indeterminate** spinner, colored with currentColor
4
+ (defaults to primary). M3 sizing: **40px** diameter (the non-wavy
5
+ `CircularProgressIndicatorTokens.Size`; 48 is only the wavy baseline), 4px
6
+ stroke. The animation is an SVG arc with round caps that grows ~10°↔270°
7
+ while it spins — the **same `_Spinner` `Circle` renders when
8
+ indeterminate**.
9
+
10
+ `Loading` is not a separate Material component: visually it **is**
11
+ `<Circle indeterminate />`, kept as a convenience that inherits
12
+ `currentColor` and uses `role="status"` so it drops cleanly inside buttons,
13
+ text and tight inline contexts. For the distinct shape-morphing M3
14
+ Expressive component (SoftBurst → … → Oval) see
15
+ [LoadingIndicator](LoadingIndicator.md); for determinate/labelled progress
16
+ use [Circle](Progress.md).
17
+
18
+ ## `Loading` vs `<Circle indeterminate />`
19
+
20
+ They render the **same spinner** (shared `_Spinner`, identical animation
21
+ and 40px default), so pick by intent, not by looks:
22
+
23
+ | | `Loading` | `Circle indeterminate` |
24
+ | -------- | ------------------------------------------ | --------------------------------------------------------------------- |
25
+ | ARIA | `role="status"` (live region — "loading…") | `role="progressbar"` (busy, no value) |
26
+ | Color | `currentColor` — recolor via `className` | fixed `primary` (the progress color) |
27
+ | Lives in | `elements` (inline spinner) | `components`, with `Progress` (`value`, `wavy`, track, center label…) |
28
+
29
+ Reach for `Loading` as an **inline "busy" spinner** you drop in a button or
30
+ a sentence and tint with `currentColor`; reach for [`Circle`](Progress.md)
31
+ `indeterminate` when it is a **progress indicator** — same component family
32
+ as the determinate / wavy / labelled progress.
33
+
34
+ ## Import
35
+
36
+ ```tsx
37
+ import {Loading} from "react-material-expressive";
38
+ ```
39
+
40
+ ## API
41
+
42
+ ```ts
43
+ interface LoadingProps {
44
+ className?: string; // e.g. "text-tertiary"
45
+ labels?: LoadingLabels; // accessible names
46
+ size?: number; // px, default 40 (M3 non-wavy circular Size)
47
+ strokeWidth?: number; // px, default 4
48
+ }
49
+
50
+ interface LoadingLabels {
51
+ label?: string; // aria-label (default "Loading")
52
+ }
53
+ ```
54
+
55
+ ## Examples
56
+
57
+ ```tsx
58
+ <Loading />
59
+ <Loading className="text-on-surface-variant" size={24} />
60
+ ```
61
+
62
+ ## Gotchas
63
+
64
+ - `role="status"` announces it; pass a contextual `labels.label`
65
+ ("Loading messages").
66
+ - For determinate progress use [Progress / Circle](Progress.md).
67
+ - **BREAKING:** the `label` prop was replaced by `labels={{label}}`.
@@ -0,0 +1,64 @@
1
+ # LoadingIndicator
2
+
3
+ M3 Expressive loading indicator: a 38dp shape morphing through the
4
+ Compose sequence (SoftBurst → Cookie9Sided → Pentagon → Pill → Sunny →
5
+ Cookie4Sided → Oval) on 650ms ticks that replay the Compose morph spring
6
+ (through the target at speed, 14% overshoot at the brake — the visible size pulse — settle and dwell; damping 0.6/stiffness 200), plus a
7
+ +90° rotation kick per morph on top of the 4666ms global rotation,
8
+ inside a 48dp container. The default paints only the `primary` shape;
9
+ `contained` adds the fully-round `primary-container` circle with an
10
+ `on-primary-container` indicator (`md.comp.loading-indicator` tokens).
11
+ The shape morph runs via SVG/SMIL — no JavaScript, works in every
12
+ engine. Indeterminate by design: for determinate progress use
13
+ `Progress`/`Circle`.
14
+
15
+ ## Import
16
+
17
+ ```tsx
18
+ import {LoadingIndicator} from "react-material-expressive";
19
+ ```
20
+
21
+ ## API
22
+
23
+ ```ts
24
+ interface LoadingIndicatorProps {
25
+ className?: string;
26
+ contained?: boolean; // 48dp primary-container circle
27
+ labels?: LoadingIndicatorLabels; // accessible names
28
+ }
29
+
30
+ interface LoadingIndicatorLabels {
31
+ label?: string; // aria-label (default "Loading")
32
+ }
33
+ ```
34
+
35
+ ## Example
36
+
37
+ ```tsx
38
+ {
39
+ isLoading ? <LoadingIndicator /> : <Results items={items} />;
40
+ }
41
+ {
42
+ isSyncing ? <LoadingIndicator contained labels={{label: "Syncing"}} /> : null;
43
+ }
44
+ ```
45
+
46
+ ## Gotchas
47
+
48
+ - Use it for short waits (per spec); longer or measurable processes
49
+ should use the progress indicators instead.
50
+ - **BREAKING:** the `aria-label` prop was replaced by `labels={{label}}`.
51
+ - Colors resolve through `--md-sys-color-*`, so themes apply
52
+ automatically.
53
+ - Motion and shapes are ported 1:1 from the Compose LoadingIndicator
54
+ source and `androidx.graphics.shapes`: the keyframe paths are the
55
+ **exact** MaterialShapes polygons (`RoundedPolygon` corner-rounding +
56
+ `Morph` feature-matching), generated by the dev-only
57
+ `scripts/gen-loading-indicator.mjs` into `_loadingIndicatorShapes.ts`
58
+ (the only artifact shipped). Regenerate with the script — never
59
+ hand-edit the data file (it is in `.prettierignore` so the generator
60
+ stays the single source of truth).
61
+ - The `<svg>` is the 48dp container (the Compose draw surface), not the
62
+ 38dp active size: the shapes scale to the 38dp nominal but the spring
63
+ overshoot pulses them larger (~46px peak), and a 38px box would clip
64
+ the pulse since an `<svg>` defaults to `overflow:hidden`.
@@ -0,0 +1,48 @@
1
+ # MaterialSymbol
2
+
3
+ Zero-dependency Material Symbols glyph: renders a ligature `<span>` with
4
+ `font-variation-settings`. It only displays if the consumer loaded the
5
+ corresponding Material Symbols variable font — the library bundles no
6
+ font.
7
+
8
+ ## Import
9
+
10
+ ```tsx
11
+ import {MaterialSymbol} from "react-material-expressive";
12
+ ```
13
+
14
+ Load the font (once, in your app):
15
+
16
+ ```html
17
+ <link
18
+ href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=block"
19
+ rel="stylesheet" />
20
+ ```
21
+
22
+ ## API
23
+
24
+ ```ts
25
+ interface MaterialSymbolProps extends Omit<ComponentProps<"span">, "children"> {
26
+ fill?: boolean; // FILL axis
27
+ grade?: number; // GRAD (-25..200)
28
+ name: string; // ligature, e.g. "favorite"
29
+ opticalSize?: number; // opsz (defaults to size)
30
+ size?: number; // px, default 24
31
+ variant?: "rounded" | "outlined" | "sharp"; // must match the loaded font
32
+ weight?: number; // wght (100..700)
33
+ }
34
+ ```
35
+
36
+ ## Examples
37
+
38
+ ```tsx
39
+ <MaterialSymbol name="favorite" />
40
+ <MaterialSymbol fill name="favorite" className="text-error" />
41
+ <MaterialSymbol name="settings" size={18} weight={500} />
42
+ ```
43
+
44
+ ## Gotchas
45
+
46
+ - `aria-hidden` by default — give the parent control an `aria-label`.
47
+ - Components accept ANY ReactNode icon; this helper is just the
48
+ recommended zero-dep path.
@@ -0,0 +1,46 @@
1
+ # MediaFrame
2
+
3
+ Presentational frame for media: size/aspect/radius/overflow plus an
4
+ optional placeholder, on a surface-container-highest backdrop. Renders no
5
+ image of its own — pair it with `Img`, `Video` or any media node.
6
+
7
+ ## Import
8
+
9
+ ```tsx
10
+ import {MediaFrame} from "react-material-expressive";
11
+ ```
12
+
13
+ ## API
14
+
15
+ ```ts
16
+ interface MediaFrameProps {
17
+ aspect?: number | string;
18
+ children?: ReactNode; // the media
19
+ className?: string;
20
+ height?: number | string;
21
+ width?: number | string;
22
+ onClick?: MouseEventHandler<HTMLDivElement>;
23
+ overflow?: boolean; // clip to shape (default true)
24
+ placeholder?: ReactNode; // centered fallback
25
+ radius?: number | string;
26
+ size?: number | string;
27
+ style?: CSSProperties;
28
+ }
29
+ ```
30
+
31
+ ## Example
32
+
33
+ ```tsx
34
+ <MediaFrame
35
+ aspect={16 / 9}
36
+ radius={16}
37
+ width={320}
38
+ placeholder={<MaterialSymbol name="movie" size={32} />}>
39
+ <Video className="absolute inset-0 size-full" src="/clip.mp4" />
40
+ </MediaFrame>
41
+ ```
42
+
43
+ ## Gotchas
44
+
45
+ - Children should usually be absolutely positioned (`absolute inset-0
46
+ size-full`) to fill the frame.
@@ -0,0 +1,149 @@
1
+ # Menu
2
+
3
+ The M3 Expressive **vertical menu** surface and behaviour, used as the
4
+ shared primitive behind the anchored menus (`Dropdown`, and — as they
5
+ migrate — `OverflowMenu` / `ToggleThemeMenu`). It is **anchorless**: the
6
+ host positions and mounts it; `Menu` owns the surface, the reveal
7
+ animation and the WAI-ARIA menu keyboard.
8
+
9
+ Backs `Dropdown`, `OverflowMenu`, `ToggleThemeMenu` and the `SplitButton`
10
+ menu (thin trigger/anchor adapters over this primitive).
11
+
12
+ Covered: surface, items (`selected`, `badge`, leading/trailing slots),
13
+ group surfaces (`Menu.Group` — the "with gap" grouping), section labels
14
+ (`Menu.Label`), dividers (`Menu.Divider`), **submenus (`Menu.Sub`)**, the
15
+ standard + vibrant schemes and the full menu keyboard. Accepted gap: a
16
+ 2-line supporting-text item (not a kit building block — use the item
17
+ content).
18
+
19
+ ## Import
20
+
21
+ ```tsx
22
+ import {Menu} from "react-material-expressive";
23
+ ```
24
+
25
+ ## API
26
+
27
+ ```ts
28
+ interface MenuProps {
29
+ children?: ReactNode; // Menu.Item list
30
+ className?: string; // host positioning
31
+ exiting?: boolean; // true while the close animation plays
32
+ onClose?: () => void; // items call it on activation; Escape/Tab too
33
+ up?: boolean; // reveal upward (anchored above the trigger)
34
+ vibrant?: boolean; // tertiary-based scheme instead of standard
35
+ }
36
+
37
+ interface MenuItemProps {
38
+ badge?: boolean | string; // trailing error badge (string = count/label)
39
+ children?: ReactNode;
40
+ className?: string;
41
+ disabled?: boolean;
42
+ keepOpen?: boolean; // don't close the menu on activation
43
+ label?: ReactNode;
44
+ leftElement?: ReactNode; // 20px leading box (use icon size 20)
45
+ onClick?: MouseEventHandler<HTMLButtonElement>;
46
+ rightElement?: ReactNode; // trailing shortcut text (label-large) or 20px icon
47
+ selected?: boolean; // tertiary-container fill + corner-medium
48
+ }
49
+
50
+ interface MenuGroupProps {
51
+ children?: ReactNode; // Menu.Item list (and nested Menu.Sub)
52
+ className?: string;
53
+ }
54
+
55
+ interface MenuLabelProps {
56
+ children?: ReactNode; // section label text (label-large)
57
+ className?: string;
58
+ leftElement?: ReactNode; // optional 20px leading icon
59
+ }
60
+
61
+ interface MenuDividerProps {
62
+ className?: string;
63
+ }
64
+
65
+ interface MenuSubProps {
66
+ children?: ReactNode; // submenu content (Menu.Item / nested Menu.Sub)
67
+ className?: string;
68
+ disabled?: boolean;
69
+ label?: ReactNode;
70
+ leftElement?: ReactNode; // 20px leading box
71
+ }
72
+ ```
73
+
74
+ `Menu.Sub` opens a nested menu to the side (rendered in a portal so it
75
+ escapes the surface clip): on hover, Right/Enter, or click; closes on
76
+ Left/Escape (focus returns to the trigger). A leaf activation anywhere
77
+ dismisses the whole chain. The trigger keeps the on-surface state layer
78
+ while open (the M3E active state) and shows a trailing chevron.
79
+
80
+ Group items two ways — the M3 menu anatomy lists both "Gap" and "Divider"
81
+ as optional parts:
82
+
83
+ - **With a gap** — wrap each cluster in `Menu.Group`. Each group renders as
84
+ its own surface (corner-large, its own elevation) and the menu stacks them
85
+ with a 2dp gap. First/last items round against their own group's corner.
86
+ - **With a divider / label** — interleave `Menu.Divider` (1px
87
+ `outline-variant`, inset) and `Menu.Label` (section header, 32 tall,
88
+ `on-surface-variant` / `on-tertiary-container`) as flat children of
89
+ `Menu`, separating groups within a single surface.
90
+
91
+ Either way the keyboard treats the whole menu as one list (arrows move
92
+ across groups).
93
+
94
+ ## Anatomy & behaviour
95
+
96
+ - Container: corner-large (16), `surface-container-low` (standard) or
97
+ `tertiary-container` (`vibrant`), elevation 3, width clamped 112–280dp.
98
+ With `Menu.Group` children the container is instead a transparent stack:
99
+ each group is its own surface (same tokens) and they sit 2dp apart.
100
+ - Item: 48 tall, inset state layer (corner extra-small) with `label-large`,
101
+ 20dp leading/trailing icons, 12dp padding. First/last items round their
102
+ outer corner to corner-medium (concentric with the container); `selected`
103
+ morphs the state layer to corner-medium with a tertiary fill.
104
+ - Keyboard (WAI-ARIA menu): on open the menu **surface** is focused, so no
105
+ item is pre-highlighted and the first ArrowDown/ArrowUp lands on the
106
+ first/last item; thereafter ArrowUp/Down move between enabled items
107
+ (wrapping, across groups), Home/End jump, printable keys typeahead
108
+ (matched against the item's **label** only — leading icons and trailing
109
+ shortcuts are ignored), Escape closes. (A submenu opened by keyboard
110
+ instead focuses its first item, per the WAI-ARIA submenu pattern.) Items
111
+ are `role="menuitem"` with roving `tabindex`, so they are **not** in the
112
+ Tab order on purpose — Tab moves focus out of the menu; use the arrow keys
113
+ to move between items.
114
+
115
+ ## Example
116
+
117
+ ```tsx
118
+ <Menu onClose={close}>
119
+ <Menu.Item
120
+ label="Profile"
121
+ leftElement={<MaterialSymbol name="person" size={20} />}
122
+ />
123
+ <Menu.Item label="Settings" selected onClick={openSettings} />
124
+ <Menu.Item disabled label="Workspace" />
125
+ </Menu>
126
+ ```
127
+
128
+ Grouped with a gap (separate surfaces):
129
+
130
+ ```tsx
131
+ <Menu onClose={close}>
132
+ <Menu.Group>
133
+ <Menu.Item label="Italic" />
134
+ <Menu.Item label="Bold" selected />
135
+ <Menu.Item label="Underline" />
136
+ </Menu.Group>
137
+ <Menu.Group>
138
+ <Menu.Item label="Cut" rightElement="⌘X" />
139
+ <Menu.Item label="Copy" rightElement="⌘C" />
140
+ </Menu.Group>
141
+ </Menu>
142
+ ```
143
+
144
+ ## Gotchas
145
+
146
+ - Anchorless: wrap it in a positioned element and control mount/unmount
147
+ (e.g. via `useDismissable`) yourself, or use `Dropdown` which does this.
148
+ - `selected` is visual; for single-selection semantics in a picker, the
149
+ consumer still owns the selected value.
@@ -0,0 +1,78 @@
1
+ # NavigationBar
2
+
3
+ M3 Expressive flexible navigation bar: height 64,
4
+ surface-container, 3–5 destinations. Vertical items (default) carry a
5
+ 32×56 indicator pill over the 24px icon with `label-medium` below;
6
+ `horizontal` switches to the medium-window configuration — fixed-width
7
+ 40dp pills holding icon + label, centered in the bar. On selection the
8
+ indicator springs its width and alpha from the center (one axis,
9
+ default-spatial spring) while the icon swaps to its filled variant, and
10
+ the press ripple runs inside the pill. Active colors: indicator
11
+ `secondary-container`, icon `on-secondary-container`, label `secondary`
12
+ when vertical (below the pill) but `on-secondary-container` when
13
+ `horizontal` (inside the pill, on the indicator, matching the icon);
14
+ inactive `on-surface-variant`.
15
+
16
+ ## Import
17
+
18
+ ```tsx
19
+ import {NavigationBar} from "react-material-expressive";
20
+ ```
21
+
22
+ ## API
23
+
24
+ ```ts
25
+ interface NavigationBarProps extends ComponentProps<"nav"> {
26
+ horizontal?: boolean; // 40dp icon+label pills (medium windows)
27
+ }
28
+
29
+ interface NavBarItemProps {
30
+ active?: boolean; // explicit; otherwise derived from href+currentPath
31
+ activeIcon?: ReactNode; // e.g. the filled glyph
32
+ badge?: boolean;
33
+ badgeColor?: string;
34
+ badgeText?: string;
35
+ className?: string;
36
+ currentPath?: string; // pathname from YOUR router
37
+ dotBadge?: boolean;
38
+ href?: string; // renders a native <a>
39
+ icon?: ReactNode;
40
+ label?: ReactNode;
41
+ onClick?: MouseEventHandler<HTMLElement>;
42
+ target?: string;
43
+ }
44
+ ```
45
+
46
+ ## Example
47
+
48
+ ```tsx
49
+ <NavigationBar>
50
+ <NavigationBar.Item
51
+ activeIcon={<MaterialSymbol fill name="home" />}
52
+ currentPath={pathname}
53
+ href="/home"
54
+ icon={<MaterialSymbol name="home" />}
55
+ label="Home"
56
+ />
57
+ <NavigationBar.Item
58
+ badge
59
+ badgeText="3"
60
+ href="/inbox"
61
+ currentPath={pathname}
62
+ icon={<MaterialSymbol name="inbox" />}
63
+ label="Inbox"
64
+ />
65
+ </NavigationBar>
66
+ ```
67
+
68
+ ## Gotchas
69
+
70
+ - Router-agnostic: pass `currentPath` (or control `active` yourself) and
71
+ intercept `onClick` for client-side routing.
72
+ - Items without `href` render `<button>`.
73
+ - Active items set `aria-current="page"`.
74
+ - Per spec: always pass `label` (don't remove them), use the filled icon
75
+ as `activeIcon`, bottom of the window only, compact/medium sizes —
76
+ desktop layouts should use a navigation rail.
77
+ - Vertical items split the bar width equally; horizontal items are
78
+ fixed-width and centered (the bar adds the outer margins).
@@ -0,0 +1,105 @@
1
+ # NavigationRail
2
+
3
+ M3 Expressive navigation rail. Collapsed: 96dp wide (80 with `narrow`)
4
+ on `surface`, 44dp top spacing, vertical items (32×56 indicator pill,
5
+ `label-medium` below, 4dp apart). `expanded` morphs it into the 220dp
6
+ rail that replaces the navigation drawer: the springing width
7
+ (default-spatial; fast-spatial when `modal`) drives a continuous morph —
8
+ each item is a single row pill stretched open by its label slot into a
9
+ content-hugging 56dp row (`label-large` — larger than the collapsed
10
+ `label-medium` below-label, leading icon, anchored at the 20dp side
11
+ padding — the pill wraps icon + label, it is not full width), the
12
+ below-label collapses while the side-label reveals inside the growing
13
+ pill (the active expanded label sits on the indicator, so it takes
14
+ `on-secondary-container` to match the icon — the collapsed below-label
15
+ stays `secondary` on the surface), and the rail FAB extends with its
16
+ label. The menu button (rendered when `onMenuClick` is set) toggles the
17
+ morph; the library draws its default M3 icon, swapping `menu` → `menu_open`
18
+ with `expanded` (override either glyph via `menuIcon`/`menuOpenIcon`). `modal` overlays
19
+ the expansion as a full-window drawer (fixed, spanning the available
20
+ height like the 32% scrim; surface-container, level 2, large end corners)
21
+ instead of pushing content; the overlay surface eases out in lockstep with the
22
+ width as the rail collapses, deflating smoothly into the resting rail. The selection animation matches the bar:
23
+ indicator expands from the center with the press ripple inside the pill.
24
+
25
+ ## Import
26
+
27
+ ```tsx
28
+ import {NavigationRail} from "react-material-expressive";
29
+ ```
30
+
31
+ ## API
32
+
33
+ ```ts
34
+ interface NavigationRailProps {
35
+ bottom?: ReactNode; // bottom-aligned slot
36
+ children?: ReactNode; // NavigationRail.Item list
37
+ className?: string;
38
+ expanded?: boolean; // 220dp morph (drive from the menu button)
39
+ fabIcon?: ReactNode; // rail FAB (extends with fabLabel when expanded)
40
+ fabLabel?: ReactNode;
41
+ labels?: NavigationRailLabels; // menu button aria-label (expand/collapse)
42
+ menuIcon?: ReactNode; // override the collapsed glyph (default M3 menu)
43
+ menuOpenIcon?: ReactNode; // override the expanded glyph (default M3 menu_open)
44
+ modal?: boolean; // expansion overlays content with a scrim
45
+ narrow?: boolean; // 80dp collapsed width
46
+ onFabClick?: MouseEventHandler<HTMLButtonElement>;
47
+ onClose?: () => void; // modal dismiss (scrim click / Escape)
48
+ onMenuClick?: MouseEventHandler<HTMLButtonElement>; // renders the menu button (lib draws menu/menu_open)
49
+ }
50
+ interface NavigationRailLabels {
51
+ expand?: string; // menu aria-label while collapsed. Default "Expand"
52
+ collapse?: string; // menu aria-label while expanded. Default "Collapse"
53
+ }
54
+ // NavigationRail.Item: same props as NavigationBar.Item
55
+ ```
56
+
57
+ ## Example
58
+
59
+ ```tsx
60
+ const [expanded, setExpanded] = useState(false);
61
+
62
+ <NavigationRail
63
+ expanded={expanded}
64
+ fabIcon={<MaterialSymbol name="edit" />}
65
+ fabLabel="Compose"
66
+ onFabClick={compose}
67
+ onMenuClick={() => setExpanded((value) => !value)}>
68
+ <NavigationRail.Item
69
+ activeIcon={<MaterialSymbol fill name="inbox" />}
70
+ currentPath={pathname}
71
+ href="/inbox"
72
+ icon={<MaterialSymbol name="inbox" />}
73
+ label="Inbox"
74
+ />
75
+ <NavigationRail.Item
76
+ currentPath={pathname}
77
+ href="/sent"
78
+ icon={<MaterialSymbol name="send" />}
79
+ label="Sent"
80
+ />
81
+ </NavigationRail>;
82
+ ```
83
+
84
+ ## Gotchas
85
+
86
+ - The rail is `h-full` — give its parent (or the rail via `className`) a
87
+ height. The expanded width can be tuned with the
88
+ `--rail-expanded-width` custom property (spec range 220–360).
89
+ - `expanded` is plain-controlled; provide `onMenuClick` to render the menu
90
+ button. The library draws the default `menu` ↔ `menu_open` glyph itself
91
+ (override either with `menuIcon`/`menuOpenIcon`), sets `aria-expanded`,
92
+ and uses `labels.expand`/`labels.collapse` for the aria-label. Wire
93
+ `onMenuClick` to flip `expanded`. (Breaking: the old `menu` slot was
94
+ removed in favor of `onMenuClick`.)
95
+ - With `modal`, the collapsed footprint stays in the layout while the
96
+ expansion overlays the content as a full-window drawer (it spans the
97
+ whole window height, like the scrim — give the page that height) — wire
98
+ `onClose` for scrim/Escape.
99
+ - The rail FAB is coplanar (no shadow) and defaults to
100
+ `primary-container`; recolor via `className` if needed.
101
+ - Each item renders one pill (no duplicated layouts): the side-label is
102
+ `aria-hidden` and the accessible name comes from the below-label.
103
+ - Per spec: 3–7 destinations, vertical rails sit opposite the content
104
+ edge with ≥24dp margins on large screens, and the rail stays fixed
105
+ while content scrolls vertically.
@@ -0,0 +1,65 @@
1
+ # OverflowMenu
2
+
3
+ M3 contextual menu anchored to a corner of its trigger (default
4
+ bottom-right): shape extra-small (4), surface-container, elevation 2, 48dp items
5
+ with optional badges.
6
+
7
+ ## Import
8
+
9
+ ```tsx
10
+ import {OverflowMenu} from "react-material-expressive";
11
+ ```
12
+
13
+ ## API
14
+
15
+ ```ts
16
+ interface OverflowMenuProps {
17
+ bottomLeft?: boolean;
18
+ bottomRight?: boolean;
19
+ topLeft?: boolean;
20
+ topRight?: boolean;
21
+ children?: ReactNode; // trigger (usually an IconButton)
22
+ className?: string;
23
+ menu?: ReactNode; // OverflowMenu.Item list
24
+ menuClassName?: string;
25
+ }
26
+
27
+ interface OverflowMenuItemProps {
28
+ badge?: boolean;
29
+ badgeText?: string;
30
+ children?;
31
+ className?;
32
+ disabled?: boolean;
33
+ label?: ReactNode;
34
+ leftElement?: ReactNode;
35
+ onClick?: MouseEventHandler<HTMLButtonElement>;
36
+ rightElement?: ReactNode;
37
+ }
38
+ ```
39
+
40
+ ## Example
41
+
42
+ ```tsx
43
+ <OverflowMenu
44
+ bottomRight
45
+ menu={
46
+ <>
47
+ <OverflowMenu.Item
48
+ label="Share"
49
+ leftElement={<MaterialSymbol name="share" />}
50
+ />
51
+ <OverflowMenu.Item badge badgeText="2" label="Comments" />
52
+ </>
53
+ }>
54
+ <IconButton
55
+ aria-label="More"
56
+ icon={<MaterialSymbol name="more_vert" />}
57
+ variant="standard"
58
+ />
59
+ </OverflowMenu>
60
+ ```
61
+
62
+ ## Gotchas
63
+
64
+ - Position flags are booleans; with none set it falls back to bottomRight.
65
+ - Closes on outside click, item click and Escape.
@@ -0,0 +1,45 @@
1
+ # PerspectiveImage / PerspectiveCard
2
+
3
+ 3D tilt showcases. `PerspectiveImage` follows the cursor anywhere on the
4
+ page (hero/parallax); `PerspectiveCard` tilts only while hovered and
5
+ resets on leave. Pointer-only — static on touch devices, and the tilt is
6
+ suppressed under `prefers-reduced-motion` (the effect is purely
7
+ decorative).
8
+
9
+ ## Import
10
+
11
+ ```tsx
12
+ import {
13
+ PerspectiveCard,
14
+ PerspectiveImage,
15
+ } from "react-material-expressive";
16
+ ```
17
+
18
+ ## API
19
+
20
+ ```ts
21
+ interface PerspectiveImageProps {
22
+ children?: ReactNode;
23
+ className?: string;
24
+ intensity?: number; // deg per cursor px (default 0.03)
25
+ perspective?: number; // px (default 800)
26
+ }
27
+ // PerspectiveCardProps: same (intensity default 0.025)
28
+ ```
29
+
30
+ ## Example
31
+
32
+ ```tsx
33
+ <PerspectiveCard>
34
+ <Card variant="elevated">…</Card>
35
+ </PerspectiveCard>
36
+
37
+ <PerspectiveImage intensity={0.02}>
38
+ <div className="w-fit"><Img alt="" size={280} src={art} /></div>
39
+ </PerspectiveImage>
40
+ ```
41
+
42
+ ## Gotchas
43
+
44
+ - The transform applies to the wrapper — keep it `w-fit` so the rotation
45
+ pivots on the content, not a full-width row.