@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,83 +1,83 @@
1
- import { normalizeFormError } from "./form-controller.js";
2
- const EMPTY_MEMBERS = Object.freeze([]);
1
+ import { firstFieldError } from "./field-error.js";
2
+ // src/form/form-groups.ts
3
+ /** Shared empty snapshot so unknown/empty groups return a stable reference. */
4
+ var EMPTY_MEMBERS = Object.freeze([]);
3
5
  function createFormGroups() {
4
- const groups = /* @__PURE__ */ new Map();
5
- const listeners = /* @__PURE__ */ new Set();
6
- const snapshots = /* @__PURE__ */ new Map();
7
- const notify = (group) => {
8
- snapshots.delete(group);
9
- for (const listener of listeners) listener();
10
- };
11
- return {
12
- register(group, source) {
13
- let members = groups.get(group);
14
- if (members?.has(source)) return;
15
- if (!members) {
16
- members = /* @__PURE__ */ new Set();
17
- groups.set(group, members);
18
- }
19
- members.add(source);
20
- notify(group);
21
- },
22
- unregister(group, source) {
23
- const members = groups.get(group);
24
- if (!members?.delete(source)) return;
25
- if (members.size === 0) groups.delete(group);
26
- notify(group);
27
- },
28
- getMembers(group) {
29
- const members = groups.get(group);
30
- if (!members || members.size === 0) return EMPTY_MEMBERS;
31
- const cached = snapshots.get(group);
32
- if (cached) return cached;
33
- const snapshot = Object.freeze([...members]);
34
- snapshots.set(group, snapshot);
35
- return snapshot;
36
- },
37
- subscribe(listener) {
38
- listeners.add(listener);
39
- return () => {
40
- listeners.delete(listener);
41
- };
42
- }
43
- };
44
- }
45
- const ERROR_MAP_PRIORITY = [
46
- "onSubmit",
47
- "onDynamic",
48
- "onChange",
49
- "onBlur",
50
- "onMount",
51
- "onServer"
52
- ];
53
- function firstMemberError(errorMap) {
54
- for (const cause of ERROR_MAP_PRIORITY) {
55
- const normalized = normalizeFormError(errorMap[cause]);
56
- if (normalized !== void 0) return normalized;
57
- }
58
- return void 0;
6
+ /** group → ordered set of member sources. */
7
+ const groups = /* @__PURE__ */ new Map();
8
+ const listeners = /* @__PURE__ */ new Set();
9
+ /** Per-group cached snapshot; invalidated (deleted) on that group's mutation. */
10
+ const snapshots = /* @__PURE__ */ new Map();
11
+ const notify = (group) => {
12
+ snapshots.delete(group);
13
+ for (const listener of listeners) listener();
14
+ };
15
+ return {
16
+ register(group, source) {
17
+ let members = groups.get(group);
18
+ if (members?.has(source)) return;
19
+ if (!members) {
20
+ members = /* @__PURE__ */ new Set();
21
+ groups.set(group, members);
22
+ }
23
+ members.add(source);
24
+ notify(group);
25
+ },
26
+ unregister(group, source) {
27
+ const members = groups.get(group);
28
+ if (!members?.delete(source)) return;
29
+ if (members.size === 0) groups.delete(group);
30
+ notify(group);
31
+ },
32
+ getMembers(group) {
33
+ const members = groups.get(group);
34
+ if (!members || members.size === 0) return EMPTY_MEMBERS;
35
+ const cached = snapshots.get(group);
36
+ if (cached) return cached;
37
+ const snapshot = Object.freeze([...members]);
38
+ snapshots.set(group, snapshot);
39
+ return snapshot;
40
+ },
41
+ subscribe(listener) {
42
+ listeners.add(listener);
43
+ return () => {
44
+ listeners.delete(listener);
45
+ };
46
+ }
47
+ };
59
48
  }
49
+ /**
50
+ * Pure reducer: aggregate a group's member fields into a badge-ready
51
+ * {@link GroupState}. A member with NO `fieldMeta` entry (a registered field
52
+ * not yet mounted) contributes nothing — it is treated as valid + clean, so a
53
+ * pending mount never flips the group invalid. `isValid` is an AND (any invalid
54
+ * member fails the group); `isDirty`/`isTouched`/`isValidating` are ORs.
55
+ * `errors` collects the first displayable message per invalid member.
56
+ */
60
57
  function reduceGroupState(fieldMeta, members) {
61
- let isValid = true;
62
- let isDirty = false;
63
- let isTouched = false;
64
- let isValidating = false;
65
- const errors = {};
66
- for (const source of members) {
67
- const meta = fieldMeta[source];
68
- if (!meta) continue;
69
- if (meta.isDirty) isDirty = true;
70
- if (meta.isTouched) isTouched = true;
71
- if (meta.isValidating) isValidating = true;
72
- if (!meta.isValid) {
73
- isValid = false;
74
- const error = firstMemberError(meta.errorMap);
75
- if (error !== void 0) errors[source] = error;
76
- }
77
- }
78
- return { isValid, isDirty, isTouched, isValidating, errors };
58
+ let isValid = true;
59
+ let isDirty = false;
60
+ let isTouched = false;
61
+ let isValidating = false;
62
+ const errors = {};
63
+ for (const source of members) {
64
+ const meta = fieldMeta[source];
65
+ if (!meta) continue;
66
+ if (meta.isDirty) isDirty = true;
67
+ if (meta.isTouched) isTouched = true;
68
+ if (meta.isValidating) isValidating = true;
69
+ if (!meta.isValid) {
70
+ isValid = false;
71
+ const error = firstFieldError(meta.errorMap);
72
+ if (error !== void 0) errors[source] = error;
73
+ }
74
+ }
75
+ return {
76
+ isValid,
77
+ isDirty,
78
+ isTouched,
79
+ isValidating,
80
+ errors
81
+ };
79
82
  }
80
- export {
81
- createFormGroups,
82
- reduceGroupState
83
- };
83
+ export { createFormGroups, reduceGroupState };
@@ -0,0 +1,175 @@
1
+ import type { ValidationCause } from "@tanstack/form-core";
2
+ import type { RegisteredResources } from "../resource/register.js";
3
+ import type { RecordOf, ResourceMap } from "../resource/registry.js";
4
+ import type { SourcePath, SourceValue } from "../resource/source-path.js";
5
+ import { type ArrayBinding, type ArrayBindingOptions } from "./array-binding.js";
6
+ import type { CheloneFormApi, CheloneFormState } from "./form-controller.js";
7
+ import { type FieldBinding, type FieldBindingOptions } from "./input-binding.js";
8
+ /**
9
+ * A map of resource name -> record type — the shape {@link FormContextHandleOf}
10
+ * reads. Core names the records and never their fields.
11
+ */
12
+ export type RecordMap = {
13
+ [name: string]: object;
14
+ };
15
+ /** The records of a resource-config map, keyed by resource name. */
16
+ type RecordsOf<TResources extends ResourceMap> = {
17
+ [TName in keyof TResources]: RecordOf<TResources, TName>;
18
+ };
19
+ /** form-core's derived store of a form's state, as the form itself keeps it. */
20
+ type FormStore<TRecord> = CheloneFormApi<TRecord>["store"];
21
+ /**
22
+ * The field-meta map of a form whose record type is erased: a form context
23
+ * spans every record below it and names none of them.
24
+ */
25
+ export type AmbientFieldMeta = CheloneFormState<unknown>["fieldMeta"];
26
+ /**
27
+ * The form state a handle reads: its values plus the flags an adapter renders.
28
+ * Deliberately not the whole form-core state — `fieldMeta` and `errorMap` are
29
+ * keyed by the record's paths and by its validator slots, and a handle has to
30
+ * stay readable from a form context that knows neither.
31
+ */
32
+ export interface FormSnapshot<TRecord> {
33
+ values: TRecord;
34
+ isSubmitting: boolean;
35
+ isDirty: boolean;
36
+ isValid: boolean;
37
+ isTouched: boolean;
38
+ isDefaultValue: boolean;
39
+ isSubmitSuccessful: boolean;
40
+ canSubmit: boolean;
41
+ }
42
+ type AmbientFieldOptions = FieldBindingOptions<unknown, string, unknown>;
43
+ type AmbientFieldBinding = FieldBinding<unknown, string, unknown>;
44
+ type AmbientArrayOptions = ArrayBindingOptions<unknown, string>;
45
+ type AmbientArrayBinding = ArrayBinding<unknown, string>;
46
+ /**
47
+ * A form with its record type erased: the shape a form context carries, since
48
+ * one context spans every input below it whatever their record. Every path is a
49
+ * `string` and every value unchecked; a {@link FormHandle} is the way back to a
50
+ * record, and {@link UnboundFormHandle} is this form as a context member.
51
+ */
52
+ export interface AmbientForm {
53
+ /**
54
+ * form-core's derived store of the record-free state: `values`, `fieldMeta`,
55
+ * `errorMap` and the form flags.
56
+ */
57
+ readonly store: FormStore<unknown>;
58
+ /** The value at a path. */
59
+ getFieldValue(path: string): unknown;
60
+ /** Writes a value at a path, marking the field touched and dirty. */
61
+ setFieldValue(path: string, value: unknown): void;
62
+ /** The field's meta (touched/dirty/errors/validators) at a path. */
63
+ getFieldMeta(path: string): AmbientFieldMeta[string];
64
+ /** Runs the validators of one field for `cause`, settling when they have. */
65
+ validateField(path: string, cause: ValidationCause): Promise<void>;
66
+ /** Runs the form's own submit: validate, then the save the form was built with. */
67
+ handleSubmit(): Promise<void>;
68
+ /** Rebaselines the form from `values`, or from its current defaults when omitted. */
69
+ reset(values?: unknown): void;
70
+ /**
71
+ * Binds one field of this form. Inert until the binding is subscribed to
72
+ * (see {@link FieldBinding}).
73
+ */
74
+ bindField(options: AmbientFieldOptions): AmbientFieldBinding;
75
+ /** Binds one array field of this form. Inert until subscribed to. */
76
+ bindArray(options: AmbientArrayOptions): AmbientArrayBinding;
77
+ }
78
+ /**
79
+ * The value at one path of `TRecord`. The path-union case — what a generic
80
+ * signature comparison instantiates `TPath` with — short-circuits instead of
81
+ * walking `Get` over the whole `Paths` union.
82
+ */
83
+ type HandleValue<TRecord, TPath extends string> = string extends TPath ? unknown : SourceValue<TRecord, TPath>;
84
+ /**
85
+ * The record-typed way into a form: every `source` is a path of `TRecord` and
86
+ * every value read or written is the value AT that path, not `unknown`.
87
+ *
88
+ * The handle is the form side of a form — values and state. Submission belongs
89
+ * to the controller that owns the save pipeline (`createFormController`,
90
+ * `createEditFormController`), which exposes a handle next to its own `submit`.
91
+ *
92
+ * Nothing here is keyed by the record's path union, so a handle always widens
93
+ * to {@link UnboundFormHandle} — the way a form context erases it.
94
+ */
95
+ export interface FormHandle<TRecord> {
96
+ /** The same form with its record type erased — what a form context carries. */
97
+ readonly ambient: AmbientForm;
98
+ /** The current values and form flags. */
99
+ getSnapshot(): FormSnapshot<TRecord>;
100
+ /** The value at `TPath`, including the `null`/`undefined` of an optional parent. */
101
+ getFieldValue<TPath extends string>(path: TPath & SourcePath<TRecord>): HandleValue<TRecord, TPath>;
102
+ /** Writes `value` at `TPath` (touched + dirty, validated per the form's logic). */
103
+ setFieldValue<TPath extends string>(path: TPath & SourcePath<TRecord>, value: HandleValue<TRecord, TPath>): void;
104
+ }
105
+ /**
106
+ * A {@link FormHandle} tagged with the resource its form edits. `TFormData` is
107
+ * the form's data type (a `Partial` of the record: a create form holds none of
108
+ * it, an edit form some).
109
+ */
110
+ export interface ResourceFormHandle<TName extends string, TFormData = unknown> extends FormHandle<TFormData> {
111
+ readonly resource: TName;
112
+ }
113
+ /** A handle the registry types no record for — the ambient form as a context member. */
114
+ export interface UnboundFormHandle extends FormHandle<unknown> {
115
+ readonly resource: undefined;
116
+ }
117
+ /**
118
+ * The handle a form context can hold: one member per resource of `TRecords`,
119
+ * each narrowing on its own `resource`, plus {@link UnboundFormHandle} for a
120
+ * form the registry does not type. `UnboundFormHandle` alone when the map names
121
+ * no fixed resource (`ResourceMap`, the unregistered case).
122
+ */
123
+ export type FormContextHandleOf<TRecords extends RecordMap> = string extends keyof TRecords ? UnboundFormHandle : {
124
+ [TName in keyof TRecords & string]: ResourceFormHandle<TName, Partial<TRecords[TName]>>;
125
+ }[keyof TRecords & string] | UnboundFormHandle;
126
+ /** {@link FormContextHandleOf} over the app's registered resources. */
127
+ export type FormContextHandle = FormContextHandleOf<RecordsOf<RegisteredResources>>;
128
+ /**
129
+ * Wraps a form-core form in a {@link FormHandle}: the typed members read and
130
+ * write through the form, and `ambient` is the same form with its record type
131
+ * erased.
132
+ */
133
+ export declare function createFormHandle<TRecord>(form: CheloneFormApi<TRecord>): FormHandle<TRecord>;
134
+ /**
135
+ * Tags `handle` with `resource`, keeping its record type: the handle a record
136
+ * view hands to its form components.
137
+ */
138
+ export declare function toResourceFormHandle<TName extends string, TFormData>(handle: FormHandle<TFormData>, resource: TName): ResourceFormHandle<TName, TFormData>;
139
+ /**
140
+ * Widens a handle to the form-context union, tagging it with `resource`, or
141
+ * leaving it unbound when the form edits no resource.
142
+ *
143
+ * A type-level move only: at runtime the object is `handle` with `resource`
144
+ * set. The registry — not the caller's generic — decides which record type the
145
+ * name carries, so no plain assignment can express it (see
146
+ * `form-handle.test-d.ts`).
147
+ */
148
+ export declare function toFormContextHandle<TName extends string, TFormData>(handle: FormHandle<TFormData>, resource: TName | undefined): FormContextHandle;
149
+ /**
150
+ * The members every form handle carries, whatever record it types. The
151
+ * inference anchor for {@link isResourceHandle}: a context union is a union of
152
+ * handles, and a union is not inferable through a mapped type.
153
+ */
154
+ export interface AnyFormHandle {
155
+ readonly resource?: string;
156
+ }
157
+ /**
158
+ * The names `THandle` can carry. A handle union that names no resource (nothing
159
+ * registered) takes any string, so a generic caller still compiles.
160
+ */
161
+ export type HandleName<THandle extends AnyFormHandle> = [
162
+ NonNullable<THandle["resource"]>
163
+ ] extends [never] ? string : NonNullable<THandle["resource"]>;
164
+ /**
165
+ * Narrows a handle to the member that names `name`.
166
+ *
167
+ * The narrowing a generic body cannot do itself: `handle.resource === name`
168
+ * with a generic `name` leaves the whole union (probed in
169
+ * `form-handle.test-d.ts`) because the compiler cannot rule out the other
170
+ * members. At runtime this checks `resource` and nothing else.
171
+ */
172
+ export declare function isResourceHandle<THandle extends AnyFormHandle, TName extends HandleName<THandle>>(handle: THandle, name: TName): handle is Extract<THandle, {
173
+ resource: TName;
174
+ }> & ResourceFormHandle<TName>;
175
+ export {};
@@ -0,0 +1,68 @@
1
+ import { createArrayBinding } from "./array-binding.js";
2
+ import { createFieldBinding } from "./input-binding.js";
3
+ // src/form/form-handle.ts
4
+ /**
5
+ * Wraps a form-core form in a {@link FormHandle}: the typed members read and
6
+ * write through the form, and `ambient` is the same form with its record type
7
+ * erased.
8
+ */
9
+ function createFormHandle(form) {
10
+ const loose = form;
11
+ const bindField = (options) => createFieldBinding(loose, options);
12
+ const bindArray = (options) => createArrayBinding(loose, options);
13
+ return {
14
+ ambient: {
15
+ store: loose.store,
16
+ getFieldValue: (path) => loose.getFieldValue(path),
17
+ setFieldValue: (path, value) => loose.setFieldValue(path, value),
18
+ getFieldMeta: (path) => loose.getFieldMeta(path),
19
+ validateField: async (path, cause) => {
20
+ await loose.validateField(path, cause);
21
+ },
22
+ handleSubmit: () => loose.handleSubmit(),
23
+ reset: (values) => loose.reset(values),
24
+ bindField,
25
+ bindArray
26
+ },
27
+ getSnapshot: () => form.state,
28
+ getFieldValue: (path) => loose.getFieldValue(path),
29
+ setFieldValue: (path, value) => loose.setFieldValue(path, value)
30
+ };
31
+ }
32
+ /**
33
+ * Tags `handle` with `resource`, keeping its record type: the handle a record
34
+ * view hands to its form components.
35
+ */
36
+ function toResourceFormHandle(handle, resource) {
37
+ return {
38
+ ...handle,
39
+ resource
40
+ };
41
+ }
42
+ /**
43
+ * Widens a handle to the form-context union, tagging it with `resource`, or
44
+ * leaving it unbound when the form edits no resource.
45
+ *
46
+ * A type-level move only: at runtime the object is `handle` with `resource`
47
+ * set. The registry — not the caller's generic — decides which record type the
48
+ * name carries, so no plain assignment can express it (see
49
+ * `form-handle.test-d.ts`).
50
+ */
51
+ function toFormContextHandle(handle, resource) {
52
+ return {
53
+ ...handle,
54
+ resource
55
+ };
56
+ }
57
+ /**
58
+ * Narrows a handle to the member that names `name`.
59
+ *
60
+ * The narrowing a generic body cannot do itself: `handle.resource === name`
61
+ * with a generic `name` leaves the whole union (probed in
62
+ * `form-handle.test-d.ts`) because the compiler cannot rule out the other
63
+ * members. At runtime this checks `resource` and nothing else.
64
+ */
65
+ function isResourceHandle(handle, name) {
66
+ return handle.resource === name;
67
+ }
68
+ export { createFormHandle, isResourceHandle, toFormContextHandle, toResourceFormHandle };
@@ -1,32 +1,36 @@
1
- import { type DeepKeys, type DeepValue, FieldApi, type FieldAsyncValidateOrFn, type FieldValidateOrFn, type StandardSchemaV1 } from "@tanstack/form-core";
1
+ import { FieldApi, type FieldAsyncValidateOrFn, type FieldValidateOrFn, type StandardSchemaV1 } from "@tanstack/form-core";
2
2
  import type { LooseRecord } from "../resource/record.js";
3
- import type { SourcePath } from "../resource/source-path.js";
3
+ import type { SourcePath, SourceValue } from "../resource/source-path.js";
4
+ import { type FieldData, type FieldName } from "./field-name.js";
4
5
  import type { AsyncValidator, CheloneFormApi, SyncValidator } from "./form-controller.js";
5
6
  import { type Validator } from "./validators.js";
6
7
  /**
7
8
  * ra's default format: render `null`/`undefined` as `''` so text inputs stay
8
9
  * controlled; every other value (including `0`/`false`) passes through.
9
10
  */
10
- export declare function defaultFormat(value: unknown): unknown;
11
+ export declare function defaultFormat<TValue>(value: TValue): FormattedValue<TValue>;
12
+ /**
13
+ * What a field's input holds under {@link defaultFormat}: the stored value with
14
+ * its holes spelled as `''`.
15
+ */
16
+ export type FormattedValue<TValue> = Exclude<TValue, null | undefined> | "";
11
17
  /**
12
18
  * ra's default parse: store a cleared text input (`''`) as `null` — the
13
19
  * shape most backends expect; every other value (including `0`/`false`)
14
20
  * passes through.
15
21
  */
16
- export declare function defaultParse(input: unknown): unknown;
17
- /** The widest data type a runtime-composed source path can address. */
18
- type FieldData<TRecord> = DeepValue<TRecord, DeepKeys<TRecord>>;
22
+ export declare function defaultParse<TInput>(input: TInput): Exclude<TInput, ""> | null;
19
23
  /** A sync field-validator slot at its widest legal instantiation. */
20
- type FieldSync<TRecord> = FieldValidateOrFn<TRecord, DeepKeys<TRecord>, FieldData<TRecord>> | undefined;
24
+ type FieldSync<TRecord> = FieldValidateOrFn<TRecord, FieldName<TRecord>, FieldData<TRecord>> | undefined;
21
25
  /** An async field-validator slot at its widest legal instantiation. */
22
- type FieldAsync<TRecord> = FieldAsyncValidateOrFn<TRecord, DeepKeys<TRecord>, FieldData<TRecord>> | undefined;
26
+ type FieldAsync<TRecord> = FieldAsyncValidateOrFn<TRecord, FieldName<TRecord>, FieldData<TRecord>> | undefined;
23
27
  /**
24
28
  * The wrapped FieldApi, generic in the DATA type only (the T1 strategy):
25
29
  * source paths are composed at runtime (`items[0].sku`), so the name/value
26
30
  * generics are pinned to their widest legal instantiations and the form
27
31
  * slots mirror {@link CheloneFormApi} exactly.
28
32
  */
29
- export type CheloneFieldApi<TRecord> = FieldApi<TRecord, DeepKeys<TRecord>, FieldData<TRecord>, FieldSync<TRecord>, // onMount
33
+ export type CheloneFieldApi<TRecord> = FieldApi<TRecord, FieldName<TRecord>, FieldData<TRecord>, FieldSync<TRecord>, // onMount
30
34
  FieldSync<TRecord>, // onChange
31
35
  FieldAsync<TRecord>, // onChangeAsync
32
36
  FieldSync<TRecord>, // onBlur
@@ -49,28 +53,31 @@ never>;
49
53
  /** The field state snapshot type — what {@link FieldBinding.getSnapshot} returns. */
50
54
  export type CheloneFieldState<TRecord> = CheloneFieldApi<TRecord>["state"];
51
55
  /**
52
- * The third validator argument (ra's `props` slot): always carries the final
53
- * source path; the binding's `validatorMeta` (input props in the React
54
- * layer) is merged in.
56
+ * form-core's field store: `{ value, meta }` for an adapter selector. An adapter
57
+ * cannot know the record of the field it renders, so it reads the default.
55
58
  */
56
- export interface FieldValidatorMeta {
57
- source: string;
58
- [key: string]: unknown;
59
- }
60
- export interface FieldBindingOptions<TRecord = LooseRecord> {
61
- /** The FINAL field path (already source-scoped — see `source.ts`). */
62
- source: SourcePath<TRecord>;
59
+ export type CheloneFieldStore<TRecord = unknown> = CheloneFieldApi<TRecord>["store"];
60
+ /**
61
+ * Everything a field binding takes except the identity-defining `source`,
62
+ * spelled for one value type: `TValue` is what the field holds, `TInput` what
63
+ * the input holds.
64
+ */
65
+ export interface FieldBindingBaseOptions<TValue, TInput> {
63
66
  /**
64
67
  * Input-level default, ra precedence: seeds ONLY into a hole (untouched
65
68
  * field whose form value is `undefined`) — an existing record value,
66
69
  * including `null`, always wins. Re-applied whenever the hole reopens
67
70
  * (reset/rehydration), never dirties the form.
68
71
  */
69
- defaultValue?: unknown;
70
- /** Read transform (raw form value -> input value). Default {@link defaultFormat}. */
71
- format?: (value: unknown) => unknown;
72
+ defaultValue?: TValue;
73
+ /**
74
+ * Read transform (raw form value -> input value). Default {@link defaultFormat}.
75
+ * Takes the holes a record's type does not show: an untouched field is
76
+ * `undefined`, and {@link defaultParse} stores a cleared input as `null`.
77
+ */
78
+ format?: (value: TValue | null | undefined) => TInput;
72
79
  /** Write transform (input value -> stored value). Default {@link defaultParse}. */
73
- parse?: (input: unknown) => unknown;
80
+ parse?: (input: TInput) => TValue;
74
81
  /** ra-contract validators — composed, first error wins, sync stays sync. */
75
82
  validate?: Validator | readonly Validator[];
76
83
  /**
@@ -82,8 +89,15 @@ export interface FieldBindingOptions<TRecord = LooseRecord> {
82
89
  /** Extra context merged into the validators' meta argument. */
83
90
  validatorMeta?: Record<string, unknown>;
84
91
  }
85
- /** `update()` payload: everything but the identity-defining source. */
86
- export type FieldBindingUpdateOptions = Omit<FieldBindingOptions<LooseRecord>, "source">;
92
+ export interface FieldBindingOptions<TRecord = LooseRecord, TPath extends SourcePath<TRecord> = SourcePath<TRecord>, TInput = FormattedValue<SourceValue<TRecord, TPath>>> extends FieldBindingBaseOptions<SourceValue<TRecord, TPath>, TInput> {
93
+ /** The FINAL field path (already source-scoped — see `source.ts`). */
94
+ source: TPath;
95
+ }
96
+ /**
97
+ * `update()` payload: everything but the identity-defining source. An adapter
98
+ * renders under any record, so it reads the defaults.
99
+ */
100
+ export type FieldBindingUpdateOptions<TValue = unknown, TInput = unknown> = FieldBindingBaseOptions<TValue, TInput>;
87
101
  /**
88
102
  * Vanilla field binding over a single `@tanstack/form-core` FieldApi —
89
103
  * the useInput substrate (T4 wraps it in React contexts).
@@ -102,12 +116,17 @@ export type FieldBindingUpdateOptions = Omit<FieldBindingOptions<LooseRecord>, "
102
116
  * submit). The sync slot stays synchronous; the FIRST thenable result
103
117
  * latches the binding onto the async slot (injected lazily so pure-sync
104
118
  * validators never flicker `isValidating`).
119
+ *
120
+ * `TRecord` is the form's data type, `TPath` the field's own source path and
121
+ * `TInput` what the input holds (`TPath`'s value under `format`), so
122
+ * `getValue()`/`setValue()` are typed for one field, not for the widest value
123
+ * in the record.
105
124
  */
106
- export interface FieldBinding<TRecord> {
125
+ export interface FieldBinding<TRecord, TPath extends SourcePath<TRecord> = SourcePath<TRecord>, TInput = unknown> {
107
126
  /** The wrapped FieldApi — the full form-core surface for advanced use. */
108
127
  field: CheloneFieldApi<TRecord>;
109
128
  /** The final field path this binding is attached to. */
110
- readonly source: string;
129
+ readonly source: TPath;
111
130
  /**
112
131
  * Refcounted subscription (uSES-compatible): registers `listener` with the
113
132
  * identity-memoized `field.store` (per-field render isolation) and mounts
@@ -117,10 +136,16 @@ export interface FieldBinding<TRecord> {
117
136
  subscribe(listener: () => void): () => void;
118
137
  /** The current field state — `field.store.state` (reference-stable until a write). */
119
138
  getSnapshot(): CheloneFieldState<TRecord>;
139
+ /**
140
+ * form-core's derived store of this field: `{ value, meta }` for an adapter
141
+ * selector. Same store object as `field.store` — subscribing here and to the
142
+ * form costs one subscription each, and neither mounts the field.
143
+ */
144
+ readonly store: CheloneFieldStore<TRecord>;
120
145
  /** The input-facing value: `format(raw)` — never `null`/`undefined` under the default format. */
121
- getValue(): unknown;
146
+ getValue(): TInput;
122
147
  /** Writes `parse(input)` through `handleChange` (marks touched+dirty, validates per revalidation mode). */
123
- setValue(input: unknown): void;
148
+ setValue(input: TInput): void;
124
149
  /** Marks the field blurred and runs blur-cause validation. */
125
150
  blur(): void;
126
151
  /** Requiredness detected from the RAW validate prop (`required()`'s tag). */
@@ -131,7 +156,7 @@ export interface FieldBinding<TRecord> {
131
156
  * current option set each render. Re-applies the guarded default when the
132
157
  * value hole reopened. Inert while detached (options apply on attach).
133
158
  */
134
- update(options: FieldBindingUpdateOptions): void;
159
+ update(options: FieldBindingUpdateOptions<SourceValue<TRecord, TPath>, TInput>): void;
135
160
  /**
136
161
  * Imperative teardown for vanilla (non-React) consumers: unmounts if
137
162
  * attached, severs every listener, and blocks re-attach. Idempotent. React
@@ -139,5 +164,5 @@ export interface FieldBinding<TRecord> {
139
164
  */
140
165
  destroy(): void;
141
166
  }
142
- export declare function createFieldBinding<TRecord>(form: CheloneFormApi<TRecord>, options: FieldBindingOptions<TRecord>): FieldBinding<TRecord>;
167
+ export declare function createFieldBinding<TRecord, TPath extends SourcePath<TRecord> = SourcePath<TRecord>, TInput = FormattedValue<SourceValue<TRecord, TPath>>>(form: CheloneFormApi<TRecord>, options: FieldBindingOptions<TRecord, TPath, TInput>): FieldBinding<TRecord, TPath, TInput>;
143
168
  export {};