Skip to content

fix(fields): FieldEditWidget forwards the DOM pass-through block it declares - #7009

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6909-fieldeditwidget-passthrough
Aug 31, 2026
Merged

fix(fields): FieldEditWidget forwards the DOM pass-through block it declares#7009
os-sam merged 1 commit into
mainfrom
claude/issue-6909-fieldeditwidget-passthrough

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6909

FieldEditWidget declared the whole DOM pass-through block and delivered one key of it. It now delivers the block.

Direction 1 from the card, and only direction 1. Direction 2 (narrowing this component's published prop type to the five keys it honoured) is deliberately not taken here: it would break any caller passing those keys today, and it is a contract decision for the maintainer.

The defect

The component's props are FieldWidgetComponentProps — the controlled-input keys intersected with FieldWidgetDomProps, AriaAttributes and the open data- family. A host could always pass id, name, autoFocus, tabIndex, onBlur, onFocus, onClick, any aria- and any data- attribute with no type error. The body then destructured field, value, onChange, readonly, autoFocus and rendered the widget with those five plus compact. autoFocus was the only survivor of the whole block.

That is this package's own first-class defect class, named in packages/fields/src/widgets/toDomProps.ts: a key that type-checks, reads as supported, and silently never reaches the element (#3290's aria-required, #3222's validation slot). toDomProps binds the WIDGET contract to its whitelist with compile-time assertions in both directions. Nothing bound this factory to either, so the factory was the one unbound link in the chain.

Not a widening, and not a contract change

The keys were already declared, and every widget still re-filters through its own toDomProps before anything reaches a DOM element. What any widget accepts or rejects is unchanged — this only makes the implementation deliver what the declaration already promised.

The fix, and why it is the reuse rather than a new list

The factory hands the widget toDomProps(props) — this package's own executor of the declaration — instead of a second key list written out in the factory. That reuse is the structural guard: toDomProps.ts's direction-2 assertion already makes keyof FieldWidgetDomProps extends DomPassThroughKey a compile error to violate, so a key added to the declared DOM block now reaches the widget through this factory automatically. One mechanism, one judge. A private list here would have been free to drift, which is exactly how the factory came to deliver one key out of seven.

The forwarded set is a deliberate superset of FieldWidgetDomProps: it also carries className and disabled, declared on the controlled-input block and forwarded by the same executor for the reason stated there (withholding them makes it a silent styling- and interactivity-dropper). Semantic props — field, value, onChange, readonly, and compact for the relational pickers — stay explicit and are applied after the spread, so a host cannot displace them.

The triage measurement the card asked for

Per host, read at the call site on this branch's base:

hostfilekeys passed to FieldEditWidgetpasses a dropped key?
ObjectGrid.renderCellEditorpackages/plugin-grid/src/ObjectGrid.tsx:3690field, value, onChangeno
InlineFieldInputpackages/plugin-detail/src/InlineFieldInput.tsx:434field, value, onChange, autoFocusno — autoFocus is the one key that already survived
RequiredFieldsDialogpackages/plugin-kanban/src/RequiredFieldsDialog.tsx:125field, value, onChange, readonlyno

Those are the only three JSX call sites in the repo outside this package's own tests (grep -rn "FieldEditWidget" --include=*.tsx --include=*.ts over packages and apps).

So: plain repair, not a live regression — no host is silently losing a key today. But one host had already WORKED AROUND the drop. RequiredFieldsDialog wraps every control in a label and says why: "FieldEditWidget renders the widget itself and takes no id to associate with, and widening its contract belongs to @object-ui/fields, not to a caller." The premise was false at the type level all along — id is declared — and true only at the implementation level, which is this defect. It takes an id now.

Card measurement re-derived, not inherited: EDIT_WIDGETS has 31 keys resolving to 27 distinct components; 26 call toDomProps themselves and UserField delegates its whole props object to LookupField via {...(props as any)}; none strips onBlur — the five that own a blur handler compose the host's (#6780, #6802). That matches the card's 27 / 26 / 1 exactly.

Evidence the fix is load-bearing

Pin: packages/fields/src/__tests__/FieldEditWidget.domPassThrough-6909.test.tsx, written before the fix.

RED first (pin only, before touching the factory) — Test Files 1 failed (1) · Tests 3 failed (3). The boundary assertion printed the defect directly:

AssertionError: expected [ 'autoFocus', 'field', ...(3) ] to deeply equal [ 'aria-label', 'autoFocus', ...(13) ]
- "aria-label" - "className" - "data-os6909" - "disabled"
- "id" - "name" - "onBlur" - "onClick" - "onFocus" - "tabIndex"

GREEN after — same command, Test Files 1 passed (1) · Tests 3 passed (3).

Ablation — revert only the forwarding (toDomProps(props) spread replaced by autoFocus={props.autoFocus}, i.e. the exact pre-fix shape) and the pin goes red again with the identical 5-key set. No build or dist leg is involved and that is measured, not assumed: vitest.config.mts aliases @object-ui/fields to packages/fields/src, and the pin imports ../FieldEditWidget relatively, so the test executes the source file the script mutates (both asserted in the script's preflight).

Mutation proved on disk by anchored counts in BOTH directions plus hashes, never by an exit code:

PRE forward-anchor 1 · ablation-anchor 0 · worktree 70ad0de26e489c5d99a4fa6d9f2b85a4e9a2b934 (== HEAD blob)
POST forward-anchor 0 · ablation-anchor 1 · worktree fa983d0c4ab7b96c417a3c2a5ed02fa52f98b5b0
RUN Test Files 1 failed (1) · Tests 3 failed (3) · vitest exit 1
REST worktree 70ad0de26e489c5d99a4fa6d9f2b85a4e9a2b934 == HEAD blob · git diff HEAD empty · anchors back to 1 / 0

The restore is git checkout HEAD -- <absolute path> (never bare — a bare restore reads the index, which a mutation can pollute), under trap ... EXIT INT TERM, with the repo root taken from git rev-parse --show-toplevel.

Control — an undeclared authored key (zzcanary) must still be dropped, or "everything forwards now" would be indistinguishable from having reopened the bare spread toDomProps exists to prevent. It is asserted at two points, because the DOM alone cannot see a reopened spread (each widget re-filters and would quietly rescue the mistake):

  • at the factory boundary, as an EXACT prop-key set — zzcanary absent, and no other extra key;
  • at the DOM, with the data- probe present in the same render so the absence is a measurement and not an empty tree.

Verification

Union run after the final commit, at 0071a0825 (git status --porcelain empty):

  • pnpm exec vitest run packages/fields/Test Files 126 passed (126) · Tests 2105 passed (2105)
  • pnpm exec vitest run packages/plugin-grid/ packages/plugin-detail/ packages/plugin-kanban/ (the three hosts) — Test Files 235 passed (235) · Tests 2105 passed (2105)
  • pnpm --filter @object-ui/fields --filter @object-ui/components type-check — both Done, exit 0. The new test file is a real program input, not merely present: before a typing fix, this same command failed with src/__tests__/FieldEditWidget.domPassThrough-6909.test.tsx(168,24): error TS2769, which is what proves tsconfig.test.json compiles it.
  • pnpm exec eslint . --format json (plain form) — 4035 files, 0 errors, exit 0. Repo-wide, so no narrowing to justify.
  • Gates: check-changeset-presence "3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)"; check-changeset-no-major, check-changeset-fixed, check-changeset-overwrite, check-control-bytes (5815 files), check-side-effects-array, check-entry-guard, check-type-check-coverage (45/46), check-lint-coverage (46/46), check-package-self-import, check-node-esm-load --specifiers-only, check-vi-mock-specifiers, check-vi-mock-inherit — all exit 0.
  • check-eager-closure-budget is NOT MEASURED locally, not green and not red: it needs apps/console/dist/eager-closure.json from a console build this branch never ran, and it says so itself ("This is a broken gauge, not a passing budget"). Nothing in this diff touches apps/console or the eager-closure surface. CI builds the console first.

Dependency closure built before any type-check (pnpm --filter '@object-ui/fields^...' build), so no unbuilt-tree false red.

One bounded in-place correction, declared

packages/components/src/renderers/complex/data-table.tsx — comment only, no code. Its injected-editor comment justified the document-level pointerdown listener partly with "FieldEditWidget forwards autoFocus and nothing else out of the DOM block, so a host handler could not reach the control through it even if one were passed." This PR makes that sentence false, so it is corrected in the same change rather than left to rot — that comment was itself landed to replace an earlier stale one (#6912, for #6859), and this repo treats a confidently wrong comment as a first-class defect.

The listener stays load-bearing, on the untouched half of the same reason: the renderCellEditor context object still has nowhere to put an onBlur. Widening it is a DataTableSchema contract change and is not proposed here. No test pinned the corrected sentence (grepped), and packages/components type-check and the repo-wide eslint are green above.

Not in this PR

FieldEditWidget also does not forward the DECLARED non-DOM host-plumbing keys — error, dataSource, dependentValues, dependsOn, dependsOnLabels, emptyHint, onUploadingChange, onSelectRecord, onCreateNew. Same declared-but-not-delivered shape, but each carries real design content (error drives aria-invalid; who produces dependsOnLabels for an inline host is a question, not a mechanical answer), so it is not a mechanical extension of this repair. That half is filed as #7008 and remains open — out of scope here.

Generated by Claude Code


Generated by Claude Code

…eclares
Its props are `FieldWidgetComponentProps` — the controlled-input keys
intersected with `FieldWidgetDomProps`, `AriaAttributes` and the open `data-`
family — so a host could always pass `id`, `name`, `autoFocus`, `tabIndex`,
`onBlur`, `onFocus`, `onClick`, any `aria-*` and any `data-*` with no type
error. The body destructured five keys and rendered the widget with those, so
`autoFocus` was the only survivor of the whole block.
The factory now hands the widget `toDomProps(props)` — this package's own
executor of that declaration — rather than a second key list. The reuse is the
guard: `toDomProps.ts`'s direction-2 assertion makes
`keyof FieldWidgetDomProps extends DomPassThroughKey` a compile error to
violate, so a key added to the declared block reaches the widget through this
factory automatically. Semantic props stay explicit and are applied after the
spread.
Not a widening: the keys were already declared, and every widget still
re-filters through its own `toDomProps` before anything reaches the DOM.
Also corrects the `data-table.tsx` comment this falsifies — the injected-editor
listener's other justification (the `renderCellEditor` context has nowhere to
put an `onBlur`) is untouched and still load-bearing.
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, 45 chunks)3180.3 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-DXcoi8Tk.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)12.46KB4.71KB
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.08KB116.42KB
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.63KB
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.26KB63.17KB
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.08KB54.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.05KB8.37KB
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

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

Projects

None yet

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
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;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(fields): FieldEditWidget forwards the DOM pass-through block it declares by claude[bot] · Pull Request #7009 · objectstack-ai/objectui · GitHub
Skip to content

fix(fields): FieldEditWidget forwards the DOM pass-through block it declares - #7009

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6909-fieldeditwidget-passthrough
Aug 31, 2026
Merged

fix(fields): FieldEditWidget forwards the DOM pass-through block it declares#7009
os-sam merged 1 commit into
mainfrom
claude/issue-6909-fieldeditwidget-passthrough

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6909

FieldEditWidget declared the whole DOM pass-through block and delivered one key of it. It now delivers the block.

Direction 1 from the card, and only direction 1. Direction 2 (narrowing this component's published prop type to the five keys it honoured) is deliberately not taken here: it would break any caller passing those keys today, and it is a contract decision for the maintainer.

The defect

The component's props are FieldWidgetComponentProps — the controlled-input keys intersected with FieldWidgetDomProps, AriaAttributes and the open data- family. A host could always pass id, name, autoFocus, tabIndex, onBlur, onFocus, onClick, any aria- and any data- attribute with no type error. The body then destructured field, value, onChange, readonly, autoFocus and rendered the widget with those five plus compact. autoFocus was the only survivor of the whole block.

That is this package's own first-class defect class, named in packages/fields/src/widgets/toDomProps.ts: a key that type-checks, reads as supported, and silently never reaches the element (#3290's aria-required, #3222's validation slot). toDomProps binds the WIDGET contract to its whitelist with compile-time assertions in both directions. Nothing bound this factory to either, so the factory was the one unbound link in the chain.

Not a widening, and not a contract change

The keys were already declared, and every widget still re-filters through its own toDomProps before anything reaches a DOM element. What any widget accepts or rejects is unchanged — this only makes the implementation deliver what the declaration already promised.

The fix, and why it is the reuse rather than a new list

The factory hands the widget toDomProps(props) — this package's own executor of the declaration — instead of a second key list written out in the factory. That reuse is the structural guard: toDomProps.ts's direction-2 assertion already makes keyof FieldWidgetDomProps extends DomPassThroughKey a compile error to violate, so a key added to the declared DOM block now reaches the widget through this factory automatically. One mechanism, one judge. A private list here would have been free to drift, which is exactly how the factory came to deliver one key out of seven.

The forwarded set is a deliberate superset of FieldWidgetDomProps: it also carries className and disabled, declared on the controlled-input block and forwarded by the same executor for the reason stated there (withholding them makes it a silent styling- and interactivity-dropper). Semantic props — field, value, onChange, readonly, and compact for the relational pickers — stay explicit and are applied after the spread, so a host cannot displace them.

The triage measurement the card asked for

Per host, read at the call site on this branch's base:

hostfilekeys passed to FieldEditWidgetpasses a dropped key?
ObjectGrid.renderCellEditorpackages/plugin-grid/src/ObjectGrid.tsx:3690field, value, onChangeno
InlineFieldInputpackages/plugin-detail/src/InlineFieldInput.tsx:434field, value, onChange, autoFocusno — autoFocus is the one key that already survived
RequiredFieldsDialogpackages/plugin-kanban/src/RequiredFieldsDialog.tsx:125field, value, onChange, readonlyno

Those are the only three JSX call sites in the repo outside this package's own tests (grep -rn "FieldEditWidget" --include=*.tsx --include=*.ts over packages and apps).

So: plain repair, not a live regression — no host is silently losing a key today. But one host had already WORKED AROUND the drop. RequiredFieldsDialog wraps every control in a label and says why: "FieldEditWidget renders the widget itself and takes no id to associate with, and widening its contract belongs to @object-ui/fields, not to a caller." The premise was false at the type level all along — id is declared — and true only at the implementation level, which is this defect. It takes an id now.

Card measurement re-derived, not inherited: EDIT_WIDGETS has 31 keys resolving to 27 distinct components; 26 call toDomProps themselves and UserField delegates its whole props object to LookupField via {...(props as any)}; none strips onBlur — the five that own a blur handler compose the host's (#6780, #6802). That matches the card's 27 / 26 / 1 exactly.

Evidence the fix is load-bearing

Pin: packages/fields/src/__tests__/FieldEditWidget.domPassThrough-6909.test.tsx, written before the fix.

RED first (pin only, before touching the factory) — Test Files 1 failed (1) · Tests 3 failed (3). The boundary assertion printed the defect directly:

AssertionError: expected [ 'autoFocus', 'field', ...(3) ] to deeply equal [ 'aria-label', 'autoFocus', ...(13) ]
- "aria-label" - "className" - "data-os6909" - "disabled"
- "id" - "name" - "onBlur" - "onClick" - "onFocus" - "tabIndex"

GREEN after — same command, Test Files 1 passed (1) · Tests 3 passed (3).

Ablation — revert only the forwarding (toDomProps(props) spread replaced by autoFocus={props.autoFocus}, i.e. the exact pre-fix shape) and the pin goes red again with the identical 5-key set. No build or dist leg is involved and that is measured, not assumed: vitest.config.mts aliases @object-ui/fields to packages/fields/src, and the pin imports ../FieldEditWidget relatively, so the test executes the source file the script mutates (both asserted in the script's preflight).

Mutation proved on disk by anchored counts in BOTH directions plus hashes, never by an exit code:

PRE forward-anchor 1 · ablation-anchor 0 · worktree 70ad0de26e489c5d99a4fa6d9f2b85a4e9a2b934 (== HEAD blob)
POST forward-anchor 0 · ablation-anchor 1 · worktree fa983d0c4ab7b96c417a3c2a5ed02fa52f98b5b0
RUN Test Files 1 failed (1) · Tests 3 failed (3) · vitest exit 1
REST worktree 70ad0de26e489c5d99a4fa6d9f2b85a4e9a2b934 == HEAD blob · git diff HEAD empty · anchors back to 1 / 0

The restore is git checkout HEAD -- <absolute path> (never bare — a bare restore reads the index, which a mutation can pollute), under trap ... EXIT INT TERM, with the repo root taken from git rev-parse --show-toplevel.

Control — an undeclared authored key (zzcanary) must still be dropped, or "everything forwards now" would be indistinguishable from having reopened the bare spread toDomProps exists to prevent. It is asserted at two points, because the DOM alone cannot see a reopened spread (each widget re-filters and would quietly rescue the mistake):

  • at the factory boundary, as an EXACT prop-key set — zzcanary absent, and no other extra key;
  • at the DOM, with the data- probe present in the same render so the absence is a measurement and not an empty tree.

Verification

Union run after the final commit, at 0071a0825 (git status --porcelain empty):

  • pnpm exec vitest run packages/fields/Test Files 126 passed (126) · Tests 2105 passed (2105)
  • pnpm exec vitest run packages/plugin-grid/ packages/plugin-detail/ packages/plugin-kanban/ (the three hosts) — Test Files 235 passed (235) · Tests 2105 passed (2105)
  • pnpm --filter @object-ui/fields --filter @object-ui/components type-check — both Done, exit 0. The new test file is a real program input, not merely present: before a typing fix, this same command failed with src/__tests__/FieldEditWidget.domPassThrough-6909.test.tsx(168,24): error TS2769, which is what proves tsconfig.test.json compiles it.
  • pnpm exec eslint . --format json (plain form) — 4035 files, 0 errors, exit 0. Repo-wide, so no narrowing to justify.
  • Gates: check-changeset-presence "3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)"; check-changeset-no-major, check-changeset-fixed, check-changeset-overwrite, check-control-bytes (5815 files), check-side-effects-array, check-entry-guard, check-type-check-coverage (45/46), check-lint-coverage (46/46), check-package-self-import, check-node-esm-load --specifiers-only, check-vi-mock-specifiers, check-vi-mock-inherit — all exit 0.
  • check-eager-closure-budget is NOT MEASURED locally, not green and not red: it needs apps/console/dist/eager-closure.json from a console build this branch never ran, and it says so itself ("This is a broken gauge, not a passing budget"). Nothing in this diff touches apps/console or the eager-closure surface. CI builds the console first.

Dependency closure built before any type-check (pnpm --filter '@object-ui/fields^...' build), so no unbuilt-tree false red.

One bounded in-place correction, declared

packages/components/src/renderers/complex/data-table.tsx — comment only, no code. Its injected-editor comment justified the document-level pointerdown listener partly with "FieldEditWidget forwards autoFocus and nothing else out of the DOM block, so a host handler could not reach the control through it even if one were passed." This PR makes that sentence false, so it is corrected in the same change rather than left to rot — that comment was itself landed to replace an earlier stale one (#6912, for #6859), and this repo treats a confidently wrong comment as a first-class defect.

The listener stays load-bearing, on the untouched half of the same reason: the renderCellEditor context object still has nowhere to put an onBlur. Widening it is a DataTableSchema contract change and is not proposed here. No test pinned the corrected sentence (grepped), and packages/components type-check and the repo-wide eslint are green above.

Not in this PR

FieldEditWidget also does not forward the DECLARED non-DOM host-plumbing keys — error, dataSource, dependentValues, dependsOn, dependsOnLabels, emptyHint, onUploadingChange, onSelectRecord, onCreateNew. Same declared-but-not-delivered shape, but each carries real design content (error drives aria-invalid; who produces dependsOnLabels for an inline host is a question, not a mechanical answer), so it is not a mechanical extension of this repair. That half is filed as #7008 and remains open — out of scope here.

Generated by Claude Code


Generated by Claude Code

…eclares
Its props are `FieldWidgetComponentProps` — the controlled-input keys
intersected with `FieldWidgetDomProps`, `AriaAttributes` and the open `data-`
family — so a host could always pass `id`, `name`, `autoFocus`, `tabIndex`,
`onBlur`, `onFocus`, `onClick`, any `aria-*` and any `data-*` with no type
error. The body destructured five keys and rendered the widget with those, so
`autoFocus` was the only survivor of the whole block.
The factory now hands the widget `toDomProps(props)` — this package's own
executor of that declaration — rather than a second key list. The reuse is the
guard: `toDomProps.ts`'s direction-2 assertion makes
`keyof FieldWidgetDomProps extends DomPassThroughKey` a compile error to
violate, so a key added to the declared block reaches the widget through this
factory automatically. Semantic props stay explicit and are applied after the
spread.
Not a widening: the keys were already declared, and every widget still
re-filters through its own `toDomProps` before anything reaches the DOM.
Also corrects the `data-table.tsx` comment this falsifies — the injected-editor
listener's other justification (the `renderCellEditor` context has nowhere to
put an `onBlur`) is untouched and still load-bearing.
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, 45 chunks)3180.3 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-DXcoi8Tk.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)12.46KB4.71KB
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.08KB116.42KB
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.63KB
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.26KB63.17KB
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.08KB54.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.05KB8.37KB
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

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

Projects

None yet

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(fields): FieldEditWidget forwards the DOM pass-through block it declares by claude[bot] · Pull Request #7009 · objectstack-ai/objectui · GitHub
Skip to content

fix(fields): FieldEditWidget forwards the DOM pass-through block it declares - #7009

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6909-fieldeditwidget-passthrough
Aug 31, 2026
Merged

fix(fields): FieldEditWidget forwards the DOM pass-through block it declares#7009
os-sam merged 1 commit into
mainfrom
claude/issue-6909-fieldeditwidget-passthrough

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6909

FieldEditWidget declared the whole DOM pass-through block and delivered one key of it. It now delivers the block.

Direction 1 from the card, and only direction 1. Direction 2 (narrowing this component's published prop type to the five keys it honoured) is deliberately not taken here: it would break any caller passing those keys today, and it is a contract decision for the maintainer.

The defect

The component's props are FieldWidgetComponentProps — the controlled-input keys intersected with FieldWidgetDomProps, AriaAttributes and the open data- family. A host could always pass id, name, autoFocus, tabIndex, onBlur, onFocus, onClick, any aria- and any data- attribute with no type error. The body then destructured field, value, onChange, readonly, autoFocus and rendered the widget with those five plus compact. autoFocus was the only survivor of the whole block.

That is this package's own first-class defect class, named in packages/fields/src/widgets/toDomProps.ts: a key that type-checks, reads as supported, and silently never reaches the element (#3290's aria-required, #3222's validation slot). toDomProps binds the WIDGET contract to its whitelist with compile-time assertions in both directions. Nothing bound this factory to either, so the factory was the one unbound link in the chain.

Not a widening, and not a contract change

The keys were already declared, and every widget still re-filters through its own toDomProps before anything reaches a DOM element. What any widget accepts or rejects is unchanged — this only makes the implementation deliver what the declaration already promised.

The fix, and why it is the reuse rather than a new list

The factory hands the widget toDomProps(props) — this package's own executor of the declaration — instead of a second key list written out in the factory. That reuse is the structural guard: toDomProps.ts's direction-2 assertion already makes keyof FieldWidgetDomProps extends DomPassThroughKey a compile error to violate, so a key added to the declared DOM block now reaches the widget through this factory automatically. One mechanism, one judge. A private list here would have been free to drift, which is exactly how the factory came to deliver one key out of seven.

The forwarded set is a deliberate superset of FieldWidgetDomProps: it also carries className and disabled, declared on the controlled-input block and forwarded by the same executor for the reason stated there (withholding them makes it a silent styling- and interactivity-dropper). Semantic props — field, value, onChange, readonly, and compact for the relational pickers — stay explicit and are applied after the spread, so a host cannot displace them.

The triage measurement the card asked for

Per host, read at the call site on this branch's base:

hostfilekeys passed to FieldEditWidgetpasses a dropped key?
ObjectGrid.renderCellEditorpackages/plugin-grid/src/ObjectGrid.tsx:3690field, value, onChangeno
InlineFieldInputpackages/plugin-detail/src/InlineFieldInput.tsx:434field, value, onChange, autoFocusno — autoFocus is the one key that already survived
RequiredFieldsDialogpackages/plugin-kanban/src/RequiredFieldsDialog.tsx:125field, value, onChange, readonlyno

Those are the only three JSX call sites in the repo outside this package's own tests (grep -rn "FieldEditWidget" --include=*.tsx --include=*.ts over packages and apps).

So: plain repair, not a live regression — no host is silently losing a key today. But one host had already WORKED AROUND the drop. RequiredFieldsDialog wraps every control in a label and says why: "FieldEditWidget renders the widget itself and takes no id to associate with, and widening its contract belongs to @object-ui/fields, not to a caller." The premise was false at the type level all along — id is declared — and true only at the implementation level, which is this defect. It takes an id now.

Card measurement re-derived, not inherited: EDIT_WIDGETS has 31 keys resolving to 27 distinct components; 26 call toDomProps themselves and UserField delegates its whole props object to LookupField via {...(props as any)}; none strips onBlur — the five that own a blur handler compose the host's (#6780, #6802). That matches the card's 27 / 26 / 1 exactly.

Evidence the fix is load-bearing

Pin: packages/fields/src/__tests__/FieldEditWidget.domPassThrough-6909.test.tsx, written before the fix.

RED first (pin only, before touching the factory) — Test Files 1 failed (1) · Tests 3 failed (3). The boundary assertion printed the defect directly:

AssertionError: expected [ 'autoFocus', 'field', ...(3) ] to deeply equal [ 'aria-label', 'autoFocus', ...(13) ]
- "aria-label" - "className" - "data-os6909" - "disabled"
- "id" - "name" - "onBlur" - "onClick" - "onFocus" - "tabIndex"

GREEN after — same command, Test Files 1 passed (1) · Tests 3 passed (3).

Ablation — revert only the forwarding (toDomProps(props) spread replaced by autoFocus={props.autoFocus}, i.e. the exact pre-fix shape) and the pin goes red again with the identical 5-key set. No build or dist leg is involved and that is measured, not assumed: vitest.config.mts aliases @object-ui/fields to packages/fields/src, and the pin imports ../FieldEditWidget relatively, so the test executes the source file the script mutates (both asserted in the script's preflight).

Mutation proved on disk by anchored counts in BOTH directions plus hashes, never by an exit code:

PRE forward-anchor 1 · ablation-anchor 0 · worktree 70ad0de26e489c5d99a4fa6d9f2b85a4e9a2b934 (== HEAD blob)
POST forward-anchor 0 · ablation-anchor 1 · worktree fa983d0c4ab7b96c417a3c2a5ed02fa52f98b5b0
RUN Test Files 1 failed (1) · Tests 3 failed (3) · vitest exit 1
REST worktree 70ad0de26e489c5d99a4fa6d9f2b85a4e9a2b934 == HEAD blob · git diff HEAD empty · anchors back to 1 / 0

The restore is git checkout HEAD -- <absolute path> (never bare — a bare restore reads the index, which a mutation can pollute), under trap ... EXIT INT TERM, with the repo root taken from git rev-parse --show-toplevel.

Control — an undeclared authored key (zzcanary) must still be dropped, or "everything forwards now" would be indistinguishable from having reopened the bare spread toDomProps exists to prevent. It is asserted at two points, because the DOM alone cannot see a reopened spread (each widget re-filters and would quietly rescue the mistake):

  • at the factory boundary, as an EXACT prop-key set — zzcanary absent, and no other extra key;
  • at the DOM, with the data- probe present in the same render so the absence is a measurement and not an empty tree.

Verification

Union run after the final commit, at 0071a0825 (git status --porcelain empty):

  • pnpm exec vitest run packages/fields/Test Files 126 passed (126) · Tests 2105 passed (2105)
  • pnpm exec vitest run packages/plugin-grid/ packages/plugin-detail/ packages/plugin-kanban/ (the three hosts) — Test Files 235 passed (235) · Tests 2105 passed (2105)
  • pnpm --filter @object-ui/fields --filter @object-ui/components type-check — both Done, exit 0. The new test file is a real program input, not merely present: before a typing fix, this same command failed with src/__tests__/FieldEditWidget.domPassThrough-6909.test.tsx(168,24): error TS2769, which is what proves tsconfig.test.json compiles it.
  • pnpm exec eslint . --format json (plain form) — 4035 files, 0 errors, exit 0. Repo-wide, so no narrowing to justify.
  • Gates: check-changeset-presence "3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)"; check-changeset-no-major, check-changeset-fixed, check-changeset-overwrite, check-control-bytes (5815 files), check-side-effects-array, check-entry-guard, check-type-check-coverage (45/46), check-lint-coverage (46/46), check-package-self-import, check-node-esm-load --specifiers-only, check-vi-mock-specifiers, check-vi-mock-inherit — all exit 0.
  • check-eager-closure-budget is NOT MEASURED locally, not green and not red: it needs apps/console/dist/eager-closure.json from a console build this branch never ran, and it says so itself ("This is a broken gauge, not a passing budget"). Nothing in this diff touches apps/console or the eager-closure surface. CI builds the console first.

Dependency closure built before any type-check (pnpm --filter '@object-ui/fields^...' build), so no unbuilt-tree false red.

One bounded in-place correction, declared

packages/components/src/renderers/complex/data-table.tsx — comment only, no code. Its injected-editor comment justified the document-level pointerdown listener partly with "FieldEditWidget forwards autoFocus and nothing else out of the DOM block, so a host handler could not reach the control through it even if one were passed." This PR makes that sentence false, so it is corrected in the same change rather than left to rot — that comment was itself landed to replace an earlier stale one (#6912, for #6859), and this repo treats a confidently wrong comment as a first-class defect.

The listener stays load-bearing, on the untouched half of the same reason: the renderCellEditor context object still has nowhere to put an onBlur. Widening it is a DataTableSchema contract change and is not proposed here. No test pinned the corrected sentence (grepped), and packages/components type-check and the repo-wide eslint are green above.

Not in this PR

FieldEditWidget also does not forward the DECLARED non-DOM host-plumbing keys — error, dataSource, dependentValues, dependsOn, dependsOnLabels, emptyHint, onUploadingChange, onSelectRecord, onCreateNew. Same declared-but-not-delivered shape, but each carries real design content (error drives aria-invalid; who produces dependsOnLabels for an inline host is a question, not a mechanical answer), so it is not a mechanical extension of this repair. That half is filed as #7008 and remains open — out of scope here.

Generated by Claude Code


Generated by Claude Code

…eclares
Its props are `FieldWidgetComponentProps` — the controlled-input keys
intersected with `FieldWidgetDomProps`, `AriaAttributes` and the open `data-`
family — so a host could always pass `id`, `name`, `autoFocus`, `tabIndex`,
`onBlur`, `onFocus`, `onClick`, any `aria-*` and any `data-*` with no type
error. The body destructured five keys and rendered the widget with those, so
`autoFocus` was the only survivor of the whole block.
The factory now hands the widget `toDomProps(props)` — this package's own
executor of that declaration — rather than a second key list. The reuse is the
guard: `toDomProps.ts`'s direction-2 assertion makes
`keyof FieldWidgetDomProps extends DomPassThroughKey` a compile error to
violate, so a key added to the declared block reaches the widget through this
factory automatically. Semantic props stay explicit and are applied after the
spread.
Not a widening: the keys were already declared, and every widget still
re-filters through its own `toDomProps` before anything reaches the DOM.
Also corrects the `data-table.tsx` comment this falsifies — the injected-editor
listener's other justification (the `renderCellEditor` context has nowhere to
put an `onBlur`) is untouched and still load-bearing.
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, 45 chunks)3180.3 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-DXcoi8Tk.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)12.46KB4.71KB
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.08KB116.42KB
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.63KB
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.26KB63.17KB
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.08KB54.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.05KB8.37KB
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

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

Projects

None yet

2 participants

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

fix(fields): FieldEditWidget forwards the DOM pass-through block it declares - #7009

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6909-fieldeditwidget-passthrough
Aug 31, 2026
Merged

fix(fields): FieldEditWidget forwards the DOM pass-through block it declares#7009
os-sam merged 1 commit into
mainfrom
claude/issue-6909-fieldeditwidget-passthrough

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6909

FieldEditWidget declared the whole DOM pass-through block and delivered one key of it. It now delivers the block.

Direction 1 from the card, and only direction 1. Direction 2 (narrowing this component's published prop type to the five keys it honoured) is deliberately not taken here: it would break any caller passing those keys today, and it is a contract decision for the maintainer.

The defect

The component's props are FieldWidgetComponentProps — the controlled-input keys intersected with FieldWidgetDomProps, AriaAttributes and the open data- family. A host could always pass id, name, autoFocus, tabIndex, onBlur, onFocus, onClick, any aria- and any data- attribute with no type error. The body then destructured field, value, onChange, readonly, autoFocus and rendered the widget with those five plus compact. autoFocus was the only survivor of the whole block.

That is this package's own first-class defect class, named in packages/fields/src/widgets/toDomProps.ts: a key that type-checks, reads as supported, and silently never reaches the element (#3290's aria-required, #3222's validation slot). toDomProps binds the WIDGET contract to its whitelist with compile-time assertions in both directions. Nothing bound this factory to either, so the factory was the one unbound link in the chain.

Not a widening, and not a contract change

The keys were already declared, and every widget still re-filters through its own toDomProps before anything reaches a DOM element. What any widget accepts or rejects is unchanged — this only makes the implementation deliver what the declaration already promised.

The fix, and why it is the reuse rather than a new list

The factory hands the widget toDomProps(props) — this package's own executor of the declaration — instead of a second key list written out in the factory. That reuse is the structural guard: toDomProps.ts's direction-2 assertion already makes keyof FieldWidgetDomProps extends DomPassThroughKey a compile error to violate, so a key added to the declared DOM block now reaches the widget through this factory automatically. One mechanism, one judge. A private list here would have been free to drift, which is exactly how the factory came to deliver one key out of seven.

The forwarded set is a deliberate superset of FieldWidgetDomProps: it also carries className and disabled, declared on the controlled-input block and forwarded by the same executor for the reason stated there (withholding them makes it a silent styling- and interactivity-dropper). Semantic props — field, value, onChange, readonly, and compact for the relational pickers — stay explicit and are applied after the spread, so a host cannot displace them.

The triage measurement the card asked for

Per host, read at the call site on this branch's base:

hostfilekeys passed to FieldEditWidgetpasses a dropped key?
ObjectGrid.renderCellEditorpackages/plugin-grid/src/ObjectGrid.tsx:3690field, value, onChangeno
InlineFieldInputpackages/plugin-detail/src/InlineFieldInput.tsx:434field, value, onChange, autoFocusno — autoFocus is the one key that already survived
RequiredFieldsDialogpackages/plugin-kanban/src/RequiredFieldsDialog.tsx:125field, value, onChange, readonlyno

Those are the only three JSX call sites in the repo outside this package's own tests (grep -rn "FieldEditWidget" --include=*.tsx --include=*.ts over packages and apps).

So: plain repair, not a live regression — no host is silently losing a key today. But one host had already WORKED AROUND the drop. RequiredFieldsDialog wraps every control in a label and says why: "FieldEditWidget renders the widget itself and takes no id to associate with, and widening its contract belongs to @object-ui/fields, not to a caller." The premise was false at the type level all along — id is declared — and true only at the implementation level, which is this defect. It takes an id now.

Card measurement re-derived, not inherited: EDIT_WIDGETS has 31 keys resolving to 27 distinct components; 26 call toDomProps themselves and UserField delegates its whole props object to LookupField via {...(props as any)}; none strips onBlur — the five that own a blur handler compose the host's (#6780, #6802). That matches the card's 27 / 26 / 1 exactly.

Evidence the fix is load-bearing

Pin: packages/fields/src/__tests__/FieldEditWidget.domPassThrough-6909.test.tsx, written before the fix.

RED first (pin only, before touching the factory) — Test Files 1 failed (1) · Tests 3 failed (3). The boundary assertion printed the defect directly:

AssertionError: expected [ 'autoFocus', 'field', ...(3) ] to deeply equal [ 'aria-label', 'autoFocus', ...(13) ]
- "aria-label" - "className" - "data-os6909" - "disabled"
- "id" - "name" - "onBlur" - "onClick" - "onFocus" - "tabIndex"

GREEN after — same command, Test Files 1 passed (1) · Tests 3 passed (3).

Ablation — revert only the forwarding (toDomProps(props) spread replaced by autoFocus={props.autoFocus}, i.e. the exact pre-fix shape) and the pin goes red again with the identical 5-key set. No build or dist leg is involved and that is measured, not assumed: vitest.config.mts aliases @object-ui/fields to packages/fields/src, and the pin imports ../FieldEditWidget relatively, so the test executes the source file the script mutates (both asserted in the script's preflight).

Mutation proved on disk by anchored counts in BOTH directions plus hashes, never by an exit code:

PRE forward-anchor 1 · ablation-anchor 0 · worktree 70ad0de26e489c5d99a4fa6d9f2b85a4e9a2b934 (== HEAD blob)
POST forward-anchor 0 · ablation-anchor 1 · worktree fa983d0c4ab7b96c417a3c2a5ed02fa52f98b5b0
RUN Test Files 1 failed (1) · Tests 3 failed (3) · vitest exit 1
REST worktree 70ad0de26e489c5d99a4fa6d9f2b85a4e9a2b934 == HEAD blob · git diff HEAD empty · anchors back to 1 / 0

The restore is git checkout HEAD -- <absolute path> (never bare — a bare restore reads the index, which a mutation can pollute), under trap ... EXIT INT TERM, with the repo root taken from git rev-parse --show-toplevel.

Control — an undeclared authored key (zzcanary) must still be dropped, or "everything forwards now" would be indistinguishable from having reopened the bare spread toDomProps exists to prevent. It is asserted at two points, because the DOM alone cannot see a reopened spread (each widget re-filters and would quietly rescue the mistake):

  • at the factory boundary, as an EXACT prop-key set — zzcanary absent, and no other extra key;
  • at the DOM, with the data- probe present in the same render so the absence is a measurement and not an empty tree.

Verification

Union run after the final commit, at 0071a0825 (git status --porcelain empty):

  • pnpm exec vitest run packages/fields/Test Files 126 passed (126) · Tests 2105 passed (2105)
  • pnpm exec vitest run packages/plugin-grid/ packages/plugin-detail/ packages/plugin-kanban/ (the three hosts) — Test Files 235 passed (235) · Tests 2105 passed (2105)
  • pnpm --filter @object-ui/fields --filter @object-ui/components type-check — both Done, exit 0. The new test file is a real program input, not merely present: before a typing fix, this same command failed with src/__tests__/FieldEditWidget.domPassThrough-6909.test.tsx(168,24): error TS2769, which is what proves tsconfig.test.json compiles it.
  • pnpm exec eslint . --format json (plain form) — 4035 files, 0 errors, exit 0. Repo-wide, so no narrowing to justify.
  • Gates: check-changeset-presence "3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)"; check-changeset-no-major, check-changeset-fixed, check-changeset-overwrite, check-control-bytes (5815 files), check-side-effects-array, check-entry-guard, check-type-check-coverage (45/46), check-lint-coverage (46/46), check-package-self-import, check-node-esm-load --specifiers-only, check-vi-mock-specifiers, check-vi-mock-inherit — all exit 0.
  • check-eager-closure-budget is NOT MEASURED locally, not green and not red: it needs apps/console/dist/eager-closure.json from a console build this branch never ran, and it says so itself ("This is a broken gauge, not a passing budget"). Nothing in this diff touches apps/console or the eager-closure surface. CI builds the console first.

Dependency closure built before any type-check (pnpm --filter '@object-ui/fields^...' build), so no unbuilt-tree false red.

One bounded in-place correction, declared

packages/components/src/renderers/complex/data-table.tsx — comment only, no code. Its injected-editor comment justified the document-level pointerdown listener partly with "FieldEditWidget forwards autoFocus and nothing else out of the DOM block, so a host handler could not reach the control through it even if one were passed." This PR makes that sentence false, so it is corrected in the same change rather than left to rot — that comment was itself landed to replace an earlier stale one (#6912, for #6859), and this repo treats a confidently wrong comment as a first-class defect.

The listener stays load-bearing, on the untouched half of the same reason: the renderCellEditor context object still has nowhere to put an onBlur. Widening it is a DataTableSchema contract change and is not proposed here. No test pinned the corrected sentence (grepped), and packages/components type-check and the repo-wide eslint are green above.

Not in this PR

FieldEditWidget also does not forward the DECLARED non-DOM host-plumbing keys — error, dataSource, dependentValues, dependsOn, dependsOnLabels, emptyHint, onUploadingChange, onSelectRecord, onCreateNew. Same declared-but-not-delivered shape, but each carries real design content (error drives aria-invalid; who produces dependsOnLabels for an inline host is a question, not a mechanical answer), so it is not a mechanical extension of this repair. That half is filed as #7008 and remains open — out of scope here.

Generated by Claude Code


Generated by Claude Code

…eclares
Its props are `FieldWidgetComponentProps` — the controlled-input keys
intersected with `FieldWidgetDomProps`, `AriaAttributes` and the open `data-`
family — so a host could always pass `id`, `name`, `autoFocus`, `tabIndex`,
`onBlur`, `onFocus`, `onClick`, any `aria-*` and any `data-*` with no type
error. The body destructured five keys and rendered the widget with those, so
`autoFocus` was the only survivor of the whole block.
The factory now hands the widget `toDomProps(props)` — this package's own
executor of that declaration — rather than a second key list. The reuse is the
guard: `toDomProps.ts`'s direction-2 assertion makes
`keyof FieldWidgetDomProps extends DomPassThroughKey` a compile error to
violate, so a key added to the declared block reaches the widget through this
factory automatically. Semantic props stay explicit and are applied after the
spread.
Not a widening: the keys were already declared, and every widget still
re-filters through its own `toDomProps` before anything reaches the DOM.
Also corrects the `data-table.tsx` comment this falsifies — the injected-editor
listener's other justification (the `renderCellEditor` context has nowhere to
put an `onBlur`) is untouched and still load-bearing.
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, 45 chunks)3180.3 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-DXcoi8Tk.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)12.46KB4.71KB
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.08KB116.42KB
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.63KB
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.26KB63.17KB
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.08KB54.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.05KB8.37KB
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

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

Projects

None yet

2 participants

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

fix(fields): FieldEditWidget forwards the DOM pass-through block it declares - #7009

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6909-fieldeditwidget-passthrough
Aug 31, 2026
Merged

fix(fields): FieldEditWidget forwards the DOM pass-through block it declares#7009
os-sam merged 1 commit into
mainfrom
claude/issue-6909-fieldeditwidget-passthrough

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6909

FieldEditWidget declared the whole DOM pass-through block and delivered one key of it. It now delivers the block.

Direction 1 from the card, and only direction 1. Direction 2 (narrowing this component's published prop type to the five keys it honoured) is deliberately not taken here: it would break any caller passing those keys today, and it is a contract decision for the maintainer.

The defect

The component's props are FieldWidgetComponentProps — the controlled-input keys intersected with FieldWidgetDomProps, AriaAttributes and the open data- family. A host could always pass id, name, autoFocus, tabIndex, onBlur, onFocus, onClick, any aria- and any data- attribute with no type error. The body then destructured field, value, onChange, readonly, autoFocus and rendered the widget with those five plus compact. autoFocus was the only survivor of the whole block.

That is this package's own first-class defect class, named in packages/fields/src/widgets/toDomProps.ts: a key that type-checks, reads as supported, and silently never reaches the element (#3290's aria-required, #3222's validation slot). toDomProps binds the WIDGET contract to its whitelist with compile-time assertions in both directions. Nothing bound this factory to either, so the factory was the one unbound link in the chain.

Not a widening, and not a contract change

The keys were already declared, and every widget still re-filters through its own toDomProps before anything reaches a DOM element. What any widget accepts or rejects is unchanged — this only makes the implementation deliver what the declaration already promised.

The fix, and why it is the reuse rather than a new list

The factory hands the widget toDomProps(props) — this package's own executor of the declaration — instead of a second key list written out in the factory. That reuse is the structural guard: toDomProps.ts's direction-2 assertion already makes keyof FieldWidgetDomProps extends DomPassThroughKey a compile error to violate, so a key added to the declared DOM block now reaches the widget through this factory automatically. One mechanism, one judge. A private list here would have been free to drift, which is exactly how the factory came to deliver one key out of seven.

The forwarded set is a deliberate superset of FieldWidgetDomProps: it also carries className and disabled, declared on the controlled-input block and forwarded by the same executor for the reason stated there (withholding them makes it a silent styling- and interactivity-dropper). Semantic props — field, value, onChange, readonly, and compact for the relational pickers — stay explicit and are applied after the spread, so a host cannot displace them.

The triage measurement the card asked for

Per host, read at the call site on this branch's base:

hostfilekeys passed to FieldEditWidgetpasses a dropped key?
ObjectGrid.renderCellEditorpackages/plugin-grid/src/ObjectGrid.tsx:3690field, value, onChangeno
InlineFieldInputpackages/plugin-detail/src/InlineFieldInput.tsx:434field, value, onChange, autoFocusno — autoFocus is the one key that already survived
RequiredFieldsDialogpackages/plugin-kanban/src/RequiredFieldsDialog.tsx:125field, value, onChange, readonlyno

Those are the only three JSX call sites in the repo outside this package's own tests (grep -rn "FieldEditWidget" --include=*.tsx --include=*.ts over packages and apps).

So: plain repair, not a live regression — no host is silently losing a key today. But one host had already WORKED AROUND the drop. RequiredFieldsDialog wraps every control in a label and says why: "FieldEditWidget renders the widget itself and takes no id to associate with, and widening its contract belongs to @object-ui/fields, not to a caller." The premise was false at the type level all along — id is declared — and true only at the implementation level, which is this defect. It takes an id now.

Card measurement re-derived, not inherited: EDIT_WIDGETS has 31 keys resolving to 27 distinct components; 26 call toDomProps themselves and UserField delegates its whole props object to LookupField via {...(props as any)}; none strips onBlur — the five that own a blur handler compose the host's (#6780, #6802). That matches the card's 27 / 26 / 1 exactly.

Evidence the fix is load-bearing

Pin: packages/fields/src/__tests__/FieldEditWidget.domPassThrough-6909.test.tsx, written before the fix.

RED first (pin only, before touching the factory) — Test Files 1 failed (1) · Tests 3 failed (3). The boundary assertion printed the defect directly:

AssertionError: expected [ 'autoFocus', 'field', ...(3) ] to deeply equal [ 'aria-label', 'autoFocus', ...(13) ]
- "aria-label" - "className" - "data-os6909" - "disabled"
- "id" - "name" - "onBlur" - "onClick" - "onFocus" - "tabIndex"

GREEN after — same command, Test Files 1 passed (1) · Tests 3 passed (3).

Ablation — revert only the forwarding (toDomProps(props) spread replaced by autoFocus={props.autoFocus}, i.e. the exact pre-fix shape) and the pin goes red again with the identical 5-key set. No build or dist leg is involved and that is measured, not assumed: vitest.config.mts aliases @object-ui/fields to packages/fields/src, and the pin imports ../FieldEditWidget relatively, so the test executes the source file the script mutates (both asserted in the script's preflight).

Mutation proved on disk by anchored counts in BOTH directions plus hashes, never by an exit code:

PRE forward-anchor 1 · ablation-anchor 0 · worktree 70ad0de26e489c5d99a4fa6d9f2b85a4e9a2b934 (== HEAD blob)
POST forward-anchor 0 · ablation-anchor 1 · worktree fa983d0c4ab7b96c417a3c2a5ed02fa52f98b5b0
RUN Test Files 1 failed (1) · Tests 3 failed (3) · vitest exit 1
REST worktree 70ad0de26e489c5d99a4fa6d9f2b85a4e9a2b934 == HEAD blob · git diff HEAD empty · anchors back to 1 / 0

The restore is git checkout HEAD -- <absolute path> (never bare — a bare restore reads the index, which a mutation can pollute), under trap ... EXIT INT TERM, with the repo root taken from git rev-parse --show-toplevel.

Control — an undeclared authored key (zzcanary) must still be dropped, or "everything forwards now" would be indistinguishable from having reopened the bare spread toDomProps exists to prevent. It is asserted at two points, because the DOM alone cannot see a reopened spread (each widget re-filters and would quietly rescue the mistake):

  • at the factory boundary, as an EXACT prop-key set — zzcanary absent, and no other extra key;
  • at the DOM, with the data- probe present in the same render so the absence is a measurement and not an empty tree.

Verification

Union run after the final commit, at 0071a0825 (git status --porcelain empty):

  • pnpm exec vitest run packages/fields/Test Files 126 passed (126) · Tests 2105 passed (2105)
  • pnpm exec vitest run packages/plugin-grid/ packages/plugin-detail/ packages/plugin-kanban/ (the three hosts) — Test Files 235 passed (235) · Tests 2105 passed (2105)
  • pnpm --filter @object-ui/fields --filter @object-ui/components type-check — both Done, exit 0. The new test file is a real program input, not merely present: before a typing fix, this same command failed with src/__tests__/FieldEditWidget.domPassThrough-6909.test.tsx(168,24): error TS2769, which is what proves tsconfig.test.json compiles it.
  • pnpm exec eslint . --format json (plain form) — 4035 files, 0 errors, exit 0. Repo-wide, so no narrowing to justify.
  • Gates: check-changeset-presence "3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)"; check-changeset-no-major, check-changeset-fixed, check-changeset-overwrite, check-control-bytes (5815 files), check-side-effects-array, check-entry-guard, check-type-check-coverage (45/46), check-lint-coverage (46/46), check-package-self-import, check-node-esm-load --specifiers-only, check-vi-mock-specifiers, check-vi-mock-inherit — all exit 0.
  • check-eager-closure-budget is NOT MEASURED locally, not green and not red: it needs apps/console/dist/eager-closure.json from a console build this branch never ran, and it says so itself ("This is a broken gauge, not a passing budget"). Nothing in this diff touches apps/console or the eager-closure surface. CI builds the console first.

Dependency closure built before any type-check (pnpm --filter '@object-ui/fields^...' build), so no unbuilt-tree false red.

One bounded in-place correction, declared

packages/components/src/renderers/complex/data-table.tsx — comment only, no code. Its injected-editor comment justified the document-level pointerdown listener partly with "FieldEditWidget forwards autoFocus and nothing else out of the DOM block, so a host handler could not reach the control through it even if one were passed." This PR makes that sentence false, so it is corrected in the same change rather than left to rot — that comment was itself landed to replace an earlier stale one (#6912, for #6859), and this repo treats a confidently wrong comment as a first-class defect.

The listener stays load-bearing, on the untouched half of the same reason: the renderCellEditor context object still has nowhere to put an onBlur. Widening it is a DataTableSchema contract change and is not proposed here. No test pinned the corrected sentence (grepped), and packages/components type-check and the repo-wide eslint are green above.

Not in this PR

FieldEditWidget also does not forward the DECLARED non-DOM host-plumbing keys — error, dataSource, dependentValues, dependsOn, dependsOnLabels, emptyHint, onUploadingChange, onSelectRecord, onCreateNew. Same declared-but-not-delivered shape, but each carries real design content (error drives aria-invalid; who produces dependsOnLabels for an inline host is a question, not a mechanical answer), so it is not a mechanical extension of this repair. That half is filed as #7008 and remains open — out of scope here.

Generated by Claude Code


Generated by Claude Code

…eclares
Its props are `FieldWidgetComponentProps` — the controlled-input keys
intersected with `FieldWidgetDomProps`, `AriaAttributes` and the open `data-`
family — so a host could always pass `id`, `name`, `autoFocus`, `tabIndex`,
`onBlur`, `onFocus`, `onClick`, any `aria-*` and any `data-*` with no type
error. The body destructured five keys and rendered the widget with those, so
`autoFocus` was the only survivor of the whole block.
The factory now hands the widget `toDomProps(props)` — this package's own
executor of that declaration — rather than a second key list. The reuse is the
guard: `toDomProps.ts`'s direction-2 assertion makes
`keyof FieldWidgetDomProps extends DomPassThroughKey` a compile error to
violate, so a key added to the declared block reaches the widget through this
factory automatically. Semantic props stay explicit and are applied after the
spread.
Not a widening: the keys were already declared, and every widget still
re-filters through its own `toDomProps` before anything reaches the DOM.
Also corrects the `data-table.tsx` comment this falsifies — the injected-editor
listener's other justification (the `renderCellEditor` context has nowhere to
put an `onBlur`) is untouched and still load-bearing.
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, 45 chunks)3180.3 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-DXcoi8Tk.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)12.46KB4.71KB
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.08KB116.42KB
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.63KB
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.26KB63.17KB
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.08KB54.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.05KB8.37KB
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

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

Projects

None yet

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(fields): FieldEditWidget forwards the DOM pass-through block it declares by claude[bot] · Pull Request #7009 · objectstack-ai/objectui · GitHub
Skip to content

fix(fields): FieldEditWidget forwards the DOM pass-through block it declares - #7009

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6909-fieldeditwidget-passthrough
Aug 31, 2026
Merged

fix(fields): FieldEditWidget forwards the DOM pass-through block it declares#7009
os-sam merged 1 commit into
mainfrom
claude/issue-6909-fieldeditwidget-passthrough

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6909

FieldEditWidget declared the whole DOM pass-through block and delivered one key of it. It now delivers the block.

Direction 1 from the card, and only direction 1. Direction 2 (narrowing this component's published prop type to the five keys it honoured) is deliberately not taken here: it would break any caller passing those keys today, and it is a contract decision for the maintainer.

The defect

The component's props are FieldWidgetComponentProps — the controlled-input keys intersected with FieldWidgetDomProps, AriaAttributes and the open data- family. A host could always pass id, name, autoFocus, tabIndex, onBlur, onFocus, onClick, any aria- and any data- attribute with no type error. The body then destructured field, value, onChange, readonly, autoFocus and rendered the widget with those five plus compact. autoFocus was the only survivor of the whole block.

That is this package's own first-class defect class, named in packages/fields/src/widgets/toDomProps.ts: a key that type-checks, reads as supported, and silently never reaches the element (#3290's aria-required, #3222's validation slot). toDomProps binds the WIDGET contract to its whitelist with compile-time assertions in both directions. Nothing bound this factory to either, so the factory was the one unbound link in the chain.

Not a widening, and not a contract change

The keys were already declared, and every widget still re-filters through its own toDomProps before anything reaches a DOM element. What any widget accepts or rejects is unchanged — this only makes the implementation deliver what the declaration already promised.

The fix, and why it is the reuse rather than a new list

The factory hands the widget toDomProps(props) — this package's own executor of the declaration — instead of a second key list written out in the factory. That reuse is the structural guard: toDomProps.ts's direction-2 assertion already makes keyof FieldWidgetDomProps extends DomPassThroughKey a compile error to violate, so a key added to the declared DOM block now reaches the widget through this factory automatically. One mechanism, one judge. A private list here would have been free to drift, which is exactly how the factory came to deliver one key out of seven.

The forwarded set is a deliberate superset of FieldWidgetDomProps: it also carries className and disabled, declared on the controlled-input block and forwarded by the same executor for the reason stated there (withholding them makes it a silent styling- and interactivity-dropper). Semantic props — field, value, onChange, readonly, and compact for the relational pickers — stay explicit and are applied after the spread, so a host cannot displace them.

The triage measurement the card asked for

Per host, read at the call site on this branch's base:

hostfilekeys passed to FieldEditWidgetpasses a dropped key?
ObjectGrid.renderCellEditorpackages/plugin-grid/src/ObjectGrid.tsx:3690field, value, onChangeno
InlineFieldInputpackages/plugin-detail/src/InlineFieldInput.tsx:434field, value, onChange, autoFocusno — autoFocus is the one key that already survived
RequiredFieldsDialogpackages/plugin-kanban/src/RequiredFieldsDialog.tsx:125field, value, onChange, readonlyno

Those are the only three JSX call sites in the repo outside this package's own tests (grep -rn "FieldEditWidget" --include=*.tsx --include=*.ts over packages and apps).

So: plain repair, not a live regression — no host is silently losing a key today. But one host had already WORKED AROUND the drop. RequiredFieldsDialog wraps every control in a label and says why: "FieldEditWidget renders the widget itself and takes no id to associate with, and widening its contract belongs to @object-ui/fields, not to a caller." The premise was false at the type level all along — id is declared — and true only at the implementation level, which is this defect. It takes an id now.

Card measurement re-derived, not inherited: EDIT_WIDGETS has 31 keys resolving to 27 distinct components; 26 call toDomProps themselves and UserField delegates its whole props object to LookupField via {...(props as any)}; none strips onBlur — the five that own a blur handler compose the host's (#6780, #6802). That matches the card's 27 / 26 / 1 exactly.

Evidence the fix is load-bearing

Pin: packages/fields/src/__tests__/FieldEditWidget.domPassThrough-6909.test.tsx, written before the fix.

RED first (pin only, before touching the factory) — Test Files 1 failed (1) · Tests 3 failed (3). The boundary assertion printed the defect directly:

AssertionError: expected [ 'autoFocus', 'field', ...(3) ] to deeply equal [ 'aria-label', 'autoFocus', ...(13) ]
- "aria-label" - "className" - "data-os6909" - "disabled"
- "id" - "name" - "onBlur" - "onClick" - "onFocus" - "tabIndex"

GREEN after — same command, Test Files 1 passed (1) · Tests 3 passed (3).

Ablation — revert only the forwarding (toDomProps(props) spread replaced by autoFocus={props.autoFocus}, i.e. the exact pre-fix shape) and the pin goes red again with the identical 5-key set. No build or dist leg is involved and that is measured, not assumed: vitest.config.mts aliases @object-ui/fields to packages/fields/src, and the pin imports ../FieldEditWidget relatively, so the test executes the source file the script mutates (both asserted in the script's preflight).

Mutation proved on disk by anchored counts in BOTH directions plus hashes, never by an exit code:

PRE forward-anchor 1 · ablation-anchor 0 · worktree 70ad0de26e489c5d99a4fa6d9f2b85a4e9a2b934 (== HEAD blob)
POST forward-anchor 0 · ablation-anchor 1 · worktree fa983d0c4ab7b96c417a3c2a5ed02fa52f98b5b0
RUN Test Files 1 failed (1) · Tests 3 failed (3) · vitest exit 1
REST worktree 70ad0de26e489c5d99a4fa6d9f2b85a4e9a2b934 == HEAD blob · git diff HEAD empty · anchors back to 1 / 0

The restore is git checkout HEAD -- <absolute path> (never bare — a bare restore reads the index, which a mutation can pollute), under trap ... EXIT INT TERM, with the repo root taken from git rev-parse --show-toplevel.

Control — an undeclared authored key (zzcanary) must still be dropped, or "everything forwards now" would be indistinguishable from having reopened the bare spread toDomProps exists to prevent. It is asserted at two points, because the DOM alone cannot see a reopened spread (each widget re-filters and would quietly rescue the mistake):

  • at the factory boundary, as an EXACT prop-key set — zzcanary absent, and no other extra key;
  • at the DOM, with the data- probe present in the same render so the absence is a measurement and not an empty tree.

Verification

Union run after the final commit, at 0071a0825 (git status --porcelain empty):

  • pnpm exec vitest run packages/fields/Test Files 126 passed (126) · Tests 2105 passed (2105)
  • pnpm exec vitest run packages/plugin-grid/ packages/plugin-detail/ packages/plugin-kanban/ (the three hosts) — Test Files 235 passed (235) · Tests 2105 passed (2105)
  • pnpm --filter @object-ui/fields --filter @object-ui/components type-check — both Done, exit 0. The new test file is a real program input, not merely present: before a typing fix, this same command failed with src/__tests__/FieldEditWidget.domPassThrough-6909.test.tsx(168,24): error TS2769, which is what proves tsconfig.test.json compiles it.
  • pnpm exec eslint . --format json (plain form) — 4035 files, 0 errors, exit 0. Repo-wide, so no narrowing to justify.
  • Gates: check-changeset-presence "3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)"; check-changeset-no-major, check-changeset-fixed, check-changeset-overwrite, check-control-bytes (5815 files), check-side-effects-array, check-entry-guard, check-type-check-coverage (45/46), check-lint-coverage (46/46), check-package-self-import, check-node-esm-load --specifiers-only, check-vi-mock-specifiers, check-vi-mock-inherit — all exit 0.
  • check-eager-closure-budget is NOT MEASURED locally, not green and not red: it needs apps/console/dist/eager-closure.json from a console build this branch never ran, and it says so itself ("This is a broken gauge, not a passing budget"). Nothing in this diff touches apps/console or the eager-closure surface. CI builds the console first.

Dependency closure built before any type-check (pnpm --filter '@object-ui/fields^...' build), so no unbuilt-tree false red.

One bounded in-place correction, declared

packages/components/src/renderers/complex/data-table.tsx — comment only, no code. Its injected-editor comment justified the document-level pointerdown listener partly with "FieldEditWidget forwards autoFocus and nothing else out of the DOM block, so a host handler could not reach the control through it even if one were passed." This PR makes that sentence false, so it is corrected in the same change rather than left to rot — that comment was itself landed to replace an earlier stale one (#6912, for #6859), and this repo treats a confidently wrong comment as a first-class defect.

The listener stays load-bearing, on the untouched half of the same reason: the renderCellEditor context object still has nowhere to put an onBlur. Widening it is a DataTableSchema contract change and is not proposed here. No test pinned the corrected sentence (grepped), and packages/components type-check and the repo-wide eslint are green above.

Not in this PR

FieldEditWidget also does not forward the DECLARED non-DOM host-plumbing keys — error, dataSource, dependentValues, dependsOn, dependsOnLabels, emptyHint, onUploadingChange, onSelectRecord, onCreateNew. Same declared-but-not-delivered shape, but each carries real design content (error drives aria-invalid; who produces dependsOnLabels for an inline host is a question, not a mechanical answer), so it is not a mechanical extension of this repair. That half is filed as #7008 and remains open — out of scope here.

Generated by Claude Code


Generated by Claude Code

…eclares
Its props are `FieldWidgetComponentProps` — the controlled-input keys
intersected with `FieldWidgetDomProps`, `AriaAttributes` and the open `data-`
family — so a host could always pass `id`, `name`, `autoFocus`, `tabIndex`,
`onBlur`, `onFocus`, `onClick`, any `aria-*` and any `data-*` with no type
error. The body destructured five keys and rendered the widget with those, so
`autoFocus` was the only survivor of the whole block.
The factory now hands the widget `toDomProps(props)` — this package's own
executor of that declaration — rather than a second key list. The reuse is the
guard: `toDomProps.ts`'s direction-2 assertion makes
`keyof FieldWidgetDomProps extends DomPassThroughKey` a compile error to
violate, so a key added to the declared block reaches the widget through this
factory automatically. Semantic props stay explicit and are applied after the
spread.
Not a widening: the keys were already declared, and every widget still
re-filters through its own `toDomProps` before anything reaches the DOM.
Also corrects the `data-table.tsx` comment this falsifies — the injected-editor
listener's other justification (the `renderCellEditor` context has nowhere to
put an `onBlur`) is untouched and still load-bearing.
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, 45 chunks)3180.3 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-DXcoi8Tk.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)12.46KB4.71KB
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.08KB116.42KB
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.63KB
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.26KB63.17KB
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.08KB54.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.05KB8.37KB
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

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

Projects

None yet

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); fix(fields): FieldEditWidget forwards the DOM pass-through block it declares by claude[bot] · Pull Request #7009 · objectstack-ai/objectui · GitHub
Skip to content

fix(fields): FieldEditWidget forwards the DOM pass-through block it declares - #7009

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6909-fieldeditwidget-passthrough
Aug 31, 2026
Merged

fix(fields): FieldEditWidget forwards the DOM pass-through block it declares#7009
os-sam merged 1 commit into
mainfrom
claude/issue-6909-fieldeditwidget-passthrough

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6909

FieldEditWidget declared the whole DOM pass-through block and delivered one key of it. It now delivers the block.

Direction 1 from the card, and only direction 1. Direction 2 (narrowing this component's published prop type to the five keys it honoured) is deliberately not taken here: it would break any caller passing those keys today, and it is a contract decision for the maintainer.

The defect

The component's props are FieldWidgetComponentProps — the controlled-input keys intersected with FieldWidgetDomProps, AriaAttributes and the open data- family. A host could always pass id, name, autoFocus, tabIndex, onBlur, onFocus, onClick, any aria- and any data- attribute with no type error. The body then destructured field, value, onChange, readonly, autoFocus and rendered the widget with those five plus compact. autoFocus was the only survivor of the whole block.

That is this package's own first-class defect class, named in packages/fields/src/widgets/toDomProps.ts: a key that type-checks, reads as supported, and silently never reaches the element (#3290's aria-required, #3222's validation slot). toDomProps binds the WIDGET contract to its whitelist with compile-time assertions in both directions. Nothing bound this factory to either, so the factory was the one unbound link in the chain.

Not a widening, and not a contract change

The keys were already declared, and every widget still re-filters through its own toDomProps before anything reaches a DOM element. What any widget accepts or rejects is unchanged — this only makes the implementation deliver what the declaration already promised.

The fix, and why it is the reuse rather than a new list

The factory hands the widget toDomProps(props) — this package's own executor of the declaration — instead of a second key list written out in the factory. That reuse is the structural guard: toDomProps.ts's direction-2 assertion already makes keyof FieldWidgetDomProps extends DomPassThroughKey a compile error to violate, so a key added to the declared DOM block now reaches the widget through this factory automatically. One mechanism, one judge. A private list here would have been free to drift, which is exactly how the factory came to deliver one key out of seven.

The forwarded set is a deliberate superset of FieldWidgetDomProps: it also carries className and disabled, declared on the controlled-input block and forwarded by the same executor for the reason stated there (withholding them makes it a silent styling- and interactivity-dropper). Semantic props — field, value, onChange, readonly, and compact for the relational pickers — stay explicit and are applied after the spread, so a host cannot displace them.

The triage measurement the card asked for

Per host, read at the call site on this branch's base:

hostfilekeys passed to FieldEditWidgetpasses a dropped key?
ObjectGrid.renderCellEditorpackages/plugin-grid/src/ObjectGrid.tsx:3690field, value, onChangeno
InlineFieldInputpackages/plugin-detail/src/InlineFieldInput.tsx:434field, value, onChange, autoFocusno — autoFocus is the one key that already survived
RequiredFieldsDialogpackages/plugin-kanban/src/RequiredFieldsDialog.tsx:125field, value, onChange, readonlyno

Those are the only three JSX call sites in the repo outside this package's own tests (grep -rn "FieldEditWidget" --include=*.tsx --include=*.ts over packages and apps).

So: plain repair, not a live regression — no host is silently losing a key today. But one host had already WORKED AROUND the drop. RequiredFieldsDialog wraps every control in a label and says why: "FieldEditWidget renders the widget itself and takes no id to associate with, and widening its contract belongs to @object-ui/fields, not to a caller." The premise was false at the type level all along — id is declared — and true only at the implementation level, which is this defect. It takes an id now.

Card measurement re-derived, not inherited: EDIT_WIDGETS has 31 keys resolving to 27 distinct components; 26 call toDomProps themselves and UserField delegates its whole props object to LookupField via {...(props as any)}; none strips onBlur — the five that own a blur handler compose the host's (#6780, #6802). That matches the card's 27 / 26 / 1 exactly.

Evidence the fix is load-bearing

Pin: packages/fields/src/__tests__/FieldEditWidget.domPassThrough-6909.test.tsx, written before the fix.

RED first (pin only, before touching the factory) — Test Files 1 failed (1) · Tests 3 failed (3). The boundary assertion printed the defect directly:

AssertionError: expected [ 'autoFocus', 'field', ...(3) ] to deeply equal [ 'aria-label', 'autoFocus', ...(13) ]
- "aria-label" - "className" - "data-os6909" - "disabled"
- "id" - "name" - "onBlur" - "onClick" - "onFocus" - "tabIndex"

GREEN after — same command, Test Files 1 passed (1) · Tests 3 passed (3).

Ablation — revert only the forwarding (toDomProps(props) spread replaced by autoFocus={props.autoFocus}, i.e. the exact pre-fix shape) and the pin goes red again with the identical 5-key set. No build or dist leg is involved and that is measured, not assumed: vitest.config.mts aliases @object-ui/fields to packages/fields/src, and the pin imports ../FieldEditWidget relatively, so the test executes the source file the script mutates (both asserted in the script's preflight).

Mutation proved on disk by anchored counts in BOTH directions plus hashes, never by an exit code:

PRE forward-anchor 1 · ablation-anchor 0 · worktree 70ad0de26e489c5d99a4fa6d9f2b85a4e9a2b934 (== HEAD blob)
POST forward-anchor 0 · ablation-anchor 1 · worktree fa983d0c4ab7b96c417a3c2a5ed02fa52f98b5b0
RUN Test Files 1 failed (1) · Tests 3 failed (3) · vitest exit 1
REST worktree 70ad0de26e489c5d99a4fa6d9f2b85a4e9a2b934 == HEAD blob · git diff HEAD empty · anchors back to 1 / 0

The restore is git checkout HEAD -- <absolute path> (never bare — a bare restore reads the index, which a mutation can pollute), under trap ... EXIT INT TERM, with the repo root taken from git rev-parse --show-toplevel.

Control — an undeclared authored key (zzcanary) must still be dropped, or "everything forwards now" would be indistinguishable from having reopened the bare spread toDomProps exists to prevent. It is asserted at two points, because the DOM alone cannot see a reopened spread (each widget re-filters and would quietly rescue the mistake):

  • at the factory boundary, as an EXACT prop-key set — zzcanary absent, and no other extra key;
  • at the DOM, with the data- probe present in the same render so the absence is a measurement and not an empty tree.

Verification

Union run after the final commit, at 0071a0825 (git status --porcelain empty):

  • pnpm exec vitest run packages/fields/Test Files 126 passed (126) · Tests 2105 passed (2105)
  • pnpm exec vitest run packages/plugin-grid/ packages/plugin-detail/ packages/plugin-kanban/ (the three hosts) — Test Files 235 passed (235) · Tests 2105 passed (2105)
  • pnpm --filter @object-ui/fields --filter @object-ui/components type-check — both Done, exit 0. The new test file is a real program input, not merely present: before a typing fix, this same command failed with src/__tests__/FieldEditWidget.domPassThrough-6909.test.tsx(168,24): error TS2769, which is what proves tsconfig.test.json compiles it.
  • pnpm exec eslint . --format json (plain form) — 4035 files, 0 errors, exit 0. Repo-wide, so no narrowing to justify.
  • Gates: check-changeset-presence "3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)"; check-changeset-no-major, check-changeset-fixed, check-changeset-overwrite, check-control-bytes (5815 files), check-side-effects-array, check-entry-guard, check-type-check-coverage (45/46), check-lint-coverage (46/46), check-package-self-import, check-node-esm-load --specifiers-only, check-vi-mock-specifiers, check-vi-mock-inherit — all exit 0.
  • check-eager-closure-budget is NOT MEASURED locally, not green and not red: it needs apps/console/dist/eager-closure.json from a console build this branch never ran, and it says so itself ("This is a broken gauge, not a passing budget"). Nothing in this diff touches apps/console or the eager-closure surface. CI builds the console first.

Dependency closure built before any type-check (pnpm --filter '@object-ui/fields^...' build), so no unbuilt-tree false red.

One bounded in-place correction, declared

packages/components/src/renderers/complex/data-table.tsx — comment only, no code. Its injected-editor comment justified the document-level pointerdown listener partly with "FieldEditWidget forwards autoFocus and nothing else out of the DOM block, so a host handler could not reach the control through it even if one were passed." This PR makes that sentence false, so it is corrected in the same change rather than left to rot — that comment was itself landed to replace an earlier stale one (#6912, for #6859), and this repo treats a confidently wrong comment as a first-class defect.

The listener stays load-bearing, on the untouched half of the same reason: the renderCellEditor context object still has nowhere to put an onBlur. Widening it is a DataTableSchema contract change and is not proposed here. No test pinned the corrected sentence (grepped), and packages/components type-check and the repo-wide eslint are green above.

Not in this PR

FieldEditWidget also does not forward the DECLARED non-DOM host-plumbing keys — error, dataSource, dependentValues, dependsOn, dependsOnLabels, emptyHint, onUploadingChange, onSelectRecord, onCreateNew. Same declared-but-not-delivered shape, but each carries real design content (error drives aria-invalid; who produces dependsOnLabels for an inline host is a question, not a mechanical answer), so it is not a mechanical extension of this repair. That half is filed as #7008 and remains open — out of scope here.

Generated by Claude Code


Generated by Claude Code

…eclares
Its props are `FieldWidgetComponentProps` — the controlled-input keys
intersected with `FieldWidgetDomProps`, `AriaAttributes` and the open `data-`
family — so a host could always pass `id`, `name`, `autoFocus`, `tabIndex`,
`onBlur`, `onFocus`, `onClick`, any `aria-*` and any `data-*` with no type
error. The body destructured five keys and rendered the widget with those, so
`autoFocus` was the only survivor of the whole block.
The factory now hands the widget `toDomProps(props)` — this package's own
executor of that declaration — rather than a second key list. The reuse is the
guard: `toDomProps.ts`'s direction-2 assertion makes
`keyof FieldWidgetDomProps extends DomPassThroughKey` a compile error to
violate, so a key added to the declared block reaches the widget through this
factory automatically. Semantic props stay explicit and are applied after the
spread.
Not a widening: the keys were already declared, and every widget still
re-filters through its own `toDomProps` before anything reaches the DOM.
Also corrects the `data-table.tsx` comment this falsifies — the injected-editor
listener's other justification (the `renderCellEditor` context has nowhere to
put an `onBlur`) is untouched and still load-bearing.
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, 45 chunks)3180.3 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-DXcoi8Tk.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)12.46KB4.71KB
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.08KB116.42KB
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.63KB
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.26KB63.17KB
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.08KB54.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.05KB8.37KB
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

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

Projects

None yet

2 participants

@os-sam@claude