Skip to content

fix(examples,docs): correct the toast demos to spellings the engine executes - #6498

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-6250-toast-demo-shapes
Aug 26, 2026
Merged

fix(examples,docs): correct the toast demos to spellings the engine executes#6498
os-support-ai merged 1 commit into
mainfrom
claude/issue-6250-toast-demo-shapes

Conversation

@os-support-ai

@os-support-aios-support-ai commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Fixes#6250

All measurements below are on 076ed3b51, branched from origin/main7975f2d85, with packages/types built fresh.

Before → after, as a reading

The seven components-feedback-toast/* and seven components-feedback-sonner/*SchemaExample fixtures hung an action object off onClick. Parsing each fixture against the mirror its own type selects:

BEFORE red=14 green=5 (5 green are the untouched components-feedback-toaster/* entries)
AFTER red=0 green=19

Every one of the 14 was the same issue, on the ENVELOPE and not on any variant — ButtonSchema.onClick is z.function() (zod/form.zod.ts:179):

{"code":"invalid_type","expected":"function","path":["onClick"],
"message":"Invalid input: expected function, received object"}

The census, run on the shape rather than on a literal

A grep for "action": "toast" under-counts by construction: the sonner half spells the same shape { action: 'sonner', … }, and one fixture spells it { action: { label, onClick } }. So the sweep walks every catalog entry at every depth for any key that reads as a handler slot (/^on[A-Z]/ or events), bucketed by the value's kind:

BEFORE object=14 string=2 array=0
AFTER object=0 string=1 array=0

The 14 object-valued hits were exactly the two directories this card names — no others anywhere in the 430-entry corpus. Independent corroboration: docs/audits/2026-08-zod-to-json-schema-fidelity.md:317 counted the same 14, from a different instrument, before this card was filed. That audit is a dated snapshot and is deliberately left as written.

The remaining string=1 is components-feedback-toaster/with-toast-trigger.json's children[0].onClick: "toast(\"Hello from ObjectUI!\")" — the handler-EXPRESSION dialect, which is #6182's open decision and a different shape. Untouched on purpose; a sweep that banned handler keys outright would be answering #6182 by accident.

What actually runs, measured rather than picked for idiom

  • ActionRunner's runnable vocabulary is script | url | modal | flow | api | form | navigation (builtinExecutors, core/src/actions/ActionRunner.ts:845). No toast, no sonner — so { action: 'toast' } had no dispatcher even if something had read it.
  • Nothing reads a handler key as an action object anywhere in packages/react, packages/core, packages/components, packages/layout. Every onClick hit is a call of a function (await action.onClick()).
  • ComponentRegistry.register('toast', …) and ('sonner', …) DO run: each renders its own trigger button and calls sonner's toast() from its handler.
  • Prior art check: zerotype: 'toast' or type: 'sonner' fixtures existed anywhere in the corpus. Positive control — the same search finds 5 type: 'toaster' fixtures. The registered spelling had no demo at all; the demos used an unregistered one.

⚠️It was worse than inert, and this is a new reading.onClick is a member of SDUI_DOM_PASS_THROUGH_KEYS (core/src/utils/dom-props.ts:88), so the action object reached the rendered button's DOM listener slot. Clicking it throws:

Expected `onClick` listener to be a function, instead got a value of `object` type.

That is pinned as a counter-probe, so "the old shape did nothing" is a measurement taken in this harness rather than a claim inherited from the card.

The key rule the corrections follow

Only keys that the governing schema declares AND the renderer reads:

toastsonner
declared ∧ readtitle, description, variant, duration, classNamemessage, title, description, variant, buttonLabel, buttonVariant, className
read, undeclaredbuttonLabel, buttonVariantnot used
declared, unreadaction, onDismiss, positionnot used

SonnerSchema declares buttonLabel/buttonVariant; ToastSchema does not, though its renderer reads both. That asymmetry is why the sonner demos carry per-demo trigger labels and the toast demos render the default Show Toast. Teaching them on toast would have re-introduced an undeclared key into the fixtures this card is cleaning — #6161 set the posture on these same pages ("a key a renderer genuinely reads would have been routed out as an undeclared capability rather than deleted"). Routed out as #6496.

Two demos had no working spelling, and that is why they changed premise

The toast renderer has no in-toast action button and no dismiss callback; the sonner renderer has no promise form. So "With Action", "With Undo Action" and "Promise Toast" could not be corrected into working demos of what their headings promised. They now demonstrate the info, warning, description and buttonVariant surfaces that do run, and the pages follow. Curated titles for those five entries went into src/catalog-meta.json — the sidecar the generator only ever reads — so both the docs page and the standalone catalog gallery read correctly without renaming any fixture or changing any catalog id. index.ts is regenerated by scripts/regenerate-catalog-index.py; --check was green on the untouched tree first, so the 15/10 diff there is only those five entries.

The missing capability is filed as #6496, not built here.

Overlap with an open card — needs your call, not mine

#6347 is still open and this PR lands two of its four items. It independently measured that sonner.mdx documents duration (which SonnerSchema does not declare and the renderer does not pass) and that its "With Action" section renders a fixture that is a button, not a sonner node. Both are on a page inside this card's declared file face, both are the same defect class, and both had their correct form already pinned by SonnerSchema plus the renderer — so they are corrected here rather than left contradicting the fixtures beside them. The action row went with them, on the same evidence.

#6347's button-group.mdx half — value, selectionMode, and the whole Selection Mode section — is untouched by this PR, and that card should stay open for it. content/docs/components/basic/button-group.mdx is not in this diff.

Beyond the declared file face — declared, not slipped in

  • examples/schema-catalog/test/form-control-dom-leak-5632.test.tsxNODE_CENSUS.button 140 → 126. That is an exact census, not a shrink-only baseline, and its own header sanctions this case: "These move when the CATALOG is authored, not when a renderer changes." The gate's diff moved exactly one number, by exactly 14 — itself a corroboration of the census above. No renderer was touched.
  • examples/schema-catalog/src/catalog-meta.json and the regenerated src/index.ts — described above.
  • content/docs/components/form/button.mdx was in the dispatched face and is not in this diff: the shape census found no handler-object payload in components-form-button/*, and line 64's onClick?: () => void | Promise<void> is accurate to ButtonSchema. A measured non-edit.

Clause ② — public surface, following the re-export chain

No public surface is touched. Checked three ways, none of them a symbol-name grep:

  1. No file under packages/ is in this diff at all — so no published package's entry, and no chain from one, can reach a changed file.
  2. The one changed file that IS a package entry (examples/schema-catalog/src/index.ts) has an identical export-statement set before and after — same five statements, same signatures, only line numbers shifted by the five added tags entries. It contains no export *; its one re-export is export type { Example, ExampleMeta } from './types.js', and types.ts is untouched.
  3. Following the chain outward: @object-ui/example-schema-catalog is private: true and matched by the changesets ignore pattern @object-ui/example-*. Its only consumer in the workspace is @object-ui/site, which is itself in ignore. The chain terminates at an app, never at a published package.

Corroborated by check:readme-exports and check:entry-guard, both green.

Tests

Two pins, one per half of the defect.

  • examples/schema-catalog/test/toast-demo-dispatch-6250.test.tsx (new) — renders each of the 14 demos against a real <Toaster />, clicks the trigger, and asserts the authored strings land in a toast node. Toasts are identified by node identity, not text: sonner's store is module-global and replays to every newly mounted <Toaster />, which was measured — basic-toast and success-toast share a description and a text query found it twice. No vi.mock('sonner'): sonner is not a dependency of this package, so the specifier would resolve to a different module than the renderer imports — a mock that cannot fail.
  • examples/schema-catalog/test/component-fixture-declared-keys.test.ts — the finding(examples): the live SchemaExample fixtures on four component pages teach the same invented keys the prose blocks just stopped teaching #6157 pin asserted fixture.type === 'button' and destructured fixture.onClick, i.e. it pinned the exact branch this card removes. Measured red first (6 failed / 10 passed), then replaced rather than reworded: what finding(examples): the live SchemaExample fixtures on four component pages teach the same invented keys the prose blocks just stopped teaching #6157 established — the toast variant value is a declared ToastSchema member, with destructive refused — is carried forward against the top-level variant these nodes now declare, counter-probe intact. A corpus-wide census assertion was added, with a positive control that the walker really descends (it names the one nested handler key the corpus still carries).

Reverse verification — predicted RED, observed RED

The fix was committed first, then one fixture (components-feedback-toast/destructive.json) was reverted to the retired shape. The mutation was proved on disk before any reading — grep -c '"action": "toast"' = 1, grep -c '"type": "toast"' = 0, and git hash-object differing from the HEAD blob — and restored through an EXIT INT TERM trap using an absolute path.

Reverting one fixture turned 7 assertions red across both files:

toast-demo-dispatch-6250.test.tsx 3 failed | 14 passed
× the corpus is the fourteen entries this card covers — non-vacuity control
× no demo hangs a payload off a handler key any more — the retired shape
× components-feedback-toast/destructive fires its toast on click
component-fixture-declared-keys.test.ts 4 failed | 15 passed
× …/destructive is a `toast` node that parses green under ToastSchema
× …/destructive declares a variant ToastSchema knows
× …/destructive counter-probe: the pre-#6157 value is still refused
× every handler value in the corpus is a string expression, never an action object

Restore proved byte-identical (git hash-object == git rev-parse HEAD:<path> = b4a37b1c1, both non-empty; git diff HEAD empty), and both files returned to green — 36 passed (36).

Gates — all at 076ed3b51, each quoted from its own verdict line

Exit codes captured before any pipe.

gateexitverdict
check:doc-types0✅ Every documented component type is registered. (184 docs, 1064 blocks, 895 type literals)
check:doc-snippets0Every covered documentation snippet compiles against the built types.Semantic phase: 267 of 267 block(s) judged, 0 failed.
check:doc-fences0✅ check:doc-fences — every TypeScript block in 223 document(s) is fenced ts/tsx/typescript…
check:docs-route-closure0✅ gauge: 1349 modules crawled from 148 route roots (144 MDX)… 18 of 46 workspace packages stay OUTSIDE the closure
check-doc-links0Links are valid across 17 scan roots.
check:control-bytes0✅ check-control-bytes: OK (scanned 5363 tracked text file(s); skipped 85 binary).
check:entry-guard0✓ check:entry-guard: 50 scripts/ file(s) — no entry guard outside the baseline…
check:readme-exports0✅ check-readme-exports: OK (43 README(s)… 378 self-imports judged (378 real, 0 wrong-path, 0 fabricated))
check:vi-mock-specifiers0✅ check-vi-mock-specifiers: OK (3816 tracked source file(s)…)
check:phantom-deps0✅ Every in-scope import is declared by the package that publishes it.
regenerate-catalog-index.py --check0examples/schema-catalog/src/index.ts is up to date (430 entries).
check-changeset-presence0✅ No source of a released package changed in this range, so no changeset is owed. (1 changeset added anyway, empty frontmatter — the repo's first-class "declares no release")
check-changeset-no-major0✅ No changeset declares a major bump.
check-changeset-fixed0✅ All workspace packages are in the changeset fixed group.
vitest examples/schema-catalog/ scripts/__tests__/catalog-index-regenerable-4633.test.ts0Test Files 15 passed (15) · Tests 1824 passed (1824)
pnpm --filter @object-ui/example-schema-catalog type-check0script name echoed, tsc --noEmit && tsc -p tsconfig.test.json, no output

Two non-zero exits that were NOT MEASURED, excluded from the table and named here:check:readme-exports first exited 1 with "3 self-import(s) could not be judged — @object-ui/plugin-ai's type entry ./dist/index.d.ts is not on disk — run pnpm build first", and the catalog type-check first exited 2 with two TS2882s naming @object-ui/plugin-gantt and @object-ui/plugin-map in a file this PR does not touch. Both were missing dist/, not defects; both went green after building those packages. Neither is counted as a failure.

Lint — a measured narrowing, not a skipped run

Repo-wide lint is CI's run. Locally the narrowing is measured, from eslint's own config rather than from a guess about which files count:

  1. Population source. Asked eslint directly, per file. .mdx, .json and .md come back File ignored because no matching configuration was supplied — 16 of the 19 changed files are outside the configured population, so a green on them would have been a non-reading, not a pass.
  2. File count, from --format json. The three changed files that ARE in the population (.ts / .tsx) report 0 errors, 0 warnings. The bounding package's whole population is 17 files, all genuinely linted: 0 errors, 1 warning — 'ExampleMeta' is defined but never used in the generated src/index.ts, present verbatim on 7975f2d85 and on a line this diff does not touch. Left alone; the generator owns that file.
  3. Immutability of untouched files.eslint.config.js configures no type-aware linting — no projectService, no parserOptions.project, no project: anywhere in it — so no rule's verdict on a file outside this diff can depend on this diff.

Filed in passing

All three unassigned, finding label only, no grading.

The fence — held

⛔ No action union declared on ButtonSchema.onClick. ⛔ No dispatcher built. ⛔ No toast or sonner executor added to ActionRunner. ⛔ No packages/ file changed at all. #6249 is out of scope here and remains open. The maintainer's route is not foreclosed by anything in this diff — and #6497 is new evidence for it.

Draft, no auto-merge: the PM lands this.


Generated by Claude Code


Generated by Claude Code

…xecutes
The seven `components-feedback-toast/*` and seven `components-feedback-sonner/*`
`SchemaExample` fixtures hung an action object off `onClick`:
{ "type": "button", "label": "Destructive Toast", "variant": "destructive",
"onClick": { "action": "toast", "variant": "error", "title": "Error", … } }
`ButtonSchema.onClick` is `z.function()`, so all fourteen were a RED
`safeParse` on the ENVELOPE — measured before the change, `{"code":
"invalid_type","expected":"function","path":["onClick"],"message":"Invalid
input: expected function, received object"}` on 14 of 14. And nothing read a
handler key as an action object: `ActionRunner`'s runnable vocabulary is
`script | url | modal | flow | api | form | navigation`, with no `toast` and no
`sonner`. The docs page prints `JSON.stringify(schema)` beside each demo, so
that shape was the copy-paste surface for every reader.
It was worse than inert. `onClick` is a member of `SDUI_DOM_PASS_THROUGH_KEYS`,
so the action object reached the rendered button's DOM listener slot, and React
refuses it on click: "Expected `onClick` listener to be a function, instead got
a value of `object` type."
All fourteen now author the registered spellings the engine already executes —
`type: 'toast'` and `type: 'sonner'` — whose renderers draw their own trigger
button and call sonner's `toast()` from it. Keys are limited to those the
governing schema declares AND the renderer reads; `buttonLabel` /
`buttonVariant` are used on `sonner` (SonnerSchema declares both) and not on
`toast` (ToastSchema declares neither, though its renderer reads them).
`feedback/toast.mdx` and `feedback/sonner.mdx` follow, including two keys
sonner's page taught that neither `SonnerSchema` declares nor its renderer
reads (`duration`, `action`).
Not done here, both left to the maintainer: declaring an action union on
`ButtonSchema.onClick` with a dispatcher behind it, and giving the toast
renderers the in-toast action button and promise form the removed demos
implied. Both are capability expansions with zero runtime today.
Part of #6250
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
@os-support-aiClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review: ACCEPT. Verified from the tree at 076ed3b, not from the report.

The fence held. 22 files, zero under packages/. No action union on ButtonSchema.onClick, no dispatcher, no new ActionRunner executor. content/docs/releases/ untouched.

Every row the rewritten sonner Schema block teaches is declared, and both removed rows are declared by nothing. Checked against both faces of the type rather than one:

  • packages/types/src/feedback.tsSonnerSchemamessage?, title? (its JSDoc says "alias for message" verbatim), description?, variant? over five members, buttonLabel?, and buttonVariant? over exactly the six the block now prints.
  • packages/types/src/zod/feedback.zod.tsSonnerSchema mirror — same key set.
  • duration and action appear in neither, and packages/components/src/renderers/feedback/sonner.tsx passes sonner's toast() only a description option. Removing those two rows is a correction, not a judgement call.

The new toast page prose is true. "ToastSchema declares exactly five members" — the zod enum is default, success, warning, error, info, and the four demos now in the grid carry default / error / info / warning with success on the Success Message example. Five for five.

Leaving the toast page's Schema block alone is principled, not convenient — this is the part I checked hardest, because at a glance it looks like the same defect the sonner block just had. It is not. ToastSchema really does declare both action and duration, in both faces, so that block is accurate against the shipped type; its problem is that no renderer reads action. That is declared-but-not-read, which is #6496's enforce-or-remove question. Sonner's two rows were declared nowhere and read nowhere. Different defect, different tier, correctly split.

The corrected spelling executes. The sonner renderer draws its own single Button and calls toast() from its handler — which is also what makes getByRole('button') in the new dispatch pin unambiguous rather than lucky. The buttonVariant values the fixtures author, outline and destructive, are both members of the declared union.

The census move is complete, and the instrument is the right one.button: 140 -> 126. MEASURED_TYPES in the DOM-leak pin contains neither toast nor sonner, and its walker keys on the schema tree's type string rather than on rendered DOM — so a sonner node contributes zero to the button row even though it renders a button element. 14 is therefore the whole move, and no other row was owed one.

The #6157 pin was repointed, not deleted. Measured RED first, then replaced, carrying that card's actual fact — the toast variant value is a declared member, destructive refused — forward against the top-level variant the corrected nodes now declare, counter-probe intact. This is the case where the obvious remedy is the forbidden one, a guard inverting into a ratchet against its own success. It was handled the right way round.

Both open questions, answered.

  1. The docs(components): sonner and button-group still document four more props no shipped type declares #6347 overlap: option A. The sonner rows stay here. That page is inside this card's declared file face, the correct form was determined by SonnerSchema plus the renderer rather than by taste, and backing them out would ship a page teaching duration and action beside the fixtures this PR just corrected — the exact contradiction that produced finding(examples): the live SchemaExample fixtures on four component pages teach the same invented keys the prose blocks just stopped teaching #6157. I am not rewriting docs(components): sonner and button-group still document four more props no shipped type declares #6347's body: its scope was written by another producer and that text is not this seat's to edit. A note on its thread reaches the same reader, and it is recorded there now. This seat is that card's dispatcher, so its order will go out scoped to the button-group half only.

  2. The three demos' editorial premise: option A, accepted as landed. This does not reach the human floor. No packages/ file changed, so no published capability was removed — the toast renderer never had an in-toast action button or a dismiss callback, and the sonner renderer never had a promise form. There was no capability to remove, only a promise no runtime kept. The page now teaches strictly more of what runs than it did before. Whether those capabilities should exist is finding(types): ToastSchema and the toast renderer disagree in BOTH directions — two keys read but undeclared, two declared but unread #6496, which you filed; nothing here forecloses restoring the sections if it says yes.

One thing to carry forward, not a blocker. The counter-probe pins React's exact wording. That couples this example package's suite to a foreign dependency's message text, so a React bump can turn it red on an unrelated PR. It is the secondary assertion — "no toast was added, whatever the click did" is load-bearing and independent of it — and the comment says out loud that it is meant to turn red for review. Fine as authored. Worth remembering as the first suspect if this file ever goes red without the catalog changing.

Landing state: FAILED=none on 076ed3b; four test shards, Lint and Type Check still in progress. Queued the moment those settle green.


Generated by Claude Code

@os-support-ai
os-support-ai marked this pull request as ready for review August 26, 2026 06:05
@os-support-ai
os-support-ai added this pull request to the merge queueAug 26, 2026
Merged via the queue into main with commit d09e13fAug 26, 2026
29 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-6250-toast-demo-shapes branch August 26, 2026 06:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(types,examples): every toast demo hangs an action object off onClick, which is declared as a function and read by no dispatcher

2 participants

@os-support-ai@claude