feat: task slide-over panel, DM Sans typography, UI refinements
- TaskSlideOver: right-side panel with Next Up, Today, Upcoming, Quick Add - Dashboard: compact task trigger replaces inline tasks - Typography: DM Sans display font, antialiased rendering - Cards: subtle hover elevation, colored left accent borders - Navbar: accent-colored active states, frosted glass blur - Badges: tighter uppercase style Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,8 +5,18 @@
|
||||
import BudgetModule from '$lib/components/dashboard/BudgetModule.svelte';
|
||||
import FitnessModule from '$lib/components/dashboard/FitnessModule.svelte';
|
||||
import IssuesModule from '$lib/components/dashboard/IssuesModule.svelte';
|
||||
import TasksPanel from '$lib/components/dashboard/TasksPanel.svelte';
|
||||
import TasksModule from '$lib/components/dashboard/TasksModule.svelte';
|
||||
import TaskSlideOver from '$lib/components/dashboard/TaskSlideOver.svelte';
|
||||
|
||||
interface QuickTask {
|
||||
id: string;
|
||||
title: string;
|
||||
startDate?: string;
|
||||
dueDate?: string;
|
||||
isAllDay?: boolean;
|
||||
_projectName: string;
|
||||
projectId: string;
|
||||
_projectId: string;
|
||||
}
|
||||
|
||||
const userName = $derived((page as any).data?.user?.display_name || 'there');
|
||||
|
||||
@@ -19,6 +29,51 @@
|
||||
let fitnessCalLogged = $state(0);
|
||||
let fitnessProtein = $state(0);
|
||||
let fitnessCarbs = $state(0);
|
||||
let headerTasks = $state<QuickTask[]>([]);
|
||||
let headerOverdue = $state(0);
|
||||
let headerTotalCount = $state(0);
|
||||
let taskPanelOpen = $state(false);
|
||||
|
||||
function getGreeting(): string {
|
||||
const h = new Date().getHours();
|
||||
if (h < 12) return 'Good morning';
|
||||
if (h < 17) return 'Good afternoon';
|
||||
return 'Good evening';
|
||||
}
|
||||
|
||||
function getDateString(): string {
|
||||
return new Date().toLocaleDateString('en-US', {
|
||||
weekday: 'long', month: 'long', day: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
function formatTaskTime(t: QuickTask): string {
|
||||
const d = t.startDate || t.dueDate;
|
||||
if (!d || t.isAllDay) return '';
|
||||
try {
|
||||
const date = new Date(d);
|
||||
const h = date.getHours(), m = date.getMinutes();
|
||||
if (h === 0 && m === 0) return '';
|
||||
const ampm = h >= 12 ? 'PM' : 'AM';
|
||||
const hour = h % 12 || 12;
|
||||
return m > 0 ? `${hour}:${String(m).padStart(2, '0')} ${ampm}` : `${hour} ${ampm}`;
|
||||
} catch { return ''; }
|
||||
}
|
||||
|
||||
async function loadTasks() {
|
||||
try {
|
||||
const res = await fetch('/api/tasks/today', { credentials: 'include' });
|
||||
if (res.ok) {
|
||||
const t = await res.json();
|
||||
headerOverdue = t.overdueCount || 0;
|
||||
const today = t.today || [];
|
||||
const overdue = t.overdue || [];
|
||||
headerTotalCount = today.length + overdue.length;
|
||||
// First task for preview
|
||||
headerTasks = [...overdue, ...today].slice(0, 1);
|
||||
}
|
||||
} catch { /* silent */ }
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
const n = new Date();
|
||||
@@ -57,20 +112,52 @@
|
||||
fitnessCalRemaining = Math.max(0, (g.calories || 2000) - fitnessCalLogged);
|
||||
}
|
||||
} catch { /* silent */ }
|
||||
|
||||
await loadTasks();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="page dash-page">
|
||||
<div class="dash-layout">
|
||||
<aside class="tasks-sidebar">
|
||||
<TasksPanel />
|
||||
</aside>
|
||||
<div class="app-surface">
|
||||
<div class="page-header">
|
||||
<div class="page-subtitle">Dashboard</div>
|
||||
<div class="page-greeting">Good to see you, <strong>{userName}</strong></div>
|
||||
<div class="page">
|
||||
<div class="app-surface">
|
||||
<!-- Dashboard header with task trigger -->
|
||||
<div class="dash-header">
|
||||
<div class="dash-left">
|
||||
<div class="dash-date">{getDateString()}</div>
|
||||
<h1 class="dash-greeting">{getGreeting()}, <strong>{userName}</strong></h1>
|
||||
</div>
|
||||
<button class="task-trigger" onclick={() => taskPanelOpen = true}>
|
||||
<div class="task-trigger-icon" class:has-overdue={headerOverdue > 0}>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
|
||||
<rect x="2" y="2" width="14" height="14" rx="3"/>
|
||||
<path d="M6 9l2 2 4-4"/>
|
||||
</svg>
|
||||
{#if headerOverdue > 0}
|
||||
<span class="task-trigger-dot"></span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="task-trigger-text">
|
||||
<span class="task-trigger-count">
|
||||
{headerTotalCount} task{headerTotalCount !== 1 ? 's' : ''}
|
||||
{#if headerOverdue > 0}
|
||||
<span class="task-trigger-overdue">· {headerOverdue} overdue</span>
|
||||
{/if}
|
||||
</span>
|
||||
{#if headerTasks[0]}
|
||||
<span class="task-trigger-next">
|
||||
Next: {headerTasks[0].title}
|
||||
{#if formatTaskTime(headerTasks[0])} · {formatTaskTime(headerTasks[0])}{/if}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="task-trigger-next">All clear for today</span>
|
||||
{/if}
|
||||
</div>
|
||||
<svg class="task-trigger-arrow" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M6 4l4 4-4 4"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<TaskSlideOver bind:open={taskPanelOpen} onclose={() => { taskPanelOpen = false; loadTasks(); }} />
|
||||
|
||||
<!-- Action cards -->
|
||||
<div class="action-cards">
|
||||
<DashboardActionCard
|
||||
title="{budgetUncatCount} uncategorized transactions"
|
||||
@@ -96,8 +183,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TasksModule />
|
||||
|
||||
<!-- Modules -->
|
||||
<div class="modules-grid">
|
||||
<BudgetModule />
|
||||
<div class="right-stack">
|
||||
@@ -106,59 +192,133 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.dash-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 280px 1fr;
|
||||
gap: var(--module-gap);
|
||||
max-width: 1500px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--sp-6);
|
||||
/* ── Dashboard header ── */
|
||||
.dash-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--sp-6);
|
||||
margin-bottom: var(--section-gap);
|
||||
}
|
||||
|
||||
.tasks-sidebar {
|
||||
position: sticky;
|
||||
top: 80px;
|
||||
align-self: start;
|
||||
.dash-date {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: var(--sp-1);
|
||||
}
|
||||
|
||||
.dash-layout > :global(.app-surface) {
|
||||
padding: 0;
|
||||
max-width: none;
|
||||
.dash-greeting {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 400;
|
||||
color: var(--text-1);
|
||||
line-height: var(--leading-tight);
|
||||
letter-spacing: -0.02em;
|
||||
margin: 0;
|
||||
}
|
||||
.dash-greeting strong { font-weight: 700; }
|
||||
|
||||
/* ── Task trigger button ── */
|
||||
.task-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-3);
|
||||
padding: var(--sp-3) var(--sp-4);
|
||||
border-radius: var(--radius);
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: var(--shadow-sm);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-align: left;
|
||||
flex-shrink: 0;
|
||||
max-width: 320px;
|
||||
}
|
||||
.task-trigger:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
border-color: var(--accent-border);
|
||||
}
|
||||
.task-trigger:active { transform: scale(0.98); }
|
||||
|
||||
.task-trigger-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--accent-dim);
|
||||
color: var(--accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
.task-trigger-icon.has-overdue {
|
||||
background: var(--error-dim);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.dash-layout {
|
||||
display: block;
|
||||
padding: 0;
|
||||
}
|
||||
.tasks-sidebar {
|
||||
display: none;
|
||||
}
|
||||
.dash-layout > :global(.app-surface) {
|
||||
padding: 0 var(--sp-6);
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.task-trigger-dot {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -2px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--error);
|
||||
border: 2px solid var(--card);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dash-layout > :global(.app-surface) {
|
||||
padding: 0 var(--sp-5);
|
||||
}
|
||||
.task-trigger-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-trigger-count {
|
||||
display: block;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
color: var(--text-1);
|
||||
}
|
||||
|
||||
.task-trigger-overdue {
|
||||
color: var(--error);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.task-trigger-next {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--text-3);
|
||||
margin-top: 1px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.task-trigger-arrow {
|
||||
color: var(--text-4);
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.task-trigger:hover .task-trigger-arrow {
|
||||
transform: translateX(2px);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* ── Action cards ── */
|
||||
.action-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--section-gap);
|
||||
margin-bottom: calc(var(--section-gap) + 8px);
|
||||
padding-top: 4px;
|
||||
gap: var(--sp-3);
|
||||
margin-bottom: var(--section-gap);
|
||||
}
|
||||
|
||||
/* ── Modules grid ── */
|
||||
.modules-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.3fr 0.7fr;
|
||||
@@ -172,6 +332,21 @@
|
||||
gap: var(--module-gap);
|
||||
}
|
||||
|
||||
/* ── Mobile ── */
|
||||
@media (max-width: 900px) {
|
||||
.dash-header {
|
||||
flex-direction: column;
|
||||
gap: var(--sp-3);
|
||||
}
|
||||
.dash-greeting {
|
||||
font-size: var(--text-xl);
|
||||
}
|
||||
.task-trigger {
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.modules-grid {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
Reference in New Issue
Block a user