v0.3.0: OCR via camera, FAB UX, side drawer #4

Merged
jpnadas merged 16 commits from feature/ocr-camera into main 2026-06-09 18:38:34 +00:00
2 changed files with 429 additions and 0 deletions
Showing only changes of commit a4064d0d72 - Show all commits

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. - 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). - 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 ### 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. - "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; box-shadow: none;
} }
/* Input-pane header: label on the left, "Scan image" button on the right */ /* Floating action button for OCR scan — bottom-right, mobile-thumb friendly */
.input-header { .scan-fab {
display: flex; position: fixed;
align-items: baseline; bottom: calc(1.5rem + env(safe-area-inset-bottom, 0px));
justify-content: space-between; right: calc(1.5rem + env(safe-area-inset-right, 0px));
margin-bottom: 0.25rem; width: 56px;
} height: 56px;
.input-header label { margin-bottom: 0; } padding: 0;
.scan-btn {
background: transparent;
border: 1px solid var(--border);
color: var(--muted);
font-size: 0.75rem;
padding: 0.2rem 0.6rem;
margin: 0; margin: 0;
border-radius: 4px; border-radius: 50%;
background: var(--accent);
color: #fff;
border: none;
cursor: pointer; cursor: pointer;
letter-spacing: 0.04em; font-size: 1.75rem;
text-transform: uppercase; line-height: 1;
width: auto; 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) { .scan-fab:hover:not(:disabled) { transform: scale(1.06); }
border-color: var(--accent); .scan-fab:disabled { opacity: 0.6; cursor: progress; }
color: var(--fg);
}
.scan-btn:disabled { cursor: progress; opacity: 0.6; }
.scan-status { .scan-status {
font-size: 0.75rem; font-size: 0.75rem;
color: var(--muted); color: var(--muted);
@@ -198,7 +197,9 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: 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; z-index: 1000;
} }
.modal { .modal {
@@ -222,6 +223,8 @@
min-height: 0; min-height: 0;
max-height: 70vh; max-height: 70vh;
background: #000; 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-body img { display: block; max-width: 100%; }
.modal-footer { .modal-footer {
@@ -310,10 +313,7 @@
<div class="panes"> <div class="panes">
<div> <div>
<div class="input-header">
<label for="input">Input</label> <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>
<div id="input"></div> <div id="input"></div>
<input type="file" id="scan-input" accept="image/*" capture="environment" hidden> <input type="file" id="scan-input" accept="image/*" capture="environment" hidden>
<div id="scan-status" class="scan-status"></div> <div id="scan-status" class="scan-status"></div>
@@ -328,6 +328,8 @@
<div id="meta" class="meta"></div> <div id="meta" class="meta"></div>
</div> </div>
<button type="button" id="scan-btn" class="scan-fab" aria-label="Scan image (OCR)" title="Scan image (OCR)">📷</button>
<script type="module"> <script type="module">
import { EditorView, keymap, placeholder } from "https://esm.sh/@codemirror/view@6"; import { EditorView, keymap, placeholder } from "https://esm.sh/@codemirror/view@6";
import { EditorState } from "https://esm.sh/@codemirror/state@6"; import { EditorState } from "https://esm.sh/@codemirror/state@6";