feat(todo): integrate shared TagStrip and createTagStore

- Add createTagStore factory to @manacore/shared-stores (Svelte 5 runes, backed by @manacore/shared-tags)
- Replace Todo's local TagStrip with shared TagStrip from @manacore/shared-ui
- Replace Todo's labels store with createTagStore wrapper (backward-compatible)
- Remove "Tags" tab from PillNav TabGroup, add it as toggle pill (like Filter)
- Tags pill toggles TagStrip overlay visibility instead of navigating to /tags
- TagStrip has "Tags verwalten" pill linking to /tags management page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-26 20:51:55 +01:00
parent 1316ef57f3
commit ce900d5fd3
5 changed files with 333 additions and 103 deletions

View file

@ -1,119 +1,62 @@
/**
* Labels Store - Manages label state using Svelte 5 runes
*
* Uses the central Tags API from mana-core-auth. Labels and Tags are now
* unified across all Manacore apps (Todo, Calendar, Contacts).
* Labels Store - Uses shared Tag Store backed by central mana-core-auth
*/
import type { Label } from '$lib/api/labels';
import * as labelsApi from '$lib/api/labels';
import { TodoEvents } from '@manacore/shared-utils/analytics';
import { browser } from '$app/environment';
import { createTagStore, type TagStore } from '@manacore/shared-stores';
import { authStore } from '$lib/stores/auth.svelte';
import type { Tag } from '@manacore/shared-tags';
// State
let labels = $state<Label[]>([]);
let loading = $state(false);
let error = $state<string | null>(null);
// Re-export Tag as Label for backward compatibility
export type Label = Tag;
function getAuthUrl(): string {
if (browser && typeof window !== 'undefined') {
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
.__PUBLIC_MANA_CORE_AUTH_URL__;
return injectedUrl || 'http://localhost:3001';
}
return 'http://localhost:3001';
}
// Create the shared tag store
const tagStore: TagStore = createTagStore({
authUrl: getAuthUrl(),
getToken: () => authStore.getValidToken(),
});
// Backward-compatible labelsStore wrapper
export const labelsStore = {
// Getters
get labels() {
return labels;
return tagStore.tags;
},
get loading() {
return loading;
return tagStore.loading;
},
get error() {
return error;
return tagStore.error;
},
/**
* Fetch all labels from API
*/
async fetchLabels() {
loading = true;
error = null;
try {
labels = await labelsApi.getLabels();
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to fetch labels';
console.error('Failed to fetch labels:', e);
} finally {
loading = false;
}
return tagStore.fetchTags();
},
getById(id: string) {
return tagStore.getById(id);
},
getColor(labelId: string) {
return tagStore.getColor(labelId);
},
/**
* Get label by ID
*/
getById(id: string): Label | undefined {
return labels.find((l) => l.id === id);
},
/**
* Get label color by ID
*/
getColor(labelId: string): string {
const label = labels.find((l) => l.id === labelId);
return label?.color || '#6b7280';
},
/**
* Create a new label
*/
async createLabel(data: { name: string; color?: string }) {
loading = true;
error = null;
try {
const newLabel = await labelsApi.createLabel(data);
labels = [...labels, newLabel];
TodoEvents.labelCreated();
return newLabel;
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to create label';
console.error('Failed to create label:', e);
throw e;
} finally {
loading = false;
}
return tagStore.createTag(data);
},
/**
* Update an existing label
*/
async updateLabel(id: string, data: { name?: string; color?: string }) {
error = null;
try {
const updatedLabel = await labelsApi.updateLabel(id, data);
labels = labels.map((l) => (l.id === id ? updatedLabel : l));
return updatedLabel;
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to update label';
console.error('Failed to update label:', e);
throw e;
}
return tagStore.updateTag(id, data);
},
/**
* Delete a label
*/
async deleteLabel(id: string) {
error = null;
try {
await labelsApi.deleteLabel(id);
labels = labels.filter((l) => l.id !== id);
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to delete label';
console.error('Failed to delete label:', e);
throw e;
}
return tagStore.deleteTag(id);
},
/**
* Clear all state (for logout)
*/
clear() {
labels = [];
loading = false;
error = null;
tagStore.clear();
},
};

View file

@ -2,7 +2,12 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { locale } from 'svelte-i18n';
import { PillNavigation, QuickInputBar, ImmersiveModeToggle } from '@manacore/shared-ui';
import {
PillNavigation,
QuickInputBar,
ImmersiveModeToggle,
TagStrip,
} from '@manacore/shared-ui';
import {
SplitPaneContainer,
setSplitPanelContext,
@ -23,7 +28,6 @@
import { tasksStore } from '$lib/stores/tasks.svelte';
import { theme } from '$lib/stores/theme';
import TaskFilters from '$lib/components/TaskFilters.svelte';
import TagStrip from '$lib/components/TagStrip.svelte';
import { viewStore, type SortBy } from '$lib/stores/view.svelte';
import type { TaskPriority } from '@todo/shared';
import {
@ -171,11 +175,17 @@
todoSettings.toggleFilterStrip();
}
// TagStrip visibility (toggle via Tags button in PillNav)
let isTagStripVisible = $state(true);
function handleTagStripToggle() {
isTagStripVisible = !isTagStripVisible;
}
// View routes for the tab group (pages that navigate)
const viewRoutes: Record<string, string> = {
liste: '/',
kanban: '/kanban',
tags: '/tags',
};
// Determine active view tab from current path
@ -183,13 +193,12 @@
Object.entries(viewRoutes).find(([_, path]) => $page.url.pathname === path)?.[0] || 'liste'
);
// Tab group for view switching (Liste, Kanban, Tags) - grouped in one pill
// Tab group for view switching (Liste, Kanban) - grouped in one pill
let viewTabGroup = $derived<PillNavElement>({
type: 'tabs' as const,
options: [
{ id: 'liste', icon: 'list', label: 'Liste', title: 'Listenansicht' },
{ id: 'kanban', icon: 'columns', label: 'Kanban', title: 'Kanban-Board' },
{ id: 'tags', icon: 'tag', label: 'Tags', title: 'Tags verwalten' },
],
value: activeViewTab,
onChange: (id: string) => {
@ -198,7 +207,7 @@
},
});
// Filter stays as a standalone pill (toggle behavior, not navigation)
// Filter and Tags stay as standalone pills (toggle behavior, not navigation)
let baseNavItems = $derived<PillNavItem[]>([
{
href: '/',
@ -207,6 +216,13 @@
onClick: handleFilterToggle,
active: isFilterStripVisible,
},
{
href: '/',
label: 'Tags',
icon: 'tag',
onClick: handleTagStripToggle,
active: isTagStripVisible,
},
]);
// Navigation items filtered by visibility settings (with fallback for guest mode)
@ -353,8 +369,28 @@
ariaLabel="Hauptnavigation"
/>
<!-- TagStrip (above PillNav, always visible when PillNav is open) -->
<TagStrip filterStripVisible={isFilterStripVisible} />
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
{#if isTagStripVisible}
<TagStrip
tags={labelsStore.labels.map((l) => ({
id: l.id,
name: l.name,
color: l.color || '#6b7280',
}))}
selectedIds={viewStore.filterLabelIds}
onToggle={(tagId) => {
const current = viewStore.filterLabelIds;
if (current.includes(tagId)) {
viewStore.setFilterLabelIds(current.filter((id) => id !== tagId));
} else {
viewStore.setFilterLabelIds([...current, tagId]);
}
}}
onClear={() => viewStore.setFilterLabelIds([])}
managementHref="/tags"
aboveFilterStrip={isFilterStripVisible}
/>
{/if}
<!-- TaskFilters strip (shown when Filter pill is active in PillNav) -->
{#if isFilterStripVisible}