@object-ui/react 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 (103) hide show
  1. package/CHANGELOG.md +4263 -0
  2. package/README.md +383 -42
  3. package/dist/SchemaRenderer.d.ts +64 -7
  4. package/dist/SchemaRenderer.js +1326 -106
  5. package/dist/context/ActionContext.js +40 -1
  6. package/dist/context/AppShellContext.d.ts +27 -0
  7. package/dist/context/DiscussionContext.d.ts +27 -0
  8. package/dist/context/DiscussionContext.js +4 -0
  9. package/dist/context/DrillNavigationContext.d.ts +13 -0
  10. package/dist/context/InlineEditContext.d.ts +151 -1
  11. package/dist/context/InlineEditContext.js +36 -2
  12. package/dist/context/PrintPageContext.d.ts +60 -0
  13. package/dist/context/PrintPageContext.js +32 -0
  14. package/dist/context/RecordContext.d.ts +48 -5
  15. package/dist/context/RecordContext.js +10 -1
  16. package/dist/context/RelatedRecordActionsContext.d.ts +72 -2
  17. package/dist/context/RelatedRecordActionsContext.js +4 -0
  18. package/dist/context/SchemaRendererContext.d.ts +61 -6
  19. package/dist/context/SchemaRendererContext.js +39 -6
  20. package/dist/context/ThemeContext.js +1 -1
  21. package/dist/context/actionRunnerTranslator.d.ts +27 -0
  22. package/dist/context/actionRunnerTranslator.js +37 -0
  23. package/dist/context/index.d.ts +1 -0
  24. package/dist/context/index.js +1 -0
  25. package/dist/element-data-source/ElementApiDataSourcePanel.d.ts +28 -0
  26. package/dist/element-data-source/ElementApiDataSourcePanel.js +10 -0
  27. package/dist/element-data-source/ElementDataSourceGate.d.ts +118 -3
  28. package/dist/element-data-source/ElementDataSourceGate.js +401 -19
  29. package/dist/element-data-source/useElementApiDataSource.d.ts +119 -0
  30. package/dist/element-data-source/useElementApiDataSource.js +198 -0
  31. package/dist/hooks/index.d.ts +3 -1
  32. package/dist/hooks/index.js +10 -1
  33. package/dist/hooks/useActionEngine.js +46 -3
  34. package/dist/hooks/useActionRunner.js +4 -0
  35. package/dist/hooks/useActionTextLocalizer.d.ts +35 -9
  36. package/dist/hooks/useActionTextLocalizer.js +70 -14
  37. package/dist/hooks/useConfigBagEvaluator.d.ts +31 -0
  38. package/dist/hooks/useConfigBagEvaluator.js +40 -0
  39. package/dist/hooks/useDatasetDimensionLabels.js +25 -10
  40. package/dist/hooks/useDensityMode.d.ts +8 -2
  41. package/dist/hooks/useDensityMode.js +8 -2
  42. package/dist/hooks/useDiscovery.d.ts +1 -10
  43. package/dist/hooks/useETagCache.js +25 -4
  44. package/dist/hooks/useElementDataSource.d.ts +15 -2
  45. package/dist/hooks/useElementDataSource.js +24 -7
  46. package/dist/hooks/useExpression.d.ts +52 -59
  47. package/dist/hooks/useExpression.js +58 -90
  48. package/dist/hooks/useFilterScope.d.ts +22 -2
  49. package/dist/hooks/useFilterScope.js +29 -4
  50. package/dist/hooks/useGlobalUndo.d.ts +10 -0
  51. package/dist/hooks/useGlobalUndo.js +112 -46
  52. package/dist/hooks/useNavigationOverlay.d.ts +76 -11
  53. package/dist/hooks/useNavigationOverlay.js +119 -9
  54. package/dist/hooks/useOffline.js +65 -11
  55. package/dist/hooks/usePageAssignment.d.ts +33 -7
  56. package/dist/hooks/usePageAssignment.js +85 -21
  57. package/dist/hooks/useRecordSearch.d.ts +34 -1
  58. package/dist/hooks/useRecordSearch.js +42 -7
  59. package/dist/hooks/useResolvedFilter.d.ts +46 -0
  60. package/dist/hooks/useResolvedFilter.js +130 -0
  61. package/dist/hooks/useSchemaPersistence.d.ts +7 -0
  62. package/dist/hooks/useSchemaPersistence.js +144 -5
  63. package/dist/hooks/useSettledSchema.d.ts +123 -0
  64. package/dist/hooks/useSettledSchema.js +186 -0
  65. package/dist/hooks/useViewData.d.ts +10 -3
  66. package/dist/hooks/useViewData.js +5 -14
  67. package/dist/index.d.ts +9 -1
  68. package/dist/index.js +48 -1
  69. package/dist/schema-input.d.ts +38 -8
  70. package/dist/schema-input.js +39 -7
  71. package/dist/utils/configBag.d.ts +68 -0
  72. package/dist/utils/configBag.js +70 -0
  73. package/dist/utils/error-message.d.ts +1 -1
  74. package/dist/utils/error-message.js +1 -1
  75. package/dist/utils/inlineAria.d.ts +61 -0
  76. package/dist/utils/inlineAria.js +70 -0
  77. package/dist/utils/localeMap.d.ts +27 -0
  78. package/dist/utils/localeMap.js +106 -0
  79. package/dist/utils/nonGridRowCeiling.d.ts +44 -0
  80. package/dist/utils/nonGridRowCeiling.js +78 -0
  81. package/dist/utils/paramsBag.d.ts +28 -0
  82. package/dist/utils/paramsBag.js +146 -0
  83. package/dist/utils/propsBagDiagnostic.d.ts +196 -0
  84. package/dist/utils/propsBagDiagnostic.js +422 -0
  85. package/dist/utils/refusedDataPropDiagnostic.d.ts +44 -0
  86. package/dist/utils/refusedDataPropDiagnostic.js +94 -0
  87. package/dist/utils/unevaluatedExpression.d.ts +44 -1
  88. package/dist/utils/unevaluatedExpression.js +92 -11
  89. package/dist/utils/visibilityDiagnostic.d.ts +282 -21
  90. package/dist/utils/visibilityDiagnostic.js +321 -46
  91. package/package.json +10 -10
  92. package/dist/hooks/useClientNotifications.d.ts +0 -47
  93. package/dist/hooks/useClientNotifications.js +0 -162
  94. package/dist/spec-bridge/SpecBridge.d.ts +0 -28
  95. package/dist/spec-bridge/SpecBridge.js +0 -56
  96. package/dist/spec-bridge/bridges/form-view.d.ts +0 -158
  97. package/dist/spec-bridge/bridges/form-view.js +0 -170
  98. package/dist/spec-bridge/bridges/list-view.d.ts +0 -20
  99. package/dist/spec-bridge/bridges/list-view.js +0 -268
  100. package/dist/spec-bridge/index.d.ts +0 -11
  101. package/dist/spec-bridge/index.js +0 -10
  102. package/dist/spec-bridge/types.d.ts +0 -55
  103. package/dist/spec-bridge/types.js +0 -8
package/dist/index.js CHANGED
@@ -10,15 +10,62 @@ export * from './schema-input.js';
10
10
  export * from './hooks/index.js'; // will be empty for now
11
11
  export * from './context/index.js'; // will be empty for now
12
12
  export * from './LazyPluginLoader.js';
13
- export * from './spec-bridge/index.js';
14
13
  export * from './data-invalidation.js';
15
14
  // PageComponentSchema.dataSource — mapping the spec's per-element data binding
16
15
  // onto the schema keys each object-bound block reads (objectstack#6953).
17
16
  export * from './element-data-source/ElementDataSourceGate.js';
17
+ // PageComponentSchema.dataSource as the spec's API binding — the read behind
18
+ // the `data` root `SchemaRenderer` binds for it (objectui#12197).
19
+ export * from './element-data-source/useElementApiDataSource.js';
18
20
  // i18n utilities
19
21
  export { resolveKeyedI18nLabel } from './utils/i18n.js';
22
+ // The spec's NESTED `aria` bag (`AriaPropsSchema`) → `aria-label` /
23
+ // `aria-describedby` / `role`, with `ariaLabel` resolved as an inline locale
24
+ // map (objectui#11051). Exported for the renderers in `@object-ui/components`,
25
+ // which depends on this package, so the mapping is written once. It is not the
26
+ // flat keyed reader `SchemaRenderer` applies to every node; objectui#4580 Q2-B
27
+ // keeps the two vocabularies apart.
28
+ export { resolveInlineAriaProps } from './utils/inlineAria.js';
29
+ // "Is this a real config bag?" — the ONE definition of that question
30
+ // (objectui#6761), exported at the package entry for the same reason the node-gate
31
+ // reporter above is (objectui#6038): a surface in `@object-ui/components`, which
32
+ // depends on this package, asks it too. `element:*` renderers there read their
33
+ // config out of `{ ...schema.props, ...schema.properties }`, the third channel of
34
+ // the degenerate-bag hazard objectui#6752 / objectui#6760 closed here
35
+ // (objectui#6783). A second copy one package over would be a spelling that
36
+ // `utils/configBag.pin.test.ts` — which scans `packages/react/src` — cannot see,
37
+ // which is the drift that pin exists to stop.
38
+ export { isConfigBag } from './utils/configBag.js';
39
+ // Node-gate predicate diagnostics. Exported at the package entry (objectui#6038)
40
+ // so every surface that evaluates a node `visibleWhen` reports a fault through
41
+ // ONE reporter and ONE dedupe `Set` — `page:tabs` item predicates live in
42
+ // `@object-ui/components`, which depends on this package. A second copy of the
43
+ // reporter would mean a second rate limit, and the same broken predicate would
44
+ // then be entitled to one line per package instead of one line, which is the
45
+ // property the 2026-08-25 ruling asked for.
46
+ export { reportUnresolvableVisibilityPredicate, formatUnresolvableVisibilityMessage, UNRESOLVABLE_VISIBILITY_PREFIX,
47
+ // The ENABLEMENT gate's opening line (objectui#6445). Exported beside its
48
+ // sibling for the reason that one is: an app filtering these out of its
49
+ // console transport filters by the constant, and a second line it cannot
50
+ // name is a second thing to hard-code. Not a second reporter — the emit,
51
+ // the dedupe `Set` and the reset above are still the only ones.
52
+ UNRESOLVABLE_ENABLEMENT_PREFIX, __resetVisibilityPredicateWarnings, } from './utils/visibilityDiagnostic.js';
20
53
  // Write-error surfacing utilities (shared by drag-write plugins so a failed
21
54
  // PATCH — e.g. an RLS 403 — is never silently swallowed).
22
55
  export { extractWriteErrorMessage, isPermissionError, extractFieldErrors, classifyLoadError, declaredUserMessage } from './utils/error-message.js';
23
56
  // Built-in i18n support
24
57
  export { I18nProvider, useObjectTranslation, useSafeTranslate, useObjectLabel, useSafeFieldLabel, useI18nContext, createI18n, getDirection, getAvailableLanguages, formatDate, formatDateTime, formatRelativeTime, formatCurrency, formatNumber, } from '@object-ui/i18n';
58
+ // The platform row ceiling for NON-GRID visualisations — gantt, calendar, map
59
+ // and tree (objectui#7210, maintainer ruling a′). One constant across the
60
+ // four, because a per-plugin copy would be four constants wearing one name,
61
+ // which is the thing the ruling ruled out.
62
+ //
63
+ // HOME: `@object-ui/core`, beside the `extractRecords` it wraps (objectui#7508,
64
+ // maintainer ruling A′). This entry re-exports the three names it published
65
+ // first, the same way it re-exports `@object-ui/i18n` above, and keeps only
66
+ // the React half — `NonGridRowCeilingNote`, which takes the
67
+ // `NonGridCeilingResult` and nothing else. The probe row's `+ 1` lives in
68
+ // core's `nonGridRowCeilingQuery()` and nowhere else; the constant that used
69
+ // to carry it here is deleted, not re-exported.
70
+ export { NON_GRID_ROW_CEILING, applyNonGridRowCeiling } from '@object-ui/core';
71
+ export { NonGridRowCeilingNote } from './utils/nonGridRowCeiling.js';
@@ -5,23 +5,53 @@
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 { BaseSchema } from '@object-ui/types';
8
+ import type { SchemaNode } from '@object-ui/types';
9
9
  import type { SchemaRendererProps } from './SchemaRenderer.js';
10
10
  /**
11
11
  * Narrow a loosely-typed metadata node onto {@link SchemaRendererProps.schema}.
12
12
  *
13
13
  * `@object-ui/types` declares `SchemaNode` as
14
- * `BaseSchema | string | number | boolean | null | undefined`, and a lot of
15
- * metadata plumbing (page regions, header-bar actions, detail tabs, view
14
+ * `DeclaredNode | string | number | boolean | null | undefined` (its object
15
+ * arm the declared-node union, objectui#11466), and a lot of metadata plumbing (page regions, header-bar actions, detail tabs, view
16
16
  * configs) is typed with it. `SchemaRenderer` deliberately does NOT declare the
17
17
  * `number` / `boolean` members — nobody should be invited to author them — so
18
18
  * forwarding such a value needs one honest step in between.
19
19
  *
20
20
  * This is that step, and it is a TOTAL function rather than a cast: it maps the
21
- * two primitive members onto their text form, which is precisely what the
22
- * renderer's own defensive branch does with them. So it changes no behaviour and
23
- * tells no lie — the value a caller forwards renders identically whether or not
24
- * it passes through here.
21
+ * two primitive members onto whatever `SchemaRenderer` would itself render for
22
+ * them. So it changes no behaviour and tells no lie — the value a caller
23
+ * forwards renders identically whether or not it passes through here.
24
+ *
25
+ * ## Why the mapping has TWO legs and not one
26
+ *
27
+ * Because the renderer's own narrowing has two, in a fixed order: the
28
+ * `!evaluatedSchema` leg returns nothing and runs FIRST, and only below it does
29
+ * the defensive branch return the value's `String` form. So the falsy
30
+ * primitives — `0`, `-0`, `NaN`, `false` — never reach that branch at all; they
31
+ * render as nothing. `!node` is exactly that set for the two members this
32
+ * function converts, so the ternary below IS the renderer's order, restated for
33
+ * a value that has to cross the type boundary as a string.
34
+ *
35
+ * ⚠️ The single-leg `String(node)` that stood here until objectui#8908 was the
36
+ * lie this paragraph used to tell. `String(0)` and `String(false)` are the
37
+ * NON-EMPTY strings `'0'` and `'false'`, which are truthy, so they sailed past
38
+ * the renderer's first leg and printed themselves — while the same two values
39
+ * handed to `SchemaRenderer` directly render nothing, pinned since
40
+ * objectui#4548 in `packages/react/src/__tests__/SchemaRenderer.primitiveSchema.test.tsx`.
41
+ * The shipped `empty` renderer, whose `action` slot gates on nullish rather than
42
+ * truthiness, printed a stray `0` for `{ type: 'empty', action: 0 }` because of
43
+ * it. ⛔ Do not "simplify" the two legs back into one.
44
+ *
45
+ * ## Why the falsy leg returns nothing rather than the value itself
46
+ *
47
+ * Passing `0` / `false` through UNCHANGED would widen this function's return
48
+ * type to admit `number` / `boolean`, and that is not a free choice: the return
49
+ * type is pinned exactly equal to `SchemaRendererProps['schema']` by
50
+ * `packages/react/src/__tests__/SchemaRenderer.propsResolution.test.ts`, which
51
+ * also pins that the prop admits NEITHER member (objectui#4548 ruling Q2). A
52
+ * widened return would therefore be a TS2322 at every call site that forwards
53
+ * it straight into `<SchemaRenderer schema={…} />`, which is all of them. The
54
+ * nullish leg is the spelling that keeps the guarantee and the type both.
25
55
  *
26
56
  * The bridge is PERMANENT — not scaffolding awaiting a merge. The two competing
27
57
  * repo-wide `SchemaNode` spellings ARE reconciled (objectui#4580, PR #4608):
@@ -39,4 +69,4 @@ import type { SchemaRendererProps } from './SchemaRenderer.js';
39
69
  * PR #4621 routed all five through here. An earlier revision of this paragraph
40
70
  * said the reconciliation was still pending and invited exactly that edit.
41
71
  */
42
- export declare function toRenderableSchema(node: BaseSchema | string | number | boolean | null | undefined): SchemaRendererProps['schema'];
72
+ export declare function toRenderableSchema(node: SchemaNode): SchemaRendererProps['schema'];
@@ -9,17 +9,47 @@
9
9
  * Narrow a loosely-typed metadata node onto {@link SchemaRendererProps.schema}.
10
10
  *
11
11
  * `@object-ui/types` declares `SchemaNode` as
12
- * `BaseSchema | string | number | boolean | null | undefined`, and a lot of
13
- * metadata plumbing (page regions, header-bar actions, detail tabs, view
12
+ * `DeclaredNode | string | number | boolean | null | undefined` (its object
13
+ * arm the declared-node union, objectui#11466), and a lot of metadata plumbing (page regions, header-bar actions, detail tabs, view
14
14
  * configs) is typed with it. `SchemaRenderer` deliberately does NOT declare the
15
15
  * `number` / `boolean` members — nobody should be invited to author them — so
16
16
  * forwarding such a value needs one honest step in between.
17
17
  *
18
18
  * This is that step, and it is a TOTAL function rather than a cast: it maps the
19
- * two primitive members onto their text form, which is precisely what the
20
- * renderer's own defensive branch does with them. So it changes no behaviour and
21
- * tells no lie — the value a caller forwards renders identically whether or not
22
- * it passes through here.
19
+ * two primitive members onto whatever `SchemaRenderer` would itself render for
20
+ * them. So it changes no behaviour and tells no lie — the value a caller
21
+ * forwards renders identically whether or not it passes through here.
22
+ *
23
+ * ## Why the mapping has TWO legs and not one
24
+ *
25
+ * Because the renderer's own narrowing has two, in a fixed order: the
26
+ * `!evaluatedSchema` leg returns nothing and runs FIRST, and only below it does
27
+ * the defensive branch return the value's `String` form. So the falsy
28
+ * primitives — `0`, `-0`, `NaN`, `false` — never reach that branch at all; they
29
+ * render as nothing. `!node` is exactly that set for the two members this
30
+ * function converts, so the ternary below IS the renderer's order, restated for
31
+ * a value that has to cross the type boundary as a string.
32
+ *
33
+ * ⚠️ The single-leg `String(node)` that stood here until objectui#8908 was the
34
+ * lie this paragraph used to tell. `String(0)` and `String(false)` are the
35
+ * NON-EMPTY strings `'0'` and `'false'`, which are truthy, so they sailed past
36
+ * the renderer's first leg and printed themselves — while the same two values
37
+ * handed to `SchemaRenderer` directly render nothing, pinned since
38
+ * objectui#4548 in `packages/react/src/__tests__/SchemaRenderer.primitiveSchema.test.tsx`.
39
+ * The shipped `empty` renderer, whose `action` slot gates on nullish rather than
40
+ * truthiness, printed a stray `0` for `{ type: 'empty', action: 0 }` because of
41
+ * it. ⛔ Do not "simplify" the two legs back into one.
42
+ *
43
+ * ## Why the falsy leg returns nothing rather than the value itself
44
+ *
45
+ * Passing `0` / `false` through UNCHANGED would widen this function's return
46
+ * type to admit `number` / `boolean`, and that is not a free choice: the return
47
+ * type is pinned exactly equal to `SchemaRendererProps['schema']` by
48
+ * `packages/react/src/__tests__/SchemaRenderer.propsResolution.test.ts`, which
49
+ * also pins that the prop admits NEITHER member (objectui#4548 ruling Q2). A
50
+ * widened return would therefore be a TS2322 at every call site that forwards
51
+ * it straight into `<SchemaRenderer schema={…} />`, which is all of them. The
52
+ * nullish leg is the spelling that keeps the guarantee and the type both.
23
53
  *
24
54
  * The bridge is PERMANENT — not scaffolding awaiting a merge. The two competing
25
55
  * repo-wide `SchemaNode` spellings ARE reconciled (objectui#4580, PR #4608):
@@ -38,5 +68,7 @@
38
68
  * said the reconciliation was still pending and invited exactly that edit.
39
69
  */
40
70
  export function toRenderableSchema(node) {
41
- return typeof node === 'number' || typeof node === 'boolean' ? String(node) : node;
71
+ return typeof node === 'number' || typeof node === 'boolean'
72
+ ? (node ? String(node) : undefined)
73
+ : node;
42
74
  }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * Is this value a real config bag — an object, and not an array pretending to
10
+ * be one? (objectui#6761)
11
+ *
12
+ * This is the ONE definition of that question in `@object-ui/react`, and it
13
+ * lives here rather than in whichever module happened to need it first,
14
+ * because every module that asks it is a CONSUMER of the same authored shape:
15
+ * the `properties` / `props` bags a node may carry. `typeof null === 'object'`
16
+ * is covered by the truthiness test.
17
+ *
18
+ * ## Why one definition, when all the copies agreed
19
+ *
20
+ * They agreed; that was never the cost. The cost is that disagreement between
21
+ * them produces NO error — each copy is a boolean expression, and a copy that
22
+ * drifts answers a different question on ONE channel while every other channel
23
+ * keeps the old answer. That is the same failure mode `hasDeclaredPredicate`
24
+ * (`@object-ui/core`) was created to end for "is a gate DECLARED?", which had
25
+ * three spellings with three different SCOPES before objectui#3850 —
26
+ * `disabled: null` greying a control out on one path and not another
27
+ * (objectui#3862).
28
+ *
29
+ * ## The spellings this replaces, measured on `b98352a15`
30
+ *
31
+ * Six occurrences, four spellings, all in `packages/react/src`:
32
+ *
33
+ * 1. `SchemaRenderer`'s module-private `isConfigBag` — itself already the
34
+ * merge of the `properties` evaluation guard and `propsWithoutCanonicalKeys`
35
+ * (objectui#6752, which unified those two because it had to touch both,
36
+ * and filed the rest as objectui#6761 rather than smuggling it in);
37
+ * 2. `SchemaRenderer`'s `winningVisibilityKey`, inline, spelled with
38
+ * `!= null` where the others spell truthiness;
39
+ * 3. `utils/propsBagDiagnostic.ts`'s module-private twin (objectui#6708);
40
+ * 4. `utils/unevaluatedExpression.ts`'s `scanBag` early return, spelled
41
+ * NEGATED (objectui#4795);
42
+ * 5. `utils/unevaluatedExpression.ts`'s `hoisted`, in the same file as (4)
43
+ * and spelled positively — this one is not in objectui#6761's inventory
44
+ * of five, which counted the file once; six occurrences is the count
45
+ * measured on this branch's base, and it was six on the card's base
46
+ * (`b76ca6764`) too.
47
+ *
48
+ * `!= null` (2) and truthiness (1, 3, 5) cannot disagree HERE, and the
49
+ * difference is worth naming rather than smoothing over: they part company
50
+ * only on a falsy value, and every falsy value except `document.all` fails
51
+ * `typeof === 'object'` in the very next conjunct. The texts differ; the
52
+ * answers cannot.
53
+ *
54
+ * ## What this is NOT
55
+ *
56
+ * Not a general "is this a plain object?" utility, and deliberately not shared
57
+ * with the two sites that ask the same SHAPE question about a data ROW —
58
+ * `SchemaRenderer`'s `boundRecord` scope entry and
59
+ * `usePredicateRecordContext` (`hooks/useExpression.ts`). Those answer "did a
60
+ * row bind?", and their answer has its own pinned meaning: binding NOTHING
61
+ * rather than an empty row is what keeps a host-supplied `record` from being
62
+ * shadowed. Should "config bag" ever narrow (rejecting a class instance, say),
63
+ * the row sites must NOT follow — merging them would make that a single edit
64
+ * with two rulings behind it. The pin in `configBag.pin.test.ts` names both
65
+ * sites with this reason, so the next reader finds a decision rather than an
66
+ * oversight.
67
+ */
68
+ export declare function isConfigBag(value: unknown): value is Record<string, unknown>;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * Is this value a real config bag — an object, and not an array pretending to
10
+ * be one? (objectui#6761)
11
+ *
12
+ * This is the ONE definition of that question in `@object-ui/react`, and it
13
+ * lives here rather than in whichever module happened to need it first,
14
+ * because every module that asks it is a CONSUMER of the same authored shape:
15
+ * the `properties` / `props` bags a node may carry. `typeof null === 'object'`
16
+ * is covered by the truthiness test.
17
+ *
18
+ * ## Why one definition, when all the copies agreed
19
+ *
20
+ * They agreed; that was never the cost. The cost is that disagreement between
21
+ * them produces NO error — each copy is a boolean expression, and a copy that
22
+ * drifts answers a different question on ONE channel while every other channel
23
+ * keeps the old answer. That is the same failure mode `hasDeclaredPredicate`
24
+ * (`@object-ui/core`) was created to end for "is a gate DECLARED?", which had
25
+ * three spellings with three different SCOPES before objectui#3850 —
26
+ * `disabled: null` greying a control out on one path and not another
27
+ * (objectui#3862).
28
+ *
29
+ * ## The spellings this replaces, measured on `b98352a15`
30
+ *
31
+ * Six occurrences, four spellings, all in `packages/react/src`:
32
+ *
33
+ * 1. `SchemaRenderer`'s module-private `isConfigBag` — itself already the
34
+ * merge of the `properties` evaluation guard and `propsWithoutCanonicalKeys`
35
+ * (objectui#6752, which unified those two because it had to touch both,
36
+ * and filed the rest as objectui#6761 rather than smuggling it in);
37
+ * 2. `SchemaRenderer`'s `winningVisibilityKey`, inline, spelled with
38
+ * `!= null` where the others spell truthiness;
39
+ * 3. `utils/propsBagDiagnostic.ts`'s module-private twin (objectui#6708);
40
+ * 4. `utils/unevaluatedExpression.ts`'s `scanBag` early return, spelled
41
+ * NEGATED (objectui#4795);
42
+ * 5. `utils/unevaluatedExpression.ts`'s `hoisted`, in the same file as (4)
43
+ * and spelled positively — this one is not in objectui#6761's inventory
44
+ * of five, which counted the file once; six occurrences is the count
45
+ * measured on this branch's base, and it was six on the card's base
46
+ * (`b76ca6764`) too.
47
+ *
48
+ * `!= null` (2) and truthiness (1, 3, 5) cannot disagree HERE, and the
49
+ * difference is worth naming rather than smoothing over: they part company
50
+ * only on a falsy value, and every falsy value except `document.all` fails
51
+ * `typeof === 'object'` in the very next conjunct. The texts differ; the
52
+ * answers cannot.
53
+ *
54
+ * ## What this is NOT
55
+ *
56
+ * Not a general "is this a plain object?" utility, and deliberately not shared
57
+ * with the two sites that ask the same SHAPE question about a data ROW —
58
+ * `SchemaRenderer`'s `boundRecord` scope entry and
59
+ * `usePredicateRecordContext` (`hooks/useExpression.ts`). Those answer "did a
60
+ * row bind?", and their answer has its own pinned meaning: binding NOTHING
61
+ * rather than an empty row is what keeps a host-supplied `record` from being
62
+ * shadowed. Should "config bag" ever narrow (rejecting a class instance, say),
63
+ * the row sites must NOT follow — merging them would make that a single edit
64
+ * with two rulings behind it. The pin in `configBag.pin.test.ts` names both
65
+ * sites with this reason, so the next reader finds a decision rather than an
66
+ * oversight.
67
+ */
68
+ export function isConfigBag(value) {
69
+ return !!value && typeof value === 'object' && !Array.isArray(value);
70
+ }
@@ -16,7 +16,7 @@ export declare function extractWriteErrorMessage(err: unknown): string | null;
16
16
  * The refusal text the PRODUCER explicitly marked as addressed to the end user,
17
17
  * or `null` when the refusal carries no marking.
18
18
  *
19
- * `userMessage` is the opt-in channel added by objectstack#9934 (maintainer
19
+ * `userMessage` is the opt-in channel added by objectstack `79c46da90` (maintainer
20
20
  * ruling 2026-08-19 on objectui#5210), declared on `ApiErrorSchema` /
21
21
  * `EnhancedApiErrorSchema` in the pinned `@objectstack/spec`.
22
22
  * Three properties of the contract decide this reader's whole shape:
@@ -74,7 +74,7 @@ export function extractWriteErrorMessage(err) {
74
74
  * The refusal text the PRODUCER explicitly marked as addressed to the end user,
75
75
  * or `null` when the refusal carries no marking.
76
76
  *
77
- * `userMessage` is the opt-in channel added by objectstack#9934 (maintainer
77
+ * `userMessage` is the opt-in channel added by objectstack `79c46da90` (maintainer
78
78
  * ruling 2026-08-19 on objectui#5210), declared on `ApiErrorSchema` /
79
79
  * `EnhancedApiErrorSchema` in the pinned `@objectstack/spec`.
80
80
  * Three properties of the contract decide this reader's whole shape:
@@ -0,0 +1,61 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ import type { AriaProps } from '@objectstack/spec/ui';
9
+ /**
10
+ * Resolves the spec's NESTED `aria` bag to the DOM attributes it declares
11
+ * (objectui#11051).
12
+ *
13
+ * The bag is `@objectstack/spec`'s `AriaPropsSchema`, the `aria` member a
14
+ * block's own props schema carries (`element:text`, `element:button`,
15
+ * `element:image` and `element:number` among them). It declares three keys,
16
+ * and each maps to one attribute:
17
+ *
18
+ * `ariaLabel` → `aria-label`, resolved with the spec's own
19
+ * `resolveI18nLabel` against `locale`: a plain string is
20
+ * used as written, and an inline locale map
21
+ * (`{ en: 'Order total', 'zh-CN': '订单合计' }`) gives the
22
+ * entry for the viewer's language;
23
+ * `ariaDescribedBy` → `aria-describedby`;
24
+ * `role` → `role`.
25
+ *
26
+ * A key that resolves to nothing, or to an empty string, is left out of the
27
+ * result instead of being written as an empty or `undefined` attribute. A key
28
+ * the bag does not declare is not read: the spec refuses such keys when the
29
+ * document is parsed, so this function adds no alias for them.
30
+ *
31
+ * ## Two vocabularies, two readers (objectui#4580 Q2-B)
32
+ *
33
+ * This is NOT `SchemaRenderer`'s `resolveAriaProps`. That one reads the FLAT
34
+ * node keys (`BaseSchema.ariaLabel` / `ariaDescribedBy` / `role`) and resolves
35
+ * `ariaLabel` in objectui's KEYED vocabulary (`{ key, defaultValue?, params? }`,
36
+ * via `resolveKeyedI18nLabel`). The nested bag uses the INLINE locale map.
37
+ * Neither resolver accepts the other's shape: the keyed one returns `undefined`
38
+ * for a locale map, and the spec's one treats `key` / `defaultValue` as locale
39
+ * tags. So the two readers stay separate, as objectui#4580 Q2-B ruled.
40
+ *
41
+ * ## Why a pure function and not a hook
42
+ *
43
+ * The caller passes the locale; this function reads no React context. That
44
+ * lets a renderer call it anywhere in its body, including after an early
45
+ * return, and lets code outside React call it too. Renderers pass
46
+ * `useDisplayLocale()` from `@object-ui/i18n`, the locale every
47
+ * `resolveI18nLabel` read site in this repo resolves against (objectui#4580
48
+ * revised Q1-A). Like the spec's resolver, `locale` is a required parameter
49
+ * that may be `undefined` (which resolves as `en`), so a caller cannot forget
50
+ * it by accident.
51
+ *
52
+ * @param aria - The authored bag, taken directly from the node's config. A
53
+ * missing bag, or a value that is not an object, gives `{}`.
54
+ * @param locale - BCP-47 tag of the viewer's language, e.g. `zh-CN`.
55
+ * @returns The attributes to spread onto the element that carries the name.
56
+ */
57
+ export declare function resolveInlineAriaProps(aria: AriaProps | null | undefined, locale: string | undefined): {
58
+ 'aria-label'?: string;
59
+ 'aria-describedby'?: string;
60
+ role?: string;
61
+ };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ import { resolveI18nLabel as resolveInlineI18nLabel } from '@objectstack/spec/ui';
9
+ /**
10
+ * Resolves the spec's NESTED `aria` bag to the DOM attributes it declares
11
+ * (objectui#11051).
12
+ *
13
+ * The bag is `@objectstack/spec`'s `AriaPropsSchema`, the `aria` member a
14
+ * block's own props schema carries (`element:text`, `element:button`,
15
+ * `element:image` and `element:number` among them). It declares three keys,
16
+ * and each maps to one attribute:
17
+ *
18
+ * `ariaLabel` → `aria-label`, resolved with the spec's own
19
+ * `resolveI18nLabel` against `locale`: a plain string is
20
+ * used as written, and an inline locale map
21
+ * (`{ en: 'Order total', 'zh-CN': '订单合计' }`) gives the
22
+ * entry for the viewer's language;
23
+ * `ariaDescribedBy` → `aria-describedby`;
24
+ * `role` → `role`.
25
+ *
26
+ * A key that resolves to nothing, or to an empty string, is left out of the
27
+ * result instead of being written as an empty or `undefined` attribute. A key
28
+ * the bag does not declare is not read: the spec refuses such keys when the
29
+ * document is parsed, so this function adds no alias for them.
30
+ *
31
+ * ## Two vocabularies, two readers (objectui#4580 Q2-B)
32
+ *
33
+ * This is NOT `SchemaRenderer`'s `resolveAriaProps`. That one reads the FLAT
34
+ * node keys (`BaseSchema.ariaLabel` / `ariaDescribedBy` / `role`) and resolves
35
+ * `ariaLabel` in objectui's KEYED vocabulary (`{ key, defaultValue?, params? }`,
36
+ * via `resolveKeyedI18nLabel`). The nested bag uses the INLINE locale map.
37
+ * Neither resolver accepts the other's shape: the keyed one returns `undefined`
38
+ * for a locale map, and the spec's one treats `key` / `defaultValue` as locale
39
+ * tags. So the two readers stay separate, as objectui#4580 Q2-B ruled.
40
+ *
41
+ * ## Why a pure function and not a hook
42
+ *
43
+ * The caller passes the locale; this function reads no React context. That
44
+ * lets a renderer call it anywhere in its body, including after an early
45
+ * return, and lets code outside React call it too. Renderers pass
46
+ * `useDisplayLocale()` from `@object-ui/i18n`, the locale every
47
+ * `resolveI18nLabel` read site in this repo resolves against (objectui#4580
48
+ * revised Q1-A). Like the spec's resolver, `locale` is a required parameter
49
+ * that may be `undefined` (which resolves as `en`), so a caller cannot forget
50
+ * it by accident.
51
+ *
52
+ * @param aria - The authored bag, taken directly from the node's config. A
53
+ * missing bag, or a value that is not an object, gives `{}`.
54
+ * @param locale - BCP-47 tag of the viewer's language, e.g. `zh-CN`.
55
+ * @returns The attributes to spread onto the element that carries the name.
56
+ */
57
+ export function resolveInlineAriaProps(aria, locale) {
58
+ if (!aria || typeof aria !== 'object')
59
+ return {};
60
+ const out = {};
61
+ const label = resolveInlineI18nLabel(aria.ariaLabel, locale);
62
+ if (label)
63
+ out['aria-label'] = label;
64
+ if (typeof aria.ariaDescribedBy === 'string' && aria.ariaDescribedBy) {
65
+ out['aria-describedby'] = aria.ariaDescribedBy;
66
+ }
67
+ if (typeof aria.role === 'string' && aria.role)
68
+ out.role = aria.role;
69
+ return out;
70
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * Is this value an inline locale map, in `InlineLocaleMapSchema`'s sense?
10
+ *
11
+ * The member loop first is a fast reject and not a second definition: the
12
+ * schema requires every value to be a string, so the loop can only turn away
13
+ * what the schema turns away, and it does so without the schema's per-key
14
+ * issue reporting. Most data objects carry a non-string member.
15
+ */
16
+ export declare function isInlineLocaleMap(value: unknown): value is Record<string, string>;
17
+ /** Visit one member. `locale` is the member's key (`en`, `zh-CN`, `default`). */
18
+ export type LocaleMapMemberVisitor = (member: string, locale: string) => unknown;
19
+ /**
20
+ * Map every member of an inline locale map through `visit` and return the
21
+ * result, spelled as a string: the map itself when nothing changed (copy on
22
+ * write).
23
+ *
24
+ * The evaluation memo passes the evaluator; the dev diagnostic passes a visitor
25
+ * that records the member and returns it unchanged.
26
+ */
27
+ export declare function mapLocaleMapMembers(map: Record<string, string>, visit: LocaleMapMemberVisitor): Record<string, string>;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * Each member of an inline locale map is evaluated like a plain string on the
10
+ * same key (objectui#12936).
11
+ *
12
+ * ## The rule
13
+ *
14
+ * A node's config-bag value that is an INLINE LOCALE MAP - the second
15
+ * authorized form of the spec's `I18nLabel`, for example
16
+ * `title: { en: 'Published ${data.at}', 'zh-CN': '...${data.at}' }` - has
17
+ * every member evaluated by the `SchemaRenderer` evaluation memo, with the same
18
+ * evaluator and so against the same scope as a plain string on that key:
19
+ * `record`, `current_user` / `user`, `page`, the host's ambient roots, and
20
+ * `data` on a node with an API binding. The block then picks one member for
21
+ * the viewer's language (`pickLocalized`) and draws an evaluated string.
22
+ *
23
+ * Before it, the map was a data object to the per-value loops, which hand a
24
+ * data object over as authored, so the member the block picked kept its
25
+ * `${...}` and drew it as text. A plain string with the same hole on the same
26
+ * key was evaluated, so localizing a line was what broke it.
27
+ *
28
+ * ## What a locale map is: the spec's answer, not a third one
29
+ *
30
+ * {@link isInlineLocaleMap} asks `@objectstack/spec`'s `InlineLocaleMapSchema`,
31
+ * the schema the contract declares the form with: every key a BCP-47 tag or
32
+ * `default` (never `key` / `defaultValue`), every value a string. A value with
33
+ * a non-string member, or with a key that is not a locale tag, is NOT a locale
34
+ * map: it stays a data object and is handed over as authored, with nothing
35
+ * inside it evaluated. No other object is walked.
36
+ *
37
+ * ⚠️ The boundary that follows from asking the contract: an object whose keys
38
+ * all parse as locale tags (any two or three letters do, so `{ id, url }`
39
+ * qualifies) and whose values are all strings IS an inline locale map to the
40
+ * spec, and so it is one here too, and a `${...}` in its members is evaluated.
41
+ *
42
+ * ## Why this module exists
43
+ *
44
+ * Two readers must agree on one radius, as they do for `params`
45
+ * (`paramsBag.ts`): the memo that EVALUATES the members, and the dev
46
+ * diagnostic (`unevaluatedExpression.ts`) that REPORTS a member still carrying
47
+ * `${...}` afterwards because its expression threw. Both import this module, so
48
+ * a member that fails to evaluate is reported exactly as a plain string that
49
+ * fails is.
50
+ *
51
+ * ## The result is still a locale map
52
+ *
53
+ * A member's result is spelled as a string. A member that is one whole hole,
54
+ * `'${data.count}'`, evaluates to the number itself, as a plain string does;
55
+ * left a number, the member would be skipped by every `pickLocalized` limb
56
+ * (each reads string entries only) and the viewer would get another locale's
57
+ * line, or nothing. `String(result ?? '')` is the spelling the evaluator
58
+ * already gives an interpolated part of a plain string.
59
+ *
60
+ * ## Copy on write
61
+ *
62
+ * The map is copied only when a member changed, so a map without a hole comes
63
+ * back as the very object that went in, and the authored map is never mutated.
64
+ * Nothing may depend on that identity (AGENTS.md #10).
65
+ */
66
+ import { InlineLocaleMapSchema } from '@objectstack/spec/ui';
67
+ import { isConfigBag } from './configBag.js';
68
+ /**
69
+ * Is this value an inline locale map, in `InlineLocaleMapSchema`'s sense?
70
+ *
71
+ * The member loop first is a fast reject and not a second definition: the
72
+ * schema requires every value to be a string, so the loop can only turn away
73
+ * what the schema turns away, and it does so without the schema's per-key
74
+ * issue reporting. Most data objects carry a non-string member.
75
+ */
76
+ export function isInlineLocaleMap(value) {
77
+ if (!isConfigBag(value))
78
+ return false;
79
+ for (const member of Object.values(value)) {
80
+ if (typeof member !== 'string')
81
+ return false;
82
+ }
83
+ return InlineLocaleMapSchema.safeParse(value).success;
84
+ }
85
+ /**
86
+ * Map every member of an inline locale map through `visit` and return the
87
+ * result, spelled as a string: the map itself when nothing changed (copy on
88
+ * write).
89
+ *
90
+ * The evaluation memo passes the evaluator; the dev diagnostic passes a visitor
91
+ * that records the member and returns it unchanged.
92
+ */
93
+ export function mapLocaleMapMembers(map, visit) {
94
+ let copy;
95
+ for (const locale of Object.keys(map)) {
96
+ const member = map[locale];
97
+ const result = visit(member, locale);
98
+ const next = typeof result === 'string' ? result : String(result ?? '');
99
+ if (next !== member) {
100
+ if (!copy)
101
+ copy = { ...map };
102
+ copy[locale] = next;
103
+ }
104
+ }
105
+ return copy ?? map;
106
+ }