frontend: hover-revealed controls stay put where hovering is impossible (task 2697)
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 10s
CI & Build / Build & push image (push) Successful in 29s
Desktop (Tauri) / Tauri desktop (Linux) (push) Canceled after 2m36s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Canceled after 2m36s
Desktop (Tauri) / Update manifest (push) Canceled after 0s
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 10s
CI & Build / Build & push image (push) Successful in 29s
Desktop (Tauri) / Tauri desktop (Linux) (push) Canceled after 2m36s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Canceled after 2m36s
Desktop (Tauri) / Update manifest (push) Canceled after 0s
A finger cannot hover, and on a note card the hover toolbar is the only way to pin, colour or archive — so on a phone those notes could not be acted on at all. Same for deleting a checklist item, a saved view, an attachment or a preview. Marked rather than rewritten inline: one `.hover-reveal` class on the five elements and a single rule that says what it is for. The `group-hover:` reveal stays in the markup because the trigger differs per component (named groups); only the fallback is shared. `@media (hover: none)` asks the device directly, which is more honest than inferring from viewport width — a narrow window on a laptop still hovers, and a large tablet still doesn't. It sits after the Tailwind directives so it beats the opacity-0/pointer-events-none utilities on source order without !important. Tap targets follow the same shape: p-1.5 around an 18px icon lands near 30px, which is fine for a cursor and too small for a thumb. Bumped to 44px on coarse pointers only, so desktop chrome doesn't inflate. The drag grip is deliberately NOT revealed yet. Reordering still uses HTML5 drag-and-drop, which never fires from touch, so showing the handle would only promise something that does nothing. It comes with the pointer-events rewrite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -388,7 +388,7 @@ async function signOut() {
|
||||
<span class="flex-1 truncate">{{ f.name }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="text-neutral-400 opacity-0 hover:text-red-500 group-hover/view:opacity-100"
|
||||
class="hover-reveal text-neutral-400 opacity-0 hover:text-red-500 group-hover/view:opacity-100"
|
||||
:aria-label="`Delete view ${f.name}`"
|
||||
@click.prevent.stop="removeView(f)"
|
||||
>
|
||||
|
||||
@@ -36,7 +36,7 @@ defineEmits<{ (e: "remove"): void }>();
|
||||
<button
|
||||
v-if="removable"
|
||||
type="button"
|
||||
class="absolute right-1 top-1 rounded-full bg-black/50 px-1.5 text-white opacity-0 transition group-hover/lp:opacity-100"
|
||||
class="hover-reveal absolute right-1 top-1 rounded-full bg-black/50 px-1.5 text-white opacity-0 transition group-hover/lp:opacity-100"
|
||||
aria-label="Remove preview"
|
||||
@click="$emit('remove')"
|
||||
>
|
||||
|
||||
@@ -277,9 +277,11 @@ onBeforeUnmount(() => document.removeEventListener("mousedown", onDocMousedown))
|
||||
<!-- Toolbar overlays the card's top-right on hover/focus as a floating pill
|
||||
(window-control style) instead of reserving a permanent row — so at rest
|
||||
the card is content-sized with even padding, not text pinned to the top
|
||||
above an empty strip. -->
|
||||
above an empty strip.
|
||||
`hover-reveal`: on a device that can't hover this is the ONLY way to pin,
|
||||
colour or archive a note, so there it stays visible (see style.css). -->
|
||||
<div
|
||||
class="pointer-events-none absolute right-1.5 top-1.5 flex items-center gap-0.5 rounded-full bg-white/85 p-0.5 opacity-0 shadow-sm ring-1 ring-black/5 backdrop-blur-sm transition focus-within:pointer-events-auto focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100 dark:bg-neutral-900/85 dark:ring-white/10"
|
||||
class="hover-reveal pointer-events-none absolute right-1.5 top-1.5 flex items-center gap-0.5 rounded-full bg-white/85 p-0.5 opacity-0 shadow-sm ring-1 ring-black/5 backdrop-blur-sm transition focus-within:pointer-events-auto focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100 dark:bg-neutral-900/85 dark:ring-white/10"
|
||||
>
|
||||
<template v-if="note.trashed">
|
||||
<button type="button" class="icon-btn" title="Restore" aria-label="Restore" @click="notes.restore(note.id)">
|
||||
|
||||
@@ -52,7 +52,7 @@ function remove(item: ChecklistItem) {
|
||||
<button
|
||||
v-if="editable"
|
||||
type="button"
|
||||
class="text-neutral-300 opacity-0 hover:text-neutral-600 group-hover/item:opacity-100 dark:hover:text-neutral-200"
|
||||
class="hover-reveal text-neutral-300 opacity-0 hover:text-neutral-600 group-hover/item:opacity-100 dark:hover:text-neutral-200"
|
||||
aria-label="Delete item"
|
||||
@click="remove(item)"
|
||||
>
|
||||
|
||||
@@ -558,7 +558,7 @@ function revPreview(rev: NoteRevision): string {
|
||||
<img :src="att.url" alt="" loading="lazy" decoding="async" class="h-24 w-24 rounded-lg object-cover" />
|
||||
<button
|
||||
type="button"
|
||||
class="absolute right-1 top-1 rounded-full bg-black/50 px-1.5 text-white opacity-0 transition group-hover/att:opacity-100"
|
||||
class="hover-reveal absolute right-1 top-1 rounded-full bg-black/50 px-1.5 text-white opacity-0 transition group-hover/att:opacity-100"
|
||||
aria-label="Remove attachment"
|
||||
@click="notes.deleteAttachment(liveNote.id, att.id)"
|
||||
>
|
||||
|
||||
@@ -39,11 +39,42 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/* Controls that a card or row reveals on hover — the note toolbar, the drag grip,
|
||||
* per-item delete buttons. Marked with this class rather than expressed inline,
|
||||
* because the rule below is the whole reason they need naming.
|
||||
*
|
||||
* A finger cannot hover. On a device with no hover at all these are not "harder to
|
||||
* reach", they are unreachable — and for a note card the hover toolbar is the ONLY
|
||||
* way to pin, colour or archive. So where hovering is impossible they are simply
|
||||
* always present. `@media (hover: none)` asks the device directly, which is more
|
||||
* honest than inferring it from viewport width: a small window on a laptop still
|
||||
* hovers, and a large tablet still doesn't.
|
||||
*
|
||||
* Placed after the Tailwind directives so it wins on source order against the
|
||||
* `opacity-0` / `pointer-events-none` utilities it overrides, without !important.
|
||||
* The `group-hover:` reveal stays in the markup — the trigger differs per component
|
||||
* (named groups), only this fallback is shared. */
|
||||
@media (hover: none) {
|
||||
.hover-reveal {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.icon-btn {
|
||||
@apply rounded-md p-1.5 text-neutral-500 transition hover:bg-black/5 focus:outline-none
|
||||
focus-visible:ring-2 focus-visible:ring-brand dark:text-neutral-400 dark:hover:bg-white/10;
|
||||
}
|
||||
/* Finger-sized where a finger is the input. p-1.5 around an 18px icon lands near
|
||||
* 30px — comfortable for a cursor, too small for a thumb (Android asks 48dp,
|
||||
* WCAG 2.2 AAA asks 44px). Scoped to coarse pointers so desktop chrome doesn't
|
||||
* inflate; inline-flex because a bare min-height would leave the icon off-centre. */
|
||||
@media (pointer: coarse) {
|
||||
.icon-btn {
|
||||
@apply inline-flex min-h-[2.75rem] min-w-[2.75rem] items-center justify-center;
|
||||
}
|
||||
}
|
||||
.nav-link {
|
||||
@apply flex items-center gap-2 rounded-lg px-3 py-2 font-medium text-neutral-600 transition
|
||||
hover:bg-neutral-200/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand
|
||||
|
||||
Reference in New Issue
Block a user