CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 11s
CI & Build / Build & push image (push) Successful in 59s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m33s
Android (Tauri) / Android APK (debug) (push) Successful in 4m24s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m32s
Desktop (Tauri) / Update manifest (push) Successful in 5s
"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, its non-board branch, search, and timeline — so a lens could drift from home by a single edit. One already had: the FLIP reflow from 1914 landed on the board's three grids and left search and timeline popping. NoteGrid is now the only file that knows how the masonry is laid out or how it moves, and search and timeline gained the motion by adopting it. It takes activeId rather than an index. The board splits its notes across two grids, so index-based focus made the call site do offset arithmetic (focusedIndex === pinnedNotes.length + i) against a list the grid didn't own. The lens cross-fade is deliberately UNKEYED, which is the whole trick. Board, archive, trash and label all render the same BoardView; keying the transition on the route would remount it, blanking the board and refetching — exactly the page-change feeling this is meant to remove. Unkeyed, Vue transitions only when the component TYPE changes (board to search to timeline to graph), and moving between the board's own lenses stays an in-place reflow that NoteGrid animates. The two behaviours fall out of one rule rather than needing to be special-cased. Out is quicker than in because mode="out-in" makes the durations additive. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
49 lines
1.9 KiB
Vue
49 lines
1.9 KiB
Vue
<script setup lang="ts">
|
|
import { computed, watch } from "vue";
|
|
import { useRoute } from "vue-router";
|
|
import { repo } from "../adapters";
|
|
import { useNoteList } from "../composables/useNoteList";
|
|
import { useNoteEditor } from "../composables/useNoteEditor";
|
|
import AsyncState from "../components/AsyncState.vue";
|
|
import EmptyState from "../components/EmptyState.vue";
|
|
import NoteGrid from "../components/NoteGrid.vue";
|
|
import NoteEditor from "../components/NoteEditor.vue";
|
|
|
|
const route = useRoute();
|
|
const query = computed(() => (typeof route.query.q === "string" ? route.query.q : ""));
|
|
const noMatchSubtitle = computed(() => `Nothing found for "${query.value}".`);
|
|
|
|
const { items: results, loading, error, load: run } = useNoteList(async () => {
|
|
const q = query.value.trim();
|
|
if (!q) return [];
|
|
return repo.notes.search(q);
|
|
}, "Search failed.");
|
|
|
|
const { editing, open: openEditor, close: closeEditor, navigate: onNavigate } = useNoteEditor({
|
|
list: () => results.value,
|
|
onClose: run, // reflect any edits made from a result
|
|
});
|
|
|
|
watch(query, run, { immediate: true });
|
|
</script>
|
|
|
|
<template>
|
|
<div class="mx-auto w-full max-w-6xl px-4 py-6">
|
|
<p class="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
|
|
<template v-if="query"
|
|
>Results for <span class="font-semibold text-neutral-800 dark:text-neutral-200">{{ query }}</span></template
|
|
>
|
|
<template v-else>Type in the search box to find your notes.</template>
|
|
</p>
|
|
|
|
<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" />
|
|
<NoteGrid v-else-if="results.length" :notes="results" @open="openEditor" />
|
|
</AsyncState>
|
|
</div>
|
|
|
|
<template v-if="editing">
|
|
<NoteEditor :note="editing" @close="closeEditor" @navigate="onNavigate" />
|
|
</template>
|
|
</template>
|