Skip to content

fix(types): ariaLabel declares the keyed vocabulary, disabled accepts the predicate string (#4581) - #4603

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-4581-label-vocabulary
Aug 13, 2026
Merged

fix(types): ariaLabel declares the keyed vocabulary, disabled accepts the predicate string (#4581)#4603
yinlianghui merged 2 commits into
mainfrom
claude/issue-4581-label-vocabulary

Conversation

@yinlianghui

@yinlianghui yinlianghui commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Fixes #4581.

Executes the label-vocabulary rulings recorded on #4580 (comment 5284007579) — the follow-up seat that ruling asked for. PR #4593 landed the visible half and escalated the rest; this lands Q2-B and Q3-A, and stops on Q1-B because its stated premise measured false.

Q2-B — ariaLabel declares the KEYED vocabulary, and the shape gets a name

BaseSchema.ariaLabel is now string | KeyedI18nLabel, where KeyedI18nLabel is a new exported type in @object-ui/types:

export type KeyedI18nLabel = {
  key: string;
  defaultValue?: string;
  params?: Record< string, any >;
};

That is the vocabulary the renderer actually resolves — packages/react/src/SchemaRenderer.tsx:111:

if (schema.ariaLabel) {
  aria['aria-label'] = resolveKeyedI18nLabel(schema.ariaLabel);
}

Not I18nLabel. The original #4581 text asked for string | I18nLabel; PR #4593 measured that spelling wrong in three ways and the ruling withdrew it. I18nLabel is the spec's INLINE LOCALE MAP (string | Record< string, string >), so it accepted the shipped keyed fixture only vacuously — as a locale map whose "locales" are named key and defaultValuerejected the same label carrying params, and type-checked a genuine { en: 'Owner' } that resolveKeyedI18nLabel returns undefined for, rendering an empty aria-label. The two vocabularies are structurally confusable: #4167's exact hazard.

The keyed-shape census (verified before the name was minted)

All three inline copies agreed exactly on the object half:

Site Object half Wrapper
packages/react/src/utils/i18n.ts:38 { key: string; defaultValue?: string; params?: Record< string, any > } string | … | undefined
packages/layout/src/NavigationRenderer.tsx:261 identical string | … (no undefined)
packages/app-shell/src/utils/index.ts:63 identical string | … | undefined

So KeyedI18nLabel names the object half only; each site keeps its own wrapper. It is a name for what was already there, not a new capability.

#4208 train-overlap decision, per file — measured, not assumed

The card flagged PR #4208 (the rc.6 train, open and blocked on #4165). GitHub's file list for #4208 is computed against a stale merge-base (6314e87f2), so it lists files whose changes have since landed on main. I measured the content delta instead (git diff origin/main refs/remotes/pr/4208 -- PATH):

File Train content vs main Decision
packages/react/src/utils/i18n.ts byte-identical — the train's change already landed retired the inline copy
packages/layout/src/NavigationRenderer.tsx byte-identical; not in #4208's file list at all retired the inline copy
packages/app-shell/src/utils/index.ts differs — a pending hunk still rewrites the ./appRoute re-export block left in place, with a comment naming the type and why the swap waits

⚠️ Worth the reviewer's eye: the dispatch card stated "react/utils/i18n.ts has no train overlap" and treated app-shell/utils/index.ts as the overlapping one. On the stale-merge-base file list both appear; on the content measurement the app-shell judgement is right and the react one is right for a different reason than stated. I followed the measurement. For the record, the app-shell hunk sits ~25 lines from the inline copy (the re-export block, not the signature), so a swap there would most likely not have conflicted either — I still left it alone, because the rule was file-granular and app-shell is also #4024's in-flight package. Only that file's doc comment is touched there; nothing else in app-shell.

Q3-A — disabled accepts the predicate string

BaseSchema.disabled is now boolean | string, on exactly the visible evidence one slot over: SchemaRenderer.tsx:466 evaluates it through the same evaluateCondition ((condition: string | boolean | undefined, context?) = > boolean), and the disabledOn?: string sibling exists for the same reason. The asymmetry was accidental.

Casts dropped — the census is now closed

SchemaRenderer.expressions.test.tsx :132/:141 (the two disabled casts) and SchemaRenderer.aria.test.tsx:59 (the ariaLabel cast) are gone, and both files' BaseSchema imports went with them. The expressions header explicitly said "drop them when that lands" — this is that landing. A new aria case pins a keyed label carrying params, the limb the withdrawn spelling rejected.

Of the six as BaseSchema casts PR #4593's census recorded, this card's ruled widenings close the three that were this gap. The remaining three are the other classes that census identified and are untouched: components/__tests__/html-anchor-links.test.tsx:33 (a Record< string, unknown > spread), and plugin-dashboard's two #4548 narrowing casts.

The consumer sweep found a real defect — @object-ui/components

Declaring the vocabulary honestly is what surfaced it, and a downstream type-check sweep is what caught it, not a test. packages/components/src/renderers/form/toggle.tsx is one of the few renderers that writes aria-label ITSELF instead of relying on SchemaRenderer's resolveAriaProps, and it forwarded the value raw:

aria-label={schema.ariaLabel}

Under string | KeyedI18nLabel that stops type-checking against Toggle's aria-label: string | undefined. It now resolves. Invoked directly, the raw forward emitted aria-label="[object Object]" — announced verbatim by a screen reader.

My first prediction about this was wrong, and the test header records that rather than being quietly rewritten. I predicted the harm would show through SchemaRenderer. It does not: SchemaRenderer injects its own already-resolved aria-label via ...ariaProps (SchemaRenderer.tsx:599 + :625), and this renderer spreads {...props} AFTER its own attribute, so the resolved value always wins and the raw expression is shadowed. A SchemaRenderer-driven test is therefore green in both directions — vacuous, and it would have shipped looking like proof. The discriminating case invokes the registered renderer directly via ComponentRegistry.get('toggle'), and that one measured, verbatim:

Expected the element to have attribute:
  aria-label="Close dialog"
Received:
  aria-label="[object Object]"

Both paths are kept in packages/components/src/__tests__/toggle-aria-label-keyed.test.tsx and each is labelled for what it can and cannot show.

⛔ Q1-B — STOPPED. The premise measured false on both halves.

The ruling made Q1-B conditional: "the bridge's invocation context can know the locale AND re-translates on locale change — if measured otherwise, STOP and report." Measured otherwise, so no bridge code is touched here.

(a) The call path, and who calls it. SpecBridge.transformListView (SpecBridge.ts:38) → transform() (:30) → return bridge(spec, this.context) (:34) → bridgeListView (bridges/list-view.ts:166), sites :180 and :224. BridgeContext (spec-bridge/types.ts:12-20) declares exactly user?, variables?, objectDefs?no locale field.

An exhaustive grep for SpecBridge|bridgeListView|bridgeFormView|transformListView|transformFormView across all of packages/ and apps/ finds zero production callers. Every reference is the module itself, the barrel re-export (spec-bridge/index.ts, react/src/index.ts:14), or a test — five suites under react/src/spec-bridge/__tests__/ plus plugin-grid/src/__tests__/specBridgeExportFormats.test.tsx:73. Every in-repo construction is new SpecBridge() or new SpecBridge({ user: … }). It is a published API surface with no in-repo consumer.

(b) Can it know the locale? No. The display-locale channel is useDisplayLocale()packages/i18n/src/useDisplayLocale.ts:53, a React hook composed of two further context hooks (useLocalization, useObjectTranslation). bridgeListView is a plain function invoked from a plain class method, not a component render. Rules of hooks, not a style preference.

(c) Does a locale change re-run it? No. SpecBridge assigns context once in its constructor to a mutable field and reads it at call time. There is no memoization, no React subscription, no invalidation channel; updateContext() (:47) has zero callers. transform() returns a plain object, so a resolved string is frozen into it at transform time.

So even if BridgeContext gained locale?: string, resolving at the bridge would bake one audience's language into the node tree with no re-translation channel — the freeze the ruling named as a stop condition. The spec's own resolver doc warns about this class of defect in as many words: the locale parameter is "positional rather than optional so that a producer cannot silently ship one audience's language to every audience by forgetting an argument — the defect #6761 records".

BaseSchema.label and .description therefore stay string (which is the ruling), and the two list-view.ts sites are unchanged. #4580's Q1-B blocker is NOT cleared by this PR — the seat's report carries the measured alternative (read-time resolution in the renderer against useDisplayLocale(), which is how every other localized read site in this repo already works) and the tension that makes it a real question rather than a mechanical follow-up: read-time resolution requires the renderer to accept an inline map in schema.label, i.e. the BaseSchema.label widening Q1-B deliberately declined.

Red-first — predictions written into the test header before the run

packages/types/src/__tests__/base-schema-label-vocabulary.test.ts, the sibling of #4593's base-schema-visible-predicate.test.ts. Against origin/main (52d878a3b), tsc -p packages/types/tsconfig.test.json reported, verbatim:

base-schema-label-vocabulary.test.ts(103,27): error TS2305: Module '"../base"' has no exported member 'KeyedI18nLabel'.
base-schema-label-vocabulary.test.ts(120,3): error TS2344: Type 'false' does not satisfy the constraint 'true'.
base-schema-label-vocabulary.test.ts(130,3): error TS2344: Type 'false' does not satisfy the constraint 'true'.
base-schema-label-vocabulary.test.ts(143,3): error TS2322: Type '{ key: string; defaultValue: string; }' is not assignable to type 'string'.
base-schema-label-vocabulary.test.ts(149,3): error TS2322: Type '{ key: string; defaultValue: string; params: { name: string; }; }' is not assignable to type 'string'.
base-schema-label-vocabulary.test.ts(161,3): error TS2322: Type 'string' is not assignable to type 'boolean | undefined'.

Post-fix: clean, exit 0.

One prediction was half wrong, and it is left standing in the header rather than rewritten to match. I predicted assertionAriaLabel and assertionKeyedShape would both report TS2344 pre-fix; only assertionKeyedShape (120) did — TypeScript suppresses the cascade once the import is an error type, so assertionAriaLabel (126) reported nothing. That is why the header flagged those two as non-independent evidence in advance: a pin whose pre-fix silence is a compiler artifact proves nothing on its own, and the load-bearing reds are the other four.

Every Equal is invariant, for the reason #4593 spelled out and this card inherits: a satisfies-style or one-way extends check is vacuous for a widening in both directions — the narrow string is assignable to the wide string | KeyedI18nLabel, so a widening that never happened and a widening that overshot to any would both stay green. BaseSchema's [key: string]: any makes the overshoot live, not hypothetical.

must-not-change

  • label / description stay string — pinned, not merely left alone. assertionLabel and assertionDescription are the Q1-B ruling written down: the only two pins in the file expected green pre-fix, and a future card that "fixes" the bridge defect by widening BaseSchema.label turns them red on purpose. Confirmed in the published .d.ts: label?: string, description?: string, unchanged.
  • Reverse verification: fix removed with git checkout origin/main -- PATHS (never git stash — shared refs/stash), the pins re-reported the same errors, then restored from a patch file and verified byte-identical by sha256 across all 7 tracked files.
  • Control-byte and NBSP self-scan clean over every touched file including the untracked test.

Per-package grading — measured

Both trees built with dist/ and *.tsbuildinfo cleared, then every published .d.ts compared file-by-file.

Package .d.ts files differing What changed Grade
@object-ui/types 4 of 54 base.d.ts (+KeyedI18nLabel, two widened unions, evidence comments), index.d.ts (+the export) — plus 2 zod files that are order-only, see below minor
@object-ui/react 1 of 63 utils/i18n.d.tsresolveKeyedI18nLabel's parameter names the type instead of inlining it patch
@object-ui/layout 1 of 8 NavigationRenderer.d.ts — same, for resolveLabel patch
@object-ui/app-shell 1 of 416 utils/index.d.tsdoc comment only; the inline copy is deliberately not swapped patch
@object-ui/components no declaration change (the renderer module exports nothing); the change is runtime patch

types is minor by position analysis, not assumption: the diff ADDS an exported name and ADDS members to unions on two authored-input-dominant properties. It removes nothing, and nothing that type-checked before stops doing so — the #4593/#4586/#4591 shape. react / layout are patch because the type is structurally identical: naming a shape is a spelling change in the .d.ts, not a contract change. app-shell is patch under the "grade patch if the published .d.ts changes at all" rule — flagged plainly because no consumer can observe a comment, so a reviewer who would rather drop that entry has my measurement to do it with.

Never major (major tracks @objectstack).

The two zod .d.ts diffs are build nondeterminism, not this change

types/zod/complex.zod.d.ts and types/zod/index.zod.d.ts differ between the two builds. They are identical as a multiset of lines (verified by sorting both and comparing hashes) — pure key-ORDER churn in tsc's zod inference, of the priority: { medium, critical, low, high } reordering kind. Neither mentions KeyedI18nLabel; no zod .d.ts in the package does. I checked rather than waved it away because an unexplained diff in a published declaration is exactly what a grading claim must not contain.

Verification

  • Tests: repo-root vitest run --maxWorkers=2 over the radius (react, components, types, layout, app-shell) — 603 files, 5947 passed, 1 skipped. Touched suites re-run separately, green.
  • Downstream type-check sweeps, PREFIX filter (= the consumers, the direction that matters for a widening): ...@object-ui/types77/77 packages, ...@object-ui/react67/67. Zero errors. The first run of that sweep is what found the toggle.tsx defect above.
  • eslint over all 10 touched files: 0 errors. On the 6 files that existed before, 51 → 50 problems, net −1 (the two retired inline copies each drop a no-explicit-any; KeyedI18nLabel's own params adds one back).
  • Reverse verification: sources reverted with git checkout PATHS at the fork point (never git stash — shared refs/stash), the six pins re-reported the same errors, then restored and verified byte-identical by sha256 on all six files.
  • All 11 local gates pass: control-bytes, phantom-dependencies, changeset-presence, changeset-no-major, changeset-fixed, type-check-coverage, lint-coverage, spec-symbol-derivation, i18n-call-site-keys, i18n-en-drift, doc-links. Control-byte and NBSP self-scan clean over every touched file including untracked ones.

Filed, not fixed

#4605 — the zod mirror in packages/types/src/zod/base.zod.ts still declares visible and disabled as z.boolean() and ariaLabel as z.string(), now narrower than the TypeScript declarations they mirror. visible has been drifted since PR #4593; this PR adds the other two. Measured as observation-class: nothing enforces it on the render path (core's validateSchema is hand-written and imports only the TS type; SchemaRenderer's __DEV__ check only sets a data-obj-schema-invalid attribute), but @object-ui/types/zod is a published validator that would reject spellings the published types invite. Widening it is a contract decision — especially for ariaLabel, where the keyed object must not be confused with the spec's inline map — so it is filed rather than guessed at here.


Generated by Claude Code

… the predicate string (#4581)

Executes #4580's label-vocabulary rulings Q2-B and Q3-A. PR #4593 landed the
`visible` half and escalated the rest.

Q2-B: `BaseSchema.ariaLabel` widens from `string` to `string | KeyedI18nLabel`,
where `KeyedI18nLabel` is a new exported type in `@object-ui/types` naming the
keyed form `{ key, defaultValue?, params? }` that `SchemaRenderer.tsx:111`
actually resolves via `resolveKeyedI18nLabel`. Deliberately NOT the spec's
`I18nLabel` (the inline locale map) — PR #4593 measured that spelling accepting
the keyed fixture only vacuously, rejecting `params`, and type-checking a
genuine locale map that renders an empty aria-label.

The three inline copies of the keyed shape were verified identical before the
name was minted; react/utils/i18n.ts and layout/NavigationRenderer.tsx now
import it. app-shell/utils/index.ts keeps its inline spelling because PR #4208
has a pending change to that file — comment added naming the type and the wait.

Q3-A: `BaseSchema.disabled` widens from `boolean` to `boolean | string`, same
evidence as `visible` (SchemaRenderer.tsx:466 evaluates it through the same
`evaluateCondition`; `disabledOn?: string` sibling).

Three `as unknown as BaseSchema` casts drop (aria :59, expressions :132/:141),
closing the class the #4581 census named.

Q1-B is NOT implemented: its premise measured false. The spec bridges have zero
production callers, `BridgeContext` has no locale field, the display-locale
channel is a React hook unusable from a plain class method, and nothing re-runs
the bridge on a locale change. `BaseSchema.label`/`.description` stay `string`
and are pinned that way.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 13, 2026 6:13pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 24.7 KB 350 KB
Entry file index-D__aFqth.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 25.13KB 5.40KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 38.46KB 10.17KB
auth (createAuthenticatedFetch.js) 6.34KB 2.43KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.02KB 0.88KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 489.28KB 108.46KB
core (index.js) 3.79KB 1.52KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 163.56KB 44.83KB
fields (index.js) 230.37KB 57.17KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.84KB 1.45KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.98KB 10.85KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.86KB 12.91KB
plugin-charts (index.js) 62.10KB 17.67KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 121.04KB 31.57KB
plugin-designer (index.js) 212.58KB 42.83KB
plugin-detail (index.js) 239.93KB 60.01KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.72KB 27.70KB
plugin-gantt (index.js) 164.30KB 40.02KB
plugin-grid (index.js) 190.02KB 50.48KB
plugin-kanban (index.js) 52.74KB 14.53KB
plugin-list (index.js) 112.01KB 27.27KB
plugin-map (index.js) 18.16KB 5.81KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 41.38KB 11.09KB
plugin-timeline (index.js) 26.68KB 7.66KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.09KB 20.56KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 27.64KB 9.44KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.26KB 0.67KB
react (schema-input.js) 1.45KB 0.83KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

…tead of forwarding it raw (#4581)

Found by the downstream type-check sweep over consumers of @object-ui/types,
not by a test: with `BaseSchema.ariaLabel` honestly declared as
`string | KeyedI18nLabel`, `aria-label={schema.ariaLabel}` in
packages/components/src/renderers/form/toggle.tsx stops type-checking against
Toggle's `aria-label: string | undefined`.

The runtime harm is narrower than it first looks, and the test says so. Through
SchemaRenderer the bug is INVISIBLE: SchemaRenderer injects its own already
resolved `aria-label` via `...ariaProps` (SchemaRenderer.tsx:599/:625) and this
renderer spreads `{...props}` after its own attribute, so the resolved value
wins. Invoked directly the raw forward emits `aria-label="[object Object]"`.

My first prediction — that the harm would show through SchemaRenderer — was
wrong, and the test header records that rather than being rewritten to match:
a SchemaRenderer-driven case is green in both directions and would have shipped
looking like proof. The discriminating case invokes the registered renderer
directly via ComponentRegistry.get('toggle').

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 24.7 KB 350 KB
Entry file index-DLtr1ubq.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 25.13KB 5.40KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 38.46KB 10.17KB
auth (createAuthenticatedFetch.js) 6.34KB 2.43KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.02KB 0.88KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 489.28KB 108.47KB
core (index.js) 3.79KB 1.52KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 163.56KB 44.83KB
fields (index.js) 230.37KB 57.17KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.84KB 1.45KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.98KB 10.85KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.86KB 12.91KB
plugin-charts (index.js) 62.10KB 17.67KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 121.04KB 31.57KB
plugin-designer (index.js) 212.58KB 42.83KB
plugin-detail (index.js) 239.93KB 60.01KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.72KB 27.70KB
plugin-gantt (index.js) 164.30KB 40.02KB
plugin-grid (index.js) 190.02KB 50.48KB
plugin-kanban (index.js) 52.74KB 14.53KB
plugin-list (index.js) 112.01KB 27.27KB
plugin-map (index.js) 18.16KB 5.81KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 41.38KB 11.09KB
plugin-timeline (index.js) 26.68KB 7.66KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.09KB 20.56KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 27.64KB 9.44KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.26KB 0.67KB
react (schema-input.js) 1.45KB 0.83KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

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

Projects

None yet

2 participants