Uh oh!
There was an error while loading. Please reload this page.
fix(components): declare the I18nLabel arm on element:text_input's label / placeholder / description - #5736
Merged
Conversation
…bel / placeholder / description (#5717) The renderer has resolved all three keys through `pickLocalized` since it was written, while the `ComponentMeta` entries still declared a single `'string'` arm — so `validateTree` reported `type-mismatch` on a locale map the contract admits and the screen renders correctly in the viewer's language. The inverse of #5590 / #5637, where the declaration correctly stayed narrow until the render site caught up. Here the render site was never behind. Per-key, not blanket: `defaultValue` keeps `['string','number']` (no object arm in its contract) and the console specimen control asserting that stays green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EuPCi56cnGyykygi3z9w4m
… an inert lint directive ESLint reports the copied `eslint-disable-next-line react-hooks/static-components` as UNUSED under this repo's own config — the rule never fires at that site. The reason the pattern is safe is worth keeping; the suppression that never suppressed anything is not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EuPCi56cnGyykygi3z9w4m
Contributor
✅ 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
|
os-zhuang
marked this pull request as ready for review
August 23, 2026 02:49
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#5717
element:text_inputdeclares the inline-translation arm onlabel,placeholderanddescription, so the manifest gate stops reportingtype-mismatchon a locale map its own renderer has always resolved correctly.The defect, measured
@objectstack/spectypes all three keys as theI18nLabelunion. Measured here on the installed 17.1.0 pin, per key, from the schema's own verdicts rather than copied from the card:text-input.tsxhas resolved all three throughpickLocalizedat their read sites (:96/:97/:98) since it was written. Only theComponentMetaentries stayed at a single'string'arm (:146/:147/:183). Driven through the samemanifestFromConfigs+validateTreepair the JSX-page compiler (renderers/layout/page.tsx:462) and the save gate use, an author writing{ en: 'Owner', 'zh-CN': '负责人' }got three warnings on a write the screen renders correctly. Before, ond8afbe519:After, same probe, same path:
The gate is the subject here, not the types: the defect is a diagnostic emitted by
validateTreeover a manifest, and no type-level or declaration-level assertion runs that path.Why this is the inverse of #5590 / #5637
Every other member of this family earned its object arm in the change that taught its render site to resolve the map.
element:record_picker.emptyText(#5590) and that block'slabel/placeholder(#5637) each held one arm for exactly as long as their renderer dropped the map — declaring an arm the renderer drops advertises a shape that never reaches the screen.Here the render site was never behind. That is the same rule's other half, and
ComponentInput.typestates it in its own words: withholding an arm the renderer resolves makes the gate contradict itself on the write it just recommended, at warning severity — "which is worse than it sounds, because noise on legal writes trains authors (AI authors included) to dismiss theunknown-propandtype-mismatchreports that ARE real."No renderer behaviour changed. This is a declaration fix.
Look-item 1 — does
descriptiontravel with the two label-ish keys?Yes, all three travel together. The card was right that
descriptionhas a different destination, and the destinations really are three different things:labellabelelement above the field, tied to it byhtmlForplaceholderplaceholderattribute, inside the fielddescriptionpelement below the fieldDestination is not what decides an arm.
ComponentInput.typenames exactly two conditions — the contract accepts the shape, and the renderer resolves it — anddescriptionsatisfies both identically tolabel: the samepickLocalized(props.X, language)call shape one line apart, and the same string-or-locale-map contract (I18nLabel, a union of a string with a string-valued record), each measured per key rather than inherited from its neighbours. A destination-based split would have declared an arm on one key and withheld it on another for a difference neither the gate nor the contract can observe, and would have leftdescriptionemittingtype-mismatchon a value that renders correctly — the very defect this card is about, kept alive on one third of it.The destinations are pinned anyway, in
text-input-i18n-label-arms.test.tsx: one case writes a distinct map per key and asserts each lands in its own place, because three identical maps cannot tell "each key reached its own destination" from "one value was rendered three times".There is one behavioural difference between them that the arms cannot carry, so it went into the descriptions instead:
labelis announced to assistive technology anddescriptionis not. Filed separately as #5735 — not fixed here.Look-item 2 — the specimen pin
Added to
apps/console/src/__tests__/component-input-union-specimens.test.tsas its owndescribe, shaped like the #4970 block next to it — arms derived fromElementTextInputPropsSchema's own verdicts, never restated, so either side moving turns it red: a spec release that drops the map arm, or a declaration that grows one the spec rejects. It carries the derivation guard the house style requires (a schema that refused every probe would return[]and make the comparisons agree vacuously), a per-arm control on every specimen, and a whole-node case reproducing the exact write the card reported.Two files, two halves, neither subsuming the other:
text-input-inputs-spec-parity.test.tsgoes red when the declaration and the contract disagree; the specimen file goes red when the gate and the declaration do.The
defaultValuecontrol — verdict after this changeStill reports
type-mismatchon anI18N_MAP, unchanged and green as a failure assertion. That is the assertion atcomponent-input-union-specimens.test.tswhich keeps this widening per-key rather than blanket, and it was run first and last.defaultValue's spec type isstring | number— a contract with no object arm, re-measured here — so its arms did not move and must not. This PR also adds the separation as evidence rather than convention: both files now assertspecAcceptedArms(..., 'defaultValue') === ['string','number']from the contract, next to the trio's['string','object'].Ablations — both legs confirmed on disk
Vitest resolves
@object-ui/*to siblingsrc/through the root config's alias table, so these run against source and nodistis involved. Each leg confirmed its mutation on disk with anchored counts before the run, and restored through atrap ... EXIT INT TERM.Ablation A — revert the three arms to
'string', keep the tests. Predicted: the new declaration assertions go red, the renderer pin stays green. Observed: mutation3 -> 0widened /0 -> 3narrow on disk; 7 failed, 34 passed; the seven are exactly the four new specimen cases and the three parityit.eacharms.text-input-i18n-label-arms.test.tsxstayed entirely green — the renderer was untouched.Ablation B — drop
pickLocalizedat the three read sites, keep the declaration. Predicted: the renderer pin goes red, both declaration files stay green; failure mode a throw for the two React-child positions and[object Object]for the attribute. Observed: mutation3read sites replaced, markers confirmed on disk; 6 failed, 35 passed, all six in the renderer pin, and the mode was the throw:byte-consistent with what
inline-locale-label-read-sites.test.tsxmeasured for the same class one key over. Both declaration files stayed green. That asymmetry is why the renderer pin exists: after this change the declaration rests on "the renderer resolves it", and nothing pinned that for these three keys — delete the resolution today and every arm assertion still passes.Family sweep
Swept
packages/**for the same shape — a renderer resolvingpickLocalized(props.X, ...)while declaring a single'string'arm forX. No hits after this change. The instrument was controlled against the pre-fix file, where it returns exactlylabel, placeholder, description, so the empty result is a measurement rather than a broken regex.Verification
Union re-run on the final commit
9711f5ca8, after the last commit, quoting each gate's own verdict line:pnpm --filter @object-ui/components --filter @object-ui/console type-checkpackages/components type-check: Done·apps/console type-check: Donepnpm --filter @object-ui/components --filter @object-ui/console lint0 errors, 910 warnings·0 errors, 202 warningsvitest run packages/components/Test Files 183 passed (183)·Tests 1654 passed (1654)vitest run apps/console/Test Files 72 passed (72)·Tests 790 passed (790)check-changeset-presence.mjs4 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)check-changeset-fixed.mjs/check-changeset-no-major.mjscheck-control-bytes.mjsOK (scanned 4784 tracked text file(s); skipped 85 binary)check-doc-component-types.mjsEvery documented component type is registered.check:i18n-keysEvery in-scope call-site key resolves against the en pack (2924 keys)check:spec-symbols1295 files scanned against 4966 spec export namesvitest run apps/console/was green onbe690be13; the only delta to9711f5ca8is a JSX comment inside apackages/componentstest file, and the four directly-affected test files were re-run on9711f5ca8(47 passed). The lint numbers are the repo's known warning debt —lint.ymlsets no--max-warningsdeliberately; this gate is about errors, and there are none.packages/componentsdropped 911 to 910 because this PR removes an inerteslint-disabledirective that ESLint itself reported as unused.Lint scope note: objectui's
pnpm lintisturbo run lintover per-packageeslint ., so the two packages above are a complete cover of every file in this diff, not a sample. ESLint here is not type-aware —eslint.config.jsconfigures noproject/projectService— so this diff cannot move the verdict on any file it does not touch.Out of scope
element:text_input'sdescriptionrenders as an unassociated paragraph — noaria-describedby, so assistive tech never announces it with the field #5735 —element:text_input'sdescriptionrenders as an unassociated paragraph (noaria-describedby), while theformrenderer wires the identical key correctly. Filed unassigned, dedupe-searched first. Not fixed here.element:record_picker's keys untouched — components:record-picker'slabelandplaceholderdrop the same inline-locale-map armemptyTextjust stopped dropping — one throws, the other silently renders English or nothing #5637 is the inverse direction and sits in the decision box.Generated by Claude Code