@dayflow/plugin-sidebar 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -1,4 +1,4 @@
1
- import { useLocale, createPortal, cancelButton, ChevronsUpDown, Check, ChevronRight, sidebarHeader, sidebarHeaderToggle, PanelRightClose, PanelRightOpen, sidebarHeaderTitle, getCalendarColorsForHex, generateUniKey, downloadICS, ContentSlot, MiniCalendar, ContextMenu, ContextMenuLabel, ContextMenuItem, ContextMenuSeparator, ContextMenuColorPicker, DefaultColorPicker, BlossomColorPicker, importICSFile, sidebarContainer, normalizeCssWidth, registerSidebarImplementation, CreateCalendarDialog } from '@dayflow/core';
1
+ import { ChevronDown, AudioLines, useLocale, createPortal, cancelButton, ChevronsUpDown, Check, ChevronRight, sidebarHeader, sidebarHeaderToggle, PanelRightClose, PanelRightOpen, sidebarHeaderTitle, getCalendarColorsForHex, generateUniKey, downloadICS, ContentSlot, MiniCalendar, ContextMenu, ContextMenuLabel, ContextMenuItem, ContextMenuSeparator, ContextMenuColorPicker, DefaultColorPicker, BlossomColorPicker, importICSFile, parseICS, sidebarContainer, normalizeCssWidth, registerSidebarImplementation, CreateCalendarDialog } from '@dayflow/core';
2
2
  import { options, Fragment, h } from 'preact';
3
3
  import { useState, useRef, useCallback, useEffect, useMemo } from 'preact/hooks';
4
4
  import { hexToHsl, lightnessToSliderValue } from '@dayflow/blossom-color-picker';
@@ -44,21 +44,33 @@ const getCalendarInitials = (calendar) => {
44
44
  const name = calendar.name || calendar.id;
45
45
  return name.charAt(0).toUpperCase();
46
46
  };
47
+ const CalendarItem = ({ calendar, isDraggable, isEditable: _isEditable, editingId, editingName, setEditingName, editInputRef, draggedCalendarId, dropTarget, activeContextMenuCalendarId, onDragStart, onDragEnd, onDragOver, onDragLeave, onDrop, onContextMenu, onToggleVisibility, onRenameStart, onRenameSave, onRenameKeyDown, }) => {
48
+ var _a;
49
+ const isVisible = calendar.isVisible !== false;
50
+ const calendarColor = ((_a = calendar.colors) === null || _a === void 0 ? void 0 : _a.lineColor) || '#3b82f6';
51
+ const showIcon = Boolean(calendar.icon);
52
+ const isDropTarget = (dropTarget === null || dropTarget === void 0 ? void 0 : dropTarget.id) === calendar.id;
53
+ const isActive = activeContextMenuCalendarId === calendar.id || editingId === calendar.id;
54
+ return (u("li", { className: 'df-calendar-list-item relative', onDragOver: e => onDragOver(e, calendar.id), onDragLeave: onDragLeave, onDrop: () => onDrop(calendar), onContextMenu: e => onContextMenu(e, calendar.id), children: [isDropTarget && dropTarget.position === 'top' && (u("div", { className: 'pointer-events-none absolute top-0 right-0 left-0 z-10 h-0.5 bg-[var(--df-color-primary)]' })), u("div", { draggable: isDraggable && !editingId, onDragStart: e => onDragStart(calendar, e), onDragEnd: onDragEnd, className: `rounded transition ${draggedCalendarId === calendar.id ? 'opacity-50' : ''} ${isDraggable ? 'cursor-grab' : 'cursor-default'}`, children: u("div", { className: `group flex items-center rounded px-2 py-2 transition hover:bg-gray-100 dark:hover:bg-slate-800 ${isActive ? 'bg-gray-100 dark:bg-slate-800' : ''}`, title: calendar.name, children: [u("input", { type: 'checkbox', className: 'df-calendar-checkbox shrink-0 cursor-pointer', style: {
55
+ '--checkbox-color': calendarColor,
56
+ }, checked: isVisible, onChange: event => onToggleVisibility(calendar.id, event.target.checked) }), showIcon && (u("span", { className: 'ml-2 flex h-5 w-5 shrink-0 items-center justify-center text-xs font-semibold text-white', "aria-hidden": 'true', children: getCalendarInitials(calendar) })), editingId === calendar.id ? (u("input", { ref: editInputRef, type: 'text', value: editingName, onChange: e => setEditingName(e.target.value), onBlur: onRenameSave, onKeyDown: onRenameKeyDown, className: 'ml-2 h-5 min-w-0 flex-1 rounded bg-white px-0 py-0 text-sm text-gray-900 focus:outline-none dark:bg-slate-700 dark:text-gray-100', onClick: e => e.stopPropagation() })) : (u(Fragment, { children: [u("span", { className: 'ml-2 flex-1 truncate pl-1 text-sm text-gray-700 group-hover:text-gray-900 dark:text-gray-200 dark:group-hover:text-white', onDblClick: () => onRenameStart(calendar), children: calendar.name || calendar.id }), calendar.subscribed && (u(AudioLines, { width: 13, height: 13, className: 'ml-1 shrink-0 text-gray-400 dark:text-gray-500' }))] }))] }) }), isDropTarget && dropTarget.position === 'bottom' && (u("div", { className: 'pointer-events-none absolute right-0 bottom-0 left-0 z-10 h-0.5 bg-[var(--df-color-primary)]' }))] }, calendar.id));
57
+ };
47
58
  const CalendarList = ({ calendars, onToggleVisibility, onReorder, onRename, onContextMenu, editingId, setEditingId, activeContextMenuCalendarId, isDraggable = true, isEditable = true, }) => {
59
+ var _a;
48
60
  const [editingName, setEditingName] = useState('');
49
61
  const editInputRef = useRef(null);
50
62
  const isProcessedRef = useRef(false);
51
63
  // Drag state
52
64
  const [draggedCalendarId, setDraggedCalendarId] = useState(null);
53
65
  const [dropTarget, setDropTarget] = useState(null);
66
+ // Collapsed sources state
67
+ const [collapsedSources, setCollapsedSources] = useState({});
54
68
  const handleDragStart = useCallback((calendar, e) => {
55
- // Prevent dragging when editing or not draggable
56
69
  if (editingId || !isDraggable) {
57
70
  e.preventDefault();
58
71
  return;
59
72
  }
60
73
  setDraggedCalendarId(calendar.id);
61
- // Store calendar data for drop handling
62
74
  const dragData = {
63
75
  calendarId: calendar.id,
64
76
  calendarName: calendar.name,
@@ -107,11 +119,9 @@ const CalendarList = ({ calendars, onToggleVisibility, onReorder, onRename, onCo
107
119
  return;
108
120
  const fromIndex = calendars.findIndex(c => c.id === draggedCalendarId);
109
121
  let toIndex = calendars.findIndex(c => c.id === targetCalendar.id);
110
- // Adjust target index based on position
111
122
  if (dropTarget.position === 'bottom') {
112
123
  toIndex += 1;
113
124
  }
114
- // Adjust for removal of the item
115
125
  if (toIndex > fromIndex) {
116
126
  toIndex -= 1;
117
127
  }
@@ -169,24 +179,75 @@ const CalendarList = ({ calendars, onToggleVisibility, onReorder, onRename, onCo
169
179
  }
170
180
  }
171
181
  }, [editingId, calendars]);
172
- return (u("div", { className: 'df-calendar-list flex-1 overflow-y-auto px-2 pb-3', children: u("ul", { className: 'relative space-y-1', children: calendars.map(calendar => {
173
- var _a;
174
- const isVisible = calendar.isVisible !== false;
175
- const calendarColor = ((_a = calendar.colors) === null || _a === void 0 ? void 0 : _a.lineColor) || '#3b82f6';
176
- const showIcon = Boolean(calendar.icon);
177
- const isDropTarget = (dropTarget === null || dropTarget === void 0 ? void 0 : dropTarget.id) === calendar.id;
178
- const isActive = activeContextMenuCalendarId === calendar.id ||
179
- editingId === calendar.id;
180
- return (u("li", { className: 'df-calendar-list-item relative', onDragOver: e => handleDragOver(e, calendar.id), onDragLeave: handleDragLeave, onDrop: () => handleDrop(calendar), onContextMenu: e => onContextMenu(e, calendar.id), children: [isDropTarget && dropTarget.position === 'top' && (u("div", { className: 'pointer-events-none absolute top-0 right-0 left-0 z-10 h-0.5 bg-[var(--df-color-primary)]' })), u("div", { draggable: isDraggable && !editingId, onDragStart: e => handleDragStart(calendar, e), onDragEnd: handleDragEnd, className: `rounded transition ${draggedCalendarId === calendar.id ? 'opacity-50' : ''} ${isDraggable ? 'cursor-grab' : 'cursor-default'}`, children: u("div", { className: `group flex items-center rounded px-2 py-2 transition hover:bg-gray-100 dark:hover:bg-slate-800 ${isActive ? 'bg-gray-100 dark:bg-slate-800' : ''}`, title: calendar.name, children: [u("input", { type: 'checkbox', className: 'df-calendar-checkbox shrink-0 cursor-pointer', style: {
181
- '--checkbox-color': calendarColor,
182
- }, checked: isVisible, onChange: event => onToggleVisibility(calendar.id, event.target.checked) }), showIcon && (u("span", { className: 'ml-2 flex h-5 w-5 shrink-0 items-center justify-center text-xs font-semibold text-white', "aria-hidden": 'true', children: getCalendarInitials(calendar) })), editingId === calendar.id ? (u("input", { ref: editInputRef, type: 'text', value: editingName, onChange: e => setEditingName(e.target.value), onBlur: handleRenameSave, onKeyDown: handleRenameKeyDown, className: 'ml-2 h-5 min-w-0 flex-1 rounded bg-white px-0 py-0 text-sm text-gray-900 focus:outline-none dark:bg-slate-700 dark:text-gray-100', onClick: e => e.stopPropagation() })) : (u("span", { className: 'ml-2 flex-1 truncate pl-1 text-sm text-gray-700 group-hover:text-gray-900 dark:text-gray-200 dark:group-hover:text-white', onDblClick: () => handleRenameStart(calendar), children: calendar.name || calendar.id }))] }) }), isDropTarget && dropTarget.position === 'bottom' && (u("div", { className: 'pointer-events-none absolute right-0 bottom-0 left-0 z-10 h-0.5 bg-[var(--df-color-primary)]' }))] }, calendar.id));
183
- }) }) }));
182
+ const toggleSource = useCallback((source) => {
183
+ setCollapsedSources(prev => (Object.assign(Object.assign({}, prev), { [source]: !prev[source] })));
184
+ }, []);
185
+ // Shared item props (excludes per-item fields)
186
+ const sharedItemProps = {
187
+ isDraggable,
188
+ isEditable,
189
+ editingId,
190
+ editingName,
191
+ setEditingName,
192
+ editInputRef,
193
+ isProcessedRef,
194
+ draggedCalendarId,
195
+ dropTarget,
196
+ activeContextMenuCalendarId,
197
+ onDragStart: handleDragStart,
198
+ onDragEnd: handleDragEnd,
199
+ onDragOver: handleDragOver,
200
+ onDragLeave: handleDragLeave,
201
+ onDrop: handleDrop,
202
+ onContextMenu,
203
+ onToggleVisibility,
204
+ onRenameStart: handleRenameStart,
205
+ onRenameSave: handleRenameSave,
206
+ onRenameKeyDown: handleRenameKeyDown,
207
+ setEditingId,
208
+ };
209
+ // Check if any calendar has a source
210
+ const hasSources = calendars.some(c => c.source);
211
+ if (!hasSources) {
212
+ // Flat list (original behaviour)
213
+ return (u("div", { className: 'df-calendar-list flex-1 overflow-y-auto px-2 pb-3', children: u("ul", { className: 'relative space-y-1', children: calendars.map(calendar => (u(CalendarItem, Object.assign({ calendar: calendar }, sharedItemProps), calendar.id))) }) }));
214
+ }
215
+ // Group calendars by source; calendars without a source go into a null group
216
+ const groups = new Map();
217
+ for (const calendar of calendars) {
218
+ const key = (_a = calendar.source) !== null && _a !== void 0 ? _a : null;
219
+ if (!groups.has(key))
220
+ groups.set(key, []);
221
+ groups.get(key).push(calendar);
222
+ }
223
+ return (u("div", { className: 'df-calendar-list flex-1 overflow-y-auto px-2 pb-3', children: [groups.has(null) && (u("ul", { className: 'relative space-y-1', children: groups.get(null).map(calendar => (u(CalendarItem, Object.assign({ calendar: calendar }, sharedItemProps), calendar.id))) })), Array.from(groups.entries())
224
+ .filter(([source]) => source !== null)
225
+ .map(([source, groupCalendars]) => {
226
+ const isCollapsed = collapsedSources[source];
227
+ return (u("div", { className: 'mt-1', children: [u("button", { type: 'button', className: 'flex w-full items-center justify-between rounded px-2 py-1.5 text-xs font-medium text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-slate-800', onClick: () => toggleSource(source), children: [u("span", { className: 'truncate', children: source }), u(ChevronDown, { width: 13, height: 13, className: `ml-1 shrink-0 transition-transform duration-200 ${isCollapsed ? '-rotate-90' : ''}` })] }), u("div", { style: {
228
+ display: 'grid',
229
+ gridTemplateRows: isCollapsed ? '0fr' : '1fr',
230
+ transition: 'grid-template-rows 200ms ease',
231
+ }, children: u("div", { style: { overflow: 'hidden' }, children: u("ul", { className: 'relative space-y-1', children: groupCalendars.map(calendar => (u(CalendarItem, Object.assign({ calendar: calendar }, sharedItemProps), calendar.id))) }) }) })] }, source));
232
+ })] }));
184
233
  };
185
234
 
235
+ const CalendarChip = ({ name, color }) => (u("span", { className: 'mx-0.5 inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium', style: { backgroundColor: `${color}26`, color }, children: name }));
236
+
237
+ const CAL_SENTINEL = '\u0001C\u0001';
238
+ function renderWithChip(template, name, color) {
239
+ return template
240
+ .split(CAL_SENTINEL)
241
+ .flatMap((part, i) => i === 0
242
+ ? [part]
243
+ : [u(CalendarChip, { name: name, color: color }, i), part]);
244
+ }
186
245
  const DeleteCalendarDialog = ({ calendarId, calendarName, calendars, step, onStepChange, onConfirmDelete, onCancel, onMergeSelect, }) => {
246
+ var _a, _b;
187
247
  const [showMergeDropdown, setShowMergeDropdown] = useState(false);
188
248
  const { t } = useLocale();
189
- return createPortal(u("div", { className: 'df-portal fixed inset-0 z-[9999] flex items-center justify-center bg-black/50', children: u("div", { className: 'w-full max-w-md rounded-lg bg-background p-6 shadow-xl', children: step === 'initial' ? (u(Fragment, { children: [u("h2", { className: 'text-lg font-semibold text-gray-900 dark:text-white', children: t('deleteCalendar', { calendarName }) }), u("p", { className: 'mt-3 text-sm text-gray-600 dark:text-gray-300', children: t('deleteCalendarMessage', { calendarName }) }), u("div", { className: 'mt-6 flex items-center justify-between', children: [u("div", { className: 'relative', children: [u("button", { type: 'button', onClick: () => setShowMergeDropdown(!showMergeDropdown), className: 'flex items-center gap-1 rounded-md border border-gray-300 px-4 py-2 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-slate-700', children: t('merge') }), showMergeDropdown && (u("div", { className: 'absolute top-full left-0 z-10 mt-1 max-h-60 w-max min-w-full overflow-y-auto rounded-md border border-gray-200 bg-background shadow-lg dark:border-slate-700', children: calendars
249
+ const calendarColor = (_b = (_a = calendars.find(c => c.id === calendarId)) === null || _a === void 0 ? void 0 : _a.colors.lineColor) !== null && _b !== void 0 ? _b : '#6b7280';
250
+ return createPortal(u("div", { className: 'df-portal fixed inset-0 z-[9999] flex items-center justify-center bg-black/50', children: u("div", { className: 'w-full max-w-md rounded-lg bg-white p-6 shadow-xl dark:bg-gray-800', children: step === 'initial' ? (u(Fragment, { children: [u("h2", { className: 'text-lg font-semibold text-gray-900 dark:text-white', children: t('deleteCalendar', { calendarName }) }), u("p", { className: 'mt-3 flex flex-wrap items-center gap-y-0.5 text-sm text-gray-600 dark:text-gray-300', children: renderWithChip(t('deleteCalendarMessage', { calendarName: CAL_SENTINEL }), calendarName, calendarColor) }), u("div", { className: 'mt-6 flex items-center justify-between', children: [u("div", { className: 'relative', children: [u("button", { type: 'button', onClick: () => setShowMergeDropdown(!showMergeDropdown), className: 'flex items-center gap-1 rounded-md border border-gray-300 px-4 py-2 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-slate-700', children: t('merge') }), showMergeDropdown && (u("div", { className: 'absolute top-full left-0 z-10 mt-1 max-h-60 w-max min-w-full overflow-y-auto rounded-md border border-gray-200 bg-white shadow-lg dark:border-slate-700 dark:bg-gray-800', children: calendars
190
251
  .filter(c => c.id !== calendarId)
191
252
  .map(calendar => (u("div", { className: 'flex cursor-pointer items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-slate-700', onClick: () => {
192
253
  onMergeSelect(calendar.id);
@@ -243,6 +304,7 @@ const ImportCalendarDialog = ({ calendars, filename, onConfirm, onCancel, }) =>
243
304
  top: rect.bottom,
244
305
  left: rect.left,
245
306
  width: rect.width,
307
+ overscrollBehavior: 'none',
246
308
  }, children: u("div", { className: 'py-1', children: [calendars.map(calendar => (u("div", { className: `flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100 dark:hover:bg-slate-700 ${selectedCalendarId === calendar.id ? 'bg-primary/10' : ''}`, onClick: () => handleSelect(calendar.id), children: [u("div", { className: 'mr-3 h-3 w-3 shrink-0 rounded-sm', style: { backgroundColor: calendar.colors.lineColor } }), u("span", { className: `flex-1 truncate text-sm ${selectedCalendarId === calendar.id ? 'font-medium text-primary' : 'text-gray-700 dark:text-gray-200'}`, children: calendar.name || calendar.id }), selectedCalendarId === calendar.id && (u(Check, { className: 'ml-2 h-4 w-4 shrink-0 text-primary' }))] }, calendar.id))), u("div", { className: 'my-1 border-t border-gray-100 dark:border-slate-700' }), u("div", { className: `flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100 dark:hover:bg-slate-700 ${isNewSelected ? 'bg-primary/10' : ''}`, onClick: () => handleSelect(NEW_CALENDAR_ID), children: [u("span", { className: `flex-1 truncate text-sm ${isNewSelected ? 'font-medium text-primary' : 'pl-6 text-gray-700 dark:text-gray-200'}`, children: [t('newCalendar') || 'New Calendar', ": ", filename] }), isNewSelected && (u(Check, { className: 'ml-2 h-4 w-4 shrink-0 text-primary' }))] })] }) }), document.body);
247
309
  };
248
310
  return (u("div", { className: 'df-portal fixed inset-0 z-100 flex items-center justify-center bg-black/50', children: u("div", { className: 'w-full max-w-md rounded-lg bg-white p-6 shadow-xl dark:bg-gray-900', children: [u("h2", { className: 'mb-4 text-lg font-semibold text-gray-900 dark:text-white', children: t('addSchedule') || 'Add Schedule' }), u("p", { className: 'mb-4 text-sm text-gray-600 dark:text-gray-300', children: t('importCalendarMessage') ||
@@ -251,9 +313,29 @@ const ImportCalendarDialog = ({ calendars, filename, onConfirm, onCancel, }) =>
251
313
  : (selectedCalendar === null || selectedCalendar === void 0 ? void 0 : selectedCalendar.name) || (selectedCalendar === null || selectedCalendar === void 0 ? void 0 : selectedCalendar.id) }), u(ChevronsUpDown, { className: 'ml-2 h-4 w-4 shrink-0 text-gray-400' })] }), renderDropdown()] }), u("div", { className: 'mt-8 flex justify-end gap-3', children: [u("button", { type: 'button', onClick: onCancel, className: cancelButton, children: t('cancel') || 'Cancel' }), u("button", { type: 'button', onClick: () => onConfirm(selectedCalendarId), className: 'rounded-md bg-primary px-6 py-2 text-sm font-medium text-primary-foreground shadow-sm transition-colors hover:bg-primary/90', children: t('ok') || 'OK' })] })] }) }));
252
314
  };
253
315
 
254
- const MergeCalendarDialog = ({ sourceName, targetName, onConfirm, onCancel, }) => {
316
+ const SOURCE_SENTINEL = '\u0001S\u0001';
317
+ const TARGET_SENTINEL = '\u0001T\u0001';
318
+ function renderLine(line, source, target) {
319
+ return line
320
+ .split(new RegExp(`(${SOURCE_SENTINEL}|${TARGET_SENTINEL})`))
321
+ .map((part, i) => {
322
+ if (part === SOURCE_SENTINEL)
323
+ return u(CalendarChip, { name: source.name, color: source.color }, i);
324
+ if (part === TARGET_SENTINEL)
325
+ return u(CalendarChip, { name: target.name, color: target.color }, i);
326
+ return part;
327
+ });
328
+ }
329
+ const MergeCalendarDialog = ({ sourceName, sourceColor, targetName, targetColor, onConfirm, onCancel, }) => {
255
330
  const { t } = useLocale();
256
- return (u("div", { className: 'fixed inset-0 z-100 flex items-center justify-center bg-black/50', children: u("div", { className: 'w-full max-w-md rounded-lg bg-background p-6 shadow-xl', children: [u("h2", { className: 'text-lg font-semibold text-gray-900 dark:text-white', children: t('mergeConfirmTitle', { sourceName, targetName }) }), u("p", { className: 'mt-3 text-sm text-gray-600 dark:text-gray-300', children: t('mergeConfirmMessage', { sourceName, targetName }) }), u("div", { className: 'mt-6 flex justify-end gap-3', children: [u("button", { type: 'button', onClick: onCancel, className: cancelButton, children: t('cancel') }), u("button", { type: 'button', onClick: onConfirm, className: 'rounded-md bg-destructive px-3 py-2 text-xs font-medium text-destructive-foreground hover:bg-destructive/90', children: t('merge') })] })] }) }));
331
+ const source = { name: sourceName, color: sourceColor };
332
+ const target = { name: targetName, color: targetColor };
333
+ const messageTemplate = t('mergeConfirmMessage', {
334
+ sourceName: SOURCE_SENTINEL,
335
+ targetName: TARGET_SENTINEL,
336
+ });
337
+ const messageLines = messageTemplate.split('\n');
338
+ return (u("div", { className: 'df-portal fixed inset-0 z-[9999] flex items-center justify-center bg-black/50', children: u("div", { className: 'rounded-lg bg-white p-6 shadow-xl dark:bg-gray-800', children: [u("h2", { className: 'text-lg font-semibold text-gray-900 dark:text-white', children: t('mergeConfirmTitle', { sourceName, targetName }) }), u("div", { className: 'mt-3 space-y-1 text-sm text-gray-600 dark:text-gray-300', children: messageLines.map((line, i) => (u("p", { className: 'flex flex-wrap items-center gap-y-0.5', children: renderLine(line, source, target) }, i))) }), u("div", { className: 'mt-6 flex justify-end gap-3', children: [u("button", { type: 'button', onClick: onCancel, className: cancelButton, children: t('cancel') }), u("button", { type: 'button', onClick: onConfirm, className: 'rounded-md bg-destructive px-3 py-2 text-xs font-medium text-destructive-foreground hover:bg-destructive/90', children: t('merge') })] })] }) }));
257
339
  };
258
340
 
259
341
  const stopPropagation = (e) => e.stopPropagation();
@@ -302,8 +384,38 @@ const SidebarHeader = ({ isCollapsed, onCollapseToggle, }) => {
302
384
  return (u("div", { className: sidebarHeader, children: [u("button", { type: 'button', "aria-label": isCollapsed ? t('expandSidebar') : t('collapseSidebar'), className: sidebarHeaderToggle, onClick: onCollapseToggle, children: isCollapsed ? (u(PanelRightClose, { className: 'h-4 w-4 text-gray-500 dark:text-gray-400' })) : (u(PanelRightOpen, { className: 'h-4 w-4 text-gray-500 dark:text-gray-400' })) }), !isCollapsed && (u("div", { className: 'ml-3 flex flex-1 items-center justify-between', children: u("span", { className: sidebarHeaderTitle, children: t('calendars') }) }))] }));
303
385
  };
304
386
 
387
+ const SubscribeCalendarDialog = ({ onSubscribe, onCancel, }) => {
388
+ const { t } = useLocale();
389
+ const [url, setUrl] = useState('');
390
+ const [loading, setLoading] = useState(false);
391
+ const [error, setError] = useState(null);
392
+ const handleSubmit = () => __awaiter(void 0, void 0, void 0, function* () {
393
+ const trimmed = url.trim();
394
+ if (!trimmed)
395
+ return;
396
+ setError(null);
397
+ setLoading(true);
398
+ try {
399
+ yield onSubscribe(trimmed);
400
+ }
401
+ catch (_a) {
402
+ setError(t('subscribeError'));
403
+ }
404
+ finally {
405
+ setLoading(false);
406
+ }
407
+ });
408
+ const handleKeyDown = (e) => {
409
+ if (e.key === 'Enter')
410
+ handleSubmit();
411
+ if (e.key === 'Escape')
412
+ onCancel();
413
+ };
414
+ return (u("div", { className: 'df-portal fixed inset-0 z-[9999] flex items-center justify-center bg-black/50', children: u("div", { className: 'w-full max-w-xl rounded-lg bg-white p-6 shadow-xl dark:bg-gray-800', children: [u("h2", { className: 'text-lg font-semibold text-gray-900 dark:text-white', children: t('subscribeCalendarTitle') }), u("div", { className: 'mt-4', children: [u("div", { className: 'flex items-center gap-3', children: [u("label", { className: 'shrink-0 text-sm font-medium text-gray-700 dark:text-gray-300', children: t('calendarUrl') }), u("input", { type: 'url', value: url, onInput: e => setUrl(e.target.value), onKeyDown: handleKeyDown, placeholder: t('calendarUrlPlaceholder'), disabled: loading, autoFocus: true, className: 'flex-1 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:border-primary focus:ring-1 focus:ring-primary focus:outline-none disabled:opacity-50 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400' })] }), error && (u("p", { className: 'mt-2 text-xs text-red-500 dark:text-red-400', children: error }))] }), u("div", { className: 'mt-6 flex justify-end gap-3', children: [u("button", { type: 'button', onClick: onCancel, disabled: loading, className: cancelButton, children: t('cancel') }), u("button", { type: 'button', onClick: handleSubmit, disabled: loading || !url.trim(), className: 'rounded-md bg-primary px-4 py-2 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50', children: loading ? t('fetchingCalendar') : t('subscribe') })] })] }) }));
415
+ };
416
+
305
417
  const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCollapsed, setCollapsed, renderCalendarContextMenu, renderSidebarHeader, editingCalendarId: propEditingCalendarId, setEditingCalendarId: propSetEditingCalendarId, onCreateCalendar, }) => {
306
- var _a, _b, _c, _d;
418
+ var _a, _b;
307
419
  const { t } = useLocale();
308
420
  // Detect if custom color picker slot is provided
309
421
  const hasCustomPicker = app.state.overrides.includes('colorPicker');
@@ -331,6 +443,8 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
331
443
  const [deleteState, setDeleteState] = useState(null);
332
444
  // Import Calendar State
333
445
  const [importState, setImportState] = useState(null);
446
+ // Subscribe Calendar State
447
+ const [subscribeDialogOpen, setSubscribeDialogOpen] = useState(false);
334
448
  const handleContextMenu = useCallback((e, calendarId) => {
335
449
  e.preventDefault();
336
450
  e.stopPropagation(); // Stop propagation to prevent sidebar context menu
@@ -445,6 +559,51 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
445
559
  (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click();
446
560
  handleCloseSidebarContextMenu();
447
561
  }, [handleCloseSidebarContextMenu]);
562
+ // Subscribe Calendar handler
563
+ const handleSubscribeClick = useCallback(() => {
564
+ setSubscribeDialogOpen(true);
565
+ handleCloseSidebarContextMenu();
566
+ }, [handleCloseSidebarContextMenu]);
567
+ const handleSubscribeConfirm = useCallback((url) => __awaiter(void 0, void 0, void 0, function* () {
568
+ const response = yield fetch(url);
569
+ if (!response.ok)
570
+ throw new Error(`HTTP ${response.status}`);
571
+ const icsContent = yield response.text();
572
+ const result = parseICS(icsContent);
573
+ // Extract calendar name from X-WR-CALNAME if present
574
+ const nameMatch = icsContent.match(/X-WR-CALNAME[^:]*:([^\r\n]+)/);
575
+ const calendarName = nameMatch
576
+ ? nameMatch[1].trim()
577
+ : new URL(url).hostname;
578
+ const presetColors = [
579
+ '#3b82f6',
580
+ '#10b981',
581
+ '#8b5cf6',
582
+ '#f59e0b',
583
+ '#ef4444',
584
+ '#f97316',
585
+ '#ec4899',
586
+ '#14b8a6',
587
+ '#6366f1',
588
+ '#6b7280',
589
+ ];
590
+ const randomColor = presetColors[Math.floor(Math.random() * presetColors.length)];
591
+ const { colors: calendarColors, darkColors } = getCalendarColorsForHex(randomColor);
592
+ const calendarId = generateUniKey();
593
+ app.createCalendar({
594
+ id: calendarId,
595
+ name: calendarName,
596
+ isDefault: false,
597
+ colors: calendarColors,
598
+ darkColors,
599
+ isVisible: true,
600
+ subscribed: true,
601
+ });
602
+ result.events.forEach(event => {
603
+ app.addEvent(Object.assign(Object.assign({}, event), { calendarId }));
604
+ });
605
+ setSubscribeDialogOpen(false);
606
+ }), [app]);
448
607
  const handleFileChange = useCallback((e) => __awaiter(void 0, void 0, void 0, function* () {
449
608
  var _a;
450
609
  const file = (_a = e.currentTarget.files) === null || _a === void 0 ? void 0 : _a[0];
@@ -514,14 +673,18 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
514
673
  handleCloseContextMenu();
515
674
  }
516
675
  }, [contextMenu, calendars, app, handleCloseContextMenu]);
517
- const sourceCalendarName = mergeState
518
- ? ((_a = calendars.find(c => c.id === mergeState.sourceId)) === null || _a === void 0 ? void 0 : _a.name) || 'Unknown'
519
- : '';
520
- const targetCalendarName = mergeState
521
- ? ((_b = calendars.find(c => c.id === mergeState.targetId)) === null || _b === void 0 ? void 0 : _b.name) || 'Unknown'
522
- : '';
676
+ const sourceCalendar = mergeState
677
+ ? calendars.find(c => c.id === mergeState.sourceId)
678
+ : null;
679
+ const targetCalendar = mergeState
680
+ ? calendars.find(c => c.id === mergeState.targetId)
681
+ : null;
682
+ const sourceCalendarName = (sourceCalendar === null || sourceCalendar === void 0 ? void 0 : sourceCalendar.name) || 'Unknown';
683
+ const targetCalendarName = (targetCalendar === null || targetCalendar === void 0 ? void 0 : targetCalendar.name) || 'Unknown';
684
+ const sourceCalendarColor = (sourceCalendar === null || sourceCalendar === void 0 ? void 0 : sourceCalendar.colors.lineColor) || '#6b7280';
685
+ const targetCalendarColor = (targetCalendar === null || targetCalendar === void 0 ? void 0 : targetCalendar.colors.lineColor) || '#6b7280';
523
686
  const deleteCalendarName = deleteState
524
- ? ((_c = calendars.find(c => c.id === deleteState.calendarId)) === null || _c === void 0 ? void 0 : _c.name) || 'Unknown'
687
+ ? ((_a = calendars.find(c => c.id === deleteState.calendarId)) === null || _a === void 0 ? void 0 : _a.name) || 'Unknown'
525
688
  : '';
526
689
  const readOnlyConfig = app.getReadOnlyConfig();
527
690
  const isEditable = !app.state.readOnly;
@@ -559,16 +722,17 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
559
722
  }, editingId: editingCalendarId, setEditingId: setEditingCalendarId, activeContextMenuCalendarId: contextMenu === null || contextMenu === void 0 ? void 0 : contextMenu.calendarId, isDraggable: isDraggable, isEditable: isEditable }), u("div", { className: 'border-t border-gray-200 dark:border-slate-800', children: u(MiniCalendar, { visibleMonth: app.getVisibleMonth(), currentDate: app.getCurrentDate(), showHeader: true, onMonthChange: handleMonthChange, onDateSelect: date => app.setCurrentDate(date) }) })] })), contextMenu && (u(ContextMenu, { ref: contextMenuRef, x: contextMenu.x, y: contextMenu.y, onClose: handleCloseContextMenu, className: 'w-64 p-2', children: u(ContentSlot, { generatorName: 'calendarContextMenu', generatorArgs: {
560
723
  calendar: calendars.find(c => c.id === contextMenu.calendarId),
561
724
  onClose: handleCloseContextMenu,
562
- }, defaultContent: renderCalendarContextMenu ? (renderCalendarContextMenu(calendars.find(c => c.id === contextMenu.calendarId), handleCloseContextMenu)) : (u(Fragment, { children: [u(ContextMenuLabel, { children: t('calendarOptions') }), u(MergeMenuItem, { calendars: calendars, currentCalendarId: contextMenu.calendarId, onMergeSelect: handleMergeSelect }), u(ContextMenuItem, { onClick: handleDeleteCalendar, children: t('delete') }), u(ContextMenuItem, { onClick: handleExportCalendar, children: t('exportCalendar') || 'Export Calendar' }), u(ContextMenuSeparator, {}), u(ContextMenuColorPicker, { selectedColor: (_d = calendars.find(c => c.id === contextMenu.calendarId)) === null || _d === void 0 ? void 0 : _d.colors.lineColor, onSelect: handleColorSelect, onCustomColor: handleCustomColor })] })) }) })), sidebarContextMenu &&
725
+ }, defaultContent: renderCalendarContextMenu ? (renderCalendarContextMenu(calendars.find(c => c.id === contextMenu.calendarId), handleCloseContextMenu)) : (u(Fragment, { children: [u(ContextMenuLabel, { children: t('calendarOptions') }), u(MergeMenuItem, { calendars: calendars, currentCalendarId: contextMenu.calendarId, onMergeSelect: handleMergeSelect }), u(ContextMenuItem, { onClick: handleDeleteCalendar, children: t('delete') }), u(ContextMenuItem, { onClick: handleExportCalendar, children: t('exportCalendar') || 'Export Calendar' }), u(ContextMenuSeparator, {}), u(ContextMenuColorPicker, { selectedColor: (_b = calendars.find(c => c.id === contextMenu.calendarId)) === null || _b === void 0 ? void 0 : _b.colors.lineColor, onSelect: handleColorSelect, onCustomColor: handleCustomColor })] })) }) })), sidebarContextMenu &&
563
726
  createPortal(u(ContextMenu, { x: sidebarContextMenu.x, y: sidebarContextMenu.y, onClose: handleCloseSidebarContextMenu, className: 'w-max p-2', children: [u(ContextMenuItem, { onClick: () => {
564
727
  onCreateCalendar === null || onCreateCalendar === void 0 ? void 0 : onCreateCalendar();
565
728
  handleCloseSidebarContextMenu();
566
- }, children: t('newCalendar') || 'New Calendar' }), u(ContextMenuItem, { onClick: handleImportClick, children: t('importCalendar') || 'Import Calendar' }), u(ContextMenuItem, { onClick: () => {
729
+ }, children: t('newCalendar') || 'New Calendar' }), u(ContextMenuItem, { onClick: handleImportClick, children: t('importCalendar') || 'Import Calendar' }), u(ContextMenuItem, { onClick: handleSubscribeClick, children: t('subscribeCalendar') || 'Subscribe to Calendar' }), u(ContextMenuItem, { onClick: () => {
567
730
  app.triggerRender();
568
731
  handleCloseSidebarContextMenu();
569
732
  }, children: t('refreshAll') || 'Refresh All' })] }), document.body), u("input", { ref: fileInputRef, type: 'file', accept: '.ics', style: { display: 'none' }, onChange: handleFileChange }), importState &&
570
- createPortal(u(ImportCalendarDialog, { calendars: calendars, filename: importState.filename, onConfirm: handleImportConfirm, onCancel: () => setImportState(null) }), document.body), mergeState &&
571
- createPortal(u(MergeCalendarDialog, { sourceName: sourceCalendarName, targetName: targetCalendarName, onConfirm: handleMergeConfirm, onCancel: () => setMergeState(null) }), document.body), deleteState &&
733
+ createPortal(u(ImportCalendarDialog, { calendars: calendars, filename: importState.filename, onConfirm: handleImportConfirm, onCancel: () => setImportState(null) }), document.body), subscribeDialogOpen &&
734
+ createPortal(u(SubscribeCalendarDialog, { onSubscribe: handleSubscribeConfirm, onCancel: () => setSubscribeDialogOpen(false) }), document.body), mergeState &&
735
+ createPortal(u(MergeCalendarDialog, { sourceName: sourceCalendarName, sourceColor: sourceCalendarColor, targetName: targetCalendarName, targetColor: targetCalendarColor, onConfirm: handleMergeConfirm, onCancel: () => setMergeState(null) }), document.body), deleteState &&
572
736
  createPortal(u(DeleteCalendarDialog, { calendarId: deleteState.calendarId, calendarName: deleteCalendarName, calendars: calendars, step: deleteState.step, onStepChange: step => setDeleteState(prev => (prev ? Object.assign(Object.assign({}, prev), { step }) : null)), onConfirmDelete: handleConfirmDelete, onCancel: () => setDeleteState(null), onMergeSelect: handleDeleteMergeSelect }), document.body), customColorPicker &&
573
737
  createPortal(u("div", { className: 'fixed inset-0 z-50', onMouseDown: () => {
574
738
  app.updateCalendar(customColorPicker.calendarId, {});
package/dist/index.js CHANGED
@@ -46,21 +46,33 @@ const getCalendarInitials = (calendar) => {
46
46
  const name = calendar.name || calendar.id;
47
47
  return name.charAt(0).toUpperCase();
48
48
  };
49
+ const CalendarItem = ({ calendar, isDraggable, isEditable: _isEditable, editingId, editingName, setEditingName, editInputRef, draggedCalendarId, dropTarget, activeContextMenuCalendarId, onDragStart, onDragEnd, onDragOver, onDragLeave, onDrop, onContextMenu, onToggleVisibility, onRenameStart, onRenameSave, onRenameKeyDown, }) => {
50
+ var _a;
51
+ const isVisible = calendar.isVisible !== false;
52
+ const calendarColor = ((_a = calendar.colors) === null || _a === void 0 ? void 0 : _a.lineColor) || '#3b82f6';
53
+ const showIcon = Boolean(calendar.icon);
54
+ const isDropTarget = (dropTarget === null || dropTarget === void 0 ? void 0 : dropTarget.id) === calendar.id;
55
+ const isActive = activeContextMenuCalendarId === calendar.id || editingId === calendar.id;
56
+ return (u("li", { className: 'df-calendar-list-item relative', onDragOver: e => onDragOver(e, calendar.id), onDragLeave: onDragLeave, onDrop: () => onDrop(calendar), onContextMenu: e => onContextMenu(e, calendar.id), children: [isDropTarget && dropTarget.position === 'top' && (u("div", { className: 'pointer-events-none absolute top-0 right-0 left-0 z-10 h-0.5 bg-[var(--df-color-primary)]' })), u("div", { draggable: isDraggable && !editingId, onDragStart: e => onDragStart(calendar, e), onDragEnd: onDragEnd, className: `rounded transition ${draggedCalendarId === calendar.id ? 'opacity-50' : ''} ${isDraggable ? 'cursor-grab' : 'cursor-default'}`, children: u("div", { className: `group flex items-center rounded px-2 py-2 transition hover:bg-gray-100 dark:hover:bg-slate-800 ${isActive ? 'bg-gray-100 dark:bg-slate-800' : ''}`, title: calendar.name, children: [u("input", { type: 'checkbox', className: 'df-calendar-checkbox shrink-0 cursor-pointer', style: {
57
+ '--checkbox-color': calendarColor,
58
+ }, checked: isVisible, onChange: event => onToggleVisibility(calendar.id, event.target.checked) }), showIcon && (u("span", { className: 'ml-2 flex h-5 w-5 shrink-0 items-center justify-center text-xs font-semibold text-white', "aria-hidden": 'true', children: getCalendarInitials(calendar) })), editingId === calendar.id ? (u("input", { ref: editInputRef, type: 'text', value: editingName, onChange: e => setEditingName(e.target.value), onBlur: onRenameSave, onKeyDown: onRenameKeyDown, className: 'ml-2 h-5 min-w-0 flex-1 rounded bg-white px-0 py-0 text-sm text-gray-900 focus:outline-none dark:bg-slate-700 dark:text-gray-100', onClick: e => e.stopPropagation() })) : (u(preact.Fragment, { children: [u("span", { className: 'ml-2 flex-1 truncate pl-1 text-sm text-gray-700 group-hover:text-gray-900 dark:text-gray-200 dark:group-hover:text-white', onDblClick: () => onRenameStart(calendar), children: calendar.name || calendar.id }), calendar.subscribed && (u(core.AudioLines, { width: 13, height: 13, className: 'ml-1 shrink-0 text-gray-400 dark:text-gray-500' }))] }))] }) }), isDropTarget && dropTarget.position === 'bottom' && (u("div", { className: 'pointer-events-none absolute right-0 bottom-0 left-0 z-10 h-0.5 bg-[var(--df-color-primary)]' }))] }, calendar.id));
59
+ };
49
60
  const CalendarList = ({ calendars, onToggleVisibility, onReorder, onRename, onContextMenu, editingId, setEditingId, activeContextMenuCalendarId, isDraggable = true, isEditable = true, }) => {
61
+ var _a;
50
62
  const [editingName, setEditingName] = hooks.useState('');
51
63
  const editInputRef = hooks.useRef(null);
52
64
  const isProcessedRef = hooks.useRef(false);
53
65
  // Drag state
54
66
  const [draggedCalendarId, setDraggedCalendarId] = hooks.useState(null);
55
67
  const [dropTarget, setDropTarget] = hooks.useState(null);
68
+ // Collapsed sources state
69
+ const [collapsedSources, setCollapsedSources] = hooks.useState({});
56
70
  const handleDragStart = hooks.useCallback((calendar, e) => {
57
- // Prevent dragging when editing or not draggable
58
71
  if (editingId || !isDraggable) {
59
72
  e.preventDefault();
60
73
  return;
61
74
  }
62
75
  setDraggedCalendarId(calendar.id);
63
- // Store calendar data for drop handling
64
76
  const dragData = {
65
77
  calendarId: calendar.id,
66
78
  calendarName: calendar.name,
@@ -109,11 +121,9 @@ const CalendarList = ({ calendars, onToggleVisibility, onReorder, onRename, onCo
109
121
  return;
110
122
  const fromIndex = calendars.findIndex(c => c.id === draggedCalendarId);
111
123
  let toIndex = calendars.findIndex(c => c.id === targetCalendar.id);
112
- // Adjust target index based on position
113
124
  if (dropTarget.position === 'bottom') {
114
125
  toIndex += 1;
115
126
  }
116
- // Adjust for removal of the item
117
127
  if (toIndex > fromIndex) {
118
128
  toIndex -= 1;
119
129
  }
@@ -171,24 +181,75 @@ const CalendarList = ({ calendars, onToggleVisibility, onReorder, onRename, onCo
171
181
  }
172
182
  }
173
183
  }, [editingId, calendars]);
174
- return (u("div", { className: 'df-calendar-list flex-1 overflow-y-auto px-2 pb-3', children: u("ul", { className: 'relative space-y-1', children: calendars.map(calendar => {
175
- var _a;
176
- const isVisible = calendar.isVisible !== false;
177
- const calendarColor = ((_a = calendar.colors) === null || _a === void 0 ? void 0 : _a.lineColor) || '#3b82f6';
178
- const showIcon = Boolean(calendar.icon);
179
- const isDropTarget = (dropTarget === null || dropTarget === void 0 ? void 0 : dropTarget.id) === calendar.id;
180
- const isActive = activeContextMenuCalendarId === calendar.id ||
181
- editingId === calendar.id;
182
- return (u("li", { className: 'df-calendar-list-item relative', onDragOver: e => handleDragOver(e, calendar.id), onDragLeave: handleDragLeave, onDrop: () => handleDrop(calendar), onContextMenu: e => onContextMenu(e, calendar.id), children: [isDropTarget && dropTarget.position === 'top' && (u("div", { className: 'pointer-events-none absolute top-0 right-0 left-0 z-10 h-0.5 bg-[var(--df-color-primary)]' })), u("div", { draggable: isDraggable && !editingId, onDragStart: e => handleDragStart(calendar, e), onDragEnd: handleDragEnd, className: `rounded transition ${draggedCalendarId === calendar.id ? 'opacity-50' : ''} ${isDraggable ? 'cursor-grab' : 'cursor-default'}`, children: u("div", { className: `group flex items-center rounded px-2 py-2 transition hover:bg-gray-100 dark:hover:bg-slate-800 ${isActive ? 'bg-gray-100 dark:bg-slate-800' : ''}`, title: calendar.name, children: [u("input", { type: 'checkbox', className: 'df-calendar-checkbox shrink-0 cursor-pointer', style: {
183
- '--checkbox-color': calendarColor,
184
- }, checked: isVisible, onChange: event => onToggleVisibility(calendar.id, event.target.checked) }), showIcon && (u("span", { className: 'ml-2 flex h-5 w-5 shrink-0 items-center justify-center text-xs font-semibold text-white', "aria-hidden": 'true', children: getCalendarInitials(calendar) })), editingId === calendar.id ? (u("input", { ref: editInputRef, type: 'text', value: editingName, onChange: e => setEditingName(e.target.value), onBlur: handleRenameSave, onKeyDown: handleRenameKeyDown, className: 'ml-2 h-5 min-w-0 flex-1 rounded bg-white px-0 py-0 text-sm text-gray-900 focus:outline-none dark:bg-slate-700 dark:text-gray-100', onClick: e => e.stopPropagation() })) : (u("span", { className: 'ml-2 flex-1 truncate pl-1 text-sm text-gray-700 group-hover:text-gray-900 dark:text-gray-200 dark:group-hover:text-white', onDblClick: () => handleRenameStart(calendar), children: calendar.name || calendar.id }))] }) }), isDropTarget && dropTarget.position === 'bottom' && (u("div", { className: 'pointer-events-none absolute right-0 bottom-0 left-0 z-10 h-0.5 bg-[var(--df-color-primary)]' }))] }, calendar.id));
185
- }) }) }));
184
+ const toggleSource = hooks.useCallback((source) => {
185
+ setCollapsedSources(prev => (Object.assign(Object.assign({}, prev), { [source]: !prev[source] })));
186
+ }, []);
187
+ // Shared item props (excludes per-item fields)
188
+ const sharedItemProps = {
189
+ isDraggable,
190
+ isEditable,
191
+ editingId,
192
+ editingName,
193
+ setEditingName,
194
+ editInputRef,
195
+ isProcessedRef,
196
+ draggedCalendarId,
197
+ dropTarget,
198
+ activeContextMenuCalendarId,
199
+ onDragStart: handleDragStart,
200
+ onDragEnd: handleDragEnd,
201
+ onDragOver: handleDragOver,
202
+ onDragLeave: handleDragLeave,
203
+ onDrop: handleDrop,
204
+ onContextMenu,
205
+ onToggleVisibility,
206
+ onRenameStart: handleRenameStart,
207
+ onRenameSave: handleRenameSave,
208
+ onRenameKeyDown: handleRenameKeyDown,
209
+ setEditingId,
210
+ };
211
+ // Check if any calendar has a source
212
+ const hasSources = calendars.some(c => c.source);
213
+ if (!hasSources) {
214
+ // Flat list (original behaviour)
215
+ return (u("div", { className: 'df-calendar-list flex-1 overflow-y-auto px-2 pb-3', children: u("ul", { className: 'relative space-y-1', children: calendars.map(calendar => (u(CalendarItem, Object.assign({ calendar: calendar }, sharedItemProps), calendar.id))) }) }));
216
+ }
217
+ // Group calendars by source; calendars without a source go into a null group
218
+ const groups = new Map();
219
+ for (const calendar of calendars) {
220
+ const key = (_a = calendar.source) !== null && _a !== void 0 ? _a : null;
221
+ if (!groups.has(key))
222
+ groups.set(key, []);
223
+ groups.get(key).push(calendar);
224
+ }
225
+ return (u("div", { className: 'df-calendar-list flex-1 overflow-y-auto px-2 pb-3', children: [groups.has(null) && (u("ul", { className: 'relative space-y-1', children: groups.get(null).map(calendar => (u(CalendarItem, Object.assign({ calendar: calendar }, sharedItemProps), calendar.id))) })), Array.from(groups.entries())
226
+ .filter(([source]) => source !== null)
227
+ .map(([source, groupCalendars]) => {
228
+ const isCollapsed = collapsedSources[source];
229
+ return (u("div", { className: 'mt-1', children: [u("button", { type: 'button', className: 'flex w-full items-center justify-between rounded px-2 py-1.5 text-xs font-medium text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-slate-800', onClick: () => toggleSource(source), children: [u("span", { className: 'truncate', children: source }), u(core.ChevronDown, { width: 13, height: 13, className: `ml-1 shrink-0 transition-transform duration-200 ${isCollapsed ? '-rotate-90' : ''}` })] }), u("div", { style: {
230
+ display: 'grid',
231
+ gridTemplateRows: isCollapsed ? '0fr' : '1fr',
232
+ transition: 'grid-template-rows 200ms ease',
233
+ }, children: u("div", { style: { overflow: 'hidden' }, children: u("ul", { className: 'relative space-y-1', children: groupCalendars.map(calendar => (u(CalendarItem, Object.assign({ calendar: calendar }, sharedItemProps), calendar.id))) }) }) })] }, source));
234
+ })] }));
186
235
  };
187
236
 
237
+ const CalendarChip = ({ name, color }) => (u("span", { className: 'mx-0.5 inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium', style: { backgroundColor: `${color}26`, color }, children: name }));
238
+
239
+ const CAL_SENTINEL = '\u0001C\u0001';
240
+ function renderWithChip(template, name, color) {
241
+ return template
242
+ .split(CAL_SENTINEL)
243
+ .flatMap((part, i) => i === 0
244
+ ? [part]
245
+ : [u(CalendarChip, { name: name, color: color }, i), part]);
246
+ }
188
247
  const DeleteCalendarDialog = ({ calendarId, calendarName, calendars, step, onStepChange, onConfirmDelete, onCancel, onMergeSelect, }) => {
248
+ var _a, _b;
189
249
  const [showMergeDropdown, setShowMergeDropdown] = hooks.useState(false);
190
250
  const { t } = core.useLocale();
191
- return core.createPortal(u("div", { className: 'df-portal fixed inset-0 z-[9999] flex items-center justify-center bg-black/50', children: u("div", { className: 'w-full max-w-md rounded-lg bg-background p-6 shadow-xl', children: step === 'initial' ? (u(preact.Fragment, { children: [u("h2", { className: 'text-lg font-semibold text-gray-900 dark:text-white', children: t('deleteCalendar', { calendarName }) }), u("p", { className: 'mt-3 text-sm text-gray-600 dark:text-gray-300', children: t('deleteCalendarMessage', { calendarName }) }), u("div", { className: 'mt-6 flex items-center justify-between', children: [u("div", { className: 'relative', children: [u("button", { type: 'button', onClick: () => setShowMergeDropdown(!showMergeDropdown), className: 'flex items-center gap-1 rounded-md border border-gray-300 px-4 py-2 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-slate-700', children: t('merge') }), showMergeDropdown && (u("div", { className: 'absolute top-full left-0 z-10 mt-1 max-h-60 w-max min-w-full overflow-y-auto rounded-md border border-gray-200 bg-background shadow-lg dark:border-slate-700', children: calendars
251
+ const calendarColor = (_b = (_a = calendars.find(c => c.id === calendarId)) === null || _a === void 0 ? void 0 : _a.colors.lineColor) !== null && _b !== void 0 ? _b : '#6b7280';
252
+ return core.createPortal(u("div", { className: 'df-portal fixed inset-0 z-[9999] flex items-center justify-center bg-black/50', children: u("div", { className: 'w-full max-w-md rounded-lg bg-white p-6 shadow-xl dark:bg-gray-800', children: step === 'initial' ? (u(preact.Fragment, { children: [u("h2", { className: 'text-lg font-semibold text-gray-900 dark:text-white', children: t('deleteCalendar', { calendarName }) }), u("p", { className: 'mt-3 flex flex-wrap items-center gap-y-0.5 text-sm text-gray-600 dark:text-gray-300', children: renderWithChip(t('deleteCalendarMessage', { calendarName: CAL_SENTINEL }), calendarName, calendarColor) }), u("div", { className: 'mt-6 flex items-center justify-between', children: [u("div", { className: 'relative', children: [u("button", { type: 'button', onClick: () => setShowMergeDropdown(!showMergeDropdown), className: 'flex items-center gap-1 rounded-md border border-gray-300 px-4 py-2 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-slate-700', children: t('merge') }), showMergeDropdown && (u("div", { className: 'absolute top-full left-0 z-10 mt-1 max-h-60 w-max min-w-full overflow-y-auto rounded-md border border-gray-200 bg-white shadow-lg dark:border-slate-700 dark:bg-gray-800', children: calendars
192
253
  .filter(c => c.id !== calendarId)
193
254
  .map(calendar => (u("div", { className: 'flex cursor-pointer items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-slate-700', onClick: () => {
194
255
  onMergeSelect(calendar.id);
@@ -245,6 +306,7 @@ const ImportCalendarDialog = ({ calendars, filename, onConfirm, onCancel, }) =>
245
306
  top: rect.bottom,
246
307
  left: rect.left,
247
308
  width: rect.width,
309
+ overscrollBehavior: 'none',
248
310
  }, children: u("div", { className: 'py-1', children: [calendars.map(calendar => (u("div", { className: `flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100 dark:hover:bg-slate-700 ${selectedCalendarId === calendar.id ? 'bg-primary/10' : ''}`, onClick: () => handleSelect(calendar.id), children: [u("div", { className: 'mr-3 h-3 w-3 shrink-0 rounded-sm', style: { backgroundColor: calendar.colors.lineColor } }), u("span", { className: `flex-1 truncate text-sm ${selectedCalendarId === calendar.id ? 'font-medium text-primary' : 'text-gray-700 dark:text-gray-200'}`, children: calendar.name || calendar.id }), selectedCalendarId === calendar.id && (u(core.Check, { className: 'ml-2 h-4 w-4 shrink-0 text-primary' }))] }, calendar.id))), u("div", { className: 'my-1 border-t border-gray-100 dark:border-slate-700' }), u("div", { className: `flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100 dark:hover:bg-slate-700 ${isNewSelected ? 'bg-primary/10' : ''}`, onClick: () => handleSelect(NEW_CALENDAR_ID), children: [u("span", { className: `flex-1 truncate text-sm ${isNewSelected ? 'font-medium text-primary' : 'pl-6 text-gray-700 dark:text-gray-200'}`, children: [t('newCalendar') || 'New Calendar', ": ", filename] }), isNewSelected && (u(core.Check, { className: 'ml-2 h-4 w-4 shrink-0 text-primary' }))] })] }) }), document.body);
249
311
  };
250
312
  return (u("div", { className: 'df-portal fixed inset-0 z-100 flex items-center justify-center bg-black/50', children: u("div", { className: 'w-full max-w-md rounded-lg bg-white p-6 shadow-xl dark:bg-gray-900', children: [u("h2", { className: 'mb-4 text-lg font-semibold text-gray-900 dark:text-white', children: t('addSchedule') || 'Add Schedule' }), u("p", { className: 'mb-4 text-sm text-gray-600 dark:text-gray-300', children: t('importCalendarMessage') ||
@@ -253,9 +315,29 @@ const ImportCalendarDialog = ({ calendars, filename, onConfirm, onCancel, }) =>
253
315
  : (selectedCalendar === null || selectedCalendar === void 0 ? void 0 : selectedCalendar.name) || (selectedCalendar === null || selectedCalendar === void 0 ? void 0 : selectedCalendar.id) }), u(core.ChevronsUpDown, { className: 'ml-2 h-4 w-4 shrink-0 text-gray-400' })] }), renderDropdown()] }), u("div", { className: 'mt-8 flex justify-end gap-3', children: [u("button", { type: 'button', onClick: onCancel, className: core.cancelButton, children: t('cancel') || 'Cancel' }), u("button", { type: 'button', onClick: () => onConfirm(selectedCalendarId), className: 'rounded-md bg-primary px-6 py-2 text-sm font-medium text-primary-foreground shadow-sm transition-colors hover:bg-primary/90', children: t('ok') || 'OK' })] })] }) }));
254
316
  };
255
317
 
256
- const MergeCalendarDialog = ({ sourceName, targetName, onConfirm, onCancel, }) => {
318
+ const SOURCE_SENTINEL = '\u0001S\u0001';
319
+ const TARGET_SENTINEL = '\u0001T\u0001';
320
+ function renderLine(line, source, target) {
321
+ return line
322
+ .split(new RegExp(`(${SOURCE_SENTINEL}|${TARGET_SENTINEL})`))
323
+ .map((part, i) => {
324
+ if (part === SOURCE_SENTINEL)
325
+ return u(CalendarChip, { name: source.name, color: source.color }, i);
326
+ if (part === TARGET_SENTINEL)
327
+ return u(CalendarChip, { name: target.name, color: target.color }, i);
328
+ return part;
329
+ });
330
+ }
331
+ const MergeCalendarDialog = ({ sourceName, sourceColor, targetName, targetColor, onConfirm, onCancel, }) => {
257
332
  const { t } = core.useLocale();
258
- return (u("div", { className: 'fixed inset-0 z-100 flex items-center justify-center bg-black/50', children: u("div", { className: 'w-full max-w-md rounded-lg bg-background p-6 shadow-xl', children: [u("h2", { className: 'text-lg font-semibold text-gray-900 dark:text-white', children: t('mergeConfirmTitle', { sourceName, targetName }) }), u("p", { className: 'mt-3 text-sm text-gray-600 dark:text-gray-300', children: t('mergeConfirmMessage', { sourceName, targetName }) }), u("div", { className: 'mt-6 flex justify-end gap-3', children: [u("button", { type: 'button', onClick: onCancel, className: core.cancelButton, children: t('cancel') }), u("button", { type: 'button', onClick: onConfirm, className: 'rounded-md bg-destructive px-3 py-2 text-xs font-medium text-destructive-foreground hover:bg-destructive/90', children: t('merge') })] })] }) }));
333
+ const source = { name: sourceName, color: sourceColor };
334
+ const target = { name: targetName, color: targetColor };
335
+ const messageTemplate = t('mergeConfirmMessage', {
336
+ sourceName: SOURCE_SENTINEL,
337
+ targetName: TARGET_SENTINEL,
338
+ });
339
+ const messageLines = messageTemplate.split('\n');
340
+ return (u("div", { className: 'df-portal fixed inset-0 z-[9999] flex items-center justify-center bg-black/50', children: u("div", { className: 'rounded-lg bg-white p-6 shadow-xl dark:bg-gray-800', children: [u("h2", { className: 'text-lg font-semibold text-gray-900 dark:text-white', children: t('mergeConfirmTitle', { sourceName, targetName }) }), u("div", { className: 'mt-3 space-y-1 text-sm text-gray-600 dark:text-gray-300', children: messageLines.map((line, i) => (u("p", { className: 'flex flex-wrap items-center gap-y-0.5', children: renderLine(line, source, target) }, i))) }), u("div", { className: 'mt-6 flex justify-end gap-3', children: [u("button", { type: 'button', onClick: onCancel, className: core.cancelButton, children: t('cancel') }), u("button", { type: 'button', onClick: onConfirm, className: 'rounded-md bg-destructive px-3 py-2 text-xs font-medium text-destructive-foreground hover:bg-destructive/90', children: t('merge') })] })] }) }));
259
341
  };
260
342
 
261
343
  const stopPropagation = (e) => e.stopPropagation();
@@ -304,8 +386,38 @@ const SidebarHeader = ({ isCollapsed, onCollapseToggle, }) => {
304
386
  return (u("div", { className: core.sidebarHeader, children: [u("button", { type: 'button', "aria-label": isCollapsed ? t('expandSidebar') : t('collapseSidebar'), className: core.sidebarHeaderToggle, onClick: onCollapseToggle, children: isCollapsed ? (u(core.PanelRightClose, { className: 'h-4 w-4 text-gray-500 dark:text-gray-400' })) : (u(core.PanelRightOpen, { className: 'h-4 w-4 text-gray-500 dark:text-gray-400' })) }), !isCollapsed && (u("div", { className: 'ml-3 flex flex-1 items-center justify-between', children: u("span", { className: core.sidebarHeaderTitle, children: t('calendars') }) }))] }));
305
387
  };
306
388
 
389
+ const SubscribeCalendarDialog = ({ onSubscribe, onCancel, }) => {
390
+ const { t } = core.useLocale();
391
+ const [url, setUrl] = hooks.useState('');
392
+ const [loading, setLoading] = hooks.useState(false);
393
+ const [error, setError] = hooks.useState(null);
394
+ const handleSubmit = () => __awaiter(void 0, void 0, void 0, function* () {
395
+ const trimmed = url.trim();
396
+ if (!trimmed)
397
+ return;
398
+ setError(null);
399
+ setLoading(true);
400
+ try {
401
+ yield onSubscribe(trimmed);
402
+ }
403
+ catch (_a) {
404
+ setError(t('subscribeError'));
405
+ }
406
+ finally {
407
+ setLoading(false);
408
+ }
409
+ });
410
+ const handleKeyDown = (e) => {
411
+ if (e.key === 'Enter')
412
+ handleSubmit();
413
+ if (e.key === 'Escape')
414
+ onCancel();
415
+ };
416
+ return (u("div", { className: 'df-portal fixed inset-0 z-[9999] flex items-center justify-center bg-black/50', children: u("div", { className: 'w-full max-w-xl rounded-lg bg-white p-6 shadow-xl dark:bg-gray-800', children: [u("h2", { className: 'text-lg font-semibold text-gray-900 dark:text-white', children: t('subscribeCalendarTitle') }), u("div", { className: 'mt-4', children: [u("div", { className: 'flex items-center gap-3', children: [u("label", { className: 'shrink-0 text-sm font-medium text-gray-700 dark:text-gray-300', children: t('calendarUrl') }), u("input", { type: 'url', value: url, onInput: e => setUrl(e.target.value), onKeyDown: handleKeyDown, placeholder: t('calendarUrlPlaceholder'), disabled: loading, autoFocus: true, className: 'flex-1 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:border-primary focus:ring-1 focus:ring-primary focus:outline-none disabled:opacity-50 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400' })] }), error && (u("p", { className: 'mt-2 text-xs text-red-500 dark:text-red-400', children: error }))] }), u("div", { className: 'mt-6 flex justify-end gap-3', children: [u("button", { type: 'button', onClick: onCancel, disabled: loading, className: core.cancelButton, children: t('cancel') }), u("button", { type: 'button', onClick: handleSubmit, disabled: loading || !url.trim(), className: 'rounded-md bg-primary px-4 py-2 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50', children: loading ? t('fetchingCalendar') : t('subscribe') })] })] }) }));
417
+ };
418
+
307
419
  const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCollapsed, setCollapsed, renderCalendarContextMenu, renderSidebarHeader, editingCalendarId: propEditingCalendarId, setEditingCalendarId: propSetEditingCalendarId, onCreateCalendar, }) => {
308
- var _a, _b, _c, _d;
420
+ var _a, _b;
309
421
  const { t } = core.useLocale();
310
422
  // Detect if custom color picker slot is provided
311
423
  const hasCustomPicker = app.state.overrides.includes('colorPicker');
@@ -333,6 +445,8 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
333
445
  const [deleteState, setDeleteState] = hooks.useState(null);
334
446
  // Import Calendar State
335
447
  const [importState, setImportState] = hooks.useState(null);
448
+ // Subscribe Calendar State
449
+ const [subscribeDialogOpen, setSubscribeDialogOpen] = hooks.useState(false);
336
450
  const handleContextMenu = hooks.useCallback((e, calendarId) => {
337
451
  e.preventDefault();
338
452
  e.stopPropagation(); // Stop propagation to prevent sidebar context menu
@@ -447,6 +561,51 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
447
561
  (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click();
448
562
  handleCloseSidebarContextMenu();
449
563
  }, [handleCloseSidebarContextMenu]);
564
+ // Subscribe Calendar handler
565
+ const handleSubscribeClick = hooks.useCallback(() => {
566
+ setSubscribeDialogOpen(true);
567
+ handleCloseSidebarContextMenu();
568
+ }, [handleCloseSidebarContextMenu]);
569
+ const handleSubscribeConfirm = hooks.useCallback((url) => __awaiter(void 0, void 0, void 0, function* () {
570
+ const response = yield fetch(url);
571
+ if (!response.ok)
572
+ throw new Error(`HTTP ${response.status}`);
573
+ const icsContent = yield response.text();
574
+ const result = core.parseICS(icsContent);
575
+ // Extract calendar name from X-WR-CALNAME if present
576
+ const nameMatch = icsContent.match(/X-WR-CALNAME[^:]*:([^\r\n]+)/);
577
+ const calendarName = nameMatch
578
+ ? nameMatch[1].trim()
579
+ : new URL(url).hostname;
580
+ const presetColors = [
581
+ '#3b82f6',
582
+ '#10b981',
583
+ '#8b5cf6',
584
+ '#f59e0b',
585
+ '#ef4444',
586
+ '#f97316',
587
+ '#ec4899',
588
+ '#14b8a6',
589
+ '#6366f1',
590
+ '#6b7280',
591
+ ];
592
+ const randomColor = presetColors[Math.floor(Math.random() * presetColors.length)];
593
+ const { colors: calendarColors, darkColors } = core.getCalendarColorsForHex(randomColor);
594
+ const calendarId = core.generateUniKey();
595
+ app.createCalendar({
596
+ id: calendarId,
597
+ name: calendarName,
598
+ isDefault: false,
599
+ colors: calendarColors,
600
+ darkColors,
601
+ isVisible: true,
602
+ subscribed: true,
603
+ });
604
+ result.events.forEach(event => {
605
+ app.addEvent(Object.assign(Object.assign({}, event), { calendarId }));
606
+ });
607
+ setSubscribeDialogOpen(false);
608
+ }), [app]);
450
609
  const handleFileChange = hooks.useCallback((e) => __awaiter(void 0, void 0, void 0, function* () {
451
610
  var _a;
452
611
  const file = (_a = e.currentTarget.files) === null || _a === void 0 ? void 0 : _a[0];
@@ -516,14 +675,18 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
516
675
  handleCloseContextMenu();
517
676
  }
518
677
  }, [contextMenu, calendars, app, handleCloseContextMenu]);
519
- const sourceCalendarName = mergeState
520
- ? ((_a = calendars.find(c => c.id === mergeState.sourceId)) === null || _a === void 0 ? void 0 : _a.name) || 'Unknown'
521
- : '';
522
- const targetCalendarName = mergeState
523
- ? ((_b = calendars.find(c => c.id === mergeState.targetId)) === null || _b === void 0 ? void 0 : _b.name) || 'Unknown'
524
- : '';
678
+ const sourceCalendar = mergeState
679
+ ? calendars.find(c => c.id === mergeState.sourceId)
680
+ : null;
681
+ const targetCalendar = mergeState
682
+ ? calendars.find(c => c.id === mergeState.targetId)
683
+ : null;
684
+ const sourceCalendarName = (sourceCalendar === null || sourceCalendar === void 0 ? void 0 : sourceCalendar.name) || 'Unknown';
685
+ const targetCalendarName = (targetCalendar === null || targetCalendar === void 0 ? void 0 : targetCalendar.name) || 'Unknown';
686
+ const sourceCalendarColor = (sourceCalendar === null || sourceCalendar === void 0 ? void 0 : sourceCalendar.colors.lineColor) || '#6b7280';
687
+ const targetCalendarColor = (targetCalendar === null || targetCalendar === void 0 ? void 0 : targetCalendar.colors.lineColor) || '#6b7280';
525
688
  const deleteCalendarName = deleteState
526
- ? ((_c = calendars.find(c => c.id === deleteState.calendarId)) === null || _c === void 0 ? void 0 : _c.name) || 'Unknown'
689
+ ? ((_a = calendars.find(c => c.id === deleteState.calendarId)) === null || _a === void 0 ? void 0 : _a.name) || 'Unknown'
527
690
  : '';
528
691
  const readOnlyConfig = app.getReadOnlyConfig();
529
692
  const isEditable = !app.state.readOnly;
@@ -561,16 +724,17 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
561
724
  }, editingId: editingCalendarId, setEditingId: setEditingCalendarId, activeContextMenuCalendarId: contextMenu === null || contextMenu === void 0 ? void 0 : contextMenu.calendarId, isDraggable: isDraggable, isEditable: isEditable }), u("div", { className: 'border-t border-gray-200 dark:border-slate-800', children: u(core.MiniCalendar, { visibleMonth: app.getVisibleMonth(), currentDate: app.getCurrentDate(), showHeader: true, onMonthChange: handleMonthChange, onDateSelect: date => app.setCurrentDate(date) }) })] })), contextMenu && (u(core.ContextMenu, { ref: contextMenuRef, x: contextMenu.x, y: contextMenu.y, onClose: handleCloseContextMenu, className: 'w-64 p-2', children: u(core.ContentSlot, { generatorName: 'calendarContextMenu', generatorArgs: {
562
725
  calendar: calendars.find(c => c.id === contextMenu.calendarId),
563
726
  onClose: handleCloseContextMenu,
564
- }, defaultContent: renderCalendarContextMenu ? (renderCalendarContextMenu(calendars.find(c => c.id === contextMenu.calendarId), handleCloseContextMenu)) : (u(preact.Fragment, { children: [u(core.ContextMenuLabel, { children: t('calendarOptions') }), u(MergeMenuItem, { calendars: calendars, currentCalendarId: contextMenu.calendarId, onMergeSelect: handleMergeSelect }), u(core.ContextMenuItem, { onClick: handleDeleteCalendar, children: t('delete') }), u(core.ContextMenuItem, { onClick: handleExportCalendar, children: t('exportCalendar') || 'Export Calendar' }), u(core.ContextMenuSeparator, {}), u(core.ContextMenuColorPicker, { selectedColor: (_d = calendars.find(c => c.id === contextMenu.calendarId)) === null || _d === void 0 ? void 0 : _d.colors.lineColor, onSelect: handleColorSelect, onCustomColor: handleCustomColor })] })) }) })), sidebarContextMenu &&
727
+ }, defaultContent: renderCalendarContextMenu ? (renderCalendarContextMenu(calendars.find(c => c.id === contextMenu.calendarId), handleCloseContextMenu)) : (u(preact.Fragment, { children: [u(core.ContextMenuLabel, { children: t('calendarOptions') }), u(MergeMenuItem, { calendars: calendars, currentCalendarId: contextMenu.calendarId, onMergeSelect: handleMergeSelect }), u(core.ContextMenuItem, { onClick: handleDeleteCalendar, children: t('delete') }), u(core.ContextMenuItem, { onClick: handleExportCalendar, children: t('exportCalendar') || 'Export Calendar' }), u(core.ContextMenuSeparator, {}), u(core.ContextMenuColorPicker, { selectedColor: (_b = calendars.find(c => c.id === contextMenu.calendarId)) === null || _b === void 0 ? void 0 : _b.colors.lineColor, onSelect: handleColorSelect, onCustomColor: handleCustomColor })] })) }) })), sidebarContextMenu &&
565
728
  core.createPortal(u(core.ContextMenu, { x: sidebarContextMenu.x, y: sidebarContextMenu.y, onClose: handleCloseSidebarContextMenu, className: 'w-max p-2', children: [u(core.ContextMenuItem, { onClick: () => {
566
729
  onCreateCalendar === null || onCreateCalendar === void 0 ? void 0 : onCreateCalendar();
567
730
  handleCloseSidebarContextMenu();
568
- }, children: t('newCalendar') || 'New Calendar' }), u(core.ContextMenuItem, { onClick: handleImportClick, children: t('importCalendar') || 'Import Calendar' }), u(core.ContextMenuItem, { onClick: () => {
731
+ }, children: t('newCalendar') || 'New Calendar' }), u(core.ContextMenuItem, { onClick: handleImportClick, children: t('importCalendar') || 'Import Calendar' }), u(core.ContextMenuItem, { onClick: handleSubscribeClick, children: t('subscribeCalendar') || 'Subscribe to Calendar' }), u(core.ContextMenuItem, { onClick: () => {
569
732
  app.triggerRender();
570
733
  handleCloseSidebarContextMenu();
571
734
  }, children: t('refreshAll') || 'Refresh All' })] }), document.body), u("input", { ref: fileInputRef, type: 'file', accept: '.ics', style: { display: 'none' }, onChange: handleFileChange }), importState &&
572
- core.createPortal(u(ImportCalendarDialog, { calendars: calendars, filename: importState.filename, onConfirm: handleImportConfirm, onCancel: () => setImportState(null) }), document.body), mergeState &&
573
- core.createPortal(u(MergeCalendarDialog, { sourceName: sourceCalendarName, targetName: targetCalendarName, onConfirm: handleMergeConfirm, onCancel: () => setMergeState(null) }), document.body), deleteState &&
735
+ core.createPortal(u(ImportCalendarDialog, { calendars: calendars, filename: importState.filename, onConfirm: handleImportConfirm, onCancel: () => setImportState(null) }), document.body), subscribeDialogOpen &&
736
+ core.createPortal(u(SubscribeCalendarDialog, { onSubscribe: handleSubscribeConfirm, onCancel: () => setSubscribeDialogOpen(false) }), document.body), mergeState &&
737
+ core.createPortal(u(MergeCalendarDialog, { sourceName: sourceCalendarName, sourceColor: sourceCalendarColor, targetName: targetCalendarName, targetColor: targetCalendarColor, onConfirm: handleMergeConfirm, onCancel: () => setMergeState(null) }), document.body), deleteState &&
574
738
  core.createPortal(u(DeleteCalendarDialog, { calendarId: deleteState.calendarId, calendarName: deleteCalendarName, calendars: calendars, step: deleteState.step, onStepChange: step => setDeleteState(prev => (prev ? Object.assign(Object.assign({}, prev), { step }) : null)), onConfirmDelete: handleConfirmDelete, onCancel: () => setDeleteState(null), onMergeSelect: handleDeleteMergeSelect }), document.body), customColorPicker &&
575
739
  core.createPortal(u("div", { className: 'fixed inset-0 z-50', onMouseDown: () => {
576
740
  app.updateCalendar(customColorPicker.calendarId, {});
@@ -0,0 +1,6 @@
1
+ export interface CalendarChipProps {
2
+ name: string;
3
+ /** lineColor hex, e.g. "#3b82f6" */
4
+ color: string;
5
+ }
6
+ export declare const CalendarChip: ({ name, color }: CalendarChipProps) => import("preact").JSX.Element;
@@ -1,8 +1,10 @@
1
1
  interface MergeCalendarDialogProps {
2
2
  sourceName: string;
3
+ sourceColor: string;
3
4
  targetName: string;
5
+ targetColor: string;
4
6
  onConfirm: () => void;
5
7
  onCancel: () => void;
6
8
  }
7
- export declare const MergeCalendarDialog: ({ sourceName, targetName, onConfirm, onCancel, }: MergeCalendarDialogProps) => import("preact").JSX.Element;
9
+ export declare const MergeCalendarDialog: ({ sourceName, sourceColor, targetName, targetColor, onConfirm, onCancel, }: MergeCalendarDialogProps) => import("preact").JSX.Element;
8
10
  export {};
@@ -0,0 +1,6 @@
1
+ interface SubscribeCalendarDialogProps {
2
+ onSubscribe: (url: string) => Promise<void>;
3
+ onCancel: () => void;
4
+ }
5
+ export declare const SubscribeCalendarDialog: ({ onSubscribe, onCancel, }: SubscribeCalendarDialogProps) => import("preact").JSX.Element;
6
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dayflow/plugin-sidebar",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Sidebar plugin for DayFlow calendar",
5
5
  "keywords": [
6
6
  "calendar",
@@ -37,10 +37,10 @@
37
37
  "rollup-plugin-dts": "^6.3.0",
38
38
  "temporal-polyfill": "^0.3.0",
39
39
  "typescript": "^5.9.3",
40
- "@dayflow/core": "3.3.0"
40
+ "@dayflow/core": "3.3.2"
41
41
  },
42
42
  "peerDependencies": {
43
- "@dayflow/core": "3.3.0"
43
+ "@dayflow/core": "3.3.2"
44
44
  },
45
45
  "scripts": {
46
46
  "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && rollup -c",