@chelone/core 0.6.0 → 0.9.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 (255) hide show
  1. package/dist/assistant/assistant-session.d.ts +6 -0
  2. package/dist/assistant/assistant-session.js +204 -181
  3. package/dist/assistant/echo-transport.js +26 -29
  4. package/dist/assistant/http-transport.d.ts +10 -0
  5. package/dist/assistant/http-transport.js +98 -0
  6. package/dist/assistant/types.d.ts +1 -7
  7. package/dist/auth/access-control.d.ts +13 -5
  8. package/dist/auth/access-control.js +25 -21
  9. package/dist/auth/auth-options.d.ts +23 -17
  10. package/dist/auth/auth-options.js +82 -48
  11. package/dist/auth/permission-matrix.d.ts +12 -0
  12. package/dist/auth/permission-matrix.js +67 -0
  13. package/dist/auth/types.d.ts +14 -4
  14. package/dist/block-editor/block-document.d.ts +64 -0
  15. package/dist/block-editor/block-document.js +147 -0
  16. package/dist/block-editor/data-blocks.d.ts +63 -0
  17. package/dist/block-editor/data-blocks.js +117 -0
  18. package/dist/chart/chart-data.js +46 -44
  19. package/dist/command-menu/hotkey.js +58 -47
  20. package/dist/command-menu/recents.d.ts +4 -0
  21. package/dist/command-menu/recents.js +12 -11
  22. package/dist/command-menu/registry.d.ts +9 -5
  23. package/dist/command-menu/registry.js +36 -46
  24. package/dist/controller/calendar-dates.d.ts +12 -0
  25. package/dist/controller/calendar-dates.js +94 -113
  26. package/dist/controller/calendar-events.js +93 -88
  27. package/dist/controller/client-list-controller.d.ts +2 -2
  28. package/dist/controller/client-list-controller.js +251 -254
  29. package/dist/controller/data-table-columns.d.ts +2 -0
  30. package/dist/controller/data-table-columns.js +50 -56
  31. package/dist/controller/deep-equal.d.ts +1 -0
  32. package/dist/controller/deep-equal.js +14 -0
  33. package/dist/controller/filter-toggle.js +36 -44
  34. package/dist/controller/in-place-editor.d.ts +43 -0
  35. package/dist/controller/in-place-editor.js +43 -0
  36. package/dist/controller/infinite-list-controller.js +219 -189
  37. package/dist/controller/inspector.d.ts +55 -0
  38. package/dist/controller/inspector.js +70 -0
  39. package/dist/controller/job-monitor.d.ts +10 -4
  40. package/dist/controller/job-monitor.js +46 -47
  41. package/dist/controller/kanban-board.js +45 -24
  42. package/dist/controller/list-controller.d.ts +8 -5
  43. package/dist/controller/list-controller.js +447 -400
  44. package/dist/controller/list-params.js +96 -87
  45. package/dist/controller/pivot-grid.d.ts +4 -4
  46. package/dist/controller/pivot-grid.js +89 -95
  47. package/dist/controller/prev-next.d.ts +2 -2
  48. package/dist/controller/prev-next.js +74 -54
  49. package/dist/controller/record-controllers.d.ts +14 -6
  50. package/dist/controller/record-controllers.js +288 -257
  51. package/dist/controller/record-diff.d.ts +21 -0
  52. package/dist/controller/record-diff.js +50 -0
  53. package/dist/controller/record-timeline.d.ts +5 -5
  54. package/dist/controller/record-timeline.js +42 -30
  55. package/dist/controller/record-tree.js +57 -47
  56. package/dist/controller/record-workflow.d.ts +38 -16
  57. package/dist/controller/record-workflow.js +85 -64
  58. package/dist/controller/reference-many-controller.d.ts +2 -0
  59. package/dist/controller/reference-many-controller.js +199 -192
  60. package/dist/controller/reference-one-controller.d.ts +1 -1
  61. package/dist/controller/reference-one-controller.js +63 -56
  62. package/dist/controller/saved-queries.js +104 -91
  63. package/dist/controller/translate-fallback.js +10 -6
  64. package/dist/csv/export.d.ts +4 -3
  65. package/dist/csv/export.js +43 -40
  66. package/dist/csv/import-controller.d.ts +4 -0
  67. package/dist/csv/import-controller.js +189 -169
  68. package/dist/csv/match-headers.js +29 -22
  69. package/dist/csv/parse-with.js +60 -54
  70. package/dist/csv/run-import.js +69 -72
  71. package/dist/csv/validate-rows.d.ts +1 -1
  72. package/dist/csv/validate-rows.js +65 -52
  73. package/dist/data/cache-ops.d.ts +5 -4
  74. package/dist/data/cache-ops.js +208 -188
  75. package/dist/data/combine-data-providers.js +25 -23
  76. package/dist/data/data-provider-log.d.ts +45 -0
  77. package/dist/data/data-provider-log.js +93 -0
  78. package/dist/data/errors.js +14 -10
  79. package/dist/data/fake-provider.d.ts +6 -3
  80. package/dist/data/fake-provider.js +135 -114
  81. package/dist/data/fetch-utils.js +61 -44
  82. package/dist/data/filter-branch.d.ts +5 -0
  83. package/dist/data/filter-branch.js +10 -0
  84. package/dist/data/get-many-aggregate.js +84 -76
  85. package/dist/data/http-error.js +17 -12
  86. package/dist/data/keys.js +50 -36
  87. package/dist/data/mutation-options.d.ts +14 -9
  88. package/dist/data/mutation-options.js +93 -109
  89. package/dist/data/query-options.js +73 -68
  90. package/dist/data/remove-empty.d.ts +3 -0
  91. package/dist/data/remove-empty.js +11 -0
  92. package/dist/data/undoable.d.ts +2 -2
  93. package/dist/data/undoable.js +190 -174
  94. package/dist/data/with-lifecycle-callbacks.d.ts +45 -17
  95. package/dist/data/with-lifecycle-callbacks.js +66 -52
  96. package/dist/environment/application-update-store.js +58 -55
  97. package/dist/environment/offline-store.js +21 -21
  98. package/dist/form/array-binding.d.ts +30 -11
  99. package/dist/form/array-binding.js +184 -166
  100. package/dist/form/choice-ids.d.ts +7 -0
  101. package/dist/form/choice-ids.js +11 -0
  102. package/dist/form/choices-controller.d.ts +22 -2
  103. package/dist/form/choices-controller.js +315 -275
  104. package/dist/form/date-format.d.ts +38 -0
  105. package/dist/form/date-format.js +89 -0
  106. package/dist/form/dirty-values.d.ts +2 -2
  107. package/dist/form/dirty-values.js +22 -11
  108. package/dist/form/duration.d.ts +22 -0
  109. package/dist/form/duration.js +49 -0
  110. package/dist/form/edit-create-form.d.ts +12 -5
  111. package/dist/form/edit-create-form.js +243 -188
  112. package/dist/form/event-value.d.ts +6 -0
  113. package/dist/form/event-value.js +17 -0
  114. package/dist/form/field-error.d.ts +14 -0
  115. package/dist/form/field-error.js +28 -0
  116. package/dist/form/field-name.d.ts +20 -0
  117. package/dist/form/field-name.js +6 -0
  118. package/dist/form/filter-form-values.js +63 -49
  119. package/dist/form/form-controller.d.ts +9 -1
  120. package/dist/form/form-controller.js +165 -160
  121. package/dist/form/form-groups.d.ts +4 -3
  122. package/dist/form/form-groups.js +79 -79
  123. package/dist/form/form-handle.d.ts +175 -0
  124. package/dist/form/form-handle.js +68 -0
  125. package/dist/form/input-binding.d.ts +56 -31
  126. package/dist/form/input-binding.js +171 -118
  127. package/dist/form/input-values.d.ts +140 -0
  128. package/dist/form/input-values.js +206 -0
  129. package/dist/form/json-text.d.ts +11 -0
  130. package/dist/form/json-text.js +62 -37
  131. package/dist/form/save-pipeline.d.ts +6 -8
  132. package/dist/form/save-pipeline.js +91 -82
  133. package/dist/form/set-form-values.d.ts +6 -0
  134. package/dist/form/set-form-values.js +10 -0
  135. package/dist/form/source.js +97 -65
  136. package/dist/form/text-of.d.ts +2 -0
  137. package/dist/form/text-of.js +7 -0
  138. package/dist/form/validators.d.ts +58 -19
  139. package/dist/form/validators.js +97 -71
  140. package/dist/geo/bbox.d.ts +5 -4
  141. package/dist/geo/bbox.js +69 -48
  142. package/dist/geo/bounds.d.ts +14 -3
  143. package/dist/geo/bounds.js +98 -68
  144. package/dist/geo/constraints.js +88 -93
  145. package/dist/geo/coordinates.d.ts +10 -0
  146. package/dist/geo/coordinates.js +44 -23
  147. package/dist/geo/editor.d.ts +13 -13
  148. package/dist/geo/editor.js +176 -153
  149. package/dist/geo/geocoding.d.ts +4 -4
  150. package/dist/geo/geocoding.js +38 -41
  151. package/dist/geo/geojson.d.ts +12 -74
  152. package/dist/geo/geojson.js +52 -40
  153. package/dist/geo/geometry-math.d.ts +4 -4
  154. package/dist/geo/geometry-math.js +100 -93
  155. package/dist/geo/index.d.ts +9 -8
  156. package/dist/geo/map-layers.d.ts +42 -7
  157. package/dist/geo/map-layers.js +88 -62
  158. package/dist/geo/map-style.d.ts +12 -0
  159. package/dist/geo/map-style.js +43 -32
  160. package/dist/geo/messages.js +48 -45
  161. package/dist/geo/nominatim.js +88 -80
  162. package/dist/geo/osm-presets.d.ts +4 -3
  163. package/dist/geo/osm-presets.js +108 -86
  164. package/dist/geo/osm-tags.js +14 -12
  165. package/dist/geo/overpass.d.ts +14 -2
  166. package/dist/geo/overpass.js +83 -90
  167. package/dist/geo/shape-value.d.ts +9 -5
  168. package/dist/geo/shape-value.js +169 -146
  169. package/dist/geo/snap-to-roads.d.ts +3 -3
  170. package/dist/geo/snap-to-roads.js +55 -50
  171. package/dist/geo/throttle.js +36 -39
  172. package/dist/guards.d.ts +44 -0
  173. package/dist/guards.js +77 -0
  174. package/dist/i18n/default-messages.js +34 -29
  175. package/dist/i18n/english-messages.d.ts +172 -0
  176. package/dist/i18n/english-messages.js +359 -189
  177. package/dist/i18n/i18n.d.ts +2 -2
  178. package/dist/i18n/i18n.js +47 -60
  179. package/dist/i18n/inflection.d.ts +34 -0
  180. package/dist/i18n/inflection.js +82 -0
  181. package/dist/i18n/messages.d.ts +10 -2
  182. package/dist/i18n/messages.js +17 -4
  183. package/dist/i18n/missing-keys.d.ts +4 -0
  184. package/dist/i18n/missing-keys.js +70 -62
  185. package/dist/i18n/translatable.d.ts +16 -1
  186. package/dist/i18n/translatable.js +40 -47
  187. package/dist/index.d.ts +87 -39
  188. package/dist/index.js +54 -301
  189. package/dist/json/deep-equal.d.ts +8 -0
  190. package/dist/json/deep-equal.js +24 -0
  191. package/dist/json/strip-json-comments.js +38 -42
  192. package/dist/noop.d.ts +2 -0
  193. package/dist/noop.js +4 -0
  194. package/dist/notification/error-message.d.ts +2 -0
  195. package/dist/notification/error-message.js +10 -0
  196. package/dist/notification/notification-store.d.ts +3 -2
  197. package/dist/notification/notification-store.js +41 -45
  198. package/dist/onboarding-tour/placement.d.ts +2 -0
  199. package/dist/onboarding-tour/placement.js +65 -78
  200. package/dist/onboarding-tour/tour-controller.js +46 -54
  201. package/dist/realtime/add-events-for-mutations.js +75 -81
  202. package/dist/realtime/announce-lock.js +16 -13
  203. package/dist/realtime/broadcast-channel-transport.js +65 -59
  204. package/dist/realtime/copy-members.js +22 -14
  205. package/dist/realtime/in-memory-lock-provider.js +40 -48
  206. package/dist/realtime/live.js +95 -101
  207. package/dist/realtime/lock-options.js +16 -16
  208. package/dist/realtime/lock-session.js +91 -78
  209. package/dist/realtime/memory-transport.js +25 -25
  210. package/dist/realtime/presence.js +120 -122
  211. package/dist/realtime/realtime-data-provider.d.ts +10 -0
  212. package/dist/realtime/realtime-data-provider.js +45 -42
  213. package/dist/realtime/reconnect-backoff.js +17 -0
  214. package/dist/realtime/sse-transport.d.ts +31 -0
  215. package/dist/realtime/sse-transport.js +235 -0
  216. package/dist/realtime/status-store.js +23 -28
  217. package/dist/realtime/topic-hub.js +62 -71
  218. package/dist/realtime/topics.js +10 -12
  219. package/dist/realtime/types.js +10 -12
  220. package/dist/realtime/unread-count.js +21 -21
  221. package/dist/realtime/websocket-transport.d.ts +32 -0
  222. package/dist/realtime/websocket-transport.js +306 -0
  223. package/dist/resource/describe-resource.d.ts +2 -0
  224. package/dist/resource/describe-resource.js +247 -219
  225. package/dist/resource/get-at-path.js +29 -14
  226. package/dist/resource/identifier.d.ts +18 -0
  227. package/dist/resource/identifier.js +27 -0
  228. package/dist/resource/record.d.ts +4 -0
  229. package/dist/resource/record.js +10 -0
  230. package/dist/resource/register.d.ts +7 -0
  231. package/dist/resource/registry-store.d.ts +45 -15
  232. package/dist/resource/registry-store.js +67 -60
  233. package/dist/resource/registry.d.ts +10 -0
  234. package/dist/resource/registry.js +8 -4
  235. package/dist/resource/source-path.d.ts +23 -2
  236. package/dist/rich-text/rich-text-extras.d.ts +134 -0
  237. package/dist/rich-text/rich-text-extras.js +181 -0
  238. package/dist/rich-text/rich-text-image.d.ts +124 -0
  239. package/dist/rich-text/rich-text-image.js +163 -0
  240. package/dist/rich-text/rich-text.d.ts +6 -0
  241. package/dist/rich-text/rich-text.js +125 -88
  242. package/dist/router/location.d.ts +24 -4
  243. package/dist/router/location.js +43 -18
  244. package/dist/router/match.js +47 -39
  245. package/dist/router/paths.js +40 -34
  246. package/dist/router/record-from-location.d.ts +1 -1
  247. package/dist/router/record-from-location.js +25 -17
  248. package/dist/store/adapters.js +51 -55
  249. package/dist/store/micro-store.js +57 -59
  250. package/dist/store/shallow-equal.d.ts +12 -0
  251. package/dist/store/shallow-equal.js +20 -0
  252. package/dist/store/store.js +54 -57
  253. package/dist/ui-defaults.d.ts +15 -0
  254. package/dist/ui-defaults.js +17 -0
  255. package/package.json +13 -11
@@ -1,201 +1,256 @@
1
1
  import { createCreateController, createEditController } from "../controller/record-controllers.js";
2
+ import { isPlainObject } from "../guards.js";
3
+ import { toFieldName } from "./field-name.js";
2
4
  import { createFormController } from "./form-controller.js";
5
+ import { toResourceFormHandle } from "./form-handle.js";
6
+ // src/form/edit-create-form.ts
7
+ /** The registry schema for `resource`, widened to the form's Partial data type. */
3
8
  function registrySchema(ctx, resource) {
4
- return ctx.resources?.[resource]?.schema;
5
- }
6
- function isPlainObject(value) {
7
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
8
- return false;
9
- }
10
- const proto = Object.getPrototypeOf(value);
11
- return proto === Object.prototype || proto === null;
9
+ return ctx.resources?.[resource]?.schema;
12
10
  }
11
+ /**
12
+ * Parity merge (§0b defaultValues precedence): `override` (the record) WINS,
13
+ * including `null`; explicit `undefined` falls back to the base (an absent
14
+ * server field never erases a default); arrays replace wholesale. Fresh
15
+ * objects on every merged level.
16
+ */
13
17
  function deepMerge(base, override) {
14
- if (override === void 0) return base;
15
- if (!isPlainObject(base) || !isPlainObject(override)) return override;
16
- const result = { ...base };
17
- for (const [key, value] of Object.entries(override)) {
18
- result[key] = key in base ? deepMerge(base[key], value) : value;
19
- }
20
- return result;
18
+ if (override === void 0) return base;
19
+ if (!isPlainObject(base) || !isPlainObject(override)) return override;
20
+ const result = { ...base };
21
+ for (const [key, value] of Object.entries(override)) result[key] = key in base ? deepMerge(base[key], value) : value;
22
+ return result;
21
23
  }
24
+ /**
25
+ * The record-hydration decision (§0b: record identity drives reset):
26
+ * - a record with a DIFFERENT id (loose comparison) -> hard reset, dropping
27
+ * dirty values. Unreachable through `createEditFormController` today (the
28
+ * P4 fetch guard errors on id mismatches before a record lands), kept as
29
+ * the pinned semantic for any composition that can swap records.
30
+ * - untouched form -> rebaseline (apply the merged record as new defaults —
31
+ * `form.update` auto-applies defaults to untouched forms without dirtying).
32
+ * - FIRST record arrival into a touched form -> partial apply (§0b
33
+ * keepDirtyValues): the form has never seen the record, so clean fields
34
+ * still hold construction defaults — keeping them would make the next save
35
+ * send those defaults over the real record fields (a data wipe). The
36
+ * record hydrates every clean field; dirty values (and their dirty meta)
37
+ * survive.
38
+ * - touched form, same record (refetch) -> keep ALL current values (soft):
39
+ * clean fields already hold the previous record's values — stale at worst,
40
+ * never destructive.
41
+ */
22
42
  function resolveHydrationAction(previousId, nextId, isTouched) {
23
- if (previousId !== void 0 && String(nextId) !== String(previousId)) {
24
- return "hard-reset";
25
- }
26
- if (!isTouched) return "rebaseline";
27
- return previousId === void 0 ? "partial-apply" : "keep-dirty";
43
+ if (previousId !== void 0 && String(nextId) !== String(previousId)) return "hard-reset";
44
+ if (!isTouched) return "rebaseline";
45
+ return previousId === void 0 ? "partial-apply" : "keep-dirty";
46
+ }
47
+ /**
48
+ * Wires the shared composed lifecycle. `onAttach` runs on the 0 -> 1
49
+ * subscriber transition BEFORE the form mounts (the edit hydration listener
50
+ * — so a cached record hydrates before mount validators see the form) and
51
+ * its teardown runs on the last unsubscribe.
52
+ */
53
+ function composeControllers(formController, recordStore, submit, onAttach) {
54
+ let destroyed = false;
55
+ let attachTeardown;
56
+ const active = /* @__PURE__ */ new Set();
57
+ let lastFormState;
58
+ let lastRecordState;
59
+ let cachedSnapshot;
60
+ return {
61
+ subscribe(listener) {
62
+ if (destroyed) return () => {};
63
+ const token = Symbol("composed-subscription");
64
+ const isFirst = active.size === 0;
65
+ active.add(token);
66
+ if (isFirst && onAttach) attachTeardown = onAttach();
67
+ const offForm = formController.subscribe(listener);
68
+ const offRecord = recordStore.subscribe(listener);
69
+ return () => {
70
+ if (!active.delete(token)) return;
71
+ offForm();
72
+ offRecord();
73
+ if (active.size === 0) {
74
+ const teardown = attachTeardown;
75
+ attachTeardown = void 0;
76
+ teardown?.();
77
+ }
78
+ };
79
+ },
80
+ getSnapshot() {
81
+ const formState = formController.getSnapshot();
82
+ const recordState = recordStore.getSnapshot();
83
+ if (cachedSnapshot === void 0 || lastFormState !== formState || lastRecordState !== recordState) {
84
+ lastFormState = formState;
85
+ lastRecordState = recordState;
86
+ cachedSnapshot = {
87
+ ...recordState,
88
+ form: formState
89
+ };
90
+ }
91
+ return cachedSnapshot;
92
+ },
93
+ submit,
94
+ destroy() {
95
+ if (destroyed) return;
96
+ destroyed = true;
97
+ const teardown = attachTeardown;
98
+ attachTeardown = void 0;
99
+ teardown?.();
100
+ formController.destroy();
101
+ recordStore.destroy();
102
+ active.clear();
103
+ }
104
+ };
28
105
  }
29
- function composeControllers(formController, recordStore, onAttach) {
30
- let destroyed = false;
31
- let attachTeardown;
32
- const active = /* @__PURE__ */ new Set();
33
- let lastFormState;
34
- let lastRecordState;
35
- let cachedSnapshot;
36
- return {
37
- subscribe(listener) {
38
- if (destroyed) return () => {
39
- };
40
- const token = /* @__PURE__ */ Symbol("composed-subscription");
41
- const isFirst = active.size === 0;
42
- active.add(token);
43
- if (isFirst && onAttach) attachTeardown = onAttach();
44
- const offForm = formController.subscribe(listener);
45
- const offRecord = recordStore.subscribe(listener);
46
- return () => {
47
- if (!active.delete(token)) return;
48
- offForm();
49
- offRecord();
50
- if (active.size === 0) {
51
- const teardown = attachTeardown;
52
- attachTeardown = void 0;
53
- teardown?.();
54
- }
55
- };
56
- },
57
- getSnapshot() {
58
- const formState = formController.getSnapshot();
59
- const recordState = recordStore.getSnapshot();
60
- if (cachedSnapshot === void 0 || lastFormState !== formState || lastRecordState !== recordState) {
61
- lastFormState = formState;
62
- lastRecordState = recordState;
63
- cachedSnapshot = { ...recordState, form: formState };
64
- }
65
- return cachedSnapshot;
66
- },
67
- submit: () => formController.submit(),
68
- destroy() {
69
- if (destroyed) return;
70
- destroyed = true;
71
- const teardown = attachTeardown;
72
- attachTeardown = void 0;
73
- teardown?.();
74
- formController.destroy();
75
- recordStore.destroy();
76
- active.clear();
77
- }
78
- };
106
+ /**
107
+ * The per-submit save options. The form's `onSubmit` takes no arguments, so
108
+ * `submit(options)` parks `options` here for the length of that one submit and
109
+ * the form's `save` reads them back through `saveOptions`. Two overlapping
110
+ * submits share the slot.
111
+ */
112
+ function createSubmitOptions() {
113
+ let current = {};
114
+ return {
115
+ /** The form's `save`: the call's options, with `reconcile` run before the caller's own `onSaved`. */
116
+ save(recordController, reconcile) {
117
+ return (data) => {
118
+ const call = current;
119
+ return recordController.save(data, {
120
+ ...call,
121
+ onSaved: async (saved) => {
122
+ await reconcile(saved);
123
+ await call.onSaved?.(saved);
124
+ }
125
+ });
126
+ };
127
+ },
128
+ async submit(formController, options = {}) {
129
+ current = options;
130
+ try {
131
+ await formController.submit();
132
+ } finally {
133
+ current = {};
134
+ }
135
+ }
136
+ };
79
137
  }
138
+ /**
139
+ * The money path (D10): P4 `createEditController` + the form controller,
140
+ * wired — record hydration into form defaults, `save` flowing server
141
+ * `fieldErrors` onto the form, success reconciling with SERVER truth
142
+ * (`form.reset(saved)` — D5), one subscription lifecycle spanning both.
143
+ */
80
144
  function createEditFormController(ctx, resource, options) {
81
- const recordController = createEditController(
82
- ctx,
83
- resource,
84
- {
85
- id: options.id,
86
- enabled: options.enabled,
87
- mutationMode: options.mutationMode,
88
- transform: options.transform,
89
- onRedirect: options.onRedirect
90
- }
91
- );
92
- const baseDefaults = options.defaultValues ?? {};
93
- const resetOnSuccess = options.resetOnSuccess !== false;
94
- const mergedWith = (record) => deepMerge(baseDefaults, record);
95
- let lastHydrated;
96
- const formController = createFormController({
97
- defaultValues: baseDefaults,
98
- schema: options.schema ?? registrySchema(ctx, resource),
99
- validate: options.validate,
100
- sanitizeEmptyValues: options.sanitizeEmptyValues,
101
- getRecord: () => recordController.getSnapshot().record,
102
- notifications: ctx.notifications,
103
- translate: ctx.translate,
104
- // The composed layer reconciles with SERVER truth in onSaved below — the
105
- // standalone submitted-values reset must not fire first.
106
- resetOnSuccess: false,
107
- save: (data) => recordController.save(data, {
108
- onSaved: async (saved) => {
109
- if (resetOnSuccess) {
110
- formController.form.reset(mergedWith(saved));
111
- lastHydrated = saved;
112
- }
113
- await options.onSuccess?.(saved);
114
- }
115
- }),
116
- formOptions: options.formOptions
117
- });
118
- const hydrate = () => {
119
- const { record } = recordController.getSnapshot();
120
- if (record === void 0 || record === lastHydrated) return;
121
- const form = formController.form;
122
- const action = resolveHydrationAction(
123
- lastHydrated === void 0 ? void 0 : lastHydrated.id,
124
- record.id,
125
- form.state.isTouched
126
- );
127
- lastHydrated = record;
128
- if (action === "keep-dirty") return;
129
- const merged = mergedWith(record);
130
- if (action === "hard-reset") {
131
- form.reset(merged);
132
- return;
133
- }
134
- if (action === "partial-apply") {
135
- const dirtyValues = [];
136
- for (const [path, meta] of Object.entries(
137
- form.state.fieldMeta
138
- )) {
139
- if (!meta?.isDirty) continue;
140
- const name = path;
141
- dirtyValues.push([name, form.getFieldValue(name)]);
142
- }
143
- form.reset(merged);
144
- for (const [path, value] of dirtyValues) {
145
- form.setFieldValue(path, value, { dontRunListeners: true });
146
- }
147
- return;
148
- }
149
- form.update({ ...form.options, defaultValues: merged });
150
- };
151
- const lifecycle = composeControllers(formController, recordController, () => {
152
- const off = recordController.subscribe(hydrate);
153
- hydrate();
154
- return off;
155
- });
156
- return { form: formController.form, recordController, ...lifecycle };
145
+ const recordController = createEditController(ctx, resource, {
146
+ id: options.id,
147
+ enabled: options.enabled,
148
+ mutationMode: options.mutationMode,
149
+ transform: options.transform,
150
+ onRedirect: options.onRedirect
151
+ });
152
+ const baseDefaults = options.defaultValues ?? {};
153
+ const resetOnSuccess = options.resetOnSuccess !== false;
154
+ const mergedWith = (record) => deepMerge(baseDefaults, record);
155
+ /** The last hydrated record REFERENCE — unrelated record-store writes (saving flips) skip hydration. */
156
+ let lastHydrated;
157
+ const submitOptions = createSubmitOptions();
158
+ const formController = createFormController({
159
+ defaultValues: baseDefaults,
160
+ schema: options.schema ?? registrySchema(ctx, resource),
161
+ validate: options.validate,
162
+ sanitizeEmptyValues: options.sanitizeEmptyValues,
163
+ getRecord: () => recordController.getSnapshot().record,
164
+ notifications: ctx.notifications,
165
+ translate: ctx.translate,
166
+ resetOnSuccess: false,
167
+ save: submitOptions.save(recordController, async (saved) => {
168
+ if (resetOnSuccess) {
169
+ formController.form.reset(mergedWith(saved));
170
+ lastHydrated = saved;
171
+ }
172
+ await options.onSuccess?.(saved);
173
+ }),
174
+ formOptions: options.formOptions
175
+ });
176
+ const hydrate = () => {
177
+ const { record } = recordController.getSnapshot();
178
+ if (record === void 0 || record === lastHydrated) return;
179
+ const form = formController.form;
180
+ const action = resolveHydrationAction(lastHydrated === void 0 ? void 0 : lastHydrated.id, record.id, form.state.isTouched);
181
+ lastHydrated = record;
182
+ if (action === "keep-dirty") return;
183
+ const merged = mergedWith(record);
184
+ if (action === "hard-reset") {
185
+ form.reset(merged);
186
+ return;
187
+ }
188
+ if (action === "partial-apply") {
189
+ const dirtyValues = [];
190
+ for (const [path, meta] of Object.entries(form.state.fieldMeta)) {
191
+ if (!meta?.isDirty) continue;
192
+ const name = toFieldName(path);
193
+ dirtyValues.push([name, form.getFieldValue(name)]);
194
+ }
195
+ form.reset(merged);
196
+ for (const [path, value] of dirtyValues) form.setFieldValue(path, value, { dontRunListeners: true });
197
+ return;
198
+ }
199
+ form.update({
200
+ ...form.options,
201
+ defaultValues: merged
202
+ });
203
+ };
204
+ const lifecycle = composeControllers(formController, recordController, (saveOptions) => submitOptions.submit(formController, saveOptions), () => {
205
+ const off = recordController.subscribe(hydrate);
206
+ hydrate();
207
+ return off;
208
+ });
209
+ return {
210
+ form: formController.form,
211
+ handle: toResourceFormHandle(formController.handle, resource),
212
+ recordController,
213
+ ...lifecycle
214
+ };
157
215
  }
216
+ /**
217
+ * D10's create arm: P4 `createCreateController` + the form controller. No
218
+ * hydration machinery — the seed record and defaults are both known at
219
+ * construction (seed wins, §0b precedence); success reconciles with the
220
+ * CREATED record (server-assigned id included).
221
+ */
158
222
  function createCreateFormController(ctx, resource, options = {}) {
159
- const recordController = createCreateController(
160
- ctx,
161
- resource,
162
- {
163
- record: options.record,
164
- transform: options.transform,
165
- onRedirect: options.onRedirect,
166
- mutationMode: options.mutationMode
167
- }
168
- );
169
- const seed = options.record ?? {};
170
- const baseDefaults = options.defaultValues ?? {};
171
- const resetOnSuccess = options.resetOnSuccess !== false;
172
- const formController = createFormController({
173
- defaultValues: deepMerge(baseDefaults, seed),
174
- schema: options.schema ?? registrySchema(ctx, resource),
175
- validate: options.validate,
176
- sanitizeEmptyValues: options.sanitizeEmptyValues,
177
- // ra parity: the create sanitize baseline is the seed record.
178
- getRecord: () => seed,
179
- notifications: ctx.notifications,
180
- translate: ctx.translate,
181
- resetOnSuccess: false,
182
- save: (data) => recordController.save(data, {
183
- onSaved: async (saved) => {
184
- if (resetOnSuccess) {
185
- formController.form.reset(
186
- deepMerge(baseDefaults, saved)
187
- );
188
- }
189
- await options.onSuccess?.(saved);
190
- }
191
- }),
192
- formOptions: options.formOptions
193
- });
194
- const lifecycle = composeControllers(formController, recordController);
195
- return { form: formController.form, recordController, ...lifecycle };
223
+ const recordController = createCreateController(ctx, resource, {
224
+ record: options.record,
225
+ transform: options.transform,
226
+ onRedirect: options.onRedirect,
227
+ mutationMode: options.mutationMode
228
+ });
229
+ const seed = options.record ?? {};
230
+ const baseDefaults = options.defaultValues ?? {};
231
+ const resetOnSuccess = options.resetOnSuccess !== false;
232
+ const submitOptions = createSubmitOptions();
233
+ const formController = createFormController({
234
+ defaultValues: deepMerge(baseDefaults, seed),
235
+ schema: options.schema ?? registrySchema(ctx, resource),
236
+ validate: options.validate,
237
+ sanitizeEmptyValues: options.sanitizeEmptyValues,
238
+ getRecord: () => seed,
239
+ notifications: ctx.notifications,
240
+ translate: ctx.translate,
241
+ resetOnSuccess: false,
242
+ save: submitOptions.save(recordController, async (saved) => {
243
+ if (resetOnSuccess) formController.form.reset(deepMerge(baseDefaults, saved));
244
+ await options.onSuccess?.(saved);
245
+ }),
246
+ formOptions: options.formOptions
247
+ });
248
+ const lifecycle = composeControllers(formController, recordController, (saveOptions) => submitOptions.submit(formController, saveOptions));
249
+ return {
250
+ form: formController.form,
251
+ handle: toResourceFormHandle(formController.handle, resource),
252
+ recordController,
253
+ ...lifecycle
254
+ };
196
255
  }
197
- export {
198
- createCreateFormController,
199
- createEditFormController,
200
- resolveHydrationAction
201
- };
256
+ export { createCreateFormController, createEditFormController, resolveHydrationAction };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * RHF-compatible event extraction: DOM/synthetic change events yield
3
+ * `target.checked` for checkboxes and `target.value` otherwise; anything
4
+ * else (a raw value, a custom picker's payload) passes through untouched.
5
+ */
6
+ export declare function extractEventValue(input: unknown): unknown;
@@ -0,0 +1,17 @@
1
+ import { hasProperty } from "../guards.js";
2
+ // src/form/event-value.ts
3
+ /**
4
+ * RHF-compatible event extraction: DOM/synthetic change events yield
5
+ * `target.checked` for checkboxes and `target.value` otherwise; anything
6
+ * else (a raw value, a custom picker's payload) passes through untouched.
7
+ */
8
+ function extractEventValue(input) {
9
+ if (!hasProperty(input, "target")) return input;
10
+ const { target } = input;
11
+ const hasValue = hasProperty(target, "value");
12
+ const hasChecked = hasProperty(target, "checked");
13
+ if (!hasValue && !hasChecked) return input;
14
+ if (hasProperty(target, "type") && target.type === "checkbox") return hasChecked ? target.checked : void 0;
15
+ return hasValue ? target.value : void 0;
16
+ }
17
+ export { extractEventValue };
@@ -0,0 +1,14 @@
1
+ import { type TranslatableMessage } from "./form-controller.js";
2
+ /**
3
+ * Error-map scan order: server/submit errors first, then validator causes.
4
+ * Internal to core — `firstFieldError` and the form-level scan both read it, so
5
+ * the order cannot drift between a field's error and a notification's.
6
+ */
7
+ export declare const ERROR_MAP_PRIORITY: readonly ["onSubmit", "onDynamic", "onChange", "onBlur", "onMount", "onServer"];
8
+ /**
9
+ * The first displayable error across a field's error-map causes, normalized.
10
+ * Takes any error-map shaped object (form-core's `ValidationErrorMap`, its
11
+ * form-level `FormValidationErrorMap`, a field meta's `errorMap`) and reads
12
+ * each cause through `Reflect.get`, so no call site needs a cast.
13
+ */
14
+ export declare function firstFieldError(errorMap: object): string | TranslatableMessage | undefined;
@@ -0,0 +1,28 @@
1
+ import { normalizeFormError } from "./form-controller.js";
2
+ // src/form/field-error.ts
3
+ /**
4
+ * Error-map scan order: server/submit errors first, then validator causes.
5
+ * Internal to core — `firstFieldError` and the form-level scan both read it, so
6
+ * the order cannot drift between a field's error and a notification's.
7
+ */
8
+ var ERROR_MAP_PRIORITY = [
9
+ "onSubmit",
10
+ "onDynamic",
11
+ "onChange",
12
+ "onBlur",
13
+ "onMount",
14
+ "onServer"
15
+ ];
16
+ /**
17
+ * The first displayable error across a field's error-map causes, normalized.
18
+ * Takes any error-map shaped object (form-core's `ValidationErrorMap`, its
19
+ * form-level `FormValidationErrorMap`, a field meta's `errorMap`) and reads
20
+ * each cause through `Reflect.get`, so no call site needs a cast.
21
+ */
22
+ function firstFieldError(errorMap) {
23
+ for (const cause of ERROR_MAP_PRIORITY) {
24
+ const normalized = normalizeFormError(Reflect.get(errorMap, cause));
25
+ if (normalized !== void 0) return normalized;
26
+ }
27
+ }
28
+ export { ERROR_MAP_PRIORITY, firstFieldError };
@@ -0,0 +1,20 @@
1
+ import type { DeepKeys, DeepValue } from "@tanstack/form-core";
2
+ /**
3
+ * The one place chelone speaks form-core's path types. `FieldApi` and `FormApi`
4
+ * constrain their generics on `DeepKeys`/`DeepValue`, while chelone's own paths
5
+ * are dot.paths' `SourcePath`/`SourceValue`. The two agree on a concrete record
6
+ * (pinned in `field-name.test-d.ts`), but TS cannot relate them while the record
7
+ * is a type parameter, so a path crosses over through {@link toFieldName}.
8
+ *
9
+ * These are renames, not aliases: a new alias of a conditional type is a
10
+ * distinct type to TS, which form-core's own signatures then fail to match.
11
+ */
12
+ export type { DeepKeys as FieldName, DeepValue as FieldValue };
13
+ /**
14
+ * The widest value a runtime-composed path can address. An alias, so it is for
15
+ * chelone's own signatures: spell `FieldValue<T, FieldName<T>>` to meet a
16
+ * value form-core returned.
17
+ */
18
+ export type FieldData<TRecord> = DeepValue<TRecord, DeepKeys<TRecord>>;
19
+ /** A runtime-composed path, as form-core's name for it. */
20
+ export declare function toFieldName<TRecord>(path: string): DeepKeys<TRecord>;
@@ -0,0 +1,6 @@
1
+ // src/form/field-name.ts
2
+ /** A runtime-composed path, as form-core's name for it. */
3
+ function toFieldName(path) {
4
+ return path;
5
+ }
6
+ export { toFieldName };