Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changeset/11022-strict-widget-slot-registered-inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ The strict authoring face accepts a correctly authored `metric-card` in a dashbo
No migration: a document that parsed on either face still parses there.

⚠️ **Dated note, 2026-10-01 — the slot arm declares `layout` — objectui#11070.** "the slot arm's shape and catchall are what they were" and "a document that parsed on either face still parses there" above held when this change landed. Later in this same release, round 11 of objectui#11070 added `layout` to the slot arm's shape, by reference to the spec's widget `layout`. So the tolerant face now refuses a malformed `layout` on a `metric-card` in the widget slot. `.changeset/11070-dashboard-keys-round11.md` states what ships; the text above is kept as the reading of this change.

⚠️ **Dated note, 2026-10-02 — the slot arm declares `metric-card`'s registered inputs — objectui#11467.** Three readings above held when this change landed. Later in this same release, objectui#11467 made all three false. "The slot's component-node arm now records the input names the `metric-card` registration declares" and "`deriveStrictAuthoringSchema` admits those names on that node before closing it": the arm now declares `title`, `value`, `icon`, `trend` and `trendValue` as members on both faces. The recorded names and the walker step that admitted them are retired, so both faces judge each value by its member. "its `component` is still judged as a plain `BaseSchema` node on both faces": that slot now takes the arm first, then `BaseSchema`. "No migration: a document that parsed on either face still parses there": both faces now refuse a `metric-card` in the widget slot whose `trend`, `title`, `icon`, `trendValue` or `value` falls outside its member, or that carries `icon`, `trend` or `trendValue` without `value`. `.changeset/11467-metric-card-arm-inputs.md` states what ships; the text above is kept as the reading of this change.

⚠️ **Dated note, 2026-10-02 — the tolerant face's arm moves too — objectui#11467.** "The tolerant face (`AnyComponentSchema`, `DashboardComponentSchema` and every other mirror): its accept set, output and inferred types are unchanged" above held when this change landed. objectui#11467 changed the slot arm's accept set and inferred type on the tolerant face, as the note above states for what it now refuses. `.changeset/11467-metric-card-arm-inputs.md` states what ships; the text above is kept as the reading of this change.
2 changes: 2 additions & 0 deletions .changeset/11070-dashboard-keys-round11.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ The strict authoring face accepts the `layout` that the editable dashboard grid'
**Fix:** write `layout` as the four numbers Save Layout writes, or leave it out.

`options.description` is not changed by this release. `@objectstack/spec` does not declare it on the widget's `options`, so the strict face keeps refusing an authored one (objectui#11228 ruling C), and its status is an open decision on objectui#11070. Comments in `@object-ui/plugin-dashboard` that called it declared now say so; that package's runtime behaviour is unchanged.

⚠️ **Dated note, 2026-10-02 — the slot arm declares `metric-card`'s registered inputs — objectui#11467.** "The widget arm, the component node's other keys and the registered-input record objectui#11022 added", under "What does not move" above, held when this change landed. Later in this same release, objectui#11467 moved all three. The widget arm's `component` member now takes the slot's component arm first, then `BaseSchema`. The component node declares `title`, `value`, `icon`, `trend` and `trendValue` as members on both faces, so each value is judged by its member. The registered-input record and the strict walker's step that admitted it are retired. `.changeset/11467-metric-card-arm-inputs.md` states what ships; the text above is kept as the reading of this change.
2 changes: 2 additions & 0 deletions .changeset/11348-dashboard-widget-reads.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ a declaration on `ObjectDataTableSchema` with its own changeset; the drawer's
literal is unchanged.

⚠️ **Dated note, 2026-10-01 — the component arm now declares `layout` — objectui#11070.** "the component arm has no spec row and declares none of them" above held when this change landed. Later in this same release, round 11 of objectui#11070 declared `layout` on the component arm, by reference to the spec's widget `layout`, because Save Layout writes it onto every `widgets[]` entry. `title` and `colorVariant` are still declared on the widget arm only. `.changeset/11070-dashboard-keys-round11.md` states what ships; the text above is kept as the reading of this change.

⚠️ **Dated note, 2026-10-02 — the component arm now declares `title` — objectui#11467.** "`title` and `colorVariant` are still declared on the widget arm only" in the note above held when that note was written. Later in this same release, objectui#11467 declared `metric-card`'s registered inputs on the component arm, `title` among them as the card's heading, typed as `MetricCard` reads it. So `title` is declared on both arms, and it reads as `string | I18nLabel` straight off a `widgets[]` entry. `colorVariant` is still declared on the widget arm only. `.changeset/11467-metric-card-arm-inputs.md` states what ships; the text above is kept as the reading of this change.
17 changes: 17 additions & 0 deletions .changeset/11467-metric-card-arm-inputs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@object-ui/types': minor
---

A `metric-card` in a dashboard's widget slot is checked against the props `MetricCard` renders, on the TypeScript face and in the validator (objectui#11467). This narrows a published accept set on both faces and widens the strict authoring face in one place. Both changes are stated below. `minor`, per this repository's version alignment; the narrowing is the breaking part.

`DashboardWidgetSlotComponentSchema` is the widget slot's component arm. It used to type the card's props only through `BaseSchema`'s `[key: string]: any`, and its zod twin admitted them through `.passthrough()` and judged nothing. The strict face (`StrictAnyComponentSchema`) admitted their names from a side table and judged their values by that same catchall.

- **What changed.** The arm declares `metric-card`'s registered inputs as members, with the types of the props `MetricCard` reads: `title` (a string or an inline per-locale map, `I18nLabel`), `value` (`string | number`, required, as the registration marks it), `icon` (`string`), `trend` (`'up' | 'down' | 'neutral'`, the registration's enum) and `trendValue` (`string`). The sixth input, `description`, was already a `BaseSchema` member. The zod twin declares the same members, so every face judges each value by its member. The widget's legacy `component` slot now takes the arm first and then any other node, on both faces: `component?: DashboardWidgetSlotComponentSchema | SchemaNode`. The zod twin is `z.union([arm, BaseSchema])`.
- **What now refuses that did not.** Directly in `widgets[]`, `AnyComponentSchema` / `DashboardComponentSchema` and `StrictAnyComponentSchema` now refuse a card with any of these: a `trend` outside the three; a `title`, `icon` or `trendValue` that is not a string (`title` may also be a per-locale map); a `value` that is neither a string nor a number; or `icon`, `trend` or `trendValue` with no `value`. On the TypeScript face each of these literals is now a compile error, and so is a card annotated `DashboardWidgetSlotComponentSchema` with no `value`. All of them used to pass through the index signature and the passthrough. Every `metric-card` in the schema catalog, the docs fences and the apps' authored documents still parses on both faces. This was re-measured with `scripts/measure-strict-authoring-face.mjs` at this change's base and on this change, and no document moved on the tolerant face.
- **What now parses that did not.** On the strict face, a `metric-card` in a widget's `component` slot carrying its inputs. `BaseSchema` alone in that slot refused them as unrecognized, including in the plugin-dashboard README's own legacy-envelope example.
- **What does not move.** Any key the arm does not declare is still kept by the tolerant face's passthrough and refused by name on the strict face. The widget arm, `DashboardWidgetSchema`, still declares none of `value`, `icon`, `trend` and `trendValue`. Any other component node in the `component` slot is judged by `BaseSchema` as before.
- **Read types.** `title` now reads as `string | I18nLabel` straight off a `widgets[]` entry instead of `any`, because both arms declare it.

**Fix:** write `trend` as one of the three, give the card a `value`, and keep `title`, `icon` and `trendValue` as strings (`title` may also be a per-locale map).

The internal `declareRegisteredInputs` side table (objectui#11022) and the strict walker's branch for it are removed. Every name it recorded is now a member of the arm. Neither was exported from the package.
26 changes: 15 additions & 11 deletions content/docs/plugins/plugin-dashboard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -76,17 +76,18 @@ A `metric-card` is a dashboard widget-slot entry: it sits directly in a
and the dashboard renders it as the `plugin-dashboard:metric-card` node. The
bare `metric-card` NODE key is retired (objectui#10859), so a `metric-card`
outside a dashboard's `widgets[]` renders nothing but the renderer's
"Unknown component type" panel. Its keys:
"Unknown component type" panel. Its keys, as `DashboardWidgetSlotComponentSchema`
(`@object-ui/types`) declares them and `@object-ui/types/zod` judges them:

```plaintext
{
type: 'metric-card',
title: string,
value: string | number,
title?: string | I18nLabel, // plain string or inline per-locale map
value: string | number, // required
icon?: string, // Lucide icon name
trend?: 'up' | 'down' | 'neutral',
trendValue?: string,
description?: string,
trend?: 'up' | 'down' | 'neutral', // drawn with trendValue
trendValue?: string, // drawn with trend
description?: string | I18nLabel,
className?: string
}
```
Expand Down Expand Up @@ -463,7 +464,8 @@ const dashboard: DashboardComponentSchema = {

An entry of `widgets[]` is either a widget (`DashboardWidgetSchema`) or a
component node placed directly in the slot (`DashboardWidgetSlotComponentSchema`).
The widget keys (`title`, `colorVariant`, `filter`, …) are declared on the widget
The component arm declares the card's registered inputs — the keys above. The
widget keys (`colorVariant`, `filter`, `dataset`, …) are declared on the widget
arm, which takes them from the spec's `DashboardWidget` row, and on no member of
the component arm. To read one off `widgets[]`, read it through
`DashboardWidgetSchema`. The component arm is assignable to that type, so the
Expand All @@ -479,10 +481,12 @@ const widgets: DashboardWidgetSchema[] = dashboard.widgets;
const accents = widgets.map((w) => w.colorVariant ?? 'default');
```

`layout` is the exception: both arms declare it, the component arm by reference
to the spec's widget `layout`, because the editable grid's Save Layout writes it
onto every entry, a `metric-card` node included. It reads with the spec's type
off any `widgets[]` entry, the strict authoring face accepts it on a component
`title` and `layout` are the exceptions: both arms declare them. `title` is the
card's heading on the component arm, typed as `MetricCard` reads it. The
component arm declares `layout` by reference to the spec's widget `layout`,
because the editable grid's Save Layout writes it onto every entry, a
`metric-card` node included. `layout` reads with the spec's type off any
`widgets[]` entry, the strict authoring face accepts it on a component
node, and a malformed one (not four numbers, or a key besides `x`, `y`, `w` and
`h`) is refused on either arm.

Expand Down
37 changes: 27 additions & 10 deletions packages/plugin-dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ The authored shape is typed by `@object-ui/types`:
| --- | --- |
| `DashboardComponentSchema` | the whole `type: 'dashboard'` node — `columns`, `gap`, `widgets`, `header`, `globalFilters`, `dateRange`, `refreshIntervalSeconds`, … |
| `DashboardWidgetSchema` | one entry of `widgets[]` — the spec's `DashboardWidget` keys, plus objectui's own (`component`, `layout`, `options`, …) |
| `DashboardWidgetSlotComponentSchema` | the other kind of `widgets[]` entry — a component node placed directly in the slot, `type` one of the closed component set (`metric-card`); every other key is that component's own prop, except the spec's widget `layout`, which places the node |
| `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 |
| `DashboardWidgetLayout` | a widget's `{ x, y, w, h }` grid box |

```typescript
Expand Down Expand Up @@ -592,7 +592,8 @@ const users: DashboardWidgetSchema = {
};

// Component format: a registered component node in the widget's `component`
// slot. Its keys are that COMPONENT's props, not widget keys.
// slot. Its keys are that COMPONENT's props, not widget keys; a `metric-card`
// here is typed by the same arm as `kpi` below.
const custom: DashboardWidgetSchema = {
id: 'kpi_custom',
component: {
Expand All @@ -607,7 +608,8 @@ const custom: DashboardWidgetSchema = {

// Component node directly in the slot — the shape every `metric-card` example
// above uses. `type` is one of the closed component set; the other keys are
// the component's own props, carried by `BaseSchema`'s index signature.
// the component's own props, which `DashboardWidgetSlotComponentSchema`
// declares.
const kpi: DashboardWidgetSlotComponentSchema = {
type: 'metric-card',
title: 'Revenue',
Expand All @@ -633,13 +635,26 @@ package's export surface either. A `metric-card` node is typed as a COMPONENT
node in both places it can appear: in a widget's `component` slot (`custom`
above) and directly in `widgets[]` (`kpi` above, `DashboardWidgetSlotComponentSchema`
— the component arm of `DashboardComponentSchema['widgets']`, first in the
declaration as in the zod schema's two-arm slot). Its keys are checked as
`BaseSchema` keys either way,
never as widget keys.
declaration as in the zod schema's two-arm slot). Either way its keys are
checked against that arm, which declares the card's registered inputs, never
as widget keys:

| Key | Type |
| --- | --- |
| `value` | `string \| number` — required |
| `title` | `string` or an inline per-locale map (`I18nLabel`) |
| `icon` | `string` — a Lucide icon name |
| `trend` | `'up' \| 'down' \| 'neutral'` — drawn with `trendValue` |
| `trendValue` | `string` — drawn with `trend` |
| `description` | `string` or `I18nLabel`, from `BaseSchema` |

`@object-ui/types/zod` judges the same members, so `objectui validate` refuses
a card directly in `widgets[]` whose `trend` is outside the three, or whose
`value` is neither a string nor a number.

### Reading a widget key off `widgets[]`

The widget keys (`title`, `colorVariant`, `filter`, …) are declared on the
The widget keys (`colorVariant`, `filter`, `dataset`, …) are declared on the
widget arm, `DashboardWidgetSchema`, which takes them from the spec's
`DashboardWidget` row. The component arm declares none of them. Read straight
off a `widgets[]` entry, such a key is typed `any`, supplied by the component
Expand All @@ -657,9 +672,11 @@ const widgets: DashboardWidgetSchema[] = dashboard.widgets;
const accents = widgets.map((w) => w.colorVariant ?? 'default');
```

`layout` is the one widget key both arms declare. The component arm takes the
spec's widget `layout` by reference, because Save Layout writes it onto every
entry of `widgets[]`, a `metric-card` node included (see
`title` and `layout` are the two widget keys both arms declare. `title` is the
card's heading on the component arm, typed as `MetricCard` reads it, so it reads
as a string or an `I18nLabel` straight off a `widgets[]` entry. The component
arm takes the spec's widget `layout` by reference, because Save Layout writes it
onto every entry of `widgets[]`, a `metric-card` node included (see
[DashboardGridLayout](#dashboardgridlayout--persisting-drag--resize-edits)). So
`layout` reads with the spec's type straight off a `widgets[]` entry, and the
strict authoring face accepts it on a component node. A malformed one (not
Expand Down
Loading
Loading