@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,31 @@
1
+ import { SwapText } from "./swap-text.js";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ //#region src/lib/rolling-value.tsx
4
+ /**
5
+ * The value shown in a picker's trigger. The caller's own text stays, visually
6
+ * hidden, as what a screen reader reads (`srValue`); the copy the eye sees rolls
7
+ * when it changes: a later value rises in from below, an earlier one drops in from
8
+ * above, and the old one lifts away. Nothing animates on first mount. Long text is
9
+ * cut with an ellipsis. `instanceKey` remounts the swap, which is how a caller whose
10
+ * first value arrives a render late makes that arrival the first mount.
11
+ */
12
+ function RollingValue({ srValue, swapKey, dir, instanceKey, children }) {
13
+ return /* @__PURE__ */ jsxs("span", {
14
+ className: "grid min-w-0 flex-1",
15
+ children: [srValue, /* @__PURE__ */ jsx("span", {
16
+ "aria-hidden": "true",
17
+ className: "min-w-0 overflow-hidden whitespace-nowrap",
18
+ children: /* @__PURE__ */ jsx(SwapText, {
19
+ swapKey,
20
+ dir,
21
+ motion: "roll",
22
+ className: "block [&>span:not([aria-hidden])]:max-w-full [&>span:not([aria-hidden])]:truncate",
23
+ children
24
+ }, instanceKey)
25
+ })]
26
+ });
27
+ }
28
+ //#endregion
29
+ export { RollingValue };
30
+
31
+ //# sourceMappingURL=rolling-value.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rolling-value.js","names":[],"sources":["../../src/lib/rolling-value.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\n\nimport { SwapText } from \"./swap-text.js\";\n\n/**\n * The value shown in a picker's trigger. The caller's own text stays, visually\n * hidden, as what a screen reader reads (`srValue`); the copy the eye sees rolls\n * when it changes: a later value rises in from below, an earlier one drops in from\n * above, and the old one lifts away. Nothing animates on first mount. Long text is\n * cut with an ellipsis. `instanceKey` remounts the swap, which is how a caller whose\n * first value arrives a render late makes that arrival the first mount.\n */\nexport function RollingValue({\n srValue,\n swapKey,\n dir,\n instanceKey,\n children,\n}: {\n srValue: ReactNode;\n swapKey: string;\n dir: number;\n instanceKey?: string;\n children: ReactNode;\n}) {\n return (\n <span className=\"grid min-w-0 flex-1\">\n {srValue}\n <span aria-hidden=\"true\" className=\"min-w-0 overflow-hidden whitespace-nowrap\">\n <SwapText\n key={instanceKey}\n swapKey={swapKey}\n dir={dir}\n motion=\"roll\"\n className=\"block [&>span:not([aria-hidden])]:max-w-full [&>span:not([aria-hidden])]:truncate\"\n >\n {children}\n </SwapText>\n </span>\n </span>\n );\n}\n"],"mappings":";;;;;;;;;;;AAYA,SAAgB,aAAa,EAC3B,SACA,SACA,KACA,aACA,YAOC;CACD,OACE,qBAAC,QAAD;EAAM,WAAU;EAAhB,UAAA,CACG,SACD,oBAAC,QAAD;GAAM,eAAY;GAAO,WAAU;GACjC,UAAA,oBAAC,UAAD;IAEW;IACJ;IACL,QAAO;IACP,WAAU;IAET;GACO,GAPH,WAOG;EACN,CAAA,CACF;;AAEV"}
@@ -0,0 +1,114 @@
1
+ import { prefersReducedMotion } from "./motion.js";
2
+ import { useEffect, useRef } from "react";
3
+ //#region src/lib/spring-progress.ts
4
+ /**
5
+ * The morph spring (--ease-morph, --duration-morph: visualDuration 0.42s, bounce
6
+ * 0.16), sampled every 16ms from Motion's solver. It is here as numbers because a
7
+ * script has to evaluate it: CSS cannot interpolate the `d` of a path in every
8
+ * browser, so an icon that morphs its outline drives the path from this.
9
+ */
10
+ const MORPH_CURVE = [
11
+ 0,
12
+ .0178,
13
+ .0636,
14
+ .1279,
15
+ .2033,
16
+ .2841,
17
+ .366,
18
+ .4459,
19
+ .5218,
20
+ .5921,
21
+ .6562,
22
+ .7136,
23
+ .7643,
24
+ .8086,
25
+ .8466,
26
+ .879,
27
+ .9063,
28
+ .9289,
29
+ .9475,
30
+ .9626,
31
+ .9746,
32
+ .9841,
33
+ .9914,
34
+ .9969,
35
+ 1.001,
36
+ 1.0038,
37
+ 1.0058,
38
+ 1.0069,
39
+ 1.0075,
40
+ 1.0077,
41
+ 1.0076,
42
+ 1.0072,
43
+ 1.0067,
44
+ 1.0062,
45
+ 1.0055,
46
+ 1.0049,
47
+ 1.0043,
48
+ 1.0037,
49
+ 1.0032,
50
+ 1.0027,
51
+ 1.0022,
52
+ 1.0018,
53
+ 1.0015,
54
+ 1.0012,
55
+ 1.0009,
56
+ 1.0007,
57
+ 1
58
+ ];
59
+ const MORPH_MS = 736;
60
+ const ease = (t) => {
61
+ const at = t * MORPH_MS / 16;
62
+ const i = Math.min(Math.floor(at), MORPH_CURVE.length - 2);
63
+ const a = MORPH_CURVE[i];
64
+ return a + (MORPH_CURVE[i + 1] - a) * (at - i);
65
+ };
66
+ /**
67
+ * Follows `target` (0 or 1) on the morph spring and reports progress to
68
+ * `apply` every frame, mutating the DOM directly rather than re-rendering.
69
+ * It is interruptible: a new target mid-flight starts from where the value is.
70
+ * The first render, and reduced motion, jump straight to the target.
71
+ */
72
+ function useSpringProgress(target, apply) {
73
+ const current = useRef(target);
74
+ const frame = useRef(0);
75
+ const applyRef = useRef(apply);
76
+ const first = useRef(true);
77
+ useEffect(() => {
78
+ applyRef.current = apply;
79
+ });
80
+ useEffect(() => {
81
+ cancelAnimationFrame(frame.current);
82
+ if (first.current || prefersReducedMotion()) {
83
+ first.current = false;
84
+ current.current = target;
85
+ applyRef.current(target);
86
+ return;
87
+ }
88
+ const from = current.current;
89
+ const start = performance.now();
90
+ const tick = (now) => {
91
+ const t = Math.min(Math.max((now - start) / MORPH_MS, 0), 1);
92
+ current.current = from + (target - from) * ease(t);
93
+ applyRef.current(current.current);
94
+ if (t < 1) frame.current = requestAnimationFrame(tick);
95
+ };
96
+ frame.current = requestAnimationFrame(tick);
97
+ return () => cancelAnimationFrame(frame.current);
98
+ }, [target]);
99
+ }
100
+ /** Interpolates the numbers of a path whose two states have the same commands. */
101
+ function lerpPath(from, to, progress) {
102
+ const a = from.match(/-?\d*\.?\d+/g).map(Number);
103
+ const b = to.match(/-?\d*\.?\d+/g).map(Number);
104
+ let i = 0;
105
+ return from.replace(/-?\d*\.?\d+/g, () => {
106
+ const n = a[i] + (b[i] - a[i]) * progress;
107
+ i += 1;
108
+ return String(Math.round(n * 1e3) / 1e3);
109
+ });
110
+ }
111
+ //#endregion
112
+ export { lerpPath, useSpringProgress };
113
+
114
+ //# sourceMappingURL=spring-progress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spring-progress.js","names":[],"sources":["../../src/lib/spring-progress.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\nimport { prefersReducedMotion } from \"./motion.js\";\n\n/**\n * The morph spring (--ease-morph, --duration-morph: visualDuration 0.42s, bounce\n * 0.16), sampled every 16ms from Motion's solver. It is here as numbers because a\n * script has to evaluate it: CSS cannot interpolate the `d` of a path in every\n * browser, so an icon that morphs its outline drives the path from this.\n */\nconst MORPH_CURVE: readonly number[] = [\n 0, 0.0178, 0.0636, 0.1279, 0.2033, 0.2841, 0.366, 0.4459, 0.5218, 0.5921, 0.6562, 0.7136, 0.7643,\n 0.8086, 0.8466, 0.879, 0.9063, 0.9289, 0.9475, 0.9626, 0.9746, 0.9841, 0.9914, 0.9969, 1.001,\n 1.0038, 1.0058, 1.0069, 1.0075, 1.0077, 1.0076, 1.0072, 1.0067, 1.0062, 1.0055, 1.0049, 1.0043,\n 1.0037, 1.0032, 1.0027, 1.0022, 1.0018, 1.0015, 1.0012, 1.0009, 1.0007, 1,\n];\nconst MORPH_MS = 736;\n\nconst ease = (t: number) => {\n const at = (t * MORPH_MS) / 16;\n const i = Math.min(Math.floor(at), MORPH_CURVE.length - 2);\n const a = MORPH_CURVE[i]!;\n const b = MORPH_CURVE[i + 1]!;\n return a + (b - a) * (at - i);\n};\n\n/**\n * Follows `target` (0 or 1) on the morph spring and reports progress to\n * `apply` every frame, mutating the DOM directly rather than re-rendering.\n * It is interruptible: a new target mid-flight starts from where the value is.\n * The first render, and reduced motion, jump straight to the target.\n */\nexport function useSpringProgress(target: number, apply: (progress: number) => void) {\n const current = useRef(target);\n const frame = useRef(0);\n const applyRef = useRef(apply);\n const first = useRef(true);\n\n useEffect(() => {\n applyRef.current = apply;\n });\n\n useEffect(() => {\n cancelAnimationFrame(frame.current);\n if (first.current || prefersReducedMotion()) {\n first.current = false;\n current.current = target;\n applyRef.current(target);\n return;\n }\n const from = current.current;\n const start = performance.now();\n const tick = (now: number) => {\n // A frame's timestamp can precede `start`; a negative t would index the curve at -1.\n const t = Math.min(Math.max((now - start) / MORPH_MS, 0), 1);\n current.current = from + (target - from) * ease(t);\n applyRef.current(current.current);\n if (t < 1) frame.current = requestAnimationFrame(tick);\n };\n frame.current = requestAnimationFrame(tick);\n return () => cancelAnimationFrame(frame.current);\n }, [target]);\n}\n\n/** Interpolates the numbers of a path whose two states have the same commands. */\nexport function lerpPath(from: string, to: string, progress: number) {\n const a = from.match(/-?\\d*\\.?\\d+/g)!.map(Number);\n const b = to.match(/-?\\d*\\.?\\d+/g)!.map(Number);\n let i = 0;\n return from.replace(/-?\\d*\\.?\\d+/g, () => {\n const n = a[i]! + (b[i]! - a[i]!) * progress;\n i += 1;\n return String(Math.round(n * 1000) / 1000);\n });\n}\n"],"mappings":";;;;;;;;;AAUA,MAAM,cAAiC;CACrC;CAAG;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAO;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAC1F;CAAQ;CAAQ;CAAO;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CACvF;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CACxF;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;AAC1E;AACA,MAAM,WAAW;AAEjB,MAAM,QAAQ,MAAc;CAC1B,MAAM,KAAM,IAAI,WAAY;CAC5B,MAAM,IAAI,KAAK,IAAI,KAAK,MAAM,EAAE,GAAG,YAAY,SAAS,CAAC;CACzD,MAAM,IAAI,YAAY;CAEtB,OAAO,KADG,YAAY,IAAI,KACV,MAAM,KAAK;AAC7B;;;;;;;AAQA,SAAgB,kBAAkB,QAAgB,OAAmC;CACnF,MAAM,UAAU,OAAO,MAAM;CAC7B,MAAM,QAAQ,OAAO,CAAC;CACtB,MAAM,WAAW,OAAO,KAAK;CAC7B,MAAM,QAAQ,OAAO,IAAI;CAEzB,gBAAgB;EACd,SAAS,UAAU;CACrB,CAAC;CAED,gBAAgB;EACd,qBAAqB,MAAM,OAAO;EAClC,IAAI,MAAM,WAAW,qBAAqB,GAAG;GAC3C,MAAM,UAAU;GAChB,QAAQ,UAAU;GAClB,SAAS,QAAQ,MAAM;GACvB;EACF;EACA,MAAM,OAAO,QAAQ;EACrB,MAAM,QAAQ,YAAY,IAAI;EAC9B,MAAM,QAAQ,QAAgB;GAE5B,MAAM,IAAI,KAAK,IAAI,KAAK,KAAK,MAAM,SAAS,UAAU,CAAC,GAAG,CAAC;GAC3D,QAAQ,UAAU,QAAQ,SAAS,QAAQ,KAAK,CAAC;GACjD,SAAS,QAAQ,QAAQ,OAAO;GAChC,IAAI,IAAI,GAAG,MAAM,UAAU,sBAAsB,IAAI;EACvD;EACA,MAAM,UAAU,sBAAsB,IAAI;EAC1C,aAAa,qBAAqB,MAAM,OAAO;CACjD,GAAG,CAAC,MAAM,CAAC;AACb;;AAGA,SAAgB,SAAS,MAAc,IAAY,UAAkB;CACnE,MAAM,IAAI,KAAK,MAAM,cAAc,CAAC,CAAE,IAAI,MAAM;CAChD,MAAM,IAAI,GAAG,MAAM,cAAc,CAAC,CAAE,IAAI,MAAM;CAC9C,IAAI,IAAI;CACR,OAAO,KAAK,QAAQ,sBAAsB;EACxC,MAAM,IAAI,EAAE,MAAO,EAAE,KAAM,EAAE,MAAO;EACpC,KAAK;EACL,OAAO,OAAO,KAAK,MAAM,IAAI,GAAI,IAAI,GAAI;CAC3C,CAAC;AACH"}
@@ -0,0 +1,10 @@
1
+ import { useId } from "react";
2
+ //#region src/lib/svg-id.ts
3
+ /** An id safe inside url(#...), for masks and clip paths: React's own ids hold characters (colons, underscores) that break the reference. */
4
+ function useSvgId() {
5
+ return useId().replace(/[^a-zA-Z0-9-]/g, "");
6
+ }
7
+ //#endregion
8
+ export { useSvgId };
9
+
10
+ //# sourceMappingURL=svg-id.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"svg-id.js","names":[],"sources":["../../src/lib/svg-id.ts"],"sourcesContent":["import { useId } from \"react\";\n\n/** An id safe inside url(#...), for masks and clip paths: React's own ids hold characters (colons, underscores) that break the reference. */\nexport function useSvgId() {\n return useId().replace(/[^a-zA-Z0-9-]/g, \"\");\n}\n"],"mappings":";;;AAGA,SAAgB,WAAW;CACzB,OAAO,MAAM,CAAC,CAAC,QAAQ,kBAAkB,EAAE;AAC7C"}
@@ -0,0 +1,77 @@
1
+ import { cn } from "./cn.js";
2
+ import { useExitBackstop, useIsomorphicLayoutEffect, useLeavingItems } from "./motion.js";
3
+ import { useCallback, useRef } from "react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ //#region src/lib/swap-text.tsx
6
+ const LEAVING = {
7
+ rise: "absolute motion-safe:animate-field-swap-out motion-reduce:animate-fade-out",
8
+ roll: "absolute motion-safe:animate-field-swap-out motion-reduce:animate-fade-out",
9
+ slide: "absolute motion-safe:animate-calendar-title-out motion-reduce:animate-fade-out",
10
+ word: "absolute motion-safe:animate-range-word-out motion-reduce:animate-range-word-fade-out",
11
+ glyph: "absolute motion-safe:animate-glyph-swap-out motion-reduce:animate-fade-out",
12
+ dot: "absolute motion-safe:animate-avatar-dot-out motion-reduce:animate-fade-out"
13
+ };
14
+ const ARRIVING = {
15
+ rise: "inline-block motion-safe:animate-field-swap-in motion-reduce:animate-field-swap-fade-in",
16
+ roll: "inline-block motion-safe:animate-select-value-in motion-reduce:animate-fade-in",
17
+ slide: "inline-block motion-safe:animate-calendar-title-in motion-reduce:animate-fade-in",
18
+ word: "inline-block motion-safe:animate-range-word-in motion-reduce:animate-fade-in",
19
+ glyph: "inline-block motion-safe:animate-glyph-swap-in motion-reduce:animate-fade-in",
20
+ dot: "inline-block motion-safe:animate-avatar-dot-in motion-reduce:animate-fade-in"
21
+ };
22
+ /** A replaced piece of content on its way out, popped out of flow where it stood and lifting away. */
23
+ function Leaving({ id, empty, rect, motion, onDone, children }) {
24
+ const remove = useCallback(() => onDone(id), [onDone, id]);
25
+ useExitBackstop(true, remove);
26
+ useIsomorphicLayoutEffect(() => {
27
+ if (empty) remove();
28
+ }, [empty, remove]);
29
+ if (empty) return null;
30
+ return /* @__PURE__ */ jsx("span", {
31
+ "aria-hidden": "true",
32
+ "data-leaving": "",
33
+ onAnimationEnd: (event) => {
34
+ if (event.target === event.currentTarget) remove();
35
+ },
36
+ style: rect,
37
+ className: LEAVING[motion],
38
+ children
39
+ });
40
+ }
41
+ /**
42
+ * Content that is replaced as a whole when `swapKey` changes: the new content
43
+ * rises in from `dir` (1 from below, -1 from above; one variable shared with the
44
+ * leaving content so both follow the newest direction) while the old content,
45
+ * kept where it stood, lifts away the other way. An empty `swapKey` shows nothing
46
+ * (and the content before it leaves). Nothing animates on first mount, and
47
+ * reduced motion fades instead of moving. This is for the eye only: a screen
48
+ * reader reads the caller's own copy. The wrapper is `position: relative`, which
49
+ * the popped-out content is placed against.
50
+ */
51
+ function SwapText({ swapKey, dir = 1, motion = "rise", className, children }) {
52
+ const ref = useRef(null);
53
+ const { leaving, changed, remove } = useLeavingItems(swapKey, {
54
+ key: swapKey,
55
+ node: children
56
+ }, () => ref.current);
57
+ return /* @__PURE__ */ jsxs("span", {
58
+ style: { "--dir": dir },
59
+ className: cn("relative", className),
60
+ children: [leaving.map((item) => /* @__PURE__ */ jsx(Leaving, {
61
+ id: item.id,
62
+ empty: item.value.key === "",
63
+ rect: item.rect,
64
+ motion,
65
+ onDone: remove,
66
+ children: item.value.node
67
+ }, item.id)), swapKey ? /* @__PURE__ */ jsx("span", {
68
+ ref,
69
+ className: changed ? ARRIVING[motion] : "inline-block",
70
+ children
71
+ }, swapKey) : null]
72
+ });
73
+ }
74
+ //#endregion
75
+ export { SwapText };
76
+
77
+ //# sourceMappingURL=swap-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"swap-text.js","names":[],"sources":["../../src/lib/swap-text.tsx"],"sourcesContent":["import { useCallback, useRef, type CSSProperties, type ReactNode } from \"react\";\n\nimport { cn } from \"./cn.js\";\nimport { useExitBackstop, useIsomorphicLayoutEffect, useLeavingItems } from \"./motion.js\";\nimport type { Rect } from \"./motion.js\";\n\n/**\n * How replaced content moves. `rise`: up or down a little (a field message). `roll`:\n * a little further, and a 120ms fade under reduced motion (the value a picker shows).\n * `slide`: sideways (a calendar's month title). `word`: a word of a rolling label.\n * `glyph`: an icon, which grows in from small and blurred. `dot`: a presence dot.\n */\nexport type SwapMotion = \"rise\" | \"roll\" | \"slide\" | \"word\" | \"glyph\" | \"dot\";\n\n// Static strings: Tailwind scans source text, so a class built at runtime would never be emitted.\nconst LEAVING: Record<SwapMotion, string> = {\n rise: \"absolute motion-safe:animate-field-swap-out motion-reduce:animate-fade-out\",\n roll: \"absolute motion-safe:animate-field-swap-out motion-reduce:animate-fade-out\",\n slide: \"absolute motion-safe:animate-calendar-title-out motion-reduce:animate-fade-out\",\n word: \"absolute motion-safe:animate-range-word-out motion-reduce:animate-range-word-fade-out\",\n glyph: \"absolute motion-safe:animate-glyph-swap-out motion-reduce:animate-fade-out\",\n dot: \"absolute motion-safe:animate-avatar-dot-out motion-reduce:animate-fade-out\",\n};\n\nconst ARRIVING: Record<SwapMotion, string> = {\n rise: \"inline-block motion-safe:animate-field-swap-in motion-reduce:animate-field-swap-fade-in\",\n roll: \"inline-block motion-safe:animate-select-value-in motion-reduce:animate-fade-in\",\n slide: \"inline-block motion-safe:animate-calendar-title-in motion-reduce:animate-fade-in\",\n word: \"inline-block motion-safe:animate-range-word-in motion-reduce:animate-fade-in\",\n glyph: \"inline-block motion-safe:animate-glyph-swap-in motion-reduce:animate-fade-in\",\n dot: \"inline-block motion-safe:animate-avatar-dot-in motion-reduce:animate-fade-in\",\n};\n\n/** A replaced piece of content on its way out, popped out of flow where it stood and lifting away. */\nfunction Leaving({\n id,\n empty,\n rect,\n motion,\n onDone,\n children,\n}: {\n id: number;\n empty: boolean;\n rect: Rect;\n motion: SwapMotion;\n onDone: (id: number) => void;\n children: ReactNode;\n}) {\n const remove = useCallback(() => onDone(id), [onDone, id]);\n useExitBackstop(true, remove);\n // The empty value of \"nothing shown\" has nothing to lift away.\n useIsomorphicLayoutEffect(() => {\n if (empty) remove();\n }, [empty, remove]);\n if (empty) return null;\n return (\n <span\n aria-hidden=\"true\"\n data-leaving=\"\"\n onAnimationEnd={(event) => {\n if (event.target === event.currentTarget) remove();\n }}\n style={rect}\n className={LEAVING[motion]}\n >\n {children}\n </span>\n );\n}\n\n/**\n * Content that is replaced as a whole when `swapKey` changes: the new content\n * rises in from `dir` (1 from below, -1 from above; one variable shared with the\n * leaving content so both follow the newest direction) while the old content,\n * kept where it stood, lifts away the other way. An empty `swapKey` shows nothing\n * (and the content before it leaves). Nothing animates on first mount, and\n * reduced motion fades instead of moving. This is for the eye only: a screen\n * reader reads the caller's own copy. The wrapper is `position: relative`, which\n * the popped-out content is placed against.\n */\nexport function SwapText({\n swapKey,\n dir = 1,\n motion = \"rise\",\n className,\n children,\n}: {\n swapKey: string;\n dir?: number;\n /** How it moves. `dir` is 1 from below or forward, -1 from above or back. @default \"rise\" */\n motion?: SwapMotion;\n className?: string;\n children: ReactNode;\n}) {\n const ref = useRef<HTMLSpanElement>(null);\n const { leaving, changed, remove } = useLeavingItems(\n swapKey,\n { key: swapKey, node: children },\n () => ref.current,\n );\n return (\n <span style={{ \"--dir\": dir } as CSSProperties} className={cn(\"relative\", className)}>\n {leaving.map((item) => (\n <Leaving\n key={item.id}\n id={item.id}\n empty={item.value.key === \"\"}\n rect={item.rect}\n motion={motion}\n onDone={remove}\n >\n {item.value.node}\n </Leaving>\n ))}\n {swapKey ? (\n <span\n key={swapKey}\n ref={ref}\n className={changed ? ARRIVING[motion] : \"inline-block\"}\n >\n {children}\n </span>\n ) : null}\n </span>\n );\n}\n"],"mappings":";;;;;AAeA,MAAM,UAAsC;CAC1C,MAAM;CACN,MAAM;CACN,OAAO;CACP,MAAM;CACN,OAAO;CACP,KAAK;AACP;AAEA,MAAM,WAAuC;CAC3C,MAAM;CACN,MAAM;CACN,OAAO;CACP,MAAM;CACN,OAAO;CACP,KAAK;AACP;;AAGA,SAAS,QAAQ,EACf,IACA,OACA,MACA,QACA,QACA,YAQC;CACD,MAAM,SAAS,kBAAkB,OAAO,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC;CACzD,gBAAgB,MAAM,MAAM;CAE5B,gCAAgC;EAC9B,IAAI,OAAO,OAAO;CACpB,GAAG,CAAC,OAAO,MAAM,CAAC;CAClB,IAAI,OAAO,OAAO;CAClB,OACE,oBAAC,QAAD;EACE,eAAY;EACZ,gBAAa;EACb,iBAAiB,UAAU;GACzB,IAAI,MAAM,WAAW,MAAM,eAAe,OAAO;EACnD;EACA,OAAO;EACP,WAAW,QAAQ;EAElB;CACG,CAAA;AAEV;;;;;;;;;;;AAYA,SAAgB,SAAS,EACvB,SACA,MAAM,GACN,SAAS,QACT,WACA,YAQC;CACD,MAAM,MAAM,OAAwB,IAAI;CACxC,MAAM,EAAE,SAAS,SAAS,WAAW,gBACnC,SACA;EAAE,KAAK;EAAS,MAAM;CAAS,SACzB,IAAI,OACZ;CACA,OACE,qBAAC,QAAD;EAAM,OAAO,EAAE,SAAS,IAAI;EAAoB,WAAW,GAAG,YAAY,SAAS;EAAnF,UAAA,CACG,QAAQ,KAAK,SACZ,oBAAC,SAAD;GAEE,IAAI,KAAK;GACT,OAAO,KAAK,MAAM,QAAQ;GAC1B,MAAM,KAAK;GACH;GACR,QAAQ;GAEP,UAAA,KAAK,MAAM;EACL,GARF,KAAK,EAQH,CACV,GACA,UACC,oBAAC,QAAD;GAEO;GACL,WAAW,UAAU,SAAS,UAAU;GAEvC;EACG,GALC,OAKD,IACJ,IACA;;AAEV"}
@@ -0,0 +1,36 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ //#region src/lib/use-copy-feedback.ts
3
+ /** Clipboard state for actions that render their own button: copy, show copied or failed for a while, then go back to idle. */
4
+ function useCopyFeedback(duration = 1900) {
5
+ const [state, setState] = useState("idle");
6
+ const timeout = useRef(null);
7
+ const reset = useCallback(() => {
8
+ if (timeout.current) clearTimeout(timeout.current);
9
+ timeout.current = null;
10
+ setState("idle");
11
+ }, []);
12
+ useEffect(() => () => {
13
+ if (timeout.current) clearTimeout(timeout.current);
14
+ }, []);
15
+ return {
16
+ state,
17
+ copy: useCallback(async (value) => {
18
+ if (timeout.current) clearTimeout(timeout.current);
19
+ try {
20
+ await navigator.clipboard.writeText(value);
21
+ setState("copied");
22
+ timeout.current = setTimeout(reset, duration);
23
+ return true;
24
+ } catch {
25
+ setState("error");
26
+ timeout.current = setTimeout(reset, duration);
27
+ return false;
28
+ }
29
+ }, [duration, reset]),
30
+ reset
31
+ };
32
+ }
33
+ //#endregion
34
+ export { useCopyFeedback };
35
+
36
+ //# sourceMappingURL=use-copy-feedback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-copy-feedback.js","names":[],"sources":["../../src/lib/use-copy-feedback.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\n\nexport type CopyFeedbackState = \"idle\" | \"copied\" | \"error\";\n\n/** Clipboard state for actions that render their own button: copy, show copied or failed for a while, then go back to idle. */\nexport function useCopyFeedback(duration = 1900) {\n const [state, setState] = useState<CopyFeedbackState>(\"idle\");\n const timeout = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const reset = useCallback(() => {\n if (timeout.current) clearTimeout(timeout.current);\n timeout.current = null;\n setState(\"idle\");\n }, []);\n\n useEffect(\n () => () => {\n if (timeout.current) clearTimeout(timeout.current);\n },\n [],\n );\n\n /** Resolves true if the text reached the clipboard. */\n const copy = useCallback(\n async (value: string) => {\n if (timeout.current) clearTimeout(timeout.current);\n try {\n await navigator.clipboard.writeText(value);\n setState(\"copied\");\n timeout.current = setTimeout(reset, duration);\n return true;\n } catch {\n setState(\"error\");\n timeout.current = setTimeout(reset, duration);\n return false;\n }\n },\n [duration, reset],\n );\n\n return { state, copy, reset };\n}\n"],"mappings":";;;AAKA,SAAgB,gBAAgB,WAAW,MAAM;CAC/C,MAAM,CAAC,OAAO,YAAY,SAA4B,MAAM;CAC5D,MAAM,UAAU,OAA6C,IAAI;CAEjE,MAAM,QAAQ,kBAAkB;EAC9B,IAAI,QAAQ,SAAS,aAAa,QAAQ,OAAO;EACjD,QAAQ,UAAU;EAClB,SAAS,MAAM;CACjB,GAAG,CAAC,CAAC;CAEL,sBACc;EACV,IAAI,QAAQ,SAAS,aAAa,QAAQ,OAAO;CACnD,GACA,CAAC,CACH;CAoBA,OAAO;EAAE;EAAO,MAjBH,YACX,OAAO,UAAkB;GACvB,IAAI,QAAQ,SAAS,aAAa,QAAQ,OAAO;GACjD,IAAI;IACF,MAAM,UAAU,UAAU,UAAU,KAAK;IACzC,SAAS,QAAQ;IACjB,QAAQ,UAAU,WAAW,OAAO,QAAQ;IAC5C,OAAO;GACT,QAAQ;IACN,SAAS,OAAO;IAChB,QAAQ,UAAU,WAAW,OAAO,QAAQ;IAC5C,OAAO;GACT;EACF,GACA,CAAC,UAAU,KAAK,CAGC;EAAG;CAAM;AAC9B"}
@@ -0,0 +1,2 @@
1
+ import { PageHeader, PageHeaderProps, pageHeaderActionsVariants, pageHeaderBackVariants, pageHeaderDescriptionVariants, pageHeaderMetaVariants, pageHeaderRowVariants, pageHeaderTitleVariants, pageHeaderVariants } from "./page-header.js";
2
+ export { PageHeader, type PageHeaderProps, pageHeaderActionsVariants, pageHeaderBackVariants, pageHeaderDescriptionVariants, pageHeaderMetaVariants, pageHeaderRowVariants, pageHeaderTitleVariants, pageHeaderVariants };
@@ -0,0 +1,2 @@
1
+ import { PageHeader, pageHeaderActionsVariants, pageHeaderBackVariants, pageHeaderDescriptionVariants, pageHeaderMetaVariants, pageHeaderRowVariants, pageHeaderTitleVariants, pageHeaderVariants } from "./page-header.js";
2
+ export { PageHeader, pageHeaderActionsVariants, pageHeaderBackVariants, pageHeaderDescriptionVariants, pageHeaderMetaVariants, pageHeaderRowVariants, pageHeaderTitleVariants, pageHeaderVariants };
@@ -0,0 +1,75 @@
1
+ import { ComponentPropsWithoutRef, ReactNode } from "react";
2
+ //#region src/page-header/page-header.d.ts
3
+ /**
4
+ * PageHeader: the top of a page. One title, an optional line saying what the
5
+ * page is for, the page's own actions, a short piece of metadata and an
6
+ * optional way back.
7
+ *
8
+ * Presentational, so there is no React Aria base. It exists because a page
9
+ * title is the one element every screen has, and without a shared component
10
+ * each screen picks its own size, weight, spacing and action placement, so the
11
+ * title moves as a person moves between pages.
12
+ *
13
+ * The title renders the page's single `<h1>`. A page has one, and putting it
14
+ * here means a consumer cannot render two by accident.
15
+ *
16
+ * Every string in the cva blocks below is a STATIC literal. Tailwind scans
17
+ * source text, so a class assembled at runtime is invisible to it and would
18
+ * simply never be emitted.
19
+ */
20
+ export declare const pageHeaderVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
21
+ /**
22
+ * Title block and actions share one row. `items-start` keeps the actions level
23
+ * with the title when a description wraps under it; `flex-wrap` moves the
24
+ * actions below the title on a narrow screen instead of squeezing it.
25
+ */
26
+ export declare const pageHeaderRowVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
27
+ export declare const pageHeaderTitleVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
28
+ export declare const pageHeaderMetaVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
29
+ /**
30
+ * Capped at the md breakpoint (48rem), a readable measure for a sentence or
31
+ * two. The scale defines no container size between `max` and nothing, and a
32
+ * bracket length is rejected by check:hardcoding, so the token is referenced.
33
+ */
34
+ export declare const pageHeaderDescriptionVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
35
+ export declare const pageHeaderActionsVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
36
+ export declare const pageHeaderBackVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
37
+ /**
38
+ * The header at the top of a page.
39
+ *
40
+ * @example
41
+ * <PageHeader
42
+ * title="Projects"
43
+ * description="Everything your team is working on."
44
+ * meta="updated 14 Oct 2026"
45
+ * actions={<Button>New project</Button>}
46
+ * />
47
+ */
48
+ export interface PageHeaderProps extends Omit<ComponentPropsWithoutRef<"header">, "title"> {
49
+ /** The page's title. Rendered as the page's single `<h1>`. */
50
+ title: ReactNode;
51
+ /**
52
+ * One or two sentences under the title saying what the page is for.
53
+ * Muted, and capped at a readable measure.
54
+ */
55
+ description?: ReactNode;
56
+ /**
57
+ * The page's primary actions, right-aligned on the title row. They wrap
58
+ * below the title on a narrow screen. Pass buttons or links; the header
59
+ * spaces them and does not style them.
60
+ */
61
+ actions?: ReactNode;
62
+ /**
63
+ * A short piece of context inline after the title, on the same baseline:
64
+ * a date the figures are as of, a status chip, a count.
65
+ */
66
+ meta?: ReactNode;
67
+ /**
68
+ * A slot above the title for a way back, on a detail page. Supply the link
69
+ * yourself so it uses your router; the header sizes and colours the slot.
70
+ */
71
+ back?: ReactNode;
72
+ }
73
+ export declare const PageHeader: import("react").ForwardRefExoticComponent<PageHeaderProps & import("react").RefAttributes<HTMLElement>>;
74
+ //#endregion
75
+ //# sourceMappingURL=page-header.d.ts.map
@@ -0,0 +1,83 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.js";
3
+ import { cva } from "class-variance-authority";
4
+ import { forwardRef } from "react";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/page-header/page-header.tsx
7
+ /**
8
+ * PageHeader: the top of a page. One title, an optional line saying what the
9
+ * page is for, the page's own actions, a short piece of metadata and an
10
+ * optional way back.
11
+ *
12
+ * Presentational, so there is no React Aria base. It exists because a page
13
+ * title is the one element every screen has, and without a shared component
14
+ * each screen picks its own size, weight, spacing and action placement, so the
15
+ * title moves as a person moves between pages.
16
+ *
17
+ * The title renders the page's single `<h1>`. A page has one, and putting it
18
+ * here means a consumer cannot render two by accident.
19
+ *
20
+ * Every string in the cva blocks below is a STATIC literal. Tailwind scans
21
+ * source text, so a class assembled at runtime is invisible to it and would
22
+ * simply never be emitted.
23
+ */
24
+ const pageHeaderVariants = cva(["mb-6 block", "box-border font-sans text-foreground"]);
25
+ /**
26
+ * Title block and actions share one row. `items-start` keeps the actions level
27
+ * with the title when a description wraps under it; `flex-wrap` moves the
28
+ * actions below the title on a narrow screen instead of squeezing it.
29
+ */
30
+ const pageHeaderRowVariants = cva("flex flex-wrap items-start justify-between gap-3");
31
+ const pageHeaderTitleVariants = cva(["font-display text-h3 leading-(--leading-snug) font-semibold", "m-0"]);
32
+ const pageHeaderMetaVariants = cva("text-xs leading-(--leading-normal) text-muted-foreground");
33
+ /**
34
+ * Capped at the md breakpoint (48rem), a readable measure for a sentence or
35
+ * two. The scale defines no container size between `max` and nothing, and a
36
+ * bracket length is rejected by check:hardcoding, so the token is referenced.
37
+ */
38
+ const pageHeaderDescriptionVariants = cva(["mt-1 mb-0 max-w-(--breakpoint-md)", "text-sm leading-(--leading-normal) text-muted-foreground"]);
39
+ const pageHeaderActionsVariants = cva("flex shrink-0 flex-wrap items-center gap-2");
40
+ const pageHeaderBackVariants = cva("mb-2 text-xs leading-(--leading-normal) text-muted-foreground");
41
+ const PageHeader = forwardRef(function PageHeader({ className, title, description, actions, meta, back, ...props }, ref) {
42
+ return /* @__PURE__ */ jsxs("header", {
43
+ ref,
44
+ ...props,
45
+ "data-slot": "page-header",
46
+ className: cn(pageHeaderVariants(), className),
47
+ children: [back === void 0 ? null : /* @__PURE__ */ jsx("div", {
48
+ "data-slot": "page-header-back",
49
+ className: pageHeaderBackVariants(),
50
+ children: back
51
+ }), /* @__PURE__ */ jsxs("div", {
52
+ "data-slot": "page-header-row",
53
+ className: pageHeaderRowVariants(),
54
+ children: [/* @__PURE__ */ jsxs("div", {
55
+ className: "min-w-0 grow basis-64",
56
+ children: [/* @__PURE__ */ jsxs("div", {
57
+ className: "flex flex-wrap items-baseline gap-x-3 gap-y-1",
58
+ children: [/* @__PURE__ */ jsx("h1", {
59
+ "data-slot": "page-header-title",
60
+ className: pageHeaderTitleVariants(),
61
+ children: title
62
+ }), meta === void 0 ? null : /* @__PURE__ */ jsx("div", {
63
+ "data-slot": "page-header-meta",
64
+ className: pageHeaderMetaVariants(),
65
+ children: meta
66
+ })]
67
+ }), description === void 0 ? null : /* @__PURE__ */ jsx("p", {
68
+ "data-slot": "page-header-description",
69
+ className: pageHeaderDescriptionVariants(),
70
+ children: description
71
+ })]
72
+ }), actions === void 0 ? null : /* @__PURE__ */ jsx("div", {
73
+ "data-slot": "page-header-actions",
74
+ className: pageHeaderActionsVariants(),
75
+ children: actions
76
+ })]
77
+ })]
78
+ });
79
+ });
80
+ //#endregion
81
+ export { PageHeader, pageHeaderActionsVariants, pageHeaderBackVariants, pageHeaderDescriptionVariants, pageHeaderMetaVariants, pageHeaderRowVariants, pageHeaderTitleVariants, pageHeaderVariants };
82
+
83
+ //# sourceMappingURL=page-header.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-header.js","names":[],"sources":["../../src/page-header/page-header.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type ComponentPropsWithoutRef, type ReactNode } from \"react\";\n\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * PageHeader: the top of a page. One title, an optional line saying what the\n * page is for, the page's own actions, a short piece of metadata and an\n * optional way back.\n *\n * Presentational, so there is no React Aria base. It exists because a page\n * title is the one element every screen has, and without a shared component\n * each screen picks its own size, weight, spacing and action placement, so the\n * title moves as a person moves between pages.\n *\n * The title renders the page's single `<h1>`. A page has one, and putting it\n * here means a consumer cannot render two by accident.\n *\n * Every string in the cva blocks below is a STATIC literal. Tailwind scans\n * source text, so a class assembled at runtime is invisible to it and would\n * simply never be emitted.\n */\nexport const pageHeaderVariants = cva([\n \"mb-6 block\",\n // box-border and font-sans are not boilerplate: the reset in\n // @seekalfred/styles is opt-in, so a consumer may load none, and an\n // unstyled document renders text in a serif.\n \"box-border font-sans text-foreground\",\n]);\n\n/**\n * Title block and actions share one row. `items-start` keeps the actions level\n * with the title when a description wraps under it; `flex-wrap` moves the\n * actions below the title on a narrow screen instead of squeezing it.\n */\nexport const pageHeaderRowVariants = cva(\"flex flex-wrap items-start justify-between gap-3\");\n\nexport const pageHeaderTitleVariants = cva([\n // leading-* AFTER text-*: tailwind-merge treats a later text-* as owning\n // line-height and silently strips a leading-* that precedes it.\n \"font-display text-h3 leading-(--leading-snug) font-semibold\",\n // The user-agent <h1> ships a 0.67em block margin and font-size 2em.\n \"m-0\",\n]);\n\nexport const pageHeaderMetaVariants = cva(\n \"text-xs leading-(--leading-normal) text-muted-foreground\",\n);\n\n/**\n * Capped at the md breakpoint (48rem), a readable measure for a sentence or\n * two. The scale defines no container size between `max` and nothing, and a\n * bracket length is rejected by check:hardcoding, so the token is referenced.\n */\nexport const pageHeaderDescriptionVariants = cva([\n \"mt-1 mb-0 max-w-(--breakpoint-md)\",\n \"text-sm leading-(--leading-normal) text-muted-foreground\",\n]);\n\nexport const pageHeaderActionsVariants = cva(\"flex shrink-0 flex-wrap items-center gap-2\");\n\nexport const pageHeaderBackVariants = cva(\n \"mb-2 text-xs leading-(--leading-normal) text-muted-foreground\",\n);\n\n/**\n * The header at the top of a page.\n *\n * @example\n * <PageHeader\n * title=\"Projects\"\n * description=\"Everything your team is working on.\"\n * meta=\"updated 14 Oct 2026\"\n * actions={<Button>New project</Button>}\n * />\n */\nexport interface PageHeaderProps\n // The native `title` attribute is omitted so this component can use the\n // name for its heading. A tooltip on a page header is not keyboard\n // reachable, and silently shadowing it with a ReactNode would be a type\n // error waiting for the first consumer who tried both.\n extends Omit<ComponentPropsWithoutRef<\"header\">, \"title\"> {\n /** The page's title. Rendered as the page's single `<h1>`. */\n title: ReactNode;\n /**\n * One or two sentences under the title saying what the page is for.\n * Muted, and capped at a readable measure.\n */\n description?: ReactNode;\n /**\n * The page's primary actions, right-aligned on the title row. They wrap\n * below the title on a narrow screen. Pass buttons or links; the header\n * spaces them and does not style them.\n */\n actions?: ReactNode;\n /**\n * A short piece of context inline after the title, on the same baseline:\n * a date the figures are as of, a status chip, a count.\n */\n meta?: ReactNode;\n /**\n * A slot above the title for a way back, on a detail page. Supply the link\n * yourself so it uses your router; the header sizes and colours the slot.\n */\n back?: ReactNode;\n}\n\nexport const PageHeader = forwardRef<HTMLElement, PageHeaderProps>(function PageHeader(\n { className, title, description, actions, meta, back, ...props },\n ref,\n) {\n return (\n <header\n ref={ref}\n {...props}\n data-slot=\"page-header\"\n className={cn(pageHeaderVariants(), className)}\n >\n {back === undefined ? null : (\n <div data-slot=\"page-header-back\" className={pageHeaderBackVariants()}>\n {back}\n </div>\n )}\n <div data-slot=\"page-header-row\" className={pageHeaderRowVariants()}>\n {/*\n * grow from a 16rem basis, not flex-1. flex-1 is a 0% basis, so the\n * row never wraps: the actions keep their width and the title is\n * crushed into a one-word column beside them on a phone. A real basis\n * is the width the title needs before the actions move below it.\n */}\n <div className=\"min-w-0 grow basis-64\">\n <div className=\"flex flex-wrap items-baseline gap-x-3 gap-y-1\">\n <h1 data-slot=\"page-header-title\" className={pageHeaderTitleVariants()}>\n {title}\n </h1>\n {meta === undefined ? null : (\n <div data-slot=\"page-header-meta\" className={pageHeaderMetaVariants()}>\n {meta}\n </div>\n )}\n </div>\n {description === undefined ? null : (\n <p data-slot=\"page-header-description\" className={pageHeaderDescriptionVariants()}>\n {description}\n </p>\n )}\n </div>\n {actions === undefined ? null : (\n <div data-slot=\"page-header-actions\" className={pageHeaderActionsVariants()}>\n {actions}\n </div>\n )}\n </div>\n </header>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAwBA,MAAa,qBAAqB,IAAI,CACpC,cAIA,sCACF,CAAC;;;;;;AAOD,MAAa,wBAAwB,IAAI,kDAAkD;AAE3F,MAAa,0BAA0B,IAAI,CAGzC,+DAEA,KACF,CAAC;AAED,MAAa,yBAAyB,IACpC,0DACF;;;;;;AAOA,MAAa,gCAAgC,IAAI,CAC/C,qCACA,0DACF,CAAC;AAED,MAAa,4BAA4B,IAAI,4CAA4C;AAEzF,MAAa,yBAAyB,IACpC,+DACF;AA4CA,MAAa,aAAa,WAAyC,SAAS,WAC1E,EAAE,WAAW,OAAO,aAAa,SAAS,MAAM,MAAM,GAAG,SACzD,KACA;CACA,OACE,qBAAC,UAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,mBAAmB,GAAG,SAAS;EAJ/C,UAAA,CAMG,SAAS,KAAA,IAAY,OACpB,oBAAC,OAAD;GAAK,aAAU;GAAmB,WAAW,uBAAuB;GACjE,UAAA;EACE,CAAA,GAEP,qBAAC,OAAD;GAAK,aAAU;GAAkB,WAAW,sBAAsB;GAAlE,UAAA,CAOE,qBAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACE,qBAAC,OAAD;KAAK,WAAU;KAAf,UAAA,CACE,oBAAC,MAAD;MAAI,aAAU;MAAoB,WAAW,wBAAwB;MAClE,UAAA;KACC,CAAA,GACH,SAAS,KAAA,IAAY,OACpB,oBAAC,OAAD;MAAK,aAAU;MAAmB,WAAW,uBAAuB;MACjE,UAAA;KACE,CAAA,CAEJ;IACJ,CAAA,GAAA,gBAAgB,KAAA,IAAY,OAC3B,oBAAC,KAAD;KAAG,aAAU;KAA0B,WAAW,8BAA8B;KAC7E,UAAA;IACA,CAAA,CAEF;GACJ,CAAA,GAAA,YAAY,KAAA,IAAY,OACvB,oBAAC,OAAD;IAAK,aAAU;IAAsB,WAAW,0BAA0B;IACvE,UAAA;GACE,CAAA,CAEJ;EACC,CAAA,CAAA;;AAEZ,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { PasswordField, PasswordFieldMessages, PasswordFieldProps, defaultPasswordFieldMessages } from "./password-field.js";
2
+ export { PasswordField, type PasswordFieldMessages, type PasswordFieldProps, defaultPasswordFieldMessages };
@@ -0,0 +1,2 @@
1
+ import { PasswordField, defaultPasswordFieldMessages } from "./password-field.js";
2
+ export { PasswordField, defaultPasswordFieldMessages };
@@ -0,0 +1,37 @@
1
+ import { InputProps } from "react-aria-components";
2
+ //#region src/password-field/password-field.d.ts
3
+ /** The toggle's accessible names. Both have English defaults; pass the ones you translate. */
4
+ export interface PasswordFieldMessages {
5
+ showPassword: string;
6
+ hidePassword: string;
7
+ }
8
+ export declare const defaultPasswordFieldMessages: PasswordFieldMessages;
9
+ export interface PasswordFieldProps extends Omit<InputProps, "className" | "style" | "size" | "type"> {
10
+ /** Extra classes for the input. Merged last, so they win conflicts with ours. */
11
+ className?: string;
12
+ /** The visible label, tied to the input with `htmlFor`. */
13
+ label: string;
14
+ /**
15
+ * Helper text under the field, named by `aria-describedby`. A string, because
16
+ * the copy is animated word by word: it opens on a spring when it appears
17
+ * after mount and changed words rise in. Nothing animates on first render.
18
+ */
19
+ description?: string;
20
+ /** Translated or reworded toggle names. */
21
+ messages?: Partial<PasswordFieldMessages>;
22
+ }
23
+ /**
24
+ * A password input with a show and hide toggle. Switching resolves the text in
25
+ * place from a soft blur, and the eye is one outline that a slash draws across.
26
+ * Native input props apply to the input; `disabled` also disables the toggle,
27
+ * and `aria-invalid` gives the box its invalid border.
28
+ *
29
+ * The toggle's name changes with the state and it is also `aria-pressed`, so a
30
+ * screen reader may announce both ("Hide password, pressed").
31
+ *
32
+ * @example
33
+ * <PasswordField label="Password" description="At least eight characters." />
34
+ */
35
+ export declare const PasswordField: import("react").ForwardRefExoticComponent<PasswordFieldProps & import("react").RefAttributes<HTMLInputElement>>;
36
+ //#endregion
37
+ //# sourceMappingURL=password-field.d.ts.map
@@ -0,0 +1,101 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.js";
3
+ import { fieldSurfaceVariants } from "../lib/field-surface.js";
4
+ import { FieldShell, useFieldWiring } from "../lib/field-shell.js";
5
+ import { EyeMorph } from "../lib/eye-morph.js";
6
+ import { forwardRef, useState } from "react";
7
+ import { Button, Group, Input } from "react-aria-components";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+ //#region src/password-field/password-field.tsx
10
+ const defaultPasswordFieldMessages = {
11
+ showPassword: "Show password",
12
+ hidePassword: "Hide password"
13
+ };
14
+ /**
15
+ * The field's box. A React Aria `Group` rather than the input, because the
16
+ * toggle sits inside the same border: it carries the shared field surface (the
17
+ * 160ms border transition, the darker border on hover and focus, the muted
18
+ * disabled look, the invalid border) and the input inside is borderless.
19
+ *
20
+ * `data-focus-within` darkens the border too. A Group reports focus-visible
21
+ * from the input modality, so a mouse click into the input, or onto the toggle,
22
+ * would otherwise leave the border pale; a plain Input is different because
23
+ * React Aria treats a text input as always keyboard-focused.
24
+ */
25
+ const shellClass = cn(fieldSurfaceVariants(), "flex items-center h-(--density-control-h-md) ps-(--density-control-pad-md) field-toggle-shell [&:where([data-focus-within])]:border-foreground");
26
+ /** The input inside the box: no border, no fill, no outline of its own. */
27
+ const inputClass = "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";
28
+ const toggleClass = "field-toggle-button grid size-7.5 flex-none cursor-pointer place-items-center rounded-md border-0 bg-transparent p-0 text-muted-foreground 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";
29
+ /**
30
+ * A password input with a show and hide toggle. Switching resolves the text in
31
+ * place from a soft blur, and the eye is one outline that a slash draws across.
32
+ * Native input props apply to the input; `disabled` also disables the toggle,
33
+ * and `aria-invalid` gives the box its invalid border.
34
+ *
35
+ * The toggle's name changes with the state and it is also `aria-pressed`, so a
36
+ * screen reader may announce both ("Hide password, pressed").
37
+ *
38
+ * @example
39
+ * <PasswordField label="Password" description="At least eight characters." />
40
+ */
41
+ const PasswordField = forwardRef(function PasswordField({ label, description, messages: messagesProp, className, ...props }, ref) {
42
+ const messages = {
43
+ ...defaultPasswordFieldMessages,
44
+ ...messagesProp
45
+ };
46
+ const [visible, setVisible] = useState(false);
47
+ const [toggled, setToggled] = useState(false);
48
+ const field = useFieldWiring({
49
+ enabled: true,
50
+ id: props.id,
51
+ describedBy: props["aria-describedby"],
52
+ invalid: props["aria-invalid"],
53
+ description,
54
+ error: void 0
55
+ });
56
+ const disabled = Boolean(props.disabled);
57
+ const ariaInvalid = props["aria-invalid"];
58
+ const control = /* @__PURE__ */ jsxs(Group, {
59
+ role: "presentation",
60
+ isDisabled: disabled,
61
+ isInvalid: ariaInvalid !== void 0 && ariaInvalid !== false && ariaInvalid !== "false",
62
+ "data-slot": "password-field-shell",
63
+ className: shellClass,
64
+ children: [/* @__PURE__ */ jsx(Input, {
65
+ ref,
66
+ ...props,
67
+ ...field.controlProps,
68
+ type: visible ? "text" : "password",
69
+ "data-slot": "password-field",
70
+ "data-reveal": toggled ? visible ? "shown" : "hidden" : void 0,
71
+ className: cn(inputClass, className)
72
+ }), /* @__PURE__ */ jsx(Button, {
73
+ type: "button",
74
+ isDisabled: disabled,
75
+ "aria-label": visible ? messages.hidePassword : messages.showPassword,
76
+ "aria-pressed": visible,
77
+ onPress: () => {
78
+ setVisible((current) => !current);
79
+ setToggled(true);
80
+ },
81
+ className: toggleClass,
82
+ children: /* @__PURE__ */ jsx(EyeMorph, { slashed: visible })
83
+ })]
84
+ });
85
+ if (!field.active) return control;
86
+ return /* @__PURE__ */ jsx(FieldShell, {
87
+ label,
88
+ controlId: field.controlId,
89
+ description,
90
+ hintId: field.hintId,
91
+ error: void 0,
92
+ errorId: void 0,
93
+ wordExit: "sharp",
94
+ numbers: false,
95
+ children: control
96
+ });
97
+ });
98
+ //#endregion
99
+ export { PasswordField, defaultPasswordFieldMessages };
100
+
101
+ //# sourceMappingURL=password-field.js.map