@windmorph/react 0.1.2 → 0.2.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/dist/components/checkbox/checkbox-tokens.d.ts +1 -1
- package/dist/components/date-picker/date-picker-styles.d.ts +3 -3
- package/dist/components/dialog/dialog-styles.js +5 -5
- package/dist/components/dialog/dialog-styles.js.map +1 -1
- package/dist/components/menu/use-reveal-entering.d.ts +19 -0
- package/dist/components/navigation/NavItem.js +1 -1
- package/dist/components/navigation/NavItem.js.map +1 -1
- package/dist/components/navigation/NavigationRail.d.ts +21 -2
- package/dist/components/navigation/NavigationRail.js +18 -4
- package/dist/components/navigation/NavigationRail.js.map +1 -1
- package/dist/components/navigation/navigation-styles.d.ts +21 -0
- package/dist/components/navigation/navigation-styles.js +6 -1
- package/dist/components/navigation/navigation-styles.js.map +1 -1
- package/dist/components/select/Select.js +6 -1
- package/dist/components/select/Select.js.map +1 -1
- package/dist/components/select/search-list.js +1 -1
- package/dist/components/select/search-list.js.map +1 -1
- package/dist/components/snackbar/snackbar-tokens.d.ts +1 -1
- package/dist/components/text-field/TextField.js +9 -1
- package/dist/components/text-field/TextField.js.map +1 -1
- package/dist/components/time-picker/TimeField.d.ts +118 -0
- package/dist/components/time-picker/TimeField.js +183 -0
- package/dist/components/time-picker/TimeField.js.map +1 -0
- package/dist/components/time-picker/TimeFieldDialog.js +70 -0
- package/dist/components/time-picker/TimeFieldDialog.js.map +1 -0
- package/dist/components/time-picker/TimePicker.js +1 -4
- package/dist/components/time-picker/TimePicker.js.map +1 -1
- package/dist/components/time-picker/hour-cycle.js +10 -0
- package/dist/components/time-picker/hour-cycle.js.map +1 -0
- package/dist/components/time-picker/time-format.js +84 -0
- package/dist/components/time-picker/time-format.js.map +1 -0
- package/dist/composites/button-group/ButtonGroup.d.ts +7 -0
- package/dist/composites/button-group/ButtonGroup.js +3 -2
- package/dist/composites/button-group/ButtonGroup.js.map +1 -1
- package/dist/composites/button-group/button-group-styles.d.ts +15 -3
- package/dist/composites/button-group/button-group-styles.js +16 -1
- package/dist/composites/button-group/button-group-styles.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/primitives.d.ts +4 -1
- package/dist/primitives.js +4 -1
- package/dist/styles.compiled.css +1 -1
- package/dist/utils/closed-by.d.ts +5 -0
- package/dist/utils/placement.d.ts +11 -0
- package/package.json +2 -2
|
@@ -2,6 +2,6 @@ import { ComponentTokenSet } from "../../tokens/component.js";
|
|
|
2
2
|
import "../../index.js";
|
|
3
3
|
//#region src/components/checkbox/checkbox-tokens.d.ts
|
|
4
4
|
/** The tokens of {@link Checkbox}: `--md-comp-checkbox-<part>`. */
|
|
5
|
-
export declare const checkboxTokenSet: ComponentTokenSet<"selected-
|
|
5
|
+
export declare const checkboxTokenSet: ComponentTokenSet<"selected-icon-color" | "selected-container-color" | "unselected-outline-color">;
|
|
6
6
|
//#endregion
|
|
7
7
|
//# sourceMappingURL=checkbox-tokens.d.ts.map
|
|
@@ -156,9 +156,9 @@ export declare const datePickerStyles: import("tailwind-variants").TVReturnType<
|
|
|
156
156
|
export declare const dockedDatePickerStyles: import("tailwind-variants").TVReturnType<{
|
|
157
157
|
[key: string]: {
|
|
158
158
|
[key: string]: import("tailwind-variants").ClassValue | {
|
|
159
|
+
panel?: import("tailwind-variants").ClassValue;
|
|
159
160
|
actions?: import("tailwind-variants").ClassValue;
|
|
160
161
|
item?: import("tailwind-variants").ClassValue;
|
|
161
|
-
panel?: import("tailwind-variants").ClassValue;
|
|
162
162
|
header?: import("tailwind-variants").ClassValue;
|
|
163
163
|
view?: import("tailwind-variants").ClassValue;
|
|
164
164
|
list?: import("tailwind-variants").ClassValue;
|
|
@@ -172,9 +172,9 @@ export declare const dockedDatePickerStyles: import("tailwind-variants").TVRetur
|
|
|
172
172
|
} | {
|
|
173
173
|
[x: string]: {
|
|
174
174
|
[x: string]: import("tailwind-variants").ClassValue | {
|
|
175
|
+
panel?: import("tailwind-variants").ClassValue;
|
|
175
176
|
actions?: import("tailwind-variants").ClassValue;
|
|
176
177
|
item?: import("tailwind-variants").ClassValue;
|
|
177
|
-
panel?: import("tailwind-variants").ClassValue;
|
|
178
178
|
header?: import("tailwind-variants").ClassValue;
|
|
179
179
|
view?: import("tailwind-variants").ClassValue;
|
|
180
180
|
list?: import("tailwind-variants").ClassValue;
|
|
@@ -200,9 +200,9 @@ export declare const dockedDatePickerStyles: import("tailwind-variants").TVRetur
|
|
|
200
200
|
}, undefined, {
|
|
201
201
|
[key: string]: {
|
|
202
202
|
[key: string]: import("tailwind-variants").ClassValue | {
|
|
203
|
+
panel?: import("tailwind-variants").ClassValue;
|
|
203
204
|
actions?: import("tailwind-variants").ClassValue;
|
|
204
205
|
item?: import("tailwind-variants").ClassValue;
|
|
205
|
-
panel?: import("tailwind-variants").ClassValue;
|
|
206
206
|
header?: import("tailwind-variants").ClassValue;
|
|
207
207
|
view?: import("tailwind-variants").ClassValue;
|
|
208
208
|
list?: import("tailwind-variants").ClassValue;
|
|
@@ -57,7 +57,7 @@ const dialogStyles = tv({
|
|
|
57
57
|
headerButton: "",
|
|
58
58
|
close: "[--md-comp-icon-button-icon-color:var(--md-sys-color-on-surface)]",
|
|
59
59
|
content: [
|
|
60
|
-
"min-h-0 flex-1 overflow-y-auto pb-[24px] text-body-medium",
|
|
60
|
+
"-mt-[8px] min-h-0 flex-1 overflow-y-auto pt-[8px] pb-[24px] text-body-medium",
|
|
61
61
|
"text-[color:var(--_md-comp-dialog-supporting-text-color)]",
|
|
62
62
|
"focus-ring"
|
|
63
63
|
],
|
|
@@ -138,7 +138,7 @@ const dialogStyles = tv({
|
|
|
138
138
|
panel: ["size-full max-h-none max-w-none rounded-none p-0 shadow-none", "bg-surface-container-high"],
|
|
139
139
|
header: "flex h-[56px] shrink-0 items-center gap-[4px] ps-[4px] pe-[12px]",
|
|
140
140
|
title: "min-w-0 flex-1 truncate ps-[4px] pb-0 text-start text-title-large",
|
|
141
|
-
content: "px-[24px] pt-[24px]",
|
|
141
|
+
content: "mt-0 px-[24px] pt-[24px]",
|
|
142
142
|
actions: "hidden"
|
|
143
143
|
},
|
|
144
144
|
compact: {
|
|
@@ -177,7 +177,7 @@ const dialogStyles = tv({
|
|
|
177
177
|
header: ["max-medium:flex max-medium:h-[56px] max-medium:shrink-0 max-medium:items-center", "max-medium:gap-[4px] max-medium:ps-[4px] max-medium:pe-[12px]"],
|
|
178
178
|
headerButton: "medium:hidden",
|
|
179
179
|
title: ["max-medium:min-w-0 max-medium:flex-1 max-medium:truncate max-medium:ps-[4px]", "max-medium:pb-0 max-medium:text-start max-medium:text-title-large"],
|
|
180
|
-
content: "max-medium:px-[24px] max-medium:pt-[24px]",
|
|
180
|
+
content: "max-medium:mt-0 max-medium:px-[24px] max-medium:pt-[24px]",
|
|
181
181
|
actions: "max-medium:hidden"
|
|
182
182
|
}
|
|
183
183
|
},
|
|
@@ -244,12 +244,12 @@ const dialogStyles = tv({
|
|
|
244
244
|
{
|
|
245
245
|
fullScreen: "never",
|
|
246
246
|
dividers: true,
|
|
247
|
-
class: { content: "-mx-[24px] mb-[24px] border-y border-outline-variant px-[24px] py-[8px]" }
|
|
247
|
+
class: { content: "mt-0 -mx-[24px] mb-[24px] border-y border-outline-variant px-[24px] py-[8px]" }
|
|
248
248
|
},
|
|
249
249
|
{
|
|
250
250
|
fullScreen: "compact",
|
|
251
251
|
dividers: true,
|
|
252
|
-
class: { content: ["medium:-mx-[24px] medium:mb-[24px] medium:border-y medium:border-outline-variant", "medium:px-[24px] medium:py-[8px]"] }
|
|
252
|
+
class: { content: ["medium:mt-0 medium:-mx-[24px] medium:mb-[24px] medium:border-y medium:border-outline-variant", "medium:px-[24px] medium:py-[8px]"] }
|
|
253
253
|
},
|
|
254
254
|
{
|
|
255
255
|
fullScreen: "never",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dialog-styles.js","names":[],"sources":["../../../src/components/dialog/dialog-styles.ts"],"sourcesContent":["import { tv, type VariantProps } from '../../utils/tv';\n\n/*\n * Values follow Compose Material 3 (AlertDialog.kt and DialogTokens): 280–560px wide (the\n * minimum capped by the window's 24px margins), 24px padding, 28px corners,\n * surface-container-high at elevation 3, a 24px secondary icon and the title 16px apart, the\n * title centred when there is an icon, 24px below the supporting text, 8px between actions,\n * and stacked actions with the confirming action on top. Optional dividers above and below\n * the content are 1px `outline-variant`. The scrim is `scrim` at 32% (M3).\n *\n * Motion (2026-10-14): Compose has no dialog motion of its own (the platform animates\n * windows), so the basic dialog follows the M3 dialog enter and exit as Material Web encodes\n * it, mapped onto the M3 Expressive springs (spatial for movement and size, effects for\n * opacity). Entering, the dialog slides 50px away from the window edge it sits against\n * (downward when centred), its container grows from that edge from 0 to its full size (a\n * clip on a library-owned wrapper, so text is never shown outside the container and the\n * spec's 0 start is kept), and it fades in on the fast effects spring while the scrim fades\n * in on the slow one; the headline, icon and supporting text fade in after 50ms and the\n * actions after 150ms. Leaving follows the reference's close timing exactly, on M3's\n * emphasized-accelerate easing and duration tokens: the parts fade out over 100ms, the\n * dialog slides back 50px and its container shrinks to 35% over 150ms (slow at first, so\n * it is still moving and shrinking until it is gone), the container fades out from 100ms to\n * 150ms, and the scrim fades out over 150ms. Reduced motion keeps only the\n * fades of the dialog and the scrim; `instant` turns all of it off.\n * The positioner keeps 24px from the window's edges on compact windows and the spec's 56px\n * on wider ones, and `placement` aligns the dialog in it.\n *\n * Full-screen dialogs follow the M3 dialog spec's full-screen type: the panel fills the\n * window on `surface-container-high` with no corners, elevation or padding; a 56px header\n * holds the `on-surface` close icon button, the `title-large` headline and the confirming\n * text button (with an optional `outline-variant` divider), and the content is padded 24px\n * at the top and sides. With a header action `DialogActions` are hidden; without one they\n * form a 56px bottom action bar. The panel slides up 48px and fades in on the slow springs\n * (the spec's speed for full-screen animations), or, with an `origin`, grows out of it (a\n * container transform). `fullScreen: 'compact'` applies all of this only below 600px (the M3\n * compact window class) and is a basic dialog otherwise.\n *\n * The basic dialog's container colour, corners and shadow, and the icon, title and supporting\n * text colours, come from the dialog's component tokens (`dialog-tokens.ts`,\n * `--md-comp-dialog-*`); the panel carries `data-tokens=\"md-comp-dialog\"`, and the parts read\n * one private variable each. A full-screen panel keeps its own `surface-container-high`. A\n * consumer class (`bg-surface-container`) still wins. Every class is written out in full.\n */\n\n/** The dialog root's placement, inline so it wins over consumer layout classes. */\nexport const DIALOG_ROOT_STYLE = { position: 'fixed', inset: 0 } as const;\n\n/** Variant definitions for {@link Dialog} and its parts. */\nexport const dialogStyles = tv({\n slots: {\n scrim: [\n 'fixed inset-0 z-(--md-sys-z-scrim) bg-scrim/32 opacity-100',\n '[transition-property:opacity]',\n '[transition-duration:var(--md-sys-motion-spring-effects-slow-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-slow-easing)]',\n 'starting:opacity-0',\n 'data-exiting:opacity-0',\n 'data-exiting:[transition-duration:var(--md-sys-motion-duration-short-3)]',\n 'data-exiting:[transition-timing-function:var(--md-sys-motion-easing-linear)]',\n ],\n // The dialog's outermost element, which takes the consumer's `className` and `style`. It\n // covers the window through an inline `position: fixed; inset: 0` (`DIALOG_ROOT_STYLE`),\n // which a consumer class can't move: a layout class such as `static` or `absolute top-24`\n // never puts the dialog in the page's flow or out of the window.\n root: 'z-(--md-sys-z-dialog)',\n positioner: 'fixed inset-0 z-(--md-sys-z-dialog) flex p-[24px] medium:p-[56px]',\n /*\n * The library-owned wrapper that moves: it slides 50px (`--m3-dialog-slide`), its clip\n * grows the container (`--m3-dialog-enter-clip` → `--m3-dialog-open-clip`, which rests\n * 32px outside the panel so the shadow shows, its corners concentric with the panel's\n * `--m3-dialog-corner`) and it fades. Leaving, the clip shrinks to 35%\n * (`--m3-dialog-exit-clip`) and the fade waits 100ms for the parts to fade first. The\n * exit uses the reference's curves rather than springs (2026-10-14): the slide and shrink\n * run 150ms on emphasized-accelerate, which never overshoots and keeps moving until the\n * fade (50ms, linear, after 100ms) has ended. A spring front-loads the motion: the\n * expressive spatial one overshot past 50px and below 35%, and the critically damped\n * effects one finished shrinking before the fade, leaving a still 35% box fading. The\n * clip's sides open from the panel's edges to the shadow's 32px together with its corner\n * radius, so the growing edge keeps the panel's corners on every frame.\n */\n motion: [\n 'flex max-h-full max-w-full',\n '[--m3-dialog-corner:var(--md-comp-dialog-container-shape,var(--md-sys-shape-corner-extra-large))]',\n '[--m3-dialog-open-clip:inset(-32px_round_calc(var(--m3-dialog-corner)_+_32px))]',\n '[--m3-dialog-enter-clip:inset(0_0_100%_round_var(--m3-dialog-corner))]',\n '[--m3-dialog-exit-clip:inset(0_0_65%_round_var(--m3-dialog-corner))]',\n '[--m3-dialog-slide:0_-50px] [--m3-dialog-scale-origin:center]',\n 'translate-none opacity-100 [clip-path:var(--m3-dialog-open-clip)]',\n 'starting:[translate:var(--m3-dialog-slide)] starting:opacity-0',\n 'starting:[clip-path:var(--m3-dialog-enter-clip)]',\n 'data-exiting:[translate:var(--m3-dialog-slide)] data-exiting:opacity-0',\n 'data-exiting:[clip-path:var(--m3-dialog-exit-clip)]',\n 'motion-reduce:data-exiting:translate-none',\n 'motion-reduce:data-exiting:[clip-path:var(--m3-dialog-open-clip)]',\n '[transition-property:opacity,translate,clip-path] motion-reduce:[transition-property:opacity]',\n '[transition-duration:var(--md-sys-motion-spring-effects-fast-duration),var(--md-sys-motion-spring-spatial-default-duration),var(--md-sys-motion-spring-spatial-default-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing),var(--md-sys-motion-spring-spatial-default-easing),var(--md-sys-motion-spring-spatial-default-easing)]',\n 'data-exiting:[transition-duration:var(--md-sys-motion-duration-short-1),var(--md-sys-motion-duration-short-3),var(--md-sys-motion-duration-short-3)]',\n 'data-exiting:[transition-timing-function:var(--md-sys-motion-easing-linear),var(--md-sys-motion-easing-emphasized-accelerate),var(--md-sys-motion-easing-emphasized-accelerate)]',\n 'data-exiting:[transition-delay:var(--md-sys-motion-duration-short-2),0s,0s]',\n 'motion-reduce:data-exiting:[transition-delay:0s]',\n 'motion-reduce:data-exiting:[transition-duration:var(--md-sys-motion-duration-short-3)]',\n ],\n /*\n * Classes for the panel that stagger its parts (its children): every part fades in after\n * 50ms on the default effects spring (the headline, icon and supporting text, and any\n * custom child), and the actions (`data-slot=\"actions\"`) after 150ms on the fast one.\n * Leaving, they all fade out at once on the fast spring. Basic dialogs only (see the\n * `fullScreen` variants); reduced motion leaves the parts to the wrapper's fade.\n */\n parts: '',\n panel: [\n 'flex max-h-full min-w-[min(280px,calc(100vw-48px))] max-w-[560px] flex-col p-[24px]',\n 'rounded-[var(--_md-comp-dialog-container-shape)] bg-[var(--_md-comp-dialog-container-color)]',\n 'text-on-surface shadow-[shadow:var(--_md-comp-dialog-container-elevation)]',\n 'outline-none',\n // Forced colours drop the fill and shadow, so the panel draws its edge instead.\n 'forced-colors:outline-solid forced-colors:outline-1 forced-colors:outline-[CanvasText]',\n ],\n icon: [\n 'mx-auto inline-flex pb-[16px] [&>svg]:size-[24px]',\n 'text-[color:var(--_md-comp-dialog-with-icon-icon-color)]',\n ],\n title: [\n 'pb-[16px] text-headline-small font-brand',\n 'text-[color:var(--_md-comp-dialog-headline-color)]',\n ],\n header: '',\n headerButton: '',\n close: '[--md-comp-icon-button-icon-color:var(--md-sys-color-on-surface)]',\n content: [\n 'min-h-0 flex-1 overflow-y-auto pb-[24px] text-body-medium',\n 'text-[color:var(--_md-comp-dialog-supporting-text-color)]',\n 'focus-ring',\n ],\n // Stacked (`data-stacked`, set by `DialogActions` when they don't fit on one line), the\n // buttons are rendered confirming action first and wrap forwards instead of in reverse:\n // the same picture, sizes included, in focus order. The custom property tells it\n // whether the layout lets them stack.\n actions: [\n 'flex flex-wrap-reverse items-center justify-end gap-[8px] [--m3-dialog-actions-stack:1]',\n 'data-stacked:flex-wrap',\n ],\n },\n variants: {\n hasIcon: {\n true: { title: 'text-center' },\n false: { title: 'text-start' },\n },\n /*\n * The dialog enters from the window edge it sits against (the spec's enter and exit\n * pattern): it slides 50px away from that edge while its container grows away from it,\n * and leaves the same way back. Centred or at the top it slides down and grows downward\n * (the reference motion), at the bottom it slides up and grows upward, and at the start\n * or end it moves and grows along the x axis, mirrored in right-to-left layouts.\n */\n placement: {\n center: { positioner: 'items-center justify-center' },\n top: {\n positioner: 'items-start justify-center',\n motion: '[--m3-dialog-scale-origin:top]',\n },\n bottom: {\n positioner: 'items-end justify-center',\n motion: [\n '[--m3-dialog-enter-clip:inset(100%_0_0_round_var(--m3-dialog-corner))]',\n '[--m3-dialog-exit-clip:inset(65%_0_0_round_var(--m3-dialog-corner))]',\n '[--m3-dialog-slide:0_50px] [--m3-dialog-scale-origin:bottom]',\n ],\n },\n start: {\n positioner: 'items-center justify-start',\n motion: [\n '[--m3-dialog-enter-clip:inset(0_100%_0_0_round_var(--m3-dialog-corner))]',\n '[--m3-dialog-exit-clip:inset(0_65%_0_0_round_var(--m3-dialog-corner))]',\n '[--m3-dialog-slide:-50px_0] [--m3-dialog-scale-origin:left]',\n 'rtl:[--m3-dialog-enter-clip:inset(0_0_0_100%_round_var(--m3-dialog-corner))]',\n 'rtl:[--m3-dialog-exit-clip:inset(0_0_0_65%_round_var(--m3-dialog-corner))]',\n 'rtl:[--m3-dialog-slide:50px_0] rtl:[--m3-dialog-scale-origin:right]',\n ],\n },\n end: {\n positioner: 'items-center justify-end',\n motion: [\n '[--m3-dialog-enter-clip:inset(0_0_0_100%_round_var(--m3-dialog-corner))]',\n '[--m3-dialog-exit-clip:inset(0_0_0_65%_round_var(--m3-dialog-corner))]',\n '[--m3-dialog-slide:50px_0] [--m3-dialog-scale-origin:right]',\n 'rtl:[--m3-dialog-enter-clip:inset(0_100%_0_0_round_var(--m3-dialog-corner))]',\n 'rtl:[--m3-dialog-exit-clip:inset(0_65%_0_0_round_var(--m3-dialog-corner))]',\n 'rtl:[--m3-dialog-slide:-50px_0] rtl:[--m3-dialog-scale-origin:left]',\n ],\n },\n },\n fullScreen: {\n never: {\n headerButton: 'hidden',\n parts: [\n 'motion-safe:*:starting:opacity-0 motion-safe:data-exiting:*:opacity-0',\n '*:[transition-property:opacity]',\n '*:[transition-duration:var(--md-sys-motion-spring-effects-default-duration)]',\n '*:[transition-timing-function:var(--md-sys-motion-spring-effects-default-easing)]',\n '*:[transition-delay:var(--md-sys-motion-duration-short-1)]',\n '*:data-[slot=actions]:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)]',\n '*:data-[slot=actions]:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]',\n '*:data-[slot=actions]:[transition-delay:var(--md-sys-motion-duration-short-3)]',\n 'data-exiting:*:[transition-duration:var(--md-sys-motion-duration-short-2)]',\n 'data-exiting:*:[transition-timing-function:var(--md-sys-motion-easing-linear)]',\n 'data-exiting:*:[transition-delay:0s] data-exiting:*:data-[slot=actions]:[transition-delay:0s]',\n 'data-exiting:*:data-[slot=actions]:[transition-duration:var(--md-sys-motion-duration-short-2)]',\n 'data-exiting:*:data-[slot=actions]:[transition-timing-function:var(--md-sys-motion-easing-linear)]',\n ],\n },\n /*\n * Full screen keeps its own motion: the dialog slides up 48px as it fades in on the slow\n * springs (the spec's speed for full-screen motion) with no clip, and slides back down\n * as it fades out on the fast ones, its parts fading with it.\n */\n always: {\n positioner: 'p-0 medium:p-0',\n motion: [\n 'size-full [--m3-dialog-slide:0_48px]',\n '[--m3-dialog-open-clip:none] [--m3-dialog-enter-clip:none] [--m3-dialog-exit-clip:none]',\n '[transition-duration:var(--md-sys-motion-spring-effects-slow-duration),var(--md-sys-motion-spring-spatial-slow-duration),var(--md-sys-motion-spring-spatial-slow-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-slow-easing),var(--md-sys-motion-spring-spatial-slow-easing),var(--md-sys-motion-spring-spatial-slow-easing)]',\n 'data-exiting:[transition-delay:0s]',\n 'data-exiting:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)]',\n 'data-exiting:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]',\n ],\n panel: [\n 'size-full max-h-none max-w-none rounded-none p-0 shadow-none',\n 'bg-surface-container-high',\n ],\n header: 'flex h-[56px] shrink-0 items-center gap-[4px] ps-[4px] pe-[12px]',\n title: 'min-w-0 flex-1 truncate ps-[4px] pb-0 text-start text-title-large',\n content: 'px-[24px] pt-[24px]',\n actions: 'hidden',\n },\n compact: {\n positioner: 'max-medium:p-0',\n motion: [\n 'max-medium:size-full max-medium:[--m3-dialog-slide:0_48px]',\n 'max-medium:[--m3-dialog-open-clip:none] max-medium:[--m3-dialog-enter-clip:none]',\n 'max-medium:[--m3-dialog-exit-clip:none]',\n 'max-medium:[transition-duration:var(--md-sys-motion-spring-effects-slow-duration),var(--md-sys-motion-spring-spatial-slow-duration),var(--md-sys-motion-spring-spatial-slow-duration)]',\n 'max-medium:[transition-timing-function:var(--md-sys-motion-spring-effects-slow-easing),var(--md-sys-motion-spring-spatial-slow-easing),var(--md-sys-motion-spring-spatial-slow-easing)]',\n 'max-medium:data-exiting:[transition-delay:0s]',\n 'max-medium:data-exiting:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)]',\n 'max-medium:data-exiting:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]',\n ],\n parts: [\n 'medium:motion-safe:*:starting:opacity-0 medium:motion-safe:data-exiting:*:opacity-0',\n 'medium:*:[transition-property:opacity]',\n 'medium:*:[transition-duration:var(--md-sys-motion-spring-effects-default-duration)]',\n 'medium:*:[transition-timing-function:var(--md-sys-motion-spring-effects-default-easing)]',\n 'medium:*:[transition-delay:var(--md-sys-motion-duration-short-1)]',\n 'medium:*:data-[slot=actions]:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)]',\n 'medium:*:data-[slot=actions]:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]',\n 'medium:*:data-[slot=actions]:[transition-delay:var(--md-sys-motion-duration-short-3)]',\n 'medium:data-exiting:*:[transition-duration:var(--md-sys-motion-duration-short-2)]',\n 'medium:data-exiting:*:[transition-timing-function:var(--md-sys-motion-easing-linear)]',\n 'medium:data-exiting:*:[transition-delay:0s]',\n 'medium:data-exiting:*:data-[slot=actions]:[transition-delay:0s]',\n 'medium:data-exiting:*:data-[slot=actions]:[transition-duration:var(--md-sys-motion-duration-short-2)]',\n 'medium:data-exiting:*:data-[slot=actions]:[transition-timing-function:var(--md-sys-motion-easing-linear)]',\n ],\n panel: [\n 'max-medium:size-full max-medium:max-h-none max-medium:max-w-none',\n 'max-medium:rounded-none max-medium:p-0 max-medium:shadow-none',\n 'max-medium:bg-surface-container-high',\n ],\n header: [\n 'max-medium:flex max-medium:h-[56px] max-medium:shrink-0 max-medium:items-center',\n 'max-medium:gap-[4px] max-medium:ps-[4px] max-medium:pe-[12px]',\n ],\n headerButton: 'medium:hidden',\n title: [\n 'max-medium:min-w-0 max-medium:flex-1 max-medium:truncate max-medium:ps-[4px]',\n 'max-medium:pb-0 max-medium:text-start max-medium:text-title-large',\n ],\n content: 'max-medium:px-[24px] max-medium:pt-[24px]',\n actions: 'max-medium:hidden',\n },\n },\n /** Full screen without a header action: the actions form a 56px bottom bar. */\n bottomBar: { true: {}, false: {} },\n /** A divider under the full-screen header. */\n divider: { true: {}, false: {} },\n /** The basic dialog's optional dividers above and below `DialogContent`. */\n dividers: { true: {}, false: {} },\n /** A container transform from a trigger (`origin`) into the full-screen dialog. */\n origin: { true: {}, false: {} },\n /**\n * The basic dialog's enter and exit: `reveal`, the M3 dialog motion above, or `scale`,\n * the original pop (see the compound variants). Full-screen dialogs ignore it.\n */\n transition: { reveal: {}, scale: {} },\n /** No enter or exit motion: the dialog, its parts and the scrim appear and go at once. */\n instant: {\n true: {\n scrim: '[transition-property:none]!',\n motion: '[transition-property:none]!',\n panel: '[transition-property:none]!',\n parts: '*:[transition-property:none]!',\n },\n false: {},\n },\n },\n compoundVariants: [\n {\n fullScreen: 'always',\n bottomBar: true,\n class: {\n actions: 'flex h-[56px] shrink-0 flex-nowrap px-[24px] [--m3-dialog-actions-stack:0]',\n },\n },\n {\n fullScreen: 'compact',\n bottomBar: true,\n class: {\n actions:\n 'max-medium:flex max-medium:h-[56px] max-medium:shrink-0 max-medium:flex-nowrap max-medium:px-[24px] max-medium:[--m3-dialog-actions-stack:0]',\n },\n },\n {\n fullScreen: 'always',\n divider: true,\n class: { header: 'border-b border-outline-variant' },\n },\n {\n fullScreen: 'compact',\n divider: true,\n class: { header: 'max-medium:border-b max-medium:border-outline-variant' },\n },\n /*\n * Dividers run the panel's full width, 1px `outline-variant`, with 8px between them and\n * the content; the body keeps 24px to the actions below the bottom one.\n */\n {\n fullScreen: 'never',\n dividers: true,\n class: {\n content: '-mx-[24px] mb-[24px] border-y border-outline-variant px-[24px] py-[8px]',\n },\n },\n {\n fullScreen: 'compact',\n dividers: true,\n class: {\n content: [\n 'medium:-mx-[24px] medium:mb-[24px] medium:border-y medium:border-outline-variant',\n 'medium:px-[24px] medium:py-[8px]',\n ],\n },\n },\n /*\n * `transition=\"scale\"`, the library's original pop (an extension beyond the M3 dialog\n * motion): the scrim fades on the default effects spring, and the dialog fades in while it\n * scales up from 90% on the default springs, then fades out while it scales to 95% on the\n * fast ones. No clip, no slide, no staggered parts. It scales from the edge it sits\n * against (`--m3-dialog-scale-origin`: the centre for `center`, the top, bottom, start or\n * end edge otherwise). Reduced motion keeps only the fades. Basic dialogs only (from 600px\n * for `compact`).\n */\n {\n fullScreen: 'never',\n transition: 'scale',\n class: {\n scrim: [\n '[transition-duration:var(--md-sys-motion-spring-effects-default-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-default-easing)]',\n 'data-exiting:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)]',\n 'data-exiting:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]',\n ],\n motion: [\n '[--m3-dialog-slide:0_0] [--m3-dialog-open-clip:none]',\n '[--m3-dialog-enter-clip:none] [--m3-dialog-exit-clip:none]',\n 'origin-(--m3-dialog-scale-origin) scale-none starting:scale-90 data-exiting:scale-95',\n 'motion-reduce:data-exiting:scale-none',\n '[transition-property:opacity,scale] motion-reduce:[transition-property:opacity]',\n '[transition-duration:var(--md-sys-motion-spring-effects-default-duration),var(--md-sys-motion-spring-spatial-default-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-default-easing),var(--md-sys-motion-spring-spatial-default-easing)]',\n 'data-exiting:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration)]',\n 'data-exiting:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing)]',\n 'data-exiting:[transition-delay:0s]',\n ],\n },\n },\n {\n fullScreen: 'compact',\n transition: 'scale',\n class: {\n scrim: [\n 'medium:[transition-duration:var(--md-sys-motion-spring-effects-default-duration)]',\n 'medium:[transition-timing-function:var(--md-sys-motion-spring-effects-default-easing)]',\n 'medium:data-exiting:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)]',\n 'medium:data-exiting:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]',\n ],\n motion: [\n 'medium:[--m3-dialog-slide:0_0] medium:[--m3-dialog-open-clip:none]',\n 'medium:[--m3-dialog-enter-clip:none] medium:[--m3-dialog-exit-clip:none]',\n 'medium:origin-(--m3-dialog-scale-origin) medium:scale-none',\n 'medium:starting:scale-90 medium:data-exiting:scale-95',\n 'medium:motion-reduce:data-exiting:scale-none',\n 'medium:[transition-property:opacity,scale]',\n 'medium:motion-reduce:[transition-property:opacity]',\n 'medium:[transition-duration:var(--md-sys-motion-spring-effects-default-duration),var(--md-sys-motion-spring-spatial-default-duration)]',\n 'medium:[transition-timing-function:var(--md-sys-motion-spring-effects-default-easing),var(--md-sys-motion-spring-spatial-default-easing)]',\n 'medium:data-exiting:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration)]',\n 'medium:data-exiting:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing)]',\n 'medium:data-exiting:[transition-delay:0s]',\n ],\n },\n },\n /*\n * Container transform (the spec: \"Use a container transform pattern to transition a FAB\n * into a full-screen dialog\"): the clip starts on the origin's box and corners\n * (`--m3-dialog-origin`, measured by the dialog) and opens to the window on the slow\n * spatial spring, while the container colour moves from the origin's\n * (`--m3-dialog-origin-color`) and the content fades in. Leaving collapses back into the\n * origin on emphasized-accelerate over 250ms (2026-10-15), as the basic exit avoids\n * springs: the fast spatial spring collapsed the clip past the origin to nothing and\n * regrew it while still visible. The container stays opaque for the first 60% and fades\n * over the last 100ms, linear, ending with the collapse (2026-10-17, after Material\n * Components' container transform, whose return fades from 60% to 90%), so it is seen going\n * back into the origin. Reduced motion keeps only the fades (the dialog's and its content's).\n */\n {\n fullScreen: 'always',\n origin: true,\n class: {\n motion: [\n '[--m3-dialog-slide:0_0] [--m3-dialog-open-clip:inset(0_round_0)]',\n '[--m3-dialog-enter-clip:var(--m3-dialog-origin)] [--m3-dialog-exit-clip:var(--m3-dialog-origin)]',\n '[transition-duration:var(--md-sys-motion-spring-effects-fast-duration),var(--md-sys-motion-spring-spatial-slow-duration),var(--md-sys-motion-spring-spatial-slow-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing),var(--md-sys-motion-spring-spatial-slow-easing),var(--md-sys-motion-spring-spatial-slow-easing)]',\n 'data-exiting:[transition-duration:var(--md-sys-motion-duration-short-2),var(--md-sys-motion-duration-medium-1),var(--md-sys-motion-duration-medium-1)]',\n 'data-exiting:[transition-timing-function:var(--md-sys-motion-easing-linear),var(--md-sys-motion-easing-emphasized-accelerate),var(--md-sys-motion-easing-emphasized-accelerate)]',\n // Full screen's exit starts every transition at once; the fade waits here.\n 'data-exiting:[transition-delay:var(--md-sys-motion-duration-short-3),0s,0s]',\n 'motion-reduce:data-exiting:[transition-delay:0s]',\n ],\n panel: [\n '[transition-property:background-color] motion-reduce:[transition-property:none]',\n '[transition-duration:var(--md-sys-motion-spring-effects-slow-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-slow-easing)]',\n 'starting:bg-(--m3-dialog-origin-color) motion-safe:data-exiting:bg-(--m3-dialog-origin-color)',\n 'data-exiting:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)]',\n '*:[transition-property:opacity] *:starting:opacity-0 data-exiting:*:opacity-0',\n '*:[transition-duration:var(--md-sys-motion-spring-effects-slow-duration)]',\n '*:[transition-timing-function:var(--md-sys-motion-spring-effects-slow-easing)]',\n ],\n },\n },\n {\n fullScreen: 'compact',\n origin: true,\n class: {\n motion: [\n 'max-medium:[--m3-dialog-slide:0_0] max-medium:[--m3-dialog-open-clip:inset(0_round_0)]',\n 'max-medium:[--m3-dialog-enter-clip:var(--m3-dialog-origin)]',\n 'max-medium:[--m3-dialog-exit-clip:var(--m3-dialog-origin)]',\n 'max-medium:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration),var(--md-sys-motion-spring-spatial-slow-duration),var(--md-sys-motion-spring-spatial-slow-duration)]',\n 'max-medium:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing),var(--md-sys-motion-spring-spatial-slow-easing),var(--md-sys-motion-spring-spatial-slow-easing)]',\n 'max-medium:data-exiting:[transition-duration:var(--md-sys-motion-duration-short-2),var(--md-sys-motion-duration-medium-1),var(--md-sys-motion-duration-medium-1)]',\n 'max-medium:data-exiting:[transition-timing-function:var(--md-sys-motion-easing-linear),var(--md-sys-motion-easing-emphasized-accelerate),var(--md-sys-motion-easing-emphasized-accelerate)]',\n 'max-medium:data-exiting:[transition-delay:var(--md-sys-motion-duration-short-3),0s,0s]',\n // The breakpoint outranks the base reduced-motion exit: restate it, so the fade\n // keeps its duration and starts at once while the spatial springs are instant.\n 'max-medium:motion-reduce:data-exiting:[transition-duration:var(--md-sys-motion-duration-short-3)]',\n 'max-medium:motion-reduce:data-exiting:[transition-delay:0s]',\n ],\n panel: [\n 'max-medium:[transition-property:background-color]',\n 'max-medium:motion-reduce:[transition-property:none]',\n 'max-medium:[transition-duration:var(--md-sys-motion-spring-effects-slow-duration)]',\n 'max-medium:[transition-timing-function:var(--md-sys-motion-spring-effects-slow-easing)]',\n 'max-medium:starting:bg-(--m3-dialog-origin-color)',\n 'max-medium:motion-safe:data-exiting:bg-(--m3-dialog-origin-color)',\n 'max-medium:data-exiting:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)]',\n 'max-medium:*:[transition-property:opacity] max-medium:*:starting:opacity-0',\n 'max-medium:data-exiting:*:opacity-0',\n 'max-medium:*:[transition-duration:var(--md-sys-motion-spring-effects-slow-duration)]',\n 'max-medium:*:[transition-timing-function:var(--md-sys-motion-spring-effects-slow-easing)]',\n ],\n },\n },\n ],\n defaultVariants: {\n dividers: false,\n origin: false,\n instant: false,\n transition: 'reveal',\n hasIcon: false,\n fullScreen: 'never',\n placement: 'center',\n bottomBar: false,\n divider: false,\n },\n});\n\n/**\n * The variants of {@link dialogStyles}, for building on its styles. They are the recipe's keys, not\n * component props: their names (`hasIcon`, `isOpen`…) follow the classes and may change.\n */\nexport type DialogStyleProps = VariantProps<typeof dialogStyles>;\n"],"mappings":";;;AA6CA,MAAa,oBAAoB;CAAE,UAAU;CAAS,OAAO;AAAE;;AAG/D,MAAa,eAAe,GAAG;CAC7B,OAAO;EACL,OAAO;GACL;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF;EAKA,MAAM;EACN,YAAY;EAeZ,QAAQ;GACN;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF;EAQA,OAAO;EACP,OAAO;GACL;GACA;GACA;GACA;GAEA;EACF;EACA,MAAM,CACJ,qDACA,0DACF;EACA,OAAO,CACL,4CACA,oDACF;EACA,QAAQ;EACR,cAAc;EACd,OAAO;EACP,SAAS;GACP;GACA;GACA;EACF;EAKA,SAAS,CACP,2FACA,wBACF;CACF;CACA,UAAU;EACR,SAAS;GACP,MAAM,EAAE,OAAO,cAAc;GAC7B,OAAO,EAAE,OAAO,aAAa;EAC/B;EAQA,WAAW;GACT,QAAQ,EAAE,YAAY,8BAA8B;GACpD,KAAK;IACH,YAAY;IACZ,QAAQ;GACV;GACA,QAAQ;IACN,YAAY;IACZ,QAAQ;KACN;KACA;KACA;IACF;GACF;GACA,OAAO;IACL,YAAY;IACZ,QAAQ;KACN;KACA;KACA;KACA;KACA;KACA;IACF;GACF;GACA,KAAK;IACH,YAAY;IACZ,QAAQ;KACN;KACA;KACA;KACA;KACA;KACA;IACF;GACF;EACF;EACA,YAAY;GACV,OAAO;IACL,cAAc;IACd,OAAO;KACL;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;IACF;GACF;GAMA,QAAQ;IACN,YAAY;IACZ,QAAQ;KACN;KACA;KACA;KACA;KACA;KACA;KACA;IACF;IACA,OAAO,CACL,gEACA,2BACF;IACA,QAAQ;IACR,OAAO;IACP,SAAS;IACT,SAAS;GACX;GACA,SAAS;IACP,YAAY;IACZ,QAAQ;KACN;KACA;KACA;KACA;KACA;KACA;KACA;KACA;IACF;IACA,OAAO;KACL;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;IACF;IACA,OAAO;KACL;KACA;KACA;IACF;IACA,QAAQ,CACN,mFACA,+DACF;IACA,cAAc;IACd,OAAO,CACL,gFACA,mEACF;IACA,SAAS;IACT,SAAS;GACX;EACF;;EAEA,WAAW;GAAE,MAAM,CAAC;GAAG,OAAO,CAAC;EAAE;;EAEjC,SAAS;GAAE,MAAM,CAAC;GAAG,OAAO,CAAC;EAAE;;EAE/B,UAAU;GAAE,MAAM,CAAC;GAAG,OAAO,CAAC;EAAE;;EAEhC,QAAQ;GAAE,MAAM,CAAC;GAAG,OAAO,CAAC;EAAE;;;;;EAK9B,YAAY;GAAE,QAAQ,CAAC;GAAG,OAAO,CAAC;EAAE;;EAEpC,SAAS;GACP,MAAM;IACJ,OAAO;IACP,QAAQ;IACR,OAAO;IACP,OAAO;GACT;GACA,OAAO,CAAC;EACV;CACF;CACA,kBAAkB;EAChB;GACE,YAAY;GACZ,WAAW;GACX,OAAO,EACL,SAAS,6EACX;EACF;EACA;GACE,YAAY;GACZ,WAAW;GACX,OAAO,EACL,SACE,+IACJ;EACF;EACA;GACE,YAAY;GACZ,SAAS;GACT,OAAO,EAAE,QAAQ,kCAAkC;EACrD;EACA;GACE,YAAY;GACZ,SAAS;GACT,OAAO,EAAE,QAAQ,wDAAwD;EAC3E;EAKA;GACE,YAAY;GACZ,UAAU;GACV,OAAO,EACL,SAAS,0EACX;EACF;EACA;GACE,YAAY;GACZ,UAAU;GACV,OAAO,EACL,SAAS,CACP,oFACA,kCACF,EACF;EACF;EAUA;GACE,YAAY;GACZ,YAAY;GACZ,OAAO;IACL,OAAO;KACL;KACA;KACA;KACA;IACF;IACA,QAAQ;KACN;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;IACF;GACF;EACF;EACA;GACE,YAAY;GACZ,YAAY;GACZ,OAAO;IACL,OAAO;KACL;KACA;KACA;KACA;IACF;IACA,QAAQ;KACN;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;IACF;GACF;EACF;EAcA;GACE,YAAY;GACZ,QAAQ;GACR,OAAO;IACL,QAAQ;KACN;KACA;KACA;KACA;KACA;KACA;KAEA;KACA;IACF;IACA,OAAO;KACL;KACA;KACA;KACA;KACA;KACA;KACA;KACA;IACF;GACF;EACF;EACA;GACE,YAAY;GACZ,QAAQ;GACR,OAAO;IACL,QAAQ;KACN;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KAGA;KACA;IACF;IACA,OAAO;KACL;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;IACF;GACF;EACF;CACF;CACA,iBAAiB;EACf,UAAU;EACV,QAAQ;EACR,SAAS;EACT,YAAY;EACZ,SAAS;EACT,YAAY;EACZ,WAAW;EACX,WAAW;EACX,SAAS;CACX;AACF,CAAC"}
|
|
1
|
+
{"version":3,"file":"dialog-styles.js","names":[],"sources":["../../../src/components/dialog/dialog-styles.ts"],"sourcesContent":["import { tv, type VariantProps } from '../../utils/tv';\n\n/*\n * Values follow Compose Material 3 (AlertDialog.kt and DialogTokens): 280–560px wide (the\n * minimum capped by the window's 24px margins), 24px padding, 28px corners,\n * surface-container-high at elevation 3, a 24px secondary icon and the title 16px apart, the\n * title centred when there is an icon, 24px below the supporting text, 8px between actions,\n * and stacked actions with the confirming action on top. Optional dividers above and below\n * the content are 1px `outline-variant`. The scrim is `scrim` at 32% (M3).\n *\n * Motion (2026-10-14): Compose has no dialog motion of its own (the platform animates\n * windows), so the basic dialog follows the M3 dialog enter and exit as Material Web encodes\n * it, mapped onto the M3 Expressive springs (spatial for movement and size, effects for\n * opacity). Entering, the dialog slides 50px away from the window edge it sits against\n * (downward when centred), its container grows from that edge from 0 to its full size (a\n * clip on a library-owned wrapper, so text is never shown outside the container and the\n * spec's 0 start is kept), and it fades in on the fast effects spring while the scrim fades\n * in on the slow one; the headline, icon and supporting text fade in after 50ms and the\n * actions after 150ms. Leaving follows the reference's close timing exactly, on M3's\n * emphasized-accelerate easing and duration tokens: the parts fade out over 100ms, the\n * dialog slides back 50px and its container shrinks to 35% over 150ms (slow at first, so\n * it is still moving and shrinking until it is gone), the container fades out from 100ms to\n * 150ms, and the scrim fades out over 150ms. Reduced motion keeps only the\n * fades of the dialog and the scrim; `instant` turns all of it off.\n * The positioner keeps 24px from the window's edges on compact windows and the spec's 56px\n * on wider ones, and `placement` aligns the dialog in it.\n *\n * Full-screen dialogs follow the M3 dialog spec's full-screen type: the panel fills the\n * window on `surface-container-high` with no corners, elevation or padding; a 56px header\n * holds the `on-surface` close icon button, the `title-large` headline and the confirming\n * text button (with an optional `outline-variant` divider), and the content is padded 24px\n * at the top and sides. With a header action `DialogActions` are hidden; without one they\n * form a 56px bottom action bar. The panel slides up 48px and fades in on the slow springs\n * (the spec's speed for full-screen animations), or, with an `origin`, grows out of it (a\n * container transform). `fullScreen: 'compact'` applies all of this only below 600px (the M3\n * compact window class) and is a basic dialog otherwise.\n *\n * The basic dialog's container colour, corners and shadow, and the icon, title and supporting\n * text colours, come from the dialog's component tokens (`dialog-tokens.ts`,\n * `--md-comp-dialog-*`); the panel carries `data-tokens=\"md-comp-dialog\"`, and the parts read\n * one private variable each. A full-screen panel keeps its own `surface-container-high`. A\n * consumer class (`bg-surface-container`) still wins. Every class is written out in full.\n */\n\n/** The dialog root's placement, inline so it wins over consumer layout classes. */\nexport const DIALOG_ROOT_STYLE = { position: 'fixed', inset: 0 } as const;\n\n/** Variant definitions for {@link Dialog} and its parts. */\nexport const dialogStyles = tv({\n slots: {\n scrim: [\n 'fixed inset-0 z-(--md-sys-z-scrim) bg-scrim/32 opacity-100',\n '[transition-property:opacity]',\n '[transition-duration:var(--md-sys-motion-spring-effects-slow-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-slow-easing)]',\n 'starting:opacity-0',\n 'data-exiting:opacity-0',\n 'data-exiting:[transition-duration:var(--md-sys-motion-duration-short-3)]',\n 'data-exiting:[transition-timing-function:var(--md-sys-motion-easing-linear)]',\n ],\n // The dialog's outermost element, which takes the consumer's `className` and `style`. It\n // covers the window through an inline `position: fixed; inset: 0` (`DIALOG_ROOT_STYLE`),\n // which a consumer class can't move: a layout class such as `static` or `absolute top-24`\n // never puts the dialog in the page's flow or out of the window.\n root: 'z-(--md-sys-z-dialog)',\n positioner: 'fixed inset-0 z-(--md-sys-z-dialog) flex p-[24px] medium:p-[56px]',\n /*\n * The library-owned wrapper that moves: it slides 50px (`--m3-dialog-slide`), its clip\n * grows the container (`--m3-dialog-enter-clip` → `--m3-dialog-open-clip`, which rests\n * 32px outside the panel so the shadow shows, its corners concentric with the panel's\n * `--m3-dialog-corner`) and it fades. Leaving, the clip shrinks to 35%\n * (`--m3-dialog-exit-clip`) and the fade waits 100ms for the parts to fade first. The\n * exit uses the reference's curves rather than springs (2026-10-14): the slide and shrink\n * run 150ms on emphasized-accelerate, which never overshoots and keeps moving until the\n * fade (50ms, linear, after 100ms) has ended. A spring front-loads the motion: the\n * expressive spatial one overshot past 50px and below 35%, and the critically damped\n * effects one finished shrinking before the fade, leaving a still 35% box fading. The\n * clip's sides open from the panel's edges to the shadow's 32px together with its corner\n * radius, so the growing edge keeps the panel's corners on every frame.\n */\n motion: [\n 'flex max-h-full max-w-full',\n '[--m3-dialog-corner:var(--md-comp-dialog-container-shape,var(--md-sys-shape-corner-extra-large))]',\n '[--m3-dialog-open-clip:inset(-32px_round_calc(var(--m3-dialog-corner)_+_32px))]',\n '[--m3-dialog-enter-clip:inset(0_0_100%_round_var(--m3-dialog-corner))]',\n '[--m3-dialog-exit-clip:inset(0_0_65%_round_var(--m3-dialog-corner))]',\n '[--m3-dialog-slide:0_-50px] [--m3-dialog-scale-origin:center]',\n 'translate-none opacity-100 [clip-path:var(--m3-dialog-open-clip)]',\n 'starting:[translate:var(--m3-dialog-slide)] starting:opacity-0',\n 'starting:[clip-path:var(--m3-dialog-enter-clip)]',\n 'data-exiting:[translate:var(--m3-dialog-slide)] data-exiting:opacity-0',\n 'data-exiting:[clip-path:var(--m3-dialog-exit-clip)]',\n 'motion-reduce:data-exiting:translate-none',\n 'motion-reduce:data-exiting:[clip-path:var(--m3-dialog-open-clip)]',\n '[transition-property:opacity,translate,clip-path] motion-reduce:[transition-property:opacity]',\n '[transition-duration:var(--md-sys-motion-spring-effects-fast-duration),var(--md-sys-motion-spring-spatial-default-duration),var(--md-sys-motion-spring-spatial-default-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing),var(--md-sys-motion-spring-spatial-default-easing),var(--md-sys-motion-spring-spatial-default-easing)]',\n 'data-exiting:[transition-duration:var(--md-sys-motion-duration-short-1),var(--md-sys-motion-duration-short-3),var(--md-sys-motion-duration-short-3)]',\n 'data-exiting:[transition-timing-function:var(--md-sys-motion-easing-linear),var(--md-sys-motion-easing-emphasized-accelerate),var(--md-sys-motion-easing-emphasized-accelerate)]',\n 'data-exiting:[transition-delay:var(--md-sys-motion-duration-short-2),0s,0s]',\n 'motion-reduce:data-exiting:[transition-delay:0s]',\n 'motion-reduce:data-exiting:[transition-duration:var(--md-sys-motion-duration-short-3)]',\n ],\n /*\n * Classes for the panel that stagger its parts (its children): every part fades in after\n * 50ms on the default effects spring (the headline, icon and supporting text, and any\n * custom child), and the actions (`data-slot=\"actions\"`) after 150ms on the fast one.\n * Leaving, they all fade out at once on the fast spring. Basic dialogs only (see the\n * `fullScreen` variants); reduced motion leaves the parts to the wrapper's fade.\n */\n parts: '',\n panel: [\n 'flex max-h-full min-w-[min(280px,calc(100vw-48px))] max-w-[560px] flex-col p-[24px]',\n 'rounded-[var(--_md-comp-dialog-container-shape)] bg-[var(--_md-comp-dialog-container-color)]',\n 'text-on-surface shadow-[shadow:var(--_md-comp-dialog-container-elevation)]',\n 'outline-none',\n // Forced colours drop the fill and shadow, so the panel draws its edge instead.\n 'forced-colors:outline-solid forced-colors:outline-1 forced-colors:outline-[CanvasText]',\n ],\n icon: [\n 'mx-auto inline-flex pb-[16px] [&>svg]:size-[24px]',\n 'text-[color:var(--_md-comp-dialog-with-icon-icon-color)]',\n ],\n title: [\n 'pb-[16px] text-headline-small font-brand',\n 'text-[color:var(--_md-comp-dialog-headline-color)]',\n ],\n header: '',\n headerButton: '',\n close: '[--md-comp-icon-button-icon-color:var(--md-sys-color-on-surface)]',\n content: [\n // 8px of room above the first field, for an outlined field's label, which sits across\n // its top edge and would be clipped by this scroller. The margin takes the same 8px off\n // the title's spacing, so nothing moves.\n '-mt-[8px] min-h-0 flex-1 overflow-y-auto pt-[8px] pb-[24px] text-body-medium',\n 'text-[color:var(--_md-comp-dialog-supporting-text-color)]',\n 'focus-ring',\n ],\n // Stacked (`data-stacked`, set by `DialogActions` when they don't fit on one line), the\n // buttons are rendered confirming action first and wrap forwards instead of in reverse:\n // the same picture, sizes included, in focus order. The custom property tells it\n // whether the layout lets them stack.\n actions: [\n 'flex flex-wrap-reverse items-center justify-end gap-[8px] [--m3-dialog-actions-stack:1]',\n 'data-stacked:flex-wrap',\n ],\n },\n variants: {\n hasIcon: {\n true: { title: 'text-center' },\n false: { title: 'text-start' },\n },\n /*\n * The dialog enters from the window edge it sits against (the spec's enter and exit\n * pattern): it slides 50px away from that edge while its container grows away from it,\n * and leaves the same way back. Centred or at the top it slides down and grows downward\n * (the reference motion), at the bottom it slides up and grows upward, and at the start\n * or end it moves and grows along the x axis, mirrored in right-to-left layouts.\n */\n placement: {\n center: { positioner: 'items-center justify-center' },\n top: {\n positioner: 'items-start justify-center',\n motion: '[--m3-dialog-scale-origin:top]',\n },\n bottom: {\n positioner: 'items-end justify-center',\n motion: [\n '[--m3-dialog-enter-clip:inset(100%_0_0_round_var(--m3-dialog-corner))]',\n '[--m3-dialog-exit-clip:inset(65%_0_0_round_var(--m3-dialog-corner))]',\n '[--m3-dialog-slide:0_50px] [--m3-dialog-scale-origin:bottom]',\n ],\n },\n start: {\n positioner: 'items-center justify-start',\n motion: [\n '[--m3-dialog-enter-clip:inset(0_100%_0_0_round_var(--m3-dialog-corner))]',\n '[--m3-dialog-exit-clip:inset(0_65%_0_0_round_var(--m3-dialog-corner))]',\n '[--m3-dialog-slide:-50px_0] [--m3-dialog-scale-origin:left]',\n 'rtl:[--m3-dialog-enter-clip:inset(0_0_0_100%_round_var(--m3-dialog-corner))]',\n 'rtl:[--m3-dialog-exit-clip:inset(0_0_0_65%_round_var(--m3-dialog-corner))]',\n 'rtl:[--m3-dialog-slide:50px_0] rtl:[--m3-dialog-scale-origin:right]',\n ],\n },\n end: {\n positioner: 'items-center justify-end',\n motion: [\n '[--m3-dialog-enter-clip:inset(0_0_0_100%_round_var(--m3-dialog-corner))]',\n '[--m3-dialog-exit-clip:inset(0_0_0_65%_round_var(--m3-dialog-corner))]',\n '[--m3-dialog-slide:50px_0] [--m3-dialog-scale-origin:right]',\n 'rtl:[--m3-dialog-enter-clip:inset(0_100%_0_0_round_var(--m3-dialog-corner))]',\n 'rtl:[--m3-dialog-exit-clip:inset(0_65%_0_0_round_var(--m3-dialog-corner))]',\n 'rtl:[--m3-dialog-slide:-50px_0] rtl:[--m3-dialog-scale-origin:left]',\n ],\n },\n },\n fullScreen: {\n never: {\n headerButton: 'hidden',\n parts: [\n 'motion-safe:*:starting:opacity-0 motion-safe:data-exiting:*:opacity-0',\n '*:[transition-property:opacity]',\n '*:[transition-duration:var(--md-sys-motion-spring-effects-default-duration)]',\n '*:[transition-timing-function:var(--md-sys-motion-spring-effects-default-easing)]',\n '*:[transition-delay:var(--md-sys-motion-duration-short-1)]',\n '*:data-[slot=actions]:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)]',\n '*:data-[slot=actions]:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]',\n '*:data-[slot=actions]:[transition-delay:var(--md-sys-motion-duration-short-3)]',\n 'data-exiting:*:[transition-duration:var(--md-sys-motion-duration-short-2)]',\n 'data-exiting:*:[transition-timing-function:var(--md-sys-motion-easing-linear)]',\n 'data-exiting:*:[transition-delay:0s] data-exiting:*:data-[slot=actions]:[transition-delay:0s]',\n 'data-exiting:*:data-[slot=actions]:[transition-duration:var(--md-sys-motion-duration-short-2)]',\n 'data-exiting:*:data-[slot=actions]:[transition-timing-function:var(--md-sys-motion-easing-linear)]',\n ],\n },\n /*\n * Full screen keeps its own motion: the dialog slides up 48px as it fades in on the slow\n * springs (the spec's speed for full-screen motion) with no clip, and slides back down\n * as it fades out on the fast ones, its parts fading with it.\n */\n always: {\n positioner: 'p-0 medium:p-0',\n motion: [\n 'size-full [--m3-dialog-slide:0_48px]',\n '[--m3-dialog-open-clip:none] [--m3-dialog-enter-clip:none] [--m3-dialog-exit-clip:none]',\n '[transition-duration:var(--md-sys-motion-spring-effects-slow-duration),var(--md-sys-motion-spring-spatial-slow-duration),var(--md-sys-motion-spring-spatial-slow-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-slow-easing),var(--md-sys-motion-spring-spatial-slow-easing),var(--md-sys-motion-spring-spatial-slow-easing)]',\n 'data-exiting:[transition-delay:0s]',\n 'data-exiting:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)]',\n 'data-exiting:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]',\n ],\n panel: [\n 'size-full max-h-none max-w-none rounded-none p-0 shadow-none',\n 'bg-surface-container-high',\n ],\n header: 'flex h-[56px] shrink-0 items-center gap-[4px] ps-[4px] pe-[12px]',\n title: 'min-w-0 flex-1 truncate ps-[4px] pb-0 text-start text-title-large',\n content: 'mt-0 px-[24px] pt-[24px]',\n actions: 'hidden',\n },\n compact: {\n positioner: 'max-medium:p-0',\n motion: [\n 'max-medium:size-full max-medium:[--m3-dialog-slide:0_48px]',\n 'max-medium:[--m3-dialog-open-clip:none] max-medium:[--m3-dialog-enter-clip:none]',\n 'max-medium:[--m3-dialog-exit-clip:none]',\n 'max-medium:[transition-duration:var(--md-sys-motion-spring-effects-slow-duration),var(--md-sys-motion-spring-spatial-slow-duration),var(--md-sys-motion-spring-spatial-slow-duration)]',\n 'max-medium:[transition-timing-function:var(--md-sys-motion-spring-effects-slow-easing),var(--md-sys-motion-spring-spatial-slow-easing),var(--md-sys-motion-spring-spatial-slow-easing)]',\n 'max-medium:data-exiting:[transition-delay:0s]',\n 'max-medium:data-exiting:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)]',\n 'max-medium:data-exiting:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]',\n ],\n parts: [\n 'medium:motion-safe:*:starting:opacity-0 medium:motion-safe:data-exiting:*:opacity-0',\n 'medium:*:[transition-property:opacity]',\n 'medium:*:[transition-duration:var(--md-sys-motion-spring-effects-default-duration)]',\n 'medium:*:[transition-timing-function:var(--md-sys-motion-spring-effects-default-easing)]',\n 'medium:*:[transition-delay:var(--md-sys-motion-duration-short-1)]',\n 'medium:*:data-[slot=actions]:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)]',\n 'medium:*:data-[slot=actions]:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]',\n 'medium:*:data-[slot=actions]:[transition-delay:var(--md-sys-motion-duration-short-3)]',\n 'medium:data-exiting:*:[transition-duration:var(--md-sys-motion-duration-short-2)]',\n 'medium:data-exiting:*:[transition-timing-function:var(--md-sys-motion-easing-linear)]',\n 'medium:data-exiting:*:[transition-delay:0s]',\n 'medium:data-exiting:*:data-[slot=actions]:[transition-delay:0s]',\n 'medium:data-exiting:*:data-[slot=actions]:[transition-duration:var(--md-sys-motion-duration-short-2)]',\n 'medium:data-exiting:*:data-[slot=actions]:[transition-timing-function:var(--md-sys-motion-easing-linear)]',\n ],\n panel: [\n 'max-medium:size-full max-medium:max-h-none max-medium:max-w-none',\n 'max-medium:rounded-none max-medium:p-0 max-medium:shadow-none',\n 'max-medium:bg-surface-container-high',\n ],\n header: [\n 'max-medium:flex max-medium:h-[56px] max-medium:shrink-0 max-medium:items-center',\n 'max-medium:gap-[4px] max-medium:ps-[4px] max-medium:pe-[12px]',\n ],\n headerButton: 'medium:hidden',\n title: [\n 'max-medium:min-w-0 max-medium:flex-1 max-medium:truncate max-medium:ps-[4px]',\n 'max-medium:pb-0 max-medium:text-start max-medium:text-title-large',\n ],\n content: 'max-medium:mt-0 max-medium:px-[24px] max-medium:pt-[24px]',\n actions: 'max-medium:hidden',\n },\n },\n /** Full screen without a header action: the actions form a 56px bottom bar. */\n bottomBar: { true: {}, false: {} },\n /** A divider under the full-screen header. */\n divider: { true: {}, false: {} },\n /** The basic dialog's optional dividers above and below `DialogContent`. */\n dividers: { true: {}, false: {} },\n /** A container transform from a trigger (`origin`) into the full-screen dialog. */\n origin: { true: {}, false: {} },\n /**\n * The basic dialog's enter and exit: `reveal`, the M3 dialog motion above, or `scale`,\n * the original pop (see the compound variants). Full-screen dialogs ignore it.\n */\n transition: { reveal: {}, scale: {} },\n /** No enter or exit motion: the dialog, its parts and the scrim appear and go at once. */\n instant: {\n true: {\n scrim: '[transition-property:none]!',\n motion: '[transition-property:none]!',\n panel: '[transition-property:none]!',\n parts: '*:[transition-property:none]!',\n },\n false: {},\n },\n },\n compoundVariants: [\n {\n fullScreen: 'always',\n bottomBar: true,\n class: {\n actions: 'flex h-[56px] shrink-0 flex-nowrap px-[24px] [--m3-dialog-actions-stack:0]',\n },\n },\n {\n fullScreen: 'compact',\n bottomBar: true,\n class: {\n actions:\n 'max-medium:flex max-medium:h-[56px] max-medium:shrink-0 max-medium:flex-nowrap max-medium:px-[24px] max-medium:[--m3-dialog-actions-stack:0]',\n },\n },\n {\n fullScreen: 'always',\n divider: true,\n class: { header: 'border-b border-outline-variant' },\n },\n {\n fullScreen: 'compact',\n divider: true,\n class: { header: 'max-medium:border-b max-medium:border-outline-variant' },\n },\n /*\n * Dividers run the panel's full width, 1px `outline-variant`, with 8px between them and\n * the content; the body keeps 24px to the actions below the bottom one.\n */\n {\n fullScreen: 'never',\n dividers: true,\n class: {\n content: 'mt-0 -mx-[24px] mb-[24px] border-y border-outline-variant px-[24px] py-[8px]',\n },\n },\n {\n fullScreen: 'compact',\n dividers: true,\n class: {\n content: [\n 'medium:mt-0 medium:-mx-[24px] medium:mb-[24px] medium:border-y medium:border-outline-variant',\n 'medium:px-[24px] medium:py-[8px]',\n ],\n },\n },\n /*\n * `transition=\"scale\"`, the library's original pop (an extension beyond the M3 dialog\n * motion): the scrim fades on the default effects spring, and the dialog fades in while it\n * scales up from 90% on the default springs, then fades out while it scales to 95% on the\n * fast ones. No clip, no slide, no staggered parts. It scales from the edge it sits\n * against (`--m3-dialog-scale-origin`: the centre for `center`, the top, bottom, start or\n * end edge otherwise). Reduced motion keeps only the fades. Basic dialogs only (from 600px\n * for `compact`).\n */\n {\n fullScreen: 'never',\n transition: 'scale',\n class: {\n scrim: [\n '[transition-duration:var(--md-sys-motion-spring-effects-default-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-default-easing)]',\n 'data-exiting:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)]',\n 'data-exiting:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]',\n ],\n motion: [\n '[--m3-dialog-slide:0_0] [--m3-dialog-open-clip:none]',\n '[--m3-dialog-enter-clip:none] [--m3-dialog-exit-clip:none]',\n 'origin-(--m3-dialog-scale-origin) scale-none starting:scale-90 data-exiting:scale-95',\n 'motion-reduce:data-exiting:scale-none',\n '[transition-property:opacity,scale] motion-reduce:[transition-property:opacity]',\n '[transition-duration:var(--md-sys-motion-spring-effects-default-duration),var(--md-sys-motion-spring-spatial-default-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-default-easing),var(--md-sys-motion-spring-spatial-default-easing)]',\n 'data-exiting:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration)]',\n 'data-exiting:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing)]',\n 'data-exiting:[transition-delay:0s]',\n ],\n },\n },\n {\n fullScreen: 'compact',\n transition: 'scale',\n class: {\n scrim: [\n 'medium:[transition-duration:var(--md-sys-motion-spring-effects-default-duration)]',\n 'medium:[transition-timing-function:var(--md-sys-motion-spring-effects-default-easing)]',\n 'medium:data-exiting:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)]',\n 'medium:data-exiting:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]',\n ],\n motion: [\n 'medium:[--m3-dialog-slide:0_0] medium:[--m3-dialog-open-clip:none]',\n 'medium:[--m3-dialog-enter-clip:none] medium:[--m3-dialog-exit-clip:none]',\n 'medium:origin-(--m3-dialog-scale-origin) medium:scale-none',\n 'medium:starting:scale-90 medium:data-exiting:scale-95',\n 'medium:motion-reduce:data-exiting:scale-none',\n 'medium:[transition-property:opacity,scale]',\n 'medium:motion-reduce:[transition-property:opacity]',\n 'medium:[transition-duration:var(--md-sys-motion-spring-effects-default-duration),var(--md-sys-motion-spring-spatial-default-duration)]',\n 'medium:[transition-timing-function:var(--md-sys-motion-spring-effects-default-easing),var(--md-sys-motion-spring-spatial-default-easing)]',\n 'medium:data-exiting:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration)]',\n 'medium:data-exiting:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing)]',\n 'medium:data-exiting:[transition-delay:0s]',\n ],\n },\n },\n /*\n * Container transform (the spec: \"Use a container transform pattern to transition a FAB\n * into a full-screen dialog\"): the clip starts on the origin's box and corners\n * (`--m3-dialog-origin`, measured by the dialog) and opens to the window on the slow\n * spatial spring, while the container colour moves from the origin's\n * (`--m3-dialog-origin-color`) and the content fades in. Leaving collapses back into the\n * origin on emphasized-accelerate over 250ms (2026-10-15), as the basic exit avoids\n * springs: the fast spatial spring collapsed the clip past the origin to nothing and\n * regrew it while still visible. The container stays opaque for the first 60% and fades\n * over the last 100ms, linear, ending with the collapse (2026-10-17, after Material\n * Components' container transform, whose return fades from 60% to 90%), so it is seen going\n * back into the origin. Reduced motion keeps only the fades (the dialog's and its content's).\n */\n {\n fullScreen: 'always',\n origin: true,\n class: {\n motion: [\n '[--m3-dialog-slide:0_0] [--m3-dialog-open-clip:inset(0_round_0)]',\n '[--m3-dialog-enter-clip:var(--m3-dialog-origin)] [--m3-dialog-exit-clip:var(--m3-dialog-origin)]',\n '[transition-duration:var(--md-sys-motion-spring-effects-fast-duration),var(--md-sys-motion-spring-spatial-slow-duration),var(--md-sys-motion-spring-spatial-slow-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing),var(--md-sys-motion-spring-spatial-slow-easing),var(--md-sys-motion-spring-spatial-slow-easing)]',\n 'data-exiting:[transition-duration:var(--md-sys-motion-duration-short-2),var(--md-sys-motion-duration-medium-1),var(--md-sys-motion-duration-medium-1)]',\n 'data-exiting:[transition-timing-function:var(--md-sys-motion-easing-linear),var(--md-sys-motion-easing-emphasized-accelerate),var(--md-sys-motion-easing-emphasized-accelerate)]',\n // Full screen's exit starts every transition at once; the fade waits here.\n 'data-exiting:[transition-delay:var(--md-sys-motion-duration-short-3),0s,0s]',\n 'motion-reduce:data-exiting:[transition-delay:0s]',\n ],\n panel: [\n '[transition-property:background-color] motion-reduce:[transition-property:none]',\n '[transition-duration:var(--md-sys-motion-spring-effects-slow-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-slow-easing)]',\n 'starting:bg-(--m3-dialog-origin-color) motion-safe:data-exiting:bg-(--m3-dialog-origin-color)',\n 'data-exiting:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)]',\n '*:[transition-property:opacity] *:starting:opacity-0 data-exiting:*:opacity-0',\n '*:[transition-duration:var(--md-sys-motion-spring-effects-slow-duration)]',\n '*:[transition-timing-function:var(--md-sys-motion-spring-effects-slow-easing)]',\n ],\n },\n },\n {\n fullScreen: 'compact',\n origin: true,\n class: {\n motion: [\n 'max-medium:[--m3-dialog-slide:0_0] max-medium:[--m3-dialog-open-clip:inset(0_round_0)]',\n 'max-medium:[--m3-dialog-enter-clip:var(--m3-dialog-origin)]',\n 'max-medium:[--m3-dialog-exit-clip:var(--m3-dialog-origin)]',\n 'max-medium:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration),var(--md-sys-motion-spring-spatial-slow-duration),var(--md-sys-motion-spring-spatial-slow-duration)]',\n 'max-medium:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing),var(--md-sys-motion-spring-spatial-slow-easing),var(--md-sys-motion-spring-spatial-slow-easing)]',\n 'max-medium:data-exiting:[transition-duration:var(--md-sys-motion-duration-short-2),var(--md-sys-motion-duration-medium-1),var(--md-sys-motion-duration-medium-1)]',\n 'max-medium:data-exiting:[transition-timing-function:var(--md-sys-motion-easing-linear),var(--md-sys-motion-easing-emphasized-accelerate),var(--md-sys-motion-easing-emphasized-accelerate)]',\n 'max-medium:data-exiting:[transition-delay:var(--md-sys-motion-duration-short-3),0s,0s]',\n // The breakpoint outranks the base reduced-motion exit: restate it, so the fade\n // keeps its duration and starts at once while the spatial springs are instant.\n 'max-medium:motion-reduce:data-exiting:[transition-duration:var(--md-sys-motion-duration-short-3)]',\n 'max-medium:motion-reduce:data-exiting:[transition-delay:0s]',\n ],\n panel: [\n 'max-medium:[transition-property:background-color]',\n 'max-medium:motion-reduce:[transition-property:none]',\n 'max-medium:[transition-duration:var(--md-sys-motion-spring-effects-slow-duration)]',\n 'max-medium:[transition-timing-function:var(--md-sys-motion-spring-effects-slow-easing)]',\n 'max-medium:starting:bg-(--m3-dialog-origin-color)',\n 'max-medium:motion-safe:data-exiting:bg-(--m3-dialog-origin-color)',\n 'max-medium:data-exiting:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)]',\n 'max-medium:*:[transition-property:opacity] max-medium:*:starting:opacity-0',\n 'max-medium:data-exiting:*:opacity-0',\n 'max-medium:*:[transition-duration:var(--md-sys-motion-spring-effects-slow-duration)]',\n 'max-medium:*:[transition-timing-function:var(--md-sys-motion-spring-effects-slow-easing)]',\n ],\n },\n },\n ],\n defaultVariants: {\n dividers: false,\n origin: false,\n instant: false,\n transition: 'reveal',\n hasIcon: false,\n fullScreen: 'never',\n placement: 'center',\n bottomBar: false,\n divider: false,\n },\n});\n\n/**\n * The variants of {@link dialogStyles}, for building on its styles. They are the recipe's keys, not\n * component props: their names (`hasIcon`, `isOpen`…) follow the classes and may change.\n */\nexport type DialogStyleProps = VariantProps<typeof dialogStyles>;\n"],"mappings":";;;AA6CA,MAAa,oBAAoB;CAAE,UAAU;CAAS,OAAO;AAAE;;AAG/D,MAAa,eAAe,GAAG;CAC7B,OAAO;EACL,OAAO;GACL;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF;EAKA,MAAM;EACN,YAAY;EAeZ,QAAQ;GACN;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF;EAQA,OAAO;EACP,OAAO;GACL;GACA;GACA;GACA;GAEA;EACF;EACA,MAAM,CACJ,qDACA,0DACF;EACA,OAAO,CACL,4CACA,oDACF;EACA,QAAQ;EACR,cAAc;EACd,OAAO;EACP,SAAS;GAIP;GACA;GACA;EACF;EAKA,SAAS,CACP,2FACA,wBACF;CACF;CACA,UAAU;EACR,SAAS;GACP,MAAM,EAAE,OAAO,cAAc;GAC7B,OAAO,EAAE,OAAO,aAAa;EAC/B;EAQA,WAAW;GACT,QAAQ,EAAE,YAAY,8BAA8B;GACpD,KAAK;IACH,YAAY;IACZ,QAAQ;GACV;GACA,QAAQ;IACN,YAAY;IACZ,QAAQ;KACN;KACA;KACA;IACF;GACF;GACA,OAAO;IACL,YAAY;IACZ,QAAQ;KACN;KACA;KACA;KACA;KACA;KACA;IACF;GACF;GACA,KAAK;IACH,YAAY;IACZ,QAAQ;KACN;KACA;KACA;KACA;KACA;KACA;IACF;GACF;EACF;EACA,YAAY;GACV,OAAO;IACL,cAAc;IACd,OAAO;KACL;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;IACF;GACF;GAMA,QAAQ;IACN,YAAY;IACZ,QAAQ;KACN;KACA;KACA;KACA;KACA;KACA;KACA;IACF;IACA,OAAO,CACL,gEACA,2BACF;IACA,QAAQ;IACR,OAAO;IACP,SAAS;IACT,SAAS;GACX;GACA,SAAS;IACP,YAAY;IACZ,QAAQ;KACN;KACA;KACA;KACA;KACA;KACA;KACA;KACA;IACF;IACA,OAAO;KACL;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;IACF;IACA,OAAO;KACL;KACA;KACA;IACF;IACA,QAAQ,CACN,mFACA,+DACF;IACA,cAAc;IACd,OAAO,CACL,gFACA,mEACF;IACA,SAAS;IACT,SAAS;GACX;EACF;;EAEA,WAAW;GAAE,MAAM,CAAC;GAAG,OAAO,CAAC;EAAE;;EAEjC,SAAS;GAAE,MAAM,CAAC;GAAG,OAAO,CAAC;EAAE;;EAE/B,UAAU;GAAE,MAAM,CAAC;GAAG,OAAO,CAAC;EAAE;;EAEhC,QAAQ;GAAE,MAAM,CAAC;GAAG,OAAO,CAAC;EAAE;;;;;EAK9B,YAAY;GAAE,QAAQ,CAAC;GAAG,OAAO,CAAC;EAAE;;EAEpC,SAAS;GACP,MAAM;IACJ,OAAO;IACP,QAAQ;IACR,OAAO;IACP,OAAO;GACT;GACA,OAAO,CAAC;EACV;CACF;CACA,kBAAkB;EAChB;GACE,YAAY;GACZ,WAAW;GACX,OAAO,EACL,SAAS,6EACX;EACF;EACA;GACE,YAAY;GACZ,WAAW;GACX,OAAO,EACL,SACE,+IACJ;EACF;EACA;GACE,YAAY;GACZ,SAAS;GACT,OAAO,EAAE,QAAQ,kCAAkC;EACrD;EACA;GACE,YAAY;GACZ,SAAS;GACT,OAAO,EAAE,QAAQ,wDAAwD;EAC3E;EAKA;GACE,YAAY;GACZ,UAAU;GACV,OAAO,EACL,SAAS,+EACX;EACF;EACA;GACE,YAAY;GACZ,UAAU;GACV,OAAO,EACL,SAAS,CACP,gGACA,kCACF,EACF;EACF;EAUA;GACE,YAAY;GACZ,YAAY;GACZ,OAAO;IACL,OAAO;KACL;KACA;KACA;KACA;IACF;IACA,QAAQ;KACN;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;IACF;GACF;EACF;EACA;GACE,YAAY;GACZ,YAAY;GACZ,OAAO;IACL,OAAO;KACL;KACA;KACA;KACA;IACF;IACA,QAAQ;KACN;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;IACF;GACF;EACF;EAcA;GACE,YAAY;GACZ,QAAQ;GACR,OAAO;IACL,QAAQ;KACN;KACA;KACA;KACA;KACA;KACA;KAEA;KACA;IACF;IACA,OAAO;KACL;KACA;KACA;KACA;KACA;KACA;KACA;KACA;IACF;GACF;EACF;EACA;GACE,YAAY;GACZ,QAAQ;GACR,OAAO;IACL,QAAQ;KACN;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KAGA;KACA;IACF;IACA,OAAO;KACL;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;IACF;GACF;EACF;CACF;CACA,iBAAiB;EACf,UAAU;EACV,QAAQ;EACR,SAAS;EACT,YAAY;EACZ,SAAS;EACT,YAAY;EACZ,WAAW;EACX,WAAW;EACX,SAAS;CACX;AACF,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { RefObject } from "react";
|
|
2
|
+
//#region src/components/menu/use-reveal-entering.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Holds a menu's reveal closed (`data-entering`) until React Aria has placed it, then lets
|
|
5
|
+
* it open, so it grows from the edge nearest its anchor.
|
|
6
|
+
*
|
|
7
|
+
* The reveal's closed clip depends on `data-placement` (from the top below the anchor, from
|
|
8
|
+
* the bottom above it, from the side beside it). A `@starting-style` rule is read the first
|
|
9
|
+
* time the element's style is resolved, which React Aria's measuring forces before it has
|
|
10
|
+
* set the placement, so every menu used to open from its top edge. Instead, the closed state
|
|
11
|
+
* is a class with no transition: once the placement is known (before the first paint, as
|
|
12
|
+
* React Aria places in a layout effect), the closed clip for that edge is resolved and the
|
|
13
|
+
* class removed, and the clip and opacity transition open from there.
|
|
14
|
+
*
|
|
15
|
+
* Should the placement never arrive, the menu opens on the next frame anyway.
|
|
16
|
+
*/
|
|
17
|
+
export declare function useRevealEntering(placement: string | null | undefined, ref: RefObject<HTMLElement | null>): boolean;
|
|
18
|
+
//#endregion
|
|
19
|
+
//# sourceMappingURL=use-reveal-entering.d.ts.map
|
|
@@ -117,7 +117,7 @@ function NavItem({ layout, enterLabel, lastIndicatorRef, icon, selectedIcon, chi
|
|
|
117
117
|
if (stateLayerRef.current && !action.disabled && (event.key === "Enter" || event.key === " ")) setRippleOrigin(stateLayerRef.current);
|
|
118
118
|
},
|
|
119
119
|
"data-nav-item": "",
|
|
120
|
-
"aria-current": selected ? "page" : void 0,
|
|
120
|
+
"aria-current": action["aria-current"] ?? (selected ? "page" : void 0),
|
|
121
121
|
"aria-describedby": [action["aria-describedby"], badge && description != null ? descriptionId : void 0].filter(Boolean).join(" ") || void 0,
|
|
122
122
|
"data-current": selected || void 0,
|
|
123
123
|
"data-tokens": mergeDataTokens(layout.startsWith("rail") ? "md-comp-nav-rail" : "md-comp-nav-bar", action),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NavItem.js","names":[],"sources":["../../../src/components/navigation/NavItem.tsx"],"sourcesContent":["'use client';\n\nimport { animate, useReducedMotion } from 'motion/react';\nimport {\n isValidElement,\n useId,\n useLayoutEffect,\n useRef,\n useState,\n type KeyboardEvent,\n type PointerEvent,\n type ReactElement,\n type ReactNode,\n type RefObject,\n} from 'react';\nimport { useM3Spring } from '../../motion/use-m3-spring';\nimport { setRippleOrigin, useRippleHold } from '../../primitives/use-m3-interaction';\nimport {\n ButtonBase,\n type ButtonBaseActionProps,\n type ButtonBaseLinkProps,\n} from '../../primitives/ButtonBase';\nimport { cn } from '../../utils/cn';\nimport { BadgedBox, type BadgeProps } from '../badge/Badge';\nimport { navItemStyles, type NavItemLayout } from './nav-item-styles';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\n\nexport interface NavItemClassNames {\n /** The item's button or link, merged with `className`. */\n root?: string;\n /** The pill around the icon that holds the active indicator and the state layer. */\n pill?: string;\n /** The active indicator: the filled pill shown on the current destination. */\n indicator?: string;\n /** The state layer over the pill (hover, focus and press). */\n stateLayer?: string;\n /** The icon. */\n icon?: string;\n /** The label text. */\n label?: string;\n /** The badge's wrapper: on the icon, or after the label in the expanded rail. */\n badge?: string;\n}\n\nexport interface NavItemOwnProps {\n /** The icon. Hidden from assistive tech; the label names the item. */\n icon: ReactElement;\n /** Icon shown while selected, e.g. a filled variant. */\n selectedIcon?: ReactElement;\n /** The label. */\n children: ReactNode;\n /** Marks the current destination (`aria-current=\"page\"`). */\n selected?: boolean;\n /**\n * A `Badge` for dynamic information, such as a count. It overlaps the icon's top end, or\n * sits after the label in the expanded rail. It describes the item to assistive tech\n * with its text, or with `badgeLabel`.\n */\n badge?: ReactElement<BadgeProps>;\n /**\n * What the badge means, read after the item's name, e.g. \"3 new messages\". Defaults to\n * the badge's `label` or text; a small (dot) badge reads its `labels.newNotification`\n * (\"New notification\"), as M3 asks of badges without a count.\n */\n badgeLabel?: string;\n /**\n * Called when the current destination is pressed again. M3: re-selecting the active\n * destination resets the page's scroll position to the top, so scroll to the top here.\n */\n onReselect?: () => void;\n /** Classes for each part, by slot. */\n classNames?: NavItemClassNames;\n}\n\ntype OmitItem<T> = Omit<T, keyof NavItemOwnProps | 'toggle'>;\n\nexport type NavItemProps = NavItemOwnProps &\n (OmitItem<ButtonBaseActionProps> | OmitItem<ButtonBaseLinkProps>);\n\n/**\n * Slides the current item's pill from the previous current item's, on the fast spatial\n * spring, changing width on the way when their labels differ. `lastIndicatorRef` holds the\n * last current pill for every item in one list; without it the pill doesn't slide, and under\n * reduced motion it fades in place.\n */\nfunction useSlidingIndicator(\n selected: boolean,\n lastIndicatorRef: RefObject<HTMLElement | null> | undefined,\n) {\n const indicatorRef = useRef<HTMLSpanElement>(null);\n const wasSelected = useRef(selected);\n const spring = useM3Spring('spatial', 'fast');\n const reducedMotion = useReducedMotion();\n useLayoutEffect(() => {\n const indicator = indicatorRef.current;\n const changed = wasSelected.current !== selected;\n wasSelected.current = selected;\n if (!lastIndicatorRef || !selected || !indicator) return;\n const previous = lastIndicatorRef.current;\n lastIndicatorRef.current = indicator;\n if (!changed || reducedMotion || !previous?.isConnected || previous === indicator) return;\n // Measure both pills at rest; an earlier slide may have left a transform or width behind.\n indicator.style.transform = '';\n indicator.style.width = '';\n const from = previous.getBoundingClientRect();\n const to = indicator.getBoundingClientRect();\n const offset = from.top - to.top;\n if (offset === 0) return;\n // Start from the old place and width now: Motion's first frame comes a frame later.\n indicator.style.transform = `translateY(${offset}px)`;\n if (from.width === to.width) {\n animate(indicator, { y: [offset, 0] }, spring);\n return;\n }\n indicator.style.width = `${from.width}px`;\n // Width animates in px; once settled the pill sizes itself to its content again.\n void animate(indicator, { y: [offset, 0], width: [from.width, to.width] }, spring).then(() => {\n indicator.style.width = '';\n });\n }, [selected, lastIndicatorRef, reducedMotion, spring]);\n return indicatorRef;\n}\n\n/**\n * What a badge tells assistive tech about its item: its `label`, its text, or, for a small\n * badge (no children), \"New notification\" (`labels.newNotification`).\n */\nfunction badgeDescription(badge: ReactElement<BadgeProps> | undefined): string | null {\n // A badge can arrive as a lazy server reference without props yet (React's dev server\n // rendering), so read it only from a real element.\n if (!isValidElement<BadgeProps>(badge)) return null;\n const { label, labels, children } = badge.props;\n if (label != null) return label;\n if (typeof children === 'string' || typeof children === 'number') return String(children);\n if (children == null || children === false) {\n return labels?.newNotification ?? 'New notification';\n }\n return null;\n}\n\n/** Shared navigation item: a link (`href`) or button with the M3 pill indicator. */\nexport function NavItem({\n layout,\n enterLabel,\n lastIndicatorRef,\n icon,\n selectedIcon,\n children,\n selected,\n badge,\n badgeLabel,\n onReselect,\n classNames,\n className,\n ...base\n}: NavItemProps & {\n layout: NavItemLayout;\n enterLabel?: boolean;\n /** The list's last current pill, so this item's pill slides from it when it becomes current. */\n lastIndicatorRef?: RefObject<HTMLElement | null>;\n}) {\n const styles = navItemStyles({ layout, enterLabel, slide: Boolean(lastIndicatorRef) });\n const indicatorRef = useSlidingIndicator(Boolean(selected), lastIndicatorRef);\n const descriptionId = useId();\n const inline = layout === 'rail-start' || layout === 'bar-start';\n // M3: badges sit on the icon, except in the expanded rail, where they follow the label.\n const badgeAfterLabel = layout === 'rail-start';\n const description = badgeLabel ?? badgeDescription(badge);\n const label = (\n <span\n data-nav-label=\"\"\n data-slot=\"label\"\n className={styles.label({ class: classNames?.label })}\n >\n {children}\n </span>\n );\n const iconFaces = selectedIcon ? (\n <>\n <span data-shown={!selected || undefined} className={styles.iconFace()}>\n {icon}\n </span>\n <span data-shown={selected || undefined} className={styles.iconFace()}>\n {selectedIcon}\n </span>\n </>\n ) : (\n icon\n );\n const action = base as ButtonBaseActionProps;\n // M3: a press ripples through the pill. The item is the pressable element, so the pill's\n // state layer gets its own press state and a ripple origin in its own box.\n const stateLayerRef = useRef<HTMLSpanElement>(null);\n const [pressed, setPressed] = useState(false);\n const rippling = useRippleHold(pressed && !action.disabled);\n return (\n <ButtonBase\n {...action}\n onPress={(event) => {\n action.onPress?.(event);\n if (selected) onReselect?.();\n }}\n onPressChange={(isPressed) => {\n action.onPressChange?.(isPressed);\n setPressed(isPressed);\n }}\n onPointerDown={(event: PointerEvent<HTMLButtonElement>) => {\n action.onPointerDown?.(event);\n if (stateLayerRef.current && !action.disabled && event.button === 0) {\n setRippleOrigin(stateLayerRef.current, event.clientX, event.clientY);\n }\n }}\n onKeyDown={(event: KeyboardEvent<HTMLButtonElement>) => {\n action.onKeyDown?.(event);\n if (\n stateLayerRef.current &&\n !action.disabled &&\n (event.key === 'Enter' || event.key === ' ')\n ) {\n setRippleOrigin(stateLayerRef.current);\n }\n }}\n data-nav-item=\"\"\n aria-current={selected ? 'page' : undefined}\n aria-describedby={\n [action['aria-describedby'], badge && description != null ? descriptionId : undefined]\n .filter(Boolean)\n .join(' ') || undefined\n }\n data-current={selected || undefined}\n // The bar's or rail's component tokens, resolved on the item for its state and layout.\n data-tokens={mergeDataTokens(\n layout.startsWith('rail') ? 'md-comp-nav-rail' : 'md-comp-nav-bar',\n action,\n )}\n data-layout={layout}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n <span data-slot=\"pill\" className={styles.pill({ class: classNames?.pill })}>\n <span\n ref={indicatorRef}\n aria-hidden=\"true\"\n data-slot=\"indicator\"\n className={styles.indicator({ class: classNames?.indicator })}\n />\n <span\n ref={stateLayerRef}\n aria-hidden=\"true\"\n data-rippling={rippling || undefined}\n data-disabled={action.disabled || undefined}\n className={styles.stateLayer({ class: classNames?.stateLayer })}\n />\n <span className={styles.content()}>\n <span\n aria-hidden=\"true\"\n data-slot=\"icon\"\n className={styles.icon({ class: classNames?.icon })}\n >\n {badge && !badgeAfterLabel ? (\n <BadgedBox badge={badge} className={styles.badgedBox({ class: classNames?.badge })}>\n <span className={styles.iconStack()}>{iconFaces}</span>\n </BadgedBox>\n ) : (\n iconFaces\n )}\n </span>\n {inline && label}\n {badge && badgeAfterLabel && (\n <span aria-hidden=\"true\" className={styles.badge({ class: classNames?.badge })}>\n {badge}\n </span>\n )}\n </span>\n </span>\n {!inline && label}\n {badge && description != null && (\n <span id={descriptionId} hidden>\n {description}\n </span>\n )}\n </ButtonBase>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"NavItem.js","names":[],"sources":["../../../src/components/navigation/NavItem.tsx"],"sourcesContent":["'use client';\n\nimport { animate, useReducedMotion } from 'motion/react';\nimport {\n isValidElement,\n useId,\n useLayoutEffect,\n useRef,\n useState,\n type KeyboardEvent,\n type PointerEvent,\n type ReactElement,\n type ReactNode,\n type RefObject,\n} from 'react';\nimport { useM3Spring } from '../../motion/use-m3-spring';\nimport { setRippleOrigin, useRippleHold } from '../../primitives/use-m3-interaction';\nimport {\n ButtonBase,\n type ButtonBaseActionProps,\n type ButtonBaseLinkProps,\n} from '../../primitives/ButtonBase';\nimport { cn } from '../../utils/cn';\nimport { BadgedBox, type BadgeProps } from '../badge/Badge';\nimport { navItemStyles, type NavItemLayout } from './nav-item-styles';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\n\nexport interface NavItemClassNames {\n /** The item's button or link, merged with `className`. */\n root?: string;\n /** The pill around the icon that holds the active indicator and the state layer. */\n pill?: string;\n /** The active indicator: the filled pill shown on the current destination. */\n indicator?: string;\n /** The state layer over the pill (hover, focus and press). */\n stateLayer?: string;\n /** The icon. */\n icon?: string;\n /** The label text. */\n label?: string;\n /** The badge's wrapper: on the icon, or after the label in the expanded rail. */\n badge?: string;\n}\n\nexport interface NavItemOwnProps {\n /** The icon. Hidden from assistive tech; the label names the item. */\n icon: ReactElement;\n /** Icon shown while selected, e.g. a filled variant. */\n selectedIcon?: ReactElement;\n /** The label. */\n children: ReactNode;\n /** Marks the current destination (`aria-current=\"page\"`). */\n selected?: boolean;\n /**\n * A `Badge` for dynamic information, such as a count. It overlaps the icon's top end, or\n * sits after the label in the expanded rail. It describes the item to assistive tech\n * with its text, or with `badgeLabel`.\n */\n badge?: ReactElement<BadgeProps>;\n /**\n * What the badge means, read after the item's name, e.g. \"3 new messages\". Defaults to\n * the badge's `label` or text; a small (dot) badge reads its `labels.newNotification`\n * (\"New notification\"), as M3 asks of badges without a count.\n */\n badgeLabel?: string;\n /**\n * Called when the current destination is pressed again. M3: re-selecting the active\n * destination resets the page's scroll position to the top, so scroll to the top here.\n */\n onReselect?: () => void;\n /** Classes for each part, by slot. */\n classNames?: NavItemClassNames;\n}\n\ntype OmitItem<T> = Omit<T, keyof NavItemOwnProps | 'toggle'>;\n\nexport type NavItemProps = NavItemOwnProps &\n (OmitItem<ButtonBaseActionProps> | OmitItem<ButtonBaseLinkProps>);\n\n/**\n * Slides the current item's pill from the previous current item's, on the fast spatial\n * spring, changing width on the way when their labels differ. `lastIndicatorRef` holds the\n * last current pill for every item in one list; without it the pill doesn't slide, and under\n * reduced motion it fades in place.\n */\nfunction useSlidingIndicator(\n selected: boolean,\n lastIndicatorRef: RefObject<HTMLElement | null> | undefined,\n) {\n const indicatorRef = useRef<HTMLSpanElement>(null);\n const wasSelected = useRef(selected);\n const spring = useM3Spring('spatial', 'fast');\n const reducedMotion = useReducedMotion();\n useLayoutEffect(() => {\n const indicator = indicatorRef.current;\n const changed = wasSelected.current !== selected;\n wasSelected.current = selected;\n if (!lastIndicatorRef || !selected || !indicator) return;\n const previous = lastIndicatorRef.current;\n lastIndicatorRef.current = indicator;\n if (!changed || reducedMotion || !previous?.isConnected || previous === indicator) return;\n // Measure both pills at rest; an earlier slide may have left a transform or width behind.\n indicator.style.transform = '';\n indicator.style.width = '';\n const from = previous.getBoundingClientRect();\n const to = indicator.getBoundingClientRect();\n const offset = from.top - to.top;\n if (offset === 0) return;\n // Start from the old place and width now: Motion's first frame comes a frame later.\n indicator.style.transform = `translateY(${offset}px)`;\n if (from.width === to.width) {\n animate(indicator, { y: [offset, 0] }, spring);\n return;\n }\n indicator.style.width = `${from.width}px`;\n // Width animates in px; once settled the pill sizes itself to its content again.\n void animate(indicator, { y: [offset, 0], width: [from.width, to.width] }, spring).then(() => {\n indicator.style.width = '';\n });\n }, [selected, lastIndicatorRef, reducedMotion, spring]);\n return indicatorRef;\n}\n\n/**\n * What a badge tells assistive tech about its item: its `label`, its text, or, for a small\n * badge (no children), \"New notification\" (`labels.newNotification`).\n */\nfunction badgeDescription(badge: ReactElement<BadgeProps> | undefined): string | null {\n // A badge can arrive as a lazy server reference without props yet (React's dev server\n // rendering), so read it only from a real element.\n if (!isValidElement<BadgeProps>(badge)) return null;\n const { label, labels, children } = badge.props;\n if (label != null) return label;\n if (typeof children === 'string' || typeof children === 'number') return String(children);\n if (children == null || children === false) {\n return labels?.newNotification ?? 'New notification';\n }\n return null;\n}\n\n/** Shared navigation item: a link (`href`) or button with the M3 pill indicator. */\nexport function NavItem({\n layout,\n enterLabel,\n lastIndicatorRef,\n icon,\n selectedIcon,\n children,\n selected,\n badge,\n badgeLabel,\n onReselect,\n classNames,\n className,\n ...base\n}: NavItemProps & {\n layout: NavItemLayout;\n enterLabel?: boolean;\n /** The list's last current pill, so this item's pill slides from it when it becomes current. */\n lastIndicatorRef?: RefObject<HTMLElement | null>;\n}) {\n const styles = navItemStyles({ layout, enterLabel, slide: Boolean(lastIndicatorRef) });\n const indicatorRef = useSlidingIndicator(Boolean(selected), lastIndicatorRef);\n const descriptionId = useId();\n const inline = layout === 'rail-start' || layout === 'bar-start';\n // M3: badges sit on the icon, except in the expanded rail, where they follow the label.\n const badgeAfterLabel = layout === 'rail-start';\n const description = badgeLabel ?? badgeDescription(badge);\n const label = (\n <span\n data-nav-label=\"\"\n data-slot=\"label\"\n className={styles.label({ class: classNames?.label })}\n >\n {children}\n </span>\n );\n const iconFaces = selectedIcon ? (\n <>\n <span data-shown={!selected || undefined} className={styles.iconFace()}>\n {icon}\n </span>\n <span data-shown={selected || undefined} className={styles.iconFace()}>\n {selectedIcon}\n </span>\n </>\n ) : (\n icon\n );\n const action = base as ButtonBaseActionProps;\n // M3: a press ripples through the pill. The item is the pressable element, so the pill's\n // state layer gets its own press state and a ripple origin in its own box.\n const stateLayerRef = useRef<HTMLSpanElement>(null);\n const [pressed, setPressed] = useState(false);\n const rippling = useRippleHold(pressed && !action.disabled);\n return (\n <ButtonBase\n {...action}\n onPress={(event) => {\n action.onPress?.(event);\n if (selected) onReselect?.();\n }}\n onPressChange={(isPressed) => {\n action.onPressChange?.(isPressed);\n setPressed(isPressed);\n }}\n onPointerDown={(event: PointerEvent<HTMLButtonElement>) => {\n action.onPointerDown?.(event);\n if (stateLayerRef.current && !action.disabled && event.button === 0) {\n setRippleOrigin(stateLayerRef.current, event.clientX, event.clientY);\n }\n }}\n onKeyDown={(event: KeyboardEvent<HTMLButtonElement>) => {\n action.onKeyDown?.(event);\n if (\n stateLayerRef.current &&\n !action.disabled &&\n (event.key === 'Enter' || event.key === ' ')\n ) {\n setRippleOrigin(stateLayerRef.current);\n }\n }}\n data-nav-item=\"\"\n // An explicit value wins, for a destination that shows as current without being the page.\n aria-current={action['aria-current'] ?? (selected ? 'page' : undefined)}\n aria-describedby={\n [action['aria-describedby'], badge && description != null ? descriptionId : undefined]\n .filter(Boolean)\n .join(' ') || undefined\n }\n data-current={selected || undefined}\n // The bar's or rail's component tokens, resolved on the item for its state and layout.\n data-tokens={mergeDataTokens(\n layout.startsWith('rail') ? 'md-comp-nav-rail' : 'md-comp-nav-bar',\n action,\n )}\n data-layout={layout}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n <span data-slot=\"pill\" className={styles.pill({ class: classNames?.pill })}>\n <span\n ref={indicatorRef}\n aria-hidden=\"true\"\n data-slot=\"indicator\"\n className={styles.indicator({ class: classNames?.indicator })}\n />\n <span\n ref={stateLayerRef}\n aria-hidden=\"true\"\n data-rippling={rippling || undefined}\n data-disabled={action.disabled || undefined}\n className={styles.stateLayer({ class: classNames?.stateLayer })}\n />\n <span className={styles.content()}>\n <span\n aria-hidden=\"true\"\n data-slot=\"icon\"\n className={styles.icon({ class: classNames?.icon })}\n >\n {badge && !badgeAfterLabel ? (\n <BadgedBox badge={badge} className={styles.badgedBox({ class: classNames?.badge })}>\n <span className={styles.iconStack()}>{iconFaces}</span>\n </BadgedBox>\n ) : (\n iconFaces\n )}\n </span>\n {inline && label}\n {badge && badgeAfterLabel && (\n <span aria-hidden=\"true\" className={styles.badge({ class: classNames?.badge })}>\n {badge}\n </span>\n )}\n </span>\n </span>\n {!inline && label}\n {badge && description != null && (\n <span id={descriptionId} hidden>\n {description}\n </span>\n )}\n </ButtonBase>\n );\n}\n"],"mappings":"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"}
|
|
@@ -35,8 +35,13 @@ interface NavigationRailOwnProps {
|
|
|
35
35
|
* @default false
|
|
36
36
|
*/
|
|
37
37
|
modal?: boolean;
|
|
38
|
-
/**
|
|
39
|
-
|
|
38
|
+
/**
|
|
39
|
+
* With `modal`, show nothing while collapsed (the expanded rail slides in). `compact` does so
|
|
40
|
+
* only below 600px wide, in CSS so the server's HTML is right: one rail then serves as the
|
|
41
|
+
* rail on larger windows and as the drawer on a phone.
|
|
42
|
+
* @default false
|
|
43
|
+
*/
|
|
44
|
+
hideOnCollapse?: boolean | 'compact';
|
|
40
45
|
/** Content above the items, e.g. a menu button and a FAB; a function receives the state. */
|
|
41
46
|
header?: ReactNode | ((state: {
|
|
42
47
|
expanded: boolean;
|
|
@@ -85,6 +90,20 @@ export type NavigationRailProps = NavigationRailOwnProps & Naming & Omit<Compone
|
|
|
85
90
|
*/
|
|
86
91
|
export declare function NavigationRail({ expanded, defaultExpanded, onExpandedChange, modal, hideOnCollapse, header, itemAlignment, divider, indicatorMotion, children, classNames, className, style, ...rest }: NavigationRailProps): import("react").JSX.Element;
|
|
87
92
|
export type NavigationRailItemProps = NavItemProps;
|
|
93
|
+
/** What {@link useNavigationRail} reports about the rail around a component. */
|
|
94
|
+
export interface NavigationRailState {
|
|
95
|
+
/** Whether the rail shows its labels beside the icons (expanded, or the modal sheet). */
|
|
96
|
+
expanded: boolean;
|
|
97
|
+
/** Whether this is the rail's modal sheet. */
|
|
98
|
+
modal: boolean;
|
|
99
|
+
/** Collapses the modal sheet; does nothing in the rail itself. */
|
|
100
|
+
close: () => void;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The rail around a component, for a custom destination that sits in a {@link NavigationRail}
|
|
104
|
+
* beside its items and changes with it. Throws outside a rail.
|
|
105
|
+
*/
|
|
106
|
+
export declare function useNavigationRail(): NavigationRailState;
|
|
88
107
|
/** A destination in a {@link NavigationRail}: a link (`href`) or a button. */
|
|
89
108
|
export declare function NavigationRailItem({ onPress, ...props }: NavigationRailItemProps): import("react").JSX.Element;
|
|
90
109
|
//#endregion
|
|
@@ -124,13 +124,14 @@ function NavigationRail({ expanded, defaultExpanded = false, onExpandedChange, m
|
|
|
124
124
|
expanded: inlineExpanded,
|
|
125
125
|
itemAlignment,
|
|
126
126
|
divider,
|
|
127
|
-
slidingIndicator: slide && inlineExpanded
|
|
127
|
+
slidingIndicator: slide && inlineExpanded,
|
|
128
|
+
hiddenOnCompact: modal && hideOnCollapse === "compact"
|
|
128
129
|
});
|
|
129
130
|
const headerContent = typeof header === "function" ? header({
|
|
130
131
|
expanded: isExpanded,
|
|
131
132
|
toggle
|
|
132
133
|
}) : header;
|
|
133
|
-
return /* @__PURE__ */ jsxs(Fragment$1, { children: [!(modal && hideOnCollapse) && /* @__PURE__ */ jsx("nav", {
|
|
134
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [!(modal && hideOnCollapse === true) && /* @__PURE__ */ jsx("nav", {
|
|
134
135
|
...rest,
|
|
135
136
|
ref: rootRef,
|
|
136
137
|
style: {
|
|
@@ -166,7 +167,7 @@ function NavigationRail({ expanded, defaultExpanded = false, onExpandedChange, m
|
|
|
166
167
|
onClose: () => setExpanded(false),
|
|
167
168
|
header: headerContent,
|
|
168
169
|
naming: rest,
|
|
169
|
-
slide: hideOnCollapse,
|
|
170
|
+
slide: hideOnCollapse !== false,
|
|
170
171
|
itemAlignment,
|
|
171
172
|
slidingIndicator: slide,
|
|
172
173
|
railRef: rootRef,
|
|
@@ -264,6 +265,19 @@ function ModalSheet({ state, isExiting, exitProps, header, naming, slide, itemAl
|
|
|
264
265
|
})]
|
|
265
266
|
})] });
|
|
266
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* The rail around a component, for a custom destination that sits in a {@link NavigationRail}
|
|
270
|
+
* beside its items and changes with it. Throws outside a rail.
|
|
271
|
+
*/
|
|
272
|
+
function useNavigationRail() {
|
|
273
|
+
const rail = useContext(RailContext);
|
|
274
|
+
if (!rail) throw new Error("useNavigationRail must be used inside a NavigationRail");
|
|
275
|
+
return {
|
|
276
|
+
expanded: rail.layout === "rail-start",
|
|
277
|
+
modal: rail.onNavigate !== void 0,
|
|
278
|
+
close: () => rail.onNavigate?.()
|
|
279
|
+
};
|
|
280
|
+
}
|
|
267
281
|
/** A destination in a {@link NavigationRail}: a link (`href`) or a button. */
|
|
268
282
|
function NavigationRailItem({ onPress, ...props }) {
|
|
269
283
|
const rail = useContext(RailContext);
|
|
@@ -280,6 +294,6 @@ function NavigationRailItem({ onPress, ...props }) {
|
|
|
280
294
|
});
|
|
281
295
|
}
|
|
282
296
|
//#endregion
|
|
283
|
-
export { NavigationRail, NavigationRailItem };
|
|
297
|
+
export { NavigationRail, NavigationRailItem, useNavigationRail };
|
|
284
298
|
|
|
285
299
|
//# sourceMappingURL=NavigationRail.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NavigationRail.js","names":["Overlay"],"sources":["../../../src/components/navigation/NavigationRail.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n useContext,\n useLayoutEffect,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type CSSProperties,\n type ReactNode,\n type RefObject,\n} from 'react';\nimport { mergeProps, useDialog, useModalOverlay } from 'react-aria';\nimport { useOverlayTriggerState, type OverlayTriggerState } from 'react-stately';\nimport { useControlledState } from 'react-stately/useControlledState';\nimport { Overlay } from '../../primitives/Overlay';\nimport { usePresence } from '../../primitives/use-presence';\nimport { cn } from '../../utils/cn';\nimport type { NavItemLayout } from './nav-item-styles';\nimport { DensityZeroButtons } from '../button/DensityZeroButtons';\nimport { NavItem, type NavItemProps } from './NavItem';\nimport { navigationRailStyles } from './navigation-styles';\nimport { onItemArrowKeys } from './use-item-arrow-keys';\nimport { useRailItemMorph } from './use-rail-item-morph';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\n\n/** Registered in utilities.css (96px); every rail inset is derived from it. */\nconst COLLAPSED_WIDTH_VAR = '--m3-rail-collapsed-width';\nconst DEFAULT_COLLAPSED_WIDTH = 96;\nconst EXPANDED_MIN = 220;\nconst EXPANDED_MAX = 360;\n/** Elements that take focus with Tab, for the modal rail's initial focus. */\nconst FOCUSABLE =\n 'a[href]:not([aria-disabled=\"true\"]), button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * The item chrome beside an expanded label: an item inset each side, (collapsed − 56) / 2,\n * plus 16px pill padding each side, the 24px icon and the 8px gap. 104px at 96px.\n */\nfunction expandedChrome(el: HTMLElement) {\n const collapsed =\n Number.parseFloat(getComputedStyle(el).getPropertyValue(COLLAPSED_WIDTH_VAR)) ||\n DEFAULT_COLLAPSED_WIDTH;\n return collapsed - 56 + 64;\n}\n\ninterface RailContextValue {\n layout: NavItemLayout;\n enterLabel: boolean;\n /** The expanded list's last current pill, which the next current item's pill slides from. */\n lastIndicatorRef?: RefObject<HTMLElement | null>;\n /** The modal rail collapses after an item is chosen. */\n onNavigate?: () => void;\n}\n\nconst RailContext = createContext<RailContextValue | null>(null);\n\ntype Naming =\n | {\n /** Accessible name for the navigation rail. Required unless `aria-labelledby` is set. */\n 'aria-label': string;\n }\n | {\n /** The id of the element that names the navigation rail. Required unless `aria-label` is set. */\n 'aria-labelledby': string;\n };\n\nexport interface NavigationRailClassNames {\n /** The `<nav>` container, merged with `className`. */\n root?: string;\n /** The in-flow rail's scrolling column, which holds the 44px top inset. */\n body?: string;\n /** The content above the items (`header`). */\n header?: string;\n /** The list of items. */\n items?: string;\n /** The expanded modal rail's sheet. */\n sheet?: string;\n /** The scrim behind the expanded modal rail. */\n scrim?: string;\n}\n\ninterface NavigationRailOwnProps {\n /** Whether the rail is expanded (controlled). */\n expanded?: boolean;\n /** Whether the rail starts expanded (uncontrolled). @default false */\n defaultExpanded?: boolean;\n /** Called when the rail expands or collapses. */\n onExpandedChange?: (expanded: boolean) => void;\n /**\n * Expand over the page as a modal sheet with a scrim instead of pushing content.\n * Pressing outside, Escape or choosing an item collapses it.\n * @default false\n */\n modal?: boolean;\n /** With `modal`, show nothing while collapsed (the expanded rail slides in). @default false */\n hideOnCollapse?: boolean;\n /** Content above the items, e.g. a menu button and a FAB; a function receives the state. */\n header?: ReactNode | ((state: { expanded: boolean; toggle: () => void }) => ReactNode);\n /**\n * Align the items as a group to the top, or to the middle of the space below the header\n * (M3 recommends centre on tablets, within easier reach). The header stays at the top.\n * @default \"top\"\n */\n itemAlignment?: 'top' | 'center';\n /** Draw a divider on the edge next to the content. @default false */\n divider?: boolean;\n /**\n * How the current item's pill moves to a newly chosen item. `expand` (M3: \"the active\n * indicator expands from the center of the icon\") grows it in place. `slide` moves it from\n * the previously chosen item on the fast spatial spring while the rail is expanded (in\n * place or modal), as in Google's Expressive showcase; an extension beyond the M3 spec.\n * The collapsed rail always expands, and reduced motion fades the pill in place.\n * @default \"expand\"\n */\n indicatorMotion?: 'expand' | 'slide';\n /** `NavigationRailItem` elements. */\n children: ReactNode;\n /** Classes for each part, by slot. */\n classNames?: NavigationRailClassNames;\n}\n\nexport type NavigationRailProps = NavigationRailOwnProps &\n Naming &\n Omit<\n ComponentPropsWithRef<'nav'>,\n keyof NavigationRailOwnProps | 'aria-label' | 'aria-labelledby'\n >;\n\n/**\n * Expanded width: the widest label on one line plus the item chrome, 220–360px (Compose).\n * Past 360px labels wrap.\n */\nfunction useExpandedWidth(ref: RefObject<HTMLElement | null>, active: boolean, content: ReactNode) {\n const [width, setWidth] = useState(EXPANDED_MIN);\n useLayoutEffect(() => {\n const el = ref.current;\n if (!active || !el) return;\n const labels = [...el.querySelectorAll<HTMLElement>('[data-nav-label]')];\n // Labels wrap in a narrow rail; measure each on one line.\n const widest = Math.max(\n 0,\n ...labels.map((label) => {\n label.style.whiteSpace = 'nowrap';\n const natural = label.scrollWidth;\n label.style.whiteSpace = '';\n return natural;\n }),\n );\n const header = el.querySelector<HTMLElement>('[data-rail-header]');\n const headerWidth = header ? header.scrollWidth : 0;\n const chrome = expandedChrome(el);\n setWidth(Math.min(Math.max(widest + chrome, headerWidth, EXPANDED_MIN), EXPANDED_MAX));\n }, [ref, active, content]);\n return width;\n}\n\n/**\n * Whether `ref` is still widening after it became `expanding` (or mounted expanding, as the\n * modal sheet grows from the collapsed width): from that render until its width transition\n * ends, and false at once when none runs (reduced motion, or a consumer width). Meanwhile the\n * rail keeps its labels on one line, so none wraps letter by letter in a rail that is still\n * narrow and no icon jumps; the rail's clipping hides what doesn't fit yet.\n */\nfunction useWidening(ref: RefObject<HTMLElement | null>, expanding: boolean) {\n const [widening, setWidening] = useState(expanding);\n const [wasExpanding, setWasExpanding] = useState(expanding);\n if (expanding !== wasExpanding) {\n setWasExpanding(expanding);\n setWidening(expanding);\n }\n useLayoutEffect(() => {\n const el = ref.current;\n if (!widening || !el) return;\n // Reading the running transitions resolves this render's styles, starting the spring.\n const running =\n typeof el.getAnimations === 'function' &&\n el\n .getAnimations()\n .some(\n (animation) =>\n 'transitionProperty' in animation && animation.transitionProperty === 'width',\n );\n if (!running) {\n setWidening(false);\n return;\n }\n // A later width (the measured one replacing the minimum) retargets the same spring.\n const done = (event: TransitionEvent) => {\n if (event.target === el && event.propertyName === 'width') setWidening(false);\n };\n el.addEventListener('transitionend', done);\n return () => el.removeEventListener('transitionend', done);\n }, [ref, widening]);\n return widening;\n}\n\n/**\n * M3 Expressive navigation rail: destinations along the start edge. Collapsed (96px) items\n * show the icon above the label; expanded (220–360px) they show it beside the label and\n * the rail's width springs open. With `modal`, the expanded rail opens over the page as a\n * sheet with a scrim, focusing its first interactive element. Put a menu button in\n * `header` to toggle `expanded`. Tab or the Up / Down arrows move between items, and\n * labels wrap rather than truncate.\n *\n * Icons stay on one line through the middle of the collapsed rail in both states. For a\n * different collapsed width, set `--m3-rail-collapsed-width` (e.g. the class\n * `[--m3-rail-collapsed-width:80px]`) rather than a `w-*` class; M3's widths are 96px\n * (Expressive) and 80px (baseline).\n *\n * @example\n * <NavigationRail aria-label=\"Main\" header={({ toggle }) => <IconButton icon={<MenuIcon />} aria-label=\"Menu\" onPress={toggle} />}>\n * <NavigationRailItem href=\"/inbox\" icon={<InboxIcon />} selected>Inbox</NavigationRailItem>\n * </NavigationRail>\n */\nexport function NavigationRail({\n expanded,\n defaultExpanded = false,\n onExpandedChange,\n modal = false,\n hideOnCollapse = false,\n header,\n itemAlignment = 'top',\n divider = false,\n indicatorMotion = 'expand',\n children,\n classNames,\n className,\n style,\n ...rest\n}: NavigationRailProps) {\n const [isExpanded, setExpanded] = useControlledState(expanded, defaultExpanded, onExpandedChange);\n const toggle = () => setExpanded(!isExpanded);\n const inlineExpanded = isExpanded && !modal;\n const rootRef = useRef<HTMLElement>(null);\n const lastIndicatorRef = useRef<HTMLElement>(null);\n const slide = indicatorMotion === 'slide';\n const width = useExpandedWidth(rootRef, inlineExpanded, children);\n const widening = useWidening(rootRef, inlineExpanded);\n // Labels fade in at their new place once the rail has changed mode (not on first render).\n const [mode, setMode] = useState(inlineExpanded);\n const [modeChanged, setModeChanged] = useState(false);\n const captureItems = useRailItemMorph(rootRef, mode);\n if (mode !== inlineExpanded) {\n // The DOM still shows the old layout: note where each item's icon is.\n captureItems();\n setMode(inlineExpanded);\n setModeChanged(true);\n }\n\n const styles = navigationRailStyles({\n expanded: inlineExpanded,\n itemAlignment,\n divider,\n slidingIndicator: slide && inlineExpanded,\n });\n const headerContent =\n typeof header === 'function' ? header({ expanded: isExpanded, toggle }) : header;\n const showInline = !(modal && hideOnCollapse);\n\n return (\n <>\n {showInline && (\n <nav\n {...rest}\n ref={rootRef}\n style={\n {\n '--m3-rail-width': inlineExpanded ? `${width}px` : `var(${COLLAPSED_WIDTH_VAR})`,\n ...style,\n } as CSSProperties\n }\n data-tokens={mergeDataTokens('md-comp-nav-rail', rest)}\n data-expanded={inlineExpanded || undefined}\n data-widening={widening || undefined}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n <div className={styles.body({ class: classNames?.body })}>\n {headerContent != null && (\n <div data-rail-header=\"\" className={styles.header({ class: classNames?.header })}>\n <DensityZeroButtons>{headerContent}</DensityZeroButtons>\n </div>\n )}\n <div\n data-item-alignment={itemAlignment}\n onKeyDown={onItemArrowKeys('vertical')}\n className={styles.items({ class: classNames?.items })}\n >\n <RailContext\n value={{\n layout: inlineExpanded ? 'rail-start' : 'rail-top',\n enterLabel: modeChanged,\n ...(slide && inlineExpanded && { lastIndicatorRef }),\n }}\n >\n {children}\n </RailContext>\n </div>\n </div>\n </nav>\n )}\n {modal && (\n <ModalRail\n open={isExpanded}\n onClose={() => setExpanded(false)}\n header={headerContent}\n naming={rest as Naming}\n slide={hideOnCollapse}\n itemAlignment={itemAlignment}\n slidingIndicator={slide}\n railRef={rootRef}\n classNames={classNames}\n >\n {children}\n </ModalRail>\n )}\n </>\n );\n}\n\n/** The sheet's exit transitions; whichever runs marks the end of its exit. */\nconst SHEET_EXIT_PROPERTIES = ['width', 'translate', 'opacity'] as const;\n\nfunction ModalRail({\n open,\n onClose,\n header,\n naming,\n slide,\n itemAlignment,\n slidingIndicator,\n railRef,\n classNames,\n children,\n}: {\n open: boolean;\n onClose: () => void;\n header: ReactNode;\n naming: Naming;\n slide: boolean;\n itemAlignment: 'top' | 'center';\n /** `indicatorMotion=\"slide\"`: the pill slides between items. */\n slidingIndicator: boolean;\n railRef: RefObject<HTMLElement | null>;\n classNames?: NavigationRailClassNames;\n children: ReactNode;\n}) {\n const state = useOverlayTriggerState({\n isOpen: open,\n onOpenChange: (isOpen) => {\n if (!isOpen) onClose();\n },\n });\n // Presence ends with the sheet's own motion, not the scrim's shorter fade, which removed\n // the sheet mid-spring inside the collapsed rail (2026-10-15). Width when it shrinks back,\n // translate when it slides out, opacity under reduced motion.\n const { isPresent, isExiting, exitProps } = usePresence(open, SHEET_EXIT_PROPERTIES);\n if (!isPresent) return null;\n return (\n <Overlay exiting={isExiting}>\n <ModalSheet\n state={state}\n isExiting={isExiting}\n exitProps={exitProps}\n header={header}\n naming={naming}\n slide={slide}\n itemAlignment={itemAlignment}\n slidingIndicator={slidingIndicator}\n railRef={railRef}\n classNames={classNames}\n >\n {children}\n </ModalSheet>\n </Overlay>\n );\n}\n\nfunction ModalSheet({\n state,\n isExiting,\n exitProps,\n header,\n naming,\n slide,\n itemAlignment,\n slidingIndicator,\n railRef,\n classNames,\n children,\n}: {\n state: OverlayTriggerState;\n isExiting: boolean;\n exitProps: ReturnType<typeof usePresence>['exitProps'];\n header: ReactNode;\n naming: Naming;\n slide: boolean;\n itemAlignment: 'top' | 'center';\n /** `indicatorMotion=\"slide\"`: the pill slides between items. */\n slidingIndicator: boolean;\n railRef: RefObject<HTMLElement | null>;\n classNames?: NavigationRailClassNames;\n children: ReactNode;\n}) {\n const ref = useRef<HTMLDivElement>(null);\n const lastIndicatorRef = useRef<HTMLElement>(null);\n const { modalProps, underlayProps } = useModalOverlay({ isDismissable: !isExiting }, state, ref);\n const { dialogProps } = useDialog(naming, ref);\n // The sheet is portalled out of the rail, so it can't inherit a collapsed width set on the\n // rail itself. Copy it before the width is measured and the sheet first paints.\n useLayoutEffect(() => {\n const rail = railRef.current;\n if (!rail || !ref.current) return;\n ref.current.style.setProperty(\n COLLAPSED_WIDTH_VAR,\n getComputedStyle(rail).getPropertyValue(COLLAPSED_WIDTH_VAR),\n );\n }, [railRef]);\n // M3: initial focus lands on the first interactive item, whether it's the menu button,\n // the FAB or the first destination (useDialog would otherwise focus the sheet itself).\n useLayoutEffect(() => {\n const first = ref.current?.querySelector<HTMLElement>(FOCUSABLE);\n first?.focus();\n }, []);\n const width = useExpandedWidth(ref, true, children);\n const widening = useWidening(ref, true);\n const styles = navigationRailStyles({\n expanded: true,\n sheetEntry: slide ? 'slide' : 'grow',\n itemAlignment,\n slidingIndicator,\n });\n const exiting = isExiting || undefined;\n return (\n <>\n <div\n {...underlayProps}\n aria-hidden=\"true\"\n data-exiting={exiting}\n className={styles.scrim({ class: classNames?.scrim })}\n />\n <div\n {...mergeProps(modalProps, dialogProps, exitProps)}\n ref={ref}\n data-tokens=\"md-comp-nav-rail\"\n data-modal=\"\"\n data-exiting={exiting}\n data-widening={widening || undefined}\n style={{ '--m3-rail-width': `${width}px` } as CSSProperties}\n className={styles.sheet({ class: classNames?.sheet })}\n >\n {header != null && (\n <div data-rail-header=\"\" className={styles.header({ class: classNames?.header })}>\n <DensityZeroButtons>{header}</DensityZeroButtons>\n </div>\n )}\n <nav\n aria-label={'aria-label' in naming ? naming['aria-label'] : undefined}\n aria-labelledby={'aria-labelledby' in naming ? naming['aria-labelledby'] : undefined}\n data-item-alignment={itemAlignment}\n onKeyDown={onItemArrowKeys('vertical')}\n className={styles.items({ class: classNames?.items })}\n >\n <RailContext\n value={{\n layout: 'rail-start',\n enterLabel: false,\n ...(slidingIndicator && { lastIndicatorRef }),\n onNavigate: state.close,\n }}\n >\n {children}\n </RailContext>\n </nav>\n </div>\n </>\n );\n}\n\nexport type NavigationRailItemProps = NavItemProps;\n\n/** A destination in a {@link NavigationRail}: a link (`href`) or a button. */\nexport function NavigationRailItem({ onPress, ...props }: NavigationRailItemProps) {\n const rail = useContext(RailContext);\n if (!rail) throw new Error('NavigationRailItem must be rendered inside a NavigationRail');\n return (\n <NavItem\n {...(props as NavItemProps)}\n layout={rail.layout}\n enterLabel={rail.enterLabel}\n {...(rail.lastIndicatorRef && { lastIndicatorRef: rail.lastIndicatorRef })}\n onPress={(event) => {\n onPress?.(event);\n rail.onNavigate?.();\n }}\n />\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"NavigationRail.js","names":["Overlay"],"sources":["../../../src/components/navigation/NavigationRail.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n useContext,\n useLayoutEffect,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type CSSProperties,\n type ReactNode,\n type RefObject,\n} from 'react';\nimport { mergeProps, useDialog, useModalOverlay } from 'react-aria';\nimport { useOverlayTriggerState, type OverlayTriggerState } from 'react-stately';\nimport { useControlledState } from 'react-stately/useControlledState';\nimport { Overlay } from '../../primitives/Overlay';\nimport { usePresence } from '../../primitives/use-presence';\nimport { cn } from '../../utils/cn';\nimport type { NavItemLayout } from './nav-item-styles';\nimport { DensityZeroButtons } from '../button/DensityZeroButtons';\nimport { NavItem, type NavItemProps } from './NavItem';\nimport { navigationRailStyles } from './navigation-styles';\nimport { onItemArrowKeys } from './use-item-arrow-keys';\nimport { useRailItemMorph } from './use-rail-item-morph';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\n\n/** Registered in utilities.css (96px); every rail inset is derived from it. */\nconst COLLAPSED_WIDTH_VAR = '--m3-rail-collapsed-width';\nconst DEFAULT_COLLAPSED_WIDTH = 96;\nconst EXPANDED_MIN = 220;\nconst EXPANDED_MAX = 360;\n/** Elements that take focus with Tab, for the modal rail's initial focus. */\nconst FOCUSABLE =\n 'a[href]:not([aria-disabled=\"true\"]), button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * The item chrome beside an expanded label: an item inset each side, (collapsed − 56) / 2,\n * plus 16px pill padding each side, the 24px icon and the 8px gap. 104px at 96px.\n */\nfunction expandedChrome(el: HTMLElement) {\n const collapsed =\n Number.parseFloat(getComputedStyle(el).getPropertyValue(COLLAPSED_WIDTH_VAR)) ||\n DEFAULT_COLLAPSED_WIDTH;\n return collapsed - 56 + 64;\n}\n\ninterface RailContextValue {\n layout: NavItemLayout;\n enterLabel: boolean;\n /** The expanded list's last current pill, which the next current item's pill slides from. */\n lastIndicatorRef?: RefObject<HTMLElement | null>;\n /** The modal rail collapses after an item is chosen. */\n onNavigate?: () => void;\n}\n\nconst RailContext = createContext<RailContextValue | null>(null);\n\ntype Naming =\n | {\n /** Accessible name for the navigation rail. Required unless `aria-labelledby` is set. */\n 'aria-label': string;\n }\n | {\n /** The id of the element that names the navigation rail. Required unless `aria-label` is set. */\n 'aria-labelledby': string;\n };\n\nexport interface NavigationRailClassNames {\n /** The `<nav>` container, merged with `className`. */\n root?: string;\n /** The in-flow rail's scrolling column, which holds the 44px top inset. */\n body?: string;\n /** The content above the items (`header`). */\n header?: string;\n /** The list of items. */\n items?: string;\n /** The expanded modal rail's sheet. */\n sheet?: string;\n /** The scrim behind the expanded modal rail. */\n scrim?: string;\n}\n\ninterface NavigationRailOwnProps {\n /** Whether the rail is expanded (controlled). */\n expanded?: boolean;\n /** Whether the rail starts expanded (uncontrolled). @default false */\n defaultExpanded?: boolean;\n /** Called when the rail expands or collapses. */\n onExpandedChange?: (expanded: boolean) => void;\n /**\n * Expand over the page as a modal sheet with a scrim instead of pushing content.\n * Pressing outside, Escape or choosing an item collapses it.\n * @default false\n */\n modal?: boolean;\n /**\n * With `modal`, show nothing while collapsed (the expanded rail slides in). `compact` does so\n * only below 600px wide, in CSS so the server's HTML is right: one rail then serves as the\n * rail on larger windows and as the drawer on a phone.\n * @default false\n */\n hideOnCollapse?: boolean | 'compact';\n /** Content above the items, e.g. a menu button and a FAB; a function receives the state. */\n header?: ReactNode | ((state: { expanded: boolean; toggle: () => void }) => ReactNode);\n /**\n * Align the items as a group to the top, or to the middle of the space below the header\n * (M3 recommends centre on tablets, within easier reach). The header stays at the top.\n * @default \"top\"\n */\n itemAlignment?: 'top' | 'center';\n /** Draw a divider on the edge next to the content. @default false */\n divider?: boolean;\n /**\n * How the current item's pill moves to a newly chosen item. `expand` (M3: \"the active\n * indicator expands from the center of the icon\") grows it in place. `slide` moves it from\n * the previously chosen item on the fast spatial spring while the rail is expanded (in\n * place or modal), as in Google's Expressive showcase; an extension beyond the M3 spec.\n * The collapsed rail always expands, and reduced motion fades the pill in place.\n * @default \"expand\"\n */\n indicatorMotion?: 'expand' | 'slide';\n /** `NavigationRailItem` elements. */\n children: ReactNode;\n /** Classes for each part, by slot. */\n classNames?: NavigationRailClassNames;\n}\n\nexport type NavigationRailProps = NavigationRailOwnProps &\n Naming &\n Omit<\n ComponentPropsWithRef<'nav'>,\n keyof NavigationRailOwnProps | 'aria-label' | 'aria-labelledby'\n >;\n\n/**\n * Expanded width: the widest label on one line plus the item chrome, 220–360px (Compose).\n * Past 360px labels wrap.\n */\nfunction useExpandedWidth(ref: RefObject<HTMLElement | null>, active: boolean, content: ReactNode) {\n const [width, setWidth] = useState(EXPANDED_MIN);\n useLayoutEffect(() => {\n const el = ref.current;\n if (!active || !el) return;\n const labels = [...el.querySelectorAll<HTMLElement>('[data-nav-label]')];\n // Labels wrap in a narrow rail; measure each on one line.\n const widest = Math.max(\n 0,\n ...labels.map((label) => {\n label.style.whiteSpace = 'nowrap';\n const natural = label.scrollWidth;\n label.style.whiteSpace = '';\n return natural;\n }),\n );\n const header = el.querySelector<HTMLElement>('[data-rail-header]');\n const headerWidth = header ? header.scrollWidth : 0;\n const chrome = expandedChrome(el);\n setWidth(Math.min(Math.max(widest + chrome, headerWidth, EXPANDED_MIN), EXPANDED_MAX));\n }, [ref, active, content]);\n return width;\n}\n\n/**\n * Whether `ref` is still widening after it became `expanding` (or mounted expanding, as the\n * modal sheet grows from the collapsed width): from that render until its width transition\n * ends, and false at once when none runs (reduced motion, or a consumer width). Meanwhile the\n * rail keeps its labels on one line, so none wraps letter by letter in a rail that is still\n * narrow and no icon jumps; the rail's clipping hides what doesn't fit yet.\n */\nfunction useWidening(ref: RefObject<HTMLElement | null>, expanding: boolean) {\n const [widening, setWidening] = useState(expanding);\n const [wasExpanding, setWasExpanding] = useState(expanding);\n if (expanding !== wasExpanding) {\n setWasExpanding(expanding);\n setWidening(expanding);\n }\n useLayoutEffect(() => {\n const el = ref.current;\n if (!widening || !el) return;\n // Reading the running transitions resolves this render's styles, starting the spring.\n const running =\n typeof el.getAnimations === 'function' &&\n el\n .getAnimations()\n .some(\n (animation) =>\n 'transitionProperty' in animation && animation.transitionProperty === 'width',\n );\n if (!running) {\n setWidening(false);\n return;\n }\n // A later width (the measured one replacing the minimum) retargets the same spring.\n const done = (event: TransitionEvent) => {\n if (event.target === el && event.propertyName === 'width') setWidening(false);\n };\n el.addEventListener('transitionend', done);\n return () => el.removeEventListener('transitionend', done);\n }, [ref, widening]);\n return widening;\n}\n\n/**\n * M3 Expressive navigation rail: destinations along the start edge. Collapsed (96px) items\n * show the icon above the label; expanded (220–360px) they show it beside the label and\n * the rail's width springs open. With `modal`, the expanded rail opens over the page as a\n * sheet with a scrim, focusing its first interactive element. Put a menu button in\n * `header` to toggle `expanded`. Tab or the Up / Down arrows move between items, and\n * labels wrap rather than truncate.\n *\n * Icons stay on one line through the middle of the collapsed rail in both states. For a\n * different collapsed width, set `--m3-rail-collapsed-width` (e.g. the class\n * `[--m3-rail-collapsed-width:80px]`) rather than a `w-*` class; M3's widths are 96px\n * (Expressive) and 80px (baseline).\n *\n * @example\n * <NavigationRail aria-label=\"Main\" header={({ toggle }) => <IconButton icon={<MenuIcon />} aria-label=\"Menu\" onPress={toggle} />}>\n * <NavigationRailItem href=\"/inbox\" icon={<InboxIcon />} selected>Inbox</NavigationRailItem>\n * </NavigationRail>\n */\nexport function NavigationRail({\n expanded,\n defaultExpanded = false,\n onExpandedChange,\n modal = false,\n hideOnCollapse = false,\n header,\n itemAlignment = 'top',\n divider = false,\n indicatorMotion = 'expand',\n children,\n classNames,\n className,\n style,\n ...rest\n}: NavigationRailProps) {\n const [isExpanded, setExpanded] = useControlledState(expanded, defaultExpanded, onExpandedChange);\n const toggle = () => setExpanded(!isExpanded);\n const inlineExpanded = isExpanded && !modal;\n const rootRef = useRef<HTMLElement>(null);\n const lastIndicatorRef = useRef<HTMLElement>(null);\n const slide = indicatorMotion === 'slide';\n const width = useExpandedWidth(rootRef, inlineExpanded, children);\n const widening = useWidening(rootRef, inlineExpanded);\n // Labels fade in at their new place once the rail has changed mode (not on first render).\n const [mode, setMode] = useState(inlineExpanded);\n const [modeChanged, setModeChanged] = useState(false);\n const captureItems = useRailItemMorph(rootRef, mode);\n if (mode !== inlineExpanded) {\n // The DOM still shows the old layout: note where each item's icon is.\n captureItems();\n setMode(inlineExpanded);\n setModeChanged(true);\n }\n\n const styles = navigationRailStyles({\n expanded: inlineExpanded,\n itemAlignment,\n divider,\n slidingIndicator: slide && inlineExpanded,\n hiddenOnCompact: modal && hideOnCollapse === 'compact',\n });\n const headerContent =\n typeof header === 'function' ? header({ expanded: isExpanded, toggle }) : header;\n const showInline = !(modal && hideOnCollapse === true);\n\n return (\n <>\n {showInline && (\n <nav\n {...rest}\n ref={rootRef}\n style={\n {\n '--m3-rail-width': inlineExpanded ? `${width}px` : `var(${COLLAPSED_WIDTH_VAR})`,\n ...style,\n } as CSSProperties\n }\n data-tokens={mergeDataTokens('md-comp-nav-rail', rest)}\n data-expanded={inlineExpanded || undefined}\n data-widening={widening || undefined}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n <div className={styles.body({ class: classNames?.body })}>\n {headerContent != null && (\n <div data-rail-header=\"\" className={styles.header({ class: classNames?.header })}>\n <DensityZeroButtons>{headerContent}</DensityZeroButtons>\n </div>\n )}\n <div\n data-item-alignment={itemAlignment}\n onKeyDown={onItemArrowKeys('vertical')}\n className={styles.items({ class: classNames?.items })}\n >\n <RailContext\n value={{\n layout: inlineExpanded ? 'rail-start' : 'rail-top',\n enterLabel: modeChanged,\n ...(slide && inlineExpanded && { lastIndicatorRef }),\n }}\n >\n {children}\n </RailContext>\n </div>\n </div>\n </nav>\n )}\n {modal && (\n <ModalRail\n open={isExpanded}\n onClose={() => setExpanded(false)}\n header={headerContent}\n naming={rest as Naming}\n slide={hideOnCollapse !== false}\n itemAlignment={itemAlignment}\n slidingIndicator={slide}\n railRef={rootRef}\n classNames={classNames}\n >\n {children}\n </ModalRail>\n )}\n </>\n );\n}\n\n/** The sheet's exit transitions; whichever runs marks the end of its exit. */\nconst SHEET_EXIT_PROPERTIES = ['width', 'translate', 'opacity'] as const;\n\nfunction ModalRail({\n open,\n onClose,\n header,\n naming,\n slide,\n itemAlignment,\n slidingIndicator,\n railRef,\n classNames,\n children,\n}: {\n open: boolean;\n onClose: () => void;\n header: ReactNode;\n naming: Naming;\n slide: boolean;\n itemAlignment: 'top' | 'center';\n /** `indicatorMotion=\"slide\"`: the pill slides between items. */\n slidingIndicator: boolean;\n railRef: RefObject<HTMLElement | null>;\n classNames?: NavigationRailClassNames;\n children: ReactNode;\n}) {\n const state = useOverlayTriggerState({\n isOpen: open,\n onOpenChange: (isOpen) => {\n if (!isOpen) onClose();\n },\n });\n // Presence ends with the sheet's own motion, not the scrim's shorter fade, which removed\n // the sheet mid-spring inside the collapsed rail (2026-10-15). Width when it shrinks back,\n // translate when it slides out, opacity under reduced motion.\n const { isPresent, isExiting, exitProps } = usePresence(open, SHEET_EXIT_PROPERTIES);\n if (!isPresent) return null;\n return (\n <Overlay exiting={isExiting}>\n <ModalSheet\n state={state}\n isExiting={isExiting}\n exitProps={exitProps}\n header={header}\n naming={naming}\n slide={slide}\n itemAlignment={itemAlignment}\n slidingIndicator={slidingIndicator}\n railRef={railRef}\n classNames={classNames}\n >\n {children}\n </ModalSheet>\n </Overlay>\n );\n}\n\nfunction ModalSheet({\n state,\n isExiting,\n exitProps,\n header,\n naming,\n slide,\n itemAlignment,\n slidingIndicator,\n railRef,\n classNames,\n children,\n}: {\n state: OverlayTriggerState;\n isExiting: boolean;\n exitProps: ReturnType<typeof usePresence>['exitProps'];\n header: ReactNode;\n naming: Naming;\n slide: boolean;\n itemAlignment: 'top' | 'center';\n /** `indicatorMotion=\"slide\"`: the pill slides between items. */\n slidingIndicator: boolean;\n railRef: RefObject<HTMLElement | null>;\n classNames?: NavigationRailClassNames;\n children: ReactNode;\n}) {\n const ref = useRef<HTMLDivElement>(null);\n const lastIndicatorRef = useRef<HTMLElement>(null);\n const { modalProps, underlayProps } = useModalOverlay({ isDismissable: !isExiting }, state, ref);\n const { dialogProps } = useDialog(naming, ref);\n // The sheet is portalled out of the rail, so it can't inherit a collapsed width set on the\n // rail itself. Copy it before the width is measured and the sheet first paints.\n useLayoutEffect(() => {\n const rail = railRef.current;\n if (!rail || !ref.current) return;\n ref.current.style.setProperty(\n COLLAPSED_WIDTH_VAR,\n getComputedStyle(rail).getPropertyValue(COLLAPSED_WIDTH_VAR),\n );\n }, [railRef]);\n // M3: initial focus lands on the first interactive item, whether it's the menu button,\n // the FAB or the first destination (useDialog would otherwise focus the sheet itself).\n useLayoutEffect(() => {\n const first = ref.current?.querySelector<HTMLElement>(FOCUSABLE);\n first?.focus();\n }, []);\n const width = useExpandedWidth(ref, true, children);\n const widening = useWidening(ref, true);\n const styles = navigationRailStyles({\n expanded: true,\n sheetEntry: slide ? 'slide' : 'grow',\n itemAlignment,\n slidingIndicator,\n });\n const exiting = isExiting || undefined;\n return (\n <>\n <div\n {...underlayProps}\n aria-hidden=\"true\"\n data-exiting={exiting}\n className={styles.scrim({ class: classNames?.scrim })}\n />\n <div\n {...mergeProps(modalProps, dialogProps, exitProps)}\n ref={ref}\n data-tokens=\"md-comp-nav-rail\"\n data-modal=\"\"\n data-exiting={exiting}\n data-widening={widening || undefined}\n style={{ '--m3-rail-width': `${width}px` } as CSSProperties}\n className={styles.sheet({ class: classNames?.sheet })}\n >\n {header != null && (\n <div data-rail-header=\"\" className={styles.header({ class: classNames?.header })}>\n <DensityZeroButtons>{header}</DensityZeroButtons>\n </div>\n )}\n <nav\n aria-label={'aria-label' in naming ? naming['aria-label'] : undefined}\n aria-labelledby={'aria-labelledby' in naming ? naming['aria-labelledby'] : undefined}\n data-item-alignment={itemAlignment}\n onKeyDown={onItemArrowKeys('vertical')}\n className={styles.items({ class: classNames?.items })}\n >\n <RailContext\n value={{\n layout: 'rail-start',\n enterLabel: false,\n ...(slidingIndicator && { lastIndicatorRef }),\n onNavigate: state.close,\n }}\n >\n {children}\n </RailContext>\n </nav>\n </div>\n </>\n );\n}\n\nexport type NavigationRailItemProps = NavItemProps;\n\n/** What {@link useNavigationRail} reports about the rail around a component. */\nexport interface NavigationRailState {\n /** Whether the rail shows its labels beside the icons (expanded, or the modal sheet). */\n expanded: boolean;\n /** Whether this is the rail's modal sheet. */\n modal: boolean;\n /** Collapses the modal sheet; does nothing in the rail itself. */\n close: () => void;\n}\n\n/**\n * The rail around a component, for a custom destination that sits in a {@link NavigationRail}\n * beside its items and changes with it. Throws outside a rail.\n */\nexport function useNavigationRail(): NavigationRailState {\n const rail = useContext(RailContext);\n if (!rail) throw new Error('useNavigationRail must be used inside a NavigationRail');\n return {\n expanded: rail.layout === 'rail-start',\n modal: rail.onNavigate !== undefined,\n close: () => rail.onNavigate?.(),\n };\n}\n\n/** A destination in a {@link NavigationRail}: a link (`href`) or a button. */\nexport function NavigationRailItem({ onPress, ...props }: NavigationRailItemProps) {\n const rail = useContext(RailContext);\n if (!rail) throw new Error('NavigationRailItem must be rendered inside a NavigationRail');\n return (\n <NavItem\n {...(props as NavItemProps)}\n layout={rail.layout}\n enterLabel={rail.enterLabel}\n {...(rail.lastIndicatorRef && { lastIndicatorRef: rail.lastIndicatorRef })}\n onPress={(event) => {\n onPress?.(event);\n rail.onNavigate?.();\n }}\n />\n );\n}\n"],"mappings":"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"}
|