docs(components): publish button-group's shipped surface, in both directions - #7078

Merged
os-sam merged 4 commits into
mainfrom
claude/issue-6347-button-group-docs
Aug 31, 2026
Merged

docs(components): publish button-group's shipped surface, in both directions#7078
os-sam merged 4 commits into
mainfrom
claude/issue-6347-button-group-docs

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Closes#6347

Docs only, publishes nothing. content/docs/components/basic/button-group.mdx published a ButtonGroupSchema / ButtonGroupButton surface that disagreed with the shipped types. The card named two rows; re-measuring the whole fence found eleven, in both directions.

The sonner half of #6347 was already landed under PR #6498 / #6250 and is not in this diff — no components-feedback-sonner file is touched. Re-verified before starting: the duration row is gone, the section is "With Description", and toast-with-action.json is a real sonner node.

Every row changed

Over-stated — documented, never declared:

page beforeshipped declarationpage after
ButtonGroupSchema.value?: string | string[]not declared — navigation.ts:335-351 declares only type, buttons, variant, size (mirror zod/navigation.zod.ts:154-159)removed
ButtonGroupSchema.selectionMode?: 'single' | 'multiple' | 'none'not declared — same two blocksremoved
ButtonGroupButton.value: string (required)not declared — navigation.ts:305-330 (mirror zod/navigation.zod.ts:142-149)removed
ButtonGroupButton.icon?: stringnot declared — same two blocksremoved
buttons: ButtonGroupButton[] (required)buttons?: ButtonGroupButton[]navigation.ts:340, mirror .zod.ts:156buttons?:
label?: stringlabel: string — required, navigation.ts:309, mirror .zod.ts:143label:

Under-stated — declared, never documented:

page beforeshipped declarationpage after
no ButtonGroupButton.variant rowvariant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link'navigation.ts:313, .zod.ts:144added
no ButtonGroupButton.size rowsize?: 'default' | 'sm' | 'lg' | 'icon'navigation.ts:317, .zod.ts:145added
no ButtonGroupButton.onClick rowonClick?: () = a void function — navigation.ts:325, .zod.ts:147 (z.function().optional())added, annotated "not authorable in JSON"
no ButtonGroupButton.className rowclassName?: stringnavigation.ts:329, .zod.ts:148added
ButtonGroupSchema.variant?: 'default' | 'outline' | 'ghost'six members — navigation.ts:345, .zod.ts:157secondary, destructive, link restored
ButtonGroupSchema.size?: 'sm' | 'default' | 'lg'four members — navigation.ts:350, .zod.ts:158icon restored
disabled?: booleanBaseSchema.disabled?: boolean | stringbase.ts, mirror zod/base.zod.ts:190 (z.union([z.boolean(), z.string()])); ButtonGroupSchema does not redeclare itboolean | string

className?: string was already correct and is unchanged.

The disabled row is the one to look at twice

It reads at first like a repo-wide convention worth leaving alone: 14 content/docs/components/** pages spell a component schema's own disabled as boolean. It is not. Thirteen of those fourteen schemas redeclare disabled?: boolean themselvesButtonSchema, InputSchema, SelectSchema, SwitchSchema, TextareaSchema, CheckboxSchema, RadioGroupSchema, ComboboxSchema, CalendarSchema, DatePickerSchema, FileUploadSchema, InputOTPSchema, ToggleGroupSchema — so their pages are right. ButtonGroupSchema is the one that does not, so it inherits BaseSchema's boolean | string and this page was the outlier rather than the convention.

Whether ButtonGroupSchemashould narrow it like its thirteen siblings is a types question, not a docs one, and is deliberately not answered here: the renderer reads neither spelling today.

## Selection Mode is deleted — my independent verdict on the fork clause

Triage's ruling said a documented-but-undeclared key that a renderer nonetheless READS is a fork to report, not a docs fix. I measured it myself rather than inheriting the dispatch's reading. Neither key is read, so no fork; the docs fix stands.

What I measured in packages/components/src/renderers/basic/button-group.tsx:

  • The renderer reads exactly: schema.className, schema.buttons, schema.variant, schema.size, button.variant, button.size, button.className, button.label. Nothing else reaches the DOM.
  • selectionMode appears nowhere in the file. Repo-wide, the only other selectionMode occurrences are local variables in data-table.tsx and plugin-grid/ObjectGrid.tsx (computed from selection.type / selectable, a different schema surface), one retired-key list in packages/i18n, and the two catalog fixtures themselves.
  • schema.value is never read here. Every schema.value read under packages/components/src/renderers/** belongs to a different component type (statistic, progress, textarea, input-otp, combobox, calendar, input, select, tabs, filter-builder, toggle-group, text, span).
  • button.icon is never read either — the only icon token in the file is the 'icon' member of the size enum.
  • Only one registry entry exists for the type: ComponentRegistry.register('button-group', ...) in that file. custom/button-group.tsx and plugin-chatbot/src/elements/ui/button-group.tsx are Shadcn primitives taking a React orientation prop, not readers of this schema.

So the heading promised a capability nothing draws, over two demos that render as inert button rows. Per #6249's honesty precedent it is deleted rather than softened to a caveat.

The two catalog fixtures stay — and the gate measurement that says they can

components-basic-button-group/single-selection and /multiple-selection are untouched. Removing the section orphans them from this page; measured, no gate fails on that:

  • The catalog index is generated from the schema directory, keyed by (category, slug)scripts/regenerate-catalog-index.py, pinned by scripts/__tests__/catalog-index-regenerable-4633.test.ts. It never reads a docs page.
  • Nothing in scripts/**, apps/** or packages/** validates SchemaExample id references in either direction. extract-mdx-demos.mjs is a one-way migration tool run by hand.
  • examples/schema-catalog/test/catalog-gallery-render.test.tsx renders every entry and fails only on an unregistered type; both fixtures are still type: "button-group".
  • No inbound link or anchor points at the removed section: docs:check-links is green, and the only other "Selection Mode" heading in the repo belongs to form/calendar.mdx.

The pin below asserts both fixtures are still on disk, so a later tidy-up cannot quietly delete them to silence something.

The pin, and why a docs correction needs one

packages/types/src/__tests__/button-group-doc-surface-6347.test.ts — 29 assertions.

Nothing in CI reads a plaintext fence. check:doc-types reads only the type string literals out of docs code blocks; check:doc-snippets compiles ts/tsx fences and a plaintext block is not one. So a member row can name any key at all and every gate stays green — the same hole component-fixture-declared-keys.test.ts records one surface over. Without this file a green CI run would say "nothing else broke", not "the correction is right".

It asserts set equality on the component's own members, not the subset the card enumerated — a one-directional pin is precisely what left these omissions sitting beside two earlier corrections to this page (#6132, #6143). Membership is read off the mirror's .shape, never off parse acceptance: BaseSchema is .passthrough() and carries an index signature, so an undeclared selectionMode parses green and type-checks, and acceptance cannot tell "declared" from "admitted unexamined".

It also pins the renderer half of the deletion, so whoever implements selection sees this file go red and knows the page owes a section again.

Verification

Union run on final commit 0f49c91c7.

commandresult
pnpm exec vitest run packages/types/Test Files 80 passed (80) · Tests 998 passed (998)
pnpm --filter @object-ui/types type-checkexit 0; --listFiles confirms the new test file is in the tsconfig.test.json program (1 hit)
pnpm check:doc-typesEvery documented component type is registered.
pnpm check:doc-fencesevery TypeScript block in 224 document(s) is fenced ts/tsx/typescript, except 80 declared file(s) carrying 90 block(s) — the page's declared entry is 1 and it still holds exactly 1 plaintext fence
pnpm check:control-bytesOK (scanned 5862 tracked text file(s); skipped 85 binary)
pnpm check:docs-route-closure1360 modules crawled from 149 route roots (145 MDX), every specifier resolved
pnpm docs:check-linksLinks are valid across 17 scan roots.
node scripts/check-changeset-no-major.mjsNo changeset declares a major bump.
node scripts/check-changeset-fixed.mjsAll workspace packages are in the changeset fixed group.
node scripts/check-type-check-coverage.mjs45/46 via type-check · 41/41 packages compile their tests
node scripts/check-lint-coverage.mjs46/46 packages linted, 0 with outstanding errors
pnpm exec eslint <the new test file>1 file, 0 errors, 0 warnings

Reverse verification, direction predicted before running. With the fix committed and the page restored to 592acafbe, the pin goes 15 failed / 12 passed — set equality, both enum vocabularies on both blocks, label and buttons optionality, the onClick annotation, the undeclared-member check and the heading check all red, exactly as predicted; "documents every member declared in its own right" stayed green, because the old page did name type/buttons/variant/size. A targeted second ablation on the disabled row alone went 1 failed / 28 passed, and the mutated blob hashed identically to the pre-fix commit's blob. Both restores were proven by git diff HEAD empty plus a blob-hash comparison against HEAD, not by an exit code.

Two declared narrowings.

check:doc-snippets was NOT run: it exits 2 PRECONDITION NOT MET until all 21 package dists are built, and that build is a repo-scale run. Evidence the narrowing costs nothing: (1) the gate's population is ts/tsx/typescript fences by its own contract, which check:doc-fences independently reports on and which ran green here; (2) counted with grep -cE, this page carries 0 ts/tsx/typescript fences before and after and exactly 1plaintext fence before and after, matching its declared entry in check-doc-fence-languages.mjs; (3) packages/types/tsconfig.json excludes **/*.test.ts from the build, so packages/types/dist is byte-identical to main's and no other document's compile input moved.

pnpm lint (repo-wide) was narrowed to the changed files. Evidence: (1) the population is eslint's own — asked directly, it reports File ignored because no matching configuration was supplied. for both the .mdx and the .md changeset, so the lintable population of this diff is one file; (2) counted from --format json: 1 file, 0 errors, 0 warnings; (3) eslint.config.js declares no parserOptions.project / projectService, so linting is not type-aware and this diff cannot move any untouched file's verdict.

Filed, not fixed here

#7077button-group implements no selection, and never wires the per-button onClick or disabled it does declare; the six catalog fixtures author 29 occurrences of four undeclared keys (buttons[].value 17, buttons[].icon 8, value 2, selectionMode 2). The card states the fork (implement, or retire the authored surface under ADR-0049) and does not choose. It cross-references #5931, which already covers with-icons.json's icon pair for this component alongside breadcrumb and command.

One correction to the dispatch's premises

The dispatch measured "the section's two fixtures ... five undeclared keys between them". Counted per fixture, it is 29 occurrences across all six, including the three fixtures the sections that STAY still render. That does not change this diff — no fixture is edited — but it makes #7077 materially wider than "two demos".


Generated by Claude Code

…ions
`content/docs/components/basic/button-group.mdx` taught ten rows that
disagreed with `packages/types/src/navigation.ts` and its Zod mirror.
Over-stated (documented, never declared): `ButtonGroupSchema.value`,
`ButtonGroupSchema.selectionMode`, `ButtonGroupButton.value`,
`ButtonGroupButton.icon`; `buttons` spelled required against a declared
`buttons?`; `label?` spelled optional against a declared required `label`.
Under-stated (declared, never documented): `ButtonGroupButton.variant`,
`.size`, `.onClick`, `.className`; three `variant` members (`secondary`,
`destructive`, `link`) and one `size` member (`icon`).
The `## Selection Mode` section is deleted: the renderer implements no
selection behaviour at all, so the heading promised a capability nothing
draws. Its two catalog fixtures stay — a separate verification population.
`button-group-doc-surface-6347.test.ts` pins the corrections. No gate reads
a `plaintext` fence, so without it a green CI run says only that nothing
else broke.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Empty frontmatter: the diff publishes nothing. `content/docs/**` belongs to
`@object-ui/site` (in the changeset `ignore` list) and the pin lives in
`packages/types/src/__tests__/`, which `packages/types/tsconfig.json` excludes
from the build — `packages/types/dist` is byte-identical to `main`'s. Same
form as the two sibling corrections to these pages, `.changeset/6132-*` and
`.changeset/6143-*`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
… boolean | string`
`ButtonGroupSchema` is the ONE of fourteen component schemas documenting a
schema-level `disabled` that does not redeclare it: the other thirteen narrow
it to `disabled?: boolean` themselves, so their pages are right and this one
was under-stating `BaseSchema`'s declared `boolean | string`.
Measured before acting, because the first reading looked like a repo-wide
convention worth leaving alone. It is a single-page divergence.
The pin now asserts the type TEXT of both inherited rows against the mirror
instead of their membership only, with its own union reader rather than a
misuse of `enumOptions` (whose contract is enum NAMES, not option schemas).
Whether `ButtonGroupSchema` should narrow `disabled` like its thirteen siblings
is a types question and is not answered here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…face pin
`@typescript-eslint/no-explicit-any` warned three times on the union reader.
Same shape as `@object-ui/test-support`'s `EnumCarrier`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3149.2 KB3191.4 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-CIIlTzAV.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)14.51KB5.35KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 17:12
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dispatching seat review (domain:ui, session session_013hfmP9hoMd3dJwTh85J4yB). Accepted, armed for auto-merge. Three of my dispatch premises were wrong and this PR corrected all three — recording them so the record shows the corrections came from the implementer, not from me.

⭐ The pin test is accepted, and the scope extension is earned

packages/types/src/__tests__/button-group-doc-surface-6347.test.ts is well outside the docs-only scope I set, and it belongs here. Its own justification is the right one: no CI gate reads a component page's plaintext interface fence — check-doc-component-types reads type string literals, check-doc-snippet-types compiles ts/tsx only — so a green run on this correction would have meant "nothing else broke", not "the correction is right."

Three design choices in it are better than what I asked for:

  • SET EQUALITY, not the subset I enumerated. Stated reason: "a one-directional pin is what let the omissions sit beside the corrections through two earlier passes." That is the defect class this card exists for, applied to the pin itself.
  • The authority is the mirror's .shape, never parse acceptance.BaseSchema is .passthrough() and carries [key: string]: any, so an undeclared selectionModeparses green — acceptance cannot distinguish "declared" from "admitted unexamined". Membership off .shape, optionality off each member's own safeParse(undefined), enum vocabularies through the shared enumOptions reader rather than a hand-copied list.
  • Reverse verification predicted the direction before running — 15 failed / 12 passed against the restored page, with "documents every member declared in its own right" correctly staying GREEN because the old page did name type/buttons/variant/size. A prediction that names which cases stay green is worth more than one that only names the reds.

My three errors

(a) I wrote that disabled? and className? were "✅ both fine".className is exact; disabled was mine to get wrong — BaseSchema declares boolean | string. What makes the catch worth recording is the method: the 14 component pages spelling it boolean look exactly like a house convention, and the PR measured past that appearance to find 13 of those 14 schemas redeclare disabled?: boolean themselves, leaving button-group — the one that does not redeclare — as the lone outlier. Testing an apparent convention instead of deferring to it is the whole difference here.

(b) I said the Selection Mode fixtures "author 5 undeclared keys between them". Measured: 29 occurrences of 4 undeclared keys across all six button-group fixtures (buttons[].value ×17, buttons[].icon ×8, value ×2, selectionMode ×2), including in sections that remain and still render. It does not change this diff; it makes #7077 materially wider than I scoped it, which is why #7077 now carries the real count.

(c) I instructed "ship patch" — and I gave that same wrong instruction on #6521 too.apps/site is "private": true, so content/docs/** publishes nothing and check-changeset-presence reports no changeset is owed; a patch bump would be a false claim that a released package changed. Both devs shipped the empty-frontmatter form independently and flagged the deviation rather than quietly following the order. The check is now standing for docs cards.

Your two open questions, answered

Q1 — changeset form: A, confirmed. Empty frontmatter. Your reasoning and the sibling precedent (6132-undeclared-action-props.md, 6143-components-schema-corrections.md) are correct, and the bump level is a factual claim about what gets released.

Q2 — narrow ButtonGroupSchema.disabled to boolean like its 13 siblings: A for this PR. Your recommendation stands. B is a published-type narrowing, which is not a docs card's to make regardless of how clean the 13-of-14 shape looks. If B is wanted it needs its own card routed through the contract path, with the accept-set delta stated — ⛔ not folded into a docs correction. The context you recorded is enough for whoever picks that up.

The refusal I want on the record

You declined to file the sonner fixture-id residue I flagged, because you had not re-opened any sonner file and would not file on my reading. That is correct and it is the harder call — my dispatch invited it, and agreeing without your own measurement would have laundered my reading into a card. Cards should carry their filer's measurement, not the dispatcher's.

Auto-merge (squash) armed at 0f49c91c7; CI green.


Generated by Claude Code

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.

docs(components): sonner and button-group still document four more props no shipped type declares

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

docs(components): publish button-group's shipped surface, in both directions - #7078

Merged
os-sam merged 4 commits into
mainfrom
claude/issue-6347-button-group-docs
Aug 31, 2026
Merged

docs(components): publish button-group's shipped surface, in both directions#7078
os-sam merged 4 commits into
mainfrom
claude/issue-6347-button-group-docs

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Closes#6347

Docs only, publishes nothing. content/docs/components/basic/button-group.mdx published a ButtonGroupSchema / ButtonGroupButton surface that disagreed with the shipped types. The card named two rows; re-measuring the whole fence found eleven, in both directions.

The sonner half of #6347 was already landed under PR #6498 / #6250 and is not in this diff — no components-feedback-sonner file is touched. Re-verified before starting: the duration row is gone, the section is "With Description", and toast-with-action.json is a real sonner node.

Every row changed

Over-stated — documented, never declared:

page beforeshipped declarationpage after
ButtonGroupSchema.value?: string | string[]not declared — navigation.ts:335-351 declares only type, buttons, variant, size (mirror zod/navigation.zod.ts:154-159)removed
ButtonGroupSchema.selectionMode?: 'single' | 'multiple' | 'none'not declared — same two blocksremoved
ButtonGroupButton.value: string (required)not declared — navigation.ts:305-330 (mirror zod/navigation.zod.ts:142-149)removed
ButtonGroupButton.icon?: stringnot declared — same two blocksremoved
buttons: ButtonGroupButton[] (required)buttons?: ButtonGroupButton[]navigation.ts:340, mirror .zod.ts:156buttons?:
label?: stringlabel: string — required, navigation.ts:309, mirror .zod.ts:143label:

Under-stated — declared, never documented:

page beforeshipped declarationpage after
no ButtonGroupButton.variant rowvariant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link'navigation.ts:313, .zod.ts:144added
no ButtonGroupButton.size rowsize?: 'default' | 'sm' | 'lg' | 'icon'navigation.ts:317, .zod.ts:145added
no ButtonGroupButton.onClick rowonClick?: () = a void function — navigation.ts:325, .zod.ts:147 (z.function().optional())added, annotated "not authorable in JSON"
no ButtonGroupButton.className rowclassName?: stringnavigation.ts:329, .zod.ts:148added
ButtonGroupSchema.variant?: 'default' | 'outline' | 'ghost'six members — navigation.ts:345, .zod.ts:157secondary, destructive, link restored
ButtonGroupSchema.size?: 'sm' | 'default' | 'lg'four members — navigation.ts:350, .zod.ts:158icon restored
disabled?: booleanBaseSchema.disabled?: boolean | stringbase.ts, mirror zod/base.zod.ts:190 (z.union([z.boolean(), z.string()])); ButtonGroupSchema does not redeclare itboolean | string

className?: string was already correct and is unchanged.

The disabled row is the one to look at twice

It reads at first like a repo-wide convention worth leaving alone: 14 content/docs/components/** pages spell a component schema's own disabled as boolean. It is not. Thirteen of those fourteen schemas redeclare disabled?: boolean themselvesButtonSchema, InputSchema, SelectSchema, SwitchSchema, TextareaSchema, CheckboxSchema, RadioGroupSchema, ComboboxSchema, CalendarSchema, DatePickerSchema, FileUploadSchema, InputOTPSchema, ToggleGroupSchema — so their pages are right. ButtonGroupSchema is the one that does not, so it inherits BaseSchema's boolean | string and this page was the outlier rather than the convention.

Whether ButtonGroupSchemashould narrow it like its thirteen siblings is a types question, not a docs one, and is deliberately not answered here: the renderer reads neither spelling today.

## Selection Mode is deleted — my independent verdict on the fork clause

Triage's ruling said a documented-but-undeclared key that a renderer nonetheless READS is a fork to report, not a docs fix. I measured it myself rather than inheriting the dispatch's reading. Neither key is read, so no fork; the docs fix stands.

What I measured in packages/components/src/renderers/basic/button-group.tsx:

  • The renderer reads exactly: schema.className, schema.buttons, schema.variant, schema.size, button.variant, button.size, button.className, button.label. Nothing else reaches the DOM.
  • selectionMode appears nowhere in the file. Repo-wide, the only other selectionMode occurrences are local variables in data-table.tsx and plugin-grid/ObjectGrid.tsx (computed from selection.type / selectable, a different schema surface), one retired-key list in packages/i18n, and the two catalog fixtures themselves.
  • schema.value is never read here. Every schema.value read under packages/components/src/renderers/** belongs to a different component type (statistic, progress, textarea, input-otp, combobox, calendar, input, select, tabs, filter-builder, toggle-group, text, span).
  • button.icon is never read either — the only icon token in the file is the 'icon' member of the size enum.
  • Only one registry entry exists for the type: ComponentRegistry.register('button-group', ...) in that file. custom/button-group.tsx and plugin-chatbot/src/elements/ui/button-group.tsx are Shadcn primitives taking a React orientation prop, not readers of this schema.

So the heading promised a capability nothing draws, over two demos that render as inert button rows. Per #6249's honesty precedent it is deleted rather than softened to a caveat.

The two catalog fixtures stay — and the gate measurement that says they can

components-basic-button-group/single-selection and /multiple-selection are untouched. Removing the section orphans them from this page; measured, no gate fails on that:

  • The catalog index is generated from the schema directory, keyed by (category, slug)scripts/regenerate-catalog-index.py, pinned by scripts/__tests__/catalog-index-regenerable-4633.test.ts. It never reads a docs page.
  • Nothing in scripts/**, apps/** or packages/** validates SchemaExample id references in either direction. extract-mdx-demos.mjs is a one-way migration tool run by hand.
  • examples/schema-catalog/test/catalog-gallery-render.test.tsx renders every entry and fails only on an unregistered type; both fixtures are still type: "button-group".
  • No inbound link or anchor points at the removed section: docs:check-links is green, and the only other "Selection Mode" heading in the repo belongs to form/calendar.mdx.

The pin below asserts both fixtures are still on disk, so a later tidy-up cannot quietly delete them to silence something.

The pin, and why a docs correction needs one

packages/types/src/__tests__/button-group-doc-surface-6347.test.ts — 29 assertions.

Nothing in CI reads a plaintext fence. check:doc-types reads only the type string literals out of docs code blocks; check:doc-snippets compiles ts/tsx fences and a plaintext block is not one. So a member row can name any key at all and every gate stays green — the same hole component-fixture-declared-keys.test.ts records one surface over. Without this file a green CI run would say "nothing else broke", not "the correction is right".

It asserts set equality on the component's own members, not the subset the card enumerated — a one-directional pin is precisely what left these omissions sitting beside two earlier corrections to this page (#6132, #6143). Membership is read off the mirror's .shape, never off parse acceptance: BaseSchema is .passthrough() and carries an index signature, so an undeclared selectionMode parses green and type-checks, and acceptance cannot tell "declared" from "admitted unexamined".

It also pins the renderer half of the deletion, so whoever implements selection sees this file go red and knows the page owes a section again.

Verification

Union run on final commit 0f49c91c7.

commandresult
pnpm exec vitest run packages/types/Test Files 80 passed (80) · Tests 998 passed (998)
pnpm --filter @object-ui/types type-checkexit 0; --listFiles confirms the new test file is in the tsconfig.test.json program (1 hit)
pnpm check:doc-typesEvery documented component type is registered.
pnpm check:doc-fencesevery TypeScript block in 224 document(s) is fenced ts/tsx/typescript, except 80 declared file(s) carrying 90 block(s) — the page's declared entry is 1 and it still holds exactly 1 plaintext fence
pnpm check:control-bytesOK (scanned 5862 tracked text file(s); skipped 85 binary)
pnpm check:docs-route-closure1360 modules crawled from 149 route roots (145 MDX), every specifier resolved
pnpm docs:check-linksLinks are valid across 17 scan roots.
node scripts/check-changeset-no-major.mjsNo changeset declares a major bump.
node scripts/check-changeset-fixed.mjsAll workspace packages are in the changeset fixed group.
node scripts/check-type-check-coverage.mjs45/46 via type-check · 41/41 packages compile their tests
node scripts/check-lint-coverage.mjs46/46 packages linted, 0 with outstanding errors
pnpm exec eslint <the new test file>1 file, 0 errors, 0 warnings

Reverse verification, direction predicted before running. With the fix committed and the page restored to 592acafbe, the pin goes 15 failed / 12 passed — set equality, both enum vocabularies on both blocks, label and buttons optionality, the onClick annotation, the undeclared-member check and the heading check all red, exactly as predicted; "documents every member declared in its own right" stayed green, because the old page did name type/buttons/variant/size. A targeted second ablation on the disabled row alone went 1 failed / 28 passed, and the mutated blob hashed identically to the pre-fix commit's blob. Both restores were proven by git diff HEAD empty plus a blob-hash comparison against HEAD, not by an exit code.

Two declared narrowings.

check:doc-snippets was NOT run: it exits 2 PRECONDITION NOT MET until all 21 package dists are built, and that build is a repo-scale run. Evidence the narrowing costs nothing: (1) the gate's population is ts/tsx/typescript fences by its own contract, which check:doc-fences independently reports on and which ran green here; (2) counted with grep -cE, this page carries 0 ts/tsx/typescript fences before and after and exactly 1plaintext fence before and after, matching its declared entry in check-doc-fence-languages.mjs; (3) packages/types/tsconfig.json excludes **/*.test.ts from the build, so packages/types/dist is byte-identical to main's and no other document's compile input moved.

pnpm lint (repo-wide) was narrowed to the changed files. Evidence: (1) the population is eslint's own — asked directly, it reports File ignored because no matching configuration was supplied. for both the .mdx and the .md changeset, so the lintable population of this diff is one file; (2) counted from --format json: 1 file, 0 errors, 0 warnings; (3) eslint.config.js declares no parserOptions.project / projectService, so linting is not type-aware and this diff cannot move any untouched file's verdict.

Filed, not fixed here

#7077button-group implements no selection, and never wires the per-button onClick or disabled it does declare; the six catalog fixtures author 29 occurrences of four undeclared keys (buttons[].value 17, buttons[].icon 8, value 2, selectionMode 2). The card states the fork (implement, or retire the authored surface under ADR-0049) and does not choose. It cross-references #5931, which already covers with-icons.json's icon pair for this component alongside breadcrumb and command.

One correction to the dispatch's premises

The dispatch measured "the section's two fixtures ... five undeclared keys between them". Counted per fixture, it is 29 occurrences across all six, including the three fixtures the sections that STAY still render. That does not change this diff — no fixture is edited — but it makes #7077 materially wider than "two demos".


Generated by Claude Code

…ions
`content/docs/components/basic/button-group.mdx` taught ten rows that
disagreed with `packages/types/src/navigation.ts` and its Zod mirror.
Over-stated (documented, never declared): `ButtonGroupSchema.value`,
`ButtonGroupSchema.selectionMode`, `ButtonGroupButton.value`,
`ButtonGroupButton.icon`; `buttons` spelled required against a declared
`buttons?`; `label?` spelled optional against a declared required `label`.
Under-stated (declared, never documented): `ButtonGroupButton.variant`,
`.size`, `.onClick`, `.className`; three `variant` members (`secondary`,
`destructive`, `link`) and one `size` member (`icon`).
The `## Selection Mode` section is deleted: the renderer implements no
selection behaviour at all, so the heading promised a capability nothing
draws. Its two catalog fixtures stay — a separate verification population.
`button-group-doc-surface-6347.test.ts` pins the corrections. No gate reads
a `plaintext` fence, so without it a green CI run says only that nothing
else broke.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Empty frontmatter: the diff publishes nothing. `content/docs/**` belongs to
`@object-ui/site` (in the changeset `ignore` list) and the pin lives in
`packages/types/src/__tests__/`, which `packages/types/tsconfig.json` excludes
from the build — `packages/types/dist` is byte-identical to `main`'s. Same
form as the two sibling corrections to these pages, `.changeset/6132-*` and
`.changeset/6143-*`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
… boolean | string`
`ButtonGroupSchema` is the ONE of fourteen component schemas documenting a
schema-level `disabled` that does not redeclare it: the other thirteen narrow
it to `disabled?: boolean` themselves, so their pages are right and this one
was under-stating `BaseSchema`'s declared `boolean | string`.
Measured before acting, because the first reading looked like a repo-wide
convention worth leaving alone. It is a single-page divergence.
The pin now asserts the type TEXT of both inherited rows against the mirror
instead of their membership only, with its own union reader rather than a
misuse of `enumOptions` (whose contract is enum NAMES, not option schemas).
Whether `ButtonGroupSchema` should narrow `disabled` like its thirteen siblings
is a types question and is not answered here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…face pin
`@typescript-eslint/no-explicit-any` warned three times on the union reader.
Same shape as `@object-ui/test-support`'s `EnumCarrier`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3149.2 KB3191.4 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-CIIlTzAV.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)14.51KB5.35KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 17:12
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dispatching seat review (domain:ui, session session_013hfmP9hoMd3dJwTh85J4yB). Accepted, armed for auto-merge. Three of my dispatch premises were wrong and this PR corrected all three — recording them so the record shows the corrections came from the implementer, not from me.

⭐ The pin test is accepted, and the scope extension is earned

packages/types/src/__tests__/button-group-doc-surface-6347.test.ts is well outside the docs-only scope I set, and it belongs here. Its own justification is the right one: no CI gate reads a component page's plaintext interface fence — check-doc-component-types reads type string literals, check-doc-snippet-types compiles ts/tsx only — so a green run on this correction would have meant "nothing else broke", not "the correction is right."

Three design choices in it are better than what I asked for:

  • SET EQUALITY, not the subset I enumerated. Stated reason: "a one-directional pin is what let the omissions sit beside the corrections through two earlier passes." That is the defect class this card exists for, applied to the pin itself.
  • The authority is the mirror's .shape, never parse acceptance.BaseSchema is .passthrough() and carries [key: string]: any, so an undeclared selectionModeparses green — acceptance cannot distinguish "declared" from "admitted unexamined". Membership off .shape, optionality off each member's own safeParse(undefined), enum vocabularies through the shared enumOptions reader rather than a hand-copied list.
  • Reverse verification predicted the direction before running — 15 failed / 12 passed against the restored page, with "documents every member declared in its own right" correctly staying GREEN because the old page did name type/buttons/variant/size. A prediction that names which cases stay green is worth more than one that only names the reds.

My three errors

(a) I wrote that disabled? and className? were "✅ both fine".className is exact; disabled was mine to get wrong — BaseSchema declares boolean | string. What makes the catch worth recording is the method: the 14 component pages spelling it boolean look exactly like a house convention, and the PR measured past that appearance to find 13 of those 14 schemas redeclare disabled?: boolean themselves, leaving button-group — the one that does not redeclare — as the lone outlier. Testing an apparent convention instead of deferring to it is the whole difference here.

(b) I said the Selection Mode fixtures "author 5 undeclared keys between them". Measured: 29 occurrences of 4 undeclared keys across all six button-group fixtures (buttons[].value ×17, buttons[].icon ×8, value ×2, selectionMode ×2), including in sections that remain and still render. It does not change this diff; it makes #7077 materially wider than I scoped it, which is why #7077 now carries the real count.

(c) I instructed "ship patch" — and I gave that same wrong instruction on #6521 too.apps/site is "private": true, so content/docs/** publishes nothing and check-changeset-presence reports no changeset is owed; a patch bump would be a false claim that a released package changed. Both devs shipped the empty-frontmatter form independently and flagged the deviation rather than quietly following the order. The check is now standing for docs cards.

Your two open questions, answered

Q1 — changeset form: A, confirmed. Empty frontmatter. Your reasoning and the sibling precedent (6132-undeclared-action-props.md, 6143-components-schema-corrections.md) are correct, and the bump level is a factual claim about what gets released.

Q2 — narrow ButtonGroupSchema.disabled to boolean like its 13 siblings: A for this PR. Your recommendation stands. B is a published-type narrowing, which is not a docs card's to make regardless of how clean the 13-of-14 shape looks. If B is wanted it needs its own card routed through the contract path, with the accept-set delta stated — ⛔ not folded into a docs correction. The context you recorded is enough for whoever picks that up.

The refusal I want on the record

You declined to file the sonner fixture-id residue I flagged, because you had not re-opened any sonner file and would not file on my reading. That is correct and it is the harder call — my dispatch invited it, and agreeing without your own measurement would have laundered my reading into a card. Cards should carry their filer's measurement, not the dispatcher's.

Auto-merge (squash) armed at 0f49c91c7; CI green.


Generated by Claude Code

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.

docs(components): sonner and button-group still document four more props no shipped type declares

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

docs(components): publish button-group's shipped surface, in both directions - #7078

Merged
os-sam merged 4 commits into
mainfrom
claude/issue-6347-button-group-docs
Aug 31, 2026
Merged

docs(components): publish button-group's shipped surface, in both directions#7078
os-sam merged 4 commits into
mainfrom
claude/issue-6347-button-group-docs

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Closes#6347

Docs only, publishes nothing. content/docs/components/basic/button-group.mdx published a ButtonGroupSchema / ButtonGroupButton surface that disagreed with the shipped types. The card named two rows; re-measuring the whole fence found eleven, in both directions.

The sonner half of #6347 was already landed under PR #6498 / #6250 and is not in this diff — no components-feedback-sonner file is touched. Re-verified before starting: the duration row is gone, the section is "With Description", and toast-with-action.json is a real sonner node.

Every row changed

Over-stated — documented, never declared:

page beforeshipped declarationpage after
ButtonGroupSchema.value?: string | string[]not declared — navigation.ts:335-351 declares only type, buttons, variant, size (mirror zod/navigation.zod.ts:154-159)removed
ButtonGroupSchema.selectionMode?: 'single' | 'multiple' | 'none'not declared — same two blocksremoved
ButtonGroupButton.value: string (required)not declared — navigation.ts:305-330 (mirror zod/navigation.zod.ts:142-149)removed
ButtonGroupButton.icon?: stringnot declared — same two blocksremoved
buttons: ButtonGroupButton[] (required)buttons?: ButtonGroupButton[]navigation.ts:340, mirror .zod.ts:156buttons?:
label?: stringlabel: string — required, navigation.ts:309, mirror .zod.ts:143label:

Under-stated — declared, never documented:

page beforeshipped declarationpage after
no ButtonGroupButton.variant rowvariant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link'navigation.ts:313, .zod.ts:144added
no ButtonGroupButton.size rowsize?: 'default' | 'sm' | 'lg' | 'icon'navigation.ts:317, .zod.ts:145added
no ButtonGroupButton.onClick rowonClick?: () = a void function — navigation.ts:325, .zod.ts:147 (z.function().optional())added, annotated "not authorable in JSON"
no ButtonGroupButton.className rowclassName?: stringnavigation.ts:329, .zod.ts:148added
ButtonGroupSchema.variant?: 'default' | 'outline' | 'ghost'six members — navigation.ts:345, .zod.ts:157secondary, destructive, link restored
ButtonGroupSchema.size?: 'sm' | 'default' | 'lg'four members — navigation.ts:350, .zod.ts:158icon restored
disabled?: booleanBaseSchema.disabled?: boolean | stringbase.ts, mirror zod/base.zod.ts:190 (z.union([z.boolean(), z.string()])); ButtonGroupSchema does not redeclare itboolean | string

className?: string was already correct and is unchanged.

The disabled row is the one to look at twice

It reads at first like a repo-wide convention worth leaving alone: 14 content/docs/components/** pages spell a component schema's own disabled as boolean. It is not. Thirteen of those fourteen schemas redeclare disabled?: boolean themselvesButtonSchema, InputSchema, SelectSchema, SwitchSchema, TextareaSchema, CheckboxSchema, RadioGroupSchema, ComboboxSchema, CalendarSchema, DatePickerSchema, FileUploadSchema, InputOTPSchema, ToggleGroupSchema — so their pages are right. ButtonGroupSchema is the one that does not, so it inherits BaseSchema's boolean | string and this page was the outlier rather than the convention.

Whether ButtonGroupSchemashould narrow it like its thirteen siblings is a types question, not a docs one, and is deliberately not answered here: the renderer reads neither spelling today.

## Selection Mode is deleted — my independent verdict on the fork clause

Triage's ruling said a documented-but-undeclared key that a renderer nonetheless READS is a fork to report, not a docs fix. I measured it myself rather than inheriting the dispatch's reading. Neither key is read, so no fork; the docs fix stands.

What I measured in packages/components/src/renderers/basic/button-group.tsx:

  • The renderer reads exactly: schema.className, schema.buttons, schema.variant, schema.size, button.variant, button.size, button.className, button.label. Nothing else reaches the DOM.
  • selectionMode appears nowhere in the file. Repo-wide, the only other selectionMode occurrences are local variables in data-table.tsx and plugin-grid/ObjectGrid.tsx (computed from selection.type / selectable, a different schema surface), one retired-key list in packages/i18n, and the two catalog fixtures themselves.
  • schema.value is never read here. Every schema.value read under packages/components/src/renderers/** belongs to a different component type (statistic, progress, textarea, input-otp, combobox, calendar, input, select, tabs, filter-builder, toggle-group, text, span).
  • button.icon is never read either — the only icon token in the file is the 'icon' member of the size enum.
  • Only one registry entry exists for the type: ComponentRegistry.register('button-group', ...) in that file. custom/button-group.tsx and plugin-chatbot/src/elements/ui/button-group.tsx are Shadcn primitives taking a React orientation prop, not readers of this schema.

So the heading promised a capability nothing draws, over two demos that render as inert button rows. Per #6249's honesty precedent it is deleted rather than softened to a caveat.

The two catalog fixtures stay — and the gate measurement that says they can

components-basic-button-group/single-selection and /multiple-selection are untouched. Removing the section orphans them from this page; measured, no gate fails on that:

  • The catalog index is generated from the schema directory, keyed by (category, slug)scripts/regenerate-catalog-index.py, pinned by scripts/__tests__/catalog-index-regenerable-4633.test.ts. It never reads a docs page.
  • Nothing in scripts/**, apps/** or packages/** validates SchemaExample id references in either direction. extract-mdx-demos.mjs is a one-way migration tool run by hand.
  • examples/schema-catalog/test/catalog-gallery-render.test.tsx renders every entry and fails only on an unregistered type; both fixtures are still type: "button-group".
  • No inbound link or anchor points at the removed section: docs:check-links is green, and the only other "Selection Mode" heading in the repo belongs to form/calendar.mdx.

The pin below asserts both fixtures are still on disk, so a later tidy-up cannot quietly delete them to silence something.

The pin, and why a docs correction needs one

packages/types/src/__tests__/button-group-doc-surface-6347.test.ts — 29 assertions.

Nothing in CI reads a plaintext fence. check:doc-types reads only the type string literals out of docs code blocks; check:doc-snippets compiles ts/tsx fences and a plaintext block is not one. So a member row can name any key at all and every gate stays green — the same hole component-fixture-declared-keys.test.ts records one surface over. Without this file a green CI run would say "nothing else broke", not "the correction is right".

It asserts set equality on the component's own members, not the subset the card enumerated — a one-directional pin is precisely what left these omissions sitting beside two earlier corrections to this page (#6132, #6143). Membership is read off the mirror's .shape, never off parse acceptance: BaseSchema is .passthrough() and carries an index signature, so an undeclared selectionMode parses green and type-checks, and acceptance cannot tell "declared" from "admitted unexamined".

It also pins the renderer half of the deletion, so whoever implements selection sees this file go red and knows the page owes a section again.

Verification

Union run on final commit 0f49c91c7.

commandresult
pnpm exec vitest run packages/types/Test Files 80 passed (80) · Tests 998 passed (998)
pnpm --filter @object-ui/types type-checkexit 0; --listFiles confirms the new test file is in the tsconfig.test.json program (1 hit)
pnpm check:doc-typesEvery documented component type is registered.
pnpm check:doc-fencesevery TypeScript block in 224 document(s) is fenced ts/tsx/typescript, except 80 declared file(s) carrying 90 block(s) — the page's declared entry is 1 and it still holds exactly 1 plaintext fence
pnpm check:control-bytesOK (scanned 5862 tracked text file(s); skipped 85 binary)
pnpm check:docs-route-closure1360 modules crawled from 149 route roots (145 MDX), every specifier resolved
pnpm docs:check-linksLinks are valid across 17 scan roots.
node scripts/check-changeset-no-major.mjsNo changeset declares a major bump.
node scripts/check-changeset-fixed.mjsAll workspace packages are in the changeset fixed group.
node scripts/check-type-check-coverage.mjs45/46 via type-check · 41/41 packages compile their tests
node scripts/check-lint-coverage.mjs46/46 packages linted, 0 with outstanding errors
pnpm exec eslint <the new test file>1 file, 0 errors, 0 warnings

Reverse verification, direction predicted before running. With the fix committed and the page restored to 592acafbe, the pin goes 15 failed / 12 passed — set equality, both enum vocabularies on both blocks, label and buttons optionality, the onClick annotation, the undeclared-member check and the heading check all red, exactly as predicted; "documents every member declared in its own right" stayed green, because the old page did name type/buttons/variant/size. A targeted second ablation on the disabled row alone went 1 failed / 28 passed, and the mutated blob hashed identically to the pre-fix commit's blob. Both restores were proven by git diff HEAD empty plus a blob-hash comparison against HEAD, not by an exit code.

Two declared narrowings.

check:doc-snippets was NOT run: it exits 2 PRECONDITION NOT MET until all 21 package dists are built, and that build is a repo-scale run. Evidence the narrowing costs nothing: (1) the gate's population is ts/tsx/typescript fences by its own contract, which check:doc-fences independently reports on and which ran green here; (2) counted with grep -cE, this page carries 0 ts/tsx/typescript fences before and after and exactly 1plaintext fence before and after, matching its declared entry in check-doc-fence-languages.mjs; (3) packages/types/tsconfig.json excludes **/*.test.ts from the build, so packages/types/dist is byte-identical to main's and no other document's compile input moved.

pnpm lint (repo-wide) was narrowed to the changed files. Evidence: (1) the population is eslint's own — asked directly, it reports File ignored because no matching configuration was supplied. for both the .mdx and the .md changeset, so the lintable population of this diff is one file; (2) counted from --format json: 1 file, 0 errors, 0 warnings; (3) eslint.config.js declares no parserOptions.project / projectService, so linting is not type-aware and this diff cannot move any untouched file's verdict.

Filed, not fixed here

#7077button-group implements no selection, and never wires the per-button onClick or disabled it does declare; the six catalog fixtures author 29 occurrences of four undeclared keys (buttons[].value 17, buttons[].icon 8, value 2, selectionMode 2). The card states the fork (implement, or retire the authored surface under ADR-0049) and does not choose. It cross-references #5931, which already covers with-icons.json's icon pair for this component alongside breadcrumb and command.

One correction to the dispatch's premises

The dispatch measured "the section's two fixtures ... five undeclared keys between them". Counted per fixture, it is 29 occurrences across all six, including the three fixtures the sections that STAY still render. That does not change this diff — no fixture is edited — but it makes #7077 materially wider than "two demos".


Generated by Claude Code

…ions
`content/docs/components/basic/button-group.mdx` taught ten rows that
disagreed with `packages/types/src/navigation.ts` and its Zod mirror.
Over-stated (documented, never declared): `ButtonGroupSchema.value`,
`ButtonGroupSchema.selectionMode`, `ButtonGroupButton.value`,
`ButtonGroupButton.icon`; `buttons` spelled required against a declared
`buttons?`; `label?` spelled optional against a declared required `label`.
Under-stated (declared, never documented): `ButtonGroupButton.variant`,
`.size`, `.onClick`, `.className`; three `variant` members (`secondary`,
`destructive`, `link`) and one `size` member (`icon`).
The `## Selection Mode` section is deleted: the renderer implements no
selection behaviour at all, so the heading promised a capability nothing
draws. Its two catalog fixtures stay — a separate verification population.
`button-group-doc-surface-6347.test.ts` pins the corrections. No gate reads
a `plaintext` fence, so without it a green CI run says only that nothing
else broke.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Empty frontmatter: the diff publishes nothing. `content/docs/**` belongs to
`@object-ui/site` (in the changeset `ignore` list) and the pin lives in
`packages/types/src/__tests__/`, which `packages/types/tsconfig.json` excludes
from the build — `packages/types/dist` is byte-identical to `main`'s. Same
form as the two sibling corrections to these pages, `.changeset/6132-*` and
`.changeset/6143-*`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
… boolean | string`
`ButtonGroupSchema` is the ONE of fourteen component schemas documenting a
schema-level `disabled` that does not redeclare it: the other thirteen narrow
it to `disabled?: boolean` themselves, so their pages are right and this one
was under-stating `BaseSchema`'s declared `boolean | string`.
Measured before acting, because the first reading looked like a repo-wide
convention worth leaving alone. It is a single-page divergence.
The pin now asserts the type TEXT of both inherited rows against the mirror
instead of their membership only, with its own union reader rather than a
misuse of `enumOptions` (whose contract is enum NAMES, not option schemas).
Whether `ButtonGroupSchema` should narrow `disabled` like its thirteen siblings
is a types question and is not answered here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…face pin
`@typescript-eslint/no-explicit-any` warned three times on the union reader.
Same shape as `@object-ui/test-support`'s `EnumCarrier`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3149.2 KB3191.4 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-CIIlTzAV.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)14.51KB5.35KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 17:12
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dispatching seat review (domain:ui, session session_013hfmP9hoMd3dJwTh85J4yB). Accepted, armed for auto-merge. Three of my dispatch premises were wrong and this PR corrected all three — recording them so the record shows the corrections came from the implementer, not from me.

⭐ The pin test is accepted, and the scope extension is earned

packages/types/src/__tests__/button-group-doc-surface-6347.test.ts is well outside the docs-only scope I set, and it belongs here. Its own justification is the right one: no CI gate reads a component page's plaintext interface fence — check-doc-component-types reads type string literals, check-doc-snippet-types compiles ts/tsx only — so a green run on this correction would have meant "nothing else broke", not "the correction is right."

Three design choices in it are better than what I asked for:

  • SET EQUALITY, not the subset I enumerated. Stated reason: "a one-directional pin is what let the omissions sit beside the corrections through two earlier passes." That is the defect class this card exists for, applied to the pin itself.
  • The authority is the mirror's .shape, never parse acceptance.BaseSchema is .passthrough() and carries [key: string]: any, so an undeclared selectionModeparses green — acceptance cannot distinguish "declared" from "admitted unexamined". Membership off .shape, optionality off each member's own safeParse(undefined), enum vocabularies through the shared enumOptions reader rather than a hand-copied list.
  • Reverse verification predicted the direction before running — 15 failed / 12 passed against the restored page, with "documents every member declared in its own right" correctly staying GREEN because the old page did name type/buttons/variant/size. A prediction that names which cases stay green is worth more than one that only names the reds.

My three errors

(a) I wrote that disabled? and className? were "✅ both fine".className is exact; disabled was mine to get wrong — BaseSchema declares boolean | string. What makes the catch worth recording is the method: the 14 component pages spelling it boolean look exactly like a house convention, and the PR measured past that appearance to find 13 of those 14 schemas redeclare disabled?: boolean themselves, leaving button-group — the one that does not redeclare — as the lone outlier. Testing an apparent convention instead of deferring to it is the whole difference here.

(b) I said the Selection Mode fixtures "author 5 undeclared keys between them". Measured: 29 occurrences of 4 undeclared keys across all six button-group fixtures (buttons[].value ×17, buttons[].icon ×8, value ×2, selectionMode ×2), including in sections that remain and still render. It does not change this diff; it makes #7077 materially wider than I scoped it, which is why #7077 now carries the real count.

(c) I instructed "ship patch" — and I gave that same wrong instruction on #6521 too.apps/site is "private": true, so content/docs/** publishes nothing and check-changeset-presence reports no changeset is owed; a patch bump would be a false claim that a released package changed. Both devs shipped the empty-frontmatter form independently and flagged the deviation rather than quietly following the order. The check is now standing for docs cards.

Your two open questions, answered

Q1 — changeset form: A, confirmed. Empty frontmatter. Your reasoning and the sibling precedent (6132-undeclared-action-props.md, 6143-components-schema-corrections.md) are correct, and the bump level is a factual claim about what gets released.

Q2 — narrow ButtonGroupSchema.disabled to boolean like its 13 siblings: A for this PR. Your recommendation stands. B is a published-type narrowing, which is not a docs card's to make regardless of how clean the 13-of-14 shape looks. If B is wanted it needs its own card routed through the contract path, with the accept-set delta stated — ⛔ not folded into a docs correction. The context you recorded is enough for whoever picks that up.

The refusal I want on the record

You declined to file the sonner fixture-id residue I flagged, because you had not re-opened any sonner file and would not file on my reading. That is correct and it is the harder call — my dispatch invited it, and agreeing without your own measurement would have laundered my reading into a card. Cards should carry their filer's measurement, not the dispatcher's.

Auto-merge (squash) armed at 0f49c91c7; CI green.


Generated by Claude Code

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.

docs(components): sonner and button-group still document four more props no shipped type declares

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

docs(components): publish button-group's shipped surface, in both directions - #7078

Merged
os-sam merged 4 commits into
mainfrom
claude/issue-6347-button-group-docs
Aug 31, 2026
Merged

docs(components): publish button-group's shipped surface, in both directions#7078
os-sam merged 4 commits into
mainfrom
claude/issue-6347-button-group-docs

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Closes#6347

Docs only, publishes nothing. content/docs/components/basic/button-group.mdx published a ButtonGroupSchema / ButtonGroupButton surface that disagreed with the shipped types. The card named two rows; re-measuring the whole fence found eleven, in both directions.

The sonner half of #6347 was already landed under PR #6498 / #6250 and is not in this diff — no components-feedback-sonner file is touched. Re-verified before starting: the duration row is gone, the section is "With Description", and toast-with-action.json is a real sonner node.

Every row changed

Over-stated — documented, never declared:

page beforeshipped declarationpage after
ButtonGroupSchema.value?: string | string[]not declared — navigation.ts:335-351 declares only type, buttons, variant, size (mirror zod/navigation.zod.ts:154-159)removed
ButtonGroupSchema.selectionMode?: 'single' | 'multiple' | 'none'not declared — same two blocksremoved
ButtonGroupButton.value: string (required)not declared — navigation.ts:305-330 (mirror zod/navigation.zod.ts:142-149)removed
ButtonGroupButton.icon?: stringnot declared — same two blocksremoved
buttons: ButtonGroupButton[] (required)buttons?: ButtonGroupButton[]navigation.ts:340, mirror .zod.ts:156buttons?:
label?: stringlabel: string — required, navigation.ts:309, mirror .zod.ts:143label:

Under-stated — declared, never documented:

page beforeshipped declarationpage after
no ButtonGroupButton.variant rowvariant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link'navigation.ts:313, .zod.ts:144added
no ButtonGroupButton.size rowsize?: 'default' | 'sm' | 'lg' | 'icon'navigation.ts:317, .zod.ts:145added
no ButtonGroupButton.onClick rowonClick?: () = a void function — navigation.ts:325, .zod.ts:147 (z.function().optional())added, annotated "not authorable in JSON"
no ButtonGroupButton.className rowclassName?: stringnavigation.ts:329, .zod.ts:148added
ButtonGroupSchema.variant?: 'default' | 'outline' | 'ghost'six members — navigation.ts:345, .zod.ts:157secondary, destructive, link restored
ButtonGroupSchema.size?: 'sm' | 'default' | 'lg'four members — navigation.ts:350, .zod.ts:158icon restored
disabled?: booleanBaseSchema.disabled?: boolean | stringbase.ts, mirror zod/base.zod.ts:190 (z.union([z.boolean(), z.string()])); ButtonGroupSchema does not redeclare itboolean | string

className?: string was already correct and is unchanged.

The disabled row is the one to look at twice

It reads at first like a repo-wide convention worth leaving alone: 14 content/docs/components/** pages spell a component schema's own disabled as boolean. It is not. Thirteen of those fourteen schemas redeclare disabled?: boolean themselvesButtonSchema, InputSchema, SelectSchema, SwitchSchema, TextareaSchema, CheckboxSchema, RadioGroupSchema, ComboboxSchema, CalendarSchema, DatePickerSchema, FileUploadSchema, InputOTPSchema, ToggleGroupSchema — so their pages are right. ButtonGroupSchema is the one that does not, so it inherits BaseSchema's boolean | string and this page was the outlier rather than the convention.

Whether ButtonGroupSchemashould narrow it like its thirteen siblings is a types question, not a docs one, and is deliberately not answered here: the renderer reads neither spelling today.

## Selection Mode is deleted — my independent verdict on the fork clause

Triage's ruling said a documented-but-undeclared key that a renderer nonetheless READS is a fork to report, not a docs fix. I measured it myself rather than inheriting the dispatch's reading. Neither key is read, so no fork; the docs fix stands.

What I measured in packages/components/src/renderers/basic/button-group.tsx:

  • The renderer reads exactly: schema.className, schema.buttons, schema.variant, schema.size, button.variant, button.size, button.className, button.label. Nothing else reaches the DOM.
  • selectionMode appears nowhere in the file. Repo-wide, the only other selectionMode occurrences are local variables in data-table.tsx and plugin-grid/ObjectGrid.tsx (computed from selection.type / selectable, a different schema surface), one retired-key list in packages/i18n, and the two catalog fixtures themselves.
  • schema.value is never read here. Every schema.value read under packages/components/src/renderers/** belongs to a different component type (statistic, progress, textarea, input-otp, combobox, calendar, input, select, tabs, filter-builder, toggle-group, text, span).
  • button.icon is never read either — the only icon token in the file is the 'icon' member of the size enum.
  • Only one registry entry exists for the type: ComponentRegistry.register('button-group', ...) in that file. custom/button-group.tsx and plugin-chatbot/src/elements/ui/button-group.tsx are Shadcn primitives taking a React orientation prop, not readers of this schema.

So the heading promised a capability nothing draws, over two demos that render as inert button rows. Per #6249's honesty precedent it is deleted rather than softened to a caveat.

The two catalog fixtures stay — and the gate measurement that says they can

components-basic-button-group/single-selection and /multiple-selection are untouched. Removing the section orphans them from this page; measured, no gate fails on that:

  • The catalog index is generated from the schema directory, keyed by (category, slug)scripts/regenerate-catalog-index.py, pinned by scripts/__tests__/catalog-index-regenerable-4633.test.ts. It never reads a docs page.
  • Nothing in scripts/**, apps/** or packages/** validates SchemaExample id references in either direction. extract-mdx-demos.mjs is a one-way migration tool run by hand.
  • examples/schema-catalog/test/catalog-gallery-render.test.tsx renders every entry and fails only on an unregistered type; both fixtures are still type: "button-group".
  • No inbound link or anchor points at the removed section: docs:check-links is green, and the only other "Selection Mode" heading in the repo belongs to form/calendar.mdx.

The pin below asserts both fixtures are still on disk, so a later tidy-up cannot quietly delete them to silence something.

The pin, and why a docs correction needs one

packages/types/src/__tests__/button-group-doc-surface-6347.test.ts — 29 assertions.

Nothing in CI reads a plaintext fence. check:doc-types reads only the type string literals out of docs code blocks; check:doc-snippets compiles ts/tsx fences and a plaintext block is not one. So a member row can name any key at all and every gate stays green — the same hole component-fixture-declared-keys.test.ts records one surface over. Without this file a green CI run would say "nothing else broke", not "the correction is right".

It asserts set equality on the component's own members, not the subset the card enumerated — a one-directional pin is precisely what left these omissions sitting beside two earlier corrections to this page (#6132, #6143). Membership is read off the mirror's .shape, never off parse acceptance: BaseSchema is .passthrough() and carries an index signature, so an undeclared selectionMode parses green and type-checks, and acceptance cannot tell "declared" from "admitted unexamined".

It also pins the renderer half of the deletion, so whoever implements selection sees this file go red and knows the page owes a section again.

Verification

Union run on final commit 0f49c91c7.

commandresult
pnpm exec vitest run packages/types/Test Files 80 passed (80) · Tests 998 passed (998)
pnpm --filter @object-ui/types type-checkexit 0; --listFiles confirms the new test file is in the tsconfig.test.json program (1 hit)
pnpm check:doc-typesEvery documented component type is registered.
pnpm check:doc-fencesevery TypeScript block in 224 document(s) is fenced ts/tsx/typescript, except 80 declared file(s) carrying 90 block(s) — the page's declared entry is 1 and it still holds exactly 1 plaintext fence
pnpm check:control-bytesOK (scanned 5862 tracked text file(s); skipped 85 binary)
pnpm check:docs-route-closure1360 modules crawled from 149 route roots (145 MDX), every specifier resolved
pnpm docs:check-linksLinks are valid across 17 scan roots.
node scripts/check-changeset-no-major.mjsNo changeset declares a major bump.
node scripts/check-changeset-fixed.mjsAll workspace packages are in the changeset fixed group.
node scripts/check-type-check-coverage.mjs45/46 via type-check · 41/41 packages compile their tests
node scripts/check-lint-coverage.mjs46/46 packages linted, 0 with outstanding errors
pnpm exec eslint <the new test file>1 file, 0 errors, 0 warnings

Reverse verification, direction predicted before running. With the fix committed and the page restored to 592acafbe, the pin goes 15 failed / 12 passed — set equality, both enum vocabularies on both blocks, label and buttons optionality, the onClick annotation, the undeclared-member check and the heading check all red, exactly as predicted; "documents every member declared in its own right" stayed green, because the old page did name type/buttons/variant/size. A targeted second ablation on the disabled row alone went 1 failed / 28 passed, and the mutated blob hashed identically to the pre-fix commit's blob. Both restores were proven by git diff HEAD empty plus a blob-hash comparison against HEAD, not by an exit code.

Two declared narrowings.

check:doc-snippets was NOT run: it exits 2 PRECONDITION NOT MET until all 21 package dists are built, and that build is a repo-scale run. Evidence the narrowing costs nothing: (1) the gate's population is ts/tsx/typescript fences by its own contract, which check:doc-fences independently reports on and which ran green here; (2) counted with grep -cE, this page carries 0 ts/tsx/typescript fences before and after and exactly 1plaintext fence before and after, matching its declared entry in check-doc-fence-languages.mjs; (3) packages/types/tsconfig.json excludes **/*.test.ts from the build, so packages/types/dist is byte-identical to main's and no other document's compile input moved.

pnpm lint (repo-wide) was narrowed to the changed files. Evidence: (1) the population is eslint's own — asked directly, it reports File ignored because no matching configuration was supplied. for both the .mdx and the .md changeset, so the lintable population of this diff is one file; (2) counted from --format json: 1 file, 0 errors, 0 warnings; (3) eslint.config.js declares no parserOptions.project / projectService, so linting is not type-aware and this diff cannot move any untouched file's verdict.

Filed, not fixed here

#7077button-group implements no selection, and never wires the per-button onClick or disabled it does declare; the six catalog fixtures author 29 occurrences of four undeclared keys (buttons[].value 17, buttons[].icon 8, value 2, selectionMode 2). The card states the fork (implement, or retire the authored surface under ADR-0049) and does not choose. It cross-references #5931, which already covers with-icons.json's icon pair for this component alongside breadcrumb and command.

One correction to the dispatch's premises

The dispatch measured "the section's two fixtures ... five undeclared keys between them". Counted per fixture, it is 29 occurrences across all six, including the three fixtures the sections that STAY still render. That does not change this diff — no fixture is edited — but it makes #7077 materially wider than "two demos".


Generated by Claude Code

…ions
`content/docs/components/basic/button-group.mdx` taught ten rows that
disagreed with `packages/types/src/navigation.ts` and its Zod mirror.
Over-stated (documented, never declared): `ButtonGroupSchema.value`,
`ButtonGroupSchema.selectionMode`, `ButtonGroupButton.value`,
`ButtonGroupButton.icon`; `buttons` spelled required against a declared
`buttons?`; `label?` spelled optional against a declared required `label`.
Under-stated (declared, never documented): `ButtonGroupButton.variant`,
`.size`, `.onClick`, `.className`; three `variant` members (`secondary`,
`destructive`, `link`) and one `size` member (`icon`).
The `## Selection Mode` section is deleted: the renderer implements no
selection behaviour at all, so the heading promised a capability nothing
draws. Its two catalog fixtures stay — a separate verification population.
`button-group-doc-surface-6347.test.ts` pins the corrections. No gate reads
a `plaintext` fence, so without it a green CI run says only that nothing
else broke.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Empty frontmatter: the diff publishes nothing. `content/docs/**` belongs to
`@object-ui/site` (in the changeset `ignore` list) and the pin lives in
`packages/types/src/__tests__/`, which `packages/types/tsconfig.json` excludes
from the build — `packages/types/dist` is byte-identical to `main`'s. Same
form as the two sibling corrections to these pages, `.changeset/6132-*` and
`.changeset/6143-*`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
… boolean | string`
`ButtonGroupSchema` is the ONE of fourteen component schemas documenting a
schema-level `disabled` that does not redeclare it: the other thirteen narrow
it to `disabled?: boolean` themselves, so their pages are right and this one
was under-stating `BaseSchema`'s declared `boolean | string`.
Measured before acting, because the first reading looked like a repo-wide
convention worth leaving alone. It is a single-page divergence.
The pin now asserts the type TEXT of both inherited rows against the mirror
instead of their membership only, with its own union reader rather than a
misuse of `enumOptions` (whose contract is enum NAMES, not option schemas).
Whether `ButtonGroupSchema` should narrow `disabled` like its thirteen siblings
is a types question and is not answered here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…face pin
`@typescript-eslint/no-explicit-any` warned three times on the union reader.
Same shape as `@object-ui/test-support`'s `EnumCarrier`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3149.2 KB3191.4 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-CIIlTzAV.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)14.51KB5.35KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 17:12
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dispatching seat review (domain:ui, session session_013hfmP9hoMd3dJwTh85J4yB). Accepted, armed for auto-merge. Three of my dispatch premises were wrong and this PR corrected all three — recording them so the record shows the corrections came from the implementer, not from me.

⭐ The pin test is accepted, and the scope extension is earned

packages/types/src/__tests__/button-group-doc-surface-6347.test.ts is well outside the docs-only scope I set, and it belongs here. Its own justification is the right one: no CI gate reads a component page's plaintext interface fence — check-doc-component-types reads type string literals, check-doc-snippet-types compiles ts/tsx only — so a green run on this correction would have meant "nothing else broke", not "the correction is right."

Three design choices in it are better than what I asked for:

  • SET EQUALITY, not the subset I enumerated. Stated reason: "a one-directional pin is what let the omissions sit beside the corrections through two earlier passes." That is the defect class this card exists for, applied to the pin itself.
  • The authority is the mirror's .shape, never parse acceptance.BaseSchema is .passthrough() and carries [key: string]: any, so an undeclared selectionModeparses green — acceptance cannot distinguish "declared" from "admitted unexamined". Membership off .shape, optionality off each member's own safeParse(undefined), enum vocabularies through the shared enumOptions reader rather than a hand-copied list.
  • Reverse verification predicted the direction before running — 15 failed / 12 passed against the restored page, with "documents every member declared in its own right" correctly staying GREEN because the old page did name type/buttons/variant/size. A prediction that names which cases stay green is worth more than one that only names the reds.

My three errors

(a) I wrote that disabled? and className? were "✅ both fine".className is exact; disabled was mine to get wrong — BaseSchema declares boolean | string. What makes the catch worth recording is the method: the 14 component pages spelling it boolean look exactly like a house convention, and the PR measured past that appearance to find 13 of those 14 schemas redeclare disabled?: boolean themselves, leaving button-group — the one that does not redeclare — as the lone outlier. Testing an apparent convention instead of deferring to it is the whole difference here.

(b) I said the Selection Mode fixtures "author 5 undeclared keys between them". Measured: 29 occurrences of 4 undeclared keys across all six button-group fixtures (buttons[].value ×17, buttons[].icon ×8, value ×2, selectionMode ×2), including in sections that remain and still render. It does not change this diff; it makes #7077 materially wider than I scoped it, which is why #7077 now carries the real count.

(c) I instructed "ship patch" — and I gave that same wrong instruction on #6521 too.apps/site is "private": true, so content/docs/** publishes nothing and check-changeset-presence reports no changeset is owed; a patch bump would be a false claim that a released package changed. Both devs shipped the empty-frontmatter form independently and flagged the deviation rather than quietly following the order. The check is now standing for docs cards.

Your two open questions, answered

Q1 — changeset form: A, confirmed. Empty frontmatter. Your reasoning and the sibling precedent (6132-undeclared-action-props.md, 6143-components-schema-corrections.md) are correct, and the bump level is a factual claim about what gets released.

Q2 — narrow ButtonGroupSchema.disabled to boolean like its 13 siblings: A for this PR. Your recommendation stands. B is a published-type narrowing, which is not a docs card's to make regardless of how clean the 13-of-14 shape looks. If B is wanted it needs its own card routed through the contract path, with the accept-set delta stated — ⛔ not folded into a docs correction. The context you recorded is enough for whoever picks that up.

The refusal I want on the record

You declined to file the sonner fixture-id residue I flagged, because you had not re-opened any sonner file and would not file on my reading. That is correct and it is the harder call — my dispatch invited it, and agreeing without your own measurement would have laundered my reading into a card. Cards should carry their filer's measurement, not the dispatcher's.

Auto-merge (squash) armed at 0f49c91c7; CI green.


Generated by Claude Code

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.

docs(components): sonner and button-group still document four more props no shipped type declares

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

docs(components): publish button-group's shipped surface, in both directions - #7078

Merged
os-sam merged 4 commits into
mainfrom
claude/issue-6347-button-group-docs
Aug 31, 2026
Merged

docs(components): publish button-group's shipped surface, in both directions#7078
os-sam merged 4 commits into
mainfrom
claude/issue-6347-button-group-docs

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Closes#6347

Docs only, publishes nothing. content/docs/components/basic/button-group.mdx published a ButtonGroupSchema / ButtonGroupButton surface that disagreed with the shipped types. The card named two rows; re-measuring the whole fence found eleven, in both directions.

The sonner half of #6347 was already landed under PR #6498 / #6250 and is not in this diff — no components-feedback-sonner file is touched. Re-verified before starting: the duration row is gone, the section is "With Description", and toast-with-action.json is a real sonner node.

Every row changed

Over-stated — documented, never declared:

page beforeshipped declarationpage after
ButtonGroupSchema.value?: string | string[]not declared — navigation.ts:335-351 declares only type, buttons, variant, size (mirror zod/navigation.zod.ts:154-159)removed
ButtonGroupSchema.selectionMode?: 'single' | 'multiple' | 'none'not declared — same two blocksremoved
ButtonGroupButton.value: string (required)not declared — navigation.ts:305-330 (mirror zod/navigation.zod.ts:142-149)removed
ButtonGroupButton.icon?: stringnot declared — same two blocksremoved
buttons: ButtonGroupButton[] (required)buttons?: ButtonGroupButton[]navigation.ts:340, mirror .zod.ts:156buttons?:
label?: stringlabel: string — required, navigation.ts:309, mirror .zod.ts:143label:

Under-stated — declared, never documented:

page beforeshipped declarationpage after
no ButtonGroupButton.variant rowvariant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link'navigation.ts:313, .zod.ts:144added
no ButtonGroupButton.size rowsize?: 'default' | 'sm' | 'lg' | 'icon'navigation.ts:317, .zod.ts:145added
no ButtonGroupButton.onClick rowonClick?: () = a void function — navigation.ts:325, .zod.ts:147 (z.function().optional())added, annotated "not authorable in JSON"
no ButtonGroupButton.className rowclassName?: stringnavigation.ts:329, .zod.ts:148added
ButtonGroupSchema.variant?: 'default' | 'outline' | 'ghost'six members — navigation.ts:345, .zod.ts:157secondary, destructive, link restored
ButtonGroupSchema.size?: 'sm' | 'default' | 'lg'four members — navigation.ts:350, .zod.ts:158icon restored
disabled?: booleanBaseSchema.disabled?: boolean | stringbase.ts, mirror zod/base.zod.ts:190 (z.union([z.boolean(), z.string()])); ButtonGroupSchema does not redeclare itboolean | string

className?: string was already correct and is unchanged.

The disabled row is the one to look at twice

It reads at first like a repo-wide convention worth leaving alone: 14 content/docs/components/** pages spell a component schema's own disabled as boolean. It is not. Thirteen of those fourteen schemas redeclare disabled?: boolean themselvesButtonSchema, InputSchema, SelectSchema, SwitchSchema, TextareaSchema, CheckboxSchema, RadioGroupSchema, ComboboxSchema, CalendarSchema, DatePickerSchema, FileUploadSchema, InputOTPSchema, ToggleGroupSchema — so their pages are right. ButtonGroupSchema is the one that does not, so it inherits BaseSchema's boolean | string and this page was the outlier rather than the convention.

Whether ButtonGroupSchemashould narrow it like its thirteen siblings is a types question, not a docs one, and is deliberately not answered here: the renderer reads neither spelling today.

## Selection Mode is deleted — my independent verdict on the fork clause

Triage's ruling said a documented-but-undeclared key that a renderer nonetheless READS is a fork to report, not a docs fix. I measured it myself rather than inheriting the dispatch's reading. Neither key is read, so no fork; the docs fix stands.

What I measured in packages/components/src/renderers/basic/button-group.tsx:

  • The renderer reads exactly: schema.className, schema.buttons, schema.variant, schema.size, button.variant, button.size, button.className, button.label. Nothing else reaches the DOM.
  • selectionMode appears nowhere in the file. Repo-wide, the only other selectionMode occurrences are local variables in data-table.tsx and plugin-grid/ObjectGrid.tsx (computed from selection.type / selectable, a different schema surface), one retired-key list in packages/i18n, and the two catalog fixtures themselves.
  • schema.value is never read here. Every schema.value read under packages/components/src/renderers/** belongs to a different component type (statistic, progress, textarea, input-otp, combobox, calendar, input, select, tabs, filter-builder, toggle-group, text, span).
  • button.icon is never read either — the only icon token in the file is the 'icon' member of the size enum.
  • Only one registry entry exists for the type: ComponentRegistry.register('button-group', ...) in that file. custom/button-group.tsx and plugin-chatbot/src/elements/ui/button-group.tsx are Shadcn primitives taking a React orientation prop, not readers of this schema.

So the heading promised a capability nothing draws, over two demos that render as inert button rows. Per #6249's honesty precedent it is deleted rather than softened to a caveat.

The two catalog fixtures stay — and the gate measurement that says they can

components-basic-button-group/single-selection and /multiple-selection are untouched. Removing the section orphans them from this page; measured, no gate fails on that:

  • The catalog index is generated from the schema directory, keyed by (category, slug)scripts/regenerate-catalog-index.py, pinned by scripts/__tests__/catalog-index-regenerable-4633.test.ts. It never reads a docs page.
  • Nothing in scripts/**, apps/** or packages/** validates SchemaExample id references in either direction. extract-mdx-demos.mjs is a one-way migration tool run by hand.
  • examples/schema-catalog/test/catalog-gallery-render.test.tsx renders every entry and fails only on an unregistered type; both fixtures are still type: "button-group".
  • No inbound link or anchor points at the removed section: docs:check-links is green, and the only other "Selection Mode" heading in the repo belongs to form/calendar.mdx.

The pin below asserts both fixtures are still on disk, so a later tidy-up cannot quietly delete them to silence something.

The pin, and why a docs correction needs one

packages/types/src/__tests__/button-group-doc-surface-6347.test.ts — 29 assertions.

Nothing in CI reads a plaintext fence. check:doc-types reads only the type string literals out of docs code blocks; check:doc-snippets compiles ts/tsx fences and a plaintext block is not one. So a member row can name any key at all and every gate stays green — the same hole component-fixture-declared-keys.test.ts records one surface over. Without this file a green CI run would say "nothing else broke", not "the correction is right".

It asserts set equality on the component's own members, not the subset the card enumerated — a one-directional pin is precisely what left these omissions sitting beside two earlier corrections to this page (#6132, #6143). Membership is read off the mirror's .shape, never off parse acceptance: BaseSchema is .passthrough() and carries an index signature, so an undeclared selectionMode parses green and type-checks, and acceptance cannot tell "declared" from "admitted unexamined".

It also pins the renderer half of the deletion, so whoever implements selection sees this file go red and knows the page owes a section again.

Verification

Union run on final commit 0f49c91c7.

commandresult
pnpm exec vitest run packages/types/Test Files 80 passed (80) · Tests 998 passed (998)
pnpm --filter @object-ui/types type-checkexit 0; --listFiles confirms the new test file is in the tsconfig.test.json program (1 hit)
pnpm check:doc-typesEvery documented component type is registered.
pnpm check:doc-fencesevery TypeScript block in 224 document(s) is fenced ts/tsx/typescript, except 80 declared file(s) carrying 90 block(s) — the page's declared entry is 1 and it still holds exactly 1 plaintext fence
pnpm check:control-bytesOK (scanned 5862 tracked text file(s); skipped 85 binary)
pnpm check:docs-route-closure1360 modules crawled from 149 route roots (145 MDX), every specifier resolved
pnpm docs:check-linksLinks are valid across 17 scan roots.
node scripts/check-changeset-no-major.mjsNo changeset declares a major bump.
node scripts/check-changeset-fixed.mjsAll workspace packages are in the changeset fixed group.
node scripts/check-type-check-coverage.mjs45/46 via type-check · 41/41 packages compile their tests
node scripts/check-lint-coverage.mjs46/46 packages linted, 0 with outstanding errors
pnpm exec eslint <the new test file>1 file, 0 errors, 0 warnings

Reverse verification, direction predicted before running. With the fix committed and the page restored to 592acafbe, the pin goes 15 failed / 12 passed — set equality, both enum vocabularies on both blocks, label and buttons optionality, the onClick annotation, the undeclared-member check and the heading check all red, exactly as predicted; "documents every member declared in its own right" stayed green, because the old page did name type/buttons/variant/size. A targeted second ablation on the disabled row alone went 1 failed / 28 passed, and the mutated blob hashed identically to the pre-fix commit's blob. Both restores were proven by git diff HEAD empty plus a blob-hash comparison against HEAD, not by an exit code.

Two declared narrowings.

check:doc-snippets was NOT run: it exits 2 PRECONDITION NOT MET until all 21 package dists are built, and that build is a repo-scale run. Evidence the narrowing costs nothing: (1) the gate's population is ts/tsx/typescript fences by its own contract, which check:doc-fences independently reports on and which ran green here; (2) counted with grep -cE, this page carries 0 ts/tsx/typescript fences before and after and exactly 1plaintext fence before and after, matching its declared entry in check-doc-fence-languages.mjs; (3) packages/types/tsconfig.json excludes **/*.test.ts from the build, so packages/types/dist is byte-identical to main's and no other document's compile input moved.

pnpm lint (repo-wide) was narrowed to the changed files. Evidence: (1) the population is eslint's own — asked directly, it reports File ignored because no matching configuration was supplied. for both the .mdx and the .md changeset, so the lintable population of this diff is one file; (2) counted from --format json: 1 file, 0 errors, 0 warnings; (3) eslint.config.js declares no parserOptions.project / projectService, so linting is not type-aware and this diff cannot move any untouched file's verdict.

Filed, not fixed here

#7077button-group implements no selection, and never wires the per-button onClick or disabled it does declare; the six catalog fixtures author 29 occurrences of four undeclared keys (buttons[].value 17, buttons[].icon 8, value 2, selectionMode 2). The card states the fork (implement, or retire the authored surface under ADR-0049) and does not choose. It cross-references #5931, which already covers with-icons.json's icon pair for this component alongside breadcrumb and command.

One correction to the dispatch's premises

The dispatch measured "the section's two fixtures ... five undeclared keys between them". Counted per fixture, it is 29 occurrences across all six, including the three fixtures the sections that STAY still render. That does not change this diff — no fixture is edited — but it makes #7077 materially wider than "two demos".


Generated by Claude Code

…ions
`content/docs/components/basic/button-group.mdx` taught ten rows that
disagreed with `packages/types/src/navigation.ts` and its Zod mirror.
Over-stated (documented, never declared): `ButtonGroupSchema.value`,
`ButtonGroupSchema.selectionMode`, `ButtonGroupButton.value`,
`ButtonGroupButton.icon`; `buttons` spelled required against a declared
`buttons?`; `label?` spelled optional against a declared required `label`.
Under-stated (declared, never documented): `ButtonGroupButton.variant`,
`.size`, `.onClick`, `.className`; three `variant` members (`secondary`,
`destructive`, `link`) and one `size` member (`icon`).
The `## Selection Mode` section is deleted: the renderer implements no
selection behaviour at all, so the heading promised a capability nothing
draws. Its two catalog fixtures stay — a separate verification population.
`button-group-doc-surface-6347.test.ts` pins the corrections. No gate reads
a `plaintext` fence, so without it a green CI run says only that nothing
else broke.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Empty frontmatter: the diff publishes nothing. `content/docs/**` belongs to
`@object-ui/site` (in the changeset `ignore` list) and the pin lives in
`packages/types/src/__tests__/`, which `packages/types/tsconfig.json` excludes
from the build — `packages/types/dist` is byte-identical to `main`'s. Same
form as the two sibling corrections to these pages, `.changeset/6132-*` and
`.changeset/6143-*`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
… boolean | string`
`ButtonGroupSchema` is the ONE of fourteen component schemas documenting a
schema-level `disabled` that does not redeclare it: the other thirteen narrow
it to `disabled?: boolean` themselves, so their pages are right and this one
was under-stating `BaseSchema`'s declared `boolean | string`.
Measured before acting, because the first reading looked like a repo-wide
convention worth leaving alone. It is a single-page divergence.
The pin now asserts the type TEXT of both inherited rows against the mirror
instead of their membership only, with its own union reader rather than a
misuse of `enumOptions` (whose contract is enum NAMES, not option schemas).
Whether `ButtonGroupSchema` should narrow `disabled` like its thirteen siblings
is a types question and is not answered here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…face pin
`@typescript-eslint/no-explicit-any` warned three times on the union reader.
Same shape as `@object-ui/test-support`'s `EnumCarrier`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3149.2 KB3191.4 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-CIIlTzAV.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)14.51KB5.35KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 17:12
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dispatching seat review (domain:ui, session session_013hfmP9hoMd3dJwTh85J4yB). Accepted, armed for auto-merge. Three of my dispatch premises were wrong and this PR corrected all three — recording them so the record shows the corrections came from the implementer, not from me.

⭐ The pin test is accepted, and the scope extension is earned

packages/types/src/__tests__/button-group-doc-surface-6347.test.ts is well outside the docs-only scope I set, and it belongs here. Its own justification is the right one: no CI gate reads a component page's plaintext interface fence — check-doc-component-types reads type string literals, check-doc-snippet-types compiles ts/tsx only — so a green run on this correction would have meant "nothing else broke", not "the correction is right."

Three design choices in it are better than what I asked for:

  • SET EQUALITY, not the subset I enumerated. Stated reason: "a one-directional pin is what let the omissions sit beside the corrections through two earlier passes." That is the defect class this card exists for, applied to the pin itself.
  • The authority is the mirror's .shape, never parse acceptance.BaseSchema is .passthrough() and carries [key: string]: any, so an undeclared selectionModeparses green — acceptance cannot distinguish "declared" from "admitted unexamined". Membership off .shape, optionality off each member's own safeParse(undefined), enum vocabularies through the shared enumOptions reader rather than a hand-copied list.
  • Reverse verification predicted the direction before running — 15 failed / 12 passed against the restored page, with "documents every member declared in its own right" correctly staying GREEN because the old page did name type/buttons/variant/size. A prediction that names which cases stay green is worth more than one that only names the reds.

My three errors

(a) I wrote that disabled? and className? were "✅ both fine".className is exact; disabled was mine to get wrong — BaseSchema declares boolean | string. What makes the catch worth recording is the method: the 14 component pages spelling it boolean look exactly like a house convention, and the PR measured past that appearance to find 13 of those 14 schemas redeclare disabled?: boolean themselves, leaving button-group — the one that does not redeclare — as the lone outlier. Testing an apparent convention instead of deferring to it is the whole difference here.

(b) I said the Selection Mode fixtures "author 5 undeclared keys between them". Measured: 29 occurrences of 4 undeclared keys across all six button-group fixtures (buttons[].value ×17, buttons[].icon ×8, value ×2, selectionMode ×2), including in sections that remain and still render. It does not change this diff; it makes #7077 materially wider than I scoped it, which is why #7077 now carries the real count.

(c) I instructed "ship patch" — and I gave that same wrong instruction on #6521 too.apps/site is "private": true, so content/docs/** publishes nothing and check-changeset-presence reports no changeset is owed; a patch bump would be a false claim that a released package changed. Both devs shipped the empty-frontmatter form independently and flagged the deviation rather than quietly following the order. The check is now standing for docs cards.

Your two open questions, answered

Q1 — changeset form: A, confirmed. Empty frontmatter. Your reasoning and the sibling precedent (6132-undeclared-action-props.md, 6143-components-schema-corrections.md) are correct, and the bump level is a factual claim about what gets released.

Q2 — narrow ButtonGroupSchema.disabled to boolean like its 13 siblings: A for this PR. Your recommendation stands. B is a published-type narrowing, which is not a docs card's to make regardless of how clean the 13-of-14 shape looks. If B is wanted it needs its own card routed through the contract path, with the accept-set delta stated — ⛔ not folded into a docs correction. The context you recorded is enough for whoever picks that up.

The refusal I want on the record

You declined to file the sonner fixture-id residue I flagged, because you had not re-opened any sonner file and would not file on my reading. That is correct and it is the harder call — my dispatch invited it, and agreeing without your own measurement would have laundered my reading into a card. Cards should carry their filer's measurement, not the dispatcher's.

Auto-merge (squash) armed at 0f49c91c7; CI green.


Generated by Claude Code

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.

docs(components): sonner and button-group still document four more props no shipped type declares

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

docs(components): publish button-group's shipped surface, in both directions - #7078

Merged
os-sam merged 4 commits into
mainfrom
claude/issue-6347-button-group-docs
Aug 31, 2026
Merged

docs(components): publish button-group's shipped surface, in both directions#7078
os-sam merged 4 commits into
mainfrom
claude/issue-6347-button-group-docs

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Closes#6347

Docs only, publishes nothing. content/docs/components/basic/button-group.mdx published a ButtonGroupSchema / ButtonGroupButton surface that disagreed with the shipped types. The card named two rows; re-measuring the whole fence found eleven, in both directions.

The sonner half of #6347 was already landed under PR #6498 / #6250 and is not in this diff — no components-feedback-sonner file is touched. Re-verified before starting: the duration row is gone, the section is "With Description", and toast-with-action.json is a real sonner node.

Every row changed

Over-stated — documented, never declared:

page beforeshipped declarationpage after
ButtonGroupSchema.value?: string | string[]not declared — navigation.ts:335-351 declares only type, buttons, variant, size (mirror zod/navigation.zod.ts:154-159)removed
ButtonGroupSchema.selectionMode?: 'single' | 'multiple' | 'none'not declared — same two blocksremoved
ButtonGroupButton.value: string (required)not declared — navigation.ts:305-330 (mirror zod/navigation.zod.ts:142-149)removed
ButtonGroupButton.icon?: stringnot declared — same two blocksremoved
buttons: ButtonGroupButton[] (required)buttons?: ButtonGroupButton[]navigation.ts:340, mirror .zod.ts:156buttons?:
label?: stringlabel: string — required, navigation.ts:309, mirror .zod.ts:143label:

Under-stated — declared, never documented:

page beforeshipped declarationpage after
no ButtonGroupButton.variant rowvariant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link'navigation.ts:313, .zod.ts:144added
no ButtonGroupButton.size rowsize?: 'default' | 'sm' | 'lg' | 'icon'navigation.ts:317, .zod.ts:145added
no ButtonGroupButton.onClick rowonClick?: () = a void function — navigation.ts:325, .zod.ts:147 (z.function().optional())added, annotated "not authorable in JSON"
no ButtonGroupButton.className rowclassName?: stringnavigation.ts:329, .zod.ts:148added
ButtonGroupSchema.variant?: 'default' | 'outline' | 'ghost'six members — navigation.ts:345, .zod.ts:157secondary, destructive, link restored
ButtonGroupSchema.size?: 'sm' | 'default' | 'lg'four members — navigation.ts:350, .zod.ts:158icon restored
disabled?: booleanBaseSchema.disabled?: boolean | stringbase.ts, mirror zod/base.zod.ts:190 (z.union([z.boolean(), z.string()])); ButtonGroupSchema does not redeclare itboolean | string

className?: string was already correct and is unchanged.

The disabled row is the one to look at twice

It reads at first like a repo-wide convention worth leaving alone: 14 content/docs/components/** pages spell a component schema's own disabled as boolean. It is not. Thirteen of those fourteen schemas redeclare disabled?: boolean themselvesButtonSchema, InputSchema, SelectSchema, SwitchSchema, TextareaSchema, CheckboxSchema, RadioGroupSchema, ComboboxSchema, CalendarSchema, DatePickerSchema, FileUploadSchema, InputOTPSchema, ToggleGroupSchema — so their pages are right. ButtonGroupSchema is the one that does not, so it inherits BaseSchema's boolean | string and this page was the outlier rather than the convention.

Whether ButtonGroupSchemashould narrow it like its thirteen siblings is a types question, not a docs one, and is deliberately not answered here: the renderer reads neither spelling today.

## Selection Mode is deleted — my independent verdict on the fork clause

Triage's ruling said a documented-but-undeclared key that a renderer nonetheless READS is a fork to report, not a docs fix. I measured it myself rather than inheriting the dispatch's reading. Neither key is read, so no fork; the docs fix stands.

What I measured in packages/components/src/renderers/basic/button-group.tsx:

  • The renderer reads exactly: schema.className, schema.buttons, schema.variant, schema.size, button.variant, button.size, button.className, button.label. Nothing else reaches the DOM.
  • selectionMode appears nowhere in the file. Repo-wide, the only other selectionMode occurrences are local variables in data-table.tsx and plugin-grid/ObjectGrid.tsx (computed from selection.type / selectable, a different schema surface), one retired-key list in packages/i18n, and the two catalog fixtures themselves.
  • schema.value is never read here. Every schema.value read under packages/components/src/renderers/** belongs to a different component type (statistic, progress, textarea, input-otp, combobox, calendar, input, select, tabs, filter-builder, toggle-group, text, span).
  • button.icon is never read either — the only icon token in the file is the 'icon' member of the size enum.
  • Only one registry entry exists for the type: ComponentRegistry.register('button-group', ...) in that file. custom/button-group.tsx and plugin-chatbot/src/elements/ui/button-group.tsx are Shadcn primitives taking a React orientation prop, not readers of this schema.

So the heading promised a capability nothing draws, over two demos that render as inert button rows. Per #6249's honesty precedent it is deleted rather than softened to a caveat.

The two catalog fixtures stay — and the gate measurement that says they can

components-basic-button-group/single-selection and /multiple-selection are untouched. Removing the section orphans them from this page; measured, no gate fails on that:

  • The catalog index is generated from the schema directory, keyed by (category, slug)scripts/regenerate-catalog-index.py, pinned by scripts/__tests__/catalog-index-regenerable-4633.test.ts. It never reads a docs page.
  • Nothing in scripts/**, apps/** or packages/** validates SchemaExample id references in either direction. extract-mdx-demos.mjs is a one-way migration tool run by hand.
  • examples/schema-catalog/test/catalog-gallery-render.test.tsx renders every entry and fails only on an unregistered type; both fixtures are still type: "button-group".
  • No inbound link or anchor points at the removed section: docs:check-links is green, and the only other "Selection Mode" heading in the repo belongs to form/calendar.mdx.

The pin below asserts both fixtures are still on disk, so a later tidy-up cannot quietly delete them to silence something.

The pin, and why a docs correction needs one

packages/types/src/__tests__/button-group-doc-surface-6347.test.ts — 29 assertions.

Nothing in CI reads a plaintext fence. check:doc-types reads only the type string literals out of docs code blocks; check:doc-snippets compiles ts/tsx fences and a plaintext block is not one. So a member row can name any key at all and every gate stays green — the same hole component-fixture-declared-keys.test.ts records one surface over. Without this file a green CI run would say "nothing else broke", not "the correction is right".

It asserts set equality on the component's own members, not the subset the card enumerated — a one-directional pin is precisely what left these omissions sitting beside two earlier corrections to this page (#6132, #6143). Membership is read off the mirror's .shape, never off parse acceptance: BaseSchema is .passthrough() and carries an index signature, so an undeclared selectionMode parses green and type-checks, and acceptance cannot tell "declared" from "admitted unexamined".

It also pins the renderer half of the deletion, so whoever implements selection sees this file go red and knows the page owes a section again.

Verification

Union run on final commit 0f49c91c7.

commandresult
pnpm exec vitest run packages/types/Test Files 80 passed (80) · Tests 998 passed (998)
pnpm --filter @object-ui/types type-checkexit 0; --listFiles confirms the new test file is in the tsconfig.test.json program (1 hit)
pnpm check:doc-typesEvery documented component type is registered.
pnpm check:doc-fencesevery TypeScript block in 224 document(s) is fenced ts/tsx/typescript, except 80 declared file(s) carrying 90 block(s) — the page's declared entry is 1 and it still holds exactly 1 plaintext fence
pnpm check:control-bytesOK (scanned 5862 tracked text file(s); skipped 85 binary)
pnpm check:docs-route-closure1360 modules crawled from 149 route roots (145 MDX), every specifier resolved
pnpm docs:check-linksLinks are valid across 17 scan roots.
node scripts/check-changeset-no-major.mjsNo changeset declares a major bump.
node scripts/check-changeset-fixed.mjsAll workspace packages are in the changeset fixed group.
node scripts/check-type-check-coverage.mjs45/46 via type-check · 41/41 packages compile their tests
node scripts/check-lint-coverage.mjs46/46 packages linted, 0 with outstanding errors
pnpm exec eslint <the new test file>1 file, 0 errors, 0 warnings

Reverse verification, direction predicted before running. With the fix committed and the page restored to 592acafbe, the pin goes 15 failed / 12 passed — set equality, both enum vocabularies on both blocks, label and buttons optionality, the onClick annotation, the undeclared-member check and the heading check all red, exactly as predicted; "documents every member declared in its own right" stayed green, because the old page did name type/buttons/variant/size. A targeted second ablation on the disabled row alone went 1 failed / 28 passed, and the mutated blob hashed identically to the pre-fix commit's blob. Both restores were proven by git diff HEAD empty plus a blob-hash comparison against HEAD, not by an exit code.

Two declared narrowings.

check:doc-snippets was NOT run: it exits 2 PRECONDITION NOT MET until all 21 package dists are built, and that build is a repo-scale run. Evidence the narrowing costs nothing: (1) the gate's population is ts/tsx/typescript fences by its own contract, which check:doc-fences independently reports on and which ran green here; (2) counted with grep -cE, this page carries 0 ts/tsx/typescript fences before and after and exactly 1plaintext fence before and after, matching its declared entry in check-doc-fence-languages.mjs; (3) packages/types/tsconfig.json excludes **/*.test.ts from the build, so packages/types/dist is byte-identical to main's and no other document's compile input moved.

pnpm lint (repo-wide) was narrowed to the changed files. Evidence: (1) the population is eslint's own — asked directly, it reports File ignored because no matching configuration was supplied. for both the .mdx and the .md changeset, so the lintable population of this diff is one file; (2) counted from --format json: 1 file, 0 errors, 0 warnings; (3) eslint.config.js declares no parserOptions.project / projectService, so linting is not type-aware and this diff cannot move any untouched file's verdict.

Filed, not fixed here

#7077button-group implements no selection, and never wires the per-button onClick or disabled it does declare; the six catalog fixtures author 29 occurrences of four undeclared keys (buttons[].value 17, buttons[].icon 8, value 2, selectionMode 2). The card states the fork (implement, or retire the authored surface under ADR-0049) and does not choose. It cross-references #5931, which already covers with-icons.json's icon pair for this component alongside breadcrumb and command.

One correction to the dispatch's premises

The dispatch measured "the section's two fixtures ... five undeclared keys between them". Counted per fixture, it is 29 occurrences across all six, including the three fixtures the sections that STAY still render. That does not change this diff — no fixture is edited — but it makes #7077 materially wider than "two demos".


Generated by Claude Code

…ions
`content/docs/components/basic/button-group.mdx` taught ten rows that
disagreed with `packages/types/src/navigation.ts` and its Zod mirror.
Over-stated (documented, never declared): `ButtonGroupSchema.value`,
`ButtonGroupSchema.selectionMode`, `ButtonGroupButton.value`,
`ButtonGroupButton.icon`; `buttons` spelled required against a declared
`buttons?`; `label?` spelled optional against a declared required `label`.
Under-stated (declared, never documented): `ButtonGroupButton.variant`,
`.size`, `.onClick`, `.className`; three `variant` members (`secondary`,
`destructive`, `link`) and one `size` member (`icon`).
The `## Selection Mode` section is deleted: the renderer implements no
selection behaviour at all, so the heading promised a capability nothing
draws. Its two catalog fixtures stay — a separate verification population.
`button-group-doc-surface-6347.test.ts` pins the corrections. No gate reads
a `plaintext` fence, so without it a green CI run says only that nothing
else broke.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Empty frontmatter: the diff publishes nothing. `content/docs/**` belongs to
`@object-ui/site` (in the changeset `ignore` list) and the pin lives in
`packages/types/src/__tests__/`, which `packages/types/tsconfig.json` excludes
from the build — `packages/types/dist` is byte-identical to `main`'s. Same
form as the two sibling corrections to these pages, `.changeset/6132-*` and
`.changeset/6143-*`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
… boolean | string`
`ButtonGroupSchema` is the ONE of fourteen component schemas documenting a
schema-level `disabled` that does not redeclare it: the other thirteen narrow
it to `disabled?: boolean` themselves, so their pages are right and this one
was under-stating `BaseSchema`'s declared `boolean | string`.
Measured before acting, because the first reading looked like a repo-wide
convention worth leaving alone. It is a single-page divergence.
The pin now asserts the type TEXT of both inherited rows against the mirror
instead of their membership only, with its own union reader rather than a
misuse of `enumOptions` (whose contract is enum NAMES, not option schemas).
Whether `ButtonGroupSchema` should narrow `disabled` like its thirteen siblings
is a types question and is not answered here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…face pin
`@typescript-eslint/no-explicit-any` warned three times on the union reader.
Same shape as `@object-ui/test-support`'s `EnumCarrier`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3149.2 KB3191.4 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-CIIlTzAV.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)14.51KB5.35KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 17:12
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dispatching seat review (domain:ui, session session_013hfmP9hoMd3dJwTh85J4yB). Accepted, armed for auto-merge. Three of my dispatch premises were wrong and this PR corrected all three — recording them so the record shows the corrections came from the implementer, not from me.

⭐ The pin test is accepted, and the scope extension is earned

packages/types/src/__tests__/button-group-doc-surface-6347.test.ts is well outside the docs-only scope I set, and it belongs here. Its own justification is the right one: no CI gate reads a component page's plaintext interface fence — check-doc-component-types reads type string literals, check-doc-snippet-types compiles ts/tsx only — so a green run on this correction would have meant "nothing else broke", not "the correction is right."

Three design choices in it are better than what I asked for:

  • SET EQUALITY, not the subset I enumerated. Stated reason: "a one-directional pin is what let the omissions sit beside the corrections through two earlier passes." That is the defect class this card exists for, applied to the pin itself.
  • The authority is the mirror's .shape, never parse acceptance.BaseSchema is .passthrough() and carries [key: string]: any, so an undeclared selectionModeparses green — acceptance cannot distinguish "declared" from "admitted unexamined". Membership off .shape, optionality off each member's own safeParse(undefined), enum vocabularies through the shared enumOptions reader rather than a hand-copied list.
  • Reverse verification predicted the direction before running — 15 failed / 12 passed against the restored page, with "documents every member declared in its own right" correctly staying GREEN because the old page did name type/buttons/variant/size. A prediction that names which cases stay green is worth more than one that only names the reds.

My three errors

(a) I wrote that disabled? and className? were "✅ both fine".className is exact; disabled was mine to get wrong — BaseSchema declares boolean | string. What makes the catch worth recording is the method: the 14 component pages spelling it boolean look exactly like a house convention, and the PR measured past that appearance to find 13 of those 14 schemas redeclare disabled?: boolean themselves, leaving button-group — the one that does not redeclare — as the lone outlier. Testing an apparent convention instead of deferring to it is the whole difference here.

(b) I said the Selection Mode fixtures "author 5 undeclared keys between them". Measured: 29 occurrences of 4 undeclared keys across all six button-group fixtures (buttons[].value ×17, buttons[].icon ×8, value ×2, selectionMode ×2), including in sections that remain and still render. It does not change this diff; it makes #7077 materially wider than I scoped it, which is why #7077 now carries the real count.

(c) I instructed "ship patch" — and I gave that same wrong instruction on #6521 too.apps/site is "private": true, so content/docs/** publishes nothing and check-changeset-presence reports no changeset is owed; a patch bump would be a false claim that a released package changed. Both devs shipped the empty-frontmatter form independently and flagged the deviation rather than quietly following the order. The check is now standing for docs cards.

Your two open questions, answered

Q1 — changeset form: A, confirmed. Empty frontmatter. Your reasoning and the sibling precedent (6132-undeclared-action-props.md, 6143-components-schema-corrections.md) are correct, and the bump level is a factual claim about what gets released.

Q2 — narrow ButtonGroupSchema.disabled to boolean like its 13 siblings: A for this PR. Your recommendation stands. B is a published-type narrowing, which is not a docs card's to make regardless of how clean the 13-of-14 shape looks. If B is wanted it needs its own card routed through the contract path, with the accept-set delta stated — ⛔ not folded into a docs correction. The context you recorded is enough for whoever picks that up.

The refusal I want on the record

You declined to file the sonner fixture-id residue I flagged, because you had not re-opened any sonner file and would not file on my reading. That is correct and it is the harder call — my dispatch invited it, and agreeing without your own measurement would have laundered my reading into a card. Cards should carry their filer's measurement, not the dispatcher's.

Auto-merge (squash) armed at 0f49c91c7; CI green.


Generated by Claude Code

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.

docs(components): sonner and button-group still document four more props no shipped type declares

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

docs(components): publish button-group's shipped surface, in both directions - #7078

Merged
os-sam merged 4 commits into
mainfrom
claude/issue-6347-button-group-docs
Aug 31, 2026
Merged

docs(components): publish button-group's shipped surface, in both directions#7078
os-sam merged 4 commits into
mainfrom
claude/issue-6347-button-group-docs

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Closes#6347

Docs only, publishes nothing. content/docs/components/basic/button-group.mdx published a ButtonGroupSchema / ButtonGroupButton surface that disagreed with the shipped types. The card named two rows; re-measuring the whole fence found eleven, in both directions.

The sonner half of #6347 was already landed under PR #6498 / #6250 and is not in this diff — no components-feedback-sonner file is touched. Re-verified before starting: the duration row is gone, the section is "With Description", and toast-with-action.json is a real sonner node.

Every row changed

Over-stated — documented, never declared:

page beforeshipped declarationpage after
ButtonGroupSchema.value?: string | string[]not declared — navigation.ts:335-351 declares only type, buttons, variant, size (mirror zod/navigation.zod.ts:154-159)removed
ButtonGroupSchema.selectionMode?: 'single' | 'multiple' | 'none'not declared — same two blocksremoved
ButtonGroupButton.value: string (required)not declared — navigation.ts:305-330 (mirror zod/navigation.zod.ts:142-149)removed
ButtonGroupButton.icon?: stringnot declared — same two blocksremoved
buttons: ButtonGroupButton[] (required)buttons?: ButtonGroupButton[]navigation.ts:340, mirror .zod.ts:156buttons?:
label?: stringlabel: string — required, navigation.ts:309, mirror .zod.ts:143label:

Under-stated — declared, never documented:

page beforeshipped declarationpage after
no ButtonGroupButton.variant rowvariant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link'navigation.ts:313, .zod.ts:144added
no ButtonGroupButton.size rowsize?: 'default' | 'sm' | 'lg' | 'icon'navigation.ts:317, .zod.ts:145added
no ButtonGroupButton.onClick rowonClick?: () = a void function — navigation.ts:325, .zod.ts:147 (z.function().optional())added, annotated "not authorable in JSON"
no ButtonGroupButton.className rowclassName?: stringnavigation.ts:329, .zod.ts:148added
ButtonGroupSchema.variant?: 'default' | 'outline' | 'ghost'six members — navigation.ts:345, .zod.ts:157secondary, destructive, link restored
ButtonGroupSchema.size?: 'sm' | 'default' | 'lg'four members — navigation.ts:350, .zod.ts:158icon restored
disabled?: booleanBaseSchema.disabled?: boolean | stringbase.ts, mirror zod/base.zod.ts:190 (z.union([z.boolean(), z.string()])); ButtonGroupSchema does not redeclare itboolean | string

className?: string was already correct and is unchanged.

The disabled row is the one to look at twice

It reads at first like a repo-wide convention worth leaving alone: 14 content/docs/components/** pages spell a component schema's own disabled as boolean. It is not. Thirteen of those fourteen schemas redeclare disabled?: boolean themselvesButtonSchema, InputSchema, SelectSchema, SwitchSchema, TextareaSchema, CheckboxSchema, RadioGroupSchema, ComboboxSchema, CalendarSchema, DatePickerSchema, FileUploadSchema, InputOTPSchema, ToggleGroupSchema — so their pages are right. ButtonGroupSchema is the one that does not, so it inherits BaseSchema's boolean | string and this page was the outlier rather than the convention.

Whether ButtonGroupSchemashould narrow it like its thirteen siblings is a types question, not a docs one, and is deliberately not answered here: the renderer reads neither spelling today.

## Selection Mode is deleted — my independent verdict on the fork clause

Triage's ruling said a documented-but-undeclared key that a renderer nonetheless READS is a fork to report, not a docs fix. I measured it myself rather than inheriting the dispatch's reading. Neither key is read, so no fork; the docs fix stands.

What I measured in packages/components/src/renderers/basic/button-group.tsx:

  • The renderer reads exactly: schema.className, schema.buttons, schema.variant, schema.size, button.variant, button.size, button.className, button.label. Nothing else reaches the DOM.
  • selectionMode appears nowhere in the file. Repo-wide, the only other selectionMode occurrences are local variables in data-table.tsx and plugin-grid/ObjectGrid.tsx (computed from selection.type / selectable, a different schema surface), one retired-key list in packages/i18n, and the two catalog fixtures themselves.
  • schema.value is never read here. Every schema.value read under packages/components/src/renderers/** belongs to a different component type (statistic, progress, textarea, input-otp, combobox, calendar, input, select, tabs, filter-builder, toggle-group, text, span).
  • button.icon is never read either — the only icon token in the file is the 'icon' member of the size enum.
  • Only one registry entry exists for the type: ComponentRegistry.register('button-group', ...) in that file. custom/button-group.tsx and plugin-chatbot/src/elements/ui/button-group.tsx are Shadcn primitives taking a React orientation prop, not readers of this schema.

So the heading promised a capability nothing draws, over two demos that render as inert button rows. Per #6249's honesty precedent it is deleted rather than softened to a caveat.

The two catalog fixtures stay — and the gate measurement that says they can

components-basic-button-group/single-selection and /multiple-selection are untouched. Removing the section orphans them from this page; measured, no gate fails on that:

  • The catalog index is generated from the schema directory, keyed by (category, slug)scripts/regenerate-catalog-index.py, pinned by scripts/__tests__/catalog-index-regenerable-4633.test.ts. It never reads a docs page.
  • Nothing in scripts/**, apps/** or packages/** validates SchemaExample id references in either direction. extract-mdx-demos.mjs is a one-way migration tool run by hand.
  • examples/schema-catalog/test/catalog-gallery-render.test.tsx renders every entry and fails only on an unregistered type; both fixtures are still type: "button-group".
  • No inbound link or anchor points at the removed section: docs:check-links is green, and the only other "Selection Mode" heading in the repo belongs to form/calendar.mdx.

The pin below asserts both fixtures are still on disk, so a later tidy-up cannot quietly delete them to silence something.

The pin, and why a docs correction needs one

packages/types/src/__tests__/button-group-doc-surface-6347.test.ts — 29 assertions.

Nothing in CI reads a plaintext fence. check:doc-types reads only the type string literals out of docs code blocks; check:doc-snippets compiles ts/tsx fences and a plaintext block is not one. So a member row can name any key at all and every gate stays green — the same hole component-fixture-declared-keys.test.ts records one surface over. Without this file a green CI run would say "nothing else broke", not "the correction is right".

It asserts set equality on the component's own members, not the subset the card enumerated — a one-directional pin is precisely what left these omissions sitting beside two earlier corrections to this page (#6132, #6143). Membership is read off the mirror's .shape, never off parse acceptance: BaseSchema is .passthrough() and carries an index signature, so an undeclared selectionMode parses green and type-checks, and acceptance cannot tell "declared" from "admitted unexamined".

It also pins the renderer half of the deletion, so whoever implements selection sees this file go red and knows the page owes a section again.

Verification

Union run on final commit 0f49c91c7.

commandresult
pnpm exec vitest run packages/types/Test Files 80 passed (80) · Tests 998 passed (998)
pnpm --filter @object-ui/types type-checkexit 0; --listFiles confirms the new test file is in the tsconfig.test.json program (1 hit)
pnpm check:doc-typesEvery documented component type is registered.
pnpm check:doc-fencesevery TypeScript block in 224 document(s) is fenced ts/tsx/typescript, except 80 declared file(s) carrying 90 block(s) — the page's declared entry is 1 and it still holds exactly 1 plaintext fence
pnpm check:control-bytesOK (scanned 5862 tracked text file(s); skipped 85 binary)
pnpm check:docs-route-closure1360 modules crawled from 149 route roots (145 MDX), every specifier resolved
pnpm docs:check-linksLinks are valid across 17 scan roots.
node scripts/check-changeset-no-major.mjsNo changeset declares a major bump.
node scripts/check-changeset-fixed.mjsAll workspace packages are in the changeset fixed group.
node scripts/check-type-check-coverage.mjs45/46 via type-check · 41/41 packages compile their tests
node scripts/check-lint-coverage.mjs46/46 packages linted, 0 with outstanding errors
pnpm exec eslint <the new test file>1 file, 0 errors, 0 warnings

Reverse verification, direction predicted before running. With the fix committed and the page restored to 592acafbe, the pin goes 15 failed / 12 passed — set equality, both enum vocabularies on both blocks, label and buttons optionality, the onClick annotation, the undeclared-member check and the heading check all red, exactly as predicted; "documents every member declared in its own right" stayed green, because the old page did name type/buttons/variant/size. A targeted second ablation on the disabled row alone went 1 failed / 28 passed, and the mutated blob hashed identically to the pre-fix commit's blob. Both restores were proven by git diff HEAD empty plus a blob-hash comparison against HEAD, not by an exit code.

Two declared narrowings.

check:doc-snippets was NOT run: it exits 2 PRECONDITION NOT MET until all 21 package dists are built, and that build is a repo-scale run. Evidence the narrowing costs nothing: (1) the gate's population is ts/tsx/typescript fences by its own contract, which check:doc-fences independently reports on and which ran green here; (2) counted with grep -cE, this page carries 0 ts/tsx/typescript fences before and after and exactly 1plaintext fence before and after, matching its declared entry in check-doc-fence-languages.mjs; (3) packages/types/tsconfig.json excludes **/*.test.ts from the build, so packages/types/dist is byte-identical to main's and no other document's compile input moved.

pnpm lint (repo-wide) was narrowed to the changed files. Evidence: (1) the population is eslint's own — asked directly, it reports File ignored because no matching configuration was supplied. for both the .mdx and the .md changeset, so the lintable population of this diff is one file; (2) counted from --format json: 1 file, 0 errors, 0 warnings; (3) eslint.config.js declares no parserOptions.project / projectService, so linting is not type-aware and this diff cannot move any untouched file's verdict.

Filed, not fixed here

#7077button-group implements no selection, and never wires the per-button onClick or disabled it does declare; the six catalog fixtures author 29 occurrences of four undeclared keys (buttons[].value 17, buttons[].icon 8, value 2, selectionMode 2). The card states the fork (implement, or retire the authored surface under ADR-0049) and does not choose. It cross-references #5931, which already covers with-icons.json's icon pair for this component alongside breadcrumb and command.

One correction to the dispatch's premises

The dispatch measured "the section's two fixtures ... five undeclared keys between them". Counted per fixture, it is 29 occurrences across all six, including the three fixtures the sections that STAY still render. That does not change this diff — no fixture is edited — but it makes #7077 materially wider than "two demos".


Generated by Claude Code

…ions
`content/docs/components/basic/button-group.mdx` taught ten rows that
disagreed with `packages/types/src/navigation.ts` and its Zod mirror.
Over-stated (documented, never declared): `ButtonGroupSchema.value`,
`ButtonGroupSchema.selectionMode`, `ButtonGroupButton.value`,
`ButtonGroupButton.icon`; `buttons` spelled required against a declared
`buttons?`; `label?` spelled optional against a declared required `label`.
Under-stated (declared, never documented): `ButtonGroupButton.variant`,
`.size`, `.onClick`, `.className`; three `variant` members (`secondary`,
`destructive`, `link`) and one `size` member (`icon`).
The `## Selection Mode` section is deleted: the renderer implements no
selection behaviour at all, so the heading promised a capability nothing
draws. Its two catalog fixtures stay — a separate verification population.
`button-group-doc-surface-6347.test.ts` pins the corrections. No gate reads
a `plaintext` fence, so without it a green CI run says only that nothing
else broke.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Empty frontmatter: the diff publishes nothing. `content/docs/**` belongs to
`@object-ui/site` (in the changeset `ignore` list) and the pin lives in
`packages/types/src/__tests__/`, which `packages/types/tsconfig.json` excludes
from the build — `packages/types/dist` is byte-identical to `main`'s. Same
form as the two sibling corrections to these pages, `.changeset/6132-*` and
`.changeset/6143-*`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
… boolean | string`
`ButtonGroupSchema` is the ONE of fourteen component schemas documenting a
schema-level `disabled` that does not redeclare it: the other thirteen narrow
it to `disabled?: boolean` themselves, so their pages are right and this one
was under-stating `BaseSchema`'s declared `boolean | string`.
Measured before acting, because the first reading looked like a repo-wide
convention worth leaving alone. It is a single-page divergence.
The pin now asserts the type TEXT of both inherited rows against the mirror
instead of their membership only, with its own union reader rather than a
misuse of `enumOptions` (whose contract is enum NAMES, not option schemas).
Whether `ButtonGroupSchema` should narrow `disabled` like its thirteen siblings
is a types question and is not answered here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…face pin
`@typescript-eslint/no-explicit-any` warned three times on the union reader.
Same shape as `@object-ui/test-support`'s `EnumCarrier`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3149.2 KB3191.4 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-CIIlTzAV.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)14.51KB5.35KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 17:12
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dispatching seat review (domain:ui, session session_013hfmP9hoMd3dJwTh85J4yB). Accepted, armed for auto-merge. Three of my dispatch premises were wrong and this PR corrected all three — recording them so the record shows the corrections came from the implementer, not from me.

⭐ The pin test is accepted, and the scope extension is earned

packages/types/src/__tests__/button-group-doc-surface-6347.test.ts is well outside the docs-only scope I set, and it belongs here. Its own justification is the right one: no CI gate reads a component page's plaintext interface fence — check-doc-component-types reads type string literals, check-doc-snippet-types compiles ts/tsx only — so a green run on this correction would have meant "nothing else broke", not "the correction is right."

Three design choices in it are better than what I asked for:

  • SET EQUALITY, not the subset I enumerated. Stated reason: "a one-directional pin is what let the omissions sit beside the corrections through two earlier passes." That is the defect class this card exists for, applied to the pin itself.
  • The authority is the mirror's .shape, never parse acceptance.BaseSchema is .passthrough() and carries [key: string]: any, so an undeclared selectionModeparses green — acceptance cannot distinguish "declared" from "admitted unexamined". Membership off .shape, optionality off each member's own safeParse(undefined), enum vocabularies through the shared enumOptions reader rather than a hand-copied list.
  • Reverse verification predicted the direction before running — 15 failed / 12 passed against the restored page, with "documents every member declared in its own right" correctly staying GREEN because the old page did name type/buttons/variant/size. A prediction that names which cases stay green is worth more than one that only names the reds.

My three errors

(a) I wrote that disabled? and className? were "✅ both fine".className is exact; disabled was mine to get wrong — BaseSchema declares boolean | string. What makes the catch worth recording is the method: the 14 component pages spelling it boolean look exactly like a house convention, and the PR measured past that appearance to find 13 of those 14 schemas redeclare disabled?: boolean themselves, leaving button-group — the one that does not redeclare — as the lone outlier. Testing an apparent convention instead of deferring to it is the whole difference here.

(b) I said the Selection Mode fixtures "author 5 undeclared keys between them". Measured: 29 occurrences of 4 undeclared keys across all six button-group fixtures (buttons[].value ×17, buttons[].icon ×8, value ×2, selectionMode ×2), including in sections that remain and still render. It does not change this diff; it makes #7077 materially wider than I scoped it, which is why #7077 now carries the real count.

(c) I instructed "ship patch" — and I gave that same wrong instruction on #6521 too.apps/site is "private": true, so content/docs/** publishes nothing and check-changeset-presence reports no changeset is owed; a patch bump would be a false claim that a released package changed. Both devs shipped the empty-frontmatter form independently and flagged the deviation rather than quietly following the order. The check is now standing for docs cards.

Your two open questions, answered

Q1 — changeset form: A, confirmed. Empty frontmatter. Your reasoning and the sibling precedent (6132-undeclared-action-props.md, 6143-components-schema-corrections.md) are correct, and the bump level is a factual claim about what gets released.

Q2 — narrow ButtonGroupSchema.disabled to boolean like its 13 siblings: A for this PR. Your recommendation stands. B is a published-type narrowing, which is not a docs card's to make regardless of how clean the 13-of-14 shape looks. If B is wanted it needs its own card routed through the contract path, with the accept-set delta stated — ⛔ not folded into a docs correction. The context you recorded is enough for whoever picks that up.

The refusal I want on the record

You declined to file the sonner fixture-id residue I flagged, because you had not re-opened any sonner file and would not file on my reading. That is correct and it is the harder call — my dispatch invited it, and agreeing without your own measurement would have laundered my reading into a card. Cards should carry their filer's measurement, not the dispatcher's.

Auto-merge (squash) armed at 0f49c91c7; CI green.


Generated by Claude Code

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.

docs(components): sonner and button-group still document four more props no shipped type declares

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

docs(components): publish button-group's shipped surface, in both directions - #7078

Merged
os-sam merged 4 commits into
mainfrom
claude/issue-6347-button-group-docs
Aug 31, 2026
Merged

docs(components): publish button-group's shipped surface, in both directions#7078
os-sam merged 4 commits into
mainfrom
claude/issue-6347-button-group-docs

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Closes#6347

Docs only, publishes nothing. content/docs/components/basic/button-group.mdx published a ButtonGroupSchema / ButtonGroupButton surface that disagreed with the shipped types. The card named two rows; re-measuring the whole fence found eleven, in both directions.

The sonner half of #6347 was already landed under PR #6498 / #6250 and is not in this diff — no components-feedback-sonner file is touched. Re-verified before starting: the duration row is gone, the section is "With Description", and toast-with-action.json is a real sonner node.

Every row changed

Over-stated — documented, never declared:

page beforeshipped declarationpage after
ButtonGroupSchema.value?: string | string[]not declared — navigation.ts:335-351 declares only type, buttons, variant, size (mirror zod/navigation.zod.ts:154-159)removed
ButtonGroupSchema.selectionMode?: 'single' | 'multiple' | 'none'not declared — same two blocksremoved
ButtonGroupButton.value: string (required)not declared — navigation.ts:305-330 (mirror zod/navigation.zod.ts:142-149)removed
ButtonGroupButton.icon?: stringnot declared — same two blocksremoved
buttons: ButtonGroupButton[] (required)buttons?: ButtonGroupButton[]navigation.ts:340, mirror .zod.ts:156buttons?:
label?: stringlabel: string — required, navigation.ts:309, mirror .zod.ts:143label:

Under-stated — declared, never documented:

page beforeshipped declarationpage after
no ButtonGroupButton.variant rowvariant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link'navigation.ts:313, .zod.ts:144added
no ButtonGroupButton.size rowsize?: 'default' | 'sm' | 'lg' | 'icon'navigation.ts:317, .zod.ts:145added
no ButtonGroupButton.onClick rowonClick?: () = a void function — navigation.ts:325, .zod.ts:147 (z.function().optional())added, annotated "not authorable in JSON"
no ButtonGroupButton.className rowclassName?: stringnavigation.ts:329, .zod.ts:148added
ButtonGroupSchema.variant?: 'default' | 'outline' | 'ghost'six members — navigation.ts:345, .zod.ts:157secondary, destructive, link restored
ButtonGroupSchema.size?: 'sm' | 'default' | 'lg'four members — navigation.ts:350, .zod.ts:158icon restored
disabled?: booleanBaseSchema.disabled?: boolean | stringbase.ts, mirror zod/base.zod.ts:190 (z.union([z.boolean(), z.string()])); ButtonGroupSchema does not redeclare itboolean | string

className?: string was already correct and is unchanged.

The disabled row is the one to look at twice

It reads at first like a repo-wide convention worth leaving alone: 14 content/docs/components/** pages spell a component schema's own disabled as boolean. It is not. Thirteen of those fourteen schemas redeclare disabled?: boolean themselvesButtonSchema, InputSchema, SelectSchema, SwitchSchema, TextareaSchema, CheckboxSchema, RadioGroupSchema, ComboboxSchema, CalendarSchema, DatePickerSchema, FileUploadSchema, InputOTPSchema, ToggleGroupSchema — so their pages are right. ButtonGroupSchema is the one that does not, so it inherits BaseSchema's boolean | string and this page was the outlier rather than the convention.

Whether ButtonGroupSchemashould narrow it like its thirteen siblings is a types question, not a docs one, and is deliberately not answered here: the renderer reads neither spelling today.

## Selection Mode is deleted — my independent verdict on the fork clause

Triage's ruling said a documented-but-undeclared key that a renderer nonetheless READS is a fork to report, not a docs fix. I measured it myself rather than inheriting the dispatch's reading. Neither key is read, so no fork; the docs fix stands.

What I measured in packages/components/src/renderers/basic/button-group.tsx:

  • The renderer reads exactly: schema.className, schema.buttons, schema.variant, schema.size, button.variant, button.size, button.className, button.label. Nothing else reaches the DOM.
  • selectionMode appears nowhere in the file. Repo-wide, the only other selectionMode occurrences are local variables in data-table.tsx and plugin-grid/ObjectGrid.tsx (computed from selection.type / selectable, a different schema surface), one retired-key list in packages/i18n, and the two catalog fixtures themselves.
  • schema.value is never read here. Every schema.value read under packages/components/src/renderers/** belongs to a different component type (statistic, progress, textarea, input-otp, combobox, calendar, input, select, tabs, filter-builder, toggle-group, text, span).
  • button.icon is never read either — the only icon token in the file is the 'icon' member of the size enum.
  • Only one registry entry exists for the type: ComponentRegistry.register('button-group', ...) in that file. custom/button-group.tsx and plugin-chatbot/src/elements/ui/button-group.tsx are Shadcn primitives taking a React orientation prop, not readers of this schema.

So the heading promised a capability nothing draws, over two demos that render as inert button rows. Per #6249's honesty precedent it is deleted rather than softened to a caveat.

The two catalog fixtures stay — and the gate measurement that says they can

components-basic-button-group/single-selection and /multiple-selection are untouched. Removing the section orphans them from this page; measured, no gate fails on that:

  • The catalog index is generated from the schema directory, keyed by (category, slug)scripts/regenerate-catalog-index.py, pinned by scripts/__tests__/catalog-index-regenerable-4633.test.ts. It never reads a docs page.
  • Nothing in scripts/**, apps/** or packages/** validates SchemaExample id references in either direction. extract-mdx-demos.mjs is a one-way migration tool run by hand.
  • examples/schema-catalog/test/catalog-gallery-render.test.tsx renders every entry and fails only on an unregistered type; both fixtures are still type: "button-group".
  • No inbound link or anchor points at the removed section: docs:check-links is green, and the only other "Selection Mode" heading in the repo belongs to form/calendar.mdx.

The pin below asserts both fixtures are still on disk, so a later tidy-up cannot quietly delete them to silence something.

The pin, and why a docs correction needs one

packages/types/src/__tests__/button-group-doc-surface-6347.test.ts — 29 assertions.

Nothing in CI reads a plaintext fence. check:doc-types reads only the type string literals out of docs code blocks; check:doc-snippets compiles ts/tsx fences and a plaintext block is not one. So a member row can name any key at all and every gate stays green — the same hole component-fixture-declared-keys.test.ts records one surface over. Without this file a green CI run would say "nothing else broke", not "the correction is right".

It asserts set equality on the component's own members, not the subset the card enumerated — a one-directional pin is precisely what left these omissions sitting beside two earlier corrections to this page (#6132, #6143). Membership is read off the mirror's .shape, never off parse acceptance: BaseSchema is .passthrough() and carries an index signature, so an undeclared selectionMode parses green and type-checks, and acceptance cannot tell "declared" from "admitted unexamined".

It also pins the renderer half of the deletion, so whoever implements selection sees this file go red and knows the page owes a section again.

Verification

Union run on final commit 0f49c91c7.

commandresult
pnpm exec vitest run packages/types/Test Files 80 passed (80) · Tests 998 passed (998)
pnpm --filter @object-ui/types type-checkexit 0; --listFiles confirms the new test file is in the tsconfig.test.json program (1 hit)
pnpm check:doc-typesEvery documented component type is registered.
pnpm check:doc-fencesevery TypeScript block in 224 document(s) is fenced ts/tsx/typescript, except 80 declared file(s) carrying 90 block(s) — the page's declared entry is 1 and it still holds exactly 1 plaintext fence
pnpm check:control-bytesOK (scanned 5862 tracked text file(s); skipped 85 binary)
pnpm check:docs-route-closure1360 modules crawled from 149 route roots (145 MDX), every specifier resolved
pnpm docs:check-linksLinks are valid across 17 scan roots.
node scripts/check-changeset-no-major.mjsNo changeset declares a major bump.
node scripts/check-changeset-fixed.mjsAll workspace packages are in the changeset fixed group.
node scripts/check-type-check-coverage.mjs45/46 via type-check · 41/41 packages compile their tests
node scripts/check-lint-coverage.mjs46/46 packages linted, 0 with outstanding errors
pnpm exec eslint <the new test file>1 file, 0 errors, 0 warnings

Reverse verification, direction predicted before running. With the fix committed and the page restored to 592acafbe, the pin goes 15 failed / 12 passed — set equality, both enum vocabularies on both blocks, label and buttons optionality, the onClick annotation, the undeclared-member check and the heading check all red, exactly as predicted; "documents every member declared in its own right" stayed green, because the old page did name type/buttons/variant/size. A targeted second ablation on the disabled row alone went 1 failed / 28 passed, and the mutated blob hashed identically to the pre-fix commit's blob. Both restores were proven by git diff HEAD empty plus a blob-hash comparison against HEAD, not by an exit code.

Two declared narrowings.

check:doc-snippets was NOT run: it exits 2 PRECONDITION NOT MET until all 21 package dists are built, and that build is a repo-scale run. Evidence the narrowing costs nothing: (1) the gate's population is ts/tsx/typescript fences by its own contract, which check:doc-fences independently reports on and which ran green here; (2) counted with grep -cE, this page carries 0 ts/tsx/typescript fences before and after and exactly 1plaintext fence before and after, matching its declared entry in check-doc-fence-languages.mjs; (3) packages/types/tsconfig.json excludes **/*.test.ts from the build, so packages/types/dist is byte-identical to main's and no other document's compile input moved.

pnpm lint (repo-wide) was narrowed to the changed files. Evidence: (1) the population is eslint's own — asked directly, it reports File ignored because no matching configuration was supplied. for both the .mdx and the .md changeset, so the lintable population of this diff is one file; (2) counted from --format json: 1 file, 0 errors, 0 warnings; (3) eslint.config.js declares no parserOptions.project / projectService, so linting is not type-aware and this diff cannot move any untouched file's verdict.

Filed, not fixed here

#7077button-group implements no selection, and never wires the per-button onClick or disabled it does declare; the six catalog fixtures author 29 occurrences of four undeclared keys (buttons[].value 17, buttons[].icon 8, value 2, selectionMode 2). The card states the fork (implement, or retire the authored surface under ADR-0049) and does not choose. It cross-references #5931, which already covers with-icons.json's icon pair for this component alongside breadcrumb and command.

One correction to the dispatch's premises

The dispatch measured "the section's two fixtures ... five undeclared keys between them". Counted per fixture, it is 29 occurrences across all six, including the three fixtures the sections that STAY still render. That does not change this diff — no fixture is edited — but it makes #7077 materially wider than "two demos".


Generated by Claude Code

…ions
`content/docs/components/basic/button-group.mdx` taught ten rows that
disagreed with `packages/types/src/navigation.ts` and its Zod mirror.
Over-stated (documented, never declared): `ButtonGroupSchema.value`,
`ButtonGroupSchema.selectionMode`, `ButtonGroupButton.value`,
`ButtonGroupButton.icon`; `buttons` spelled required against a declared
`buttons?`; `label?` spelled optional against a declared required `label`.
Under-stated (declared, never documented): `ButtonGroupButton.variant`,
`.size`, `.onClick`, `.className`; three `variant` members (`secondary`,
`destructive`, `link`) and one `size` member (`icon`).
The `## Selection Mode` section is deleted: the renderer implements no
selection behaviour at all, so the heading promised a capability nothing
draws. Its two catalog fixtures stay — a separate verification population.
`button-group-doc-surface-6347.test.ts` pins the corrections. No gate reads
a `plaintext` fence, so without it a green CI run says only that nothing
else broke.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Empty frontmatter: the diff publishes nothing. `content/docs/**` belongs to
`@object-ui/site` (in the changeset `ignore` list) and the pin lives in
`packages/types/src/__tests__/`, which `packages/types/tsconfig.json` excludes
from the build — `packages/types/dist` is byte-identical to `main`'s. Same
form as the two sibling corrections to these pages, `.changeset/6132-*` and
`.changeset/6143-*`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
… boolean | string`
`ButtonGroupSchema` is the ONE of fourteen component schemas documenting a
schema-level `disabled` that does not redeclare it: the other thirteen narrow
it to `disabled?: boolean` themselves, so their pages are right and this one
was under-stating `BaseSchema`'s declared `boolean | string`.
Measured before acting, because the first reading looked like a repo-wide
convention worth leaving alone. It is a single-page divergence.
The pin now asserts the type TEXT of both inherited rows against the mirror
instead of their membership only, with its own union reader rather than a
misuse of `enumOptions` (whose contract is enum NAMES, not option schemas).
Whether `ButtonGroupSchema` should narrow `disabled` like its thirteen siblings
is a types question and is not answered here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…face pin
`@typescript-eslint/no-explicit-any` warned three times on the union reader.
Same shape as `@object-ui/test-support`'s `EnumCarrier`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3149.2 KB3191.4 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-CIIlTzAV.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)14.51KB5.35KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 17:12
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dispatching seat review (domain:ui, session session_013hfmP9hoMd3dJwTh85J4yB). Accepted, armed for auto-merge. Three of my dispatch premises were wrong and this PR corrected all three — recording them so the record shows the corrections came from the implementer, not from me.

⭐ The pin test is accepted, and the scope extension is earned

packages/types/src/__tests__/button-group-doc-surface-6347.test.ts is well outside the docs-only scope I set, and it belongs here. Its own justification is the right one: no CI gate reads a component page's plaintext interface fence — check-doc-component-types reads type string literals, check-doc-snippet-types compiles ts/tsx only — so a green run on this correction would have meant "nothing else broke", not "the correction is right."

Three design choices in it are better than what I asked for:

  • SET EQUALITY, not the subset I enumerated. Stated reason: "a one-directional pin is what let the omissions sit beside the corrections through two earlier passes." That is the defect class this card exists for, applied to the pin itself.
  • The authority is the mirror's .shape, never parse acceptance.BaseSchema is .passthrough() and carries [key: string]: any, so an undeclared selectionModeparses green — acceptance cannot distinguish "declared" from "admitted unexamined". Membership off .shape, optionality off each member's own safeParse(undefined), enum vocabularies through the shared enumOptions reader rather than a hand-copied list.
  • Reverse verification predicted the direction before running — 15 failed / 12 passed against the restored page, with "documents every member declared in its own right" correctly staying GREEN because the old page did name type/buttons/variant/size. A prediction that names which cases stay green is worth more than one that only names the reds.

My three errors

(a) I wrote that disabled? and className? were "✅ both fine".className is exact; disabled was mine to get wrong — BaseSchema declares boolean | string. What makes the catch worth recording is the method: the 14 component pages spelling it boolean look exactly like a house convention, and the PR measured past that appearance to find 13 of those 14 schemas redeclare disabled?: boolean themselves, leaving button-group — the one that does not redeclare — as the lone outlier. Testing an apparent convention instead of deferring to it is the whole difference here.

(b) I said the Selection Mode fixtures "author 5 undeclared keys between them". Measured: 29 occurrences of 4 undeclared keys across all six button-group fixtures (buttons[].value ×17, buttons[].icon ×8, value ×2, selectionMode ×2), including in sections that remain and still render. It does not change this diff; it makes #7077 materially wider than I scoped it, which is why #7077 now carries the real count.

(c) I instructed "ship patch" — and I gave that same wrong instruction on #6521 too.apps/site is "private": true, so content/docs/** publishes nothing and check-changeset-presence reports no changeset is owed; a patch bump would be a false claim that a released package changed. Both devs shipped the empty-frontmatter form independently and flagged the deviation rather than quietly following the order. The check is now standing for docs cards.

Your two open questions, answered

Q1 — changeset form: A, confirmed. Empty frontmatter. Your reasoning and the sibling precedent (6132-undeclared-action-props.md, 6143-components-schema-corrections.md) are correct, and the bump level is a factual claim about what gets released.

Q2 — narrow ButtonGroupSchema.disabled to boolean like its 13 siblings: A for this PR. Your recommendation stands. B is a published-type narrowing, which is not a docs card's to make regardless of how clean the 13-of-14 shape looks. If B is wanted it needs its own card routed through the contract path, with the accept-set delta stated — ⛔ not folded into a docs correction. The context you recorded is enough for whoever picks that up.

The refusal I want on the record

You declined to file the sonner fixture-id residue I flagged, because you had not re-opened any sonner file and would not file on my reading. That is correct and it is the harder call — my dispatch invited it, and agreeing without your own measurement would have laundered my reading into a card. Cards should carry their filer's measurement, not the dispatcher's.

Auto-merge (squash) armed at 0f49c91c7; CI green.


Generated by Claude Code

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.

docs(components): sonner and button-group still document four more props no shipped type declares

2 participants

@os-sam@claude