web: make the board usable on a phone, not just reachable
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 8s
CI & Build / Python tests (push) Successful in 34s
CI & Build / Build & push image (push) Successful in 50s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m13s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m6s
Desktop (Tauri) / Update manifest (push) Successful in 6s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 8s
CI & Build / Python tests (push) Successful in 34s
CI & Build / Build & push image (push) Successful in 50s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m13s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m6s
Desktop (Tauri) / Update manifest (push) Successful in 6s
The controls a card carries were always-visible overlays on a touch device — correct as far as it went (task 2697: a finger cannot hover, and the pill is the only way to pin or archive), but they were still absolutely positioned, so they sat ON the note's own title. A card reading "thought sync tauri app" rendered as "ught sync tauri app" with the grip parked over the first three characters, and the four-icon pill covering the right half of the first line. Placement is now CSS's decision. One element each, two placements: where a pointer can hover they lift out of flow into the floating top-corner pills they have always been; where nothing can hover they stay in flow as a footer row, which cannot overlap anything by construction. Keyed on hover rather than width, for the same reason `.hover-reveal` already is — a narrow window on a laptop still hovers, a wide tablet still doesn't. The colour popover moved inside the action set so it follows it, and opens into the card from either end. The header was sharing one phone-width row between a menu button, the logo, the lens name, a search field and four icons; everything in it was truncated, the lens down to "N…" and the search box to an empty pill. It wraps now, so search takes its own line below sm, and account / settings / sign-out move into the drawer where there is room to name them rather than guess at a glyph. One input, moved by CSS — duplicating it would have meant two `searchInput` refs and a `/` shortcut that focuses the wrong one. Also closes the other half of task 2706, which was waiting on a device to look at: `viewport-fit=cover` together with the `env(safe-area-inset-*)` padding that makes it safe (sides on body, top on the sticky header, bottom on the board and the drawer), and `100dvh` behind an @supports so the app box follows the visual viewport when the keyboard opens instead of the layout viewport. Both halves in one change, as that task insisted. And the composer no longer tells a phone to "Press Enter".
This commit is contained in:
@@ -186,27 +186,6 @@ onBeforeUnmount(() => document.removeEventListener("mousedown", onDocMousedown))
|
||||
]"
|
||||
:data-note-id="note.id"
|
||||
>
|
||||
<!-- Drag handle: reorder is gated behind this grip so a normal click or tap
|
||||
never starts a drag. Board views only (reorderable). Still a pointer-only
|
||||
affordance — there is no keyboard equivalent, hence tabindex="-1".
|
||||
`touch-none` hands the gesture to us instead of the browser's scrolling,
|
||||
and `hover-reveal` keeps the grip on screen where hovering is impossible
|
||||
(it is now reachable there, which it was not under native drag-and-drop). -->
|
||||
<button
|
||||
v-if="canDrag()"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="hover-reveal pointer-events-none absolute left-1.5 top-1.5 z-10 flex touch-none cursor-grab items-center rounded-full bg-white/85 p-1 text-neutral-500 opacity-0 shadow-sm ring-1 ring-black/5 backdrop-blur-sm transition hover:text-neutral-800 active:cursor-grabbing group-hover:pointer-events-auto group-hover:opacity-100 dark:bg-neutral-900/85 dark:text-neutral-400 dark:ring-white/10 dark:hover:text-neutral-100"
|
||||
title="Drag to reorder"
|
||||
aria-label="Drag to reorder"
|
||||
@pointerdown="onGripDown"
|
||||
@pointermove="onGripMove"
|
||||
@pointerup="onGripUp"
|
||||
@pointercancel="onGripCancel"
|
||||
>
|
||||
<Icon name="grip" />
|
||||
</button>
|
||||
|
||||
<img
|
||||
v-if="firstImage"
|
||||
:src="firstImage.url"
|
||||
@@ -342,88 +321,114 @@ onBeforeUnmount(() => document.removeEventListener("mousedown", onDocMousedown))
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 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.
|
||||
`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="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)">
|
||||
<Icon name="restore" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
title="Delete forever"
|
||||
aria-label="Delete forever"
|
||||
@click="notes.deleteForever(note.id)"
|
||||
>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
title="Change color"
|
||||
aria-label="Change color"
|
||||
@click.stop="colorOpen = !colorOpen"
|
||||
>
|
||||
<span
|
||||
class="h-4 w-4 rounded-full border border-black/10 dark:border-white/20"
|
||||
:class="swatch(note.color)"
|
||||
></span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
:class="note.pinned ? 'text-brand-700 dark:text-brand' : ''"
|
||||
:title="note.pinned ? 'Unpin' : 'Pin'"
|
||||
:aria-label="note.pinned ? 'Unpin' : 'Pin'"
|
||||
:aria-pressed="note.pinned"
|
||||
@click="notes.setPinned(note.id, !note.pinned)"
|
||||
>
|
||||
<Icon name="pin" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
:title="note.archived ? 'Unarchive' : 'Archive'"
|
||||
:aria-label="note.archived ? 'Unarchive' : 'Archive'"
|
||||
@click="notes.setArchived(note.id, !note.archived)"
|
||||
>
|
||||
<Icon name="archive" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
title="Move to trash"
|
||||
aria-label="Move to trash"
|
||||
@click="notes.trash(note.id)"
|
||||
>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="colorOpen"
|
||||
class="absolute right-1.5 top-11 z-20 flex w-40 flex-wrap gap-1.5 rounded-lg border border-neutral-200 bg-white p-2 shadow-lg dark:border-neutral-700 dark:bg-neutral-800"
|
||||
>
|
||||
<!-- The card's controls: the drag grip, then the action set.
|
||||
Placement is CSS's job, not the markup's — `.note-actions` in style.css puts
|
||||
them where the device can actually use them. Where a pointer hovers they lift
|
||||
out of flow into the floating top-corner pills they have always been; where
|
||||
nothing can hover they stay here, in flow, as a footer row. The alternative
|
||||
(a permanently visible overlay) sat on top of the note's own title, which is
|
||||
what a phone was showing.
|
||||
`hover-reveal` still handles the visibility half — see style.css. -->
|
||||
<div class="note-actions">
|
||||
<!-- Reorder is gated behind this grip so a normal click or tap never starts a
|
||||
drag. Board views only (reorderable). A pointer-only affordance — there is
|
||||
no keyboard equivalent, hence tabindex="-1". `touch-none` hands the gesture
|
||||
to us instead of the browser's scrolling. -->
|
||||
<button
|
||||
v-for="key in NOTE_COLOR_KEYS"
|
||||
:key="key"
|
||||
v-if="canDrag()"
|
||||
type="button"
|
||||
:title="NOTE_COLOR_LABELS[key]"
|
||||
:aria-label="NOTE_COLOR_LABELS[key]"
|
||||
class="h-6 w-6 rounded-full border border-black/10 transition hover:scale-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand"
|
||||
:class="[NOTE_SWATCH_CLASSES[key], note.color === key ? 'ring-2 ring-brand' : '']"
|
||||
@click.stop="pickColor(key)"
|
||||
/>
|
||||
tabindex="-1"
|
||||
class="note-grip hover-reveal pointer-events-none flex touch-none cursor-grab items-center rounded-full bg-white/85 p-1 text-neutral-500 opacity-0 shadow-sm ring-1 ring-black/5 backdrop-blur-sm transition hover:text-neutral-800 active:cursor-grabbing group-hover:pointer-events-auto group-hover:opacity-100 dark:bg-neutral-900/85 dark:text-neutral-400 dark:ring-white/10 dark:hover:text-neutral-100"
|
||||
title="Drag to reorder"
|
||||
aria-label="Drag to reorder"
|
||||
@pointerdown="onGripDown"
|
||||
@pointermove="onGripMove"
|
||||
@pointerup="onGripUp"
|
||||
@pointercancel="onGripCancel"
|
||||
>
|
||||
<Icon name="grip" />
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="note-actions-set hover-reveal pointer-events-none 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)">
|
||||
<Icon name="restore" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
title="Delete forever"
|
||||
aria-label="Delete forever"
|
||||
@click="notes.deleteForever(note.id)"
|
||||
>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
title="Change color"
|
||||
aria-label="Change color"
|
||||
:aria-expanded="colorOpen"
|
||||
@click.stop="colorOpen = !colorOpen"
|
||||
>
|
||||
<span
|
||||
class="h-4 w-4 rounded-full border border-black/10 dark:border-white/20"
|
||||
:class="swatch(note.color)"
|
||||
></span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
:class="note.pinned ? 'text-brand-700 dark:text-brand' : ''"
|
||||
:title="note.pinned ? 'Unpin' : 'Pin'"
|
||||
:aria-label="note.pinned ? 'Unpin' : 'Pin'"
|
||||
:aria-pressed="note.pinned"
|
||||
@click="notes.setPinned(note.id, !note.pinned)"
|
||||
>
|
||||
<Icon name="pin" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
:title="note.archived ? 'Unarchive' : 'Archive'"
|
||||
:aria-label="note.archived ? 'Unarchive' : 'Archive'"
|
||||
@click="notes.setArchived(note.id, !note.archived)"
|
||||
>
|
||||
<Icon name="archive" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
title="Move to trash"
|
||||
aria-label="Move to trash"
|
||||
@click="notes.trash(note.id)"
|
||||
>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<!-- Inside the action set rather than beside it, so it follows the set to
|
||||
whichever corner or footer the device put it in. -->
|
||||
<div
|
||||
v-if="colorOpen"
|
||||
class="note-swatches flex w-40 flex-wrap gap-1.5 rounded-lg border border-neutral-200 bg-white p-2 shadow-lg dark:border-neutral-700 dark:bg-neutral-800"
|
||||
>
|
||||
<button
|
||||
v-for="key in NOTE_COLOR_KEYS"
|
||||
:key="key"
|
||||
type="button"
|
||||
:title="NOTE_COLOR_LABELS[key]"
|
||||
:aria-label="NOTE_COLOR_LABELS[key]"
|
||||
class="h-6 w-6 rounded-full border border-black/10 transition hover:scale-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand"
|
||||
:class="[NOTE_SWATCH_CLASSES[key], note.color === key ? 'ring-2 ring-brand' : '']"
|
||||
@click.stop="pickColor(key)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user