@object-ui/react 17.3.0 → 17.6.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 (62) hide show
  1. package/CHANGELOG.md +1242 -0
  2. package/README.md +48 -21
  3. package/dist/LazyPluginLoader.d.ts +14 -7
  4. package/dist/LazyPluginLoader.js +14 -7
  5. package/dist/SchemaRenderer.d.ts +78 -5
  6. package/dist/SchemaRenderer.js +572 -34
  7. package/dist/context/AppShellContext.d.ts +21 -2
  8. package/dist/context/HostNavigationContext.d.ts +82 -0
  9. package/dist/context/HostNavigationContext.js +65 -0
  10. package/dist/context/RecordContext.d.ts +0 -4
  11. package/dist/context/RecordContext.js +0 -2
  12. package/dist/context/RelatedRecordActionsContext.d.ts +44 -0
  13. package/dist/context/RelatedRecordActionsContext.js +6 -0
  14. package/dist/context/index.d.ts +13 -12
  15. package/dist/context/index.js +13 -12
  16. package/dist/element-data-source/ElementDataSourceGate.d.ts +272 -0
  17. package/dist/element-data-source/ElementDataSourceGate.js +274 -0
  18. package/dist/hooks/index.d.ts +35 -32
  19. package/dist/hooks/index.js +42 -32
  20. package/dist/hooks/useActionEngine.js +1 -1
  21. package/dist/hooks/useActionTextLocalizer.d.ts +69 -0
  22. package/dist/hooks/useActionTextLocalizer.js +91 -0
  23. package/dist/hooks/useCapabilityGate.js +6 -5
  24. package/dist/hooks/useClientNotifications.js +2 -2
  25. package/dist/hooks/useCrudShortcuts.js +1 -1
  26. package/dist/hooks/useDatasetDimensionLabels.d.ts +104 -0
  27. package/dist/hooks/useDatasetDimensionLabels.js +154 -0
  28. package/dist/hooks/useDiscovery.js +1 -1
  29. package/dist/hooks/useElementDataSource.d.ts +68 -0
  30. package/dist/hooks/useElementDataSource.js +141 -0
  31. package/dist/hooks/useExpression.d.ts +82 -3
  32. package/dist/hooks/useExpression.js +121 -4
  33. package/dist/hooks/useNavigationOverlay.d.ts +59 -11
  34. package/dist/hooks/useOffline.d.ts +62 -36
  35. package/dist/hooks/useOffline.js +2 -2
  36. package/dist/hooks/usePageAssignment.js +5 -5
  37. package/dist/hooks/usePageTransition.js +1 -1
  38. package/dist/hooks/usePageVariableActionBridge.js +2 -2
  39. package/dist/hooks/useViewData.js +1 -1
  40. package/dist/hooks/useViewTransition.js +1 -1
  41. package/dist/index.d.ts +11 -9
  42. package/dist/index.js +12 -8
  43. package/dist/schema-input.d.ts +42 -0
  44. package/dist/schema-input.js +42 -0
  45. package/dist/spec-bridge/SpecBridge.d.ts +12 -6
  46. package/dist/spec-bridge/SpecBridge.js +9 -3
  47. package/dist/spec-bridge/bridges/form-view.d.ts +99 -23
  48. package/dist/spec-bridge/bridges/form-view.js +51 -7
  49. package/dist/spec-bridge/bridges/list-view.d.ts +1 -1
  50. package/dist/spec-bridge/bridges/list-view.js +143 -24
  51. package/dist/spec-bridge/index.d.ts +4 -4
  52. package/dist/spec-bridge/index.js +3 -3
  53. package/dist/spec-bridge/types.d.ts +24 -3
  54. package/dist/utils/error-message.d.ts +59 -0
  55. package/dist/utils/error-message.js +116 -1
  56. package/dist/utils/i18n.d.ts +43 -8
  57. package/dist/utils/i18n.js +42 -4
  58. package/dist/utils/unevaluatedExpression.d.ts +69 -0
  59. package/dist/utils/unevaluatedExpression.js +194 -0
  60. package/dist/utils/visibilityDiagnostic.d.ts +119 -0
  61. package/dist/utils/visibilityDiagnostic.js +284 -0
  62. package/package.json +8 -8
package/CHANGELOG.md CHANGED
@@ -1,5 +1,1232 @@
1
1
  # @object-ui/react
2
2
 
3
+ ## 17.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f1d4748: Remove the retired `striped` / `bordered` / `virtualScroll` list-view surface
8
+
9
+ objectstack#7176 retired `list.striped`, `list.bordered` and `list.virtualScroll`
10
+ from the spec after measuring every objectui reader as pass-through: each one
11
+ copied the key onward and no renderer ever applied it. objectui stops declaring,
12
+ typing and forwarding them.
13
+
14
+ Off the chain: the `@objectstack/spec` list-view bridge in `@object-ui/react`,
15
+ `ListView`'s child-view props in `@object-ui/plugin-list`, both `ObjectView`
16
+ relays (`@object-ui/plugin-view` and `@object-ui/app-shell`), the `ObjectGridSchema`
17
+ and `NamedListView` declarations in `@object-ui/types` (interface and zod),
18
+ `ObjectGrid.component.yml` in `@object-ui/components`, and the page-block
19
+ inspector's `striped` / `bordered` toggles in the metadata-admin designer.
20
+
21
+ Behaviour is unchanged: nothing read these keys, so nothing rendered differently
22
+ for them. Stored view metadata that still carries one keeps validating — the keys
23
+ are simply no longer relayed. `ListViewSchema` continues to take the spec's
24
+ list-view fields by reference, so the protocol's own retirement tombstones
25
+ arrive with the next `@objectstack/spec` bump and reject the keys at the
26
+ authoring boundary. Restoring any of the three as live surface requires an
27
+ implementation card filed first, per the ruling.
28
+ - 8c0d52e: A form's ruled `submitBehavior.url` redirect can now be performed by the HOST, so a destination stays inside a console mounted at a sub-path (objectui#4989 defect 4).
29
+
30
+ `ObjectForm` and `WizardForm` accept a relative in-app path and used to travel to it with `window.location.assign`. A rooted path resolves against the ORIGIN there, so under a mounted host — `<BrowserRouter basename="/_console">`, which the framework CLI configures for every embedded deployment — an authored `/thanks` left the application. The destination was correct and the navigation was wrong, and a published renderer cannot fix that alone: only the host knows its mount.
31
+
32
+ **New: `HostNavigationContext` (`@object-ui/react`).** A host offers its own navigate; a renderer uses it when it is there:
33
+
34
+ ```tsx
35
+ import { HostNavigationProvider } from '@object-ui/react';
36
+ import { useNavigate } from 'react-router-dom';
37
+
38
+ function Bridge({ children }) {
39
+ const navigate = useNavigate();
40
+ return (
41
+ <HostNavigationProvider value={{ navigate: (to, o) => navigate(to, { replace: o?.replace ?? false }) }}>
42
+ {children}
43
+ </HostNavigationProvider>
44
+ );
45
+ }
46
+ ```
47
+
48
+ `@object-ui/app-shell`'s `ConsoleShell` now mounts that bridge above every console route, so the console's own routes, and its basename, are what a post-submit redirect resolves against — and the redirect becomes an SPA transition instead of a full page load.
49
+
50
+ **Nothing changes for a host that wires nothing.** With no provider the behaviour is byte-for-byte what it was: one `window.location.assign` of the resolved path after the declared `delayMs`. A host with no router has no basename, so origin-rooted resolution is already right there — the seam changes what a MOUNTED host gets and nothing else. `useHostNavigation()` outside a provider answers `{ navigate: undefined }` and never throws.
51
+
52
+ Two mechanisms were weighed and rejected by the ruling, recorded so they are not re-proposed. **Reading React Router's context when a router happens to be present** is implicit, and unavailable anyway: a React context is a module-instance object, so reading the host's means importing `react-router` into the renderer — which `@object-ui/plugin-form` declares in none of its dependency fields, whose published build externalises every bare specifier, and which two real consumers (`apps/site`, `packages/plugin-view`) do not install. **Requiring `react-router` as a peer** is the honest version of the same thing and costs the package its property of dropping into any React application.
53
+
54
+ What the host takes on by supplying a navigate: the destination becomes a client-side transition, so an in-app path with no matching route renders the host's not-found instead of a full page load. That is the host's routing table to answer for, which is why the choice is the host's. The contract verdict is unchanged either way — a destination `@objectstack/spec` refuses is refused identically with or without a seam, so an injected navigate can never launder a value the authoring door rejects.
55
+
56
+ `navigateOnSuccess` is a different declared key with its own open contract question (objectui#5034) and is deliberately untouched here.
57
+ - dbbd38a: fix(react): `bridgeListView` emits the column spelling the spec declares
58
+
59
+ `mapColumn` took a spec-canonical `ListColumn` — whose columns are **already**
60
+ spelled `field` / `label` — and down-translated every one of them to
61
+ `{ accessorKey, header }` before emitting the `object-grid` node, which
62
+ `ObjectGrid` then translated back. A round trip through a spelling
63
+ `ListColumnSchema` refuses by name, on a value that arrived canonical. The
64
+ bridge now forwards the declared shape, and the tolerance branch on the other
65
+ side retires in the same release (see `@object-ui/plugin-grid`).
66
+
67
+ **Output shape.** `bridgeListView` / `SpecBridge.transformListView` emit
68
+ `columns: [{ field, label?, … }]`. Code reading `node.columns[i].accessorKey`
69
+ off a bridged node reads `field` instead; `header` becomes `label`. The bare
70
+ string shorthand `columns: ['name']` now maps to `{ field: 'name' }`.
71
+
72
+ **No label is invented any more.** `header: col.label ?? col.field` turned "the
73
+ author declared no label" into "the author declared the machine name", and that
74
+ synthesized value pre-empted `ObjectGrid`'s own header chain — the column's
75
+ label, then the **object field's** label, then the prettified machine name —
76
+ whose middle step exists so a localized field label wins on a non-English app.
77
+ A bridged view therefore rendered raw machine names where a directly authored
78
+ `object-grid` rendered the field's real label. A bare `{ field }` column now
79
+ reaches that chain intact.
80
+
81
+ Speaking the declared spelling also routes bridged views through the renderer's
82
+ full ListColumn path rather than its type-inference-only one: object-schema
83
+ field enrichment, `hidden` filtering, primary-field auto-linking, and per-column
84
+ `link` / `action` handling now apply to a bridged `ListView` exactly as they do
85
+ to an authored grid.
86
+
87
+ ### Patch Changes
88
+
89
+ - 516663d: RecordAttachmentsPanel no longer offers a Retry for an api-disabled `sys_attachment` read.
90
+
91
+ `OBJECT_API_DISABLED` (404, `enable.apiEnabled: false`) and its sibling
92
+ `OBJECT_API_METHOD_NOT_ALLOWED` (405, the operation is absent from
93
+ `enable.apiMethods`) are pure functions of the object's metadata — no user, no
94
+ session, no request body — so every retry of every persona re-fetches the
95
+ identical refusal. Before this change both landed in `RecordAttachmentsPanel`'s
96
+ `unavailable` state and offered a Retry that was guaranteed to change nothing,
97
+ the same wrong advice `ListView`'s error panel already stops giving for list
98
+ reads.
99
+
100
+ The panel gains a fifth status, `api-unavailable`: no Retry button, and honest
101
+ copy ("The attachments list is not available on this object.", new
102
+ `detail.attachmentsApiUnavailable` key in all ten locale packs) instead of
103
+ "We couldn't load the attachments for this record." The pre-existing `denied`
104
+ (authorization) and `unavailable` (network/5xx/expired-session) states and
105
+ their affordances are unchanged.
106
+
107
+ `ListView.classifyLoadError` — the classifier that already separated this case
108
+ into its own `api-disabled` kind for list views — is lifted out of
109
+ `packages/plugin-list/src/ListView.tsx`'s module scope into
110
+ `@object-ui/react` (`classifyLoadError`, `LoadErrorKind`), so both surfaces
111
+ consume one classification instead of `RecordAttachmentsPanel` re-deriving it.
112
+ `ListView`'s own behavior is unchanged — it now imports the function it
113
+ previously defined locally. The classifier delegates its api-disabled check to
114
+ `isApiAccessDeniedError` (`@object-ui/data-objectstack`), removing a second,
115
+ independently-maintained copy of the same code list.
116
+ - d374caf: SpecBridge's form-view bridge stops reading `defaultSort` and `aria`, two keys spec 17
117
+ retired on the FormView carrier (`retiredKey()` tombstones — authoring either is a parse
118
+ error). Both guards were unreachable for any FormView that passed validation, and both
119
+ ends were measured dead before removal: no form renderer reads either off the node
120
+ (`plugin-form` reads neither, and `SchemaRenderer`'s ARIA injection resolves flat
121
+ `ariaLabel`/`ariaDescribedBy`/`role`, never a nested `aria` object). Behaviour for
122
+ spec-valid metadata is unchanged; a host that fed the exported bridge a raw pre-17
123
+ document no longer gets these two keys copied onto a node slot nothing consumed. The
124
+ LIST view's `aria` pass-through is untouched — that carrier stayed live and is applied by
125
+ `ListView`.
126
+ - af5e292: Emit explicit file extensions on relative import specifiers, so the published
127
+ entries can be imported by Node's own ESM resolver.
128
+
129
+ `@object-ui/react`'s built entry re-exported through extensionless relative
130
+ specifiers (`export * from './SchemaRenderer'`). Node does not extension-search
131
+ relative specifiers, so `import('@object-ui/react')` under plain Node — an SSR
132
+ host, or any consumer without a bundler — failed with `ERR_MODULE_NOT_FOUND`.
133
+ Bundled consumers were never affected and are unchanged by this.
134
+
135
+ `@object-ui/types`, `@object-ui/core` and `@object-ui/i18n` carried the same
136
+ emission; `@object-ui/react`'s entry stayed unloadable until they were fixed
137
+ too, because evaluation crosses into them. No exported API changed.
138
+ - 0046d8f: Evaluate expressions written under a node's `properties`, not just under `props`
139
+
140
+ `properties` is the spec spelling of a node's config bag and `props` is the
141
+ legacy alias, but `SchemaRenderer`'s evaluation memo only ran the expression
142
+ evaluator over `props`. Renderers in the `element:*` namespace read
143
+ `schema.properties` first, so a node written the canonical way handed the
144
+ renderer the raw `${…}` source and rendered it verbatim, while the same node
145
+ written with the alias evaluated correctly — writing the spec-compliant form
146
+ was the way to lose your expressions.
147
+
148
+ `schema.properties` values are now evaluated per value, exactly as `props`
149
+ already was, and the evaluation runs before the existing
150
+ `properties`-to-top-level hoist so a key means the same thing whether it is read
151
+ as `schema.properties.x`, as `schema.x`, or as the spread `x` React prop.
152
+ Evaluation stays shallow on both spellings (nested objects and arrays are passed
153
+ through, not walked), and `properties` keeps its precedence over `props`.
154
+ - 70a774b: A node writing both `properties` and `props` now gets ONE answer per key, and it is the canonical `properties` one — on both read channels.
155
+
156
+ `properties` is the spec spelling of a node's config bag and `props` is the
157
+ annotated legacy alias, but which one actually reached the screen depended on
158
+ how the receiving renderer happened to read it — and the two channels disagreed
159
+ in opposite directions:
160
+
161
+ - **config bag** (`schema.properties.x`) — the `element:*` family's
162
+ `readProps()` merges `{ ...schema.props, ...schema.properties }`, so
163
+ `properties` won.
164
+ - **React prop** (`x` arriving as a prop) — `SchemaRenderer`'s `createElement`
165
+ spread the hoisted `properties.*` values first and then
166
+ `...(evaluatedSchema.props || {})` last, which overwrote them, so `props` won.
167
+
168
+ Measured on one render of one such node: the bag read `FROM_PROPERTIES` while
169
+ the same key read as a React prop gave `FROM_PROPS`. Which value rendered was
170
+ decided by nothing an author can see — only by whether their chosen component
171
+ belonged to the `readProps()` family.
172
+
173
+ The React-prop channel now declines to let the legacy alias override a key the
174
+ canonical bag also declares; the config-bag order was already correct and is
175
+ unchanged. Scope is co-occurrence only: a key that only `props` declares still
176
+ works exactly as before, and a node that writes one spelling is untouched. The
177
+ `props` alias is not retired here — only its precedence against a co-present
178
+ canonical spelling is settled.
179
+ - dfc6975: Related-list "+ New" now honours `userActions.create` predicates, and the grid
180
+ toolbar's inline-edit affordance is gated on `update` permission (objectui#4646,
181
+ objectui#4647).
182
+
183
+ Two declared-but-unenforced gaps on the same toolbar surface.
184
+
185
+ **#4646 — `createPredicates` had a producer and no consumer.**
186
+ `@objectstack/spec@17.0.0` widened `userActions.create` to
187
+ `z.union([z.boolean(), RowCrudActionOverrideSchema])`, so `resolveCrudAffordances`
188
+ emits `createPredicates` — and nothing in objectui read them, against roughly
189
+ fifteen consumption sites apiece for `editPredicates` / `deletePredicates`. The
190
+ symptom: a parent record entering a frozen state correctly greyed its children's
191
+ row Edit/Delete while the related list's "+ New" stayed fully live, so the user
192
+ filled in the whole child form to earn a server 409. The related-list toolbar now
193
+ evaluates `visibleWhen` / `disabledWhen` **once against the host parent record**,
194
+ per the spec docblock's binding for this key, on top of the existing
195
+ `o.create ∧ can(child, 'create')` check. `visibleWhen` hides "+ New" and fails
196
+ CLOSED; `disabledWhen` greys it and fails SOFT — the same evaluator, fail
197
+ directions and hidden-vs-disabled split the record header already uses for
198
+ edit/delete (objectui#4419 / PR #4515). A bare-boolean `userActions.create` is
199
+ untouched: with no predicates there is nothing to evaluate.
200
+
201
+ **#4647 — the inline-edit toggle was the one ungated affordance on its toolbar.**
202
+ It rendered on "grid view ∧ the host wired `onInlineEditChange` ∧ not the compact
203
+ toolbar", and every host wires that callback unconditionally. New and Import are
204
+ hidden for an account without the grant and the bulk-delete entry on the same
205
+ toolbar ANDs `can(obj, 'delete')`, but a read-only principal could flip inline
206
+ edit, modify cells and press "Save all" to earn a server 403. It is now gated on
207
+ the object's resolved edit affordance ∧ `can(object, 'update')`, mirroring that
208
+ bulk-delete gate. The gate is applied at all three sites that carry this
209
+ affordance — the wide toolbar's toggle, the compact toolbar's settings-popover
210
+ entry (which previously had no gate at all, not even the callback), and the
211
+ `editable` mode handed to the grid, so a stored view carrying `inlineEdit: true`
212
+ can no longer drop a read-only principal into editable cells with no toggle to
213
+ press.
214
+
215
+ `ListViewSchema.userActions.editInline` is also consumed now: an explicit `false`
216
+ withholds the affordance wholesale, which authors previously could not do.
217
+
218
+ **Behaviour change for read-only users, stated plainly.** Where the UI used to
219
+ offer inline editing and let the server refuse it, it now declines to offer the
220
+ entry point at all. No data access changes — the server gate was and remains the
221
+ enforcement boundary; this only stops the UI walking users into round-trips
222
+ guaranteed to fail. Accounts *with* the grant see no change, and hosts with no
223
+ `PermissionProvider` mounted (standalone embeds, the Studio designer) keep
224
+ today's behaviour, since `can()` answers `true` there by design.
225
+
226
+ One deliberate non-change: the absent case of `userActions.editInline` defers to
227
+ the host's existing `inlineEdit` channel rather than enforcing the spec's
228
+ `.default(false)`. Enforcing that default would remove the toggle from every
229
+ stored console list view in one release, since nothing folds a legacy key into
230
+ `editInline` and no existing view declares it. This follows the rule the
231
+ surrounding toolbar-flag block already states for itself — defaults chosen to
232
+ match what the flags have always done. `InterfaceListPage`, the key's other
233
+ consumer, reads the absent case as OFF, because the ADR-0047 interface page has
234
+ no such host channel to defer to.
235
+ - 144ef9b: Dev builds now shout when an unevaluated `${…}` expression reaches the DOM.
236
+
237
+ A value only reaches the user if `SchemaRenderer` EVALUATES it and the renderer
238
+ READS IT BACK, and those two sets do not fully overlap. Where they miss, the
239
+ failure was silent. Measured on a real render with `dataSource: { n: 99 }`:
240
+ `{ type: 'ui:statistic', value: '${data.n}' }` puts the literal text `${data.n}`
241
+ on screen, because the evaluation memo covers `content`, the `properties` /
242
+ `props` bags and the predicate keys and passes every other top-level key through
243
+ untouched. An author — increasingly, an AI authoring metadata — got no signal at
244
+ all: a literal `${data.n}` in front of a user reads like a data problem rather
245
+ than a contract violation.
246
+
247
+ `SchemaRenderer` now reports such a value once per node via `console.error`,
248
+ naming the node type and id, the key the raw source survived on (spelled the way
249
+ it was authored — `properties.value`, not the hoisted top-level copy), the
250
+ expression source verbatim, and the channels that do work today. It also catches
251
+ the second, harder shape: an expression that WAS evaluated but THREW, which
252
+ `ExpressionEvaluator` returns as its source text — indistinguishable on screen
253
+ from a key that was never evaluated.
254
+
255
+ Diagnostic only. No evaluation behaviour changes, no DOM attribute is added, and
256
+ the whole module is behind the module-load `NODE_ENV` constant a bundler folds
257
+ away, so production pays nothing.
258
+
259
+ Two boundaries are deliberate. The scan is exactly as deep as evaluation is —
260
+ shallow — because a nested `aria: { label: '${…}' }` keeps its raw source today
261
+ by decision (objectui#4799 pinned that shallowness on both bags), and a deeper
262
+ scan would report a shape the engine has not yet decided to change. And schema
263
+ METADATA is never reported: `visible` / `visibleWhen` / `hidden` / `disabled` /
264
+ … hold raw predicate source by design, and the diagnostic reads the set of
265
+ values that actually leaves for the DOM, after those keys have been stripped.
266
+
267
+ Part of objectui#4795 (Direction 3, per the maintainer's 2026-08-17 ruling).
268
+ Widening the set of evaluated text keys is Direction 1 and stays deferred behind
269
+ its named restart condition; this change deliberately does not pre-empt it.
270
+ - Updated dependencies [88085e3]
271
+ - Updated dependencies [69251bf]
272
+ - Updated dependencies [57e668f]
273
+ - Updated dependencies [516663d]
274
+ - Updated dependencies [41ac1b7]
275
+ - Updated dependencies [1eaf0a1]
276
+ - Updated dependencies [2533ec5]
277
+ - Updated dependencies [bbe8b86]
278
+ - Updated dependencies [8477be5]
279
+ - Updated dependencies [279fb13]
280
+ - Updated dependencies [2e82ab2]
281
+ - Updated dependencies [ad07b65]
282
+ - Updated dependencies [41f498b]
283
+ - Updated dependencies [ef0d150]
284
+ - Updated dependencies [1ef236e]
285
+ - Updated dependencies [f34226e]
286
+ - Updated dependencies [564b605]
287
+ - Updated dependencies [e1d4251]
288
+ - Updated dependencies [1184192]
289
+ - Updated dependencies [a2a9747]
290
+ - Updated dependencies [a1609a6]
291
+ - Updated dependencies [37f6844]
292
+ - Updated dependencies [2b50261]
293
+ - Updated dependencies [ac600e5]
294
+ - Updated dependencies [c1ef923]
295
+ - Updated dependencies [af5e292]
296
+ - Updated dependencies [7f96b10]
297
+ - Updated dependencies [167ec42]
298
+ - Updated dependencies [cf4f8a6]
299
+ - Updated dependencies [f1d4748]
300
+ - Updated dependencies [bea374e]
301
+ - Updated dependencies [b1119ec]
302
+ - Updated dependencies [9f23d2b]
303
+ - Updated dependencies [578e025]
304
+ - Updated dependencies [af025ee]
305
+ - Updated dependencies [d109a4d]
306
+ - Updated dependencies [3d053bb]
307
+ - Updated dependencies [598c89a]
308
+ - Updated dependencies [b8b9af4]
309
+ - Updated dependencies [31676be]
310
+ - Updated dependencies [9ce096f]
311
+ - Updated dependencies [e05db88]
312
+ - Updated dependencies [ad13d63]
313
+ - Updated dependencies [5ffcc14]
314
+ - Updated dependencies [d971e51]
315
+ - Updated dependencies [97abb24]
316
+ - Updated dependencies [deb157a]
317
+ - Updated dependencies [9c60144]
318
+ - Updated dependencies [d2ce342]
319
+ - Updated dependencies [a8411ad]
320
+ - Updated dependencies [9695da7]
321
+ - Updated dependencies [58b8346]
322
+ - Updated dependencies [a9e17b4]
323
+ - Updated dependencies [b8ce7dc]
324
+ - Updated dependencies [8871c14]
325
+ - Updated dependencies [dfc6975]
326
+ - Updated dependencies [3cf4de0]
327
+ - Updated dependencies [c9dc811]
328
+ - Updated dependencies [d871f8e]
329
+ - Updated dependencies [a0b9e91]
330
+ - Updated dependencies [99bd015]
331
+ - Updated dependencies [21e4585]
332
+ - @object-ui/types@17.6.0
333
+ - @object-ui/i18n@17.6.0
334
+ - @object-ui/core@17.6.0
335
+ - @object-ui/data-objectstack@17.6.0
336
+
337
+ ## 17.5.0
338
+
339
+ ### Minor Changes
340
+
341
+ - d9d3463: Retire four zero-consumer declared surfaces (dead-surface sweep batch 3, #4328). Each was
342
+ measured as declared-but-never-read at the branch point, and each is removed rather than
343
+ left as an authoring surface whose values nothing acts on.
344
+
345
+ Breaking for anyone who typed against the removed declarations, marked `minor` per this
346
+ repository's version-alignment convention (the major tracks `@objectstack`, never an
347
+ API-break count):
348
+
349
+ - `@object-ui/core` no longer exports `mergeViewsIntoObjects`. It was a second copy left
350
+ behind by the move of that step to the provider layer, and it had drifted: it ignored a
351
+ view container's default `list` and keyed views by the authored bare key instead of the
352
+ composer's `<object>.<key>` identity. The live implementation — `MetadataProvider`'s, in
353
+ `@object-ui/app-shell` — is unchanged and remains the only one. (#3775)
354
+ - `@object-ui/types`' `RoleDefinition` no longer declares `permissions`. A role's grants
355
+ live in `ObjectPermissionConfig.roles`, keyed by object; that is the only home any
356
+ consumer reads (`resolveRoles` walks `inherits` and matches on `name`). The removed
357
+ field was _required_, so five fixtures across three packages had been declaring an empty
358
+ array for a value nothing would ever look at. Role-attached grants are now a compile
359
+ error rather than silently ignored data. (#4288)
360
+ - `@object-ui/react`'s `RecordContextValue` no longer declares `loading` / `error`. Both
361
+ had zero producers and zero consumers — no host passed them, no `record:*` renderer read
362
+ them — and only the provider's memo dependency list still named them. Record-level
363
+ loading and error state stays where it is actually expressed: each renderer's own data
364
+ source. (#3773)
365
+
366
+ No behaviour change, no request-count change:
367
+
368
+ - `@object-ui/data-objectstack` drops five `metadataCache.invalidate('views:<object>')`
369
+ calls across `updateViewConfig` / `createView` / `updateView` / `deleteView`. No read
370
+ path has ever populated that key — `listViews` fetches directly, uncached — so all five
371
+ were permanent no-ops. The invalidations of the keys that do have readers
372
+ (`view:<object>:<viewId>` for `getView`, `view-overrides:<object>` for
373
+ `listViewOverrides`) are untouched and now pinned. (#3778)
374
+
375
+ - b953a97: fix(detail): lookup field values link to the referenced record
376
+
377
+ A valued lookup on a record detail page rendered as plain text plus a copy
378
+ button — the referenced document's name was visible but unreachable, so users
379
+ copied the number and searched for it from the list page instead. Lookup cells
380
+ inside a related list that pointed at a third object were dead the same way.
381
+
382
+ `LookupCellRenderer` — the one cell renderer both surfaces resolve through —
383
+ now renders the display value as a link to the referenced record. The display
384
+ name resolution, the copy affordance and every non-lookup field are unchanged,
385
+ and a lookup with no value still renders its placeholder rather than an empty
386
+ link.
387
+
388
+ The URL is not assembled in the renderer. `RelatedRecordActionsContext` gains
389
+ an optional `recordHref` / `openRecord` pair, published by the console's
390
+ `RelatedRecordActionsBridge` from the SAME builder its related-list row
391
+ navigation already used, so there is one record-route shape rather than a
392
+ second one. A host that does not provide it (Studio designer, embedded
393
+ renderers, standalone grids) renders exactly what it rendered before.
394
+
395
+ - d7f3e30: `bridgeListView` maps the five row heights the spec admits, and only those — the four dead spellings are gone
396
+
397
+ `mapDensity` carried a nine-key table: `compact`, `short`, `comfortable`,
398
+ `spacious`, `small`, `medium`, `large`, `tall`, `extra_tall`. `RowHeightSchema`
399
+ in `@objectstack/spec` admits five — `short | compact | medium | tall |
400
+ extra_tall` — so `comfortable`, `spacious`, `small` and `large` were unreachable
401
+ from any spec-valid list view. The bridge's own parameter type said as much
402
+ (`Partial< ListView >`), and `mapDensity` widened it back to `rowHeight?: string`
403
+ to let them in. They survived because the fixture asserting three of them
404
+ compiled against nothing: the package's build tsconfig excludes tests and no
405
+ other `tsc` read them, so four branches of renderer-side dialect read as live
406
+ capability (objectui#4352, surfaced by objectui#4040 / PR #4351).
407
+
408
+ They are deleted. The parameter takes the spec type honestly, and the table is
409
+ now `Record< RowHeight, … >`, so a row height added upstream fails the build here
410
+ instead of arriving with no density. This is AGENTS.md #0.1: a lenient reading
411
+ for off-spec metadata is a second de-facto contract, and one strict contract
412
+ beats N dialects — a bad `rowHeight` gets fixed at the producer, where the schema
413
+ already rejects it.
414
+
415
+ **Breaking semantics, deliberately graded `minor`** (this repo never publishes
416
+ `major` — its major tracks `@objectstack`). Nothing narrows in the published type
417
+ surface: `mapDensity` is module-local and never appeared in the emitted `.d.ts`,
418
+ and `bridgeListView`'s declaration is unchanged. What changes is runtime output,
419
+ and only for input the spec already rejects: a host handing the bridge
420
+ `rowHeight: 'comfortable'` (or `'spacious'` / `'small'` / `'large'`) used to get
421
+ `density: 'comfortable'` / `'spacious'` / `'compact'` back, and now gets no
422
+ `density` key at all, so the renderer's own default applies. A sweep of this repo,
423
+ the `objectstack` example apps and the console's view metadata found zero authored
424
+ uses of any of the four; the legacy `densityMode` alias cannot produce one either,
425
+ since `DENSITY_MODE_TO_ROW_HEIGHT` is typed `Record< DensityMode, RowHeight >` and
426
+ folds onto `compact` / `medium` / `tall`.
427
+
428
+ - a84385b: `NavigationConfig.mode` is optional — the type now says what the hook does
429
+
430
+ `@object-ui/react` published a `NavigationConfig` that required `mode`, in front of a `useNavigationOverlay` that has always defaulted it. The declaration took the spec's authored config, `Omit`ted `mode`, and re-added it as `NonNullable< … >`; 140 lines below, the hook read `navigation?.mode ?? 'page'`. The type was strictly stricter than the implementation it fronted, and `'page'` is meaningful behaviour rather than a placeholder.
431
+
432
+ The spec never asked for that. `NavigationConfigSchema` declares `mode: NavigationModeSchema.default('page')`, and a `.default()` lands on the authoring side as `| undefined` — so `navigation: { view: 'summary_view' }` is legal authored metadata that lets the mode default. `@object-ui/types` already re-exported the spec's own `NavigationConfig` unchanged, which meant one monorepo shipped two published types of the same name that disagreed about whether `mode` could be omitted.
433
+
434
+ The alias is now the spec's authored config verbatim, with no divergence of its own:
435
+
436
+ ```ts
437
+ export type NavigationConfig = SpecAuthoredInput<
438
+ typeof NavigationConfigSchema
439
+ >;
440
+ ```
441
+
442
+ The cost of the old spelling was paid by callers. `ListView` carried `schema.navigation as NavigationConfig | undefined` for no reason except to get a valid spec-shaped value past the declaration; that assertion is deleted here, not replaced. A type in front of an implementation must not be stricter than the implementation — when it is, every caller pays in casts, and a cast is exactly the renderer-side workaround that belongs back at the producer.
443
+
444
+ **Nothing changes at runtime.** `navigation?.mode ?? 'page'` is untouched, and the default is now pinned as observable behaviour (`useNavigationOverlay.modeDefault.test.tsx`) rather than only as a comment — the explicit modes, the `preventNavigation` and `none` short-circuits, the `onRowClick` priority, and the Cmd/Ctrl/middle-click and `new_window` branches are all pinned alongside it.
445
+
446
+ **Why minor rather than patch**, from the measured `.d.ts`. Optional-izing a property is looser for writers and narrower for readers, so the grade turns on which role the published surface actually plays. In this package `NavigationConfig` occurs only in input positions — `useNavigationOverlay`'s `navigation?:` option and `resolveOverlayWidth`'s parameter — and never in a return type; the package consumes these values and never hands one back. For consumers the change is therefore purely permissive: every call that compiled before still compiles, and spec-shaped configs that previously needed an assertion now compile without one. That gained input shape is a real capability rather than an internal repair, which is more than a patch describes. The reader-side narrowing is real but secondary: code that imports the bare type, annotates its own value with it and reads `.mode` now sees `NavigationMode | undefined`. The in-repo census found exactly one such importer — `ListView` — and it imported the type only to write the assertion this change removes.
447
+
448
+ - c1d939f: One `SchemaNode`, and one label vocabulary — the union wins, and labels resolve where the locale lives
449
+
450
+ Two packages published a type called `SchemaNode` and they were not the same type. `@object-ui/core` hand-declared `interface SchemaNode { type: string; … [key: string]: any }`; `@object-ui/types` exported `type SchemaNode = BaseSchema | string | number | boolean | null | undefined`, whose own doc comment names `'Plain string'` a valid node. Both were exported under one name from packages the same consumers import together, so which declaration a call site got depended on which package it happened to import from — #4548's canary measured 19 of 35 errors as exactly that collision. Core's declaration is now a re-export of types', so there is one declaration left to disagree with. Core's entry surface is unchanged: `dist/index.d.ts` is byte-identical across the change.
451
+
452
+ Reconciling it exposed a real defect rather than a mechanical narrowing, which is why the first attempt was withdrawn instead of forced. The spec bridges write `spec.label` — the spec's `I18nLabel`, an INLINE locale map like `{ en: 'Owner', 'zh-CN': '负责人' }` — into `node.label`, and `BaseSchema.label` declared `string`. Under core's old index signature that assignment was invisibly `any`; under one honest `SchemaNode` it is a type error. `BaseSchema.label` and `.description` therefore now accept `string | I18nLabel`, and the two bridge assignments compile with their expressions untouched.
453
+
454
+ Resolution happens at READ time, in the renderer, against the display locale — not at the bridge. Resolving at the bridge was measured unimplementable: it is a plain class method that cannot call a hook, `BridgeContext` declares no locale, and `updateContext()` has zero callers, so a bridge-resolved label would freeze one audience's language into the node tree with no re-translation channel. React's own invalidation re-translates for free at the read site.
455
+
456
+ The widening turned every blind `schema.label`-as-string read into a named compiler error, and that inventory is the audit: it named four sites repo-wide, all one class — the label reaching a React child position, where a map does not render as `[object Object]` but THROWS `Objects are not valid as a React child`, failing the whole subtree. Three are `@object-ui/components` renderers (`filter-builder`, `sidebar-group`, `dropdown-menu`), which now resolve with the spec's own `resolveI18nLabel` against `useDisplayLocale()`. The fourth is `plugin-dashboard`'s `DashboardGridLayout` heading, which resolves with `pickLocalized` against the active UI language — matching the widget-title resolution already in that same component rather than putting two resolvers and two disagreeing locale channels in one render; the two resolvers are limb-for-limb twins with a parity test pinning them.
457
+
458
+ One interface now carries both label vocabularies two properties apart — `label`/`description` are the spec's INLINE map, `ariaLabel` is the KEYED bundle reference — and each accepts the other's shape vacuously. That confusability is objectui#4167's known hazard, inherent to the spec's `I18nLabel` design; both shapes are named with cross-referenced doc comments stating which resolver owns which slot, and a pin asserts the two unions do not collapse into each other.
459
+
460
+ Finally, the spec bridges declare their return type as `BaseSchema` instead of the union. Both bridges end in a single `return node` on an object literal, so the union described nothing real while forcing a narrowing at every read — 272 mechanical errors across five suites in the first round. That change is a type annotation only; the emitted JavaScript is byte-identical.
461
+
462
+ - 3f5f87c: `SchemaRenderer` states its real contract — a typed, required `schema` and a deliberate forwarding surface
463
+
464
+ `SchemaRenderer` is the renderer loop: every registered SDUI component is rendered through it. It handed `forwardRef` a props type of `{ schema: SchemaNode } & Record<string, any>`, which puts `string` into `keyof Props`, so `'ref' extends keyof Props` was always true, React's `PropsWithoutRef` took its `Omit` branch, and `Omit` over a type carrying a string index signature keeps only the index signature. Every declared prop was erased. Measured on the pre-fix source: `keyof ComponentProps<typeof SchemaRenderer>` was `string` and `ComponentProps<typeof SchemaRenderer>['schema']` was `any`, while the type argument went on declaring `SchemaNode`. The other half is the same defect seen from the call site — `<SchemaRenderer />` with no schema at all, `<SchemaRenderer schema={12345} />`, and an arbitrary misspelled prop each type-checked in silence. This is objectui#4422 / PR #4438's trap in the most central component in the repo, spelled `Record<string, any>` rather than `[key: string]: any`, which is why every previous sweep's grep and both shipped guards' detector reported the site as clean.
465
+
466
+ Graded **minor, not major**, on objectui#4528's reasoning: the type argument has always DECLARED `schema`; the index signature erased it from the resolved type, and restoring what the declaration documents is a fix to the published contract rather than a contract break.
467
+
468
+ **The forwarding surface is kept, deliberately.** This component forwards every prop it does not read to the component the schema names, resolved at runtime from a plugin-extensible registry — `packages/react/README.md` documents exactly that, and `@object-ui/components`' form renderer consumes the `onSubmit` it shows being forwarded. Closing that surface would state a false contract and would force every leaf plugin's props into this package. So the two halves are separated: the `forwardRef` type argument is the honest `SchemaRendererProps`, with no index signature for `PropsWithoutRef` to collapse, and the open surface is stated once in an explicit export annotation, which nothing routes through `Omit`. The published `.d.ts` shows the erasure disappearing: `ForwardRefExoticComponent<Omit<{ schema: SchemaNode } & Record<string, any>, "ref"> & RefAttributes<any>>` becomes `ForwardRefExoticComponent<SchemaRendererProps & Record<string, any> & RefAttributes<any>>`.
469
+
470
+ `SchemaRendererProps.schema` is declared as `BaseSchema | string | null | undefined` — what this component actually handles. It previously declared `@object-ui/core`'s `SchemaNode` interface, which requires `type: string` and so contradicted the component's own early returns for strings and nullish, while every caller held `@object-ui/types`' wider union. The erasure hid that mismatch completely.
471
+
472
+ **One declared behaviour change.** A non-object, non-string primitive schema now renders as its own text. It previously fell through to the shallow copy `{ ...schema }`, which spreads a primitive to an empty object, lost the `type` the renderer then looked up, and surfaced the red "Unknown component type: undefined" box — an accident of the spread rather than a decision. The declared props type excludes `number` / `boolean` so no author is invited to pass them; the runtime handling is defence-in-depth for untyped callers and stored metadata. Strings, `null`, `undefined`, `0` and `false` render exactly as before, and an object naming an unregistered type still gets the error box; all four are pinned.
473
+
474
+ Latent defects the erasure had been hiding, each surfaced by the repo-wide type-check and fixed at its call site: `DashboardRenderer` cast its widget schema to `Record<string, any>`, dropping the `type` every branch of `getComponentSchema` sets; `DashboardGridLayout`'s equivalent now states its return type instead of inferring a union that admitted a shape with no `type`; and `ReportViewer` handed a section's `content` array to the renderer whole, so a multi-node section rendered the unknown-component box instead of its content — arrays are mapped rather than widened into the renderer's declared input.
475
+
476
+ A repo-wide structural guard replaces the two per-package siblings' blocked direction: it judges every `forwardRef` in `packages/*/src` (219 sites) and its detector resolves `Record<string, …>` and `string`-keyed mapped types in addition to literal index signatures — the spelling the previous detector went blind on. It judges the type argument only, where an index signature is an accidental eraser, and never an export annotation, where one is a stated contract.
477
+
478
+ - bb68488: Stop declaring 14 symbols under names `@objectstack/spec` owns at `17.0.0-rc.6`
479
+ (objectui#4167, objectstack#4115).
480
+
481
+ The rc.6 bump published nine names this repo already declared locally, on top of
482
+ four that predate it — `check:spec-symbols` reported all thirteen at once, and a
483
+ fourteenth (`GlobalFilterSchema`) appeared during the bump itself. Each was
484
+ triaged on its own rather than blanket-renamed, because the right answer differs
485
+ per symbol: five bind to the spec, three are renamed because the spec's
486
+ same-named export means something else, five arrive by derivation, and one is a
487
+ declared dialect with a written reason.
488
+
489
+ **Breaking for importers of `@object-ui/react`, `@object-ui/app-shell` and
490
+ `@object-ui/types`** — three exported names changed, because the spec exports the
491
+ same name for a _different_ thing:
492
+
493
+ | package | was | now | what the spec's same-named export actually is |
494
+ | :-------------------- | :----------------- | :----------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- |
495
+ | `react` / `app-shell` | `MetadataState` | `MetadataCacheState` | a metadata item's LIFECYCLE state — `'draft' \| 'active' \| 'deprecated' \| 'archived'` (`MetadataStateSchema`, `@objectstack/spec/system`) |
496
+ | `react` / `app-shell` | `resolveI18nLabel` | `resolveKeyedI18nLabel` | a resolver for the INLINE per-locale map (`{ en: 'Owner', 'zh-CN': '负责人' }`) against a BCP-47 locale |
497
+ | `types` | `DateRangePreset` | `FilterBuilderDateRangePreset` | the thirteen HISTORICAL dashboard filter-bar presets; this one is the filter-builder set, which adds eight FUTURE windows the dashboard schema rejects |
498
+
499
+ `resolveI18nLabel` is the one where the collision had already started costing
500
+ something. rc.6 widened `I18nLabel` from `string` to
501
+ `string | Record< string, string >`, so the same authored value now reaches
502
+ either resolver — and each answers wrongly, silently, for the other's input: the
503
+ keyed one returns `undefined` for `{ en: 'Owner' }` (no `key`, no
504
+ `defaultValue`), and the spec's reads `key` / `defaultValue` / `params` as locale
505
+ tags. The rc.6 bump PR met this and aliased the spec's import as
506
+ `resolveInlineI18nLabel` in five files, with hand-written comments at two of
507
+ them. That is a review convention, which is what objectstack#4115 exists to
508
+ replace with a rule — so `Keyed` is now the counterpart of that `Inline`, and the
509
+ name says which vocabulary it resolves at every call site.
510
+
511
+ **Eleven keep their names and are now imported or derived from the spec** instead
512
+ of re-declared: `DATE_RANGE_PRESETS`, `NavigationMode`, `AddressValue`,
513
+ `BreakpointColumnMap`, `BreakpointOrderMap`, `KanbanConfig`, `CalendarConfig`,
514
+ `GanttConfig`, plus the three renamed above at their new names.
515
+
516
+ **Four of the copies were losing information, not just duplicating it.**
517
+
518
+ - **`GanttConfig` declared six keys and called itself canonical; rc.6's
519
+ `GanttConfigSchema` declares seventeen.** The eleven it never mentioned —
520
+ `parentField`, `typeField`, `baselineStartField`, `baselineEndField`,
521
+ `groupByField`, `resourceView`, `assigneeField`, `effortField`, `capacity`,
522
+ `quickFilters`, `autoZoomToFilter` — are all read by
523
+ `plugin-gantt/src/ObjectGantt.tsx`, through a local `GanttConfigEx`
524
+ intersection that existed only because this type did not carry them. It now
525
+ derives from the spec, with `timeSegments` (shift segmentation) as the one
526
+ genuinely local extension; the schema is `$loose` upstream, so that key is
527
+ legal metadata rather than a second dialect.
528
+ - **`GanttConfig.tooltipFields` carried the comment "not part of the upstream
529
+ GanttConfigSchema".** It is, as of rc.6, so the key now arrives from the spec.
530
+ - **`AddressValue` declared five of the spec's seven parts** — `countryCode` and
531
+ `formatted` were missing, under a comment already claiming to be "the part
532
+ names of `AddressSchema`". The widget still renders five inputs; binding the
533
+ type stops it from asserting the platform cannot store the other two, and makes
534
+ the `{ ...address }` write-through say so.
535
+ - **`DATE_RANGE_PRESETS` was `Object.keys(PRESET_RANGES)`,** a third copy of a
536
+ vocabulary the spec extracted in objectstack#4614 precisely to collapse — its
537
+ own doc comment names this module as one of the three. It is now the spec's
538
+ array by reference, and the local date-macro bounds table is pinned complete
539
+ against it with `satisfies`, so a preset the schema gains without bounds here
540
+ is a compile error rather than a filter that validates clean and then selects
541
+ nothing.
542
+
543
+ `NavigationMode` was one hop from the spec already (`NavigationConfig['mode']`);
544
+ it is bound directly, with a both-directions type pin that it stays the same type
545
+ as the config's own `mode`. `KanbanConfig` / `CalendarConfig` /
546
+ `BreakpointColumnMap` / `BreakpointOrderMap` were exact hand copies of `$strict`
547
+ schemas and are now re-exports — "still exact" is the argument for binding them,
548
+ since a copy with nothing to protect can only drift.
549
+
550
+ `GlobalFilterSchema` is the one ALLOW entry. It is the same spread-composition
551
+ dialect as `SelectOptionSchema` next to it, and it collided only because rc.6's
552
+ new refinement forced `.extend()` to be respelled as a `.shape` spread — which
553
+ moved a derivation the guard could see into an object literal it deliberately
554
+ does not descend into. The dialect is unchanged and its three divergences are
555
+ pinned; which side moves on the refinement itself is objectui#4165.
556
+
557
+ `@objectstack/spec` moves from `devDependencies` to `dependencies` in
558
+ `@object-ui/layout`: its public type surface now references the spec.
559
+
560
+ ### Patch Changes
561
+
562
+ - ceccdcf: Action confirm dialogs and success toasts now honour the bundle's translated
563
+ `confirmText` / `successMessage`, not just `label` (objectui#4265).
564
+
565
+ A TranslationBundle entry for an action carries three keys under one
566
+ `_actions.<name>` node — `label`, `confirmText`, `successMessage` — and
567
+ `useObjectLabel()` has always exposed a resolver for each. What had drifted was
568
+ the call sites: `page:header` (authored record pages), `record:quick_actions`
569
+ and the related-list row menu resolved the button `label` only and dispatched
570
+ the authored `confirmText` / `successMessage` untouched. One bundle entry met
571
+ two fates: the button rendered the translation, the confirm dialog rendered the
572
+ authored English.
573
+
574
+ All action-rendering surfaces now go through one resolver,
575
+ `useActionTextLocalizer()` (new, exported from `@object-ui/react`), which
576
+ applies the existing `actionLabel` / `actionConfirm` / `actionSuccess`
577
+ resolvers over the three keys together. Fallback is unchanged: with no bundle
578
+ entry — or an entry lacking a key — the authored text renders. A bundle cannot
579
+ introduce a `confirmText` or `successMessage` the metadata never declared.
580
+
581
+ - ee26e65: Analytics: the dimension label net's fetch-and-memo glue is written once, not once per surface
582
+
583
+ PR #4388 (objectui#4330) put the same React glue on two surfaces — the dashboard's `DatasetWidget` and plugin-report's dataset block. The resolution RULES were never duplicated (both call the same `@object-ui/core` helpers), but the wiring around them was: read the object schema through the host's authenticated `apiFetch`, keep the fetched metadata locale-free in state, derive the label maps in a render memo. Two copies meant two statements of the same two bug fixes, which is a drift surface rather than a defect — nothing a user could hit today, filed as objectui#4389 so it was retired deliberately.
584
+
585
+ It is now split along the layer that can actually hold each half. `@object-ui/core` gains the React-free parts — `loadDimensionFieldMeta` (the base-object read composed with the dimension walk), `deriveDimensionLabelMaps` (the locale-applying derivation) and `dimensionOptionTranslator` (binding the bundle resolver to the object that OWNS a terminal field, which for a dotted path is the relationship target). `@object-ui/react` gains `useDatasetDimensionLabels` / `useDatasetDimensionMeta`, the React wiring that cannot live in core, beside the `useViewData` / `useElementDataSource` / `useDiscovery` hooks that already read `SchemaRendererContext` the same way. Both plugins consume it; the dashboard keeps its chart-only per-category colour and category-order derivation layered locally, since a table renders no palette.
586
+
587
+ The card originally proposed `@object-ui/core` as the whole glue's home. That home was disproven by measurement and retired in the card's PM RULING #2: `SchemaRendererContext` is defined in `@object-ui/react`, which depends on core, so core importing it back is a cycle — and core is React-free by declaration, by content, and by the topology in AGENTS.md. objectui#3367 had already ruled this direction for the same family (core-canonical logic, react re-exports).
588
+
589
+ Behaviour is unchanged by construction: same read count, same best-effort fallback, same memoization boundary. The two bug fixes are now stated once and pinned at the shared hook — the read rides the host's authenticated `apiFetch` (objectui#4121, pinned by asserting that a new channel re-issues the read, i.e. that it really is in the effect's deps), and the fetched metadata stays locale-free (objectui#4030 / PR #4324, pinned by switching language at runtime and asserting the labels flip with no second metadata read). All 39 assertions PR #4388 landed across both surfaces pass unchanged, and their files are byte-identical to before.
590
+
591
+ - f650253: `BaseSchema.ariaLabel` declares the keyed i18n vocabulary the renderer actually
592
+ resolves, `.disabled` accepts the predicate string it actually evaluates, and the
593
+ keyed shape finally has a name (objectui#4581)
594
+
595
+ Three slots on one base type had drifted from what the renderer does with them.
596
+ PR #4593 fixed `visible` and measured the rest; these are the rest.
597
+
598
+ `ariaLabel` was `string`, but `SchemaRenderer.tsx:111` resolves it with
599
+ `resolveKeyedI18nLabel`, whose input is the KEYED form
600
+ `{ key, defaultValue?, params? }` — a reference into a translation bundle. It is
601
+ now `string | KeyedI18nLabel`, and `KeyedI18nLabel` is a new exported type in
602
+ `@object-ui/types` rather than a fourth inline copy of one object literal: the
603
+ three that existed (`@object-ui/react`'s resolver, `@object-ui/layout`'s
604
+ `resolveLabel`, `@object-ui/app-shell`'s `t`-taking twin) were verified identical
605
+ in their object half first, and two of them now import the name.
606
+
607
+ The vocabulary matters more than the widening. `#4581` originally asked for
608
+ `string | I18nLabel`, and that spelling was withdrawn as measured-wrong: the
609
+ spec's `I18nLabel` is the INLINE LOCALE MAP (`string | Record<string, string>`),
610
+ a different vocabulary resolved against a BCP-47 locale by a different function
611
+ of a confusingly similar name. Under it the shipped keyed fixture type-checked
612
+ only vacuously — as a locale map whose "locales" are named `key` and
613
+ `defaultValue` — the same label carrying `params` was rejected outright, and a
614
+ genuine `{ en: 'Owner' }` compiled while rendering an EMPTY `aria-label`. Naming
615
+ the keyed shape is the declaration half of the fix objectui#4167 started on the
616
+ naming side; `@object-ui/app-shell`'s copy keeps its inline spelling for now
617
+ because an open PR has a pending change to that file, and the comment there says
618
+ so.
619
+
620
+ `disabled` was `boolean` on a key the renderer never reads as one:
621
+ `SchemaRenderer.tsx:466` evaluates it through the same `evaluateCondition` as
622
+ `visible`, and a `disabledOn?: string` sibling exists for the same reason. It is
623
+ now `boolean | string`. The asymmetry with `visible` was accidental rather than
624
+ deliberate.
625
+
626
+ Both are widenings on authored-input-dominant properties: authors gain a
627
+ spelling, nothing that type-checked before stops doing so, and readers already
628
+ coped with `any` through `BaseSchema`'s index signature. Three test fixtures that
629
+ had been casting past these declarations with `as unknown as BaseSchema` state
630
+ their values directly now, and the declared unions are pinned invariantly so
631
+ neither a missing widening nor an overshoot to `any` can pass unnoticed.
632
+
633
+ Declaring the vocabulary honestly also surfaced a real one: the `toggle`
634
+ renderer writes `aria-label` itself instead of going through SchemaRenderer's
635
+ resolver, and it forwarded the raw value. Invoked directly it emitted
636
+ `aria-label="[object Object]"` for a keyed label — announced verbatim by a
637
+ screen reader. It resolves now. Through `SchemaRenderer` the bug was invisible,
638
+ because SchemaRenderer injects its own resolved `aria-label` afterwards; a
639
+ downstream type-check sweep found it, not a test.
640
+
641
+ `BaseSchema.label` and `.description` are deliberately unchanged and pinned that
642
+ way. They receive the spec's inline `I18nLabel` from the view bridges, which is a
643
+ real defect, but resolving it belongs at the spec-to-schema boundary rather than
644
+ in this declaration — and that work is still blocked on a design question about
645
+ where the display locale enters, so it is not in this release.
646
+
647
+ - 8f85f8b: The spec bridge abstains on prototype-member `rowHeight` spellings instead of leaking a
648
+ function into `density`.
649
+
650
+ `bridgeListView`'s `mapDensity` indexed a plain object literal with an unchecked key, so
651
+ the lookup reached `Object.prototype`. The parameter is typed `RowHeight`, but the
652
+ boundary a host's stored view definition actually crosses is `SpecBridge.transformListView`,
653
+ whose parameter is `any` — so `rowHeight: 'toString'` came back as `Object.prototype.toString`,
654
+ a **function**, out of a read whose return type is three strings or nothing. `bridgeListView`
655
+ then writes the key under `if (density)`, and a function is truthy, so the bad value was not
656
+ merely returned: it was stored on a `SchemaNode` whose renderer expects
657
+ `'compact' | 'comfortable' | 'spacious'`. Same for `constructor`, `valueOf`,
658
+ `hasOwnProperty`, `isPrototypeOf`, `propertyIsEnumerable` and `toLocaleString`.
659
+
660
+ The lookup is now guarded with `Object.prototype.hasOwnProperty.call(...)` — the same guard
661
+ `@object-ui/core`'s `rowHeightToDensityMode` grew in objectui#4440, and the repo's existing
662
+ convention at eight other sites. Both `rowHeight` surfaces now abstain identically on every
663
+ off-spec **string** spelling, and objectui#4440's agreement pin covers the prototype-member
664
+ family instead of excluding it (objectui#4442).
665
+
666
+ Runtime-only: no public type moved, and no spec-valid `rowHeight` changes its answer.
667
+
668
+ - d0c3b26: Every plain `<button>` now declares its `type`. HTML defaults an untyped button to
669
+ `type="submit"`, so any of these buttons would submit the form it was composed into
670
+ instead of running its own handler — a real risk for renderers (`drawer`, `tree-view`,
671
+ `navigation-overlay`) whose placement inside a form is a JSON metadata decision. 114
672
+ sites were converted to `type="button"`; no site was a genuine submit button, and the
673
+ DOM is otherwise unchanged.
674
+
675
+ The defect class is now closed mechanically by a new `object-ui/button-has-type` ESLint
676
+ rule (error), so the next untyped button fails CI at write time rather than being found
677
+ by a fourth audit round (objectui#4045, closing the objectui#3344 family).
678
+
679
+ - f148a64: SpecBridge lifts a legacy bare `exportOptions` array to the spec's object form, so a
680
+ spec-authored view's declared export formats reach the grid (objectui#4585).
681
+
682
+ A spec `ListView` may spell `exportOptions` either way, and `@objectstack/spec` lifts the
683
+ legacy bare format array to `{ formats: [...] }` when it parses one (objectstack#8010).
684
+ That lift never ran on the bridge path: the bridge's input is a TypeScript type, not a
685
+ parsed value — there is no `parse`/`safeParse` anywhere under `spec-bridge/` — so a host
686
+ forwarding raw stored metadata handed the array straight through, and the bridge copied it
687
+ onto the `object-grid` node verbatim. ObjectGrid reads the object form and only that, and
688
+ `.formats` on an array is `undefined`, so the renderer's `['csv', 'json']` default won.
689
+
690
+ A view declaring `['csv', 'xlsx']` therefore rendered an export menu offering CSV and
691
+ JSON: the declared xlsx never appeared, an undeclared json did, and nothing said so — the
692
+ export button still showed, because a non-empty array is truthy. The bridge now applies
693
+ the spec's own transform at the assignment site, so both spellings leave it as one shape.
694
+
695
+ Deliberately narrow: this mirrors the spec's lift and nothing else. The object form passes
696
+ through by reference, unread and unrewritten; a view with no `exportOptions` is untouched;
697
+ and a `'pdf'` stored before its retirement is carried rather than filtered, because the
698
+ spec refuses that value at parse with a migration prescription instead of silently
699
+ dropping it — such a format still dies downstream in ObjectGrid's format-agnostic menu
700
+ filter (objectui#4535). The fix is at the producer for the same reason: a tolerant
701
+ `Array.isArray` fallback in the renderer would make a second de-facto contract out of one
702
+ spec key.
703
+
704
+ One behavior follows from reading the lift literally: `exportOptions: []` now lifts to
705
+ `{ formats: [] }` and the export button is hidden, where before the unreadable `[]` was
706
+ merely truthy and produced a menu built entirely from the `['csv', 'json']` default. A
707
+ view that declares zero formats now offers zero.
708
+
709
+ - 47f551b: fix(react): the spec bridge abstains on a non-string `rowHeight` instead of coercing it to a density, matching core
710
+
711
+ `mapDensity` opened with a truthiness guard, so any **truthy non-string** survived it and was
712
+ then coerced into a lookup key — both `Object.prototype.hasOwnProperty.call` and the table index
713
+ run `String(...)`. `rowHeight: ['compact']`, a boxed `String('compact')` or
714
+ `{ toString: () => 'compact' }` therefore each selected a real density, while
715
+ `@object-ui/core`'s `rowHeightToDensityMode` — which opens with `typeof rowHeight !== 'string'` —
716
+ abstained for the same input. Two published surfaces, two answers for one input.
717
+
718
+ The bridge now opens with core's type guard. An off-spec non-string `rowHeight` renders exactly
719
+ like an absent one, and the producer is where it gets fixed (AGENTS.md #0.1). Behaviour for the
720
+ five spec row heights and for off-spec **strings** is unchanged; `''` keeps its answer by a
721
+ different route (a string now, refused one line later because it is not one of the five keys).
722
+
723
+ Note the direction against the previous fix in this function: that leak returned a _function_,
724
+ visibly wrong to everything downstream. This one returned a legitimate-looking `'compact'` that
725
+ nothing downstream could tell apart from an authored density.
726
+
727
+ - Updated dependencies [0e67b53]
728
+ - Updated dependencies [ee66e2e]
729
+ - Updated dependencies [ee26e65]
730
+ - Updated dependencies [5900ac5]
731
+ - Updated dependencies [932cbcd]
732
+ - Updated dependencies [734d186]
733
+ - Updated dependencies [f650253]
734
+ - Updated dependencies [3d9769a]
735
+ - Updated dependencies [3fc2971]
736
+ - Updated dependencies [aca27fa]
737
+ - Updated dependencies [dde7283]
738
+ - Updated dependencies [f7c6430]
739
+ - Updated dependencies [ae10a01]
740
+ - Updated dependencies [92876f0]
741
+ - Updated dependencies [f279deb]
742
+ - Updated dependencies [eb7f586]
743
+ - Updated dependencies [e901131]
744
+ - Updated dependencies [d9d3463]
745
+ - Updated dependencies [2a40f69]
746
+ - Updated dependencies [537a0d1]
747
+ - Updated dependencies [bec3e14]
748
+ - Updated dependencies [613b167]
749
+ - Updated dependencies [1f9b905]
750
+ - Updated dependencies [828549a]
751
+ - Updated dependencies [e1ade8f]
752
+ - Updated dependencies [abb0f81]
753
+ - Updated dependencies [38ab505]
754
+ - Updated dependencies [3e19fe7]
755
+ - Updated dependencies [bb58d1d]
756
+ - Updated dependencies [5cc847c]
757
+ - Updated dependencies [fa21254]
758
+ - Updated dependencies [33c32bf]
759
+ - Updated dependencies [66fb4fa]
760
+ - Updated dependencies [6d641c9]
761
+ - Updated dependencies [479cc7b]
762
+ - Updated dependencies [7e4f0e5]
763
+ - Updated dependencies [45e1949]
764
+ - Updated dependencies [92250d6]
765
+ - Updated dependencies [c1d939f]
766
+ - Updated dependencies [58bebf6]
767
+ - Updated dependencies [405e808]
768
+ - Updated dependencies [49ae9f4]
769
+ - Updated dependencies [c0f9a4b]
770
+ - Updated dependencies [2459a3e]
771
+ - Updated dependencies [2776b11]
772
+ - Updated dependencies [ac853ce]
773
+ - Updated dependencies [fa51109]
774
+ - Updated dependencies [d6aa172]
775
+ - Updated dependencies [fe52a04]
776
+ - Updated dependencies [d46f9b8]
777
+ - Updated dependencies [605b747]
778
+ - Updated dependencies [2fea4d2]
779
+ - Updated dependencies [7f1cb33]
780
+ - Updated dependencies [bb68488]
781
+ - Updated dependencies [2e3b0c0]
782
+ - Updated dependencies [9461dd3]
783
+ - Updated dependencies [78fa331]
784
+ - Updated dependencies [31ab1ac]
785
+ - Updated dependencies [0082db8]
786
+ - Updated dependencies [b42558a]
787
+ - Updated dependencies [d2f6e6b]
788
+ - Updated dependencies [ab04728]
789
+ - Updated dependencies [85a3082]
790
+ - Updated dependencies [06915b0]
791
+ - Updated dependencies [ff84b05]
792
+ - @object-ui/i18n@17.5.0
793
+ - @object-ui/core@17.5.0
794
+ - @object-ui/data-objectstack@17.5.0
795
+ - @object-ui/types@17.5.0
796
+
797
+ ## 17.4.0
798
+
799
+ ### Minor Changes
800
+
801
+ - 5bfaabd: `PageComponentSchema.dataSource` now reaches every object-bound block, not just
802
+ `list-view` — and `element:record_picker` stops discarding `view`
803
+ (objectstack#6953).
804
+
805
+ objectstack#5576 wired the spec's per-element data binding
806
+ (`dataSource: { object, view?, filter?, sort?, limit? }`) to `list-view` and left
807
+ the same declaration inert on every other page component. Two gaps remained, and
808
+ both were silent:
809
+
810
+ - **`element:record_picker` read four of the five keys and dropped `view`.** So
811
+ `dataSource: { object: 'account', view: 'hot' }` — the spec's own example —
812
+ built a picker over EVERY account instead of the rows the saved view selects.
813
+ Nothing threw and nothing rendered an error; the option list was simply wider
814
+ than what was authored, which also means a user could select a record the page
815
+ said was out of scope.
816
+ - **`object-grid` / `object-form` / `object-kanban` / `object-calendar` /
817
+ `object-chart` / `object-metric` / `record:related_list` read none of it.**
818
+ Each gates its fetch on its own `objectName`, and nothing mapped
819
+ `dataSource.object` onto it, so a page written the way the spec documents
820
+ rendered an empty grid / a field-less form / a board with no cards / an empty
821
+ month / an empty chart / a static metric number — with no request and no
822
+ diagnostic anywhere. Spec-valid metadata rendering nothing is the
823
+ objectstack#4413 shape.
824
+
825
+ Composition follows objectstack#5576's landed semantics unchanged on every block:
826
+ a named saved view supplies the baseline, a key written on the component itself
827
+ overrides it, an explicit binding key overrides both, `filter` AND-combines
828
+ ("additional filter criteria" — a binding can narrow a view, never widen it), and
829
+ a `view` name that does not resolve renders a configuration error instead of
830
+ degrading to the object's full scope.
831
+
832
+ - `@object-ui/react` — new `useElementDataSourceSchema(schema, mapping, dataSource?)`
833
+ and `ElementDataSourceGate` apply a resolved binding to the schema keys a given
834
+ block reads, plus `ElementDataSourceErrorPanel` / `ElementDataSourceLoadingPanel`
835
+ for the two non-final states. One precedence table for all blocks rather than
836
+ one copy per block — that copy is how "additional filter criteria" would have
837
+ become two dialects.
838
+ - A mapping names **only** keys its block genuinely reads. A composed value
839
+ written onto a key the block ignores would be accepted and dropped, which is
840
+ the defect being removed, one layer deeper — so a kanban's swimlane `columns`
841
+ never receive a view's field list, and a block with no row cap leaves `limit`
842
+ unmapped. The per-block coverage table, including two residual gaps that are
843
+ named rather than papered over, is in `content/docs/guide/data-source.md`.
844
+
845
+ No behaviour changes for a block that carries no `dataSource`: the binding-free
846
+ path returns the schema by reference, so nothing remounts and nothing refetches.
847
+
848
+ - 48132f7: Track the `@objectstack` family at `17.0.0-rc.5` (objectui#3560).
849
+
850
+ The pin moves from `^17.0.0-rc.2` to `^17.0.0-rc.5` across all 37 declarations in
851
+ 30 `package.json` files, and the sibling `@objectstack/*` packages (`client` /
852
+ `formula` / `lint`) move with it — they pin `@objectstack/spec` **exactly**, so
853
+ leaving them behind would keep a second copy of the spec in the tree and have
854
+ `@objectstack/lint` validating against schemas that still accept the keys rc.3–rc.5
855
+ retire. `pnpm-lock.yaml` now resolves one copy of each of the six family packages
856
+ (`spec` / `client` / `core` / `formula` / `lint` / `sdui-parser`), all at rc.5.
857
+
858
+ Bumping the pin and repairing the fallout cannot be split: the pin alone reddens
859
+ CI, and the code alone targets a shape that is not in effect yet.
860
+
861
+ ## A live bug this upgrade fixes
862
+
863
+ **`ObjectStackDataSource.delete()` never emitted its mutation event, and resolved
864
+ `undefined` instead of a boolean.** `@objectstack/client`'s `DeleteDataResult`
865
+ declared a key called `deleted` — a key no schema has ever declared and no server
866
+ path has ever returned on `DELETE /data/:object/:id`. So `result.deleted`
867
+ compiled and read `undefined` at runtime: the guard never fired, a successful
868
+ delete notified no subscriber, and every consumer's cache stayed stale.
869
+ objectstack#5638 corrected the interface to the schema's `success`; following the
870
+ rename is what restores both behaviours. Nothing in this repo had to change shape
871
+ for it — the code was already asking the right question of the wrong key.
872
+
873
+ ## Breaking, in FROM → TO form
874
+
875
+ - **The five `@objectstack/spec/ui` interaction-config modules are gone** —
876
+ touch / dnd / keyboard / animation / offline, 32 defs and 64 exports
877
+ (objectstack#4988, PR objectstack#5321). None of them had an authoring door: no
878
+ metadata document could ever carry one of these blocks, so a stack that parsed
879
+ before the retirement parses byte-for-byte the same after it. `@object-ui/types`
880
+ drops the 32 `export type` re-exports. The vocabulary each one's only real
881
+ consumer needs is now declared by that consumer, which is the remedy the spec's
882
+ own retirement ledger prescribes ("declare that union locally — it is your
883
+ client's policy, not the platform's"):
884
+
885
+ - `@object-ui/react`'s `useOffline` owns `OfflineStrategy`, `ConflictResolution`,
886
+ `PersistStorageType`, `EvictionPolicyType`, `OfflineConfig`,
887
+ `OfflineCacheConfig`, `OfflineSyncConfig`;
888
+ - `@object-ui/core`'s `DndProtocol` / `KeyboardProtocol` own `DndConfig`,
889
+ `DragItem`, `DropZone`, `DragConstraint`, `DragHandle`, `DropEffect`,
890
+ `KeyboardNavigationConfig`, `KeyboardShortcut`, `FocusManagement`,
891
+ `FocusTrapConfig`;
892
+ - `@object-ui/types`' `mobile` module owns `SpecGestureConfig`,
893
+ `SwipeGestureConfig`, `PinchGestureConfig`, `LongPressGestureConfig`,
894
+ `TouchTargetConfig`, `TouchInteraction` (plus a new `SPEC_GESTURE_TYPES`
895
+ runtime tuple), so `@object-ui/mobile`'s import paths are unchanged.
896
+
897
+ Every shape is moved verbatim — same keys, same members, same optionality — so
898
+ no hook or bridge changes behaviour. Consumers importing these names from
899
+ `@object-ui/types` must import them from the owning package instead. Note the
900
+ spec's _surviving_ `ConnectorConflictResolution` (`/integration`, connector sync)
901
+ and `ConflictResolutionStrategy` (`/api`, route merge policy) are **different
902
+ concepts** — do not re-point at them.
903
+
904
+ - **`@object-ui/types` no longer re-exports `NotificationAction` or `EmbedConfig`**
905
+ (objectstack#5015, PR objectstack#5300). Both were published `ui` vocabulary with
906
+ no authoring door; no notification action was ever parsed from metadata and no
907
+ iframe route ever read an embed config. The presentation enums
908
+ (`NotificationType` / `NotificationSeverity` / `NotificationPosition`) and
909
+ `SharingConfig` **survive** and are untouched — public form sharing still gates
910
+ the anonymous endpoints on `allowAnonymous` + `publicLink`.
911
+ `@object-ui/core`'s `SharingProtocol` keeps `resolveEmbedConfig` /
912
+ `generateEmbedCode` against a locally declared `EmbedConfig`, so its surface is
913
+ unchanged.
914
+ - **`ThemeEngine` stops emitting nine retired CSS variable groups**
915
+ (objectstack#5021 option 2, PR objectstack#5289). `theme.animation`,
916
+ `theme.zIndex` and five typography groups (`fontSize` / `fontWeight` /
917
+ `lineHeight` / `letterSpacing`, plus `fontFamily.heading` / `fontFamily.mono`)
918
+ are tombstones the schema now rejects by name, so `--duration-*`, `--timing-*`,
919
+ `--z-*`, `--font-size-*`, `--font-weight-*`, `--line-height-*`,
920
+ `--letter-spacing-*`, `--font-heading` and `--font-mono` had become structurally
921
+ dead code — no author could produce the input that reached them.
922
+ `generateAnimationVars` and `generateZIndexVars` are removed from
923
+ `@object-ui/core`, and `@object-ui/types` drops `Animation` / `ZIndex` /
924
+ `AnimationSchema` / `ZIndexSchema`. **`theme.customVars` is the declared — and
925
+ since #5021 the only — door**: each entry is emitted verbatim as
926
+ `--<key>: <value>`, so a `--z-modal` or a `--duration-fast` goes there now.
927
+ LIVE emission is untouched byte for byte: `colors`, `borderRadius`, `shadows`,
928
+ `typography.fontFamily.base` (→ `--font-sans`) and `customVars`.
929
+ - **`@object-ui/types`' `HttpMethodSchema` now binds the spec's
930
+ `HttpMethodSubsetSchema`, and `HttpMethod` binds `HttpMethodSubset`**
931
+ (objectstack#5832, PR objectstack#5976 — objectui#3499). The spec renamed its
932
+ 5-value UI subset because `schemaNameFromExportKey` strips the `Schema` suffix,
933
+ so the 5-value and 7-value enums both published as `shared/HttpMethod` and the
934
+ later write won — the emitted JSON Schema and reference page described only one
935
+ of them. **The runtime domain is unchanged and this repo's exported names are
936
+ unchanged**; this follows the rename without touching cross-package semantics.
937
+ Deliberately NOT re-pointed at the spec's bare `HttpMethod`: that is the 7-value
938
+ enum, and widening to it would let `method: 'HEAD'` compile and then throw in
939
+ `HttpRequestSchema.parse()`.
940
+ - **`dashboard.widgets[].actionUrl` / `actionType` / `actionIcon` / `aria` are
941
+ refused, not stripped** (objectstack#5010, ADR-0049 enforce-or-remove). A
942
+ dashboard widget has no action button and never had one — every action the
943
+ dashboard dispatches comes from `header.actions[]` — and no renderer ever applied
944
+ the widget `aria`, so it promised accessibility compliance it did not deliver.
945
+ A stale dashboard now gets a named error telling it where the affordance moved,
946
+ instead of silently losing it. Run `os migrate meta --from 16` to rewrite.
947
+
948
+ ### Patch Changes
949
+
950
+ - 8aad9fd: Action-face predicates written against the canonical `record.` root now evaluate
951
+
952
+ `action:button`, `action:icon`, `action:menu` and `action:group` gated their
953
+ actions on `useCondition(pred, context)`, which evaluates on
954
+ `new ExpressionEvaluator({ ...scope, ...context })` — and the context each of
955
+ them passed was the row spread flat, or nothing at all. Only the shorthand
956
+ spelling resolved:
957
+
958
+ | predicate | verdict, before |
959
+ | ---------------------------- | -------------------------------- |
960
+ | `status == "pending"` | evaluates (`action:button` only) |
961
+ | `record.status == "pending"` | throws `record is not defined` |
962
+ | `data.status == "pending"` | throws `data is not defined` |
963
+
964
+ `record.` is not a mistaken spelling — it is the canonical one. It is what
965
+ `ExpressionEvaluator`'s CEL path binds (`bag.record` as the record namespace),
966
+ what `evalRowPredicate` binds on the record header, list rows, the row kebab
967
+ and conditional formatting (`record.status` / bare `status` / `data.status`),
968
+ and what the server enforces with. A `visible` that fails CLOSED turns the throw
969
+ into "hidden", so a correctly-authored predicate deleted its own button —
970
+ indistinguishable from the gate having said no. On the fail-soft legs the same
971
+ throw lands the other way: `disabled` greyed a control out for everyone.
972
+
973
+ Live rather than theoretical: every declared action on framework's
974
+ `sys_approval_request` gates on `record.viewer.*`, so the whole server-declared
975
+ approval decision set was invisible wherever the declared-action bar rendered
976
+ until objectui#4077 fixed that bar. These four generic renderers carried the
977
+ same binding.
978
+
979
+ What changed:
980
+
981
+ - all four bind the row the three canonical ways, through one named helper
982
+ (`usePredicateRecordContext`, exported from `@object-ui/react` beside
983
+ `useCondition`), so the action face and the row surfaces answer an author's
984
+ `visible:` the same way;
985
+ - `action:icon` reads the row at all. It evaluated against an empty bag, so not
986
+ even the bare-field shorthand resolved — and its `data` prop was landing in
987
+ the props spread onto the DOM button;
988
+ - `action:menu`'s items and `action:group`'s two leaves receive the row from
989
+ their host, which they previously never got;
990
+ - `action:bar` forwards the row into the overflow menu it builds, not just to
991
+ its inline members. An action's predicate had been answering a different
992
+ question purely because it spilled past `maxVisible` — which on mobile
993
+ defaults to 1, making the verdict a function of the viewport.
994
+
995
+ Deliberately unchanged: the evaluation entry and each site's error policy. A
996
+ predicate that genuinely faults still fails closed on `action:button` /
997
+ `action:menu` `visible` and still fails soft on the other legs, exactly as
998
+ before; `toPredicateInput`, `hasDeclaredVisibilityGate` and the empty-predicate
999
+ rules keep their pinned semantics. Binding the row is a separate question from
1000
+ what to do when the predicate faults.
1001
+
1002
+ A surface with no row of its own binds nothing rather than an empty record, so
1003
+ a host that supplies the row through the ambient predicate scope is not blanked
1004
+ out; a row passed explicitly still wins over the scope.
1005
+
1006
+ - e06810e: `PageComponentSchema.dataSource` is now consumed instead of discarded — a
1007
+ `list-view` page component can reference a **saved view by name** for the first
1008
+ time, and writing the binding no longer breaks the component
1009
+ (objectstack#5576).
1010
+
1011
+ The spec declares a per-element data binding on every page component —
1012
+ `dataSource: { object, view?, filter?, sort?, limit? }` — and objectui read none
1013
+ of it. `ViewDataProvider.resolveElementDataSource` forwarded
1014
+ `filter`/`sort`/`limit` and dropped `view` entirely, and had no caller outside its
1015
+ own test; nothing mapped `object` onto the `objectName` a list actually reads. So
1016
+ "reference a saved view by name" was published, validated and inert, and every
1017
+ page that wanted a saved view's columns/filter/sort had to inline a second copy of
1018
+ them — the drift the binding exists to remove.
1019
+
1020
+ Writing the binding also **broke** the block, for a reason unrelated to `view`:
1021
+ `SchemaRenderer` spread the schema's `dataSource` metadata onto the component as a
1022
+ React prop, and that is the prop name the host uses to inject the data-source
1023
+ ADAPTER. The plain `{ object, view }` object shadowed the adapter, so the first
1024
+ `dataSource.find(…)` threw `dataSource.find is not a function` and `list-view`
1025
+ rendered "Couldn't load records" — a spec-compliant component failing next to
1026
+ identical ones that omitted the binding.
1027
+
1028
+ - `@object-ui/react` — `SchemaRenderer` no longer spreads `schema.dataSource` as a
1029
+ prop (it is metadata, like `visibleWhen`); renderers read it off `schema`. An
1030
+ explicit React `dataSource` prop is unaffected. New
1031
+ `useElementDataSource(schema, dataSource?)` hook resolves a binding, fetching
1032
+ the named saved view from the object definition's `listViews` and the metadata
1033
+ overlay's `listViews()`.
1034
+ - `@object-ui/core` — new `isElementDataSourceConfig` / `collectSavedViews` /
1035
+ `resolveSavedView` / `composeElementDataSource`, and `resolveElementDataSource`
1036
+ now honours `view` through an optional `DataFetcher.fetchViews`, reporting an
1037
+ unresolvable view as an error instead of silently returning every record.
1038
+ `resolveViewId` moved here from `@object-ui/app-shell` (re-exported there) so
1039
+ one matcher serves both the object page and a page component.
1040
+ - `@object-ui/plugin-list` — `list-view` maps the binding onto the props
1041
+ `ListView` reads. `dataSource.*` keys are authoritative, view-supplied values
1042
+ are a baseline the component's own keys override, and `filter` AND-combines at
1043
+ every level (the spec calls the binding's filter "additional criteria"), so a
1044
+ binding can narrow a saved view but never widen it. A `view` name that does not
1045
+ resolve renders a configuration error naming the object's actual views and
1046
+ issues no query — it never falls back to the object's default view, because that
1047
+ turns a typo into a silently wider answer.
1048
+
1049
+ - ab3ad4f: An empty predicate is no longer a declared gate anywhere (objectui#3850, objectui#3862)
1050
+
1051
+ "Is a gate DECLARED on this key — is there a condition to reach a verdict on?" was
1052
+ answered three times in this repo, with three different scopes, and the widest
1053
+ answers sat on `disabled`, where the mistake is not benign:
1054
+
1055
+ - `hasDeclaredVisibilityGate` (the action face) asked `!= null && !== ''`, so every
1056
+ OBJECT counted — including `{ dialect: 'cel', source: '' }`. That envelope is not
1057
+ a hand-written curiosity: `@objectstack/spec`'s `ExpressionInputSchema` normalizes
1058
+ every authored predicate into one, so "the author left the predicate empty"
1059
+ compiles to exactly it. The verdict path normalized the same value back to
1060
+ `undefined`, and `evaluateCondition(undefined)` answers `true` — "no condition, so
1061
+ visible/enabled". On `visible` that `true` means SHOW, so the two mistakes
1062
+ cancelled; on `disabled` it means GREY, so they compounded: a button disabled
1063
+ forever that no author asked to disable (objectui#3850, the residue objectui#3842
1064
+ left behind).
1065
+ - `SchemaRenderer` asked `disabled !== undefined` inline, one notch wider again, so
1066
+ `disabled: null` greyed out too — on the GENERIC rendering path, since that block
1067
+ runs for every node type, and not as an internal flag either: `_disabled` is
1068
+ forwarded to the component as a real `disabled` prop (objectui#3862).
1069
+ - `ActionRunner`'s execution gates asked "does this normalize to something
1070
+ evaluable?" — the scope that turned out to be right (objectui#3848 / objectui#3872).
1071
+
1072
+ There is now ONE definition, `hasDeclaredPredicate`, exported from
1073
+ `@object-ui/core` (`evaluator/declaredPredicate.ts`, beside the `toPredicateInput`
1074
+ normalizer it is derived from): a gate is declared when normalization still leaves a
1075
+ condition to evaluate. `''`, a whitespace-only string, an empty-`source` envelope
1076
+ and any non-predicate value (`0`, `{}`) are NOT declared; `false` IS (a verdict is
1077
+ not a missing gate — objectui#3812). `hasDeclaredVisibilityGate` keeps its name as a
1078
+ re-export of it, so the five member-action renderer call sites, `DeclaredActionsBar`
1079
+ and `record-quick-actions` are unchanged and inherit the scope;
1080
+ `SchemaRenderer`'s `disabled` / `disabledOn` chain and `ActionRunner`'s two gates
1081
+ read the same function. No consumer got a local "and also check for empty" test —
1082
+ that fourth dialect is what objectui#3842 / objectui#3849 spent two PRs merging away.
1083
+
1084
+ Measured behaviour change, `action:button` and the generic path, before → after:
1085
+
1086
+ | value | `visible` | `disabled` | `enabled` | `SchemaRenderer` `disabled` prop |
1087
+ | --------------------------------------------------------- | -------------- | ---------- | --------- | -------------------------------- |
1088
+ | `''` | shown → shown | on → on | on → on | forwarded → absent |
1089
+ | `null` | shown → shown | on → on | on → on | forwarded → absent |
1090
+ | `{ dialect: 'cel', source: '' }` | shown → shown | GREY → on | on → on | forwarded → absent |
1091
+ | `{ source: '' }` | shown → shown | GREY → on | on → on | forwarded → absent |
1092
+ | `' '` (whitespace) | HIDDEN → shown | on → on | GREY → on | forwarded → absent |
1093
+ | `0` / `{}` (not predicates) | shown → shown | GREY → on | on → on | forwarded → absent |
1094
+ | `true` / `false` / bare CEL / `${…}` / non-empty envelope | unchanged | unchanged | unchanged | unchanged |
1095
+
1096
+ Every row moves toward "there is no gate here", never away from it, and no value
1097
+ that HAS a verdict changes it — the verdict is still read from the raw value, only
1098
+ the gate in front of it narrowed. Two rows are behaviour changes rather than the
1099
+ equivalence the ruling expected, and are pinned as such: the whitespace string moves
1100
+ on `visible` / `enabled` (it used to normalize to `'${ }'`, which evaluates falsy,
1101
+ so a predicate that says nothing HID the action from everyone), and non-predicate
1102
+ junk stops greying controls out (fail-open, the posture `ActionRunner` already
1103
+ committed to).
1104
+
1105
+ One blank spelling is knowingly still outside the scope: an envelope whose `source`
1106
+ is blank but not EMPTY (`{ dialect: 'cel', source: ' ' }`) — the normalizer folds a
1107
+ `source` of `''` and does not trim, so the string spelling of a blank predicate is
1108
+ trimmed and the envelope spelling is not, and `disabled` still greys out for that one
1109
+ value. The ruling enumerated three empty spellings; this is a fourth, measured and
1110
+ filed as objectui#3960 rather than widened in here.
1111
+
1112
+ One chain is deliberately untouched: `SchemaRenderer`'s `visible` / `visibleWhen` /
1113
+ `visibleOn` / `visibility` / `hidden` / `hiddenOn` legs keep `!== undefined`, because
1114
+ narrowing them would change ALIAS PRECEDENCE, not just emptiness. The `hidden` legs
1115
+ are not negated and therefore carry this same defect with the polarity that makes the
1116
+ node vanish — measured, out of this ruling's scope, filed as objectui#3955.
1117
+
1118
+ - 0109f54: Blank predicates and non-predicate values are no longer gates, at the last three entries that still judged them (objectui#3955, objectui#3957, objectui#3960)
1119
+
1120
+ objectui#3850 sank "is a predicate gate DECLARED here?" into one definition,
1121
+ `@object-ui/core`'s `hasDeclaredPredicate`. Three places were left out of that
1122
+ ruling's placement clause, each with the same shape of defect: the evaluator's
1123
+ single default for "there is nothing here to evaluate" is `true`, meaning
1124
+ _visible/enabled_, and wherever a too-wide "declared" test hands it an empty
1125
+ predicate on an INVERTED key, that `true` turns a control off for a value the
1126
+ metadata never used to say anything.
1127
+
1128
+ **`SchemaRenderer`'s `hidden` / `hiddenOn` legs (objectui#3955)** asked
1129
+ `!== undefined` and did NOT negate the verdict, so an empty predicate meant HIDE
1130
+ and the node disappeared — on the generic rendering path, since that block runs
1131
+ for every schema type. Harder to diagnose than the `disabled` twin objectui#3862
1132
+ fixed: a greyed-out control is still on screen, while a node that never rendered
1133
+ is indistinguishable from metadata that meant to hide it. Both legs now read the
1134
+ shared definition.
1135
+
1136
+ **The "blank" criterion now covers the envelope spelling (objectui#3960).** The
1137
+ definition trimmed a whitespace-only STRING and not an envelope's whitespace-only
1138
+ `source`, because `toPredicateInput` folds a `source` of `''` and does not trim.
1139
+ So `{ dialect: 'cel', source: ' ' }` was a declared gate whose verdict came from
1140
+ core's own CEL entry calling that exact value "no predicate" (`if (!source.trim())
1141
+ return true`) — `disabled` greyed out forever and `ActionRunner.execute` answered
1142
+ `{ success: false, error: 'Action is disabled' }` with the handler never invoked.
1143
+ Blankness is now decided once for both spellings, at the definition. The
1144
+ NORMALIZER's contract is deliberately unchanged: "what shape does the evaluator
1145
+ accept" is not the same question as "is there a condition", and moving the trim
1146
+ there would have flipped verdicts for every
1147
+ `useCondition(toPredicateInput(…))` call site, including container-level `visible`
1148
+ reads that never asked this question at all.
1149
+
1150
+ **`ActionEngine.getActionsForLocation`'s `visible` filter (objectui#3957)** was the
1151
+ last consumer answering the question with a range of its own — three empty
1152
+ spellings folded by hand, everything else coerced with `Boolean(raw)`. It now reads
1153
+ the shared definition and the coercion branch is gone, so one value no longer gets
1154
+ two answers depending on whether an action was surfaced by the engine or rendered
1155
+ standalone (the invariant objectui#3314 established). Its fail-CLOSED posture on a
1156
+ predicate that THROWS is untouched (`throwOnError: true` + `warnHiddenPredicate`):
1157
+ "the predicate faulted" and "there is no predicate" are different facts.
1158
+
1159
+ Behaviour changes, before → after. Observation-class: each needs an author to write
1160
+ an empty/blank predicate or a non-predicate value, and there is no known user path
1161
+ today.
1162
+
1163
+ | value | `ActionEngine` `visible` | `SchemaRenderer` `hidden` | `disabled` (action face + generic path) | `ActionRunner.execute` `disabled` |
1164
+ | ----------------------------------------------------------- | ------------------------ | ------------------------- | --------------------------------------- | --------------------------------- |
1165
+ | `''` / `null` | shown → shown | HIDDEN → rendered | unchanged | unchanged |
1166
+ | `' '` (blank text) | HIDDEN → shown | HIDDEN → rendered | unchanged | unchanged |
1167
+ | `0` / `NaN` | HIDDEN → shown | HIDDEN → rendered | unchanged | unchanged |
1168
+ | `{}` / `[]` | shown → shown | HIDDEN → rendered | unchanged | unchanged |
1169
+ | `{ dialect: 'cel', source: '' }` | shown → shown | HIDDEN → rendered | unchanged | unchanged |
1170
+ | `{ dialect: 'cel', source: ' ' }` | shown → shown | HIDDEN → rendered | GREY → on | refused → runs |
1171
+ | `{ source: ' ' }` (no dialect) | HIDDEN → shown | HIDDEN → rendered | GREY → on | refused → runs |
1172
+ | `true` / `false` / bare CEL / `${…}` / a non-blank envelope | unchanged | unchanged | unchanged | unchanged |
1173
+
1174
+ Every row moves toward "there is no gate here", never away from it, and no value
1175
+ that HAS a verdict changes it — a declared `false` is still a verdict, not a
1176
+ missing gate (objectui#3812), and blankness is `trim()`, not "short": `{ dialect:
1177
+ 'cel', source: ' x ' }` is a predicate. One alias precedence changes with the
1178
+ `hidden` legs and is pinned rather than claimed as an equivalence: an undeclared
1179
+ `hidden` no longer short-circuits the chain, so a declared `hiddenOn` is finally
1180
+ consulted.
1181
+
1182
+ `SchemaRenderer`'s four `visible*` legs keep `!== undefined` deliberately, as
1183
+ objectui#3850 ruled: their `true` is negated, so an empty predicate already lands
1184
+ on "shown", and narrowing them would change alias precedence rather than fix
1185
+ anything.
1186
+
1187
+ - Updated dependencies [6719877]
1188
+ - Updated dependencies [56ff091]
1189
+ - Updated dependencies [7864f03]
1190
+ - Updated dependencies [d229dfa]
1191
+ - Updated dependencies [4bc6c23]
1192
+ - Updated dependencies [c3b01a7]
1193
+ - Updated dependencies [f5f8744]
1194
+ - Updated dependencies [3765678]
1195
+ - Updated dependencies [d83f6b3]
1196
+ - Updated dependencies [5f08c05]
1197
+ - Updated dependencies [69becd2]
1198
+ - Updated dependencies [5e52495]
1199
+ - Updated dependencies [b750823]
1200
+ - Updated dependencies [e06810e]
1201
+ - Updated dependencies [ab3ad4f]
1202
+ - Updated dependencies [c2fd122]
1203
+ - Updated dependencies [ac2139c]
1204
+ - Updated dependencies [b14ab3a]
1205
+ - Updated dependencies [8c60819]
1206
+ - Updated dependencies [41d6022]
1207
+ - Updated dependencies [e64a52e]
1208
+ - Updated dependencies [844d17f]
1209
+ - Updated dependencies [48132f7]
1210
+ - Updated dependencies [4dcd52a]
1211
+ - Updated dependencies [42ae5c6]
1212
+ - Updated dependencies [1d723e3]
1213
+ - Updated dependencies [0109f54]
1214
+ - Updated dependencies [7e5bb5d]
1215
+ - Updated dependencies [fbc23e0]
1216
+ - Updated dependencies [6d762da]
1217
+ - Updated dependencies [e6fdbdc]
1218
+ - Updated dependencies [f9faa7d]
1219
+ - Updated dependencies [6bb454a]
1220
+ - Updated dependencies [523be48]
1221
+ - Updated dependencies [7e2b7e9]
1222
+ - Updated dependencies [33526fd]
1223
+ - Updated dependencies [32413ec]
1224
+ - Updated dependencies [c1e1e6b]
1225
+ - @object-ui/core@17.4.0
1226
+ - @object-ui/i18n@17.4.0
1227
+ - @object-ui/types@17.4.0
1228
+ - @object-ui/data-objectstack@17.4.0
1229
+
3
1230
  ## 17.3.0
4
1231
 
5
1232
  ### Patch Changes
@@ -1555,6 +2782,7 @@
1555
2782
  result and the "warn-once" tracking are now stored separately: a
1556
2783
  `WeakMap` caches the validation outcome (so the visual flag is stable),
1557
2784
  while a `WeakSet` continues to dedupe `console.warn` output.
2785
+
1558
2786
  - @object-ui/types@6.2.2
1559
2787
  - @object-ui/core@6.2.2
1560
2788
  - @object-ui/i18n@6.2.2
@@ -1704,6 +2932,7 @@
1704
2932
  ### Minor Changes
1705
2933
 
1706
2934
  - b2d1704: feat(cmdk): record search across objects in the Command Palette
2935
+
1707
2936
  - New `useRecordSearch` hook in `@object-ui/react` debounces a query, fans out
1708
2937
  to `dataSource.find(name, { $search, $top })` across candidate objects, and
1709
2938
  aggregates hits. Race-safe via a monotonic runId; per-object 404s are
@@ -1718,6 +2947,7 @@
1718
2947
 
1719
2948
  Three related fixes that all addressed the same UX: a user follows a URL
1720
2949
  shaped `/{object}/{recordId}` and sees a completely blank content area.
2950
+
1721
2951
  1. **`useNavigationOverlay` produced the broken URL itself.** When
1722
2952
  middle-click / Cmd-click opened a gallery card in a new tab and no
1723
2953
  `onNavigate` was provided, the hook built `/{object}/{id}` — a URL
@@ -1737,6 +2967,7 @@
1737
2967
 
1738
2968
  - aa063db: `useRecordSearch` now orders hits by relevance instead of object-fanout
1739
2969
  order. Tiers (higher wins):
2970
+
1740
2971
  - 110: exact recordId paste
1741
2972
  - 100: display exactly equals the query
1742
2973
  - 80: display starts with the query
@@ -1796,17 +3027,20 @@
1796
3027
  - bd8447d: Three platform-wide detail polish items.
1797
3028
 
1798
3029
  **Tighter page rhythm**
3030
+
1799
3031
  - Outer `PageRenderer` padding `p-4 md:p-6 lg:p-8` → `p-3 md:p-4 lg:p-6`
1800
3032
  and outer body wrap `space-y-8` → `space-y-6` so list / detail / home
1801
3033
  pages share the same edge rhythm. Cuts ~16px of edge slack on lg.
1802
3034
 
1803
3035
  **Highlights KPI treatment**
3036
+
1804
3037
  - `HeaderHighlight` now renders numeric / currency / percent / decimal
1805
3038
  values as KPI numbers (`text-xl md:text-2xl font-semibold tabular-nums`)
1806
3039
  instead of the uniform `text-sm font-semibold`, so amount / probability
1807
3040
  / count fields read as headline stats — Salesforce-style key facts.
1808
3041
 
1809
3042
  **Discussion footer upgrade**
3043
+
1810
3044
  - `RecordActivityTimeline` now uses `RichTextCommentInput` (bold / italic /
1811
3045
  list / code, `@`-mention autocomplete, preview toggle, Send) instead of
1812
3046
  a bare `<textarea>`.
@@ -1817,6 +3051,7 @@
1817
3051
  and sidebar positions.
1818
3052
 
1819
3053
  - d51a577: feat(platform): Discussion attachments + @mention directory + Reference Rail aside
3054
+
1820
3055
  - **Discussion attachments** — `RichTextCommentInput` now accepts an `extraSlot`
1821
3056
  and a `canSubmitEmpty` flag so hosts can mount the existing
1822
3057
  `CommentAttachment` composer beneath the editor without forking the toolbar.
@@ -1906,6 +3141,7 @@
1906
3141
  page so the two renderers share state.
1907
3142
 
1908
3143
  - 74962b0: feat(detail): record:discussion schema component + flush accordion variant
3144
+
1909
3145
  - New `record:discussion` schema type lets authors place the record
1910
3146
  chatter feed anywhere in a custom Page schema. Wired through a
1911
3147
  shared `DiscussionContext` provider on the `assignedPage` branch
@@ -1926,6 +3162,7 @@
1926
3162
  customize the header or one tab.
1927
3163
 
1928
3164
  **Slot menu (v1):**
3165
+
1929
3166
  - `header` — replaces `page:header`
1930
3167
  - `actions` — replaces the `record:quick_actions` action bar
1931
3168
  - `highlights` — replaces the chips + chevron path strip
@@ -1953,6 +3190,7 @@
1953
3190
  ```
1954
3191
 
1955
3192
  **API changes:**
3193
+
1956
3194
  - `PageSchema` (in `@object-ui/types`): adds `kind?: 'full' | 'slotted'`
1957
3195
  (default `'full'`) and `slots?: PageSlotMap`.
1958
3196
  - `usePageAssignment` (in `@object-ui/react`): result now exposes a
@@ -2132,6 +3370,7 @@
2132
3370
  detail view (`TypeError: titleFormat.replace is not a function`) and printed
2133
3371
  `Failed to evaluate expression: ${[object Object]}` for every action visibility
2134
3372
  predicate.
3373
+
2135
3374
  - `@object-ui/core`: `ExpressionEvaluator.evaluate` / `evaluateCondition` now
2136
3375
  unwrap Expression envelopes transparently.
2137
3376
  - `@object-ui/react`: new `toPredicateInput()` helper to safely normalize
@@ -2383,6 +3622,7 @@
2383
3622
  - New plugin-object and ObjectQL SDK updates
2384
3623
 
2385
3624
  **Added:**
3625
+
2386
3626
  - New Plugin: @object-ui/plugin-object - ObjectQL plugin for automatic table and form generation
2387
3627
  - ObjectTable: Auto-generates tables from ObjectQL object schemas
2388
3628
  - ObjectForm: Auto-generates forms from ObjectQL object schemas with create/edit/view modes
@@ -2391,6 +3631,7 @@
2391
3631
  - ObjectQL Integration: Enhanced ObjectQLDataSource with getObjectSchema() method using MetadataApiClient
2392
3632
 
2393
3633
  **Changed:**
3634
+
2394
3635
  - Updated @objectql/sdk from ^1.8.3 to ^1.9.1
2395
3636
  - Updated @objectql/types from ^1.8.3 to ^1.9.1
2396
3637
 
@@ -2404,6 +3645,7 @@
2404
3645
  - Patch release: Add automated changeset workflow and CI/CD improvements
2405
3646
 
2406
3647
  This release includes infrastructure improvements:
3648
+
2407
3649
  - Added changeset-based version management
2408
3650
  - Enhanced CI/CD workflows with GitHub Actions
2409
3651
  - Improved documentation for contributing and releasing