1 parent
e4c960b0ca
commit
86a06acccb
35 files changed
+4137
-1717
No files matched your search
@@ -0,0 +1,933 @@
|
||||
<script setup>
|
||||
import { useCalendarStore } from '@/stores/CalendarStore'
|
||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import WeekdaySelector from './WeekdaySelector.vue'
|
||||
import Numeric from './Numeric.vue'
|
||||
import { addDaysStr } from '@/utils/date'
|
||||
|
||||
const props = defineProps({
|
||||
selection: { type: Object, default: () => ({ startDate: null, dayCount: 0 }) },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['clear-selection'])
|
||||
|
||||
const calendarStore = useCalendarStore()
|
||||
|
||||
const showDialog = ref(false)
|
||||
const dialogMode = ref('create') // 'create' or 'edit'
|
||||
const editingEventId = ref(null) // base event id if repeating occurrence clicked
|
||||
const occurrenceContext = ref(null) // { baseId, occurrenceIndex, weekday, occurrenceDate }
|
||||
const title = ref('')
|
||||
const recurrenceEnabled = ref(false)
|
||||
const recurrenceInterval = ref(1) // N in "Every N weeks/months"
|
||||
const recurrenceFrequency = ref('weeks') // 'weeks' | 'months'
|
||||
const recurrenceWeekdays = ref([false, false, false, false, false, false, false])
|
||||
const recurrenceOccurrences = ref(0) // 0 = unlimited
|
||||
const colorId = ref(0)
|
||||
const eventSaved = ref(false)
|
||||
const titleInput = ref(null)
|
||||
|
||||
// Helper to get starting weekday (Sunday-first index)
|
||||
function getStartingWeekday(selectionData = null) {
|
||||
const currentSelection = selectionData || props.selection
|
||||
if (!currentSelection.start) return 0 // Default to Sunday
|
||||
const date = new Date(currentSelection.start + 'T00:00:00')
|
||||
const dayOfWeek = date.getDay() // 0=Sunday, 1=Monday, ...
|
||||
return dayOfWeek // Keep Sunday-first (0=Sunday, 6=Saturday)
|
||||
}
|
||||
|
||||
// Computed property for fallback weekdays - true for the initial day of the event, false for others
|
||||
const fallbackWeekdays = computed(() => {
|
||||
const startingDay = getStartingWeekday()
|
||||
const fallback = [false, false, false, false, false, false, false]
|
||||
fallback[startingDay] = true
|
||||
return fallback
|
||||
})
|
||||
|
||||
// Repeat mapping uses 'weeks' | 'months' | 'none' directly (legacy 'weekly'/'monthly' accepted on load)
|
||||
const repeat = computed({
|
||||
get() {
|
||||
if (!recurrenceEnabled.value) return 'none'
|
||||
return recurrenceFrequency.value // 'weeks' | 'months'
|
||||
},
|
||||
set(val) {
|
||||
if (val === 'none') {
|
||||
recurrenceEnabled.value = false
|
||||
return
|
||||
}
|
||||
recurrenceEnabled.value = true
|
||||
if (val === 'weeks' || val === 'weekly') recurrenceFrequency.value = 'weeks'
|
||||
else if (val === 'months' || val === 'monthly') recurrenceFrequency.value = 'months'
|
||||
},
|
||||
})
|
||||
|
||||
// Convert Sunday-first recurrenceWeekdays to Sunday-first pattern for store
|
||||
function buildStoreWeekdayPattern() {
|
||||
// store expects Sun..Sat; we have Sun..Sat
|
||||
// Direct mapping: recurrenceWeekdays indices 0..6 (Sun..Sat) -> store array [Sun,Mon,Tue,Wed,Thu,Fri,Sat]
|
||||
let sunFirst = [...recurrenceWeekdays.value]
|
||||
|
||||
// Ensure at least one day is selected - fallback to starting day
|
||||
if (!sunFirst.some(Boolean)) {
|
||||
const startingDay = getStartingWeekday()
|
||||
sunFirst[startingDay] = true
|
||||
}
|
||||
|
||||
return sunFirst
|
||||
}
|
||||
|
||||
function loadWeekdayPatternFromStore(storePattern) {
|
||||
if (!Array.isArray(storePattern) || storePattern.length !== 7) return
|
||||
// store: Sun..Sat -> keep as Sun..Sat
|
||||
recurrenceWeekdays.value = [...storePattern]
|
||||
}
|
||||
|
||||
const selectedColor = computed({
|
||||
get: () => colorId.value,
|
||||
set: (val) => {
|
||||
colorId.value = parseInt(val)
|
||||
// Update the event immediately when color changes
|
||||
if (editingEventId.value) {
|
||||
updateEventInStore()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function openCreateDialog(selectionData = null) {
|
||||
const currentSelection = selectionData || props.selection
|
||||
|
||||
// Convert new format to start/end for compatibility with existing logic
|
||||
let start, end
|
||||
if (currentSelection.startDate && currentSelection.dayCount) {
|
||||
start = currentSelection.startDate
|
||||
end = addDaysStr(currentSelection.startDate, currentSelection.dayCount - 1)
|
||||
} else if (currentSelection.start && currentSelection.end) {
|
||||
// Fallback for old format
|
||||
start = currentSelection.start
|
||||
end = currentSelection.end
|
||||
} else {
|
||||
start = null
|
||||
end = null
|
||||
}
|
||||
|
||||
occurrenceContext.value = null
|
||||
dialogMode.value = 'create'
|
||||
title.value = ''
|
||||
recurrenceEnabled.value = false
|
||||
recurrenceInterval.value = 1
|
||||
recurrenceFrequency.value = 'weeks'
|
||||
recurrenceWeekdays.value = [false, false, false, false, false, false, false]
|
||||
recurrenceOccurrences.value = 0
|
||||
colorId.value = calendarStore.selectEventColorId(start, end)
|
||||
eventSaved.value = false
|
||||
|
||||
const startingDay = getStartingWeekday({ start, end })
|
||||
recurrenceWeekdays.value[startingDay] = true
|
||||
|
||||
editingEventId.value = calendarStore.createEvent({
|
||||
title: '',
|
||||
startDate: start,
|
||||
endDate: end,
|
||||
colorId: colorId.value,
|
||||
repeat: repeat.value,
|
||||
repeatInterval: recurrenceInterval.value,
|
||||
repeatCount:
|
||||
recurrenceOccurrences.value === 0 ? 'for now' : String(recurrenceOccurrences.value),
|
||||
repeatWeekdays: buildStoreWeekdayPattern(),
|
||||
})
|
||||
|
||||
showDialog.value = true
|
||||
|
||||
// Focus and select text after dialog is shown
|
||||
nextTick(() => {
|
||||
if (titleInput.value) {
|
||||
titleInput.value.focus()
|
||||
if (title.value) {
|
||||
titleInput.value.select()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function openEditDialog(eventInstanceId) {
|
||||
occurrenceContext.value = null
|
||||
let baseId = eventInstanceId
|
||||
let occurrenceIndex = 0
|
||||
let weekday = null
|
||||
let occurrenceDate = null
|
||||
if (typeof eventInstanceId === 'string' && eventInstanceId.includes('_repeat_')) {
|
||||
const [bid, suffix] = eventInstanceId.split('_repeat_')
|
||||
baseId = bid
|
||||
const parts = suffix.split('_')
|
||||
occurrenceIndex = parseInt(parts[0], 10) || 0
|
||||
if (parts.length > 1) weekday = parseInt(parts[1], 10)
|
||||
}
|
||||
const event = calendarStore.getEventById(baseId)
|
||||
if (!event) return
|
||||
// Derive occurrence date if weekly occurrence
|
||||
if (weekday != null) {
|
||||
// Recompute occurrence date: iterate days accumulating selected weekdays
|
||||
const repeatWeekdaysLocal = event.repeatWeekdays
|
||||
let idx = 0
|
||||
let cur = new Date(event.startDate + 'T00:00:00')
|
||||
while (idx < occurrenceIndex && idx < 10000) {
|
||||
// safety bound
|
||||
cur.setDate(cur.getDate() + 1)
|
||||
if (repeatWeekdaysLocal[cur.getDay()]) idx++
|
||||
}
|
||||
occurrenceDate = cur
|
||||
}
|
||||
dialogMode.value = 'edit'
|
||||
editingEventId.value = baseId
|
||||
title.value = event.title
|
||||
loadWeekdayPatternFromStore(event.repeatWeekdays)
|
||||
repeat.value = event.repeat // triggers setter mapping into recurrence state
|
||||
if (event.repeatInterval) recurrenceInterval.value = event.repeatInterval
|
||||
// Map repeatCount
|
||||
const rc = event.repeatCount ?? 'unlimited'
|
||||
recurrenceOccurrences.value = rc === 'unlimited' ? 0 : parseInt(rc, 10) || 0
|
||||
colorId.value = event.colorId
|
||||
eventSaved.value = false
|
||||
if (event.isRepeating && occurrenceIndex >= 0 && weekday != null) {
|
||||
occurrenceContext.value = { baseId, occurrenceIndex, weekday, occurrenceDate }
|
||||
}
|
||||
showDialog.value = true
|
||||
|
||||
// Focus and select text after dialog is shown
|
||||
nextTick(() => {
|
||||
if (titleInput.value) {
|
||||
titleInput.value.focus()
|
||||
if (title.value) {
|
||||
titleInput.value.select()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
showDialog.value = false
|
||||
// If we were creating a new event and user cancels (didn't save), delete it
|
||||
if (dialogMode.value === 'create' && editingEventId.value && !eventSaved.value) {
|
||||
calendarStore.deleteEvent(editingEventId.value)
|
||||
}
|
||||
}
|
||||
|
||||
function updateEventInStore() {
|
||||
if (!editingEventId.value) return
|
||||
|
||||
// For simple property updates (title, color, repeat), update all instances directly
|
||||
// This avoids the expensive remove/re-add cycle
|
||||
for (const [, eventList] of calendarStore.events) {
|
||||
for (const event of eventList) {
|
||||
if (event.id === editingEventId.value) {
|
||||
event.title = title.value
|
||||
event.colorId = colorId.value
|
||||
event.repeat = repeat.value
|
||||
event.repeatInterval = recurrenceInterval.value
|
||||
event.repeatWeekdays = buildStoreWeekdayPattern()
|
||||
event.repeatCount =
|
||||
recurrenceOccurrences.value === 0 ? 'unlimited' : String(recurrenceOccurrences.value)
|
||||
event.isRepeating = recurrenceEnabled.value && repeat.value !== 'none'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveEvent() {
|
||||
if (editingEventId.value) {
|
||||
updateEventInStore()
|
||||
}
|
||||
|
||||
eventSaved.value = true
|
||||
|
||||
if (dialogMode.value === 'create') {
|
||||
emit('clear-selection')
|
||||
}
|
||||
|
||||
closeDialog()
|
||||
}
|
||||
|
||||
function deleteEventAll() {
|
||||
if (editingEventId.value) calendarStore.deleteEvent(editingEventId.value)
|
||||
closeDialog()
|
||||
}
|
||||
|
||||
function deleteEventOne() {
|
||||
if (occurrenceContext.value) {
|
||||
calendarStore.deleteSingleOccurrence(occurrenceContext.value)
|
||||
} else if (isRepeatingBaseEdit.value && editingEventId.value) {
|
||||
calendarStore.deleteFirstOccurrence(editingEventId.value)
|
||||
}
|
||||
closeDialog()
|
||||
}
|
||||
|
||||
function deleteEventFrom() {
|
||||
if (!occurrenceContext.value) return
|
||||
calendarStore.deleteFromOccurrence(occurrenceContext.value)
|
||||
closeDialog()
|
||||
}
|
||||
|
||||
function toggleWeekday(index) {
|
||||
recurrenceWeekdays.value[index] = !recurrenceWeekdays.value[index]
|
||||
}
|
||||
|
||||
// Watch for title changes and update the event immediately
|
||||
watch(title, (newTitle) => {
|
||||
if (editingEventId.value && showDialog.value) {
|
||||
updateEventInStore()
|
||||
}
|
||||
})
|
||||
|
||||
watch([recurrenceEnabled, recurrenceInterval, recurrenceFrequency], () => {
|
||||
if (editingEventId.value && showDialog.value) updateEventInStore()
|
||||
})
|
||||
watch(
|
||||
recurrenceWeekdays,
|
||||
() => {
|
||||
if (editingEventId.value && showDialog.value && repeat.value === 'weeks') updateEventInStore()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
watch(recurrenceOccurrences, () => {
|
||||
if (editingEventId.value && showDialog.value) updateEventInStore()
|
||||
})
|
||||
|
||||
// Handle Esc key to close dialog
|
||||
function handleKeydown(event) {
|
||||
if (event.key === 'Escape' && showDialog.value) {
|
||||
closeDialog()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', handleKeydown)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleKeydown)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
openCreateDialog,
|
||||
openEditDialog,
|
||||
})
|
||||
|
||||
// Computed helpers for delete UI
|
||||
const isRepeatingEdit = computed(
|
||||
() => dialogMode.value === 'edit' && recurrenceEnabled.value && repeat.value !== 'none',
|
||||
)
|
||||
const showDeleteVariants = computed(() => isRepeatingEdit.value && occurrenceContext.value)
|
||||
const isRepeatingBaseEdit = computed(() => isRepeatingEdit.value && !occurrenceContext.value)
|
||||
const formattedOccurrenceShort = computed(() => {
|
||||
if (occurrenceContext.value?.occurrenceDate) {
|
||||
try {
|
||||
return occurrenceContext.value.occurrenceDate
|
||||
.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
|
||||
.replace(/, /, ' ')
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
}
|
||||
if (isRepeatingBaseEdit.value && editingEventId.value) {
|
||||
const ev = calendarStore.getEventById(editingEventId.value)
|
||||
if (ev?.startDate) {
|
||||
try {
|
||||
return new Date(ev.startDate + 'T00:00:00')
|
||||
.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
|
||||
.replace(/, /, ' ')
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
}
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const finalOccurrenceDate = computed(() => {
|
||||
if (!recurrenceEnabled.value) return null
|
||||
const count = recurrenceOccurrences.value
|
||||
if (!count || count < 1) return null // unlimited or invalid
|
||||
// Need start date
|
||||
const base = editingEventId.value ? calendarStore.getEventById(editingEventId.value) : null
|
||||
if (!base) return null
|
||||
const start = new Date(base.startDate + 'T00:00:00')
|
||||
if (recurrenceFrequency.value === 'weeks') {
|
||||
// iterate days until we count 'count-1' additional occurrences (first is base if selected weekday)
|
||||
const pattern = buildStoreWeekdayPattern() // Sun..Sat
|
||||
// Build Monday-first pattern again for selection clarity
|
||||
const monFirst = recurrenceWeekdays.value
|
||||
const selectedCount = monFirst.some(Boolean)
|
||||
if (!selectedCount) return null
|
||||
let occs = 0
|
||||
// Determine if the start day counts
|
||||
const startWeekdaySun = start.getDay()
|
||||
// Convert to Monday-first index
|
||||
// We'll just check store pattern
|
||||
if (pattern[startWeekdaySun]) occs = 1
|
||||
let cursor = new Date(start)
|
||||
while (occs < count && occs < 10000) {
|
||||
cursor.setDate(cursor.getDate() + 1)
|
||||
if (pattern[cursor.getDay()]) occs++
|
||||
}
|
||||
if (occs === count) return cursor
|
||||
return null
|
||||
} else if (recurrenceFrequency.value === 'months') {
|
||||
const monthsToAdd = recurrenceInterval.value * (count - 1)
|
||||
const d = new Date(start)
|
||||
d.setMonth(d.getMonth() + monthsToAdd)
|
||||
return d
|
||||
}
|
||||
})
|
||||
|
||||
const formattedFinalOccurrence = computed(() => {
|
||||
const d = finalOccurrenceDate.value
|
||||
if (!d) return ''
|
||||
const now = new Date()
|
||||
const includeYear =
|
||||
d.getFullYear() !== now.getFullYear() ||
|
||||
d.getTime() - now.getTime() >= 1000 * 60 * 60 * 24 * 365
|
||||
const opts = {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
...(includeYear ? { year: 'numeric' } : {}),
|
||||
}
|
||||
try {
|
||||
return d.toLocaleDateString(undefined, opts)
|
||||
} catch {
|
||||
return d.toDateString()
|
||||
}
|
||||
})
|
||||
|
||||
const recurrenceSummary = computed(() => {
|
||||
if (!recurrenceEnabled.value) return 'Does not recur'
|
||||
if (recurrenceFrequency.value === 'weeks') {
|
||||
return recurrenceInterval.value === 1 ? 'Weekly' : `Every ${recurrenceInterval.value} weeks`
|
||||
}
|
||||
// months frequency
|
||||
if (recurrenceInterval.value % 12 === 0) {
|
||||
const years = recurrenceInterval.value / 12
|
||||
return years === 1 ? 'Annually' : `Every ${years} years`
|
||||
}
|
||||
return recurrenceInterval.value === 1 ? 'Monthly' : `Every ${recurrenceInterval.value} months`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ec-modal-backdrop" v-if="showDialog" @click.self="closeDialog">
|
||||
<div class="ec-modal">
|
||||
<form class="ec-form" @submit.prevent="saveEvent">
|
||||
<header class="ec-header">
|
||||
<h2 id="ec-modal-title">{{ dialogMode === 'create' ? 'Create Event' : 'Edit Event' }}</h2>
|
||||
</header>
|
||||
<div class="ec-body">
|
||||
<label class="ec-field">
|
||||
<span>Title</span>
|
||||
<input type="text" v-model="title" autocomplete="off" ref="titleInput" />
|
||||
</label>
|
||||
<div class="ec-color-swatches">
|
||||
<label v-for="i in 8" :key="i - 1" class="swatch-label">
|
||||
<input
|
||||
class="swatch"
|
||||
:class="'event-color-' + (i - 1)"
|
||||
type="radio"
|
||||
name="colorId"
|
||||
:value="i - 1"
|
||||
v-model="selectedColor"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="recurrence-block">
|
||||
<div class="recurrence-header">
|
||||
<label class="switch">
|
||||
<input type="checkbox" v-model="recurrenceEnabled" />
|
||||
<span>Repeat</span>
|
||||
</label>
|
||||
<span class="recurrence-summary" v-if="recurrenceEnabled">
|
||||
{{ recurrenceSummary }}
|
||||
<template v-if="recurrenceOccurrences > 0">
|
||||
until {{ formattedFinalOccurrence }}</template
|
||||
>
|
||||
</span>
|
||||
<span class="recurrence-summary muted" v-else>Does not recur</span>
|
||||
</div>
|
||||
<div v-if="recurrenceEnabled" class="recurrence-form">
|
||||
<div class="line compact">
|
||||
<Numeric
|
||||
v-model="recurrenceInterval"
|
||||
:prefix-values="[{ value: 1, display: 'Every' }]"
|
||||
:min="2"
|
||||
number-prefix="Every "
|
||||
aria-label="Interval"
|
||||
/>
|
||||
<select v-model="recurrenceFrequency" class="freq-select">
|
||||
<option value="weeks">{{ recurrenceInterval === 1 ? 'week' : 'weeks' }}</option>
|
||||
<option value="months">
|
||||
{{ recurrenceInterval === 1 ? 'month' : 'months' }}
|
||||
</option>
|
||||
</select>
|
||||
<Numeric
|
||||
class="occ-stepper"
|
||||
v-model="recurrenceOccurrences"
|
||||
:min="2"
|
||||
:prefix-values="[{ value: 0, display: '∞' }]"
|
||||
number-postfix=" times"
|
||||
aria-label="Occurrences (0 = no end)"
|
||||
extra-class="occ"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="recurrenceFrequency === 'weeks'" @click.stop>
|
||||
<WeekdaySelector v-model="recurrenceWeekdays" :fallback="fallbackWeekdays" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="ec-footer">
|
||||
<template v-if="dialogMode === 'create'">
|
||||
<button type="button" class="ec-btn delete-btn" @click="deleteEventAll">Delete</button>
|
||||
<button type="submit" class="ec-btn save-btn">Save</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="showDeleteVariants">
|
||||
<div class="ec-delete-group">
|
||||
<button type="button" class="ec-btn delete-btn" @click="deleteEventOne">
|
||||
Delete {{ formattedOccurrenceShort }}
|
||||
</button>
|
||||
<button type="button" class="ec-btn delete-btn" @click="deleteEventFrom">
|
||||
Rest
|
||||
</button>
|
||||
<button type="button" class="ec-btn delete-btn" @click="deleteEventAll">All</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="isRepeatingBaseEdit">
|
||||
<div class="ec-delete-group">
|
||||
<button type="button" class="ec-btn delete-btn" @click="deleteEventOne">
|
||||
Delete {{ formattedOccurrenceShort }}
|
||||
</button>
|
||||
<button type="button" class="ec-btn delete-btn" @click="deleteEventAll">All</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button type="button" class="ec-btn delete-btn" @click="deleteEventAll">
|
||||
Delete
|
||||
</button>
|
||||
</template>
|
||||
<button type="button" class="ec-btn close-btn" @click="closeDialog">Close</button>
|
||||
</template>
|
||||
</footer>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Modal dialog */
|
||||
.ec-modal-backdrop[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ec-modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: color-mix(in srgb, var(--strong) 30%, transparent);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.ec-modal {
|
||||
background: var(--panel);
|
||||
color: var(--ink);
|
||||
border-radius: 0.6rem;
|
||||
min-width: 320px;
|
||||
max-width: min(520px, 90vw);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.ec-form {
|
||||
padding: 1rem;
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.ec-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.ec-body {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.ec-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.ec-field {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.ec-field > span {
|
||||
font-size: 0.85em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.ec-field input[type='text'],
|
||||
.ec-field input[type='time'],
|
||||
.ec-field input[type='number'],
|
||||
.ec-field select {
|
||||
border: 1px solid var(--muted);
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.5rem 0.6rem;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.ec-color-swatches {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.ec-color-swatches .swatch {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.25rem;
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
width: 3em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.ec-color-swatches .swatch:checked {
|
||||
outline-color: var(--ink);
|
||||
}
|
||||
|
||||
.ec-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.ec-btn {
|
||||
border: 1px solid var(--muted);
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
padding: 0.5rem 0.8rem;
|
||||
border-radius: 0.4rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.ec-btn:hover {
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
.ec-btn.save-btn {
|
||||
background: var(--today);
|
||||
color: #000;
|
||||
border-color: transparent;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ec-btn.save-btn:hover {
|
||||
background: color-mix(in srgb, var(--today) 90%, black);
|
||||
}
|
||||
|
||||
.ec-btn.close-btn {
|
||||
background: var(--panel);
|
||||
border-color: var(--muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ec-btn.close-btn:hover {
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
.ec-btn.delete-btn {
|
||||
background: hsl(0, 70%, 50%);
|
||||
color: white;
|
||||
border-color: transparent;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ec-btn.delete-btn:hover {
|
||||
background: hsl(0, 70%, 45%);
|
||||
}
|
||||
|
||||
.ec-weekday-selector {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.ec-field-label {
|
||||
font-size: 0.85em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.ec-weekdays {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.ec-weekday-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem 0.25rem;
|
||||
border-radius: 0.3rem;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.ec-weekday-label:hover {
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
.ec-weekday-checkbox {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ec-weekday-text {
|
||||
font-size: 0.8em;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* New recurrence block */
|
||||
.recurrence-block {
|
||||
display: grid;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.recurrence-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.recurrence-header .recurrence-summary {
|
||||
font-size: 0.75rem;
|
||||
color: var(--ink);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.recurrence-header .recurrence-summary.muted {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.switch {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.switch input {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
.recurrence-form {
|
||||
display: grid;
|
||||
gap: 0.6rem;
|
||||
padding: 0.6rem 0.75rem 0.75rem;
|
||||
border: 1px solid var(--muted);
|
||||
border-radius: 0.5rem;
|
||||
background: color-mix(in srgb, var(--muted) 15%, transparent);
|
||||
}
|
||||
.line.compact {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.freq-select {
|
||||
padding: 0.4rem 0.55rem;
|
||||
font-size: 0.75rem;
|
||||
border: 1px solid var(--input-border);
|
||||
background: var(--panel-alt);
|
||||
color: var(--ink);
|
||||
border-radius: 0.45rem;
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
background-color 0.18s ease;
|
||||
}
|
||||
.freq-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--input-focus);
|
||||
background: var(--panel-accent);
|
||||
color: var(--ink);
|
||||
box-shadow:
|
||||
0 0 0 1px var(--input-focus),
|
||||
0 0 0 4px rgba(37, 99, 235, 0.15);
|
||||
}
|
||||
.interval-input,
|
||||
.occ-input {
|
||||
display: none;
|
||||
}
|
||||
.ec-field input[type='text'] {
|
||||
border: 1px solid var(--input-border);
|
||||
background: var(--panel-alt);
|
||||
border-radius: 0.45rem;
|
||||
padding: 0.4rem 0.5rem;
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
background-color 0.18s ease,
|
||||
box-shadow 0.18s ease;
|
||||
}
|
||||
.ec-field input[type='text']:focus {
|
||||
outline: none;
|
||||
border-color: var(--input-focus);
|
||||
background: var(--panel-accent);
|
||||
box-shadow:
|
||||
0 0 0 1px var(--input-focus),
|
||||
0 0 0 4px rgba(37, 99, 235, 0.15);
|
||||
}
|
||||
.mini-stepper {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: var(--panel-alt);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
font-size: 0.7rem;
|
||||
height: 1.9rem;
|
||||
}
|
||||
.mini-stepper .step {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--ink);
|
||||
padding: 0 0.55rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
}
|
||||
.mini-stepper .step:hover:not(:disabled) {
|
||||
background: var(--pill-hover-bg);
|
||||
}
|
||||
.mini-stepper .step:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: default;
|
||||
}
|
||||
.mini-stepper .value {
|
||||
min-width: 1.6rem;
|
||||
text-align: center;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.mini-stepper:focus-within {
|
||||
border-color: var(--input-focus);
|
||||
box-shadow:
|
||||
0 0 0 1px var(--input-focus),
|
||||
0 0 0 4px rgba(37, 99, 235, 0.15);
|
||||
}
|
||||
.mini-stepper.occ .value {
|
||||
min-width: 2rem;
|
||||
}
|
||||
.occ-stepper.mini-stepper.occ .value {
|
||||
min-width: 2rem;
|
||||
}
|
||||
.mini-stepper .step:focus-visible {
|
||||
outline: 2px solid var(--input-focus);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
.hint {
|
||||
font-size: 0.65rem;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
/* Recurrence UI */
|
||||
.ec-recurrence-section {
|
||||
display: grid;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.ec-recurrence-toggle {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0.6rem 0.8rem;
|
||||
border: 1px solid var(--muted);
|
||||
background: var(--panel);
|
||||
border-radius: 0.4rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
text-align: left;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
.ec-recurrence-toggle:hover {
|
||||
background: var(--muted);
|
||||
}
|
||||
.ec-recurrence-toggle .toggle-icon {
|
||||
transition: transform 0.2s ease;
|
||||
font-size: 0.7rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
.ec-recurrence-toggle .toggle-icon.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.ec-recurrence-panel {
|
||||
display: grid;
|
||||
gap: 0.6rem;
|
||||
padding: 0.6rem;
|
||||
border: 1px solid var(--muted);
|
||||
border-radius: 0.4rem;
|
||||
background: color-mix(in srgb, var(--muted) 20%, transparent);
|
||||
}
|
||||
|
||||
/* Repeat modes */
|
||||
.ec-repeat-modes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
.ec-repeat-modes .mode-btn {
|
||||
flex: 1 1 auto;
|
||||
padding: 0.4rem 0.6rem;
|
||||
border: 1px solid var(--muted);
|
||||
background: var(--panel);
|
||||
border-radius: 0.4rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease,
|
||||
border-color 0.15s ease;
|
||||
}
|
||||
.ec-repeat-modes .mode-btn.active {
|
||||
background: var(--today);
|
||||
color: #000;
|
||||
border-color: var(--today);
|
||||
font-weight: 600;
|
||||
}
|
||||
.ec-repeat-modes .mode-btn:hover {
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
.ec-occurrences-field {
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
.ec-occurrences-field .ec-field input[type='number'] {
|
||||
max-width: 6rem;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user