Drawing a rectangle left it selected with its handles showing, and then refused to let you touch them. The handles were only live under the select tool, so sizing the shape you were still looking at meant a trip to the toolbar and back — for a gesture the editor had already drawn the grips for. Handles are now grabbable whatever tool is armed. They cannot be confused with drawing: a handle is a nine-pixel square that only exists while something is selected, nobody lands on one by accident, and Escape drops the selection if the space is wanted back for drawing. The original gate was protecting against a collision that does not really happen, at the cost of one that does. Making them universal opened a trap in the release path, fixed here too: the text-markup tools return early from onUp to commit a text selection, which would have stranded a resize half-done — applied to the live mark, never journalled, with the drag still set so the next press behaved oddly. A handle drag is now finished first, whatever tool is armed.
273 lines
16 KiB
CSS
273 lines
16 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; }
|
|
.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; }
|