mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
fix(frontend): align desktop drawer detail cards
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user