@stapel/forms-react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/MODULE.md +172 -0
  3. package/README.md +207 -0
  4. package/dist/api/export.d.ts +66 -0
  5. package/dist/api/export.d.ts.map +1 -0
  6. package/dist/api/export.js +138 -0
  7. package/dist/api/export.js.map +1 -0
  8. package/dist/api/extensions.d.ts +8 -0
  9. package/dist/api/extensions.d.ts.map +1 -0
  10. package/dist/api/extensions.js +8 -0
  11. package/dist/api/extensions.js.map +1 -0
  12. package/dist/api/formsApi.d.ts +113 -0
  13. package/dist/api/formsApi.d.ts.map +1 -0
  14. package/dist/api/formsApi.js +81 -0
  15. package/dist/api/formsApi.js.map +1 -0
  16. package/dist/api/generated/schema.d.ts +987 -0
  17. package/dist/api/generated/schema.d.ts.map +1 -0
  18. package/dist/api/generated/schema.js +2 -0
  19. package/dist/api/generated/schema.js.map +1 -0
  20. package/dist/api/types.d.ts +222 -0
  21. package/dist/api/types.d.ts.map +1 -0
  22. package/dist/api/types.js +14 -0
  23. package/dist/api/types.js.map +1 -0
  24. package/dist/default/ConfigField.d.ts +32 -0
  25. package/dist/default/ConfigField.d.ts.map +1 -0
  26. package/dist/default/ConfigField.js +101 -0
  27. package/dist/default/ConfigField.js.map +1 -0
  28. package/dist/default/ErrorAlert.d.ts +9 -0
  29. package/dist/default/ErrorAlert.d.ts.map +1 -0
  30. package/dist/default/ErrorAlert.js +27 -0
  31. package/dist/default/ErrorAlert.js.map +1 -0
  32. package/dist/default/FormBuilderPane.d.ts +36 -0
  33. package/dist/default/FormBuilderPane.d.ts.map +1 -0
  34. package/dist/default/FormBuilderPane.js +70 -0
  35. package/dist/default/FormBuilderPane.js.map +1 -0
  36. package/dist/default/FormsListPane.d.ts +18 -0
  37. package/dist/default/FormsListPane.d.ts.map +1 -0
  38. package/dist/default/FormsListPane.js +28 -0
  39. package/dist/default/FormsListPane.js.map +1 -0
  40. package/dist/default/ResponsesPane.d.ts +21 -0
  41. package/dist/default/ResponsesPane.d.ts.map +1 -0
  42. package/dist/default/ResponsesPane.js +118 -0
  43. package/dist/default/ResponsesPane.js.map +1 -0
  44. package/dist/default/StapelForm.d.ts +76 -0
  45. package/dist/default/StapelForm.d.ts.map +1 -0
  46. package/dist/default/StapelForm.js +118 -0
  47. package/dist/default/StapelForm.js.map +1 -0
  48. package/dist/default/fields.d.ts +11 -0
  49. package/dist/default/fields.d.ts.map +1 -0
  50. package/dist/default/fields.js +273 -0
  51. package/dist/default/fields.js.map +1 -0
  52. package/dist/default/index.d.ts +42 -0
  53. package/dist/default/index.d.ts.map +1 -0
  54. package/dist/default/index.js +37 -0
  55. package/dist/default/index.js.map +1 -0
  56. package/dist/default/slots.d.ts +59 -0
  57. package/dist/default/slots.d.ts.map +1 -0
  58. package/dist/default/slots.js +25 -0
  59. package/dist/default/slots.js.map +1 -0
  60. package/dist/default/theme.d.ts +23 -0
  61. package/dist/default/theme.d.ts.map +1 -0
  62. package/dist/default/theme.js +38 -0
  63. package/dist/default/theme.js.map +1 -0
  64. package/dist/default/types.d.ts +12 -0
  65. package/dist/default/types.d.ts.map +1 -0
  66. package/dist/default/types.js +2 -0
  67. package/dist/default/types.js.map +1 -0
  68. package/dist/flows/errors.d.ts +12 -0
  69. package/dist/flows/errors.d.ts.map +1 -0
  70. package/dist/flows/errors.js +13 -0
  71. package/dist/flows/errors.js.map +1 -0
  72. package/dist/flows/registry.d.ts +22 -0
  73. package/dist/flows/registry.d.ts.map +1 -0
  74. package/dist/flows/registry.js +20 -0
  75. package/dist/flows/registry.js.map +1 -0
  76. package/dist/headless/FormBuilder.d.ts +100 -0
  77. package/dist/headless/FormBuilder.d.ts.map +1 -0
  78. package/dist/headless/FormBuilder.js +278 -0
  79. package/dist/headless/FormBuilder.js.map +1 -0
  80. package/dist/headless/FormFill.d.ts +88 -0
  81. package/dist/headless/FormFill.d.ts.map +1 -0
  82. package/dist/headless/FormFill.js +279 -0
  83. package/dist/headless/FormFill.js.map +1 -0
  84. package/dist/headless/FormList.d.ts +34 -0
  85. package/dist/headless/FormList.d.ts.map +1 -0
  86. package/dist/headless/FormList.js +37 -0
  87. package/dist/headless/FormList.js.map +1 -0
  88. package/dist/headless/FormsProvider.d.ts +19 -0
  89. package/dist/headless/FormsProvider.d.ts.map +1 -0
  90. package/dist/headless/FormsProvider.js +15 -0
  91. package/dist/headless/FormsProvider.js.map +1 -0
  92. package/dist/headless/ResponsesTable.d.ts +82 -0
  93. package/dist/headless/ResponsesTable.d.ts.map +1 -0
  94. package/dist/headless/ResponsesTable.js +195 -0
  95. package/dist/headless/ResponsesTable.js.map +1 -0
  96. package/dist/i18n/errorsMap.d.ts +10 -0
  97. package/dist/i18n/errorsMap.d.ts.map +1 -0
  98. package/dist/i18n/errorsMap.js +20 -0
  99. package/dist/i18n/errorsMap.js.map +1 -0
  100. package/dist/i18n/es.d.ts +7 -0
  101. package/dist/i18n/es.d.ts.map +1 -0
  102. package/dist/i18n/es.js +118 -0
  103. package/dist/i18n/es.js.map +1 -0
  104. package/dist/i18n/generated/errors.es.gen.d.ts +16 -0
  105. package/dist/i18n/generated/errors.es.gen.d.ts.map +1 -0
  106. package/dist/i18n/generated/errors.es.gen.js +79 -0
  107. package/dist/i18n/generated/errors.es.gen.js.map +1 -0
  108. package/dist/i18n/generated/errors.gen.d.ts +485 -0
  109. package/dist/i18n/generated/errors.gen.d.ts.map +1 -0
  110. package/dist/i18n/generated/errors.gen.js +246 -0
  111. package/dist/i18n/generated/errors.gen.js.map +1 -0
  112. package/dist/i18n/generated/errors.ru.gen.d.ts +16 -0
  113. package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -0
  114. package/dist/i18n/generated/errors.ru.gen.js +79 -0
  115. package/dist/i18n/generated/errors.ru.gen.js.map +1 -0
  116. package/dist/i18n/keys.d.ts +141 -0
  117. package/dist/i18n/keys.d.ts.map +1 -0
  118. package/dist/i18n/keys.js +233 -0
  119. package/dist/i18n/keys.js.map +1 -0
  120. package/dist/i18n/ru.d.ts +7 -0
  121. package/dist/i18n/ru.d.ts.map +1 -0
  122. package/dist/i18n/ru.js +127 -0
  123. package/dist/i18n/ru.js.map +1 -0
  124. package/dist/index.d.ts +61 -0
  125. package/dist/index.d.ts.map +1 -0
  126. package/dist/index.js +55 -0
  127. package/dist/index.js.map +1 -0
  128. package/dist/model/context.d.ts +11 -0
  129. package/dist/model/context.d.ts.map +1 -0
  130. package/dist/model/context.js +16 -0
  131. package/dist/model/context.js.map +1 -0
  132. package/dist/model/mutations.d.ts +135 -0
  133. package/dist/model/mutations.d.ts.map +1 -0
  134. package/dist/model/mutations.js +223 -0
  135. package/dist/model/mutations.js.map +1 -0
  136. package/dist/model/queries.d.ts +59 -0
  137. package/dist/model/queries.d.ts.map +1 -0
  138. package/dist/model/queries.js +117 -0
  139. package/dist/model/queries.js.map +1 -0
  140. package/dist/model/queryKeys.d.ts +28 -0
  141. package/dist/model/queryKeys.d.ts.map +1 -0
  142. package/dist/model/queryKeys.js +23 -0
  143. package/dist/model/queryKeys.js.map +1 -0
  144. package/dist/model/runtime.d.ts +28 -0
  145. package/dist/model/runtime.d.ts.map +1 -0
  146. package/dist/model/runtime.js +14 -0
  147. package/dist/model/runtime.js.map +1 -0
  148. package/dist/widgets/configForms.d.ts +112 -0
  149. package/dist/widgets/configForms.d.ts.map +1 -0
  150. package/dist/widgets/configForms.js +211 -0
  151. package/dist/widgets/configForms.js.map +1 -0
  152. package/dist/widgets/registry.d.ts +83 -0
  153. package/dist/widgets/registry.d.ts.map +1 -0
  154. package/dist/widgets/registry.js +27 -0
  155. package/dist/widgets/registry.js.map +1 -0
  156. package/dist/widgets/validate.d.ts +57 -0
  157. package/dist/widgets/validate.d.ts.map +1 -0
  158. package/dist/widgets/validate.js +173 -0
  159. package/dist/widgets/validate.js.map +1 -0
  160. package/llms.txt +124 -0
  161. package/manifest.json +1038 -0
  162. package/package.json +108 -0
  163. package/src/analytics/generated/events.json +7 -0
  164. package/src/api/export.ts +195 -0
  165. package/src/api/extensions.ts +7 -0
  166. package/src/api/formsApi.ts +329 -0
  167. package/src/api/generated/schema.ts +989 -0
  168. package/src/api/types.ts +276 -0
  169. package/src/default/ConfigField.tsx +227 -0
  170. package/src/default/ErrorAlert.tsx +46 -0
  171. package/src/default/FormBuilderPane.tsx +414 -0
  172. package/src/default/FormsListPane.tsx +144 -0
  173. package/src/default/ResponsesPane.tsx +332 -0
  174. package/src/default/StapelForm.tsx +377 -0
  175. package/src/default/fields.tsx +454 -0
  176. package/src/default/index.ts +71 -0
  177. package/src/default/slots.ts +78 -0
  178. package/src/default/theme.tsx +65 -0
  179. package/src/default/types.ts +12 -0
  180. package/src/flows/errors.ts +16 -0
  181. package/src/flows/registry.ts +32 -0
  182. package/src/headless/FormBuilder.tsx +453 -0
  183. package/src/headless/FormFill.tsx +396 -0
  184. package/src/headless/FormList.tsx +74 -0
  185. package/src/headless/FormsProvider.tsx +20 -0
  186. package/src/headless/ResponsesTable.tsx +341 -0
  187. package/src/i18n/errorsMap.ts +31 -0
  188. package/src/i18n/es.ts +140 -0
  189. package/src/i18n/generated/errors.es.gen.ts +85 -0
  190. package/src/i18n/generated/errors.gen.ts +277 -0
  191. package/src/i18n/generated/errors.json +583 -0
  192. package/src/i18n/generated/errors.ru.gen.ts +85 -0
  193. package/src/i18n/keys.ts +264 -0
  194. package/src/i18n/ru.ts +150 -0
  195. package/src/index.ts +184 -0
  196. package/src/model/context.tsx +28 -0
  197. package/src/model/mutations.ts +387 -0
  198. package/src/model/queries.ts +154 -0
  199. package/src/model/queryKeys.ts +61 -0
  200. package/src/model/runtime.ts +48 -0
  201. package/src/widgets/registry.ts +101 -0
  202. package/src/widgets/validate.ts +224 -0
  203. package/tsconfig.json +26 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,62 @@
1
+ # @stapel/forms-react
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b1f9a4e: `@stapel/forms-react` — the frontend pair for stapel-forms.
8
+
9
+ A host page says "put form `<id>` here" and gets a rendered form:
10
+
11
+ ```tsx
12
+ const runtime = createFormsRuntime({ baseUrl: "/forms/api/v1/" });
13
+ <FormsProvider runtime={runtime}>
14
+ <StapelForm publicId="k3J…x9" />
15
+ </FormsProvider>;
16
+ ```
17
+
18
+ No session, no workspace id, no auth client — the two public endpoints are
19
+ anonymous, so a marketing page can embed a form and nothing else.
20
+
21
+ **A failed schema fetch is never "no form here."** The skin distinguishes "this
22
+ link is not valid" (404), "this form is closed" (410) and "we could not load it,
23
+ our problem, not your link" (network/5xx, with a retry). The data sits behind
24
+ `LoadState`'s discriminant, so the empty-state lie is a compile error rather
25
+ than a code-review note.
26
+
27
+ **Three override levers, none of them a fork.** `registerFormFieldWidget(kind, C)`
28
+ replaces how one field kind draws and outranks the skin's builtin;
29
+ `registerFormsSkinComponent(slot, C)` replaces a piece of the skin across eight
30
+ typed slots; and retheming through the token JSON reaches every surface with
31
+ zero code. A field kind nothing can draw renders a loud notice _and_ blocks the
32
+ submit — quietly skipping a possibly-required field would fabricate an invalid
33
+ submission and get the person refused for a question they never saw.
34
+
35
+ Ten builtin antd widgets, one per kind stapel-forms allows. Headless
36
+ `FormFill` / `FormBuilder` / `ResponsesTable` / `FormList` under the main entry;
37
+ `StapelForm` / `FormBuilderPane` / `ResponsesPane` / `FormsListPane` under
38
+ `./default`; `ru` and `es` as opt-in `./i18n/*` subpaths.
39
+
40
+ The submit path echoes `version_id` (a racing publish becomes a clean 409),
41
+ routes per-field `error.400.feature_*` onto controls by `params.field`, folds a
42
+ 409 supersede into a refetch that preserves compatible answers and _says so_,
43
+ and threads `captcha_token` through. CSV export follows the
44
+ `X-Forms-Next-Before` header cursor verbatim.
45
+
46
+ Built against stapel-forms 0.2.0: the builder reads its field-kind catalogue
47
+ from `GET /forms/api/v1/field-kinds` (no mirrored table in the client), and
48
+ the `error.400.feature_*` family comes from the backend's own error contract.
49
+ `stapel_attributes` ships English only, so the generated ru/es bundles are
50
+ `Partial` and the pair layers authored strings for those 12 keys over them.
51
+
52
+ Enrollment note for consumers of other pairs: the `stapel-core` contract pin
53
+ moves v0.23.1 → v0.32.0 to pick up `error.503.mandate_unavailable`. Verified a
54
+ one-key delta — no already-enrolled pair's locale bundle changes.
55
+
56
+ ## 0.0.0
57
+
58
+ - Scaffolded by `stapel-new-react-lib` from the auth-react etalon
59
+ (frontend-standard §9, frontend-core-architecture §4 checklist). Layers
60
+ api → model → flows → headless → i18n; drift-gated generated surfaces
61
+ (flows registry, backend error map, manifest + llms.txt) via the shared
62
+ monorepo `gen:*` drivers.
package/MODULE.md ADDED
@@ -0,0 +1,172 @@
1
+ # @stapel/forms-react — module guide
2
+
3
+ The human companion to the generated `llms.txt` (agent context) and
4
+ `manifest.json` (machine catalog). `README.md` is the how-to; this is the
5
+ why-it-is-shaped-this-way.
6
+
7
+ Built against **stapel-forms 0.2.0** (contract pin
8
+ `d66a1fb048feebebebaf98ec971fb41ab60f820b`), which closed both upstream asks
9
+ this pair filed against 0.1.0 — so two workarounds were deleted rather than
10
+ carried. See "What 0.2.0 deleted" below.
11
+
12
+ ## Layers
13
+
14
+ - **api/** — `createFormsApi(client)`, one method per endpoint a JS client may
15
+ call (18 operations over 14 paths), over the generated `components["schemas"]` from this pair's own
16
+ package-local `api/generated/schema.ts` (`pnpm gen:api` ← stapel-forms
17
+ `docs/schema.json`). Two families, and the split is load-bearing:
18
+ `getPublicForm`/`submit` are **anonymous**; everything else is
19
+ `IsNotAnonymousUser` + `authorize()` + an explicit `workspace_id`.
20
+ `api/export.ts` is the raw-`fetch` carve-out (see below).
21
+ - **model/** — `formsQueryKeys` (one key factory, `["forms"]` namespace),
22
+ `createFormsRuntime`, context/hooks, and the read/write hooks.
23
+ - **flows/** — `toFlowError` + the zero-flow `FORMS_FLOWS` registry shim.
24
+ stapel-forms annotates no `@flow_step`, so `docs/flows.json` carries no
25
+ `forms.*` flow and `gen:flows` emits nothing. When the backend annotates
26
+ flows, swap the shim for re-exports of `generated/flows.gen.ts`.
27
+ - **widgets/** — the field-widget registry and the client-side validation
28
+ mirror. Main entry, not `/default`: a host building its own renderer uses the
29
+ same seam the skin does. (The config-form declarations used to live here too,
30
+ as `configForms.ts`; they come off the wire now.)
31
+ - **headless/** — `FormFill`, `FormBuilder`, `ResponsesTable`, `FormList`.
32
+ - **i18n/** — the en floor + generated backend error bundles; `ru`/`es` as
33
+ their own subpaths so a host that does not need them carries nothing.
34
+ - **default/** — the antd skin, a separate entry point.
35
+
36
+ ## Why the CSV export does not use core's client
37
+
38
+ `GET /forms/<id>/submissions/export` cannot ride `StapelClient` for two
39
+ independent reasons: the body is a CSV **file**, and the continuation cursor
40
+ rides the **`X-Forms-Next-Before` response header**, which `StapelClient.request`
41
+ does not surface at all. So `api/export.ts` speaks `fetch` directly — the same
42
+ hand-authored carve-out docs-react's `api/content.ts` and recordings-react's
43
+ `uploadRecordingBlob` use, and the one legal home of `fetch` under
44
+ `stapel/no-raw-fetch`.
45
+
46
+ The cursor is passed back **verbatim**. It is Z-suffixed rather than isoformat
47
+ because a bare `+00:00` in a query string decodes to a space, which made the
48
+ second page a silent 400 during the backend build; re-formatting it here would
49
+ reintroduce exactly that.
50
+
51
+ **v1 limitation, stated:** the bearer-refresh and verification-403 seams of
52
+ `createStapelClient` do NOT run on this raw surface. A 401 here throws like any
53
+ other error status instead of triggering a refresh-and-retry. `credentials` and
54
+ `defaultHeaders` from the runtime ARE forwarded, so cookie-mode hosts work
55
+ unchanged. Closing this properly is a core change (a response-returning
56
+ primitive on `StapelClient`), not a per-module workaround.
57
+
58
+ ## What 0.2.0 deleted
59
+
60
+ **`widgets/configForms.ts` — the pinned TypeScript mirror — is gone.**
61
+ stapel-forms 0.2.0 serves `stapel_attributes.config_form()` at
62
+ `GET /forms/api/v1/field-kinds`, so the declaration is the single source of
63
+ truth again: a type registered through `EXTRA_TYPES` shows up in the builder
64
+ with no client release, and there is no table left to drift. The builder reads
65
+ it through `useFieldKinds`.
66
+
67
+ Three things about that route are easy to get wrong and are pinned by
68
+ `test/contract.test.ts`: **no `admin/` prefix**, **no trailing slash** (unlike
69
+ the two anonymous routes, which require one), and `?workspace_id=` is
70
+ **required**. It carries `forms.manage`, not `forms.view` — the catalogue names
71
+ host types whose slugs are internal vocabulary, and a principal who cannot build
72
+ a form has no use for the builder's dictionary.
73
+
74
+ Reading the real declarations also corrected a detail the mirror had wrong:
75
+ widget params (`step`, `placeholder`, inline `options`) live **under
76
+ `spec.params`**, not flattened onto the spec. `ConfigField` reads them from
77
+ there now.
78
+
79
+ **The hand-carried `error.400.feature_*` English is gone.** 0.2.0's
80
+ `docs/errors.json` grew 63 → 75 keys, the 12 new ones owned by
81
+ `stapel_attributes`, so the family arrives through `gen:errors` like every other
82
+ backend code. The self-deleting test that asserted the registry omitted them is
83
+ deleted with it; its inverse now asserts the family IS generated, so nobody
84
+ re-adds a hand copy.
85
+
86
+ ## Why the ru/es bundles are `Partial`
87
+
88
+ `stapel_attributes` ships English only — it has no `translations/` directory at
89
+ all — so its 12 keys can appear in NO locale catalog, and demanding them made
90
+ the ru/es bundles unbuildable while stapel-forms' own catalog was perfectly
91
+ correct. `scripts/gen-errors.mjs` gained a general knob for exactly this shape
92
+ (a module inheriting an L1 library's keys):
93
+ `ERRORS_LOCALE_EXEMPT_OWNERS=stapel_attributes` for this pair.
94
+
95
+ Exempted keys are not required in a locale catalog and are left OUT of that
96
+ locale's generated bundle, which widens its type to
97
+ `Partial<Record<FormsErrorCode, string>>` — so the gap is visible to TypeScript
98
+ instead of silently falling back to English. The pair's own `i18n/ru.ts` /
99
+ `i18n/es.ts` layer authored strings for all 12 over the generated bundle.
100
+ English is unaffected: it comes from the registry artifact, which is
101
+ authoritative for every owner.
102
+
103
+ This is opt-in per pair, so a module that DOES own a key and dropped it from its
104
+ catalog still goes red. Upstream localization is logged as stapel-forms
105
+ MODULE.md §12.6; when it lands, drop the env knob and the authored strings.
106
+
107
+ ## Deviations from the spec's sketch, with reasons
108
+
109
+ 1. **`date` renders a native input, not antd's `DatePicker`.** `DatePicker`
110
+ speaks Dayjs; adopting it would add a runtime dependency for one widget and
111
+ insert a format-guessing step where the native control already yields the ISO
112
+ string the attributes date type parses. `precision` picks the input type.
113
+ 2. **Builder-less is now a SERVER signal, not a pair opinion** (§12 risk 5,
114
+ applied). Two distinct facts arrive from `/field-kinds` and the bag reports
115
+ them separately so a skin can word them differently:
116
+ `registered: false` (the host allowlisted a kind the attributes registry
117
+ does not carry) and `fields: []` (registered, but declares no config form —
118
+ how `convertible_unit` arrives). Either way the field stays LISTED,
119
+ reorderable and removable, and authorable through the draft PUT: a builder
120
+ that dropped an unknown kind would silently drop the field from a stored
121
+ schema. Separately, two of upstream's 13 config WIDGETS are not implemented
122
+ in this skin (`hierarchical_options`, `timestamp_array`); those individual
123
+ rows say so, and the rest of the kind stays editable.
124
+ 3. **The submit-blocked key is `forms.submit.blocked.unsupported_kind`**, not
125
+ the spec's illustrative `forms.unsupported_kind` — every other blocked reason
126
+ in this pair is `forms.<surface>.blocked.<why>`, and one key in a different
127
+ shape is a key somebody will mistype.
128
+ 4. **Error params are NOT narrowed to the artifact's declared list.** The
129
+ registry declares only the slots a message interpolates
130
+ (`feature_mandatory_missing` declares `["feature"]`), but the runtime also
131
+ sets `field`, `slug` and `ref_value` — and `params.field` is precisely what
132
+ routes a refusal onto a control. Typing `params` to the declared list would
133
+ make the pair's own field-error routing a type error against a payload the
134
+ server really sends.
135
+ 5. **The client-side mirror emits the SERVER's error keys.** A "too long" caught
136
+ locally and one caught by the server render the same sentence in the same
137
+ language. Pair-invented copy for the local half would give one problem two
138
+ wordings and become a lie the moment the backend's rule moved.
139
+
140
+ ## Seams
141
+
142
+ | Seam | Call | Beats |
143
+ |---|---|---|
144
+ | Field widget | `registerFormFieldWidget(kind, C)` | the skin's builtin |
145
+ | Skin slot | `registerFormsSkinComponent(slot, C)` | the skin's own part |
146
+ | Theme | regenerate the token JSON | everything, with zero code |
147
+ | Client | `clients={{ forms: runtime.client }}` | the default client |
148
+ | i18n | register a bundle last | any pair copy |
149
+
150
+ ## Query keys and invalidation
151
+
152
+ Everything under `["forms"]`. Admin mutations invalidate the module ROOT rather
153
+ than guessing which entries moved — `rotateLink` changes the very `public_id`
154
+ the anonymous read is cached under, so a targeted invalidation would leave a
155
+ stale schema under a token that no longer resolves.
156
+
157
+ `useSubmitForm` invalidates nothing (an anonymous respondent holds no admin
158
+ cache) and does not retry (a resubmit is not idempotent — a retried POST the
159
+ server actually received is a duplicate row in somebody's spreadsheet).
160
+
161
+ ## Session gating
162
+
163
+ Every admin read is gated on `useActiveSessionReady`. `usePublicForm` is
164
+ deliberately NOT — core's own doc comment carves out "a public GET", and gating
165
+ it would make an embedded form on a marketing page wait for a login bootstrap it
166
+ has no stake in.
167
+
168
+ ## Live counts
169
+
170
+ Refetch only. A `forms:ws:<workspace_id>` Signal stream is reserved naming for
171
+ when the stapel-realtime substrate lands; forms does not build a socket, and
172
+ that is a lint boundary rather than an unfinished feature.
package/README.md ADDED
@@ -0,0 +1,207 @@
1
+ # @stapel/forms-react
2
+
3
+ The frontend pair for **stapel-forms**: an admin defines a form's schema, a host
4
+ page says *"put form `<id>` here"*, and this library fetches the schema and
5
+ renders it — default skin, or your own.
6
+
7
+ Business + state in the main entry, zero visual opinion; the antd skin lives
8
+ behind `./default` so a host that renders its own visuals never carries it.
9
+ Built on `@stapel/core` (typed client + `StapelApiError` envelope, token
10
+ refresh, verification-403 interception, i18n engine, analytics seam, TanStack
11
+ Query).
12
+
13
+ ## Install
14
+
15
+ ```
16
+ pnpm add @stapel/forms-react @stapel/core @tanstack/react-query react
17
+ # for the default skin:
18
+ pnpm add antd @stapel/tokens-antd
19
+ ```
20
+
21
+ ## Put a form on a page
22
+
23
+ The whole ask, in five lines. **No session, no workspace id, no auth client** —
24
+ the two public endpoints are anonymous, so a marketing page can embed a form and
25
+ nothing else:
26
+
27
+ ```tsx
28
+ import { createFormsRuntime, FormsProvider } from "@stapel/forms-react";
29
+ import { StapelForm } from "@stapel/forms-react/default";
30
+
31
+ const runtime = createFormsRuntime({ baseUrl: "/forms/api/v1/" });
32
+
33
+ export function ContactPage() {
34
+ return (
35
+ <FormsProvider runtime={runtime}>
36
+ <StapelForm publicId="k3JhQ2Zt8uY1sVb7cD9xLg" />
37
+ </FormsProvider>
38
+ );
39
+ }
40
+ ```
41
+
42
+ The handle you embed is the **`public_id`** — the same non-enumerable token the
43
+ backend serves anonymously. The pair never needs the row UUID or a workspace id
44
+ to render.
45
+
46
+ In an app that already has a session, put the forms client on core's provider
47
+ alongside the others and keep one client per module:
48
+
49
+ ```tsx
50
+ <StapelProvider client={authRuntime.client} clients={{ forms: runtime.client }} i18n={i18n}>
51
+ <FormsProvider runtime={runtime}>{app}</FormsProvider>
52
+ </StapelProvider>
53
+ ```
54
+
55
+ ## A failed fetch is never "no form here"
56
+
57
+ The one rule this pair is built around. A schema read has **three** outcomes and
58
+ the skin says a different thing for each:
59
+
60
+ | What happened | What the person sees |
61
+ |---|---|
62
+ | `404 error.404.forms_not_found` | "This form link is not valid." |
63
+ | `410 error.410.forms_closed` | "This form is closed and is no longer accepting responses." |
64
+ | network fault / 5xx | "We could not load this form. This is a problem on our side, not with your link." **+ a retry button** |
65
+
66
+ The bag exposes `state: LoadState<PublicForm>`, so the data is *behind* the
67
+ discriminant — there is no `.data` to read on a failed load, and the
68
+ `no-flattened-load-state` lint rule enforces it mechanically. Branch on the
69
+ verdicts with core's `hasErrorCode`.
70
+
71
+ ## Headless
72
+
73
+ `<StapelForm>` is one renderer over `<FormFill>`. Write another and lose nothing:
74
+
75
+ ```tsx
76
+ <FormFill publicId="k3J…x9">
77
+ {(bag) => matchLoad(bag.state, {
78
+ loading: () => <Spinner />,
79
+ failed: (e) => <MyError error={e} onRetry={bag.refetch} />,
80
+ ready: (form) => <MyFields form={form} bag={bag} />,
81
+ })}
82
+ </FormFill>
83
+ ```
84
+
85
+ The bag carries `values` / `setValue`, `fieldErrors` keyed by slug, an
86
+ `ActionAvailability` submit gate that always states *why* it is off,
87
+ `superseded`, `unsupportedKinds`, and `setCaptchaToken`.
88
+
89
+ Also headless: `<FormBuilder>`, `<ResponsesTable>`, `<FormList>`.
90
+
91
+ ## Three ways to override, none of them a fork
92
+
93
+ **1. Props** — `mode`, `showTitle`, `submitLabel`.
94
+
95
+ **2. The field-widget registry.** The backend's field vocabulary is an open
96
+ registry (`stapel_attributes`), so the frontend's is too:
97
+
98
+ ```tsx
99
+ registerFormFieldWidget("signature", SignaturePad);
100
+ ```
101
+
102
+ Resolution is *your registration → the skin's antd builtin → an
103
+ "unsupported field" notice*. A host registration always wins. A kind nothing can
104
+ draw renders the notice **and blocks the submit** — silently skipping a
105
+ possibly-required field would fabricate an invalid submission and get the person
106
+ refused for a field they never saw.
107
+
108
+ **3. The skin-slot registry** — replace a piece of the skin itself:
109
+
110
+ ```tsx
111
+ registerFormsSkinComponent("fill.submitBar", MySubmitBar);
112
+ ```
113
+
114
+ Slots: `fill.fieldRow`, `fill.submitBar`, `fill.confirmation`,
115
+ `fill.unsupportedField`, `responses.cell`, `responses.toolbar`,
116
+ `builder.fieldRow`, `builder.toolbar`.
117
+
118
+ **And underneath all three: retheming.** Every `/default` surface wraps itself in
119
+ `<FormsSkinTheme>`, which reads `@stapel/tokens` through `@stapel/tokens-antd`.
120
+ Regenerate your `--stapel-*` custom properties from the token JSON and the form
121
+ follows, with zero code.
122
+
123
+ ## Builtin widgets
124
+
125
+ | Kind | Widget |
126
+ |---|---|
127
+ | `string` | `Input`, or `Input.TextArea` when `config.multiline` |
128
+ | `int` / `float` | `InputNumber` (min/max/precision/prefix/postfix) |
129
+ | `bool` | `Switch` with the config's `trueLabel`/`falseLabel` |
130
+ | `select` | `Segmented` at ≤4 single choices, else `Select` (multi/tags) |
131
+ | `date` | native `<input type=date\|month\|datetime-local>`, or a year `InputNumber` |
132
+ | `header` | `Typography.Title` — a caption, never a control |
133
+ | `hex_color` | `ColorPicker` |
134
+ | `hierarchical_select` | `Cascader` |
135
+ | `convertible_unit` | `InputNumber` + a unit `Select` |
136
+
137
+ `date` deliberately uses a native input rather than antd's `DatePicker`: the
138
+ picker speaks Dayjs, which would add a runtime dependency for one widget and put
139
+ a format-guessing step between the person and a wire format the backend already
140
+ parses. Register the picker yourself if you want it.
141
+
142
+ ## Captcha
143
+
144
+ The netintel tier decides whether a token is required at all, so the pair only
145
+ transports one. Render your challenge through the `captcha` prop and hand the bag
146
+ a token:
147
+
148
+ ```tsx
149
+ <StapelForm
150
+ publicId="k3J…x9"
151
+ captcha={(bag) => <Turnstile onVerify={(t) => bag.setCaptchaToken(t)} />}
152
+ />
153
+ ```
154
+
155
+ ## Admin surface
156
+
157
+ Capability-gated over REST (`forms.view`, `forms.manage`,
158
+ `forms.responses.view`, `forms.responses.manage`):
159
+
160
+ ```tsx
161
+ <FormsListPane workspaceId={ws} onOpen={(form) => navigate(form.id)} />
162
+ <FormBuilderPane workspaceId={ws} formId={id} />
163
+ <ResponsesPane workspaceId={ws} formId={id} />
164
+ ```
165
+
166
+ `<FormBuilderPane>` is **data-driven**: a field's options come from
167
+ `GET /field-kinds`, which serves the config declarations `stapel_attributes`
168
+ publishes per type. There is no hand-written form per kind and no mirrored table
169
+ — register a feature type upstream and it appears in the builder with no client
170
+ release.
171
+
172
+ A kind arrives *builder-less* for either of two reasons the server distinguishes:
173
+ `registered: false` (this deployment allowlisted a type the attributes registry
174
+ does not carry) or `fields: []` (registered, but it declares no config form —
175
+ `convertible_unit`). Either way the field is still listed, still renders, still
176
+ submits, and stays authorable through the draft API — a builder that dropped an
177
+ unknown kind would silently drop the field from a stored schema.
178
+
179
+ `<ResponsesPane>` draws **per-version columns** (a response records which schema
180
+ it answered, so an old row shows the questions actually asked), keyset paging,
181
+ resend, and CSV export.
182
+
183
+ ### Resend overrides REPLACE
184
+
185
+ An explicit destination on a resend replaces the form's configured recipients for
186
+ that one send — "send this one to legal" must not also re-send it to everybody
187
+ who already received it.
188
+
189
+ ## i18n
190
+
191
+ ```tsx
192
+ registerFormsI18n(i18n); // en floor, always
193
+ registerFormsI18nRu(i18n); // from @stapel/forms-react/i18n/ru
194
+ registerFormsI18nEs(i18n); // from @stapel/forms-react/i18n/es
195
+ ```
196
+
197
+ Registration order is override priority — a host bundle registered last wins.
198
+
199
+ **Field labels are not i18n keys.** They are admin-authored content carried in
200
+ the schema and render verbatim in whatever language the admin typed. Translating
201
+ form *content* is a separate problem from translating the pair's chrome.
202
+
203
+ ## Docs
204
+
205
+ `MODULE.md` — layer map, seams, and the deliberate deviations, with reasons.
206
+ `llms.txt` / `manifest.json` — the generated agent/machine views, drift-gated
207
+ against the backend contract.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Raw-transport binding forwarded from the runtime. Declared here rather than
3
+ * in `formsApi.ts` so the two modules do not import each other's types in a
4
+ * cycle — `formsApi.ts` re-exports it as part of its public surface.
5
+ */
6
+ export interface FormsApiOptions {
7
+ readonly fetch?: typeof globalThis.fetch;
8
+ readonly credentials?: RequestCredentials;
9
+ readonly defaultHeaders?: Record<string, string>;
10
+ }
11
+ /**
12
+ * Response header carrying the keyset cursor for the NEXT export page.
13
+ * Absent (or empty) means this was the last page.
14
+ */
15
+ export declare const FORMS_NEXT_BEFORE_HEADER = "X-Forms-Next-Before";
16
+ /** The raw-transport binding `createFormsApi` closes over. */
17
+ export interface FormsRawTransport {
18
+ /** e.g. `/forms/api/v1` — the same base the pair's `StapelClient` uses. */
19
+ readonly baseUrl: string;
20
+ readonly fetch?: typeof globalThis.fetch;
21
+ readonly credentials?: RequestCredentials;
22
+ /** Merged into every raw request (e.g. a tenant id / auth header). */
23
+ readonly headers?: Record<string, string>;
24
+ }
25
+ /** One page of the CSV export. */
26
+ export interface CsvExportPage {
27
+ /** The page's CSV text, header row included. */
28
+ readonly csv: string;
29
+ /**
30
+ * The cursor to pass as the next call's `before`, or `null` when the export
31
+ * is complete. Opaque — pass it back verbatim (see the module header).
32
+ */
33
+ readonly nextBefore: string | null;
34
+ }
35
+ /** Build the transport from the runtime options, dropping absent keys so
36
+ * `exactOptionalPropertyTypes` stays satisfied. */
37
+ export declare function createExportTransport(baseUrl: string, options: FormsApiOptions): FormsRawTransport;
38
+ /**
39
+ * Fetch one page of a form's CSV export.
40
+ *
41
+ * Drive it to completion by feeding {@link CsvExportPage.nextBefore} back in
42
+ * as `before` until it is `null`, concatenating the pages MINUS the repeated
43
+ * header row (see `useCsvExport`, which does exactly that).
44
+ */
45
+ export declare function exportSubmissionsCsv(transport: FormsRawTransport, params: {
46
+ readonly workspaceId: string;
47
+ readonly formId: string;
48
+ readonly before?: string;
49
+ readonly version?: number;
50
+ readonly signal?: AbortSignal;
51
+ }): Promise<CsvExportPage>;
52
+ /**
53
+ * Concatenate export pages into one CSV, keeping the header row exactly once.
54
+ *
55
+ * Every page the server streams carries its own header row (it is a standalone
56
+ * CSV file); pasting the pages verbatim would sprinkle header rows through the
57
+ * middle of the spreadsheet. Splitting on the first newline is safe here
58
+ * because a stapel-forms header row is a list of field slugs — slugs cannot
59
+ * contain a newline, so the first line is always exactly the header.
60
+ *
61
+ * Each retained chunk is newline-terminated before the join: a page whose last
62
+ * row has no trailing newline would otherwise be glued to the next page's
63
+ * first row, silently producing one corrupt record per page boundary.
64
+ */
65
+ export declare function concatCsvPages(pages: readonly string[]): string;
66
+ //# sourceMappingURL=export.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../src/api/export.ts"],"names":[],"mappings":"AA6BA;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IACzC,QAAQ,CAAC,WAAW,CAAC,EAAE,kBAAkB,CAAC;IAC1C,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClD;AAED;;;GAGG;AACH,eAAO,MAAM,wBAAwB,wBAAwB,CAAC;AAE9D,8DAA8D;AAC9D,MAAM,WAAW,iBAAiB;IAChC,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IACzC,QAAQ,CAAC,WAAW,CAAC,EAAE,kBAAkB,CAAC;IAC1C,sEAAsE;IACtE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC3C;AAED,kCAAkC;AAClC,MAAM,WAAW,aAAa;IAC5B,gDAAgD;IAChD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED;mDACmD;AACnD,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,eAAe,GACvB,iBAAiB,CAWnB;AAkCD;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,iBAAiB,EAC5B,MAAM,EAAE;IACN,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B,GACA,OAAO,CAAC,aAAa,CAAC,CA8BxB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAgB/D"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * The CSV half of the stapel-forms surface — hand-authored api/ carve-out
3
+ * (the docs-react `api/content.ts` / recordings-react `uploadRecordingBlob`
4
+ * precedent, the one legal home of `fetch` per `stapel/no-raw-fetch`).
5
+ *
6
+ * `GET /forms/<id>/submissions/export` is not a JSON operation and cannot ride
7
+ * core's `StapelClient` for two independent reasons:
8
+ *
9
+ * 1. the body is a CSV file, and `StapelClient.request` parses/returns a
10
+ * decoded JSON value;
11
+ * 2. the CONTINUATION CURSOR rides a response header,
12
+ * `X-Forms-Next-Before` — which `StapelClient` does not surface at all.
13
+ *
14
+ * Backend delta note 6 explains why the cursor is a header rather than a
15
+ * trailing body field: the body is a spreadsheet, and a cursor appended to it
16
+ * lands as a row in somebody's export. The same note records why the value is
17
+ * **Z-suffixed** rather than isoformat — a bare `+00:00` in a query string
18
+ * decodes to a space, which made the second page a silent 400. This module
19
+ * therefore round-trips the header value VERBATIM into the next `?before=`
20
+ * and never re-formats it.
21
+ *
22
+ * Auth: the runtime's `credentials` / `defaultHeaders` are forwarded here, so
23
+ * a cookie-mode host works unchanged. The bearer-refresh and verification-403
24
+ * seams of `createStapelClient` do NOT run on this raw surface (the same v1
25
+ * limitation docs-react's raw transport documents); a 401 here throws like any
26
+ * other error status rather than triggering a refresh-and-retry.
27
+ */
28
+ import { parseErrorEnvelope } from "@stapel/core";
29
+ /**
30
+ * Response header carrying the keyset cursor for the NEXT export page.
31
+ * Absent (or empty) means this was the last page.
32
+ */
33
+ export const FORMS_NEXT_BEFORE_HEADER = "X-Forms-Next-Before";
34
+ /** Build the transport from the runtime options, dropping absent keys so
35
+ * `exactOptionalPropertyTypes` stays satisfied. */
36
+ export function createExportTransport(baseUrl, options) {
37
+ return {
38
+ baseUrl,
39
+ ...(options.fetch !== undefined ? { fetch: options.fetch } : {}),
40
+ ...(options.credentials !== undefined
41
+ ? { credentials: options.credentials }
42
+ : {}),
43
+ ...(options.defaultHeaders !== undefined
44
+ ? { headers: options.defaultHeaders }
45
+ : {}),
46
+ };
47
+ }
48
+ function exportUrl(transport, formId, query) {
49
+ const base = transport.baseUrl.endsWith("/")
50
+ ? transport.baseUrl.slice(0, -1)
51
+ : transport.baseUrl;
52
+ const search = new URLSearchParams();
53
+ for (const [key, value] of Object.entries(query)) {
54
+ if (value !== undefined)
55
+ search.set(key, String(value));
56
+ }
57
+ const qs = search.toString();
58
+ return (`${base}/forms/${encodeURIComponent(formId)}/submissions/export` +
59
+ (qs.length > 0 ? `?${qs}` : ""));
60
+ }
61
+ async function throwEnvelope(response) {
62
+ const text = await response.text();
63
+ let body = text.length > 0 ? text : undefined;
64
+ if (text.length > 0) {
65
+ try {
66
+ body = JSON.parse(text);
67
+ }
68
+ catch {
69
+ /* a non-JSON error body stays the raw text */
70
+ }
71
+ }
72
+ throw parseErrorEnvelope(response.status, body);
73
+ }
74
+ /**
75
+ * Fetch one page of a form's CSV export.
76
+ *
77
+ * Drive it to completion by feeding {@link CsvExportPage.nextBefore} back in
78
+ * as `before` until it is `null`, concatenating the pages MINUS the repeated
79
+ * header row (see `useCsvExport`, which does exactly that).
80
+ */
81
+ export async function exportSubmissionsCsv(transport, params) {
82
+ const fetchImpl = transport.fetch ?? globalThis.fetch.bind(globalThis);
83
+ const headers = new Headers({ Accept: "text/csv" });
84
+ if (transport.headers) {
85
+ for (const [key, value] of Object.entries(transport.headers)) {
86
+ if (!headers.has(key))
87
+ headers.set(key, value);
88
+ }
89
+ }
90
+ const init = { method: "GET", headers };
91
+ if (transport.credentials !== undefined)
92
+ init.credentials = transport.credentials;
93
+ if (params.signal)
94
+ init.signal = params.signal;
95
+ const response = await fetchImpl(exportUrl(transport, params.formId, {
96
+ workspace_id: params.workspaceId,
97
+ ...(params.before !== undefined ? { before: params.before } : {}),
98
+ ...(params.version !== undefined ? { version: params.version } : {}),
99
+ }), init);
100
+ if (!response.ok)
101
+ return throwEnvelope(response);
102
+ const raw = response.headers.get(FORMS_NEXT_BEFORE_HEADER);
103
+ return {
104
+ csv: await response.text(),
105
+ // An empty header is "no more pages", not a cursor of "".
106
+ nextBefore: raw !== null && raw.length > 0 ? raw : null,
107
+ };
108
+ }
109
+ /**
110
+ * Concatenate export pages into one CSV, keeping the header row exactly once.
111
+ *
112
+ * Every page the server streams carries its own header row (it is a standalone
113
+ * CSV file); pasting the pages verbatim would sprinkle header rows through the
114
+ * middle of the spreadsheet. Splitting on the first newline is safe here
115
+ * because a stapel-forms header row is a list of field slugs — slugs cannot
116
+ * contain a newline, so the first line is always exactly the header.
117
+ *
118
+ * Each retained chunk is newline-terminated before the join: a page whose last
119
+ * row has no trailing newline would otherwise be glued to the next page's
120
+ * first row, silently producing one corrupt record per page boundary.
121
+ */
122
+ export function concatCsvPages(pages) {
123
+ const nonEmpty = pages.filter((page) => page.length > 0);
124
+ const first = nonEmpty[0];
125
+ if (first === undefined)
126
+ return "";
127
+ const chunks = [
128
+ first,
129
+ ...nonEmpty.slice(1).map((page) => {
130
+ const newline = page.indexOf("\n");
131
+ return newline === -1 ? "" : page.slice(newline + 1);
132
+ }),
133
+ ].filter((chunk) => chunk.length > 0);
134
+ return chunks
135
+ .map((chunk, index) => index === chunks.length - 1 || chunk.endsWith("\n") ? chunk : `${chunk}\n`)
136
+ .join("");
137
+ }
138
+ //# sourceMappingURL=export.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export.js","sourceRoot":"","sources":["../../src/api/export.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Hand-authored API surface the codegen does not (yet) cover — browser-redirect
3
+ * URL builders, open-redirect guards, narrow domain type-guards, header
4
+ * conventions. Everything that CAN be derived from schema.json belongs in the
5
+ * generated operations (`api/formsApi.ts`), not here.
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=extensions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extensions.d.ts","sourceRoot":"","sources":["../../src/api/extensions.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,CAAC"}