@object-ui/core 16.1.0 → 17.2.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 (60) hide show
  1. package/CHANGELOG.md +1881 -4
  2. package/dist/actions/ActionEngine.d.ts +11 -1
  3. package/dist/actions/ActionEngine.js +14 -3
  4. package/dist/actions/ActionRunner.d.ts +199 -24
  5. package/dist/actions/ActionRunner.js +189 -84
  6. package/dist/actions/actionKeys.d.ts +135 -0
  7. package/dist/actions/actionKeys.js +283 -0
  8. package/dist/actions/index.d.ts +1 -0
  9. package/dist/actions/index.js +1 -0
  10. package/dist/adapters/ApiDataSource.js +10 -1
  11. package/dist/adapters/ValueDataSource.d.ts +2 -2
  12. package/dist/evaluator/fieldRules.d.ts +0 -2
  13. package/dist/evaluator/fieldRules.js +3 -2
  14. package/dist/evaluator/optionLint.js +5 -2
  15. package/dist/evaluator/optionRules.d.ts +13 -6
  16. package/dist/evaluator/optionRules.js +0 -23
  17. package/dist/index.d.ts +4 -0
  18. package/dist/index.js +7 -0
  19. package/dist/query/query-ast.d.ts +6 -6
  20. package/dist/query/query-ast.js +2 -2
  21. package/dist/registry/PluginSystem.d.ts +15 -5
  22. package/dist/registry/PluginSystem.js +1 -1
  23. package/dist/registry/Registry.d.ts +81 -2
  24. package/dist/registry/Registry.js +102 -2
  25. package/dist/registry/WidgetRegistry.d.ts +7 -7
  26. package/dist/registry/WidgetRegistry.js +1 -1
  27. package/dist/registry/public-blocks.js +52 -1
  28. package/dist/styling/scoped-styles.d.ts +14 -8
  29. package/dist/theme/ThemeEngine.js +0 -12
  30. package/dist/utils/chart-series.d.ts +42 -2
  31. package/dist/utils/chart-series.js +55 -0
  32. package/dist/utils/column-identity.d.ts +110 -0
  33. package/dist/utils/column-identity.js +210 -0
  34. package/dist/utils/dashboard-filters.d.ts +11 -2
  35. package/dist/utils/dashboard-filters.js +99 -4
  36. package/dist/utils/dataset-format.d.ts +13 -1
  37. package/dist/utils/dataset-format.js +10 -2
  38. package/dist/utils/expand-fields.js +8 -8
  39. package/dist/utils/filter-converter.d.ts +47 -0
  40. package/dist/utils/filter-converter.js +94 -7
  41. package/dist/utils/filter-tokens.d.ts +108 -0
  42. package/dist/utils/filter-tokens.js +180 -0
  43. package/dist/utils/freeze-schema.d.ts +14 -6
  44. package/dist/utils/freeze-schema.js +14 -6
  45. package/dist/utils/managedBy.d.ts +97 -51
  46. package/dist/utils/managedBy.js +117 -53
  47. package/dist/utils/normalize-list-view.d.ts +109 -0
  48. package/dist/utils/normalize-list-view.js +255 -0
  49. package/dist/utils/sort-values.d.ts +52 -0
  50. package/dist/utils/sort-values.js +125 -0
  51. package/dist/validation/index.d.ts +10 -1
  52. package/dist/validation/index.js +10 -1
  53. package/dist/validation/required-presence.d.ts +31 -0
  54. package/dist/validation/required-presence.js +36 -0
  55. package/dist/validation/schema-validator.d.ts +24 -8
  56. package/dist/validation/validators/index.d.ts +7 -1
  57. package/dist/validation/validators/index.js +7 -1
  58. package/dist/validation/validators/object-validation-engine.d.ts +151 -31
  59. package/dist/validation/validators/object-validation-engine.js +275 -142
  60. package/package.json +4 -4
@@ -62,6 +62,22 @@ export type ComponentConfig<T = any> = ComponentMeta & {
62
62
  type: string;
63
63
  component: ComponentRenderer<T>;
64
64
  };
65
+ /**
66
+ * A CONTRACT-surface entry (ADR-0080), as returned by
67
+ * {@link Registry.getPublicConfigs}.
68
+ *
69
+ * Same shape as {@link ComponentConfig} except `component` is absent while the
70
+ * entry is still a pending `registerLazy` stub: the plugin module has not been
71
+ * imported yet, so there is no renderer to hand out. Consumers render such an
72
+ * entry through `SchemaRenderer`, which triggers the loader and shows a
73
+ * placeholder in the meantime (objectui#2953).
74
+ */
75
+ export type PublicComponentConfig<T = any> = ComponentMeta & {
76
+ type: string;
77
+ component?: ComponentRenderer<T>;
78
+ /** True while this entry is a `registerLazy` stub whose loader has not run. */
79
+ lazy?: boolean;
80
+ };
65
81
  /**
66
82
  * Lazy loader function used by `Registry.registerLazy`. The loader is invoked
67
83
  * the first time a missing component type is requested through `getAsync`/the
@@ -79,6 +95,14 @@ export declare class Registry<T = any> {
79
95
  * load completes.
80
96
  */
81
97
  private listeners;
98
+ /**
99
+ * Bumped whenever the set of KNOWN types changes — a registration, an
100
+ * unregistration, or a new lazy stub. A cheap cache key for consumers that
101
+ * derive something from the whole registry (whitelists, manifests, palettes)
102
+ * and need to rebuild when it grows. Counting types is not a substitute: a
103
+ * registration paired with an unregistration leaves the count untouched.
104
+ */
105
+ private version;
82
106
  /**
83
107
  * Register a component with optional namespace support.
84
108
  * If namespace is provided in meta, the component will be registered as "namespace:type".
@@ -141,6 +165,11 @@ export declare class Registry<T = any> {
141
165
  */
142
166
  subscribe(listener: () => void): () => void;
143
167
  private notify;
168
+ /**
169
+ * Monotonic counter of changes to the set of known types. Rebuild anything
170
+ * derived from the whole registry when this moves — see {@link getKnownTypes}.
171
+ */
172
+ getVersion(): number;
144
173
  /**
145
174
  * Get a component by type. Supports both namespaced and non-namespaced lookups.
146
175
  *
@@ -164,6 +193,13 @@ export declare class Registry<T = any> {
164
193
  /**
165
194
  * Get component configuration by type with namespace support.
166
195
  *
196
+ * LOADED registrations only — a type that exists solely as a `registerLazy`
197
+ * stub returns `undefined` here, because callers read `.component` off the
198
+ * result and a stub has no renderer until its loader runs. The *contract*
199
+ * question ("is this tag part of the public surface?") must not depend on
200
+ * load order, so {@link getPublicConfigs} resolves lazy stubs separately
201
+ * (objectui#2953). Use {@link hasLazy} / {@link loadLazy} to reach a stub.
202
+ *
167
203
  * @param type - Component type (e.g., 'button' or 'ui:button')
168
204
  * @param namespace - Optional namespace for lookup priority
169
205
  * @returns Component configuration or undefined
@@ -178,23 +214,66 @@ export declare class Registry<T = any> {
178
214
  */
179
215
  has(type: string, namespace?: string): boolean;
180
216
  /**
181
- * Get all registered component types.
217
+ * Get all LOADED component types — what can be rendered right now.
218
+ *
219
+ * Pending `registerLazy` stubs are not included; use {@link getKnownTypes}
220
+ * for the question "is this a type this app knows about", which is what a
221
+ * whitelist or manifest wants (objectui#2953).
182
222
  *
183
223
  * @returns Array of all component type identifiers
184
224
  */
185
225
  getAllTypes(): string[];
226
+ /**
227
+ * Every type the registry can resolve — loaded registrations PLUS pending
228
+ * lazy stubs, deduped. Both the bare and namespaced keys are included, as
229
+ * they are in {@link getAllTypes}.
230
+ *
231
+ * This is the set to build a whitelist or a manifest from. Keying one off
232
+ * `getAllTypes()` instead makes it depend on which plugin chunks happen to
233
+ * have loaded: a lazily-registered block gets rejected as unknown, and
234
+ * whether it does varies by session (objectui#2953).
235
+ */
236
+ getKnownTypes(): string[];
237
+ /**
238
+ * Metadata for `type` — from the loaded registration when there is one,
239
+ * otherwise from a pending `registerLazy` stub.
240
+ *
241
+ * Use this when you need to know what a type IS (namespace, container-ness,
242
+ * declared inputs). {@link getConfig} answers a different question — "can I
243
+ * render this right now" — and is deliberately loaded-only, since callers
244
+ * read `.component` off it.
245
+ *
246
+ * A stub carries only what `registerLazy` was given, so `inputs` is usually
247
+ * absent until the chunk loads. Consumers should treat that as "not yet
248
+ * known", not as "declares no props".
249
+ */
250
+ getMeta(type: string, namespace?: string): ComponentMeta | undefined;
186
251
  /**
187
252
  * Get all registered component configurations.
188
253
  *
189
254
  * @returns Array of all component configurations
190
255
  */
191
256
  getAllConfigs(): ComponentConfig<T>[];
257
+ /**
258
+ * Resolve `tag` for the CONTRACT surface: the loaded registration when the
259
+ * component is already in the registry, otherwise the metadata of a pending
260
+ * `registerLazy` stub.
261
+ *
262
+ * A block registered lazily is a first-class member of the contract — the
263
+ * loader is recorded at boot and the plugin chunk is imported on first use —
264
+ * so contract membership must not hinge on whether that import happened to
265
+ * have run yet (objectui#2953).
266
+ */
267
+ private getContractConfig;
192
268
  /**
193
269
  * Get the curated PUBLIC-tier component configs (ADR-0080) — those registered
194
270
  * with `tier: 'public'`. This is the contract/AI-vocabulary surface, a subset
195
271
  * of the full rendering capability returned by {@link getAllConfigs}.
272
+ *
273
+ * Includes blocks that are only lazily registered so far; those come back
274
+ * with `lazy: true` and no `component` (see {@link PublicComponentConfig}).
196
275
  */
197
- getPublicConfigs(): ComponentConfig<T>[];
276
+ getPublicConfigs(): PublicComponentConfig<T>[];
198
277
  /**
199
278
  * Get all components in a specific namespace.
200
279
  *
@@ -31,6 +31,19 @@ export class Registry {
31
31
  writable: true,
32
32
  value: new Set()
33
33
  });
34
+ /**
35
+ * Bumped whenever the set of KNOWN types changes — a registration, an
36
+ * unregistration, or a new lazy stub. A cheap cache key for consumers that
37
+ * derive something from the whole registry (whitelists, manifests, palettes)
38
+ * and need to rebuild when it grows. Counting types is not a substitute: a
39
+ * registration paired with an unregistration leaves the count untouched.
40
+ */
41
+ Object.defineProperty(this, "version", {
42
+ enumerable: true,
43
+ configurable: true,
44
+ writable: true,
45
+ value: 0
46
+ });
34
47
  }
35
48
  /**
36
49
  * Register a component with optional namespace support.
@@ -144,6 +157,10 @@ export class Registry {
144
157
  if (meta?.namespace && !meta?.skipFallback) {
145
158
  this.lazyEntries.set(type, entry);
146
159
  }
160
+ // Bump the version but do NOT notify: the set of KNOWN types grew, so
161
+ // whitelists and manifests must rebuild, but nothing new can render yet —
162
+ // waking every subscriber for each stub at boot would be pure churn.
163
+ this.version++;
147
164
  }
148
165
  /**
149
166
  * Returns true if `type` (or its namespaced form) has a registered lazy
@@ -186,6 +203,7 @@ export class Registry {
186
203
  };
187
204
  }
188
205
  notify() {
206
+ this.version++;
189
207
  for (const listener of this.listeners) {
190
208
  try {
191
209
  listener();
@@ -195,6 +213,13 @@ export class Registry {
195
213
  }
196
214
  }
197
215
  }
216
+ /**
217
+ * Monotonic counter of changes to the set of known types. Rebuild anything
218
+ * derived from the whole registry when this moves — see {@link getKnownTypes}.
219
+ */
220
+ getVersion() {
221
+ return this.version;
222
+ }
198
223
  /**
199
224
  * Get a component by type. Supports both namespaced and non-namespaced lookups.
200
225
  *
@@ -226,6 +251,13 @@ export class Registry {
226
251
  /**
227
252
  * Get component configuration by type with namespace support.
228
253
  *
254
+ * LOADED registrations only — a type that exists solely as a `registerLazy`
255
+ * stub returns `undefined` here, because callers read `.component` off the
256
+ * result and a stub has no renderer until its loader runs. The *contract*
257
+ * question ("is this tag part of the public surface?") must not depend on
258
+ * load order, so {@link getPublicConfigs} resolves lazy stubs separately
259
+ * (objectui#2953). Use {@link hasLazy} / {@link loadLazy} to reach a stub.
260
+ *
229
261
  * @param type - Component type (e.g., 'button' or 'ui:button')
230
262
  * @param namespace - Optional namespace for lookup priority
231
263
  * @returns Component configuration or undefined
@@ -256,13 +288,47 @@ export class Registry {
256
288
  return this.components.has(type);
257
289
  }
258
290
  /**
259
- * Get all registered component types.
291
+ * Get all LOADED component types — what can be rendered right now.
292
+ *
293
+ * Pending `registerLazy` stubs are not included; use {@link getKnownTypes}
294
+ * for the question "is this a type this app knows about", which is what a
295
+ * whitelist or manifest wants (objectui#2953).
260
296
  *
261
297
  * @returns Array of all component type identifiers
262
298
  */
263
299
  getAllTypes() {
264
300
  return Array.from(this.components.keys());
265
301
  }
302
+ /**
303
+ * Every type the registry can resolve — loaded registrations PLUS pending
304
+ * lazy stubs, deduped. Both the bare and namespaced keys are included, as
305
+ * they are in {@link getAllTypes}.
306
+ *
307
+ * This is the set to build a whitelist or a manifest from. Keying one off
308
+ * `getAllTypes()` instead makes it depend on which plugin chunks happen to
309
+ * have loaded: a lazily-registered block gets rejected as unknown, and
310
+ * whether it does varies by session (objectui#2953).
311
+ */
312
+ getKnownTypes() {
313
+ return Array.from(new Set([...this.components.keys(), ...this.lazyEntries.keys()]));
314
+ }
315
+ /**
316
+ * Metadata for `type` — from the loaded registration when there is one,
317
+ * otherwise from a pending `registerLazy` stub.
318
+ *
319
+ * Use this when you need to know what a type IS (namespace, container-ness,
320
+ * declared inputs). {@link getConfig} answers a different question — "can I
321
+ * render this right now" — and is deliberately loaded-only, since callers
322
+ * read `.component` off it.
323
+ *
324
+ * A stub carries only what `registerLazy` was given, so `inputs` is usually
325
+ * absent until the chunk loads. Consumers should treat that as "not yet
326
+ * known", not as "declares no props".
327
+ */
328
+ getMeta(type, namespace) {
329
+ const key = namespace ? `${namespace}:${type}` : type;
330
+ return this.components.get(key) ?? this.lazyEntries.get(key)?.meta;
331
+ }
266
332
  /**
267
333
  * Get all registered component configurations.
268
334
  *
@@ -271,10 +337,39 @@ export class Registry {
271
337
  getAllConfigs() {
272
338
  return Array.from(this.components.values());
273
339
  }
340
+ /**
341
+ * Resolve `tag` for the CONTRACT surface: the loaded registration when the
342
+ * component is already in the registry, otherwise the metadata of a pending
343
+ * `registerLazy` stub.
344
+ *
345
+ * A block registered lazily is a first-class member of the contract — the
346
+ * loader is recorded at boot and the plugin chunk is imported on first use —
347
+ * so contract membership must not hinge on whether that import happened to
348
+ * have run yet (objectui#2953).
349
+ */
350
+ getContractConfig(tag) {
351
+ const loaded = this.components.get(tag);
352
+ if (loaded)
353
+ return loaded;
354
+ const entry = this.lazyEntries.get(tag);
355
+ if (!entry)
356
+ return undefined;
357
+ // Mirror registerLazy's key derivation so the canonical `type` matches the
358
+ // one `register()` will store once the loader runs — that keeps the dedupe
359
+ // below stable across the load. `tag` may already carry the namespace
360
+ // (lazyEntries holds both keys; curated tags like `record:details` are
361
+ // themselves colon-shaped), so don't prefix it twice.
362
+ const ns = entry.meta?.namespace;
363
+ const canonical = ns && !tag.startsWith(`${ns}:`) ? `${ns}:${tag}` : tag;
364
+ return { ...entry.meta, type: canonical, lazy: true };
365
+ }
274
366
  /**
275
367
  * Get the curated PUBLIC-tier component configs (ADR-0080) — those registered
276
368
  * with `tier: 'public'`. This is the contract/AI-vocabulary surface, a subset
277
369
  * of the full rendering capability returned by {@link getAllConfigs}.
370
+ *
371
+ * Includes blocks that are only lazily registered so far; those come back
372
+ * with `lazy: true` and no `component` (see {@link PublicComponentConfig}).
278
373
  */
279
374
  getPublicConfigs() {
280
375
  // Dedupe by the config's canonical (namespaced) `type` — a component is
@@ -292,12 +387,17 @@ export class Registry {
292
387
  };
293
388
  // Curated contract list first (stable, reviewable order) …
294
389
  for (const tag of PUBLIC_BLOCKS)
295
- add(tag, this.getConfig(tag));
390
+ add(tag, this.getContractConfig(tag));
296
391
  // … plus any bare registration that opted in explicitly via `tier: 'public'`.
297
392
  for (const [key, cfg] of this.components.entries()) {
298
393
  if (cfg.tier === 'public' && !key.includes(':'))
299
394
  add(key, cfg);
300
395
  }
396
+ // … and the same opt-in for stubs whose loader has not run yet.
397
+ for (const [key, entry] of this.lazyEntries.entries()) {
398
+ if (entry.meta?.tier === 'public' && !key.includes(':'))
399
+ add(key, this.getContractConfig(key));
400
+ }
301
401
  return out;
302
402
  }
303
403
  /**
@@ -9,12 +9,12 @@
9
9
  * WidgetRegistry - Runtime widget management with auto-discovery.
10
10
  *
11
11
  * Provides registration, loading, and lookup of runtime widgets
12
- * described by WidgetManifest objects. Widgets can be loaded from
12
+ * described by RuntimeWidgetManifest objects. Widgets can be loaded from
13
13
  * ES module URLs, provided inline, or resolved from the component registry.
14
14
  *
15
15
  * @module
16
16
  */
17
- import type { WidgetManifest, ResolvedWidget, WidgetRegistryListener } from '@object-ui/types';
17
+ import type { RuntimeWidgetManifest, ResolvedWidget, WidgetRegistryListener } from '@object-ui/types';
18
18
  import type { Registry } from './Registry.js';
19
19
  /**
20
20
  * Options for creating a WidgetRegistry instance.
@@ -54,11 +54,11 @@ export declare class WidgetRegistry {
54
54
  * Register a widget manifest.
55
55
  * Does not load the widget; call `load()` to resolve it.
56
56
  */
57
- register(manifest: WidgetManifest): void;
57
+ register(manifest: RuntimeWidgetManifest): void;
58
58
  /**
59
59
  * Register multiple widget manifests at once.
60
60
  */
61
- registerAll(manifests: WidgetManifest[]): void;
61
+ registerAll(manifests: RuntimeWidgetManifest[]): void;
62
62
  /**
63
63
  * Unregister a widget by name.
64
64
  */
@@ -66,15 +66,15 @@ export declare class WidgetRegistry {
66
66
  /**
67
67
  * Get a widget manifest by name.
68
68
  */
69
- getManifest(name: string): WidgetManifest | undefined;
69
+ getManifest(name: string): RuntimeWidgetManifest | undefined;
70
70
  /**
71
71
  * Get all registered widget manifests.
72
72
  */
73
- getAllManifests(): WidgetManifest[];
73
+ getAllManifests(): RuntimeWidgetManifest[];
74
74
  /**
75
75
  * Get manifests filtered by category.
76
76
  */
77
- getByCategory(category: string): WidgetManifest[];
77
+ getByCategory(category: string): RuntimeWidgetManifest[];
78
78
  /**
79
79
  * Check if a widget is registered.
80
80
  */
@@ -244,7 +244,7 @@ export class WidgetRegistry {
244
244
  // source.url is only known at runtime.
245
245
  //
246
246
  // Security: Widget URLs must be from trusted sources only. Never pass
247
- // user-supplied URLs directly to WidgetManifest. URLs should be validated
247
+ // user-supplied URLs directly to RuntimeWidgetManifest. URLs should be validated
248
248
  // and controlled by the application developer.
249
249
  //
250
250
  // CSP Consideration: If your application uses strict Content Security Policy,
@@ -42,7 +42,58 @@ export const PUBLIC_BLOCKS = [
42
42
  'record:highlights',
43
43
  'record:related_list',
44
44
  'record:path',
45
- 'line_items',
45
+ // `record:`-prefixed, like its four siblings above. @object-ui/plugin-form
46
+ // registers this one with `skipFallback: true`, so the bare `line_items` key
47
+ // it was listed under here never existed — the block has shipped all along
48
+ // but could never resolve through the contract (objectui#2953 follow-up).
49
+ 'record:line_items',
50
+ // Configurable as of objectui#3023 follow-up: these shipped with renderers
51
+ // but no declared `inputs`, so a model could only emit them bare — which is
52
+ // why they sat outside the contract. Their inputs now mirror what the
53
+ // renderers actually read, so they are authorable and belong here.
54
+ //
55
+ // `record:chatter` is deliberately NOT in this list: it is the same renderer
56
+ // as `record:discussion` under a Salesforce-familiar name, kept for schemas
57
+ // already in the wild. Two spellings of one block is ambiguity an authoring
58
+ // model has no way to resolve, so the vocabulary carries the spec's name.
59
+ 'record:activity',
60
+ 'record:discussion',
61
+ 'record:history',
62
+ 'record:quick_actions',
63
+ 'record:reference_rail',
64
+ 'record:alert',
65
+ // ── Tier A, continued — page-semantic blocks (the spec ui/page vocabulary) ─
66
+ // The `page:` / `element:` / `action:` families the Studio page designer
67
+ // offers and @objectstack/spec's page schema enumerates. Same standing as
68
+ // the `record:` family above: semantic page blocks, not primitives.
69
+ //
70
+ // Deliberately NOT here (each guarded, with its reason, by the console's
71
+ // reverse-coverage test):
72
+ // action:bar record:quick_actions covers the record action
73
+ // strip, and the spec blesses button/group/icon/
74
+ // menu but not bar
75
+ // element:image duplicates the curated `image` primitive below —
76
+ // one spelling per concept
77
+ // element:record_picker record picking is a field widget, not a page
78
+ // block (Studio palette exclusion)
79
+ // element:text_input bare inputs belong to a form, not a page block
80
+ // (Studio palette exclusion)
81
+ // element:metadata_viewer internal metadata debugging surface
82
+ 'page:tabs',
83
+ 'page:card',
84
+ 'page:accordion',
85
+ 'page:section',
86
+ 'page:footer',
87
+ 'page:sidebar',
88
+ 'element:text',
89
+ 'element:number',
90
+ 'element:button',
91
+ 'element:definition-list',
92
+ 'element:repeater',
93
+ 'action:button',
94
+ 'action:group',
95
+ 'action:menu',
96
+ 'action:icon',
46
97
  // ── Tier B — layout / content primitives ──────────────────────────────────
47
98
  'flex',
48
99
  'grid',
@@ -19,14 +19,20 @@
19
19
  * `large` is the unconditional base; `medium`/`small`/`xsmall` are `max-width`
20
20
  * overrides.
21
21
  */
22
- export type StyleMap = Record<string, string | number>;
23
- export interface ResponsiveStyles {
24
- /** Unconditional base (desktop-first). */
25
- large?: StyleMap;
26
- medium?: StyleMap;
27
- small?: StyleMap;
28
- xsmall?: StyleMap;
29
- }
22
+ import type { ResponsiveStyles } from '@objectstack/spec/ui';
23
+ /**
24
+ * `StyleMap` / `ResponsiveStyles` are **owned by the spec** (ADR-0065,
25
+ * `@objectstack/spec/ui`) and re-exported here, not re-declared.
26
+ *
27
+ * They used to be hand-written copies under the spec's own names. Both were
28
+ * member-for-member identical to the spec's at the time of the burn-down
29
+ * (objectstack#4115), which is exactly what makes a copy dangerous: it passes
30
+ * every structural comparison right up until the spec adds a breakpoint or
31
+ * widens a value type, and the doc comment above claims spec canonicity the
32
+ * whole time. Binding the names to the spec at compile time is the only form
33
+ * that cannot drift.
34
+ */
35
+ export type { StyleMap, ResponsiveStyles } from '@objectstack/spec/ui';
30
36
  /** max-width breakpoints (px), mirroring Builder.io's default device sizes. */
31
37
  export declare const STYLE_BREAKPOINTS: Record<'medium' | 'small' | 'xsmall', number>;
32
38
  /** True when a node actually carries responsive styles worth compiling. */
@@ -316,10 +316,6 @@ export function mergeThemes(parent, child) {
316
316
  shadows: child.shadows || parent.shadows
317
317
  ? { ...parent.shadows, ...child.shadows }
318
318
  : undefined,
319
- // Deep-merge breakpoints
320
- breakpoints: child.breakpoints || parent.breakpoints
321
- ? { ...parent.breakpoints, ...child.breakpoints }
322
- : undefined,
323
319
  // Deep-merge animation
324
320
  animation: child.animation || parent.animation
325
321
  ? {
@@ -339,18 +335,10 @@ export function mergeThemes(parent, child) {
339
335
  zIndex: child.zIndex || parent.zIndex
340
336
  ? { ...parent.zIndex, ...child.zIndex }
341
337
  : undefined,
342
- // Deep-merge spacing
343
- spacing: child.spacing || parent.spacing
344
- ? { ...parent.spacing, ...child.spacing }
345
- : undefined,
346
338
  // Deep-merge customVars
347
339
  customVars: child.customVars || parent.customVars
348
340
  ? { ...parent.customVars, ...child.customVars }
349
341
  : undefined,
350
- // Deep-merge logo
351
- logo: child.logo || parent.logo
352
- ? { ...parent.logo, ...child.logo }
353
- : undefined,
354
342
  };
355
343
  }
356
344
  /**
@@ -26,14 +26,26 @@ export interface ChartResultField {
26
26
  label?: string;
27
27
  format?: string;
28
28
  }
29
- export interface ChartSeries {
29
+ /**
30
+ * One renderer-internal series binding produced by {@link buildChartSeries}:
31
+ * WHICH result-set column to draw and what to call it.
32
+ *
33
+ * NOT the spec's `ChartSeries` (`@objectstack/spec/ui`), which this type was
34
+ * named after until objectstack#4115. That one is the AUTHORED dataset-binding
35
+ * descriptor — `{ name, label?, type?, color?, stack?, yAxis, variant?,
36
+ * dashArray?, opacity? }`, where `name` is a measure and no `data` is carried.
37
+ * A third shape, `ChartDataSeries` in `@object-ui/types` (renamed in the same
38
+ * burn-down), is the inline static-data series of an SDUI `ChartSchema` node.
39
+ * Three different concepts; this is the one the chart renderers consume.
40
+ */
41
+ export interface ChartSeriesBinding {
30
42
  dataKey: string;
31
43
  label: string;
32
44
  }
33
45
  export interface ChartSeriesResult {
34
46
  data: Array<Record<string, unknown>>;
35
47
  xAxisKey: string | undefined;
36
- series: ChartSeries[];
48
+ series: ChartSeriesBinding[];
37
49
  }
38
50
  export declare function buildChartSeries(rows: Array<Record<string, unknown>> | null | undefined, dimensions: string[] | null | undefined, values: string[] | null | undefined, fields?: ChartResultField[] | null): ChartSeriesResult;
39
51
  /**
@@ -89,6 +101,34 @@ export declare function findChartSeriesRow(rows: Array<Record<string, unknown>>
89
101
  * paint identically across the chart view and dashboard widgets.
90
102
  */
91
103
  export declare function buildOptionColorMap(options: unknown): Record<string, string> | null;
104
+ /**
105
+ * Build the DECLARED category order from a select field's `options` — the
106
+ * sequence the author wrote them in on the object (framework#3588).
107
+ *
108
+ * A picklist's option order is already the domain order: a sales `stage` field
109
+ * lists Qualification → Needs Analysis → Proposal → Negotiation because that IS
110
+ * the pipeline. Analytics groups by that field and returns buckets in whatever
111
+ * order the GROUP BY produced (usually alphabetical), which for an
112
+ * ordered-sequence chart — a funnel above all — draws a shape that reads as a
113
+ * pipeline but isn't one.
114
+ *
115
+ * Emits BOTH the stored `value` and its display `label` per option, adjacent
116
+ * and in declared order, for the same reason {@link buildOptionColorMap} keys
117
+ * by both: a chart row's category may carry either, depending on whether the
118
+ * server resolved the dimension's labels. Rank is "index of first match", so
119
+ * either key ranks the option identically.
120
+ *
121
+ * Returns `null` for a field with no options, so callers keep their existing
122
+ * ordering (a funnel falls back to descending by value).
123
+ */
124
+ export declare function buildCategoryOrder(options: unknown): string[] | null;
125
+ /**
126
+ * Rank map for {@link buildCategoryOrder} keys — `category → position`, first
127
+ * occurrence wins. Categories absent from the declared order get no entry; the
128
+ * caller decides where those sort (see `AdvancedChartImpl`, which keeps them
129
+ * after the declared ones rather than dropping them).
130
+ */
131
+ export declare function buildCategoryRank(order: string[] | null | undefined): Map<string, number> | null;
92
132
  /**
93
133
  * Build a `{ value → label }` map from a select/enum field's `options`, for
94
134
  * resolving a grouped dimension's stored value to its display label (fed to
@@ -147,6 +147,61 @@ export function buildOptionColorMap(options) {
147
147
  }
148
148
  return Object.keys(map).length > 0 ? map : null;
149
149
  }
150
+ /**
151
+ * Build the DECLARED category order from a select field's `options` — the
152
+ * sequence the author wrote them in on the object (framework#3588).
153
+ *
154
+ * A picklist's option order is already the domain order: a sales `stage` field
155
+ * lists Qualification → Needs Analysis → Proposal → Negotiation because that IS
156
+ * the pipeline. Analytics groups by that field and returns buckets in whatever
157
+ * order the GROUP BY produced (usually alphabetical), which for an
158
+ * ordered-sequence chart — a funnel above all — draws a shape that reads as a
159
+ * pipeline but isn't one.
160
+ *
161
+ * Emits BOTH the stored `value` and its display `label` per option, adjacent
162
+ * and in declared order, for the same reason {@link buildOptionColorMap} keys
163
+ * by both: a chart row's category may carry either, depending on whether the
164
+ * server resolved the dimension's labels. Rank is "index of first match", so
165
+ * either key ranks the option identically.
166
+ *
167
+ * Returns `null` for a field with no options, so callers keep their existing
168
+ * ordering (a funnel falls back to descending by value).
169
+ */
170
+ export function buildCategoryOrder(options) {
171
+ if (!Array.isArray(options) || options.length === 0)
172
+ return null;
173
+ const keys = [];
174
+ for (const opt of options) {
175
+ if (typeof opt === 'string' || typeof opt === 'number' || typeof opt === 'boolean') {
176
+ keys.push(String(opt));
177
+ continue;
178
+ }
179
+ if (opt && typeof opt === 'object') {
180
+ const o = opt;
181
+ if (o.value != null)
182
+ keys.push(String(o.value));
183
+ if (o.label != null && String(o.label) !== String(o.value))
184
+ keys.push(String(o.label));
185
+ }
186
+ }
187
+ return keys.length > 0 ? keys : null;
188
+ }
189
+ /**
190
+ * Rank map for {@link buildCategoryOrder} keys — `category → position`, first
191
+ * occurrence wins. Categories absent from the declared order get no entry; the
192
+ * caller decides where those sort (see `AdvancedChartImpl`, which keeps them
193
+ * after the declared ones rather than dropping them).
194
+ */
195
+ export function buildCategoryRank(order) {
196
+ if (!Array.isArray(order) || order.length === 0)
197
+ return null;
198
+ const rank = new Map();
199
+ order.forEach((key, i) => {
200
+ if (!rank.has(key))
201
+ rank.set(key, i);
202
+ });
203
+ return rank.size > 0 ? rank : null;
204
+ }
150
205
  /**
151
206
  * Build a `{ value → label }` map from a select/enum field's `options`, for
152
207
  * resolving a grouped dimension's stored value to its display label (fed to