@object-ui/plugin-dashboard 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 (56) hide show
  1. package/CHANGELOG.md +5042 -0
  2. package/README.md +544 -74
  3. package/dist/DashboardConfigPanel.d.ts +3 -0
  4. package/dist/DashboardConfigPanel.d.ts.map +1 -1
  5. package/dist/DashboardFilterBar.d.ts.map +1 -1
  6. package/dist/DashboardGridLayout.d.ts +3 -2
  7. package/dist/DashboardGridLayout.d.ts.map +1 -1
  8. package/dist/DashboardRenderer.d.ts +32 -2
  9. package/dist/DashboardRenderer.d.ts.map +1 -1
  10. package/dist/DashboardWithConfig.d.ts +1 -1
  11. package/dist/DashboardWithConfig.d.ts.map +1 -1
  12. package/dist/DatasetWidget.d.ts +43 -119
  13. package/dist/DatasetWidget.d.ts.map +1 -1
  14. package/dist/DrillDownDrawer.d.ts +14 -5
  15. package/dist/DrillDownDrawer.d.ts.map +1 -1
  16. package/dist/MetricCard.d.ts +1 -1
  17. package/dist/MetricWidget.d.ts +27 -2
  18. package/dist/MetricWidget.d.ts.map +1 -1
  19. package/dist/ObjectDataTable.d.ts +473 -19
  20. package/dist/ObjectDataTable.d.ts.map +1 -1
  21. package/dist/ObjectMetricWidget.d.ts +65 -27
  22. package/dist/ObjectMetricWidget.d.ts.map +1 -1
  23. package/dist/ObjectPivotTable.d.ts +46 -7
  24. package/dist/ObjectPivotTable.d.ts.map +1 -1
  25. package/dist/PivotTable.d.ts +20 -3
  26. package/dist/PivotTable.d.ts.map +1 -1
  27. package/dist/RecordDetailDrawer.d.ts +8 -0
  28. package/dist/RecordDetailDrawer.d.ts.map +1 -1
  29. package/dist/WidgetConfigPanel.d.ts +29 -1
  30. package/dist/WidgetConfigPanel.d.ts.map +1 -1
  31. package/dist/WidgetEmptyState.d.ts +27 -0
  32. package/dist/WidgetEmptyState.d.ts.map +1 -0
  33. package/dist/domPassthroughPins.d.ts.map +1 -1
  34. package/dist/index.d.ts +30 -25
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +2306 -1858
  37. package/dist/index.umd.cjs +2 -2
  38. package/dist/legacyRetiredWidget.d.ts +5 -3
  39. package/dist/legacyRetiredWidget.d.ts.map +1 -1
  40. package/dist/measureRefusal.d.ts +50 -0
  41. package/dist/measureRefusal.d.ts.map +1 -0
  42. package/dist/recordFields.d.ts +159 -9
  43. package/dist/recordFields.d.ts.map +1 -1
  44. package/dist/retired-node-types.d.ts +29 -0
  45. package/dist/retired-node-types.d.ts.map +1 -0
  46. package/dist/useConfigPanelTranslation.d.ts +35 -0
  47. package/dist/useConfigPanelTranslation.d.ts.map +1 -0
  48. package/dist/useDashboardAutoRefresh.d.ts +47 -0
  49. package/dist/useDashboardAutoRefresh.d.ts.map +1 -0
  50. package/dist/utils.d.ts +36 -7
  51. package/dist/utils.d.ts.map +1 -1
  52. package/dist/widgetDispatch.d.ts +144 -18
  53. package/dist/widgetDispatch.d.ts.map +1 -1
  54. package/dist/widgetSubCaption.d.ts +40 -0
  55. package/dist/widgetSubCaption.d.ts.map +1 -0
  56. package/package.json +15 -15
package/README.md CHANGED
@@ -16,6 +16,30 @@ Dashboard plugin for Object UI - Create beautiful dashboards with metrics, chart
16
16
  pnpm add @object-ui/plugin-dashboard
17
17
  ```
18
18
 
19
+ ## Requires a bundler — plain Node cannot import this package
20
+
21
+ `DashboardGridLayout` imports `react-grid-layout`'s stylesheet at module scope
22
+ (`import 'react-grid-layout/css/styles.css'`), and Node has no loader for `.css` at all.
23
+ Importing the published entry from plain Node ESM — no bundler, no loader hooks — therefore
24
+ resolves and then fails during evaluation:
25
+
26
+ ```text
27
+ TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".css"
28
+ for .../react-grid-layout/css/styles.css
29
+ ```
30
+
31
+ **This is a supported-configuration statement, not a bug to report.** Unbundled Node
32
+ consumption is not supported for style-carrying plugin packages. It was ruled that way on
33
+ [objectui#5384](https://github.com/objectstack-ai/objectui/issues/5384) — deliberately, over
34
+ the alternative of moving the stylesheet out of module scope — because the grid's layout rules
35
+ are not optional and no unbundled-Node consumer exists to serve.
36
+
37
+ Consume it through a host that handles CSS imports, which every supported host does: Vite,
38
+ webpack, or Next with the package listed in `transpilePackages`. If you have a real need to
39
+ import it under plain Node — SSR with no bundler, a Node-side script — please open an issue.
40
+ That reopens the question as a design decision rather than a defect, and the shape of your
41
+ consumer is the missing input.
42
+
19
43
  ## Usage
20
44
 
21
45
  ### Automatic Registration (Side-Effect Import)
@@ -39,18 +63,89 @@ const schema = {
39
63
  };
40
64
  ```
41
65
 
42
- ### Manual Registration
66
+ ### What the side-effect import registers
67
+
68
+ That single import is the whole of registration — there is no components map to
69
+ iterate over. Importing the entry runs the seven live `ComponentRegistry.register(...)`
70
+ calls in `src/index.tsx`, which claim exactly these schema types. The keys below
71
+ are read off those calls:
72
+
73
+ | Namespaced key | Bare-name fallback | Renderer behind it |
74
+ | --- | --- | --- |
75
+ | `plugin-dashboard:dashboard` | `dashboard` | `DashboardRenderer` — the widget container |
76
+ | `plugin-dashboard:metric` | none — `skipFallback: true` | `MetricWidget` — one KPI value |
77
+ | `plugin-dashboard:metric-card` | none — `skipFallback: true` | `MetricCard` — KPI with trend and icon |
78
+ | `plugin-dashboard:object-metric` | `object-metric` | internal wrapper around `ObjectMetricWidget` — aggregates over an object |
79
+ | `plugin-dashboard:pivot` | `pivot` | `PivotTable` — pivot over rows you pass in |
80
+ | `plugin-dashboard:object-pivot` | `object-pivot` | `ObjectPivotTable` — a pivot queried from an object; its props go in `properties` |
81
+ | `plugin-dashboard:object-data-table` | `object-data-table` | `ObjectDataTable` — table queried from an object |
82
+
83
+ `ComponentRegistry.register` publishes `namespace:type`, and — unless the call
84
+ passes `skipFallback: true` — the bare `type` as a back-compat fallback
85
+ (`packages/core/src/registry/Registry.ts:194`, fallback branch at `:226`). Every
86
+ call behind the table above leaves `skipFallback` unset except `metric` and
87
+ `metric-card`, so each other type there resolves under both spellings.
88
+
89
+ `metric` and `metric-card` are two vocabularies that share a spelling: the
90
+ dashboard WIDGET types (`widgets[].type: 'metric'`, and the `metric-card` slot
91
+ entry) and the internal NODE keys the dashboard renders them through. The widget
92
+ types are unchanged. The node keys register with `skipFallback: true`
93
+ (objectui#10859 batch 8), and both dashboard surfaces emit the namespaced keys, so
94
+ only `plugin-dashboard:metric` / `plugin-dashboard:metric-card` resolve as nodes.
95
+
96
+ `dashboard-grid` is no longer registered (objectui#10859 batch 8: builder chrome,
97
+ with no producer and no runtime emitter). `DashboardGridLayout` is still exported;
98
+ mount it directly (see "DashboardGridLayout — persisting drag / resize edits"). The two `object-*` types are served by internal wrappers that
99
+ first resolve the spec's per-element `dataSource` binding (through
100
+ `ElementDataSourceGate` from `@object-ui/react`) and then render the exported
101
+ component, which is why those rows name a wrapper rather than an export.
102
+
103
+ ⛔ One further registration is deliberately absent from the table above. Until
104
+ objectui#9533 the dashboard renderer was published as `view:dashboard`, while
105
+ `apps/console`'s lazy stubs and the CLI's known-type whitelist already spelled it
106
+ `plugin-dashboard:dashboard`; the bare `dashboard` key therefore declared one
107
+ namespace before the chunk loaded and the other after it, and the
108
+ `plugin-dashboard:dashboard` stub was never cleared, so that spelling could never
109
+ resolve. The renderer now registers under `plugin-dashboard`, and the retired
110
+ `view:dashboard` key is answered by a tombstone widget that refuses BY NAME and
111
+ names `plugin-dashboard:dashboard` as its replacement — so an authored
112
+ `view:dashboard` gets a visible refusal carrying its own migration, never a
113
+ silent fall-through. That tombstone registration passes `skipFallback: true`, so
114
+ it claims no bare key, and `view:dashboard` is deliberately NOT a renderable key:
115
+ `objectui check` reports it as unknown.
116
+
117
+ ### Registering a component under your own key
118
+
119
+ To serve one of this package's components under a key of your own, register the
120
+ exported component — that is what a manual registration is here:
43
121
 
44
122
  ```typescript
45
- import { dashboardComponents } from '@object-ui/plugin-dashboard';
46
123
  import { ComponentRegistry } from '@object-ui/core';
124
+ import { MetricCard } from '@object-ui/plugin-dashboard';
47
125
 
48
- // Register dashboard components
49
- Object.entries(dashboardComponents).forEach(([type, component]) => {
50
- ComponentRegistry.register(type, component);
126
+ ComponentRegistry.register('my-metric', MetricCard, {
127
+ namespace: 'my-app',
128
+ label: 'My Metric',
129
+ category: 'Dashboard',
51
130
  });
52
131
  ```
53
132
 
133
+ There is also a `dashboardComponents` export: the manual-integration map, keyed
134
+ by the **same seven schema types** as the table above (objectui#5064 re-keyed it
135
+ from component class names; since objectui#10859 batch 8 the `metric` and
136
+ `metric-card` entries are keyed `plugin-dashboard:metric` /
137
+ `plugin-dashboard:metric-card`, the type each serves, so iterating the map cannot
138
+ re-create their retired bare keys). Each key maps to the exact component the import
139
+ registers for that type — for the two `object-*` types that is the internal
140
+ data-source-gate wrapper, not the exported widget. Iterating it with
141
+ `ComponentRegistry.register(type, component)` therefore re-registers the seven
142
+ types the import has already claimed, which is still not the manual registration
143
+ above: each such call passes no `meta`, so it trips the no-namespace deprecation
144
+ warning in `register` (`packages/core/src/registry/Registry.ts:198`) and
145
+ rewrites each bare-name registry entry without its `label`/`category` metadata
146
+ (the `namespace:type` entries are untouched). The side-effect import remains the
147
+ whole of registration.
148
+
54
149
  ## Schema API
55
150
 
56
151
  ### Dashboard
@@ -58,32 +153,90 @@ Object.entries(dashboardComponents).forEach(([type, component]) => {
58
153
  Container for dashboard widgets:
59
154
 
60
155
  ```typescript
61
- {
156
+ import type { DashboardComponentSchema, DashboardWidgetSchema } from '@object-ui/types';
157
+
158
+ declare const widgets: DashboardWidgetSchema[];
159
+
160
+ const schema: DashboardComponentSchema = {
62
161
  type: 'dashboard',
63
- widgets: Widget[],
64
- columns?: number, // Grid columns (default: 3)
65
- gap?: number, // Gap between widgets
66
- className?: string
67
- }
162
+ widgets,
163
+ columns: 3, // Grid columns (default: 3)
164
+ gap: 4, // Gap between widgets
165
+ className: 'w-full',
166
+ };
68
167
  ```
69
168
 
169
+ `type` and `widgets` are the required keys; every other key above is optional.
170
+
70
171
  ### Metric Card
71
172
 
72
- Display a single metric or KPI:
173
+ Display a single metric or KPI as a dashboard widget-slot entry:
73
174
 
74
175
  ```typescript
75
- {
176
+ import type { ComponentProps } from 'react';
177
+ import { MetricCard } from '@object-ui/plugin-dashboard';
178
+
179
+ // A `metric-card` entry sits directly in a dashboard's `widgets[]` — the closed
180
+ // component slot of the 2026-08-14 ruling. Its keys besides `type` are
181
+ // `MetricCard`'s own props. That props interface is not on this package's export
182
+ // surface (see "TypeScript Support" below), so they are read off the shipped
183
+ // component rather than restated here — a renamed or retyped prop stops this
184
+ // block compiling.
185
+ type MetricCardEntry = { type: 'metric-card' } & ComponentProps<typeof MetricCard>;
186
+
187
+ const card: MetricCardEntry = {
76
188
  type: 'metric-card',
77
- title: string,
78
- value: string | number,
79
- icon?: string, // Lucide icon name
80
- trend?: 'up' | 'down' | 'neutral',
81
- trendValue?: string,
82
- description?: string,
83
- className?: string
84
- }
189
+ title: 'Total Sales',
190
+ value: '$123,456',
191
+ icon: 'trending-up', // Lucide icon name
192
+ trend: 'up', // 'up' | 'down' | 'neutral'
193
+ trendValue: '+12%',
194
+ description: 'vs last month',
195
+ className: 'col-span-2',
196
+ };
197
+
198
+ const schema = { type: 'dashboard', widgets: [card] };
85
199
  ```
86
200
 
201
+ The dashboard renders the entry as the `plugin-dashboard:metric-card` node. The
202
+ bare `metric-card` NODE key is retired (objectui#10859), so a `metric-card`
203
+ outside a dashboard's `widgets[]` renders the "Unknown component type" panel.
204
+
205
+ `value` is the only required key. `title` and `description` take a plain string
206
+ or the spec's inline per-locale map (`I18nLabel`).
207
+
208
+ A card with no `value` is refused by `@object-ui/types/zod`, on the tolerant
209
+ face and the strict one. `metric-card` is not a widget type, so a card is never
210
+ read through widget keys alone, and no widget key stands in for `value`: a
211
+ `dataset` makes the dashboard draw its dataset tile in the card's place, and
212
+ `options` is not a binding (objectui#11483). For a figure queried from a
213
+ dataset, write a `metric` widget with `dataset` and `values` (see "TypeScript
214
+ Support" below).
215
+
216
+ #### Percent `format` patterns (`'0%'`, `'0.00%'`)
217
+
218
+ A numeral pattern ending in `%` is handed whole to `formatPercent`
219
+ (`@object-ui/fields`), the same call the list-view percent cell and this
220
+ package's own record-field renderer already make. Two consequences, both of
221
+ them shared with every other percent surface in the console rather than decided
222
+ by the tile:
223
+
224
+ - **Magnitude** is read at a STATED storage, never guessed from the value. A
225
+ `metric` tile holds no field, only a value and a pattern, and numeral's `%`
226
+ multiplies by 100, so the pattern states a fraction: `0.25` reads `25%`, `1`
227
+ reads `100%`, `-0.05` reads `-5%`. An `object-metric` tile over a `percent`
228
+ field renders at that field's own storage instead, the one the list cell
229
+ reads (`percentScaleOf` in `@objectstack/spec/data`: a fraction unless the
230
+ field declares a `max` above 1), so a field declaring `max: 100` that
231
+ averages `50` reads `50%`.
232
+ - **The percent sign is the locale's**, not a literal `%`: a `de-DE` session
233
+ gets the no-break space German writes before the sign, and grouping follows
234
+ the locale (a fraction of `12.5` reads `1,250%` in `en`).
235
+
236
+ The pattern's decimal count still belongs to the tile — `'0.00%'` renders two
237
+ decimals — because that is an author declaration on the widget rather than a
238
+ guess about the value.
239
+
87
240
  ## Examples
88
241
 
89
242
  ### Basic Dashboard
@@ -137,26 +290,35 @@ const schema = {
137
290
  value: '$123,456'
138
291
  },
139
292
  {
140
- type: 'card',
293
+ id: 'sales_trend',
294
+ type: 'line',
141
295
  title: 'Sales Trend',
142
- body: {
143
- type: 'line-chart',
144
- data: [/* chart data */],
145
- height: 300
146
- }
296
+ dataset: 'sales',
297
+ dimensions: ['month'],
298
+ values: ['revenue']
147
299
  },
148
300
  {
149
- type: 'card',
301
+ id: 'revenue_by_category',
302
+ type: 'pie',
150
303
  title: 'Category Distribution',
151
- body: {
152
- type: 'pie-chart',
153
- data: [/* chart data */]
154
- }
304
+ dataset: 'sales',
305
+ dimensions: ['category'],
306
+ values: ['revenue']
155
307
  }
156
308
  ]
157
309
  };
158
310
  ```
159
311
 
312
+ A chart widget names its family in `type` — one of the spec's chart families,
313
+ the closed vocabulary `DashboardWidgetTypeName` declares — and binds a
314
+ `dataset` (ADR-0021), selecting the dimension it plots in `dimensions` and its
315
+ measures in `values`. It never carries rows: `options.data`,
316
+ `options.xField` and `options.yField` are not widget keys, and
317
+ `@object-ui/types/zod`'s `StrictAnyComponentSchema` refuses them by name
318
+ (objectui#11228). There is no `card` widget family and no nested `body` slot: a
319
+ widget whose `type` is outside that vocabulary is refused at validation, by
320
+ `@object-ui/types/zod`'s `DashboardComponentSchema`.
321
+
160
322
  ### Responsive Dashboard
161
323
 
162
324
  ```typescript
@@ -171,30 +333,68 @@ const schema = {
171
333
 
172
334
  ## Integration with Data Sources
173
335
 
174
- Connect dashboard to live data:
336
+ Connect dashboard to live data. `metric-card` renders the `value` it is handed:
337
+ it has no row in the spec's expression carriage map, so a `${…}` written in
338
+ `value` or `trend` reaches the screen as those characters. A widget that should
339
+ read live data is one of the `object-*` types above — they resolve the spec's
340
+ per-element `dataSource` binding and query the object themselves.
175
341
 
176
- ```typescript
177
- import { createObjectStackAdapter } from '@object-ui/data-objectstack';
342
+ **The adapter is not a schema key.** A schema is a serialisable document; a live
343
+ adapter is an object with methods, so it cannot travel in one. An `object-*`
344
+ widget reads its adapter from React context — `useContext(SchemaRendererContext)`
345
+ at `src/ObjectMetricWidget.tsx:159`, with an explicit `dataSource` prop taking
346
+ precedence when the host renders the widget directly.
178
347
 
179
- const dataSource = createObjectStackAdapter({
180
- baseUrl: 'https://api.example.com',
181
- token: 'your-auth-token'
182
- });
348
+ So the document stays plain data — every value in it survives `JSON.stringify`:
183
349
 
184
- const schema = {
350
+ ```typescript
351
+ import type { DashboardComponentSchema } from '@object-ui/types';
352
+
353
+ const schema: DashboardComponentSchema = {
185
354
  type: 'dashboard',
186
- dataSource,
187
355
  widgets: [
188
356
  {
189
357
  type: 'metric-card',
190
358
  title: 'Total Users',
191
- value: '${data.metrics.totalUsers}',
192
- trend: '${data.metrics.userTrend}'
359
+ value: 12480,
360
+ trend: 'up'
193
361
  }
194
362
  ]
195
363
  };
196
364
  ```
197
365
 
366
+ The adapter is installed once, above the whole tree, and every `object-*` widget
367
+ underneath reads it from context:
368
+
369
+ ```tsx
370
+ import { SchemaRendererProvider, SchemaRenderer } from '@object-ui/react';
371
+ import { createObjectStackAdapter } from '@object-ui/data-objectstack';
372
+ import '@object-ui/plugin-dashboard';
373
+ import type { DashboardComponentSchema } from '@object-ui/types';
374
+
375
+ // The document from the block above.
376
+ declare const schema: DashboardComponentSchema;
377
+
378
+ const dataSource = createObjectStackAdapter({
379
+ baseUrl: 'https://api.example.com',
380
+ token: 'your-auth-token'
381
+ });
382
+
383
+ export const App = () => (
384
+ <SchemaRendererProvider dataSource={dataSource}>
385
+ <SchemaRenderer schema={schema} />
386
+ </SchemaRendererProvider>
387
+ );
388
+ ```
389
+
390
+ > A `dataSource` key **does** mean something on a schema node, but it is not this:
391
+ > it is the spec's element **binding** (`PageComponentSchema.dataSource`) — a
392
+ > declarative descriptor such as `{ object: 'orders', view: 'my_view' }`, resolved
393
+ > against the host and mapped onto the widget's own keys by the `object-*`
394
+ > registry shells in `src/index.tsx`. It belongs on the widget that reads it, not
395
+ > on the dashboard node, and a live adapter written into that slot wires nothing
396
+ > up — it is a different kind of thing wearing the same name.
397
+
198
398
  ## Dashboard-level filters
199
399
 
200
400
  A dashboard can declare top-level filters — a date range and any number of
@@ -218,16 +418,21 @@ into each bound widget's inline query (`AND`-combined with the widget's own
218
418
  "name": "region", // stable filter name (defaults to field)
219
419
  "field": "region", // default binding target
220
420
  "label": "Region",
421
+ // optional "object": the object `field` lives on — its fields.<object>.<field> / fieldOptions.<object>.<field>.<value> bundle entries then win, "label" is the fallback
221
422
  "type": "select", // text | select | date | number | lookup
222
423
  // Canonical @objectstack/spec pair form — the only form the platform
223
- // accepts at publish. The bare-string shorthand (["EMEA", …]) is
224
- // deprecated: still lifted at runtime, now warns (objectui#4356).
424
+ // accepts at publish, and the only form `@object-ui/types` validates
425
+ // (objectui#7759). The bare-string shorthand (["EMEA", …]) is NOT
426
+ // accepted: the runtime no longer lifts it (objectui#4356) — a bare member
427
+ // yields no option, and a dev-mode warning names it. Rewrite each X as
428
+ // { "value": X, "label": X }.
225
429
  "options": [
226
430
  { "value": "EMEA", "label": "EMEA" },
227
431
  { "value": "APAC", "label": "APAC" },
228
432
  { "value": "AMER", "label": "AMER" }
229
433
  ]
230
- // or dynamic: "optionsFrom": { "object": "accounts", "valueField": "region" }
434
+ // or dynamic: "optionsFrom": { "object": "accounts", "valueField": "region", "labelField": "region" }
435
+ // (`labelField` is required by the spec, even when it names the value field)
231
436
  }
232
437
  ],
233
438
  "widgets": [
@@ -235,9 +440,13 @@ into each bound widget's inline query (`AND`-combined with the widget's own
235
440
  // dimensions/measures by name. The pre-ADR-0021 top-level `object` +
236
441
  // `categoryField`/`valueField`/`aggregate` shape was REMOVED — a widget
237
442
  // still carrying it renders "This widget uses a retired data format.
238
- // Edit it to bind a dataset." instead of a chart. A renderer-internal
239
- // query lives under `options.data` as `{ provider: 'object', object,
240
- // aggregate }`; an `options.data` array is fixed demo data.
443
+ // Edit it to bind a dataset." instead of a chart. Inline widget data
444
+ // (`options.data`, `options.xField` / `options.yField`) is not an
445
+ // authoring surface either: the strict authoring face refuses those keys
446
+ // by name (objectui#11228). A stored single-value widget (`metric`,
447
+ // `gauge`, `solid-gauge`, `kpi`, `bullet`, or one with no `type`) whose
448
+ // `options.data` is a `{ "provider": "object", … }` query draws that same
449
+ // retired-format prompt instead of its number (objectui#11525).
241
450
  //
242
451
  // Default binding: the filter's own `field` (dateRange → created_at).
243
452
  { "id": "w1", "type": "bar", "dataset": "invoices", "dimensions": ["status"], "values": ["count"] },
@@ -270,8 +479,6 @@ Notes:
270
479
  query time, so widgets resolve them exactly like hand-authored filters.
271
480
  - Dataset-bound widgets receive the merged filter through the dataset
272
481
  query's `runtimeFilter`.
273
- - Static-data widgets (inline `data` arrays) have no query to scope and are
274
- not filtered.
275
482
  - Filter values are also readable in widget expressions as `page.<name>`
276
483
  (e.g. `page.region`), since they are hosted as dashboard variables.
277
484
  - `optionsFrom` resolves distinct option values server-side (a dataset
@@ -290,7 +497,9 @@ A KPI widget can declare a semantic accent. The vocabulary is
290
497
  tokens the designer's swatch picker offers:
291
498
 
292
499
  ```typescript
293
- {
500
+ import type { DashboardWidgetSchema } from '@object-ui/types';
501
+
502
+ const atRisk: DashboardWidgetSchema = {
294
503
  id: 'kpi_at_risk',
295
504
  type: 'metric',
296
505
  title: 'At-Risk Projects',
@@ -298,14 +507,14 @@ tokens the designer's swatch picker offers:
298
507
  values: ['project_count'],
299
508
  filter: { health: 'red' },
300
509
  colorVariant: 'danger', // tints the value; card chrome stays neutral
301
- }
510
+ };
302
511
  ```
303
512
 
304
513
  Where the accent lands depends on the layout, not on the token:
305
514
 
306
515
  | Layout | Accent |
307
516
  | --- | --- |
308
- | Card chrome (`MetricWidget`, inline `object-metric`) | the icon chip's background + foreground |
517
+ | Card chrome (`MetricWidget`, an `object-metric` block) | the icon chip's background + foreground |
309
518
  | Chrome-less (`MetricWidget variant: 'bare'`, and every dataset-bound `metric`) | the big number's text colour |
310
519
 
311
520
  Both read one shared table (`src/colorVariants.ts`), so the same declaration
@@ -315,36 +524,228 @@ the enum gets no accent and is not aliased to a nearby colour: it is invalid
315
524
  metadata, rejected where it is authored and published rather than reinterpreted
316
525
  here.
317
526
 
527
+ ## A widget with no `type`, or a `type` that names no family
528
+
529
+ A widget that declares no `type` is a `metric` widget. `@objectstack/spec`'s
530
+ `DashboardWidget.type` defaults to `metric`, and both dashboard surfaces
531
+ (`DashboardRenderer` and `DashboardGridLayout`) read that default from the spec,
532
+ so the widget draws exactly as the same widget with `type: 'metric'` does,
533
+ inline or bound to a dataset (objectui#11514). objectui's validator accepts the
534
+ widget without a `type` and does not write the default in, so the surfaces are
535
+ where it resolves.
536
+
537
+ objectui's legacy `component` envelope (`{ id, component, layout }`) is not the
538
+ spec's widget, and the default is not applied to it: an envelope with no `type`
539
+ draws its `component` under its card heading, as it always did. One node is
540
+ retired there: an `object-metric` (under `object-metric` or
541
+ `plugin-dashboard:object-metric`) draws the same retired-format prompt as a
542
+ dataset-less metric widget and sends no query (objectui#11466). Bind a metric
543
+ widget to a dataset instead. Every other envelope node draws as written, and the
544
+ filter bar still scopes an envelope's `object-chart` and `object-data-table`.
545
+
546
+ A `type` that names no widget family and no component type (a typo, or a family
547
+ the spec no longer has) is refused by both validator faces at `type`. A stored
548
+ one draws the labelled placeholder "「type」chart type is not supported yet", as
549
+ a known family with no renderer (`heatmap`) does, instead of the renderer's red
550
+ "Unknown component type" panel.
551
+
552
+ ## How many measures a widget renders
553
+
554
+ A dataset-bound widget queries every measure in `values`. What it renders
555
+ depends on its shape:
556
+
557
+ - **A metric-family widget** (`metric`, `kpi`, `gauge`, `solid-gauge`,
558
+ `bullet`) is a one-number tile. It takes exactly one measure, and
559
+ `@objectstack/spec` refuses a second one at its door; several numbers is a
560
+ different visual.
561
+ - **A widget with no `dimensions` and several measures** renders every one of
562
+ them when its type is `table` / `pivot` (one row of measures) or a chart of
563
+ the bar family (`bar`, `column`, `horizontal-bar`) or `line` / `area` /
564
+ `combo` (one mark per measure, the measures' labels on the category axis,
565
+ which runs down the side on a `horizontal-bar`).
566
+ - **Any other dimensionless widget with several measures** still renders the
567
+ first one as a tile.
568
+ - **A `pie`, `donut`, `funnel`, `treemap` or `sankey` with a dimension** draws
569
+ one series, the first measure, however many it declares: the shared chart
570
+ renderer reads the first series only on those families.
571
+
572
+ When a widget keeps a declared measure off the screen, `DatasetWidget` logs
573
+ one console warning naming the widget, the measures it renders, and the ones it
574
+ queried and never displayed. Three shapes do that. Two take the tile: a metric
575
+ tile stored before the spec narrowed, and a widget the third bullet above
576
+ covers (no `dimensions`, several measures, and a type the second bullet does not
577
+ list, so a dimensionless `pie` is one). The third is a `pie`, `donut`,
578
+ `funnel`, `treemap` or `sankey` with a dimension and several measures
579
+ (objectui#11417). For a tile the warning
580
+ then names the spec's ADR-0087 entry
581
+ `dashboard-widget-metric-family-multi-measure-refused`, whose `replacement`
582
+ says what to author instead of a one-number tile with several measures. For
583
+ one of those five charts it says that the chart family draws a single series,
584
+ the first declared measure, and points to no entry: that one answers for a
585
+ tile. Nothing about what is drawn changes. Every door accepts the chart shape
586
+ today; the spec is to refuse it (objectstack#21293).
587
+ The widget panel (`WidgetConfigPanel`) asks the same door the spec runs: it
588
+ offers no further measure the door would refuse, and shows the door's own
589
+ message under measures it already refuses.
590
+
591
+ The behaviour is pinned by `src/__tests__/DatasetWidget.unrenderedMeasures-8894.test.tsx`,
592
+ `src/__tests__/DatasetWidget.dimensionlessMeasures-11261.test.tsx` and
593
+ `src/__tests__/WidgetConfigPanel.measureDoor-8894.test.tsx`.
594
+
318
595
  ## TypeScript Support
319
596
 
597
+ This package ships **components, not schema types** — its whole type export
598
+ surface is `WidgetConfigPanelProps`, `ConfigPanelTranslate` and the three
599
+ `WidgetDataset*` catalog types, none of which describe an authored dashboard.
600
+ The authored shape is typed by `@object-ui/types`:
601
+
602
+ | Import from `@object-ui/types` | What it types |
603
+ | --- | --- |
604
+ | `DashboardComponentSchema` | the whole `type: 'dashboard'` node — `columns`, `gap`, `widgets`, `header`, `globalFilters`, `dateRange`, `refreshIntervalSeconds`, … |
605
+ | `DashboardWidgetSchema` | one entry of `widgets[]` — the spec's `DashboardWidget` keys, plus objectui's own (`component`, `layout`, `options`, …) |
606
+ | `DashboardWidgetSlotComponentSchema` | the other kind of `widgets[]` entry — a component node placed directly in the slot, `type` one of the closed component set (`metric-card`); its other keys are that component's own props, which it declares (`title`, `value`, `icon`, `trend`, `trendValue`, and `description` from `BaseSchema`), except the spec's widget `layout`, which places the node |
607
+ | `DashboardWidgetLayout` | a widget's `{ x, y, w, h }` grid box |
608
+
320
609
  ```typescript
321
- import type { DashboardSchema, MetricCardSchema } from '@object-ui/plugin-dashboard';
610
+ import type {
611
+ DashboardComponentSchema,
612
+ DashboardWidgetSchema,
613
+ DashboardWidgetSlotComponentSchema,
614
+ } from '@object-ui/types';
615
+
616
+ // Dataset-bound KPI — the widget vocabulary (see "Dashboard-level filters").
617
+ const revenue: DashboardWidgetSchema = {
618
+ id: 'kpi_revenue',
619
+ type: 'metric',
620
+ title: 'Revenue',
621
+ dataset: 'invoices',
622
+ values: ['count'],
623
+ colorVariant: 'success',
624
+ };
322
625
 
323
- const metricCard: MetricCardSchema = {
626
+ // Single-value widget with no query: the number lives under `options`.
627
+ const users: DashboardWidgetSchema = {
628
+ id: 'kpi_users',
629
+ type: 'metric',
630
+ title: 'Total Users',
631
+ options: { value: '1,234' },
632
+ };
633
+
634
+ // Component format: a registered component node in the widget's `component`
635
+ // slot. Its keys are that COMPONENT's props, not widget keys; a `metric-card`
636
+ // here is typed by the same arm as `kpi` below.
637
+ const custom: DashboardWidgetSchema = {
638
+ id: 'kpi_custom',
639
+ component: {
640
+ type: 'metric-card',
641
+ title: 'Revenue',
642
+ value: '$123,456',
643
+ trend: 'up',
644
+ trendValue: '+12%',
645
+ },
646
+ layout: { x: 0, y: 0, w: 3, h: 2 },
647
+ };
648
+
649
+ // Component node directly in the slot — the shape every `metric-card` example
650
+ // above uses. `type` is one of the closed component set; the other keys are
651
+ // the component's own props, which `DashboardWidgetSlotComponentSchema`
652
+ // declares.
653
+ const kpi: DashboardWidgetSlotComponentSchema = {
324
654
  type: 'metric-card',
325
655
  title: 'Revenue',
326
656
  value: '$123,456',
327
657
  trend: 'up',
328
- trendValue: '+12%'
658
+ trendValue: '+12%',
329
659
  };
330
660
 
331
- const dashboard: DashboardSchema = {
661
+ const dashboard: DashboardComponentSchema = {
332
662
  type: 'dashboard',
333
663
  columns: 3,
334
- widgets: [metricCard]
664
+ gap: 4,
665
+ widgets: [revenue, users, custom, kpi],
335
666
  };
336
667
  ```
337
668
 
669
+ There is **no per-widget-family schema type**: one `DashboardWidgetSchema`
670
+ covers every `type` (`metric`, `bar`, `table`, …) and the family-specific
671
+ settings live under `options`. `MetricCard`'s own props — `value`, `trend`,
672
+ `trendValue` — are the component's, not the widget's: `DashboardWidgetSchema`
673
+ declares none of them, and the component's props interface is not on this
674
+ package's export surface either. A `metric-card` node is typed as a COMPONENT
675
+ node in both places it can appear: in a widget's `component` slot (`custom`
676
+ above) and directly in `widgets[]` (`kpi` above, `DashboardWidgetSlotComponentSchema`
677
+ — the component arm of `DashboardComponentSchema['widgets']`, first in the
678
+ declaration as in the zod schema's two-arm slot). Either way its keys are
679
+ checked against that arm, which declares the card's registered inputs, never
680
+ as widget keys:
681
+
682
+ | Key | Type |
683
+ | --- | --- |
684
+ | `value` | `string \| number` — required |
685
+ | `title` | `string` or an inline per-locale map (`I18nLabel`) |
686
+ | `icon` | `string` — a Lucide icon name |
687
+ | `trend` | `'up' \| 'down' \| 'neutral'` — drawn with `trendValue` |
688
+ | `trendValue` | `string` — drawn with `trend` |
689
+ | `description` | `string` or `I18nLabel`, from `BaseSchema` |
690
+
691
+ `@object-ui/types/zod` judges the same members, so `objectui validate` refuses
692
+ a card directly in `widgets[]` whose `trend` is outside the three, or whose
693
+ `value` is neither a string nor a number.
694
+
695
+ ### Reading a widget key off `widgets[]`
696
+
697
+ The widget keys (`colorVariant`, `filter`, `dataset`, …) are declared on the
698
+ widget arm, `DashboardWidgetSchema`, which takes them from the spec's
699
+ `DashboardWidget` row. The component arm declares none of them. Read straight
700
+ off a `widgets[]` entry, such a key is typed `any`, supplied by the component
701
+ arm's passthrough. This package's own readers take an entry by the slot's
702
+ element type, `DashboardComponentSchema['widgets'][number]`. The component arm
703
+ is not assignable to `DashboardWidgetSchema`, whose `type` names no component
704
+ type, so narrow an entry on `type` first: `metric-card` is the one component
705
+ type the slot holds, and any other entry is the widget arm. The compiler checks
706
+ the narrowing, not an annotation, and the key gets its declared type:
707
+
708
+ ```typescript
709
+ import type { DashboardComponentSchema } from '@object-ui/types';
710
+
711
+ declare const dashboard: DashboardComponentSchema;
712
+
713
+ const accents = dashboard.widgets.map((w) =>
714
+ w.type === 'metric-card' ? 'default' : (w.colorVariant ?? 'default'),
715
+ );
716
+ ```
717
+
718
+ `title` and `layout` are the two widget keys both arms declare. `title` is the
719
+ card's heading on the component arm, typed as `MetricCard` reads it, so it reads
720
+ as a string or an `I18nLabel` straight off a `widgets[]` entry. The component
721
+ arm takes the spec's widget `layout` by reference, because Save Layout writes it
722
+ onto every entry of `widgets[]`, a `metric-card` node included (see
723
+ [DashboardGridLayout](#dashboardgridlayout--persisting-drag--resize-edits)). So
724
+ `layout` reads with the spec's type straight off a `widgets[]` entry, and the
725
+ strict authoring face accepts it on a component node. A malformed one (not
726
+ four numbers, or a key besides `x`, `y`, `w` and `h`) is refused on either arm.
727
+
728
+ A widget with no `layout` is auto-placed by the grid. `DashboardWithConfig`'s
729
+ width and height sliders write the whole box: the dimension a slider does not
730
+ edit, and `x` and `y`, come from where the grid auto-places that widget
731
+ (`completeWidgetLayout` and `defaultWidgetPlacement` from `@object-ui/types`,
732
+ objectui#11388), so a one-dimension edit never stores a box the spec refuses.
733
+
338
734
  ## Customization
339
735
 
340
736
  All components support Tailwind CSS classes:
341
737
 
342
738
  ```typescript
343
739
  const schema = {
344
- type: 'metric-card',
345
- title: 'Custom Metric',
346
- value: '100',
347
- className: 'bg-gradient-to-r from-blue-500 to-purple-600 text-white'
740
+ type: 'dashboard',
741
+ widgets: [
742
+ {
743
+ type: 'metric-card',
744
+ title: 'Custom Metric',
745
+ value: '100',
746
+ className: 'bg-gradient-to-r from-blue-500 to-purple-600 text-white'
747
+ }
748
+ ]
348
749
  };
349
750
  ```
350
751
 
@@ -369,10 +770,29 @@ object schema once and infers the renderer from the bound field:
369
770
  | `date` / `datetime` | Locale-formatted date |
370
771
  | `currency` | Locale currency (or honour `format: '$0,0'`) |
371
772
  | `percent` | `0%` / `0.0%` formatted (honour `format`) |
773
+ | `password` / `secret` | `••••••`, and the column is flagged `masked` (see below) |
372
774
 
373
775
  Author overrides always win — pass `type`, `format`, `options`,
374
- `referenceTo`, or your own `cell` function on a column to bypass
375
- auto-detection.
776
+ `currency`, or your own `cell` function on a column to bypass
777
+ auto-detection. An explicit `currency` (ISO 4217 code, e.g. `"EUR"`)
778
+ wins over both the symbol inferred from `format` and the tenant default
779
+ currency. A lookup column's related-object target always comes from the
780
+ object schema's own field definition — there is no column-level override
781
+ for it (objectui#6597: measured no authoring story for one).
782
+
783
+ A masked column (objectui#10657) is one the table withholds: no Ctrl+C / Cmd+C
784
+ copy, no tooltip, no CSV export column, no match in the search box, no header
785
+ sort, and a width sized from its header rather than its values. The widget
786
+ decides it with `isMaskedFieldType()` from `@object-ui/fields`, over the
787
+ column's authored `type` and the object's field type, so this is the one place
788
+ an author override does not win: `type: 'text'` over a `secret` field keeps the
789
+ flag, though the cell then draws the value as text. Rows handed in as `data`
790
+ or through `bind` can be drawn before the object schema arrives, and a failed
791
+ schema read never delivers one; in that window the widget flags every column,
792
+ and a column with no `type` of its own draws as the mask, never as text, until
793
+ the schema arrives, or for good when the read failed (objectui#10657). A column
794
+ that authors its `type` draws from it meanwhile. The record drawer a row opens
795
+ (record drill-down) draws every value as the mask in that window too.
376
796
 
377
797
  ```jsonc
378
798
  {
@@ -390,9 +810,7 @@ auto-detection.
390
810
  }
391
811
  ```
392
812
 
393
- ## DashboardGridLayout — persisting drag / resize edits
394
-
395
- ### DashboardRenderer — design-mode widget reorder
813
+ ## DashboardRenderer — design-mode widget reorder
396
814
 
397
815
  When `DashboardRenderer` is used in design mode (`designMode={true}` plus an
398
816
  `onWidgetsReorder` callback), widgets become sortable via
@@ -405,22 +823,74 @@ calls `onWidgetsReorder(nextWidgets)` with the reordered array; the host (e.g.
405
823
  A 5px pointer-activation distance keeps click-to-select working on the same
406
824
  widget surface.
407
825
 
826
+ ## DashboardRenderer — a document the server already translated (`localized`)
827
+
828
+ A dashboard read from ObjectStack's `/meta` route arrives translated for the
829
+ request's locale. The server resolves the packaged translation catalog, and it
830
+ keeps a published edit over that catalog: an explicit override beats the
831
+ packaged default. Pass `localized={true}` when your document came from such a
832
+ read. The renderer then draws these texts as given:
833
+
834
+ - the widget `title`, `description` and sub-caption (`options.description`);
835
+ - its own header `label` and `description`.
836
+
837
+ An inline per-locale map is still collapsed to the active language. The client
838
+ bundle is not consulted again. A second lookup would let the packaged catalog
839
+ win back over a published edit (objectui#11295). The console's dashboard page
840
+ (`DashboardView`) sets the prop.
841
+
842
+ Leave it unset for a document the server never translated: an inline block, a
843
+ preview or a design surface. The bundle keys under
844
+ `dashboards.<name>.widgets.<id>.*` are then that document's one translation
845
+ pass, as before. Header-action labels always go through the bundle, because the
846
+ server does not translate them.
847
+
408
848
  ## DashboardGridLayout — persisting drag / resize edits
409
849
 
410
- `DashboardGridLayout` (registered as schema `type: 'dashboard-grid'`) has an
850
+ `DashboardGridLayout` (a React component you mount directly; its old schema
851
+ `type: 'dashboard-grid'` was retired by objectui#10859) has an
411
852
  inline **"Edit Layout"** mode that lets users drag and resize widgets via
412
853
  `react-grid-layout`. When the user clicks **Save Layout**, the new grid
413
854
  coordinates are merged back into `schema.widgets[].layout` and handed off
414
855
  through the `onSchemaChange` callback.
415
856
 
416
857
  ```tsx
417
- <DashboardGridLayout
418
- schema={dashboard}
419
- // ✅ Preferred — write the updated schema through your data adapter.
420
- onSchemaChange={(next) => client.meta.saveItem('dashboard', next.name, next)}
421
- />
858
+ import { DashboardGridLayout } from '@object-ui/plugin-dashboard';
859
+ import type { ObjectStackAdapter } from '@object-ui/data-objectstack';
860
+ import type { DashboardComponentSchema } from '@object-ui/types';
861
+
862
+ // `dashboard` is the node being edited; `adapter` is host-provided.
863
+ declare const dashboard: DashboardComponentSchema;
864
+ declare const adapter: ObjectStackAdapter;
865
+
866
+ function DashboardEditor() {
867
+ const client = adapter.getClient();
868
+ return (
869
+ <DashboardGridLayout
870
+ schema={dashboard}
871
+ // ✅ Preferred — write the updated schema through your data adapter.
872
+ onSchemaChange={(next) => {
873
+ // `name` is optional on the schema, and `saveItem` requires it — decide
874
+ // what an unnamed dashboard means to your host instead of writing through.
875
+ if (!next.name) return;
876
+ client.meta.saveItem('dashboard', next.name, next);
877
+ }}
878
+ />
879
+ );
880
+ }
422
881
  ```
423
882
 
883
+ The guard is not defensive noise: the callback receives a
884
+ `DashboardComponentSchema`, whose `name` is optional (`BaseSchema.name` in
885
+ `@object-ui/types`), while `client.meta.saveItem(type, name, item)` declares
886
+ `name: string`. Passing `next.name` straight through is `TS2345` under `strict`
887
+ (`Argument of type 'string | undefined' is not assignable to parameter of type
888
+ 'string'`), so a copied snippet does not compile — and what the server does with
889
+ an absent name has **not** been measured here, which is the other reason this
890
+ example declines to send one rather than guessing. A host that already knows
891
+ which metadata item the grid is editing should pass that name from its own state
892
+ instead of reading it off the node.
893
+
424
894
  If `onSchemaChange` is **not** provided, layout edits stay in component
425
895
  state and are lost on refresh — a `console.warn` is emitted in development
426
896
  to flag the missing wiring. The component never writes to `localStorage`