refactor: DRY calendar additions + define --color-surface in theme

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-04-01 12:27:23 -04:00
parent f96013a4bc
commit 145c18d8a3
5 changed files with 82 additions and 56 deletions
+2
View File
@@ -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;
+65
View File
@@ -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" })
}
+11 -30
View File
@@ -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<InstanceType<typeof FullCalendar> | 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(() => {
<div class="popover-title">{{ popover.title }}</div>
<div class="popover-time">
<template v-if="popover.all_day">
{{ fmtDate(popover.start_dt, true) }}
{{ fmtDateTime(popover.start_dt, true) }}
</template>
<template v-else>
{{ fmtDate(popover.start_dt, false) }}
{{ fmtDateTime(popover.start_dt, false) }}
<span v-if="popover.end_dt"> {{ fmtTime(popover.end_dt) }}</span>
</template>
</div>
@@ -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);
}
+2 -21
View File
@@ -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);
}
</script>
+2 -5
View File
@@ -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 ──────────────────────────────────────────────────────────────