diff --git a/apps/contacts/apps/web/src/lib/components/NetworkToolbar.svelte b/apps/contacts/apps/web/src/lib/components/NetworkToolbar.svelte new file mode 100644 index 000000000..b2e35bbc5 --- /dev/null +++ b/apps/contacts/apps/web/src/lib/components/NetworkToolbar.svelte @@ -0,0 +1,28 @@ + + + + + diff --git a/apps/contacts/apps/web/src/lib/components/NetworkToolbarContent.svelte b/apps/contacts/apps/web/src/lib/components/NetworkToolbarContent.svelte new file mode 100644 index 000000000..10394c811 --- /dev/null +++ b/apps/contacts/apps/web/src/lib/components/NetworkToolbarContent.svelte @@ -0,0 +1,305 @@ + + +
+ + {#if networkStore.uniqueTags.length > 0} +
+ +
+ {/if} + + + {#if networkStore.uniqueCompanies.length > 0} +
+ +
+ {/if} + +
+ + +
+ + +
+ +
+ + +
+ + + + +
+ + + {#if hasActiveFilters} +
+ + {/if} + + +
+ {networkStore.nodes.length} Kontakte + + {networkStore.links.length} Verbindungen +
+
+ + diff --git a/apps/contacts/apps/web/src/lib/components/ViewModeToggle.svelte b/apps/contacts/apps/web/src/lib/components/ViewModeToggle.svelte index 96a9df394..80ab572e3 100644 --- a/apps/contacts/apps/web/src/lib/components/ViewModeToggle.svelte +++ b/apps/contacts/apps/web/src/lib/components/ViewModeToggle.svelte @@ -4,7 +4,6 @@ const modes: { id: ViewMode; icon: string; label: string }[] = [ { id: 'alphabet', icon: 'alphabet', label: 'views.alphabet' }, - { id: 'list', icon: 'list', label: 'views.list' }, { id: 'grid', icon: 'grid', label: 'views.grid' }, ]; @@ -18,16 +17,7 @@ onclick={() => viewModeStore.setMode(mode.id)} title={$_(mode.label)} > - {#if mode.icon === 'list'} - - - - {:else if mode.icon === 'grid'} + {#if mode.icon === 'grid'} - import { _ } from 'svelte-i18n'; - import type { Contact } from '$lib/api/contacts'; - import { newContactModalStore } from '$lib/stores/new-contact-modal.svelte'; - - interface Props { - contacts: Contact[]; - onContactClick: (id: string) => void; - onToggleFavorite: (e: MouseEvent, id: string) => void; - selectionMode?: boolean; - selectedIds?: Set; - onToggleSelection?: (id: string) => void; - showNewContactCard?: boolean; - } - - let { - contacts, - onContactClick, - onToggleFavorite, - selectionMode = false, - selectedIds = new Set(), - onToggleSelection, - showNewContactCard = true, - }: Props = $props(); - - function getInitials(contact: Contact) { - const first = contact.firstName?.[0] || ''; - const last = contact.lastName?.[0] || ''; - return (first + last).toUpperCase() || contact.email?.[0]?.toUpperCase() || '?'; - } - - function getDisplayName(contact: Contact) { - if (contact.displayName) return contact.displayName; - if (contact.firstName || contact.lastName) { - return [contact.firstName, contact.lastName].filter(Boolean).join(' '); - } - return contact.email || 'Unbekannt'; - } - - function handleCheckboxClick(e: MouseEvent, id: string) { - e.stopPropagation(); - onToggleSelection?.(id); - } - - -
- - {#if showNewContactCard && !selectionMode} -
newContactModalStore.open()} - onkeydown={(e) => e.key === 'Enter' && newContactModalStore.open()} - class="contact-card new-contact-card w-full text-left cursor-pointer" - > - -
- - - -
- - -
-
- {$_('contacts.new')} -
-
- {$_('contacts.addFirst')} -
-
-
- {/if} - - {#each contacts as contact (contact.id)} -
onContactClick(contact.id)} - onkeydown={(e) => e.key === 'Enter' && onContactClick(contact.id)} - class="contact-card w-full text-left cursor-pointer {selectionMode && - selectedIds.has(contact.id) - ? 'selected' - : ''}" - > - - {#if selectionMode} - - {/if} - - -
- {#if contact.photoUrl} - {getDisplayName(contact)} - {:else} - {getInitials(contact)} - {/if} -
- - -
-
- {getDisplayName(contact)} -
- {#if contact.company || contact.jobTitle} -
- {[contact.jobTitle, contact.company].filter(Boolean).join(' @ ')} -
- {/if} - {#if contact.email} -
- {contact.email} -
- {/if} -
- - - -
- {/each} -
- - diff --git a/apps/contacts/apps/web/src/lib/stores/network.svelte.ts b/apps/contacts/apps/web/src/lib/stores/network.svelte.ts index 777b6141d..9410ec1c6 100644 --- a/apps/contacts/apps/web/src/lib/stores/network.svelte.ts +++ b/apps/contacts/apps/web/src/lib/stores/network.svelte.ts @@ -17,11 +17,43 @@ import type { SimulationNode as SharedSimulationNode, SimulationLink as SharedSimulationLink, } from '@manacore/shared-ui'; +import { NetworkGraph } from '@manacore/shared-ui'; // Re-export types from shared-ui for convenience export type SimulationNode = SharedSimulationNode; export type SimulationLink = SharedSimulationLink; +// Graph component reference for zoom controls +let graphComponentRef: NetworkGraph | null = null; + +// localStorage key for toolbar state +const TOOLBAR_STORAGE_KEY = 'network-toolbar-state'; + +// Load toolbar state from localStorage +function loadToolbarState(): boolean { + if (!browser) return true; + try { + const stored = localStorage.getItem(TOOLBAR_STORAGE_KEY); + if (stored) { + const parsed = JSON.parse(stored); + return parsed.isCollapsed ?? true; + } + } catch { + // Ignore parse errors + } + return true; +} + +// Save toolbar state to localStorage +function saveToolbarState(isCollapsed: boolean) { + if (!browser) return; + try { + localStorage.setItem(TOOLBAR_STORAGE_KEY, JSON.stringify({ isCollapsed })); + } catch { + // Ignore storage errors + } +} + // State let nodes = $state([]); let links = $state([]); @@ -37,6 +69,7 @@ let tickCounter = $state(0); // Used to trigger reactivity on simulation tick let simulationInitialized = false; let dataLoaded = false; // Prevent double loading let lastDimensions = { width: 0, height: 0 }; +let isToolbarCollapsed = $state(loadToolbarState()); // Derived state for filtering const filteredNodes = $derived.by(() => { @@ -159,6 +192,60 @@ export const networkStore = { get uniqueTags() { return uniqueTags; }, + get isToolbarCollapsed() { + return isToolbarCollapsed; + }, + + /** + * Set toolbar collapsed state + */ + setToolbarCollapsed(value: boolean) { + isToolbarCollapsed = value; + saveToolbarState(value); + }, + + /** + * Toggle toolbar collapsed state + */ + toggleToolbar() { + isToolbarCollapsed = !isToolbarCollapsed; + saveToolbarState(isToolbarCollapsed); + }, + + /** + * Register graph component reference for zoom controls + */ + setGraphComponent(component: NetworkGraph | null) { + graphComponentRef = component; + }, + + /** + * Zoom in on the graph + */ + zoomIn() { + graphComponentRef?.zoomIn(); + }, + + /** + * Zoom out on the graph + */ + zoomOut() { + graphComponentRef?.zoomOut(); + }, + + /** + * Reset zoom to fit all nodes + */ + resetZoom() { + graphComponentRef?.resetZoom(); + }, + + /** + * Focus on the currently selected node + */ + focusOnSelected() { + graphComponentRef?.focusOnSelectedNode(); + }, /** * Load network graph data from API diff --git a/apps/contacts/apps/web/src/lib/stores/settings.svelte.ts b/apps/contacts/apps/web/src/lib/stores/settings.svelte.ts index f5d679f13..7480186ce 100644 --- a/apps/contacts/apps/web/src/lib/stores/settings.svelte.ts +++ b/apps/contacts/apps/web/src/lib/stores/settings.svelte.ts @@ -8,7 +8,7 @@ import { browser } from '$app/environment'; // Settings types export type ContactSortBy = 'name' | 'company' | 'created' | 'updated'; export type ContactSortOrder = 'asc' | 'desc'; -export type ContactView = 'list' | 'grid' | 'alphabet'; +export type ContactView = 'grid' | 'alphabet'; export type DateFormat = 'dd.MM.yyyy' | 'MM/dd/yyyy' | 'yyyy-MM-dd'; export interface ContactsAppSettings { diff --git a/apps/contacts/apps/web/src/lib/stores/view-mode.svelte.ts b/apps/contacts/apps/web/src/lib/stores/view-mode.svelte.ts index 1a61b9bbb..74bd7a442 100644 --- a/apps/contacts/apps/web/src/lib/stores/view-mode.svelte.ts +++ b/apps/contacts/apps/web/src/lib/stores/view-mode.svelte.ts @@ -16,7 +16,7 @@ function getInitialMode(): ViewMode { // First check if there's a session-specific preference const sessionMode = sessionStorage.getItem(STORAGE_KEY); - if (sessionMode === 'list' || sessionMode === 'grid' || sessionMode === 'alphabet') { + if (sessionMode === 'grid' || sessionMode === 'alphabet') { return sessionMode; } @@ -57,7 +57,7 @@ export const viewModeStore = { // Check if there's a session preference const sessionMode = sessionStorage.getItem(STORAGE_KEY); - if (sessionMode === 'list' || sessionMode === 'grid' || sessionMode === 'alphabet') { + if (sessionMode === 'grid' || sessionMode === 'alphabet') { mode = sessionMode; } else { // Use default from settings diff --git a/apps/contacts/apps/web/src/routes/(app)/+layout.svelte b/apps/contacts/apps/web/src/routes/(app)/+layout.svelte index 68c279715..a7bf002e3 100644 --- a/apps/contacts/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/contacts/apps/web/src/routes/(app)/+layout.svelte @@ -47,6 +47,8 @@ formatParsedContactPreview, } from '$lib/utils/contact-parser'; import ContactsToolbar from '$lib/components/ContactsToolbar.svelte'; + import NetworkToolbar from '$lib/components/NetworkToolbar.svelte'; + import { networkStore } from '$lib/stores/network.svelte'; // Tags state for Quick-Create let availableTags = $state<{ id: string; name: string }[]>([]); @@ -75,15 +77,23 @@ // Show toolbar only on main contacts page const showContactsToolbar = $derived($page.url.pathname === '/' && !isSidebarMode); + // Show network toolbar only on network page + const showNetworkToolbar = $derived($page.url.pathname === '/network' && !isSidebarMode); + + // Check if any toolbar is expanded + const isAnyToolbarExpanded = $derived( + (showContactsToolbar && !contactsFilterStore.isToolbarCollapsed) || + (showNetworkToolbar && !networkStore.isToolbarCollapsed) + ); + // Dynamic bottom offset based on toolbar state const inputBarBottomOffset = $derived( - isSidebarMode - ? '0px' - : showContactsToolbar && !contactsFilterStore.isToolbarCollapsed - ? '140px' - : '70px' + isSidebarMode ? '0px' : isAnyToolbarExpanded ? '140px' : '70px' ); + // Show FAB when any toolbar is active + const showToolbarFab = $derived(showContactsToolbar || showNetworkToolbar); + // Use theme store's isDark directly let isDark = $derived(theme.isDark); @@ -387,13 +397,18 @@ primaryColor="#3b82f6" autoFocus={true} bottomOffset={inputBarBottomOffset} - hasFabRight={showContactsToolbar} + hasFabRight={showToolbarFab} /> {#if showContactsToolbar} {/if} + + + {#if showNetworkToolbar} + + {/if} diff --git a/apps/contacts/apps/web/src/routes/(app)/network/+page.svelte b/apps/contacts/apps/web/src/routes/(app)/network/+page.svelte index 7441545fa..0749db726 100644 --- a/apps/contacts/apps/web/src/routes/(app)/network/+page.svelte +++ b/apps/contacts/apps/web/src/routes/(app)/network/+page.svelte @@ -3,7 +3,7 @@ import { goto } from '$app/navigation'; import { networkStore, type SimulationNode } from '$lib/stores/network.svelte'; import { contactsFilterStore } from '$lib/stores/filter.svelte'; - import { NetworkGraph, NetworkControls } from '@manacore/shared-ui'; + import { NetworkGraph } from '@manacore/shared-ui'; import ContactDetailModal from '$lib/components/ContactDetailModal.svelte'; import { NetworkGraphSkeleton } from '$lib/components/skeletons'; import '$lib/i18n'; @@ -62,37 +62,10 @@ networkStore.releaseNode(node.id); } - function handleZoomIn() { - graphComponent?.zoomIn(); - } - - function handleZoomOut() { - graphComponent?.zoomOut(); - } - - function handleResetZoom() { - graphComponent?.resetZoom(); - } - - function handleFocusSelected() { - graphComponent?.focusOnSelectedNode(); - } - - function handleTagFilter(tagId: string | null) { - networkStore.setFilterTag(tagId); - } - - function handleSubtitleFilter(company: string | null) { - networkStore.setFilterCompany(company); - } - - function handleStrengthFilter(strength: number) { - networkStore.setMinStrength(strength); - } - - function handleClearFilters() { - networkStore.clearFilters(); - } + // Register graph component with store when it changes + $effect(() => { + networkStore.setGraphComponent(graphComponent); + }); // Initialize simulation when data is loaded and container is ready $effect(() => { @@ -109,6 +82,7 @@ }); onDestroy(() => { + networkStore.setGraphComponent(null); networkStore.stopSimulation(); }); @@ -118,31 +92,6 @@
- -
- -
- {#if networkStore.error}