@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
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/cn.js";
3
3
  import { fieldSurfaceVariants } from "../lib/field-surface.js";
4
+ import { FieldShell, hasFieldProps, useFieldWiring } from "../lib/field-shell.js";
4
5
  import { cva } from "class-variance-authority";
5
6
  import { forwardRef } from "react";
6
7
  import { Input } from "react-aria-components";
@@ -30,7 +31,6 @@ import { jsx, jsxs } from "react/jsx-runtime";
30
31
  */
31
32
  const inputVariants = cva([
32
33
  fieldSurfaceVariants(),
33
- "text-base",
34
34
  "file:inline-flex file:border-0 file:bg-transparent file:text-sm file:font-medium",
35
35
  "file:text-foreground"
36
36
  ], {
@@ -42,16 +42,21 @@ const inputVariants = cva([
42
42
  * second most common at 167 call sites, and while the field was locked
43
43
  * to `md` the pair was 10px out of alignment with no supported fix.
44
44
  *
45
- * Type does NOT scale, which is the one place this axis deliberately
46
- * departs from Button's. iOS Safari zooms the viewport when a focused
47
- * field's text is under 16px, so `text-base` stays pinned in the base
48
- * at every rung -- see the note there. Shrinking the box is a layout
49
- * choice; shrinking the type is a bug on a phone.
45
+ * Type follows Button's sm rung, with one guard. `sm` is 14px, the
46
+ * size of the body text a small field sits among (Button's sm label is
47
+ * text-sm for the same reason). `md` and `lg` stay 16px.
48
+ *
49
+ * The guard: iOS Safari zooms the viewport when a focused field's text
50
+ * is under 16px, so a coarse pointer (a touch screen) keeps 16px on
51
+ * `sm` too. The switch is the input device, not the viewport width:
52
+ * the legacy `text-base md:text-sm` changed size at a breakpoint that
53
+ * has nothing to do with zoom, so a narrow desktop window got 16px and
54
+ * a tablet in landscape got 14px and zoomed.
50
55
  */
51
56
  size: {
52
- sm: "h-(--density-control-h-sm) px-(--density-control-pad-sm)",
53
- md: "h-(--density-control-h-md) px-(--density-control-pad-md)",
54
- lg: "h-(--density-control-h-lg) px-(--density-control-pad-lg)"
57
+ sm: "h-(--density-control-h-sm) px-(--density-control-pad-sm) text-sm pointer-coarse:text-base",
58
+ md: "h-(--density-control-h-md) px-(--density-control-pad-md) text-base",
59
+ lg: "h-(--density-control-h-lg) px-(--density-control-pad-lg) text-base"
55
60
  },
56
61
  hasStartIcon: { true: "" },
57
62
  hasEndIcon: { true: "" }
@@ -155,10 +160,20 @@ const affixVariants = cva([
155
160
  ],
156
161
  defaultVariants: { size: "md" }
157
162
  });
158
- const Input$1 = forwardRef(function Input$2({ className, size = "md", startIcon, endIcon, ...props }, ref) {
163
+ const Input$1 = forwardRef(function Input$2(allProps, ref) {
164
+ const { className, size = "md", startIcon, endIcon, label, description, error, ...props } = allProps;
165
+ const field = useFieldWiring({
166
+ enabled: hasFieldProps(allProps),
167
+ id: props.id,
168
+ describedBy: props["aria-describedby"],
169
+ invalid: props["aria-invalid"],
170
+ description,
171
+ error
172
+ });
159
173
  const input = /* @__PURE__ */ jsx(Input, {
160
174
  ref,
161
175
  ...props,
176
+ ...field.controlProps,
162
177
  "data-slot": "input",
163
178
  className: cn(inputVariants({
164
179
  size,
@@ -166,8 +181,7 @@ const Input$1 = forwardRef(function Input$2({ className, size = "md", startIcon,
166
181
  hasEndIcon: endIcon !== void 0
167
182
  }), className)
168
183
  });
169
- if (startIcon === void 0 && endIcon === void 0) return input;
170
- return /* @__PURE__ */ jsxs("span", {
184
+ const control = startIcon === void 0 && endIcon === void 0 ? input : /* @__PURE__ */ jsxs("span", {
171
185
  "data-slot": "input-wrapper",
172
186
  className: "relative block w-full",
173
187
  children: [
@@ -190,6 +204,16 @@ const Input$1 = forwardRef(function Input$2({ className, size = "md", startIcon,
190
204
  })
191
205
  ]
192
206
  });
207
+ if (!field.active) return control;
208
+ return /* @__PURE__ */ jsx(FieldShell, {
209
+ label,
210
+ controlId: field.controlId,
211
+ description,
212
+ hintId: field.hintId,
213
+ error,
214
+ errorId: field.errorId,
215
+ children: control
216
+ });
193
217
  });
194
218
  //#endregion
195
219
  export { Input$1 as Input, inputVariants };
@@ -1 +1 @@
1
- {"version":3,"file":"input.js","names":["Input","AriaInput"],"sources":["../../src/input/input.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport { Input as AriaInput, type InputProps as AriaInputProps } from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { fieldSurfaceVariants } from \"../lib/field-surface.js\";\n\n/**\n * Input, ported from the legacy shadcn component onto React Aria's Input.\n *\n * Census: 25 files, 51 uses -- the busiest control in the app after Button.\n * Props are `onChange` 36, `value` 35, `placeholder` 33, `id` 23, `type` 21.\n *\n * EVERY ONE OF THOSE 51 CALL SITES MIGRATES UNCHANGED. React Aria's `Input`\n * takes NATIVE props -- `disabled`, `aria-invalid`, `onChange` with a real\n * event -- not the `isDisabled`/`onChange(value)` shape the rest of React\n * Aria uses. It reads `InputContext` when one is present, so the same\n * component is a bare styled input today and composes inside a `TextField`\n * the moment one is wrapped around it. There is no rename to schedule.\n *\n * That is also why this is not `TextField`. Wrapping 51 call sites in a\n * field they do not have, to get a `onChange(value: string)` they did not\n * ask for, would be the migration doing work on the app's behalf.\n *\n * `border-input` becomes `border-border`: in the legacy theme `--input` is\n * character-for-character `--border` on light (`oklch(0.922 0 0)`), and the\n * dark values differ only in alpha. A fourth token role that turns out to\n * be an existing one, after `popover`, `accent` and the scrim.\n */\nexport const inputVariants = cva(\n [\n fieldSurfaceVariants(),\n\n // text-base always, not the legacy `text-base md:text-sm`. 16px is the\n // threshold below which iOS Safari zooms the viewport on focus, and the\n // legacy dropped to 14px at exactly the widths where that stops being a\n // phone -- which reads as a deliberate fix but leaves the field a\n // different size on either side of a breakpoint for no visual reason.\n \"text-base\",\n\n // type=\"file\" has one call site, which is not zero. Enough to keep it\n // from rendering as an unstyled user-agent button.\n \"file:inline-flex file:border-0 file:bg-transparent file:text-sm file:font-medium\",\n \"file:text-foreground\",\n ],\n {\n variants: {\n /**\n * Height and inline padding from the control scale, so `data-density`\n * moves a whole form at once. Same three names and the same tokens as\n * Button, because they sit in the same row: Button's `sm` is the app's\n * second most common at 167 call sites, and while the field was locked\n * to `md` the pair was 10px out of alignment with no supported fix.\n *\n * Type does NOT scale, which is the one place this axis deliberately\n * departs from Button's. iOS Safari zooms the viewport when a focused\n * field's text is under 16px, so `text-base` stays pinned in the base\n * at every rung -- see the note there. Shrinking the box is a layout\n * choice; shrinking the type is a bug on a phone.\n */\n size: {\n sm: \"h-(--density-control-h-sm) px-(--density-control-pad-sm)\",\n md: \"h-(--density-control-h-md) px-(--density-control-pad-md)\",\n lg: \"h-(--density-control-h-lg) px-(--density-control-pad-lg)\",\n },\n // Declared empty and filled by the compound variants below: the affix\n // inset depends on BOTH whether a slot is used and which rung it is on.\n hasStartIcon: { true: \"\" },\n hasEndIcon: { true: \"\" },\n },\n compoundVariants: [\n // The affix slots float ABOVE the field rather than sitting beside it\n // in a flex row, so the only thing keeping the value from running\n // underneath a glyph is this padding. It is the same three tokens that\n // place the glyph -- inline padding, icon box, control gap -- so the\n // text and the glyph move together across density AND size. A\n // hand-tuned `pl-9` at the call site can do neither, and the product\n // app has eighteen of those.\n {\n size: \"sm\",\n hasStartIcon: true,\n class:\n \"ps-[calc(var(--density-control-pad-sm)_+_var(--icon-sm)_+_var(--density-control-gap))]\",\n },\n {\n size: \"md\",\n hasStartIcon: true,\n class:\n \"ps-[calc(var(--density-control-pad-md)_+_var(--icon-sm)_+_var(--density-control-gap))]\",\n },\n {\n size: \"lg\",\n hasStartIcon: true,\n class:\n \"ps-[calc(var(--density-control-pad-lg)_+_var(--icon-sm)_+_var(--density-control-gap))]\",\n },\n {\n size: \"sm\",\n hasEndIcon: true,\n class:\n \"pe-[calc(var(--density-control-pad-sm)_+_var(--icon-sm)_+_var(--density-control-gap))]\",\n },\n {\n size: \"md\",\n hasEndIcon: true,\n class:\n \"pe-[calc(var(--density-control-pad-md)_+_var(--icon-sm)_+_var(--density-control-gap))]\",\n },\n {\n size: \"lg\",\n hasEndIcon: true,\n class:\n \"pe-[calc(var(--density-control-pad-lg)_+_var(--icon-sm)_+_var(--density-control-gap))]\",\n },\n ],\n defaultVariants: { size: \"md\" },\n },\n);\n\n/**\n * An affix slot.\n *\n * `pointer-events-none` is load-bearing, not cosmetic: the slot is painted on\n * top of the field, so without it the glyph swallows the click that was meant\n * to focus the input. Which also fixes the slot's scope -- these are\n * DECORATIVE marks. An interactive trailing control (a password reveal, a\n * clear button) needs its own pointer events and its own accessible name, and\n * the census found no call site for one, so it is not built here.\n *\n * Logical properties throughout. The product app's eighteen hand-rolled\n * adornments are all `absolute left-*`, with `absolute start-*` at zero\n * occurrences, so every one of them puts the glyph on the wrong side under\n * RTL and lets the text run over it.\n */\nconst affixVariants = cva(\n [\n \"pointer-events-none absolute inset-y-0 flex items-center\",\n \"text-muted-foreground\",\n // Sized like Button's icon slots, and skipped when the caller sized the\n // glyph themselves. An svg with neither a width attribute nor a resolved\n // class renders at 0x0 and every position assertion still passes.\n \"[&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n ],\n {\n variants: {\n side: { start: \"\", end: \"\" },\n size: { sm: \"\", md: \"\", lg: \"\" },\n },\n // The glyph's inset is the field's own inline padding, so it lines up with\n // where the value would have started. That makes it a pair of axes, not\n // one: a small field at the medium inset puts the glyph under the text.\n compoundVariants: [\n { side: \"start\", size: \"sm\", class: \"start-(--density-control-pad-sm)\" },\n { side: \"start\", size: \"md\", class: \"start-(--density-control-pad-md)\" },\n { side: \"start\", size: \"lg\", class: \"start-(--density-control-pad-lg)\" },\n { side: \"end\", size: \"sm\", class: \"end-(--density-control-pad-sm)\" },\n { side: \"end\", size: \"md\", class: \"end-(--density-control-pad-md)\" },\n { side: \"end\", size: \"lg\", class: \"end-(--density-control-pad-lg)\" },\n ],\n defaultVariants: { size: \"md\" },\n },\n);\n\n/**\n * A single-line text field.\n *\n * @example\n * <Input type=\"email\" placeholder=\"you@example.com\" value={v} onChange={e => set(e.target.value)} />\n * <Input aria-label=\"Search\" startIcon={<SearchIcon aria-hidden />} />\n */\nexport interface InputProps extends Omit<AriaInputProps, \"className\" | \"style\" | \"size\"> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n /**\n * Control scale. Height and inline padding come from the same\n * `--density-control-*` tokens as Button, so a field and a button on one\n * rung are the same height.\n *\n * Type does not scale with it: the field stays at 16px everywhere, because\n * iOS Safari zooms the viewport when a focused input's text is smaller.\n *\n * This REPLACES the native `size` attribute, which React Aria inherits from\n * `InputHTMLAttributes` as a character count. No call site in the census\n * used it; set a width with `className` instead.\n *\n * @default \"md\"\n */\n size?: \"sm\" | \"md\" | \"lg\";\n /**\n * Decorative glyph at the leading edge of the field.\n *\n * Mark it `aria-hidden` -- the field's label carries the accessible name,\n * and a second name source only makes it announce twice. It is sized at\n * `--icon-sm` unless it carries a `size-*` class of its own, positioned\n * from `--density-control-pad-md`, and the field's leading padding grows to\n * match so the value cannot run underneath it.\n *\n * The slot is `pointer-events: none`, so a click on the glyph focuses the\n * field. That makes it decorative by construction: an interactive\n * adornment -- a password reveal, a clear button -- is NOT this prop.\n *\n * Two named slots rather than one `icon` plus a position prop, matching\n * Button: a search field can legitimately carry a leading glyph AND a\n * trailing unit, which one slot cannot express.\n */\n startIcon?: ReactNode;\n /** Decorative glyph at the trailing edge. Same rules as {@link InputProps.startIcon}. */\n endIcon?: ReactNode;\n}\n\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n { className, size = \"md\", startIcon, endIcon, ...props },\n ref,\n) {\n const input = (\n <AriaInput\n // The ref goes on the INPUT, never on the wrapper span below. A ref\n // pointing at the wrapper is still non-null, so it satisfies the obvious\n // assertion while breaking `.focus()`, `.select()` and every\n // react-hook-form site -- but only at the call sites that pass an icon.\n // RefLandsOnTheInputNotTheWrapper is the guard.\n ref={ref}\n {...props}\n data-slot=\"input\"\n className={cn(\n inputVariants({\n size,\n hasStartIcon: startIcon !== undefined,\n hasEndIcon: endIcon !== undefined,\n }),\n className,\n )}\n />\n );\n\n // With neither slot used there is no wrapper AT ALL, so the DOM is\n // byte-identical to what shipped before this prop existed. That is what\n // keeps this additive for the 97 call sites that pass no glyph, and\n // NoIconsLeaveDomUnchanged is the assertion holding it true.\n if (startIcon === undefined && endIcon === undefined) return input;\n\n // An overlay, not a flex row. Moving the border onto this wrapper would\n // take the painted surface off [data-slot=\"input\"] -- the selector the\n // escape hatch is built on -- and would force the focus ring onto\n // :focus-within and the invalid border onto :has(), when React Aria\n // already puts data-focus-visible and data-invalid on the input itself.\n return (\n <span data-slot=\"input-wrapper\" className=\"relative block w-full\">\n {input}\n {startIcon === undefined ? null : (\n <span data-slot=\"input-start-icon\" className={affixVariants({ side: \"start\", size })}>\n {startIcon}\n </span>\n )}\n {endIcon === undefined ? null : (\n <span data-slot=\"input-end-icon\" className={affixVariants({ side: \"end\", size })}>\n {endIcon}\n </span>\n )}\n </span>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,MAAa,gBAAgB,IAC3B;CACE,qBAAqB;CAOrB;CAIA;CACA;AACF,GACA;CACE,UAAU;;;;;;;;;;;;;;EAcR,MAAM;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN;EAGA,cAAc,EAAE,MAAM,GAAG;EACzB,YAAY,EAAE,MAAM,GAAG;CACzB;CACA,kBAAkB;EAQhB;GACE,MAAM;GACN,cAAc;GACd,OACE;EACJ;EACA;GACE,MAAM;GACN,cAAc;GACd,OACE;EACJ;EACA;GACE,MAAM;GACN,cAAc;GACd,OACE;EACJ;EACA;GACE,MAAM;GACN,YAAY;GACZ,OACE;EACJ;EACA;GACE,MAAM;GACN,YAAY;GACZ,OACE;EACJ;EACA;GACE,MAAM;GACN,YAAY;GACZ,OACE;EACJ;CACF;CACA,iBAAiB,EAAE,MAAM,KAAK;AAChC,CACF;;;;;;;;;;;;;;;;AAiBA,MAAM,gBAAgB,IACpB;CACE;CACA;CAIA;AACF,GACA;CACE,UAAU;EACR,MAAM;GAAE,OAAO;GAAI,KAAK;EAAG;EAC3B,MAAM;GAAE,IAAI;GAAI,IAAI;GAAI,IAAI;EAAG;CACjC;CAIA,kBAAkB;EAChB;GAAE,MAAM;GAAS,MAAM;GAAM,OAAO;EAAmC;EACvE;GAAE,MAAM;GAAS,MAAM;GAAM,OAAO;EAAmC;EACvE;GAAE,MAAM;GAAS,MAAM;GAAM,OAAO;EAAmC;EACvE;GAAE,MAAM;GAAO,MAAM;GAAM,OAAO;EAAiC;EACnE;GAAE,MAAM;GAAO,MAAM;GAAM,OAAO;EAAiC;EACnE;GAAE,MAAM;GAAO,MAAM;GAAM,OAAO;EAAiC;CACrE;CACA,iBAAiB,EAAE,MAAM,KAAK;AAChC,CACF;AAiDA,MAAaA,UAAQ,WAAyC,SAASA,QACrE,EAAE,WAAW,OAAO,MAAM,WAAW,SAAS,GAAG,SACjD,KACA;CACA,MAAM,QACJ,oBAACC,OAAD;EAMO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GACT,cAAc;GACZ;GACA,cAAc,cAAc,KAAA;GAC5B,YAAY,YAAY,KAAA;EAC1B,CAAC,GACD,SACF;CACD,CAAA;CAOH,IAAI,cAAc,KAAA,KAAa,YAAY,KAAA,GAAW,OAAO;CAO7D,OACE,qBAAC,QAAD;EAAM,aAAU;EAAgB,WAAU;EAA1C,UAAA;GACG;GACA,cAAc,KAAA,IAAY,OACzB,oBAAC,QAAD;IAAM,aAAU;IAAmB,WAAW,cAAc;KAAE,MAAM;KAAS;IAAK,CAAC;IAChF,UAAA;GACG,CAAA;GAEP,YAAY,KAAA,IAAY,OACvB,oBAAC,QAAD;IAAM,aAAU;IAAiB,WAAW,cAAc;KAAE,MAAM;KAAO;IAAK,CAAC;IAC5E,UAAA;GACG,CAAA;EAEJ;;AAEV,CAAC"}
1
+ {"version":3,"file":"input.js","names":["Input","AriaInput"],"sources":["../../src/input/input.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport { Input as AriaInput, type InputProps as AriaInputProps } from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { FieldShell, hasFieldProps, useFieldWiring } from \"../lib/field-shell.js\";\nimport { fieldSurfaceVariants } from \"../lib/field-surface.js\";\n\n/**\n * Input, ported from the legacy shadcn component onto React Aria's Input.\n *\n * Census: 25 files, 51 uses -- the busiest control in the app after Button.\n * Props are `onChange` 36, `value` 35, `placeholder` 33, `id` 23, `type` 21.\n *\n * EVERY ONE OF THOSE 51 CALL SITES MIGRATES UNCHANGED. React Aria's `Input`\n * takes NATIVE props -- `disabled`, `aria-invalid`, `onChange` with a real\n * event -- not the `isDisabled`/`onChange(value)` shape the rest of React\n * Aria uses. It reads `InputContext` when one is present, so the same\n * component is a bare styled input today and composes inside a `TextField`\n * the moment one is wrapped around it. There is no rename to schedule.\n *\n * That is also why this is not `TextField`. Wrapping 51 call sites in a\n * field they do not have, to get a `onChange(value: string)` they did not\n * ask for, would be the migration doing work on the app's behalf.\n *\n * `border-input` becomes `border-border`: in the legacy theme `--input` is\n * character-for-character `--border` on light (`oklch(0.922 0 0)`), and the\n * dark values differ only in alpha. A fourth token role that turns out to\n * be an existing one, after `popover`, `accent` and the scrim.\n */\nexport const inputVariants = cva(\n [\n fieldSurfaceVariants(),\n\n // type=\"file\" has one call site, which is not zero. Enough to keep it\n // from rendering as an unstyled user-agent button.\n \"file:inline-flex file:border-0 file:bg-transparent file:text-sm file:font-medium\",\n \"file:text-foreground\",\n ],\n {\n variants: {\n /**\n * Height and inline padding from the control scale, so `data-density`\n * moves a whole form at once. Same three names and the same tokens as\n * Button, because they sit in the same row: Button's `sm` is the app's\n * second most common at 167 call sites, and while the field was locked\n * to `md` the pair was 10px out of alignment with no supported fix.\n *\n * Type follows Button's sm rung, with one guard. `sm` is 14px, the\n * size of the body text a small field sits among (Button's sm label is\n * text-sm for the same reason). `md` and `lg` stay 16px.\n *\n * The guard: iOS Safari zooms the viewport when a focused field's text\n * is under 16px, so a coarse pointer (a touch screen) keeps 16px on\n * `sm` too. The switch is the input device, not the viewport width:\n * the legacy `text-base md:text-sm` changed size at a breakpoint that\n * has nothing to do with zoom, so a narrow desktop window got 16px and\n * a tablet in landscape got 14px and zoomed.\n */\n size: {\n sm: \"h-(--density-control-h-sm) px-(--density-control-pad-sm) text-sm pointer-coarse:text-base\",\n md: \"h-(--density-control-h-md) px-(--density-control-pad-md) text-base\",\n lg: \"h-(--density-control-h-lg) px-(--density-control-pad-lg) text-base\",\n },\n // Declared empty and filled by the compound variants below: the affix\n // inset depends on BOTH whether a slot is used and which rung it is on.\n hasStartIcon: { true: \"\" },\n hasEndIcon: { true: \"\" },\n },\n compoundVariants: [\n // The affix slots float ABOVE the field rather than sitting beside it\n // in a flex row, so the only thing keeping the value from running\n // underneath a glyph is this padding. It is the same three tokens that\n // place the glyph -- inline padding, icon box, control gap -- so the\n // text and the glyph move together across density AND size. A\n // hand-tuned `pl-9` at the call site can do neither, and the product\n // app has eighteen of those.\n {\n size: \"sm\",\n hasStartIcon: true,\n class:\n \"ps-[calc(var(--density-control-pad-sm)_+_var(--icon-sm)_+_var(--density-control-gap))]\",\n },\n {\n size: \"md\",\n hasStartIcon: true,\n class:\n \"ps-[calc(var(--density-control-pad-md)_+_var(--icon-sm)_+_var(--density-control-gap))]\",\n },\n {\n size: \"lg\",\n hasStartIcon: true,\n class:\n \"ps-[calc(var(--density-control-pad-lg)_+_var(--icon-sm)_+_var(--density-control-gap))]\",\n },\n {\n size: \"sm\",\n hasEndIcon: true,\n class:\n \"pe-[calc(var(--density-control-pad-sm)_+_var(--icon-sm)_+_var(--density-control-gap))]\",\n },\n {\n size: \"md\",\n hasEndIcon: true,\n class:\n \"pe-[calc(var(--density-control-pad-md)_+_var(--icon-sm)_+_var(--density-control-gap))]\",\n },\n {\n size: \"lg\",\n hasEndIcon: true,\n class:\n \"pe-[calc(var(--density-control-pad-lg)_+_var(--icon-sm)_+_var(--density-control-gap))]\",\n },\n ],\n defaultVariants: { size: \"md\" },\n },\n);\n\n/**\n * An affix slot.\n *\n * `pointer-events-none` is load-bearing, not cosmetic: the slot is painted on\n * top of the field, so without it the glyph swallows the click that was meant\n * to focus the input. Which also fixes the slot's scope -- these are\n * DECORATIVE marks. An interactive trailing control (a password reveal, a\n * clear button) needs its own pointer events and its own accessible name, and\n * the census found no call site for one, so it is not built here.\n *\n * Logical properties throughout. The product app's eighteen hand-rolled\n * adornments are all `absolute left-*`, with `absolute start-*` at zero\n * occurrences, so every one of them puts the glyph on the wrong side under\n * RTL and lets the text run over it.\n */\nconst affixVariants = cva(\n [\n \"pointer-events-none absolute inset-y-0 flex items-center\",\n \"text-muted-foreground\",\n // Sized like Button's icon slots, and skipped when the caller sized the\n // glyph themselves. An svg with neither a width attribute nor a resolved\n // class renders at 0x0 and every position assertion still passes.\n \"[&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n ],\n {\n variants: {\n side: { start: \"\", end: \"\" },\n size: { sm: \"\", md: \"\", lg: \"\" },\n },\n // The glyph's inset is the field's own inline padding, so it lines up with\n // where the value would have started. That makes it a pair of axes, not\n // one: a small field at the medium inset puts the glyph under the text.\n compoundVariants: [\n { side: \"start\", size: \"sm\", class: \"start-(--density-control-pad-sm)\" },\n { side: \"start\", size: \"md\", class: \"start-(--density-control-pad-md)\" },\n { side: \"start\", size: \"lg\", class: \"start-(--density-control-pad-lg)\" },\n { side: \"end\", size: \"sm\", class: \"end-(--density-control-pad-sm)\" },\n { side: \"end\", size: \"md\", class: \"end-(--density-control-pad-md)\" },\n { side: \"end\", size: \"lg\", class: \"end-(--density-control-pad-lg)\" },\n ],\n defaultVariants: { size: \"md\" },\n },\n);\n\n/**\n * A single-line text field.\n *\n * @example\n * <Input type=\"email\" placeholder=\"you@example.com\" value={v} onChange={e => set(e.target.value)} />\n * <Input aria-label=\"Search\" startIcon={<SearchIcon aria-hidden />} />\n */\nexport interface InputProps extends Omit<AriaInputProps, \"className\" | \"style\" | \"size\"> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n /**\n * Control scale. Height and inline padding come from the same\n * `--density-control-*` tokens as Button, so a field and a button on one\n * rung are the same height.\n *\n * Type does not scale with it: the field stays at 16px everywhere, because\n * iOS Safari zooms the viewport when a focused input's text is smaller.\n *\n * This REPLACES the native `size` attribute, which React Aria inherits from\n * `InputHTMLAttributes` as a character count. No call site in the census\n * used it; set a width with `className` instead.\n *\n * @default \"md\"\n */\n size?: \"sm\" | \"md\" | \"lg\";\n /**\n * Decorative glyph at the leading edge of the field.\n *\n * Mark it `aria-hidden` -- the field's label carries the accessible name,\n * and a second name source only makes it announce twice. It is sized at\n * `--icon-sm` unless it carries a `size-*` class of its own, positioned\n * from `--density-control-pad-md`, and the field's leading padding grows to\n * match so the value cannot run underneath it.\n *\n * The slot is `pointer-events: none`, so a click on the glyph focuses the\n * field. That makes it decorative by construction: an interactive\n * adornment -- a password reveal, a clear button -- is NOT this prop.\n *\n * Two named slots rather than one `icon` plus a position prop, matching\n * Button: a search field can legitimately carry a leading glyph AND a\n * trailing unit, which one slot cannot express.\n */\n startIcon?: ReactNode;\n /** Decorative glyph at the trailing edge. Same rules as {@link InputProps.startIcon}. */\n endIcon?: ReactNode;\n /**\n * A visible label above the field, tied to it with `htmlFor`. Passing any of\n * `label`, `description` or `error` (even as `undefined`) wraps the field;\n * with none of them the DOM is exactly the bare input. Do not also pass\n * `aria-label`: it wins over the label, so the visible name is not the\n * accessible one (WCAG 2.5.3). Ignored inside a React Aria `TextField`,\n * which supplies its own label and messages.\n */\n label?: ReactNode;\n /**\n * Helper text under the field, named by `aria-describedby`. It opens on a\n * spring when it appears after mount, changed words rise in and numbers roll\n * digit by digit; nothing animates on first render, and reduced motion swaps\n * it instantly. A string, because the copy is animated word by word.\n */\n description?: string;\n /**\n * Error text under the field. Sets `aria-invalid`, so the field takes its\n * invalid border, and is announced as an alert when it appears. Opens and\n * closes like `description`. An empty or `undefined` value closes the row.\n */\n error?: string;\n}\n\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(allProps, ref) {\n const {\n className,\n size = \"md\",\n startIcon,\n endIcon,\n label,\n description,\n error,\n ...props\n } = allProps;\n const field = useFieldWiring({\n enabled: hasFieldProps(allProps),\n id: props.id,\n describedBy: props[\"aria-describedby\"],\n invalid: props[\"aria-invalid\"],\n description,\n error,\n });\n const input = (\n <AriaInput\n // The ref goes on the INPUT, never on the wrapper span below. A ref\n // pointing at the wrapper is still non-null, so it satisfies the obvious\n // assertion while breaking `.focus()`, `.select()` and every\n // react-hook-form site -- but only at the call sites that pass an icon.\n // RefLandsOnTheInputNotTheWrapper is the guard.\n ref={ref}\n {...props}\n {...field.controlProps}\n data-slot=\"input\"\n className={cn(\n inputVariants({\n size,\n hasStartIcon: startIcon !== undefined,\n hasEndIcon: endIcon !== undefined,\n }),\n className,\n )}\n />\n );\n\n // With neither slot used there is no wrapper AT ALL, so the DOM is\n // byte-identical to what shipped before this prop existed. That is what\n // keeps this additive for the 97 call sites that pass no glyph, and\n // NoIconsLeaveDomUnchanged is the assertion holding it true.\n // With neither slot used there is no wrapper AT ALL, so the DOM is\n // byte-identical to what shipped before this prop existed. That is what\n // keeps this additive for the 97 call sites that pass no glyph, and\n // NoIconsLeaveDomUnchanged is the assertion holding it true.\n //\n // An overlay, not a flex row. Moving the border onto this wrapper would\n // take the painted surface off [data-slot=\"input\"] -- the selector the\n // escape hatch is built on -- and would force the focus ring onto\n // :focus-within and the invalid border onto :has(), when React Aria\n // already puts data-focus-visible and data-invalid on the input itself.\n const control =\n startIcon === undefined && endIcon === undefined ? (\n input\n ) : (\n <span data-slot=\"input-wrapper\" className=\"relative block w-full\">\n {input}\n {startIcon === undefined ? null : (\n <span data-slot=\"input-start-icon\" className={affixVariants({ side: \"start\", size })}>\n {startIcon}\n </span>\n )}\n {endIcon === undefined ? null : (\n <span data-slot=\"input-end-icon\" className={affixVariants({ side: \"end\", size })}>\n {endIcon}\n </span>\n )}\n </span>\n );\n\n // Without a field prop there is no field wrapper either, and the DOM is the bare input (or the icon wrapper).\n if (!field.active) return control;\n return (\n <FieldShell\n label={label}\n controlId={field.controlId}\n description={description}\n hintId={field.hintId}\n error={error}\n errorId={field.errorId}\n >\n {control}\n </FieldShell>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,MAAa,gBAAgB,IAC3B;CACE,qBAAqB;CAIrB;CACA;AACF,GACA;CACE,UAAU;;;;;;;;;;;;;;;;;;;EAmBR,MAAM;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN;EAGA,cAAc,EAAE,MAAM,GAAG;EACzB,YAAY,EAAE,MAAM,GAAG;CACzB;CACA,kBAAkB;EAQhB;GACE,MAAM;GACN,cAAc;GACd,OACE;EACJ;EACA;GACE,MAAM;GACN,cAAc;GACd,OACE;EACJ;EACA;GACE,MAAM;GACN,cAAc;GACd,OACE;EACJ;EACA;GACE,MAAM;GACN,YAAY;GACZ,OACE;EACJ;EACA;GACE,MAAM;GACN,YAAY;GACZ,OACE;EACJ;EACA;GACE,MAAM;GACN,YAAY;GACZ,OACE;EACJ;CACF;CACA,iBAAiB,EAAE,MAAM,KAAK;AAChC,CACF;;;;;;;;;;;;;;;;AAiBA,MAAM,gBAAgB,IACpB;CACE;CACA;CAIA;AACF,GACA;CACE,UAAU;EACR,MAAM;GAAE,OAAO;GAAI,KAAK;EAAG;EAC3B,MAAM;GAAE,IAAI;GAAI,IAAI;GAAI,IAAI;EAAG;CACjC;CAIA,kBAAkB;EAChB;GAAE,MAAM;GAAS,MAAM;GAAM,OAAO;EAAmC;EACvE;GAAE,MAAM;GAAS,MAAM;GAAM,OAAO;EAAmC;EACvE;GAAE,MAAM;GAAS,MAAM;GAAM,OAAO;EAAmC;EACvE;GAAE,MAAM;GAAO,MAAM;GAAM,OAAO;EAAiC;EACnE;GAAE,MAAM;GAAO,MAAM;GAAM,OAAO;EAAiC;EACnE;GAAE,MAAM;GAAO,MAAM;GAAM,OAAO;EAAiC;CACrE;CACA,iBAAiB,EAAE,MAAM,KAAK;AAChC,CACF;AAuEA,MAAaA,UAAQ,WAAyC,SAASA,QAAM,UAAU,KAAK;CAC1F,MAAM,EACJ,WACA,OAAO,MACP,WACA,SACA,OACA,aACA,OACA,GAAG,UACD;CACJ,MAAM,QAAQ,eAAe;EAC3B,SAAS,cAAc,QAAQ;EAC/B,IAAI,MAAM;EACV,aAAa,MAAM;EACnB,SAAS,MAAM;EACf;EACA;CACF,CAAC;CACD,MAAM,QACJ,oBAACC,OAAD;EAMO;EACL,GAAI;EACJ,GAAI,MAAM;EACV,aAAU;EACV,WAAW,GACT,cAAc;GACZ;GACA,cAAc,cAAc,KAAA;GAC5B,YAAY,YAAY,KAAA;EAC1B,CAAC,GACD,SACF;CACD,CAAA;CAiBH,MAAM,UACJ,cAAc,KAAA,KAAa,YAAY,KAAA,IACrC,QAEA,qBAAC,QAAD;EAAM,aAAU;EAAgB,WAAU;EAA1C,UAAA;GACG;GACA,cAAc,KAAA,IAAY,OACzB,oBAAC,QAAD;IAAM,aAAU;IAAmB,WAAW,cAAc;KAAE,MAAM;KAAS;IAAK,CAAC;IAChF,UAAA;GACG,CAAA;GAEP,YAAY,KAAA,IAAY,OACvB,oBAAC,QAAD;IAAM,aAAU;IAAiB,WAAW,cAAc;KAAE,MAAM;KAAO;IAAK,CAAC;IAC5E,UAAA;GACG,CAAA;EAEJ;;CAIV,IAAI,CAAC,MAAM,QAAQ,OAAO;CAC1B,OACE,oBAAC,YAAD;EACS;EACP,WAAW,MAAM;EACJ;EACb,QAAQ,MAAM;EACP;EACP,SAAS,MAAM;EAEd,UAAA;CACS,CAAA;AAEhB,CAAC"}
@@ -0,0 +1,113 @@
1
+ import { prefersReducedMotion, useIsomorphicLayoutEffect } from "./motion.js";
2
+ import { useEffect, useRef, useState } from "react";
3
+ //#region src/lib/count-to.ts
4
+ /** The smooth spring (visual duration .4s, no bounce) sampled every 16ms, so a count agrees with a CSS transition on `--ease-smooth`. */
5
+ const SMOOTH_CURVE = [
6
+ 0,
7
+ .0191,
8
+ .0667,
9
+ .1313,
10
+ .2048,
11
+ .2816,
12
+ .3577,
13
+ .4308,
14
+ .4991,
15
+ .562,
16
+ .6189,
17
+ .67,
18
+ .7154,
19
+ .7554,
20
+ .7905,
21
+ .821,
22
+ .8475,
23
+ .8704,
24
+ .89,
25
+ .9069,
26
+ .9213,
27
+ .9336,
28
+ .9441,
29
+ .9529,
30
+ .9605,
31
+ .9668,
32
+ .9722,
33
+ .9767,
34
+ .9805,
35
+ .9837,
36
+ .9864,
37
+ .9887,
38
+ .9905,
39
+ .9921,
40
+ .9934,
41
+ .9945,
42
+ .9955,
43
+ .9962,
44
+ .9969,
45
+ .9974,
46
+ .9978,
47
+ .9982,
48
+ .9985,
49
+ .9988,
50
+ .999,
51
+ 1
52
+ ];
53
+ const SMOOTH_MS = 720;
54
+ const ease = (t) => {
55
+ const at = t * SMOOTH_MS / 16;
56
+ const i = Math.min(Math.floor(at), SMOOTH_CURVE.length - 2);
57
+ const a = SMOOTH_CURVE[i];
58
+ return a + (SMOOTH_CURVE[i + 1] - a) * (at - i);
59
+ };
60
+ /**
61
+ * A number that counts to `target` on the smooth spring, whenever `target` changes.
62
+ * The first value is shown as it is, and reduced motion jumps. Returns the whole
63
+ * number to show. The count is for the eye; the caller keeps the real value for
64
+ * assistive technology.
65
+ */
66
+ function useCountTo(target) {
67
+ const [shown, setShown] = useState(target);
68
+ const current = useRef(target);
69
+ const first = useRef(true);
70
+ useIsomorphicLayoutEffect(() => {
71
+ if (first.current) {
72
+ first.current = false;
73
+ current.current = target;
74
+ return;
75
+ }
76
+ if (prefersReducedMotion()) {
77
+ current.current = target;
78
+ const frame = requestAnimationFrame(() => setShown(target));
79
+ return () => cancelAnimationFrame(frame);
80
+ }
81
+ const from = current.current;
82
+ const start = performance.now() - 16;
83
+ let frame = 0;
84
+ const tick = (now) => {
85
+ const t = Math.min(Math.max((now - start) / SMOOTH_MS, 0), 1);
86
+ current.current = from + (target - from) * ease(t);
87
+ setShown(current.current);
88
+ if (t < 1) frame = requestAnimationFrame(tick);
89
+ };
90
+ frame = requestAnimationFrame(tick);
91
+ return () => cancelAnimationFrame(frame);
92
+ }, [target]);
93
+ return Math.round(shown);
94
+ }
95
+ /** `value`, but only after `ms` have passed since it turned true; false at once. For a detail that lands as a move ends. */
96
+ function useAfter(value, ms) {
97
+ const [later, setLater] = useState(value);
98
+ const [seen, setSeen] = useState(value);
99
+ if (seen !== value) {
100
+ setSeen(value);
101
+ if (!value) setLater(false);
102
+ }
103
+ useEffect(() => {
104
+ if (!value) return;
105
+ const timer = window.setTimeout(() => setLater(true), prefersReducedMotion() ? 0 : ms);
106
+ return () => window.clearTimeout(timer);
107
+ }, [value, ms]);
108
+ return value && later;
109
+ }
110
+ //#endregion
111
+ export { useAfter, useCountTo };
112
+
113
+ //# sourceMappingURL=count-to.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"count-to.js","names":[],"sources":["../../src/lib/count-to.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\n\nimport { prefersReducedMotion, useIsomorphicLayoutEffect } from \"./motion.js\";\n\n/** The smooth spring (visual duration .4s, no bounce) sampled every 16ms, so a count agrees with a CSS transition on `--ease-smooth`. */\nconst SMOOTH_CURVE: readonly number[] = [\n 0, 0.0191, 0.0667, 0.1313, 0.2048, 0.2816, 0.3577, 0.4308, 0.4991, 0.562, 0.6189, 0.67, 0.7154,\n 0.7554, 0.7905, 0.821, 0.8475, 0.8704, 0.89, 0.9069, 0.9213, 0.9336, 0.9441, 0.9529, 0.9605,\n 0.9668, 0.9722, 0.9767, 0.9805, 0.9837, 0.9864, 0.9887, 0.9905, 0.9921, 0.9934, 0.9945, 0.9955,\n 0.9962, 0.9969, 0.9974, 0.9978, 0.9982, 0.9985, 0.9988, 0.999, 1,\n];\nconst SMOOTH_MS = 720;\n\nconst ease = (t: number) => {\n const at = (t * SMOOTH_MS) / 16;\n const i = Math.min(Math.floor(at), SMOOTH_CURVE.length - 2);\n const a = SMOOTH_CURVE[i]!;\n const b = SMOOTH_CURVE[i + 1]!;\n return a + (b - a) * (at - i);\n};\n\n/**\n * A number that counts to `target` on the smooth spring, whenever `target` changes.\n * The first value is shown as it is, and reduced motion jumps. Returns the whole\n * number to show. The count is for the eye; the caller keeps the real value for\n * assistive technology.\n */\nexport function useCountTo(target: number) {\n const [shown, setShown] = useState(target);\n const current = useRef(target);\n const first = useRef(true);\n // A layout effect, so the count starts in the same frame the bar's transition does and the two agree.\n useIsomorphicLayoutEffect(() => {\n if (first.current) {\n // The first value is already what is shown.\n first.current = false;\n current.current = target;\n return;\n }\n if (prefersReducedMotion()) {\n current.current = target;\n const frame = requestAnimationFrame(() => setShown(target));\n return () => cancelAnimationFrame(frame);\n }\n const from = current.current;\n // A frame early: the number it shows is rendered a frame after the bar's transition has started.\n const start = performance.now() - 16;\n let frame = 0;\n const tick = (now: number) => {\n const t = Math.min(Math.max((now - start) / SMOOTH_MS, 0), 1);\n current.current = from + (target - from) * ease(t);\n setShown(current.current);\n if (t < 1) frame = requestAnimationFrame(tick);\n };\n frame = requestAnimationFrame(tick);\n return () => cancelAnimationFrame(frame);\n }, [target]);\n return Math.round(shown);\n}\n\n/** `value`, but only after `ms` have passed since it turned true; false at once. For a detail that lands as a move ends. */\nexport function useAfter(value: boolean, ms: number) {\n const [later, setLater] = useState(value);\n const [seen, setSeen] = useState(value);\n // Turning false resets it, during render, so the next turn true waits again.\n if (seen !== value) {\n setSeen(value);\n if (!value) setLater(false);\n }\n useEffect(() => {\n if (!value) return;\n const timer = window.setTimeout(() => setLater(true), prefersReducedMotion() ? 0 : ms);\n return () => window.clearTimeout(timer);\n }, [value, ms]);\n return value && later;\n}\n"],"mappings":";;;;AAKA,MAAM,eAAkC;CACtC;CAAG;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAO;CAAQ;CAAM;CACxF;CAAQ;CAAQ;CAAO;CAAQ;CAAQ;CAAM;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CACrF;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CACxF;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAO;AACjE;AACA,MAAM,YAAY;AAElB,MAAM,QAAQ,MAAc;CAC1B,MAAM,KAAM,IAAI,YAAa;CAC7B,MAAM,IAAI,KAAK,IAAI,KAAK,MAAM,EAAE,GAAG,aAAa,SAAS,CAAC;CAC1D,MAAM,IAAI,aAAa;CAEvB,OAAO,KADG,aAAa,IAAI,KACX,MAAM,KAAK;AAC7B;;;;;;;AAQA,SAAgB,WAAW,QAAgB;CACzC,MAAM,CAAC,OAAO,YAAY,SAAS,MAAM;CACzC,MAAM,UAAU,OAAO,MAAM;CAC7B,MAAM,QAAQ,OAAO,IAAI;CAEzB,gCAAgC;EAC9B,IAAI,MAAM,SAAS;GAEjB,MAAM,UAAU;GAChB,QAAQ,UAAU;GAClB;EACF;EACA,IAAI,qBAAqB,GAAG;GAC1B,QAAQ,UAAU;GAClB,MAAM,QAAQ,4BAA4B,SAAS,MAAM,CAAC;GAC1D,aAAa,qBAAqB,KAAK;EACzC;EACA,MAAM,OAAO,QAAQ;EAErB,MAAM,QAAQ,YAAY,IAAI,IAAI;EAClC,IAAI,QAAQ;EACZ,MAAM,QAAQ,QAAgB;GAC5B,MAAM,IAAI,KAAK,IAAI,KAAK,KAAK,MAAM,SAAS,WAAW,CAAC,GAAG,CAAC;GAC5D,QAAQ,UAAU,QAAQ,SAAS,QAAQ,KAAK,CAAC;GACjD,SAAS,QAAQ,OAAO;GACxB,IAAI,IAAI,GAAG,QAAQ,sBAAsB,IAAI;EAC/C;EACA,QAAQ,sBAAsB,IAAI;EAClC,aAAa,qBAAqB,KAAK;CACzC,GAAG,CAAC,MAAM,CAAC;CACX,OAAO,KAAK,MAAM,KAAK;AACzB;;AAGA,SAAgB,SAAS,OAAgB,IAAY;CACnD,MAAM,CAAC,OAAO,YAAY,SAAS,KAAK;CACxC,MAAM,CAAC,MAAM,WAAW,SAAS,KAAK;CAEtC,IAAI,SAAS,OAAO;EAClB,QAAQ,KAAK;EACb,IAAI,CAAC,OAAO,SAAS,KAAK;CAC5B;CACA,gBAAgB;EACd,IAAI,CAAC,OAAO;EACZ,MAAM,QAAQ,OAAO,iBAAiB,SAAS,IAAI,GAAG,qBAAqB,IAAI,IAAI,EAAE;EACrF,aAAa,OAAO,aAAa,KAAK;CACxC,GAAG,CAAC,OAAO,EAAE,CAAC;CACd,OAAO,SAAS;AAClB"}
@@ -0,0 +1,62 @@
1
+ import { useSvgId } from "./svg-id.js";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ //#region src/lib/eye-morph.tsx
4
+ /**
5
+ * One eye that a slash draws across, cutting the outline beneath it, instead of
6
+ * swapping two icons. The slash is a path normalised to length 1 (`field-eye-slash`
7
+ * draws and undraws it with a dash transition); the same path, thick and black,
8
+ * in the mask is what cuts the outline. `slashPath` is the line's `d`.
9
+ */
10
+ function EyeMorph({ slashed, slashPath = "M2 2l20 20" }) {
11
+ const mask = `eye-${useSvgId()}`;
12
+ return /* @__PURE__ */ jsxs("svg", {
13
+ viewBox: "0 0 24 24",
14
+ fill: "none",
15
+ stroke: "currentColor",
16
+ strokeWidth: 1.75,
17
+ strokeLinecap: "round",
18
+ strokeLinejoin: "round",
19
+ "aria-hidden": "true",
20
+ "data-slashed": slashed ? "" : void 0,
21
+ className: "size-4.5",
22
+ children: [
23
+ /* @__PURE__ */ jsxs("mask", {
24
+ id: mask,
25
+ maskUnits: "userSpaceOnUse",
26
+ x: "0",
27
+ y: "0",
28
+ width: "24",
29
+ height: "24",
30
+ children: [/* @__PURE__ */ jsx("rect", {
31
+ width: "24",
32
+ height: "24",
33
+ fill: "white",
34
+ stroke: "none"
35
+ }), /* @__PURE__ */ jsx("path", {
36
+ d: slashPath,
37
+ pathLength: 1,
38
+ stroke: "black",
39
+ strokeWidth: 5,
40
+ className: "field-eye-slash"
41
+ })]
42
+ }),
43
+ /* @__PURE__ */ jsxs("g", {
44
+ mask: `url(#${mask})`,
45
+ children: [/* @__PURE__ */ jsx("path", { d: "M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" }), /* @__PURE__ */ jsx("circle", {
46
+ cx: "12",
47
+ cy: "12",
48
+ r: "3"
49
+ })]
50
+ }),
51
+ /* @__PURE__ */ jsx("path", {
52
+ d: slashPath,
53
+ pathLength: 1,
54
+ className: "field-eye-slash"
55
+ })
56
+ ]
57
+ });
58
+ }
59
+ //#endregion
60
+ export { EyeMorph };
61
+
62
+ //# sourceMappingURL=eye-morph.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eye-morph.js","names":[],"sources":["../../src/lib/eye-morph.tsx"],"sourcesContent":["import { useSvgId } from \"./svg-id.js\";\n\n/**\n * One eye that a slash draws across, cutting the outline beneath it, instead of\n * swapping two icons. The slash is a path normalised to length 1 (`field-eye-slash`\n * draws and undraws it with a dash transition); the same path, thick and black,\n * in the mask is what cuts the outline. `slashPath` is the line's `d`.\n */\nexport function EyeMorph({\n slashed,\n slashPath = \"M2 2l20 20\",\n}: {\n slashed: boolean;\n slashPath?: string;\n}) {\n const mask = `eye-${useSvgId()}`;\n return (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={1.75}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n data-slashed={slashed ? \"\" : undefined}\n className=\"size-4.5\"\n >\n <mask id={mask} maskUnits=\"userSpaceOnUse\" x=\"0\" y=\"0\" width=\"24\" height=\"24\">\n <rect width=\"24\" height=\"24\" fill=\"white\" stroke=\"none\" />\n <path\n d={slashPath}\n pathLength={1}\n stroke=\"black\"\n strokeWidth={5}\n className=\"field-eye-slash\"\n />\n </mask>\n <g mask={`url(#${mask})`}>\n <path d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\" />\n <circle cx=\"12\" cy=\"12\" r=\"3\" />\n </g>\n <path d={slashPath} pathLength={1} className=\"field-eye-slash\" />\n </svg>\n );\n}\n"],"mappings":";;;;;;;;;AAQA,SAAgB,SAAS,EACvB,SACA,YAAY,gBAIX;CACD,MAAM,OAAO,OAAO,SAAS;CAC7B,OACE,qBAAC,OAAD;EACE,SAAQ;EACR,MAAK;EACL,QAAO;EACP,aAAa;EACb,eAAc;EACd,gBAAe;EACf,eAAY;EACZ,gBAAc,UAAU,KAAK,KAAA;EAC7B,WAAU;EATZ,UAAA;GAWE,qBAAC,QAAD;IAAM,IAAI;IAAM,WAAU;IAAiB,GAAE;IAAI,GAAE;IAAI,OAAM;IAAK,QAAO;IAAzE,UAAA,CACE,oBAAC,QAAD;KAAM,OAAM;KAAK,QAAO;KAAK,MAAK;KAAQ,QAAO;IAAQ,CAAA,GACzD,oBAAC,QAAD;KACE,GAAG;KACH,YAAY;KACZ,QAAO;KACP,aAAa;KACb,WAAU;IACX,CAAA,CACG;;GACN,qBAAC,KAAD;IAAG,MAAM,QAAQ,KAAK;IAAtB,UAAA,CACE,oBAAC,QAAD,EAAM,GAAE,wGAAyG,CAAA,GACjH,oBAAC,UAAD;KAAQ,IAAG;KAAK,IAAG;KAAK,GAAE;IAAK,CAAA,CAC9B;;GACH,oBAAC,QAAD;IAAM,GAAG;IAAW,YAAY;IAAG,WAAU;GAAmB,CAAA;EAC7D;;AAET"}