') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); docs(aspect-ratio): author the keys the renderer reads in all five demos by os-sales · Pull Request #6786 · objectstack-ai/objectui · GitHub
Skip to content

docs(aspect-ratio): author the keys the renderer reads in all five demos - #6786

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-6773-aspect-ratio-demos
Aug 29, 2026
Merged

docs(aspect-ratio): author the keys the renderer reads in all five demos#6786
os-sales merged 1 commit into
mainfrom
claude/issue-6773-aspect-ratio-demos

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes#6773

Re-derived on my own base (c673282) — 5 of 5, confirmed

packages/components/src/renderers/layout/aspect-ratio.tsx reads exactly ratio,
className, image (with alt), and children || body. It never reads content, and
content is not among its declared inputs. All five entries in
examples/schema-catalog/src/schemas/components-layout-aspect-ratio/ authored ratio +
content and nothing else; a repo-wide sweep for type: "aspect-ratio" found no sixth
authoring site.

Measured before the fix, rendering each entry through the real SchemaRenderer the way the
docs gallery does (harness wrapper + SchemaRenderer, no data source):

entryelementstextimg
16-9-aspect-ratio3""0
square3""0
4-33""0
ultrawide3""0
video-aspect-ratio3""0

Three elements is the harness wrapper plus the two Radix AspectRatio emits — the ratio box
and nothing inside it. The authored content reached the DOM only as the leaked host
attribute content="[object Object]" (the objectui#5574 class; ui:aspect-ratio is
already ledgered for it in packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx,
so this PR files nothing new for it).

What each demo now authors, and why

The renderer was read first and defines the shape; the docs page was not consulted for it,
because the docs page is the thing that was wrong.

entrynow authorswhy that key
16-9-aspect-ratioimage + altThe photo demo. image is the renderer's own declared input and its arm draws an img element classed rounded-md object-cover w-full h-full, i.e. the photo fills the box. The old nested { type: 'image', src, alt } node would render at natural size with no sizing class even once it was reachable.
square, 4-3, ultrawidechildren on the box, children on the cardThe renderer's no-image arm is renderChildren(schema.children ǀǀ schema.body), and renderChildren renders a bare string directly. Both levels had to move: the ui:card renderer reads title, description, header, children ǀǀ body, footer — never content — so moving only the outer key would have traded an empty box for an empty card.
video-aspect-ratiosame, keeping its authored classNameSame two-level rename; bg-muted flex items-center justify-center was already authored.

One presentational addition, named because it is not a pure key rename: the four card demos
now carry flex h-full items-center justify-center. h-full is what makes the card fill
the ratio box rather than sit as a strip at its top, and the centering is the intent
video-aspect-ratio already authored verbatim; the other three now match it.

After the fix, same measurement: the photo entry draws 4 elements with 1 img
(src and alt as authored), and each card entry draws 5 elements with its own label as
the tile's text ("Square (1:1)", "4:3 Ratio", "21:9 Ratio", "Video Player (16:9)").

The Schema block

It published content: ComponentSchema as contract and omitted image / alt, which the
renderer does read and does declare. It now documents the read surface: ratio, image,
alt, children, className, with the two content modes stated in one line of prose.

Two deliberate choices:

  • body is not documented. The renderer reads children ǀǀ body, but a back-compat
    read is not a second authorable spelling — the page:card discipline, verbatim
    (objectstack#5775 / PR fix(app-shell): the Studio grid selects only fields the server has (cloud#1652) #6281). children is also what all nine sibling
    components-layout-semantic fixtures author.
  • SchemaNode, not ComponentSchema.AspectRatioSchema declares
    children?: SchemaNode ǀ SchemaNode[]; ComponentSchema is a real export but it is the
    blocks-component interface, not the child-node union.

The fence stays plaintext: this file is one of the 80 in check-doc-fence-languages.mjs's
shrink-only ledger, at 1 block, and it still holds 1 block.

Gate verdict: would anything have caught this?

Nothing did, and the reasons are measured, not inferred:

  1. examples/schema-catalog/test/catalog-gallery-render.test.tsxdoes render all five
    and passes. Its non-vacuity control is
    drewSomething = elements > WRAPPER_ELEMENTS ǀǀ text.trim().length > 0 with
    WRAPPER_ELEMENTS = 2, and an empty ratio box clears it on the wrapper Radix draws for
    the ratio itself (3 > 2). Its stronger control — the entry's own authored strings on
    screen — is scoped to NEWLY_REGISTERED_CATEGORIES, which this family is not in.
  2. No parse could have caught it. BaseSchema is .passthrough() and carries
    [key: string]: any, so zod and tsc both acceptcontent — the objectui#6157
    class-3 shape exactly. These five are therefore not part of 53 in-repo schema files carry a registered ObjectUI type but fail safeValidateSchema #6318's
    safeValidateSchema population.
  3. check-doc-component-types.mjs rules the question out by name: "NOT in scope,
    deliberately: whether the snippet's OTHER keys are read by the renderer the type
    resolves to."
  4. The manifest tier reports content as unknown-prop, the same warning it gives a typo,
    and nothing fails on it.

A corpus-wide "container fixture must render its authored content" sweep is NOT
source-decidable today
, and this PR does not propose one. The predicate needs "which
authored key is this renderer's content slot", and the registry cannot answer it here:
ui:aspect-ratio declares isContainer: true yet declares no type: 'slot' input at all
(its inputs are ratio, image, alt, className), while page:card does declare
children as a slot. A sweep would have to guess the slot for every renderer in the first
shape, which is the false-red generator that gets gates deleted.

What IS decidable is the per-family pin, so that is what ships:
examples/schema-catalog/test/aspect-ratio-demo-content-6773.test.tsx — the same shape
toast-demo-dispatch-6250.test.tsx took for the same class of fixture correction. It
asserts at category scope, not over a list of five ids, so a sixth demo authoring the
phantom key fails without anyone remembering to add it:

  • every entry in the category authors only keys the renderer reads;
  • every entry puts its authored content in the box (its img src/alt for the image
    arm, its authored slot strings otherwise);
  • a counter-probe renders the exact pre-fix content shape and pins that it draws no
    text and no img — so the two assertions above cannot pass vacuously;
  • a second counter-probe pins that the judge does see content authored under a read key.

Verification

All at 8c093f6b5 (the final commit), heavy commands serialized through the shared verify
lock:

pnpm check:doc-fences OK every TypeScript block in 223 document(s) ... No unknown fence spelling hides one.
pnpm check:doc-types OK Every documented component type is registered.
pnpm check:doc-snippets OK Every covered documentation snippet compiles against the built types.
pnpm docs:check-links OK Links are valid across 17 scan roots.
pnpm check:control-bytes OK scanned 5648 tracked text file(s)
pnpm changeset:check OK + check-changeset-presence: no changeset is owed (1 added anyway)
pnpm type-check:coverage OK 41/41 packages compile their tests
pnpm lint:coverage OK 46/46 packages linted
pnpm exec vitest run examples/schema-catalog/ Test Files 16 passed (16) / Tests 1866 passed (1866)
turbo run lint type-check --filter=@object-ui/example-schema-catalog 32 successful, 32 total

check:doc-snippets first returned exit 2 (PRECONDITION NOT MET — packages not built); the
verdict above is from the re-run after
turbo run build $(node scripts/check-doc-snippet-types.mjs --build-filter).

The repo-wide pnpm lint / full pnpm test are CI's runs, not narrowed away here: the
package-scoped lint and type-check above cover every file this PR touches that either
gate reads, and the docs page is read by the four doc gates, all four run in full.

Scope

Only content/docs/components/layout/aspect-ratio.mdx, the five aspect-ratio fixtures,
the pin test beside them, and a changeset. The renderer is untouched — it is the contract,
and widening it to accept content would add a third spelling beside children and body
(AGENTS.md #0.1 direction: fix the metadata, not the renderer). No release-notes file is
touched. The changeset has an empty frontmatter: a docs page plus
@object-ui/example-schema-catalog fixtures publish nothing.

Generated by Claude Code


Generated by Claude Code

Every shipped `components-layout-aspect-ratio` demo authored `content`, which
`packages/components/src/renderers/layout/aspect-ratio.tsx` never reads: it
reads `ratio`, `className`, `image` (with `alt`) and `children || body`.
Measured on c673282 by rendering each entry through the real SchemaRenderer
the way the docs gallery does — 3 elements, empty text, zero images for all
five: the Radix wrapper for the ratio itself and nothing inside it.
The photo demo now authors `image` + `alt`, the renderer's own declared inputs,
whose img element is sized to fill the box. The four card demos author
`children` at BOTH levels — the nested `card` renderer reads
`children || body`, `title`, `description`, `header` and `footer`, never
`content`, so moving only the outer key would have traded an empty box for an
empty card.
The page's Schema block published `content` as contract while omitting
`image`/`alt`; it now documents the keys the renderer reads.
Adds `examples/schema-catalog/test/aspect-ratio-demo-content-6773.test.tsx`:
category scope rather than a list of five ids, with a counter-probe that
renders the pre-fix shape and proves the assertion can still fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
@os-sales
os-sales marked this pull request as ready for review August 29, 2026 13:58
@os-sales
os-sales added this pull request to the merge queueAug 29, 2026
Merged via the queue into main with commit dfb889fAug 29, 2026
31 checks passed
@os-sales
os-sales deleted the claude/issue-6773-aspect-ratio-demos branch August 29, 2026 14:11
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-sales@claude