M1: masonry board UI — quick-add, note cards, editor, views
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 5s
CI & Build / Python tests (push) Successful in 8s
CI & Build / Build & push image (push) Successful in 30s

- 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:
2026-07-19 16:08:53 -04:00
co-authored by Claude Opus 4.8
parent df59a30cca
commit 0f604f9a26
11 changed files with 649 additions and 38 deletions
+109 -38
View File
@@ -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>