@object-ui/core 17.5.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 (157) hide show
  1. package/CHANGELOG.md +7404 -0
  2. package/README.md +127 -26
  3. package/dist/actions/ActionEngine.d.ts +5 -3
  4. package/dist/actions/ActionEngine.js +14 -10
  5. package/dist/actions/ActionRunner.d.ts +363 -30
  6. package/dist/actions/ActionRunner.js +544 -58
  7. package/dist/actions/TransactionManager.d.ts +7 -2
  8. package/dist/actions/actionKeys.d.ts +75 -13
  9. package/dist/actions/actionKeys.js +131 -16
  10. package/dist/actions/actionResponse.js +3 -3
  11. package/dist/actions/index.d.ts +2 -0
  12. package/dist/actions/index.js +2 -0
  13. package/dist/actions/recordDelete.d.ts +118 -0
  14. package/dist/actions/recordDelete.js +112 -0
  15. package/dist/actions/recordIdParam.d.ts +77 -0
  16. package/dist/actions/recordIdParam.js +37 -0
  17. package/dist/actions/serverActionHandler.d.ts +1 -1
  18. package/dist/actions/serverActionHandler.js +1 -1
  19. package/dist/adapters/ValueDataSource.d.ts +6 -1
  20. package/dist/adapters/ValueDataSource.js +1090 -98
  21. package/dist/adapters/batchTransaction.d.ts +1 -1
  22. package/dist/adapters/batchTransaction.js +1 -1
  23. package/dist/builder/schema-builder.d.ts +38 -81
  24. package/dist/builder/schema-builder.js +33 -152
  25. package/dist/data-scope/ViewDataProvider.d.ts +1 -1
  26. package/dist/data-scope/ViewDataProvider.js +21 -1
  27. package/dist/data-scope/element-data-source.d.ts +261 -7
  28. package/dist/data-scope/element-data-source.js +342 -52
  29. package/dist/data-scope/index.d.ts +9 -4
  30. package/dist/data-scope/index.js +9 -4
  31. package/dist/evaluator/ExpressionEvaluator.d.ts +128 -1
  32. package/dist/evaluator/ExpressionEvaluator.js +226 -13
  33. package/dist/evaluator/FormulaFunctions.d.ts +0 -10
  34. package/dist/evaluator/FormulaFunctions.js +166 -27
  35. package/dist/evaluator/SafeExpressionParser.js +0 -4
  36. package/dist/evaluator/declaredPredicate.d.ts +93 -9
  37. package/dist/evaluator/declaredPredicate.js +118 -17
  38. package/dist/evaluator/fieldRules.d.ts +107 -4
  39. package/dist/evaluator/fieldRules.js +231 -35
  40. package/dist/evaluator/index.d.ts +2 -0
  41. package/dist/evaluator/index.js +2 -0
  42. package/dist/evaluator/listConditional.d.ts +125 -9
  43. package/dist/evaluator/listConditional.js +167 -26
  44. package/dist/evaluator/optionRules.d.ts +126 -0
  45. package/dist/evaluator/optionRules.js +141 -4
  46. package/dist/evaluator/predicateInput.d.ts +15 -4
  47. package/dist/evaluator/predicateInput.js +35 -2
  48. package/dist/evaluator/rowPredicateCanon.d.ts +56 -0
  49. package/dist/evaluator/rowPredicateCanon.js +158 -0
  50. package/dist/evaluator/subjectPermissions.d.ts +76 -0
  51. package/dist/evaluator/subjectPermissions.js +44 -0
  52. package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
  53. package/dist/evaluator/unevaluablePredicate.js +70 -0
  54. package/dist/index.d.ts +18 -0
  55. package/dist/index.js +97 -2
  56. package/dist/protocols/index.d.ts +0 -1
  57. package/dist/protocols/index.js +9 -1
  58. package/dist/registry/Registry.d.ts +397 -48
  59. package/dist/registry/Registry.js +263 -11
  60. package/dist/registry/WidgetRegistry.js +7 -3
  61. package/dist/registry/html-tier-intrinsics.d.ts +90 -0
  62. package/dist/registry/html-tier-intrinsics.js +110 -0
  63. package/dist/registry/public-blocks.d.ts +8 -0
  64. package/dist/registry/public-blocks.js +57 -0
  65. package/dist/theme/ThemeEngine.js +1 -1
  66. package/dist/theme/index.d.ts +1 -1
  67. package/dist/theme/index.js +1 -1
  68. package/dist/types/index.d.ts +92 -13
  69. package/dist/utils/chart-category-key.d.ts +82 -0
  70. package/dist/utils/chart-category-key.js +84 -0
  71. package/dist/utils/chart-measure-key.d.ts +64 -0
  72. package/dist/utils/chart-measure-key.js +65 -0
  73. package/dist/utils/chart-presentation.d.ts +188 -0
  74. package/dist/utils/chart-presentation.js +345 -0
  75. package/dist/utils/chart-series.d.ts +271 -31
  76. package/dist/utils/chart-series.js +478 -55
  77. package/dist/utils/column-identity.d.ts +43 -0
  78. package/dist/utils/column-identity.js +47 -1
  79. package/dist/utils/column-sortability.d.ts +157 -0
  80. package/dist/utils/column-sortability.js +217 -0
  81. package/dist/utils/dashboard-filters.d.ts +38 -6
  82. package/dist/utils/dashboard-filters.js +107 -61
  83. package/dist/utils/dataset-format.d.ts +211 -30
  84. package/dist/utils/dataset-format.js +407 -52
  85. package/dist/utils/date-display.d.ts +354 -0
  86. package/dist/utils/date-display.js +463 -0
  87. package/dist/utils/date-macros.js +7 -0
  88. package/dist/utils/debug.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 +171 -11
  94. package/dist/utils/expand-fields.js +171 -11
  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 +152 -4
  98. package/dist/utils/filter-converter.js +1092 -22
  99. package/dist/utils/filter-tokens.d.ts +49 -5
  100. package/dist/utils/filter-tokens.js +105 -34
  101. package/dist/utils/freeze-schema.d.ts +38 -1
  102. package/dist/utils/freeze-schema.js +10 -0
  103. package/dist/utils/grouping-fields.d.ts +61 -0
  104. package/dist/utils/grouping-fields.js +84 -0
  105. package/dist/utils/humanize-label.d.ts +60 -0
  106. package/dist/utils/humanize-label.js +62 -0
  107. package/dist/utils/managedBy.d.ts +30 -5
  108. package/dist/utils/managedBy.js +5 -5
  109. package/dist/utils/native-date-value.d.ts +48 -0
  110. package/dist/utils/native-date-value.js +167 -0
  111. package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
  112. package/dist/utils/non-grid-row-ceiling.js +141 -0
  113. package/dist/utils/normalize-list-view.d.ts +131 -1
  114. package/dist/utils/normalize-list-view.js +248 -6
  115. package/dist/utils/number-display.d.ts +70 -17
  116. package/dist/utils/number-display.js +34 -12
  117. package/dist/utils/object-aggregate-query.d.ts +113 -0
  118. package/dist/utils/object-aggregate-query.js +50 -0
  119. package/dist/utils/parent-scope.d.ts +123 -0
  120. package/dist/utils/parent-scope.js +140 -0
  121. package/dist/utils/predicate-fields.d.ts +62 -4
  122. package/dist/utils/predicate-fields.js +169 -4
  123. package/dist/utils/record-color.d.ts +75 -0
  124. package/dist/utils/record-color.js +65 -0
  125. package/dist/utils/record-source.d.ts +298 -0
  126. package/dist/utils/record-source.js +331 -0
  127. package/dist/utils/record-title.d.ts +148 -8
  128. package/dist/utils/record-title.js +220 -26
  129. package/dist/utils/reference-keys.d.ts +21 -22
  130. package/dist/utils/reference-keys.js +532 -29
  131. package/dist/utils/retired-field-types.d.ts +90 -0
  132. package/dist/utils/retired-field-types.js +124 -0
  133. package/dist/utils/sort-query.d.ts +128 -26
  134. package/dist/utils/sort-query.js +240 -13
  135. package/dist/utils/text-comparand.d.ts +92 -0
  136. package/dist/utils/text-comparand.js +99 -0
  137. package/dist/utils/unmaterialized-fields.d.ts +52 -0
  138. package/dist/utils/unmaterialized-fields.js +57 -0
  139. package/dist/utils/without-denied-fields.d.ts +49 -0
  140. package/dist/utils/without-denied-fields.js +52 -0
  141. package/dist/validation/index.d.ts +9 -4
  142. package/dist/validation/index.js +9 -4
  143. package/dist/validation/schema-validator.d.ts +1 -1
  144. package/dist/validation/schema-validator.js +305 -82
  145. package/dist/validation/server-owned-value.d.ts +62 -0
  146. package/dist/validation/server-owned-value.js +99 -0
  147. package/dist/validation/validators/object-validation-engine.d.ts +1 -1
  148. package/dist/validation/validators/object-validation-engine.js +14 -5
  149. package/package.json +8 -9
  150. package/dist/__benchmarks__/core.bench.d.ts +0 -8
  151. package/dist/__benchmarks__/core.bench.js +0 -53
  152. package/dist/data-scope/DataScopeManager.d.ts +0 -127
  153. package/dist/data-scope/DataScopeManager.js +0 -229
  154. package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
  155. package/dist/protocols/ResponsiveProtocol.js +0 -158
  156. package/dist/validation/validation-engine.d.ts +0 -88
  157. package/dist/validation/validation-engine.js +0 -434
@@ -5,6 +5,7 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
+ import type { ViewType } from '@object-ui/types';
8
9
  import type { RowHeight } from '@objectstack/spec/ui';
9
10
  /** ListView's toolbar density vocabulary — three steps, not the spec's five. */
10
11
  export type DensityMode = 'compact' | 'comfortable' | 'spacious';
@@ -62,6 +63,101 @@ export declare const ROW_HEIGHT_TO_DENSITY_MODE: Record<RowHeight, DensityMode>;
62
63
  * something typed `DensityMode`.
63
64
  */
64
65
  export declare function rowHeightToDensityMode(rowHeight: unknown): DensityMode | undefined;
66
+ /**
67
+ * A visualization `ListView` actually draws — one member per `case` in its
68
+ * `viewComponentSchema` switch (`packages/plugin-list/src/ListView.tsx`).
69
+ *
70
+ * DERIVED from {@link ViewType} (`ca3942729`), which is itself derived from
71
+ * `@objectstack/spec/ui` `ListView['type']`. Spelled as an `Exclude` of the
72
+ * members this renderer does not draw rather than as the spec's own
73
+ * `VisualizationType`, deliberately: `Exclude` leaves the SPEC's list as the
74
+ * thing being subtracted from, so ANY member the spec adds — visualization or
75
+ * not — lands here as a required key and fails the build, which is what the
76
+ * promise below actually says. Keying on `VisualizationType` instead would let
77
+ * a future non-visualization list type (another `page`) pass this site in
78
+ * silence, which is the exact failure being fixed.
79
+ *
80
+ * ⭐ What it subtracts is {@link UndrawableViewKind} — the keys of
81
+ * {@link UNDRAWABLE_VIEW_KINDS} the current `ViewType` still carries — and
82
+ * never a literal list of kinds (objectui#9880). A literal list is a second
83
+ * hand-written copy of the vocabulary, the failure class `ca3942729` removed
84
+ * from `ViewType` itself, and it rots in the direction the spec actually moved:
85
+ * `Exclude<ViewType, 'list' | 'detail' | 'page'>` kept subtracting `'page'`
86
+ * after objectstack RETIRED it, where `Exclude` of a non-member is a silent
87
+ * no-op no compiler reads. The sibling record's excess `page:` key is what went
88
+ * red; this line stayed green while saying something that had stopped being
89
+ * true.
90
+ *
91
+ * It happens to equal `@objectstack/spec/ui`'s `VisualizationType` today. That
92
+ * is a fact about the resolved spec, not the definition.
93
+ */
94
+ export type ListViewVisualization = Exclude<ViewType, UndrawableViewKind>;
95
+ /**
96
+ * Every member of {@link ViewType} that {@link LIST_VIEW_KINDS} does NOT carry,
97
+ * with the reason — and, where the answer is "the author authored something
98
+ * valid and will get a grid", the sentence to say out loud.
99
+ *
100
+ * The two tables together still partition the derived union — a member the spec
101
+ * ADDS must be classed as drawable or explained HERE, and until it is the build
102
+ * is red — but the red lands on {@link LIST_VIEW_KINDS} alone, because a kind
103
+ * neither table classes is not extracted into {@link UndrawableViewKind} and so
104
+ * stays inside {@link ListViewVisualization}, whose record is total. A key
105
+ * MISSPELLED here fails the same way, for the same reason: the kind it was
106
+ * meant to class is not extracted either. Both directions are pinned in
107
+ * `__tests__/normalize-list-view.undrawableBothLegs-9880.test.ts` rather than
108
+ * asserted in this sentence.
109
+ *
110
+ * ⛔ NOT annotated `Record<Exclude<ViewType, ListViewVisualization>, …>`, and
111
+ * the `satisfies` is not a style choice (objectui#9880). An annotation makes
112
+ * the literal EXACT in BOTH directions: a kind the spec adds is a missing key
113
+ * (wanted) and a kind the spec RETIRES is an EXCESS PROPERTY — a TS2353 that
114
+ * demands a deletion this repository cannot make while it resolves a published
115
+ * spec which still carries the kind (as it did with `page` through 17.4.0).
116
+ * `satisfies` keeps the value constraint
117
+ * (every reason is a `string` or `null`) and drops the exactness, which is what
118
+ * lets one spelling compile against both the pinned published spec and one
119
+ * built from objectstack `main`.
120
+ *
121
+ * `null` means silence is correct — these are objectui view CATEGORIES, never
122
+ * authored as a list-view kind:
123
+ * - `list` is the view CATEGORY, not a kind — it already folds to `grid`.
124
+ * - `detail` is a different renderer (`plugin-detail`), never a ListView case.
125
+ *
126
+ * `page` had a row here until objectui#11073. objectstack#17063 removed
127
+ * `type: 'page'` from the list-view enum, and the row answered for it while this
128
+ * repository still resolved a spec that published the kind. `@objectstack/spec`
129
+ * 17.5.0 carries the removal: both published `@object-ui/types` faces refuse
130
+ * `page`, `Extract` no longer extracted the row, and the residual pins that held
131
+ * it (`normalize-list-view.pageResidual-8429.test.ts`) were converted, so it was
132
+ * deleted as it said it would be. A stored `page` view now degrades like any
133
+ * kind outside the vocabulary.
134
+ */
135
+ declare const UNDRAWABLE_VIEW_KINDS: {
136
+ list: null;
137
+ detail: null;
138
+ };
139
+ /**
140
+ * The members of the current {@link ViewType} that {@link UNDRAWABLE_VIEW_KINDS}
141
+ * classes — the other half of the partition, derived rather than written out.
142
+ *
143
+ * `Extract` over the table's keys, not the table's raw `keyof`, is the load-
144
+ * bearing operator (objectui#9880): the table may carry a row for a kind the
145
+ * spec has since RETIRED, and `Extract` drops that row from the TYPE without
146
+ * the table having to lose it at runtime. That is what made one spelling
147
+ * correct against both the published spec (which published `page` through
148
+ * 17.4.0) and a spec built from objectstack `main` (which retired it), and what
149
+ * made the row's deletion at 17.5.0 a plain deletion (objectui#11073).
150
+ */
151
+ type UndrawableViewKind = Extract<ViewType, keyof typeof UNDRAWABLE_VIEW_KINDS>;
152
+ /**
153
+ * Whether `value` names a visualization `ListView` draws.
154
+ *
155
+ * Exported so a read site cannot restate the membership question as its own
156
+ * literal array: the gate and the seam must answer one question, which is the
157
+ * rule this file's own `availableViews` notes already argue for (objectui#5042,
158
+ * objectui#7544, and now `ca3942729`).
159
+ */
160
+ export declare function isListViewVisualization(value: unknown): value is ListViewVisualization;
65
161
  /**
66
162
  * ObjectUI's `list-view` node historically used a different vocabulary from
67
163
  * `@objectstack/spec` for the same concepts (`fields` where the spec says
@@ -114,10 +210,34 @@ export declare function rowHeightToDensityMode(rowHeight: unknown): DensityMode
114
210
  * so objectui's field is typed from the spec but used as something else.
115
211
  * That mismatch is real and out of scope here; converting formats inside a
116
212
  * vocabulary fold would change what reaches the data source.
213
+ * - `data: { provider: 'object', object }` → `objectName` (#7477, step 6 of
214
+ * #2890). This is the spelling the published `react-blocks` contract
215
+ * recommends and the one `@objectstack/spec`'s `ViewDataSchema` declares;
216
+ * ListView read it at ZERO sites, so a page bound that way validated green
217
+ * upstream and rendered an empty list here with no diagnostic. The `object`
218
+ * provider is a `strictObject` carrying exactly `{ provider, object }`, so
219
+ * `objectName` captures all of it. Two deliberate departures from the folds
220
+ * above, both narrowing:
221
+ * · an already-present `objectName` WINS — this fold only fills a gap, it
222
+ * never re-points a binding that already resolves;
223
+ * · `data` is NOT deleted. Every other fold deletes because the legacy key
224
+ * has one meaning and one home; `data` has four providers, `api`/`value`
225
+ * are read live in `ListView`, and the whole block is FORWARDED to child
226
+ * views (the gantt branch), whose own `getDataConfig` reads `data` before
227
+ * `objectName`. Deleting it for one provider would rewrite what a child
228
+ * resolves. Nothing in `ListView` reads `data.provider === 'object'`, so
229
+ * keeping it cannot create a second de-facto contract inside the renderer.
117
230
  * - `viewType`: a missing kind, or the view CATEGORY `'list'` that AI-authored
118
231
  * metadata stores and hosts forward verbatim, becomes the renderable `'grid'`
119
232
  * — otherwise it reaches the renderer's typeless default branch and shows as
120
- * a red "Unknown component type" box.
233
+ * a red "Unknown component type" box. Before that default applies, the
234
+ * AUTHOR's kind is read (#7477): `specType` — the slot
235
+ * `components/renderers/layout/react-page.tsx` parks a react-tier `type` in
236
+ * when the SDUI envelope claims the `type` key (ADR-0078) — and then a bare
237
+ * `type` when it names a kind ListView draws, which the component
238
+ * discriminator (`'list-view'`) never does. Same two legs, same order, as
239
+ * `normalizeChartSchema`'s chart-family read. An explicit `viewType` still
240
+ * wins: this leg only fills the gap that used to resolve to `'grid'`.
121
241
  * - each `columns` entry's IDENTITY — `name` / `fieldName` → the spec's `field`
122
242
  * (#3104). This one MIRRORS instead of deleting, for the reason given on
123
243
  * {@link normalizeColumnIdentities}: `columns` entries cross the package
@@ -125,5 +245,15 @@ export declare function rowHeightToDensityMode(rowHeight: unknown): DensityMode
125
245
  * breaking change with no inventory. Runs AFTER the `fields` → `columns` fold
126
246
  * above, so a view that spells its column list the legacy way still gets its
127
247
  * entries canonicalized.
248
+ * - the four PER-VIEW-TYPE config aliases phase 3 carried over
249
+ * ({@link PER_VIEW_CONFIG_ALIASES}): `kanban.groupField` → `groupByField`,
250
+ * `kanban.cardFields` → `columns`, `gallery.imageField` → `coverField`,
251
+ * `timeline.dateField` → `startDateField`. One read-site changes behaviour
252
+ * as a result, and it is a CORRECTION of the same inverted precedence A2
253
+ * fixed for `densityMode`: `ListView`'s kanban adapter resolves
254
+ * `cardFields || columns`, i.e. legacy over canonical, so a config carrying
255
+ * BOTH used to render the legacy value. After the fold the canonical
256
+ * `columns` reaches it. Every other reader of these four was already
257
+ * canonical-first and is unaffected.
128
258
  */
129
259
  export declare function normalizeListViewSchema<T>(schema: T): T;
@@ -73,8 +73,16 @@ const isRecord = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
73
73
  * available to users in the view toolbar", and already carries `rowHeight` —
74
74
  * objectui's `showDensity` under its spec name. `group` / `hideFields` /
75
75
  * `rowColor` are the same kind of toggle and are named after the config key
76
- * they gate (`grouping`, `hiddenFields`, `rowColor`), pending promotion into
77
- * `UserActionsConfigSchema` upstream.
76
+ * they gate (`grouping`, `hiddenFields`, `rowColor`).
77
+ *
78
+ * That promotion has LANDED (objectui#5435, ruled Option A 2026-08-22):
79
+ * `UserActionsConfigSchema` declares all three from `@objectstack/spec@17.3.0`,
80
+ * and the spec's declaring docblock names this card. Every key this table emits
81
+ * is therefore authorable, which
82
+ * `__tests__/normalize-list-view.foldOutputAuthorable-5435.test.ts` pins with a
83
+ * firing control. ⛔ Do not read the three as legacy-only spellings awaiting
84
+ * retirement — the protocol declares them, so removing them here would leave
85
+ * objectui narrower than the protocol.
78
86
  */
79
87
  const SHOW_FLAG_TO_USER_ACTION = {
80
88
  showSearch: 'search',
@@ -101,6 +109,163 @@ const ARIA_KEY_ALIASES = {
101
109
  label: 'ariaLabel',
102
110
  describedBy: 'ariaDescribedBy',
103
111
  };
112
+ /**
113
+ * The view kinds `ListView` actually draws.
114
+ *
115
+ * A total `Record<…, true>` over {@link ListViewVisualization}, for the same
116
+ * reason the density maps above are `Record<RowHeight, …>`: a kind added to the
117
+ * union fails the build HERE instead of silently staying unreadable authored
118
+ * input.
119
+ *
120
+ * ⚠️ That promise was FALSE between `@objectstack/spec@17.3.0` and `ca3942729`.
121
+ * `ViewType` was a hand-written copy of the spec's list, so this map was total
122
+ * over the copy and compiled green while the spec's `page` had nowhere to land.
123
+ * Both faces are derived now, and the two totality structures below close the
124
+ * other half of the union, so every member of the spec's list is accounted for
125
+ * by a structure that fails the build rather than by a reader's memory.
126
+ *
127
+ * `hasOwnProperty`, not `in` — same trap as {@link rowHeightToDensityMode}:
128
+ * `in` walks the prototype chain, so `'toString'` would read as a view kind.
129
+ */
130
+ const LIST_VIEW_KINDS = {
131
+ grid: true,
132
+ kanban: true,
133
+ gallery: true,
134
+ calendar: true,
135
+ timeline: true,
136
+ gantt: true,
137
+ map: true,
138
+ chart: true,
139
+ tree: true,
140
+ };
141
+ /**
142
+ * Every member of {@link ViewType} that {@link LIST_VIEW_KINDS} does NOT carry,
143
+ * with the reason — and, where the answer is "the author authored something
144
+ * valid and will get a grid", the sentence to say out loud.
145
+ *
146
+ * The two tables together still partition the derived union — a member the spec
147
+ * ADDS must be classed as drawable or explained HERE, and until it is the build
148
+ * is red — but the red lands on {@link LIST_VIEW_KINDS} alone, because a kind
149
+ * neither table classes is not extracted into {@link UndrawableViewKind} and so
150
+ * stays inside {@link ListViewVisualization}, whose record is total. A key
151
+ * MISSPELLED here fails the same way, for the same reason: the kind it was
152
+ * meant to class is not extracted either. Both directions are pinned in
153
+ * `__tests__/normalize-list-view.undrawableBothLegs-9880.test.ts` rather than
154
+ * asserted in this sentence.
155
+ *
156
+ * ⛔ NOT annotated `Record<Exclude<ViewType, ListViewVisualization>, …>`, and
157
+ * the `satisfies` is not a style choice (objectui#9880). An annotation makes
158
+ * the literal EXACT in BOTH directions: a kind the spec adds is a missing key
159
+ * (wanted) and a kind the spec RETIRES is an EXCESS PROPERTY — a TS2353 that
160
+ * demands a deletion this repository cannot make while it resolves a published
161
+ * spec which still carries the kind (as it did with `page` through 17.4.0).
162
+ * `satisfies` keeps the value constraint
163
+ * (every reason is a `string` or `null`) and drops the exactness, which is what
164
+ * lets one spelling compile against both the pinned published spec and one
165
+ * built from objectstack `main`.
166
+ *
167
+ * `null` means silence is correct — these are objectui view CATEGORIES, never
168
+ * authored as a list-view kind:
169
+ * - `list` is the view CATEGORY, not a kind — it already folds to `grid`.
170
+ * - `detail` is a different renderer (`plugin-detail`), never a ListView case.
171
+ *
172
+ * `page` had a row here until objectui#11073. objectstack#17063 removed
173
+ * `type: 'page'` from the list-view enum, and the row answered for it while this
174
+ * repository still resolved a spec that published the kind. `@objectstack/spec`
175
+ * 17.5.0 carries the removal: both published `@object-ui/types` faces refuse
176
+ * `page`, `Extract` no longer extracted the row, and the residual pins that held
177
+ * it (`normalize-list-view.pageResidual-8429.test.ts`) were converted, so it was
178
+ * deleted as it said it would be. A stored `page` view now degrades like any
179
+ * kind outside the vocabulary.
180
+ */
181
+ const UNDRAWABLE_VIEW_KINDS = {
182
+ list: null,
183
+ detail: null,
184
+ };
185
+ /** One warning per undrawable kind per process — see {@link warnUndrawableViewKind}. */
186
+ const warnedUndrawableKinds = new Set();
187
+ /**
188
+ * Fail-open is **loud**, not silent — the convention this repo already applies
189
+ * to unevaluable predicates (`../evaluator/fieldRules.ts`, objectstack#5149).
190
+ *
191
+ * The bug `ca3942729` records is not only that `type: 'page'` degrades to a
192
+ * grid; it is that it degrades IDENTICALLY to a typo. Measured on `5505aec`,
193
+ * `specType: 'page'` and `specType: 'nonsense'` both left `viewType: 'grid'`
194
+ * with no error, no warning and no console line — and `@object-ui/types`'
195
+ * published validator ACCEPTS `viewType: 'page'` while refusing the typo. An
196
+ * author who writes a spec-valid view and is silently handed a different one
197
+ * has no way to tell that from a working grid, so the degrade says so once.
198
+ *
199
+ * Once per kind per process, and never for the two objectui categories: this
200
+ * runs inside a normalizer that a render can call on every keystroke, and the
201
+ * point is one line, not a scrolling wall.
202
+ */
203
+ function warnUndrawableViewKind(kind) {
204
+ const reason = Object.prototype.hasOwnProperty.call(UNDRAWABLE_VIEW_KINDS, kind)
205
+ ? UNDRAWABLE_VIEW_KINDS[kind]
206
+ : null;
207
+ if (reason === null || warnedUndrawableKinds.has(kind))
208
+ return;
209
+ warnedUndrawableKinds.add(kind);
210
+ console.warn(`[object-ui] This build accepts a ${JSON.stringify(kind)} list view but cannot draw one: ` +
211
+ `${reason}. The view falls back to a grid. ` +
212
+ 'Author a visualization this renderer draws, or render the page through the surface that owns it.');
213
+ }
214
+ /**
215
+ * Whether `value` names a visualization `ListView` draws.
216
+ *
217
+ * Exported so a read site cannot restate the membership question as its own
218
+ * literal array: the gate and the seam must answer one question, which is the
219
+ * rule this file's own `availableViews` notes already argue for (objectui#5042,
220
+ * objectui#7544, and now `ca3942729`).
221
+ */
222
+ export function isListViewVisualization(value) {
223
+ return typeof value === 'string' && Object.prototype.hasOwnProperty.call(LIST_VIEW_KINDS, value);
224
+ }
225
+ /**
226
+ * The author's view kind, or `undefined` when the value names no kind ListView
227
+ * draws. An unrecognized kind is deliberately left unresolved rather than
228
+ * written through to `viewType`: the caller's own `'grid'` default is a more
229
+ * honest answer than a `viewType` no branch matches (the same call
230
+ * `normalizeChartSchema` makes for a chart family it cannot draw).
231
+ *
232
+ * A kind the SPEC allows but this renderer cannot draw takes the same fallback
233
+ * and additionally warns — see {@link warnUndrawableViewKind}.
234
+ */
235
+ function readListViewKind(value) {
236
+ if (typeof value !== 'string')
237
+ return undefined;
238
+ if (isListViewVisualization(value))
239
+ return value;
240
+ warnUndrawableViewKind(value);
241
+ return undefined;
242
+ }
243
+ /**
244
+ * Per-view-type config aliases → the spec key each one aliases (#2890, the
245
+ * phase-3 carry-over).
246
+ *
247
+ * Phase 3 derived `kanban` / `gallery` / `timeline` from the spec configs but
248
+ * kept the pre-#2231 objectui spellings declared alongside the spec keys so
249
+ * stored view metadata would keep validating
250
+ * (`packages/types/src/zod/objectql.zod.ts`, each marked `@deprecated legacy
251
+ * alias for the spec's X`). Folding them here is what lets a read-site stop
252
+ * carrying its own `canonical || legacy` pair — the same move A1–A5 made for
253
+ * the top-level vocabulary.
254
+ *
255
+ * `calendar` is deliberately absent: its one local key, `defaultView`, is not
256
+ * an alias of anything. It has no spec counterpart at all, so it wants
257
+ * PROMOTION upstream, not a rename — folding it would delete an authored value
258
+ * with nowhere to put it.
259
+ *
260
+ * Note `kanban.columns` is the spec's "fields shown on each card", NOT the
261
+ * table columns that the same word names at the ListView top level. The fold
262
+ * is scoped to the nested config, so the two never meet.
263
+ */
264
+ const PER_VIEW_CONFIG_ALIASES = {
265
+ kanban: { groupField: 'groupByField', cardFields: 'columns' },
266
+ gallery: { imageField: 'coverField' },
267
+ timeline: { dateField: 'startDateField' },
268
+ };
104
269
  /**
105
270
  * ObjectUI's `list-view` node historically used a different vocabulary from
106
271
  * `@objectstack/spec` for the same concepts (`fields` where the spec says
@@ -153,10 +318,34 @@ const ARIA_KEY_ALIASES = {
153
318
  * so objectui's field is typed from the spec but used as something else.
154
319
  * That mismatch is real and out of scope here; converting formats inside a
155
320
  * vocabulary fold would change what reaches the data source.
321
+ * - `data: { provider: 'object', object }` → `objectName` (#7477, step 6 of
322
+ * #2890). This is the spelling the published `react-blocks` contract
323
+ * recommends and the one `@objectstack/spec`'s `ViewDataSchema` declares;
324
+ * ListView read it at ZERO sites, so a page bound that way validated green
325
+ * upstream and rendered an empty list here with no diagnostic. The `object`
326
+ * provider is a `strictObject` carrying exactly `{ provider, object }`, so
327
+ * `objectName` captures all of it. Two deliberate departures from the folds
328
+ * above, both narrowing:
329
+ * · an already-present `objectName` WINS — this fold only fills a gap, it
330
+ * never re-points a binding that already resolves;
331
+ * · `data` is NOT deleted. Every other fold deletes because the legacy key
332
+ * has one meaning and one home; `data` has four providers, `api`/`value`
333
+ * are read live in `ListView`, and the whole block is FORWARDED to child
334
+ * views (the gantt branch), whose own `getDataConfig` reads `data` before
335
+ * `objectName`. Deleting it for one provider would rewrite what a child
336
+ * resolves. Nothing in `ListView` reads `data.provider === 'object'`, so
337
+ * keeping it cannot create a second de-facto contract inside the renderer.
156
338
  * - `viewType`: a missing kind, or the view CATEGORY `'list'` that AI-authored
157
339
  * metadata stores and hosts forward verbatim, becomes the renderable `'grid'`
158
340
  * — otherwise it reaches the renderer's typeless default branch and shows as
159
- * a red "Unknown component type" box.
341
+ * a red "Unknown component type" box. Before that default applies, the
342
+ * AUTHOR's kind is read (#7477): `specType` — the slot
343
+ * `components/renderers/layout/react-page.tsx` parks a react-tier `type` in
344
+ * when the SDUI envelope claims the `type` key (ADR-0078) — and then a bare
345
+ * `type` when it names a kind ListView draws, which the component
346
+ * discriminator (`'list-view'`) never does. Same two legs, same order, as
347
+ * `normalizeChartSchema`'s chart-family read. An explicit `viewType` still
348
+ * wins: this leg only fills the gap that used to resolve to `'grid'`.
160
349
  * - each `columns` entry's IDENTITY — `name` / `fieldName` → the spec's `field`
161
350
  * (#3104). This one MIRRORS instead of deleting, for the reason given on
162
351
  * {@link normalizeColumnIdentities}: `columns` entries cross the package
@@ -164,6 +353,16 @@ const ARIA_KEY_ALIASES = {
164
353
  * breaking change with no inventory. Runs AFTER the `fields` → `columns` fold
165
354
  * above, so a view that spells its column list the legacy way still gets its
166
355
  * entries canonicalized.
356
+ * - the four PER-VIEW-TYPE config aliases phase 3 carried over
357
+ * ({@link PER_VIEW_CONFIG_ALIASES}): `kanban.groupField` → `groupByField`,
358
+ * `kanban.cardFields` → `columns`, `gallery.imageField` → `coverField`,
359
+ * `timeline.dateField` → `startDateField`. One read-site changes behaviour
360
+ * as a result, and it is a CORRECTION of the same inverted precedence A2
361
+ * fixed for `densityMode`: `ListView`'s kanban adapter resolves
362
+ * `cardFields || columns`, i.e. legacy over canonical, so a config carrying
363
+ * BOTH used to render the legacy value. After the fold the canonical
364
+ * `columns` reaches it. Every other reader of these four was already
365
+ * canonical-first and is unaffected.
167
366
  */
168
367
  export function normalizeListViewSchema(schema) {
169
368
  if (!schema || typeof schema !== 'object')
@@ -172,7 +371,12 @@ export function normalizeListViewSchema(schema) {
172
371
  const legacyFields = s.fields;
173
372
  const foldColumns = Array.isArray(legacyFields);
174
373
  const legacyDensity = s.densityMode;
175
- const foldRowHeight = typeof legacyDensity === 'string' && legacyDensity in DENSITY_MODE_TO_ROW_HEIGHT;
374
+ // `hasOwnProperty`, not `in` — same trap as {@link rowHeightToDensityMode},
375
+ // in the fold direction: `in` walks the prototype chain, so `'toString'` used
376
+ // to fold into `rowHeight` as `Object.prototype.toString`, a FUNCTION, and
377
+ // drop the key (objectui#10868). An inherited key is an unrecognized density.
378
+ const foldRowHeight = typeof legacyDensity === 'string' &&
379
+ Object.prototype.hasOwnProperty.call(DENSITY_MODE_TO_ROW_HEIGHT, legacyDensity);
176
380
  const legacyFilters = s.filters;
177
381
  const foldFilter = Array.isArray(legacyFilters);
178
382
  const legacyFlags = Object.keys(SHOW_FLAG_TO_USER_ACTION).filter((k) => typeof s[k] === 'boolean');
@@ -181,7 +385,17 @@ export function normalizeListViewSchema(schema) {
181
385
  const foldAria = !!aria && Object.keys(ARIA_KEY_ALIASES).some((k) => aria[k] !== undefined);
182
386
  const sharing = isRecord(s.sharing) ? s.sharing : undefined;
183
387
  const foldSharing = !!sharing && (sharing.visibility !== undefined || sharing.enabled !== undefined);
388
+ // `data: { provider: 'object', object }` → `objectName` (#7477). Gap-fill
389
+ // only: a non-empty `objectName` already on the schema wins, so this can
390
+ // never re-point a binding that resolves today.
391
+ const dataConfig = isRecord(s.data) ? s.data : undefined;
392
+ const dataObjectName = dataConfig?.provider === 'object' && typeof dataConfig.object === 'string' && dataConfig.object
393
+ ? dataConfig.object
394
+ : undefined;
395
+ const foldObjectName = dataObjectName !== undefined && !(typeof s.objectName === 'string' && s.objectName);
184
396
  const viewType = s.viewType;
397
+ // The author's kind, read before the `'grid'` default applies (#7477).
398
+ const authoredViewKind = readListViewKind(s.specType) ?? readListViewKind(s.type);
185
399
  const defaultViewKind = !viewType || viewType === 'list';
186
400
  // The columns array the identity fold will see — mirroring the `foldColumns`
187
401
  // precedence below (canonical `columns` wins; otherwise the legacy `fields`
@@ -195,9 +409,22 @@ export function normalizeListViewSchema(schema) {
195
409
  : undefined;
196
410
  const foldedColumns = normalizeColumnIdentities(columnsSource);
197
411
  const foldColumnIdentity = foldedColumns !== columnsSource;
412
+ // Per-view-type config aliases (#2890 phase-3 carry-over). Collected before
413
+ // the early return so an otherwise-canonical view carrying only a nested
414
+ // legacy key still folds — and so a view carrying none still returns by
415
+ // reference.
416
+ const perViewFolds = Object.entries(PER_VIEW_CONFIG_ALIASES)
417
+ .map(([viewKey, aliases]) => {
418
+ const cfg = isRecord(s[viewKey]) ? s[viewKey] : undefined;
419
+ if (!cfg)
420
+ return undefined;
421
+ const legacyKeys = Object.keys(aliases).filter((legacy) => cfg[legacy] !== undefined);
422
+ return legacyKeys.length ? { viewKey, aliases, cfg, legacyKeys } : undefined;
423
+ })
424
+ .filter((entry) => entry !== undefined);
198
425
  if (!foldColumns && !foldRowHeight && !foldFilter && !legacyFlags.length &&
199
426
  !foldDescription && !foldAria && !foldSharing && !defaultViewKind &&
200
- !foldColumnIdentity) {
427
+ !foldColumnIdentity && !perViewFolds.length && !foldObjectName) {
201
428
  return schema;
202
429
  }
203
430
  const next = { ...s };
@@ -271,7 +498,22 @@ export function normalizeListViewSchema(schema) {
271
498
  delete nextSharing.enabled;
272
499
  next.sharing = nextSharing;
273
500
  }
501
+ for (const { viewKey, aliases, cfg, legacyKeys } of perViewFolds) {
502
+ const nextCfg = { ...cfg };
503
+ for (const legacy of legacyKeys) {
504
+ const canonical = aliases[legacy];
505
+ // Same one-directional shape as every fold above: the canonical key wins
506
+ // when both are present, and the legacy key is REMOVED so a missed
507
+ // read-site fails loudly instead of quietly taking the legacy path.
508
+ if (nextCfg[canonical] === undefined)
509
+ nextCfg[canonical] = cfg[legacy];
510
+ delete nextCfg[legacy];
511
+ }
512
+ next[viewKey] = nextCfg;
513
+ }
514
+ if (foldObjectName)
515
+ next.objectName = dataObjectName;
274
516
  if (defaultViewKind)
275
- next.viewType = 'grid';
517
+ next.viewType = authoredViewKind ?? 'grid';
276
518
  return next;
277
519
  }
@@ -20,11 +20,12 @@
20
20
  * grouped and pointed decimals the US way; and
21
21
  * 2. `useGrouping` was never set, so a four-digit YEAR stored as
22
22
  * `Field.number({ scale: 0 })` rendered as `2,026` — in every locale, with
23
- * no field property able to turn it off.
23
+ * no field property able to turn it off. (That property now exists: the
24
+ * spec's `FieldSchema.useGrouping`, read here since objectui#11026.)
24
25
  *
25
- * Both policies now live here and nowhere else. Call sites bring the value and
26
- * the display width; they do not bring a locale default and they do not decide
27
- * grouping.
26
+ * Both policies now live here and nowhere else. Call sites bring the value, the
27
+ * display width and whatever the FIELD declared (`scale`, `useGrouping`); they
28
+ * do not bring a locale default and they do not decide grouping.
28
29
  *
29
30
  * ── Why this module lives in `@object-ui/core` (objectui#4576) ──
30
31
  * It used to live in `@object-ui/i18n`, and that home cost the repo a real
@@ -57,6 +58,22 @@ export interface DisplayNumberFormatOptions {
57
58
  * for a non-React caller that has no locale in hand. It never means `en-US`:
58
59
  * assuming US conventions for the whole world is the defect this module was
59
60
  * created to remove.
61
+ *
62
+ * ── The caller this rule governs (objectui#10098) ──
63
+ * The `undefined` rule above is written for a NON-REACT display caller with
64
+ * no tag in hand: a plain formatter, an exporter, engine code — anything that
65
+ * cannot call a hook. For that caller the runtime default is the viewer's
66
+ * own environment, and the viewer's own environment is the honest locale for
67
+ * a user-facing display. A malformed tag ends in the same place, whoever the
68
+ * caller: {@link formatDisplayNumber} retries without it rather than
69
+ * throwing, so the number renders in the runtime default instead of taking
70
+ * the cell down.
71
+ *
72
+ * A React renderer is governed by a different rule, and the two do not
73
+ * contradict each other. It passes what `useDisplayLocale` returns, and that
74
+ * hook — `@object-ui/i18n`, `packages/i18n/src/useDisplayLocale.ts` — ends
75
+ * its provider chain on the concrete `'en'`, for determinism, when the chain
76
+ * yields no tag at all. That hook's docblock names this one in return.
60
77
  */
61
78
  locale?: string;
62
79
  /**
@@ -88,14 +105,40 @@ export interface DisplayNumberFormatOptions {
88
105
  *
89
106
  * ⚠️ INTERIM DEFAULT (objectui#4033, PM ruling 2026-08-11). Suppressing
90
107
  * grouping for every scale-0 number is a transitional policy with a known,
91
- * accepted cost: a large scale-0 COUNT loses its separators too. It is the
92
- * better trade only until the spec gains an authorable presentation hint
93
- * (`useGrouping` / `displayFormat` — being specified separately, contract-first,
94
- * in the objectstack repo). When that hint lands it OVERRIDES this default,
95
- * and this heuristic should be reduced to the fallback for fields that
96
- * declare nothing.
108
+ * accepted cost: a large scale-0 COUNT loses its separators too. The
109
+ * authorable hint it was waiting for has landed as
110
+ * {@link DisplayNumberFormatOptions.useGrouping} (objectui#11026): an
111
+ * authored boolean OVERRIDES this default, and the heuristic is now only the
112
+ * fallback for a field that declares nothing.
97
113
  */
98
114
  scale?: number;
115
+ /**
116
+ * The FIELD's authored `useGrouping` — `FieldSchema.useGrouping` in
117
+ * `@objectstack/spec`, the author's digit-grouping hint — and nothing else.
118
+ * Like `scale` it is a POLICY input: pass it only when a field declaration
119
+ * actually said so (objectui#11026).
120
+ *
121
+ * Three-valued, and the spec's own reading of each value is the one this
122
+ * option implements:
123
+ *
124
+ * - `undefined` — the author made no judgement, so the renderer decides:
125
+ * the `scale`/`currency` heuristic in {@link shouldGroupDisplayNumber},
126
+ * unchanged.
127
+ * - `false` — the author's explicit opt-out (a year, an ID, a zip code).
128
+ * Never grouped, whatever `scale` or `currency` would have suggested.
129
+ * - `true` — the author pins grouping ON, overriding the heuristic the
130
+ * other way (a scale-0 count that should read `2,026`).
131
+ *
132
+ * The spec maps the key 1:1 onto `Intl.NumberFormat`'s `useGrouping`, so an
133
+ * authored `true` reaches `Intl` as `true`, which means "always". That is
134
+ * deliberately NOT what the heuristic's own "group" answer does: it omits
135
+ * the key and keeps the locale's "auto" (see the `Intl` note inside
136
+ * {@link formatDisplayNumber}). The two differ where a locale leaves a
137
+ * four-digit number alone under "auto" (es-ES and pl-PL render `1234`); an
138
+ * author who pinned grouping on gets `1.234` / `1 234` there, because that
139
+ * is what they declared.
140
+ */
141
+ useGrouping?: boolean;
99
142
  /**
100
143
  * How the number relates to a percentage — the ONE place that distinction is
101
144
  * expressed, because getting it wrong is the whole of objectui#4576.
@@ -111,9 +154,13 @@ export interface DisplayNumberFormatOptions {
111
154
  * repo's runner, `80.175` percentage points formatted to 2 decimals renders
112
155
  * `80.18%` when formatted directly and `80.17%` after the divide-and-remultiply
113
156
  * — 27,581 of 1,200,013 ordinary-magnitude en-US forms move, plus every
114
- * value at the top of the double range (`MAX_SAFE_INTEGER`, `1e23`). A caller
115
- * holding percentage points must therefore NOT reach for `'percent'`; that is
116
- * exactly the trap this option removes.
157
+ * value at the top of the double range (`MAX_SAFE_INTEGER`, `1e23`). That
158
+ * count is objectui#4576's tie-dense grid — 0.005 steps to 2,000, precisions
159
+ * 0/1/2 — measured on `formatDisplayNumber` / `formatMeasure`'s call shape.
160
+ * objectui#4590 re-measured the same route through `formatPercent` and
161
+ * reports 27,577 of 1,200,003: a different form set, not a correction of this
162
+ * one. A caller holding percentage points must therefore NOT reach for
163
+ * `'percent'`; that is exactly the trap this option removes.
117
164
  *
118
165
  * `'percentPoints'` is implemented with `Intl`'s `style: 'unit'` /
119
166
  * `unit: 'percent'`, which was measured to produce a BYTE-IDENTICAL percent
@@ -133,11 +180,17 @@ export interface DisplayNumberFormatOptions {
133
180
  * The grouping policy, alone and testable: does this number get thousands
134
181
  * separators?
135
182
  *
136
- * @param scale the field's declared `scale`, or `undefined` when the caller
137
- * has no field declaration behind it
138
- * @param currency ISO 4217 code when the number is money
183
+ * This is the ONE place the decision is made. An authored `useGrouping`
184
+ * answers it outright; only when the field declared nothing does the
185
+ * `scale`/`currency` heuristic below get a say (objectui#11026).
186
+ *
187
+ * @param scale the field's declared `scale`, or `undefined` when the
188
+ * caller has no field declaration behind it
189
+ * @param currency ISO 4217 code when the number is money
190
+ * @param useGrouping the field's authored `useGrouping`, or `undefined` when
191
+ * the author declared none
139
192
  */
140
- export declare function shouldGroupDisplayNumber(scale?: number, currency?: string): boolean;
193
+ export declare function shouldGroupDisplayNumber(scale?: number, currency?: string, useGrouping?: boolean): boolean;
141
194
  /**
142
195
  * Format a number for DISPLAY, in the active locale, under the grouping policy
143
196
  * above.