mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 18:41:08 +02:00
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:
parent
9c8bae3dea
commit
91116bf0f1
57 changed files with 1852 additions and 93 deletions
|
|
@ -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:*",
|
||||
|
|
|
|||
20
apps/chat/apps/web/src/lib/stores/tags.svelte.ts
Normal file
20
apps/chat/apps/web/src/lib/stores/tags.svelte.ts
Normal 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(),
|
||||
});
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
49
apps/chat/apps/web/src/routes/(protected)/tags/+page.svelte
Normal file
49
apps/chat/apps/web/src/routes/(protected)/tags/+page.svelte
Normal 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>
|
||||
|
|
@ -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:*",
|
||||
|
|
|
|||
20
apps/citycorners/apps/web/src/lib/stores/tags.svelte.ts
Normal file
20
apps/citycorners/apps/web/src/lib/stores/tags.svelte.ts
Normal 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(),
|
||||
});
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
49
apps/citycorners/apps/web/src/routes/(app)/tags/+page.svelte
Normal file
49
apps/citycorners/apps/web/src/routes/(app)/tags/+page.svelte
Normal 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>
|
||||
|
|
@ -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:*",
|
||||
|
|
|
|||
20
apps/clock/apps/web/src/lib/stores/tags.svelte.ts
Normal file
20
apps/clock/apps/web/src/lib/stores/tags.svelte.ts
Normal 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(),
|
||||
});
|
||||
|
|
@ -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()}
|
||||
|
|
|
|||
49
apps/clock/apps/web/src/routes/(app)/tags/+page.svelte
Normal file
49
apps/clock/apps/web/src/routes/(app)/tags/+page.svelte
Normal 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>
|
||||
|
|
@ -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:*",
|
||||
|
|
|
|||
20
apps/context/apps/web/src/lib/stores/tags.svelte.ts
Normal file
20
apps/context/apps/web/src/lib/stores/tags.svelte.ts
Normal 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(),
|
||||
});
|
||||
|
|
@ -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()}
|
||||
|
|
|
|||
49
apps/context/apps/web/src/routes/(app)/tags/+page.svelte
Normal file
49
apps/context/apps/web/src/routes/(app)/tags/+page.svelte
Normal 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>
|
||||
|
|
@ -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:*",
|
||||
|
|
|
|||
20
apps/manacore/apps/web/src/lib/stores/tags.svelte.ts
Normal file
20
apps/manacore/apps/web/src/lib/stores/tags.svelte.ts
Normal 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(),
|
||||
});
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
49
apps/manacore/apps/web/src/routes/(app)/tags/+page.svelte
Normal file
49
apps/manacore/apps/web/src/routes/(app)/tags/+page.svelte
Normal 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>
|
||||
|
|
@ -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:*",
|
||||
|
|
|
|||
20
apps/manadeck/apps/web/src/lib/stores/tags.svelte.ts
Normal file
20
apps/manadeck/apps/web/src/lib/stores/tags.svelte.ts
Normal 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(),
|
||||
});
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
49
apps/manadeck/apps/web/src/routes/(app)/tags/+page.svelte
Normal file
49
apps/manadeck/apps/web/src/routes/(app)/tags/+page.svelte
Normal 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>
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
24
apps/matrix/apps/web/src/lib/stores/tags.svelte.ts
Normal file
24
apps/matrix/apps/web/src/lib/stores/tags.svelte.ts
Normal 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();
|
||||
},
|
||||
});
|
||||
|
|
@ -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()}
|
||||
|
|
|
|||
49
apps/matrix/apps/web/src/routes/(app)/tags/+page.svelte
Normal file
49
apps/matrix/apps/web/src/routes/(app)/tags/+page.svelte
Normal 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>
|
||||
|
|
@ -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:*",
|
||||
|
|
|
|||
20
apps/mukke/apps/web/src/lib/stores/tags.svelte.ts
Normal file
20
apps/mukke/apps/web/src/lib/stores/tags.svelte.ts
Normal 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(),
|
||||
});
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
49
apps/mukke/apps/web/src/routes/(app)/tags/+page.svelte
Normal file
49
apps/mukke/apps/web/src/routes/(app)/tags/+page.svelte
Normal 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>
|
||||
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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:*",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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:*",
|
||||
|
|
|
|||
20
apps/planta/apps/web/src/lib/stores/tags.svelte.ts
Normal file
20
apps/planta/apps/web/src/lib/stores/tags.svelte.ts
Normal 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(),
|
||||
});
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
49
apps/planta/apps/web/src/routes/(app)/tags/+page.svelte
Normal file
49
apps/planta/apps/web/src/routes/(app)/tags/+page.svelte
Normal 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>
|
||||
|
|
@ -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:*",
|
||||
|
|
|
|||
20
apps/presi/apps/web/src/lib/stores/tags.svelte.ts
Normal file
20
apps/presi/apps/web/src/lib/stores/tags.svelte.ts
Normal 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(),
|
||||
});
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
49
apps/presi/apps/web/src/routes/(app)/tags/+page.svelte
Normal file
49
apps/presi/apps/web/src/routes/(app)/tags/+page.svelte
Normal 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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
20
apps/questions/apps/web/src/lib/stores/tags.svelte.ts
Normal file
20
apps/questions/apps/web/src/lib/stores/tags.svelte.ts
Normal 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(),
|
||||
});
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
49
apps/questions/apps/web/src/routes/(app)/tags/+page.svelte
Normal file
49
apps/questions/apps/web/src/routes/(app)/tags/+page.svelte
Normal 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>
|
||||
|
|
@ -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:*",
|
||||
|
|
|
|||
62
apps/storage/apps/web/src/lib/stores/tags.svelte.ts
Normal file
62
apps/storage/apps/web/src/lib/stores/tags.svelte.ts
Normal 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();
|
||||
},
|
||||
};
|
||||
|
|
@ -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()}
|
||||
|
|
|
|||
307
apps/storage/apps/web/src/routes/tags/+page.svelte
Normal file
307
apps/storage/apps/web/src/routes/tags/+page.svelte
Normal 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}
|
||||
|
|
@ -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:*",
|
||||
|
|
|
|||
20
apps/zitare/apps/web/src/lib/stores/tags.svelte.ts
Normal file
20
apps/zitare/apps/web/src/lib/stores/tags.svelte.ts
Normal 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(),
|
||||
});
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
49
apps/zitare/apps/web/src/routes/(app)/tags/+page.svelte
Normal file
49
apps/zitare/apps/web/src/routes/(app)/tags/+page.svelte
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue