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
13 changes: 13 additions & 0 deletions .changeset/27375-object-metric-polarity-before-invert.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion content/docs/references/ui/component.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1222,7 +1222,7 @@ Sort field and direction pair
| **trend** | `{ value: number; label?: string \| Record<string, string>; 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`

Expand Down
7 changes: 5 additions & 2 deletions packages/spec/src/ui/component.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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). */
Expand Down
8 changes: 4 additions & 4 deletions skills/objectstack-ui/rules/pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
Expand Down Expand Up @@ -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`
Expand All @@ -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)` /
Expand All @@ -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: [
Expand Down
Loading