diff --git a/CHANGELOG.md b/CHANGELOG.md index 920a7b8..306b2b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 0.65.0 (2026-07-27) + +- **The schedule's HTML export reads as a calendar** (T-0099). Week rows used + to be only as tall as whatever they held, so the grid's rhythm changed from + week to week and a row with a bar towered over an empty one. Every day cell + now gets the same minimum height, the seven columns are separated by rules, + and a non-working day is shaded over its whole cell instead of behind its + date alone — the same treatment the on-screen grid already gave them. Bars + and milestones keep exactly the geometry the layout computes; only the space + around them changed. Taller rows mean fewer weeks per printed page, which is + the trade the readability is worth. + ## 0.64.0 (2026-07-25) - **The schedule's side panel is resizable** (T-0096). Drag the divider between diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ebc6d49..4070aba 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -5997,7 +5997,7 @@ dependencies = [ [[package]] name = "workhub" -version = "0.64.0" +version = "0.65.0" dependencies = [ "cpal", "dirs 5.0.1", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e79cd3a..12b24ab 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "workhub" -version = "0.64.0" +version = "0.65.0" edition = "2021" description = "Developer Workspace Manager — a fast hub for AI-driven, multi-repo development" license = "MIT" diff --git a/src/lib/schedule/export.ts b/src/lib/schedule/export.ts index 41cdf11..92ff36a 100644 --- a/src/lib/schedule/export.ts +++ b/src/lib/schedule/export.ts @@ -34,6 +34,15 @@ function itemColor(item: ScheduleItem): string { return item.color ? COLOR_HEX[item.color] : COLOR_HEX.gray; } +/** Height of one bar lane, in px — the same lane pitch the screen grid uses. */ +const LANE_H = 18; +/** Height of the day-number line, which is also where the bar lanes start. */ +const HEAD_H = 20; +/** Minimum height of a day cell. Every week gets this much space whether or + * not it holds anything, so the grid keeps a constant week pitch instead of + * collapsing to the height of its contents. */ +const CELL_H = 96; + /** * Print styling lives here rather than in a shared stylesheet because the * export is the only surface that is ever printed. A4 landscape matches the @@ -58,26 +67,49 @@ table.grid th { border-bottom: 1px solid #d1d5db; text-align: left; } th.gutter, td.gutter { width: 44px; color: #6b7280; font-size: 11px; text-align: right; padding-right: 8px; } +td.gutter { vertical-align: top; padding-top: 3px; } +/* The weekday header wraps its own seven-column table, so the outer cell must + add no padding of its own — otherwise its column rules sit a few pixels off + from the ones in the week rows below. */ +table.grid th.daycols { padding: 0; } tr.week { break-inside: avoid; page-break-inside: avoid; } td.dayrow { padding: 0; border-bottom: 1px solid #e5e7eb; } +.weekbox { position: relative; } table.days { width: 100%; border-collapse: collapse; table-layout: fixed; } -table.days td { width: 14.285%; vertical-align: top; padding: 0; } -.daynum { padding: 3px 5px; font-size: 11px; font-variant-numeric: tabular-nums; } -.nonworking { background: #f3f4f6; color: #9ca3af; } +table.days td { width: 14.285%; padding: 0; } +table.days th { width: 14.285%; padding: 4px 6px; border-right: 1px solid #e5e7eb; } +/* A calendar reads as a calendar only if every week is the same block of + space and the seven columns are actually divided. Both are given here + rather than left to the content: a height on a table cell is a minimum, so + a week with more lanes than fit still grows. */ +td.daycell { + position: relative; vertical-align: top; height: ${CELL_H}px; + border-right: 1px solid #e5e7eb; padding: 0 0 3px; +} +td.daycell:last-child, table.days th:last-child { border-right: none; } +/* The day number and any non-working label share one fixed-height line, so + the bar lanes below start at the same offset in every cell. */ +.dayhead { height: ${HEAD_H}px; padding: 3px 5px 0; white-space: nowrap; overflow: hidden; } +.daynum { font-size: 11px; font-variant-numeric: tabular-nums; } +.nonworking { background: #f3f4f6; } +.nonworking .daynum { color: #9ca3af; } /* The x is what distinguishes a non-working day at a glance; the shading alone is easy to lose on a printed page. */ .nwmark { margin-left: 2px; font-size: 9px; color: #9ca3af; } -.outside { color: #d1d5db; } +.outside .daynum { color: #d1d5db; } /* Today is outlined rather than filled: a printed page is often black and white, where a filled pill turns into a black blob and a solid block of toner. The header already states the export date, so this only has to say "here". */ -.today { display: inline-block; border: 1px solid #111827; border-radius: 999px; - padding: 1px 5px; font-weight: 700; color: #111827; } -.monthstart { font-weight: 700; border-left: 2px solid #9ca3af; } -.nwlabel { display: block; font-size: 9px; color: #9ca3af; padding: 0 5px 2px; } -.lanes { padding: 0 0 4px; } -.lane { position: relative; height: 18px; } +.today .daynum { display: inline-block; border: 1px solid #111827; border-radius: 999px; + padding: 0 5px; font-weight: 700; color: #111827; } +.monthstart { border-left: 2px solid #9ca3af; } +.monthstart .daynum { font-weight: 700; } +.nwlabel { margin-left: 4px; font-size: 9px; color: #9ca3af; } +/* Bars span columns, so they cannot live inside a cell: they float over the + whole week, below the day-number line. */ +.lanes { position: absolute; left: 0; right: 0; top: ${HEAD_H}px; } +.lane { position: relative; height: ${LANE_H}px; } .bar { position: absolute; height: 16px; line-height: 16px; padding: 0 5px; font-size: 10px; color: #fff; white-space: nowrap; @@ -101,8 +133,10 @@ table.days td { width: 14.285%; vertical-align: top; padding: 0; } overflow: hidden; text-overflow: ellipsis; } .marker { position: absolute; top: 0; right: 0; width: 0; height: 0; border-top: 7px solid #b45309; border-left: 7px solid transparent; } -.daycell { position: relative; } -.points { padding: 0 4px 3px; } +/* Reserves the space the floating lanes occupy, so points always sit below + the bars of their own week. */ +.spacer { height: 0; } +.points { padding: 0 4px; } .point { display: block; font-size: 10px; line-height: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .dot { display: inline-block; width: 6px; height: 6px; margin-right: 3px; } @@ -135,7 +169,7 @@ footer { margin-top: 16px; border-top: 1px solid #e5e7eb; padding-top: 8px; function renderWeek(week: Layout["weeks"][number], locale: ScheduleLocale): string { const dayCells = week.days .map((d) => { - const classes = ["daynum"]; + const classes = ["daycell"]; if (d.isNonWorking) classes.push("nonworking"); if (d.isOutside) classes.push("outside"); if (d.isMonthStart) classes.push("monthstart"); @@ -148,9 +182,20 @@ function renderWeek(week: Layout["weeks"][number], locale: ScheduleLocale): stri const marker = d.points.some((p) => p.kind === "note") ? '' : ""; - return `