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
17 changes: 17 additions & 0 deletions .changeset/ui-text-variant-align-enforce.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@object-ui/components": patch
---

`ui:text` now honours `TextSchema.variant` and `TextSchema.align`

The nine published variants (`h1`-`h6`, `body`, `caption`, `overline`) each map to
a distinct typographic class, and `h1`-`h6` render the heading element they name;
the four published alignments (`left`, `center`, `right`, `justify`) reach the DOM
as their Tailwind class. The registration's `inputs` list declares both keys, so
the JSX-page prop whitelist stops reporting `unknown-prop` for them.

The published enum is unchanged — the renderer catches up to it, and a spelling
outside the enum is still refused by `safeValidateSchema` rather than quietly
ignored. Behaviour note: a document that already authored `variant: 'h1'` and saw
no heading will now see one. That is the declared behaviour arriving. A node that
never authored `variant` renders exactly as before.
14 changes: 9 additions & 5 deletions content/docs/components/basic/text.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import { DemoGrid } from '@/app/components/ComponentDemo';

The Text component displays text content with predefined typography variants.

`variant` is the published vocabulary of `TextSchema` and the renderer maps every
value in it: `h1`-`h6` render the heading element they name, `body`, `caption` and
`overline` render an inline span. Any other spelling is refused by the validator
rather than ignored.

## Basic Usage

<SchemaExample id="components-basic-text/simple-text" />
Expand All @@ -17,11 +22,10 @@ The Text component displays text content with predefined typography variants.
<SchemaExample id="components-basic-text/heading-1" />
<SchemaExample id="components-basic-text/heading-2" />
<SchemaExample id="components-basic-text/heading-3" />
<SchemaExample id="components-basic-text/heading-4" />
<SchemaExample id="components-basic-text/heading-5" />
<SchemaExample id="components-basic-text/paragraph" />
<SchemaExample id="components-basic-text/lead" />
<SchemaExample id="components-basic-text/large" />
<SchemaExample id="components-basic-text/small" />
<SchemaExample id="components-basic-text/muted" />
<SchemaExample id="components-basic-text/caption" />
</DemoGrid>

## Schema
Expand All @@ -31,7 +35,7 @@ interface TextSchema {
type: 'text';
content: string; // Text content to display
value?: string; // Alias for content
variant?: 'h1' | 'h2' | 'h3' | 'h4' | 'p' | 'lead' | 'large' | 'small' | 'muted';
variant?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'body' | 'caption' | 'overline';
align?: 'left' | 'center' | 'right' | 'justify';
color?: string; // Tailwind color class
className?: string; // Additional CSS classes
Expand Down
50 changes: 20 additions & 30 deletions examples/schema-catalog/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,14 @@ import components_basic_span_default_badge from './schemas/components-basic-span
import components_basic_span_secondary_badge from './schemas/components-basic-span/secondary-badge.json' with { type: 'json' };
import components_basic_span_status_badges from './schemas/components-basic-span/status-badges.json' with { type: 'json' };
import components_basic_span_text_component from './schemas/components-basic-span/text-component.json' with { type: 'json' };
import components_basic_text_caption from './schemas/components-basic-text/caption.json' with { type: 'json' };
import components_basic_text_heading_1 from './schemas/components-basic-text/heading-1.json' with { type: 'json' };
import components_basic_text_heading_2 from './schemas/components-basic-text/heading-2.json' with { type: 'json' };
import components_basic_text_heading_3 from './schemas/components-basic-text/heading-3.json' with { type: 'json' };
import components_basic_text_large from './schemas/components-basic-text/large.json' with { type: 'json' };
import components_basic_text_lead from './schemas/components-basic-text/lead.json' with { type: 'json' };
import components_basic_text_muted from './schemas/components-basic-text/muted.json' with { type: 'json' };
import components_basic_text_heading_4 from './schemas/components-basic-text/heading-4.json' with { type: 'json' };
import components_basic_text_heading_5 from './schemas/components-basic-text/heading-5.json' with { type: 'json' };
import components_basic_text_paragraph from './schemas/components-basic-text/paragraph.json' with { type: 'json' };
import components_basic_text_simple_text from './schemas/components-basic-text/simple-text.json' with { type: 'json' };
import components_basic_text_small from './schemas/components-basic-text/small.json' with { type: 'json' };
import components_basic_text_text_alignment from './schemas/components-basic-text/text-alignment.json' with { type: 'json' };
import components_basic_text_text_with_colors from './schemas/components-basic-text/text-with-colors.json' with { type: 'json' };
import components_complex_carousel_customer_reviews from './schemas/components-complex-carousel/customer-reviews.json' with { type: 'json' };
Expand Down Expand Up @@ -884,6 +883,15 @@ const REGISTRY: Record<string, Example> = {
},
schema: components_basic_span_text_component,
},
'components-basic-text/caption': {
id: 'components-basic-text/caption',
meta: {
title: "Caption",
description: "",
category: 'components-basic-text',
},
schema: components_basic_text_caption,
},
'components-basic-text/heading-1': {
id: 'components-basic-text/heading-1',
meta: {
Expand Down Expand Up @@ -911,32 +919,23 @@ const REGISTRY: Record<string, Example> = {
},
schema: components_basic_text_heading_3,
},
'components-basic-text/large': {
id: 'components-basic-text/large',
'components-basic-text/heading-4': {
id: 'components-basic-text/heading-4',
meta: {
title: "Large",
title: "Heading 4",
description: "",
category: 'components-basic-text',
},
schema: components_basic_text_large,
schema: components_basic_text_heading_4,
},
'components-basic-text/lead': {
id: 'components-basic-text/lead',
'components-basic-text/heading-5': {
id: 'components-basic-text/heading-5',
meta: {
title: "Lead",
title: "Heading 5",
description: "",
category: 'components-basic-text',
},
schema: components_basic_text_lead,
},
'components-basic-text/muted': {
id: 'components-basic-text/muted',
meta: {
title: "Muted",
description: "",
category: 'components-basic-text',
},
schema: components_basic_text_muted,
schema: components_basic_text_heading_5,
},
'components-basic-text/paragraph': {
id: 'components-basic-text/paragraph',
Expand All @@ -956,15 +955,6 @@ const REGISTRY: Record<string, Example> = {
},
schema: components_basic_text_simple_text,
},
'components-basic-text/small': {
id: 'components-basic-text/small',
meta: {
title: "Small",
description: "",
category: 'components-basic-text',
},
schema: components_basic_text_small,
},
'components-basic-text/text-alignment': {
id: 'components-basic-text/text-alignment',
meta: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "text",
"content": "Caption text",
"variant": "caption"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "text",
"content": "Heading 4",
"variant": "h4"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "text",
"content": "Heading 5",
"variant": "h5"
}

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"type": "text",
"content": "Paragraph text",
"variant": "p"
"variant": "body"
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{
"type": "text",
"content": "Loading...",
"variant": "muted"
"variant": "caption"
}
]
}
9 changes: 6 additions & 3 deletions examples/schema-catalog/test/catalog-gallery-render.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -406,9 +406,12 @@ interface Rendered {

/**
* Did the tile draw anything of its own? Elements OR text, because both are
* real: `components-basic-text/*` render a bare text node with no element
* around it (measured — "Heading 1" is a direct child of the wrapper), so an
* element-count-only control reads nine correct tiles as empty.
* real: a `text` node that authors neither `variant`, `align`, `className` nor a
* designer id renders as a bare text node with no element around it (measured —
* `components-basic-text/simple-text` puts "Hello, World!" directly in the
* wrapper), so an element-count-only control reads correct tiles as empty. Since
* objectui#6942 the variant-carrying entries in that category DO wrap, but the
* unadorned ones — here and in every other category — still do not.
*/
const drewSomething = (elements: number, text: string) =>
elements > WRAPPER_ELEMENTS || text.trim().length > 0;
Expand Down
26 changes: 19 additions & 7 deletions examples/schema-catalog/test/layout-dom-leak-5574.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,14 @@
* 2. The JUDGE is self-checked against a deliberately leaking element, so a
* zero can never come from an attribute reader that reports nothing.
*
* The 176 `text` nodes that render NO element are recorded rather than hidden:
* `text` returns a bare fragment when a node carries neither designer id nor
* className, and 176 catalog nodes take that path. They were never evidence of
* safety, which is the phantom-clean class objectui#5574's first pass found
* seven real leaks behind.
* The 162 `text` nodes that render NO element are recorded rather than hidden:
* `text` returns a bare fragment when a node carries neither designer id,
* className, `variant` nor `align`, and 162 catalog nodes take that path. They
* were never evidence of safety, which is the phantom-clean class
* objectui#5574's first pass found seven real leaks behind. (176 before
* objectui#6942 taught `ui:text` to read `variant` and `align`: a node that
* authors either now needs an element to carry the class, so 13 of them left
* this class for the measured one.)
*
* Module-scope import of `@object-ui/components`, not `beforeAll` (AGENTS.md
* §测试纪律): registering the renderers is an unbounded module load and must not
Expand Down Expand Up @@ -102,8 +105,17 @@ const NODE_CENSUS: Readonly<Record<string, { rendered: number; noElement: number
container: { rendered: 15, noElement: 0 },
grid: { rendered: 26, noElement: 0 },
// 699 -> 702: the two components-layout-box exemplars author 3 text nodes
// (objectui#3965).
text: { rendered: 702, noElement: 176 },
// (objectui#3965). 702 -> 701 and 176 -> 162 with objectui#6942, and the two
// moves have different causes: `components-basic-text/muted.json` was deleted
// (one node fewer), and 13 nodes LEFT the no-element class because `ui:text`
// began reading `variant` and `align` — a node that authors either now needs
// an element to carry the class. Those 13 are the 7 variant-carrying
// `components-basic-text` entries, the 3 `text-alignment` children, the
// `components-feedback-loading/with-text` label, and the `h3` headings in
// `components-layout-container/basic-container` and
// `components-layout-stack/basic-stack`. This is the census moving in the
// SAFE direction: fewer phantom-clean nodes, more elements actually read.
text: { rendered: 701, noElement: 162 },
// The objectui#3965 migration population (80 nodes retyped from `div`) plus
// the 4 nodes of the components-layout-box exemplars. `box` is born on
// `toDomProps` and class-transparency, so it joins the measured set as a
Expand Down
Loading
Loading