M2 labels frontend: sidebar shell + label filter, chips, picker, manage
- AppShell: persistent left sidebar (Notes · labels · Archive · Trash) + top bar (site name, admin Settings, sign out); BoardView now renders inside it. - labels store (list/create/rename/delete); Note gains labels[]; notes store gains setLabels + label-aware reconcile + /api/notes?label= loading. - /label/:id route → label-filtered board. - LabelPicker (tag a note, create-on-the-fly) in the editor; label chips shown on cards and in the editor; LabelsModal to create/rename/delete labels. - api client PUT; new icons (note/tag/pencil/plus/check); nav-link styles. 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,36 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useSessionStore } from "../stores/session";
|
||||
import { useConfigStore } from "../stores/config";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useNotesStore, type Note, type NoteView } from "../stores/notes";
|
||||
import AppShell from "../components/AppShell.vue";
|
||||
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 config = useConfigStore();
|
||||
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";
|
||||
return "active"; // board + label views
|
||||
}
|
||||
|
||||
const currentView = computed<NoteView>(() => viewForRoute(route.name));
|
||||
const currentLabel = computed<string | null>(() => (route.name === "label" ? String(route.params.id) : null));
|
||||
|
||||
// Quick-add + pinned/others split only on the main board (not archive/trash/label).
|
||||
const isMainBoard = computed(() => route.name === "board");
|
||||
const pinnedNotes = computed(() => notes.items.filter((n) => n.pinned));
|
||||
const otherNotes = computed(() => notes.items.filter((n) => !n.pinned));
|
||||
|
||||
@@ -38,15 +30,16 @@ 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." };
|
||||
if (currentLabel.value) return { title: "No notes with this label", subtitle: "Tag a note to see it here." };
|
||||
return { title: "No notes yet", subtitle: "Capture your first thought in the box above." };
|
||||
});
|
||||
|
||||
async function reload() {
|
||||
await notes.load(currentView.value);
|
||||
await notes.load(currentView.value, currentLabel.value);
|
||||
}
|
||||
|
||||
onMounted(reload);
|
||||
watch(currentView, reload);
|
||||
watch([currentView, currentLabel], reload);
|
||||
|
||||
function openEditor(note: Note) {
|
||||
editing.value = note;
|
||||
@@ -54,65 +47,12 @@ function openEditor(note: Note) {
|
||||
function closeEditor() {
|
||||
editing.value = null;
|
||||
}
|
||||
|
||||
async function signOut() {
|
||||
await session.logout();
|
||||
await router.replace("/login");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex min-h-full flex-col">
|
||||
<header
|
||||
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 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="hidden font-semibold sm:inline">{{ config.siteName }}</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">
|
||||
<RouterLink
|
||||
v-if="session.user?.is_admin"
|
||||
to="/settings"
|
||||
class="icon-btn"
|
||||
title="Settings"
|
||||
aria-label="Settings"
|
||||
>
|
||||
<Icon name="settings" />
|
||||
</RouterLink>
|
||||
<span class="hidden text-sm text-neutral-500 sm:inline dark:text-neutral-400">{{
|
||||
session.user?.display_name
|
||||
}}</span>
|
||||
<BaseButton variant="ghost" @click="signOut">
|
||||
<Icon name="logout" />
|
||||
<span class="hidden sm:inline">Sign out</span>
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="mx-auto w-full max-w-6xl flex-1 px-4 py-6">
|
||||
<QuickAdd v-if="currentView === 'active'" autofocus class="mb-8" />
|
||||
<AppShell>
|
||||
<div class="mx-auto w-full max-w-6xl px-4 py-6">
|
||||
<QuickAdd v-if="isMainBoard" autofocus class="mb-8" />
|
||||
|
||||
<div v-if="notes.loading" class="py-24 text-center text-sm text-neutral-400">Loading…</div>
|
||||
|
||||
@@ -122,7 +62,7 @@ async function signOut() {
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<template v-if="currentView === 'active'">
|
||||
<template v-if="isMainBoard">
|
||||
<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">
|
||||
@@ -146,10 +86,10 @@ async function signOut() {
|
||||
<NoteCard v-for="n in notes.items" :key="n.id" :note="n" @open="openEditor" />
|
||||
</div>
|
||||
</template>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<template v-if="editing">
|
||||
<NoteEditor :note="editing" @close="closeEditor" />
|
||||
</template>
|
||||
</div>
|
||||
</AppShell>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user