From 3923377e75fbe4a350bde79a8a0b1b5256bebb32 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 13:24:23 +0000 Subject: [PATCH 1/2] fix(plugin-detail): header highlights share the row's free width and 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 --- .changeset/11684-highlight-share-row.md | 7 + .../plugin-detail/src/HeaderHighlight.tsx | 24 ++- ...aderHighlight.shareRowWidth-11684.test.tsx | 146 ++++++++++++++++++ ...ordHighlightsRenderer.percentClip.test.tsx | 5 +- 4 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 .changeset/11684-highlight-share-row.md create mode 100644 packages/plugin-detail/src/__tests__/HeaderHighlight.shareRowWidth-11684.test.tsx diff --git a/.changeset/11684-highlight-share-row.md b/.changeset/11684-highlight-share-row.md new file mode 100644 index 0000000000..8b67b81c8c --- /dev/null +++ b/.changeset/11684-highlight-share-row.md @@ -0,0 +1,7 @@ +--- +'@object-ui/plugin-detail': patch +--- + +**The record header's highlight chips share the row's free width and truncate only when it runs out (objectui#11684).** A Product with SKU "QA Widget 1" read "QA Wid…" in the record drawer's highlight row, with most of the row empty. Every chip in `HeaderHighlight` was a fixed column: a 9rem basis (16rem for wide types and for a column being edited), no grow, and a 16rem / 24rem cap. A value wider than the chip was clipped, however much room the row had left. + +A chip's basis is now its floor, and the chips grow into the line's free width. Each chip is capped at its own content, so a chip that fits stops growing and the rest of the free width goes to the chips that still need it. Which chips share a line does not change, because line breaking still reads the floors. A short value keeps its 9rem column, and a sparse strip still packs left, because no chip is ever wider than what it shows. A value is cut with an ellipsis only once its line has no free width left. The whole value stays available as the hover title. A column being edited still takes the 16rem floor, and it can grow to its editor's natural width. diff --git a/packages/plugin-detail/src/HeaderHighlight.tsx b/packages/plugin-detail/src/HeaderHighlight.tsx index 86089cadcf..98d218122a 100644 --- a/packages/plugin-detail/src/HeaderHighlight.tsx +++ b/packages/plugin-detail/src/HeaderHighlight.tsx @@ -217,12 +217,32 @@ export const HeaderHighlight: React.FC = ({ // expand-on-edit) instead of cramming it into a 9rem column. const useWide = isWide || editorActive; + // How the row's width is shared (objectui#11684). The basis is the + // column's FLOOR, and line breaking still reads it, so which chips + // share a line is what it always was. `grow` then hands the line's + // free width to the columns, and `max-w-max` caps each one at its + // own content: a column that already fits stops growing and the + // rest of the free width goes on to the columns that still need + // it. A value truncates only once its line has no free width left. + // + // The old fixed caps (16rem / 24rem, no grow) cut "QA Widget 1" to + // "QA Wid…" in a 9rem chip with most of the drawer row empty. The + // content cap keeps what those caps were for: no column is ever + // wider than what it shows, so a sparse strip still packs left + // and the hover pencil stays beside its value. + // + // The floor is a min-width as well as the basis, so a short value + // keeps its 9rem column. The `min(…, 100%)` lets a column narrow to + // the row when the row itself is narrower than the floor. return (
diff --git a/packages/plugin-detail/src/__tests__/HeaderHighlight.shareRowWidth-11684.test.tsx b/packages/plugin-detail/src/__tests__/HeaderHighlight.shareRowWidth-11684.test.tsx new file mode 100644 index 0000000000..1520fd398b --- /dev/null +++ b/packages/plugin-detail/src/__tests__/HeaderHighlight.shareRowWidth-11684.test.tsx @@ -0,0 +1,146 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#11684: the header highlights share the row's free width and + * truncate only when it runs out. + * + * The 2026-10-06 console sweep created a Product with SKU "QA Widget 1" and + * read "QA Wid…" in the record drawer's highlights with most of the row empty. + * Every chip was a fixed column: a `basis-[9rem]` with no grow and a 16rem + * cap. A value wider than the chip's ~79px of content was clipped, however much + * of the row was left. + * + * The distribution now has three parts, and each is pinned below: + * - the basis stays the FLOOR, so line breaking (which chips share a line) + * reads what it always read; + * - `grow` hands the line's free width to the chips; + * - `max-w-max` caps each chip at its own content, so a chip that fits stops + * growing and the free width goes on to the chips that still need it. No + * chip is wider than what it shows, which is what kept a sparse strip + * packed left under the old fixed caps. + * + * happy-dom has no layout engine, so these are class-semantics pins, not pixel + * measurements (the same approach as + * `RecordHighlightsRenderer.phoneWidth-11659.test.tsx`). The Chromium reading + * they rest on is in the pull request that landed this file. It was taken in + * the console's own drawer at its default width, before and after. + */ + +import { describe, it, expect } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import * as React from 'react'; +import { InlineEditProvider, RecordContextProvider } from '@object-ui/react'; +import { HeaderHighlight } from '../HeaderHighlight'; +import { RecordHighlightsRenderer } from '../renderers/record-highlights'; + +/** The value the sweep measured, in both chips it was cut in. */ +const SKU = 'QA Widget 1'; + +/** The showcase Product's own field types (`showcase_product`). */ +const productSchema = { + fields: { + name: { type: 'text', label: 'Name' }, + sku: { type: 'text', label: 'SKU' }, + description: { type: 'text', label: 'Description' }, + unit_price: { type: 'currency', label: 'Unit Price' }, + }, +}; + +/** The fixed caps and the shrink floor this card retired. */ +const RETIRED = ['max-w-[16rem]', 'max-w-[24rem]', 'min-w-[7rem]']; + +const renderProductStrip = () => + render( + + + , + ); + +/** The chip column (`group flex flex-col …`) around an element. */ +const chipAround = (el: Element) => el.closest('div.group') as HTMLElement; + +describe('record:highlights shares the row instead of fixing each chip (objectui#11684)', () => { + it('gives every chip carrying the measured SKU a floor, a grow and a content cap', () => { + renderProductStrip(); + + const chips = screen.getAllByText(SKU).map(chipAround); + // SKU and Description: the sweep read "QA Wid…" in both. + expect(chips).toHaveLength(2); + for (const chip of chips) { + expect(chip).toHaveClass('grow', 'max-w-max', 'basis-[9rem]', 'min-w-[min(9rem,100%)]'); + for (const retired of RETIRED) expect(chip).not.toHaveClass(retired); + } + }); + + it('keeps truncation for the case the line DOES run out, with the whole value as the title', () => { + renderProductStrip(); + + for (const text of screen.getAllByText(SKU)) { + const clipBox = chipAround(text).querySelector('span.truncate'); + expect(clipBox).not.toBeNull(); + expect(clipBox).toHaveAttribute('title', SKU); + } + }); + + it('applies the same three parts to a short chip, so no chip keeps a fixed width', () => { + const { container } = renderProductStrip(); + + const chips = Array.from(container.querySelectorAll('div.group')); + expect(chips).toHaveLength(3); + for (const chip of chips) { + expect(chip).toHaveClass('grow', 'max-w-max'); + for (const retired of RETIRED) expect(chip).not.toHaveClass(retired); + } + }); + + it('still lays the chips out on a wrapping flex row', () => { + const { container } = renderProductStrip(); + + const row = container.querySelector('div.group')?.parentElement; + expect(row).toHaveClass('flex', 'flex-wrap'); + }); +}); + +describe('the wide floor keeps its role under the shared row (objectui#11684)', () => { + const fields = [ + { name: 'owner', label: 'Owner', type: 'text' }, + { name: 'email', label: 'Email', type: 'email' }, + ] as any; + const data = { owner: 'Alice', email: 'ada@example.com' }; + + it('gives a wide display type the 16rem floor, the grow and the content cap', () => { + render(); + + const chip = chipAround(screen.getByText('ada@example.com')); + expect(chip).toHaveClass('grow', 'max-w-max', 'basis-[16rem]', 'min-w-[min(16rem,100%)]'); + expect(chip).not.toHaveClass('basis-[9rem]'); + for (const retired of RETIRED) expect(chip).not.toHaveClass(retired); + }); + + it('widens an editing column to the 16rem floor, which still grows (expand-on-edit)', () => { + render( + + + , + ); + const readChip = chipAround(screen.getByText('Alice')); + expect(readChip).toHaveClass('basis-[9rem]', 'min-w-[min(9rem,100%)]'); + + fireEvent.doubleClick(screen.getByText('Alice')); + + const editChip = chipAround(screen.getByDisplayValue('Alice')); + expect(editChip).toHaveClass('grow', 'max-w-max', 'basis-[16rem]', 'min-w-[min(16rem,100%)]'); + expect(editChip).not.toHaveClass('basis-[9rem]'); + }); +}); diff --git a/packages/plugin-detail/src/__tests__/RecordHighlightsRenderer.percentClip.test.tsx b/packages/plugin-detail/src/__tests__/RecordHighlightsRenderer.percentClip.test.tsx index faf380f823..4daa550550 100644 --- a/packages/plugin-detail/src/__tests__/RecordHighlightsRenderer.percentClip.test.tsx +++ b/packages/plugin-detail/src/__tests__/RecordHighlightsRenderer.percentClip.test.tsx @@ -8,8 +8,9 @@ /** * objectstack#5066 — a `percent` chip in `record:highlights` showed a smaller, - * plausible number. The chip clips with `truncate` (`basis-[9rem]`, shrinking - * toward `min-w-[7rem]` as more chips are added, so ~72-104px of content), and + * plausible number. The chip clips with `truncate` (then a fixed `basis-[9rem]` + * column, ~72-104px of content; since objectui#11684 a chip grows into its + * line's free width, but a full line still clips it), and * the percent display renderer put a `w-16 shrink-0` bar in front of a * shrinkable value span: bar (64) + gap (8) left the value 0-32px and `truncate` * removed the rest. Downstream a stored `33.33` was read off the screen as `3` From bc8daa3b7e0b905268492eb1c014eb441b8b329d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 13:45:13 +0000 Subject: [PATCH 2/2] test(plugin-detail): type the wide-floor fixture as HighlightField[] instead of an any cast (objectui#11684) Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju Co-authored-by: Claude --- .../__tests__/HeaderHighlight.shareRowWidth-11684.test.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/plugin-detail/src/__tests__/HeaderHighlight.shareRowWidth-11684.test.tsx b/packages/plugin-detail/src/__tests__/HeaderHighlight.shareRowWidth-11684.test.tsx index 1520fd398b..16c89f7f41 100644 --- a/packages/plugin-detail/src/__tests__/HeaderHighlight.shareRowWidth-11684.test.tsx +++ b/packages/plugin-detail/src/__tests__/HeaderHighlight.shareRowWidth-11684.test.tsx @@ -35,6 +35,7 @@ import { describe, it, expect } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/react'; import * as React from 'react'; +import type { HighlightField } from '@object-ui/types'; import { InlineEditProvider, RecordContextProvider } from '@object-ui/react'; import { HeaderHighlight } from '../HeaderHighlight'; import { RecordHighlightsRenderer } from '../renderers/record-highlights'; @@ -113,10 +114,10 @@ describe('record:highlights shares the row instead of fixing each chip (objectui }); describe('the wide floor keeps its role under the shared row (objectui#11684)', () => { - const fields = [ + const fields: HighlightField[] = [ { name: 'owner', label: 'Owner', type: 'text' }, { name: 'email', label: 'Email', type: 'email' }, - ] as any; + ]; const data = { owner: 'Alice', email: 'ada@example.com' }; it('gives a wide display type the 16rem floor, the grow and the content cap', () => {