@object-ui/react 17.6.0 → 17.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +3766 -0
  2. package/README.md +294 -38
  3. package/dist/SchemaRenderer.d.ts +38 -7
  4. package/dist/SchemaRenderer.js +1218 -87
  5. package/dist/context/ActionContext.js +40 -1
  6. package/dist/context/DiscussionContext.d.ts +12 -0
  7. package/dist/context/DiscussionContext.js +2 -0
  8. package/dist/context/DrillNavigationContext.d.ts +13 -0
  9. package/dist/context/InlineEditContext.d.ts +81 -1
  10. package/dist/context/InlineEditContext.js +8 -2
  11. package/dist/context/RecordContext.d.ts +48 -5
  12. package/dist/context/RecordContext.js +10 -1
  13. package/dist/context/RelatedRecordActionsContext.d.ts +5 -1
  14. package/dist/context/SchemaRendererContext.d.ts +54 -2
  15. package/dist/context/SchemaRendererContext.js +38 -5
  16. package/dist/context/ThemeContext.js +1 -1
  17. package/dist/context/actionRunnerTranslator.d.ts +27 -0
  18. package/dist/context/actionRunnerTranslator.js +37 -0
  19. package/dist/element-data-source/ElementDataSourceGate.d.ts +88 -1
  20. package/dist/element-data-source/ElementDataSourceGate.js +326 -13
  21. package/dist/hooks/index.d.ts +3 -1
  22. package/dist/hooks/index.js +10 -1
  23. package/dist/hooks/useActionEngine.js +46 -3
  24. package/dist/hooks/useActionRunner.js +4 -0
  25. package/dist/hooks/useActionTextLocalizer.d.ts +21 -8
  26. package/dist/hooks/useActionTextLocalizer.js +33 -8
  27. package/dist/hooks/useConfigBagEvaluator.d.ts +30 -0
  28. package/dist/hooks/useConfigBagEvaluator.js +39 -0
  29. package/dist/hooks/useDensityMode.d.ts +6 -2
  30. package/dist/hooks/useDensityMode.js +6 -2
  31. package/dist/hooks/useDiscovery.d.ts +1 -10
  32. package/dist/hooks/useETagCache.js +25 -4
  33. package/dist/hooks/useElementDataSource.d.ts +15 -2
  34. package/dist/hooks/useElementDataSource.js +24 -7
  35. package/dist/hooks/useExpression.d.ts +52 -59
  36. package/dist/hooks/useExpression.js +58 -90
  37. package/dist/hooks/useFilterScope.d.ts +22 -2
  38. package/dist/hooks/useFilterScope.js +29 -4
  39. package/dist/hooks/useGlobalUndo.d.ts +10 -0
  40. package/dist/hooks/useGlobalUndo.js +112 -46
  41. package/dist/hooks/useNavigationOverlay.d.ts +76 -11
  42. package/dist/hooks/useNavigationOverlay.js +119 -9
  43. package/dist/hooks/useOffline.js +65 -11
  44. package/dist/hooks/usePageAssignment.d.ts +8 -5
  45. package/dist/hooks/usePageAssignment.js +37 -16
  46. package/dist/hooks/useRecordSearch.d.ts +34 -1
  47. package/dist/hooks/useRecordSearch.js +42 -7
  48. package/dist/hooks/useResolvedFilter.d.ts +46 -0
  49. package/dist/hooks/useResolvedFilter.js +130 -0
  50. package/dist/hooks/useSchemaPersistence.d.ts +7 -0
  51. package/dist/hooks/useSchemaPersistence.js +144 -5
  52. package/dist/hooks/useSettledSchema.d.ts +123 -0
  53. package/dist/hooks/useSettledSchema.js +186 -0
  54. package/dist/hooks/useViewData.d.ts +7 -2
  55. package/dist/index.d.ts +8 -1
  56. package/dist/index.js +45 -1
  57. package/dist/schema-input.d.ts +38 -8
  58. package/dist/schema-input.js +39 -7
  59. package/dist/utils/configBag.d.ts +68 -0
  60. package/dist/utils/configBag.js +70 -0
  61. package/dist/utils/error-message.d.ts +1 -1
  62. package/dist/utils/error-message.js +1 -1
  63. package/dist/utils/inlineAria.d.ts +61 -0
  64. package/dist/utils/inlineAria.js +70 -0
  65. package/dist/utils/nonGridRowCeiling.d.ts +44 -0
  66. package/dist/utils/nonGridRowCeiling.js +78 -0
  67. package/dist/utils/paramsBag.d.ts +28 -0
  68. package/dist/utils/paramsBag.js +146 -0
  69. package/dist/utils/propsBagDiagnostic.d.ts +196 -0
  70. package/dist/utils/propsBagDiagnostic.js +421 -0
  71. package/dist/utils/refusedDataPropDiagnostic.d.ts +44 -0
  72. package/dist/utils/refusedDataPropDiagnostic.js +94 -0
  73. package/dist/utils/unevaluatedExpression.d.ts +42 -1
  74. package/dist/utils/unevaluatedExpression.js +74 -11
  75. package/dist/utils/visibilityDiagnostic.d.ts +271 -19
  76. package/dist/utils/visibilityDiagnostic.js +311 -44
  77. package/package.json +10 -10
  78. package/dist/hooks/useClientNotifications.d.ts +0 -47
  79. package/dist/hooks/useClientNotifications.js +0 -162
  80. package/dist/spec-bridge/SpecBridge.d.ts +0 -28
  81. package/dist/spec-bridge/SpecBridge.js +0 -56
  82. package/dist/spec-bridge/bridges/form-view.d.ts +0 -158
  83. package/dist/spec-bridge/bridges/form-view.js +0 -170
  84. package/dist/spec-bridge/bridges/list-view.d.ts +0 -20
  85. package/dist/spec-bridge/bridges/list-view.js +0 -268
  86. package/dist/spec-bridge/index.d.ts +0 -11
  87. package/dist/spec-bridge/index.js +0 -10
  88. package/dist/spec-bridge/types.d.ts +0 -55
  89. package/dist/spec-bridge/types.js +0 -8
package/README.md CHANGED
@@ -25,10 +25,11 @@ npm install @object-ui/react @object-ui/core
25
25
 
26
26
  ```tsx
27
27
  import { SchemaRenderer } from '@object-ui/react'
28
+ import type { DeclaredNode } from '@object-ui/types'
28
29
 
29
- const schema = {
30
+ const schema: DeclaredNode = {
30
31
  type: 'text',
31
- value: 'Hello, Object UI!'
32
+ content: 'Hello, Object UI!'
32
33
  }
33
34
 
34
35
  function App() {
@@ -36,29 +37,57 @@ function App() {
36
37
  }
37
38
  ```
38
39
 
40
+ `schema` takes a `DeclaredNode` from `@object-ui/types`, a bare string, or nothing. A
41
+ `DeclaredNode` is a node of a declared type, keyed by its `type`: a component schema that
42
+ package declares, a spec page block such as `element:text` with its typed `properties` bag, a
43
+ stored page document under its page kind, or a type your application registers and declares
44
+ in `CustomNodeRegistry`. Each node, nested ones included, is checked against its own type; a
45
+ `type` nothing declares is refused.
46
+
39
47
  ### With Data
40
48
 
49
+ Expression scope reaches the renderer through `PredicateScopeProvider`, never through a prop
50
+ on the element: `SchemaRenderer` declares only `schema`, so anything else is forwarded to the
51
+ component the schema names (see the open forwarding surface below). Every key of the `scope`
52
+ you publish becomes a root the evaluator can read.
53
+
54
+ ⛔ `SchemaRendererProvider`'s `dataSource` is **not** an expression root. It carries the
55
+ host's `DataSource` adapter, and objectui#9308 removed the binding that used to publish that
56
+ adapter under the name `data` — an adapter answers no `data.*` path, so the root was constant
57
+ for every conformant host.
58
+
41
59
  ```tsx
42
- import { SchemaRenderer } from '@object-ui/react'
60
+ import { SchemaRenderer, PredicateScopeProvider } from '@object-ui/react'
61
+ import type { DeclaredNode } from '@object-ui/types'
43
62
 
44
- const schema = {
63
+ const schema: DeclaredNode = {
45
64
  type: 'form',
46
- body: [
65
+ children: [
66
+ {
67
+ // `content` is evaluated on every component type. `input` has no row in
68
+ // the spec's expression carriage map, so a `${…}` in ITS `value` would be
69
+ // rendered as those characters rather than resolved.
70
+ type: 'text',
71
+ content: 'Editing ${user.name}'
72
+ },
47
73
  {
48
74
  type: 'input',
49
75
  name: 'name',
50
- label: 'Name',
51
- value: '${user.name}'
76
+ label: 'Name'
52
77
  }
53
78
  ]
54
79
  }
55
80
 
56
- const data = {
81
+ const scope = {
57
82
  user: { name: 'John Doe' }
58
83
  }
59
84
 
60
85
  function App() {
61
- return <SchemaRenderer schema={schema} data={data} />
86
+ return (
87
+ <PredicateScopeProvider scope={scope}>
88
+ <SchemaRenderer schema={schema} />
89
+ </PredicateScopeProvider>
90
+ )
62
91
  }
63
92
  ```
64
93
 
@@ -66,14 +95,19 @@ function App() {
66
95
 
67
96
  ```tsx
68
97
  import { SchemaRenderer } from '@object-ui/react'
98
+ import type { DeclaredNode } from '@object-ui/types'
99
+
100
+ declare const formSchema: DeclaredNode
69
101
 
70
102
  function App() {
71
- const handleSubmit = (data) => {
103
+ const handleSubmit = (data: Record<string, unknown>) => {
72
104
  console.log('Form submitted:', data)
73
105
  }
74
106
 
107
+ // `onSubmit` is not a prop `SchemaRenderer` reads: it is forwarded to the
108
+ // component the schema names, which is why the props type stays open.
75
109
  return (
76
- <SchemaRenderer
110
+ <SchemaRenderer
77
111
  schema={formSchema}
78
112
  onSubmit={handleSubmit}
79
113
  />
@@ -87,7 +121,13 @@ Injects the host's data source (and optional capabilities) into every renderer
87
121
  below it:
88
122
 
89
123
  ```tsx
90
- import { SchemaRendererProvider } from '@object-ui/react'
124
+ import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react'
125
+ import type { ApiFetch } from '@object-ui/react'
126
+ import type { DataSource, DeclaredNode } from '@object-ui/types'
127
+
128
+ declare const adapter: DataSource
129
+ declare const authenticatedFetch: ApiFetch
130
+ declare const schema: DeclaredNode
91
131
 
92
132
  <SchemaRendererProvider
93
133
  dataSource={adapter}
@@ -101,6 +141,16 @@ import { SchemaRendererProvider } from '@object-ui/react'
101
141
  </SchemaRendererProvider>
102
142
  ```
103
143
 
144
+ `dataSource` carries the host's adapter as the published `DataSource` contract
145
+ (`@object-ui/types`), typed `DataSource | null | undefined`: a host either hands
146
+ over an adapter or states that it has none — a Studio preview, a page rendered
147
+ before the host's adapter has connected, or a widget probe driving `apiFetch`
148
+ alone. Both spellings of "none" are accepted because every reader in the tree
149
+ guards for it. The prop used to be typed `any`, so a string passed where the
150
+ adapter belongs — the shape of a config value read from the wrong place —
151
+ raised nothing until the first `find()` at runtime (objectui#7912).
152
+ `useSchemaContext()` hands the same type back to every consumer.
153
+
104
154
  Nested providers inherit `apiFetch` from their parent when they don't supply
105
155
  their own, so re-wrapped subtrees (embedded pages, preview surfaces) keep the
106
156
  host's authentication.
@@ -109,15 +159,23 @@ host's authentication.
109
159
 
110
160
  ### useSchemaContext
111
161
 
112
- Access the current schema context:
162
+ Access what `SchemaRendererProvider` injected — `dataSource`, `debug`,
163
+ `debugFlags` and `apiFetch`. It does **not** carry the page's values: read those
164
+ with `useDataScope`, which addresses the ambient expression scope by path. The
165
+ two are different channels on purpose — `dataSource` is the adapter a renderer
166
+ queries, `useDataScope` is what a node's `bind` resolves against
167
+ (objectui#9308).
113
168
 
114
169
  ```tsx
115
- import { useSchemaContext } from '@object-ui/react'
170
+ import { useDataScope, useSchemaContext } from '@object-ui/react'
116
171
 
117
172
  function MyComponent() {
118
- const { data, updateData } = useSchemaContext()
119
-
120
- return <div>{data.value}</div>
173
+ const { dataSource, debug } = useSchemaContext()
174
+ // Resolves `scope.value` from the nearest PredicateScopeProvider above.
175
+ const value = useDataScope('value')
176
+
177
+ if (!dataSource) return null
178
+ return <div data-debug={debug}>{String(value)}</div>
121
179
  }
122
180
  ```
123
181
 
@@ -130,25 +188,56 @@ views so `view` can be matched); these two apply the composed result to the
130
188
  block's schema and render the two non-final states.
131
189
 
132
190
  ```tsx
191
+ import type { FC } from 'react'
133
192
  import { ElementDataSourceGate } from '@object-ui/react'
193
+ import type { ElementDataSourceMapping } from '@object-ui/react'
194
+ // The seam is imported from CORE, not from here — see the note below.
195
+ import { elementDataSourceBlock } from '@object-ui/core'
196
+ import type { BaseSchema } from '@object-ui/types'
197
+
198
+ // The block this renderer wraps — whatever the registry resolves for its type.
199
+ declare const ObjectGrid: FC<{ schema: BaseSchema }>
134
200
 
135
201
  // `mapping` names ONLY the keys this block reads. A composed value written onto
136
202
  // a key the block ignores would be accepted and silently dropped — the defect
137
203
  // the binding exists to remove.
138
- const OBJECT_GRID_BINDING = {
204
+ const OBJECT_GRID_BINDING: ElementDataSourceMapping = {
139
205
  columns: true, // the view's FIELD list may fill `schema.columns`
140
206
  filter: true, // AND-combined, never replaced ("additional criteria")
141
207
  sort: true,
142
208
  limit: 'pagination.pageSize' as const,
143
209
  }
144
210
 
145
- const ObjectGridRenderer = ({ schema, ...props }) => (
211
+ // `elementDataSourceBlock` is not optional decoration — see below.
212
+ const ObjectGridRenderer = elementDataSourceBlock<FC<{ schema: BaseSchema }>>(({ schema, ...props }) => (
146
213
  <ElementDataSourceGate schema={schema} mapping={OBJECT_GRID_BINDING} testId="object-grid">
147
214
  {(bound) => <ObjectGrid schema={bound} {...props} />}
148
215
  </ElementDataSourceGate>
149
- )
216
+ ))
150
217
  ```
151
218
 
219
+ **Wrap the registered renderer in `elementDataSourceBlock`.** It is what makes
220
+ `ComponentRegistry.register` emit the `dataSource` input on that block's
221
+ authoring surface, so the key the gate READS is also the key the manifest, the
222
+ save gate, the generated JSX types and the designer DECLARE. Skip it and the
223
+ binding still works at runtime while the html tier reports `dataSource` as a prop
224
+ that does not exist — the one spelling that resolves a saved view, reported
225
+ exactly like the spellings that do nothing (objectui#6678). The declaration is
226
+ emitted from this one seam, so never hand-write a `dataSource` entry in a block's
227
+ `inputs`. `pnpm check:element-data-source-declaration` fails any source that
228
+ consumes the gate without reaching the seam.
229
+
230
+ ⚠️ **Import the seam from `@object-ui/core`, not from this package** — it is one
231
+ function under one name, re-exported here for discoverability, and the check
232
+ above enforces the core import at call sites. The reason is measured, not
233
+ stylistic: a registration runs at MODULE SCOPE, 101 suites in this repo partially
234
+ mock `@object-ui/react` by hand-listing the exports they return, and a
235
+ module-scope read of a name absent from such a list throws at COLLECTION time —
236
+ the importing test file dies before running a single assertion. Taking the seam
237
+ from `@object-ui/react` reddened 17 files across all four CI shards with zero
238
+ failed assertions among them. Nothing mocks `@object-ui/core`, and every
239
+ registration module already imports `ComponentRegistry` from it.
240
+
152
241
  `object` lands on `objectName` by default (pass `object: 'apiName'` for another
153
242
  key, or `object: false` for a block that reads the composed binding itself).
154
243
  Precedence: binding keys beat the component's own, view-sourced values are only a
@@ -157,6 +246,180 @@ A `view` name that does not resolve renders a configuration error rather than
157
246
  falling back to the object's full scope. Use `useElementDataSourceSchema` (plus
158
247
  the exported `ElementDataSourceErrorPanel` / `ElementDataSourceLoadingPanel`) when
159
248
  a block cannot be wrapped — a renderer whose hooks must run before the panels.
249
+ That form still reads `dataSource`, so it still owes the seam: mark the renderer
250
+ at its registration, `register('x', elementDataSourceBlock(XRenderer), { … })` —
251
+ again importing `elementDataSourceBlock` from `@object-ui/core`.
252
+
253
+ ### useSettledSchema
254
+
255
+ The settled-schema RESOLUTION half, shared by every view that gates a record
256
+ query on its object definition — `ObjectKanban`, `ObjectView`,
257
+ `ObjectCalendar`, `ObjectTree` and `ObjectGantt` (objectui#6482, converged in
258
+ objectui#7225). Tracks whether an object's definition has finished resolving
259
+ FOR THE KEY THE CURRENT RENDER IS ASKING ABOUT — `ready` and `def` are two views of one piece of state, so a
260
+ stale key can never read as ready. GATE PLACEMENT — which effect actually
261
+ waits on `ready` — stays a per-component decision; this hook only owns the
262
+ resolution.
263
+
264
+ ```tsx
265
+ import { useEffect } from 'react'
266
+ import { useSettledSchema } from '@object-ui/react'
267
+ import type { DataSource } from '@object-ui/types'
268
+
269
+ function ObjectSomething({
270
+ schema,
271
+ dataSource,
272
+ }: {
273
+ schema: { objectName?: string }
274
+ dataSource: DataSource
275
+ }) {
276
+ const key = schema.objectName ?? ''
277
+ const { ready, def } = useSettledSchema(key, dataSource)
278
+
279
+ useEffect(() => {
280
+ if (!ready) return // gate placement is local to this component
281
+ // issue the record query, e.g. buildExpandFields(def?.fields)
282
+ }, [ready, def])
283
+ }
284
+ ```
285
+
286
+ Pass `dataSource: undefined` for a render that should settle immediately with
287
+ no definition (e.g. a provider that issues no metadata read at all) instead of
288
+ adding a separate enable flag.
289
+
290
+ ### useFilterScope / useResolvedFilter
291
+
292
+ A data node that sends its own authored `filter` into a query resolves the
293
+ spec's context tokens in it first: `{current_user_id}`, `{current_org_id}`,
294
+ `{record_id}` and the date macros such as `{today}` (objectui#10666).
295
+ `useFilterScope()` reads the session scope a host mounts with
296
+ `FilterScopeProvider`, plus the id of the record in view when a
297
+ `RecordContextProvider` is mounted above the node (objectui#7297);
298
+ `useResolvedFilter(filter, scope)` resolves the filter through
299
+ `@object-ui/core`'s `resolveFilterPlaceholders` and HOLDS the result.
300
+
301
+ `{record_id}` is the id of the record a `type: 'record'` page shows, taken from
302
+ that page's mounted record context and never from a URL parameter or a page
303
+ variable: on a person's record page, `{ assignee: '{record_id}' }` counts that
304
+ person's tasks. With no record in context (a list view, a dashboard, a page
305
+ that is not a record page) it is refused by name, with the same warning an
306
+ unresolved `{current_user_id}` gets, and left as written, so the filter never
307
+ widens. It scopes what a component shows; it is not access control, which
308
+ stays with the server's row-level security. `FilterScopeProvider` carries the
309
+ session values only.
310
+
311
+ ```tsx
312
+ import { useEffect } from 'react'
313
+ import { useFilterScope, useResolvedFilter } from '@object-ui/react'
314
+ import type { DataSource, QueryParams } from '@object-ui/types'
315
+
316
+ function ObjectSomething({
317
+ schema,
318
+ dataSource,
319
+ }: {
320
+ schema: { objectName: string; filter?: QueryParams['$filter'] }
321
+ dataSource: DataSource
322
+ }) {
323
+ const filterScope = useFilterScope()
324
+ const filter = useResolvedFilter(schema.filter, filterScope)
325
+
326
+ useEffect(() => {
327
+ // Query with the held value and key the effect on it, never on schema.filter.
328
+ void dataSource.find(schema.objectName, { $filter: filter })
329
+ }, [dataSource, schema.objectName, filter])
330
+ }
331
+ ```
332
+
333
+ The held value keeps its reference while the authored filter is structurally
334
+ equal (a filter rebuilt inline on every render included) and the scope's user,
335
+ organization, record and `onUnresolved` are unchanged, so the effect above runs
336
+ once. A structurally different filter, a new signed-in user or organization, or
337
+ another record in view resolves again. A filter with no token to resolve is handed back as the value
338
+ passed in. A token the scope cannot resolve is left in place, and the resolver
339
+ logs one warning naming it.
340
+
341
+ ### NON_GRID_ROW_CEILING
342
+
343
+ The platform's hard row ceiling for a NON-GRID visualisation — gantt, calendar,
344
+ map and tree (objectui#7210). Those four fetch the whole FILTERED result set,
345
+ because a truthful range or layout needs all of it, but the fetch is bounded:
346
+ past the ceiling they draw the first N rows and say so.
347
+
348
+ The mechanism lives in `@object-ui/core` (objectui#7508): `NON_GRID_ROW_CEILING`,
349
+ `nonGridRowCeilingQuery` and `applyNonGridRowCeiling`, with the result type
350
+ `NonGridCeilingResult`. This package re-exports `NON_GRID_ROW_CEILING`,
351
+ `applyNonGridRowCeiling` and `NonGridCeilingResult`, and owns the React half —
352
+ the footnote, `NonGridRowCeilingNote`.
353
+
354
+ ```tsx
355
+ import { NonGridRowCeilingNote, applyNonGridRowCeiling } from '@object-ui/react'
356
+ import { nonGridRowCeilingQuery } from '@object-ui/core'
357
+ import type { DataSource, QueryParams } from '@object-ui/types'
358
+
359
+ declare const dataSource: DataSource
360
+ declare const objectName: string
361
+ declare const schema: { filter?: QueryParams['$filter'] }
362
+
363
+ const result = await dataSource.find(objectName, {
364
+ $filter: schema.filter,
365
+ ...nonGridRowCeilingQuery(), // `$top`: the ceiling plus ONE probe row
366
+ })
367
+ // The semicolon is load-bearing: the next statement opens with `<`, so without
368
+ // it the call above is parsed as the left side of a relational expression.
369
+ const capped = applyNonGridRowCeiling(result);
370
+ // …draw `capped.rows`, then hand the note the whole result:
371
+ <NonGridRowCeilingNote result={capped} />
372
+ ```
373
+
374
+ `nonGridRowCeilingQuery()` is the one place the probe row is written: `$top` is
375
+ the ceiling plus one, because the extra row is what makes truncation a fact about
376
+ the rows in hand — `QueryResult.total` is optional and a bare-array response
377
+ carries none. Spread it into the query; never spell the `+ 1` at a call site.
378
+
379
+ `NonGridRowCeilingNote` takes the `NonGridCeilingResult` and nothing else. The
380
+ count it prints is `result.rows.length` and the total is `result.total`, so the
381
+ footnote cannot name numbers that disagree with the rows drawn. It renders `null`
382
+ when nothing was truncated, so it can be mounted whenever a result is in hand.
383
+
384
+ ⛔ The ceiling is not authorable and must not become so. Silent truncation is
385
+ the failure it exists to prevent — a cut-off schedule still looks like a
386
+ schedule — so a view that caps rows without rendering the note is a defect, not
387
+ an optimisation.
388
+
389
+ ### resolveInlineAriaProps
390
+
391
+ Maps the spec's nested `aria` bag (`AriaPropsSchema`, the `aria` prop a block
392
+ such as `element:text` declares) to the DOM attributes it names
393
+ (objectui#11051):
394
+
395
+ - `ariaLabel` → `aria-label`. A plain string is used as written; an inline
396
+ locale map (`{ en: 'Order total', 'zh-CN': '订单合计' }`) gives the entry for
397
+ `locale`, through the spec's own `resolveI18nLabel`.
398
+ - `ariaDescribedBy` → `aria-describedby`.
399
+ - `role` → `role`.
400
+
401
+ Only attributes that have a value are returned, so the result can be spread
402
+ onto an element directly. It is a pure function: pass the locale yourself, in
403
+ React from `useDisplayLocale()`.
404
+
405
+ ```tsx
406
+ import { resolveInlineAriaProps } from '@object-ui/react'
407
+ import { useDisplayLocale } from '@object-ui/i18n'
408
+ import type { AriaProps } from '@object-ui/types'
409
+
410
+ function CloseButton({ aria, onClose }: { aria?: AriaProps; onClose: () => void }) {
411
+ const locale = useDisplayLocale()
412
+ return (
413
+ <button type="button" onClick={onClose} {...resolveInlineAriaProps(aria, locale)}>
414
+ ×
415
+ </button>
416
+ )
417
+ }
418
+ ```
419
+
420
+ This is not the reader `SchemaRenderer` applies to a node's FLAT `ariaLabel`,
421
+ which is objectui's keyed `{ key, defaultValue }` form. Each resolver returns
422
+ nothing useful for the other's shape, so the two stay separate (objectui#4580).
160
423
 
161
424
  ### ComponentRegistry
162
425
 
@@ -176,7 +439,7 @@ function MyComponent(props: Record<string, unknown>) {
176
439
 
177
440
  ### useDiscovery
178
441
 
179
- Access server discovery information including preview mode detection:
442
+ Access server discovery information — service availability and server identity:
180
443
 
181
444
  ```tsx
182
445
  import { useDiscovery } from '@object-ui/react'
@@ -184,11 +447,6 @@ import { useDiscovery } from '@object-ui/react'
184
447
  function MyComponent() {
185
448
  const { discovery, isLoading, isAuthEnabled, isAiEnabled } = useDiscovery()
186
449
 
187
- // Check if the server is in preview mode
188
- if (discovery?.mode === 'preview') {
189
- console.log('Preview mode active:', discovery.previewMode)
190
- }
191
-
192
450
  // Check if AI service is available
193
451
  if (isAiEnabled) {
194
452
  console.log('AI service route:', discovery?.services?.ai?.route)
@@ -204,21 +462,17 @@ function MyComponent() {
204
462
  | --- | --- | --- |
205
463
  | `name` | `string` | Server name |
206
464
  | `version` | `string` | Server version |
207
- | `mode` | `string` | Runtime mode (e.g. `'development'`, `'production'`, `'preview'`) |
208
- | `previewMode` | `object` | Preview mode configuration (present when mode is `'preview'`) |
465
+ | `mode` | `string` | Runtime mode the server reports (`RuntimeMode` in `@objectstack/spec`) |
209
466
  | `services` | `object` | Service availability status (auth, data, metadata, ai) |
210
467
  | `capabilities` | `string[]` | API capabilities |
211
468
 
212
- The `previewMode` object contains:
213
-
214
- | Property | Type | Default | Description |
215
- | --- | --- | --- | --- |
216
- | `autoLogin` | `boolean` | `true` | Skip login/registration pages |
217
- | `simulatedRole` | `'admin' \| 'user' \| 'viewer'` | `'admin'` | Simulated user role |
218
- | `simulatedUserName` | `string` | `'Preview User'` | Display name |
219
- | `readOnly` | `boolean` | `false` | Read-only mode |
220
- | `expiresInSeconds` | `number` | `0` | Session duration (0 = no expiry) |
221
- | `bannerMessage` | `string` | — | UI banner message |
469
+ > The `previewMode` block and the `'preview'` runtime mode were retired in
470
+ > objectui#6654, following their retirement in `@objectstack/spec`
471
+ > (objectstack#11846). A server that still sends them is read as any other
472
+ > payload: authentication follows `services.auth` alone, so a preview
473
+ > deployment requires login rather than silently running with auth off.
474
+ > `AuthProvider`'s `previewMode` **prop** in `@object-ui/auth` is a separate,
475
+ > host-supplied capability and is unaffected.
222
476
 
223
477
  ### useNotifications / useNotificationsByPresentation
224
478
 
@@ -240,6 +494,8 @@ belong (a banner is in flow, an inline notification sits next to its raiser).
240
494
  why every type looked like a toast.
241
495
 
242
496
  ```tsx
497
+ import { useNotifications } from '@object-ui/react'
498
+
243
499
  const { notify } = useNotifications()
244
500
 
245
501
  notify({ title: 'Saved', severity: 'success' }) // toast (spec default)
@@ -6,7 +6,24 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  import React, { Component, type ForwardRefExoticComponent, type RefAttributes } from 'react';
9
- import type { BaseSchema } from '@object-ui/types';
9
+ import type { DeclaredNode } from '@object-ui/types';
10
+ /**
11
+ * The `properties` evaluation of the {@link SchemaRenderer} memo, for a config
12
+ * bag that does not pass through `SchemaRenderer` (objectui#10290): the same
13
+ * per-key rule, the same `ExpressionEvaluator`, and the same scope, built from
14
+ * the same three inputs. It is not a second template engine.
15
+ *
16
+ * The plain half of `useConfigBagEvaluator()` (`hooks/useConfigBagEvaluator`),
17
+ * which reads those inputs from the contexts the memo reads them from. Call
18
+ * the hook; this function exists because the rule it applies is this module's.
19
+ * (The hook is not declared in this module: a hook here puts this file under
20
+ * the React Compiler lint, which then reports the render-time
21
+ * `performance.now()` / `Date.now()` debug probes below.)
22
+ *
23
+ * A value that is not a config bag is returned unchanged; a bag comes back as a
24
+ * new, evaluated object and the authored one is never mutated.
25
+ */
26
+ export declare function evaluateConfigBagInScope(bag: unknown, predicateScope: Record<string, unknown>, boundRecord: unknown, pageVariables: unknown): unknown;
10
27
  /**
11
28
  * Per-component Error Boundary for SchemaRenderer.
12
29
  * Catches render errors in individual components, preventing one broken
@@ -35,9 +52,9 @@ export declare class SchemaErrorBoundary extends Component<{
35
52
  *
36
53
  * ## Why `schema` is spelled as this union and not as a `SchemaNode`
37
54
  *
38
- * The repo carries two competing `SchemaNode` types: `@object-ui/core`'s
39
- * interface (which requires `type: string`) and `@object-ui/types`' union
40
- * (`BaseSchema | string | number | boolean | null | undefined`). This component
55
+ * The repo carried two competing `SchemaNode` types: `@object-ui/core`'s
56
+ * interface (which requires `type: string`) and `@object-ui/types`' union (then
57
+ * `BaseSchema | string | number | boolean | null | undefined`). This component
41
58
  * matched NEITHER. It declared core's — narrower than what it accepts, so every
42
59
  * caller holding the types union was wrong and could not be told — while its
43
60
  * runtime returns early for strings and nullish, which core's interface forbids.
@@ -48,11 +65,25 @@ export declare class SchemaErrorBoundary extends Component<{
48
65
  * handles: an object schema, a bare string (rendered as text), or nothing at
49
66
  * all. `number` / `boolean` are deliberately excluded — the runtime tolerates
50
67
  * them defensively (see the primitive guard in the evaluation memo) but no
51
- * author should be invited to pass them. Reconciling the two repo-wide
52
- * `SchemaNode` spellings is a separate concern and deliberately not done here.
68
+ * author should be invited to pass them; `toRenderableSchema` (`./schema-input`)
69
+ * is the bridge from a `SchemaNode` that may hold one.
70
+ *
71
+ * ## The object member is `DeclaredNode` (objectui#11466)
72
+ *
73
+ * The object member is `DeclaredNode` from `@object-ui/types`, the same type a
74
+ * node slot takes: the discriminated union, keyed by the literal `type`, of the
75
+ * component schemas that package declares, the spec-declared `AuthoringNode`s
76
+ * (objectui#11364: public blocks, `element:text_input` /
77
+ * `element:record_picker`, a stored page document under its page kind), and
78
+ * the types an application declares in `CustomNodeRegistry`. It has no
79
+ * `type: string` arm, so a node whose `type` no declaration names is refused
80
+ * here, and each node's own keys are judged: once objectui#8347 removes
81
+ * `BaseSchema`'s index signature, a misspelled key is refused while the
82
+ * declared spelling compiles. ⛔ The prop is widened by declared types only,
83
+ * never by an index signature, a `Record` or a `type: string` arm.
53
84
  */
54
85
  export interface SchemaRendererProps {
55
- schema: BaseSchema | string | null | undefined;
86
+ schema: DeclaredNode | string | null | undefined;
56
87
  }
57
88
  /**
58
89
  * The open forwarding surface, named once so it reads as the decision it is.