Calendar visual overhaul: translucent events, AU locale, typography hierarchy
- Import en-AU locale (object, not string) for proper date format (day/month) - Add 12-hour time format (9:00 AM), side-by-side overlap columns - Replace flat opaque event rectangles with translucent color-mix fills (12% opacity) - Add 2px left accent border per calendar color via CSS custom property - Implement eventContent render hook with typography hierarchy (time secondary, title primary) - Add recurring event indicator (Repeat icon) next to recurring event titles - Add now-indicator 6px pulse dot with prefers-reduced-motion respect - Add subtle weekend column tint (hsl 0 0% 6%) - Mobile: hide time spans in month view custom eventContent - Update +more popover to inherit translucent event styling - Document calendar event patterns in stylesheet.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
652be41da4
commit
c473e7e235
@ -8,8 +8,9 @@ import FullCalendar from '@fullcalendar/react';
|
|||||||
import dayGridPlugin from '@fullcalendar/daygrid';
|
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||||
import timeGridPlugin from '@fullcalendar/timegrid';
|
import timeGridPlugin from '@fullcalendar/timegrid';
|
||||||
import interactionPlugin from '@fullcalendar/interaction';
|
import interactionPlugin from '@fullcalendar/interaction';
|
||||||
import type { EventClickArg, DateSelectArg, EventDropArg, DatesSetArg } from '@fullcalendar/core';
|
import enAuLocale from '@fullcalendar/core/locales/en-au';
|
||||||
import { ChevronLeft, ChevronRight, PanelLeft, Plus, Search } from 'lucide-react';
|
import type { EventClickArg, DateSelectArg, EventDropArg, DatesSetArg, EventContentArg } from '@fullcalendar/core';
|
||||||
|
import { ChevronLeft, ChevronRight, PanelLeft, Plus, Search, Repeat } from 'lucide-react';
|
||||||
import api, { getErrorMessage } from '@/lib/api';
|
import api, { getErrorMessage } from '@/lib/api';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type { CalendarEvent, EventTemplate, Location as LocationType, CalendarPermission } from '@/types';
|
import type { CalendarEvent, EventTemplate, Location as LocationType, CalendarPermission } from '@/types';
|
||||||
@ -500,6 +501,56 @@ export default function CalendarPage() {
|
|||||||
const navigateToday = () => calendarRef.current?.getApi().today();
|
const navigateToday = () => calendarRef.current?.getApi().today();
|
||||||
const changeView = (view: CalendarView) => calendarRef.current?.getApi().changeView(view);
|
const changeView = (view: CalendarView) => calendarRef.current?.getApi().changeView(view);
|
||||||
|
|
||||||
|
// Set --event-color CSS var via eventDidMount (write-only, no reads)
|
||||||
|
const handleEventDidMount = useCallback((info: { el: HTMLElement; event: { extendedProps: Record<string, unknown> } }) => {
|
||||||
|
const color = info.event.extendedProps.calendarColor as string;
|
||||||
|
if (color) {
|
||||||
|
info.el.style.setProperty('--event-color', color);
|
||||||
|
}
|
||||||
|
info.el.classList.add('umbra-event');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderEventContent = useCallback((arg: EventContentArg) => {
|
||||||
|
const isMonth = arg.view.type === 'dayGridMonth';
|
||||||
|
const isAllDay = arg.event.allDay;
|
||||||
|
const isRecurring = arg.event.extendedProps.is_recurring || arg.event.extendedProps.parent_event_id;
|
||||||
|
|
||||||
|
const repeatIcon = isRecurring ? (
|
||||||
|
<Repeat className="h-2.5 w-2.5 shrink-0 opacity-50" />
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
if (isAllDay) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1 truncate">
|
||||||
|
<span className="text-[11px] font-medium truncate">{arg.event.title}</span>
|
||||||
|
{repeatIcon}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMonth) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1 truncate">
|
||||||
|
<span className="umbra-event-time text-[11px] opacity-60 shrink-0">{arg.timeText}</span>
|
||||||
|
<span className="text-[11px] font-medium truncate">{arg.event.title}</span>
|
||||||
|
{repeatIcon}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Week/day view
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col overflow-hidden h-full">
|
||||||
|
<span className="text-[10px] opacity-60 leading-tight">{arg.timeText}</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-[12px] font-medium truncate">{arg.event.title}</span>
|
||||||
|
{repeatIcon}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full overflow-hidden animate-fade-in">
|
<div className="flex h-full overflow-hidden animate-fade-in">
|
||||||
<div className="hidden lg:flex lg:flex-row shrink-0">
|
<div className="hidden lg:flex lg:flex-row shrink-0">
|
||||||
@ -644,6 +695,13 @@ export default function CalendarPage() {
|
|||||||
select={handleDateSelect}
|
select={handleDateSelect}
|
||||||
datesSet={handleDatesSet}
|
datesSet={handleDatesSet}
|
||||||
height="100%"
|
height="100%"
|
||||||
|
locale={enAuLocale}
|
||||||
|
eventTimeFormat={{ hour: 'numeric', minute: '2-digit', meridiem: 'short' }}
|
||||||
|
slotLabelFormat={{ hour: 'numeric', minute: '2-digit', meridiem: 'short' }}
|
||||||
|
dayHeaderFormat={{ weekday: 'short', day: 'numeric', month: 'numeric' }}
|
||||||
|
slotEventOverlap={false}
|
||||||
|
eventDidMount={handleEventDidMount}
|
||||||
|
eventContent={renderEventContent}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -91,9 +91,9 @@
|
|||||||
--fc-button-hover-border-color: hsl(0 0% 20%);
|
--fc-button-hover-border-color: hsl(0 0% 20%);
|
||||||
--fc-button-active-bg-color: hsl(var(--accent-color));
|
--fc-button-active-bg-color: hsl(var(--accent-color));
|
||||||
--fc-button-active-border-color: hsl(var(--accent-color));
|
--fc-button-active-border-color: hsl(var(--accent-color));
|
||||||
--fc-event-bg-color: hsl(var(--accent-color));
|
--fc-event-bg-color: transparent;
|
||||||
--fc-event-border-color: hsl(var(--accent-color));
|
--fc-event-border-color: transparent;
|
||||||
--fc-event-text-color: hsl(0 0% 98%);
|
--fc-event-text-color: inherit;
|
||||||
--fc-page-bg-color: transparent;
|
--fc-page-bg-color: transparent;
|
||||||
--fc-neutral-bg-color: hsl(0 0% 8% / 0.65);
|
--fc-neutral-bg-color: hsl(0 0% 8% / 0.65);
|
||||||
--fc-neutral-text-color: hsl(0 0% 98%);
|
--fc-neutral-text-color: hsl(0 0% 98%);
|
||||||
@ -144,6 +144,32 @@
|
|||||||
border-bottom-color: hsl(var(--accent-color));
|
border-bottom-color: hsl(var(--accent-color));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Now indicator pulse dot */
|
||||||
|
.fc .fc-timegrid-now-indicator-line::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -3px;
|
||||||
|
top: -3px;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: hsl(var(--accent-color));
|
||||||
|
animation: pulse-dot 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.fc .fc-timegrid-now-indicator-line::before {
|
||||||
|
animation: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Weekend column tint */
|
||||||
|
.fc .fc-day-sat,
|
||||||
|
.fc .fc-day-sun {
|
||||||
|
background-color: hsl(0 0% 6%);
|
||||||
|
}
|
||||||
|
|
||||||
.fc .fc-col-header-cell {
|
.fc .fc-col-header-cell {
|
||||||
background-color: hsl(0 0% 8% / 0.65);
|
background-color: hsl(0 0% 8% / 0.65);
|
||||||
border-color: var(--fc-border-color);
|
border-color: var(--fc-border-color);
|
||||||
@ -171,21 +197,41 @@
|
|||||||
color: hsl(0 0% 63.9%);
|
color: hsl(0 0% 63.9%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Event pills — compact rounded style */
|
/* ── Translucent event styling ── */
|
||||||
.fc .fc-daygrid-event {
|
.fc .umbra-event {
|
||||||
border-radius: 4px;
|
background: color-mix(in srgb, var(--event-color) 12%, transparent) !important;
|
||||||
|
border: none !important;
|
||||||
|
border-left: 2px solid var(--event-color) !important;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .umbra-event .fc-event-main {
|
||||||
|
color: var(--event-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover state */
|
||||||
|
.fc .umbra-event:hover {
|
||||||
|
background: color-mix(in srgb, var(--event-color) 22%, transparent) !important;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Day grid (month view) */
|
||||||
|
.fc .fc-daygrid-event.umbra-event {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
padding: 0px 4px;
|
padding: 1px 6px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc .fc-timegrid-event {
|
/* Time grid (week/day view) */
|
||||||
border-radius: 4px;
|
.fc .fc-timegrid-event.umbra-event {
|
||||||
font-size: 0.75rem;
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc .fc-timegrid-event .fc-event-main {
|
.fc .fc-timegrid-event.umbra-event .fc-event-main {
|
||||||
padding: 2px 4px;
|
padding: 3px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Day number styling for today */
|
/* Day number styling for today */
|
||||||
@ -262,6 +308,10 @@
|
|||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
/* Ensure translucent events in popover inherit styling */
|
||||||
|
.fc .fc-more-popover .umbra-event {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Chromium native date picker icon fix (safety net) ── */
|
/* ── Chromium native date picker icon fix (safety net) ── */
|
||||||
@ -358,6 +408,11 @@ form[data-submitted] input:invalid + button {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hide time spans in custom eventContent on mobile month view */
|
||||||
|
.fc .fc-daygrid-event .umbra-event-time {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.fc .fc-timegrid-event {
|
.fc .fc-timegrid-event {
|
||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user