mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 18:29:39 +02:00
feat(shared-ui): add theme mode selector to PillNavigation dropdown
Integrate Light/Dark/System mode toggle into the theme variant dropdown as a header section with icon-only buttons. The standalone theme toggle is now hidden when showThemeVariants is enabled. - Add header snippet support to PillDropdown component - Add themeMode and onThemeModeChange props to PillNavigation - Create compact 3-button mode selector (sun/moon/monitor icons) - Style mode selector to match glass-pill design system 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f436fbb99d
commit
3cfa6a765a
3 changed files with 166 additions and 10 deletions
|
|
@ -47,13 +47,17 @@
|
|||
|
||||
// Navigation items for Chat
|
||||
const navItems: PillNavItem[] = [
|
||||
{ href: '/', label: 'Chat', icon: 'home' },
|
||||
{ href: '/chat', label: 'Chat', icon: 'home' },
|
||||
{ href: '/templates', label: 'Templates', icon: 'document' },
|
||||
{ href: '/spaces', label: 'Spaces', icon: 'building' },
|
||||
{ href: '/documents', label: 'Dokumente', icon: 'archive' },
|
||||
{ href: '/archive', label: 'Archiv', icon: 'list' },
|
||||
{ href: '/settings', label: 'Einstellungen', icon: 'settings' },
|
||||
];
|
||||
|
||||
// Check if current page is a chat page (needs full-width layout)
|
||||
let isChatPage = $derived($page.url.pathname.startsWith('/chat'));
|
||||
|
||||
// Navigation shortcuts (Ctrl+1-5)
|
||||
const navRoutes = navItems.map((item) => item.href);
|
||||
|
||||
|
|
@ -96,6 +100,10 @@
|
|||
theme.toggleMode();
|
||||
}
|
||||
|
||||
function handleThemeModeChange(mode: 'light' | 'dark' | 'system') {
|
||||
theme.setMode(mode);
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
await authStore.signOut();
|
||||
goto('/login');
|
||||
|
|
@ -152,7 +160,7 @@
|
|||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="ManaChat"
|
||||
homeRoute="/"
|
||||
homeRoute="/chat"
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
{isSidebarMode}
|
||||
|
|
@ -163,6 +171,8 @@
|
|||
showThemeVariants={true}
|
||||
{themeVariantItems}
|
||||
{currentThemeVariantLabel}
|
||||
themeMode={theme.mode}
|
||||
onThemeModeChange={handleThemeModeChange}
|
||||
showLanguageSwitcher={false}
|
||||
showLogout={true}
|
||||
onLogout={handleLogout}
|
||||
|
|
@ -174,10 +184,16 @@
|
|||
class="main-content bg-background"
|
||||
class:sidebar-mode={isSidebarMode && !isCollapsed}
|
||||
class:floating-mode={!isSidebarMode && !isCollapsed}
|
||||
class:chat-page={isChatPage}
|
||||
>
|
||||
<div class="content-wrapper">
|
||||
{#if isChatPage}
|
||||
<!-- Full-width layout for chat pages -->
|
||||
{@render children()}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="content-wrapper">
|
||||
{@render children()}
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -204,6 +220,11 @@
|
|||
padding-left: 180px;
|
||||
}
|
||||
|
||||
/* Chat page - no content wrapper, but keep nav padding */
|
||||
.main-content.chat-page {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
max-width: 80rem;
|
||||
margin-left: auto;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { PillDropdownItem } from './types';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -8,9 +9,11 @@
|
|||
icon?: 'globe' | 'language' | 'chevronDown' | 'check' | string;
|
||||
isOpen?: boolean;
|
||||
onToggle?: (open: boolean) => void;
|
||||
/** Optional header content (e.g., mode selector) */
|
||||
header?: Snippet;
|
||||
}
|
||||
|
||||
let { items, direction = 'down', label, icon, isOpen = false, onToggle }: Props = $props();
|
||||
let { items, direction = 'down', label, icon, isOpen = false, onToggle, header }: Props = $props();
|
||||
|
||||
let internalOpen = $state(false);
|
||||
let triggerButton: HTMLButtonElement;
|
||||
|
|
@ -115,13 +118,20 @@
|
|||
class:fan-down={direction === 'down'}
|
||||
style="top: {dropdownPosition.top}px; left: {dropdownPosition.left}px;"
|
||||
>
|
||||
<!-- Optional header (e.g., mode selector) -->
|
||||
{#if header}
|
||||
<div class="dropdown-header">
|
||||
{@render header()}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each items.filter((i) => !i.disabled) as item, i (item.id)}
|
||||
<button
|
||||
onclick={() => handleItemClick(item)}
|
||||
class="pill glass-pill fan-pill"
|
||||
class:danger-pill={item.danger}
|
||||
class:active-pill={item.active}
|
||||
style="animation-delay: {i * 15}ms"
|
||||
style="animation-delay: {(header ? i + 1 : i) * 15}ms"
|
||||
>
|
||||
{#if item.icon}
|
||||
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -299,4 +309,15 @@
|
|||
border: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Header for custom content (e.g., mode selector) */
|
||||
.dropdown-header {
|
||||
animation: fanIn 0.15s ease-out forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
.fan-up .dropdown-header {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
currentLanguageLabel?: string;
|
||||
/** Show language switcher */
|
||||
showLanguageSwitcher?: boolean;
|
||||
/** Show theme toggle */
|
||||
/** Show theme toggle (standalone button, hidden if showThemeVariants is true) */
|
||||
showThemeToggle?: boolean;
|
||||
/** Primary color for active state (CSS custom property or hex) */
|
||||
primaryColor?: string;
|
||||
|
|
@ -49,6 +49,10 @@
|
|||
currentThemeVariantLabel?: string;
|
||||
/** Show theme variant selector */
|
||||
showThemeVariants?: boolean;
|
||||
/** Current theme mode ('light', 'dark', 'system') */
|
||||
themeMode?: 'light' | 'dark' | 'system';
|
||||
/** Called when theme mode changes */
|
||||
onThemeModeChange?: (mode: 'light' | 'dark' | 'system') => void;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -74,6 +78,8 @@
|
|||
themeVariantItems = [],
|
||||
currentThemeVariantLabel = 'Theme',
|
||||
showThemeVariants = false,
|
||||
themeMode = 'system',
|
||||
onThemeModeChange,
|
||||
}: Props = $props();
|
||||
|
||||
// Type guards for elements
|
||||
|
|
@ -315,11 +321,50 @@
|
|||
direction="down"
|
||||
label={currentThemeVariantLabel}
|
||||
icon="palette"
|
||||
/>
|
||||
>
|
||||
{#snippet header()}
|
||||
<div class="theme-mode-selector">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => onThemeModeChange?.('light')}
|
||||
class="mode-btn"
|
||||
class:active={themeMode === 'light'}
|
||||
title="Light mode"
|
||||
>
|
||||
<svg class="mode-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={getIconPath('sun')} />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => onThemeModeChange?.('dark')}
|
||||
class="mode-btn"
|
||||
class:active={themeMode === 'dark'}
|
||||
title="Dark mode"
|
||||
>
|
||||
<svg class="mode-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={getIconPath('moon')} />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => onThemeModeChange?.('system')}
|
||||
class="mode-btn"
|
||||
class:active={themeMode === 'system'}
|
||||
title="System mode"
|
||||
>
|
||||
<svg class="mode-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" stroke-width="2" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 21h8M12 17v4" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</PillDropdown>
|
||||
{/if}
|
||||
|
||||
<!-- Theme Toggle -->
|
||||
{#if showThemeToggle && onToggleTheme}
|
||||
<!-- Theme Toggle (only show when not using theme variants dropdown) -->
|
||||
{#if showThemeToggle && onToggleTheme && !showThemeVariants}
|
||||
<button
|
||||
onclick={onToggleTheme}
|
||||
class="pill glass-pill"
|
||||
|
|
@ -723,4 +768,73 @@
|
|||
.pill-nav-container {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Theme mode selector in dropdown header */
|
||||
.theme-mode-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 9999px;
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
:global(.dark) .theme-mode-selector {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.mode-btn {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.375rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 9999px;
|
||||
cursor: pointer;
|
||||
color: #374151;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
:global(.dark) .mode-btn {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.mode-btn:hover:not(.active) {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
:global(.dark) .mode-btn:hover:not(.active) {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.mode-btn.active {
|
||||
background: var(--pill-primary-color, var(--color-primary-500, rgba(248, 214, 43, 0.2)));
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--pill-primary-color, var(--color-primary-500, #3b82f6)) 20%,
|
||||
white 80%
|
||||
);
|
||||
}
|
||||
|
||||
:global(.dark) .mode-btn.active {
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--pill-primary-color, var(--color-primary-500, #3b82f6)) 30%,
|
||||
transparent 70%
|
||||
);
|
||||
}
|
||||
|
||||
.mode-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue