@object-ui/core 17.6.0 → 18.0.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 (147) hide show
  1. package/CHANGELOG.md +7260 -0
  2. package/README.md +228 -14
  3. package/dist/actions/ActionEngine.d.ts +4 -2
  4. package/dist/actions/ActionEngine.js +13 -8
  5. package/dist/actions/ActionRunner.d.ts +473 -69
  6. package/dist/actions/ActionRunner.js +792 -125
  7. package/dist/actions/TransactionManager.d.ts +7 -2
  8. package/dist/actions/UndoManager.d.ts +8 -13
  9. package/dist/actions/actionKeys.d.ts +9 -7
  10. package/dist/actions/actionKeys.js +67 -14
  11. package/dist/actions/actionResponse.js +2 -2
  12. package/dist/actions/index.d.ts +2 -0
  13. package/dist/actions/index.js +2 -0
  14. package/dist/actions/navigationTarget.d.ts +41 -0
  15. package/dist/actions/navigationTarget.js +20 -0
  16. package/dist/actions/recordDelete.d.ts +154 -0
  17. package/dist/actions/recordDelete.js +110 -0
  18. package/dist/adapters/ApiDataSource.d.ts +8 -4
  19. package/dist/adapters/ApiDataSource.js +19 -7
  20. package/dist/adapters/ValueDataSource.d.ts +6 -1
  21. package/dist/adapters/ValueDataSource.js +1090 -98
  22. package/dist/adapters/batchTransaction.d.ts +1 -1
  23. package/dist/adapters/batchTransaction.js +1 -1
  24. package/dist/adapters/resolveDataSource.d.ts +9 -6
  25. package/dist/adapters/resolveDataSource.js +9 -12
  26. package/dist/builder/schema-builder.d.ts +38 -81
  27. package/dist/builder/schema-builder.js +33 -152
  28. package/dist/data-scope/ViewDataProvider.d.ts +8 -25
  29. package/dist/data-scope/ViewDataProvider.js +27 -54
  30. package/dist/data-scope/element-data-source.d.ts +261 -7
  31. package/dist/data-scope/element-data-source.js +342 -52
  32. package/dist/data-scope/index.d.ts +9 -4
  33. package/dist/data-scope/index.js +9 -4
  34. package/dist/evaluator/ExpressionEvaluator.d.ts +86 -0
  35. package/dist/evaluator/ExpressionEvaluator.js +184 -12
  36. package/dist/evaluator/FormulaFunctions.d.ts +0 -10
  37. package/dist/evaluator/FormulaFunctions.js +166 -27
  38. package/dist/evaluator/deadlineSettled.d.ts +40 -0
  39. package/dist/evaluator/deadlineSettled.js +45 -0
  40. package/dist/evaluator/declaredPredicate.d.ts +93 -9
  41. package/dist/evaluator/declaredPredicate.js +117 -16
  42. package/dist/evaluator/fieldRules.d.ts +88 -4
  43. package/dist/evaluator/fieldRules.js +208 -31
  44. package/dist/evaluator/index.d.ts +2 -0
  45. package/dist/evaluator/index.js +2 -0
  46. package/dist/evaluator/listConditional.d.ts +103 -34
  47. package/dist/evaluator/listConditional.js +127 -50
  48. package/dist/evaluator/optionRules.d.ts +77 -6
  49. package/dist/evaluator/optionRules.js +87 -10
  50. package/dist/evaluator/predicateInput.d.ts +15 -4
  51. package/dist/evaluator/predicateInput.js +35 -2
  52. package/dist/evaluator/rowPredicateCanon.d.ts +7 -18
  53. package/dist/evaluator/rowPredicateCanon.js +65 -115
  54. package/dist/evaluator/subjectPermissions.d.ts +105 -0
  55. package/dist/evaluator/subjectPermissions.js +86 -0
  56. package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
  57. package/dist/evaluator/unevaluablePredicate.js +70 -0
  58. package/dist/index.d.ts +15 -0
  59. package/dist/index.js +83 -2
  60. package/dist/protocols/index.d.ts +0 -1
  61. package/dist/protocols/index.js +9 -1
  62. package/dist/registry/Registry.d.ts +382 -34
  63. package/dist/registry/Registry.js +247 -12
  64. package/dist/registry/WidgetRegistry.js +7 -3
  65. package/dist/registry/html-tier-intrinsics.d.ts +90 -0
  66. package/dist/registry/html-tier-intrinsics.js +110 -0
  67. package/dist/registry/public-blocks.d.ts +8 -0
  68. package/dist/registry/public-blocks.js +57 -0
  69. package/dist/theme/ThemeEngine.js +1 -1
  70. package/dist/utils/affordanceGrants.d.ts +371 -0
  71. package/dist/utils/affordanceGrants.js +252 -0
  72. package/dist/utils/chart-category-key.d.ts +82 -0
  73. package/dist/utils/chart-category-key.js +84 -0
  74. package/dist/utils/chart-measure-key.d.ts +64 -0
  75. package/dist/utils/chart-measure-key.js +65 -0
  76. package/dist/utils/chart-presentation.d.ts +95 -112
  77. package/dist/utils/chart-presentation.js +169 -104
  78. package/dist/utils/chart-series.d.ts +165 -5
  79. package/dist/utils/chart-series.js +123 -18
  80. package/dist/utils/column-sortability.d.ts +157 -0
  81. package/dist/utils/column-sortability.js +217 -0
  82. package/dist/utils/dashboard-filters.d.ts +38 -6
  83. package/dist/utils/dashboard-filters.js +107 -61
  84. package/dist/utils/dataset-format.d.ts +211 -30
  85. package/dist/utils/dataset-format.js +389 -46
  86. package/dist/utils/date-display.d.ts +346 -0
  87. package/dist/utils/date-display.js +644 -0
  88. package/dist/utils/date-macros.js +7 -0
  89. package/dist/utils/drill-down.d.ts +78 -0
  90. package/dist/utils/drill-down.js +93 -0
  91. package/dist/utils/emptiness.d.ts +96 -0
  92. package/dist/utils/emptiness.js +101 -0
  93. package/dist/utils/expand-fields.d.ts +81 -9
  94. package/dist/utils/expand-fields.js +81 -9
  95. package/dist/utils/extract-records.d.ts +65 -5
  96. package/dist/utils/extract-records.js +65 -8
  97. package/dist/utils/filter-converter.d.ts +158 -6
  98. package/dist/utils/filter-converter.js +1098 -24
  99. package/dist/utils/filter-tokens.d.ts +49 -5
  100. package/dist/utils/filter-tokens.js +105 -33
  101. package/dist/utils/grouping-fields.d.ts +61 -0
  102. package/dist/utils/grouping-fields.js +84 -0
  103. package/dist/utils/managedBy.d.ts +3 -3
  104. package/dist/utils/managedBy.js +5 -5
  105. package/dist/utils/native-date-value.d.ts +48 -0
  106. package/dist/utils/native-date-value.js +167 -0
  107. package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
  108. package/dist/utils/non-grid-row-ceiling.js +141 -0
  109. package/dist/utils/normalize-list-view.d.ts +135 -11
  110. package/dist/utils/normalize-list-view.js +243 -64
  111. package/dist/utils/number-display.d.ts +63 -14
  112. package/dist/utils/number-display.js +34 -12
  113. package/dist/utils/object-aggregate-query.d.ts +113 -0
  114. package/dist/utils/object-aggregate-query.js +50 -0
  115. package/dist/utils/parent-scope.d.ts +123 -0
  116. package/dist/utils/parent-scope.js +140 -0
  117. package/dist/utils/predicate-fields.d.ts +45 -12
  118. package/dist/utils/predicate-fields.js +141 -12
  119. package/dist/utils/record-color.d.ts +75 -0
  120. package/dist/utils/record-color.js +65 -0
  121. package/dist/utils/record-source.d.ts +298 -0
  122. package/dist/utils/record-source.js +331 -0
  123. package/dist/utils/record-title.d.ts +148 -8
  124. package/dist/utils/record-title.js +220 -26
  125. package/dist/utils/reference-keys.d.ts +21 -22
  126. package/dist/utils/reference-keys.js +532 -29
  127. package/dist/utils/sort-query.d.ts +115 -15
  128. package/dist/utils/sort-query.js +240 -13
  129. package/dist/utils/text-comparand.d.ts +92 -0
  130. package/dist/utils/text-comparand.js +99 -0
  131. package/dist/utils/without-denied-fields.d.ts +49 -0
  132. package/dist/utils/without-denied-fields.js +52 -0
  133. package/dist/validation/index.d.ts +8 -4
  134. package/dist/validation/index.js +8 -4
  135. package/dist/validation/schema-validator.d.ts +1 -1
  136. package/dist/validation/schema-validator.js +225 -83
  137. package/dist/validation/server-owned-value.d.ts +5 -1
  138. package/dist/validation/server-owned-value.js +5 -1
  139. package/dist/validation/validators/object-validation-engine.d.ts +1 -1
  140. package/dist/validation/validators/object-validation-engine.js +14 -3
  141. package/package.json +8 -9
  142. package/dist/data-scope/DataScopeManager.d.ts +0 -127
  143. package/dist/data-scope/DataScopeManager.js +0 -229
  144. package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
  145. package/dist/protocols/ResponsiveProtocol.js +0 -158
  146. package/dist/validation/validation-engine.d.ts +0 -88
  147. package/dist/validation/validation-engine.js +0 -434
package/README.md CHANGED
@@ -7,7 +7,7 @@ Core logic, types, and validation for Object UI. Zero React dependencies.
7
7
  - 🎯 **Type Definitions** - Re-exported runtime types; the component schema
8
8
  vocabulary itself is `@object-ui/types`
9
9
  - 🔍 **Component Registry** - Framework-agnostic component registration system
10
- - 📊 **Data Scope** - Data scope management and expression evaluation
10
+ - 🧮 **Expressions** - `${...}` expression evaluation against a context
11
11
  - ✅ **Validation** - Zod-based schema validation
12
12
  - 🚀 **Zero React** - Can run in Node.js or any JavaScript environment
13
13
 
@@ -36,8 +36,9 @@ import type {
36
36
 
37
37
  const mySchema: PageNodeSchema = {
38
38
  type: 'page',
39
+ pageType: 'app',
39
40
  title: 'My Page',
40
- body: []
41
+ children: []
41
42
  }
42
43
  ```
43
44
 
@@ -45,28 +46,28 @@ const mySchema: PageNodeSchema = {
45
46
 
46
47
  ```typescript
47
48
  import { ComponentRegistry } from '@object-ui/core'
49
+ import type { ComponentRenderer } from '@object-ui/core'
48
50
 
49
- ComponentRegistry.register('button', buttonMetadata)
50
- const metadata = ComponentRegistry.get('button')
51
+ // Your component, in whatever renderer shape the host framework uses.
52
+ declare const buttonRenderer: ComponentRenderer
53
+
54
+ ComponentRegistry.register('button', buttonRenderer)
55
+ const renderer = ComponentRegistry.get('button')
51
56
  ```
52
57
 
53
58
  `ComponentRegistry` is a process-level singleton exported by `@object-ui/core`;
54
59
  `SchemaRenderer` resolves every `type` against it, so a component registered
55
- here is renderable from schema anywhere in the app.
60
+ here is renderable from schema anywhere in the app. `register()`'s second
61
+ argument is the component itself; registration metadata is its optional third
62
+ argument, and `getMeta()` — not `get()` — reads that metadata back.
56
63
 
57
- ### Data Scope
64
+ ### Expressions
58
65
 
59
- `DataScopeManager` owns the named scopes a component tree reads from, and
60
- `evaluateExpression` evaluates a `${...}` expression against a context. They
61
- are separate exports: a scope holds data, it does not evaluate.
66
+ `evaluateExpression` evaluates a `${...}` expression against a context.
62
67
 
63
68
  ```typescript
64
- import { DataScopeManager, evaluateExpression } from '@object-ui/core'
65
-
66
- const manager = new DataScopeManager()
67
- manager.registerScope('user', { data: { name: 'John', role: 'admin' } })
69
+ import { evaluateExpression } from '@object-ui/core'
68
70
 
69
- const userName = manager.getScope('user')?.data.name // 'John'
70
71
  const isAdmin = evaluateExpression('${user.role === "admin"}', {
71
72
  user: { name: 'John', role: 'admin' },
72
73
  }) // true
@@ -82,6 +83,14 @@ object scope, which are injected:
82
83
 
83
84
  ```typescript
84
85
  import { createServerActionHandler } from '@object-ui/core'
86
+ import type { ActionRunner, ServerActionFetch } from '@object-ui/core'
87
+
88
+ // Injected by the host. Each stand-in is typed from the shipped surface, so
89
+ // this example is checked against the factory's own config rather than a copy.
90
+ declare const myAuthenticatedFetch: ServerActionFetch
91
+ declare const currentObject: string | undefined
92
+ declare const refetchData: () => void
93
+ declare const runner: ActionRunner
85
94
 
86
95
  const script = createServerActionHandler({
87
96
  fetch: myAuthenticatedFetch, // your auth wrapper (Bearer/cookies/...)
@@ -116,6 +125,7 @@ export const userListView = defineSystemView({
116
125
  columns: [{ name: 'email' }],
117
126
  })
118
127
 
128
+ // @ts-expect-error readonly by defineSystemView — refused at compile time, not just at run time
119
129
  userListView.columns.push({ name: 'name' }) // ❌ TypeError (strict mode)
120
130
  isSystemView(userListView) // ✅ true
121
131
 
@@ -136,6 +146,210 @@ isSystemView(draft) // false — clone is no longer Sys
136
146
  `Date`, `RegExp`, `Map`, `Set`, and class instances passed via `props` are
137
147
  intentionally **not** frozen so infrastructure objects keep working.
138
148
 
149
+ ### Field-level read gate (`withoutDeniedFields`)
150
+
151
+ Field-level security gates what the renderer DRAWS (objectui#7215 /
152
+ objectui#7230): a value the loaded permission policy denies is not shown, even
153
+ when a backend serves it. `withoutDeniedFields` is that rule for one record —
154
+ call it on a row before building a display value from the whole row (a record
155
+ title, a lookup label, a search-hit label), so a denied field reads exactly as
156
+ an absent one.
157
+
158
+ ```typescript
159
+ import { withoutDeniedFields } from '@object-ui/core'
160
+
161
+ // `policy` is structural: `usePermissions()` from `@object-ui/permissions`
162
+ // satisfies it, as does any `{ isLoaded, checkField }` object.
163
+ const policy = {
164
+ isLoaded: true,
165
+ checkField: (object: string, field: string) => !(object === 'account' && field === 'salary'),
166
+ }
167
+
168
+ withoutDeniedFields({ id: 'a1', name: 'Acme', salary: 100 }, policy, 'account')
169
+ // { id: 'a1', name: 'Acme' }
170
+
171
+ // A declared id field is kept without asking the policy, like `id` and `_id`:
172
+ withoutDeniedFields({ account_code: 'A-1', salary: 100 }, policy, 'account', ['account_code'])
173
+ // { account_code: 'A-1' }
174
+ ```
175
+
176
+ - `id`, `_id` and every key in the optional fourth argument are never judged.
177
+ - Before the policy loads, with no policy, with no object name, or for a value
178
+ that is not an object, the record comes back as it is.
179
+ - When nothing is withheld the SAME object comes back, so callers can tell the
180
+ two cases apart by identity.
181
+
182
+ ### Affordance-to-grant map (`AFFORDANCE_GRANTS`, `resolveAffordance`)
183
+
184
+ Every console affordance that offers a write — a create form's fields, the
185
+ record header's Edit, a list's New and Import, a lookup's "Create new", a grid
186
+ row's Delete, and the rest — reads the grant it exercises from ONE table,
187
+ `AFFORDANCE_GRANTS` (objectui#12082). A row names the CRUD-affordance bit the
188
+ affordance needs, the object grant it exercises (`create`, `update` or
189
+ `delete`) and, for an affordance that offers fields, the field question it asks
190
+ (`create` for an insert, `write` for an update). The fields of a form, of the
191
+ record page's in-place edit (`recordEdit`) and of a list's in-cell edit
192
+ (`listInlineEdit`) each ask that question, so a field the caller's permission
193
+ set marks `editable: false` is offered read-only on every one of them
194
+ (objectui#12103). The `write` question is the server's whole update rule for a
195
+ field, so for the record's owner field it also needs the transfer grant
196
+ (`allowTransfer`, or `modifyAllRecords`).
197
+
198
+ `resolveAffordance` is the one verdict: the object's managed-object policy, the
199
+ server's effective API operation set and the caller's grant must all allow it,
200
+ and the row's `userActions` predicates are surfaced only then.
201
+
202
+ ```typescript
203
+ import { resolveAffordance, resolveFieldAffordance } from '@object-ui/core'
204
+
205
+ // `perms` is structural: `usePermissions()` from `@object-ui/permissions`
206
+ // satisfies it. With no provider mounted every grant reads open.
207
+ const perms = {
208
+ isLoaded: true,
209
+ can: (_object: string, grant: 'create' | 'update' | 'delete') => grant === 'create',
210
+ checkField: (_object: string, _field: string, action: 'create' | 'write') => action === 'create',
211
+ getObjectApiOperations: () => undefined,
212
+ }
213
+
214
+ resolveAffordance('listNew', { objectSchema: null, objectName: 'orders', perms })
215
+ // { allowed: true } — the create grant is held
216
+ resolveAffordance('recordEdit', { objectSchema: null, objectName: 'orders', perms })
217
+ // { allowed: false } — Edit reads the update grant, which is not
218
+
219
+ resolveFieldAffordance('createFormFields', perms, 'orders', 'title') // true
220
+ resolveFieldAffordance('editFormFields', perms, 'orders', 'title') // false
221
+ ```
222
+
223
+ An affordance added later gets a row here and reads it through these functions;
224
+ it does not spell its own `can(object, verb)`. That includes an affordance that
225
+ reads NO grant at all — the calendar's quick-create and drag-to-reschedule
226
+ (`calendarQuickCreate`, `calendarReschedule`) and the kanban card move
227
+ (`kanbanCardMove`) were such affordances until they gained their rows. An
228
+ affordance that writes with the same verb on the same object reuses that row:
229
+ the record overlay's inline edit reads `recordEdit`, a gantt's bar edits and a
230
+ grid's bulk field update read `rowEdit`, a gantt row's Delete reads `rowDelete`,
231
+ the AI build panel's spreadsheet import reads `listImport`, a related list's
232
+ Add existing reads `relatedNew` (a link row) or `relatedRowEdit` (a re-parent),
233
+ and Setup's permission-set and position holders read `relatedNew` (a holder
234
+ added) and `relatedRowDelete` (a holder removed), each asked of its assignment
235
+ object (`sys_user_permission_set`, `sys_user_position`). Reaching Setup is not
236
+ that grant: the Setup app asks `setup.access`, which a caller can hold while the
237
+ server refuses their write of an assignment row.
238
+ The record feed's writes have rows of their own, each asked of the object it
239
+ writes: `feedComment` (a comment or reply, `sys_comment`), `feedReactionAdd` and
240
+ `feedReactionRemove` (the caller's own `sys_comment_reaction` row), and
241
+ `feedReactionColumn` (a reaction on a deployment that stores them in a
242
+ `sys_comment` column). The map's
243
+ enumeration pin in `@object-ui/plugin-form` (`affordanceGrantMap-12082.test.tsx`)
244
+ also counts every write call site in the console tree and holds each to the row
245
+ it sits behind, or to the reason it has none — and, for a component whose write
246
+ its host gates (the import wizard), holds every mount to reading the row. The
247
+ sites it found with no row yet formed a frozen backlog, which the pin now holds
248
+ empty: a new one cannot be added, so a write affordance gets its row when it
249
+ lands.
250
+
251
+ ### Undo snapshot for an update (`captureUpdateUndoData`)
252
+
253
+ An `undoable` update restores exactly the fields it wrote, from their values
254
+ before the write. `captureUpdateUndoData` is the one rule for reading those
255
+ values off a record. `ActionRunner` uses it, and so should any surface that
256
+ builds its own `update` Undo operation.
257
+
258
+ ```typescript
259
+ import { captureUpdateUndoData, type FieldContainerLike } from '@object-ui/core'
260
+
261
+ // The written object's field definitions, as its schema serves them
262
+ // (`objectSchema.fields`). Below, `status` is a text field, `account` a
263
+ // lookup and `config` a json field.
264
+ declare const fields: FieldContainerLike
265
+
266
+ captureUpdateUndoData(['status'], { id: 't1', status: 'open' }, fields) // { status: 'open' }
267
+ captureUpdateUndoData(['status'], { id: 't1', status: null }, fields) // { status: null }
268
+ captureUpdateUndoData(['status'], { id: 't1' }, fields) // undefined
269
+
270
+ // A relation read with `$expand` is captured as its stored id.
271
+ captureUpdateUndoData(['account'], { id: 't1', account: { id: 'a1', name: 'Acme' } }, fields)
272
+ // { account: 'a1' }
273
+
274
+ // Any other field is captured as the record carries it, whatever its shape.
275
+ captureUpdateUndoData(['config'], { id: 't1', config: { id: 'c1', mode: 'strict' } }, fields)
276
+ // { config: { id: 'c1', mode: 'strict' } }
277
+ ```
278
+
279
+ - A field counts as carried when it is an own key whose value is not
280
+ `undefined`. A carried `null` is a real empty value and is captured.
281
+ - When any written field is not carried, the answer is `undefined` and the
282
+ caller offers no Undo at all. A record projected by `$select`, or one the
283
+ server stripped of fields the reader may not read, can lack a written field
284
+ while the server holds a real value for it; a partial or `null` snapshot
285
+ would overwrite that value on Undo.
286
+ - A field the object declares as a relation (`lookup`, `master_detail`,
287
+ `user`, `tree`) is captured as the id it stores, and a `multiple` one as the
288
+ array of ids. `$expand` puts the related record where the id was, and
289
+ writing that record back into the reference is what Undo must never do.
290
+ Which fields are relations is read from `fields`, never from the value's
291
+ shape: a `json` field may hold an object with an `id`.
292
+ - `fields` is required. Pass `undefined` only when the caller has no field
293
+ definitions for the object; nothing is then treated as a relation.
294
+
295
+ ### Which fields an `operation: 'update'` Undo covers
296
+
297
+ The platform action route writes the `patch` of the declared action the
298
+ action's `name` names, under the collected params. So `ActionRunner`'s Undo
299
+ snapshot covers that declaration's `patch` keys plus the collected params. The
300
+ runner reads the declaration from its context: a host publishes the object's
301
+ declared actions as `objectActions`, beside `objectName` and `objectFields`.
302
+
303
+ ```typescript
304
+ import { ActionRunner } from '@object-ui/core'
305
+
306
+ // The object definition the host already holds, as the metadata API serves it.
307
+ declare const taskDef: { fields: Record<string, unknown>; actions: unknown[] }
308
+
309
+ // The same context `ActionProvider` takes. `objectFields` and `objectActions`
310
+ // are read for `objectName`'s object only.
311
+ new ActionRunner({
312
+ objectName: 'task',
313
+ objectFields: taskDef.fields,
314
+ objectActions: taskDef.actions,
315
+ })
316
+ ```
317
+
318
+ - The list is read only for the context's own object. A dispatch to another
319
+ object, or a host that publishes no list, reads the dispatched action's own
320
+ `patch`: a host that dispatches an object's action whole hands it in.
321
+ - A page action row's own `patch` is never covered, and no action renderer
322
+ forwards one: `@objectstack/spec` 18 retires the key on the row. A row that
323
+ names an action whose declaration has no `patch` snapshots the collected
324
+ params alone.
325
+
326
+ ### Display time zone (`setDisplayTimeZone`)
327
+
328
+ The date and datetime faces this package exports (`formatDate`,
329
+ `formatDateTime`, `formatDateTimeCompactParts`, `formatRelativeDate`) render
330
+ an instant in one display zone. A host declares it once; no caller passes a
331
+ zone, so every face on the page agrees.
332
+
333
+ ```typescript
334
+ import { formatDateTime, setDisplayTimeZone } from '@object-ui/core'
335
+
336
+ formatDateTime('2026-09-02T03:00:00Z', { locale: 'en-US' }) // the viewer's zone
337
+ setDisplayTimeZone('America/Los_Angeles')
338
+ formatDateTime('2026-09-02T03:00:00Z', { locale: 'en-US' }) // "Sep 1, 2026, 08:00 PM"
339
+ formatDateTime('2026-09-01', { locale: 'en-US' }) // "Sep 1, 2026, 12:00 AM", in every zone
340
+ setDisplayTimeZone(undefined) // back to the viewer's zone
341
+ ```
342
+
343
+ - A date-only value names a calendar day and keeps it in every zone, including
344
+ the `Date` that `toDisplayDate` builds for one and a caller hands on.
345
+ - "Today" for the relative face is the display zone's today.
346
+ - An IANA name the runtime's `Intl` does not know clears the zone, with a
347
+ console warning.
348
+ - `getDisplayTimeZone()` reads it back, and `subscribeDisplayTimeZone(listener)`
349
+ reports changes in React's `useSyncExternalStore` shape. In a React tree,
350
+ `LocalizationProvider` from `@object-ui/i18n` sets it from its `timezone`
351
+ value; a renderer never calls the setter.
352
+
139
353
  ## Philosophy
140
354
 
141
355
  This package is designed to be **framework-agnostic**. It contains:
@@ -118,8 +118,10 @@ export declare class ActionEngine {
118
118
  * DECLARED, which is asked through core's one definition
119
119
  * `hasDeclaredPredicate` (objectui#3850's ruling, adopted here by
120
120
  * objectui#3957). No gate — absent, `''`, blank predicate text in either
121
- * spelling, an envelope with no evaluable `source`, or a value that is
122
- * not a predicate at all — means visible. Evaluator errors hide the
121
+ * spelling, or an envelope whose `source` is `''` — means visible. A
122
+ * value that is present but has no evaluable `source` (`0`, `{}`, an
123
+ * array, an `ast`-only envelope) is a declared gate that faults
124
+ * (objectui#11358), so it hides like any other. Evaluator errors hide the
123
125
  * action (fail-closed) rather than throwing — this matches the contract
124
126
  * used by every individual action renderer (`action-button`,
125
127
  * `action-menu`, `action-bar`, …) so the same action behaves identically
@@ -171,8 +171,10 @@ export class ActionEngine {
171
171
  * DECLARED, which is asked through core's one definition
172
172
  * `hasDeclaredPredicate` (objectui#3850's ruling, adopted here by
173
173
  * objectui#3957). No gate — absent, `''`, blank predicate text in either
174
- * spelling, an envelope with no evaluable `source`, or a value that is
175
- * not a predicate at all — means visible. Evaluator errors hide the
174
+ * spelling, or an envelope whose `source` is `''` — means visible. A
175
+ * value that is present but has no evaluable `source` (`0`, `{}`, an
176
+ * array, an `ast`-only envelope) is a declared gate that faults
177
+ * (objectui#11358), so it hides like any other. Evaluator errors hide the
176
178
  * action (fail-closed) rather than throwing — this matches the contract
177
179
  * used by every individual action renderer (`action-button`,
178
180
  * `action-menu`, `action-bar`, …) so the same action behaves identically
@@ -226,10 +228,12 @@ export class ActionEngine {
226
228
  // hid it here because the normalizer wraps a blank string into `'${ }'`
227
229
  // whose verdict is falsy. That is the shape objectui#3314's invariant
228
230
  // forbids, and it is fixed by deleting a range, not by adding one
229
- // (objectui#3957). A value that is not a predicate, or a predicate that
230
- // says nothing, must not be the reason an action is hidden from
231
- // everyone — the same fail-open posture `ActionRunner` already committed
232
- // to for `disabled` (`catch { isDisabled = false }`).
231
+ // (objectui#3957). A predicate that says nothing (blank) must not be
232
+ // the reason an action is hidden from everyone. A value that is not a
233
+ // predicate at all is no longer "nothing" (objectui#11358): it is a
234
+ // declared gate the client cannot evaluate, the normalizer keeps it
235
+ // as such, and the `throwOnError` evaluation below hides it and
236
+ // reports it, as every action renderer's `visible` leg does.
233
237
  //
234
238
  // Booleans need no branch of their own: they are DECLARED (`visible:
235
239
  // false` is a verdict, objectui#3812) and `evaluateCondition`
@@ -241,7 +245,8 @@ export class ActionEngine {
241
245
  // predicate to the legacy JS engine; `toPredicateInput` keeps the
242
246
  // envelope so `evaluateCondition` can route it to `@objectstack/formula`
243
247
  // (the engine the server enforces with), exactly as the renderers do.
244
- // Declared ⇒ the normalizer left something to evaluate, so there is no
248
+ // Declared ⇒ the normalizer left something (possibly the declared-but-
249
+ // unevaluable `cel` envelope, which throws below), so there is no
245
250
  // `undefined` case left to handle here.
246
251
  const expr = toPredicateInput(raw);
247
252
  try {
@@ -261,7 +266,7 @@ export class ActionEngine {
261
266
  // BARE field reference (`done` instead of `record.done`), which is
262
267
  // an undeclared variable in the `{ record, recordId, objectName,
263
268
  // user }` eval scope. Silently hiding it makes that bug invisible
264
- // (the #2183 hunt). Warn once per predicate so it is diagnosable
269
+ // (the objectstack-ai/objectstack#2183 hunt). Warn once per predicate so it is diagnosable
265
270
  // without spamming re-renders.
266
271
  warnHiddenPredicate(ra.action.name, raw, err);
267
272
  return false;