M1: masonry board UI — quick-add, note cards, editor, views
- notes Pinia store (load/create/pin/archive/color/trash/restore/delete) with view-aware reconcile; api client patch/del methods. - colors.ts palette (10 keys → light/dark card + swatch tints). - QuickAdd (collapsed → expand, title/body/color, click-outside/Esc to save), NoteCard (color tint, click-to-edit, hover action bar), NoteEditor modal, ColorPicker, inline Icon set (no icon dep). - BoardView rewrite: Notes/Archive/Trash routes, CSS-columns masonry, Pinned + Others sections, per-view empty states, sign-out. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
@@ -1,11 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from "vue-router";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useSessionStore } from "../stores/session";
|
||||
import { useNotesStore, type Note, type NoteView } from "../stores/notes";
|
||||
import QuickAdd from "../components/QuickAdd.vue";
|
||||
import NoteCard from "../components/NoteCard.vue";
|
||||
import NoteEditor from "../components/NoteEditor.vue";
|
||||
import BaseButton from "../components/BaseButton.vue";
|
||||
import Icon from "../components/Icon.vue";
|
||||
|
||||
const session = useSessionStore();
|
||||
const notes = useNotesStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const editing = ref<Note | null>(null);
|
||||
|
||||
const navItems = [
|
||||
{ name: "board", label: "Notes", to: "/" },
|
||||
{ name: "archive", label: "Archive", to: "/archive" },
|
||||
{ name: "trash", label: "Trash", to: "/trash" },
|
||||
];
|
||||
|
||||
function viewForRoute(name: unknown): NoteView {
|
||||
if (name === "archive") return "archived";
|
||||
if (name === "trash") return "trash";
|
||||
return "active";
|
||||
}
|
||||
|
||||
const currentView = computed<NoteView>(() => viewForRoute(route.name));
|
||||
const pinnedNotes = computed(() => notes.items.filter((n) => n.pinned));
|
||||
const otherNotes = computed(() => notes.items.filter((n) => !n.pinned));
|
||||
|
||||
const emptyState = computed(() => {
|
||||
if (currentView.value === "trash") return { title: "Trash is empty", subtitle: "Notes you delete land here first." };
|
||||
if (currentView.value === "archived")
|
||||
return { title: "Nothing archived", subtitle: "Archived notes are tucked away here." };
|
||||
return { title: "No notes yet", subtitle: "Capture your first thought in the box above." };
|
||||
});
|
||||
|
||||
async function reload() {
|
||||
await notes.load(currentView.value);
|
||||
}
|
||||
|
||||
onMounted(reload);
|
||||
watch(currentView, reload);
|
||||
|
||||
function openEditor(note: Note) {
|
||||
editing.value = note;
|
||||
}
|
||||
function closeEditor() {
|
||||
editing.value = null;
|
||||
}
|
||||
|
||||
async function signOut() {
|
||||
await session.logout();
|
||||
await router.replace("/login");
|
||||
@@ -15,59 +62,83 @@ async function signOut() {
|
||||
<template>
|
||||
<div class="flex min-h-full flex-col">
|
||||
<header
|
||||
class="sticky top-0 z-10 border-b border-neutral-200 bg-neutral-50/80 backdrop-blur dark:border-neutral-800 dark:bg-neutral-950/80"
|
||||
class="sticky top-0 z-10 border-b border-neutral-200 bg-neutral-50/90 backdrop-blur dark:border-neutral-800 dark:bg-neutral-950/90"
|
||||
>
|
||||
<div class="mx-auto flex max-w-6xl items-center justify-between px-4 py-3">
|
||||
<div class="mx-auto flex max-w-6xl items-center justify-between gap-4 px-4 py-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-brand text-sm font-black text-neutral-900">
|
||||
TS
|
||||
</div>
|
||||
<span class="font-semibold">ThoughtSync</span>
|
||||
<span class="hidden font-semibold sm:inline">ThoughtSync</span>
|
||||
</div>
|
||||
|
||||
<nav class="flex items-center gap-1">
|
||||
<RouterLink
|
||||
v-for="item in navItems"
|
||||
:key="item.name"
|
||||
:to="item.to"
|
||||
class="rounded-md px-3 py-1.5 text-sm font-medium transition focus:outline-none focus-visible:ring-2 focus-visible:ring-brand"
|
||||
:class="
|
||||
route.name === item.name
|
||||
? 'bg-neutral-200 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100'
|
||||
: 'text-neutral-500 hover:bg-neutral-100 dark:text-neutral-400 dark:hover:bg-neutral-800'
|
||||
"
|
||||
>
|
||||
{{ item.label }}
|
||||
</RouterLink>
|
||||
</nav>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="hidden text-sm text-neutral-500 sm:inline dark:text-neutral-400">{{
|
||||
session.user?.display_name
|
||||
}}</span>
|
||||
<BaseButton variant="ghost" @click="signOut">
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
||||
<polyline points="16 17 21 12 16 7" />
|
||||
<line x1="21" y1="12" x2="9" y2="12" />
|
||||
</svg>
|
||||
Sign out
|
||||
<Icon name="logout" />
|
||||
<span class="hidden sm:inline">Sign out</span>
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="mx-auto flex w-full max-w-6xl flex-1 flex-col items-center justify-center px-4 py-20 text-center">
|
||||
<svg
|
||||
class="h-12 w-12 text-neutral-300 dark:text-neutral-600"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.75"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M15.5 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h9l7-7V5a2 2 0 0 0-2-2Z" />
|
||||
<path d="M14 21v-5a2 2 0 0 1 2-2h5" />
|
||||
</svg>
|
||||
<h1 class="mt-4 text-xl font-semibold">Your board is ready</h1>
|
||||
<p class="mt-1.5 max-w-sm text-sm text-neutral-500 dark:text-neutral-400">
|
||||
A Google-Keep-style masonry board for quick-capturing notes lands here next. The foundation — your account and
|
||||
workspace — is in place.
|
||||
</p>
|
||||
<main class="mx-auto w-full max-w-6xl flex-1 px-4 py-6">
|
||||
<QuickAdd v-if="currentView === 'active'" class="mb-8" />
|
||||
|
||||
<div v-if="notes.loading" class="py-24 text-center text-sm text-neutral-400">Loading…</div>
|
||||
|
||||
<div v-else-if="notes.items.length === 0" class="py-24 text-center">
|
||||
<h2 class="text-lg font-semibold text-neutral-700 dark:text-neutral-200">{{ emptyState.title }}</h2>
|
||||
<p class="mt-1 text-sm text-neutral-400">{{ emptyState.subtitle }}</p>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<template v-if="currentView === 'active'">
|
||||
<section v-if="pinnedNotes.length">
|
||||
<h2 class="mb-2 text-xs font-semibold uppercase tracking-wide text-neutral-400">Pinned</h2>
|
||||
<div class="columns-1 gap-4 sm:columns-2 lg:columns-3 xl:columns-4">
|
||||
<NoteCard v-for="n in pinnedNotes" :key="n.id" :note="n" @open="openEditor" />
|
||||
</div>
|
||||
</section>
|
||||
<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"
|
||||
>
|
||||
Others
|
||||
</h2>
|
||||
<div class="columns-1 gap-4 sm:columns-2 lg:columns-3 xl:columns-4">
|
||||
<NoteCard v-for="n in otherNotes" :key="n.id" :note="n" @open="openEditor" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<div v-else class="columns-1 gap-4 sm:columns-2 lg:columns-3 xl:columns-4">
|
||||
<NoteCard v-for="n in notes.items" :key="n.id" :note="n" @open="openEditor" />
|
||||
</div>
|
||||
</template>
|
||||
</main>
|
||||
|
||||
<template v-if="editing">
|
||||
<NoteEditor :note="editing" @close="closeEditor" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user