Skip to content

fix(plugin-detail): header highlights share the row's free width and truncate only when it runs out (objectui#11684) - #11719

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11684-highlight-share-row
Oct 6, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11684-highlight-share-row

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11684

Clause-②: no

What was wrong

HeaderHighlight (@object-ui/plugin-detail) laid each highlight chip out as a fixed column. The basis was 9rem (16rem for a wide display type or a column being edited), with no grow and a 16rem / 24rem cap. The row is flex flex-wrap with overflow: visible, and the value is a block min-w-0 truncate span. So a chip never got wider than its basis, and a value wider than the chip's content box was cut, whatever the row had left.

Measured in Chromium in the console's own drawer path at the default width. The scratch harness mounted the real RecordDetailView embedded inside the real NavigationOverlay drawer, with the showcase Product object. The harness was deleted before commit. At 1440px the drawer is 864px and the highlight row is 767px. The three chips use 432px and leave 335px free. The Description chip holding "QA Widget 1" has a 79px value box for an 80px text, so it clips. The 79px is the 144px basis, minus 40px padding, a 1px divider and 24px for the hover pencil.

The change

One class string on the chip column, plus its comment:

  • basis-[9rem] / basis-[16rem] stay. They are now the chip's floor, and line breaking still reads them, so which chips share a line does not change.
  • grow hands the line's free width to the chips.
  • max-w-max replaces the 16rem / 24rem caps. Each chip is capped at its own content. A chip that fits stops growing, and the flex algorithm passes the rest of the free width to the chips that still need it.
  • min-w-[min(9rem,100%)] / min-w-[min(16rem,100%)] replace min-w-[7rem]. A short value keeps its 9rem column, and a column can still narrow to a row that is itself narrower than the floor.

No new authored key, no prop and no export. The row container is unchanged.

Route: content cap, not the suggested share-of-row cap

The order suggested letting each column grow with its basis as the floor, under a cap that is a share of the row. I measured the alternatives against the card's Done-when and took a content cap. Reasons:

  1. With equal growth, a long value gets only its 1/n share of the free width, while short neighbours keep unused room inside their own boxes. With the content cap, the free width goes to whichever value needs it. In the long-description reading below, Description takes all 335px.
  2. A share cap still cuts a long value while the row has room. Computed, not measured, for the 767px drawer row: with SKU at 144px and a 50% cap of 383px on the description, 240px of the row stays empty while the description truncates. That is this card's symptom again.
  3. Equal growth makes every chip wider than what it shows. That stretches a sparse strip across the row, which "Phase N.3 — Highlight strip packs left" (@object-ui/components CHANGELOG) set the caps to stop. By construction (not measured), it also moves the hover pencil, which justify-between places, to the far edge of a stretched chip. Under the content cap no chip is wider than its content, so a sparse strip still packs left and the pencil stays beside its value.
  4. Strips whose values already fit keep the same geometry: dividers, pencil positions and line breaks. Only a chip whose value did not fit gets wider, and only by what it needs, out of free width.

What I kept from the old caps: their reason, namely that a highlight is never stretched beyond its content and a sparse strip packs left. What I dropped is the fixed 16rem / 24rem point where a long value was cut regardless of free width. A single highlight can now reach the full row width only when its own content is that long and its line has room.

Premise checks (the order's mechanism assumptions, measured at base 48c82c9)

  1. Truncation inside a narrow column: confirmed. Row display: flex, flex-wrap: wrap, overflow: visible. Chip flex: 0 1 144px, max-width: 256px, min-width: 112px. Value truncate. The Description chip read 79 of 80px at every drawer width measured (table below). The SKU chip is the first chip, so it has no left padding and a 100px box. Its 100px text fit exactly in this container's fonts. The sweep's host clipped both, so the pin covers every chip that carries the measured text.
  2. Flex distribution: taken in its "grow with the basis as floor" half. Its "share-of-row cap" half was replaced by the content cap (see Route). The editor still gets room: an editing column keeps the 16rem floor and can grow to its editor's natural width. The other chips give way by wrapping, on the same line structure as before (reading below).
  3. The default width: the console's object page passes no authored navigation for Product, so deriveRecordSurface opens a drawer with overlayWidthFor('auto', objectDef). Five fields give the md bucket, min(92vw, 720px). The shell treats that as a floor. The rendered drawer carries --ov-w: max(min(92vw, 720px), min(60vw, 880px)), which is 720 / 768 / 864 / 880px at 1024 / 1280 / 1440 / 1920. The published RecordDetailDrawer default (RECORD_OVERLAY_DEFAULT_WIDTH, min(960px, 60vw)) goes through the same shell expression. jsdom does not lay out, so the committed pin asserts the classes that decide the distribution. The browser reading is below and was not committed (see Acceptance notes).
  4. The full record page: the same strip, and the same component, renders there. I read it with the harness mounting RecordDetailView at full viewport width. That is wider than the console page shell, so it is the stress case for wide rows. Short values look exactly as in the drawer: the chips pack left and nothing stretches. A long value grows to its content and stops there.

Browser reading (Chromium 1194, one-time, scratch harness deleted)

"col" is the chip column width and "box / text" is the value's clip box against its scroll width, in px.

reading before (base, and the ablation leg) after (head)
drawer 1024 · row 623 Description col 144 · box 79 / text 80 clipped · 191 free col 145.16 · box 80.16 / text 80 whole · 189.84 free
drawer 1280 · row 671 col 144 · 79 / 80 clipped · 239 free col 145.16 · 80.16 / 80 whole · 237.84 free
drawer 1440 · row 767 col 144 · 79 / 80 clipped · 335 free col 145.16 · 80.16 / 80 whole · 333.84 free
drawer 1920 · row 783 col 144 · 79 / 80 clipped · 351 free col 145.16 · 80.16 / 80 whole · 349.84 free
SKU chip (first), all four widths col 144 · 100 / 100, whole at the limit col 144 · 100 / 100, whole
drawer 1440, a 493px description col 144 · 79 / 493 clipped · 335 free col 479 (all free width) · 414 / 493 clipped · 0 free
page 1440 · row 1344, same description col 144 · 79 / 493 clipped · 912 free col 558.44 · 493.44 / 493 whole
7 chips in a 767px row (wrap) line 1: Stage, Amount, Owner Email, Region · line 2: Phone, Code, Notes identical lines and widths
same, a long Notes value Notes col 144, clipped Notes col 355.05, whole, same lines
200px / 300px rows wide chip 200 (row) / 256 identical
drawer 1440, edit mode (3 editors) 256 / 256 / 256, third on line 2 256 / 277 / 285, third on line 2

Pins

packages/plugin-detail/src/__tests__/HeaderHighlight.shareRowWidth-11684.test.tsx, 6 cases. It renders the console's path, record:highlights under a record context, with the showcase Product field types.

  • Both chips carrying "QA Widget 1" (SKU and Description) carry grow, max-w-max, basis-[9rem] and min-w-[min(9rem,100%)], and none of the retired max-w-[16rem], max-w-[24rem] or min-w-[7rem].
  • Truncation is kept for a full line: the clip span stays truncate, with the whole value as its title.
  • Every chip, short ones included, carries the grow and the content cap.
  • The row stays flex flex-wrap.
  • A wide display type (email) gets the 16rem floor with the grow and the cap.
  • An editing column switches from the 9rem to the 16rem floor and still grows.

The existing pins that read the basis classes stay green unchanged: RecordHighlightsRenderer.phoneWidth-11659, and expandableFamily.identity-5874's wide/narrow reading. The RecordHighlightsRenderer.percentClip header described the chip as "shrinking toward min-w-[7rem]". It now describes the old fixed column in the past tense.

Ablation (one-shot, from committed state 3923377)

The distribution was reverted to the base class strings with scripts/ablation-replace.mjs in wrap mode, which owns the EXIT/INT/TERM restore on the absolute path. The anchor went x1 to x0, the replacement x0 to x1, and the blob 98d218122a4b to a293d79c18f5. During the mutation the on-disk counts were: max-w-max 1 (the comment only; 2 when restored), the old cap string 1, and min-w-[7rem] 1.

  • Predicted: the four class-contract cases red; the truncate/title and the flex-wrap cases green. Observed: Tests 4 failed | 2 passed (6).
  • Browser, same leg: the Description chip clipped again (79 / 80) at 1280 and 1440. Line structure in the wrapped and edit-mode readings is identical to head.
  • Restore: blob after restore == HEAD (98d218122a4b), and git diff HEAD is empty.
  • Resolution path: the pin imports ../HeaderHighlight and ../renderers/record-highlights relatively, so the subject is src and no dist rebuild sat on the path.

Gates

command tree exit · verdict
pnpm turbo run build --filter='@object-ui/plugin-detail^...' --concurrency=2 (the dependency closure) 3923377 0 · 11 of 11 tasks
pnpm --filter @object-ui/plugin-detail build 3923377 (HeaderHighlight.tsx unchanged since) 0
pnpm --filter @object-ui/plugin-detail type-check (tsc --noEmit && tsc -p tsconfig.test.json; --listFiles shows the test program reads the new pin) 3923377; the test program re-run on the bytes committed as bc8daa3 0 · 0
pnpm --filter @object-ui/plugin-detail lint bc8daa3 0 (0 errors; no warning on a line this diff added)
pnpm exec vitest run packages/plugin-detail/ bc8daa3 0 · files 240 passed, 1 skipped; tests 2391 passed, 8 skipped
pnpm check:new-line-citations · check:control-bytes bc8daa3 0 (0 new citations) · 0
pnpm check:changeset-claims · check:pending-changeset-literals bc8daa3 0 · 0
node scripts/check-changeset-fixed.mjs · check-changeset-no-major.mjs · check-changeset-presence.mjs bc8daa3 0 · 0 · 0 (1 released package, 1 changeset)
pnpm check:test-path-roots · check:unreferenced-sources bc8daa3 0 · 0
node scripts/check-governed-queue-guard.mjs --test over the four paths bc8daa3 0 · NOT GOVERNED

Lint workflow check:* families not listed here are CI's. scripts/pm/dispatch-gates.mjs is objectstack's and cannot derive objectui's families, so these were derived by hand from objectui's package.json for this diff.

Acceptance notes

  • The committed pin is class-shape. A permanent browser-level pin for this geometry would be a Playwright spec beside e2e/record-header-title-width.spec.ts. That would mean real Tailwind compiled from the class strings read out of HeaderHighlight.tsx, and "QA Widget 1" whole in a 767px row. e2e/ is outside this claim's file surface, so I did not add it. The seat decides whether to add it as a follow-up.
  • Font metrics: in this container the SKU chip (first, no left padding) fit "QA Widget 1" exactly (100 / 100px) before the change, while Description clipped. The sweep's host clipped both. After the change both chips have free width to spare.
  • Observation, not a finding: the strip resolves its label from the highlight entry's own label and the i18n bundle, and passes no object-schema label as the fallback. With no bundle, as in the stub harness, the label renders blank. On the real backend the sweep read "SKU" and "Description", and I did not measure a host where this shows. Carrier: none.

Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju


Generated by Claude Code

claude added 2 commits October 6, 2026 13:24
…truncate only when it runs out (objectui#11684)

Each highlight chip was a fixed column (9rem / 16rem basis, no grow, 16rem /
24rem cap), so "QA Widget 1" read "QA Wid..." in the record drawer with most of
the row empty. The basis is now the chip's floor (line breaking still reads
it), `grow` hands the line's free width to the chips, and `max-w-max` caps each
chip at its own content so the free width goes to the chips that still need it.

Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju
Co-authored-by: Claude <noreply@anthropic.com>
…instead of an any cast (objectui#11684)

Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju
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) 3323.9 KB 3330.4 KB
Main entry chunk (gzip) 153.6 KB 350 KB
Entry file index-zShY-Rag.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) 574.72KB 137.94KB
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.75KB 66.62KB
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.18KB 11.71KB
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.72KB 29.10KB
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) 14.51KB 5.15KB
plugin-view (index.js) 90.23KB 22.73KB
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
objectstack-fleet Bot marked this pull request as ready for review October 6, 2026 14:16
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 6, 2026 14:17
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit 7300fca Oct 6, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11684-highlight-share-row branch October 6, 2026 14:34
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.

record drawer: header highlight fields truncate short values ("QA Wid…") with most of the row empty

2 participants