@windmorph/react 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/components/date-picker/date-picker-styles.d.ts +9 -9
  2. package/dist/components/dialog/Dialog.d.ts +4 -2
  3. package/dist/components/dialog/Dialog.js +2 -2
  4. package/dist/components/dialog/Dialog.js.map +1 -1
  5. package/dist/components/dialog/dialog-styles.js +5 -5
  6. package/dist/components/dialog/dialog-styles.js.map +1 -1
  7. package/dist/components/icon-button/IconButton.d.ts +3 -0
  8. package/dist/components/icon-button/IconButton.js +3 -0
  9. package/dist/components/icon-button/IconButton.js.map +1 -1
  10. package/dist/components/menu/use-reveal-entering.d.ts +19 -0
  11. package/dist/components/navigation/NavItem.js +1 -1
  12. package/dist/components/navigation/NavItem.js.map +1 -1
  13. package/dist/components/navigation/NavigationRail.d.ts +21 -2
  14. package/dist/components/navigation/NavigationRail.js +18 -4
  15. package/dist/components/navigation/NavigationRail.js.map +1 -1
  16. package/dist/components/navigation/nav-item-styles.js +4 -4
  17. package/dist/components/navigation/nav-item-styles.js.map +1 -1
  18. package/dist/components/navigation/navigation-styles.d.ts +21 -0
  19. package/dist/components/navigation/navigation-styles.js +6 -1
  20. package/dist/components/navigation/navigation-styles.js.map +1 -1
  21. package/dist/components/select/Select.js +6 -1
  22. package/dist/components/select/Select.js.map +1 -1
  23. package/dist/components/select/search-list.js +1 -1
  24. package/dist/components/select/search-list.js.map +1 -1
  25. package/dist/components/sheet/SideSheet.d.ts +2 -0
  26. package/dist/components/sheet/SideSheet.js +1 -1
  27. package/dist/components/sheet/SideSheet.js.map +1 -1
  28. package/dist/components/text-field/TextField.js +9 -1
  29. package/dist/components/text-field/TextField.js.map +1 -1
  30. package/dist/components/time-picker/TimeField.d.ts +118 -0
  31. package/dist/components/time-picker/TimeField.js +183 -0
  32. package/dist/components/time-picker/TimeField.js.map +1 -0
  33. package/dist/components/time-picker/TimeFieldDialog.js +70 -0
  34. package/dist/components/time-picker/TimeFieldDialog.js.map +1 -0
  35. package/dist/components/time-picker/TimePicker.js +1 -4
  36. package/dist/components/time-picker/TimePicker.js.map +1 -1
  37. package/dist/components/time-picker/hour-cycle.js +10 -0
  38. package/dist/components/time-picker/hour-cycle.js.map +1 -0
  39. package/dist/components/time-picker/time-format.js +84 -0
  40. package/dist/components/time-picker/time-format.js.map +1 -0
  41. package/dist/composites/button-group/ButtonGroup.d.ts +7 -0
  42. package/dist/composites/button-group/ButtonGroup.js +3 -2
  43. package/dist/composites/button-group/ButtonGroup.js.map +1 -1
  44. package/dist/composites/button-group/button-group-styles.d.ts +15 -3
  45. package/dist/composites/button-group/button-group-styles.js +16 -1
  46. package/dist/composites/button-group/button-group-styles.js.map +1 -1
  47. package/dist/index.d.ts +4 -2
  48. package/dist/index.js +4 -2
  49. package/dist/primitives/ButtonBase.js +1 -0
  50. package/dist/primitives/ButtonBase.js.map +1 -1
  51. package/dist/primitives.d.ts +4 -1
  52. package/dist/primitives.js +4 -1
  53. package/dist/styles/utilities.css +8 -0
  54. package/dist/styles.compiled.css +1 -1
  55. package/dist/theme/ThemeProvider.js +17 -0
  56. package/dist/theme/ThemeProvider.js.map +1 -1
  57. package/dist/utils/closed-by.d.ts +5 -0
  58. package/dist/utils/placement.d.ts +11 -0
  59. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"IconButton.js","names":[],"sources":["../../../src/components/icon-button/IconButton.tsx"],"sourcesContent":["'use client';\n\nimport { useContext, type ReactElement, type Ref } from 'react';\nimport {\n ButtonBase,\n type ButtonBaseActionProps,\n type ButtonBaseLinkProps,\n type ButtonBaseProps,\n type ButtonBaseToggleProps,\n} from '../../primitives/ButtonBase';\nimport { IconFaces } from '../../primitives/IconFaces';\nimport { iconFacesStyles, type IconTransition } from '../../primitives/icon-faces-styles';\nimport { TouchTarget } from '../../primitives/TouchTarget';\nimport { densityAttribute, type Density } from '../../tokens/density';\nimport { cn } from '../../utils/cn';\nimport { useWarnOnce } from '../../utils/warn-once';\nimport type { ButtonShape, ButtonSize } from '../button/button-styles';\nimport { ButtonContext } from '../button/ButtonContext';\nimport { ToolbarContext } from '../../primitives/ToolbarContext';\nimport { LazyTooltip } from '../tooltip/lazy-tooltip';\nimport {\n TooltipTrigger,\n useInTooltipTrigger,\n WithoutDescription,\n} from '../tooltip/tooltip-trigger';\nimport {\n iconButtonStyles,\n type IconButtonVariant,\n type IconButtonWidth,\n} from './icon-button-styles';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\nimport { pendingClasses, useButtonPending, type ButtonLabels } from '../button/button-pending';\n\nexport interface IconButtonClassNames {\n /** The button element itself; merged with `className`. */\n root?: string;\n /** Inner wrapper around the icon. */\n content?: string;\n /** The icon. */\n icon?: string;\n /** The built-in plain tooltip that shows the label (`tooltip`). */\n tooltip?: string;\n /** The slot holding the loading indicator while `pending`, centred over the hidden icon. */\n pendingIndicator?: string;\n}\n\n/** Icon buttons have no visible text, so an accessible name is required. */\ntype AccessibleName =\n | {\n /** The button's accessible name, also shown in its tooltip. Required without `aria-labelledby`. */\n 'aria-label': string;\n /** The id of an element that names the button, instead of `aria-label`. */\n 'aria-labelledby'?: string;\n }\n | {\n /** The button's accessible name, also shown in its tooltip. Required without `aria-labelledby`. */\n 'aria-label'?: string;\n /** The id of an element that names the button, instead of `aria-label`. */\n 'aria-labelledby': string;\n };\n\ninterface IconButtonOwnProps {\n /**\n * Colour style. Filled is the M3 default; toolbars and app bars default their icon\n * buttons to `standard` through `ButtonContext`.\n * @default \"filled\"\n */\n variant?: IconButtonVariant;\n /** Container and icon size, from extra small to extra large. @default \"sm\" */\n size?: ButtonSize;\n /** Container width: narrow, default or wide, at the same height. @default \"default\" */\n width?: IconButtonWidth;\n /** Fully round corners, or square ones (still rounded, but less). @default \"round\" */\n shape?: ButtonShape;\n /**\n * Web density, `0` to `-3`: each step takes 4px off the container's height and width (an\n * S button: 40px at 0, 28px at -3), never below the icon plus 4px; the touch target stays\n * 48 × 48px. Left out, the button takes the density of the nearest `ThemeProvider` or\n * `ThemeScope`; icon buttons in a field's icon slots, and in app bars, toolbars, search bars,\n * a navigation rail's header and dialog and sheet headers, keep their size. M3 asks for density\n * to be the reader's choice rather than a default, so prefer setting it there.\n * @default 0\n */\n density?: Density;\n /** The icon, e.g. a Material Symbols SVG. Hidden from assistive tech; `aria-label` names the button. */\n icon: ReactElement;\n /**\n * Shows the `aria-label` in a plain tooltip on hover and keyboard focus, as M3 asks of\n * icon buttons on the web. The tooltip only repeats the name, so it isn't linked as a\n * description (no double announcement). It steps aside when the button is already inside a\n * `TooltipTrigger` or `RichTooltipTrigger`, and when it's named by `aria-labelledby`.\n * @default true\n */\n tooltip?: boolean;\n /** Classes for each part. `classNames.root` is merged with `className`. */\n classNames?: IconButtonClassNames;\n /** Words the button announces. */\n labels?: Partial<ButtonLabels>;\n}\n\ninterface IconButtonPendingProps {\n /**\n * Shows a loading indicator in place of the icon while an action runs, and blocks presses.\n * The button keeps its size, its colours and keyboard focus; its accessible name gains\n * `labels.pending` (the tooltip keeps showing only the `aria-label`), and a focused button\n * announces it. The indicator appears after 200ms, so a quick action doesn't flash. Use it\n * for actions that take a moment, not for every button in a list.\n * @default false\n */\n pending?: boolean;\n /**\n * Replaces the loading indicator shown while `pending`, e.g. a `CircularProgressIndicator`\n * for a wait that may pass five seconds. It's centred over the icon and takes the content\n * colour; a progress indicator also loses its track, as M3 asks inside a button.\n */\n pendingIndicator?: ReactElement;\n}\n\ntype OmitOwn<T> = Omit<\n T,\n | keyof IconButtonOwnProps\n | keyof IconButtonPendingProps\n | 'children'\n | 'aria-label'\n | 'aria-labelledby'\n>;\n\nexport type IconButtonActionProps = IconButtonOwnProps &\n IconButtonPendingProps &\n OmitOwn<ButtonBaseActionProps> &\n AccessibleName;\nexport type IconButtonLinkProps = IconButtonOwnProps &\n IconButtonPendingProps &\n OmitOwn<ButtonBaseLinkProps> &\n AccessibleName;\nexport type IconButtonToggleProps = IconButtonOwnProps &\n OmitOwn<ButtonBaseToggleProps> &\n AccessibleName & {\n /** Toggle buttons have no pending state. */\n pending?: never;\n /** Toggle buttons have no pending state. */\n pendingIndicator?: never;\n /** Icon shown while selected, e.g. the filled form of `icon`. */\n selectedIcon?: ReactElement;\n /**\n * How `icon` hands over to `selectedIcon`: `swap` changes it at once, `rotate` turns and\n * crossfades the two (counter-clockwise as it selects, in LTR). `rotate` is an extension\n * beyond the M3 spec, matching Google's Expressive showcase, like the FAB menu's.\n * Reduced motion keeps only the crossfade. Needs `selectedIcon`.\n * @default \"swap\"\n */\n iconTransition?: IconTransition;\n };\n\nexport type IconButtonProps = IconButtonActionProps | IconButtonLinkProps | IconButtonToggleProps;\n\n/**\n * M3 Expressive icon button: filled (default), tonal, outlined and standard variants;\n * XS–XL sizes; narrow, default or wide widths; round or square shape; an optional toggle\n * form; a press morph; and its label as a tooltip on hover and keyboard focus.\n *\n * Shares `size`, `shape` and `disabled` with `ButtonContext`, so it fits in button groups,\n * and takes its default variant from it (`iconButtonVariant`).\n *\n * @example\n * <IconButton icon={<SearchIcon />} aria-label=\"Search\" onPress={openSearch} />\n * <IconButton variant=\"standard\" icon={<MoreIcon />} aria-label=\"More options\" />\n * <IconButton toggle variant=\"filled\" icon={<StarIcon />} selectedIcon={<StarFilledIcon />}\n * aria-label=\"Favourite\" selected={isFavourite} onSelectedChange={setFavourite} />\n * <IconButton icon={<RefreshIcon />} aria-label=\"Refresh\" pending={refreshing} onPress={refresh} />\n */\nexport function IconButton(props: IconButtonProps) {\n const {\n variant: variantProp,\n size: sizeProp,\n width = 'default',\n shape: shapeProp,\n density: densityProp,\n disabled: disabledProp,\n icon,\n tooltip = true,\n pendingIndicator,\n labels,\n classNames,\n className,\n // Only toggle props carry `selectedIcon`; the others read it as undefined.\n selectedIcon,\n iconTransition = 'swap',\n ...base\n } = props as Omit<IconButtonToggleProps, 'pendingIndicator'> &\n IconButtonProps & { pendingIndicator?: ReactElement };\n const rotates = iconTransition === 'rotate' && selectedIcon !== undefined;\n\n const context = useContext(ButtonContext);\n const toolbar = useContext(ToolbarContext);\n const inTooltipTrigger = useInTooltipTrigger();\n const size = sizeProp ?? context.size ?? 'sm';\n const shape = shapeProp ?? context.shape ?? 'round';\n const squareInFloatingToolbar = toolbar?.kind === 'floating' && shape === 'square';\n // Once per button, not on every render.\n useWarnOnce(\n squareInFloatingToolbar,\n '[@windmorph/react] Avoid square icon buttons in a floating toolbar (M3): their shape conflicts with its fully rounded container.',\n );\n useWarnOnce(\n toolbar?.orientation === 'vertical' && width === 'wide',\n '[@windmorph/react] Avoid wide icon buttons in a vertical toolbar (M3): use the default or narrow width.',\n );\n const variant = variantProp ?? context.iconButtonVariant ?? 'filled';\n const density = densityProp ?? context.density;\n const styles = iconButtonStyles({\n variant,\n size,\n width,\n shape,\n toggle: base.toggle === true,\n connected: context.connected ?? 'none',\n toolbar: toolbar?.color ?? 'none',\n });\n\n // Destructuring a union merges its members (see Button); `base` holds one member's props.\n // The component tokens' hooks (icon-button-tokens.ts), also public styling hooks for apps.\n const tokenAttributes = {\n 'data-tokens': mergeDataTokens('md-comp-icon-button', base),\n 'data-variant': variant,\n 'data-size': size,\n // Read by a button group, which spaces narrow icon buttons wider apart.\n 'data-width': width,\n };\n const pending = useButtonPending<HTMLElement>({\n pending: base.pending,\n pendingIndicator,\n labels,\n size,\n ariaLabel: base['aria-label'],\n ariaLabelledby: base['aria-labelledby'],\n ref: base.ref as Ref<HTMLElement> | undefined,\n indicatorClassName: cn(pendingClasses.indicator, classNames?.pendingIndicator),\n });\n const baseProps = {\n ...base,\n ...tokenAttributes,\n ...pending.rootProps,\n ...densityAttribute(density),\n disabled: disabledProp ?? context.disabled ?? false,\n className: styles.root({ class: cn(classNames?.root, className) }),\n } as ButtonBaseProps;\n\n const button = (\n <ButtonBase {...baseProps}>\n {({ isSelected }) => (\n <span\n {...pending.contentProps}\n className={styles.content({\n class: cn(pending.takesPending && pendingClasses.content, classNames?.content),\n })}\n >\n {(size === 'xs' || size === 'sm') && <TouchTarget />}\n <span\n aria-hidden=\"true\"\n className={styles.icon({\n class: cn(\n rotates && iconFacesStyles().cell(),\n pending.takesPending && pendingClasses.replaced,\n classNames?.icon,\n ),\n })}\n >\n {rotates ? (\n <IconFaces\n icon={icon}\n alternateIcon={selectedIcon}\n alternate={isSelected}\n transition=\"rotate\"\n />\n ) : isSelected && selectedIcon ? (\n selectedIcon\n ) : (\n icon\n )}\n </span>\n {pending.indicator}\n {pending.hiddenName}\n </span>\n )}\n </ButtonBase>\n );\n\n const label = base['aria-label'];\n if (!tooltip || inTooltipTrigger || !label || base['aria-labelledby']) return button;\n return (\n <TooltipTrigger>\n <WithoutDescription>{button}</WithoutDescription>\n <LazyTooltip classNames={{ panel: classNames?.tooltip }}>{label}</LazyTooltip>\n </TooltipTrigger>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2KA,SAAgB,WAAW,OAAwB;CACjD,MAAM,EACJ,SAAS,aACT,MAAM,UACN,QAAQ,WACR,OAAO,WACP,SAAS,aACT,UAAU,cACV,MACA,UAAU,MACV,kBACA,QACA,YACA,WAEA,cACA,iBAAiB,QACjB,GAAG,SACD;CAEJ,MAAM,UAAU,mBAAmB,YAAY,iBAAiB,KAAA;CAEhE,MAAM,UAAU,WAAW,aAAa;CACxC,MAAM,UAAU,WAAW,cAAc;CACzC,MAAM,mBAAmB,oBAAoB;CAC7C,MAAM,OAAO,YAAY,QAAQ,QAAQ;CACzC,MAAM,QAAQ,aAAa,QAAQ,SAAS;CAC5C,MAAM,0BAA0B,SAAS,SAAS,cAAc,UAAU;CAE1E,YACE,yBACA,kIACF;CACA,YACE,SAAS,gBAAgB,cAAc,UAAU,QACjD,yGACF;CACA,MAAM,UAAU,eAAe,QAAQ,qBAAqB;CAC5D,MAAM,UAAU,eAAe,QAAQ;CACvC,MAAM,SAAS,iBAAiB;EAC9B;EACA;EACA;EACA;EACA,QAAQ,KAAK,WAAW;EACxB,WAAW,QAAQ,aAAa;EAChC,SAAS,SAAS,SAAS;CAC7B,CAAC;CAID,MAAM,kBAAkB;EACtB,eAAe,gBAAgB,uBAAuB,IAAI;EAC1D,gBAAgB;EAChB,aAAa;EAEb,cAAc;CAChB;CACA,MAAM,UAAU,iBAA8B;EAC5C,SAAS,KAAK;EACd;EACA;EACA;EACA,WAAW,KAAK;EAChB,gBAAgB,KAAK;EACrB,KAAK,KAAK;EACV,oBAAoB,GAAG,eAAe,WAAW,YAAY,gBAAgB;CAC/E,CAAC;CACD,MAAM,YAAY;EAChB,GAAG;EACH,GAAG;EACH,GAAG,QAAQ;EACX,GAAG,iBAAiB,OAAO;EAC3B,UAAU,gBAAgB,QAAQ,YAAY;EAC9C,WAAW,OAAO,KAAK,EAAE,OAAO,GAAG,YAAY,MAAM,SAAS,EAAE,CAAC;CACnE;CAEA,MAAM,SACJ,oBAAC,YAAD;EAAY,GAAI;EACZ,WAAA,EAAE,iBACF,qBAAC,QAAD;GACE,GAAI,QAAQ;GACZ,WAAW,OAAO,QAAQ,EACxB,OAAO,GAAG,QAAQ,gBAAgB,eAAe,SAAS,YAAY,OAAO,EAC/E,CAAC;GAJH,UAAA;KAMI,SAAS,QAAQ,SAAS,SAAS,oBAAC,aAAD,CAAc,CAAA;IACnD,oBAAC,QAAD;KACE,eAAY;KACZ,WAAW,OAAO,KAAK,EACrB,OAAO,GACL,WAAW,gBAAgB,CAAC,CAAC,KAAK,GAClC,QAAQ,gBAAgB,eAAe,UACvC,YAAY,IACd,EACF,CAAC;KAEA,UAAA,UACC,oBAAC,WAAD;MACQ;MACN,eAAe;MACf,WAAW;MACX,YAAW;KACZ,CAAA,IACC,cAAc,eAChB,eAEA;IAEE,CAAA;IACL,QAAQ;IACR,QAAQ;GACL;;CAEE,CAAA;CAGd,MAAM,QAAQ,KAAK;CACnB,IAAI,CAAC,WAAW,oBAAoB,CAAC,SAAS,KAAK,oBAAoB,OAAO;CAC9E,OACE,qBAAC,gBAAD,EAAA,UAAA,CACE,oBAAC,oBAAD,EAAA,UAAqB,OAA2B,CAAA,GAChD,oBAAC,aAAD;EAAa,YAAY,EAAE,OAAO,YAAY,QAAQ;EAAI,UAAA;CAAmB,CAAA,CAC/D,EAAA,CAAA;AAEpB"}
1
+ {"version":3,"file":"IconButton.js","names":[],"sources":["../../../src/components/icon-button/IconButton.tsx"],"sourcesContent":["'use client';\n\nimport { useContext, type ReactElement, type Ref } from 'react';\nimport {\n ButtonBase,\n type ButtonBaseActionProps,\n type ButtonBaseLinkProps,\n type ButtonBaseProps,\n type ButtonBaseToggleProps,\n} from '../../primitives/ButtonBase';\nimport { IconFaces } from '../../primitives/IconFaces';\nimport { iconFacesStyles, type IconTransition } from '../../primitives/icon-faces-styles';\nimport { TouchTarget } from '../../primitives/TouchTarget';\nimport { densityAttribute, type Density } from '../../tokens/density';\nimport { cn } from '../../utils/cn';\nimport { useWarnOnce } from '../../utils/warn-once';\nimport type { ButtonShape, ButtonSize } from '../button/button-styles';\nimport { ButtonContext } from '../button/ButtonContext';\nimport { ToolbarContext } from '../../primitives/ToolbarContext';\nimport { LazyTooltip } from '../tooltip/lazy-tooltip';\nimport {\n TooltipTrigger,\n useInTooltipTrigger,\n WithoutDescription,\n} from '../tooltip/tooltip-trigger';\nimport {\n iconButtonStyles,\n type IconButtonVariant,\n type IconButtonWidth,\n} from './icon-button-styles';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\nimport { pendingClasses, useButtonPending, type ButtonLabels } from '../button/button-pending';\n\nexport interface IconButtonClassNames {\n /** The button element itself; merged with `className`. */\n root?: string;\n /** Inner wrapper around the icon. */\n content?: string;\n /** The icon. */\n icon?: string;\n /** The built-in plain tooltip that shows the label (`tooltip`). */\n tooltip?: string;\n /** The slot holding the loading indicator while `pending`, centred over the hidden icon. */\n pendingIndicator?: string;\n}\n\n/** Icon buttons have no visible text, so an accessible name is required. */\ntype AccessibleName =\n | {\n /** The button's accessible name, also shown in its tooltip. Required without `aria-labelledby`. */\n 'aria-label': string;\n /** The id of an element that names the button, instead of `aria-label`. */\n 'aria-labelledby'?: string;\n }\n | {\n /** The button's accessible name, also shown in its tooltip. Required without `aria-labelledby`. */\n 'aria-label'?: string;\n /** The id of an element that names the button, instead of `aria-label`. */\n 'aria-labelledby': string;\n };\n\ninterface IconButtonOwnProps {\n /**\n * Colour style. Filled is the M3 default; toolbars and app bars default their icon\n * buttons to `standard` through `ButtonContext`.\n * @default \"filled\"\n */\n variant?: IconButtonVariant;\n /** Container and icon size, from extra small to extra large. @default \"sm\" */\n size?: ButtonSize;\n /** Container width: narrow, default or wide, at the same height. @default \"default\" */\n width?: IconButtonWidth;\n /** Fully round corners, or square ones (still rounded, but less). @default \"round\" */\n shape?: ButtonShape;\n /**\n * Web density, `0` to `-3`: each step takes 4px off the container's height and width (an\n * S button: 40px at 0, 28px at -3), never below the icon plus 4px; the touch target stays\n * 48 × 48px. Left out, the button takes the density of the nearest `ThemeProvider` or\n * `ThemeScope`; icon buttons in a field's icon slots, and in app bars, toolbars, search bars,\n * a navigation rail's header and dialog and sheet headers, keep their size. M3 asks for density\n * to be the reader's choice rather than a default, so prefer setting it there.\n * @default 0\n */\n density?: Density;\n /** The icon, e.g. a Material Symbols SVG. Hidden from assistive tech; `aria-label` names the button. */\n icon: ReactElement;\n /**\n * Shows the `aria-label` in a plain tooltip on hover and keyboard focus, as M3 asks of\n * icon buttons on the web. The tooltip only repeats the name, so it isn't linked as a\n * description (no double announcement). It steps aside when the button is already inside a\n * `TooltipTrigger` or `RichTooltipTrigger`, and when it's named by `aria-labelledby`.\n * @default true\n */\n tooltip?: boolean;\n /** Classes for each part. `classNames.root` is merged with `className`. */\n classNames?: IconButtonClassNames;\n /** Words the button announces. */\n labels?: Partial<ButtonLabels>;\n}\n\ninterface IconButtonPendingProps {\n /**\n * Shows a loading indicator in place of the icon while an action runs, and blocks presses.\n * The button keeps its size, its colours and keyboard focus; its accessible name gains\n * `labels.pending` (the tooltip keeps showing only the `aria-label`), and a focused button\n * announces it. The indicator appears after 200ms, so a quick action doesn't flash. Use it\n * for actions that take a moment, not for every button in a list.\n * @default false\n */\n pending?: boolean;\n /**\n * Replaces the loading indicator shown while `pending`, e.g. a `CircularProgressIndicator`\n * for a wait that may pass five seconds. It's centred over the icon and takes the content\n * colour; a progress indicator also loses its track, as M3 asks inside a button.\n */\n pendingIndicator?: ReactElement;\n}\n\ntype OmitOwn<T> = Omit<\n T,\n | keyof IconButtonOwnProps\n | keyof IconButtonPendingProps\n | 'children'\n | 'aria-label'\n | 'aria-labelledby'\n>;\n\nexport type IconButtonActionProps = IconButtonOwnProps &\n IconButtonPendingProps &\n OmitOwn<ButtonBaseActionProps> &\n AccessibleName;\nexport type IconButtonLinkProps = IconButtonOwnProps &\n IconButtonPendingProps &\n OmitOwn<ButtonBaseLinkProps> &\n AccessibleName;\nexport type IconButtonToggleProps = IconButtonOwnProps &\n OmitOwn<ButtonBaseToggleProps> &\n AccessibleName & {\n /** Toggle buttons have no pending state. */\n pending?: never;\n /** Toggle buttons have no pending state. */\n pendingIndicator?: never;\n /** Icon shown while selected, e.g. the filled form of `icon`. */\n selectedIcon?: ReactElement;\n /**\n * How `icon` hands over to `selectedIcon`: `swap` changes it at once, `rotate` turns and\n * crossfades the two (counter-clockwise as it selects, in LTR). `rotate` is an extension\n * beyond the M3 spec, matching Google's Expressive showcase, like the FAB menu's.\n * Reduced motion keeps only the crossfade. Needs `selectedIcon`.\n * @default \"swap\"\n */\n iconTransition?: IconTransition;\n };\n\nexport type IconButtonProps = IconButtonActionProps | IconButtonLinkProps | IconButtonToggleProps;\n\n/**\n * M3 Expressive icon button: filled (default), tonal, outlined and standard variants;\n * XS–XL sizes; narrow, default or wide widths; round or square shape; an optional toggle\n * form; a press morph; and its label as a tooltip on hover and keyboard focus.\n *\n * Shares `size`, `shape` and `disabled` with `ButtonContext`, so it fits in button groups,\n * and takes its default variant from it (`iconButtonVariant`).\n *\n * To animate its size or position, put the transition on a wrapper: while pressed, the button's\n * own motion replaces the timing of a transition added through `className`.\n *\n * @example\n * <IconButton icon={<SearchIcon />} aria-label=\"Search\" onPress={openSearch} />\n * <IconButton variant=\"standard\" icon={<MoreIcon />} aria-label=\"More options\" />\n * <IconButton toggle variant=\"filled\" icon={<StarIcon />} selectedIcon={<StarFilledIcon />}\n * aria-label=\"Favourite\" selected={isFavourite} onSelectedChange={setFavourite} />\n * <IconButton icon={<RefreshIcon />} aria-label=\"Refresh\" pending={refreshing} onPress={refresh} />\n */\nexport function IconButton(props: IconButtonProps) {\n const {\n variant: variantProp,\n size: sizeProp,\n width = 'default',\n shape: shapeProp,\n density: densityProp,\n disabled: disabledProp,\n icon,\n tooltip = true,\n pendingIndicator,\n labels,\n classNames,\n className,\n // Only toggle props carry `selectedIcon`; the others read it as undefined.\n selectedIcon,\n iconTransition = 'swap',\n ...base\n } = props as Omit<IconButtonToggleProps, 'pendingIndicator'> &\n IconButtonProps & { pendingIndicator?: ReactElement };\n const rotates = iconTransition === 'rotate' && selectedIcon !== undefined;\n\n const context = useContext(ButtonContext);\n const toolbar = useContext(ToolbarContext);\n const inTooltipTrigger = useInTooltipTrigger();\n const size = sizeProp ?? context.size ?? 'sm';\n const shape = shapeProp ?? context.shape ?? 'round';\n const squareInFloatingToolbar = toolbar?.kind === 'floating' && shape === 'square';\n // Once per button, not on every render.\n useWarnOnce(\n squareInFloatingToolbar,\n '[@windmorph/react] Avoid square icon buttons in a floating toolbar (M3): their shape conflicts with its fully rounded container.',\n );\n useWarnOnce(\n toolbar?.orientation === 'vertical' && width === 'wide',\n '[@windmorph/react] Avoid wide icon buttons in a vertical toolbar (M3): use the default or narrow width.',\n );\n const variant = variantProp ?? context.iconButtonVariant ?? 'filled';\n const density = densityProp ?? context.density;\n const styles = iconButtonStyles({\n variant,\n size,\n width,\n shape,\n toggle: base.toggle === true,\n connected: context.connected ?? 'none',\n toolbar: toolbar?.color ?? 'none',\n });\n\n // Destructuring a union merges its members (see Button); `base` holds one member's props.\n // The component tokens' hooks (icon-button-tokens.ts), also public styling hooks for apps.\n const tokenAttributes = {\n 'data-tokens': mergeDataTokens('md-comp-icon-button', base),\n 'data-variant': variant,\n 'data-size': size,\n // Read by a button group, which spaces narrow icon buttons wider apart.\n 'data-width': width,\n };\n const pending = useButtonPending<HTMLElement>({\n pending: base.pending,\n pendingIndicator,\n labels,\n size,\n ariaLabel: base['aria-label'],\n ariaLabelledby: base['aria-labelledby'],\n ref: base.ref as Ref<HTMLElement> | undefined,\n indicatorClassName: cn(pendingClasses.indicator, classNames?.pendingIndicator),\n });\n const baseProps = {\n ...base,\n ...tokenAttributes,\n ...pending.rootProps,\n ...densityAttribute(density),\n disabled: disabledProp ?? context.disabled ?? false,\n className: styles.root({ class: cn(classNames?.root, className) }),\n } as ButtonBaseProps;\n\n const button = (\n <ButtonBase {...baseProps}>\n {({ isSelected }) => (\n <span\n {...pending.contentProps}\n className={styles.content({\n class: cn(pending.takesPending && pendingClasses.content, classNames?.content),\n })}\n >\n {(size === 'xs' || size === 'sm') && <TouchTarget />}\n <span\n aria-hidden=\"true\"\n className={styles.icon({\n class: cn(\n rotates && iconFacesStyles().cell(),\n pending.takesPending && pendingClasses.replaced,\n classNames?.icon,\n ),\n })}\n >\n {rotates ? (\n <IconFaces\n icon={icon}\n alternateIcon={selectedIcon}\n alternate={isSelected}\n transition=\"rotate\"\n />\n ) : isSelected && selectedIcon ? (\n selectedIcon\n ) : (\n icon\n )}\n </span>\n {pending.indicator}\n {pending.hiddenName}\n </span>\n )}\n </ButtonBase>\n );\n\n const label = base['aria-label'];\n if (!tooltip || inTooltipTrigger || !label || base['aria-labelledby']) return button;\n return (\n <TooltipTrigger>\n <WithoutDescription>{button}</WithoutDescription>\n <LazyTooltip classNames={{ panel: classNames?.tooltip }}>{label}</LazyTooltip>\n </TooltipTrigger>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8KA,SAAgB,WAAW,OAAwB;CACjD,MAAM,EACJ,SAAS,aACT,MAAM,UACN,QAAQ,WACR,OAAO,WACP,SAAS,aACT,UAAU,cACV,MACA,UAAU,MACV,kBACA,QACA,YACA,WAEA,cACA,iBAAiB,QACjB,GAAG,SACD;CAEJ,MAAM,UAAU,mBAAmB,YAAY,iBAAiB,KAAA;CAEhE,MAAM,UAAU,WAAW,aAAa;CACxC,MAAM,UAAU,WAAW,cAAc;CACzC,MAAM,mBAAmB,oBAAoB;CAC7C,MAAM,OAAO,YAAY,QAAQ,QAAQ;CACzC,MAAM,QAAQ,aAAa,QAAQ,SAAS;CAC5C,MAAM,0BAA0B,SAAS,SAAS,cAAc,UAAU;CAE1E,YACE,yBACA,kIACF;CACA,YACE,SAAS,gBAAgB,cAAc,UAAU,QACjD,yGACF;CACA,MAAM,UAAU,eAAe,QAAQ,qBAAqB;CAC5D,MAAM,UAAU,eAAe,QAAQ;CACvC,MAAM,SAAS,iBAAiB;EAC9B;EACA;EACA;EACA;EACA,QAAQ,KAAK,WAAW;EACxB,WAAW,QAAQ,aAAa;EAChC,SAAS,SAAS,SAAS;CAC7B,CAAC;CAID,MAAM,kBAAkB;EACtB,eAAe,gBAAgB,uBAAuB,IAAI;EAC1D,gBAAgB;EAChB,aAAa;EAEb,cAAc;CAChB;CACA,MAAM,UAAU,iBAA8B;EAC5C,SAAS,KAAK;EACd;EACA;EACA;EACA,WAAW,KAAK;EAChB,gBAAgB,KAAK;EACrB,KAAK,KAAK;EACV,oBAAoB,GAAG,eAAe,WAAW,YAAY,gBAAgB;CAC/E,CAAC;CACD,MAAM,YAAY;EAChB,GAAG;EACH,GAAG;EACH,GAAG,QAAQ;EACX,GAAG,iBAAiB,OAAO;EAC3B,UAAU,gBAAgB,QAAQ,YAAY;EAC9C,WAAW,OAAO,KAAK,EAAE,OAAO,GAAG,YAAY,MAAM,SAAS,EAAE,CAAC;CACnE;CAEA,MAAM,SACJ,oBAAC,YAAD;EAAY,GAAI;EACZ,WAAA,EAAE,iBACF,qBAAC,QAAD;GACE,GAAI,QAAQ;GACZ,WAAW,OAAO,QAAQ,EACxB,OAAO,GAAG,QAAQ,gBAAgB,eAAe,SAAS,YAAY,OAAO,EAC/E,CAAC;GAJH,UAAA;KAMI,SAAS,QAAQ,SAAS,SAAS,oBAAC,aAAD,CAAc,CAAA;IACnD,oBAAC,QAAD;KACE,eAAY;KACZ,WAAW,OAAO,KAAK,EACrB,OAAO,GACL,WAAW,gBAAgB,CAAC,CAAC,KAAK,GAClC,QAAQ,gBAAgB,eAAe,UACvC,YAAY,IACd,EACF,CAAC;KAEA,UAAA,UACC,oBAAC,WAAD;MACQ;MACN,eAAe;MACf,WAAW;MACX,YAAW;KACZ,CAAA,IACC,cAAc,eAChB,eAEA;IAEE,CAAA;IACL,QAAQ;IACR,QAAQ;GACL;;CAEE,CAAA;CAGd,MAAM,QAAQ,KAAK;CACnB,IAAI,CAAC,WAAW,oBAAoB,CAAC,SAAS,KAAK,oBAAoB,OAAO;CAC9E,OACE,qBAAC,gBAAD,EAAA,UAAA,CACE,oBAAC,oBAAD,EAAA,UAAqB,OAA2B,CAAA,GAChD,oBAAC,aAAD;EAAa,YAAY,EAAE,OAAO,YAAY,QAAQ;EAAI,UAAA;CAAmB,CAAA,CAC/D,EAAA,CAAA;AAEpB"}
@@ -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
- /** With `modal`, show nothing while collapsed (the expanded rail slides in). @default false */
39
- hideOnCollapse?: boolean;
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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"}
@@ -49,7 +49,7 @@ const navItemStyles = tv({
49
49
  label: [
50
50
  "mt-[4px] max-w-full text-center text-label-medium",
51
51
  "text-[color:var(--_md-comp-nav-rail-item-inactive-label-text-color)]",
52
- "group-data-current/nav:text-label-medium-emphasized group-data-current/nav:text-[color:var(--_md-comp-nav-rail-item-active-label-text-color)]"
52
+ "group-data-current/nav:text-[color:var(--_md-comp-nav-rail-item-active-label-text-color)]"
53
53
  ]
54
54
  },
55
55
  "rail-start": {
@@ -61,7 +61,7 @@ const navItemStyles = tv({
61
61
  label: [
62
62
  "forced-colors:group-data-current/nav:text-[HighlightText] forced-colors:group-data-current/nav:forced-color-adjust-none",
63
63
  "text-label-large text-[color:var(--_md-comp-nav-rail-item-inactive-label-text-color)]",
64
- "group-data-current/nav:text-label-large-emphasized group-data-current/nav:text-[color:var(--_md-comp-nav-rail-item-active-label-text-color)]"
64
+ "group-data-current/nav:text-[color:var(--_md-comp-nav-rail-item-active-label-text-color)]"
65
65
  ]
66
66
  },
67
67
  "bar-top": {
@@ -72,7 +72,7 @@ const navItemStyles = tv({
72
72
  label: [
73
73
  "mt-[4px] max-w-full px-[4px] text-center text-label-medium",
74
74
  "text-[color:var(--_md-comp-nav-bar-item-inactive-label-text-color)]",
75
- "group-data-current/nav:text-label-medium-emphasized group-data-current/nav:text-[color:var(--_md-comp-nav-bar-item-active-label-text-color)]"
75
+ "group-data-current/nav:text-[color:var(--_md-comp-nav-bar-item-active-label-text-color)]"
76
76
  ]
77
77
  },
78
78
  "bar-start": {
@@ -89,7 +89,7 @@ const navItemStyles = tv({
89
89
  label: [
90
90
  "forced-colors:group-data-current/nav:text-[HighlightText] forced-colors:group-data-current/nav:forced-color-adjust-none",
91
91
  "text-label-medium text-[color:var(--_md-comp-nav-bar-item-inactive-label-text-color)]",
92
- "group-data-current/nav:text-label-medium-emphasized group-data-current/nav:text-[color:var(--_md-comp-nav-bar-item-active-label-text-color)]"
92
+ "group-data-current/nav:text-[color:var(--_md-comp-nav-bar-item-active-label-text-color)]"
93
93
  ]
94
94
  }
95
95
  },
@@ -1 +1 @@
1
- {"version":3,"file":"nav-item-styles.js","names":[],"sources":["../../../src/components/navigation/nav-item-styles.ts"],"sourcesContent":["import { tv, type VariantProps } from '../../utils/tv';\n\n/*\n * Navigation items shared by the navigation rail and the flexible navigation bar. Values\n * follow Compose Material 3 (NavigationItem.kt, WideNavigationRail.kt, ShortNavigationBar.kt\n * and the NavigationRail* / NavigationBar* token files):\n *\n * - rail-top (collapsed rail): 56×32 pill around a 24px icon, `label-medium` 4px below,\n * centred in the 96px collapsed width (labels may use all of it), at least 64px tall.\n * The item is that wide even while the rail's width animates, so its icon never moves.\n * - rail-start (expanded rail): 56px pill holding icon · 8px · `label-large`, 16px inside,\n * 20px item inset, which puts the icon on the same line as when collapsed. The pill hugs\n * its icon and label, as in M3 Expressive (the full-width pill was the navigation\n * drawer's), while the whole row stays the link or button: a larger target than Compose's\n * pill-only one, with the state layer and focus ring still on the pill.\n * - bar-top: 6px · 56×32 pill · 4px · `label-medium` · 6px (64px).\n * - bar-start: 40px pill holding icon · 4px · `label-medium`, 16px inside, centred in an\n * item as tall as the bar (64px), which is the touch target.\n *\n * Colours: icons `on-surface-variant`, selected `on-secondary-container` on a\n * `secondary-container` pill; labels `on-surface-variant`, selected `secondary` below the\n * icon or `on-secondary-container` beside it; disabled 38%. The state layer is\n * `on-secondary-container` on the pill, and a press ripples through it from the press point\n * (M3: \"a ripple passes through the indicator\"), as on every other M3 control. In every layout the current item's pill expands from\n * the centre of its icon and fades in on the default spatial spring (M3: \"the active\n * indicator expands from the center of the icon\"), and colours change on default effects.\n * The current label is bold (the emphasized label role). With a `selectedIcon`, the two\n * icons cross-fade on default effects.\n *\n * `slide` (the expanded rail's `indicatorMotion=\"slide\"`, an extension beyond the spec that\n * matches Google's Expressive showcase): the pill is whole while current and Motion moves it\n * from the previous current item on the fast spatial spring; the old pill goes at once, and\n * the content sits above every pill so a moving one never covers another item's label.\n * Under reduced motion it fades in place instead.\n *\n * Under reduced motion every pill fades in whole on the default effects spring, without\n * expanding (architecture §6).\n *\n * Labels don't truncate (M3: \"Don't truncate or display an ellipsis in place of label\n * text\"): they wrap between words, and the item grows taller, so the full label stays\n * visible under large text sizes. A word wider than the item may break where the browser\n * hyphenates it for the document's `lang` (`hyphens: auto`), but never bare (\"Searc/h\"):\n * `overflow-wrap: normal` keeps it whole, and a line it still overflows ends with an\n * ellipsis (`overflow: hidden` with `text-overflow: ellipsis`, applied per line), while the\n * other words keep wrapping. The text stays whole in the DOM, so the accessible name is the\n * full label (2026-10-16, refining 2026-10-13 and 2026-10-15).\n *\n * Badges overlap the icon's top end (BadgedBox), except in the expanded rail, where M3 puts\n * them next to the label, 4px after it.\n *\n * Forced colours (2026-10-15): the current pill keeps a `Highlight` fill\n * (`forced-color-adjust: none`), with its icon, and a label inside it, in `HighlightText`;\n * otherwise the pill became Canvas and only the bold label marked the current item. The\n * icon and a label inside the pill opt out of forced colours too (2026-10-17): with\n * `forced-color-adjust: auto` the browser paints a `Canvas` backplate behind text, so a\n * `HighlightText` label read white on white.\n *\n * The pill, its state layer and its content share one grid cell, so nothing is positioned.\n *\n * The pill, icon and label colours come from the bar's or rail's component tokens\n * (`navigation-tokens.ts`, `--md-comp-nav-{bar,rail}-item-*`): each item carries\n * `data-tokens` and `data-layout`, so the bar's layouts read the bar's item parts and the\n * rail's layouts the rail's. Disabled colours stay classes.\n *\n * Every class is written out in full so Tailwind can find it.\n */\n\n/** Variant definitions for navigation items; extend them to add variants. */\nexport const navItemStyles = tv({\n slots: {\n root: [\n 'group/nav grid min-w-0 cursor-pointer justify-items-center outline-none select-none',\n 'font-plain text-on-surface-variant',\n 'data-disabled:cursor-default',\n ],\n // The pill area: indicator, state layer and content overlap here; it carries the focus ring.\n pill: [\n 'grid rounded-full outline-offset-(--md-sys-focus-indicator-offset) outline-secondary',\n 'group-data-focus-visible/nav:outline-(length:--md-sys-focus-indicator-thickness) group-data-focus-visible/nav:outline-solid',\n ],\n indicator: [\n 'col-start-1 row-start-1 rounded-full opacity-0',\n 'group-data-current/nav:opacity-100 group-data-current/nav:[clip-path:inset(0_0_round_9999px)]',\n // Forced colours would paint the pill as Canvas: it keeps a Highlight fill instead.\n 'forced-colors:bg-[Highlight] forced-colors:forced-color-adjust-none',\n '[transition-property:clip-path,opacity]',\n '[transition-duration:var(--md-sys-motion-spring-spatial-default-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-default-easing)]',\n // Reduced motion: the pill fades in whole on the default effects spring.\n 'motion-reduce:[transition-property:opacity]',\n 'motion-reduce:[transition-duration:var(--md-sys-motion-spring-effects-default-duration)]',\n 'motion-reduce:[transition-timing-function:var(--md-sys-motion-spring-effects-default-easing)]',\n ],\n // The shared state layer and ripple, on the pill: the item's hover and focus show\n // through it, and a press ripples from the press point (NavItem sets the origin).\n stateLayer: [\n 'col-start-1 row-start-1 rounded-full state-layer',\n '[--m3-state-layer-color:var(--md-sys-color-on-secondary-container)]',\n 'group-data-hovered/nav:[--m3-state-opacity:var(--md-sys-state-hover-state-layer-opacity)]',\n 'group-data-focus-visible/nav:[--m3-state-opacity:var(--md-sys-state-focus-state-layer-opacity)]',\n ],\n // `relative` keeps the content above the indicator and state layer: elements with\n // opacity or clip-path paint in the positioned layer, above plain in-flow content.\n content: 'relative col-start-1 row-start-1 flex min-w-0 items-center justify-center',\n icon: [\n 'inline-grid size-[24px] shrink-0 place-items-center [&>svg]:size-full',\n 'group-data-disabled/nav:text-on-surface-variant/38',\n // HighlightText on the Highlight pill, kept as painted (no Canvas backplate).\n 'forced-colors:group-data-current/nav:text-[HighlightText] forced-colors:group-data-current/nav:forced-color-adjust-none',\n 'transition-colors duration-(--md-sys-motion-spring-effects-default-duration) ease-(--md-sys-motion-spring-effects-default-easing)',\n ],\n // An icon and its selected variant share one cell and cross-fade.\n iconFace: [\n 'col-start-1 row-start-1 size-full opacity-0 data-shown:opacity-100 [&>svg]:size-full',\n '[transition-property:opacity] duration-(--md-sys-motion-spring-effects-default-duration) ease-(--md-sys-motion-spring-effects-default-easing)',\n ],\n // Holds the icon (or both faces) inside a badge's anchor.\n iconStack: 'inline-grid size-[24px] place-items-center [&>svg]:size-full',\n // The expanded rail's badge, 4px after the label (the content gap is 8px).\n badge: 'inline-flex shrink-0 -ms-[4px]',\n // The `BadgedBox` around a badged icon (its root; `classNames.badge` merges in). The\n // badge takes forced colours back, which the current icon opts out of.\n badgedBox: 'forced-colors:[&>span:last-child]:forced-color-adjust-auto',\n label: [\n 'min-w-0 overflow-hidden text-ellipsis text-balance hyphens-auto wrap-normal group-data-disabled/nav:text-on-surface-variant/38',\n '[transition-property:color,opacity] duration-(--md-sys-motion-spring-effects-default-duration) ease-(--md-sys-motion-spring-effects-default-easing)',\n ],\n },\n variants: {\n layout: {\n 'rail-top': {\n // The 56px pill centres in the collapsed width; labels may use all of it (Compose).\n // The minimum height springs 64 ↔ 48px with the rail's width (`railMorph`).\n root: 'min-h-[64px] w-(--m3-rail-collapsed-width) content-start',\n pill: 'h-[32px] w-[56px]',\n indicator:\n '[clip-path:inset(0_50%_round_9999px)] bg-[var(--_md-comp-nav-rail-item-active-indicator-color)]',\n icon: 'text-[color:var(--_md-comp-nav-rail-item-inactive-icon-color)] group-data-current/nav:text-[color:var(--_md-comp-nav-rail-item-active-icon-color)]',\n label: [\n 'mt-[4px] max-w-full text-center text-label-medium',\n 'text-[color:var(--_md-comp-nav-rail-item-inactive-label-text-color)]',\n 'group-data-current/nav:text-label-medium-emphasized group-data-current/nav:text-[color:var(--_md-comp-nav-rail-item-active-label-text-color)]',\n ],\n },\n 'rail-start': {\n root: 'min-h-[48px] w-full justify-items-start px-(--m3-rail-item-inset)',\n pill: 'min-h-[56px] max-w-full',\n // The icon's centre: 16px padding + half the 24px icon from the start edge.\n indicator: [\n '[clip-path:inset(0_calc(100%_-_28px)_0_28px_round_9999px)] rtl:[clip-path:inset(0_28px_0_calc(100%_-_28px)_round_9999px)]',\n 'bg-[var(--_md-comp-nav-rail-item-active-indicator-color)]',\n ],\n content: 'justify-start gap-[8px] ps-[16px] pe-[16px] py-[8px]',\n icon: 'text-[color:var(--_md-comp-nav-rail-item-inactive-icon-color)] group-data-current/nav:text-[color:var(--_md-comp-nav-rail-item-active-icon-color)]',\n label: [\n // Inside the Highlight pill: HighlightText, without the Canvas backplate forced\n // colours paint behind text (white on white otherwise).\n 'forced-colors:group-data-current/nav:text-[HighlightText] forced-colors:group-data-current/nav:forced-color-adjust-none',\n 'text-label-large text-[color:var(--_md-comp-nav-rail-item-inactive-label-text-color)]',\n 'group-data-current/nav:text-label-large-emphasized group-data-current/nav:text-[color:var(--_md-comp-nav-rail-item-active-label-text-color)]',\n ],\n },\n // Bar items stretch to the bar's height (64px, or taller when a label wraps), so the\n // whole height is the touch target. Stacked items keep their icons on one line at the\n // top; inline items centre their pill.\n 'bar-top': {\n root: 'self-stretch content-start py-[6px]',\n pill: 'h-[32px] w-[56px]',\n indicator:\n '[clip-path:inset(0_50%_round_9999px)] bg-[var(--_md-comp-nav-bar-item-active-indicator-color)]',\n icon: 'text-[color:var(--_md-comp-nav-bar-item-inactive-icon-color)] group-data-current/nav:text-[color:var(--_md-comp-nav-bar-item-active-icon-color)]',\n label: [\n 'mt-[4px] max-w-full px-[4px] text-center text-label-medium',\n 'text-[color:var(--_md-comp-nav-bar-item-inactive-label-text-color)]',\n 'group-data-current/nav:text-label-medium-emphasized group-data-current/nav:text-[color:var(--_md-comp-nav-bar-item-active-label-text-color)]',\n ],\n },\n 'bar-start': {\n // Content-sized; 4px each side keeps neighbouring pills 8px apart.\n root: 'self-stretch content-center px-[4px]',\n pill: 'min-h-[40px] max-w-full',\n indicator: [\n '[clip-path:inset(0_calc(100%_-_28px)_0_28px_round_9999px)] rtl:[clip-path:inset(0_28px_0_calc(100%_-_28px)_round_9999px)]',\n 'bg-[var(--_md-comp-nav-bar-item-active-indicator-color)]',\n ],\n content: 'gap-[4px] ps-[16px] pe-[16px] py-[4px]',\n // A large badge reaches past the icon's end edge into the label's place (M3: badges\n // overlap the icon). Its overhang takes room in the row so the label starts 4px after\n // the badge (2026-10-15): the icon box grows to the badge, which hangs from a box as\n // wide as itself, 12px inside the 24px icon, as `BadgedBox` places it.\n icon: [\n 'text-[color:var(--_md-comp-nav-bar-item-inactive-icon-color)] group-data-current/nav:text-[color:var(--_md-comp-nav-bar-item-active-icon-color)]',\n 'has-[[data-size=large]]:w-auto has-[[data-size=large]]:min-w-[24px] has-[[data-size=large]]:justify-items-start',\n ],\n badgedBox: [\n 'has-[[data-size=large]]:grid-cols-[auto]',\n '[&:has([data-size=large])>span:first-child]:w-[24px] [&:has([data-size=large])>span:first-child]:justify-self-start',\n '[&>span:last-child:has([data-size=large])]:w-max [&>span:last-child:has([data-size=large])]:ms-[12px]',\n ],\n label: [\n // Inside the Highlight pill: HighlightText, without the Canvas backplate forced\n // colours paint behind text (white on white otherwise).\n 'forced-colors:group-data-current/nav:text-[HighlightText] forced-colors:group-data-current/nav:forced-color-adjust-none',\n 'text-label-medium text-[color:var(--_md-comp-nav-bar-item-inactive-label-text-color)]',\n 'group-data-current/nav:text-label-medium-emphasized group-data-current/nav:text-[color:var(--_md-comp-nav-bar-item-active-label-text-color)]',\n ],\n },\n },\n /** Labels that move when the rail changes width fade in at their new place. */\n enterLabel: { true: { label: 'starting:opacity-0' }, false: {} },\n /** The pill slides between items (Motion moves it) instead of expanding in place. */\n slide: {\n true: {\n indicator: '[transition-property:none] motion-reduce:[transition-property:opacity]',\n content: 'z-[1]',\n },\n false: {},\n },\n },\n compoundVariants: [\n // As the rail expands or collapses, an item's minimum height springs between 64px and\n // 48px on the default spatial spring, as Compose's itemMinHeight (WideNavigationRail.kt;\n // 2026-10-17: it swapped in one frame, so the icons jumped). Instant under reduced motion.\n {\n layout: ['rail-top', 'rail-start'],\n class: {\n root: '[transition-property:min-height] [transition-duration:var(--md-sys-motion-spring-spatial-default-duration)] [transition-timing-function:var(--md-sys-motion-spring-spatial-default-easing)]',\n },\n },\n ],\n defaultVariants: { layout: 'bar-top', enterLabel: false, slide: false },\n});\n\nexport type NavItemLayout = NonNullable<VariantProps<typeof navItemStyles>['layout']>;\n"],"mappings":";;;AAoEA,MAAa,gBAAgB,GAAG;CAC9B,OAAO;EACL,MAAM;GACJ;GACA;GACA;EACF;EAEA,MAAM,CACJ,wFACA,6HACF;EACA,WAAW;GACT;GACA;GAEA;GACA;GACA;GACA;GAEA;GACA;GACA;EACF;EAGA,YAAY;GACV;GACA;GACA;GACA;EACF;EAGA,SAAS;EACT,MAAM;GACJ;GACA;GAEA;GACA;EACF;EAEA,UAAU,CACR,wFACA,+IACF;EAEA,WAAW;EAEX,OAAO;EAGP,WAAW;EACX,OAAO,CACL,kIACA,qJACF;CACF;CACA,UAAU;EACR,QAAQ;GACN,YAAY;IAGV,MAAM;IACN,MAAM;IACN,WACE;IACF,MAAM;IACN,OAAO;KACL;KACA;KACA;IACF;GACF;GACA,cAAc;IACZ,MAAM;IACN,MAAM;IAEN,WAAW,CACT,6HACA,2DACF;IACA,SAAS;IACT,MAAM;IACN,OAAO;KAGL;KACA;KACA;IACF;GACF;GAIA,WAAW;IACT,MAAM;IACN,MAAM;IACN,WACE;IACF,MAAM;IACN,OAAO;KACL;KACA;KACA;IACF;GACF;GACA,aAAa;IAEX,MAAM;IACN,MAAM;IACN,WAAW,CACT,6HACA,0DACF;IACA,SAAS;IAKT,MAAM,CACJ,oJACA,iHACF;IACA,WAAW;KACT;KACA;KACA;IACF;IACA,OAAO;KAGL;KACA;KACA;IACF;GACF;EACF;;EAEA,YAAY;GAAE,MAAM,EAAE,OAAO,qBAAqB;GAAG,OAAO,CAAC;EAAE;;EAE/D,OAAO;GACL,MAAM;IACJ,WAAW;IACX,SAAS;GACX;GACA,OAAO,CAAC;EACV;CACF;CACA,kBAAkB,CAIhB;EACE,QAAQ,CAAC,YAAY,YAAY;EACjC,OAAO,EACL,MAAM,8LACR;CACF,CACF;CACA,iBAAiB;EAAE,QAAQ;EAAW,YAAY;EAAO,OAAO;CAAM;AACxE,CAAC"}
1
+ {"version":3,"file":"nav-item-styles.js","names":[],"sources":["../../../src/components/navigation/nav-item-styles.ts"],"sourcesContent":["import { tv, type VariantProps } from '../../utils/tv';\n\n/*\n * Navigation items shared by the navigation rail and the flexible navigation bar. Values\n * follow Compose Material 3 (NavigationItem.kt, WideNavigationRail.kt, ShortNavigationBar.kt\n * and the NavigationRail* / NavigationBar* token files):\n *\n * - rail-top (collapsed rail): 56×32 pill around a 24px icon, `label-medium` 4px below,\n * centred in the 96px collapsed width (labels may use all of it), at least 64px tall.\n * The item is that wide even while the rail's width animates, so its icon never moves.\n * - rail-start (expanded rail): 56px pill holding icon · 8px · `label-large`, 16px inside,\n * 20px item inset, which puts the icon on the same line as when collapsed. The pill hugs\n * its icon and label, as in M3 Expressive (the full-width pill was the navigation\n * drawer's), while the whole row stays the link or button: a larger target than Compose's\n * pill-only one, with the state layer and focus ring still on the pill.\n * - bar-top: 6px · 56×32 pill · 4px · `label-medium` · 6px (64px).\n * - bar-start: 40px pill holding icon · 4px · `label-medium`, 16px inside, centred in an\n * item as tall as the bar (64px), which is the touch target.\n *\n * Colours: icons `on-surface-variant`, selected `on-secondary-container` on a\n * `secondary-container` pill; labels `on-surface-variant`, selected `secondary` below the\n * icon or `on-secondary-container` beside it; disabled 38%. The state layer is\n * `on-secondary-container` on the pill, and a press ripples through it from the press point\n * (M3: \"a ripple passes through the indicator\"), as on every other M3 control. In every layout the current item's pill expands from\n * the centre of its icon and fades in on the default spatial spring (M3: \"the active\n * indicator expands from the center of the icon\"), and colours change on default effects.\n * The current label keeps its weight (M3 Expressive). With a `selectedIcon`, the two\n * icons cross-fade on default effects.\n *\n * `slide` (the expanded rail's `indicatorMotion=\"slide\"`, an extension beyond the spec that\n * matches Google's Expressive showcase): the pill is whole while current and Motion moves it\n * from the previous current item on the fast spatial spring; the old pill goes at once, and\n * the content sits above every pill so a moving one never covers another item's label.\n * Under reduced motion it fades in place instead.\n *\n * Under reduced motion every pill fades in whole on the default effects spring, without\n * expanding (architecture §6).\n *\n * Labels don't truncate (M3: \"Don't truncate or display an ellipsis in place of label\n * text\"): they wrap between words, and the item grows taller, so the full label stays\n * visible under large text sizes. A word wider than the item may break where the browser\n * hyphenates it for the document's `lang` (`hyphens: auto`), but never bare (\"Searc/h\"):\n * `overflow-wrap: normal` keeps it whole, and a line it still overflows ends with an\n * ellipsis (`overflow: hidden` with `text-overflow: ellipsis`, applied per line), while the\n * other words keep wrapping. The text stays whole in the DOM, so the accessible name is the\n * full label (2026-10-16, refining 2026-10-13 and 2026-10-15).\n *\n * Badges overlap the icon's top end (BadgedBox), except in the expanded rail, where M3 puts\n * them next to the label, 4px after it.\n *\n * Forced colours (2026-10-15): the current pill keeps a `Highlight` fill\n * (`forced-color-adjust: none`), with its icon, and a label inside it, in `HighlightText`;\n * otherwise the pill became Canvas and only the bold label marked the current item. The\n * icon and a label inside the pill opt out of forced colours too (2026-10-17): with\n * `forced-color-adjust: auto` the browser paints a `Canvas` backplate behind text, so a\n * `HighlightText` label read white on white.\n *\n * The pill, its state layer and its content share one grid cell, so nothing is positioned.\n *\n * The pill, icon and label colours come from the bar's or rail's component tokens\n * (`navigation-tokens.ts`, `--md-comp-nav-{bar,rail}-item-*`): each item carries\n * `data-tokens` and `data-layout`, so the bar's layouts read the bar's item parts and the\n * rail's layouts the rail's. Disabled colours stay classes.\n *\n * Every class is written out in full so Tailwind can find it.\n */\n\n/** Variant definitions for navigation items; extend them to add variants. */\nexport const navItemStyles = tv({\n slots: {\n root: [\n 'group/nav grid min-w-0 cursor-pointer justify-items-center outline-none select-none',\n 'font-plain text-on-surface-variant',\n 'data-disabled:cursor-default',\n ],\n // The pill area: indicator, state layer and content overlap here; it carries the focus ring.\n pill: [\n 'grid rounded-full outline-offset-(--md-sys-focus-indicator-offset) outline-secondary',\n 'group-data-focus-visible/nav:outline-(length:--md-sys-focus-indicator-thickness) group-data-focus-visible/nav:outline-solid',\n ],\n indicator: [\n 'col-start-1 row-start-1 rounded-full opacity-0',\n 'group-data-current/nav:opacity-100 group-data-current/nav:[clip-path:inset(0_0_round_9999px)]',\n // Forced colours would paint the pill as Canvas: it keeps a Highlight fill instead.\n 'forced-colors:bg-[Highlight] forced-colors:forced-color-adjust-none',\n '[transition-property:clip-path,opacity]',\n '[transition-duration:var(--md-sys-motion-spring-spatial-default-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-default-easing)]',\n // Reduced motion: the pill fades in whole on the default effects spring.\n 'motion-reduce:[transition-property:opacity]',\n 'motion-reduce:[transition-duration:var(--md-sys-motion-spring-effects-default-duration)]',\n 'motion-reduce:[transition-timing-function:var(--md-sys-motion-spring-effects-default-easing)]',\n ],\n // The shared state layer and ripple, on the pill: the item's hover and focus show\n // through it, and a press ripples from the press point (NavItem sets the origin).\n stateLayer: [\n 'col-start-1 row-start-1 rounded-full state-layer',\n '[--m3-state-layer-color:var(--md-sys-color-on-secondary-container)]',\n 'group-data-hovered/nav:[--m3-state-opacity:var(--md-sys-state-hover-state-layer-opacity)]',\n 'group-data-focus-visible/nav:[--m3-state-opacity:var(--md-sys-state-focus-state-layer-opacity)]',\n ],\n // `relative` keeps the content above the indicator and state layer: elements with\n // opacity or clip-path paint in the positioned layer, above plain in-flow content.\n content: 'relative col-start-1 row-start-1 flex min-w-0 items-center justify-center',\n icon: [\n 'inline-grid size-[24px] shrink-0 place-items-center [&>svg]:size-full',\n 'group-data-disabled/nav:text-on-surface-variant/38',\n // HighlightText on the Highlight pill, kept as painted (no Canvas backplate).\n 'forced-colors:group-data-current/nav:text-[HighlightText] forced-colors:group-data-current/nav:forced-color-adjust-none',\n 'transition-colors duration-(--md-sys-motion-spring-effects-default-duration) ease-(--md-sys-motion-spring-effects-default-easing)',\n ],\n // An icon and its selected variant share one cell and cross-fade.\n iconFace: [\n 'col-start-1 row-start-1 size-full opacity-0 data-shown:opacity-100 [&>svg]:size-full',\n '[transition-property:opacity] duration-(--md-sys-motion-spring-effects-default-duration) ease-(--md-sys-motion-spring-effects-default-easing)',\n ],\n // Holds the icon (or both faces) inside a badge's anchor.\n iconStack: 'inline-grid size-[24px] place-items-center [&>svg]:size-full',\n // The expanded rail's badge, 4px after the label (the content gap is 8px).\n badge: 'inline-flex shrink-0 -ms-[4px]',\n // The `BadgedBox` around a badged icon (its root; `classNames.badge` merges in). The\n // badge takes forced colours back, which the current icon opts out of.\n badgedBox: 'forced-colors:[&>span:last-child]:forced-color-adjust-auto',\n label: [\n 'min-w-0 overflow-hidden text-ellipsis text-balance hyphens-auto wrap-normal group-data-disabled/nav:text-on-surface-variant/38',\n '[transition-property:color,opacity] duration-(--md-sys-motion-spring-effects-default-duration) ease-(--md-sys-motion-spring-effects-default-easing)',\n ],\n },\n variants: {\n layout: {\n 'rail-top': {\n // The 56px pill centres in the collapsed width; labels may use all of it (Compose).\n // The minimum height springs 64 ↔ 48px with the rail's width (`railMorph`).\n root: 'min-h-[64px] w-(--m3-rail-collapsed-width) content-start',\n pill: 'h-[32px] w-[56px]',\n indicator:\n '[clip-path:inset(0_50%_round_9999px)] bg-[var(--_md-comp-nav-rail-item-active-indicator-color)]',\n icon: 'text-[color:var(--_md-comp-nav-rail-item-inactive-icon-color)] group-data-current/nav:text-[color:var(--_md-comp-nav-rail-item-active-icon-color)]',\n label: [\n 'mt-[4px] max-w-full text-center text-label-medium',\n 'text-[color:var(--_md-comp-nav-rail-item-inactive-label-text-color)]',\n 'group-data-current/nav:text-[color:var(--_md-comp-nav-rail-item-active-label-text-color)]',\n ],\n },\n 'rail-start': {\n root: 'min-h-[48px] w-full justify-items-start px-(--m3-rail-item-inset)',\n pill: 'min-h-[56px] max-w-full',\n // The icon's centre: 16px padding + half the 24px icon from the start edge.\n indicator: [\n '[clip-path:inset(0_calc(100%_-_28px)_0_28px_round_9999px)] rtl:[clip-path:inset(0_28px_0_calc(100%_-_28px)_round_9999px)]',\n 'bg-[var(--_md-comp-nav-rail-item-active-indicator-color)]',\n ],\n content: 'justify-start gap-[8px] ps-[16px] pe-[16px] py-[8px]',\n icon: 'text-[color:var(--_md-comp-nav-rail-item-inactive-icon-color)] group-data-current/nav:text-[color:var(--_md-comp-nav-rail-item-active-icon-color)]',\n label: [\n // Inside the Highlight pill: HighlightText, without the Canvas backplate forced\n // colours paint behind text (white on white otherwise).\n 'forced-colors:group-data-current/nav:text-[HighlightText] forced-colors:group-data-current/nav:forced-color-adjust-none',\n 'text-label-large text-[color:var(--_md-comp-nav-rail-item-inactive-label-text-color)]',\n 'group-data-current/nav:text-[color:var(--_md-comp-nav-rail-item-active-label-text-color)]',\n ],\n },\n // Bar items stretch to the bar's height (64px, or taller when a label wraps), so the\n // whole height is the touch target. Stacked items keep their icons on one line at the\n // top; inline items centre their pill.\n 'bar-top': {\n root: 'self-stretch content-start py-[6px]',\n pill: 'h-[32px] w-[56px]',\n indicator:\n '[clip-path:inset(0_50%_round_9999px)] bg-[var(--_md-comp-nav-bar-item-active-indicator-color)]',\n icon: 'text-[color:var(--_md-comp-nav-bar-item-inactive-icon-color)] group-data-current/nav:text-[color:var(--_md-comp-nav-bar-item-active-icon-color)]',\n label: [\n 'mt-[4px] max-w-full px-[4px] text-center text-label-medium',\n 'text-[color:var(--_md-comp-nav-bar-item-inactive-label-text-color)]',\n 'group-data-current/nav:text-[color:var(--_md-comp-nav-bar-item-active-label-text-color)]',\n ],\n },\n 'bar-start': {\n // Content-sized; 4px each side keeps neighbouring pills 8px apart.\n root: 'self-stretch content-center px-[4px]',\n pill: 'min-h-[40px] max-w-full',\n indicator: [\n '[clip-path:inset(0_calc(100%_-_28px)_0_28px_round_9999px)] rtl:[clip-path:inset(0_28px_0_calc(100%_-_28px)_round_9999px)]',\n 'bg-[var(--_md-comp-nav-bar-item-active-indicator-color)]',\n ],\n content: 'gap-[4px] ps-[16px] pe-[16px] py-[4px]',\n // A large badge reaches past the icon's end edge into the label's place (M3: badges\n // overlap the icon). Its overhang takes room in the row so the label starts 4px after\n // the badge (2026-10-15): the icon box grows to the badge, which hangs from a box as\n // wide as itself, 12px inside the 24px icon, as `BadgedBox` places it.\n icon: [\n 'text-[color:var(--_md-comp-nav-bar-item-inactive-icon-color)] group-data-current/nav:text-[color:var(--_md-comp-nav-bar-item-active-icon-color)]',\n 'has-[[data-size=large]]:w-auto has-[[data-size=large]]:min-w-[24px] has-[[data-size=large]]:justify-items-start',\n ],\n badgedBox: [\n 'has-[[data-size=large]]:grid-cols-[auto]',\n '[&:has([data-size=large])>span:first-child]:w-[24px] [&:has([data-size=large])>span:first-child]:justify-self-start',\n '[&>span:last-child:has([data-size=large])]:w-max [&>span:last-child:has([data-size=large])]:ms-[12px]',\n ],\n label: [\n // Inside the Highlight pill: HighlightText, without the Canvas backplate forced\n // colours paint behind text (white on white otherwise).\n 'forced-colors:group-data-current/nav:text-[HighlightText] forced-colors:group-data-current/nav:forced-color-adjust-none',\n 'text-label-medium text-[color:var(--_md-comp-nav-bar-item-inactive-label-text-color)]',\n 'group-data-current/nav:text-[color:var(--_md-comp-nav-bar-item-active-label-text-color)]',\n ],\n },\n },\n /** Labels that move when the rail changes width fade in at their new place. */\n enterLabel: { true: { label: 'starting:opacity-0' }, false: {} },\n /** The pill slides between items (Motion moves it) instead of expanding in place. */\n slide: {\n true: {\n indicator: '[transition-property:none] motion-reduce:[transition-property:opacity]',\n content: 'z-[1]',\n },\n false: {},\n },\n },\n compoundVariants: [\n // As the rail expands or collapses, an item's minimum height springs between 64px and\n // 48px on the default spatial spring, as Compose's itemMinHeight (WideNavigationRail.kt;\n // 2026-10-17: it swapped in one frame, so the icons jumped). Instant under reduced motion.\n {\n layout: ['rail-top', 'rail-start'],\n class: {\n root: '[transition-property:min-height] [transition-duration:var(--md-sys-motion-spring-spatial-default-duration)] [transition-timing-function:var(--md-sys-motion-spring-spatial-default-easing)]',\n },\n },\n ],\n defaultVariants: { layout: 'bar-top', enterLabel: false, slide: false },\n});\n\nexport type NavItemLayout = NonNullable<VariantProps<typeof navItemStyles>['layout']>;\n"],"mappings":";;;AAoEA,MAAa,gBAAgB,GAAG;CAC9B,OAAO;EACL,MAAM;GACJ;GACA;GACA;EACF;EAEA,MAAM,CACJ,wFACA,6HACF;EACA,WAAW;GACT;GACA;GAEA;GACA;GACA;GACA;GAEA;GACA;GACA;EACF;EAGA,YAAY;GACV;GACA;GACA;GACA;EACF;EAGA,SAAS;EACT,MAAM;GACJ;GACA;GAEA;GACA;EACF;EAEA,UAAU,CACR,wFACA,+IACF;EAEA,WAAW;EAEX,OAAO;EAGP,WAAW;EACX,OAAO,CACL,kIACA,qJACF;CACF;CACA,UAAU;EACR,QAAQ;GACN,YAAY;IAGV,MAAM;IACN,MAAM;IACN,WACE;IACF,MAAM;IACN,OAAO;KACL;KACA;KACA;IACF;GACF;GACA,cAAc;IACZ,MAAM;IACN,MAAM;IAEN,WAAW,CACT,6HACA,2DACF;IACA,SAAS;IACT,MAAM;IACN,OAAO;KAGL;KACA;KACA;IACF;GACF;GAIA,WAAW;IACT,MAAM;IACN,MAAM;IACN,WACE;IACF,MAAM;IACN,OAAO;KACL;KACA;KACA;IACF;GACF;GACA,aAAa;IAEX,MAAM;IACN,MAAM;IACN,WAAW,CACT,6HACA,0DACF;IACA,SAAS;IAKT,MAAM,CACJ,oJACA,iHACF;IACA,WAAW;KACT;KACA;KACA;IACF;IACA,OAAO;KAGL;KACA;KACA;IACF;GACF;EACF;;EAEA,YAAY;GAAE,MAAM,EAAE,OAAO,qBAAqB;GAAG,OAAO,CAAC;EAAE;;EAE/D,OAAO;GACL,MAAM;IACJ,WAAW;IACX,SAAS;GACX;GACA,OAAO,CAAC;EACV;CACF;CACA,kBAAkB,CAIhB;EACE,QAAQ,CAAC,YAAY,YAAY;EACjC,OAAO,EACL,MAAM,8LACR;CACF,CACF;CACA,iBAAiB;EAAE,QAAQ;EAAW,YAAY;EAAO,OAAO;CAAM;AACxE,CAAC"}
@@ -17,6 +17,13 @@ export declare const navigationRailStyles: import("tailwind-variants").TVReturnT
17
17
  items: string;
18
18
  };
19
19
  };
20
+ /** `hideOnCollapse="compact"`: the collapsed rail shows from 600px wide. */
21
+ hiddenOnCompact: {
22
+ true: {
23
+ root: string;
24
+ };
25
+ false: {};
26
+ };
20
27
  /** A divider on the edge next to the content. */
21
28
  divider: {
22
29
  true: {
@@ -69,6 +76,13 @@ export declare const navigationRailStyles: import("tailwind-variants").TVReturnT
69
76
  items: string;
70
77
  };
71
78
  };
79
+ /** `hideOnCollapse="compact"`: the collapsed rail shows from 600px wide. */
80
+ hiddenOnCompact: {
81
+ true: {
82
+ root: string;
83
+ };
84
+ false: {};
85
+ };
72
86
  /** A divider on the edge next to the content. */
73
87
  divider: {
74
88
  true: {
@@ -121,6 +135,13 @@ export declare const navigationRailStyles: import("tailwind-variants").TVReturnT
121
135
  items: string;
122
136
  };
123
137
  };
138
+ /** `hideOnCollapse="compact"`: the collapsed rail shows from 600px wide. */
139
+ hiddenOnCompact: {
140
+ true: {
141
+ root: string;
142
+ };
143
+ false: {};
144
+ };
124
145
  /** A divider on the edge next to the content. */
125
146
  divider: {
126
147
  true: {
@@ -12,7 +12,7 @@ const oneLineWhileResizing = "[&[data-widening]_[data-nav-label]]:text-nowrap";
12
12
  const navigationRailStyles = tv({
13
13
  slots: {
14
14
  root: [
15
- "flex w-(--m3-rail-width) shrink-0 flex-col text-on-surface",
15
+ "relative flex w-(--m3-rail-width) shrink-0 flex-col text-on-surface",
16
16
  "bg-[var(--_md-comp-nav-rail-collapsed-container-color)] data-expanded:bg-[var(--_md-comp-nav-rail-expanded-container-color)]",
17
17
  itemInset,
18
18
  oneLineWhileResizing,
@@ -55,6 +55,11 @@ const navigationRailStyles = tv({
55
55
  top: {},
56
56
  center: { items: "my-auto" }
57
57
  },
58
+ /** `hideOnCollapse="compact"`: the collapsed rail shows from 600px wide. */
59
+ hiddenOnCompact: {
60
+ true: { root: "max-medium:hidden" },
61
+ false: {}
62
+ },
58
63
  /** A divider on the edge next to the content. */
59
64
  divider: {
60
65
  true: { root: "border-e border-outline-variant" },