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:
@@ -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.
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user