Major new version (#2)

Release Notes

Architecture
- Component refactor: removed monoliths (`Calendar.vue`, `CalendarGrid.vue`); expanded granular view + header/control components.
- Dialog system introduced (`BaseDialog`, `SettingsDialog`).

State & Data
- Store redesigned: Map-based events + recurrence map; mutation counters.
- Local persistence + undo/redo history (custom plugins).

Date & Holidays
- Migrated all date logic to `date-fns` (+ tz).
- Added national holiday support (toggle + loading utilities).

Recurrence & Events
- Consolidated recurrence handling; fixes for monthly edge days (29–31), annual, multi‑day, and complex weekly repeats.
- Reliable splitting/moving/resizing/deletion of repeating and multi‑day events.

Interaction & UX
- Double‑tap to create events; improved drag (multi‑day + position retention).
- Scroll & inertial/momentum navigation; year change via numeric scroller.
- Movable event dialog; live settings application.

Performance
- Progressive / virtual week rendering, reduced off‑screen buffer.
- Targeted repaint strategy; minimized full re-renders.

Plugins Added
- History, undo normalization, persistence, scroll manager, virtual weeks.

Styling & Layout
- Responsive + compact layout refinements; header restructured.
- Simplified visual elements (removed dots/overflow text); holiday styling adjustments.

Reliability / Fixes
- Numerous recurrence, deletion, orientation/rotation, and event indexing corrections.
- Cross-browser fallback (Firefox week info).

Dependencies Added
- date-fns, date-fns-tz, date-holidays, pinia-plugin-persistedstate.

Net Change
- 28 files modified; ~4.4K insertions / ~2.2K deletions (major refactor + feature set).
This commit is contained in:
2025-08-26 03:58:24 +00:00
parent 86a06acccb
commit dbe4c99b07
28 changed files with 4465 additions and 2207 deletions
+200
View File
@@ -0,0 +1,200 @@
// Custom lightweight undo/redo specifically for calendar store with Map support
// Adds store.$history = { undo(), redo(), canUndo, canRedo, clear(), pushManual() }
// Wraps action calls to create history entries only for meaningful mutations.
function deepCloneCalendarState(raw) {
// We only need to snapshot keys we care about; omit volatile fields
const { today, events, config, weekend } = raw
return {
today,
weekend: Array.isArray(weekend) ? [...weekend] : weekend,
config: JSON.parse(JSON.stringify(config)),
events: new Map([...events].map(([k, v]) => [k, { ...v }])),
}
}
function restoreCalendarState(store, snap) {
store.today = snap.today
store.weekend = Array.isArray(snap.weekend) ? [...snap.weekend] : snap.weekend
store.config = JSON.parse(JSON.stringify(snap.config))
store.events = new Map([...snap.events].map(([k, v]) => [k, { ...v }]))
store.eventsMutation = (store.eventsMutation + 1) % 1_000_000_000
}
export function calendarHistory({ store }) {
if (store.$id !== 'calendar') return
const max = 100 // history depth limit
const history = [] // past states
let pointer = -1 // index of current state in history
let isRestoring = false
let lastSerialized = null
// Compound editing session (e.g. event dialog) flags
let compoundActive = false
let compoundBaseSig = null
let compoundChanged = false
function serializeForComparison() {
const evCount = store.events instanceof Map ? store.events.size : 0
const em = store.eventsMutation || 0
return `${em}|${evCount}|${store.today}|${JSON.stringify(store.config)}`
}
function pushSnapshot() {
if (isRestoring) return
const sig = serializeForComparison()
if (sig === lastSerialized) return
// Drop any redo branch
if (pointer < history.length - 1) history.splice(pointer + 1)
history.push(deepCloneCalendarState(store))
if (history.length > max) history.shift()
pointer = history.length - 1
lastSerialized = sig
bumpIndicators()
// console.log('[history] pushed', pointer, sig)
}
function bumpIndicators() {
if (typeof store.historyTick === 'number') {
store.historyTick = (store.historyTick + 1) % 1_000_000_000
}
if (typeof store.historyCanUndo === 'boolean') {
store.historyCanUndo = pointer > 0
}
if (typeof store.historyCanRedo === 'boolean') {
store.historyCanRedo = pointer >= 0 && pointer < history.length - 1
}
}
function markPotentialChange() {
if (isRestoring) return
if (compoundActive) {
const sig = serializeForComparison()
if (sig !== compoundBaseSig) compoundChanged = true
return
}
pushSnapshot()
}
function beginCompound() {
if (compoundActive) return
compoundActive = true
compoundBaseSig = serializeForComparison()
compoundChanged = false
}
function endCompound() {
if (!compoundActive) return
const finalSig = serializeForComparison()
const changed = compoundChanged || finalSig !== compoundBaseSig
compoundActive = false
compoundBaseSig = null
if (changed) pushSnapshot()
else bumpIndicators() // session ended without change still refresh flags
}
function undo() {
// Ensure any active compound changes are finalized before moving back
if (compoundActive) endCompound()
else {
// If current state differs from last snapshot, push it so redo can restore it
const curSig = serializeForComparison()
if (curSig !== lastSerialized) pushSnapshot()
}
if (pointer <= 0) return
pointer--
isRestoring = true
try {
restoreCalendarState(store, history[pointer])
lastSerialized = serializeForComparison()
} finally {
isRestoring = false
}
bumpIndicators()
}
function redo() {
if (compoundActive) endCompound()
else {
const curSig = serializeForComparison()
if (curSig !== lastSerialized) pushSnapshot()
}
if (pointer >= history.length - 1) return
pointer++
isRestoring = true
try {
restoreCalendarState(store, history[pointer])
lastSerialized = serializeForComparison()
} finally {
isRestoring = false
}
bumpIndicators()
}
function clear() {
history.length = 0
pointer = -1
lastSerialized = null
bumpIndicators()
}
// Wrap selected mutating actions to push snapshot AFTER they run if state changed.
const actionNames = [
'createEvent',
'deleteEvent',
'deleteFirstOccurrence',
'deleteSingleOccurrence',
'deleteFromOccurrence',
'setEventRange',
'splitMoveVirtualOccurrence',
'splitRepeatSeries',
'_terminateRepeatSeriesAtIndex',
'toggleHolidays',
'initializeHolidays',
]
for (const name of actionNames) {
if (typeof store[name] === 'function') {
const original = store[name].bind(store)
store[name] = (...args) => {
const beforeSig = serializeForComparison()
const result = original(...args)
const afterSig = serializeForComparison()
if (afterSig !== beforeSig) markPotentialChange()
return result
}
}
}
// Capture direct property edits (e.g., deep field edits signaled via touchEvents())
store.$subscribe((mutation, _state) => {
if (mutation.storeId !== 'calendar') return
markPotentialChange()
})
// Initial snapshot after hydration (next microtask to let persistence load)
Promise.resolve().then(() => pushSnapshot())
store.$history = {
undo,
redo,
clear,
pushManual: pushSnapshot,
beginCompound,
endCompound,
flush() {
pushSnapshot()
},
get canUndo() {
return pointer > 0
},
get canRedo() {
return pointer >= 0 && pointer < history.length - 1
},
get compoundActive() {
return compoundActive
},
_debug() {
return { pointer, length: history.length }
},
}
}