mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 10:46:41 +02:00
feat(shared-ui): extend PillNavigation with tab groups and migrate Picture app
- Add PillTabGroup component for segmented controls within navigation - Extend PillNavigation with elements prop supporting tabs and dividers - Add new types: PillTabOption, PillTabGroupConfig, PillDivider, PillNavElement - Migrate Picture app from custom Sidebar to shared PillNavigation - Add transparent filter bars to Gallery and Explore pages - Add dev credentials auto-fill on logo click in shared LoginPage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
af8bb9bcb0
commit
4eed41499a
8 changed files with 661 additions and 30 deletions
|
|
@ -1,17 +1,50 @@
|
|||
<script lang="ts">
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import Sidebar from '$lib/components/layout/Sidebar.svelte';
|
||||
import KeyboardShortcutsModal from '$lib/components/ui/KeyboardShortcutsModal.svelte';
|
||||
import { currentTheme } from '$lib/stores/theme';
|
||||
import { isSidebarCollapsed, toggleSidebar } from '$lib/stores/sidebar';
|
||||
import { isUIVisible, toggleUI, showKeyboardShortcuts } from '$lib/stores/ui';
|
||||
import { viewMode } from '$lib/stores/view';
|
||||
import { page } from '$app/stores';
|
||||
import { PillNavigation } from '@manacore/shared-ui';
|
||||
import type { PillNavItem, PillNavElement } from '@manacore/shared-ui';
|
||||
import KeyboardShortcutsModal from '$lib/components/ui/KeyboardShortcutsModal.svelte';
|
||||
import { currentTheme, actualMode, toggleThemeMode } from '$lib/stores/theme';
|
||||
import { isUIVisible, toggleUI, showKeyboardShortcuts } from '$lib/stores/ui';
|
||||
import { viewMode, setViewMode, type ViewMode } from '$lib/stores/view';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
// PillNav state
|
||||
let isSidebarMode = $state(false);
|
||||
let isCollapsed = $state(false);
|
||||
|
||||
// Load persisted nav state
|
||||
$effect(() => {
|
||||
if (browser) {
|
||||
const savedSidebarMode = localStorage.getItem('picture-nav-sidebar');
|
||||
const savedCollapsed = localStorage.getItem('picture-nav-collapsed');
|
||||
if (savedSidebarMode !== null) isSidebarMode = savedSidebarMode === 'true';
|
||||
if (savedCollapsed !== null) isCollapsed = savedCollapsed === 'true';
|
||||
}
|
||||
});
|
||||
|
||||
function handleModeChange(isSidebar: boolean) {
|
||||
isSidebarMode = isSidebar;
|
||||
if (browser) localStorage.setItem('picture-nav-sidebar', String(isSidebar));
|
||||
}
|
||||
|
||||
function handleCollapsedChange(collapsed: boolean) {
|
||||
isCollapsed = collapsed;
|
||||
if (browser) localStorage.setItem('picture-nav-collapsed', String(collapsed));
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
await authStore.signOut();
|
||||
goto('/auth/login');
|
||||
}
|
||||
|
||||
function handleToggleTheme() {
|
||||
toggleThemeMode();
|
||||
}
|
||||
|
||||
// Client-side auth check
|
||||
$effect(() => {
|
||||
if (authStore.initialized && !authStore.loading && !authStore.user) {
|
||||
|
|
@ -19,9 +52,42 @@
|
|||
}
|
||||
});
|
||||
|
||||
// Navigation items
|
||||
const navItems: PillNavItem[] = [
|
||||
{ href: '/app/gallery', label: 'Galerie', icon: 'home' },
|
||||
{ href: '/app/board', label: 'Moodboards', icon: 'grid' },
|
||||
{ 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: '/app/archive', label: 'Archiv', icon: 'archive' },
|
||||
{ href: '/app/subscription', label: 'Abo', icon: 'creditCard' },
|
||||
];
|
||||
|
||||
// View mode options for tab group
|
||||
const viewModeOptions = [
|
||||
{ id: 'single', icon: 'list', title: 'Liste (1)' },
|
||||
{ id: 'grid3', icon: 'grid', title: 'Mittel (2)' },
|
||||
{ id: 'gridSmall', icon: 'gridSmall', title: 'Klein (3)' },
|
||||
];
|
||||
|
||||
// Elements (divider + view mode tabs)
|
||||
let elements: PillNavElement[] = $derived([
|
||||
{ type: 'divider' as const },
|
||||
{
|
||||
type: 'tabs' as const,
|
||||
sectionLabel: 'Ansicht',
|
||||
options: viewModeOptions,
|
||||
value: $viewMode === 'grid5' ? 'gridSmall' : $viewMode,
|
||||
onChange: (id: string) => {
|
||||
const mode = id === 'gridSmall' ? 'grid5' : id as ViewMode;
|
||||
setViewMode(mode);
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// Global keyboard shortcuts
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
// Ignore if user is typing in an input/textarea
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
|
||||
return;
|
||||
|
|
@ -65,19 +131,15 @@
|
|||
break;
|
||||
case '1':
|
||||
e.preventDefault();
|
||||
viewMode.set('single');
|
||||
setViewMode('single');
|
||||
break;
|
||||
case '2':
|
||||
e.preventDefault();
|
||||
viewMode.set('grid3');
|
||||
setViewMode('grid3');
|
||||
break;
|
||||
case '3':
|
||||
e.preventDefault();
|
||||
viewMode.set('grid5');
|
||||
break;
|
||||
case 's':
|
||||
e.preventDefault();
|
||||
toggleSidebar();
|
||||
setViewMode('grid5');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -96,20 +158,34 @@
|
|||
</div>
|
||||
{:else if authStore.user}
|
||||
<div class="min-h-screen" style="background-color: {$currentTheme.background};">
|
||||
<!-- Sidebar (conditionally visible) -->
|
||||
<!-- PillNavigation (conditionally visible) -->
|
||||
{#if $isUIVisible}
|
||||
<Sidebar />
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
{elements}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Picture"
|
||||
homeRoute="/app/gallery"
|
||||
onLogout={handleLogout}
|
||||
onToggleTheme={handleToggleTheme}
|
||||
isDark={$actualMode === 'dark'}
|
||||
{isSidebarMode}
|
||||
onModeChange={handleModeChange}
|
||||
{isCollapsed}
|
||||
onCollapsedChange={handleCollapsedChange}
|
||||
showThemeToggle={true}
|
||||
showLanguageSwitcher={false}
|
||||
primaryColor="#3b82f6"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main
|
||||
class="transition-all duration-300 {$isSidebarCollapsed || !$isUIVisible
|
||||
? 'lg:pl-0'
|
||||
: 'lg:pl-[17rem]'}"
|
||||
class="main-content transition-all duration-300"
|
||||
class:sidebar-mode={isSidebarMode && !isCollapsed && $isUIVisible}
|
||||
class:floating-mode={!isSidebarMode && !isCollapsed && $isUIVisible}
|
||||
>
|
||||
<!-- Desktop: Left padding when sidebar is open -->
|
||||
<!-- Mobile: Top padding for header + Bottom padding for nav -->
|
||||
<div class="min-h-screen pb-20 pt-16 lg:pb-0 lg:pt-0">
|
||||
<div class="min-h-screen">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</main>
|
||||
|
|
@ -118,3 +194,26 @@
|
|||
<KeyboardShortcutsModal />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
/* Floating nav mode - add top padding for fixed nav */
|
||||
.main-content.floating-mode {
|
||||
padding-top: 80px;
|
||||
}
|
||||
|
||||
/* Sidebar mode - add left padding for sidebar nav */
|
||||
.main-content.sidebar-mode {
|
||||
padding-left: 180px;
|
||||
}
|
||||
|
||||
/* Mobile adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.main-content.floating-mode {
|
||||
padding-top: 70px;
|
||||
}
|
||||
.main-content.sidebar-mode {
|
||||
padding-left: 0;
|
||||
padding-top: 70px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -167,6 +167,99 @@
|
|||
<title>Entdecken - Picture</title>
|
||||
</svelte:head>
|
||||
|
||||
<!-- Filter Bar -->
|
||||
<div class="sticky top-0 z-20 px-4 py-3">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<!-- Search -->
|
||||
<div class="relative min-w-[200px] flex-1 max-w-md">
|
||||
<input
|
||||
type="text"
|
||||
value={searchInput}
|
||||
oninput={handleSearchInput}
|
||||
placeholder="Bilder suchen..."
|
||||
class="w-full rounded-full border border-gray-300/50 bg-white/80 px-4 py-2 pl-10 text-sm text-gray-900 placeholder-gray-500 backdrop-blur-xl transition-all focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600/50 dark:bg-gray-800/80 dark:text-gray-100 dark:placeholder-gray-400"
|
||||
/>
|
||||
<svg
|
||||
class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400 dark:text-gray-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
{#if searchInput}
|
||||
<button
|
||||
onclick={() => {
|
||||
searchInput = '';
|
||||
exploreSearchQuery.set('');
|
||||
loadInitialImages();
|
||||
}}
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Sort -->
|
||||
<select
|
||||
value={$exploreSortBy}
|
||||
onchange={handleSortChange}
|
||||
class="rounded-full border border-gray-300/50 bg-white/80 px-4 py-2 text-sm text-gray-900 backdrop-blur-xl focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600/50 dark:bg-gray-800/80 dark:text-gray-100"
|
||||
>
|
||||
<option value="recent">Neueste</option>
|
||||
<option value="popular">Beliebt</option>
|
||||
<option value="trending">Im Trend</option>
|
||||
</select>
|
||||
|
||||
<!-- Favorites Toggle -->
|
||||
<button
|
||||
onclick={() => showExploreFavoritesOnly.update((v) => !v)}
|
||||
class="inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm font-medium transition-all {$showExploreFavoritesOnly
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-100/80 text-gray-700 backdrop-blur-xl hover:bg-gray-200/80 dark:bg-gray-800/80 dark:text-gray-300 dark:hover:bg-gray-700/80'}"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
fill={$showExploreFavoritesOnly ? 'currentColor' : 'none'}
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Favoriten</span>
|
||||
</button>
|
||||
|
||||
<!-- Reset Filter -->
|
||||
{#if $exploreSearchQuery || $showExploreFavoritesOnly || $exploreSortBy !== 'recent'}
|
||||
<button
|
||||
onclick={() => {
|
||||
searchInput = '';
|
||||
exploreSearchQuery.set('');
|
||||
exploreSortBy.set('recent');
|
||||
showExploreFavoritesOnly.set(false);
|
||||
loadInitialImages();
|
||||
}}
|
||||
class="text-sm font-medium text-blue-600 hover:underline dark:text-blue-400"
|
||||
>
|
||||
Zurücksetzen
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $isLoadingExplore && $exploreImages.length === 0}
|
||||
<div class="px-4 py-8">
|
||||
<ImageSkeleton count={20} />
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
import ContextMenu from '$lib/components/ui/ContextMenu.svelte';
|
||||
import ImageSkeleton from '$lib/components/ui/ImageSkeleton.svelte';
|
||||
import ViewModeSwitcher from '$lib/components/ui/ViewModeSwitcher.svelte';
|
||||
import TagPills from '$lib/components/tags/TagPills.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let loadingMore = $state(false);
|
||||
|
|
@ -119,6 +120,70 @@
|
|||
<title>Gallery - Picture</title>
|
||||
</svelte:head>
|
||||
|
||||
<!-- Filter Bar -->
|
||||
<div class="sticky top-0 z-20 px-4 py-3">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<!-- Favorites Toggle -->
|
||||
<button
|
||||
onclick={() => showFavoritesOnly.update((v) => !v)}
|
||||
class="inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm font-medium transition-all {$showFavoritesOnly
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-100/80 text-gray-700 backdrop-blur-xl hover:bg-gray-200/80 dark:bg-gray-800/80 dark:text-gray-300 dark:hover:bg-gray-700/80'}"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
fill={$showFavoritesOnly ? 'currentColor' : 'none'}
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Favoriten</span>
|
||||
</button>
|
||||
|
||||
<!-- Tags -->
|
||||
{#if $tags.length > 0}
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">Tags:</span>
|
||||
<TagPills />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Reset Filter -->
|
||||
{#if $selectedTags.length > 0 || $showFavoritesOnly}
|
||||
<button
|
||||
onclick={() => {
|
||||
selectedTags.set([]);
|
||||
showFavoritesOnly.set(false);
|
||||
}}
|
||||
class="ml-auto text-sm font-medium text-blue-600 hover:underline dark:text-blue-400"
|
||||
>
|
||||
Filter zurücksetzen
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Active Filter Summary -->
|
||||
{#if $selectedTags.length > 0 || $showFavoritesOnly}
|
||||
<div class="mt-2 rounded-lg bg-blue-50/80 px-3 py-1.5 backdrop-blur-xl dark:bg-blue-900/30">
|
||||
<p class="text-xs font-medium text-blue-900 dark:text-blue-100">
|
||||
{#if $showFavoritesOnly && $selectedTags.length > 0}
|
||||
Favoriten + {$selectedTags.length} {$selectedTags.length === 1 ? 'Tag' : 'Tags'}
|
||||
{:else if $showFavoritesOnly}
|
||||
Nur Favoriten
|
||||
{:else}
|
||||
{$selectedTags.length} {$selectedTags.length === 1 ? 'Tag' : 'Tags'} ausgewählt
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if $isLoading}
|
||||
<div class="px-4 py-8">
|
||||
<ImageSkeleton count={20} />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue