theseus/bundled-addons/pdf-editor/editor.css
Local Dev 05ac67067a fix(pdf-editor): a shape can be resized the moment it is drawn
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.
2026-09-22 21:33:48 +02:00

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; }