Files
pad/web/src/app.css
T
xarmian 3bf1b60365 feat(attachments): editor image crop with aspect presets (TASK-880) (#298)
Adds a drag-to-crop modal on top of the AttachmentImage toolbar
introduced in TASK-879. The /transform endpoint already accepted
the "crop" operation shape from TASK-879 — this PR wires the editor
UI plus the supporting tests.

Editor:
  - attachment-crop-modal.ts (new): pure-DOM crop modal in the same
    style as the existing image lightbox. Returns a Promise that
    resolves to the crop rect in ORIGINAL-IMAGE pixel coordinates
    when the user clicks Apply, or null on cancel / dismiss /
    image-load failure.
    - Image fits to a centered <dialog> via flex layout; backdrop
      click and Esc both cancel cleanly.
    - Crop rectangle starts at 80% of the image, centered. Body is
      a "move" handle; four corner handles resize.
    - Aspect presets: Free, 1:1, 4:3, 16:9. Preset clicks snap the
      current rect to the new ratio while preserving its center;
      subsequent corner drags clamp to the locked ratio.
    - Pointer events (touch + mouse for free) with setPointerCapture
      so drag continues even if the cursor leaves the handle.
    - Coordinate translation: rect in preview-pixel space →
      naturalWidth / offsetWidth scale → original-image pixel
      space. Result is clamped to natural bounds so a fractional-
      rounding overrun doesn't push the rect off-image.

  - attachment-image.ts: extracts swapNodeUuid() helper from
    runRotate so runCrop can share the setNodeMarkup +
    invalidate-old-metadata flow. The toolbar gains a fourth
    button (⌶ Crop…) that opens the modal pointed at the original
    variant. Per-format gating (refreshToolbarState) treats the
    crop button identically to the rotate trio — both go through
    /transform, so a libvips-only format (e.g. WebP on the pure-Go
    build) disables the whole toolbar with the same explanatory
    tooltip.

  - app.css: full styling for the crop modal — header with aspect
    toolbar, image stage with shadow-cutout overlay around the
    crop rect, four corner handles, footer with Cancel + Apply.
    Uses the existing CSS-variable palette so light/dark mode
    track automatically.

Server tests (3 new):
  - TestTransform_CropProducesNewBlobAtRectDimensions: end-to-end
    PNG crop, verify the response dimensions AND that the served
    bytes decode at the same dimensions (guards against an
    encode-pipeline off-by-one).
  - TestTransform_CropClipsToImageBounds: rect that extends past
    the image boundary clips rather than 400ing — the editor's
    rounding can produce rect+1px past natural width/height in
    rare fractional-scale cases, and the processor's Crop
    intersects with image bounds for exactly this reason.
  - TestTransform_CropRejectsBadRect: missing rect, zero width,
    negative xy, rect entirely outside → 400.

Parent: PLAN-866. Closes the editor-side image-tools track on top of
TASK-878 (Processor) and TASK-879 (rotate / transform endpoint).
2026-04-29 15:02:07 -04:00

959 lines
24 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);
}
/* Attachment images — both inside the editor and in rendered markdown.
Lazy-loaded via the node's renderHTML/NodeView; styles below set a
sane default size so the editor doesn't reflow once thumbnails arrive. */
.attachment-image {
max-width: 100%;
height: auto;
border-radius: var(--radius);
cursor: zoom-in;
display: block;
margin: 0.4em 0;
}
.attachment-image:hover {
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-blue) 35%, transparent);
}
/* Wrapper appended by the AttachmentImage NodeView to host the
selection-time rotate toolbar (TASK-879). Position relative so
the toolbar can pin to the top-right corner of the image. */
.attachment-image-wrapper {
position: relative;
display: inline-block;
max-width: 100%;
}
.attachment-image-wrapper.attachment-image-selected .attachment-image {
box-shadow: 0 0 0 2px var(--accent-blue);
}
/* Rotate toolbar — appears when the AttachmentImage node is
selected. Pinned top-right with absolute positioning so it doesn't
shift the image's layout when toggling visibility. */
.attachment-image-toolbar {
position: absolute;
top: 8px;
right: 8px;
display: inline-flex;
gap: 2px;
padding: 4px;
background: rgba(20, 20, 20, 0.85);
border-radius: var(--radius);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
z-index: 5;
-webkit-user-select: none;
user-select: none;
}
.attachment-image-toolbar-hidden {
display: none;
}
.attachment-image-toolbar-btn {
width: 28px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
border: none;
background: transparent;
color: #fff;
font-size: 18px;
line-height: 1;
border-radius: 4px;
cursor: pointer;
transition: background 0.15s, opacity 0.15s;
font-family: inherit;
}
.attachment-image-toolbar-btn:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.15);
}
.attachment-image-toolbar-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Attachment file chips — Notion-style chip rendering for non-image
attachments. Used by both the editor's AttachmentChip node and the
read-only markdown render path (TASK-874). Styles target the same
class names from both surfaces so the visual matches edge to edge. */
.file-chip {
display: inline-flex;
align-items: center;
gap: 0.4em;
max-width: 100%;
padding: 4px 10px;
margin: 0 1px;
background: var(--bg-tertiary);
border: 1px solid var(--border-subtle);
border-radius: 6px;
color: var(--text-primary);
text-decoration: none;
font-size: 0.92em;
line-height: 1.3;
vertical-align: baseline;
transition: background 0.15s, border-color 0.15s;
cursor: pointer;
}
.file-chip:hover {
background: var(--bg-hover);
border-color: color-mix(in srgb, var(--accent-blue) 35%, var(--border));
text-decoration: none;
}
.file-chip-icon {
flex-shrink: 0;
font-size: 1.05em;
line-height: 1;
}
.file-chip-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 38ch;
font-weight: 500;
}
.file-chip-size {
flex-shrink: 0;
color: var(--text-muted);
font-size: 0.88em;
}
.file-chip-size:empty {
display: none;
}
/* Editor selection styling — when the chip is the ProseMirror
selection (atom node selected for delete/replace), give it the same
selection treatment ProseMirror gives other inline nodes. */
.ProseMirror-selectednode.file-chip,
.file-chip.ProseMirror-selectednode {
outline: 2px solid var(--accent-blue);
outline-offset: 1px;
}
/* Upload-in-progress placeholder — rendered as a ProseMirror widget
decoration by the AttachmentUpload extension. Dashed border + faded
color so it visually reads as "not yet committed content" while the
network round-trip resolves. */
.attachment-upload-placeholder {
display: inline-flex;
align-items: center;
gap: 0.4em;
padding: 4px 10px;
margin: 0 1px;
background: var(--bg-tertiary);
border: 1px dashed color-mix(in srgb, var(--accent-blue) 50%, var(--border));
border-radius: 6px;
color: var(--text-secondary);
font-size: 0.92em;
line-height: 1.3;
vertical-align: baseline;
}
.attachment-upload-spinner {
width: 12px;
height: 12px;
border: 2px solid color-mix(in srgb, var(--accent-blue) 35%, transparent);
border-top-color: var(--accent-blue);
border-radius: 50%;
animation: attachment-upload-spin 0.7s linear infinite;
flex-shrink: 0;
}
@keyframes attachment-upload-spin {
to {
transform: rotate(360deg);
}
}
.attachment-upload-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 30ch;
}
/* Crop modal (TASK-880). Appended to document.body so styles must
be global. Uses the native <dialog> element with a flex layout
so the image fills available space while header/footer stay fixed. */
dialog.attachment-crop-modal {
border: none;
background: transparent;
padding: 0;
max-width: 100vw;
max-height: 100vh;
color: #fff;
overflow: visible;
}
dialog.attachment-crop-modal::backdrop {
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(2px);
}
.attachment-crop-modal-shell {
display: flex;
flex-direction: column;
width: min(95vw, 1200px);
height: min(92vh, 900px);
background: var(--bg-secondary);
color: var(--text-primary);
border-radius: var(--radius);
overflow: hidden;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.55);
}
.attachment-crop-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--border);
background: var(--bg-secondary);
}
.attachment-crop-modal-title {
font-weight: 600;
font-size: 0.95em;
}
.attachment-crop-modal-aspects {
display: inline-flex;
gap: 4px;
background: var(--bg-tertiary);
padding: 2px;
border-radius: var(--radius);
}
.attachment-crop-modal-aspect-btn {
padding: 4px 12px;
border: none;
background: transparent;
color: var(--text-secondary);
border-radius: 4px;
font-size: 0.85em;
cursor: pointer;
font-family: inherit;
transition: background 0.15s, color 0.15s;
}
.attachment-crop-modal-aspect-btn:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.attachment-crop-modal-aspect-active,
.attachment-crop-modal-aspect-active:hover {
background: var(--accent-blue);
color: #fff;
}
.attachment-crop-modal-stage {
flex: 1;
min-height: 0;
position: relative;
overflow: hidden;
background: rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
-webkit-user-select: none;
user-select: none;
}
.attachment-crop-modal-img {
max-width: 100%;
max-height: 100%;
display: block;
-webkit-user-drag: none;
pointer-events: none;
}
.attachment-crop-modal-rect {
position: absolute;
box-sizing: border-box;
border: 1px solid #fff;
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.45);
cursor: move;
touch-action: none;
}
.attachment-crop-modal-handle {
position: absolute;
width: 12px;
height: 12px;
background: var(--accent-blue);
border: 2px solid #fff;
border-radius: 50%;
touch-action: none;
}
.attachment-crop-modal-handle-nw {
top: -7px;
left: -7px;
cursor: nwse-resize;
}
.attachment-crop-modal-handle-ne {
top: -7px;
right: -7px;
cursor: nesw-resize;
}
.attachment-crop-modal-handle-sw {
bottom: -7px;
left: -7px;
cursor: nesw-resize;
}
.attachment-crop-modal-handle-se {
bottom: -7px;
right: -7px;
cursor: nwse-resize;
}
.attachment-crop-modal-footer {
display: flex;
justify-content: flex-end;
gap: var(--space-2);
padding: var(--space-3) var(--space-4);
border-top: 1px solid var(--border);
background: var(--bg-secondary);
}
.attachment-crop-modal-cancel,
.attachment-crop-modal-apply {
padding: 6px 16px;
border: 1px solid var(--border);
background: var(--bg-tertiary);
color: var(--text-primary);
border-radius: var(--radius);
font-family: inherit;
font-size: 0.92em;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.attachment-crop-modal-cancel:hover {
background: var(--bg-hover);
}
.attachment-crop-modal-primary {
background: var(--accent-blue);
color: #fff;
border-color: var(--accent-blue);
}
.attachment-crop-modal-primary:hover {
filter: brightness(1.1);
}
/* Lightbox dialog — appended to document.body when an attachment image
is clicked, so styles must be global rather than scoped to Editor.svelte.
Uses the native <dialog> element; the ::backdrop pseudo handles the
overlay shade. The dialog content fills the viewport with a centered
image and a single close button. */
dialog.attachment-image-lightbox {
border: none;
background: transparent;
padding: 0;
max-width: 100vw;
max-height: 100vh;
overflow: visible;
color: #fff;
}
dialog.attachment-image-lightbox::backdrop {
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(2px);
}
dialog.attachment-image-lightbox .attachment-image-lightbox-img {
max-width: 95vw;
max-height: 95vh;
display: block;
margin: 0 auto;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.55);
cursor: zoom-out;
border-radius: var(--radius);
}
dialog.attachment-image-lightbox .attachment-image-lightbox-close {
position: fixed;
top: 16px;
right: 16px;
width: 40px;
height: 40px;
border-radius: 50%;
border: none;
background: rgba(0, 0, 0, 0.6);
color: #fff;
font-size: 28px;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
dialog.attachment-image-lightbox .attachment-image-lightbox-close:hover {
background: rgba(0, 0, 0, 0.85);
}
/* 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 {
/* Top margin is generous-but-plain (no reserved header strip):
the page-1 document header lives in normal flow via
`.title-row` + `.meta-info`, so subsequent pages start at
this margin with no overlap risk. Bottom margin reserves
space for the fixed footer + the `@bottom-right` page number.
See BUG-626 for why the repeating header was dropped. */
margin: 0.6in 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;
}
}