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:
@@ -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<NoteColor>(props.note.color);
|
||||
const labelList = ref<NoteLabel[]>([...props.note.labels]);
|
||||
const bodyInput = ref<HTMLTextAreaElement | null>(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<void>) {
|
||||
placeholder="Take a note…"
|
||||
class="w-full resize-none bg-transparent text-sm leading-relaxed outline-none placeholder:text-neutral-400"
|
||||
/>
|
||||
<div v-if="labelList.length" class="flex flex-wrap gap-1.5 pt-1">
|
||||
<span
|
||||
v-for="lb in labelList"
|
||||
:key="lb.id"
|
||||
class="inline-flex items-center gap-1 rounded-full bg-black/5 px-2 py-0.5 text-xs text-neutral-600 dark:bg-white/10 dark:text-neutral-300"
|
||||
>
|
||||
{{ lb.name }}
|
||||
<button
|
||||
type="button"
|
||||
class="text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-100"
|
||||
:aria-label="`Remove ${lb.name}`"
|
||||
@click="removeLabel(lb.id)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2 border-t border-neutral-100 px-3 py-2 dark:border-neutral-800">
|
||||
<ColorPicker v-model="color" />
|
||||
<div class="flex items-center gap-0.5">
|
||||
<LabelPicker v-if="!note.trashed" :model-value="labelList" @update:model-value="onLabelsChange" />
|
||||
<template v-if="!note.trashed">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user