@windmorph/react 0.2.1 → 0.2.3

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 (55) hide show
  1. package/dist/components/autocomplete/Autocomplete.js +1 -1
  2. package/dist/components/autocomplete/Autocomplete.js.map +1 -1
  3. package/dist/components/carousel/carousel-styles.d.ts +6 -15
  4. package/dist/components/carousel/carousel-styles.js +4 -7
  5. package/dist/components/carousel/carousel-styles.js.map +1 -1
  6. package/dist/components/checkbox/checkbox-tokens.d.ts +1 -1
  7. package/dist/components/date-picker/DatePicker.js +11 -4
  8. package/dist/components/date-picker/DatePicker.js.map +1 -1
  9. package/dist/components/date-picker/date-picker-styles.d.ts +3 -3
  10. package/dist/components/fab-menu/FabMenu.js +2 -2
  11. package/dist/components/fab-menu/FabMenu.js.map +1 -1
  12. package/dist/components/list/List.d.ts +9 -0
  13. package/dist/components/list/List.js +7 -4
  14. package/dist/components/list/List.js.map +1 -1
  15. package/dist/components/list/list-styles.js +6 -6
  16. package/dist/components/list/list-styles.js.map +1 -1
  17. package/dist/components/list/list-tokens.d.ts +1 -1
  18. package/dist/components/list/list-tokens.js +50 -0
  19. package/dist/components/list/list-tokens.js.map +1 -1
  20. package/dist/components/menu/Menu.d.ts +13 -0
  21. package/dist/components/menu/Menu.js +14 -1
  22. package/dist/components/menu/Menu.js.map +1 -1
  23. package/dist/components/navigation/NavigationRail.js +52 -15
  24. package/dist/components/navigation/NavigationRail.js.map +1 -1
  25. package/dist/components/navigation/navigation-styles.js +1 -1
  26. package/dist/components/navigation/navigation-styles.js.map +1 -1
  27. package/dist/components/search/SearchBar.js +19 -34
  28. package/dist/components/search/SearchBar.js.map +1 -1
  29. package/dist/components/search/SearchField.d.ts +111 -0
  30. package/dist/components/search/SearchField.js +128 -0
  31. package/dist/components/search/SearchField.js.map +1 -0
  32. package/dist/components/search/search-field-frame.js +36 -0
  33. package/dist/components/search/search-field-frame.js.map +1 -0
  34. package/dist/components/search/search-styles.d.ts +58 -0
  35. package/dist/components/search/search-styles.js +29 -4
  36. package/dist/components/search/search-styles.js.map +1 -1
  37. package/dist/components/select/search-list.js +8 -13
  38. package/dist/components/select/search-list.js.map +1 -1
  39. package/dist/components/slider/Slider.js +5 -1
  40. package/dist/components/slider/Slider.js.map +1 -1
  41. package/dist/components/snackbar/snackbar-tokens.d.ts +1 -1
  42. package/dist/components/text-field/TextField.js +19 -5
  43. package/dist/components/text-field/TextField.js.map +1 -1
  44. package/dist/components/text-field/text-field-styles.js +2 -2
  45. package/dist/components/text-field/text-field-styles.js.map +1 -1
  46. package/dist/components/toolbar/toolbar-styles.js +2 -2
  47. package/dist/components/toolbar/toolbar-styles.js.map +1 -1
  48. package/dist/index.d.ts +3 -2
  49. package/dist/index.js +3 -2
  50. package/dist/primitives/use-m3-interaction.js +10 -6
  51. package/dist/primitives/use-m3-interaction.js.map +1 -1
  52. package/dist/styles/generated/components.css +5 -0
  53. package/dist/styles/utilities.css +22 -15
  54. package/dist/styles.compiled.css +1 -1
  55. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"search-list.js","names":[],"sources":["../../../src/components/select/search-list.tsx"],"sourcesContent":["'use client';\n\nimport {\n useCallback,\n useEffect,\n useRef,\n useState,\n type HTMLAttributes,\n type RefObject,\n} from 'react';\nimport { useAutocomplete, useFocusRing, useSearchField } from 'react-aria';\nimport { useListState, type Node } from 'react-stately';\nimport type { ListProps } from 'react-stately/useListState';\nimport { cn } from '../../utils/cn';\nimport { menuStyles } from '../menu/menu-styles';\nimport { searchBarStyles } from '../search/search-styles';\nimport { FOCUSABLE_DISABLED_OPTIONS, OptionList, type OptionListClassNames } from './option-list';\n\n/** Material Symbols `search`. */\nconst SearchIcon = () => (\n <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z\" />\n </svg>\n);\n\n/**\n * Keeps the items that match, and the sections that still hold one (as React Stately's\n * combobox filters), so a section with no matches disappears with its heading.\n */\nfunction filterNodes<T>(nodes: Iterable<Node<T>>, matches: (node: Node<T>) => boolean): Node<T>[] {\n const kept: Node<T>[] = [];\n for (const node of nodes) {\n if (node.type === 'section') {\n const children = filterNodes(node.childNodes, matches);\n if (children.some((child) => child.type === 'item')) {\n kept.push({ ...node, childNodes: children });\n }\n } else if (node.type !== 'item' || matches(node)) {\n kept.push(node);\n }\n }\n return kept;\n}\n\nexport interface SearchListProps<T> {\n /** The options, their selection and disabled keys, as for `useListState`. */\n listProps: ListProps<T>;\n /** The typed text. */\n search: string;\n onSearchChange: (search: string) => void;\n /** Whether an option matches the search; `null` when the app filters the options itself. */\n filter: ((textValue: string, search: string, key: Node<T>['key']) => boolean) | null;\n /** Focus the search when the list opens (in a menu; a sheet shows the list first). */\n autoFocusSearch: boolean;\n /** Names the list (the field's label): `aria-labelledby`, or `aria-label` without one. */\n labelling: { 'aria-label'?: string | undefined; 'aria-labelledby'?: string | undefined };\n /** In a menu, the panel is the dialog the field opens (`role`, `id`, its name). */\n dialogProps?: HTMLAttributes<HTMLDivElement> | undefined;\n labels: { search: string; noResults: string; loading: string };\n loading: boolean;\n isBlocked: ((key: Node<T>['key']) => boolean) | undefined;\n /** In a sheet: no menu panel of its own. */\n embedded: boolean;\n classNames?: (OptionListClassNames & { search?: string }) | undefined;\n}\n\n/*\n * A dense search field (44, 40 or 36px tall at -1 to -3) extends its hit area by up to 4px\n * below it (the gap above the list) and up to 4px above it (the panel's padding, which clips\n * anything further): 48px at -1 and -2, 44px at -3. Like dense menu rows, it can't reach 48px\n * at -3 without covering the first option; it stays well above WCAG 2.5.8's 24px.\n */\nconst denseSearchTarget = [\n 'relative',\n \"before:absolute before:inset-x-0 before:bottom-full before:h-[clamp(0px,calc(var(--md-sys-density,0)*-4px-clamp(0px,calc(var(--md-sys-density,0)*-2px),4px)),4px)] before:content-['']\",\n \"after:absolute after:inset-x-0 after:top-full after:h-[clamp(0px,calc(var(--md-sys-density,0)*-2px),4px)] after:content-['']\",\n];\n\n/**\n * A search field over an option list, wired as one editable combobox with list autocomplete\n * (React Aria `useAutocomplete`): typing filters the options, the arrow keys move through\n * them while focus stays in the search (`aria-activedescendant`), Enter chooses, and Escape\n * clears the search, then reaches the menu or sheet to close it. The search is the M3 search\n * bar's field at 48px, M3's minimum touch target, so it doesn't dominate the 44px rows.\n * Used by `Select`'s `searchable` and `PhoneField`'s country field.\n */\nexport function SearchList<T>({\n listProps,\n search,\n onSearchChange,\n filter,\n autoFocusSearch,\n labelling,\n dialogProps,\n labels,\n loading,\n isBlocked,\n embedded,\n classNames,\n}: SearchListProps<T>) {\n const matches = useCallback(\n (nodes: Iterable<Node<T>>) =>\n filter ? filterNodes(nodes, (node) => filter(node.textValue, search, node.key)) : [...nodes],\n [filter, search],\n );\n const state = useListState<T>({ ...listProps, ...FOCUSABLE_DISABLED_OPTIONS, filter: matches });\n\n const [focusedNodeId, setFocusedNodeId] = useState<string | null>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const listBoxRef = useRef<HTMLUListElement>(null);\n const {\n inputProps: autocompleteInputProps,\n collectionProps,\n collectionRef,\n } = useAutocomplete(\n { inputRef, collectionRef: listBoxRef },\n { inputValue: search, setInputValue: onSearchChange, focusedNodeId, setFocusedNodeId },\n );\n const { inputProps } = useSearchField(\n { ...autocompleteInputProps, 'aria-label': labels.search, placeholder: labels.search },\n { value: search, setValue: onSearchChange },\n inputRef,\n );\n const { focusProps, isFocusVisible } = useFocusRing({ within: true, isTextInput: true });\n\n useEffect(() => {\n if (autoFocusSearch) inputRef.current?.focus();\n }, [autoFocusSearch]);\n\n const field = searchBarStyles({ hasLeading: true });\n const menu = menuStyles({ variant: 'standard', groupPosition: 'only' });\n\n return (\n <div\n {...dialogProps}\n // The menu's component tokens (menu-tokens.ts) for its panel and options.\n data-tokens=\"md-comp-menus\"\n data-variant=\"standard\"\n className={cn(\n 'flex min-h-0 flex-col',\n embedded ? 'flex-1' : [menu.group(), 'max-h-[inherit] overflow-hidden'],\n )}\n >\n {/* 48px (4px less per density step), 4px above the list, like the panel's 4px padding\n around it. In a sheet the whole content scrolls (its swipe-to-dismiss reads that\n scroller's position), so the search sticks to the top, over the options passing under. */}\n <div\n className={cn(\n 'flex h-[calc(52px+var(--md-sys-density,0)*4px)] shrink-0 pb-[4px]',\n embedded && 'sticky top-0 z-[1] bg-[var(--_md-comp-sheet-bottom-docked-container-color)]',\n )}\n >\n <div\n {...focusProps}\n data-tokens=\"md-comp-search-bar\"\n data-focus-visible={isFocusVisible || undefined}\n // A press on the hit area around a dense field (below) puts the caret in the input.\n onPointerDown={(event) => {\n if (event.target !== event.currentTarget) return;\n event.preventDefault();\n inputRef.current?.focus();\n }}\n className={field.field({ class: cn(denseSearchTarget, classNames?.search) })}\n >\n <span data-slot=\"leading\" className={field.leading({ class: 'h-full' })}>\n <SearchIcon />\n </span>\n <input {...inputProps} ref={inputRef} data-slot=\"input\" className={field.input()} />\n </div>\n </div>\n <OptionList\n embedded\n animateFiltering\n listBoxProps={{\n ...collectionProps,\n ...labelling,\n // Escape belongs to the search and then the menu; the list must not take it to\n // clear the selection (which also kept a chosen option's menu from closing).\n escapeKeyBehavior: 'none',\n }}\n state={state}\n listBoxRef={collectionRef as RefObject<HTMLUListElement | null>}\n emptyLabel={labels.noResults}\n loading={loading}\n loadingLabel={labels.loading}\n isBlocked={isBlocked}\n classNames={classNames}\n />\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"search-list.js","names":[],"sources":["../../../src/components/select/search-list.tsx"],"sourcesContent":["'use client';\n\nimport {\n useCallback,\n useEffect,\n useRef,\n useState,\n type HTMLAttributes,\n type RefObject,\n} from 'react';\nimport { useAutocomplete, useSearchField } from 'react-aria';\nimport { useListState, type Node } from 'react-stately';\nimport type { ListProps } from 'react-stately/useListState';\nimport { cn } from '../../utils/cn';\nimport { menuStyles } from '../menu/menu-styles';\nimport { SearchFieldFrame } from '../search/search-field-frame';\nimport { searchBarStyles } from '../search/search-styles';\nimport { FOCUSABLE_DISABLED_OPTIONS, OptionList, type OptionListClassNames } from './option-list';\n\n/** Material Symbols `search`. */\nconst SearchIcon = () => (\n <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z\" />\n </svg>\n);\n\n/**\n * Keeps the items that match, and the sections that still hold one (as React Stately's\n * combobox filters), so a section with no matches disappears with its heading.\n */\nfunction filterNodes<T>(nodes: Iterable<Node<T>>, matches: (node: Node<T>) => boolean): Node<T>[] {\n const kept: Node<T>[] = [];\n for (const node of nodes) {\n if (node.type === 'section') {\n const children = filterNodes(node.childNodes, matches);\n if (children.some((child) => child.type === 'item')) {\n kept.push({ ...node, childNodes: children });\n }\n } else if (node.type !== 'item' || matches(node)) {\n kept.push(node);\n }\n }\n return kept;\n}\n\nexport interface SearchListProps<T> {\n /** The options, their selection and disabled keys, as for `useListState`. */\n listProps: ListProps<T>;\n /** The typed text. */\n search: string;\n onSearchChange: (search: string) => void;\n /** Whether an option matches the search; `null` when the app filters the options itself. */\n filter: ((textValue: string, search: string, key: Node<T>['key']) => boolean) | null;\n /** Focus the search when the list opens (in a menu; a sheet shows the list first). */\n autoFocusSearch: boolean;\n /** Names the list (the field's label): `aria-labelledby`, or `aria-label` without one. */\n labelling: { 'aria-label'?: string | undefined; 'aria-labelledby'?: string | undefined };\n /** In a menu, the panel is the dialog the field opens (`role`, `id`, its name). */\n dialogProps?: HTMLAttributes<HTMLDivElement> | undefined;\n labels: { search: string; noResults: string; loading: string };\n loading: boolean;\n isBlocked: ((key: Node<T>['key']) => boolean) | undefined;\n /** In a sheet: no menu panel of its own. */\n embedded: boolean;\n classNames?: (OptionListClassNames & { search?: string }) | undefined;\n}\n\n/*\n * A dense search field (44, 40 or 36px tall at -1 to -3) extends its hit area by up to 4px\n * below it (the gap above the list) and up to 4px above it (the panel's padding, which clips\n * anything further): 48px at -1 and -2, 44px at -3. Like dense menu rows, it can't reach 48px\n * at -3 without covering the first option; it stays well above WCAG 2.5.8's 24px.\n */\nconst denseSearchTarget = [\n 'relative',\n \"before:absolute before:inset-x-0 before:bottom-full before:h-[clamp(0px,calc(var(--md-sys-density,0)*-4px-clamp(0px,calc(var(--md-sys-density,0)*-2px),4px)),4px)] before:content-['']\",\n \"after:absolute after:inset-x-0 after:top-full after:h-[clamp(0px,calc(var(--md-sys-density,0)*-2px),4px)] after:content-['']\",\n];\n\n/**\n * A search field over an option list, wired as one editable combobox with list autocomplete\n * (React Aria `useAutocomplete`): typing filters the options, the arrow keys move through\n * them while focus stays in the search (`aria-activedescendant`), Enter chooses, and Escape\n * clears the search, then reaches the menu or sheet to close it. The search is the M3 search\n * bar's field at 48px, M3's minimum touch target, so it doesn't dominate the 44px rows.\n * Used by `Select`'s `searchable` and `PhoneField`'s country field.\n */\nexport function SearchList<T>({\n listProps,\n search,\n onSearchChange,\n filter,\n autoFocusSearch,\n labelling,\n dialogProps,\n labels,\n loading,\n isBlocked,\n embedded,\n classNames,\n}: SearchListProps<T>) {\n const matches = useCallback(\n (nodes: Iterable<Node<T>>) =>\n filter ? filterNodes(nodes, (node) => filter(node.textValue, search, node.key)) : [...nodes],\n [filter, search],\n );\n const state = useListState<T>({ ...listProps, ...FOCUSABLE_DISABLED_OPTIONS, filter: matches });\n\n const [focusedNodeId, setFocusedNodeId] = useState<string | null>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const listBoxRef = useRef<HTMLUListElement>(null);\n const {\n inputProps: autocompleteInputProps,\n collectionProps,\n collectionRef,\n } = useAutocomplete(\n { inputRef, collectionRef: listBoxRef },\n { inputValue: search, setInputValue: onSearchChange, focusedNodeId, setFocusedNodeId },\n );\n const { inputProps } = useSearchField(\n { ...autocompleteInputProps, 'aria-label': labels.search, placeholder: labels.search },\n { value: search, setValue: onSearchChange },\n inputRef,\n );\n\n useEffect(() => {\n if (autoFocusSearch) inputRef.current?.focus();\n }, [autoFocusSearch]);\n\n const field = searchBarStyles({ hasLeading: true });\n const menu = menuStyles({ variant: 'standard', groupPosition: 'only' });\n\n return (\n <div\n {...dialogProps}\n // The menu's component tokens (menu-tokens.ts) for its panel and options.\n data-tokens=\"md-comp-menus\"\n data-variant=\"standard\"\n className={cn(\n 'flex min-h-0 flex-col',\n embedded ? 'flex-1' : [menu.group(), 'max-h-[inherit] overflow-hidden'],\n )}\n >\n {/* 48px (4px less per density step), 4px above the list, like the panel's 4px padding\n around it. In a sheet the whole content scrolls (its swipe-to-dismiss reads that\n scroller's position), so the search sticks to the top, over the options passing under. */}\n <div\n className={cn(\n 'flex h-[calc(52px+var(--md-sys-density,0)*4px)] shrink-0 pb-[4px]',\n embedded && 'sticky top-0 z-[1] bg-[var(--_md-comp-sheet-bottom-docked-container-color)]',\n )}\n >\n {/* The search bar's pill, as `SearchField` draws it, at 48px. */}\n <SearchFieldFrame\n // A press on the hit area around a dense field (below) puts the caret in the input.\n onPointerDown={(event) => {\n if (event.target !== event.currentTarget) return;\n event.preventDefault();\n inputRef.current?.focus();\n }}\n className={cn(denseSearchTarget, classNames?.search)}\n leading={\n <span data-slot=\"leading\" className={field.leading({ class: 'h-full' })}>\n <SearchIcon />\n </span>\n }\n >\n <input {...inputProps} ref={inputRef} data-slot=\"input\" className={field.input()} />\n </SearchFieldFrame>\n </div>\n <OptionList\n embedded\n animateFiltering\n listBoxProps={{\n ...collectionProps,\n ...labelling,\n // Escape belongs to the search and then the menu; the list must not take it to\n // clear the selection (which also kept a chosen option's menu from closing).\n escapeKeyBehavior: 'none',\n }}\n state={state}\n listBoxRef={collectionRef as RefObject<HTMLUListElement | null>}\n emptyLabel={labels.noResults}\n loading={loading}\n loadingLabel={labels.loading}\n isBlocked={isBlocked}\n classNames={classNames}\n />\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAoBA,MAAM,mBACJ,oBAAC,OAAD;CAAK,SAAQ;CAAiB,MAAK;CAAe,eAAY;CAC5D,UAAA,oBAAC,QAAD,EAAM,GAAE,8PAA+P,CAAA;AACpQ,CAAA;;;;;AAOP,SAAS,YAAe,OAA0B,SAAgD;CAChG,MAAM,OAAkB,CAAC;CACzB,KAAK,MAAM,QAAQ,OACjB,IAAI,KAAK,SAAS,WAAW;EAC3B,MAAM,WAAW,YAAY,KAAK,YAAY,OAAO;EACrD,IAAI,SAAS,MAAM,UAAU,MAAM,SAAS,MAAM,GAChD,KAAK,KAAK;GAAE,GAAG;GAAM,YAAY;EAAS,CAAC;CAE/C,OAAO,IAAI,KAAK,SAAS,UAAU,QAAQ,IAAI,GAC7C,KAAK,KAAK,IAAI;CAGlB,OAAO;AACT;AA8BA,MAAM,oBAAoB;CACxB;CACA;CACA;AACF;;;;;;;;;AAUA,SAAgB,WAAc,EAC5B,WACA,QACA,gBACA,QACA,iBACA,WACA,aACA,QACA,SACA,WACA,UACA,cACqB;CACrB,MAAM,UAAU,aACb,UACC,SAAS,YAAY,QAAQ,SAAS,OAAO,KAAK,WAAW,QAAQ,KAAK,GAAG,CAAC,IAAI,CAAC,GAAG,KAAK,GAC7F,CAAC,QAAQ,MAAM,CACjB;CACA,MAAM,QAAQ,aAAgB;EAAE,GAAG;EAAW,GAAG;EAA4B,QAAQ;CAAQ,CAAC;CAE9F,MAAM,CAAC,eAAe,oBAAoB,SAAwB,IAAI;CACtE,MAAM,WAAW,OAAyB,IAAI;CAC9C,MAAM,aAAa,OAAyB,IAAI;CAChD,MAAM,EACJ,YAAY,wBACZ,iBACA,kBACE,gBACF;EAAE;EAAU,eAAe;CAAW,GACtC;EAAE,YAAY;EAAQ,eAAe;EAAgB;EAAe;CAAiB,CACvF;CACA,MAAM,EAAE,eAAe,eACrB;EAAE,GAAG;EAAwB,cAAc,OAAO;EAAQ,aAAa,OAAO;CAAO,GACrF;EAAE,OAAO;EAAQ,UAAU;CAAe,GAC1C,QACF;CAEA,gBAAgB;EACd,IAAI,iBAAiB,SAAS,SAAS,MAAM;CAC/C,GAAG,CAAC,eAAe,CAAC;CAEpB,MAAM,QAAQ,gBAAgB,EAAE,YAAY,KAAK,CAAC;CAClD,MAAM,OAAO,WAAW;EAAE,SAAS;EAAY,eAAe;CAAO,CAAC;CAEtE,OACE,qBAAC,OAAD;EACE,GAAI;EAEJ,eAAY;EACZ,gBAAa;EACb,WAAW,GACT,yBACA,WAAW,WAAW,CAAC,KAAK,MAAM,GAAG,iCAAiC,CACxE;EARF,UAAA,CAaE,oBAAC,OAAD;GACE,WAAW,GACT,qEACA,YAAY,6EACd;GAGA,UAAA,oBAAC,kBAAD;IAEE,gBAAgB,UAAU;KACxB,IAAI,MAAM,WAAW,MAAM,eAAe;KAC1C,MAAM,eAAe;KACrB,SAAS,SAAS,MAAM;IAC1B;IACA,WAAW,GAAG,mBAAmB,YAAY,MAAM;IACnD,SACE,oBAAC,QAAD;KAAM,aAAU;KAAU,WAAW,MAAM,QAAQ,EAAE,OAAO,SAAS,CAAC;KACpE,UAAA,oBAAC,YAAD,CAAa,CAAA;IACT,CAAA;IAGR,UAAA,oBAAC,SAAD;KAAO,GAAI;KAAY,KAAK;KAAU,aAAU;KAAQ,WAAW,MAAM,MAAM;IAAI,CAAA;GACnE,CAAA;EACf,CAAA,GACL,oBAAC,YAAD;GACE,UAAA;GACA,kBAAA;GACA,cAAc;IACZ,GAAG;IACH,GAAG;IAGH,mBAAmB;GACrB;GACO;GACP,YAAY;GACZ,YAAY,OAAO;GACV;GACT,cAAc,OAAO;GACV;GACC;EACb,CAAA,CACE;;AAET"}
@@ -156,6 +156,8 @@ function SliderRoot(props) {
156
156
  state,
157
157
  trackRef,
158
158
  name,
159
+ groupId: groupProps.id,
160
+ labelledBy,
159
161
  label: range ? index === 0 ? labels.start : labels.end : void 0,
160
162
  errorMessage,
161
163
  valueLabel,
@@ -223,7 +225,7 @@ const INCREASE_KEYS = {
223
225
  ArrowDown: false
224
226
  }
225
227
  };
226
- function Thumb({ index, at, state, trackRef, name, label, errorMessage, valueLabel, styles, classNames }) {
228
+ function Thumb({ index, at, state, trackRef, name, groupId, labelledBy, label, errorMessage, valueLabel, styles, classNames }) {
227
229
  const inputRef = useRef(null);
228
230
  const { thumbProps, inputProps, isDragging } = useSliderThumb({
229
231
  index,
@@ -233,8 +235,10 @@ function Thumb({ index, at, state, trackRef, name, label, errorMessage, valueLab
233
235
  "aria-label": label,
234
236
  "aria-errormessage": errorMessage
235
237
  }, state);
238
+ const thumbLabelledBy = labelledBy?.trim() ? inputProps["aria-labelledby"]?.split(" ").flatMap((id) => id === groupId ? labelledBy.trim().split(/\s+/) : [id]).join(" ") : inputProps["aria-labelledby"];
236
239
  const thumbInputProps = {
237
240
  ...inputProps,
241
+ "aria-labelledby": thumbLabelledBy,
238
242
  "aria-describedby": inputProps["aria-describedby"] || void 0,
239
243
  "aria-details": inputProps["aria-details"] || void 0
240
244
  };
@@ -1 +1 @@
1
- {"version":3,"file":"Slider.js","names":[],"sources":["../../../src/components/slider/Slider.tsx"],"sourcesContent":["'use client';\n\nimport {\n useRef,\n useState,\n type CSSProperties,\n type KeyboardEvent,\n type ReactNode,\n type Ref,\n type RefObject,\n} from 'react';\nimport {\n mergeProps,\n useFocusRing,\n useLocale,\n useNumberFormatter,\n useObjectRef,\n useSlider,\n useSliderThumb,\n VisuallyHidden,\n} from 'react-aria';\nimport { useSliderState, type SliderState } from 'react-stately';\nimport { DomDirectionLocale } from '../../primitives/DomDirectionLocale';\nimport { cn } from '../../utils/cn';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\nimport { splitRootProps, type RootDOMProps } from '../../utils/root-props';\nimport { useTouchTrack } from './use-touch-track';\nimport {\n iconSizeWhenFits,\n movedStartIcon,\n segmentLength,\n trackGeometry,\n type TrackSegment,\n} from './slider-geometry';\nimport { sliderStyles, type SliderOrientation, type SliderSize } from './slider-styles';\n\nexport interface SliderClassNames {\n /** The outer element. */\n root?: string;\n /** The track, holding the active and inactive segments and the stop indicators. */\n track?: string;\n /** Each active track segment (the filled part, from the start or the centre). */\n activeTrack?: string;\n /** Each inactive track segment. */\n inactiveTrack?: string;\n /** The stop indicators: at the track's ends, and at every step with `ticks`. */\n stop?: string;\n /** The inset icons (`leadingIcon`, `trailingIcon`). */\n icon?: string;\n /** Each thumb, the pressable area around its handle. */\n thumb?: string;\n /** The handle bar drawn inside each thumb. */\n handle?: string;\n /** The value indicator above a thumb (`valueLabel`). */\n label?: string;\n}\n\n/** A slider needs a name: `aria-label`, or `aria-labelledby` pointing at a visible label. */\ntype Naming =\n | {\n /** Accessible name when there's no visible label. */\n 'aria-label': string;\n }\n | {\n /** The id of the visible label that names the slider. */\n 'aria-labelledby': string;\n };\n\ninterface SliderCommonProps extends RootDOMProps {\n /** The track's thickness, from XS (a 16px track) to XL. @default \"xs\" */\n size?: SliderSize;\n /**\n * The direction the slider runs. Vertical sliders run bottom to top and default to 240px\n * tall. @default \"horizontal\"\n */\n orientation?: SliderOrientation;\n /** The lowest value. @default 0 */\n min?: number;\n /** The highest value. @default 100 */\n max?: number;\n /** The amount the value changes by in one step. @default 1 */\n step?: number;\n /** Discrete: show a stop indicator at every step (use a coarse `step`). @default false */\n ticks?: boolean;\n /** Turns the slider off: it can't be moved or focused. @default false */\n disabled?: boolean;\n /** Show the value above the thumb while it is dragged or keyboard-focused. @default false */\n valueLabel?: boolean;\n /** Formats the value for the value indicator and assistive tech. */\n formatOptions?: Intl.NumberFormatOptions;\n /** Form field name (one hidden input per thumb). */\n name?: string;\n /** The id of the slider's root, its `group` (an error summary can link to it). */\n id?: string;\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?: SliderClassNames;\n /** `data-*` attributes, like the other DOM attributes, go to the root. */\n [data: `data-${string}`]: string | number | boolean | undefined;\n}\n\nexport type SliderProps = SliderCommonProps &\n Naming & {\n /** The value (controlled). */\n value?: number;\n /** The initial value when uncontrolled. @default min */\n defaultValue?: number;\n /** Called as the value changes, while dragging too. */\n onChange?: (value: number) => void;\n /** Called when the user stops dragging or releases a key. */\n onChangeEnd?: (value: number) => void;\n /**\n * Fill the track from its centre (for values around a midpoint). M3 doesn't use inset\n * icons on centred sliders, so `leadingIcon` and `trailingIcon` are ignored. @default false\n */\n centered?: boolean;\n /**\n * An icon inside the active track at its start (sizes md and up; ignored, with a dev\n * warning, on xs and sm). When the active track is too short for it, it moves to the\n * start of the inactive track.\n */\n leadingIcon?: ReactNode;\n /** An icon inside the inactive track at its end (sizes md and up; ignored on xs and sm). */\n trailingIcon?: ReactNode;\n };\n\nexport interface RangeSliderLabels {\n /** Name of the start thumb. @default \"Minimum\" */\n start: string;\n /** Name of the end thumb. @default \"Maximum\" */\n end: string;\n}\n\nconst DEFAULT_LABELS: RangeSliderLabels = { start: 'Minimum', end: 'Maximum' };\n\nexport type RangeSliderProps = SliderCommonProps &\n Naming & {\n /** The range, as `[start, end]` (controlled). */\n value?: [number, number];\n /** The initial range when uncontrolled. @default [min, max] */\n defaultValue?: [number, number];\n /** Called as the range changes, while dragging too. */\n onChange?: (value: [number, number]) => void;\n /** Called when the user stops dragging or releases a key. */\n onChangeEnd?: (value: [number, number]) => void;\n /** Words the range slider announces (its thumbs' names), with English defaults. */\n labels?: Partial<RangeSliderLabels>;\n };\n\n/**\n * M3 Expressive slider: XS–XL sizes, horizontal or vertical, continuous or discrete with\n * stop indicators, optionally centred, with inset icons and a value indicator. Arrow keys,\n * Page Up / Down and Home / End adjust it (mirrored in RTL).\n *\n * @example\n * <Slider aria-label=\"Volume\" size=\"md\" leadingIcon={<VolumeIcon />} defaultValue={40} />\n */\nexport function Slider(props: SliderProps) {\n return (\n <DomDirectionLocale>\n {(directionRef) => <SliderRoot {...props} range={false} directionRef={directionRef} />}\n </DomDirectionLocale>\n );\n}\n\n/**\n * M3 Expressive range slider: two thumbs selecting a range, with the same sizes,\n * orientations and discrete stops as {@link Slider}.\n *\n * @example\n * <RangeSlider aria-label=\"Price\" defaultValue={[20, 80]} />\n */\nexport function RangeSlider(props: RangeSliderProps) {\n return (\n <DomDirectionLocale>\n {(directionRef) => <SliderRoot {...props} range directionRef={directionRef} />}\n </DomDirectionLocale>\n );\n}\n\ntype RootProps = (SliderProps | RangeSliderProps) & {\n range: boolean;\n directionRef: (element: HTMLElement | null) => void;\n};\n\nfunction SliderRoot(props: RootProps) {\n const {\n size = 'xs',\n orientation = 'horizontal',\n min = 0,\n max = 100,\n step = 1,\n ticks = false,\n disabled = false,\n valueLabel = false,\n formatOptions,\n name,\n id,\n ref,\n className,\n style,\n classNames,\n range,\n directionRef,\n value,\n defaultValue,\n onChange,\n onChangeEnd,\n ...rest\n } = props;\n const {\n centered = false,\n leadingIcon: startIconProp,\n trailingIcon: endIconProp,\n } = props as SliderProps;\n // M3: only M, L and XL standard sliders take inset icons (\"Avoid adding inset icons to XS\n // or S\"; none on centred sliders).\n const iconSize = size === 'md' || size === 'lg' || size === 'xl';\n const startIcon = iconSize && !centered ? startIconProp : undefined;\n const endIcon = iconSize && !centered ? endIconProp : undefined;\n const labels = { ...DEFAULT_LABELS, ...(props as RangeSliderProps).labels };\n // The root is the slider's group: its DOM attributes join the hook's.\n const { root: data, rest: labelling } = splitRootProps(rest);\n // The naming attributes: the name goes to the group and each thumb (React Aria), and the\n // description, details and error message to each thumb's input, which takes focus.\n const {\n 'aria-label': label,\n 'aria-labelledby': labelledBy,\n 'aria-describedby': describedBy,\n 'aria-details': details,\n 'aria-errormessage': errorMessage,\n } = labelling as {\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n 'aria-describedby'?: string;\n 'aria-details'?: string;\n 'aria-errormessage'?: string;\n };\n\n const numberFormatter = useNumberFormatter(formatOptions);\n // React Stately works with arrays; a single slider wraps and unwraps its number.\n const wrap = (v: number | [number, number] | undefined) =>\n v === undefined ? undefined : Array.isArray(v) ? v : [v];\n const unwrap = (v: number[]) => (range ? (v as [number, number]) : v[0]!);\n const ariaProps = {\n id,\n minValue: min,\n maxValue: max,\n step,\n orientation,\n isDisabled: disabled,\n 'aria-label': label,\n 'aria-labelledby': labelledBy,\n 'aria-describedby': describedBy,\n 'aria-details': details,\n value: wrap(value),\n defaultValue: wrap(defaultValue) ?? (range ? [min, max] : [min]),\n onChange: onChange && ((v: number[]) => (onChange as (x: unknown) => void)(unwrap(v))),\n onChangeEnd: onChangeEnd && ((v: number[]) => (onChangeEnd as (x: unknown) => void)(unwrap(v))),\n };\n const state = useSliderState({ ...ariaProps, numberFormatter });\n const rootRef = useObjectRef(ref);\n const trackRef = useRef<HTMLDivElement>(null);\n const { groupProps, trackProps } = useSlider(ariaProps, state, trackRef);\n const { direction } = useLocale();\n const count = Math.round((max - min) / step);\n const touchTrackProps = useTouchTrack(trackProps, {\n state,\n trackRef,\n orientation,\n direction,\n disabled,\n discrete: ticks && count > 0,\n });\n\n const tickFractions =\n ticks && count > 0 ? Array.from({ length: count + 1 }, (_, i) => i / count) : [];\n const fractions = state.values.map((_, index) => state.getThumbPercent(index));\n const geometry = trackGeometry({ fractions, ticks: tickFractions, centered: centered && !range });\n const styles = sliderStyles({ size, orientation });\n if (process.env.NODE_ENV !== 'production' && (startIconProp != null || endIconProp != null)) {\n if (centered) {\n console.warn(\n 'Slider: inset icons are not used on centred sliders (M3), so they are ignored.',\n );\n } else if (!iconSize) {\n console.warn('Slider: inset icons need size md, lg or xl (M3), so they are ignored.');\n }\n }\n if (process.env.NODE_ENV !== 'production' && range && orientation === 'vertical') {\n console.warn('RangeSlider: M3 advises against vertical range sliders.');\n }\n const activeSegment = geometry.segments.find((segment) => segment.icon === 'start');\n const moved =\n activeSegment && startIcon != null && movedStartIcon(activeSegment, geometry.thumbs[0]!);\n\n return (\n <div\n {...mergeProps(data, groupProps)}\n ref={(element) => {\n rootRef.current = element;\n directionRef(element);\n }}\n style={style}\n data-tokens={mergeDataTokens('md-comp-slider', data)}\n data-size={size}\n data-orientation={orientation}\n data-disabled={disabled || undefined}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n <div\n {...touchTrackProps}\n ref={trackRef}\n className={styles.track({ class: classNames?.track })}\n >\n {geometry.segments.map((segment, index) => (\n <Segment\n key={index}\n segment={segment}\n styles={styles}\n classNames={classNames}\n icon={\n segment.icon === 'start' ? startIcon : segment.icon === 'end' ? endIcon : undefined\n }\n />\n ))}\n {moved && (\n <span\n aria-hidden=\"true\"\n data-moved-icon=\"\"\n style={{ marginInlineStart: moved.start, inlineSize: moved.size }}\n className={styles.movedIcon({ class: classNames?.icon })}\n >\n {startIcon}\n </span>\n )}\n {geometry.ticks.map((tick, index) => (\n <span\n key={index}\n aria-hidden=\"true\"\n data-slot=\"stop-indicator\"\n data-active={tick.active || undefined}\n style={{ marginInlineStart: `calc(${tick.at} - 2px)` }}\n className={styles.tick({ class: classNames?.stop })}\n />\n ))}\n {geometry.thumbs.map((at, index) => (\n <Thumb\n key={index}\n index={index as 0 | 1}\n at={at}\n state={state}\n trackRef={trackRef}\n name={name}\n label={range ? (index === 0 ? labels.start : labels.end) : undefined}\n errorMessage={errorMessage}\n valueLabel={valueLabel}\n styles={styles}\n classNames={classNames}\n />\n ))}\n </div>\n </div>\n );\n}\n\nfunction Segment({\n segment,\n styles,\n classNames,\n icon,\n}: {\n segment: TrackSegment;\n styles: ReturnType<typeof sliderStyles>;\n classNames: SliderClassNames | undefined;\n icon: ReactNode;\n}) {\n const radius = {\n borderStartStartRadius: segment.startRadius,\n borderEndStartRadius: segment.startRadius,\n borderStartEndRadius: segment.endRadius,\n borderEndEndRadius: segment.endRadius,\n };\n // Inset icons sit 10px inside and show only when the segment fits them (MDC).\n const iconBox = (\n <span\n aria-hidden=\"true\"\n style={{\n inlineSize: iconSizeWhenFits('100%'),\n marginInlineStart: segment.icon === 'start' ? '10px' : 'auto',\n marginInlineEnd: segment.icon === 'end' ? '10px' : undefined,\n }}\n className={styles.icon({ class: classNames?.icon })}\n >\n {icon}\n </span>\n );\n // Stop indicators sit a corner's length in from the track's ends.\n const stop = (at: 'start' | 'end') => (\n <span\n aria-hidden=\"true\"\n data-slot=\"stop-indicator\"\n style={\n at === 'start'\n ? { marginInlineStart: 'calc(var(--m3-slider-corner) - 2px)' }\n : { marginInlineStart: 'auto', marginInlineEnd: 'calc(var(--m3-slider-corner) - 2px)' }\n }\n className={styles.stop({ class: classNames?.stop })}\n />\n );\n return (\n <span\n data-tone={segment.tone}\n data-slot={`${segment.tone}-track`}\n style={{ marginInlineStart: segment.start, inlineSize: segmentLength(segment), ...radius }}\n className={styles.segment({\n class: segment.tone === 'active' ? classNames?.activeTrack : classNames?.inactiveTrack,\n })}\n >\n {segment.stopAtStart && stop('start')}\n {icon != null && iconBox}\n {/* An end icon takes the stop indicator's place. */}\n {segment.stopAtEnd && icon == null && stop('end')}\n </span>\n );\n}\n\n/** Whether each arrow key increases the value (true) or decreases it (false), by direction. */\nconst INCREASE_KEYS: Record<'ltr' | 'rtl', Record<string, boolean>> = {\n ltr: { ArrowRight: true, ArrowUp: true, ArrowLeft: false, ArrowDown: false },\n rtl: { ArrowLeft: true, ArrowUp: true, ArrowRight: false, ArrowDown: false },\n};\n\nfunction Thumb({\n index,\n at,\n state,\n trackRef,\n name,\n label,\n errorMessage,\n valueLabel,\n styles,\n classNames,\n}: {\n index: 0 | 1;\n at: string;\n state: SliderState;\n trackRef: RefObject<HTMLDivElement | null>;\n name?: string;\n label?: string;\n errorMessage?: string;\n valueLabel: boolean;\n styles: ReturnType<typeof sliderStyles>;\n classNames?: SliderClassNames;\n}) {\n const inputRef = useRef<HTMLInputElement>(null);\n const { thumbProps, inputProps, isDragging } = useSliderThumb(\n { index, trackRef, inputRef, name, 'aria-label': label, 'aria-errormessage': errorMessage },\n state,\n );\n // React Aria joins the description's ids into an empty string when there are none.\n const thumbInputProps = {\n ...inputProps,\n 'aria-describedby': inputProps['aria-describedby'] || undefined,\n 'aria-details': inputProps['aria-details'] || undefined,\n };\n const { focusProps, isFocusVisible } = useFocusRing();\n const { direction } = useLocale();\n // M3: \"Space & Arrows — Increase and decrease the value by one interval\", so arrows step\n // by React Aria's page size while Space is held (as Shift + arrows already do).\n const [spaceHeld, setSpaceHeld] = useState(false);\n const intervalKeys = {\n onKeyDown: (event: KeyboardEvent<HTMLInputElement>) => {\n if (event.key === ' ') {\n event.preventDefault();\n setSpaceHeld(true);\n return;\n }\n const increase = INCREASE_KEYS[direction][event.key];\n if (!spaceHeld || increase === undefined) return;\n event.preventDefault();\n event.stopPropagation();\n state.setThumbDragging(index, true);\n if (increase) state.incrementThumb(index, state.pageSize);\n else state.decrementThumb(index, state.pageSize);\n state.setThumbDragging(index, false);\n },\n onKeyUp: (event: KeyboardEvent<HTMLInputElement>) => {\n if (event.key === ' ') setSpaceHeld(false);\n },\n onBlur: () => setSpaceHeld(false),\n };\n // The handle narrows to 2px while it is pressed or dragged (M3), or keyboard-focused.\n const active = isDragging || isFocusVisible;\n return (\n <>\n <div\n {...thumbProps}\n data-thumb={index}\n data-focus-visible={isFocusVisible || undefined}\n data-dragging={isDragging || undefined}\n style={{ marginInlineStart: `calc(${at} - 2px)` }}\n className={styles.thumb({ class: classNames?.thumb })}\n >\n <span\n data-slot=\"handle\"\n data-active={active || undefined}\n className={styles.handle({ class: classNames?.handle })}\n />\n <VisuallyHidden>\n <input ref={inputRef} {...mergeProps(thumbInputProps, focusProps, intervalKeys)} />\n </VisuallyHidden>\n </div>\n {valueLabel && (isDragging || isFocusVisible) && (\n <span aria-hidden=\"true\" style={{ marginInlineStart: at }} className={styles.labelAnchor()}>\n <span data-slot=\"value-indicator\" className={styles.label({ class: classNames?.label })}>\n {state.getThumbValueLabel(index)}\n </span>\n </span>\n )}\n </>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"Slider.js","names":[],"sources":["../../../src/components/slider/Slider.tsx"],"sourcesContent":["'use client';\n\nimport {\n useRef,\n useState,\n type CSSProperties,\n type KeyboardEvent,\n type ReactNode,\n type Ref,\n type RefObject,\n} from 'react';\nimport {\n mergeProps,\n useFocusRing,\n useLocale,\n useNumberFormatter,\n useObjectRef,\n useSlider,\n useSliderThumb,\n VisuallyHidden,\n} from 'react-aria';\nimport { useSliderState, type SliderState } from 'react-stately';\nimport { DomDirectionLocale } from '../../primitives/DomDirectionLocale';\nimport { cn } from '../../utils/cn';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\nimport { splitRootProps, type RootDOMProps } from '../../utils/root-props';\nimport { useTouchTrack } from './use-touch-track';\nimport {\n iconSizeWhenFits,\n movedStartIcon,\n segmentLength,\n trackGeometry,\n type TrackSegment,\n} from './slider-geometry';\nimport { sliderStyles, type SliderOrientation, type SliderSize } from './slider-styles';\n\nexport interface SliderClassNames {\n /** The outer element. */\n root?: string;\n /** The track, holding the active and inactive segments and the stop indicators. */\n track?: string;\n /** Each active track segment (the filled part, from the start or the centre). */\n activeTrack?: string;\n /** Each inactive track segment. */\n inactiveTrack?: string;\n /** The stop indicators: at the track's ends, and at every step with `ticks`. */\n stop?: string;\n /** The inset icons (`leadingIcon`, `trailingIcon`). */\n icon?: string;\n /** Each thumb, the pressable area around its handle. */\n thumb?: string;\n /** The handle bar drawn inside each thumb. */\n handle?: string;\n /** The value indicator above a thumb (`valueLabel`). */\n label?: string;\n}\n\n/** A slider needs a name: `aria-label`, or `aria-labelledby` pointing at a visible label. */\ntype Naming =\n | {\n /** Accessible name when there's no visible label. */\n 'aria-label': string;\n }\n | {\n /** The id of the visible label that names the slider. */\n 'aria-labelledby': string;\n };\n\ninterface SliderCommonProps extends RootDOMProps {\n /** The track's thickness, from XS (a 16px track) to XL. @default \"xs\" */\n size?: SliderSize;\n /**\n * The direction the slider runs. Vertical sliders run bottom to top and default to 240px\n * tall. @default \"horizontal\"\n */\n orientation?: SliderOrientation;\n /** The lowest value. @default 0 */\n min?: number;\n /** The highest value. @default 100 */\n max?: number;\n /** The amount the value changes by in one step. @default 1 */\n step?: number;\n /** Discrete: show a stop indicator at every step (use a coarse `step`). @default false */\n ticks?: boolean;\n /** Turns the slider off: it can't be moved or focused. @default false */\n disabled?: boolean;\n /** Show the value above the thumb while it is dragged or keyboard-focused. @default false */\n valueLabel?: boolean;\n /** Formats the value for the value indicator and assistive tech. */\n formatOptions?: Intl.NumberFormatOptions;\n /** Form field name (one hidden input per thumb). */\n name?: string;\n /** The id of the slider's root, its `group` (an error summary can link to it). */\n id?: string;\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?: SliderClassNames;\n /** `data-*` attributes, like the other DOM attributes, go to the root. */\n [data: `data-${string}`]: string | number | boolean | undefined;\n}\n\nexport type SliderProps = SliderCommonProps &\n Naming & {\n /** The value (controlled). */\n value?: number;\n /** The initial value when uncontrolled. @default min */\n defaultValue?: number;\n /** Called as the value changes, while dragging too. */\n onChange?: (value: number) => void;\n /** Called when the user stops dragging or releases a key. */\n onChangeEnd?: (value: number) => void;\n /**\n * Fill the track from its centre (for values around a midpoint). M3 doesn't use inset\n * icons on centred sliders, so `leadingIcon` and `trailingIcon` are ignored. @default false\n */\n centered?: boolean;\n /**\n * An icon inside the active track at its start (sizes md and up; ignored, with a dev\n * warning, on xs and sm). When the active track is too short for it, it moves to the\n * start of the inactive track.\n */\n leadingIcon?: ReactNode;\n /** An icon inside the inactive track at its end (sizes md and up; ignored on xs and sm). */\n trailingIcon?: ReactNode;\n };\n\nexport interface RangeSliderLabels {\n /** Name of the start thumb. @default \"Minimum\" */\n start: string;\n /** Name of the end thumb. @default \"Maximum\" */\n end: string;\n}\n\nconst DEFAULT_LABELS: RangeSliderLabels = { start: 'Minimum', end: 'Maximum' };\n\nexport type RangeSliderProps = SliderCommonProps &\n Naming & {\n /** The range, as `[start, end]` (controlled). */\n value?: [number, number];\n /** The initial range when uncontrolled. @default [min, max] */\n defaultValue?: [number, number];\n /** Called as the range changes, while dragging too. */\n onChange?: (value: [number, number]) => void;\n /** Called when the user stops dragging or releases a key. */\n onChangeEnd?: (value: [number, number]) => void;\n /** Words the range slider announces (its thumbs' names), with English defaults. */\n labels?: Partial<RangeSliderLabels>;\n };\n\n/**\n * M3 Expressive slider: XS–XL sizes, horizontal or vertical, continuous or discrete with\n * stop indicators, optionally centred, with inset icons and a value indicator. Arrow keys,\n * Page Up / Down and Home / End adjust it (mirrored in RTL).\n *\n * @example\n * <Slider aria-label=\"Volume\" size=\"md\" leadingIcon={<VolumeIcon />} defaultValue={40} />\n */\nexport function Slider(props: SliderProps) {\n return (\n <DomDirectionLocale>\n {(directionRef) => <SliderRoot {...props} range={false} directionRef={directionRef} />}\n </DomDirectionLocale>\n );\n}\n\n/**\n * M3 Expressive range slider: two thumbs selecting a range, with the same sizes,\n * orientations and discrete stops as {@link Slider}.\n *\n * @example\n * <RangeSlider aria-label=\"Price\" defaultValue={[20, 80]} />\n */\nexport function RangeSlider(props: RangeSliderProps) {\n return (\n <DomDirectionLocale>\n {(directionRef) => <SliderRoot {...props} range directionRef={directionRef} />}\n </DomDirectionLocale>\n );\n}\n\ntype RootProps = (SliderProps | RangeSliderProps) & {\n range: boolean;\n directionRef: (element: HTMLElement | null) => void;\n};\n\nfunction SliderRoot(props: RootProps) {\n const {\n size = 'xs',\n orientation = 'horizontal',\n min = 0,\n max = 100,\n step = 1,\n ticks = false,\n disabled = false,\n valueLabel = false,\n formatOptions,\n name,\n id,\n ref,\n className,\n style,\n classNames,\n range,\n directionRef,\n value,\n defaultValue,\n onChange,\n onChangeEnd,\n ...rest\n } = props;\n const {\n centered = false,\n leadingIcon: startIconProp,\n trailingIcon: endIconProp,\n } = props as SliderProps;\n // M3: only M, L and XL standard sliders take inset icons (\"Avoid adding inset icons to XS\n // or S\"; none on centred sliders).\n const iconSize = size === 'md' || size === 'lg' || size === 'xl';\n const startIcon = iconSize && !centered ? startIconProp : undefined;\n const endIcon = iconSize && !centered ? endIconProp : undefined;\n const labels = { ...DEFAULT_LABELS, ...(props as RangeSliderProps).labels };\n // The root is the slider's group: its DOM attributes join the hook's.\n const { root: data, rest: labelling } = splitRootProps(rest);\n // The naming attributes: the name goes to the group and each thumb (React Aria), and the\n // description, details and error message to each thumb's input, which takes focus.\n const {\n 'aria-label': label,\n 'aria-labelledby': labelledBy,\n 'aria-describedby': describedBy,\n 'aria-details': details,\n 'aria-errormessage': errorMessage,\n } = labelling as {\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n 'aria-describedby'?: string;\n 'aria-details'?: string;\n 'aria-errormessage'?: string;\n };\n\n const numberFormatter = useNumberFormatter(formatOptions);\n // React Stately works with arrays; a single slider wraps and unwraps its number.\n const wrap = (v: number | [number, number] | undefined) =>\n v === undefined ? undefined : Array.isArray(v) ? v : [v];\n const unwrap = (v: number[]) => (range ? (v as [number, number]) : v[0]!);\n const ariaProps = {\n id,\n minValue: min,\n maxValue: max,\n step,\n orientation,\n isDisabled: disabled,\n 'aria-label': label,\n 'aria-labelledby': labelledBy,\n 'aria-describedby': describedBy,\n 'aria-details': details,\n value: wrap(value),\n defaultValue: wrap(defaultValue) ?? (range ? [min, max] : [min]),\n onChange: onChange && ((v: number[]) => (onChange as (x: unknown) => void)(unwrap(v))),\n onChangeEnd: onChangeEnd && ((v: number[]) => (onChangeEnd as (x: unknown) => void)(unwrap(v))),\n };\n const state = useSliderState({ ...ariaProps, numberFormatter });\n const rootRef = useObjectRef(ref);\n const trackRef = useRef<HTMLDivElement>(null);\n const { groupProps, trackProps } = useSlider(ariaProps, state, trackRef);\n const { direction } = useLocale();\n const count = Math.round((max - min) / step);\n const touchTrackProps = useTouchTrack(trackProps, {\n state,\n trackRef,\n orientation,\n direction,\n disabled,\n discrete: ticks && count > 0,\n });\n\n const tickFractions =\n ticks && count > 0 ? Array.from({ length: count + 1 }, (_, i) => i / count) : [];\n const fractions = state.values.map((_, index) => state.getThumbPercent(index));\n const geometry = trackGeometry({ fractions, ticks: tickFractions, centered: centered && !range });\n const styles = sliderStyles({ size, orientation });\n if (process.env.NODE_ENV !== 'production' && (startIconProp != null || endIconProp != null)) {\n if (centered) {\n console.warn(\n 'Slider: inset icons are not used on centred sliders (M3), so they are ignored.',\n );\n } else if (!iconSize) {\n console.warn('Slider: inset icons need size md, lg or xl (M3), so they are ignored.');\n }\n }\n if (process.env.NODE_ENV !== 'production' && range && orientation === 'vertical') {\n console.warn('RangeSlider: M3 advises against vertical range sliders.');\n }\n const activeSegment = geometry.segments.find((segment) => segment.icon === 'start');\n const moved =\n activeSegment && startIcon != null && movedStartIcon(activeSegment, geometry.thumbs[0]!);\n\n return (\n <div\n {...mergeProps(data, groupProps)}\n ref={(element) => {\n rootRef.current = element;\n directionRef(element);\n }}\n style={style}\n data-tokens={mergeDataTokens('md-comp-slider', data)}\n data-size={size}\n data-orientation={orientation}\n data-disabled={disabled || undefined}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n <div\n {...touchTrackProps}\n ref={trackRef}\n className={styles.track({ class: classNames?.track })}\n >\n {geometry.segments.map((segment, index) => (\n <Segment\n key={index}\n segment={segment}\n styles={styles}\n classNames={classNames}\n icon={\n segment.icon === 'start' ? startIcon : segment.icon === 'end' ? endIcon : undefined\n }\n />\n ))}\n {moved && (\n <span\n aria-hidden=\"true\"\n data-moved-icon=\"\"\n style={{ marginInlineStart: moved.start, inlineSize: moved.size }}\n className={styles.movedIcon({ class: classNames?.icon })}\n >\n {startIcon}\n </span>\n )}\n {geometry.ticks.map((tick, index) => (\n <span\n key={index}\n aria-hidden=\"true\"\n data-slot=\"stop-indicator\"\n data-active={tick.active || undefined}\n style={{ marginInlineStart: `calc(${tick.at} - 2px)` }}\n className={styles.tick({ class: classNames?.stop })}\n />\n ))}\n {geometry.thumbs.map((at, index) => (\n <Thumb\n key={index}\n index={index as 0 | 1}\n at={at}\n state={state}\n trackRef={trackRef}\n name={name}\n groupId={groupProps.id}\n labelledBy={labelledBy}\n label={range ? (index === 0 ? labels.start : labels.end) : undefined}\n errorMessage={errorMessage}\n valueLabel={valueLabel}\n styles={styles}\n classNames={classNames}\n />\n ))}\n </div>\n </div>\n );\n}\n\nfunction Segment({\n segment,\n styles,\n classNames,\n icon,\n}: {\n segment: TrackSegment;\n styles: ReturnType<typeof sliderStyles>;\n classNames: SliderClassNames | undefined;\n icon: ReactNode;\n}) {\n const radius = {\n borderStartStartRadius: segment.startRadius,\n borderEndStartRadius: segment.startRadius,\n borderStartEndRadius: segment.endRadius,\n borderEndEndRadius: segment.endRadius,\n };\n // Inset icons sit 10px inside and show only when the segment fits them (MDC).\n const iconBox = (\n <span\n aria-hidden=\"true\"\n style={{\n inlineSize: iconSizeWhenFits('100%'),\n marginInlineStart: segment.icon === 'start' ? '10px' : 'auto',\n marginInlineEnd: segment.icon === 'end' ? '10px' : undefined,\n }}\n className={styles.icon({ class: classNames?.icon })}\n >\n {icon}\n </span>\n );\n // Stop indicators sit a corner's length in from the track's ends.\n const stop = (at: 'start' | 'end') => (\n <span\n aria-hidden=\"true\"\n data-slot=\"stop-indicator\"\n style={\n at === 'start'\n ? { marginInlineStart: 'calc(var(--m3-slider-corner) - 2px)' }\n : { marginInlineStart: 'auto', marginInlineEnd: 'calc(var(--m3-slider-corner) - 2px)' }\n }\n className={styles.stop({ class: classNames?.stop })}\n />\n );\n return (\n <span\n data-tone={segment.tone}\n data-slot={`${segment.tone}-track`}\n style={{ marginInlineStart: segment.start, inlineSize: segmentLength(segment), ...radius }}\n className={styles.segment({\n class: segment.tone === 'active' ? classNames?.activeTrack : classNames?.inactiveTrack,\n })}\n >\n {segment.stopAtStart && stop('start')}\n {icon != null && iconBox}\n {/* An end icon takes the stop indicator's place. */}\n {segment.stopAtEnd && icon == null && stop('end')}\n </span>\n );\n}\n\n/** Whether each arrow key increases the value (true) or decreases it (false), by direction. */\nconst INCREASE_KEYS: Record<'ltr' | 'rtl', Record<string, boolean>> = {\n ltr: { ArrowRight: true, ArrowUp: true, ArrowLeft: false, ArrowDown: false },\n rtl: { ArrowLeft: true, ArrowUp: true, ArrowRight: false, ArrowDown: false },\n};\n\nfunction Thumb({\n index,\n at,\n state,\n trackRef,\n name,\n groupId,\n labelledBy,\n label,\n errorMessage,\n valueLabel,\n styles,\n classNames,\n}: {\n index: 0 | 1;\n at: string;\n state: SliderState;\n trackRef: RefObject<HTMLDivElement | null>;\n name?: string;\n /** The id of the slider's group, which React Aria names every thumb by. */\n groupId?: string;\n /** The ids that name the group (`aria-labelledby`), when it's named by a visible label. */\n labelledBy?: string;\n label?: string;\n errorMessage?: string;\n valueLabel: boolean;\n styles: ReturnType<typeof sliderStyles>;\n classNames?: SliderClassNames;\n}) {\n const inputRef = useRef<HTMLInputElement>(null);\n const { thumbProps, inputProps, isDragging } = useSliderThumb(\n { index, trackRef, inputRef, name, 'aria-label': label, 'aria-errormessage': errorMessage },\n state,\n );\n // React Aria names each thumb by the group (`aria-labelledby` → the group's id). A name is\n // never followed through a second `aria-labelledby`, so a group named by a visible label would\n // name the thumb by its own content, the value text (\"0%\"): the thumb points at the label\n // itself instead, as the group does. An `aria-label` on the group is read through it as is.\n const thumbLabelledBy = labelledBy?.trim()\n ? inputProps['aria-labelledby']\n ?.split(' ')\n .flatMap((id) => (id === groupId ? labelledBy.trim().split(/\\s+/) : [id]))\n .join(' ')\n : inputProps['aria-labelledby'];\n // React Aria joins the description's ids into an empty string when there are none.\n const thumbInputProps = {\n ...inputProps,\n 'aria-labelledby': thumbLabelledBy,\n 'aria-describedby': inputProps['aria-describedby'] || undefined,\n 'aria-details': inputProps['aria-details'] || undefined,\n };\n const { focusProps, isFocusVisible } = useFocusRing();\n const { direction } = useLocale();\n // M3: \"Space & Arrows — Increase and decrease the value by one interval\", so arrows step\n // by React Aria's page size while Space is held (as Shift + arrows already do).\n const [spaceHeld, setSpaceHeld] = useState(false);\n const intervalKeys = {\n onKeyDown: (event: KeyboardEvent<HTMLInputElement>) => {\n if (event.key === ' ') {\n event.preventDefault();\n setSpaceHeld(true);\n return;\n }\n const increase = INCREASE_KEYS[direction][event.key];\n if (!spaceHeld || increase === undefined) return;\n event.preventDefault();\n event.stopPropagation();\n state.setThumbDragging(index, true);\n if (increase) state.incrementThumb(index, state.pageSize);\n else state.decrementThumb(index, state.pageSize);\n state.setThumbDragging(index, false);\n },\n onKeyUp: (event: KeyboardEvent<HTMLInputElement>) => {\n if (event.key === ' ') setSpaceHeld(false);\n },\n onBlur: () => setSpaceHeld(false),\n };\n // The handle narrows to 2px while it is pressed or dragged (M3), or keyboard-focused.\n const active = isDragging || isFocusVisible;\n return (\n <>\n <div\n {...thumbProps}\n data-thumb={index}\n data-focus-visible={isFocusVisible || undefined}\n data-dragging={isDragging || undefined}\n style={{ marginInlineStart: `calc(${at} - 2px)` }}\n className={styles.thumb({ class: classNames?.thumb })}\n >\n <span\n data-slot=\"handle\"\n data-active={active || undefined}\n className={styles.handle({ class: classNames?.handle })}\n />\n <VisuallyHidden>\n <input ref={inputRef} {...mergeProps(thumbInputProps, focusProps, intervalKeys)} />\n </VisuallyHidden>\n </div>\n {valueLabel && (isDragging || isFocusVisible) && (\n <span aria-hidden=\"true\" style={{ marginInlineStart: at }} className={styles.labelAnchor()}>\n <span data-slot=\"value-indicator\" className={styles.label({ class: classNames?.label })}>\n {state.getThumbValueLabel(index)}\n </span>\n </span>\n )}\n </>\n );\n}\n"],"mappings":"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"}
@@ -2,6 +2,6 @@ import { ComponentTokenSet } from "../../tokens/component.js";
2
2
  import "../../index.js";
3
3
  //#region src/components/snackbar/snackbar-tokens.d.ts
4
4
  /** The tokens of {@link Snackbar}: `--md-comp-snackbar-<part>`. */
5
- export declare const snackbarTokenSet: ComponentTokenSet<"container-color" | "container-elevation" | "icon-color" | "container-shape" | "supporting-text-color" | "action-label-text-color">;
5
+ export declare const snackbarTokenSet: ComponentTokenSet<"icon-color" | "container-color" | "container-elevation" | "container-shape" | "supporting-text-color" | "action-label-text-color">;
6
6
  //#endregion
7
7
  //# sourceMappingURL=snackbar-tokens.d.ts.map
@@ -67,11 +67,25 @@ function TextField(props) {
67
67
  useLayoutEffect(() => {
68
68
  const element = domInputRef.current;
69
69
  if (!growing || !element) return;
70
- element.style.height = "auto";
71
- const lineHeight = Number.parseFloat(getComputedStyle(element).lineHeight) || 24;
72
- const max = maxRows ? lineHeight * maxRows : Number.POSITIVE_INFINITY;
73
- element.style.height = `${Math.min(element.scrollHeight, max)}px`;
74
- element.style.overflowY = element.scrollHeight > max ? "auto" : "hidden";
70
+ const fit = () => {
71
+ element.style.height = "auto";
72
+ const lineHeight = Number.parseFloat(getComputedStyle(element).lineHeight) || 24;
73
+ const max = maxRows ? lineHeight * maxRows : Number.POSITIVE_INFINITY;
74
+ element.style.height = `${Math.min(element.scrollHeight, max)}px`;
75
+ element.style.overflowY = element.scrollHeight > max ? "auto" : "hidden";
76
+ };
77
+ if (element.getClientRects().length > 0 || typeof ResizeObserver === "undefined") {
78
+ fit();
79
+ return;
80
+ }
81
+ element.style.height = "";
82
+ const observer = new ResizeObserver(() => {
83
+ if (element.getClientRects().length === 0) return;
84
+ observer.disconnect();
85
+ fit();
86
+ });
87
+ observer.observe(element);
88
+ return () => observer.disconnect();
75
89
  }, [
76
90
  value,
77
91
  growing,
@@ -1 +1 @@
1
- {"version":3,"file":"TextField.js","names":[],"sources":["../../../src/components/text-field/TextField.tsx"],"sourcesContent":["'use client';\n\nimport {\n useId,\n useLayoutEffect,\n type AriaAttributes,\n type CSSProperties,\n type MouseEvent,\n type PointerEvent,\n type ReactNode,\n type Ref,\n} from 'react';\nimport {\n mergeProps,\n useFocusRing,\n useHover,\n useObjectRef,\n useTextField,\n type AriaTextFieldOptions,\n type AriaTextFieldProps,\n} from 'react-aria';\nimport { useControlledState } from 'react-stately/useControlledState';\nimport {\n CharacterCounter,\n ErrorText,\n FieldErrorIcon,\n FieldLabel,\n SupportingText,\n} from '../../primitives/Field';\nimport { densityAttribute, type Density } from '../../tokens/density';\nimport { cn } from '../../utils/cn';\nimport { renderedDescription } from '../../utils/rendered-description';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\nimport { pickWidgetAria, splitRootProps, type WrapperRootDOMProps } from '../../utils/root-props';\nimport { textFieldStyles, type TextFieldVariant } from './text-field-styles';\n\nexport interface TextFieldClassNames {\n /** The root element, merged with `className`. */\n root?: string;\n /** The filled or outlined container around the label, input and icons. */\n container?: string;\n /** The floating label. */\n label?: string;\n /** The `<input>` or `<textarea>`. */\n input?: string;\n /** The leading icon's 48px box. */\n leadingIcon?: string;\n /** The trailing icon's 48px box. */\n trailingIcon?: string;\n /** The prefix text before the input. */\n prefix?: string;\n /** The suffix text after the input. */\n suffix?: string;\n /** The helper text below the field. */\n supportingText?: string;\n /** The error message below the field. */\n errorText?: string;\n /** The character counter below the field. */\n counter?: string;\n /** The built-in error icon. */\n errorIcon?: string;\n /** The outlined field's border (a `<fieldset>`, notched for the floated label). */\n outline?: string;\n /** The filled field's active indicator, the line along the container's bottom. */\n activeIndicator?: string;\n}\n\n/** Words the text field announces; override them for other languages. */\nexport interface TextFieldLabels {\n /** The error icon's name (M3: \"Error\"). @default \"Error\" */\n error: string;\n /**\n * What screen readers hear for the character counter (M3 calls it the \"character count\").\n * @default (count, max) => `Character count: ${count} of ${max}`\n */\n characterCount: (count: number, max: number) => string;\n}\n\nconst DEFAULT_LABELS: TextFieldLabels = {\n error: 'Error',\n characterCount: (count, max) => `Character count: ${count} of ${max}`,\n};\n\n/** Input types the browser draws its own format in when empty, which a resting label would cover. */\nconst FORMAT_SHOWING_TYPES = new Set(['date', 'time', 'datetime-local', 'month', 'week']);\n\ntype FieldState = 'disabled' | 'error-focus' | 'error-hover' | 'error' | 'focus' | 'hover' | 'rest';\n\n/** Props React Aria names with an `is` prefix; we use the HTML-style names instead. */\ntype RenamedAriaProps =\n | 'isDisabled'\n | 'isReadOnly'\n | 'isRequired'\n | 'isInvalid'\n | 'label'\n | 'description'\n | 'inputElementType'\n | 'value'\n | 'defaultValue'\n | 'onChange'\n // Left out: the deprecated `validationState` (`invalid` replaces it) and React Aria's\n // `excludeFromTabOrder` (`tabIndex={-1}` covers the rare case).\n | 'validationState'\n | 'excludeFromTabOrder';\n\ninterface TextFieldOwnProps\n extends\n Omit<AriaTextFieldProps<HTMLInputElement>, RenamedAriaProps>,\n Omit<WrapperRootDOMProps, keyof AriaTextFieldProps<HTMLInputElement>>,\n // The attributes that describe the input go to it, with React Aria's `aria-controls`,\n // `aria-haspopup`, `aria-autocomplete` and `aria-activedescendant`: a field that opens its\n // own suggestions can say so.\n Pick<\n AriaAttributes,\n | 'aria-expanded'\n | 'aria-required'\n | 'aria-invalid'\n | 'aria-readonly'\n | 'aria-multiline'\n | 'aria-placeholder'\n > {\n /** The `<input>`'s (or `<textarea>`'s) id, for a `<label for>` or an error summary's link. */\n id?: string;\n /** The container style: a filled surface or an outline. @default \"filled\" */\n variant?: TextFieldVariant;\n /**\n * Web density, `0` to `-3`: each step takes 4px off the container (56px at 0, 44px at -3).\n * Left out, the field takes the density of the nearest `ThemeProvider` or `ThemeScope`. M3\n * asks for density to be the reader's choice rather than a default, so prefer setting it\n * there, from a setting.\n * @default 0\n */\n density?: Density;\n /** The text, when controlled. */\n value?: string;\n /** The initial text, when uncontrolled. @default \"\" */\n defaultValue?: string;\n /** Called with the new text on every change. */\n onChange?: (value: string) => void;\n /**\n * Helper text below the field. Replaced visually by the error message when invalid, and\n * shown in the error colour while the field is invalid.\n */\n supportingText?: ReactNode;\n /**\n * Shows the supporting text only while the field has focus (M3 allows it \"persistently\n * visible or visible only on focus\"). Its space is kept, and screen readers always get it.\n * @default false\n */\n supportingTextOnFocus?: boolean;\n /** Marks the field invalid (in addition to `validate` and native constraints). */\n invalid?: boolean;\n /** A value is required: adds the `*` to the label and native `required` validation. */\n required?: boolean;\n /** Disables the field: it can't be focused or edited. @default false */\n disabled?: boolean;\n /**\n * The value can be read and copied but not changed. M3 styles it as a regular field and\n * asks for it to be \"clearly labeled as read-only\", e.g. in the supporting text. The root\n * gets `data-readonly`.\n * @default false\n */\n readOnly?: boolean;\n /** Decorative icon at the start, in a 48px box. */\n leadingIcon?: ReactNode;\n /**\n * Icon or icon button at the end (e.g. clear, visibility toggle), in a 48px box. It takes\n * the place of the error icon.\n */\n trailingIcon?: ReactNode;\n /**\n * The icon shown at the end while the field is invalid and has no `trailingIcon` (M3\n * strongly recommends one). Defaults to Material Symbols `error`, named by `labels.error`;\n * `false` turns it off.\n */\n errorIcon?: ReactNode | false;\n /** Text before the input, e.g. \"$\". Shown once the label floats. */\n prefix?: ReactNode;\n /**\n * What screen readers hear for the prefix, e.g. \"US dollars\" for \"$\". The prefix is in the\n * input's description either way; without this, its visible text is read.\n */\n prefixLabel?: string;\n /** Text after the input, e.g. \"kg\". Shown once the label floats. */\n suffix?: ReactNode;\n /** What screen readers hear for the suffix, e.g. \"kilograms\" for \"kg\". */\n suffixLabel?: string;\n /**\n * Renders a `<textarea>`. By default it's an M3 text area: a fixed height of `rows` lines\n * that scrolls vertically, which M3 recommends over growing fields on the web.\n * @default false\n */\n multiline?: boolean;\n /**\n * With `multiline`: grow with the content from `rows` up to `maxRows` (M3's multi-line\n * field) instead of keeping a fixed height.\n * @default false\n */\n autoGrow?: boolean;\n /** Visible lines of a multiline field. @default 4, or 1 with autoGrow */\n rows?: number;\n /** With `autoGrow`: lines after which the field scrolls instead of growing. */\n maxRows?: number;\n /** Shows `count/maxLength` below the field. Defaults to `true` when `maxLength` is set. */\n counter?: boolean;\n /** Words the field announces, for other languages. */\n labels?: Partial<TextFieldLabels>;\n /** Classes for the root element, merged with `classNames.root`; consumer classes win. */\n className?: string;\n /** Classes for each part, by slot. */\n classNames?: TextFieldClassNames;\n /** Inline styles for the root element. */\n style?: CSSProperties;\n /** The root element. Use `inputRef` for the input itself. */\n ref?: Ref<HTMLDivElement>;\n /** The `<input>` (or `<textarea>` when multiline), e.g. for form libraries. */\n inputRef?: Ref<HTMLInputElement | HTMLTextAreaElement>;\n}\n\n/** A visible label, or an accessible name for fields without one. */\ntype TextFieldLabel =\n | {\n /** The visible label, which floats above the input once it has focus or a value. */\n label: ReactNode;\n }\n | {\n label?: undefined;\n /** Accessible name when there's no visible label. */\n 'aria-label': string;\n }\n | {\n label?: undefined;\n /** The id of the element that names the field, when there's no visible label. */\n 'aria-labelledby': string;\n };\n\nexport type TextFieldProps = TextFieldOwnProps & TextFieldLabel;\n\nconst INTERACTIVE = 'input, textarea, button, a, [role=\"button\"]';\n\n/**\n * M3 text field: filled or outlined, with a floating label, leading / trailing icons,\n * prefix and suffix, supporting text, error message and icon, character counter, and a\n * multiline form (a fixed-height text area, or a growing field with `autoGrow`). Built on React Aria `useTextField`, so labelling,\n * descriptions, `validate` and native constraints work as in React Aria.\n *\n * `className` and `style` go on the root; `inputRef` reaches the input.\n *\n * @example\n * <TextField label=\"Email\" type=\"email\" required supportingText=\"We never share it.\" />\n * <TextField variant=\"outlined\" label=\"Price\" prefix=\"$\" inputMode=\"decimal\" />\n */\nexport function TextField(props: TextFieldProps) {\n const {\n variant = 'filled',\n density,\n label,\n value: valueProp,\n defaultValue,\n onChange,\n supportingText,\n supportingTextOnFocus = false,\n errorMessage,\n invalid,\n required,\n disabled = false,\n readOnly,\n leadingIcon,\n trailingIcon,\n errorIcon,\n prefix,\n prefixLabel,\n suffix,\n suffixLabel,\n multiline = false,\n autoGrow = false,\n rows = autoGrow ? 1 : 4,\n maxRows,\n counter,\n labels: labelsProp,\n className,\n classNames,\n style,\n ref,\n inputRef,\n ...otherProps\n } = props;\n\n // `data-*` attributes and the root's DOM attributes describe the component, so they go on the\n // root with `className`; everything else (the input's ARIA, attributes and events) goes to\n // the input via React Aria.\n const { root: rootData, rest: ariaProps } = splitRootProps(otherProps, { wrapper: true });\n const labels = { ...DEFAULT_LABELS, ...labelsProp };\n const prefixId = useId();\n const suffixId = useId();\n const counterId = useId();\n\n const [value, setValue] = useControlledState(valueProp, defaultValue ?? '', onChange);\n const domInputRef = useObjectRef(inputRef as Ref<HTMLInputElement>);\n const {\n labelProps,\n inputProps,\n descriptionProps,\n errorMessageProps,\n isInvalid,\n validationErrors,\n validationDetails,\n } = useTextField(\n {\n ...ariaProps,\n label,\n description: supportingText,\n errorMessage,\n value,\n onChange: setValue,\n isInvalid: invalid,\n isRequired: required,\n isDisabled: disabled,\n isReadOnly: readOnly,\n inputElementType: multiline ? 'textarea' : 'input',\n } as AriaTextFieldOptions<'input' | 'textarea'>,\n domInputRef,\n );\n const { focusProps, isFocused } = useFocusRing({ isTextInput: true });\n const { hoverProps, isHovered } = useHover({ isDisabled: disabled });\n\n const growing = multiline && autoGrow;\n // Grow a multiline field with its content, up to maxRows.\n useLayoutEffect(() => {\n const element = domInputRef.current as HTMLTextAreaElement | null;\n if (!growing || !element) return;\n element.style.height = 'auto';\n const lineHeight = Number.parseFloat(getComputedStyle(element).lineHeight) || 24;\n const max = maxRows ? lineHeight * maxRows : Number.POSITIVE_INFINITY;\n element.style.height = `${Math.min(element.scrollHeight, max)}px`;\n element.style.overflowY = element.scrollHeight > max ? 'auto' : 'hidden';\n }, [value, growing, maxRows, domInputRef]);\n\n const hasLabel = label !== undefined && label !== null;\n const floated =\n hasLabel && (isFocused || value.length > 0 || FORMAT_SHOWING_TYPES.has(ariaProps.type ?? ''));\n const state: FieldState = disabled\n ? 'disabled'\n : isInvalid\n ? isFocused\n ? 'error-focus'\n : isHovered\n ? 'error-hover'\n : 'error'\n : isFocused\n ? 'focus'\n : isHovered\n ? 'hover'\n : 'rest';\n\n const resolvedError =\n typeof errorMessage === 'function'\n ? errorMessage({ isInvalid, validationErrors, validationDetails })\n : (errorMessage ?? validationErrors.join(' '));\n const showError = isInvalid && Boolean(resolvedError);\n const maxLength = ariaProps.maxLength;\n const showCounter = maxLength !== undefined && counter !== false;\n const showErrorIcon = isInvalid && !trailingIcon && errorIcon !== false;\n\n const styles = textFieldStyles({\n variant,\n hasLabel,\n hasLeadingIcon: Boolean(leadingIcon),\n hasTrailingIcon: Boolean(trailingIcon) || showErrorIcon,\n multiline,\n fixedHeight: multiline && !autoGrow,\n supportingTextOnFocus,\n });\n // The affixes and the counter join React Aria's description (supporting text, then error).\n const describedBy =\n [\n prefix ? prefixId : null,\n suffix ? suffixId : null,\n renderedDescription(inputProps['aria-describedby'], [\n !supportingText && descriptionProps.id,\n !showError && errorMessageProps.id,\n ]),\n showCounter ? counterId : null,\n ]\n .filter(Boolean)\n .join(' ') || undefined;\n\n // Pressing the container (not an icon button) focuses the input, like Compose.\n const focusInput = (event: PointerEvent<HTMLDivElement>) => {\n if ((event.target as Element).closest(INTERACTIVE) || disabled) return;\n event.preventDefault();\n // The field is where the press landed: the page never scrolls to it (2026-10-15).\n domInputRef.current?.focus({ preventScroll: true });\n };\n // Chrome's touch adjustment can send a tap's pointer events to the input while the mouse\n // events it emulates, and the click, land on the padding beside it, so the input never\n // takes focus (2026-10-17). A click on the field that leaves the input unfocused focuses it.\n // The mouse never needs it: its press on the padding already focused the input.\n const focusInputOnClick = (event: MouseEvent<HTMLDivElement>) => {\n const target = event.target as Element;\n if (\n (event.nativeEvent as globalThis.PointerEvent).pointerType === 'mouse' ||\n !event.currentTarget.contains(target) ||\n target.closest(INTERACTIVE) ||\n disabled ||\n document.activeElement === domInputRef.current\n ) {\n return;\n }\n domInputRef.current?.focus({ preventScroll: true });\n };\n\n // Part of the accessible name, as M3 asks (\"The accessibility label must include the\n // asterisk\"); the copy in the outline's notch is hidden with the fieldset.\n const requiredMark = required ? ' *' : null;\n const Input = multiline ? 'textarea' : 'input';\n\n return (\n <div\n {...rootData}\n ref={ref}\n style={style}\n className={styles.root({ class: cn(classNames?.root, className) })}\n data-tokens={mergeDataTokens('md-comp-text-field', rootData)}\n data-variant={variant}\n {...densityAttribute(density)}\n data-field-state={state}\n data-focused={isFocused || undefined}\n data-hovered={(isHovered && !disabled) || undefined}\n data-invalid={isInvalid || undefined}\n data-disabled={disabled || undefined}\n data-readonly={readOnly || undefined}\n data-floated={floated || undefined}\n >\n <div\n {...hoverProps}\n onPointerDown={focusInput}\n onClick={focusInputOnClick}\n data-slot=\"container\"\n className={styles.container({ class: classNames?.container })}\n >\n {variant === 'outlined' && (\n <fieldset\n aria-hidden=\"true\"\n data-slot=\"outline\"\n className={styles.outline({ class: classNames?.outline })}\n >\n {hasLabel && (\n <legend className={styles.notch()}>\n <span className={styles.notchClip()}>\n <span className={styles.notchText()}>\n {label}\n {requiredMark}\n </span>\n </span>\n </legend>\n )}\n </fieldset>\n )}\n {leadingIcon && (\n <span\n aria-hidden=\"true\"\n data-slot=\"leading-icon\"\n className={styles.icon({ class: cn(styles.leadingIcon(), classNames?.leadingIcon) })}\n >\n {leadingIcon}\n </span>\n )}\n <div className={styles.field()}>\n {prefix && (\n <span\n id={prefixId}\n className={styles.affix({ class: cn(styles.prefix(), classNames?.prefix) })}\n >\n <Affix text={prefix} label={prefixLabel} />\n </span>\n )}\n <Input\n {...(mergeProps(inputProps, pickWidgetAria(ariaProps), focusProps) as object)}\n aria-describedby={describedBy}\n ref={domInputRef as Ref<never>}\n data-slot=\"input\"\n rows={multiline ? rows : undefined}\n className={styles.input({ class: classNames?.input })}\n />\n {suffix && (\n <span\n id={suffixId}\n className={styles.affix({ class: cn(styles.suffix(), classNames?.suffix) })}\n >\n <Affix text={suffix} label={suffixLabel} />\n </span>\n )}\n </div>\n {trailingIcon ? (\n <span\n data-slot=\"trailing-icon\"\n className={styles.icon({\n class: cn(styles.trailingIcon(), classNames?.trailingIcon),\n })}\n >\n {trailingIcon}\n </span>\n ) : showErrorIcon ? (\n <span className={styles.icon({ class: styles.trailingIcon() })}>\n <FieldErrorIcon\n label={labels.error}\n icon={errorIcon}\n className={styles.errorIcon({ class: classNames?.errorIcon })}\n />\n </span>\n ) : null}\n {hasLabel && (\n <FieldLabel\n {...labelProps}\n data-slot=\"label\"\n className={styles.label({ class: classNames?.label })}\n >\n {label}\n {requiredMark}\n </FieldLabel>\n )}\n <span\n aria-hidden=\"true\"\n data-slot=\"active-indicator\"\n className={styles.indicator({ class: classNames?.activeIndicator })}\n />\n </div>\n {(supportingText || showError || showCounter) && (\n <div className={styles.supporting()}>\n {supportingText && (\n <SupportingText\n {...descriptionProps}\n data-slot=\"supporting-text\"\n className={cn(\n styles.supportingText({\n class: cn(styles.supportingTextColor(), classNames?.supportingText),\n }),\n showError && 'sr-only',\n )}\n >\n {supportingText}\n </SupportingText>\n )}\n {showError && (\n <ErrorText\n {...errorMessageProps}\n className={styles.supportingText({\n supportingTextOnFocus: false,\n class: classNames?.errorText,\n })}\n >\n {resolvedError}\n </ErrorText>\n )}\n {showCounter && (\n <CharacterCounter\n id={counterId}\n count={value.length}\n max={maxLength}\n label={labels.characterCount(value.length, maxLength)}\n className={styles.counter({ class: classNames?.counter })}\n />\n )}\n </div>\n )}\n </div>\n );\n}\n\n/** An affix's visible text, read as its `label` when there is one. */\nfunction Affix({ text, label }: { text: ReactNode; label: string | undefined }) {\n if (label === undefined) return text;\n return (\n <>\n <span aria-hidden=\"true\">{text}</span>\n <span className=\"sr-only\">{label}</span>\n </>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TextField.js","names":[],"sources":["../../../src/components/text-field/TextField.tsx"],"sourcesContent":["'use client';\n\nimport {\n useId,\n useLayoutEffect,\n type AriaAttributes,\n type CSSProperties,\n type MouseEvent,\n type PointerEvent,\n type ReactNode,\n type Ref,\n} from 'react';\nimport {\n mergeProps,\n useFocusRing,\n useHover,\n useObjectRef,\n useTextField,\n type AriaTextFieldOptions,\n type AriaTextFieldProps,\n} from 'react-aria';\nimport { useControlledState } from 'react-stately/useControlledState';\nimport {\n CharacterCounter,\n ErrorText,\n FieldErrorIcon,\n FieldLabel,\n SupportingText,\n} from '../../primitives/Field';\nimport { densityAttribute, type Density } from '../../tokens/density';\nimport { cn } from '../../utils/cn';\nimport { renderedDescription } from '../../utils/rendered-description';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\nimport { pickWidgetAria, splitRootProps, type WrapperRootDOMProps } from '../../utils/root-props';\nimport { textFieldStyles, type TextFieldVariant } from './text-field-styles';\n\nexport interface TextFieldClassNames {\n /** The root element, merged with `className`. */\n root?: string;\n /** The filled or outlined container around the label, input and icons. */\n container?: string;\n /** The floating label. */\n label?: string;\n /** The `<input>` or `<textarea>`. */\n input?: string;\n /** The leading icon's 48px box. */\n leadingIcon?: string;\n /** The trailing icon's 48px box. */\n trailingIcon?: string;\n /** The prefix text before the input. */\n prefix?: string;\n /** The suffix text after the input. */\n suffix?: string;\n /** The helper text below the field. */\n supportingText?: string;\n /** The error message below the field. */\n errorText?: string;\n /** The character counter below the field. */\n counter?: string;\n /** The built-in error icon. */\n errorIcon?: string;\n /** The outlined field's border (a `<fieldset>`, notched for the floated label). */\n outline?: string;\n /** The filled field's active indicator, the line along the container's bottom. */\n activeIndicator?: string;\n}\n\n/** Words the text field announces; override them for other languages. */\nexport interface TextFieldLabels {\n /** The error icon's name (M3: \"Error\"). @default \"Error\" */\n error: string;\n /**\n * What screen readers hear for the character counter (M3 calls it the \"character count\").\n * @default (count, max) => `Character count: ${count} of ${max}`\n */\n characterCount: (count: number, max: number) => string;\n}\n\nconst DEFAULT_LABELS: TextFieldLabels = {\n error: 'Error',\n characterCount: (count, max) => `Character count: ${count} of ${max}`,\n};\n\n/** Input types the browser draws its own format in when empty, which a resting label would cover. */\nconst FORMAT_SHOWING_TYPES = new Set(['date', 'time', 'datetime-local', 'month', 'week']);\n\ntype FieldState = 'disabled' | 'error-focus' | 'error-hover' | 'error' | 'focus' | 'hover' | 'rest';\n\n/** Props React Aria names with an `is` prefix; we use the HTML-style names instead. */\ntype RenamedAriaProps =\n | 'isDisabled'\n | 'isReadOnly'\n | 'isRequired'\n | 'isInvalid'\n | 'label'\n | 'description'\n | 'inputElementType'\n | 'value'\n | 'defaultValue'\n | 'onChange'\n // Left out: the deprecated `validationState` (`invalid` replaces it) and React Aria's\n // `excludeFromTabOrder` (`tabIndex={-1}` covers the rare case).\n | 'validationState'\n | 'excludeFromTabOrder';\n\ninterface TextFieldOwnProps\n extends\n Omit<AriaTextFieldProps<HTMLInputElement>, RenamedAriaProps>,\n Omit<WrapperRootDOMProps, keyof AriaTextFieldProps<HTMLInputElement>>,\n // The attributes that describe the input go to it, with React Aria's `aria-controls`,\n // `aria-haspopup`, `aria-autocomplete` and `aria-activedescendant`: a field that opens its\n // own suggestions can say so.\n Pick<\n AriaAttributes,\n | 'aria-expanded'\n | 'aria-required'\n | 'aria-invalid'\n | 'aria-readonly'\n | 'aria-multiline'\n | 'aria-placeholder'\n > {\n /** The `<input>`'s (or `<textarea>`'s) id, for a `<label for>` or an error summary's link. */\n id?: string;\n /** The container style: a filled surface or an outline. @default \"filled\" */\n variant?: TextFieldVariant;\n /**\n * Web density, `0` to `-3`: each step takes 4px off the container (56px at 0, 44px at -3).\n * Left out, the field takes the density of the nearest `ThemeProvider` or `ThemeScope`. M3\n * asks for density to be the reader's choice rather than a default, so prefer setting it\n * there, from a setting.\n * @default 0\n */\n density?: Density;\n /** The text, when controlled. */\n value?: string;\n /** The initial text, when uncontrolled. @default \"\" */\n defaultValue?: string;\n /** Called with the new text on every change. */\n onChange?: (value: string) => void;\n /**\n * Helper text below the field. Replaced visually by the error message when invalid, and\n * shown in the error colour while the field is invalid.\n */\n supportingText?: ReactNode;\n /**\n * Shows the supporting text only while the field has focus (M3 allows it \"persistently\n * visible or visible only on focus\"). Its space is kept, and screen readers always get it.\n * @default false\n */\n supportingTextOnFocus?: boolean;\n /** Marks the field invalid (in addition to `validate` and native constraints). */\n invalid?: boolean;\n /** A value is required: adds the `*` to the label and native `required` validation. */\n required?: boolean;\n /** Disables the field: it can't be focused or edited. @default false */\n disabled?: boolean;\n /**\n * The value can be read and copied but not changed. M3 styles it as a regular field and\n * asks for it to be \"clearly labeled as read-only\", e.g. in the supporting text. The root\n * gets `data-readonly`.\n * @default false\n */\n readOnly?: boolean;\n /** Decorative icon at the start, in a 48px box. */\n leadingIcon?: ReactNode;\n /**\n * Icon or icon button at the end (e.g. clear, visibility toggle), in a 48px box. It takes\n * the place of the error icon.\n */\n trailingIcon?: ReactNode;\n /**\n * The icon shown at the end while the field is invalid and has no `trailingIcon` (M3\n * strongly recommends one). Defaults to Material Symbols `error`, named by `labels.error`;\n * `false` turns it off.\n */\n errorIcon?: ReactNode | false;\n /** Text before the input, e.g. \"$\". Shown once the label floats. */\n prefix?: ReactNode;\n /**\n * What screen readers hear for the prefix, e.g. \"US dollars\" for \"$\". The prefix is in the\n * input's description either way; without this, its visible text is read.\n */\n prefixLabel?: string;\n /** Text after the input, e.g. \"kg\". Shown once the label floats. */\n suffix?: ReactNode;\n /** What screen readers hear for the suffix, e.g. \"kilograms\" for \"kg\". */\n suffixLabel?: string;\n /**\n * Renders a `<textarea>`. By default it's an M3 text area: a fixed height of `rows` lines\n * that scrolls vertically, which M3 recommends over growing fields on the web.\n * @default false\n */\n multiline?: boolean;\n /**\n * With `multiline`: grow with the content from `rows` up to `maxRows` (M3's multi-line\n * field) instead of keeping a fixed height.\n * @default false\n */\n autoGrow?: boolean;\n /** Visible lines of a multiline field. @default 4, or 1 with autoGrow */\n rows?: number;\n /** With `autoGrow`: lines after which the field scrolls instead of growing. */\n maxRows?: number;\n /** Shows `count/maxLength` below the field. Defaults to `true` when `maxLength` is set. */\n counter?: boolean;\n /** Words the field announces, for other languages. */\n labels?: Partial<TextFieldLabels>;\n /** Classes for the root element, merged with `classNames.root`; consumer classes win. */\n className?: string;\n /** Classes for each part, by slot. */\n classNames?: TextFieldClassNames;\n /** Inline styles for the root element. */\n style?: CSSProperties;\n /** The root element. Use `inputRef` for the input itself. */\n ref?: Ref<HTMLDivElement>;\n /** The `<input>` (or `<textarea>` when multiline), e.g. for form libraries. */\n inputRef?: Ref<HTMLInputElement | HTMLTextAreaElement>;\n}\n\n/** A visible label, or an accessible name for fields without one. */\ntype TextFieldLabel =\n | {\n /** The visible label, which floats above the input once it has focus or a value. */\n label: ReactNode;\n }\n | {\n label?: undefined;\n /** Accessible name when there's no visible label. */\n 'aria-label': string;\n }\n | {\n label?: undefined;\n /** The id of the element that names the field, when there's no visible label. */\n 'aria-labelledby': string;\n };\n\nexport type TextFieldProps = TextFieldOwnProps & TextFieldLabel;\n\nconst INTERACTIVE = 'input, textarea, button, a, [role=\"button\"]';\n\n/**\n * M3 text field: filled or outlined, with a floating label, leading / trailing icons,\n * prefix and suffix, supporting text, error message and icon, character counter, and a\n * multiline form (a fixed-height text area, or a growing field with `autoGrow`). Built on React Aria `useTextField`, so labelling,\n * descriptions, `validate` and native constraints work as in React Aria.\n *\n * `className` and `style` go on the root; `inputRef` reaches the input.\n *\n * @example\n * <TextField label=\"Email\" type=\"email\" required supportingText=\"We never share it.\" />\n * <TextField variant=\"outlined\" label=\"Price\" prefix=\"$\" inputMode=\"decimal\" />\n */\nexport function TextField(props: TextFieldProps) {\n const {\n variant = 'filled',\n density,\n label,\n value: valueProp,\n defaultValue,\n onChange,\n supportingText,\n supportingTextOnFocus = false,\n errorMessage,\n invalid,\n required,\n disabled = false,\n readOnly,\n leadingIcon,\n trailingIcon,\n errorIcon,\n prefix,\n prefixLabel,\n suffix,\n suffixLabel,\n multiline = false,\n autoGrow = false,\n rows = autoGrow ? 1 : 4,\n maxRows,\n counter,\n labels: labelsProp,\n className,\n classNames,\n style,\n ref,\n inputRef,\n ...otherProps\n } = props;\n\n // `data-*` attributes and the root's DOM attributes describe the component, so they go on the\n // root with `className`; everything else (the input's ARIA, attributes and events) goes to\n // the input via React Aria.\n const { root: rootData, rest: ariaProps } = splitRootProps(otherProps, { wrapper: true });\n const labels = { ...DEFAULT_LABELS, ...labelsProp };\n const prefixId = useId();\n const suffixId = useId();\n const counterId = useId();\n\n const [value, setValue] = useControlledState(valueProp, defaultValue ?? '', onChange);\n const domInputRef = useObjectRef(inputRef as Ref<HTMLInputElement>);\n const {\n labelProps,\n inputProps,\n descriptionProps,\n errorMessageProps,\n isInvalid,\n validationErrors,\n validationDetails,\n } = useTextField(\n {\n ...ariaProps,\n label,\n description: supportingText,\n errorMessage,\n value,\n onChange: setValue,\n isInvalid: invalid,\n isRequired: required,\n isDisabled: disabled,\n isReadOnly: readOnly,\n inputElementType: multiline ? 'textarea' : 'input',\n } as AriaTextFieldOptions<'input' | 'textarea'>,\n domInputRef,\n );\n const { focusProps, isFocused } = useFocusRing({ isTextInput: true });\n const { hoverProps, isHovered } = useHover({ isDisabled: disabled });\n\n const growing = multiline && autoGrow;\n // Grow a multiline field with its content, up to maxRows.\n useLayoutEffect(() => {\n const element = domInputRef.current as HTMLTextAreaElement | null;\n if (!growing || !element) return;\n const fit = () => {\n element.style.height = 'auto';\n const lineHeight = Number.parseFloat(getComputedStyle(element).lineHeight) || 24;\n const max = maxRows ? lineHeight * maxRows : Number.POSITIVE_INFINITY;\n element.style.height = `${Math.min(element.scrollHeight, max)}px`;\n element.style.overflowY = element.scrollHeight > max ? 'auto' : 'hidden';\n };\n if (element.getClientRects().length > 0 || typeof ResizeObserver === 'undefined') {\n fit();\n return;\n }\n // Out of sight (`display: none` above it: a closed tab or panel), it has no height to read,\n // and set to its 0px it stayed invisible once shown. It keeps its rows until it shows.\n element.style.height = '';\n const observer = new ResizeObserver(() => {\n if (element.getClientRects().length === 0) return;\n observer.disconnect();\n fit();\n });\n observer.observe(element);\n return () => observer.disconnect();\n }, [value, growing, maxRows, domInputRef]);\n\n const hasLabel = label !== undefined && label !== null;\n const floated =\n hasLabel && (isFocused || value.length > 0 || FORMAT_SHOWING_TYPES.has(ariaProps.type ?? ''));\n const state: FieldState = disabled\n ? 'disabled'\n : isInvalid\n ? isFocused\n ? 'error-focus'\n : isHovered\n ? 'error-hover'\n : 'error'\n : isFocused\n ? 'focus'\n : isHovered\n ? 'hover'\n : 'rest';\n\n const resolvedError =\n typeof errorMessage === 'function'\n ? errorMessage({ isInvalid, validationErrors, validationDetails })\n : (errorMessage ?? validationErrors.join(' '));\n const showError = isInvalid && Boolean(resolvedError);\n const maxLength = ariaProps.maxLength;\n const showCounter = maxLength !== undefined && counter !== false;\n const showErrorIcon = isInvalid && !trailingIcon && errorIcon !== false;\n\n const styles = textFieldStyles({\n variant,\n hasLabel,\n hasLeadingIcon: Boolean(leadingIcon),\n hasTrailingIcon: Boolean(trailingIcon) || showErrorIcon,\n multiline,\n fixedHeight: multiline && !autoGrow,\n supportingTextOnFocus,\n });\n // The affixes and the counter join React Aria's description (supporting text, then error).\n const describedBy =\n [\n prefix ? prefixId : null,\n suffix ? suffixId : null,\n renderedDescription(inputProps['aria-describedby'], [\n !supportingText && descriptionProps.id,\n !showError && errorMessageProps.id,\n ]),\n showCounter ? counterId : null,\n ]\n .filter(Boolean)\n .join(' ') || undefined;\n\n // Pressing the container (not an icon button) focuses the input, like Compose.\n const focusInput = (event: PointerEvent<HTMLDivElement>) => {\n if ((event.target as Element).closest(INTERACTIVE) || disabled) return;\n event.preventDefault();\n // The field is where the press landed: the page never scrolls to it (2026-10-15).\n domInputRef.current?.focus({ preventScroll: true });\n };\n // Chrome's touch adjustment can send a tap's pointer events to the input while the mouse\n // events it emulates, and the click, land on the padding beside it, so the input never\n // takes focus (2026-10-17). A click on the field that leaves the input unfocused focuses it.\n // The mouse never needs it: its press on the padding already focused the input.\n const focusInputOnClick = (event: MouseEvent<HTMLDivElement>) => {\n const target = event.target as Element;\n if (\n (event.nativeEvent as globalThis.PointerEvent).pointerType === 'mouse' ||\n !event.currentTarget.contains(target) ||\n target.closest(INTERACTIVE) ||\n disabled ||\n document.activeElement === domInputRef.current\n ) {\n return;\n }\n domInputRef.current?.focus({ preventScroll: true });\n };\n\n // Part of the accessible name, as M3 asks (\"The accessibility label must include the\n // asterisk\"); the copy in the outline's notch is hidden with the fieldset.\n const requiredMark = required ? ' *' : null;\n const Input = multiline ? 'textarea' : 'input';\n\n return (\n <div\n {...rootData}\n ref={ref}\n style={style}\n className={styles.root({ class: cn(classNames?.root, className) })}\n data-tokens={mergeDataTokens('md-comp-text-field', rootData)}\n data-variant={variant}\n {...densityAttribute(density)}\n data-field-state={state}\n data-focused={isFocused || undefined}\n data-hovered={(isHovered && !disabled) || undefined}\n data-invalid={isInvalid || undefined}\n data-disabled={disabled || undefined}\n data-readonly={readOnly || undefined}\n data-floated={floated || undefined}\n >\n <div\n {...hoverProps}\n onPointerDown={focusInput}\n onClick={focusInputOnClick}\n data-slot=\"container\"\n className={styles.container({ class: classNames?.container })}\n >\n {variant === 'outlined' && (\n <fieldset\n aria-hidden=\"true\"\n data-slot=\"outline\"\n className={styles.outline({ class: classNames?.outline })}\n >\n {hasLabel && (\n <legend className={styles.notch()}>\n <span className={styles.notchClip()}>\n <span className={styles.notchText()}>\n {label}\n {requiredMark}\n </span>\n </span>\n </legend>\n )}\n </fieldset>\n )}\n {leadingIcon && (\n <span\n aria-hidden=\"true\"\n data-slot=\"leading-icon\"\n className={styles.icon({ class: cn(styles.leadingIcon(), classNames?.leadingIcon) })}\n >\n {leadingIcon}\n </span>\n )}\n <div className={styles.field()}>\n {prefix && (\n <span\n id={prefixId}\n className={styles.affix({ class: cn(styles.prefix(), classNames?.prefix) })}\n >\n <Affix text={prefix} label={prefixLabel} />\n </span>\n )}\n <Input\n {...(mergeProps(inputProps, pickWidgetAria(ariaProps), focusProps) as object)}\n aria-describedby={describedBy}\n ref={domInputRef as Ref<never>}\n data-slot=\"input\"\n rows={multiline ? rows : undefined}\n className={styles.input({ class: classNames?.input })}\n />\n {suffix && (\n <span\n id={suffixId}\n className={styles.affix({ class: cn(styles.suffix(), classNames?.suffix) })}\n >\n <Affix text={suffix} label={suffixLabel} />\n </span>\n )}\n </div>\n {trailingIcon ? (\n <span\n data-slot=\"trailing-icon\"\n className={styles.icon({\n class: cn(styles.trailingIcon(), classNames?.trailingIcon),\n })}\n >\n {trailingIcon}\n </span>\n ) : showErrorIcon ? (\n <span className={styles.icon({ class: styles.trailingIcon() })}>\n <FieldErrorIcon\n label={labels.error}\n icon={errorIcon}\n className={styles.errorIcon({ class: classNames?.errorIcon })}\n />\n </span>\n ) : null}\n {hasLabel && (\n <FieldLabel\n {...labelProps}\n data-slot=\"label\"\n className={styles.label({ class: classNames?.label })}\n >\n {label}\n {requiredMark}\n </FieldLabel>\n )}\n <span\n aria-hidden=\"true\"\n data-slot=\"active-indicator\"\n className={styles.indicator({ class: classNames?.activeIndicator })}\n />\n </div>\n {(supportingText || showError || showCounter) && (\n <div className={styles.supporting()}>\n {supportingText && (\n <SupportingText\n {...descriptionProps}\n data-slot=\"supporting-text\"\n className={cn(\n styles.supportingText({\n class: cn(styles.supportingTextColor(), classNames?.supportingText),\n }),\n showError && 'sr-only',\n )}\n >\n {supportingText}\n </SupportingText>\n )}\n {showError && (\n <ErrorText\n {...errorMessageProps}\n className={styles.supportingText({\n supportingTextOnFocus: false,\n class: classNames?.errorText,\n })}\n >\n {resolvedError}\n </ErrorText>\n )}\n {showCounter && (\n <CharacterCounter\n id={counterId}\n count={value.length}\n max={maxLength}\n label={labels.characterCount(value.length, maxLength)}\n className={styles.counter({ class: classNames?.counter })}\n />\n )}\n </div>\n )}\n </div>\n );\n}\n\n/** An affix's visible text, read as its `label` when there is one. */\nfunction Affix({ text, label }: { text: ReactNode; label: string | undefined }) {\n if (label === undefined) return text;\n return (\n <>\n <span aria-hidden=\"true\">{text}</span>\n <span className=\"sr-only\">{label}</span>\n </>\n );\n}\n"],"mappings":"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"}
@@ -78,9 +78,9 @@ const textFieldStyles = tv({
78
78
  ],
79
79
  field: "flex min-w-0 flex-1 items-center",
80
80
  input: [
81
- "min-w-0 flex-1 bg-transparent p-0 text-body-large outline-none",
81
+ "min-w-0 flex-1 bg-transparent p-0 text-body-large outline-none text-ellipsis",
82
82
  "text-[color:var(--_md-comp-text-field-input-text-color)] caret-[color:var(--_md-comp-text-field-caret-color)]",
83
- "placeholder:text-on-surface-variant placeholder:opacity-100",
83
+ "placeholder:text-on-surface-variant placeholder:opacity-100 placeholder:text-ellipsis",
84
84
  "disabled:cursor-default group-data-[field-state=disabled]/field:text-on-surface/38",
85
85
  "group-data-invalid/field:caret-error",
86
86
  "resize-none"
@@ -1 +1 @@
1
- {"version":3,"file":"text-field-styles.js","names":[],"sources":["../../../src/components/text-field/text-field-styles.ts"],"sourcesContent":["import { tv, type VariantProps } from '../../utils/tv';\n\n/*\n * Values follow Compose Material 3 (TextField.kt, OutlinedTextField.kt,\n * internal/TextFieldImpl.kt and the Filled/OutlinedTextFieldTokens files):\n * 56px minimum height, 16px horizontal padding (4px next to a 48px icon box), 8px\n * vertical padding with a label, 4px above supporting text, 2px around prefix/suffix,\n * a 4px gap around the outlined label notch, label float on the fast spatial spring.\n *\n * Web density (`--md-sys-density`, 0 to -3; M3: \"decreases the top and bottom padding\"): each\n * step takes 4px off the container (56 → 52 → 48 → 44px), 2px from the padding above and below\n * the value, and moves the resting and the filled field's floated label up 2px, so they stay\n * centred and clear of the value. The icon boxes are at most as tall as the container (the\n * 48px touch targets of icon buttons in them extend past it), and at -3 the container's\n * `::before` / `::after` add 2px of hit area above and below, so pressing within 48px of its\n * centre still focuses the field.\n *\n * Colours depend on one mutually exclusive `data-field-state` on the root\n * (disabled > error-focus > error-hover > error > focus > hover > rest), so part styles\n * never compete.\n *\n * The resting container colour and shape, outline and indicator colours, the focused and\n * hovered outline, the focused indicator and label, and the label, input, caret, supporting\n * text and icon colours come from the text field's component tokens (`text-field-tokens.ts`,\n * `--md-comp-<variant>-text-field-*`); the root carries `data-tokens=\"md-comp-text-field\"`\n * and `data-variant`. A consumer class (`classNames.label`, …) still wins in `cn()`. Every\n * class is written out in full so Tailwind can find it.\n */\n\n// The label's top, start, font size, line height and letter spacing on the fast spatial\n// spring; its colour and opacity on the fast effects spring.\nconst geometryTransition =\n '[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration),var(--md-sys-motion-spring-effects-fast-duration),var(--md-sys-motion-spring-effects-fast-duration)] [transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing),var(--md-sys-motion-spring-effects-fast-easing),var(--md-sys-motion-spring-effects-fast-easing)]';\n\n/*\n * Colour on the fast effects spring, size on the fast spatial spring (Compose animates the\n * indicator thickness on FastSpatial and its colour on FastEffects).\n */\nconst colourAndSizeTransition =\n '[transition-duration:var(--md-sys-motion-spring-effects-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration)] [transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing)]';\n\nconst spatialTransition =\n '[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration)] [transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing)]';\n\nconst effectsTransition =\n '[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)] [transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]';\n\n/** Variant definitions for {@link TextField}. */\nexport const textFieldStyles = tv({\n slots: {\n root: 'group/field inline-flex w-[280px] max-w-full flex-col align-top text-start',\n // Library-owned inner wrapper: positions the label, outline and indicator.\n container: [\n 'relative flex min-h-[calc(56px+var(--md-sys-density,0)*4px)] cursor-text items-stretch',\n // Dense fields keep a 48px hit area: at -3 (44px) 2px more above and below.\n \"before:absolute before:inset-x-0 before:bottom-full before:h-[max(0px,calc(-4px-var(--md-sys-density,0)*2px))] before:content-['']\",\n \"after:absolute after:inset-x-0 after:top-full after:h-[max(0px,calc(-4px-var(--md-sys-density,0)*2px))] after:content-['']\",\n 'bg-[var(--_md-comp-text-field-container-color)]',\n ],\n outline: [\n 'pointer-events-none absolute inset-x-0 bottom-0 m-0 min-w-0 px-[12px]',\n 'rounded-[var(--_md-comp-text-field-container-shape)] border border-solid',\n 'border-[color:var(--_md-comp-text-field-outline-color)]',\n // Colour and width both on the fast effects spring. Compose animates the width on\n // FastSpatial, but a browser draws a border in whole device pixels, so a width that\n // overshoots 2px and settles back draws 2 → 1 → 2px. The effects spring never\n // overshoots, so the width only grows (and never dips below 1px on the way back).\n // The filled indicator keeps the spatial spring: its `height` draws fractions.\n '[transition-property:border-color,border-width]',\n effectsTransition,\n 'group-data-[field-state=hover]/field:border-[color:var(--_md-comp-text-field-hover-outline-color)]',\n 'group-data-[field-state=focus]/field:border-2',\n 'group-data-[field-state=focus]/field:border-[color:var(--_md-comp-text-field-focus-outline-color)]',\n 'group-data-[field-state=error]/field:border-error',\n 'group-data-[field-state=error-hover]/field:border-on-error-container',\n 'group-data-[field-state=error-focus]/field:border-2 group-data-[field-state=error-focus]/field:border-error',\n 'group-data-[field-state=disabled]/field:border-on-surface/12',\n ],\n // Hidden text the width of the floated label: cuts the notch in the outline. It opens\n // from 0fr to 1fr of the label's own width on the label's spring, so the gap keeps pace\n // with the label as it moves up.\n notch: [\n 'invisible grid h-[16px] grid-cols-[0fr] p-0 text-body-small whitespace-nowrap',\n 'transition-[grid-template-columns]',\n spatialTransition,\n 'group-data-floated/field:grid-cols-[1fr] group-has-[:autofill]/field:grid-cols-[1fr]',\n ],\n notchClip: 'min-w-0 overflow-hidden',\n notchText: 'inline-block px-[4px]',\n indicator: [\n 'pointer-events-none absolute inset-x-0 bottom-0 h-px',\n 'bg-[var(--_md-comp-text-field-active-indicator-color)]',\n '[transition-property:background-color,height]',\n colourAndSizeTransition,\n 'group-data-[field-state=hover]/field:bg-on-surface',\n 'group-data-[field-state=focus]/field:h-[2px]',\n 'group-data-[field-state=focus]/field:bg-[var(--_md-comp-text-field-focus-active-indicator-color)]',\n 'group-data-[field-state=error]/field:bg-error',\n 'group-data-[field-state=error-hover]/field:bg-on-error-container',\n 'group-data-[field-state=error-focus]/field:h-[2px] group-data-[field-state=error-focus]/field:bg-error',\n 'group-data-[field-state=disabled]/field:bg-on-surface/38',\n // Forced colours paint backgrounds as Canvas, which hid the filled field's only edge:\n // the line takes system colours instead (2026-10-15).\n 'forced-colors:bg-[CanvasText]! forced-colors:forced-color-adjust-none',\n 'forced-colors:group-data-[field-state=focus]/field:bg-[Highlight]! forced-colors:group-data-[field-state=error-focus]/field:bg-[Highlight]!',\n 'forced-colors:group-data-[field-state=disabled]/field:bg-[GrayText]!',\n ],\n label: [\n 'pointer-events-none absolute top-[calc(16px+var(--md-sys-density,0)*2px)] max-w-[calc(100%-32px)] truncate',\n 'text-body-large text-[color:var(--_md-comp-text-field-label-text-color)]',\n '[transition-property:top,inset-inline-start,font-size,line-height,letter-spacing,color,opacity]',\n geometryTransition,\n 'group-data-[field-state=focus]/field:text-[color:var(--_md-comp-text-field-focus-label-text-color)]',\n 'group-data-[field-state=error]/field:text-error',\n 'group-data-[field-state=error-hover]/field:text-on-error-container',\n 'group-data-[field-state=error-focus]/field:text-error',\n 'group-data-[field-state=disabled]/field:text-on-surface/38',\n ],\n icon: [\n 'flex h-[min(48px,calc(56px+var(--md-sys-density,0)*4px))] w-[48px] shrink-0 items-center justify-center self-center',\n // Icon buttons here keep their size at every density: the box is a fixed 48px column\n // that density only shortens. One with its own `density` still follows it.\n '[&_:not([data-density])]:[--md-sys-density:0]',\n '[&>svg]:size-[24px]',\n 'group-data-[field-state=disabled]/field:text-on-surface/38',\n ],\n leadingIcon: 'text-[color:var(--_md-comp-text-field-leading-icon-color)]',\n trailingIcon: [\n 'text-[color:var(--_md-comp-text-field-trailing-icon-color)]',\n 'group-data-[field-state=error]/field:text-error',\n 'group-data-[field-state=error-focus]/field:text-error',\n 'group-data-[field-state=error-hover]/field:text-on-error-container',\n ],\n field: 'flex min-w-0 flex-1 items-center',\n input: [\n 'min-w-0 flex-1 bg-transparent p-0 text-body-large outline-none',\n 'text-[color:var(--_md-comp-text-field-input-text-color)] caret-[color:var(--_md-comp-text-field-caret-color)]',\n 'placeholder:text-on-surface-variant placeholder:opacity-100',\n 'disabled:cursor-default group-data-[field-state=disabled]/field:text-on-surface/38',\n 'group-data-invalid/field:caret-error',\n 'resize-none',\n ],\n affix: [\n 'text-body-large text-on-surface-variant whitespace-nowrap',\n 'group-data-[field-state=disabled]/field:text-on-surface/38',\n ],\n prefix: 'pe-[2px]',\n suffix: 'ps-[2px]',\n supporting: 'flex gap-[16px] px-[16px] pt-[4px]',\n // The supporting text's resting colour; the error message keeps its own.\n supportingTextColor: 'text-[color:var(--_md-comp-text-field-supporting-text-color)]',\n // Error colour whenever the field is invalid, with or without a message (Compose's\n // `ErrorSupportingColor`), so the supporting text reads as the error's explanation.\n supportingText: [\n 'min-w-0 flex-1',\n 'group-data-[field-state=error]/field:text-error',\n 'group-data-[field-state=error-hover]/field:text-error',\n 'group-data-[field-state=error-focus]/field:text-error',\n 'group-data-[field-state=disabled]/field:text-on-surface/38',\n ],\n counter: [\n 'ms-auto',\n 'group-data-[field-state=error]/field:text-error',\n 'group-data-[field-state=error-hover]/field:text-error',\n 'group-data-[field-state=error-focus]/field:text-error',\n 'group-data-[field-state=disabled]/field:text-on-surface/38',\n ],\n // The built-in error icon's own box; the trailing slot colours it.\n errorIcon: '[&>svg]:size-[24px]',\n required: 'text-inherit',\n },\n variants: {\n variant: {\n filled: {\n container: [\n 'rounded-t-[var(--_md-comp-text-field-container-shape)]',\n 'group-data-[field-state=disabled]/field:bg-on-surface/4',\n ],\n outline: 'hidden',\n },\n outlined: {\n container: 'rounded-[var(--_md-comp-text-field-container-shape)]',\n indicator: 'hidden',\n // OutlinedTextFieldTokens.HoverLabelColor (filled keeps on-surface-variant).\n label: 'group-data-[field-state=hover]/field:text-on-surface',\n },\n },\n // With a label the outline starts 8px higher, so its border runs through the middle of\n // the 16px notch legend and lands on the container's top edge.\n hasLabel: { true: { outline: '-top-[8px]' }, false: { outline: 'top-0' } },\n hasLeadingIcon: {\n true: { field: 'ps-[4px]', label: 'start-[52px]' },\n false: { field: 'ps-[16px]', label: 'start-[16px]' },\n },\n hasTrailingIcon: {\n true: { field: 'pe-[4px]' },\n false: { field: 'pe-[16px]' },\n },\n multiline: {\n true: { field: 'items-start', input: 'block' },\n false: {},\n },\n // A text area keeps its height and scrolls (M3: \"a fixed height and scroll vertically\").\n fixedHeight: {\n true: { input: 'overflow-y-auto' },\n false: {},\n },\n // M3: supporting text can be \"visible only on focus\". It keeps its space, so nothing\n // jumps, and stays in the accessible description.\n supportingTextOnFocus: {\n true: {\n supportingText: [\n 'opacity-0 transition-opacity group-data-focused/field:opacity-100',\n effectsTransition,\n ],\n },\n false: {},\n },\n },\n compoundVariants: [\n /*\n * Input padding: filled fields with a label leave room above for the floated label.\n * Below 52px (density -2 and -3) there is no room for it: as MDC's text field\n * (`$minimum-height-for-filled-label: 52px`), the floated label is hidden and the value\n * is centred like a field without a label. The resting label still shows in the empty\n * field, and still names the input. `--m3-field-label-room` is 1 down to density -1\n * and 0 from -2 (a clamped step of the density, so CSS alone switches the layout).\n */\n {\n variant: 'filled',\n hasLabel: true,\n class: {\n container: '[--m3-field-label-room:clamp(0,calc((var(--md-sys-density,0)+1.5)*100),1)]',\n field: [\n 'pt-[calc(16px+var(--md-sys-density,0)*2px+8px*var(--m3-field-label-room))]',\n 'pb-[calc(16px+var(--md-sys-density,0)*2px-8px*var(--m3-field-label-room))]',\n ],\n },\n },\n {\n variant: 'filled',\n hasLabel: false,\n class: { field: 'py-[calc(16px+var(--md-sys-density,0)*2px)]' },\n },\n { variant: 'outlined', class: { field: 'py-[calc(16px+var(--md-sys-density,0)*2px)]' } },\n // Floated label: filled → 8px from the top (less 2px per density step); outlined → centred on the border, at 16px.\n {\n variant: 'filled',\n hasLabel: true,\n class: {\n label: [\n 'group-data-floated/field:top-[calc(8px+var(--md-sys-density,0)*2px)] group-data-floated/field:text-body-small',\n 'group-has-[:autofill]/field:top-[calc(8px+var(--md-sys-density,0)*2px)] group-has-[:autofill]/field:text-body-small',\n // Hidden once floated below 52px (see the padding above).\n 'group-data-floated/field:opacity-[var(--m3-field-label-room)] group-has-[:autofill]/field:opacity-[var(--m3-field-label-room)]',\n ],\n },\n },\n {\n variant: 'outlined',\n hasLabel: true,\n class: {\n label: [\n 'group-data-floated/field:top-[-8px] group-data-floated/field:start-[16px] group-data-floated/field:text-body-small',\n 'group-has-[:autofill]/field:top-[-8px] group-has-[:autofill]/field:start-[16px] group-has-[:autofill]/field:text-body-small',\n ],\n },\n },\n // With a label, the placeholder and prefix/suffix only show once the label floats.\n {\n hasLabel: true,\n class: {\n // The placeholder fades in as the label floats (Compose fades it on FastEffects).\n input: [\n 'placeholder:opacity-0 group-data-floated/field:placeholder:opacity-100',\n 'placeholder:transition-opacity placeholder:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)] placeholder:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]',\n ],\n affix: [\n 'opacity-0 transition-opacity group-data-floated/field:opacity-100 group-has-[:autofill]/field:opacity-100',\n effectsTransition,\n ],\n },\n },\n ],\n defaultVariants: {\n variant: 'filled',\n hasLabel: true,\n hasLeadingIcon: false,\n hasTrailingIcon: false,\n multiline: false,\n fixedHeight: false,\n supportingTextOnFocus: false,\n },\n});\n\n/**\n * The variants of {@link textFieldStyles}, for building on its styles. They are the recipe's keys, not\n * component props: their names (`hasIcon`, `isOpen`…) follow the classes and may change.\n */\nexport type TextFieldStyleProps = VariantProps<typeof textFieldStyles>;\nexport type TextFieldVariant = NonNullable<TextFieldStyleProps['variant']>;\n"],"mappings":";;AA+BA,MAAM,qBACJ;AAMF,MAAM,0BACJ;AAEF,MAAM,oBACJ;AAEF,MAAM,oBACJ;;AAGF,MAAa,kBAAkB,GAAG;CAChC,OAAO;EACL,MAAM;EAEN,WAAW;GACT;GAEA;GACA;GACA;EACF;EACA,SAAS;GACP;GACA;GACA;GAMA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF;EAIA,OAAO;GACL;GACA;GACA;GACA;EACF;EACA,WAAW;EACX,WAAW;EACX,WAAW;GACT;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GAGA;GACA;GACA;EACF;EACA,OAAO;GACL;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF;EACA,MAAM;GACJ;GAGA;GACA;GACA;EACF;EACA,aAAa;EACb,cAAc;GACZ;GACA;GACA;GACA;EACF;EACA,OAAO;EACP,OAAO;GACL;GACA;GACA;GACA;GACA;GACA;EACF;EACA,OAAO,CACL,6DACA,4DACF;EACA,QAAQ;EACR,QAAQ;EACR,YAAY;EAEZ,qBAAqB;EAGrB,gBAAgB;GACd;GACA;GACA;GACA;GACA;EACF;EACA,SAAS;GACP;GACA;GACA;GACA;GACA;EACF;EAEA,WAAW;EACX,UAAU;CACZ;CACA,UAAU;EACR,SAAS;GACP,QAAQ;IACN,WAAW,CACT,0DACA,yDACF;IACA,SAAS;GACX;GACA,UAAU;IACR,WAAW;IACX,WAAW;IAEX,OAAO;GACT;EACF;EAGA,UAAU;GAAE,MAAM,EAAE,SAAS,aAAa;GAAG,OAAO,EAAE,SAAS,QAAQ;EAAE;EACzE,gBAAgB;GACd,MAAM;IAAE,OAAO;IAAY,OAAO;GAAe;GACjD,OAAO;IAAE,OAAO;IAAa,OAAO;GAAe;EACrD;EACA,iBAAiB;GACf,MAAM,EAAE,OAAO,WAAW;GAC1B,OAAO,EAAE,OAAO,YAAY;EAC9B;EACA,WAAW;GACT,MAAM;IAAE,OAAO;IAAe,OAAO;GAAQ;GAC7C,OAAO,CAAC;EACV;EAEA,aAAa;GACX,MAAM,EAAE,OAAO,kBAAkB;GACjC,OAAO,CAAC;EACV;EAGA,uBAAuB;GACrB,MAAM,EACJ,gBAAgB,CACd,qEACA,iBACF,EACF;GACA,OAAO,CAAC;EACV;CACF;CACA,kBAAkB;EAShB;GACE,SAAS;GACT,UAAU;GACV,OAAO;IACL,WAAW;IACX,OAAO,CACL,8EACA,4EACF;GACF;EACF;EACA;GACE,SAAS;GACT,UAAU;GACV,OAAO,EAAE,OAAO,8CAA8C;EAChE;EACA;GAAE,SAAS;GAAY,OAAO,EAAE,OAAO,8CAA8C;EAAE;EAEvF;GACE,SAAS;GACT,UAAU;GACV,OAAO,EACL,OAAO;IACL;IACA;IAEA;GACF,EACF;EACF;EACA;GACE,SAAS;GACT,UAAU;GACV,OAAO,EACL,OAAO,CACL,sHACA,6HACF,EACF;EACF;EAEA;GACE,UAAU;GACV,OAAO;IAEL,OAAO,CACL,0EACA,6MACF;IACA,OAAO,CACL,6GACA,iBACF;GACF;EACF;CACF;CACA,iBAAiB;EACf,SAAS;EACT,UAAU;EACV,gBAAgB;EAChB,iBAAiB;EACjB,WAAW;EACX,aAAa;EACb,uBAAuB;CACzB;AACF,CAAC"}
1
+ {"version":3,"file":"text-field-styles.js","names":[],"sources":["../../../src/components/text-field/text-field-styles.ts"],"sourcesContent":["import { tv, type VariantProps } from '../../utils/tv';\n\n/*\n * Values follow Compose Material 3 (TextField.kt, OutlinedTextField.kt,\n * internal/TextFieldImpl.kt and the Filled/OutlinedTextFieldTokens files):\n * 56px minimum height, 16px horizontal padding (4px next to a 48px icon box), 8px\n * vertical padding with a label, 4px above supporting text, 2px around prefix/suffix,\n * a 4px gap around the outlined label notch, label float on the fast spatial spring.\n *\n * Web density (`--md-sys-density`, 0 to -3; M3: \"decreases the top and bottom padding\"): each\n * step takes 4px off the container (56 → 52 → 48 → 44px), 2px from the padding above and below\n * the value, and moves the resting and the filled field's floated label up 2px, so they stay\n * centred and clear of the value. The icon boxes are at most as tall as the container (the\n * 48px touch targets of icon buttons in them extend past it), and at -3 the container's\n * `::before` / `::after` add 2px of hit area above and below, so pressing within 48px of its\n * centre still focuses the field.\n *\n * Colours depend on one mutually exclusive `data-field-state` on the root\n * (disabled > error-focus > error-hover > error > focus > hover > rest), so part styles\n * never compete.\n *\n * The resting container colour and shape, outline and indicator colours, the focused and\n * hovered outline, the focused indicator and label, and the label, input, caret, supporting\n * text and icon colours come from the text field's component tokens (`text-field-tokens.ts`,\n * `--md-comp-<variant>-text-field-*`); the root carries `data-tokens=\"md-comp-text-field\"`\n * and `data-variant`. A consumer class (`classNames.label`, …) still wins in `cn()`. Every\n * class is written out in full so Tailwind can find it.\n */\n\n// The label's top, start, font size, line height and letter spacing on the fast spatial\n// spring; its colour and opacity on the fast effects spring.\nconst geometryTransition =\n '[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration),var(--md-sys-motion-spring-effects-fast-duration),var(--md-sys-motion-spring-effects-fast-duration)] [transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing),var(--md-sys-motion-spring-effects-fast-easing),var(--md-sys-motion-spring-effects-fast-easing)]';\n\n/*\n * Colour on the fast effects spring, size on the fast spatial spring (Compose animates the\n * indicator thickness on FastSpatial and its colour on FastEffects).\n */\nconst colourAndSizeTransition =\n '[transition-duration:var(--md-sys-motion-spring-effects-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration)] [transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing)]';\n\nconst spatialTransition =\n '[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration)] [transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing)]';\n\nconst effectsTransition =\n '[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)] [transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]';\n\n/** Variant definitions for {@link TextField}. */\nexport const textFieldStyles = tv({\n slots: {\n root: 'group/field inline-flex w-[280px] max-w-full flex-col align-top text-start',\n // Library-owned inner wrapper: positions the label, outline and indicator.\n container: [\n 'relative flex min-h-[calc(56px+var(--md-sys-density,0)*4px)] cursor-text items-stretch',\n // Dense fields keep a 48px hit area: at -3 (44px) 2px more above and below.\n \"before:absolute before:inset-x-0 before:bottom-full before:h-[max(0px,calc(-4px-var(--md-sys-density,0)*2px))] before:content-['']\",\n \"after:absolute after:inset-x-0 after:top-full after:h-[max(0px,calc(-4px-var(--md-sys-density,0)*2px))] after:content-['']\",\n 'bg-[var(--_md-comp-text-field-container-color)]',\n ],\n outline: [\n 'pointer-events-none absolute inset-x-0 bottom-0 m-0 min-w-0 px-[12px]',\n 'rounded-[var(--_md-comp-text-field-container-shape)] border border-solid',\n 'border-[color:var(--_md-comp-text-field-outline-color)]',\n // Colour and width both on the fast effects spring. Compose animates the width on\n // FastSpatial, but a browser draws a border in whole device pixels, so a width that\n // overshoots 2px and settles back draws 2 → 1 → 2px. The effects spring never\n // overshoots, so the width only grows (and never dips below 1px on the way back).\n // The filled indicator keeps the spatial spring: its `height` draws fractions.\n '[transition-property:border-color,border-width]',\n effectsTransition,\n 'group-data-[field-state=hover]/field:border-[color:var(--_md-comp-text-field-hover-outline-color)]',\n 'group-data-[field-state=focus]/field:border-2',\n 'group-data-[field-state=focus]/field:border-[color:var(--_md-comp-text-field-focus-outline-color)]',\n 'group-data-[field-state=error]/field:border-error',\n 'group-data-[field-state=error-hover]/field:border-on-error-container',\n 'group-data-[field-state=error-focus]/field:border-2 group-data-[field-state=error-focus]/field:border-error',\n 'group-data-[field-state=disabled]/field:border-on-surface/12',\n ],\n // Hidden text the width of the floated label: cuts the notch in the outline. It opens\n // from 0fr to 1fr of the label's own width on the label's spring, so the gap keeps pace\n // with the label as it moves up.\n notch: [\n 'invisible grid h-[16px] grid-cols-[0fr] p-0 text-body-small whitespace-nowrap',\n 'transition-[grid-template-columns]',\n spatialTransition,\n 'group-data-floated/field:grid-cols-[1fr] group-has-[:autofill]/field:grid-cols-[1fr]',\n ],\n notchClip: 'min-w-0 overflow-hidden',\n notchText: 'inline-block px-[4px]',\n indicator: [\n 'pointer-events-none absolute inset-x-0 bottom-0 h-px',\n 'bg-[var(--_md-comp-text-field-active-indicator-color)]',\n '[transition-property:background-color,height]',\n colourAndSizeTransition,\n 'group-data-[field-state=hover]/field:bg-on-surface',\n 'group-data-[field-state=focus]/field:h-[2px]',\n 'group-data-[field-state=focus]/field:bg-[var(--_md-comp-text-field-focus-active-indicator-color)]',\n 'group-data-[field-state=error]/field:bg-error',\n 'group-data-[field-state=error-hover]/field:bg-on-error-container',\n 'group-data-[field-state=error-focus]/field:h-[2px] group-data-[field-state=error-focus]/field:bg-error',\n 'group-data-[field-state=disabled]/field:bg-on-surface/38',\n // Forced colours paint backgrounds as Canvas, which hid the filled field's only edge:\n // the line takes system colours instead (2026-10-15).\n 'forced-colors:bg-[CanvasText]! forced-colors:forced-color-adjust-none',\n 'forced-colors:group-data-[field-state=focus]/field:bg-[Highlight]! forced-colors:group-data-[field-state=error-focus]/field:bg-[Highlight]!',\n 'forced-colors:group-data-[field-state=disabled]/field:bg-[GrayText]!',\n ],\n label: [\n 'pointer-events-none absolute top-[calc(16px+var(--md-sys-density,0)*2px)] max-w-[calc(100%-32px)] truncate',\n 'text-body-large text-[color:var(--_md-comp-text-field-label-text-color)]',\n '[transition-property:top,inset-inline-start,font-size,line-height,letter-spacing,color,opacity]',\n geometryTransition,\n 'group-data-[field-state=focus]/field:text-[color:var(--_md-comp-text-field-focus-label-text-color)]',\n 'group-data-[field-state=error]/field:text-error',\n 'group-data-[field-state=error-hover]/field:text-on-error-container',\n 'group-data-[field-state=error-focus]/field:text-error',\n 'group-data-[field-state=disabled]/field:text-on-surface/38',\n ],\n icon: [\n 'flex h-[min(48px,calc(56px+var(--md-sys-density,0)*4px))] w-[48px] shrink-0 items-center justify-center self-center',\n // Icon buttons here keep their size at every density: the box is a fixed 48px column\n // that density only shortens. One with its own `density` still follows it.\n '[&_:not([data-density])]:[--md-sys-density:0]',\n '[&>svg]:size-[24px]',\n 'group-data-[field-state=disabled]/field:text-on-surface/38',\n ],\n leadingIcon: 'text-[color:var(--_md-comp-text-field-leading-icon-color)]',\n trailingIcon: [\n 'text-[color:var(--_md-comp-text-field-trailing-icon-color)]',\n 'group-data-[field-state=error]/field:text-error',\n 'group-data-[field-state=error-focus]/field:text-error',\n 'group-data-[field-state=error-hover]/field:text-on-error-container',\n ],\n field: 'flex min-w-0 flex-1 items-center',\n // A hint or value longer than the field ends in an ellipsis, as the label does, not\n // mid-letter; the value only while the field isn't focused, since focused it scrolls\n // (2026-11-11). A text area wraps instead, so it has no effect there.\n input: [\n 'min-w-0 flex-1 bg-transparent p-0 text-body-large outline-none text-ellipsis',\n 'text-[color:var(--_md-comp-text-field-input-text-color)] caret-[color:var(--_md-comp-text-field-caret-color)]',\n 'placeholder:text-on-surface-variant placeholder:opacity-100 placeholder:text-ellipsis',\n 'disabled:cursor-default group-data-[field-state=disabled]/field:text-on-surface/38',\n 'group-data-invalid/field:caret-error',\n 'resize-none',\n ],\n affix: [\n 'text-body-large text-on-surface-variant whitespace-nowrap',\n 'group-data-[field-state=disabled]/field:text-on-surface/38',\n ],\n prefix: 'pe-[2px]',\n suffix: 'ps-[2px]',\n supporting: 'flex gap-[16px] px-[16px] pt-[4px]',\n // The supporting text's resting colour; the error message keeps its own.\n supportingTextColor: 'text-[color:var(--_md-comp-text-field-supporting-text-color)]',\n // Error colour whenever the field is invalid, with or without a message (Compose's\n // `ErrorSupportingColor`), so the supporting text reads as the error's explanation.\n supportingText: [\n 'min-w-0 flex-1',\n 'group-data-[field-state=error]/field:text-error',\n 'group-data-[field-state=error-hover]/field:text-error',\n 'group-data-[field-state=error-focus]/field:text-error',\n 'group-data-[field-state=disabled]/field:text-on-surface/38',\n ],\n counter: [\n 'ms-auto',\n 'group-data-[field-state=error]/field:text-error',\n 'group-data-[field-state=error-hover]/field:text-error',\n 'group-data-[field-state=error-focus]/field:text-error',\n 'group-data-[field-state=disabled]/field:text-on-surface/38',\n ],\n // The built-in error icon's own box; the trailing slot colours it.\n errorIcon: '[&>svg]:size-[24px]',\n required: 'text-inherit',\n },\n variants: {\n variant: {\n filled: {\n container: [\n 'rounded-t-[var(--_md-comp-text-field-container-shape)]',\n 'group-data-[field-state=disabled]/field:bg-on-surface/4',\n ],\n outline: 'hidden',\n },\n outlined: {\n container: 'rounded-[var(--_md-comp-text-field-container-shape)]',\n indicator: 'hidden',\n // OutlinedTextFieldTokens.HoverLabelColor (filled keeps on-surface-variant).\n label: 'group-data-[field-state=hover]/field:text-on-surface',\n },\n },\n // With a label the outline starts 8px higher, so its border runs through the middle of\n // the 16px notch legend and lands on the container's top edge.\n hasLabel: { true: { outline: '-top-[8px]' }, false: { outline: 'top-0' } },\n hasLeadingIcon: {\n true: { field: 'ps-[4px]', label: 'start-[52px]' },\n false: { field: 'ps-[16px]', label: 'start-[16px]' },\n },\n hasTrailingIcon: {\n true: { field: 'pe-[4px]' },\n false: { field: 'pe-[16px]' },\n },\n multiline: {\n true: { field: 'items-start', input: 'block' },\n false: {},\n },\n // A text area keeps its height and scrolls (M3: \"a fixed height and scroll vertically\").\n fixedHeight: {\n true: { input: 'overflow-y-auto' },\n false: {},\n },\n // M3: supporting text can be \"visible only on focus\". It keeps its space, so nothing\n // jumps, and stays in the accessible description.\n supportingTextOnFocus: {\n true: {\n supportingText: [\n 'opacity-0 transition-opacity group-data-focused/field:opacity-100',\n effectsTransition,\n ],\n },\n false: {},\n },\n },\n compoundVariants: [\n /*\n * Input padding: filled fields with a label leave room above for the floated label.\n * Below 52px (density -2 and -3) there is no room for it: as MDC's text field\n * (`$minimum-height-for-filled-label: 52px`), the floated label is hidden and the value\n * is centred like a field without a label. The resting label still shows in the empty\n * field, and still names the input. `--m3-field-label-room` is 1 down to density -1\n * and 0 from -2 (a clamped step of the density, so CSS alone switches the layout).\n */\n {\n variant: 'filled',\n hasLabel: true,\n class: {\n container: '[--m3-field-label-room:clamp(0,calc((var(--md-sys-density,0)+1.5)*100),1)]',\n field: [\n 'pt-[calc(16px+var(--md-sys-density,0)*2px+8px*var(--m3-field-label-room))]',\n 'pb-[calc(16px+var(--md-sys-density,0)*2px-8px*var(--m3-field-label-room))]',\n ],\n },\n },\n {\n variant: 'filled',\n hasLabel: false,\n class: { field: 'py-[calc(16px+var(--md-sys-density,0)*2px)]' },\n },\n { variant: 'outlined', class: { field: 'py-[calc(16px+var(--md-sys-density,0)*2px)]' } },\n // Floated label: filled → 8px from the top (less 2px per density step); outlined → centred on the border, at 16px.\n {\n variant: 'filled',\n hasLabel: true,\n class: {\n label: [\n 'group-data-floated/field:top-[calc(8px+var(--md-sys-density,0)*2px)] group-data-floated/field:text-body-small',\n 'group-has-[:autofill]/field:top-[calc(8px+var(--md-sys-density,0)*2px)] group-has-[:autofill]/field:text-body-small',\n // Hidden once floated below 52px (see the padding above).\n 'group-data-floated/field:opacity-[var(--m3-field-label-room)] group-has-[:autofill]/field:opacity-[var(--m3-field-label-room)]',\n ],\n },\n },\n {\n variant: 'outlined',\n hasLabel: true,\n class: {\n label: [\n 'group-data-floated/field:top-[-8px] group-data-floated/field:start-[16px] group-data-floated/field:text-body-small',\n 'group-has-[:autofill]/field:top-[-8px] group-has-[:autofill]/field:start-[16px] group-has-[:autofill]/field:text-body-small',\n ],\n },\n },\n // With a label, the placeholder and prefix/suffix only show once the label floats.\n {\n hasLabel: true,\n class: {\n // The placeholder fades in as the label floats (Compose fades it on FastEffects).\n input: [\n 'placeholder:opacity-0 group-data-floated/field:placeholder:opacity-100',\n 'placeholder:transition-opacity placeholder:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)] placeholder:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]',\n ],\n affix: [\n 'opacity-0 transition-opacity group-data-floated/field:opacity-100 group-has-[:autofill]/field:opacity-100',\n effectsTransition,\n ],\n },\n },\n ],\n defaultVariants: {\n variant: 'filled',\n hasLabel: true,\n hasLeadingIcon: false,\n hasTrailingIcon: false,\n multiline: false,\n fixedHeight: false,\n supportingTextOnFocus: false,\n },\n});\n\n/**\n * The variants of {@link textFieldStyles}, for building on its styles. They are the recipe's keys, not\n * component props: their names (`hasIcon`, `isOpen`…) follow the classes and may change.\n */\nexport type TextFieldStyleProps = VariantProps<typeof textFieldStyles>;\nexport type TextFieldVariant = NonNullable<TextFieldStyleProps['variant']>;\n"],"mappings":";;AA+BA,MAAM,qBACJ;AAMF,MAAM,0BACJ;AAEF,MAAM,oBACJ;AAEF,MAAM,oBACJ;;AAGF,MAAa,kBAAkB,GAAG;CAChC,OAAO;EACL,MAAM;EAEN,WAAW;GACT;GAEA;GACA;GACA;EACF;EACA,SAAS;GACP;GACA;GACA;GAMA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF;EAIA,OAAO;GACL;GACA;GACA;GACA;EACF;EACA,WAAW;EACX,WAAW;EACX,WAAW;GACT;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GAGA;GACA;GACA;EACF;EACA,OAAO;GACL;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF;EACA,MAAM;GACJ;GAGA;GACA;GACA;EACF;EACA,aAAa;EACb,cAAc;GACZ;GACA;GACA;GACA;EACF;EACA,OAAO;EAIP,OAAO;GACL;GACA;GACA;GACA;GACA;GACA;EACF;EACA,OAAO,CACL,6DACA,4DACF;EACA,QAAQ;EACR,QAAQ;EACR,YAAY;EAEZ,qBAAqB;EAGrB,gBAAgB;GACd;GACA;GACA;GACA;GACA;EACF;EACA,SAAS;GACP;GACA;GACA;GACA;GACA;EACF;EAEA,WAAW;EACX,UAAU;CACZ;CACA,UAAU;EACR,SAAS;GACP,QAAQ;IACN,WAAW,CACT,0DACA,yDACF;IACA,SAAS;GACX;GACA,UAAU;IACR,WAAW;IACX,WAAW;IAEX,OAAO;GACT;EACF;EAGA,UAAU;GAAE,MAAM,EAAE,SAAS,aAAa;GAAG,OAAO,EAAE,SAAS,QAAQ;EAAE;EACzE,gBAAgB;GACd,MAAM;IAAE,OAAO;IAAY,OAAO;GAAe;GACjD,OAAO;IAAE,OAAO;IAAa,OAAO;GAAe;EACrD;EACA,iBAAiB;GACf,MAAM,EAAE,OAAO,WAAW;GAC1B,OAAO,EAAE,OAAO,YAAY;EAC9B;EACA,WAAW;GACT,MAAM;IAAE,OAAO;IAAe,OAAO;GAAQ;GAC7C,OAAO,CAAC;EACV;EAEA,aAAa;GACX,MAAM,EAAE,OAAO,kBAAkB;GACjC,OAAO,CAAC;EACV;EAGA,uBAAuB;GACrB,MAAM,EACJ,gBAAgB,CACd,qEACA,iBACF,EACF;GACA,OAAO,CAAC;EACV;CACF;CACA,kBAAkB;EAShB;GACE,SAAS;GACT,UAAU;GACV,OAAO;IACL,WAAW;IACX,OAAO,CACL,8EACA,4EACF;GACF;EACF;EACA;GACE,SAAS;GACT,UAAU;GACV,OAAO,EAAE,OAAO,8CAA8C;EAChE;EACA;GAAE,SAAS;GAAY,OAAO,EAAE,OAAO,8CAA8C;EAAE;EAEvF;GACE,SAAS;GACT,UAAU;GACV,OAAO,EACL,OAAO;IACL;IACA;IAEA;GACF,EACF;EACF;EACA;GACE,SAAS;GACT,UAAU;GACV,OAAO,EACL,OAAO,CACL,sHACA,6HACF,EACF;EACF;EAEA;GACE,UAAU;GACV,OAAO;IAEL,OAAO,CACL,0EACA,6MACF;IACA,OAAO,CACL,6GACA,iBACF;GACF;EACF;CACF;CACA,iBAAiB;EACf,SAAS;EACT,UAAU;EACV,gBAAgB;EAChB,iBAAiB;EACjB,WAAW;EACX,aAAa;EACb,uBAAuB;CACzB;AACF,CAAC"}
@@ -83,7 +83,7 @@ const floatingToolbarStyles = tv({
83
83
  root: "min-h-[64px] flex-row",
84
84
  items: "flex-row gap-[8px] ps-[4px] pe-[4px]",
85
85
  collapsibleItems: "w-max shrink-0",
86
- collapse: "data-collapsed:w-0",
86
+ collapse: "data-collapsed:w-0 data-collapsed:overflow-x-clip",
87
87
  clip: "w-full flex-row [clip-path:inset(-12px_-2px)]",
88
88
  toolbarSlot: "h-[64px] w-(--m3-toolbar-size) flex-row",
89
89
  surface: "h-[64px] w-(--m3-toolbar-size) shrink-0 data-collapsed:w-0",
@@ -94,7 +94,7 @@ const floatingToolbarStyles = tv({
94
94
  root: "min-w-[64px] flex-col",
95
95
  items: "flex-col gap-[8px] pt-[4px] pb-[4px]",
96
96
  collapsibleItems: "h-max shrink-0",
97
- collapse: "flex-col data-collapsed:h-0",
97
+ collapse: "flex-col data-collapsed:h-0 data-collapsed:overflow-y-clip",
98
98
  clip: "h-full flex-col [clip-path:inset(-2px_-12px)]",
99
99
  toolbarSlot: "h-(--m3-toolbar-size) w-[64px] flex-col",
100
100
  surface: "h-(--m3-toolbar-size) w-[64px] shrink-0 data-collapsed:h-0",