From d45ffecf01b80e6b96d88dd7341e52b6e794b074 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 05:18:01 +0000 Subject: [PATCH 1/2] docs(spec,skills): object-metric polarity reads the aggregate before invert; pages.md points at compareTo Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01L9qibyvwrLkwf1kTpdV1Qc --- packages/spec/src/ui/component.zod.ts | 7 +++++-- skills/objectstack-ui/rules/pages.md | 8 ++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts index f4deacf10fa..e4f7ee367d3 100644 --- a/packages/spec/src/ui/component.zod.ts +++ b/packages/spec/src/ui/component.zod.ts @@ -5793,12 +5793,15 @@ export const ObjectMetricPropsSchema = lazySchema(() => strictObject({ * - `neutral`: neither; the badge keeps its arrow and percentage, muted. * * The arrow always shows which way the NUMBER moved; polarity decides only - * the colour. Nothing infers a polarity from the aggregated field's name. + * the colour. Both read the aggregated value BEFORE `invert`, so polarity is + * declared against the raw aggregate: an error rate shown as uptime is + * `lower`. Nothing infers a polarity from the aggregated field's name. */ polarity: DatasetMeasurePolarity.optional().describe( 'Which direction of change is good news: higher (rise green, fall red), lower (fall green, rise red) ' + 'or neutral (no good/bad colour). Colours the trend badge, computed (`compareTo`) or static (`trend`); ' - + 'undeclared keeps rise green, fall red', + + 'undeclared keeps rise green, fall red. The badge and polarity read the aggregate before `invert`, so ' + + 'declare polarity against the raw value: an error rate shown as uptime is lower', ), })); /** Author state (ADR-0122: the bare name is the author state). */ diff --git a/skills/objectstack-ui/rules/pages.md b/skills/objectstack-ui/rules/pages.md index e5f2f9a7469..a6fee474f22 100644 --- a/skills/objectstack-ui/rules/pages.md +++ b/skills/objectstack-ui/rules/pages.md @@ -53,7 +53,7 @@ A `slotted` record page has no `tabs` slot: shape the strip with `relatedList` / | `record:path` | Stage progress bar — `statusField` + `stages: [{ value, label }]` | | `record:related_list` | Related-list (child records via lookup) | | `nav:menu` | Quick-create / nav menu bound to current context | -| `object-metric` | Single KPI widget (count/sum/avg) | +| `object-metric` | Single KPI widget (count/sum/avg); trend colour = `polarity`, see [compareTo](dashboards.md#period-over-period--compareto) | | `object-chart` | Embedded chart | > **Variable substitution** — `{first_name}`, `{current_user.first_name}`, @@ -228,7 +228,7 @@ function Page() { by the host capability `react-pages` — **OFF by default**: a react page's source is code every viewer runs. A deployment that trusts its authors turns it on server-side with `OS_PAGE_REACT_ENABLED=true`; while off, the page shows a "disabled" notice instead. -`os build` does NOT lint react source (it is real JS, not constrained JSX) — errors surface +`os build` does NOT lint react source — errors surface at render behind an error boundary, so always test a react page in the browser. ### Styling a page (ADR-0065) — `responsiveStyles`, NOT `className` @@ -242,7 +242,7 @@ arbitrary classes (`text-[27px]`, `grid-cols-7`) silently do nothing. Rules: - **`responsiveStyles` and `id` are top-level** envelope fields; **child nodes go in `properties.children`** (the renderer hoists `properties` to schema level). -- Every styled node **needs a stable `id`** (the CSS is scoped to it). +- Every styled node **needs a stable `id`**. - **Values should be design tokens** for consistency: spacing `var(--space-1..12)`, radius `var(--radius)` / `var(--radius-xl)`, shadow `var(--shadow-sm|md|lg)`, colors `var(--surface)` / `var(--surface-sunken)` / `var(--text-strong)` / @@ -266,7 +266,7 @@ Rules: backgroundColor: 'var(--surface)', border: '1px solid hsl(var(--primary))', boxShadow: '0 0 0 3px hsl(var(--primary) / 0.25), var(--shadow-lg)', }, - small: { padding: 'var(--space-4)', gap: 'var(--space-3)' }, // responsive via the model + small: { padding: 'var(--space-4)', gap: 'var(--space-3)' }, }, properties: { children: [ From ebeb778357c244efe0aa3e7d540cfc767875df3b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 05:25:40 +0000 Subject: [PATCH 2/2] docs(spec): regenerate component reference; changeset for the object-metric polarity prose Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01L9qibyvwrLkwf1kTpdV1Qc --- .../27375-object-metric-polarity-before-invert.md | 13 +++++++++++++ content/docs/references/ui/component.mdx | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 .changeset/27375-object-metric-polarity-before-invert.md diff --git a/.changeset/27375-object-metric-polarity-before-invert.md b/.changeset/27375-object-metric-polarity-before-invert.md new file mode 100644 index 00000000000..bc2d0dc0d55 --- /dev/null +++ b/.changeset/27375-object-metric-polarity-before-invert.md @@ -0,0 +1,13 @@ +--- +'@objectstack/spec': patch +'@objectstack/skills': patch +--- + +docs(spec,skills): an `object-metric` tile's `polarity` is declared against the aggregate before `invert`, and `pages.md` points at it + +Clause-②: no +Metadata-protocol: none + +- **Before.** The `polarity` description said the badge arrow shows which way the number moved. Under `invert: true` the tile shows `1 - value`, so there are two numbers, and the text did not say which one the arrow and `polarity` read. The `objectstack-ui` skill's `rules/pages.md` taught the inline `object-metric` without naming `compareTo` or `polarity`. +- **Now.** The `polarity` description (and the reference page generated from it) says the badge and `polarity` read the aggregated value before `invert`, so `polarity` is declared against the raw value: an error rate shown as uptime is `lower`. The `object-metric` row in `rules/pages.md` names `polarity` and links to the `compareTo` section of `rules/dashboards.md`. The pointer is paid for by deleting three restatements in the same file; the file stays at its token count and no ceiling moves. +- **Unchanged.** No schema, accepted value, default or renderer behaviour moves. diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index eb9940acf7d..7c9afeec754 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -1222,7 +1222,7 @@ Sort field and direction pair | **trend** | `{ value: number; label?: string \| Record; direction?: Enum<'up' \| 'down' \| 'neutral'> }` | optional | Static trend badge `{ value, label?, direction? }` — `value` painted as a percentage, `direction` up / down / neutral. A `compareTo`-derived trend replaces it | | **drillDown** | `{ enabled?: boolean; title?: string; target?: Enum<'drawer' \| 'dialog' \| 'navigate'>; columns?: string[]; … }` | optional | Click-through drill config `{ enabled?, title?, target?, columns?, maxRows?, report? }` — opens the records behind the number, scoped by the metric's own `filter` — or draws the report `report` defines; a present block is on unless `enabled: false`. `filter` and `mode` are refused: a metric tile has no click context and no row | | **compareTo** | `{ kind: Enum<'previousPeriod' \| 'previousYear'> }` | optional | Period-over-period comparison `{ kind }` — `previousPeriod` or `previousYear`, shifting the date macros in the tile's own `filter`. `dimension` is refused: this tile never reads a dataset time dimension | -| **polarity** | `Enum<'higher' \| 'lower' \| 'neutral'>` | optional | Which direction of change is good news: higher (rise green, fall red), lower (fall green, rise red) or neutral (no good/bad colour). Colours the trend badge, computed (`compareTo`) or static (`trend`); undeclared keeps rise green, fall red | +| **polarity** | `Enum<'higher' \| 'lower' \| 'neutral'>` | optional | Which direction of change is good news: higher (rise green, fall red), lower (fall green, rise red) or neutral (no good/bad colour). Colours the trend badge, computed (`compareTo`) or static (`trend`); undeclared keeps rise green, fall red. The badge and polarity read the aggregate before `invert`, so declare polarity against the raw value: an error rate shown as uptime is lower | ### Nested Shape: `ObjectMetricProps.aggregate`