Skip to content

fix(app-shell): draw the shared EmptyDescription for the previews' empty-collection sentences - #8569

Merged
os-justin merged 2 commits into
mainfrom
claude/issue-8526-previews-shared-empty-collection
Sep 8, 2026
Merged

os-justin merged 2 commits into
mainfrom
claude/issue-8526-previews-shared-empty-collection

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #8526

Sixteen empty-collection sentences across twelve files under packages/app-shell/src/views/metadata-admin/previews/ each hand-rolled the same state — a muted italic sentence in a plain div — and two of those files (AgentPreview, ToolPreview) declared a file-local component literally named Empty, which shadowed the shared family in the one place a maintainer would reach for it. All sixteen now render EmptyDescription from @object-ui/components, used alone, outside any Empty container — the shape PR #8527 (objectui#8520) landed and measured at 16px / 12px / start-aligned / italic against the row it replaced. The two local Empty components are deleted. Nothing lands in @object-ui/components; @object-ui/components is already workspace:* in app-shell, so no manifest edge was added.

Per the ruling on the card: the Empty container is not used anywhere (118.8px at the rail's 215px width), and EmptyValue is not used anywhere (its aria-label says "No value", which is false about a collection).

Census, re-derived

Pathspec 'packages/app-shell/src/views/metadata-admin/previews/*', run from the repo root. Lit control on the same pathspec: git grep -c PreviewShell fires on 24 files, so a zero from any probe below is an absence rather than a dead glob. Probes: (A) \bitalic\b, (B) border-dashed, (C) text-amber, (D) PreviewEmptyState, (E) function Empty\b, (F) the Empty* family names, (G) length-conditional rendering (.length === 0, !x.length, Object.keys(x).length), (H) JSX text opening with No / none / empty, (I) tr('engine.*.(no…|empty)') keys. Every probe's matched lines were read in context, not counted.

Reconciled by their differences, not their union: probe G (length shape) returned a false zero on TranslationPreview:152, whose condition is cat.count === 0 — probes A and H found it. Probe A (italic) returned a false zero on the two sites that went through the local Empty (AgentPreview:136, ToolPreview:185) — probes E and H found them. Probe G is the one that surfaces the families probe A cannot see at all: the dashed drop zones, the amber warnings, the palette search empties.

Effective type scale, read from each site's ancestors rather than from its own class list: text-xs at 13 of the 16 migrated sites (explicit at six; inherited from a text-xs container at seven — ActionPreview:578, EmailTemplatePreview:93, FlowPreview:341, FlowRunsPanel:388, FlowSimulatorPanel:228, JobPreview:330, AgentPreview:161), 10px at one, 11px at one, text-sm at one. The card's "six different type scales" does not reproduce; the ruling's "uniform with a few genuine outliers" does. Instrument for the class merge: tailwind-merge@3.6.0 (the version cn() resolves), which evicts the family's text-sm/relaxed for any explicit size class — and keeps it for bare italic. So every site spells its size out; bare italic would have been a 14px / 22.75px delta at the seven inheriting sites.

Per-site delta

Site (line on main) Before className now Delta
AgentPreview:136 local Empty → div.text-xs.text-muted-foreground.italic text-xs italic zero
AgentPreview:192 "Defaults in use." div.text-muted-foreground.italic, inherits xs text-xs italic zero
AgentPreview:256 chip-list emptyHint div.text-xs.text-muted-foreground.italic text-xs italic zero
AgentPreview:221 local Empty declaration deleted —
ToolPreview:185 local Empty text-xs italic zero
ToolPreview:296 local Empty declaration deleted —
DatasourcePreview:159 div.text-xs.text-muted-foreground.italic text-xs italic zero
SkillPreview:107 same text-xs italic zero
ActionPreview:581 div.text-muted-foreground.italic, inherits xs text-xs italic zero
EmailTemplatePreview:98 same shape, inherits xs; keeps its code child (the family's child rules target a only) text-xs italic zero
FlowPreview:346 same, inherits xs text-xs italic zero
FlowRunsPanel:349 step log div.text-[10px].italic.text-muted-foreground text-[10px] italic zero — 10px kept: the run row's own metadata (status, time, duration at lines 331–336) is 10px
FlowRunsPanel:406 runs list div.italic.text-muted-foreground, inherits xs text-xs italic zero
FlowSimulatorPanel:324 scratch rows same, inherits xs text-xs italic zero
FlowSimulatorPanel:383 variable watch same, inherits xs text-xs italic zero
JobPreview:348 same, inherits xs text-xs italic zero
TranslationPreview:153 div.text-[11px].text-muted-foreground.italic text-[11px] italic zero — 11px kept: the category card's sample list it stands in for is 11px (line 155)
ScreenPreview:77 p.text-sm.italic.text-muted-foreground text-sm italic zero metrically (14px / 20px both ways; preflight zeroes p margins); element p → div

The outliers, one sentence each

  • text-sm (ScreenPreview:77) — kept at text-sm, deliberately: this sentence stands in for the screen body, whose description renders at text-sm one line below (line 84), not for a text-xs rail row; averaging it down to xs would have been a visible delta with no sibling to justify it. The pin asserts the class stays.
  • Inline span "none" (ValidationPreview:353) — left as is; the ruling does not cover inline markers. A second inline carrier the ruling did not enumerate turned up: DatasourcePreview:225 span.text-muted-foreground.italic "not configured" (a rail card whose object has no keys). Left for the same reason and reported.
  • Chrome site (ValidationPreview:387, rounded border bg-background p-2.5) — establishing what the chrome does: it is the CelBlock box (ValidationPreview:439, rounded border bg-background p-2.5 text-xs font-mono), so the sentence occupies the slot the rule body would have. It is also not an empty-collection state: it is the default: arm for an unknown rule type ("Unknown rule type … Showing common fields only."). Left untouched, chrome and all.

Not migrated, by class

  • Amber warning-toned sentences — JobPreview:357, JobPreview:387, SkillPreview:100, ValidationPreview:282 / 335 / 378 / 408 / 436, BookPreview:254. Two of these are in the card's fifteen. They deliberately are not muted; migrating them means either dropping the amber (a visible delta the ruling did not authorize) or overriding the family's colour so the membership is nominal. A design call above this card.
  • Dashed drop-zone placeholders — AppNavCanvas:330, FieldsListEditor:170, PageBlockCanvas:554 / 828, ObjectFormCanvas:516; bordered, centred editor targets, a different visual class (and two dashed conventions: border border-dashed vs border-2 border-dashed in FieldStub).
  • Palette / search empties — PageBlockCanvas:915, ObjectFormCanvas:1323, AddWidgetPicker:86, ViewColumnPanes:320 (via its own Hint); centred, padded, not italic.
  • The directory's own full-canvas answers — PreviewEmptyState (BookPreview:191, DatasetPreview, ReportPreview) and PreviewMessage sites; ProblemsPanel:53 (a success state with an icon).
  • Inline value placeholders — DashboardPreview:266 "untitled", ValidationPreview:167 "no message set", FieldStub:205 (a stub input's placeholder; in the card's fifteen). EmptyValue's question, not this card's.
  • Adjacent non-empty branches with the same styling — FlowRunsPanel:402 (service unavailable; its sibling branch at 406 is migrated), FlowSimulatorPanel:442 (idle hint), BookPreview:250 (explains that members are derived; not an absence).

Tests

packages/app-shell/src/views/metadata-admin/previews/emptyCollection-8526.test.tsx — 33 cases: for each of the sixteen sites an empty leg (the family's data-slot="empty-description" is present under the titled container, carries the site's sentence, and no empty-value is present) and a populated leg (the rows render and no empty-description exists under that container — the caricature refuser), plus one whole-surface case on a fully populated agent. Every navigator reaches the container that owns a title (Section heading, rail label, panel header, category-card label), never the empty state and never a child position.

Measured, not predicted — three legs, each restored by state (git diff HEAD empty over the 12 paths and all 12 blob hashes equal to HEAD:path), mutation proven on disk in both directions, trap on EXIT INT TERM:

  • Fix reverted (12 source files at the base sha; on disk: 1 EmptyDescription tag left — the objectui#8520 precedent — and 2 local Empty declarations back): 16 failed / 17 passed; all 16 on "the shared EmptyDescription slot" expected truthy; 0 harness deaths.
  • Caricature (an unconditional EmptyDescription inserted before every migrated conditional; on disk: 16 markers, 0 after restore): 33 failed / 0 passed, 0 harness deaths — 16 populated cases on toBeNull, the whole-surface case on toHaveLength(0), 16 empty cases on the sentence mismatch. The first run of this leg against the first version of the pin was the instrument that mattered: 8 of the 17 populated cases died on their marker lookup ("Unable to find an element") before the refusing assertion ran, because the harness took children[1] as the body and the inserted sibling shifted the rows one slot over. The pin was rewritten to container-level navigation with descendant queries (second commit) and the leg re-run; the numbers above are from the re-run.
  • Harness-kill (ToolPreview's section title renamed): 2 failed / 31 passed, both Unable to find an element with the text: Input Parameters — textually distinct from both modes above.

Verification

  • Closure built first: turbo run build --filter='@object-ui/app-shell^...' — 28/28 tasks. pnpm --filter @object-ui/app-shell type-check (source leg and the tsconfig.test.json leg): exit 0 before and after.
  • Tests, from the repo root with paths: the pin 33/33; the 14 test files that import a touched module (enumerated by grep with a firing control) 14 files / 143 tests; the extended set — the seven FlowNodeInspector tests (the one consumer of ScreenPreview outside the directory) and the two console tests naming the previews — 9 files / 297 tests. turbo ls --affected names app-shell, console and two example packages; the two examples carry no test that reaches these modules.
  • Lint, declared narrowing: CI's pnpm lint is turbo run lint, one eslint . per package, and only app-shell changed. The CI-exact invocation for app-shell (eslint . inside the package, --format json) at 61b113204: 1110 files, 0 errors, 2925 warnings (the known-debt stream; 24 of them in the twelve touched files, all pre-existing, none on a changed line; 0 in the pin). Invariance: eslint --print-config on a preview file shows no parserOptions.project / projectService, so this diff cannot move any untouched package's verdict.
  • Gates green: check:control-bytes, check-changeset-presence (12 source files of 1 released package, 1 changeset), check-changeset-no-major, check:vi-mock-specifiers, check:i18n-keys, check:phantom-deps. check-governed-queue-guard --test on the diff paths: NOT GOVERNED.
  • Changeset: .changeset/previews-shared-empty-description-8526.md, @object-ui/app-shell: patch (skip-changeset is a phantom label in this repo and is not applied).

Where the card and the ruling read differently from main

🤖 Generated with Claude Code

https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S


Generated by Claude Code

…pty-collection sentences

Sixteen sites across twelve files under `metadata-admin/previews/` each
hand-rolled the same empty-collection state — a muted italic sentence in a
plain `div` — and two of them (`AgentPreview`, `ToolPreview`) declared a
file-local component literally named `Empty` that shadowed the shared family
in the one place a maintainer would reach for it. They now render
`EmptyDescription` from `@object-ui/components`, used alone outside any
`Empty` container, the shape PR #8527 measured at 16px / 12px / start-aligned
/ italic against the row it replaced.

Every site keeps its measured size class, so the per-site layout delta is
zero: `text-xs` where the row sat in (or inherited from) a text-xs rail,
`text-[10px]` for the run row's step log, `text-[11px]` for the translation
category card, and `text-sm` for the screen body's sentence, which stands in
for a description that renders at text-sm. Bare `italic` would have kept the
family's `text-sm/relaxed`, so each size is spelled out.

Not migrated, each for its own reason: the inline `span` carriers
("none", "not configured"), the amber warning-toned sentences, the dashed
drop-zone placeholders, the palette search empties, the `PreviewEmptyState` /
`PreviewMessage` full-canvas states, and the unknown-rule-type fallback whose
chrome is the CEL block's own box.

The pin navigates every site by a title its container owns, never by the
empty state, so a caricature that renders the description unconditionally
fails the populated assertions instead of killing the harness.

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

The caricature (EmptyDescription rendered unconditionally, populated
collections included) was run against the first version of this pin: eight of
the seventeen populated cases died on their marker lookup — "Unable to find an
element" — before the refusing `toBeNull` assertion ever ran, because the
harness took `children[1]` as the body slot and the caricature's inserted
sibling shifted the rows one slot over. Every navigator now returns the
container that owns the title, and both assertions are descendant queries on
it; the empty state can never be that container, because the container is
what holds the title.

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) 3475.4 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-C8LbWm6P.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.93KB 114.12KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.12KB 3.28KB
data-objectstack (index.js) 192.25KB 53.44KB
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.46KB 63.90KB
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

@os-justin
os-justin marked this pull request as ready for review September 8, 2026 12:20
@os-justin
os-justin enabled auto-merge September 8, 2026 12:20
@os-justin
os-justin added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit 1cca441 Sep 8, 2026
35 checks passed
@os-justin
os-justin deleted the claude/issue-8526-previews-shared-empty-collection branch September 8, 2026 12:48
os-sales added a commit that referenced this pull request Sep 8, 2026
Brings the branch up to date after #8569, #8574 and the rest landed. No conflict:
nothing on main touched packages/types/src/__tests__/zod-mirror-parity.test.ts since
this branch's base f5cfbbd, so the objectui#8458 pin and the objectui#8248 amendment
compose with main rather than contending with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants