mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-24 11:26:34 +00:00
74589e98bb
From the orchestrator's pass on TASK-2424. The chip opens the options panel — it does not navigate — but it was still an <a href download> that only intercepted primary click. Middle-click and aux-click therefore opened or downloaded the file straight past the panel, which is the exact accidental download the panel exists to prevent, and screen readers announced a link for a control that opens a menu. A button has no URL to activate, so the bypass cannot exist rather than being intercepted, and it matches the strip's file tile — the same control, now with the same semantics. renderHTML stays an <a download>: that is the clipboard / read-only shape, where a link is honest and there is no panel to open. Deleted chips now use `disabled` rather than a dropped href: a disabled button receives no click or keydown at all, where the anchor's handlers still ran and had to swallow events. It is also blurred explicitly first, since a chip the user is focused on when another surface deletes the row would otherwise strand focus on an inert element. The keydown suppression moved ahead of the deleted bail so a stray Enter can never reach ProseMirror's keymap and split the paragraph the chip sits in. .file-chip carries the button reset explicitly (font: inherit), since the UA's 13.33px Arial would otherwise shrink the live chip away from the read-only one.
1121 lines
31 KiB
CSS
1121 lines
31 KiB
CSS
:root {
|
||
/* PLAN-2290 violet refresh — dark is the primary theme. Neutrals are
|
||
violet-biased; muted text meets WCAG AA on all bg tokens (TASK-2262 C9). */
|
||
--bg-primary: #0b0b10;
|
||
--bg-secondary: #14141b;
|
||
--bg-tertiary: #1c1c25;
|
||
/* Elevated overlay surfaces (menus, popovers). Same as tertiary in
|
||
dark; pure white in light where tertiary is a gray input tone. */
|
||
--bg-raised: #1c1c25;
|
||
--bg-hover: #22222d;
|
||
--bg-active: #2a2a36;
|
||
|
||
--text-primary: #f0f0f4;
|
||
--text-secondary: #a9a9b8;
|
||
--text-muted: #8b8b9c;
|
||
|
||
--border: #262633;
|
||
--border-subtle: #1d1d27;
|
||
--border-strong: #4a4a5e;
|
||
|
||
/* Card surface family (consumed app-wide from Phase 3 on). */
|
||
--card-bg: #16161f;
|
||
--card-border: #363648;
|
||
--shadow-card: 0 2px 6px rgba(0, 0, 0, 0.35);
|
||
|
||
/* Tint strength for Chip/badge fills (color-mix percentage). */
|
||
--chip-alpha: 16%;
|
||
/* Chip TEXT darkening: 100% = raw accent (dark theme); light theme
|
||
drops to ~72% mixed toward black so small colored labels hold AA. */
|
||
--chip-text-mix: 100%;
|
||
|
||
/* Brand accent. --accent-blue is aliased to it because ~95% of its 462
|
||
call sites mean "brand accent", not "blue" (see PLAN-2290); the
|
||
literally-blue categorical sites use --status-blue instead.
|
||
#9268f8 (not the mock's #8b5cf6): the one violet that passes AA as
|
||
link text on --bg-secondary (4.85:1) while beating the old blue for
|
||
white button text (3.78:1 vs 2.75:1). True text-on-fill AA lands
|
||
with the Phase 2 Button primitive via --text-on-accent. */
|
||
--accent-primary: #9268f8;
|
||
--accent-primary-soft: #a78bfa;
|
||
/* Filled-control variant: the violet band where white text passes AA
|
||
(4.96:1) while the fill stays ≥3:1 against --bg-secondary. Used by
|
||
Button primary fills; links/text keep --accent-primary. */
|
||
--accent-primary-strong: #7c4ff0;
|
||
--accent-blue: var(--accent-primary);
|
||
--accent-green: #4ade80;
|
||
--accent-purple: #a78bfa;
|
||
--accent-amber: #fbbf24;
|
||
--accent-cyan: #22d3ee;
|
||
--accent-orange: #fb923c;
|
||
--accent-gray: #9ca3af;
|
||
--accent-brown: #a8896c;
|
||
--accent-red: #ef4444;
|
||
/* Solid destructive fill — the red where white text passes AA (4.83:1)
|
||
in both themes. Button variant="danger-solid" (final confirms). */
|
||
--accent-red-strong: #dc2626;
|
||
|
||
/* Literal blue for categorical uses (status maps, charts, info badges).
|
||
Stays blue even if the brand accent changes — see PLAN-2290. */
|
||
--status-blue: #4a9eff;
|
||
|
||
/* Text color on accent-filled surfaces (buttons, filled badges). */
|
||
--text-on-accent: #ffffff;
|
||
|
||
--status-draft: #666666;
|
||
--status-active: #4ade80;
|
||
--status-completed: #4a9eff;
|
||
--status-archived: #444444;
|
||
|
||
--topbar-height: 44px;
|
||
--bottom-nav-height: 56px;
|
||
--context-bar-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: 8px;
|
||
--radius-sm: 5px;
|
||
--radius-lg: 12px;
|
||
|
||
/* Elevation scale. Values match the long-standing inline fallbacks
|
||
(ItemActionsMenu/LaneActionsMenu/BoardView/Modal) so defining them
|
||
is behavior-preserving. Theme-invariant for now — PLAN-2290 PR B
|
||
may tune per theme. */
|
||
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.12);
|
||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
--shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||
--modal-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||
--scrim: rgba(0, 0, 0, 0.5);
|
||
}
|
||
|
||
* {
|
||
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. `width` styles the VERTICAL scrollbar; `height` styles the
|
||
HORIZONTAL one. Both are needed — without `height` a horizontal scrollbar
|
||
(e.g. the board-view lane track) falls back to the chunky ~15px browser
|
||
default while every vertical scrollbar is a thin 6px, which reads as a
|
||
"weirdly large" scrollbar (BUG-2127 follow-up). Keep them equal. */
|
||
::-webkit-scrollbar {
|
||
width: 6px;
|
||
height: 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. */
|
||
/* Two elements share this class: the editor NodeView renders a <button> (a
|
||
live chip opens the options panel, it does not navigate — PLAN-2392 DR-12),
|
||
while the read-only markdown path and the clipboard shape render an <a
|
||
download>. So it carries the button reset explicitly: without `font:
|
||
inherit` the UA's 13.33px Arial would replace the inherited font and shrink
|
||
the chip, and `text-align` keeps the label left-aligned in the button. */
|
||
.file-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.4em;
|
||
max-width: 100%;
|
||
padding: 4px 10px;
|
||
margin: 0 1px;
|
||
font: inherit;
|
||
text-align: start;
|
||
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;
|
||
}
|
||
/* The editor NodeView renders an SVG file-type icon here (TASK-2417); the
|
||
read-only markdown render path still emits a text glyph, so these rules
|
||
have to work for both. currentColor keeps the SVG themed and
|
||
forced-colors safe. */
|
||
.file-chip-icon {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 1.05em;
|
||
line-height: 1;
|
||
}
|
||
.file-chip-icon > svg {
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
/* Deleted / missing target. The chip used to signal this by swapping its
|
||
glyph to a paperclip; the icon now says what KIND of file it is, so the
|
||
dead state gets its own treatment on the chip instead (TASK-2417). The
|
||
compound selector keeps this off the markdown renderer's bare
|
||
.attachment-missing span, which has its own inline marker. */
|
||
.file-chip.attachment-missing {
|
||
color: var(--text-muted);
|
||
border-style: dashed;
|
||
cursor: default;
|
||
text-decoration: line-through;
|
||
}
|
||
/* The NodeView marks a dead chip `disabled`, which is what makes it inert and
|
||
unfocusable; keep it looking the same as the read-only path's missing chip
|
||
rather than inheriting the UA's disabled-button greying. */
|
||
.file-chip:disabled {
|
||
color: var(--text-muted);
|
||
cursor: default;
|
||
opacity: 1;
|
||
}
|
||
.file-chip.attachment-missing:hover {
|
||
background: var(--bg-tertiary);
|
||
border-color: var(--border-subtle);
|
||
}
|
||
.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: #f5f5f9;
|
||
--bg-secondary: #ffffff;
|
||
--bg-tertiary: #efeff4;
|
||
--bg-raised: #ffffff;
|
||
--bg-hover: #ededf3;
|
||
--bg-active: #e3e3ec;
|
||
--text-primary: #191922;
|
||
--text-secondary: #4d4d5c;
|
||
--text-muted: #63637a;
|
||
--border: #dcdce6;
|
||
--border-subtle: #e8e8f0;
|
||
--border-strong: #b4b4c6;
|
||
--card-bg: #ffffff;
|
||
--card-border: #d0d0dc;
|
||
--shadow-card: 0 1px 3px rgba(23, 23, 40, 0.1);
|
||
--chip-alpha: 12%;
|
||
--chip-text-mix: 72%;
|
||
--accent-primary: #7c3aed;
|
||
--accent-primary-soft: #8b5cf6;
|
||
--accent-primary-strong: #7c3aed;
|
||
--accent-blue: var(--accent-primary);
|
||
--accent-red: #dc2626;
|
||
--accent-green: #16a34a;
|
||
--accent-purple: #7c3aed;
|
||
--accent-amber: #d97706;
|
||
--accent-cyan: #0891b2;
|
||
--accent-orange: #ea580c;
|
||
--accent-gray: #6b7280;
|
||
--accent-brown: #92400e;
|
||
--accent-red: #ef4444;
|
||
--status-blue: #2563eb;
|
||
--status-draft: #999999;
|
||
--status-active: #16a34a;
|
||
--status-completed: #2563eb;
|
||
--status-archived: #bbbbbb;
|
||
}
|
||
|
||
@media (prefers-color-scheme: light) {
|
||
:root:not([data-theme="dark"]) {
|
||
--bg-primary: #f5f5f9;
|
||
--bg-secondary: #ffffff;
|
||
--bg-tertiary: #efeff4;
|
||
--bg-raised: #ffffff;
|
||
--bg-hover: #ededf3;
|
||
--bg-active: #e3e3ec;
|
||
--text-primary: #191922;
|
||
--text-secondary: #4d4d5c;
|
||
--text-muted: #63637a;
|
||
--border: #dcdce6;
|
||
--border-subtle: #e8e8f0;
|
||
--border-strong: #b4b4c6;
|
||
--card-bg: #ffffff;
|
||
--card-border: #d0d0dc;
|
||
--shadow-card: 0 1px 3px rgba(23, 23, 40, 0.1);
|
||
--chip-alpha: 12%;
|
||
--chip-text-mix: 72%;
|
||
--accent-primary: #7c3aed;
|
||
--accent-primary-soft: #8b5cf6;
|
||
--accent-primary-strong: #7c3aed;
|
||
--accent-blue: var(--accent-primary);
|
||
--accent-red: #dc2626;
|
||
--accent-green: #16a34a;
|
||
--accent-purple: #7c3aed;
|
||
--accent-amber: #d97706;
|
||
--accent-cyan: #0891b2;
|
||
--accent-orange: #ea580c;
|
||
--accent-gray: #6b7280;
|
||
--accent-brown: #92400e;
|
||
--accent-red: #ef4444;
|
||
--status-blue: #2563eb;
|
||
--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;
|
||
/* Pin light-theme accents for paper — otherwise printing from the
|
||
dark theme puts the bright dark-mode accent on white (PR #1018
|
||
Codex finding). */
|
||
--accent-primary: #7c3aed;
|
||
--accent-blue: var(--accent-primary);
|
||
--accent-red: #dc2626;
|
||
}
|
||
|
||
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,
|
||
dialog[open] {
|
||
display: none !important;
|
||
}
|
||
|
||
/* Modals / dialogs / tooltips. Most KeyboardShortcuts-style overlays
|
||
carry role="dialog" on the backdrop. The shared Modal primitive uses a
|
||
native <dialog> (implicit dialog role, no literal role attribute), so it
|
||
is covered by the `dialog[open]` selector above.
|
||
`:not(.item-pane)` keeps the detail pane printable: on mobile the pane
|
||
carries role="dialog" (TASK-2131) but IS the content being printed, not a
|
||
transient overlay to strip. */
|
||
[role="dialog"]:not(.item-pane),
|
||
[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;
|
||
}
|
||
}
|
||
|
||
/*
|
||
Mobile bottom-nav reflow (PLAN-1694). <BottomNav /> is rendered in the
|
||
workspace layout and uses position: fixed; bottom: 0 on mobile. It toggles
|
||
`body.has-bottom-nav` while mounted on mobile so the scroll container
|
||
(.main-content, defined in the root +layout.svelte) gets bottom padding and
|
||
content can't hide under the bar. Gated by the same ≤768px breakpoint
|
||
uiStore.isMobile uses, and by the body class so non-workspace shell pages
|
||
(which share .main-content but have no bottom nav) aren't padded. The
|
||
safe-area inset keeps the last row clear of the iOS home indicator.
|
||
*/
|
||
@media (max-width: 768px) {
|
||
body.has-bottom-nav .main-content {
|
||
padding-bottom: calc(var(--bottom-nav-height) + env(safe-area-inset-bottom, 0px));
|
||
}
|
||
/*
|
||
Contextual top bar on detail screens (MobileContextBar, PLAN-1694
|
||
Phase 2). It's position: fixed; top: 0 and toggles body.has-context-bar
|
||
while shown, so pad the scroll container's top to match. Same body-class
|
||
pattern as the bottom-nav reflow above; safe-area inset clears the notch.
|
||
*/
|
||
body.has-context-bar .main-content {
|
||
padding-top: calc(var(--context-bar-height) + env(safe-area-inset-top, 0px));
|
||
}
|
||
}
|
||
|