From 16f86bef93b10b44545945eca7c6a1f3a102d099 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 21 Aug 2026 21:55:46 -0400 Subject: [PATCH 01/22] web: make the board usable on a phone, not just reachable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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". --- frontend/index.html | 8 +- frontend/src/components/AppShell.vue | 59 ++++++-- frontend/src/components/NoteCard.vue | 207 ++++++++++++++------------- frontend/src/style.css | 101 +++++++++++++ frontend/src/views/BoardView.vue | 19 ++- 5 files changed, 279 insertions(+), 115 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 5727c4e..7a7bb44 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,7 +2,13 @@ - + + Skip to notes +
-
+ +
diff --git a/frontend/src/components/NoteCard.vue b/frontend/src/components/NoteCard.vue index 4370a1b..6f95476 100644 --- a/frontend/src/components/NoteCard.vue +++ b/frontend/src/components/NoteCard.vue @@ -186,27 +186,6 @@ onBeforeUnmount(() => document.removeEventListener("mousedown", onDocMousedown)) ]" :data-note-id="note.id" > - - - document.removeEventListener("mousedown", onDocMousedown))
- -
- - -
- -
+ +
+ + +
+ + + + +
+
+
diff --git a/frontend/src/style.css b/frontend/src/style.css index 576a0d5..4041e69 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -8,6 +8,35 @@ body, height: 100%; } +/* Track the VISUAL viewport, not the layout viewport. + * + * On a phone the two diverge the moment the on-screen keyboard opens or the URL bar + * retracts: `100%` keeps the taller layout viewport, which slides the sticky header + * off screen and can park a focused field underneath the keyboard. `dvh` is the unit + * that follows the box actually being shown. + * + * Same three selectors rather than a fourth rule, so the box model is unchanged and + * only the number moves; behind @supports so a browser without `dvh` keeps the + * `100%` above instead of falling back to nothing. */ +@supports (height: 100dvh) { + html, + body, + #app { + height: 100dvh; + } +} + +/* Landscape on a notched phone puts the cutout down one SIDE of the page, and with + * `viewport-fit=cover` (index.html) the page is drawing under it. Applied to body so + * every view inherits it, rather than each container remembering; the insets are 0 + * on every device that has no cutout, and 0 in portrait, so this costs nothing where + * it isn't needed. Top and bottom are NOT done here — a sticky header and a scrolling + * board need them in their own boxes, not on the page. */ +body { + padding-left: env(safe-area-inset-left, 0px); + padding-right: env(safe-area-inset-right, 0px); +} + body { @apply bg-neutral-50 text-neutral-900 antialiased; } @@ -61,6 +90,78 @@ body { } } +/* A note card's own controls — the drag grip and the pin/colour/archive/trash set. + * ONE element each, in TWO placements, because "always visible" and "floating over + * the card's top corners" cannot both be true without the controls sitting on top of + * the note's own words. + * + * That is exactly what a phone got. `.hover-reveal` above made the pills permanent + * where nothing can hover (task 2697, correctly — they are the only way to pin or + * archive), but they were still absolutely positioned, so they covered the title: + * "thought sync tauri app" rendered as "ught sync tauri app" with the grip parked on + * the first three characters. + * + * So the default — no hover — is a footer row IN FLOW, which cannot overlap anything + * by construction. A pointer that can hover lifts them back out into the floating + * pills they have always been, where they cost no vertical space and appear only on + * approach. `display: contents` drops the flex row itself out of the way so each + * child positions against the card. + * + * Keyed on hover rather than width, for the reason `.hover-reveal` already gives: a + * narrow window on a laptop still hovers, and a wide tablet still doesn't. */ +.note-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 0.125rem; + margin-top: 0.5rem; +} +.note-grip { + /* Pushes the action set to the far edge, so the two read as opposite ends of a + * footer rather than as a clump. */ + margin-right: auto; +} +.note-actions-set { + position: relative; /* the colour popover anchors here in both placements */ +} + +@media (hover: hover) { + .note-actions { + display: contents; + } + .note-grip { + position: absolute; + left: 0.375rem; + top: 0.375rem; + z-index: 10; + margin-right: 0; + } + .note-actions-set { + position: absolute; + right: 0.375rem; + top: 0.375rem; + } +} + +/* The per-card colour popover, anchored to whichever end of the card the action set + * currently occupies: it opens DOWNWARD from a floating top-corner pill, and UPWARD + * from a footer row, so in both cases it grows into the card rather than off it. */ +.note-swatches { + position: absolute; + right: 0; + bottom: 100%; + margin-bottom: 0.375rem; + z-index: 20; +} +@media (hover: hover) { + .note-swatches { + top: 100%; + bottom: auto; + margin-top: 0.375rem; + margin-bottom: 0; + } +} + /* Board motion (M7). Defined once here rather than three times in BoardView's * markup, because "how the board moves" is one idea even though the pinned, other * and non-board grids are three TransitionGroups. diff --git a/frontend/src/views/BoardView.vue b/frontend/src/views/BoardView.vue index d77b992..0e2d61d 100644 --- a/frontend/src/views/BoardView.vue +++ b/frontend/src/views/BoardView.vue @@ -258,16 +258,27 @@ async function onDrop(targetId: string) {