+
+
-
+
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))
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ 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"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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) {
-
+
+
- Press
- Enter
- or start typing to add a note
+
+
+ Press
+ Enter
+ or start typing to add a note
+
+ Tap to add a note