Merge pull request #713 from mauriceboe/feat/dashboard-unified-toolbar

feat(dashboard): unify desktop header with planner toolbar
This commit is contained in:
Maurice
2026-04-18 01:38:50 +02:00
committed by GitHub
4 changed files with 147 additions and 108 deletions
+6 -15
View File
@@ -416,15 +416,10 @@ describe('DashboardPage', () => {
expect(screen.getAllByText('Paris Adventure').length).toBeGreaterThan(0); expect(screen.getAllByText('Paris Adventure').length).toBeGreaterThan(0);
}); });
// Find settings button — it's the gear icon button without title or text // Find settings button — the gear icon button (icon-only, no visible label)
const allBtns = screen.getAllByRole('button'); const allBtns = screen.getAllByRole('button');
const settingsButton = allBtns.find( const settingsButton = allBtns.find(btn =>
btn => { btn.querySelector('.lucide-settings') && !btn.textContent?.trim()
const title = btn.getAttribute('title');
const text = btn.textContent?.trim() || '';
// Settings gear: no title, no meaningful text, not the notification bell
return !title && !text && btn.querySelector('.lucide-settings');
}
); );
expect(settingsButton).toBeDefined(); expect(settingsButton).toBeDefined();
@@ -646,14 +641,10 @@ describe('DashboardPage', () => {
expect(screen.getAllByText('Paris Adventure').length).toBeGreaterThan(0); expect(screen.getAllByText('Paris Adventure').length).toBeGreaterThan(0);
}); });
// Open widget settings // Open widget settings — gear icon button (icon-only, no visible label)
const allBtns = screen.getAllByRole('button'); const allBtns = screen.getAllByRole('button');
const settingsButton = allBtns.find( const settingsButton = allBtns.find(btn =>
btn => { btn.querySelector('.lucide-settings') && !btn.textContent?.trim()
const title = btn.getAttribute('title');
const text = btn.textContent?.trim() || '';
return !title && !text && btn.querySelector('.lucide-settings');
}
); );
expect(settingsButton).toBeDefined(); expect(settingsButton).toBeDefined();
+67 -52
View File
@@ -897,61 +897,76 @@ export default function DashboardPage(): React.ReactElement {
</button> </button>
</div> </div>
{/* Desktop header */} {/* Desktop header — unified toolbar */}
<div className="hidden md:flex" style={{ alignItems: 'center', justifyContent: 'space-between', marginBottom: 28 }}> <div className="hidden md:block" style={{ marginBottom: 20 }}>
<div> <div style={{
<h1 style={{ margin: 0, fontSize: 24, fontWeight: 800, color: 'var(--text-primary)' }}>{t('dashboard.title')}</h1> background: 'var(--bg-tertiary)', borderRadius: 18,
<p style={{ margin: '3px 0 0', fontSize: 13, color: '#9ca3af' }}> border: '1px solid var(--border-primary)',
boxShadow: '0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04)',
padding: '14px 16px 14px 22px',
display: 'flex', alignItems: 'center', gap: 16, flexWrap: 'wrap',
}}>
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600, color: 'var(--text-primary)', letterSpacing: '-0.01em', flexShrink: 0 }}>
{t('dashboard.title')}
</h2>
<div style={{ width: 1, height: 22, background: 'var(--border-faint)', flexShrink: 0 }} />
<span style={{ fontSize: 13, color: 'var(--text-muted)' }}>
{isLoading ? t('common.loading') {isLoading ? t('common.loading')
: trips.length > 0 ? `${t(trips.length !== 1 ? 'dashboard.subtitle.activeMany' : 'dashboard.subtitle.activeOne', { count: trips.length })}${archivedTrips.length > 0 ? t('dashboard.subtitle.archivedSuffix', { count: archivedTrips.length }) : ''}` : trips.length > 0 ? `${t(trips.length !== 1 ? 'dashboard.subtitle.activeMany' : 'dashboard.subtitle.activeOne', { count: trips.length })}${archivedTrips.length > 0 ? t('dashboard.subtitle.archivedSuffix', { count: archivedTrips.length }) : ''}`
: t('dashboard.subtitle.empty')} : t('dashboard.subtitle.empty')}
</p> </span>
</div>
<div style={{ display: 'flex', gap: 8, alignItems: 'stretch' }}> <div style={{ display: 'inline-flex', gap: 6, alignItems: 'center', marginLeft: 'auto', flexShrink: 0 }}>
{/* View mode toggle */} <button
<button onClick={toggleViewMode}
onClick={toggleViewMode} title={viewMode === 'grid' ? t('dashboard.listView') : t('dashboard.gridView')}
title={viewMode === 'grid' ? t('dashboard.listView') : t('dashboard.gridView')} style={{
style={{ appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
padding: '0 14px', height: 37, padding: '7px 11px', borderRadius: 99,
background: 'var(--bg-card)', border: '1px solid var(--border-primary)', borderRadius: 12, background: 'transparent', color: 'var(--text-muted)',
cursor: 'pointer', color: 'var(--text-faint)', fontFamily: 'inherit', transition: 'all 0.15s ease',
transition: 'background 0.15s, border-color 0.15s', }}
}} onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg-card)'; e.currentTarget.style.color = 'var(--text-primary)' }}
onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg-hover)'; e.currentTarget.style.borderColor = 'var(--text-faint)' }} onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--text-muted)' }}
onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-card)'; e.currentTarget.style.borderColor = 'var(--border-primary)' }} >
> {viewMode === 'grid' ? <List size={15} /> : <LayoutGrid size={15} />}
{viewMode === 'grid' ? <List size={15} /> : <LayoutGrid size={15} />} </button>
</button> <button
{/* Widget settings */} onClick={() => setShowWidgetSettings(s => s ? false : true)}
<button title={t('dashboard.widgets') || 'Widgets'}
onClick={() => setShowWidgetSettings(s => s ? false : true)} style={{
style={{ appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
padding: '0 14px', height: 37, padding: '7px 11px', borderRadius: 99,
background: 'var(--bg-card)', border: '1px solid var(--border-primary)', borderRadius: 12, background: showWidgetSettings ? 'var(--bg-card)' : 'transparent',
cursor: 'pointer', color: 'var(--text-faint)', fontFamily: 'inherit', color: showWidgetSettings ? 'var(--text-primary)' : 'var(--text-muted)',
transition: 'background 0.15s, border-color 0.15s', boxShadow: showWidgetSettings ? '0 1px 2px rgba(0,0,0,0.06)' : 'none',
}} transition: 'all 0.15s ease',
onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg-hover)'; e.currentTarget.style.borderColor = 'var(--text-faint)' }} }}
onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-card)'; e.currentTarget.style.borderColor = 'var(--border-primary)' }} onMouseEnter={e => { if (!showWidgetSettings) { e.currentTarget.style.background = 'var(--bg-card)'; e.currentTarget.style.color = 'var(--text-primary)' } }}
> onMouseLeave={e => { if (!showWidgetSettings) { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--text-muted)' } }}
<Settings size={15} /> >
</button> <Settings size={15} />
{can('trip_create') && <button </button>
onClick={() => { setEditingTrip(null); setShowForm(true) }} {can('trip_create') && (
style={{ <button
display: 'flex', alignItems: 'center', gap: 7, padding: '9px 18px', onClick={() => { setEditingTrip(null); setShowForm(true) }}
background: 'var(--accent)', color: 'var(--accent-text)', border: 'none', borderRadius: 12, style={{
fontSize: 13, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit', appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
boxShadow: '0 2px 8px rgba(0,0,0,0.15)', display: 'inline-flex', alignItems: 'center', gap: 6,
}} padding: '9px 14px', borderRadius: 10, fontSize: 13, fontWeight: 500,
onMouseEnter={e => e.currentTarget.style.opacity = '0.85'} background: 'var(--accent)', color: 'var(--accent-text)', flexShrink: 0,
onMouseLeave={e => e.currentTarget.style.opacity = '1'} marginLeft: 2,
> transition: 'opacity 0.15s ease',
<Plus size={15} /> {t('dashboard.newTrip')} }}
</button>} onMouseEnter={e => e.currentTarget.style.opacity = '0.88'}
onMouseLeave={e => e.currentTarget.style.opacity = '1'}
>
<Plus size={14} strokeWidth={2.5} /> {t('dashboard.newTrip')}
</button>
)}
</div>
</div> </div>
</div> </div>
+35 -33
View File
@@ -150,39 +150,41 @@ export default function JourneyPage() {
)} )}
</div> </div>
{/* Header — desktop */} {/* Header — desktop (unified toolbar) */}
<div className="hidden md:flex items-start justify-between px-8 pt-10 pb-7"> <div className="hidden md:block px-8 pt-10 pb-7">
<div> <div style={{
<h1 className="text-[32px] font-extrabold tracking-[-0.025em] text-zinc-900 dark:text-white leading-none">{t('journey.title')}</h1> background: 'var(--bg-tertiary)', borderRadius: 18,
<p className="text-[13px] text-zinc-500 mt-1.5">{t("journey.frontpage.subtitle")}</p> border: '1px solid var(--border-primary)',
</div> boxShadow: '0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04)',
<div className="flex items-center gap-2"> padding: '14px 16px 14px 22px',
{searchOpen && ( display: 'flex', alignItems: 'center', gap: 16, flexWrap: 'wrap',
<input }}>
value={searchQuery} <h2 style={{ margin: 0, fontSize: 18, fontWeight: 600, color: 'var(--text-primary)', letterSpacing: '-0.01em', flexShrink: 0 }}>
onChange={e => setSearchQuery(e.target.value)} {t('journey.title')}
onKeyDown={e => { if (e.key === 'Escape') { setSearchQuery(''); setSearchOpen(false) } }} </h2>
placeholder={t('journey.search.placeholder')} <div style={{ width: 1, height: 22, background: 'var(--border-faint)', flexShrink: 0 }} />
autoFocus <span style={{ fontSize: 13, color: 'var(--text-muted)' }}>
className="w-52 px-3 py-2 border border-zinc-200 dark:border-zinc-700 rounded-[10px] text-[13px] bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white focus:border-zinc-400 focus:outline-none" {t('journey.frontpage.subtitle')}
/> </span>
)}
<button <div style={{ display: 'inline-flex', gap: 6, alignItems: 'center', marginLeft: 'auto', flexShrink: 0 }}>
onClick={() => { <button
setSearchOpen(s => !s) onClick={() => openCreateModal()}
if (searchOpen) setSearchQuery('') style={{
}} appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
className={`w-9 h-9 rounded-[10px] border border-zinc-200 dark:border-zinc-700 flex items-center justify-center text-zinc-500 transition-colors ${searchOpen ? 'bg-zinc-100 dark:bg-zinc-700' : 'bg-white dark:bg-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-700'}`} display: 'inline-flex', alignItems: 'center', gap: 6,
> padding: '9px 14px', borderRadius: 10, fontSize: 13, fontWeight: 500,
{searchOpen ? <X size={15} /> : <Search size={15} />} background: 'var(--accent)', color: 'var(--accent-text)', flexShrink: 0,
</button> marginLeft: 2,
<button transition: 'opacity 0.15s ease',
onClick={() => openCreateModal()} }}
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-[10px] bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 text-[13px] font-medium hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-all hover:-translate-y-px" onMouseEnter={e => e.currentTarget.style.opacity = '0.88'}
> onMouseLeave={e => e.currentTarget.style.opacity = '1'}
<Plus size={14} /> >
{t('journey.frontpage.createJourney')} <Plus size={14} strokeWidth={2.5} />
</button> {t('journey.frontpage.createJourney')}
</button>
</div>
</div> </div>
</div> </div>
+39 -8
View File
@@ -138,19 +138,15 @@ export default function VacayPage(): React.ReactElement {
<div style={{ paddingTop: 'var(--nav-h)' }}> <div style={{ paddingTop: 'var(--nav-h)' }}>
<div className="max-w-[1800px] mx-auto px-3 sm:px-4 py-4 sm:py-6"> <div className="max-w-[1800px] mx-auto px-3 sm:px-4 py-4 sm:py-6">
{/* Header */} {/* Mobile + tablet header (filter toggle lives here) */}
<div className="flex items-center justify-between mb-4 sm:mb-5"> <div className="lg:hidden flex items-center justify-between mb-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-xl flex items-center justify-center" style={{ background: 'var(--bg-secondary)' }}> <div className="w-9 h-9 rounded-xl flex items-center justify-center" style={{ background: 'var(--bg-secondary)' }}>
<CalendarDays size={18} style={{ color: 'var(--text-primary)' }} /> <CalendarDays size={18} style={{ color: 'var(--text-primary)' }} />
</div> </div>
<div> <h1 className="text-lg font-bold" style={{ color: 'var(--text-primary)' }}>{t('admin.addons.catalog.vacay.name')}</h1>
<h1 className="text-lg sm:text-xl font-bold" style={{ color: 'var(--text-primary)' }}>{t('admin.addons.catalog.vacay.name')}</h1>
<p className="text-xs hidden sm:block" style={{ color: 'var(--text-muted)' }}>{t('vacay.subtitle')}</p>
</div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{/* Mobile sidebar toggle */}
<button <button
onClick={() => setShowMobileSidebar(true)} onClick={() => setShowMobileSidebar(true)}
className="lg:hidden flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm transition-colors" className="lg:hidden flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm transition-colors"
@@ -164,11 +160,46 @@ export default function VacayPage(): React.ReactElement {
style={{ background: 'var(--bg-secondary)', color: 'var(--text-muted)' }} style={{ background: 'var(--bg-secondary)', color: 'var(--text-muted)' }}
> >
<Settings size={14} /> <Settings size={14} />
<span className="hidden sm:inline">{t('vacay.settings')}</span>
</button> </button>
</div> </div>
</div> </div>
{/* Desktop header — unified toolbar (sidebar is always visible at this width) */}
<div className="hidden lg:block" style={{ marginBottom: 20 }}>
<div style={{
background: 'var(--bg-tertiary)', borderRadius: 18,
border: '1px solid var(--border-primary)',
boxShadow: '0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04)',
padding: '14px 16px 14px 22px',
display: 'flex', alignItems: 'center', gap: 16, flexWrap: 'wrap',
}}>
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600, color: 'var(--text-primary)', letterSpacing: '-0.01em', flexShrink: 0 }}>
{t('admin.addons.catalog.vacay.name')}
</h2>
<div style={{ width: 1, height: 22, background: 'var(--border-faint)', flexShrink: 0 }} />
<span style={{ fontSize: 13, color: 'var(--text-muted)' }}>
{t('vacay.subtitle')}
</span>
<div style={{ display: 'inline-flex', gap: 6, alignItems: 'center', marginLeft: 'auto', flexShrink: 0 }}>
<button
onClick={() => setShowSettings(true)}
style={{
appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
display: 'inline-flex', alignItems: 'center', gap: 6,
padding: '9px 14px', borderRadius: 10, fontSize: 13, fontWeight: 500,
background: 'var(--accent)', color: 'var(--accent-text)', flexShrink: 0,
marginLeft: 2,
transition: 'opacity 0.15s ease',
}}
onMouseEnter={e => e.currentTarget.style.opacity = '0.88'}
onMouseLeave={e => e.currentTarget.style.opacity = '1'}
>
<Settings size={14} strokeWidth={2.5} /> {t('vacay.settings')}
</button>
</div>
</div>
</div>
{/* Main layout */} {/* Main layout */}
<div className="flex gap-4 items-start"> <div className="flex gap-4 items-start">
{/* Desktop Sidebar */} {/* Desktop Sidebar */}