M2 labels frontend: sidebar shell + label filter, chips, picker, manage
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 13s
CI & Build / Build & push image (push) Successful in 34s

- 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:
2026-07-19 21:56:14 -04:00
co-authored by Claude Opus 4.8
parent 4d1fc1bdf9
commit 2046600a95
12 changed files with 432 additions and 89 deletions
+106
View File
@@ -0,0 +1,106 @@
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useSessionStore } from "../stores/session";
import { useConfigStore } from "../stores/config";
import { useLabelsStore } from "../stores/labels";
import Icon from "./Icon.vue";
import LabelsModal from "./LabelsModal.vue";
const route = useRoute();
const router = useRouter();
const session = useSessionStore();
const config = useConfigStore();
const labels = useLabelsStore();
const managing = ref(false);
onMounted(() => {
if (!labels.loaded) void labels.load();
});
const currentLabelId = computed(() => (route.name === "label" ? String(route.params.id) : 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-20 border-b border-neutral-200 bg-neutral-50/90 backdrop-blur dark:border-neutral-800 dark:bg-neutral-950/90"
>
<div class="flex 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>
<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>
<RouterLink
v-if="session.user?.is_admin"
to="/settings"
class="icon-btn"
title="Settings"
aria-label="Settings"
>
<Icon name="settings" />
</RouterLink>
<button type="button" class="icon-btn" title="Sign out" aria-label="Sign out" @click="signOut">
<Icon name="logout" />
</button>
</div>
</div>
</header>
<div class="flex flex-1">
<aside class="hidden w-56 shrink-0 border-r border-neutral-200 p-3 sm:block dark:border-neutral-800">
<nav class="flex flex-col gap-0.5 text-sm">
<RouterLink to="/" class="nav-link" :class="route.name === 'board' ? 'nav-link-active' : ''">
<Icon name="note" /> Notes
</RouterLink>
<div class="mt-3 flex items-center justify-between px-3 pb-1">
<span class="text-xs font-semibold uppercase tracking-wide text-neutral-400">Labels</span>
<button
type="button"
class="text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-200"
title="Edit labels"
aria-label="Edit labels"
@click="managing = true"
>
<Icon name="pencil" />
</button>
</div>
<p v-if="!labels.items.length" class="px-3 py-1 text-xs text-neutral-400">No labels yet</p>
<RouterLink
v-for="lb in labels.items"
:key="lb.id"
:to="`/label/${lb.id}`"
class="nav-link"
:class="currentLabelId === lb.id ? 'nav-link-active' : ''"
>
<Icon name="tag" /> <span class="truncate">{{ lb.name }}</span>
</RouterLink>
<RouterLink to="/archive" class="nav-link mt-3" :class="route.name === 'archive' ? 'nav-link-active' : ''">
<Icon name="archive" /> Archive
</RouterLink>
<RouterLink to="/trash" class="nav-link" :class="route.name === 'trash' ? 'nav-link-active' : ''">
<Icon name="trash" /> Trash
</RouterLink>
</nav>
</aside>
<main class="min-w-0 flex-1"><slot /></main>
</div>
<LabelsModal v-if="managing" @close="managing = false" />
</div>
</template>