From 2046600a95ba697fd59cec6954e746648faaa381 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sun, 19 Jul 2026 21:56:14 -0400 Subject: [PATCH] M2 labels frontend: sidebar shell + label filter, chips, picker, manage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm --- frontend/src/api/client.ts | 1 + frontend/src/components/AppShell.vue | 106 ++++++++++++++++++++++++ frontend/src/components/Icon.vue | 5 ++ frontend/src/components/LabelPicker.vue | 101 ++++++++++++++++++++++ frontend/src/components/LabelsModal.vue | 73 ++++++++++++++++ frontend/src/components/NoteCard.vue | 9 ++ frontend/src/components/NoteEditor.vue | 35 +++++++- frontend/src/router/index.ts | 6 ++ frontend/src/stores/labels.ts | 46 ++++++++++ frontend/src/stores/notes.ts | 39 ++++++--- frontend/src/style.css | 8 ++ frontend/src/views/BoardView.vue | 92 ++++---------------- 12 files changed, 432 insertions(+), 89 deletions(-) create mode 100644 frontend/src/components/AppShell.vue create mode 100644 frontend/src/components/LabelPicker.vue create mode 100644 frontend/src/components/LabelsModal.vue create mode 100644 frontend/src/stores/labels.ts diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index a70ef48..6e1038c 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -30,5 +30,6 @@ export const api = { get: (path: string) => request("GET", path), post: (path: string, body?: unknown) => request("POST", path, body), patch: (path: string, body?: unknown) => request("PATCH", path, body), + put: (path: string, body?: unknown) => request("PUT", path, body), del: (path: string) => request("DELETE", path), }; diff --git a/frontend/src/components/AppShell.vue b/frontend/src/components/AppShell.vue new file mode 100644 index 0000000..2abd2a1 --- /dev/null +++ b/frontend/src/components/AppShell.vue @@ -0,0 +1,106 @@ + + + diff --git a/frontend/src/components/Icon.vue b/frontend/src/components/Icon.vue index ca8718d..321df71 100644 --- a/frontend/src/components/Icon.vue +++ b/frontend/src/components/Icon.vue @@ -9,6 +9,11 @@ const paths: Record = { restore: '', logout: '', settings: '', + note: '', + tag: '', + pencil: '', + plus: '', + check: '', }; diff --git a/frontend/src/components/LabelPicker.vue b/frontend/src/components/LabelPicker.vue new file mode 100644 index 0000000..07f4d9e --- /dev/null +++ b/frontend/src/components/LabelPicker.vue @@ -0,0 +1,101 @@ + + + diff --git a/frontend/src/components/LabelsModal.vue b/frontend/src/components/LabelsModal.vue new file mode 100644 index 0000000..4433f4b --- /dev/null +++ b/frontend/src/components/LabelsModal.vue @@ -0,0 +1,73 @@ + + + diff --git a/frontend/src/components/NoteCard.vue b/frontend/src/components/NoteCard.vue index 5766a0c..8253861 100644 --- a/frontend/src/components/NoteCard.vue +++ b/frontend/src/components/NoteCard.vue @@ -32,6 +32,15 @@ function cardClass(color: NoteColor): string {

Empty note

+
+ {{ lb.name }} +
+
diff --git a/frontend/src/components/NoteEditor.vue b/frontend/src/components/NoteEditor.vue index 2b11e17..471943f 100644 --- a/frontend/src/components/NoteEditor.vue +++ b/frontend/src/components/NoteEditor.vue @@ -3,7 +3,8 @@ import { nextTick, onMounted, ref, watch } from "vue"; import { useNotesStore } from "../stores/notes"; import ColorPicker from "./ColorPicker.vue"; import Icon from "./Icon.vue"; -import type { Note } from "../stores/notes"; +import LabelPicker from "./LabelPicker.vue"; +import type { Note, NoteLabel } from "../stores/notes"; import type { NoteColor } from "../notes/colors"; const props = defineProps<{ note: Note }>(); @@ -13,6 +14,7 @@ const notes = useNotesStore(); const title = ref(props.note.title ?? ""); const body = ref(props.note.body); const color = ref(props.note.color); +const labelList = ref([...props.note.labels]); const bodyInput = ref(null); watch( @@ -21,6 +23,7 @@ watch( title.value = n.title ?? ""; body.value = n.body; color.value = n.color; + labelList.value = [...n.labels]; }, ); @@ -29,6 +32,18 @@ onMounted(async () => { bodyInput.value?.focus(); }); +async function onLabelsChange(next: NoteLabel[]) { + labelList.value = next; + await notes.setLabels( + props.note.id, + next.map((lb) => lb.id), + ); +} + +async function removeLabel(id: string) { + await onLabelsChange(labelList.value.filter((lb) => lb.id !== id)); +} + async function close() { const changed = (title.value.trim() || null) !== (props.note.title ?? null) || @@ -71,10 +86,28 @@ async function act(fn: () => Promise) { placeholder="Take a note…" class="w-full resize-none bg-transparent text-sm leading-relaxed outline-none placeholder:text-neutral-400" /> +
+ + {{ lb.name }} + + +
+