Repository navigation
fix(components): KbdGroup declares the kbd it renders, not a div (objectui#8576) - #8891
Conversation
…ectui#8576) `KbdGroup` annotated its props `React.ComponentProps<"div">` and returned a `kbd` — upstream shadcn's shape too, and the last entry in the class-wide ledger PR #8574 opened for objectui#8571. The ruling is direction 1, type follows element: `kbd` is what ships, what upstream ships, and a `kbd` grouping `kbd`s is legal HTML, so the half that was lying is the half that moves. Making the element follow the declaration instead would change the DOM every existing consumer already receives. Measured before choosing an instrument, one narrow program per leg (TypeScript 6.0.3, @types/react 19.2.18): `Equal<ComponentProps<'div'>, ComponentProps<'kbd'>>` is FALSE, with `Equal<ComponentProps<'div'>, ComponentProps<'p'>>` TRUE in the same program as a positive control — objectui#8571's result reproduced, so the FALSE is a reading rather than a broken `Equal`. The prop names are identical (`keyof` equal; `HTMLAttributes` is one interface for every element); the element type parameter is what moves, so `ref` and `currentTarget` become `HTMLElement`. That makes `tsc` a discriminating instrument here, which it was not on objectui#8571. Census: `KbdGroup` has no call site in this repository, only its definition and the `export *` barrel. For published consumers the props type widens (`ComponentProps<'div'>` is assignable to `ComponentProps<'kbd'>`, not the reverse), and a `div`-typed ref object or ref callback is still accepted. The ratchet was exercised, not assumed: the ledger line `'kbd.tsx:KbdGroup': 'div -> kbd'` was deleted FIRST, with the declaration still `"div"`, and the sweep failed on `every judged member under src/custom names the intrinsic it returns, except the ledgered cards` before the repair was written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract review at
|
|
Handoff provenance — director seat, summon #18 segment 6 ( Generated by Claude Code |
|
Tier notice — the contract-review-tier requirement on this PR is lifted (skills seat, session Maintainer ruling, verbatim: 「现有的卡片如果写了要求fable的,也要让相关的项目经理知道,opus就够了。」 Under the same ruling set (quoted in full on objectstack-ai/objectstack#17285), the contract-review tier is reserved for the skills seat (protocol files + the published For this PR: its Generated by Claude Code |
Fixes #8576
ComponentProps OF "div", never the angle-bracket form. This is a pull request about type shapes, and GitHub's body sanitizer eats tag-shaped fragments even inside backticks and fences (AGENTS.md, "GitHub 会改写你写进 issue/PR 正文的字节"); on the sibling card a before/after table of exactly these types collapsed into two identical strings, i.e. into "nothing changed".What changes
KbdGroupinpackages/components/src/custom/kbd.tsxannotated its propsReact.ComponentProps OF "div"and returned akbd. The annotation now names"kbd". Nothing rendered moves — this component has always put akbdin the DOM, and upstream shadcn ships the same element.Direction 1, type follows element, per the dispatch ruling:
kbdis what ships and what upstream ships, akbdgroupingkbds is legal HTML, and making the element follow the declaration instead would change the DOM every existing consumer already receives. The half that was lying is the half that moved.The measurement that had to come first
The card required this before any instrument was designed, because objectui#8571 shrank from a type-safety repair to a documentation repair exactly for want of it. Measured in this tree, one narrow program per leg, TypeScript 6.0.3 /
@types/react19.2.18 — the same versions objectui#8571 was measured under:Equalof ComponentProps OF "div" and ComponentProps OF "kbd"Equalof ComponentProps OF "div" and ComponentProps OF "p"keyofof the two props typesHTMLDivElementadds overHTMLElementalignRefObjectofHTMLDivElement or nullaccepted by the new props'refHTMLDivElement or nullaccepted by the new props'refThe positive control is why the first row is a reading and not a broken
Equal.⇒ Unlike objectui#8571,
tscIS a discriminating instrument on this card. The difference is carried entirely by the element type parameter, not by the prop names:HTMLAttributesis one interface for every element, so what actually moves isrefandcurrentTarget. All three instruments are therefore pinned: the SOURCE pin (the class-wide sweep next door), the TYPE pin (new, erased at runtime, compiled bytsc -p tsconfig.test.jsonwhich the packagetype-checkscript chains), and the DOM pin (new,tagNameafter a real render).Premise P1 — the call-site census
The dispatch hung the ruling on P1: no call site depends on the props being
div-shaped.KbdGroupappears in exactly four places tree-wide — its own definition, itsexport, the sweep's ledger line, and one comment. No.tsx,.mdx, example or app renders it. (content/docs/components/data-display/kbd.mdxdocuments the SDUIkbdschema and its registry renderer, a different module that does not importKbdGroup.)div-typed ref object or ref callback is still accepted (measured, and pinned in the new test).div-only member off an inferredreforcurrentTarget, and there is exactly one such member: the deprecatedalign.P1 holds. The props were never widened, unioned, or erased — see the caricature section.
The ratchet, exercised in the required order
PR #8574 ledgered this component as an accepted exception,
'kbd.tsx:KbdGroup': 'div -> kbd'. That line was deleted first, with the declaration still"div", and the sweep was run before any repair was written:So the guard is known to SEE this, by test name, rather than assumed to. The repair came after; the ledger is now empty, which is the ratchet at its floor — any disagreement under
custom/now fails outright.The caricature, refused by both instruments
Widening the props to
ComponentProps OF "div"intersected withComponentProps OF "kbd"— the shape the card and the dispatch both named as the red line — was run as its own ablation from the committed tree:On objectui#8571 that caricature was invisible to
tsc, because all three types were one type. Here it is refused by the type pin as well as the source pin — measured, both directions.Ablations and controls
Both legs mutate from the committed tree, prove the mutation reached disk (marker counts and
git hash-objectblob, with a hard abort if it did not), and restore under atrap; restoration is verified by STATE —git diff HEADempty and the blob back to9f05260fdbdf7295e7ca66137b8ad59b721e3a76— never by an exit code.tsc)"div"_Group,_GroupIsNotDiv)_Group,_GroupIsNotWidened)The DOM pin staying green under both mutations is the honest reading, not a hole: a declaration-only change cannot move the rendered element, which is precisely why the card insisted the source pin exists. It is reported rather than hidden.
The control moves independently. The sibling
Kbdin the same file already agreed with its declaration; its assertions (_Kbd, and its render leg) stayed green through every leg above, and the six other tests of the sweep stayed green while the seventh went red. A control that reddens with the subject would not be one.No
disthop is involved in any of this:tsc -p tsconfig.test.json --listFilesshows both the new test andpackages/components/src/custom/kbd.tsxas program inputs, and the tests import../custom/kbdrelatively, so every leg reads the source it mutated.Local runs (all at
2fb899321, tree clean)pnpm exec vitest run packages/components/pnpm exec turbo run type-check --filter @object-ui/componentstsc --noEmitandtsc -p tsconfig.test.json)pnpm --filter @object-ui/components lintnode scripts/check-changeset-presence.mjspnpm run changeset:checkmajordeclaredpnpm run type-check:coveragepnpm run check:control-bytespnpm run check:published-tsconfig-excludenode scripts/cross-file-line-citation-census.mjspnpm run check:readme-exportsLint narrowing, stated as a measurement: the population is eslint's own resolution of
.underpackages/components, 468 files counted from--format json, which is exactly what CI'sturbo run lintruns for this package; the excluded remainder is the other packages' lint runs, andeslint.config.jsdeclares noparserOptions.projectand noprojectService, so linting is not type-aware and nothing in this diff can move an untouched file's verdict. The 941 warnings are the package's standingreact-refreshnoise; both touched files report 0 errors and 0 warnings.check:readme-exportsexits 1 in this worktree with "the population COLLAPSED — this run proves nothing" andpackagesRead: found 8, floor is 25because 29 packages are unbuilt here. That is a prerequisite failure, not a red gate, and it is reported as NOT MEASURED rather than as either colour. The diff changes no README and no package export, and CI builds before running it.Position axis (objectui#8875): the line-citation census reports 1267 citations / 540 false / 295 resolving / 432 unjudged at the branch point
4e3a4f07eand the identical four numbers here, so this change neither adds a cross-file line address nor moves a line anything cites. Nothing in the tree cites either touched file by line address; the one cross-file reference to the sweep is the new test, and it cites by NAME.Clause-②
Clause-②: yes— my own judgement, not inherited from the dispatch.KbdGroupis a published component of@object-ui/components, and this changes the published type surface in a way that is measurably observable to consumers:refandcurrentTargetmove fromHTMLDivElementtoHTMLElement. It adds no export and no key, so it is not the mechanical floor; it is the "unsure ⇒ yes" branch, and the sibling card is exactly why — there the two types were identical and anowould have been defensible on measurement, here they are not. The declaration is carried on both carriers (this pull request and the claim comment on the card), and the pull request docks in draft until the contract-review tier clears it. That docking is the safe state; nothing in the shape below was traded for it.验收备注
Out-of-scope observations, none filed, none touched:
packages/components/src/renderers/data-display/kbd.tsx— the SDUIkbdrenderer wraps its keys in adivwhilecustom/kbd.tsxuses akbdfor the same grouping job. Two different modules with two different jobs, and the sweep does not judgerenderers/. Not a defect, not filed; a follow-up carrier would be objectui#8875-style consistency work, and there is no PR or person heading for that file today, so: no successor.toBeGreaterThanOrEqual(12)) and its family enumeration remain as PR fix(components): EmptyDescription declares the div it renders, not a p #8574 wrote them; the ledger simply became empty. Not filed.Generated by Claude Code