mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-19 07:01:23 +02:00
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:
parent
794424d6ad
commit
019f3eb9fd
3 changed files with 215 additions and 2 deletions
|
|
@ -6,9 +6,10 @@
|
||||||
import { liveQuery } from 'dexie';
|
import { liveQuery } from 'dexie';
|
||||||
import { db } from '$lib/data/database';
|
import { db } from '$lib/data/database';
|
||||||
import { eventsStore } from '../stores/events.svelte';
|
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 { ViewProps } from '$lib/components/workbench/nav-stack';
|
||||||
import type { LocalEvent } from '../types';
|
import type { LocalEvent } from '../types';
|
||||||
|
import { useAllTags, getTagsByIds } from '$lib/stores/tags.svelte';
|
||||||
|
|
||||||
let { navigate, goBack, params }: ViewProps = $props();
|
let { navigate, goBack, params }: ViewProps = $props();
|
||||||
let eventId = $derived(params.eventId as string);
|
let eventId = $derived(params.eventId as string);
|
||||||
|
|
@ -26,6 +27,18 @@
|
||||||
|
|
||||||
let focused = $state(false);
|
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(() => {
|
$effect(() => {
|
||||||
eventId; // track
|
eventId; // track
|
||||||
confirmDelete = false;
|
confirmDelete = false;
|
||||||
|
|
@ -145,6 +158,26 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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 -->
|
<!-- Description -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<span class="section-label">Beschreibung</span>
|
<span class="section-label">Beschreibung</span>
|
||||||
|
|
@ -298,6 +331,42 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.375rem;
|
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 {
|
.section-label {
|
||||||
font-size: 0.6875rem;
|
font-size: 0.6875rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,11 @@
|
||||||
MapPin,
|
MapPin,
|
||||||
Briefcase,
|
Briefcase,
|
||||||
Globe,
|
Globe,
|
||||||
|
X,
|
||||||
} from '@manacore/shared-icons';
|
} from '@manacore/shared-icons';
|
||||||
import type { ViewProps } from '$lib/components/workbench/nav-stack';
|
import type { ViewProps } from '$lib/components/workbench/nav-stack';
|
||||||
import type { LocalContact } from '../types';
|
import type { LocalContact } from '../types';
|
||||||
|
import { useAllTags, getTagsByIds } from '$lib/stores/tags.svelte';
|
||||||
|
|
||||||
let { navigate, goBack, params }: ViewProps = $props();
|
let { navigate, goBack, params }: ViewProps = $props();
|
||||||
let contactId = $derived(params.contactId as string);
|
let contactId = $derived(params.contactId as string);
|
||||||
|
|
@ -40,6 +42,18 @@
|
||||||
let editWebsite = $state('');
|
let editWebsite = $state('');
|
||||||
let editNotes = $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(() => {
|
$effect(() => {
|
||||||
contactId; // track
|
contactId; // track
|
||||||
confirmDelete = false;
|
confirmDelete = false;
|
||||||
|
|
@ -255,6 +269,26 @@
|
||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Notes -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<span class="section-label">Notizen</span>
|
<span class="section-label">Notizen</span>
|
||||||
|
|
@ -447,6 +481,42 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.375rem;
|
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 {
|
.section-label {
|
||||||
font-size: 0.6875rem;
|
font-size: 0.6875rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,10 @@
|
||||||
import { liveQuery } from 'dexie';
|
import { liveQuery } from 'dexie';
|
||||||
import { db } from '$lib/data/database';
|
import { db } from '$lib/data/database';
|
||||||
import { tasksStore } from '../stores/tasks.svelte';
|
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 { ViewProps } from '$lib/components/workbench/nav-stack';
|
||||||
import type { LocalTask, TaskPriority } from '../types';
|
import type { LocalTask, TaskPriority } from '../types';
|
||||||
|
import { useAllTags, getTagsByIds } from '$lib/stores/tags.svelte';
|
||||||
|
|
||||||
let { navigate, goBack, params }: ViewProps = $props();
|
let { navigate, goBack, params }: ViewProps = $props();
|
||||||
let taskId = $derived(params.taskId as string);
|
let taskId = $derived(params.taskId as string);
|
||||||
|
|
@ -25,6 +26,23 @@
|
||||||
// Track whether user is actively editing to prevent overwrite from liveQuery
|
// Track whether user is actively editing to prevent overwrite from liveQuery
|
||||||
let focused = $state(false);
|
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(() => {
|
$effect(() => {
|
||||||
taskId; // track
|
taskId; // track
|
||||||
confirmDelete = false;
|
confirmDelete = false;
|
||||||
|
|
@ -152,6 +170,26 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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 -->
|
<!-- Description -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<span class="section-label">Beschreibung</span>
|
<span class="section-label">Beschreibung</span>
|
||||||
|
|
@ -349,6 +387,42 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.375rem;
|
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 {
|
.section-label {
|
||||||
font-size: 0.6875rem;
|
font-size: 0.6875rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue