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