mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-24 11:26:34 +00:00
5c491b78b5
* feat(web): mobile bottom navigation + quick-capture (PLAN-1694)
Add a mobile-only persistent bottom navigation bar that coexists with the
existing TopBar. Five slots: Dashboard, Search, Quick-capture (center +),
Activity, More. The "More" slot opens a BottomSheet with the sidebar's
overflow destinations + collections; the center action opens a quick-capture
sheet that creates an item via the existing API.
- New shared nav source (lib/nav/destinations.ts) consumed by both the
desktop Sidebar and the new BottomNav/More sheet so the two can't drift;
Sidebar refactored to derive active-state from it (lossless).
- BottomNav mounts in the workspace layout (workspace-only), shows only on
mobile (uiStore.isMobile, ≤768px), and toggles body.has-bottom-nav so
app.css reflows .main-content above the fixed bar.
- iOS: app.html viewport-fit=cover + env(safe-area-inset-bottom) padding.
- z-index 40 (above TopBar/sidebar, below sheets).
Header retirement + full "You" (workspace/account) consolidation are
deliberately deferred to a human design checkpoint (DR-3).
Verify: web npm run check (0 errors) + npm run build (✓). Codex review loop
clean (1 P2 fixed: quick-capture re-defaults a stale collection slug).
Refs PLAN-1694 (TASK-1695, TASK-1696, TASK-1697, TASK-1698); parent ROADM-28.
* feat(web): retire mobile header in-workspace, add You sheet + contextual bar (PLAN-1694)
Complete the mobile nav vision on top of the bottom bar: inside a workspace
the global mobile header is gone, replaced by the BottomNav + a "You" sheet +
a contextual back/title bar on detail screens.
- BottomNav 5th slot More → You (👤). New YouSheet composes the workspace
switcher, the nav overflow, and the full account menu carried over from the
retired TopBar (Workspaces/Settings/Billing/Admin/theme/Resources/Connect/
Sign out) so nothing is lost.
- Root layout gates <TopBar mobile /> on !inWorkspace (page.params.workspace),
keeping it for the non-workspace picker/home which has no BottomNav; the
.app-layout top offset now applies only via body.has-mobile-topbar.
- New MobileContextBar: fixed back+title bar shown only on detail screens
(path depth ≥2); body.has-context-bar reflows .main-content top. Root/tab
screens render full-height (reclaimed space).
Codex consensus + review clean (no P1). P2s fixed: safe in-app-history check
via afterNavigate (deep-link fallback to parent URL), humanized title fallback
for pages that don't wire titleStore, and the You sheet closes on navigation
so a workspace switch dismisses it.
Verify: web npm run check (0 errors) + npm run build (✓).
Refs PLAN-1694 (TASK-1699, TASK-1700); parent ROADM-28.
* feat(web): redesign mobile nav into Workspace + You docked sheets (PLAN-1694)
Address design feedback: the previous "You" sheet reused common/BottomSheet
(covered the nav bar) and embedded the old WorkspaceSwitcher (plain, unadapted).
Replace it with two purpose-built surfaces that dock ABOVE the nav.
- DockedSheet (new): bottom sheet anchored above the bottom nav — backdrop
stops at the nav's top edge so the bar stays visible + tappable and the
active slot stays lit. ~2/3 height, grab handle, slide-up, swipe-down /
tap-out / Escape to dismiss. No more full-screen overlay covering the nav.
- Bottom-nav slot 1 Dashboard → Workspace (icon = current workspace avatar),
opening WorkspaceSheet: a designed switcher card (inline-expand list) +
Navigate tile grid + Collections.
- "You" slot → YouSheet, now account-only: profile header, theme toggle,
account Settings / Workspaces / Billing / Admin / Connect / Resources /
Sign out. Search / + / Activity unchanged.
- New avatar util (avatarColor/avatarInitial) for workspace + user avatars.
Splits the overstuffed single sheet into two focused surfaces (where-am-I vs
me) and removes the reused WorkspaceSwitcher/BottomSheet. Codex review CLEAN.
Verify: web npm run check (0 errors) + npm run build (✓).
Refs PLAN-1694 (TASK-1701); parent ROADM-28.
* feat(web): toggle Workspace/You sheets on repeat nav tap (PLAN-1694)
Tapping the Workspace or You nav slot a second time now closes its open
sheet (tap to open, tap again to close). Opening one still closes the other.
The docked backdrop stops above the nav, so the slot button stays tappable
while its sheet is open.
Refs PLAN-1694 (TASK-1701).
* feat(web): dock mobile search to match the nav sheets (PLAN-1694)
The CommandPalette was the last mobile surface that didn't match — a
full-screen takeover (square, no handle, hid the nav). Give its mobile
presentation the same docked treatment as the Workspace/You sheets;
desktop ⌘K is untouched.
- Mobile: dock the palette above the bottom nav (backdrop stops at the nav's
top edge so the bar stays visible), rounded top, grab handle with
swipe-down-to-dismiss, tap-outside to close, no X. Lift above the nav only
when it's present (:global(body.has-bottom-nav)); flush to bottom on the
non-workspace picker. Keyboard handling preserved: input stays at the sheet
top, .results is the sole scroll area, min-height keeps the input clear of
the keyboard when empty.
- BottomNav: Search slot toggles (tap to open, tap again to close) and lights
up while open; the three surfaces (Workspace / You / Search) are now
mutually exclusive.
Codex review CLEAN. Verify: web npm run check (0 errors) + build (✓).
Note: on-screen-keyboard feel should get an on-device smoke test.
Refs PLAN-1694 (TASK-1701); parent ROADM-28.
986 lines
25 KiB
CSS
986 lines
25 KiB
CSS
: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;
|
||
--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: 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 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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
/*
|
||
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));
|
||
}
|
||
}
|
||
|