@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,11 +1,11 @@
1
1
  import { toInstant } from "@cosmicdrift/kumiko-headless";
2
-
3
2
  // Shared timestamp formatter for operator screens (audit log, job runs) —
4
3
  // falls back to the raw ISO string on an unparseable value instead of "Invalid Date".
5
- export function formatWhen(value: string): string {
6
- try {
7
- return toInstant(value).toLocaleString();
8
- } catch {
9
- return value;
10
- }
4
+ export function formatWhen(value) {
5
+ try {
6
+ return toInstant(value).toLocaleString();
7
+ }
8
+ catch {
9
+ return value;
10
+ }
11
11
  }
@@ -0,0 +1,3 @@
1
+ export declare const REFERENCE_COMBOBOX_LIMIT = 50;
2
+ export declare const REFERENCE_LIST_LOOKUP_LIMIT = 200;
3
+ export declare const REFERENCE_SEARCH_DEBOUNCE_MS = 300;
@@ -12,7 +12,6 @@
12
12
  // Die Konstanten leben in einem eigenen Modul damit Future-Author
13
13
  // sie an einer Stelle findet statt verstreut zwischen render-field
14
14
  // und use-reference-lookup.
15
-
16
15
  export const REFERENCE_COMBOBOX_LIMIT = 50;
17
16
  export const REFERENCE_LIST_LOOKUP_LIMIT = 200;
18
17
  export const REFERENCE_SEARCH_DEBOUNCE_MS = 300;
@@ -0,0 +1,52 @@
1
+ import type { DispatcherError } from "@cosmicdrift/kumiko-headless";
2
+ export declare const AI_TEXT_RUN_QN = "ai-text:query:run";
3
+ export type AiTextMode = "complete" | "correct" | "translate" | "rewrite";
4
+ export type AiTextRewriteStyle = "formal" | "casual" | "concise" | "expand";
5
+ export type AiTextRunPayload = {
6
+ readonly mode: "complete";
7
+ readonly text: string;
8
+ } | {
9
+ readonly mode: "correct";
10
+ readonly text: string;
11
+ } | {
12
+ readonly mode: "translate";
13
+ readonly text: string;
14
+ readonly targetLanguage: string;
15
+ } | {
16
+ readonly mode: "rewrite";
17
+ readonly text: string;
18
+ readonly style?: AiTextRewriteStyle;
19
+ };
20
+ export type AiTextUsage = {
21
+ readonly inputTokens: number;
22
+ readonly outputTokens: number;
23
+ };
24
+ export type AiTextRunResult = {
25
+ readonly type: "text";
26
+ readonly text: string;
27
+ readonly usage: AiTextUsage;
28
+ } | {
29
+ readonly type: "error";
30
+ readonly reason: string;
31
+ readonly usage: AiTextUsage;
32
+ };
33
+ export type AiTextActionState = "idle" | "loading" | "success" | "error" | "cap-exceeded" | "unavailable";
34
+ export type UseAiTextActionResult = {
35
+ readonly run: (payload: AiTextRunPayload) => Promise<AiTextRunResult | null>;
36
+ readonly state: AiTextActionState;
37
+ readonly result: AiTextRunResult | null;
38
+ readonly error: DispatcherError | null;
39
+ readonly reset: () => void;
40
+ };
41
+ export declare function useAiTextAction(): UseAiTextActionResult;
42
+ export type UseCompletionResult = {
43
+ readonly suggestion: string | null;
44
+ readonly state: AiTextActionState;
45
+ readonly error: DispatcherError | null;
46
+ /** Debounced — schedules a completion request `debounceMs` after the
47
+ * last call. Calling again before the timer fires replaces it. */
48
+ readonly requestCompletion: (text: string) => void;
49
+ /** Cancels any pending/in-flight request and clears the suggestion. */
50
+ readonly clear: () => void;
51
+ };
52
+ export declare function useCompletion(debounceMs?: number): UseCompletionResult;
@@ -0,0 +1,90 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { useDispatcher } from "../context/dispatcher-context.js";
3
+ // AiTextField/AiTextArea's client-side surface. Deliberately duplicates the
4
+ // wire-contract shape from kumiko-enterprise's `ai-text` feature instead of
5
+ // importing it — this package is public NPM, ai-text is a private Enterprise
6
+ // package, and the widgets only ever talk HTTP (ai-text-primitive plan doc,
7
+ // Architecture Decision 1). Keep these in lockstep with
8
+ // kumiko-enterprise/packages/ai-text/src/{modes,feature}.ts by hand; there's
9
+ // no compile-time link between the two repos.
10
+ export const AI_TEXT_RUN_QN = "ai-text:query:run";
11
+ function stateForError(error) {
12
+ if (error.code === "cap_exceeded" || error.code === "rate_limited")
13
+ return "cap-exceeded";
14
+ if (error.code === "feature_disabled")
15
+ return "unavailable";
16
+ return "error";
17
+ }
18
+ export function useAiTextAction() {
19
+ const dispatcher = useDispatcher();
20
+ const [state, setState] = useState("idle");
21
+ const [result, setResult] = useState(null);
22
+ const [error, setError] = useState(null);
23
+ // Track the in-flight call so a newer `run()` cancels an older one, and
24
+ // unmount doesn't set state after the component is gone — same pattern
25
+ // as useQuery's activeCtrl.
26
+ const activeCtrl = useRef(null);
27
+ useEffect(() => {
28
+ return () => {
29
+ activeCtrl.current?.abort();
30
+ };
31
+ }, []);
32
+ const run = useCallback(async (payload) => {
33
+ activeCtrl.current?.abort();
34
+ const ctrl = new AbortController();
35
+ activeCtrl.current = ctrl;
36
+ setState("loading");
37
+ setError(null);
38
+ const res = await dispatcher.query(AI_TEXT_RUN_QN, payload, {
39
+ signal: ctrl.signal,
40
+ });
41
+ if (ctrl.signal.aborted)
42
+ return null;
43
+ if (!res.isSuccess) {
44
+ if (res.error.code === "aborted")
45
+ return null;
46
+ setError(res.error);
47
+ setState(stateForError(res.error));
48
+ return null;
49
+ }
50
+ setResult(res.data);
51
+ setState("success");
52
+ return res.data;
53
+ }, [dispatcher]);
54
+ const reset = useCallback(() => {
55
+ activeCtrl.current?.abort();
56
+ setState("idle");
57
+ setResult(null);
58
+ setError(null);
59
+ }, []);
60
+ return { run, state, result, error, reset };
61
+ }
62
+ const DEFAULT_DEBOUNCE_MS = 500;
63
+ export function useCompletion(debounceMs = DEFAULT_DEBOUNCE_MS) {
64
+ const { run, state, result, error, reset } = useAiTextAction();
65
+ const timerRef = useRef(undefined);
66
+ const clearTimer = useCallback(() => {
67
+ if (timerRef.current !== undefined) {
68
+ clearTimeout(timerRef.current);
69
+ timerRef.current = undefined;
70
+ }
71
+ }, []);
72
+ const requestCompletion = useCallback((text) => {
73
+ clearTimer();
74
+ if (text.length === 0) {
75
+ reset();
76
+ // skip: empty text, state already reset above
77
+ return;
78
+ }
79
+ timerRef.current = setTimeout(() => {
80
+ void run({ mode: "complete", text });
81
+ }, debounceMs);
82
+ }, [clearTimer, reset, run, debounceMs]);
83
+ const clear = useCallback(() => {
84
+ clearTimer();
85
+ reset();
86
+ }, [clearTimer, reset]);
87
+ useEffect(() => clearTimer, [clearTimer]);
88
+ const suggestion = result?.type === "text" ? result.text : null;
89
+ return { suggestion, state, error, requestCompletion, clear };
90
+ }
@@ -0,0 +1,7 @@
1
+ export type UseDisclosureResult = {
2
+ readonly open: boolean;
3
+ readonly onOpen: () => void;
4
+ readonly onClose: () => void;
5
+ readonly onToggle: () => void;
6
+ };
7
+ export declare function useDisclosure(initialOpen?: boolean): UseDisclosureResult;
@@ -0,0 +1,8 @@
1
+ import { useCallback, useState } from "react";
2
+ export function useDisclosure(initialOpen = false) {
3
+ const [open, setOpen] = useState(initialOpen);
4
+ const onOpen = useCallback(() => setOpen(true), []);
5
+ const onClose = useCallback(() => setOpen(false), []);
6
+ const onToggle = useCallback(() => setOpen((prev) => !prev), []);
7
+ return { open, onOpen, onClose, onToggle };
8
+ }
@@ -0,0 +1,7 @@
1
+ import type { FormController, FormControllerOptions, FormSnapshot, FormValues } from "@cosmicdrift/kumiko-headless";
2
+ export type UseFormOptions<TValues extends FormValues, TCtx = unknown> = FormControllerOptions<TValues, TCtx>;
3
+ export type UseFormResult<TValues extends FormValues> = {
4
+ readonly controller: FormController<TValues>;
5
+ readonly snapshot: FormSnapshot<TValues>;
6
+ };
7
+ export declare function useForm<TValues extends FormValues, TCtx = unknown>(options: UseFormOptions<TValues, TCtx>): UseFormResult<TValues>;
@@ -0,0 +1,36 @@
1
+ import { createFormController } from "@cosmicdrift/kumiko-headless";
2
+ import { useMemo, useSyncExternalStore } from "react";
3
+ import { useOptionalDispatcher } from "../context/dispatcher-context.js";
4
+ export function useForm(options) {
5
+ // Optional: a form with no `submit` config (or one with an explicit
6
+ // dispatcher) never needs the ambient one, and mounting without a
7
+ // DispatcherProvider must not crash just because useForm was called.
8
+ const contextDispatcher = useOptionalDispatcher();
9
+ // The controller is created once per mount. `options` mutates across
10
+ // renders in normal React usage (closures get new references), but
11
+ // the controller's behaviour depends only on the initial shape — we
12
+ // deliberately don't re-create on option-identity change, which
13
+ // would wipe in-flight state and "forget" dirty edits on every
14
+ // parent re-render. If the app needs a reset, call controller.reset()
15
+ // or re-mount the form (key prop).
16
+ // Controller lifetime = hook lifetime. Options deliberately NOT in
17
+ // the deps array; re-creating on option-identity change would wipe
18
+ // in-flight state and dirty edits on every parent re-render.
19
+ // biome-ignore lint/correctness/useExhaustiveDependencies: intentional — controller is lifetime-scoped, not render-scoped
20
+ const controller = useMemo(() => {
21
+ const rawSubmit = options.submit;
22
+ const submitConfig = rawSubmit
23
+ ? { ...rawSubmit, dispatcher: rawSubmit.dispatcher ?? contextDispatcher }
24
+ : undefined;
25
+ const controllerOptions = {
26
+ ...options,
27
+ ...(submitConfig !== undefined && { submit: submitConfig }),
28
+ };
29
+ return createFormController(controllerOptions);
30
+ }, []);
31
+ const snapshot = useSyncExternalStore((notify) => controller.subscribe(notify), () => controller.getSnapshot(),
32
+ // SSR snapshot: same as getSnapshot(). The controller's initial
33
+ // snapshot is deterministic from options.initial, safe for server.
34
+ () => controller.getSnapshot());
35
+ return { controller, snapshot };
36
+ }
@@ -0,0 +1,43 @@
1
+ import type { DataTableSort, DataTableSortDir } from "../primitives.js";
2
+ export type ListSortDir = DataTableSortDir;
3
+ export type ListSort = DataTableSort;
4
+ export type ListUrlState = {
5
+ /** Aktive Sortierung (oder null = unsorted, Server liefert Default-Order). */
6
+ readonly sort: ListSort | null;
7
+ /** Search-Term (leer wenn nicht gesetzt). */
8
+ readonly q: string;
9
+ /** 1-basierte Page-Nummer. Bei pagination="infinite" oder false ist
10
+ * der Wert ignoriert; Caller liest ihn nur wenn relevant. */
11
+ readonly page: number;
12
+ /** User-chosen page size from `PAGE_SIZE_OPTIONS`; undefined = screen default. */
13
+ readonly pageSize: number | undefined;
14
+ /** Aktive Faceted-Filter: field → ausgewählte Werte (Strings; Caller
15
+ * coerced pro Field-Type). Leer wenn kein Filter gesetzt. */
16
+ readonly filters: Readonly<Record<string, readonly string[]>>;
17
+ };
18
+ export type ListUrlStateApi = ListUrlState & {
19
+ /** Setzt sort + dir atomar. null = unsorted (löscht beide Keys). */
20
+ readonly setSort: (next: ListSort | null) => void;
21
+ /** Setzt den Search-Term. Empty-String löscht den Key. Caller debounced
22
+ * selber (z.B. useDebouncedCallback in der Search-Input-Komponente). */
23
+ readonly setQ: (next: string) => void;
24
+ /** Setzt die Page. 1 oder kleiner löscht den Key (Default-Page). */
25
+ readonly setPage: (next: number) => void;
26
+ /** Sets the page size and resets the page; undefined clears the key. */
27
+ readonly setPageSize: (next: number | undefined) => void;
28
+ /** Setzt die ausgewählten Werte eines Facet-Felds. Leeres Array löscht
29
+ * den Key. Resettet die Page (wie sort/search). */
30
+ readonly setFilter: (field: string, values: readonly string[]) => void;
31
+ /** Löscht alle aktiven Faceted-Filter (Reset-Button). */
32
+ readonly clearFilters: () => void;
33
+ /** Sets both bounds of a dateRange facet in ONE params update — two
34
+ * setFilter calls would overwrite each other, since the second still sees
35
+ * the old searchParams. An empty string clears that bound. Resets the page
36
+ * like setFilter. */
37
+ readonly setDateRange: (field: string, next: {
38
+ readonly from: string;
39
+ readonly to: string;
40
+ }) => void;
41
+ };
42
+ export declare const PAGE_SIZE_OPTIONS: readonly number[];
43
+ export declare function useListUrlState(screenId: string): ListUrlStateApi;
@@ -0,0 +1,131 @@
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
+ import { useCallback, useMemo } from "react";
16
+ import { useNav } from "../app/nav.js";
17
+ // `.` als Trenner: lesbar (`?orders.sort=name`), kollidiert nicht mit
18
+ // üblichen Field-Namen (kebab- oder camelCase ohne Punkt). Boot-Validator
19
+ // pinnt screen.id ohne Punkt — siehe boot-validator entityList Section.
20
+ export const PAGE_SIZE_OPTIONS = [25, 50, 100];
21
+ function parsePageSize(value) {
22
+ if (value === undefined)
23
+ return undefined;
24
+ const n = Number.parseInt(value, 10);
25
+ return PAGE_SIZE_OPTIONS.includes(n) ? n : undefined;
26
+ }
27
+ function key(screenId, suffix) {
28
+ return `${screenId}.${suffix}`;
29
+ }
30
+ function parseDir(value) {
31
+ return value === "asc" || value === "desc" ? value : undefined;
32
+ }
33
+ function parsePage(value) {
34
+ if (value === undefined)
35
+ return 1;
36
+ const n = Number.parseInt(value, 10);
37
+ return Number.isFinite(n) && n > 0 ? n : 1;
38
+ }
39
+ export function useListUrlState(screenId) {
40
+ const nav = useNav();
41
+ const params = nav.searchParams;
42
+ const sort = useMemo(() => {
43
+ const field = params[key(screenId, "sort")];
44
+ const dir = parseDir(params[key(screenId, "dir")]);
45
+ if (field === undefined || field === "" || dir === undefined)
46
+ return null;
47
+ return { field, dir };
48
+ }, [params, screenId]);
49
+ const q = params[key(screenId, "q")] ?? "";
50
+ const page = parsePage(params[key(screenId, "page")]);
51
+ const pageSize = parsePageSize(params[key(screenId, "size")]);
52
+ const filterPrefix = `${screenId}.f.`;
53
+ const filters = useMemo(() => {
54
+ const out = {};
55
+ for (const [k, v] of Object.entries(params)) {
56
+ if (!k.startsWith(filterPrefix) || v === "")
57
+ continue;
58
+ const field = k.slice(filterPrefix.length);
59
+ if (field === "")
60
+ continue;
61
+ // ponytail: comma-join — Facet-Werte sind select-Options/booleans
62
+ // ohne Komma; upgrade auf repeated-keys falls je Komma-Werte nötig.
63
+ out[field] = v.split(",").filter((s) => s !== "");
64
+ }
65
+ return out;
66
+ }, [params, filterPrefix]);
67
+ const setSort = useCallback((next) => {
68
+ // Atomares Update: bei jedem Sort-Wechsel resetten wir auch die
69
+ // Page (sonst wäre der User auf "Seite 5 von alter Sortierung"
70
+ // hängen, was visuell verwirrend ist). Page-Reset gilt auch bei
71
+ // null — gleiche Logik.
72
+ nav.setSearchParams({
73
+ [key(screenId, "sort")]: next === null ? null : next.field,
74
+ [key(screenId, "dir")]: next === null ? null : next.dir,
75
+ [key(screenId, "page")]: null,
76
+ });
77
+ }, [nav, screenId]);
78
+ const setQ = useCallback((next) => {
79
+ // Search-Change resettet Page (gleicher Grund wie Sort) UND
80
+ // Sort? Nein — User kann mit aktivem Sort suchen wollen, das
81
+ // soll die Sortierung nicht zerlegen. Nur Page wird gereset.
82
+ nav.setSearchParams({
83
+ [key(screenId, "q")]: next === "" ? null : next,
84
+ [key(screenId, "page")]: null,
85
+ });
86
+ }, [nav, screenId]);
87
+ const setPage = useCallback((next) => {
88
+ nav.setSearchParams({
89
+ [key(screenId, "page")]: next <= 1 ? null : String(next),
90
+ });
91
+ }, [nav, screenId]);
92
+ const setPageSize = useCallback((next) => {
93
+ nav.setSearchParams({
94
+ [key(screenId, "size")]: next === undefined ? null : String(next),
95
+ [key(screenId, "page")]: null,
96
+ });
97
+ }, [nav, screenId]);
98
+ const setFilter = useCallback((field, values) => {
99
+ nav.setSearchParams({
100
+ [key(screenId, `f.${field}`)]: values.length === 0 ? null : values.join(","),
101
+ [key(screenId, "page")]: null,
102
+ });
103
+ }, [nav, screenId]);
104
+ const setDateRange = useCallback((field, next) => {
105
+ nav.setSearchParams({
106
+ [key(screenId, `f.${field}.from`)]: next.from === "" ? null : next.from,
107
+ [key(screenId, `f.${field}.to`)]: next.to === "" ? null : next.to,
108
+ [key(screenId, "page")]: null,
109
+ });
110
+ }, [nav, screenId]);
111
+ const clearFilters = useCallback(() => {
112
+ const updates = { [key(screenId, "page")]: null };
113
+ for (const field of Object.keys(filters))
114
+ updates[key(screenId, `f.${field}`)] = null;
115
+ nav.setSearchParams(updates);
116
+ }, [nav, screenId, filters]);
117
+ return {
118
+ sort,
119
+ q,
120
+ page,
121
+ pageSize,
122
+ filters,
123
+ setSort,
124
+ setQ,
125
+ setPage,
126
+ setPageSize,
127
+ setFilter,
128
+ clearFilters,
129
+ setDateRange,
130
+ };
131
+ }
@@ -0,0 +1,9 @@
1
+ import type { DispatcherError, WriteResult } from "@cosmicdrift/kumiko-headless";
2
+ export type UseMutationResult<TData> = {
3
+ readonly mutate: (payload: unknown) => Promise<WriteResult<TData>>;
4
+ readonly pending: boolean;
5
+ readonly error: DispatcherError | null;
6
+ readonly data: TData | null;
7
+ readonly reset: () => void;
8
+ };
9
+ export declare function useMutation<TData = unknown>(type: string): UseMutationResult<TData>;
@@ -0,0 +1,37 @@
1
+ import { useCallback, useRef, useState } from "react";
2
+ import { useDispatcher } from "../context/dispatcher-context.js";
3
+ export function useMutation(type) {
4
+ const dispatcher = useDispatcher();
5
+ const [pending, setPending] = useState(false);
6
+ const [error, setError] = useState(null);
7
+ const [data, setData] = useState(null);
8
+ // Sequence guard: `mutate` has no abort (unlike useQuery's GETs, a write
9
+ // already landed server-side and can't be cancelled). Two overlapping
10
+ // calls on one instance (e.g. two list-row actions) must not let the
11
+ // first-to-resolve clobber pending/error/data set by the second — only
12
+ // the most recently STARTED call's outcome may update shared state.
13
+ const sequence = useRef(0);
14
+ const mutate = useCallback(async (payload) => {
15
+ const callSeq = ++sequence.current;
16
+ setPending(true);
17
+ setError(null);
18
+ const result = await dispatcher.write(type, payload);
19
+ if (callSeq === sequence.current) {
20
+ if (result.isSuccess) {
21
+ setData(result.data);
22
+ }
23
+ else {
24
+ setError(result.error);
25
+ }
26
+ setPending(false);
27
+ }
28
+ return result;
29
+ }, [dispatcher, type]);
30
+ const reset = useCallback(() => {
31
+ sequence.current++; // invalidate any in-flight mutate's late update
32
+ setPending(false);
33
+ setError(null);
34
+ setData(null);
35
+ }, []);
36
+ return { mutate, pending, error, data, reset };
37
+ }
@@ -0,0 +1,14 @@
1
+ import type { DispatcherError } from "@cosmicdrift/kumiko-headless";
2
+ export type UseQueryResult<TData> = {
3
+ readonly data: TData | null;
4
+ readonly error: DispatcherError | null;
5
+ readonly loading: boolean;
6
+ readonly refetch: () => Promise<void>;
7
+ };
8
+ export type UseQueryOptions = {
9
+ readonly enabled?: boolean;
10
+ readonly live?: boolean;
11
+ readonly refetchIntervalMs?: number;
12
+ };
13
+ export declare function entityFromQueryType(type: string): string | undefined;
14
+ export declare function useQuery<TData = unknown>(type: string, payload: unknown, options?: UseQueryOptions): UseQueryResult<TData>;
@@ -0,0 +1,118 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { useDispatcher } from "../context/dispatcher-context.js";
3
+ import { useLiveEvents } from "../sse/live-events.js";
4
+ // Extract the entity-name from a standard Kumiko query type. Returns
5
+ // undefined for non-conforming types so the live-mode silently skips
6
+ // them instead of subscribing to a channel no event will ever match.
7
+ export function entityFromQueryType(type) {
8
+ // Expected shape: "<feature>:query:<entity>:<verb>"
9
+ const parts = type.split(":");
10
+ if (parts.length !== 4)
11
+ return undefined;
12
+ if (parts[1] !== "query")
13
+ return undefined;
14
+ return parts[2];
15
+ }
16
+ export function useQuery(type, payload, options = {}) {
17
+ const dispatcher = useDispatcher();
18
+ const { enabled = true, live = false, refetchIntervalMs } = options;
19
+ const [data, setData] = useState(null);
20
+ const [error, setError] = useState(null);
21
+ const [loading, setLoading] = useState(enabled);
22
+ // Track the AbortController for the most recent fetch so a newer
23
+ // call can cancel the older one. Ref, not state, because the
24
+ // controller identity shouldn't trigger a re-render.
25
+ const activeCtrl = useRef(null);
26
+ // A tick that aborted a running fetch would starve slow queries, so
27
+ // ticks consult this flag instead.
28
+ const fetchInFlight = useRef(false);
29
+ // Serialize payload so object-identity-changes across renders (the
30
+ // caller building a fresh `{}` every render) don't loop. Strings are
31
+ // stable by reference in React's dep-array check.
32
+ const payloadKey = JSON.stringify(payload);
33
+ // `dispatcher` is stable (context identity); `type` + `payloadKey`
34
+ // are the meaningful re-run triggers. `payload` itself is intentionally
35
+ // not in deps — it's serialized into payloadKey above.
36
+ // biome-ignore lint/correctness/useExhaustiveDependencies: payload goes through payloadKey
37
+ const runFetch = useCallback(async (background) => {
38
+ // Abort whatever's in flight. observer on Safari 17+ handles this
39
+ // without raising a fetch-throw in the previous caller — they
40
+ // already set the error path.
41
+ activeCtrl.current?.abort();
42
+ const ctrl = new AbortController();
43
+ activeCtrl.current = ctrl;
44
+ fetchInFlight.current = true;
45
+ if (!background)
46
+ setLoading(true);
47
+ let result;
48
+ try {
49
+ result = await dispatcher.query(type, payload, { signal: ctrl.signal });
50
+ }
51
+ finally {
52
+ // A rejected fetch must not leave polling stuck; a superseded one must
53
+ // not clear the newer fetch's flag.
54
+ if (activeCtrl.current === ctrl)
55
+ fetchInFlight.current = false;
56
+ }
57
+ // skip: a newer fetch already superseded this one, don't clobber its state
58
+ if (ctrl.signal.aborted)
59
+ return;
60
+ if (result.isSuccess) {
61
+ setData(result.data);
62
+ setError(null);
63
+ }
64
+ else {
65
+ // A cancelled request comes back with code "aborted" from the
66
+ // skip: aborted request, a newer run already replaces this result
67
+ if (result.error.code === "aborted")
68
+ return;
69
+ setError(result.error);
70
+ }
71
+ setLoading(false);
72
+ }, [dispatcher, type, payloadKey]);
73
+ const run = useCallback(() => runFetch(false), [runFetch]);
74
+ useEffect(() => {
75
+ if (!enabled) {
76
+ setLoading(false);
77
+ // skip: query disabled, loading state already cleared above
78
+ return;
79
+ }
80
+ void run();
81
+ return () => {
82
+ activeCtrl.current?.abort();
83
+ fetchInFlight.current = false;
84
+ };
85
+ }, [enabled, run]);
86
+ useEffect(() => {
87
+ // skip: polling off or disabled query
88
+ if (!enabled || refetchIntervalMs === undefined)
89
+ return;
90
+ // skip: non-positive or non-finite interval disables polling
91
+ if (!Number.isFinite(refetchIntervalMs) || refetchIntervalMs <= 0)
92
+ return;
93
+ const timer = setInterval(() => {
94
+ // skip: a fetch is still running, the next tick retries
95
+ if (fetchInFlight.current)
96
+ return;
97
+ void runFetch(true);
98
+ }, refetchIntervalMs);
99
+ return () => clearInterval(timer);
100
+ }, [enabled, refetchIntervalMs, runFetch]);
101
+ // Live-mode: auf SSE-Events für die Query-Entity hören und refetchen.
102
+ // Separater Effect, damit eine Änderung an `live` oder `type` das
103
+ // Subscription-Lifecycle genau einmal durchwalzt.
104
+ const subscribeLive = useLiveEvents();
105
+ useEffect(() => {
106
+ // skip: live mode or query disabled, no SSE subscription needed
107
+ if (!live || !enabled)
108
+ return;
109
+ const entity = entityFromQueryType(type);
110
+ // skip: query type has no mapped entity, nothing to subscribe to
111
+ if (entity === undefined)
112
+ return;
113
+ return subscribeLive(entity, () => {
114
+ void run();
115
+ });
116
+ }, [live, enabled, type, run, subscribeLive]);
117
+ return { data, error, loading, refetch: run };
118
+ }
@@ -0,0 +1,14 @@
1
+ export type ReferenceLookupMap = ReadonlyMap<string, string>;
2
+ /** Where a reference target's label rows come from: the central override for
3
+ * that `feature:entity` if one exists, else the entity-convention list
4
+ * handler plus the caller's own label field. */
5
+ export declare function referenceLookupSource(refFeature: string, refEntity: string, labelField: string): {
6
+ readonly queryQn: string;
7
+ readonly labelKey: string;
8
+ };
9
+ /** Empty while the query loads (or fails), so callers fall back to the raw id.
10
+ * `featureName` is the reference's target feature, not the screen's. */
11
+ export declare function useReferenceLookup(featureName: string, refEntity: string, labelField: string): {
12
+ readonly map: ReferenceLookupMap;
13
+ readonly loading: boolean;
14
+ };