Skip to content

fix(plugin-view,console,layout,components,fields): name and nest the controls axe flagged on the list and settings pages (objectui#11690) - #11732

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-11690-a11y-names-roles
Oct 6, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-11690-a11y-names-roles

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #11690
Clause-②: yes

Screen readers can name and reach the controls axe flagged on the showcase task list and on Setup → Settings → Authentication. Each traced producer is pinned with axe-core at component level. Implemented by session https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 (seat domain:ui#3, claim comment 6018454300).

What changed, by producer

axe rule (card count) Producer found Fix
button-name (showcase, 1 of 12): view-tab-add packages/plugin-view/src/ViewTabBar.tsx aria-label and tooltip read the existing view.addView key (the tooltip was a hard-coded "Add View")
aria-required-parent x4, nested-interactive x1 ViewTabBar.tsx: each view was role="tab" with no tablist, and the active view's actions button sat inside its tab Each view is a button (the current one has aria-current="true"). The actions button is now its sibling inside a role-less frame. The rename box now sits outside the button and is named via view.rename
button-name (showcase, 11 of 12) packages/components/src/renderers/complex/data-table.tsx: select-all and per-row Radix checkboxes (a button with role checkbox), unnamed named via the existing table.selectAllRows / table.selectRow keys; en defaults added to TABLE_DEFAULT_TRANSLATIONS
aria-progressbar-name x10 packages/fields/src/index.tsx, PercentCellRenderer only the bar is aria-labelledby its formatted value span, so the name is in the viewer's locale
label x15, button-name x9 (settings) apps/console/src/pages/settings/SettingsField.tsx: the row's Label had no htmlFor label bound by htmlFor (inputs, textarea, switch, select trigger, domain combobox, colour). The radio group, multiselect group and colour hex box use aria-labelledby
list / listitem x50 (settings) packages/layout/src/NavigationRenderer.tsx: with drag-to-reorder on (the desktop default), each sortable row was a div between the menu ul and its li the sortable node is the row's own li. A separator (an aria-hidden item) and a nested group inside a menu now root at an li too

Why buttons and not a fixed tablist. A tab's children are presentational, so a focusable button inside it is nested-interactive. A tablist may own only tabs, so the same button beside a tab is aria-required-children. Under tab roles the per-view actions button could be neither reachable nor valid. These views also never had the tabs keyboard model (arrow keys, a tab panel): every view was its own Tab stop, which is a button. Enter or Space switches to it when the bar is not reorderable, as the console renders it. With drag-to-reorder on, both keys start dnd-kit's keyboard drag instead, measured the same before and after this PR. Keyboard reach is unchanged: view, then its actions button, then the next view. The layout keeps its measure (ml-2 mr-3 on the moved trigger equals the old gap plus ml-0.5 plus the tab's right padding). The changeset marks the role="tab" / aria-selected removal as breaking for anything that queried by them.

Where the producer was outside the expected surface. The dispatch allowed a fix at the producer when it was measured elsewhere. That happened twice. NavigationRenderer.tsx: the claim's surface covers list nesting "wherever on the settings page". data-table.tsx: the 11 other unnamed buttons on the task list. Attributing those 11 to the data table is an inference, not a page reading. A component axe run on data-table reproduces button-name on exactly the select-all plus one-per-row checkboxes. The showcase task list renders 10 rows (the card's aria-progressbar-name x10), and 1 + 10 = 11.

Pins and the reverse verification

Five new pins. Each runs axe-core 4.13 with tags wcag2a + wcag2aa (color-contrast off, because it needs layout happy-dom does not compute and is out of this card). Each asserts an empty violation list AND that the relevant rules appear in passes, so a zero there is a reading:

  • packages/plugin-view/src/__tests__/ViewTabBar.a11y-11690.test.tsx: static and drag-reorderable bars, rename state, keyboard-reach checks, add-button name read in zh against the pack
  • apps/console/src/pages/settings/__tests__/SettingsField.labelBinding-11690.test.tsx: every input type; each control's accessible name equals its own row label
  • packages/fields/src/__tests__/PercentCellRenderer.barName-11690.test.tsx: name equals the shown value in de (1.235 %)
  • packages/layout/src/__tests__/NavigationRenderer.listNesting-11690.test.tsx: grouped and group-free menus, reorder on and off, with separators and a nested group; every ul child is an li
  • packages/components/src/__tests__/data-table-selection-names-11690.test.tsx: names read in de against the pack

Reverse verification, from the committed fix 1ebcf6d. Each source file was put back to base 7300fca, its pin run, then restored with git checkout HEAD. The restore was proven by the HEAD blob hash; final git diff HEAD was empty:

source restored to base pin result on base violations axe reported
ViewTabBar.tsx 6 failed / 1 passed of 7 (the pass is the unchanged click-to-switch) aria-required-parent, button-name, nested-interactive, label
SettingsField.tsx 15 failed of 15 button-name, label
fields/src/index.tsx 4 failed of 4 aria-progressbar-name
NavigationRenderer.tsx 4 failed of 4 menus hold non-li children
data-table.tsx 2 failed of 2 button-name

One existing pin was re-pointed: NavigationRenderer.groupedReorder-11626.test.tsx counted the sortable wrappers as [data-sidebar="menu"] > div. That is exactly the defect. It now counts > li with the same intent (one sortable row per drawn entry, none focusable) and also asserts no non-li child.

Verification (head 1ebcf6d; console compile and settings suite re-run on c8699a0 and aaa5bfc)

  • pnpm exec vitest run --maxWorkers=2 at the repo root: packages/plugin-view/ 66 files / 615 tests passed; packages/layout/ 39 / 348; packages/fields/ 235 passed, 1 skipped / 3670 tests; packages/components/ 362 passed, 1 skipped / 3681 tests; apps/console/src/pages/settings/ 10 / 80
  • consumer sweeps (the tab bar's role change is a public DOM change): app-shell ObjectView.* tests 39 / 332; every app-shell and console test naming UnifiedSidebar or NavigationRenderer 22 / 167; plugin-grid and plugin-list tests driving checkboxes by role 12 / 101; all green
  • pnpm --filter PKG type-check exit 0 for plugin-view, layout, fields and components (script echoed tsc --noEmit && tsc -p tsconfig.test.json). The dependency closure was built first. Each new pin was confirmed inside its package's test program with --listFilesOnly
  • pnpm exec eslint on the 11 touched files: 0 errors. Warning counts on the 5 source files equal the base's
  • check:control-bytes, check:test-path-roots, check:phantom-deps, check:changeset-claims, check:pending-changeset-literals, check:new-line-citations, check:i18n-keys, check:i18n-drift, check:i18n-dead-keys, check:i18n-designer-parity, plus check-changeset-presence / -no-major / -fixed / -overwrite: all exit 0
  • NOT MEASURED: page-level axe on the two pages. Reason: this container has no backend. The sibling objectstack checkout has no install or build, and an objectstack showcase server is a full-farm build. The card's two-page pin is therefore carried by the component pins above plus the count mapping in the first table.
  • pnpm --filter @object-ui/console type-check exit 0 on c8699a0, against the built 34-package closure. Round 1 (1ebcf6d) failed the console build with TS6133 on an unused React import in the new settings pin; c8699a0 removes it. A planted leg (the pin at 1ebcf6d) reproduces exactly that one error.
  • SettingsIcons.knownName-11679.test.tsx (the objectui#11679 pin beside SettingsField.tsx) failed shard 7 on ec3561d with an act() warning naming ForwardRef. The lucide DynamicIcon sets its icon data from a promise, and vi.waitFor left IS_REACT_ACT_ENVIRONMENT on, so the update was an act violation whenever it landed after findByText restored the flag. This is pre-existing on main: a throwaway in-process harness repeating the test sequence logged it on 6be0f7a (2 of 550) as on this branch (5 of 550), and never with the RTL waitFor (0 of 550 on both). aaa5bfc waits with the RTL waitFor; iconNoise() and its assertion are unchanged. On aaa5bfc: the isolated test passed 40 of 40 under 4 CPU burners and the whole file 10 of 10; console type-check exit 0; settings suite 10 files / 80 tests.
  • NOT MEASURED: a visual check of the tab bar in a browser (same no-backend reason).

origin/main has moved since base (7a2c60b, objectui#11725, also edits data-table.tsx, in a different region). git merge-tree against current main is clean.

Clause-② note

The claim declared Clause-②: yes because it expected new language-pack keys. The diff adds none: every new name reads a key the packs already carried (view.addView, view.rename, table.selectAllRows, table.selectRow). No export, prop or type member changes either. What does change publicly is the tab bar's DOM: role="tab" / aria-selected become button / aria-current, and the changeset says so as breaking under minor. The seat can judge whether yes still applies.

Acceptance notes (observed, not filed, no carrier)

  • ViewTabBar still renders English literals for "Unsaved changes", "Save as View", "Reset", the "Active filters" / "Active sort" tooltip, and aria-roledescription "view tab" (drag mode). The packs already have view.unsavedChanges, view.saveAsView, view.activeFilters, view.activeSort. data-table row actions carry aria-label "Row actions" in English. All are named, so none is in this card's rules.
  • A settings slider row: the Radix thumb has no accessible name (the shadcn primitive is a no-touch zone). The authentication page has no slider, so it was not measured on a page.
  • check:i18n-dead-keys (report-only) lists view.addView and view.rename as needs-review. It cannot see call sites made through the file-local viewTabLabel helper.
  • Dispatch hypothesis 6 is half false: axe-core and vitest-axe are root devDependencies, but no test in the tree ran axe before this PR. The cited app-shell hit is the word "relaxed".
  • Every row checkbox has the same name ("Select row"), not one per record.

Related: objectui#11689 (seat 2) edits BooleanCellRenderer in packages/fields/src/index.tsx. This PR touches only the PercentCellRenderer region there. The later lander merges.


Generated by Claude Code

…controls axe flagged on the list and settings pages (objectui#11690)

- ViewTabBar: the add button is named through `view.addView`; views are
  buttons (aria-current on the current one) and the actions button is the
  view button's sibling, not its child; the rename box is named.
- SettingsField: each row's label is bound to its control (htmlFor, and
  aria-labelledby for the group and colour-hex controls).
- NavigationRenderer: the sortable node is the row's own li, and a
  separator or nested group inside a menu roots at an li.
- data-table: the selection checkboxes are named through the existing
  table.selectAllRows / table.selectRow keys.
- PercentCellRenderer: the progress bar is named by its value span.

Each surface is pinned by an axe run (wcag2a + wcag2aa, color-contrast off)
that also asserts the rules really ran.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

ℹ️ Console Performance Budget — not measured

This run did not produce a console bundle to measure, so there is no pass/fail verdict for the performance budget.

This is not a budget violation. Nothing was measured — the numbers a real violation would carry are simply absent.

Step Outcome
Build packages success
Check console performance budget skipped

See the workflow run for details.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 17.82KB 6.58KB
app-shell (runtime-config.js) 22.52KB 7.86KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 579.15KB 139.25KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 233.72KB 64.83KB
fields (index.js) 262.80KB 66.65KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 35.66KB 9.49KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.50KB 11.82KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.39KB 15.52KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 198.81KB 47.14KB
plugin-dashboard (index.js) 143.75KB 38.87KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 247.25KB 65.05KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.09KB 45.89KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 238.48KB 65.51KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.85KB 29.12KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.90KB 11.74KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 90.29KB 22.77KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

…in (objectui#11690)

The console compiles its own tests (`include: ["src"]`, `noUnusedLocals`,
`jsx: react-jsx`), so the unread default import failed `tsc` in the
console build with TS6133.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 331 chunks) 3325.9 KB 3330.4 KB
Main entry chunk (gzip) 154.3 KB 350 KB
Entry file index-BJDNHhKG.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 17.82KB 6.58KB
app-shell (runtime-config.js) 22.52KB 7.86KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 579.15KB 139.25KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 233.72KB 64.83KB
fields (index.js) 262.80KB 66.65KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 35.66KB 9.49KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.50KB 11.82KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.39KB 15.52KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 198.81KB 47.14KB
plugin-dashboard (index.js) 143.75KB 38.87KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 247.25KB 65.05KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.09KB 45.89KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 238.48KB 65.51KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.85KB 29.12KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.90KB 11.74KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 90.29KB 22.77KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c8699a095264e90358793619aee66e10b4f6e715
Local-runs: none

Inputs read: card objectui#11690 (body and its four comments: claim 6018454300, report 6020678071, claim amendment 6020771085, round-2 report 6020967800), PR objectui#11732 (body, 12-file list, its two bot comments, the net diff against main), and the head's check-runs. Check-runs when this record was rendered: 43 in all, 40 success, 3 skipped (Test (coverage), its shard-matrix row, dependabot), 0 failure, none still running; the two shards still in progress when the read began (Test shard 1/8 and 4/8) completed success during the read, and the aggregate Test run appeared and completed success. Those conclusions are the gate verdicts here; nothing was built, run or re-run.

① Derived judgments

Accept set. Nothing an author writes changes: no schema, prop, config or spec key is added, widened or removed. Every input type the diff touches (SettingsFieldProps, ViewTabBarProps, NavigationRendererProps, CellRendererProps, the data-table schema) is byte-identical. The two props NavigationItemRenderer gains (row, inList) and the NavRowNode interface sit on a module-private function: the head's NavigationRenderer.tsx exports neither, and FieldHeader, SortableTab and TABLE_DEFAULT_TRANSLATIONS are module-private too. Judged right: no accept-set change, and none is claimed.

Public surface. What consumers can observe changes in five rendered outputs:

  1. ViewTabBar (plugin-view). Each view's role="tab" / aria-selected / tabIndex div becomes a native button element with aria-current="true" on the active view; the actions-menu trigger moves from inside the view to a sibling inside a role-less frame; the rename box is named through view.rename; the "+" button is named through view.addView and its tooltip reads the same key (the hard-coded "Add View" is gone); under reorder, dnd-kit's attributes land as role: 'button' (redundant on a native button, harmless). Judged right: a tab's children are presentational and a tablist may own only tabs, so the per-view actions button could be neither inside nor beside a tab role; the bar never implemented the APG tabs keyboard model; the seat ruled A. The pin asserts zero tab roles, button tags, aria-current on exactly the active view, the actions button a named sibling, and axe clean with button-name and nested-interactive in passes.

    • A DOM-contract detail the changeset's migration sentence does not cover: data-testid="view-tab-ID" moved from the frame onto the button, so during an inline rename that node is absent (before, the frame carried it throughout). In-repo consumers are unaffected (plugin-view 615 tests, the app-shell sweeps, Build & E2E and Live E2E all green). A nit, not a blocker.
    • A pre-existing behaviour the changeset sentence "Every view stays its own Tab stop that Enter or Space switches to" overstates for the reorderable arm: useSensors wires KeyboardSensor (about :368), the listeners spread onto the view button carry its onKeyDown, and no setActivatorNodeRef is set, so Enter or Space on a view button starts a keyboard drag and prevents the native click. The same listeners sat on the old div after its own onKeyDown, so main behaves the same. Not this diff's regression, not in the card's rules, and unmeasured here (read, not run). Escalated in ③.
  2. SettingsField (console). The row Label now carries id and htmlFor; single-control cases bind by htmlFor={id} (text, email, url, phone, password, textarea, number, json, toggle switch, select trigger, domain combobox, colour swatch); the radio group, the multiselect role="group" and the colour hex box name themselves with aria-labelledby. Judged right for every rendered name: the pin reads each control by its own row label and axe is clean with label and button-name in passes.

    • Wrong in intent, inert in effect: wrapper(children, labelFor = id) is called with an explicit undefined for the radio, multiselect, slider and default cases to mean "no htmlFor", but a JavaScript default parameter also fires on an explicit undefined, so those four rows still emit htmlFor={id}, pointing at no element (radio items carry ID-value; multiselect, slider and default carry none). A dangling for labels nothing and no axe rule reads it, so the rendered names are exactly as pinned. Fix for a follow-up: type the argument string | null, pass null, map null to no attribute. Not a blocker.
  3. NavigationRenderer (layout). With reorder on, the sortable node is now the row's own li element (SidebarMenuItem is a forwardRef to li at the head, so ref and style land); a separator roots at an aria-hidden li; a nested group roots at a plain li; a top-level group passes inList={false} and stays a section. Judged right: every NavigationItemRenderer call site at the head sits inside a SidebarMenu list (favorites, the group-free branch, each group's children, each run of top-level leaves, the sortable rows) except the top-level-group one, which is the one passing inList={false}. The pin covers grouped and flat menus with reorder on and off, each with separators and the grouped ones a nested group, asserts every list child is an LI, and axe is clean with list and listitem in passes. The re-pointed groupedReorder-11626 pin keeps its intent (one sortable row per drawn entry, none focusable) and adds a no-non-li assertion. Right.

  4. data-table (components). aria-label through t('table.selectAllRows') and t('table.selectRow'), with en defaults added to the module-private TABLE_DEFAULT_TRANSLATIONS map; both keys already exist under table in the en pack (about :603) and the zh pack (about :399), and the pin reads them in de against the pack. Right; the card's "through translation keys" clause is met.

  5. PercentCellRenderer (fields). React.useId() sits before the blank-value early return (hook count stable); the bar gets aria-labelledby to the formatted value span. Right, and a deliberate, sound deviation from "through translation keys": the name is locale-formatted and distinct per cell (pinned in de, two bars named apart), where a key would name every percent column the same words.

Card pins. "Each named control gets an accessible name, through translation keys": met (the one deviation above is better than the letter). "Settings labels bind to their controls, and the tab bar's roles nest validly": met; the roles nest validly by not being tab roles, which is the seat's ruling A. "axe on the two pages reports none of the listed rules": NOT MEASURED at page level by the dev (no backend in the container); substituted by component pins at each traced producer, each with a rule-ran control, and the card's node counts map onto the producers: showcase button-name 12 = 1 add button + 1 select-all + 10 row checkboxes (the 10 matches aria-progressbar-name 10), aria-required-parent 4 = 4 views, nested-interactive 1 = the active view's actions button; settings label 15 and button-name 9 = the auth manifest's input rows and its 7 switches + 2 select triggers; list / listitem 50 = the sidebar rows under reorder. Accepted as evidence, with the residual named in ③.

Reverse verification. The dev's base-restore table (each pin red on 7300fca, green on the fix) is the implementer's claim; it is consistent with what the pins assert as read.

Attribution and governance. PR body, changeset and comments carry session ids only, no model identifier. No governed surface is in the file list (Governed Surface Queue Guard: success).

② Semver level

Changeset .changeset/11690-a11y-names-roles.md: minor for @object-ui/plugin-view, @object-ui/console, @object-ui/layout, @object-ui/components, @object-ui/fields. All five are released packages in the fixed group, and @object-ui/console is versioned (privatePackages.version: true). Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Overwrite Report and Changeset Claim Re-read: all success.

Level against the diff: the diff publishes no new export, prop, type member or language-pack key, so nothing demands minor by widening; it does change the rendered DOM contract of a published component (ViewTabBar: role="tab" and aria-selected removed, aria-current added) in a way consumers' selectors can see, which is more than a patch signals. minor is the right level for that. It is not a Clause-② narrowing: the removed attributes are an output shape, not a published accept set (nothing an author writes is removed), so no (narrowing) arm and no ADR-0087 marker is owed, and the body's "Breaking for anything that queried…" sentence with its FROM → TO (data-testid="view-tab-ID" or aria-current) is a consumer courtesy, correctly not a BREAKING declaration. Right.

Clause-②: line: Clause-②: yes in the claim and the PR body, re-affirmed by the seat's amendment 6020771085 after the dev flagged that the claimed basis (new i18n keys) did not materialise. On the clause's letter (relax the accept set or enlarge the public face) the diff does neither, so yes is over-declared; it errs in the one direction the gates never block and routes to this review, and the level it compels (at least minor) is the level the DOM change warrants on its own. Accepted as the seat's declaration; recorded as conservative, not false in effect.

③ Boundary flags

Dev open_questions (report 6020678071).

  1. Tab bar semantics, A (buttons plus aria-current, actions button as sibling) or B (APG tablist): answered by the seat, A (amendment 6020771085). This review concurs: under tab roles the actions button is reachable or valid, not both.
  2. Does Clause-②: yes still apply with no new keys: answered by the seat, keep yes, minor. Judged in ②: conservative, accepted.
    Round-2 report 6020967800: open_questions empty.

Dev deviations.

  • Two producers outside the claim's file list (NavigationRenderer.tsx, data-table.tsx) and the re-pointed groupedReorder-11626 pin: answered; the claim amendment widened the surface to exactly these, with the overlap read against objectui#11682 and objectui#11689 (same files, disjoint regions).
  • No locale pack edited: answered; the four keys already exist in every pack read (en, zh; the pins read de).
  • Round-1 CI red (Bundle Analysis / Build Console, TS6133 on an unused React import in the new console pin): answered by c8699a0, which removes the import only; Bundle Analysis and Type Check are success on this head.
  • PR body stale bullet: answered; the body at read time carries the replacement bullet and the amended Verification heading.
  • A foreground run moved to background, verify-lock queue timeouts, the worktree recreated: procedural, no bearing on the diff.

NOT MEASURED (dev-flagged). Page-level axe on /apps/setup/system/settings/auth and /apps/showcase_app/showcase_task, and a browser visual of the tab bar. Answered as evidence by substitution in ①, with this residual escalated to the seat: the 11-of-12 button-name attribution to the data table is an inference from counts, so the card's two-page pin is confirmed only when the next console dogfood sweep (the card's source, objectui#11672) re-runs axe on both pages; if any listed rule still reports there, that is a new card naming the untraced producer, not a reopening of this one.

Out-of-scope findings (dev; carrier none, noted, not filed). English literals still in ViewTabBar ("Unsaved changes", "Save as View", "Reset", the filters/sort tooltip, aria-roledescription "view tab") and the data-table "Row actions" label; the settings slider thumb unnamed (shadcn primitive, no-touch zone); check:i18n-dead-keys false positive on view.addView and view.rename through the file-local viewTabLabel helper; dispatch hypothesis 6 half false. All outside the card's rules; none blocks. Carding is the seat's call.

Reviewer's own escalation (not a dev flag). The reorderable arm's Enter-or-Space-starts-a-drag behaviour named in ① item 1: pre-existing on main, unmeasured here, but the changeset sentence states the opposite for that arm. Suggested: a card for keyboard view-switching under reorderable (an activator node on the grip would confine the KeyboardSensor to it), and the sentence corrected there, not as a rider on this PR. The inert htmlFor default-parameter slip in SettingsField (① item 2) can ride on that card or any later touch of the file.

Implemented-by: claude/issue-11690-a11y-names-roles
Reviewed-by: session_01CGZy1BGCjdN5cXqL9cnvB8

VERDICT: PASS


Generated by Claude Code

…ctui#11690)

Measured with a throwaway user-event probe (not committed): with the bar
not reorderable, Enter and Space switch to the focused view button; with
drag-to-reorder on, dnd-kit's KeyboardSensor prevents both keydowns and
starts a keyboard drag, so no switch, while a click still switches. The
base ViewTabBar behaves the same in both arms. The changeset sentence
claimed Enter or Space switches in every case.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 331 chunks) 3326.0 KB 3330.4 KB
Main entry chunk (gzip) 154.3 KB 350 KB
Entry file index-BpYrbOSh.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 17.82KB 6.58KB
app-shell (runtime-config.js) 22.52KB 7.86KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 579.15KB 139.25KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 233.72KB 64.83KB
fields (index.js) 263.41KB 66.72KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 35.66KB 9.49KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.50KB 11.82KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.39KB 15.52KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 198.81KB 47.14KB
plugin-dashboard (index.js) 143.75KB 38.87KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 247.28KB 65.06KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.09KB 45.89KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 238.51KB 65.53KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.85KB 29.12KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.90KB 11.74KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 90.29KB 22.77KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

…i.waitFor (objectui#11690)

`getLazyIcon('Building2')` is lucide's DynamicIcon, a forwardRef that sets
its icon data from a promise its mount effect starts. `vi.waitFor` leaves
IS_REACT_ACT_ENVIRONMENT on, so when that update landed after
`findByText` restored the flag, React logged "An update to ForwardRef
inside a test was not wrapped in act(...)" through the console.error the
test holds silent, and the assertion that nothing is logged failed on some
runs. RTL's waitFor keeps the flag off while it waits, as findByText does.
iconNoise() and its assertion are unchanged.

Pre-existing on main: an in-process harness repeating the test's sequence
logged the ForwardRef warning on main 6be0f7a as on this branch, and never
with RTL's waitFor.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 331 chunks) 3326.9 KB 3330.4 KB
Main entry chunk (gzip) 154.7 KB 350 KB
Entry file index-DW9bVdXl.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 17.82KB 6.58KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 579.15KB 139.25KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 233.72KB 64.83KB
fields (index.js) 263.41KB 66.72KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 37.51KB 10.04KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.50KB 11.82KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.39KB 15.52KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 199.63KB 47.46KB
plugin-dashboard (index.js) 144.26KB 38.97KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 247.28KB 65.06KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.09KB 45.89KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 238.51KB 65.53KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.85KB 29.12KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.90KB 11.74KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 90.29KB 22.77KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: aaa5bfcf77857449cf2aecbd254c4ed950206a40
Local-runs: none

Inputs read: card objectui#11690 (body and its six comments: claim 6018454300, report 6020678071, claim amendment 6020771085, round-2 report 6020967800, round-3 report 6021264463, round-4 report 6023574940), PR objectui#11732 (body, 13-file list, its five comments, among them the ## Contract review record 6021198733 on head c8699a0, its four commits, and the net diff against main from merge base 7300fca), and the check-runs on this head. Check-runs: 43 in all, 40 success, 3 skipped (Test (coverage), its shard-matrix row, dependabot), 0 failure, none in progress; those conclusions are the gate verdicts here. Nothing was built, run or re-run. main has moved 15 commits past the merge base; the PR reads mergeable and clean, and the one main commit that also touches data-table.tsx (objectui#11725) edits a region this diff does not.

Since the record on c8699a0, two commits: ec3561d rewrites one changeset sentence (what Enter and Space do on a view, per arm) and aaa5bfc changes the wait in SettingsIcons.knownName-11679.test.tsx. Every source hunk is byte-identical to what that record read; this record judges the whole net diff at this head on its own reading, the surroundings of each hunk read from the head's files.

① Derived judgments

Accept set. Nothing an author writes changes. No schema, spec key, prop, config field or exported type is added, widened or removed: SettingsFieldProps, ViewTabBarProps, ViewTabItem, NavigationRendererProps, CellRendererProps and the data-table schema are untouched. What the diff adds to function signatures (NavigationItemRenderer gains row and inList; FieldHeader gains labelId and htmlFor; the NavRowNode interface) sits on module-private declarations: neither NavigationRenderer.tsx nor SettingsField.tsx exports them at the head, and SortableTab and TABLE_DEFAULT_TRANSLATIONS are module-private too. No language-pack key is added: the four keys read (view.addView and view.rename under view; table.selectAllRows and table.selectRow under table) exist at the head in the en, zh and de packs, and the pins read zh and de against the pack. Judged right: no accept-set change, and none is claimed.

Public surface. Five rendered outputs change, plus one test outside the card; each judged:

  1. ViewTabBar (plugin-view). Each view's div with role="tab", aria-selected and tabIndex becomes a native button element; the active one carries aria-current="true"; the actions-menu trigger (view-tab-actions-ID) is the view button's sibling inside a role-less frame; the inline-rename input is named through view.rename and stands outside any button; the "+" button is named through view.addView and its tooltip reads the same key, the hard-coded "Add View" gone. Under reorder the dnd-kit attributes now say role: 'button' instead of 'tab', redundant on a native button and harmless. Judged right: a tab's children are presentational and a tablist may own only tabs, so the per-view actions button could be neither inside a tab (axe nested-interactive) nor beside one (aria-required-children); the bar never had the APG tabs keyboard model; the seat ruled A. The children left inside the view button are non-interactive (the grip span is aria-hidden with pointer-events-none; the two tooltip triggers wrap an icon and a span), which the pin's nested-interactive pass confirms on both arms with a read-only view and an active-filters view in its fixture. The pin asserts zero tab roles, button tags, aria-current on exactly the active view, the actions button a named sibling that opens its menu, and axe clean with button-name and nested-interactive in passes.

    • Keyboard, by arm, read from the head: with reorder off the button's native click carries Enter and Space, so the removed onKeyDown handler lost nothing; with reorder on, useSensors wires KeyboardSensor and the sortable listeners spread onto the view button carry dnd-kit's onKeyDown, so Enter or Space starts a keyboard drag and no switch happens. On the base the same listeners spread came after the div's own onKeyDown and replaced it, so main behaves the same; the dev's round-3 probe measured both arms identical before and after. Not a regression; the changeset now states each arm (ec3561d). The code comment over buildTabContent still says every view is "its own Tab stop that Enter / Space switches to", which the reorderable arm contradicts: a same-file comment, so a nit, not a blocker.
    • data-testid="view-tab-ID" now sits on the button, so during an inline rename that node is absent (the frame carried it throughout before), and the changeset's migration sentence names that testid as the replacement selector without this caveat. In-repo consumers hold (plugin-view, the app-shell sweeps, Build & E2E and Live E2E all success). A nit.
  2. SettingsField (console). The row Label carries id and htmlFor: single-control arms bind by htmlFor to the control carrying the row id (text, email, url, phone, password, textarea, number, json, the toggle Switch, the SelectTrigger, the domain combobox input, the colour swatch); the radio group, the multiselect role="group" and the colour hex input name themselves by aria-labelledby. Judged right for every rendered name: the pin covers each specifier type, reads each control by its own row label and asserts the accessible name equals it, and axe is clean with label and button-name in passes.

    • Wrong in intent, inert in effect: the second wrapper argument is declared labelFor: string | undefined = id, and the radio, multiselect, slider and default arms pass an explicit undefined to mean "no htmlFor", but a default parameter also fires on an explicit undefined, so those four rows still emit htmlFor equal to the row id, which no element in those arms carries (radio items carry ID-value; the multiselect checkboxes, the slider and the fallback carry no id). A dangling for labels nothing and no wcag2a/aa rule reads it, so the pinned names are exactly as rendered. Follow-up: type the argument string | null, pass null, map null to no attribute. A nit.
  3. NavigationRenderer (layout). With reorder on, the sortable node is the row's own li element (SidebarMenuItem is a forwardRef to li at the head, so dnd-kit's setNodeRef and transform style land on it); a separator roots at an aria-hidden li; a group inside a menu roots at a plain li; a top-level group gets inList={false} and stays a section. Judged right against every NavigationItemRenderer call site at the head: the favorites menu, the group-free menu, a group's children, each run of top-level leaves and both SortableNavigationItem paths sit inside a SidebarMenu (a ul element), and the one call outside a list, the top-level group, is the one passing inList={false}. The pin draws grouped and group-free menus with reorder on and off, each with a separator and the grouped ones with a nested group, asserts every ul child is an li, and axe is clean with list and listitem in passes. The re-pointed groupedReorder-11626 pin keeps its intent (one sortable row per drawn entry, none focusable) and adds a no-non-li assertion. Right.

  4. data-table (components). aria-label on the select-all and per-row Checkbox through t('table.selectAllRows') / t('table.selectRow'); useTableTranslation is createSafeTranslation(TABLE_DEFAULT_TRANSLATIONS, …), so the en defaults added to that module-private map are the fallback and the packs win. The pin renders the registered renderer, counts three checkboxes as its control, asserts axe clean with button-name in passes, and reads the names in de against the pack. Right; the card's "through translation keys" clause is met. Every row checkbox shares one name, as the dev notes.

  5. PercentCellRenderer (fields). React.useId() is called before the blank-value early return (hook count stable); the bar gets aria-labelledby pointing at the formatted value span. Right, and a sound departure from "through translation keys": the name is the locale-formatted value and distinct per cell (pinned in de as 1.235 %, and two bars on one page named apart), where a key would name every percent column the same words.

  6. SettingsIcons.knownName-11679.test.tsx (console test, aaa5bfc; outside the card's rules). Both waits for the Building2 glyph go through one helper, waitForBuildingGlyph, built on RTL's waitFor instead of vi.waitFor; iconNoise() and its empty-array assertion are unchanged, the timeout is the same 5000. Judged right: RTL's async utilities lower IS_REACT_ACT_ENVIRONMENT while they wait, so the DynamicIcon state update that lands during the wait is not an act() violation, which is the mechanism the dev's harness traced; the remedy is the wait, not a wider window, so the test discipline is met; the file sits beside SettingsField.tsx in the claim's "tests beside them" surface; the dev's harness logged the same warning on main (2 of 550) as on this branch (5 of 550), so it is pre-existing, not introduced. Test shard 7/8 is success on this head.

Card pins. "Each named control gets an accessible name, through translation keys": met, with item 5's deliberate departure. "Settings labels bind to their controls, and the tab bar's roles nest validly": met; the roles nest validly by not being tab roles, the seat's ruling A. "axe on the two pages reports none of the listed rules": NOT MEASURED at page level by the dev (no backend in the container), substituted by component pins at each traced producer, each with a rule-ran control; the card's counts map onto the producers: showcase button-name 12 = the add button + select-all + 10 row checkboxes (the 10 matching aria-progressbar-name 10), aria-required-parent 4 = four views, nested-interactive 1 = the active view's actions button; settings label 15 and button-name 9 = the auth manifest's input rows and its 7 switches + 2 select triggers; list / listitem 50 = the sidebar rows under reorder. Accepted as evidence; the residual is escalated in ③.

Reverse verification. The dev's base-restore table (each pin red on 7300fca, green on the fix; the round-2 planted leg reproducing the TS6133; the round-3 keyboard probe; the round-4 act() harness) is the implementer's claim, consistent with what the pins assert as read; none was re-run here.

Attribution and governance. Commit trailer pairs are model-free (Claude-Session: plus Co-authored-by: Claude); the PR body carries the session-URL footer; the diff, the changeset and the comments carry no model identifier. No governed surface is in the 13-file list (Governed Surface Queue Guard: success). Line Citation Gate: success; the new comments cite by issue number and symbol.

② Semver level

Changeset .changeset/11690-a11y-names-roles.md: minor for @object-ui/plugin-view, @object-ui/console, @object-ui/layout, @object-ui/components, @object-ui/fields. All five are in the fixed group at the head and @object-ui/console is versioned (privatePackages.version: true); the released packages whose src/ the diff touches are exactly these five. Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Overwrite Report and Changeset Claim Re-read: success. No major, as the fixed-group rule requires.

Level against the diff: no export, prop, type member or language-pack key is published, so nothing demands minor by widening; what changes is the rendered DOM contract of a published component (ViewTabBar: role="tab" and aria-selected removed, aria-current added), which consumers' selectors can see and which is more than patch signals. minor with the breaking semantics stated in the body is this repository's spelling for a breaking change of its own, and the body states it with the FROM → TO selectors. Not a Clause-② narrowing: the removed attributes are an output shape, not a published accept set, so no (narrowing) arm and no ADR-0087 marker is owed. The ec3561d sentence matches the behaviour measured per arm. Right.

Clause-②: line: Clause-②: yes in the claim and the PR body, re-affirmed by the seat (amendment 6020771085) after the dev flagged that the claimed basis (new i18n keys) did not materialise. On the clause's letter (relax the accept set or enlarge the public face, the public face being what the built entry declarations reach) the diff does neither, so yes is over-declared; it errs in the direction that routes a PR to this review rather than past it, a wrong yes is not the auditable false declaration a wrong no is, and the level it compels (at least minor) is the level the DOM change warrants on its own. Accepted as the seat's declaration; recorded as conservative, not false in effect.

③ Boundary flags

Dev open_questions. Round 1 (report 6020678071): (1) tab-bar semantics, A (buttons plus aria-current, actions button as sibling) or B (APG tablist): answered by the seat, A; this review concurs, under tab roles the actions button is reachable or valid, never both. (2) Does Clause-②: yes still apply with no new keys: answered by the seat, keep yes, minor; judged in ②. Rounds 2, 3 and 4 (reports 6020967800, 6021264463, 6023574940): open_questions empty.

Dev deviations.

  • Two producers outside the claim's file list (NavigationRenderer.tsx, data-table.tsx) and the re-pointed groupedReorder-11626 pin: answered; the claim amendment widened the surface to exactly these, with the overlap read against objectui#11682 and objectui#11689 (same files, disjoint regions; the later lander merges).
  • No locale pack edited: answered; the four keys exist at the head in en, zh and de, and the pins read zh and de.
  • Round-1 CI red (Build Console, TS6133 on an unused React import in the new console pin): answered by c8699a0; Bundle Analysis and Type Check are success on this head.
  • Round-3 changeset sentence: answered by ec3561d, matching the measured arms; the stale code comment over buildTabContent is the nit in ① item 1.
  • Round-4 shard-7 red (SettingsIcons.knownName-11679 act() warning): answered by aaa5bfc, judged in ① item 6; the control leg on main 6be0f7a shows it pre-existing.
  • PR body replacement text (rounds 2, 3, 4): the body at read time carries the amended Verification heading, the console type-check bullet, the act()-race bullet and the per-arm keyboard sentence. Answered.
  • Procedural (a foreground run moved to background, verify-lock queue timeouts, worktrees recreated and removed, CPU burners killed by PID): no bearing on the diff.

NOT MEASURED (dev-flagged). Page-level axe on /apps/setup/system/settings/auth and /apps/showcase_app/showcase_task, and a browser visual of the tab bar. Answered as evidence by substitution in ①, with this residual escalated to the seat: the 11-of-12 button-name attribution to the data table is a count inference, not a page reading, so the card's two-page pin is confirmed only when the next console dogfood sweep (the card's source, objectui#11672) re-runs axe on both pages; a listed rule still reporting there is a new card naming the untraced producer, not a reopening of this one.

Dev out_of_scope_findings (carrier none, noted, not filed). English literals still in ViewTabBar ("Unsaved changes", "Save as View", "Reset", the "Active filters" / "Active sort" tooltip, aria-roledescription "view tab") and the data-table "Row actions" label; the settings slider thumb unnamed (shadcn primitive, no-touch zone; not on the auth page); check:i18n-dead-keys false positive on view.addView and view.rename through the file-local viewTabLabel helper; dispatch hypothesis 6 half false (axe-core and vitest-axe are devDependencies, but no test ran axe before this PR); round 3's class-a finding, keyboard view-switching under reorderable (no first-party caller passes it; an activator node on the grip would confine the KeyboardSensor to it); round 4's read-only inference that AppHeader.myOrganizations.test.tsx waits on a DynamicIcon glyph with vi.waitFor the same way. All outside the card's rules; none blocks. Carding is the seat's call; the reorderable keyboard finding and the vi.waitFor sibling are the two worth a card.

Reviewer's own notes (not dev flags). The two nits in ① item 1 (the stale buildTabContent comment; view-tab-ID absent during rename) and the inert htmlFor default-parameter slip in ① item 2 can ride on the reorderable-keyboard card or any later touch of those files. None is a blocker.

Implemented-by: claude/issue-11690-a11y-names-roles
Reviewed-by: session_01CGZy1BGCjdN5cXqL9cnvB8

VERDICT: PASS


Generated by Claude Code

…asured (objectui#11690)

Page-level axe was never re-run on the two pages (no backend in the
container), so the release text no longer says the pages report none of
the faults now. It says what was measured: each fault is fixed where it is
produced and pinned by an axe run on that component.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 331 chunks) 3326.9 KB 3330.4 KB
Main entry chunk (gzip) 154.7 KB 350 KB
Entry file index-DW9bVdXl.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 17.82KB 6.58KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 579.15KB 139.25KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 233.72KB 64.83KB
fields (index.js) 263.41KB 66.72KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 37.51KB 10.04KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.50KB 11.82KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.39KB 15.52KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 199.63KB 47.46KB
plugin-dashboard (index.js) 144.26KB 38.97KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 247.28KB 65.06KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.09KB 45.89KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 238.51KB 65.53KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.85KB 29.12KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.90KB 11.74KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 90.29KB 22.77KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 06bf379934765253c9e7c5b6d8e1dd8407cbbb00
Local-runs: none

Inputs read: card objectui#11690 (body and its seven comments: claim 6018454300, report 6020678071, claim amendment 6020771085, reports 6020967800, 6021264463, 6023574940 and 6023838506), PR objectui#11732 (body, 13-file list, its seven comments, among them the ## Contract review records 6021198733 on c8699a0 and 6023785237 on aaa5bfc, its five commits, and the net diff against main from merge base 7300fca), and the check-runs on this head. Check-runs: 43 in all, 40 success, 3 skipped (Test (coverage), its shard-matrix row, dependabot), 0 failure. Eleven were still in progress at the first read (the eight Test shards, Type Check, Lint, Spec Main Shape Gate); this record waited about eight minutes and the final read found none in progress, every one of them success, and the aggregate Test run present and success. Those conclusions are the gate verdicts here; nothing was built, run or re-run. main is 16 commits past the merge base; GitHub reads the PR mergeable and clean. The PR is a draft with auto-merge unarmed, its head repository is the base repository, and it changes 803 lines in 13 files.

Since the record on aaa5bfc, one commit: 06bf379 changes one line of .changeset/11690-a11y-names-roles.md and nothing else. Every source and test hunk is byte-identical to what that record read; this record judges the whole net diff at this head on its own reading, each hunk's surroundings read from the head's files.

① Derived judgments

Accept set. Nothing an author writes changes. No schema, spec key, prop, config field or exported type is added, widened or removed: SettingsFieldProps, ViewTabBarProps, ViewTabItem, NavigationRendererProps, CellRendererProps and the data-table schema are untouched. What the diff adds to signatures sits on module-private declarations the head does not export: NavigationItemRenderer gains row and inList with the NavRowNode interface; FieldHeader gains labelId and htmlFor; SortableTab's dnd-kit attributes change; TABLE_DEFAULT_TRANSLATIONS gains two entries. No language-pack key is added and no pack file is in the diff: at the head view.addView (en about :1231, zh :1020, de :995), view.rename (en :1223, zh :1012, de :987), table.selectAllRows and table.selectRow (en :603–:604, zh :399–:400, de :388–:389) all exist, and the pins read zh and de against the pack. Judged right: no accept-set change, and the PR claims none.

Public surface. Five rendered outputs change, plus one test outside the card and the release text; each judged:

  1. ViewTabBar (plugin-view). Each view's div element with role="tab", aria-selected and tabIndex becomes a native button element; the active one carries aria-current="true"; the actions-menu trigger (view-tab-actions-ID) is the view button's sibling inside a role-less frame div; the inline-rename input is named through view.rename and stands outside any button; the "+" button is named through view.addView and its tooltip reads the same key, the hard-coded "Add View" gone. Under reorder, SortableTab (about :208) hands setNodeRef and the transform style to its own wrapper div and the dnd-kit listeners and attributes land on the view button, the attributes now saying role: 'button' (redundant on a native button, harmless). The children left inside the view button are non-interactive: the grip span is aria-hidden with pointer-events-none and no tabindex (about :462), and the two tooltip triggers wrap an icon and a span. Judged right: a tab's children are presentational and a tablist may own only tabs, so the per-view actions button could be neither inside a tab (axe nested-interactive) nor beside one (aria-required-children); the bar never had the APG tabs keyboard model; the seat ruled A (amendment 6020771085). The pin asserts zero tab roles, button tags, aria-current on exactly the active view, the actions button a named sibling that opens its menu, the rename box named and outside any button, and axe clean with button-name and nested-interactive in passes on both the static and the reorderable arm.

    • Keyboard, by arm, read from the head: with reorder off the button's native click carries Enter and Space, so the removed onKeyDown handler lost nothing; with reorder on, useSensors wires KeyboardSensor (about :366) and the sortable listeners on the view button carry dnd-kit's onKeyDown, so Enter or Space starts a keyboard drag and no switch happens. On the base the same spread came after the div's own onKeyDown and replaced it, so main behaves the same; the dev's round-3 probe measured both arms identical before and after. Not a regression; the changeset states each arm since ec3561d.
    • Two nits carried unchanged from the earlier two records: the comment over buildTabContent (about :663) still says every view is a Tab stop that Enter or Space switches to, which the reorderable arm contradicts; and data-testid="view-tab-ID" now sits on the button, so during an inline rename that node is absent (the frame carried it throughout before), while the changeset's migration sentence names that testid as the replacement selector without the caveat. In-repo consumers hold (plugin-view, the app-shell sweeps, Build & E2E and Live E2E all success). Neither blocks; both are named in ③.
  2. SettingsField (console). The row Label carries id and htmlFor; single-control arms bind by htmlFor to the control carrying the row id (the text, email, url, phone, password, textarea, number and json inputs; the toggle Switch at about :445; the SelectTrigger at :485; the domain combobox input at :214; the colour swatch at :566); the radio group (:507), the multiselect role="group" (:523) and the colour hex input (:577) name themselves by aria-labelledby. Judged right for every rendered name: the pin covers fourteen specifier types, reads each control by its own row label and asserts the accessible name equals it, and axe is clean with label and button-name in passes.

    • Wrong in intent, inert in effect, now recorded a third time: the second wrapper argument is declared labelFor: string | undefined = id, and the radio, multiselect, slider and default arms pass an explicit undefined to mean "no htmlFor", but a default parameter also fires on an explicit undefined, so those four rows still emit htmlFor equal to the row id, which no element in those arms carries (radio items carry ID-value; the multiselect checkboxes, the slider and the fallback carry no id). A dangling for labels nothing and no wcag2a/aa rule reads it, so the pinned names are exactly as rendered. Fix: type the argument string | null, pass null, map null to no attribute. Not a blocker; escalated in ③ because three patch commits have passed over it.
  3. NavigationRenderer (layout). With reorder on, the sortable node is the row's own li element: SidebarMenuItem is a forwardRef to an li at the head (sidebar.tsx about :524), and React is 19.2.8, so dnd-kit's setNodeRef and transform style land on it; a separator roots at an aria-hidden li; a group inside a menu roots at a plain li; a top-level group gets inList={false} and stays a section. Judged right against every NavigationItemRenderer call site at the head: the favorites menu (:1808), the group-free menu (:1841), a group's children (:1533), each run of top-level leaves (:1908) and both SortableNavigationItem paths (:1234, :1828) sit inside a SidebarMenu ul element, and the one call outside a list, the top-level group (:1927), is the one passing inList={false}. One detail the pin cannot see was read here: the pin asserts every ul child is an li but not that every li has a ul, and in grouped mode a top-level separator is pushed into leafBuffer and flushed inside a leaf run's SidebarMenu, so it never roots an li outside a list. The pin draws grouped and group-free menus with reorder on and off, each with a separator and the grouped ones with a nested group, and axe is clean with list and listitem in passes. The re-pointed groupedReorder-11626 pin keeps its intent (one sortable row per drawn entry, none focusable) and adds an assertion that the menu has no non-li child. Right.

  4. data-table (components). aria-label on the select-all and per-row Checkbox through t('table.selectAllRows') and t('table.selectRow'); useTableTranslation is createSafeTranslation(TABLE_DEFAULT_TRANSLATIONS, …) (about :163), so the pack wins when a provider is present and the two added defaults are the provider-less fallback; the en pack's two values equal the two defaults word for word, so provider-less and provider-ed rendering read the same. The pin renders the registered renderer, counts three checkboxes as its control, asserts axe clean with button-name in passes, and reads the names in de against the pack. Right; the card's "through translation keys" clause is met. Every row checkbox shares one name, as the dev notes.

  5. PercentCellRenderer (fields). React.useId() is called after useDisplayLocale() and before the blank-value early return (hook count stable); the bar gets aria-labelledby pointing at the formatted value span, which renders in the same branch. Right, and a sound departure from "through translation keys": the name is the locale-formatted value and distinct per cell (pinned in de as 1.235 %, and two bars on one page named apart), where a key would name every percent column the same words.

  6. SettingsIcons.knownName-11679.test.tsx (console test, aaa5bfc; outside the card's rules, inside the claim's "tests beside them" surface). Both waits for the Building2 glyph go through one helper built on RTL's waitFor instead of vi.waitFor, same 5000 timeout; iconNoise() and its empty-array assertion are unchanged. Judged right: RTL's async utilities lower IS_REACT_ACT_ENVIRONMENT while they wait, so the DynamicIcon state update that lands during the wait is not an act() violation; the unknown-icon warning the test exists to catch is logged at render, before any wait, so the assertion still catches it; the dev's control leg on main 6be0f7a shows the race pre-existing. Test shard 7/8 is success on this head.

  7. The changeset's first paragraph (06bf379). "reported each fault below, and none of them now" becomes "reported the faults below; each is fixed where it is produced and pinned by an axe run on that component". Right: page-level axe was never re-run (no backend in the container), so the release text now claims only what was measured, which is the rule against advertising what was not delivered applied to a measurement. "the faults" rather than "each fault" is right too: the rename-box name and the separator and nested-group list items came from the component runs, not the page sweep, so "each" would have claimed the pages reported them.

Card pins. "Each named control gets an accessible name, through translation keys": met, with item 5's deliberate departure. "Settings labels bind to their controls, and the tab bar's roles nest validly": met; the roles nest validly by not being tab roles, the seat's ruling A. "axe on the two pages reports none of the listed rules": NOT MEASURED at page level by the dev, substituted by component pins at each traced producer, each with a rule-ran control; the card's counts map onto the producers: showcase button-name 12 = the add button + select-all + 10 row checkboxes (the 10 matching aria-progressbar-name 10), aria-required-parent 4 = four views, nested-interactive 1 = the active view's actions button; settings label 15 and button-name 9 = the auth manifest's input rows and its 7 switches + 2 select triggers; list / listitem 50 = the sidebar rows under reorder. Accepted as evidence; the residual is escalated in ③, and since 06bf379 the release text no longer claims the page-level result.

Reverse verification. The dev's base-restore table (each pin red on 7300fca, green on the fix; the round-2 planted TS6133 leg; the round-3 keyboard probe; the round-4 act() harness) is the implementer's claim, consistent with what the pins assert as read; none was re-run here.

Attribution and governance. The five commits end in the model-free trailer pair; the PR body carries the session-URL footer; the diff, the changeset and the comments carry no model identifier. No governed surface is in the 13-file list (Governed Surface Queue Guard: success). Line Citation Gate: success.

② Semver level

Changeset .changeset/11690-a11y-names-roles.md: minor for @object-ui/plugin-view, @object-ui/console, @object-ui/layout, @object-ui/components, @object-ui/fields, which are exactly the five released packages whose src/ the diff touches. Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Overwrite Report and Changeset Claim Re-read: success. No major, as the fixed group requires.

Level against the diff: no export, prop, type member or language-pack key is published, so nothing demands minor by widening; what changes is the rendered DOM contract of a published component (ViewTabBar: role="tab" and aria-selected removed, aria-current added), which consumers' selectors can see and which is more than patch signals, so minor is the right level, and the body states the FROM → TO selectors (data-testid="view-tab-ID" or aria-current). Not a Clause-② narrowing: the removed attributes are an output shape, not a published accept set (nothing an author writes is removed), so no (narrowing) arm and no ADR-0087 marker is owed. The changeset's behavioural sentences match the head: the per-arm keyboard sentence matches the measured arms, and the first paragraph since 06bf379 matches what was measured. Right.

Clause-②: line: Clause-②: yes in the claim and the PR body, re-affirmed by the seat (amendment 6020771085) after the dev flagged that the claimed basis (new i18n keys) did not materialise. On the clause's letter (relax the accept set or enlarge the public face) the diff does neither, so yes is over-declared; it errs in the direction that routes a PR to this review rather than past it, and the level it compels (at least minor) is the level the DOM change warrants on its own. Accepted as the seat's declaration; recorded as conservative, not false in effect.

③ Boundary flags

Dev open_questions. Round 1 (report 6020678071): (1) tab-bar semantics, A (buttons plus aria-current, actions button as sibling) or B (APG tablist): answered by the seat, A; this review concurs, under tab roles the actions button is reachable or valid, never both. (2) Does Clause-②: yes still apply with no new keys: answered by the seat, keep yes, minor; judged in ②. Rounds 2 to 5 (reports 6020967800, 6021264463, 6023574940, 6023838506): open_questions empty.

Dev deviations.

  • Two producers outside the claim's file list (NavigationRenderer.tsx, data-table.tsx) and the re-pointed groupedReorder-11626 pin: answered; the claim amendment widened the surface to exactly these, with the overlap read against objectui#11682 and objectui#11689 (same files, disjoint regions; the later lander merges).
  • No locale pack edited: answered; the four keys exist at the head in en, zh and de, and the pins read zh and de.
  • Round-1 CI red (Build Console, TS6133 on an unused React import in the new console pin): answered by c8699a0; Bundle Analysis and Type Check are success on this head.
  • Round-3 changeset sentence: answered by ec3561d, matching the measured arms; the stale code comment over buildTabContent is the nit in ① item 1.
  • Round-4 shard-7 red (SettingsIcons.knownName-11679 act() warning): answered by aaa5bfc, judged in ① item 6.
  • Round-5 wording one word away from the seat's example ("the faults below" for "each fault below"): answered; judged right in ① item 7.
  • PR body replacement text (rounds 2, 3, 4): the body at read time carries the amended Verification heading, the console type-check bullet, the act()-race bullet and the per-arm keyboard sentence; round 5 needed none and the body's opening already says the pins are component-level. Answered.
  • Procedural (a foreground run moved to background, verify-lock queue timeouts, worktrees recreated and removed, CPU burners killed by PID): no bearing on the diff.

NOT MEASURED (dev-flagged). Page-level axe on /apps/setup/system/settings/auth and /apps/showcase_app/showcase_task, and a browser visual of the tab bar. Answered as evidence by substitution in ①, and the release text no longer claims the page-level result; this residual is escalated to the seat: the 11-of-12 button-name attribution to the data table is a count inference, not a page reading, so the card's two-page pin is confirmed only when the next console dogfood sweep (the card's source, objectui#11672) re-runs axe on both pages; a listed rule still reporting there is a new card naming the untraced producer, not a reopening of this one.

Dev out_of_scope_findings (carrier none, noted, not filed). English literals still in ViewTabBar ("Unsaved changes", "Save as View", "Reset", the "Active filters" / "Active sort" tooltip, aria-roledescription "view tab") and the data-table "Row actions" label; the settings slider thumb unnamed (shadcn primitive, no-touch zone; not on the auth page); check:i18n-dead-keys false positive on view.addView and view.rename through the file-local viewTabLabel helper; dispatch hypothesis 6 half false (axe-core and vitest-axe are devDependencies, but no test ran axe before this PR); round 3's class-a finding, keyboard view-switching under reorderable (no first-party caller passes it; an activator node on the grip would confine the KeyboardSensor to it); round 4's read-only inference that AppHeader.myOrganizations.test.tsx waits on a DynamicIcon glyph with vi.waitFor the same way. All outside the card's rules; none blocks. Carding is the seat's call; the reorderable keyboard finding and the vi.waitFor sibling are the two worth a card.

Reviewer's own escalation (not a dev flag). Three nits have now stood in three records across three patch commits without a carrier: the stale buildTabContent comment and the view-tab-ID testid absent during rename (① item 1), and the inert htmlFor default-parameter slip in SettingsField (① item 2). None changes a rendered name or blocks this head, but a note that rides from record to record is on its way to being lost at merge: the seat should give them one carrier, either the reorderable-keyboard card above or a small follow-up card against these two files, rather than a fourth note.

Implemented-by: claude/issue-11690-a11y-names-roles
Reviewed-by: session_01CGZy1BGCjdN5cXqL9cnvB8

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 6, 2026 19:44
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 6, 2026 19:44
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit 9fc68aa Oct 6, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11690-a11y-names-roles branch October 6, 2026 20:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

a11y (axe wcag2a/aa): unnamed buttons and broken tab roles in the view tab bar, unbound labels on the settings form, and unnamed percent progress bars

2 participants