@cosmicdrift/kumiko-renderer 0.328.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 (259) 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 -46
  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 -41
  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/{src/inside-drawer.ts → dist/inside-drawer.js} +2 -5
  119. package/dist/page-header-slot.d.ts +2 -0
  120. package/{src/page-header-slot.ts → dist/page-header-slot.js} +2 -5
  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/__tests__/error-i18n-defaults.test.ts +0 -82
  135. package/src/__tests__/format-when.test.ts +0 -12
  136. package/src/__tests__/i18n.test.tsx +0 -283
  137. package/src/__tests__/merge-search-params-into-initial.test.ts +0 -525
  138. package/src/__tests__/qn.test.ts +0 -83
  139. package/src/__tests__/share-links.test.ts +0 -26
  140. package/src/__tests__/sort-by-accessor.test.ts +0 -48
  141. package/src/__tests__/stringify-nav-params.test.ts +0 -33
  142. package/src/__tests__/use-list-url-state.test.tsx +0 -250
  143. package/src/app/__tests__/action-form-field-labels.test.tsx +0 -92
  144. package/src/app/__tests__/action-form-shim.test.ts +0 -137
  145. package/src/app/__tests__/build-list-query-payload.test.ts +0 -107
  146. package/src/app/__tests__/config-edit-shim.test.ts +0 -57
  147. package/src/app/__tests__/content-editors.test.tsx +0 -128
  148. package/src/app/__tests__/content-preview.test.tsx +0 -184
  149. package/src/app/__tests__/entity-edit-create-search-params.test.tsx +0 -236
  150. package/src/app/__tests__/entity-edit-fill-height.test.tsx +0 -118
  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 -128
  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 -3801
  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 -86
  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 -640
  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 -1733
  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 -88
  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 -491
  228. package/src/components/render-edit-action-button.tsx +0 -109
  229. package/src/components/render-edit-logic.ts +0 -85
  230. package/src/components/render-edit-types.ts +0 -226
  231. package/src/components/render-edit.tsx +0 -1871
  232. package/src/components/render-field.tsx +0 -951
  233. package/src/components/render-list.tsx +0 -593
  234. package/src/components/write-form-section.tsx +0 -198
  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-query.test.tsx +0 -149
  241. package/src/hooks/__tests__/use-stream-handler.test.tsx +0 -107
  242. package/src/hooks/use-ai-text.ts +0 -172
  243. package/src/hooks/use-disclosure.ts +0 -20
  244. package/src/hooks/use-form.ts +0 -78
  245. package/src/hooks/use-list-url-state.ts +0 -207
  246. package/src/hooks/use-mutation.ts +0 -61
  247. package/src/hooks/use-query.ts +0 -166
  248. package/src/hooks/use-reference-lookup.ts +0 -76
  249. package/src/hooks/use-store.ts +0 -48
  250. package/src/hooks/use-stream-handler.ts +0 -134
  251. package/src/i18n-defaults.ts +0 -240
  252. package/src/i18n.tsx +0 -286
  253. package/src/index.ts +0 -279
  254. package/src/primitives.tsx +0 -1567
  255. package/src/screen-fills-height.ts +0 -3
  256. package/src/share-links.ts +0 -12
  257. package/src/sort-by-accessor.ts +0 -20
  258. package/src/sse/live-events.tsx +0 -56
  259. package/src/tokens.tsx +0 -170
@@ -1,149 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, jest, test } from "bun:test";
2
- import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
3
- import { act, renderHook } from "@testing-library/react";
4
- import type { ReactNode } from "react";
5
- import { DispatcherProvider } from "../../context/dispatcher-context";
6
- import { useQuery } from "../use-query";
7
-
8
- type QueryFn = Dispatcher["query"];
9
-
10
- function makeDispatcher(query: QueryFn): Dispatcher {
11
- return {
12
- write: (async () => ({ isSuccess: true, data: {} })) as unknown as Dispatcher["write"],
13
- query,
14
- batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
15
- statusStore: {
16
- getState: () => "online",
17
- subscribe: () => () => {},
18
- } as unknown as Dispatcher["statusStore"],
19
- async *stream() {},
20
- pendingWrites: () => [],
21
- pendingFiles: () => [],
22
- };
23
- }
24
-
25
- function wrapperFor(dispatcher: Dispatcher) {
26
- return ({ children }: { readonly children: ReactNode }) => (
27
- <DispatcherProvider dispatcher={dispatcher}>{children}</DispatcherProvider>
28
- );
29
- }
30
-
31
- function countingDispatcher(): { dispatcher: Dispatcher; calls: () => number } {
32
- let calls = 0;
33
- const query = (async () => {
34
- calls += 1;
35
- return { isSuccess: true, data: { n: calls } };
36
- }) as unknown as QueryFn;
37
- return { dispatcher: makeDispatcher(query), calls: () => calls };
38
- }
39
-
40
- async function advance(ms: number): Promise<void> {
41
- await act(async () => {
42
- jest.advanceTimersByTime(ms);
43
- });
44
- }
45
-
46
- const QUERY_TYPE = "f:query:x:list";
47
-
48
- describe("useQuery refetchIntervalMs", () => {
49
- beforeEach(() => {
50
- jest.useFakeTimers();
51
- });
52
- afterEach(() => {
53
- jest.useRealTimers();
54
- });
55
-
56
- test("re-runs the query on every interval tick", async () => {
57
- const { dispatcher, calls } = countingDispatcher();
58
- const { result } = renderHook(
59
- () => useQuery<{ n: number }>(QUERY_TYPE, {}, { refetchIntervalMs: 1000 }),
60
- {
61
- wrapper: wrapperFor(dispatcher),
62
- },
63
- );
64
- await advance(0);
65
- expect(calls()).toBe(1);
66
-
67
- await advance(1000);
68
- expect(calls()).toBe(2);
69
- await advance(1000);
70
- expect(calls()).toBe(3);
71
- expect(result.current.data).toEqual({ n: 3 });
72
- });
73
-
74
- test("unmount stops the ticks", async () => {
75
- const { dispatcher, calls } = countingDispatcher();
76
- const { unmount } = renderHook(() => useQuery(QUERY_TYPE, {}, { refetchIntervalMs: 1000 }), {
77
- wrapper: wrapperFor(dispatcher),
78
- });
79
- await advance(1000);
80
- const before = calls();
81
- unmount();
82
- await advance(5000);
83
- expect(calls()).toBe(before);
84
- });
85
-
86
- test("enabled: false never ticks", async () => {
87
- const { dispatcher, calls } = countingDispatcher();
88
- renderHook(() => useQuery(QUERY_TYPE, {}, { enabled: false, refetchIntervalMs: 1000 }), {
89
- wrapper: wrapperFor(dispatcher),
90
- });
91
- await advance(5000);
92
- expect(calls()).toBe(0);
93
- });
94
-
95
- test("a tick while a fetch is in flight is skipped instead of aborting it", async () => {
96
- let calls = 0;
97
- let release: (() => void) | undefined;
98
- const query = (async (_type: string, _payload: unknown, opts?: { signal?: AbortSignal }) => {
99
- calls += 1;
100
- await new Promise<void>((resolve) => {
101
- release = resolve;
102
- });
103
- return opts?.signal?.aborted
104
- ? { isSuccess: false, error: { code: "aborted", message: "aborted", i18nKey: "x" } }
105
- : { isSuccess: true, data: { done: true } };
106
- }) as unknown as QueryFn;
107
- const { result } = renderHook(
108
- () => useQuery<{ done: boolean }>(QUERY_TYPE, {}, { refetchIntervalMs: 1000 }),
109
- {
110
- wrapper: wrapperFor(makeDispatcher(query)),
111
- },
112
- );
113
- await advance(0);
114
- expect(calls).toBe(1);
115
-
116
- await advance(3000);
117
- expect(calls).toBe(1);
118
-
119
- await act(async () => {
120
- release?.();
121
- });
122
- expect(result.current.data).toEqual({ done: true });
123
-
124
- await advance(1000);
125
- expect(calls).toBe(2);
126
- });
127
-
128
- test("background ticks do not flip loading to true", async () => {
129
- const { dispatcher, calls } = countingDispatcher();
130
- const seenLoading: boolean[] = [];
131
- const { result } = renderHook(
132
- () => {
133
- const r = useQuery<{ n: number }>(QUERY_TYPE, {}, { refetchIntervalMs: 1000 });
134
- seenLoading.push(r.loading);
135
- return r;
136
- },
137
- { wrapper: wrapperFor(dispatcher) },
138
- );
139
- await advance(0);
140
- expect(result.current.loading).toBe(false);
141
- seenLoading.length = 0;
142
-
143
- await advance(1000);
144
- await advance(1000);
145
- expect(calls()).toBe(3);
146
- expect(result.current.data).toEqual({ n: 3 });
147
- expect(seenLoading.every((l) => l === false)).toBe(true);
148
- });
149
- });
@@ -1,107 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import type { Dispatcher, DispatcherError } from "@cosmicdrift/kumiko-headless";
3
- import { act, renderHook, waitFor } from "@testing-library/react";
4
- import type { ReactNode } from "react";
5
- import { DispatcherProvider } from "../../context/dispatcher-context";
6
- import { useStreamHandler } from "../use-stream-handler";
7
-
8
- function makeDispatcher(streamImpl: unknown): Dispatcher {
9
- return {
10
- write: (async () => ({ isSuccess: true, data: {} })) as unknown as Dispatcher["write"],
11
- query: (async () => ({ isSuccess: true, data: {} })) as unknown as Dispatcher["query"],
12
- batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
13
- stream: streamImpl as Dispatcher["stream"],
14
- statusStore: {
15
- getState: () => "online",
16
- subscribe: () => () => {},
17
- } as unknown as Dispatcher["statusStore"],
18
- pendingWrites: () => [],
19
- pendingFiles: () => [],
20
- };
21
- }
22
-
23
- function wrapperFor(dispatcher: Dispatcher) {
24
- return ({ children }: { readonly children: ReactNode }) => (
25
- <DispatcherProvider dispatcher={dispatcher}>{children}</DispatcherProvider>
26
- );
27
- }
28
-
29
- describe("useStreamHandler", () => {
30
- test("start accumulates chunks then status=done", async () => {
31
- const dispatcher = makeDispatcher(async function* () {
32
- yield { i: 0 };
33
- yield { i: 1 };
34
- });
35
-
36
- const { result } = renderHook(() => useStreamHandler<{ i: number }>("f:stream:x:tail"), {
37
- wrapper: wrapperFor(dispatcher),
38
- });
39
-
40
- await act(async () => {
41
- await result.current.start({ count: 2 });
42
- });
43
-
44
- await waitFor(() => expect(result.current.status).toBe("done"));
45
- expect(result.current.chunks).toEqual([{ i: 0 }, { i: 1 }]);
46
- expect(result.current.error).toBeNull();
47
- });
48
-
49
- test("stream error sets status=error and error envelope", async () => {
50
- const err: DispatcherError = {
51
- code: "access_denied",
52
- httpStatus: 403,
53
- i18nKey: "errors.access",
54
- message: "denied",
55
- };
56
- const dispatcher = makeDispatcher(async function* () {
57
- yield* []; // satisfy generator shape; error is the only outcome
58
- throw err;
59
- });
60
-
61
- const { result } = renderHook(() => useStreamHandler("f:stream:x:tail"), {
62
- wrapper: wrapperFor(dispatcher),
63
- });
64
-
65
- await act(async () => {
66
- await result.current.start();
67
- });
68
-
69
- await waitFor(() => expect(result.current.status).toBe("error"));
70
- expect(result.current.error?.code).toBe("access_denied");
71
- });
72
-
73
- test("abort during stream resets status to idle (no late done)", async () => {
74
- let release!: () => void;
75
- const gate = new Promise<void>((r) => {
76
- release = r;
77
- });
78
- const dispatcher = makeDispatcher(async function* (
79
- _t: string,
80
- _p: unknown,
81
- opts?: { signal?: AbortSignal },
82
- ) {
83
- yield { i: 0 };
84
- await gate;
85
- if (opts?.signal?.aborted) {
86
- const e = { code: "aborted", httpStatus: 0, i18nKey: "x", message: "aborted" };
87
- throw e;
88
- }
89
- yield { i: 1 };
90
- });
91
-
92
- const { result } = renderHook(() => useStreamHandler<{ i: number }>("f:stream:x:tail"), {
93
- wrapper: wrapperFor(dispatcher),
94
- });
95
-
96
- let started!: Promise<void>;
97
- act(() => {
98
- started = result.current.start();
99
- });
100
- await waitFor(() => expect(result.current.chunks).toEqual([{ i: 0 }]));
101
- act(() => result.current.abort());
102
- release();
103
- await started;
104
- expect(result.current.chunks).toEqual([{ i: 0 }]);
105
- expect(result.current.status).toBe("idle");
106
- });
107
- });
@@ -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
- }