From a6667152cc7187f82d6d77faf9666b1d45f8be29 Mon Sep 17 00:00:00 2001 From: xarmian Date: Fri, 29 May 2026 22:18:58 -0400 Subject: [PATCH] fix(insights): professional print formatting for the exec report (TASK-1647) (#656) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(insights): professional print formatting for the exec report (TASK-1647) Polish the /insights/print @media print block (screen view unchanged): - Print typography: 10pt base so em sizes cascade down; capped headings (title 16pt, section 12pt, stat values 14pt, labels 8–9pt). - Charts: cap canvas height to 150px in print (overrides the inline 200–240px); smaller legend. - Page breaks: break-inside:avoid on status tables + rows + stat cards + chart canvases; section headings kept with their content (break-after:avoid). - Density: tighter report/section/stat/list gaps for a compact report. Parent: PLAN-1628 (IDEA-1627). * redesign(insights): charts-up-front dashboard layout + fix LayerCake print overlap (TASK-1647) - Move the three charts to a compact 2-up grid right after the headline stats (charts up front, half-width, not full-bleed). - Fix the chart-overlap bug: charts were sized via an inline screen height that LayerCake measured and cached, then an @media-print rule shrank the box afterwards, leaving the SVG drawn at the old range and spilling onto the next panel. Now pass small heights (130-150px) identical for screen + print so the measurement matches; add overflow:hidden as a clip-safety. - Cycle time becomes a full-width band: metrics rail + short chart. - What shipped flows into two columns (dense, fills page width) and is no longer kept whole — .shipped-group/.block/.status-table dropped from the break-inside:avoid set so a tall section no longer jumps to a fresh page and leaves the big blank gap on page 1. Only small atoms (tr, shipped line) stay unsplit. - Compact table cell padding + repeat thead across page breaks. * fix(insights): stop print charts clipping + shorten throughput X labels (TASK-1647) - Charts were measured at the 960px app width on screen (~450px per 2-col panel) then printed into ~360px columns, so LayerCake's cached width drew the SVG too wide and overflow:hidden clipped the right edge (only ~3 of 4 collection bars showed). Constrain the print page's on-screen report to a paper column (720px) so the measured width already fits the printed sheet — WYSIWYG, no clipping, all bars visible. - Throughput X-axis used full ISO dates (2026-05-23) that overlap once several buckets share a narrow chart; render compact M/D (or 'M/D Hh' for hourly) labels via fmtBucket. maxTicks=8 already thins longer windows. --- .../[workspace]/insights/print/+page.svelte | 363 ++++++++++++++---- 1 file changed, 288 insertions(+), 75 deletions(-) diff --git a/web/src/routes/[username]/[workspace]/insights/print/+page.svelte b/web/src/routes/[username]/[workspace]/insights/print/+page.svelte index d23e7b68..99845c6f 100644 --- a/web/src/routes/[username]/[workspace]/insights/print/+page.svelte +++ b/web/src/routes/[username]/[workspace]/insights/print/+page.svelte @@ -131,9 +131,20 @@ { key: 'completed', label: 'Completed', color: 'var(--chart-4, #10b981)' } ]; + // Compact X-axis label for the throughput buckets. Full ISO dates + // ("2026-05-23", ~70px) overlap once several buckets share a narrow chart; + // render "M/D" (day) or "M/D Hh" (hour) instead. Falls back to the raw + // bucket string for week/month or any unrecognised format. + function fmtBucket(b: string): string { + const m = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}))?/.exec(b); + if (!m) return b; + const label = `${Number(m[2])}/${Number(m[3])}`; + return m[4] !== undefined ? `${label} ${Number(m[4])}h` : label; + } + const throughputData = $derived( (report?.buckets ?? []).map((b) => ({ - bucket: b.bucket, + bucket: fmtBucket(b.bucket), created: b.created, completed: b.completed })) @@ -241,6 +252,7 @@ {:else if loading && !report}
Loading…
{:else if report} +
@@ -277,7 +289,86 @@
- + +
+ +
+

Throughput

+

Created vs completed per {report.granularity}

+ {#if noActivity} +

No activity in this period.

+ {:else} + + {/if} +
+ + +
+

Completed by collection

+ {#if completedByCollectionData.length === 0} +

Nothing completed in this period.

+ {:else} + + {/if} +
+ + + {#if report.cycle_time.sample_size > 0} +
+

Cycle time

+

Creation to completion

+
+
+
+ Median + {fmtHours(report.cycle_time.median_hours)} +
+
+ p90 + {fmtHours(report.cycle_time.p90_hours)} +
+
+ Sample + {report.cycle_time.sample_size} +
+
+ {#if cycleTimeData.length > 0} +
+ +
+ {/if} +
+
+ {/if} +
+ +

What shipped

{#if shippedGroups.length === 0} @@ -309,72 +400,6 @@ {/if}
- -
-

Throughput

-

Created vs completed per {report.granularity}

- {#if noActivity} -

No activity in this period.

- {:else} - - {/if} -
- - -
-

Completed by collection

- {#if completedByCollectionData.length === 0} -

Nothing completed in this period.

- {:else} - - {/if} -
- - - {#if report.cycle_time.sample_size > 0} -
-

Cycle time

-

Creation to completion

-
-
- Median - {fmtHours(report.cycle_time.median_hours)} -
-
- p90 - {fmtHours(report.cycle_time.p90_hours)} -
-
- Sample - {report.cycle_time.sample_size} -
-
- {#if cycleTimeData.length > 0} - - {/if} -
- {/if} - {#if statusByCollection.length > 0}
@@ -443,8 +468,13 @@ } /* ── Report document ─────────────────────────────────────────────────── */ + /* Constrained to paper-column width (≈ Letter/A4 content area) rather than + the app's 960px so the on-screen preview matches the printed sheet. This + is also what keeps the LayerCake charts from clipping in print: each chart + measures its width on screen, so a paper-width screen means that cached + measurement already fits the print column — no off-page overflow. */ .report { - max-width: var(--content-max-width); + max-width: 720px; margin: 0 auto; padding: var(--space-8) var(--space-6); display: flex; @@ -567,6 +597,51 @@ font-size: 0.9em; } + /* ── Charts grid (dashboard band, up front) ──────────────────────────── */ + .charts-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: var(--space-5); + } + .panel { + display: flex; + flex-direction: column; + gap: var(--space-2); + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg-secondary); + padding: var(--space-4); + } + .panel-title { + font-size: 1em; + font-weight: 700; + color: var(--text-primary); + } + .panel-sub { + font-size: 0.78em; + color: var(--text-muted); + margin-top: calc(-1 * var(--space-1)); + } + /* Cycle time spans the full grid width as a band: metrics on the left, + its (short) chart on the right, so it doesn't sit as a lonely half-cell. */ + .cycle-panel { + grid-column: 1 / -1; + } + .cycle-body { + display: grid; + grid-template-columns: auto 1fr; + align-items: center; + gap: var(--space-6); + } + .cycle-panel .metric-row { + gap: var(--space-5); + flex-direction: column; + flex-wrap: nowrap; + } + .cycle-chart { + min-width: 0; + } + /* ── What shipped ────────────────────────────────────────────────────── */ .shipped { display: flex; @@ -735,26 +810,164 @@ display: none !important; } + /* Smaller print base so the em-based sizes cascade down to a tidy + report scale (body ~10pt). Tighter gap reads as a compact report + rather than a sprawling web page. */ .report { max-width: none; margin: 0; padding: 0; color: #000; - gap: 1.25rem; + font-size: 9.5pt; + line-height: 1.3; + gap: 0.55rem; } .stat-card, .error-state { background: transparent; } - /* Keep logical units intact across page breaks. */ - .block, - .stat-row, - .shipped-group, + /* Cap the big screen-scale headings to report proportions. */ + .report-title h1 { + font-size: 16pt; + } + .report-subtitle { + font-size: 8.5pt; + } + .period { + font-size: 9.5pt; + } + .generated { + font-size: 8pt; + } .report-header { - break-inside: avoid; + padding-bottom: 0.4rem; } .block-title { + font-size: 12pt; + padding-bottom: 0.25rem; + } + .block-sub { + font-size: 8.5pt; + } + .block { + gap: 0.45rem; + } + + /* Compact stat row. */ + .stat-row { + gap: 0.5rem; + } + .stat-card { + padding: 0.4rem 0.5rem; + } + .stat-label { + font-size: 8pt; + } + .stat-value { + font-size: 14pt; + } + + /* Compact "what shipped" list, flowed into two columns so a long + completed set fills the page width and runs continuously instead of a + tall single column that leaves big vertical gaps. */ + .shipped { + gap: 0.6rem; + } + .shipped-list { + column-count: 2; + column-gap: 1.4rem; + } + .shipped-item { + font-size: 8.5pt; + padding: 0.5pt 0; + gap: 0.5rem; + break-inside: avoid; + } + .shipped-ref { + min-width: 4.6em; + } + .shipped-group-name { + font-size: 9.5pt; + } + .shipped-group-count { + font-size: 7.5pt; + } + .metric-value { + font-size: 11pt; + } + .metric-label { + font-size: 8pt; + } + .status-table { + font-size: 8.5pt; + } + + /* Compact rows: cap cell padding + line-height so a status table is + physically short. Repeat the header row on every page the table + spills onto (thead as a table-header-group), and right-size the + grid so two compact tables sit side by side instead of one tall stack. */ + .status-tables { + gap: 0.6rem 1.2rem; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + } + .status-table th, + .status-table td { + padding: 0.5pt 5pt; + line-height: 1.2; + } + .status-table thead { + display: table-header-group; + } + + /* Charts grid for print. Tighten the gap and de-emphasise the panel + chrome (no fill / border on paper). The chart heights are passed as + small props (≈130–150px) identical to screen, so we DON'T override + .canvas height here — that was the cause of the overlap: shrinking the + box after LayerCake measured the screen height left the SVG drawn at + the old range, spilling over the next panel. `overflow: hidden` clips + any residual sub-pixel spill so panels can never bleed into each + other. */ + .charts-grid { + gap: 0.5rem 1.2rem; + } + .panel { + padding: 0; + border: none; + background: transparent; + gap: 0.2rem; + } + .panel-title { + font-size: 11pt; + } + .panel-sub { + font-size: 8pt; + } + .chart-panel :global(.canvas) { + overflow: hidden; + } + .chart-panel :global(.legend) { + font-size: 8pt; + margin-bottom: 0.2rem; + } + + /* Keep small logical units intact across page breaks — a stat row, a + stat card, a chart panel, the header. Deliberately DO NOT add + `.block`, `.status-table`, or `.shipped-group` here: those can be + taller than a page, and keeping them whole forces a jump to the next + page that leaves a big blank gap (the page-1 symptom). They're allowed + to flow across page boundaries instead; only the small atoms inside + them (a table `tr`, a shipped line) are kept from splitting. */ + .stat-row, + .stat-card, + .report-header, + .chart-panel, + .status-table tr, + .chart-panel :global(.canvas) { + break-inside: avoid; + } + .block-title, + .report-title h1 { break-after: avoid; }