@cosmicdrift/kumiko-renderer 0.327.0 → 0.328.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 (257) hide show
  1. package/dist/app/action-form-shim.d.ts +14 -0
  2. package/{src/app/action-form-shim.ts → dist/app/action-form-shim.js} +26 -44
  3. package/dist/app/app-features-context.d.ts +8 -0
  4. package/dist/app/app-features-context.js +16 -0
  5. package/dist/app/column-renderers.d.ts +30 -0
  6. package/dist/app/column-renderers.js +34 -0
  7. package/dist/app/config-edit-shim.d.ts +11 -0
  8. package/{src/app/config-edit-shim.ts → dist/app/config-edit-shim.js} +12 -25
  9. package/dist/app/content-editors.d.ts +38 -0
  10. package/dist/app/content-editors.js +26 -0
  11. package/dist/app/content-preview.d.ts +11 -0
  12. package/dist/app/content-preview.js +30 -0
  13. package/dist/app/custom-screens.d.ts +11 -0
  14. package/dist/app/custom-screens.js +20 -0
  15. package/dist/app/dashboard-body.d.ts +16 -0
  16. package/dist/app/dashboard-body.js +11 -0
  17. package/dist/app/extension-form-submit.d.ts +32 -0
  18. package/dist/app/extension-form-submit.js +73 -0
  19. package/dist/app/extension-sections.d.ts +72 -0
  20. package/dist/app/extension-sections.js +34 -0
  21. package/dist/app/feature-schema.d.ts +4 -0
  22. package/{src/app/feature-schema.ts → dist/app/feature-schema.js} +12 -22
  23. package/dist/app/form-schema.d.ts +4 -0
  24. package/dist/app/form-schema.js +85 -0
  25. package/dist/app/kumiko-screen.d.ts +43 -0
  26. package/dist/app/kumiko-screen.js +2206 -0
  27. package/dist/app/layout-fields.d.ts +3 -0
  28. package/dist/app/layout-fields.js +20 -0
  29. package/dist/app/list-facets.d.ts +48 -0
  30. package/dist/app/list-facets.js +185 -0
  31. package/dist/app/nav.d.ts +61 -0
  32. package/dist/app/nav.js +139 -0
  33. package/dist/app/projection-detail-shim.d.ts +8 -0
  34. package/dist/app/projection-detail-shim.js +71 -0
  35. package/dist/app/projection-list-shim.d.ts +13 -0
  36. package/{src/app/projection-list-shim.ts → dist/app/projection-list-shim.js} +26 -38
  37. package/dist/app/qn.d.ts +7 -0
  38. package/{src/app/qn.ts → dist/app/qn.js} +9 -11
  39. package/dist/app/qualify-screen-id.d.ts +2 -0
  40. package/{src/app/qualify-screen-id.ts → dist/app/qualify-screen-id.js} +5 -6
  41. package/dist/app/reference-facet-bridge.d.ts +10 -0
  42. package/dist/app/reference-facet-bridge.js +22 -0
  43. package/dist/app/return-to.d.ts +31 -0
  44. package/dist/app/return-to.js +188 -0
  45. package/dist/app/row-actions.d.ts +77 -0
  46. package/dist/app/row-actions.js +416 -0
  47. package/dist/app/screen-access.d.ts +8 -0
  48. package/dist/app/screen-access.js +48 -0
  49. package/dist/app/secret-mint-body.d.ts +10 -0
  50. package/dist/app/secret-mint-body.js +140 -0
  51. package/dist/app/secrets-edit-body.d.ts +8 -0
  52. package/dist/app/secrets-edit-body.js +76 -0
  53. package/dist/app/use-embedded-screen.d.ts +6 -0
  54. package/dist/app/use-embedded-screen.js +19 -0
  55. package/dist/app/variable-chips.d.ts +7 -0
  56. package/dist/app/variable-chips.js +16 -0
  57. package/dist/app/write-failed-error.d.ts +6 -0
  58. package/dist/app/write-failed-error.js +21 -0
  59. package/dist/components/embedded-list-field.d.ts +10 -0
  60. package/dist/components/embedded-list-field.js +259 -0
  61. package/dist/components/grid-cell-for-field.d.ts +25 -0
  62. package/dist/components/grid-cell-for-field.js +31 -0
  63. package/dist/components/multi-select-checkboxes.d.ts +14 -0
  64. package/dist/components/multi-select-checkboxes.js +36 -0
  65. package/dist/components/reference-create-dialog.d.ts +13 -0
  66. package/dist/components/reference-create-dialog.js +46 -0
  67. package/dist/components/related-list-section.d.ts +25 -0
  68. package/dist/components/related-list-section.js +273 -0
  69. package/dist/components/render-edit-action-button.d.ts +22 -0
  70. package/dist/components/render-edit-action-button.js +43 -0
  71. package/dist/components/render-edit-logic.d.ts +6 -0
  72. package/dist/components/render-edit-logic.js +63 -0
  73. package/dist/components/render-edit-types.d.ts +209 -0
  74. package/dist/components/render-edit-types.js +1 -0
  75. package/dist/components/render-edit.d.ts +6 -0
  76. package/dist/components/render-edit.js +1169 -0
  77. package/dist/components/render-field.d.ts +38 -0
  78. package/dist/components/render-field.js +544 -0
  79. package/dist/components/render-list.d.ts +117 -0
  80. package/dist/components/render-list.js +232 -0
  81. package/dist/components/write-form-section.d.ts +17 -0
  82. package/dist/components/write-form-section.js +104 -0
  83. package/dist/context/dispatcher-context.d.ts +10 -0
  84. package/{src/context/dispatcher-context.tsx → dist/context/dispatcher-context.js} +16 -29
  85. package/dist/context/draft-storage-context.d.ts +12 -0
  86. package/dist/context/draft-storage-context.js +18 -0
  87. package/dist/context/user-roles-context.d.ts +7 -0
  88. package/{src/context/user-roles-context.tsx → dist/context/user-roles-context.js} +7 -15
  89. package/dist/format-when.d.ts +1 -0
  90. package/{src/format-when.ts → dist/format-when.js} +7 -7
  91. package/dist/hooks/reference-limits.d.ts +3 -0
  92. package/{src/hooks/reference-limits.ts → dist/hooks/reference-limits.js} +0 -1
  93. package/dist/hooks/use-ai-text.d.ts +52 -0
  94. package/dist/hooks/use-ai-text.js +90 -0
  95. package/dist/hooks/use-disclosure.d.ts +7 -0
  96. package/dist/hooks/use-disclosure.js +8 -0
  97. package/dist/hooks/use-form.d.ts +7 -0
  98. package/dist/hooks/use-form.js +36 -0
  99. package/dist/hooks/use-list-url-state.d.ts +43 -0
  100. package/dist/hooks/use-list-url-state.js +131 -0
  101. package/dist/hooks/use-mutation.d.ts +9 -0
  102. package/dist/hooks/use-mutation.js +37 -0
  103. package/dist/hooks/use-query.d.ts +14 -0
  104. package/dist/hooks/use-query.js +118 -0
  105. package/dist/hooks/use-reference-lookup.d.ts +14 -0
  106. package/dist/hooks/use-reference-lookup.js +63 -0
  107. package/dist/hooks/use-store.d.ts +3 -0
  108. package/dist/hooks/use-store.js +38 -0
  109. package/dist/hooks/use-stream-handler.d.ts +14 -0
  110. package/dist/hooks/use-stream-handler.js +93 -0
  111. package/dist/i18n-defaults.d.ts +2 -0
  112. package/dist/i18n-defaults.js +201 -0
  113. package/dist/i18n.d.ts +64 -0
  114. package/dist/i18n.js +166 -0
  115. package/dist/index.d.ts +77 -0
  116. package/dist/index.js +55 -0
  117. package/dist/inside-drawer.d.ts +2 -0
  118. package/dist/inside-drawer.js +9 -0
  119. package/dist/page-header-slot.d.ts +2 -0
  120. package/dist/page-header-slot.js +9 -0
  121. package/dist/primitives.d.ts +1351 -0
  122. package/dist/primitives.js +73 -0
  123. package/dist/screen-fills-height.d.ts +3 -0
  124. package/dist/screen-fills-height.js +3 -0
  125. package/dist/share-links.d.ts +4 -0
  126. package/dist/share-links.js +11 -0
  127. package/dist/sort-by-accessor.d.ts +5 -0
  128. package/dist/sort-by-accessor.js +16 -0
  129. package/dist/sse/live-events.d.ts +23 -0
  130. package/dist/sse/live-events.js +28 -0
  131. package/dist/tokens.d.ts +83 -0
  132. package/dist/tokens.js +80 -0
  133. package/package.json +16 -9
  134. package/src/changes.json +52 -0
  135. package/src/__tests__/error-i18n-defaults.test.ts +0 -82
  136. package/src/__tests__/format-when.test.ts +0 -12
  137. package/src/__tests__/i18n.test.tsx +0 -283
  138. package/src/__tests__/merge-search-params-into-initial.test.ts +0 -525
  139. package/src/__tests__/qn.test.ts +0 -83
  140. package/src/__tests__/share-links.test.ts +0 -26
  141. package/src/__tests__/sort-by-accessor.test.ts +0 -48
  142. package/src/__tests__/stringify-nav-params.test.ts +0 -33
  143. package/src/__tests__/use-list-url-state.test.tsx +0 -250
  144. package/src/app/__tests__/action-form-field-labels.test.tsx +0 -92
  145. package/src/app/__tests__/action-form-shim.test.ts +0 -137
  146. package/src/app/__tests__/build-list-query-payload.test.ts +0 -107
  147. package/src/app/__tests__/config-edit-shim.test.ts +0 -57
  148. package/src/app/__tests__/content-editors.test.tsx +0 -128
  149. package/src/app/__tests__/content-preview.test.tsx +0 -184
  150. package/src/app/__tests__/entity-edit-create-search-params.test.tsx +0 -236
  151. package/src/app/__tests__/entity-edit-jsonb-embedded-submit.test.tsx +0 -239
  152. package/src/app/__tests__/entity-list-create-singleton.test.tsx +0 -128
  153. package/src/app/__tests__/entity-list-row-action-entity-target.test.tsx +0 -424
  154. package/src/app/__tests__/entity-list-row-action-icon.test.tsx +0 -248
  155. package/src/app/__tests__/entity-list-row-action-kumiko-actions-view.test.tsx +0 -162
  156. package/src/app/__tests__/entity-list-row-action-refetch.test.tsx +0 -401
  157. package/src/app/__tests__/entity-list-screen-padding.test.tsx +0 -111
  158. package/src/app/__tests__/feature-schema.test.ts +0 -40
  159. package/src/app/__tests__/form-schema.test.ts +0 -247
  160. package/src/app/__tests__/list-daterange-facet.test.ts +0 -165
  161. package/src/app/__tests__/list-filter-facets.test.tsx +0 -521
  162. package/src/app/__tests__/nav.test.ts +0 -198
  163. package/src/app/__tests__/projection-detail-actions.test.tsx +0 -719
  164. package/src/app/__tests__/projection-detail-reference-field.test.tsx +0 -165
  165. package/src/app/__tests__/projection-detail-shim.test.ts +0 -188
  166. package/src/app/__tests__/projection-detail-singleton.test.tsx +0 -191
  167. package/src/app/__tests__/projection-list-bad-shape.test.tsx +0 -152
  168. package/src/app/__tests__/projection-list-reference-column.test.tsx +0 -219
  169. package/src/app/__tests__/projection-list-search-sort.test.tsx +0 -212
  170. package/src/app/__tests__/qualified-names.test.ts +0 -37
  171. package/src/app/__tests__/return-to.test.ts +0 -396
  172. package/src/app/__tests__/row-actions-navigate-tab.test.ts +0 -223
  173. package/src/app/__tests__/row-default-edit-action.test.tsx +0 -324
  174. package/src/app/__tests__/screen-access-allows.test.ts +0 -34
  175. package/src/app/__tests__/secret-mint-body.test.tsx +0 -404
  176. package/src/app/__tests__/variable-chips.test.tsx +0 -70
  177. package/src/app/app-features-context.tsx +0 -25
  178. package/src/app/column-renderers.tsx +0 -68
  179. package/src/app/content-editors.tsx +0 -85
  180. package/src/app/content-preview.tsx +0 -67
  181. package/src/app/custom-screens.tsx +0 -29
  182. package/src/app/dashboard-body.tsx +0 -33
  183. package/src/app/extension-form-submit.tsx +0 -123
  184. package/src/app/extension-sections.tsx +0 -111
  185. package/src/app/form-schema.ts +0 -91
  186. package/src/app/kumiko-screen.tsx +0 -3614
  187. package/src/app/layout-fields.ts +0 -31
  188. package/src/app/list-facets.ts +0 -259
  189. package/src/app/nav.tsx +0 -258
  190. package/src/app/projection-detail-shim.ts +0 -85
  191. package/src/app/reference-facet-bridge.tsx +0 -58
  192. package/src/app/return-to.tsx +0 -234
  193. package/src/app/row-actions.ts +0 -639
  194. package/src/app/screen-access.ts +0 -63
  195. package/src/app/secret-mint-body.tsx +0 -259
  196. package/src/app/secrets-edit-body.tsx +0 -194
  197. package/src/app/use-embedded-screen.ts +0 -27
  198. package/src/app/variable-chips.tsx +0 -39
  199. package/src/app/write-failed-error.ts +0 -27
  200. package/src/components/__tests__/embedded-list-field.test.tsx +0 -531
  201. package/src/components/__tests__/multi-select-checkboxes.test.tsx +0 -162
  202. package/src/components/__tests__/related-list-section-empty-state.test.tsx +0 -143
  203. package/src/components/__tests__/related-list-section.test.tsx +0 -1669
  204. package/src/components/__tests__/render-edit-action-button.test.tsx +0 -205
  205. package/src/components/__tests__/render-edit-hidden-section.test.tsx +0 -128
  206. package/src/components/__tests__/render-edit-logic.test.ts +0 -314
  207. package/src/components/__tests__/render-edit-screen-description.test.tsx +0 -152
  208. package/src/components/__tests__/render-edit-submit-actions.test.tsx +0 -460
  209. package/src/components/__tests__/render-field-app-locale.test.tsx +0 -142
  210. package/src/components/__tests__/render-field-enum-option.test.tsx +0 -99
  211. package/src/components/__tests__/render-field-icon-derivation.test.tsx +0 -119
  212. package/src/components/__tests__/render-field-json-format.test.tsx +0 -164
  213. package/src/components/__tests__/render-field-money-embedded-list-consistency.test.tsx +0 -212
  214. package/src/components/__tests__/render-field-money-roundtrip.test.tsx +0 -217
  215. package/src/components/__tests__/render-field-number-grouping.test.tsx +0 -76
  216. package/src/components/__tests__/render-field-number-unit.test.tsx +0 -227
  217. package/src/components/__tests__/render-field-password-format.test.tsx +0 -101
  218. package/src/components/__tests__/render-field-select-display.test.tsx +0 -134
  219. package/src/components/__tests__/render-field-unit-format.test.tsx +0 -107
  220. package/src/components/__tests__/render-field-unsupported-types.test.tsx +0 -195
  221. package/src/components/__tests__/render-list-toolbar-icon-only.test.tsx +0 -146
  222. package/src/components/__tests__/write-form-section.test.tsx +0 -375
  223. package/src/components/embedded-list-field.tsx +0 -354
  224. package/src/components/grid-cell-for-field.tsx +0 -60
  225. package/src/components/multi-select-checkboxes.tsx +0 -93
  226. package/src/components/reference-create-dialog.tsx +0 -91
  227. package/src/components/related-list-section.tsx +0 -467
  228. package/src/components/render-edit-action-button.tsx +0 -106
  229. package/src/components/render-edit-logic.ts +0 -85
  230. package/src/components/render-edit-types.ts +0 -216
  231. package/src/components/render-edit.tsx +0 -1700
  232. package/src/components/render-field.tsx +0 -942
  233. package/src/components/render-list.tsx +0 -561
  234. package/src/components/write-form-section.tsx +0 -199
  235. package/src/context/draft-storage-context.tsx +0 -39
  236. package/src/hooks/__tests__/use-ai-text.test.tsx +0 -177
  237. package/src/hooks/__tests__/use-disclosure.test.tsx +0 -25
  238. package/src/hooks/__tests__/use-form.test.tsx +0 -65
  239. package/src/hooks/__tests__/use-mutation.test.tsx +0 -77
  240. package/src/hooks/__tests__/use-stream-handler.test.tsx +0 -107
  241. package/src/hooks/use-ai-text.ts +0 -172
  242. package/src/hooks/use-disclosure.ts +0 -20
  243. package/src/hooks/use-form.ts +0 -78
  244. package/src/hooks/use-list-url-state.ts +0 -171
  245. package/src/hooks/use-mutation.ts +0 -61
  246. package/src/hooks/use-query.ts +0 -132
  247. package/src/hooks/use-reference-lookup.ts +0 -76
  248. package/src/hooks/use-store.ts +0 -48
  249. package/src/hooks/use-stream-handler.ts +0 -134
  250. package/src/i18n-defaults.ts +0 -220
  251. package/src/i18n.tsx +0 -286
  252. package/src/index.ts +0 -272
  253. package/src/primitives.tsx +0 -1446
  254. package/src/share-links.ts +0 -12
  255. package/src/sort-by-accessor.ts +0 -20
  256. package/src/sse/live-events.tsx +0 -56
  257. package/src/tokens.tsx +0 -170
@@ -1,172 +0,0 @@
1
- import type { DispatcherError } from "@cosmicdrift/kumiko-headless";
2
- import { useCallback, useEffect, useRef, useState } from "react";
3
- import { useDispatcher } from "../context/dispatcher-context";
4
-
5
- // AiTextField/AiTextArea's client-side surface. Deliberately duplicates the
6
- // wire-contract shape from kumiko-enterprise's `ai-text` feature instead of
7
- // importing it — this package is public NPM, ai-text is a private Enterprise
8
- // package, and the widgets only ever talk HTTP (ai-text-primitive plan doc,
9
- // Architecture Decision 1). Keep these in lockstep with
10
- // kumiko-enterprise/packages/ai-text/src/{modes,feature}.ts by hand; there's
11
- // no compile-time link between the two repos.
12
-
13
- export const AI_TEXT_RUN_QN = "ai-text:query:run";
14
-
15
- export type AiTextMode = "complete" | "correct" | "translate" | "rewrite";
16
- export type AiTextRewriteStyle = "formal" | "casual" | "concise" | "expand";
17
-
18
- export type AiTextRunPayload =
19
- | { readonly mode: "complete"; readonly text: string }
20
- | { readonly mode: "correct"; readonly text: string }
21
- | { readonly mode: "translate"; readonly text: string; readonly targetLanguage: string }
22
- | { readonly mode: "rewrite"; readonly text: string; readonly style?: AiTextRewriteStyle };
23
-
24
- export type AiTextUsage = { readonly inputTokens: number; readonly outputTokens: number };
25
-
26
- export type AiTextRunResult =
27
- | { readonly type: "text"; readonly text: string; readonly usage: AiTextUsage }
28
- | { readonly type: "error"; readonly reason: string; readonly usage: AiTextUsage };
29
-
30
- // =============================================================================
31
- // useAiTextAction — one-shot request/response, any mode
32
- // =============================================================================
33
- //
34
- // v1 has no streaming (ai-text-primitive plan doc, sequencing note —
35
- // SSE-with-auth-reuse needs a framework-core `r.streamHandler` primitive,
36
- // tracked separately). `complete` goes through this same request/response
37
- // path as correct/translate/rewrite; `useCompletion` below adds debounce on
38
- // top for the ghost-text use-case specifically.
39
-
40
- export type AiTextActionState =
41
- | "idle"
42
- | "loading"
43
- | "success"
44
- | "error"
45
- | "cap-exceeded"
46
- | "unavailable";
47
-
48
- export type UseAiTextActionResult = {
49
- readonly run: (payload: AiTextRunPayload) => Promise<AiTextRunResult | null>;
50
- readonly state: AiTextActionState;
51
- readonly result: AiTextRunResult | null;
52
- readonly error: DispatcherError | null;
53
- readonly reset: () => void;
54
- };
55
-
56
- function stateForError(error: DispatcherError): AiTextActionState {
57
- if (error.code === "cap_exceeded" || error.code === "rate_limited") return "cap-exceeded";
58
- if (error.code === "feature_disabled") return "unavailable";
59
- return "error";
60
- }
61
-
62
- export function useAiTextAction(): UseAiTextActionResult {
63
- const dispatcher = useDispatcher();
64
- const [state, setState] = useState<AiTextActionState>("idle");
65
- const [result, setResult] = useState<AiTextRunResult | null>(null);
66
- const [error, setError] = useState<DispatcherError | null>(null);
67
-
68
- // Track the in-flight call so a newer `run()` cancels an older one, and
69
- // unmount doesn't set state after the component is gone — same pattern
70
- // as useQuery's activeCtrl.
71
- const activeCtrl = useRef<AbortController | null>(null);
72
- useEffect(() => {
73
- return () => {
74
- activeCtrl.current?.abort();
75
- };
76
- }, []);
77
-
78
- const run = useCallback(
79
- async (payload: AiTextRunPayload): Promise<AiTextRunResult | null> => {
80
- activeCtrl.current?.abort();
81
- const ctrl = new AbortController();
82
- activeCtrl.current = ctrl;
83
-
84
- setState("loading");
85
- setError(null);
86
-
87
- const res = await dispatcher.query<AiTextRunResult>(AI_TEXT_RUN_QN, payload, {
88
- signal: ctrl.signal,
89
- });
90
- if (ctrl.signal.aborted) return null;
91
-
92
- if (!res.isSuccess) {
93
- if (res.error.code === "aborted") return null;
94
- setError(res.error);
95
- setState(stateForError(res.error));
96
- return null;
97
- }
98
-
99
- setResult(res.data);
100
- setState("success");
101
- return res.data;
102
- },
103
- [dispatcher],
104
- );
105
-
106
- const reset = useCallback(() => {
107
- activeCtrl.current?.abort();
108
- setState("idle");
109
- setResult(null);
110
- setError(null);
111
- }, []);
112
-
113
- return { run, state, result, error, reset };
114
- }
115
-
116
- // =============================================================================
117
- // useCompletion — debounced ghost-text for AiTextField/AiTextArea
118
- // =============================================================================
119
- //
120
- // Debounce exists to keep the request-rate down against the monthly cap,
121
- // not for UX polish — every keystroke would otherwise burn a request.
122
-
123
- export type UseCompletionResult = {
124
- readonly suggestion: string | null;
125
- readonly state: AiTextActionState;
126
- readonly error: DispatcherError | null;
127
- /** Debounced — schedules a completion request `debounceMs` after the
128
- * last call. Calling again before the timer fires replaces it. */
129
- readonly requestCompletion: (text: string) => void;
130
- /** Cancels any pending/in-flight request and clears the suggestion. */
131
- readonly clear: () => void;
132
- };
133
-
134
- const DEFAULT_DEBOUNCE_MS = 500;
135
-
136
- export function useCompletion(debounceMs: number = DEFAULT_DEBOUNCE_MS): UseCompletionResult {
137
- const { run, state, result, error, reset } = useAiTextAction();
138
- const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
139
-
140
- const clearTimer = useCallback(() => {
141
- if (timerRef.current !== undefined) {
142
- clearTimeout(timerRef.current);
143
- timerRef.current = undefined;
144
- }
145
- }, []);
146
-
147
- const requestCompletion = useCallback(
148
- (text: string) => {
149
- clearTimer();
150
- if (text.length === 0) {
151
- reset();
152
- // skip: empty text, state already reset above
153
- return;
154
- }
155
- timerRef.current = setTimeout(() => {
156
- void run({ mode: "complete", text });
157
- }, debounceMs);
158
- },
159
- [clearTimer, reset, run, debounceMs],
160
- );
161
-
162
- const clear = useCallback(() => {
163
- clearTimer();
164
- reset();
165
- }, [clearTimer, reset]);
166
-
167
- useEffect(() => clearTimer, [clearTimer]);
168
-
169
- const suggestion = result?.type === "text" ? result.text : null;
170
-
171
- return { suggestion, state, error, requestCompletion, clear };
172
- }
@@ -1,20 +0,0 @@
1
- import { useCallback, useState } from "react";
2
-
3
- // Open/close state for dialogs, sheets and collapsibles — the standard
4
- // replacement for the hand-rolled `useState(false)` + toggle-callback
5
- // trio in app screens. All callbacks are referentially stable.
6
-
7
- export type UseDisclosureResult = {
8
- readonly open: boolean;
9
- readonly onOpen: () => void;
10
- readonly onClose: () => void;
11
- readonly onToggle: () => void;
12
- };
13
-
14
- export function useDisclosure(initialOpen = false): UseDisclosureResult {
15
- const [open, setOpen] = useState(initialOpen);
16
- const onOpen = useCallback(() => setOpen(true), []);
17
- const onClose = useCallback(() => setOpen(false), []);
18
- const onToggle = useCallback(() => setOpen((prev) => !prev), []);
19
- return { open, onOpen, onClose, onToggle };
20
- }
@@ -1,78 +0,0 @@
1
- import type {
2
- FormController,
3
- FormControllerOptions,
4
- FormSnapshot,
5
- FormValues,
6
- } from "@cosmicdrift/kumiko-headless";
7
- import { createFormController } from "@cosmicdrift/kumiko-headless";
8
- import { useMemo, useSyncExternalStore } from "react";
9
- import { useOptionalDispatcher } from "../context/dispatcher-context";
10
-
11
- // Thin React wrapper around createFormController. Returns both the
12
- // controller (imperative — setField, submit, reset) and the current
13
- // snapshot (reactive — values, errors, isDirty). React subscribes via
14
- // useSyncExternalStore so re-renders happen exactly when the snapshot
15
- // reference changes, which the controller guarantees.
16
- //
17
- // The `submit.dispatcher` config is filled from context if omitted,
18
- // so the typical call site is just:
19
- // const { snapshot, controller } = useForm({
20
- // initial: { ... },
21
- // schema,
22
- // submit: { type: "foo:create" },
23
- // });
24
- // and the hook wires the ambient DispatcherProvider's dispatcher in.
25
-
26
- export type UseFormOptions<TValues extends FormValues, TCtx = unknown> = FormControllerOptions<
27
- TValues,
28
- TCtx
29
- >;
30
-
31
- export type UseFormResult<TValues extends FormValues> = {
32
- readonly controller: FormController<TValues>;
33
- readonly snapshot: FormSnapshot<TValues>;
34
- };
35
-
36
- export function useForm<TValues extends FormValues, TCtx = unknown>(
37
- options: UseFormOptions<TValues, TCtx>,
38
- ): UseFormResult<TValues> {
39
- // Optional: a form with no `submit` config (or one with an explicit
40
- // dispatcher) never needs the ambient one, and mounting without a
41
- // DispatcherProvider must not crash just because useForm was called.
42
- const contextDispatcher = useOptionalDispatcher();
43
-
44
- // The controller is created once per mount. `options` mutates across
45
- // renders in normal React usage (closures get new references), but
46
- // the controller's behaviour depends only on the initial shape — we
47
- // deliberately don't re-create on option-identity change, which
48
- // would wipe in-flight state and "forget" dirty edits on every
49
- // parent re-render. If the app needs a reset, call controller.reset()
50
- // or re-mount the form (key prop).
51
- // Controller lifetime = hook lifetime. Options deliberately NOT in
52
- // the deps array; re-creating on option-identity change would wipe
53
- // in-flight state and dirty edits on every parent re-render.
54
- // biome-ignore lint/correctness/useExhaustiveDependencies: intentional — controller is lifetime-scoped, not render-scoped
55
- const controller = useMemo<FormController<TValues>>(() => {
56
- // Default submit.dispatcher to the ambient one when omitted.
57
- type FullOptions = FormControllerOptions<TValues, TCtx>;
58
- const rawSubmit = options.submit;
59
- const submitConfig: FullOptions["submit"] = rawSubmit
60
- ? { ...rawSubmit, dispatcher: rawSubmit.dispatcher ?? contextDispatcher }
61
- : undefined;
62
- const controllerOptions: FullOptions = {
63
- ...(options as FullOptions),
64
- ...(submitConfig !== undefined && { submit: submitConfig }),
65
- };
66
- return createFormController<TValues, TCtx>(controllerOptions);
67
- }, []);
68
-
69
- const snapshot = useSyncExternalStore(
70
- (notify) => controller.subscribe(notify),
71
- () => controller.getSnapshot(),
72
- // SSR snapshot: same as getSnapshot(). The controller's initial
73
- // snapshot is deterministic from options.initial, safe for server.
74
- () => controller.getSnapshot(),
75
- );
76
-
77
- return { controller, snapshot };
78
- }
@@ -1,171 +0,0 @@
1
- // useListUrlState — bündelt sort/dir/page/q einer entityList in einen
2
- // per-screen-id namespaced URL-State. Mit Screen-ID-Prefix damit zwei
3
- // Listen auf derselben Route (z.B. Dashboard mit Orders + Incidents)
4
- // nicht über dieselben Query-Keys streiten.
5
- //
6
- // Param-Schema pro Liste:
7
- // <screenId>.sort — field name (string)
8
- // <screenId>.dir — "asc" | "desc"
9
- // <screenId>.q — search term (URL-encoded)
10
- // <screenId>.page — 1-based page number (nur bei pagination="pages")
11
- // <screenId>.f.<field> — Faceted-Filter: comma-joined selected values
12
- //
13
- // Schreibt mit setSearchParams (replaceState — kein push), damit
14
- // Sort/Filter-Toggles nicht die Browser-History fluten.
15
-
16
- import { useCallback, useMemo } from "react";
17
- import { useNav } from "../app/nav";
18
- import type { DataTableSort, DataTableSortDir } from "../primitives";
19
-
20
- // ListSort + DataTableSort hatten dieselbe Shape und drohten zu driften
21
- // — aliased auf den primitives-Type (eine Quelle, kein Cast in RenderList).
22
- export type ListSortDir = DataTableSortDir;
23
- export type ListSort = DataTableSort;
24
-
25
- export type ListUrlState = {
26
- /** Aktive Sortierung (oder null = unsorted, Server liefert Default-Order). */
27
- readonly sort: ListSort | null;
28
- /** Search-Term (leer wenn nicht gesetzt). */
29
- readonly q: string;
30
- /** 1-basierte Page-Nummer. Bei pagination="infinite" oder false ist
31
- * der Wert ignoriert; Caller liest ihn nur wenn relevant. */
32
- readonly page: number;
33
- /** Aktive Faceted-Filter: field → ausgewählte Werte (Strings; Caller
34
- * coerced pro Field-Type). Leer wenn kein Filter gesetzt. */
35
- readonly filters: Readonly<Record<string, readonly string[]>>;
36
- };
37
-
38
- export type ListUrlStateApi = ListUrlState & {
39
- /** Setzt sort + dir atomar. null = unsorted (löscht beide Keys). */
40
- readonly setSort: (next: ListSort | null) => void;
41
- /** Setzt den Search-Term. Empty-String löscht den Key. Caller debounced
42
- * selber (z.B. useDebouncedCallback in der Search-Input-Komponente). */
43
- readonly setQ: (next: string) => void;
44
- /** Setzt die Page. 1 oder kleiner löscht den Key (Default-Page). */
45
- readonly setPage: (next: number) => void;
46
- /** Setzt die ausgewählten Werte eines Facet-Felds. Leeres Array löscht
47
- * den Key. Resettet die Page (wie sort/search). */
48
- readonly setFilter: (field: string, values: readonly string[]) => void;
49
- /** Löscht alle aktiven Faceted-Filter (Reset-Button). */
50
- readonly clearFilters: () => void;
51
- /** Sets both bounds of a dateRange facet in ONE params update — two
52
- * setFilter calls would overwrite each other, since the second still sees
53
- * the old searchParams. An empty string clears that bound. Resets the page
54
- * like setFilter. */
55
- readonly setDateRange: (
56
- field: string,
57
- next: { readonly from: string; readonly to: string },
58
- ) => void;
59
- };
60
-
61
- // `.` als Trenner: lesbar (`?orders.sort=name`), kollidiert nicht mit
62
- // üblichen Field-Namen (kebab- oder camelCase ohne Punkt). Boot-Validator
63
- // pinnt screen.id ohne Punkt — siehe boot-validator entityList Section.
64
- function key(screenId: string, suffix: string): string {
65
- return `${screenId}.${suffix}`;
66
- }
67
-
68
- function parseDir(value: string | undefined): ListSortDir | undefined {
69
- return value === "asc" || value === "desc" ? value : undefined;
70
- }
71
-
72
- function parsePage(value: string | undefined): number {
73
- if (value === undefined) return 1;
74
- const n = Number.parseInt(value, 10);
75
- return Number.isFinite(n) && n > 0 ? n : 1;
76
- }
77
-
78
- export function useListUrlState(screenId: string): ListUrlStateApi {
79
- const nav = useNav();
80
- const params = nav.searchParams;
81
-
82
- const sort = useMemo<ListSort | null>(() => {
83
- const field = params[key(screenId, "sort")];
84
- const dir = parseDir(params[key(screenId, "dir")]);
85
- if (field === undefined || field === "" || dir === undefined) return null;
86
- return { field, dir };
87
- }, [params, screenId]);
88
-
89
- const q = params[key(screenId, "q")] ?? "";
90
- const page = parsePage(params[key(screenId, "page")]);
91
-
92
- const filterPrefix = `${screenId}.f.`;
93
- const filters = useMemo<Readonly<Record<string, readonly string[]>>>(() => {
94
- const out: Record<string, readonly string[]> = {};
95
- for (const [k, v] of Object.entries(params)) {
96
- if (!k.startsWith(filterPrefix) || v === "") continue;
97
- const field = k.slice(filterPrefix.length);
98
- if (field === "") continue;
99
- // ponytail: comma-join — Facet-Werte sind select-Options/booleans
100
- // ohne Komma; upgrade auf repeated-keys falls je Komma-Werte nötig.
101
- out[field] = v.split(",").filter((s) => s !== "");
102
- }
103
- return out;
104
- }, [params, filterPrefix]);
105
-
106
- const setSort = useCallback(
107
- (next: ListSort | null) => {
108
- // Atomares Update: bei jedem Sort-Wechsel resetten wir auch die
109
- // Page (sonst wäre der User auf "Seite 5 von alter Sortierung"
110
- // hängen, was visuell verwirrend ist). Page-Reset gilt auch bei
111
- // null — gleiche Logik.
112
- nav.setSearchParams({
113
- [key(screenId, "sort")]: next === null ? null : next.field,
114
- [key(screenId, "dir")]: next === null ? null : next.dir,
115
- [key(screenId, "page")]: null,
116
- });
117
- },
118
- [nav, screenId],
119
- );
120
-
121
- const setQ = useCallback(
122
- (next: string) => {
123
- // Search-Change resettet Page (gleicher Grund wie Sort) UND
124
- // Sort? Nein — User kann mit aktivem Sort suchen wollen, das
125
- // soll die Sortierung nicht zerlegen. Nur Page wird gereset.
126
- nav.setSearchParams({
127
- [key(screenId, "q")]: next === "" ? null : next,
128
- [key(screenId, "page")]: null,
129
- });
130
- },
131
- [nav, screenId],
132
- );
133
-
134
- const setPage = useCallback(
135
- (next: number) => {
136
- nav.setSearchParams({
137
- [key(screenId, "page")]: next <= 1 ? null : String(next),
138
- });
139
- },
140
- [nav, screenId],
141
- );
142
-
143
- const setFilter = useCallback(
144
- (field: string, values: readonly string[]) => {
145
- nav.setSearchParams({
146
- [key(screenId, `f.${field}`)]: values.length === 0 ? null : values.join(","),
147
- [key(screenId, "page")]: null,
148
- });
149
- },
150
- [nav, screenId],
151
- );
152
-
153
- const setDateRange = useCallback(
154
- (field: string, next: { readonly from: string; readonly to: string }) => {
155
- nav.setSearchParams({
156
- [key(screenId, `f.${field}.from`)]: next.from === "" ? null : next.from,
157
- [key(screenId, `f.${field}.to`)]: next.to === "" ? null : next.to,
158
- [key(screenId, "page")]: null,
159
- });
160
- },
161
- [nav, screenId],
162
- );
163
-
164
- const clearFilters = useCallback(() => {
165
- const updates: Record<string, string | null> = { [key(screenId, "page")]: null };
166
- for (const field of Object.keys(filters)) updates[key(screenId, `f.${field}`)] = null;
167
- nav.setSearchParams(updates);
168
- }, [nav, screenId, filters]);
169
-
170
- return { sort, q, page, filters, setSort, setQ, setPage, setFilter, clearFilters, setDateRange };
171
- }
@@ -1,61 +0,0 @@
1
- import type { DispatcherError, WriteResult } from "@cosmicdrift/kumiko-headless";
2
- import { useCallback, useRef, useState } from "react";
3
- import { useDispatcher } from "../context/dispatcher-context";
4
-
5
- // React wrapper around dispatcher.write — the write-side sibling of
6
- // useQuery. One hook instance per handler-type; `mutate` carries the
7
- // payload so a single instance serves list-row actions with varying
8
- // payloads.
9
- //
10
- // `mutate` resolves with the raw WriteResult so callers can branch
11
- // (navigate on success, keep the form open on failure) without waiting
12
- // for a re-render of `error`/`data`.
13
-
14
- export type UseMutationResult<TData> = {
15
- readonly mutate: (payload: unknown) => Promise<WriteResult<TData>>;
16
- readonly pending: boolean;
17
- readonly error: DispatcherError | null;
18
- readonly data: TData | null;
19
- readonly reset: () => void;
20
- };
21
-
22
- export function useMutation<TData = unknown>(type: string): UseMutationResult<TData> {
23
- const dispatcher = useDispatcher();
24
- const [pending, setPending] = useState(false);
25
- const [error, setError] = useState<DispatcherError | null>(null);
26
- const [data, setData] = useState<TData | null>(null);
27
- // Sequence guard: `mutate` has no abort (unlike useQuery's GETs, a write
28
- // already landed server-side and can't be cancelled). Two overlapping
29
- // calls on one instance (e.g. two list-row actions) must not let the
30
- // first-to-resolve clobber pending/error/data set by the second — only
31
- // the most recently STARTED call's outcome may update shared state.
32
- const sequence = useRef(0);
33
-
34
- const mutate = useCallback(
35
- async (payload: unknown): Promise<WriteResult<TData>> => {
36
- const callSeq = ++sequence.current;
37
- setPending(true);
38
- setError(null);
39
- const result = await dispatcher.write<TData>(type, payload);
40
- if (callSeq === sequence.current) {
41
- if (result.isSuccess) {
42
- setData(result.data);
43
- } else {
44
- setError(result.error);
45
- }
46
- setPending(false);
47
- }
48
- return result;
49
- },
50
- [dispatcher, type],
51
- );
52
-
53
- const reset = useCallback(() => {
54
- sequence.current++; // invalidate any in-flight mutate's late update
55
- setPending(false);
56
- setError(null);
57
- setData(null);
58
- }, []);
59
-
60
- return { mutate, pending, error, data, reset };
61
- }
@@ -1,132 +0,0 @@
1
- import type { DispatcherError } from "@cosmicdrift/kumiko-headless";
2
- import { useCallback, useEffect, useRef, useState } from "react";
3
- import { useDispatcher } from "../context/dispatcher-context";
4
- import { useLiveEvents } from "../sse/live-events";
5
-
6
- // React wrapper around dispatcher.query. Fires on mount, re-fires
7
- // whenever `type` or the serialized payload change, and exposes a
8
- // `refetch` imperative hook for after-mutation reloads.
9
- //
10
- // Cancellation: each fetch owns an AbortController; a newer fetch
11
- // (change in deps or manual refetch) aborts the older one's network
12
- // call before firing, so a slow request can't overwrite a fresh one.
13
- // Unmount aborts any in-flight call too — React will throw the
14
- // "state on unmounted" warning if we set state after unmount, and
15
- // aborting pre-emptively makes that path a no-op.
16
- //
17
- // Result shape modeled on the dispatcher's own QueryResult: a
18
- // discriminated state so callers can branch on `loading | data | error`
19
- // without undefined-juggling. The first render is `loading: true,
20
- // data: null, error: null` (no optimistic "online" cache — add that
21
- // later if needed).
22
-
23
- export type UseQueryResult<TData> = {
24
- readonly data: TData | null;
25
- readonly error: DispatcherError | null;
26
- readonly loading: boolean;
27
- readonly refetch: () => Promise<void>;
28
- };
29
-
30
- export type UseQueryOptions = {
31
- // When false, skips the automatic fetch-on-mount and re-fetch on
32
- // dep-change. Useful for lazy queries that only run after a user
33
- // action. `refetch()` still works as an imperative trigger.
34
- readonly enabled?: boolean;
35
- // When true, subscribe to SSE events for the entity this query
36
- // targets and refetch on any create/update/delete/restore event.
37
- // The entity-name is parsed from the query type using Kumiko's
38
- // `<feature>:query:<entity>:<verb>` convention — if the type
39
- // doesn't follow that shape, live-mode is a no-op.
40
- readonly live?: boolean;
41
- };
42
-
43
- // Extract the entity-name from a standard Kumiko query type. Returns
44
- // undefined for non-conforming types so the live-mode silently skips
45
- // them instead of subscribing to a channel no event will ever match.
46
- export function entityFromQueryType(type: string): string | undefined {
47
- // Expected shape: "<feature>:query:<entity>:<verb>"
48
- const parts = type.split(":");
49
- if (parts.length !== 4) return undefined;
50
- if (parts[1] !== "query") return undefined;
51
- return parts[2];
52
- }
53
-
54
- export function useQuery<TData = unknown>(
55
- type: string,
56
- payload: unknown,
57
- options: UseQueryOptions = {},
58
- ): UseQueryResult<TData> {
59
- const dispatcher = useDispatcher();
60
- const { enabled = true, live = false } = options;
61
-
62
- const [data, setData] = useState<TData | null>(null);
63
- const [error, setError] = useState<DispatcherError | null>(null);
64
- const [loading, setLoading] = useState<boolean>(enabled);
65
-
66
- // Track the AbortController for the most recent fetch so a newer
67
- // call can cancel the older one. Ref, not state, because the
68
- // controller identity shouldn't trigger a re-render.
69
- const activeCtrl = useRef<AbortController | null>(null);
70
-
71
- // Serialize payload so object-identity-changes across renders (the
72
- // caller building a fresh `{}` every render) don't loop. Strings are
73
- // stable by reference in React's dep-array check.
74
- const payloadKey = JSON.stringify(payload);
75
-
76
- // `dispatcher` is stable (context identity); `type` + `payloadKey`
77
- // are the meaningful re-run triggers. `payload` itself is intentionally
78
- // not in deps — it's serialized into payloadKey above.
79
- // biome-ignore lint/correctness/useExhaustiveDependencies: payload goes through payloadKey
80
- const run = useCallback(async (): Promise<void> => {
81
- // Abort whatever's in flight. observer on Safari 17+ handles this
82
- // without raising a fetch-throw in the previous caller — they
83
- // already set the error path.
84
- activeCtrl.current?.abort();
85
- const ctrl = new AbortController();
86
- activeCtrl.current = ctrl;
87
-
88
- setLoading(true);
89
- const result = await dispatcher.query<TData>(type, payload, { signal: ctrl.signal });
90
- // skip: a newer fetch already superseded this one, don't clobber its state
91
- if (ctrl.signal.aborted) return;
92
- if (result.isSuccess) {
93
- setData(result.data);
94
- setError(null);
95
- } else {
96
- // A cancelled request comes back with code "aborted" from the
97
- // skip: aborted request, a newer run already replaces this result
98
- if (result.error.code === "aborted") return;
99
- setError(result.error);
100
- }
101
- setLoading(false);
102
- }, [dispatcher, type, payloadKey]);
103
-
104
- useEffect(() => {
105
- if (!enabled) {
106
- setLoading(false);
107
- // skip: query disabled, loading state already cleared above
108
- return;
109
- }
110
- void run();
111
- return () => {
112
- activeCtrl.current?.abort();
113
- };
114
- }, [enabled, run]);
115
-
116
- // Live-mode: auf SSE-Events für die Query-Entity hören und refetchen.
117
- // Separater Effect, damit eine Änderung an `live` oder `type` das
118
- // Subscription-Lifecycle genau einmal durchwalzt.
119
- const subscribeLive = useLiveEvents();
120
- useEffect(() => {
121
- // skip: live mode or query disabled, no SSE subscription needed
122
- if (!live || !enabled) return;
123
- const entity = entityFromQueryType(type);
124
- // skip: query type has no mapped entity, nothing to subscribe to
125
- if (entity === undefined) return;
126
- return subscribeLive(entity, () => {
127
- void run();
128
- });
129
- }, [live, enabled, type, run, subscribeLive]);
130
-
131
- return { data, error, loading, refetch: run };
132
- }