From 145c18d8a3832badccb2e99ba57ac98754dcf955 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 1 Apr 2026 12:27:23 -0400 Subject: [PATCH] refactor: DRY calendar additions + define --color-surface in theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Define --color-surface in theme.css (light: #f0f0f8, dark: #1a1b22) — was used across 10+ components with no-op fallbacks; now properly defined alongside --color-bg and --color-bg-card - Extract shared date formatters into utils/dateFormat.ts: fmtTime, fmtDateTime, fmtRelativeDateTime, fmtDayLabel, fmtCompact - Replace duplicate inline formatters in CalendarView, HomeView (formatUpcomingTime), and KnowledgeView (formatEventDate) - CalendarView: replace hardcoded rgba(99,102,241,0.4) hover colour with color-mix(in srgb, var(--color-primary) 40%, transparent); fix --color-input-bg fallback to use var(--color-bg); remove hardcoded hex fallbacks now that --color-surface is defined Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/assets/theme.css | 2 + frontend/src/utils/dateFormat.ts | 65 ++++++++++++++++++++++++++++ frontend/src/views/CalendarView.vue | 41 +++++------------- frontend/src/views/HomeView.vue | 23 +--------- frontend/src/views/KnowledgeView.vue | 7 +-- 5 files changed, 82 insertions(+), 56 deletions(-) create mode 100644 frontend/src/utils/dateFormat.ts diff --git a/frontend/src/assets/theme.css b/frontend/src/assets/theme.css index 7c34a2e..e5668e5 100644 --- a/frontend/src/assets/theme.css +++ b/frontend/src/assets/theme.css @@ -4,6 +4,7 @@ --color-bg: #f5f5fb; --color-bg-secondary: #ededf5; --color-bg-card: #ffffff; + --color-surface: #f0f0f8; --color-text: #1a1a1a; --color-text-secondary: #666666; --color-text-muted: #999999; @@ -55,6 +56,7 @@ --color-bg: #111113; --color-bg-secondary: #18181f; --color-bg-card: #1e1e27; + --color-surface: #1a1b22; --color-text: #e4e4f0; --color-text-secondary: #8888a8; --color-text-muted: #52526a; diff --git a/frontend/src/utils/dateFormat.ts b/frontend/src/utils/dateFormat.ts new file mode 100644 index 0000000..667a7ea --- /dev/null +++ b/frontend/src/utils/dateFormat.ts @@ -0,0 +1,65 @@ +/** Shared date/time formatting helpers used across Calendar, Home, Knowledge, etc. */ + +function _isSameDay(a: Date, b: Date): boolean { + return a.getFullYear() === b.getFullYear() && + a.getMonth() === b.getMonth() && + a.getDate() === b.getDate() +} + +/** "9:30 AM" */ +export function fmtTime(dt: string): string { + return new Date(dt).toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" }) +} + +/** "Mon, Jan 15" or "Mon, Jan 15, 9:30 AM" */ +export function fmtDateTime(dt: string, allDay: boolean): string { + const d = new Date(dt) + const datePart = d.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" }) + if (allDay) return datePart + return `${datePart}, ${d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" })}` +} + +/** + * "Today 9:30 AM" / "Tomorrow 9:30 AM" / "Mon, Jan 15 9:30 AM" + * For all-day events returns "Today" / "Tomorrow" / "Mon, Jan 15" + */ +export function fmtRelativeDateTime(dt: string, allDay: boolean): string { + try { + const d = new Date(dt) + const now = new Date() + const tomorrow = new Date(now) + tomorrow.setDate(now.getDate() + 1) + + const timeStr = allDay ? "" : ` ${d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" })}` + + if (_isSameDay(d, now)) return `Today${timeStr}` + if (_isSameDay(d, tomorrow)) return `Tomorrow${timeStr}` + return d.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" }) + timeStr + } catch { + return dt + } +} + +/** + * Label-only: "Today" / "Tomorrow" / "Mon, Jan 15" + */ +export function fmtDayLabel(dt: string): string { + try { + const d = new Date(dt) + const now = new Date() + const tomorrow = new Date(now) + tomorrow.setDate(now.getDate() + 1) + if (_isSameDay(d, now)) return "Today" + if (_isSameDay(d, tomorrow)) return "Tomorrow" + return d.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" }) + } catch { + return dt + } +} + +/** "Jan 15" or "Jan 15, 9:30 AM" — compact, no weekday */ +export function fmtCompact(dt: string, allDay: boolean): string { + const d = new Date(dt) + if (allDay) return d.toLocaleDateString(undefined, { month: "short", day: "numeric" }) + return d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) +} diff --git a/frontend/src/views/CalendarView.vue b/frontend/src/views/CalendarView.vue index 1f3160f..b1a6513 100644 --- a/frontend/src/views/CalendarView.vue +++ b/frontend/src/views/CalendarView.vue @@ -9,6 +9,7 @@ import type { DateClickArg, EventResizeDoneArg } from "@fullcalendar/interaction import { listEvents, updateEvent, type EventEntry } from "@/api/client"; import EventSlideOver from "@/components/EventSlideOver.vue"; import { useToastStore } from "@/stores/toast"; +import { fmtTime, fmtDateTime, fmtDayLabel } from "@/utils/dateFormat"; const toast = useToastStore(); const calendarRef = ref | null>(null); @@ -221,31 +222,11 @@ const calendarOptions: CalendarOptions = { height: "auto", }; -// ── Formatting helpers ───────────────────────────────────────────────────── -function fmtDate(dt: string, allDay: boolean): string { - const d = new Date(dt); - if (allDay) return d.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" }); - return d.toLocaleString(undefined, { weekday: "short", month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }); -} - -function fmtTime(dt: string): string { - return new Date(dt).toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" }); -} - -function fmtDateShort(dt: string): string { - const d = new Date(dt); - const now = new Date(); - const tomorrow = new Date(now); tomorrow.setDate(now.getDate() + 1); - if (d.toDateString() === now.toDateString()) return "Today"; - if (d.toDateString() === tomorrow.toDateString()) return "Tomorrow"; - return d.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" }); -} - // Group upcoming events by day label const upcomingGrouped = computed(() => { const groups: { label: string; date: string; events: EventEntry[] }[] = []; for (const e of upcomingEvents.value) { - const label = fmtDateShort(e.start_dt); + const label = fmtDayLabel(e.start_dt); const existing = groups.find((g) => g.label === label); if (existing) { existing.events.push(e); @@ -321,10 +302,10 @@ const upcomingGrouped = computed(() => {
{{ popover.title }}
@@ -390,7 +371,7 @@ const upcomingGrouped = computed(() => { .btn-new-event:hover { opacity: 0.88; } .fc-wrapper { - background: var(--color-surface, #1a1b1e); + background: var(--color-surface); border: 1px solid var(--color-border, #2a2b30); border-radius: var(--radius-lg, 18px); padding: 1rem; @@ -407,7 +388,7 @@ const upcomingGrouped = computed(() => { font-weight: 600; } :deep(.fc-button) { - background: var(--color-input-bg, #111113); + background: var(--color-input-bg, var(--color-bg)); border: 1px solid var(--color-border, #2a2b30); color: var(--color-text-muted, #888); font-size: 0.82rem; @@ -488,7 +469,7 @@ const upcomingGrouped = computed(() => { display: flex; align-items: stretch; gap: 0; - background: var(--color-surface, #1a1b1e); + background: var(--color-surface); border: 1px solid var(--color-border, #2a2b30); border-radius: 10px; overflow: hidden; @@ -496,8 +477,8 @@ const upcomingGrouped = computed(() => { transition: border-color 0.15s, background 0.15s; } .upcoming-card:hover { - border-color: rgba(99,102,241,0.4); - background: var(--color-bg-secondary, #16171a); + border-color: color-mix(in srgb, var(--color-primary) 40%, transparent); + background: var(--color-bg-secondary); } .upcoming-card-accent { @@ -548,7 +529,7 @@ const upcomingGrouped = computed(() => { /* ── Event popover ──────────────────────────────────────────────────────── */ .event-popover { - background: var(--color-bg-card, #1e1f24); + background: var(--color-bg-card); border: 1px solid var(--color-border, #2a2b30); border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.45); @@ -623,7 +604,7 @@ const upcomingGrouped = computed(() => { color: #fff; } .popover-btn--close { - background: var(--color-input-bg, #111113); + background: var(--color-input-bg, var(--color-bg)); color: var(--color-text-muted, #888); border: 1px solid var(--color-border, #2a2b30); } diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index ca79eda..929fff3 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -3,6 +3,7 @@ import { ref, computed, onMounted, onUnmounted } from "vue"; import { apiGet, listEvents } from "@/api/client"; import { useBackgroundRefresh } from "@/composables/useBackgroundRefresh"; import { milestoneColor } from "@/utils/palette"; +import { fmtRelativeDateTime } from "@/utils/dateFormat"; import type { Note } from "@/types/note"; import type { Task, TaskListResponse, TaskStatus } from "@/types/task"; import type { ToolCallRecord, Message } from "@/types/chat"; @@ -304,28 +305,8 @@ function onEventDeleted(id: number) { } function formatUpcomingTime(event: EventEntry): string { - if (event.all_day) return "All day"; if (!event.start_dt) return ""; - try { - const d = new Date(event.start_dt); - const today = new Date(); - const tomorrow = new Date(today); - tomorrow.setDate(today.getDate() + 1); - const isToday = - d.getFullYear() === today.getFullYear() && - d.getMonth() === today.getMonth() && - d.getDate() === today.getDate(); - const isTomorrow = - d.getFullYear() === tomorrow.getFullYear() && - d.getMonth() === tomorrow.getMonth() && - d.getDate() === tomorrow.getDate(); - const timeStr = d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" }); - if (isToday) return `Today ${timeStr}`; - if (isTomorrow) return `Tomorrow ${timeStr}`; - return d.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" }) + " " + timeStr; - } catch { - return event.start_dt; - } + return fmtRelativeDateTime(event.start_dt, event.all_day); } diff --git a/frontend/src/views/KnowledgeView.vue b/frontend/src/views/KnowledgeView.vue index 8c8c65d..eb65111 100644 --- a/frontend/src/views/KnowledgeView.vue +++ b/frontend/src/views/KnowledgeView.vue @@ -2,6 +2,7 @@ import { ref, computed, watch, onMounted, onUnmounted, nextTick } from "vue"; import { useRouter } from "vue-router"; import { apiGet, transcribeAudio } from "@/api/client"; +import { fmtCompact } from "@/utils/dateFormat"; import { useChatStore } from "@/stores/chat"; import { useSettingsStore } from "@/stores/settings"; import { useVoiceRecorder } from "@/composables/useVoiceRecorder"; @@ -118,11 +119,7 @@ async function fetchTodayBar() { } catch { /* silent */ } } -function formatEventDate(dtStr: string, allDay: boolean): string { - const d = new Date(dtStr); - if (allDay) return d.toLocaleDateString(undefined, { month: "short", day: "numeric" }); - return d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }); -} +const formatEventDate = fmtCompact // ─── Graph panel ──────────────────────────────────────────────────────────────