diff --git a/frontend-v2/src/app.css b/frontend-v2/src/app.css index 3974661..74465c2 100644 --- a/frontend-v2/src/app.css +++ b/frontend-v2/src/app.css @@ -16,9 +16,9 @@ body { padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom); } :root { /* ── Fonts ── */ - --font: 'Inter', -apple-system, system-ui, sans-serif; + --font: 'DM Sans', -apple-system, system-ui, sans-serif; --mono: 'JetBrains Mono', ui-monospace, monospace; - --transition: 180ms ease; + --transition: 150ms ease; /* ── Spacing scale (4px grid) ── * Use these everywhere: padding, margin, gap. @@ -106,16 +106,16 @@ /* ── LIGHT MODE ── */ :root { /* Surface hierarchy: canvas (page bg) → surface (sidebars, panels) → card (content containers) */ - --canvas: #F5F6F8; + --canvas: #F4F5F7; --surface: #FFFFFF; - --surface-secondary: #FAFAFB; + --surface-secondary: #F8F9FA; --card: #FFFFFF; - --card-secondary: #FAFAFB; - --card-hover: #f0f0f3; + --card-secondary: #F8F9FA; + --card-hover: #F0F1F4; /* Borders */ - --border: rgba(0,0,0,0.06); - --border-strong: rgba(0,0,0,0.1); + --border: rgba(0,0,0,0.07); + --border-strong: rgba(0,0,0,0.12); /* Text hierarchy: 1 (headings/names) → 2 (body) → 3 (labels/meta) → 4 (placeholder/disabled) */ --text-1: #1a1a1f; @@ -202,6 +202,9 @@ background: var(--canvas); color: var(--text-1); min-height: 100vh; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; transition: background var(--transition), color var(--transition); -webkit-font-smoothing: antialiased; } @@ -306,18 +309,12 @@ border: 1px solid var(--border); box-shadow: var(--shadow-sm); padding: var(--card-pad); - transition: box-shadow 0.2s ease; -} -.module:hover { - box-shadow: var(--shadow-md); + transition: box-shadow 0.25s ease, transform 0.25s ease; } .module.primary { padding: var(--card-pad-primary); box-shadow: var(--shadow-md); } -.module.primary:hover { - box-shadow: var(--shadow-lg); -} .module.flush { padding: 0; } .module-header { @@ -328,11 +325,11 @@ } .module-title { - font-size: var(--text-sm); - font-weight: 600; + font-size: 11px; + font-weight: 700; color: var(--text-3); text-transform: uppercase; - letter-spacing: 0.06em; + letter-spacing: 0.1em; } .module-action { @@ -353,22 +350,22 @@ align-items: center; gap: var(--inner-gap); padding: var(--row-pad-y) var(--row-pad-x); - transition: background var(--transition); + transition: background 0.15s ease; } .data-row:hover { background: var(--card-hover); } .data-row + .data-row { border-top: 1px solid var(--border); } -.data-row:nth-child(even) { background: color-mix(in srgb, var(--surface) 68%, var(--card)); } -.data-row:nth-child(even):hover { background: var(--card-hover); } /* ── Badges ── * Semantic status badges. Variants: error, success, warning, accent, muted. */ .badge { - font-size: var(--text-xs); - font-weight: 500; - padding: 3px 10px; - border-radius: var(--radius-sm); + font-size: 10px; + font-weight: 600; + padding: 2px 8px; + border-radius: var(--radius-xs); flex-shrink: 0; + letter-spacing: 0.02em; + text-transform: uppercase; } .badge.error { background: var(--error-dim); color: var(--error); } .badge.success { background: var(--success-dim); color: var(--success); } @@ -412,12 +409,12 @@ * Uppercase label above a group of content. */ .section-label { - font-size: var(--text-xs); + font-size: 11px; font-weight: 600; text-transform: uppercase; - letter-spacing: 0.06em; + letter-spacing: 0.08em; color: var(--text-3); - margin-bottom: var(--sp-1.5); + margin-bottom: var(--sp-2); } /* ── Page wrapper ── */ @@ -427,24 +424,25 @@ font-size: var(--text-xl); font-weight: 600; color: var(--text-1); - letter-spacing: -0.01em; + letter-spacing: -0.02em; line-height: var(--leading-tight); } .page-subtitle { - font-size: var(--text-sm); - font-weight: 500; - color: var(--text-3); + font-size: 11px; + font-weight: 600; + color: var(--accent); text-transform: uppercase; - letter-spacing: 0.05em; + letter-spacing: 0.08em; margin-bottom: var(--sp-1); } .page-greeting { font-size: var(--text-2xl); - font-weight: 300; + font-weight: 400; color: var(--text-1); line-height: var(--leading-tight); + letter-spacing: -0.02em; } -.page-greeting strong { font-weight: 600; } +.page-greeting strong { font-weight: 700; } /* ── App surface (centered container) ── */ .app-surface { diff --git a/frontend-v2/src/app.html b/frontend-v2/src/app.html index f0ea0b5..6b07520 100644 --- a/frontend-v2/src/app.html +++ b/frontend-v2/src/app.html @@ -3,6 +3,9 @@ + + + %sveltekit.head% diff --git a/frontend-v2/src/lib/components/dashboard/TaskSlideOver.svelte b/frontend-v2/src/lib/components/dashboard/TaskSlideOver.svelte new file mode 100644 index 0000000..bfae630 --- /dev/null +++ b/frontend-v2/src/lib/components/dashboard/TaskSlideOver.svelte @@ -0,0 +1,552 @@ + + + + +{#if open} + +
{ if (e.target === e.currentTarget) onclose(); }} onkeydown={() => {}}> +
+ + +
+
+
Tasks
+
+ {#if overdueTasks.length > 0} + {overdueTasks.length} overdue + · + {/if} + {todayTasks.length} today +
+
+
+ Manage + +
+
+ + +
+ {#if loading} +
+
+
+
+
+ {:else} + + + {@const nextTask = overdueTasks[0] || todayTasks[0]} + {#if nextTask} +
+ +
+ +
+
{nextTask.title}
+
+ {#if formatTime(nextTask)}{formatTime(nextTask)} · {/if}{nextTask._projectName} + {#if isOverdue(nextTask)} + {formatDueLabel(nextTask)} + {/if} +
+
+
+
+ {/if} + + + {@const restToday = nextTask ? [...overdueTasks.slice(isOverdue(nextTask) ? 1 : 0), ...todayTasks.slice(isOverdue(nextTask) ? 0 : 1)] : []} + {#if restToday.length > 0} +
+ + {#each restToday as task (task.id)} +
+ + {task.title} + + {#if formatTime(task)}{formatTime(task)}{:else}{task._projectName}{/if} + +
+ {/each} +
+ {/if} + + + {#if upcomingTasks.length > 0} +
+ + {#each upcomingTasks as task (task.id)} +
+ + {task.title} + {formatDueLabel(task)} +
+ {/each} +
+ {/if} + + + {#if !nextTask && restToday.length === 0 && upcomingTasks.length === 0} +
All clear. Nothing on the agenda.
+ {/if} + + {/if} +
+ + + + +
+
+{/if} + + diff --git a/frontend-v2/src/lib/components/layout/Navbar.svelte b/frontend-v2/src/lib/components/layout/Navbar.svelte index 7112eef..d915805 100644 --- a/frontend-v2/src/lib/components/layout/Navbar.svelte +++ b/frontend-v2/src/lib/components/layout/Navbar.svelte @@ -93,8 +93,8 @@ z-index: 50; background: var(--nav-bg); border-bottom: 1px solid var(--border); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); + backdrop-filter: blur(20px) saturate(1.4); + -webkit-backdrop-filter: blur(20px) saturate(1.4); } .navbar-inner { max-width: 1400px; @@ -106,15 +106,16 @@ gap: var(--sp-5); } .navbar-logo { - font-weight: 600; + font-weight: 700; font-size: var(--text-md); display: flex; align-items: center; gap: var(--sp-2); color: var(--text-1); flex-shrink: 0; + letter-spacing: -0.02em; } - .navbar-logo svg { width: 20px; height: 20px; } + .navbar-logo svg { width: 18px; height: 18px; color: var(--accent); } .navbar-links { display: flex; diff --git a/frontend-v2/src/routes/(app)/+page.svelte b/frontend-v2/src/routes/(app)/+page.svelte index e287b3c..9938f7a 100644 --- a/frontend-v2/src/routes/(app)/+page.svelte +++ b/frontend-v2/src/routes/(app)/+page.svelte @@ -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([]); + 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(); }); -
-
- -
-