0.2.0 — a notebook in your pocket, ready to be hosted #3
+7
-1
@@ -2,7 +2,13 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<!-- `viewport-fit=cover` is what makes `env(safe-area-inset-*)` resolve to
|
||||
anything other than 0. It opts the page into drawing under the notch and the
|
||||
gesture bar, so it is only safe alongside the padding that keeps content out
|
||||
of them — see style.css (sides), the sticky header (top) and the board
|
||||
(bottom). The two halves ship together, deliberately: turning this on alone
|
||||
is worse than leaving it off (task 2706). -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#F5C518" />
|
||||
<meta
|
||||
name="description"
|
||||
|
||||
@@ -252,10 +252,18 @@ async function signOut() {
|
||||
>
|
||||
Skip to notes
|
||||
</a>
|
||||
<!-- `pt-[env(safe-area-inset-top)]`: with `viewport-fit=cover` the page draws
|
||||
under the status bar / notch, and this is the one box that sits there — a
|
||||
sticky header pinned to y=0. Resolves to 0 wherever there is no cutout, so
|
||||
it costs nothing on a desktop or a flat-topped phone. -->
|
||||
<header
|
||||
class="sticky top-0 z-20 border-b border-neutral-200 bg-neutral-50/90 backdrop-blur dark:border-neutral-800 dark:bg-neutral-950/90"
|
||||
class="sticky top-0 z-20 border-b border-neutral-200 bg-neutral-50/90 pt-[env(safe-area-inset-top,0px)] backdrop-blur dark:border-neutral-800 dark:bg-neutral-950/90"
|
||||
>
|
||||
<div class="flex items-center gap-3 px-4 py-3">
|
||||
<!-- Wraps, so the search field can take a line of its own on a narrow screen.
|
||||
In one row it was sharing ~360px with a menu button, the logo, the lens
|
||||
name and four icons, which left every one of them truncated — the lens read
|
||||
as "N…" and the search box as an empty pill. -->
|
||||
<div class="flex flex-wrap items-center gap-x-3 gap-y-2 px-4 py-3">
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn sm:hidden"
|
||||
@@ -295,19 +303,24 @@ async function signOut() {
|
||||
<span class="truncate font-medium text-neutral-600 dark:text-neutral-300">{{ lensName }}</span>
|
||||
</span>
|
||||
|
||||
<div class="flex flex-1 justify-center">
|
||||
<!-- `order-last w-full` drops this onto its own line below sm (a full-width
|
||||
flex item forces the wrap); from sm it returns to the middle of the row.
|
||||
ONE input either way, moved by CSS rather than duplicated — the `/`
|
||||
shortcut focuses `searchInput`, and two of those would be one ref too
|
||||
many. -->
|
||||
<div class="order-last flex w-full justify-center sm:order-none sm:w-auto sm:flex-1">
|
||||
<input
|
||||
ref="searchInput"
|
||||
:value="searchText"
|
||||
type="search"
|
||||
placeholder="Search notes…"
|
||||
aria-label="Search notes"
|
||||
class="w-full max-w-md rounded-lg border border-neutral-300 bg-white px-3 py-1.5 text-sm outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-neutral-700 dark:bg-neutral-900"
|
||||
class="w-full max-w-md rounded-lg border border-neutral-300 bg-white px-3 py-2 text-base outline-none focus-visible:ring-2 focus-visible:ring-brand sm:py-1.5 sm:text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||
@input="onSearch(($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-3">
|
||||
<div class="ml-auto flex shrink-0 items-center gap-1 sm:ml-0 sm:gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded-lg bg-brand px-2.5 py-1.5 text-sm font-semibold text-neutral-900 hover:brightness-95 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand"
|
||||
@@ -341,7 +354,7 @@ async function signOut() {
|
||||
<RouterLink
|
||||
v-if="!desktopApp"
|
||||
to="/account"
|
||||
class="icon-btn"
|
||||
class="icon-btn hidden sm:inline-flex"
|
||||
title="Linked devices"
|
||||
aria-label="Linked devices"
|
||||
>
|
||||
@@ -350,7 +363,7 @@ async function signOut() {
|
||||
<RouterLink
|
||||
v-if="session.user?.is_admin"
|
||||
to="/settings"
|
||||
class="icon-btn"
|
||||
class="icon-btn hidden sm:inline-flex"
|
||||
title="Settings"
|
||||
aria-label="Settings"
|
||||
>
|
||||
@@ -364,7 +377,7 @@ async function signOut() {
|
||||
<button
|
||||
v-if="!desktopApp"
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
class="icon-btn hidden sm:inline-flex"
|
||||
title="Sign out"
|
||||
aria-label="Sign out"
|
||||
@click="signOut"
|
||||
@@ -383,7 +396,7 @@ async function signOut() {
|
||||
@click="drawer = false"
|
||||
></div>
|
||||
<aside
|
||||
class="fixed inset-y-0 left-0 z-40 w-64 -translate-x-full overflow-y-auto border-r border-neutral-200 bg-neutral-50 p-3 transition-transform duration-200 sm:static sm:z-auto sm:w-56 sm:translate-x-0 dark:border-neutral-800 dark:bg-neutral-950"
|
||||
class="fixed inset-y-0 left-0 z-40 w-64 -translate-x-full overflow-y-auto border-r border-neutral-200 bg-neutral-50 p-3 pb-[calc(0.75rem+env(safe-area-inset-bottom,0px))] pt-[calc(0.75rem+env(safe-area-inset-top,0px))] transition-transform duration-200 sm:static sm:z-auto sm:w-56 sm:translate-x-0 sm:pb-3 sm:pt-3 dark:border-neutral-800 dark:bg-neutral-950"
|
||||
:class="drawer ? 'translate-x-0' : ''"
|
||||
>
|
||||
<nav class="flex flex-col gap-0.5 text-sm" @click="drawer = false">
|
||||
@@ -469,6 +482,34 @@ async function signOut() {
|
||||
<Icon name="download" /> Export
|
||||
</a>
|
||||
<ImportNotes />
|
||||
|
||||
<!-- Account, settings and sign-out, for the screens where they are NOT in
|
||||
the header. Four icons plus a search field never fit one phone-width
|
||||
row, and the header is the wrong place to lose: it holds the only way
|
||||
back to the board. Here they get room to be named instead of guessed
|
||||
at from a glyph.
|
||||
Hidden from sm up, where the header carries them again — so they are
|
||||
in exactly one place at any width, never both. -->
|
||||
<div
|
||||
v-if="!desktopApp"
|
||||
class="mt-3 flex flex-col gap-0.5 border-t border-neutral-200 pt-3 sm:hidden dark:border-neutral-800"
|
||||
>
|
||||
<p class="truncate px-3 pb-1 text-xs text-neutral-400">{{ session.user?.display_name }}</p>
|
||||
<RouterLink to="/account" class="nav-link" :class="route.name === 'account' ? 'nav-link-active' : ''">
|
||||
<Icon name="device" /> Linked devices
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
v-if="session.user?.is_admin"
|
||||
to="/settings"
|
||||
class="nav-link"
|
||||
:class="route.name === 'settings' ? 'nav-link-active' : ''"
|
||||
>
|
||||
<Icon name="settings" /> Settings
|
||||
</RouterLink>
|
||||
<button type="button" class="nav-link w-full text-left" @click="signOut">
|
||||
<Icon name="logout" /> Sign out
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -258,16 +258,27 @@ async function onDrop(targetId: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto w-full max-w-6xl px-4 py-6">
|
||||
<!-- `pb-[…safe-area-inset-bottom]`: the board is the last thing on the page, so
|
||||
it is what ends up underneath the gesture bar once `viewport-fit=cover` lets
|
||||
the page draw there. Added to the existing py-6 rather than replacing it. -->
|
||||
<div class="mx-auto w-full max-w-6xl px-4 pb-[calc(1.5rem+env(safe-area-inset-bottom,0px))] pt-6">
|
||||
<button
|
||||
v-if="isMainBoard"
|
||||
type="button"
|
||||
class="mx-auto mb-4 block w-full max-w-xl rounded-xl border border-dashed border-neutral-300 px-4 py-2.5 text-center text-sm text-neutral-400 transition hover:border-neutral-400 hover:text-neutral-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-neutral-700 dark:hover:border-neutral-500"
|
||||
@click="ui.requestCompose()"
|
||||
>
|
||||
Press
|
||||
<kbd class="mx-0.5 rounded border border-neutral-300 px-1 text-xs dark:border-neutral-600">Enter</kbd>
|
||||
or start typing to add a note
|
||||
<!-- Two ways of saying the same thing, because the keyboard half is a lie on a
|
||||
phone: there is no Enter key until something is already focused, and the
|
||||
box has always been tappable. `hidden sm:inline` / `sm:hidden` rather than
|
||||
a JS pointer check — this is presentation, and it should be right in the
|
||||
first paint rather than after one. -->
|
||||
<span class="hidden sm:inline">
|
||||
Press
|
||||
<kbd class="mx-0.5 rounded border border-neutral-300 px-1 text-xs dark:border-neutral-600">Enter</kbd>
|
||||
or start typing to add a note
|
||||
</span>
|
||||
<span class="sm:hidden">Tap to add a note</span>
|
||||
</button>
|
||||
<FilterBar v-if="isMainBoard" />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user