@pilotiq/pilotiq 0.10.0 → 0.11.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.
@@ -1,8 +1,8 @@
1
1
 
2
- > @pilotiq/pilotiq@0.10.0 build /home/runner/work/pilotiq/pilotiq/packages/pilotiq
2
+ > @pilotiq/pilotiq@0.11.0 build /home/runner/work/pilotiq/pilotiq/packages/pilotiq
3
3
  > tsc -p tsconfig.build.json && pnpm run copy-assets
4
4
 
5
5
 
6
- > @pilotiq/pilotiq@0.10.0 copy-assets /home/runner/work/pilotiq/pilotiq/packages/pilotiq
6
+ > @pilotiq/pilotiq@0.11.0 copy-assets /home/runner/work/pilotiq/pilotiq/packages/pilotiq
7
7
  > mkdir -p dist/styles && cp -R src/styles/*.css dist/styles/
8
8
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,125 @@
1
1
  # @pilotiq/pilotiq
2
2
 
3
+ ## 0.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d36902d: feat(pilotiq): F.5a — Repeater/Builder row-identity contract for collab
8
+
9
+ Widens `FormCollabBinding` with five optional row-array methods plus a
10
+ `RowsEvent` type so the upcoming `Y.Array<Y.Map>` impl in
11
+ `@pilotiq-pro/collab` (F.5b) has a stable surface to hook into. Renderer
12
+ side wiring is live in this release — `RepeaterInput` + `BuilderInput`
13
+ already dispatch `add` / `remove` / `reorder` / `subscribe` through the
14
+ binding when one is registered and reconcile remote row events into
15
+ their local state by `__id`. No behaviour change for non-collab forms
16
+ or bindings that pre-date F.5; pre-F.5 bindings keep typechecking
17
+ because every new method is optional.
18
+
19
+ ### New public surface
20
+
21
+ - **`useRowBinding(arrayName)`** — returns a `RowBindingApi` pre-bound
22
+ to a Repeater/Builder field name, or `null` when no F.5 binding is
23
+ active (outside a collab room, pre-F.5 plugin, opted out via
24
+ `.collab(false)`, or non-array field).
25
+ - **`RowBindingApi`** — `{ add, remove, reorder, subscribe }`. Each
26
+ method's `arrayName` arg is pre-bound; `subscribe(fn)` returns an
27
+ unsubscribe function for `useEffect` cleanup.
28
+ - **`RowsEvent`** — `add | remove | move` discriminated union with
29
+ `rowId` + indices for the renderer to reconcile against its current
30
+ `rows` state.
31
+ - **`FormCollabBinding.addRow / removeRow / reorderRows / setRow /
32
+ getRowTextBinding / subscribeRows`** — all optional. Bindings opt
33
+ into F.5 by implementing the trio `addRow + removeRow + reorderRows`;
34
+ `subscribeRows` and `setRow` layer on for remote-event + dotted-path
35
+ routing; `getRowTextBinding` is reserved for F.5c (per-row `Y.Text`).
36
+
37
+ ### `FormStateProvider` routing
38
+
39
+ - `setValue` and the live-resolve overlay both route through
40
+ `routeBindingWrite` — top-level names go to `binding.set`, row leaves
41
+ (matching `parseRowFieldPath`) go to `binding.setRow` when available.
42
+ Pre-F.5 row leaves continue to stay local-only.
43
+ - The provider walks `formMeta` for top-level Repeater/Builder field
44
+ names at binding mount and builds a per-array `RowBindingApi` map
45
+ exposed via `useRowBinding`.
46
+
47
+ ### Known v1 limitations (kept from the F.5 plan)
48
+
49
+ - Nested Repeaters (e.g. `articles.0.comments.0.body`) stay local-only
50
+ — `parseRowFieldPath` returns `null` and the binding never sees them.
51
+ - Server-derived row values now propagate through `setRow` when
52
+ available; without an F.5 binding they continue to be dropped.
53
+ - F.5c (`getRowTextBinding`) — character-level `Y.Text` per row text
54
+ field — lands in a follow-up; row leaves stay on row-level LWW until
55
+ then.
56
+
57
+ - b70cb49: feat(pilotiq): F.5c — per-row Y.Text composition with F.6
58
+
59
+ Wires `useFieldState(dottedName).textBinding` to resolve through
60
+ `FormCollabBinding.getRowTextBinding(arrayName, rowId, fieldName)` so
61
+ Repeater/Builder row text fields ride character-level CRDT when the
62
+ plugin implements F.5c. Previously dotted-name `textBinding` always
63
+ returned `null`; now it returns a stable handle when:
64
+
65
+ - the row's `__id` is already stamped in the values map,
66
+ - the inner field's `fieldType` is in the F.6 allowlist
67
+ (`text / textarea / email / slug / markdown`),
68
+ - the field isn't opted out via `.collab(false)`,
69
+ - the active binding implements `getRowTextBinding`.
70
+
71
+ ### Walker
72
+
73
+ A new `collectRowTextLeavesByArray(formMeta)` helper walks each
74
+ Repeater's inner schema + each Builder block's template once at
75
+ binding mount and stashes the per-array text-leaf names on
76
+ `FormStateApi.rowTextLeaves`. Nested Repeater/Builder boundaries stop
77
+ the walk — 5-segment dotted paths remain out of scope.
78
+
79
+ ### Renderer surface unchanged
80
+
81
+ `BoundTextInput` already branches on `textBinding != null` from F.6,
82
+ so rows pick up the character-level path automatically once an F.5c-
83
+ capable binding is registered. No new renderer wiring beyond the
84
+ walker + `useFieldState` resolver.
85
+
86
+ ### Patch Changes
87
+
88
+ - 08ab5bb: fix(pilotiq): F.5c row-text integration — stamp row `__id` and walk `template` not `children`
89
+
90
+ Two integration gaps in the just-shipped F.5c per-row Y.Text path made
91
+ character-level CRDT silently fall back to LWW for every Repeater /
92
+ Builder row text leaf. Both green-CI / broken-at-render bugs.
93
+
94
+ ### `collectRowTextLeavesByArray` walked the wrong meta key
95
+
96
+ The walker read `meta.children` for the Repeater's inner row schema,
97
+ but `RepeaterField.toMeta()` emits the row schema under `meta.template`
98
+ (`meta.children` is the per-resolved-row child list, not the field-
99
+ level template). Walker always returned empty → `FormStateApi.rowTextLeaves`
100
+ stayed `null` → `useFieldState(dottedName).textBinding` short-circuited
101
+ on every dotted row-leaf name. The unit-test fixture mirrored the same
102
+ wrong shape, so CI passed while the renderer was inert.
103
+
104
+ ### `RepeaterInput` / `BuilderInput` never stamped row `__id` in `ctx.values`
105
+
106
+ `resolveRowTextBinding` looks up `rowIdAtIndex(ctx.values, name, i)` which
107
+ reads `values['${name}.${i}.__id']`. The renderer maintained row identity
108
+ in local component state but never mirrored it into `ctx.values`, so the
109
+ lookup returned `null` and the binding chain never fired — even for
110
+ locally-added rows.
111
+
112
+ Both renderers now mirror `rows` into `ctx.values` via a single
113
+ `useEffect` keyed on the rows array. Pre-existing server-seeded rows
114
+ were unaffected because the seed wasn't a renderer concern; only
115
+ locally-added or remote-reconciled rows hit the gap.
116
+
117
+ ### Tests
118
+
119
+ `formStateHelpers.test.ts`'s hand-built `repeater()` helper now emits
120
+ `template:` instead of `children:` to match `RepeaterField.toMeta()`.
121
+ Catches future drift between meta producers and walkers.
122
+
3
123
  ## 0.10.0
4
124
 
5
125
  ### Minor Changes
@@ -89,7 +89,137 @@ export interface FormCollabBinding {
89
89
  getTextBinding?(name: string): TextBinding | null;
90
90
  /** Cleanup hook called when the form unmounts. */
91
91
  destroy(): void;
92
+ /**
93
+ * Add a row to a Repeater or Builder field. `rowId` is the stable
94
+ * `__id` the renderer already mints (UUID for new rows / DB PK for
95
+ * relationship-backed rows) — the binding indexes the row's CRDT
96
+ * surface by this id so concurrent inserts from peers both survive.
97
+ *
98
+ * Idempotent — calling with a `rowId` that already exists in the
99
+ * Repeater's array is a no-op. `initial` may carry pre-filled values
100
+ * for the new row (e.g. the inner fields' `default()` from schema
101
+ * resolution); the binding seeds them onto the row's storage under
102
+ * the same idempotent posture as top-level `set` (first writer wins
103
+ * across concurrent first-mounters).
104
+ *
105
+ * Renderer call sites: `RepeaterInput.addRow()` + `BuilderInput.addBlock()`.
106
+ * When absent, F.5 row-level CRDT degrades to v1 behaviour (whole-array
107
+ * LWW under the top-level Y.Map).
108
+ */
109
+ addRow?(arrayName: string, rowId: string, initial: Record<string, unknown>): void;
110
+ /**
111
+ * Remove a row from a Repeater/Builder field by stable id. Idempotent
112
+ * — a missing `rowId` is a no-op. Triggered by the renderer's row
113
+ * delete button.
114
+ */
115
+ removeRow?(arrayName: string, rowId: string): void;
116
+ /**
117
+ * Apply a new row order. `newOrder` is the full list of row ids in
118
+ * their final positions; the binding computes the minimal CRDT move
119
+ * sequence and applies it inside a single transaction (peers see one
120
+ * coalesced update, not N intermediate states).
121
+ *
122
+ * Called by drag-and-drop / up-down move handlers in
123
+ * `RepeaterInput` + `BuilderInput`.
124
+ */
125
+ reorderRows?(arrayName: string, newOrder: string[]): void;
126
+ /**
127
+ * Write a single field on a row. Replaces the dotted-path `set` for
128
+ * row leaves (`tags.0.label` → `setRow('tags', rowId, 'label', value)`).
129
+ *
130
+ * Binding routes by allowlist same as top-level `set`: text-shaped
131
+ * fields go through the row's `TextBinding` when a `Y.Text` exists
132
+ * (see `getRowTextBinding`); non-text fields land on the row's
133
+ * scalar field-map under LWW.
134
+ *
135
+ * `FormStateProvider` calls this on every local edit when both a
136
+ * dotted-path name is being written AND `setRow` is implemented;
137
+ * otherwise the v1 skip-on-dot path runs.
138
+ */
139
+ setRow?(arrayName: string, rowId: string, fieldName: string, value: unknown): void;
140
+ /**
141
+ * Per-row text-CRDT handle. Composes with F.6's `BoundTextInput`:
142
+ * the renderer asks for one of these when a row leaf is text-shaped
143
+ * AND not opted out via `.collab(false)`, then threads it through
144
+ * the existing `TextBinding` plumbing inside the row.
145
+ *
146
+ * Returns `null` for non-text fields, fields opted out via collab,
147
+ * rows not yet present in the binding's index (e.g. before
148
+ * `addRow` has propagated), or when the binding doesn't yet
149
+ * implement per-row text CRDT (deferred to F.5c).
150
+ */
151
+ getRowTextBinding?(arrayName: string, rowId: string, fieldName: string): TextBinding | null;
152
+ /**
153
+ * Subscribe to row-lifecycle events for a Repeater/Builder array.
154
+ * Fires on remote add / remove / move; the renderer reconciles its
155
+ * `rows` state by `__id`. Local mutations fire too — the renderer
156
+ * deduplicates by checking whether the event's `rowId` matches one
157
+ * it just dispatched itself (the binding's roundtrip + the
158
+ * renderer's optimistic update converge).
159
+ */
160
+ subscribeRows?(arrayName: string, fn: (event: RowsEvent) => void): () => void;
92
161
  }
162
+ /**
163
+ * Phase F.5 — array-scoped imperative API exposed to Repeater / Builder
164
+ * renderers through `useRowBinding(arrayName)`. Each method's first arg
165
+ * (`arrayName`) is pre-bound by the hook so call sites read naturally.
166
+ *
167
+ * Returned by the hook only when the active binding implements all four
168
+ * F.5 row methods (`addRow + removeRow + reorderRows`); partial impls
169
+ * read as null so renderers can do a single existence check before
170
+ * proceeding. `setRow` is intentionally NOT exposed here — it's invoked
171
+ * implicitly via `FormStateProvider.setValue`'s dotted-path routing,
172
+ * so renderers never need to touch it directly.
173
+ */
174
+ export interface RowBindingApi {
175
+ /** Register a new row in the array's CRDT surface. `initial` may
176
+ * carry pre-seeded values (e.g. clone of a sibling); pass `{}` (or
177
+ * omit) for empty rows. Idempotent under existing rowId. */
178
+ add(rowId: string, initial?: Record<string, unknown>): void;
179
+ /** Remove the row with the given id. Idempotent for unknown ids. */
180
+ remove(rowId: string): void;
181
+ /** Replace the array's row order. `newOrder` is the full list of row
182
+ * ids in their post-reorder positions; the binding emits the minimal
183
+ * CRDT move sequence in one transaction so peers observe a single
184
+ * coalesced update. */
185
+ reorder(newOrder: string[]): void;
186
+ /** Subscribe to remote row-lifecycle events on this array. Returns
187
+ * an unsubscribe fn the renderer's `useEffect` cleanup hangs on.
188
+ * Local mutations may also surface here (Yjs observers fire on
189
+ * local transactions); the renderer is expected to dedupe by
190
+ * rowId presence in its current state. Optional on the underlying
191
+ * contract — `null` when the binding lacks `subscribeRows`. */
192
+ subscribe(fn: (event: RowsEvent) => void): () => void;
193
+ }
194
+ /**
195
+ * Phase F.5 — row-lifecycle event surfaced through `subscribeRows`.
196
+ *
197
+ * - `add` — a new row was inserted at `index`. `values` carries
198
+ * the row's seeded field values (mirrors `addRow.initial`).
199
+ * - `remove` — a row was removed; `index` is its position at the
200
+ * moment of removal.
201
+ * - `move` — a row shifted positions. `from` and `to` are 0-based
202
+ * indices in the post-reorder layout (i.e. the row at
203
+ * position `from` ended up at `to`).
204
+ *
205
+ * Pilotiq core stays Yjs-free — the binding impl in `@pilotiq-pro/collab`
206
+ * translates `Y.Array<Y.Map>` `observe` deltas into these events.
207
+ */
208
+ export type RowsEvent = {
209
+ kind: 'add';
210
+ rowId: string;
211
+ index: number;
212
+ values: Record<string, unknown>;
213
+ } | {
214
+ kind: 'remove';
215
+ rowId: string;
216
+ index: number;
217
+ } | {
218
+ kind: 'move';
219
+ rowId: string;
220
+ from: number;
221
+ to: number;
222
+ };
93
223
  export interface FormCollabBindingFactoryArgs {
94
224
  /** Active collab room — provides `ydoc`, `provider`, `user`. Opaque to pilotiq core. */
95
225
  room: CollabRoom;
@@ -1 +1 @@
1
- {"version":3,"file":"FormCollabBindingRegistry.d.ts","sourceRoot":"","sources":["../../src/react/FormCollabBindingRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AAExD;;;;;;;GAOG;AACH,MAAM,MAAM,SAAS,GACjB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GACnD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAG,MAAM,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAEjE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,IAAQ,MAAM,CAAA;IAClB,UAAU,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAAA;IAClC,OAAO,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAAA;IAC/C,OAAO,IAAK,IAAI,CAAA;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,WAAW,iBAAiB;IAChC,mEAAmE;IACnE,GAAG,IAAW,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACrC,uEAAuE;IACvE,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAA;IACvC,oEAAoE;IACpE,SAAS,CAAC,EAAE,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,CAAA;IACtE;;;;kEAI8D;IAC9D,cAAc,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAAA;IACjD,kDAAkD;IAClD,OAAO,IAAO,IAAI,CAAA;CACnB;AAED,MAAM,WAAW,4BAA4B;IAC3C,wFAAwF;IACxF,IAAI,EAAK,UAAU,CAAA;IACnB;;;;;OAKG;IACH,MAAM,EAAG,MAAM,CAAA;IACf;;;;;;OAMG;IACH,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAChC;;;;;;;;;OASG;IACH,QAAQ,EAAE,WAAW,CAAA;CACtB;AAED,MAAM,MAAM,wBAAwB,GAAG,CAAC,IAAI,EAAE,4BAA4B,KAAK,iBAAiB,CAAA;AAIhG;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI,CAEjF;AAED,yDAAyD;AACzD,wBAAgB,oBAAoB,IAAI,wBAAwB,GAAG,IAAI,CAEtE"}
1
+ {"version":3,"file":"FormCollabBindingRegistry.d.ts","sourceRoot":"","sources":["../../src/react/FormCollabBindingRegistry.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"FormCollabBindingRegistry.js","sourceRoot":"","sources":["../../src/react/FormCollabBindingRegistry.ts"],"names":[],"mappings":"AAuHA,IAAI,QAAQ,GAAoC,IAAI,CAAA;AAEpD;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAAiC;IACzE,QAAQ,GAAG,OAAO,CAAA;AACpB,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,oBAAoB;IAClC,OAAO,QAAQ,CAAA;AACjB,CAAC"}
1
+ {"version":3,"file":"FormCollabBindingRegistry.js","sourceRoot":"","sources":["../../src/react/FormCollabBindingRegistry.ts"],"names":[],"mappings":"AAwPA,IAAI,QAAQ,GAAoC,IAAI,CAAA;AAEpD;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAAiC;IACzE,QAAQ,GAAG,OAAO,CAAA;AACpB,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,oBAAoB;IAClC,OAAO,QAAQ,CAAA;AACjB,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import type { ElementMeta } from '../schema/Element.js';
3
- import { type TextBinding } from './FormCollabBindingRegistry.js';
3
+ import { type RowBindingApi, type TextBinding } from './FormCollabBindingRegistry.js';
4
4
  export type FieldStatus = 'idle' | 'pending';
5
5
  export interface FormStateApi {
6
6
  values: Record<string, unknown>;
@@ -27,6 +27,29 @@ export interface FormStateApi {
27
27
  * non-null answers, so a `Map.get()` hit means the binding has opted
28
28
  * this field into the character-level path. */
29
29
  textBindings: ReadonlyMap<string, TextBinding> | null;
30
+ /** Phase F.5 — per-Repeater/Builder row-array bindings. `null` outside a
31
+ * collab room or when the binding doesn't implement F.5 row methods.
32
+ * Each entry's API methods are pre-bound to the array name so renderers
33
+ * call `.add(rowId, initial)` rather than `binding.addRow(name, …)`. */
34
+ rowBindings: ReadonlyMap<string, RowBindingApi> | null;
35
+ /**
36
+ * Phase F.5c — per-array set of inner-field names that should route
37
+ * through `Y.Text` (character-level CRDT) instead of row-level Y.Map
38
+ * LWW. Built from a single meta walk at binding mount. Read by
39
+ * `useFieldState(dottedName).textBinding` to decide whether to call
40
+ * `getRowTextBinding`. Sparse — only arrays with at least one
41
+ * text-shaped row leaf appear; absence on a key means "no text leaves
42
+ * in this Repeater/Builder".
43
+ */
44
+ rowTextLeaves: ReadonlyMap<string, ReadonlySet<string>> | null;
45
+ /**
46
+ * Phase F.5c — resolve a per-row `TextBinding`. Pre-bound to the
47
+ * active F.5 binding so consumers don't reach for `bindingRef`
48
+ * directly. Returns `null` when no binding implements F.5c OR the
49
+ * row+field doesn't qualify (renderer caller should fall back to
50
+ * `defaultValue` like a non-collab form).
51
+ */
52
+ getRowTextBinding: ((arrayName: string, rowId: string, fieldName: string) => TextBinding | null) | null;
30
53
  }
31
54
  /** Hook for direct access to the form context. Returns `null` outside a
32
55
  * `FormStateProvider` (e.g. an action modal, or a form without any live
@@ -70,6 +93,21 @@ export interface UseFieldStateResult {
70
93
  * `applyDelta + observe`; null → today's whole-string LWW path. */
71
94
  textBinding: TextBinding | null;
72
95
  }
96
+ /**
97
+ * Phase F.5 — return the row-array CRDT API for a Repeater/Builder field.
98
+ * Returns `null` when:
99
+ *
100
+ * - No `FormStateProvider` is mounted (e.g. uncontrolled form path).
101
+ * - No `<RecordCollabRoom>` is up-tree (no binding registered).
102
+ * - The active binding doesn't implement F.5's row methods.
103
+ * - The named field opted out via `.collab(false)` (skipped at meta walk).
104
+ * - The named field isn't a Repeater/Builder.
105
+ *
106
+ * RepeaterInput + BuilderInput call this once per render and proceed
107
+ * with the v1 local-only behaviour when null. The returned API methods
108
+ * are pre-bound to the array name so consumers don't repeat it.
109
+ */
110
+ export declare function useRowBinding(arrayName: string): RowBindingApi | null;
73
111
  /** Per-field accessor. Inside a `FormStateProvider` it returns the controlled
74
112
  * value + setter + live trigger; outside, it returns sentinels and callers
75
113
  * should fall back to `defaultValue` (uncontrolled inputs). */
@@ -1 +1 @@
1
- {"version":3,"file":"FormStateContext.d.ts","sourceRoot":"","sources":["../../src/react/FormStateContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAA;AACd,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAWvD,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,gCAAgC,CAAA;AAEvC,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,SAAS,CAAA;AAE5C,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACtC,QAAQ,EAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IACrD;;;;;;qEAMiE;IACjE,WAAW,EAAI,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;IAC9D,MAAM,EAAS,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;IACvC;8EAC0E;IAC1E,WAAW,EAAI,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,KAAK,IAAI,CAAA;IACzD,QAAQ,EAAO,WAAW,CAAA;IAC1B,QAAQ,EAAO,OAAO,CAAA;IACtB,WAAW,EAAI,CAAC,IAAI,EAAE,MAAM,KAAK,WAAW,CAAA;IAC5C;;;;;oDAKgD;IAChD,YAAY,EAAG,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,IAAI,CAAA;CACvD;AAID;;4DAE4D;AAC5D,wBAAgB,YAAY,IAAI,YAAY,GAAG,IAAI,CAElD;AAED;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,uBAA4B,CAAA;AAetD,MAAM,WAAW,mBAAmB;IAClC;;;;;;oDAMgD;IAChD,UAAU,EAAG,OAAO,CAAA;IACpB,KAAK,EAAQ,OAAO,CAAA;IACpB,QAAQ,EAAK,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;IACjC;;;;mBAIe;IACf,WAAW,EAAE,CAAC,aAAa,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;IAC9C,qEAAqE;IACrE,OAAO,EAAM,OAAO,CAAA;IACpB,MAAM,EAAO,MAAM,EAAE,CAAA;IACrB;;;;;;;wEAOoE;IACpE,WAAW,EAAE,WAAW,GAAG,IAAI,CAAA;CAChC;AAED;;gEAEgE;AAChE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,CA6B/D;AAWD,MAAM,WAAW,sBAAsB;IACrC;oDACgD;IAChD,WAAW,EAAI,WAAW,CAAA;IAC1B,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;IACvC,QAAQ,EAAO,KAAK,CAAC,SAAS,CAAA;IAC9B,+EAA+E;IAC/E,SAAS,CAAC,EAAK,OAAO,KAAK,CAAA;IAC3B;gFAC4E;IAC5E,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAA;IAC1C;;;;wDAIoD;IACpD,OAAO,CAAC,EAAO,KAAK,CAAC,SAAS,CAAC,eAAe,GAAG,IAAI,CAAC,CAAA;CACvD;AAED;;;uEAGuE;AACvE,wBAAgB,iBAAiB,CAAC,EAChC,WAAW,EACX,aAAa,EACb,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,OAAO,GACR,EAAE,sBAAsB,GAAG,KAAK,CAAC,YAAY,CAqW7C"}
1
+ {"version":3,"file":"FormStateContext.d.ts","sourceRoot":"","sources":["../../src/react/FormStateContext.tsx"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
3
- import { collectFieldDefaults, findFieldMeta, parseFormDataToNested, readNestedValue, writeNestedValue, } from './formStateHelpers.js';
3
+ import { collectFieldDefaults, collectRowArrayFieldNames, collectRowTextLeavesByArray, findFieldMeta, parseFormDataToNested, parseRowFieldPath, readNestedValue, routeBindingWrite, rowIdAtIndex, writeNestedValue, } from './formStateHelpers.js';
4
4
  import { runJsHandler } from './fieldJsHandler.js';
5
5
  import { useToast } from './Toaster.js';
6
6
  import { useCollabRoom } from './CollabRoomContext.js';
@@ -21,16 +21,24 @@ export function useFormState() {
21
21
  */
22
22
  export const FormIdContext = createContext('');
23
23
  /**
24
- * Phase F2returns `true` iff the named field has explicitly opted out
25
- * of realtime collab via `Field.collab(false)`. Sparse meta — absent =
26
- * inherit the panel default (collab on). Walks the form meta tree the
27
- * same way `findFieldMeta` does; cheap because it only runs on the
28
- * per-write path (already a hot path, but every check is one map
29
- * lookup + one boolean compare).
24
+ * Phase F.5return the row-array CRDT API for a Repeater/Builder field.
25
+ * Returns `null` when:
26
+ *
27
+ * - No `FormStateProvider` is mounted (e.g. uncontrolled form path).
28
+ * - No `<RecordCollabRoom>` is up-tree (no binding registered).
29
+ * - The active binding doesn't implement F.5's row methods.
30
+ * - The named field opted out via `.collab(false)` (skipped at meta walk).
31
+ * - The named field isn't a Repeater/Builder.
32
+ *
33
+ * RepeaterInput + BuilderInput call this once per render and proceed
34
+ * with the v1 local-only behaviour when null. The returned API methods
35
+ * are pre-bound to the array name so consumers don't repeat it.
30
36
  */
31
- function fieldOptsOutOfCollab(formMeta, name) {
32
- const meta = findFieldMeta(formMeta, name);
33
- return meta?.collab === false;
37
+ export function useRowBinding(arrayName) {
38
+ const ctx = useContext(FormStateContext);
39
+ if (!ctx?.rowBindings)
40
+ return null;
41
+ return ctx.rowBindings.get(arrayName) ?? null;
34
42
  }
35
43
  /** Per-field accessor. Inside a `FormStateProvider` it returns the controlled
36
44
  * value + setter + live trigger; outside, it returns sentinels and callers
@@ -58,13 +66,37 @@ export function useFieldState(name) {
58
66
  triggerLive: (valueOverride) => ctx.triggerLive(name, valueOverride),
59
67
  pending: ctx.fieldStatus(name) === 'pending',
60
68
  errors: ctx.errors[name] ?? [],
61
- // Phase F.6 — dotted-path inner-Repeater rows skipped in v1 (deferred
62
- // to F.5 alongside Y.Array row identity). Outside a collab room or
63
- // for non-text fields, the stash returns null and the renderer falls
64
- // back to today's whole-string LWW path.
65
- textBinding: dotted ? null : (ctx.textBindings?.get(name) ?? null),
69
+ // Phase F.6 — top-level text fields resolve from the binding-mount
70
+ // text stash. Phase F.5c dotted-path row leaves resolve through
71
+ // `getRowTextBinding` when the field is text-shaped AND the row's
72
+ // `__id` is already stamped in the values map. Outside a collab
73
+ // room, for non-text fields, or before `addRow` has settled the
74
+ // row's id, the lookup returns null and `BoundTextInput` falls
75
+ // back to today's uncontrolled-input path.
76
+ textBinding: dotted
77
+ ? resolveRowTextBinding(ctx, name)
78
+ : (ctx.textBindings?.get(name) ?? null),
66
79
  };
67
80
  }
81
+ /**
82
+ * Phase F.5c — dotted-name `TextBinding` resolver. Returns `null`
83
+ * whenever any precondition fails so the caller's renderer can take a
84
+ * single branch on null vs non-null.
85
+ */
86
+ function resolveRowTextBinding(ctx, dottedName) {
87
+ if (!ctx.rowTextLeaves || !ctx.getRowTextBinding)
88
+ return null;
89
+ const parsed = parseRowFieldPath(dottedName);
90
+ if (!parsed)
91
+ return null;
92
+ const set = ctx.rowTextLeaves.get(parsed.arrayName);
93
+ if (!set?.has(parsed.fieldName))
94
+ return null;
95
+ const rowId = rowIdAtIndex(ctx.values, parsed.arrayName, parsed.index);
96
+ if (!rowId)
97
+ return null;
98
+ return ctx.getRowTextBinding(parsed.arrayName, rowId, parsed.fieldName);
99
+ }
68
100
  /** Provider component for the controlled form path. Holds the values map,
69
101
  * the current form meta (replaced wholesale on live POST), and the
70
102
  * per-field live trigger. Mounted by `FormRenderer` when the form has a
@@ -82,6 +114,15 @@ export function FormStateProvider({ initialMeta, initialErrors, children, fetchI
82
114
  // existing `setValuesState` overlay below already triggers one when the
83
115
  // room has pre-existing state.
84
116
  const [textBindings, setTextBindings] = useState(null);
117
+ // Phase F.5 — per-Repeater/Builder row-array stash. Same lifecycle as
118
+ // `textBindings`: populated on collab mount when the binding implements
119
+ // F.5 row methods, cleared on unmount.
120
+ const [rowBindings, setRowBindings] = useState(null);
121
+ // Phase F.5c — per-array set of text-shaped row leaves + a pre-bound
122
+ // resolver. Both populated at binding mount when the active binding
123
+ // implements `getRowTextBinding`; cleared on unmount.
124
+ const [rowTextLeaves, setRowTextLeaves] = useState(null);
125
+ const [getRowTextBinding, setGetRowTextBinding] = useState(null);
85
126
  const { notify } = useToast();
86
127
  // Track an incrementing in-flight id so out-of-order responses are dropped.
87
128
  // useRef (not useState) so React StrictMode dev double-invokes don't
@@ -157,6 +198,52 @@ export function FormStateProvider({ initialMeta, initialErrors, children, fetchI
157
198
  if (textStash.size > 0)
158
199
  setTextBindings(textStash);
159
200
  }
201
+ // Phase F.5 — build a `RowBindingApi` per top-level Repeater/Builder
202
+ // field when the binding implements all three lifecycle methods. The
203
+ // walk reads from formMeta (structural — fields exist regardless of
204
+ // whether the form has any rows yet); the API is then pre-bound to
205
+ // the array name so `RepeaterInput` calls `rb.add(rowId, …)` rather
206
+ // than `binding.addRow(name, rowId, …)`. Partial F.5 impls (e.g. a
207
+ // binding that has addRow but not reorderRows) skip the stash — the
208
+ // contract says all three or nothing. F.5c's per-row text path is
209
+ // exposed separately via `getRowTextBinding` and stays optional.
210
+ if (binding.addRow && binding.removeRow && binding.reorderRows) {
211
+ const { addRow, removeRow, reorderRows, subscribeRows } = binding;
212
+ const arrayNames = collectRowArrayFieldNames(formMetaRef.current);
213
+ if (arrayNames.length > 0) {
214
+ const rowStash = new Map();
215
+ for (const arrayName of arrayNames) {
216
+ rowStash.set(arrayName, {
217
+ add: (rowId, initial = {}) => addRow.call(binding, arrayName, rowId, initial),
218
+ remove: (rowId) => removeRow.call(binding, arrayName, rowId),
219
+ reorder: (newOrder) => reorderRows.call(binding, arrayName, newOrder),
220
+ // Partial F.5 impl: a binding may ship add/remove/reorder
221
+ // without `subscribeRows` (e.g. tests). Substitute a no-op
222
+ // subscription so renderer code stays uniform — the cleanup
223
+ // fn is still called on unmount but no events ever arrive.
224
+ subscribe: subscribeRows
225
+ ? (fn) => subscribeRows.call(binding, arrayName, fn)
226
+ : () => () => { },
227
+ });
228
+ }
229
+ setRowBindings(rowStash);
230
+ }
231
+ }
232
+ // Phase F.5c — capture the per-array text-leaf allowlist + bind the
233
+ // row-text resolver to the active binding. `getRowTextBinding` may
234
+ // be absent on partial F.5 impls; we expose the resolver as null in
235
+ // that case so `useFieldState` short-circuits cleanly.
236
+ if (binding.getRowTextBinding) {
237
+ const leaves = collectRowTextLeavesByArray(formMetaRef.current);
238
+ if (leaves.size > 0) {
239
+ setRowTextLeaves(leaves);
240
+ const bound = binding.getRowTextBinding;
241
+ // useState's functional-updater overload would invoke the
242
+ // stored function during set; wrapping in a fresh closure keeps
243
+ // React's setState path from confusing it for an updater fn.
244
+ setGetRowTextBinding(() => (arrayName, rowId, fieldName) => bound.call(binding, arrayName, rowId, fieldName));
245
+ }
246
+ }
160
247
  // Subscribe to remote changes. Local writes ALSO trigger this
161
248
  // (Yjs observers fire on local transactions too) — the per-key
162
249
  // Object.is short-circuit below collapses them into no-op renders.
@@ -178,6 +265,9 @@ export function FormStateProvider({ initialMeta, initialErrors, children, fetchI
178
265
  binding.destroy();
179
266
  bindingRef.current = null;
180
267
  setTextBindings(null);
268
+ setRowBindings(null);
269
+ setRowTextLeaves(null);
270
+ setGetRowTextBinding(null);
181
271
  };
182
272
  // `valuesRef.current` is intentionally read once at mount — initial
183
273
  // values seed the binding; subsequent edits flow through `setValue`
@@ -239,14 +329,13 @@ export function FormStateProvider({ initialMeta, initialErrors, children, fetchI
239
329
  return prev;
240
330
  return { ...prev, [name]: value };
241
331
  });
242
- // Phase F2 — proxy the write through the collab binding when active
243
- // AND the field hasn't opted out via `.collab(false)`. Dotted-path
244
- // names (Repeater / Builder row leaves) stay local-only in v1; their
245
- // syncing belongs to Phase F.5 (`Y.Array<Y.Map>` row identity).
246
- const binding = bindingRef.current;
247
- if (binding && !name.includes('.') && !fieldOptsOutOfCollab(formMetaRef.current, name)) {
248
- binding.set(name, value);
249
- }
332
+ // Phase F2 / F.5 — proxy the write through the collab binding when
333
+ // active AND the field hasn't opted out via `.collab(false)`. Top-level
334
+ // fields ride `binding.set`. Row leaves (dotted paths matching
335
+ // `parseRowFieldPath`) route through `binding.setRow` when the
336
+ // binding implements F.5 — otherwise stay local-only (same posture
337
+ // as pre-F.5).
338
+ routeBindingWrite(bindingRef.current, formMetaRef.current, valuesRef.current, name, value);
250
339
  // Fire the client-side JS hook synchronously after the state write.
251
340
  // Dotted-name fields don't go through here (their setter is a no-op
252
341
  // in `useFieldState`); they fire JS via `triggerLive` instead so we
@@ -317,18 +406,19 @@ export function FormStateProvider({ initialMeta, initialErrors, children, fetchI
317
406
  const serverValues = data.form.values;
318
407
  if (serverValues) {
319
408
  setValuesState((prev) => ({ ...prev, ...serverValues }));
320
- // Phase F2 (Q2) — derived fields propagate to peers via the
321
- // collab binding so every client sees the auto-`slug` / etc.
322
- // without each peer roundtripping the server. Skip dotted-path
323
- // names + fields that opted out.
409
+ // Phase F2 (Q2) / F.5 — derived fields propagate to peers via the
410
+ // collab binding so every client sees the auto-`slug` / etc. without
411
+ // each peer roundtripping the server. Row leaves route through
412
+ // `setRow` when the binding implements F.5; top-level fields ride
413
+ // `set`. The rowId lookup needs the freshest values — merge
414
+ // `valuesRef.current` with the server overlay so a row-id stamped
415
+ // by this very server-resolve response is visible to `rowIdAtIndex`.
324
416
  const binding = bindingRef.current;
325
417
  if (binding) {
418
+ const lookupValues = { ...valuesRef.current, ...serverValues };
326
419
  for (const [k, v] of Object.entries(serverValues)) {
327
- if (k.includes('.'))
328
- continue;
329
- if (fieldOptsOutOfCollab(data.form, k))
330
- continue;
331
- binding.set(k, v);
420
+ // routeBindingWrite handles `.collab(false)` opt-out internally.
421
+ routeBindingWrite(binding, data.form, lookupValues, k, v);
332
422
  }
333
423
  }
334
424
  }
@@ -406,7 +496,10 @@ export function FormStateProvider({ initialMeta, initialErrors, children, fetchI
406
496
  inFlight,
407
497
  fieldStatus,
408
498
  textBindings,
409
- }), [values, setValue, triggerLive, errors, applyErrors, formMeta, inFlight, fieldStatus, textBindings]);
499
+ rowBindings,
500
+ rowTextLeaves,
501
+ getRowTextBinding,
502
+ }), [values, setValue, triggerLive, errors, applyErrors, formMeta, inFlight, fieldStatus, textBindings, rowBindings, rowTextLeaves, getRowTextBinding]);
410
503
  return (_jsx(FormStateContext.Provider, { value: api, children: children }));
411
504
  }
412
505
  //# sourceMappingURL=FormStateContext.js.map