@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,60 +1,63 @@
1
1
  import { createMicroStore } from "../store/micro-store.js";
2
+ // src/environment/application-update-store.ts
3
+ /** djb2, forced to an unsigned 32-bit integer. */
2
4
  function hashText(text) {
3
- let hash = 5381;
4
- for (let i = 0; i < text.length; i++) {
5
- hash = (hash << 5) + hash + text.charCodeAt(i) >>> 0;
6
- }
7
- return hash;
5
+ let hash = 5381;
6
+ for (let i = 0; i < text.length; i++) hash = (hash << 5) + hash + text.charCodeAt(i) >>> 0;
7
+ return hash;
8
8
  }
9
+ /**
10
+ * Polls the deployed page and reports when its body differs from the first
11
+ * one seen, which means a new build is live. Inert until subscribed. The
12
+ * baseline survives detach and re-attach; polling stops for good once a change
13
+ * is seen.
14
+ */
9
15
  function createApplicationUpdateStore(options = {}) {
10
- const { interval = 36e5, disabled = false } = options;
11
- let baseline;
12
- let attachment = 0;
13
- let stopPolling;
14
- const store = createMicroStore(
15
- { hasNewVersion: false },
16
- {
17
- onSubscribed() {
18
- if (disabled || store.getSnapshot().hasNewVersion) return;
19
- const current = ++attachment;
20
- const timer = setInterval(() => void check(current), interval);
21
- stopPolling = () => clearInterval(timer);
22
- void check(current);
23
- return () => {
24
- attachment++;
25
- stopPolling?.();
26
- stopPolling = void 0;
27
- };
28
- }
29
- }
30
- );
31
- async function check(current) {
32
- const url = options.url ?? (typeof location === "undefined" ? void 0 : `${location.origin}/index.html`);
33
- if (url === void 0) return;
34
- const { fetchOptions } = options;
35
- let text;
36
- try {
37
- const init = typeof fetchOptions === "function" ? fetchOptions() : fetchOptions;
38
- const response = await fetch(url, { ...init, cache: "no-store" });
39
- if (!response.ok) return;
40
- text = await response.text();
41
- } catch {
42
- return;
43
- }
44
- if (current !== attachment || store.getSnapshot().hasNewVersion) return;
45
- const hash = hashText(text);
46
- if (baseline === void 0) {
47
- baseline = hash;
48
- return;
49
- }
50
- if (hash === baseline) return;
51
- stopPolling?.();
52
- stopPolling = void 0;
53
- store.setState({ hasNewVersion: true });
54
- options.onNewVersionAvailable?.();
55
- }
56
- return store;
16
+ const { interval = 36e5, disabled = false } = options;
17
+ let baseline;
18
+ /** Bumped on every detach so a check still in flight can tell it was dropped. */
19
+ let attachment = 0;
20
+ let stopPolling;
21
+ const store = createMicroStore({ hasNewVersion: false }, { onSubscribed() {
22
+ if (disabled || store.getSnapshot().hasNewVersion) return;
23
+ const current = ++attachment;
24
+ const timer = setInterval(() => void check(current), interval);
25
+ stopPolling = () => clearInterval(timer);
26
+ check(current);
27
+ return () => {
28
+ attachment++;
29
+ stopPolling?.();
30
+ stopPolling = void 0;
31
+ };
32
+ } });
33
+ async function check(current) {
34
+ const url = options.url ?? (typeof location === "undefined" ? void 0 : `${location.origin}/index.html`);
35
+ if (url === void 0) return;
36
+ const { fetchOptions } = options;
37
+ let text;
38
+ try {
39
+ const init = typeof fetchOptions === "function" ? fetchOptions() : fetchOptions;
40
+ const response = await fetch(url, {
41
+ ...init,
42
+ cache: "no-store"
43
+ });
44
+ if (!response.ok) return;
45
+ text = await response.text();
46
+ } catch {
47
+ return;
48
+ }
49
+ if (current !== attachment || store.getSnapshot().hasNewVersion) return;
50
+ const hash = hashText(text);
51
+ if (baseline === void 0) {
52
+ baseline = hash;
53
+ return;
54
+ }
55
+ if (hash === baseline) return;
56
+ stopPolling?.();
57
+ stopPolling = void 0;
58
+ store.setState({ hasNewVersion: true });
59
+ options.onNewVersionAvailable?.();
60
+ }
61
+ return store;
57
62
  }
58
- export {
59
- createApplicationUpdateStore
60
- };
63
+ export { createApplicationUpdateStore };
@@ -1,24 +1,24 @@
1
1
  import { createMicroStore } from "../store/micro-store.js";
2
- const readIsOffline = () => typeof navigator !== "undefined" && navigator.onLine === false;
2
+ // src/environment/offline-store.ts
3
+ var readIsOffline = () => typeof navigator !== "undefined" && navigator.onLine === false;
4
+ /**
5
+ * Whether the browser reports itself offline. Reads `navigator.onLine` rather
6
+ * than assuming online until an event fires, so a page opened offline is
7
+ * offline from the first snapshot. Inert until subscribed: nothing touches
8
+ * `window` before then, so it is safe to build during SSR.
9
+ */
3
10
  function createOfflineStore() {
4
- const store = createMicroStore(
5
- { isOffline: readIsOffline() },
6
- {
7
- onSubscribed() {
8
- store.setState({ isOffline: readIsOffline() });
9
- const goOffline = () => store.setState({ isOffline: true });
10
- const goOnline = () => store.setState({ isOffline: false });
11
- window.addEventListener("offline", goOffline);
12
- window.addEventListener("online", goOnline);
13
- return () => {
14
- window.removeEventListener("offline", goOffline);
15
- window.removeEventListener("online", goOnline);
16
- };
17
- }
18
- }
19
- );
20
- return store;
11
+ const store = createMicroStore({ isOffline: readIsOffline() }, { onSubscribed() {
12
+ store.setState({ isOffline: readIsOffline() });
13
+ const goOffline = () => store.setState({ isOffline: true });
14
+ const goOnline = () => store.setState({ isOffline: false });
15
+ window.addEventListener("offline", goOffline);
16
+ window.addEventListener("online", goOnline);
17
+ return () => {
18
+ window.removeEventListener("offline", goOffline);
19
+ window.removeEventListener("online", goOnline);
20
+ };
21
+ } });
22
+ return store;
21
23
  }
22
- export {
23
- createOfflineStore
24
- };
24
+ export { createOfflineStore };
@@ -1,4 +1,6 @@
1
1
  import type { LooseRecord } from "../resource/record.js";
2
+ import type { SourcePath } from "../resource/source-path.js";
3
+ import type { FieldName, FieldValue } from "./field-name.js";
2
4
  import type { CheloneFormApi } from "./form-controller.js";
3
5
  import { type FieldBinding, type FieldBindingOptions } from "./input-binding.js";
4
6
  /**
@@ -28,13 +30,29 @@ export interface ArrayRow {
28
30
  /** Moves THIS row to `to` — {@link ArrayBinding.reOrder} from the row's index. */
29
31
  reOrder(to: number): void;
30
32
  }
31
- export interface ArrayBindingOptions<TRecord = LooseRecord> extends Omit<FieldBindingOptions<TRecord>, "format" | "parse"> {
33
+ /** The array form-core stores at the binding's own path. */
34
+ type PathValue<TRecord, TPath extends string> = FieldValue<TRecord, TPath & FieldName<TRecord>>;
35
+ /**
36
+ * The item type of the array at `TPath`: the row shape of THIS binding's
37
+ * source, not the union of every array in the record. `unknown` when the path
38
+ * is one form-core cannot type — a composed `string` source (the ambient form),
39
+ * a scalar path, or a row-relative path that is no path of the record.
40
+ */
41
+ export type ArrayItemOf<TRecord, TPath extends string = SourcePath<TRecord>> = string extends TPath ? unknown : [PathValue<TRecord, TPath>] extends [never] ? unknown : PathValue<TRecord, TPath> extends readonly unknown[] ? PathValue<TRecord, TPath>[number] : unknown;
42
+ /**
43
+ * What a new row may be seeded with: a partial of an object row, or any object
44
+ * when the row type is unknown (a composed `string` source).
45
+ */
46
+ export type ItemDefaultsOf<TItem> = unknown extends TItem ? object : Partial<Extract<TItem, object>>;
47
+ export interface ArrayBindingOptions<TRecord = LooseRecord, TPath extends SourcePath<TRecord> = SourcePath<TRecord>> extends Omit<FieldBindingOptions<TRecord>, "format" | "parse" | "defaultValue"> {
48
+ /** The array field path this binding drives. */
49
+ source: TPath;
32
50
  /**
33
51
  * Seed rows for a value hole (ra precedence: an existing record value,
34
52
  * including `[]`, always wins). Defaults to `[]` so positional ops always
35
53
  * have an array to act on once attached. Never dirties the form.
36
54
  */
37
- defaultValue?: readonly unknown[];
55
+ defaultValue?: readonly ArrayItemOf<TRecord, TPath>[];
38
56
  /**
39
57
  * Scalar-array mode (#20): rows are primitives and `add()` pushes `''`.
40
58
  * When omitted the mode is DETECTED: `itemDefaults` present -> object;
@@ -49,10 +67,10 @@ export interface ArrayBindingOptions<TRecord = LooseRecord> extends Omit<FieldBi
49
67
  * (ra parity: object new item = first-row-keys-nulled + provided
50
68
  * defaults). Providing this implies object mode.
51
69
  */
52
- itemDefaults?: Record<string, unknown>;
70
+ itemDefaults?: ItemDefaultsOf<ArrayItemOf<TRecord, TPath>>;
53
71
  }
54
72
  /** `update()` payload: everything but the identity-defining source. */
55
- export type ArrayBindingUpdateOptions = Omit<ArrayBindingOptions<LooseRecord>, "source">;
73
+ export type ArrayBindingUpdateOptions<TRecord = LooseRecord, TPath extends SourcePath<TRecord> = SourcePath<TRecord>> = Omit<ArrayBindingOptions<TRecord, TPath>, "source">;
56
74
  /**
57
75
  * Vanilla array binding over a form-core array field — the useArrayInput
58
76
  * substrate (T8 wraps it in React contexts).
@@ -76,13 +94,13 @@ export type ArrayBindingUpdateOptions = Omit<ArrayBindingOptions<LooseRecord>, "
76
94
  * reconcile positionally (append/truncate) — use the binding's ops when row
77
95
  * identity matters.
78
96
  */
79
- export interface ArrayBinding<TRecord> {
97
+ export interface ArrayBinding<TRecord, TPath extends SourcePath<TRecord> = SourcePath<TRecord>> {
80
98
  /**
81
99
  * The array field's own binding (validators via options, meta/errors via
82
100
  * `field.getSnapshot()`). Do NOT create a second FieldBinding on the same
83
101
  * source — form-core registers ONE instance per field name.
84
102
  */
85
- field: FieldBinding<TRecord>;
103
+ field: FieldBinding<TRecord, TPath>;
86
104
  /** The array field path this binding is attached to. */
87
105
  readonly source: string;
88
106
  /**
@@ -103,11 +121,11 @@ export interface ArrayBinding<TRecord> {
103
121
  * Appends a row. Without `item`: `''` in scalar mode; in object mode the
104
122
  * first row's keys nulled merged with `itemDefaults`.
105
123
  */
106
- add(item?: unknown): void;
124
+ add(item?: ArrayItemOf<TRecord, TPath>): void;
107
125
  /** Inserts a row at `index` (clamped into range); same default item as `add`. */
108
- insert(index: number, item?: unknown): Promise<void>;
126
+ insert(index: number, item?: ArrayItemOf<TRecord, TPath>): Promise<void>;
109
127
  /** Replaces the row at `index` with `item` — the slot gets a NEW id. */
110
- replace(index: number, item: unknown): Promise<void>;
128
+ replace(index: number, item: ArrayItemOf<TRecord, TPath>): Promise<void>;
111
129
  /** Removes the row at `index` (form-core shifts meta, deletes the trailing slot, revalidates). */
112
130
  remove(index: number): Promise<void>;
113
131
  /** Swaps two rows — ids travel with the values. */
@@ -121,7 +139,7 @@ export interface ArrayBinding<TRecord> {
121
139
  * (validators flow into the underlying field binding; `scalar` /
122
140
  * `itemDefaults` / `defaultValue` update the array layer).
123
141
  */
124
- update(options: ArrayBindingUpdateOptions): void;
142
+ update(options: ArrayBindingUpdateOptions<TRecord, TPath>): void;
125
143
  /**
126
144
  * Imperative teardown for vanilla (non-React) consumers — delegates to the
127
145
  * underlying field binding: unmounts if attached, severs listeners, blocks
@@ -129,4 +147,5 @@ export interface ArrayBinding<TRecord> {
129
147
  */
130
148
  destroy(): void;
131
149
  }
132
- export declare function createArrayBinding<TRecord>(form: CheloneFormApi<TRecord>, options: ArrayBindingOptions<TRecord>): ArrayBinding<TRecord>;
150
+ export declare function createArrayBinding<TRecord, TPath extends SourcePath<TRecord> = SourcePath<TRecord>>(form: CheloneFormApi<TRecord>, options: ArrayBindingOptions<TRecord, TPath>): ArrayBinding<TRecord, TPath>;
151
+ export {};
@@ -1,170 +1,188 @@
1
+ import { isRecord } from "../guards.js";
1
2
  import { createFieldBinding } from "./input-binding.js";
2
- const HYDRATION_WRITE = {
3
- dontUpdateMeta: true,
4
- dontValidate: true,
5
- dontRunListeners: true
3
+ // src/form/array-binding.ts
4
+ /** Record-hydration write: value only, meta untouched, no validation. */
5
+ var HYDRATION_WRITE = {
6
+ dontUpdateMeta: true,
7
+ dontValidate: true,
8
+ dontRunListeners: true
6
9
  };
7
- const EMPTY = [];
8
- const isPlainRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
10
+ var EMPTY = [];
9
11
  function createArrayBinding(form, options) {
10
- const { source } = options;
11
- let current = options;
12
- const api = form;
13
- const toFieldOptions = () => ({
14
- source,
15
- defaultValue: current.defaultValue ? [...current.defaultValue] : [],
16
- validate: current.validate,
17
- schema: current.schema,
18
- validatorMeta: current.validatorMeta
19
- });
20
- const fieldBinding = createFieldBinding(form, toFieldOptions());
21
- const currentArray = () => {
22
- const value = api.getFieldValue(source);
23
- return Array.isArray(value) ? value : EMPTY;
24
- };
25
- const ensureSeeded = () => {
26
- if (api.getFieldValue(source) !== void 0) return;
27
- api.setFieldValue(
28
- source,
29
- current.defaultValue ? [...current.defaultValue] : [],
30
- HYDRATION_WRITE
31
- );
32
- };
33
- let nextRowId = 0;
34
- const ids = [];
35
- const allocId = () => `row-${nextRowId++}`;
36
- const syncIds = (length) => {
37
- while (ids.length < length) ids.push(allocId());
38
- if (ids.length > length) ids.length = length;
39
- };
40
- const structuralStamp = () => `${api.getFieldMeta(source)?._arrayVersion ?? 0}:${currentArray().length}`;
41
- const firstRowProbe = () => {
42
- const rows = currentArray();
43
- return rows.length > 0 ? rows[0] : current.defaultValue?.[0];
44
- };
45
- const isScalarMode = () => {
46
- if (current.scalar !== void 0) return current.scalar;
47
- if (current.itemDefaults !== void 0) return false;
48
- const probe = firstRowProbe();
49
- return probe === void 0 ? true : !isPlainRecord(probe);
50
- };
51
- const newItem = () => {
52
- if (isScalarMode()) return "";
53
- const template = {};
54
- const probe = firstRowProbe();
55
- if (isPlainRecord(probe)) {
56
- for (const key of Object.keys(probe)) template[key] = null;
57
- }
58
- return { ...template, ...current.itemDefaults };
59
- };
60
- const add = (item) => {
61
- ensureSeeded();
62
- syncIds(currentArray().length);
63
- ids.push(allocId());
64
- api.pushFieldValue(source, item !== void 0 ? item : newItem());
65
- };
66
- const insert = async (index, item) => {
67
- ensureSeeded();
68
- const length = currentArray().length;
69
- syncIds(length);
70
- const at = Math.max(0, Math.min(index, length));
71
- ids.splice(at, 0, allocId());
72
- await api.insertFieldValue(
73
- source,
74
- at,
75
- item !== void 0 ? item : newItem()
76
- );
77
- };
78
- const replace = async (index, item) => {
79
- ensureSeeded();
80
- const length = currentArray().length;
81
- syncIds(length);
82
- if (index < 0 || index >= length) return;
83
- ids[index] = allocId();
84
- await api.replaceFieldValue(source, index, item);
85
- };
86
- const remove = async (index) => {
87
- ensureSeeded();
88
- const length = currentArray().length;
89
- syncIds(length);
90
- if (index < 0 || index >= length) return;
91
- ids.splice(index, 1);
92
- await api.removeFieldValue(source, index);
93
- };
94
- const swap = (indexA, indexB) => {
95
- ensureSeeded();
96
- const length = currentArray().length;
97
- syncIds(length);
98
- if (indexA < 0 || indexA >= length || indexB < 0 || indexB >= length) {
99
- return;
100
- }
101
- const idA = ids[indexA];
102
- const idB = ids[indexB];
103
- if (idA === void 0 || idB === void 0) return;
104
- ids[indexA] = idB;
105
- ids[indexB] = idA;
106
- api.swapFieldValues(source, indexA, indexB);
107
- };
108
- const reOrder = (from, to) => {
109
- ensureSeeded();
110
- const length = currentArray().length;
111
- syncIds(length);
112
- if (from < 0 || from >= length) return;
113
- const [moved] = ids.splice(from, 1);
114
- if (moved === void 0) return;
115
- ids.splice(to, 0, moved);
116
- api.moveFieldValues(source, from, to);
117
- };
118
- const clear = () => {
119
- ensureSeeded();
120
- ids.length = 0;
121
- api.clearFieldValues(source);
122
- };
123
- const makeRow = (id, index) => ({
124
- id,
125
- index,
126
- name: `${source}[${index}]`,
127
- remove: () => remove(index),
128
- reOrder: (to) => reOrder(index, to)
129
- });
130
- let cachedRows;
131
- let cachedStamp;
132
- const getRows = () => {
133
- const stamp = structuralStamp();
134
- if (cachedRows !== void 0 && cachedStamp === stamp) return cachedRows;
135
- syncIds(currentArray().length);
136
- cachedRows = ids.map((id, index) => makeRow(id, index));
137
- cachedStamp = stamp;
138
- return cachedRows;
139
- };
140
- return {
141
- field: fieldBinding,
142
- source,
143
- subscribe(listener) {
144
- let previous = structuralStamp();
145
- return fieldBinding.subscribe(() => {
146
- const next = structuralStamp();
147
- if (next === previous) return;
148
- previous = next;
149
- listener();
150
- });
151
- },
152
- getRows,
153
- getTotal: () => currentArray().length,
154
- add,
155
- insert,
156
- replace,
157
- remove,
158
- swap,
159
- reOrder,
160
- clear,
161
- update(next) {
162
- current = { source, ...next };
163
- fieldBinding.update(toFieldOptions());
164
- },
165
- destroy: () => fieldBinding.destroy()
166
- };
12
+ const { source } = options;
13
+ let current = options;
14
+ const name = source;
15
+ /** A fresh copy of the seed rows — never the option's own array. */
16
+ const seedRows = () => current.defaultValue ? [...current.defaultValue] : [];
17
+ /** The field-binding view of the current options (fresh seed array per call). */
18
+ const toFieldOptions = () => ({
19
+ defaultValue: seedRows(),
20
+ validate: current.validate,
21
+ schema: current.schema,
22
+ validatorMeta: current.validatorMeta
23
+ });
24
+ const fieldBinding = createFieldBinding(form, {
25
+ source,
26
+ ...toFieldOptions()
27
+ });
28
+ const currentArray = () => {
29
+ const value = form.getFieldValue(name);
30
+ return Array.isArray(value) ? value : EMPTY;
31
+ };
32
+ /** Materializes the guarded default so positional ops act on a real array. */
33
+ const ensureSeeded = () => {
34
+ if (form.getFieldValue(name) !== void 0) return;
35
+ const seed = seedRows();
36
+ form.setFieldValue(name, seed, HYDRATION_WRITE);
37
+ };
38
+ let nextRowId = 0;
39
+ const ids = [];
40
+ const allocId = () => `row-${nextRowId++}`;
41
+ /**
42
+ * Reconciles ids with the actual row count before every op/read — external
43
+ * writes (reset, hydration, direct setFieldValue) reconcile positionally:
44
+ * extra rows get fresh ids appended, missing rows truncate from the end.
45
+ */
46
+ const syncIds = (length) => {
47
+ while (ids.length < length) ids.push(allocId());
48
+ if (ids.length > length) ids.length = length;
49
+ };
50
+ /** Changes exactly when the row STRUCTURE changes (ground truth #6). */
51
+ const structuralStamp = () => `${form.getFieldMeta(name)?._arrayVersion ?? 0}:${currentArray().length}`;
52
+ /** The row shape probe: first current row, else first default row. */
53
+ const firstRowProbe = () => {
54
+ const rows = currentArray();
55
+ return rows.length > 0 ? rows[0] : current.defaultValue?.[0];
56
+ };
57
+ const isScalarMode = () => {
58
+ if (current.scalar !== void 0) return current.scalar;
59
+ if (current.itemDefaults !== void 0) return false;
60
+ const probe = firstRowProbe();
61
+ return probe === void 0 ? true : !isRecord(probe);
62
+ };
63
+ /** Object mode: the first row's keys, every one nulled. */
64
+ const emptyRowTemplate = () => {
65
+ const template = {};
66
+ const probe = firstRowProbe();
67
+ if (isRecord(probe)) for (const key of Object.keys(probe)) template[key] = null;
68
+ return template;
69
+ };
70
+ const newItem = () => {
71
+ return isScalarMode() ? "" : {
72
+ ...emptyRowTemplate(),
73
+ ...current.itemDefaults
74
+ };
75
+ };
76
+ /**
77
+ * Hands a row to form-core, whose array ops are typed by the union of every
78
+ * array path in the record. {@link ArrayItemOf} is the row type of THIS
79
+ * binding's path, and `unknown` for a composed `string` source, where
80
+ * form-core types the item as `never`.
81
+ */
82
+ const formItem = (value) => value;
83
+ const add = (item) => {
84
+ ensureSeeded();
85
+ syncIds(currentArray().length);
86
+ ids.push(allocId());
87
+ form.pushFieldValue(name, formItem(item !== void 0 ? item : newItem()));
88
+ };
89
+ const insert = async (index, item) => {
90
+ ensureSeeded();
91
+ const length = currentArray().length;
92
+ syncIds(length);
93
+ const at = Math.max(0, Math.min(index, length));
94
+ ids.splice(at, 0, allocId());
95
+ await form.insertFieldValue(name, at, formItem(item !== void 0 ? item : newItem()));
96
+ };
97
+ const replace = async (index, item) => {
98
+ ensureSeeded();
99
+ const length = currentArray().length;
100
+ syncIds(length);
101
+ if (index < 0 || index >= length) return;
102
+ ids[index] = allocId();
103
+ await form.replaceFieldValue(name, index, formItem(item));
104
+ };
105
+ const remove = async (index) => {
106
+ ensureSeeded();
107
+ const length = currentArray().length;
108
+ syncIds(length);
109
+ if (index < 0 || index >= length) return;
110
+ ids.splice(index, 1);
111
+ await form.removeFieldValue(name, index);
112
+ };
113
+ const swap = (indexA, indexB) => {
114
+ ensureSeeded();
115
+ const length = currentArray().length;
116
+ syncIds(length);
117
+ if (indexA < 0 || indexA >= length || indexB < 0 || indexB >= length) return;
118
+ const idA = ids[indexA];
119
+ const idB = ids[indexB];
120
+ if (idA === void 0 || idB === void 0) return;
121
+ ids[indexA] = idB;
122
+ ids[indexB] = idA;
123
+ form.swapFieldValues(name, indexA, indexB);
124
+ };
125
+ const reOrder = (from, to) => {
126
+ ensureSeeded();
127
+ const length = currentArray().length;
128
+ syncIds(length);
129
+ if (from < 0 || from >= length) return;
130
+ const [moved] = ids.splice(from, 1);
131
+ if (moved === void 0) return;
132
+ ids.splice(to, 0, moved);
133
+ form.moveFieldValues(name, from, to);
134
+ };
135
+ const clear = () => {
136
+ ensureSeeded();
137
+ ids.length = 0;
138
+ form.clearFieldValues(name);
139
+ };
140
+ const makeRow = (id, index) => ({
141
+ id,
142
+ index,
143
+ name: `${source}[${index}]`,
144
+ remove: () => remove(index),
145
+ reOrder: (to) => reOrder(index, to)
146
+ });
147
+ let cachedRows;
148
+ let cachedStamp;
149
+ const getRows = () => {
150
+ const stamp = structuralStamp();
151
+ if (cachedRows !== void 0 && cachedStamp === stamp) return cachedRows;
152
+ syncIds(currentArray().length);
153
+ cachedRows = ids.map((id, index) => makeRow(id, index));
154
+ cachedStamp = stamp;
155
+ return cachedRows;
156
+ };
157
+ return {
158
+ field: fieldBinding,
159
+ source,
160
+ subscribe(listener) {
161
+ let previous = structuralStamp();
162
+ return fieldBinding.subscribe(() => {
163
+ const next = structuralStamp();
164
+ if (next === previous) return;
165
+ previous = next;
166
+ listener();
167
+ });
168
+ },
169
+ getRows,
170
+ getTotal: () => currentArray().length,
171
+ add,
172
+ insert,
173
+ replace,
174
+ remove,
175
+ swap,
176
+ reOrder,
177
+ clear,
178
+ update(next) {
179
+ current = {
180
+ source,
181
+ ...next
182
+ };
183
+ fieldBinding.update(toFieldOptions());
184
+ },
185
+ destroy: () => fieldBinding.destroy()
186
+ };
167
187
  }
168
- export {
169
- createArrayBinding
170
- };
188
+ export { createArrayBinding };
@@ -0,0 +1,7 @@
1
+ import type { Identifier } from "../resource/identifier.js";
2
+ /**
3
+ * The ids a choice input holds: the field value, or the values of its choices.
4
+ * A member that is not an {@link Identifier} is skipped, and anything that is
5
+ * not an array is no ids.
6
+ */
7
+ export declare function toIdentifierList(value: unknown): Identifier[];
@@ -0,0 +1,11 @@
1
+ import { identifiersOf } from "../resource/identifier.js";
2
+ // src/form/choice-ids.ts
3
+ /**
4
+ * The ids a choice input holds: the field value, or the values of its choices.
5
+ * A member that is not an {@link Identifier} is skipped, and anything that is
6
+ * not an array is no ids.
7
+ */
8
+ function toIdentifierList(value) {
9
+ return identifiersOf(value);
10
+ }
11
+ export { toIdentifierList };