Files
policy-ui/app/pages/calendar.vue
Jordan Weingarten 67482f6629 WIP jordan
2026-04-16 11:11:44 -05:00

1515 lines
55 KiB
Vue

<script setup lang="ts">
usePageTitle('Calendar')
type CalEventType = 'meeting' | 'renewal' | 'claim' | 'payment' | 'reminder' | 'threshold'
interface CalEvent {
id: string
title: string
time: string
type: CalEventType
source?: 'gmail' | 'system' | 'manual'
customer?: string
detail?: string
urgent?: boolean
/** Day of month (1-based) for month view placement */
dayOfMonth?: number
}
/* ── Layer system ── */
type CalLayer = 'renewals' | 'claims' | 'payments' | 'meetings' | 'reminders' | 'alerts'
const layerMeta: Record<CalLayer, { label: string; types: CalEventType[]; color: string; icon: string }> = {
meetings: { label: 'Meetings & calls', types: ['meeting'], color: '#01696f', icon: 'i-heroicons-calendar-days' },
renewals: { label: 'Renewals', types: ['renewal'], color: '#7c3aed', icon: 'i-heroicons-arrow-path' },
claims: { label: 'Claims', types: ['claim'], color: '#c13838', icon: 'i-heroicons-shield-exclamation' },
payments: { label: 'Payments & AR', types: ['payment'], color: '#c27b1a', icon: 'i-heroicons-banknotes' },
reminders: { label: 'Reminders', types: ['reminder'], color: '#0f7b5f', icon: 'i-heroicons-bell' },
alerts: { label: 'Threshold alerts', types: ['threshold'], color: '#be185d', icon: 'i-heroicons-exclamation-triangle' },
}
const LAYER_ORDER: CalLayer[] = ['meetings', 'renewals', 'claims', 'payments', 'reminders', 'alerts']
const activeLayers = reactive<Record<CalLayer, boolean>>({
meetings: true,
renewals: true,
claims: true,
payments: true,
reminders: true,
alerts: true,
})
const layerPanelOpen = ref(false)
const activeLayerCount = computed(() =>
LAYER_ORDER.filter(l => activeLayers[l]).length
)
/** Which event types are enabled by active layers */
const enabledTypes = computed(() => {
const types = new Set<CalEventType>()
for (const layer of LAYER_ORDER) {
if (activeLayers[layer]) {
for (const t of layerMeta[layer].types) types.add(t)
}
}
return types
})
function toggleLayer(layer: CalLayer) { activeLayers[layer] = !activeLayers[layer] }
function enableAllLayers() { LAYER_ORDER.forEach(l => { activeLayers[l] = true }) }
function disableAllLayers() { LAYER_ORDER.forEach(l => { activeLayers[l] = false }) }
const calView = ref<'month' | 'week' | 'day'>('month')
const typeMeta: Record<CalEventType, { label: string; color: string; icon: string }> = {
meeting: { label: 'Meetings', color: '#01696f', icon: 'i-heroicons-calendar-days' },
renewal: { label: 'Renewals', color: '#7c3aed', icon: 'i-heroicons-arrow-path' },
claim: { label: 'Claims', color: '#c13838', icon: 'i-heroicons-shield-exclamation' },
payment: { label: 'Payments', color: '#c27b1a', icon: 'i-heroicons-banknotes' },
reminder: { label: 'Reminders', color: '#0f7b5f', icon: 'i-heroicons-bell' },
threshold: { label: 'Alerts', color: '#be185d', icon: 'i-heroicons-exclamation-triangle' },
}
/* ── Today's events ── */
const todayEventsSeed: CalEvent[] = [
{ id: 'c1', title: 'Renewal review — Constructora Delta', time: '9:00 AM', type: 'renewal', source: 'system', customer: 'Constructora Delta S.A.', detail: 'Policy expires Jul 15. Premium $55K, 3 yr client.', urgent: true },
{ id: 'c2', title: 'Call with María Pérez', time: '10:30 AM', type: 'meeting', source: 'gmail', customer: 'María Pérez', detail: 'Auto quote follow-up. Synced from Gmail.' },
{ id: 'c3', title: 'Unpaid premium — Farmacia Salud', time: '11:00 AM', type: 'payment', source: 'system', customer: 'Farmacia Salud', detail: '45 days overdue. $12,800 group life.', urgent: true },
{ id: 'c4', title: 'Claim status update — Hotel Pacífico', time: '1:00 PM', type: 'claim', source: 'system', customer: 'Hotel Pacífico', detail: 'Fire claim #CL-2891. Adjuster report due.' },
{ id: 'c5', title: 'Client crossed $100K premium', time: '2:00 PM', type: 'threshold', source: 'system', customer: 'Banco Regional', detail: 'Total book now $186K. VIP follow-up recommended.' },
{ id: 'c6', title: 'Prepare renewal proposal', time: '3:30 PM', type: 'reminder', source: 'manual', detail: 'Transportes del Sur fleet policy. Get 3 comparative quotes.' },
{ id: 'c7', title: 'Team sync — weekly pipeline', time: '4:00 PM', type: 'meeting', source: 'gmail', detail: 'Google Meet link attached. All agents.' },
{ id: 'c8', title: '5 renewals due next 7 days', time: '—', type: 'renewal', source: 'system', detail: 'Batch alert: review upcoming expirations.', urgent: true },
]
/* ── Month-spread mock events (scattered across the month) ── */
const monthMockEventsSeed: CalEvent[] = [
{ id: 'm1', title: 'Renewal — Transportes Delta fleet', time: '9:00', type: 'renewal', dayOfMonth: 2, urgent: true },
{ id: 'm2', title: 'Payment due — Retail Plaza', time: '—', type: 'payment', dayOfMonth: 3 },
{ id: 'm3', title: 'Client meeting — Banco Regional', time: '11:00', type: 'meeting', dayOfMonth: 5, customer: 'Banco Regional' },
{ id: 'm4', title: 'Claim follow-up — Hotel Pacífico', time: '2:00', type: 'claim', dayOfMonth: 7 },
{ id: 'm5', title: 'Renewal — Clínica Norte group health', time: '—', type: 'renewal', dayOfMonth: 8, urgent: true },
{ id: 'm6', title: 'Premium threshold — Solís Calderón', time: '—', type: 'threshold', dayOfMonth: 10, customer: 'Luis Andrés Solís' },
{ id: 'm7', title: 'Quarterly review prep', time: '3:00', type: 'reminder', dayOfMonth: 12 },
{ id: 'm8', title: 'Team sync', time: '4:00', type: 'meeting', dayOfMonth: 14 },
{ id: 'm9', title: 'Collections call — 3 accounts', time: '10:00', type: 'payment', dayOfMonth: 15, urgent: true },
{ id: 'm10', title: 'New business onboarding — Startup Labs', time: '1:00', type: 'meeting', dayOfMonth: 17, customer: 'Startup Labs' },
{ id: 'm11', title: 'Renewal batch — 4 policies', time: '—', type: 'renewal', dayOfMonth: 19 },
{ id: 'm12', title: 'Claim deadline — CL-3020', time: '—', type: 'claim', dayOfMonth: 21, urgent: true },
{ id: 'm13', title: 'Reminder: quote comparison due', time: '—', type: 'reminder', dayOfMonth: 22 },
{ id: 'm14', title: 'Payment reconciliation', time: '9:00', type: 'payment', dayOfMonth: 24 },
{ id: 'm15', title: 'Board presentation prep', time: '2:00', type: 'meeting', dayOfMonth: 26 },
{ id: 'm16', title: 'Month-end close', time: '—', type: 'reminder', dayOfMonth: 28 },
{ id: 'm17', title: 'Renewal — María Pérez auto', time: '—', type: 'renewal', dayOfMonth: 30 },
]
/* ── User-created appointments (reactive, add/remove) ── */
const userAppointments = ref<CalEvent[]>([])
let nextApptId = 1
/* ── Add appointment modal ── */
const addModalOpen = ref(false)
const addForm = reactive({
title: '',
date: new Date().toISOString().slice(0, 10),
startTime: '09:00',
endTime: '10:00',
allDay: false,
type: 'meeting' as CalEventType,
customer: '',
dayOfMonth: new Date().getDate(),
location: '',
videoCall: false,
repeat: 'none' as 'none' | 'daily' | 'weekly' | 'monthly' | 'yearly',
guests: '',
notification: '15' as string,
color: '' as string,
detail: '',
})
function resetAddForm() {
addForm.title = ''
addForm.date = new Date().toISOString().slice(0, 10)
addForm.startTime = '09:00'
addForm.endTime = '10:00'
addForm.allDay = false
addForm.type = 'meeting'
addForm.customer = ''
addForm.dayOfMonth = new Date().getDate()
addForm.location = ''
addForm.videoCall = false
addForm.repeat = 'none'
addForm.guests = ''
addForm.notification = '15'
addForm.color = ''
addForm.detail = ''
}
const eventColors = [
{ label: 'Teal', value: '#01696f' },
{ label: 'Purple', value: '#7c3aed' },
{ label: 'Red', value: '#c13838' },
{ label: 'Amber', value: '#c27b1a' },
{ label: 'Green', value: '#0f7b5f' },
{ label: 'Pink', value: '#be185d' },
{ label: 'Blue', value: '#2563eb' },
]
function openAddModal(presetDay?: number) {
resetAddForm()
if (presetDay) {
addForm.dayOfMonth = presetDay
const y = currentMonth.value.getFullYear()
const m = currentMonth.value.getMonth()
const d = new Date(y, m, presetDay)
addForm.date = d.toISOString().slice(0, 10)
}
addModalOpen.value = true
}
function submitAppointment() {
if (!addForm.title.trim()) return
const parsedDay = addForm.date ? new Date(addForm.date + 'T12:00:00').getDate() : addForm.dayOfMonth
const timeLabel = addForm.allDay ? 'All day' : addForm.startTime ? formatTime12(addForm.startTime) : '—'
const appt: CalEvent = {
id: `user-${nextApptId++}`,
title: addForm.title.trim(),
time: timeLabel,
type: addForm.type,
source: 'manual',
customer: addForm.customer.trim() || undefined,
detail: [
addForm.detail.trim(),
addForm.location ? `Location: ${addForm.location}` : '',
addForm.guests ? `Guests: ${addForm.guests}` : '',
].filter(Boolean).join(' · ') || undefined,
dayOfMonth: parsedDay,
}
userAppointments.value = [...userAppointments.value, appt]
addModalOpen.value = false
}
function formatTime12(t: string) {
const [h, m] = t.split(':').map(Number)
const ampm = h >= 12 ? 'PM' : 'AM'
const h12 = h % 12 || 12
return `${h12}:${String(m).padStart(2, '0')} ${ampm}`
}
function removeAppointment(id: string) {
userAppointments.value = userAppointments.value.filter(a => a.id !== id)
}
/* ── Merged events ── */
const todayEvents = computed(() => {
const today = new Date().getDate()
const userToday = userAppointments.value.filter(a => a.dayOfMonth === today)
return [...todayEventsSeed, ...userToday]
})
const monthMockEvents = computed(() => {
return [...monthMockEventsSeed, ...userAppointments.value]
})
const filteredEvents = computed(() => todayEvents.value.filter(e => enabledTypes.value.has(e.type)))
/* ── Month calendar grid ── */
const currentMonth = ref(new Date())
const monthLabel = computed(() =>
currentMonth.value.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })
)
function prevMonth() {
const d = new Date(currentMonth.value)
d.setMonth(d.getMonth() - 1)
currentMonth.value = d
}
function nextMonth() {
const d = new Date(currentMonth.value)
d.setMonth(d.getMonth() + 1)
currentMonth.value = d
}
function goToday() {
currentMonth.value = new Date()
}
const monthGrid = computed(() => {
const year = currentMonth.value.getFullYear()
const month = currentMonth.value.getMonth()
const firstDay = new Date(year, month, 1)
const lastDay = new Date(year, month + 1, 0)
const daysInMonth = lastDay.getDate()
// Monday = 0 start
let startDow = firstDay.getDay() - 1
if (startDow < 0) startDow = 6
const today = new Date()
const isCurrentMonth = today.getFullYear() === year && today.getMonth() === month
const todayDate = today.getDate()
const cells: {
date: number
isCurrentMonth: boolean
isToday: boolean
events: CalEvent[]
}[] = []
// Previous month trailing days
const prevLastDay = new Date(year, month, 0).getDate()
for (let i = startDow - 1; i >= 0; i--) {
cells.push({ date: prevLastDay - i, isCurrentMonth: false, isToday: false, events: [] })
}
// Current month days
for (let d = 1; d <= daysInMonth; d++) {
const dayEvents = monthMockEvents.value.filter(e => e.dayOfMonth === d && enabledTypes.value.has(e.type))
// Add today's events to today's date
if (isCurrentMonth && d === todayDate) {
const todayFiltered = todayEventsSeed.filter(e => enabledTypes.value.has(e.type))
dayEvents.push(...todayFiltered)
}
cells.push({
date: d,
isCurrentMonth: true,
isToday: isCurrentMonth && d === todayDate,
events: dayEvents
})
}
// Next month leading days (fill to 6 rows)
const remaining = (7 - (cells.length % 7)) % 7
for (let i = 1; i <= remaining; i++) {
cells.push({ date: i, isCurrentMonth: false, isToday: false, events: [] })
}
return cells
})
const weekDays = computed(() => {
const today = new Date()
const dow = today.getDay()
const monday = new Date(today)
monday.setDate(today.getDate() - (dow === 0 ? 6 : dow - 1))
return Array.from({ length: 7 }, (_, i) => {
const d = new Date(monday)
d.setDate(monday.getDate() + i)
return {
label: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][i],
date: d.getDate(),
isToday: d.toDateString() === today.toDateString(),
events: i === (dow === 0 ? 6 : dow - 1) ? todayEvents.value.length : Math.floor(Math.random() * 4)
}
})
})
function dotStyle(type: CalEventType) { return `background: ${typeMeta[type].color}` }
/* ── Click outside to close panels ── */
function onClickOutside(e: MouseEvent) {
const target = e.target as HTMLElement
if (layerPanelOpen.value && !target.closest('.cal-layers-wrap')) layerPanelOpen.value = false
}
onMounted(() => document.addEventListener('click', onClickOutside))
onUnmounted(() => document.removeEventListener('click', onClickOutside))
/* ── Google Calendar link (placeholder) ── */
function openGoogleCalendar() {
window.open('https://calendar.google.com', '_blank')
}
</script>
<template>
<div class="cal-page">
<!-- Header row -->
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<h1 class="mt-1 text-2xl font-semibold tracking-tight text-[var(--text-primary)]">Calendar</h1>
<p class="mt-2 text-[14px] leading-relaxed text-[var(--text-muted)]">
Your schedule, system alerts, renewal deadlines, and follow-ups in one view.
</p>
</div>
<div class="flex items-center gap-2 flex-wrap">
<!-- Add appointment -->
<button type="button" class="cal-action-btn cal-action-primary" @click="openAddModal()">
<UIcon name="i-heroicons-plus" style="width: 14px; height: 14px;" />
New event
</button>
<!-- Google Calendar link -->
<button type="button" class="cal-action-btn" @click="openGoogleCalendar">
<UIcon name="i-heroicons-arrow-top-right-on-square" style="width: 13px; height: 13px;" />
Google Calendar
</button>
<!-- View toggle -->
<div class="cal-toggle">
<button type="button" class="cal-toggle-btn" :class="calView === 'month' ? 'cal-toggle-on' : 'cal-toggle-off'" @click="calView = 'month'">Month</button>
<button type="button" class="cal-toggle-btn" :class="calView === 'week' ? 'cal-toggle-on' : 'cal-toggle-off'" @click="calView = 'week'">Week</button>
<button type="button" class="cal-toggle-btn" :class="calView === 'day' ? 'cal-toggle-on' : 'cal-toggle-off'" @click="calView = 'day'">Day</button>
</div>
</div>
</div>
<!-- Layer strip + integration badges -->
<div class="cal-layers-row">
<!-- Active layer pills -->
<div class="flex items-center gap-2 flex-wrap flex-1 min-w-0">
<TransitionGroup
enter-active-class="transition-all duration-200 ease-out"
enter-from-class="opacity-0 scale-90"
enter-to-class="opacity-100 scale-100"
leave-active-class="transition-all duration-150 ease-in"
leave-from-class="opacity-100 scale-100"
leave-to-class="opacity-0 scale-90"
>
<button
v-for="layer in LAYER_ORDER"
v-show="activeLayers[layer]"
:key="layer"
type="button"
class="cal-layer-pill"
:style="`--lc: ${layerMeta[layer].color}`"
@click="toggleLayer(layer)"
:title="`Click to hide ${layerMeta[layer].label}`"
>
<span class="cal-layer-dot" :style="`background: ${layerMeta[layer].color}`" />
{{ layerMeta[layer].label }}
<UIcon name="i-heroicons-x-mark" style="width: 10px; height: 10px; opacity: 0.5;" />
</button>
</TransitionGroup>
<span v-if="activeLayerCount === 0" class="text-[12px] text-[var(--text-muted)] italic">No layers active click Layers to add some</span>
</div>
<!-- Layers config button -->
<div class="cal-layers-wrap relative">
<button type="button" class="cal-ctrl-btn" @click.stop="layerPanelOpen = !layerPanelOpen">
<UIcon name="i-heroicons-adjustments-horizontal" style="width: 14px; height: 14px;" />
Layers
<span v-if="activeLayerCount < 6" class="cal-ctrl-count">{{ activeLayerCount }}/6</span>
</button>
<!-- Layer config dropdown -->
<Transition
enter-active-class="transition duration-150 ease-out"
enter-from-class="opacity-0 scale-95 -translate-y-1"
enter-to-class="opacity-100 scale-100 translate-y-0"
leave-active-class="transition duration-100 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0 scale-95"
>
<div v-if="layerPanelOpen" class="cal-layer-dropdown">
<div class="cal-layer-dropdown-head">
<span>Calendar layers</span>
<div class="flex gap-1">
<button type="button" class="cal-layer-action" @click="enableAllLayers">All on</button>
<button type="button" class="cal-layer-action" @click="disableAllLayers">All off</button>
</div>
</div>
<p class="cal-layer-dropdown-hint">Toggle information layers on/off to customize your view.</p>
<ul class="cal-layer-list">
<li
v-for="layer in LAYER_ORDER"
:key="layer"
class="cal-layer-item"
:class="activeLayers[layer] ? 'cal-layer-item-on' : ''"
@click="toggleLayer(layer)"
>
<div class="cal-layer-check" :style="activeLayers[layer] ? `background: ${layerMeta[layer].color}; border-color: ${layerMeta[layer].color}` : ''">
<UIcon v-if="activeLayers[layer]" name="i-heroicons-check" style="width: 11px; height: 11px; color: #fff;" />
</div>
<UIcon :name="layerMeta[layer].icon" style="width: 14px; height: 14px;" :style="`color: ${layerMeta[layer].color}; opacity: 0.7;`" />
<span class="text-[12px] font-medium text-[var(--text-primary)]">{{ layerMeta[layer].label }}</span>
</li>
</ul>
</div>
</Transition>
</div>
<!-- Integration badges -->
<div class="flex items-center gap-1.5 ml-1">
<span class="cal-badge cal-badge-gmail"><UIcon name="i-heroicons-envelope" style="width: 11px; height: 11px;" /> Gmail</span>
<span class="cal-badge cal-badge-system"><UIcon name="i-heroicons-cog-6-tooth" style="width: 11px; height: 11px;" /> System</span>
</div>
</div>
<!-- Legend -->
<div class="cal-legend">
<div class="flex items-center justify-between">
<p class="text-[11px] font-semibold uppercase tracking-[0.04em] text-[#8a8a86]">Event types</p>
<p class="text-[10px] text-[var(--text-muted)]">Double-click a day to add an event</p>
</div>
<div class="flex flex-wrap gap-3 mt-2">
<div v-for="(meta, type) in typeMeta" :key="type" class="flex items-center gap-1.5">
<span class="cal-dot" :style="`background: ${meta.color}`" />
<span class="text-[12px] text-[var(--text-muted)]">{{ meta.label }}</span>
</div>
</div>
</div>
<!-- Main card -->
<div class="cal-card">
<!-- MONTH VIEW -->
<template v-if="calView === 'month'">
<!-- Month navigation -->
<div class="cal-month-nav">
<button type="button" class="cal-month-arrow" @click="prevMonth">
<UIcon name="i-heroicons-chevron-left" style="width: 16px; height: 16px;" />
</button>
<div class="flex items-center gap-2">
<h2 class="text-[14px] font-semibold text-[var(--text-primary)]">{{ monthLabel }}</h2>
<button type="button" class="cal-today-btn" @click="goToday">Today</button>
</div>
<button type="button" class="cal-month-arrow" @click="nextMonth">
<UIcon name="i-heroicons-chevron-right" style="width: 16px; height: 16px;" />
</button>
</div>
<!-- Day-of-week headers -->
<div class="cal-month-header">
<div v-for="day in ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']" :key="day" class="cal-month-dow">
{{ day }}
</div>
</div>
<!-- Calendar grid -->
<div class="cal-month-grid">
<div
v-for="(cell, ci) in monthGrid"
:key="ci"
class="cal-month-cell"
:class="{
'cal-month-cell-other': !cell.isCurrentMonth,
'cal-month-cell-today': cell.isToday,
}"
@dblclick="cell.isCurrentMonth && openAddModal(cell.date)"
>
<div class="cal-month-cell-head">
<span class="cal-month-date" :class="cell.isToday ? 'cal-month-date-today' : ''">
{{ cell.date }}
</span>
<!-- Add button on hover for current month cells -->
<button
v-if="cell.isCurrentMonth"
type="button"
class="cal-cell-add"
title="Add event"
@click.stop="openAddModal(cell.date)"
>
<UIcon name="i-heroicons-plus" style="width: 10px; height: 10px;" />
</button>
</div>
<div v-if="cell.events.length > 0" class="cal-month-events">
<div
v-for="ev in cell.events.slice(0, 3)"
:key="ev.id"
class="cal-month-ev"
:class="ev.urgent ? 'cal-month-ev-urgent' : ''"
:title="ev.title"
>
<span class="cal-month-ev-dot" :style="dotStyle(ev.type)" />
<span class="cal-month-ev-text">{{ ev.title }}</span>
<!-- Remove button for user-created events -->
<button
v-if="ev.id.startsWith('user-')"
type="button"
class="cal-ev-remove-mini"
title="Remove"
@click.stop="removeAppointment(ev.id)"
>
<UIcon name="i-heroicons-x-mark" style="width: 8px; height: 8px;" />
</button>
</div>
<p v-if="cell.events.length > 3" class="cal-month-ev-more">
+{{ cell.events.length - 3 }} more
</p>
</div>
</div>
</div>
</template>
<!-- WEEK VIEW -->
<template v-if="calView === 'week'">
<div class="cal-week">
<div v-for="day in weekDays" :key="day.label" class="cal-week-cell" :class="day.isToday ? 'cal-week-active' : ''">
<span class="text-[10px] uppercase tracking-wider" :class="day.isToday ? 'font-semibold text-[#01696f]' : 'text-[var(--text-muted)]'">{{ day.label }}</span>
<span class="cal-week-num" :class="day.isToday ? 'cal-week-num-today' : ''">{{ day.date }}</span>
<div v-if="day.events > 0" class="flex justify-center gap-0.5 mt-1">
<span v-for="n in Math.min(day.events, 4)" :key="n" class="cal-week-pip" :class="day.isToday ? 'bg-[#01696f]' : 'bg-[rgba(0,0,0,0.12)]'" />
</div>
</div>
</div>
<div class="cal-events">
<div
v-for="event in filteredEvents"
:key="event.id"
class="cal-ev"
:class="event.urgent ? 'cal-ev-urgent' : ''"
>
<div class="cal-ev-time">{{ event.time }}</div>
<div class="cal-ev-dot" :style="dotStyle(event.type)" />
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2 flex-wrap">
<p class="text-[13px] font-medium text-[var(--text-primary)]">{{ event.title }}</p>
<span v-if="event.urgent" class="cal-ev-urgent-tag">Urgent</span>
<span v-if="event.id.startsWith('user-')" class="cal-ev-user-tag">You</span>
</div>
<p v-if="event.detail" class="mt-0.5 text-[12px] leading-snug text-[var(--text-muted)]">{{ event.detail }}</p>
</div>
<button
v-if="event.id.startsWith('user-')"
type="button"
class="cal-ev-remove"
title="Remove event"
@click="removeAppointment(event.id)"
>
<UIcon name="i-heroicons-trash" style="width: 14px; height: 14px;" />
</button>
<UIcon v-else :name="typeMeta[event.type].icon" style="width: 16px; height: 16px; flex-shrink: 0; opacity: 0.4;" :style="`color: ${typeMeta[event.type].color}`" />
</div>
</div>
</template>
<!-- DAY VIEW -->
<template v-if="calView === 'day'">
<div class="cal-week">
<div v-for="day in weekDays" :key="day.label" class="cal-week-cell" :class="day.isToday ? 'cal-week-active' : ''">
<span class="text-[10px] uppercase tracking-wider" :class="day.isToday ? 'font-semibold text-[#01696f]' : 'text-[var(--text-muted)]'">{{ day.label }}</span>
<span class="cal-week-num" :class="day.isToday ? 'cal-week-num-today' : ''">{{ day.date }}</span>
<div v-if="day.events > 0" class="flex justify-center gap-0.5 mt-1">
<span v-for="n in Math.min(day.events, 4)" :key="n" class="cal-week-pip" :class="day.isToday ? 'bg-[#01696f]' : 'bg-[rgba(0,0,0,0.12)]'" />
</div>
</div>
</div>
<div v-if="filteredEvents.length === 0" class="px-6 py-12 text-center">
<p class="text-[13px] text-[var(--text-muted)]">No events match your current layers.</p>
</div>
<div v-else class="cal-events">
<div
v-for="event in filteredEvents"
:key="event.id"
class="cal-ev"
:class="event.urgent ? 'cal-ev-urgent' : ''"
>
<div class="cal-ev-time">{{ event.time }}</div>
<div class="cal-ev-dot" :style="dotStyle(event.type)" />
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2 flex-wrap">
<p class="text-[13px] font-medium text-[var(--text-primary)]">{{ event.title }}</p>
<span v-if="event.urgent" class="cal-ev-urgent-tag">Urgent</span>
<span v-if="event.id.startsWith('user-')" class="cal-ev-user-tag">You</span>
</div>
<p v-if="event.detail" class="mt-0.5 text-[12px] leading-snug text-[var(--text-muted)]">{{ event.detail }}</p>
<div class="mt-1.5 flex items-center gap-2 flex-wrap">
<span class="cal-ev-src">{{ event.source === 'gmail' ? 'Gmail' : event.source === 'system' ? 'System' : 'Manual' }}</span>
<span v-if="event.customer" class="cal-ev-cust">{{ event.customer }}</span>
</div>
</div>
<button
v-if="event.id.startsWith('user-')"
type="button"
class="cal-ev-remove"
title="Remove event"
@click="removeAppointment(event.id)"
>
<UIcon name="i-heroicons-trash" style="width: 14px; height: 14px;" />
</button>
<UIcon v-else :name="typeMeta[event.type].icon" style="width: 16px; height: 16px; flex-shrink: 0; opacity: 0.4;" :style="`color: ${typeMeta[event.type].color}`" />
</div>
</div>
</template>
</div>
<!-- ADD EVENT MODAL -->
<Teleport to="body">
<Transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition duration-150 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div v-if="addModalOpen" class="cal-modal-overlay" @click.self="addModalOpen = false">
<Transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="opacity-0 scale-95 translate-y-2"
enter-to-class="opacity-100 scale-100 translate-y-0"
leave-active-class="transition duration-150 ease-in"
leave-from-class="opacity-100 scale-100"
leave-to-class="opacity-0 scale-95"
>
<div v-if="addModalOpen" class="cal-modal">
<!-- Title bar -->
<div class="cal-modal-head">
<div class="cal-modal-head-left">
<h3>New event</h3>
</div>
<button type="button" class="cal-modal-close" @click="addModalOpen = false">
<UIcon name="i-heroicons-x-mark" style="width: 16px; height: 16px;" />
</button>
</div>
<div class="cal-modal-body">
<!-- Title (large input, Gmail-style) -->
<input
v-model="addForm.title"
type="text"
class="cal-title-input"
placeholder="Add title"
@keydown.enter="submitAppointment"
/>
<!-- Event type chips -->
<div class="cal-type-chips">
<button
v-for="(meta, type) in typeMeta"
:key="type"
type="button"
class="cal-type-chip"
:class="{ 'cal-type-chip--active': addForm.type === type }"
:style="addForm.type === type ? `--chip-c: ${meta.color}` : ''"
@click="addForm.type = type"
>
<UIcon :name="meta.icon" style="width: 13px; height: 13px;" />
{{ meta.label }}
</button>
</div>
<!-- Date & time row -->
<div class="cal-row cal-row-icon">
<UIcon name="i-heroicons-clock" class="cal-row-icon-el" />
<div class="cal-row-content">
<div class="cal-datetime-row">
<input v-model="addForm.date" type="date" class="cal-input cal-input-date" />
<template v-if="!addForm.allDay">
<input v-model="addForm.startTime" type="time" class="cal-input cal-input-time" />
<span class="cal-time-sep"></span>
<input v-model="addForm.endTime" type="time" class="cal-input cal-input-time" />
</template>
</div>
<label class="cal-checkbox-label">
<input v-model="addForm.allDay" type="checkbox" class="cal-checkbox" />
All day
</label>
</div>
</div>
<!-- Repeat -->
<div class="cal-row cal-row-icon">
<UIcon name="i-heroicons-arrow-path" class="cal-row-icon-el" />
<select v-model="addForm.repeat" class="cal-select cal-select-full">
<option value="none">Does not repeat</option>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
<option value="yearly">Annually</option>
</select>
</div>
<!-- Guests -->
<div class="cal-row cal-row-icon">
<UIcon name="i-heroicons-user-plus" class="cal-row-icon-el" />
<input v-model="addForm.guests" type="text" class="cal-input cal-input-full" placeholder="Add guests (emails, comma separated)" />
</div>
<!-- Video call toggle -->
<div class="cal-row cal-row-icon">
<UIcon name="i-heroicons-video-camera" class="cal-row-icon-el" />
<button
type="button"
class="cal-video-btn"
:class="{ 'cal-video-btn--active': addForm.videoCall }"
@click="addForm.videoCall = !addForm.videoCall"
>
<UIcon :name="addForm.videoCall ? 'i-heroicons-check-circle' : 'i-heroicons-plus-circle'" style="width: 15px; height: 15px;" />
{{ addForm.videoCall ? 'Google Meet link added' : 'Add Google Meet video conferencing' }}
</button>
</div>
<!-- Location -->
<div class="cal-row cal-row-icon">
<UIcon name="i-heroicons-map-pin" class="cal-row-icon-el" />
<input v-model="addForm.location" type="text" class="cal-input cal-input-full" placeholder="Add location" />
</div>
<!-- Notification -->
<div class="cal-row cal-row-icon">
<UIcon name="i-heroicons-bell" class="cal-row-icon-el" />
<select v-model="addForm.notification" class="cal-select cal-select-full">
<option value="0">At time of event</option>
<option value="5">5 minutes before</option>
<option value="10">10 minutes before</option>
<option value="15">15 minutes before</option>
<option value="30">30 minutes before</option>
<option value="60">1 hour before</option>
<option value="1440">1 day before</option>
</select>
</div>
<!-- Customer (insurance-specific) -->
<div class="cal-row cal-row-icon">
<UIcon name="i-heroicons-building-office" class="cal-row-icon-el" />
<input v-model="addForm.customer" type="text" class="cal-input cal-input-full" placeholder="Link to customer or policy (optional)" />
</div>
<!-- Color -->
<div class="cal-row cal-row-icon">
<UIcon name="i-heroicons-swatch" class="cal-row-icon-el" />
<div class="cal-color-row">
<button
v-for="c in eventColors"
:key="c.value"
type="button"
class="cal-color-dot"
:class="{ 'cal-color-dot--active': addForm.color === c.value }"
:style="`background: ${c.value}`"
:title="c.label"
@click="addForm.color = addForm.color === c.value ? '' : c.value"
/>
</div>
</div>
<!-- Description -->
<div class="cal-row cal-row-icon cal-row-icon-top">
<UIcon name="i-heroicons-bars-3-bottom-left" class="cal-row-icon-el" />
<textarea v-model="addForm.detail" class="cal-textarea cal-textarea-full" rows="3" placeholder="Add description or notes" />
</div>
</div>
<div class="cal-modal-foot">
<div class="cal-gcal-hint">
<UIcon name="i-heroicons-arrow-top-right-on-square" style="width: 12px; height: 12px;" />
<span>Sync with <button type="button" class="cal-gcal-link" @click="openGoogleCalendar">Google Calendar</button></span>
</div>
<div class="cal-modal-foot-actions">
<button type="button" class="cal-btn-cancel" @click="addModalOpen = false">Cancel</button>
<button
type="button"
class="cal-btn-submit"
:disabled="!addForm.title.trim()"
@click="submitAppointment"
>
Save
</button>
</div>
</div>
</div>
</Transition>
</div>
</Transition>
</Teleport>
</div>
</template>
<style scoped>
.cal-page {
display: flex;
flex-direction: column;
gap: 20px;
padding-bottom: 3rem;
}
/* ── Action buttons ── */
.cal-action-btn {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 6px 12px;
border-radius: 8px;
border: 1px solid rgba(0,0,0,0.08);
font-size: 12px;
font-weight: 500;
color: var(--text-muted);
background: #fff;
cursor: pointer;
transition: all 150ms ease;
}
.cal-action-btn:hover { border-color: rgba(0,0,0,0.15); color: var(--text-primary); }
.cal-action-primary {
background: #01696f;
color: #fff;
border-color: #01696f;
}
.cal-action-primary:hover {
background: #015a5f;
color: #fff;
border-color: #015a5f;
}
/* ── Controls ── */
.cal-ctrl-btn {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 5px 10px;
border-radius: 8px;
border: 1px solid rgba(0,0,0,0.08);
font-size: 12px;
font-weight: 500;
color: var(--text-muted);
background: transparent;
cursor: pointer;
transition: all 150ms ease;
}
.cal-ctrl-btn:hover { border-color: rgba(0,0,0,0.15); color: var(--text-primary); }
.cal-ctrl-count {
font-size: 10px; font-weight: 600; padding: 0 5px;
border-radius: 9999px; background: rgba(1,105,111,0.1); color: #01696f;
}
.cal-toggle {
display: inline-flex; gap: 1px; padding: 2px; border-radius: 8px; background: rgba(0,0,0,0.04);
}
.cal-toggle-btn {
padding: 4px 10px; border-radius: 6px; font-size: 12px; font-weight: 500;
border: none; cursor: pointer; transition: all 150ms ease;
}
.cal-toggle-on { background: #fff; color: var(--text-primary); box-shadow: 0 1px 2px rgba(0,0,0,0.06); }
.cal-toggle-off { background: transparent; color: var(--text-muted); }
/* ── Layers row ── */
.cal-layers-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.cal-layer-pill {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 10px 3px 8px;
border-radius: 9999px;
border: 1px solid color-mix(in srgb, var(--lc) 20%, transparent);
background: color-mix(in srgb, var(--lc) 5%, transparent);
font-size: 11px;
font-weight: 500;
color: var(--lc);
cursor: pointer;
transition: all 150ms ease;
white-space: nowrap;
}
.cal-layer-pill:hover {
background: color-mix(in srgb, var(--lc) 10%, transparent);
border-color: color-mix(in srgb, var(--lc) 30%, transparent);
}
.cal-layer-dot {
width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0;
}
/* ── Layer dropdown ── */
.cal-layer-dropdown {
position: absolute;
right: 0;
top: calc(100% + 6px);
z-index: 30;
width: 260px;
border-radius: 12px;
border: 1px solid rgba(0,0,0,0.08);
background: #fff;
box-shadow: 0 8px 30px rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.06);
overflow: hidden;
}
.cal-layer-dropdown-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px 0;
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
}
.cal-layer-action {
font-size: 10px;
font-weight: 500;
color: #01696f;
background: none;
border: none;
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
transition: background 150ms ease;
}
.cal-layer-action:hover { background: rgba(1,105,111,0.06); }
.cal-layer-dropdown-hint {
padding: 4px 14px 8px;
font-size: 11px;
color: #8a8a86;
}
.cal-layer-list {
list-style: none;
margin: 0;
padding: 0;
}
.cal-layer-item {
display: flex;
align-items: center;
gap: 8px;
padding: 9px 14px;
cursor: pointer;
transition: background 100ms ease;
border-top: 1px solid rgba(0,0,0,0.03);
}
.cal-layer-item:hover { background: rgba(0,0,0,0.02); }
.cal-layer-item-on { background: rgba(1,105,111,0.02); }
.cal-layer-item-on:hover { background: rgba(1,105,111,0.04); }
.cal-layer-check {
width: 16px; height: 16px;
border-radius: 4px;
border: 1.5px solid rgba(0,0,0,0.15);
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
transition: all 150ms ease;
}
/* ── Badges ── */
.cal-badge {
display: inline-flex; align-items: center; gap: 3px;
padding: 2px 7px; border-radius: 6px; font-size: 10px; font-weight: 500;
}
.cal-badge-gmail { background: rgba(219,68,55,0.06); color: #b33a2e; }
.cal-badge-system { background: rgba(1,105,111,0.06); color: #01696f; }
/* ── Card ── */
.cal-card {
border-radius: 12px; border: 1px solid rgba(0,0,0,0.06);
background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.03); overflow: hidden;
}
/* ══════════════════════════════════════════════════════
MONTH VIEW
══════════════════════════════════════════════════════ */
.cal-month-nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 20px;
border-bottom: 1px solid rgba(0,0,0,0.06);
}
.cal-month-arrow {
display: flex; align-items: center; justify-content: center;
width: 28px; height: 28px; border-radius: 6px;
border: none; background: transparent; color: #8a8a86;
cursor: pointer; transition: all 150ms ease;
}
.cal-month-arrow:hover { background: rgba(0,0,0,0.04); color: var(--text-primary); }
.cal-today-btn {
font-size: 11px; font-weight: 500; color: #01696f;
padding: 2px 8px; border-radius: 4px; border: none;
background: rgba(1,105,111,0.06); cursor: pointer;
transition: background 150ms ease;
}
.cal-today-btn:hover { background: rgba(1,105,111,0.1); }
.cal-month-header {
display: grid;
grid-template-columns: repeat(7, 1fr);
border-bottom: 1px solid rgba(0,0,0,0.06);
}
.cal-month-dow {
padding: 8px 0;
text-align: center;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #8a8a86;
}
.cal-month-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
}
.cal-month-cell {
min-height: 90px;
padding: 6px 8px;
border-right: 1px solid rgba(0,0,0,0.04);
border-bottom: 1px solid rgba(0,0,0,0.04);
transition: background 100ms ease;
position: relative;
overflow: hidden;
}
.cal-month-cell:nth-child(7n) { border-right: none; }
.cal-month-cell:hover { background: rgba(0,0,0,0.01); }
.cal-month-cell-other { opacity: 0.35; }
.cal-month-cell-today { background: rgba(1,105,111,0.02); }
.cal-month-cell-today:hover { background: rgba(1,105,111,0.04); }
.cal-month-cell-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.cal-cell-add {
display: flex;
align-items: center;
justify-content: center;
width: 16px; height: 16px;
border-radius: 4px;
border: none;
background: transparent;
color: #8a8a86;
cursor: pointer;
opacity: 0;
transition: all 150ms ease;
}
.cal-month-cell:hover .cal-cell-add { opacity: 1; }
.cal-cell-add:hover { background: rgba(1,105,111,0.1); color: #01696f; }
.cal-month-date {
display: inline-flex; align-items: center; justify-content: center;
width: 24px; height: 24px; border-radius: 50%;
font-size: 12px; font-weight: 500; color: var(--text-primary);
}
.cal-month-date-today {
background: #01696f; color: #ffffff; font-weight: 600;
}
.cal-month-events {
display: flex; flex-direction: column; gap: 2px; margin-top: 4px;
}
.cal-month-ev {
display: flex; align-items: center; gap: 4px;
padding: 2px 4px; border-radius: 4px;
transition: background 100ms ease;
cursor: default;
}
.cal-month-ev:hover { background: rgba(0,0,0,0.03); }
.cal-month-ev-urgent { background: rgba(193,56,56,0.03); }
.cal-month-ev-dot {
width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0;
}
.cal-month-ev-text {
font-size: 10px; font-weight: 500; color: var(--text-primary);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
min-width: 0; flex: 1;
}
.cal-month-ev-more {
font-size: 9px; font-weight: 600; color: #01696f;
padding: 1px 4px;
}
.cal-ev-remove-mini {
display: flex;
align-items: center;
justify-content: center;
width: 14px; height: 14px;
border-radius: 3px;
border: none;
background: transparent;
color: #c13838;
cursor: pointer;
opacity: 0;
flex-shrink: 0;
transition: all 120ms ease;
}
.cal-month-ev:hover .cal-ev-remove-mini { opacity: 0.6; }
.cal-ev-remove-mini:hover { opacity: 1 !important; background: rgba(193,56,56,0.08); }
/* ══════════════════════════════════════════════════════
WEEK STRIP
══════════════════════════════════════════════════════ */
.cal-week {
display: grid; grid-template-columns: repeat(7, 1fr);
border-bottom: 1px solid rgba(0,0,0,0.06);
}
.cal-week-cell {
display: flex; flex-direction: column; align-items: center;
padding: 12px 4px 10px; transition: background 100ms ease;
}
.cal-week-active { background: rgba(1,105,111,0.03); }
.cal-week-num {
font-size: 16px; font-weight: 600; margin-top: 2px; color: var(--text-primary);
width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; border-radius: 50%;
}
.cal-week-num-today { background: #01696f; color: #fff; }
.cal-week-pip { width: 4px; height: 4px; border-radius: 50%; }
/* ══════════════════════════════════════════════════════
DAY EVENT LIST
══════════════════════════════════════════════════════ */
.cal-events { display: flex; flex-direction: column; }
.cal-ev {
display: flex; align-items: flex-start; gap: 10px;
padding: 16px 20px; border-bottom: 1px solid rgba(0,0,0,0.04);
transition: background 100ms ease;
}
.cal-ev:last-child { border-bottom: none; }
.cal-ev:hover { background: rgba(0,0,0,0.015); }
.cal-ev-urgent { background: rgba(193,56,56,0.02); }
.cal-ev-urgent:hover { background: rgba(193,56,56,0.04); }
.cal-ev-time {
width: 68px; flex-shrink: 0; font-size: 12px; font-weight: 500;
color: var(--text-muted); padding-top: 1px; font-variant-numeric: tabular-nums;
}
.cal-ev-dot {
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; margin-top: 5px;
}
.cal-ev-urgent-tag {
font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 9999px;
background: rgba(193,56,56,0.08); color: #c13838; flex-shrink: 0;
}
.cal-ev-user-tag {
font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 9999px;
background: rgba(1,105,111,0.08); color: #01696f; flex-shrink: 0;
}
.cal-ev-src {
font-size: 10px; font-weight: 500; padding: 1px 5px; border-radius: 4px;
background: rgba(0,0,0,0.04); color: var(--text-muted);
}
.cal-ev-cust {
font-size: 10px; font-weight: 500; padding: 1px 5px; border-radius: 4px;
background: rgba(1,105,111,0.06); color: #01696f;
}
.cal-ev-remove {
display: flex;
align-items: center;
justify-content: center;
width: 28px; height: 28px;
border-radius: 6px;
border: none;
background: transparent;
color: #c13838;
cursor: pointer;
opacity: 0;
flex-shrink: 0;
transition: all 150ms ease;
}
.cal-ev:hover .cal-ev-remove { opacity: 0.5; }
.cal-ev-remove:hover { opacity: 1 !important; background: rgba(193,56,56,0.06); }
/* ── Legend ── */
.cal-legend {
padding: 16px 20px; border-radius: 10px;
border: 1px solid rgba(0,0,0,0.06); background: var(--surface);
}
.cal-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
/* ══════════════════════════════════════════════════════
ADD EVENT MODAL
══════════════════════════════════════════════════════ */
.cal-modal-overlay {
position: fixed;
inset: 0;
z-index: 100;
background: rgba(0,0,0,0.25);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
backdrop-filter: blur(2px);
}
.cal-modal {
width: 100%;
max-width: 560px;
border-radius: 12px;
background: #fff;
box-shadow: 0 20px 60px rgba(0,0,0,0.15), 0 4px 16px rgba(0,0,0,0.08);
overflow: hidden;
max-height: calc(100vh - 40px);
display: flex;
flex-direction: column;
}
.cal-modal-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 20px 0;
flex-shrink: 0;
}
.cal-modal-head h3 {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
}
.cal-modal-close {
display: flex;
align-items: center;
justify-content: center;
width: 28px; height: 28px;
border-radius: 6px;
border: none;
background: transparent;
color: #8a8a86;
cursor: pointer;
transition: all 150ms ease;
}
.cal-modal-close:hover { background: rgba(0,0,0,0.05); color: var(--text-primary); }
.cal-modal-body {
padding: 16px 20px;
display: flex;
flex-direction: column;
gap: 12px;
overflow-y: auto;
flex: 1;
min-height: 0;
}
/* ── Gmail-style title input ── */
.cal-title-input {
width: 100%;
border: none;
outline: none;
font-size: 20px;
font-weight: 500;
color: var(--text-primary);
padding: 4px 0 8px;
border-bottom: 2px solid rgba(0,0,0,0.06);
background: transparent;
transition: border-color 200ms ease;
}
.cal-title-input::placeholder { color: #b0b0ac; font-weight: 400; }
.cal-title-input:focus { border-bottom-color: #01696f; }
/* ── Event type chips ── */
.cal-type-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.cal-type-chip {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 5px 11px;
border-radius: 9999px;
border: 1px solid rgba(0,0,0,0.08);
background: #fff;
font-size: 12px;
font-weight: 500;
color: var(--text-muted);
cursor: pointer;
transition: all 150ms ease;
}
.cal-type-chip:hover { border-color: rgba(0,0,0,0.15); color: var(--text-primary); }
.cal-type-chip--active {
background: color-mix(in srgb, var(--chip-c, #01696f) 8%, transparent);
border-color: color-mix(in srgb, var(--chip-c, #01696f) 25%, transparent);
color: var(--chip-c, #01696f);
}
/* ── Form rows with icon ── */
.cal-row { display: flex; align-items: center; gap: 10px; }
.cal-row-icon { display: flex; align-items: center; gap: 10px; }
.cal-row-icon-el {
width: 18px; height: 18px;
color: #8a8a86;
flex-shrink: 0;
}
.cal-row-icon-top { align-items: flex-start; }
.cal-row-icon-top .cal-row-icon-el { margin-top: 8px; }
.cal-row-content {
display: flex;
flex-direction: column;
gap: 6px;
flex: 1;
min-width: 0;
}
/* ── Date & time row ── */
.cal-datetime-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.cal-input-date { min-width: 140px; flex: 1; }
.cal-input-time { width: 110px; }
.cal-time-sep {
font-size: 13px;
color: #8a8a86;
flex-shrink: 0;
}
.cal-input-full { flex: 1; min-width: 0; }
.cal-select-full { flex: 1; min-width: 0; }
.cal-textarea-full { flex: 1; min-width: 0; }
/* ── Checkbox ── */
.cal-checkbox-label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 500;
color: var(--text-muted);
cursor: pointer;
}
.cal-checkbox {
width: 14px; height: 14px;
border-radius: 3px;
accent-color: #01696f;
cursor: pointer;
}
/* ── Video call button ── */
.cal-video-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 7px 12px;
border-radius: 8px;
border: 1px solid rgba(0,0,0,0.08);
background: #fff;
font-size: 12px;
font-weight: 500;
color: var(--text-muted);
cursor: pointer;
transition: all 150ms ease;
flex: 1;
}
.cal-video-btn:hover { border-color: rgba(0,0,0,0.15); }
.cal-video-btn--active {
background: rgba(1,105,111,0.04);
border-color: rgba(1,105,111,0.2);
color: #01696f;
}
/* ── Color dots ── */
.cal-color-row {
display: flex;
align-items: center;
gap: 8px;
}
.cal-color-dot {
width: 20px; height: 20px;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
transition: all 150ms ease;
position: relative;
}
.cal-color-dot:hover { transform: scale(1.15); }
.cal-color-dot--active {
border-color: var(--text-primary);
box-shadow: 0 0 0 2px #fff, 0 0 0 3.5px currentColor;
}
/* ── Modal footer ── */
.cal-modal-foot-actions {
display: flex;
align-items: center;
gap: 8px;
}
.cal-field {
display: flex;
flex-direction: column;
gap: 4px;
}
.cal-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: #8a8a86;
}
.cal-input {
padding: 7px 10px;
border-radius: 8px;
border: 1px solid rgba(0,0,0,0.1);
font-size: 13px;
color: var(--text-primary);
background: #fff;
outline: none;
transition: border-color 150ms ease;
}
.cal-input:focus { border-color: #01696f; }
.cal-select {
padding: 7px 10px;
border-radius: 8px;
border: 1px solid rgba(0,0,0,0.1);
font-size: 13px;
color: var(--text-primary);
background: #fff;
outline: none;
transition: border-color 150ms ease;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' fill='none'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%238a8a86' stroke-width='1.2' stroke-linecap='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
padding-right: 28px;
}
.cal-select:focus { border-color: #01696f; }
.cal-textarea {
padding: 7px 10px;
border-radius: 8px;
border: 1px solid rgba(0,0,0,0.1);
font-size: 13px;
color: var(--text-primary);
background: #fff;
outline: none;
resize: vertical;
font-family: inherit;
transition: border-color 150ms ease;
}
.cal-textarea:focus { border-color: #01696f; }
.cal-gcal-hint {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 10px;
border-radius: 8px;
background: rgba(0,0,0,0.02);
font-size: 11px;
color: #8a8a86;
}
.cal-gcal-link {
color: #01696f;
font-weight: 500;
background: none;
border: none;
cursor: pointer;
text-decoration: underline;
text-decoration-color: rgba(1,105,111,0.3);
text-underline-offset: 2px;
transition: color 150ms ease;
}
.cal-gcal-link:hover { color: #015a5f; }
.cal-modal-foot {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
padding: 12px 20px 16px;
border-top: 1px solid rgba(0,0,0,0.06);
}
.cal-btn-cancel {
padding: 7px 14px;
border-radius: 8px;
border: 1px solid rgba(0,0,0,0.08);
background: #fff;
font-size: 13px;
font-weight: 500;
color: var(--text-muted);
cursor: pointer;
transition: all 150ms ease;
}
.cal-btn-cancel:hover { border-color: rgba(0,0,0,0.15); color: var(--text-primary); }
.cal-btn-submit {
padding: 7px 16px;
border-radius: 8px;
border: none;
background: #01696f;
font-size: 13px;
font-weight: 600;
color: #fff;
cursor: pointer;
transition: all 150ms ease;
}
.cal-btn-submit:hover { background: #015a5f; }
.cal-btn-submit:disabled { opacity: 0.4; cursor: not-allowed; }
/* ── Responsive ── */
@media (max-width: 640px) {
.cal-month-cell { min-height: 60px; padding: 4px; }
.cal-month-ev-text { font-size: 9px; }
.cal-month-date { width: 20px; height: 20px; font-size: 11px; }
.cal-layers-row { flex-direction: column; align-items: flex-start; }
}
</style>