@jini-ai/admin 0.1.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 (235) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +138 -0
  3. package/dist/browser/index.d.ts +8 -0
  4. package/dist/browser/index.d.ts.map +1 -0
  5. package/dist/browser/index.js +8 -0
  6. package/dist/browser/index.js.map +1 -0
  7. package/dist/browser/navigation.d.ts +39 -0
  8. package/dist/browser/navigation.d.ts.map +1 -0
  9. package/dist/browser/navigation.js +113 -0
  10. package/dist/browser/navigation.js.map +1 -0
  11. package/dist/core/gated/types.d.ts +87 -0
  12. package/dist/core/gated/types.d.ts.map +1 -0
  13. package/dist/core/gated/types.js +37 -0
  14. package/dist/core/gated/types.js.map +1 -0
  15. package/dist/core/index.d.ts +20 -0
  16. package/dist/core/index.d.ts.map +1 -0
  17. package/dist/core/index.js +15 -0
  18. package/dist/core/index.js.map +1 -0
  19. package/dist/core/manifest/rules.d.ts +84 -0
  20. package/dist/core/manifest/rules.d.ts.map +1 -0
  21. package/dist/core/manifest/rules.js +130 -0
  22. package/dist/core/manifest/rules.js.map +1 -0
  23. package/dist/core/manifest/types.d.ts +154 -0
  24. package/dist/core/manifest/types.d.ts.map +1 -0
  25. package/dist/core/manifest/types.js +31 -0
  26. package/dist/core/manifest/types.js.map +1 -0
  27. package/dist/core/permissions/rules.d.ts +32 -0
  28. package/dist/core/permissions/rules.d.ts.map +1 -0
  29. package/dist/core/permissions/rules.js +34 -0
  30. package/dist/core/permissions/rules.js.map +1 -0
  31. package/dist/core/ports/analytics.d.ts +35 -0
  32. package/dist/core/ports/analytics.d.ts.map +1 -0
  33. package/dist/core/ports/analytics.js +14 -0
  34. package/dist/core/ports/analytics.js.map +1 -0
  35. package/dist/core/ports/auth.d.ts +49 -0
  36. package/dist/core/ports/auth.d.ts.map +1 -0
  37. package/dist/core/ports/auth.js +19 -0
  38. package/dist/core/ports/auth.js.map +1 -0
  39. package/dist/core/ports/comments.d.ts +90 -0
  40. package/dist/core/ports/comments.d.ts.map +1 -0
  41. package/dist/core/ports/comments.js +24 -0
  42. package/dist/core/ports/comments.js.map +1 -0
  43. package/dist/core/ports/database.d.ts +101 -0
  44. package/dist/core/ports/database.d.ts.map +1 -0
  45. package/dist/core/ports/database.js +2 -0
  46. package/dist/core/ports/database.js.map +1 -0
  47. package/dist/core/ports/forms.d.ts +130 -0
  48. package/dist/core/ports/forms.d.ts.map +1 -0
  49. package/dist/core/ports/forms.js +62 -0
  50. package/dist/core/ports/forms.js.map +1 -0
  51. package/dist/core/ports/identity.d.ts +114 -0
  52. package/dist/core/ports/identity.d.ts.map +1 -0
  53. package/dist/core/ports/identity.js +28 -0
  54. package/dist/core/ports/identity.js.map +1 -0
  55. package/dist/core/ports/index.d.ts +21 -0
  56. package/dist/core/ports/index.d.ts.map +1 -0
  57. package/dist/core/ports/index.js +6 -0
  58. package/dist/core/ports/index.js.map +1 -0
  59. package/dist/core/ports/integrations.d.ts +81 -0
  60. package/dist/core/ports/integrations.d.ts.map +1 -0
  61. package/dist/core/ports/integrations.js +21 -0
  62. package/dist/core/ports/integrations.js.map +1 -0
  63. package/dist/core/ports/media.d.ts +74 -0
  64. package/dist/core/ports/media.d.ts.map +1 -0
  65. package/dist/core/ports/media.js +28 -0
  66. package/dist/core/ports/media.js.map +1 -0
  67. package/dist/core/ports/members.d.ts +50 -0
  68. package/dist/core/ports/members.d.ts.map +1 -0
  69. package/dist/core/ports/members.js +17 -0
  70. package/dist/core/ports/members.js.map +1 -0
  71. package/dist/core/ports/menus.d.ts +179 -0
  72. package/dist/core/ports/menus.d.ts.map +1 -0
  73. package/dist/core/ports/menus.js +70 -0
  74. package/dist/core/ports/menus.js.map +1 -0
  75. package/dist/core/ports/plugins.d.ts +68 -0
  76. package/dist/core/ports/plugins.d.ts.map +1 -0
  77. package/dist/core/ports/plugins.js +27 -0
  78. package/dist/core/ports/plugins.js.map +1 -0
  79. package/dist/core/ports/recovery.d.ts +171 -0
  80. package/dist/core/ports/recovery.d.ts.map +1 -0
  81. package/dist/core/ports/recovery.js +2 -0
  82. package/dist/core/ports/recovery.js.map +1 -0
  83. package/dist/core/ports/redirects.d.ts +139 -0
  84. package/dist/core/ports/redirects.d.ts.map +1 -0
  85. package/dist/core/ports/redirects.js +55 -0
  86. package/dist/core/ports/redirects.js.map +1 -0
  87. package/dist/core/ports/seo.d.ts +147 -0
  88. package/dist/core/ports/seo.d.ts.map +1 -0
  89. package/dist/core/ports/seo.js +39 -0
  90. package/dist/core/ports/seo.js.map +1 -0
  91. package/dist/core/ports/settings.d.ts +87 -0
  92. package/dist/core/ports/settings.d.ts.map +1 -0
  93. package/dist/core/ports/settings.js +23 -0
  94. package/dist/core/ports/settings.js.map +1 -0
  95. package/dist/core/ports/workspace.d.ts +44 -0
  96. package/dist/core/ports/workspace.d.ts.map +1 -0
  97. package/dist/core/ports/workspace.js +29 -0
  98. package/dist/core/ports/workspace.js.map +1 -0
  99. package/dist/core/routing/rules.d.ts +54 -0
  100. package/dist/core/routing/rules.d.ts.map +1 -0
  101. package/dist/core/routing/rules.js +113 -0
  102. package/dist/core/routing/rules.js.map +1 -0
  103. package/dist/core/routing/types.d.ts +36 -0
  104. package/dist/core/routing/types.d.ts.map +1 -0
  105. package/dist/core/routing/types.js +17 -0
  106. package/dist/core/routing/types.js.map +1 -0
  107. package/dist/core/transport/errors.d.ts +53 -0
  108. package/dist/core/transport/errors.d.ts.map +1 -0
  109. package/dist/core/transport/errors.js +63 -0
  110. package/dist/core/transport/errors.js.map +1 -0
  111. package/dist/core/transport/http.d.ts +63 -0
  112. package/dist/core/transport/http.d.ts.map +1 -0
  113. package/dist/core/transport/http.js +63 -0
  114. package/dist/core/transport/http.js.map +1 -0
  115. package/dist/core/transport/index.d.ts +5 -0
  116. package/dist/core/transport/index.d.ts.map +1 -0
  117. package/dist/core/transport/index.js +3 -0
  118. package/dist/core/transport/index.js.map +1 -0
  119. package/dist/core/transport/types.d.ts +81 -0
  120. package/dist/core/transport/types.d.ts.map +1 -0
  121. package/dist/core/transport/types.js +43 -0
  122. package/dist/core/transport/types.js.map +1 -0
  123. package/dist/react/components/ConfirmButton/ConfirmButton.d.ts +40 -0
  124. package/dist/react/components/ConfirmButton/ConfirmButton.d.ts.map +1 -0
  125. package/dist/react/components/ConfirmButton/ConfirmButton.hooks.d.ts +22 -0
  126. package/dist/react/components/ConfirmButton/ConfirmButton.hooks.d.ts.map +1 -0
  127. package/dist/react/components/ConfirmButton/ConfirmButton.hooks.js +59 -0
  128. package/dist/react/components/ConfirmButton/ConfirmButton.hooks.js.map +1 -0
  129. package/dist/react/components/ConfirmButton/ConfirmButton.js +40 -0
  130. package/dist/react/components/ConfirmButton/ConfirmButton.js.map +1 -0
  131. package/dist/react/components/ConfirmButton.d.ts +39 -0
  132. package/dist/react/components/ConfirmButton.d.ts.map +1 -0
  133. package/dist/react/components/ConfirmButton.js +79 -0
  134. package/dist/react/components/ConfirmButton.js.map +1 -0
  135. package/dist/react/components/ConfirmDialog/ConfirmDialog.d.ts +90 -0
  136. package/dist/react/components/ConfirmDialog/ConfirmDialog.d.ts.map +1 -0
  137. package/dist/react/components/ConfirmDialog/ConfirmDialog.hooks.d.ts +49 -0
  138. package/dist/react/components/ConfirmDialog/ConfirmDialog.hooks.d.ts.map +1 -0
  139. package/dist/react/components/ConfirmDialog/ConfirmDialog.hooks.js +81 -0
  140. package/dist/react/components/ConfirmDialog/ConfirmDialog.hooks.js.map +1 -0
  141. package/dist/react/components/ConfirmDialog/ConfirmDialog.js +92 -0
  142. package/dist/react/components/ConfirmDialog/ConfirmDialog.js.map +1 -0
  143. package/dist/react/components/ConfirmDialog.d.ts +57 -0
  144. package/dist/react/components/ConfirmDialog.d.ts.map +1 -0
  145. package/dist/react/components/ConfirmDialog.js +85 -0
  146. package/dist/react/components/ConfirmDialog.js.map +1 -0
  147. package/dist/react/components/DataTable.d.ts +97 -0
  148. package/dist/react/components/DataTable.d.ts.map +1 -0
  149. package/dist/react/components/DataTable.js +20 -0
  150. package/dist/react/components/DataTable.js.map +1 -0
  151. package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.d.ts +25 -0
  152. package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.d.ts.map +1 -0
  153. package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.js +148 -0
  154. package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.js.map +1 -0
  155. package/dist/react/components/RowMenu/RowMenu.d.ts +77 -0
  156. package/dist/react/components/RowMenu/RowMenu.d.ts.map +1 -0
  157. package/dist/react/components/RowMenu/RowMenu.hooks.d.ts +30 -0
  158. package/dist/react/components/RowMenu/RowMenu.hooks.d.ts.map +1 -0
  159. package/dist/react/components/RowMenu/RowMenu.hooks.js +154 -0
  160. package/dist/react/components/RowMenu/RowMenu.hooks.js.map +1 -0
  161. package/dist/react/components/RowMenu/RowMenu.js +46 -0
  162. package/dist/react/components/RowMenu/RowMenu.js.map +1 -0
  163. package/dist/react/components/RowMenu.d.ts +44 -0
  164. package/dist/react/components/RowMenu.d.ts.map +1 -0
  165. package/dist/react/components/RowMenu.js +149 -0
  166. package/dist/react/components/RowMenu.js.map +1 -0
  167. package/dist/react/components/Sidebar.d.ts +128 -0
  168. package/dist/react/components/Sidebar.d.ts.map +1 -0
  169. package/dist/react/components/Sidebar.js +168 -0
  170. package/dist/react/components/Sidebar.js.map +1 -0
  171. package/dist/react/hooks/use-nav-sections.d.ts +50 -0
  172. package/dist/react/hooks/use-nav-sections.d.ts.map +1 -0
  173. package/dist/react/hooks/use-nav-sections.js +112 -0
  174. package/dist/react/hooks/use-nav-sections.js.map +1 -0
  175. package/dist/react/hooks/use-sidebar-rail.d.ts +39 -0
  176. package/dist/react/hooks/use-sidebar-rail.d.ts.map +1 -0
  177. package/dist/react/hooks/use-sidebar-rail.js +93 -0
  178. package/dist/react/hooks/use-sidebar-rail.js.map +1 -0
  179. package/dist/react/index.d.ts +53 -0
  180. package/dist/react/index.d.ts.map +1 -0
  181. package/dist/react/index.js +53 -0
  182. package/dist/react/index.js.map +1 -0
  183. package/dist/react/types.d.ts +28 -0
  184. package/dist/react/types.d.ts.map +1 -0
  185. package/dist/react/types.js +23 -0
  186. package/dist/react/types.js.map +1 -0
  187. package/dist/server/composio/bounded-data.d.ts +36 -0
  188. package/dist/server/composio/bounded-data.d.ts.map +1 -0
  189. package/dist/server/composio/bounded-data.js +181 -0
  190. package/dist/server/composio/bounded-data.js.map +1 -0
  191. package/dist/server/composio/catalog.d.ts +147 -0
  192. package/dist/server/composio/catalog.d.ts.map +1 -0
  193. package/dist/server/composio/catalog.js +126 -0
  194. package/dist/server/composio/catalog.js.map +1 -0
  195. package/dist/server/composio/composio-config.d.ts +42 -0
  196. package/dist/server/composio/composio-config.d.ts.map +1 -0
  197. package/dist/server/composio/composio-config.js +145 -0
  198. package/dist/server/composio/composio-config.js.map +1 -0
  199. package/dist/server/composio/composio-descriptions.d.ts +16 -0
  200. package/dist/server/composio/composio-descriptions.d.ts.map +1 -0
  201. package/dist/server/composio/composio-descriptions.js +766 -0
  202. package/dist/server/composio/composio-descriptions.js.map +1 -0
  203. package/dist/server/composio/composio.d.ts +364 -0
  204. package/dist/server/composio/composio.d.ts.map +1 -0
  205. package/dist/server/composio/composio.js +1902 -0
  206. package/dist/server/composio/composio.js.map +1 -0
  207. package/dist/server/composio/errors.d.ts +15 -0
  208. package/dist/server/composio/errors.d.ts.map +1 -0
  209. package/dist/server/composio/errors.js +16 -0
  210. package/dist/server/composio/errors.js.map +1 -0
  211. package/dist/server/composio/file-lock.d.ts +13 -0
  212. package/dist/server/composio/file-lock.d.ts.map +1 -0
  213. package/dist/server/composio/file-lock.js +92 -0
  214. package/dist/server/composio/file-lock.js.map +1 -0
  215. package/dist/server/composio/index.d.ts +18 -0
  216. package/dist/server/composio/index.d.ts.map +1 -0
  217. package/dist/server/composio/index.js +7 -0
  218. package/dist/server/composio/index.js.map +1 -0
  219. package/dist/server/composio/json-schema.d.ts +93 -0
  220. package/dist/server/composio/json-schema.d.ts.map +1 -0
  221. package/dist/server/composio/json-schema.js +498 -0
  222. package/dist/server/composio/json-schema.js.map +1 -0
  223. package/dist/server/composio/output-protection.d.ts +32 -0
  224. package/dist/server/composio/output-protection.d.ts.map +1 -0
  225. package/dist/server/composio/output-protection.js +105 -0
  226. package/dist/server/composio/output-protection.js.map +1 -0
  227. package/dist/server/composio/service.d.ts +181 -0
  228. package/dist/server/composio/service.d.ts.map +1 -0
  229. package/dist/server/composio/service.js +758 -0
  230. package/dist/server/composio/service.js.map +1 -0
  231. package/dist/server/index.d.ts +15 -0
  232. package/dist/server/index.d.ts.map +1 -0
  233. package/dist/server/index.js +15 -0
  234. package/dist/server/index.js.map +1 -0
  235. package/package.json +92 -0
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @file The transport seam — the single primitive every admin route group is built on.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * The reference implementation's `lib/api.ts` is 1,548 lines and 134 methods in one flat object literal,
7
+ * every one of them closing over a module-private `request()` and a module-private
8
+ * `BASE = "/api/admin/v1"`. That shape has two costs: the whole object is one unit (you cannot
9
+ * take the 67 generic methods without the 67 CMS-domain ones), and there is no way for a host to
10
+ * add a route group without editing the file.
11
+ *
12
+ * Inverting it — `request` becomes an injected `AdminTransport`, and each route group becomes a
13
+ * factory over it — makes both work:
14
+ *
15
+ * ```ts
16
+ * const transport = createHttpTransport({ baseUrl: '/api/admin/v1' });
17
+ *
18
+ * const client = createAdminClient(transport, {
19
+ * identity: createIdentityRoutes, // shipped by @jini-ai/admin
20
+ * media: createMediaRoutes, // shipped by @jini-ai/admin
21
+ * posts: createHostPostRoutes, // the host's own, same signature
22
+ * widgets: createHostWidgetRoutes, // the host's own
23
+ * });
24
+ *
25
+ * await client.identity.listUsers(); // Jini's
26
+ * await client.posts.list(); // the host's
27
+ * ```
28
+ *
29
+ * Both halves share one auth policy, one error class, one place to add retries or tracing. A host
30
+ * adding a route is a factory plus one line in the map — it never edits a Jini file, and it never
31
+ * grows a second `fetch` wrapper with its own subtly different error handling, which is exactly
32
+ * how `api.ts` reached 1,548 lines.
33
+ *
34
+ * ## Why `AdminTransport` is an interface and not just a function
35
+ *
36
+ * A bare `request<T>(path, init)` function would have been enough for HTTP. It is an interface so
37
+ * a host can supply a non-HTTP implementation without any route group knowing: a fleet orchestrator
38
+ * drives many site instances in-process and can hand over a direct-dispatch transport that never opens a
39
+ * socket, and tests can supply a fake that returns fixtures. Both are the same seam, which is the
40
+ * point.
41
+ */
42
+ export {};
43
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/transport/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG"}
@@ -0,0 +1,40 @@
1
+ import { useConfirmButton } from './ConfirmButton.hooks.js';
2
+ export interface ConfirmButtonProps {
3
+ /** Resting-state label, e.g. "Delete". */
4
+ label: string;
5
+ /** Label shown after the first (arming) click, while awaiting the confirming second click. */
6
+ confirmLabel: string;
7
+ /** Fired on the second (confirming) click. The caller owns in-flight/error state after that —
8
+ * this component only owns the armed/disarmed distinction. */
9
+ onConfirm: () => void;
10
+ /** Applies `.btn-danger`. Reserve for genuinely irreversible actions; a reversible-but-access-
11
+ * affecting action (e.g. Disable) should leave this `false` — see {@link ConfirmTone}'s
12
+ * warning-vs-danger split, which this boolean predates and cannot express. */
13
+ destructive?: boolean;
14
+ /** External in-flight flag (e.g. `rowSavingId === row.id`). Shows `pendingLabel` and disables the
15
+ * control. Not itself cancelable — a click already fired `onConfirm` by the time this is true. */
16
+ pending?: boolean;
17
+ pendingLabel?: string;
18
+ /** Disables the control outright — e.g. a built-in row the server would reject anyway. */
19
+ disabled?: boolean;
20
+ className?: string;
21
+ /** Overrides the computed accessible name. Use when several identical-looking controls share a
22
+ * page (one "Delete" per table row) and the row's own text doesn't disambiguate them for a
23
+ * screen-reader user navigating control-by-control rather than row-by-row. */
24
+ ariaLabel?: string;
25
+ /** Injectable seam for the armed/disarmed state hook. Defaults to the real
26
+ * {@link useConfirmButton}; a test can pass a fake here to exercise `ConfirmButton`'s rendering
27
+ * without driving the real Escape/outside-click/blur dismissal logic. */
28
+ useConfirmButton?: typeof useConfirmButton;
29
+ }
30
+ /**
31
+ * Two-click in-place confirm button. First click arms it: swaps the visible label to `confirmLabel`
32
+ * and announces the armed state through a `.visually-hidden` live region (a separate region rather
33
+ * than relying on the button's own label swap, since AT support for announcing a text change inside
34
+ * a live-region-less `<button>`'s accessible name is inconsistent across screen readers). Second
35
+ * click fires `onConfirm` and disarms immediately. Arm/disarm state and its Escape/outside-click/
36
+ * blur dismissal live in {@link useConfirmButton} (injectable via the `useConfirmButton` prop,
37
+ * defaulted to the real implementation, so a test can supply a fake without mocking modules).
38
+ */
39
+ export declare function ConfirmButton({ useConfirmButton: useConfirmButtonState, ...props }: ConfirmButtonProps): import("react").JSX.Element;
40
+ //# sourceMappingURL=ConfirmButton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConfirmButton.d.ts","sourceRoot":"","sources":["../../../../src/react/components/ConfirmButton/ConfirmButton.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAsB5D,MAAM,WAAW,kBAAkB;IACjC,0CAA0C;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,YAAY,EAAE,MAAM,CAAC;IACrB;mEAC+D;IAC/D,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB;;mFAE+E;IAC/E,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;uGACmG;IACnG,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;mFAE+E;IAC/E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;8EAE0E;IAC1E,gBAAgB,CAAC,EAAE,OAAO,gBAAgB,CAAC;CAC5C;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,EAAE,gBAAgB,EAAE,qBAAwC,EAAE,GAAG,KAAK,EAAE,EAAE,kBAAkB,+BA0CzH"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @file `ConfirmButton`'s armed/disarmed state, split out of the component so it can be swapped for
3
+ * a fake via the `useConfirmButton` prop on `ConfirmButtonProps` — see that prop's doc comment in
4
+ * `ConfirmButton.tsx`.
5
+ */
6
+ /**
7
+ * Owns `ConfirmButton`'s armed/disarmed state: arms on the first click, disarms on Escape, an
8
+ * outside mousedown, or a blur — all without firing `onConfirm`, so a control armed and then
9
+ * abandoned (a slow screen-reader read-through, a misclick landing elsewhere) cannot later be fired
10
+ * by an unrelated click landing on the same screen position. The second click while armed fires
11
+ * `onConfirm` and disarms immediately.
12
+ *
13
+ * @complexity O(1) per render/interaction. The two document-level listeners are attached only while
14
+ * armed, not for the component's whole mounted lifetime.
15
+ */
16
+ export declare function useConfirmButton(pending: boolean | undefined, disabled: boolean | undefined, onConfirm: () => void): {
17
+ confirming: boolean;
18
+ buttonRef: import("react").RefObject<HTMLButtonElement | null>;
19
+ handleClick: () => void;
20
+ handleBlur: () => void;
21
+ };
22
+ //# sourceMappingURL=ConfirmButton.hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConfirmButton.hooks.d.ts","sourceRoot":"","sources":["../../../../src/react/components/ConfirmButton/ConfirmButton.hooks.tsx"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,EAAE,QAAQ,EAAE,OAAO,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,IAAI;;;;;EAyClH"}
@@ -0,0 +1,59 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ /**
3
+ * @file `ConfirmButton`'s armed/disarmed state, split out of the component so it can be swapped for
4
+ * a fake via the `useConfirmButton` prop on `ConfirmButtonProps` — see that prop's doc comment in
5
+ * `ConfirmButton.tsx`.
6
+ */
7
+ /**
8
+ * Owns `ConfirmButton`'s armed/disarmed state: arms on the first click, disarms on Escape, an
9
+ * outside mousedown, or a blur — all without firing `onConfirm`, so a control armed and then
10
+ * abandoned (a slow screen-reader read-through, a misclick landing elsewhere) cannot later be fired
11
+ * by an unrelated click landing on the same screen position. The second click while armed fires
12
+ * `onConfirm` and disarms immediately.
13
+ *
14
+ * @complexity O(1) per render/interaction. The two document-level listeners are attached only while
15
+ * armed, not for the component's whole mounted lifetime.
16
+ */
17
+ export function useConfirmButton(pending, disabled, onConfirm) {
18
+ const [confirming, setConfirming] = useState(false);
19
+ const buttonRef = useRef(null);
20
+ // Listeners are scoped to the armed window only — an idle (disarmed) ConfirmButton costs nothing
21
+ // beyond the button element itself, so mounting many of them (one per table row) does not add a
22
+ // standing per-row document listener.
23
+ useEffect(() => {
24
+ if (!confirming)
25
+ return;
26
+ function disarm() {
27
+ setConfirming(false);
28
+ }
29
+ function onDocMouseDown(e) {
30
+ if (!buttonRef.current?.contains(e.target))
31
+ disarm();
32
+ }
33
+ function onKeyDown(e) {
34
+ if (e.key === 'Escape')
35
+ disarm();
36
+ }
37
+ document.addEventListener('mousedown', onDocMouseDown);
38
+ document.addEventListener('keydown', onKeyDown);
39
+ return () => {
40
+ document.removeEventListener('mousedown', onDocMouseDown);
41
+ document.removeEventListener('keydown', onKeyDown);
42
+ };
43
+ }, [confirming]);
44
+ function handleClick() {
45
+ if (pending || disabled)
46
+ return;
47
+ if (!confirming) {
48
+ setConfirming(true);
49
+ return;
50
+ }
51
+ setConfirming(false);
52
+ onConfirm();
53
+ }
54
+ function handleBlur() {
55
+ setConfirming(false);
56
+ }
57
+ return { confirming, buttonRef, handleClick, handleBlur };
58
+ }
59
+ //# sourceMappingURL=ConfirmButton.hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConfirmButton.hooks.js","sourceRoot":"","sources":["../../../../src/react/components/ConfirmButton/ConfirmButton.hooks.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEpD;;;;GAIG;AAEH;;;;;;;;;GASG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAA4B,EAAE,QAA6B,EAAE,SAAqB;IACjH,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IACpD,MAAM,SAAS,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAC;IAElD,iGAAiG;IACjG,gGAAgG;IAChG,sCAAsC;IACtC,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,UAAU;YAAE,OAAO;QACxB,SAAS,MAAM;YACb,aAAa,CAAC,KAAK,CAAC,CAAC;QACvB,CAAC;QACD,SAAS,cAAc,CAAC,CAAwB;YAC9C,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,MAAc,CAAC;gBAAE,MAAM,EAAE,CAAC;QAC/D,CAAC;QACD,SAAS,SAAS,CAAC,CAA2B;YAC5C,IAAI,CAAC,CAAC,GAAG,KAAK,QAAQ;gBAAE,MAAM,EAAE,CAAC;QACnC,CAAC;QACD,QAAQ,CAAC,gBAAgB,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;QACvD,QAAQ,CAAC,gBAAgB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAChD,OAAO,GAAG,EAAE;YACV,QAAQ,CAAC,mBAAmB,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;YAC1D,QAAQ,CAAC,mBAAmB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QACrD,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;IAEjB,SAAS,WAAW;QAClB,IAAI,OAAO,IAAI,QAAQ;YAAE,OAAO;QAChC,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,aAAa,CAAC,IAAI,CAAC,CAAC;YACpB,OAAO;QACT,CAAC;QACD,aAAa,CAAC,KAAK,CAAC,CAAC;QACrB,SAAS,EAAE,CAAC;IACd,CAAC;IAED,SAAS,UAAU;QACjB,aAAa,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;IAED,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC;AAC5D,CAAC"}
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useConfirmButton } from './ConfirmButton.hooks.js';
3
+ /**
4
+ * @file Shared two-click, in-place confirmation control for destructive or access-affecting
5
+ * actions.
6
+ *
7
+ * Exists because confirmation coverage is otherwise a coin flip: some screens guard a delete, some
8
+ * ship a bare single-click Disable, and the ones that do guard each reinvent the interaction. This
9
+ * is the one shape every screen can import instead of rebuilding.
10
+ *
11
+ * Chosen over a `window.confirm` wrapper: `window.confirm` blocks the whole tab and cannot carry
12
+ * destructive-vs-warning styling or a screen-reader-specific announcement. Chosen over a shared
13
+ * modal for the common case because the in-place shape needs no new markup at all — it renders as
14
+ * an ordinary `<button>`, optionally with `.btn-danger`.
15
+ *
16
+ * Not a replacement for {@link ConfirmDialog}: an operation with real context to disclose ("this
17
+ * restores a snapshot from 3 days ago and drops 41 entries") needs a modal that can show it. This
18
+ * is for the common case — a single row action that would otherwise be bare or a `window.confirm`.
19
+ */
20
+ const DEFAULT_PENDING_LABEL = '…';
21
+ /**
22
+ * Two-click in-place confirm button. First click arms it: swaps the visible label to `confirmLabel`
23
+ * and announces the armed state through a `.visually-hidden` live region (a separate region rather
24
+ * than relying on the button's own label swap, since AT support for announcing a text change inside
25
+ * a live-region-less `<button>`'s accessible name is inconsistent across screen readers). Second
26
+ * click fires `onConfirm` and disarms immediately. Arm/disarm state and its Escape/outside-click/
27
+ * blur dismissal live in {@link useConfirmButton} (injectable via the `useConfirmButton` prop,
28
+ * defaulted to the real implementation, so a test can supply a fake without mocking modules).
29
+ */
30
+ export function ConfirmButton({ useConfirmButton: useConfirmButtonState = useConfirmButton, ...props }) {
31
+ const { confirming, buttonRef, handleClick, handleBlur } = useConfirmButtonState(props.pending, props.disabled, props.onConfirm);
32
+ const label = props.pending
33
+ ? (props.pendingLabel ?? DEFAULT_PENDING_LABEL)
34
+ : confirming
35
+ ? props.confirmLabel
36
+ : props.label;
37
+ const className = [props.className, props.destructive ? 'btn-danger' : null].filter(Boolean).join(' ') || undefined;
38
+ return (_jsxs(_Fragment, { children: [_jsx("button", { ref: buttonRef, type: "button", className: className, "data-armed": confirming ? 'true' : undefined, disabled: props.disabled || props.pending, onClick: handleClick, onBlur: handleBlur, "aria-label": props.ariaLabel, children: label }), _jsx("span", { className: "visually-hidden", role: "status", "aria-live": "polite", children: confirming ? `Press "${props.confirmLabel}" to confirm, or press Escape to cancel.` : '' })] }));
39
+ }
40
+ //# sourceMappingURL=ConfirmButton.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConfirmButton.js","sourceRoot":"","sources":["../../../../src/react/components/ConfirmButton/ConfirmButton.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAE5D;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,qBAAqB,GAAG,GAAG,CAAC;AA+BlC;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,EAAE,gBAAgB,EAAE,qBAAqB,GAAG,gBAAgB,EAAE,GAAG,KAAK,EAAsB;IACxH,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,GAAG,qBAAqB,CAC9E,KAAK,CAAC,OAAO,EACb,KAAK,CAAC,QAAQ,EACd,KAAK,CAAC,SAAS,CAChB,CAAC;IAEF,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO;QACzB,CAAC,CAAC,CAAC,KAAK,CAAC,YAAY,IAAI,qBAAqB,CAAC;QAC/C,CAAC,CAAC,UAAU;YACV,CAAC,CAAC,KAAK,CAAC,YAAY;YACpB,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC;IAClB,MAAM,SAAS,GAAG,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC;IAEpH,OAAO,CACL,8BAOE,iBACE,GAAG,EAAE,SAAS,EACd,IAAI,EAAC,QAAQ,EACb,SAAS,EAAE,SAAS,gBACR,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EAC3C,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,OAAO,EACzC,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,UAAU,gBACN,KAAK,CAAC,SAAS,YAE1B,KAAK,GACC,EAIT,eAAM,SAAS,EAAC,iBAAiB,EAAC,IAAI,EAAC,QAAQ,eAAW,QAAQ,YAC/D,UAAU,CAAC,CAAC,CAAC,UAAU,KAAK,CAAC,YAAY,0CAA0C,CAAC,CAAC,CAAC,EAAE,GACpF,IACN,CACJ,CAAC;AACJ,CAAC"}
@@ -0,0 +1,39 @@
1
+ export interface ConfirmButtonProps {
2
+ /** Resting-state label, e.g. "Delete". */
3
+ label: string;
4
+ /** Label shown after the first (arming) click, while awaiting the confirming second click. */
5
+ confirmLabel: string;
6
+ /** Fired on the second (confirming) click. The caller owns in-flight/error state after that —
7
+ * this component only owns the armed/disarmed distinction. */
8
+ onConfirm: () => void;
9
+ /** Applies `.btn-danger`. Reserve for genuinely irreversible actions; a reversible-but-access-
10
+ * affecting action (e.g. Disable) should leave this `false` — see {@link ConfirmTone}'s
11
+ * warning-vs-danger split, which this boolean predates and cannot express. */
12
+ destructive?: boolean;
13
+ /** External in-flight flag (e.g. `rowSavingId === row.id`). Shows `pendingLabel` and disables the
14
+ * control. Not itself cancelable — a click already fired `onConfirm` by the time this is true. */
15
+ pending?: boolean;
16
+ pendingLabel?: string;
17
+ /** Disables the control outright — e.g. a built-in row the server would reject anyway. */
18
+ disabled?: boolean;
19
+ className?: string;
20
+ /** Overrides the computed accessible name. Use when several identical-looking controls share a
21
+ * page (one "Delete" per table row) and the row's own text doesn't disambiguate them for a
22
+ * screen-reader user navigating control-by-control rather than row-by-row. */
23
+ ariaLabel?: string;
24
+ }
25
+ /**
26
+ * Two-click in-place confirm button. First click arms it: swaps the visible label to `confirmLabel`
27
+ * and announces the armed state through a `.visually-hidden` live region (a separate region rather
28
+ * than relying on the button's own label swap, since AT support for announcing a text change inside
29
+ * a live-region-less `<button>`'s accessible name is inconsistent across screen readers). Second
30
+ * click fires `onConfirm` and disarms immediately. Losing focus, an outside click, or Escape
31
+ * disarms without firing, so a control armed and then abandoned (a slow screen-reader read-through,
32
+ * a misclick landing elsewhere) cannot later be fired by an unrelated click landing on the same
33
+ * screen position.
34
+ *
35
+ * @complexity O(1) per render/interaction. The two document-level listeners are attached only while
36
+ * armed, not for the component's whole mounted lifetime.
37
+ */
38
+ export declare function ConfirmButton(props: ConfirmButtonProps): import("react").JSX.Element;
39
+ //# sourceMappingURL=ConfirmButton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConfirmButton.d.ts","sourceRoot":"","sources":["../../../src/react/components/ConfirmButton.tsx"],"names":[],"mappings":"AAsBA,MAAM,WAAW,kBAAkB;IACjC,0CAA0C;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,YAAY,EAAE,MAAM,CAAC;IACrB;mEAC+D;IAC/D,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB;;mFAE+E;IAC/E,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;uGACmG;IACnG,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;mFAE+E;IAC/E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,+BAuEtD"}
@@ -0,0 +1,79 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ /**
4
+ * @file Shared two-click, in-place confirmation control for destructive or access-affecting
5
+ * actions.
6
+ *
7
+ * Exists because confirmation coverage is otherwise a coin flip: some screens guard a delete, some
8
+ * ship a bare single-click Disable, and the ones that do guard each reinvent the interaction. This
9
+ * is the one shape every screen can import instead of rebuilding.
10
+ *
11
+ * Chosen over a `window.confirm` wrapper: `window.confirm` blocks the whole tab and cannot carry
12
+ * destructive-vs-warning styling or a screen-reader-specific announcement. Chosen over a shared
13
+ * modal for the common case because the in-place shape needs no new markup at all — it renders as
14
+ * an ordinary `<button>`, optionally with `.btn-danger`.
15
+ *
16
+ * Not a replacement for {@link ConfirmDialog}: an operation with real context to disclose ("this
17
+ * restores a snapshot from 3 days ago and drops 41 entries") needs a modal that can show it. This
18
+ * is for the common case — a single row action that would otherwise be bare or a `window.confirm`.
19
+ */
20
+ const DEFAULT_PENDING_LABEL = '…';
21
+ /**
22
+ * Two-click in-place confirm button. First click arms it: swaps the visible label to `confirmLabel`
23
+ * and announces the armed state through a `.visually-hidden` live region (a separate region rather
24
+ * than relying on the button's own label swap, since AT support for announcing a text change inside
25
+ * a live-region-less `<button>`'s accessible name is inconsistent across screen readers). Second
26
+ * click fires `onConfirm` and disarms immediately. Losing focus, an outside click, or Escape
27
+ * disarms without firing, so a control armed and then abandoned (a slow screen-reader read-through,
28
+ * a misclick landing elsewhere) cannot later be fired by an unrelated click landing on the same
29
+ * screen position.
30
+ *
31
+ * @complexity O(1) per render/interaction. The two document-level listeners are attached only while
32
+ * armed, not for the component's whole mounted lifetime.
33
+ */
34
+ export function ConfirmButton(props) {
35
+ const [confirming, setConfirming] = useState(false);
36
+ const buttonRef = useRef(null);
37
+ // Listeners are scoped to the armed window only — an idle (disarmed) ConfirmButton costs nothing
38
+ // beyond the button element itself, so mounting many of them (one per table row) does not add a
39
+ // standing per-row document listener.
40
+ useEffect(() => {
41
+ if (!confirming)
42
+ return;
43
+ function disarm() {
44
+ setConfirming(false);
45
+ }
46
+ function onDocMouseDown(e) {
47
+ if (!buttonRef.current?.contains(e.target))
48
+ disarm();
49
+ }
50
+ function onKeyDown(e) {
51
+ if (e.key === 'Escape')
52
+ disarm();
53
+ }
54
+ document.addEventListener('mousedown', onDocMouseDown);
55
+ document.addEventListener('keydown', onKeyDown);
56
+ return () => {
57
+ document.removeEventListener('mousedown', onDocMouseDown);
58
+ document.removeEventListener('keydown', onKeyDown);
59
+ };
60
+ }, [confirming]);
61
+ function handleClick() {
62
+ if (props.pending || props.disabled)
63
+ return;
64
+ if (!confirming) {
65
+ setConfirming(true);
66
+ return;
67
+ }
68
+ setConfirming(false);
69
+ props.onConfirm();
70
+ }
71
+ const label = props.pending
72
+ ? (props.pendingLabel ?? DEFAULT_PENDING_LABEL)
73
+ : confirming
74
+ ? props.confirmLabel
75
+ : props.label;
76
+ const className = [props.className, props.destructive ? 'btn-danger' : null].filter(Boolean).join(' ') || undefined;
77
+ return (_jsxs(_Fragment, { children: [_jsx("button", { ref: buttonRef, type: "button", className: className, "data-armed": confirming ? 'true' : undefined, disabled: props.disabled || props.pending, onClick: handleClick, onBlur: () => setConfirming(false), "aria-label": props.ariaLabel, children: label }), _jsx("span", { className: "visually-hidden", role: "status", "aria-live": "polite", children: confirming ? `Press "${props.confirmLabel}" to confirm, or press Escape to cancel.` : '' })] }));
78
+ }
79
+ //# sourceMappingURL=ConfirmButton.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConfirmButton.js","sourceRoot":"","sources":["../../../src/react/components/ConfirmButton.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEpD;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,qBAAqB,GAAG,GAAG,CAAC;AA2BlC;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,aAAa,CAAC,KAAyB;IACrD,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IACpD,MAAM,SAAS,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAC;IAElD,iGAAiG;IACjG,gGAAgG;IAChG,sCAAsC;IACtC,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,UAAU;YAAE,OAAO;QACxB,SAAS,MAAM;YACb,aAAa,CAAC,KAAK,CAAC,CAAC;QACvB,CAAC;QACD,SAAS,cAAc,CAAC,CAAwB;YAC9C,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,MAAc,CAAC;gBAAE,MAAM,EAAE,CAAC;QAC/D,CAAC;QACD,SAAS,SAAS,CAAC,CAA2B;YAC5C,IAAI,CAAC,CAAC,GAAG,KAAK,QAAQ;gBAAE,MAAM,EAAE,CAAC;QACnC,CAAC;QACD,QAAQ,CAAC,gBAAgB,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;QACvD,QAAQ,CAAC,gBAAgB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAChD,OAAO,GAAG,EAAE;YACV,QAAQ,CAAC,mBAAmB,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;YAC1D,QAAQ,CAAC,mBAAmB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QACrD,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;IAEjB,SAAS,WAAW;QAClB,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,QAAQ;YAAE,OAAO;QAC5C,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,aAAa,CAAC,IAAI,CAAC,CAAC;YACpB,OAAO;QACT,CAAC;QACD,aAAa,CAAC,KAAK,CAAC,CAAC;QACrB,KAAK,CAAC,SAAS,EAAE,CAAC;IACpB,CAAC;IAED,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO;QACzB,CAAC,CAAC,CAAC,KAAK,CAAC,YAAY,IAAI,qBAAqB,CAAC;QAC/C,CAAC,CAAC,UAAU;YACV,CAAC,CAAC,KAAK,CAAC,YAAY;YACpB,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC;IAClB,MAAM,SAAS,GAAG,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC;IAEpH,OAAO,CACL,8BAOE,iBACE,GAAG,EAAE,SAAS,EACd,IAAI,EAAC,QAAQ,EACb,SAAS,EAAE,SAAS,gBACR,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EAC3C,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,OAAO,EACzC,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,gBACtB,KAAK,CAAC,SAAS,YAE1B,KAAK,GACC,EAIT,eAAM,SAAS,EAAC,iBAAiB,EAAC,IAAI,EAAC,QAAQ,eAAW,QAAQ,YAC/D,UAAU,CAAC,CAAC,CAAC,UAAU,KAAK,CAAC,YAAY,0CAA0C,CAAC,CAAC,CAAC,EAAE,GACpF,IACN,CACJ,CAAC;AACJ,CAAC"}
@@ -0,0 +1,90 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type ConfirmTone } from '../../types.js';
3
+ import { type UseConfirmDialog } from './ConfirmDialog.hooks.js';
4
+ /**
5
+ * @file Shared modal confirmation primitive — the replacement for `window.confirm`, which blocks
6
+ * the whole tab, cannot carry destructive-vs-neutral styling, and reads as a browser artifact
7
+ * rather than part of the product.
8
+ *
9
+ * Built on the native `<dialog>` element (`showModal()`/`close()`) rather than a plain-`<div>`
10
+ * overlay + backdrop idiom: that shape hand-rolls focus trapping, Escape handling, and a backdrop
11
+ * element, all of which the browser's own top layer gives a real `<dialog>` for free, including
12
+ * correct stacking above everything else on the page without a chosen `z-index`. The `showModal`/
13
+ * `close` calls themselves, and the jsdom fallback they need, live in `ConfirmDialog.hooks.tsx` —
14
+ * see that file's doc comment.
15
+ *
16
+ * Unstyled, like everything in this layer: the `.confirm-dialog` / `.btn-secondary` /
17
+ * `.btn-danger` / `.btn-warning` class names are emitted for the host stylesheet to define.
18
+ *
19
+ * ## Agent handles
20
+ *
21
+ * Given `agentHandle="delete-role"` this publishes:
22
+ *
23
+ * | element | handle | role |
24
+ * |---|---|---|
25
+ * | the confirm action | `delete-role-confirm` | `button` |
26
+ * | the cancel/dismiss action | `delete-role-cancel` | `button` |
27
+ *
28
+ * Both segments are literals this component chooses itself, never host data — unlike `RowMenu`'s
29
+ * per-item keys (arbitrary host strings that can collide after slugifying), `confirm`/`cancel`
30
+ * need no sanitizing. `agentHandle` itself is NOT sanitized either: it is the caller's own explicit
31
+ * choice of name, and a bad one should fail loudly at first render rather than silently answer to a
32
+ * handle the caller never wrote. Omit `agentHandle` and no `data-agent-*` markup is emitted at all.
33
+ *
34
+ * Both handles are present in the DOM as soon as `agentHandle` is supplied, regardless of `open` —
35
+ * this component stays mounted and toggles the native `<dialog>`'s open state rather than being
36
+ * conditionally rendered by its caller (see `useConfirmDialog`'s doc comment), so there is no
37
+ * "closed" render that omits them, the same reasoning `RowMenu`'s always-visible trigger handle
38
+ * documents for itself.
39
+ *
40
+ * This component renders no typed-confirmation input today (no "type DELETE to confirm" field) —
41
+ * if one is added later it needs its own handle under this same scheme (e.g. `<base>-confirm-input`,
42
+ * role `field`).
43
+ *
44
+ * Labels state both the action and its consequence tier, derived from this component's own
45
+ * three-tier `ConfirmTone` vocabulary rather than reparsed from `title`/`body`: `body` is arbitrary
46
+ * `ReactNode` (a caller can pass JSX), not guaranteed to be a string at all, so it cannot be safely
47
+ * folded into agent-facing text — see {@link confirmDialogConsequencePhrase}.
48
+ */
49
+ export interface ConfirmDialogProps {
50
+ open: boolean;
51
+ title: string;
52
+ body: ReactNode;
53
+ confirmLabel: string;
54
+ /** @default "Cancel" */
55
+ cancelLabel?: string;
56
+ /** Applies `.btn-warning`/`.btn-danger` to the confirm action. Defaults to `"default"` (no class,
57
+ * the plain primary button). Wins over `destructive` below when both are passed. */
58
+ tone?: ConfirmTone;
59
+ /** @deprecated Use `tone: "danger"` instead — this only ever expressed the danger tier, and the
60
+ * vocabulary has a second one (`"warning"`) this boolean cannot reach. Kept working (mapped to
61
+ * `tone: "danger"` when `tone` is not set) for existing callers rather than a breaking rename. */
62
+ destructive?: boolean;
63
+ /** External in-flight flag. Disables both actions and blocks Escape/backdrop dismissal so a
64
+ * request already underway cannot be raced by a second dismiss. */
65
+ pending?: boolean;
66
+ onConfirm: () => void;
67
+ onCancel: () => void;
68
+ /** Injectable seam for the dialog's open/close and focus-management hook. Defaults to the real
69
+ * {@link useConfirmDialog}; a test can pass a fake here to exercise `ConfirmDialog`'s rendering
70
+ * without invoking the native `<dialog>` methods or DOM focus calls at all.
71
+ *
72
+ * Typed against the explicit `UseConfirmDialog` contract rather than `typeof useConfirmDialog`:
73
+ * see `ConfirmDialogController`'s doc comment for why binding a public prop to a concrete
74
+ * implementation's inferred return object is a coupling rather than a convenience. */
75
+ useDialog?: UseConfirmDialog;
76
+ /** This dialog's own agent handle — see this file's "Agent handles" doc comment for the full
77
+ * scheme. Omit and no `data-agent-*` markup is emitted at all. */
78
+ agentHandle?: string;
79
+ }
80
+ /**
81
+ * Controlled modal confirm — renders the `<dialog>` markup and delegates its open/close and focus
82
+ * lifecycle to {@link useConfirmDialog} (injectable via the `useDialog` prop, defaulted to the real
83
+ * implementation, so a test can supply a fake without mocking modules).
84
+ *
85
+ * Both actions are plain `type="button"` (no `<form method="dialog">`, no `type="submit"`), so there
86
+ * is no browser-assigned "default button" for Enter to reach for at all; confirm can only ever fire
87
+ * from an explicit click or explicit Tab-then-Enter onto it.
88
+ */
89
+ export declare function ConfirmDialog({ useDialog, agentHandle: baseHandle, ...props }: ConfirmDialogProps): import("react").JSX.Element;
90
+ //# sourceMappingURL=ConfirmDialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConfirmDialog.d.ts","sourceRoot":"","sources":["../../../../src/react/components/ConfirmDialog/ConfirmDialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAA8B,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC9E,OAAO,EAAoB,KAAK,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,SAAS,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,wBAAwB;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;yFACqF;IACrF,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB;;uGAEmG;IACnG,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;wEACoE;IACpE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB;;;;;;2FAMuF;IACvF,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B;uEACmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AA2ED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,EAAE,SAA4B,EAAE,WAAW,EAAE,UAAU,EAAE,GAAG,KAAK,EAAE,EAAE,kBAAkB,+BAiDpH"}
@@ -0,0 +1,49 @@
1
+ import { type MouseEvent, type RefObject, type SyntheticEvent } from 'react';
2
+ /**
3
+ * @file `ConfirmDialog`'s open/close and focus-management state, split out of the component so it
4
+ * can be swapped for a fake via the `useDialog` prop on `ConfirmDialogProps` — see that prop's doc
5
+ * comment in `ConfirmDialog.tsx`.
6
+ *
7
+ * `dialog.showModal()`/`dialog.close()` are guarded by a `typeof` check rather than called
8
+ * unconditionally: jsdom (this package's React test environment, verified directly rather than
9
+ * assumed) does not implement either method at all — calling them throws `TypeError`. The fallback
10
+ * branch below toggles the plain `open` attribute instead, which no browser shipping a real
11
+ * `<dialog>` ever takes (the `showModal`/`close` branch always wins there), so this only changes
12
+ * behavior under jsdom, where it keeps the dialog mountable and its content queryable.
13
+ */
14
+ /**
15
+ * What `ConfirmDialog` needs back from whatever drives its dialog lifecycle: the two refs it
16
+ * attaches and the two handlers it wires, plus the id its `aria-labelledby` points at.
17
+ *
18
+ * Declared as its own interface rather than left to be inferred from {@link useConfirmDialog}'s
19
+ * return object. That inference is what made the injectable `useDialog` prop couple to the concrete
20
+ * implementation: `typeof useConfirmDialog` meant every internal detail of the default hook's shape
21
+ * became part of the component's PUBLIC contract, so adding a field for the default's own
22
+ * convenience would silently break every substitute a consumer had already written. Naming the
23
+ * contract inverts that — the hook now has to satisfy the component, not the other way round.
24
+ */
25
+ export interface ConfirmDialogController {
26
+ /** Id for the dialog's `<h2>`, unique per mounted instance — see {@link useConfirmDialog}. */
27
+ titleId: string;
28
+ dialogRef: RefObject<HTMLDialogElement | null>;
29
+ cancelRef: RefObject<HTMLButtonElement | null>;
30
+ handleNativeCancel: (event: SyntheticEvent<HTMLDialogElement>) => void;
31
+ handleBackdropClick: (event: MouseEvent<HTMLDialogElement>) => void;
32
+ }
33
+ /** The signature `ConfirmDialogProps.useDialog` accepts. Any hook matching this can drive the
34
+ * component; it does not have to be {@link useConfirmDialog}, or even use React state at all. */
35
+ export type UseConfirmDialog = (open: boolean, pending: boolean | undefined, onCancel: () => void) => ConfirmDialogController;
36
+ /**
37
+ * Owns the `<dialog>` element's open/close lifecycle and focus management for `ConfirmDialog`.
38
+ *
39
+ * The caller keeps `ConfirmDialog` mounted and toggles `open` — it is never conditionally rendered
40
+ * by its parent — so the effect below has a stable `<dialog>` element to call `showModal()`/
41
+ * `close()` on and to restore focus through when it closes.
42
+ *
43
+ * Focus moves to the *cancel* action on open, not confirm — an operator whose first keystroke after
44
+ * a slow read-through is Enter should land on the safe action, not the destructive one.
45
+ *
46
+ * @complexity O(1) per open/close transition — one `showModal`/`close` call and one focus move.
47
+ */
48
+ export declare function useConfirmDialog(open: boolean, pending: boolean | undefined, onCancel: () => void): ConfirmDialogController;
49
+ //# sourceMappingURL=ConfirmDialog.hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConfirmDialog.hooks.d.ts","sourceRoot":"","sources":["../../../../src/react/components/ConfirmDialog/ConfirmDialog.hooks.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA4B,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,OAAO,CAAC;AAEvG;;;;;;;;;;;GAWG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,WAAW,uBAAuB;IACtC,8FAA8F;IAC9F,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,SAAS,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IAC/C,SAAS,EAAE,SAAS,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IAC/C,kBAAkB,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAC;IACvE,mBAAmB,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAC;CACrE;AAED;kGACkG;AAClG,MAAM,MAAM,gBAAgB,GAAG,CAC7B,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,OAAO,GAAG,SAAS,EAC5B,QAAQ,EAAE,MAAM,IAAI,KACjB,uBAAuB,CAAC;AAE7B;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,OAAO,GAAG,SAAS,EAC5B,QAAQ,EAAE,MAAM,IAAI,GACnB,uBAAuB,CA4DzB"}
@@ -0,0 +1,81 @@
1
+ import { useEffect, useId, useRef } from 'react';
2
+ /**
3
+ * Owns the `<dialog>` element's open/close lifecycle and focus management for `ConfirmDialog`.
4
+ *
5
+ * The caller keeps `ConfirmDialog` mounted and toggles `open` — it is never conditionally rendered
6
+ * by its parent — so the effect below has a stable `<dialog>` element to call `showModal()`/
7
+ * `close()` on and to restore focus through when it closes.
8
+ *
9
+ * Focus moves to the *cancel* action on open, not confirm — an operator whose first keystroke after
10
+ * a slow read-through is Enter should land on the safe action, not the destructive one.
11
+ *
12
+ * @complexity O(1) per open/close transition — one `showModal`/`close` call and one focus move.
13
+ */
14
+ export function useConfirmDialog(open, pending, onCancel) {
15
+ // `useId()`, not a string literal — a hardcoded id breaks the moment a screen mounts two
16
+ // `ConfirmDialog`s at once (a delete-role and a delete-policy confirm on one page, both
17
+ // stay-mounted/toggle-`open` per this hook's own doc comment above): two `<h2>`s share one
18
+ // id, and `aria-labelledby` resolves via `getElementById`, which always returns the FIRST match
19
+ // in document order regardless of which dialog is actually open — a screen reader announcing
20
+ // "Delete role?" while the operator is about to confirm "Delete policy?", on a destructive
21
+ // action. `useId()` gives every mounted instance its own id for free.
22
+ const titleId = useId();
23
+ const dialogRef = useRef(null);
24
+ const cancelRef = useRef(null);
25
+ // Captured at the moment `open` flips true, before focus moves into the dialog — the element that
26
+ // had focus then is, by construction, whatever triggered this dialog (a `RowMenu` item's
27
+ // "Delete", a plain trigger button, …). Restored on close rather than left wherever the browser's
28
+ // own modal-focus algorithm happened to land (its default without this is `<body>`, which drops a
29
+ // keyboard user back to the top of the page).
30
+ const triggerRef = useRef(null);
31
+ useEffect(() => {
32
+ const dialog = dialogRef.current;
33
+ if (!dialog)
34
+ return;
35
+ if (open) {
36
+ triggerRef.current = document.activeElement;
37
+ if (typeof dialog.showModal === 'function') {
38
+ if (!dialog.open)
39
+ dialog.showModal();
40
+ }
41
+ else {
42
+ dialog.setAttribute('open', '');
43
+ }
44
+ cancelRef.current?.focus();
45
+ }
46
+ else {
47
+ if (typeof dialog.close === 'function') {
48
+ if (dialog.open)
49
+ dialog.close();
50
+ }
51
+ else {
52
+ dialog.removeAttribute('open');
53
+ }
54
+ if (triggerRef.current instanceof HTMLElement)
55
+ triggerRef.current.focus();
56
+ }
57
+ }, [open]);
58
+ function handleNativeCancel(e) {
59
+ // Fires on Escape while a real `showModal()`-opened dialog has focus. Always prevented: the
60
+ // effect above is the single source of truth for open/closed (driven by `open`), so
61
+ // letting the browser close the element on its own would desync DOM state from React state.
62
+ // Escape still closes the dialog — it just does so by routing through `onCancel`, same as a
63
+ // Cancel-button click, so the caller's `open` state (and therefore this same effect) is what
64
+ // actually calls `dialog.close()`.
65
+ e.preventDefault();
66
+ if (pending)
67
+ return;
68
+ onCancel();
69
+ }
70
+ function handleBackdropClick(e) {
71
+ // A `<dialog>` element's own box is sized to its content, not the viewport — a click that lands
72
+ // on the `<dialog>` element itself (as opposed to one of its children) is therefore a click on
73
+ // the backdrop area outside that content box.
74
+ if (pending)
75
+ return;
76
+ if (e.target === dialogRef.current)
77
+ onCancel();
78
+ }
79
+ return { titleId, dialogRef, cancelRef, handleNativeCancel, handleBackdropClick };
80
+ }
81
+ //# sourceMappingURL=ConfirmDialog.hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConfirmDialog.hooks.js","sourceRoot":"","sources":["../../../../src/react/components/ConfirmDialog/ConfirmDialog.hooks.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAwD,MAAM,OAAO,CAAC;AA2CvG;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAAa,EACb,OAA4B,EAC5B,QAAoB;IAEpB,yFAAyF;IACzF,wFAAwF;IACxF,2FAA2F;IAC3F,gGAAgG;IAChG,6FAA6F;IAC7F,2FAA2F;IAC3F,sEAAsE;IACtE,MAAM,OAAO,GAAG,KAAK,EAAE,CAAC;IACxB,MAAM,SAAS,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAC;IAClD,MAAM,SAAS,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAC;IAClD,kGAAkG;IAClG,yFAAyF;IACzF,kGAAkG;IAClG,kGAAkG;IAClG,8CAA8C;IAC9C,MAAM,UAAU,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAC;IAEhD,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC;QACjC,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,IAAI,IAAI,EAAE,CAAC;YACT,UAAU,CAAC,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC;YAC5C,IAAI,OAAO,MAAM,CAAC,SAAS,KAAK,UAAU,EAAE,CAAC;gBAC3C,IAAI,CAAC,MAAM,CAAC,IAAI;oBAAE,MAAM,CAAC,SAAS,EAAE,CAAC;YACvC,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;YAClC,CAAC;YACD,SAAS,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC;QAC7B,CAAC;aAAM,CAAC;YACN,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;gBACvC,IAAI,MAAM,CAAC,IAAI;oBAAE,MAAM,CAAC,KAAK,EAAE,CAAC;YAClC,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;YACjC,CAAC;YACD,IAAI,UAAU,CAAC,OAAO,YAAY,WAAW;gBAAE,UAAU,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QAC5E,CAAC;IACH,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAEX,SAAS,kBAAkB,CAAC,CAAoC;QAC9D,4FAA4F;QAC5F,oFAAoF;QACpF,4FAA4F;QAC5F,4FAA4F;QAC5F,6FAA6F;QAC7F,mCAAmC;QACnC,CAAC,CAAC,cAAc,EAAE,CAAC;QACnB,IAAI,OAAO;YAAE,OAAO;QACpB,QAAQ,EAAE,CAAC;IACb,CAAC;IAED,SAAS,mBAAmB,CAAC,CAAgC;QAC3D,gGAAgG;QAChG,+FAA+F;QAC/F,8CAA8C;QAC9C,IAAI,OAAO;YAAE,OAAO;QACpB,IAAI,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC,OAAO;YAAE,QAAQ,EAAE,CAAC;IACjD,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,CAAC;AACpF,CAAC"}