fix(ui): panels stay above the keyboard, the day two offers share is listed once
The date field would not open at all on some days: on a Friday "Friday" and "In a week" resolve to the same day, and the option list was keyed by the day it writes, so Svelte tore it down with each_key_duplicate. On a desktop this took the whole view with it - the same bug, not two. Duplicate offers are dropped (the more specific name wins) and the list is keyed by position, so no caller can tear it down that way again. Panels are placed against the visual viewport rather than the window, which a phone's keyboard shrinks without resizing anything: they scroll inside what is left, re-place as the keyboard animates in, and come back into view when the keyboard covers the very row they hang off - visible beats staying next to the anchor. The quick look centres the same way. Its actions are thumb-sized where the pointer is coarse, with the keycaps hidden - there is no keyboard to press them on - and it gains "no date" and a close button, since a phone has no Escape. Both menus gain a plain "Edit" beside "Edit in Tasks"; it was only ever reachable by pressing e.
This commit is contained in:
1 parent
9c0e57e29b
commit
b70331f7bd
8 files changed
+208
-26
No files matched your search
+38
-8
@@ -14,7 +14,7 @@
|
||||
import { dueLabel, dueTone, fromKey } from "../lib/due";
|
||||
import { labelHue } from "../lib/hue";
|
||||
import type { Action } from "../lib/keymap";
|
||||
import { onScreen } from "../lib/place";
|
||||
import { onScreen, viewport } from "../lib/place";
|
||||
import type { Box } from "../lib/place";
|
||||
import { cn } from "../lib/utils";
|
||||
import { PRIORITY_LABEL, STATUS_META } from "../lib/vocab";
|
||||
@@ -63,6 +63,8 @@
|
||||
label: string;
|
||||
}
|
||||
|
||||
// Every row is a button first and a key second: on a phone the keys are not
|
||||
// there to be pressed, so the lens has to be operable by thumb alone.
|
||||
const rows = $derived<Row[]>(
|
||||
task
|
||||
? [
|
||||
@@ -72,6 +74,7 @@
|
||||
{ action: "taskBack", keys: ["⇧h"], label: "−day" },
|
||||
{ action: "taskForward", keys: ["⇧l"], label: "+day" },
|
||||
{ action: "taskToday", keys: ["⇧t"], label: "today" },
|
||||
{ action: "taskUnschedule", keys: ["⇧x"], label: "no date" },
|
||||
{ action: "taskOpen", keys: ["⏎"], label: "open note" },
|
||||
]
|
||||
: [],
|
||||
@@ -85,13 +88,23 @@
|
||||
* the middle, the way a window feels centred rather than measures it.
|
||||
*/
|
||||
function centre(node: HTMLElement): void {
|
||||
const box = view.rootBox() ?? new DOMRect(0, 0, window.innerWidth, window.innerHeight);
|
||||
const win = node.ownerDocument.defaultView ?? window;
|
||||
const area = viewport(win);
|
||||
const box =
|
||||
view.rootBox() ??
|
||||
new DOMRect(area.left ?? 0, area.top ?? 0, area.width, area.height);
|
||||
const width = node.offsetWidth;
|
||||
const height = node.offsetHeight;
|
||||
const left = box.left + (box.width - width) / 2;
|
||||
const top = box.top + (box.height - height) * 0.42;
|
||||
node.style.left = `${Math.max(8, Math.min(left, window.innerWidth - width - 8))}px`;
|
||||
node.style.top = `${Math.max(8, Math.min(top, window.innerHeight - height - 8))}px`;
|
||||
// Kept inside what is actually visible: a phone's keyboard shrinks the
|
||||
// viewport without touching the window, and the lens must stay reachable.
|
||||
const minLeft = (area.left ?? 0) + 8;
|
||||
const maxLeft = (area.left ?? 0) + area.width - width - 8;
|
||||
const minTop = (area.top ?? 0) + 8;
|
||||
const maxTop = (area.top ?? 0) + area.height - height - 8;
|
||||
node.style.left = `${Math.max(minLeft, Math.min(left, Math.max(minLeft, maxLeft)))}px`;
|
||||
node.style.top = `${Math.max(minTop, Math.min(top, Math.max(minTop, maxTop)))}px`;
|
||||
}
|
||||
|
||||
/** The row or card under the cursor, wherever the current view draws it. */
|
||||
@@ -151,6 +164,11 @@
|
||||
watcher?.observe(node);
|
||||
const reflow = (): void => centre(node);
|
||||
window.addEventListener("resize", reflow);
|
||||
// A keyboard (an editor opening behind the lens) moves the visible area
|
||||
// without resizing the window.
|
||||
const vv = (node.ownerDocument.defaultView ?? window).visualViewport;
|
||||
vv?.addEventListener("resize", reflow);
|
||||
vv?.addEventListener("scroll", reflow);
|
||||
|
||||
// A click on another task moves the lens rather than closing it; a click
|
||||
// anywhere else puts it away.
|
||||
@@ -166,6 +184,8 @@
|
||||
destroy() {
|
||||
watcher?.disconnect();
|
||||
window.removeEventListener("resize", reflow);
|
||||
vv?.removeEventListener("resize", reflow);
|
||||
vv?.removeEventListener("scroll", reflow);
|
||||
document.removeEventListener("pointerdown", outside, true);
|
||||
// Only take the keyboard back if the lens had it; an editor that has
|
||||
// just opened keeps its caret.
|
||||
@@ -348,23 +368,33 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-x-0.5 gap-y-0.5 border-border/70 border-t bg-secondary/25 px-2 py-1.5 text-[11px] text-muted-foreground"
|
||||
class="flex flex-wrap items-center gap-x-0.5 gap-y-0.5 border-border/70 border-t bg-secondary/25 px-2 py-1.5 text-[11px] text-muted-foreground pointer-coarse:gap-x-1.5 pointer-coarse:gap-y-1.5 pointer-coarse:p-2"
|
||||
>
|
||||
{#each rows as row (row.action)}
|
||||
<button
|
||||
class="inline-flex h-6 items-center gap-1 whitespace-nowrap rounded-md px-1.5 outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
|
||||
class="inline-flex h-6 items-center gap-1 whitespace-nowrap rounded-md px-1.5 outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring pointer-coarse:h-9 pointer-coarse:border pointer-coarse:border-border pointer-coarse:bg-popover pointer-coarse:px-3 pointer-coarse:text-xs pointer-coarse:text-foreground"
|
||||
onclick={() => act(row.action)}
|
||||
type="button"
|
||||
>
|
||||
{#each row.keys as key (key)}
|
||||
<kbd class="kbd">{key}</kbd>
|
||||
<kbd class="kbd pointer-coarse:hidden">{key}</kbd>
|
||||
{/each}
|
||||
{row.label}
|
||||
</button>
|
||||
{/each}
|
||||
<span class="ml-auto whitespace-nowrap px-1.5">
|
||||
<span class="ml-auto whitespace-nowrap px-1.5 pointer-coarse:hidden">
|
||||
<kbd class="kbd">␣</kbd> close
|
||||
</span>
|
||||
<!-- No Escape key on a phone, so the lens needs a way out by thumb. -->
|
||||
<button
|
||||
class="ml-auto hidden h-9 items-center rounded-md border border-border bg-popover px-3 text-foreground text-xs pointer-coarse:inline-flex"
|
||||
onclick={() => {
|
||||
store.peeking = false;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
|
||||
Reference in new issue
Block a user