feat(apps): integrate shared TagStrip into all 15 remaining apps

Migrated apps with existing local tags (photos, storage, picture):
- Replace local tag stores with createTagStore wrapper
- Add shared TagStrip to layouts with tag filtering support
- Storage: new tag store, /tags management page
- Picture: migrated from Svelte 4 writables to createTagStore

New TagStrip added to 12 apps without prior tag system:
- chat, citycorners, clock, context, manadeck, manacore, matrix,
  mukke, planta, presi, questions, zitare
- Each gets: tag store, Tags toggle pill in PillNav, TagStrip overlay,
  /tags management page, fetchTags on auth ready
- All backed by central mana-core-auth Tags API

All 18 apps now have:
- Tags pill in PillNav (toggles TagStrip overlay)
- Shared TagStrip component from @manacore/shared-ui
- Tag store using createTagStore from @manacore/shared-stores
- /tags management page
- Cross-app tags via central mana-core-auth

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-26 21:41:14 +01:00
parent 9c8bae3dea
commit 91116bf0f1
57 changed files with 1852 additions and 93 deletions

View file

@ -47,6 +47,7 @@
"@manacore/shared-icons": "workspace:*",
"@manacore/shared-profile-ui": "workspace:*",
"@manacore/shared-stores": "workspace:*",
"@manacore/shared-tags": "workspace:*",
"@manacore/shared-subscription-ui": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",
"@manacore/shared-theme": "workspace:*",

View file

@ -0,0 +1,20 @@
/**
* Tag Store - Uses shared createTagStore backed by central mana-core-auth
*/
import { browser } from '$app/environment';
import { createTagStore } from '@manacore/shared-stores';
import { authStore } from '$lib/stores/auth.svelte';
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';
}
export const tagStore = createTagStore({
authUrl: getAuthUrl(),
getToken: () => authStore.getValidToken(),
});

View file

@ -15,8 +15,9 @@
import type { ThemeVariant } from '@manacore/shared-theme';
import { filterHiddenNavItems } from '@manacore/shared-theme';
import { isNavCollapsed as collapsedStore } from '$lib/stores/navigation';
import { PillNavigation } from '@manacore/shared-ui';
import { PillNavigation, TagStrip } from '@manacore/shared-ui';
import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui';
import { tagStore } from '$lib/stores/tags.svelte';
import { getPillAppItems } from '@manacore/shared-branding';
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { setLocale, supportedLocales } from '$lib/i18n';
@ -78,6 +79,12 @@
);
let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale));
// TagStrip visibility
let isTagStripVisible = $state(false);
function handleTagStripToggle() {
isTagStripVisible = !isTagStripVisible;
}
// Base navigation items for Chat (settings moved to user dropdown)
const baseNavItems: PillNavItem[] = [
{ href: '/chat', label: 'Chat', icon: 'home' },
@ -86,6 +93,13 @@
{ href: '/spaces', label: 'Spaces', icon: 'building' },
{ href: '/documents', label: 'Dokumente', icon: 'archive' },
{ href: '/archive', label: 'Archiv', icon: 'list' },
{
href: '/',
label: 'Tags',
icon: 'tag',
onClick: handleTagStripToggle,
active: isTagStripVisible,
},
];
// Navigation items filtered by visibility settings (with fallback for guest mode)
@ -153,8 +167,9 @@
collapsedStore.set(true);
}
// Load user settings
// Load user settings and tags
await userSettings.load();
await tagStore.fetchTags();
// Check for session conversations to migrate
if (conversationsStore.hasSessionConversations) {
@ -207,6 +222,22 @@
allAppsHref="/apps"
/>
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
{#if isTagStripVisible}
<TagStrip
tags={tagStore.tags.map((t) => ({
id: t.id,
name: t.name,
color: t.color || '#3b82f6',
}))}
selectedIds={[]}
onToggle={() => {}}
onClear={() => {}}
managementHref="/tags"
loading={tagStore.loading}
/>
{/if}
<!-- Main Content -->
<main class="main-content bg-background" class:chat-page={isChatPage}>
{#if isChatPage}

View file

@ -0,0 +1,49 @@
<script lang="ts">
import { tagStore } from '$lib/stores/tags.svelte';
import { onMount } from 'svelte';
onMount(() => {
if (tagStore.tags.length === 0) {
tagStore.fetchTags();
}
});
</script>
<svelte:head>
<title>Tags | ManaChat</title>
</svelte:head>
<div class="tags-page">
<h1>Tags verwalten</h1>
<p class="text-sm text-muted-foreground mb-4">
Tags sind app-übergreifend — Änderungen gelten in allen ManaCore-Apps.
</p>
{#if tagStore.loading}
<p>Lädt...</p>
{:else if tagStore.tags.length === 0}
<p>Keine Tags vorhanden.</p>
{:else}
<div class="grid gap-2">
{#each tagStore.tags as tag}
<div class="flex items-center gap-2 p-2 rounded-lg bg-card">
<span class="w-3 h-3 rounded-full" style="background-color: {tag.color}"></span>
<span>{tag.name}</span>
</div>
{/each}
</div>
{/if}
</div>
<style>
.tags-page {
padding: 1.5rem;
max-width: 600px;
margin: 0 auto;
}
h1 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
</style>

View file

@ -43,6 +43,7 @@
"@manacore/shared-icons": "workspace:*",
"@manacore/shared-profile-ui": "workspace:*",
"@manacore/shared-stores": "workspace:*",
"@manacore/shared-tags": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",
"@manacore/shared-theme": "workspace:*",
"@manacore/shared-theme-ui": "workspace:*",

View file

@ -0,0 +1,20 @@
/**
* Tag Store - Uses shared createTagStore backed by central mana-core-auth
*/
import { browser } from '$app/environment';
import { createTagStore } from '@manacore/shared-stores';
import { authStore } from '$lib/stores/auth.svelte';
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';
}
export const tagStore = createTagStore({
authUrl: getAuthUrl(),
getToken: () => authStore.getValidToken(),
});

View file

@ -3,8 +3,9 @@
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { _, locale } from 'svelte-i18n';
import { PillNavigation, QuickInputBar } from '@manacore/shared-ui';
import { PillNavigation, QuickInputBar, TagStrip } from '@manacore/shared-ui';
import type { PillNavItem, PillDropdownItem, QuickInputItem } from '@manacore/shared-ui';
import { tagStore } from '$lib/stores/tags.svelte';
import { theme } from '$lib/stores/theme.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { favoritesStore } from '$lib/stores/favorites.svelte';
@ -50,12 +51,25 @@
let userEmail = $derived(authStore.user?.email || $_('nav.settings'));
// TagStrip visibility
let isTagStripVisible = $state(false);
function handleTagStripToggle() {
isTagStripVisible = !isTagStripVisible;
}
let navItems = $derived<PillNavItem[]>([
{ href: '/', label: $_('nav.explore'), icon: 'compass' },
{ href: '/map', label: $_('nav.map'), icon: 'mappin' },
{ href: '/add', label: $_('nav.add'), icon: 'plus' },
{ href: '/favorites', label: $_('nav.favorites'), icon: 'heart' },
{ href: '/settings', label: $_('nav.settings'), icon: 'settings' },
{
href: '/',
label: 'Tags',
icon: 'tag',
onClick: handleTagStripToggle,
active: isTagStripVisible,
},
]);
function handleToggleTheme() {
@ -158,9 +172,13 @@
showNav = !showNav;
}
onMount(() => {
onMount(async () => {
const savedNav = localStorage.getItem('citycorners-nav-visible');
if (savedNav !== null) showNav = savedNav !== 'false';
if (authStore.isAuthenticated) {
await tagStore.fetchTags();
}
});
</script>
@ -196,6 +214,22 @@
/>
{/if}
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
{#if isTagStripVisible}
<TagStrip
tags={tagStore.tags.map((t) => ({
id: t.id,
name: t.name,
color: t.color || '#3b82f6',
}))}
selectedIds={[]}
onToggle={() => {}}
onClear={() => {}}
managementHref="/tags"
loading={tagStore.loading}
/>
{/if}
<!-- Quick Search Bar -->
<QuickInputBar
onSearch={handleSearch}

View file

@ -0,0 +1,49 @@
<script lang="ts">
import { tagStore } from '$lib/stores/tags.svelte';
import { onMount } from 'svelte';
onMount(() => {
if (tagStore.tags.length === 0) {
tagStore.fetchTags();
}
});
</script>
<svelte:head>
<title>Tags | CityCorners</title>
</svelte:head>
<div class="tags-page">
<h1>Tags verwalten</h1>
<p class="text-sm text-muted-foreground mb-4">
Tags sind app-übergreifend — Änderungen gelten in allen ManaCore-Apps.
</p>
{#if tagStore.loading}
<p>Lädt...</p>
{:else if tagStore.tags.length === 0}
<p>Keine Tags vorhanden.</p>
{:else}
<div class="grid gap-2">
{#each tagStore.tags as tag}
<div class="flex items-center gap-2 p-2 rounded-lg bg-card">
<span class="w-3 h-3 rounded-full" style="background-color: {tag.color}"></span>
<span>{tag.name}</span>
</div>
{/each}
</div>
{/if}
</div>
<style>
.tags-page {
padding: 1.5rem;
max-width: 600px;
margin: 0 auto;
}
h1 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
</style>

View file

@ -50,6 +50,7 @@
"@manacore/shared-icons": "workspace:*",
"@manacore/shared-profile-ui": "workspace:*",
"@manacore/shared-stores": "workspace:*",
"@manacore/shared-tags": "workspace:*",
"@manacore/shared-subscription-ui": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",
"@manacore/shared-theme": "workspace:*",

View file

@ -0,0 +1,20 @@
/**
* Tag Store - Uses shared createTagStore backed by central mana-core-auth
*/
import { browser } from '$app/environment';
import { createTagStore } from '@manacore/shared-stores';
import { authStore } from '$lib/stores/auth.svelte';
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';
}
export const tagStore = createTagStore({
authUrl: getAuthUrl(),
getToken: () => authStore.getValidToken(),
});

View file

@ -2,7 +2,7 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { locale } from 'svelte-i18n';
import { PillNavigation, CommandBar } from '@manacore/shared-ui';
import { PillNavigation, CommandBar, TagStrip } from '@manacore/shared-ui';
import type {
PillNavItem,
PillDropdownItem,
@ -32,6 +32,7 @@
import { clockOnboarding } from '$lib/stores/app-onboarding.svelte';
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
import { SessionExpiredBanner, AuthGate } from '@manacore/shared-auth-ui';
import { tagStore } from '$lib/stores/tags.svelte';
// App switcher items
const appItems = getPillAppItems('clock');
@ -164,6 +165,12 @@
// User email for user dropdown
let userEmail = $derived(authStore.user?.email || 'Menü');
// TagStrip visibility
let isTagStripVisible = $state(false);
function handleTagStripToggle() {
isTagStripVisible = !isTagStripVisible;
}
// Base navigation items for Clock
const baseNavItems: PillNavItem[] = [
{ href: '/', label: 'Übersicht', icon: 'home' },
@ -174,6 +181,13 @@
{ href: '/world-clock', label: 'Weltzeituhr', icon: 'globe' },
{ href: '/life', label: 'Lebensuhr', icon: 'heart' },
{ href: '/settings', label: 'Einstellungen', icon: 'settings' },
{
href: '/',
label: 'Tags',
icon: 'tag',
onClick: handleTagStripToggle,
active: isTagStripVisible,
},
];
// Navigation items filtered by visibility settings
@ -239,8 +253,9 @@
collapsedStore.set(true);
}
// Load user settings
// Load user settings and tags
await userSettings.load();
await tagStore.fetchTags();
// Check for session data to migrate
if (alarmsStore.hasSessionAlarms) {
@ -295,6 +310,22 @@
allAppsHref="/apps"
/>
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
{#if isTagStripVisible}
<TagStrip
tags={tagStore.tags.map((t) => ({
id: t.id,
name: t.name,
color: t.color || '#3b82f6',
}))}
selectedIds={[]}
onToggle={() => {}}
onClear={() => {}}
managementHref="/tags"
loading={tagStore.loading}
/>
{/if}
<main class="main-content bg-background">
<div class="content-wrapper">
{@render children()}

View file

@ -0,0 +1,49 @@
<script lang="ts">
import { tagStore } from '$lib/stores/tags.svelte';
import { onMount } from 'svelte';
onMount(() => {
if (tagStore.tags.length === 0) {
tagStore.fetchTags();
}
});
</script>
<svelte:head>
<title>Tags | Clock</title>
</svelte:head>
<div class="tags-page">
<h1>Tags verwalten</h1>
<p class="text-sm text-muted-foreground mb-4">
Tags sind app-übergreifend — Änderungen gelten in allen ManaCore-Apps.
</p>
{#if tagStore.loading}
<p>Lädt...</p>
{:else if tagStore.tags.length === 0}
<p>Keine Tags vorhanden.</p>
{:else}
<div class="grid gap-2">
{#each tagStore.tags as tag}
<div class="flex items-center gap-2 p-2 rounded-lg bg-card">
<span class="w-3 h-3 rounded-full" style="background-color: {tag.color}"></span>
<span>{tag.name}</span>
</div>
{/each}
</div>
{/if}
</div>
<style>
.tags-page {
padding: 1.5rem;
max-width: 600px;
margin: 0 auto;
}
h1 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
</style>

View file

@ -46,6 +46,7 @@
"@manacore/shared-icons": "workspace:*",
"@manacore/shared-profile-ui": "workspace:*",
"@manacore/shared-stores": "workspace:*",
"@manacore/shared-tags": "workspace:*",
"@manacore/shared-subscription-ui": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",
"@manacore/shared-theme": "workspace:*",

View file

@ -0,0 +1,20 @@
/**
* Tag Store - Uses shared createTagStore backed by central mana-core-auth
*/
import { browser } from '$app/environment';
import { createTagStore } from '@manacore/shared-stores';
import { authStore } from '$lib/stores/auth.svelte';
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';
}
export const tagStore = createTagStore({
authUrl: getAuthUrl(),
getToken: () => authStore.getValidToken(),
});

View file

@ -3,7 +3,7 @@
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { locale } from 'svelte-i18n';
import { PillNavigation, CommandBar } from '@manacore/shared-ui';
import { PillNavigation, CommandBar, TagStrip } from '@manacore/shared-ui';
import type {
PillNavItem,
PillDropdownItem,
@ -28,6 +28,7 @@
import { setLocale, supportedLocales } from '$lib/i18n';
import { contextOnboarding } from '$lib/stores/app-onboarding.svelte';
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
import { tagStore } from '$lib/stores/tags.svelte';
const appItems = getPillAppItems('context');
@ -138,12 +139,25 @@
let userEmail = $derived(authStore.user?.email || 'Menü');
// TagStrip visibility
let isTagStripVisible = $state(false);
function handleTagStripToggle() {
isTagStripVisible = !isTagStripVisible;
}
const baseNavItems: PillNavItem[] = [
{ href: '/', label: 'Übersicht', icon: 'home' },
{ href: '/spaces', label: 'Spaces', icon: 'folder' },
{ href: '/documents', label: 'Dokumente', icon: 'file-text' },
{ href: '/tokens', label: 'Tokens', icon: 'sparkle' },
{ href: '/settings', label: 'Einstellungen', icon: 'settings' },
{
href: '/',
label: 'Tags',
icon: 'tag',
onClick: handleTagStripToggle,
active: isTagStripVisible,
},
];
const navItems = $derived(
@ -213,6 +227,9 @@
await userSettings.load();
// Load tags
await tagStore.fetchTags();
// Pre-load data for CommandBar search
await Promise.all([spacesStore.load(), documentsStore.load()]);
});
@ -254,6 +271,22 @@
allAppsHref="/apps"
/>
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
{#if isTagStripVisible}
<TagStrip
tags={tagStore.tags.map((t) => ({
id: t.id,
name: t.name,
color: t.color || '#3b82f6',
}))}
selectedIds={[]}
onToggle={() => {}}
onClear={() => {}}
managementHref="/tags"
loading={tagStore.loading}
/>
{/if}
<main class="main-content bg-background">
<div class="content-wrapper">
{@render children()}

View file

@ -0,0 +1,49 @@
<script lang="ts">
import { tagStore } from '$lib/stores/tags.svelte';
import { onMount } from 'svelte';
onMount(() => {
if (tagStore.tags.length === 0) {
tagStore.fetchTags();
}
});
</script>
<svelte:head>
<title>Tags | Context</title>
</svelte:head>
<div class="tags-page">
<h1>Tags verwalten</h1>
<p class="text-sm text-muted-foreground mb-4">
Tags sind app-übergreifend — Änderungen gelten in allen ManaCore-Apps.
</p>
{#if tagStore.loading}
<p>Lädt...</p>
{:else if tagStore.tags.length === 0}
<p>Keine Tags vorhanden.</p>
{:else}
<div class="grid gap-2">
{#each tagStore.tags as tag}
<div class="flex items-center gap-2 p-2 rounded-lg bg-card">
<span class="w-3 h-3 rounded-full" style="background-color: {tag.color}"></span>
<span>{tag.name}</span>
</div>
{/each}
</div>
{/if}
</div>
<style>
.tags-page {
padding: 1.5rem;
max-width: 600px;
margin: 0 auto;
}
h1 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
</style>

View file

@ -57,6 +57,7 @@
"@manacore/shared-icons": "workspace:*",
"@manacore/shared-profile-ui": "workspace:*",
"@manacore/shared-stores": "workspace:*",
"@manacore/shared-tags": "workspace:*",
"@manacore/shared-subscription-types": "workspace:*",
"@manacore/shared-subscription-ui": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",

View file

@ -0,0 +1,20 @@
/**
* Tag Store - Uses shared createTagStore backed by central mana-core-auth
*/
import { browser } from '$app/environment';
import { createTagStore } from '@manacore/shared-stores';
import { authStore } from '$lib/stores/auth.svelte';
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';
}
export const tagStore = createTagStore({
authUrl: getAuthUrl(),
getToken: () => authStore.getValidToken(),
});

View file

@ -6,8 +6,9 @@
import KeyboardShortcutsModal from '$lib/components/KeyboardShortcutsModal.svelte';
import SessionWarning from '$lib/components/SessionWarning.svelte';
import { locale } from 'svelte-i18n';
import { PillNavigation } from '@manacore/shared-ui';
import { PillNavigation, TagStrip } from '@manacore/shared-ui';
import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui';
import { tagStore } from '$lib/stores/tags.svelte';
import {
THEME_DEFINITIONS,
DEFAULT_THEME_VARIANTS,
@ -83,6 +84,12 @@
// User email for user dropdown
let userEmail = $derived(authStore.user?.email);
// TagStrip visibility
let isTagStripVisible = $state(false);
function handleTagStripToggle() {
isTagStripVisible = !isTagStripVisible;
}
// Navigation items for ManaCore
const baseNavItems: PillNavItem[] = [
{ href: '/home', label: 'Home', icon: 'home' },
@ -93,6 +100,13 @@
{ href: '/api-keys', label: 'API Keys', icon: 'key' },
{ href: '/profile', label: 'Profil', icon: 'user' },
{ href: '/settings', label: 'Settings', icon: 'settings' },
{
href: '/',
label: 'Tags',
icon: 'tag',
onClick: handleTagStripToggle,
active: isTagStripVisible,
},
];
// Only show admin link if user has admin role
@ -194,6 +208,9 @@
// Settings API not available - use defaults
});
// Load tags
tagStore.fetchTags();
// Load onboarding state and show wizard if needed
onboardingStore.load();
if (onboardingStore.shouldShow) {
@ -261,6 +278,22 @@
allAppsHref="/apps"
/>
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
{#if isTagStripVisible}
<TagStrip
tags={tagStore.tags.map((t) => ({
id: t.id,
name: t.name,
color: t.color || '#3b82f6',
}))}
selectedIds={[]}
onToggle={() => {}}
onClear={() => {}}
managementHref="/tags"
loading={tagStore.loading}
/>
{/if}
<!-- Main content -->
<main class="pb-24">
<div class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">

View file

@ -0,0 +1,49 @@
<script lang="ts">
import { tagStore } from '$lib/stores/tags.svelte';
import { onMount } from 'svelte';
onMount(() => {
if (tagStore.tags.length === 0) {
tagStore.fetchTags();
}
});
</script>
<svelte:head>
<title>Tags | ManaCore</title>
</svelte:head>
<div class="tags-page">
<h1>Tags verwalten</h1>
<p class="text-sm text-muted-foreground mb-4">
Tags sind app-übergreifend — Änderungen gelten in allen ManaCore-Apps.
</p>
{#if tagStore.loading}
<p>Lädt...</p>
{:else if tagStore.tags.length === 0}
<p>Keine Tags vorhanden.</p>
{:else}
<div class="grid gap-2">
{#each tagStore.tags as tag}
<div class="flex items-center gap-2 p-2 rounded-lg bg-card">
<span class="w-3 h-3 rounded-full" style="background-color: {tag.color}"></span>
<span>{tag.name}</span>
</div>
{/each}
</div>
{/if}
</div>
<style>
.tags-page {
padding: 1.5rem;
max-width: 600px;
margin: 0 auto;
}
h1 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
</style>

View file

@ -44,6 +44,7 @@
"@manacore/shared-icons": "workspace:*",
"@manacore/shared-profile-ui": "workspace:*",
"@manacore/shared-stores": "workspace:*",
"@manacore/shared-tags": "workspace:*",
"@manacore/shared-subscription-types": "workspace:*",
"@manacore/shared-subscription-ui": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",

View file

@ -0,0 +1,20 @@
/**
* Tag Store - Uses shared createTagStore backed by central mana-core-auth
*/
import { browser } from '$app/environment';
import { createTagStore } from '@manacore/shared-stores';
import { authStore } from '$lib/stores/auth.svelte';
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';
}
export const tagStore = createTagStore({
authUrl: getAuthUrl(),
getToken: () => authStore.getValidToken(),
});

View file

@ -7,8 +7,9 @@
import { userSettings } from '$lib/stores/user-settings.svelte';
import { theme } from '$lib/stores/theme';
import { isNavCollapsed as collapsedStore } from '$lib/stores/navigation';
import { PillNavigation, QuickInputBar } from '@manacore/shared-ui';
import { PillNavigation, QuickInputBar, TagStrip } from '@manacore/shared-ui';
import type { PillNavItem, PillDropdownItem, QuickInputItem } from '@manacore/shared-ui';
import { tagStore } from '$lib/stores/tags.svelte';
import {
THEME_DEFINITIONS,
DEFAULT_THEME_VARIANTS,
@ -33,11 +34,24 @@
// Get theme state
let isDark = $derived(theme.isDark);
// TagStrip visibility
let isTagStripVisible = $state(false);
function handleTagStripToggle() {
isTagStripVisible = !isTagStripVisible;
}
// Base navigation items for ManaDeck (Mana and Profile are in user dropdown)
const baseNavItems: PillNavItem[] = [
{ href: '/decks', label: 'Decks', icon: 'archive' },
{ href: '/explore', label: 'Explore', icon: 'search' },
{ href: '/progress', label: 'Progress', icon: 'chart' },
{
href: '/',
label: 'Tags',
icon: 'tag',
onClick: handleTagStripToggle,
active: isTagStripVisible,
},
];
// Navigation items filtered by visibility settings (with fallback for guest mode)
@ -165,8 +179,9 @@
return;
}
// Load user settings
// Load user settings and tags
await userSettings.load();
await tagStore.fetchTags();
// Redirect to start page if on root and a custom start page is set
const currentPath = window.location.pathname;
@ -229,6 +244,22 @@
allAppsHref="/apps"
/>
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
{#if isTagStripVisible}
<TagStrip
tags={tagStore.tags.map((t) => ({
id: t.id,
name: t.name,
color: t.color || '#3b82f6',
}))}
selectedIds={[]}
onToggle={() => {}}
onClear={() => {}}
managementHref="/tags"
loading={tagStore.loading}
/>
{/if}
<!-- Quick Input Bar -->
<QuickInputBar
onSearch={handleInputSearch}

View file

@ -0,0 +1,49 @@
<script lang="ts">
import { tagStore } from '$lib/stores/tags.svelte';
import { onMount } from 'svelte';
onMount(() => {
if (tagStore.tags.length === 0) {
tagStore.fetchTags();
}
});
</script>
<svelte:head>
<title>Tags | ManaDeck</title>
</svelte:head>
<div class="tags-page">
<h1>Tags verwalten</h1>
<p class="text-sm text-muted-foreground mb-4">
Tags sind app-übergreifend — Änderungen gelten in allen ManaCore-Apps.
</p>
{#if tagStore.loading}
<p>Lädt...</p>
{:else if tagStore.tags.length === 0}
<p>Keine Tags vorhanden.</p>
{:else}
<div class="grid gap-2">
{#each tagStore.tags as tag}
<div class="flex items-center gap-2 p-2 rounded-lg bg-card">
<span class="w-3 h-3 rounded-full" style="background-color: {tag.color}"></span>
<span>{tag.name}</span>
</div>
{/each}
</div>
{/if}
</div>
<style>
.tags-page {
padding: 1.5rem;
max-width: 600px;
margin: 0 auto;
}
h1 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
</style>

View file

@ -49,6 +49,8 @@
"@manacore/shared-icons": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",
"@manacore/shared-theme": "workspace:*",
"@manacore/shared-stores": "workspace:*",
"@manacore/shared-tags": "workspace:*",
"@manacore/shared-ui": "workspace:*",
"@matrix-org/matrix-sdk-crypto-wasm": "^13.0.0",
"buffer": "^6.0.3",

View file

@ -0,0 +1,24 @@
/**
* Tag Store - Uses shared createTagStore backed by central mana-core-auth
*
* Matrix uses its own auth (Matrix homeserver), not mana-core-auth directly.
* The mana-core-auth token is obtained via session-to-token exchange and stored
* in localStorage. Tags will work when user has a mana-core-auth session.
*/
import { browser } from '$app/environment';
import { createTagStore } from '@manacore/shared-stores';
import { loadStoredAccessToken } from '$lib/stores/userSettings.svelte';
const AUTH_URL = import.meta.env.VITE_MANA_AUTH_URL || 'https://auth.mana.how';
function getAuthUrl(): string {
return AUTH_URL;
}
export const tagStore = createTagStore({
authUrl: getAuthUrl(),
getToken: () => {
if (!browser) return null;
return loadStoredAccessToken();
},
});

View file

@ -20,8 +20,9 @@
} from '@manacore/shared-theme';
import type { ThemeVariant } from '@manacore/shared-theme';
import { isNavCollapsed as collapsedStore } from '$lib/stores/navigation.svelte';
import { PillNavigation } from '@manacore/shared-ui';
import { PillNavigation, TagStrip } from '@manacore/shared-ui';
import type { PillNavItem, PillDropdownItem, QuickInputItem } from '@manacore/shared-ui';
import { tagStore } from '$lib/stores/tags.svelte';
import { MagnifyingGlass, X } from '@manacore/shared-icons';
import { getPillAppItems } from '@manacore/shared-branding';
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
@ -71,6 +72,9 @@
// Load user settings (will use the token we just set)
await userSettings.load();
// Load tags (uses mana-core-auth token)
await tagStore.fetchTags();
}
// App switcher items
@ -121,11 +125,24 @@
);
let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale));
// TagStrip visibility
let isTagStripVisible = $state(false);
function handleTagStripToggle() {
isTagStripVisible = !isTagStripVisible;
}
// Navigation items for Matrix
const navItems: PillNavItem[] = [
{ href: '/chat', label: 'Chat', icon: 'home' },
{ href: '/bots', label: 'Bots', icon: 'robot' },
{ href: '/settings', label: 'Einstellungen', icon: 'settings' },
{
href: '/',
label: 'Tags',
icon: 'tag',
onClick: handleTagStripToggle,
active: isTagStripVisible,
},
];
// User info from Matrix
@ -422,6 +439,22 @@
/>
{/if}
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
{#if isTagStripVisible && !isMobileRoomView}
<TagStrip
tags={tagStore.tags.map((t) => ({
id: t.id,
name: t.name,
color: t.color || '#3b82f6',
}))}
selectedIds={[]}
onToggle={() => {}}
onClear={() => {}}
managementHref="/tags"
loading={tagStore.loading}
/>
{/if}
<!-- Main Content -->
<main class="main-content bg-background">
{@render children()}

View file

@ -0,0 +1,49 @@
<script lang="ts">
import { tagStore } from '$lib/stores/tags.svelte';
import { onMount } from 'svelte';
onMount(() => {
if (tagStore.tags.length === 0) {
tagStore.fetchTags();
}
});
</script>
<svelte:head>
<title>Tags | Manalink</title>
</svelte:head>
<div class="tags-page">
<h1>Tags verwalten</h1>
<p class="text-sm text-muted-foreground mb-4">
Tags sind app-übergreifend — Änderungen gelten in allen ManaCore-Apps.
</p>
{#if tagStore.loading}
<p>Lädt...</p>
{:else if tagStore.tags.length === 0}
<p>Keine Tags vorhanden.</p>
{:else}
<div class="grid gap-2">
{#each tagStore.tags as tag}
<div class="flex items-center gap-2 p-2 rounded-lg bg-card">
<span class="w-3 h-3 rounded-full" style="background-color: {tag.color}"></span>
<span>{tag.name}</span>
</div>
{/each}
</div>
{/if}
</div>
<style>
.tags-page {
padding: 1.5rem;
max-width: 600px;
margin: 0 auto;
}
h1 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
</style>

View file

@ -50,6 +50,7 @@
"@manacore/shared-profile-ui": "workspace:*",
"@manacore/shared-splitscreen": "workspace:^",
"@manacore/shared-stores": "workspace:*",
"@manacore/shared-tags": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",
"@manacore/shared-theme": "workspace:*",
"@manacore/shared-theme-ui": "workspace:*",

View file

@ -0,0 +1,20 @@
/**
* Tag Store - Uses shared createTagStore backed by central mana-core-auth
*/
import { browser } from '$app/environment';
import { createTagStore } from '@manacore/shared-stores';
import { authStore } from '$lib/stores/auth.svelte';
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';
}
export const tagStore = createTagStore({
authUrl: getAuthUrl(),
getToken: () => authStore.getValidToken(),
});

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { PillNavigation, QuickInputBar, DevBuildBadge } from '@manacore/shared-ui';
import { PillNavigation, QuickInputBar, DevBuildBadge, TagStrip } from '@manacore/shared-ui';
import type {
PillNavItem,
PillDropdownItem,
@ -27,6 +27,7 @@
import { projectStore } from '$lib/stores/project.svelte';
import { parseSongInput, formatParsedSongPreview } from '$lib/utils/song-parser';
import { SessionExpiredBanner, AuthGate } from '@manacore/shared-auth-ui';
import { tagStore } from '$lib/stores/tags.svelte';
import MiniPlayer from '$lib/components/MiniPlayer.svelte';
import FullPlayer from '$lib/components/FullPlayer.svelte';
import QueuePanel from '$lib/components/QueuePanel.svelte';
@ -68,6 +69,12 @@
// User
let userEmail = $derived(authStore.user?.email || 'Menu');
// TagStrip visibility
let isTagStripVisible = $state(false);
function handleTagStripToggle() {
isTagStripVisible = !isTagStripVisible;
}
// Navigation items
const baseNavItems: PillNavItem[] = [
{ href: '/library', label: 'Library', icon: 'music-notes' },
@ -76,6 +83,13 @@
{ href: '/upload', label: 'Upload', icon: 'upload' },
{ href: '/settings', label: 'Settings', icon: 'settings' },
{ href: '/help', label: 'Help', icon: 'help-circle' },
{
href: '/',
label: 'Tags',
icon: 'tag',
onClick: handleTagStripToggle,
active: isTagStripVisible,
},
];
const navItems = $derived(baseNavItems);
@ -168,6 +182,7 @@
async function handleAuthReady() {
splitPanel.initialize();
await tagStore.fetchTags();
}
</script>
@ -211,6 +226,22 @@
ariaLabel="Main navigation"
/>
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
{#if isTagStripVisible}
<TagStrip
tags={tagStore.tags.map((t) => ({
id: t.id,
name: t.name,
color: t.color || '#3b82f6',
}))}
selectedIds={[]}
onToggle={() => {}}
onClear={() => {}}
managementHref="/tags"
loading={tagStore.loading}
/>
{/if}
<!-- Quick Input Bar -->
<QuickInputBar
onSearch={handleInputSearch}

View file

@ -0,0 +1,49 @@
<script lang="ts">
import { tagStore } from '$lib/stores/tags.svelte';
import { onMount } from 'svelte';
onMount(() => {
if (tagStore.tags.length === 0) {
tagStore.fetchTags();
}
});
</script>
<svelte:head>
<title>Tags | Mukke</title>
</svelte:head>
<div class="tags-page">
<h1>Tags verwalten</h1>
<p class="text-sm text-muted-foreground mb-4">
Tags sind app-übergreifend — Änderungen gelten in allen ManaCore-Apps.
</p>
{#if tagStore.loading}
<p>Lädt...</p>
{:else if tagStore.tags.length === 0}
<p>Keine Tags vorhanden.</p>
{:else}
<div class="grid gap-2">
{#each tagStore.tags as tag}
<div class="flex items-center gap-2 p-2 rounded-lg bg-card">
<span class="w-3 h-3 rounded-full" style="background-color: {tag.color}"></span>
<span>{tag.name}</span>
</div>
{/each}
</div>
{/if}
</div>
<style>
.tags-page {
padding: 1.5rem;
max-width: 600px;
margin: 0 auto;
}
h1 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
</style>

View file

@ -1,48 +1,50 @@
/**
* Tags Store - Manages tag state using Svelte 5 runes
* Tags Store - Uses shared Tag Store backed by central mana-core-auth
*
* Wraps createTagStore for backward compatibility with existing tagStore interface.
* Also preserves photo-specific tag operations (getPhotoTags, addTagToPhoto, etc.)
* which go through the Photos backend, not mana-core-auth.
*/
import { browser } from '$app/environment';
import { createTagStore, type TagStore } from '@manacore/shared-stores';
import { authStore } from '$lib/stores/auth.svelte';
import { api } from '$lib/api/client';
import type { Tag } from '@photos/shared';
// State
let tags = $state<Tag[]>([]);
let loading = $state(false);
let error = $state<string | null>(null);
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 sharedTagStore: TagStore = createTagStore({
authUrl: getAuthUrl(),
getToken: () => authStore.getValidToken(),
});
// Backward-compatible tagStore wrapper
export const tagStore = {
// Getters
// Getters (delegate to shared store)
get tags() {
return tags;
return sharedTagStore.tags;
},
get loading() {
return loading;
return sharedTagStore.loading;
},
get error() {
return error;
return sharedTagStore.error;
},
/**
* Load all tags
*/
async loadTags() {
loading = true;
error = null;
try {
const result = await api.get<Tag[]>('/tags');
if (result.error) {
error = result.error.message;
return;
}
if (result.data) {
tags = result.data;
}
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to load tags';
} finally {
loading = false;
}
return sharedTagStore.fetchTags();
},
/**
@ -50,18 +52,9 @@ export const tagStore = {
*/
async createTag(data: { name: string; color?: string }) {
try {
const result = await api.post<Tag>('/tags', data);
if (result.error) {
error = result.error.message;
return null;
}
if (result.data) {
tags = [...tags, result.data];
return result.data;
}
return null;
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to create tag';
const tag = await sharedTagStore.createTag(data);
return tag;
} catch {
return null;
}
},
@ -71,18 +64,9 @@ export const tagStore = {
*/
async updateTag(id: string, data: { name?: string; color?: string }) {
try {
const result = await api.patch<Tag>(`/tags/${id}`, data);
if (result.error) {
error = result.error.message;
return null;
}
if (result.data) {
tags = tags.map((t) => (t.id === id ? result.data! : t));
return result.data;
}
return null;
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to update tag';
const tag = await sharedTagStore.updateTag(id, data);
return tag;
} catch {
return null;
}
},
@ -92,21 +76,15 @@ export const tagStore = {
*/
async deleteTag(id: string) {
try {
const result = await api.delete(`/tags/${id}`);
if (result.error) {
error = result.error.message;
return false;
}
tags = tags.filter((t) => t.id !== id);
await sharedTagStore.deleteTag(id);
return true;
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to delete tag';
} catch {
return false;
}
},
/**
* Get tags for a photo
* Get tags for a photo (from Photos backend)
*/
async getPhotoTags(mediaId: string): Promise<Tag[]> {
try {
@ -122,7 +100,7 @@ export const tagStore = {
},
/**
* Add tag to photo
* Add tag to photo (from Photos backend)
*/
async addTagToPhoto(mediaId: string, tagId: string) {
try {
@ -135,7 +113,7 @@ export const tagStore = {
},
/**
* Remove tag from photo
* Remove tag from photo (from Photos backend)
*/
async removeTagFromPhoto(mediaId: string, tagId: string) {
try {
@ -148,7 +126,7 @@ export const tagStore = {
},
/**
* Set all tags for a photo
* Set all tags for a photo (from Photos backend)
*/
async setPhotoTags(mediaId: string, tagIds: string[]) {
try {
@ -164,8 +142,6 @@ export const tagStore = {
* Reset store
*/
reset() {
tags = [];
loading = false;
error = null;
sharedTagStore.clear();
},
};

View file

@ -2,7 +2,7 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { _, locale } from 'svelte-i18n';
import { PillNavigation, QuickInputBar } from '@manacore/shared-ui';
import { PillNavigation, QuickInputBar, TagStrip } from '@manacore/shared-ui';
import type { PillNavItem, PillDropdownItem, QuickInputItem } from '@manacore/shared-ui';
import { theme } from '$lib/stores/theme';
import { authStore } from '$lib/stores/auth.svelte';
@ -18,12 +18,39 @@
let isDark = $derived(theme.isDark);
let userEmail = $derived(authStore.user?.email || 'Menu');
// TagStrip state
let isTagStripVisible = $state(true);
let selectedTagIds = $state<string[]>([]);
function handleTagStripToggle() {
isTagStripVisible = !isTagStripVisible;
}
function handleTagToggle(tagId: string) {
if (selectedTagIds.includes(tagId)) {
selectedTagIds = selectedTagIds.filter((id) => id !== tagId);
} else {
selectedTagIds = [...selectedTagIds, tagId];
}
}
function handleTagClear() {
selectedTagIds = [];
}
// Navigation items
const navItems: PillNavItem[] = [
{ href: '/', label: $_('nav.gallery'), icon: 'image' },
{ href: '/albums', label: $_('nav.albums'), icon: 'folder' },
{ href: '/favorites', label: $_('nav.favorites'), icon: 'heart' },
{ href: '/upload', label: $_('nav.upload'), icon: 'upload' },
{
href: '/',
label: 'Tags',
icon: 'tag',
onClick: handleTagStripToggle,
active: isTagStripVisible,
},
{ href: '/settings', label: $_('nav.settings'), icon: 'settings' },
];
@ -111,6 +138,21 @@
profileHref="/profile"
/>
<!-- TagStrip (toggled via Tags pill) -->
{#if isTagStripVisible}
<TagStrip
tags={tagStore.tags.map((t) => ({
id: t.id,
name: t.name,
color: t.color || '#6b7280',
}))}
selectedIds={selectedTagIds}
onToggle={handleTagToggle}
onClear={handleTagClear}
managementHref="/tags"
/>
{/if}
<!-- Quick Input Bar -->
<QuickInputBar
onSearch={handleInputSearch}

View file

@ -29,6 +29,8 @@
"@manacore/shared-help-ui": "workspace:*",
"@manacore/shared-icons": "workspace:*",
"@manacore/shared-profile-ui": "workspace:*",
"@manacore/shared-stores": "workspace:*",
"@manacore/shared-tags": "workspace:*",
"@manacore/shared-subscription-types": "workspace:*",
"@manacore/shared-subscription-ui": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",

View file

@ -1,6 +1,57 @@
import { writable } from 'svelte/store';
import type { Tag } from '$lib/api/tags';
/**
* Tags Store - Uses shared Tag Store backed by central mana-core-auth
*
* Replaces old Svelte 4 writable stores with createTagStore wrapper.
* Exports writable-compatible stores for backward compatibility with existing consumers.
*/
import { browser } from '$app/environment';
import { writable, derived } from 'svelte/store';
import { createTagStore, type TagStore } from '@manacore/shared-stores';
import { authStore } from '$lib/stores/auth.svelte';
import type { Tag } from '@manacore/shared-tags';
// Re-export Tag for backward compatibility with '$lib/api/tags' Tag type
export type { 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 (Svelte 5 runes-based)
const sharedTagStore: TagStore = createTagStore({
authUrl: getAuthUrl(),
getToken: () => authStore.getValidToken(),
});
// Backward-compatible writable stores for existing consumers
// These are kept as writables so $tags and $selectedTags syntax still works
export const tags = writable<Tag[]>([]);
export const selectedTags = writable<string[]>([]);
export const isLoadingTags = writable<boolean>(false);
/**
* Fetch tags from the shared store and sync to the writable store.
* Call this on mount instead of using the old getAllTags() API function.
*/
export async function fetchAndSyncTags(): Promise<void> {
isLoadingTags.set(true);
try {
await sharedTagStore.fetchTags();
tags.set(sharedTagStore.tags);
} catch (e) {
console.error('Failed to fetch tags:', e);
} finally {
isLoadingTags.set(false);
}
}
/**
* Direct access to the shared tag store for components that want the runes-based API.
*/
export const tagStore = sharedTagStore;

View file

@ -4,7 +4,7 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { locale } from 'svelte-i18n';
import { PillNavigation } from '@manacore/shared-ui';
import { PillNavigation, TagStrip } from '@manacore/shared-ui';
import type { PillNavItem, PillNavElement, PillDropdownItem } from '@manacore/shared-ui';
import {
THEME_DEFINITIONS,
@ -19,6 +19,7 @@
import KeyboardShortcutsModal from '$lib/components/ui/KeyboardShortcutsModal.svelte';
import { theme } from '$lib/stores/theme';
import { isUIVisible, toggleUI, showKeyboardShortcuts } from '$lib/stores/ui';
import { tagStore } from '$lib/stores/tags';
import { pictureOnboarding } from '$lib/stores/app-onboarding.svelte';
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
import { SessionExpiredBanner, AuthGate } from '@manacore/shared-auth-ui';
@ -42,6 +43,26 @@
}
});
// TagStrip state
let isTagStripVisible = $state(true);
let selectedTagIds = $state<string[]>([]);
function handleTagStripToggle() {
isTagStripVisible = !isTagStripVisible;
}
function handleTagToggle(tagId: string) {
if (selectedTagIds.includes(tagId)) {
selectedTagIds = selectedTagIds.filter((id) => id !== tagId);
} else {
selectedTagIds = [...selectedTagIds, tagId];
}
}
function handleTagClear() {
selectedTagIds = [];
}
function handleCollapsedChange(collapsed: boolean) {
isCollapsed = collapsed;
if (browser) localStorage.setItem('picture-nav-collapsed', String(collapsed));
@ -61,7 +82,7 @@
}
async function handleAuthReady() {
await userSettings.load();
await Promise.all([userSettings.load(), tagStore.fetchTags()]);
// Redirect to start page if on /app and a custom start page is set
const currentPath = window.location.pathname;
@ -85,7 +106,13 @@
{ href: '/app/explore', label: 'Entdecken', icon: 'search' },
{ href: '/app/generate', label: 'Generieren', icon: 'fire' },
{ href: '/app/upload', label: 'Upload', icon: 'upload' },
{ href: '/app/tags', label: 'Tags', icon: 'tag' },
{
href: '/',
label: 'Tags',
icon: 'tag',
onClick: handleTagStripToggle,
active: isTagStripVisible,
},
{ href: '/app/archive', label: 'Archiv', icon: 'archive' },
];
@ -262,6 +289,20 @@
feedbackHref="/app/feedback"
allAppsHref="/app/apps"
/>
<!-- TagStrip (toggled via Tags pill) -->
{#if isTagStripVisible}
<TagStrip
tags={tagStore.tags.map((t) => ({
id: t.id,
name: t.name,
color: t.color || '#6b7280',
}))}
selectedIds={selectedTagIds}
onToggle={handleTagToggle}
onClear={handleTagClear}
managementHref="/app/tags"
/>
{/if}
{/if}
<!-- Main Content Area -->

View file

@ -45,6 +45,8 @@
"@manacore/shared-tailwind": "workspace:*",
"@manacore/shared-theme": "workspace:*",
"@manacore/shared-theme-ui": "workspace:*",
"@manacore/shared-stores": "workspace:*",
"@manacore/shared-tags": "workspace:*",
"@manacore/shared-ui": "workspace:*",
"@manacore/shared-utils": "workspace:*",
"@manacore/shared-app-onboarding": "workspace:*",

View file

@ -0,0 +1,20 @@
/**
* Tag Store - Uses shared createTagStore backed by central mana-core-auth
*/
import { browser } from '$app/environment';
import { createTagStore } from '@manacore/shared-stores';
import { authStore } from '$lib/stores/auth.svelte';
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';
}
export const tagStore = createTagStore({
authUrl: getAuthUrl(),
getToken: () => authStore.getValidToken(),
});

View file

@ -1,8 +1,9 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { PillNavigation, QuickInputBar } from '@manacore/shared-ui';
import { PillNavigation, QuickInputBar, TagStrip } from '@manacore/shared-ui';
import type { PillNavItem, QuickInputItem, CreatePreview } from '@manacore/shared-ui';
import { tagStore } from '$lib/stores/tags.svelte';
import { theme } from '$lib/stores/theme';
import { authStore } from '$lib/stores/auth.svelte';
import { plantsApi } from '$lib/api/plants';
@ -15,11 +16,24 @@
let { children } = $props();
// TagStrip visibility
let isTagStripVisible = $state(false);
function handleTagStripToggle() {
isTagStripVisible = !isTagStripVisible;
}
// Navigation items for Planta
const navItems: PillNavItem[] = [
{ href: '/dashboard', label: 'Meine Pflanzen', icon: 'document' },
{ href: '/add', label: 'Hinzufügen', icon: 'plus' },
{ href: '/settings', label: 'Einstellungen', icon: 'settings' },
{
href: '/',
label: 'Tags',
icon: 'tag',
onClick: handleTagStripToggle,
active: isTagStripVisible,
},
];
let isDark = $derived(theme.isDark);
@ -83,7 +97,7 @@
}
</script>
<AuthGate {authStore} {goto}>
<AuthGate {authStore} {goto} onReady={() => tagStore.fetchTags()}>
<div class="layout-container">
<PillNavigation
items={navItems}
@ -102,6 +116,22 @@
profileHref="/profile"
/>
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
{#if isTagStripVisible}
<TagStrip
tags={tagStore.tags.map((t) => ({
id: t.id,
name: t.name,
color: t.color || '#3b82f6',
}))}
selectedIds={[]}
onToggle={() => {}}
onClear={() => {}}
managementHref="/tags"
loading={tagStore.loading}
/>
{/if}
<!-- Quick Input Bar -->
<QuickInputBar
onSearch={handleInputSearch}

View file

@ -0,0 +1,49 @@
<script lang="ts">
import { tagStore } from '$lib/stores/tags.svelte';
import { onMount } from 'svelte';
onMount(() => {
if (tagStore.tags.length === 0) {
tagStore.fetchTags();
}
});
</script>
<svelte:head>
<title>Tags | Planta</title>
</svelte:head>
<div class="tags-page">
<h1>Tags verwalten</h1>
<p class="text-sm text-muted-foreground mb-4">
Tags sind app-übergreifend — Änderungen gelten in allen ManaCore-Apps.
</p>
{#if tagStore.loading}
<p>Lädt...</p>
{:else if tagStore.tags.length === 0}
<p>Keine Tags vorhanden.</p>
{:else}
<div class="grid gap-2">
{#each tagStore.tags as tag}
<div class="flex items-center gap-2 p-2 rounded-lg bg-card">
<span class="w-3 h-3 rounded-full" style="background-color: {tag.color}"></span>
<span>{tag.name}</span>
</div>
{/each}
</div>
{/if}
</div>
<style>
.tags-page {
padding: 1.5rem;
max-width: 600px;
margin: 0 auto;
}
h1 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
</style>

View file

@ -44,6 +44,7 @@
"@manacore/shared-icons": "workspace:*",
"@manacore/shared-profile-ui": "workspace:*",
"@manacore/shared-stores": "workspace:*",
"@manacore/shared-tags": "workspace:*",
"@manacore/shared-subscription-ui": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",
"@manacore/shared-theme": "workspace:*",

View file

@ -0,0 +1,20 @@
/**
* Tag Store - Uses shared createTagStore backed by central mana-core-auth
*/
import { browser } from '$app/environment';
import { createTagStore } from '@manacore/shared-stores';
import { auth } from '$lib/stores/auth.svelte';
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';
}
export const tagStore = createTagStore({
authUrl: getAuthUrl(),
getToken: () => auth.getValidToken(),
});

View file

@ -3,8 +3,9 @@
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { locale } from 'svelte-i18n';
import { PillNavigation, QuickInputBar } from '@manacore/shared-ui';
import { PillNavigation, QuickInputBar, TagStrip } from '@manacore/shared-ui';
import type { PillNavItem, PillDropdownItem, QuickInputItem } from '@manacore/shared-ui';
import { tagStore } from '$lib/stores/tags.svelte';
import { auth } from '$lib/stores/auth.svelte';
import { userSettings } from '$lib/stores/user-settings.svelte';
import { theme } from '$lib/stores/theme';
@ -58,8 +59,23 @@
// User email for user dropdown
let userEmail = $derived(auth.user?.email);
// TagStrip visibility
let isTagStripVisible = $state(false);
function handleTagStripToggle() {
isTagStripVisible = !isTagStripVisible;
}
// Navigation items for Presi
const navItems: PillNavItem[] = [{ href: '/', label: 'Decks', icon: 'document' }];
const navItems: PillNavItem[] = [
{ href: '/', label: 'Decks', icon: 'document' },
{
href: '/',
label: 'Tags',
icon: 'tag',
onClick: handleTagStripToggle,
active: isTagStripVisible,
},
];
// Routes where nav should be hidden (present mode, shared view)
const hideNavRoutes = ['/present/', '/shared/'];
@ -128,8 +144,9 @@
return;
}
// Load user settings
// Load user settings and tags
await userSettings.load();
await tagStore.fetchTags();
// Redirect to start page if on root and a custom start page is set
const currentPath = window.location.pathname;
@ -189,6 +206,22 @@
allAppsHref="/apps"
/>
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
{#if isTagStripVisible}
<TagStrip
tags={tagStore.tags.map((t) => ({
id: t.id,
name: t.name,
color: t.color || '#3b82f6',
}))}
selectedIds={[]}
onToggle={() => {}}
onClear={() => {}}
managementHref="/tags"
loading={tagStore.loading}
/>
{/if}
<!-- Quick Input Bar -->
<QuickInputBar
onSearch={handleInputSearch}

View file

@ -0,0 +1,49 @@
<script lang="ts">
import { tagStore } from '$lib/stores/tags.svelte';
import { onMount } from 'svelte';
onMount(() => {
if (tagStore.tags.length === 0) {
tagStore.fetchTags();
}
});
</script>
<svelte:head>
<title>Tags | Presi</title>
</svelte:head>
<div class="tags-page">
<h1>Tags verwalten</h1>
<p class="text-sm text-muted-foreground mb-4">
Tags sind app-übergreifend — Änderungen gelten in allen ManaCore-Apps.
</p>
{#if tagStore.loading}
<p>Lädt...</p>
{:else if tagStore.tags.length === 0}
<p>Keine Tags vorhanden.</p>
{:else}
<div class="grid gap-2">
{#each tagStore.tags as tag}
<div class="flex items-center gap-2 p-2 rounded-lg bg-card">
<span class="w-3 h-3 rounded-full" style="background-color: {tag.color}"></span>
<span>{tag.name}</span>
</div>
{/each}
</div>
{/if}
</div>
<style>
.tags-page {
padding: 1.5rem;
max-width: 600px;
margin: 0 auto;
}
h1 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
</style>

View file

@ -50,6 +50,8 @@
"@manacore/shared-tailwind": "workspace:*",
"@manacore/shared-theme": "workspace:*",
"@manacore/shared-theme-ui": "workspace:*",
"@manacore/shared-stores": "workspace:*",
"@manacore/shared-tags": "workspace:*",
"@manacore/shared-ui": "workspace:*",
"date-fns": "^4.1.0",
"svelte-i18n": "^4.0.1"

View file

@ -0,0 +1,20 @@
/**
* Tag Store - Uses shared createTagStore backed by central mana-core-auth
*/
import { browser } from '$app/environment';
import { createTagStore } from '@manacore/shared-stores';
import { authStore } from '$lib/stores';
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';
}
export const tagStore = createTagStore({
authUrl: getAuthUrl(),
getToken: () => authStore.getValidToken(),
});

View file

@ -8,7 +8,7 @@
import { apiClient } from '$lib/api/client';
import { questionsApi } from '$lib/api/questions';
import { theme } from '$lib/stores/theme';
import { PillNavigation, QuickInputBar } from '@manacore/shared-ui';
import { PillNavigation, QuickInputBar, TagStrip } from '@manacore/shared-ui';
import type {
PillNavItem,
PillDropdownItem,
@ -16,6 +16,7 @@
CreatePreview,
} from '@manacore/shared-ui';
import { getPillAppItems } from '@manacore/shared-branding';
import { tagStore } from '$lib/stores/tags.svelte';
let { children } = $props();
@ -55,6 +56,7 @@
// Load initial data
await collectionsStore.load();
await questionsStore.load();
await tagStore.fetchTags();
// Initialize mobile state
updateMobileState();
@ -174,17 +176,46 @@
}
}
// TagStrip visibility
let isTagStripVisible = $state(false);
function handleTagStripToggle() {
isTagStripVisible = !isTagStripVisible;
}
// Navigation items
let navItems = $derived<PillNavItem[]>([
{ href: '/', label: 'Questions', icon: 'help-circle' },
{ href: '/collections', label: 'Collections', icon: 'folder' },
{ href: '/settings', label: 'Settings', icon: 'settings' },
{
href: '/',
label: 'Tags',
icon: 'tag',
onClick: handleTagStripToggle,
active: isTagStripVisible,
},
]);
</script>
<svelte:window onresize={updateMobileState} />
<div class="layout-container">
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
{#if isTagStripVisible}
<TagStrip
tags={tagStore.tags.map((t) => ({
id: t.id,
name: t.name,
color: t.color || '#3b82f6',
}))}
selectedIds={[]}
onToggle={() => {}}
onClear={() => {}}
managementHref="/tags"
loading={tagStore.loading}
/>
{/if}
<!-- Navigation -->
<PillNavigation
items={navItems}

View file

@ -0,0 +1,49 @@
<script lang="ts">
import { tagStore } from '$lib/stores/tags.svelte';
import { onMount } from 'svelte';
onMount(() => {
if (tagStore.tags.length === 0) {
tagStore.fetchTags();
}
});
</script>
<svelte:head>
<title>Tags | Questions</title>
</svelte:head>
<div class="tags-page">
<h1>Tags verwalten</h1>
<p class="text-sm text-muted-foreground mb-4">
Tags sind app-übergreifend — Änderungen gelten in allen ManaCore-Apps.
</p>
{#if tagStore.loading}
<p>Lädt...</p>
{:else if tagStore.tags.length === 0}
<p>Keine Tags vorhanden.</p>
{:else}
<div class="grid gap-2">
{#each tagStore.tags as tag}
<div class="flex items-center gap-2 p-2 rounded-lg bg-card">
<span class="w-3 h-3 rounded-full" style="background-color: {tag.color}"></span>
<span>{tag.name}</span>
</div>
{/each}
</div>
{/if}
</div>
<style>
.tags-page {
padding: 1.5rem;
max-width: 600px;
margin: 0 auto;
}
h1 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
</style>

View file

@ -52,6 +52,7 @@
"@manacore/shared-profile-ui": "workspace:*",
"@manacore/shared-stores": "workspace:*",
"@manacore/shared-subscription-ui": "workspace:*",
"@manacore/shared-tags": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",
"@manacore/shared-theme": "workspace:*",
"@manacore/shared-theme-ui": "workspace:*",

View file

@ -0,0 +1,62 @@
/**
* Tags Store - Uses shared Tag Store backed by central mana-core-auth
*/
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';
// Re-export Tag for convenience
export type { 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(),
});
// Export the store with a backward-compatible wrapper
export const tagsStore = {
get tags() {
return tagStore.tags;
},
get loading() {
return tagStore.loading;
},
get error() {
return tagStore.error;
},
async fetchTags() {
return tagStore.fetchTags();
},
getById(id: string) {
return tagStore.getById(id);
},
getColor(tagId: string) {
return tagStore.getColor(tagId);
},
async createTag(data: { name: string; color?: string }) {
return tagStore.createTag(data);
},
async updateTag(id: string, data: { name?: string; color?: string }) {
return tagStore.updateTag(id, data);
},
async deleteTag(id: string) {
return tagStore.deleteTag(id);
},
clear() {
tagStore.clear();
},
};

View file

@ -3,11 +3,12 @@
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { locale } from 'svelte-i18n';
import { PillNavigation, setupGlobalErrorHandler } from '@manacore/shared-ui';
import { PillNavigation, setupGlobalErrorHandler, TagStrip } from '@manacore/shared-ui';
import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui';
import { theme } from '$lib/stores/theme.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { userSettings } from '$lib/stores/user-settings.svelte';
import { tagsStore } from '$lib/stores/tags.svelte';
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
import { isNavCollapsed as collapsedStore } from '$lib/stores/navigation';
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
@ -65,12 +66,39 @@
// User email for user dropdown
let userEmail = $derived(authStore.user?.email || 'Menü');
// TagStrip state
let isTagStripVisible = $state(true);
let selectedTagIds = $state<string[]>([]);
function handleTagStripToggle() {
isTagStripVisible = !isTagStripVisible;
}
function handleTagToggle(tagId: string) {
if (selectedTagIds.includes(tagId)) {
selectedTagIds = selectedTagIds.filter((id) => id !== tagId);
} else {
selectedTagIds = [...selectedTagIds, tagId];
}
}
function handleTagClear() {
selectedTagIds = [];
}
// Navigation items for Storage
const navItems: PillNavItem[] = [
{ href: '/files', label: 'Dateien', icon: 'folder' },
{ href: '/shared', label: 'Geteilt', icon: 'share' },
{ href: '/favorites', label: 'Favoriten', icon: 'heart' },
{ href: '/trash', label: 'Papierkorb', icon: 'trash' },
{
href: '/',
label: 'Tags',
icon: 'tag',
onClick: handleTagStripToggle,
active: isTagStripVisible,
},
{ href: '/search', label: 'Suche', icon: 'search' },
];
@ -135,8 +163,8 @@
// Initialize theme
theme.initialize();
// Load user settings
await userSettings.load();
// Load user settings and tags
await Promise.all([userSettings.load(), tagsStore.fetchTags()]);
// Initialize collapsed state from localStorage
const savedCollapsed = localStorage.getItem('storage-nav-collapsed');
@ -197,6 +225,21 @@
allAppsHref="/apps"
/>
<!-- TagStrip (toggled via Tags pill) -->
{#if isTagStripVisible}
<TagStrip
tags={tagsStore.tags.map((t) => ({
id: t.id,
name: t.name,
color: t.color || '#6b7280',
}))}
selectedIds={selectedTagIds}
onToggle={handleTagToggle}
onClear={handleTagClear}
managementHref="/tags"
/>
{/if}
<main class="main-content bg-background">
<div class="content-wrapper">
{@render children()}

View file

@ -0,0 +1,307 @@
<script lang="ts">
import { tagsStore } from '$lib/stores/tags.svelte';
import { onMount } from 'svelte';
import { toastStore, PageHeader } from '@manacore/shared-ui';
import { Plus, Tag as TagIcon, PencilSimple, Trash } from '@manacore/shared-icons';
let showCreateModal = $state(false);
let showEditModal = $state(false);
let editingTag = $state<{ id: string; name: string; color: string } | null>(null);
let newTagName = $state('');
let newTagColor = $state('#3B82F6');
let editTagName = $state('');
let editTagColor = $state('');
const predefinedColors = [
'#EF4444',
'#F59E0B',
'#10B981',
'#3B82F6',
'#8B5CF6',
'#EC4899',
'#6366F1',
'#14B8A6',
];
onMount(async () => {
if (tagsStore.tags.length === 0) {
await tagsStore.fetchTags();
}
});
async function handleCreateTag() {
if (!newTagName.trim()) return;
try {
await tagsStore.createTag({
name: newTagName.trim(),
color: newTagColor,
});
toastStore.show('Tag erfolgreich erstellt', 'success');
newTagName = '';
newTagColor = '#3B82F6';
showCreateModal = false;
} catch (error) {
console.error('Error creating tag:', error);
toastStore.show('Fehler beim Erstellen des Tags', 'error');
}
}
function openEditModal(tag: { id: string; name: string; color?: string | null }) {
editingTag = { id: tag.id, name: tag.name, color: tag.color || '#3B82F6' };
editTagName = tag.name;
editTagColor = tag.color || '#3B82F6';
showEditModal = true;
}
async function handleUpdateTag() {
if (!editingTag || !editTagName.trim()) return;
try {
await tagsStore.updateTag(editingTag.id, {
name: editTagName.trim(),
color: editTagColor,
});
toastStore.show('Tag erfolgreich aktualisiert', 'success');
showEditModal = false;
editingTag = null;
} catch (error) {
console.error('Error updating tag:', error);
toastStore.show('Fehler beim Aktualisieren des Tags', 'error');
}
}
async function handleDeleteTag(tagId: string) {
if (!confirm('Möchten Sie diesen Tag wirklich löschen?')) return;
try {
await tagsStore.deleteTag(tagId);
toastStore.show('Tag erfolgreich gelöscht', 'success');
} catch (error) {
console.error('Error deleting tag:', error);
toastStore.show('Fehler beim Löschen des Tags', 'error');
}
}
</script>
<svelte:head>
<title>Tags verwalten | Storage</title>
</svelte:head>
<div class="min-h-screen px-4 py-8">
<div class="mx-auto max-w-4xl">
<PageHeader
title="Tag-Verwaltung"
description="Verwalte deine Tags für eine bessere Organisation deiner Dateien"
size="lg"
>
{#snippet actions()}
<button
onclick={() => (showCreateModal = true)}
class="flex items-center gap-2 rounded-2xl bg-blue-600/90 px-6 py-3 text-sm font-medium text-white backdrop-blur-xl transition-all hover:bg-blue-700/90 dark:bg-blue-500/90 dark:hover:bg-blue-600/90"
>
<Plus size={20} />
Neuer Tag
</button>
{/snippet}
</PageHeader>
<!-- Tags Grid -->
{#if tagsStore.loading}
<div class="flex items-center justify-center py-12">
<div
class="h-8 w-8 animate-spin rounded-full border-4 border-solid border-blue-600 border-r-transparent dark:border-blue-400"
></div>
</div>
{:else if tagsStore.tags.length === 0}
<div
class="rounded-3xl border border-gray-200/50 bg-white/80 p-12 text-center backdrop-blur-xl dark:border-gray-700/50 dark:bg-gray-900/80"
>
<TagIcon size={64} weight="thin" class="mx-auto text-gray-400 dark:text-gray-500" />
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-gray-100">
Keine Tags vorhanden
</h3>
<p class="mt-2 text-gray-600 dark:text-gray-400">
Erstelle deinen ersten Tag, um deine Dateien zu organisieren
</p>
</div>
{:else}
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{#each tagsStore.tags as tag (tag.id)}
<div
class="group relative rounded-2xl border border-gray-200/50 bg-white/80 p-6 backdrop-blur-xl transition-all hover:shadow-lg dark:border-gray-700/50 dark:bg-gray-900/80"
>
<div class="flex items-start justify-between">
<div class="flex items-center gap-3">
{#if tag.color}
<div class="h-8 w-8 rounded-full" style="background-color: {tag.color};"></div>
{/if}
<div>
<h3 class="font-medium text-gray-900 dark:text-gray-100">{tag.name}</h3>
</div>
</div>
<div class="flex gap-2">
<button
onclick={() => openEditModal(tag)}
class="rounded-lg bg-gray-100/80 p-2 text-gray-600 backdrop-blur-xl transition-all hover:bg-gray-200/80 dark:bg-gray-800/80 dark:text-gray-400 dark:hover:bg-gray-700/80"
aria-label="Bearbeiten"
>
<PencilSimple size={16} />
</button>
<button
onclick={() => handleDeleteTag(tag.id)}
class="rounded-lg bg-red-100/80 p-2 text-red-600 backdrop-blur-xl transition-all hover:bg-red-200/80 dark:bg-red-900/20 dark:text-red-400 dark:hover:bg-red-900/30"
aria-label="Löschen"
>
<Trash size={16} />
</button>
</div>
</div>
</div>
{/each}
</div>
{/if}
</div>
</div>
<!-- Create Tag Modal -->
{#if showCreateModal}
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4 backdrop-blur-sm"
onclick={() => (showCreateModal = false)}
role="presentation"
>
<div
class="w-full max-w-md rounded-3xl border border-gray-200/50 bg-white/95 p-6 shadow-2xl backdrop-blur-xl dark:border-gray-700/50 dark:bg-gray-900/95"
onclick={(e) => e.stopPropagation()}
role="dialog"
>
<h2 class="mb-4 text-xl font-bold text-gray-900 dark:text-gray-100">Neuer Tag</h2>
<div class="space-y-4">
<div>
<label
for="tag-name"
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Name
</label>
<input
id="tag-name"
type="text"
bind:value={newTagName}
placeholder="z.B. Wichtig, Projekt, etc."
class="w-full rounded-xl border border-gray-300 bg-white px-4 py-2 text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
/>
</div>
<div>
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
Farbe
</label>
<div class="flex flex-wrap gap-3">
{#each predefinedColors as color}
<button
onclick={() => (newTagColor = color)}
class="h-10 w-10 rounded-full transition-all {newTagColor === color
? 'ring-2 ring-offset-2 ring-offset-white dark:ring-offset-gray-900'
: ''}"
style="background-color: {color}; {newTagColor === color
? `--tw-ring-color: ${color}`
: ''}"
aria-label="Farbe auswählen"
></button>
{/each}
</div>
</div>
</div>
<div class="mt-6 flex gap-3">
<button
onclick={() => (showCreateModal = false)}
class="flex-1 rounded-xl bg-gray-100 px-4 py-2 text-gray-700 transition-colors hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
Abbrechen
</button>
<button
onclick={handleCreateTag}
disabled={!newTagName.trim()}
class="flex-1 rounded-xl bg-blue-600 px-4 py-2 text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-blue-500 dark:hover:bg-blue-600"
>
Erstellen
</button>
</div>
</div>
</div>
{/if}
<!-- Edit Tag Modal -->
{#if showEditModal && editingTag}
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4 backdrop-blur-sm"
onclick={() => (showEditModal = false)}
role="presentation"
>
<div
class="w-full max-w-md rounded-3xl border border-gray-200/50 bg-white/95 p-6 shadow-2xl backdrop-blur-xl dark:border-gray-700/50 dark:bg-gray-900/95"
onclick={(e) => e.stopPropagation()}
role="dialog"
>
<h2 class="mb-4 text-xl font-bold text-gray-900 dark:text-gray-100">Tag bearbeiten</h2>
<div class="space-y-4">
<div>
<label
for="edit-tag-name"
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Name
</label>
<input
id="edit-tag-name"
type="text"
bind:value={editTagName}
class="w-full rounded-xl border border-gray-300 bg-white px-4 py-2 text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
/>
</div>
<div>
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
Farbe
</label>
<div class="flex flex-wrap gap-3">
{#each predefinedColors as color}
<button
onclick={() => (editTagColor = color)}
class="h-10 w-10 rounded-full transition-all {editTagColor === color
? 'ring-2 ring-offset-2 ring-offset-white dark:ring-offset-gray-900'
: ''}"
style="background-color: {color}; {editTagColor === color
? `--tw-ring-color: ${color}`
: ''}"
aria-label="Farbe auswählen"
></button>
{/each}
</div>
</div>
</div>
<div class="mt-6 flex gap-3">
<button
onclick={() => (showEditModal = false)}
class="flex-1 rounded-xl bg-gray-100 px-4 py-2 text-gray-700 transition-colors hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
Abbrechen
</button>
<button
onclick={handleUpdateTag}
disabled={!editTagName.trim()}
class="flex-1 rounded-xl bg-blue-600 px-4 py-2 text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-blue-500 dark:hover:bg-blue-600"
>
Speichern
</button>
</div>
</div>
</div>
{/if}

View file

@ -45,6 +45,7 @@
"@manacore/shared-icons": "workspace:*",
"@manacore/shared-profile-ui": "workspace:*",
"@manacore/shared-stores": "workspace:*",
"@manacore/shared-tags": "workspace:*",
"@manacore/shared-subscription-ui": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",
"@manacore/shared-theme": "workspace:*",

View file

@ -0,0 +1,20 @@
/**
* Tag Store - Uses shared createTagStore backed by central mana-core-auth
*/
import { browser } from '$app/environment';
import { createTagStore } from '@manacore/shared-stores';
import { authStore } from '$lib/stores/auth.svelte';
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';
}
export const tagStore = createTagStore({
authUrl: getAuthUrl(),
getToken: () => authStore.getValidToken(),
});

View file

@ -2,8 +2,14 @@
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 type { PillNavItem, PillDropdownItem, QuickInputItem } from '@manacore/shared-ui';
import { tagStore } from '$lib/stores/tags.svelte';
// Extend QuickInputItem for zitare-specific search results with href navigation
interface ZitareSearchItem extends QuickInputItem {
@ -88,12 +94,25 @@
// User email for user dropdown
let userEmail = $derived(authStore.user?.email || $_('nav.menu'));
// TagStrip visibility
let isTagStripVisible = $state(false);
function handleTagStripToggle() {
isTagStripVisible = !isTagStripVisible;
}
// Base navigation items for Zitare
let baseNavItems = $derived<PillNavItem[]>([
{ href: '/', label: $_('nav.today'), icon: 'sun' },
{ href: '/categories', label: $_('nav.categories'), icon: 'grid' },
{ href: '/favorites', label: $_('nav.favorites'), icon: 'heart' },
{ href: '/lists', label: $_('nav.lists'), icon: 'list' },
{
href: '/',
label: 'Tags',
icon: 'tag',
onClick: handleTagStripToggle,
active: isTagStripVisible,
},
]);
// Filter hidden nav items
@ -216,6 +235,7 @@
userSettings.load();
favoritesStore.load();
listsStore.loadLists();
tagStore.fetchTags();
}
}
</script>
@ -260,6 +280,22 @@
/>
{/if}
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
{#if isTagStripVisible}
<TagStrip
tags={tagStore.tags.map((t) => ({
id: t.id,
name: t.name,
color: t.color || '#3b82f6',
}))}
selectedIds={[]}
onToggle={() => {}}
onClear={() => {}}
managementHref="/tags"
loading={tagStore.loading}
/>
{/if}
<!-- Global Quick Input Bar -->
<QuickInputBar
onSearch={handleSearch}

View file

@ -0,0 +1,49 @@
<script lang="ts">
import { tagStore } from '$lib/stores/tags.svelte';
import { onMount } from 'svelte';
onMount(() => {
if (tagStore.tags.length === 0) {
tagStore.fetchTags();
}
});
</script>
<svelte:head>
<title>Tags | Zitare</title>
</svelte:head>
<div class="tags-page">
<h1>Tags verwalten</h1>
<p class="text-sm text-muted-foreground mb-4">
Tags sind app-übergreifend — Änderungen gelten in allen ManaCore-Apps.
</p>
{#if tagStore.loading}
<p>Lädt...</p>
{:else if tagStore.tags.length === 0}
<p>Keine Tags vorhanden.</p>
{:else}
<div class="grid gap-2">
{#each tagStore.tags as tag}
<div class="flex items-center gap-2 p-2 rounded-lg bg-card">
<span class="w-3 h-3 rounded-full" style="background-color: {tag.color}"></span>
<span>{tag.name}</span>
</div>
{/each}
</div>
{/if}
</div>
<style>
.tags-page {
padding: 1.5rem;
max-width: 600px;
margin: 0 auto;
}
h1 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
</style>