v0.3.0: OCR via camera, FAB UX, side drawer #4
@@ -17,7 +17,10 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
### Changed
|
### Changed
|
||||||
- "Scan image" button replaced with a floating action button at the bottom-right of the viewport, using a Material Symbols Outlined camera glyph (`photo_camera`) for consistent rendering across platforms (was a small text button in the input pane header).
|
- "Scan image" button replaced with a floating action button at the bottom-right of the viewport, using a Material Symbols Outlined camera glyph (`photo_camera`) for consistent rendering across platforms (was a small text button in the input pane header).
|
||||||
- "Translate" inline button replaced with a primary FAB using the `translate` Material Symbols icon, stacked directly under the camera FAB at the bottom-right corner (closest to the thumb on phones). The camera FAB is now the *secondary* in the stack, just above. Both 56 × 56 px, same accent fill. The translate FAB gains a pulsing-ring `busy` animation during a running translation; `goBtn.textContent` swap is gone (was meaningless for an icon-only button). `body` got `~9 rem` of bottom padding so content scrolls clear of the stack.
|
- "Translate" inline button replaced with a primary FAB using the `translate` Material Symbols icon, stacked directly under the camera FAB at the bottom-right corner (closest to the thumb on phones). The camera FAB is now the *secondary* in the stack, just above. Both 56 × 56 px, same accent fill. The translate FAB gains a pulsing-ring `busy` animation during a running translation; `goBtn.textContent` swap is gone (was meaningless for an icon-only button). `body` got `~9 rem` of bottom padding so content scrolls clear of the stack.
|
||||||
- FAB stack rides above the on-screen keyboard on mobile. Added `interactive-widget=resizes-content` to the viewport meta (handles Chrome on Android) and a `VisualViewport` listener that exposes the keyboard's intrusion as a `--kb-inset` CSS custom property (handles iOS Safari). The FAB `bottom` uses `max(--kb-inset, env(safe-area-inset-bottom))` so the keyboard inset wins when it's open and the home-indicator inset wins when it isn't.
|
- FAB stack rides above the on-screen keyboard on mobile via `interactive-widget=resizes-content` in the viewport meta tag (handled by Chrome on Android).
|
||||||
|
- Layout: From / To dropdowns now occupy 2 columns at the top (was 3 columns including Model).
|
||||||
|
- Model selector moved into a kebab (⋮) menu in the top-left corner. Uses a native `<details>` element so toggle/keyboard-accessibility works without extra JS.
|
||||||
|
- Default source language is now Dutch (was Auto-detect). Target stays English.
|
||||||
- Crop modal:
|
- Crop modal:
|
||||||
- No default crop box. User drags on the image to draw the region; falls back to full image if they confirm without drawing.
|
- No default crop box. User drags on the image to draw the region; falls back to full image if they confirm without drawing.
|
||||||
- Buttons are now Material Symbols icons: `rotate_left`, `rotate_right`, `invert_colors` (new — see below), `check`. Cancel kept as text. The `check` (Use this) button is larger and more prominent. The Reset button was removed (rarely useful with no default box).
|
- Buttons are now Material Symbols icons: `rotate_left`, `rotate_right`, `invert_colors` (new — see below), `check`. Cancel kept as text. The `check` (Use this) button is larger and more prominent. The Reset button was removed (rarely useful with no default box).
|
||||||
|
|||||||
@@ -46,12 +46,55 @@
|
|||||||
.sub { color: var(--muted); font-size: 0.9rem; margin-bottom: 1.5rem; }
|
.sub { color: var(--muted); font-size: 0.9rem; margin-bottom: 1.5rem; }
|
||||||
.controls {
|
.controls {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
@media (max-width: 640px) {
|
|
||||||
.controls { grid-template-columns: 1fr; }
|
/* Kebab menu (top-left) housing the Model dropdown — keeps the main
|
||||||
|
controls focused on the From/To pair. */
|
||||||
|
.kebab {
|
||||||
|
position: fixed;
|
||||||
|
top: calc(1rem + env(safe-area-inset-top, 0px));
|
||||||
|
left: calc(1rem + env(safe-area-inset-left, 0px));
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
.kebab > summary {
|
||||||
|
list-style: none;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--fg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
.kebab > summary::-webkit-details-marker { display: none; }
|
||||||
|
.kebab > summary::marker { display: none; }
|
||||||
|
.kebab > summary .material-symbols-outlined { font-size: 22px; }
|
||||||
|
.kebab[open] > summary { border-color: var(--accent); color: var(--accent); }
|
||||||
|
.kebab-panel {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 0.5rem);
|
||||||
|
left: 0;
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
min-width: 220px;
|
||||||
|
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
.kebab-panel label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
label { display: block; font-size: 0.75rem; color: var(--muted); margin-bottom: 0.25rem; text-transform: uppercase; letter-spacing: 0.08em; }
|
label { display: block; font-size: 0.75rem; color: var(--muted); margin-bottom: 0.25rem; text-transform: uppercase; letter-spacing: 0.08em; }
|
||||||
select, textarea, button {
|
select, textarea, button {
|
||||||
@@ -190,15 +233,15 @@
|
|||||||
.fab .material-symbols-outlined { font-size: 28px; }
|
.fab .material-symbols-outlined { font-size: 28px; }
|
||||||
.fab:hover:not(:disabled) { transform: scale(1.06); }
|
.fab:hover:not(:disabled) { transform: scale(1.06); }
|
||||||
.fab:disabled { opacity: 0.55; cursor: progress; }
|
.fab:disabled { opacity: 0.55; cursor: progress; }
|
||||||
/* --kb-inset is set from JS based on VisualViewport so the FABs ride
|
/* Android Chrome shrinks the layout viewport when the soft keyboard
|
||||||
above the on-screen keyboard. When there's no keyboard it's 0 and the
|
opens (via `interactive-widget=resizes-content` on the viewport meta),
|
||||||
safe-area inset wins via max(). */
|
so plain `bottom: 1.5rem` is already above the keyboard. */
|
||||||
.fab-primary {
|
.fab-primary {
|
||||||
bottom: calc(1.5rem + max(var(--kb-inset, 0px), env(safe-area-inset-bottom, 0px)));
|
bottom: calc(1.5rem + env(safe-area-inset-bottom, 0px));
|
||||||
}
|
}
|
||||||
.fab-secondary {
|
.fab-secondary {
|
||||||
/* sits above the primary: 56 (primary height) + 0.75rem gap + base offset */
|
/* sits above the primary: 56 (primary height) + 0.75rem gap + base offset */
|
||||||
bottom: calc(1.5rem + 56px + 0.75rem + max(var(--kb-inset, 0px), env(safe-area-inset-bottom, 0px)));
|
bottom: calc(1.5rem + 56px + 0.75rem + env(safe-area-inset-bottom, 0px));
|
||||||
}
|
}
|
||||||
.fab.busy {
|
.fab.busy {
|
||||||
animation: fab-pulse 1.4s ease-in-out infinite;
|
animation: fab-pulse 1.4s ease-in-out infinite;
|
||||||
@@ -389,6 +432,13 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<details class="kebab">
|
||||||
|
<summary aria-label="Settings" title="Settings"><span class="material-symbols-outlined">more_vert</span></summary>
|
||||||
|
<div class="kebab-panel">
|
||||||
|
<label for="model">Model</label>
|
||||||
|
<select id="model"></select>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<h1>finrod</h1>
|
<h1>finrod</h1>
|
||||||
<div class="sub">Translation via Ollama · loremaster of tongues</div>
|
<div class="sub">Translation via Ollama · loremaster of tongues</div>
|
||||||
@@ -402,10 +452,6 @@
|
|||||||
<label for="target">To</label>
|
<label for="target">To</label>
|
||||||
<select id="target"></select>
|
<select id="target"></select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label for="model">Model</label>
|
|
||||||
<select id="model"></select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="hot-status" class="hot-status idle">
|
<div id="hot-status" class="hot-status idle">
|
||||||
@@ -906,7 +952,7 @@
|
|||||||
o2.value = lang; o2.textContent = lang;
|
o2.value = lang; o2.textContent = lang;
|
||||||
targetSel.appendChild(o2);
|
targetSel.appendChild(o2);
|
||||||
}
|
}
|
||||||
sourceSel.value = "auto";
|
sourceSel.value = "Dutch";
|
||||||
targetSel.value = "English";
|
targetSel.value = "English";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1143,26 +1189,6 @@
|
|||||||
Vim.mapCommand("L", "action", "finrodFocusNext", {}, { context: "normal" });
|
Vim.mapCommand("L", "action", "finrodFocusNext", {}, { context: "normal" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep the FABs floating above the on-screen keyboard when it's open.
|
|
||||||
// On Chrome+Android the `interactive-widget=resizes-content` meta directive
|
|
||||||
// already shrinks the layout viewport — kb-inset stays 0 there.
|
|
||||||
// On iOS Safari (which ignores the meta) the layout viewport stays full
|
|
||||||
// and we compute the hidden bottom strip from VisualViewport.
|
|
||||||
function updateKbInset() {
|
|
||||||
const vv = window.visualViewport;
|
|
||||||
if (!vv) {
|
|
||||||
document.documentElement.style.setProperty("--kb-inset", "0px");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const inset = Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
|
|
||||||
document.documentElement.style.setProperty("--kb-inset", inset + "px");
|
|
||||||
}
|
|
||||||
if (window.visualViewport) {
|
|
||||||
window.visualViewport.addEventListener("resize", updateKbInset);
|
|
||||||
window.visualViewport.addEventListener("scroll", updateKbInset);
|
|
||||||
}
|
|
||||||
updateKbInset();
|
|
||||||
|
|
||||||
fillLangs();
|
fillLangs();
|
||||||
loadModels().then(() => refreshLoaded(true));
|
loadModels().then(() => refreshLoaded(true));
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user