Files
pad/web/src/app.css
T
xarmian 7c29413685 fix(web): print title overlap, page number, and select chevrons (BUG-625) (#156)
* fix(web): print title overlap, page number, and select chevrons (BUG-625)

Address three issues surfaced by a real Ctrl/Cmd+P test on an Idea
detail page (PLAN-620 follow-up):

1. Title cut off at top of page 1. The `@page { margin: 1.1in ... }`
   rule was declared in +page.svelte's scoped style block, but Svelte
   scoped-CSS at-rule loading meant the 0.75in default from app.css
   (TASK-621) kept winning. The fixed print header was ~0.4in tall and
   the content area started at 0.75in, but layout timing left them
   overlapping. Consolidate to a single @page rule in app.css with a
   widened `margin: 1.25in 0.6in 1in 0.6in` -- guaranteed clearance.

2. Footer showed "Page 0" on every page. `counter(page)` inside the
   ::after pseudo-element of a fixed-positioned element is captured
   once at initial layout (before pagination) and reused, so it never
   increments. Move the page number into a `@page { @bottom-right {
   content: "Page " counter(page); } }` margin-box where the counter
   evaluates correctly per page. Remove the `.print-footer-page` span
   and its `.print-page-num::after` rule from the item detail page.
   Add `padding-right: 1.2in` to the fixed footer so its content
   doesn't overlap the new margin-box page number.

3. FieldEditor selects still showed a `∨` chevron in print output --
   the chevron is an inline <svg class="select-chevron">, not the
   native UA dropdown arrow, so `appearance: none` on the button had
   no effect. Hide `.select-chevron` and `.select-dropdown` explicitly
   in the global print block.

Bonus: skip empty `.field-row`s via `.field-row:has(.field-value:empty)`
so rows like an unset "Category" don't print as a label with no value.

* fix(web): drop dead empty-field-row print rules (PR #156)

Address Codex P2 review comment on BUG-625. The `:empty`-based rules
added as a bonus to hide label-only rows (e.g. unset "Category")
never actually match in this codebase:

- Non-computed fields wrap a `<FieldEditor>` child inside `.field-value`,
  so `.field-value` always has children and is never `:empty`.
- Computed fields call `formatFieldDisplay(value)`, which returns `"—"`
  for null / empty, so `.computed-value` is never `:empty` either.

Remove the rules rather than leaving dead selectors that suggest the
behavior exists. Hiding blank rows in print is worth revisiting with a
real signal (e.g. a `data-empty` attribute or a template `{#if}`
guard), but out of scope for BUG-625 -- the title / page-number /
chevron fixes are what this PR is about.
2026-04-18 16:27:40 -04:00

581 lines
14 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
:root {
--bg-primary: #1a1a1a;
--bg-secondary: #242424;
--bg-tertiary: #2e2e2e;
--bg-hover: #353535;
--bg-active: #3a3a3a;
--text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
--text-muted: #666666;
--border: #333333;
--border-subtle: #2a2a2a;
--accent-blue: #4a9eff;
--accent-green: #4ade80;
--accent-purple: #a78bfa;
--accent-amber: #fbbf24;
--accent-cyan: #22d3ee;
--accent-orange: #fb923c;
--accent-gray: #9ca3af;
--accent-brown: #a8896c;
--status-draft: #666666;
--status-active: #4ade80;
--status-completed: #4a9eff;
--status-archived: #444444;
--topbar-height: 44px;
--sidebar-width: 260px;
--detail-panel-width: 300px;
--content-max-width: 960px;
--font-ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
--font-content: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
--font-mono: 'SF Mono', 'Fira Code', 'Consolas', monospace;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--radius: 6px;
--radius-sm: 4px;
--radius-lg: 8px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
font-family: var(--font-ui);
font-size: 14px;
line-height: 1.5;
color: var(--text-primary);
background: var(--bg-primary);
-webkit-font-smoothing: antialiased;
}
a {
color: var(--accent-blue);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
button {
font-family: inherit;
font-size: inherit;
cursor: pointer;
border: none;
background: none;
color: inherit;
}
input, textarea, select {
font-family: inherit;
font-size: inherit;
color: var(--text-primary);
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: var(--space-2) var(--space-3);
outline: none;
}
input:focus, textarea:focus {
border-color: var(--accent-blue);
}
/* Scrollbar */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 3px;
}
/* Prose styles for rendered markdown */
.prose {
font-family: var(--font-content);
font-size: 15px;
line-height: 1.7;
color: var(--text-primary);
max-width: var(--content-max-width);
}
.prose h1 { font-size: 1.8em; margin: 1.2em 0 0.6em; font-weight: 700; }
.prose h2 { font-size: 1.4em; margin: 1em 0 0.5em; font-weight: 600; }
.prose h3 { font-size: 1.15em; margin: 0.8em 0 0.4em; font-weight: 600; }
.prose p { margin: 0.6em 0; }
.prose ul, .prose ol { margin: 0.6em 0; padding-left: 1.5em; }
.prose li { margin: 0.2em 0; }
.prose li input[type="checkbox"] { margin-right: 0.4em; }
.prose pre {
background: var(--bg-tertiary);
padding: var(--space-4);
border-radius: var(--radius);
overflow-x: auto;
margin: 0.8em 0;
font-family: var(--font-mono);
font-size: 0.9em;
line-height: 1.5;
}
.prose code {
background: var(--bg-tertiary);
padding: 0.15em 0.4em;
border-radius: var(--radius-sm);
font-family: var(--font-mono);
font-size: 0.9em;
}
.prose pre code {
background: none;
padding: 0;
}
.prose blockquote {
border-left: 3px solid var(--border);
padding-left: var(--space-4);
margin: 0.8em 0;
color: var(--text-secondary);
}
.prose table {
border-collapse: collapse;
width: 100%;
margin: 0.8em 0;
}
.prose th, .prose td {
border: 1px solid var(--border);
padding: var(--space-2) var(--space-3);
text-align: left;
}
.prose th {
background: var(--bg-secondary);
font-weight: 600;
}
.prose hr {
border: none;
border-top: 1px solid var(--border);
margin: 1.5em 0;
}
.prose img {
max-width: 100%;
border-radius: var(--radius);
}
.prose a.doc-link {
background: var(--bg-tertiary);
padding: 0.1em 0.5em;
border-radius: var(--radius-sm);
color: var(--accent-blue);
font-weight: 500;
}
.prose a.doc-link:hover {
background: var(--bg-hover);
text-decoration: none;
}
.prose .doc-link.broken {
color: var(--accent-orange);
opacity: 0.7;
text-decoration: line-through;
}
/* External links in rendered markdown */
.prose .external-link {
color: var(--accent-blue);
text-decoration: none;
}
.prose .external-link:hover {
text-decoration: underline;
}
.prose .external-icon {
font-size: 0.75em;
opacity: 0.6;
vertical-align: super;
}
/* Editor link styles — autolinked URLs and wiki-links */
.prose a.editor-link {
color: var(--accent-blue);
text-decoration: none;
border-bottom: 1px solid color-mix(in srgb, var(--accent-blue) 30%, transparent);
transition: border-color 0.15s;
}
.prose a.editor-link:hover {
border-bottom-color: var(--accent-blue);
}
/* Light mode */
[data-theme="light"] {
--bg-primary: #ffffff;
--bg-secondary: #f7f7f8;
--bg-tertiary: #eeeff1;
--bg-hover: #e5e6e8;
--bg-active: #dddee0;
--text-primary: #1a1a1a;
--text-secondary: #555555;
--text-muted: #999999;
--border: #e0e0e0;
--border-subtle: #eaeaea;
--accent-blue: #2563eb;
--accent-green: #16a34a;
--accent-purple: #7c3aed;
--accent-amber: #d97706;
--accent-cyan: #0891b2;
--accent-orange: #ea580c;
--accent-gray: #6b7280;
--accent-brown: #92400e;
--status-draft: #999999;
--status-active: #16a34a;
--status-completed: #2563eb;
--status-archived: #bbbbbb;
}
@media (prefers-color-scheme: light) {
:root:not([data-theme="dark"]) {
--bg-primary: #ffffff;
--bg-secondary: #f7f7f8;
--bg-tertiary: #eeeff1;
--bg-hover: #e5e6e8;
--bg-active: #dddee0;
--text-primary: #1a1a1a;
--text-secondary: #555555;
--text-muted: #999999;
--border: #e0e0e0;
--border-subtle: #eaeaea;
--accent-blue: #2563eb;
--accent-green: #16a34a;
--accent-purple: #7c3aed;
--accent-amber: #d97706;
--accent-cyan: #0891b2;
--accent-orange: #ea580c;
--accent-gray: #6b7280;
--accent-brown: #92400e;
--status-draft: #999999;
--status-active: #16a34a;
--status-completed: #2563eb;
--status-archived: #bbbbbb;
}
}
/* Responsive */
@media (max-width: 768px) {
:root {
--sidebar-width: 280px;
}
}
/* ============================================================================
Print styles (PLAN-620 / TASK-621).
Tune Ctrl/Cmd+P output so users can save any Pad page as a clean PDF.
This file handles LAYOUT-LEVEL concerns:
- hide the workspace chrome (sidebar, top bar, modals, floating UI)
- force a light color palette regardless of the active theme
- unlock the fixed 100vh / overflow:hidden app shell so content flows
across pages in the print preview
Item-level formatting (title, properties block, markdown body, header /
footer rules, child-item checklist) is layered on top in tasks 622624.
============================================================================ */
@media print {
/* Force a light color palette for print regardless of the active theme.
We only need to pin the color variables; spacing / radii stay. */
:root,
:root[data-theme="dark"],
:root[data-theme="light"] {
--bg-primary: #ffffff;
--bg-secondary: #ffffff;
--bg-tertiary: #f5f5f5;
--bg-hover: transparent;
--bg-active: transparent;
--text-primary: #000000;
--text-secondary: #222222;
--text-muted: #555555;
--border: #cccccc;
--border-subtle: #dddddd;
}
html, body {
height: auto;
background: #ffffff !important;
color: #000000 !important;
font-size: 11pt;
}
/* Unlock the screen shell — it uses height: 100vh / overflow: hidden
to keep the sidebar + main column pinned. In print we need the
content to flow naturally across pages. */
.app-layout,
.app-shell {
display: block !important;
height: auto !important;
max-height: none !important;
overflow: visible !important;
}
.main-content {
flex: none !important;
width: 100% !important;
min-width: 0 !important;
overflow: visible !important;
}
/* Hide the workspace chrome. */
.sidebar,
.topbar,
.sidebar-expand-btn,
.topbar-expand-btn,
.mobile-header,
.hamburger,
.toast-container,
.overlay,
.palette,
.modal,
.modal-backdrop {
display: none !important;
}
/* Modals / dialogs / tooltips. Most KeyboardShortcuts-style overlays
carry role="dialog" on the backdrop. */
[role="dialog"],
[role="tooltip"],
[role="menu"] {
display: none !important;
}
/* Opt-in: any element tagged with data-print-hide disappears in print.
Components can use this to strip interactive affordances (edit buttons,
status pickers, hover cards, etc.) without needing a new CSS rule. */
[data-print-hide] {
display: none !important;
}
/* Strip decorative shadows, gradients, and background images. */
* {
box-shadow: none !important;
text-shadow: none !important;
background-image: none !important;
}
/* Page margins + page number (PLAN-620 / BUG-625).
- Top/bottom margins carve out space for the rendered header /
footer (TASK-623). Previously declared in +page.svelte, but
Svelte scoped-style timing meant the rule didn't always win
over this block, causing the header strip to overlap the
title on page 1. Single source of truth here.
- The page number lives in `@bottom-right` because
`counter(page)` evaluated inside a fixed element's ::after
pseudo captures its initial value (0) and never increments
per page in Chromium. Margin-box counters evaluate per page
correctly. */
@page {
margin: 1.25in 0.6in 1in 0.6in;
@bottom-right {
content: "Page " counter(page);
font-family: var(--font-ui);
font-size: 9pt;
color: #555;
}
}
/* -----------------------------------------------------------------
TASK-622 — rendered markdown (.prose) tuned for print.
----------------------------------------------------------------- */
.prose {
font-size: 11pt;
line-height: 1.55;
color: #000 !important;
max-width: none;
}
.prose h1, .prose h2, .prose h3,
.prose h4, .prose h5, .prose h6 {
color: #000 !important;
page-break-after: avoid;
break-after: avoid-page;
page-break-inside: avoid;
break-inside: avoid-page;
}
.prose h1 { font-size: 18pt; }
.prose h2 { font-size: 14pt; }
.prose h3 { font-size: 12pt; }
.prose p, .prose li {
orphans: 3;
widows: 3;
}
/* Links: keep the colored style AND append the URL inline so a printed
page still carries the reference. Skip internal wiki-links (which
resolve to the printed document's siblings) and fragment-only links.
Note: rich-editor links are rendered via Tiptap's SafeLink extension
using `data-href` instead of `href`, so we target both. */
.prose a[href]::after {
content: " (" attr(href) ")";
font-size: 0.85em;
color: #555;
word-break: break-all;
}
.prose a[data-href]::after {
content: " (" attr(data-href) ")";
font-size: 0.85em;
color: #555;
word-break: break-all;
}
.prose a[href^="#"]::after,
.prose a[data-href^="#"]::after,
.prose a.doc-link::after,
.prose a.editor-link[href^="/"]::after,
.prose a.editor-link[data-href^="/"]::after {
content: "";
}
/* Code blocks: visible, readable in light palette, don't orphan across
pages when we can help it. */
.prose pre {
background: #f5f5f5 !important;
border: 1px solid #ddd !important;
color: #000 !important;
font-size: 9.5pt;
page-break-inside: avoid;
break-inside: avoid;
}
.prose code {
background: #f5f5f5 !important;
color: #000 !important;
}
/* Images: full page width max, don't split across pages. */
.prose img {
max-width: 100% !important;
page-break-inside: avoid;
break-inside: avoid;
}
/* Tables: visible borders, contained to page width. */
.prose table {
page-break-inside: avoid;
break-inside: avoid;
border: 1px solid #ccc !important;
}
.prose th, .prose td {
border: 1px solid #ccc !important;
color: #000 !important;
}
.prose th {
background: #f5f5f5 !important;
}
.prose blockquote {
border-left: 3px solid #666 !important;
color: #333 !important;
}
/* Editor overlays (bubble menu, link popover, slash menu, mobile
toolbar, table toolbar, editor-toolbar) — never print these. */
.bubble-menu,
.link-popover,
.slash-menu,
.mobile-toolbar,
.table-toolbar,
.toolbar,
.editor-toolbar,
.editor-link-popover,
.editor-bubble-menu {
display: none !important;
}
/* Editor wrapper: in print the content is read-only — remove any
interactive cursor, borders, or focus styling. */
.editor-wrapper,
.editor-content {
border: none !important;
background: transparent !important;
padding: 0 !important;
outline: none !important;
}
/* -----------------------------------------------------------------
TASK-622 — Properties panel form widgets in print.
These rules live here (not in the item detail page's scoped style
block) because the widgets are rendered by the FieldEditor child
component — Svelte's scoped selectors don't cross component
boundaries. Strip interactive styling so the selected value reads
as plain text; the `.toggle` checkbox button is exempted and
rendered as a visible outlined box with a check mark when on.
----------------------------------------------------------------- */
.field-value select,
.field-value input,
.field-value textarea,
.field-value button:not(.toggle) {
appearance: none !important;
-webkit-appearance: none !important;
background: transparent !important;
border: none !important;
padding: 0 !important;
margin: 0 !important;
color: #000 !important;
font: inherit !important;
cursor: default !important;
pointer-events: none;
box-shadow: none !important;
}
/* Custom select controls (FieldEditor) render a dropdown chevron as
an inline SVG (`.select-chevron`) inside the button — `appearance:
none` on the button doesn't affect SVG children, so the `` caret
leaks into print. Same story for the expanded dropdown panel
(`.select-dropdown`) if one happens to be open at print time.
BUG-625. */
.field-value .select-chevron,
.field-value .select-dropdown,
.select-chevron,
.select-dropdown {
display: none !important;
}
/* Checkbox field (`.toggle`): visible outlined box; on-state gets a
check mark overlay so the on/off signal survives in print. */
.field-value .toggle {
appearance: none !important;
-webkit-appearance: none !important;
display: inline-block !important;
width: 11pt !important;
height: 11pt !important;
padding: 0 !important;
margin: 0 !important;
background: #fff !important;
border: 1px solid #000 !important;
border-radius: 2pt !important;
position: relative;
cursor: default !important;
pointer-events: none;
box-shadow: none !important;
vertical-align: -1pt;
}
.field-value .toggle-knob {
display: none !important;
}
.field-value .toggle.on::after {
content: "✓";
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: #000;
font-size: 10pt;
font-weight: 700;
line-height: 1;
}
}