v0.3.0: OCR via camera, FAB UX, side drawer #4
@@ -19,7 +19,7 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
- "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 via `interactive-widget=resizes-content` in the viewport meta tag (handled by Chrome on Android).
|
- 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).
|
- 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.
|
- Model selector moved into a slide-out side drawer (hamburger menu in the top-left). Drawer opens from the left with a backdrop, closes via the X button, backdrop click, or `Esc`. Honours Android safe-area insets.
|
||||||
- Default source language is now Dutch (was Auto-detect). Target stays English.
|
- 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.
|
||||||
|
|||||||
@@ -51,44 +51,94 @@
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Kebab menu (top-left) housing the Model dropdown — keeps the main
|
/* Hamburger menu (top-left) → slide-out side drawer that houses the
|
||||||
controls focused on the From/To pair. */
|
Model selector (and future per-app settings). */
|
||||||
.kebab {
|
.drawer-toggle {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: calc(1rem + env(safe-area-inset-top, 0px));
|
top: calc(1rem + env(safe-area-inset-top, 0px));
|
||||||
left: calc(1rem + env(safe-area-inset-left, 0px));
|
left: calc(1rem + env(safe-area-inset-left, 0px));
|
||||||
z-index: 50;
|
|
||||||
}
|
|
||||||
.kebab > summary {
|
|
||||||
list-style: none;
|
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
|
z-index: 50;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
|
||||||
}
|
}
|
||||||
.kebab > summary::-webkit-details-marker { display: none; }
|
.drawer-toggle .material-symbols-outlined { font-size: 22px; }
|
||||||
.kebab > summary::marker { display: none; }
|
|
||||||
.kebab > summary .material-symbols-outlined { font-size: 22px; }
|
.drawer-backdrop {
|
||||||
.kebab[open] > summary { border-color: var(--accent); color: var(--accent); }
|
position: fixed;
|
||||||
.kebab-panel {
|
inset: 0;
|
||||||
position: absolute;
|
background: rgba(0, 0, 0, 0.55);
|
||||||
top: calc(100% + 0.5rem);
|
opacity: 0;
|
||||||
left: 0;
|
pointer-events: none;
|
||||||
background: var(--bg);
|
transition: opacity 0.2s;
|
||||||
border: 1px solid var(--border);
|
z-index: 200;
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0.75rem;
|
|
||||||
min-width: 220px;
|
|
||||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
}
|
||||||
.kebab-panel label {
|
.drawer-backdrop.open { opacity: 1; pointer-events: auto; }
|
||||||
|
|
||||||
|
.drawer {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: min(85vw, 320px);
|
||||||
|
background: var(--bg);
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
padding:
|
||||||
|
max(1rem, env(safe-area-inset-top, 0px))
|
||||||
|
1.25rem
|
||||||
|
max(1rem, env(safe-area-inset-bottom, 0px))
|
||||||
|
max(1.25rem, env(safe-area-inset-left, 0px));
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform 0.25s ease;
|
||||||
|
z-index: 201;
|
||||||
|
box-shadow: 4px 0 14px rgba(0, 0, 0, 0.3);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.drawer.open { transform: translateX(0); }
|
||||||
|
.drawer-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.drawer-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: var(--muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.drawer-close {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--muted);
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.drawer-close:hover { color: var(--fg); background: var(--panel); }
|
||||||
|
.drawer-close .material-symbols-outlined { font-size: 20px; }
|
||||||
|
.drawer label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
@@ -432,13 +482,22 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<details class="kebab">
|
<button type="button" id="drawer-toggle" class="drawer-toggle" aria-label="Open menu" title="Menu">
|
||||||
<summary aria-label="Settings" title="Settings"><span class="material-symbols-outlined">more_vert</span></summary>
|
<span class="material-symbols-outlined">menu</span>
|
||||||
<div class="kebab-panel">
|
</button>
|
||||||
|
<div id="drawer-backdrop" class="drawer-backdrop"></div>
|
||||||
|
<aside id="drawer" class="drawer" aria-hidden="true">
|
||||||
|
<div class="drawer-header">
|
||||||
|
<h2>Settings</h2>
|
||||||
|
<button type="button" id="drawer-close" class="drawer-close" aria-label="Close menu" title="Close">
|
||||||
|
<span class="material-symbols-outlined">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
<label for="model">Model</label>
|
<label for="model">Model</label>
|
||||||
<select id="model"></select>
|
<select id="model"></select>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</aside>
|
||||||
<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>
|
||||||
@@ -1189,6 +1248,28 @@
|
|||||||
Vim.mapCommand("L", "action", "finrodFocusNext", {}, { context: "normal" });
|
Vim.mapCommand("L", "action", "finrodFocusNext", {}, { context: "normal" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Side drawer (Settings) wiring — opens from the top-left hamburger.
|
||||||
|
const drawerEl = document.getElementById("drawer");
|
||||||
|
const drawerBackdrop = document.getElementById("drawer-backdrop");
|
||||||
|
const drawerToggle = document.getElementById("drawer-toggle");
|
||||||
|
const drawerCloseBtn = document.getElementById("drawer-close");
|
||||||
|
function openDrawer() {
|
||||||
|
drawerEl.classList.add("open");
|
||||||
|
drawerBackdrop.classList.add("open");
|
||||||
|
drawerEl.setAttribute("aria-hidden", "false");
|
||||||
|
}
|
||||||
|
function closeDrawer() {
|
||||||
|
drawerEl.classList.remove("open");
|
||||||
|
drawerBackdrop.classList.remove("open");
|
||||||
|
drawerEl.setAttribute("aria-hidden", "true");
|
||||||
|
}
|
||||||
|
drawerToggle.addEventListener("click", openDrawer);
|
||||||
|
drawerCloseBtn.addEventListener("click", closeDrawer);
|
||||||
|
drawerBackdrop.addEventListener("click", closeDrawer);
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Escape" && drawerEl.classList.contains("open")) closeDrawer();
|
||||||
|
});
|
||||||
|
|
||||||
fillLangs();
|
fillLangs();
|
||||||
loadModels().then(() => refreshLoaded(true));
|
loadModels().then(() => refreshLoaded(true));
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user