Skip to content

fix(app-shell,plugin-dashboard,plugin-grid,plugin-chatbot,console): draw the shared EmptyValue for the six remaining hand-rolled placeholders - #8518

Merged
os-justin merged 5 commits into
mainfrom
claude/issue-8504-emptyvalue-remaining-carriers
Sep 8, 2026
Merged

fix(app-shell,plugin-dashboard,plugin-grid,plugin-chatbot,console): draw the shared EmptyValue for the six remaining hand-rolled placeholders#8518
os-justin merged 5 commits into
mainfrom
claude/issue-8504-emptyvalue-remaining-carriers

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #8504

Six hand-rolled <span>—</span> placeholders become the shared EmptyValue from @object-ui/components, the way PR #8503 did for the five sites in objectui#8491. This closes the no accessible name half of the class; the DetailSection / HeaderHighlight half (which already carries aria-label={t('detail.noValue', …)}) is a separate card and is untouched here.

The six carriers

file line on origin/main change
packages/app-shell/src/views/metadata-admin/ResourceListPage.tsx :636 defaultCell<EmptyValue />
packages/app-shell/src/views/metadata-admin/AuditPanel.tsx :252 lock column → <EmptyValue />
packages/plugin-dashboard/src/RecordDetailDrawer.tsx :127 empty <dd><EmptyValue />
packages/plugin-grid/src/ImportWizard.tsx :1034 saved-mapping transform cell → <EmptyValue />
packages/plugin-chatbot/src/AiPendingActionsInbox.tsx :193 JsonBlock<EmptyValue className="text-xs" />
apps/console/src/pages/developer/PublicFormsPage.tsx :385 object column → <EmptyValue />

Each was a plain span with no data-slot, no aria-label and none of the shared component's select-none / no-underline / pointer-events-none. The pointer-events-none half is not cosmetic in the metadata list: defaultCell's output for column 0 is rendered inside the row's <Link>, so the hand-rolled span inherited the link colour and stayed selectable — a missing value that looked clickable, the same affordance bug #8503 named in the grid's link cells.

The adjacent pair, decided deliberately

AiPendingActionsInbox.tsx:507 / :511 ({selected.proposed_by ?? '—'}, {selected.decided_by ?? '—'}) are in. They are a different source spelling — a bare text node in a plain <div>, not a styled span — but the rendered defect is identical, they sit in the same drawer as :193, and the card verified them individually rather than leaving them in the un-triaged population. Leaving them would ship an unannounced em dash out of a PR whose purpose is to retire them from that file, and force a second PR onto the same four lines.

Deliberately NOT swept in: formatRelative (AiPendingActionsInbox.tsx:172) and formatImportJobTime (ImportWizard.tsx:1433), both if (!x) return '—' inside a : string-declared helper. Converting those changes a return type and every call site — they belong to the fenced 41-member population under separate triage. Likewise untouched: ImportWizard's '— None —' / '— Map columns manually —' / '— Skip —' i18n labels and PublicFormsPage's <option>— Select a FormView —</option>, which are em dashes as label decoration, not placeholders.

Reachability — checked, not assumed

All five packages already declare @object-ui/components at workspace:*. No manifest edge was added, and none was needed. apps/console (the one carrier in apps/, not packages/) has it on devDependencies, and 29 files under apps/console/src already import from it — this page among them, so EmptyValue joined an import list that was already there. check:phantom-deps and check:unused-deps are both green on the result.

Visual delta, per site — honestly

site delta
ResourceListPage, AuditPanel, PublicFormsPage, ImportWizard text-muted-foreground (full opacity) → shared text-muted-foreground/50. One deliberate step more muted, plus the three affordances and the accessible name.
RecordDetailDrawer text-muted-foreground/60/50. Same, one step smaller.
JsonBlock (:193) Purely additive. text-xs is kept through className — it stands where a text-xs <pre> would, with no neighbouring shared placeholder to match — so only the accessible name, the three affordances and the opacity change.
:507 / :511 Purely additive. They were bare text inheriting text-xs font-mono from the parent <div>, which they still inherit.

The glyph is an unchanged em dash at every site: AuditPanel.lockState.test.tsx's pre-existing expect(screen.getByText('—')) still passes untouched.

Pins — 22 cases across 6 files, and what each one MEASURED

The caricature was run, not reasoned about: EmptyValue drawn unconditionally at all eight sites, filled values included. 21 of 22 red, 1 green.

  • 7 assertions fail because a filled cell GAINED a placeholder — the load-bearing refusal. Every file has one (and the filled row does NOT, CONTROL: the locked sibling is still not a placeholder, …). Two files had none until this measurement; the cases were added for exactly that reason.
  • 7 NON-REGRESSION cases fail one assertion earlier, on value presence (the value reaches the cell): the caricature also stops these surfaces printing values, so their own no placeholder half is never reached. They do refuse it — just not through the half that names the defect.
  • 6 THE DEFECT cases fail ONLY on their CONTROL. Their headline claims — "the empty cell has an accessible name" — are equally true of a surface that has stopped rendering values, exactly as fix(plugin-grid,plugin-detail): draw the shared EmptyValue for missing cell values #8503 measured. The controls are not optional decoration.
  • 1 case survives: SCOPE DECLARATION — the placeholder is inert inside the row link. It reads pointer-events-none / no-underline / select-none off the placeholder, which is true of an implementation that draws one over every cell. It is labelled a scope declaration in its own name and docblock, not quoted as proof. It goes red on the revert leg, so it does hold what the hand-rolled span lacked.

Revert leg (hand-rolled spans restored on the committed implementation): 16 red / 6 green, every failure on a labelled headline assertion (the empty cell draws the shared placeholder), every NON-REGRESSION-shaped case green — the correct shape.

A first caricature run failed the metadata-list pin's three cases on the harness (the row for beta rendered) rather than on any assertion: the row lookup read column 0, which defaultCell also renders. It now reads the row link's href, and two dereferences that had been failing with a bare TypeError — so their messages never reached the summary — are guarded.

Every assertion is scoped to one cell / one row / one labelled field, never the container (objectui#8495).

Both ablation legs restore by state: git checkout HEAD -- <absolute path> (never the bare form, which reads the index), proven by git hash-object against git rev-parse HEAD:<path> for all six files plus an empty git diff HEAD, with a trap … EXIT INT TERM. Mutations are proven to have landed on disk by before/after occurrence counts, and an anchor miss aborts the leg rather than silently measuring nothing.

Verification

  • pnpm exec vitest run <the six pins>6 files, 22 tests, green.
  • Narrowed non-regression set (declared): the full packages/plugin-dashboard/, packages/plugin-chatbot/ and apps/console/src/pages/developer/, plus AuditPanel.lockState, AuditPanel.loadFailure, ImportWizard.i18n-parity, ImportWizard.interpolation and savedMapping155 files, 1457 tests, green.
  • turbo run type-check over all 7 affected packages → 42/42 tasks green.
  • turbo run lint over the 5 edited packages → 0 errors (warnings are the pre-existing baseline). --no-inline-config deliberately not used — that is an objectstack convention and manufactures errors CI does not have here.
  • Gates: check:vi-mock-inherit, check:vi-mock-specifiers, check:control-bytes, check:phantom-deps, check:unused-deps, check:unreferenced-sources, check-changeset-presence → all green. check-governed-queue-guard --test reports NOT GOVERNED.
  • Declared narrowing: the full affected-package suite (packages/app-shell/ alone is 281 files) was started and abandoned after 31 minutes under container contention from a sibling agent. CI runs the whole farm. Only this session's own recorded PIDs were killed — never by process name.

Note for review

ImportWizard.tsx gains one entry on its existing __testables seam (SavedMappingSummary, the 22nd, same @internal contract as the 21 before it). Reaching that component through the wizard means driving a spreadsheet parse and a Radix Select, neither of which the component owns. What the seam does not cover: that StepMapping still routes to it — that line is untouched here.

A real changeset is included (minor on all five packages, matching #8503). skip-changeset is a phantom label in this repo and is deliberately not applied.

Left in draft with no auto-merge, per dispatch.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S


Generated by Claude Code

…raw the shared EmptyValue for six hand-rolled placeholders

Six sites built their own `<span>` holding a bare em dash, so an empty cell
had no accessible name at all: no `data-slot`, no `aria-label`, and none of
the shared component's `select-none` / `no-underline` / `pointer-events-none`.
`EmptyValue` from `@object-ui/components` answers exactly this, and every one
of the five packages already depends on that package.

Two adjacent lines in the AI-approvals drawer (`proposed_by` / `decided_by`,
bare `'—'` text nodes) are converted with them — a different source spelling
of the same rendered defect, verified individually rather than swept up.
`formatRelative`'s `: string` fallback is deliberately left alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
…column 0

Column 0 is rendered through `defaultCell` itself, so the caricature
ablation (EmptyValue everywhere) erased the very name the harness looked
the row up by: all three cases went red on "the row for beta rendered"
— the harness dying, not an assertion refusing the caricature.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
…were missing

The caricature (EmptyValue drawn over every cell) was run against all six
pins. Measured: every THE DEFECT case failed only on its CONTROL — its
headline claim is equally true of a surface that has stopped rendering
values — and each NON-REGRESSION case failed one assertion earlier, on
value presence, so its own 'no placeholder' half never ran.

Three files already had a case whose assertion fails BECAUSE a filled cell
gained a placeholder. Two did not; they do now. Two dereferences that had
been failing with a bare TypeError are guarded so their messages reach the
summary, and the one case the caricature survives is relabelled a scope
declaration rather than quoted as proof.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
…aricature directly

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
…cblock

They were inserted to keep a glob from closing the block comment, and
`no-irregular-whitespace` is an ERROR in apps/console — the only three
lint errors on the branch. `check:control-bytes` is green either way:
U+200B is not a control byte. The sentence names the pathspec in prose
instead.

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

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3473.9 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-PfcX5DZr.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) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
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) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 498.87KB 114.10KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.12KB 3.28KB
data-objectstack (index.js) 191.36KB 53.16KB
fields (index.js) 243.24KB 61.42KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
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) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
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) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.53KB 46.34KB
plugin-dashboard (index.js) 131.43KB 34.44KB
plugin-designer (index.js) 213.21KB 43.63KB
plugin-detail (index.js) 248.73KB 63.97KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.30KB 56.63KB
plugin-kanban (index.js) 55.40KB 15.71KB
plugin-list (index.js) 112.74KB 27.70KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.54KB 20.84KB
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.66KB 3.50KB
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) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
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) 0.20KB 0.18KB
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) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
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

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.

Six hand-rolled em-dash placeholders render with NO accessible name — the same class objectui#8491 closed at five sites, in six more files

2 participants