Skip to content

Commit 0b38f81

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-21464-s-metric
2 parents 14305ce + 36ad321 commit 0b38f81

11 files changed

Lines changed: 596 additions & 52 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/platform-objects': patch
4+
---
5+
6+
feat(spec)!: `element:text` `variant` refuses `heading` / `subheading` by name — the vocabulary is the nine `ui:text` publishes, and `os migrate meta` rewrites them to `h2` / `h3` (#21015)
7+
8+
**BREAKING** — `heading` and `subheading` leave `ElementTextPropsSchema.variant` (an
9+
`element:text` page component's `properties.variant`). This is the second release of
10+
the ruled two-release convergence on the nine values `ui:text` publishes — `h1`-`h6`,
11+
`body`, `caption`, `overline`. 17.5.0 added the nine and refused nothing; 17.6.0 was
12+
the full release in which both vocabularies parsed; this release refuses the two old
13+
spellings. A heading is a document level, not a text style: `heading` and
14+
`subheading` named a style and left the renderer to pick the level.
15+
16+
### FROM → TO
17+
18+
| removed | what to write instead |
19+
| --- | --- |
20+
| `variant: 'heading'` | `variant: 'h2'` — the heading element `heading` always rendered — or the level the page outline means. |
21+
| `variant: 'subheading'` | `variant: 'h3'` — the heading element `subheading` always rendered — or the level the page outline means. |
22+
23+
**The one-line fix: `heading` → `h2`, `subheading` → `h3`.**
24+
`os migrate meta --from 17` lists the mechanical edits for existing sources.
25+
26+
The rewrite keeps the heading ELEMENT (so the document outline is unchanged) but not
27+
the size: `heading` drew in the `h3` style and `subheading` in a medium-weight small
28+
heading style, and `h2` / `h3` draw their own, larger styles. Where the old look
29+
mattered more than the level, pick the level whose style you want.
30+
31+
Each retired spelling is refused at parse with a prescription naming the level to
32+
write, and in `tsc` (the two members are gone from the input type). Any other unknown
33+
value keeps zod's own message. An `element:text` with no `variant` still parses to
34+
`body`.
35+
36+
### The retirement kit
37+
38+
- **Value-level retirement.** The enum is declared through `enumWithRetiredValues`
39+
(`shared/retired-key.ts`), with the two prescriptions module-private. No authorable
40+
KEY and no def changed, so nothing lands in `RETIRED_KEYS_BY_MAJOR` and the four
41+
surface ratchets (`api-surface`, `authorable-surface`, `json-schema.manifest`,
42+
`api-surface-signatures`) are byte-identical; the generated component reference
43+
page drops the two values.
44+
- **D2 conversion `element-text-variant-heading-levels`** (step 18, retired from the
45+
load path): `heading` → `h2` and `subheading` → `h3` on every `element:text` page
46+
component — regions, named slots and container nesting. Stored `sys_metadata` page
47+
rows replay it at rehydration; one notice per rewritten block.
48+
- **D3 entry `element-text-variant-heading-subheading-retired`** carries the judgement
49+
the conversion cannot make: whether the rewritten level is the one the page means.
50+
- **No further deprecation window**: 17.6.0 was the window the ruling asked for.
51+
52+
### Producers moved in this repository
53+
54+
- `@objectstack/platform-objects`: the four section headings on the `sys_user` record
55+
page's Security tab (`Password & Sign-in`, `Two-Factor Authentication`, `Email
56+
Verification`, `Danger Zone`) move from `subheading` to `h3`. They render the same
57+
h3 element, in the `h3` style.
58+
- `examples/app-showcase`: the `page-variables` detail heading moves to `h3`.
59+
60+
⚠️ **The out-of-repo author population is NOT MEASURED.** `@objectstack/spec` is
61+
published, and tenant-authored pages were not measured. In this repository the five
62+
writers above were the only ones outside `packages/spec`. objectui at `main` authors
63+
neither value; its `element:text` renderer, registry `inputs` enum, html tier and the
64+
published `sdui.manifest.json` still list the two, and drop them once this release is
65+
installable there (the objectui follow-up).
66+
67+
Clause-②: no (narrowing)
68+
69+
<!-- adr-0087: registered element-text-variant-heading-levels, element-text-variant-heading-subheading-retired -->

‎examples/app-showcase/src/ui/pages/page-variables.page.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export const PageVariablesPage = definePage({
8787
visibleWhen: "page.selectedProjectId != ''",
8888
properties: {
8989
content: '✓ Project selected',
90-
variant: 'subheading',
90+
variant: 'h3',
9191
},
9292
},
9393
{

‎packages/platform-objects/src/pages/sys-user.page.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ export const SysUserDetailPage: Page = {
365365
{
366366
type: 'element:text',
367367
properties: {
368-
variant: 'subheading',
368+
variant: 'h3',
369369
content: {
370370
en: 'Password & Sign-in',
371371
'zh-CN': '密码与登录',
@@ -398,7 +398,7 @@ export const SysUserDetailPage: Page = {
398398
{
399399
type: 'element:text',
400400
properties: {
401-
variant: 'subheading',
401+
variant: 'h3',
402402
content: {
403403
en: 'Two-Factor Authentication',
404404
'zh-CN': '两步验证',
@@ -431,7 +431,7 @@ export const SysUserDetailPage: Page = {
431431
{
432432
type: 'element:text',
433433
properties: {
434-
variant: 'subheading',
434+
variant: 'h3',
435435
content: {
436436
en: 'Email Verification',
437437
'zh-CN': '邮箱验证',
@@ -464,7 +464,7 @@ export const SysUserDetailPage: Page = {
464464
{
465465
type: 'element:text',
466466
properties: {
467-
variant: 'subheading',
467+
variant: 'h3',
468468
content: {
469469
en: 'Danger Zone',
470470
'zh-CN': '危险操作',

‎packages/spec/src/conversions/registry.ts‎

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7718,6 +7718,147 @@ const translationComponentSubmitLabelRemoved: MetadataConversion = {
77187718
},
77197719
};
77207720

7721+
/**
7722+
* `element:text` `variant`'s two pre-convergence spellings become the heading
7723+
* levels they always rendered — `heading` → `h2`, `subheading` → `h3`
7724+
* (protocol 18, #21015: release 2 of objectui#7450's ruling B, maintainer
7725+
* 「其他同意」 2026-09-07, split across two releases 2026-09-09).
7726+
*
7727+
* Release 1 (#17108, 17.5.0) widened the enum to the nine values `ui:text`
7728+
* publishes — `h1`-`h6`, `body`, `caption`, `overline` — and refused nothing;
7729+
* 17.6.0 was the one full release in which both vocabularies parsed. This
7730+
* release refuses the two old spellings by name (`enumWithRetiredValues`,
7731+
* ui/component.zod.ts), and this entry carries the ruled migration hint as the
7732+
* mechanical edit.
7733+
*
7734+
* **The rewrite keeps the heading element, not the look.** Measured at the
7735+
* `.objectui-sha` pin `89cad75d55` (`renderers/basic/elements.tsx`), the
7736+
* `element:text` renderer drew `heading` as an h2 element and `subheading` as an
7737+
* h3 element, so the document outline a screen reader walks is unchanged. The
7738+
* size is not: `heading` drew `h3`'s style and `subheading` a medium-weight
7739+
* `text-lg`, while `h2` and `h3` draw their own, larger ones. The level IS the
7740+
* ruled meaning ("or pick the level you mean"), so the edit follows the
7741+
* element; the D3 entry `element-text-variant-heading-subheading-retired`
7742+
* carries the judgement the chain cannot make — whether this page wanted that
7743+
* level, or another.
7744+
*
7745+
* **One reach: every page component of type `element:text`**, through
7746+
* {@link mapPageComponents} — regions, named slots and container nesting, the
7747+
* positions the component-props gate judges. `variant` on any other component
7748+
* type is that component's own vocabulary and is never read here.
7749+
*
7750+
* `retiredFromLoadPath`: an author is refused at parse with the prescription
7751+
* rather than silently rewritten; stored rows replay it at rehydration
7752+
* (`applyConversionsToStoredItem`) and `os migrate meta` lists the edit for
7753+
* existing sources. Idempotent by construction: the rewrite's output is
7754+
* outside its own input set.
7755+
*/
7756+
const elementTextVariantHeadingLevels: MetadataConversion = {
7757+
id: 'element-text-variant-heading-levels',
7758+
toMajor: 18,
7759+
retiredFromLoadPath: true,
7760+
retiredAfter: '17.6.0',
7761+
surface: 'page.component.element:text.variant',
7762+
summary:
7763+
"element:text 'variant' spellings 'heading' → 'h2' and 'subheading' → 'h3' (the vocabulary converged "
7764+
+ "on the nine values ui:text publishes; each old spelling already rendered that heading element, "
7765+
+ 'so the outline is unchanged and the heading takes that level\'s style)',
7766+
apply(stack, emit) {
7767+
const VARIANT_REWRITE: Readonly<Record<string, string>> = { heading: 'h2', subheading: 'h3' };
7768+
return mapPageComponents(stack, (component, path) => {
7769+
if (component.type !== 'element:text') return component;
7770+
const properties = component.properties;
7771+
if (!isDict(properties)) return component;
7772+
const variant = properties.variant;
7773+
if (typeof variant !== 'string' || !Object.prototype.hasOwnProperty.call(VARIANT_REWRITE, variant)) {
7774+
return component;
7775+
}
7776+
const to = VARIANT_REWRITE[variant]!;
7777+
emit({ from: variant, to, path: `${path}.properties.variant` });
7778+
return { ...component, properties: { ...properties, variant: to } };
7779+
});
7780+
},
7781+
fixture: {
7782+
before: {
7783+
pages: [
7784+
{
7785+
name: 'text_variant_levels',
7786+
regions: [
7787+
{
7788+
name: 'main',
7789+
components: [
7790+
{ type: 'element:text', properties: { content: 'Overview', variant: 'heading', align: 'center' } },
7791+
{ type: 'element:text', properties: { content: 'Details', variant: 'subheading' } },
7792+
// A published level and an absent `variant` ride through.
7793+
{ type: 'element:text', properties: { content: 'Body copy', variant: 'h3' } },
7794+
{ type: 'element:text', properties: { content: 'Default body' } },
7795+
// `variant` on another component type is that type's own
7796+
// vocabulary, untouched here.
7797+
{ type: 'element:button', properties: { label: 'Go', variant: 'heading' } },
7798+
// Nested one container down.
7799+
{
7800+
type: 'page:card',
7801+
properties: {
7802+
title: 'Card',
7803+
children: [{ type: 'element:text', properties: { content: 'In a card', variant: 'subheading' } }],
7804+
},
7805+
},
7806+
],
7807+
},
7808+
],
7809+
},
7810+
// A slotted page's named slot — the same component, the other authoring shape.
7811+
{
7812+
name: 'text_variant_levels_slotted',
7813+
kind: 'slotted',
7814+
regions: [],
7815+
slots: {
7816+
details: [{ type: 'element:text', properties: { content: 'Title', variant: 'heading' } }],
7817+
},
7818+
},
7819+
],
7820+
},
7821+
after: {
7822+
pages: [
7823+
{
7824+
name: 'text_variant_levels',
7825+
regions: [
7826+
{
7827+
name: 'main',
7828+
components: [
7829+
{ type: 'element:text', properties: { content: 'Overview', variant: 'h2', align: 'center' } },
7830+
{ type: 'element:text', properties: { content: 'Details', variant: 'h3' } },
7831+
{ type: 'element:text', properties: { content: 'Body copy', variant: 'h3' } },
7832+
{ type: 'element:text', properties: { content: 'Default body' } },
7833+
{ type: 'element:button', properties: { label: 'Go', variant: 'heading' } },
7834+
{
7835+
type: 'page:card',
7836+
properties: {
7837+
title: 'Card',
7838+
children: [{ type: 'element:text', properties: { content: 'In a card', variant: 'h3' } }],
7839+
},
7840+
},
7841+
],
7842+
},
7843+
],
7844+
},
7845+
{
7846+
name: 'text_variant_levels_slotted',
7847+
kind: 'slotted',
7848+
regions: [],
7849+
slots: {
7850+
details: [{ type: 'element:text', properties: { content: 'Title', variant: 'h2' } }],
7851+
},
7852+
},
7853+
],
7854+
},
7855+
// One per rewritten `variant`: the region pair, the nested card child and
7856+
// the slotted one — the published level, the absent key and the
7857+
// other component type are untouched.
7858+
expectedNotices: 4,
7859+
},
7860+
};
7861+
77217862
/**
77227863
* The inline grid column's one mechanical respelling, shared by both of its
77237864
* carriers — a relationship field's `inlineColumns`
@@ -14439,6 +14580,7 @@ const MAJOR_18_CONVERSIONS: readonly OrderedConversion[] = [
1443914580
{ conversion: elementFilterRemoved, order: 4 },
1444014581
{ conversion: elementFormRemoved, order: 5 },
1444114582
{ conversion: elementInputTargetVariableRemoved, order: 3 },
14583+
{ conversion: elementTextVariantHeadingLevels, order: 59 },
1444214584
{ conversion: fieldColumnListsCanonicalized, order: 6 },
1444314585
{ conversion: fieldMalformedScalePrecisionRemoved, order: 1 },
1444414586
{ conversion: fieldReferenceToAlias, order: 18 },
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { SemanticMigration } from '../../types.js';
4+
5+
// #21015 — release 2 of objectui#7450's ruling B: `element:text` `variant`
6+
// refuses the pre-convergence spellings `heading` and `subheading` by name
7+
// (`enumWithRetiredValues`). The family's one D3 entry; the D2 half is
8+
// `element-text-variant-heading-levels`, which rewrites each to the heading
9+
// element it always rendered. This entry carries the judgement the chain
10+
// cannot make — whether that level is the one the page means, now that it
11+
// draws in that level's style.
12+
export const entry: SemanticMigration = {
13+
id: 'element-text-variant-heading-subheading-retired',
14+
// No backticks and no pipes in `surface` — build-upgrade-guide.ts renders it
15+
// inside a code span AND a table cell.
16+
surface:
17+
'page components of type element:text — properties.variant authored as heading or subheading '
18+
+ '(ElementTextPropsSchema.variant)',
19+
replacement:
20+
"one of the nine values `ui:text` publishes: `h1`-`h6`, `body`, `caption` or `overline`. 'heading' "
21+
+ "→ 'h2' and 'subheading' → 'h3' (the heading element each one always rendered), or the level the "
22+
+ 'page outline means',
23+
reason:
24+
'The ruling converged `element:text` on the vocabulary `ui:text` already publishes, because a '
25+
+ 'heading is a document level, not a text style: `heading` and `subheading` named a style and '
26+
+ 'left the renderer to pick a level. It landed in two releases so authors outside this repository '
27+
+ 'could move first — 17.5.0 added the nine and refused nothing, and 17.6.0 was a full release in '
28+
+ 'which both vocabularies parsed. The D2 conversion `element-text-variant-heading-levels` makes '
29+
+ 'the ruled edit: `heading` → `h2`, `subheading` → `h3`. That keeps the heading element (the '
30+
+ 'renderer drew `heading` as an h2 element and `subheading` as an h3 element), so the document '
31+
+ 'outline a screen reader walks is unchanged, but not the size: `heading` drew in the `h3` style '
32+
+ 'and `subheading` in a medium-weight small heading style, and `h2` / `h3` draw their own, larger '
33+
+ 'styles. Whether the page wanted that level is the author\'s call — a heading placed for its '
34+
+ 'size rather than its place in the outline may want a deeper level. Nothing is dropped at rest: '
35+
+ 'a stored page replays the rewrite at rehydration; a page component\'s `properties` is not '
36+
+ 'parsed on the save path, and the component-props gate reports an old spelling as an advisory '
37+
+ '`component-props-invalid` finding, carrying the prescription, on `os validate`, `os build` and '
38+
+ '`os lint`. ADR-0087',
39+
acceptanceCriteria:
40+
'No `element:text` page component carries `variant` `heading` or `subheading`; `os validate` '
41+
+ 'reports no `component-props-invalid` finding under `properties.variant` for these blocks. For '
42+
+ 'each rewritten block, open the page and check the heading: it renders the same heading element '
43+
+ 'as before, in its level\'s style. Where the old, smaller look mattered more than the level, '
44+
+ 'pick the level whose style you want and confirm the outline still reads in order. A block that '
45+
+ 'omits `variant` still renders as `body`.',
46+
conversionIds: ['element-text-variant-heading-levels'],
47+
};

0 commit comments

Comments
 (0)