@seekalfred/primitives 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/README.md +664 -9
  2. package/dist/accordion/accordion.js +10 -4
  3. package/dist/accordion/accordion.js.map +1 -1
  4. package/dist/action-button/action-button.d.ts +50 -0
  5. package/dist/action-button/action-button.js +159 -0
  6. package/dist/action-button/action-button.js.map +1 -0
  7. package/dist/action-button/index.d.ts +2 -0
  8. package/dist/action-button/index.js +2 -0
  9. package/dist/alert/alert.d.ts +15 -1
  10. package/dist/alert/alert.js +116 -20
  11. package/dist/alert/alert.js.map +1 -1
  12. package/dist/avatar/avatar.d.ts +6 -0
  13. package/dist/avatar/avatar.js +47 -16
  14. package/dist/avatar/avatar.js.map +1 -1
  15. package/dist/badge/badge.d.ts +11 -4
  16. package/dist/badge/badge.js +35 -5
  17. package/dist/badge/badge.js.map +1 -1
  18. package/dist/breadcrumb/breadcrumb.d.ts +0 -27
  19. package/dist/breadcrumb/breadcrumb.js +20 -6
  20. package/dist/breadcrumb/breadcrumb.js.map +1 -1
  21. package/dist/button/button.d.ts +35 -22
  22. package/dist/button/button.js +177 -34
  23. package/dist/button/button.js.map +1 -1
  24. package/dist/button-group/button-group.d.ts +76 -0
  25. package/dist/button-group/button-group.js +403 -0
  26. package/dist/button-group/button-group.js.map +1 -0
  27. package/dist/button-group/index.d.ts +2 -0
  28. package/dist/button-group/index.js +2 -0
  29. package/dist/calendar/calendar.d.ts +1 -1
  30. package/dist/calendar/calendar.js +257 -30
  31. package/dist/calendar/calendar.js.map +1 -1
  32. package/dist/card/card.d.ts +9 -2
  33. package/dist/card/card.js +15 -4
  34. package/dist/card/card.js.map +1 -1
  35. package/dist/checkbox/checkbox.d.ts +0 -7
  36. package/dist/checkbox/checkbox.js +24 -27
  37. package/dist/checkbox/checkbox.js.map +1 -1
  38. package/dist/confirm-morph/confirm-morph.d.ts +85 -0
  39. package/dist/confirm-morph/confirm-morph.js +652 -0
  40. package/dist/confirm-morph/confirm-morph.js.map +1 -0
  41. package/dist/confirm-morph/index.d.ts +2 -0
  42. package/dist/confirm-morph/index.js +2 -0
  43. package/dist/copy-button/copy-button.d.ts +32 -0
  44. package/dist/copy-button/copy-button.js +134 -0
  45. package/dist/copy-button/copy-button.js.map +1 -0
  46. package/dist/copy-button/index.d.ts +2 -0
  47. package/dist/copy-button/index.js +2 -0
  48. package/dist/date-picker/date-picker.d.ts +51 -0
  49. package/dist/date-picker/date-picker.js +241 -0
  50. package/dist/date-picker/date-picker.js.map +1 -0
  51. package/dist/date-picker/date-range-picker.d.ts +78 -0
  52. package/dist/date-picker/date-range-picker.js +1015 -0
  53. package/dist/date-picker/date-range-picker.js.map +1 -0
  54. package/dist/date-picker/index.d.ts +4 -0
  55. package/dist/date-picker/index.js +4 -0
  56. package/dist/date-picker/picker-parts.d.ts +46 -0
  57. package/dist/date-picker/picker-parts.js +135 -0
  58. package/dist/date-picker/picker-parts.js.map +1 -0
  59. package/dist/dialog/dialog.d.ts +10 -14
  60. package/dist/dialog/dialog.js +39 -11
  61. package/dist/dialog/dialog.js.map +1 -1
  62. package/dist/empty-state/empty-state.d.ts +44 -0
  63. package/dist/empty-state/empty-state.js +83 -0
  64. package/dist/empty-state/empty-state.js.map +1 -0
  65. package/dist/empty-state/index.d.ts +2 -0
  66. package/dist/empty-state/index.js +2 -0
  67. package/dist/expanding-button-group/expanding-button-group.d.ts +54 -0
  68. package/dist/expanding-button-group/expanding-button-group.js +350 -0
  69. package/dist/expanding-button-group/expanding-button-group.js.map +1 -0
  70. package/dist/expanding-button-group/index.d.ts +2 -0
  71. package/dist/expanding-button-group/index.js +2 -0
  72. package/dist/file-drop-zone/file-drop-zone.d.ts +71 -0
  73. package/dist/file-drop-zone/file-drop-zone.js +116 -0
  74. package/dist/file-drop-zone/file-drop-zone.js.map +1 -0
  75. package/dist/file-drop-zone/index.d.ts +2 -0
  76. package/dist/file-drop-zone/index.js +2 -0
  77. package/dist/filter-bar/filter-bar.d.ts +121 -0
  78. package/dist/filter-bar/filter-bar.js +300 -0
  79. package/dist/filter-bar/filter-bar.js.map +1 -0
  80. package/dist/filter-bar/index.d.ts +2 -0
  81. package/dist/filter-bar/index.js +2 -0
  82. package/dist/floating-button-group/floating-button-group.d.ts +61 -0
  83. package/dist/floating-button-group/floating-button-group.js +340 -0
  84. package/dist/floating-button-group/floating-button-group.js.map +1 -0
  85. package/dist/floating-button-group/index.d.ts +2 -0
  86. package/dist/floating-button-group/index.js +2 -0
  87. package/dist/icon-morph/icon-morph.d.ts +55 -0
  88. package/dist/icon-morph/icon-morph.js +331 -0
  89. package/dist/icon-morph/icon-morph.js.map +1 -0
  90. package/dist/icon-morph/index.d.ts +2 -0
  91. package/dist/icon-morph/index.js +2 -0
  92. package/dist/index.d.ts +24 -2
  93. package/dist/index.js +29 -7
  94. package/dist/input/input.d.ts +22 -0
  95. package/dist/input/input.js +36 -12
  96. package/dist/input/input.js.map +1 -1
  97. package/dist/lib/count-to.js +113 -0
  98. package/dist/lib/count-to.js.map +1 -0
  99. package/dist/lib/eye-morph.js +62 -0
  100. package/dist/lib/eye-morph.js.map +1 -0
  101. package/dist/lib/field-message.js +336 -0
  102. package/dist/lib/field-message.js.map +1 -0
  103. package/dist/lib/field-shell.js +81 -0
  104. package/dist/lib/field-shell.js.map +1 -0
  105. package/dist/lib/field-surface.js +4 -2
  106. package/dist/lib/field-surface.js.map +1 -1
  107. package/dist/lib/flip.js +80 -0
  108. package/dist/lib/flip.js.map +1 -0
  109. package/dist/lib/height-follow.js +59 -0
  110. package/dist/lib/height-follow.js.map +1 -0
  111. package/dist/lib/icon-swap.js +47 -0
  112. package/dist/lib/icon-swap.js.map +1 -0
  113. package/dist/lib/leaving-list.js +57 -0
  114. package/dist/lib/leaving-list.js.map +1 -0
  115. package/dist/lib/merge-refs.js +3 -3
  116. package/dist/lib/merge-refs.js.map +1 -1
  117. package/dist/lib/morph-text.js +128 -0
  118. package/dist/lib/morph-text.js.map +1 -0
  119. package/dist/lib/motion.js +162 -0
  120. package/dist/lib/motion.js.map +1 -0
  121. package/dist/lib/pausable-timeout.js +67 -0
  122. package/dist/lib/pausable-timeout.js.map +1 -0
  123. package/dist/lib/rolling-value.js +31 -0
  124. package/dist/lib/rolling-value.js.map +1 -0
  125. package/dist/lib/spring-progress.js +114 -0
  126. package/dist/lib/spring-progress.js.map +1 -0
  127. package/dist/lib/svg-id.js +10 -0
  128. package/dist/lib/svg-id.js.map +1 -0
  129. package/dist/lib/swap-text.js +77 -0
  130. package/dist/lib/swap-text.js.map +1 -0
  131. package/dist/lib/use-copy-feedback.js +36 -0
  132. package/dist/lib/use-copy-feedback.js.map +1 -0
  133. package/dist/page-header/index.d.ts +2 -0
  134. package/dist/page-header/index.js +2 -0
  135. package/dist/page-header/page-header.d.ts +75 -0
  136. package/dist/page-header/page-header.js +83 -0
  137. package/dist/page-header/page-header.js.map +1 -0
  138. package/dist/password-field/index.d.ts +2 -0
  139. package/dist/password-field/index.js +2 -0
  140. package/dist/password-field/password-field.d.ts +37 -0
  141. package/dist/password-field/password-field.js +101 -0
  142. package/dist/password-field/password-field.js.map +1 -0
  143. package/dist/password-strength/index.d.ts +2 -0
  144. package/dist/password-strength/index.js +2 -0
  145. package/dist/password-strength/password-strength.d.ts +127 -0
  146. package/dist/password-strength/password-strength.js +455 -0
  147. package/dist/password-strength/password-strength.js.map +1 -0
  148. package/dist/popover/popover.d.ts +8 -0
  149. package/dist/popover/popover.js +3 -3
  150. package/dist/popover/popover.js.map +1 -1
  151. package/dist/progress/progress.js +46 -5
  152. package/dist/progress/progress.js.map +1 -1
  153. package/dist/radio-group/radio-group.js +7 -6
  154. package/dist/radio-group/radio-group.js.map +1 -1
  155. package/dist/scroll-area/scroll-area.d.ts +10 -0
  156. package/dist/scroll-area/scroll-area.js +25 -3
  157. package/dist/scroll-area/scroll-area.js.map +1 -1
  158. package/dist/search-field/index.d.ts +2 -0
  159. package/dist/search-field/index.js +2 -0
  160. package/dist/search-field/search-field.d.ts +26 -0
  161. package/dist/search-field/search-field.js +155 -0
  162. package/dist/search-field/search-field.js.map +1 -0
  163. package/dist/select/select.js +79 -21
  164. package/dist/select/select.js.map +1 -1
  165. package/dist/sheet/sheet.js +2 -1
  166. package/dist/sheet/sheet.js.map +1 -1
  167. package/dist/side-nav/index.d.ts +2 -0
  168. package/dist/side-nav/index.js +2 -0
  169. package/dist/side-nav/side-nav.d.ts +72 -0
  170. package/dist/side-nav/side-nav.js +142 -0
  171. package/dist/side-nav/side-nav.js.map +1 -0
  172. package/dist/skeleton/skeleton.d.ts +8 -2
  173. package/dist/skeleton/skeleton.js +6 -2
  174. package/dist/skeleton/skeleton.js.map +1 -1
  175. package/dist/slider/slider.js +61 -7
  176. package/dist/slider/slider.js.map +1 -1
  177. package/dist/split-button/index.d.ts +2 -0
  178. package/dist/split-button/index.js +2 -0
  179. package/dist/split-button/split-button.d.ts +46 -0
  180. package/dist/split-button/split-button.js +122 -0
  181. package/dist/split-button/split-button.js.map +1 -0
  182. package/dist/stat-card/index.d.ts +2 -0
  183. package/dist/stat-card/index.js +2 -0
  184. package/dist/stat-card/stat-card.d.ts +81 -0
  185. package/dist/stat-card/stat-card.js +152 -0
  186. package/dist/stat-card/stat-card.js.map +1 -0
  187. package/dist/styles.css +1 -1
  188. package/dist/switch/switch.js +9 -8
  189. package/dist/switch/switch.js.map +1 -1
  190. package/dist/table/index.d.ts +2 -2
  191. package/dist/table/index.js +2 -2
  192. package/dist/table/table.d.ts +36 -5
  193. package/dist/table/table.js +112 -11
  194. package/dist/table/table.js.map +1 -1
  195. package/dist/tabs/tabs.js +142 -6
  196. package/dist/tabs/tabs.js.map +1 -1
  197. package/dist/textarea/textarea.d.ts +23 -0
  198. package/dist/textarea/textarea.js +24 -2
  199. package/dist/textarea/textarea.js.map +1 -1
  200. package/dist/theme-switch/index.d.ts +2 -0
  201. package/dist/theme-switch/index.js +2 -0
  202. package/dist/theme-switch/theme-switch.d.ts +37 -0
  203. package/dist/theme-switch/theme-switch.js +123 -0
  204. package/dist/theme-switch/theme-switch.js.map +1 -0
  205. package/dist/time-picker/index.d.ts +2 -0
  206. package/dist/time-picker/index.js +2 -0
  207. package/dist/time-picker/time-picker.d.ts +54 -0
  208. package/dist/time-picker/time-picker.js +201 -0
  209. package/dist/time-picker/time-picker.js.map +1 -0
  210. package/dist/toast/toast.js +58 -38
  211. package/dist/toast/toast.js.map +1 -1
  212. package/dist/tooltip/tooltip.d.ts +1 -1
  213. package/dist/tooltip/tooltip.js +63 -14
  214. package/dist/tooltip/tooltip.js.map +1 -1
  215. package/package.json +83 -3
@@ -0,0 +1 @@
1
+ {"version":3,"file":"password-strength.js","names":["Label","AriaGroup","AriaInput","AriaButton"],"sources":["../../src/password-strength/password-strength.tsx"],"sourcesContent":["\"use client\";\n\nimport { forwardRef, useCallback, useEffect, useRef, useState, type CSSProperties } from \"react\";\nimport {\n Button as AriaButton,\n Group as AriaGroup,\n Input as AriaInput,\n type InputProps as AriaInputProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { EyeMorph } from \"../lib/eye-morph.js\";\nimport { FieldMessage } from \"../lib/field-message.js\";\nimport { useFieldWiring } from \"../lib/field-shell.js\";\nimport { fieldSurfaceVariants } from \"../lib/field-surface.js\";\nimport {\n prefersReducedMotion,\n useExitBackstop,\n useIsomorphicLayoutEffect,\n useLeavingItems,\n} from \"../lib/motion.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\nimport { Label } from \"../label/label.js\";\n\nexport interface PasswordRule {\n id: string;\n label: string;\n test: (password: string) => boolean;\n /** Characters still missing. Shown as a rolling count beside the rule while it is unmet. */\n remaining?: (password: string) => number;\n}\n\nexport interface PasswordStrengthResult {\n level: 0 | 1 | 2 | 3 | 4;\n label: string;\n met: string[];\n /** True while the password is shorter than the short length, so the meter holds at its first step. */\n short?: boolean;\n}\n\n/** The strength words, one per meter step. Override them to translate the meter. */\nexport interface PasswordStrengthLevels {\n tooShort: string;\n weak: string;\n fair: string;\n good: string;\n strong: string;\n}\n\n/** Every user-facing string in PasswordStrength. All have English defaults; pass only the ones you change. */\nexport interface PasswordStrengthMessages {\n levels: PasswordStrengthLevels;\n /** Name of the show password toggle. It is a pressed toggle (aria-pressed), so the name stays the same in both states. */\n showPassword: string;\n /** Accessible name of the strength meter. */\n meterLabel: string;\n /** Meter value text while the field is empty. */\n noPassword: string;\n /** Accessible name of the requirements list. */\n rulesLabel: string;\n /** Read after a rule's label for screen readers, including any leading punctuation. */\n ruleMet: string;\n ruleNotMet: string;\n /** Words around the rolling count beside an unmet rule, as in \"4 more\". */\n remainingPrefix: string;\n remainingSuffix: string;\n /** The polite live-region summary read after each change. */\n summary: (result: { label: string; met: number; total: number }) => string;\n}\n\nexport const defaultPasswordStrengthMessages: PasswordStrengthMessages = {\n levels: { tooShort: \"Too short\", weak: \"Weak\", fair: \"Fair\", good: \"Good\", strong: \"Strong\" },\n showPassword: \"Show password\",\n meterLabel: \"Password strength\",\n noPassword: \"No password yet\",\n rulesLabel: \"Password requirements\",\n ruleMet: \", met\",\n ruleNotMet: \", not met\",\n remainingPrefix: \"\",\n remainingSuffix: \"more\",\n summary: ({ label, met, total }) => `Strength: ${label}. ${met} of ${total} requirements met.`,\n};\n\n/** Partial messages, with partial strength words, for the `messages` prop. */\nexport type PasswordStrengthMessagesInput = Partial<Omit<PasswordStrengthMessages, \"levels\">> & {\n levels?: Partial<PasswordStrengthLevels>;\n};\n\n/**\n * A new-password field that shows how strong the password is while it is typed:\n * four segments fill and change tone, each rule checks off with a drawn tick, and\n * the strength word swaps in place. Use it when creating or changing a password;\n * use `PasswordField` for sign in. Scoring happens on the device, nothing is sent\n * anywhere.\n *\n * Native input props apply to the input (`className` too); `disabled` also\n * disables the toggle, and `aria-invalid` gives the box its invalid border.\n *\n * @example\n * <PasswordStrength label=\"New password\" onValueChange={(value, strength) => save(value, strength.level)} />\n */\nexport interface PasswordStrengthProps extends Omit<\n AriaInputProps,\n \"className\" | \"style\" | \"size\" | \"type\" | \"value\" | \"defaultValue\" | \"children\"\n> {\n /** Extra classes for the input. Merged last, so they win conflicts with ours. */\n className?: string;\n /** The visible label, tied to the input with `htmlFor`. */\n label: string;\n value?: string;\n defaultValue?: string;\n onValueChange?: (value: string, strength: PasswordStrengthResult) => void;\n /** Rules to check. Strength is the share of rules met, spread over four steps. */\n rules?: PasswordRule[];\n /**\n * Error copy tied to the field. It is an alert, opens on a spring and swaps as a\n * whole when it changes; the field shakes once each time a new error appears.\n */\n error?: string;\n revealed?: boolean;\n onRevealedChange?: (revealed: boolean) => void;\n /** Translated or reworded copy. Every string has an English default; see PasswordStrengthMessages. */\n messages?: PasswordStrengthMessagesInput;\n /** Below this many characters the meter holds at its first step and reads \"Too short\". Defaults to 8. */\n shortLength?: number;\n /** How characters are counted for shortLength. Defaults to code points, so an emoji counts once; pass `password => password.length` to count UTF-16 units like a server might. */\n countCharacters?: (password: string) => number;\n}\n\n/** Counts code points, so an emoji or other astral character counts as one. */\nexport const countCodePoints = (password: string) => Array.from(password).length;\n\n/** Labels for the built-in rules. `length` receives the minimum length. */\nexport interface PasswordRuleLabels {\n length: (minLength: number) => string;\n case: string;\n number: string;\n symbol: string;\n}\n\nexport interface PasswordRuleOptions {\n /** Characters the length rule asks for. Defaults to 12. */\n minLength?: number;\n /** Translated or reworded rule labels. */\n labels?: Partial<PasswordRuleLabels>;\n /** How the length rule counts characters. Defaults to code points. */\n countCharacters?: (password: string) => number;\n}\n\nconst defaultRuleLabels: PasswordRuleLabels = {\n length: (minLength) => `At least ${minLength} characters`,\n case: \"Upper and lowercase letters\",\n number: \"At least one number\",\n symbol: \"At least one symbol\",\n};\n\n/** The built-in rules (length, mixed case, number, symbol) with your own minimum length, labels and character count. */\nexport function createPasswordRules({\n minLength = 12,\n labels,\n countCharacters = countCodePoints,\n}: PasswordRuleOptions = {}): PasswordRule[] {\n const copy = { ...defaultRuleLabels, ...labels };\n return [\n {\n id: \"length\",\n label: copy.length(minLength),\n test: (password) => countCharacters(password) >= minLength,\n remaining: (password) => Math.max(0, minLength - countCharacters(password)),\n },\n {\n id: \"case\",\n label: copy.case,\n test: (password) => /\\p{Ll}/u.test(password) && /\\p{Lu}/u.test(password),\n },\n { id: \"number\", label: copy.number, test: (password) => /\\p{N}/u.test(password) },\n { id: \"symbol\", label: copy.symbol, test: (password) => /[^\\p{L}\\p{N}\\s]/u.test(password) },\n ];\n}\n\nexport const defaultPasswordRules: PasswordRule[] = createPasswordRules();\n\nexport interface EstimateStrengthOptions {\n /** Below this many characters a password stays at the first step, however varied it is. Defaults to 8. */\n shortLength?: number;\n /** Strength words for the result's label. */\n levels?: Partial<PasswordStrengthLevels>;\n /** How characters are counted for shortLength. Defaults to code points. */\n countCharacters?: (password: string) => number;\n}\n\n/** Scores a password on the device: one step per share of rules met, capped at the first step while it is shorter than the short length (8 by default). */\nexport function estimateStrength(\n password: string,\n rules: PasswordRule[] = defaultPasswordRules,\n { shortLength = 8, levels, countCharacters = countCodePoints }: EstimateStrengthOptions = {},\n): PasswordStrengthResult {\n if (!password) return { level: 0, label: \"\", met: [] };\n const words = { ...defaultPasswordStrengthMessages.levels, ...levels };\n const met = rules.filter((rule) => rule.test(password)).map((rule) => rule.id);\n if (countCharacters(password) < shortLength)\n return { level: 1, label: words.tooShort, met, short: true };\n const level = Math.min(\n 4,\n Math.max(1, Math.round((met.length / Math.max(rules.length, 1)) * 4)),\n ) as 1 | 2 | 3 | 4;\n return {\n level,\n label: [words.weak, words.fair, words.good, words.strong][level - 1] ?? words.weak,\n met,\n };\n}\n\n/**\n * The field's box: a React Aria `Group` with the shared field surface (see\n * PasswordField for why a Group and for `data-focus-within`). Hover darkens the\n * border only to `muted-foreground`, as the reference does here, and a new error\n * shakes the box (two animation names, so each restart plays).\n */\nconst shellClass = cn(\n fieldSurfaceVariants(),\n \"flex items-center gap-1 h-(--density-control-h-md) ps-(--density-control-pad-md) field-toggle-shell [--field-toggle-size:calc(var(--spacing)*8.5)] [&:where([data-focus-within])]:border-foreground data-[hovered]:not-data-[disabled]:not-data-[focus-within]:not-data-[invalid]:border-muted-foreground motion-safe:data-[shake=a]:animate-field-shake-a motion-safe:data-[shake=b]:animate-field-shake-b\",\n);\n\nconst inputClass =\n \"min-w-0 flex-1 border-0 bg-transparent p-0 font-sans text-base text-foreground outline-none placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground data-[disabled]:cursor-not-allowed motion-safe:data-[reveal=shown]:animate-field-reveal-shown motion-safe:data-[reveal=hidden]:animate-field-reveal-hidden\";\n\nconst toggleClass =\n \"field-toggle-button grid size-(--field-toggle-size) flex-none cursor-pointer place-items-center rounded-md border-0 bg-transparent p-0 text-muted-foreground [--press-scale:.94] [-webkit-tap-highlight-color:transparent] outline-none data-[hovered]:bg-muted data-[hovered]:text-foreground aria-pressed:text-foreground data-[focus-visible]:outline-solid data-[focus-visible]:outline-(length:--focus-ring-width) data-[focus-visible]:outline-offset-(--focus-ring-offset) data-[focus-visible]:outline-(--color-ring) data-[disabled]:cursor-not-allowed\";\n\ninterface Place {\n /** Place value, so 10 to 9 lets the tens column close while the ones column turns. */\n key: number;\n digit: string;\n seq: number;\n}\n\nconst toPlaces = (value: number, seq: number, previous: Map<number, Place>): Place[] => {\n const digits = String(value).split(\"\");\n return digits.map((digit, index) => {\n const key = digits.length - 1 - index;\n return { key, digit, seq: previous.get(key)?.seq ?? seq };\n });\n};\n\nfunction LeavingDigit({\n id,\n rect,\n onDone,\n children,\n}: {\n id: number;\n rect: { left: number; top: number; width: number; height: number };\n onDone: (id: number) => void;\n children: string;\n}) {\n const remove = useCallback(() => onDone(id), [onDone, id]);\n useExitBackstop(true, remove);\n return (\n <span\n aria-hidden=\"true\"\n onAnimationEnd={(event) => {\n if (event.target === event.currentTarget) remove();\n }}\n style={rect}\n className=\"absolute motion-safe:animate-field-count-out motion-reduce:animate-fade-out\"\n >\n {children}\n </span>\n );\n}\n\nfunction PlaceColumn({\n place,\n direction,\n leaving,\n onDone,\n}: {\n place: Place;\n direction: number;\n leaving: boolean;\n onDone: (key: number) => void;\n}) {\n const digitRef = useRef<HTMLSpanElement>(null);\n const digits = useLeavingItems(place.digit, place.digit, () => digitRef.current);\n const done = useCallback(() => onDone(place.key), [onDone, place.key]);\n useExitBackstop(leaving, done, 900);\n useIsomorphicLayoutEffect(() => {\n if (leaving && prefersReducedMotion()) done();\n }, [leaving, done]);\n return (\n <span\n style={{ \"--roll\": direction } as CSSProperties}\n onAnimationEnd={(event) => {\n if (\n leaving &&\n event.target === event.currentTarget &&\n event.animationName === \"field-column-shrink\"\n )\n done();\n }}\n className={cn(\n \"field-number-column overflow-x-clip\",\n leaving\n ? \"motion-safe:animate-field-column-out\"\n : place.seq > 0 && \"motion-safe:animate-field-column-in\",\n )}\n >\n {digits.leaving.map((item) => (\n <LeavingDigit key={item.id} id={item.id} rect={item.rect} onDone={digits.remove}>\n {item.value}\n </LeavingDigit>\n ))}\n <span\n key={place.digit}\n ref={digitRef}\n className={\n digits.changed\n ? \"motion-safe:animate-field-count-in motion-reduce:animate-field-swap-fade-in\"\n : undefined\n }\n >\n {place.digit}\n </span>\n </span>\n );\n}\n\n/** The count beside an unmet rule. It rolls digit by digit: arriving from below when the count grew, from above when it fell, and a gained or lost digit opens or closes its column. */\nfunction RollingCount({ value }: { value: number }) {\n const [state, setState] = useState(() => ({\n value,\n direction: 1,\n seq: 0,\n places: toPlaces(value, 0, new Map<number, Place>()),\n gone: [] as Place[],\n }));\n let { places, gone, direction } = state;\n if (state.value !== value) {\n const seq = state.seq + 1;\n direction = value > state.value ? 1 : -1;\n places = toPlaces(value, seq, new Map(state.places.map((place) => [place.key, place])));\n const kept = new Set(places.map((place) => place.key));\n gone = [\n ...state.gone.filter((place) => !kept.has(place.key)),\n ...state.places.filter((place) => !kept.has(place.key)),\n ];\n setState({ value, direction, seq, places, gone });\n }\n const finish = useCallback(\n (key: number) =>\n setState((current) => ({\n ...current,\n gone: current.gone.filter((place) => place.key !== key),\n })),\n [],\n );\n const all = [\n ...places.map((place) => ({ place, leaving: false })),\n ...gone.map((place) => ({ place, leaving: true })),\n ].sort((a, b) => b.place.key - a.place.key);\n return (\n <span className=\"inline-flex tabular-nums\">\n {all.map(({ place, leaving }) => (\n <PlaceColumn\n key={place.key}\n place={place}\n direction={direction}\n leaving={leaving}\n onDone={finish}\n />\n ))}\n </span>\n );\n}\n\n/** One rule's tick: the tint grows under it while it draws from its short stroke; unchecking retracts it faster than it drew. */\nfunction RuleMark() {\n return (\n <span aria-hidden=\"true\" className=\"field-rule-mark\">\n <span className=\"field-rule-fill\" />\n <svg\n className=\"field-rule-glyph\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={1.75}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M4.75 8.25 7 10.5l4.25-4.75\" pathLength={1} className=\"field-rule-tick\" />\n </svg>\n </span>\n );\n}\n\n/**\n * A new-password input with a strength meter and a checklist (see\n * {@link PasswordStrengthProps}).\n */\nexport const PasswordStrength = forwardRef<HTMLInputElement, PasswordStrengthProps>(\n function PasswordStrength(\n {\n label,\n value: valueProp,\n defaultValue = \"\",\n onValueChange,\n onChange,\n rules = defaultPasswordRules,\n error,\n revealed: revealedProp,\n onRevealedChange,\n messages: messagesProp,\n shortLength,\n countCharacters,\n className,\n ...props\n },\n ref,\n ) {\n const messages: PasswordStrengthMessages = {\n ...defaultPasswordStrengthMessages,\n ...messagesProp,\n levels: { ...defaultPasswordStrengthMessages.levels, ...messagesProp?.levels },\n };\n const options: EstimateStrengthOptions = {\n ...(shortLength === undefined ? {} : { shortLength }),\n levels: messages.levels,\n ...(countCharacters === undefined ? {} : { countCharacters }),\n };\n const field = useFieldWiring({\n enabled: true,\n id: props.id,\n describedBy: props[\"aria-describedby\"],\n invalid: props[\"aria-invalid\"],\n description: undefined,\n error,\n });\n const rulesId = `${field.controlId}-rules`;\n const [internal, setInternal] = useState(defaultValue);\n const value = valueProp ?? internal;\n const [revealedInternal, setRevealedInternal] = useState(false);\n const revealed = revealedProp ?? revealedInternal;\n const strength = estimateStrength(value, rules, options);\n const met = new Set(strength.met);\n\n // The previous level and rule states tell which way the word moves and which segments and ticks join a staggered wave.\n const rank = strength.short ? 0.5 : strength.level;\n const metKey = strength.met.join(\" \");\n const [track, setTrack] = useState({\n rank,\n level: strength.level,\n previousLevel: strength.level,\n direction: 1,\n metKey,\n previousMet: metKey,\n });\n if (track.rank !== rank || track.metKey !== metKey) {\n setTrack({\n rank,\n level: strength.level,\n previousLevel: track.rank !== rank ? track.level : track.previousLevel,\n direction: track.rank === rank ? track.direction : rank > track.rank ? 1 : -1,\n metKey,\n previousMet: track.metKey !== metKey ? track.metKey : track.previousMet,\n });\n }\n const previousMet = new Set(track.previousMet.split(\" \").filter(Boolean));\n const changed = rules\n .filter((rule) => met.has(rule.id) !== previousMet.has(rule.id))\n .map((rule) => rule.id);\n\n // Only a change after mount resolves the text, so the field never blurs in on first paint.\n const [reveal, setReveal] = useState({ revealed, changed: false });\n if (reveal.revealed !== revealed) setReveal({ revealed, changed: true });\n\n // A new error shakes the box once. The animation name alternates, held in state, so each one restarts it.\n const lastError = useRef(error);\n const [shake, setShake] = useState<\"a\" | \"b\" | undefined>(undefined);\n useEffect(() => {\n const previous = lastError.current;\n lastError.current = error;\n if (!error || error === previous) return;\n setShake((current) => (current === \"a\" ? \"b\" : \"a\"));\n }, [error]);\n\n // The live region speaks only when the level or the count of met rules changes, not on every keystroke.\n const summary = strength.level\n ? messages.summary({\n label: strength.label,\n met: strength.met.length,\n total: rules.length,\n })\n : \"\";\n\n const disabled = Boolean(props.disabled);\n const ariaInvalid = props[\"aria-invalid\"];\n const invalid =\n Boolean(error) ||\n (ariaInvalid !== undefined && ariaInvalid !== false && ariaInvalid !== \"false\");\n const describedBy = [\n field.controlProps[\"aria-describedby\"] ?? props[\"aria-describedby\"],\n rulesId,\n ]\n .filter(Boolean)\n .join(\" \");\n\n function toggleReveal() {\n if (revealedProp === undefined) setRevealedInternal(!revealed);\n onRevealedChange?.(!revealed);\n }\n\n return (\n <div\n data-slot=\"password-strength\"\n data-level={strength.level}\n className=\"grid min-w-0 font-sans [--tone:var(--color-border)] data-[level=1]:[--tone:var(--color-destructive-strong)] data-[level=2]:[--tone:var(--color-warning-strong)] data-[level=3]:[--tone:var(--color-success-strong)] data-[level=4]:[--tone:var(--color-success-strong)]\"\n >\n <Label htmlFor={field.controlId} className=\"mb-1.5\">\n {label}\n </Label>\n <AriaGroup\n role=\"presentation\"\n isDisabled={disabled}\n isInvalid={invalid}\n data-shake={shake}\n data-slot=\"password-strength-shell\"\n className={shellClass}\n >\n <AriaInput\n autoComplete=\"new-password\"\n autoCapitalize=\"off\"\n autoCorrect=\"off\"\n spellCheck={false}\n {...props}\n ref={ref}\n {...field.controlProps}\n type={revealed ? \"text\" : \"password\"}\n value={value}\n onChange={(event) => {\n const next = event.target.value;\n if (valueProp === undefined) setInternal(next);\n onChange?.(event);\n onValueChange?.(next, estimateStrength(next, rules, options));\n }}\n aria-describedby={describedBy}\n data-slot=\"password-strength-input\"\n data-reveal={reveal.changed ? (revealed ? \"shown\" : \"hidden\") : undefined}\n className={cn(inputClass, className)}\n />\n <AriaButton\n type=\"button\"\n isDisabled={disabled}\n aria-label={messages.showPassword}\n aria-pressed={revealed}\n aria-controls={field.controlId}\n onPress={toggleReveal}\n className={toggleClass}\n >\n <EyeMorph slashed={revealed} slashPath=\"M3 3l18 18\" />\n </AriaButton>\n </AriaGroup>\n <FieldMessage id={field.errorId} text={error} tone=\"error\" copy=\"swap\" />\n <div className=\"mt-3 flex items-center gap-3\">\n <div\n role=\"meter\"\n aria-label={messages.meterLabel}\n aria-valuemin={0}\n aria-valuemax={4}\n aria-valuenow={strength.level}\n aria-valuetext={strength.level ? strength.label : messages.noPassword}\n className=\"grid min-w-0 flex-1 grid-cols-4 gap-1.25\"\n >\n {[0, 1, 2, 3].map((index) => {\n const on = index < strength.level;\n // A pasted password fills left to right; clearing empties right to left.\n const wave = on ? index - track.previousLevel : track.previousLevel - 1 - index;\n const delay = Math.max(0, wave) * (on ? 0.05 : 0.03);\n return (\n <span\n key={index}\n className=\"relative isolate h-1 overflow-hidden rounded-full bg-muted\"\n >\n <span\n data-on={on || undefined}\n style={{ \"--wave-delay\": `${delay}s` } as CSSProperties}\n className=\"field-meter-fill absolute inset-0 rounded-[inherit]\"\n />\n </span>\n );\n })}\n </div>\n <span\n aria-hidden=\"true\"\n className=\"field-strength-word text-sm font-medium text-foreground\"\n >\n <SwapText swapKey={strength.level ? strength.label : \"\"} dir={track.direction}>\n {strength.label}\n </SwapText>\n </span>\n </div>\n <ul\n id={rulesId}\n aria-label={messages.rulesLabel}\n className=\"mt-4 grid list-none gap-1.5 p-0\"\n >\n {rules.map((rule) => {\n const ok = met.has(rule.id);\n const remaining = value && !ok ? (rule.remaining?.(value) ?? 0) : 0;\n const order = changed.indexOf(rule.id);\n return (\n <li\n key={rule.id}\n data-met={ok || undefined}\n style={{ \"--delay\": `${order > 0 ? order * 0.05 : 0}s` } as CSSProperties}\n className=\"flex min-h-5 items-center gap-2.5 text-sm leading-(--leading-normal) text-muted-foreground data-[met]:text-foreground motion-safe:transition-colors motion-safe:duration-(--duration-standard) motion-safe:ease-settle\"\n >\n <RuleMark />\n <span className=\"min-w-0\">\n {rule.label}\n <span className=\"sr-only\">{ok ? messages.ruleMet : messages.ruleNotMet}</span>\n </span>\n <span\n aria-hidden=\"true\"\n className=\"relative ms-auto ps-2 text-xs whitespace-nowrap text-muted-foreground\"\n >\n <SwapText swapKey={remaining > 0 ? \"remaining\" : \"\"} dir={1}>\n <span className=\"field-count-text\">\n {messages.remainingPrefix ? `${messages.remainingPrefix} ` : null}\n <RollingCount value={remaining} />\n {messages.remainingSuffix ? ` ${messages.remainingSuffix}` : null}\n </span>\n </SwapText>\n </span>\n </li>\n );\n })}\n </ul>\n <span className=\"sr-only\" role=\"status\">\n {summary}\n </span>\n </div>\n );\n },\n);\n"],"mappings":"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"}
@@ -43,6 +43,14 @@ interface PopoverProps$1 extends Omit<PopoverProps, "className" | "style" | "chi
43
43
  * @default "bottom"
44
44
  */
45
45
  placement?: PopoverProps["placement"];
46
+ /**
47
+ * How it comes in: `rise` grows out of the trigger (offset toward it, a little
48
+ * small, then settles on a spring) and leaves quicker; `none` leaves the entrance
49
+ * to the caller, as a picker that animates its own popover does.
50
+ *
51
+ * @default "rise"
52
+ */
53
+ entrance?: "rise" | "none";
46
54
  }
47
55
  declare const Popover$1: import("react").ForwardRefExoticComponent<PopoverProps$1 & import("react").RefAttributes<HTMLElement>>;
48
56
  //#endregion
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { cn } from "../lib/cn.js";
2
+ import { cnAll } from "../lib/cn.js";
3
3
  import { cva } from "class-variance-authority";
4
4
  import { forwardRef } from "react";
5
5
  import { DialogTrigger as PopoverTrigger, Popover } from "react-aria-components";
@@ -31,14 +31,14 @@ const popoverVariants = cva([
31
31
  "box-border font-sans text-sm leading-(--leading-normal)",
32
32
  "outline-none"
33
33
  ]);
34
- const Popover$1 = forwardRef(function Popover$2({ children, className, placement = "bottom", offset = 4, ...props }, ref) {
34
+ const Popover$1 = forwardRef(function Popover$2({ children, className, placement = "bottom", offset = 4, entrance = "rise", ...props }, ref) {
35
35
  return /* @__PURE__ */ jsx(Popover, {
36
36
  ref,
37
37
  ...props,
38
38
  placement,
39
39
  offset,
40
40
  "data-slot": "popover",
41
- className: cn(popoverVariants(), className),
41
+ className: cnAll(popoverVariants(), entrance === "rise" && "popover-motion", className),
42
42
  children
43
43
  });
44
44
  });
@@ -1 +1 @@
1
- {"version":3,"file":"popover.js","names":["Popover","AriaPopover"],"sources":["../../src/popover/popover.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport {\n Popover as AriaPopover,\n type PopoverProps as AriaPopoverProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Popover, ported from the legacy shadcn/Radix component.\n *\n * Census: 2 files, 2 uses. `asChild` on the trigger twice, `className`\n * twice, `align=\"start\"` once. `PopoverAnchor` is exported by the legacy\n * and imported by nobody, so it is not ported; neither are `side`,\n * `sideOffset`, `alignOffset`, `open` / `onOpenChange`, `modal`,\n * `collisionPadding` or `avoidCollisions` -- zero call sites each. Both\n * existing uses are uncontrolled.\n *\n * IT GAINS AN ACCESSIBLE NAME. The legacy renders `role=\"dialog\"` with no\n * `aria-label` and no `aria-labelledby`, so both call sites announce an\n * unnamed dialog. React Aria labels the popover from its trigger\n * automatically once the two are paired, which is the only way to build\n * one here.\n *\n * The surface is `card`: in the legacy theme `--popover` is byte-identical\n * to `--card` in both themes, so the role the migration plan proposed\n * already exists under another name.\n */\nexport const popoverVariants = cva([\n // w-72 (288px) is the legacy default width, as a spacing-scale step.\n \"w-72 rounded-md border-(length:--border-width) border-border p-4\",\n \"bg-card text-card-foreground shadow-floating\",\n // The reset in @seekalfred/styles is OPT-IN, so a consumer may load\n // none: an unstyled document renders text in a serif, and content-box\n // would add the 16px padding on top of the width.\n \"box-border font-sans text-sm leading-(--leading-normal)\",\n // React Aria gives the popover tabindex=-1 and focuses it on open, so it\n // can take a focus ring it does not want.\n \"outline-none\",\n]);\n\n/**\n * Content anchored to a trigger.\n *\n * @example\n * <PopoverTrigger>\n * <Button>Pick a date</Button>\n * <Popover placement=\"bottom start\">...</Popover>\n * </PopoverTrigger>\n */\nexport interface PopoverProps\n extends Omit<AriaPopoverProps, \"className\" | \"style\" | \"children\"> {\n children?: ReactNode;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n /**\n * Where it opens, as React Aria's single `placement` string. The legacy\n * split this across `side` and `align`; its one non-default call site is\n * `align=\"start\"`, which is `\"bottom start\"` here.\n *\n * @default \"bottom\"\n */\n placement?: AriaPopoverProps[\"placement\"];\n}\n\nexport const Popover = forwardRef<HTMLElement, PopoverProps>(function Popover(\n { children, className, placement = \"bottom\", offset = 4, ...props },\n ref,\n) {\n return (\n <AriaPopover\n ref={ref}\n {...props}\n placement={placement}\n offset={offset}\n data-slot=\"popover\"\n className={cn(popoverVariants(), className)}\n >\n {/*\n No inner Dialog. Wrapping the content in one is the common React\n Aria idiom and it was in the first draft here -- but deleting it\n broke no assertion, so it was doing nothing: a Popover under a\n DialogTrigger already gets role=\"dialog\", a focus scope, and\n aria-labelledby pointing at the trigger. The wrapper was one more\n element and one more className to explain.\n */}\n {children}\n </AriaPopover>\n );\n});\n\n/**\n * Pairs a trigger with a popover and owns the open state.\n *\n * React Aria's `DialogTrigger`, renamed for the migration: it takes the\n * trigger element directly as its first child, which is what `asChild` was\n * doing on both legacy call sites.\n */\nexport { DialogTrigger as PopoverTrigger } from \"react-aria-components\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,MAAa,kBAAkB,IAAI;CAEjC;CACA;CAIA;CAGA;AACF,CAAC;AA0BD,MAAaA,YAAU,WAAsC,SAASA,UACpE,EAAE,UAAU,WAAW,YAAY,UAAU,SAAS,GAAG,GAAG,SAC5D,KACA;CACA,OACE,oBAACC,SAAD;EACO;EACL,GAAI;EACO;EACH;EACR,aAAU;EACV,WAAW,GAAG,gBAAgB,GAAG,SAAS;EAUzC;CACU,CAAA;AAEjB,CAAC"}
1
+ {"version":3,"file":"popover.js","names":["Popover","AriaPopover"],"sources":["../../src/popover/popover.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport {\n Popover as AriaPopover,\n type PopoverProps as AriaPopoverProps,\n} from \"react-aria-components\";\n\nimport { cnAll } from \"../lib/cn.js\";\n\n/**\n * Popover, ported from the legacy shadcn/Radix component.\n *\n * Census: 2 files, 2 uses. `asChild` on the trigger twice, `className`\n * twice, `align=\"start\"` once. `PopoverAnchor` is exported by the legacy\n * and imported by nobody, so it is not ported; neither are `side`,\n * `sideOffset`, `alignOffset`, `open` / `onOpenChange`, `modal`,\n * `collisionPadding` or `avoidCollisions` -- zero call sites each. Both\n * existing uses are uncontrolled.\n *\n * IT GAINS AN ACCESSIBLE NAME. The legacy renders `role=\"dialog\"` with no\n * `aria-label` and no `aria-labelledby`, so both call sites announce an\n * unnamed dialog. React Aria labels the popover from its trigger\n * automatically once the two are paired, which is the only way to build\n * one here.\n *\n * The surface is `card`: in the legacy theme `--popover` is byte-identical\n * to `--card` in both themes, so the role the migration plan proposed\n * already exists under another name.\n */\nexport const popoverVariants = cva([\n // w-72 (288px) is the legacy default width, as a spacing-scale step.\n \"w-72 rounded-md border-(length:--border-width) border-border p-4\",\n \"bg-card text-card-foreground shadow-floating\",\n // The reset in @seekalfred/styles is OPT-IN, so a consumer may load\n // none: an unstyled document renders text in a serif, and content-box\n // would add the 16px padding on top of the width.\n \"box-border font-sans text-sm leading-(--leading-normal)\",\n // React Aria gives the popover tabindex=-1 and focuses it on open, so it\n // can take a focus ring it does not want.\n \"outline-none\",\n]);\n\n/**\n * Content anchored to a trigger.\n *\n * @example\n * <PopoverTrigger>\n * <Button>Pick a date</Button>\n * <Popover placement=\"bottom start\">...</Popover>\n * </PopoverTrigger>\n */\nexport interface PopoverProps\n extends Omit<AriaPopoverProps, \"className\" | \"style\" | \"children\"> {\n children?: ReactNode;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n /**\n * Where it opens, as React Aria's single `placement` string. The legacy\n * split this across `side` and `align`; its one non-default call site is\n * `align=\"start\"`, which is `\"bottom start\"` here.\n *\n * @default \"bottom\"\n */\n placement?: AriaPopoverProps[\"placement\"];\n /**\n * How it comes in: `rise` grows out of the trigger (offset toward it, a little\n * small, then settles on a spring) and leaves quicker; `none` leaves the entrance\n * to the caller, as a picker that animates its own popover does.\n *\n * @default \"rise\"\n */\n entrance?: \"rise\" | \"none\";\n}\n\nexport const Popover = forwardRef<HTMLElement, PopoverProps>(function Popover(\n { children, className, placement = \"bottom\", offset = 4, entrance = \"rise\", ...props },\n ref,\n) {\n return (\n <AriaPopover\n ref={ref}\n {...props}\n placement={placement}\n offset={offset}\n data-slot=\"popover\"\n className={cnAll(popoverVariants(), entrance === \"rise\" && \"popover-motion\", className)}\n >\n {/*\n No inner Dialog. Wrapping the content in one is the common React\n Aria idiom and it was in the first draft here -- but deleting it\n broke no assertion, so it was doing nothing: a Popover under a\n DialogTrigger already gets role=\"dialog\", a focus scope, and\n aria-labelledby pointing at the trigger. The wrapper was one more\n element and one more className to explain.\n */}\n {children}\n </AriaPopover>\n );\n});\n\n/**\n * Pairs a trigger with a popover and owns the open state.\n *\n * React Aria's `DialogTrigger`, renamed for the migration: it takes the\n * trigger element directly as its first child, which is what `asChild` was\n * doing on both legacy call sites.\n */\nexport { DialogTrigger as PopoverTrigger } from \"react-aria-components\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,MAAa,kBAAkB,IAAI;CAEjC;CACA;CAIA;CAGA;AACF,CAAC;AAkCD,MAAaA,YAAU,WAAsC,SAASA,UACpE,EAAE,UAAU,WAAW,YAAY,UAAU,SAAS,GAAG,WAAW,QAAQ,GAAG,SAC/E,KACA;CACA,OACE,oBAACC,SAAD;EACO;EACL,GAAI;EACO;EACH;EACR,aAAU;EACV,WAAW,MAAM,gBAAgB,GAAG,aAAa,UAAU,kBAAkB,SAAS;EAUrF;CACU,CAAA;AAEjB,CAAC"}
@@ -1,9 +1,11 @@
1
1
  "use client";
2
2
  import { cn, cnAll } from "../lib/cn.js";
3
+ import { SwapText } from "../lib/swap-text.js";
4
+ import { useAfter, useCountTo } from "../lib/count-to.js";
3
5
  import { cva } from "class-variance-authority";
4
6
  import { forwardRef } from "react";
5
7
  import { Label, ProgressBar } from "react-aria-components";
6
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
7
9
  //#region src/progress/progress.tsx
8
10
  /**
9
11
  * Progress, ported from the legacy shadcn/Radix component.
@@ -37,7 +39,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
37
39
  */
38
40
  const progressVariants = cva(["flex w-full flex-col gap-1.5 box-border font-sans"]);
39
41
  const progressTrackVariants = cva(["relative h-2 w-full overflow-hidden rounded-pill box-border", "bg-muted border-(length:--border-width) border-border"]);
40
- const progressFillVariants = cva(["h-full rounded-pill", "motion-safe:transition-[width] motion-safe:duration-(--duration-base) motion-safe:ease-standard"], {
42
+ const progressFillVariants = cva(["h-full rounded-pill", "progress-fill"], {
41
43
  variants: { tone: {
42
44
  neutral: "bg-primary-strong",
43
45
  success: "bg-success-strong",
@@ -47,24 +49,62 @@ const progressFillVariants = cva(["h-full rounded-pill", "motion-safe:transition
47
49
  } },
48
50
  defaultVariants: { tone: "neutral" }
49
51
  });
52
+ /**
53
+ * The printed value. A percentage counts to its new number on the same spring the
54
+ * fill follows, and a check lands beside the final count once the bar is full.
55
+ * Any other text (a price, a step count) is shown as it is.
56
+ */
57
+ function ProgressValue({ percentage, valueText }) {
58
+ const percent = Math.round(percentage);
59
+ const counted = useCountTo(percent);
60
+ const done = useAfter(percent >= 100, 240);
61
+ const isPercent = valueText === `${percent}%`;
62
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [isPercent ? `${counted}%` : valueText, isPercent ? /* @__PURE__ */ jsx("span", {
63
+ "data-slot": "progress-done",
64
+ className: "ms-1 inline-flex align-middle text-success-strong",
65
+ children: /* @__PURE__ */ jsx(SwapText, {
66
+ swapKey: done ? "done" : "",
67
+ motion: "glyph",
68
+ className: "inline-flex",
69
+ children: /* @__PURE__ */ jsx("svg", {
70
+ viewBox: "0 0 16 16",
71
+ fill: "none",
72
+ stroke: "currentColor",
73
+ strokeWidth: "2",
74
+ strokeLinecap: "round",
75
+ strokeLinejoin: "round",
76
+ className: "size-3.5",
77
+ "aria-hidden": "true",
78
+ children: /* @__PURE__ */ jsx("path", { d: "m3.5 8.5 3 3 6-6" })
79
+ })
80
+ })
81
+ }) : null] });
82
+ }
50
83
  const Progress = forwardRef(function Progress({ label, labelHidden, showValue, tone, trackClassName, fillClassName, className, ...props }, ref) {
51
84
  return /* @__PURE__ */ jsx(ProgressBar, {
52
85
  ref,
53
86
  ...props,
54
87
  "data-slot": "progress",
55
88
  className: cn(progressVariants(), className),
56
- children: ({ percentage, isIndeterminate, valueText }) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
89
+ children: ({ percentage, isIndeterminate, valueText }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
57
90
  "data-slot": "progress-header",
58
91
  className: cnAll(showValue && "flex items-baseline justify-between gap-2"),
59
92
  children: [/* @__PURE__ */ jsx(Label, {
60
93
  "data-slot": "progress-label",
61
94
  className: cnAll("text-sm leading-(--leading-normal) text-foreground", labelHidden && "sr-only"),
62
- children: label
95
+ children: typeof label === "string" ? /* @__PURE__ */ jsx(SwapText, {
96
+ swapKey: label,
97
+ className: "block",
98
+ children: label
99
+ }) : label
63
100
  }), showValue ? /* @__PURE__ */ jsx("span", {
64
101
  "data-slot": "progress-value",
65
102
  "aria-hidden": "true",
66
103
  className: "text-sm leading-(--leading-normal) text-muted-foreground tabular-nums",
67
- children: valueText
104
+ children: /* @__PURE__ */ jsx(ProgressValue, {
105
+ percentage: percentage ?? 0,
106
+ valueText
107
+ })
68
108
  }) : null]
69
109
  }), /* @__PURE__ */ jsx("div", {
70
110
  "data-slot": "progress-track",
@@ -72,6 +112,7 @@ const Progress = forwardRef(function Progress({ label, labelHidden, showValue, t
72
112
  children: /* @__PURE__ */ jsx("div", {
73
113
  "data-slot": "progress-fill",
74
114
  "data-indeterminate": isIndeterminate || void 0,
115
+ "data-complete": (!tone || tone === "neutral") && !isIndeterminate && (percentage ?? 0) >= 100 ? "" : void 0,
75
116
  className: cn(progressFillVariants({ tone }), fillClassName),
76
117
  style: { width: isIndeterminate ? "0%" : `${percentage ?? 0}%` }
77
118
  })
@@ -1 +1 @@
1
- {"version":3,"file":"progress.js","names":["AriaProgressBar","AriaLabel"],"sources":["../../src/progress/progress.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport {\n Label as AriaLabel,\n ProgressBar as AriaProgressBar,\n type ProgressBarProps as AriaProgressBarProps,\n} from \"react-aria-components\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\n\n/**\n * Progress, ported from the legacy shadcn/Radix component.\n *\n * Census: seven files import it and only TWO render anything --\n * `ResponseOption.tsx` and `LiveQA.tsx`. Both pass `value` and\n * `className` and nothing else.\n *\n * `label` IS REQUIRED, and that is the point. Neither live call site\n * passes `aria-label`, `aria-labelledby` or an `id` a label targets, and\n * `role=\"progressbar\"` does not take its name from content -- so both\n * rendered progress bars are announced as an unnamed progressbar. React\n * Aria supplies the plumbing for three naming routes but no name of its\n * own: `<ProgressBar value={60}/>` is just as nameless there. Making the\n * prop required is what turns it into something you cannot forget.\n *\n * The migration plan said this component \"adds the missing\n * aria-valuenow\". That is wrong -- Radix already supplies `aria-valuenow`,\n * `aria-valuemin`, `aria-valuemax` and `aria-valuetext`. The missing thing\n * was always the NAME.\n *\n * THE COLOURS ARE NOT THE LEGACY'S. Its track is `bg-primary/20`, an alpha\n * fill that composites against unknown page content and therefore has no\n * fixed contrast pair at all -- over white it is roughly 1.05:1, an\n * invisible boundary. Its indicator is `bg-primary`, 2.44:1 on the page.\n * Here the track is `muted` with a `border` boundary and the fill is\n * `primary-strong`, which puts the fill against the track at 4.61:1 and\n * the track against the page at 5.25:1.\n *\n * Both are in the build-failing contrast contract.\n */\nexport const progressVariants = cva([\"flex w-full flex-col gap-1.5 box-border font-sans\"]);\n\nexport const progressTrackVariants = cva([\n \"relative h-2 w-full overflow-hidden rounded-pill box-border\",\n // muted, NOT `border` as the Switch uses. A switch has no fill; a\n // progress bar does, and primary-strong on a `border` track is 1.01:1 --\n // the two halves are the same grey and the value disappears.\n \"bg-muted border-(length:--border-width) border-border\",\n]);\n\nexport const progressFillVariants = cva(\n [\n \"h-full rounded-pill\",\n \"motion-safe:transition-[width] motion-safe:duration-(--duration-base) motion-safe:ease-standard\",\n ],\n {\n variants: {\n /*\n * What the value MEANS, which for a progress bar is usually a score\n * rather than a completion. The product's bar carried exactly this axis\n * (`default`/`success`/`warning`/`error`/`info`) and reached for it to\n * colour performance metrics.\n *\n * `-strong` throughout: the fill sits on the `muted` track and has to\n * clear the 3:1 non-text floor of WCAG 1.4.11 against it, which is the\n * same reason the track is muted rather than `border` -- primary-strong\n * on a `border` track measures 1.01:1 and the value disappears.\n */\n tone: {\n neutral: \"bg-primary-strong\",\n success: \"bg-success-strong\",\n warning: \"bg-warning-strong\",\n danger: \"bg-destructive-strong\",\n info: \"bg-info-strong\",\n },\n },\n defaultVariants: { tone: \"neutral\" },\n },\n);\n\n/**\n * A determinate or indeterminate progress bar.\n *\n * @example\n * <Progress label=\"Upload\" value={62} />\n * <Progress label=\"Loading results\" />\n */\nexport interface ProgressProps\n extends Omit<AriaProgressBarProps, \"className\" | \"style\" | \"children\">,\n VariantProps<typeof progressFillVariants> {\n /**\n * What is progressing, and the bar's accessible name.\n *\n * **Required.** Both legacy call sites render an unnamed progressbar.\n * Pass `labelHidden` when the surrounding UI already says it.\n */\n label: ReactNode;\n /**\n * Render the label for screen readers only.\n *\n * Both legacy call sites sit beside visible text that was never\n * associated with the bar -- this is how that text becomes the name\n * without printing it twice.\n */\n labelHidden?: boolean;\n /**\n * Print the value beside the label.\n *\n * The text itself comes from React Aria, which already formats it from\n * `value`/`minValue`/`maxValue` and honours `formatOptions` and\n * `valueLabel` -- so a percentage, a currency or \"3 of 7\" are all reachable\n * without a second prop here. It was never RENDERED before: the bar\n * announced its value to assistive tech and showed a sighted reader nothing.\n */\n showValue?: boolean;\n /** Extra classes for the track. */\n trackClassName?: string;\n /** Extra classes for the fill -- a per-call-site colour the tone axis cannot express. */\n fillClassName?: string;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport const Progress = forwardRef<HTMLDivElement, ProgressProps>(function Progress(\n { label, labelHidden, showValue, tone, trackClassName, fillClassName, className, ...props },\n ref,\n) {\n return (\n <AriaProgressBar\n ref={ref}\n {...props}\n data-slot=\"progress\"\n className={cn(progressVariants(), className)}\n >\n {({ percentage, isIndeterminate, valueText }) => (\n <>\n {/*\n The row exists only when the value is printed. Wrapping the label\n in a flex row unconditionally would change the label's box for\n every existing consumer to no purpose.\n */}\n <div\n data-slot=\"progress-header\"\n className={cnAll(showValue && \"flex items-baseline justify-between gap-2\")}\n >\n <AriaLabel\n data-slot=\"progress-label\"\n className={cnAll(\n \"text-sm leading-(--leading-normal) text-foreground\",\n labelHidden && \"sr-only\",\n )}\n >\n {label}\n </AriaLabel>\n {showValue ? (\n <span\n data-slot=\"progress-value\"\n // aria-hidden: React Aria already announces the value on the\n // progressbar itself. Without this a screen reader reads it\n // twice, once as the widget's value and once as loose text.\n aria-hidden=\"true\"\n className=\"text-sm leading-(--leading-normal) text-muted-foreground tabular-nums\"\n >\n {valueText}\n </span>\n ) : null}\n </div>\n <div\n data-slot=\"progress-track\"\n className={cn(progressTrackVariants(), trackClassName)}\n >\n {/*\n width, not the legacy translateX(-(100 - value)%). That\n overflowed for value > 100 -- a positive translate slid the\n indicator off to the right, while the announced value stayed\n whatever was passed. React Aria clamps the value, so the two\n can no longer disagree.\n\n An indeterminate bar renders no fill rather than the\n legacy's silent 0%: `value={undefined}` became `(value || 0)`\n there, so a sighted user saw \"0% done\" while a screen reader\n said \"indeterminate\".\n */}\n <div\n data-slot=\"progress-fill\"\n data-indeterminate={isIndeterminate || undefined}\n className={cn(progressFillVariants({ tone }), fillClassName)}\n style={{ width: isIndeterminate ? \"0%\" : `${percentage ?? 0}%` }}\n />\n </div>\n </>\n )}\n </AriaProgressBar>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,MAAa,mBAAmB,IAAI,CAAC,mDAAmD,CAAC;AAEzF,MAAa,wBAAwB,IAAI,CACvC,+DAIA,uDACF,CAAC;AAED,MAAa,uBAAuB,IAClC,CACE,uBACA,iGACF,GACA;CACE,UAAU,EAYR,MAAM;EACJ,SAAS;EACT,SAAS;EACT,SAAS;EACT,QAAQ;EACR,MAAM;CACR,EACF;CACA,iBAAiB,EAAE,MAAM,UAAU;AACrC,CACF;AA6CA,MAAa,WAAW,WAA0C,SAAS,SACzE,EAAE,OAAO,aAAa,WAAW,MAAM,gBAAgB,eAAe,WAAW,GAAG,SACpF,KACA;CACA,OACE,oBAACA,aAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,iBAAiB,GAAG,SAAS;EAEzC,WAAA,EAAE,YAAY,iBAAiB,gBAC/B,qBAAA,UAAA,EAAA,UAAA,CAME,qBAAC,OAAD;GACE,aAAU;GACV,WAAW,MAAM,aAAa,2CAA2C;GAF3E,UAAA,CAIE,oBAACC,OAAD;IACE,aAAU;IACV,WAAW,MACT,sDACA,eAAe,SACjB;IAEC,UAAA;GACQ,CAAA,GACV,YACC,oBAAC,QAAD;IACE,aAAU;IAIV,eAAY;IACZ,WAAU;IAET,UAAA;GACG,CAAA,IACJ,IACD;EACL,CAAA,GAAA,oBAAC,OAAD;GACE,aAAU;GACV,WAAW,GAAG,sBAAsB,GAAG,cAAc;GAcrD,UAAA,oBAAC,OAAD;IACE,aAAU;IACV,sBAAoB,mBAAmB,KAAA;IACvC,WAAW,GAAG,qBAAqB,EAAE,KAAK,CAAC,GAAG,aAAa;IAC3D,OAAO,EAAE,OAAO,kBAAkB,OAAO,GAAG,cAAc,EAAE,GAAG;GAChE,CAAA;EACE,CAAA,CACL,EAAA,CAAA;CAEW,CAAA;AAErB,CAAC"}
1
+ {"version":3,"file":"progress.js","names":["AriaProgressBar","AriaLabel"],"sources":["../../src/progress/progress.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport {\n Label as AriaLabel,\n ProgressBar as AriaProgressBar,\n type ProgressBarProps as AriaProgressBarProps,\n} from \"react-aria-components\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { useAfter, useCountTo } from \"../lib/count-to.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * Progress, ported from the legacy shadcn/Radix component.\n *\n * Census: seven files import it and only TWO render anything --\n * `ResponseOption.tsx` and `LiveQA.tsx`. Both pass `value` and\n * `className` and nothing else.\n *\n * `label` IS REQUIRED, and that is the point. Neither live call site\n * passes `aria-label`, `aria-labelledby` or an `id` a label targets, and\n * `role=\"progressbar\"` does not take its name from content -- so both\n * rendered progress bars are announced as an unnamed progressbar. React\n * Aria supplies the plumbing for three naming routes but no name of its\n * own: `<ProgressBar value={60}/>` is just as nameless there. Making the\n * prop required is what turns it into something you cannot forget.\n *\n * The migration plan said this component \"adds the missing\n * aria-valuenow\". That is wrong -- Radix already supplies `aria-valuenow`,\n * `aria-valuemin`, `aria-valuemax` and `aria-valuetext`. The missing thing\n * was always the NAME.\n *\n * THE COLOURS ARE NOT THE LEGACY'S. Its track is `bg-primary/20`, an alpha\n * fill that composites against unknown page content and therefore has no\n * fixed contrast pair at all -- over white it is roughly 1.05:1, an\n * invisible boundary. Its indicator is `bg-primary`, 2.44:1 on the page.\n * Here the track is `muted` with a `border` boundary and the fill is\n * `primary-strong`, which puts the fill against the track at 4.61:1 and\n * the track against the page at 5.25:1.\n *\n * Both are in the build-failing contrast contract.\n */\nexport const progressVariants = cva([\"flex w-full flex-col gap-1.5 box-border font-sans\"]);\n\nexport const progressTrackVariants = cva([\n \"relative h-2 w-full overflow-hidden rounded-pill box-border\",\n // muted, NOT `border` as the Switch uses. A switch has no fill; a\n // progress bar does, and primary-strong on a `border` track is 1.01:1 --\n // the two halves are the same grey and the value disappears.\n \"bg-muted border-(length:--border-width) border-border\",\n]);\n\nexport const progressFillVariants = cva(\n [\n \"h-full rounded-pill\",\n // The fill follows the value on the smooth spring and turns success-coloured when it completes (see progress-fill).\n \"progress-fill\",\n ],\n {\n variants: {\n /*\n * What the value MEANS, which for a progress bar is usually a score\n * rather than a completion. The product's bar carried exactly this axis\n * (`default`/`success`/`warning`/`error`/`info`) and reached for it to\n * colour performance metrics.\n *\n * `-strong` throughout: the fill sits on the `muted` track and has to\n * clear the 3:1 non-text floor of WCAG 1.4.11 against it, which is the\n * same reason the track is muted rather than `border` -- primary-strong\n * on a `border` track measures 1.01:1 and the value disappears.\n */\n tone: {\n neutral: \"bg-primary-strong\",\n success: \"bg-success-strong\",\n warning: \"bg-warning-strong\",\n danger: \"bg-destructive-strong\",\n info: \"bg-info-strong\",\n },\n },\n defaultVariants: { tone: \"neutral\" },\n },\n);\n\n/**\n * A determinate or indeterminate progress bar.\n *\n * @example\n * <Progress label=\"Upload\" value={62} />\n * <Progress label=\"Loading results\" />\n */\nexport interface ProgressProps\n extends Omit<AriaProgressBarProps, \"className\" | \"style\" | \"children\">,\n VariantProps<typeof progressFillVariants> {\n /**\n * What is progressing, and the bar's accessible name.\n *\n * **Required.** Both legacy call sites render an unnamed progressbar.\n * Pass `labelHidden` when the surrounding UI already says it.\n */\n label: ReactNode;\n /**\n * Render the label for screen readers only.\n *\n * Both legacy call sites sit beside visible text that was never\n * associated with the bar -- this is how that text becomes the name\n * without printing it twice.\n */\n labelHidden?: boolean;\n /**\n * Print the value beside the label.\n *\n * The text itself comes from React Aria, which already formats it from\n * `value`/`minValue`/`maxValue` and honours `formatOptions` and\n * `valueLabel` -- so a percentage, a currency or \"3 of 7\" are all reachable\n * without a second prop here. It was never RENDERED before: the bar\n * announced its value to assistive tech and showed a sighted reader nothing.\n */\n showValue?: boolean;\n /** Extra classes for the track. */\n trackClassName?: string;\n /** Extra classes for the fill -- a per-call-site colour the tone axis cannot express. */\n fillClassName?: string;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\n/**\n * The printed value. A percentage counts to its new number on the same spring the\n * fill follows, and a check lands beside the final count once the bar is full.\n * Any other text (a price, a step count) is shown as it is.\n */\nfunction ProgressValue({ percentage, valueText }: { percentage: number; valueText: string | undefined }) {\n const percent = Math.round(percentage);\n const counted = useCountTo(percent);\n const done = useAfter(percent >= 100, 240);\n const isPercent = valueText === `${percent}%`;\n return (\n <>\n {isPercent ? `${counted}%` : valueText}\n {isPercent ? (\n <span data-slot=\"progress-done\" className=\"ms-1 inline-flex align-middle text-success-strong\">\n <SwapText swapKey={done ? \"done\" : \"\"} motion=\"glyph\" className=\"inline-flex\">\n <svg\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"size-3.5\"\n aria-hidden=\"true\"\n >\n <path d=\"m3.5 8.5 3 3 6-6\" />\n </svg>\n </SwapText>\n </span>\n ) : null}\n </>\n );\n}\n\nexport const Progress = forwardRef<HTMLDivElement, ProgressProps>(function Progress(\n { label, labelHidden, showValue, tone, trackClassName, fillClassName, className, ...props },\n ref,\n) {\n return (\n <AriaProgressBar\n ref={ref}\n {...props}\n data-slot=\"progress\"\n className={cn(progressVariants(), className)}\n >\n {({ percentage, isIndeterminate, valueText }) => (\n <>\n {/*\n The row exists only when the value is printed. Wrapping the label\n in a flex row unconditionally would change the label's box for\n every existing consumer to no purpose.\n */}\n <div\n data-slot=\"progress-header\"\n className={cnAll(showValue && \"flex items-baseline justify-between gap-2\")}\n >\n <AriaLabel\n data-slot=\"progress-label\"\n className={cnAll(\n \"text-sm leading-(--leading-normal) text-foreground\",\n labelHidden && \"sr-only\",\n )}\n >\n {typeof label === \"string\" ? (\n <SwapText swapKey={label} className=\"block\">\n {label}\n </SwapText>\n ) : (\n label\n )}\n </AriaLabel>\n {showValue ? (\n <span\n data-slot=\"progress-value\"\n // aria-hidden: React Aria already announces the value on the\n // progressbar itself. Without this a screen reader reads it\n // twice, once as the widget's value and once as loose text.\n aria-hidden=\"true\"\n className=\"text-sm leading-(--leading-normal) text-muted-foreground tabular-nums\"\n >\n <ProgressValue percentage={percentage ?? 0} valueText={valueText} />\n </span>\n ) : null}\n </div>\n <div\n data-slot=\"progress-track\"\n className={cn(progressTrackVariants(), trackClassName)}\n >\n {/*\n width, not the legacy translateX(-(100 - value)%). That\n overflowed for value > 100 -- a positive translate slid the\n indicator off to the right, while the announced value stayed\n whatever was passed. React Aria clamps the value, so the two\n can no longer disagree.\n\n An indeterminate bar renders no fill rather than the\n legacy's silent 0%: `value={undefined}` became `(value || 0)`\n there, so a sighted user saw \"0% done\" while a screen reader\n said \"indeterminate\".\n */}\n <div\n data-slot=\"progress-fill\"\n data-indeterminate={isIndeterminate || undefined}\n // A default-toned bar turns success-coloured when it completes; an explicit tone is never overridden.\n data-complete={(!tone || tone === \"neutral\") && !isIndeterminate && (percentage ?? 0) >= 100 ? \"\" : undefined}\n className={cn(progressFillVariants({ tone }), fillClassName)}\n style={{ width: isIndeterminate ? \"0%\" : `${percentage ?? 0}%` }}\n />\n </div>\n </>\n )}\n </AriaProgressBar>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,MAAa,mBAAmB,IAAI,CAAC,mDAAmD,CAAC;AAEzF,MAAa,wBAAwB,IAAI,CACvC,+DAIA,uDACF,CAAC;AAED,MAAa,uBAAuB,IAClC,CACE,uBAEA,eACF,GACA;CACE,UAAU,EAYR,MAAM;EACJ,SAAS;EACT,SAAS;EACT,SAAS;EACT,QAAQ;EACR,MAAM;CACR,EACF;CACA,iBAAiB,EAAE,MAAM,UAAU;AACrC,CACF;;;;;;AAkDA,SAAS,cAAc,EAAE,YAAY,aAAoE;CACvG,MAAM,UAAU,KAAK,MAAM,UAAU;CACrC,MAAM,UAAU,WAAW,OAAO;CAClC,MAAM,OAAO,SAAS,WAAW,KAAK,GAAG;CACzC,MAAM,YAAY,cAAc,GAAG,QAAQ;CAC3C,OACE,qBAAA,YAAA,EAAA,UAAA,CACG,YAAY,GAAG,QAAQ,KAAK,WAC5B,YACC,oBAAC,QAAD;EAAM,aAAU;EAAgB,WAAU;EACxC,UAAA,oBAAC,UAAD;GAAU,SAAS,OAAO,SAAS;GAAI,QAAO;GAAQ,WAAU;GAC9D,UAAA,oBAAC,OAAD;IACE,SAAQ;IACR,MAAK;IACL,QAAO;IACP,aAAY;IACZ,eAAc;IACd,gBAAe;IACf,WAAU;IACV,eAAY;IAEZ,UAAA,oBAAC,QAAD,EAAM,GAAE,mBAAoB,CAAA;GACzB,CAAA;EACG,CAAA;CACN,CAAA,IACJ,IACJ,EAAA,CAAA;AAEN;AAEA,MAAa,WAAW,WAA0C,SAAS,SACzE,EAAE,OAAO,aAAa,WAAW,MAAM,gBAAgB,eAAe,WAAW,GAAG,SACpF,KACA;CACA,OACE,oBAACA,aAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,iBAAiB,GAAG,SAAS;EAEzC,WAAA,EAAE,YAAY,iBAAiB,gBAC/B,qBAAA,YAAA,EAAA,UAAA,CAME,qBAAC,OAAD;GACE,aAAU;GACV,WAAW,MAAM,aAAa,2CAA2C;GAF3E,UAAA,CAIE,oBAACC,OAAD;IACE,aAAU;IACV,WAAW,MACT,sDACA,eAAe,SACjB;IAEC,UAAA,OAAO,UAAU,WAChB,oBAAC,UAAD;KAAU,SAAS;KAAO,WAAU;KACjC,UAAA;IACO,CAAA,IAEV;GAEO,CAAA,GACV,YACC,oBAAC,QAAD;IACE,aAAU;IAIV,eAAY;IACZ,WAAU;IAEV,UAAA,oBAAC,eAAD;KAAe,YAAY,cAAc;KAAc;IAAY,CAAA;GAC/D,CAAA,IACJ,IACD;EACL,CAAA,GAAA,oBAAC,OAAD;GACE,aAAU;GACV,WAAW,GAAG,sBAAsB,GAAG,cAAc;GAcrD,UAAA,oBAAC,OAAD;IACE,aAAU;IACV,sBAAoB,mBAAmB,KAAA;IAEvC,kBAAgB,CAAC,QAAQ,SAAS,cAAc,CAAC,oBAAoB,cAAc,MAAM,MAAM,KAAK,KAAA;IACpG,WAAW,GAAG,qBAAqB,EAAE,KAAK,CAAC,GAAG,aAAa;IAC3D,OAAO,EAAE,OAAO,kBAAkB,OAAO,GAAG,cAAc,EAAE,GAAG;GAChE,CAAA;EACE,CAAA,CACL,EAAA,CAAA;CAEW,CAAA;AAErB,CAAC"}
@@ -4,7 +4,7 @@ import { selectionLabelVariants, selectionRingVariants } from "../lib/selection-
4
4
  import { cva } from "class-variance-authority";
5
5
  import { forwardRef } from "react";
6
6
  import { Radio, RadioGroup } from "react-aria-components";
7
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
8
  //#region src/radio-group/radio-group.tsx
9
9
  /**
10
10
  * RadioGroup, ported from the legacy shadcn/Radix component onto React Aria.
@@ -29,7 +29,7 @@ const radioVariants = cva([
29
29
  "inline-flex size-4 shrink-0 items-center justify-center",
30
30
  "rounded-full border-(length:--border-width) box-border",
31
31
  "bg-background border-border",
32
- "motion-safe:transition-colors motion-safe:duration-(--duration-fast) motion-safe:ease-standard",
32
+ "radio-mark",
33
33
  "group-data-[selected]:bg-primary group-data-[selected]:border-primary-strong",
34
34
  "group-data-[selected]:text-primary-foreground",
35
35
  selectionRingVariants()
@@ -48,14 +48,15 @@ const Radio$1 = forwardRef(function Radio$2({ className, children, ...props }, r
48
48
  inputRef: ref,
49
49
  "data-slot": "radio",
50
50
  className: cn(cnAll(selectionLabelVariants(), "justify-start"), className),
51
- children: ({ isSelected }) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
51
+ children: ({ isSelected }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
52
52
  "aria-hidden": "true",
53
53
  "data-slot": "radio-dot",
54
54
  className: radioVariants(),
55
- children: isSelected ? /* @__PURE__ */ jsx("span", {
55
+ children: /* @__PURE__ */ jsx("span", {
56
56
  "data-slot": "radio-indicator",
57
- className: "size-1.5 rounded-full bg-current"
58
- }) : null
57
+ "data-selected": isSelected || void 0,
58
+ className: "radio-dot size-1.5 rounded-full bg-current"
59
+ })
59
60
  }), children == null ? null : /* @__PURE__ */ jsx("span", {
60
61
  "data-slot": "radio-label",
61
62
  children
@@ -1 +1 @@
1
- {"version":3,"file":"radio-group.js","names":["RadioGroup","AriaRadioGroup","Radio","AriaRadio"],"sources":["../../src/radio-group/radio-group.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport {\n Radio as AriaRadio,\n RadioGroup as AriaRadioGroup,\n type RadioGroupProps as AriaRadioGroupProps,\n type RadioProps as AriaRadioProps,\n} from \"react-aria-components\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { selectionLabelVariants, selectionRingVariants } from \"../lib/selection-control.js\";\n\n/**\n * RadioGroup, ported from the legacy shadcn/Radix component onto React Aria.\n *\n * Census: 1 file, 3 uses -- `RadioGroup` 1, `RadioGroupItem` 2. The\n * smallest real surface in the migration, so the port is deliberately plain.\n *\n * THE LABEL MOVES INSIDE THE ITEM. Radix's `RadioGroupItem` is a bare\n * button and the caller pairs it with a separate `<Label htmlFor>`; React\n * Aria's `Radio` takes its label as `children` and wires the association\n * itself. That is the one rename a call site has to make, and it removes\n * the id bookkeeping.\n *\n * `border-primary` becomes `border-border` for the same reason it did on\n * Checkbox: brand orange is 2.44:1 on white and cannot legally be the only\n * thing describing a control's edge (WCAG 1.4.11). Selected reuses\n * Checkbox's exact pair -- `bg-primary` with a `primary-strong` boundary --\n * so a radio and a checkbox in the same form agree.\n */\nexport const radioGroupVariants = cva([\n \"flex flex-col gap-2\",\n \"box-border font-sans\",\n]);\n\nexport const radioVariants = cva([\n \"inline-flex size-4 shrink-0 items-center justify-center\",\n \"rounded-full border-(length:--border-width) box-border\",\n \"bg-background border-border\",\n \"motion-safe:transition-colors motion-safe:duration-(--duration-fast) motion-safe:ease-standard\",\n \"group-data-[selected]:bg-primary group-data-[selected]:border-primary-strong\",\n \"group-data-[selected]:text-primary-foreground\",\n selectionRingVariants(),\n]);\n\n/**\n * A set of mutually exclusive choices.\n *\n * @example\n * <RadioGroup value={plan} onChange={setPlan} aria-label=\"Plan\">\n * <Radio value=\"free\">Free</Radio>\n * <Radio value=\"pro\">Pro</Radio>\n * </RadioGroup>\n */\nexport interface RadioGroupProps extends Omit<AriaRadioGroupProps, \"className\" | \"style\"> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport const RadioGroup = forwardRef<HTMLDivElement, RadioGroupProps>(function RadioGroup(\n { className, ...props },\n ref,\n) {\n return (\n <AriaRadioGroup\n ref={ref}\n {...props}\n data-slot=\"radio-group\"\n className={cn(radioGroupVariants(), className)}\n />\n );\n});\n\n/**\n * One choice.\n *\n * @example\n * <Radio value=\"pro\">Pro</Radio>\n */\nexport interface RadioProps extends Omit<AriaRadioProps, \"className\" | \"style\" | \"children\"> {\n /** The label, and this option's accessible name. */\n children?: ReactNode;\n /** Extra classes for the label. Merged last, so they win conflicts. */\n className?: string;\n}\n\nexport const Radio = forwardRef<HTMLInputElement, RadioProps>(function Radio(\n { className, children, ...props },\n ref,\n) {\n return (\n <AriaRadio\n {...props}\n // inputRef, not ref: AriaRadio's own ref targets the label it renders\n // around a visually-hidden native input, and a label cannot be\n // focused -- ref.current.focus() would silently no-op. inputRef is\n // React Aria's own escape hatch to the real control.\n inputRef={ref}\n data-slot=\"radio\"\n // justify-start, overriding the shared label's centring: a radio sits\n // in a vertical list where every label must start on the same line,\n // not centre itself in a 24px box.\n className={cn(cnAll(selectionLabelVariants(), \"justify-start\"), className)}\n >\n {({ isSelected }) => (\n <>\n <span aria-hidden=\"true\" data-slot=\"radio-dot\" className={radioVariants()}>\n {isSelected ? (\n // Rendered in JS rather than toggled with CSS, so the\n // precedence is explicit and a test can read it from the DOM.\n <span data-slot=\"radio-indicator\" className=\"size-1.5 rounded-full bg-current\" />\n ) : null}\n </span>\n {children == null ? null : <span data-slot=\"radio-label\">{children}</span>}\n </>\n )}\n </AriaRadio>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,MAAa,qBAAqB,IAAI,CACpC,uBACA,sBACF,CAAC;AAED,MAAa,gBAAgB,IAAI;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA,sBAAsB;AACxB,CAAC;AAgBD,MAAaA,eAAa,WAA4C,SAASA,aAC7E,EAAE,WAAW,GAAG,SAChB,KACA;CACA,OACE,oBAACC,YAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,mBAAmB,GAAG,SAAS;CAC9C,CAAA;AAEL,CAAC;AAeD,MAAaC,UAAQ,WAAyC,SAASA,QACrE,EAAE,WAAW,UAAU,GAAG,SAC1B,KACA;CACA,OACE,oBAACC,OAAD;EACE,GAAI;EAKJ,UAAU;EACV,aAAU;EAIV,WAAW,GAAG,MAAM,uBAAuB,GAAG,eAAe,GAAG,SAAS;EAEvE,WAAA,EAAE,iBACF,qBAAA,UAAA,EAAA,UAAA,CACE,oBAAC,QAAD;GAAM,eAAY;GAAO,aAAU;GAAY,WAAW,cAAc;GACrE,UAAA,aAGC,oBAAC,QAAD;IAAM,aAAU;IAAkB,WAAU;GAAoC,CAAA,IAC9E;EACA,CAAA,GACL,YAAY,OAAO,OAAO,oBAAC,QAAD;GAAM,aAAU;GAAe;EAAe,CAAA,CACzE,EAAA,CAAA;CAEK,CAAA;AAEf,CAAC"}
1
+ {"version":3,"file":"radio-group.js","names":["RadioGroup","AriaRadioGroup","Radio","AriaRadio"],"sources":["../../src/radio-group/radio-group.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport {\n Radio as AriaRadio,\n RadioGroup as AriaRadioGroup,\n type RadioGroupProps as AriaRadioGroupProps,\n type RadioProps as AriaRadioProps,\n} from \"react-aria-components\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { selectionLabelVariants, selectionRingVariants } from \"../lib/selection-control.js\";\n\n/**\n * RadioGroup, ported from the legacy shadcn/Radix component onto React Aria.\n *\n * Census: 1 file, 3 uses -- `RadioGroup` 1, `RadioGroupItem` 2. The\n * smallest real surface in the migration, so the port is deliberately plain.\n *\n * THE LABEL MOVES INSIDE THE ITEM. Radix's `RadioGroupItem` is a bare\n * button and the caller pairs it with a separate `<Label htmlFor>`; React\n * Aria's `Radio` takes its label as `children` and wires the association\n * itself. That is the one rename a call site has to make, and it removes\n * the id bookkeeping.\n *\n * `border-primary` becomes `border-border` for the same reason it did on\n * Checkbox: brand orange is 2.44:1 on white and cannot legally be the only\n * thing describing a control's edge (WCAG 1.4.11). Selected reuses\n * Checkbox's exact pair -- `bg-primary` with a `primary-strong` boundary --\n * so a radio and a checkbox in the same form agree.\n */\nexport const radioGroupVariants = cva([\n \"flex flex-col gap-2\",\n \"box-border font-sans\",\n]);\n\nexport const radioVariants = cva([\n \"inline-flex size-4 shrink-0 items-center justify-center\",\n \"rounded-full border-(length:--border-width) box-border\",\n \"bg-background border-border\",\n // Colours ease, the mark dips under a press and springs back (see radio-mark).\n \"radio-mark\",\n \"group-data-[selected]:bg-primary group-data-[selected]:border-primary-strong\",\n \"group-data-[selected]:text-primary-foreground\",\n selectionRingVariants(),\n]);\n\n/**\n * A set of mutually exclusive choices.\n *\n * @example\n * <RadioGroup value={plan} onChange={setPlan} aria-label=\"Plan\">\n * <Radio value=\"free\">Free</Radio>\n * <Radio value=\"pro\">Pro</Radio>\n * </RadioGroup>\n */\nexport interface RadioGroupProps extends Omit<AriaRadioGroupProps, \"className\" | \"style\"> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport const RadioGroup = forwardRef<HTMLDivElement, RadioGroupProps>(function RadioGroup(\n { className, ...props },\n ref,\n) {\n return (\n <AriaRadioGroup\n ref={ref}\n {...props}\n data-slot=\"radio-group\"\n className={cn(radioGroupVariants(), className)}\n />\n );\n});\n\n/**\n * One choice.\n *\n * @example\n * <Radio value=\"pro\">Pro</Radio>\n */\nexport interface RadioProps extends Omit<AriaRadioProps, \"className\" | \"style\" | \"children\"> {\n /** The label, and this option's accessible name. */\n children?: ReactNode;\n /** Extra classes for the label. Merged last, so they win conflicts. */\n className?: string;\n}\n\nexport const Radio = forwardRef<HTMLInputElement, RadioProps>(function Radio(\n { className, children, ...props },\n ref,\n) {\n return (\n <AriaRadio\n {...props}\n // inputRef, not ref: AriaRadio's own ref targets the label it renders\n // around a visually-hidden native input, and a label cannot be\n // focused -- ref.current.focus() would silently no-op. inputRef is\n // React Aria's own escape hatch to the real control.\n inputRef={ref}\n data-slot=\"radio\"\n // justify-start, overriding the shared label's centring: a radio sits\n // in a vertical list where every label must start on the same line,\n // not centre itself in a 24px box.\n className={cn(cnAll(selectionLabelVariants(), \"justify-start\"), className)}\n >\n {({ isSelected }) => (\n <>\n <span aria-hidden=\"true\" data-slot=\"radio-dot\" className={radioVariants()}>\n {/*\n Always there: the state decides whether it shows, so the dot can\n spring in and shrink away. data-selected carries the state to a\n test, which can no longer read it from the dot's presence.\n */}\n <span\n data-slot=\"radio-indicator\"\n data-selected={isSelected || undefined}\n className=\"radio-dot size-1.5 rounded-full bg-current\"\n />\n </span>\n {children == null ? null : <span data-slot=\"radio-label\">{children}</span>}\n </>\n )}\n </AriaRadio>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,MAAa,qBAAqB,IAAI,CACpC,uBACA,sBACF,CAAC;AAED,MAAa,gBAAgB,IAAI;CAC/B;CACA;CACA;CAEA;CACA;CACA;CACA,sBAAsB;AACxB,CAAC;AAgBD,MAAaA,eAAa,WAA4C,SAASA,aAC7E,EAAE,WAAW,GAAG,SAChB,KACA;CACA,OACE,oBAACC,YAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,mBAAmB,GAAG,SAAS;CAC9C,CAAA;AAEL,CAAC;AAeD,MAAaC,UAAQ,WAAyC,SAASA,QACrE,EAAE,WAAW,UAAU,GAAG,SAC1B,KACA;CACA,OACE,oBAACC,OAAD;EACE,GAAI;EAKJ,UAAU;EACV,aAAU;EAIV,WAAW,GAAG,MAAM,uBAAuB,GAAG,eAAe,GAAG,SAAS;EAEvE,WAAA,EAAE,iBACF,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,QAAD;GAAM,eAAY;GAAO,aAAU;GAAY,WAAW,cAAc;GAMtE,UAAA,oBAAC,QAAD;IACE,aAAU;IACV,iBAAe,cAAc,KAAA;IAC7B,WAAU;GACX,CAAA;EACG,CAAA,GACL,YAAY,OAAO,OAAO,oBAAC,QAAD;GAAM,aAAU;GAAe;EAAe,CAAA,CACzE,EAAA,CAAA;CAEK,CAAA;AAEf,CAAC"}
@@ -53,6 +53,16 @@ export interface ScrollAreaProps extends ComponentPropsWithoutRef<"div"> {
53
53
  * jump to it is the point.
54
54
  */
55
55
  label: string;
56
+ /**
57
+ * Fade the content out at an edge by exactly as much as there is content hidden
58
+ * past it, up to this many pixels: the fade grows in with the first pixels of
59
+ * scroll and is gone at the ends. `0` turns it off, and the default leaves the
60
+ * area as it was. Written on each scroll frame straight to the element, so
61
+ * scrolling never re-renders.
62
+ *
63
+ * @default 0
64
+ */
65
+ fade?: number;
56
66
  /**
57
67
  * Change this when the contents change, and the region scrolls back to the
58
68
  * top.
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { cn } from "../lib/cn.js";
2
+ import { cnAll } from "../lib/cn.js";
3
3
  import { mergeRefs } from "../lib/merge-refs.js";
4
4
  import { cva } from "class-variance-authority";
5
5
  import { forwardRef, useEffect, useMemo, useRef } from "react";
@@ -42,12 +42,29 @@ const scrollAreaVariants = cva([
42
42
  "focus-visible:outline-offset-(--focus-ring-offset)",
43
43
  "focus-visible:outline-(--color-ring)"
44
44
  ]);
45
- const ScrollArea = forwardRef(function ScrollArea({ className, children, label, scrollResetKey, ...props }, ref) {
45
+ const ScrollArea = forwardRef(function ScrollArea({ className, children, label, scrollResetKey, fade = 0, onScroll, ...props }, ref) {
46
46
  const region = useRef(null);
47
47
  useEffect(() => {
48
48
  if (region.current) region.current.scrollTop = 0;
49
49
  }, [scrollResetKey]);
50
50
  const rootRef = useMemo(() => mergeRefs(ref, region), [ref, region]);
51
+ const syncFade = () => {
52
+ const node = region.current;
53
+ if (!node || fade <= 0) return;
54
+ const max = Math.max(0, node.scrollHeight - node.clientHeight);
55
+ node.style.setProperty("--fade-top", `${Math.min(fade, node.scrollTop)}px`);
56
+ node.style.setProperty("--fade-bottom", `${Math.min(fade, max - node.scrollTop)}px`);
57
+ };
58
+ useEffect(() => {
59
+ const node = region.current;
60
+ if (!node || fade <= 0) return;
61
+ syncFade();
62
+ if (typeof ResizeObserver === "undefined") return;
63
+ const observer = new ResizeObserver(syncFade);
64
+ observer.observe(node);
65
+ for (const child of Array.from(node.children)) observer.observe(child);
66
+ return () => observer.disconnect();
67
+ }, [fade, children]);
51
68
  return /* @__PURE__ */ jsx("div", {
52
69
  ...props,
53
70
  ref: rootRef,
@@ -55,7 +72,12 @@ const ScrollArea = forwardRef(function ScrollArea({ className, children, label,
55
72
  role: "region",
56
73
  "aria-label": label,
57
74
  "data-slot": "scroll-area",
58
- className: cn(scrollAreaVariants(), className),
75
+ "data-fade": fade > 0 ? "" : void 0,
76
+ onScroll: (event) => {
77
+ syncFade();
78
+ onScroll?.(event);
79
+ },
80
+ className: cnAll(scrollAreaVariants(), fade > 0 && "scroll-fade-y", className),
59
81
  children
60
82
  });
61
83
  });
@@ -1 +1 @@
1
- {"version":3,"file":"scroll-area.js","names":[],"sources":["../../src/scroll-area/scroll-area.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport {\n forwardRef,\n useEffect,\n useMemo,\n useRef,\n type ComponentPropsWithoutRef,\n type ReactNode,\n} from \"react\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\n\n/**\n * ScrollArea. React Aria has no equivalent, so this is a plain scrolling\n * div -- and that is the honest answer rather than a shortcut.\n *\n * Census: fourteen paths match, thirteen are counted, ELEVEN render\n * anything, and across them 17 uses. Every single one passes `className`\n * and nothing else, and 13 of the 17 are a height or flex constraint plus\n * padding. `ScrollBar` is exported by the legacy and imported by nobody.\n * `orientation=\"horizontal\"` has zero call sites.\n *\n * So there is no styled-scrollbar requirement to port. The legacy pays for\n * one anyway: Radix injects a `<style>` tag that hides the native\n * scrollbar, then re-draws it in JavaScript with a ResizeObserver, a\n * portal-free overlay and a `display: table` content wrapper. That is a\n * lot of machinery to reproduce a scrollbar, and it is why the legacy\n * `ScrollArea` cannot be scrolled by keyboard at all.\n *\n * Tailwind 4.3.3 ships `scrollbar-width` and `scrollbar-color` utilities\n * (verified in the installed dist, not assumed), which gets the same look\n * in three classes and no JavaScript.\n *\n * WHAT CANNOT BE STYLED, stated rather than glossed: `scrollbar-color`\n * takes a thumb and a track colour and nothing else -- no width in pixels,\n * no radius, no hover state, no margins. `::-webkit-scrollbar` can do\n * those but is non-standard and Firefox has never supported it. This\n * component uses the standard properties only.\n */\nexport const scrollAreaVariants = cva([\n \"overflow-y-auto overscroll-contain\",\n \"box-border\",\n // Standard scrollbar properties. thumb = --color-border (5.25:1 on the\n // page), track transparent so it does not draw a channel.\n \"scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent\",\n // A scrollable region must be reachable by keyboard, or its content is\n // unreachable without a pointer -- WCAG 2.1.1, and axe's\n // scrollable-region-focusable rule. The legacy has no tabIndex at all,\n // so all 17 of its scroll areas are keyboard traps in the other\n // direction: you can see the content but never scroll to it.\n \"outline-none\",\n \"focus-visible:outline-solid\",\n \"focus-visible:outline-(length:--focus-ring-width)\",\n \"focus-visible:outline-offset-(--focus-ring-offset)\",\n \"focus-visible:outline-(--color-ring)\",\n]);\n\n/**\n * A scrolling region.\n *\n * @example\n * <ScrollArea label=\"Chat messages\" className=\"h-64\">...</ScrollArea>\n */\nexport interface ScrollAreaProps extends ComponentPropsWithoutRef<\"div\"> {\n children?: ReactNode;\n /**\n * Names the scrolling region.\n *\n * **Required**, and it took a failing test to get here. A focusable\n * element with no role fails axe `focus-order-semantics`, so the\n * keyboard fix below cannot ship without a role -- and a role with no\n * accessible name is a violation of its own. Naming it is the only\n * combination that is actually correct.\n *\n * `role=\"region\"` specifically. `group` was the first attempt -- it is\n * lighter, since region is a landmark and seventeen landmarks in one app\n * is noise -- but axe does not accept `group` as an appropriate role for\n * a focusable element, and it is right: a named region is the role the\n * ARIA practices name for a scrollable content area, and being able to\n * jump to it is the point.\n */\n label: string;\n /**\n * Change this when the contents change, and the region scrolls back to the\n * top.\n *\n * This stays a declarative prop even though a `ref` now exists: reaching\n * for it would mean the caller writes `ref.current.scrollTop = 0` in their\n * own effect on every content change, which is worse ergonomics than a\n * prop that says \"reset when this value changes.\" That is a design choice\n * independent of whether a ref is available, not a consequence of one\n * being withheld.\n *\n * Sheet forwards its own `scrollResetKey` straight through: going a level\n * deeper in a panel and landing halfway down the new view is disorienting,\n * and it is exactly the thing every call site forgets.\n */\n scrollResetKey?: string | number;\n}\n\nexport const ScrollArea = forwardRef<HTMLDivElement, ScrollAreaProps>(function ScrollArea(\n { className, children, label, scrollResetKey, ...props },\n ref,\n) {\n const region = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n // Unconditional rather than guarded on scrollResetKey being set: with no\n // key this runs once on mount, where scrollTop is already 0.\n if (region.current) region.current.scrollTop = 0;\n }, [scrollResetKey]);\n\n // Memoized, not called inline on the div below: mergeRefs returns a new\n // callback every call, and an inline call is a new callback every render --\n // React would detach and reattach the DOM ref on every re-render instead of\n // only on mount, which matters here more than most, since ScrollArea's own\n // purpose is wrapping content that re-renders often.\n const rootRef = useMemo(() => mergeRefs(ref, region), [ref, region]);\n\n return (\n <div\n {...props}\n ref={rootRef}\n // tabIndex 0 unconditionally: whether the content overflows is a\n // runtime fact that changes as the content does, and a region that\n // becomes scrollable after a fetch would otherwise never gain focus.\n tabIndex={0}\n role=\"region\"\n aria-label={label}\n data-slot=\"scroll-area\"\n className={cn(scrollAreaVariants(), className)}\n >\n {children}\n </div>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,MAAa,qBAAqB,IAAI;CACpC;CACA;CAGA;CAMA;CACA;CACA;CACA;CACA;AACF,CAAC;AA6CD,MAAa,aAAa,WAA4C,SAAS,WAC7E,EAAE,WAAW,UAAU,OAAO,gBAAgB,GAAG,SACjD,KACA;CACA,MAAM,SAAS,OAAuB,IAAI;CAE1C,gBAAgB;EAGd,IAAI,OAAO,SAAS,OAAO,QAAQ,YAAY;CACjD,GAAG,CAAC,cAAc,CAAC;CAOnB,MAAM,UAAU,cAAc,UAAU,KAAK,MAAM,GAAG,CAAC,KAAK,MAAM,CAAC;CAEnE,OACE,oBAAC,OAAD;EACE,GAAI;EACJ,KAAK;EAIL,UAAU;EACV,MAAK;EACL,cAAY;EACZ,aAAU;EACV,WAAW,GAAG,mBAAmB,GAAG,SAAS;EAE5C;CACE,CAAA;AAET,CAAC"}
1
+ {"version":3,"file":"scroll-area.js","names":[],"sources":["../../src/scroll-area/scroll-area.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport {\n forwardRef,\n useEffect,\n useMemo,\n useRef,\n type ComponentPropsWithoutRef,\n type ReactNode,\n} from \"react\";\n\nimport { cnAll } from \"../lib/cn.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\n\n/**\n * ScrollArea. React Aria has no equivalent, so this is a plain scrolling\n * div -- and that is the honest answer rather than a shortcut.\n *\n * Census: fourteen paths match, thirteen are counted, ELEVEN render\n * anything, and across them 17 uses. Every single one passes `className`\n * and nothing else, and 13 of the 17 are a height or flex constraint plus\n * padding. `ScrollBar` is exported by the legacy and imported by nobody.\n * `orientation=\"horizontal\"` has zero call sites.\n *\n * So there is no styled-scrollbar requirement to port. The legacy pays for\n * one anyway: Radix injects a `<style>` tag that hides the native\n * scrollbar, then re-draws it in JavaScript with a ResizeObserver, a\n * portal-free overlay and a `display: table` content wrapper. That is a\n * lot of machinery to reproduce a scrollbar, and it is why the legacy\n * `ScrollArea` cannot be scrolled by keyboard at all.\n *\n * Tailwind 4.3.3 ships `scrollbar-width` and `scrollbar-color` utilities\n * (verified in the installed dist, not assumed), which gets the same look\n * in three classes and no JavaScript.\n *\n * WHAT CANNOT BE STYLED, stated rather than glossed: `scrollbar-color`\n * takes a thumb and a track colour and nothing else -- no width in pixels,\n * no radius, no hover state, no margins. `::-webkit-scrollbar` can do\n * those but is non-standard and Firefox has never supported it. This\n * component uses the standard properties only.\n */\nexport const scrollAreaVariants = cva([\n \"overflow-y-auto overscroll-contain\",\n \"box-border\",\n // Standard scrollbar properties. thumb = --color-border (5.25:1 on the\n // page), track transparent so it does not draw a channel.\n \"scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent\",\n // A scrollable region must be reachable by keyboard, or its content is\n // unreachable without a pointer -- WCAG 2.1.1, and axe's\n // scrollable-region-focusable rule. The legacy has no tabIndex at all,\n // so all 17 of its scroll areas are keyboard traps in the other\n // direction: you can see the content but never scroll to it.\n \"outline-none\",\n \"focus-visible:outline-solid\",\n \"focus-visible:outline-(length:--focus-ring-width)\",\n \"focus-visible:outline-offset-(--focus-ring-offset)\",\n \"focus-visible:outline-(--color-ring)\",\n]);\n\n/**\n * A scrolling region.\n *\n * @example\n * <ScrollArea label=\"Chat messages\" className=\"h-64\">...</ScrollArea>\n */\nexport interface ScrollAreaProps extends ComponentPropsWithoutRef<\"div\"> {\n children?: ReactNode;\n /**\n * Names the scrolling region.\n *\n * **Required**, and it took a failing test to get here. A focusable\n * element with no role fails axe `focus-order-semantics`, so the\n * keyboard fix below cannot ship without a role -- and a role with no\n * accessible name is a violation of its own. Naming it is the only\n * combination that is actually correct.\n *\n * `role=\"region\"` specifically. `group` was the first attempt -- it is\n * lighter, since region is a landmark and seventeen landmarks in one app\n * is noise -- but axe does not accept `group` as an appropriate role for\n * a focusable element, and it is right: a named region is the role the\n * ARIA practices name for a scrollable content area, and being able to\n * jump to it is the point.\n */\n label: string;\n /**\n * Fade the content out at an edge by exactly as much as there is content hidden\n * past it, up to this many pixels: the fade grows in with the first pixels of\n * scroll and is gone at the ends. `0` turns it off, and the default leaves the\n * area as it was. Written on each scroll frame straight to the element, so\n * scrolling never re-renders.\n *\n * @default 0\n */\n fade?: number;\n /**\n * Change this when the contents change, and the region scrolls back to the\n * top.\n *\n * This stays a declarative prop even though a `ref` now exists: reaching\n * for it would mean the caller writes `ref.current.scrollTop = 0` in their\n * own effect on every content change, which is worse ergonomics than a\n * prop that says \"reset when this value changes.\" That is a design choice\n * independent of whether a ref is available, not a consequence of one\n * being withheld.\n *\n * Sheet forwards its own `scrollResetKey` straight through: going a level\n * deeper in a panel and landing halfway down the new view is disorienting,\n * and it is exactly the thing every call site forgets.\n */\n scrollResetKey?: string | number;\n}\n\nexport const ScrollArea = forwardRef<HTMLDivElement, ScrollAreaProps>(function ScrollArea(\n { className, children, label, scrollResetKey, fade = 0, onScroll, ...props },\n ref,\n) {\n const region = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n // Unconditional rather than guarded on scrollResetKey being set: with no\n // key this runs once on mount, where scrollTop is already 0.\n if (region.current) region.current.scrollTop = 0;\n }, [scrollResetKey]);\n\n // Memoized, not called inline on the div below: mergeRefs returns a new\n // callback every call, and an inline call is a new callback every render --\n // React would detach and reattach the DOM ref on every re-render instead of\n // only on mount, which matters here more than most, since ScrollArea's own\n // purpose is wrapping content that re-renders often.\n const rootRef = useMemo(() => mergeRefs(ref, region), [ref, region]);\n\n // The fade at each edge is how much content is hidden past it, up to `fade`.\n const syncFade = () => {\n const node = region.current;\n if (!node || fade <= 0) return;\n const max = Math.max(0, node.scrollHeight - node.clientHeight);\n node.style.setProperty(\"--fade-top\", `${Math.min(fade, node.scrollTop)}px`);\n node.style.setProperty(\"--fade-bottom\", `${Math.min(fade, max - node.scrollTop)}px`);\n };\n useEffect(() => {\n const node = region.current;\n if (!node || fade <= 0) return;\n syncFade();\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(syncFade);\n observer.observe(node);\n for (const child of Array.from(node.children)) observer.observe(child);\n return () => observer.disconnect();\n // syncFade only reads `fade` and the node, both covered here.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [fade, children]);\n\n return (\n <div\n {...props}\n ref={rootRef}\n // tabIndex 0 unconditionally: whether the content overflows is a\n // runtime fact that changes as the content does, and a region that\n // becomes scrollable after a fetch would otherwise never gain focus.\n tabIndex={0}\n role=\"region\"\n aria-label={label}\n data-slot=\"scroll-area\"\n data-fade={fade > 0 ? \"\" : undefined}\n onScroll={(event) => {\n syncFade();\n onScroll?.(event);\n }}\n className={cnAll(scrollAreaVariants(), fade > 0 && \"scroll-fade-y\", className)}\n >\n {children}\n </div>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,MAAa,qBAAqB,IAAI;CACpC;CACA;CAGA;CAMA;CACA;CACA;CACA;CACA;AACF,CAAC;AAuDD,MAAa,aAAa,WAA4C,SAAS,WAC7E,EAAE,WAAW,UAAU,OAAO,gBAAgB,OAAO,GAAG,UAAU,GAAG,SACrE,KACA;CACA,MAAM,SAAS,OAAuB,IAAI;CAE1C,gBAAgB;EAGd,IAAI,OAAO,SAAS,OAAO,QAAQ,YAAY;CACjD,GAAG,CAAC,cAAc,CAAC;CAOnB,MAAM,UAAU,cAAc,UAAU,KAAK,MAAM,GAAG,CAAC,KAAK,MAAM,CAAC;CAGnE,MAAM,iBAAiB;EACrB,MAAM,OAAO,OAAO;EACpB,IAAI,CAAC,QAAQ,QAAQ,GAAG;EACxB,MAAM,MAAM,KAAK,IAAI,GAAG,KAAK,eAAe,KAAK,YAAY;EAC7D,KAAK,MAAM,YAAY,cAAc,GAAG,KAAK,IAAI,MAAM,KAAK,SAAS,EAAE,GAAG;EAC1E,KAAK,MAAM,YAAY,iBAAiB,GAAG,KAAK,IAAI,MAAM,MAAM,KAAK,SAAS,EAAE,GAAG;CACrF;CACA,gBAAgB;EACd,MAAM,OAAO,OAAO;EACpB,IAAI,CAAC,QAAQ,QAAQ,GAAG;EACxB,SAAS;EACT,IAAI,OAAO,mBAAmB,aAAa;EAC3C,MAAM,WAAW,IAAI,eAAe,QAAQ;EAC5C,SAAS,QAAQ,IAAI;EACrB,KAAK,MAAM,SAAS,MAAM,KAAK,KAAK,QAAQ,GAAG,SAAS,QAAQ,KAAK;EACrE,aAAa,SAAS,WAAW;CAGnC,GAAG,CAAC,MAAM,QAAQ,CAAC;CAEnB,OACE,oBAAC,OAAD;EACE,GAAI;EACJ,KAAK;EAIL,UAAU;EACV,MAAK;EACL,cAAY;EACZ,aAAU;EACV,aAAW,OAAO,IAAI,KAAK,KAAA;EAC3B,WAAW,UAAU;GACnB,SAAS;GACT,WAAW,KAAK;EAClB;EACA,WAAW,MAAM,mBAAmB,GAAG,OAAO,KAAK,iBAAiB,SAAS;EAE5E;CACE,CAAA;AAET,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { SearchField, SearchFieldProps } from "./search-field.js";
2
+ export { SearchField, type SearchFieldProps };
@@ -0,0 +1,2 @@
1
+ import { SearchField } from "./search-field.js";
2
+ export { SearchField };
@@ -0,0 +1,26 @@
1
+ import { InputProps } from "react-aria-components";
2
+ //#region src/search-field/search-field.d.ts
3
+ export interface SearchFieldProps extends Omit<InputProps, "className" | "style" | "size" | "type" | "value" | "defaultValue" | "onChange"> {
4
+ /** Extra classes for the input. Merged last, so they win conflicts with ours. */
5
+ className?: string;
6
+ /** The visible label, tied to the input with `htmlFor`. */
7
+ label: string;
8
+ /** The current text. Controlled: the field has no state of its own. */
9
+ value: string;
10
+ /** Called with the new text on every edit, and with `""` when the clear button is pressed. */
11
+ onValueChange: (value: string) => void;
12
+ /** The clear button's accessible name. @default "Clear search" */
13
+ clearLabel?: string;
14
+ }
15
+ /**
16
+ * A search input with a clear button. The button has a reserved box, so the
17
+ * field never changes width as it comes and goes; pressing it clears the text
18
+ * and returns focus to the input. `onChange` is not accepted: use `onValueChange`.
19
+ * Native input props apply to the input, and `disabled` also disables the button.
20
+ *
21
+ * @example
22
+ * <SearchField label="Search contacts" value={query} onValueChange={setQuery} />
23
+ */
24
+ export declare const SearchField: import("react").ForwardRefExoticComponent<SearchFieldProps & import("react").RefAttributes<HTMLInputElement>>;
25
+ //#endregion
26
+ //# sourceMappingURL=search-field.d.ts.map