feat(ux): translate becomes a primary FAB stacked under camera
Promotes Translate from an inline button under the input pane to the primary 56x56 FAB at the bottom-right corner. Camera FAB becomes the secondary in the stack, just above. Both share a single .fab base class; positioning via .fab-primary / .fab-secondary. - Translate icon: Material Symbols `translate` (28 px). - Pulsing-ring `busy` animation while a translation runs, since the icon-only button can't show "Translating…" text anymore. - body padding-bottom bumped to ~9 rem so the output pane scrolls clear of the stacked FABs. - ⌘/Ctrl-Enter still triggers translate via the existing CodeMirror keymap; tooltip text updated.
This commit is contained in:
@@ -16,6 +16,7 @@ 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.
|
||||||
- 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).
|
||||||
|
|||||||
@@ -34,7 +34,8 @@
|
|||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 2rem 1rem;
|
/* extra bottom padding so content scrolls clear of the stacked FABs */
|
||||||
|
padding: 2rem 1rem calc(9rem + env(safe-area-inset-bottom, 0px)) 1rem;
|
||||||
}
|
}
|
||||||
.wrap { max-width: 960px; margin: 0 auto; }
|
.wrap { max-width: 960px; margin: 0 auto; }
|
||||||
h1 {
|
h1 {
|
||||||
@@ -165,10 +166,10 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Floating action button for OCR scan — bottom-right, mobile-thumb friendly */
|
/* Floating action buttons — stacked at bottom-right.
|
||||||
.scan-fab {
|
Translate is the primary action (closest to the thumb), Scan sits above. */
|
||||||
|
.fab {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: calc(1.5rem + env(safe-area-inset-bottom, 0px));
|
|
||||||
right: calc(1.5rem + env(safe-area-inset-right, 0px));
|
right: calc(1.5rem + env(safe-area-inset-right, 0px));
|
||||||
width: 56px;
|
width: 56px;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
@@ -184,11 +185,25 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: transform 0.15s;
|
transition: transform 0.15s, box-shadow 0.15s;
|
||||||
|
}
|
||||||
|
.fab .material-symbols-outlined { font-size: 28px; }
|
||||||
|
.fab:hover:not(:disabled) { transform: scale(1.06); }
|
||||||
|
.fab:disabled { opacity: 0.55; cursor: progress; }
|
||||||
|
.fab-primary {
|
||||||
|
bottom: calc(1.5rem + env(safe-area-inset-bottom, 0px));
|
||||||
|
}
|
||||||
|
.fab-secondary {
|
||||||
|
/* sits above the primary: 56 (primary height) + 0.75rem gap + base offset */
|
||||||
|
bottom: calc(1.5rem + 56px + 0.75rem + env(safe-area-inset-bottom, 0px));
|
||||||
|
}
|
||||||
|
.fab.busy {
|
||||||
|
animation: fab-pulse 1.4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes fab-pulse {
|
||||||
|
0%, 100% { box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35); }
|
||||||
|
50% { box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35), 0 0 0 8px rgba(224, 122, 62, 0.35); }
|
||||||
}
|
}
|
||||||
.scan-fab .material-symbols-outlined { font-size: 28px; }
|
|
||||||
.scan-fab:hover:not(:disabled) { transform: scale(1.06); }
|
|
||||||
.scan-fab:disabled { opacity: 0.6; cursor: progress; }
|
|
||||||
.scan-status {
|
.scan-status {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
@@ -408,13 +423,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="go">Translate</button>
|
|
||||||
<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 type="button" id="scan-btn" class="fab fab-secondary" aria-label="Scan image (OCR)" title="Scan image (OCR)">
|
||||||
<span class="material-symbols-outlined">photo_camera</span>
|
<span class="material-symbols-outlined">photo_camera</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" id="go" class="fab fab-primary" aria-label="Translate" title="Translate (⌘/Ctrl + Enter)">
|
||||||
|
<span class="material-symbols-outlined">translate</span>
|
||||||
|
</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";
|
||||||
@@ -935,7 +952,7 @@
|
|||||||
const text = inputText().trim();
|
const text = inputText().trim();
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
goBtn.disabled = true;
|
goBtn.disabled = true;
|
||||||
goBtn.textContent = "Translating…";
|
goBtn.classList.add("busy");
|
||||||
output.textContent = "";
|
output.textContent = "";
|
||||||
output.classList.add("streaming");
|
output.classList.add("streaming");
|
||||||
|
|
||||||
@@ -1007,7 +1024,7 @@
|
|||||||
} finally {
|
} finally {
|
||||||
output.classList.remove("streaming");
|
output.classList.remove("streaming");
|
||||||
goBtn.disabled = false;
|
goBtn.disabled = false;
|
||||||
goBtn.textContent = "Translate";
|
goBtn.classList.remove("busy");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user