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