fix(frontend): align desktop drawer detail cards

This commit is contained in:
Pulse Test
2026-08-29 16:37:05 +01:00
parent 974f102333
commit ed5de5d144
5 changed files with 95 additions and 38 deletions
@@ -3868,12 +3868,17 @@ interactive provider-support content belongs behind
boundary. `DetailSectionTable` keeps the single bordered table at narrow widths
where density matters, then presents those same canonical rows as bounded
section cards on desktop. Desktop cards share the available row width, stretch
to the same row height, balance five- and six-section drawers across three-card
rows, and use a bounded local label column with left-aligned values so the
layout has no ragged fixed-width island, stranded full-width final card, or
full-drawer scan distance. Unified-resource technical summaries are part of
this boundary and must not retain a full-width local table on desktop. The
responsive presentation stays owned by the shared primitive;
to the same row height, and use stable three- or four-column tracks. An
incomplete final row must fill those same tracks with explicit integral spans;
it must not independently flex-grow every remaining card into unrelated column
edges. Five- and six-section drawers remain balanced across three-column rows,
while seven-section drawers use a two-column span for the first card in the
final row so the remaining cards keep the four-column alignment. Cards use a
bounded local label column with left-aligned values so the layout has no ragged
fixed-width island, stranded full-width final card, or full-drawer scan
distance. Unified-resource technical summaries are part of this boundary and
must not retain a full-width local table on desktop. The responsive
presentation stays owned by the shared primitive;
provider drawers must not fork their own desktop card renderers. Monitoring
Optional detail-row progress is also owned by that shared presentation: the
value text remains visible, `DetailSectionTable` composes `ProgressBar` for the
+13 -20
View File
@@ -1,22 +1,19 @@
{
"version": 1,
"base_sha": "0df9747bd7ef97d768d5320e1952116ad4e675ea",
"verified_at": "2026-08-29T13:46:17Z",
"base_sha": "974f102333d5e394ce6ddf966ad2759d1888386f",
"verified_at": "2026-08-29T15:34:14Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/api/settings.ts"
"frontend-modern/src/components/shared/DetailSectionTable.tsx"
],
"content_sha256": {
"frontend-modern/src/api/settings.ts": "6b3b67e14054d6b93a0eeb92107426384b4d2badeec7c52798b7c00cf6f5e920"
"frontend-modern/src/components/shared/DetailSectionTable.tsx": "9164c52fe67c6e6504a961a48e562479613d498d8d12ae10411d5b193dba36b5"
},
"routes": [
"/settings/system-general",
"/docs/PRIVACY"
],
"routes": ["/proxmox/overview"],
"viewports": [
{
"width": 1280,
"height": 800
"width": 1920,
"height": 900
},
{
"width": 390,
@@ -24,17 +21,13 @@
}
],
"states": [
"authenticated General settings with outbound telemetry disabled by PULSE_TELEMETRY=false",
"schema-14 heartbeat payload preview expanded at desktop width",
"schema-14 heartbeat payload preview expanded without document overflow at narrow width",
"rotating pseudonymous install ID refreshed after operator confirmation",
"shipped privacy document opened from the telemetry disclosure"
"mock-backed Proxmox Overview with a seven-section node drawer expanded",
"desktop node details aligned to four shared column tracks across both rows",
"compact mobile node details retained as one full-width bordered table"
],
"interactions": [
"opened General settings at 1280x800 and selected Preview payload",
"confirmed schema_version 14 and representative alert severity, resolution-time, and persistence-health aggregates in the rendered payload",
"resized the current interaction to 390x844, confirmed the payload stayed visible, and verified no horizontal document overflow",
"selected Reset ID at narrow width, accepted the confirmation dialog, and confirmed the rendered install ID rotated",
"opened Full details and confirmed the shipped /docs/PRIVACY document rendered in a browser popup"
"opened Proxmox Overview at 1920x900, expanded Analytics A, and confirmed the four first-row cards share equal quarter-width tracks",
"confirmed Network spans two desktop tracks while Storage and Telemetry each span one, filling the final row with aligned card edges",
"repeated the expanded drawer at 390x844 and confirmed every section remains a full-width table row group with adjacent labels and values"
]
}
@@ -26,10 +26,40 @@ const detailValueToneClass = (tone: DetailValueTone | undefined): string => {
return 'text-base-content';
};
const detailSectionDesktopBasisClass = (sectionCount: number): string =>
sectionCount === 5 || sectionCount === 6
? 'lg:basis-[calc(33.333%-0.5rem)]'
: 'lg:basis-[calc(25%-0.5rem)]';
const detailSectionDesktopColumnCount = (sectionCount: number): 1 | 2 | 3 | 4 => {
if (sectionCount <= 1) return 1;
if (sectionCount === 2) return 2;
if (sectionCount === 3 || sectionCount === 5 || sectionCount === 6) return 3;
return 4;
};
const detailSectionDesktopSpan = (sectionCount: number, sectionIndex: number): number => {
const columnCount = detailSectionDesktopColumnCount(sectionCount);
const remainder = sectionCount % columnCount;
if (remainder === 0 || sectionIndex < sectionCount - remainder) return 1;
const remainderIndex = sectionIndex - (sectionCount - remainder);
const baseSpan = Math.floor(columnCount / remainder);
const widerSectionCount = columnCount % remainder;
return baseSpan + (remainderIndex < widerSectionCount ? 1 : 0);
};
const detailSectionDesktopBasisClass = (sectionCount: number, sectionIndex: number): string => {
const columnCount = detailSectionDesktopColumnCount(sectionCount);
const span = detailSectionDesktopSpan(sectionCount, sectionIndex);
if (span >= columnCount) return 'lg:basis-full';
if (columnCount === 4) {
if (span === 3) return 'lg:basis-[calc(75%-0.125rem)]';
if (span === 2) return 'lg:basis-[calc(50%-0.25rem)]';
return 'lg:basis-[calc(25%-0.375rem)]';
}
if (columnCount === 3) {
if (span === 2) return 'lg:basis-[calc(66.667%-0.167rem)]';
return 'lg:basis-[calc(33.333%-0.333rem)]';
}
return 'lg:basis-[calc(50%-0.25rem)]';
};
export const DetailSectionTable: Component<{
sections: DetailSection[];
@@ -45,10 +75,10 @@ export const DetailSectionTable: Component<{
wrapperClass="lg:overflow-visible"
>
<For each={props.sections}>
{(section) => (
{(section, sectionIndex) => (
<TableBody
data-testid={section.testId}
class={`divide-y divide-border lg:flex lg:min-w-[16rem] lg:flex-1 lg:flex-col lg:overflow-hidden lg:rounded lg:border lg:border-border lg:bg-surface lg:p-3 lg:shadow-sm lg:divide-y-0 ${detailSectionDesktopBasisClass(props.sections.length)}`}
class={`divide-y divide-border lg:flex lg:min-w-0 lg:flex-none lg:flex-col lg:overflow-hidden lg:rounded lg:border lg:border-border lg:bg-surface lg:p-3 lg:shadow-sm lg:divide-y-0 ${detailSectionDesktopBasisClass(props.sections.length, sectionIndex())}`}
>
<TableRow class="bg-surface-alt lg:mb-1 lg:block lg:bg-transparent lg:hover:bg-transparent">
<TableHead
@@ -7976,8 +7976,13 @@ describe('shared primitive guardrails', () => {
expect(detailSectionTableSource).toContain('DetailSectionTable');
expect(detailSectionTableSource).toContain('InlineDetailPanel');
expect(detailSectionTableSource).toContain('ObjectDrawerHeader');
expect(detailSectionTableSource).toContain('lg:basis-[calc(25%-0.5rem)]');
expect(detailSectionTableSource).toContain('lg:basis-[calc(33.333%-0.5rem)]');
expect(detailSectionTableSource).toContain('detailSectionDesktopSpan');
expect(detailSectionTableSource).toContain('lg:basis-[calc(25%-0.375rem)]');
expect(detailSectionTableSource).toContain('lg:basis-[calc(50%-0.25rem)]');
expect(detailSectionTableSource).toContain('lg:basis-[calc(33.333%-0.333rem)]');
expect(detailSectionTableSource).toContain('lg:basis-[calc(66.667%-0.167rem)]');
expect(detailSectionTableSource).toContain('lg:flex-none');
expect(detailSectionTableSource).not.toContain('lg:flex-1');
expect(detailSectionTableSource).toContain('lg:grid-cols-[7rem_minmax(0,1fr)]');
expect(detailSectionTableSource).toContain('lg:text-left');
expect(detailSectionTableSource).toContain('lg:divide-y-0');
@@ -87,8 +87,8 @@ describe('DetailSectionTable', () => {
expect(sections).toHaveLength(2);
expect(sections[0]).toHaveClass(
'lg:flex',
'lg:flex-1',
'lg:basis-[calc(25%-0.5rem)]',
'lg:flex-none',
'lg:basis-[calc(50%-0.25rem)]',
'lg:rounded',
'lg:border',
'lg:p-3',
@@ -185,7 +185,7 @@ describe('DetailSectionTable', () => {
expect(fill?.firstElementChild).toHaveClass('bg-emerald-500');
});
it('balances five desktop sections across three- and two-card rows', () => {
it('fills five desktop sections on the same three-column tracks', () => {
const { container } = render(() => (
<DetailSectionTable
sections={Array.from({ length: 5 }, (_, index) => ({
@@ -197,7 +197,31 @@ describe('DetailSectionTable', () => {
const sections = container.querySelectorAll('tbody');
expect(sections).toHaveLength(5);
sections.forEach((section) => expect(section).toHaveClass('lg:basis-[calc(33.333%-0.5rem)]'));
Array.from(sections)
.slice(0, 3)
.forEach((section) => expect(section).toHaveClass('lg:basis-[calc(33.333%-0.333rem)]'));
expect(sections[3]).toHaveClass('lg:basis-[calc(66.667%-0.167rem)]');
expect(sections[4]).toHaveClass('lg:basis-[calc(33.333%-0.333rem)]');
});
it('fills seven desktop sections on the same four-column tracks', () => {
const { container } = render(() => (
<DetailSectionTable
sections={Array.from({ length: 7 }, (_, index) => ({
label: `Section ${index + 1}`,
rows: [{ label: 'Value', value: String(index + 1) }],
}))}
/>
));
const sections = container.querySelectorAll('tbody');
expect(sections).toHaveLength(7);
Array.from(sections)
.slice(0, 4)
.forEach((section) => expect(section).toHaveClass('lg:basis-[calc(25%-0.375rem)]'));
expect(sections[4]).toHaveClass('lg:basis-[calc(50%-0.25rem)]');
expect(sections[5]).toHaveClass('lg:basis-[calc(25%-0.375rem)]');
expect(sections[6]).toHaveClass('lg:basis-[calc(25%-0.375rem)]');
});
it('lazily renders technical details with the same compact section rows', () => {