Clicking the dock raised a native file browser, which was the right answer while the editor had exactly one thing to offer an empty tab. It is the wrong answer now: a file browser can only ask which PDF, and the answer is sometimes none of them. So the dock opens the editor, and the empty editor says what it can do. The drop zone stays, and learns to read what it is given — pictures become pages, several PDFs become one document. Beside it sit the three ways in as buttons. Not included: compress, which cannot be done honestly without re-encoding the images, and split, which is the page rail plus Save a copy. A document built from pictures or joins has never been on disk, so it is marked unsaved from the moment it opens — otherwise closing the tab would bin it without asking. An empty editor also stops claiming to hold a file called document.pdf.
303 lines
18 KiB
CSS
303 lines
18 KiB
CSS
/* PDF Editor — full-tab. Same palette and control shapes as the screenshot
|
|
editor so the two add-ons read as one family; loaded AFTER pdf.js's
|
|
pdf_viewer.css so the overrides at the bottom win. */
|
|
:root { color-scheme: light dark;
|
|
--bg:#0e131c; --panel:#141a24; --panel2:#191f2b; --line:rgba(255,255,255,.09);
|
|
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#d6ff3d;
|
|
--danger:#ff5b5b; --board:#0a0d13; --shadow:rgba(0,0,0,.45); }
|
|
@media (prefers-color-scheme: light) {
|
|
:root { --bg:#f8faff; --panel:#ffffff; --panel2:#eff3fb; --line:rgba(0,0,0,.10);
|
|
--ink:#1a1f2b; --mut:#5c6577; --dim:#8a93a5; --acid:#0AC18E;
|
|
--board:#dde3ee; --shadow:rgba(20,30,50,.20); }
|
|
}
|
|
* { box-sizing: border-box; }
|
|
html, body { margin: 0; height: 100%; }
|
|
body { background: var(--bg); color: var(--ink);
|
|
font: 13px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
|
display: flex; flex-direction: column; overflow: hidden; }
|
|
/* pdf.js's stylesheet sets display on some of the nodes we toggle, so plain
|
|
[hidden] loses the cascade fight. */
|
|
[hidden] { display: none !important; }
|
|
|
|
/* ---- bars ----------------------------------------------------------- */
|
|
.topbar, .toolbar { display: flex; align-items: center; gap: 4px; padding: 6px 8px;
|
|
border-bottom: 1px solid var(--line); background: var(--panel);
|
|
user-select: none; }
|
|
.topbar { flex-wrap: nowrap; }
|
|
.toolbar { align-items: flex-start; gap: 8px; row-gap: 6px; flex-wrap: wrap; }
|
|
.tool-cluster { display: flex; flex-wrap: wrap; align-items: center;
|
|
gap: 10px; row-gap: 6px; flex: 1 1 auto; justify-content: center; }
|
|
.tgroup { display: inline-flex; align-items: center; gap: 4px; flex: 0 0 auto; }
|
|
.tgroup.zoomgroup { align-self: center; }
|
|
.spacer { flex: 1; }
|
|
.sep { width: 1px; height: 20px; background: var(--line); margin: 0 4px; flex: 0 0 auto; }
|
|
|
|
.docname { color: var(--mut); font-size: 12.5px; overflow: hidden; min-width: 0;
|
|
text-overflow: ellipsis; white-space: nowrap; max-width: 46vw; }
|
|
.docname.dirty::after { content: " •"; color: var(--acid); font-weight: 700; }
|
|
|
|
.btn, .tool {
|
|
border: 1px solid var(--line); background: var(--panel2); color: var(--ink);
|
|
width: 30px; height: 30px; border-radius: 6px; cursor: pointer;
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
padding: 0; transition: background 100ms, border-color 100ms;
|
|
font: inherit; line-height: 0; flex: 0 0 auto;
|
|
}
|
|
.btn:hover, .tool:hover { border-color: rgb(from var(--acid) r g b / .5); }
|
|
.btn:disabled, .tool:disabled { opacity: .4; cursor: default; }
|
|
.btn:disabled:hover { border-color: var(--line); }
|
|
.btn svg, .tool svg { width: 16px; height: 16px; display: block; }
|
|
.btn.wide { width: auto; padding: 0 10px; gap: 6px; line-height: 1; }
|
|
.btn.wide span { font-size: 12px; }
|
|
.btn.primary { background: var(--acid); color: #101418; border-color: transparent; font-weight: 600; }
|
|
.btn.primary:hover { filter: brightness(1.06); border-color: transparent; }
|
|
.btn.primary:disabled:hover { filter: none; }
|
|
.btn.danger { color: var(--danger); }
|
|
.btn.danger:hover { border-color: rgba(255,91,91,.55); }
|
|
.tool.active { background: rgb(from var(--acid) r g b / .15);
|
|
border-color: rgb(from var(--acid) r g b / .7); color: var(--acid); }
|
|
/* Redact is destructive; it reads red when armed rather than acid. */
|
|
.tool[data-tool="redact"].active { background: rgba(255,91,91,.16);
|
|
border-color: rgba(255,91,91,.7); color: var(--danger); }
|
|
|
|
.swatch { width: 22px; height: 22px; border-radius: 50%; padding: 0;
|
|
border: 2px solid var(--line); cursor: pointer; flex: 0 0 auto;
|
|
/* The near-black ink swatch would otherwise disappear into a dark
|
|
toolbar; a faint inner ring keeps every colour countable. */
|
|
box-shadow: inset 0 0 0 1px rgb(from var(--ink) r g b / .28); }
|
|
.swatch.active { border-color: var(--acid);
|
|
outline: 1px solid rgb(from var(--acid) r g b / .35); outline-offset: 1px; }
|
|
.width { border: 1px solid var(--line); background: var(--panel2); color: var(--ink);
|
|
width: 30px; height: 30px; border-radius: 6px; cursor: pointer;
|
|
display: inline-flex; align-items: center; justify-content: center; padding: 0; }
|
|
.width.active { border-color: rgb(from var(--acid) r g b / .7);
|
|
background: rgb(from var(--acid) r g b / .15); }
|
|
.width .dot { border-radius: 50%; background: currentColor; color: var(--ink); }
|
|
.width.active .dot { color: var(--acid); }
|
|
|
|
select, input[type="text"], textarea {
|
|
border: 1px solid var(--line); background: var(--panel2); color: var(--ink);
|
|
border-radius: 6px; padding: 5px 7px; font: inherit; height: 30px;
|
|
}
|
|
textarea { height: auto; resize: vertical; width: 100%; line-height: 1.45; }
|
|
select:focus, input:focus, textarea:focus { outline: none; border-color: rgb(from var(--acid) r g b / .6); }
|
|
|
|
/* ---- body: viewer + rail -------------------------------------------- */
|
|
.body { flex: 1; display: flex; min-height: 0; }
|
|
.viewerContainer { position: absolute; inset: 0; overflow: auto; background: var(--board); }
|
|
.body { position: relative; }
|
|
.viewerContainer { position: relative; flex: 1 1 auto; min-width: 0; }
|
|
|
|
.rail { flex: 0 0 176px; border-left: 1px solid var(--line); background: var(--panel);
|
|
display: flex; flex-direction: column; min-height: 0; }
|
|
body[data-rail="off"] .rail { display: none; }
|
|
.rail-head { display: flex; align-items: center; gap: 6px; padding: 7px 10px;
|
|
border-bottom: 1px solid var(--line); color: var(--dim); font-size: 11.5px; }
|
|
.rail-list { flex: 1; overflow: auto; padding: 8px; display: flex;
|
|
flex-direction: column; gap: 8px; }
|
|
|
|
.thumb { position: relative; border: 1px solid var(--line); border-radius: 6px;
|
|
background: var(--panel2); padding: 6px; cursor: grab; }
|
|
.thumb.current { border-color: rgb(from var(--acid) r g b / .75); }
|
|
.thumb.dragging { opacity: .45; }
|
|
.thumb.dropbefore { box-shadow: 0 -3px 0 0 var(--acid); }
|
|
.thumb.dropafter { box-shadow: 0 3px 0 0 var(--acid); }
|
|
.thumb.deleted { opacity: .38; }
|
|
.thumb.deleted .tcanvas { filter: grayscale(1); }
|
|
.thumb .tcanvas { display: block; width: 100%; height: auto; background: #fff;
|
|
border-radius: 3px; box-shadow: 0 1px 4px var(--shadow); }
|
|
.thumb .tfoot { display: flex; align-items: center; gap: 3px; margin-top: 5px; }
|
|
.thumb .tnum { color: var(--dim); font-size: 11px; font-variant-numeric: tabular-nums;
|
|
flex: 1 1 auto; min-width: 0; }
|
|
.thumb .tbtn { width: 20px; height: 20px; border-radius: 4px; border: 1px solid var(--line);
|
|
background: var(--panel); color: var(--ink); cursor: pointer; padding: 0;
|
|
display: inline-flex; align-items: center; justify-content: center; }
|
|
.thumb .tbtn svg { width: 12px; height: 12px; display: block; }
|
|
.thumb .tbtn:hover { border-color: rgb(from var(--acid) r g b / .55); }
|
|
.thumb .tbtn.del:hover { border-color: rgba(255,91,91,.6); color: var(--danger); }
|
|
|
|
.fbtn { border: 1px solid var(--line); background: var(--panel2); color: var(--ink);
|
|
border-radius: 6px; cursor: pointer; padding: 3px 7px; font: inherit;
|
|
font-size: 11px; line-height: 1.4; }
|
|
.fbtn:hover { border-color: rgb(from var(--acid) r g b / .55); }
|
|
|
|
/* ---- footer --------------------------------------------------------- */
|
|
.footer { display: flex; align-items: center; gap: 8px; padding: 5px 10px;
|
|
border-top: 1px solid var(--line); background: var(--panel); font-size: 11.5px; }
|
|
.fstat { color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.fstat.err { color: var(--danger); }
|
|
|
|
/* ---- drop zone ------------------------------------------------------ */
|
|
.dropzone { position: absolute; inset: 0; display: none; align-items: center;
|
|
justify-content: center; pointer-events: none; z-index: 20; }
|
|
body[data-empty="1"] .dropzone { display: flex; pointer-events: auto; }
|
|
body[data-dragover="1"] .dropzone { display: flex; pointer-events: none;
|
|
background: rgb(from var(--acid) r g b / .08); }
|
|
.dz-inner { text-align: center; color: var(--dim); border: 2px dashed var(--line);
|
|
border-radius: 14px; padding: 40px 54px; background: var(--panel);
|
|
max-width: 82%; }
|
|
body[data-dragover="1"] .dz-inner { border-color: rgb(from var(--acid) r g b / .8); color: var(--ink); }
|
|
.dz-inner svg { width: 48px; height: 48px; margin-bottom: 10px; }
|
|
.dz-title { font-size: 15px; color: var(--ink); font-weight: 600; }
|
|
.dz-sub { margin-top: 5px; max-width: 42ch; margin-left: auto; margin-right: auto; }
|
|
|
|
/* Start-screen tiles. Hidden while a drag is in flight: during a drop the
|
|
drop zone is the whole target, and a row of buttons under the cursor
|
|
reads as somewhere to aim, which it is not. */
|
|
.dz-actions { display: flex; gap: 10px; margin-top: 22px; justify-content: center;
|
|
flex-wrap: wrap; }
|
|
body[data-dragover="1"] .dz-actions { visibility: hidden; }
|
|
.tile { display: flex; flex-direction: column; align-items: flex-start; gap: 3px;
|
|
width: 176px; padding: 13px 14px; text-align: left; cursor: pointer;
|
|
background: var(--bg); color: var(--ink); border: 1px solid var(--line);
|
|
border-radius: 10px; font: inherit; }
|
|
.tile:hover { border-color: rgb(from var(--acid) r g b / .55);
|
|
background: rgb(from var(--acid) r g b / .06); }
|
|
.tile:focus-visible { outline: 2px solid var(--acid); outline-offset: 2px; }
|
|
.tile svg { width: 22px; height: 22px; margin: 0 0 5px; color: var(--acid); }
|
|
.tile-t { font-weight: 600; font-size: 13px; }
|
|
.tile-s { font-size: 11.5px; color: var(--dim); line-height: 1.35; }
|
|
.linkish { background: none; border: none; padding: 0; font: inherit; cursor: pointer;
|
|
color: var(--acid); text-decoration: underline; }
|
|
|
|
/* ---- modals --------------------------------------------------------- */
|
|
.scrim { position: fixed; inset: 0; background: rgba(4,7,12,.62); z-index: 50;
|
|
display: flex; align-items: center; justify-content: center; padding: 24px; }
|
|
.modal { background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
|
|
box-shadow: 0 18px 60px rgba(0,0,0,.5); padding: 18px 20px; max-width: 560px;
|
|
width: 100%; max-height: 90vh; overflow: auto; }
|
|
.modal h2 { margin: 0 0 10px; font-size: 15px; }
|
|
.modal p { margin: 0 0 10px; color: var(--mut); }
|
|
.modal p strong { color: var(--ink); }
|
|
.mhint { font-size: 12px; }
|
|
.mhint.warn { color: var(--danger); }
|
|
.mrow { display: flex; align-items: center; gap: 8px; margin-top: 14px; }
|
|
.mfield { display: inline-flex; align-items: center; gap: 6px; color: var(--mut); font-size: 12px; }
|
|
.mfield.block { display: flex; flex-direction: column; align-items: stretch; gap: 5px; }
|
|
.check { display: flex; align-items: flex-start; gap: 8px; margin: 12px 0 0;
|
|
color: var(--ink); font-size: 12.5px; cursor: pointer; }
|
|
.check input { margin: 2px 0 0; accent-color: var(--acid); }
|
|
#sign-pad { width: 100%; height: auto; aspect-ratio: 640 / 220; background: #fff;
|
|
border: 1px solid var(--line); border-radius: 8px; cursor: crosshair;
|
|
touch-action: none; display: block; }
|
|
.summary { margin: 10px 0 0; padding-left: 18px; color: var(--mut); font-size: 12px; }
|
|
.summary li { margin: 2px 0; }
|
|
.summary li.warn { color: var(--danger); }
|
|
|
|
.toast { position: fixed; left: 50%; bottom: 44px; transform: translateX(-50%) translateY(20px);
|
|
padding: 8px 14px; border-radius: 6px; background: rgba(11,14,20,.95);
|
|
color: #e7eaf1; font-size: 12.5px; border: 1px solid var(--line);
|
|
opacity: 0; pointer-events: none; transition: opacity 180ms, transform 180ms;
|
|
z-index: 100; max-width: 80vw; text-align: center; }
|
|
.toast.on { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
.toast.err { border-color: rgba(255,91,91,.6); color: #ffb3b3; }
|
|
|
|
/* ---- pdf.js layer overrides ----------------------------------------- */
|
|
/* A real gap between pages plus a drop shadow, instead of pdf.js's
|
|
transparent-border trick. */
|
|
/* overflow-anchor off: pages change height as they take their real size and
|
|
again on every zoom, and scroll anchoring "helpfully" moves the scroll
|
|
position to compensate — which walks the reader away from the page they
|
|
were on and, worse, can park the whole document outside the viewport. */
|
|
.viewerContainer { overflow-anchor: none; }
|
|
.pdfViewer { --page-margin: 0 auto 14px; --page-border: none; padding: 14px 0 4px; }
|
|
.pdfViewer .page { box-shadow: 0 2px 14px var(--shadow); }
|
|
|
|
/* Our overlay sits above the canvas but below the annotation (form) layer, so
|
|
a form field stays clickable even with marks drawn around it. */
|
|
/* Marks are clipped to the page by a clipPath on the marks group, not by the
|
|
SVG's own overflow — the selection handles sit just outside a mark's bounds
|
|
and a mark flush with the page edge still has to be grabbable. */
|
|
.pdfViewer .page .smOverlay { position: absolute; inset: 0; z-index: 3;
|
|
pointer-events: none; overflow: visible; }
|
|
.pdfViewer .page .annotationLayer { z-index: 4; }
|
|
.pdfViewer .page .textLayer { z-index: 2; }
|
|
|
|
/* Tool modality. When a drawing tool is armed the text and form layers stop
|
|
taking pointer events, so a drag paints instead of selecting text or
|
|
landing in a field. Highlight is the exception: it works BY selecting
|
|
text, so the text layer stays live for it. */
|
|
body[data-mode="draw"] .textLayer,
|
|
body[data-mode="draw"] .annotationLayer { pointer-events: none !important; }
|
|
body[data-mode="draw"] .pdfViewer .page { cursor: crosshair; }
|
|
body[data-mode="stamp"] .textLayer,
|
|
body[data-mode="stamp"] .annotationLayer { pointer-events: none !important; }
|
|
body[data-mode="stamp"] .pdfViewer .page { cursor: copy; }
|
|
body[data-mode="highlight"] .annotationLayer { pointer-events: none !important; }
|
|
body[data-mode="highlight"] .pdfViewer .page { cursor: text; }
|
|
|
|
/* Marks are only hit-testable with the select tool, so a drawing drag is
|
|
never stolen by something already on the page. */
|
|
body[data-mode="select"] .smOverlay { pointer-events: none; }
|
|
body[data-mode="select"] .smOverlay .mark { pointer-events: stroke; cursor: move; }
|
|
body[data-mode="select"] .smOverlay .mark[data-fill="1"] { pointer-events: all; }
|
|
.smOverlay .mark.sel { outline: none; }
|
|
.smOverlay .selbox { fill: none; stroke: var(--acid); stroke-width: 1;
|
|
stroke-dasharray: 4 3; vector-effect: non-scaling-stroke; }
|
|
.smOverlay .handle { fill: var(--acid); stroke: #101418; stroke-width: 1;
|
|
vector-effect: non-scaling-stroke; cursor: pointer; pointer-events: all; }
|
|
|
|
/* pdf.js renders form widgets with a coloured backdrop; keep it, it is the
|
|
only cue that a field is fillable. */
|
|
.annotationLayer .textWidgetAnnotation input,
|
|
.annotationLayer .textWidgetAnnotation textarea,
|
|
.annotationLayer .choiceWidgetAnnotation select { font: inherit; }
|
|
|
|
/* ---- selection bar --------------------------------------------------- */
|
|
/* Pinned to the current selection in viewport coordinates, so a page edge
|
|
never clips it and moving the selection to another page needs no
|
|
re-parenting. It steps aside mid-drag rather than jittering under the
|
|
cursor. */
|
|
.selbar {
|
|
position: fixed; z-index: 40; display: flex; align-items: center; gap: 2px;
|
|
padding: 3px; border-radius: 8px; background: var(--panel);
|
|
border: 1px solid var(--line); box-shadow: 0 6px 22px var(--shadow);
|
|
user-select: none;
|
|
}
|
|
body[data-dragging="1"] .selbar,
|
|
.selbar.offscreen { opacity: 0; pointer-events: none; }
|
|
.selbar .sb {
|
|
border: 1px solid transparent; background: transparent; color: var(--ink);
|
|
min-width: 26px; height: 26px; border-radius: 5px; cursor: pointer; padding: 0 5px;
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
font: inherit; font-size: 13px; line-height: 1;
|
|
}
|
|
.selbar .sb svg { width: 15px; height: 15px; display: block; }
|
|
.selbar .sb:hover { background: var(--panel2); border-color: rgb(from var(--acid) r g b / .5); }
|
|
.selbar .sb.active { background: rgb(from var(--acid) r g b / .16);
|
|
border-color: rgb(from var(--acid) r g b / .7); color: var(--acid); }
|
|
.selbar .sb.danger:hover { border-color: rgba(255,91,91,.6); color: var(--danger); }
|
|
.selbar .sb-group { display: inline-flex; align-items: center; gap: 1px; }
|
|
.selbar .sb-val { min-width: 22px; text-align: center; font-size: 11.5px;
|
|
color: var(--mut); font-variant-numeric: tabular-nums; }
|
|
.selbar .sb-sep { width: 1px; height: 17px; background: var(--line); margin: 0 3px; }
|
|
|
|
/* Style toggles inside the text dialog. */
|
|
.btn.styl { width: 30px; font-size: 14px; line-height: 1; }
|
|
.btn.styl.active { background: rgb(from var(--acid) r g b / .16);
|
|
border-color: rgb(from var(--acid) r g b / .7); color: var(--acid); }
|
|
|
|
/* Resize handles stay grabbable whatever tool is armed, so a shape can be
|
|
sized the moment it is drawn without a detour through the select tool.
|
|
Safe to leave live because a handle is a 9 px square that exists only while
|
|
something is selected — aiming at one is not something you do by accident,
|
|
and Escape clears the selection if you want the space back for drawing. */
|
|
.smOverlay .handle { pointer-events: all; }
|
|
|
|
/* The invisible grab area under a thin stroke. `stroke` rather than
|
|
`visibleStroke`: the copy is transparent on purpose. */
|
|
body[data-mode="select"] .smOverlay .mark .hit { pointer-events: stroke; }
|
|
.smOverlay .mark .hit { pointer-events: none; }
|
|
|
|
/* Picking a run of the document's own text to replace. The text layer stays
|
|
live so a click can land on a run; the form layer steps aside so a widget
|
|
does not swallow it. The hover outline is the only cue that the invisible
|
|
text layer is a target at all. */
|
|
body[data-mode="pickrun"] .annotationLayer { pointer-events: none !important; }
|
|
body[data-mode="pickrun"] .pdfViewer .page { cursor: text; }
|
|
body[data-mode="pickrun"] .textLayer span:hover {
|
|
outline: 2px solid rgb(from var(--acid) r g b / .9);
|
|
outline-offset: 1px;
|
|
background: rgb(from var(--acid) r g b / .16);
|
|
border-radius: 2px;
|
|
}
|