fix(contacts): improve ContactList and FilterBar styling

- Update ContactList with better spacing and layout
- Improve FilterBar responsiveness
- Adjust app.css for new component styles

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-12-12 02:40:44 +01:00
parent b42db508a3
commit 5cd550c5a1
3 changed files with 392 additions and 311 deletions

View file

@ -37,10 +37,10 @@
@layer components {
/* Card styles */
.card {
background-color: hsl(var(--card));
background-color: var(--color-card);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
border: 1px solid hsl(var(--border));
border: 1px solid var(--color-border);
transition: transform var(--transition-base), box-shadow var(--transition-base);
}
@ -56,14 +56,14 @@
gap: var(--spacing-md);
padding: var(--spacing-md);
border-radius: var(--radius-md);
border: 1px solid hsl(var(--border));
background-color: hsl(var(--card));
border: 1px solid var(--color-border);
background-color: var(--color-card);
transition: all var(--transition-base);
}
.contact-card:hover {
border-color: hsl(var(--primary));
background-color: hsl(var(--accent));
border-color: var(--color-primary);
background-color: var(--color-accent);
}
/* Avatar styles */
@ -72,8 +72,8 @@
height: 56px;
min-width: 56px;
border-radius: var(--radius-full);
background-color: hsl(var(--primary));
color: hsl(var(--primary-foreground));
background-color: var(--color-primary);
color: var(--color-primary-foreground);
display: flex;
align-items: center;
justify-content: center;
@ -98,27 +98,27 @@
}
.btn-primary {
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
background: var(--color-primary);
color: var(--color-primary-foreground);
}
.btn-primary:hover {
background: hsl(var(--primary) / 0.9);
filter: brightness(0.9);
}
/* Input styles */
.input {
padding: var(--spacing-sm) var(--spacing-md);
border: 2px solid hsl(var(--border));
border: 2px solid var(--color-border);
border-radius: var(--radius-md);
background-color: hsl(var(--background));
color: hsl(var(--foreground));
background-color: var(--color-background);
color: var(--color-foreground);
transition: border-color var(--transition-fast);
}
.input:focus {
outline: none;
border-color: hsl(var(--primary));
border-color: var(--color-primary);
}
/* Tag styles */

View file

@ -4,12 +4,8 @@
import { contactsStore } from '$lib/stores/contacts.svelte';
import { viewModeStore } from '$lib/stores/view-mode.svelte';
import { goto } from '$app/navigation';
import ViewModeToggle from '$lib/components/ViewModeToggle.svelte';
import SortToggle, { type SortField } from '$lib/components/SortToggle.svelte';
import FilterBar, {
type ContactFilter,
type BirthdayFilter,
} from '$lib/components/FilterBar.svelte';
import type { ContactFilter, BirthdayFilter } from '$lib/components/FilterBar.svelte';
import ContactsToolbar, { type SortField } from '$lib/components/ContactsToolbar.svelte';
import ContactListView from '$lib/components/views/ContactListView.svelte';
import ContactGridView from '$lib/components/views/ContactGridView.svelte';
import ContactAlphabetView from '$lib/components/views/ContactAlphabetView.svelte';
@ -32,9 +28,6 @@
let birthdayFilter = $state<BirthdayFilter>('all');
let selectedCompany = $state<string | null>(null);
// Count favorites for quick filter button
let favoritesCount = $derived(contactsStore.contacts.filter((c) => c.isFavorite).length);
// Batch selection state
let selectionMode = $state(false);
let selectedIds = $state<Set<string>>(new Set());
@ -277,32 +270,7 @@
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between flex-wrap gap-4">
<h1 class="text-2xl font-bold text-foreground">{$_('contacts.title')}</h1>
<div class="flex items-center gap-2">
<!-- Selection Mode Toggle -->
<button
type="button"
onclick={toggleSelectionMode}
class="btn {selectionMode ? 'btn-primary' : 'btn-secondary'} flex items-center gap-2"
title={selectionMode ? 'Auswahl beenden' : 'Mehrere auswählen'}
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"
/>
</svg>
<span class="hidden sm:inline">{selectionMode ? 'Fertig' : 'Auswählen'}</span>
</button>
<a href="/contacts/new" class="btn btn-primary flex items-center gap-2">
<span>+</span>
<span>{$_('contacts.new')}</span>
</a>
</div>
</div>
<!-- Batch Actions Bar (shown when in selection mode) -->
{#if selectionMode}
@ -381,9 +349,8 @@
</div>
{/if}
<!-- Search, Filters and View Toggle -->
<div class="flex items-center gap-4 flex-wrap">
<div class="relative flex-1 min-w-[200px]">
<!-- Search Bar -->
<div class="relative">
<input
type="text"
placeholder={$_('contacts.search')}
@ -405,34 +372,16 @@
/>
</svg>
</div>
<!-- Quick Favorites Filter -->
<button
type="button"
class="favorites-quick-btn"
class:active={contactFilter === 'favorites'}
onclick={() => (contactFilter = contactFilter === 'favorites' ? 'all' : 'favorites')}
title={contactFilter === 'favorites' ? 'Alle Kontakte anzeigen' : 'Nur Favoriten anzeigen'}
>
<svg
class="w-5 h-5"
class:filled={contactFilter === 'favorites'}
fill={contactFilter === 'favorites' ? 'currentColor' : 'none'}
stroke="currentColor"
viewBox="0 0 24 24"
>
<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>
{#if favoritesCount > 0}
<span class="favorites-count">{favoritesCount}</span>
{/if}
</button>
<FilterBar
<!-- Unified Toolbar -->
<ContactsToolbar
contacts={contactsStore.contacts}
{sortField}
onSortFieldChange={(v) => (sortField = v)}
{contactFilter}
onContactFilterChange={(f) => (contactFilter = f)}
{birthdayFilter}
onBirthdayFilterChange={(f) => (birthdayFilter = f)}
{selectedTagId}
onTagChange={(id) => {
selectedTagId = id;
@ -443,16 +392,11 @@
}
contactsStore.loadContacts();
}}
{contactFilter}
onContactFilterChange={(f) => (contactFilter = f)}
{birthdayFilter}
onBirthdayFilterChange={(f) => (birthdayFilter = f)}
{selectedCompany}
onCompanyChange={(c) => (selectedCompany = c)}
{selectionMode}
onToggleSelectionMode={toggleSelectionMode}
/>
<SortToggle value={sortField} onchange={(v) => (sortField = v)} />
<ViewModeToggle />
</div>
<!-- Loading state with skeleton -->
{#if contactsStore.loading}
@ -565,57 +509,6 @@
color: hsl(var(--color-error));
}
/* Favorites Quick Filter Button */
.favorites-quick-btn {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.75rem;
background: hsl(var(--background) / 0.75);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid hsl(var(--border) / 0.5);
border-radius: 9999px;
color: hsl(var(--muted-foreground));
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.875rem;
font-weight: 500;
}
.favorites-quick-btn:hover {
color: hsl(var(--foreground));
border-color: hsl(var(--border));
}
.favorites-quick-btn.active {
color: #ef4444;
border-color: #ef4444 / 0.5;
background: hsl(0 84% 60% / 0.1);
}
.favorites-quick-btn.active:hover {
background: hsl(0 84% 60% / 0.15);
}
.favorites-count {
display: flex;
align-items: center;
justify-content: center;
min-width: 1.25rem;
height: 1.25rem;
padding: 0 0.375rem;
font-size: 0.6875rem;
font-weight: 600;
background: hsl(var(--muted));
border-radius: 9999px;
}
.favorites-quick-btn.active .favorites-count {
background: #ef4444;
color: white;
}
/* Infinite scroll */
.load-more-trigger {
height: 1px;

View file

@ -16,6 +16,8 @@
onBirthdayFilterChange: (filter: BirthdayFilter) => void;
selectedCompany: string | null;
onCompanyChange: (company: string | null) => void;
/** When embedded in a toolbar, renders as just a button without background container */
embedded?: boolean;
}
let {
@ -28,6 +30,7 @@
onBirthdayFilterChange,
selectedCompany,
onCompanyChange,
embedded = false,
}: Props = $props();
let tags = $state<ContactTag[]>([]);
@ -81,6 +84,105 @@
});
</script>
{#if embedded}
<!-- Embedded mode: just the button for use in a toolbar -->
<div class="filter-bar-embedded">
<button
type="button"
class="filter-toggle-embedded"
class:active={showFilters || activeFilterCount > 0}
onclick={() => (showFilters = !showFilters)}
title={$_('filters.title')}
>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
/>
</svg>
{#if activeFilterCount > 0}
<span class="filter-badge-embedded">{activeFilterCount}</span>
{/if}
</button>
<!-- Dropdown panel for embedded mode -->
{#if showFilters}
<div class="filter-dropdown">
<!-- Tags Filter -->
<div class="filter-section">
<label class="filter-label">{$_('filters.tag')}</label>
<select
class="filter-select"
value={selectedTagId || ''}
onchange={(e) => onTagChange(e.currentTarget.value || null)}
>
<option value="">{$_('filters.allTags')}</option>
{#each tags as tag}
<option value={tag.id}>{tag.name}</option>
{/each}
</select>
</div>
<!-- Contact Info Filter -->
<div class="filter-section">
<label class="filter-label">{$_('filters.contactInfo')}</label>
<select
class="filter-select"
value={contactFilter}
onchange={(e) => onContactFilterChange(e.currentTarget.value as ContactFilter)}
>
<option value="all">{$_('filters.contact.all')}</option>
<option value="favorites">{$_('filters.contact.favorites')}</option>
<option value="hasPhone">{$_('filters.contact.hasPhone')}</option>
<option value="hasEmail">{$_('filters.contact.hasEmail')}</option>
<option value="incomplete">{$_('filters.contact.incomplete')}</option>
</select>
</div>
<!-- Birthday Filter -->
<div class="filter-section">
<label class="filter-label">{$_('filters.birthdayLabel')}</label>
<select
class="filter-select"
value={birthdayFilter}
onchange={(e) => onBirthdayFilterChange(e.currentTarget.value as BirthdayFilter)}
>
<option value="all">{$_('filters.birthday.all')}</option>
<option value="today">{$_('filters.birthday.today')}</option>
<option value="thisWeek">{$_('filters.birthday.thisWeek')}</option>
<option value="thisMonth">{$_('filters.birthday.thisMonth')}</option>
</select>
</div>
<!-- Company Filter -->
{#if companies.length > 0}
<div class="filter-section">
<label class="filter-label">{$_('filters.company')}</label>
<select
class="filter-select"
value={selectedCompany || ''}
onchange={(e) => onCompanyChange(e.currentTarget.value || null)}
>
<option value="">{$_('filters.allCompanies')}</option>
{#each companies as company}
<option value={company}>{company}</option>
{/each}
</select>
</div>
{/if}
<!-- Clear Filters -->
{#if activeFilterCount > 0}
<button type="button" class="clear-filters-btn" onclick={clearAllFilters}>
{$_('filters.clearAll')}
</button>
{/if}
</div>
{/if}
</div>
{:else}
<div class="filter-bar">
<!-- Filter Toggle Button -->
<button
@ -243,8 +345,94 @@
</div>
{/if}
</div>
{/if}
<style>
/* Embedded mode styles */
.filter-bar-embedded {
position: relative;
display: flex;
align-items: center;
}
.filter-toggle-embedded {
display: flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
padding: 0.5rem;
background: transparent;
border: none;
border-radius: 9999px;
cursor: pointer;
color: #374151;
transition: all 0.15s ease;
}
:global(.dark) .filter-toggle-embedded {
color: #f3f4f6;
}
.filter-toggle-embedded:hover {
background: rgba(0, 0, 0, 0.05);
}
:global(.dark) .filter-toggle-embedded:hover {
background: rgba(255, 255, 255, 0.1);
}
.filter-toggle-embedded.active {
background: color-mix(in srgb, #3b82f6 15%, transparent 85%);
color: #3b82f6;
}
.filter-toggle-embedded :global(svg) {
width: 1rem;
height: 1rem;
flex-shrink: 0;
}
.filter-badge-embedded {
display: flex;
align-items: center;
justify-content: center;
min-width: 1rem;
height: 1rem;
padding: 0 0.25rem;
font-size: 0.625rem;
font-weight: 600;
color: white;
background: #3b82f6;
border-radius: 9999px;
}
.filter-dropdown {
position: absolute;
top: calc(100% + 0.5rem);
left: 50%;
transform: translateX(-50%);
min-width: 280px;
padding: 1rem;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 0.75rem;
box-shadow:
0 10px 25px -5px rgba(0, 0, 0, 0.1),
0 8px 10px -6px rgba(0, 0, 0, 0.1);
z-index: 50;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
:global(.dark) .filter-dropdown {
background: rgba(30, 30, 30, 0.95);
border-color: rgba(255, 255, 255, 0.1);
}
/* Standard mode styles */
.filter-bar {
display: flex;
align-items: center;