refactor(frontend): extract calendar scene into workspace component
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
import { nextTick, onBeforeUnmount, onMounted } from "vue";
|
import { nextTick, onBeforeUnmount, onMounted } from "vue";
|
||||||
import CrmAuthLoading from "~~/app/components/workspace/auth/CrmAuthLoading.vue";
|
import CrmAuthLoading from "~~/app/components/workspace/auth/CrmAuthLoading.vue";
|
||||||
import CrmAuthLoginForm from "~~/app/components/workspace/auth/CrmAuthLoginForm.vue";
|
import CrmAuthLoginForm from "~~/app/components/workspace/auth/CrmAuthLoginForm.vue";
|
||||||
|
import CrmCalendarPanel from "~~/app/components/workspace/calendar/CrmCalendarPanel.vue";
|
||||||
import CrmDocumentsPanel from "~~/app/components/workspace/documents/CrmDocumentsPanel.vue";
|
import CrmDocumentsPanel from "~~/app/components/workspace/documents/CrmDocumentsPanel.vue";
|
||||||
import CrmWorkspaceTopbar from "~~/app/components/workspace/header/CrmWorkspaceTopbar.vue";
|
import CrmWorkspaceTopbar from "~~/app/components/workspace/header/CrmWorkspaceTopbar.vue";
|
||||||
import CrmPilotSidebar from "~~/app/components/workspace/pilot/CrmPilotSidebar.vue";
|
import CrmPilotSidebar from "~~/app/components/workspace/pilot/CrmPilotSidebar.vue";
|
||||||
@@ -2364,6 +2365,41 @@ const calendarZoomOverlayRef = ref<HTMLElement | null>(null);
|
|||||||
const calendarHoveredMonthIndex = ref<number | null>(null);
|
const calendarHoveredMonthIndex = ref<number | null>(null);
|
||||||
const calendarHoveredWeekStartKey = ref("");
|
const calendarHoveredWeekStartKey = ref("");
|
||||||
const calendarHoveredDayKey = ref("");
|
const calendarHoveredDayKey = ref("");
|
||||||
|
|
||||||
|
function setCalendarContentWrapRef(element: HTMLElement | null) {
|
||||||
|
calendarContentWrapRef.value = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCalendarContentScrollRef(element: HTMLElement | null) {
|
||||||
|
calendarContentScrollRef.value = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCalendarSceneRef(element: HTMLElement | null) {
|
||||||
|
calendarSceneRef.value = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCalendarZoomOverlayRef(element: HTMLElement | null) {
|
||||||
|
calendarZoomOverlayRef.value = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCalendarHoveredMonthIndex(value: number | null) {
|
||||||
|
calendarHoveredMonthIndex.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCalendarHoveredWeekStartKey(value: string) {
|
||||||
|
calendarHoveredWeekStartKey.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCalendarHoveredDayKey(value: string) {
|
||||||
|
calendarHoveredDayKey.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCalendarSceneMouseLeave() {
|
||||||
|
calendarHoveredMonthIndex.value = null;
|
||||||
|
calendarHoveredWeekStartKey.value = "";
|
||||||
|
calendarHoveredDayKey.value = "";
|
||||||
|
clearCalendarZoomPrime();
|
||||||
|
}
|
||||||
const calendarZoomOverlay = ref<{ active: boolean } & CalendarRect>({
|
const calendarZoomOverlay = ref<{ active: boolean } & CalendarRect>({
|
||||||
active: false,
|
active: false,
|
||||||
left: 0,
|
left: 0,
|
||||||
@@ -4820,250 +4856,57 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
class="min-h-0 flex-1"
|
class="min-h-0 flex-1"
|
||||||
:class="selectedTab === 'documents' || (selectedTab === 'communications' && peopleLeftMode === 'contacts') ? 'px-0 pt-0 pb-0' : 'px-3 pt-3 pb-0 md:px-4 md:pt-4 md:pb-0'"
|
:class="selectedTab === 'documents' || (selectedTab === 'communications' && peopleLeftMode === 'contacts') ? 'px-0 pt-0 pb-0' : 'px-3 pt-3 pb-0 md:px-4 md:pt-4 md:pb-0'"
|
||||||
>
|
>
|
||||||
<section
|
<CrmCalendarPanel
|
||||||
v-if="selectedTab === 'communications' && peopleLeftMode === 'calendar'"
|
v-if="selectedTab === 'communications' && peopleLeftMode === 'calendar'"
|
||||||
class="relative flex h-full min-h-0 flex-col gap-3"
|
:context-picker-enabled="contextPickerEnabled"
|
||||||
:class="[
|
:has-context-scope="hasContextScope"
|
||||||
contextPickerEnabled ? 'context-scope-block context-scope-block-active' : '',
|
:toggle-context-scope="toggleContextScope"
|
||||||
hasContextScope('calendar') ? 'context-scope-block-selected' : '',
|
:context-scope-label="contextScopeLabel"
|
||||||
]"
|
:set-today="setToday"
|
||||||
@click="toggleContextScope('calendar')"
|
:calendar-period-label="calendarPeriodLabel"
|
||||||
>
|
:calendar-zoom-level-index="calendarZoomLevelIndex"
|
||||||
<span
|
:on-calendar-zoom-slider-input="onCalendarZoomSliderInput"
|
||||||
v-if="contextPickerEnabled"
|
:focused-calendar-event="focusedCalendarEvent"
|
||||||
class="context-scope-label"
|
:format-day="formatDay"
|
||||||
>{{ contextScopeLabel('calendar') }}</span>
|
:format-time="formatTime"
|
||||||
<div class="grid grid-cols-[auto_1fr_auto] items-center gap-2">
|
:set-calendar-content-wrap-ref="setCalendarContentWrapRef"
|
||||||
<div class="flex items-center gap-1">
|
:shift-calendar="shiftCalendar"
|
||||||
<button class="btn btn-xs" @click="setToday">Today</button>
|
:set-calendar-content-scroll-ref="setCalendarContentScrollRef"
|
||||||
</div>
|
:on-calendar-hierarchy-wheel="onCalendarHierarchyWheel"
|
||||||
|
:set-calendar-scene-ref="setCalendarSceneRef"
|
||||||
<div class="text-center text-sm font-medium">
|
:normalized-calendar-view="normalizedCalendarView"
|
||||||
{{ calendarPeriodLabel }}
|
:calendar-scene-masked="calendarSceneMasked"
|
||||||
</div>
|
:calendar-scene-transform-style="calendarSceneTransformStyle"
|
||||||
|
:on-calendar-scene-mouse-leave="onCalendarSceneMouseLeave"
|
||||||
<div class="justify-self-end calendar-zoom-inline" @click.stop>
|
:calendar-view="calendarView"
|
||||||
<input
|
:year-months="yearMonths"
|
||||||
class="calendar-zoom-slider"
|
:calendar-cursor-month="calendarCursor.getMonth()"
|
||||||
type="range"
|
:calendar-hovered-month-index="calendarHoveredMonthIndex"
|
||||||
min="0"
|
:set-calendar-hovered-month-index="setCalendarHoveredMonthIndex"
|
||||||
max="3"
|
:calendar-zoom-prime-token="calendarZoomPrimeToken"
|
||||||
step="1"
|
:calendar-prime-month-token="calendarPrimeMonthToken"
|
||||||
:value="calendarZoomLevelIndex"
|
:calendar-prime-style="calendarPrimeStyle"
|
||||||
aria-label="Calendar zoom level"
|
:zoom-to-month="zoomToMonth"
|
||||||
@input="onCalendarZoomSliderInput"
|
:open-thread-from-calendar-item="openThreadFromCalendarItem"
|
||||||
>
|
:month-rows="monthRows"
|
||||||
<div class="calendar-zoom-marks" aria-hidden="true">
|
:calendar-hovered-week-start-key="calendarHoveredWeekStartKey"
|
||||||
<span
|
:set-calendar-hovered-week-start-key="setCalendarHoveredWeekStartKey"
|
||||||
v-for="index in 4"
|
:calendar-prime-week-token="calendarPrimeWeekToken"
|
||||||
:key="`calendar-zoom-mark-${index}`"
|
:selected-date-key="selectedDateKey"
|
||||||
class="calendar-zoom-mark"
|
:month-cell-has-focused-event="monthCellHasFocusedEvent"
|
||||||
:class="calendarZoomLevelIndex === index - 1 ? 'calendar-zoom-mark-active' : ''"
|
:calendar-hovered-day-key="calendarHoveredDayKey"
|
||||||
|
:set-calendar-hovered-day-key="setCalendarHoveredDayKey"
|
||||||
|
:pick-date="pickDate"
|
||||||
|
:month-cell-events="monthCellEvents"
|
||||||
|
:is-review-highlighted-event="isReviewHighlightedEvent"
|
||||||
|
:week-days="weekDays"
|
||||||
|
:calendar-prime-day-token="calendarPrimeDayToken"
|
||||||
|
:selected-day-events="selectedDayEvents"
|
||||||
|
:calendar-zoom-overlay="calendarZoomOverlay"
|
||||||
|
:set-calendar-zoom-overlay-ref="setCalendarZoomOverlayRef"
|
||||||
|
:calendar-zoom-overlay-style="calendarZoomOverlayStyle"
|
||||||
|
:calendar-zoom-ghost="calendarZoomGhost"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<article
|
|
||||||
v-if="focusedCalendarEvent"
|
|
||||||
class="rounded-xl border border-success/50 bg-success/10 px-3 py-2"
|
|
||||||
>
|
|
||||||
<p class="text-xs font-semibold uppercase tracking-wide text-success/80">Review focus event</p>
|
|
||||||
<p class="text-sm font-medium text-base-content">{{ focusedCalendarEvent.title }}</p>
|
|
||||||
<p class="text-xs text-base-content/70">
|
|
||||||
{{ formatDay(focusedCalendarEvent.start) }} · {{ formatTime(focusedCalendarEvent.start) }} - {{ formatTime(focusedCalendarEvent.end) }}
|
|
||||||
</p>
|
|
||||||
<p class="mt-1 text-xs text-base-content/80">{{ focusedCalendarEvent.note || "No note" }}</p>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<div ref="calendarContentWrapRef" class="calendar-content-wrap min-h-0 flex-1">
|
|
||||||
<button
|
|
||||||
class="calendar-side-nav calendar-side-nav-left"
|
|
||||||
type="button"
|
|
||||||
title="Previous period"
|
|
||||||
@click="shiftCalendar(-1)"
|
|
||||||
>
|
|
||||||
<span>←</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="calendar-side-nav calendar-side-nav-right"
|
|
||||||
type="button"
|
|
||||||
title="Next period"
|
|
||||||
@click="shiftCalendar(1)"
|
|
||||||
>
|
|
||||||
<span>→</span>
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
ref="calendarContentScrollRef"
|
|
||||||
class="calendar-content-scroll min-h-0 h-full overflow-y-auto pr-1"
|
|
||||||
@wheel.prevent="onCalendarHierarchyWheel"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref="calendarSceneRef"
|
|
||||||
:class="[
|
|
||||||
'calendar-scene',
|
|
||||||
normalizedCalendarView === 'day' ? 'cursor-zoom-out' : 'cursor-zoom-in',
|
|
||||||
calendarSceneMasked ? 'calendar-scene-hidden' : '',
|
|
||||||
]"
|
|
||||||
:style="calendarSceneTransformStyle"
|
|
||||||
@mouseleave="calendarHoveredMonthIndex = null; calendarHoveredWeekStartKey = ''; calendarHoveredDayKey = ''; clearCalendarZoomPrime()"
|
|
||||||
>
|
|
||||||
<div class="grid gap-2" :class="calendarView === 'year' ? 'sm:grid-cols-2 xl:grid-cols-3' : 'grid-cols-1'">
|
|
||||||
<article
|
|
||||||
v-for="item in yearMonths"
|
|
||||||
:key="`year-month-${item.monthIndex}`"
|
|
||||||
v-show="calendarView === 'year' || item.monthIndex === calendarCursor.getMonth()"
|
|
||||||
class="group relative rounded-xl border border-base-300 p-3 text-left transition calendar-hover-targetable"
|
|
||||||
:class="[
|
|
||||||
calendarView === 'year' ? 'hover:border-primary/50 hover:bg-primary/5 cursor-zoom-in' : 'cursor-default min-h-[26rem] bg-base-100',
|
|
||||||
calendarHoveredMonthIndex === item.monthIndex ? 'calendar-hover-target' : '',
|
|
||||||
calendarZoomPrimeToken === calendarPrimeMonthToken(item.monthIndex) ? 'calendar-zoom-prime-active' : '',
|
|
||||||
]"
|
|
||||||
:style="calendarPrimeStyle(calendarPrimeMonthToken(item.monthIndex))"
|
|
||||||
:data-calendar-month-index="item.monthIndex"
|
|
||||||
@mouseenter="calendarHoveredMonthIndex = item.monthIndex"
|
|
||||||
@click="calendarView === 'year' ? zoomToMonth(item.monthIndex) : undefined"
|
|
||||||
>
|
|
||||||
<p class="font-medium">{{ item.label }}</p>
|
|
||||||
<p class="text-xs text-base-content/60">{{ item.count }} events</p>
|
|
||||||
<button
|
|
||||||
v-if="calendarView === 'year' && item.first"
|
|
||||||
class="mt-1 block w-full text-left text-xs text-base-content/70 hover:underline"
|
|
||||||
@click.stop="openThreadFromCalendarItem(item.first)"
|
|
||||||
>
|
|
||||||
{{ formatDay(item.first.start) }} · {{ item.first.title }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="item.monthIndex === calendarCursor.getMonth() && (calendarView === 'month' || calendarView === 'agenda')"
|
|
||||||
class="mt-3 space-y-1"
|
|
||||||
>
|
|
||||||
<div class="grid grid-cols-7 gap-1 text-center text-xs font-semibold text-base-content/60">
|
|
||||||
<span>Sun</span>
|
|
||||||
<span>Mon</span>
|
|
||||||
<span>Tue</span>
|
|
||||||
<span>Wed</span>
|
|
||||||
<span>Thu</span>
|
|
||||||
<span>Fri</span>
|
|
||||||
<span>Sat</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-1">
|
|
||||||
<div
|
|
||||||
v-for="row in monthRows"
|
|
||||||
:key="row.key"
|
|
||||||
class="group relative calendar-hover-targetable"
|
|
||||||
:class="[
|
|
||||||
calendarHoveredWeekStartKey === row.startKey ? 'calendar-hover-target' : '',
|
|
||||||
calendarZoomPrimeToken === calendarPrimeWeekToken(row.startKey) ? 'calendar-zoom-prime-active' : '',
|
|
||||||
]"
|
|
||||||
:style="calendarPrimeStyle(calendarPrimeWeekToken(row.startKey))"
|
|
||||||
:data-calendar-week-start-key="row.startKey"
|
|
||||||
@mouseenter="calendarHoveredWeekStartKey = row.startKey"
|
|
||||||
>
|
|
||||||
<div class="grid grid-cols-7 gap-1">
|
|
||||||
<button
|
|
||||||
v-for="cell in row.cells"
|
|
||||||
:key="cell.key"
|
|
||||||
class="group relative min-h-24 rounded-lg border p-1 text-left"
|
|
||||||
:class="[
|
|
||||||
cell.inMonth ? 'border-base-300 bg-base-100' : 'border-base-200 bg-base-200/40 text-base-content/40',
|
|
||||||
selectedDateKey === cell.key ? 'border-primary bg-primary/5' : '',
|
|
||||||
monthCellHasFocusedEvent(cell.events) ? 'border-success/60 bg-success/10' : '',
|
|
||||||
]"
|
|
||||||
:data-calendar-day-key="cell.key"
|
|
||||||
@mouseenter="calendarHoveredDayKey = cell.key"
|
|
||||||
@click="pickDate(cell.key)"
|
|
||||||
>
|
|
||||||
<p class="mb-1 text-xs font-semibold">{{ cell.day }}</p>
|
|
||||||
<button
|
|
||||||
v-for="event in monthCellEvents(cell.events)"
|
|
||||||
:key="event.id"
|
|
||||||
class="block w-full truncate rounded px-1 text-left text-[10px] text-base-content/70 transition hover:underline"
|
|
||||||
:class="isReviewHighlightedEvent(event.id) ? 'bg-success/20 text-success-content ring-1 ring-success/40' : ''"
|
|
||||||
@click.stop="openThreadFromCalendarItem(event)"
|
|
||||||
>
|
|
||||||
{{ formatTime(event.start) }} {{ event.title }}
|
|
||||||
</button>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-else-if="item.monthIndex === calendarCursor.getMonth() && calendarView === 'week'"
|
|
||||||
class="mt-3 calendar-week-scroll overflow-x-auto pb-1"
|
|
||||||
>
|
|
||||||
<div class="calendar-week-grid">
|
|
||||||
<article
|
|
||||||
v-for="day in weekDays"
|
|
||||||
:key="day.key"
|
|
||||||
class="group relative flex min-h-[18rem] flex-col rounded-xl border border-base-300 bg-base-100 p-2.5 cursor-zoom-in calendar-hover-targetable"
|
|
||||||
:class="[
|
|
||||||
selectedDateKey === day.key ? 'border-primary bg-primary/5' : '',
|
|
||||||
calendarHoveredDayKey === day.key ? 'calendar-hover-target' : '',
|
|
||||||
calendarZoomPrimeToken === calendarPrimeDayToken(day.key) ? 'calendar-zoom-prime-active' : '',
|
|
||||||
]"
|
|
||||||
:style="calendarPrimeStyle(calendarPrimeDayToken(day.key))"
|
|
||||||
:data-calendar-day-key="day.key"
|
|
||||||
@mouseenter="calendarHoveredDayKey = day.key"
|
|
||||||
@click="pickDate(day.key)"
|
|
||||||
>
|
|
||||||
<div class="mb-2 flex items-start justify-between gap-2">
|
|
||||||
<p class="text-sm font-semibold leading-tight">{{ day.label }} {{ day.day }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-1.5">
|
|
||||||
<button
|
|
||||||
v-for="event in day.events"
|
|
||||||
:key="event.id"
|
|
||||||
class="block w-full rounded-md px-2 py-1.5 text-left text-xs"
|
|
||||||
:class="isReviewHighlightedEvent(event.id) ? 'bg-success/20 ring-1 ring-success/45' : 'bg-base-200 hover:bg-base-300/80'"
|
|
||||||
@click.stop="openThreadFromCalendarItem(event)"
|
|
||||||
>
|
|
||||||
{{ formatTime(event.start) }} - {{ event.title }} ({{ event.contact }})
|
|
||||||
</button>
|
|
||||||
<p v-if="day.events.length === 0" class="pt-1 text-xs text-base-content/50">No events</p>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-else-if="item.monthIndex === calendarCursor.getMonth() && calendarView === 'day'"
|
|
||||||
class="mt-3 space-y-2"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
v-for="event in selectedDayEvents"
|
|
||||||
:key="event.id"
|
|
||||||
class="block w-full rounded-xl border border-base-300 p-3 text-left transition hover:bg-base-200/60"
|
|
||||||
:class="isReviewHighlightedEvent(event.id) ? 'border-success/60 bg-success/10' : ''"
|
|
||||||
@click="openThreadFromCalendarItem(event)"
|
|
||||||
>
|
|
||||||
<p class="font-medium">{{ event.title }}</p>
|
|
||||||
<p class="text-xs text-base-content/60">{{ event.contact }}</p>
|
|
||||||
<p class="text-xs text-base-content/60">{{ formatTime(event.start) }} - {{ formatTime(event.end) }}</p>
|
|
||||||
<p class="mt-1 text-sm text-base-content/80">{{ event.note }}</p>
|
|
||||||
</button>
|
|
||||||
<p v-if="selectedDayEvents.length === 0" class="text-sm text-base-content/60">No events on this day.</p>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="calendarZoomOverlay.active"
|
|
||||||
ref="calendarZoomOverlayRef"
|
|
||||||
class="calendar-zoom-overlay"
|
|
||||||
:style="calendarZoomOverlayStyle"
|
|
||||||
>
|
|
||||||
<div v-if="calendarZoomGhost" class="calendar-zoom-overlay-content">
|
|
||||||
<p class="calendar-zoom-overlay-title">{{ calendarZoomGhost.title }}</p>
|
|
||||||
<p v-if="calendarZoomGhost.subtitle" class="calendar-zoom-overlay-subtitle">{{ calendarZoomGhost.subtitle }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-else-if="selectedTab === 'communications' && false" class="space-y-3">
|
<section v-else-if="selectedTab === 'communications' && false" class="space-y-3">
|
||||||
<div class="mb-1 flex justify-end">
|
<div class="mb-1 flex justify-end">
|
||||||
@@ -6181,233 +6024,6 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.calendar-content-wrap {
|
|
||||||
position: relative;
|
|
||||||
padding-left: 40px;
|
|
||||||
padding-right: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-content-scroll {
|
|
||||||
height: 100%;
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-scene {
|
|
||||||
min-height: 100%;
|
|
||||||
min-width: 100%;
|
|
||||||
transform-origin: center center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-scene-hidden {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-scene.cursor-zoom-in,
|
|
||||||
.calendar-scene.cursor-zoom-in * {
|
|
||||||
cursor: zoom-in;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-scene.cursor-zoom-out,
|
|
||||||
.calendar-scene.cursor-zoom-out * {
|
|
||||||
cursor: zoom-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-week-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(7, minmax(165px, 1fr));
|
|
||||||
gap: 8px;
|
|
||||||
min-width: 1180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-side-nav {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
z-index: 4;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border: 1px solid color-mix(in oklab, var(--color-base-content) 16%, transparent);
|
|
||||||
border-radius: 999px;
|
|
||||||
background: color-mix(in oklab, var(--color-base-100) 88%, transparent);
|
|
||||||
color: color-mix(in oklab, var(--color-base-content) 86%, transparent);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: background-color 120ms ease, border-color 120ms ease, transform 120ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-side-nav:hover {
|
|
||||||
border-color: color-mix(in oklab, var(--color-primary) 50%, transparent);
|
|
||||||
background: color-mix(in oklab, var(--color-primary) 14%, var(--color-base-100));
|
|
||||||
transform: translateY(-50%) scale(1.03);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-side-nav-left {
|
|
||||||
left: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-side-nav-right {
|
|
||||||
right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-hover-targetable {
|
|
||||||
transform-origin: center center;
|
|
||||||
transition: transform 320ms ease, box-shadow 320ms ease, outline-color 320ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-hover-target {
|
|
||||||
outline: 2px solid color-mix(in oklab, var(--color-primary) 66%, transparent);
|
|
||||||
outline-offset: 1px;
|
|
||||||
box-shadow: 0 0 0 1px color-mix(in oklab, var(--color-primary) 32%, transparent) inset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-zoom-prime-active {
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-zoom-inline {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 128px;
|
|
||||||
height: 22px;
|
|
||||||
padding: 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-zoom-slider {
|
|
||||||
width: 100%;
|
|
||||||
height: 18px;
|
|
||||||
margin: 0;
|
|
||||||
background: transparent;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-zoom-slider:focus-visible {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-zoom-slider::-webkit-slider-runnable-track {
|
|
||||||
height: 2px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: color-mix(in oklab, var(--color-base-content) 22%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-zoom-slider::-webkit-slider-thumb {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
margin-top: -4px;
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 999px;
|
|
||||||
border: 1px solid color-mix(in oklab, var(--color-base-content) 35%, transparent);
|
|
||||||
background: color-mix(in oklab, var(--color-base-100) 98%, var(--color-base-content));
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-zoom-slider::-moz-range-track {
|
|
||||||
height: 2px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: color-mix(in oklab, var(--color-base-content) 22%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-zoom-slider::-moz-range-progress {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-zoom-slider::-moz-range-thumb {
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 999px;
|
|
||||||
border: 1px solid color-mix(in oklab, var(--color-base-content) 35%, transparent);
|
|
||||||
background: color-mix(in oklab, var(--color-base-100) 98%, var(--color-base-content));
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-zoom-marks {
|
|
||||||
position: absolute;
|
|
||||||
inset-inline: 10px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-zoom-mark {
|
|
||||||
width: 4px;
|
|
||||||
height: 4px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: color-mix(in oklab, var(--color-base-content) 30%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-zoom-mark-active {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
background: color-mix(in oklab, var(--color-base-content) 78%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-zoom-overlay {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 6;
|
|
||||||
border: 3px solid color-mix(in oklab, var(--color-primary) 64%, transparent);
|
|
||||||
border-radius: 12px;
|
|
||||||
background: color-mix(in oklab, var(--color-primary) 14%, transparent);
|
|
||||||
overflow: hidden;
|
|
||||||
pointer-events: none;
|
|
||||||
transition:
|
|
||||||
left 2400ms cubic-bezier(0.16, 0.86, 0.18, 1),
|
|
||||||
top 2400ms cubic-bezier(0.16, 0.86, 0.18, 1),
|
|
||||||
width 2400ms cubic-bezier(0.16, 0.86, 0.18, 1),
|
|
||||||
height 2400ms cubic-bezier(0.16, 0.86, 0.18, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-zoom-overlay-content {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: flex-start;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 10px;
|
|
||||||
color: color-mix(in oklab, var(--color-base-content) 86%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-zoom-overlay-title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-zoom-overlay-subtitle {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 1.2;
|
|
||||||
opacity: 0.74;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 960px) {
|
|
||||||
.calendar-content-wrap {
|
|
||||||
padding-left: 32px;
|
|
||||||
padding-right: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-week-grid {
|
|
||||||
grid-template-columns: repeat(7, minmax(150px, 1fr));
|
|
||||||
min-width: 1060px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-side-nav {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-zoom-inline {
|
|
||||||
width: 108px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.comm-input-wrap {
|
.comm-input-wrap {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
|||||||
559
frontend/app/components/workspace/calendar/CrmCalendarPanel.vue
Normal file
559
frontend/app/components/workspace/calendar/CrmCalendarPanel.vue
Normal file
@@ -0,0 +1,559 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
type CalendarEvent = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
contact: string;
|
||||||
|
note: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type YearMonthItem = {
|
||||||
|
monthIndex: number;
|
||||||
|
label: string;
|
||||||
|
count: number;
|
||||||
|
first?: CalendarEvent;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MonthCell = {
|
||||||
|
key: string;
|
||||||
|
day: number;
|
||||||
|
inMonth: boolean;
|
||||||
|
events: CalendarEvent[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type MonthRow = {
|
||||||
|
key: string;
|
||||||
|
startKey: string;
|
||||||
|
cells: MonthCell[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type WeekDay = {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
day: number;
|
||||||
|
events: CalendarEvent[];
|
||||||
|
};
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
contextPickerEnabled: boolean;
|
||||||
|
hasContextScope: (scope: "calendar") => boolean;
|
||||||
|
toggleContextScope: (scope: "calendar") => void;
|
||||||
|
contextScopeLabel: (scope: "calendar") => string;
|
||||||
|
setToday: () => void;
|
||||||
|
calendarPeriodLabel: string;
|
||||||
|
calendarZoomLevelIndex: number;
|
||||||
|
onCalendarZoomSliderInput: (event: Event) => void;
|
||||||
|
focusedCalendarEvent: CalendarEvent | null;
|
||||||
|
formatDay: (iso: string) => string;
|
||||||
|
formatTime: (iso: string) => string;
|
||||||
|
setCalendarContentWrapRef: (element: HTMLDivElement | null) => void;
|
||||||
|
shiftCalendar: (step: number) => void;
|
||||||
|
setCalendarContentScrollRef: (element: HTMLDivElement | null) => void;
|
||||||
|
onCalendarHierarchyWheel: (event: WheelEvent) => void;
|
||||||
|
setCalendarSceneRef: (element: HTMLDivElement | null) => void;
|
||||||
|
normalizedCalendarView: string;
|
||||||
|
calendarSceneMasked: boolean;
|
||||||
|
calendarSceneTransformStyle: Record<string, string>;
|
||||||
|
onCalendarSceneMouseLeave: () => void;
|
||||||
|
calendarView: string;
|
||||||
|
yearMonths: YearMonthItem[];
|
||||||
|
calendarCursorMonth: number;
|
||||||
|
calendarHoveredMonthIndex: number | null;
|
||||||
|
setCalendarHoveredMonthIndex: (value: number | null) => void;
|
||||||
|
calendarZoomPrimeToken: string;
|
||||||
|
calendarPrimeMonthToken: (monthIndex: number) => string;
|
||||||
|
calendarPrimeStyle: (token: string) => Record<string, string>;
|
||||||
|
zoomToMonth: (monthIndex: number) => void;
|
||||||
|
openThreadFromCalendarItem: (event: CalendarEvent) => void;
|
||||||
|
monthRows: MonthRow[];
|
||||||
|
calendarHoveredWeekStartKey: string;
|
||||||
|
setCalendarHoveredWeekStartKey: (value: string) => void;
|
||||||
|
calendarPrimeWeekToken: (startKey: string) => string;
|
||||||
|
selectedDateKey: string;
|
||||||
|
monthCellHasFocusedEvent: (events: CalendarEvent[]) => boolean;
|
||||||
|
calendarHoveredDayKey: string;
|
||||||
|
setCalendarHoveredDayKey: (value: string) => void;
|
||||||
|
pickDate: (key: string) => void;
|
||||||
|
monthCellEvents: (events: CalendarEvent[]) => CalendarEvent[];
|
||||||
|
isReviewHighlightedEvent: (eventId: string) => boolean;
|
||||||
|
weekDays: WeekDay[];
|
||||||
|
calendarPrimeDayToken: (dayKey: string) => string;
|
||||||
|
selectedDayEvents: CalendarEvent[];
|
||||||
|
calendarZoomOverlay: { active: boolean };
|
||||||
|
setCalendarZoomOverlayRef: (element: HTMLDivElement | null) => void;
|
||||||
|
calendarZoomOverlayStyle: Record<string, string>;
|
||||||
|
calendarZoomGhost: { title: string; subtitle?: string } | null;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section
|
||||||
|
class="relative flex h-full min-h-0 flex-col gap-3"
|
||||||
|
:class="[
|
||||||
|
contextPickerEnabled ? 'context-scope-block context-scope-block-active' : '',
|
||||||
|
hasContextScope('calendar') ? 'context-scope-block-selected' : '',
|
||||||
|
]"
|
||||||
|
@click="toggleContextScope('calendar')"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="contextPickerEnabled"
|
||||||
|
class="context-scope-label"
|
||||||
|
>{{ contextScopeLabel('calendar') }}</span>
|
||||||
|
<div class="grid grid-cols-[auto_1fr_auto] items-center gap-2">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<button class="btn btn-xs" @click="setToday">Today</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center text-sm font-medium">
|
||||||
|
{{ calendarPeriodLabel }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="justify-self-end calendar-zoom-inline" @click.stop>
|
||||||
|
<input
|
||||||
|
class="calendar-zoom-slider"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="3"
|
||||||
|
step="1"
|
||||||
|
:value="calendarZoomLevelIndex"
|
||||||
|
aria-label="Calendar zoom level"
|
||||||
|
@input="onCalendarZoomSliderInput"
|
||||||
|
>
|
||||||
|
<div class="calendar-zoom-marks" aria-hidden="true">
|
||||||
|
<span
|
||||||
|
v-for="index in 4"
|
||||||
|
:key="`calendar-zoom-mark-${index}`"
|
||||||
|
class="calendar-zoom-mark"
|
||||||
|
:class="calendarZoomLevelIndex === index - 1 ? 'calendar-zoom-mark-active' : ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<article
|
||||||
|
v-if="focusedCalendarEvent"
|
||||||
|
class="rounded-xl border border-success/50 bg-success/10 px-3 py-2"
|
||||||
|
>
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-success/80">Review focus event</p>
|
||||||
|
<p class="text-sm font-medium text-base-content">{{ focusedCalendarEvent.title }}</p>
|
||||||
|
<p class="text-xs text-base-content/70">
|
||||||
|
{{ formatDay(focusedCalendarEvent.start) }} · {{ formatTime(focusedCalendarEvent.start) }} - {{ formatTime(focusedCalendarEvent.end) }}
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 text-xs text-base-content/80">{{ focusedCalendarEvent.note || "No note" }}</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<div :ref="setCalendarContentWrapRef" class="calendar-content-wrap min-h-0 flex-1">
|
||||||
|
<button
|
||||||
|
class="calendar-side-nav calendar-side-nav-left"
|
||||||
|
type="button"
|
||||||
|
title="Previous period"
|
||||||
|
@click="shiftCalendar(-1)"
|
||||||
|
>
|
||||||
|
<span>←</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="calendar-side-nav calendar-side-nav-right"
|
||||||
|
type="button"
|
||||||
|
title="Next period"
|
||||||
|
@click="shiftCalendar(1)"
|
||||||
|
>
|
||||||
|
<span>→</span>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
:ref="setCalendarContentScrollRef"
|
||||||
|
class="calendar-content-scroll min-h-0 h-full overflow-y-auto pr-1"
|
||||||
|
@wheel.prevent="onCalendarHierarchyWheel"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:ref="setCalendarSceneRef"
|
||||||
|
:class="[
|
||||||
|
'calendar-scene',
|
||||||
|
normalizedCalendarView === 'day' ? 'cursor-zoom-out' : 'cursor-zoom-in',
|
||||||
|
calendarSceneMasked ? 'calendar-scene-hidden' : '',
|
||||||
|
]"
|
||||||
|
:style="calendarSceneTransformStyle"
|
||||||
|
@mouseleave="onCalendarSceneMouseLeave"
|
||||||
|
>
|
||||||
|
<div class="grid gap-2" :class="calendarView === 'year' ? 'sm:grid-cols-2 xl:grid-cols-3' : 'grid-cols-1'">
|
||||||
|
<article
|
||||||
|
v-for="item in yearMonths"
|
||||||
|
:key="`year-month-${item.monthIndex}`"
|
||||||
|
v-show="calendarView === 'year' || item.monthIndex === calendarCursorMonth"
|
||||||
|
class="group relative rounded-xl border border-base-300 p-3 text-left transition calendar-hover-targetable"
|
||||||
|
:class="[
|
||||||
|
calendarView === 'year' ? 'hover:border-primary/50 hover:bg-primary/5 cursor-zoom-in' : 'cursor-default min-h-[26rem] bg-base-100',
|
||||||
|
calendarHoveredMonthIndex === item.monthIndex ? 'calendar-hover-target' : '',
|
||||||
|
calendarZoomPrimeToken === calendarPrimeMonthToken(item.monthIndex) ? 'calendar-zoom-prime-active' : '',
|
||||||
|
]"
|
||||||
|
:style="calendarPrimeStyle(calendarPrimeMonthToken(item.monthIndex))"
|
||||||
|
:data-calendar-month-index="item.monthIndex"
|
||||||
|
@mouseenter="setCalendarHoveredMonthIndex(item.monthIndex)"
|
||||||
|
@click="calendarView === 'year' ? zoomToMonth(item.monthIndex) : undefined"
|
||||||
|
>
|
||||||
|
<p class="font-medium">{{ item.label }}</p>
|
||||||
|
<p class="text-xs text-base-content/60">{{ item.count }} events</p>
|
||||||
|
<button
|
||||||
|
v-if="calendarView === 'year' && item.first"
|
||||||
|
class="mt-1 block w-full text-left text-xs text-base-content/70 hover:underline"
|
||||||
|
@click.stop="openThreadFromCalendarItem(item.first)"
|
||||||
|
>
|
||||||
|
{{ formatDay(item.first.start) }} · {{ item.first.title }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="item.monthIndex === calendarCursorMonth && (calendarView === 'month' || calendarView === 'agenda')"
|
||||||
|
class="mt-3 space-y-1"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-7 gap-1 text-center text-xs font-semibold text-base-content/60">
|
||||||
|
<span>Sun</span>
|
||||||
|
<span>Mon</span>
|
||||||
|
<span>Tue</span>
|
||||||
|
<span>Wed</span>
|
||||||
|
<span>Thu</span>
|
||||||
|
<span>Fri</span>
|
||||||
|
<span>Sat</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div
|
||||||
|
v-for="row in monthRows"
|
||||||
|
:key="row.key"
|
||||||
|
class="group relative calendar-hover-targetable"
|
||||||
|
:class="[
|
||||||
|
calendarHoveredWeekStartKey === row.startKey ? 'calendar-hover-target' : '',
|
||||||
|
calendarZoomPrimeToken === calendarPrimeWeekToken(row.startKey) ? 'calendar-zoom-prime-active' : '',
|
||||||
|
]"
|
||||||
|
:style="calendarPrimeStyle(calendarPrimeWeekToken(row.startKey))"
|
||||||
|
:data-calendar-week-start-key="row.startKey"
|
||||||
|
@mouseenter="setCalendarHoveredWeekStartKey(row.startKey)"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-7 gap-1">
|
||||||
|
<button
|
||||||
|
v-for="cell in row.cells"
|
||||||
|
:key="cell.key"
|
||||||
|
class="group relative min-h-24 rounded-lg border p-1 text-left"
|
||||||
|
:class="[
|
||||||
|
cell.inMonth ? 'border-base-300 bg-base-100' : 'border-base-200 bg-base-200/40 text-base-content/40',
|
||||||
|
selectedDateKey === cell.key ? 'border-primary bg-primary/5' : '',
|
||||||
|
monthCellHasFocusedEvent(cell.events) ? 'border-success/60 bg-success/10' : '',
|
||||||
|
]"
|
||||||
|
:data-calendar-day-key="cell.key"
|
||||||
|
@mouseenter="setCalendarHoveredDayKey(cell.key)"
|
||||||
|
@click="pickDate(cell.key)"
|
||||||
|
>
|
||||||
|
<p class="mb-1 text-xs font-semibold">{{ cell.day }}</p>
|
||||||
|
<button
|
||||||
|
v-for="event in monthCellEvents(cell.events)"
|
||||||
|
:key="event.id"
|
||||||
|
class="block w-full truncate rounded px-1 text-left text-[10px] text-base-content/70 transition hover:underline"
|
||||||
|
:class="isReviewHighlightedEvent(event.id) ? 'bg-success/20 text-success-content ring-1 ring-success/40' : ''"
|
||||||
|
@click.stop="openThreadFromCalendarItem(event)"
|
||||||
|
>
|
||||||
|
{{ formatTime(event.start) }} {{ event.title }}
|
||||||
|
</button>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="item.monthIndex === calendarCursorMonth && calendarView === 'week'"
|
||||||
|
class="mt-3 calendar-week-scroll overflow-x-auto pb-1"
|
||||||
|
>
|
||||||
|
<div class="calendar-week-grid">
|
||||||
|
<article
|
||||||
|
v-for="day in weekDays"
|
||||||
|
:key="day.key"
|
||||||
|
class="group relative flex min-h-[18rem] flex-col rounded-xl border border-base-300 bg-base-100 p-2.5 cursor-zoom-in calendar-hover-targetable"
|
||||||
|
:class="[
|
||||||
|
selectedDateKey === day.key ? 'border-primary bg-primary/5' : '',
|
||||||
|
calendarHoveredDayKey === day.key ? 'calendar-hover-target' : '',
|
||||||
|
calendarZoomPrimeToken === calendarPrimeDayToken(day.key) ? 'calendar-zoom-prime-active' : '',
|
||||||
|
]"
|
||||||
|
:style="calendarPrimeStyle(calendarPrimeDayToken(day.key))"
|
||||||
|
:data-calendar-day-key="day.key"
|
||||||
|
@mouseenter="setCalendarHoveredDayKey(day.key)"
|
||||||
|
@click="pickDate(day.key)"
|
||||||
|
>
|
||||||
|
<div class="mb-2 flex items-start justify-between gap-2">
|
||||||
|
<p class="text-sm font-semibold leading-tight">{{ day.label }} {{ day.day }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<button
|
||||||
|
v-for="event in day.events"
|
||||||
|
:key="event.id"
|
||||||
|
class="block w-full rounded-md px-2 py-1.5 text-left text-xs"
|
||||||
|
:class="isReviewHighlightedEvent(event.id) ? 'bg-success/20 ring-1 ring-success/45' : 'bg-base-200 hover:bg-base-300/80'"
|
||||||
|
@click.stop="openThreadFromCalendarItem(event)"
|
||||||
|
>
|
||||||
|
{{ formatTime(event.start) }} - {{ event.title }} ({{ event.contact }})
|
||||||
|
</button>
|
||||||
|
<p v-if="day.events.length === 0" class="pt-1 text-xs text-base-content/50">No events</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="item.monthIndex === calendarCursorMonth && calendarView === 'day'"
|
||||||
|
class="mt-3 space-y-2"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="event in selectedDayEvents"
|
||||||
|
:key="event.id"
|
||||||
|
class="block w-full rounded-xl border border-base-300 p-3 text-left transition hover:bg-base-200/60"
|
||||||
|
:class="isReviewHighlightedEvent(event.id) ? 'border-success/60 bg-success/10' : ''"
|
||||||
|
@click="openThreadFromCalendarItem(event)"
|
||||||
|
>
|
||||||
|
<p class="font-medium">{{ event.title }}</p>
|
||||||
|
<p class="text-xs text-base-content/60">{{ event.contact }}</p>
|
||||||
|
<p class="text-xs text-base-content/60">{{ formatTime(event.start) }} - {{ formatTime(event.end) }}</p>
|
||||||
|
<p class="mt-1 text-sm text-base-content/80">{{ event.note }}</p>
|
||||||
|
</button>
|
||||||
|
<p v-if="selectedDayEvents.length === 0" class="text-sm text-base-content/60">No events on this day.</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="calendarZoomOverlay.active"
|
||||||
|
:ref="setCalendarZoomOverlayRef"
|
||||||
|
class="calendar-zoom-overlay"
|
||||||
|
:style="calendarZoomOverlayStyle"
|
||||||
|
>
|
||||||
|
<div v-if="calendarZoomGhost" class="calendar-zoom-overlay-content">
|
||||||
|
<p class="calendar-zoom-overlay-title">{{ calendarZoomGhost.title }}</p>
|
||||||
|
<p v-if="calendarZoomGhost.subtitle" class="calendar-zoom-overlay-subtitle">{{ calendarZoomGhost.subtitle }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.calendar-content-wrap {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 40px;
|
||||||
|
padding-right: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-content-scroll {
|
||||||
|
height: 100%;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-scene {
|
||||||
|
min-height: 100%;
|
||||||
|
min-width: 100%;
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-scene-hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-scene.cursor-zoom-in,
|
||||||
|
.calendar-scene.cursor-zoom-in * {
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-scene.cursor-zoom-out,
|
||||||
|
.calendar-scene.cursor-zoom-out * {
|
||||||
|
cursor: zoom-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-week-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, minmax(165px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 1180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-side-nav {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 4;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: 1px solid color-mix(in oklab, var(--color-base-content) 16%, transparent);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: color-mix(in oklab, var(--color-base-100) 88%, transparent);
|
||||||
|
color: color-mix(in oklab, var(--color-base-content) 86%, transparent);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background-color 120ms ease, border-color 120ms ease, transform 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-side-nav:hover {
|
||||||
|
border-color: color-mix(in oklab, var(--color-primary) 50%, transparent);
|
||||||
|
background: color-mix(in oklab, var(--color-primary) 14%, var(--color-base-100));
|
||||||
|
transform: translateY(-50%) scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-side-nav-left {
|
||||||
|
left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-side-nav-right {
|
||||||
|
right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-hover-targetable {
|
||||||
|
transform-origin: center center;
|
||||||
|
transition: transform 320ms ease, box-shadow 320ms ease, outline-color 320ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-hover-target {
|
||||||
|
outline: 2px solid color-mix(in oklab, var(--color-primary) 66%, transparent);
|
||||||
|
outline-offset: 1px;
|
||||||
|
box-shadow: 0 0 0 1px color-mix(in oklab, var(--color-primary) 32%, transparent) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-zoom-prime-active {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-zoom-inline {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 128px;
|
||||||
|
height: 22px;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-zoom-slider {
|
||||||
|
width: 100%;
|
||||||
|
height: 18px;
|
||||||
|
margin: 0;
|
||||||
|
background: transparent;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-zoom-slider:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-zoom-slider::-webkit-slider-runnable-track {
|
||||||
|
height: 2px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: color-mix(in oklab, var(--color-base-content) 22%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-zoom-slider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
margin-top: -4px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: color-mix(in oklab, var(--color-base-content) 88%, transparent);
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-zoom-slider::-moz-range-track {
|
||||||
|
height: 2px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: color-mix(in oklab, var(--color-base-content) 22%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-zoom-slider::-moz-range-progress {
|
||||||
|
height: 2px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-zoom-slider::-moz-range-thumb {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: color-mix(in oklab, var(--color-base-content) 88%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-zoom-marks {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-zoom-mark {
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: color-mix(in oklab, var(--color-base-content) 35%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-zoom-mark-active {
|
||||||
|
background: color-mix(in oklab, var(--color-base-content) 85%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-zoom-overlay {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 18;
|
||||||
|
border: 1px solid color-mix(in oklab, var(--color-primary) 62%, transparent);
|
||||||
|
border-radius: 14px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 20% 20%, color-mix(in oklab, var(--color-primary) 30%, transparent), transparent 55%),
|
||||||
|
color-mix(in oklab, var(--color-base-100) 84%, transparent);
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px color-mix(in oklab, var(--color-primary) 42%, transparent) inset,
|
||||||
|
0 18px 38px rgba(18, 30, 58, 0.28);
|
||||||
|
pointer-events: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-zoom-overlay-content {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 14px;
|
||||||
|
color: color-mix(in oklab, var(--color-base-content) 90%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-zoom-overlay-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-zoom-overlay-subtitle {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 11px;
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.calendar-content-wrap {
|
||||||
|
padding-left: 32px;
|
||||||
|
padding-right: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-week-grid {
|
||||||
|
grid-template-columns: repeat(7, minmax(150px, 1fr));
|
||||||
|
min-width: 1060px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-side-nav {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-zoom-inline {
|
||||||
|
width: 108px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user