ux(ocr): camera FAB + modal padding for Android edge gestures

- Scan button is now a 📷 floating action button at bottom-right
  of the viewport (camera emoji per explicit request). Replaces
  the small "Scan image" button in the input header.
- Crop modal: inner padding on .modal-body so cropperjs's
  bottom-right corner handle isn't on top of Android's edge-swipe
  back-gesture zone. Backdrop also respects iOS safe-area insets
  (env(safe-area-inset-{right,left,bottom})).
This commit is contained in:
João Pedro Battistella Nadas
2026-06-09 19:10:22 +02:00
parent 74b8877550
commit a4064d0d72
2 changed files with 34 additions and 28 deletions

View File

@@ -14,6 +14,10 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Per-word confidence filter (≥ 60) drops the low-confidence noise Tesseract still emits. Re-assembles lines from kept words; status line reports how many words were dropped.
- Crop & rotate modal between image pick and OCR. After choosing an image, a modal pops up with cropperjs's drag-handle crop UI and Rotate left / Rotate right / Reset buttons. EXIF orientation is honoured automatically. Cancel / `Esc` / clicking the backdrop aborts; `Enter` accepts. Library lazy-loaded from esm.sh (no bundling).
### Changed
- "Scan image" button replaced with a 📷 floating action button at the bottom-right of the viewport (was a small button in the input pane header). Mobile thumb-friendly, doesn't crowd the input label.
- Crop modal has inner padding so cropperjs's corner handles stay away from the screen's right/bottom edges — fixes the bottom-right resize handle triggering Android's edge-swipe back gesture. Also respects iOS safe-area insets.
### Fixed
- "Hot: …" status no longer claims "unloads in X" when the server's `keep_alive` is `-1` (Ollama returns an absurd year-4001 `expires_at`). Threshold: any expiry >7 days drops the suffix and just shows the model name.

View File

@@ -156,32 +156,31 @@
box-shadow: none;
}
/* Input-pane header: label on the left, "Scan image" button on the right */
.input-header {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 0.25rem;
}
.input-header label { margin-bottom: 0; }
.scan-btn {
background: transparent;
border: 1px solid var(--border);
color: var(--muted);
font-size: 0.75rem;
padding: 0.2rem 0.6rem;
/* Floating action button for OCR scan — bottom-right, mobile-thumb friendly */
.scan-fab {
position: fixed;
bottom: calc(1.5rem + env(safe-area-inset-bottom, 0px));
right: calc(1.5rem + env(safe-area-inset-right, 0px));
width: 56px;
height: 56px;
padding: 0;
margin: 0;
border-radius: 4px;
border-radius: 50%;
background: var(--accent);
color: #fff;
border: none;
cursor: pointer;
letter-spacing: 0.04em;
text-transform: uppercase;
width: auto;
font-size: 1.75rem;
line-height: 1;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.15s;
}
.scan-btn:hover:not(:disabled) {
border-color: var(--accent);
color: var(--fg);
}
.scan-btn:disabled { cursor: progress; opacity: 0.6; }
.scan-fab:hover:not(:disabled) { transform: scale(1.06); }
.scan-fab:disabled { opacity: 0.6; cursor: progress; }
.scan-status {
font-size: 0.75rem;
color: var(--muted);
@@ -198,7 +197,9 @@
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
/* Generous side padding pushes the modal in from Android's edge-swipe
gesture zone (~24dp from screen edges triggers back/home). */
padding: 1rem max(1rem, env(safe-area-inset-right, 0px)) 1rem max(1rem, env(safe-area-inset-left, 0px));
z-index: 1000;
}
.modal {
@@ -222,6 +223,8 @@
min-height: 0;
max-height: 70vh;
background: #000;
/* Inset so cropper's corner handles never sit on a screen-edge gesture zone */
padding: 1.25rem;
}
.modal-body img { display: block; max-width: 100%; }
.modal-footer {
@@ -310,10 +313,7 @@
<div class="panes">
<div>
<div class="input-header">
<label for="input">Input</label>
<button type="button" id="scan-btn" class="scan-btn" title="Extract text from a camera shot or image file">Scan image</button>
</div>
<label for="input">Input</label>
<div id="input"></div>
<input type="file" id="scan-input" accept="image/*" capture="environment" hidden>
<div id="scan-status" class="scan-status"></div>
@@ -328,6 +328,8 @@
<div id="meta" class="meta"></div>
</div>
<button type="button" id="scan-btn" class="scan-fab" aria-label="Scan image (OCR)" title="Scan image (OCR)">📷</button>
<script type="module">
import { EditorView, keymap, placeholder } from "https://esm.sh/@codemirror/view@6";
import { EditorState } from "https://esm.sh/@codemirror/state@6";