mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-25 03:42:06 +00:00
feat(web): print-format the item detail page (TASK-622) (#153)
* feat(web): print-format the item detail page (TASK-622) Layer item-page print formatting on top of the base stylesheet added in TASK-621: - Title row renders as plain text (large, serif-friendly, no button affordance); issue ref prefix stays as a subtle prefix. - Meta info (created/updated + actor) keeps as small-print subtitle. - Properties panel becomes a definition-list block (label / value grid) wrapped in a light card, with form widgets stripped so the selected value reads as plain text. - Content layout stacks the fields panel above the markdown body (no side-by-side columns in print). - Code context section, relationships list, and child items stay. - Comments / activity / version timeline are hidden entirely. - Action buttons, breadcrumb, share/move/delete controls, edit-mode toggle, add-relationship form, save-status chip, link-delete buttons are all stripped. Rendered markdown (.prose) gets a print tune-up in app.css: 11pt body, inline URL suffix on external links (skipped for wiki-links and fragment links), break-inside guards on code / images / tables, light-palette overrides for code blocks and blockquotes. Editor overlays (bubble menu, link popover, slash menu, mobile toolbar, table toolbar, editor toolbar) are hidden in print. Parent: PLAN-620. * fix(web): keep relationship status chips + print title during edit mode (PR #153) Address Codex P2 review comments on TASK-622: - Relationship rows: previously hid the entire `.link-row-actions` wrapper, which silently dropped the `.link-status` chip alongside the destructive delete button. Hide only `.link-delete-btn` so the status stays visible in print. - Title during inline edit: the screen renders either a `.title` button (read) or a `.title-input` textarea (edit); previous rules displayed the button and hid the textarea, so printing while editing produced a title-less page. Apply the same print typography to both, turning the textarea into a non-interactive, borderless plain-text heading. * fix(web): preserve checkbox field state in print output (PR #153) Address Codex P2 review comment on TASK-622. The form-widget strip rule `.field-value button { border: none; background: transparent; }` killed the visual state of `.toggle` (the checkbox field's switch button), since it renders state purely via styling — no text label. The printed page would lose the on/off signal entirely. Exempt `.toggle` from the strip rule via `:not(.toggle)` and add a dedicated print style that renders the toggle as an outlined 11pt box; when the field is on, overlay a check mark via `::after`. The toggle-knob is hidden (it's the sliding switch visual, not useful in print). * fix(web): print URL suffixes for SafeLink + print raw markdown legibly (PR #153) Address Codex P2 review comments on TASK-622: - Rich-editor links (Tiptap SafeLink extension) render with `data-href` instead of `href`, so the print suffix rule `.prose a[href]::after` never fired for the main document body. Add a parallel selector `.prose a[data-href]::after { content: " (" attr(data-href) ")"; }` plus matching skips for internal data-href wiki-links. - The Markdown editor's raw textarea had no print styling. Printing while the Markdown tab was active either clipped the textarea to its screen height or rendered with dark-theme chrome. Add a @media print block to `RawMarkdownEditor.svelte` that flattens the textarea into a plain monospace flow: no border, no background, auto height, visible overflow, page-break-inside: auto. Content prints as markdown source -- not ideal, but readable and content-preserving. * fix(web): hoist FieldEditor print strip rules to global scope (PR #153) Address Codex P2: the `.field-value select / input / button / .toggle` print overrides were defined inside the item detail page's scoped style block. Svelte scoped selectors don't cross component boundaries, so the form widgets rendered inside `FieldEditor` kept their interactive styling in print preview -- selects rendered with their screen chrome, toggles disappeared, etc. Move these rules into app.css's @media print block (which applies globally) and leave a note in +page.svelte explaining why. The `.assignment-select` rule stays in +page.svelte because those selects are inline in this template and correctly scoped.
This commit is contained in:
+177
@@ -371,5 +371,182 @@ input:focus, textarea:focus {
|
||||
@page {
|
||||
margin: 0.75in;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,4 +62,31 @@
|
||||
border-color: var(--accent-blue);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Print: let the raw markdown source flow naturally — no textarea
|
||||
box, no fixed height, monospace plain text so the source is at
|
||||
least legible on paper when a user prints while editing in
|
||||
Markdown mode (PLAN-620 / TASK-622). */
|
||||
@media print {
|
||||
.raw-textarea {
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
color: #000 !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
min-height: 0 !important;
|
||||
height: auto !important;
|
||||
overflow: visible !important;
|
||||
resize: none !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
width: 100% !important;
|
||||
font-family: var(--font-mono) !important;
|
||||
font-size: 10pt !important;
|
||||
line-height: 1.55 !important;
|
||||
white-space: pre-wrap !important;
|
||||
word-wrap: break-word;
|
||||
page-break-inside: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1723,4 +1723,252 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* =============================================================
|
||||
PRINT — item detail page (PLAN-620 / TASK-622).
|
||||
Layout-level chrome is hidden by app.css's base @media print
|
||||
block (TASK-621); this block formats the item page CONTENT:
|
||||
title, metadata, properties-as-definition-list, markdown body.
|
||||
Comments / activity / version history are stripped. Relationships
|
||||
and code context stay because they're document-level context.
|
||||
============================================================= */
|
||||
@media print {
|
||||
.item-page {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Hide interactive / screen-only chrome inside the item page. */
|
||||
.sticky-header,
|
||||
.meta-actions,
|
||||
.editor-mode-toggle,
|
||||
.add-relationship-section,
|
||||
.save-status,
|
||||
.copy-ref-btn,
|
||||
.copied-tooltip,
|
||||
.link-delete-btn {
|
||||
display: none !important;
|
||||
}
|
||||
#item-timeline,
|
||||
.timeline-section {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Title row — large, plain text, no button affordance. */
|
||||
.title-row {
|
||||
display: block;
|
||||
margin: 0 0 4pt 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
.title-row .item-ref {
|
||||
display: inline-block;
|
||||
font-size: 10pt;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
margin: 0 8pt 0 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.title-row .title,
|
||||
.title-row .title-input {
|
||||
display: inline;
|
||||
font-size: 20pt;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
cursor: default;
|
||||
line-height: 1.25;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
resize: none;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
overflow: visible;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
font-family: inherit;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Meta info (created / updated by). */
|
||||
.meta-info {
|
||||
font-size: 9pt;
|
||||
color: #555;
|
||||
margin: 0 0 12pt 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
display: block;
|
||||
}
|
||||
.meta-info .meta-sep { color: #888; }
|
||||
|
||||
/* Code context section — compact. */
|
||||
.code-context-section {
|
||||
margin: 0 0 12pt 0;
|
||||
page-break-inside: avoid;
|
||||
break-inside: avoid;
|
||||
}
|
||||
.code-context-section .section-title {
|
||||
font-size: 10pt;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #333;
|
||||
margin: 0 0 4pt 0;
|
||||
}
|
||||
.code-context-card {
|
||||
border: 1px solid #ccc !important;
|
||||
background: #fafafa !important;
|
||||
padding: 6pt 8pt !important;
|
||||
border-radius: 3pt;
|
||||
font-size: 9.5pt;
|
||||
}
|
||||
|
||||
/* Body: stack fields + content vertically instead of side-by-side. */
|
||||
.item-body,
|
||||
.item-body.layout-balanced,
|
||||
.item-body.layout-focus {
|
||||
display: block !important;
|
||||
grid-template-columns: none !important;
|
||||
gap: 0 !important;
|
||||
}
|
||||
|
||||
/* Properties panel → definition-list style block under the title. */
|
||||
.fields-panel {
|
||||
border: 1px solid #ccc !important;
|
||||
background: #fafafa !important;
|
||||
padding: 8pt 10pt !important;
|
||||
margin: 0 0 14pt 0 !important;
|
||||
border-radius: 3pt;
|
||||
page-break-inside: avoid;
|
||||
break-inside: avoid;
|
||||
grid-template-columns: none !important;
|
||||
display: block !important;
|
||||
}
|
||||
.fields-header {
|
||||
font-size: 9pt !important;
|
||||
font-weight: 600 !important;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #333 !important;
|
||||
margin: 0 0 4pt 0 !important;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
.fields-header + .fields-header,
|
||||
.fields-panel > .fields-header ~ .fields-header {
|
||||
margin-top: 8pt !important;
|
||||
}
|
||||
.field-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(80pt, 120pt) 1fr;
|
||||
gap: 6pt;
|
||||
padding: 1.5pt 0;
|
||||
border: none !important;
|
||||
font-size: 10pt;
|
||||
align-items: baseline;
|
||||
}
|
||||
.field-label {
|
||||
color: #555 !important;
|
||||
font-weight: 500;
|
||||
font-size: 9.5pt;
|
||||
}
|
||||
.field-value {
|
||||
color: #000 !important;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
/* Form-widget strip rules for .field-value are in app.css
|
||||
(global) because most of those controls render inside the
|
||||
FieldEditor child component and Svelte scoped selectors
|
||||
from this file don't cross component boundaries. Keep the
|
||||
assignment-select rule here since those selects are inline
|
||||
in this template. */
|
||||
.assignment-select {
|
||||
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;
|
||||
}
|
||||
|
||||
/* Progress bar → compact text percentage. */
|
||||
.progress-bar {
|
||||
background: #eee !important;
|
||||
border: 1px solid #ccc !important;
|
||||
height: 10pt;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.progress-fill {
|
||||
background: #888 !important;
|
||||
}
|
||||
.progress-text {
|
||||
color: #000 !important;
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
/* Content panel: full width, no padding from screen. */
|
||||
.content-panel {
|
||||
padding: 0 !important;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Relationships: keep but compact. */
|
||||
.relationships-section {
|
||||
margin: 14pt 0 0 0 !important;
|
||||
padding: 8pt 0 0 0 !important;
|
||||
border-top: 1px solid #ccc !important;
|
||||
page-break-inside: avoid;
|
||||
break-inside: avoid;
|
||||
}
|
||||
.relationships-section .section-title {
|
||||
font-size: 10pt;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #333;
|
||||
margin: 0 0 4pt 0;
|
||||
}
|
||||
.relationship-groups {
|
||||
display: block !important;
|
||||
}
|
||||
.relationship-group {
|
||||
margin: 0 0 6pt 0 !important;
|
||||
}
|
||||
.relationship-group-title {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0 0 2pt 0;
|
||||
}
|
||||
.links-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.link-row {
|
||||
display: block !important;
|
||||
padding: 1pt 0 !important;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
font-size: 9.5pt;
|
||||
}
|
||||
.link-target {
|
||||
color: #000 !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user