feat(redesign): collapsible dashboard sections + glass mobile-more sheet
Generalize dashboard panel expansion: Stream / On Watch / Pulse / Wires each get a chevron toggle persisted in localStorage under dashboard_section_state (migrates legacy dashboard_chart_visible). Drop the redundant inner border on EventChart so it doesn't double-frame the panel. Mobile "more" sheet becomes full-height with translucent glass (rgba(...,0.72) + 28px blur) instead of nearly-solid.
This commit is contained in:
@@ -143,16 +143,12 @@
|
||||
|
||||
<style>
|
||||
.chart-wrapper {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.chart-wrapper:hover {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 16px var(--color-glow);
|
||||
/* Outer chrome lives on the parent panel — keep this transparent so
|
||||
we don't get a double border / nested card look. */
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.chart-header {
|
||||
display: flex;
|
||||
|
||||
@@ -547,7 +547,7 @@
|
||||
{#if mobileMoreOpen}
|
||||
<div class="mobile-more-backdrop" style="position: fixed; inset: 0; z-index: 49; background: rgba(0,0,0,0.4); backdrop-filter: blur(2px);"
|
||||
onclick={closeMobileMore} role="presentation"></div>
|
||||
<div class="mobile-more-panel" style="position: fixed; bottom: calc(3rem + env(safe-area-inset-bottom, 0px)); left: 0; right: 0; z-index: 50; background: var(--color-sidebar); border-top: 1px solid var(--color-border); border-radius: 1rem 1rem 0 0; padding: 1rem; max-height: calc(70vh - env(safe-area-inset-bottom, 0px)); overflow-y: auto;"
|
||||
<div class="mobile-more-panel"
|
||||
transition:slide={{ duration: 200, easing: cubicOut }}>
|
||||
{#if allProviders.length >= 1}
|
||||
<div class="mb-3 pb-3" style="border-bottom: 1px solid var(--color-border);">
|
||||
@@ -1066,6 +1066,22 @@
|
||||
border-right: 1px solid var(--color-border);
|
||||
}
|
||||
.mobile-nav { display: flex !important; }
|
||||
.mobile-more-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: calc(3rem + env(safe-area-inset-bottom, 0px));
|
||||
z-index: 50;
|
||||
background: var(--mobile-more-bg, rgba(19, 21, 32, 0.72));
|
||||
backdrop-filter: blur(28px) saturate(170%);
|
||||
-webkit-backdrop-filter: blur(28px) saturate(170%);
|
||||
border-top: 1px solid var(--color-rule-strong);
|
||||
padding: calc(1rem + env(safe-area-inset-top, 0px)) calc(1rem + env(safe-area-inset-right, 0px)) 1rem calc(1rem + env(safe-area-inset-left, 0px));
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
:global([data-theme="light"]) .mobile-more-panel { --mobile-more-bg: rgba(250, 250, 254, 0.72); }
|
||||
.mobile-more-panel a:hover,
|
||||
.mobile-more-panel button:hover {
|
||||
background: var(--color-glass-strong);
|
||||
|
||||
@@ -21,11 +21,26 @@
|
||||
|
||||
import type { DashboardStatus } from '$lib/types';
|
||||
|
||||
const CHART_KEY = 'dashboard_chart_visible';
|
||||
let chartVisible = $state(typeof localStorage !== 'undefined' ? localStorage.getItem(CHART_KEY) !== 'false' : true);
|
||||
function toggleChart() {
|
||||
chartVisible = !chartVisible;
|
||||
if (typeof localStorage !== 'undefined') localStorage.setItem(CHART_KEY, String(chartVisible));
|
||||
const SECTIONS_KEY = 'dashboard_section_state';
|
||||
type SectionKey = 'stream' | 'on_watch' | 'pulse' | 'wires';
|
||||
function loadSectionState(): Record<SectionKey, boolean> {
|
||||
const defaults: Record<SectionKey, boolean> = { stream: true, on_watch: true, pulse: true, wires: true };
|
||||
if (typeof localStorage === 'undefined') return defaults;
|
||||
try {
|
||||
const raw = localStorage.getItem(SECTIONS_KEY);
|
||||
if (raw) return { ...defaults, ...JSON.parse(raw) };
|
||||
// Migrate legacy single-key for the chart
|
||||
const legacy = localStorage.getItem('dashboard_chart_visible');
|
||||
if (legacy !== null) defaults.pulse = legacy !== 'false';
|
||||
} catch { /* ignore */ }
|
||||
return defaults;
|
||||
}
|
||||
let sectionExpanded = $state<Record<SectionKey, boolean>>(loadSectionState());
|
||||
function toggleSection(key: SectionKey) {
|
||||
sectionExpanded = { ...sectionExpanded, [key]: !sectionExpanded[key] };
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem(SECTIONS_KEY, JSON.stringify(sectionExpanded));
|
||||
}
|
||||
}
|
||||
|
||||
let status = $state<DashboardStatus | null>(null);
|
||||
@@ -463,16 +478,25 @@
|
||||
<b>{status.total_events}</b> {t('dashboard.eventsLabel')}
|
||||
</p>
|
||||
</div>
|
||||
{#if status.total_events > 0}
|
||||
<button type="button" onclick={() => confirmClearEvents = true}
|
||||
class="clear-events-btn flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg transition-colors"
|
||||
title={t('dashboard.clearEvents')}>
|
||||
<MdiIcon name="mdiTrashCanOutline" size={14} />
|
||||
<span class="hidden sm:inline">{t('dashboard.clearEvents')}</span>
|
||||
<div class="panel-head-actions">
|
||||
{#if status.total_events > 0}
|
||||
<button type="button" onclick={() => confirmClearEvents = true}
|
||||
class="clear-events-btn flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg transition-colors"
|
||||
title={t('dashboard.clearEvents')}>
|
||||
<MdiIcon name="mdiTrashCanOutline" size={14} />
|
||||
<span class="hidden sm:inline">{t('dashboard.clearEvents')}</span>
|
||||
</button>
|
||||
{/if}
|
||||
<button type="button" onclick={() => toggleSection('stream')}
|
||||
class="ghost-icon-btn"
|
||||
title={sectionExpanded.stream ? t('common.hide') : t('common.show')}>
|
||||
<NavIcon name={sectionExpanded.stream ? 'mdiChevronDown' : 'mdiChevronRight'} size={16} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{#if sectionExpanded.stream}
|
||||
<div in:slide={{ duration: 200 }}>
|
||||
<div class="panel-filters">
|
||||
<div class="flex-1 min-w-[150px] max-w-[280px]">
|
||||
<input type="text" bind:value={filterSearch} oninput={onSearchInput}
|
||||
@@ -565,6 +589,8 @@
|
||||
{@render paginator()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<!-- On watch — provider deck. Hidden when a global provider filter is
|
||||
@@ -576,8 +602,15 @@
|
||||
<h2 class="panel-title">{t('dashboard.onWatchTitle')} <em>{t('dashboard.onWatchEmphasis')}</em></h2>
|
||||
<p class="panel-meta"><b>{providerDeck.length}</b> {t('dashboard.providersShort')}</p>
|
||||
</div>
|
||||
<button type="button" onclick={() => toggleSection('on_watch')}
|
||||
class="ghost-icon-btn"
|
||||
title={sectionExpanded.on_watch ? t('common.hide') : t('common.show')}>
|
||||
<NavIcon name={sectionExpanded.on_watch ? 'mdiChevronDown' : 'mdiChevronRight'} size={16} />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{#if sectionExpanded.on_watch}
|
||||
<div in:slide={{ duration: 200 }}>
|
||||
{#if providerDeck.length === 0}
|
||||
<div class="empty-state">
|
||||
<MdiIcon name="mdiServerNetwork" size={32} />
|
||||
@@ -617,6 +650,8 @@
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -628,13 +663,13 @@
|
||||
<h2 class="panel-title">{t('dashboard.pulseTitle')} <em>{t('dashboard.pulseEmphasis')}</em></h2>
|
||||
<p class="panel-meta">{t('dashboard.pulseSub')}</p>
|
||||
</div>
|
||||
<button type="button" onclick={toggleChart}
|
||||
<button type="button" onclick={() => toggleSection('pulse')}
|
||||
class="ghost-icon-btn"
|
||||
title={chartVisible ? t('common.hide') : t('common.show')}>
|
||||
<NavIcon name={chartVisible ? 'mdiChevronDown' : 'mdiChevronRight'} size={16} />
|
||||
title={sectionExpanded.pulse ? t('common.hide') : t('common.show')}>
|
||||
<NavIcon name={sectionExpanded.pulse ? 'mdiChevronDown' : 'mdiChevronRight'} size={16} />
|
||||
</button>
|
||||
</header>
|
||||
{#if chartVisible}
|
||||
{#if sectionExpanded.pulse}
|
||||
<div class="panel-body" in:slide={{ duration: 200 }}>
|
||||
<EventChart days={chartDays} />
|
||||
</div>
|
||||
@@ -649,8 +684,14 @@
|
||||
<h2 class="panel-title">{t('dashboard.wiresTitle')} <em>{t('dashboard.wiresEmphasis')}</em></h2>
|
||||
<p class="panel-meta"><b>{activeWires.length}</b> {t('dashboard.wiresSub')}</p>
|
||||
</div>
|
||||
<button type="button" onclick={() => toggleSection('wires')}
|
||||
class="ghost-icon-btn"
|
||||
title={sectionExpanded.wires ? t('common.hide') : t('common.show')}>
|
||||
<NavIcon name={sectionExpanded.wires ? 'mdiChevronDown' : 'mdiChevronRight'} size={16} />
|
||||
</button>
|
||||
</header>
|
||||
<div class="wires-body">
|
||||
{#if sectionExpanded.wires}
|
||||
<div class="wires-body" in:slide={{ duration: 200 }}>
|
||||
{#each activeWires as wire}
|
||||
<div class="wire">
|
||||
<div class="wire-from">
|
||||
@@ -673,6 +714,7 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
@@ -936,6 +978,10 @@
|
||||
padding: 1.25rem 1.5rem 0.85rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
.panel-head-actions {
|
||||
display: flex; align-items: center; gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.panel-title {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 400;
|
||||
|
||||
Reference in New Issue
Block a user