mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 02:02:13 +02:00
feat(ui): add shared ExpandableToolbar and unify toolbar dropdowns
- Create ExpandableToolbar shared component in shared-ui - Migrate CalendarToolbar to use shared component (253 → 90 LOC) - Migrate ContactsToolbar to use shared component (177 → 31 LOC) - Add portal pattern to FilterBar dropdown for proper positioning - Unify FilterBar dropdown styling with ContextMenu design - Add fly transition with offset for smooth dropdown appearance - Add ContactsToolbarContent for toolbar content separation - Add filter store for contacts filter state management - Add NewContactModal component - Various contacts view improvements 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9eb3f42483
commit
863f296733
23 changed files with 2347 additions and 787 deletions
|
|
@ -88,6 +88,7 @@ export {
|
|||
PillToolbar,
|
||||
PillToolbarButton,
|
||||
PillToolbarDivider,
|
||||
ExpandableToolbar,
|
||||
} from './navigation';
|
||||
export type {
|
||||
NavItem,
|
||||
|
|
@ -100,6 +101,7 @@ export type {
|
|||
PillNavElement,
|
||||
PillNavigationProps,
|
||||
PillTabOption,
|
||||
ExpandableToolbarProps,
|
||||
} from './navigation';
|
||||
|
||||
// Settings
|
||||
|
|
|
|||
|
|
@ -0,0 +1,223 @@
|
|||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
/** Whether the toolbar is collapsed */
|
||||
isCollapsed?: boolean;
|
||||
/** Called when collapsed state changes */
|
||||
onCollapsedChange?: (isCollapsed: boolean) => void;
|
||||
/** Whether in sidebar mode (affects positioning) */
|
||||
isSidebarMode?: boolean;
|
||||
/** Bottom offset from viewport bottom (default: '70px') */
|
||||
bottomOffset?: string;
|
||||
/** Sidebar mode bottom offset (default: '0px') */
|
||||
sidebarBottomOffset?: string;
|
||||
/** Panel height when expanded (default: '70px') */
|
||||
panelHeight?: string;
|
||||
/** FAB tooltip when collapsed */
|
||||
collapsedTitle?: string;
|
||||
/** FAB tooltip when expanded */
|
||||
expandedTitle?: string;
|
||||
/** Custom collapsed icon snippet */
|
||||
collapsedIcon?: Snippet;
|
||||
/** Custom expanded icon snippet */
|
||||
expandedIcon?: Snippet;
|
||||
/** Panel content (required) */
|
||||
children: Snippet;
|
||||
/** Optional right-side content (e.g., layout toggle) */
|
||||
rightActions?: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
isCollapsed = true,
|
||||
onCollapsedChange,
|
||||
isSidebarMode = false,
|
||||
bottomOffset = '70px',
|
||||
sidebarBottomOffset = '0px',
|
||||
panelHeight = '70px',
|
||||
collapsedTitle = 'Optionen',
|
||||
expandedTitle = 'Schließen',
|
||||
collapsedIcon,
|
||||
expandedIcon,
|
||||
children,
|
||||
rightActions,
|
||||
}: Props = $props();
|
||||
|
||||
function toggleToolbar() {
|
||||
onCollapsedChange?.(!isCollapsed);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- FAB Button - positioned next to InputBar -->
|
||||
<div
|
||||
class="fab-container"
|
||||
class:sidebar-mode={isSidebarMode}
|
||||
class:expanded={!isCollapsed}
|
||||
style="--bottom-offset: {isSidebarMode
|
||||
? sidebarBottomOffset
|
||||
: bottomOffset}; --panel-height: {panelHeight};"
|
||||
>
|
||||
<button
|
||||
onclick={toggleToolbar}
|
||||
class="toolbar-fab glass-pill"
|
||||
class:active={!isCollapsed}
|
||||
title={isCollapsed ? collapsedTitle : expandedTitle}
|
||||
>
|
||||
<svg class="fab-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{#if isCollapsed}
|
||||
{#if collapsedIcon}
|
||||
{@render collapsedIcon()}
|
||||
{:else}
|
||||
<!-- Default settings/sliders icon -->
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"
|
||||
/>
|
||||
{/if}
|
||||
{:else if expandedIcon}
|
||||
{@render expandedIcon()}
|
||||
{:else}
|
||||
<!-- Default chevron down icon -->
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
{/if}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Expanded Toolbar Panel - below InputBar, pushes content up -->
|
||||
{#if !isCollapsed}
|
||||
<div
|
||||
class="toolbar-bar glass-panel"
|
||||
class:sidebar-mode={isSidebarMode}
|
||||
style="--bottom-offset: {isSidebarMode ? sidebarBottomOffset : bottomOffset};"
|
||||
transition:slide={{ duration: 200 }}
|
||||
>
|
||||
<div class="toolbar-content">
|
||||
{@render children()}
|
||||
|
||||
{#if rightActions}
|
||||
<div class="toolbar-divider"></div>
|
||||
{@render rightActions()}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
/* FAB Container - positioned next to InputBar (aligned with input-container) */
|
||||
.fab-container {
|
||||
position: fixed;
|
||||
bottom: calc(
|
||||
var(--bottom-offset, 70px) + 9px + env(safe-area-inset-bottom, 0px)
|
||||
); /* base offset + 9px to align with input-container */
|
||||
right: calc(50% - 350px - 70px); /* Right of InputBar (max-width 700px / 2 + gap) */
|
||||
z-index: 91; /* Above InputBar (90) */
|
||||
pointer-events: none;
|
||||
transition: bottom 0.2s ease;
|
||||
}
|
||||
|
||||
/* When expanded, move FAB up with InputBar */
|
||||
.fab-container.expanded {
|
||||
bottom: calc(
|
||||
var(--bottom-offset, 70px) + var(--panel-height, 70px) + 9px +
|
||||
env(safe-area-inset-bottom, 0px)
|
||||
);
|
||||
}
|
||||
|
||||
/* Responsive positioning */
|
||||
@media (max-width: 900px) {
|
||||
.fab-container {
|
||||
right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Toolbar Bar - full width below InputBar */
|
||||
.toolbar-bar {
|
||||
position: fixed;
|
||||
bottom: calc(var(--bottom-offset, 70px) + env(safe-area-inset-bottom, 0px));
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 89; /* Below InputBar (90) */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.toolbar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: hsl(var(--color-surface) / 0.92);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
box-shadow: 0 -2px 16px hsl(var(--color-foreground) / 0.08);
|
||||
border-radius: 1rem;
|
||||
white-space: nowrap;
|
||||
max-width: calc(100vw - 2rem);
|
||||
/* Allow dropdowns to overflow */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Glass styling */
|
||||
.glass-pill {
|
||||
background: hsl(var(--color-surface) / 0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
box-shadow: 0 2px 8px hsl(var(--color-foreground) / 0.08);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.glass-panel {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* FAB Button - same height as InputBar (54px) */
|
||||
.toolbar-fab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.2s ease;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.toolbar-fab:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px hsl(var(--color-foreground) / 0.15);
|
||||
}
|
||||
|
||||
.toolbar-fab.active {
|
||||
background: hsl(var(--color-muted));
|
||||
}
|
||||
|
||||
.toolbar-fab.active .fab-icon {
|
||||
color: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.fab-icon {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.toolbar-fab:hover .fab-icon {
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.toolbar-divider {
|
||||
width: 1px;
|
||||
height: 1.5rem;
|
||||
background: hsl(var(--color-border));
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export { default as ExpandableToolbar } from './ExpandableToolbar.svelte';
|
||||
export type { ExpandableToolbarProps } from './types';
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import type { Snippet } from 'svelte';
|
||||
|
||||
export interface ExpandableToolbarProps {
|
||||
/** Whether the toolbar is collapsed */
|
||||
isCollapsed?: boolean;
|
||||
/** Called when collapsed state changes */
|
||||
onCollapsedChange?: (isCollapsed: boolean) => void;
|
||||
/** Whether in sidebar mode (affects positioning) */
|
||||
isSidebarMode?: boolean;
|
||||
/** Bottom offset from viewport bottom (default: '70px') */
|
||||
bottomOffset?: string;
|
||||
/** Sidebar mode bottom offset (default: '0px') */
|
||||
sidebarBottomOffset?: string;
|
||||
/** Panel height when expanded (default: '70px') */
|
||||
panelHeight?: string;
|
||||
/** FAB tooltip when collapsed */
|
||||
collapsedTitle?: string;
|
||||
/** FAB tooltip when expanded */
|
||||
expandedTitle?: string;
|
||||
/** Custom collapsed icon snippet */
|
||||
collapsedIcon?: Snippet;
|
||||
/** Custom expanded icon snippet */
|
||||
expandedIcon?: Snippet;
|
||||
/** Panel content (required) */
|
||||
children: Snippet;
|
||||
/** Optional right-side content (e.g., layout toggle) */
|
||||
rightActions?: Snippet;
|
||||
}
|
||||
|
|
@ -10,6 +10,8 @@ export { default as PillViewSwitcher } from './PillViewSwitcher.svelte';
|
|||
export { default as PillToolbar } from './PillToolbar.svelte';
|
||||
export { default as PillToolbarButton } from './PillToolbarButton.svelte';
|
||||
export { default as PillToolbarDivider } from './PillToolbarDivider.svelte';
|
||||
export { ExpandableToolbar } from './expandable-toolbar';
|
||||
export type { ExpandableToolbarProps } from './expandable-toolbar';
|
||||
export type {
|
||||
NavItem,
|
||||
NavbarProps,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
linkLabel?: string;
|
||||
searchPlaceholder?: string;
|
||||
minStrength?: number;
|
||||
showSearch?: boolean;
|
||||
onSearch?: (query: string) => void;
|
||||
onTagFilter?: (tagId: string | null) => void;
|
||||
onSubtitleFilter?: (subtitle: string | null) => void;
|
||||
|
|
@ -39,6 +40,7 @@
|
|||
linkLabel = 'Verbindungen',
|
||||
searchPlaceholder = 'Suchen...',
|
||||
minStrength = 0,
|
||||
showSearch = true,
|
||||
onSearch,
|
||||
onTagFilter,
|
||||
onSubtitleFilter,
|
||||
|
|
@ -122,22 +124,24 @@
|
|||
|
||||
<div class="network-controls">
|
||||
<!-- Search bar -->
|
||||
<div class="search-container">
|
||||
<Search size={18} class="search-icon" />
|
||||
<input
|
||||
bind:this={searchInputElement}
|
||||
type="text"
|
||||
placeholder={searchPlaceholder}
|
||||
value={searchInput}
|
||||
oninput={handleSearchInput}
|
||||
class="search-input"
|
||||
/>
|
||||
{#if searchInput}
|
||||
<button onclick={clearSearch} class="clear-btn" aria-label="Suche löschen">
|
||||
<X size={16} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if showSearch}
|
||||
<div class="search-container">
|
||||
<Search size={18} class="search-icon" />
|
||||
<input
|
||||
bind:this={searchInputElement}
|
||||
type="text"
|
||||
placeholder={searchPlaceholder}
|
||||
value={searchInput}
|
||||
oninput={handleSearchInput}
|
||||
class="search-input"
|
||||
/>
|
||||
{#if searchInput}
|
||||
<button onclick={clearSearch} class="clear-btn" aria-label="Suche löschen">
|
||||
<X size={16} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Filter toggle -->
|
||||
{#if tags.length > 0 || subtitles.length > 0}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue