@entifix/react-integration 0.1.0-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 (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -0
  3. package/dist/context/adapters-context/adapters-context.d.ts +4 -0
  4. package/dist/context/adapters-context/adapters-context.d.ts.map +1 -0
  5. package/dist/context/adapters-context/adapters-context.js +19 -0
  6. package/dist/context/adapters-context/adapters-context.js.map +1 -0
  7. package/dist/context/adapters-context/index.d.ts +2 -0
  8. package/dist/context/adapters-context/index.d.ts.map +1 -0
  9. package/dist/context/adapters-context/index.js +3 -0
  10. package/dist/context/adapters-context/index.js.map +1 -0
  11. package/dist/hooks/use-data-loading/index.d.ts +3 -0
  12. package/dist/hooks/use-data-loading/index.d.ts.map +1 -0
  13. package/dist/hooks/use-data-loading/index.js +4 -0
  14. package/dist/hooks/use-data-loading/index.js.map +1 -0
  15. package/dist/hooks/use-data-loading/use-data-loading.d.ts +26 -0
  16. package/dist/hooks/use-data-loading/use-data-loading.d.ts.map +1 -0
  17. package/dist/hooks/use-data-loading/use-data-loading.js +167 -0
  18. package/dist/hooks/use-data-loading/use-data-loading.js.map +1 -0
  19. package/dist/hooks/use-data-loading/use-data-loading.types.d.ts +53 -0
  20. package/dist/hooks/use-data-loading/use-data-loading.types.d.ts.map +1 -0
  21. package/dist/hooks/use-data-loading/use-data-loading.types.js +3 -0
  22. package/dist/hooks/use-data-loading/use-data-loading.types.js.map +1 -0
  23. package/dist/hooks/use-entity-form/index.d.ts +4 -0
  24. package/dist/hooks/use-entity-form/index.d.ts.map +1 -0
  25. package/dist/hooks/use-entity-form/index.js +5 -0
  26. package/dist/hooks/use-entity-form/index.js.map +1 -0
  27. package/dist/hooks/use-entity-form/use-entity-form.d.ts +31 -0
  28. package/dist/hooks/use-entity-form/use-entity-form.d.ts.map +1 -0
  29. package/dist/hooks/use-entity-form/use-entity-form.helpers.d.ts +136 -0
  30. package/dist/hooks/use-entity-form/use-entity-form.helpers.d.ts.map +1 -0
  31. package/dist/hooks/use-entity-form/use-entity-form.helpers.js +337 -0
  32. package/dist/hooks/use-entity-form/use-entity-form.helpers.js.map +1 -0
  33. package/dist/hooks/use-entity-form/use-entity-form.js +209 -0
  34. package/dist/hooks/use-entity-form/use-entity-form.js.map +1 -0
  35. package/dist/hooks/use-entity-form/use-entity-form.types.d.ts +173 -0
  36. package/dist/hooks/use-entity-form/use-entity-form.types.d.ts.map +1 -0
  37. package/dist/hooks/use-entity-form/use-entity-form.types.js +3 -0
  38. package/dist/hooks/use-entity-form/use-entity-form.types.js.map +1 -0
  39. package/dist/hooks/use-entity-link-resolver/index.d.ts +2 -0
  40. package/dist/hooks/use-entity-link-resolver/index.d.ts.map +1 -0
  41. package/dist/hooks/use-entity-link-resolver/index.js +3 -0
  42. package/dist/hooks/use-entity-link-resolver/index.js.map +1 -0
  43. package/dist/hooks/use-entity-link-resolver/use-entity-link-resolver.d.ts +17 -0
  44. package/dist/hooks/use-entity-link-resolver/use-entity-link-resolver.d.ts.map +1 -0
  45. package/dist/hooks/use-entity-link-resolver/use-entity-link-resolver.js +26 -0
  46. package/dist/hooks/use-entity-link-resolver/use-entity-link-resolver.js.map +1 -0
  47. package/dist/hooks/use-entity-link-source/index.d.ts +3 -0
  48. package/dist/hooks/use-entity-link-source/index.d.ts.map +1 -0
  49. package/dist/hooks/use-entity-link-source/index.js +4 -0
  50. package/dist/hooks/use-entity-link-source/index.js.map +1 -0
  51. package/dist/hooks/use-entity-link-source/use-entity-link-source.d.ts +25 -0
  52. package/dist/hooks/use-entity-link-source/use-entity-link-source.d.ts.map +1 -0
  53. package/dist/hooks/use-entity-link-source/use-entity-link-source.js +169 -0
  54. package/dist/hooks/use-entity-link-source/use-entity-link-source.js.map +1 -0
  55. package/dist/hooks/use-entity-link-source/use-entity-link-source.types.d.ts +49 -0
  56. package/dist/hooks/use-entity-link-source/use-entity-link-source.types.d.ts.map +1 -0
  57. package/dist/hooks/use-entity-link-source/use-entity-link-source.types.js +3 -0
  58. package/dist/hooks/use-entity-link-source/use-entity-link-source.types.js.map +1 -0
  59. package/dist/hooks/use-entity-mutation/index.d.ts +3 -0
  60. package/dist/hooks/use-entity-mutation/index.d.ts.map +1 -0
  61. package/dist/hooks/use-entity-mutation/index.js +4 -0
  62. package/dist/hooks/use-entity-mutation/index.js.map +1 -0
  63. package/dist/hooks/use-entity-mutation/use-entity-mutation.d.ts +22 -0
  64. package/dist/hooks/use-entity-mutation/use-entity-mutation.d.ts.map +1 -0
  65. package/dist/hooks/use-entity-mutation/use-entity-mutation.js +88 -0
  66. package/dist/hooks/use-entity-mutation/use-entity-mutation.js.map +1 -0
  67. package/dist/hooks/use-entity-mutation/use-entity-mutation.types.d.ts +19 -0
  68. package/dist/hooks/use-entity-mutation/use-entity-mutation.types.d.ts.map +1 -0
  69. package/dist/hooks/use-entity-mutation/use-entity-mutation.types.js +3 -0
  70. package/dist/hooks/use-entity-mutation/use-entity-mutation.types.js.map +1 -0
  71. package/dist/hooks/use-entity-record/index.d.ts +3 -0
  72. package/dist/hooks/use-entity-record/index.d.ts.map +1 -0
  73. package/dist/hooks/use-entity-record/index.js +4 -0
  74. package/dist/hooks/use-entity-record/index.js.map +1 -0
  75. package/dist/hooks/use-entity-record/use-entity-record.d.ts +21 -0
  76. package/dist/hooks/use-entity-record/use-entity-record.d.ts.map +1 -0
  77. package/dist/hooks/use-entity-record/use-entity-record.js +93 -0
  78. package/dist/hooks/use-entity-record/use-entity-record.js.map +1 -0
  79. package/dist/hooks/use-entity-record/use-entity-record.types.d.ts +22 -0
  80. package/dist/hooks/use-entity-record/use-entity-record.types.d.ts.map +1 -0
  81. package/dist/hooks/use-entity-record/use-entity-record.types.js +3 -0
  82. package/dist/hooks/use-entity-record/use-entity-record.types.js.map +1 -0
  83. package/dist/hooks/use-entity-use-cases/index.d.ts +2 -0
  84. package/dist/hooks/use-entity-use-cases/index.d.ts.map +1 -0
  85. package/dist/hooks/use-entity-use-cases/index.js +3 -0
  86. package/dist/hooks/use-entity-use-cases/index.js.map +1 -0
  87. package/dist/hooks/use-entity-use-cases/use-entity-use-cases.d.ts +25 -0
  88. package/dist/hooks/use-entity-use-cases/use-entity-use-cases.d.ts.map +1 -0
  89. package/dist/hooks/use-entity-use-cases/use-entity-use-cases.js +47 -0
  90. package/dist/hooks/use-entity-use-cases/use-entity-use-cases.js.map +1 -0
  91. package/dist/hooks/use-wizard/index.d.ts +3 -0
  92. package/dist/hooks/use-wizard/index.d.ts.map +1 -0
  93. package/dist/hooks/use-wizard/index.js +4 -0
  94. package/dist/hooks/use-wizard/index.js.map +1 -0
  95. package/dist/hooks/use-wizard/use-wizard.d.ts +19 -0
  96. package/dist/hooks/use-wizard/use-wizard.d.ts.map +1 -0
  97. package/dist/hooks/use-wizard/use-wizard.js +136 -0
  98. package/dist/hooks/use-wizard/use-wizard.js.map +1 -0
  99. package/dist/hooks/use-wizard/use-wizard.types.d.ts +87 -0
  100. package/dist/hooks/use-wizard/use-wizard.types.d.ts.map +1 -0
  101. package/dist/hooks/use-wizard/use-wizard.types.js +3 -0
  102. package/dist/hooks/use-wizard/use-wizard.types.js.map +1 -0
  103. package/dist/index.d.ts +17 -0
  104. package/dist/index.d.ts.map +1 -0
  105. package/dist/index.js +18 -0
  106. package/dist/index.js.map +1 -0
  107. package/dist/query/combine-filtering.d.ts +11 -0
  108. package/dist/query/combine-filtering.d.ts.map +1 -0
  109. package/dist/query/combine-filtering.js +18 -0
  110. package/dist/query/combine-filtering.js.map +1 -0
  111. package/dist/query/entity-query-key.d.ts +28 -0
  112. package/dist/query/entity-query-key.d.ts.map +1 -0
  113. package/dist/query/entity-query-key.js +28 -0
  114. package/dist/query/entity-query-key.js.map +1 -0
  115. package/dist/query/query-provider.d.ts +21 -0
  116. package/dist/query/query-provider.d.ts.map +1 -0
  117. package/dist/query/query-provider.js +35 -0
  118. package/dist/query/query-provider.js.map +1 -0
  119. package/dist/reactive/event-source-channel.d.ts +22 -0
  120. package/dist/reactive/event-source-channel.d.ts.map +1 -0
  121. package/dist/reactive/event-source-channel.js +93 -0
  122. package/dist/reactive/event-source-channel.js.map +1 -0
  123. package/dist/reactive/reactive-channel.d.ts +75 -0
  124. package/dist/reactive/reactive-channel.d.ts.map +1 -0
  125. package/dist/reactive/reactive-channel.js +55 -0
  126. package/dist/reactive/reactive-channel.js.map +1 -0
  127. package/dist/reactive/use-reactive-invalidation.d.ts +10 -0
  128. package/dist/reactive/use-reactive-invalidation.d.ts.map +1 -0
  129. package/dist/reactive/use-reactive-invalidation.js +25 -0
  130. package/dist/reactive/use-reactive-invalidation.js.map +1 -0
  131. package/dist/reactive/use-transaction-settlement.d.ts +41 -0
  132. package/dist/reactive/use-transaction-settlement.d.ts.map +1 -0
  133. package/dist/reactive/use-transaction-settlement.js +123 -0
  134. package/dist/reactive/use-transaction-settlement.js.map +1 -0
  135. package/package.json +73 -0
@@ -0,0 +1,337 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { describeChildColumns, editableChildColumns, EntifixLogicError, EntityCollectionLink, EntityLink, isRowDraftArray, joinFieldPath, readDraftString, readRowDrafts, ROW_KEY, rowFieldPath, seededRowKey } from '@entifix/core';
3
+ /**
4
+ * A member no form writes back, whatever the draft says about it — and the only
5
+ * one is a read-only member, because it is the only one no editor may ever
6
+ * exist for.
7
+ *
8
+ * `linkCollection` used to be here too, which meant `required` on a to-many
9
+ * relation was silently unenforced: a rule the entity declared and the form
10
+ * never applied. Whether a collection has an editor yet is a question about the
11
+ * controls, not about what the domain insists on, so it is excluded from the
12
+ * *format* check below instead.
13
+ */ function isNeverEdited(descriptor) {
14
+ return descriptor.readonly;
15
+ }
16
+ /**
17
+ * A member whose *format* the metadata can judge. Both relation shapes are
18
+ * excluded: their draft value is a foreign key (or a joined list of them), and
19
+ * "is this a real id" is a question only the service can answer — but whether
20
+ * one is present at all is checked, which is what `required` on a relation has
21
+ * to mean.
22
+ *
23
+ * A `composition` is excluded here too, but it is no longer unjudged: its rows
24
+ * are checked one at a time by {@link validateRowDrafts}, against the child's
25
+ * own descriptors. What has no meaning is a *format* rule on the collection
26
+ * itself — there is no string to judge, only rows.
27
+ *
28
+ * `scalarCollection` is deliberately *not* excluded — its comma list is real
29
+ * text the user typed.
30
+ */ function hasCheckableFormat(descriptor) {
31
+ return descriptor.type !== 'link' && descriptor.type !== 'linkCollection' && descriptor.type !== 'composition';
32
+ }
33
+ /**
34
+ * The value a field seeds with from a record.
35
+ *
36
+ * A scalar seeds as a **string**: links with their foreign key(s), dates with a
37
+ * `yyyy-mm-dd` value a `date` input accepts, everything else stringified.
38
+ *
39
+ * The array branch is explicit rather than left to `String(raw)`, even though
40
+ * the two produce the same characters today. `String(['a','b'])` is `'a,b'` by
41
+ * way of `Array.prototype.toString`, which is an accident: it was doing the
42
+ * work of a `scalarCollection` seed before the type existed, and
43
+ * `reconstructEntity` was handing the same string straight back as a `string`.
44
+ * Both halves were wrong in the same direction, so the fixed-point spec could
45
+ * not see it. Now the join is declared here and the split is declared there,
46
+ * and they are inverses on purpose.
47
+ *
48
+ * A **`composition`** is the one member that does not seed as a string, because
49
+ * it has no lossless string form at all — its rows seed as row drafts, each
50
+ * with a freshly minted key. That key is the only thing here the record does not
51
+ * supply, and it is minted at seed time rather than at render so that the rows a
52
+ * form starts with are already addressable.
53
+ */ export function seedFieldValue(descriptor, entity) {
54
+ if (descriptor.type === 'composition') {
55
+ return seedRowDrafts(descriptor, entity);
56
+ }
57
+ return seedScalarValue(descriptor, entity);
58
+ }
59
+ /** {@link seedFieldValue} for every member that round-trips through a string. */ function seedScalarValue(descriptor, entity) {
60
+ if (entity == null) return '';
61
+ const raw = entity[descriptor.name];
62
+ if (raw == null) return '';
63
+ if (raw instanceof EntityLink) return raw.id == null ? '' : String(raw.id);
64
+ if (raw instanceof EntityCollectionLink) return raw.ids.map(String).join(',');
65
+ if (raw instanceof Date) return raw.toISOString().slice(0, 10);
66
+ if (Array.isArray(raw)) return raw.map(String).join(',');
67
+ return String(raw);
68
+ }
69
+ /**
70
+ * The rows a composition member seeds with.
71
+ *
72
+ * Each row is walked with the child's **own** descriptors, through the same
73
+ * `seedScalarValue` the master's members use — so a child's `date` seeds as
74
+ * `yyyy-mm-dd` on a line exactly as it would on a record, and
75
+ * `reconstructChild` is its inverse for the same reason `coerceFieldValue` is.
76
+ *
77
+ * A row arrives as a plain object as readily as an instance (a payload off the
78
+ * wire, a fixture), which is fine: nothing here asks what class it is, matching
79
+ * `ChildConstructor`'s rule that a child is never `instanceof`-tested.
80
+ *
81
+ * A member with no declared child seeds as no rows — there are no columns to
82
+ * walk, so there is nothing a row could be.
83
+ */ function seedRowDrafts(descriptor, entity) {
84
+ if (entity == null || descriptor.childType === undefined) return [];
85
+ const raw = entity[descriptor.name];
86
+ if (!Array.isArray(raw)) return [];
87
+ const columns = describeChildColumns(descriptor.childType);
88
+ return raw.map((row, index)=>{
89
+ // Deterministic, so re-seeding the same record produces the same draft —
90
+ // see `seededRowKey` for the render loop the random one causes.
91
+ const draft = {
92
+ [ROW_KEY]: seededRowKey(index)
93
+ };
94
+ for (const column of columns){
95
+ draft[column.name] = seedScalarValue(column, row);
96
+ }
97
+ return draft;
98
+ });
99
+ }
100
+ /** Builds the initial draft for a record (or an empty one for a create). */ export function seedEntityDraft(descriptors, entity) {
101
+ const draft = {};
102
+ for (const descriptor of descriptors){
103
+ draft[descriptor.name] = seedFieldValue(descriptor, entity);
104
+ }
105
+ return draft;
106
+ }
107
+ /**
108
+ * Restores a persisted draft **over** a freshly seeded one, rather than in place
109
+ * of it.
110
+ *
111
+ * A draft outlives the entity that wrote it — it sits in IndexedDB across
112
+ * refreshes and deployments — so the two can disagree by the time it comes back.
113
+ * Replacing the seed outright is how that disagreement becomes a bug the user
114
+ * sees: a member added since the draft was written has no entry, reaches the form
115
+ * engine as `undefined`, and its input flips from controlled to uncontrolled
116
+ * mid-render. A member since removed goes the other way, surviving as a value
117
+ * nothing renders and `reconstructEntity` cannot assign.
118
+ *
119
+ * So the seed decides the *keys* and the draft decides the *values*: a name the
120
+ * entity no longer declares is dropped, and a name it declares that the draft
121
+ * lacks keeps its seeded value.
122
+ *
123
+ * **A value is restored only in the shape its member can hold**, which is now
124
+ * two shapes rather than one. A scalar takes a string; a `composition` takes a
125
+ * readable row list. Anything else is dropped back to its seed — a member that
126
+ * changed type between the write and the read, or a row list from a build that
127
+ * did not carry keys, restores as the record's own value instead of as
128
+ * something no editor can render. This is per member, so one unreadable entry
129
+ * never costs the user the rest of the form.
130
+ *
131
+ * This is the shape guard at the one place the shape is actually known. The
132
+ * store-level version check (`drafts-state.ts`) can only decide that a whole
133
+ * generation of drafts is unreadable; it cannot know which members one entity
134
+ * declares.
135
+ */ export function restoreEntityDraft(descriptors, seed, persisted) {
136
+ if (persisted === undefined) return seed;
137
+ const restored = _extends({}, seed);
138
+ for (const descriptor of descriptors){
139
+ const value = persisted[descriptor.name];
140
+ if (value === undefined) continue;
141
+ if (descriptor.type === 'composition') {
142
+ if (isRowDraftArray(value)) restored[descriptor.name] = value;
143
+ continue;
144
+ }
145
+ if (typeof value === 'string') restored[descriptor.name] = value;
146
+ }
147
+ return restored;
148
+ }
149
+ /**
150
+ * Flattens the form engine's per-field state into the `name → message` map the
151
+ * agnostic `EntityForm` renders. Only the first message per field survives: a
152
+ * row shows one line, and the metadata rules already stop at the first failure.
153
+ *
154
+ * Kept structural rather than typed against the engine so the rest of this
155
+ * module stays independent of which form library backs the hook.
156
+ */ export function readFieldErrors(fieldMeta) {
157
+ const errors = {};
158
+ for (const [name, meta] of Object.entries(fieldMeta)){
159
+ var _meta_errors;
160
+ const message = meta == null ? void 0 : (_meta_errors = meta.errors) == null ? void 0 : _meta_errors.find((entry)=>typeof entry === 'string');
161
+ if (typeof message === 'string') errors[name] = message;
162
+ }
163
+ return errors;
164
+ }
165
+ /**
166
+ * Validates a draft against what the metadata implies — `required` members must
167
+ * be filled, and a filled `number`/`date`/`enum` must be well-formed. Read-only
168
+ * members are skipped entirely; a relation is checked for presence but never
169
+ * for format, since its draft value is a foreign key this side cannot resolve.
170
+ *
171
+ * Metadata is only the first of three rule sources; {@link composeEntityFormErrors}
172
+ * is what layers a schema and the caller's own rules over this.
173
+ */ export function validateEntityDraft(descriptors, values, messages) {
174
+ const errors = {};
175
+ for (const descriptor of descriptors){
176
+ if (isNeverEdited(descriptor)) continue;
177
+ if (descriptor.type === 'composition') {
178
+ Object.assign(errors, validateComposition(descriptor, values, messages));
179
+ continue;
180
+ }
181
+ const raw = readDraftString(values, descriptor.name);
182
+ if (descriptor.required && raw.trim() === '') {
183
+ errors[descriptor.name] = messages.required(descriptor.label);
184
+ continue;
185
+ }
186
+ if (raw === '' || !hasCheckableFormat(descriptor)) continue;
187
+ const message = formatMessage(descriptor, raw, messages);
188
+ if (message !== undefined) errors[descriptor.name] = message;
189
+ }
190
+ return errors;
191
+ }
192
+ /**
193
+ * The metadata rule for one filled scalar draft value, or `undefined` when it
194
+ * passes.
195
+ *
196
+ * Extracted so a row's cell is judged by the **same** rule as a record's field.
197
+ * Two copies is how a child's `number` member would quietly start accepting
198
+ * something its master's would reject.
199
+ */ function formatMessage(descriptor, raw, messages) {
200
+ if (descriptor.type === 'number' && Number.isNaN(Number(raw))) {
201
+ return messages.number(descriptor.label);
202
+ }
203
+ if (descriptor.type === 'date' && Number.isNaN(new Date(raw).getTime())) {
204
+ return messages.date(descriptor.label);
205
+ }
206
+ if (descriptor.type === 'enum' && descriptor.enumValues && !descriptor.enumValues.includes(raw)) {
207
+ return messages.option(descriptor.label);
208
+ }
209
+ return undefined;
210
+ }
211
+ /**
212
+ * The rules an owned collection carries, at both of its levels.
213
+ *
214
+ * **`required` on the collection means "at least one row"** — the only thing a
215
+ * master can assert about a collection it owns, and a genuinely different fact
216
+ * from `required` on a child member, which is per row. Conflating the two would
217
+ * make an order with three lines, one of them blank, indistinguishable from an
218
+ * order with none.
219
+ *
220
+ * A row's own members are judged against the child's descriptors and reported at
221
+ * `items[2].quantity`, so the grid can put the message in the cell that caused
222
+ * it. Read-only child members are skipped for the reason they always are: no
223
+ * editor exists for them, so no rule the user could satisfy applies.
224
+ */ function validateComposition(descriptor, values, messages) {
225
+ const errors = {};
226
+ const rows = readRowDrafts(values[descriptor.name]);
227
+ if (descriptor.required && rows.length === 0) {
228
+ errors[descriptor.name] = messages.required(descriptor.label);
229
+ return errors;
230
+ }
231
+ if (descriptor.childType === undefined) return errors;
232
+ const columns = editableChildColumns(describeChildColumns(descriptor.childType));
233
+ rows.forEach((row, index)=>{
234
+ for (const column of columns){
235
+ var _row_column_name;
236
+ if (isNeverEdited(column)) continue;
237
+ const raw = (_row_column_name = row[column.name]) != null ? _row_column_name : '';
238
+ const path = rowFieldPath(descriptor.name, index, column.name);
239
+ if (column.required && raw.trim() === '') {
240
+ errors[path] = messages.required(column.label);
241
+ continue;
242
+ }
243
+ if (raw === '') continue;
244
+ const message = formatMessage(column, raw, messages);
245
+ if (message !== undefined) errors[path] = message;
246
+ }
247
+ });
248
+ return errors;
249
+ }
250
+ /**
251
+ * The error-map key an issue belongs to — the **whole** path, not its head.
252
+ *
253
+ * It used to read `issue.path?.[0]`, which was right while every member was a
254
+ * scalar and wrong the moment one held rows: `['items', 2, 'quantity']`
255
+ * collapsed to `'items'`, so a rule about the third line's quantity reported
256
+ * itself against the collection and the grid had no way to know which cell it
257
+ * meant. Joining instead produces `items[2].quantity`, which is the key
258
+ * `validateComposition` already writes — so a schema rule and a metadata rule
259
+ * address the same cell the same way and `composeEntityFormErrors` can merge
260
+ * them.
261
+ *
262
+ * A one-segment path still yields exactly the head, so no existing rule changes
263
+ * meaning.
264
+ */ function issueFieldName(issue) {
265
+ const path = issue.path;
266
+ if (path == null || path.length === 0) return undefined;
267
+ const segments = path.map((segment)=>{
268
+ const raw = typeof segment === 'object' && segment !== null && 'key' in segment ? segment.key : segment;
269
+ return typeof raw === 'number' ? raw : String(raw);
270
+ });
271
+ return joinFieldPath(segments);
272
+ }
273
+ /**
274
+ * Runs a Standard Schema over the draft and sorts its issues into per-field
275
+ * messages plus a form-level one (an issue with no path — a cross-field rule).
276
+ *
277
+ * **Schemas are authored against the string draft**, so they coerce
278
+ * (`z.coerce.number()`) rather than expect typed values: the draft is what the
279
+ * inputs round-trip, and the same schema then validates exactly what the user
280
+ * sees. Only the first issue per field survives, matching how a row renders.
281
+ *
282
+ * Synchronous rules only. An async schema returns a Promise the form's
283
+ * synchronous validation pass cannot wait on, so it is rejected loudly rather
284
+ * than silently passing.
285
+ */ export function readSchemaIssues(schema, values, translateIssue) {
286
+ const result = schema['~standard'].validate(values);
287
+ if (result instanceof Promise) {
288
+ throw new EntifixLogicError('useEntityForm cannot run an async schema: its validation pass is ' + 'synchronous. Move the asynchronous rule into the `validate` option.');
289
+ }
290
+ if (!result.issues) return {
291
+ fields: {}
292
+ };
293
+ const fields = {};
294
+ let form;
295
+ for (const issue of result.issues){
296
+ var _fields, _name, _;
297
+ const name = issueFieldName(issue);
298
+ const message = translateIssue(issue.message, name);
299
+ if (name === undefined) form != null ? form : form = message;
300
+ else (_ = (_fields = fields)[_name = name]) != null ? _ : _fields[_name] = message;
301
+ }
302
+ return {
303
+ fields,
304
+ form
305
+ };
306
+ }
307
+ /**
308
+ * The single composition point for a draft's errors: metadata rules first, then
309
+ * the entity's schema, then the caller's `validate` callback. Later sources win
310
+ * on conflict, so a caller can always override a message it disagrees with.
311
+ */ export function composeEntityFormErrors({ descriptors, values, messages, schema, translateIssue, validate }) {
312
+ const metadata = validateEntityDraft(descriptors, values, messages);
313
+ const schemaIssues = schema ? readSchemaIssues(schema, values, translateIssue) : {
314
+ fields: {},
315
+ form: undefined
316
+ };
317
+ return {
318
+ fields: _extends({}, metadata, schemaIssues.fields, validate == null ? void 0 : validate(values)),
319
+ form: schemaIssues.form
320
+ };
321
+ }
322
+ /**
323
+ * What a caller who supplies no {@link ValidationMessages} gets: the catalog key
324
+ * itself, with the field appended.
325
+ *
326
+ * Honest rather than English — there is no catalog in this package to resolve
327
+ * against — and visible enough in a form that a host notices it has wired no
328
+ * copy. It sits beside the validator rather than in `.types.ts`, which this
329
+ * repository keeps type-only.
330
+ */ export const DEFAULT_VALIDATION_MESSAGES = {
331
+ required: (field)=>`validation.required:${field}`,
332
+ number: (field)=>`validation.number:${field}`,
333
+ date: (field)=>`validation.date:${field}`,
334
+ option: (field)=>`validation.option:${field}`
335
+ };
336
+
337
+ //# sourceMappingURL=use-entity-form.helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/hooks/use-entity-form/use-entity-form.helpers.ts"],"sourcesContent":["import {\n describeChildColumns,\n editableChildColumns,\n EntifixLogicError,\n EntityCollectionLink,\n type EntityDraft,\n type EntityDraftValue,\n type EntityFieldDescriptor,\n EntityLink,\n type EntityRowDraft,\n isRowDraftArray,\n joinFieldPath,\n readDraftString,\n readRowDrafts,\n ROW_KEY,\n rowFieldPath,\n seededRowKey,\n type StandardSchemaV1,\n type StandardSchemaV1Issue,\n} from '@entifix/core';\n\n/**\n * A member no form writes back, whatever the draft says about it — and the only\n * one is a read-only member, because it is the only one no editor may ever\n * exist for.\n *\n * `linkCollection` used to be here too, which meant `required` on a to-many\n * relation was silently unenforced: a rule the entity declared and the form\n * never applied. Whether a collection has an editor yet is a question about the\n * controls, not about what the domain insists on, so it is excluded from the\n * *format* check below instead.\n */\nfunction isNeverEdited(descriptor: EntityFieldDescriptor): boolean {\n return descriptor.readonly;\n}\n\n/**\n * A member whose *format* the metadata can judge. Both relation shapes are\n * excluded: their draft value is a foreign key (or a joined list of them), and\n * \"is this a real id\" is a question only the service can answer — but whether\n * one is present at all is checked, which is what `required` on a relation has\n * to mean.\n *\n * A `composition` is excluded here too, but it is no longer unjudged: its rows\n * are checked one at a time by {@link validateRowDrafts}, against the child's\n * own descriptors. What has no meaning is a *format* rule on the collection\n * itself — there is no string to judge, only rows.\n *\n * `scalarCollection` is deliberately *not* excluded — its comma list is real\n * text the user typed.\n */\nfunction hasCheckableFormat(descriptor: EntityFieldDescriptor): boolean {\n return (\n descriptor.type !== 'link' &&\n descriptor.type !== 'linkCollection' &&\n descriptor.type !== 'composition'\n );\n}\n\n/**\n * The value a field seeds with from a record.\n *\n * A scalar seeds as a **string**: links with their foreign key(s), dates with a\n * `yyyy-mm-dd` value a `date` input accepts, everything else stringified.\n *\n * The array branch is explicit rather than left to `String(raw)`, even though\n * the two produce the same characters today. `String(['a','b'])` is `'a,b'` by\n * way of `Array.prototype.toString`, which is an accident: it was doing the\n * work of a `scalarCollection` seed before the type existed, and\n * `reconstructEntity` was handing the same string straight back as a `string`.\n * Both halves were wrong in the same direction, so the fixed-point spec could\n * not see it. Now the join is declared here and the split is declared there,\n * and they are inverses on purpose.\n *\n * A **`composition`** is the one member that does not seed as a string, because\n * it has no lossless string form at all — its rows seed as row drafts, each\n * with a freshly minted key. That key is the only thing here the record does not\n * supply, and it is minted at seed time rather than at render so that the rows a\n * form starts with are already addressable.\n */\nexport function seedFieldValue(\n descriptor: EntityFieldDescriptor,\n entity: unknown,\n): EntityDraftValue {\n if (descriptor.type === 'composition') {\n return seedRowDrafts(descriptor, entity);\n }\n return seedScalarValue(descriptor, entity);\n}\n\n/** {@link seedFieldValue} for every member that round-trips through a string. */\nfunction seedScalarValue(\n descriptor: EntityFieldDescriptor,\n entity: unknown,\n): string {\n if (entity == null) return '';\n const raw = (entity as Record<string, unknown>)[descriptor.name];\n if (raw == null) return '';\n if (raw instanceof EntityLink) return raw.id == null ? '' : String(raw.id);\n if (raw instanceof EntityCollectionLink) return raw.ids.map(String).join(',');\n if (raw instanceof Date) return raw.toISOString().slice(0, 10);\n if (Array.isArray(raw)) return raw.map(String).join(',');\n return String(raw);\n}\n\n/**\n * The rows a composition member seeds with.\n *\n * Each row is walked with the child's **own** descriptors, through the same\n * `seedScalarValue` the master's members use — so a child's `date` seeds as\n * `yyyy-mm-dd` on a line exactly as it would on a record, and\n * `reconstructChild` is its inverse for the same reason `coerceFieldValue` is.\n *\n * A row arrives as a plain object as readily as an instance (a payload off the\n * wire, a fixture), which is fine: nothing here asks what class it is, matching\n * `ChildConstructor`'s rule that a child is never `instanceof`-tested.\n *\n * A member with no declared child seeds as no rows — there are no columns to\n * walk, so there is nothing a row could be.\n */\nfunction seedRowDrafts(\n descriptor: EntityFieldDescriptor,\n entity: unknown,\n): EntityRowDraft[] {\n if (entity == null || descriptor.childType === undefined) return [];\n const raw = (entity as Record<string, unknown>)[descriptor.name];\n if (!Array.isArray(raw)) return [];\n\n const columns = describeChildColumns(descriptor.childType);\n\n return raw.map((row, index) => {\n // Deterministic, so re-seeding the same record produces the same draft —\n // see `seededRowKey` for the render loop the random one causes.\n const draft: EntityRowDraft = { [ROW_KEY]: seededRowKey(index) };\n for (const column of columns) {\n draft[column.name] = seedScalarValue(column, row);\n }\n return draft;\n });\n}\n\n/** Builds the initial draft for a record (or an empty one for a create). */\nexport function seedEntityDraft(\n descriptors: readonly EntityFieldDescriptor[],\n entity: unknown,\n): EntityDraft {\n const draft: EntityDraft = {};\n for (const descriptor of descriptors) {\n draft[descriptor.name] = seedFieldValue(descriptor, entity);\n }\n return draft;\n}\n\n/**\n * Restores a persisted draft **over** a freshly seeded one, rather than in place\n * of it.\n *\n * A draft outlives the entity that wrote it — it sits in IndexedDB across\n * refreshes and deployments — so the two can disagree by the time it comes back.\n * Replacing the seed outright is how that disagreement becomes a bug the user\n * sees: a member added since the draft was written has no entry, reaches the form\n * engine as `undefined`, and its input flips from controlled to uncontrolled\n * mid-render. A member since removed goes the other way, surviving as a value\n * nothing renders and `reconstructEntity` cannot assign.\n *\n * So the seed decides the *keys* and the draft decides the *values*: a name the\n * entity no longer declares is dropped, and a name it declares that the draft\n * lacks keeps its seeded value.\n *\n * **A value is restored only in the shape its member can hold**, which is now\n * two shapes rather than one. A scalar takes a string; a `composition` takes a\n * readable row list. Anything else is dropped back to its seed — a member that\n * changed type between the write and the read, or a row list from a build that\n * did not carry keys, restores as the record's own value instead of as\n * something no editor can render. This is per member, so one unreadable entry\n * never costs the user the rest of the form.\n *\n * This is the shape guard at the one place the shape is actually known. The\n * store-level version check (`drafts-state.ts`) can only decide that a whole\n * generation of drafts is unreadable; it cannot know which members one entity\n * declares.\n */\nexport function restoreEntityDraft(\n descriptors: readonly EntityFieldDescriptor[],\n seed: EntityDraft,\n persisted: EntityDraft | undefined,\n): EntityDraft {\n if (persisted === undefined) return seed;\n\n const restored: EntityDraft = { ...seed };\n for (const descriptor of descriptors) {\n const value = persisted[descriptor.name];\n if (value === undefined) continue;\n\n if (descriptor.type === 'composition') {\n if (isRowDraftArray(value)) restored[descriptor.name] = value;\n continue;\n }\n if (typeof value === 'string') restored[descriptor.name] = value;\n }\n return restored;\n}\n\n/** The slice of a form field's state this module reads: its error list. */\nexport interface FieldErrorMeta {\n errors?: readonly unknown[];\n}\n\n/**\n * Flattens the form engine's per-field state into the `name → message` map the\n * agnostic `EntityForm` renders. Only the first message per field survives: a\n * row shows one line, and the metadata rules already stop at the first failure.\n *\n * Kept structural rather than typed against the engine so the rest of this\n * module stays independent of which form library backs the hook.\n */\nexport function readFieldErrors(\n fieldMeta: Partial<Record<string, FieldErrorMeta | undefined>>,\n): Record<string, string> {\n const errors: Record<string, string> = {};\n\n for (const [name, meta] of Object.entries(fieldMeta)) {\n const message = meta?.errors?.find(entry => typeof entry === 'string');\n if (typeof message === 'string') errors[name] = message;\n }\n\n return errors;\n}\n\n/**\n * The four metadata-derived messages, already localized. Taken as an argument\n * rather than built here so this stays a pure function — and because the\n * sentence cannot be assembled from a label plus a hardcoded suffix: word order\n * differs between locales.\n */\nexport interface EntityDraftMessages {\n required(field: string): string;\n number(field: string): string;\n date(field: string): string;\n option(field: string): string;\n}\n\n/**\n * Validates a draft against what the metadata implies — `required` members must\n * be filled, and a filled `number`/`date`/`enum` must be well-formed. Read-only\n * members are skipped entirely; a relation is checked for presence but never\n * for format, since its draft value is a foreign key this side cannot resolve.\n *\n * Metadata is only the first of three rule sources; {@link composeEntityFormErrors}\n * is what layers a schema and the caller's own rules over this.\n */\nexport function validateEntityDraft(\n descriptors: readonly EntityFieldDescriptor[],\n values: EntityDraft,\n messages: EntityDraftMessages,\n): Record<string, string> {\n const errors: Record<string, string> = {};\n\n for (const descriptor of descriptors) {\n if (isNeverEdited(descriptor)) continue;\n\n if (descriptor.type === 'composition') {\n Object.assign(errors, validateComposition(descriptor, values, messages));\n continue;\n }\n\n const raw = readDraftString(values, descriptor.name);\n\n if (descriptor.required && raw.trim() === '') {\n errors[descriptor.name] = messages.required(descriptor.label);\n continue;\n }\n if (raw === '' || !hasCheckableFormat(descriptor)) continue;\n\n const message = formatMessage(descriptor, raw, messages);\n if (message !== undefined) errors[descriptor.name] = message;\n }\n\n return errors;\n}\n\n/**\n * The metadata rule for one filled scalar draft value, or `undefined` when it\n * passes.\n *\n * Extracted so a row's cell is judged by the **same** rule as a record's field.\n * Two copies is how a child's `number` member would quietly start accepting\n * something its master's would reject.\n */\nfunction formatMessage(\n descriptor: EntityFieldDescriptor,\n raw: string,\n messages: EntityDraftMessages,\n): string | undefined {\n if (descriptor.type === 'number' && Number.isNaN(Number(raw))) {\n return messages.number(descriptor.label);\n }\n if (descriptor.type === 'date' && Number.isNaN(new Date(raw).getTime())) {\n return messages.date(descriptor.label);\n }\n if (\n descriptor.type === 'enum' &&\n descriptor.enumValues &&\n !descriptor.enumValues.includes(raw)\n ) {\n return messages.option(descriptor.label);\n }\n return undefined;\n}\n\n/**\n * The rules an owned collection carries, at both of its levels.\n *\n * **`required` on the collection means \"at least one row\"** — the only thing a\n * master can assert about a collection it owns, and a genuinely different fact\n * from `required` on a child member, which is per row. Conflating the two would\n * make an order with three lines, one of them blank, indistinguishable from an\n * order with none.\n *\n * A row's own members are judged against the child's descriptors and reported at\n * `items[2].quantity`, so the grid can put the message in the cell that caused\n * it. Read-only child members are skipped for the reason they always are: no\n * editor exists for them, so no rule the user could satisfy applies.\n */\nfunction validateComposition(\n descriptor: EntityFieldDescriptor,\n values: EntityDraft,\n messages: EntityDraftMessages,\n): Record<string, string> {\n const errors: Record<string, string> = {};\n const rows = readRowDrafts(values[descriptor.name]);\n\n if (descriptor.required && rows.length === 0) {\n errors[descriptor.name] = messages.required(descriptor.label);\n return errors;\n }\n if (descriptor.childType === undefined) return errors;\n\n const columns = editableChildColumns(\n describeChildColumns(descriptor.childType),\n );\n\n rows.forEach((row, index) => {\n for (const column of columns) {\n if (isNeverEdited(column)) continue;\n\n const raw = row[column.name] ?? '';\n const path = rowFieldPath(descriptor.name, index, column.name);\n\n if (column.required && raw.trim() === '') {\n errors[path] = messages.required(column.label);\n continue;\n }\n if (raw === '') continue;\n\n const message = formatMessage(column, raw, messages);\n if (message !== undefined) errors[path] = message;\n }\n });\n\n return errors;\n}\n\n/**\n * The error-map key an issue belongs to — the **whole** path, not its head.\n *\n * It used to read `issue.path?.[0]`, which was right while every member was a\n * scalar and wrong the moment one held rows: `['items', 2, 'quantity']`\n * collapsed to `'items'`, so a rule about the third line's quantity reported\n * itself against the collection and the grid had no way to know which cell it\n * meant. Joining instead produces `items[2].quantity`, which is the key\n * `validateComposition` already writes — so a schema rule and a metadata rule\n * address the same cell the same way and `composeEntityFormErrors` can merge\n * them.\n *\n * A one-segment path still yields exactly the head, so no existing rule changes\n * meaning.\n */\nfunction issueFieldName(issue: StandardSchemaV1Issue): string | undefined {\n const path = issue.path;\n if (path == null || path.length === 0) return undefined;\n\n const segments = path.map(segment => {\n const raw =\n typeof segment === 'object' && segment !== null && 'key' in segment\n ? segment.key\n : segment;\n return typeof raw === 'number' ? raw : String(raw);\n });\n\n return joinFieldPath(segments);\n}\n\n/**\n * Runs a Standard Schema over the draft and sorts its issues into per-field\n * messages plus a form-level one (an issue with no path — a cross-field rule).\n *\n * **Schemas are authored against the string draft**, so they coerce\n * (`z.coerce.number()`) rather than expect typed values: the draft is what the\n * inputs round-trip, and the same schema then validates exactly what the user\n * sees. Only the first issue per field survives, matching how a row renders.\n *\n * Synchronous rules only. An async schema returns a Promise the form's\n * synchronous validation pass cannot wait on, so it is rejected loudly rather\n * than silently passing.\n */\nexport function readSchemaIssues(\n schema: StandardSchemaV1,\n values: EntityDraft,\n translateIssue: (message: string, field: string | undefined) => string,\n): { fields: Record<string, string>; form?: string } {\n const result = schema['~standard'].validate(values);\n\n if (result instanceof Promise) {\n throw new EntifixLogicError(\n 'useEntityForm cannot run an async schema: its validation pass is ' +\n 'synchronous. Move the asynchronous rule into the `validate` option.',\n );\n }\n if (!result.issues) return { fields: {} };\n\n const fields: Record<string, string> = {};\n let form: string | undefined;\n\n for (const issue of result.issues) {\n const name = issueFieldName(issue);\n const message = translateIssue(issue.message, name);\n\n if (name === undefined) form ??= message;\n else fields[name] ??= message;\n }\n\n return { fields, form };\n}\n\n/** Everything {@link composeEntityFormErrors} needs to judge one draft. */\nexport interface ComposeEntityFormErrorsOptions {\n descriptors: readonly EntityFieldDescriptor[];\n values: EntityDraft;\n messages: EntityDraftMessages;\n schema?: StandardSchemaV1;\n translateIssue: (message: string, field: string | undefined) => string;\n validate?: (values: EntityDraft) => Record<string, string>;\n}\n\n/**\n * The single composition point for a draft's errors: metadata rules first, then\n * the entity's schema, then the caller's `validate` callback. Later sources win\n * on conflict, so a caller can always override a message it disagrees with.\n */\nexport function composeEntityFormErrors({\n descriptors,\n values,\n messages,\n schema,\n translateIssue,\n validate,\n}: ComposeEntityFormErrorsOptions): {\n fields: Record<string, string>;\n form?: string;\n} {\n const metadata = validateEntityDraft(descriptors, values, messages);\n const schemaIssues = schema\n ? readSchemaIssues(schema, values, translateIssue)\n : { fields: {}, form: undefined };\n\n return {\n fields: { ...metadata, ...schemaIssues.fields, ...validate?.(values) },\n form: schemaIssues.form,\n };\n}\n\n/**\n * What a caller who supplies no {@link ValidationMessages} gets: the catalog key\n * itself, with the field appended.\n *\n * Honest rather than English — there is no catalog in this package to resolve\n * against — and visible enough in a form that a host notices it has wired no\n * copy. It sits beside the validator rather than in `.types.ts`, which this\n * repository keeps type-only.\n */\nexport const DEFAULT_VALIDATION_MESSAGES: EntityDraftMessages = {\n required: field => `validation.required:${field}`,\n number: field => `validation.number:${field}`,\n date: field => `validation.date:${field}`,\n option: field => `validation.option:${field}`,\n};\n"],"names":["describeChildColumns","editableChildColumns","EntifixLogicError","EntityCollectionLink","EntityLink","isRowDraftArray","joinFieldPath","readDraftString","readRowDrafts","ROW_KEY","rowFieldPath","seededRowKey","isNeverEdited","descriptor","readonly","hasCheckableFormat","type","seedFieldValue","entity","seedRowDrafts","seedScalarValue","raw","name","id","String","ids","map","join","Date","toISOString","slice","Array","isArray","childType","undefined","columns","row","index","draft","column","seedEntityDraft","descriptors","restoreEntityDraft","seed","persisted","restored","value","readFieldErrors","fieldMeta","errors","meta","Object","entries","message","find","entry","validateEntityDraft","values","messages","assign","validateComposition","required","trim","label","formatMessage","Number","isNaN","number","getTime","date","enumValues","includes","option","rows","length","forEach","path","issueFieldName","issue","segments","segment","key","readSchemaIssues","schema","translateIssue","result","validate","Promise","issues","fields","form","composeEntityFormErrors","metadata","schemaIssues","DEFAULT_VALIDATION_MESSAGES","field"],"mappings":"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"}
@@ -0,0 +1,209 @@
1
+ 'use client';
2
+ import { _ as _extends } from "@swc/helpers/_/_extends";
3
+ import { describeEntityColumns, seedEntityLinkSelection } from '@entifix/core';
4
+ import { revalidateLogic, useForm, useStore } from '@tanstack/react-form';
5
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
+ import { composeEntityFormErrors, DEFAULT_VALIDATION_MESSAGES, readFieldErrors, restoreEntityDraft, seedEntityDraft } from './use-entity-form.helpers.js';
7
+ /**
8
+ * The write half of an entity form: holds the string draft and reports
9
+ * metadata-derived validation, the counterpart to {@link useEntityRecord}
10
+ * (which reads the record it seeds from).
11
+ *
12
+ * It stays out of the transport: it neither fetches nor saves. `onSubmit`
13
+ * receives the validated draft, and the per-entity wrapper reconstructs the
14
+ * domain entity and calls the mutation. The result plugs straight into the
15
+ * agnostic `EntityForm` (`values`/`onFieldChange`/`errors`/`onSubmit`).
16
+ *
17
+ * TanStack Form owns the draft and the submission lifecycle; that is an
18
+ * implementation detail, and the returned shape is deliberately the plain
19
+ * `values`/`errors`/`setField`/`submit`/`isDirty` facade so `EntityForm` — which
20
+ * renders native inputs and knows nothing about any form library — stays
21
+ * agnostic. Fields are therefore never registered through `form.Field`; a
22
+ * form-level validator reporting `{ fields }` reaches them anyway, because
23
+ * `FormApi` creates meta for any field name an error arrives for.
24
+ *
25
+ * `revalidateLogic` is what makes a pristine form quiet: nothing validates until
26
+ * the first submit, and every keystroke revalidates after it. Running one
27
+ * validator under a single cause also keeps a fixed field from holding a stale
28
+ * message left behind by another cause.
29
+ *
30
+ * The seed applies once, so a caller that loads its record after mount keys the
31
+ * form by the record id to reseed — the same convention `EntityForm`'s wrappers
32
+ * already follow.
33
+ */ export function useEntityForm({ entityConstructor, entity, draft, fields, schema, validate, validationMessages, translateKey, onSubmit }) {
34
+ // Keyed on the **names**, not the array, so a caller may write the list inline
35
+ // — which every wizard step does, since its field set is part of its own
36
+ // declaration rather than something worth hoisting.
37
+ const scope = fields == null ? void 0 : fields.join(',');
38
+ const descriptors = useMemo(()=>{
39
+ const described = describeEntityColumns(entityConstructor, entity);
40
+ return scope === undefined ? described : described.filter((descriptor)=>scope.split(',').includes(descriptor.name));
41
+ }, [
42
+ entityConstructor,
43
+ entity,
44
+ scope
45
+ ]);
46
+ // A persisted draft is layered over the seed, never substituted for it: the
47
+ // entity decides which members exist, the draft only decides their values.
48
+ // See `restoreEntityDraft` for what a straight substitution costs.
49
+ const restored = draft == null ? void 0 : draft.draft;
50
+ const seed = useMemo(()=>restoreEntityDraft(descriptors, seedEntityDraft(descriptors, entity), restored), [
51
+ descriptors,
52
+ entity,
53
+ restored
54
+ ]);
55
+ // The relations the record already carries, so opening a loaded record needs no
56
+ // label lookup and a save keeps whatever embedded shape it arrived in. Seeded
57
+ // once, like the draft — a caller that loads late keys the form by record id.
58
+ const [links, setLinks] = useState(()=>seedEntityLinkSelection(descriptors, entity));
59
+ // Supplied by the caller, and that is the seam.
60
+ //
61
+ // ⚠️ This used to reach into react-i18next and `@entifix/i18n` directly,
62
+ // because both React packages are `entifix:react` and the boundary rule
63
+ // forbids a sideways import of the controls package that owns the `controls`
64
+ // catalog. The effect was that anyone taking the integration hooks also took
65
+ // i18next and a Spanish catalog — the composition defect the tier register
66
+ // was written to catch. The one real caller, `entity-crud-form.tsx`, sits a
67
+ // layer up where `useT('controls')` is reachable, so it resolves the four
68
+ // sentences and hands them over.
69
+ const messages = useMemo(()=>validationMessages != null ? validationMessages : DEFAULT_VALIDATION_MESSAGES, [
70
+ validationMessages
71
+ ]);
72
+ // A schema's message is authored as a catalog key (`validation.minLength`),
73
+ // because a rule written in one language would reach the user untranslated —
74
+ // the one thing the i18n gate exists to prevent. `defaultValue` keeps an
75
+ // unkeyed literal readable instead of rendering the key back at the user, and
76
+ // `field` is offered as a parameter so a message can name its own label.
77
+ // Supplied by the caller for the same reason the four sentences above are —
78
+ // and this one is `useTranslateKey`'s job rather than `useT`'s, because a
79
+ // schema's message is a key only known at runtime. With nothing supplied the
80
+ // message renders as authored, which is what `defaultValue` always did.
81
+ const translateIssue = useCallback((message, field)=>{
82
+ var _ref;
83
+ var _descriptors_find;
84
+ return translateKey === undefined ? message : translateKey(message, {
85
+ defaultValue: message,
86
+ field: (_ref = (_descriptors_find = descriptors.find((entry)=>entry.name === field)) == null ? void 0 : _descriptors_find.label) != null ? _ref : field
87
+ });
88
+ }, [
89
+ translateKey,
90
+ descriptors
91
+ ]);
92
+ /**
93
+ * Whether the last {@link submit} reached the caller's `onSubmit`.
94
+ *
95
+ * A ref rather than state: nothing renders from it, and making it state would
96
+ * re-render the whole form on every submit attempt for a value only the
97
+ * caller awaiting `submit()` ever reads.
98
+ */ const submitted = useRef(false);
99
+ const form = useForm({
100
+ defaultValues: seed,
101
+ // Quiet until the first submit, then revalidate on every change — the
102
+ // behaviour the hand-rolled `submitted` flag used to produce.
103
+ validationLogic: revalidateLogic(),
104
+ validators: {
105
+ onDynamic: ({ value })=>{
106
+ const { fields, form } = composeEntityFormErrors({
107
+ descriptors,
108
+ values: value,
109
+ messages,
110
+ schema,
111
+ translateIssue,
112
+ validate
113
+ });
114
+ if (Object.keys(fields).length === 0 && form === undefined) return undefined;
115
+ // `{ fields }` is how a form-level validator addresses individual
116
+ // fields; the bare `form` message is what has no field to sit under.
117
+ return {
118
+ form,
119
+ fields
120
+ };
121
+ }
122
+ },
123
+ onSubmit: ({ value })=>{
124
+ // The engine calls this only when validation passed, so recording that it
125
+ // ran *is* the answer {@link UseEntityFormResult.submit} returns. Reading
126
+ // `form.state.isValid` afterwards would ask the engine a second question
127
+ // whose answer depends on when it settled.
128
+ submitted.current = true;
129
+ return onSubmit(value);
130
+ }
131
+ });
132
+ const values = useStore(form.store, (state)=>state.values);
133
+ const fieldMeta = useStore(form.store, (state)=>state.fieldMeta);
134
+ const formError = useStore(form.store, (state)=>state.errorMap.onDynamic);
135
+ // `isDefaultValue`, not `isDirty`: the draft is dirty when it *differs* from
136
+ // its seed, so editing a field and typing the seed value back is not a change
137
+ // the workspace should autosave.
138
+ const isDefaultValue = useStore(form.store, (state)=>state.isDefaultValue);
139
+ const errors = useMemo(()=>readFieldErrors(fieldMeta), [
140
+ fieldMeta
141
+ ]);
142
+ /**
143
+ * Validate, then submit when clean — and **say whether it went**.
144
+ *
145
+ * The promise is what lets a caller gate on the result. A wizard's "Siguiente"
146
+ * is the step's own submit, and before this it had nothing to wait for:
147
+ * `handleSubmit()`'s promise was discarded, so the only way to learn the
148
+ * outcome was to await a tick and re-read `errors`, which is the trap this
149
+ * hook's own specs worked around.
150
+ */ const submit = useCallback(async ()=>{
151
+ submitted.current = false;
152
+ await form.handleSubmit();
153
+ return submitted.current;
154
+ }, [
155
+ form
156
+ ]);
157
+ // Both halves of a pick, in one call: the id the draft persists and the
158
+ // instance only memory holds.
159
+ const setLink = useCallback((name, picked)=>{
160
+ setLinks((current)=>_extends({}, current, {
161
+ [name]: picked
162
+ }));
163
+ form.setFieldValue(name, (picked == null ? void 0 : picked.id) == null ? '' : String(picked.id));
164
+ }, [
165
+ form
166
+ ]);
167
+ // The sidecar half alone. A restored draft holds the id and lost the instance,
168
+ // and something resolved it back — that is not a user pick, so it must not
169
+ // touch the draft: writing the field would be an edit the user did not make,
170
+ // and on a form whose seed *is* the restored draft it would also be a no-op
171
+ // that only risks disagreeing with the engine about equality.
172
+ const hydrateLink = useCallback((name, resolved)=>{
173
+ setLinks((current)=>current[name] === undefined ? _extends({}, current, {
174
+ [name]: resolved
175
+ }) : current);
176
+ }, []);
177
+ // Autosave. The seam lives here rather than in each page, so a form gets
178
+ // persistence by naming a store and no call site threads a draft through.
179
+ //
180
+ // Two properties keep it from writing more than it means to. It writes only
181
+ // while the draft *differs* from its seed — and on mount over a restored
182
+ // draft the seed **is** that draft, so opening a tab re-persists nothing. And
183
+ // it is keyed on `save`, which the port requires to be referentially stable;
184
+ // a new identity per render would make every render a write.
185
+ //
186
+ // Nothing here clears: a draft is spent when the write commits, and this hook
187
+ // never learns whether it did. See `EntityDraftStore.clear`.
188
+ const save = draft == null ? void 0 : draft.save;
189
+ useEffect(()=>{
190
+ if (!isDefaultValue) save == null ? void 0 : save(values);
191
+ }, [
192
+ values,
193
+ isDefaultValue,
194
+ save
195
+ ]);
196
+ return {
197
+ values,
198
+ errors,
199
+ formError: typeof formError === 'string' ? formError : undefined,
200
+ setField: form.setFieldValue,
201
+ links,
202
+ setLink,
203
+ hydrateLink,
204
+ submit,
205
+ isDirty: !isDefaultValue
206
+ };
207
+ }
208
+
209
+ //# sourceMappingURL=use-entity-form.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/hooks/use-entity-form/use-entity-form.ts"],"sourcesContent":["'use client';\n\nimport {\n describeEntityColumns,\n type Entity,\n type EntityDraft,\n type EntityLinkSelection,\n seedEntityLinkSelection,\n} from '@entifix/core';\nimport { revalidateLogic, useForm, useStore } from '@tanstack/react-form';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport {\n composeEntityFormErrors,\n DEFAULT_VALIDATION_MESSAGES,\n readFieldErrors,\n restoreEntityDraft,\n seedEntityDraft,\n} from './use-entity-form.helpers.js';\nimport type {\n UseEntityFormOptions,\n UseEntityFormResult,\n} from './use-entity-form.types.js';\n\n/**\n * The write half of an entity form: holds the string draft and reports\n * metadata-derived validation, the counterpart to {@link useEntityRecord}\n * (which reads the record it seeds from).\n *\n * It stays out of the transport: it neither fetches nor saves. `onSubmit`\n * receives the validated draft, and the per-entity wrapper reconstructs the\n * domain entity and calls the mutation. The result plugs straight into the\n * agnostic `EntityForm` (`values`/`onFieldChange`/`errors`/`onSubmit`).\n *\n * TanStack Form owns the draft and the submission lifecycle; that is an\n * implementation detail, and the returned shape is deliberately the plain\n * `values`/`errors`/`setField`/`submit`/`isDirty` facade so `EntityForm` — which\n * renders native inputs and knows nothing about any form library — stays\n * agnostic. Fields are therefore never registered through `form.Field`; a\n * form-level validator reporting `{ fields }` reaches them anyway, because\n * `FormApi` creates meta for any field name an error arrives for.\n *\n * `revalidateLogic` is what makes a pristine form quiet: nothing validates until\n * the first submit, and every keystroke revalidates after it. Running one\n * validator under a single cause also keeps a fixed field from holding a stale\n * message left behind by another cause.\n *\n * The seed applies once, so a caller that loads its record after mount keys the\n * form by the record id to reseed — the same convention `EntityForm`'s wrappers\n * already follow.\n */\nexport function useEntityForm<TEntity extends Entity>({\n entityConstructor,\n entity,\n draft,\n fields,\n schema,\n validate,\n validationMessages,\n translateKey,\n onSubmit,\n}: UseEntityFormOptions<TEntity>): UseEntityFormResult {\n // Keyed on the **names**, not the array, so a caller may write the list inline\n // — which every wizard step does, since its field set is part of its own\n // declaration rather than something worth hoisting.\n const scope = fields?.join(',');\n const descriptors = useMemo(() => {\n const described = describeEntityColumns(entityConstructor, entity);\n return scope === undefined\n ? described\n : described.filter(descriptor =>\n scope.split(',').includes(descriptor.name),\n );\n }, [entityConstructor, entity, scope]);\n // A persisted draft is layered over the seed, never substituted for it: the\n // entity decides which members exist, the draft only decides their values.\n // See `restoreEntityDraft` for what a straight substitution costs.\n const restored = draft?.draft;\n const seed = useMemo(\n () =>\n restoreEntityDraft(\n descriptors,\n seedEntityDraft(descriptors, entity),\n restored,\n ),\n [descriptors, entity, restored],\n );\n\n // The relations the record already carries, so opening a loaded record needs no\n // label lookup and a save keeps whatever embedded shape it arrived in. Seeded\n // once, like the draft — a caller that loads late keys the form by record id.\n const [links, setLinks] = useState<EntityLinkSelection>(() =>\n seedEntityLinkSelection(descriptors, entity),\n );\n\n // Supplied by the caller, and that is the seam.\n //\n // ⚠️ This used to reach into react-i18next and `@entifix/i18n` directly,\n // because both React packages are `entifix:react` and the boundary rule\n // forbids a sideways import of the controls package that owns the `controls`\n // catalog. The effect was that anyone taking the integration hooks also took\n // i18next and a Spanish catalog — the composition defect the tier register\n // was written to catch. The one real caller, `entity-crud-form.tsx`, sits a\n // layer up where `useT('controls')` is reachable, so it resolves the four\n // sentences and hands them over.\n const messages = useMemo(\n () => validationMessages ?? DEFAULT_VALIDATION_MESSAGES,\n [validationMessages],\n );\n\n // A schema's message is authored as a catalog key (`validation.minLength`),\n // because a rule written in one language would reach the user untranslated —\n // the one thing the i18n gate exists to prevent. `defaultValue` keeps an\n // unkeyed literal readable instead of rendering the key back at the user, and\n // `field` is offered as a parameter so a message can name its own label.\n // Supplied by the caller for the same reason the four sentences above are —\n // and this one is `useTranslateKey`'s job rather than `useT`'s, because a\n // schema's message is a key only known at runtime. With nothing supplied the\n // message renders as authored, which is what `defaultValue` always did.\n const translateIssue = useCallback(\n (message: string, field: string | undefined) =>\n translateKey === undefined\n ? message\n : translateKey(message, {\n defaultValue: message,\n field:\n descriptors.find(entry => entry.name === field)?.label ?? field,\n }),\n [translateKey, descriptors],\n );\n\n /**\n * Whether the last {@link submit} reached the caller's `onSubmit`.\n *\n * A ref rather than state: nothing renders from it, and making it state would\n * re-render the whole form on every submit attempt for a value only the\n * caller awaiting `submit()` ever reads.\n */\n const submitted = useRef(false);\n\n const form = useForm({\n defaultValues: seed,\n // Quiet until the first submit, then revalidate on every change — the\n // behaviour the hand-rolled `submitted` flag used to produce.\n validationLogic: revalidateLogic(),\n validators: {\n onDynamic: ({ value }: { value: EntityDraft }) => {\n const { fields, form } = composeEntityFormErrors({\n descriptors,\n values: value,\n messages,\n schema,\n translateIssue,\n validate,\n });\n\n if (Object.keys(fields).length === 0 && form === undefined)\n return undefined;\n // `{ fields }` is how a form-level validator addresses individual\n // fields; the bare `form` message is what has no field to sit under.\n return { form, fields };\n },\n },\n onSubmit: ({ value }) => {\n // The engine calls this only when validation passed, so recording that it\n // ran *is* the answer {@link UseEntityFormResult.submit} returns. Reading\n // `form.state.isValid` afterwards would ask the engine a second question\n // whose answer depends on when it settled.\n submitted.current = true;\n return onSubmit(value);\n },\n });\n\n const values = useStore(form.store, state => state.values);\n const fieldMeta = useStore(form.store, state => state.fieldMeta);\n const formError = useStore(form.store, state => state.errorMap.onDynamic);\n // `isDefaultValue`, not `isDirty`: the draft is dirty when it *differs* from\n // its seed, so editing a field and typing the seed value back is not a change\n // the workspace should autosave.\n const isDefaultValue = useStore(form.store, state => state.isDefaultValue);\n\n const errors = useMemo(() => readFieldErrors(fieldMeta), [fieldMeta]);\n\n /**\n * Validate, then submit when clean — and **say whether it went**.\n *\n * The promise is what lets a caller gate on the result. A wizard's \"Siguiente\"\n * is the step's own submit, and before this it had nothing to wait for:\n * `handleSubmit()`'s promise was discarded, so the only way to learn the\n * outcome was to await a tick and re-read `errors`, which is the trap this\n * hook's own specs worked around.\n */\n const submit = useCallback(async () => {\n submitted.current = false;\n await form.handleSubmit();\n return submitted.current;\n }, [form]);\n\n // Both halves of a pick, in one call: the id the draft persists and the\n // instance only memory holds.\n const setLink = useCallback(\n (name: string, picked: Entity | undefined) => {\n setLinks(current => ({ ...current, [name]: picked }));\n form.setFieldValue(name, picked?.id == null ? '' : String(picked.id));\n },\n [form],\n );\n\n // The sidecar half alone. A restored draft holds the id and lost the instance,\n // and something resolved it back — that is not a user pick, so it must not\n // touch the draft: writing the field would be an edit the user did not make,\n // and on a form whose seed *is* the restored draft it would also be a no-op\n // that only risks disagreeing with the engine about equality.\n const hydrateLink = useCallback((name: string, resolved: Entity) => {\n setLinks(current =>\n current[name] === undefined ? { ...current, [name]: resolved } : current,\n );\n }, []);\n\n // Autosave. The seam lives here rather than in each page, so a form gets\n // persistence by naming a store and no call site threads a draft through.\n //\n // Two properties keep it from writing more than it means to. It writes only\n // while the draft *differs* from its seed — and on mount over a restored\n // draft the seed **is** that draft, so opening a tab re-persists nothing. And\n // it is keyed on `save`, which the port requires to be referentially stable;\n // a new identity per render would make every render a write.\n //\n // Nothing here clears: a draft is spent when the write commits, and this hook\n // never learns whether it did. See `EntityDraftStore.clear`.\n const save = draft?.save;\n useEffect(() => {\n if (!isDefaultValue) save?.(values);\n }, [values, isDefaultValue, save]);\n\n return {\n values,\n errors,\n formError: typeof formError === 'string' ? formError : undefined,\n setField: form.setFieldValue,\n links,\n setLink,\n hydrateLink,\n submit,\n isDirty: !isDefaultValue,\n };\n}\n"],"names":["describeEntityColumns","seedEntityLinkSelection","revalidateLogic","useForm","useStore","useCallback","useEffect","useMemo","useRef","useState","composeEntityFormErrors","DEFAULT_VALIDATION_MESSAGES","readFieldErrors","restoreEntityDraft","seedEntityDraft","useEntityForm","entityConstructor","entity","draft","fields","schema","validate","validationMessages","translateKey","onSubmit","scope","join","descriptors","described","undefined","filter","descriptor","split","includes","name","restored","seed","links","setLinks","messages","translateIssue","message","field","defaultValue","find","entry","label","submitted","form","defaultValues","validationLogic","validators","onDynamic","value","values","Object","keys","length","current","store","state","fieldMeta","formError","errorMap","isDefaultValue","errors","submit","handleSubmit","setLink","picked","setFieldValue","id","String","hydrateLink","resolved","save","setField","isDirty"],"mappings":"AAAA;;AAEA,SACEA,qBAAqB,EAIrBC,uBAAuB,QAClB,gBAAgB;AACvB,SAASC,eAAe,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,uBAAuB;AAC1E,SAASC,WAAW,EAAEC,SAAS,EAAEC,OAAO,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAQ;AAE1E,SACEC,uBAAuB,EACvBC,2BAA2B,EAC3BC,eAAe,EACfC,kBAAkB,EAClBC,eAAe,QACV,+BAA+B;AAMtC;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GACD,OAAO,SAASC,cAAsC,EACpDC,iBAAiB,EACjBC,MAAM,EACNC,KAAK,EACLC,MAAM,EACNC,MAAM,EACNC,QAAQ,EACRC,kBAAkB,EAClBC,YAAY,EACZC,QAAQ,EACsB;IAC9B,+EAA+E;IAC/E,yEAAyE;IACzE,oDAAoD;IACpD,MAAMC,QAAQN,0BAAAA,OAAQO,IAAI,CAAC;IAC3B,MAAMC,cAAcpB,QAAQ;QAC1B,MAAMqB,YAAY5B,sBAAsBgB,mBAAmBC;QAC3D,OAAOQ,UAAUI,YACbD,YACAA,UAAUE,MAAM,CAACC,CAAAA,aACfN,MAAMO,KAAK,CAAC,KAAKC,QAAQ,CAACF,WAAWG,IAAI;IAEjD,GAAG;QAAClB;QAAmBC;QAAQQ;KAAM;IACrC,4EAA4E;IAC5E,2EAA2E;IAC3E,mEAAmE;IACnE,MAAMU,WAAWjB,yBAAAA,MAAOA,KAAK;IAC7B,MAAMkB,OAAO7B,QACX,IACEM,mBACEc,aACAb,gBAAgBa,aAAaV,SAC7BkB,WAEJ;QAACR;QAAaV;QAAQkB;KAAS;IAGjC,gFAAgF;IAChF,8EAA8E;IAC9E,8EAA8E;IAC9E,MAAM,CAACE,OAAOC,SAAS,GAAG7B,SAA8B,IACtDR,wBAAwB0B,aAAaV;IAGvC,gDAAgD;IAChD,EAAE;IACF,yEAAyE;IACzE,wEAAwE;IACxE,6EAA6E;IAC7E,6EAA6E;IAC7E,2EAA2E;IAC3E,4EAA4E;IAC5E,0EAA0E;IAC1E,iCAAiC;IACjC,MAAMsB,WAAWhC,QACf,IAAMe,6BAAAA,qBAAsBX,6BAC5B;QAACW;KAAmB;IAGtB,4EAA4E;IAC5E,6EAA6E;IAC7E,yEAAyE;IACzE,8EAA8E;IAC9E,yEAAyE;IACzE,4EAA4E;IAC5E,0EAA0E;IAC1E,6EAA6E;IAC7E,wEAAwE;IACxE,MAAMkB,iBAAiBnC,YACrB,CAACoC,SAAiBC;;YAMRf;eALRJ,iBAAiBM,YACbY,UACAlB,aAAakB,SAAS;YACpBE,cAAcF;YACdC,KAAK,WACHf,oBAAAA,YAAYiB,IAAI,CAACC,CAAAA,QAASA,MAAMX,IAAI,KAAKQ,2BAAzCf,kBAAiDmB,KAAK,mBAAIJ;QAC9D;OACN;QAACnB;QAAcI;KAAY;IAG7B;;;;;;GAMC,GACD,MAAMoB,YAAYvC,OAAO;IAEzB,MAAMwC,OAAO7C,QAAQ;QACnB8C,eAAeb;QACf,sEAAsE;QACtE,8DAA8D;QAC9Dc,iBAAiBhD;QACjBiD,YAAY;YACVC,WAAW,CAAC,EAAEC,KAAK,EAA0B;gBAC3C,MAAM,EAAElC,MAAM,EAAE6B,IAAI,EAAE,GAAGtC,wBAAwB;oBAC/CiB;oBACA2B,QAAQD;oBACRd;oBACAnB;oBACAoB;oBACAnB;gBACF;gBAEA,IAAIkC,OAAOC,IAAI,CAACrC,QAAQsC,MAAM,KAAK,KAAKT,SAASnB,WAC/C,OAAOA;gBACT,kEAAkE;gBAClE,qEAAqE;gBACrE,OAAO;oBAAEmB;oBAAM7B;gBAAO;YACxB;QACF;QACAK,UAAU,CAAC,EAAE6B,KAAK,EAAE;YAClB,0EAA0E;YAC1E,0EAA0E;YAC1E,yEAAyE;YACzE,2CAA2C;YAC3CN,UAAUW,OAAO,GAAG;YACpB,OAAOlC,SAAS6B;QAClB;IACF;IAEA,MAAMC,SAASlD,SAAS4C,KAAKW,KAAK,EAAEC,CAAAA,QAASA,MAAMN,MAAM;IACzD,MAAMO,YAAYzD,SAAS4C,KAAKW,KAAK,EAAEC,CAAAA,QAASA,MAAMC,SAAS;IAC/D,MAAMC,YAAY1D,SAAS4C,KAAKW,KAAK,EAAEC,CAAAA,QAASA,MAAMG,QAAQ,CAACX,SAAS;IACxE,6EAA6E;IAC7E,8EAA8E;IAC9E,iCAAiC;IACjC,MAAMY,iBAAiB5D,SAAS4C,KAAKW,KAAK,EAAEC,CAAAA,QAASA,MAAMI,cAAc;IAEzE,MAAMC,SAAS1D,QAAQ,IAAMK,gBAAgBiD,YAAY;QAACA;KAAU;IAEpE;;;;;;;;GAQC,GACD,MAAMK,SAAS7D,YAAY;QACzB0C,UAAUW,OAAO,GAAG;QACpB,MAAMV,KAAKmB,YAAY;QACvB,OAAOpB,UAAUW,OAAO;IAC1B,GAAG;QAACV;KAAK;IAET,wEAAwE;IACxE,8BAA8B;IAC9B,MAAMoB,UAAU/D,YACd,CAAC6B,MAAcmC;QACb/B,SAASoB,CAAAA,UAAY,aAAKA;gBAAS,CAACxB,KAAK,EAAEmC;;QAC3CrB,KAAKsB,aAAa,CAACpC,MAAMmC,CAAAA,0BAAAA,OAAQE,EAAE,KAAI,OAAO,KAAKC,OAAOH,OAAOE,EAAE;IACrE,GACA;QAACvB;KAAK;IAGR,+EAA+E;IAC/E,2EAA2E;IAC3E,6EAA6E;IAC7E,4EAA4E;IAC5E,8DAA8D;IAC9D,MAAMyB,cAAcpE,YAAY,CAAC6B,MAAcwC;QAC7CpC,SAASoB,CAAAA,UACPA,OAAO,CAACxB,KAAK,KAAKL,YAAY,aAAK6B;gBAAS,CAACxB,KAAK,EAAEwC;iBAAahB;IAErE,GAAG,EAAE;IAEL,yEAAyE;IACzE,0EAA0E;IAC1E,EAAE;IACF,4EAA4E;IAC5E,yEAAyE;IACzE,8EAA8E;IAC9E,6EAA6E;IAC7E,6DAA6D;IAC7D,EAAE;IACF,8EAA8E;IAC9E,6DAA6D;IAC7D,MAAMiB,OAAOzD,yBAAAA,MAAOyD,IAAI;IACxBrE,UAAU;QACR,IAAI,CAAC0D,gBAAgBW,wBAAAA,KAAOrB;IAC9B,GAAG;QAACA;QAAQU;QAAgBW;KAAK;IAEjC,OAAO;QACLrB;QACAW;QACAH,WAAW,OAAOA,cAAc,WAAWA,YAAYjC;QACvD+C,UAAU5B,KAAKsB,aAAa;QAC5BjC;QACA+B;QACAK;QACAP;QACAW,SAAS,CAACb;IACZ;AACF"}