diff --git a/package-lock.json b/package-lock.json index 6cb90908..a481c98a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@vue/eslint-config-typescript": "^14.3.0", "@vueuse/core": "^12.8.2", "@vueuse/integrations": "^12.5.0", + "chroma-js": "3.1.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dayjs": "^1.11.11", @@ -44,6 +45,7 @@ "@playwright/test": "^1.41.1", "@tailwindcss/forms": "^0.5.9", "@tailwindcss/typography": "^0.5.15", + "@types/chroma-js": "2.4.5", "@types/node": "^22.10.10", "@vitejs/plugin-vue": "^5.2.1", "@vue/tsconfig": "^0.5.1", @@ -1896,6 +1898,13 @@ "vue": "^2.7.0 || ^3.0.0" } }, + "node_modules/@types/chroma-js": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.5.tgz", + "integrity": "sha512-6ISjhzJViaPCy2q2e6PgK+8HcHQDQ0V2LDiKmYAh+jJlLqDa6HbwDh0wOevHY0kHHUx0iZwjSRbVD47WOUx5EQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -2931,6 +2940,12 @@ "node": ">= 6" } }, + "node_modules/chroma-js": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-3.1.2.tgz", + "integrity": "sha512-IJnETTalXbsLx1eKEgx19d5L6SRM7cH4vINw/99p/M11HCuXGRWL+6YmCm7FWFGIo6dtWuQoQi1dc5yQ7ESIHg==", + "license": "(BSD-3-Clause AND Apache-2.0)" + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", diff --git a/package.json b/package.json index 61d58a51..f133c166 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@playwright/test": "^1.41.1", "@tailwindcss/forms": "^0.5.9", "@tailwindcss/typography": "^0.5.15", + "@types/chroma-js": "2.4.5", "@types/node": "^22.10.10", "@vitejs/plugin-vue": "^5.2.1", "@vue/tsconfig": "^0.5.1", @@ -55,6 +56,7 @@ "@vue/eslint-config-typescript": "^14.3.0", "@vueuse/core": "^12.8.2", "@vueuse/integrations": "^12.5.0", + "chroma-js": "3.1.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dayjs": "^1.11.11", diff --git a/resources/js/packages/ui/src/FullCalendar/TimeEntryCalendar.vue b/resources/js/packages/ui/src/FullCalendar/TimeEntryCalendar.vue index 3995b3d8..6959d32e 100644 --- a/resources/js/packages/ui/src/FullCalendar/TimeEntryCalendar.vue +++ b/resources/js/packages/ui/src/FullCalendar/TimeEntryCalendar.vue @@ -5,6 +5,8 @@ import timeGridPlugin from '@fullcalendar/timegrid'; import interactionPlugin from '@fullcalendar/interaction'; import type { DatesSetArg, EventClickArg, EventDropArg, EventChangeArg } from '@fullcalendar/core'; import { computed, ref, watch, inject, type ComputedRef } from 'vue'; +import chroma from 'chroma-js'; +import { useCssVariable } from '@/utils/useCssVariable'; import { getDayJsInstance, getLocalizedDayJs } from '../utils/time'; import { getUserTimezone, getWeekStart } from '../utils/settings'; import { LoadingSpinner, TimeEntryCreateModal, TimeEntryEditModal } from '..'; @@ -94,8 +96,12 @@ const getSlotLabelFormat = () => { } }; -// Map API entries to FullCalendar events +const cssBackground = useCssVariable('--color-bg-background'); + const events = computed(() => { + const themeBackground = (() => { + return cssBackground.value?.trim(); + })(); return props.timeEntries ?.filter((timeEntry) => timeEntry.end !== null) ?.map((timeEntry) => { @@ -109,14 +115,18 @@ const events = computed(() => { const title = timeEntry.description || 'No description'; + const baseColor = project?.color || '#6B7280'; + const backgroundColor = chroma.mix(baseColor, themeBackground, 0.65, 'lab').hex(); + const borderColor = chroma.mix(baseColor, themeBackground, 0.5, 'lab').hex(); + return { id: timeEntry.id, start: getLocalizedDayJs(timeEntry.start).format(), end: getLocalizedDayJs(timeEntry.end!).format(), title, - backgroundColor: project?.color || '#6B7280', - borderColor: project?.color || '#6B7280', - textColor: '#FFFFFF', + backgroundColor, + borderColor, + textColor: 'var(--foreground)', extendedProps: { timeEntry, project, @@ -249,6 +259,8 @@ watch(showCreateTimeEntryModal, (value) => { if (!value) { newEventStart.value = null; newEventEnd.value = null; + // Ensure FullCalendar clears the selection mirror when modal closes + calendarRef.value?.getApi().unselect(); emit('refresh'); } }); @@ -427,7 +439,6 @@ watch(showEditTimeEntryModal, (value) => { } .fullcalendar :deep(.fc-event) { - border: 1px solid !important; border-radius: var(--radius); padding: 0.45rem 0.25rem; font-size: 0.75rem;