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,40 @@
|
|
|
1
|
+
# Switch
|
|
2
|
+
|
|
3
|
+
M3 switch: 52×32 track with 2dp outline, handle 16↔24 (28 while pressed),
|
|
4
|
+
40px state layer. Optional handle icons (the handle stays 24 with a 16px
|
|
5
|
+
icon). Controllable via `useControlled`.
|
|
6
|
+
|
|
7
|
+
## Import
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import {Switch} from "react-material-expressive";
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## API
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
interface SwitchProps extends Omit<
|
|
17
|
+
ComponentProps<"input">,
|
|
18
|
+
"type" | "size" | "children" | "checked" | "defaultChecked"
|
|
19
|
+
> {
|
|
20
|
+
checked?: boolean; // controlled
|
|
21
|
+
className?: string; // wrapping <label>
|
|
22
|
+
defaultChecked?: boolean; // uncontrolled seed
|
|
23
|
+
icon?: ReactNode; // selected handle icon (16px)
|
|
24
|
+
label?: ReactNode;
|
|
25
|
+
uncheckedIcon?: ReactNode;
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Examples
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
<Switch defaultChecked label="Wi-Fi" />
|
|
33
|
+
<Switch checked={on} onChange={(e) => setOn(e.target.checked)} label="Controlled" />
|
|
34
|
+
<Switch icon={<MaterialSymbol name="check" size={16} />} label="With icon" />
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Gotchas
|
|
38
|
+
|
|
39
|
+
- `role="switch"` is set on the input.
|
|
40
|
+
- Disabled follows M3: track `on-surface/12`, handle 38%.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Table
|
|
2
|
+
|
|
3
|
+
Data table with a horizontal-scroll wrapper, surface-container header and
|
|
4
|
+
zebra rows. Fully data-agnostic — rows come from the consumer. Sub-parts:
|
|
5
|
+
`Table.Head`, `Table.Body`, `Table.Row`, `Table.Cell`, `Table.HeaderCell`,
|
|
6
|
+
`Table.TextContainer`.
|
|
7
|
+
|
|
8
|
+
> **Note**: M3 publishes no data table component (it was retired after
|
|
9
|
+
> M2), so this one is an in-house design over M3 foundations: official
|
|
10
|
+
> color tokens, `title-small` headers, `body-medium` cells and zebra rows
|
|
11
|
+
> on `surface-container/50` instead of dividers. Rows are non-interactive
|
|
12
|
+
> by default, like Card.
|
|
13
|
+
|
|
14
|
+
## Import
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
import {Table} from "react-material-expressive";
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## API
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
interface TableProps extends ComponentProps<"table"> {
|
|
24
|
+
wrapperClassName?: string; // scroll wrapper
|
|
25
|
+
}
|
|
26
|
+
// Head/Body/Row: native thead/tbody/tr props
|
|
27
|
+
interface TableCellProps extends ComponentProps<"td"> {
|
|
28
|
+
data?: ReactNode;
|
|
29
|
+
}
|
|
30
|
+
interface TableHeaderCellProps extends ComponentProps<"th"> {
|
|
31
|
+
data?: ReactNode;
|
|
32
|
+
}
|
|
33
|
+
interface TextContainerProps {
|
|
34
|
+
children?;
|
|
35
|
+
className?;
|
|
36
|
+
data?;
|
|
37
|
+
width?: string;
|
|
38
|
+
} // default w-[300px]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Example
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
<Table>
|
|
45
|
+
<Table.Head>
|
|
46
|
+
<Table.Row>
|
|
47
|
+
<Table.HeaderCell>Name</Table.HeaderCell>
|
|
48
|
+
<Table.HeaderCell>Role</Table.HeaderCell>
|
|
49
|
+
</Table.Row>
|
|
50
|
+
</Table.Head>
|
|
51
|
+
<Table.Body>
|
|
52
|
+
{rows.map((row) => (
|
|
53
|
+
<Table.Row key={row.id}>
|
|
54
|
+
<Table.Cell>{row.name}</Table.Cell>
|
|
55
|
+
<Table.Cell>
|
|
56
|
+
<Table.TextContainer>{row.longText}</Table.TextContainer>
|
|
57
|
+
</Table.Cell>
|
|
58
|
+
</Table.Row>
|
|
59
|
+
))}
|
|
60
|
+
</Table.Body>
|
|
61
|
+
</Table>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Gotchas
|
|
65
|
+
|
|
66
|
+
- Wrap long copy in `Table.TextContainer` to keep a readable measure.
|
|
67
|
+
- Cells accept `data` as a children fallback (handy when mapping objects).
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# TabsPrimary / TabsSecondary
|
|
2
|
+
|
|
3
|
+
M3 tabs with `title-small` labels and proper a11y roles. Primary: 48dp
|
|
4
|
+
container (64 with 24px icons) and a 3px content-width indicator (inset 2dp
|
|
5
|
+
each side, min length 24dp, fully-rounded top corners). Secondary: 48dp with
|
|
6
|
+
inline 24px icons and a 2px full-width indicator. Active label/icon is
|
|
7
|
+
`primary` (primary) / `on-surface` (secondary); inactive is
|
|
8
|
+
`on-surface-variant` and darkens to `on-surface` on hover/focus. The
|
|
9
|
+
indicator slides between tabs with the default-spatial spring (FLIP).
|
|
10
|
+
Controllable; each tab may carry panel `content`.
|
|
11
|
+
|
|
12
|
+
## Import
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import {TabsPrimary, TabsSecondary} from "react-material-expressive";
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## API
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
interface TabItem {
|
|
22
|
+
content?: ReactNode; // panel rendered below when selected
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
header?: ReactNode;
|
|
25
|
+
icon?: ReactNode;
|
|
26
|
+
id: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface TabsPrimaryProps {
|
|
30
|
+
className?: string;
|
|
31
|
+
defaultSelected?: string; // falls back to the first tab
|
|
32
|
+
onChange?: (id: string) => void;
|
|
33
|
+
panelClassName?: string;
|
|
34
|
+
selected?: string; // controlled
|
|
35
|
+
tabs: TabItem[];
|
|
36
|
+
}
|
|
37
|
+
// TabsSecondaryProps: identical
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Example
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
<TabsPrimary
|
|
44
|
+
onChange={setSection}
|
|
45
|
+
tabs={[
|
|
46
|
+
{
|
|
47
|
+
content: <Flights />,
|
|
48
|
+
header: "Flights",
|
|
49
|
+
icon: <MaterialSymbol name="flight" />,
|
|
50
|
+
id: "f",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
content: <Hotels />,
|
|
54
|
+
header: "Hotels",
|
|
55
|
+
icon: <MaterialSymbol name="hotel" />,
|
|
56
|
+
id: "h",
|
|
57
|
+
},
|
|
58
|
+
]}
|
|
59
|
+
/>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Gotchas
|
|
63
|
+
|
|
64
|
+
- Omit `content` to use the tabs as pure navigation and render panels
|
|
65
|
+
yourself (wire `onChange`).
|
|
66
|
+
- Primary's indicator hugs the icon+label width per M3 (it sits inside the
|
|
67
|
+
content wrapper).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# TextElement
|
|
2
|
+
|
|
3
|
+
Label / title / body text stack on the M3 type scale: overline
|
|
4
|
+
`label-medium` (on-surface-variant), title `title-medium` (on-surface),
|
|
5
|
+
body `body-medium` (on-surface-variant).
|
|
6
|
+
|
|
7
|
+
## Import
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import {TextElement} from "react-material-expressive";
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## API
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
interface TextElementProps {
|
|
17
|
+
body?: ReactNode;
|
|
18
|
+
bodyStyle?: string; // class override
|
|
19
|
+
className?: string;
|
|
20
|
+
label?: ReactNode;
|
|
21
|
+
labelStyle?: string;
|
|
22
|
+
title?: ReactNode;
|
|
23
|
+
titleStyle?: string;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Examples
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
<TextElement label="OVERLINE" title="Headline" body="Supporting copy" />
|
|
31
|
+
<TextElement title="Bigger" titleStyle="text-headline-small" />
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Gotchas
|
|
35
|
+
|
|
36
|
+
- Each slot renders only when provided; `*Style` props are class strings
|
|
37
|
+
merged with `cn`.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Text fields — InputOutlined / InputFilled / TextFieldOutlined / TextFieldFilled
|
|
2
|
+
|
|
3
|
+
M3 text fields: height 56, shape **extra-small** (4 — filled: top corners
|
|
4
|
+
only), `body-large` input text, `on-surface-variant` placeholder, 24px
|
|
5
|
+
leading/trailing icons (12dp from the edge, 16dp gap to the text → 52dp
|
|
6
|
+
input padding on that side). The floating label is driven by **focus/value
|
|
7
|
+
state** (not `:placeholder-shown`), so it always lands in the same place.
|
|
8
|
+
`Input*` are single-line `<input>`; `TextField*` are `<textarea>`.
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import {
|
|
14
|
+
InputFilled,
|
|
15
|
+
InputOutlined,
|
|
16
|
+
TextFieldFilled,
|
|
17
|
+
TextFieldOutlined,
|
|
18
|
+
} from "react-material-expressive";
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## API (shared shape)
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
interface InputOutlinedProps extends Omit<ComponentProps<"input">, "size"> {
|
|
25
|
+
className?: string; // wrapper
|
|
26
|
+
error?: boolean;
|
|
27
|
+
errorText?: ReactNode; // replaces supportingText while error
|
|
28
|
+
inputClassName?: string;
|
|
29
|
+
label?: string; // floating label
|
|
30
|
+
leftElement?: ReactNode; // 24px icon box
|
|
31
|
+
rightElement?: ReactNode;
|
|
32
|
+
supportingText?: ReactNode;
|
|
33
|
+
}
|
|
34
|
+
// InputFilled: same. TextField*: extends ComponentProps<"textarea"> (rows = 4).
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Controlled (`value` + `onChange`) and uncontrolled (`defaultValue`) both
|
|
38
|
+
work — the float state tracks either.
|
|
39
|
+
|
|
40
|
+
## Anatomy notes
|
|
41
|
+
|
|
42
|
+
- Outlined: the label notches the border via `fieldset/legend`, so it works
|
|
43
|
+
over any background (cards, sheets, colored sections).
|
|
44
|
+
- Filled: surface-container-highest container, hover state layer and an
|
|
45
|
+
active indicator line (1px on-surface-variant → on-surface on hover →
|
|
46
|
+
2px primary on focus, error on error).
|
|
47
|
+
- `placeholder` only shows while the label is floated (or when no label).
|
|
48
|
+
- The float animation is a transform-only tween between two label copies
|
|
49
|
+
(the @material/web technique) — font-size never interpolates, so the
|
|
50
|
+
text doesn't re-rasterize mid-motion.
|
|
51
|
+
|
|
52
|
+
## Example
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
<InputOutlined
|
|
56
|
+
label="Email"
|
|
57
|
+
leftElement={<MaterialSymbol name="mail" />}
|
|
58
|
+
placeholder="you@example.com"
|
|
59
|
+
supportingText="We never share it"
|
|
60
|
+
type="email"
|
|
61
|
+
/>
|
|
62
|
+
<InputFilled error errorText="Taken" label="Username" defaultValue="grace" />
|
|
63
|
+
<TextFieldOutlined label="Message" rows={5} />
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Gotchas
|
|
67
|
+
|
|
68
|
+
- Pass `label` for the M3 look; with only `placeholder` they behave as
|
|
69
|
+
plain fields.
|
|
70
|
+
- `disabled` follows M3 (38% content, 4% filled container).
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# TimePicker
|
|
2
|
+
|
|
3
|
+
M3 modal time picker built over the library's `Dialog`: a 256dp clock dial
|
|
4
|
+
with a draggable selector, the big HH:MM time selector, an AM/PM period
|
|
5
|
+
selector, and a keyboard-input toggle. Container surface-container-high,
|
|
6
|
+
level 3, extra-large (28). Controllable and presentational.
|
|
7
|
+
|
|
8
|
+
- **Dial** (default) — pick the hour on the clock (releasing advances to
|
|
9
|
+
minutes), then the minute. The selected field is `primary-container`; the
|
|
10
|
+
dial selector is `primary` (2dp track, 48dp handle, 8dp centre).
|
|
11
|
+
- **Input** (`input`) — two editable fields (display-medium, focus
|
|
12
|
+
`primary-container` + 2dp primary outline) plus the AM/PM selector.
|
|
13
|
+
|
|
14
|
+
Selection is drafted and committed on **OK**.
|
|
15
|
+
|
|
16
|
+
## Import
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import {TimePicker, type TimeValue} from "react-material-expressive";
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## API
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
interface TimeValue {
|
|
26
|
+
hours: number; // 0–23
|
|
27
|
+
minutes: number; // 0–59
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface TimePickerProps {
|
|
31
|
+
input?: boolean; // start in keyboard-input mode
|
|
32
|
+
isVisible: boolean;
|
|
33
|
+
labels?: TimePickerLabels; // every user-facing string (see below)
|
|
34
|
+
onClose: () => void;
|
|
35
|
+
onConfirm?: (value: TimeValue) => void; // OK
|
|
36
|
+
value?: TimeValue; // default { hours: 12, minutes: 0 }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// All text lives in `labels` — each key optional, English defaults shown.
|
|
40
|
+
interface TimePickerLabels {
|
|
41
|
+
supporting?: ReactNode; // "Select time"
|
|
42
|
+
cancel?: ReactNode; // "Cancel"
|
|
43
|
+
confirm?: ReactNode; // "OK"
|
|
44
|
+
am?: string; // "AM"
|
|
45
|
+
pm?: string; // "PM"
|
|
46
|
+
switchToDial?: string; // aria (input→dial toggle)
|
|
47
|
+
switchToInput?: string; // aria (dial→input toggle)
|
|
48
|
+
hour?: string; // aria "Hour"
|
|
49
|
+
minute?: string; // aria "Minute"
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Example
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
const [open, setOpen] = useState(false);
|
|
57
|
+
const [time, setTime] = useState<TimeValue>({hours: 10, minutes: 30});
|
|
58
|
+
|
|
59
|
+
<Button onClick={() => setOpen(true)}>Pick a time</Button>
|
|
60
|
+
<TimePicker
|
|
61
|
+
isVisible={open}
|
|
62
|
+
onClose={() => setOpen(false)}
|
|
63
|
+
onConfirm={setTime}
|
|
64
|
+
value={time}
|
|
65
|
+
/>;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Gotchas
|
|
69
|
+
|
|
70
|
+
- `value.hours` is 24-hour (0–23); the dial/fields display 12-hour with the
|
|
71
|
+
AM/PM selector, which rewrites `hours` by ±12.
|
|
72
|
+
- The dial is pointer-driven (tap or drag a number). The keyboard-input
|
|
73
|
+
mode is the accessible path for exact entry.
|
|
74
|
+
- Every user-facing string (visible text **and** aria-labels) is in
|
|
75
|
+
`labels`, e.g. `labels={{cancel: "Cancelar", am: "a.m.", pm: "p.m."}}`;
|
|
76
|
+
unset keys keep the English defaults.
|
|
77
|
+
|
|
78
|
+
## Accepted gaps (criterion)
|
|
79
|
+
|
|
80
|
+
- 12-hour with AM/PM only; the 24-hour dial (inner ring) is not
|
|
81
|
+
implemented.
|
|
82
|
+
- The dial selection is pointer-only; there is no arrow-key rotation of the
|
|
83
|
+
hand (use input mode for keyboard).
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# ToggleTheme / ToggleThemeMenu
|
|
2
|
+
|
|
3
|
+
Prebuilt theme controls. `ToggleTheme` flips
|
|
4
|
+
light↔dark; `ToggleThemeMenu` opens an OverflowMenu listing the built-in
|
|
5
|
+
themes (light/dark) or a custom set, marking the active one.
|
|
6
|
+
|
|
7
|
+
## Import
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import {ToggleTheme, ToggleThemeMenu} from "react-material-expressive";
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## API
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
interface ToggleThemeProps {
|
|
17
|
+
className?: string;
|
|
18
|
+
darkIcon?: ReactNode; // shown while light (click → dark)
|
|
19
|
+
label?: boolean; // show the current theme name
|
|
20
|
+
labels?: ToggleThemeLabels; // accessible names
|
|
21
|
+
lightIcon?: ReactNode; // shown while dark (click → light)
|
|
22
|
+
variant?: IconButtonVariant; // default "tonal"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ToggleThemeLabels {
|
|
26
|
+
toLight?: string; // aria-label while dark (default "Switch to light theme")
|
|
27
|
+
toDark?: string; // aria-label while light (default "Switch to dark theme")
|
|
28
|
+
name?: string; // visible suffix when `label` is shown (default "theme")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ToggleThemeMenuProps {
|
|
32
|
+
className?: string;
|
|
33
|
+
icon?: ReactNode; // trigger icon (default palette)
|
|
34
|
+
label?: boolean;
|
|
35
|
+
labels?: ToggleThemeMenuLabels; // accessible names
|
|
36
|
+
themes?: {id: "light" | "dark"; label: string}[];
|
|
37
|
+
variant?: IconButtonVariant;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ToggleThemeMenuLabels {
|
|
41
|
+
trigger?: string; // trigger aria-label (default "Choose theme")
|
|
42
|
+
name?: string; // visible suffix when `label` is shown (default "theme")
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Example
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
<ToggleTheme label />
|
|
50
|
+
<ToggleThemeMenu themes={[{id: "light", label: "Claro"}, {id: "dark", label: "Oscuro"}]} />
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Gotchas
|
|
54
|
+
|
|
55
|
+
- They mutate `data-theme`/`.dark` on `<html>` and persist to
|
|
56
|
+
`localStorage("md-theme")`.
|
|
57
|
+
- Default icons are inline SVGs; pass your own for brand consistency.
|
|
58
|
+
- **Not an M3 component.** Theme switching has no Material 3 spec — these are
|
|
59
|
+
app-level helpers. `ToggleTheme` is an [IconButton](IconButton.md) (default
|
|
60
|
+
`tonal`) whose icon/`aria-label` swap with the resolved theme;
|
|
61
|
+
`ToggleThemeMenu` is an [OverflowMenu](OverflowMenu.md) over the shared M3E
|
|
62
|
+
[vertical Menu](Menu.md). All M3 fidelity (sizing, shape morph, state
|
|
63
|
+
layers, menu visuals) is inherited from those components.
|
|
64
|
+
- `ToggleThemeMenu` marks the active theme **visually only** (the item turns
|
|
65
|
+
`tertiary-container`, no checkmark) — same convention as `Menu.Item`
|
|
66
|
+
`selected`; there is no `aria-checked`/`aria-current`. Pass a custom
|
|
67
|
+
`themes` list when shipping custom token sets (`id` must match the theme
|
|
68
|
+
you set via tokens).
|
|
69
|
+
- The theme **names** ("Light"/"Dark") are translated through the existing
|
|
70
|
+
`themes` prop (an `{id, label}` list), not through `labels` — `labels` only
|
|
71
|
+
covers the trigger aria-label and the visible "theme" suffix.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# DockedToolbar and FloatingToolbar
|
|
2
|
+
|
|
3
|
+
M3 Expressive toolbars (`md.comp.toolbar` tokens, DSDB v34): frequently
|
|
4
|
+
used actions for the current page. Two variants:
|
|
5
|
+
|
|
6
|
+
- **DockedToolbar** — spans the full window width, 64dp high, square
|
|
7
|
+
corners, `surface-container`, 16dp edge paddings and a 32dp default gap
|
|
8
|
+
(the spec's max-spacing; min is 4). Bottom of the window only; successor
|
|
9
|
+
of the baseline bottom app bar. FABs placed inside sit flat.
|
|
10
|
+
- **FloatingToolbar** — a 64dp pill (8dp paddings, 4dp gap, shape full,
|
|
11
|
+
elevation level 3) floating above the content with a minimum 16dp screen
|
|
12
|
+
margin (24dp when vertical). Can pair with a FAB (8dp gap) and collapse
|
|
13
|
+
on scroll.
|
|
14
|
+
|
|
15
|
+
Both support the **standard** (`surface-container`, low emphasis) and
|
|
16
|
+
**vibrant** (`primary-container`, high emphasis) color schemes. Ghost
|
|
17
|
+
buttons inside (`IconButton variant="standard"`, `Button variant="text"`)
|
|
18
|
+
follow the toolbar token sets automatically: selected (`aria-pressed`,
|
|
19
|
+
e.g. the IconButton `selected` toggle) becomes `secondary-container` /
|
|
20
|
+
`on-secondary-container` in standard and `surface-container` /
|
|
21
|
+
`on-surface` in vibrant. Unselected content is `on-surface-variant` in
|
|
22
|
+
standard and `on-primary-container` in vibrant (so a text button drops its
|
|
23
|
+
default `primary` to match the toolbar). Filled/tonal buttons keep their
|
|
24
|
+
own colors for single-action emphasis.
|
|
25
|
+
|
|
26
|
+
## Import
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import {DockedToolbar, FloatingToolbar} from "react-material-expressive";
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## API
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
interface DockedToolbarProps extends ComponentProps<"div"> {
|
|
36
|
+
vibrant?: boolean; // primary-container scheme
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface FloatingToolbarProps extends ComponentProps<"div"> {
|
|
40
|
+
expanded?: boolean; // default true; false plays the collapse
|
|
41
|
+
fab?: ReactNode; // adjacent <FAB>, 8dp away (level 2, hover 3)
|
|
42
|
+
fabPosition?: "start" | "end"; // FAB side (default end)
|
|
43
|
+
flat?: boolean; // remove the level-3 elevation
|
|
44
|
+
leading?: ReactNode; // collapses away when expanded={false}
|
|
45
|
+
trailing?: ReactNode; // collapses away when expanded={false}
|
|
46
|
+
vertical?: boolean; // 64dp-wide column
|
|
47
|
+
vibrant?: boolean; // primary-container scheme
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Examples
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
// Docked: global actions pinned to the bottom of the window.
|
|
55
|
+
<DockedToolbar className="fixed bottom-0">
|
|
56
|
+
<IconButton aria-label="Undo" variant="standard">…</IconButton>
|
|
57
|
+
<IconButton aria-label="Redo" variant="standard">…</IconButton>
|
|
58
|
+
<IconButton aria-label="Attach" variant="standard">…</IconButton>
|
|
59
|
+
</DockedToolbar>
|
|
60
|
+
|
|
61
|
+
// Floating with FAB: collapse on scroll — the pill clips toward the FAB,
|
|
62
|
+
// which grows to the 80dp medium size (fast-spatial spring).
|
|
63
|
+
<FloatingToolbar
|
|
64
|
+
className="fixed bottom-4 left-1/2 -translate-x-1/2"
|
|
65
|
+
expanded={!scrolledDown}
|
|
66
|
+
fab={
|
|
67
|
+
<FAB aria-label="Add" variant="secondary">
|
|
68
|
+
<MaterialSymbol name="add" />
|
|
69
|
+
</FAB>
|
|
70
|
+
}>
|
|
71
|
+
<IconButton aria-label="Bold" selected={bold} variant="standard">…</IconButton>
|
|
72
|
+
<IconButton aria-label="Italic" selected={italic} variant="standard">…</IconButton>
|
|
73
|
+
</FloatingToolbar>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Motion
|
|
77
|
+
|
|
78
|
+
Expand/collapse ports the Compose fast-spatial spring (damping 0.6 /
|
|
79
|
+
stiffness 800) as a 350ms CSS `linear()` curve with a 9.5% overshoot
|
|
80
|
+
(`--md-sys-motion-spring-fast-spatial`). With a FAB the whole pill
|
|
81
|
+
clip-reveals toward the FAB side while the FAB lerps 56 → 80dp (icon
|
|
82
|
+
24 → 28); without one, only the `leading`/`trailing` slots collapse
|
|
83
|
+
(1fr → 0fr grid morph), keeping the core children. Collapsed regions are
|
|
84
|
+
`inert`, so they drop out of focus order and the accessibility tree.
|
|
85
|
+
|
|
86
|
+
## Gotchas
|
|
87
|
+
|
|
88
|
+
- Position the toolbar yourself (`fixed` wrapper): docked bars belong to
|
|
89
|
+
the window bottom; floating toolbars keep ≥16dp margins (≥24dp when
|
|
90
|
+
vertical) and must stay fully on screen.
|
|
91
|
+
- `expanded` is plain-controlled (no internal toggle) — drive it from your
|
|
92
|
+
scroll handler; the spec recommends collapsing on scroll-down and
|
|
93
|
+
expanding on scroll-up (Compose uses a 40dp threshold).
|
|
94
|
+
- Don't show a docked toolbar together with a navigation bar, and don't
|
|
95
|
+
use wide icon buttons in vertical floating toolbars (spec guidelines).
|
|
96
|
+
- The FAB slot expects a default 56dp `<FAB>`; the toolbar overrides its
|
|
97
|
+
elevation (level 2, hover 3 — not the standalone 3 → 4) and animates its
|
|
98
|
+
size, so don't pass `size`.
|
|
99
|
+
- Per spec the FAB pairs as `secondary` in standard toolbars and
|
|
100
|
+
`tertiary` in vibrant ones (`md.comp.toolbar.floating.fab` tokens).
|
|
101
|
+
- Square icon buttons are fine in docked toolbars but discouraged inside
|
|
102
|
+
floating ones (shape clash with the fully-round container).
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Tooltip
|
|
2
|
+
|
|
3
|
+
M3 tooltips shown on hover/focus of the wrapped trigger, animating in with
|
|
4
|
+
a fade + scale (0.8→1) on the fast-spatial spring. Plain: shape extra-small
|
|
5
|
+
on inverse-surface with `body-small` text. Rich: shape medium on
|
|
6
|
+
surface-container (elevation 2, max 320dp wide) with optional `title-small`
|
|
7
|
+
subhead and an action slot.
|
|
8
|
+
|
|
9
|
+
## Import
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import {Tooltip} from "react-material-expressive";
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## API
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
interface TooltipProps {
|
|
19
|
+
action?: ReactNode; // rich only (e.g. <Button variant="text">)
|
|
20
|
+
bottomLeft?: boolean;
|
|
21
|
+
bottomRight?: boolean;
|
|
22
|
+
topLeft?: boolean;
|
|
23
|
+
topRight?: boolean; // default bottomLeft
|
|
24
|
+
children?: ReactNode; // trigger
|
|
25
|
+
className?: string;
|
|
26
|
+
text?: ReactNode;
|
|
27
|
+
title?: ReactNode; // rich subhead
|
|
28
|
+
variant?: "plain" | "rich"; // default "plain"
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Examples
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
<Tooltip text="Save to favorites" topLeft>
|
|
36
|
+
<IconButton aria-label="Favorite" icon={<MaterialSymbol name="favorite" />} variant="standard" />
|
|
37
|
+
</Tooltip>
|
|
38
|
+
|
|
39
|
+
<Tooltip variant="rich" title="Rich tooltip" text="Longer supporting copy"
|
|
40
|
+
action={<Button variant="text">Learn more</Button>} bottomRight>
|
|
41
|
+
<IconButton aria-label="Info" icon={<MaterialSymbol name="info" />} variant="standard" />
|
|
42
|
+
</Tooltip>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Gotchas
|
|
46
|
+
|
|
47
|
+
- Display is pure CSS (hover/focus-within) with a 150ms appearance delay,
|
|
48
|
+
animating fade + scale 0.8→1 from center on the fast-spatial spring (the
|
|
49
|
+
same FadeInFadeOutWithScale choreography as the Snackbar); hide is
|
|
50
|
+
immediate. Hover is gated under `@media (hover: hover)` so a touch tap
|
|
51
|
+
doesn't leave it stuck open. Rich tooltips become interactive
|
|
52
|
+
(`pointer-events`) while shown so the action is clickable.
|
|
53
|
+
- Keep the trigger inside the wrapper — the tooltip positions against it.
|
|
54
|
+
|
|
55
|
+
## Accessibility
|
|
56
|
+
|
|
57
|
+
- The tooltip container is `role="tooltip"` with a generated `id`. When the
|
|
58
|
+
trigger is a single element, the library merges that id into the trigger's
|
|
59
|
+
`aria-describedby`, so the supporting text is announced when the trigger is
|
|
60
|
+
focused (the WAI-ARIA tooltip pattern). Any `aria-describedby` you already
|
|
61
|
+
set on the trigger is preserved.
|
|
62
|
+
- Give the trigger its own accessible name (e.g. `aria-label` on an icon-only
|
|
63
|
+
`IconButton`) — the tooltip describes the control, it does not name it.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# TopAppBar
|
|
2
|
+
|
|
3
|
+
M3 top app bar container with the variants as sub-parts:
|
|
4
|
+
`TopAppBar.Small` / `.Center` (64dp, `title-large`), `.Medium` and
|
|
5
|
+
`.Large`. The container is `surface` (swap to `surface-container` on
|
|
6
|
+
scroll); bar padding is 4dp. Every variant takes an optional `subtitle`
|
|
7
|
+
(rendered under the title).
|
|
8
|
+
|
|
9
|
+
`.Medium` and `.Large` render the **M3 Expressive flexible** variants:
|
|
10
|
+
Medium is `headline-medium` (112dp, 136dp with a subtitle), Large is
|
|
11
|
+
`display-small` (120dp, 152dp with a subtitle). `.Center` is the small bar
|
|
12
|
+
with centered title text (M3E merged center-aligned into small).
|
|
13
|
+
|
|
14
|
+
> **M3 Expressive note** — flexible is the only Medium/Large variant. The
|
|
15
|
+
> baseline Medium/Large bars were removed because M3E marks them "no longer
|
|
16
|
+
> recommended".
|
|
17
|
+
|
|
18
|
+
## Import
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
import {TopAppBar} from "react-material-expressive";
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## API
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
type TopAppBarProps = ComponentProps<"header">; // bg-surface by default
|
|
28
|
+
|
|
29
|
+
// Small / Center
|
|
30
|
+
interface AppBarRowProps {
|
|
31
|
+
children?: ReactNode; // title fallback
|
|
32
|
+
className?: string;
|
|
33
|
+
leftElement?: ReactNode; // nav icon
|
|
34
|
+
rightElement?: ReactNode; // action icons
|
|
35
|
+
subtitle?: ReactNode; // label-medium, under the title
|
|
36
|
+
title?: ReactNode;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Medium / Large (flexible only)
|
|
40
|
+
interface CollapsingAppBarProps {
|
|
41
|
+
children?: ReactNode; // title fallback
|
|
42
|
+
className?: string;
|
|
43
|
+
leftElement?: ReactNode; // nav icon
|
|
44
|
+
rightElement?: ReactNode; // action icons
|
|
45
|
+
subtitle?: ReactNode; // medium label-large / large title-medium
|
|
46
|
+
title?: ReactNode;
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Example
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
<TopAppBar>
|
|
54
|
+
<TopAppBar.Small
|
|
55
|
+
leftElement={
|
|
56
|
+
<IconButton
|
|
57
|
+
aria-label="Menu"
|
|
58
|
+
icon={<MaterialSymbol name="menu" />}
|
|
59
|
+
variant="standard"
|
|
60
|
+
/>
|
|
61
|
+
}
|
|
62
|
+
rightElement={
|
|
63
|
+
<IconButton
|
|
64
|
+
aria-label="More"
|
|
65
|
+
icon={<MaterialSymbol name="more_vert" />}
|
|
66
|
+
variant="standard"
|
|
67
|
+
/>
|
|
68
|
+
}
|
|
69
|
+
title="Page title"
|
|
70
|
+
/>
|
|
71
|
+
</TopAppBar>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Gotchas
|
|
75
|
+
|
|
76
|
+
- On-scroll surface change is the consumer's call: toggle
|
|
77
|
+
`className="bg-surface-container shadow-mm-2"` when `scrollY > 0` (the M3
|
|
78
|
+
on-scroll color + elevation level 2). The collapse-on-scroll motion
|
|
79
|
+
(height shrink + title resize) is not built in.
|
|
80
|
+
- `Center` keeps the title optically centered by reserving symmetric side
|
|
81
|
+
slots — pass compact elements (icon buttons / avatar).
|
|
82
|
+
- Leading/trailing icon colors come from the `IconButton`s you pass; M3's
|
|
83
|
+
leading-`on-surface` / trailing-`on-surface-variant` split is yours to
|
|
84
|
+
set if you need it.
|