@pilotiq/pilotiq 0.6.2 → 0.7.1

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 (197) hide show
  1. package/.turbo/turbo-build.log +6 -2
  2. package/CHANGELOG.md +614 -0
  3. package/CLAUDE.md +6 -5
  4. package/dist/Column.d.ts +35 -0
  5. package/dist/Column.d.ts.map +1 -1
  6. package/dist/Column.js +41 -0
  7. package/dist/Column.js.map +1 -1
  8. package/dist/Page.d.ts +13 -4
  9. package/dist/Page.d.ts.map +1 -1
  10. package/dist/Page.js +9 -2
  11. package/dist/Page.js.map +1 -1
  12. package/dist/Pilotiq.d.ts +84 -0
  13. package/dist/Pilotiq.d.ts.map +1 -1
  14. package/dist/Pilotiq.js +66 -0
  15. package/dist/Pilotiq.js.map +1 -1
  16. package/dist/Resource.d.ts +26 -0
  17. package/dist/Resource.d.ts.map +1 -1
  18. package/dist/Resource.js +9 -0
  19. package/dist/Resource.js.map +1 -1
  20. package/dist/actions/exportFactory.js +1 -1
  21. package/dist/actions/exportFactory.js.map +1 -1
  22. package/dist/columns/SelectColumn.d.ts +32 -5
  23. package/dist/columns/SelectColumn.d.ts.map +1 -1
  24. package/dist/columns/SelectColumn.js +37 -7
  25. package/dist/columns/SelectColumn.js.map +1 -1
  26. package/dist/defaultPages.d.ts.map +1 -1
  27. package/dist/defaultPages.js +3 -0
  28. package/dist/defaultPages.js.map +1 -1
  29. package/dist/elements/Form.d.ts +17 -0
  30. package/dist/elements/Form.d.ts.map +1 -1
  31. package/dist/elements/Form.js +17 -0
  32. package/dist/elements/Form.js.map +1 -1
  33. package/dist/elements/Table.d.ts +26 -0
  34. package/dist/elements/Table.d.ts.map +1 -1
  35. package/dist/elements/Table.js +15 -1
  36. package/dist/elements/Table.js.map +1 -1
  37. package/dist/elements/TableGroup.d.ts +84 -0
  38. package/dist/elements/TableGroup.d.ts.map +1 -1
  39. package/dist/elements/TableGroup.js +103 -0
  40. package/dist/elements/TableGroup.js.map +1 -1
  41. package/dist/elements/dispatchForm.d.ts.map +1 -1
  42. package/dist/elements/dispatchForm.js +36 -6
  43. package/dist/elements/dispatchForm.js.map +1 -1
  44. package/dist/elements/dispatchTable.d.ts +12 -0
  45. package/dist/elements/dispatchTable.d.ts.map +1 -1
  46. package/dist/elements/dispatchTable.js +103 -28
  47. package/dist/elements/dispatchTable.js.map +1 -1
  48. package/dist/fields/Field.d.ts +7 -2
  49. package/dist/fields/Field.d.ts.map +1 -1
  50. package/dist/fields/Field.js +8 -3
  51. package/dist/fields/Field.js.map +1 -1
  52. package/dist/fields/RepeaterField.d.ts +65 -0
  53. package/dist/fields/RepeaterField.d.ts.map +1 -1
  54. package/dist/fields/RepeaterField.js +48 -0
  55. package/dist/fields/RepeaterField.js.map +1 -1
  56. package/dist/orm/modelDefaults.d.ts.map +1 -1
  57. package/dist/orm/modelDefaults.js +19 -0
  58. package/dist/orm/modelDefaults.js.map +1 -1
  59. package/dist/pageData.d.ts +20 -0
  60. package/dist/pageData.d.ts.map +1 -1
  61. package/dist/pageData.js +242 -34
  62. package/dist/pageData.js.map +1 -1
  63. package/dist/react/AppShell.d.ts +17 -1
  64. package/dist/react/AppShell.d.ts.map +1 -1
  65. package/dist/react/AppShell.js +34 -3
  66. package/dist/react/AppShell.js.map +1 -1
  67. package/dist/react/PendingSuggestionApplierRegistry.d.ts +34 -0
  68. package/dist/react/PendingSuggestionApplierRegistry.d.ts.map +1 -0
  69. package/dist/react/PendingSuggestionApplierRegistry.js +51 -0
  70. package/dist/react/PendingSuggestionApplierRegistry.js.map +1 -0
  71. package/dist/react/PendingSuggestionOverlayRegistry.d.ts +46 -0
  72. package/dist/react/PendingSuggestionOverlayRegistry.d.ts.map +1 -0
  73. package/dist/react/PendingSuggestionOverlayRegistry.js +16 -0
  74. package/dist/react/PendingSuggestionOverlayRegistry.js.map +1 -0
  75. package/dist/react/PendingSuggestionsContext.d.ts +153 -0
  76. package/dist/react/PendingSuggestionsContext.d.ts.map +1 -0
  77. package/dist/react/PendingSuggestionsContext.js +46 -0
  78. package/dist/react/PendingSuggestionsContext.js.map +1 -0
  79. package/dist/react/SchemaRenderer.d.ts.map +1 -1
  80. package/dist/react/SchemaRenderer.js +312 -39
  81. package/dist/react/SchemaRenderer.js.map +1 -1
  82. package/dist/react/cells/EditableCell.d.ts +8 -0
  83. package/dist/react/cells/EditableCell.d.ts.map +1 -1
  84. package/dist/react/cells/EditableCell.js +6 -2
  85. package/dist/react/cells/EditableCell.js.map +1 -1
  86. package/dist/react/fields/CheckboxListInput.d.ts.map +1 -1
  87. package/dist/react/fields/CheckboxListInput.js +29 -2
  88. package/dist/react/fields/CheckboxListInput.js.map +1 -1
  89. package/dist/react/fields/ColorInput.d.ts.map +1 -1
  90. package/dist/react/fields/ColorInput.js +28 -2
  91. package/dist/react/fields/ColorInput.js.map +1 -1
  92. package/dist/react/fields/DateTimeInput.d.ts.map +1 -1
  93. package/dist/react/fields/DateTimeInput.js +28 -2
  94. package/dist/react/fields/DateTimeInput.js.map +1 -1
  95. package/dist/react/fields/FieldShell.d.ts.map +1 -1
  96. package/dist/react/fields/FieldShell.js +161 -3
  97. package/dist/react/fields/FieldShell.js.map +1 -1
  98. package/dist/react/fields/FileUploadInput.d.ts.map +1 -1
  99. package/dist/react/fields/FileUploadInput.js +27 -2
  100. package/dist/react/fields/FileUploadInput.js.map +1 -1
  101. package/dist/react/fields/KeyValueInput.d.ts.map +1 -1
  102. package/dist/react/fields/KeyValueInput.js +33 -2
  103. package/dist/react/fields/KeyValueInput.js.map +1 -1
  104. package/dist/react/fields/RadioInput.d.ts.map +1 -1
  105. package/dist/react/fields/RadioInput.js +28 -2
  106. package/dist/react/fields/RadioInput.js.map +1 -1
  107. package/dist/react/fields/SelectFieldInput.d.ts.map +1 -1
  108. package/dist/react/fields/SelectFieldInput.js +31 -2
  109. package/dist/react/fields/SelectFieldInput.js.map +1 -1
  110. package/dist/react/fields/SliderInput.d.ts.map +1 -1
  111. package/dist/react/fields/SliderInput.js +26 -2
  112. package/dist/react/fields/SliderInput.js.map +1 -1
  113. package/dist/react/fields/TagsInput.d.ts.map +1 -1
  114. package/dist/react/fields/TagsInput.js +26 -2
  115. package/dist/react/fields/TagsInput.js.map +1 -1
  116. package/dist/react/fields/ToggleFieldInput.d.ts.map +1 -1
  117. package/dist/react/fields/ToggleFieldInput.js +29 -2
  118. package/dist/react/fields/ToggleFieldInput.js.map +1 -1
  119. package/dist/react/index.d.ts +3 -0
  120. package/dist/react/index.d.ts.map +1 -1
  121. package/dist/react/index.js +3 -0
  122. package/dist/react/index.js.map +1 -1
  123. package/dist/routes.d.ts.map +1 -1
  124. package/dist/routes.js +55 -2
  125. package/dist/routes.js.map +1 -1
  126. package/dist/schema/Section.d.ts +16 -0
  127. package/dist/schema/Section.d.ts.map +1 -1
  128. package/dist/schema/Section.js +16 -0
  129. package/dist/schema/Section.js.map +1 -1
  130. package/dist/schema/Wizard.d.ts +45 -0
  131. package/dist/schema/Wizard.d.ts.map +1 -1
  132. package/dist/schema/Wizard.js +50 -0
  133. package/dist/schema/Wizard.js.map +1 -1
  134. package/dist/schema/resolveSchema.d.ts +8 -0
  135. package/dist/schema/resolveSchema.d.ts.map +1 -1
  136. package/dist/schema/resolveSchema.js +70 -1
  137. package/dist/schema/resolveSchema.js.map +1 -1
  138. package/dist/sessionFilters.d.ts.map +1 -1
  139. package/dist/sessionFilters.js +12 -1
  140. package/dist/sessionFilters.js.map +1 -1
  141. package/dist/styles/file-upload.css +13 -0
  142. package/dist/vite.d.ts.map +1 -1
  143. package/dist/vite.js +19 -12
  144. package/dist/vite.js.map +1 -1
  145. package/package.json +6 -4
  146. package/src/Column.test.ts +36 -0
  147. package/src/Column.ts +54 -0
  148. package/src/Page.ts +13 -4
  149. package/src/Pilotiq.ts +109 -0
  150. package/src/Resource.ts +29 -0
  151. package/src/actions/exportFactory.ts +1 -1
  152. package/src/columns/SelectColumn.ts +46 -8
  153. package/src/columns/editableColumns.test.ts +45 -0
  154. package/src/defaultPages.ts +3 -0
  155. package/src/elements/Form.ts +19 -0
  156. package/src/elements/Table.ts +35 -1
  157. package/src/elements/TableGroup.test.ts +111 -0
  158. package/src/elements/TableGroup.ts +135 -0
  159. package/src/elements/dispatchForm.ts +34 -7
  160. package/src/elements/dispatchTable.test.ts +267 -0
  161. package/src/elements/dispatchTable.ts +111 -32
  162. package/src/fields/Field.test.ts +15 -0
  163. package/src/fields/Field.ts +8 -3
  164. package/src/fields/RepeaterField.ts +104 -0
  165. package/src/fields/RepeaterRelationship.test.ts +173 -0
  166. package/src/nestedRelationManagerData.test.ts +21 -0
  167. package/src/orm/modelDefaults.ts +21 -0
  168. package/src/pageData.ts +267 -47
  169. package/src/react/AppShell.tsx +55 -4
  170. package/src/react/PendingSuggestionApplierRegistry.ts +80 -0
  171. package/src/react/PendingSuggestionOverlayRegistry.ts +54 -0
  172. package/src/react/PendingSuggestionsContext.tsx +172 -0
  173. package/src/react/SchemaRenderer.tsx +504 -95
  174. package/src/react/cells/EditableCell.tsx +11 -2
  175. package/src/react/fields/CheckboxListInput.tsx +23 -2
  176. package/src/react/fields/ColorInput.tsx +22 -2
  177. package/src/react/fields/DateTimeInput.tsx +22 -2
  178. package/src/react/fields/FieldShell.tsx +167 -3
  179. package/src/react/fields/FileUploadInput.tsx +21 -2
  180. package/src/react/fields/KeyValueInput.tsx +32 -2
  181. package/src/react/fields/RadioInput.tsx +23 -2
  182. package/src/react/fields/SelectFieldInput.tsx +25 -2
  183. package/src/react/fields/SliderInput.tsx +20 -2
  184. package/src/react/fields/TagsInput.tsx +20 -2
  185. package/src/react/fields/ToggleFieldInput.tsx +23 -2
  186. package/src/react/index.ts +18 -0
  187. package/src/relationManagerData.test.ts +451 -2
  188. package/src/routes.ts +58 -2
  189. package/src/schema/Section.ts +17 -0
  190. package/src/schema/Wizard.ts +67 -0
  191. package/src/schema/containers.test.ts +90 -0
  192. package/src/schema/resolveSchema.test.ts +50 -0
  193. package/src/schema/resolveSchema.ts +79 -1
  194. package/src/sessionFilters.test.ts +23 -0
  195. package/src/sessionFilters.ts +11 -1
  196. package/src/styles/file-upload.css +13 -0
  197. package/src/vite.ts +19 -12
@@ -1,4 +1,8 @@
1
1
 
2
- > @pilotiq/pilotiq@0.6.2 build /home/runner/work/pilotiq/pilotiq/packages/pilotiq
3
- > tsc -p tsconfig.build.json
2
+ > @pilotiq/pilotiq@0.7.1 build /home/runner/work/pilotiq/pilotiq/packages/pilotiq
3
+ > tsc -p tsconfig.build.json && pnpm run copy-assets
4
+
5
+
6
+ > @pilotiq/pilotiq@0.7.1 copy-assets /home/runner/work/pilotiq/pilotiq/packages/pilotiq
7
+ > mkdir -p dist/styles && cp -R src/styles/*.css dist/styles/
4
8
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,619 @@
1
1
  # @pilotiq/pilotiq
2
2
 
3
+ ## 0.7.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 229f290: Emit `parts[0]!` for the `basePath` field in every auto-generated route stub. Under consumers' `noUncheckedIndexedAccess` tsconfig, the previous `basePath: parts[0]` typed as `string | undefined`, which Vike's `RouteSync.routeParams: Record<string, string>` rejects. The non-null assertion is safe because each route guards on `parts.length` before reaching the return.
8
+
9
+ ## 0.7.0
10
+
11
+ ### Minor Changes
12
+
13
+ - b6dffde: feat(columns): Column.toggleable() user-visibility chrome
14
+
15
+ `Column.toggleable()` lets users show / hide individual columns from a
16
+ new toolbar **Columns** dropdown. Preference persists per-table to
17
+ `localStorage` (key `pilotiq.table.<currentPath>.columns.<col>`), so the
18
+ choice sticks across reloads + SPA navigations. Pass `{ initiallyHidden:
19
+ true }` to start the column off-screen — useful for technical / debug
20
+ columns that the typical viewer doesn't need.
21
+
22
+ ```ts
23
+ Resource.table = (t) =>
24
+ t.columns([
25
+ TextColumn.make("name"),
26
+ TextColumn.make("email").toggleable(),
27
+ TextColumn.make("internalId").toggleable({ initiallyHidden: true }),
28
+ ]);
29
+ ```
30
+
31
+ The dropdown trigger renders next to the existing Filters / Sort
32
+ controls; non-toggleable columns always render and never appear in the
33
+ dropdown. Hidden state is purely presentational — the column's data
34
+ still loads from the server so sorts / filters that reference a hidden
35
+ column keep working, and a re-toggle paints fresh values without a
36
+ roundtrip. Toggling multiple columns in one open: the dropdown stays
37
+ open between clicks (`closeOnClick={false}`).
38
+
39
+ `visibleColumns = columns.filter(c => !hidden.has(c.name))` flows
40
+ through the TableHead loop, body cells loop, per-group + footer summary
41
+ rows, and the empty-state colSpan.
42
+
43
+ The `toggleable` key is sparse on the wire — only set when a column
44
+ opts in.
45
+
46
+ - 8845b90: feat(core): `@pilotiq/pilotiq/styles/file-upload.css` subpath
47
+
48
+ `FileUploadField`'s image-cropping UI ships its own stylesheet via the
49
+ `react-image-crop` package — a declared dep of `@pilotiq/pilotiq`.
50
+ Consumers no longer need to declare `react-image-crop` themselves;
51
+ import the new subpath from your app's Tailwind / global stylesheet:
52
+
53
+ ```css
54
+ @import "@pilotiq/pilotiq/styles/file-upload.css";
55
+ ```
56
+
57
+ The CSS file re-imports `react-image-crop/dist/ReactCrop.css`; the
58
+ @import resolves through pilotiq's own `node_modules`, so the consumer
59
+ side doesn't need a direct dep declaration. Mirrors the same pattern
60
+ as other UI peer deps that pilotiq ships through subpaths.
61
+
62
+ **Build side:** `pnpm build` now copies `src/styles/*.css` to
63
+ `dist/styles/` via a new `copy-assets` script. Watch-mode (`pnpm dev`)
64
+ runs the copy once at startup; per-CSS-edit re-copies aren't wired
65
+ (unusual in dev — the CSS file is essentially static).
66
+
67
+ - 2c441b7: feat(core): `Form.inlineLabel()` / `Section.inlineLabel()` cascade
68
+
69
+ Set `inlineLabel` once at the top of a form (or any section) and every
70
+ descendant `Field` inherits it instead of repeating `.inlineLabel()`
71
+ on each one. Per-field calls still win.
72
+
73
+ ```ts
74
+ Form.make()
75
+ .inlineLabel()
76
+ .schema([
77
+ TextField.make("name"), // → inlineLabel: true
78
+ TextField.make("email"), // → inlineLabel: true
79
+ TextField.make("bio").inlineLabel(false), // explicit → label-above
80
+ Section.make("Address")
81
+ .inlineLabel(false)
82
+ .schema([
83
+ TextField.make("street"), // subtree resets → label-above
84
+ TextField.make("city"), // → label-above
85
+ ]),
86
+ ]);
87
+ ```
88
+
89
+ **Resolution chain (most-specific wins):**
90
+
91
+ 1. Field-level `Field.inlineLabel(true|false)` — explicit setting on the
92
+ field itself.
93
+ 2. Nearest ancestor `Section` with `.inlineLabel(true|false)` — overrides
94
+ any outer container for its subtree.
95
+ 3. Outer `Form.inlineLabel(true|false)` — applies to the whole form.
96
+ 4. Default — label-above.
97
+
98
+ **Implementation:**
99
+
100
+ - `RenderContext.inlineLabelDefault?: boolean` — pushed by
101
+ `resolveSchema.deriveChildContext` when a `Form` or `Section` calls
102
+ `.inlineLabel(...)`. Children inherit until another container resets
103
+ the flag.
104
+ - `Field._inlineLabel` widened from `boolean` (default `false`) to
105
+ `boolean | undefined`. `Field.buildMeta(ctx)` reads
106
+ `this._inlineLabel ?? ctx.inlineLabelDefault` to decide whether to
107
+ emit the meta key. No public-API change — the setter is unchanged
108
+ (`inlineLabel(v = true)`).
109
+ - New `Form.inlineLabel(v = true)` + `Form.getInlineLabel()` and the
110
+ parallel `Section.inlineLabel(v = true)` + `Section.getInlineLabel()`.
111
+
112
+ **No wire-shape change.** The on-the-wire `FieldMeta.inlineLabel` is
113
+ still emitted with `true` only — the cascade is server-side.
114
+
115
+ Closes the "Schema-wide `inlineLabel()` cascading default on
116
+ Form/Section. Easy but no consumer ask." item from the field
117
+ micro-additions audit (`docs/plans/admin-gap-audit.md`).
118
+
119
+ - ae1450e: feat(core): `Pilotiq.layoutProvider(C)` — plugin-mounted layout-root providers
120
+
121
+ Adds an open-core registry where plugins can register React provider
122
+ components that wrap the panel's `<AppShell>` children at the layout
123
+ root. Removes the per-app requirement that consumers manually wrap
124
+ their `pages/+Layout.tsx` to make plugin contexts available outside
125
+ specific component slots.
126
+
127
+ ```ts
128
+ // In a plugin's register(panel) step:
129
+ panel.layoutProvider(({ children, basePath }) => (
130
+ <AiUiProvider panelPath={basePath}>{children}</AiUiProvider>
131
+ ));
132
+
133
+ // or bulk:
134
+ panel.layoutProviders([Provider1, Provider2]);
135
+ ```
136
+
137
+ Provider components receive `{ children, basePath? }` props.
138
+ Registration order is preserved — the first-registered provider sits
139
+ OUTERMOST (closest to the layout root); the last sits INNERMOST
140
+ (closest to the page tree). Use this when one provider depends on
141
+ another being in scope: register the producer first.
142
+
143
+ **Mirrors the `panel.rightPanel(...)` pattern** — Vite plugin
144
+ harvests the live component refs into `_components.ts` (alongside
145
+ `componentRegistry` + `rightPanelRegistry`) as `layoutProviderRegistry`,
146
+ the auto-gen `+Layout.tsx` template threads it as
147
+ `<AppShell layoutProviderRegistry={...}>`, and `AppShell` folds the
148
+ registry around its rendered tree from last to first so the first
149
+ provider ends up outermost. Empty / unset → no wrapping happens.
150
+
151
+ The first consumer is `@pilotiq-pro/ai` (≥ next minor), which uses
152
+ this to auto-mount `<AiUiProvider>` so the cross-package
153
+ `PendingSuggestionsContext` queue and `<AiClientToolBindings>`
154
+ handlers reach the form tree without a per-app `+Layout.tsx` edit.
155
+ Apps on this version of pilotiq core can drop the manual `<AiUiProvider>`
156
+ wrap they were carrying as a load-bearing requirement.
157
+
158
+ - e1a79f6: feat(core+tiptap): cross-tree applier registry — Approve from anywhere
159
+
160
+ Phase 8.5 of the AI UX polish plan. Adds an open-core registry that
161
+ lets aggregate consumers — chat-sidebar pending-pills, bulk-action
162
+ menus, future "AI inbox" surfaces — apply a `PendingSuggestion` to its
163
+ target field without sharing the form's React tree.
164
+
165
+ ```ts
166
+ import { registerPendingSuggestionApplier } from "@pilotiq/pilotiq/react";
167
+
168
+ // Renderer-side (auto-wired by FieldShell + Tiptap bridge):
169
+ useEffect(
170
+ () =>
171
+ registerPendingSuggestionApplier(formId, fieldName, (suggestion) => {
172
+ /* apply to this field's underlying input or editor */
173
+ }),
174
+ [formId, fieldName]
175
+ );
176
+ ```
177
+
178
+ **Core (`@pilotiq/pilotiq`)**:
179
+
180
+ - New module `react/PendingSuggestionApplierRegistry.ts` — module-level
181
+ Map keyed by `(formId, fieldName)` (`formId` defaults to `'*'` for
182
+ global form scope; form-scoped registrations always win over the
183
+ wildcard for the same field). Exposes `registerPendingSuggestionApplier`
184
+ (returns unregister fn for `useEffect` cleanup) and
185
+ `getPendingSuggestionApplier`.
186
+ - `PendingSuggestionsApi` extended with `approve(id)` and
187
+ `approveAll(filter?)` — resolves the suggestion's `(formId,
188
+ fieldName)` against the registry, runs the applier, then dismisses.
189
+ Falls through to plain `dismiss` when no applier is registered or
190
+ the applier throws (so a busted applier doesn't strand entries).
191
+ Default no-op context implements both as plain dismiss.
192
+ - `<FieldShell>` auto-registers a generic applier on mount for every
193
+ non-richtext, non-dotted-path field. Applier uses
194
+ `useFieldState.setValue` for controlled (live) forms and a DOM
195
+ fallback (React's internal value setter via
196
+ `Object.getOwnPropertyDescriptor(proto, 'value').set`) for
197
+ uncontrolled forms. Cleanup on unmount.
198
+
199
+ **Tiptap (`@pilotiq/tiptap`)**:
200
+
201
+ - `useAiSuggestionBridge` registers a richtext-aware applier that
202
+ calls `editor.chain().focus().approveAiSuggestion(id).run()` —
203
+ same path the inline chip click takes. The transaction listener
204
+ already mirrors the editor-side dismissal back to context, so a
205
+ pill-driven Approve flows: pill → applier → editor command →
206
+ editor `onTransaction` → context `dismiss`.
207
+
208
+ The registry is generic — not AI-specific. Future field-mutation
209
+ extensions (form-recovery, undo stacks, bulk imports) can register
210
+ through the same seam.
211
+
212
+ Default no-op context still ships, so trees without a real provider
213
+ mounted (e.g. headless tests, marketing-site previews) see no behavior
214
+ change.
215
+
216
+ - df85886: feat(core): `PendingSuggestion.origin` for cross-surface filtering
217
+
218
+ Widen the `PendingSuggestion` type with an optional `origin` block so
219
+ aggregate UIs (pending-pills, overlays, etc.) can filter the shared
220
+ panel-wide queue down to the surface that produced each entry. Backward
221
+ compatible — existing producers that don't stamp `origin` keep working;
222
+ consumers that don't read it see the same flat queue they always did.
223
+
224
+ ```ts
225
+ export interface PendingSuggestionOrigin {
226
+ surface: "sidebar" | "popover" | "field-action";
227
+ runId?: string;
228
+ agentSlug?: string;
229
+ }
230
+
231
+ export interface PendingSuggestion {
232
+ // …existing fields…
233
+ origin?: PendingSuggestionOrigin;
234
+ }
235
+ ```
236
+
237
+ Plugin packages (`@pilotiq-pro/ai`) stamp `origin` when they push from a
238
+ known surface — the popover-chat scopes its `<PendingSuggestionsPill>`
239
+ filter to `o => o?.runId === currentRunId` so it only surfaces its own
240
+ session's output, even when sidebar-originated suggestions are still
241
+ visible in the same panel-wide queue.
242
+
243
+ No wire-shape break, no consumer code required.
244
+
245
+ - 56a6f62: feat(core+tiptap): PendingSuggestionsContext seam + RichTextField AI bridge
246
+
247
+ Adds a cross-package, plugin-fillable queue of suggested field-value
248
+ changes that any field renderer can subscribe to. Open-core seam — core
249
+ defines the shape + provider, plugins like `@pilotiq-pro/ai` ship the
250
+ real implementation.
251
+
252
+ ```ts
253
+ import { usePendingSuggestionsForField } from "@pilotiq/pilotiq/react";
254
+
255
+ const { list, dismiss } = usePendingSuggestionsForField("body");
256
+ // ↑ filtered to suggestions targeting this field+formId
257
+ ```
258
+
259
+ **`@pilotiq/pilotiq` exports** (`@pilotiq/pilotiq/react`):
260
+
261
+ - `PendingSuggestion` — `{ id, fieldName, formId?, currentValue,
262
+ suggestedValue, source?, createdAt, meta? }`. The `meta` bag carries
263
+ field-type-specific extras (e.g. `editorRange: { from, to }` for
264
+ `richtext`).
265
+ - `PendingSuggestionsApi` — `{ list, push, dismiss, dismissAll }`. Core
266
+ ships a no-op default context so trees without a real provider never
267
+ throw.
268
+ - `PendingSuggestionsContext`, `usePendingSuggestions()`,
269
+ `usePendingSuggestionsForField(name, formId?)` — the subscription
270
+ surface.
271
+ - `registerPendingSuggestionOverlay(C)` — mirrors
272
+ `registerFieldLabelSlot()`. A plugin registers a single component
273
+ (`{ suggestion, onApprove, onReject }` props) that `<FieldShell>`
274
+ mounts below the input whenever a matching pending suggestion exists.
275
+ Skipped on `richtext` fields (those render the diff inline via the
276
+ Tiptap extension).
277
+
278
+ **`@pilotiq/tiptap` `RichTextField` bridge**:
279
+
280
+ The Tiptap renderer now subscribes to the queue and mirrors entries
281
+ into its `AiSuggestionExtension`. Producers push a `PendingSuggestion`
282
+ with `meta.editorRange = { from, to }` and a string `suggestedValue`;
283
+ the bridge calls `editor.commands.addAiSuggestion(...)` so the inline
284
+ diff + Approve / Reject chips appear. When the user clicks a chip,
285
+ the editor command runs (mutating the doc on Approve, leaving it on
286
+ Reject) and the bridge mirrors the removal back to the queue via
287
+ `dismiss(id)` so other surfaces (chat-sidebar pill, FieldShell
288
+ overlay registered by another plugin) clear in lock-step.
289
+
290
+ The bridge is no-op when no provider is mounted — pilotiq core ships
291
+ the default no-op context, so consumers without `@pilotiq-pro/ai` see
292
+ no behavior change.
293
+
294
+ Pure helpers + types are public; the bridge hook
295
+ `useAiSuggestionBridge` is exported from `@pilotiq/tiptap` for advanced
296
+ producers that want to drive their own editor instances.
297
+
298
+ - e791f65: feat(core): per-tab `canX` gating on `RelationTabs`
299
+
300
+ The record sub-navigation strip (`[View, Edit, …managers]`) now runs the
301
+ matching authorization predicate for each tab and drops entries the
302
+ user can't reach. The routes always enforced — this is presentation
303
+ polish so the chrome doesn't promise a link that 403s on click.
304
+
305
+ **Gates evaluated per tab:**
306
+
307
+ - `__view` → `R.canView(user, parentRecord)`
308
+ - `__edit` → `R.canEdit(user, parentRecord)`
309
+ - manager → `safeManagerPolicy(M, 'canViewAny', Related, user,
310
+ parentRecord)` (falls through to the related Resource's
311
+ `canViewAny` when the manager hasn't overridden — same shape as
312
+ everywhere else)
313
+
314
+ Throwing predicate fails closed (tab hidden). Record-aware predicates
315
+ short-circuit to "visible" when the record-load failed (so the route's
316
+ own gate surfaces the 404/403, not a silent hide).
317
+
318
+ **Empty-strip collapse:** if every gated tab drops, `buildRelationTabs`
319
+ returns `undefined` and the strip is omitted entirely (consistent with
320
+ the existing "no managers registered" branch). The depth-2
321
+ `buildNestedRelationTabs` mirrors the shape — sibling nested manager
322
+ tabs gate on `safeManagerPolicy(N, 'canViewAny', Related, user,
323
+ child1Record)`; the back-link `__view` stays unconditional since the
324
+ user already passed `M.canViewAny` to reach that page; if all sibling
325
+ tabs drop the depth-2 strip is omitted (back-link alone isn't useful
326
+ sub-nav).
327
+
328
+ **No public API change.** Tab gating runs inside the existing
329
+ `buildRelationTabs` / `buildNestedRelationTabs` helpers — both private
330
+ to `pageData.ts`. Their callers (`resourceEditData` / `resourceViewData`
331
+ / relation data builders / nested relation data builders) already had
332
+ `user` and `parentRecord` (or `child1`) in scope so threading is a
333
+ one-line change at each site.
334
+
335
+ 7 tests added (6 depth-1 + 1 depth-2).
336
+
337
+ - cce4f52: feat(repeater): afterCreate / afterUpdate / afterDelete hooks for relationship-mode
338
+
339
+ `Repeater.relationship(...)` gains three per-row lifecycle hooks that
340
+ fire from `persistRelationshipRows` after each child operation:
341
+
342
+ ```ts
343
+ RepeaterField.make("attachments")
344
+ .relationship("attachments")
345
+ .schema([TextField.make("filename")])
346
+ .afterCreate(async (record, ctx) => {
347
+ /* ... */
348
+ })
349
+ .afterUpdate(async (record, ctx) => {
350
+ /* ... */
351
+ })
352
+ .afterDelete(async (removed, ctx) => {
353
+ if (ctx.mode === "hasMany" || ctx.mode === "morphMany") {
354
+ // child record was physically deleted
355
+ }
356
+ // For M2M only the pivot row was detached; the child may still exist.
357
+ });
358
+ ```
359
+
360
+ The handler receives the persisted child record and a `RepeaterRowContext`
361
+ carrying:
362
+
363
+ - `parent` — post-save parent record.
364
+ - `parentId` — `parent[primaryKey]`.
365
+ - `field` — the Repeater field's `name`.
366
+ - `index` — 0-based row index in the submitted set; `-1` for `afterDelete`.
367
+ - `mode` — the resolved `RepeaterRelationMode` (`'hasMany' | 'morphMany'
368
+ | 'belongsToMany' | 'morphToMany' | 'morphedByMany'`).
369
+
370
+ Each setter is config-time guarded: calling on a Repeater that hasn't
371
+ declared `relationship(...)` throws with a clear message (mirrors the
372
+ existing `orderColumn() / pivotColumns()` guards). Throwing handlers
373
+ propagate and stop the rest of the persist diff — earlier rows have
374
+ already saved (v1 isn't transactional).
375
+
376
+ - bd8229e: feat(core): `Resource.pages().record` — custom record sub-pages auto-mounted on the sub-nav strip
377
+
378
+ Declare custom pages that live under a single record. Each sub-page
379
+ gets its own URL (`${resourceBase}/:id/${subPageSlug}`), its own tab in
380
+ the record `RelationTabs` strip, receives the loaded record on
381
+ `ctx.record`, and runs its own `canAccess(user, record)` gate.
382
+
383
+ ```ts
384
+ class ActivityPage extends Page {
385
+ static override slug = "activity";
386
+ static override label = "Activity";
387
+ static override schema(ctx) {
388
+ return [
389
+ Heading.make(`Activity for ${(ctx.record as { name?: string })?.name}`),
390
+ ];
391
+ }
392
+ // Optional record-aware gate.
393
+ static override async canAccess(user, record) {
394
+ return (
395
+ (record as { ownerId: string })?.ownerId ===
396
+ (user as { id: string })?.id
397
+ );
398
+ }
399
+ }
400
+
401
+ class UserResource extends Resource {
402
+ static override slug = "users";
403
+ static override pages() {
404
+ return {
405
+ record: {
406
+ activity: ActivityPage,
407
+ },
408
+ };
409
+ }
410
+ }
411
+ ```
412
+
413
+ **Wiring:**
414
+
415
+ - `ResourcePages.record?: Record<string, typeof Page>` widening — keeps
416
+ the four standard roles (`index / create / edit / view`) cleanly
417
+ typed; the `record` slot signals "these are per-record sub-pages."
418
+ - `Resource.getRecordPages()` accessor (sugar over
419
+ `resolvePages().record ?? {}`).
420
+ - `PageMode` widened with `'record'`.
421
+ - `Page.canAccess(user, record?)` signature widened — second optional
422
+ arg, back-compat with existing custom-page subclasses that wrote
423
+ `canAccess(user)`.
424
+ - Routes: `GET ${resourceBase}/:id/${subPageSlug}` per registered
425
+ sub-page. The Vike `relation-list` route + `dispatchPageData` share
426
+ the URL slot — relation managers tried first, record sub-pages
427
+ second. Boot validation prevents slug collisions.
428
+ - New `resourceRecordPageData(pilotiq, slug, recordId, subPageSlug,
429
+ req)` builder mirrors `resourceViewData`'s shape.
430
+ - `RelationTabs` strip inserts a tab per sub-page between `__edit` and
431
+ the managers, gated on `SubPage.canAccess(user, record)`. Strip now
432
+ also mounts when ONLY sub-pages exist (no relation managers needed).
433
+
434
+ **Boot validation:**
435
+
436
+ Sub-page slugs must match `[A-Za-z0-9_-]+` and must not collide with:
437
+
438
+ - Reserved relation-manager tokens (`edit`, `delete`, `restore`,
439
+ `force-delete`, `_form`, `_action`, `_search`, `_uploads`,
440
+ `_attach`, `_detach`, `_bulk-detach`).
441
+ - Any of the resource's relation-manager `relationship` slugs.
442
+
443
+ Boot fails with a clear error message — silent 404 at request time is
444
+ much harder to debug than a config-time throw.
445
+
446
+ **v1 limits:** depth-1 only (sub-pages live under `Resource`, not
447
+ under `RelationManager`); no automatic sidebar surface (sub-pages are
448
+ per-record); no tab badges on record sub-pages.
449
+
450
+ Plan + guide: `docs/plans/resource-record-sub-pages.md`,
451
+ `docs/guide/record-sub-pages.md`.
452
+
453
+ - 2f42dcd: feat(columns): SelectColumn.options(record => …) per-row resolver
454
+
455
+ `SelectColumn.options()` now accepts a function form alongside the
456
+ existing static `{ key: label }` / `[{ value, label }]` shapes. The
457
+ resolver receives the raw record and may return a Promise; runs once
458
+ per visible row in `loadTableRecords` (gated behind the existing
459
+ `canEdit` hook so hidden cells skip the resolver cost).
460
+
461
+ ```ts
462
+ SelectColumn.make("assigneeId").options(async (row) => {
463
+ const team = await Team.find(row.teamId);
464
+ return team.members.map((m) => ({ value: String(m.id), label: m.name }));
465
+ });
466
+ ```
467
+
468
+ The resolved per-row option list is stamped on `row._cellSelectOptions[col.name]`;
469
+ the renderer's `<CellSelect>` reads it as `props.rowOptions` and falls
470
+ back to the column's static `selectOptions` when unset. Resolvers run
471
+ in parallel across columns within a row. A throwing resolver leaves
472
+ the slot unset on that row only — others still stamp, and the cell
473
+ falls back to the static fallback list so one bad row doesn't break
474
+ the whole table.
475
+
476
+ - d7dbc80: feat(core): `TableGroup.scopeQueryByKey()` — click-a-group-heading-to-drill-in
477
+
478
+ Click a banded group's heading to drill the table into just that group's
479
+ rows. The banded layout disappears for that render, a "Drilled into
480
+ <Label>: <Value>" chip mounts above the table with an × to clear, and
481
+ the query has already been narrowed server-side via the registered scoper.
482
+
483
+ ```ts
484
+ Table.make()
485
+ .groups([
486
+ TableGroup.make("status")
487
+ .label("Status")
488
+ .scopeQueryByKey((q, key) => q.where("status", "=", key)),
489
+ ])
490
+ .defaultGroup("status");
491
+ ```
492
+
493
+ **Three new methods on `TableGroup`:**
494
+
495
+ - `scopeQueryByKey(fn)` — query scoper applied when the user clicks a
496
+ heading. Receives `(q, key)` and returns the narrowed query. **Default
497
+ (no override):** exact-match `(q, key) => q.where(column, '=', key)`.
498
+ Date groups (`.date()`) install a whole-day range default instead —
499
+ `(q, key) => q.where(col, '>=', '${key} 00:00:00').where(col, '<=', '${key} 23:59:59')`.
500
+ Auto-arms `.scopable(true)`.
501
+ - `getKeyFromRecordUsing(fn)` — override the per-record bucket key
502
+ resolver. Returned string round-trips through `?<prefix>groupKey=` and
503
+ lands as the second arg of `scopeQueryByKey`. Default = raw column
504
+ value cast to string (or the `YYYY-MM-DD` bucket when `.date()` is on).
505
+ Auto-arms `.scopable(true)`.
506
+ - `scopable(v = true)` — explicit opt-in toggle for the clickable
507
+ heading affordance. Use `.scopable(false)` to opt back out after a
508
+ setter has auto-armed it.
509
+
510
+ **URL state:** dedicated `?groupKey=<value>` key, prefix-aware via
511
+ `Table.queryStringIdentifier`. Pairs with `?group=<col>`. Clicking a
512
+ heading resets `?page` to 1 server-side so drill-in always lands on the
513
+ first page of the bucket. The × chip clears `?groupKey=` and restores
514
+ the banded view.
515
+
516
+ **Renderer:** group heading text wraps in a real `<a href>` when
517
+ `scopable` is true (cmd-click / right-click "open in new tab" works);
518
+ plain left-click SPA-navs via `useNavigate()`. The collapsible chevron
519
+ (when `.collapsible()` is also set) stays separate so users can fold
520
+ the group without drilling in.
521
+
522
+ **Persistence:** `<prefix>groupKey` is excluded from
523
+ `persistFiltersInSession`'s persisted slice (parallel to `<prefix>page`)
524
+ — drill-in is page-state, not filter-state. Bare-URL visits return to
525
+ the banded view; the user's last drill-in URL is shareable but not
526
+ auto-restored on revisit.
527
+
528
+ **Composition:**
529
+
530
+ - Chains on top of filters / `TrashedFilter` / active tab query — runs
531
+ after all of them via `ctx.groupScope` in the model adapter.
532
+ - Suppresses per-group summaries (`groupSummaries`) for the drilled-in
533
+ render; the global `tfoot` summary still computes over the visible
534
+ bucket.
535
+ - Composes with `queryStringIdentifier` — keys parse as
536
+ `<id>_groupKey` alongside `<id>_group`.
537
+ - Works on `RelationManager` tables — `modelRelationTableRecords`
538
+ reads the same `ctx.groupScope`.
539
+
540
+ **v1 limits:** one key at a time (multi-select drill-in deferred);
541
+ drill-in URLs survive bookmarking but not session-persistence; date
542
+ range default is whole-day (sub-day buckets need a custom scoper).
543
+
544
+ Plan: `docs/plans/table-group-scope-query-by-key.md`.
545
+
546
+ - 8d92594: feat(wizard): nav-button customizers + URL-state persistence
547
+
548
+ `Wizard.submitAction(a => …) / .nextAction(...) / .previousAction(...)`
549
+ let consumers customize the chrome of the built-in nav buttons. The
550
+ customizer receives a framework-built default `Action` (Submit / Next /
551
+ Back) and returns a customized clone (or a fresh `Action` outright);
552
+ chrome (label / icon / color / size / outlined / iconOnly / tooltip /
553
+ disabled rules) carries through to the rendered button while click
554
+ behavior stays hardwired to advance / recede / submit-form.
555
+
556
+ `submitAction` is the opt-in case: by default the wizard renders a hint
557
+ pointing at the surrounding form's Save button. Setting `submitAction`
558
+ mounts a real `<button type="submit">` inside the wizard chrome on the
559
+ final step, making the wizard self-contained — pair with
560
+ `CreatePage.getFormActions(R) → []` to suppress the page-level Save when
561
+ you don't want two submits on the same page.
562
+
563
+ `Wizard.persistStepInQueryString(key='step' | true | false)` mirrors the
564
+ active step to the URL as `?<key>=N` (1-based for human-friendly URLs)
565
+ via `history.replaceState` — purely client-side state sync with no SSR
566
+ re-fetch. URL wins over localStorage on initial mount so deep-linking
567
+ to a specific step works. Multi-wizard pages should use distinct keys
568
+ to avoid collisions on the same query string.
569
+
570
+ ### Patch Changes
571
+
572
+ - 425cf50: fix(core): register field-owned AI appliers on every React-driven input
573
+
574
+ Same hidden-input bug as `SelectField`, swept across nine more field
575
+ types. Each of these renders a `<input type="hidden" name={name}>`
576
+ mirror for native form submit but drives the visible widget from React
577
+ state — `FieldShell`'s generic applier writes to the hidden input and
578
+ dispatches `change`, but the widget has no listener wired to it, so AI
579
+ Review-mode Approve (and any other `PendingSuggestionApplierRegistry`
580
+ caller) silently no-ops.
581
+
582
+ Fixed by registering a field-owned applier inside each component and
583
+ adding the field's `fieldType` to the central
584
+ `SELF_APPLIER_FIELD_TYPES` set in `FieldShell.tsx` (single source of
585
+ truth — `FieldShell` skips its generic registration so the field's
586
+ applier stays last-write-wins):
587
+
588
+ - `ToggleFieldInput` — `'toggle'`; coerces to boolean
589
+ - `SliderInput` — `'slider'`; coerces to number (clamps to `min` on NaN)
590
+ - `ColorInput` — `'color'`; falls back to `#000000` for null/empty
591
+ - `KeyValueInput` — `'keyValue'`; rebuilds rows from the suggestion
592
+ object (preserves existing row IDs by index for input-focus stability)
593
+ - `FileUploadInput` — `'fileUpload'`; routes through `toUrls()`;
594
+ honors `multiple` (single-file persists `urls[0] ?? null`)
595
+ - `TagsInput` — `'tagsInput'`; routes through the existing `toArray()`
596
+ parser (tolerates `string[]`, JSON-encoded, single string)
597
+ - `DateTimeInput` — `'dateTime'`; coerces null/empty to `''`
598
+ - `RadioInput` — `'radio'`; coerces null to `''`
599
+ - `CheckboxListInput` — `'checkboxList'`; routes through the local
600
+ `toArray()` (also fixes a pre-existing latent corruption: per-option
601
+ hidden mirrors share the `[name]` attribute, so the generic applier
602
+ would have stamped every one with the same stringified value
603
+ instead of replacing the array)
604
+
605
+ All appliers follow the canonical `SelectFieldInput` shape:
606
+ `useRef(fs)` to hold latest field-state across re-registrations,
607
+ dotted-path skip (Repeater rows are inaccessible from outside the
608
+ form's React tree), and a controlled/uncontrolled split that mirrors
609
+ each component's existing `setValue` path.
610
+
611
+ After this sweep, AI Review-mode Approve correctly updates the visible
612
+ widget on every Filament-parity field type. Custom field renderers
613
+ that drive their state from React still need to follow the same
614
+ pattern — register inside the component, add `fieldType` to the
615
+ shared set.
616
+
3
617
  ## 0.6.2
4
618
 
5
619
  ### Patch Changes