mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-22 18:43:45 +00:00
7c29413685
* 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.
581 lines
14 KiB
CSS
581 lines
14 KiB
CSS
: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 622–624.
|
||
============================================================================ */
|
||
|
||
@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;
|
||
}
|
||
}
|
||
|