@funstack/ui 0.0.0-stage → 0.1.0-alpha.1

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 (299) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +42 -2
  3. package/dist/accordion/accordion-context.d.mts +5 -0
  4. package/dist/accordion/accordion-context.d.mts.map +1 -0
  5. package/dist/accordion/accordion-context.mjs +9 -0
  6. package/dist/accordion/accordion-context.mjs.map +1 -0
  7. package/dist/accordion/accordion-item.d.mts +53 -0
  8. package/dist/accordion/accordion-item.d.mts.map +1 -0
  9. package/dist/accordion/accordion-item.mjs +85 -0
  10. package/dist/accordion/accordion-item.mjs.map +1 -0
  11. package/dist/accordion/accordion-panel.d.mts +40 -0
  12. package/dist/accordion/accordion-panel.d.mts.map +1 -0
  13. package/dist/accordion/accordion-panel.mjs +40 -0
  14. package/dist/accordion/accordion-panel.mjs.map +1 -0
  15. package/dist/accordion/accordion-root.d.mts +90 -0
  16. package/dist/accordion/accordion-root.d.mts.map +1 -0
  17. package/dist/accordion/accordion-root.mjs +229 -0
  18. package/dist/accordion/accordion-root.mjs.map +1 -0
  19. package/dist/accordion/accordion-trigger.d.mts +37 -0
  20. package/dist/accordion/accordion-trigger.d.mts.map +1 -0
  21. package/dist/accordion/accordion-trigger.mjs +81 -0
  22. package/dist/accordion/accordion-trigger.mjs.map +1 -0
  23. package/dist/accordion/index.d.mts +20 -0
  24. package/dist/accordion/index.d.mts.map +1 -0
  25. package/dist/accordion/index.mjs +20 -0
  26. package/dist/accordion/index.mjs.map +1 -0
  27. package/dist/checkbox/checkbox.d.mts +92 -0
  28. package/dist/checkbox/checkbox.d.mts.map +1 -0
  29. package/dist/checkbox/checkbox.mjs +82 -0
  30. package/dist/checkbox/checkbox.mjs.map +1 -0
  31. package/dist/checkbox/index.d.mts +3 -0
  32. package/dist/checkbox/index.mjs +1 -0
  33. package/dist/checkbox-group/checkbox-group-context.mjs +24 -0
  34. package/dist/checkbox-group/checkbox-group-context.mjs.map +1 -0
  35. package/dist/checkbox-group/checkbox-group-item.d.mts +55 -0
  36. package/dist/checkbox-group/checkbox-group-item.d.mts.map +1 -0
  37. package/dist/checkbox-group/checkbox-group-item.mjs +90 -0
  38. package/dist/checkbox-group/checkbox-group-item.mjs.map +1 -0
  39. package/dist/checkbox-group/checkbox-group-label.d.mts +21 -0
  40. package/dist/checkbox-group/checkbox-group-label.d.mts.map +1 -0
  41. package/dist/checkbox-group/checkbox-group-label.mjs +30 -0
  42. package/dist/checkbox-group/checkbox-group-label.mjs.map +1 -0
  43. package/dist/checkbox-group/checkbox-group-parent.d.mts +68 -0
  44. package/dist/checkbox-group/checkbox-group-parent.d.mts.map +1 -0
  45. package/dist/checkbox-group/checkbox-group-parent.mjs +145 -0
  46. package/dist/checkbox-group/checkbox-group-parent.mjs.map +1 -0
  47. package/dist/checkbox-group/checkbox-group-root.d.mts +100 -0
  48. package/dist/checkbox-group/checkbox-group-root.d.mts.map +1 -0
  49. package/dist/checkbox-group/checkbox-group-root.mjs +291 -0
  50. package/dist/checkbox-group/checkbox-group-root.mjs.map +1 -0
  51. package/dist/checkbox-group/index.d.mts +23 -0
  52. package/dist/checkbox-group/index.d.mts.map +1 -0
  53. package/dist/checkbox-group/index.mjs +23 -0
  54. package/dist/checkbox-group/index.mjs.map +1 -0
  55. package/dist/dialog/dialog-close.d.mts +44 -0
  56. package/dist/dialog/dialog-close.d.mts.map +1 -0
  57. package/dist/dialog/dialog-close.mjs +76 -0
  58. package/dist/dialog/dialog-close.mjs.map +1 -0
  59. package/dist/dialog/dialog-context.mjs +13 -0
  60. package/dist/dialog/dialog-context.mjs.map +1 -0
  61. package/dist/dialog/dialog-description.d.mts +22 -0
  62. package/dist/dialog/dialog-description.d.mts.map +1 -0
  63. package/dist/dialog/dialog-description.mjs +31 -0
  64. package/dist/dialog/dialog-description.mjs.map +1 -0
  65. package/dist/dialog/dialog-form.d.mts +58 -0
  66. package/dist/dialog/dialog-form.d.mts.map +1 -0
  67. package/dist/dialog/dialog-form.mjs +108 -0
  68. package/dist/dialog/dialog-form.mjs.map +1 -0
  69. package/dist/dialog/dialog-popup.d.mts +81 -0
  70. package/dist/dialog/dialog-popup.d.mts.map +1 -0
  71. package/dist/dialog/dialog-popup.mjs +367 -0
  72. package/dist/dialog/dialog-popup.mjs.map +1 -0
  73. package/dist/dialog/dialog-root.d.mts +70 -0
  74. package/dist/dialog/dialog-root.d.mts.map +1 -0
  75. package/dist/dialog/dialog-root.mjs +113 -0
  76. package/dist/dialog/dialog-root.mjs.map +1 -0
  77. package/dist/dialog/dialog-title.d.mts +23 -0
  78. package/dist/dialog/dialog-title.d.mts.map +1 -0
  79. package/dist/dialog/dialog-title.mjs +31 -0
  80. package/dist/dialog/dialog-title.mjs.map +1 -0
  81. package/dist/dialog/dialog-trigger.d.mts +39 -0
  82. package/dist/dialog/dialog-trigger.d.mts.map +1 -0
  83. package/dist/dialog/dialog-trigger.mjs +80 -0
  84. package/dist/dialog/dialog-trigger.mjs.map +1 -0
  85. package/dist/dialog/index.d.mts +28 -0
  86. package/dist/dialog/index.d.mts.map +1 -0
  87. package/dist/dialog/index.mjs +28 -0
  88. package/dist/dialog/index.mjs.map +1 -0
  89. package/dist/index.d.mts +60 -0
  90. package/dist/index.mjs +12 -0
  91. package/dist/internal/action-details.mjs +47 -0
  92. package/dist/internal/action-details.mjs.map +1 -0
  93. package/dist/internal/anchor-position.d.mts +7 -0
  94. package/dist/internal/anchor-position.d.mts.map +1 -0
  95. package/dist/internal/anchor-position.mjs +16 -0
  96. package/dist/internal/anchor-position.mjs.map +1 -0
  97. package/dist/internal/data-attr.mjs +13 -0
  98. package/dist/internal/data-attr.mjs.map +1 -0
  99. package/dist/internal/merge-props.mjs +119 -0
  100. package/dist/internal/merge-props.mjs.map +1 -0
  101. package/dist/internal/platform-attributes.d.mts +55 -0
  102. package/dist/internal/platform-attributes.d.mts.map +1 -0
  103. package/dist/internal/sync-presence.mjs +19 -0
  104. package/dist/internal/sync-presence.mjs.map +1 -0
  105. package/dist/internal/transition-boundary.mjs +52 -0
  106. package/dist/internal/transition-boundary.mjs.map +1 -0
  107. package/dist/internal/transition-types.mjs +15 -0
  108. package/dist/internal/transition-types.mjs.map +1 -0
  109. package/dist/internal/types.d.mts +38 -0
  110. package/dist/internal/types.d.mts.map +1 -0
  111. package/dist/internal/use-action-trigger.mjs +58 -0
  112. package/dist/internal/use-action-trigger.mjs.map +1 -0
  113. package/dist/internal/use-controllable-action.mjs +115 -0
  114. package/dist/internal/use-controllable-action.mjs.map +1 -0
  115. package/dist/internal/use-delayed-pair.mjs +40 -0
  116. package/dist/internal/use-delayed-pair.mjs.map +1 -0
  117. package/dist/internal/use-dev-warning.mjs +21 -0
  118. package/dist/internal/use-dev-warning.mjs.map +1 -0
  119. package/dist/internal/use-exit-animation.mjs +55 -0
  120. package/dist/internal/use-exit-animation.mjs.map +1 -0
  121. package/dist/internal/use-form-reset.mjs +50 -0
  122. package/dist/internal/use-form-reset.mjs.map +1 -0
  123. package/dist/internal/use-native-checked.mjs +113 -0
  124. package/dist/internal/use-native-checked.mjs.map +1 -0
  125. package/dist/internal/use-native-group-reconciliation.mjs +64 -0
  126. package/dist/internal/use-native-group-reconciliation.mjs.map +1 -0
  127. package/dist/internal/use-popover-popup.mjs +274 -0
  128. package/dist/internal/use-popover-popup.mjs.map +1 -0
  129. package/dist/internal/use-popover-root.d.mts +2 -0
  130. package/dist/internal/use-popover-root.mjs +112 -0
  131. package/dist/internal/use-popover-root.mjs.map +1 -0
  132. package/dist/internal/use-popover-trigger.mjs +86 -0
  133. package/dist/internal/use-popover-trigger.mjs.map +1 -0
  134. package/dist/internal/use-render.mjs +51 -0
  135. package/dist/internal/use-render.mjs.map +1 -0
  136. package/dist/internal/use-until-found.mjs +40 -0
  137. package/dist/internal/use-until-found.mjs.map +1 -0
  138. package/dist/menu/index.d.mts +34 -0
  139. package/dist/menu/index.d.mts.map +1 -0
  140. package/dist/menu/index.mjs +34 -0
  141. package/dist/menu/index.mjs.map +1 -0
  142. package/dist/menu/menu-checkbox-item.d.mts +42 -0
  143. package/dist/menu/menu-checkbox-item.d.mts.map +1 -0
  144. package/dist/menu/menu-checkbox-item.mjs +71 -0
  145. package/dist/menu/menu-checkbox-item.mjs.map +1 -0
  146. package/dist/menu/menu-context.d.mts +1 -0
  147. package/dist/menu/menu-context.mjs +28 -0
  148. package/dist/menu/menu-context.mjs.map +1 -0
  149. package/dist/menu/menu-dom.mjs +37 -0
  150. package/dist/menu/menu-dom.mjs.map +1 -0
  151. package/dist/menu/menu-group.d.mts +48 -0
  152. package/dist/menu/menu-group.d.mts.map +1 -0
  153. package/dist/menu/menu-group.mjs +69 -0
  154. package/dist/menu/menu-group.mjs.map +1 -0
  155. package/dist/menu/menu-hover.mjs +29 -0
  156. package/dist/menu/menu-hover.mjs.map +1 -0
  157. package/dist/menu/menu-item.d.mts +45 -0
  158. package/dist/menu/menu-item.d.mts.map +1 -0
  159. package/dist/menu/menu-item.mjs +54 -0
  160. package/dist/menu/menu-item.mjs.map +1 -0
  161. package/dist/menu/menu-popup.d.mts +69 -0
  162. package/dist/menu/menu-popup.d.mts.map +1 -0
  163. package/dist/menu/menu-popup.mjs +207 -0
  164. package/dist/menu/menu-popup.mjs.map +1 -0
  165. package/dist/menu/menu-radio-group.d.mts +37 -0
  166. package/dist/menu/menu-radio-group.d.mts.map +1 -0
  167. package/dist/menu/menu-radio-group.mjs +72 -0
  168. package/dist/menu/menu-radio-group.mjs.map +1 -0
  169. package/dist/menu/menu-radio-item.d.mts +33 -0
  170. package/dist/menu/menu-radio-item.d.mts.map +1 -0
  171. package/dist/menu/menu-radio-item.mjs +62 -0
  172. package/dist/menu/menu-radio-item.mjs.map +1 -0
  173. package/dist/menu/menu-root.d.mts +54 -0
  174. package/dist/menu/menu-root.d.mts.map +1 -0
  175. package/dist/menu/menu-root.mjs +102 -0
  176. package/dist/menu/menu-root.mjs.map +1 -0
  177. package/dist/menu/menu-submenu-trigger.d.mts +31 -0
  178. package/dist/menu/menu-submenu-trigger.d.mts.map +1 -0
  179. package/dist/menu/menu-submenu-trigger.mjs +92 -0
  180. package/dist/menu/menu-submenu-trigger.mjs.map +1 -0
  181. package/dist/menu/menu-trigger.d.mts +36 -0
  182. package/dist/menu/menu-trigger.d.mts.map +1 -0
  183. package/dist/menu/menu-trigger.mjs +81 -0
  184. package/dist/menu/menu-trigger.mjs.map +1 -0
  185. package/dist/menu/use-menu-item.mjs +54 -0
  186. package/dist/menu/use-menu-item.mjs.map +1 -0
  187. package/dist/popover/index.d.mts +28 -0
  188. package/dist/popover/index.d.mts.map +1 -0
  189. package/dist/popover/index.mjs +28 -0
  190. package/dist/popover/index.mjs.map +1 -0
  191. package/dist/popover/popover-close.d.mts +41 -0
  192. package/dist/popover/popover-close.d.mts.map +1 -0
  193. package/dist/popover/popover-close.mjs +90 -0
  194. package/dist/popover/popover-close.mjs.map +1 -0
  195. package/dist/popover/popover-context.mjs +26 -0
  196. package/dist/popover/popover-context.mjs.map +1 -0
  197. package/dist/popover/popover-description.d.mts +22 -0
  198. package/dist/popover/popover-description.d.mts.map +1 -0
  199. package/dist/popover/popover-description.mjs +28 -0
  200. package/dist/popover/popover-description.mjs.map +1 -0
  201. package/dist/popover/popover-popup.d.mts +82 -0
  202. package/dist/popover/popover-popup.d.mts.map +1 -0
  203. package/dist/popover/popover-popup.mjs +93 -0
  204. package/dist/popover/popover-popup.mjs.map +1 -0
  205. package/dist/popover/popover-root.d.mts +64 -0
  206. package/dist/popover/popover-root.d.mts.map +1 -0
  207. package/dist/popover/popover-root.mjs +39 -0
  208. package/dist/popover/popover-root.mjs.map +1 -0
  209. package/dist/popover/popover-title.d.mts +23 -0
  210. package/dist/popover/popover-title.d.mts.map +1 -0
  211. package/dist/popover/popover-title.mjs +28 -0
  212. package/dist/popover/popover-title.mjs.map +1 -0
  213. package/dist/popover/popover-trigger.d.mts +37 -0
  214. package/dist/popover/popover-trigger.d.mts.map +1 -0
  215. package/dist/popover/popover-trigger.mjs +40 -0
  216. package/dist/popover/popover-trigger.mjs.map +1 -0
  217. package/dist/radio-group/index.d.mts +20 -0
  218. package/dist/radio-group/index.d.mts.map +1 -0
  219. package/dist/radio-group/index.mjs +20 -0
  220. package/dist/radio-group/index.mjs.map +1 -0
  221. package/dist/radio-group/radio-group-context.mjs +13 -0
  222. package/dist/radio-group/radio-group-context.mjs.map +1 -0
  223. package/dist/radio-group/radio-group-item.d.mts +54 -0
  224. package/dist/radio-group/radio-group-item.d.mts.map +1 -0
  225. package/dist/radio-group/radio-group-item.mjs +83 -0
  226. package/dist/radio-group/radio-group-item.mjs.map +1 -0
  227. package/dist/radio-group/radio-group-label.d.mts +21 -0
  228. package/dist/radio-group/radio-group-label.d.mts.map +1 -0
  229. package/dist/radio-group/radio-group-label.mjs +30 -0
  230. package/dist/radio-group/radio-group-label.mjs.map +1 -0
  231. package/dist/radio-group/radio-group-root.d.mts +92 -0
  232. package/dist/radio-group/radio-group-root.d.mts.map +1 -0
  233. package/dist/radio-group/radio-group-root.mjs +137 -0
  234. package/dist/radio-group/radio-group-root.mjs.map +1 -0
  235. package/dist/switch/index.d.mts +3 -0
  236. package/dist/switch/index.mjs +1 -0
  237. package/dist/switch/switch.d.mts +78 -0
  238. package/dist/switch/switch.d.mts.map +1 -0
  239. package/dist/switch/switch.mjs +77 -0
  240. package/dist/switch/switch.mjs.map +1 -0
  241. package/dist/tabs/index.d.mts +24 -0
  242. package/dist/tabs/index.d.mts.map +1 -0
  243. package/dist/tabs/index.mjs +23 -0
  244. package/dist/tabs/index.mjs.map +1 -0
  245. package/dist/tabs/tabs-context.d.mts +7 -0
  246. package/dist/tabs/tabs-context.d.mts.map +1 -0
  247. package/dist/tabs/tabs-context.mjs +14 -0
  248. package/dist/tabs/tabs-context.mjs.map +1 -0
  249. package/dist/tabs/tabs-ids.mjs +39 -0
  250. package/dist/tabs/tabs-ids.mjs.map +1 -0
  251. package/dist/tabs/tabs-indicator.d.mts +32 -0
  252. package/dist/tabs/tabs-indicator.d.mts.map +1 -0
  253. package/dist/tabs/tabs-indicator.mjs +49 -0
  254. package/dist/tabs/tabs-indicator.mjs.map +1 -0
  255. package/dist/tabs/tabs-list.d.mts +34 -0
  256. package/dist/tabs/tabs-list.d.mts.map +1 -0
  257. package/dist/tabs/tabs-list.mjs +130 -0
  258. package/dist/tabs/tabs-list.mjs.map +1 -0
  259. package/dist/tabs/tabs-panel.d.mts +50 -0
  260. package/dist/tabs/tabs-panel.d.mts.map +1 -0
  261. package/dist/tabs/tabs-panel.mjs +81 -0
  262. package/dist/tabs/tabs-panel.mjs.map +1 -0
  263. package/dist/tabs/tabs-root.d.mts +64 -0
  264. package/dist/tabs/tabs-root.d.mts.map +1 -0
  265. package/dist/tabs/tabs-root.mjs +90 -0
  266. package/dist/tabs/tabs-root.mjs.map +1 -0
  267. package/dist/tabs/tabs-tab.d.mts +49 -0
  268. package/dist/tabs/tabs-tab.d.mts.map +1 -0
  269. package/dist/tabs/tabs-tab.mjs +89 -0
  270. package/dist/tabs/tabs-tab.mjs.map +1 -0
  271. package/dist/toggle/index.d.mts +3 -0
  272. package/dist/toggle/index.mjs +1 -0
  273. package/dist/toggle/toggle.d.mts +57 -0
  274. package/dist/toggle/toggle.d.mts.map +1 -0
  275. package/dist/toggle/toggle.mjs +59 -0
  276. package/dist/toggle/toggle.mjs.map +1 -0
  277. package/dist/tooltip/index.d.mts +20 -0
  278. package/dist/tooltip/index.d.mts.map +1 -0
  279. package/dist/tooltip/index.mjs +19 -0
  280. package/dist/tooltip/index.mjs.map +1 -0
  281. package/dist/tooltip/tooltip-context.d.mts +5 -0
  282. package/dist/tooltip/tooltip-context.d.mts.map +1 -0
  283. package/dist/tooltip/tooltip-context.mjs +13 -0
  284. package/dist/tooltip/tooltip-context.mjs.map +1 -0
  285. package/dist/tooltip/tooltip-popup.d.mts +49 -0
  286. package/dist/tooltip/tooltip-popup.d.mts.map +1 -0
  287. package/dist/tooltip/tooltip-popup.mjs +52 -0
  288. package/dist/tooltip/tooltip-popup.mjs.map +1 -0
  289. package/dist/tooltip/tooltip-root.d.mts +41 -0
  290. package/dist/tooltip/tooltip-root.d.mts.map +1 -0
  291. package/dist/tooltip/tooltip-root.mjs +86 -0
  292. package/dist/tooltip/tooltip-root.mjs.map +1 -0
  293. package/dist/tooltip/tooltip-trigger.d.mts +38 -0
  294. package/dist/tooltip/tooltip-trigger.d.mts.map +1 -0
  295. package/dist/tooltip/tooltip-trigger.mjs +55 -0
  296. package/dist/tooltip/tooltip-trigger.mjs.map +1 -0
  297. package/dist/tooltip/use-tooltip-interaction.mjs +119 -0
  298. package/dist/tooltip/use-tooltip-interaction.mjs.map +1 -0
  299. package/package.json +73 -3
@@ -0,0 +1,55 @@
1
+ //#region src/internal/platform-attributes.d.ts
2
+ declare module "react" {
3
+ interface HTMLAttributes<T> {
4
+ /**
5
+ * `focusgroup` (Chromium 150+): native roving focus. A behaviour token (`tablist`, `toolbar`,
6
+ * ...) plus modifiers (`inline`, `block`, `wrap`, `nowrap`, `nomemory`).
7
+ * Not in `@types/react` 19.3.0 (no DefinitelyTyped issue filed as of 2026-10-16).
8
+ */
9
+ focusgroup?: string | undefined;
10
+ /**
11
+ * `focusgroupstart` (Chromium 150+): marks the item a `focusgroup` enters on when focus
12
+ * arrives from outside (with `nomemory`, always). Boolean attribute: pass `""`.
13
+ * Not in `@types/react` 19.3.0 (no DefinitelyTyped issue filed as of 2026-10-17).
14
+ */
15
+ focusgroupstart?: "" | undefined;
16
+ }
17
+ interface ButtonHTMLAttributes<T> {
18
+ /**
19
+ * `interestfor` (Interest Invokers, Chromium 142+): id of the element, usually a popover,
20
+ * shown when the user shows interest in this button (hover, keyboard focus, long-press).
21
+ * Not in `@types/react` 19.3.0 (DefinitelyTyped #74664).
22
+ */
23
+ interestfor?: string | undefined;
24
+ /**
25
+ * `commandfor` (Invoker Commands): id of the element the button's `command` acts on.
26
+ * Not in `@types/react` 19.3.0 (DefinitelyTyped #74664).
27
+ */
28
+ commandfor?: string | undefined;
29
+ /**
30
+ * `command` (Invoker Commands): a built-in command (`show-popover`, `hide-popover`,
31
+ * `toggle-popover`, `show-modal`, `close`, `request-close`) or a custom `--command`.
32
+ * Not in `@types/react` 19.3.0 (DefinitelyTyped #74664).
33
+ */
34
+ command?: "show-popover" | "hide-popover" | "toggle-popover" | "show-modal" | "close" | "request-close" | `--${string}` | undefined;
35
+ }
36
+ interface AnchorHTMLAttributes<T> {
37
+ /** `interestfor` on `<a href>` (Chromium 142+). Not in `@types/react` 19.3.0 (DefinitelyTyped #74664). */
38
+ interestfor?: string | undefined;
39
+ }
40
+ interface AreaHTMLAttributes<T> {
41
+ /** `interestfor` on `<area href>` (Chromium 142+). Not in `@types/react` 19.3.0 (DefinitelyTyped #74664). */
42
+ interestfor?: string | undefined;
43
+ }
44
+ interface CSSProperties {
45
+ /** `interest-delay` shorthand (Chromium 142+). Not in `csstype` 3.2.3. */
46
+ interestDelay?: string | undefined;
47
+ /** `interest-delay-start` (Chromium 142+; default `0.25s`). Not in `csstype` 3.2.3. */
48
+ interestDelayStart?: string | undefined;
49
+ /** `interest-delay-end` (Chromium 142+; default `0.15s`). Not in `csstype` 3.2.3. */
50
+ interestDelayEnd?: string | undefined;
51
+ }
52
+ }
53
+ //#endregion
54
+ export {};
55
+ //# sourceMappingURL=platform-attributes.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-attributes.d.mts","names":[],"sources":["../../src/internal/platform-attributes.ts"],"mappings":";;YAkBY,eAAe;;;;;;IAMvB;;;;;;IAMA;;YAGQ,qBAAqB;;;;;;IAM7B;;;;;IAKA;;;;;;IAMA;;YAWQ,qBAAqB;;IAE7B;;YAGQ,mBAAmB;;IAE3B;;YAGQ;;IAER;;IAEA;;IAEA"}
@@ -0,0 +1,19 @@
1
+ //#region src/internal/sync-presence.ts
2
+ /**
3
+ * Makes a boolean attribute's presence match `present`. For group members that may hydrate after
4
+ * their Root (`RadioGroup.Item`, `CheckboxGroup.Item` / `Parent`): hydration keeps server
5
+ * attributes that differ from the client render (React only warns), and a member hydrating late
6
+ * can render a different state than the server did.
7
+ *
8
+ * No `"use client"` directive: no client-only API (conventions 7.1).
9
+ */
10
+ function syncPresence(el, name, present) {
11
+ if (el.hasAttribute(name) !== present) {
12
+ if (present) el.setAttribute(name, "");
13
+ else el.removeAttribute(name);
14
+ }
15
+ }
16
+ //#endregion
17
+ export { syncPresence };
18
+
19
+ //# sourceMappingURL=sync-presence.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-presence.mjs","names":[],"sources":["../../src/internal/sync-presence.ts"],"sourcesContent":["/**\n * Makes a boolean attribute's presence match `present`. For group members that may hydrate after\n * their Root (`RadioGroup.Item`, `CheckboxGroup.Item` / `Parent`): hydration keeps server\n * attributes that differ from the client render (React only warns), and a member hydrating late\n * can render a different state than the server did.\n *\n * No `\"use client\"` directive: no client-only API (conventions 7.1).\n */\nexport function syncPresence(el: Element, name: string, present: boolean): void {\n if (el.hasAttribute(name) !== present) {\n if (present) el.setAttribute(name, \"\");\n else el.removeAttribute(name);\n }\n}\n"],"mappings":";;;;;;;;;AAQA,SAAgB,aAAa,IAAa,MAAc,SAAwB;CAC9E,IAAI,GAAG,aAAa,IAAI,MAAM,SAAS;EACrC,IAAI,SAAS,GAAG,aAAa,MAAM,EAAE;OAChC,GAAG,gBAAgB,IAAI;CAC9B;AACF"}
@@ -0,0 +1,52 @@
1
+ "use client";
2
+ import { ViewTransition, useEffect, useRef } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ //#region src/internal/transition-boundary.tsx
5
+ const isDev = process.env.NODE_ENV !== "production";
6
+ /**
7
+ * Wraps a part's output in `<ViewTransition default={transition}>` **only when the consumer
8
+ * passed `transition`** (conventions 5.1). With `transition === undefined` the node is returned
9
+ * unwrapped and the part renders no boundary at all.
10
+ *
11
+ * Reason (docs/research/view-transition-mount-cost.md, issue #35): react-dom 19.3 starts a
12
+ * document view transition whenever a `<ViewTransition>` mounts or is revealed by `<Activity>` in
13
+ * a transition-lane commit, whatever its class resolves to (`"none"` included). The browser's
14
+ * 250 ms root cross-fade then runs and every later transition commit waits for it. Exits and
15
+ * updates of a `"none"` boundary are free, so an explicit `transition="none"` keeps the boundary
16
+ * and lets a consumer switch between `"none"` and a class without remounting.
17
+ *
18
+ * The boundary stays the outermost node of the part's output, so a part that is the top of an
19
+ * inserted subtree gets enter / exit. A plain function (not a component), so it adds no fiber
20
+ * between the boundary and the element.
21
+ */
22
+ function transitionBoundary(transition, node) {
23
+ if (transition === void 0) return node;
24
+ return /* @__PURE__ */ jsx(ViewTransition, {
25
+ default: transition,
26
+ children: node
27
+ });
28
+ }
29
+ /**
30
+ * {@link transitionBoundary} plus the development warning for a non-static `transition`
31
+ * (conventions 5.1): changing it between `undefined` and a value changes the element type at the
32
+ * part's root, so React remounts the part's subtree (DOM and state lost). Warns once per
33
+ * instance, from an effect. `part` is the public name used in the message (`"Tabs.Panel"`).
34
+ *
35
+ * Pass the *effective* value: a part that ignores `transition` in some mode (Accordion, Dialog
36
+ * JS-free mode) passes `undefined` there.
37
+ */
38
+ function useTransitionBoundary(part, transition, node) {
39
+ const hasBoundary = transition !== void 0;
40
+ const initial = useRef(hasBoundary);
41
+ const warned = useRef(false);
42
+ useEffect(() => {
43
+ if (!isDev || warned.current || hasBoundary === initial.current) return;
44
+ warned.current = true;
45
+ console.error(`${part}: \`transition\` changed between undefined and a value after mount. The part renders a <ViewTransition> only when \`transition\` is given, so this remounts its subtree (DOM and state are lost). Keep \`transition\` static; use \`transition="none"\` to switch animation off without removing the boundary.`);
46
+ }, [hasBoundary, part]);
47
+ return transitionBoundary(transition, node);
48
+ }
49
+ //#endregion
50
+ export { transitionBoundary, useTransitionBoundary };
51
+
52
+ //# sourceMappingURL=transition-boundary.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transition-boundary.mjs","names":[],"sources":["../../src/internal/transition-boundary.tsx"],"sourcesContent":["\"use client\";\nimport { useEffect, useRef, ViewTransition } from \"react\";\nimport type { ReactNode, ViewTransitionClass } from \"react\";\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\n/**\n * Wraps a part's output in `<ViewTransition default={transition}>` **only when the consumer\n * passed `transition`** (conventions 5.1). With `transition === undefined` the node is returned\n * unwrapped and the part renders no boundary at all.\n *\n * Reason (docs/research/view-transition-mount-cost.md, issue #35): react-dom 19.3 starts a\n * document view transition whenever a `<ViewTransition>` mounts or is revealed by `<Activity>` in\n * a transition-lane commit, whatever its class resolves to (`\"none\"` included). The browser's\n * 250 ms root cross-fade then runs and every later transition commit waits for it. Exits and\n * updates of a `\"none\"` boundary are free, so an explicit `transition=\"none\"` keeps the boundary\n * and lets a consumer switch between `\"none\"` and a class without remounting.\n *\n * The boundary stays the outermost node of the part's output, so a part that is the top of an\n * inserted subtree gets enter / exit. A plain function (not a component), so it adds no fiber\n * between the boundary and the element.\n */\nexport function transitionBoundary(\n transition: ViewTransitionClass | undefined,\n node: ReactNode,\n): ReactNode {\n if (transition === undefined) return node;\n return <ViewTransition default={transition}>{node}</ViewTransition>;\n}\n\n/**\n * {@link transitionBoundary} plus the development warning for a non-static `transition`\n * (conventions 5.1): changing it between `undefined` and a value changes the element type at the\n * part's root, so React remounts the part's subtree (DOM and state lost). Warns once per\n * instance, from an effect. `part` is the public name used in the message (`\"Tabs.Panel\"`).\n *\n * Pass the *effective* value: a part that ignores `transition` in some mode (Accordion, Dialog\n * JS-free mode) passes `undefined` there.\n */\nexport function useTransitionBoundary(\n part: string,\n transition: ViewTransitionClass | undefined,\n node: ReactNode,\n): ReactNode {\n const hasBoundary = transition !== undefined;\n const initial = useRef(hasBoundary);\n const warned = useRef(false);\n useEffect(() => {\n if (!isDev || warned.current || hasBoundary === initial.current) return;\n warned.current = true;\n console.error(\n `${part}: \\`transition\\` changed between undefined and a value after mount. The part ` +\n \"renders a <ViewTransition> only when `transition` is given, so this remounts its \" +\n 'subtree (DOM and state are lost). Keep `transition` static; use `transition=\"none\"` ' +\n \"to switch animation off without removing the boundary.\",\n );\n }, [hasBoundary, part]);\n return transitionBoundary(transition, node);\n}\n"],"mappings":";;;;AAIA,MAAM,QAAQ,QAAQ,IAAI,aAAa;;;;;;;;;;;;;;;;;AAkBvC,SAAgB,mBACd,YACA,MACW;CACX,IAAI,eAAe,KAAA,GAAW,OAAO;CACrC,OAAO,oBAAC,gBAAD;EAAgB,SAAS;EAAa,UAAA;CAAqB,CAAA;AACpE;;;;;;;;;;AAWA,SAAgB,sBACd,MACA,YACA,MACW;CACX,MAAM,cAAc,eAAe,KAAA;CACnC,MAAM,UAAU,OAAO,WAAW;CAClC,MAAM,SAAS,OAAO,KAAK;CAC3B,gBAAgB;EACd,IAAI,CAAC,SAAS,OAAO,WAAW,gBAAgB,QAAQ,SAAS;EACjE,OAAO,UAAU;EACjB,QAAQ,MACN,GAAG,KAAK,+SAIV;CACF,GAAG,CAAC,aAAa,IAAI,CAAC;CACtB,OAAO,mBAAmB,YAAY,IAAI;AAC5C"}
@@ -0,0 +1,15 @@
1
+ //#region src/internal/transition-types.ts
2
+ /**
3
+ * Builds the library's View Transition type names for one dispatch (conventions 5.3):
4
+ * `fsui-<component>` plus, when the change has a kind, `fsui-<component>-<change>`.
5
+ *
6
+ * Hyphens only: the names are also matched by `:active-view-transition-type(...)` in CSS.
7
+ */
8
+ function transitionTypes(component, change) {
9
+ const base = `fsui-${component}`;
10
+ return change ? [base, `${base}-${change}`] : [base];
11
+ }
12
+ //#endregion
13
+ export { transitionTypes };
14
+
15
+ //# sourceMappingURL=transition-types.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transition-types.mjs","names":[],"sources":["../../src/internal/transition-types.ts"],"sourcesContent":["/**\n * Builds the library's View Transition type names for one dispatch (conventions 5.3):\n * `fsui-<component>` plus, when the change has a kind, `fsui-<component>-<change>`.\n *\n * Hyphens only: the names are also matched by `:active-view-transition-type(...)` in CSS.\n */\nexport function transitionTypes(\n component: string,\n change?: string | null | undefined,\n): readonly string[] {\n const base = `fsui-${component}`;\n return change ? [base, `${base}-${change}`] : [base];\n}\n"],"mappings":";;;;;;;AAMA,SAAgB,gBACd,WACA,QACmB;CACnB,MAAM,OAAO,QAAQ;CACrB,OAAO,SAAS,CAAC,MAAM,GAAG,KAAK,GAAG,QAAQ,IAAI,CAAC,IAAI;AACrD"}
@@ -0,0 +1,38 @@
1
+ import { ReactElement, ReactNode } from "react";
2
+ //#region src/internal/types.d.ts
3
+ /**
4
+ * A callback the library runs inside a Transition. May be sync or async: the library awaits
5
+ * whatever it returns (a thenable is awaited, anything else is ignored). The return type is
6
+ * `unknown` rather than `void | Promise<void>` so that `(next) => save(next)` compiles when `save`
7
+ * returns data, as Server Functions usually do (conventions 2.2).
8
+ */
9
+ export type Action<Args extends readonly unknown[] = []> = (...args: Args) => unknown;
10
+ export interface ActionDetails {
11
+ /** The DOM event that caused the action, or `undefined` for programmatic changes. */
12
+ readonly event: Event | undefined;
13
+ }
14
+ /** An action called with the next value of a controllable state. */
15
+ export type ChangeAction<T, Details extends ActionDetails = ActionDetails> = Action<[next: T, details: Details]>;
16
+ /**
17
+ * Why a change action was dispatched (conventions 2.2). One library-wide vocabulary: every
18
+ * `<Component><State>ChangeDetails` that exposes `reason` declares it with this type.
19
+ *
20
+ * - `"activate"`: the user acted on a trigger part (click, Enter/Space, label click, focus in an
21
+ * automatic-activation mode). `details.event` is the native event.
22
+ * - `"dismiss"`: the user closed the component without acting on one of its parts: Escape,
23
+ * Android back, an assistive-technology dismiss gesture, or a click outside (light dismiss).
24
+ * `details.event` is the platform's `close` event. Dialog is the first component to use it.
25
+ * - `"platform"`: the browser changed the state itself: find-in-page, a form reset (`event` is
26
+ * the `reset` event), or interaction before hydration found at mount (`event` is `undefined`).
27
+ */
28
+ export type ChangeReason = "activate" | "dismiss" | "platform";
29
+ /** A prop value that is either a plain value or a function of the part's state. */
30
+ export type StateValue<T, State> = T | ((state: State) => T);
31
+ /**
32
+ * Element polymorphism. Element form replaces the default element and the library merges its
33
+ * props into it; function form receives the merged props and the part's state and renders
34
+ * whatever it likes (it must spread `props` onto a DOM element).
35
+ */
36
+ export type RenderProp<Props, State> = ReactElement<Partial<Props>> | ((props: Props, state: State) => ReactNode);
37
+ //#endregion
38
+ //# sourceMappingURL=types.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../../src/internal/types.ts"],"mappings":";;;;;;;;YAQY,OAAO,4CAA4C,MAAM;iBAEpD;;WAEN,OAAO;;;YAIN,aAAa,GAAG,gBAAgB,gBAAgB,iBAAiB,QAC1E,MAAM,GAAG,SAAS;;;;;;;;;;;;;YAeT;;YAGA,WAAW,GAAG,SAAS,MAAM,OAAO,UAAU;;;;;;YAO9C,WAAW,OAAO,SAC1B,aAAa,QAAQ,YACnB,OAAO,OAAO,OAAO,UAAU"}
@@ -0,0 +1,58 @@
1
+ "use client";
2
+ import { useEffect, useRef, useState, useTransition } from "react";
3
+ //#region src/internal/use-action-trigger.ts
4
+ /** Reports an error that cannot reach a boundary (conventions 2.4: never swallow). */
5
+ function report(error) {
6
+ if (typeof reportError === "function") reportError(error);
7
+ else console.error(error);
8
+ }
9
+ /**
10
+ * The transition of a pure-trigger part: an `action` with no state of its own (`Dialog.Close`,
11
+ * `Popover.Close`, `Menu.Item`; conventions 3.4). Same superseded-rejection surfacing as
12
+ * `useControllableAction`: React keeps only the newest thenable per `useTransition`, so the
13
+ * rejection of a run that a later run superseded would be dropped. The hook catches it and throws
14
+ * it from render, so it reaches the same boundary as the latest run's rejection does.
15
+ */
16
+ function useActionTrigger(openGeneration) {
17
+ const [supersededError, setSupersededError] = useState(null);
18
+ const hasSupersededError = useRef(false);
19
+ if (supersededError !== null) throw supersededError.error;
20
+ const [pending, startOwnTransition] = useTransition();
21
+ const latest = useRef(0);
22
+ const mounted = useRef(false);
23
+ useEffect(() => {
24
+ mounted.current = true;
25
+ return () => {
26
+ mounted.current = false;
27
+ };
28
+ }, []);
29
+ const run = (work, then) => {
30
+ const id = ++latest.current;
31
+ const generation = openGeneration?.();
32
+ startOwnTransition(async () => {
33
+ try {
34
+ await work();
35
+ } catch (error) {
36
+ if (id === latest.current) throw error;
37
+ if (!mounted.current) return;
38
+ if (hasSupersededError.current) {
39
+ report(error);
40
+ return;
41
+ }
42
+ hasSupersededError.current = true;
43
+ setSupersededError({ error });
44
+ return;
45
+ }
46
+ if (openGeneration !== void 0 && openGeneration() !== generation) return;
47
+ then?.();
48
+ });
49
+ };
50
+ return {
51
+ pending,
52
+ run
53
+ };
54
+ }
55
+ //#endregion
56
+ export { useActionTrigger };
57
+
58
+ //# sourceMappingURL=use-action-trigger.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-action-trigger.mjs","names":[],"sources":["../../src/internal/use-action-trigger.ts"],"sourcesContent":["\"use client\";\nimport { useEffect, useRef, useState, useTransition } from \"react\";\n\nexport interface ActionTrigger {\n /** A run started through this hook is in flight (the part's own transition). */\n readonly pending: boolean;\n /**\n * Runs `work` inside the part's own transition and, once it resolves, `then` (still inside the\n * transition: a Close part's close, a menu item's close). A rejection skips `then` and reaches\n * the nearest Error Boundary above the part, also when a later run superseded this one. With\n * `openGeneration` (the hook's argument), `then` is skipped when a newer open of the overlay\n * started after the run began.\n */\n readonly run: (work: () => unknown, then?: () => void) => void;\n}\n\n/** Reports an error that cannot reach a boundary (conventions 2.4: never swallow). */\nfunction report(error: unknown): void {\n if (typeof reportError === \"function\") reportError(error);\n else console.error(error);\n}\n\n/**\n * The transition of a pure-trigger part: an `action` with no state of its own (`Dialog.Close`,\n * `Popover.Close`, `Menu.Item`; conventions 3.4). Same superseded-rejection surfacing as\n * `useControllableAction`: React keeps only the newest thenable per `useTransition`, so the\n * rejection of a run that a later run superseded would be dropped. The hook catches it and throws\n * it from render, so it reaches the same boundary as the latest run's rejection does.\n */\nexport function useActionTrigger(openGeneration?: () => number): ActionTrigger {\n // Thrown before `useTransition` on purpose (see use-controllable-action.ts): in the retry of\n // an errored render `useTransition` would suspend on the newer run's pending thenable.\n const [supersededError, setSupersededError] = useState<{ readonly error: unknown } | null>(null);\n const hasSupersededError = useRef(false);\n if (supersededError !== null) throw supersededError.error;\n\n const [pending, startOwnTransition] = useTransition();\n const latest = useRef(0);\n const mounted = useRef(false);\n\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n const run = (work: () => unknown, then?: () => void): void => {\n const id = ++latest.current;\n // The open this run belongs to (design/components/popover.md, review M2 / PO rule): if the\n // user dismisses and re-opens while the action runs, its close must not close the new open.\n const generation = openGeneration?.();\n startOwnTransition(async () => {\n try {\n await work();\n } catch (error) {\n // The latest run's rejection takes React's own route to the boundary.\n if (id === latest.current) throw error;\n if (!mounted.current) return;\n if (hasSupersededError.current) {\n report(error);\n return;\n }\n hasSupersededError.current = true;\n // After an await, outside the transition: urgent, so the part re-renders and throws.\n setSupersededError({ error });\n return;\n }\n if (openGeneration !== undefined && openGeneration() !== generation) return;\n then?.();\n });\n };\n\n return { pending, run };\n}\n"],"mappings":";;;;AAiBA,SAAS,OAAO,OAAsB;CACpC,IAAI,OAAO,gBAAgB,YAAY,YAAY,KAAK;MACnD,QAAQ,MAAM,KAAK;AAC1B;;;;;;;;AASA,SAAgB,iBAAiB,gBAA8C;CAG7E,MAAM,CAAC,iBAAiB,sBAAsB,SAA6C,IAAI;CAC/F,MAAM,qBAAqB,OAAO,KAAK;CACvC,IAAI,oBAAoB,MAAM,MAAM,gBAAgB;CAEpD,MAAM,CAAC,SAAS,sBAAsB,cAAc;CACpD,MAAM,SAAS,OAAO,CAAC;CACvB,MAAM,UAAU,OAAO,KAAK;CAE5B,gBAAgB;EACd,QAAQ,UAAU;EAClB,aAAa;GACX,QAAQ,UAAU;EACpB;CACF,GAAG,CAAC,CAAC;CAEL,MAAM,OAAO,MAAqB,SAA4B;EAC5D,MAAM,KAAK,EAAE,OAAO;EAGpB,MAAM,aAAa,iBAAiB;EACpC,mBAAmB,YAAY;GAC7B,IAAI;IACF,MAAM,KAAK;GACb,SAAS,OAAO;IAEd,IAAI,OAAO,OAAO,SAAS,MAAM;IACjC,IAAI,CAAC,QAAQ,SAAS;IACtB,IAAI,mBAAmB,SAAS;KAC9B,OAAO,KAAK;KACZ;IACF;IACA,mBAAmB,UAAU;IAE7B,mBAAmB,EAAE,MAAM,CAAC;IAC5B;GACF;GACA,IAAI,mBAAmB,KAAA,KAAa,eAAe,MAAM,YAAY;GACrE,OAAO;EACT,CAAC;CACH;CAEA,OAAO;EAAE;EAAS;CAAI;AACxB"}
@@ -0,0 +1,115 @@
1
+ "use client";
2
+ import { addTransitionType, startTransition, useEffect, useOptimistic, useRef, useState, useTransition } from "react";
3
+ //#region src/internal/use-controllable-action.ts
4
+ const isDev = process.env.NODE_ENV !== "production";
5
+ function capitalize(s) {
6
+ return s.charAt(0).toUpperCase() + s.slice(1);
7
+ }
8
+ /** Reports an error that cannot reach a boundary (conventions 2.4: never swallow). */
9
+ function report(error) {
10
+ if (typeof reportError === "function") reportError(error);
11
+ else console.error(error);
12
+ }
13
+ /**
14
+ * The one place the state model of conventions section 3 is implemented: committed state,
15
+ * optimistic state (`useOptimistic`, reducer form), the component's own transition, the
16
+ * post-`await` write of uncontrolled state, the latest-dispatch guard, and the surfacing of
17
+ * superseded rejections (conventions 2.4).
18
+ */
19
+ function useControllableAction(options) {
20
+ const { controlled, defaultValue, initialValue, action, transitionTypes, reducer, warn } = options;
21
+ const [supersededError, setSupersededError] = useState(null);
22
+ const hasSupersededError = useRef(false);
23
+ if (supersededError !== null) throw supersededError.error;
24
+ const [internal, setInternal] = useState(defaultValue === void 0 ? initialValue : defaultValue);
25
+ const isControlled = controlled !== void 0;
26
+ const committed = isControlled ? controlled : internal;
27
+ const [optimistic, setOptimistic] = useOptimistic(committed, (prev, patch) => reducer ? reducer(prev, patch.next, patch.details) : patch.next);
28
+ const [pending, startOwnTransition] = useTransition();
29
+ const latest = useRef(0);
30
+ const inFlight = useRef({
31
+ count: 0,
32
+ next: void 0
33
+ });
34
+ const mounted = useRef(false);
35
+ useDevWarnings(isControlled, defaultValue !== void 0, warn);
36
+ useEffect(() => {
37
+ mounted.current = true;
38
+ return () => {
39
+ mounted.current = false;
40
+ };
41
+ }, []);
42
+ function dispatch(next, details) {
43
+ const prev = committed;
44
+ const id = ++latest.current;
45
+ inFlight.current.count += 1;
46
+ inFlight.current.next = next;
47
+ startOwnTransition(async () => {
48
+ for (const type of transitionTypes?.(next, prev, details) ?? []) addTransitionType(type);
49
+ setOptimistic({
50
+ next,
51
+ details
52
+ });
53
+ try {
54
+ await action?.(next, details);
55
+ } catch (error) {
56
+ settle();
57
+ if (id === latest.current) throw error;
58
+ if (!mounted.current) return;
59
+ if (hasSupersededError.current) {
60
+ report(error);
61
+ return;
62
+ }
63
+ hasSupersededError.current = true;
64
+ setSupersededError({ error });
65
+ return;
66
+ }
67
+ settle();
68
+ if (!isControlled && id === latest.current) startTransition(() => setInternal(next));
69
+ });
70
+ }
71
+ function settle() {
72
+ inFlight.current.count -= 1;
73
+ if (inFlight.current.count === 0) inFlight.current.next = void 0;
74
+ }
75
+ function latestInFlight() {
76
+ const { count, next } = inFlight.current;
77
+ return count > 0 ? { next } : null;
78
+ }
79
+ return {
80
+ committed,
81
+ optimistic,
82
+ pending,
83
+ dispatch,
84
+ latestInFlight
85
+ };
86
+ }
87
+ function useDevWarnings(isControlled, hasDefault, warn) {
88
+ const initialMode = useRef(isControlled);
89
+ const warnedBoth = useRef(false);
90
+ const warnedSwitch = useRef(false);
91
+ const component = warn?.component ?? "A component";
92
+ const prop = warn?.prop ?? "value";
93
+ useEffect(() => {
94
+ if (!isDev) return;
95
+ const defaultProp = `default${capitalize(prop)}`;
96
+ if (isControlled && hasDefault && !warnedBoth.current) {
97
+ warnedBoth.current = true;
98
+ console.error(`${component}: both \`${prop}\` and \`${defaultProp}\` are set. Use \`${prop}\` for controlled mode or \`${defaultProp}\` for uncontrolled mode, not both.`);
99
+ }
100
+ if (initialMode.current !== isControlled && !warnedSwitch.current) {
101
+ warnedSwitch.current = true;
102
+ const from = initialMode.current ? "controlled" : "uncontrolled";
103
+ console.error(`${component}: \`${prop}\` changed from ${from} to ${isControlled ? "controlled" : "uncontrolled"}. Switching modes after mount is not supported; keep \`${prop}\` either always defined or always undefined.`);
104
+ }
105
+ }, [
106
+ component,
107
+ prop,
108
+ isControlled,
109
+ hasDefault
110
+ ]);
111
+ }
112
+ //#endregion
113
+ export { useControllableAction };
114
+
115
+ //# sourceMappingURL=use-controllable-action.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-controllable-action.mjs","names":[],"sources":["../../src/internal/use-controllable-action.ts"],"sourcesContent":["\"use client\";\nimport {\n addTransitionType,\n startTransition,\n useEffect,\n useOptimistic,\n useRef,\n useState,\n useTransition,\n} from \"react\";\nimport type { ActionDetails, ChangeAction } from \"./types.js\";\n\nexport interface UseControllableActionOptions<T, Details extends ActionDetails> {\n /** The controlled prop (`x`). `undefined` means uncontrolled. */\n controlled: T | undefined;\n /** The `defaultX` prop as the consumer passed it; `undefined` when omitted. */\n defaultValue: T | undefined;\n /** Initial uncontrolled value when neither `x` nor `defaultX` is given. */\n initialValue: T;\n /** The `xChangeAction` prop. Awaited inside this hook's transition. */\n action: ChangeAction<T, Details> | undefined;\n /**\n * View Transition types to attach to the commit that ends a dispatch. Receives the previous\n * committed value and the details object (Tabs derives next/prev direction from them).\n */\n transitionTypes?: (next: T, prev: T, details: Details) => readonly string[];\n /**\n * Optimistic reducer (conventions 3.4). Default: replace the value with `next`. Multi-item\n * states (Accordion's open set) pass a reducer keyed by item, reading the item from `details`,\n * so stacked optimistic patches re-base onto a changed committed value.\n */\n reducer?: (prev: T, next: T, details: Details) => T;\n /** Names used in development warnings. */\n warn?: { readonly component: string; readonly prop: string };\n}\n\nexport interface ControllableAction<T, Details extends ActionDetails> {\n /** Committed value (`x ?? internal`). Content regions render this. */\n readonly committed: T;\n /** Optimistic value; trigger parts render this. */\n readonly optimistic: T;\n /** An action dispatched through this hook is in flight. */\n readonly pending: boolean;\n /** Runs the action inside the hook's transition with optimistic feedback. */\n readonly dispatch: (next: T, details: Details) => void;\n /**\n * The `next` of the most recent dispatch while any dispatch's action is still unsettled, else\n * `null`. Lets a component with a browser-owned path (Accordion's find-in-page reveal) build on\n * what the user already intends instead of on the DOM. Read it from handlers and effects only.\n */\n readonly latestInFlight: () => { readonly next: T } | null;\n}\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\nfunction capitalize(s: string): string {\n return s.charAt(0).toUpperCase() + s.slice(1);\n}\n\n/** Reports an error that cannot reach a boundary (conventions 2.4: never swallow). */\nfunction report(error: unknown): void {\n if (typeof reportError === \"function\") reportError(error);\n else console.error(error);\n}\n\n/**\n * The one place the state model of conventions section 3 is implemented: committed state,\n * optimistic state (`useOptimistic`, reducer form), the component's own transition, the\n * post-`await` write of uncontrolled state, the latest-dispatch guard, and the surfacing of\n * superseded rejections (conventions 2.4).\n */\nexport function useControllableAction<T, Details extends ActionDetails = ActionDetails>(\n options: UseControllableActionOptions<T, Details>,\n): ControllableAction<T, Details> {\n const { controlled, defaultValue, initialValue, action, transitionTypes, reducer, warn } =\n options;\n\n // React keeps only the newest thenable per `useTransition`, so a rejection from a dispatch that\n // was superseded by a later one would be dropped entirely (no boundary, no `reportError`). The\n // hook catches those itself, keeps the first, and throws it from render so it reaches the same\n // nearest Error Boundary as the latest dispatch's rejection does through React.\n //\n // The throw sits before `useTransition` on purpose: React retries an errored render with every\n // pending lane, and in that retry `useTransition` unwraps the still-pending action thenable and\n // suspends, which would hold the boundary commit until the newer action settles.\n const [supersededError, setSupersededError] = useState<{ readonly error: unknown } | null>(null);\n const hasSupersededError = useRef(false);\n if (supersededError !== null) throw supersededError.error;\n\n const [internal, setInternal] = useState<T>(\n defaultValue === undefined ? initialValue : defaultValue,\n );\n const isControlled = controlled !== undefined;\n const committed = isControlled ? controlled : internal;\n const [optimistic, setOptimistic] = useOptimistic(\n committed,\n (prev: T, patch: { next: T; details: Details }) =>\n reducer ? reducer(prev, patch.next, patch.details) : patch.next,\n );\n const [pending, startOwnTransition] = useTransition();\n const latest = useRef(0);\n const inFlight = useRef<{ count: number; next: T | undefined }>({ count: 0, next: undefined });\n const mounted = useRef(false);\n\n useDevWarnings(isControlled, defaultValue !== undefined, warn);\n\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n function dispatch(next: T, details: Details): void {\n const prev = committed;\n const id = ++latest.current;\n inFlight.current.count += 1;\n inFlight.current.next = next;\n startOwnTransition(async () => {\n for (const type of transitionTypes?.(next, prev, details) ?? []) addTransitionType(type);\n setOptimistic({ next, details });\n try {\n await action?.(next, details);\n } catch (error) {\n settle();\n // The latest dispatch's rejection takes React's own route to the boundary.\n if (id === latest.current) throw error;\n // Nothing to throw to once unmounted (React drops its own thenables the same way).\n if (!mounted.current) return;\n if (hasSupersededError.current) {\n // One boundary reset per burst is enough; later errors are still reported.\n report(error);\n return;\n }\n hasSupersededError.current = true;\n // Outside the transition by now (after an await), so this is an urgent update: the owner\n // re-renders promptly and throws.\n setSupersededError({ error });\n return;\n }\n settle();\n if (!isControlled && id === latest.current) {\n startTransition(() => setInternal(next));\n }\n });\n }\n\n function settle(): void {\n inFlight.current.count -= 1;\n if (inFlight.current.count === 0) inFlight.current.next = undefined;\n }\n\n function latestInFlight(): { readonly next: T } | null {\n const { count, next } = inFlight.current;\n return count > 0 ? { next: next as T } : null;\n }\n\n return { committed, optimistic, pending, dispatch, latestInFlight };\n}\n\nfunction useDevWarnings(\n isControlled: boolean,\n hasDefault: boolean,\n warn: { readonly component: string; readonly prop: string } | undefined,\n): void {\n const initialMode = useRef(isControlled);\n const warnedBoth = useRef(false);\n const warnedSwitch = useRef(false);\n const component = warn?.component ?? \"A component\";\n const prop = warn?.prop ?? \"value\";\n\n useEffect(() => {\n if (!isDev) return;\n const defaultProp = `default${capitalize(prop)}`;\n\n if (isControlled && hasDefault && !warnedBoth.current) {\n warnedBoth.current = true;\n console.error(\n `${component}: both \\`${prop}\\` and \\`${defaultProp}\\` are set. ` +\n `Use \\`${prop}\\` for controlled mode or \\`${defaultProp}\\` for uncontrolled mode, not both.`,\n );\n }\n\n if (initialMode.current !== isControlled && !warnedSwitch.current) {\n warnedSwitch.current = true;\n const from = initialMode.current ? \"controlled\" : \"uncontrolled\";\n const to = isControlled ? \"controlled\" : \"uncontrolled\";\n console.error(\n `${component}: \\`${prop}\\` changed from ${from} to ${to}. Switching modes after mount ` +\n `is not supported; keep \\`${prop}\\` either always defined or always undefined.`,\n );\n }\n }, [component, prop, isControlled, hasDefault]);\n}\n"],"mappings":";;;AAqDA,MAAM,QAAQ,QAAQ,IAAI,aAAa;AAEvC,SAAS,WAAW,GAAmB;CACrC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,EAAE,MAAM,CAAC;AAC9C;;AAGA,SAAS,OAAO,OAAsB;CACpC,IAAI,OAAO,gBAAgB,YAAY,YAAY,KAAK;MACnD,QAAQ,MAAM,KAAK;AAC1B;;;;;;;AAQA,SAAgB,sBACd,SACgC;CAChC,MAAM,EAAE,YAAY,cAAc,cAAc,QAAQ,iBAAiB,SAAS,SAChF;CAUF,MAAM,CAAC,iBAAiB,sBAAsB,SAA6C,IAAI;CAC/F,MAAM,qBAAqB,OAAO,KAAK;CACvC,IAAI,oBAAoB,MAAM,MAAM,gBAAgB;CAEpD,MAAM,CAAC,UAAU,eAAe,SAC9B,iBAAiB,KAAA,IAAY,eAAe,YAC9C;CACA,MAAM,eAAe,eAAe,KAAA;CACpC,MAAM,YAAY,eAAe,aAAa;CAC9C,MAAM,CAAC,YAAY,iBAAiB,cAClC,YACC,MAAS,UACR,UAAU,QAAQ,MAAM,MAAM,MAAM,MAAM,OAAO,IAAI,MAAM,IAC/D;CACA,MAAM,CAAC,SAAS,sBAAsB,cAAc;CACpD,MAAM,SAAS,OAAO,CAAC;CACvB,MAAM,WAAW,OAA+C;EAAE,OAAO;EAAG,MAAM,KAAA;CAAU,CAAC;CAC7F,MAAM,UAAU,OAAO,KAAK;CAE5B,eAAe,cAAc,iBAAiB,KAAA,GAAW,IAAI;CAE7D,gBAAgB;EACd,QAAQ,UAAU;EAClB,aAAa;GACX,QAAQ,UAAU;EACpB;CACF,GAAG,CAAC,CAAC;CAEL,SAAS,SAAS,MAAS,SAAwB;EACjD,MAAM,OAAO;EACb,MAAM,KAAK,EAAE,OAAO;EACpB,SAAS,QAAQ,SAAS;EAC1B,SAAS,QAAQ,OAAO;EACxB,mBAAmB,YAAY;GAC7B,KAAK,MAAM,QAAQ,kBAAkB,MAAM,MAAM,OAAO,KAAK,CAAC,GAAG,kBAAkB,IAAI;GACvF,cAAc;IAAE;IAAM;GAAQ,CAAC;GAC/B,IAAI;IACF,MAAM,SAAS,MAAM,OAAO;GAC9B,SAAS,OAAO;IACd,OAAO;IAEP,IAAI,OAAO,OAAO,SAAS,MAAM;IAEjC,IAAI,CAAC,QAAQ,SAAS;IACtB,IAAI,mBAAmB,SAAS;KAE9B,OAAO,KAAK;KACZ;IACF;IACA,mBAAmB,UAAU;IAG7B,mBAAmB,EAAE,MAAM,CAAC;IAC5B;GACF;GACA,OAAO;GACP,IAAI,CAAC,gBAAgB,OAAO,OAAO,SACjC,sBAAsB,YAAY,IAAI,CAAC;EAE3C,CAAC;CACH;CAEA,SAAS,SAAe;EACtB,SAAS,QAAQ,SAAS;EAC1B,IAAI,SAAS,QAAQ,UAAU,GAAG,SAAS,QAAQ,OAAO,KAAA;CAC5D;CAEA,SAAS,iBAA8C;EACrD,MAAM,EAAE,OAAO,SAAS,SAAS;EACjC,OAAO,QAAQ,IAAI,EAAQ,KAAU,IAAI;CAC3C;CAEA,OAAO;EAAE;EAAW;EAAY;EAAS;EAAU;CAAe;AACpE;AAEA,SAAS,eACP,cACA,YACA,MACM;CACN,MAAM,cAAc,OAAO,YAAY;CACvC,MAAM,aAAa,OAAO,KAAK;CAC/B,MAAM,eAAe,OAAO,KAAK;CACjC,MAAM,YAAY,MAAM,aAAa;CACrC,MAAM,OAAO,MAAM,QAAQ;CAE3B,gBAAgB;EACd,IAAI,CAAC,OAAO;EACZ,MAAM,cAAc,UAAU,WAAW,IAAI;EAE7C,IAAI,gBAAgB,cAAc,CAAC,WAAW,SAAS;GACrD,WAAW,UAAU;GACrB,QAAQ,MACN,GAAG,UAAU,WAAW,KAAK,WAAW,YAAY,oBACzC,KAAK,8BAA8B,YAAY,oCAC5D;EACF;EAEA,IAAI,YAAY,YAAY,gBAAgB,CAAC,aAAa,SAAS;GACjE,aAAa,UAAU;GACvB,MAAM,OAAO,YAAY,UAAU,eAAe;GAElD,QAAQ,MACN,GAAG,UAAU,MAAM,KAAK,kBAAkB,KAAK,MAFtC,eAAe,eAAe,eAEiB,yDAC1B,KAAK,8CACrC;EACF;CACF,GAAG;EAAC;EAAW;EAAM;EAAc;CAAU,CAAC;AAChD"}
@@ -0,0 +1,40 @@
1
+ "use client";
2
+ import { useEffect, useState } from "react";
3
+ //#region src/internal/use-delayed-pair.ts
4
+ /** Plain factory, for code that owns its timers inside an effect (Tooltip). */
5
+ function createDelayedPair() {
6
+ let timer;
7
+ let kind;
8
+ const cancel = () => {
9
+ if (timer !== void 0) clearTimeout(timer);
10
+ timer = void 0;
11
+ kind = void 0;
12
+ };
13
+ const schedule = (next, delay, callback) => {
14
+ cancel();
15
+ kind = next;
16
+ timer = setTimeout(() => {
17
+ timer = void 0;
18
+ kind = void 0;
19
+ callback();
20
+ }, delay);
21
+ };
22
+ return {
23
+ open: (delay, callback) => schedule("open", delay, callback),
24
+ close: (delay, callback) => schedule("close", delay, callback),
25
+ cancelClose: () => {
26
+ if (kind === "close") cancel();
27
+ },
28
+ cancel
29
+ };
30
+ }
31
+ /** One `DelayedPair` per component instance, cancelled on unmount. */
32
+ function useDelayedPair() {
33
+ const [pair] = useState(createDelayedPair);
34
+ useEffect(() => pair.cancel, [pair]);
35
+ return pair;
36
+ }
37
+ //#endregion
38
+ export { createDelayedPair, useDelayedPair };
39
+
40
+ //# sourceMappingURL=use-delayed-pair.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-delayed-pair.mjs","names":[],"sources":["../../src/internal/use-delayed-pair.ts"],"sourcesContent":["\"use client\";\nimport { useEffect, useState } from \"react\";\n\n/**\n * An open timer and a close timer that exclude each other: scheduling one cancels the other, so a\n * hover that leaves before the open delay elapses never opens, and coming back before the close\n * delay elapses never closes. Shared by Tooltip's JS path and Menu's submenu hover intent\n * (design/components/menu.md 7).\n */\nexport interface DelayedPair {\n /** Run `callback` after `delay` ms, replacing any pending open or close. */\n readonly open: (delay: number, callback: () => void) => void;\n /** Run `callback` after `delay` ms, replacing any pending open or close. */\n readonly close: (delay: number, callback: () => void) => void;\n /** Cancel a pending close only (a pending open keeps running). */\n readonly cancelClose: () => void;\n /** Cancel whatever is pending. */\n readonly cancel: () => void;\n}\n\n/** Plain factory, for code that owns its timers inside an effect (Tooltip). */\nexport function createDelayedPair(): DelayedPair {\n let timer: ReturnType<typeof setTimeout> | undefined;\n let kind: \"open\" | \"close\" | undefined;\n\n const cancel = (): void => {\n if (timer !== undefined) clearTimeout(timer);\n timer = undefined;\n kind = undefined;\n };\n const schedule = (next: \"open\" | \"close\", delay: number, callback: () => void): void => {\n cancel();\n kind = next;\n timer = setTimeout(() => {\n timer = undefined;\n kind = undefined;\n callback();\n }, delay);\n };\n\n return {\n open: (delay, callback) => schedule(\"open\", delay, callback),\n close: (delay, callback) => schedule(\"close\", delay, callback),\n cancelClose: () => {\n if (kind === \"close\") cancel();\n },\n cancel,\n };\n}\n\n/** One `DelayedPair` per component instance, cancelled on unmount. */\nexport function useDelayedPair(): DelayedPair {\n const [pair] = useState(createDelayedPair);\n useEffect(() => pair.cancel, [pair]);\n return pair;\n}\n"],"mappings":";;;;AAqBA,SAAgB,oBAAiC;CAC/C,IAAI;CACJ,IAAI;CAEJ,MAAM,eAAqB;EACzB,IAAI,UAAU,KAAA,GAAW,aAAa,KAAK;EAC3C,QAAQ,KAAA;EACR,OAAO,KAAA;CACT;CACA,MAAM,YAAY,MAAwB,OAAe,aAA+B;EACtF,OAAO;EACP,OAAO;EACP,QAAQ,iBAAiB;GACvB,QAAQ,KAAA;GACR,OAAO,KAAA;GACP,SAAS;EACX,GAAG,KAAK;CACV;CAEA,OAAO;EACL,OAAO,OAAO,aAAa,SAAS,QAAQ,OAAO,QAAQ;EAC3D,QAAQ,OAAO,aAAa,SAAS,SAAS,OAAO,QAAQ;EAC7D,mBAAmB;GACjB,IAAI,SAAS,SAAS,OAAO;EAC/B;EACA;CACF;AACF;;AAGA,SAAgB,iBAA8B;CAC5C,MAAM,CAAC,QAAQ,SAAS,iBAAiB;CACzC,gBAAgB,KAAK,QAAQ,CAAC,IAAI,CAAC;CACnC,OAAO;AACT"}
@@ -0,0 +1,21 @@
1
+ "use client";
2
+ import { useEffect, useRef } from "react";
3
+ //#region src/internal/use-dev-warning.ts
4
+ const isDev = process.env.NODE_ENV !== "production";
5
+ /**
6
+ * Logs `message` through `console.error` once per component instance, the first time
7
+ * `condition` is true. Runs in an effect (never during render) so the warning cannot fire for a
8
+ * render that is discarded.
9
+ */
10
+ function useDevWarning(condition, message) {
11
+ const warned = useRef(false);
12
+ useEffect(() => {
13
+ if (!isDev || !condition || warned.current) return;
14
+ warned.current = true;
15
+ console.error(message);
16
+ }, [condition, message]);
17
+ }
18
+ //#endregion
19
+ export { useDevWarning };
20
+
21
+ //# sourceMappingURL=use-dev-warning.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-dev-warning.mjs","names":[],"sources":["../../src/internal/use-dev-warning.ts"],"sourcesContent":["\"use client\";\nimport { useEffect, useRef } from \"react\";\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\n/**\n * Logs `message` through `console.error` once per component instance, the first time\n * `condition` is true. Runs in an effect (never during render) so the warning cannot fire for a\n * render that is discarded.\n */\nexport function useDevWarning(condition: boolean, message: string): void {\n const warned = useRef(false);\n useEffect(() => {\n if (!isDev || !condition || warned.current) return;\n warned.current = true;\n console.error(message);\n }, [condition, message]);\n}\n"],"mappings":";;;AAGA,MAAM,QAAQ,QAAQ,IAAI,aAAa;;;;;;AAOvC,SAAgB,cAAc,WAAoB,SAAuB;CACvE,MAAM,SAAS,OAAO,KAAK;CAC3B,gBAAgB;EACd,IAAI,CAAC,SAAS,CAAC,aAAa,OAAO,SAAS;EAC5C,OAAO,UAAU;EACjB,QAAQ,MAAM,OAAO;CACvB,GAAG,CAAC,WAAW,OAAO,CAAC;AACzB"}
@@ -0,0 +1,55 @@
1
+ "use client";
2
+ import { useRef, useState } from "react";
3
+ //#region src/internal/use-exit-animation.ts
4
+ /** An animation that ends: infinite ones (a spinner) would hold the content forever. */
5
+ function ends(animation) {
6
+ const end = animation.effect?.getComputedTiming().endTime;
7
+ return typeof end === "number" && Number.isFinite(end);
8
+ }
9
+ /**
10
+ * The running exit animations of a top-layer element that has just been hidden, read after a
11
+ * style flush so that `transition-behavior: allow-discrete` exits exist. Empty where the engine
12
+ * does not animate the exit (Firefox) or there is no CSS exit.
13
+ */
14
+ function exitAnimations(element) {
15
+ if (!element.isConnected || typeof element.getAnimations !== "function") return [];
16
+ getComputedStyle(element).opacity;
17
+ return element.getAnimations({ subtree: true }).filter(ends);
18
+ }
19
+ /**
20
+ * The deferred unmount of a top-layer popup's content while its hide animates. `held` is cleared
21
+ * with a normal (urgent) update, never in a transition: the popup is already hidden, nothing
22
+ * visible changes, and only transition commits start a document view transition, so a dismissal
23
+ * runs one (its notification commit) instead of two (design 4.4).
24
+ */
25
+ function useExitAnimation() {
26
+ const [held, setHeld] = useState(false);
27
+ const wait = useRef(null);
28
+ const generation = useRef(0);
29
+ const holdUntil = (animations) => {
30
+ const current = ++generation.current;
31
+ wait.current = current;
32
+ setHeld(true);
33
+ Promise.allSettled(animations.map((animation) => animation.finished)).then(() => {
34
+ if (generation.current !== current) return;
35
+ wait.current = null;
36
+ setHeld(false);
37
+ });
38
+ };
39
+ const release = () => {
40
+ generation.current++;
41
+ wait.current = null;
42
+ setHeld(false);
43
+ };
44
+ return {
45
+ held,
46
+ holdUntil,
47
+ release,
48
+ holdDuringRender: () => setHeld(true),
49
+ waiting: () => wait.current !== null
50
+ };
51
+ }
52
+ //#endregion
53
+ export { exitAnimations, useExitAnimation };
54
+
55
+ //# sourceMappingURL=use-exit-animation.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-exit-animation.mjs","names":[],"sources":["../../src/internal/use-exit-animation.ts"],"sourcesContent":["\"use client\";\nimport { useRef, useState } from \"react\";\n\n/** An animation that ends: infinite ones (a spinner) would hold the content forever. */\nfunction ends(animation: Animation): boolean {\n const end = animation.effect?.getComputedTiming().endTime;\n return typeof end === \"number\" && Number.isFinite(end);\n}\n\n/**\n * The running exit animations of a top-layer element that has just been hidden, read after a\n * style flush so that `transition-behavior: allow-discrete` exits exist. Empty where the engine\n * does not animate the exit (Firefox) or there is no CSS exit.\n */\nexport function exitAnimations(element: Element): Animation[] {\n if (!element.isConnected || typeof element.getAnimations !== \"function\") return [];\n void getComputedStyle(element).opacity;\n return element.getAnimations({ subtree: true }).filter(ends);\n}\n\nexport interface ExitAnimation {\n /** Keep the content mounted (design/components/popover.md 4.4). */\n readonly held: boolean;\n /** Set `held` now (urgent, library-internal) and clear it when `animations` settle. */\n readonly holdUntil: (animations: readonly Animation[]) => void;\n /** Clear `held` now (a normal update, never a transition), cancelling any wait in progress. */\n readonly release: () => void;\n /** Set `held` during render (the \"previous value in state\" pattern). */\n readonly holdDuringRender: () => void;\n /** Whether a `holdUntil` wait is still running. Handlers and effects only. */\n readonly waiting: () => boolean;\n}\n\n/**\n * The deferred unmount of a top-layer popup's content while its hide animates. `held` is cleared\n * with a normal (urgent) update, never in a transition: the popup is already hidden, nothing\n * visible changes, and only transition commits start a document view transition, so a dismissal\n * runs one (its notification commit) instead of two (design 4.4).\n */\nexport function useExitAnimation(): ExitAnimation {\n const [held, setHeld] = useState(false);\n // The generation of the wait in progress, or null; a newer wait supersedes an older one.\n const wait = useRef<number | null>(null);\n const generation = useRef(0);\n\n const holdUntil = (animations: readonly Animation[]): void => {\n const current = ++generation.current;\n wait.current = current;\n setHeld(true);\n void Promise.allSettled(animations.map((animation) => animation.finished)).then(() => {\n if (generation.current !== current) return;\n wait.current = null;\n setHeld(false);\n });\n };\n\n const release = (): void => {\n generation.current++;\n wait.current = null;\n setHeld(false);\n };\n\n return {\n held,\n holdUntil,\n release,\n holdDuringRender: () => setHeld(true),\n waiting: () => wait.current !== null,\n };\n}\n"],"mappings":";;;;AAIA,SAAS,KAAK,WAA+B;CAC3C,MAAM,MAAM,UAAU,QAAQ,kBAAkB,CAAC,CAAC;CAClD,OAAO,OAAO,QAAQ,YAAY,OAAO,SAAS,GAAG;AACvD;;;;;;AAOA,SAAgB,eAAe,SAA+B;CAC5D,IAAI,CAAC,QAAQ,eAAe,OAAO,QAAQ,kBAAkB,YAAY,OAAO,CAAC;CACjF,iBAAsB,OAAO,CAAC,CAAC;CAC/B,OAAO,QAAQ,cAAc,EAAE,SAAS,KAAK,CAAC,CAAC,CAAC,OAAO,IAAI;AAC7D;;;;;;;AAqBA,SAAgB,mBAAkC;CAChD,MAAM,CAAC,MAAM,WAAW,SAAS,KAAK;CAEtC,MAAM,OAAO,OAAsB,IAAI;CACvC,MAAM,aAAa,OAAO,CAAC;CAE3B,MAAM,aAAa,eAA2C;EAC5D,MAAM,UAAU,EAAE,WAAW;EAC7B,KAAK,UAAU;EACf,QAAQ,IAAI;EACZ,QAAa,WAAW,WAAW,KAAK,cAAc,UAAU,QAAQ,CAAC,CAAC,CAAC,WAAW;GACpF,IAAI,WAAW,YAAY,SAAS;GACpC,KAAK,UAAU;GACf,QAAQ,KAAK;EACf,CAAC;CACH;CAEA,MAAM,gBAAsB;EAC1B,WAAW;EACX,KAAK,UAAU;EACf,QAAQ,KAAK;CACf;CAEA,OAAO;EACL;EACA;EACA;EACA,wBAAwB,QAAQ,IAAI;EACpC,eAAe,KAAK,YAAY;CAClC;AACF"}
@@ -0,0 +1,50 @@
1
+ "use client";
2
+ import { useEffectEvent, useLayoutEffect, useRef } from "react";
3
+ //#region src/internal/use-form-reset.ts
4
+ /**
5
+ * Stable `reset` subscription on the form owner of `ref.current` (conventions 3.8).
6
+ *
7
+ * - The owner (`element.form`, which honours the `form` attribute) is re-read after every commit,
8
+ * but the listener is moved only when the owner changed. It is never removed and re-added on an
9
+ * ordinary re-render: React's automatic reset after a successful `<form action>` runs in the
10
+ * commit's mutation phase, between an effect's cleanup and its next run, so a subscription that
11
+ * followed every commit would miss it (rule 4).
12
+ * - The `reset` event fires before the reset algorithm, so `onReset` is called in a microtask,
13
+ * when the inputs hold their reset values (rule 5), and only if no listener cancelled the reset
14
+ * (rule 7).
15
+ * - `onReset` always sees the latest committed render's values (`useEffectEvent`).
16
+ */
17
+ function useFormReset(ref, onReset) {
18
+ const handleReset = useEffectEvent((event) => {
19
+ onReset(event);
20
+ });
21
+ const subscription = useRef(null);
22
+ useLayoutEffect(() => {
23
+ const form = ref.current?.form ?? null;
24
+ const current = subscription.current;
25
+ if (current !== null && current.form === form) return;
26
+ if (current !== null) current.form.removeEventListener("reset", current.listener);
27
+ subscription.current = null;
28
+ if (form === null) return;
29
+ const listener = (event) => {
30
+ queueMicrotask(() => {
31
+ if (event.defaultPrevented) return;
32
+ handleReset(event);
33
+ });
34
+ };
35
+ form.addEventListener("reset", listener);
36
+ subscription.current = {
37
+ form,
38
+ listener
39
+ };
40
+ });
41
+ useLayoutEffect(() => () => {
42
+ const current = subscription.current;
43
+ if (current !== null) current.form.removeEventListener("reset", current.listener);
44
+ subscription.current = null;
45
+ }, []);
46
+ }
47
+ //#endregion
48
+ export { useFormReset };
49
+
50
+ //# sourceMappingURL=use-form-reset.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-form-reset.mjs","names":[],"sources":["../../src/internal/use-form-reset.ts"],"sourcesContent":["\"use client\";\nimport { useEffectEvent, useLayoutEffect, useRef } from \"react\";\nimport type { RefObject } from \"react\";\n\n/** A form-associated element whose form owner the hook follows. */\nexport type FormAssociatedElement = HTMLInputElement | HTMLFieldSetElement;\n\ninterface Subscription {\n readonly form: HTMLFormElement;\n readonly listener: (event: Event) => void;\n}\n\n/**\n * Stable `reset` subscription on the form owner of `ref.current` (conventions 3.8).\n *\n * - The owner (`element.form`, which honours the `form` attribute) is re-read after every commit,\n * but the listener is moved only when the owner changed. It is never removed and re-added on an\n * ordinary re-render: React's automatic reset after a successful `<form action>` runs in the\n * commit's mutation phase, between an effect's cleanup and its next run, so a subscription that\n * followed every commit would miss it (rule 4).\n * - The `reset` event fires before the reset algorithm, so `onReset` is called in a microtask,\n * when the inputs hold their reset values (rule 5), and only if no listener cancelled the reset\n * (rule 7).\n * - `onReset` always sees the latest committed render's values (`useEffectEvent`).\n */\nexport function useFormReset(\n ref: RefObject<FormAssociatedElement | null>,\n onReset: (event: Event) => void,\n): void {\n const handleReset = useEffectEvent((event: Event) => {\n onReset(event);\n });\n const subscription = useRef<Subscription | null>(null);\n\n // No deps on purpose: follows the form owner after every commit (a changed `form` attribute, the\n // element moved to another form), but re-subscribes only when the owner actually changed.\n useLayoutEffect(() => {\n const form = ref.current?.form ?? null;\n const current = subscription.current;\n if (current !== null && current.form === form) return;\n if (current !== null) current.form.removeEventListener(\"reset\", current.listener);\n subscription.current = null;\n if (form === null) return;\n const listener = (event: Event) => {\n queueMicrotask(() => {\n if (event.defaultPrevented) return;\n handleReset(event);\n });\n };\n form.addEventListener(\"reset\", listener);\n subscription.current = { form, listener };\n });\n\n // The only place the subscription is torn down: unmount (and StrictMode's simulated unmount,\n // after which the effect above subscribes again).\n useLayoutEffect(\n () => () => {\n const current = subscription.current;\n if (current !== null) current.form.removeEventListener(\"reset\", current.listener);\n subscription.current = null;\n },\n [],\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAyBA,SAAgB,aACd,KACA,SACM;CACN,MAAM,cAAc,gBAAgB,UAAiB;EACnD,QAAQ,KAAK;CACf,CAAC;CACD,MAAM,eAAe,OAA4B,IAAI;CAIrD,sBAAsB;EACpB,MAAM,OAAO,IAAI,SAAS,QAAQ;EAClC,MAAM,UAAU,aAAa;EAC7B,IAAI,YAAY,QAAQ,QAAQ,SAAS,MAAM;EAC/C,IAAI,YAAY,MAAM,QAAQ,KAAK,oBAAoB,SAAS,QAAQ,QAAQ;EAChF,aAAa,UAAU;EACvB,IAAI,SAAS,MAAM;EACnB,MAAM,YAAY,UAAiB;GACjC,qBAAqB;IACnB,IAAI,MAAM,kBAAkB;IAC5B,YAAY,KAAK;GACnB,CAAC;EACH;EACA,KAAK,iBAAiB,SAAS,QAAQ;EACvC,aAAa,UAAU;GAAE;GAAM;EAAS;CAC1C,CAAC;CAID,4BACc;EACV,MAAM,UAAU,aAAa;EAC7B,IAAI,YAAY,MAAM,QAAQ,KAAK,oBAAoB,SAAS,QAAQ,QAAQ;EAChF,aAAa,UAAU;CACzB,GACA,CAAC,CACH;AACF"}