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:
@@ -4,6 +4,7 @@
|
|||||||
--color-bg: #f5f5fb;
|
--color-bg: #f5f5fb;
|
||||||
--color-bg-secondary: #ededf5;
|
--color-bg-secondary: #ededf5;
|
||||||
--color-bg-card: #ffffff;
|
--color-bg-card: #ffffff;
|
||||||
|
--color-surface: #f0f0f8;
|
||||||
--color-text: #1a1a1a;
|
--color-text: #1a1a1a;
|
||||||
--color-text-secondary: #666666;
|
--color-text-secondary: #666666;
|
||||||
--color-text-muted: #999999;
|
--color-text-muted: #999999;
|
||||||
@@ -55,6 +56,7 @@
|
|||||||
--color-bg: #111113;
|
--color-bg: #111113;
|
||||||
--color-bg-secondary: #18181f;
|
--color-bg-secondary: #18181f;
|
||||||
--color-bg-card: #1e1e27;
|
--color-bg-card: #1e1e27;
|
||||||
|
--color-surface: #1a1b22;
|
||||||
--color-text: #e4e4f0;
|
--color-text: #e4e4f0;
|
||||||
--color-text-secondary: #8888a8;
|
--color-text-secondary: #8888a8;
|
||||||
--color-text-muted: #52526a;
|
--color-text-muted: #52526a;
|
||||||
|
|||||||
@@ -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" })
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import type { DateClickArg, EventResizeDoneArg } from "@fullcalendar/interaction
|
|||||||
import { listEvents, updateEvent, type EventEntry } from "@/api/client";
|
import { listEvents, updateEvent, type EventEntry } from "@/api/client";
|
||||||
import EventSlideOver from "@/components/EventSlideOver.vue";
|
import EventSlideOver from "@/components/EventSlideOver.vue";
|
||||||
import { useToastStore } from "@/stores/toast";
|
import { useToastStore } from "@/stores/toast";
|
||||||
|
import { fmtTime, fmtDateTime, fmtDayLabel } from "@/utils/dateFormat";
|
||||||
|
|
||||||
const toast = useToastStore();
|
const toast = useToastStore();
|
||||||
const calendarRef = ref<InstanceType<typeof FullCalendar> | null>(null);
|
const calendarRef = ref<InstanceType<typeof FullCalendar> | null>(null);
|
||||||
@@ -221,31 +222,11 @@ const calendarOptions: CalendarOptions = {
|
|||||||
height: "auto",
|
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
|
// Group upcoming events by day label
|
||||||
const upcomingGrouped = computed(() => {
|
const upcomingGrouped = computed(() => {
|
||||||
const groups: { label: string; date: string; events: EventEntry[] }[] = [];
|
const groups: { label: string; date: string; events: EventEntry[] }[] = [];
|
||||||
for (const e of upcomingEvents.value) {
|
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);
|
const existing = groups.find((g) => g.label === label);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.events.push(e);
|
existing.events.push(e);
|
||||||
@@ -321,10 +302,10 @@ const upcomingGrouped = computed(() => {
|
|||||||
<div class="popover-title">{{ popover.title }}</div>
|
<div class="popover-title">{{ popover.title }}</div>
|
||||||
<div class="popover-time">
|
<div class="popover-time">
|
||||||
<template v-if="popover.all_day">
|
<template v-if="popover.all_day">
|
||||||
{{ fmtDate(popover.start_dt, true) }}
|
{{ fmtDateTime(popover.start_dt, true) }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ fmtDate(popover.start_dt, false) }}
|
{{ fmtDateTime(popover.start_dt, false) }}
|
||||||
<span v-if="popover.end_dt"> – {{ fmtTime(popover.end_dt) }}</span>
|
<span v-if="popover.end_dt"> – {{ fmtTime(popover.end_dt) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -390,7 +371,7 @@ const upcomingGrouped = computed(() => {
|
|||||||
.btn-new-event:hover { opacity: 0.88; }
|
.btn-new-event:hover { opacity: 0.88; }
|
||||||
|
|
||||||
.fc-wrapper {
|
.fc-wrapper {
|
||||||
background: var(--color-surface, #1a1b1e);
|
background: var(--color-surface);
|
||||||
border: 1px solid var(--color-border, #2a2b30);
|
border: 1px solid var(--color-border, #2a2b30);
|
||||||
border-radius: var(--radius-lg, 18px);
|
border-radius: var(--radius-lg, 18px);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
@@ -407,7 +388,7 @@ const upcomingGrouped = computed(() => {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
:deep(.fc-button) {
|
:deep(.fc-button) {
|
||||||
background: var(--color-input-bg, #111113);
|
background: var(--color-input-bg, var(--color-bg));
|
||||||
border: 1px solid var(--color-border, #2a2b30);
|
border: 1px solid var(--color-border, #2a2b30);
|
||||||
color: var(--color-text-muted, #888);
|
color: var(--color-text-muted, #888);
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
@@ -488,7 +469,7 @@ const upcomingGrouped = computed(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
background: var(--color-surface, #1a1b1e);
|
background: var(--color-surface);
|
||||||
border: 1px solid var(--color-border, #2a2b30);
|
border: 1px solid var(--color-border, #2a2b30);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -496,8 +477,8 @@ const upcomingGrouped = computed(() => {
|
|||||||
transition: border-color 0.15s, background 0.15s;
|
transition: border-color 0.15s, background 0.15s;
|
||||||
}
|
}
|
||||||
.upcoming-card:hover {
|
.upcoming-card:hover {
|
||||||
border-color: rgba(99,102,241,0.4);
|
border-color: color-mix(in srgb, var(--color-primary) 40%, transparent);
|
||||||
background: var(--color-bg-secondary, #16171a);
|
background: var(--color-bg-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.upcoming-card-accent {
|
.upcoming-card-accent {
|
||||||
@@ -548,7 +529,7 @@ const upcomingGrouped = computed(() => {
|
|||||||
|
|
||||||
/* ── Event popover ──────────────────────────────────────────────────────── */
|
/* ── Event popover ──────────────────────────────────────────────────────── */
|
||||||
.event-popover {
|
.event-popover {
|
||||||
background: var(--color-bg-card, #1e1f24);
|
background: var(--color-bg-card);
|
||||||
border: 1px solid var(--color-border, #2a2b30);
|
border: 1px solid var(--color-border, #2a2b30);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 8px 32px rgba(0,0,0,0.45);
|
box-shadow: 0 8px 32px rgba(0,0,0,0.45);
|
||||||
@@ -623,7 +604,7 @@ const upcomingGrouped = computed(() => {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.popover-btn--close {
|
.popover-btn--close {
|
||||||
background: var(--color-input-bg, #111113);
|
background: var(--color-input-bg, var(--color-bg));
|
||||||
color: var(--color-text-muted, #888);
|
color: var(--color-text-muted, #888);
|
||||||
border: 1px solid var(--color-border, #2a2b30);
|
border: 1px solid var(--color-border, #2a2b30);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ref, computed, onMounted, onUnmounted } from "vue";
|
|||||||
import { apiGet, listEvents } from "@/api/client";
|
import { apiGet, listEvents } from "@/api/client";
|
||||||
import { useBackgroundRefresh } from "@/composables/useBackgroundRefresh";
|
import { useBackgroundRefresh } from "@/composables/useBackgroundRefresh";
|
||||||
import { milestoneColor } from "@/utils/palette";
|
import { milestoneColor } from "@/utils/palette";
|
||||||
|
import { fmtRelativeDateTime } from "@/utils/dateFormat";
|
||||||
import type { Note } from "@/types/note";
|
import type { Note } from "@/types/note";
|
||||||
import type { Task, TaskListResponse, TaskStatus } from "@/types/task";
|
import type { Task, TaskListResponse, TaskStatus } from "@/types/task";
|
||||||
import type { ToolCallRecord, Message } from "@/types/chat";
|
import type { ToolCallRecord, Message } from "@/types/chat";
|
||||||
@@ -304,28 +305,8 @@ function onEventDeleted(id: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatUpcomingTime(event: EventEntry): string {
|
function formatUpcomingTime(event: EventEntry): string {
|
||||||
if (event.all_day) return "All day";
|
|
||||||
if (!event.start_dt) return "";
|
if (!event.start_dt) return "";
|
||||||
try {
|
return fmtRelativeDateTime(event.start_dt, event.all_day);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from "vue";
|
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { apiGet, transcribeAudio } from "@/api/client";
|
import { apiGet, transcribeAudio } from "@/api/client";
|
||||||
|
import { fmtCompact } from "@/utils/dateFormat";
|
||||||
import { useChatStore } from "@/stores/chat";
|
import { useChatStore } from "@/stores/chat";
|
||||||
import { useSettingsStore } from "@/stores/settings";
|
import { useSettingsStore } from "@/stores/settings";
|
||||||
import { useVoiceRecorder } from "@/composables/useVoiceRecorder";
|
import { useVoiceRecorder } from "@/composables/useVoiceRecorder";
|
||||||
@@ -118,11 +119,7 @@ async function fetchTodayBar() {
|
|||||||
} catch { /* silent */ }
|
} catch { /* silent */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatEventDate(dtStr: string, allDay: boolean): string {
|
const formatEventDate = fmtCompact
|
||||||
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" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Graph panel ──────────────────────────────────────────────────────────────
|
// ─── Graph panel ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user