Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

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

Closed

Description

@yinlianghui

Found while implementing #6157 (correcting the invented keys the live SchemaExample fixtures teach). #6157 corrects a value inside this payload; this card is about the payload's envelope, which is a larger question and was deliberately left alone there.

⚠️ Dedup caveat, stated up front: the GitHub search API was rate-limited for this session at filing time, so the usual "search open issues before filing" step could not be completed. If this duplicates an existing card, close it as such — the measurement below is the part worth keeping.

Measured on e3354ba08, with packages/types built fresh

All seven components-feedback-toast/* fixtures are { "type": "button", … } with an action object hung off onClick:

{
  "type": "button",
  "label": "Destructive Toast",
  "variant": "destructive",
  "onClick": { "action": "toast", "variant": "error", "title": "Error", "description": "Something went wrong." }
}

1. The shipped type says onClick is a function. ButtonSchema.onClick is () => void | Promise<void> (form.d.ts:56), and the zod mirror is z.function(). So ButtonSchema.safeParse() on either fixture is RED — not on any variant, on the envelope:

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

Control: the same object with onClick removed parses green, so this is a key-by-key result and not a schema that refuses everything.

2. No dispatcher reads it. Grepped packages/** for case 'toast', action === 'toast', === 'toast', schema.onClick and .onClick as: the only hits are ActionRunner's action.onClick() (which calls a function, the legacy callback slot) and schema-builder's setter. packages/components/src/renderers/form/button.tsx never forwards schema.onClick to the rendered Button at all. So on this tree, clicking the demo button fires nothing.

Why it matters more than one demo

This is the SDUI idiom the docs teach for "button that raises a toast" — seven fixtures on the toast page alone, plus the same shape elsewhere in the catalog — and it is declared nowhere and executed nowhere. That combination is the objectui#5250 hole at its widest: BaseSchema.passthrough() means objectui validate accepts the fixture, the docs render it, an AI few-shot retriever is invited to copy it, and nothing ever runs it.

The interesting question for triage is which way the divergence should close:

  • the fixtures are wrong — the demos should use { "type": "toast", … } (a registered component with a real renderer, renderers/feedback/toast.tsx) and stop teaching a button/action-payload shape the engine has no dispatcher for; or
  • the surface is wrong — the action-object form is the intended SDUI authoring shape, in which case ButtonSchema.onClick needs to declare the action union (it currently declares only the function) and something needs to execute it.

That is a contract question, not a fixture typo, which is why it was not folded into #6157. Note that #6157 already corrects the toast variant value inside these payloads against ToastSchema — the nearest governing declaration — and that correction stands under either resolution here.

#6157 remains open on its own scope and is not addressed by this card.

Refs: #5250 · #6157 · #6143.

Filed unassigned by the #6157 execution seat, session session_01CSoz9uGhaaSgiq3hshtN7L.

Activity

  1. os-support-ai commented on Aug 26, 2026

    @os-support-ai
    Collaborator

    Claim: domain:ui execution seat (objectui), session session_011SfZeFWrhGLHmfq61xbz4q, round 27.
    Branch: claude/issue-6250-toast-demo-shapes

    Serial constraints — file face: content/docs/components/feedback/toast.mdx and sonner.mdx, content/docs/components/form/button.mdx, and the examples/schema-catalog/src/schemas/components-feedback-sonner/*.json fixtures. Fully disjoint from everything in flight — #6240 (app-shell/src/services), #6378 (app-shell/src/chrome + apps/console), and the two queued PRs.

    ⛔ content/docs/releases/ is not in that face and must not enter it.

    #6249 is deliberately not dispatched alongside this. Triage measured the two as adjacent, not the same (menubar demo vs the toast idiom), and they live in different component directories — but they are the same species of fix on the same kind of surface, and a same-batch collision is invisible to a per-card shadow check. It goes next, not now.

    The charter's fence is the important half. Correct the seven toast demos to registered, working spellings — docs teach only what runs. ⛔ The other route (declare an action union on ButtonSchema.onClick and build a dispatcher) is a capability expansion with zero runtime today; it is the maintainer's call alongside the #6182 / #6124 handler-dialect family, and it is explicitly not foreclosed by fixing the fixtures. If the fix starts to look like it needs a dispatcher, that is the signal to stop and report, not to build one.

    Triage already completed dedup on the filer's behalf (their search channel was rate-limited) — #6249, #6157, #6182 and #6124 were each checked and none covers this idiom. ⛔ No need to redo that sweep; spend the effort on measuring which spellings actually run.


    Generated by Claude Code

  2. os-support-ai commented on Aug 26, 2026

    @os-support-ai
    Collaborator

    os-dev-report

    {
    "issue": 6250,
    "status": "done",
    "branch": "claude/issue-6250-toast-demo-shapes",
    "pr": "#6498",
    "premise_still_valid": true,
    "summary": "Reproduced the card's evidence before fixing: all 14 fixtures in components-feedback-toast/* and components-feedback-sonner/* were a RED ButtonSchema.safeParse on the ENVELOPE (expected function, received object, path ['onClick']), and ActionRunner's runnable vocabulary is script|url|modal|flow|api|form|navigation with no toast and no sonner executor. All 14 now author the registered spellings the engine already executes, type:'toast' and type:'sonner' (14 RED -> 0 RED; shape census of handler-key object payloads across all 430 catalog entries: 14 -> 0). Keys were restricted to those the governing schema declares AND the renderer reads, which is why the sonner demos carry buttonLabel/buttonVariant (SonnerSchema declares both) and the toast demos do not (ToastSchema declares neither, though its renderer reads both). ONE REFINEMENT TO THE CARD'S PREMISE, not a falsification: the 'declared nowhere' half is narrower than stated. @object-ui/types ships EventableSchema.onClick?: UIEventHandler | string (packages/types/src/api-types.ts:278), public in dist/index.d.ts, with no zod mirror, no component schema extending it and no reader - the object dialect IS already declared, as an inert island. Filed as #6497 because it is a direct input to the fenced #6182/#6124 decision. A STRONGER READING OF THE DEFECT: onClick is a member of SDUI_DOM_PASS_THROUGH_KEYS (packages/core/src/utils/dom-props.ts:88), so the action object reached the rendered button's DOM listener slot and React THROWS on click ('Expected onClick listener to be a function, instead got a value of object type.') - the old demos were worse than inert, and that is now pinned as a counter-probe. THREE DEMOS CHANGED EDITORIAL PREMISE because they had no working spelling at all: the toast renderer has no in-toast action button and no dismiss callback, and the sonner renderer has no promise form, so 'With Action', 'With Undo Action' and 'Promise Toast' now demonstrate the info/warning/description/buttonVariant surfaces that do run; curated titles went into catalog-meta.json (the sidecar the generator only reads) so no fixture was renamed and no catalog id changed. THE FENCE HELD: no action union on ButtonSchema.onClick, no dispatcher, no new ActionRunner executor, and not one file under packages/ is in the diff. Clause 2 confirmed rather than assumed, following the re-export chain three ways: no packages/ file changed at all; the one changed entry file (examples/schema-catalog/src/index.ts) has an identical export-statement set before/after with no export * and its one re-export pointing at an untouched types.ts; and the chain terminates at @object-ui/site because the catalog package is private:true and matched by the changesets ignore pattern @object-ui/example-*. PM ADJUDICATION WANTED on the #6347 overlap - see open_questions. Worktree: /home/user/objectui-6250 (removed after reporting).",
    "tests": "All at 076ed3b, exit codes captured before any pipe, each gate quoted from its own verdict line. NEW PIN examples/schema-catalog/test/toast-demo-dispatch-6250.test.tsx - renders each of the 14 demos against a real , clicks, asserts the authored strings land in a toast node identified by NODE IDENTITY (sonner's store is module-global and replays to every newly mounted Toaster - 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. REWRITTEN PIN examples/schema-catalog/test/component-fixture-declared-keys.test.ts - the #6157 pin asserted fixture.type === 'button' and destructured fixture.onClick, i.e. it pinned the exact branch this card removes; measured RED first ('Tests 6 failed | 10 passed (16)'), then replaced rather than reworded, carrying #6157's actual fact (the toast variant value is a declared ToastSchema member, destructive refused) forward against the top-level variant, counter-probe intact, plus a corpus-wide census with a positive control that the walker really descends. REVERSE VERIFICATION - predicted RED, observed RED. Fix committed FIRST, then components-feedback-toast/destructive.json reverted to the retired shape. NO REBUILD IS IN THIS ABLATION'S PATH and that is stated deliberately: the mutated artifact is a source JSON file that vitest reads directly through the catalog's TS entry - no package dist sits between the mutation and the assertion, so the assertion cannot be reading a stale build. The mutation was proved ON DISK before any reading, anchored on the text rather than on an exit code: grep -c '"action": "toast"' = 1 and grep -c '"type": "toast"' = 0 in the mutated file, and git hash-object 8a7145bec != HEAD blob b4a37b1c1. Restore ran through an EXIT INT TERM trap using an ABSOLUTE path and git checkout HEAD -- ; proved byte-identical afterwards (git hash-object == git rev-parse HEAD: = b4a37b1c1, both non-empty; git diff HEAD empty; whole tree clean). Reverting ONE fixture turned SEVEN assertions red across both files: dispatch pin 'Tests 3 failed | 14 passed (17)' (corpus non-vacuity control, retired-shape sweep, and destructive fires its toast on click) and declared-keys pin 'Tests 4 failed | 15 passed (19)' (is a toast node / declares a variant ToastSchema knows / pre-#6157 counter-probe / every handler value in the corpus is a string expression). Both green again after restore: 'Test Files 2 passed (2) Tests 36 passed (36)'. GATES, all exit 0: check:doc-types 'Every documented component type is registered.' (184 docs, 1064 blocks, 895 type literals); check:doc-snippets 'Every covered documentation snippet compiles against the built types.' / 'Semantic phase: 267 of 267 block(s) judged, 0 failed.'; 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 closure'; check-doc-links 'Links are valid across 17 scan roots.'; check:control-bytes 'OK (scanned 5363 tracked text file(s); skipped 85 binary)'; check:entry-guard 'no entry guard outside the baseline'; check:readme-exports 'OK ... 378 self-imports judged (378 real, 0 wrong-path, 0 fabricated)'; 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 --check 'index.ts is up to date (430 entries)' (and it was green on the UNTOUCHED tree first, so the 15/10 diff there is only the five curated entries); check-changeset-presence 'No source of a released package changed in this range, so no changeset is owed' (empty-frontmatter changeset added anyway - objectui has no skip-changeset label, this is the repo's first-class 'declares no release'); check-changeset-no-major; check-changeset-fixed. TESTS: pnpm exec vitest run examples/schema-catalog/ scripts/tests/catalog-index-regenerable-4633.test.ts -> 'Test Files 15 passed (15) Tests 1824 passed (1824)'. TYPECHECK: pnpm --filter @object-ui/example-schema-catalog run type-check -> exit 0, script name echoed ('tsc --noEmit && tsc -p tsconfig.test.json'), so this is not the zero-match-exits-0 trap. TWO NON-ZERO EXITS THAT ARE NOT MEASURED, EXCLUDED FROM EVERY COUNT: check:readme-exports first exited 1 with '3 self-import(s) could not be judged -- @object-ui/plugin-ai ... 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, which is how I know rather than assume. ONE PRE-EXISTING FINDING TURNED RED AND WAS UPDATED, NOT WEAKENED: examples/schema-catalog/test/form-control-dom-leak-5632.test.tsx NODE_CENSUS.button 140 -> 126. That is an exact toEqual census, not a shrink-only baseline, and its own header sanctions exactly 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, which independently corroborates the census. LINT - a MEASURED narrowing, three pieces of evidence: (1) population read from eslint's own config per file, not guessed - 16 of 19 changed files come back 'File ignored because no matching configuration was supplied' (.mdx/.json/.md are outside the configured population, so a green on them would be a non-reading); (2) file count from --format json - the 3 changed .ts/.tsx files report 0 errors 0 warnings, and the bounding package's whole population is 17 files all genuinely linted, 0 errors, 1 warning which is 'ExampleMeta is defined but never used' in the GENERATED src/index.ts, present verbatim on 7975f2d and on a line this diff does not touch; (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.",
    "open_questions": [
    {
    "question": "#6347 is OPEN, pm:queue/domain:ui, unassigned - and this PR lands two of its four measured items. It independently found that sonner.mdx documents duration (SonnerSchema does not declare it, the renderer does not pass it) and that its 'With Action' section renders a fixture that is a button, not a sonner node. Both sit on a page inside THIS card's declared file face, are the same defect class, and had their correct form already pinned by SonnerSchema plus the renderer - so I corrected them here rather than leave the page contradicting the fixtures beside it (the action row went with them, same evidence). Its button-group.mdx half (value, selectionMode, the whole Selection Mode section) is untouched - content/docs/components/basic/button-group.mdx is not in this diff. Does #6347 get re-scoped to its button-group half, or should the sonner rows be backed out of this PR and left to it?",
    "options": [
    "A - keep the sonner rows here and re-scope #6347 to button-group only. The four bounded-in-place conditions all hold (same defect class, correct form pinned by SonnerSchema + the renderer, no other agent holds the file, same doc-gate family), #6161 is the landed precedent for exactly this act on exactly these pages, and #6347 itself proposes the same answer ('documentation follows the shipped types, so the undeclared rows come out'). Cost: #6347 must be edited so a later dev is not dispatched at work already landed.",
    "B - back the sonner Schema-block rows out of this PR and leave all four items to #6347. Cost: the page would keep teaching duration and action right beside the fixtures this PR just corrected, which is the exact contradiction #6157 was filed about ('the demo rendered beside the prose still contradicted it'). It also splits one page across two PRs.",
    "C - keep them here AND leave #6347 fully open. Cost: worst of both - a later dev is dispatched at #6347, finds two of four items already done, and burns a cycle re-measuring."
    ],
    "recommendation": "A, because the overlap is on a page this card already owns and the correction was not a judgement call: SonnerSchema plus the renderer determine the answer, #6347 reached the same answer independently, and #6161 already landed that act on these pages. B is the option that leaves a known-false page shipping. The only work A creates is an edit to #6347's scope, which is a triage action rather than a dev cycle - and that is why it is a question for you rather than something I did to another seat's card."
    },
    {
    "question": "Three demos changed editorial premise ('With Action', 'With Undo Action', 'Promise Toast'), because the toast renderer has no in-toast action button or dismiss callback and the sonner renderer has no promise form - so there is no working spelling of what those headings promised. I re-pointed them at the info/warning/description/buttonVariant surfaces that do run and gave them curated titles. That is a larger act than a fixture typo, so it should be ratified rather than assumed.",
    "options": [
    "A - accept as landed: the charter says docs teach only what runs, and a demo whose premise has no runtime cannot be corrected into a working demo of that premise.",
    "B - delete those three demos and their sections outright instead of re-pointing them.",
    "C - hold the three demos unchanged pending the #6496 capability decision, landing only the other eleven."
    ],
    "recommendation": "A. B removes docs surface that #6496 may well restore, and re-pointing keeps the page a complete tour of the five declared toast variants, which is strictly more than it taught before. C is the one option that leaves fixtures on the page that still parse RED and dispatch nowhere - the exact state this card exists to end - so it fails the card's own acceptance criterion."
    }
    ],
    "out_of_scope_findings": [
    "filed as #6494: the toaster demos and toaster.mdx teach a provider key that ToasterSchema (feedback.ts:156, zod mirror :82) does not declare and the renderer (toaster.tsx, reads only :43 position and :44 limit) does not read - so the two 'provider' demos render byte-identically. Same defect class as this card, different shape, and the page makes a capability claim ('ObjectUI supports two toast providers') while packages/components/src/ui/toast.tsx does ship an unrouted Radix primitive - a contract question, not a typo.",
    "filed as #6496: ToastSchema and the toast renderer disagree in BOTH directions - buttonLabel/buttonVariant are read (toast.tsx:30-31) but declared by no type, with buttonLabel offered as a designer input (:49) the shipped type lacks and buttonVariant named by nothing at all; action (feedback.ts:143) and onDismiss (:150) are declared but read by no renderer. SonnerSchema declares both trigger keys, ToastSchema declares neither. This is what constrained two decisions in this PR, and the two halves need opposite resolutions (declare-what-runs vs enforce-or-remove).",
    "filed as #6497: the action-object handler dialect is ALREADY declared and exported - EventableSchema.onClick?: UIEventHandler | string (api-types.ts:278, UIEventHandler at :159, both re-exported at index.ts:695-696 and present in dist/index.d.ts) - with zero zod mirror, zero component schemas extending it and zero readers. It refines this card's 'declared nowhere' and is a direct input to the fenced #6182/#6124 decision: the declaration half is not a blank page, so the live options are wire-it / retire-it / leave-a-shipped-invitation-to-the-shape-we-just-removed.",
    "NOT filed, deliberately: components-feedback-toaster/with-toast-trigger.json still carries children[0].onClick as the STRING "toast(\"Hello from ObjectUI!\")". That is the handler-EXPRESSION dialect, which triage already identified as #6182's open decision - a different shape from this card's action object. The corpus sweep pins it as its own positive control rather than banning it, so this PR does not answer #6182 by accident.",
    "NOT filed, out of scope and out of face: docs/audits/2026-08-zod-to-json-schema-fidelity.md:317 counts 'the 14' in these two directories and will read stale after this lands. Left as written - it is a dated audit snapshot, and it independently corroborates this card's census from a different instrument."
    ]
    }


    Generated by Claude Code


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentationdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions