Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.

Commit 4b3d6ed

Browse files
fix(console): pin the Approvals Inbox Approve / Reject column and truncate long record titles (objectui#12816) (#12850)
Fixes #12816 Clause-②: no Implemented by session `https://claude.ai/code/session_011vfKFYJe5mXFiDw6F7CHuD` on head `9f7f390`. ## What was wrong (measured, HotCRM under `objectstack dev --fresh`, Chromium) The record titles are `nowrap`, and the inbox table uses auto layout, which sizes a column by its content's minimum width. A long title ("ACC-000011 - Contoso Pharmaceuticals and Life Sciences Group") therefore held the Record column at 473 px although it is styled `w-[30%]`. The table was 1074 px wide in a 718 px box, and the inline Approve / Reject column sat past the card's right edge. Setting `max-width: 0` on the Record cells alone, in the live page, brought the table to 718 px. ## What changed (`ApprovalsInboxPage.tsx` only) - Record cells are `max-w-0`. The title no longer sizes its column, so it truncates; its full text stays in `title`, and the link is still the whole title. The Record header keeps a `min-w-40` floor so rows stay distinguishable at 1024 px (without it the column falls to 114 px and every row reads "ACC-00…"). - The Approve / Reject column is pinned to the right edge in the header and in every row: `sticky right-0 z-10`, with the shared `PINNED_RIGHT_SEAM_SHADOW_CLASSES`, inside the overflow box's `seam="pinned"` mode. This is how the console's lists pin their Actions column: the shadow sits on the pinned cells' left edge while the table scrolls under them. The box's own edge overlay is no longer drawn. - The pinned cell's fill is the card's (`bg-card`), so a table that does not scroll looks as before. An `::after` layer behind the cell's content redraws the row's hover and keyboard-focus tints on that fill, and the focused row's ring is redrawn on the pinned cell. - Tailwind utilities and theme tokens only; no inline style. `packages/components` is untouched, and the shared overflow box is used as published. The other columns' widths, the filters, the inline approve and reject flows, the dialogs, the waiting-on-others section and every `data-testid` are unchanged. ## Before / after (Chromium, three pending account approvals with long titles) | | table scrollWidth / clientWidth | Record column | Approve / Reject inside the card at rest | Approve / Reject cell | |---|---|---|---|---| | main `5d69b0e`, 1024 | 1074 / 718 | 473 px | no (all 3 rows) | static | | this branch, 1024, light | 762 / 718 | 160 px, titles truncated | yes (all 3 rows) | sticky, seam shadow on | | this branch, 1024, dark | 762 / 718 | 160 px, titles truncated | yes | sticky, seam shadow on | | main `5d69b0e`, 1280 | 1074 / 974 | 473 px | no | static | | this branch, 1280, light | 974 / 974 (no overflow) | 282 px, titles truncated | yes | sticky, seam shadow off | The buttons are still revealed on row hover or focus, as before. The table only shows where they sit. ## Screenshots (evidence branch `claude/issue-12816-evidence`, never merged) - [main, 1024, light (before)](https://github.com/objectstack-ai/objectui/blob/e760383c390964e30e1428bcaa8048203da419d7/docs/design/findings/12816/approvals-inbox-1024-light-main.png?raw=true) - [1024, light](https://github.com/objectstack-ai/objectui/blob/e760383c390964e30e1428bcaa8048203da419d7/docs/design/findings/12816/approvals-inbox-1024-light.png?raw=true): second row hovered, Approve / Reject pinned, seam shadow on - [1024, dark](https://github.com/objectstack-ai/objectui/blob/e760383c390964e30e1428bcaa8048203da419d7/docs/design/findings/12816/approvals-inbox-1024-dark.png?raw=true) - [1280, light (CONTROL)](https://github.com/objectstack-ai/objectui/blob/e760383c390964e30e1428bcaa8048203da419d7/docs/design/findings/12816/approvals-inbox-1280-light.png?raw=true): nothing overflows, titles truncate - [1024, light, keyboard-focused row](https://github.com/objectstack-ai/objectui/blob/e760383c390964e30e1428bcaa8048203da419d7/docs/design/findings/12816/approvals-inbox-1024-light-focused-row.png?raw=true): the ring stays continuous across the pinned cell - [DOM readings](https://github.com/objectstack-ai/objectui/blob/e760383c390964e30e1428bcaa8048203da419d7/docs/design/findings/12816/dom-readings.json?raw=true) ## Deviations - `ApprovalsInboxPage.tableOverflow-12708.test.tsx`: the case that checked the box's edge shadow now checks the box's `data-overflow-right` mark, and that the edge overlay stays off. The ruled `seam="pinned"` moves the shadow to the pinned cells. - The Record header floor (`min-w-40`) is this PR's choice; the reasoning is in the dev report on the card. ## Test evidence (head `9f7f390`) - `pnpm exec vitest run` over all 18 `ApprovalsInboxPage.*.test.tsx` files: exit 0, 92 tests passed. - New pin `ApprovalsInboxPage.pinnedActions-12816.test.tsx` (stubbed 718 px box, 60-character record title): pinned header and row cells with the seam classes; the Record cell `max-w-0` with the full title in `title`; a short-title CONTROL; the focused row's ring on the pinned cell. - Ablations, one per new pin, each restored to the HEAD blob: Record `max-w-0` removed, 1 of 6 red. `sticky right-0` removed, 1 of 6 red. `seam="pinned"` changed to edge, 2 of 6 red. Record floor removed, 1 of 6 red. Focus ring removed, 1 of 6 red. Seam classes removed, 1 of 6 red. - `type-check` and `build` for `@object-ui/console` (after the dependency build): exit 0. `eslint` on the touched files: 0 errors. - `check:i18n-keys`, `check:i18n-drift`, `check:new-line-citations`, `check-changeset-presence`, `check-changeset-no-major`: all exit 0. ## Acceptance notes - On main, the 1280 px inbox also overflowed with these titles (1074 in 974). It no longer does. - Out of scope and unchanged: the served request page (objectui#12482–#12491) and the list columns' labels (objectui#12608). --- _Generated by [Claude Code](https://claude.ai/code/session_011vfKFYJe5mXFiDw6F7CHuD)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e1351a8 commit 4b3d6ed

4 files changed

Lines changed: 279 additions & 12 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@object-ui/console': patch
3+
---
4+
5+
Approvals Inbox: on a narrow screen the inline Approve / Reject column stays pinned to the table's right edge, with a shadow on its left edge while the table scrolls under it, instead of sitting past the card's edge. A long record title now truncates (its full text on hover) instead of widening the table.
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Approvals Inbox — the Approve / Reject column is pinned right and a long
5+
* record title truncates (objectui#12816).
6+
*
7+
* Measured at 1024 px (HotCRM, three account approvals titled like
8+
* "ACC-000011 - Contoso Pharmaceuticals and Life Sciences Group"): the table
9+
* was 1074 px wide in a 718 px box. The titles are `nowrap`, and an
10+
* auto-layout table sizes a column by its content's minimum width, so the
11+
* Record column took 473 px and pushed the inline Approve / Reject column
12+
* past the card's right edge.
13+
*
14+
* - The Record cells are `max-w-0`: the title no longer sizes its column, so
15+
* it truncates, its full text in `title`. The header keeps a floor.
16+
* - The Approve / Reject cells are pinned right in the header and every row,
17+
* with the shared seam shadow (`PINNED_RIGHT_SEAM_SHADOW_CLASSES`), inside
18+
* the overflow box's `seam="pinned"` mode.
19+
*
20+
* jsdom lays nothing out, so the 718 px box is stubbed geometry and these pins
21+
* read the classes that do the layout; the real-browser readings are on the
22+
* pull request.
23+
*/
24+
25+
import '@testing-library/jest-dom/vitest';
26+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
27+
import { render, screen, cleanup, fireEvent, waitFor, act, within } from '@testing-library/react';
28+
import { PINNED_RIGHT_SEAM_SHADOW_CLASSES } from '@object-ui/components';
29+
import { MemoryRouter, Routes, Route } from 'react-router-dom';
30+
31+
const APP = 'com.objectstack.account';
32+
33+
const { adapterFind, approvalsApiStub, ADAPTER, AUTH, I18N } = vi.hoisted(() => {
34+
// The card's own measured title: 60 characters.
35+
const LONG_TITLE = 'ACC-000011 - Contoso Pharmaceuticals and Life Sciences Group';
36+
const rows = [
37+
{
38+
id: 'req_long',
39+
process_name: 'invoice_approval',
40+
process_label: 'Invoice Approval',
41+
object_name: 'showcase_invoice',
42+
object_label: 'Invoice',
43+
record_id: 'acc_long',
44+
record_title: LONG_TITLE,
45+
status: 'pending',
46+
pending_approvers: ['u_1'],
47+
submitter_id: 'u_2',
48+
submitter_name: 'Sam Submitter',
49+
submitted_at: '2026-08-19T00:00:00.000Z',
50+
},
51+
{
52+
id: 'req_short',
53+
process_name: 'invoice_approval',
54+
process_label: 'Invoice Approval',
55+
object_name: 'showcase_invoice',
56+
object_label: 'Invoice',
57+
record_id: 'acc_short',
58+
record_title: 'Short Co',
59+
status: 'pending',
60+
pending_approvers: ['u_1'],
61+
submitter_id: 'u_2',
62+
submitter_name: 'Sam Submitter',
63+
submitted_at: '2026-08-19T00:00:00.000Z',
64+
},
65+
];
66+
67+
// Both records are readable, so both titles are links.
68+
const adapterFind = vi.fn(async (_object: string, params?: Record<string, unknown>) => {
69+
const ids = (params?.$filter as { id?: { $in?: string[] } } | undefined)?.id?.$in ?? [];
70+
return { data: ids.map((id) => ({ id })) };
71+
});
72+
73+
const approvalsApiStub = {
74+
listRequests: vi.fn(async () => ({ data: rows })),
75+
getRequest: vi.fn(async (id: string) => ({ data: rows.find((r) => r.id === id) })),
76+
listActions: vi.fn(async () => ({ data: [] })),
77+
approve: vi.fn(async () => ({ data: rows[0], finalized: true })),
78+
reject: vi.fn(async () => ({ data: rows[0], finalized: true })),
79+
};
80+
81+
// STABLE singletons. A mocked hook that returns a fresh object per render
82+
// gives every render a new `user` / `adapter` identity, which re-runs the
83+
// page's load effect (and the probe's) forever — the page never leaves its
84+
// loading skeleton and every query below times out on an empty table.
85+
const ADAPTER = { find: adapterFind };
86+
const AUTH = { user: { id: 'u_1', email: 'approver@example.com' } };
87+
const I18N = {
88+
t: (key: string, options?: Record<string, unknown>) => String(options?.defaultValue ?? key),
89+
language: 'en',
90+
};
91+
92+
return { adapterFind, approvalsApiStub, rows, ADAPTER, AUTH, I18N };
93+
});
94+
95+
vi.mock('@object-ui/i18n', async (importOriginal) => ({
96+
...(await importOriginal<Record<string, unknown>>()),
97+
useObjectTranslation: () => I18N,
98+
}));
99+
100+
vi.mock('@object-ui/auth', async (importOriginal) => {
101+
const authFetch = vi.fn(async () => new Response('{}', { status: 200 }));
102+
return {
103+
...(await importOriginal<Record<string, unknown>>()),
104+
useAuth: () => AUTH,
105+
createAuthenticatedFetch: () => authFetch,
106+
TokenStorage: { get: () => null },
107+
};
108+
});
109+
110+
vi.mock('@object-ui/app-shell', async (importOriginal) => ({
111+
...(await importOriginal<Record<string, unknown>>()),
112+
useAdapter: () => ADAPTER,
113+
DeclaredActionsBar: () => null,
114+
isViaOverrideRow: () => false,
115+
}));
116+
117+
// `buildApproverIdentities` stays REAL — the "can this approver act" pin is only
118+
// worth anything if the identity resolution under it is the shipped one.
119+
vi.mock('../../services/approvalsApi', async (importOriginal) => ({
120+
...(await importOriginal<Record<string, unknown>>()),
121+
approvalsApi: approvalsApiStub,
122+
}));
123+
124+
// Imported after the mocks so the page picks them up.
125+
import { ApprovalsInboxPage } from './ApprovalsInboxPage';
126+
127+
function renderInbox() {
128+
return render(
129+
<MemoryRouter initialEntries={[`/apps/${APP}/system/approvals`]}>
130+
<Routes>
131+
<Route path="/apps/:appName/system/approvals" element={<ApprovalsInboxPage />} />
132+
</Routes>
133+
</MemoryRouter>,
134+
);
135+
}
136+
137+
function setGeometry(box: HTMLElement, g: { scrollLeft: number; scrollWidth: number; clientWidth: number }) {
138+
Object.defineProperty(box, 'scrollWidth', { configurable: true, value: g.scrollWidth });
139+
Object.defineProperty(box, 'clientWidth', { configurable: true, value: g.clientWidth });
140+
box.scrollLeft = g.scrollLeft;
141+
act(() => { fireEvent.scroll(box); });
142+
}
143+
144+
const LONG = 'ACC-000011 - Contoso Pharmaceuticals and Life Sciences Group';
145+
146+
beforeEach(() => {
147+
for (const fn of Object.values(approvalsApiStub)) fn.mockClear();
148+
adapterFind.mockClear();
149+
});
150+
afterEach(cleanup);
151+
152+
async function renderAt718() {
153+
renderInbox();
154+
await waitFor(() => expect(screen.getByRole('link', { name: LONG })).toBeInTheDocument());
155+
const box = screen.getByTestId('approvals-inbox-table-scroller');
156+
setGeometry(box, { scrollLeft: 0, scrollWidth: 762, clientWidth: 718 });
157+
return box;
158+
}
159+
160+
function expectPinnedRight(cell: Element) {
161+
expect(cell).toHaveClass('sticky', 'right-0', 'z-10', 'bg-card');
162+
expect(cell).toHaveClass(...PINNED_RIGHT_SEAM_SHADOW_CLASSES.split(' '));
163+
}
164+
165+
describe('Approvals Inbox — pinned Approve / Reject, truncating Record (objectui#12816)', () => {
166+
it('pins the Approve / Reject column right, in the header and in every row, with the seam shadow', async () => {
167+
const box = await renderAt718();
168+
// `seam="pinned"`: the pinned cells draw the shadow, so the box's own
169+
// edge overlay stays off while it marks that it can scroll further right.
170+
expect(box).toHaveAttribute('data-overflow-right', 'true');
171+
expect(screen.queryByTestId('horizontal-overflow-shadow')).toBeNull();
172+
173+
const header = within(box).getByRole('columnheader', { name: 'Actions' });
174+
expectPinnedRight(header);
175+
expect(header).toBe(header.parentElement!.lastElementChild);
176+
177+
const rows = within(box).getAllByRole('row').slice(1);
178+
expect(rows).toHaveLength(2);
179+
for (const row of rows) {
180+
const cell = row.lastElementChild!;
181+
expectPinnedRight(cell);
182+
expect(within(cell as HTMLElement).getByRole('button', { name: 'Approve' })).toBeInTheDocument();
183+
expect(within(cell as HTMLElement).getByRole('button', { name: 'Reject' })).toBeInTheDocument();
184+
}
185+
});
186+
187+
it('truncates a 60-character record title instead of letting it size the column', async () => {
188+
const box = await renderAt718();
189+
expect(LONG).toHaveLength(60);
190+
const link = screen.getByRole('link', { name: LONG });
191+
// The link is still the whole title, and carries it in full.
192+
expect(link).toHaveAttribute('title', LONG);
193+
expect(link).toHaveTextContent(LONG);
194+
expect(link.querySelector('span')).toHaveClass('truncate');
195+
// The cell takes the title's unbreakable width out of the table's column
196+
// sizing, so the table does not widen for it; the header keeps a floor.
197+
expect(link.closest('td')).toHaveClass('max-w-0');
198+
expect(within(box).getByRole('columnheader', { name: 'Record' })).toHaveClass('w-[30%]', 'min-w-40');
199+
});
200+
201+
it('CONTROL: a short title renders as before, and the other columns keep their widths', async () => {
202+
const box = await renderAt718();
203+
const link = screen.getByRole('link', { name: 'Short Co' });
204+
expect(link).toHaveAttribute('title', 'Short Co');
205+
expect(link).toHaveAttribute('href', `/apps/${APP}/showcase_invoice/record/acc_short`);
206+
expect(within(box).getByRole('columnheader', { name: 'Request' })).toHaveClass('w-[22%]');
207+
expect(within(box).getByRole('columnheader', { name: 'Requester' })).toHaveClass('w-[16%]');
208+
expect(within(box).getByRole('columnheader', { name: 'Status' })).toHaveClass('w-[110px]');
209+
expect(within(box).getByRole('columnheader', { name: 'Submitted' })).toHaveClass('w-[132px]');
210+
for (const name of ['Request', 'Requester', 'Status', 'Submitted']) {
211+
expect(within(box).getByRole('columnheader', { name })).not.toHaveClass('sticky');
212+
}
213+
});
214+
215+
it('the keyboard-focused row keeps its ring across the pinned cell', async () => {
216+
const box = await renderAt718();
217+
const [first, second] = within(box).getAllByRole('row').slice(1);
218+
act(() => { fireEvent.keyDown(window, { key: 'j' }); });
219+
await waitFor(() => expect(first).toHaveClass('ring-2'));
220+
expect(first.lastElementChild!.className).toContain('shadow-[inset_0_2px_0_var(--color-ring)');
221+
expect(second.lastElementChild!.className).not.toContain('shadow-[');
222+
});
223+
});

‎apps/console/src/pages/system/ApprovalsInboxPage.tableOverflow-12708.test.tsx‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
* Submitted, the inline Approve / Reject cell) is wider than its card: a long
99
* record title holds the Record column open and the columns after it run past
1010
* the card's right edge, with nothing to say the table continues. The table
11-
* now sits in the shared overflow box (`HorizontalOverflowBox`): a right-edge
12-
* shadow while columns remain to the right, none at the right end, and its
13-
* scrollbar on hover.
11+
* now sits in the shared overflow box (`HorizontalOverflowBox`): a shadow while
12+
* columns remain to the right, none at the right end, and its scrollbar on
13+
* hover. Since objectui#12816 that shadow is the pinned Approve / Reject
14+
* cells' seam (`ApprovalsInboxPage.pinnedActions-12816.test.tsx`).
1415
*
1516
* jsdom lays nothing out, so the box's scroll geometry is stubbed, as the
1617
* box's own pins in `@object-ui/components` stub it.
@@ -155,16 +156,20 @@ describe('Approvals Inbox — the wide table shows that it scrolls (objectui#127
155156
expect(table!.parentElement).toHaveClass('overflow-visible');
156157
});
157158

158-
it('draws the right-edge shadow while the table is wider than the box, and none at the right end', async () => {
159+
// objectui#12816 pins the Approve / Reject column right, so the cue moved
160+
// from the box's edge overlay to the pinned cells' seam (`seam="pinned"`):
161+
// the box says it can scroll further right, and no edge overlay is drawn.
162+
it('marks the box while the table is wider than it, and not at the right end', async () => {
159163
renderInbox();
160164
await waitFor(() => expect(screen.getAllByText('Readable Invoice').length).toBeGreaterThan(0));
161165
const box = screen.getByTestId('approvals-inbox-table-scroller');
162-
expect(screen.queryByTestId('horizontal-overflow-shadow')).toBeNull();
166+
expect(box).not.toHaveAttribute('data-overflow-right');
163167

164168
setGeometry(box, { scrollLeft: 0, scrollWidth: 1071, clientWidth: 718 });
165-
expect(screen.getByTestId('horizontal-overflow-shadow')).toBeInTheDocument();
169+
expect(box).toHaveAttribute('data-overflow-right', 'true');
170+
expect(screen.queryByTestId('horizontal-overflow-shadow')).toBeNull();
166171

167172
setGeometry(box, { scrollLeft: 1071 - 718, scrollWidth: 1071, clientWidth: 718 });
168-
expect(screen.queryByTestId('horizontal-overflow-shadow')).toBeNull();
173+
expect(box).not.toHaveAttribute('data-overflow-right');
169174
});
170175
});

‎apps/console/src/pages/system/ApprovalsInboxPage.tsx‎

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import {
7373
SelectValue,
7474
cn,
7575
HorizontalOverflowBox,
76+
PINNED_RIGHT_SEAM_SHADOW_CLASSES,
7677
} from '@object-ui/components';
7778
import { toast } from 'sonner';
7879
import { useAuth } from '@object-ui/auth';
@@ -866,6 +867,18 @@ function RecordCell({ r, href, hiddenKeys, deadLabel, unresolvableLabel }: {
866867
);
867868
}
868869

870+
/**
871+
* objectui#12816 — the inline Approve / Reject column is pinned to the table's
872+
* right edge, the way the console's lists pin their Actions column: inside the
873+
* `seam="pinned"` overflow box the pinned cells draw the shared seam shadow on
874+
* their own left edge while columns scroll under them. The cell's fill is the
875+
* card's, so a table that does not scroll looks as before; the row's hover and
876+
* keyboard-focus tints are layered back over that fill by an `::after` behind
877+
* the cell's content (the sticky cell is its own stacking context, so `-z-10`
878+
* stays inside it).
879+
*/
880+
const PINNED_ACTIONS_CLASSES = `sticky right-0 z-10 bg-card after:pointer-events-none after:absolute after:inset-0 after:-z-10 after:opacity-0 in-[tr:hover]:after:opacity-100 ${PINNED_RIGHT_SEAM_SHADOW_CLASSES}`;
881+
869882
function InlineActions({
870883
r,
871884
actionable,
@@ -2060,7 +2073,9 @@ export function ApprovalsInboxPage() {
20602073
{/* objectui#12708 — wider than the panel, the table scrolls
20612074
in the shared overflow box: a right-edge shadow while
20622075
columns remain to the right, its scrollbar on hover. */}
2063-
<HorizontalOverflowBox outerClassName="rounded-lg overflow-hidden" data-testid="approvals-inbox-table-scroller">
2076+
{/* objectui#12816 — `seam="pinned"`: the shadow moves from
2077+
the box's edge to the pinned Approve / Reject cells. */}
2078+
<HorizontalOverflowBox seam="pinned" outerClassName="rounded-lg overflow-hidden" data-testid="approvals-inbox-table-scroller">
20642079
<Table containerClassName="overflow-visible">
20652080
<TableHeader>
20662081
<TableRow>
@@ -2081,11 +2096,15 @@ export function ApprovalsInboxPage() {
20812096
spreading five auto columns evenly and leaving 审批事项
20822097
over-wide next to a cramped 状态. */}
20832098
<TableHead className="w-[22%]">{tr('colRequest', 'Request')}</TableHead>
2084-
<TableHead className="w-[30%]">{tr('colRecord', 'Record')}</TableHead>
2099+
{/* objectui#12816 — the Record cells are `max-w-0` (below),
2100+
so this floor is the column's only minimum: enough to
2101+
tell one record from the next at 1024 px, however long
2102+
the titles are. */}
2103+
<TableHead className="w-[30%] min-w-40">{tr('colRecord', 'Record')}</TableHead>
20852104
<TableHead className="w-[16%]">{tr('colRequester', 'Requester')}</TableHead>
20862105
<TableHead className="w-[110px] min-w-[96px] whitespace-nowrap">{tr('colStatus', 'Status')}</TableHead>
20872106
<TableHead className="w-[132px]">{tr('colWaiting', 'Submitted')}</TableHead>
2088-
<TableHead className="w-20" aria-label={tr('colActions', 'Actions')} />
2107+
<TableHead className={cn('w-20', PINNED_ACTIONS_CLASSES, 'after:bg-muted/50')} aria-label={tr('colActions', 'Actions')} />
20892108
</TableRow>
20902109
</TableHeader>
20912110
<TableBody>
@@ -2109,7 +2128,11 @@ export function ApprovalsInboxPage() {
21092128
</TableCell>
21102129
)}
21112130
<TableCell><RequestCell r={r} tr={tr} /></TableCell>
2112-
<TableCell>
2131+
{/* objectui#12816 — `max-w-0` takes the record title's
2132+
unbreakable width out of the auto-layout table's
2133+
column sizing, so a long title truncates (full text
2134+
in its `title`) instead of widening the table. */}
2135+
<TableCell className="max-w-0">
21132136
<RecordCell
21142137
r={r}
21152138
// A known-dead reference needs no probe: there is
@@ -2156,7 +2179,18 @@ export function ApprovalsInboxPage() {
21562179
) : null;
21572180
})()}
21582181
</TableCell>
2159-
<TableCell className="w-20">
2182+
<TableCell
2183+
className={cn(
2184+
'w-20',
2185+
PINNED_ACTIONS_CLASSES,
2186+
// The focused row's inset ring is drawn by the row,
2187+
// under this opaque cell: the cell redraws the ring's
2188+
// top, right and bottom edges itself.
2189+
focusIndex === i
2190+
? 'after:bg-accent/30 after:opacity-100 in-[tr:hover]:after:bg-accent/50 shadow-[inset_0_2px_0_var(--color-ring),inset_-2px_0_0_var(--color-ring),inset_0_-2px_0_var(--color-ring)]'
2191+
: 'after:bg-accent/50',
2192+
)}
2193+
>
21602194
<InlineActions
21612195
r={r}
21622196
actionable={isActionable(r)}

0 commit comments

Comments
 (0)