@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,404 +0,0 @@
1
- // fw#2548: secretMint mints a one-time secret from a write-handler's own
2
- // success payload. This renders the real path (KumikoScreen → SecretMintBody
3
- // → RenderEdit) under a stub dispatcher, proving: (1) the secret is absent
4
- // before submit, (2) only the whitelisted reveal.fields survive the submit —
5
- // a stray "id" in the payload never leaks, (3) confirming clears the reveal
6
- // from the DOM, (4) no query call ever runs that could reload the secret.
7
-
8
- import { describe, expect, test } from "bun:test";
9
- import type {
10
- SecretMintScreenDefinition,
11
- TextFieldDef,
12
- } from "@cosmicdrift/kumiko-framework/ui-types";
13
- import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
14
- import { fireEvent, render, screen as rtlScreen, waitFor } from "@testing-library/react";
15
- import type { ComponentType, ReactNode } from "react";
16
- import { DispatcherProvider } from "../../context/dispatcher-context";
17
- import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
18
- import { kumikoDefaultTranslations } from "../../i18n-defaults";
19
- import {
20
- type BannerProps,
21
- type CorePrimitives,
22
- PrimitivesProvider,
23
- type SecretRevealProps,
24
- type SectionProps,
25
- type TextProps,
26
- } from "../../primitives";
27
- import type { FeatureSchema } from "../feature-schema";
28
- import { KumikoScreen } from "../kumiko-screen";
29
- import { NavProvider, type NavTarget } from "../nav";
30
-
31
- const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
32
- const noop = () => null;
33
-
34
- const testButton: ComponentType<{
35
- children?: ReactNode;
36
- onClick?: () => void;
37
- testId?: string;
38
- type?: "button" | "submit";
39
- disabled?: boolean;
40
- }> = ({ children, onClick, testId, type, disabled }) => (
41
- <button type={type ?? "button"} data-testid={testId} onClick={onClick} disabled={disabled}>
42
- {children}
43
- </button>
44
- );
45
-
46
- const testForm: ComponentType<{
47
- children?: ReactNode;
48
- actions?: ReactNode;
49
- secondaryActions?: ReactNode;
50
- onSubmit?: () => void;
51
- }> = ({ children, actions, secondaryActions, onSubmit }) => (
52
- <form
53
- onSubmit={(e) => {
54
- e.preventDefault();
55
- onSubmit?.();
56
- }}
57
- >
58
- {children}
59
- {secondaryActions}
60
- {actions}
61
- </form>
62
- );
63
-
64
- const testInput: ComponentType<{
65
- name?: string;
66
- value?: unknown;
67
- onChange?: (v: unknown) => void;
68
- }> = ({ name = "field", value, onChange }) => (
69
- <input
70
- aria-label={name}
71
- data-testid={`input-${name}`}
72
- value={typeof value === "string" ? value : ""}
73
- onChange={(e) => onChange?.(e.target.value)}
74
- />
75
- );
76
-
77
- const testSection: ComponentType<SectionProps> = ({ testId, children }) => (
78
- <div data-testid={testId}>{children}</div>
79
- );
80
- const testBanner: ComponentType<BannerProps> = ({ children, testId }) => (
81
- <div data-testid={testId}>{children}</div>
82
- );
83
- const testText: ComponentType<TextProps> = ({ children, testId }) => (
84
- <span data-testid={testId}>{children}</span>
85
- );
86
- // Renders every value's raw text plus a `data-qr` marker — lets tests assert
87
- // both DOM-leak absence (no query needed to find rendered text) and that
88
- // display: "qr" reaches the primitive as `qr: true`.
89
- const testSecretReveal: ComponentType<SecretRevealProps> = ({ values, testId }) => (
90
- <div data-testid={testId}>
91
- {values.map((v) => (
92
- <div key={v.label} data-testid={`secret-value-${v.label}`} data-qr={String(v.qr === true)}>
93
- {v.value}
94
- </div>
95
- ))}
96
- </div>
97
- );
98
-
99
- const testPrimitives: CorePrimitives = {
100
- Button: testButton,
101
- Banner: testBanner,
102
- Field: passChildren,
103
- Input: testInput,
104
- DataTable: noop,
105
- Form: testForm,
106
- Section: testSection,
107
- Card: passChildren,
108
- Grid: passChildren,
109
- GridCell: passChildren,
110
- Text: testText,
111
- Heading: passChildren,
112
- Dialog: noop,
113
- Modal: noop,
114
- Lightbox: noop,
115
- ConfigSourceBadge: noop,
116
- ConfigCascadeView: noop,
117
- Link: noop,
118
- SecretReveal: testSecretReveal,
119
- } as unknown as CorePrimitives;
120
-
121
- const mintScreen: SecretMintScreenDefinition = {
122
- id: "mint-token",
123
- type: "secretMint",
124
- handler: "shop:write:token:mint",
125
- fields: { label: { type: "text" } as TextFieldDef },
126
- layout: { sections: [{ title: "Mint", fields: ["label"] }] },
127
- reveal: { fields: [{ field: "token", label: "Token" }] },
128
- };
129
-
130
- function buildSchema(screen: SecretMintScreenDefinition): FeatureSchema {
131
- return {
132
- featureName: "shop",
133
- entities: {},
134
- screens: [screen],
135
- } as FeatureSchema;
136
- }
137
-
138
- function stubDispatcher(writeData: unknown): {
139
- dispatcher: Dispatcher;
140
- queryCalls: unknown[];
141
- } {
142
- const queryCalls: unknown[] = [];
143
- const dispatcher: Dispatcher = {
144
- write: (async () => ({ isSuccess: true, data: writeData })) as unknown as Dispatcher["write"],
145
- query: (async (type: string, payload: unknown) => {
146
- queryCalls.push({ type, payload });
147
- return { isSuccess: true, data: {} };
148
- }) as unknown as Dispatcher["query"],
149
- batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
150
- statusStore: {
151
- getState: () => "online",
152
- subscribe: () => () => {},
153
- } as unknown as Dispatcher["statusStore"],
154
- async *stream() {},
155
- pendingWrites: () => [],
156
- pendingFiles: () => [],
157
- };
158
- return { dispatcher, queryCalls };
159
- }
160
-
161
- // Keyed stub — one write-response per handler QN, and every write call
162
- // recorded with its full payload — for scenarios with more than one
163
- // write-handler in play (mint + confirm).
164
- function stubMultiWriteDispatcher(responses: Readonly<Record<string, unknown>>): {
165
- dispatcher: Dispatcher;
166
- writeCalls: Array<{ readonly command: string; readonly payload: unknown }>;
167
- } {
168
- const writeCalls: Array<{ readonly command: string; readonly payload: unknown }> = [];
169
- const dispatcher: Dispatcher = {
170
- write: (async (command: string, payload: unknown) => {
171
- writeCalls.push({ command, payload });
172
- return { isSuccess: true, data: responses[command] ?? {} };
173
- }) as unknown as Dispatcher["write"],
174
- query: (async () => ({ isSuccess: true, data: {} })) as unknown as Dispatcher["query"],
175
- batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
176
- statusStore: {
177
- getState: () => "online",
178
- subscribe: () => () => {},
179
- } as unknown as Dispatcher["statusStore"],
180
- async *stream() {},
181
- pendingWrites: () => [],
182
- pendingFiles: () => [],
183
- };
184
- return { dispatcher, writeCalls };
185
- }
186
-
187
- function renderMintScreen(
188
- dispatcher: Dispatcher,
189
- screen: SecretMintScreenDefinition = mintScreen,
190
- navigate: (target: NavTarget) => void = () => {},
191
- ) {
192
- const qn = `shop:screen:${screen.id}`;
193
- return render(
194
- <LocaleProvider
195
- resolver={createStaticLocaleResolver({ locale: "en-US" })}
196
- fallbackBundles={[kumikoDefaultTranslations]}
197
- >
198
- <DispatcherProvider dispatcher={dispatcher}>
199
- <NavProvider
200
- value={{
201
- route: { screenId: qn },
202
- navigate,
203
- replace: () => {},
204
- hrefFor: () => "",
205
- searchParams: {},
206
- setSearchParams: () => {},
207
- }}
208
- >
209
- <PrimitivesProvider value={testPrimitives}>
210
- <KumikoScreen schema={buildSchema(screen)} qn={qn} />
211
- </PrimitivesProvider>
212
- </NavProvider>
213
- </DispatcherProvider>
214
- </LocaleProvider>,
215
- );
216
- }
217
-
218
- describe("SecretMintBody (fw#2548)", () => {
219
- test("before submit: the secret is not in the DOM", () => {
220
- const { dispatcher } = stubDispatcher({ token: "kpat_secret", id: "x" });
221
- renderMintScreen(dispatcher);
222
- expect(rtlScreen.queryByText("kpat_secret")).toBeNull();
223
- });
224
-
225
- test("after a successful submit: only the whitelisted reveal field appears — not the payload's other fields", async () => {
226
- const { dispatcher } = stubDispatcher({ token: "kpat_secret", id: "x" });
227
- renderMintScreen(dispatcher);
228
-
229
- fireEvent.change(rtlScreen.getByLabelText(/label/i), { target: { value: "My token" } });
230
- fireEvent.click(rtlScreen.getByTestId("render-edit-submit"));
231
-
232
- await waitFor(() => expect(rtlScreen.queryByText("kpat_secret")).not.toBeNull());
233
- expect(rtlScreen.queryByText("x")).toBeNull();
234
- });
235
-
236
- test("confirming the reveal clears the secret from the DOM", async () => {
237
- const { dispatcher } = stubDispatcher({ token: "kpat_secret", id: "x" });
238
- renderMintScreen(dispatcher);
239
-
240
- fireEvent.change(rtlScreen.getByLabelText(/label/i), { target: { value: "My token" } });
241
- fireEvent.click(rtlScreen.getByTestId("render-edit-submit"));
242
- await waitFor(() => expect(rtlScreen.queryByText("kpat_secret")).not.toBeNull());
243
-
244
- fireEvent.click(rtlScreen.getByTestId("kumiko-screen-secret-mint-confirm"));
245
- await waitFor(() => expect(rtlScreen.queryByText("kpat_secret")).toBeNull());
246
- });
247
-
248
- test("the secret is never fetched via a query — it only ever comes from the write result", async () => {
249
- const { dispatcher, queryCalls } = stubDispatcher({ token: "kpat_secret", id: "x" });
250
- renderMintScreen(dispatcher);
251
-
252
- fireEvent.change(rtlScreen.getByLabelText(/label/i), { target: { value: "My token" } });
253
- fireEvent.click(rtlScreen.getByTestId("render-edit-submit"));
254
- await waitFor(() => expect(rtlScreen.queryByText("kpat_secret")).not.toBeNull());
255
-
256
- expect(queryCalls.length).toBe(0);
257
- });
258
-
259
- test("acknowledging without a confirm step and without a redirect shows the done banner", async () => {
260
- const { dispatcher } = stubDispatcher({ token: "kpat_secret", id: "x" });
261
- renderMintScreen(dispatcher);
262
-
263
- fireEvent.change(rtlScreen.getByLabelText(/label/i), { target: { value: "My token" } });
264
- fireEvent.click(rtlScreen.getByTestId("render-edit-submit"));
265
- await waitFor(() => expect(rtlScreen.queryByText("kpat_secret")).not.toBeNull());
266
-
267
- fireEvent.click(rtlScreen.getByTestId("kumiko-screen-secret-mint-confirm"));
268
- await waitFor(() =>
269
- expect(rtlScreen.queryByTestId("kumiko-screen-secret-mint-done")).not.toBeNull(),
270
- );
271
- });
272
-
273
- test("acknowledging with a redirect navigates instead of showing the done banner", async () => {
274
- const { dispatcher } = stubDispatcher({ token: "kpat_secret", id: "x" });
275
- const navigateCalls: NavTarget[] = [];
276
- renderMintScreen(dispatcher, { ...mintScreen, redirect: "after-mint" }, (target) =>
277
- navigateCalls.push(target),
278
- );
279
-
280
- fireEvent.change(rtlScreen.getByLabelText(/label/i), { target: { value: "My token" } });
281
- fireEvent.click(rtlScreen.getByTestId("render-edit-submit"));
282
- await waitFor(() => expect(rtlScreen.queryByText("kpat_secret")).not.toBeNull());
283
-
284
- fireEvent.click(rtlScreen.getByTestId("kumiko-screen-secret-mint-confirm"));
285
- await waitFor(() => expect(navigateCalls.length).toBe(1));
286
- expect(navigateCalls[0]).toEqual({ screenId: "after-mint" });
287
- expect(rtlScreen.queryByTestId("kumiko-screen-secret-mint-done")).toBeNull();
288
- });
289
-
290
- test("a reveal field with display 'qr' reaches the SecretReveal primitive with qr: true", async () => {
291
- const qrScreen: SecretMintScreenDefinition = {
292
- ...mintScreen,
293
- reveal: { fields: [{ field: "token", label: "Token", display: "qr" }] },
294
- };
295
- const { dispatcher } = stubDispatcher({ token: "otpauth://totp/x?secret=ABC", id: "x" });
296
- renderMintScreen(dispatcher, qrScreen);
297
-
298
- fireEvent.change(rtlScreen.getByLabelText(/label/i), { target: { value: "My token" } });
299
- fireEvent.click(rtlScreen.getByTestId("render-edit-submit"));
300
-
301
- await waitFor(() =>
302
- expect(rtlScreen.getByTestId("secret-value-Token").getAttribute("data-qr")).toBe("true"),
303
- );
304
- });
305
-
306
- test("an input-less secretMint (no fields) has an active submit button and dispatches the mint handler on click", async () => {
307
- const inputLessScreen: SecretMintScreenDefinition = {
308
- id: "mint-token",
309
- type: "secretMint",
310
- handler: "shop:write:token:mint",
311
- fields: {},
312
- layout: { sections: [] },
313
- reveal: { fields: [{ field: "token", label: "Token" }] },
314
- };
315
- const { dispatcher, writeCalls } = stubMultiWriteDispatcher({
316
- "shop:write:token:mint": { token: "kpat_secret" },
317
- });
318
- renderMintScreen(dispatcher, inputLessScreen);
319
-
320
- const submit = rtlScreen.getByTestId("render-edit-submit") as HTMLButtonElement;
321
- expect(submit.disabled).toBe(false);
322
- fireEvent.click(submit);
323
-
324
- await waitFor(() => expect(rtlScreen.queryByText("kpat_secret")).not.toBeNull());
325
- expect(writeCalls.map((c) => c.command)).toEqual(["shop:write:token:mint"]);
326
- });
327
- });
328
-
329
- describe("SecretMintBody confirm step (fw#2838)", () => {
330
- const mintScreenWithConfirm: SecretMintScreenDefinition = {
331
- ...mintScreen,
332
- confirm: {
333
- handler: "shop:write:token:confirm",
334
- fields: { code: { type: "text" } as TextFieldDef },
335
- layout: { sections: [{ title: "Confirm", fields: ["code"] }] },
336
- carry: ["setupToken"],
337
- },
338
- };
339
-
340
- // The confirm form mounts from an async post-mint update, so its form-store subscription can
341
- // still be pending when the change fires — clicking before that subscription re-renders hits a disabled submit.
342
- async function enterConfirmCodeAndSubmit(code: string): Promise<void> {
343
- fireEvent.change(rtlScreen.getByLabelText(/code/i), { target: { value: code } });
344
- await waitFor(() =>
345
- expect(rtlScreen.getByTestId<HTMLButtonElement>("render-edit-submit").disabled).toBe(false),
346
- );
347
- fireEvent.click(rtlScreen.getByTestId("render-edit-submit"));
348
- }
349
-
350
- test("renders the confirm form instead of the acknowledge button, and submits the confirm form's own values merged with the carried mint-payload field", async () => {
351
- const { dispatcher, writeCalls } = stubMultiWriteDispatcher({
352
- "shop:write:token:mint": { token: "kpat_secret", id: "x", setupToken: "stok_123" },
353
- "shop:write:token:confirm": {},
354
- });
355
- renderMintScreen(dispatcher, mintScreenWithConfirm);
356
-
357
- fireEvent.change(rtlScreen.getByLabelText(/label/i), { target: { value: "My token" } });
358
- fireEvent.click(rtlScreen.getByTestId("render-edit-submit"));
359
- await waitFor(() => expect(rtlScreen.queryByText("kpat_secret")).not.toBeNull());
360
-
361
- // No bare acknowledge button — the confirm form takes its place.
362
- expect(rtlScreen.queryByTestId("kumiko-screen-secret-mint-confirm")).toBeNull();
363
-
364
- await enterConfirmCodeAndSubmit("123456");
365
-
366
- await waitFor(() =>
367
- expect(writeCalls.some((c) => c.command === "shop:write:token:confirm")).toBe(true),
368
- );
369
- const confirmCall = writeCalls.find((c) => c.command === "shop:write:token:confirm");
370
- expect(confirmCall?.payload).toEqual({ code: "123456", setupToken: "stok_123" });
371
- });
372
-
373
- test("a carried field that is not in reveal.fields never appears in the DOM", async () => {
374
- const { dispatcher } = stubMultiWriteDispatcher({
375
- "shop:write:token:mint": { token: "kpat_secret", id: "x", setupToken: "stok_123" },
376
- });
377
- renderMintScreen(dispatcher, mintScreenWithConfirm);
378
-
379
- fireEvent.change(rtlScreen.getByLabelText(/label/i), { target: { value: "My token" } });
380
- fireEvent.click(rtlScreen.getByTestId("render-edit-submit"));
381
- await waitFor(() => expect(rtlScreen.queryByText("kpat_secret")).not.toBeNull());
382
-
383
- expect(rtlScreen.queryByText("stok_123")).toBeNull();
384
- });
385
-
386
- test("without a redirect, a successful confirm shows the done banner", async () => {
387
- const { dispatcher } = stubMultiWriteDispatcher({
388
- "shop:write:token:mint": { token: "kpat_secret", id: "x", setupToken: "stok_123" },
389
- "shop:write:token:confirm": {},
390
- });
391
- renderMintScreen(dispatcher, mintScreenWithConfirm);
392
-
393
- fireEvent.change(rtlScreen.getByLabelText(/label/i), { target: { value: "My token" } });
394
- fireEvent.click(rtlScreen.getByTestId("render-edit-submit"));
395
- await waitFor(() => expect(rtlScreen.queryByText("kpat_secret")).not.toBeNull());
396
-
397
- await enterConfirmCodeAndSubmit("123456");
398
-
399
- await waitFor(() =>
400
- expect(rtlScreen.queryByTestId("kumiko-screen-secret-mint-done")).not.toBeNull(),
401
- );
402
- expect(rtlScreen.queryByText("kpat_secret")).toBeNull();
403
- });
404
- });
@@ -1,70 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { fireEvent, render, screen } from "@testing-library/react";
3
- import type { ComponentType, ReactNode } from "react";
4
- import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
5
- import { type ButtonProps, type CorePrimitives, PrimitivesProvider } from "../../primitives";
6
- import { VariableChips } from "../variable-chips";
7
-
8
- const captureButton: ComponentType<ButtonProps> = ({ children, onClick, ariaLabel, disabled }) => (
9
- <button type="button" aria-label={ariaLabel} onClick={() => void onClick?.()} disabled={disabled}>
10
- {children}
11
- </button>
12
- );
13
-
14
- const noop = (): ReactNode => null;
15
- const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
16
-
17
- const testPrimitives: CorePrimitives = {
18
- Button: captureButton,
19
- Banner: passChildren,
20
- Field: passChildren,
21
- Input: noop,
22
- DataTable: noop,
23
- Form: passChildren,
24
- Section: passChildren,
25
- Card: passChildren,
26
- Grid: passChildren,
27
- GridCell: passChildren,
28
- Text: passChildren,
29
- Heading: noop,
30
- Dialog: noop,
31
- Modal: noop,
32
- Lightbox: noop,
33
- ConfigSourceBadge: noop,
34
- ConfigCascadeView: noop,
35
- Link: noop,
36
- };
37
-
38
- function Wrapper({ children }: { readonly children: ReactNode }): ReactNode {
39
- return (
40
- <LocaleProvider resolver={createStaticLocaleResolver({ locale: "en" })}>
41
- <PrimitivesProvider value={testPrimitives}>{children}</PrimitivesProvider>
42
- </LocaleProvider>
43
- );
44
- }
45
-
46
- describe("VariableChips", () => {
47
- test("renders no chips when there are no variables", () => {
48
- render(<VariableChips variables={[]} onInsert={() => {}} />, { wrapper: Wrapper });
49
- expect(screen.queryAllByRole("button")).toHaveLength(0);
50
- });
51
-
52
- test("renders one chip per variable and reports a click by name", () => {
53
- const inserted: string[] = [];
54
- render(
55
- <VariableChips
56
- variables={["customerName", "orderId"]}
57
- onInsert={(name) => inserted.push(name)}
58
- />,
59
- { wrapper: Wrapper },
60
- );
61
- expect(screen.getAllByRole("button")).toHaveLength(2);
62
- fireEvent.click(screen.getByText("{{orderId}}"));
63
- expect(inserted).toEqual(["orderId"]);
64
- });
65
-
66
- test("disabled propagates to every chip", () => {
67
- render(<VariableChips variables={["a"]} onInsert={() => {}} disabled />, { wrapper: Wrapper });
68
- expect((screen.getByRole("button") as HTMLButtonElement).disabled).toBe(true);
69
- });
70
- });
@@ -1,25 +0,0 @@
1
- // Cross-feature schema access for components that need to resolve a
2
- // screen owned by a DIFFERENT feature than the one they're rendering
3
- // under — e.g. a reference field opening the target entity's create
4
- // screen (kumiko-framework#1681). `createKumikoApp` provides the full
5
- // `app.features` list; components elsewhere in the render tree default
6
- // to an empty list (no cross-feature schema known, e.g. outside
7
- // createKumikoApp or in isolated tests).
8
-
9
- import { createContext, type ReactNode, useContext } from "react";
10
- import type { FeatureSchema } from "./feature-schema";
11
-
12
- const AppFeaturesContext = createContext<readonly FeatureSchema[]>([]);
13
-
14
- export type AppFeaturesProviderProps = {
15
- readonly features: readonly FeatureSchema[];
16
- readonly children: ReactNode;
17
- };
18
-
19
- export function AppFeaturesProvider({ features, children }: AppFeaturesProviderProps): ReactNode {
20
- return <AppFeaturesContext.Provider value={features}>{children}</AppFeaturesContext.Provider>;
21
- }
22
-
23
- export function useAppFeatures(): readonly FeatureSchema[] {
24
- return useContext(AppFeaturesContext);
25
- }
@@ -1,68 +0,0 @@
1
- // Column-Renderer-Map: client-side Lookup für ListColumn-Spalten die im
2
- // Schema einen PlatformComponent-Renderer (`{ react: { __component: "Name" } }`)
3
- // statt einer String-Funktion angeben. createKumikoApp im renderer-web sammelt
4
- // alle clientFeatures.columnRenderers und mountet den Provider; der DataTable-
5
- // Cell-Renderer schlägt den Component über `useColumnRenderer(name)` nach.
6
- //
7
- // Analog zu CustomScreensProvider — der Renderer-Web-Bootstrap mounted beide
8
- // Provider an derselben Stelle. Schemas selbst bleiben serializable: der
9
- // Component wird nur per String-Key referenziert, die Map liegt allein
10
- // client-seitig.
11
-
12
- import { type ComponentType, createContext, type ReactNode, useContext } from "react";
13
-
14
- // Props die ein Column-Renderer-Component bekommt. `value` ist der
15
- // Cell-Value für die Spalte (rohes Field-Value aus der Row), `row` ist
16
- // die ganze Row als plain object — nice-to-have für Renderer die auf
17
- // andere Spalten zugreifen wollen (z.B. Status-Badge der den
18
- // Erstellungs-Zeitpunkt aus der Row mitnimmt). `column.field` ist der
19
- // Field-Name; hilfreich für generische Renderer (JSON-Pretty-Printer
20
- // oder Debug-Renderer die den Feldnamen anzeigen).
21
- export type ColumnRendererProps = {
22
- readonly value: unknown;
23
- readonly row: Readonly<Record<string, unknown>>;
24
- readonly column: {
25
- readonly field: string;
26
- };
27
- /** Set only when the caller (DataTable's `onCellChange`) wired an
28
- * editable path for this cell — undefined means read-only. A renderer
29
- * can branch its own markup (input vs. plain text) on presence alone. */
30
- readonly onChange?: (value: unknown) => void;
31
- };
32
-
33
- export type ColumnRendererComponent = ComponentType<ColumnRendererProps>;
34
-
35
- export type ColumnRenderersMap = Readonly<Record<string, ColumnRendererComponent>>;
36
-
37
- const ColumnRenderersContext = createContext<ColumnRenderersMap | undefined>(undefined);
38
-
39
- export type ColumnRenderersProviderProps = {
40
- readonly children: ReactNode;
41
- readonly value: ColumnRenderersMap;
42
- };
43
-
44
- export function ColumnRenderersProvider({
45
- children,
46
- value,
47
- }: ColumnRenderersProviderProps): ReactNode {
48
- return (
49
- <ColumnRenderersContext.Provider value={value}>{children}</ColumnRenderersContext.Provider>
50
- );
51
- }
52
-
53
- /** Schaut die Component für einen Renderer-Key nach. Returnt undefined
54
- * wenn `name` leer/undefined ist (Caller hat keinen __component-Renderer)
55
- * oder weder Provider gemounted noch der Key in der Map ist. Der Caller
56
- * (DataTable.renderCell) loggt dann eine Warnung und fällt auf den
57
- * Default-Type-Renderer zurück.
58
- *
59
- * `name` ist optional damit Caller den Hook unkonditional aufrufen
60
- * können — Rules-of-Hooks verbieten einen Function-Branch der den Hook
61
- * überspringt. So bleibt der Aufruf eine einzige Zeile, ohne dass der
62
- * Caller einen Stub-Key wie `""` reichen muss. */
63
- export function useColumnRenderer(name?: string): ColumnRendererComponent | undefined {
64
- const map = useContext(ColumnRenderersContext);
65
- if (name === undefined || name === "") return undefined;
66
- if (map === undefined) return undefined;
67
- return map[name];
68
- }
@@ -1,85 +0,0 @@
1
- // Content-Editor-Components-Map: client-side lookup by `contentFormat`
2
- // ("plain" | "rich" | "markdown") for r.contentCollection() entries. A collection
3
- // declares its contentFormat in the schema; the client resolves it to a
4
- // React component here. Same registry shape as columnRenderers/
5
- // extensionSectionComponents — String-key in the schema, Component in the
6
- // client bundle, last-wins merge in createKumikoApp.
7
- //
8
- // No entry registered for a format → TextareaContentEditor, so a missing
9
- // editor is never an empty panel.
10
-
11
- import type { ContentEditorFormat } from "@cosmicdrift/kumiko-types/nav";
12
- import { type ComponentType, createContext, type ReactNode, useContext } from "react";
13
- import { usePrimitives } from "../primitives";
14
-
15
- export type ContentEditorProps = {
16
- /** DOM id every registered editor must render onto its own focusable
17
- * root element. Callers that wrap an editor in a `Field` (label +
18
- * htmlFor) pass a stable id here so the label stays associated with
19
- * whatever element actually renders — a fixed constant would break as
20
- * soon as a registered editor swaps in for the textarea fallback. */
21
- readonly id: string;
22
- readonly value: string;
23
- readonly onChange: (value: string) => void;
24
- /** Variable names insertable as chips (from the collection's
25
- * variableSchema). Empty until the variable-chip step wires it up —
26
- * editors must accept it now, that's the point of fixing the signature
27
- * here. */
28
- readonly variables: readonly string[];
29
- readonly readOnly: boolean;
30
- };
31
-
32
- export type ContentEditorComponent = ComponentType<ContentEditorProps>;
33
-
34
- /** Default id value for callers that don't generate their own (e.g. a
35
- * standalone TextareaContentEditor render outside a multi-editor page).
36
- * Registered editors don't apply this automatically — every caller passes
37
- * its own id via ContentEditorProps.id. */
38
- export const CONTENT_EDITOR_ELEMENT_ID = "content-editor-textarea";
39
-
40
- export type ContentEditorsMap = Partial<Record<ContentEditorFormat, ContentEditorComponent>>;
41
-
42
- const ContentEditorsContext = createContext<ContentEditorsMap>({});
43
-
44
- export type ContentEditorsProviderProps = {
45
- readonly children: ReactNode;
46
- readonly value: ContentEditorsMap;
47
- };
48
-
49
- export function ContentEditorsProvider({
50
- children,
51
- value,
52
- }: ContentEditorsProviderProps): ReactNode {
53
- return <ContentEditorsContext.Provider value={value}>{children}</ContentEditorsContext.Provider>;
54
- }
55
-
56
- /** Plain-textarea fallback for a contentFormat with no registered editor.
57
- * Uses the primitives Input like every other form field, so it renders on
58
- * every platform without requiring the platform's DOM/native equivalent. */
59
- export function TextareaContentEditor({
60
- id,
61
- value,
62
- onChange,
63
- readOnly,
64
- }: ContentEditorProps): ReactNode {
65
- const { Input } = usePrimitives();
66
- return (
67
- <Input
68
- kind="textarea"
69
- id={id}
70
- name={CONTENT_EDITOR_ELEMENT_ID}
71
- value={value}
72
- onChange={onChange}
73
- readOnly={readOnly}
74
- rows={14}
75
- />
76
- );
77
- }
78
-
79
- /** Resolves the editor for a contentFormat, falling back to the plain
80
- * textarea when no clientFeature registered one. `contentFormat`
81
- * undefined (collection didn't declare one) behaves like "plain". */
82
- export function useContentEditor(contentFormat?: ContentEditorFormat): ContentEditorComponent {
83
- const map = useContext(ContentEditorsContext);
84
- return map[contentFormat ?? "plain"] ?? TextareaContentEditor;
85
- }