diff --git a/web/src/app.css b/web/src/app.css index 67fc5224..0c988e95 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -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; + } } diff --git a/web/src/lib/components/editor/RawMarkdownEditor.svelte b/web/src/lib/components/editor/RawMarkdownEditor.svelte index e3817fff..edbcc032 100644 --- a/web/src/lib/components/editor/RawMarkdownEditor.svelte +++ b/web/src/lib/components/editor/RawMarkdownEditor.svelte @@ -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; + } + } diff --git a/web/src/routes/[username]/[workspace]/[collection]/[slug]/+page.svelte b/web/src/routes/[username]/[workspace]/[collection]/[slug]/+page.svelte index 4372dabe..eef33db4 100644 --- a/web/src/routes/[username]/[workspace]/[collection]/[slug]/+page.svelte +++ b/web/src/routes/[username]/[workspace]/[collection]/[slug]/+page.svelte @@ -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; + } + }