M12 — the Android client, end to end #2
@@ -428,7 +428,21 @@ async function signOut() {
|
|||||||
<!-- tabindex="-1" so the skip link above actually moves FOCUS here, not just
|
<!-- tabindex="-1" so the skip link above actually moves FOCUS here, not just
|
||||||
the viewport — several browsers scroll to a plain anchor without focusing
|
the viewport — several browsers scroll to a plain anchor without focusing
|
||||||
it, which leaves the next Tab back at the top of the page. -->
|
it, which leaves the next Tab back at the top of the page. -->
|
||||||
<main id="main" tabindex="-1" class="min-w-0 flex-1 focus:outline-none"><RouterView /></main>
|
<!-- Cross-fade between lenses so switching reads as the same space
|
||||||
|
re-framing rather than a page swap.
|
||||||
|
Deliberately UNKEYED: board / archive / trash / label all render the same
|
||||||
|
BoardView, so keying on the route would remount it — blanking the board
|
||||||
|
and refetching, which is precisely the page-change feeling this is meant
|
||||||
|
to remove. Unkeyed, Vue only transitions when the component TYPE changes
|
||||||
|
(board ↔ search ↔ timeline ↔ graph), and moving between the board's own
|
||||||
|
lenses stays an in-place reflow that NoteGrid animates. -->
|
||||||
|
<main id="main" tabindex="-1" class="min-w-0 flex-1 focus:outline-none">
|
||||||
|
<RouterView v-slot="{ Component }">
|
||||||
|
<Transition name="lens" mode="out-in">
|
||||||
|
<component :is="Component" />
|
||||||
|
</Transition>
|
||||||
|
</RouterView>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LabelsModal v-if="managing" @close="managing = false" />
|
<LabelsModal v-if="managing" @close="managing = false" />
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import NoteCard from "./NoteCard.vue";
|
||||||
|
import type { Note } from "../stores/notes";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The masonry of note cards — the one place the board's LAYOUT and its MOTION are
|
||||||
|
* defined.
|
||||||
|
*
|
||||||
|
* It exists because "the same board, re-filtered" has to be literally true to read
|
||||||
|
* as true. The column classes were copy-pasted into five places (the board's pinned
|
||||||
|
* and other sections, the non-board views, search, timeline), so a lens could drift
|
||||||
|
* from home by a single edit — and one already had: the FLIP reflow landed on the
|
||||||
|
* board's three grids and left search and timeline popping.
|
||||||
|
*
|
||||||
|
* `activeId` rather than an index, because the board splits its notes across two
|
||||||
|
* grids and index-based focus made every call site do offset arithmetic against a
|
||||||
|
* list it didn't own.
|
||||||
|
*/
|
||||||
|
defineProps<{
|
||||||
|
notes: Note[];
|
||||||
|
/** The keyboard-focused note, if any. */
|
||||||
|
activeId?: string | null;
|
||||||
|
/** Board views only — reorder is meaningless in a search result or a date group. */
|
||||||
|
reorderable?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(e: "open", note: Note): void;
|
||||||
|
(e: "dragstart", note: Note): void;
|
||||||
|
(e: "dragend", note: Note): void;
|
||||||
|
(e: "drop", targetId: string): void;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- `board` is the transition name defined in style.css; `appear` covers first
|
||||||
|
paint so a lens arrives eased rather than snapping in. -->
|
||||||
|
<TransitionGroup
|
||||||
|
tag="div"
|
||||||
|
name="board"
|
||||||
|
appear
|
||||||
|
class="columns-1 gap-4 sm:columns-2 lg:columns-3 xl:columns-4"
|
||||||
|
>
|
||||||
|
<NoteCard
|
||||||
|
v-for="n in notes"
|
||||||
|
:key="n.id"
|
||||||
|
:note="n"
|
||||||
|
:active="activeId === n.id"
|
||||||
|
:reorderable="reorderable"
|
||||||
|
@open="$emit('open', $event)"
|
||||||
|
@dragstart="$emit('dragstart', $event)"
|
||||||
|
@dragend="$emit('dragend', $event)"
|
||||||
|
@drop="$emit('drop', $event)"
|
||||||
|
/>
|
||||||
|
</TransitionGroup>
|
||||||
|
</template>
|
||||||
@@ -127,6 +127,21 @@ body {
|
|||||||
transform: scale(0.94);
|
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 {
|
@layer components {
|
||||||
.icon-btn {
|
.icon-btn {
|
||||||
@apply rounded-md p-1.5 text-neutral-500 transition hover:bg-black/5 focus:outline-none
|
@apply rounded-md p-1.5 text-neutral-500 transition hover:bg-black/5 focus:outline-none
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { useNoteEditor } from "../composables/useNoteEditor";
|
|||||||
import AsyncState from "../components/AsyncState.vue";
|
import AsyncState from "../components/AsyncState.vue";
|
||||||
import EmptyState from "../components/EmptyState.vue";
|
import EmptyState from "../components/EmptyState.vue";
|
||||||
import FilterBar from "../components/FilterBar.vue";
|
import FilterBar from "../components/FilterBar.vue";
|
||||||
import NoteCard from "../components/NoteCard.vue";
|
import NoteGrid from "../components/NoteGrid.vue";
|
||||||
import NoteEditor from "../components/NoteEditor.vue";
|
import NoteEditor from "../components/NoteEditor.vue";
|
||||||
import { isDesktop, sync as syncBridge } from "../desktop/bridge";
|
import { isDesktop, sync as syncBridge } from "../desktop/bridge";
|
||||||
|
|
||||||
@@ -76,6 +76,9 @@ const orderedNotes = computed<Note[]>(() =>
|
|||||||
isMainBoard.value ? [...pinnedNotes.value, ...otherNotes.value] : notes.items,
|
isMainBoard.value ? [...pinnedNotes.value, ...otherNotes.value] : notes.items,
|
||||||
);
|
);
|
||||||
const focusedIndex = ref(-1);
|
const focusedIndex = ref(-1);
|
||||||
|
/** The focused note's id — what the grids compare against, so neither has to know
|
||||||
|
* its own offset within the combined keyboard-navigation order. */
|
||||||
|
const focusedId = computed<string | null>(() => orderedNotes.value[focusedIndex.value]?.id ?? null);
|
||||||
const inTrash = computed(() => currentView.value === "trash");
|
const inTrash = computed(() => currentView.value === "trash");
|
||||||
|
|
||||||
function moveFocus(delta: number) {
|
function moveFocus(delta: number) {
|
||||||
@@ -300,59 +303,42 @@ async function onDrop(targetId: string) {
|
|||||||
<template v-if="isMainBoard">
|
<template v-if="isMainBoard">
|
||||||
<section v-if="pinnedNotes.length">
|
<section v-if="pinnedNotes.length">
|
||||||
<h2 class="mb-2 text-xs font-semibold uppercase tracking-wide text-neutral-400">Pinned</h2>
|
<h2 class="mb-2 text-xs font-semibold uppercase tracking-wide text-neutral-400">Pinned</h2>
|
||||||
<TransitionGroup tag="div" name="board" appear class="columns-1 gap-4 sm:columns-2 lg:columns-3 xl:columns-4">
|
<NoteGrid
|
||||||
<NoteCard
|
:notes="pinnedNotes"
|
||||||
v-for="(n, i) in pinnedNotes"
|
:active-id="focusedId"
|
||||||
:key="n.id"
|
|
||||||
:note="n"
|
|
||||||
:active="focusedIndex === i"
|
|
||||||
reorderable
|
reorderable
|
||||||
@open="openEditor"
|
@open="openEditor"
|
||||||
@dragstart="onDragStart"
|
@dragstart="onDragStart"
|
||||||
@dragend="onDragEnd"
|
@dragend="onDragEnd"
|
||||||
@drop="onDrop"
|
@drop="onDrop"
|
||||||
/>
|
/>
|
||||||
</TransitionGroup>
|
|
||||||
</section>
|
</section>
|
||||||
<section v-if="otherNotes.length" :class="pinnedNotes.length ? 'mt-8' : ''">
|
<section v-if="otherNotes.length" :class="pinnedNotes.length ? 'mt-8' : ''">
|
||||||
<h2 v-if="pinnedNotes.length" class="mb-2 text-xs font-semibold uppercase tracking-wide text-neutral-400">
|
<h2 v-if="pinnedNotes.length" class="mb-2 text-xs font-semibold uppercase tracking-wide text-neutral-400">
|
||||||
Others
|
Others
|
||||||
</h2>
|
</h2>
|
||||||
<TransitionGroup tag="div" name="board" appear class="columns-1 gap-4 sm:columns-2 lg:columns-3 xl:columns-4">
|
<NoteGrid
|
||||||
<NoteCard
|
:notes="otherNotes"
|
||||||
v-for="(n, i) in otherNotes"
|
:active-id="focusedId"
|
||||||
:key="n.id"
|
|
||||||
:note="n"
|
|
||||||
:active="focusedIndex === pinnedNotes.length + i"
|
|
||||||
reorderable
|
reorderable
|
||||||
@open="openEditor"
|
@open="openEditor"
|
||||||
@dragstart="onDragStart"
|
@dragstart="onDragStart"
|
||||||
@dragend="onDragEnd"
|
@dragend="onDragEnd"
|
||||||
@drop="onDrop"
|
@drop="onDrop"
|
||||||
/>
|
/>
|
||||||
</TransitionGroup>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<TransitionGroup
|
<NoteGrid
|
||||||
v-else
|
v-else
|
||||||
tag="div"
|
:notes="notes.items"
|
||||||
name="board"
|
:active-id="focusedId"
|
||||||
appear
|
|
||||||
class="columns-1 gap-4 sm:columns-2 lg:columns-3 xl:columns-4"
|
|
||||||
>
|
|
||||||
<NoteCard
|
|
||||||
v-for="(n, i) in notes.items"
|
|
||||||
:key="n.id"
|
|
||||||
:note="n"
|
|
||||||
:active="focusedIndex === i"
|
|
||||||
reorderable
|
reorderable
|
||||||
@open="openEditor"
|
@open="openEditor"
|
||||||
@dragstart="onDragStart"
|
@dragstart="onDragStart"
|
||||||
@dragend="onDragEnd"
|
@dragend="onDragEnd"
|
||||||
@drop="onDrop"
|
@drop="onDrop"
|
||||||
/>
|
/>
|
||||||
</TransitionGroup>
|
|
||||||
</template>
|
</template>
|
||||||
</AsyncState>
|
</AsyncState>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useNoteList } from "../composables/useNoteList";
|
|||||||
import { useNoteEditor } from "../composables/useNoteEditor";
|
import { useNoteEditor } from "../composables/useNoteEditor";
|
||||||
import AsyncState from "../components/AsyncState.vue";
|
import AsyncState from "../components/AsyncState.vue";
|
||||||
import EmptyState from "../components/EmptyState.vue";
|
import EmptyState from "../components/EmptyState.vue";
|
||||||
import NoteCard from "../components/NoteCard.vue";
|
import NoteGrid from "../components/NoteGrid.vue";
|
||||||
import NoteEditor from "../components/NoteEditor.vue";
|
import NoteEditor from "../components/NoteEditor.vue";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -38,9 +38,7 @@ watch(query, run, { immediate: true });
|
|||||||
|
|
||||||
<AsyncState :loading="loading" :error="error || undefined" error-title="Couldn't search" @retry="run">
|
<AsyncState :loading="loading" :error="error || undefined" error-title="Couldn't search" @retry="run">
|
||||||
<EmptyState v-if="query && results.length === 0" title="No matches" :subtitle="noMatchSubtitle" />
|
<EmptyState v-if="query && results.length === 0" title="No matches" :subtitle="noMatchSubtitle" />
|
||||||
<div v-else-if="results.length" class="columns-1 gap-4 sm:columns-2 lg:columns-3 xl:columns-4">
|
<NoteGrid v-else-if="results.length" :notes="results" @open="openEditor" />
|
||||||
<NoteCard v-for="n in results" :key="n.id" :note="n" @open="openEditor" />
|
|
||||||
</div>
|
|
||||||
</AsyncState>
|
</AsyncState>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useNoteList } from "../composables/useNoteList";
|
|||||||
import { useNoteEditor } from "../composables/useNoteEditor";
|
import { useNoteEditor } from "../composables/useNoteEditor";
|
||||||
import AsyncState from "../components/AsyncState.vue";
|
import AsyncState from "../components/AsyncState.vue";
|
||||||
import EmptyState from "../components/EmptyState.vue";
|
import EmptyState from "../components/EmptyState.vue";
|
||||||
import NoteCard from "../components/NoteCard.vue";
|
import NoteGrid from "../components/NoteGrid.vue";
|
||||||
import NoteEditor from "../components/NoteEditor.vue";
|
import NoteEditor from "../components/NoteEditor.vue";
|
||||||
|
|
||||||
// The "find by WHEN" recall lens: your notes grouped by when you captured them,
|
// The "find by WHEN" recall lens: your notes grouped by when you captured them,
|
||||||
@@ -149,9 +149,7 @@ onMounted(load);
|
|||||||
<div v-else class="flex flex-col gap-8">
|
<div v-else class="flex flex-col gap-8">
|
||||||
<section v-for="group in groups" :key="group.key">
|
<section v-for="group in groups" :key="group.key">
|
||||||
<h2 class="mb-3 text-xs font-semibold uppercase tracking-wide text-neutral-400">{{ group.label }}</h2>
|
<h2 class="mb-3 text-xs font-semibold uppercase tracking-wide text-neutral-400">{{ group.label }}</h2>
|
||||||
<div class="columns-1 gap-4 sm:columns-2 lg:columns-3 xl:columns-4">
|
<NoteGrid :notes="group.notes" @open="openEditor" />
|
||||||
<NoteCard v-for="n in group.notes" :key="n.id" :note="n" @open="openEditor" />
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</AsyncState>
|
</AsyncState>
|
||||||
|
|||||||
Reference in New Issue
Block a user