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:
xarmian
2026-04-18 15:39:53 -04:00
committed by GitHub
parent e81da7a24f
commit f9a248f4da
3 changed files with 452 additions and 0 deletions
+177
View File
@@ -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>