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".
284 lines
10 KiB
CSS
284 lines
10 KiB
CSS
@tailwind base;
|
|
@tailwind components;
|
|
@tailwind utilities;
|
|
|
|
html,
|
|
body,
|
|
#app {
|
|
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;
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
body {
|
|
@apply bg-neutral-950 text-neutral-100;
|
|
}
|
|
}
|
|
|
|
/* Motion is a feature, not a given. Anyone whose OS says "reduce motion" has told
|
|
* us something about vestibular comfort or attention, and the answer is to arrive
|
|
* instantly rather than to animate faster.
|
|
*
|
|
* Global and blunt on purpose: it catches every Tailwind `transition` utility
|
|
* already scattered through the components, and it will catch the M7 motion work
|
|
* without each new component having to remember. Near-zero rather than `none` so
|
|
* `transitionend`/`animationend` listeners still fire and no state machine that
|
|
* waits on them can hang. JS-driven motion can't be reached from CSS — that reads
|
|
* the same preference through `useReducedMotion`. */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
animation-duration: 0.01ms !important;
|
|
animation-iteration-count: 1 !important;
|
|
transition-duration: 0.01ms !important;
|
|
scroll-behavior: auto !important;
|
|
}
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
}
|
|
|
|
/* 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.
|
|
*
|
|
* Vue's TransitionGroup does the FLIP itself: it measures each card before and
|
|
* after the list changes and transitions the difference away. All we supply is the
|
|
* curve. Deliberately fast and small — the brief is continuity, so a card should
|
|
* read as having MOVED rather than as having performed.
|
|
*
|
|
* Leavers are NOT taken out of flow with `position: absolute`, which is the usual
|
|
* TransitionGroup trick: this masonry is CSS multi-column, and an absolutely
|
|
* positioned child escapes its column to the container's origin — it would fly
|
|
* across the board on its way out. Keeping them in flow costs a small settle when
|
|
* the element is finally removed, so the leave is the shortest of the three.
|
|
*
|
|
* Reduced motion needs no special case here: the global guard above already
|
|
* collapses every duration, so this degrades to an instant cut. */
|
|
.board-move {
|
|
transition: transform 220ms cubic-bezier(0.2, 0, 0, 1);
|
|
}
|
|
.board-enter-active {
|
|
transition:
|
|
opacity 180ms ease-out,
|
|
transform 180ms cubic-bezier(0.2, 0, 0, 1);
|
|
}
|
|
.board-leave-active {
|
|
transition: opacity 120ms ease-in;
|
|
}
|
|
.board-enter-from {
|
|
opacity: 0;
|
|
/* Barely a scale — enough to read as arriving, not as zooming. */
|
|
transform: scale(0.98);
|
|
}
|
|
.board-leave-to {
|
|
opacity: 0;
|
|
}
|
|
|
|
/* Editor open/close (M7). The backdrop only fades; the panel scales from the point
|
|
* the card occupied, which is what carries "this card became the editor" without
|
|
* the distortion a true card→panel scale would put through the text.
|
|
*
|
|
* Enter is slower than leave on purpose: arriving wants to be noticed as a
|
|
* connection, leaving just wants to be out of the way. LEAVE_MS in NoteEditor.vue
|
|
* must stay in step with the leave duration here — it waits that long before
|
|
* telling its host to unmount it. */
|
|
.editor-enter-active {
|
|
transition: opacity 200ms ease-out;
|
|
}
|
|
.editor-leave-active {
|
|
transition: opacity 140ms ease-in;
|
|
}
|
|
.editor-enter-from,
|
|
.editor-leave-to {
|
|
opacity: 0;
|
|
}
|
|
.editor-enter-active .editor-panel {
|
|
transition: transform 200ms cubic-bezier(0.2, 0, 0, 1);
|
|
}
|
|
.editor-leave-active .editor-panel {
|
|
transition: transform 140ms ease-in;
|
|
}
|
|
.editor-enter-from .editor-panel,
|
|
.editor-leave-to .editor-panel {
|
|
transform: scale(0.94);
|
|
}
|
|
|
|
/* Lens cross-fade (M7). Only fires between genuinely different surfaces; the
|
|
* board's own lenses share a component and reflow in place instead. Out is quicker
|
|
* than in so the gap between them stays short — `mode="out-in"` means the two
|
|
* durations are additive, and anything slower starts to feel like waiting. */
|
|
.lens-enter-active {
|
|
transition: opacity 150ms ease-out;
|
|
}
|
|
.lens-leave-active {
|
|
transition: opacity 100ms ease-in;
|
|
}
|
|
.lens-enter-from,
|
|
.lens-leave-to {
|
|
opacity: 0;
|
|
}
|
|
|
|
@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;
|
|
}
|
|
}
|
|
/* A small action sitting beside a chip on a card — reminder Done/snooze today.
|
|
* Quiet at rest so a board full of reminders doesn't read as a wall of buttons,
|
|
* and it earns contrast on hover/focus. */
|
|
.chip-btn {
|
|
@apply rounded-full px-2 py-0.5 text-xs text-neutral-500 transition hover:bg-black/5
|
|
hover:text-neutral-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand
|
|
dark:text-neutral-400 dark:hover:bg-white/10 dark:hover:text-neutral-100;
|
|
}
|
|
/* Same finger rule as .icon-btn: comfortable for a cursor is too small for a
|
|
* thumb, and these are the primary action on a due note. */
|
|
@media (pointer: coarse) {
|
|
.chip-btn {
|
|
@apply inline-flex min-h-[2.25rem] items-center justify-center px-3;
|
|
}
|
|
}
|
|
.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
|
|
dark:text-neutral-300 dark:hover:bg-neutral-800;
|
|
}
|
|
.nav-link-active {
|
|
@apply bg-neutral-200 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100;
|
|
}
|
|
}
|