@windmorph/react 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/autocomplete/Autocomplete.d.ts +4 -1
- package/dist/components/autocomplete/Autocomplete.js +3 -5
- package/dist/components/autocomplete/Autocomplete.js.map +1 -1
- package/dist/components/badge/Badge.d.ts +5 -2
- package/dist/components/badge/Badge.js +7 -3
- package/dist/components/badge/Badge.js.map +1 -1
- package/dist/components/button/Button.d.ts +4 -2
- package/dist/components/button/Button.js.map +1 -1
- package/dist/components/button/ButtonContext.d.ts +1 -1
- package/dist/components/button/button-pending.js +2 -4
- package/dist/components/button/button-pending.js.map +1 -1
- package/dist/{composites → components}/button-group/ButtonGroup.d.ts +4 -4
- package/dist/{composites → components}/button-group/ButtonGroup.js +9 -7
- package/dist/components/button-group/ButtonGroup.js.map +1 -0
- package/dist/{composites → components}/button-group/button-group-styles.d.ts +1 -1
- package/dist/{composites → components}/button-group/button-group-styles.js +1 -1
- package/dist/{composites → components}/button-group/button-group-styles.js.map +1 -1
- package/dist/{composites → components}/button-group/lazy-overflow-menu.js +2 -2
- package/dist/components/button-group/lazy-overflow-menu.js.map +1 -0
- package/dist/{composites → components}/button-group/overflow-menu-list.js +2 -2
- package/dist/components/button-group/overflow-menu-list.js.map +1 -0
- package/dist/{composites → components}/button-group/use-overflow.js +1 -1
- package/dist/{composites → components}/button-group/use-overflow.js.map +1 -1
- package/dist/{composites → components}/button-group/use-press-expansion.js +1 -1
- package/dist/{composites → components}/button-group/use-press-expansion.js.map +1 -1
- package/dist/components/carousel/Carousel.d.ts +4 -1
- package/dist/components/carousel/Carousel.js +2 -4
- package/dist/components/carousel/Carousel.js.map +1 -1
- package/dist/components/checkbox/checkbox-styles.d.ts +3 -3
- package/dist/components/chip/Chip.d.ts +8 -2
- package/dist/components/chip/Chip.js +3 -8
- package/dist/components/chip/Chip.js.map +1 -1
- package/dist/components/chip/chip-tokens.d.ts +4 -4
- package/dist/components/date-picker/DateField.d.ts +4 -1
- package/dist/components/date-picker/DateField.js +3 -1
- package/dist/components/date-picker/DateField.js.map +1 -1
- package/dist/components/date-picker/DatePicker.d.ts +4 -1
- package/dist/components/date-picker/DatePicker.js +5 -2
- package/dist/components/date-picker/DatePicker.js.map +1 -1
- package/dist/components/date-picker/PickerDialog.d.ts +4 -1
- package/dist/components/date-picker/PickerDialog.js +3 -1
- package/dist/components/date-picker/PickerDialog.js.map +1 -1
- package/dist/components/date-picker/date-picker-styles.d.ts +12 -12
- package/dist/components/date-picker/date-picker-tokens.d.ts +1 -1
- package/dist/components/dialog/Dialog.d.ts +5 -2
- package/dist/components/dialog/Dialog.js +4 -2
- package/dist/components/dialog/Dialog.js.map +1 -1
- package/dist/components/dialog/dialog-tokens.d.ts +1 -1
- package/dist/components/fab-menu/FabMenu.js +2 -2
- package/dist/components/fab-menu/FabMenu.js.map +1 -1
- package/dist/components/icon-button/IconButton.d.ts +4 -2
- package/dist/components/icon-button/IconButton.js.map +1 -1
- package/dist/components/labels/LabelsProvider.d.ts +35 -0
- package/dist/components/labels/LabelsProvider.js +47 -0
- package/dist/components/labels/LabelsProvider.js.map +1 -0
- package/dist/components/labels/use-labels.d.ts +16 -0
- package/dist/components/labels/use-labels.js +42 -0
- package/dist/components/labels/use-labels.js.map +1 -0
- package/dist/components/menu/Menu.d.ts +17 -1
- package/dist/components/menu/Menu.js +21 -11
- package/dist/components/menu/Menu.js.map +1 -1
- package/dist/components/menu/menu-styles.js +12 -12
- package/dist/components/menu/menu-styles.js.map +1 -1
- package/dist/components/menu/menu-tokens.d.ts +1 -1
- package/dist/components/menu/menu-tokens.js.map +1 -1
- package/dist/components/navigation/NavItem.d.ts +1 -1
- package/dist/components/navigation/NavItem.js +9 -6
- package/dist/components/navigation/NavItem.js.map +1 -1
- package/dist/components/navigation/navigation-styles.js +1 -1
- package/dist/components/navigation/navigation-styles.js.map +1 -1
- package/dist/components/pull-to-refresh/PullToRefresh.d.ts +1 -1
- package/dist/components/pull-to-refresh/PullToRefresh.js +2 -4
- package/dist/components/pull-to-refresh/PullToRefresh.js.map +1 -1
- package/dist/components/search/SearchField.d.ts +4 -1
- package/dist/components/search/SearchField.js +2 -4
- package/dist/components/search/SearchField.js.map +1 -1
- package/dist/components/search/search-styles.js +2 -2
- package/dist/components/search/search-styles.js.map +1 -1
- package/dist/components/select/Select.d.ts +4 -1
- package/dist/components/select/Select.js +2 -4
- package/dist/components/select/Select.js.map +1 -1
- package/dist/components/sheet/BottomSheet.d.ts +4 -1
- package/dist/components/sheet/BottomSheet.js +2 -4
- package/dist/components/sheet/BottomSheet.js.map +1 -1
- package/dist/components/sheet/SideSheet.d.ts +4 -1
- package/dist/components/sheet/SideSheet.js +2 -4
- package/dist/components/sheet/SideSheet.js.map +1 -1
- package/dist/components/slider/Slider.d.ts +4 -1
- package/dist/components/slider/Slider.js +7 -5
- package/dist/components/slider/Slider.js.map +1 -1
- package/dist/components/snackbar/Snackbar.d.ts +8 -2
- package/dist/components/snackbar/Snackbar.js +2 -4
- package/dist/components/snackbar/Snackbar.js.map +1 -1
- package/dist/components/snackbar/snackbar-tokens.d.ts +1 -1
- package/dist/components/split-button/SplitButton.d.ts +4 -1
- package/dist/components/split-button/SplitButton.js +4 -2
- package/dist/components/split-button/SplitButton.js.map +1 -1
- package/dist/components/switch/switch-styles.d.ts +3 -3
- package/dist/components/text-field/TextField.d.ts +4 -1
- package/dist/components/text-field/TextField.js +2 -4
- package/dist/components/text-field/TextField.js.map +1 -1
- package/dist/components/text-field/text-field-styles.js +2 -2
- package/dist/components/text-field/text-field-styles.js.map +1 -1
- package/dist/components/time-picker/TimeField.d.ts +24 -2
- package/dist/components/time-picker/TimeField.js +40 -12
- package/dist/components/time-picker/TimeField.js.map +1 -1
- package/dist/components/time-picker/TimeFieldList.js +95 -0
- package/dist/components/time-picker/TimeFieldList.js.map +1 -0
- package/dist/components/time-picker/TimePicker.d.ts +9 -5
- package/dist/components/time-picker/TimePicker.js +5 -2
- package/dist/components/time-picker/TimePicker.js.map +1 -1
- package/dist/components/time-picker/time-list.js +33 -0
- package/dist/components/time-picker/time-list.js.map +1 -0
- package/dist/components/tooltip/Tooltip.d.ts +1 -1
- package/dist/components/tooltip/tooltip-tokens.d.ts +1 -1
- package/dist/index.d.ts +40 -38
- package/dist/index.js +4 -3
- package/dist/primitives/use-m3-interaction.js +10 -6
- package/dist/primitives/use-m3-interaction.js.map +1 -1
- package/dist/primitives.d.ts +7 -4
- package/dist/primitives.js +4 -1
- package/dist/styles/utilities.css +22 -15
- package/dist/styles.compiled.css +1 -1
- package/dist/utils/use-scroll-keys.d.ts +29 -0
- package/dist/utils/use-scroll-keys.js +66 -0
- package/dist/utils/use-scroll-keys.js.map +1 -0
- package/dist/utils/use-scroll-restoration.d.ts +43 -0
- package/dist/utils/use-scroll-restoration.js +173 -0
- package/dist/utils/use-scroll-restoration.js.map +1 -0
- package/dist/windmorph-labels.d.ts +101 -0
- package/package.json +1 -1
- package/dist/composites/button-group/ButtonGroup.js.map +0 -1
- package/dist/composites/button-group/lazy-overflow-menu.js.map +0 -1
- package/dist/composites/button-group/overflow-menu-list.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menu-styles.js","names":[],"sources":["../../../src/components/menu/menu-styles.ts"],"sourcesContent":["import { tv, type VariantProps } from '../../utils/tv';\n\n/*\n * Values follow the M3 Expressive menu spec and Compose Material 3's Expressive menu\n * (Menu.kt, MenuDefaults.kt and the Menu / StandardMenu / VibrantMenu / SegmentedMenu\n * token files):\n * - two layouts (M3 \"Vertical menu with gap\" / \"with divider\"). `gap`: groups are separate\n * surfaces 2px apart at elevation 2, with 4px padding (the SegmentedMenu `GroupPadding`\n * token); corners: only 16px; first 16px top / 8px bottom; middle 8px; last 8px top /\n * 16px bottom. `divider`: one surface with 4px padding and 16px corners, groups split by\n * a 1px `outline-variant` divider with 8px above and below (the baseline menu's divider\n * padding). M3 recommends dividers on the web and in scrolling menus;\n * - the outer corners read `--m3-menu-corner`, so they morph while a submenu is open: the\n * menu holding focus takes 24px (SegmentedMenu `ActiveContainerShape`), the others 8px\n * (`InactiveContainerShape`), on the fast spatial spring;\n * - items are 44px minimum (4px less per web density step, `--md-sys-density`: 44 → 40 → 36 →\n * 32px; dividers' 8px margins lose 2px per step), 112–280px wide, 16px padding at\n * the start and end, body-large labels, 20px icons with a 12px gap and label-small trailing\n * text (the shortcut); 12px corners when selected (morph on the fast spatial spring). The\n * padding, gap and type follow the Menu token files (2026-10-14); Compose's code uses 12px,\n * an 8px gap and label-large on touch;\n * - item corners nest inside the container's: an edge item takes the outer corner minus\n * the 4px padding (16px → 12px, 8px → 4px), other corners are 4px. Compose picks item\n * shapes per group alone, so a last item could be rounder than its group's 8px corner;\n * - group labels start 16px inside the group's 4px padding, lined up with the items' 16px\n * padding, and end 4px from its edge (Compose starts them 12px from the edge; aligned\n * with the labels, 2026-10-15);\n * - standard: surface-container-low, on-surface text, on-surface-variant icons, selected\n * tertiary-container; vibrant: tertiary-container, on-tertiary-container content\n * (icons turn tertiary on hover, focus and press), selected tertiary;\n * - the menu is revealed from its anchored edge (a clip) as it fades in, on the fast springs\n * (M3 motion: components enter along the x or y axis; scale is avoided), as do the\n * exposed dropdown menus (Select, Autocomplete) and the docked date picker;\n * - the scrolling list keeps 8px of room around the groups so their shadows aren't clipped,\n * and shows a persistent scrollbar (M3).\n *\n * The container's colour, shadow and resting corners and the items' resting and selected\n * colours come from the menu's component tokens (`menu-tokens.ts`, `--md-comp-menus-*`):\n * the menu, each item and the option lists that share these styles carry\n * `data-tokens=\"md-comp-menus\"` and `data-variant`. `--m3-menu-corner` takes the shape\n * token, so the submenu morph still animates from it. A consumer class still wins in `cn()`.\n * Every class is written out in full so Tailwind can find it.\n */\n\n/**\n * M3: a menu that scrolls \"show[s] a persistent scrollbar\". Overlay scrollbars hide at rest,\n * so a styled WebKit scrollbar is always drawn (a 4px `outline` thumb in a 12px track);\n * elsewhere, a thin one. Shared by the menu and the exposed dropdown menus' option list.\n */\nexport const persistentScrollbar = [\n '[&::-webkit-scrollbar]:w-[12px] [&::-webkit-scrollbar-track]:bg-transparent',\n '[&::-webkit-scrollbar-thumb]:rounded-corner-full [&::-webkit-scrollbar-thumb]:border-[4px] [&::-webkit-scrollbar-thumb]:border-solid [&::-webkit-scrollbar-thumb]:border-transparent [&::-webkit-scrollbar-thumb]:bg-outline [&::-webkit-scrollbar-thumb]:bg-clip-padding',\n 'supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-width:thin] supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-color:var(--md-sys-color-outline)_transparent]',\n];\n\n/** Variant definitions for {@link Menu} and its parts. */\n/**\n * Keeps a menu's placed box at the menu's own width (`!`, so a consumer's `w-full` or `min-w-*`\n * doesn't merge it away): React Aria places the box by its size, so a full-width box was held\n * 12px from the window's edge and the menu inside it sat off its trigger (2026-10-17). Only the\n * menus take it: an `!important` minimum would also beat the inline minimum that keeps a\n * Select's or Autocomplete's option list as wide as its field.\n */\nexport const menuOwnWidth = 'w-max! min-w-0!';\n\nexport const menuStyles = tv({\n slots: {\n // The outermost element, which React Aria places (inline `position` and insets) and which\n // takes the consumer's `className` and `style`. A menu adds `menuOwnWidth` to it; the\n // option list of a Select or Autocomplete doesn't, since it sets its own minimum (the\n // field's width) inline.\n popover: 'z-(--md-sys-z-menu)',\n // The menu's enter and exit: revealed from its anchored edge with a clip (24px of room\n // for the shadows) as it fades in. M3 motion avoids scale, and a clip leaves the items'\n // layout untouched, so a submenu opened mid-animation is placed against its item's final\n // position. It also passes the popover's maximum height down, so a long list scrolls.\n // `w-max` keeps the menu's own width whatever width a consumer class gives the popover\n // (whose insets only React Aria sets: `data-overlay-placement`, styles/utilities.css).\n reveal: [\n 'flex max-h-[inherit] w-max flex-col opacity-100 [clip-path:inset(-24px)]',\n '[transition-property:opacity,clip-path]',\n '[transition-duration:var(--md-sys-motion-spring-effects-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing)]',\n // Closed until React Aria has placed it (`data-entering`, `useRevealEntering`), with no\n // transition, then it opens from there: a `@starting-style` was resolved before the\n // placement was known, so every menu opened from its top edge.\n 'data-entering:opacity-0 data-entering:[transition-property:none] data-exiting:opacity-0',\n // Leaving, it takes no presses (nor do its groups and items, which set their own):\n // an item clicked as it faded ran its action.\n 'data-exiting:pointer-events-none data-exiting:**:pointer-events-none',\n // Below the anchor it opens downwards, above it upwards, beside it sideways. Under\n // reduced motion it only fades: the clip stays open.\n 'motion-safe:data-entering:[clip-path:inset(-24px_-24px_100%_-24px)] motion-safe:data-exiting:[clip-path:inset(-24px_-24px_100%_-24px)]',\n 'motion-safe:data-[placement=top]:data-entering:[clip-path:inset(100%_-24px_-24px_-24px)] motion-safe:data-[placement=top]:data-exiting:[clip-path:inset(100%_-24px_-24px_-24px)]',\n 'motion-safe:data-[placement=right]:data-entering:[clip-path:inset(-24px_100%_-24px_-24px)] motion-safe:data-[placement=right]:data-exiting:[clip-path:inset(-24px_100%_-24px_-24px)]',\n 'motion-safe:data-[placement=left]:data-entering:[clip-path:inset(-24px_-24px_-24px_100%)] motion-safe:data-[placement=left]:data-exiting:[clip-path:inset(-24px_-24px_-24px_100%)]',\n ],\n // The list scrolls when tall, and a scroll container clips its children's shadows to\n // its box. 8px of padding (cancelled by a negative margin, so nothing moves) leaves room\n // for the groups' elevation-2 shadows; that ring lets clicks through, so a press just\n // outside the menu still dismisses it. Widths include the padding: 112–280px groups.\n menu: [\n 'flex max-h-[inherit] flex-col overflow-y-auto bg-[var(--_md-comp-menus-container-color)] outline-none',\n ...persistentScrollbar,\n '[--m3-menu-corner:var(--_md-comp-menus-container-shape)]',\n 'data-[shape=active]:[--m3-menu-corner:24px]',\n 'data-[shape=inactive]:[--m3-menu-corner:var(--md-sys-shape-corner-small)]',\n ],\n group: [\n 'pointer-events-auto flex flex-col bg-[var(--_md-comp-menus-container-color)] p-[4px] shadow-[shadow:var(--_md-comp-menus-container-elevation)]',\n // Forced colours drop the surface colour and the shadow: a system-colour outline keeps\n // the edge (an outline, so nothing moves).\n 'forced-colors:outline-solid forced-colors:outline-1 forced-colors:outline-[CanvasText]',\n '[transition-property:border-radius]',\n '[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing)]',\n ],\n // 8px above and below at density 0, 2px less per step (M3: density takes spacing off).\n divider: 'my-[calc(8px+var(--md-sys-density,0)*2px)] -mx-[4px] w-auto',\n // 16px inside the group's 4px padding, as the items' own 16px padding: in line with them.\n heading: 'flex min-h-[32px] items-center ps-[16px] text-label-large',\n item: [\n 'group/item flex min-h-[calc(44px+var(--md-sys-density,0)*4px)] w-full cursor-pointer items-center px-[16px] text-start',\n 'text-body-large text-[color:var(--_md-comp-menus-menu-item-label-text-color)] outline-none select-none',\n 'data-selected:bg-[var(--_md-comp-menus-menu-item-selected-container-color)] data-selected:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)]',\n 'state-layer focus-ring-inset container-motion',\n 'data-selected:rounded-corner-medium',\n 'data-disabled:cursor-default',\n ],\n selectedIcon: [\n 'grid grid-cols-[0fr] opacity-0',\n '[transition-property:grid-template-columns,opacity]',\n '[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration),var(--md-sys-motion-spring-effects-fast-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing),var(--md-sys-motion-spring-effects-fast-easing)]',\n 'group-data-selected/item:grid-cols-[1fr] group-data-selected/item:opacity-100',\n ],\n selectedIconInner: 'overflow-hidden',\n icon: 'me-[12px] inline-flex size-[20px] shrink-0 items-center justify-center text-[color:var(--_md-comp-menus-menu-item-leading-icon-color)] [&>svg]:size-full',\n text: 'flex min-w-0 flex-1 flex-col',\n description: 'text-body-medium',\n trailing:\n 'ms-auto inline-flex shrink-0 items-center ps-[12px] text-label-small text-[color:var(--_md-comp-menus-menu-item-trailing-icon-color)] [&>svg]:size-[20px]',\n badge: 'inline-flex shrink-0 items-center ps-[12px]',\n // A menu's place in a bottom sheet. An open submenu's panel follows the menu that opened\n // it (portalled in after it) and hides it, and any panel before it, while it's open.\n sheetContent: [\n 'flex min-h-0 flex-1 flex-col px-[8px] pb-[16px]',\n '[&>:not([data-overlay-scope]):has(~[data-overlay-scope])]:hidden',\n '[&>[data-overlay-scope]:has(~[data-overlay-scope])>*]:hidden',\n ],\n // A submenu in a sheet: a back button and its item's label above its list, fading in.\n sheetPanel: [\n 'flex min-h-0 flex-1 flex-col opacity-100 starting:opacity-0',\n '[transition-property:opacity]',\n '[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]',\n ],\n sheetHeader: 'flex min-h-[48px] shrink-0 items-center gap-[4px] pe-[12px]',\n sheetBack: [\n 'inline-flex size-[48px] shrink-0 cursor-pointer items-center justify-center rounded-corner-full',\n 'state-layer focus-ring text-on-surface-variant [&>svg]:size-[24px]',\n ],\n sheetTitle: 'min-w-0 flex-1 truncate text-title-medium text-on-surface',\n },\n variants: {\n variant: {\n standard: {\n heading: 'text-on-surface-variant',\n item: [\n 'data-disabled:text-on-surface/38',\n 'data-disabled:data-selected:bg-tertiary-container/38 data-disabled:data-selected:text-on-tertiary-container/38',\n ],\n icon: 'group-data-selected/item:text-on-tertiary-container group-data-disabled/item:text-on-surface/38',\n description:\n 'text-on-surface-variant group-data-selected/item:text-on-tertiary-container group-data-disabled/item:text-on-surface/38',\n trailing:\n 'group-data-selected/item:text-on-tertiary-container group-data-disabled/item:text-on-surface/38',\n },\n vibrant: {\n heading: 'text-on-tertiary-container',\n item: [\n 'data-disabled:text-on-tertiary-container/38',\n 'data-disabled:data-selected:bg-tertiary/38 data-disabled:data-selected:text-on-tertiary/38',\n ],\n icon: [\n 'group-data-hovered/item:text-tertiary group-data-focus-visible/item:text-tertiary group-data-pressed/item:text-tertiary',\n 'group-data-selected/item:text-on-tertiary',\n 'group-data-selected/item:group-data-hovered/item:text-on-tertiary',\n 'group-data-selected/item:group-data-focus-visible/item:text-on-tertiary',\n 'group-data-selected/item:group-data-pressed/item:text-on-tertiary',\n 'group-data-disabled/item:text-on-tertiary-container/38',\n ],\n description:\n 'text-on-tertiary-container group-data-selected/item:text-on-tertiary group-data-disabled/item:text-on-tertiary-container/38',\n trailing: [\n 'group-data-hovered/item:text-tertiary group-data-focus-visible/item:text-tertiary group-data-pressed/item:text-tertiary',\n 'group-data-selected/item:text-on-tertiary',\n 'group-data-selected/item:group-data-hovered/item:text-on-tertiary',\n 'group-data-selected/item:group-data-focus-visible/item:text-on-tertiary',\n 'group-data-selected/item:group-data-pressed/item:text-on-tertiary',\n 'group-data-disabled/item:text-on-tertiary-container/38',\n ],\n },\n },\n groupPosition: {\n only: { group: 'rounded-[var(--_md-comp-menus-container-shape)]' },\n first: { group: 'rounded-t-[var(--_md-comp-menus-container-shape)] rounded-b-corner-small' },\n middle: { group: 'rounded-corner-small' },\n last: { group: 'rounded-t-corner-small rounded-b-[var(--_md-comp-menus-container-shape)]' },\n },\n /**\n * The item's top corners: `nested` inside a 16px corner (12px), `outer` inside the\n * menu's morphing outer corner (`--m3-menu-corner` less the 4px padding), else 4px.\n */\n itemTop: {\n nested: { item: 'rounded-t-corner-medium' },\n outer: {\n item: 'rounded-t-[calc(var(--m3-menu-corner,var(--md-sys-shape-corner-large))-4px)]',\n },\n plain: { item: 'rounded-t-corner-extra-small' },\n },\n /** The item's bottom corners, as `itemTop`. */\n itemBottom: {\n nested: { item: 'rounded-b-corner-medium' },\n outer: {\n item: 'rounded-b-[calc(var(--m3-menu-corner,var(--md-sys-shape-corner-large))-4px)]',\n },\n plain: { item: 'rounded-b-corner-extra-small' },\n },\n /** Groups' outer corners follow `--m3-menu-corner` (the submenu morph). */\n morph: {\n true: {},\n false: {},\n },\n hasDescription: {\n true: { item: 'py-[calc(8px+var(--md-sys-density,0)*2px)]' },\n false: {},\n },\n /** Open and close with no motion (`instant`). */\n instant: {\n true: { reveal: '[transition-property:none]' },\n false: {},\n },\n /** In a bottom sheet: the list spans it, on the sheet's own surface. */\n embedded: {\n true: {},\n false: {},\n },\n /**\n * `gap`: every group is its own surface, and the list only scrolls (its 8px ring keeps\n * the groups' shadows); `divider`: the list is the one surface.\n */\n layout: {\n gap: {\n menu: [\n '-m-[8px] p-[8px] pointer-events-none gap-[2px] bg-transparent',\n 'min-w-[128px] max-w-[296px]',\n ],\n },\n divider: {\n menu: [\n 'pointer-events-auto min-w-[112px] max-w-[280px] p-[4px] shadow-[shadow:var(--_md-comp-menus-container-elevation)]',\n 'forced-colors:outline-solid forced-colors:outline-1 forced-colors:outline-[CanvasText]',\n 'rounded-[var(--m3-menu-corner,var(--md-sys-shape-corner-large))]',\n '[transition-property:border-radius]',\n '[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing)]',\n ],\n group: 'bg-transparent p-0 shadow-none forced-colors:outline-none',\n },\n },\n },\n compoundVariants: [\n {\n morph: true,\n groupPosition: 'only',\n class: { group: 'rounded-[var(--m3-menu-corner,var(--md-sys-shape-corner-large))]' },\n },\n {\n morph: true,\n groupPosition: 'first',\n class: {\n group:\n 'rounded-t-[var(--m3-menu-corner,var(--md-sys-shape-corner-large))] rounded-b-corner-small',\n },\n },\n {\n morph: true,\n groupPosition: 'last',\n class: {\n group:\n 'rounded-t-corner-small rounded-b-[var(--m3-menu-corner,var(--md-sys-shape-corner-large))]',\n },\n },\n // A divider layout's groups are plain sections of the one surface.\n { layout: 'divider', class: { group: 'rounded-none' } },\n // In a sheet the list has no surface, shadow or width limits of its own.\n {\n embedded: true,\n class: {\n menu: 'w-full max-w-none min-w-0 rounded-none bg-transparent p-0 shadow-none forced-colors:outline-none',\n },\n },\n ],\n defaultVariants: {\n variant: 'standard',\n layout: 'gap',\n morph: false,\n groupPosition: 'only',\n itemTop: 'plain',\n itemBottom: 'plain',\n hasDescription: false,\n instant: false,\n embedded: false,\n },\n});\n\n/**\n * The variants of {@link menuStyles}, for building on its styles. They are the recipe's keys, not\n * component props: their names (`hasIcon`, `isOpen`…) follow the classes and may change.\n */\nexport type MenuStyleProps = VariantProps<typeof menuStyles>;\nexport type MenuVariant = NonNullable<MenuStyleProps['variant']>;\nexport type MenuLayout = NonNullable<MenuStyleProps['layout']>;\n"],"mappings":";;;;;;;AAiDA,MAAa,sBAAsB;CACjC;CACA;CACA;AACF;;;;;;;;;AAUA,MAAa,eAAe;AAE5B,MAAa,aAAa,GAAG;CAC3B,OAAO;EAKL,SAAS;EAOT,QAAQ;GACN;GACA;GACA;GACA;GAIA;GAGA;GAGA;GACA;GACA;GACA;EACF;EAKA,MAAM;GACJ;GACA,GAAG;GACH;GACA;GACA;EACF;EACA,OAAO;GACL;GAGA;GACA;GACA;GACA;EACF;EAEA,SAAS;EAET,SAAS;EACT,MAAM;GACJ;GACA;GACA;GACA;GACA;GACA;EACF;EACA,cAAc;GACZ;GACA;GACA;GACA;GACA;EACF;EACA,mBAAmB;EACnB,MAAM;EACN,MAAM;EACN,aAAa;EACb,UACE;EACF,OAAO;EAGP,cAAc;GACZ;GACA;GACA;EACF;EAEA,YAAY;GACV;GACA;GACA;GACA;EACF;EACA,aAAa;EACb,WAAW,CACT,mGACA,oEACF;EACA,YAAY;CACd;CACA,UAAU;EACR,SAAS;GACP,UAAU;IACR,SAAS;IACT,MAAM,CACJ,oCACA,gHACF;IACA,MAAM;IACN,aACE;IACF,UACE;GACJ;GACA,SAAS;IACP,SAAS;IACT,MAAM,CACJ,+CACA,4FACF;IACA,MAAM;KACJ;KACA;KACA;KACA;KACA;KACA;IACF;IACA,aACE;IACF,UAAU;KACR;KACA;KACA;KACA;KACA;KACA;IACF;GACF;EACF;EACA,eAAe;GACb,MAAM,EAAE,OAAO,kDAAkD;GACjE,OAAO,EAAE,OAAO,2EAA2E;GAC3F,QAAQ,EAAE,OAAO,uBAAuB;GACxC,MAAM,EAAE,OAAO,2EAA2E;EAC5F;;;;;EAKA,SAAS;GACP,QAAQ,EAAE,MAAM,0BAA0B;GAC1C,OAAO,EACL,MAAM,+EACR;GACA,OAAO,EAAE,MAAM,+BAA+B;EAChD;;EAEA,YAAY;GACV,QAAQ,EAAE,MAAM,0BAA0B;GAC1C,OAAO,EACL,MAAM,+EACR;GACA,OAAO,EAAE,MAAM,+BAA+B;EAChD;;EAEA,OAAO;GACL,MAAM,CAAC;GACP,OAAO,CAAC;EACV;EACA,gBAAgB;GACd,MAAM,EAAE,MAAM,6CAA6C;GAC3D,OAAO,CAAC;EACV;;EAEA,SAAS;GACP,MAAM,EAAE,QAAQ,6BAA6B;GAC7C,OAAO,CAAC;EACV;;EAEA,UAAU;GACR,MAAM,CAAC;GACP,OAAO,CAAC;EACV;;;;;EAKA,QAAQ;GACN,KAAK,EACH,MAAM,CACJ,iEACA,6BACF,EACF;GACA,SAAS;IACP,MAAM;KACJ;KACA;KACA;KACA;KACA;KACA;IACF;IACA,OAAO;GACT;EACF;CACF;CACA,kBAAkB;EAChB;GACE,OAAO;GACP,eAAe;GACf,OAAO,EAAE,OAAO,mEAAmE;EACrF;EACA;GACE,OAAO;GACP,eAAe;GACf,OAAO,EACL,OACE,4FACJ;EACF;EACA;GACE,OAAO;GACP,eAAe;GACf,OAAO,EACL,OACE,4FACJ;EACF;EAEA;GAAE,QAAQ;GAAW,OAAO,EAAE,OAAO,eAAe;EAAE;EAEtD;GACE,UAAU;GACV,OAAO,EACL,MAAM,mGACR;EACF;CACF;CACA,iBAAiB;EACf,SAAS;EACT,QAAQ;EACR,OAAO;EACP,eAAe;EACf,SAAS;EACT,YAAY;EACZ,gBAAgB;EAChB,SAAS;EACT,UAAU;CACZ;AACF,CAAC"}
|
|
1
|
+
{"version":3,"file":"menu-styles.js","names":[],"sources":["../../../src/components/menu/menu-styles.ts"],"sourcesContent":["import { tv, type VariantProps } from '../../utils/tv';\n\n/*\n * Values follow the M3 Expressive menu spec and Compose Material 3's Expressive menu\n * (Menu.kt, MenuDefaults.kt and the Menu / StandardMenu / VibrantMenu / SegmentedMenu\n * token files):\n * - two layouts (M3 \"Vertical menu with gap\" / \"with divider\"). `gap`: groups are separate\n * surfaces 2px apart at elevation 2, with 4px padding (the SegmentedMenu `GroupPadding`\n * token); corners: only 16px; first 16px top / 8px bottom; middle 8px; last 8px top /\n * 16px bottom. `divider`: one surface with 4px padding and 16px corners, groups split by\n * a 1px `outline-variant` divider with 8px above and below (the baseline menu's divider\n * padding). M3 recommends dividers on the web and in scrolling menus;\n * - the outer corners read `--m3-menu-corner`, so they morph while a submenu is open: the\n * menu holding focus takes 24px (SegmentedMenu `ActiveContainerShape`), the others 8px\n * (`InactiveContainerShape`), on the fast spatial spring;\n * - items are 44px minimum (4px less per web density step, `--md-sys-density`: 44 → 40 → 36 →\n * 32px; dividers' 8px margins lose 2px per step), 112–280px wide, 16px padding at\n * the start and end, body-large labels, 20px icons with a 12px gap and label-small trailing\n * text (the shortcut); 12px corners when selected (morph on the fast spatial spring). The\n * padding, gap and type follow the Menu token files (2026-10-14); Compose's code uses 12px,\n * an 8px gap and label-large on touch;\n * - item corners nest inside the container's: an edge item takes the outer corner minus\n * the 4px padding (16px → 12px, 8px → 4px), other corners are 4px. Compose picks item\n * shapes per group alone, so a last item could be rounder than its group's 8px corner;\n * - group labels start 16px inside the group's 4px padding, lined up with the items' 16px\n * padding, and end 4px from its edge (Compose starts them 12px from the edge; aligned\n * with the labels, 2026-10-15);\n * - standard: surface-container-low, on-surface text, on-surface-variant icons, selected\n * tertiary-container; vibrant: tertiary-container, on-tertiary-container content\n * (icons turn tertiary on hover, focus and press), selected tertiary;\n * - the menu is revealed from its anchored edge (a clip) as it fades in, on the fast springs\n * (M3 motion: components enter along the x or y axis; scale is avoided), as do the\n * exposed dropdown menus (Select, Autocomplete) and the docked date picker;\n * - the scrolling list keeps 8px of room around the groups so their shadows aren't clipped,\n * and shows a persistent scrollbar (M3).\n *\n * The container's colour, shadow and resting corners and the items' resting and selected\n * colours come from the menu's component tokens (`menu-tokens.ts`, `--md-comp-menus-*`):\n * the menu, each item and the option lists that share these styles carry\n * `data-tokens=\"md-comp-menus\"` and `data-variant`. `--m3-menu-corner` takes the shape\n * token, so the submenu morph still animates from it. A consumer class still wins in `cn()`.\n * Every class is written out in full so Tailwind can find it.\n */\n\n/**\n * M3: a menu that scrolls \"show[s] a persistent scrollbar\". Overlay scrollbars hide at rest,\n * so a styled WebKit scrollbar is always drawn (a 4px `outline` thumb in a 12px track);\n * elsewhere, a thin one. Shared by the menu and the exposed dropdown menus' option list.\n */\nexport const persistentScrollbar = [\n '[&::-webkit-scrollbar]:w-[12px] [&::-webkit-scrollbar-track]:bg-transparent',\n '[&::-webkit-scrollbar-thumb]:rounded-corner-full [&::-webkit-scrollbar-thumb]:border-[4px] [&::-webkit-scrollbar-thumb]:border-solid [&::-webkit-scrollbar-thumb]:border-transparent [&::-webkit-scrollbar-thumb]:bg-outline [&::-webkit-scrollbar-thumb]:bg-clip-padding',\n 'supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-width:thin] supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-color:var(--md-sys-color-outline)_transparent]',\n];\n\n/** Variant definitions for {@link Menu} and its parts. */\n/**\n * Keeps a menu's placed box at the menu's own width (`!`, so a consumer's `w-full` or `min-w-*`\n * doesn't merge it away): React Aria places the box by its size, so a full-width box was held\n * 12px from the window's edge and the menu inside it sat off its trigger (2026-10-17). Only the\n * menus take it: an `!important` minimum would also beat the inline minimum that keeps a\n * Select's or Autocomplete's option list as wide as its field.\n */\nexport const menuOwnWidth = 'w-max! min-w-0!';\n\nexport const menuStyles = tv({\n slots: {\n // The outermost element, which React Aria places (inline `position` and insets) and which\n // takes the consumer's `className` and `style`. A menu adds `menuOwnWidth` to it; the\n // option list of a Select or Autocomplete doesn't, since it sets its own minimum (the\n // field's width) inline.\n popover: 'z-(--md-sys-z-menu)',\n // The menu's enter and exit: revealed from its anchored edge with a clip (24px of room\n // for the shadows) as it fades in. M3 motion avoids scale, and a clip leaves the items'\n // layout untouched, so a submenu opened mid-animation is placed against its item's final\n // position. It also passes the popover's maximum height down, so a long list scrolls.\n // `w-max` keeps the menu's own width whatever width a consumer class gives the popover\n // (whose insets only React Aria sets: `data-overlay-placement`, styles/utilities.css).\n reveal: [\n 'flex max-h-[inherit] w-max flex-col opacity-100 [clip-path:inset(-24px)]',\n '[transition-property:opacity,clip-path]',\n '[transition-duration:var(--md-sys-motion-spring-effects-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing)]',\n // Closed until React Aria has placed it (`data-entering`, `useRevealEntering`), with no\n // transition, then it opens from there: a `@starting-style` was resolved before the\n // placement was known, so every menu opened from its top edge.\n 'data-entering:opacity-0 data-entering:[transition-property:none] data-exiting:opacity-0',\n // Leaving, it takes no presses (nor do its groups and items, which set their own):\n // an item clicked as it faded ran its action.\n 'data-exiting:pointer-events-none data-exiting:**:pointer-events-none',\n // Below the anchor it opens downwards, above it upwards, beside it sideways. Under\n // reduced motion it only fades: the clip stays open.\n 'motion-safe:data-entering:[clip-path:inset(-24px_-24px_100%_-24px)] motion-safe:data-exiting:[clip-path:inset(-24px_-24px_100%_-24px)]',\n 'motion-safe:data-[placement=top]:data-entering:[clip-path:inset(100%_-24px_-24px_-24px)] motion-safe:data-[placement=top]:data-exiting:[clip-path:inset(100%_-24px_-24px_-24px)]',\n 'motion-safe:data-[placement=right]:data-entering:[clip-path:inset(-24px_100%_-24px_-24px)] motion-safe:data-[placement=right]:data-exiting:[clip-path:inset(-24px_100%_-24px_-24px)]',\n 'motion-safe:data-[placement=left]:data-entering:[clip-path:inset(-24px_-24px_-24px_100%)] motion-safe:data-[placement=left]:data-exiting:[clip-path:inset(-24px_-24px_-24px_100%)]',\n ],\n // The list scrolls when tall, and a scroll container clips its children's shadows to\n // its box. 8px of padding (cancelled by a negative margin, so nothing moves) leaves room\n // for the groups' elevation-2 shadows; that ring lets clicks through, so a press just\n // outside the menu still dismisses it. Widths include the padding: 112–280px groups.\n menu: [\n 'flex max-h-[inherit] flex-col overflow-y-auto bg-[var(--_md-comp-menus-container-color)] outline-none',\n ...persistentScrollbar,\n '[--m3-menu-corner:var(--_md-comp-menus-container-shape)]',\n 'data-[shape=active]:[--m3-menu-corner:24px]',\n 'data-[shape=inactive]:[--m3-menu-corner:var(--md-sys-shape-corner-small)]',\n ],\n group: [\n 'pointer-events-auto flex flex-col bg-[var(--_md-comp-menus-container-color)] p-[4px] shadow-[shadow:var(--_md-comp-menus-container-elevation)]',\n // Forced colours drop the surface colour and the shadow: a system-colour outline keeps\n // the edge (an outline, so nothing moves).\n 'forced-colors:outline-solid forced-colors:outline-1 forced-colors:outline-[CanvasText]',\n '[transition-property:border-radius]',\n '[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing)]',\n ],\n // 8px above and below at density 0, 2px less per step (M3: density takes spacing off).\n divider: 'my-[calc(8px+var(--md-sys-density,0)*2px)] -mx-[4px] w-auto',\n // 16px inside the group's 4px padding, as the items' own 16px padding: in line with them.\n heading: 'flex min-h-[32px] items-center ps-[16px] text-label-large',\n item: [\n 'group/item flex min-h-[calc(44px+var(--md-sys-density,0)*4px)] w-full cursor-pointer items-center px-[16px] text-start',\n 'text-body-large text-[color:var(--_md-comp-menus-menu-item-label-text-color)] outline-none select-none',\n 'data-selected:bg-[var(--_md-comp-menus-menu-item-selected-container-color)] data-selected:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)]',\n 'state-layer focus-ring-inset container-motion',\n 'data-selected:rounded-corner-medium',\n 'data-disabled:cursor-default',\n ],\n selectedIcon: [\n 'grid grid-cols-[0fr] opacity-0',\n '[transition-property:grid-template-columns,opacity]',\n '[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration),var(--md-sys-motion-spring-effects-fast-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing),var(--md-sys-motion-spring-effects-fast-easing)]',\n 'group-data-selected/item:grid-cols-[1fr] group-data-selected/item:opacity-100',\n ],\n selectedIconInner: 'overflow-hidden',\n icon: 'me-[12px] inline-flex size-[20px] shrink-0 items-center justify-center text-[color:var(--_md-comp-menus-menu-item-leading-icon-color)] [&>svg]:size-full',\n text: 'flex min-w-0 flex-1 flex-col',\n description: 'text-body-medium',\n trailing:\n 'ms-auto inline-flex shrink-0 items-center ps-[12px] text-label-small text-[color:var(--_md-comp-menus-menu-item-trailing-icon-color)] [&>svg]:size-[20px]',\n badge: 'inline-flex shrink-0 items-center ps-[12px]',\n // A menu's place in a bottom sheet. An open submenu's panel follows the menu that opened\n // it (portalled in after it) and hides it, and any panel before it, while it's open.\n sheetContent: [\n 'flex min-h-0 flex-1 flex-col px-[8px] pb-[16px]',\n '[&>:not([data-overlay-scope]):has(~[data-overlay-scope])]:hidden',\n '[&>[data-overlay-scope]:has(~[data-overlay-scope])>*]:hidden',\n ],\n // A submenu in a sheet: a back button and its item's label above its list, fading in.\n sheetPanel: [\n 'flex min-h-0 flex-1 flex-col opacity-100 starting:opacity-0',\n '[transition-property:opacity]',\n '[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]',\n ],\n sheetHeader: 'flex min-h-[48px] shrink-0 items-center gap-[4px] pe-[12px]',\n sheetBack: [\n 'inline-flex size-[48px] shrink-0 cursor-pointer items-center justify-center rounded-corner-full',\n 'state-layer focus-ring text-on-surface-variant [&>svg]:size-[24px]',\n ],\n sheetTitle: 'min-w-0 flex-1 truncate text-title-medium text-on-surface',\n },\n variants: {\n variant: {\n // A selected item's tick, icons and description follow its label's token, so an app\n // that sets the selected colours gets every part in them (2026-11-11; they were a fixed\n // role, 1.08:1 against another container in dark mode).\n standard: {\n heading: 'text-on-surface-variant',\n item: [\n 'data-disabled:text-on-surface/38',\n 'data-disabled:data-selected:bg-tertiary-container/38 data-disabled:data-selected:text-on-tertiary-container/38',\n ],\n icon: 'group-data-selected/item:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)] group-data-disabled/item:text-on-surface/38',\n description:\n 'text-on-surface-variant group-data-selected/item:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)] group-data-disabled/item:text-on-surface/38',\n trailing:\n 'group-data-selected/item:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)] group-data-disabled/item:text-on-surface/38',\n },\n vibrant: {\n heading: 'text-on-tertiary-container',\n item: [\n 'data-disabled:text-on-tertiary-container/38',\n 'data-disabled:data-selected:bg-tertiary/38 data-disabled:data-selected:text-on-tertiary/38',\n ],\n icon: [\n 'group-data-hovered/item:text-tertiary group-data-focus-visible/item:text-tertiary group-data-pressed/item:text-tertiary',\n 'group-data-selected/item:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)]',\n 'group-data-selected/item:group-data-hovered/item:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)]',\n 'group-data-selected/item:group-data-focus-visible/item:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)]',\n 'group-data-selected/item:group-data-pressed/item:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)]',\n 'group-data-disabled/item:text-on-tertiary-container/38',\n ],\n description:\n 'text-on-tertiary-container group-data-selected/item:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)] group-data-disabled/item:text-on-tertiary-container/38',\n trailing: [\n 'group-data-hovered/item:text-tertiary group-data-focus-visible/item:text-tertiary group-data-pressed/item:text-tertiary',\n 'group-data-selected/item:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)]',\n 'group-data-selected/item:group-data-hovered/item:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)]',\n 'group-data-selected/item:group-data-focus-visible/item:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)]',\n 'group-data-selected/item:group-data-pressed/item:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)]',\n 'group-data-disabled/item:text-on-tertiary-container/38',\n ],\n },\n },\n groupPosition: {\n only: { group: 'rounded-[var(--_md-comp-menus-container-shape)]' },\n first: { group: 'rounded-t-[var(--_md-comp-menus-container-shape)] rounded-b-corner-small' },\n middle: { group: 'rounded-corner-small' },\n last: { group: 'rounded-t-corner-small rounded-b-[var(--_md-comp-menus-container-shape)]' },\n },\n /**\n * The item's top corners: `nested` inside a 16px corner (12px), `outer` inside the\n * menu's morphing outer corner (`--m3-menu-corner` less the 4px padding), else 4px.\n */\n itemTop: {\n nested: { item: 'rounded-t-corner-medium' },\n outer: {\n item: 'rounded-t-[calc(var(--m3-menu-corner,var(--md-sys-shape-corner-large))-4px)]',\n },\n plain: { item: 'rounded-t-corner-extra-small' },\n },\n /** The item's bottom corners, as `itemTop`. */\n itemBottom: {\n nested: { item: 'rounded-b-corner-medium' },\n outer: {\n item: 'rounded-b-[calc(var(--m3-menu-corner,var(--md-sys-shape-corner-large))-4px)]',\n },\n plain: { item: 'rounded-b-corner-extra-small' },\n },\n /** Groups' outer corners follow `--m3-menu-corner` (the submenu morph). */\n morph: {\n true: {},\n false: {},\n },\n hasDescription: {\n true: { item: 'py-[calc(8px+var(--md-sys-density,0)*2px)]' },\n false: {},\n },\n /** Open and close with no motion (`instant`). */\n instant: {\n true: { reveal: '[transition-property:none]' },\n false: {},\n },\n /** In a bottom sheet: the list spans it, on the sheet's own surface. */\n embedded: {\n true: {},\n false: {},\n },\n /**\n * `gap`: every group is its own surface, and the list only scrolls (its 8px ring keeps\n * the groups' shadows); `divider`: the list is the one surface.\n */\n layout: {\n gap: {\n menu: [\n '-m-[8px] p-[8px] pointer-events-none gap-[2px] bg-transparent',\n 'min-w-[128px] max-w-[296px]',\n ],\n },\n divider: {\n menu: [\n 'pointer-events-auto min-w-[112px] max-w-[280px] p-[4px] shadow-[shadow:var(--_md-comp-menus-container-elevation)]',\n 'forced-colors:outline-solid forced-colors:outline-1 forced-colors:outline-[CanvasText]',\n 'rounded-[var(--m3-menu-corner,var(--md-sys-shape-corner-large))]',\n '[transition-property:border-radius]',\n '[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing)]',\n ],\n group: 'bg-transparent p-0 shadow-none forced-colors:outline-none',\n },\n },\n },\n compoundVariants: [\n {\n morph: true,\n groupPosition: 'only',\n class: { group: 'rounded-[var(--m3-menu-corner,var(--md-sys-shape-corner-large))]' },\n },\n {\n morph: true,\n groupPosition: 'first',\n class: {\n group:\n 'rounded-t-[var(--m3-menu-corner,var(--md-sys-shape-corner-large))] rounded-b-corner-small',\n },\n },\n {\n morph: true,\n groupPosition: 'last',\n class: {\n group:\n 'rounded-t-corner-small rounded-b-[var(--m3-menu-corner,var(--md-sys-shape-corner-large))]',\n },\n },\n // A divider layout's groups are plain sections of the one surface.\n { layout: 'divider', class: { group: 'rounded-none' } },\n // In a sheet the list has no surface, shadow or width limits of its own.\n {\n embedded: true,\n class: {\n menu: 'w-full max-w-none min-w-0 rounded-none bg-transparent p-0 shadow-none forced-colors:outline-none',\n },\n },\n ],\n defaultVariants: {\n variant: 'standard',\n layout: 'gap',\n morph: false,\n groupPosition: 'only',\n itemTop: 'plain',\n itemBottom: 'plain',\n hasDescription: false,\n instant: false,\n embedded: false,\n },\n});\n\n/**\n * The variants of {@link menuStyles}, for building on its styles. They are the recipe's keys, not\n * component props: their names (`hasIcon`, `isOpen`…) follow the classes and may change.\n */\nexport type MenuStyleProps = VariantProps<typeof menuStyles>;\nexport type MenuVariant = NonNullable<MenuStyleProps['variant']>;\nexport type MenuLayout = NonNullable<MenuStyleProps['layout']>;\n"],"mappings":";;;;;;;AAiDA,MAAa,sBAAsB;CACjC;CACA;CACA;AACF;;;;;;;;;AAUA,MAAa,eAAe;AAE5B,MAAa,aAAa,GAAG;CAC3B,OAAO;EAKL,SAAS;EAOT,QAAQ;GACN;GACA;GACA;GACA;GAIA;GAGA;GAGA;GACA;GACA;GACA;EACF;EAKA,MAAM;GACJ;GACA,GAAG;GACH;GACA;GACA;EACF;EACA,OAAO;GACL;GAGA;GACA;GACA;GACA;EACF;EAEA,SAAS;EAET,SAAS;EACT,MAAM;GACJ;GACA;GACA;GACA;GACA;GACA;EACF;EACA,cAAc;GACZ;GACA;GACA;GACA;GACA;EACF;EACA,mBAAmB;EACnB,MAAM;EACN,MAAM;EACN,aAAa;EACb,UACE;EACF,OAAO;EAGP,cAAc;GACZ;GACA;GACA;EACF;EAEA,YAAY;GACV;GACA;GACA;GACA;EACF;EACA,aAAa;EACb,WAAW,CACT,mGACA,oEACF;EACA,YAAY;CACd;CACA,UAAU;EACR,SAAS;GAIP,UAAU;IACR,SAAS;IACT,MAAM,CACJ,oCACA,gHACF;IACA,MAAM;IACN,aACE;IACF,UACE;GACJ;GACA,SAAS;IACP,SAAS;IACT,MAAM,CACJ,+CACA,4FACF;IACA,MAAM;KACJ;KACA;KACA;KACA;KACA;KACA;IACF;IACA,aACE;IACF,UAAU;KACR;KACA;KACA;KACA;KACA;KACA;IACF;GACF;EACF;EACA,eAAe;GACb,MAAM,EAAE,OAAO,kDAAkD;GACjE,OAAO,EAAE,OAAO,2EAA2E;GAC3F,QAAQ,EAAE,OAAO,uBAAuB;GACxC,MAAM,EAAE,OAAO,2EAA2E;EAC5F;;;;;EAKA,SAAS;GACP,QAAQ,EAAE,MAAM,0BAA0B;GAC1C,OAAO,EACL,MAAM,+EACR;GACA,OAAO,EAAE,MAAM,+BAA+B;EAChD;;EAEA,YAAY;GACV,QAAQ,EAAE,MAAM,0BAA0B;GAC1C,OAAO,EACL,MAAM,+EACR;GACA,OAAO,EAAE,MAAM,+BAA+B;EAChD;;EAEA,OAAO;GACL,MAAM,CAAC;GACP,OAAO,CAAC;EACV;EACA,gBAAgB;GACd,MAAM,EAAE,MAAM,6CAA6C;GAC3D,OAAO,CAAC;EACV;;EAEA,SAAS;GACP,MAAM,EAAE,QAAQ,6BAA6B;GAC7C,OAAO,CAAC;EACV;;EAEA,UAAU;GACR,MAAM,CAAC;GACP,OAAO,CAAC;EACV;;;;;EAKA,QAAQ;GACN,KAAK,EACH,MAAM,CACJ,iEACA,6BACF,EACF;GACA,SAAS;IACP,MAAM;KACJ;KACA;KACA;KACA;KACA;KACA;IACF;IACA,OAAO;GACT;EACF;CACF;CACA,kBAAkB;EAChB;GACE,OAAO;GACP,eAAe;GACf,OAAO,EAAE,OAAO,mEAAmE;EACrF;EACA;GACE,OAAO;GACP,eAAe;GACf,OAAO,EACL,OACE,4FACJ;EACF;EACA;GACE,OAAO;GACP,eAAe;GACf,OAAO,EACL,OACE,4FACJ;EACF;EAEA;GAAE,QAAQ;GAAW,OAAO,EAAE,OAAO,eAAe;EAAE;EAEtD;GACE,UAAU;GACV,OAAO,EACL,MAAM,mGACR;EACF;CACF;CACA,iBAAiB;EACf,SAAS;EACT,QAAQ;EACR,OAAO;EACP,eAAe;EACf,SAAS;EACT,YAAY;EACZ,gBAAgB;EAChB,SAAS;EACT,UAAU;CACZ;AACF,CAAC"}
|
|
@@ -2,6 +2,6 @@ import { ComponentTokenSet } from "../../tokens/component.js";
|
|
|
2
2
|
import "../../index.js";
|
|
3
3
|
//#region src/components/menu/menu-tokens.d.ts
|
|
4
4
|
/** The tokens of {@link Menu}: `--md-comp-menus-<part>` and `--md-comp-menus-standard-<part>`. */
|
|
5
|
-
export declare const menuTokenSet: ComponentTokenSet<"container-
|
|
5
|
+
export declare const menuTokenSet: ComponentTokenSet<"container-elevation" | "container-shape">;
|
|
6
6
|
//#endregion
|
|
7
7
|
//# sourceMappingURL=menu-tokens.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menu-tokens.js","names":[],"sources":["../../../src/components/menu/menu-tokens.ts"],"sourcesContent":["import { color, corner, defineComponentTokens, elevation } from '../../tokens/component';\n\n/*\n * The menu's component tokens, M3 Expressive's `md.comp.menus.*` (common) and\n * `md.comp.menus.standard.*` names (checked against m3.material.io's token tables,\n * 2026-10-14; `md.comp.menu.*` is the baseline set), with the defaults of `menu-styles.ts`.\n *\n * The menu, its items, and Select's and Autocomplete's option lists (which share the menu's\n * styles) carry `data-tokens=\"md-comp-menus\"` and `data-variant`, so an item's own state\n * (`data-selected`) picks its colours. The vibrant menu's colours have no public names yet\n * (fixed below). The container shape is the menu's outer corner at rest: the submenu morph's\n * active and inactive corners, hover, focus, press
|
|
1
|
+
{"version":3,"file":"menu-tokens.js","names":[],"sources":["../../../src/components/menu/menu-tokens.ts"],"sourcesContent":["import { color, corner, defineComponentTokens, elevation } from '../../tokens/component';\n\n/*\n * The menu's component tokens, M3 Expressive's `md.comp.menus.*` (common) and\n * `md.comp.menus.standard.*` names (checked against m3.material.io's token tables,\n * 2026-10-14; `md.comp.menu.*` is the baseline set), with the defaults of `menu-styles.ts`.\n *\n * The menu, its items, and Select's and Autocomplete's option lists (which share the menu's\n * styles) carry `data-tokens=\"md-comp-menus\"` and `data-variant`, so an item's own state\n * (`data-selected`) picks its colours. The vibrant menu's colours have no public names yet\n * (fixed below). The container shape is the menu's outer corner at rest: the submenu morph's\n * active and inactive corners, hover, focus, press and disabled colours stay classes; a\n * selected item's tick, icons and description read its label's token.\n */\n\n/** The tokens of {@link Menu}: `--md-comp-menus-<part>` and `--md-comp-menus-standard-<part>`. */\nexport const menuTokenSet = defineComponentTokens({\n name: 'md-comp-menus',\n tokens: {\n 'container-elevation': {\n value: elevation(2),\n description: \"The menu's shadow (each group's, in the gap layout).\",\n compose: 'MenuTokens.ContainerElevation',\n paints: { property: 'box-shadow' },\n },\n 'container-shape': {\n value: corner('large'),\n description: \"The menu's outer corners at rest.\",\n compose: 'MenuTokens.ContainerShape',\n paints: { property: 'border-top-left-radius' },\n },\n },\n scopes: {\n variant: {\n standard: {\n prefix: 'md-comp-menus-standard',\n tokens: {\n 'container-color': {\n value: color('surface-container-low'),\n description: 'The menu (each group, in the gap layout).',\n compose: 'StandardMenuTokens.ContainerColor',\n paints: { property: 'background-color' },\n },\n 'menu-item-label-text-color': {\n value: color('on-surface'),\n description: 'An item’s label.',\n compose: 'StandardMenuTokens.ItemLabelTextColor',\n paints: { property: 'color', slot: 'item' },\n },\n 'menu-item-leading-icon-color': {\n value: color('on-surface-variant'),\n description: 'An item’s leading icon.',\n compose: 'StandardMenuTokens.ItemLeadingIconColor',\n paints: { property: 'color', slot: 'leading-icon' },\n },\n 'menu-item-trailing-icon-color': {\n value: color('on-surface-variant'),\n description: 'An item’s trailing icon, keyboard shortcut or trailing text.',\n compose: 'StandardMenuTokens.ItemTrailingIconColor',\n paints: { property: 'color', slot: 'trailing-icon' },\n },\n 'menu-item-selected-container-color': {\n value: color('tertiary-container'),\n description: 'A selected item.',\n compose: 'StandardMenuTokens.ItemSelectedContainerColor',\n paints: { property: 'background-color', state: 'selected' },\n },\n 'menu-item-selected-label-text-color': {\n value: color('on-tertiary-container'),\n description: 'A selected item’s label.',\n compose: 'StandardMenuTokens.ItemSelectedLabelTextColor',\n paints: { property: 'color', state: 'selected' },\n },\n },\n },\n vibrant: {\n prefix: 'md-comp-menus-vibrant',\n fixed: {\n 'container-color': color('tertiary-container'),\n 'menu-item-label-text-color': color('on-tertiary-container'),\n 'menu-item-leading-icon-color': color('on-tertiary-container'),\n 'menu-item-trailing-icon-color': color('on-tertiary-container'),\n 'menu-item-selected-container-color': color('tertiary'),\n 'menu-item-selected-label-text-color': color('on-tertiary'),\n },\n },\n },\n },\n});\n"],"mappings":";;;AAgBA,MAAa,eAAe,sBAAsB;CAChD,MAAM;CACN,QAAQ;EACN,uBAAuB;GACrB,OAAO,UAAU,CAAC;GAClB,aAAa;GACb,SAAS;GACT,QAAQ,EAAE,UAAU,aAAa;EACnC;EACA,mBAAmB;GACjB,OAAO,OAAO,OAAO;GACrB,aAAa;GACb,SAAS;GACT,QAAQ,EAAE,UAAU,yBAAyB;EAC/C;CACF;CACA,QAAQ,EACN,SAAS;EACP,UAAU;GACR,QAAQ;GACR,QAAQ;IACN,mBAAmB;KACjB,OAAO,MAAM,uBAAuB;KACpC,aAAa;KACb,SAAS;KACT,QAAQ,EAAE,UAAU,mBAAmB;IACzC;IACA,8BAA8B;KAC5B,OAAO,MAAM,YAAY;KACzB,aAAa;KACb,SAAS;KACT,QAAQ;MAAE,UAAU;MAAS,MAAM;KAAO;IAC5C;IACA,gCAAgC;KAC9B,OAAO,MAAM,oBAAoB;KACjC,aAAa;KACb,SAAS;KACT,QAAQ;MAAE,UAAU;MAAS,MAAM;KAAe;IACpD;IACA,iCAAiC;KAC/B,OAAO,MAAM,oBAAoB;KACjC,aAAa;KACb,SAAS;KACT,QAAQ;MAAE,UAAU;MAAS,MAAM;KAAgB;IACrD;IACA,sCAAsC;KACpC,OAAO,MAAM,oBAAoB;KACjC,aAAa;KACb,SAAS;KACT,QAAQ;MAAE,UAAU;MAAoB,OAAO;KAAW;IAC5D;IACA,uCAAuC;KACrC,OAAO,MAAM,uBAAuB;KACpC,aAAa;KACb,SAAS;KACT,QAAQ;MAAE,UAAU;MAAS,OAAO;KAAW;IACjD;GACF;EACF;EACA,SAAS;GACP,QAAQ;GACR,OAAO;IACL,mBAAmB,MAAM,oBAAoB;IAC7C,8BAA8B,MAAM,uBAAuB;IAC3D,gCAAgC,MAAM,uBAAuB;IAC7D,iCAAiC,MAAM,uBAAuB;IAC9D,sCAAsC,MAAM,UAAU;IACtD,uCAAuC,MAAM,aAAa;GAC5D;EACF;CACF,EACF;AACF,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ButtonBaseActionProps, ButtonBaseLinkProps } from "../../primitives/ButtonBase.js";
|
|
2
1
|
import { BadgeProps } from "../badge/Badge.js";
|
|
2
|
+
import { ButtonBaseActionProps, ButtonBaseLinkProps } from "../../primitives/ButtonBase.js";
|
|
3
3
|
import "./nav-item-styles.js";
|
|
4
4
|
import { ReactElement, ReactNode, RefObject } from "react";
|
|
5
5
|
//#region src/components/navigation/NavItem.d.ts
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../../utils/cn.js";
|
|
3
|
+
import { useLabels } from "../labels/use-labels.js";
|
|
3
4
|
import { useM3Spring } from "../../motion/use-m3-spring.js";
|
|
4
5
|
import { setRippleOrigin, useRippleHold } from "../../primitives/use-m3-interaction.js";
|
|
5
6
|
import { ButtonBase } from "../../primitives/ButtonBase.js";
|
|
6
7
|
import { mergeDataTokens } from "../../utils/merge-data-tokens.js";
|
|
7
|
-
import { BadgedBox } from "../badge/Badge.js";
|
|
8
|
+
import { BadgedBox, DEFAULT_BADGE_LABELS } from "../badge/Badge.js";
|
|
8
9
|
import { navItemStyles } from "./nav-item-styles.js";
|
|
9
10
|
import { isValidElement, useId, useLayoutEffect, useRef, useState } from "react";
|
|
10
11
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -57,14 +58,15 @@ function useSlidingIndicator(selected, lastIndicatorRef) {
|
|
|
57
58
|
}
|
|
58
59
|
/**
|
|
59
60
|
* What a badge tells assistive tech about its item: its `label`, its text, or, for a small
|
|
60
|
-
* badge (no children),
|
|
61
|
+
* badge (no children), `newNotification`: the badge's own word, a `LabelsProvider`'s, or
|
|
62
|
+
* "New notification".
|
|
61
63
|
*/
|
|
62
|
-
function badgeDescription(badge) {
|
|
64
|
+
function badgeDescription(badge, newNotification) {
|
|
63
65
|
if (!isValidElement(badge)) return null;
|
|
64
|
-
const { label,
|
|
66
|
+
const { label, children } = badge.props;
|
|
65
67
|
if (label != null) return label;
|
|
66
68
|
if (typeof children === "string" || typeof children === "number") return String(children);
|
|
67
|
-
if (children == null || children === false) return
|
|
69
|
+
if (children == null || children === false) return newNotification;
|
|
68
70
|
return null;
|
|
69
71
|
}
|
|
70
72
|
/** Shared navigation item: a link (`href`) or button with the M3 pill indicator. */
|
|
@@ -78,7 +80,8 @@ function NavItem({ layout, enterLabel, lastIndicatorRef, icon, selectedIcon, chi
|
|
|
78
80
|
const descriptionId = useId();
|
|
79
81
|
const inline = layout === "rail-start" || layout === "bar-start";
|
|
80
82
|
const badgeAfterLabel = layout === "rail-start";
|
|
81
|
-
const
|
|
83
|
+
const { newNotification } = useLabels("Badge", DEFAULT_BADGE_LABELS, isValidElement(badge) ? badge.props.labels : void 0);
|
|
84
|
+
const description = badgeLabel ?? badgeDescription(badge, newNotification);
|
|
82
85
|
const label = /* @__PURE__ */ jsx("span", {
|
|
83
86
|
"data-nav-label": "",
|
|
84
87
|
"data-slot": "label",
|
|
@@ -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 // 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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"}
|
|
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, DEFAULT_BADGE_LABELS, type BadgeProps } from '../badge/Badge';\nimport { useLabels } from '../labels/use-labels';\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), `newNotification`: the badge's own word, a `LabelsProvider`'s, or\n * \"New notification\".\n */\nfunction badgeDescription(\n badge: ReactElement<BadgeProps> | undefined,\n newNotification: string,\n): 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, 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) return newNotification;\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 // The badge's words as the badge itself reads them, a `LabelsProvider`'s included.\n const { newNotification } = useLabels(\n 'Badge',\n DEFAULT_BADGE_LABELS,\n isValidElement<BadgeProps>(badge) ? badge.props.labels : undefined,\n );\n const description = badgeLabel ?? badgeDescription(badge, newNotification);\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":";;;;;;;;;;;;;;;;;;;AAsFA,SAAS,oBACP,UACA,kBACA;CACA,MAAM,eAAe,OAAwB,IAAI;CACjD,MAAM,cAAc,OAAO,QAAQ;CACnC,MAAM,SAAS,YAAY,WAAW,MAAM;CAC5C,MAAM,gBAAgB,iBAAiB;CACvC,sBAAsB;EACpB,MAAM,YAAY,aAAa;EAC/B,MAAM,UAAU,YAAY,YAAY;EACxC,YAAY,UAAU;EACtB,IAAI,CAAC,oBAAoB,CAAC,YAAY,CAAC,WAAW;EAClD,MAAM,WAAW,iBAAiB;EAClC,iBAAiB,UAAU;EAC3B,IAAI,CAAC,WAAW,iBAAiB,CAAC,UAAU,eAAe,aAAa,WAAW;EAEnF,UAAU,MAAM,YAAY;EAC5B,UAAU,MAAM,QAAQ;EACxB,MAAM,OAAO,SAAS,sBAAsB;EAC5C,MAAM,KAAK,UAAU,sBAAsB;EAC3C,MAAM,SAAS,KAAK,MAAM,GAAG;EAC7B,IAAI,WAAW,GAAG;EAElB,UAAU,MAAM,YAAY,cAAc,OAAO;EACjD,IAAI,KAAK,UAAU,GAAG,OAAO;GAC3B,QAAQ,WAAW,EAAE,GAAG,CAAC,QAAQ,CAAC,EAAE,GAAG,MAAM;GAC7C;EACF;EACA,UAAU,MAAM,QAAQ,GAAG,KAAK,MAAM;EAEtC,QAAa,WAAW;GAAE,GAAG,CAAC,QAAQ,CAAC;GAAG,OAAO,CAAC,KAAK,OAAO,GAAG,KAAK;EAAE,GAAG,MAAM,CAAC,CAAC,WAAW;GAC5F,UAAU,MAAM,QAAQ;EAC1B,CAAC;CACH,GAAG;EAAC;EAAU;EAAkB;EAAe;CAAM,CAAC;CACtD,OAAO;AACT;;;;;;AAOA,SAAS,iBACP,OACA,iBACe;CAGf,IAAI,CAAC,eAA2B,KAAK,GAAG,OAAO;CAC/C,MAAM,EAAE,OAAO,aAAa,MAAM;CAClC,IAAI,SAAS,MAAM,OAAO;CAC1B,IAAI,OAAO,aAAa,YAAY,OAAO,aAAa,UAAU,OAAO,OAAO,QAAQ;CACxF,IAAI,YAAY,QAAQ,aAAa,OAAO,OAAO;CACnD,OAAO;AACT;;AAGA,SAAgB,QAAQ,EACtB,QACA,YACA,kBACA,MACA,cACA,UACA,UACA,OACA,YACA,YACA,YACA,WACA,GAAG,QAMF;CACD,MAAM,SAAS,cAAc;EAAE;EAAQ;EAAY,OAAO,QAAQ,gBAAgB;CAAE,CAAC;CACrF,MAAM,eAAe,oBAAoB,QAAQ,QAAQ,GAAG,gBAAgB;CAC5E,MAAM,gBAAgB,MAAM;CAC5B,MAAM,SAAS,WAAW,gBAAgB,WAAW;CAErD,MAAM,kBAAkB,WAAW;CAEnC,MAAM,EAAE,oBAAoB,UAC1B,SACA,sBACA,eAA2B,KAAK,IAAI,MAAM,MAAM,SAAS,KAAA,CAC3D;CACA,MAAM,cAAc,cAAc,iBAAiB,OAAO,eAAe;CACzE,MAAM,QACJ,oBAAC,QAAD;EACE,kBAAe;EACf,aAAU;EACV,WAAW,OAAO,MAAM,EAAE,OAAO,YAAY,MAAM,CAAC;EAEnD;CACG,CAAA;CAER,MAAM,YAAY,eAChB,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,QAAD;EAAM,cAAY,CAAC,YAAY,KAAA;EAAW,WAAW,OAAO,SAAS;EAClE,UAAA;CACG,CAAA,GACN,oBAAC,QAAD;EAAM,cAAY,YAAY,KAAA;EAAW,WAAW,OAAO,SAAS;EACjE,UAAA;CACG,CAAA,CACN,EAAA,CAAA,IAEF;CAEF,MAAM,SAAS;CAGf,MAAM,gBAAgB,OAAwB,IAAI;CAClD,MAAM,CAAC,SAAS,cAAc,SAAS,KAAK;CAC5C,MAAM,WAAW,cAAc,WAAW,CAAC,OAAO,QAAQ;CAC1D,OACE,qBAAC,YAAD;EACE,GAAI;EACJ,UAAU,UAAU;GAClB,OAAO,UAAU,KAAK;GACtB,IAAI,UAAU,aAAa;EAC7B;EACA,gBAAgB,cAAc;GAC5B,OAAO,gBAAgB,SAAS;GAChC,WAAW,SAAS;EACtB;EACA,gBAAgB,UAA2C;GACzD,OAAO,gBAAgB,KAAK;GAC5B,IAAI,cAAc,WAAW,CAAC,OAAO,YAAY,MAAM,WAAW,GAChE,gBAAgB,cAAc,SAAS,MAAM,SAAS,MAAM,OAAO;EAEvE;EACA,YAAY,UAA4C;GACtD,OAAO,YAAY,KAAK;GACxB,IACE,cAAc,WACd,CAAC,OAAO,aACP,MAAM,QAAQ,WAAW,MAAM,QAAQ,MAExC,gBAAgB,cAAc,OAAO;EAEzC;EACA,iBAAc;EAEd,gBAAc,OAAO,oBAAoB,WAAW,SAAS,KAAA;EAC7D,oBACE,CAAC,OAAO,qBAAqB,SAAS,eAAe,OAAO,gBAAgB,KAAA,CAAS,CAAC,CACnF,OAAO,OAAO,CAAC,CACf,KAAK,GAAG,KAAK,KAAA;EAElB,gBAAc,YAAY,KAAA;EAE1B,eAAa,gBACX,OAAO,WAAW,MAAM,IAAI,qBAAqB,mBACjD,MACF;EACA,eAAa;EACb,WAAW,OAAO,KAAK,EAAE,OAAO,GAAG,YAAY,MAAM,SAAS,EAAE,CAAC;EAzCnE,UAAA;GA2CE,qBAAC,QAAD;IAAM,aAAU;IAAO,WAAW,OAAO,KAAK,EAAE,OAAO,YAAY,KAAK,CAAC;IAAzE,UAAA;KACE,oBAAC,QAAD;MACE,KAAK;MACL,eAAY;MACZ,aAAU;MACV,WAAW,OAAO,UAAU,EAAE,OAAO,YAAY,UAAU,CAAC;KAC7D,CAAA;KACD,oBAAC,QAAD;MACE,KAAK;MACL,eAAY;MACZ,iBAAe,YAAY,KAAA;MAC3B,iBAAe,OAAO,YAAY,KAAA;MAClC,WAAW,OAAO,WAAW,EAAE,OAAO,YAAY,WAAW,CAAC;KAC/D,CAAA;KACD,qBAAC,QAAD;MAAM,WAAW,OAAO,QAAQ;MAAhC,UAAA;OACE,oBAAC,QAAD;QACE,eAAY;QACZ,aAAU;QACV,WAAW,OAAO,KAAK,EAAE,OAAO,YAAY,KAAK,CAAC;QAEjD,UAAA,SAAS,CAAC,kBACT,oBAAC,WAAD;SAAkB;SAAO,WAAW,OAAO,UAAU,EAAE,OAAO,YAAY,MAAM,CAAC;SAC/E,UAAA,oBAAC,QAAD;UAAM,WAAW,OAAO,UAAU;UAAI,UAAA;SAAgB,CAAA;QAC7C,CAAA,IAEX;OAEE,CAAA;OACL,UAAU;OACV,SAAS,mBACR,oBAAC,QAAD;QAAM,eAAY;QAAO,WAAW,OAAO,MAAM,EAAE,OAAO,YAAY,MAAM,CAAC;QAC1E,UAAA;OACG,CAAA;MAEJ;;IACF;;GACL,CAAC,UAAU;GACX,SAAS,eAAe,QACvB,oBAAC,QAAD;IAAM,IAAI;IAAe,QAAA;IACtB,UAAA;GACG,CAAA;EAEE;;AAEhB"}
|
|
@@ -23,7 +23,7 @@ const navigationRailStyles = tv({
|
|
|
23
23
|
]
|
|
24
24
|
],
|
|
25
25
|
body: "flex min-h-0 flex-1 flex-col overflow-x-clip overflow-y-auto pt-[44px]",
|
|
26
|
-
header: "grid grid-cols-[56px] justify-items-center-safe ps-(--m3-rail-item-inset) pb-[40px] [&>*]:
|
|
26
|
+
header: "grid grid-cols-[56px] justify-items-center-safe ps-(--m3-rail-item-inset) pb-[40px] [&>*]:whitespace-nowrap",
|
|
27
27
|
items: ["flex flex-col", "[transition-property:gap] [transition-duration:var(--md-sys-motion-spring-spatial-default-duration)] [transition-timing-function:var(--md-sys-motion-spring-spatial-default-easing)]"],
|
|
28
28
|
scrim: [
|
|
29
29
|
"fixed inset-0 z-(--md-sys-z-scrim) bg-scrim/32 opacity-100",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navigation-styles.js","names":[],"sources":["../../../src/components/navigation/navigation-styles.ts"],"sourcesContent":["import { reducedMotionFade } from '../../motion/reduced-motion';\nimport { tv, type VariantProps } from '../../utils/tv';\n\n/*\n * Values follow Compose Material 3 (WideNavigationRail.kt, ShortNavigationBar.kt and the\n * NavigationRail* / NavigationBar* token files).\n *\n * Rail: `surface`, 96px collapsed; expanded it is as wide as its widest item plus the 20px\n * insets, 220–360px, and the width springs on the default spatial spring. Content starts\n * 44px down; a header sits 40px above the items, which are 4px apart collapsed and flush\n * expanded. The modal expanded rail is `surface-container` at level 2 with 16px end\n * corners over a 32% scrim; it grows from the collapsed width on the fast spatial spring, or\n * slides in on the default one when the collapsed rail is hidden. It stays mounted until its\n * own width or slide (or, under reduced motion, its fade) has ended.\n *\n * Icons stay on one line in both states, through the middle of the collapsed rail, as in\n * M3's rail motion: only the labels and the rail's edge move. While the width springs open\n * (and while the modal sheet shrinks away) labels stay on one line, clipped by the rail, so\n * none wraps letter by letter in a rail that is still narrow and no item grows taller. So nothing is centred in\n * the animated width. The collapsed width is `--m3-rail-collapsed-width` (96px), and the\n * item inset `--m3-rail-item-inset` is half of what's left beside a 56px pill (20px). The\n * header is a 56px column at that inset: anything up to 56px (the menu button, a FAB) is\n * centred on the icon line, and anything wider (an extended FAB) keeps its width and starts\n * at the column's edge.\n *\n * Items align as a group to the top (default) or, with `itemAlignment=\"center\"`, to the\n * middle of the space below the header (M3: centre alignment on tablets); the header stays\n * at the top. An optional divider (`outline-variant`) marks the edge next to the content.\n *\n * Bar: `surface-container`, at least 64px (it grows when labels wrap), no elevation.\n * Stacked items share the width equally, or, when centred, sit within side padding of\n * (100% − 10% × (items + 3)) / 2 (up to 6 items). Items beside their icons\n * (`iconPlacement=\"start\"`) are centred by default and sized to their content, so the extra\n * space goes to the bar's ends (M3: \"Horizontal navigation items have a fixed width\").\n * With `scrollBehavior=\"hide-on-scroll\"` the bar is sticky at the bottom and its offset\n * (`--m3-nav-bar-offset`, 0 down to minus its height) follows the page scroll.\n *\n * The rail's width is the `--m3-rail-width` variable, so a consumer `w-*` class still wins;\n * a narrower or wider collapsed rail sets `--m3-rail-collapsed-width` instead.\n *\n * Container colours and the modal rail's corners come from the component tokens\n * (`navigation-tokens.ts`, `--md-comp-nav-bar-*` and `--md-comp-nav-rail-*`): the bar, the\n * rail and the modal rail's sheet (`data-modal`) carry `data-tokens`. A consumer class still\n * wins in `cn()`.\n * Every class is written out in full so Tailwind can find it.\n */\n\n/** Half of what's left beside a 56px pill in the collapsed rail. */\nconst itemInset = '[--m3-rail-item-inset:calc((var(--m3-rail-collapsed-width)_-_56px)/2)]';\n\n/**\n * While the expanded rail is still widening (`data-widening`), labels stay on one line and\n * the rail clips them, so the items keep their height and the icons their places.\n */\nconst oneLineWhileResizing = '[&[data-widening]_[data-nav-label]]:text-nowrap';\n\nconst widthMotion = [\n '[transition-property:width]',\n '[transition-duration:var(--md-sys-motion-spring-spatial-default-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-default-easing)]',\n];\n\n/** Variant definitions for {@link NavigationRail}; extend them to add variants. */\nexport const navigationRailStyles = tv({\n slots: {\n root: [\n // Positioned, so a secondary column placed against it escapes the body's scrolling.\n 'relative flex w-(--m3-rail-width) shrink-0 flex-col text-on-surface',\n 'bg-[var(--_md-comp-nav-rail-collapsed-container-color)] data-expanded:bg-[var(--_md-comp-nav-rail-expanded-container-color)]',\n itemInset,\n oneLineWhileResizing,\n ...widthMotion,\n ],\n // Library-owned wrapper: clips labels while the width animates.\n body: 'flex min-h-0 flex-1 flex-col overflow-x-clip overflow-y-auto pt-[44px]',\n // Children keep their own width (a centred grid item would shrink to the 56px column).\n header:\n 'grid grid-cols-[56px] justify-items-center-safe ps-(--m3-rail-item-inset) pb-[40px] [&>*]:min-w-max',\n // The gap between items springs 4 → 0px as the rail expands (default spatial, as\n // Compose's itemVerticalSpacedBy; 2026-10-17), instant under reduced motion.\n items: [\n 'flex flex-col',\n '[transition-property:gap] [transition-duration:var(--md-sys-motion-spring-spatial-default-duration)] [transition-timing-function:var(--md-sys-motion-spring-spatial-default-easing)]',\n ],\n scrim: [\n 'fixed inset-0 z-(--md-sys-z-scrim) bg-scrim/32 opacity-100',\n '[transition-property:opacity]',\n '[transition-duration:var(--md-sys-motion-spring-effects-default-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-default-easing)]',\n // In and out on the default effects spring, as Compose's modal rail scrim (2026-10-15).\n 'starting:opacity-0 data-exiting:opacity-0',\n ],\n sheet: [\n 'fixed inset-y-0 start-0 z-(--md-sys-z-sheet) flex w-(--m3-rail-width) flex-col overflow-x-clip overflow-y-auto pt-[44px]',\n 'rounded-e-[var(--_md-comp-nav-rail-expanded-modal-container-shape)] bg-[var(--_md-comp-nav-rail-expanded-modal-container-color)]',\n 'text-on-surface shadow-elevation-2 outline-none',\n itemInset,\n oneLineWhileResizing,\n '[&[data-exiting]_[data-nav-label]]:text-nowrap',\n '[transition-property:width,translate]',\n '[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing)]',\n // Under reduced motion it fades in and out at its full width.\n ...reducedMotionFade,\n ],\n },\n variants: {\n expanded: {\n true: { items: 'gap-0' },\n false: { items: 'gap-[4px]' },\n },\n /** Auto margins centre the items without hiding any when the rail overflows. */\n itemAlignment: {\n top: {},\n center: { items: 'my-auto' },\n },\n /** `hideOnCollapse=\"compact\"`: the collapsed rail shows from 600px wide. */\n hiddenOnCompact: {\n true: { root: 'max-medium:hidden' },\n false: {},\n },\n /** A divider on the edge next to the content. */\n divider: {\n true: { root: 'border-e border-outline-variant' },\n false: {},\n },\n /**\n * `indicatorMotion=\"slide\"` in the expanded rail: the list is isolated, so the items'\n * raised content stays above a sliding pill and no higher.\n */\n slidingIndicator: {\n true: { items: 'isolate' },\n false: {},\n },\n /**\n * Modal sheet entry: from the collapsed width, or sliding in from the start edge. Under\n * reduced motion it fades instead.\n */\n sheetEntry: {\n grow: {\n sheet:\n 'motion-safe:starting:w-(--m3-rail-collapsed-width) motion-safe:data-exiting:w-(--m3-rail-collapsed-width)',\n },\n // Sliding in, the sheet takes the default spatial spring, as Compose's modal rail\n // (`WideNavigationRail.kt`): the fast one overshot 21px off the window edge (2026-10-15).\n slide: {\n sheet: [\n 'motion-safe:starting:-translate-x-full motion-safe:data-exiting:-translate-x-full motion-safe:rtl:starting:translate-x-full motion-safe:rtl:data-exiting:translate-x-full',\n '[transition-duration:var(--md-sys-motion-spring-spatial-default-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-default-easing)]',\n ],\n },\n },\n },\n defaultVariants: {\n expanded: false,\n sheetEntry: 'grow',\n itemAlignment: 'top',\n divider: false,\n slidingIndicator: false,\n },\n});\n\n/** Variant definitions for {@link NavigationBar}; extend them to add variants. */\nexport const navigationBarStyles = tv({\n slots: {\n root: 'flex min-h-[64px] w-full bg-[var(--_md-comp-nav-bar-container-color)] text-on-surface',\n item: 'min-w-0',\n },\n variants: {\n arrangement: {\n equal: { item: 'flex-1 basis-0' },\n centered: { root: 'justify-center', item: 'flex-1 basis-0' },\n },\n iconPlacement: { top: {}, start: {} },\n hideOnScroll: {\n // Settling after a scroll ends (`data-settling`), the offset eases to shown or hidden on\n // the default effects spring; while scrolling it follows the scroll directly.\n true: {\n root: [\n 'sticky bottom-(--m3-nav-bar-offset) z-(--md-sys-z-sticky)',\n 'data-settling:[transition-property:bottom]',\n 'data-settling:[transition-duration:var(--md-sys-motion-spring-effects-default-duration)]',\n 'data-settling:[transition-timing-function:var(--md-sys-motion-spring-effects-default-easing)]',\n ],\n },\n false: {},\n },\n },\n compoundVariants: [\n // Items beside their icons keep their own width; the extra space goes to the ends.\n { arrangement: 'centered', iconPlacement: 'start', class: { item: 'flex-initial basis-auto' } },\n ],\n defaultVariants: { arrangement: 'equal', iconPlacement: 'top', hideOnScroll: false },\n});\n\n/**\n * The variants of {@link navigationBarStyles}, for building on its styles. They are the recipe's keys, not\n * component props: their names (`hasIcon`, `isOpen`…) follow the classes and may change.\n */\nexport type NavigationBarStyleProps = VariantProps<typeof navigationBarStyles>;\nexport type NavigationBarArrangement = NonNullable<NavigationBarStyleProps['arrangement']>;\n"],"mappings":";;;;AAgDA,MAAM,YAAY;;;;;AAMlB,MAAM,uBAAuB;;AAS7B,MAAa,uBAAuB,GAAG;CACrC,OAAO;EACL,MAAM;GAEJ;GACA;GACA;GACA;GACA,GAAG;IAdP;IACA;IACA;GAYO;EACL;EAEA,MAAM;EAEN,QACE;EAGF,OAAO,CACL,iBACA,sLACF;EACA,OAAO;GACL;GACA;GACA;GACA;GAEA;EACF;EACA,OAAO;GACL;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GAEA,GAAG;EACL;CACF;CACA,UAAU;EACR,UAAU;GACR,MAAM,EAAE,OAAO,QAAQ;GACvB,OAAO,EAAE,OAAO,YAAY;EAC9B;;EAEA,eAAe;GACb,KAAK,CAAC;GACN,QAAQ,EAAE,OAAO,UAAU;EAC7B;;EAEA,iBAAiB;GACf,MAAM,EAAE,MAAM,oBAAoB;GAClC,OAAO,CAAC;EACV;;EAEA,SAAS;GACP,MAAM,EAAE,MAAM,kCAAkC;GAChD,OAAO,CAAC;EACV;;;;;EAKA,kBAAkB;GAChB,MAAM,EAAE,OAAO,UAAU;GACzB,OAAO,CAAC;EACV;;;;;EAKA,YAAY;GACV,MAAM,EACJ,OACE,4GACJ;GAGA,OAAO,EACL,OAAO;IACL;IACA;IACA;GACF,EACF;EACF;CACF;CACA,iBAAiB;EACf,UAAU;EACV,YAAY;EACZ,eAAe;EACf,SAAS;EACT,kBAAkB;CACpB;AACF,CAAC;;AAGD,MAAa,sBAAsB,GAAG;CACpC,OAAO;EACL,MAAM;EACN,MAAM;CACR;CACA,UAAU;EACR,aAAa;GACX,OAAO,EAAE,MAAM,iBAAiB;GAChC,UAAU;IAAE,MAAM;IAAkB,MAAM;GAAiB;EAC7D;EACA,eAAe;GAAE,KAAK,CAAC;GAAG,OAAO,CAAC;EAAE;EACpC,cAAc;GAGZ,MAAM,EACJ,MAAM;IACJ;IACA;IACA;IACA;GACF,EACF;GACA,OAAO,CAAC;EACV;CACF;CACA,kBAAkB,CAEhB;EAAE,aAAa;EAAY,eAAe;EAAS,OAAO,EAAE,MAAM,0BAA0B;CAAE,CAChG;CACA,iBAAiB;EAAE,aAAa;EAAS,eAAe;EAAO,cAAc;CAAM;AACrF,CAAC"}
|
|
1
|
+
{"version":3,"file":"navigation-styles.js","names":[],"sources":["../../../src/components/navigation/navigation-styles.ts"],"sourcesContent":["import { reducedMotionFade } from '../../motion/reduced-motion';\nimport { tv, type VariantProps } from '../../utils/tv';\n\n/*\n * Values follow Compose Material 3 (WideNavigationRail.kt, ShortNavigationBar.kt and the\n * NavigationRail* / NavigationBar* token files).\n *\n * Rail: `surface`, 96px collapsed; expanded it is as wide as its widest item plus the 20px\n * insets, 220–360px, and the width springs on the default spatial spring. Content starts\n * 44px down; a header sits 40px above the items, which are 4px apart collapsed and flush\n * expanded. The modal expanded rail is `surface-container` at level 2 with 16px end\n * corners over a 32% scrim; it grows from the collapsed width on the fast spatial spring, or\n * slides in on the default one when the collapsed rail is hidden. It stays mounted until its\n * own width or slide (or, under reduced motion, its fade) has ended.\n *\n * Icons stay on one line in both states, through the middle of the collapsed rail, as in\n * M3's rail motion: only the labels and the rail's edge move. While the width springs open\n * (and while the modal sheet shrinks away) labels stay on one line, clipped by the rail, so\n * none wraps letter by letter in a rail that is still narrow and no item grows taller. So nothing is centred in\n * the animated width. The collapsed width is `--m3-rail-collapsed-width` (96px), and the\n * item inset `--m3-rail-item-inset` is half of what's left beside a 56px pill (20px). The\n * header is a 56px column at that inset: anything up to 56px (the menu button, a FAB) is\n * centred on the icon line, and anything wider (an extended FAB) keeps its width and starts\n * at the column's edge.\n *\n * Items align as a group to the top (default) or, with `itemAlignment=\"center\"`, to the\n * middle of the space below the header (M3: centre alignment on tablets); the header stays\n * at the top. An optional divider (`outline-variant`) marks the edge next to the content.\n *\n * Bar: `surface-container`, at least 64px (it grows when labels wrap), no elevation.\n * Stacked items share the width equally, or, when centred, sit within side padding of\n * (100% − 10% × (items + 3)) / 2 (up to 6 items). Items beside their icons\n * (`iconPlacement=\"start\"`) are centred by default and sized to their content, so the extra\n * space goes to the bar's ends (M3: \"Horizontal navigation items have a fixed width\").\n * With `scrollBehavior=\"hide-on-scroll\"` the bar is sticky at the bottom and its offset\n * (`--m3-nav-bar-offset`, 0 down to minus its height) follows the page scroll.\n *\n * The rail's width is the `--m3-rail-width` variable, so a consumer `w-*` class still wins;\n * a narrower or wider collapsed rail sets `--m3-rail-collapsed-width` instead.\n *\n * Container colours and the modal rail's corners come from the component tokens\n * (`navigation-tokens.ts`, `--md-comp-nav-bar-*` and `--md-comp-nav-rail-*`): the bar, the\n * rail and the modal rail's sheet (`data-modal`) carry `data-tokens`. A consumer class still\n * wins in `cn()`.\n * Every class is written out in full so Tailwind can find it.\n */\n\n/** Half of what's left beside a 56px pill in the collapsed rail. */\nconst itemInset = '[--m3-rail-item-inset:calc((var(--m3-rail-collapsed-width)_-_56px)/2)]';\n\n/**\n * While the expanded rail is still widening (`data-widening`), labels stay on one line and\n * the rail clips them, so the items keep their height and the icons their places.\n */\nconst oneLineWhileResizing = '[&[data-widening]_[data-nav-label]]:text-nowrap';\n\nconst widthMotion = [\n '[transition-property:width]',\n '[transition-duration:var(--md-sys-motion-spring-spatial-default-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-default-easing)]',\n];\n\n/** Variant definitions for {@link NavigationRail}; extend them to add variants. */\nexport const navigationRailStyles = tv({\n slots: {\n root: [\n // Positioned, so a secondary column placed against it escapes the body's scrolling.\n 'relative flex w-(--m3-rail-width) shrink-0 flex-col text-on-surface',\n 'bg-[var(--_md-comp-nav-rail-collapsed-container-color)] data-expanded:bg-[var(--_md-comp-nav-rail-expanded-container-color)]',\n itemInset,\n oneLineWhileResizing,\n ...widthMotion,\n ],\n // Library-owned wrapper: clips labels while the width animates.\n body: 'flex min-h-0 flex-1 flex-col overflow-x-clip overflow-y-auto pt-[44px]',\n // A 56px column at the item inset: children up to 56px centre on the icon line, a wider\n // one (an extended FAB, an app's name) keeps its width on one line and starts at the\n // column's edge. Text doesn't wrap (2026-11-11) instead of every child taking\n // `min-width: max-content`, which grew a box holding an image at its full size (an image\n // avatar) to the image's natural width (2026-11-10).\n header:\n 'grid grid-cols-[56px] justify-items-center-safe ps-(--m3-rail-item-inset) pb-[40px] [&>*]:whitespace-nowrap',\n // The gap between items springs 4 → 0px as the rail expands (default spatial, as\n // Compose's itemVerticalSpacedBy; 2026-10-17), instant under reduced motion.\n items: [\n 'flex flex-col',\n '[transition-property:gap] [transition-duration:var(--md-sys-motion-spring-spatial-default-duration)] [transition-timing-function:var(--md-sys-motion-spring-spatial-default-easing)]',\n ],\n scrim: [\n 'fixed inset-0 z-(--md-sys-z-scrim) bg-scrim/32 opacity-100',\n '[transition-property:opacity]',\n '[transition-duration:var(--md-sys-motion-spring-effects-default-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-default-easing)]',\n // In and out on the default effects spring, as Compose's modal rail scrim (2026-10-15).\n 'starting:opacity-0 data-exiting:opacity-0',\n ],\n sheet: [\n 'fixed inset-y-0 start-0 z-(--md-sys-z-sheet) flex w-(--m3-rail-width) flex-col overflow-x-clip overflow-y-auto pt-[44px]',\n 'rounded-e-[var(--_md-comp-nav-rail-expanded-modal-container-shape)] bg-[var(--_md-comp-nav-rail-expanded-modal-container-color)]',\n 'text-on-surface shadow-elevation-2 outline-none',\n itemInset,\n oneLineWhileResizing,\n '[&[data-exiting]_[data-nav-label]]:text-nowrap',\n '[transition-property:width,translate]',\n '[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing)]',\n // Under reduced motion it fades in and out at its full width.\n ...reducedMotionFade,\n ],\n },\n variants: {\n expanded: {\n true: { items: 'gap-0' },\n false: { items: 'gap-[4px]' },\n },\n /** Auto margins centre the items without hiding any when the rail overflows. */\n itemAlignment: {\n top: {},\n center: { items: 'my-auto' },\n },\n /** `hideOnCollapse=\"compact\"`: the collapsed rail shows from 600px wide. */\n hiddenOnCompact: {\n true: { root: 'max-medium:hidden' },\n false: {},\n },\n /** A divider on the edge next to the content. */\n divider: {\n true: { root: 'border-e border-outline-variant' },\n false: {},\n },\n /**\n * `indicatorMotion=\"slide\"` in the expanded rail: the list is isolated, so the items'\n * raised content stays above a sliding pill and no higher.\n */\n slidingIndicator: {\n true: { items: 'isolate' },\n false: {},\n },\n /**\n * Modal sheet entry: from the collapsed width, or sliding in from the start edge. Under\n * reduced motion it fades instead.\n */\n sheetEntry: {\n grow: {\n sheet:\n 'motion-safe:starting:w-(--m3-rail-collapsed-width) motion-safe:data-exiting:w-(--m3-rail-collapsed-width)',\n },\n // Sliding in, the sheet takes the default spatial spring, as Compose's modal rail\n // (`WideNavigationRail.kt`): the fast one overshot 21px off the window edge (2026-10-15).\n slide: {\n sheet: [\n 'motion-safe:starting:-translate-x-full motion-safe:data-exiting:-translate-x-full motion-safe:rtl:starting:translate-x-full motion-safe:rtl:data-exiting:translate-x-full',\n '[transition-duration:var(--md-sys-motion-spring-spatial-default-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-default-easing)]',\n ],\n },\n },\n },\n defaultVariants: {\n expanded: false,\n sheetEntry: 'grow',\n itemAlignment: 'top',\n divider: false,\n slidingIndicator: false,\n },\n});\n\n/** Variant definitions for {@link NavigationBar}; extend them to add variants. */\nexport const navigationBarStyles = tv({\n slots: {\n root: 'flex min-h-[64px] w-full bg-[var(--_md-comp-nav-bar-container-color)] text-on-surface',\n item: 'min-w-0',\n },\n variants: {\n arrangement: {\n equal: { item: 'flex-1 basis-0' },\n centered: { root: 'justify-center', item: 'flex-1 basis-0' },\n },\n iconPlacement: { top: {}, start: {} },\n hideOnScroll: {\n // Settling after a scroll ends (`data-settling`), the offset eases to shown or hidden on\n // the default effects spring; while scrolling it follows the scroll directly.\n true: {\n root: [\n 'sticky bottom-(--m3-nav-bar-offset) z-(--md-sys-z-sticky)',\n 'data-settling:[transition-property:bottom]',\n 'data-settling:[transition-duration:var(--md-sys-motion-spring-effects-default-duration)]',\n 'data-settling:[transition-timing-function:var(--md-sys-motion-spring-effects-default-easing)]',\n ],\n },\n false: {},\n },\n },\n compoundVariants: [\n // Items beside their icons keep their own width; the extra space goes to the ends.\n { arrangement: 'centered', iconPlacement: 'start', class: { item: 'flex-initial basis-auto' } },\n ],\n defaultVariants: { arrangement: 'equal', iconPlacement: 'top', hideOnScroll: false },\n});\n\n/**\n * The variants of {@link navigationBarStyles}, for building on its styles. They are the recipe's keys, not\n * component props: their names (`hasIcon`, `isOpen`…) follow the classes and may change.\n */\nexport type NavigationBarStyleProps = VariantProps<typeof navigationBarStyles>;\nexport type NavigationBarArrangement = NonNullable<NavigationBarStyleProps['arrangement']>;\n"],"mappings":";;;;AAgDA,MAAM,YAAY;;;;;AAMlB,MAAM,uBAAuB;;AAS7B,MAAa,uBAAuB,GAAG;CACrC,OAAO;EACL,MAAM;GAEJ;GACA;GACA;GACA;GACA,GAAG;IAdP;IACA;IACA;GAYO;EACL;EAEA,MAAM;EAMN,QACE;EAGF,OAAO,CACL,iBACA,sLACF;EACA,OAAO;GACL;GACA;GACA;GACA;GAEA;EACF;EACA,OAAO;GACL;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GAEA,GAAG;EACL;CACF;CACA,UAAU;EACR,UAAU;GACR,MAAM,EAAE,OAAO,QAAQ;GACvB,OAAO,EAAE,OAAO,YAAY;EAC9B;;EAEA,eAAe;GACb,KAAK,CAAC;GACN,QAAQ,EAAE,OAAO,UAAU;EAC7B;;EAEA,iBAAiB;GACf,MAAM,EAAE,MAAM,oBAAoB;GAClC,OAAO,CAAC;EACV;;EAEA,SAAS;GACP,MAAM,EAAE,MAAM,kCAAkC;GAChD,OAAO,CAAC;EACV;;;;;EAKA,kBAAkB;GAChB,MAAM,EAAE,OAAO,UAAU;GACzB,OAAO,CAAC;EACV;;;;;EAKA,YAAY;GACV,MAAM,EACJ,OACE,4GACJ;GAGA,OAAO,EACL,OAAO;IACL;IACA;IACA;GACF,EACF;EACF;CACF;CACA,iBAAiB;EACf,UAAU;EACV,YAAY;EACZ,eAAe;EACf,SAAS;EACT,kBAAkB;CACpB;AACF,CAAC;;AAGD,MAAa,sBAAsB,GAAG;CACpC,OAAO;EACL,MAAM;EACN,MAAM;CACR;CACA,UAAU;EACR,aAAa;GACX,OAAO,EAAE,MAAM,iBAAiB;GAChC,UAAU;IAAE,MAAM;IAAkB,MAAM;GAAiB;EAC7D;EACA,eAAe;GAAE,KAAK,CAAC;GAAG,OAAO,CAAC;EAAE;EACpC,cAAc;GAGZ,MAAM,EACJ,MAAM;IACJ;IACA;IACA;IACA;GACF,EACF;GACA,OAAO,CAAC;EACV;CACF;CACA,kBAAkB,CAEhB;EAAE,aAAa;EAAY,eAAe;EAAS,OAAO,EAAE,MAAM,0BAA0B;CAAE,CAChG;CACA,iBAAiB;EAAE,aAAa;EAAS,eAAe;EAAO,cAAc;CAAM;AACrF,CAAC"}
|
|
@@ -45,7 +45,7 @@ interface PullToRefreshOwnProps {
|
|
|
45
45
|
disabled?: boolean;
|
|
46
46
|
/**
|
|
47
47
|
* Words the pull to refresh announces, with English defaults; name them for what refreshes
|
|
48
|
-
* ("Refreshing messages").
|
|
48
|
+
* ("Refreshing messages"). A {@link LabelsProvider} sets them for the whole app.
|
|
49
49
|
*/
|
|
50
50
|
labels?: Partial<PullToRefreshLabels>;
|
|
51
51
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../../utils/cn.js";
|
|
3
|
+
import { useLabels } from "../labels/use-labels.js";
|
|
3
4
|
import { useM3Spring } from "../../motion/use-m3-spring.js";
|
|
4
5
|
import { usePreloadWhenIdle } from "../../utils/load-on-demand.js";
|
|
5
6
|
import { LazyLoadingIndicator, preloadLoadingIndicator } from "../loading-indicator/lazy-loading-indicator.js";
|
|
@@ -79,10 +80,7 @@ const isThenable = (value) => typeof value === "object" && value !== null && typ
|
|
|
79
80
|
* </PullToRefresh>
|
|
80
81
|
*/
|
|
81
82
|
function PullToRefresh({ refreshing: refreshingProp, onRefresh, threshold = 80, disabled = false, labels: labelsProp, children, classNames, className, ref, ...rest }) {
|
|
82
|
-
const labels =
|
|
83
|
-
...DEFAULT_LABELS,
|
|
84
|
-
...labelsProp
|
|
85
|
-
};
|
|
83
|
+
const labels = useLabels("PullToRefresh", DEFAULT_LABELS, labelsProp);
|
|
86
84
|
const [pendingRefresh, setPendingRefresh] = useState(false);
|
|
87
85
|
const refreshing = refreshingProp ?? pendingRefresh;
|
|
88
86
|
const fraction = useMotionValue(refreshing ? 1 : 0);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PullToRefresh.js","names":[],"sources":["../../../src/components/pull-to-refresh/PullToRefresh.tsx"],"sourcesContent":["'use client';\n\nimport {\n animate,\n useMotionValue,\n useMotionValueEvent,\n useReducedMotion,\n type MotionValue,\n} from 'motion/react';\nimport {\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type CSSProperties,\n type ReactNode,\n} from 'react';\nimport { useObjectRef } from 'react-aria';\nimport { useM3Spring } from '../../motion/use-m3-spring';\nimport { cn } from '../../utils/cn';\nimport { usePreloadWhenIdle } from '../../utils/load-on-demand';\nimport {\n LazyLoadingIndicator,\n preloadLoadingIndicator,\n} from '../loading-indicator/lazy-loading-indicator';\nimport {\n POSITIONAL_THRESHOLD,\n distanceFraction,\n overshootRotation,\n passesThreshold,\n} from './pull-to-refresh-math';\nimport { pullToRefreshStyles } from './pull-to-refresh-styles';\n\nexport interface PullToRefreshClassNames {\n /** The outer element, around the content. */\n root?: string;\n /**\n * The zero-height sticky layer the indicator hangs from, at the top of the area. Give it a\n * `top-*` class when a sticky app bar covers the top of a scrolling page. Its margins are\n * the library's, so a flex root's `gap-*` or a block root's `space-y-*` never moves the\n * content. In a grid root it is a grid item, so one row gap opens above the content.\n */\n layer?: string;\n /** The 48px circle holding the loading indicator. */\n indicator?: string;\n}\n\n/** Words the pull to refresh announces. */\nexport interface PullToRefreshLabels {\n /** The indicator's name while it follows a pull. @default \"Pull to refresh\" */\n pull: string;\n /**\n * The indicator's name while refreshing, also announced when a refresh starts.\n * @default \"Refreshing\"\n */\n refreshing: string;\n /** Announced when a refresh completes. @default \"Refreshed\" */\n refreshed: string;\n}\n\nconst DEFAULT_LABELS: PullToRefreshLabels = {\n pull: 'Pull to refresh',\n refreshing: 'Refreshing',\n refreshed: 'Refreshed',\n};\n\n/** How far a gesture moves before it's a pull or the browser's, px. */\nconst POINTER_SLOP = 4;\n\ninterface PullToRefreshOwnProps {\n /**\n * Whether a refresh is running. While it is, the indicator rests at the threshold as an\n * indeterminate loading indicator; when it turns `false` the indicator leaves. Set it `true`\n * from a refresh button too, and the indicator comes in the same way. Leave it out to let a\n * promise returned by `onRefresh` drive it.\n */\n refreshing?: boolean;\n /**\n * Called when a pull is released past the threshold. Start the refresh and set\n * `refreshing`, or return a promise: without `refreshing`, the indicator stays until it\n * settles.\n */\n onRefresh: () => void | Promise<unknown>;\n /** How far the indicator travels before a release refreshes, px. @default 80 */\n threshold?: number;\n /** Turns the gesture off. A running refresh still shows its indicator. @default false */\n disabled?: boolean;\n /**\n * Words the pull to refresh announces, with English defaults; name them for what refreshes\n * (\"Refreshing messages\").\n */\n labels?: Partial<PullToRefreshLabels>;\n /**\n * The content: a scrolling list, grid or card collection, or a whole page. On a grid root\n * (`grid` on `className`) the indicator's layer is a grid item, so one row gap opens above\n * the content; put the grid on an element inside instead. Flex, block and `space-y-*` roots\n * are exact.\n */\n children?: ReactNode;\n /** Classes for the parts: `root`, `layer` (the sticky layer at the top) and `indicator`. */\n classNames?: PullToRefreshClassNames;\n}\n\nexport type PullToRefreshProps = PullToRefreshOwnProps &\n Omit<ComponentPropsWithRef<'div'>, keyof PullToRefreshOwnProps>;\n\n/** A pointer or touch gesture over the area. */\ninterface Gesture {\n /** The touch's identifier, or the pointer's id. */\n id: number;\n /** `touch` for touch events, otherwise the pointer type (mouse or pen). */\n source: 'touch' | 'pointer';\n startX: number;\n startY: number;\n lastY: number;\n /**\n * `tracking`: not pulling (yet). `pulling`: the indicator follows the gesture. `inert`: the\n * area claims the gesture (so the browser doesn't overscroll) but it does nothing, because a\n * refresh or a settle animation is running. `native`: the browser scrolls; hands off.\n */\n phase: 'tracking' | 'pulling' | 'inert' | 'native';\n /** Every scroll container between the gesture's target and the area is at its top. */\n atTop: boolean;\n /** How far the gesture has pulled, px (Compose's `distancePulled`). */\n pulled: number;\n}\n\nconst isScrollContainer = (element: Element) =>\n /^(auto|scroll|overlay)$/.test(getComputedStyle(element).overflowY);\n\n/**\n * Whether every scroll container from `target` up to the area's (the area itself if it\n * scrolls, else the nearest one above it, or the page) is scrolled to its top, so a downward\n * drag can't scroll anything and pulls instead.\n */\nfunction isAtTop(target: EventTarget | null, root: HTMLElement): boolean {\n let passedRoot = false;\n for (\n let node: Element | null = target instanceof Element ? target : root;\n node;\n node = node.parentElement\n ) {\n if (node.scrollTop >= 1) return false;\n if (node === root) {\n if (isScrollContainer(node)) return true;\n passedRoot = true;\n } else if (passedRoot && isScrollContainer(node)) {\n return true;\n }\n }\n return true;\n}\n\n/**\n * Whether something between `target` and the area handles vertical drags itself (its\n * `touch-action` doesn't allow vertical panning: a vertical slider, a drawing surface), so\n * the gesture is left to it.\n */\nfunction ownsVerticalDrag(target: EventTarget | null, root: HTMLElement): boolean {\n for (\n let node = target instanceof Element ? target : null;\n node && node !== root;\n node = node.parentElement\n ) {\n const touchAction = getComputedStyle(node).touchAction;\n if (touchAction === 'none') return true;\n if (/pan-x/.test(touchAction) && !/pan-y|pan-up|pan-down/.test(touchAction)) return true;\n }\n return false;\n}\n\nconst isThenable = (value: unknown): value is PromiseLike<unknown> =>\n typeof value === 'object' &&\n value !== null &&\n typeof (value as { then?: unknown }).then === 'function';\n\n/**\n * M3 pull to refresh: at the top of its content, a downward drag (touch, mouse or pen) pulls\n * a contained loading indicator in from the top edge, morphing with the pull. Releasing past\n * the threshold calls `onRefresh`; the indicator then rests at the threshold, as an\n * indeterminate loading indicator, until `refreshing` turns `false`. Releasing short of the\n * threshold, or pushing back up, cancels.\n *\n * Wrap a scroll container (give the root `overflow-y-auto` and a height, or put the scrolling\n * element inside it) or a whole page. Scrolling is untouched: the gesture only pulls when the\n * content is at its top and the drag goes down.\n *\n * A `gap-*` on a flex root or `space-y-*` on a block root spaces the content exactly. On a\n * grid root, every in-flow child is a grid item, so the indicator's sticky layer takes a\n * zero-height row of its own and one `row-gap` opens above the content (removing it would\n * mean positioning the layer, which layout safety rules out). Put the grid on an element\n * inside: `<PullToRefresh><div className=\"grid gap-4\">…</div></PullToRefresh>`.\n *\n * Pulling isn't available to everyone, so also offer a refresh button that sets `refreshing`,\n * in an app bar, a menu or next to the content (the spec's accessibility guidance).\n *\n * The motion and sizes follow Compose's `PullToRefreshBox` with\n * `PullToRefreshDefaults.LoadingIndicator`: an 80px threshold, the indicator moving at half\n * the finger's speed and meeting resistance past the threshold, a 48px `primary-container`\n * circle at elevation 0, and a crossfade to the indeterminate indicator while refreshing.\n *\n * @example\n * <PullToRefresh refreshing={loading} onRefresh={reload} className=\"h-dvh overflow-y-auto\">\n * <List aria-label=\"Messages\">…</List>\n * </PullToRefresh>\n */\nexport function PullToRefresh({\n refreshing: refreshingProp,\n onRefresh,\n threshold = POSITIONAL_THRESHOLD,\n disabled = false,\n labels: labelsProp,\n children,\n classNames,\n className,\n ref,\n ...rest\n}: PullToRefreshProps) {\n const labels = { ...DEFAULT_LABELS, ...labelsProp };\n const [pendingRefresh, setPendingRefresh] = useState(false);\n const refreshing = refreshingProp ?? pendingRefresh;\n const fraction = useMotionValue(refreshing ? 1 : 0);\n // Settling to the threshold or away never bounces: Compose animates it with\n // `Animatable.animateTo`'s default, non-bouncing spring, which the default effects spring\n // matches (2026-10-15; the fast spatial one dipped 3px under the threshold and back).\n const spring = useM3Spring('effects', 'default');\n // Under reduced motion the indicator jumps to the threshold or away; a pull still moves it.\n const settle = useReducedMotion() ? { duration: 0 } : spring;\n const [pulling, setPulling] = useState(false);\n const [announcement, setAnnouncement] = useState('');\n // The indicator's code (its shapes and morphing) loads on demand: when the browser is first\n // idle, or when a pull starts, whichever comes first.\n usePreloadWhenIdle(preloadLoadingIndicator, !disabled);\n\n const rootRef = useObjectRef(ref);\n const animationRef = useRef<{ stop: () => void } | null>(null);\n const animatingRef = useRef(false);\n const mountedRef = useRef(true);\n // The latest props and helpers, for the native listeners added once.\n const latest = useRef({ refreshing, disabled, threshold, onRefresh, refreshingProp, settle });\n useLayoutEffect(() => {\n latest.current = { refreshing, disabled, threshold, onRefresh, refreshingProp, settle };\n });\n\n // The layer is the root's first child, so a consumer `gap-*` on a flex root would open a gap\n // below it and push the content down. It's cancelled after every render and whenever the\n // root resizes (a responsive class may change the gap).\n const layerRef = useRef<HTMLDivElement>(null);\n useLayoutEffect(() => {\n if (rootRef.current && layerRef.current) cancelRootGap(rootRef.current, layerRef.current);\n });\n useEffect(() => {\n const root = rootRef.current;\n if (!root || typeof ResizeObserver === 'undefined') return;\n const observer = new ResizeObserver(() => {\n if (layerRef.current) cancelRootGap(root, layerRef.current);\n });\n observer.observe(root);\n return () => observer.disconnect();\n }, [rootRef]);\n\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n animationRef.current?.stop();\n };\n }, []);\n\n /** Springs the indicator to `target` (Compose's `animateToThreshold` / `animateToHidden`). */\n const settleRef = useRef((target: number) => {\n animationRef.current?.stop();\n animatingRef.current = true;\n const controls = animate(fraction, target, latest.current.settle);\n animationRef.current = controls;\n void controls.then(() => {\n if (animationRef.current === controls) animatingRef.current = false;\n });\n });\n\n // A refresh starting or ending moves the indicator to the threshold or away, and is\n // announced. The first render places it without either (Compose's `onAttach`).\n const previousRefreshing = useRef(refreshing);\n useEffect(() => {\n if (previousRefreshing.current === refreshing) return;\n previousRefreshing.current = refreshing;\n settleRef.current(refreshing ? 1 : 0);\n setAnnouncement(refreshing ? labels.refreshing : labels.refreshed);\n }, [refreshing, labels.refreshing, labels.refreshed]);\n\n useEffect(() => {\n const root = rootRef.current;\n if (!root) return;\n let gesture: Gesture | null = null;\n let suppressClick = false;\n\n const begin = (\n source: Gesture['source'],\n id: number,\n x: number,\n y: number,\n target: EventTarget | null,\n ) => {\n const { disabled: off } = latest.current;\n gesture =\n off || ownsVerticalDrag(target, root)\n ? null\n : {\n id,\n source,\n startX: x,\n startY: y,\n lastY: y,\n phase: 'tracking',\n atTop: isAtTop(target, root),\n pulled: 0,\n };\n // A pull may start: the indicator's code loads now (if the idle preload hasn't loaded it\n // yet), well before the gesture passes the slop and the indicator shows. A failed load is\n // tried again by the indicator itself.\n if (gesture?.atTop) void preloadLoadingIndicator().catch(() => undefined);\n };\n\n /** Moves the gesture to `y`; returns whether the area took the movement. */\n const move = (x: number, y: number, cancelable: boolean): boolean => {\n const current = gesture;\n if (!current || current.phase === 'native') return false;\n const dy = y - current.lastY;\n current.lastY = y;\n if (current.phase === 'tracking') {\n const totalX = x - current.startX;\n const totalY = y - current.startY;\n const distance = Math.max(Math.abs(totalX), Math.abs(totalY));\n // A mouse or pen must move past the slop first, so a click stays a click.\n if (current.source === 'pointer' && distance <= POINTER_SLOP) return false;\n const downward = totalY > 0 && totalY >= Math.abs(totalX) && current.atTop;\n if (!downward || !cancelable) {\n // Sideways, upward or mid-scroll: the browser's for the rest of the gesture. A\n // touch gets the same slop before it's decided, against a finger's jitter.\n if (!cancelable || distance > POINTER_SLOP) current.phase = 'native';\n return false;\n }\n const { refreshing: busy } = latest.current;\n if (busy || animatingRef.current) {\n current.phase = 'inert';\n return true;\n }\n current.phase = 'pulling';\n // A mouse or pen pull starts at the slop, so the indicator doesn't jump.\n current.pulled = Math.max(0, totalY - (current.source === 'pointer' ? POINTER_SLOP : 0));\n setPulling(true);\n fraction.set(distanceFraction(current.pulled, latest.current.threshold));\n return true;\n }\n if (current.phase === 'inert') return true;\n // Pulling: up and down both move the indicator (Compose's pre- and post-scroll).\n current.pulled = Math.max(0, current.pulled + dy);\n fraction.set(distanceFraction(current.pulled, latest.current.threshold));\n if (current.pulled === 0) {\n // Pushed all the way back: the gesture may scroll the content again.\n current.phase = 'tracking';\n current.startX = x;\n current.startY = y;\n current.atTop = true;\n setPulling(false);\n }\n return true;\n };\n\n /** Ends the gesture (Compose's `onRelease`); `cancel` never refreshes. */\n const end = (cancel: boolean) => {\n const current = gesture;\n gesture = null;\n if (!current || current.phase !== 'pulling') return;\n setPulling(false);\n if (current.source === 'pointer') {\n // A mouse pull ends with a click on whatever is under the pointer; it isn't one. The\n // click comes before any timer.\n suppressClick = true;\n setTimeout(() => {\n suppressClick = false;\n });\n }\n const { threshold: limit, refreshingProp: controlled } = latest.current;\n if (!cancel && passesThreshold(current.pulled, limit)) {\n const result = latest.current.onRefresh();\n if (controlled === undefined && isThenable(result)) {\n setPendingRefresh(true);\n const done = () => {\n if (mountedRef.current) setPendingRefresh(false);\n };\n result.then(done, done);\n }\n }\n // Compose hides the indicator here; a refresh that starts moves it to the threshold.\n settleRef.current(0);\n };\n\n const touchOf = (event: TouchEvent) =>\n gesture === null\n ? undefined\n : [...event.changedTouches].find((touch) => touch.identifier === gesture?.id);\n\n const onTouchStart = (event: TouchEvent) => {\n if (event.touches.length > 1) {\n // A second finger (pinch to zoom) ends the pull without refreshing.\n end(true);\n return;\n }\n const touch = event.changedTouches[0];\n if (touch) begin('touch', touch.identifier, touch.clientX, touch.clientY, event.target);\n };\n const onTouchMove = (event: TouchEvent) => {\n const touch = touchOf(event);\n if (!touch || event.defaultPrevented) return;\n if (move(touch.clientX, touch.clientY, event.cancelable) && event.cancelable) {\n event.preventDefault();\n }\n };\n const onTouchEnd = (event: TouchEvent) => {\n if (touchOf(event)) end(event.type === 'touchcancel');\n };\n\n const onPointerDown = (event: PointerEvent) => {\n if (event.pointerType === 'touch' || event.button !== 0 || !event.isPrimary) return;\n begin('pointer', event.pointerId, event.clientX, event.clientY, event.target);\n };\n const onPointerMove = (event: PointerEvent) => {\n if (gesture?.source !== 'pointer' || event.pointerId !== gesture.id) return;\n const wasPulling = gesture.phase === 'pulling';\n if (!move(event.clientX, event.clientY, true)) return;\n event.preventDefault();\n if (!wasPulling && gesture?.phase === 'pulling') {\n root.setPointerCapture?.(event.pointerId);\n // A mouse drag would otherwise select the text it passes.\n window.getSelection()?.removeAllRanges();\n }\n };\n const onPointerEnd = (event: PointerEvent) => {\n if (gesture?.source !== 'pointer' || event.pointerId !== gesture.id) return;\n end(event.type === 'pointercancel');\n };\n const onClick = (event: MouseEvent) => {\n if (!suppressClick) return;\n suppressClick = false;\n event.preventDefault();\n event.stopPropagation();\n };\n\n const options = { passive: false } as const;\n root.addEventListener('touchstart', onTouchStart, { passive: true });\n root.addEventListener('touchmove', onTouchMove, options);\n root.addEventListener('touchend', onTouchEnd);\n root.addEventListener('touchcancel', onTouchEnd);\n root.addEventListener('pointerdown', onPointerDown);\n root.addEventListener('pointermove', onPointerMove);\n root.addEventListener('pointerup', onPointerEnd);\n root.addEventListener('pointercancel', onPointerEnd);\n root.addEventListener('click', onClick, { capture: true });\n return () => {\n root.removeEventListener('touchstart', onTouchStart);\n root.removeEventListener('touchmove', onTouchMove);\n root.removeEventListener('touchend', onTouchEnd);\n root.removeEventListener('touchcancel', onTouchEnd);\n root.removeEventListener('pointerdown', onPointerDown);\n root.removeEventListener('pointermove', onPointerMove);\n root.removeEventListener('pointerup', onPointerEnd);\n root.removeEventListener('pointercancel', onPointerEnd);\n root.removeEventListener('click', onClick, { capture: true });\n };\n }, [fraction, rootRef]);\n\n const styles = pullToRefreshStyles();\n\n return (\n <div\n {...rest}\n ref={rootRef}\n data-pulling={pulling || undefined}\n data-refreshing={refreshing || undefined}\n data-disabled={disabled || undefined}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n <div\n ref={layerRef}\n className={styles.layer({ class: classNames?.layer })}\n style={{ '--m3-pull-to-refresh-threshold': `${threshold}px` } as CSSProperties}\n >\n <div className={styles.clip()}>\n <PullIndicator\n fraction={fraction}\n refreshing={refreshing}\n threshold={threshold}\n labels={labels}\n className={styles.indicator({ class: classNames?.indicator })}\n faceClassName={styles.face()}\n />\n </div>\n <span role=\"status\" className=\"sr-only\">\n {announcement}\n </span>\n </div>\n {children}\n </div>\n );\n}\n\n/**\n * Keeps the root's own spacing from opening below the zero-height layer. Its margins are the\n * library's: zero, so a `space-y-*` root doesn't push the content down, and in a flex root a\n * negative margin as large as the gap along the main axis. In a grid root it moves to an\n * implicit row of its own before the first one, across every column, so the content keeps its\n * rows and columns; a grid row's size can't go below zero, so one row gap stays above the\n * content (documented: put the grid on an element inside the root).\n */\nfunction cancelRootGap(root: HTMLElement, layer: HTMLElement) {\n const style = getComputedStyle(root);\n const grid = style.display.endsWith('grid');\n const flex = style.display.endsWith('flex');\n const row = flex && style.flexDirection.startsWith('row');\n const reverse = flex && style.flexDirection.endsWith('reverse');\n const gap = Number.parseFloat(row ? style.columnGap : style.rowGap);\n for (const side of ['BlockStart', 'BlockEnd', 'InlineStart', 'InlineEnd'] as const) {\n layer.style[`margin${side}`] = '0px';\n }\n if (flex && gap > 0) {\n layer.style[`margin${row ? 'Inline' : 'Block'}${reverse ? 'Start' : 'End'}`] = `${-gap}px`;\n }\n layer.style.gridRow = grid ? 'span 1 / 1' : '';\n layer.style.gridColumn = grid ? '1 / -1' : '';\n}\n\n/**\n * The indicator, subscribed to the pull on its own so a pull re-renders only it, never the\n * content. Hidden at rest; while pulled it is a determinate loading indicator following the\n * pull, and while refreshing an indeterminate one, crossfading between them.\n */\nfunction PullIndicator({\n fraction,\n refreshing,\n threshold,\n labels,\n className,\n faceClassName,\n}: {\n fraction: MotionValue<number>;\n refreshing: boolean;\n threshold: number;\n labels: PullToRefreshLabels;\n className: string;\n faceClassName: string;\n}) {\n const [value, setValue] = useState(() => fraction.get());\n useMotionValueEvent(fraction, 'change', setValue);\n if (value <= 0 && !refreshing) return null;\n const rotation = overshootRotation(value);\n return (\n <div\n data-indicator=\"\"\n className={className}\n // Compose's `translationY = distanceFraction × maxDistance − height`.\n style={{ transform: `translateY(calc(${value * threshold}px - 100%))` }}\n >\n <div\n data-active={!refreshing}\n aria-hidden={refreshing || undefined}\n className={faceClassName}\n style={rotation === 0 ? undefined : { transform: `rotate(${rotation}deg)` }}\n >\n <LazyLoadingIndicator\n variant=\"contained\"\n value={Math.min(Math.max(value, 0), 1)}\n aria-label={labels.pull}\n />\n </div>\n <div\n data-active={refreshing}\n aria-hidden={!refreshing || undefined}\n className={faceClassName}\n >\n <LazyLoadingIndicator variant=\"contained\" aria-label={labels.refreshing} />\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PullToRefresh.js","names":[],"sources":["../../../src/components/pull-to-refresh/PullToRefresh.tsx"],"sourcesContent":["'use client';\n\nimport {\n animate,\n useMotionValue,\n useMotionValueEvent,\n useReducedMotion,\n type MotionValue,\n} from 'motion/react';\nimport {\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type CSSProperties,\n type ReactNode,\n} from 'react';\nimport { useObjectRef } from 'react-aria';\nimport { useLabels } from '../labels/use-labels';\nimport { useM3Spring } from '../../motion/use-m3-spring';\nimport { cn } from '../../utils/cn';\nimport { usePreloadWhenIdle } from '../../utils/load-on-demand';\nimport {\n LazyLoadingIndicator,\n preloadLoadingIndicator,\n} from '../loading-indicator/lazy-loading-indicator';\nimport {\n POSITIONAL_THRESHOLD,\n distanceFraction,\n overshootRotation,\n passesThreshold,\n} from './pull-to-refresh-math';\nimport { pullToRefreshStyles } from './pull-to-refresh-styles';\n\nexport interface PullToRefreshClassNames {\n /** The outer element, around the content. */\n root?: string;\n /**\n * The zero-height sticky layer the indicator hangs from, at the top of the area. Give it a\n * `top-*` class when a sticky app bar covers the top of a scrolling page. Its margins are\n * the library's, so a flex root's `gap-*` or a block root's `space-y-*` never moves the\n * content. In a grid root it is a grid item, so one row gap opens above the content.\n */\n layer?: string;\n /** The 48px circle holding the loading indicator. */\n indicator?: string;\n}\n\n/** Words the pull to refresh announces. */\nexport interface PullToRefreshLabels {\n /** The indicator's name while it follows a pull. @default \"Pull to refresh\" */\n pull: string;\n /**\n * The indicator's name while refreshing, also announced when a refresh starts.\n * @default \"Refreshing\"\n */\n refreshing: string;\n /** Announced when a refresh completes. @default \"Refreshed\" */\n refreshed: string;\n}\n\nconst DEFAULT_LABELS: PullToRefreshLabels = {\n pull: 'Pull to refresh',\n refreshing: 'Refreshing',\n refreshed: 'Refreshed',\n};\n\n/** How far a gesture moves before it's a pull or the browser's, px. */\nconst POINTER_SLOP = 4;\n\ninterface PullToRefreshOwnProps {\n /**\n * Whether a refresh is running. While it is, the indicator rests at the threshold as an\n * indeterminate loading indicator; when it turns `false` the indicator leaves. Set it `true`\n * from a refresh button too, and the indicator comes in the same way. Leave it out to let a\n * promise returned by `onRefresh` drive it.\n */\n refreshing?: boolean;\n /**\n * Called when a pull is released past the threshold. Start the refresh and set\n * `refreshing`, or return a promise: without `refreshing`, the indicator stays until it\n * settles.\n */\n onRefresh: () => void | Promise<unknown>;\n /** How far the indicator travels before a release refreshes, px. @default 80 */\n threshold?: number;\n /** Turns the gesture off. A running refresh still shows its indicator. @default false */\n disabled?: boolean;\n /**\n * Words the pull to refresh announces, with English defaults; name them for what refreshes\n * (\"Refreshing messages\"). A {@link LabelsProvider} sets them for the whole app.\n */\n labels?: Partial<PullToRefreshLabels>;\n /**\n * The content: a scrolling list, grid or card collection, or a whole page. On a grid root\n * (`grid` on `className`) the indicator's layer is a grid item, so one row gap opens above\n * the content; put the grid on an element inside instead. Flex, block and `space-y-*` roots\n * are exact.\n */\n children?: ReactNode;\n /** Classes for the parts: `root`, `layer` (the sticky layer at the top) and `indicator`. */\n classNames?: PullToRefreshClassNames;\n}\n\nexport type PullToRefreshProps = PullToRefreshOwnProps &\n Omit<ComponentPropsWithRef<'div'>, keyof PullToRefreshOwnProps>;\n\n/** A pointer or touch gesture over the area. */\ninterface Gesture {\n /** The touch's identifier, or the pointer's id. */\n id: number;\n /** `touch` for touch events, otherwise the pointer type (mouse or pen). */\n source: 'touch' | 'pointer';\n startX: number;\n startY: number;\n lastY: number;\n /**\n * `tracking`: not pulling (yet). `pulling`: the indicator follows the gesture. `inert`: the\n * area claims the gesture (so the browser doesn't overscroll) but it does nothing, because a\n * refresh or a settle animation is running. `native`: the browser scrolls; hands off.\n */\n phase: 'tracking' | 'pulling' | 'inert' | 'native';\n /** Every scroll container between the gesture's target and the area is at its top. */\n atTop: boolean;\n /** How far the gesture has pulled, px (Compose's `distancePulled`). */\n pulled: number;\n}\n\nconst isScrollContainer = (element: Element) =>\n /^(auto|scroll|overlay)$/.test(getComputedStyle(element).overflowY);\n\n/**\n * Whether every scroll container from `target` up to the area's (the area itself if it\n * scrolls, else the nearest one above it, or the page) is scrolled to its top, so a downward\n * drag can't scroll anything and pulls instead.\n */\nfunction isAtTop(target: EventTarget | null, root: HTMLElement): boolean {\n let passedRoot = false;\n for (\n let node: Element | null = target instanceof Element ? target : root;\n node;\n node = node.parentElement\n ) {\n if (node.scrollTop >= 1) return false;\n if (node === root) {\n if (isScrollContainer(node)) return true;\n passedRoot = true;\n } else if (passedRoot && isScrollContainer(node)) {\n return true;\n }\n }\n return true;\n}\n\n/**\n * Whether something between `target` and the area handles vertical drags itself (its\n * `touch-action` doesn't allow vertical panning: a vertical slider, a drawing surface), so\n * the gesture is left to it.\n */\nfunction ownsVerticalDrag(target: EventTarget | null, root: HTMLElement): boolean {\n for (\n let node = target instanceof Element ? target : null;\n node && node !== root;\n node = node.parentElement\n ) {\n const touchAction = getComputedStyle(node).touchAction;\n if (touchAction === 'none') return true;\n if (/pan-x/.test(touchAction) && !/pan-y|pan-up|pan-down/.test(touchAction)) return true;\n }\n return false;\n}\n\nconst isThenable = (value: unknown): value is PromiseLike<unknown> =>\n typeof value === 'object' &&\n value !== null &&\n typeof (value as { then?: unknown }).then === 'function';\n\n/**\n * M3 pull to refresh: at the top of its content, a downward drag (touch, mouse or pen) pulls\n * a contained loading indicator in from the top edge, morphing with the pull. Releasing past\n * the threshold calls `onRefresh`; the indicator then rests at the threshold, as an\n * indeterminate loading indicator, until `refreshing` turns `false`. Releasing short of the\n * threshold, or pushing back up, cancels.\n *\n * Wrap a scroll container (give the root `overflow-y-auto` and a height, or put the scrolling\n * element inside it) or a whole page. Scrolling is untouched: the gesture only pulls when the\n * content is at its top and the drag goes down.\n *\n * A `gap-*` on a flex root or `space-y-*` on a block root spaces the content exactly. On a\n * grid root, every in-flow child is a grid item, so the indicator's sticky layer takes a\n * zero-height row of its own and one `row-gap` opens above the content (removing it would\n * mean positioning the layer, which layout safety rules out). Put the grid on an element\n * inside: `<PullToRefresh><div className=\"grid gap-4\">…</div></PullToRefresh>`.\n *\n * Pulling isn't available to everyone, so also offer a refresh button that sets `refreshing`,\n * in an app bar, a menu or next to the content (the spec's accessibility guidance).\n *\n * The motion and sizes follow Compose's `PullToRefreshBox` with\n * `PullToRefreshDefaults.LoadingIndicator`: an 80px threshold, the indicator moving at half\n * the finger's speed and meeting resistance past the threshold, a 48px `primary-container`\n * circle at elevation 0, and a crossfade to the indeterminate indicator while refreshing.\n *\n * @example\n * <PullToRefresh refreshing={loading} onRefresh={reload} className=\"h-dvh overflow-y-auto\">\n * <List aria-label=\"Messages\">…</List>\n * </PullToRefresh>\n */\nexport function PullToRefresh({\n refreshing: refreshingProp,\n onRefresh,\n threshold = POSITIONAL_THRESHOLD,\n disabled = false,\n labels: labelsProp,\n children,\n classNames,\n className,\n ref,\n ...rest\n}: PullToRefreshProps) {\n const labels = useLabels('PullToRefresh', DEFAULT_LABELS, labelsProp);\n const [pendingRefresh, setPendingRefresh] = useState(false);\n const refreshing = refreshingProp ?? pendingRefresh;\n const fraction = useMotionValue(refreshing ? 1 : 0);\n // Settling to the threshold or away never bounces: Compose animates it with\n // `Animatable.animateTo`'s default, non-bouncing spring, which the default effects spring\n // matches (2026-10-15; the fast spatial one dipped 3px under the threshold and back).\n const spring = useM3Spring('effects', 'default');\n // Under reduced motion the indicator jumps to the threshold or away; a pull still moves it.\n const settle = useReducedMotion() ? { duration: 0 } : spring;\n const [pulling, setPulling] = useState(false);\n const [announcement, setAnnouncement] = useState('');\n // The indicator's code (its shapes and morphing) loads on demand: when the browser is first\n // idle, or when a pull starts, whichever comes first.\n usePreloadWhenIdle(preloadLoadingIndicator, !disabled);\n\n const rootRef = useObjectRef(ref);\n const animationRef = useRef<{ stop: () => void } | null>(null);\n const animatingRef = useRef(false);\n const mountedRef = useRef(true);\n // The latest props and helpers, for the native listeners added once.\n const latest = useRef({ refreshing, disabled, threshold, onRefresh, refreshingProp, settle });\n useLayoutEffect(() => {\n latest.current = { refreshing, disabled, threshold, onRefresh, refreshingProp, settle };\n });\n\n // The layer is the root's first child, so a consumer `gap-*` on a flex root would open a gap\n // below it and push the content down. It's cancelled after every render and whenever the\n // root resizes (a responsive class may change the gap).\n const layerRef = useRef<HTMLDivElement>(null);\n useLayoutEffect(() => {\n if (rootRef.current && layerRef.current) cancelRootGap(rootRef.current, layerRef.current);\n });\n useEffect(() => {\n const root = rootRef.current;\n if (!root || typeof ResizeObserver === 'undefined') return;\n const observer = new ResizeObserver(() => {\n if (layerRef.current) cancelRootGap(root, layerRef.current);\n });\n observer.observe(root);\n return () => observer.disconnect();\n }, [rootRef]);\n\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n animationRef.current?.stop();\n };\n }, []);\n\n /** Springs the indicator to `target` (Compose's `animateToThreshold` / `animateToHidden`). */\n const settleRef = useRef((target: number) => {\n animationRef.current?.stop();\n animatingRef.current = true;\n const controls = animate(fraction, target, latest.current.settle);\n animationRef.current = controls;\n void controls.then(() => {\n if (animationRef.current === controls) animatingRef.current = false;\n });\n });\n\n // A refresh starting or ending moves the indicator to the threshold or away, and is\n // announced. The first render places it without either (Compose's `onAttach`).\n const previousRefreshing = useRef(refreshing);\n useEffect(() => {\n if (previousRefreshing.current === refreshing) return;\n previousRefreshing.current = refreshing;\n settleRef.current(refreshing ? 1 : 0);\n setAnnouncement(refreshing ? labels.refreshing : labels.refreshed);\n }, [refreshing, labels.refreshing, labels.refreshed]);\n\n useEffect(() => {\n const root = rootRef.current;\n if (!root) return;\n let gesture: Gesture | null = null;\n let suppressClick = false;\n\n const begin = (\n source: Gesture['source'],\n id: number,\n x: number,\n y: number,\n target: EventTarget | null,\n ) => {\n const { disabled: off } = latest.current;\n gesture =\n off || ownsVerticalDrag(target, root)\n ? null\n : {\n id,\n source,\n startX: x,\n startY: y,\n lastY: y,\n phase: 'tracking',\n atTop: isAtTop(target, root),\n pulled: 0,\n };\n // A pull may start: the indicator's code loads now (if the idle preload hasn't loaded it\n // yet), well before the gesture passes the slop and the indicator shows. A failed load is\n // tried again by the indicator itself.\n if (gesture?.atTop) void preloadLoadingIndicator().catch(() => undefined);\n };\n\n /** Moves the gesture to `y`; returns whether the area took the movement. */\n const move = (x: number, y: number, cancelable: boolean): boolean => {\n const current = gesture;\n if (!current || current.phase === 'native') return false;\n const dy = y - current.lastY;\n current.lastY = y;\n if (current.phase === 'tracking') {\n const totalX = x - current.startX;\n const totalY = y - current.startY;\n const distance = Math.max(Math.abs(totalX), Math.abs(totalY));\n // A mouse or pen must move past the slop first, so a click stays a click.\n if (current.source === 'pointer' && distance <= POINTER_SLOP) return false;\n const downward = totalY > 0 && totalY >= Math.abs(totalX) && current.atTop;\n if (!downward || !cancelable) {\n // Sideways, upward or mid-scroll: the browser's for the rest of the gesture. A\n // touch gets the same slop before it's decided, against a finger's jitter.\n if (!cancelable || distance > POINTER_SLOP) current.phase = 'native';\n return false;\n }\n const { refreshing: busy } = latest.current;\n if (busy || animatingRef.current) {\n current.phase = 'inert';\n return true;\n }\n current.phase = 'pulling';\n // A mouse or pen pull starts at the slop, so the indicator doesn't jump.\n current.pulled = Math.max(0, totalY - (current.source === 'pointer' ? POINTER_SLOP : 0));\n setPulling(true);\n fraction.set(distanceFraction(current.pulled, latest.current.threshold));\n return true;\n }\n if (current.phase === 'inert') return true;\n // Pulling: up and down both move the indicator (Compose's pre- and post-scroll).\n current.pulled = Math.max(0, current.pulled + dy);\n fraction.set(distanceFraction(current.pulled, latest.current.threshold));\n if (current.pulled === 0) {\n // Pushed all the way back: the gesture may scroll the content again.\n current.phase = 'tracking';\n current.startX = x;\n current.startY = y;\n current.atTop = true;\n setPulling(false);\n }\n return true;\n };\n\n /** Ends the gesture (Compose's `onRelease`); `cancel` never refreshes. */\n const end = (cancel: boolean) => {\n const current = gesture;\n gesture = null;\n if (!current || current.phase !== 'pulling') return;\n setPulling(false);\n if (current.source === 'pointer') {\n // A mouse pull ends with a click on whatever is under the pointer; it isn't one. The\n // click comes before any timer.\n suppressClick = true;\n setTimeout(() => {\n suppressClick = false;\n });\n }\n const { threshold: limit, refreshingProp: controlled } = latest.current;\n if (!cancel && passesThreshold(current.pulled, limit)) {\n const result = latest.current.onRefresh();\n if (controlled === undefined && isThenable(result)) {\n setPendingRefresh(true);\n const done = () => {\n if (mountedRef.current) setPendingRefresh(false);\n };\n result.then(done, done);\n }\n }\n // Compose hides the indicator here; a refresh that starts moves it to the threshold.\n settleRef.current(0);\n };\n\n const touchOf = (event: TouchEvent) =>\n gesture === null\n ? undefined\n : [...event.changedTouches].find((touch) => touch.identifier === gesture?.id);\n\n const onTouchStart = (event: TouchEvent) => {\n if (event.touches.length > 1) {\n // A second finger (pinch to zoom) ends the pull without refreshing.\n end(true);\n return;\n }\n const touch = event.changedTouches[0];\n if (touch) begin('touch', touch.identifier, touch.clientX, touch.clientY, event.target);\n };\n const onTouchMove = (event: TouchEvent) => {\n const touch = touchOf(event);\n if (!touch || event.defaultPrevented) return;\n if (move(touch.clientX, touch.clientY, event.cancelable) && event.cancelable) {\n event.preventDefault();\n }\n };\n const onTouchEnd = (event: TouchEvent) => {\n if (touchOf(event)) end(event.type === 'touchcancel');\n };\n\n const onPointerDown = (event: PointerEvent) => {\n if (event.pointerType === 'touch' || event.button !== 0 || !event.isPrimary) return;\n begin('pointer', event.pointerId, event.clientX, event.clientY, event.target);\n };\n const onPointerMove = (event: PointerEvent) => {\n if (gesture?.source !== 'pointer' || event.pointerId !== gesture.id) return;\n const wasPulling = gesture.phase === 'pulling';\n if (!move(event.clientX, event.clientY, true)) return;\n event.preventDefault();\n if (!wasPulling && gesture?.phase === 'pulling') {\n root.setPointerCapture?.(event.pointerId);\n // A mouse drag would otherwise select the text it passes.\n window.getSelection()?.removeAllRanges();\n }\n };\n const onPointerEnd = (event: PointerEvent) => {\n if (gesture?.source !== 'pointer' || event.pointerId !== gesture.id) return;\n end(event.type === 'pointercancel');\n };\n const onClick = (event: MouseEvent) => {\n if (!suppressClick) return;\n suppressClick = false;\n event.preventDefault();\n event.stopPropagation();\n };\n\n const options = { passive: false } as const;\n root.addEventListener('touchstart', onTouchStart, { passive: true });\n root.addEventListener('touchmove', onTouchMove, options);\n root.addEventListener('touchend', onTouchEnd);\n root.addEventListener('touchcancel', onTouchEnd);\n root.addEventListener('pointerdown', onPointerDown);\n root.addEventListener('pointermove', onPointerMove);\n root.addEventListener('pointerup', onPointerEnd);\n root.addEventListener('pointercancel', onPointerEnd);\n root.addEventListener('click', onClick, { capture: true });\n return () => {\n root.removeEventListener('touchstart', onTouchStart);\n root.removeEventListener('touchmove', onTouchMove);\n root.removeEventListener('touchend', onTouchEnd);\n root.removeEventListener('touchcancel', onTouchEnd);\n root.removeEventListener('pointerdown', onPointerDown);\n root.removeEventListener('pointermove', onPointerMove);\n root.removeEventListener('pointerup', onPointerEnd);\n root.removeEventListener('pointercancel', onPointerEnd);\n root.removeEventListener('click', onClick, { capture: true });\n };\n }, [fraction, rootRef]);\n\n const styles = pullToRefreshStyles();\n\n return (\n <div\n {...rest}\n ref={rootRef}\n data-pulling={pulling || undefined}\n data-refreshing={refreshing || undefined}\n data-disabled={disabled || undefined}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n <div\n ref={layerRef}\n className={styles.layer({ class: classNames?.layer })}\n style={{ '--m3-pull-to-refresh-threshold': `${threshold}px` } as CSSProperties}\n >\n <div className={styles.clip()}>\n <PullIndicator\n fraction={fraction}\n refreshing={refreshing}\n threshold={threshold}\n labels={labels}\n className={styles.indicator({ class: classNames?.indicator })}\n faceClassName={styles.face()}\n />\n </div>\n <span role=\"status\" className=\"sr-only\">\n {announcement}\n </span>\n </div>\n {children}\n </div>\n );\n}\n\n/**\n * Keeps the root's own spacing from opening below the zero-height layer. Its margins are the\n * library's: zero, so a `space-y-*` root doesn't push the content down, and in a flex root a\n * negative margin as large as the gap along the main axis. In a grid root it moves to an\n * implicit row of its own before the first one, across every column, so the content keeps its\n * rows and columns; a grid row's size can't go below zero, so one row gap stays above the\n * content (documented: put the grid on an element inside the root).\n */\nfunction cancelRootGap(root: HTMLElement, layer: HTMLElement) {\n const style = getComputedStyle(root);\n const grid = style.display.endsWith('grid');\n const flex = style.display.endsWith('flex');\n const row = flex && style.flexDirection.startsWith('row');\n const reverse = flex && style.flexDirection.endsWith('reverse');\n const gap = Number.parseFloat(row ? style.columnGap : style.rowGap);\n for (const side of ['BlockStart', 'BlockEnd', 'InlineStart', 'InlineEnd'] as const) {\n layer.style[`margin${side}`] = '0px';\n }\n if (flex && gap > 0) {\n layer.style[`margin${row ? 'Inline' : 'Block'}${reverse ? 'Start' : 'End'}`] = `${-gap}px`;\n }\n layer.style.gridRow = grid ? 'span 1 / 1' : '';\n layer.style.gridColumn = grid ? '1 / -1' : '';\n}\n\n/**\n * The indicator, subscribed to the pull on its own so a pull re-renders only it, never the\n * content. Hidden at rest; while pulled it is a determinate loading indicator following the\n * pull, and while refreshing an indeterminate one, crossfading between them.\n */\nfunction PullIndicator({\n fraction,\n refreshing,\n threshold,\n labels,\n className,\n faceClassName,\n}: {\n fraction: MotionValue<number>;\n refreshing: boolean;\n threshold: number;\n labels: PullToRefreshLabels;\n className: string;\n faceClassName: string;\n}) {\n const [value, setValue] = useState(() => fraction.get());\n useMotionValueEvent(fraction, 'change', setValue);\n if (value <= 0 && !refreshing) return null;\n const rotation = overshootRotation(value);\n return (\n <div\n data-indicator=\"\"\n className={className}\n // Compose's `translationY = distanceFraction × maxDistance − height`.\n style={{ transform: `translateY(calc(${value * threshold}px - 100%))` }}\n >\n <div\n data-active={!refreshing}\n aria-hidden={refreshing || undefined}\n className={faceClassName}\n style={rotation === 0 ? undefined : { transform: `rotate(${rotation}deg)` }}\n >\n <LazyLoadingIndicator\n variant=\"contained\"\n value={Math.min(Math.max(value, 0), 1)}\n aria-label={labels.pull}\n />\n </div>\n <div\n data-active={refreshing}\n aria-hidden={!refreshing || undefined}\n className={faceClassName}\n >\n <LazyLoadingIndicator variant=\"contained\" aria-label={labels.refreshing} />\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -82,7 +82,10 @@ interface SearchFieldOwnProps extends WrapperRootDOMProps {
|
|
|
82
82
|
* its role, name, value and type always win.
|
|
83
83
|
*/
|
|
84
84
|
inputProps?: SearchFieldInputProps;
|
|
85
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* Words the field shows or announces, in place of the English defaults. A {@link LabelsProvider}
|
|
87
|
+
* sets them for the whole app.
|
|
88
|
+
*/
|
|
86
89
|
labels?: Partial<SearchFieldLabels>;
|
|
87
90
|
/** Ref to the root `<div>` element. */
|
|
88
91
|
ref?: Ref<HTMLDivElement>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { densityAttribute } from "../../tokens/density.js";
|
|
3
3
|
import { cn } from "../../utils/cn.js";
|
|
4
|
+
import { useLabels } from "../labels/use-labels.js";
|
|
4
5
|
import { ButtonContext, standardIconButtons } from "../button/ButtonContext.js";
|
|
5
6
|
import { IconButton } from "../icon-button/IconButton.js";
|
|
6
7
|
import { pickWidgetAria, splitRootProps } from "../../utils/root-props.js";
|
|
@@ -33,10 +34,7 @@ const INTERACTIVE = "input, button, a, [role=\"button\"]";
|
|
|
33
34
|
*/
|
|
34
35
|
function SearchField(props) {
|
|
35
36
|
const { value, defaultValue, onChange, onSubmit, placeholder, leadingIcon, trailingIcon, clearIcon, density, disabled = false, autoFocus, inputRef, inputProps: consumerInputProps, labels: labelsProp, ref, className, style, classNames, ...rest } = props;
|
|
36
|
-
const labels =
|
|
37
|
-
...DEFAULT_LABELS,
|
|
38
|
-
...labelsProp
|
|
39
|
-
};
|
|
37
|
+
const labels = useLabels("SearchField", DEFAULT_LABELS, labelsProp);
|
|
40
38
|
const [query, setQuery] = useControlledState(value, defaultValue ?? "", onChange);
|
|
41
39
|
const objectInputRef = useObjectRef(inputRef);
|
|
42
40
|
const { root: data, rest: own } = splitRootProps(rest, { wrapper: true });
|