@windmorph/react 0.2.3 → 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.
Files changed (119) hide show
  1. package/dist/components/autocomplete/Autocomplete.d.ts +4 -1
  2. package/dist/components/autocomplete/Autocomplete.js +2 -4
  3. package/dist/components/autocomplete/Autocomplete.js.map +1 -1
  4. package/dist/components/badge/Badge.d.ts +5 -2
  5. package/dist/components/badge/Badge.js +7 -3
  6. package/dist/components/badge/Badge.js.map +1 -1
  7. package/dist/components/button/Button.d.ts +4 -2
  8. package/dist/components/button/Button.js.map +1 -1
  9. package/dist/components/button/ButtonContext.d.ts +1 -1
  10. package/dist/components/button/button-pending.js +2 -4
  11. package/dist/components/button/button-pending.js.map +1 -1
  12. package/dist/{composites → components}/button-group/ButtonGroup.d.ts +4 -4
  13. package/dist/{composites → components}/button-group/ButtonGroup.js +9 -7
  14. package/dist/components/button-group/ButtonGroup.js.map +1 -0
  15. package/dist/{composites → components}/button-group/button-group-styles.d.ts +1 -1
  16. package/dist/{composites → components}/button-group/button-group-styles.js +1 -1
  17. package/dist/{composites → components}/button-group/button-group-styles.js.map +1 -1
  18. package/dist/{composites → components}/button-group/lazy-overflow-menu.js +2 -2
  19. package/dist/components/button-group/lazy-overflow-menu.js.map +1 -0
  20. package/dist/{composites → components}/button-group/overflow-menu-list.js +2 -2
  21. package/dist/components/button-group/overflow-menu-list.js.map +1 -0
  22. package/dist/{composites → components}/button-group/use-overflow.js +1 -1
  23. package/dist/{composites → components}/button-group/use-overflow.js.map +1 -1
  24. package/dist/{composites → components}/button-group/use-press-expansion.js +1 -1
  25. package/dist/{composites → components}/button-group/use-press-expansion.js.map +1 -1
  26. package/dist/components/carousel/Carousel.d.ts +4 -1
  27. package/dist/components/carousel/Carousel.js +2 -4
  28. package/dist/components/carousel/Carousel.js.map +1 -1
  29. package/dist/components/checkbox/checkbox-styles.d.ts +3 -3
  30. package/dist/components/checkbox/checkbox-tokens.d.ts +1 -1
  31. package/dist/components/chip/Chip.d.ts +8 -2
  32. package/dist/components/chip/Chip.js +3 -8
  33. package/dist/components/chip/Chip.js.map +1 -1
  34. package/dist/components/date-picker/DateField.d.ts +4 -1
  35. package/dist/components/date-picker/DateField.js +3 -1
  36. package/dist/components/date-picker/DateField.js.map +1 -1
  37. package/dist/components/date-picker/DatePicker.d.ts +4 -1
  38. package/dist/components/date-picker/DatePicker.js +5 -2
  39. package/dist/components/date-picker/DatePicker.js.map +1 -1
  40. package/dist/components/date-picker/PickerDialog.d.ts +4 -1
  41. package/dist/components/date-picker/PickerDialog.js +3 -1
  42. package/dist/components/date-picker/PickerDialog.js.map +1 -1
  43. package/dist/components/date-picker/date-picker-styles.d.ts +15 -15
  44. package/dist/components/dialog/Dialog.d.ts +5 -2
  45. package/dist/components/dialog/Dialog.js +4 -2
  46. package/dist/components/dialog/Dialog.js.map +1 -1
  47. package/dist/components/icon-button/IconButton.d.ts +4 -2
  48. package/dist/components/icon-button/IconButton.js.map +1 -1
  49. package/dist/components/labels/LabelsProvider.d.ts +35 -0
  50. package/dist/components/labels/LabelsProvider.js +47 -0
  51. package/dist/components/labels/LabelsProvider.js.map +1 -0
  52. package/dist/components/labels/use-labels.d.ts +16 -0
  53. package/dist/components/labels/use-labels.js +42 -0
  54. package/dist/components/labels/use-labels.js.map +1 -0
  55. package/dist/components/menu/Menu.d.ts +4 -1
  56. package/dist/components/menu/Menu.js +7 -10
  57. package/dist/components/menu/Menu.js.map +1 -1
  58. package/dist/components/menu/menu-styles.js +12 -12
  59. package/dist/components/menu/menu-styles.js.map +1 -1
  60. package/dist/components/menu/menu-tokens.js.map +1 -1
  61. package/dist/components/navigation/NavItem.d.ts +1 -1
  62. package/dist/components/navigation/NavItem.js +9 -6
  63. package/dist/components/navigation/NavItem.js.map +1 -1
  64. package/dist/components/pull-to-refresh/PullToRefresh.d.ts +1 -1
  65. package/dist/components/pull-to-refresh/PullToRefresh.js +2 -4
  66. package/dist/components/pull-to-refresh/PullToRefresh.js.map +1 -1
  67. package/dist/components/search/SearchField.d.ts +4 -1
  68. package/dist/components/search/SearchField.js +2 -4
  69. package/dist/components/search/SearchField.js.map +1 -1
  70. package/dist/components/select/Select.d.ts +4 -1
  71. package/dist/components/select/Select.js +2 -4
  72. package/dist/components/select/Select.js.map +1 -1
  73. package/dist/components/sheet/BottomSheet.d.ts +4 -1
  74. package/dist/components/sheet/BottomSheet.js +2 -4
  75. package/dist/components/sheet/BottomSheet.js.map +1 -1
  76. package/dist/components/sheet/SideSheet.d.ts +4 -1
  77. package/dist/components/sheet/SideSheet.js +2 -4
  78. package/dist/components/sheet/SideSheet.js.map +1 -1
  79. package/dist/components/slider/Slider.d.ts +4 -1
  80. package/dist/components/slider/Slider.js +2 -4
  81. package/dist/components/slider/Slider.js.map +1 -1
  82. package/dist/components/snackbar/Snackbar.d.ts +8 -2
  83. package/dist/components/snackbar/Snackbar.js +2 -4
  84. package/dist/components/snackbar/Snackbar.js.map +1 -1
  85. package/dist/components/snackbar/snackbar-tokens.d.ts +1 -1
  86. package/dist/components/split-button/SplitButton.d.ts +4 -1
  87. package/dist/components/split-button/SplitButton.js +4 -2
  88. package/dist/components/split-button/SplitButton.js.map +1 -1
  89. package/dist/components/switch/switch-styles.d.ts +3 -3
  90. package/dist/components/text-field/TextField.d.ts +4 -1
  91. package/dist/components/text-field/TextField.js +2 -4
  92. package/dist/components/text-field/TextField.js.map +1 -1
  93. package/dist/components/time-picker/TimeField.d.ts +24 -2
  94. package/dist/components/time-picker/TimeField.js +40 -12
  95. package/dist/components/time-picker/TimeField.js.map +1 -1
  96. package/dist/components/time-picker/TimeFieldList.js +95 -0
  97. package/dist/components/time-picker/TimeFieldList.js.map +1 -0
  98. package/dist/components/time-picker/TimePicker.d.ts +9 -5
  99. package/dist/components/time-picker/TimePicker.js +5 -2
  100. package/dist/components/time-picker/TimePicker.js.map +1 -1
  101. package/dist/components/time-picker/time-list.js +33 -0
  102. package/dist/components/time-picker/time-list.js.map +1 -0
  103. package/dist/components/tooltip/Tooltip.d.ts +1 -1
  104. package/dist/index.d.ts +40 -38
  105. package/dist/index.js +4 -3
  106. package/dist/primitives.d.ts +7 -4
  107. package/dist/primitives.js +4 -1
  108. package/dist/styles.compiled.css +1 -1
  109. package/dist/utils/use-scroll-keys.d.ts +29 -0
  110. package/dist/utils/use-scroll-keys.js +66 -0
  111. package/dist/utils/use-scroll-keys.js.map +1 -0
  112. package/dist/utils/use-scroll-restoration.d.ts +43 -0
  113. package/dist/utils/use-scroll-restoration.js +173 -0
  114. package/dist/utils/use-scroll-restoration.js.map +1 -0
  115. package/dist/windmorph-labels.d.ts +101 -0
  116. package/package.json +1 -1
  117. package/dist/composites/button-group/ButtonGroup.js.map +0 -1
  118. package/dist/composites/button-group/lazy-overflow-menu.js.map +0 -1
  119. package/dist/composites/button-group/overflow-menu-list.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"button-group-styles.js","names":[],"sources":["../../../src/composites/button-group/button-group-styles.ts"],"sourcesContent":["import { tv } from '../../utils/tv';\n\n/**\n * Variant definitions for `ButtonGroup`, from the M3 Expressive button group specs\n * (2026-10-13). Standard groups space their buttons 18 / 12 / 8 / 8 / 8px apart (XS–XL), so\n * the XS and S buttons' 48px targets don't overlap. Connected groups are 2px apart at every\n * size, and span their container with the buttons sharing the extra width (\"The connected\n * button group should span the width of the page or surface it's placed on\"); a consumer\n * `w-fit` (or a `max-w-*`) narrows it. Connected buttons are equally wide (2026-10-13, as\n * Compose's connected sample gives each `weight(1f)`): the group is a grid of `1fr` columns\n * (2026-10-15), so a definite width is shared equally, and a group sized by its content gives\n * every button the widest one's natural width, never squeezing a longer label into its\n * padding. The columns are `minmax(auto, 1fr)`, so with too little room a button shrinks no\n * further than its own minimum width, as in a flex row. `auto-cols-auto` on the group sizes\n * them by their content instead. While the press\n * bump pins the buttons' widths (`data-pinned`), the group lays them out as a flex row of the\n * same widths, so the bump can animate their `flex-basis`.\n *\n * With `overflow`, a standard group fills its container's width too, so it can measure the\n * room it has; its buttons stay at the start. Buttons folded into the overflow menu carry\n * `data-overflow-hidden`; while measuring, the group is a flex row of its content's width\n * and no button grows.\n */\nexport const buttonGroupStyles = tv({\n base: 'items-center',\n variants: {\n variant: {\n standard: 'inline-flex',\n connected: 'grid grid-flow-col auto-cols-[1fr] gap-[2px] data-pinned:flex',\n },\n size: { xs: '', sm: '', md: '', lg: '', xl: '' },\n fullWidth: { true: '', false: '' },\n overflow: {\n true: 'w-full min-w-0 *:data-overflow-hidden:hidden data-measuring:flex data-measuring:w-max data-measuring:*:grow-0',\n false: '',\n },\n },\n compoundVariants: [\n { variant: 'connected', fullWidth: true, class: 'w-full' },\n { variant: 'connected', fullWidth: false, overflow: false, class: 'inline-grid w-fit' },\n { variant: 'standard', overflow: true, class: 'flex' },\n // XS and S: the spec's 18 / 12px, growing as density shrinks the icon buttons (their\n // default widths: XS max(24px, 32px + 4px a step), S 40px + 4px a step), so neighbouring\n // centres stay 48px apart and their 48px touch targets never overlap (2026-10-15). A\n // group holding a narrow icon button spaces by the narrow width instead (XS max(24px,\n // 28px + 4px a step), S max(28px, 32px + 4px a step), M 48px + 4px a step), so the gap is\n // max(spec, 48px − narrow width) (2026-10-17). The gap is a variable, so a consumer `gap-*`\n // still replaces it.\n {\n variant: 'standard',\n size: 'xs',\n class: [\n 'gap-(--m3-button-group-gap)',\n '[--m3-button-group-gap:max(18px,calc(48px-max(24px,calc(32px+var(--md-sys-density,0)*4px))))]',\n 'has-[[data-tokens~=md-comp-icon-button][data-width=narrow]]:[--m3-button-group-gap:max(18px,calc(48px-max(24px,calc(28px+var(--md-sys-density,0)*4px))))]',\n ],\n },\n {\n variant: 'standard',\n size: 'sm',\n class: [\n 'gap-(--m3-button-group-gap)',\n '[--m3-button-group-gap:max(12px,calc(8px-var(--md-sys-density,0)*4px))]',\n 'has-[[data-tokens~=md-comp-icon-button][data-width=narrow]]:[--m3-button-group-gap:max(12px,calc(48px-max(28px,calc(32px+var(--md-sys-density,0)*4px))))]',\n ],\n },\n {\n variant: 'standard',\n size: 'md',\n class: [\n 'gap-(--m3-button-group-gap) [--m3-button-group-gap:8px]',\n 'has-[[data-tokens~=md-comp-icon-button][data-width=narrow]]:[--m3-button-group-gap:max(8px,calc(var(--md-sys-density,0)*-4px))]',\n ],\n },\n { variant: 'standard', size: ['lg', 'xl'], class: 'gap-[8px]' },\n ],\n defaultVariants: { variant: 'standard', size: 'sm', overflow: false },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAuBA,MAAa,oBAAoB,GAAG;CAClC,MAAM;CACN,UAAU;EACR,SAAS;GACP,UAAU;GACV,WAAW;EACb;EACA,MAAM;GAAE,IAAI;GAAI,IAAI;GAAI,IAAI;GAAI,IAAI;GAAI,IAAI;EAAG;EAC/C,WAAW;GAAE,MAAM;GAAI,OAAO;EAAG;EACjC,UAAU;GACR,MAAM;GACN,OAAO;EACT;CACF;CACA,kBAAkB;EAChB;GAAE,SAAS;GAAa,WAAW;GAAM,OAAO;EAAS;EACzD;GAAE,SAAS;GAAa,WAAW;GAAO,UAAU;GAAO,OAAO;EAAoB;EACtF;GAAE,SAAS;GAAY,UAAU;GAAM,OAAO;EAAO;EAQrD;GACE,SAAS;GACT,MAAM;GACN,OAAO;IACL;IACA;IACA;GACF;EACF;EACA;GACE,SAAS;GACT,MAAM;GACN,OAAO;IACL;IACA;IACA;GACF;EACF;EACA;GACE,SAAS;GACT,MAAM;GACN,OAAO,CACL,2DACA,iIACF;EACF;EACA;GAAE,SAAS;GAAY,MAAM,CAAC,MAAM,IAAI;GAAG,OAAO;EAAY;CAChE;CACA,iBAAiB;EAAE,SAAS;EAAY,MAAM;EAAM,UAAU;CAAM;AACtE,CAAC"}
1
+ {"version":3,"file":"button-group-styles.js","names":[],"sources":["../../../src/components/button-group/button-group-styles.ts"],"sourcesContent":["import { tv } from '../../utils/tv';\n\n/**\n * Variant definitions for `ButtonGroup`, from the M3 Expressive button group specs\n * (2026-10-13). Standard groups space their buttons 18 / 12 / 8 / 8 / 8px apart (XS–XL), so\n * the XS and S buttons' 48px targets don't overlap. Connected groups are 2px apart at every\n * size, and span their container with the buttons sharing the extra width (\"The connected\n * button group should span the width of the page or surface it's placed on\"); a consumer\n * `w-fit` (or a `max-w-*`) narrows it. Connected buttons are equally wide (2026-10-13, as\n * Compose's connected sample gives each `weight(1f)`): the group is a grid of `1fr` columns\n * (2026-10-15), so a definite width is shared equally, and a group sized by its content gives\n * every button the widest one's natural width, never squeezing a longer label into its\n * padding. The columns are `minmax(auto, 1fr)`, so with too little room a button shrinks no\n * further than its own minimum width, as in a flex row. `auto-cols-auto` on the group sizes\n * them by their content instead. While the press\n * bump pins the buttons' widths (`data-pinned`), the group lays them out as a flex row of the\n * same widths, so the bump can animate their `flex-basis`.\n *\n * With `overflow`, a standard group fills its container's width too, so it can measure the\n * room it has; its buttons stay at the start. Buttons folded into the overflow menu carry\n * `data-overflow-hidden`; while measuring, the group is a flex row of its content's width\n * and no button grows.\n */\nexport const buttonGroupStyles = tv({\n base: 'items-center',\n variants: {\n variant: {\n standard: 'inline-flex',\n connected: 'grid grid-flow-col auto-cols-[1fr] gap-[2px] data-pinned:flex',\n },\n size: { xs: '', sm: '', md: '', lg: '', xl: '' },\n fullWidth: { true: '', false: '' },\n overflow: {\n true: 'w-full min-w-0 *:data-overflow-hidden:hidden data-measuring:flex data-measuring:w-max data-measuring:*:grow-0',\n false: '',\n },\n },\n compoundVariants: [\n { variant: 'connected', fullWidth: true, class: 'w-full' },\n { variant: 'connected', fullWidth: false, overflow: false, class: 'inline-grid w-fit' },\n { variant: 'standard', overflow: true, class: 'flex' },\n // XS and S: the spec's 18 / 12px, growing as density shrinks the icon buttons (their\n // default widths: XS max(24px, 32px + 4px a step), S 40px + 4px a step), so neighbouring\n // centres stay 48px apart and their 48px touch targets never overlap (2026-10-15). A\n // group holding a narrow icon button spaces by the narrow width instead (XS max(24px,\n // 28px + 4px a step), S max(28px, 32px + 4px a step), M 48px + 4px a step), so the gap is\n // max(spec, 48px − narrow width) (2026-10-17). The gap is a variable, so a consumer `gap-*`\n // still replaces it.\n {\n variant: 'standard',\n size: 'xs',\n class: [\n 'gap-(--m3-button-group-gap)',\n '[--m3-button-group-gap:max(18px,calc(48px-max(24px,calc(32px+var(--md-sys-density,0)*4px))))]',\n 'has-[[data-tokens~=md-comp-icon-button][data-width=narrow]]:[--m3-button-group-gap:max(18px,calc(48px-max(24px,calc(28px+var(--md-sys-density,0)*4px))))]',\n ],\n },\n {\n variant: 'standard',\n size: 'sm',\n class: [\n 'gap-(--m3-button-group-gap)',\n '[--m3-button-group-gap:max(12px,calc(8px-var(--md-sys-density,0)*4px))]',\n 'has-[[data-tokens~=md-comp-icon-button][data-width=narrow]]:[--m3-button-group-gap:max(12px,calc(48px-max(28px,calc(32px+var(--md-sys-density,0)*4px))))]',\n ],\n },\n {\n variant: 'standard',\n size: 'md',\n class: [\n 'gap-(--m3-button-group-gap) [--m3-button-group-gap:8px]',\n 'has-[[data-tokens~=md-comp-icon-button][data-width=narrow]]:[--m3-button-group-gap:max(8px,calc(var(--md-sys-density,0)*-4px))]',\n ],\n },\n { variant: 'standard', size: ['lg', 'xl'], class: 'gap-[8px]' },\n ],\n defaultVariants: { variant: 'standard', size: 'sm', overflow: false },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAuBA,MAAa,oBAAoB,GAAG;CAClC,MAAM;CACN,UAAU;EACR,SAAS;GACP,UAAU;GACV,WAAW;EACb;EACA,MAAM;GAAE,IAAI;GAAI,IAAI;GAAI,IAAI;GAAI,IAAI;GAAI,IAAI;EAAG;EAC/C,WAAW;GAAE,MAAM;GAAI,OAAO;EAAG;EACjC,UAAU;GACR,MAAM;GACN,OAAO;EACT;CACF;CACA,kBAAkB;EAChB;GAAE,SAAS;GAAa,WAAW;GAAM,OAAO;EAAS;EACzD;GAAE,SAAS;GAAa,WAAW;GAAO,UAAU;GAAO,OAAO;EAAoB;EACtF;GAAE,SAAS;GAAY,UAAU;GAAM,OAAO;EAAO;EAQrD;GACE,SAAS;GACT,MAAM;GACN,OAAO;IACL;IACA;IACA;GACF;EACF;EACA;GACE,SAAS;GACT,MAAM;GACN,OAAO;IACL;IACA;IACA;GACF;EACF;EACA;GACE,SAAS;GACT,MAAM;GACN,OAAO,CACL,2DACA,iIACF;EACF;EACA;GAAE,SAAS;GAAY,MAAM,CAAC,MAAM,IAAI;GAAG,OAAO;EAAY;CAChE;CACA,iBAAiB;EAAE,SAAS;EAAY,MAAM;EAAM,UAAU;CAAM;AACtE,CAAC"}
@@ -1,9 +1,9 @@
1
1
  "use client";
2
2
  import { onDemand } from "../../utils/load-on-demand.js";
3
- import { MenuTriggerContext } from "../../components/menu/menu-trigger.js";
3
+ import { MenuTriggerContext } from "../menu/menu-trigger.js";
4
4
  import { useContext, useEffect, useState } from "react";
5
5
  import { jsx } from "react/jsx-runtime";
6
- //#region src/composites/button-group/lazy-overflow-menu.tsx
6
+ //#region src/components/button-group/lazy-overflow-menu.tsx
7
7
  const menu = onDemand(() => import("./overflow-menu-list.js").then((module) => module.OverflowMenuList));
8
8
  menu.preload;
9
9
  /**
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lazy-overflow-menu.js","names":[],"sources":["../../../src/components/button-group/lazy-overflow-menu.tsx"],"sourcesContent":["'use client';\n\nimport { useContext, useEffect, useState } from 'react';\nimport { MenuTriggerContext } from '../menu/menu-trigger';\nimport { onDemand } from '../../utils/load-on-demand';\nimport type { OverflowMenuListProps } from './overflow-menu-list';\n\nconst menu = onDemand<OverflowMenuListProps>(() =>\n import('./overflow-menu-list').then((module) => module.OverflowMenuList),\n);\n\n/**\n * Starts loading the overflow menu's module (and `Menu` with it), once; later calls return\n * the same promise.\n */\nexport const preloadOverflowMenu = menu.preload;\n\n/**\n * `ButtonGroup`'s overflow menu, whose code loads only once a button folds: a group without\n * `overflow`, or whose buttons all fit, never carries `Menu`. It sits inside the indicator's\n * `MenuTrigger` and is rendered only while something is folded. Rendering it starts the\n * load, so the menu is usually ready long before the indicator is pressed, and then opens in\n * the same pass as the press (it never suspends, see `onDemand`). It mounts once loaded, or\n * when the menu opens if that comes first, and shows nothing until then. A failed load is\n * tried again when the pointer moves over the indicator, focus enters it, or the menu opens.\n */\nexport function LazyOverflowMenuList(props: OverflowMenuListProps) {\n const trigger = useContext(MenuTriggerContext);\n const isOpen = trigger?.state.isOpen === true;\n const [ready, setReady] = useState(isOpen);\n // Set during render, so an open before the load ends renders the menu in the same pass.\n if (isOpen && !ready) setReady(true);\n\n const triggerRef = trigger?.triggerRef;\n useEffect(() => {\n let mounted = true;\n // A failed preload is left to the next hover, focus or open, which loads again.\n const preload = () =>\n void menu.preload().then(\n () => mounted && setReady(true),\n () => undefined,\n );\n preload();\n const element = triggerRef?.current;\n element?.addEventListener('pointerover', preload);\n element?.addEventListener('focusin', preload);\n return () => {\n mounted = false;\n element?.removeEventListener('pointerover', preload);\n element?.removeEventListener('focusin', preload);\n };\n }, [triggerRef]);\n\n if (!ready) return null;\n return <menu.Component {...props} />;\n}\n"],"mappings":";;;;;;AAOA,MAAM,OAAO,eACX,OAAO,0BAAuB,CAAC,MAAM,WAAW,OAAO,gBAAgB,CACzE;AAMmC,KAAK;;;;;;;;;;AAWxC,SAAgB,qBAAqB,OAA8B;CACjE,MAAM,UAAU,WAAW,kBAAkB;CAC7C,MAAM,SAAS,SAAS,MAAM,WAAW;CACzC,MAAM,CAAC,OAAO,YAAY,SAAS,MAAM;CAEzC,IAAI,UAAU,CAAC,OAAO,SAAS,IAAI;CAEnC,MAAM,aAAa,SAAS;CAC5B,gBAAgB;EACd,IAAI,UAAU;EAEd,MAAM,gBACJ,KAAK,KAAK,QAAQ,CAAC,CAAC,WACZ,WAAW,SAAS,IAAI,SACxB,KAAA,CACR;EACF,QAAQ;EACR,MAAM,UAAU,YAAY;EAC5B,SAAS,iBAAiB,eAAe,OAAO;EAChD,SAAS,iBAAiB,WAAW,OAAO;EAC5C,aAAa;GACX,UAAU;GACV,SAAS,oBAAoB,eAAe,OAAO;GACnD,SAAS,oBAAoB,WAAW,OAAO;EACjD;CACF,GAAG,CAAC,UAAU,CAAC;CAEf,IAAI,CAAC,OAAO,OAAO;CACnB,OAAO,oBAAC,KAAK,WAAN,EAAgB,GAAI,MAAQ,CAAA;AACrC"}
@@ -1,7 +1,7 @@
1
1
  "use client";
2
- import { CheckableMenuItem, Menu } from "../../components/menu/Menu.js";
2
+ import { CheckableMenuItem, Menu } from "../menu/Menu.js";
3
3
  import { jsx } from "react/jsx-runtime";
4
- //#region src/composites/button-group/overflow-menu-list.tsx
4
+ //#region src/components/button-group/overflow-menu-list.tsx
5
5
  /**
6
6
  * The overflow indicator's menu: one item per folded button, opening at the indicator's
7
7
  * end. In a selectable menu only toggles (and buttons in the group's selection) are
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overflow-menu-list.js","names":[],"sources":["../../../src/components/button-group/overflow-menu-list.tsx"],"sourcesContent":["'use client';\n\nimport type { ReactNode } from 'react';\nimport { CheckableMenuItem, Menu, type MenuProps } from '../menu/Menu';\n\n/** A folded button as a menu item. */\nexport interface OverflowMenuItem {\n /** The button's index among the group's children, as a string. */\n key: string;\n label: ReactNode;\n icon: ReactNode;\n /**\n * Whether the item shows and announces a checked state: a toggle button, or a button in\n * the group's selection. Actions stay plain menu items in a selectable menu.\n */\n checkable: boolean;\n}\n\nexport interface OverflowMenuListProps extends Pick<\n MenuProps,\n 'selectionMode' | 'selectedKeys' | 'disabledKeys' | 'onAction' | 'classNames'\n> {\n /** The folded buttons, in order. */\n items: OverflowMenuItem[];\n}\n\n/**\n * The overflow indicator's menu: one item per folded button, opening at the indicator's\n * end. In a selectable menu only toggles (and buttons in the group's selection) are\n * checkable; actions stay plain items. `ButtonGroup` loads this module (and `Menu` with it) only once a button folds,\n * through `LazyOverflowMenuList`.\n */\nexport function OverflowMenuList({ items, ...props }: OverflowMenuListProps) {\n return (\n <Menu {...props} placement=\"bottom-end\">\n {items.map(({ key, label, icon, checkable }) => (\n <CheckableMenuItem\n key={key}\n leadingIcon={icon}\n checkable={checkable}\n textValue={typeof label === 'string' ? label : undefined}\n >\n {label}\n </CheckableMenuItem>\n ))}\n </Menu>\n );\n}\n"],"mappings":";;;;;;;;;;AAgCA,SAAgB,iBAAiB,EAAE,OAAO,GAAG,SAAgC;CAC3E,OACE,oBAAC,MAAD;EAAM,GAAI;EAAO,WAAU;EACxB,UAAA,MAAM,KAAK,EAAE,KAAK,OAAO,MAAM,gBAC9B,oBAAC,mBAAD;GAEE,aAAa;GACF;GACX,WAAW,OAAO,UAAU,WAAW,QAAQ,KAAA;GAE9C,UAAA;EACgB,GANZ,GAMY,CACpB;CACG,CAAA;AAEV"}
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { useLayoutEffect, useState } from "react";
3
- //#region src/composites/button-group/use-overflow.ts
3
+ //#region src/components/button-group/use-overflow.ts
4
4
  /** Set on a group's child while it's folded into the overflow menu; the group hides it. */
5
5
  const OVERFLOW_HIDDEN = "data-overflow-hidden";
6
6
  /** Set on the group while it measures its children at their natural widths. */
@@ -1 +1 @@
1
- {"version":3,"file":"use-overflow.js","names":[],"sources":["../../../src/composites/button-group/use-overflow.ts"],"sourcesContent":["'use client';\n\nimport { useLayoutEffect, useState, type RefObject } from 'react';\n\n/** Set on a group's child while it's folded into the overflow menu; the group hides it. */\nexport const OVERFLOW_HIDDEN = 'data-overflow-hidden';\n\n/** Set on the group while it measures its children at their natural widths. */\nconst MEASURING = 'data-measuring';\n\n/** Sub-pixel slack, so rounding never folds a button that fits. */\nconst EPSILON = 0.5;\n\n/**\n * How many of `widths` fit in `available` px with `gap` between them, leaving room for an\n * overflow indicator `indicator` px wide (and its gap) when they don't all fit (Compose\n * `ButtonGroup`'s overflow measurement).\n */\nexport function fitCount(widths: number[], gap: number, indicator: number, available: number) {\n const span = (count: number) =>\n widths.slice(0, count).reduce((sum, width) => sum + width, 0) + gap * Math.max(0, count - 1);\n if (span(widths.length) <= available + EPSILON) return widths.length;\n let count = 0;\n while (count < widths.length && span(count + 1) + gap + indicator <= available + EPSILON) {\n count += 1;\n }\n return count;\n}\n\n/**\n * M3 button group overflow (\"Buttons at the trailing edge of the button group can be\n * customized to collapse into an overflow menu at smaller breakpoints, and become visible\n * again at larger sizes\"). The group's first `count` children are its items and the next\n * one is the overflow indicator. Measures the items at their natural widths whenever the\n * group or an item resizes, hides the trailing items that don't fit (and the indicator\n * when they all do) through `data-overflow-hidden`, and returns how many items show.\n *\n * Measuring and hiding happen in one synchronous pass before paint, so nothing flashes.\n */\nexport function useOverflow(\n groupRef: RefObject<HTMLElement | null>,\n enabled: boolean,\n count: number,\n): number {\n const [visible, setVisible] = useState(count);\n\n useLayoutEffect(() => {\n const group = groupRef.current;\n if (!group || !enabled) return;\n const measure = () => {\n const children = [...group.children] as HTMLElement[];\n const items = children.slice(0, count);\n const indicator = children[count];\n const available = group.clientWidth;\n // Natural widths: every item shown and none growing into spare space.\n for (const element of children) element.removeAttribute(OVERFLOW_HIDDEN);\n group.setAttribute(MEASURING, '');\n const gap = Number.parseFloat(getComputedStyle(group).columnGap) || 0;\n const widths = items.map((element) => element.getBoundingClientRect().width);\n const indicatorWidth = indicator?.getBoundingClientRect().width ?? 0;\n group.removeAttribute(MEASURING);\n\n const fit = fitCount(widths, gap, indicatorWidth, available);\n items.forEach((element, index) => element.toggleAttribute(OVERFLOW_HIDDEN, index >= fit));\n indicator?.toggleAttribute(OVERFLOW_HIDDEN, fit === count);\n setVisible(fit);\n };\n measure();\n const reset = () => {\n for (const element of group.children) element.removeAttribute(OVERFLOW_HIDDEN);\n };\n if (typeof ResizeObserver === 'undefined') return reset;\n const observer = new ResizeObserver(measure);\n observer.observe(group);\n for (const element of group.children) observer.observe(element);\n return () => {\n observer.disconnect();\n reset();\n };\n }, [groupRef, enabled, count]);\n\n return enabled ? Math.min(visible, count) : count;\n}\n"],"mappings":";;;;AAKA,MAAa,kBAAkB;;AAG/B,MAAM,YAAY;;AAGlB,MAAM,UAAU;;;;;;AAOhB,SAAgB,SAAS,QAAkB,KAAa,WAAmB,WAAmB;CAC5F,MAAM,QAAQ,UACZ,OAAO,MAAM,GAAG,KAAK,CAAC,CAAC,QAAQ,KAAK,UAAU,MAAM,OAAO,CAAC,IAAI,MAAM,KAAK,IAAI,GAAG,QAAQ,CAAC;CAC7F,IAAI,KAAK,OAAO,MAAM,KAAK,YAAY,SAAS,OAAO,OAAO;CAC9D,IAAI,QAAQ;CACZ,OAAO,QAAQ,OAAO,UAAU,KAAK,QAAQ,CAAC,IAAI,MAAM,aAAa,YAAY,SAC/E,SAAS;CAEX,OAAO;AACT;;;;;;;;;;;AAYA,SAAgB,YACd,UACA,SACA,OACQ;CACR,MAAM,CAAC,SAAS,cAAc,SAAS,KAAK;CAE5C,sBAAsB;EACpB,MAAM,QAAQ,SAAS;EACvB,IAAI,CAAC,SAAS,CAAC,SAAS;EACxB,MAAM,gBAAgB;GACpB,MAAM,WAAW,CAAC,GAAG,MAAM,QAAQ;GACnC,MAAM,QAAQ,SAAS,MAAM,GAAG,KAAK;GACrC,MAAM,YAAY,SAAS;GAC3B,MAAM,YAAY,MAAM;GAExB,KAAK,MAAM,WAAW,UAAU,QAAQ,gBAAgB,eAAe;GACvE,MAAM,aAAa,WAAW,EAAE;GAChC,MAAM,MAAM,OAAO,WAAW,iBAAiB,KAAK,CAAC,CAAC,SAAS,KAAK;GACpE,MAAM,SAAS,MAAM,KAAK,YAAY,QAAQ,sBAAsB,CAAC,CAAC,KAAK;GAC3E,MAAM,iBAAiB,WAAW,sBAAsB,CAAC,CAAC,SAAS;GACnE,MAAM,gBAAgB,SAAS;GAE/B,MAAM,MAAM,SAAS,QAAQ,KAAK,gBAAgB,SAAS;GAC3D,MAAM,SAAS,SAAS,UAAU,QAAQ,gBAAgB,iBAAiB,SAAS,GAAG,CAAC;GACxF,WAAW,gBAAgB,iBAAiB,QAAQ,KAAK;GACzD,WAAW,GAAG;EAChB;EACA,QAAQ;EACR,MAAM,cAAc;GAClB,KAAK,MAAM,WAAW,MAAM,UAAU,QAAQ,gBAAgB,eAAe;EAC/E;EACA,IAAI,OAAO,mBAAmB,aAAa,OAAO;EAClD,MAAM,WAAW,IAAI,eAAe,OAAO;EAC3C,SAAS,QAAQ,KAAK;EACtB,KAAK,MAAM,WAAW,MAAM,UAAU,SAAS,QAAQ,OAAO;EAC9D,aAAa;GACX,SAAS,WAAW;GACpB,MAAM;EACR;CACF,GAAG;EAAC;EAAU;EAAS;CAAK,CAAC;CAE7B,OAAO,UAAU,KAAK,IAAI,SAAS,KAAK,IAAI;AAC9C"}
1
+ {"version":3,"file":"use-overflow.js","names":[],"sources":["../../../src/components/button-group/use-overflow.ts"],"sourcesContent":["'use client';\n\nimport { useLayoutEffect, useState, type RefObject } from 'react';\n\n/** Set on a group's child while it's folded into the overflow menu; the group hides it. */\nexport const OVERFLOW_HIDDEN = 'data-overflow-hidden';\n\n/** Set on the group while it measures its children at their natural widths. */\nconst MEASURING = 'data-measuring';\n\n/** Sub-pixel slack, so rounding never folds a button that fits. */\nconst EPSILON = 0.5;\n\n/**\n * How many of `widths` fit in `available` px with `gap` between them, leaving room for an\n * overflow indicator `indicator` px wide (and its gap) when they don't all fit (Compose\n * `ButtonGroup`'s overflow measurement).\n */\nexport function fitCount(widths: number[], gap: number, indicator: number, available: number) {\n const span = (count: number) =>\n widths.slice(0, count).reduce((sum, width) => sum + width, 0) + gap * Math.max(0, count - 1);\n if (span(widths.length) <= available + EPSILON) return widths.length;\n let count = 0;\n while (count < widths.length && span(count + 1) + gap + indicator <= available + EPSILON) {\n count += 1;\n }\n return count;\n}\n\n/**\n * M3 button group overflow (\"Buttons at the trailing edge of the button group can be\n * customized to collapse into an overflow menu at smaller breakpoints, and become visible\n * again at larger sizes\"). The group's first `count` children are its items and the next\n * one is the overflow indicator. Measures the items at their natural widths whenever the\n * group or an item resizes, hides the trailing items that don't fit (and the indicator\n * when they all do) through `data-overflow-hidden`, and returns how many items show.\n *\n * Measuring and hiding happen in one synchronous pass before paint, so nothing flashes.\n */\nexport function useOverflow(\n groupRef: RefObject<HTMLElement | null>,\n enabled: boolean,\n count: number,\n): number {\n const [visible, setVisible] = useState(count);\n\n useLayoutEffect(() => {\n const group = groupRef.current;\n if (!group || !enabled) return;\n const measure = () => {\n const children = [...group.children] as HTMLElement[];\n const items = children.slice(0, count);\n const indicator = children[count];\n const available = group.clientWidth;\n // Natural widths: every item shown and none growing into spare space.\n for (const element of children) element.removeAttribute(OVERFLOW_HIDDEN);\n group.setAttribute(MEASURING, '');\n const gap = Number.parseFloat(getComputedStyle(group).columnGap) || 0;\n const widths = items.map((element) => element.getBoundingClientRect().width);\n const indicatorWidth = indicator?.getBoundingClientRect().width ?? 0;\n group.removeAttribute(MEASURING);\n\n const fit = fitCount(widths, gap, indicatorWidth, available);\n items.forEach((element, index) => element.toggleAttribute(OVERFLOW_HIDDEN, index >= fit));\n indicator?.toggleAttribute(OVERFLOW_HIDDEN, fit === count);\n setVisible(fit);\n };\n measure();\n const reset = () => {\n for (const element of group.children) element.removeAttribute(OVERFLOW_HIDDEN);\n };\n if (typeof ResizeObserver === 'undefined') return reset;\n const observer = new ResizeObserver(measure);\n observer.observe(group);\n for (const element of group.children) observer.observe(element);\n return () => {\n observer.disconnect();\n reset();\n };\n }, [groupRef, enabled, count]);\n\n return enabled ? Math.min(visible, count) : count;\n}\n"],"mappings":";;;;AAKA,MAAa,kBAAkB;;AAG/B,MAAM,YAAY;;AAGlB,MAAM,UAAU;;;;;;AAOhB,SAAgB,SAAS,QAAkB,KAAa,WAAmB,WAAmB;CAC5F,MAAM,QAAQ,UACZ,OAAO,MAAM,GAAG,KAAK,CAAC,CAAC,QAAQ,KAAK,UAAU,MAAM,OAAO,CAAC,IAAI,MAAM,KAAK,IAAI,GAAG,QAAQ,CAAC;CAC7F,IAAI,KAAK,OAAO,MAAM,KAAK,YAAY,SAAS,OAAO,OAAO;CAC9D,IAAI,QAAQ;CACZ,OAAO,QAAQ,OAAO,UAAU,KAAK,QAAQ,CAAC,IAAI,MAAM,aAAa,YAAY,SAC/E,SAAS;CAEX,OAAO;AACT;;;;;;;;;;;AAYA,SAAgB,YACd,UACA,SACA,OACQ;CACR,MAAM,CAAC,SAAS,cAAc,SAAS,KAAK;CAE5C,sBAAsB;EACpB,MAAM,QAAQ,SAAS;EACvB,IAAI,CAAC,SAAS,CAAC,SAAS;EACxB,MAAM,gBAAgB;GACpB,MAAM,WAAW,CAAC,GAAG,MAAM,QAAQ;GACnC,MAAM,QAAQ,SAAS,MAAM,GAAG,KAAK;GACrC,MAAM,YAAY,SAAS;GAC3B,MAAM,YAAY,MAAM;GAExB,KAAK,MAAM,WAAW,UAAU,QAAQ,gBAAgB,eAAe;GACvE,MAAM,aAAa,WAAW,EAAE;GAChC,MAAM,MAAM,OAAO,WAAW,iBAAiB,KAAK,CAAC,CAAC,SAAS,KAAK;GACpE,MAAM,SAAS,MAAM,KAAK,YAAY,QAAQ,sBAAsB,CAAC,CAAC,KAAK;GAC3E,MAAM,iBAAiB,WAAW,sBAAsB,CAAC,CAAC,SAAS;GACnE,MAAM,gBAAgB,SAAS;GAE/B,MAAM,MAAM,SAAS,QAAQ,KAAK,gBAAgB,SAAS;GAC3D,MAAM,SAAS,SAAS,UAAU,QAAQ,gBAAgB,iBAAiB,SAAS,GAAG,CAAC;GACxF,WAAW,gBAAgB,iBAAiB,QAAQ,KAAK;GACzD,WAAW,GAAG;EAChB;EACA,QAAQ;EACR,MAAM,cAAc;GAClB,KAAK,MAAM,WAAW,MAAM,UAAU,QAAQ,gBAAgB,eAAe;EAC/E;EACA,IAAI,OAAO,mBAAmB,aAAa,OAAO;EAClD,MAAM,WAAW,IAAI,eAAe,OAAO;EAC3C,SAAS,QAAQ,KAAK;EACtB,KAAK,MAAM,WAAW,MAAM,UAAU,SAAS,QAAQ,OAAO;EAC9D,aAAa;GACX,SAAS,WAAW;GACpB,MAAM;EACR;CACF,GAAG;EAAC;EAAU;EAAS;CAAK,CAAC;CAE7B,OAAO,UAAU,KAAK,IAAI,SAAS,KAAK,IAAI;AAC9C"}
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { OVERFLOW_HIDDEN } from "./use-overflow.js";
3
3
  import { useLayoutEffect } from "react";
4
- //#region src/composites/button-group/use-press-expansion.ts
4
+ //#region src/components/button-group/use-press-expansion.ts
5
5
  /**
6
6
  * Set on the group while it sizes its items inline. A connected group, a grid at rest, is
7
7
  * then a flex row (`data-pinned:flex`), since the bump animates its items' `flex-basis`.
@@ -1 +1 @@
1
- {"version":3,"file":"use-press-expansion.js","names":[],"sources":["../../../src/composites/button-group/use-press-expansion.ts"],"sourcesContent":["'use client';\n\nimport { useLayoutEffect, type RefObject } from 'react';\nimport { OVERFLOW_HIDDEN } from './use-overflow';\n\n/**\n * Set on the group while it sizes its items inline. A connected group, a grid at rest, is\n * then a flex row (`data-pinned:flex`), since the bump animates its items' `flex-basis`.\n */\nconst PINNED = 'data-pinned';\n\ninterface Pinned {\n /** Width at rest, before the group changed it. */\n rest: number;\n /** How much the item may give up to a pressed neighbour. */\n limit: number;\n /** The consumer's inline `flex` and `min-width`, restored afterwards. */\n flex: string;\n minWidth: string;\n}\n\n/**\n * An item's layout width, and how much of it the item may give up: half the space around its\n * content (Compose's compression limit). Both ignore transforms, such as a scaled ancestor,\n * since the group sets layout widths. The used `width` is in the item's own box sizing, as\n * `flex-basis` is.\n */\nfunction measure(element: HTMLElement): { rest: number; limit: number } {\n const box = element.getBoundingClientRect().width;\n const layout = Number.parseFloat(getComputedStyle(element).width);\n const width = Number.isFinite(layout) ? layout : box;\n const scale = box > 0 ? width / box : 1;\n const content = element.firstElementChild\n ? element.firstElementChild.getBoundingClientRect().width * scale\n : width;\n return { rest: width, limit: Math.max(0, (width - content) / 2) };\n}\n\n/**\n * Changes an item's sizing without animating it. Pinning swaps the consumer's sizing (such as\n * a connected group's `flex: 1 1 0%`) for its measured width in px, and unpinning swaps it\n * back: the layout is the same either way, but the `container-motion` utility transitions\n * `flex-basis`, which would run from 0% to the px width (the item collapsing first) or back.\n * The change is flushed to style and its transition finished, so only the moves between the\n * rest and bumped widths animate.\n */\nfunction resizeInstantly(element: HTMLElement, flex: string, minWidth: string) {\n element.style.flex = flex;\n element.style.minWidth = minWidth;\n void getComputedStyle(element).flexBasis;\n if (typeof element.getAnimations !== 'function') return;\n for (const animation of element.getAnimations()) {\n if (animation instanceof CSSTransition && animation.transitionProperty === 'flex-basis') {\n animation.finish();\n }\n }\n}\n\n/**\n * How long the release takes to settle, in ms. The token is a CSS time, which a minified\n * stylesheet may write in seconds (`.36s` for `360ms`).\n */\nfunction settleDuration(element: HTMLElement): number {\n const value = getComputedStyle(element)\n .getPropertyValue('--md-sys-motion-spring-spatial-fast-duration')\n .trim();\n const time = Number.parseFloat(value);\n const ms = value.endsWith('ms') ? time : time * 1000;\n return (Number.isFinite(ms) ? ms : 400) + 50;\n}\n\n/**\n * M3 Expressive button group interaction (Compose `ButtonGroup`): the pressed item grows\n * by `expandedRatio` of its width and its neighbours shrink by the same amount, each by\n * at most their own compression limit, so the group's total width stays the same.\n *\n * Watches the public `data-pressed` attribute of the group's direct children and animates\n * their `flex-basis` (the `container-motion` utility transitions it on the fast spatial\n * spring), with `data-pinned` on the group while any item is pinned. At rest no inline sizing remains, so consumer sizing such as `flex-1` applies;\n * the switches between that sizing and the pinned px widths don't animate (2026-10-14).\n * Buttons folded into the overflow menu don't take part. Under reduced motion there's no\n * bump at all: it's movement that only decorates the press (architecture §6).\n */\nexport function usePressExpansion(groupRef: RefObject<HTMLElement | null>, expandedRatio: number) {\n useLayoutEffect(() => {\n const group = groupRef.current;\n if (!group || expandedRatio <= 0) return;\n\n const pinned = new Map<HTMLElement, Pinned>();\n let pressed: HTMLElement | null = null;\n let settle: ReturnType<typeof setTimeout> | undefined;\n\n const pin = (element: HTMLElement, size: { rest: number; limit: number }): Pinned => {\n const existing = pinned.get(element);\n if (existing) return existing;\n const entry = {\n rest: size.rest,\n limit: size.limit,\n flex: element.style.flex,\n minWidth: element.style.minWidth,\n };\n // Like Compose, the group sets exact widths: a shrinking neighbour may go below its\n // minimum width, its label overlapping the padding it gives up.\n resizeInstantly(element, `0 0 ${entry.rest}px`, '0px');\n pinned.set(element, entry);\n return entry;\n };\n\n // Every item at once, in one task, so no frame shows some of them unpinned.\n const unpin = () => {\n for (const [element, entry] of pinned) resizeInstantly(element, entry.flex, entry.minWidth);\n pinned.clear();\n group.removeAttribute(PINNED);\n };\n\n // Unpins once the release's `flex-basis` transitions have ended. They end later than the\n // spring's duration when a busy main thread started them late, and unpinning then would\n // snap each item the rest of the way (2026-10-17). A new press cancels the wait.\n let releases = 0;\n const unpinWhenSettled = () => {\n const release = releases;\n const running = [...pinned.keys()].flatMap((element) =>\n typeof element.getAnimations === 'function'\n ? element\n .getAnimations()\n .filter(\n (animation) =>\n animation instanceof CSSTransition &&\n animation.transitionProperty === 'flex-basis' &&\n animation.playState === 'running',\n )\n : [],\n );\n if (running.length === 0) {\n unpin();\n return;\n }\n void Promise.allSettled(running.map((animation) => animation.finished)).then(() => {\n if (release === releases) unpin();\n });\n };\n\n // Back to the rest widths, then unpinned once that has settled.\n const release = () => {\n const [first] = pinned.keys();\n if (!first) return;\n for (const [element, entry] of pinned) element.style.flexBasis = `${entry.rest}px`;\n clearTimeout(settle);\n releases++;\n settle = setTimeout(unpinWhenSettled, settleDuration(first));\n };\n\n const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');\n const update = () => {\n const items = ([...group.children] as HTMLElement[]).filter(\n (element) => !element.hasAttribute(OVERFLOW_HIDDEN),\n );\n const next =\n items.find((el) => el.hasAttribute('data-pressed') && !el.hasAttribute('data-disabled')) ??\n null;\n if (next === pressed) return;\n pressed = next;\n release();\n if (!next || items.length < 2 || reducedMotion.matches) return;\n clearTimeout(settle);\n releases++;\n\n const index = items.indexOf(next);\n const neighbours = [items[index - 1], items[index + 1]].filter(\n (el): el is HTMLElement => el !== undefined,\n );\n // Pin every item at its current width first (instantly) so the change to the target\n // widths animates. Every item is measured before any is pinned, and every item is\n // pinned, not only the ones that move: in a group sized by its content (`w-fit`) the\n // group's width, and so the other items', depends on how each item is sized.\n const sizes = items.map((el) => pinned.get(el) ?? measure(el));\n // A connected group lays its items out as a grid; pinned, it's a flex row of the same\n // widths, so their `flex-basis` can animate.\n group.setAttribute(PINNED, '');\n const entries = new Map(items.map((el, i) => [el, pin(el, sizes[i]!)]));\n\n // Compose: a middle item grows by min(ratio × width / 2, both neighbours' limits) on\n // each side; an end item by min(ratio × width, its neighbour's limit) on one side.\n const width = entries.get(next)!.rest;\n const share = neighbours.length === 2 ? (expandedRatio * width) / 2 : expandedRatio * width;\n const growthPerSide = Math.min(\n share,\n ...neighbours.map((neighbour) => entries.get(neighbour)!.limit),\n );\n let growth = 0;\n for (const neighbour of neighbours) {\n const neighbourWidth = entries.get(neighbour)!.rest;\n const give = Math.min(growthPerSide, neighbourWidth);\n neighbour.style.flexBasis = `${neighbourWidth - give}px`;\n growth += give;\n }\n next.style.flexBasis = `${width + growth}px`;\n };\n\n const observer = new MutationObserver((records) => {\n if (records.some((record) => (record.target as Element).parentElement === group)) update();\n });\n observer.observe(group, { subtree: true, attributes: true, attributeFilter: ['data-pressed'] });\n\n return () => {\n observer.disconnect();\n clearTimeout(settle);\n releases++;\n unpin();\n };\n }, [groupRef, expandedRatio]);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-press-expansion.js","names":[],"sources":["../../../src/components/button-group/use-press-expansion.ts"],"sourcesContent":["'use client';\n\nimport { useLayoutEffect, type RefObject } from 'react';\nimport { OVERFLOW_HIDDEN } from './use-overflow';\n\n/**\n * Set on the group while it sizes its items inline. A connected group, a grid at rest, is\n * then a flex row (`data-pinned:flex`), since the bump animates its items' `flex-basis`.\n */\nconst PINNED = 'data-pinned';\n\ninterface Pinned {\n /** Width at rest, before the group changed it. */\n rest: number;\n /** How much the item may give up to a pressed neighbour. */\n limit: number;\n /** The consumer's inline `flex` and `min-width`, restored afterwards. */\n flex: string;\n minWidth: string;\n}\n\n/**\n * An item's layout width, and how much of it the item may give up: half the space around its\n * content (Compose's compression limit). Both ignore transforms, such as a scaled ancestor,\n * since the group sets layout widths. The used `width` is in the item's own box sizing, as\n * `flex-basis` is.\n */\nfunction measure(element: HTMLElement): { rest: number; limit: number } {\n const box = element.getBoundingClientRect().width;\n const layout = Number.parseFloat(getComputedStyle(element).width);\n const width = Number.isFinite(layout) ? layout : box;\n const scale = box > 0 ? width / box : 1;\n const content = element.firstElementChild\n ? element.firstElementChild.getBoundingClientRect().width * scale\n : width;\n return { rest: width, limit: Math.max(0, (width - content) / 2) };\n}\n\n/**\n * Changes an item's sizing without animating it. Pinning swaps the consumer's sizing (such as\n * a connected group's `flex: 1 1 0%`) for its measured width in px, and unpinning swaps it\n * back: the layout is the same either way, but the `container-motion` utility transitions\n * `flex-basis`, which would run from 0% to the px width (the item collapsing first) or back.\n * The change is flushed to style and its transition finished, so only the moves between the\n * rest and bumped widths animate.\n */\nfunction resizeInstantly(element: HTMLElement, flex: string, minWidth: string) {\n element.style.flex = flex;\n element.style.minWidth = minWidth;\n void getComputedStyle(element).flexBasis;\n if (typeof element.getAnimations !== 'function') return;\n for (const animation of element.getAnimations()) {\n if (animation instanceof CSSTransition && animation.transitionProperty === 'flex-basis') {\n animation.finish();\n }\n }\n}\n\n/**\n * How long the release takes to settle, in ms. The token is a CSS time, which a minified\n * stylesheet may write in seconds (`.36s` for `360ms`).\n */\nfunction settleDuration(element: HTMLElement): number {\n const value = getComputedStyle(element)\n .getPropertyValue('--md-sys-motion-spring-spatial-fast-duration')\n .trim();\n const time = Number.parseFloat(value);\n const ms = value.endsWith('ms') ? time : time * 1000;\n return (Number.isFinite(ms) ? ms : 400) + 50;\n}\n\n/**\n * M3 Expressive button group interaction (Compose `ButtonGroup`): the pressed item grows\n * by `expandedRatio` of its width and its neighbours shrink by the same amount, each by\n * at most their own compression limit, so the group's total width stays the same.\n *\n * Watches the public `data-pressed` attribute of the group's direct children and animates\n * their `flex-basis` (the `container-motion` utility transitions it on the fast spatial\n * spring), with `data-pinned` on the group while any item is pinned. At rest no inline sizing remains, so consumer sizing such as `flex-1` applies;\n * the switches between that sizing and the pinned px widths don't animate (2026-10-14).\n * Buttons folded into the overflow menu don't take part. Under reduced motion there's no\n * bump at all: it's movement that only decorates the press (architecture §6).\n */\nexport function usePressExpansion(groupRef: RefObject<HTMLElement | null>, expandedRatio: number) {\n useLayoutEffect(() => {\n const group = groupRef.current;\n if (!group || expandedRatio <= 0) return;\n\n const pinned = new Map<HTMLElement, Pinned>();\n let pressed: HTMLElement | null = null;\n let settle: ReturnType<typeof setTimeout> | undefined;\n\n const pin = (element: HTMLElement, size: { rest: number; limit: number }): Pinned => {\n const existing = pinned.get(element);\n if (existing) return existing;\n const entry = {\n rest: size.rest,\n limit: size.limit,\n flex: element.style.flex,\n minWidth: element.style.minWidth,\n };\n // Like Compose, the group sets exact widths: a shrinking neighbour may go below its\n // minimum width, its label overlapping the padding it gives up.\n resizeInstantly(element, `0 0 ${entry.rest}px`, '0px');\n pinned.set(element, entry);\n return entry;\n };\n\n // Every item at once, in one task, so no frame shows some of them unpinned.\n const unpin = () => {\n for (const [element, entry] of pinned) resizeInstantly(element, entry.flex, entry.minWidth);\n pinned.clear();\n group.removeAttribute(PINNED);\n };\n\n // Unpins once the release's `flex-basis` transitions have ended. They end later than the\n // spring's duration when a busy main thread started them late, and unpinning then would\n // snap each item the rest of the way (2026-10-17). A new press cancels the wait.\n let releases = 0;\n const unpinWhenSettled = () => {\n const release = releases;\n const running = [...pinned.keys()].flatMap((element) =>\n typeof element.getAnimations === 'function'\n ? element\n .getAnimations()\n .filter(\n (animation) =>\n animation instanceof CSSTransition &&\n animation.transitionProperty === 'flex-basis' &&\n animation.playState === 'running',\n )\n : [],\n );\n if (running.length === 0) {\n unpin();\n return;\n }\n void Promise.allSettled(running.map((animation) => animation.finished)).then(() => {\n if (release === releases) unpin();\n });\n };\n\n // Back to the rest widths, then unpinned once that has settled.\n const release = () => {\n const [first] = pinned.keys();\n if (!first) return;\n for (const [element, entry] of pinned) element.style.flexBasis = `${entry.rest}px`;\n clearTimeout(settle);\n releases++;\n settle = setTimeout(unpinWhenSettled, settleDuration(first));\n };\n\n const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');\n const update = () => {\n const items = ([...group.children] as HTMLElement[]).filter(\n (element) => !element.hasAttribute(OVERFLOW_HIDDEN),\n );\n const next =\n items.find((el) => el.hasAttribute('data-pressed') && !el.hasAttribute('data-disabled')) ??\n null;\n if (next === pressed) return;\n pressed = next;\n release();\n if (!next || items.length < 2 || reducedMotion.matches) return;\n clearTimeout(settle);\n releases++;\n\n const index = items.indexOf(next);\n const neighbours = [items[index - 1], items[index + 1]].filter(\n (el): el is HTMLElement => el !== undefined,\n );\n // Pin every item at its current width first (instantly) so the change to the target\n // widths animates. Every item is measured before any is pinned, and every item is\n // pinned, not only the ones that move: in a group sized by its content (`w-fit`) the\n // group's width, and so the other items', depends on how each item is sized.\n const sizes = items.map((el) => pinned.get(el) ?? measure(el));\n // A connected group lays its items out as a grid; pinned, it's a flex row of the same\n // widths, so their `flex-basis` can animate.\n group.setAttribute(PINNED, '');\n const entries = new Map(items.map((el, i) => [el, pin(el, sizes[i]!)]));\n\n // Compose: a middle item grows by min(ratio × width / 2, both neighbours' limits) on\n // each side; an end item by min(ratio × width, its neighbour's limit) on one side.\n const width = entries.get(next)!.rest;\n const share = neighbours.length === 2 ? (expandedRatio * width) / 2 : expandedRatio * width;\n const growthPerSide = Math.min(\n share,\n ...neighbours.map((neighbour) => entries.get(neighbour)!.limit),\n );\n let growth = 0;\n for (const neighbour of neighbours) {\n const neighbourWidth = entries.get(neighbour)!.rest;\n const give = Math.min(growthPerSide, neighbourWidth);\n neighbour.style.flexBasis = `${neighbourWidth - give}px`;\n growth += give;\n }\n next.style.flexBasis = `${width + growth}px`;\n };\n\n const observer = new MutationObserver((records) => {\n if (records.some((record) => (record.target as Element).parentElement === group)) update();\n });\n observer.observe(group, { subtree: true, attributes: true, attributeFilter: ['data-pressed'] });\n\n return () => {\n observer.disconnect();\n clearTimeout(settle);\n releases++;\n unpin();\n };\n }, [groupRef, expandedRatio]);\n}\n"],"mappings":"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"}
@@ -91,7 +91,10 @@ interface CarouselOwnProps extends RootDOMProps {
91
91
  onAction?: (index: number) => void;
92
92
  /** Indexes of items that can't be activated. */
93
93
  disabledIndexes?: readonly number[];
94
- /** Words the carousel announces, for localisation. */
94
+ /**
95
+ * Words the carousel announces, for localisation. A {@link LabelsProvider} sets them for the
96
+ * whole app.
97
+ */
95
98
  labels?: Partial<CarouselLabels>;
96
99
  /** The items, e.g. images; each fills its slot and is clipped to its keyline. */
97
100
  children: ReactNode;
@@ -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 { useM3Interaction } from "../../primitives/use-m3-interaction.js";
4
5
  import { mergeDataTokens } from "../../utils/merge-data-tokens.js";
5
6
  import { splitRootProps } from "../../utils/root-props.js";
@@ -61,10 +62,7 @@ function tabbableOutside(container, forward) {
61
62
  function Carousel({ variant = "multi-browse", orientation = variant === "full-screen" ? "vertical" : "horizontal", preferredItemSize, itemSize = 240, itemSpacing = variant === "full-screen" ? 16 : 8, heroAlignment = "start", minSmallItemSize, maxSmallItemSize, contentPadding, snap = "none", onAction, disabledIndexes, labels: labelsProp, children, ref, className, style, classNames, ...rest }) {
62
63
  const { root: data, rest: labelling } = splitRootProps(rest);
63
64
  const { "aria-label": label, "aria-labelledby": labelledBy } = labelling;
64
- const labels = {
65
- ...DEFAULT_LABELS,
66
- ...labelsProp
67
- };
65
+ const labels = useLabels("Carousel", DEFAULT_LABELS, labelsProp);
68
66
  const numbers = useNumberFormatter();
69
67
  const items = Children.toArray(children).filter(isValidElement);
70
68
  const count = items.length;
@@ -1 +1 @@
1
- {"version":3,"file":"Carousel.js","names":[],"sources":["../../../src/components/carousel/Carousel.tsx"],"sourcesContent":["'use client';\n\nimport { useReducedMotion } from 'motion/react';\nimport {\n Children,\n isValidElement,\n useLayoutEffect,\n useRef,\n useState,\n type CSSProperties,\n type FocusEvent,\n type KeyboardEvent,\n type PointerEvent,\n type ReactNode,\n type Ref,\n} from 'react';\nimport { useNumberFormatter, useObjectRef } from 'react-aria';\nimport { useM3Interaction } from '../../primitives/use-m3-interaction';\nimport { cn } from '../../utils/cn';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\nimport { splitRootProps, type RootDOMProps } from '../../utils/root-props';\nimport {\n createStrategy,\n heroKeylineList,\n keylinesForScrollOffset,\n MAX_SMALL_ITEM_SIZE,\n maxScrollOffset,\n multiBrowseKeylineList,\n placeItem,\n snapPositionOffset,\n type Strategy,\n} from './carousel-keylines';\nimport { pressAreaReach } from './carousel-press-area';\nimport { carouselStyles } from './carousel-styles';\n\nexport interface CarouselClassNames {\n /** The outer element, which holds the carousel's name. */\n root?: string;\n /** The scrolling track that holds the items. */\n scroller?: string;\n /** Each item's slot, sized and placed on its keyline. */\n item?: string;\n /** The masked layer of each item (corners, clipping). */\n mask?: string;\n /** The state layer painted over an item that can be activated. */\n stateLayer?: string;\n}\n\n/** Words the carousel announces. */\nexport interface CarouselLabels {\n /**\n * Each item's name: its position (from 1) and the number of items, as formatted text.\n * @default (position, count) => `${position} of ${count}`\n */\n slide: (position: string, count: string) => string;\n /** What the carousel is, read before its name (`aria-roledescription`). @default \"carousel\" */\n carousel: string;\n /** What each item is, read before its name (`aria-roledescription`). @default \"slide\" */\n item: string;\n}\n\nconst DEFAULT_LABELS: CarouselLabels = {\n slide: (position, count) => `${position} of ${count}`,\n carousel: 'carousel',\n item: 'slide',\n};\n\n/** Padding around the items, in px. `start` and `end` run along the carousel. */\nexport interface CarouselContentPadding {\n /** Before the first item. */\n start: number;\n /** After the last item. */\n end: number;\n /** Either side across the carousel. */\n cross: number;\n}\n\nexport type CarouselVariant =\n 'multi-browse' | 'uncontained' | 'multi-aspect' | 'hero' | 'full-screen';\n\n/** A carousel needs a name: `aria-label`, or `aria-labelledby` pointing at a visible title. */\ntype Naming =\n | {\n /** Accessible name when there's no visible title. */\n 'aria-label': string;\n }\n | {\n /** The id of the visible title that names the carousel. */\n 'aria-labelledby': string;\n };\n\ninterface CarouselOwnProps extends RootDOMProps {\n /** The root element's id. */\n id?: string;\n /**\n * `multi-browse` shows large, medium and small items; `uncontained` shows items of one\n * size that flow past the end edge, never masked or resized; `multi-aspect` is uncontained with items of their own widths\n * (9:16 to 16:9); `hero` shows one large item with small ones beside it; `full-screen`\n * shows one edge-to-edge item at a time. @default \"multi-browse\"\n */\n variant?: CarouselVariant;\n /**\n * The direction the items scroll in.\n * @default \"horizontal\", or \"vertical\" when full-screen\n */\n orientation?: 'horizontal' | 'vertical';\n /**\n * Preferred size of the large items along the carousel (multi-browse, hero). Hero defaults\n * to the whole carousel. @default 186\n */\n preferredItemSize?: number;\n /** Size of the items along the carousel (uncontained). @default 240 */\n itemSize?: number;\n /** Space between items, px. @default 8, or 16 when full-screen */\n itemSpacing?: number;\n /** Centre the large hero item between small ones. @default \"start\" */\n heroAlignment?: 'start' | 'center';\n /** Minimum size of the small items. @default 40 */\n minSmallItemSize?: number;\n /** Maximum size of the small items. @default 56 */\n maxSmallItemSize?: number;\n /**\n * Padding around the items. Items scroll over the start and end padding.\n * @default { start: 16, end: 16, cross: 8 } (uncontained and multi-aspect: no end padding;\n * full-screen: none)\n */\n contentPadding?: Partial<CarouselContentPadding>;\n /**\n * Snap uncontained and multi-aspect carousels to their items. Multi-browse, hero and\n * full-screen carousels always snap. @default \"none\"\n */\n snap?: 'none' | 'item';\n /**\n * Called when an item is pressed, or activated with Enter or Space. Items then show\n * hover, focus and pressed states.\n */\n onAction?: (index: number) => void;\n /** Indexes of items that can't be activated. */\n disabledIndexes?: readonly number[];\n /** Words the carousel announces, for localisation. */\n labels?: Partial<CarouselLabels>;\n /** The items, e.g. images; each fills its slot and is clipped to its keyline. */\n children: ReactNode;\n /** Ref to the root `<div>` element. */\n ref?: Ref<HTMLDivElement>;\n /** Classes for the root element, merged with `classNames.root`; consumer classes win. */\n className?: string;\n /** Inline styles for the root element. */\n style?: CSSProperties;\n /** Classes for each part, by slot. `classNames.root` is merged with `className`. */\n classNames?: CarouselClassNames;\n /** `data-*` attributes, like the other DOM attributes, go to the root. */\n [data: `data-${string}`]: string | number | boolean | undefined;\n}\n\nexport type CarouselProps = CarouselOwnProps & Naming;\n\n/**\n * Variants whose items change size along keylines as they scroll. Uncontained and\n * multi-aspect items keep one size and flow past the end edge (M3: \"items are a single size\n * and flow past the edge of the screen\"), so they are never masked or cropped.\n */\nconst KEYLINE_VARIANTS: readonly CarouselVariant[] = ['multi-browse', 'hero'];\n\nfunction defaultPadding(variant: CarouselVariant): CarouselContentPadding {\n if (variant === 'full-screen') return { start: 0, end: 0, cross: 0 };\n if (variant === 'uncontained' || variant === 'multi-aspect')\n return { start: 16, end: 0, cross: 8 };\n return { start: 16, end: 16, cross: 8 };\n}\n\nconst TABBABLE =\n 'a[href],area[href],button,input,select,textarea,iframe,summary,[contenteditable],[tabindex]';\n\n/** The next (or previous) element in the tab order outside `container`. */\nfunction tabbableOutside(container: HTMLElement, forward: boolean): HTMLElement | undefined {\n const candidates = Array.from(\n container.ownerDocument.querySelectorAll<HTMLElement>(TABBABLE),\n ).filter(\n (element) =>\n !container.contains(element) &&\n element.tabIndex >= 0 &&\n !element.matches(':disabled') &&\n !element.closest('[inert],[hidden]'),\n );\n const isAfter = (element: HTMLElement) =>\n Boolean(container.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_FOLLOWING);\n return forward ? candidates.find(isAfter) : candidates.filter((e) => !isAfter(e)).pop();\n}\n\n/**\n * M3 Expressive carousel: a scrollable row (or column) of items whose sizes follow Compose's\n * keylines as they scroll: multi-browse (large, medium and small items), uncontained,\n * multi-aspect, hero or full-screen. Give it a height (or a width, vertical) with\n * `className`. Tab or the arrow keys move between items; Up and Down (Left and Right when\n * vertical) leave the carousel. Under reduced motion every item keeps the large size.\n *\n * @example\n * <Carousel aria-label=\"Photos\" className=\"h-[221px]\">\n * {photos.map((photo) => <img key={photo.id} src={photo.src} alt={photo.alt} className=\"size-full object-cover\" />)}\n * </Carousel>\n */\nexport function Carousel({\n variant = 'multi-browse',\n orientation = variant === 'full-screen' ? 'vertical' : 'horizontal',\n preferredItemSize,\n itemSize = 240,\n itemSpacing = variant === 'full-screen' ? 16 : 8,\n heroAlignment = 'start',\n minSmallItemSize,\n maxSmallItemSize,\n contentPadding,\n snap = 'none',\n onAction,\n disabledIndexes,\n labels: labelsProp,\n children,\n ref,\n className,\n style,\n classNames,\n ...rest\n}: CarouselProps) {\n const { root: data, rest: labelling } = splitRootProps(rest);\n const { 'aria-label': label, 'aria-labelledby': labelledBy } = labelling as {\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n };\n const labels = { ...DEFAULT_LABELS, ...labelsProp };\n const numbers = useNumberFormatter();\n const items = Children.toArray(children).filter(isValidElement);\n const count = items.length;\n const vertical = orientation === 'vertical';\n const reduceMotion = useReducedMotion() ?? false;\n const padding = { ...defaultPadding(variant), ...contentPadding };\n const keylined = KEYLINE_VARIANTS.includes(variant);\n // Under reduced motion items keep the large size, unmasked (the spec's reduced motion).\n const masked = keylined && !reduceMotion;\n const rootRef = useObjectRef(ref);\n const scrollerRef = useRef<HTMLDivElement>(null);\n const slotRefs = useRef<(HTMLDivElement | null)[]>([]);\n const maskRefs = useRef<(HTMLDivElement | null)[]>([]);\n const hitRefs = useRef<(HTMLSpanElement | null)[]>([]);\n const [space, setSpace] = useState(0);\n const [crossSize, setCrossSize] = useState(0);\n\n const strategy = computeStrategy();\n function computeStrategy(): Strategy | null {\n if (space === 0 || count === 0 || !keylined) return null;\n const keylines =\n variant === 'hero'\n ? heroKeylineList(\n space,\n preferredItemSize,\n itemSpacing,\n count,\n heroAlignment === 'center',\n minSmallItemSize,\n maxSmallItemSize,\n )\n : multiBrowseKeylineList(\n space,\n preferredItemSize ?? 186,\n itemSpacing,\n count,\n minSmallItemSize,\n maxSmallItemSize,\n );\n const result = masked\n ? createStrategy(\n keylines,\n space,\n itemSpacing,\n padding.start,\n padding.end,\n maxSmallItemSize ?? MAX_SMALL_ITEM_SIZE,\n )\n : createStrategy(keylines, space, itemSpacing);\n return result.isValid ? result : null;\n }\n\n // Writes each item's mask and translation for the current scroll position, without\n // re-rendering (Compose's `carouselItem` layer block).\n const update = () => {\n const scroller = scrollerRef.current;\n if (!scroller || !strategy || !masked) return;\n const rtl = !vertical && getComputedStyle(scroller).direction === 'rtl';\n const scroll = Math.abs(vertical ? scroller.scrollTop : scroller.scrollLeft);\n const max = maxScrollOffset(strategy, count);\n const keylines = keylinesForScrollOffset(strategy, scroll, max);\n const full = strategy.itemMainAxisSize;\n const round = (value: number) => Math.round(value * 100) / 100;\n const places = maskRefs.current.map((_, index) => placeItem(strategy, keylines, index, scroll));\n // Each item's visible part, from the viewport's start edge (slots are `full` apart plus\n // the spacing; the mask is centred in its slot and moved by its translation).\n const spans = places.map(({ size, translation }, index) => ({\n start: index * (full + itemSpacing) + translation + (full - size) / 2 - scroll,\n size,\n }));\n maskRefs.current.forEach((mask, index) => {\n if (!mask) return;\n const { size, translation } = places[index]!;\n const inset = round(Math.max(0, (full - size) / 2));\n const move = round(rtl ? -translation : translation);\n mask.style.translate = vertical ? `0 ${move}px` : `${move}px 0`;\n mask.style.clipPath = vertical\n ? `inset(${inset}px 0 ${inset}px 0 round var(--m3-carousel-item-corner))`\n : `inset(0 ${inset}px 0 ${inset}px round var(--m3-carousel-item-corner))`;\n mask.style.setProperty('--m3-carousel-item-size', `${round(size)}px`);\n // The press area follows the mask, widened to M3's 48px touch target for a small item.\n const hit = hitRefs.current[index];\n if (!hit) return;\n const reach = pressAreaReach(spans, index, itemSpacing, space);\n const start = round(Math.max(0, inset - reach.start));\n const end = round(Math.max(0, inset - reach.end));\n hit.style.translate = mask.style.translate;\n hit.style.clipPath = vertical\n ? `inset(${start}px 0 ${end}px 0)`\n : rtl\n ? `inset(0 ${start}px 0 ${end}px)`\n : `inset(0 ${end}px 0 ${start}px)`;\n });\n };\n\n useLayoutEffect(() => {\n const scroller = scrollerRef.current;\n if (!scroller) return;\n const measure = () => {\n const computed = getComputedStyle(scroller);\n const px = (value: string) => Number.parseFloat(value) || 0;\n // The keylines span the scroller's padding box along the carousel: Compose handles\n // the start and end padding in the keylines, not in the layout.\n setSpace(vertical ? scroller.clientHeight : scroller.clientWidth);\n setCrossSize(\n vertical\n ? scroller.clientWidth - px(computed.paddingLeft) - px(computed.paddingRight)\n : scroller.clientHeight - px(computed.paddingTop) - px(computed.paddingBottom),\n );\n };\n measure();\n if (typeof ResizeObserver === 'undefined') return;\n const observer = new ResizeObserver(measure);\n observer.observe(scroller);\n return () => observer.disconnect();\n }, [vertical]);\n\n useLayoutEffect(() => {\n // Leaving the masked layout (reduced motion) clears what `update` wrote.\n if (!masked)\n [...maskRefs.current, ...hitRefs.current].forEach((layer) => {\n layer?.style.removeProperty('translate');\n layer?.style.removeProperty('clip-path');\n layer?.style.removeProperty('--m3-carousel-item-size');\n });\n update();\n const scroller = scrollerRef.current;\n if (!scroller || !masked) return;\n let frame = 0;\n const onScroll = () => {\n if (!frame)\n frame = requestAnimationFrame(() => {\n frame = 0;\n update();\n });\n };\n scroller.addEventListener('scroll', onScroll, { passive: true });\n return () => {\n scroller.removeEventListener('scroll', onScroll);\n cancelAnimationFrame(frame);\n };\n });\n\n // Mouse drag scrolls the carousel (touch and trackpads scroll natively); snapping is\n // paused while dragging and resumes on release. A drag never activates an item. The\n // handlers run in the capture phase: an actionable item's press handler stops the\n // pointer events' propagation and focuses the item on pointer down, so in the bubble\n // phase the scroller never saw the press (the focus then read as keyboard focus and\n // scrolled the item to its snap position, and a drag neither scrolled nor was ignored).\n const drag = useRef<{ start: number; scroll: number; id: number } | null>(null);\n const dragged = useRef(false);\n const pointerFocus = useRef(false);\n const onPointerDown = (event: PointerEvent<HTMLDivElement>) => {\n pointerFocus.current = true;\n dragged.current = false;\n if (event.pointerType !== 'mouse' || event.button !== 0) return;\n const scroller = event.currentTarget;\n drag.current = {\n start: vertical ? event.clientY : event.clientX,\n scroll: vertical ? scroller.scrollTop : scroller.scrollLeft,\n id: event.pointerId,\n };\n };\n const onPointerMove = (event: PointerEvent<HTMLDivElement>) => {\n const state = drag.current;\n if (!state) return;\n const scroller = event.currentTarget;\n const delta = (vertical ? event.clientY : event.clientX) - state.start;\n if (Math.abs(delta) > 3 && !scroller.hasAttribute('data-dragging')) {\n dragged.current = true;\n scroller.setAttribute('data-dragging', '');\n scroller.setPointerCapture?.(state.id);\n }\n if (vertical) scroller.scrollTop = state.scroll - delta;\n else scroller.scrollLeft = state.scroll - delta;\n };\n const endDrag = (event: PointerEvent<HTMLDivElement>) => {\n drag.current = null;\n event.currentTarget.removeAttribute('data-dragging');\n };\n\n // Brings a focused item into view: keylined carousels scroll it to its snap position\n // (the focal keyline); the others just far enough to show it.\n const reveal = (index: number) => {\n const scroller = scrollerRef.current;\n const slot = slotRefs.current[index];\n if (!scroller || !slot) return;\n const behavior: ScrollBehavior = reduceMotion ? 'instant' : 'smooth';\n if (strategy && masked) {\n const step = strategy.itemMainAxisSize + itemSpacing;\n const max = maxScrollOffset(strategy, count);\n const target = Math.min(\n max,\n Math.max(0, index * step - snapPositionOffset(strategy, index, count)),\n );\n const rtl = !vertical && getComputedStyle(scroller).direction === 'rtl';\n scroller.scrollTo?.(\n vertical ? { top: target, behavior } : { left: rtl ? -target : target, behavior },\n );\n } else {\n slot.scrollIntoView?.({ block: 'nearest', inline: 'nearest', behavior });\n }\n };\n\n const onSlotFocus = (index: number, event: FocusEvent<HTMLDivElement>) => {\n if (event.target !== event.currentTarget) return;\n // A press focuses the item without moving the carousel.\n if (pointerFocus.current) return;\n reveal(index);\n };\n\n const focusSlot = (index: number) => {\n const slot = slotRefs.current[Math.max(0, Math.min(count - 1, index))];\n if (!slot) return;\n pointerFocus.current = true;\n slot.focus({ preventScroll: true });\n pointerFocus.current = false;\n reveal(Number(slot.dataset.index));\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n pointerFocus.current = false;\n const target = event.target as HTMLElement;\n const index = slotRefs.current.indexOf(target as HTMLDivElement);\n if (index < 0) return;\n const rtl = getComputedStyle(target).direction === 'rtl';\n const [prev, next, leaveBack, leaveForward] = vertical\n ? ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']\n : rtl\n ? ['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown']\n : ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];\n let handled = true;\n if (event.key === next) focusSlot(index + 1);\n else if (event.key === prev) focusSlot(index - 1);\n else if (event.key === 'Home') focusSlot(0);\n else if (event.key === 'End') focusSlot(count - 1);\n else if (event.key === leaveForward || event.key === leaveBack) {\n const root = rootRef.current;\n const outside = root && tabbableOutside(root, event.key === leaveForward);\n if (outside) outside.focus();\n else handled = false;\n } else handled = false;\n if (handled) event.preventDefault();\n };\n\n const snapping =\n variant === 'hero' || variant === 'full-screen'\n ? 'single'\n : variant === 'multi-browse' || snap === 'item'\n ? 'many'\n : 'none';\n const styles = carouselStyles({\n orientation,\n snapping,\n masked,\n aspect: variant === 'multi-aspect',\n interactive: onAction !== undefined,\n fullBleed: variant === 'full-screen',\n });\n const fallbackSize = variant === 'uncontained' ? itemSize : (preferredItemSize ?? 186);\n const slotSize =\n variant === 'full-screen'\n ? space || undefined\n : variant === 'multi-aspect'\n ? undefined\n : (strategy?.itemMainAxisSize ?? fallbackSize);\n // The scroller pads the items itself only when the keylines don't.\n const layoutPadding = masked ? { start: 0, end: 0 } : padding;\n const total = numbers.format(count);\n const disabled = new Set(disabledIndexes);\n\n return (\n <div\n {...data}\n id={(labelling as { id?: string }).id}\n ref={rootRef}\n role=\"region\"\n aria-roledescription={labels.carousel}\n aria-label={label}\n aria-labelledby={labelledBy}\n // The component token's hooks (carousel-tokens.ts), also public styling hooks for apps.\n data-tokens={mergeDataTokens('md-comp-carousel', data)}\n data-variant={variant}\n style={style}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n <div\n ref={scrollerRef}\n onPointerDownCapture={onPointerDown}\n onPointerMoveCapture={onPointerMove}\n onPointerUpCapture={endDrag}\n onPointerCancelCapture={endDrag}\n onKeyDown={onKeyDown}\n style={\n {\n gap: itemSpacing,\n [vertical ? 'paddingInline' : 'paddingBlock']: padding.cross,\n [vertical ? 'paddingBlockStart' : 'paddingInlineStart']: layoutPadding.start,\n [vertical ? 'paddingBlockEnd' : 'paddingInlineEnd']: layoutPadding.end,\n [vertical ? 'scrollPaddingBlockStart' : 'scrollPaddingInlineStart']:\n layoutPadding.start,\n [vertical ? 'scrollPaddingBlockEnd' : 'scrollPaddingInlineEnd']: layoutPadding.end,\n '--m3-carousel-cross-size': `${crossSize}px`,\n } as CSSProperties\n }\n className={styles.scroller({ class: classNames?.scroller })}\n >\n {items.map((item, index) => (\n <CarouselItem\n key={item.key ?? index}\n index={index}\n label={labels.slide(numbers.format(index + 1), total)}\n roleDescription={labels.item}\n disabled={disabled.has(index)}\n onAction={\n onAction &&\n (() => {\n if (!dragged.current) onAction(index);\n })\n }\n onFocus={(event) => onSlotFocus(index, event)}\n slotRef={(element) => {\n slotRefs.current[index] = element;\n }}\n maskRef={(element) => {\n maskRefs.current[index] = element;\n }}\n hitRef={(element) => {\n hitRefs.current[index] = element;\n }}\n style={{\n ...(slotSize === undefined ? {} : { [vertical ? 'height' : 'width']: slotSize }),\n [vertical ? 'scrollMarginBlockStart' : 'scrollMarginInlineStart']:\n strategy && masked ? snapPositionOffset(strategy, index, count) : 0,\n }}\n styles={styles}\n classNames={classNames}\n >\n {item}\n </CarouselItem>\n ))}\n </div>\n </div>\n );\n}\n\nfunction CarouselItem({\n index,\n label,\n roleDescription,\n disabled,\n onAction,\n onFocus,\n slotRef,\n maskRef,\n hitRef,\n style,\n styles,\n classNames,\n children,\n}: {\n index: number;\n label: string;\n roleDescription: string;\n disabled: boolean;\n onAction?: () => void;\n onFocus: (event: FocusEvent<HTMLDivElement>) => void;\n slotRef: (element: HTMLDivElement | null) => void;\n maskRef: (element: HTMLDivElement | null) => void;\n hitRef: (element: HTMLSpanElement | null) => void;\n style: CSSProperties;\n styles: ReturnType<typeof carouselStyles>;\n classNames?: CarouselClassNames;\n children: ReactNode;\n}) {\n const ref = useRef<HTMLDivElement | null>(null);\n const interactive = onAction !== undefined;\n const { interactionProps, dataAttributes } = useM3Interaction(\n { isDisabled: disabled || !interactive, onPress: onAction },\n ref,\n );\n const { 'data-focus-visible': focusVisible, ...states } = dataAttributes;\n return (\n <div\n {...interactionProps}\n ref={(element) => {\n ref.current = element;\n slotRef(element);\n }}\n role=\"group\"\n aria-roledescription={roleDescription}\n aria-label={label}\n aria-disabled={(interactive && disabled) || undefined}\n tabIndex={0}\n data-slot=\"item\"\n data-index={index}\n data-focus-visible={focusVisible}\n data-disabled={(interactive && disabled) || undefined}\n onFocus={(event) => {\n interactionProps.onFocus?.(event);\n onFocus(event);\n }}\n style={style}\n className={styles.item({ class: classNames?.item })}\n >\n {/* Where the item takes presses: under the mask, clipped to it or a little wider. */}\n <span aria-hidden=\"true\" ref={hitRef} data-slot=\"hit-area\" className={styles.hitArea()} />\n <div\n ref={maskRef}\n {...(interactive ? states : {})}\n className={styles.mask({ class: classNames?.mask })}\n >\n {children}\n {interactive && (\n <span\n aria-hidden=\"true\"\n {...states}\n data-focus-visible={focusVisible}\n className={styles.stateLayer({ class: classNames?.stateLayer })}\n />\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"Carousel.js","names":[],"sources":["../../../src/components/carousel/Carousel.tsx"],"sourcesContent":["'use client';\n\nimport { useReducedMotion } from 'motion/react';\nimport {\n Children,\n isValidElement,\n useLayoutEffect,\n useRef,\n useState,\n type CSSProperties,\n type FocusEvent,\n type KeyboardEvent,\n type PointerEvent,\n type ReactNode,\n type Ref,\n} from 'react';\nimport { useNumberFormatter, useObjectRef } from 'react-aria';\nimport { useLabels } from '../labels/use-labels';\nimport { useM3Interaction } from '../../primitives/use-m3-interaction';\nimport { cn } from '../../utils/cn';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\nimport { splitRootProps, type RootDOMProps } from '../../utils/root-props';\nimport {\n createStrategy,\n heroKeylineList,\n keylinesForScrollOffset,\n MAX_SMALL_ITEM_SIZE,\n maxScrollOffset,\n multiBrowseKeylineList,\n placeItem,\n snapPositionOffset,\n type Strategy,\n} from './carousel-keylines';\nimport { pressAreaReach } from './carousel-press-area';\nimport { carouselStyles } from './carousel-styles';\n\nexport interface CarouselClassNames {\n /** The outer element, which holds the carousel's name. */\n root?: string;\n /** The scrolling track that holds the items. */\n scroller?: string;\n /** Each item's slot, sized and placed on its keyline. */\n item?: string;\n /** The masked layer of each item (corners, clipping). */\n mask?: string;\n /** The state layer painted over an item that can be activated. */\n stateLayer?: string;\n}\n\n/** Words the carousel announces. */\nexport interface CarouselLabels {\n /**\n * Each item's name: its position (from 1) and the number of items, as formatted text.\n * @default (position, count) => `${position} of ${count}`\n */\n slide: (position: string, count: string) => string;\n /** What the carousel is, read before its name (`aria-roledescription`). @default \"carousel\" */\n carousel: string;\n /** What each item is, read before its name (`aria-roledescription`). @default \"slide\" */\n item: string;\n}\n\nconst DEFAULT_LABELS: CarouselLabels = {\n slide: (position, count) => `${position} of ${count}`,\n carousel: 'carousel',\n item: 'slide',\n};\n\n/** Padding around the items, in px. `start` and `end` run along the carousel. */\nexport interface CarouselContentPadding {\n /** Before the first item. */\n start: number;\n /** After the last item. */\n end: number;\n /** Either side across the carousel. */\n cross: number;\n}\n\nexport type CarouselVariant =\n 'multi-browse' | 'uncontained' | 'multi-aspect' | 'hero' | 'full-screen';\n\n/** A carousel needs a name: `aria-label`, or `aria-labelledby` pointing at a visible title. */\ntype Naming =\n | {\n /** Accessible name when there's no visible title. */\n 'aria-label': string;\n }\n | {\n /** The id of the visible title that names the carousel. */\n 'aria-labelledby': string;\n };\n\ninterface CarouselOwnProps extends RootDOMProps {\n /** The root element's id. */\n id?: string;\n /**\n * `multi-browse` shows large, medium and small items; `uncontained` shows items of one\n * size that flow past the end edge, never masked or resized; `multi-aspect` is uncontained with items of their own widths\n * (9:16 to 16:9); `hero` shows one large item with small ones beside it; `full-screen`\n * shows one edge-to-edge item at a time. @default \"multi-browse\"\n */\n variant?: CarouselVariant;\n /**\n * The direction the items scroll in.\n * @default \"horizontal\", or \"vertical\" when full-screen\n */\n orientation?: 'horizontal' | 'vertical';\n /**\n * Preferred size of the large items along the carousel (multi-browse, hero). Hero defaults\n * to the whole carousel. @default 186\n */\n preferredItemSize?: number;\n /** Size of the items along the carousel (uncontained). @default 240 */\n itemSize?: number;\n /** Space between items, px. @default 8, or 16 when full-screen */\n itemSpacing?: number;\n /** Centre the large hero item between small ones. @default \"start\" */\n heroAlignment?: 'start' | 'center';\n /** Minimum size of the small items. @default 40 */\n minSmallItemSize?: number;\n /** Maximum size of the small items. @default 56 */\n maxSmallItemSize?: number;\n /**\n * Padding around the items. Items scroll over the start and end padding.\n * @default { start: 16, end: 16, cross: 8 } (uncontained and multi-aspect: no end padding;\n * full-screen: none)\n */\n contentPadding?: Partial<CarouselContentPadding>;\n /**\n * Snap uncontained and multi-aspect carousels to their items. Multi-browse, hero and\n * full-screen carousels always snap. @default \"none\"\n */\n snap?: 'none' | 'item';\n /**\n * Called when an item is pressed, or activated with Enter or Space. Items then show\n * hover, focus and pressed states.\n */\n onAction?: (index: number) => void;\n /** Indexes of items that can't be activated. */\n disabledIndexes?: readonly number[];\n /**\n * Words the carousel announces, for localisation. A {@link LabelsProvider} sets them for the\n * whole app.\n */\n labels?: Partial<CarouselLabels>;\n /** The items, e.g. images; each fills its slot and is clipped to its keyline. */\n children: ReactNode;\n /** Ref to the root `<div>` element. */\n ref?: Ref<HTMLDivElement>;\n /** Classes for the root element, merged with `classNames.root`; consumer classes win. */\n className?: string;\n /** Inline styles for the root element. */\n style?: CSSProperties;\n /** Classes for each part, by slot. `classNames.root` is merged with `className`. */\n classNames?: CarouselClassNames;\n /** `data-*` attributes, like the other DOM attributes, go to the root. */\n [data: `data-${string}`]: string | number | boolean | undefined;\n}\n\nexport type CarouselProps = CarouselOwnProps & Naming;\n\n/**\n * Variants whose items change size along keylines as they scroll. Uncontained and\n * multi-aspect items keep one size and flow past the end edge (M3: \"items are a single size\n * and flow past the edge of the screen\"), so they are never masked or cropped.\n */\nconst KEYLINE_VARIANTS: readonly CarouselVariant[] = ['multi-browse', 'hero'];\n\nfunction defaultPadding(variant: CarouselVariant): CarouselContentPadding {\n if (variant === 'full-screen') return { start: 0, end: 0, cross: 0 };\n if (variant === 'uncontained' || variant === 'multi-aspect')\n return { start: 16, end: 0, cross: 8 };\n return { start: 16, end: 16, cross: 8 };\n}\n\nconst TABBABLE =\n 'a[href],area[href],button,input,select,textarea,iframe,summary,[contenteditable],[tabindex]';\n\n/** The next (or previous) element in the tab order outside `container`. */\nfunction tabbableOutside(container: HTMLElement, forward: boolean): HTMLElement | undefined {\n const candidates = Array.from(\n container.ownerDocument.querySelectorAll<HTMLElement>(TABBABLE),\n ).filter(\n (element) =>\n !container.contains(element) &&\n element.tabIndex >= 0 &&\n !element.matches(':disabled') &&\n !element.closest('[inert],[hidden]'),\n );\n const isAfter = (element: HTMLElement) =>\n Boolean(container.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_FOLLOWING);\n return forward ? candidates.find(isAfter) : candidates.filter((e) => !isAfter(e)).pop();\n}\n\n/**\n * M3 Expressive carousel: a scrollable row (or column) of items whose sizes follow Compose's\n * keylines as they scroll: multi-browse (large, medium and small items), uncontained,\n * multi-aspect, hero or full-screen. Give it a height (or a width, vertical) with\n * `className`. Tab or the arrow keys move between items; Up and Down (Left and Right when\n * vertical) leave the carousel. Under reduced motion every item keeps the large size.\n *\n * @example\n * <Carousel aria-label=\"Photos\" className=\"h-[221px]\">\n * {photos.map((photo) => <img key={photo.id} src={photo.src} alt={photo.alt} className=\"size-full object-cover\" />)}\n * </Carousel>\n */\nexport function Carousel({\n variant = 'multi-browse',\n orientation = variant === 'full-screen' ? 'vertical' : 'horizontal',\n preferredItemSize,\n itemSize = 240,\n itemSpacing = variant === 'full-screen' ? 16 : 8,\n heroAlignment = 'start',\n minSmallItemSize,\n maxSmallItemSize,\n contentPadding,\n snap = 'none',\n onAction,\n disabledIndexes,\n labels: labelsProp,\n children,\n ref,\n className,\n style,\n classNames,\n ...rest\n}: CarouselProps) {\n const { root: data, rest: labelling } = splitRootProps(rest);\n const { 'aria-label': label, 'aria-labelledby': labelledBy } = labelling as {\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n };\n const labels = useLabels('Carousel', DEFAULT_LABELS, labelsProp);\n const numbers = useNumberFormatter();\n const items = Children.toArray(children).filter(isValidElement);\n const count = items.length;\n const vertical = orientation === 'vertical';\n const reduceMotion = useReducedMotion() ?? false;\n const padding = { ...defaultPadding(variant), ...contentPadding };\n const keylined = KEYLINE_VARIANTS.includes(variant);\n // Under reduced motion items keep the large size, unmasked (the spec's reduced motion).\n const masked = keylined && !reduceMotion;\n const rootRef = useObjectRef(ref);\n const scrollerRef = useRef<HTMLDivElement>(null);\n const slotRefs = useRef<(HTMLDivElement | null)[]>([]);\n const maskRefs = useRef<(HTMLDivElement | null)[]>([]);\n const hitRefs = useRef<(HTMLSpanElement | null)[]>([]);\n const [space, setSpace] = useState(0);\n const [crossSize, setCrossSize] = useState(0);\n\n const strategy = computeStrategy();\n function computeStrategy(): Strategy | null {\n if (space === 0 || count === 0 || !keylined) return null;\n const keylines =\n variant === 'hero'\n ? heroKeylineList(\n space,\n preferredItemSize,\n itemSpacing,\n count,\n heroAlignment === 'center',\n minSmallItemSize,\n maxSmallItemSize,\n )\n : multiBrowseKeylineList(\n space,\n preferredItemSize ?? 186,\n itemSpacing,\n count,\n minSmallItemSize,\n maxSmallItemSize,\n );\n const result = masked\n ? createStrategy(\n keylines,\n space,\n itemSpacing,\n padding.start,\n padding.end,\n maxSmallItemSize ?? MAX_SMALL_ITEM_SIZE,\n )\n : createStrategy(keylines, space, itemSpacing);\n return result.isValid ? result : null;\n }\n\n // Writes each item's mask and translation for the current scroll position, without\n // re-rendering (Compose's `carouselItem` layer block).\n const update = () => {\n const scroller = scrollerRef.current;\n if (!scroller || !strategy || !masked) return;\n const rtl = !vertical && getComputedStyle(scroller).direction === 'rtl';\n const scroll = Math.abs(vertical ? scroller.scrollTop : scroller.scrollLeft);\n const max = maxScrollOffset(strategy, count);\n const keylines = keylinesForScrollOffset(strategy, scroll, max);\n const full = strategy.itemMainAxisSize;\n const round = (value: number) => Math.round(value * 100) / 100;\n const places = maskRefs.current.map((_, index) => placeItem(strategy, keylines, index, scroll));\n // Each item's visible part, from the viewport's start edge (slots are `full` apart plus\n // the spacing; the mask is centred in its slot and moved by its translation).\n const spans = places.map(({ size, translation }, index) => ({\n start: index * (full + itemSpacing) + translation + (full - size) / 2 - scroll,\n size,\n }));\n maskRefs.current.forEach((mask, index) => {\n if (!mask) return;\n const { size, translation } = places[index]!;\n const inset = round(Math.max(0, (full - size) / 2));\n const move = round(rtl ? -translation : translation);\n mask.style.translate = vertical ? `0 ${move}px` : `${move}px 0`;\n mask.style.clipPath = vertical\n ? `inset(${inset}px 0 ${inset}px 0 round var(--m3-carousel-item-corner))`\n : `inset(0 ${inset}px 0 ${inset}px round var(--m3-carousel-item-corner))`;\n mask.style.setProperty('--m3-carousel-item-size', `${round(size)}px`);\n // The press area follows the mask, widened to M3's 48px touch target for a small item.\n const hit = hitRefs.current[index];\n if (!hit) return;\n const reach = pressAreaReach(spans, index, itemSpacing, space);\n const start = round(Math.max(0, inset - reach.start));\n const end = round(Math.max(0, inset - reach.end));\n hit.style.translate = mask.style.translate;\n hit.style.clipPath = vertical\n ? `inset(${start}px 0 ${end}px 0)`\n : rtl\n ? `inset(0 ${start}px 0 ${end}px)`\n : `inset(0 ${end}px 0 ${start}px)`;\n });\n };\n\n useLayoutEffect(() => {\n const scroller = scrollerRef.current;\n if (!scroller) return;\n const measure = () => {\n const computed = getComputedStyle(scroller);\n const px = (value: string) => Number.parseFloat(value) || 0;\n // The keylines span the scroller's padding box along the carousel: Compose handles\n // the start and end padding in the keylines, not in the layout.\n setSpace(vertical ? scroller.clientHeight : scroller.clientWidth);\n setCrossSize(\n vertical\n ? scroller.clientWidth - px(computed.paddingLeft) - px(computed.paddingRight)\n : scroller.clientHeight - px(computed.paddingTop) - px(computed.paddingBottom),\n );\n };\n measure();\n if (typeof ResizeObserver === 'undefined') return;\n const observer = new ResizeObserver(measure);\n observer.observe(scroller);\n return () => observer.disconnect();\n }, [vertical]);\n\n useLayoutEffect(() => {\n // Leaving the masked layout (reduced motion) clears what `update` wrote.\n if (!masked)\n [...maskRefs.current, ...hitRefs.current].forEach((layer) => {\n layer?.style.removeProperty('translate');\n layer?.style.removeProperty('clip-path');\n layer?.style.removeProperty('--m3-carousel-item-size');\n });\n update();\n const scroller = scrollerRef.current;\n if (!scroller || !masked) return;\n let frame = 0;\n const onScroll = () => {\n if (!frame)\n frame = requestAnimationFrame(() => {\n frame = 0;\n update();\n });\n };\n scroller.addEventListener('scroll', onScroll, { passive: true });\n return () => {\n scroller.removeEventListener('scroll', onScroll);\n cancelAnimationFrame(frame);\n };\n });\n\n // Mouse drag scrolls the carousel (touch and trackpads scroll natively); snapping is\n // paused while dragging and resumes on release. A drag never activates an item. The\n // handlers run in the capture phase: an actionable item's press handler stops the\n // pointer events' propagation and focuses the item on pointer down, so in the bubble\n // phase the scroller never saw the press (the focus then read as keyboard focus and\n // scrolled the item to its snap position, and a drag neither scrolled nor was ignored).\n const drag = useRef<{ start: number; scroll: number; id: number } | null>(null);\n const dragged = useRef(false);\n const pointerFocus = useRef(false);\n const onPointerDown = (event: PointerEvent<HTMLDivElement>) => {\n pointerFocus.current = true;\n dragged.current = false;\n if (event.pointerType !== 'mouse' || event.button !== 0) return;\n const scroller = event.currentTarget;\n drag.current = {\n start: vertical ? event.clientY : event.clientX,\n scroll: vertical ? scroller.scrollTop : scroller.scrollLeft,\n id: event.pointerId,\n };\n };\n const onPointerMove = (event: PointerEvent<HTMLDivElement>) => {\n const state = drag.current;\n if (!state) return;\n const scroller = event.currentTarget;\n const delta = (vertical ? event.clientY : event.clientX) - state.start;\n if (Math.abs(delta) > 3 && !scroller.hasAttribute('data-dragging')) {\n dragged.current = true;\n scroller.setAttribute('data-dragging', '');\n scroller.setPointerCapture?.(state.id);\n }\n if (vertical) scroller.scrollTop = state.scroll - delta;\n else scroller.scrollLeft = state.scroll - delta;\n };\n const endDrag = (event: PointerEvent<HTMLDivElement>) => {\n drag.current = null;\n event.currentTarget.removeAttribute('data-dragging');\n };\n\n // Brings a focused item into view: keylined carousels scroll it to its snap position\n // (the focal keyline); the others just far enough to show it.\n const reveal = (index: number) => {\n const scroller = scrollerRef.current;\n const slot = slotRefs.current[index];\n if (!scroller || !slot) return;\n const behavior: ScrollBehavior = reduceMotion ? 'instant' : 'smooth';\n if (strategy && masked) {\n const step = strategy.itemMainAxisSize + itemSpacing;\n const max = maxScrollOffset(strategy, count);\n const target = Math.min(\n max,\n Math.max(0, index * step - snapPositionOffset(strategy, index, count)),\n );\n const rtl = !vertical && getComputedStyle(scroller).direction === 'rtl';\n scroller.scrollTo?.(\n vertical ? { top: target, behavior } : { left: rtl ? -target : target, behavior },\n );\n } else {\n slot.scrollIntoView?.({ block: 'nearest', inline: 'nearest', behavior });\n }\n };\n\n const onSlotFocus = (index: number, event: FocusEvent<HTMLDivElement>) => {\n if (event.target !== event.currentTarget) return;\n // A press focuses the item without moving the carousel.\n if (pointerFocus.current) return;\n reveal(index);\n };\n\n const focusSlot = (index: number) => {\n const slot = slotRefs.current[Math.max(0, Math.min(count - 1, index))];\n if (!slot) return;\n pointerFocus.current = true;\n slot.focus({ preventScroll: true });\n pointerFocus.current = false;\n reveal(Number(slot.dataset.index));\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n pointerFocus.current = false;\n const target = event.target as HTMLElement;\n const index = slotRefs.current.indexOf(target as HTMLDivElement);\n if (index < 0) return;\n const rtl = getComputedStyle(target).direction === 'rtl';\n const [prev, next, leaveBack, leaveForward] = vertical\n ? ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']\n : rtl\n ? ['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown']\n : ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];\n let handled = true;\n if (event.key === next) focusSlot(index + 1);\n else if (event.key === prev) focusSlot(index - 1);\n else if (event.key === 'Home') focusSlot(0);\n else if (event.key === 'End') focusSlot(count - 1);\n else if (event.key === leaveForward || event.key === leaveBack) {\n const root = rootRef.current;\n const outside = root && tabbableOutside(root, event.key === leaveForward);\n if (outside) outside.focus();\n else handled = false;\n } else handled = false;\n if (handled) event.preventDefault();\n };\n\n const snapping =\n variant === 'hero' || variant === 'full-screen'\n ? 'single'\n : variant === 'multi-browse' || snap === 'item'\n ? 'many'\n : 'none';\n const styles = carouselStyles({\n orientation,\n snapping,\n masked,\n aspect: variant === 'multi-aspect',\n interactive: onAction !== undefined,\n fullBleed: variant === 'full-screen',\n });\n const fallbackSize = variant === 'uncontained' ? itemSize : (preferredItemSize ?? 186);\n const slotSize =\n variant === 'full-screen'\n ? space || undefined\n : variant === 'multi-aspect'\n ? undefined\n : (strategy?.itemMainAxisSize ?? fallbackSize);\n // The scroller pads the items itself only when the keylines don't.\n const layoutPadding = masked ? { start: 0, end: 0 } : padding;\n const total = numbers.format(count);\n const disabled = new Set(disabledIndexes);\n\n return (\n <div\n {...data}\n id={(labelling as { id?: string }).id}\n ref={rootRef}\n role=\"region\"\n aria-roledescription={labels.carousel}\n aria-label={label}\n aria-labelledby={labelledBy}\n // The component token's hooks (carousel-tokens.ts), also public styling hooks for apps.\n data-tokens={mergeDataTokens('md-comp-carousel', data)}\n data-variant={variant}\n style={style}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n <div\n ref={scrollerRef}\n onPointerDownCapture={onPointerDown}\n onPointerMoveCapture={onPointerMove}\n onPointerUpCapture={endDrag}\n onPointerCancelCapture={endDrag}\n onKeyDown={onKeyDown}\n style={\n {\n gap: itemSpacing,\n [vertical ? 'paddingInline' : 'paddingBlock']: padding.cross,\n [vertical ? 'paddingBlockStart' : 'paddingInlineStart']: layoutPadding.start,\n [vertical ? 'paddingBlockEnd' : 'paddingInlineEnd']: layoutPadding.end,\n [vertical ? 'scrollPaddingBlockStart' : 'scrollPaddingInlineStart']:\n layoutPadding.start,\n [vertical ? 'scrollPaddingBlockEnd' : 'scrollPaddingInlineEnd']: layoutPadding.end,\n '--m3-carousel-cross-size': `${crossSize}px`,\n } as CSSProperties\n }\n className={styles.scroller({ class: classNames?.scroller })}\n >\n {items.map((item, index) => (\n <CarouselItem\n key={item.key ?? index}\n index={index}\n label={labels.slide(numbers.format(index + 1), total)}\n roleDescription={labels.item}\n disabled={disabled.has(index)}\n onAction={\n onAction &&\n (() => {\n if (!dragged.current) onAction(index);\n })\n }\n onFocus={(event) => onSlotFocus(index, event)}\n slotRef={(element) => {\n slotRefs.current[index] = element;\n }}\n maskRef={(element) => {\n maskRefs.current[index] = element;\n }}\n hitRef={(element) => {\n hitRefs.current[index] = element;\n }}\n style={{\n ...(slotSize === undefined ? {} : { [vertical ? 'height' : 'width']: slotSize }),\n [vertical ? 'scrollMarginBlockStart' : 'scrollMarginInlineStart']:\n strategy && masked ? snapPositionOffset(strategy, index, count) : 0,\n }}\n styles={styles}\n classNames={classNames}\n >\n {item}\n </CarouselItem>\n ))}\n </div>\n </div>\n );\n}\n\nfunction CarouselItem({\n index,\n label,\n roleDescription,\n disabled,\n onAction,\n onFocus,\n slotRef,\n maskRef,\n hitRef,\n style,\n styles,\n classNames,\n children,\n}: {\n index: number;\n label: string;\n roleDescription: string;\n disabled: boolean;\n onAction?: () => void;\n onFocus: (event: FocusEvent<HTMLDivElement>) => void;\n slotRef: (element: HTMLDivElement | null) => void;\n maskRef: (element: HTMLDivElement | null) => void;\n hitRef: (element: HTMLSpanElement | null) => void;\n style: CSSProperties;\n styles: ReturnType<typeof carouselStyles>;\n classNames?: CarouselClassNames;\n children: ReactNode;\n}) {\n const ref = useRef<HTMLDivElement | null>(null);\n const interactive = onAction !== undefined;\n const { interactionProps, dataAttributes } = useM3Interaction(\n { isDisabled: disabled || !interactive, onPress: onAction },\n ref,\n );\n const { 'data-focus-visible': focusVisible, ...states } = dataAttributes;\n return (\n <div\n {...interactionProps}\n ref={(element) => {\n ref.current = element;\n slotRef(element);\n }}\n role=\"group\"\n aria-roledescription={roleDescription}\n aria-label={label}\n aria-disabled={(interactive && disabled) || undefined}\n tabIndex={0}\n data-slot=\"item\"\n data-index={index}\n data-focus-visible={focusVisible}\n data-disabled={(interactive && disabled) || undefined}\n onFocus={(event) => {\n interactionProps.onFocus?.(event);\n onFocus(event);\n }}\n style={style}\n className={styles.item({ class: classNames?.item })}\n >\n {/* Where the item takes presses: under the mask, clipped to it or a little wider. */}\n <span aria-hidden=\"true\" ref={hitRef} data-slot=\"hit-area\" className={styles.hitArea()} />\n <div\n ref={maskRef}\n {...(interactive ? states : {})}\n className={styles.mask({ class: classNames?.mask })}\n >\n {children}\n {interactive && (\n <span\n aria-hidden=\"true\"\n {...states}\n data-focus-visible={focusVisible}\n className={styles.stateLayer({ class: classNames?.stateLayer })}\n />\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -4,8 +4,8 @@ import { VariantProps } from "../../utils/tv.js";
4
4
  export declare const checkboxStyles: import("tailwind-variants").TVReturnType<{
5
5
  [key: string]: {
6
6
  [key: string]: import("tailwind-variants").ClassValue | {
7
- icon?: import("tailwind-variants").ClassValue;
8
7
  mark?: import("tailwind-variants").ClassValue;
8
+ icon?: import("tailwind-variants").ClassValue;
9
9
  check?: import("tailwind-variants").ClassValue;
10
10
  box?: import("tailwind-variants").ClassValue;
11
11
  dash?: import("tailwind-variants").ClassValue;
@@ -14,15 +14,15 @@ export declare const checkboxStyles: import("tailwind-variants").TVReturnType<{
14
14
  } | {
15
15
  kind: {
16
16
  circle: import("tailwind-variants").ClassValue | {
17
- icon?: import("tailwind-variants").ClassValue;
18
17
  mark?: import("tailwind-variants").ClassValue;
18
+ icon?: import("tailwind-variants").ClassValue;
19
19
  check?: import("tailwind-variants").ClassValue;
20
20
  box?: import("tailwind-variants").ClassValue;
21
21
  dash?: import("tailwind-variants").ClassValue;
22
22
  };
23
23
  track: import("tailwind-variants").ClassValue | {
24
- icon?: import("tailwind-variants").ClassValue;
25
24
  mark?: import("tailwind-variants").ClassValue;
25
+ icon?: import("tailwind-variants").ClassValue;
26
26
  check?: import("tailwind-variants").ClassValue;
27
27
  box?: import("tailwind-variants").ClassValue;
28
28
  dash?: import("tailwind-variants").ClassValue;
@@ -2,6 +2,6 @@ import { ComponentTokenSet } from "../../tokens/component.js";
2
2
  import "../../index.js";
3
3
  //#region src/components/checkbox/checkbox-tokens.d.ts
4
4
  /** The tokens of {@link Checkbox}: `--md-comp-checkbox-<part>`. */
5
- export declare const checkboxTokenSet: ComponentTokenSet<"selected-icon-color" | "selected-container-color" | "unselected-outline-color">;
5
+ export declare const checkboxTokenSet: ComponentTokenSet<"selected-container-color" | "selected-icon-color" | "unselected-outline-color">;
6
6
  //#endregion
7
7
  //# sourceMappingURL=checkbox-tokens.d.ts.map
@@ -90,7 +90,10 @@ interface FilterChipOwnProps extends Omit<ChipCommonProps, 'classNames'> {
90
90
  * (M3: for medium and expanded windows). Backspace or Delete on the chip also calls it.
91
91
  */
92
92
  onRemove?: () => void;
93
- /** Words the chip announces, with English defaults. */
93
+ /**
94
+ * Words the chip announces, with English defaults. A {@link LabelsProvider} sets them for the
95
+ * whole app.
96
+ */
94
97
  labels?: Partial<ChipLabels>;
95
98
  /** `surface-container-low` at level 1 instead of an outline. @default false */
96
99
  elevated?: boolean;
@@ -129,7 +132,10 @@ interface InputChipOwnProps {
129
132
  onPress?: ButtonBaseActionProps['onPress'];
130
133
  /** Shows a remove button; also called on Backspace or Delete. */
131
134
  onRemove?: () => void;
132
- /** Words the chip announces, with English defaults. */
135
+ /**
136
+ * Words the chip announces, with English defaults. A {@link LabelsProvider} sets them for the
137
+ * whole app.
138
+ */
133
139
  labels?: Partial<ChipLabels>;
134
140
  /** The flat chip's border. @default "standard" */
135
141
  outline?: ChipOutline;
@@ -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 { ButtonBase, ToggleGroupStateContext } from "../../primitives/ButtonBase.js";
5
6
  import { TouchTarget } from "../../primitives/TouchTarget.js";
6
7
  import { mergeDataTokens } from "../../utils/merge-data-tokens.js";
@@ -76,10 +77,7 @@ const removeKeys = (onRemove, disabled) => (event) => {
76
77
  /** The remove button: named "Remove" + the chip's label, with a 48px target. */
77
78
  function RemoveButton({ styles, className, labelId, labels, onRemove, disabled, describedBy }) {
78
79
  const removeId = useId();
79
- const { remove } = {
80
- ...DEFAULT_LABELS,
81
- ...labels
82
- };
80
+ const { remove } = useLabels("Chip", DEFAULT_LABELS, labels);
83
81
  return /* @__PURE__ */ jsxs(ButtonBase, {
84
82
  disabled,
85
83
  onPress: onRemove,
@@ -282,10 +280,7 @@ function InputChip({ children, leadingIcon, avatar, selected, onPress, onRemove,
282
280
  const selectedId = useId();
283
281
  const split = onRemove ? onPress ? "two" : "removeOnly" : "none";
284
282
  const announceSelected = Boolean(selected) && !onPress;
285
- const selectedWord = {
286
- ...DEFAULT_LABELS,
287
- ...labels
288
- }.selected;
283
+ const selectedWord = useLabels("Chip", DEFAULT_LABELS, labels).selected;
289
284
  const styles = chipStyles({
290
285
  kind: "input",
291
286
  split,