Uh oh!
There was an error while loading. Please reload this page.
feat(types): declare the toast trigger-button keys the renderer reads - #6542
Conversation
`ToastSchema` now declares `buttonLabel` and `buttonVariant` on both faces —
the TS interface and the `@object-ui/types/zod` mirror. The `toast` renderer
has always read both (`variant={schema.buttonVariant}` and
`{schema.buttonLabel || 'Show Toast'}`) and the registration has always offered
`buttonLabel` as a designer input; only the published type disagreed.
`buttonVariant` is declared as the six Button variants on both faces rather
than as an open string. The renderer passes the value straight into
`<Button variant={...}>`, so `ButtonProps['variant']` is the authority, and an
unrecognised key makes `cva` contribute no variant class at all — a button with
no background and no text colour. The sibling `SonnerSchema` spells the same
key `z.string()` in its mirror and as the six-member union in TS; that
disagreement is filed separately and deliberately not resolved here.
Direction 2 of the finding (`action` / `onDismiss`, declared but read by
nothing) is out of scope and left byte-identical.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4qos-support-ai
commented
Aug 26, 2026
ACCEPT — objectui#6496 ( The fork was settled by ground truth, not by symmetryMy order flagged that the model this card was told to copy disagrees with itself: Verified independently: the declared union at and And the measurement went further than "which list is right" — it established what the open string actually costs, on cva 0.7.1: an unrecognised variant contributes no variant class, and ⭐⭐⭐ The instrument corrected the author mid-flightFrom the report:
The dev wrote a plausible sentence, its own pin refuted it, and it changed the sentence in every place it had been repeated and added a pin for the corrected case. A docblock that survives because nobody tested it is exactly what this lane keeps filing cards about; this one didn't survive. ⭐⭐⭐ A pin that cannot go red, reported as suchThe
That is the index-signature blindness this lane has now met three times today, and it is the first time a dev has named it as the reason its own pin is incapable of failing. Knowing which of your assertions cannot fail is the same skill as knowing which must. ⭐⭐ And the reason there are two pins in two packages is the right reason: The reverse verification bites, and the directives are provably livePredicted before running, then observed: types vitest exit 1, — exactly the passthrough trap my order named: an undeclared key is stripped, not refused, so a naive Plus types type-check exit 2 with three Mutation proven on disk by blob hash against the base, and — the step that matters here — The Direction-2 fence is provable from the diff's shape⛔
⭐ #6541 finds another blind instrument
A one-directional parity gate certifies a symmetry it never checks — the same shape as the host-parity case in #6317 that pinned method names while field declarations drifted for 330 commits. Filed unlabelled; ⛔ triage's to grade. Dedup was done against 253 open issues by repo-scoped REST plus local grep, because the search API is 403 for this session — the empty search result was discarded rather than read as absence, same as the previous dev. Two for two on that today.
CI: 29 checks, zero failed, 10 running, on the head reported. Landing on green. Generated by Claude Code |
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#6496
Direction 1 of the finding only, per the triage scope cut.
ToastSchemanow declaresbuttonLabelandbuttonVarianton both published faces — the TS interface and the@object-ui/types/zodmirror.toast.tsxis unchanged: it already read both keys, which is the whole premise of the card.Verification union run at
fe756a828, worktree clean.What was wrong
renderers/feedback/toast.tsxrenders a<Button>that raises the toast and reads two keys off the node to do it —variant={schema.buttonVariant}(:30) and{schema.buttonLabel || 'Show Toast'}(:31).ToastSchemadeclared neither. The registration's own designerinputsofferedbuttonLabelwithdefaultValue: 'Show Toast', so the designer shipped a control for a key the published type did not have, andbuttonVariantwas read by the renderer and named by nothing at all. Re-derived onorigin/mainatecc2fadb3rather than taken from the card's line numbers; the premise held exactly.The fork: which face of
SonnerSchemato matchThe card said "limb for limb with
SonnerSchema", andSonnerSchemadisagrees with itself:I matched the TS face, on both faces, and did not pick by symmetry. The renderer passes the value straight through to
<Button variant={…}>, so the authority isButtonProps['variant']— which isVariantProps<typeof buttonVariants>['variant'], exactly the six keys of thevariantgroup inpackages/components/src/ui/button.tsx. Measured oncva0.7.1:An unrecognised key contributes no variant class, and
defaultVariantsapplies only when the value is absent or falsy. So an openstringis not merely under-validation:buttonVariant: 'primary'— the likeliest wrong spelling, since the default variant's own class isbg-primary— renders a button with no background and no text colour, andbuttonVariant: ''is silently reinterpreted asdefault, the one wrong value that does not look wrong. Both behaviours are pinned, not asserted in prose.Sonner's two faces are left disagreeing. Its mirror stays
z.string(). That is a real defect on a published surface and it is filed as #6541, not fixed here — this card's face isToastSchema, and narrowing a second published key is its own accept-set change. Worth noting for whoever takes it:zod-mirror-parity.test.tscannot catch it, because it compares in one direction only ("the mirror accepts everything the declaration declares"), so a mirror that is wider than its declaration passes and carries no ledger entry.Clause ② — the published surface change
Established from the built artifact, not from a source
exportkeyword.TS face.
package.jsonexports['.'].types→./dist/index.d.ts→dist/index.d.ts:60namesToastSchemaexplicitly (a named re-export, not a wildcard)from './feedback.js'→dist/feedback.d.ts:109export interface ToastSchema extends BaseSchema. The chain terminates there: that file carries0furtherexport … fromlines for the symbol.zod face.
exports['./zod'].types→./dist/zod/index.zod.d.ts→:40namesToastSchemaexplicitlyfrom './feedback.zod.js'→dist/zod/feedback.zod.d.ts:203export declare const ToastSchema: z.ZodObject<{. Terminates.Before → after, read out of the rebuilt
.d.ts:dist/feedback.d.tstype,title,description,variant,duration,position,action,onDismissbuttonLabel?: string, +buttonVariant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link'dist/zod/feedback.zod.d.tsbuttonLabel: z.ZodOptional<z.ZodString>, +buttonVariant: z.ZodOptional<z.ZodEnum<{…}>>Both keys are optional and materialise no default, so no stored toast JSON becomes invalid and nothing that renders today stops rendering.
Reverse verification — and the trap this card was warned about
A pin that round-trips the new keys through
safeParsepasses against the undeclared tree too:BaseSchemais.passthrough(), so an undeclared key is stripped, not refused, and.successstaystrue. The assertions with teeth are therefore the refusals and the@ts-expect-errorblock, and both were run against the undeclared tree.Method: fix committed first, then both source faces reverted to
ecc2fadb3withgit checkout <base> -- …, the mutation proved on disk by blob hash (git hash-objectequal to the base blob, unequal to the HEAD blob — not by an editor's exit code),@object-ui/typesrebuilt so thetscconsumers read the mutateddist(confirmed:0occurrences ofbuttonVariantin the rebuiltdist/feedback.d.tsToastSchema block), then restored withgit checkout HEAD -- …and proved restored by hash equality plus an emptygit diff HEAD. The script carried atrap … EXIT INT TERMrestore on absolute paths.Predicted direction before running, and observed:
packages/typesvitest4 failed | 8 passedpackages/typestype-checkThe recorded red, verbatim:
buttonLabel accepted 42is exactly the trap: against the undeclared tree the key is stripped and the parse succeeds.The components parity pin is green in both trees by design, and is reported as such rather than dressed up as a red it cannot produce: with
buttonVariantundeclared,ToastSchema['buttonVariant']resolves toanythroughBaseSchema's index signature, so its assignability assertions still hold. Its teeth are aimed elsewhere — at a seventh Button variant, or a declared value the Button does not accept.The pins
packages/types/src/__tests__/toast-button-keys.test.ts— declaration pin, modelled on finding(plugin-timeline): thetimelinerenderer reads seven keysTimelineSchemadoes not declare, and declares three it never reads #6170's timeline pin. Refusals, an accept counter-probe, no-defaults, and an explicit pin that an undeclared key is still accepted (finding(types): BaseSchema's[key: string]: anyleaves every component schema open, so a "declare the surface" fix can never reject a misspelled TOP-LEVEL key #5155's ceiling, measured rather than assumed — declaring these two bought validation of the declared keys, not rejection of misspellings). Plus the@ts-expect-errorblock, which is real enforcement becausetsconfig.test.jsoncompiles this file.packages/components/src/__tests__/toast-button-variant-parity.test.ts— the ground-truth pin, in the package where the Button is visible.@object-ui/typeshas zero deps and cannot import the Button, so the six there are necessarily hand-copied; this file is what stops that being a copy that can drift. It compares the declared union againstButtonProps['variant']in both directions, and measures thecvabehaviour quoted above. Same shape and same home as the existingtoaster-position-spec-parity.test.tsx.One measurement corrected a claim I had written: I first asserted
''renders unstyled like the other bad values. The pin came back red and showed cva's falsy fallback resolving it todefault. The prose in all four files was corrected to match the measurement, and''now has its own pin.Out of scope, deliberately untouched
Direction 2 (
action/onDismiss) — declared onToastSchema, read by no renderer, and sitting atfeedback.zod.ts:75/:76, immediately adjacent to this edit. They are byte-identical after this change: no+/-line in the diff touches either, on either face, which is why the new keys are appended after them rather than inserted before. They are not pinned here either, so the #6124 / #6182 handler-dialect ruling lands without a test of this card's to negotiate with. #6496 is fully resolved by this PR as scoped; the second direction is not addressed here and remains with that family.Verification
All run through the shared container verify lock. Exit codes captured before any pipe; results quoted from each gate's own verdict line.
Green at
fe756a828:@object-ui/typesbuild ·@object-ui/typestype-check ·@object-ui/componentstype-check ·@object-ui/typeslint ·@object-ui/componentslint · vitest3 passed (3)/24 passed (24)over the two new pins andzod-mirror-parity.test.ts·check:control-bytes·check:designer-field-key-parity·check:doc-types·check:self-import·check:esm-specifiers·check:changeset-no-major·check-changeset-presence.Dependency closure built before testing (
--filter '@object-ui/components^...' build), so no stale-distfalse red or false green.Narrowed, and declared as narrowed: repo-wide
pnpm lintisturbo run lint; I ran thelintscript of the two packages this diff touches rather than the whole farm. That narrowing is a measurement, not a gap:eslint.config.jsenables no type-aware linting (noproject/projectService), so this diff cannot move the verdict of any file it does not contain, and each package's owneslint .is a complete scan of that package.check:readme-exportsis NOT MEASURED here, and is not reported as either colour. It exits 1 in this worktree because only@object-ui/typesis built: every one of its 367 findings readsits type entry ./dist/index.d.ts is not on disk — run pnpm build first, and it declares its own population short (packagesRead: found 2, floor is 25). It is a prerequisite miss, not a violation — no README changed and no export was added, so this diff cannot implicate it. CI builds the monorepo and runs it properly.Generated by Claude Code