labels: per-label color
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 8s
CI & Build / Python tests (push) Successful in 10s
CI & Build / Build & push image (push) Successful in 32s

Give labels a color (migration 0011 adds labels.color, server_default
'default' so existing labels keep the neutral chip). The PATCH endpoint now
updates name and/or color; note serialization carries each label's color.
Frontend: a swatch picker per label in the Edit-labels modal, colored chips
on cards + in the editor (LABEL_CHIP_CLASSES), and a color dot on each
sidebar label. Reuses the note color vocabulary. (Graph node coloring rides
this in the graph-liveliness task.)

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-20 13:11:50 -04:00
co-authored by Claude Opus 4.8
parent f3145a5e3f
commit f321c2bf70
11 changed files with 133 additions and 22 deletions
+35 -2
View File
@@ -1,11 +1,13 @@
<script setup lang="ts">
import { ref } from "vue";
import { useLabelsStore } from "../stores/labels";
import { NOTE_COLOR_KEYS, NOTE_COLOR_LABELS, NOTE_SWATCH_CLASSES, type NoteColor } from "../notes/colors";
import Icon from "./Icon.vue";
const emit = defineEmits<{ (e: "close"): void }>();
const labels = useLabelsStore();
const newName = ref("");
const pickerFor = ref<string | null>(null);
async function add() {
const name = newName.value.trim();
@@ -18,6 +20,15 @@ async function rename(id: string, value: string) {
const name = value.trim();
if (name) await labels.rename(id, name);
}
function labelDot(color: string): string {
return NOTE_SWATCH_CLASSES[color as NoteColor] ?? NOTE_SWATCH_CLASSES.default;
}
async function pickColor(id: string, color: NoteColor) {
pickerFor.value = null;
await labels.setColor(id, color);
}
</script>
<template>
@@ -46,8 +57,15 @@ async function rename(id: string, value: string) {
/>
</form>
<ul class="flex flex-col gap-1 pt-1">
<li v-for="lb in labels.items" :key="lb.id" class="flex items-center gap-2">
<Icon name="tag" />
<li v-for="lb in labels.items" :key="lb.id" class="relative flex items-center gap-2">
<button
type="button"
class="h-4 w-4 shrink-0 rounded-full border border-black/10 transition hover:scale-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-white/15"
:class="labelDot(lb.color)"
:title="`Color: ${NOTE_COLOR_LABELS[(lb.color as NoteColor)] ?? lb.color}`"
aria-label="Change label color"
@click="pickerFor = pickerFor === lb.id ? null : lb.id"
/>
<input
:value="lb.name"
class="flex-1 rounded-md bg-transparent px-2 py-1.5 text-sm outline-none focus-visible:ring-2 focus-visible:ring-brand"
@@ -62,6 +80,21 @@ async function rename(id: string, value: string) {
>
<Icon name="trash" />
</button>
<div
v-if="pickerFor === lb.id"
class="absolute left-0 top-full z-10 mt-1 flex max-w-[13rem] flex-wrap gap-1.5 rounded-lg border border-neutral-200 bg-white p-2 shadow-lg dark:border-neutral-700 dark:bg-neutral-800"
>
<button
v-for="key in NOTE_COLOR_KEYS"
:key="key"
type="button"
:title="NOTE_COLOR_LABELS[key]"
:aria-label="NOTE_COLOR_LABELS[key]"
class="h-6 w-6 rounded-full border border-black/10 transition hover:scale-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand"
:class="[NOTE_SWATCH_CLASSES[key], lb.color === key ? 'ring-2 ring-brand' : '']"
@click="pickColor(lb.id, key)"
/>
</div>
</li>
</ul>
<p v-if="!labels.items.length" class="py-2 text-center text-xs text-neutral-400">