feat(contacts,calendar): integrate shared TagStrip and createTagStore

Contacts:
- Replace local TagStrip with shared TagStrip from @manacore/shared-ui
- Replace local tags store with createTagStore wrapper (backward-compatible)
- Change Tags nav item from link to toggle pill (shows/hides TagStrip overlay)

Calendar:
- Replace local TagStrip in UnifiedBar with shared TagStrip component
- Replace local event-tags store with createTagStore wrapper (backward-compatible)
- Both apps now use central mana-core-auth Tags API

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-26 21:05:50 +01:00
parent ce900d5fd3
commit 69aa837898
4 changed files with 160 additions and 181 deletions

View file

@ -1,10 +1,11 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { QuickInputBar } from '@manacore/shared-ui';
import { QuickInputBar, TagStrip } from '@manacore/shared-ui';
import type { QuickInputItem, CreatePreview } from '@manacore/shared-ui';
import { unifiedBarStore } from '$lib/stores/unified-bar.svelte';
import { eventTagsStore } from '$lib/stores/event-tags.svelte';
import { settingsStore } from '$lib/stores/settings.svelte';
import DateStrip from './DateStrip.svelte';
import TagStrip from './TagStrip.svelte';
import CalendarToolbarContent from './CalendarToolbarContent.svelte';
import { viewStore } from '$lib/stores/view.svelte';
import type { CalendarViewType } from '@calendar/shared';
@ -126,7 +127,18 @@
<!-- Layer 2: Tag Filter Strip -->
{#if showCalendarLayers && unifiedBarStore.showTagStrip}
<div class="unified-bar-layer tag-layer" transition:fly={flyConfig}>
<TagStrip />
<TagStrip
tags={eventTagsStore.tags.map((t) => ({
id: t.id,
name: t.name,
color: t.color || '#3b82f6',
}))}
selectedIds={settingsStore.selectedTagIds}
onToggle={(tagId) => settingsStore.toggleTagSelection(tagId)}
onClear={() => settingsStore.clearTagSelection()}
managementHref="/tags"
loading={eventTagsStore.loading}
/>
</div>
{/if}

View file

@ -1,111 +1,93 @@
/**
* Event Tags Store - Manages event tags using Svelte 5 runes
* Event Tags Store - Uses shared Tag Store backed by central mana-core-auth
*
* Wraps createTagStore to provide a backward-compatible eventTagsStore interface
* that existing Calendar components (TagStripModal, EventForm, /tags page) rely on.
*/
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';
import type { EventTag } from '@calendar/shared';
import * as api from '$lib/api/event-tags';
// State
let tags = $state<EventTag[]>([]);
let loading = $state(false);
let error = $state<string | null>(null);
// Re-export EventTag for backward compatibility
export type { EventTag };
// Helper to safely get tags array (Svelte 5 runes safety)
function getTagsArray(): EventTag[] {
const arr = tags ?? [];
return Array.isArray(arr) ? arr : [];
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 wrapper that returns { data, error } results
* to match the old Calendar API client pattern used by TagStripModal.
*/
async function wrapResult<T>(
fn: () => Promise<T>
): Promise<{ data: T | null; error: { message: string } | null }> {
try {
const data = await fn();
return { data, error: null };
} catch (e) {
const message = e instanceof Error ? e.message : 'Unknown error';
return { data: null, error: { message } };
}
}
// Backward-compatible eventTagsStore wrapper
export const eventTagsStore = {
// Getters
get tags() {
return tags;
return tagStore.tags as unknown as EventTag[];
},
get loading() {
return loading;
return tagStore.loading;
},
get error() {
return error;
return tagStore.error;
},
/**
* Fetch all tags
*/
async fetchTags() {
loading = true;
error = null;
const result = await api.getEventTags();
if (result.error) {
error = result.error.message;
tags = [];
} else {
tags = result.data || [];
}
loading = false;
return result;
return tagStore.fetchTags();
},
/**
* Create a new tag
*/
async createTag(data: api.CreateEventTagInput) {
const result = await api.createEventTag(data);
if (result.data) {
tags = [...tags, result.data];
}
return result;
},
/**
* Update a tag
*/
async updateTag(id: string, data: api.UpdateEventTagInput) {
const result = await api.updateEventTag(id, data);
if (result.data) {
tags = getTagsArray().map((t) => (t.id === id ? result.data! : t));
}
return result;
},
/**
* Delete a tag
*/
async deleteTag(id: string) {
const result = await api.deleteEventTag(id);
if (!result.error) {
tags = getTagsArray().filter((t) => t.id !== id);
}
return result;
},
/**
* Get tag by ID
*/
getById(id: string) {
return getTagsArray().find((t) => t.id === id);
return tagStore.getById(id) as unknown as EventTag | undefined;
},
/**
* Get tags by IDs
*/
getByIds(ids: string[]) {
return getTagsArray().filter((t) => ids.includes(t.id));
return tagStore.getByIds(ids) as unknown as EventTag[];
},
async createTag(data: { name: string; color?: string; groupId?: string | null }) {
return wrapResult(() => tagStore.createTag(data)) as Promise<{
data: EventTag | null;
error: { message: string } | null;
}>;
},
async updateTag(id: string, data: { name?: string; color?: string; groupId?: string | null }) {
return wrapResult(() => tagStore.updateTag(id, data)) as Promise<{
data: EventTag | null;
error: { message: string } | null;
}>;
},
async deleteTag(id: string) {
return wrapResult(() => tagStore.deleteTag(id));
},
/**
* Clear store
*/
clear() {
tags = [];
error = null;
tagStore.clear();
},
};

View file

@ -1,117 +1,65 @@
/**
* Tags Store - Manages tag state using Svelte 5 runes
* Tags Store - Uses shared Tag Store backed by central mana-core-auth
*
* Centralized store for tags, used by TagStrip, TagStripModal, and tags page.
* Uses the central Tags API from mana-core-auth.
* Wraps createTagStore for backward compatibility with existing ContactTag interface.
*/
import { tagsApi, type ContactTag } from '$lib/api/contacts';
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 tags = $state<ContactTag[]>([]);
let loading = $state(false);
let error = $state<string | null>(null);
// Re-export Tag as ContactTag for backward compatibility
export type ContactTag = 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 tagsStore wrapper
export const tagsStore = {
// Getters
get tags() {
return tags;
return tagStore.tags;
},
get loading() {
return loading;
return tagStore.loading;
},
get error() {
return error;
return tagStore.error;
},
/**
* Fetch all tags from API
*/
async fetchTags() {
loading = true;
error = null;
try {
const response = await tagsApi.list();
tags = response.tags || [];
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to fetch tags';
console.error('Failed to fetch tags:', e);
} finally {
loading = false;
}
return tagStore.fetchTags();
},
getById(id: string) {
return tagStore.getById(id);
},
getColor(tagId: string) {
return tagStore.getColor(tagId);
},
/**
* Get tag by ID
*/
getById(id: string): ContactTag | undefined {
return tags.find((t) => t.id === id);
},
/**
* Get tag color by ID
*/
getColor(tagId: string): string {
const tag = tags.find((t) => t.id === tagId);
return tag?.color || '#6b7280';
},
/**
* Create a new tag
*/
async createTag(data: { name: string; color?: string }) {
loading = true;
error = null;
try {
const response = await tagsApi.create(data);
tags = [...tags, response.tag];
return response.tag;
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to create tag';
console.error('Failed to create tag:', e);
throw e;
} finally {
loading = false;
}
return tagStore.createTag(data);
},
/**
* Update an existing tag
*/
async updateTag(id: string, data: { name?: string; color?: string }) {
error = null;
try {
const response = await tagsApi.update(id, data);
tags = tags.map((t) => (t.id === id ? response.tag : t));
return response.tag;
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to update tag';
console.error('Failed to update tag:', e);
throw e;
}
return tagStore.updateTag(id, data);
},
/**
* Delete a tag
*/
async deleteTag(id: string) {
error = null;
try {
await tagsApi.delete(id);
tags = tags.filter((t) => t.id !== id);
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to delete tag';
console.error('Failed to delete tag:', e);
throw e;
}
return tagStore.deleteTag(id);
},
/**
* Clear all state (for logout)
*/
clear() {
tags = [];
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,
@ -41,7 +46,6 @@
formatParsedContactPreview,
} from '$lib/utils/contact-parser';
import ContactsToolbar from '$lib/components/ContactsToolbar.svelte';
import TagStrip from '$lib/components/TagStrip.svelte';
import { tagsStore } from '$lib/stores/tags.svelte';
import { contactsOnboarding } from '$lib/stores/app-onboarding.svelte';
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
@ -128,10 +132,23 @@
// User email for user dropdown (fallback to 'Menü' when not logged in)
let userEmail = $derived(authStore.user?.email || 'Menü');
// TagStrip visibility (toggle via Tags button in PillNav)
let isTagStripVisible = $state(true);
function handleTagStripToggle() {
isTagStripVisible = !isTagStripVisible;
}
// Base navigation items for Contacts
const baseNavItems: PillNavItem[] = [
{ href: '/', label: 'Kontakte', icon: 'users' },
{ href: '/tags', label: 'Tags', icon: 'tag' },
{
href: '/',
label: 'Tags',
icon: 'tag',
onClick: handleTagStripToggle,
active: isTagStripVisible,
},
{ href: '/settings', label: 'Einstellungen', icon: 'settings' },
{ href: '/help', label: 'Hilfe', icon: 'help-circle' },
{ href: '/spiral', label: 'Spiral', icon: 'sparkles' },
@ -332,8 +349,28 @@
ariaLabel="Hauptnavigation"
/>
<!-- TagStrip (above PillNav) -->
<TagStrip />
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
{#if isTagStripVisible}
<TagStrip
tags={tagsStore.tags.map((t) => ({
id: t.id,
name: t.name,
color: t.color || '#3b82f6',
}))}
selectedIds={contactsFilterStore.selectedTagId
? [contactsFilterStore.selectedTagId]
: []}
onToggle={(tagId) => {
if (contactsFilterStore.selectedTagId === tagId) {
contactsFilterStore.setSelectedTagId(null);
} else {
contactsFilterStore.setSelectedTagId(tagId);
}
}}
onClear={() => contactsFilterStore.setSelectedTagId(null)}
managementHref="/tags"
/>
{/if}
<!-- Global Quick Input Bar -->
<QuickInputBar