diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 2eaa21ee0e..f464bb0709 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -2331,6 +2331,10 @@ not a replacement status card, CTA band, or page-local nested card. `platformTableBandWidthStyle` or the sortable header's `bandWidth`, which sets `--platform-table-band-width`; the shell applies it only inside the band, after the name anchor, so md widths and phone widths are unchanged. + A header whose full label outgrows its band column (Kubernetes + Configuration's `Lifecycle / trust`) passes `compactInBand` to + `PlatformResponsiveTableLabel`, which keeps the compact label it already + shows on phones through the band; other headers switch at 34rem as before. A column whose content must read in full (a status badge) is sized for its widest label at the narrowest table in each range: a 34rem container for the band, and the table a 768px viewport leaves (about 695px) for md widths. The diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 903945b163..31e4da09fa 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -6034,6 +6034,10 @@ than the unified online/offline status, a running container's health check gets its own row (Failing, Starting, Passing), and Uptime is the current run, so a stopped container shows neither a stale health check nor leftover uptime. Both surfaces read `dockerContainerStatePresentation.ts`. +Below a 720px table the containers table has no Restarts column (its header +cannot fit beside State, the metric bars and the Update control), so a running +container that restarted more than five times says `N restarts` in its State +cell there instead; wider tables keep the count in its own column. ResourceDetailDrawer headings wrap long display names rather than ellipsising them. Overview rows may still truncate identity; keyboard expansion exposes the diff --git a/frontend-modern/browser-verification/577e3eebd7bea032.json b/frontend-modern/browser-verification/577e3eebd7bea032.json new file mode 100644 index 0000000000..40ebbf111f --- /dev/null +++ b/frontend-modern/browser-verification/577e3eebd7bea032.json @@ -0,0 +1,87 @@ +{ + "version": 1, + "base_sha": "ce3262f4230aedc10acbd38dfb0482e6f0bbb298", + "verified_at": "2026-10-06T08:14:26Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/features/docker/DockerContainersTable.tsx", + "frontend-modern/src/features/docker/dockerContainerStatePresentation.ts", + "frontend-modern/src/features/docker/dockerContainerTableModel.ts", + "frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx", + "frontend-modern/src/features/platformPage/sharedPlatformPage.tsx", + "frontend-modern/src/index.css" + ], + "content_sha256": { + "frontend-modern/src/features/docker/DockerContainersTable.tsx": "575efffccfc2285e11d497eaae3de3a7dd0dbbb1ba4c02a6d4adbc296b02ab2e", + "frontend-modern/src/features/docker/dockerContainerStatePresentation.ts": "7316bb3852c75bb135025ba245e7a55d3014b9b5581d7fe0410b4fb9be6cdea7", + "frontend-modern/src/features/docker/dockerContainerTableModel.ts": "0fac403071fbf75093e0d32425863c599471c7424ecb89f6d7ca91f3ea8930ae", + "frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx": "6f59b8f00077029fd2a19298bb72ec7654c06b0600533ed91d84592d63278b18", + "frontend-modern/src/features/platformPage/sharedPlatformPage.tsx": "717decd0b40532f47653d17cd13fe119acf4559aeb831a0b7515773259829807", + "frontend-modern/src/index.css": "4884f5bed3239c2e06ddd2415a0b0c63eedd1ada8bee84d9e1d3ff534c65fe74" + }, + "routes": [ + "/docker/containers", + "/kubernetes/configuration" + ], + "viewports": [ + { + "width": 390, + "height": 1000 + }, + { + "width": 430, + "height": 1000 + }, + { + "width": 600, + "height": 1000 + }, + { + "width": 650, + "height": 1000 + }, + { + "width": 700, + "height": 1000 + }, + { + "width": 767, + "height": 1000 + }, + { + "width": 800, + "height": 1000 + }, + { + "width": 1000, + "height": 1000 + }, + { + "width": 1280, + "height": 1000 + }, + { + "width": 1440, + "height": 1000 + }, + { + "width": 1600, + "height": 1000 + }, + { + "width": 1920, + "height": 1000 + } + ], + "states": [ + "Docker containers header sweep 390-1920 against main: main clipped the Restarts header at 600 (47px, RES...) and 800 (70px); the change has no clipped header at any width and no column's cell clips grew", + "Below a 720px table Restarts moves to the row drawer as it already did on phones; at 800 it returns with 78px and its header whole", + "A running, healthy container with 7 restarts (API response edited): at 600 its State cell reads 7 restarts in amber (95px, not clipped); at 1440 State reads Healthy and the Restarts column shows the count", + "Kubernetes Configuration at 600/650/700/767: main clipped the Lifecycle / trust header to LIFECYCLE / ...; the change shows its compact Status label through the tablet band, with no header clip, and the full label from md widths up", + "Re-checked after rebasing onto main with the Docker drawer health change (#2534): no clipped headers at 600/800/1440, the 7 restarts State warning at 600, the Kubernetes Lifecycle header at 700, and the drawer Running / Health check Failing rows unchanged" + ], + "interactions": [ + "Measured header and cell clips on main (reverse-applied patch) and on the change", + "Served an edited /api/resources response with the websocket held" + ] +} diff --git a/frontend-modern/src/__tests__/App.architecture.test.ts b/frontend-modern/src/__tests__/App.architecture.test.ts index 56e86857ad..e6ca56d6dd 100644 --- a/frontend-modern/src/__tests__/App.architecture.test.ts +++ b/frontend-modern/src/__tests__/App.architecture.test.ts @@ -302,6 +302,18 @@ describe('App architecture', () => { expect(band).toMatch( /width: 30%;\s*\}[\s\S]*?th\[style\*='--platform-table-band-width'\] \{\s*width: var\(--platform-table-band-width\);/, ); + // A header whose full label outgrows its band column keeps its compact + // label there; these rules sit after the 34rem label switch so they win. + expect(band).toMatch( + /--platform-table-band-width\);\s*\}[\s\S]*?\.platform-table-label-compact\.platform-table-label-band-compact \{\s*display: inline;\s*\}\s*\.platform-table-label-full\.platform-table-label-band-compact \{\s*display: none;/, + ); + const labelSwitch = appStylesSource.indexOf( + '@container (min-width: 34rem) {\n .platform-table-label-compact', + ); + expect(labelSwitch).toBeGreaterThan(0); + expect(appStylesSource.indexOf('.platform-table-label-band-compact')).toBeGreaterThan( + labelSwitch, + ); }); it('draws the shared select arrow that appearance-none removes', () => { diff --git a/frontend-modern/src/features/docker/DockerContainersTable.tsx b/frontend-modern/src/features/docker/DockerContainersTable.tsx index ea4640f497..01ec130c58 100644 --- a/frontend-modern/src/features/docker/DockerContainersTable.tsx +++ b/frontend-modern/src/features/docker/DockerContainersTable.tsx @@ -82,6 +82,7 @@ import { getDockerContainerSortKey, getDockerContainerTableMinWidthClass, getDockerContainerVisibleColumnsForLayout, + isDockerContainerColumnInLayout, type DockerContainerSortKey, type DockerContainerTableColumn, } from './dockerContainerTableModel'; @@ -91,6 +92,7 @@ import { DockerContainerLifecycleControls } from './DockerContainerLifecycleCont import { CONTAINER_CPU_CAPACITY_DESCRIPTION } from './dockerCpuPresentation'; import { DockerImageReferenceText } from './DockerImageReferenceText'; import { + DOCKER_RESTART_ATTENTION_THRESHOLD, getDockerContainerStatePresentation, getDockerContainerUptimeSeconds, type DockerContainerStateTone, @@ -188,10 +190,6 @@ const DOCKER_CONTAINER_STATE_TONE_CLASS: Record { const update = resource.docker?.updateStatus; if (!update) return '—'; @@ -424,6 +422,13 @@ export const DockerContainersTable: Component = (pro // User-controlled sorting layered over the attention-first default: rows // are pre-sorted by the status compare, so a user sort keeps that order for // ties and the table falls straight back to it when the sort is cleared. + // Where the layout has no Restarts column, a crash-looping container says + // so in its State cell instead. + const flagRestartsInState = createMemo( + () => !isDockerContainerColumnInLayout('restarts', layoutMode()), + ); + const stateFor = (resource: Resource) => + getDockerContainerStatePresentation(resource, { flagRestarts: flagRestartsInState() }); // Sorting reorders rows only; grouped mode re-buckets the sorted rows, so // the sort applies within each host group and stays orthogonal to grouping. const sort = createPlatformTableSortState({ @@ -432,7 +437,10 @@ export const DockerContainersTable: Component = (pro descendingFirst: ['cpu', 'memory', 'restarts', 'uptime'], }); const sortedRows = createMemo(() => - sort.sortRows([...scopedRows()].sort(compareDockerContainers), getDockerContainerSortValue), + sort.sortRows([...scopedRows()].sort(compareDockerContainers), (resource, key) => + // State sorts by the words the row shows, restart warning included. + key === 'state' ? stateFor(resource).label : getDockerContainerSortValue(resource, key), + ), ); // Grouped-by-host view, mirroring the workloads table: preference persists // locally (not in the URL) and only applies once the fleet spans more than @@ -510,7 +518,7 @@ export const DockerContainersTable: Component = (pro // A running container with a failing or starting health check needs the // State column as much as a stopped one does. const showStateColumn = createMemo(() => - scopedRows().some((resource) => getDockerContainerStatePresentation(resource).tone !== null), + scopedRows().some((resource) => stateFor(resource).tone !== null), ); const showUptimeColumn = createMemo(() => scopedRows().some((resource) => getDockerContainerUptimeSeconds(resource) !== undefined), @@ -536,7 +544,7 @@ export const DockerContainersTable: Component = (pro const renderContainerRow = (resource: Resource): JSX.Element => { const indicator = mapDockerContainerStatus(resource); const image = () => dockerTextValue(resource.docker?.image); - const state = () => getDockerContainerStatePresentation(resource); + const state = () => stateFor(resource); const runtime = () => runtimeSummary(resource); const host = () => dockerHostName(resource); const running = () => isContainerRunning(resource); @@ -659,7 +667,7 @@ export const DockerContainersTable: Component = (pro > RESTART_ATTENTION_THRESHOLD + restartCount() > DOCKER_RESTART_ATTENTION_THRESHOLD ? 'font-medium text-red-600 dark:text-red-400' : '' }`} diff --git a/frontend-modern/src/features/docker/__tests__/DockerNativeTables.test.tsx b/frontend-modern/src/features/docker/__tests__/DockerNativeTables.test.tsx index b3ebeb988e..3dfeb4b59e 100644 --- a/frontend-modern/src/features/docker/__tests__/DockerNativeTables.test.tsx +++ b/frontend-modern/src/features/docker/__tests__/DockerNativeTables.test.tsx @@ -965,6 +965,72 @@ describe('Docker native tables', () => { expect(screen.getByText('Exited (1)')).toHaveAttribute('data-docker-container-state', 'danger'); }); + it('flags a crash-looping container in State where the Restarts column is hidden', () => { + const rows = [ + makeResource({ + id: 'container-1', + type: 'app-container', + name: 'edge-web', + status: 'running', + docker: { hostname: 'edge-01', containerState: 'running', restartCount: 7 }, + }), + makeResource({ + id: 'container-2', + type: 'app-container', + name: 'edge-api', + status: 'running', + docker: { hostname: 'edge-01', containerState: 'running', restartCount: 1 }, + }), + ]; + // 600px is the mobile layout, which has no Restarts column. + setViewportWidth(600); + const { unmount } = renderInRouter(() => ( + } + emptyTitle="No containers" + emptyDescription="No containers" + showToolbar={false} + /> + )); + expect(screen.queryByText('Restarts')).not.toBeInTheDocument(); + expect(screen.getByText('7 restarts')).toHaveAttribute( + 'data-docker-container-state', + 'warning', + ); + // State sorts by the words the row shows: "7 restarts" ranks apart from + // "Running" both ways (with equal labels both orders would be the same). + const rowOrder = () => + [...document.querySelectorAll('[data-docker-container-row]')].map((row) => + row.getAttribute('data-docker-container-row'), + ); + const clickState = () => { + const header = screen + .getAllByRole('columnheader') + .find((th) => th.textContent?.trim().startsWith('State')); + fireEvent.click(header!.querySelector('button') ?? header!); + }; + clickState(); + expect(rowOrder()).toEqual(['container-1', 'container-2']); + clickState(); + expect(rowOrder()).toEqual(['container-2', 'container-1']); + unmount(); + + // A wide table shows the count in its own column instead. + setViewportWidth(WORKLOAD_TABLE_WIDE_LAYOUT_WIDTH); + renderInRouter(() => ( + } + emptyTitle="No containers" + emptyDescription="No containers" + showToolbar={false} + /> + )); + expect(screen.getByText('Restarts')).toBeInTheDocument(); + expect(screen.queryByText('7 restarts')).not.toBeInTheDocument(); + }); + it('leaves out the Uptime column when no container reports one', () => { setViewportWidth(WORKLOAD_TABLE_WIDE_LAYOUT_WIDTH); diff --git a/frontend-modern/src/features/docker/__tests__/dockerContainerStatePresentation.test.ts b/frontend-modern/src/features/docker/__tests__/dockerContainerStatePresentation.test.ts index e8c95eb33b..15334326a2 100644 --- a/frontend-modern/src/features/docker/__tests__/dockerContainerStatePresentation.test.ts +++ b/frontend-modern/src/features/docker/__tests__/dockerContainerStatePresentation.test.ts @@ -158,3 +158,30 @@ describe('drawer runtime rows', () => { expect(getDockerContainerHealthCheckPresentation(crashed)).toBeUndefined(); }); }); + +describe('restart warning where the Restarts column is hidden', () => { + it('names a crash-looping running container in State only when asked', () => { + const looping = container({ containerState: 'running', health: 'healthy', restartCount: 7 }); + expect(getDockerContainerStatePresentation(looping, { flagRestarts: true })).toEqual({ + label: '7 restarts', + title: 'Running, but Docker has restarted it 7 times', + tone: 'warning', + }); + expect(getDockerContainerStatePresentation(looping)).toMatchObject({ label: 'Healthy' }); + expect( + getDockerContainerStatePresentation( + container({ containerState: 'running', restartCount: 5 }), + { flagRestarts: true }, + ), + ).toMatchObject({ label: 'Running', tone: null }); + }); + + it('keeps a worse state ahead of the restart count', () => { + expect( + getDockerContainerStatePresentation( + container({ containerState: 'running', health: 'unhealthy', restartCount: 9 }), + { flagRestarts: true }, + ), + ).toMatchObject({ label: 'Unhealthy', tone: 'danger' }); + }); +}); diff --git a/frontend-modern/src/features/docker/__tests__/dockerContainerTableModel.test.ts b/frontend-modern/src/features/docker/__tests__/dockerContainerTableModel.test.ts index 9514086653..bd0b3c44da 100644 --- a/frontend-modern/src/features/docker/__tests__/dockerContainerTableModel.test.ts +++ b/frontend-modern/src/features/docker/__tests__/dockerContainerTableModel.test.ts @@ -57,7 +57,8 @@ describe('dockerContainerTableModel', () => { const columns = getDockerContainerVisibleColumnsForLayout('mobile', true, true, true); const ids = columns.map((column) => column.id); - expect(ids).toEqual(['container', 'state', 'cpu', 'memory', 'restarts', 'updates', 'actions']); + // Restarts waits for the tablet layout (its header clips below 720px). + expect(ids).toEqual(['container', 'state', 'cpu', 'memory', 'updates', 'actions']); expect(getDockerContainerTableMinWidthClass()).toBe('min-w-full'); expect(getDockerContainerColumnWidthStyle('container', 'mobile', ids)).toEqual({ width: '30%', @@ -65,10 +66,10 @@ describe('dockerContainerTableModel', () => { // State spells out a problem ("Exited (139)", about 77px with padding), so // it takes the room the memory bar did not need. expect(getDockerContainerColumnWidthStyle('state', 'mobile', ids)).toEqual({ - width: '14.5%', + width: '16.371%', }); expect(getDockerContainerColumnWidthStyle('memory', 'mobile', ids)).toEqual({ - width: '13.5%', + width: '15.2419%', }); }); @@ -120,6 +121,23 @@ describe('dockerContainerTableModel', () => { } }); + it('gives the Restarts header room on tablets when one host or groups hide Host', () => { + // "Restarts" is about 61px at 11px uppercase with 6px of padding a side. + for (const hostOption of [{ groupedByHost: true }, { singleHost: true }]) { + const ids = getDockerContainerVisibleColumnsForLayout( + 'tablet', + false, + true, + true, + hostOption, + ).map((column) => column.id); + const width = Number.parseFloat( + String(getDockerContainerColumnWidthStyle('restarts', 'tablet', ids).width), + ); + expect((width / 100) * 720).toBeGreaterThanOrEqual(73); + } + }); + it('drops uptime when no container in view reports one', () => { const ids = getDockerContainerVisibleColumnsForLayout('compact', true, true, true, { includeUptime: false, diff --git a/frontend-modern/src/features/docker/dockerContainerStatePresentation.ts b/frontend-modern/src/features/docker/dockerContainerStatePresentation.ts index 7aa1cef255..69c63e00df 100644 --- a/frontend-modern/src/features/docker/dockerContainerStatePresentation.ts +++ b/frontend-modern/src/features/docker/dockerContainerStatePresentation.ts @@ -48,11 +48,31 @@ const runStateTitle = (resource: Resource): string | undefined => { return undefined; }; +// v5 flagged crash-loopers in the restarts column; a container that restarted +// more than this many times needs an operator's eye even while "running". +export const DOCKER_RESTART_ATTENTION_THRESHOLD = 5; + +// flagRestarts: the row shows no Restarts column (narrow layouts), so a +// running container past the restart threshold says so in its State cell. export const getDockerContainerStatePresentation = ( resource: Resource, + options: { flagRestarts?: boolean } = {}, ): DockerContainerStatePresentation => { const status = mapDockerContainerStatus(resource); const tone = presentationTone(status.variant); + const restarts = resource.docker?.restartCount; + if ( + options.flagRestarts && + tone === null && + typeof restarts === 'number' && + restarts > DOCKER_RESTART_ATTENTION_THRESHOLD + ) { + return { + label: `${restarts} restarts`, + title: `Running, but Docker has restarted it ${restarts} times`, + tone: 'warning', + }; + } return { label: status.label, title: containerStateTitle(resource, status.label), tone }; }; diff --git a/frontend-modern/src/features/docker/dockerContainerTableModel.ts b/frontend-modern/src/features/docker/dockerContainerTableModel.ts index 133b7d9633..e47de843e7 100644 --- a/frontend-modern/src/features/docker/dockerContainerTableModel.ts +++ b/frontend-modern/src/features/docker/dockerContainerTableModel.ts @@ -72,9 +72,10 @@ const DOCKER_CONTAINER_COLUMN_MIN_LAYOUT: Record< state: 'narrow', cpu: 'narrow', memory: 'narrow', - // A phone row cannot fit the Restarts header beside the Update control; - // the count stays in the row expansion there and returns at mobile width. - restarts: 'mobile', + // Below a 720px table the Restarts header cannot fit beside State, the + // metric bars and the Update control without clipping one of them; the + // count stays in the row expansion there and returns at tablet width. + restarts: 'tablet', // Uptime is the current run, so a short one shows a container that just // restarted. Its header needs about 57px, which a tablet row cannot spare // beside State, Restarts and the Update control; the drawer keeps it there. @@ -160,7 +161,6 @@ const DOCKER_CONTAINER_RESPONSIVE_WIDTHS: Record< state: 14.5, cpu: 10, memory: 13.5, - restarts: 8, updates: 16.25, actions: 7.75, }, @@ -169,8 +169,8 @@ const DOCKER_CONTAINER_RESPONSIVE_WIDTHS: Record< host: 15, state: 14, cpu: 9.5, - memory: 13, - restarts: 9, + memory: 12, + restarts: 10, updates: 14.5, actions: 6, }, @@ -193,6 +193,15 @@ const DOCKER_CONTAINER_RESPONSIVE_WIDTHS: Record< }, }; +// Whether a layout has room for a column at all (before row-set gates such +// as "only when some container restarted"). +export const isDockerContainerColumnInLayout = ( + columnId: DockerContainerTableColumnId, + layoutMode: WorkloadTableLayoutMode, +): boolean => + DOCKER_CONTAINER_TABLE_LAYOUT_ORDER[DOCKER_CONTAINER_COLUMN_MIN_LAYOUT[columnId]] <= + DOCKER_CONTAINER_TABLE_LAYOUT_ORDER[layoutMode]; + export const getDockerContainerVisibleColumnsForLayout = ( layoutMode: WorkloadTableLayoutMode, includeRuntime: boolean, diff --git a/frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx index a4cea17020..eb407810ac 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx @@ -297,7 +297,11 @@ export const KubernetesConfigTable: Component<{ class="platform-table-phone-hidden md:w-[17%]" bandWidth={16} > - + { } }); + it('keeps long tablet headers from clipping', () => { + // "Lifecycle / trust" outgrows its tablet-band column, so it keeps the + // compact label it shows on phones through that band. + expect(kubernetesConfigTableSource).toMatch( + /compact="Status"\s+full="Lifecycle \/ trust"\s+compactInBand/, + ); + // The Docker Restarts header cannot fit below a 720px table; the count + // waits in the drawer there and a crash-looper says so in State. + expect(dockerContainerTableModelSource).toMatch(/restarts: 'tablet',/); + }); + it('keeps phone-priority visibility container-led and symmetric across table rows', () => { expect(indexCssSource).toContain(':is(th, td).platform-table-phone-hidden'); expect(indexCssSource).toContain(':is(th, td).platform-table-phone-only'); diff --git a/frontend-modern/src/features/platformPage/__tests__/sharedPlatformPage.test.ts b/frontend-modern/src/features/platformPage/__tests__/sharedPlatformPage.test.ts index d7b2822f11..ab6e8b4f7d 100644 --- a/frontend-modern/src/features/platformPage/__tests__/sharedPlatformPage.test.ts +++ b/frontend-modern/src/features/platformPage/__tests__/sharedPlatformPage.test.ts @@ -38,6 +38,7 @@ import { withPlatformStatusCounts, type PlatformResourceStatusFilter, platformTableBandWidthStyle, + PlatformResponsiveTableLabel, } from '../sharedPlatformPage'; afterEach(() => { @@ -924,4 +925,23 @@ describe('PlatformTableMetricFallback', () => { expect(platformTableBandWidthStyle(9.5)).toEqual({ '--platform-table-band-width': '9.5%' }); expect(platformTableBandWidthStyle(undefined)).toBeUndefined(); }); + + it('lets a long header keep its compact label through the tablet band', () => { + render(() => + createComponent(PlatformResponsiveTableLabel, { + compact: 'Status', + full: 'Lifecycle / trust', + compactInBand: true, + }), + ); + expect(screen.getByText('Status').className).toBe( + 'platform-table-label-compact platform-table-label-band-compact', + ); + expect(screen.getByText('Lifecycle / trust').className).toBe( + 'platform-table-label-full platform-table-label-band-compact', + ); + cleanup(); + render(() => createComponent(PlatformResponsiveTableLabel, { compact: 'Rdy', full: 'Ready' })); + expect(screen.getByText('Ready').className).toBe('platform-table-label-full'); + }); }); diff --git a/frontend-modern/src/features/platformPage/sharedPlatformPage.tsx b/frontend-modern/src/features/platformPage/sharedPlatformPage.tsx index 2244444b24..33007878a7 100644 --- a/frontend-modern/src/features/platformPage/sharedPlatformPage.tsx +++ b/frontend-modern/src/features/platformPage/sharedPlatformPage.tsx @@ -462,11 +462,18 @@ export const getPlatformTableCellClassForKind = (kind: PlatformTableColumnKind): // can expand without weakening the overview-table contract. export const getPlatformTableRowClass = (): string => PLATFORM_TABLE_SUMMARY_ROW_CLASS; -export function PlatformResponsiveTableLabel(props: { compact: string; full: string }) { +// compactInBand keeps the compact label through the tablet band (a 34rem +// container below a 768px viewport) for a full label longer than its column. +export function PlatformResponsiveTableLabel(props: { + compact: string; + full: string; + compactInBand?: boolean; +}) { + const band = () => (props.compactInBand ? ' platform-table-label-band-compact' : ''); return ( <> - {props.compact} - {props.full} + {props.compact} + {props.full} ); } diff --git a/frontend-modern/src/index.css b/frontend-modern/src/index.css index 310b3dbf35..a9fa1b479b 100644 --- a/frontend-modern/src/index.css +++ b/frontend-modern/src/index.css @@ -1489,6 +1489,16 @@ .table-scroll-shell > .table-fixed.platform-table th[style*='--platform-table-band-width'] { width: var(--platform-table-band-width); } + + /* A header whose full label is longer than its band column keeps the + compact label it already shows on phones (compactInBand). */ + .platform-table-label-compact.platform-table-label-band-compact { + display: inline; + } + + .platform-table-label-full.platform-table-label-band-compact { + display: none; + } } }