feat(manacore/web): show tags in detail views with click-to-remove

Display assigned tags as colored pills in todo, calendar, contacts
DetailViews. Clicking a tag pill removes it from the item. Hover turns
the X icon red for clear affordance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-03 11:37:41 +02:00
parent 794424d6ad
commit 019f3eb9fd
3 changed files with 215 additions and 2 deletions

View file

@ -6,9 +6,10 @@
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import { eventsStore } from '../stores/events.svelte';
import { Trash, MapPin, Clock } from '@manacore/shared-icons';
import { Trash, MapPin, Clock, X } from '@manacore/shared-icons';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { LocalEvent } from '../types';
import { useAllTags, getTagsByIds } from '$lib/stores/tags.svelte';
let { navigate, goBack, params }: ViewProps = $props();
let eventId = $derived(params.eventId as string);
@ -26,6 +27,18 @@
let focused = $state(false);
const tagsQuery = useAllTags();
let allTags = $derived(tagsQuery.value ?? []);
let eventTags = $derived(getTagsByIds(allTags, event?.tagIds ?? []));
async function removeTag(tagId: string) {
const current = event?.tagIds ?? [];
await eventsStore.updateTagIds(
eventId,
current.filter((id) => id !== tagId)
);
}
$effect(() => {
eventId; // track
confirmDelete = false;
@ -145,6 +158,26 @@
{/if}
</div>
<!-- Tags -->
{#if eventTags.length > 0}
<div class="section">
<span class="section-label">Tags</span>
<div class="tags-list">
{#each eventTags as tag (tag.id)}
<button
class="tag-pill"
style="--tag-color: {tag.color}"
onclick={() => removeTag(tag.id)}
>
<span class="tag-dot" style="background: {tag.color}"></span>
{tag.name}
<X size={10} />
</button>
{/each}
</div>
</div>
{/if}
<!-- Description -->
<div class="section">
<span class="section-label">Beschreibung</span>
@ -298,6 +331,42 @@
flex-direction: column;
gap: 0.375rem;
}
.tags-list {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
}
.tag-pill {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
border: none;
background: color-mix(in srgb, var(--tag-color) 12%, transparent);
font-size: 0.6875rem;
color: #6b7280;
cursor: pointer;
transition: all 0.15s;
}
.tag-pill:hover {
background: color-mix(in srgb, var(--tag-color) 20%, transparent);
color: #ef4444;
}
:global(.dark) .tag-pill {
background: color-mix(in srgb, var(--tag-color) 18%, transparent);
color: #9ca3af;
}
:global(.dark) .tag-pill:hover {
background: color-mix(in srgb, var(--tag-color) 28%, transparent);
color: #ef4444;
}
.tag-dot {
width: 6px;
height: 6px;
border-radius: 9999px;
flex-shrink: 0;
}
.section-label {
font-size: 0.6875rem;
font-weight: 600;

View file

@ -14,9 +14,11 @@
MapPin,
Briefcase,
Globe,
X,
} from '@manacore/shared-icons';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { LocalContact } from '../types';
import { useAllTags, getTagsByIds } from '$lib/stores/tags.svelte';
let { navigate, goBack, params }: ViewProps = $props();
let contactId = $derived(params.contactId as string);
@ -40,6 +42,18 @@
let editWebsite = $state('');
let editNotes = $state('');
const tagsQuery = useAllTags();
let allTags = $derived(tagsQuery.value ?? []);
let contactTags = $derived(getTagsByIds(allTags, contact?.tagIds ?? []));
async function removeTag(tagId: string) {
const current = contact?.tagIds ?? [];
await contactsStore.updateTagIds(
contactId,
current.filter((id) => id !== tagId)
);
}
$effect(() => {
contactId; // track
confirmDelete = false;
@ -255,6 +269,26 @@
</div>
</div>
<!-- Tags -->
{#if contactTags.length > 0}
<div class="section">
<span class="section-label">Tags</span>
<div class="tags-list">
{#each contactTags as tag (tag.id)}
<button
class="tag-pill"
style="--tag-color: {tag.color}"
onclick={() => removeTag(tag.id)}
>
<span class="tag-dot" style="background: {tag.color}"></span>
{tag.name}
<X size={10} />
</button>
{/each}
</div>
</div>
{/if}
<!-- Notes -->
<div class="section">
<span class="section-label">Notizen</span>
@ -447,6 +481,42 @@
flex-direction: column;
gap: 0.375rem;
}
.tags-list {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
}
.tag-pill {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
border: none;
background: color-mix(in srgb, var(--tag-color) 12%, transparent);
font-size: 0.6875rem;
color: #6b7280;
cursor: pointer;
transition: all 0.15s;
}
.tag-pill:hover {
background: color-mix(in srgb, var(--tag-color) 20%, transparent);
color: #ef4444;
}
:global(.dark) .tag-pill {
background: color-mix(in srgb, var(--tag-color) 18%, transparent);
color: #9ca3af;
}
:global(.dark) .tag-pill:hover {
background: color-mix(in srgb, var(--tag-color) 28%, transparent);
color: #ef4444;
}
.tag-dot {
width: 6px;
height: 6px;
border-radius: 9999px;
flex-shrink: 0;
}
.section-label {
font-size: 0.6875rem;
font-weight: 600;

View file

@ -6,9 +6,10 @@
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import { tasksStore } from '../stores/tasks.svelte';
import { Check, Trash } from '@manacore/shared-icons';
import { Check, Trash, X } from '@manacore/shared-icons';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { LocalTask, TaskPriority } from '../types';
import { useAllTags, getTagsByIds } from '$lib/stores/tags.svelte';
let { navigate, goBack, params }: ViewProps = $props();
let taskId = $derived(params.taskId as string);
@ -25,6 +26,23 @@
// Track whether user is actively editing to prevent overwrite from liveQuery
let focused = $state(false);
const tagsQuery = useAllTags();
let allTags = $derived(tagsQuery.value ?? []);
function getTaskTagIds(): string[] {
return ((task?.metadata as Record<string, unknown>)?.labelIds as string[]) ?? [];
}
let taskTags = $derived(getTagsByIds(allTags, getTaskTagIds()));
async function removeTag(tagId: string) {
const current = getTaskTagIds();
await tasksStore.updateLabels(
taskId,
current.filter((id) => id !== tagId)
);
}
$effect(() => {
taskId; // track
confirmDelete = false;
@ -152,6 +170,26 @@
{/if}
</div>
<!-- Tags -->
{#if taskTags.length > 0}
<div class="section">
<span class="section-label">Tags</span>
<div class="tags-list">
{#each taskTags as tag (tag.id)}
<button
class="tag-pill"
style="--tag-color: {tag.color}"
onclick={() => removeTag(tag.id)}
>
<span class="tag-dot" style="background: {tag.color}"></span>
{tag.name}
<X size={10} />
</button>
{/each}
</div>
</div>
{/if}
<!-- Description -->
<div class="section">
<span class="section-label">Beschreibung</span>
@ -349,6 +387,42 @@
flex-direction: column;
gap: 0.375rem;
}
.tags-list {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
}
.tag-pill {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
border: none;
background: color-mix(in srgb, var(--tag-color) 12%, transparent);
font-size: 0.6875rem;
color: #6b7280;
cursor: pointer;
transition: all 0.15s;
}
.tag-pill:hover {
background: color-mix(in srgb, var(--tag-color) 20%, transparent);
color: #ef4444;
}
:global(.dark) .tag-pill {
background: color-mix(in srgb, var(--tag-color) 18%, transparent);
color: #9ca3af;
}
:global(.dark) .tag-pill:hover {
background: color-mix(in srgb, var(--tag-color) 28%, transparent);
color: #ef4444;
}
.tag-dot {
width: 6px;
height: 6px;
border-radius: 9999px;
flex-shrink: 0;
}
.section-label {
font-size: 0.6875rem;
font-weight: 600;