From 791c7b5bccc6c0260c658a6c28acfd6a45cd24b8 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Thu, 30 Jul 2026 15:58:03 +0800 Subject: [PATCH 1/5] feat(spec,lint): the ui vocabularies admit what the renderers implement (objectui#2945) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Additions-only follow-up to the vocabulary audit. Nothing narrows, so no already-stored metadata changes meaning. - ChartTypeSchema admits `combo`. The taxonomy could not name the one family the rest of chart.zod.ts is written for: ChartSeriesSchema.type exists to override a series' type ("combo charts", per its own doc comment) and ChartSeriesSchema.yAxis binds left/right, which only means something for mixed marks. objectui draws it distinctly and carried `combo` in a local fork whose comment claimed to mirror this list. - WidgetActionTypeSchema is ActionType. They disagreed by `form`, and backwards: a widget/header action dispatches through the same ActionRunner that implements `form` (DashboardRenderer routes everything but a raw `url` into it, deliberately — #3528), so the narrower enum rejected at validation what the dispatcher executes. - ListChartConfigSchema.chartType is ChartTypeSchema.extract([...]) — same five members, a de-duplication rather than a widening. - @objectstack/lint's chart-family set derives from the taxonomy. Its omissions failed in the worst direction: an unlisted family read as "not a chart", so a widget with no chartConfig measure mapping PASSED validation. Pinning the old hand-written list back reproduces exactly that for `combo` — zero findings — which is the new test's teeth. ActionType deliberately does NOT gain `navigation`: executeNavigation is a strictly weaker executeUrl (no ${param} interpolation, no apiBase promotion, no openIn) differing only by `replace`, and its one live producer is the SDUI element:button `action` prop that ElementButtonPropsSchema does not model at all. Adding the name would put a second spelling of "navigate" into a closed vocabulary without closing the real gap. spec 6944 tests / 267 files and lint 544 / 37 green; tsc clean on both. Refs objectstack-ai/objectui#2945, objectstack-ai/objectui#2901, #3528 Co-Authored-By: Claude Opus 5 --- .../ui-vocabularies-derive-not-restate.md | 60 ++++++++++++++++ .../lint/src/validate-widget-bindings.test.ts | 16 ++++- packages/lint/src/validate-widget-bindings.ts | 29 +++++--- packages/spec/src/ui/chart.test.ts | 6 ++ packages/spec/src/ui/chart.zod.ts | 8 +++ packages/spec/src/ui/dashboard.zod.ts | 21 ++++-- packages/spec/src/ui/view.zod.ts | 10 ++- .../spec/src/ui/vocabulary-derivation.test.ts | 70 +++++++++++++++++++ 8 files changed, 203 insertions(+), 17 deletions(-) create mode 100644 .changeset/ui-vocabularies-derive-not-restate.md create mode 100644 packages/spec/src/ui/vocabulary-derivation.test.ts diff --git a/.changeset/ui-vocabularies-derive-not-restate.md b/.changeset/ui-vocabularies-derive-not-restate.md new file mode 100644 index 0000000000..6f6d899bb0 --- /dev/null +++ b/.changeset/ui-vocabularies-derive-not-restate.md @@ -0,0 +1,60 @@ +--- +"@objectstack/spec": minor +"@objectstack/lint": patch +--- + +feat(spec,lint): the `ui` vocabularies admit what the renderers implement, and derive instead of restating (objectui#2945) + +Additions-only follow-up to the vocabulary audit +(objectstack-ai/objectui#2901, #2945). Nothing here narrows a vocabulary, so no +already-stored metadata changes meaning — three of the four `ui/` enums that had +drifted from what is actually implemented, plus the fork that drift had made +invisible. + +**`ChartTypeSchema` admits `combo`.** The taxonomy could not name the one chart +family the rest of `chart.zod.ts` is written for: `ChartSeriesSchema.type` +exists to override a series' type — its doc comment literally says *"combo +charts"* — and `ChartSeriesSchema.yAxis` binds a series to the left or right +axis, which is only meaningful for mixed marks. objectui's renderer draws it +distinctly (mixed bar/line/area on dual axes, per-series type) and had to carry +`combo` in a local fork of this list, whose own comment claimed to mirror it. + +**`WidgetActionTypeSchema` is `ActionType`.** The two disagreed by one member, +`form`, and the disagreement was backwards: a dashboard header or widget action +button dispatches through the same `ActionRunner` that implements `form` — +objectui's `DashboardRenderer` deliberately routes everything except a raw `url` +into it, so a `flow` header action works (#3528). The narrower enum therefore +rejected at validation exactly what the shared dispatcher then executes. +Derived, so the next type the runner implements needs one edit, not two. + +**`ListChartConfigSchema.chartType` is `ChartTypeSchema.extract([...])`.** Same +five members as before — a de-duplication, not a widening. A member renamed in +the taxonomy now fails at build time instead of leaving a second list quietly +disagreeing. + +**`@objectstack/lint`'s chart-family set is derived from the taxonomy.** +`validate-widget-bindings` decides which widgets need a `chartConfig` measure +mapping from a hand-written list of families, and its omissions fail in the +worst direction: an unlisted family reads as *"not a chart"*, so a widget +missing its mapping **passes** validation. `combo` was exactly that case — +verified by pinning the old list back, where a `combo` widget with no +`chartConfig` produced zero findings. The set is now the taxonomy minus an +explicit `MEASURE_EXEMPT_CHART_TYPES` (single-value and tabular families), so a +family added to the spec is covered without editing the rule. + +Guards: `packages/spec/src/ui/vocabulary-derivation.test.ts` asserts both +derivations still hold (a restated list fails silently — it keeps validating, +just not what the other list says), and the lint suite now walks every +multi-series family in the taxonomy rather than a list of its own. + +`ActionType` deliberately does **not** gain `navigation`, which the audit +suggested. `ActionRunner.executeNavigation` is a strictly weaker +`executeUrl` — no `${param.X}` interpolation, no `apiBase` promotion, no +`openIn` — differing only by a `replace` option, and its one live producer is +the SDUI `element:button` `action` prop, which `ElementButtonPropsSchema` does +not model at all. Promoting the name would add a second spelling of *navigate* +to a closed authorable vocabulary (members cannot be removed later) without +closing the gap that actually exists. Tracked separately. + +Verified: `@objectstack/spec` **6944 tests / 267 files**, `@objectstack/lint` +**544 tests / 37 files**, both green; `tsc --noEmit` clean on both. diff --git a/packages/lint/src/validate-widget-bindings.test.ts b/packages/lint/src/validate-widget-bindings.test.ts index ea53238909..9786e33445 100644 --- a/packages/lint/src/validate-widget-bindings.test.ts +++ b/packages/lint/src/validate-widget-bindings.test.ts @@ -203,11 +203,25 @@ describe('validateWidgetBindings (reference integrity, issue #1721)', () => { }); it('(d) non-chart types do not warn on missing chartConfig', () => { - for (const type of ['metric', 'kpi', 'gauge', 'table']) { + for (const type of ['metric', 'kpi', 'gauge', 'solid-gauge', 'bullet', 'table', 'pivot']) { expect(validateWidgetBindings(chartStack({ type, chartConfig: undefined }))).toHaveLength(0); } }); + it('(d) every multi-series family in the taxonomy warns — including a newly added one', () => { + // The set of chart families that need a measure mapping is derived from + // `ChartTypeSchema`, so a family added to the taxonomy is covered without + // editing a list here. `combo` is the case that proved it: as a hand-written + // list, an unlisted family read as "not a chart" and the missing mapping + // went unreported. objectui#2945. + for (const type of ['bar', 'horizontal-bar', 'column', 'line', 'area', 'pie', + 'donut', 'funnel', 'scatter', 'treemap', 'sankey', 'radar', 'combo']) { + const findings = validateWidgetBindings(chartStack({ type, chartConfig: undefined })); + expect(findings, `expected a warning for chart type '${type}'`).toHaveLength(1); + expect(findings[0].rule).toBe(CHART_CONFIG_MISSING); + } + }); + it('errors are NOT suppressible via suppressWarnings', () => { const findings = validateWidgetBindings(chartStack({ dataset: 'no_such_dataset', diff --git a/packages/lint/src/validate-widget-bindings.ts b/packages/lint/src/validate-widget-bindings.ts index cd3c82282d..8ea759e28b 100644 --- a/packages/lint/src/validate-widget-bindings.ts +++ b/packages/lint/src/validate-widget-bindings.ts @@ -1,6 +1,7 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. import { isIncoherentAggregate } from '@objectstack/spec/data'; +import { ChartTypeSchema } from '@objectstack/spec/ui'; /** * Build-time dashboard widget binding diagnostics (issues #1719, #1721). @@ -139,17 +140,29 @@ function asStrings(v: unknown): string[] { } /** - * Chart families whose renderer needs a `chartConfig` measure mapping. - * Single-value types (metric/kpi/gauge/…) plot their lone value and tabular - * types (table/pivot) render every column, so they are exempt. + * Chart families that plot a single value or every column, and so need no + * `chartConfig` measure mapping: single-value types plot their lone value, + * tabular types render each column as-is. */ -const CHART_TYPES = new Set([ - 'bar', 'horizontal-bar', 'column', - 'line', 'area', - 'pie', 'donut', 'funnel', - 'scatter', 'treemap', 'sankey', 'radar', +const MEASURE_EXEMPT_CHART_TYPES = new Set([ + 'gauge', 'solid-gauge', 'metric', 'kpi', 'bullet', + 'table', 'pivot', ]); +/** + * Chart families whose renderer needs a `chartConfig` measure mapping — the + * taxonomy minus the exemptions above. + * + * Derived from `ChartTypeSchema` rather than restated. As a hand-written list it + * had no way to know when the taxonomy grew, and the omission is silent in + * exactly the wrong direction: an unlisted family is treated as "not a chart", + * so a widget missing its measure mapping passes validation instead of being + * reported. objectui#2945. + */ +const CHART_TYPES = new Set( + ChartTypeSchema.options.filter(t => !MEASURE_EXEMPT_CHART_TYPES.has(t)), +); + function levenshtein(a: string, b: string): number { const m = a.length, n = b.length; let prev = Array.from({ length: n + 1 }, (_, j) => j); diff --git a/packages/spec/src/ui/chart.test.ts b/packages/spec/src/ui/chart.test.ts index 1bbfb0b143..9cd33677a0 100644 --- a/packages/spec/src/ui/chart.test.ts +++ b/packages/spec/src/ui/chart.test.ts @@ -47,6 +47,12 @@ describe('ChartTypeSchema', () => { }); }); + it('should accept the mixed chart type', () => { + // `combo` draws bar/line/area series together on left/right axes — the + // family `ChartSeriesSchema.type` and `ChartSeriesSchema.yAxis` configure. + expect(() => ChartTypeSchema.parse('combo')).not.toThrow(); + }); + it('should accept all performance chart types', () => { const types = ['gauge', 'metric', 'kpi'] as const; diff --git a/packages/spec/src/ui/chart.zod.ts b/packages/spec/src/ui/chart.zod.ts index 5b1daa9063..41fb1f2fc3 100644 --- a/packages/spec/src/ui/chart.zod.ts +++ b/packages/spec/src/ui/chart.zod.ts @@ -37,6 +37,14 @@ export const ChartTypeSchema = lazySchema(() => z.enum([ 'treemap', 'sankey', + // Mixed — bar/line/area series on shared dual axes. `ChartSeriesSchema.type` + // and `ChartSeriesSchema.yAxis` below exist precisely to configure this + // family (the former's own doc comment says "combo charts"), so the taxonomy + // was unable to name the one chart type the rest of the file is written for. + // objectui's renderer draws it distinctly — mixed marks, left/right axes, + // per-series type — and had to carry `combo` in a local fork of this list. + 'combo', + // Performance (single value — metric/kpi render a number; gauge/solid-gauge/ // bullet are honest single-value variants pending a real dial/target renderer) 'gauge', diff --git a/packages/spec/src/ui/dashboard.zod.ts b/packages/spec/src/ui/dashboard.zod.ts index 386f3272b9..72b08440f2 100644 --- a/packages/spec/src/ui/dashboard.zod.ts +++ b/packages/spec/src/ui/dashboard.zod.ts @@ -6,6 +6,7 @@ import { MetadataProtectionFields } from '../kernel/metadata-protection.zod'; import { FilterConditionSchema } from '../data/filter.zod'; import { DateGranularity } from '../data/query.zod'; import { ChartTypeSchema, ChartConfigSchema } from './chart.zod'; +import { ActionType } from './action.zod'; import { SnakeCaseIdentifierSchema } from '../shared/identifiers.zod'; import { I18nLabelSchema, AriaPropsSchema } from './i18n.zod'; import { ResponsiveConfigSchema } from './responsive.zod'; @@ -28,14 +29,20 @@ export const WidgetColorVariantSchema = lazySchema(() => z.enum([ /** * Action type for widget action buttons. + * + * `ActionType` itself, not a hand-kept subset of it. The two lists had drifted + * apart by one member — `form` — and the disagreement was backwards: a + * dashboard header or widget action button dispatches through the same + * `ActionRunner` that implements `form` (objectui's `DashboardRenderer` routes + * everything except a raw `url` into it, deliberately, so a `flow` header + * action works — objectstack#3528). So the narrower enum rejected at validation + * exactly what the shared dispatcher then executes at runtime. + * + * Derived rather than restated: a type added to `ActionType` is dispatchable + * from a widget the moment the runner implements it, with no second list to + * remember. */ -export const WidgetActionTypeSchema = lazySchema(() => z.enum([ - 'script', - 'url', - 'modal', - 'flow', - 'api', -]).describe('Widget action type')); +export const WidgetActionTypeSchema = lazySchema(() => ActionType.describe('Widget action type')); /** * Dashboard Header Action Schema diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index fc824ffa11..8c2a048913 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -7,6 +7,7 @@ import { SnakeCaseIdentifierSchema } from '../shared/identifiers.zod'; import { ExpressionInputSchema } from '../shared/expression.zod'; import { normalizeVisibleWhen, strictVisibilityError } from '../shared/visibility'; import { I18nLabelSchema, AriaPropsSchema } from './i18n.zod'; +import { ChartTypeSchema } from './chart.zod'; import { SharingConfigSchema } from './sharing.zod'; import { retiredKey } from '../shared/retired-key'; import { FieldType, SelectOptionSchema } from '../data/field.zod'; @@ -467,7 +468,14 @@ export const KanbanConfigSchema = lazySchema(() => z.object({ * `ChartConfigSchema` in `chart.zod.ts` (which is for embedded reports). */ export const ListChartConfigSchema = lazySchema(() => z.object({ - chartType: z.enum(['bar', 'line', 'pie', 'area', 'scatter']).default('bar').describe('Chart visualisation type'), + /** + * Narrowed from `ChartTypeSchema` with `.extract()` rather than retyped. + * Same five members as before — this is a de-duplication, not a widening — + * but a member renamed in the chart taxonomy now fails here at build time + * instead of leaving a second list quietly disagreeing with the first. + */ + chartType: ChartTypeSchema.extract(['bar', 'line', 'pie', 'area', 'scatter']) + .default('bar').describe('Chart visualisation type'), /** * ADR-0021 — the semantic-layer `dataset` this chart binds to. Selects * dimensions/measures BY NAME so the chart's numbers stay consistent with diff --git a/packages/spec/src/ui/vocabulary-derivation.test.ts b/packages/spec/src/ui/vocabulary-derivation.test.ts new file mode 100644 index 0000000000..a63eeba050 --- /dev/null +++ b/packages/spec/src/ui/vocabulary-derivation.test.ts @@ -0,0 +1,70 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Two `ui/` vocabularies are now *derived* from another rather than restated + * (objectstack-ai/objectui#2945). These tests assert the derivation still holds, + * because the failure mode of a restated list is silence: it keeps validating, + * it just validates something the other list no longer says. + * + * - `ListChartConfigSchema.chartType` — a five-member `.extract()` of + * `ChartTypeSchema`, not a second enum that happened to agree with it. + * - `WidgetActionTypeSchema` — `ActionType` itself. It used to omit `form`, + * and so rejected at validation what the `ActionRunner` a widget action + * dispatches through already executes. + */ +import { describe, it, expect } from 'vitest'; +import { ChartTypeSchema } from './chart.zod'; +import { ListChartConfigSchema } from './view.zod'; +import { ActionType } from './action.zod'; +import { WidgetActionTypeSchema } from './dashboard.zod'; + +/** `z.enum(...).options` through the lazySchema proxy. */ +const optionsOf = (schema: unknown): string[] => + (schema as { options: string[] }).options; + +describe('ListChartConfigSchema.chartType is extracted from ChartTypeSchema', () => { + const listChartType = (ListChartConfigSchema as unknown as { + shape: { chartType: { def: { innerType: unknown } } }; + }).shape.chartType.def.innerType; + + it('admits exactly the five list-chart visualisations', () => { + expect(optionsOf(listChartType).slice().sort()) + .toEqual(['area', 'bar', 'line', 'pie', 'scatter']); + }); + + it('admits only members of the chart taxonomy', () => { + const taxonomy = new Set(optionsOf(ChartTypeSchema)); + const strays = optionsOf(listChartType).filter(t => !taxonomy.has(t)); + expect(strays).toEqual([]); + }); + + /** Minimal valid config — `dataset` and `values` are both required. */ + const minimal = { dataset: 'sales_by_month', values: ['revenue'] }; + + it('still rejects a taxonomy member outside the list-chart subset', () => { + expect(() => ListChartConfigSchema.parse({ ...minimal, chartType: 'donut' })).toThrow(); + expect(() => ListChartConfigSchema.parse({ ...minimal, chartType: 'combo' })).toThrow(); + }); + + it('accepts every member of the subset, and defaults to bar', () => { + for (const chartType of optionsOf(listChartType)) { + expect(ListChartConfigSchema.parse({ ...minimal, chartType }).chartType).toBe(chartType); + } + expect(ListChartConfigSchema.parse(minimal).chartType).toBe('bar'); + }); +}); + +describe('WidgetActionTypeSchema is ActionType', () => { + it('admits every action type, with no subset of its own', () => { + expect(optionsOf(WidgetActionTypeSchema).slice().sort()) + .toEqual(optionsOf(ActionType).slice().sort()); + }); + + it('admits form — the member the hand-kept subset had dropped', () => { + expect(() => WidgetActionTypeSchema.parse('form')).not.toThrow(); + }); + + it('still rejects an unknown action type', () => { + expect(() => WidgetActionTypeSchema.parse('teleport')).toThrow(); + }); +}); From a7442834cfe9fc59a25ca7dcb40e0159d15c194c Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:22:56 +0800 Subject: [PATCH 2/5] fix(showcase,docs): the gallery demonstrates `combo`, and the reference docs regenerate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gates fired on the taxonomy addition, both correctly. - app-showcase's coverage test requires a gallery widget for every distinctly-renderable ChartType, so admitting `combo` without demonstrating it failed. The Chart Gallery now shows one: task count as bars on the left axis, avg progress as a line on the right — the configuration series[].type / series[].yAxis exist for. Rows below it shift down by 4 to keep the 12-column layout non-overlapping. - content/docs/references/ui/{chart,dashboard,report}.mdx regenerated for the new chart-type member and the two derived enums. check:docs, check:api-surface and check:authorable-surface pass; the showcase coverage suite is 13/13. Co-Authored-By: Claude Opus 5 --- .../ui-vocabularies-derive-not-restate.md | 7 +++ content/docs/references/ui/chart.mdx | 7 +-- content/docs/references/ui/dashboard.mdx | 15 ++++--- content/docs/references/ui/report.mdx | 8 ++-- .../ui/dashboards/chart-gallery.dashboard.ts | 44 +++++++++++++++---- 5 files changed, 58 insertions(+), 23 deletions(-) diff --git a/.changeset/ui-vocabularies-derive-not-restate.md b/.changeset/ui-vocabularies-derive-not-restate.md index 6f6d899bb0..8e0a6e1bc8 100644 --- a/.changeset/ui-vocabularies-derive-not-restate.md +++ b/.changeset/ui-vocabularies-derive-not-restate.md @@ -47,6 +47,13 @@ derivations still hold (a restated list fails silently — it keeps validating, just not what the other list says), and the lint suite now walks every multi-series family in the taxonomy rather than a list of its own. +A third ratchet already existed and did its job: `app-showcase`'s coverage test +requires a gallery widget for every distinctly-renderable `ChartType`, and it +failed the moment `combo` was admitted. The Chart Gallery dashboard now +demonstrates it — a task count as bars on the left axis, an average as a line on +the right, which is the configuration `series[].type` / `series[].yAxis` exist +for. + `ActionType` deliberately does **not** gain `navigation`, which the audit suggested. `ActionRunner.executeNavigation` is a strictly weaker `executeUrl` — no `${param.X}` interpolation, no `apiBase` promotion, no diff --git a/content/docs/references/ui/chart.mdx b/content/docs/references/ui/chart.mdx index 448ed8bc54..c7da964bdc 100644 --- a/content/docs/references/ui/chart.mdx +++ b/content/docs/references/ui/chart.mdx @@ -97,13 +97,13 @@ Inline aggregation for an object-bound chart | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'>` | ✅ | | +| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'>` | ✅ | | | **title** | `string` | optional | Chart title | | **subtitle** | `string` | optional | Chart subtitle | | **description** | `string` | optional | Accessibility description — announced to screen readers as the chart’s label | | **xAxis** | `{ field: string; title?: string; format?: string; min?: number; … }` | optional | X-Axis configuration | | **yAxis** | `{ field: string; title?: string; format?: string; min?: number; … }[]` | optional | Y-Axis configuration (support dual axis) | -| **series** | `{ name: string; label?: string; type?: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'>; color?: string; … }[]` | optional | Defined series configuration | +| **series** | `{ name: string; label?: string; type?: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'>; color?: string; … }[]` | optional | Defined series configuration | | **colors** | `string[] \| Record` | optional | Color palette (string[]) or value→color map (`{ value: color }`) | | **height** | `number` | optional | Fixed plot height in pixels (overrides the container default) | | **showLegend** | `boolean` | ✅ | Display legend | @@ -164,7 +164,7 @@ Type: `string` | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | Field name or series identifier | | **label** | `string` | optional | Series display label | -| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'>` | optional | Override chart type for this series | +| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'>` | optional | Override chart type for this series | | **color** | `string` | optional | Series color (hex/rgb/token) | | **stack** | `string` | optional | Stack identifier to group series | | **yAxis** | `Enum<'left' \| 'right'>` | ✅ | Bind to specific Y-Axis | @@ -190,6 +190,7 @@ Type: `string` * `scatter` * `treemap` * `sankey` +* `combo` * `gauge` * `solid-gauge` * `metric` diff --git a/content/docs/references/ui/dashboard.mdx b/content/docs/references/ui/dashboard.mdx index f20d82b470..bcc735f92d 100644 --- a/content/docs/references/ui/dashboard.mdx +++ b/content/docs/references/ui/dashboard.mdx @@ -32,8 +32,8 @@ const result = Dashboard.parse(data); | **name** | `string` | ✅ | Dashboard unique name | | **label** | `string` | ✅ | Dashboard label | | **description** | `string` | optional | Dashboard description | -| **header** | `{ showTitle: boolean; showDescription: boolean; actions?: { label: string; actionUrl: string; actionType?: Enum<'script' \| 'url' \| 'modal' \| 'flow' \| 'api'>; icon?: string }[] }` | optional | Dashboard header configuration | -| **widgets** | `{ id: string; title?: string; description?: string; type: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'>; … }[]` | ✅ | Widgets to display | +| **header** | `{ showTitle: boolean; showDescription: boolean; actions?: { label: string; actionUrl: string; actionType?: Enum<'script' \| 'url' \| 'modal' \| 'flow' \| 'api' \| 'form'>; icon?: string }[] }` | optional | Dashboard header configuration | +| **widgets** | `{ id: string; title?: string; description?: string; type: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'>; … }[]` | ✅ | Widgets to display | | **columns** | `integer` | optional | Number of grid columns (default 12) | | **gap** | `integer` | optional | Grid gap in Tailwind spacing units | | **refreshInterval** | `number` | optional | Auto-refresh interval in seconds | @@ -63,7 +63,7 @@ Dashboard header configuration | :--- | :--- | :--- | :--- | | **showTitle** | `boolean` | ✅ | Show dashboard title in header | | **showDescription** | `boolean` | ✅ | Show dashboard description in header | -| **actions** | `{ label: string; actionUrl: string; actionType?: Enum<'script' \| 'url' \| 'modal' \| 'flow' \| 'api'>; icon?: string }[]` | optional | Header action buttons | +| **actions** | `{ label: string; actionUrl: string; actionType?: Enum<'script' \| 'url' \| 'modal' \| 'flow' \| 'api' \| 'form'>; icon?: string }[]` | optional | Header action buttons | --- @@ -78,7 +78,7 @@ Dashboard header action | :--- | :--- | :--- | :--- | | **label** | `string` | ✅ | Action button label | | **actionUrl** | `string` | ✅ | URL or target for the action | -| **actionType** | `Enum<'script' \| 'url' \| 'modal' \| 'flow' \| 'api'>` | optional | Type of action | +| **actionType** | `Enum<'script' \| 'url' \| 'modal' \| 'flow' \| 'api' \| 'form'>` | optional | Type of action | | **icon** | `string` | optional | Icon identifier for the action button | @@ -93,13 +93,13 @@ Dashboard header action | **id** | `string` | ✅ | Unique widget identifier (snake_case) | | **title** | `string` | optional | Widget title | | **description** | `string` | optional | Widget description text below the header | -| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'>` | ✅ | Visualization type | -| **chartConfig** | `{ type: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'>; title?: string; subtitle?: string; description?: string; … }` | optional | Chart visualization configuration | +| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'>` | ✅ | Visualization type | +| **chartConfig** | `{ type: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'>; title?: string; subtitle?: string; description?: string; … }` | optional | Chart visualization configuration | | **colorVariant** | `Enum<'default' \| 'blue' \| 'teal' \| 'orange' \| 'purple' \| 'success' \| 'warning' \| 'danger'>` | optional | Widget color variant for theming | | **requiresObject** | `string` | optional | Hide the widget unless the named object is registered | | **requiresService** | `string` | optional | Hide the widget unless the named kernel service is registered | | **actionUrl** | `string` | optional | URL or target for the widget action button | -| **actionType** | `Enum<'script' \| 'url' \| 'modal' \| 'flow' \| 'api'>` | optional | Type of action for the widget action button | +| **actionType** | `Enum<'script' \| 'url' \| 'modal' \| 'flow' \| 'api' \| 'form'>` | optional | Type of action for the widget action button | | **actionIcon** | `string` | optional | Icon identifier for the widget action button | | **filter** | `any` | optional | Presentation-scope filter (runtimeFilter) | | **compareTo** | `'previousPeriod' \| 'previousYear' \| { offset: string }` | optional | Period-over-period comparison window | @@ -179,6 +179,7 @@ Widget action type * `modal` * `flow` * `api` +* `form` --- diff --git a/content/docs/references/ui/report.mdx b/content/docs/references/ui/report.mdx index 7a699bf409..3aac434f5e 100644 --- a/content/docs/references/ui/report.mdx +++ b/content/docs/references/ui/report.mdx @@ -33,7 +33,7 @@ const result = JoinedReportBlock.parse(data); | **label** | `string` | optional | Display label (plain string; i18n keys are auto-generated by the framework) | | **description** | `string` | optional | Display label (plain string; i18n keys are auto-generated by the framework) | | **type** | `Enum<'tabular' \| 'summary' \| 'matrix'>` | ✅ | | -| **chart** | `{ type: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'>; title?: string; subtitle?: string; description?: string; … }` | optional | | +| **chart** | `{ type: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'>; title?: string; subtitle?: string; description?: string; … }` | optional | | | **dataset** | `string` | optional | Dataset name to bind (ADR-0021) | | **rows** | `string[]` | optional | Dimension names down (dataset-bound) | | **columns** | `string[]` | optional | Dimension names across (matrix, dataset-bound) | @@ -61,7 +61,7 @@ const result = JoinedReportBlock.parse(data); | **runtimeFilter** | `any` | optional | Render-time scope filter | | **order** | `{ by: string; direction: Enum<'asc' \| 'desc'> }[]` | optional | Result ordering, most significant key first | | **drilldown** | `boolean` | ✅ | Click-through to underlying records | -| **chart** | `{ type: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'>; title?: string; subtitle?: string; description?: string; … }` | optional | Embedded chart configuration | +| **chart** | `{ type: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'>; title?: string; subtitle?: string; description?: string; … }` | optional | Embedded chart configuration | | **blocks** | `{ name: string; label?: string; description?: string; type: Enum<'tabular' \| 'summary' \| 'matrix'>; … }[]` | optional | Sub-reports for type=joined | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this report. | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | @@ -81,13 +81,13 @@ const result = JoinedReportBlock.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'>` | ✅ | | +| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'>` | ✅ | | | **title** | `string` | optional | Chart title | | **subtitle** | `string` | optional | Chart subtitle | | **description** | `string` | optional | Accessibility description — announced to screen readers as the chart’s label | | **xAxis** | `string` | ✅ | Dataset dimension name for the X-axis (bound-dataset dimension, not a raw field) | | **yAxis** | `string` | ✅ | Dataset measure name for the Y-axis (bound-dataset measure, not a raw field) | -| **series** | `{ name: string; label?: string; type?: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'>; color?: string; … }[]` | optional | Defined series configuration | +| **series** | `{ name: string; label?: string; type?: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'>; color?: string; … }[]` | optional | Defined series configuration | | **colors** | `string[] \| Record` | optional | Color palette (string[]) or value→color map (`{ value: color }`) | | **height** | `number` | optional | Fixed plot height in pixels (overrides the container default) | | **showLegend** | `boolean` | ✅ | Display legend | diff --git a/examples/app-showcase/src/ui/dashboards/chart-gallery.dashboard.ts b/examples/app-showcase/src/ui/dashboards/chart-gallery.dashboard.ts index c6a0cef3b8..4ccef2d326 100644 --- a/examples/app-showcase/src/ui/dashboards/chart-gallery.dashboard.ts +++ b/examples/app-showcase/src/ui/dashboards/chart-gallery.dashboard.ts @@ -52,21 +52,47 @@ export const ChartGalleryDashboard: Dashboard = { { id: 'line_created', type: 'line', title: 'Tasks Created (monthly)', dataset: taskDs, dimensions: ['created_at'], values: ['task_count'], chartConfig: cfg('line', 'created_at', 'task_count'), layout: { x: 0, y: 6, w: 6, h: 4 } }, { id: 'area_created', type: 'area', title: 'Tasks Created (area)', dataset: taskDs, dimensions: ['created_at'], values: ['task_count'], chartConfig: cfg('area', 'created_at', 'task_count'), layout: { x: 6, y: 6, w: 6, h: 4 } }, + // ── Mixed — the family `series[].type` and `series[].yAxis` exist for: + // a count as bars on the left axis, a rate as a line on the right. ── + { + id: 'combo_count_vs_progress', + type: 'combo', + title: 'Task Count vs Avg Progress', + dataset: taskDs, + dimensions: ['created_at'], + values: ['task_count', 'avg_progress'], + chartConfig: { + type: 'combo', + xAxis: { field: 'created_at', showGridLines: true, logarithmic: false }, + yAxis: [ + { field: 'task_count', showGridLines: true, logarithmic: false }, + { field: 'avg_progress', showGridLines: false, logarithmic: false }, + ], + series: [ + { name: 'task_count', label: 'Tasks', type: 'bar', yAxis: 'left' }, + { name: 'avg_progress', label: 'Avg Progress', type: 'line', yAxis: 'right' }, + ], + showLegend: true, + showDataLabels: false, + }, + layout: { x: 0, y: 10, w: 12, h: 4 }, + }, + // ── Distribution ───────────────────────────────────────────────────────── - { id: 'pie_status', type: 'pie', title: 'Status Split', dataset: taskDs, dimensions: ['status'], values: ['task_count'], chartConfig: cfg('pie', 'status', 'task_count'), layout: { x: 0, y: 10, w: 4, h: 4 } }, - { id: 'donut_priority', type: 'donut', title: 'Priority Split', dataset: taskDs, dimensions: ['priority'], values: ['task_count'], chartConfig: cfg('donut', 'priority', 'task_count'), layout: { x: 4, y: 10, w: 4, h: 4 } }, - { id: 'funnel_status', type: 'funnel', title: 'Status Funnel', dataset: taskDs, dimensions: ['status'], values: ['task_count'], chartConfig: cfg('funnel', 'status', 'task_count'), layout: { x: 8, y: 10, w: 4, h: 4 } }, + { id: 'pie_status', type: 'pie', title: 'Status Split', dataset: taskDs, dimensions: ['status'], values: ['task_count'], chartConfig: cfg('pie', 'status', 'task_count'), layout: { x: 0, y: 14, w: 4, h: 4 } }, + { id: 'donut_priority', type: 'donut', title: 'Priority Split', dataset: taskDs, dimensions: ['priority'], values: ['task_count'], chartConfig: cfg('donut', 'priority', 'task_count'), layout: { x: 4, y: 14, w: 4, h: 4 } }, + { id: 'funnel_status', type: 'funnel', title: 'Status Funnel', dataset: taskDs, dimensions: ['status'], values: ['task_count'], chartConfig: cfg('funnel', 'status', 'task_count'), layout: { x: 8, y: 14, w: 4, h: 4 } }, // ── Relationship + Advanced ────────────────────────────────────────────── - { id: 'scatter_estimate', type: 'scatter', title: 'Estimate vs Progress', dataset: taskDs, dimensions: ['progress'], values: ['avg_estimate'], chartConfig: cfg('scatter', 'progress', 'avg_estimate'), layout: { x: 0, y: 14, w: 6, h: 4 } }, - { id: 'radar_priority', type: 'radar', title: 'Priority Radar', dataset: taskDs, dimensions: ['priority'], values: ['task_count'], chartConfig: cfg('radar', 'priority', 'task_count'), layout: { x: 6, y: 14, w: 6, h: 4 } }, + { id: 'scatter_estimate', type: 'scatter', title: 'Estimate vs Progress', dataset: taskDs, dimensions: ['progress'], values: ['avg_estimate'], chartConfig: cfg('scatter', 'progress', 'avg_estimate'), layout: { x: 0, y: 18, w: 6, h: 4 } }, + { id: 'radar_priority', type: 'radar', title: 'Priority Radar', dataset: taskDs, dimensions: ['priority'], values: ['task_count'], chartConfig: cfg('radar', 'priority', 'task_count'), layout: { x: 6, y: 18, w: 6, h: 4 } }, // ── Composition ────────────────────────────────────────────────────────── - { id: 'treemap_hours', type: 'treemap', title: 'Hours Treemap', dataset: taskDs, dimensions: ['status'], values: ['est_hours'], chartConfig: cfg('treemap', 'status', 'est_hours'), layout: { x: 0, y: 18, w: 6, h: 4 } }, - { id: 'sankey_flow', type: 'sankey', title: 'Status Flow (Sankey)', dataset: taskDs, dimensions: ['status'], values: ['task_count'], chartConfig: cfg('sankey', 'status', 'task_count'), layout: { x: 6, y: 18, w: 6, h: 4 } }, + { id: 'treemap_hours', type: 'treemap', title: 'Hours Treemap', dataset: taskDs, dimensions: ['status'], values: ['est_hours'], chartConfig: cfg('treemap', 'status', 'est_hours'), layout: { x: 0, y: 22, w: 6, h: 4 } }, + { id: 'sankey_flow', type: 'sankey', title: 'Status Flow (Sankey)', dataset: taskDs, dimensions: ['status'], values: ['task_count'], chartConfig: cfg('sankey', 'status', 'task_count'), layout: { x: 6, y: 22, w: 6, h: 4 } }, // ── Tabular (real grouped tables, multiple measures) ───────────────────── - { id: 'table_projects', type: 'table', title: 'Projects by Account', dataset: projectDs, dimensions: ['account'], values: ['project_count', 'budget_sum', 'spent_sum'], layout: { x: 0, y: 22, w: 6, h: 4 } }, - { id: 'pivot_tasks', type: 'pivot', title: 'Tasks by Status × Priority', dataset: taskDs, dimensions: ['status', 'priority'], values: ['task_count'], layout: { x: 6, y: 22, w: 6, h: 4 } }, + { id: 'table_projects', type: 'table', title: 'Projects by Account', dataset: projectDs, dimensions: ['account'], values: ['project_count', 'budget_sum', 'spent_sum'], layout: { x: 0, y: 26, w: 6, h: 4 } }, + { id: 'pivot_tasks', type: 'pivot', title: 'Tasks by Status × Priority', dataset: taskDs, dimensions: ['status', 'priority'], values: ['task_count'], layout: { x: 6, y: 26, w: 6, h: 4 } }, ], }; From 395f7671a170e8f6a68f480b817dc708715b4840 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:24:20 +0800 Subject: [PATCH 3/5] fix(skills): regenerate the objectstack-data reference index `view.zod.ts` now imports `chart.zod.ts` (for the extracted list-chart subset), so the taxonomy joins that skill's transitive reference set. Co-Authored-By: Claude Opus 5 --- skills/objectstack-data/references/_index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/skills/objectstack-data/references/_index.md b/skills/objectstack-data/references/_index.md index 037b4abc63..4594076b10 100644 --- a/skills/objectstack-data/references/_index.md +++ b/skills/objectstack-data/references/_index.md @@ -29,6 +29,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/shared/protection.zod.ts` — Package-level metadata protection (ADR-0010 §3.7 — Phase 4.3) - `node_modules/@objectstack/spec/src/shared/suggestions.zod.ts` — "Did you mean?" Suggestion Utilities - `node_modules/@objectstack/spec/src/ui/action.zod.ts` — Action Parameter Schema +- `node_modules/@objectstack/spec/src/ui/chart.zod.ts` — Unified Chart Type Taxonomy - `node_modules/@objectstack/spec/src/ui/i18n.zod.ts` — I18n Object Schema - `node_modules/@objectstack/spec/src/ui/sharing.zod.ts` — Sharing & Embedding Protocol - `node_modules/@objectstack/spec/src/ui/view.zod.ts` — HTTP Method Enum & HTTP Request Schema From f93da17186d7a08db29465e0305824b8310957fe Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:26:00 +0800 Subject: [PATCH 4/5] fix(skills): regenerate the react-blocks contract for the `combo` chart type Fifth generated artefact tracking the spec surface: the react-blocks contract and its reference table inline the ChartType union. Co-Authored-By: Claude Opus 5 --- skills/objectstack-ui/contracts/react-blocks.contract.json | 2 +- skills/objectstack-ui/references/react-blocks.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/skills/objectstack-ui/contracts/react-blocks.contract.json b/skills/objectstack-ui/contracts/react-blocks.contract.json index 18ec1ff549..f796ee2961 100644 --- a/skills/objectstack-ui/contracts/react-blocks.contract.json +++ b/skills/objectstack-ui/contracts/react-blocks.contract.json @@ -322,7 +322,7 @@ }, { "name": "type", - "type": "'bar' | 'horizontal-bar' | 'column' | 'line' | 'area' | 'pie' | 'donut' | 'funnel' | 'scatter' | 'treemap' | 'sankey' | 'gauge' | 'solid-gauge' | 'metric' | 'kpi' | 'bullet' | 'radar' | 'table' | 'pivot'", + "type": "'bar' | 'horizontal-bar' | 'column' | 'line' | 'area' | 'pie' | 'donut' | 'funnel' | 'scatter' | 'treemap' | 'sankey' | 'combo' | 'gauge' | 'solid-gauge' | 'metric' | 'kpi' | 'bullet' | 'radar' | 'table' | 'pivot'", "kind": "data", "required": true, "description": "" diff --git a/skills/objectstack-ui/references/react-blocks.md b/skills/objectstack-ui/references/react-blocks.md index f4b7a9ab7f..8237d595a8 100644 --- a/skills/objectstack-ui/references/react-blocks.md +++ b/skills/objectstack-ui/references/react-blocks.md @@ -73,7 +73,7 @@ Chart over an object’s aggregated data. Bind objectName + aggregate; the axes | `aggregate` | `{ field?: string; function: 'count' \| 'sum' \| 'avg' \| 'min' \| 'max'; groupBy: string \| { field: string; dateGranularity?: 'day' \| 'week' \| 'month' \| 'quarter' \| 'year' } }` | binding | | Aggregation run against objectName. Result rows are keyed by the RAW FIELD NAMES: one column named after groupBy (the category) and one named after field (the value; the literal "count" for a fieldless count). Bind xAxis.field / yAxis[].field / series[].name to those names. | | `data` | `any[]` | binding | | Static/precomputed data to chart directly instead of binding via objectName + aggregate. | | `filter` | `FilterArray` | controlled | | ObjectQL filter scoping the data; drive from React state. | -| `type` | `'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'` | data | ✓ | | +| `type` | `'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'` | data | ✓ | | | `title` | `string` | data | | Chart title | | `subtitle` | `string` | data | | Chart subtitle | | `description` | `string` | data | | Accessibility description — announced to screen readers as the chart’s label | From 598db21eedbaf4e96446c2256b1fe9fcd4f475b4 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:51:26 +0800 Subject: [PATCH 5/5] fix(showcase): translate the new gallery widget title at birth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check-i18n-coverage freezes each example's untranslated-declared-string count and fails on growth; the `combo` widget's title took app-showcase from 456 to 457. Translated in both locales the example claims to support, which is the remedy the bundle's own `globalActions` comment prescribes. The gallery's other 20 widget titles predate that ratchet and stay part of the frozen baseline — this does not ratchet the number up. Co-Authored-By: Claude Opus 5 --- .../src/system/translations/index.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/examples/app-showcase/src/system/translations/index.ts b/examples/app-showcase/src/system/translations/index.ts index 08fc9badee..445acb082b 100644 --- a/examples/app-showcase/src/system/translations/index.ts +++ b/examples/app-showcase/src/system/translations/index.ts @@ -149,6 +149,18 @@ export const ShowcaseTranslationBundle = { successMessage: 'Portfolio snapshot taken.', }, }, + // Translated at birth for the same reason as `globalActions` above: this + // example is ratcheted at its current untranslated count, so a declared + // label that skips zh-CN widens the debt and fails check-i18n-coverage. + // The gallery's other widget titles predate that ratchet and remain part of + // the frozen baseline. objectui#2945. + dashboards: { + showcase_chart_gallery: { + widgets: { + combo_count_vs_progress: { title: 'Task Count vs Avg Progress' }, + }, + }, + }, }, 'zh-CN': { objects: { @@ -308,5 +320,12 @@ export const ShowcaseTranslationBundle = { successMessage: '已生成业务概览。', }, }, + dashboards: { + showcase_chart_gallery: { + widgets: { + combo_count_vs_progress: { title: '任务数与平均进度' }, + }, + }, + }, }, };