Uh oh!
There was an error while loading. Please reload this page.
fix(examples,docs): correct the toast demos to spellings the engine executes - #6498
Conversation
…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_011SfZeFWrhGLHmfq61xbz4qos-support-ai
commented
Aug 26, 2026
PM review: ACCEPT. Verified from the tree at 076ed3b, not from the report. The fence held. 22 files, zero under 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:
The new toast page prose is true. "ToastSchema declares exactly five members" — the zod enum is 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. The corrected spelling executes. The sonner renderer draws its own single Button and calls The census move is complete, and the instrument is the right 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, Both open questions, answered.
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 |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#6250
All measurements below are on
076ed3b51, branched fromorigin/main7975f2d85, withpackages/typesbuilt fresh.Before → after, as a reading
The seven
components-feedback-toast/*and sevencomponents-feedback-sonner/*SchemaExamplefixtures hung an action object offonClick. Parsing each fixture against the mirror its owntypeselects:Every one of the 14 was the same issue, on the ENVELOPE and not on any variant —
ButtonSchema.onClickisz.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]/orevents), bucketed by the value's kind: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:317counted 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=1iscomponents-feedback-toaster/with-toast-trigger.json'schildren[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 isscript | url | modal | flow | api | form | navigation(builtinExecutors,core/src/actions/ActionRunner.ts:845). Notoast, nosonner— so{ action: 'toast' }had no dispatcher even if something had read it.packages/react,packages/core,packages/components,packages/layout. EveryonClickhit 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'stoast()from its handler.type: 'toast'ortype: 'sonner'fixtures existed anywhere in the corpus. Positive control — the same search finds 5type: 'toaster'fixtures. The registered spelling had no demo at all; the demos used an unregistered one.onClickis a member ofSDUI_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: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:
toastsonnertitle,description,variant,duration,classNamemessage,title,description,variant,buttonLabel,buttonVariant,classNamebuttonLabel,buttonVariant— not usedaction,onDismiss,position— not usedSonnerSchemadeclaresbuttonLabel/buttonVariant;ToastSchemadoes not, though its renderer reads both. That asymmetry is why the sonner demos carry per-demo trigger labels and the toast demos render the defaultShow Toast. Teaching them ontoastwould 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
toastrenderer has no in-toast action button and no dismiss callback; thesonnerrenderer 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 theinfo,warning,descriptionandbuttonVariantsurfaces that do run, and the pages follow. Curated titles for those five entries went intosrc/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.tsis regenerated byscripts/regenerate-catalog-index.py;--checkwas 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.mdxdocumentsduration(whichSonnerSchemadoes not declare and the renderer does not pass) and that its "With Action" section renders a fixture that is abutton, 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 bySonnerSchemaplus the renderer — so they are corrected here rather than left contradicting the fixtures beside them. Theactionrow went with them, on the same evidence.#6347's
button-group.mdxhalf —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.mdxis not in this diff.Beyond the declared file face — declared, not slipped in
examples/schema-catalog/test/form-control-dom-leak-5632.test.tsx—NODE_CENSUS.button140 → 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.jsonand the regeneratedsrc/index.ts— described above.content/docs/components/form/button.mdxwas in the dispatched face and is not in this diff: the shape census found no handler-object payload incomponents-form-button/*, and line 64'sonClick?: () => void | Promise<void>is accurate toButtonSchema. 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:
packages/is in this diff at all — so no published package's entry, and no chain from one, can reach a changed file.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 addedtagsentries. It contains noexport *; its one re-export isexport type { Example, ExampleMeta } from './types.js', andtypes.tsis untouched.@object-ui/example-schema-catalogisprivate: trueand matched by the changesetsignorepattern@object-ui/example-*. Its only consumer in the workspace is@object-ui/site, which is itself inignore. The chain terminates at an app, never at a published package.Corroborated by
check:readme-exportsandcheck: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-toastandsuccess-toastshare a description and a text query found it twice. Novi.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 assertedfixture.type === 'button'and destructuredfixture.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 declaredToastSchemamember, withdestructiverefused — is carried forward against the top-levelvariantthese 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, andgit hash-objectdiffering from the HEAD blob — and restored through anEXIT INT TERMtrap using an absolute path.Reverting one fixture turned 7 assertions red across both files:
Restore proved byte-identical (
git hash-object==git rev-parse HEAD:<path>=b4a37b1c1, both non-empty;git diff HEADempty), and both files returned to green — 36 passed (36).Gates — all at
076ed3b51, each quoted from its own verdict lineExit codes captured before any pipe.
check:doc-types✅ Every documented component type is registered.(184 docs, 1064 blocks, 895typeliterals)check:doc-snippetsEvery covered documentation snippet compiles against the built types.—Semantic phase: 267 of 267 block(s) judged, 0 failed.check:doc-fences✅ check:doc-fences — every TypeScript block in 223 document(s) is fenced ts/tsx/typescript…check:docs-route-closure✅ gauge: 1349 modules crawled from 148 route roots (144 MDX)… 18 of 46 workspace packages stay OUTSIDE the closurecheck-doc-linksLinks are valid across 17 scan roots.check:control-bytes✅ check-control-bytes: OK (scanned 5363 tracked text file(s); skipped 85 binary).check:entry-guard✓ check:entry-guard: 50 scripts/ file(s) — no entry guard outside the baseline…check:readme-exports✅ check-readme-exports: OK (43 README(s)… 378 self-imports judged (378 real, 0 wrong-path, 0 fabricated))check:vi-mock-specifiers✅ check-vi-mock-specifiers: OK (3816 tracked source file(s)…)check:phantom-deps✅ Every in-scope import is declared by the package that publishes it.regenerate-catalog-index.py --checkexamples/schema-catalog/src/index.ts is up to date (430 entries).check-changeset-presence✅ 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-major✅ No changeset declares amajorbump.check-changeset-fixed✅ All workspace packages are in the changeset fixed group.vitest examples/schema-catalog/ scripts/__tests__/catalog-index-regenerable-4633.test.tsTest Files 15 passed (15) · Tests 1824 passed (1824)pnpm --filter @object-ui/example-schema-catalog type-checktsc --noEmit && tsc -p tsconfig.test.json, no outputTwo non-zero exits that were NOT MEASURED, excluded from the table and named here:
check:readme-exportsfirst exited 1 with "3 self-import(s) could not be judged —@object-ui/plugin-ai's type entry./dist/index.d.tsis not on disk — runpnpm buildfirst", and the catalogtype-checkfirst exited 2 with twoTS2882s naming@object-ui/plugin-ganttand@object-ui/plugin-mapin a file this PR does not touch. Both were missingdist/, 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:
.mdx,.jsonand.mdcome backFile 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.--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 usedin the generatedsrc/index.ts, present verbatim on7975f2d85and on a line this diff does not touch. Left alone; the generator owns that file.eslint.config.jsconfigures no type-aware linting — noprojectService, noparserOptions.project, noproject:anywhere in it — so no rule's verdict on a file outside this diff can depend on this diff.Filed in passing
toasterdemos and page teach aproviderkey thatToasterSchemadoes not declare and the renderer does not read #6494 — thetoasterdemos and page teach aproviderkeyToasterSchemadoes not declare and the renderer does not read. Two "provider" demos render byte-identically. Same defect class, different shape, and the page makes a capability claim, so it is a contract question rather than a typo.ToastSchemaand thetoastrenderer disagree in BOTH directions — two keys read but undeclared, two declared but unread #6496 —ToastSchemaand its renderer disagree in both directions:buttonLabel/buttonVariantread but undeclared,action/onDismissdeclared but unread. This is what constrained the two decisions above.EventableSchema/UIEventHandler, with no mirror, no extender and no reader #6497 — the action-object dialect is already declared and exported:EventableSchema.onClick?: UIEventHandler | string(api-types.ts:278), public indist/index.d.ts, with no zod mirror, no component schema extending it, and no reader. It refines this card's "declared nowhere" and is a direct input to the [Decision] Is an authored handler EXPRESSION a supported dialect? 56 mirrors say function, 11 say string, and both appear in files that mirror the other #6182 / finding(types): 28 zod-mirror keys are declaredz.function(), which NO JSON document can satisfy — 14 in-repo corpus files author them and are silently invalid #6124 decision — the declaration half is not a blank page.All three unassigned,
findinglabel only, no grading.The fence — held
⛔ No action union declared on
ButtonSchema.onClick. ⛔ No dispatcher built. ⛔ Notoastorsonnerexecutor added toActionRunner. ⛔ Nopackages/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