@dayflow/plugin-sidebar 1.2.4 → 1.2.6

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.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ICalendarApp, CalendarType, TNode, SidebarHeaderSlotArgs, CreateCalendarDialogProps, CalendarPlugin } from '@dayflow/core';
1
+ import { ICalendarApp, CalendarType, TNode, SidebarHeaderSlotArgs, CreateCalendarDialogProps, Event, CalendarPlugin } from '@dayflow/core';
2
2
  export { SidebarHeaderSlotArgs } from '@dayflow/core';
3
3
 
4
4
  interface CalendarSidebarRenderProps {
@@ -15,6 +15,8 @@ interface CalendarSidebarRenderProps {
15
15
  editingCalendarId?: string | null;
16
16
  setEditingCalendarId?: (id: string | null) => void;
17
17
  onCreateCalendar?: () => void;
18
+ onSubscribeCalendar?: (calendar: CalendarType, events: Event[]) => Promise<void>;
19
+ onLoadSubscription?: (calendar: CalendarType) => Promise<void>;
18
20
  }
19
21
  interface SidebarPluginConfig {
20
22
  width?: number | string;
@@ -25,6 +27,8 @@ interface SidebarPluginConfig {
25
27
  renderCalendarContextMenu?: (calendar: CalendarType, onClose: () => void) => TNode;
26
28
  renderSidebarHeader?: (args: SidebarHeaderSlotArgs) => TNode;
27
29
  renderCreateCalendarDialog?: (props: CreateCalendarDialogProps) => TNode;
30
+ onSubscribeCalendar?: (calendar: CalendarType, events: Event[]) => Promise<void>;
31
+ onLoadSubscription?: (calendar: CalendarType) => Promise<void>;
28
32
  [key: string]: unknown;
29
33
  }
30
34
  declare function createSidebarPlugin(config?: SidebarPluginConfig): CalendarPlugin;
package/dist/index.esm.js CHANGED
@@ -1,4 +1,4 @@
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';
1
+ import { ChevronDown, AudioLines, AlertCircle, useLocale, createPortal, cancelButton, LoadingButton, ChevronsUpDown, Check, ChevronRight, sidebarHeader, sidebarHeaderToggle, PanelRightClose, PanelRightOpen, sidebarHeaderTitle, getCalendarColorsForHex, generateUniKey, downloadICS, ContentSlot, MiniCalendar, ContextMenu, ContextMenuLabel, ContextMenuItem, ContextMenuSeparator, ContextMenuColorPicker, DefaultColorPicker, BlossomColorPicker, importICSFile, subscribeCalendar, 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';
@@ -45,7 +45,7 @@ const getCalendarInitials = (calendar) => {
45
45
  return name.charAt(0).toUpperCase();
46
46
  };
47
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;
48
+ var _a, _b;
49
49
  const isVisible = calendar.isVisible !== false;
50
50
  const calendarColor = ((_a = calendar.colors) === null || _a === void 0 ? void 0 : _a.lineColor) || '#3b82f6';
51
51
  const showIcon = Boolean(calendar.icon);
@@ -53,7 +53,9 @@ const CalendarItem = ({ calendar, isDraggable, isEditable: _isEditable, editingI
53
53
  const isActive = activeContextMenuCalendarId === calendar.id || editingId === calendar.id;
54
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
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));
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 }), ((_b = calendar.subscription) === null || _b === void 0 ? void 0 : _b.status) === 'error' && (u(AlertCircle, { width: 13, height: 13, className: 'ml-1 shrink-0 text-red-500', title: 'Failed to load subscription' })), calendar.subscribed &&
57
+ (!calendar.subscription ||
58
+ calendar.subscription.status === 'ready') && (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
59
  };
58
60
  const CalendarList = ({ calendars, onToggleVisibility, onReorder, onRename, onContextMenu, editingId, setEditingId, activeContextMenuCalendarId, isDraggable = true, isEditable = true, }) => {
59
61
  var _a;
@@ -245,16 +247,39 @@ function renderWithChip(template, name, color) {
245
247
  const DeleteCalendarDialog = ({ calendarId, calendarName, calendars, step, onStepChange, onConfirmDelete, onCancel, onMergeSelect, }) => {
246
248
  var _a, _b;
247
249
  const [showMergeDropdown, setShowMergeDropdown] = useState(false);
250
+ const [isLoading, setIsLoading] = useState(false);
248
251
  const { t } = useLocale();
249
252
  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
253
+ const handleMergeSelect = (id) => __awaiter(void 0, void 0, void 0, function* () {
254
+ if (isLoading)
255
+ return;
256
+ setIsLoading(true);
257
+ try {
258
+ yield onMergeSelect(id);
259
+ setShowMergeDropdown(false);
260
+ }
261
+ finally {
262
+ setIsLoading(false);
263
+ }
264
+ });
265
+ const handleConfirmDelete = () => __awaiter(void 0, void 0, void 0, function* () {
266
+ if (isLoading)
267
+ return;
268
+ setIsLoading(true);
269
+ try {
270
+ yield onConfirmDelete();
271
+ }
272
+ finally {
273
+ setIsLoading(false);
274
+ }
275
+ });
276
+ 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', disabled: isLoading, 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 disabled:opacity-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
251
277
  .filter(c => c.id !== calendarId)
252
278
  .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: () => {
253
- onMergeSelect(calendar.id);
254
- setShowMergeDropdown(false);
279
+ handleMergeSelect(calendar.id);
255
280
  }, children: [u("div", { className: 'mr-2 h-3 w-3 shrink-0 rounded-sm', style: {
256
281
  backgroundColor: calendar.colors.lineColor,
257
- } }), u("span", { className: 'whitespace-nowrap', children: calendar.name || calendar.id })] }, calendar.id))) }))] }), u("div", { className: 'flex gap-3', children: [u("button", { type: 'button', onClick: onCancel, className: cancelButton, children: t('cancel') }), u("button", { type: 'button', onClick: () => onStepChange('confirm_delete'), className: 'rounded-md bg-destructive px-4 py-2 text-xs font-medium text-destructive-foreground hover:bg-destructive/90', children: t('delete') })] })] })] })) : (u(Fragment, { children: [u("h2", { className: 'text-lg font-semibold text-gray-900 dark:text-white', children: t('confirmDeleteTitle', { calendarName }) }), u("p", { className: 'mt-3 text-sm text-gray-600 dark:text-gray-300', children: t('confirmDeleteMessage') }), u("div", { className: 'mt-6 flex justify-end gap-3', children: [u("button", { type: 'button', onClick: onCancel, className: 'rounded-md border border-border bg-background px-3 py-2 text-xs font-medium text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-slate-700', children: t('cancel') }), u("button", { type: 'button', onClick: onConfirmDelete, className: 'rounded-md bg-destructive px-3 py-2 text-xs font-medium text-destructive-foreground hover:bg-destructive/90', children: t('delete') })] })] })) }) }), document.body);
282
+ } }), u("span", { className: 'whitespace-nowrap', children: calendar.name || calendar.id })] }, calendar.id))) }))] }), u("div", { className: 'flex gap-3', children: [u("button", { type: 'button', onClick: onCancel, disabled: isLoading, className: `${cancelButton} disabled:opacity-50`, children: t('cancel') }), u("button", { type: 'button', onClick: () => onStepChange('confirm_delete'), disabled: isLoading, className: 'df-fill-destructive df-hover-destructive rounded-md px-4 py-2 text-xs font-medium disabled:opacity-50', children: t('delete') })] })] })] })) : (u(Fragment, { children: [u("h2", { className: 'text-lg font-semibold text-gray-900 dark:text-white', children: t('confirmDeleteTitle', { calendarName }) }), u("p", { className: 'mt-3 text-sm text-gray-600 dark:text-gray-300', children: t('confirmDeleteMessage') }), u("div", { className: 'mt-6 flex justify-end gap-3', children: [u("button", { type: 'button', onClick: onCancel, disabled: isLoading, className: 'rounded-md border border-border bg-background px-3 py-2 text-xs font-medium text-gray-700 hover:bg-gray-100 disabled:opacity-50 dark:text-gray-300 dark:hover:bg-slate-700', children: t('cancel') }), u(LoadingButton, { type: 'button', onClick: handleConfirmDelete, loading: isLoading, className: 'df-fill-destructive df-hover-destructive rounded-md px-3 py-2 text-xs font-medium', children: t('delete') })] })] })) }) }), document.body);
258
283
  };
259
284
 
260
285
  const NEW_CALENDAR_ID = 'new-calendar';
@@ -264,6 +289,7 @@ const ImportCalendarDialog = ({ calendars, filename, onConfirm, onCancel, }) =>
264
289
  const [selectedCalendarId, setSelectedCalendarId] = useState(((_a = calendars[0]) === null || _a === void 0 ? void 0 : _a.id) || NEW_CALENDAR_ID);
265
290
  const [isOpen, setIsOpen] = useState(false);
266
291
  const [shouldRender, setShouldRender] = useState(false);
292
+ const [isLoading, setIsLoading] = useState(false);
267
293
  const dropdownRef = useRef(null);
268
294
  const triggerRef = useRef(null);
269
295
  useEffect(() => {
@@ -293,6 +319,17 @@ const ImportCalendarDialog = ({ calendars, filename, onConfirm, onCancel, }) =>
293
319
  setSelectedCalendarId(id);
294
320
  setIsOpen(false);
295
321
  };
322
+ const handleConfirm = () => __awaiter(void 0, void 0, void 0, function* () {
323
+ if (isLoading)
324
+ return;
325
+ setIsLoading(true);
326
+ try {
327
+ yield onConfirm(selectedCalendarId);
328
+ }
329
+ finally {
330
+ setIsLoading(false);
331
+ }
332
+ });
296
333
  const renderDropdown = () => {
297
334
  var _a;
298
335
  if (!shouldRender)
@@ -300,17 +337,17 @@ const ImportCalendarDialog = ({ calendars, filename, onConfirm, onCancel, }) =>
300
337
  const rect = (_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
301
338
  if (!rect)
302
339
  return null;
303
- return createPortal(u("div", { ref: dropdownRef, className: `fixed z-110 mt-1 max-h-60 origin-top overflow-y-auto rounded-md border border-gray-200 bg-white shadow-lg transition-all duration-200 dark:border-slate-700 dark:bg-slate-800 ${isOpen ? 'scale-100 opacity-100' : 'scale-95 opacity-0'}`, style: {
340
+ return createPortal(u("div", { ref: dropdownRef, className: `df-portal fixed z-110 mt-1 max-h-60 origin-top overflow-y-auto rounded-md border border-gray-200 bg-white shadow-lg transition-all duration-200 dark:border-slate-700 dark:bg-slate-800 ${isOpen ? 'scale-100 opacity-100' : 'scale-95 opacity-0'}`, style: {
304
341
  top: rect.bottom,
305
342
  left: rect.left,
306
343
  width: rect.width,
307
344
  overscrollBehavior: 'none',
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);
345
+ }, 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 ? 'df-tint-primary' : ''}`, 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 ? 'df-text-primary font-medium' : 'text-gray-700 dark:text-gray-200'}`, children: calendar.name || calendar.id }), selectedCalendarId === calendar.id && (u(Check, { className: 'df-text-primary ml-2 h-4 w-4 shrink-0' }))] }, 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 ? 'df-tint-primary' : ''}`, onClick: () => handleSelect(NEW_CALENDAR_ID), children: [u("span", { className: `flex-1 truncate text-sm ${isNewSelected ? 'df-text-primary font-medium' : 'pl-6 text-gray-700 dark:text-gray-200'}`, children: [t('newCalendar') || 'New Calendar', ": ", filename] }), isNewSelected && (u(Check, { className: 'df-text-primary ml-2 h-4 w-4 shrink-0' }))] })] }) }), document.body);
309
346
  };
310
347
  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') ||
311
- 'This calendar contains new events. Please select a target calendar.' }), u("div", { className: 'relative', children: [u("button", { ref: triggerRef, type: 'button', className: 'flex w-full items-center rounded-md border border-gray-300 px-3 py-2 shadow-sm transition-colors hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-800', onClick: () => setIsOpen(!isOpen), children: [!isNewSelected && selectedCalendar && (u("div", { className: 'mr-3 h-3 w-3 shrink-0 rounded-sm', style: { backgroundColor: selectedCalendar.colors.lineColor } })), u("span", { className: `flex-1 truncate text-left text-sm font-medium text-gray-700 dark:text-gray-200 ${isNewSelected ? 'pl-0' : ''}`, children: isNewSelected
348
+ 'This calendar contains new events. Please select a target calendar.' }), u("div", { className: 'relative', children: [u("button", { ref: triggerRef, type: 'button', disabled: isLoading, className: 'flex w-full items-center rounded-md border border-gray-300 px-3 py-2 shadow-sm transition-colors hover:bg-gray-50 disabled:opacity-50 dark:border-gray-600 dark:hover:bg-gray-800', onClick: () => setIsOpen(!isOpen), children: [!isNewSelected && selectedCalendar && (u("div", { className: 'mr-3 h-3 w-3 shrink-0 rounded-sm', style: { backgroundColor: selectedCalendar.colors.lineColor } })), u("span", { className: `flex-1 truncate text-left text-sm font-medium text-gray-700 dark:text-gray-200 ${isNewSelected ? 'pl-0' : ''}`, children: isNewSelected
312
349
  ? `${t('newCalendar')}: ${filename}`
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' })] })] }) }));
350
+ : (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, disabled: isLoading, className: `${cancelButton} disabled:opacity-50`, children: t('cancel') || 'Cancel' }), u(LoadingButton, { type: 'button', onClick: handleConfirm, loading: isLoading, className: 'df-fill-primary df-hover-primary-solid rounded-md px-6 py-2 text-sm font-medium shadow-sm transition-colors', children: t('ok') || 'OK' })] })] }) }));
314
351
  };
315
352
 
316
353
  const SOURCE_SENTINEL = '\u0001S\u0001';
@@ -328,6 +365,7 @@ function renderLine(line, source, target) {
328
365
  }
329
366
  const MergeCalendarDialog = ({ sourceName, sourceColor, targetName, targetColor, onConfirm, onCancel, }) => {
330
367
  const { t } = useLocale();
368
+ const [isLoading, setIsLoading] = useState(false);
331
369
  const source = { name: sourceName, color: sourceColor };
332
370
  const target = { name: targetName, color: targetColor };
333
371
  const messageTemplate = t('mergeConfirmMessage', {
@@ -335,7 +373,18 @@ const MergeCalendarDialog = ({ sourceName, sourceColor, targetName, targetColor,
335
373
  targetName: TARGET_SENTINEL,
336
374
  });
337
375
  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') })] })] }) }));
376
+ const handleConfirm = () => __awaiter(void 0, void 0, void 0, function* () {
377
+ if (isLoading)
378
+ return;
379
+ setIsLoading(true);
380
+ try {
381
+ yield onConfirm();
382
+ }
383
+ finally {
384
+ setIsLoading(false);
385
+ }
386
+ });
387
+ 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, disabled: isLoading, className: `${cancelButton} disabled:opacity-50`, children: t('cancel') }), u(LoadingButton, { type: 'button', onClick: handleConfirm, loading: isLoading, className: 'df-fill-destructive df-hover-destructive rounded-md px-3 py-2 text-xs font-medium', children: t('merge') })] })] }) }));
339
388
  };
340
389
 
341
390
  const stopPropagation = (e) => e.stopPropagation();
@@ -398,8 +447,13 @@ const SubscribeCalendarDialog = ({ onSubscribe, onCancel, }) => {
398
447
  try {
399
448
  yield onSubscribe(trimmed);
400
449
  }
401
- catch (_a) {
402
- setError(t('subscribeError'));
450
+ catch (err) {
451
+ if (err.message === 'DUPLICATE_URL') {
452
+ setError(t('calendarAlreadySubscribed') || 'This URL is already subscribed');
453
+ }
454
+ else {
455
+ setError(t('subscribeError') || 'Failed to subscribe to calendar');
456
+ }
403
457
  }
404
458
  finally {
405
459
  setLoading(false);
@@ -411,10 +465,10 @@ const SubscribeCalendarDialog = ({ onSubscribe, onCancel, }) => {
411
465
  if (e.key === 'Escape')
412
466
  onCancel();
413
467
  };
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') })] })] }) }));
468
+ 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: 'df-focus-ring flex-1 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:ring-1 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: 'df-fill-primary df-hover-primary-solid rounded-md px-4 py-2 text-xs font-medium disabled:cursor-not-allowed disabled:opacity-50', children: loading ? t('fetchingCalendar') : t('subscribe') })] })] }) }));
415
469
  };
416
470
 
417
- const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCollapsed, setCollapsed, renderCalendarContextMenu, renderSidebarHeader, editingCalendarId: propEditingCalendarId, setEditingCalendarId: propSetEditingCalendarId, onCreateCalendar, }) => {
471
+ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCollapsed, setCollapsed, renderCalendarContextMenu, renderSidebarHeader, editingCalendarId: propEditingCalendarId, setEditingCalendarId: propSetEditingCalendarId, onCreateCalendar, onSubscribeCalendar, onLoadSubscription, }) => {
418
472
  var _a, _b;
419
473
  const { t } = useLocale();
420
474
  // Detect if custom color picker slot is provided
@@ -427,6 +481,33 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
427
481
  // File input ref for import
428
482
  const fileInputRef = useRef(null);
429
483
  const contextMenuRef = useRef(null);
484
+ // Track loaded subscription URLs to avoid redundant fetching
485
+ const loadedSubscriptionsRef = useRef(new Set());
486
+ // Auto-load subscriptions on mount or when calendars change
487
+ useEffect(() => {
488
+ calendars.forEach((calendar) => __awaiter(void 0, void 0, void 0, function* () {
489
+ var _a;
490
+ if (((_a = calendar.subscription) === null || _a === void 0 ? void 0 : _a.url) &&
491
+ !loadedSubscriptionsRef.current.has(calendar.subscription.url)) {
492
+ loadedSubscriptionsRef.current.add(calendar.subscription.url);
493
+ try {
494
+ if (onLoadSubscription) {
495
+ yield onLoadSubscription(calendar);
496
+ }
497
+ else {
498
+ const { events } = yield subscribeCalendar(calendar.subscription.url);
499
+ app.addExternalEvents(calendar.id, events);
500
+ }
501
+ }
502
+ catch (err) {
503
+ console.error(`Failed to auto-load calendar ${calendar.name}:`, err);
504
+ app.updateCalendar(calendar.id, {
505
+ subscription: Object.assign(Object.assign({}, calendar.subscription), { status: 'error' }),
506
+ });
507
+ }
508
+ }
509
+ }));
510
+ }, [app, calendars, onLoadSubscription]);
430
511
  const handleMonthChange = useCallback((offset) => {
431
512
  const current = app.getVisibleMonth();
432
513
  const next = new Date(current.getFullYear(), current.getMonth() + offset, 1);
@@ -531,19 +612,19 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
531
612
  handleCloseContextMenu();
532
613
  }
533
614
  }, [contextMenu, handleCloseContextMenu]);
534
- const handleMergeConfirm = useCallback(() => {
615
+ const handleMergeConfirm = useCallback(() => __awaiter(void 0, void 0, void 0, function* () {
535
616
  if (mergeState) {
536
617
  const { sourceId, targetId } = mergeState;
537
- app.mergeCalendars(sourceId, targetId);
618
+ yield app.mergeCalendars(sourceId, targetId);
538
619
  setMergeState(null);
539
620
  }
540
- }, [app, mergeState]);
541
- const handleConfirmDelete = useCallback(() => {
621
+ }), [app, mergeState]);
622
+ const handleConfirmDelete = useCallback(() => __awaiter(void 0, void 0, void 0, function* () {
542
623
  if (deleteState) {
543
- app.deleteCalendar(deleteState.calendarId);
624
+ yield app.deleteCalendar(deleteState.calendarId);
544
625
  setDeleteState(null);
545
626
  }
546
- }, [app, deleteState]);
627
+ }), [app, deleteState]);
547
628
  const handleDeleteMergeSelect = useCallback((targetId) => {
548
629
  if (deleteState) {
549
630
  setMergeState({
@@ -565,45 +646,31 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
565
646
  handleCloseSidebarContextMenu();
566
647
  }, [handleCloseSidebarContextMenu]);
567
648
  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
- });
649
+ var _a;
650
+ // 1. Check for duplicates
651
+ const isDuplicate = calendars.some(c => { var _a; return ((_a = c.subscription) === null || _a === void 0 ? void 0 : _a.url) === url; });
652
+ if (isDuplicate) {
653
+ throw new Error('DUPLICATE_URL');
654
+ }
655
+ // 2. Load the subscription (fetch + parse) using the new utility
656
+ const { calendar, events } = yield subscribeCalendar(url);
657
+ // 3. Mark as loaded to avoid the useEffect triggering another fetch
658
+ if ((_a = calendar.subscription) === null || _a === void 0 ? void 0 : _a.url) {
659
+ loadedSubscriptionsRef.current.add(calendar.subscription.url);
660
+ }
661
+ // 4. Delegate to user if callback exists, otherwise use default behavior
662
+ if (onSubscribeCalendar) {
663
+ yield onSubscribeCalendar(calendar, events);
664
+ }
665
+ else {
666
+ // Default behavior: create calendar in the app
667
+ app.createCalendar(calendar);
668
+ }
669
+ // 4. Always add events to the internal external store for IMMEDIATE display
670
+ app.addExternalEvents(calendar.id, events);
671
+ // 5. Close dialog
605
672
  setSubscribeDialogOpen(false);
606
- }), [app]);
673
+ }), [app, onSubscribeCalendar, calendars]);
607
674
  const handleFileChange = useCallback((e) => __awaiter(void 0, void 0, void 0, function* () {
608
675
  var _a;
609
676
  const file = (_a = e.currentTarget.files) === null || _a === void 0 ? void 0 : _a[0];
@@ -687,8 +754,20 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
687
754
  ? ((_a = calendars.find(c => c.id === deleteState.calendarId)) === null || _a === void 0 ? void 0 : _a.name) || 'Unknown'
688
755
  : '';
689
756
  const readOnlyConfig = app.getReadOnlyConfig();
690
- const isEditable = !app.state.readOnly;
757
+ const isEditable = app.canMutateFromUI();
691
758
  const isDraggable = readOnlyConfig.draggable !== false;
759
+ useEffect(() => {
760
+ if (isEditable)
761
+ return;
762
+ setContextMenu(null);
763
+ setSidebarContextMenu(null);
764
+ setCustomColorPicker(null);
765
+ setMergeState(null);
766
+ setDeleteState(null);
767
+ setImportState(null);
768
+ setSubscribeDialogOpen(false);
769
+ setEditingCalendarId(null);
770
+ }, [isEditable, setEditingCalendarId]);
692
771
  return (u("div", { className: sidebarContainer, onContextMenu: isEditable ? handleSidebarContextMenu : undefined, children: [u(ContentSlot, { generatorName: 'sidebarHeader', generatorArgs: {
693
772
  isCollapsed,
694
773
  onCollapseToggle: () => setCollapsed(!isCollapsed),
@@ -719,21 +798,27 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
719
798
  ? handleContextMenu
720
799
  : () => {
721
800
  /* noop */
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: {
801
+ }, 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) }) })] })), isEditable && contextMenu && (u(ContextMenu, { ref: contextMenuRef, x: contextMenu.x, y: contextMenu.y, onClose: handleCloseContextMenu, className: 'w-64 p-2', children: u(ContentSlot, { generatorName: 'calendarContextMenu', generatorArgs: {
723
802
  calendar: calendars.find(c => c.id === contextMenu.calendarId),
724
803
  onClose: handleCloseContextMenu,
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 &&
804
+ }, 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 })] })) }) })), isEditable &&
805
+ sidebarContextMenu &&
726
806
  createPortal(u(ContextMenu, { x: sidebarContextMenu.x, y: sidebarContextMenu.y, onClose: handleCloseSidebarContextMenu, className: 'w-max p-2', children: [u(ContextMenuItem, { onClick: () => {
727
807
  onCreateCalendar === null || onCreateCalendar === void 0 ? void 0 : onCreateCalendar();
728
808
  handleCloseSidebarContextMenu();
729
809
  }, 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: () => {
730
810
  app.triggerRender();
731
811
  handleCloseSidebarContextMenu();
732
- }, children: t('refreshAll') || 'Refresh All' })] }), document.body), u("input", { ref: fileInputRef, type: 'file', accept: '.ics', style: { display: 'none' }, onChange: handleFileChange }), importState &&
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 &&
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 &&
812
+ }, children: t('refreshAll') || 'Refresh All' })] }), document.body), u("input", { ref: fileInputRef, type: 'file', accept: '.ics', style: { display: 'none' }, onChange: handleFileChange }), isEditable &&
813
+ importState &&
814
+ createPortal(u(ImportCalendarDialog, { calendars: calendars, filename: importState.filename, onConfirm: handleImportConfirm, onCancel: () => setImportState(null) }), document.body), isEditable &&
815
+ subscribeDialogOpen &&
816
+ createPortal(u(SubscribeCalendarDialog, { onSubscribe: handleSubscribeConfirm, onCancel: () => setSubscribeDialogOpen(false) }), document.body), isEditable &&
817
+ mergeState &&
818
+ createPortal(u(MergeCalendarDialog, { sourceName: sourceCalendarName, sourceColor: sourceCalendarColor, targetName: targetCalendarName, targetColor: targetCalendarColor, onConfirm: handleMergeConfirm, onCancel: () => setMergeState(null) }), document.body), isEditable &&
819
+ deleteState &&
820
+ 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), isEditable &&
821
+ customColorPicker &&
737
822
  createPortal(u("div", { className: 'fixed inset-0 z-50', onMouseDown: () => {
738
823
  app.updateCalendar(customColorPicker.calendarId, {});
739
824
  setCustomColorPicker(null);
@@ -804,6 +889,7 @@ function createSidebarPlugin(config = {}) {
804
889
  const [sidebarVersion, setSidebarVersion] = useState(0);
805
890
  const [editingCalendarId, setEditingCalendarId] = useState(null);
806
891
  const [showCreateDialog, setShowCreateDialog] = useState(false);
892
+ const isEditable = app.canMutateFromUI();
807
893
  const refreshSidebar = useCallback(() => {
808
894
  setSidebarVersion(prev => prev + 1);
809
895
  }, []);
@@ -820,6 +906,8 @@ function createSidebarPlugin(config = {}) {
820
906
  refreshSidebar();
821
907
  }, [app, refreshSidebar]);
822
908
  const handleCreateCalendar = useCallback(() => {
909
+ if (!isEditable)
910
+ return;
823
911
  const createMode = config.createCalendarMode || 'inline';
824
912
  if (createMode === 'modal') {
825
913
  setShowCreateDialog(true);
@@ -839,7 +927,13 @@ function createSidebarPlugin(config = {}) {
839
927
  app.createCalendar(newCalendar);
840
928
  setEditingCalendarId(newId);
841
929
  refreshSidebar();
842
- }, [app, t, refreshSidebar]);
930
+ }, [app, isEditable, t, refreshSidebar]);
931
+ useEffect(() => {
932
+ if (isEditable)
933
+ return;
934
+ setShowCreateDialog(false);
935
+ setEditingCalendarId(null);
936
+ }, [isEditable]);
843
937
  const sidebarProps = useMemo(() => ({
844
938
  app,
845
939
  calendars,
@@ -854,6 +948,8 @@ function createSidebarPlugin(config = {}) {
854
948
  editingCalendarId,
855
949
  setEditingCalendarId,
856
950
  onCreateCalendar: handleCreateCalendar,
951
+ onSubscribeCalendar: config.onSubscribeCalendar,
952
+ onLoadSubscription: config.onLoadSubscription,
857
953
  }), [
858
954
  app,
859
955
  calendars,
@@ -874,14 +970,14 @@ function createSidebarPlugin(config = {}) {
874
970
  return h(DefaultCalendarSidebar, Object.assign({}, sidebarProps));
875
971
  };
876
972
  const renderExtraContent = () => {
877
- if (!showCreateDialog)
973
+ if (!isEditable || !showCreateDialog)
878
974
  return null;
879
975
  const onClose = () => setShowCreateDialog(false);
880
- const onCreate = (newCalendar) => {
881
- app.createCalendar(newCalendar);
976
+ const onCreate = (newCalendar) => __awaiter(this, void 0, void 0, function* () {
977
+ yield app.createCalendar(newCalendar);
882
978
  setShowCreateDialog(false);
883
979
  refreshSidebar();
884
- };
980
+ });
885
981
  const generatorArgs = {
886
982
  onClose,
887
983
  onCreate,
package/dist/index.js CHANGED
@@ -47,7 +47,7 @@ const getCalendarInitials = (calendar) => {
47
47
  return name.charAt(0).toUpperCase();
48
48
  };
49
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;
50
+ var _a, _b;
51
51
  const isVisible = calendar.isVisible !== false;
52
52
  const calendarColor = ((_a = calendar.colors) === null || _a === void 0 ? void 0 : _a.lineColor) || '#3b82f6';
53
53
  const showIcon = Boolean(calendar.icon);
@@ -55,7 +55,9 @@ const CalendarItem = ({ calendar, isDraggable, isEditable: _isEditable, editingI
55
55
  const isActive = activeContextMenuCalendarId === calendar.id || editingId === calendar.id;
56
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
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));
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 }), ((_b = calendar.subscription) === null || _b === void 0 ? void 0 : _b.status) === 'error' && (u(core.AlertCircle, { width: 13, height: 13, className: 'ml-1 shrink-0 text-red-500', title: 'Failed to load subscription' })), calendar.subscribed &&
59
+ (!calendar.subscription ||
60
+ calendar.subscription.status === 'ready') && (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
61
  };
60
62
  const CalendarList = ({ calendars, onToggleVisibility, onReorder, onRename, onContextMenu, editingId, setEditingId, activeContextMenuCalendarId, isDraggable = true, isEditable = true, }) => {
61
63
  var _a;
@@ -247,16 +249,39 @@ function renderWithChip(template, name, color) {
247
249
  const DeleteCalendarDialog = ({ calendarId, calendarName, calendars, step, onStepChange, onConfirmDelete, onCancel, onMergeSelect, }) => {
248
250
  var _a, _b;
249
251
  const [showMergeDropdown, setShowMergeDropdown] = hooks.useState(false);
252
+ const [isLoading, setIsLoading] = hooks.useState(false);
250
253
  const { t } = core.useLocale();
251
254
  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
255
+ const handleMergeSelect = (id) => __awaiter(void 0, void 0, void 0, function* () {
256
+ if (isLoading)
257
+ return;
258
+ setIsLoading(true);
259
+ try {
260
+ yield onMergeSelect(id);
261
+ setShowMergeDropdown(false);
262
+ }
263
+ finally {
264
+ setIsLoading(false);
265
+ }
266
+ });
267
+ const handleConfirmDelete = () => __awaiter(void 0, void 0, void 0, function* () {
268
+ if (isLoading)
269
+ return;
270
+ setIsLoading(true);
271
+ try {
272
+ yield onConfirmDelete();
273
+ }
274
+ finally {
275
+ setIsLoading(false);
276
+ }
277
+ });
278
+ 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', disabled: isLoading, 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 disabled:opacity-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
253
279
  .filter(c => c.id !== calendarId)
254
280
  .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: () => {
255
- onMergeSelect(calendar.id);
256
- setShowMergeDropdown(false);
281
+ handleMergeSelect(calendar.id);
257
282
  }, children: [u("div", { className: 'mr-2 h-3 w-3 shrink-0 rounded-sm', style: {
258
283
  backgroundColor: calendar.colors.lineColor,
259
- } }), u("span", { className: 'whitespace-nowrap', children: calendar.name || calendar.id })] }, calendar.id))) }))] }), u("div", { className: 'flex gap-3', children: [u("button", { type: 'button', onClick: onCancel, className: core.cancelButton, children: t('cancel') }), u("button", { type: 'button', onClick: () => onStepChange('confirm_delete'), className: 'rounded-md bg-destructive px-4 py-2 text-xs font-medium text-destructive-foreground hover:bg-destructive/90', children: t('delete') })] })] })] })) : (u(preact.Fragment, { children: [u("h2", { className: 'text-lg font-semibold text-gray-900 dark:text-white', children: t('confirmDeleteTitle', { calendarName }) }), u("p", { className: 'mt-3 text-sm text-gray-600 dark:text-gray-300', children: t('confirmDeleteMessage') }), u("div", { className: 'mt-6 flex justify-end gap-3', children: [u("button", { type: 'button', onClick: onCancel, className: 'rounded-md border border-border bg-background px-3 py-2 text-xs font-medium text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-slate-700', children: t('cancel') }), u("button", { type: 'button', onClick: onConfirmDelete, className: 'rounded-md bg-destructive px-3 py-2 text-xs font-medium text-destructive-foreground hover:bg-destructive/90', children: t('delete') })] })] })) }) }), document.body);
284
+ } }), u("span", { className: 'whitespace-nowrap', children: calendar.name || calendar.id })] }, calendar.id))) }))] }), u("div", { className: 'flex gap-3', children: [u("button", { type: 'button', onClick: onCancel, disabled: isLoading, className: `${core.cancelButton} disabled:opacity-50`, children: t('cancel') }), u("button", { type: 'button', onClick: () => onStepChange('confirm_delete'), disabled: isLoading, className: 'df-fill-destructive df-hover-destructive rounded-md px-4 py-2 text-xs font-medium disabled:opacity-50', children: t('delete') })] })] })] })) : (u(preact.Fragment, { children: [u("h2", { className: 'text-lg font-semibold text-gray-900 dark:text-white', children: t('confirmDeleteTitle', { calendarName }) }), u("p", { className: 'mt-3 text-sm text-gray-600 dark:text-gray-300', children: t('confirmDeleteMessage') }), u("div", { className: 'mt-6 flex justify-end gap-3', children: [u("button", { type: 'button', onClick: onCancel, disabled: isLoading, className: 'rounded-md border border-border bg-background px-3 py-2 text-xs font-medium text-gray-700 hover:bg-gray-100 disabled:opacity-50 dark:text-gray-300 dark:hover:bg-slate-700', children: t('cancel') }), u(core.LoadingButton, { type: 'button', onClick: handleConfirmDelete, loading: isLoading, className: 'df-fill-destructive df-hover-destructive rounded-md px-3 py-2 text-xs font-medium', children: t('delete') })] })] })) }) }), document.body);
260
285
  };
261
286
 
262
287
  const NEW_CALENDAR_ID = 'new-calendar';
@@ -266,6 +291,7 @@ const ImportCalendarDialog = ({ calendars, filename, onConfirm, onCancel, }) =>
266
291
  const [selectedCalendarId, setSelectedCalendarId] = hooks.useState(((_a = calendars[0]) === null || _a === void 0 ? void 0 : _a.id) || NEW_CALENDAR_ID);
267
292
  const [isOpen, setIsOpen] = hooks.useState(false);
268
293
  const [shouldRender, setShouldRender] = hooks.useState(false);
294
+ const [isLoading, setIsLoading] = hooks.useState(false);
269
295
  const dropdownRef = hooks.useRef(null);
270
296
  const triggerRef = hooks.useRef(null);
271
297
  hooks.useEffect(() => {
@@ -295,6 +321,17 @@ const ImportCalendarDialog = ({ calendars, filename, onConfirm, onCancel, }) =>
295
321
  setSelectedCalendarId(id);
296
322
  setIsOpen(false);
297
323
  };
324
+ const handleConfirm = () => __awaiter(void 0, void 0, void 0, function* () {
325
+ if (isLoading)
326
+ return;
327
+ setIsLoading(true);
328
+ try {
329
+ yield onConfirm(selectedCalendarId);
330
+ }
331
+ finally {
332
+ setIsLoading(false);
333
+ }
334
+ });
298
335
  const renderDropdown = () => {
299
336
  var _a;
300
337
  if (!shouldRender)
@@ -302,17 +339,17 @@ const ImportCalendarDialog = ({ calendars, filename, onConfirm, onCancel, }) =>
302
339
  const rect = (_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
303
340
  if (!rect)
304
341
  return null;
305
- return core.createPortal(u("div", { ref: dropdownRef, className: `fixed z-110 mt-1 max-h-60 origin-top overflow-y-auto rounded-md border border-gray-200 bg-white shadow-lg transition-all duration-200 dark:border-slate-700 dark:bg-slate-800 ${isOpen ? 'scale-100 opacity-100' : 'scale-95 opacity-0'}`, style: {
342
+ return core.createPortal(u("div", { ref: dropdownRef, className: `df-portal fixed z-110 mt-1 max-h-60 origin-top overflow-y-auto rounded-md border border-gray-200 bg-white shadow-lg transition-all duration-200 dark:border-slate-700 dark:bg-slate-800 ${isOpen ? 'scale-100 opacity-100' : 'scale-95 opacity-0'}`, style: {
306
343
  top: rect.bottom,
307
344
  left: rect.left,
308
345
  width: rect.width,
309
346
  overscrollBehavior: 'none',
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);
347
+ }, 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 ? 'df-tint-primary' : ''}`, 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 ? 'df-text-primary font-medium' : 'text-gray-700 dark:text-gray-200'}`, children: calendar.name || calendar.id }), selectedCalendarId === calendar.id && (u(core.Check, { className: 'df-text-primary ml-2 h-4 w-4 shrink-0' }))] }, 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 ? 'df-tint-primary' : ''}`, onClick: () => handleSelect(NEW_CALENDAR_ID), children: [u("span", { className: `flex-1 truncate text-sm ${isNewSelected ? 'df-text-primary font-medium' : 'pl-6 text-gray-700 dark:text-gray-200'}`, children: [t('newCalendar') || 'New Calendar', ": ", filename] }), isNewSelected && (u(core.Check, { className: 'df-text-primary ml-2 h-4 w-4 shrink-0' }))] })] }) }), document.body);
311
348
  };
312
349
  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') ||
313
- 'This calendar contains new events. Please select a target calendar.' }), u("div", { className: 'relative', children: [u("button", { ref: triggerRef, type: 'button', className: 'flex w-full items-center rounded-md border border-gray-300 px-3 py-2 shadow-sm transition-colors hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-800', onClick: () => setIsOpen(!isOpen), children: [!isNewSelected && selectedCalendar && (u("div", { className: 'mr-3 h-3 w-3 shrink-0 rounded-sm', style: { backgroundColor: selectedCalendar.colors.lineColor } })), u("span", { className: `flex-1 truncate text-left text-sm font-medium text-gray-700 dark:text-gray-200 ${isNewSelected ? 'pl-0' : ''}`, children: isNewSelected
350
+ 'This calendar contains new events. Please select a target calendar.' }), u("div", { className: 'relative', children: [u("button", { ref: triggerRef, type: 'button', disabled: isLoading, className: 'flex w-full items-center rounded-md border border-gray-300 px-3 py-2 shadow-sm transition-colors hover:bg-gray-50 disabled:opacity-50 dark:border-gray-600 dark:hover:bg-gray-800', onClick: () => setIsOpen(!isOpen), children: [!isNewSelected && selectedCalendar && (u("div", { className: 'mr-3 h-3 w-3 shrink-0 rounded-sm', style: { backgroundColor: selectedCalendar.colors.lineColor } })), u("span", { className: `flex-1 truncate text-left text-sm font-medium text-gray-700 dark:text-gray-200 ${isNewSelected ? 'pl-0' : ''}`, children: isNewSelected
314
351
  ? `${t('newCalendar')}: ${filename}`
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' })] })] }) }));
352
+ : (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, disabled: isLoading, className: `${core.cancelButton} disabled:opacity-50`, children: t('cancel') || 'Cancel' }), u(core.LoadingButton, { type: 'button', onClick: handleConfirm, loading: isLoading, className: 'df-fill-primary df-hover-primary-solid rounded-md px-6 py-2 text-sm font-medium shadow-sm transition-colors', children: t('ok') || 'OK' })] })] }) }));
316
353
  };
317
354
 
318
355
  const SOURCE_SENTINEL = '\u0001S\u0001';
@@ -330,6 +367,7 @@ function renderLine(line, source, target) {
330
367
  }
331
368
  const MergeCalendarDialog = ({ sourceName, sourceColor, targetName, targetColor, onConfirm, onCancel, }) => {
332
369
  const { t } = core.useLocale();
370
+ const [isLoading, setIsLoading] = hooks.useState(false);
333
371
  const source = { name: sourceName, color: sourceColor };
334
372
  const target = { name: targetName, color: targetColor };
335
373
  const messageTemplate = t('mergeConfirmMessage', {
@@ -337,7 +375,18 @@ const MergeCalendarDialog = ({ sourceName, sourceColor, targetName, targetColor,
337
375
  targetName: TARGET_SENTINEL,
338
376
  });
339
377
  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') })] })] }) }));
378
+ const handleConfirm = () => __awaiter(void 0, void 0, void 0, function* () {
379
+ if (isLoading)
380
+ return;
381
+ setIsLoading(true);
382
+ try {
383
+ yield onConfirm();
384
+ }
385
+ finally {
386
+ setIsLoading(false);
387
+ }
388
+ });
389
+ 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, disabled: isLoading, className: `${core.cancelButton} disabled:opacity-50`, children: t('cancel') }), u(core.LoadingButton, { type: 'button', onClick: handleConfirm, loading: isLoading, className: 'df-fill-destructive df-hover-destructive rounded-md px-3 py-2 text-xs font-medium', children: t('merge') })] })] }) }));
341
390
  };
342
391
 
343
392
  const stopPropagation = (e) => e.stopPropagation();
@@ -400,8 +449,13 @@ const SubscribeCalendarDialog = ({ onSubscribe, onCancel, }) => {
400
449
  try {
401
450
  yield onSubscribe(trimmed);
402
451
  }
403
- catch (_a) {
404
- setError(t('subscribeError'));
452
+ catch (err) {
453
+ if (err.message === 'DUPLICATE_URL') {
454
+ setError(t('calendarAlreadySubscribed') || 'This URL is already subscribed');
455
+ }
456
+ else {
457
+ setError(t('subscribeError') || 'Failed to subscribe to calendar');
458
+ }
405
459
  }
406
460
  finally {
407
461
  setLoading(false);
@@ -413,10 +467,10 @@ const SubscribeCalendarDialog = ({ onSubscribe, onCancel, }) => {
413
467
  if (e.key === 'Escape')
414
468
  onCancel();
415
469
  };
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') })] })] }) }));
470
+ 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: 'df-focus-ring flex-1 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:ring-1 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: 'df-fill-primary df-hover-primary-solid rounded-md px-4 py-2 text-xs font-medium disabled:cursor-not-allowed disabled:opacity-50', children: loading ? t('fetchingCalendar') : t('subscribe') })] })] }) }));
417
471
  };
418
472
 
419
- const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCollapsed, setCollapsed, renderCalendarContextMenu, renderSidebarHeader, editingCalendarId: propEditingCalendarId, setEditingCalendarId: propSetEditingCalendarId, onCreateCalendar, }) => {
473
+ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCollapsed, setCollapsed, renderCalendarContextMenu, renderSidebarHeader, editingCalendarId: propEditingCalendarId, setEditingCalendarId: propSetEditingCalendarId, onCreateCalendar, onSubscribeCalendar, onLoadSubscription, }) => {
420
474
  var _a, _b;
421
475
  const { t } = core.useLocale();
422
476
  // Detect if custom color picker slot is provided
@@ -429,6 +483,33 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
429
483
  // File input ref for import
430
484
  const fileInputRef = hooks.useRef(null);
431
485
  const contextMenuRef = hooks.useRef(null);
486
+ // Track loaded subscription URLs to avoid redundant fetching
487
+ const loadedSubscriptionsRef = hooks.useRef(new Set());
488
+ // Auto-load subscriptions on mount or when calendars change
489
+ hooks.useEffect(() => {
490
+ calendars.forEach((calendar) => __awaiter(void 0, void 0, void 0, function* () {
491
+ var _a;
492
+ if (((_a = calendar.subscription) === null || _a === void 0 ? void 0 : _a.url) &&
493
+ !loadedSubscriptionsRef.current.has(calendar.subscription.url)) {
494
+ loadedSubscriptionsRef.current.add(calendar.subscription.url);
495
+ try {
496
+ if (onLoadSubscription) {
497
+ yield onLoadSubscription(calendar);
498
+ }
499
+ else {
500
+ const { events } = yield core.subscribeCalendar(calendar.subscription.url);
501
+ app.addExternalEvents(calendar.id, events);
502
+ }
503
+ }
504
+ catch (err) {
505
+ console.error(`Failed to auto-load calendar ${calendar.name}:`, err);
506
+ app.updateCalendar(calendar.id, {
507
+ subscription: Object.assign(Object.assign({}, calendar.subscription), { status: 'error' }),
508
+ });
509
+ }
510
+ }
511
+ }));
512
+ }, [app, calendars, onLoadSubscription]);
432
513
  const handleMonthChange = hooks.useCallback((offset) => {
433
514
  const current = app.getVisibleMonth();
434
515
  const next = new Date(current.getFullYear(), current.getMonth() + offset, 1);
@@ -533,19 +614,19 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
533
614
  handleCloseContextMenu();
534
615
  }
535
616
  }, [contextMenu, handleCloseContextMenu]);
536
- const handleMergeConfirm = hooks.useCallback(() => {
617
+ const handleMergeConfirm = hooks.useCallback(() => __awaiter(void 0, void 0, void 0, function* () {
537
618
  if (mergeState) {
538
619
  const { sourceId, targetId } = mergeState;
539
- app.mergeCalendars(sourceId, targetId);
620
+ yield app.mergeCalendars(sourceId, targetId);
540
621
  setMergeState(null);
541
622
  }
542
- }, [app, mergeState]);
543
- const handleConfirmDelete = hooks.useCallback(() => {
623
+ }), [app, mergeState]);
624
+ const handleConfirmDelete = hooks.useCallback(() => __awaiter(void 0, void 0, void 0, function* () {
544
625
  if (deleteState) {
545
- app.deleteCalendar(deleteState.calendarId);
626
+ yield app.deleteCalendar(deleteState.calendarId);
546
627
  setDeleteState(null);
547
628
  }
548
- }, [app, deleteState]);
629
+ }), [app, deleteState]);
549
630
  const handleDeleteMergeSelect = hooks.useCallback((targetId) => {
550
631
  if (deleteState) {
551
632
  setMergeState({
@@ -567,45 +648,31 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
567
648
  handleCloseSidebarContextMenu();
568
649
  }, [handleCloseSidebarContextMenu]);
569
650
  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
- });
651
+ var _a;
652
+ // 1. Check for duplicates
653
+ const isDuplicate = calendars.some(c => { var _a; return ((_a = c.subscription) === null || _a === void 0 ? void 0 : _a.url) === url; });
654
+ if (isDuplicate) {
655
+ throw new Error('DUPLICATE_URL');
656
+ }
657
+ // 2. Load the subscription (fetch + parse) using the new utility
658
+ const { calendar, events } = yield core.subscribeCalendar(url);
659
+ // 3. Mark as loaded to avoid the useEffect triggering another fetch
660
+ if ((_a = calendar.subscription) === null || _a === void 0 ? void 0 : _a.url) {
661
+ loadedSubscriptionsRef.current.add(calendar.subscription.url);
662
+ }
663
+ // 4. Delegate to user if callback exists, otherwise use default behavior
664
+ if (onSubscribeCalendar) {
665
+ yield onSubscribeCalendar(calendar, events);
666
+ }
667
+ else {
668
+ // Default behavior: create calendar in the app
669
+ app.createCalendar(calendar);
670
+ }
671
+ // 4. Always add events to the internal external store for IMMEDIATE display
672
+ app.addExternalEvents(calendar.id, events);
673
+ // 5. Close dialog
607
674
  setSubscribeDialogOpen(false);
608
- }), [app]);
675
+ }), [app, onSubscribeCalendar, calendars]);
609
676
  const handleFileChange = hooks.useCallback((e) => __awaiter(void 0, void 0, void 0, function* () {
610
677
  var _a;
611
678
  const file = (_a = e.currentTarget.files) === null || _a === void 0 ? void 0 : _a[0];
@@ -689,8 +756,20 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
689
756
  ? ((_a = calendars.find(c => c.id === deleteState.calendarId)) === null || _a === void 0 ? void 0 : _a.name) || 'Unknown'
690
757
  : '';
691
758
  const readOnlyConfig = app.getReadOnlyConfig();
692
- const isEditable = !app.state.readOnly;
759
+ const isEditable = app.canMutateFromUI();
693
760
  const isDraggable = readOnlyConfig.draggable !== false;
761
+ hooks.useEffect(() => {
762
+ if (isEditable)
763
+ return;
764
+ setContextMenu(null);
765
+ setSidebarContextMenu(null);
766
+ setCustomColorPicker(null);
767
+ setMergeState(null);
768
+ setDeleteState(null);
769
+ setImportState(null);
770
+ setSubscribeDialogOpen(false);
771
+ setEditingCalendarId(null);
772
+ }, [isEditable, setEditingCalendarId]);
694
773
  return (u("div", { className: core.sidebarContainer, onContextMenu: isEditable ? handleSidebarContextMenu : undefined, children: [u(core.ContentSlot, { generatorName: 'sidebarHeader', generatorArgs: {
695
774
  isCollapsed,
696
775
  onCollapseToggle: () => setCollapsed(!isCollapsed),
@@ -721,21 +800,27 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
721
800
  ? handleContextMenu
722
801
  : () => {
723
802
  /* noop */
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: {
803
+ }, 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) }) })] })), isEditable && 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: {
725
804
  calendar: calendars.find(c => c.id === contextMenu.calendarId),
726
805
  onClose: handleCloseContextMenu,
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 &&
806
+ }, 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 })] })) }) })), isEditable &&
807
+ sidebarContextMenu &&
728
808
  core.createPortal(u(core.ContextMenu, { x: sidebarContextMenu.x, y: sidebarContextMenu.y, onClose: handleCloseSidebarContextMenu, className: 'w-max p-2', children: [u(core.ContextMenuItem, { onClick: () => {
729
809
  onCreateCalendar === null || onCreateCalendar === void 0 ? void 0 : onCreateCalendar();
730
810
  handleCloseSidebarContextMenu();
731
811
  }, 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: () => {
732
812
  app.triggerRender();
733
813
  handleCloseSidebarContextMenu();
734
- }, children: t('refreshAll') || 'Refresh All' })] }), document.body), u("input", { ref: fileInputRef, type: 'file', accept: '.ics', style: { display: 'none' }, onChange: handleFileChange }), importState &&
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 &&
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 &&
814
+ }, children: t('refreshAll') || 'Refresh All' })] }), document.body), u("input", { ref: fileInputRef, type: 'file', accept: '.ics', style: { display: 'none' }, onChange: handleFileChange }), isEditable &&
815
+ importState &&
816
+ core.createPortal(u(ImportCalendarDialog, { calendars: calendars, filename: importState.filename, onConfirm: handleImportConfirm, onCancel: () => setImportState(null) }), document.body), isEditable &&
817
+ subscribeDialogOpen &&
818
+ core.createPortal(u(SubscribeCalendarDialog, { onSubscribe: handleSubscribeConfirm, onCancel: () => setSubscribeDialogOpen(false) }), document.body), isEditable &&
819
+ mergeState &&
820
+ core.createPortal(u(MergeCalendarDialog, { sourceName: sourceCalendarName, sourceColor: sourceCalendarColor, targetName: targetCalendarName, targetColor: targetCalendarColor, onConfirm: handleMergeConfirm, onCancel: () => setMergeState(null) }), document.body), isEditable &&
821
+ deleteState &&
822
+ 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), isEditable &&
823
+ customColorPicker &&
739
824
  core.createPortal(u("div", { className: 'fixed inset-0 z-50', onMouseDown: () => {
740
825
  app.updateCalendar(customColorPicker.calendarId, {});
741
826
  setCustomColorPicker(null);
@@ -806,6 +891,7 @@ function createSidebarPlugin(config = {}) {
806
891
  const [sidebarVersion, setSidebarVersion] = hooks.useState(0);
807
892
  const [editingCalendarId, setEditingCalendarId] = hooks.useState(null);
808
893
  const [showCreateDialog, setShowCreateDialog] = hooks.useState(false);
894
+ const isEditable = app.canMutateFromUI();
809
895
  const refreshSidebar = hooks.useCallback(() => {
810
896
  setSidebarVersion(prev => prev + 1);
811
897
  }, []);
@@ -822,6 +908,8 @@ function createSidebarPlugin(config = {}) {
822
908
  refreshSidebar();
823
909
  }, [app, refreshSidebar]);
824
910
  const handleCreateCalendar = hooks.useCallback(() => {
911
+ if (!isEditable)
912
+ return;
825
913
  const createMode = config.createCalendarMode || 'inline';
826
914
  if (createMode === 'modal') {
827
915
  setShowCreateDialog(true);
@@ -841,7 +929,13 @@ function createSidebarPlugin(config = {}) {
841
929
  app.createCalendar(newCalendar);
842
930
  setEditingCalendarId(newId);
843
931
  refreshSidebar();
844
- }, [app, t, refreshSidebar]);
932
+ }, [app, isEditable, t, refreshSidebar]);
933
+ hooks.useEffect(() => {
934
+ if (isEditable)
935
+ return;
936
+ setShowCreateDialog(false);
937
+ setEditingCalendarId(null);
938
+ }, [isEditable]);
845
939
  const sidebarProps = hooks.useMemo(() => ({
846
940
  app,
847
941
  calendars,
@@ -856,6 +950,8 @@ function createSidebarPlugin(config = {}) {
856
950
  editingCalendarId,
857
951
  setEditingCalendarId,
858
952
  onCreateCalendar: handleCreateCalendar,
953
+ onSubscribeCalendar: config.onSubscribeCalendar,
954
+ onLoadSubscription: config.onLoadSubscription,
859
955
  }), [
860
956
  app,
861
957
  calendars,
@@ -876,14 +972,14 @@ function createSidebarPlugin(config = {}) {
876
972
  return preact.h(DefaultCalendarSidebar, Object.assign({}, sidebarProps));
877
973
  };
878
974
  const renderExtraContent = () => {
879
- if (!showCreateDialog)
975
+ if (!isEditable || !showCreateDialog)
880
976
  return null;
881
977
  const onClose = () => setShowCreateDialog(false);
882
- const onCreate = (newCalendar) => {
883
- app.createCalendar(newCalendar);
978
+ const onCreate = (newCalendar) => __awaiter(this, void 0, void 0, function* () {
979
+ yield app.createCalendar(newCalendar);
884
980
  setShowCreateDialog(false);
885
981
  refreshSidebar();
886
- };
982
+ });
887
983
  const generatorArgs = {
888
984
  onClose,
889
985
  onCreate,
@@ -1,4 +1,4 @@
1
1
  import { JSX } from 'preact';
2
2
  import type { CalendarSidebarRenderProps } from './plugin';
3
- declare const DefaultCalendarSidebar: ({ app, calendars, toggleCalendarVisibility, isCollapsed, setCollapsed, renderCalendarContextMenu, renderSidebarHeader, editingCalendarId: propEditingCalendarId, setEditingCalendarId: propSetEditingCalendarId, onCreateCalendar, }: CalendarSidebarRenderProps) => JSX.Element;
3
+ declare const DefaultCalendarSidebar: ({ app, calendars, toggleCalendarVisibility, isCollapsed, setCollapsed, renderCalendarContextMenu, renderSidebarHeader, editingCalendarId: propEditingCalendarId, setEditingCalendarId: propSetEditingCalendarId, onCreateCalendar, onSubscribeCalendar, onLoadSubscription, }: CalendarSidebarRenderProps) => JSX.Element;
4
4
  export default DefaultCalendarSidebar;
@@ -5,9 +5,9 @@ interface DeleteCalendarDialogProps {
5
5
  calendars: CalendarType[];
6
6
  step: 'initial' | 'confirm_delete';
7
7
  onStepChange: (step: 'initial' | 'confirm_delete') => void;
8
- onConfirmDelete: () => void;
8
+ onConfirmDelete: () => void | Promise<void>;
9
9
  onCancel: () => void;
10
- onMergeSelect: (targetId: string) => void;
10
+ onMergeSelect: (targetId: string) => void | Promise<void>;
11
11
  }
12
12
  export declare const DeleteCalendarDialog: ({ calendarId, calendarName, calendars, step, onStepChange, onConfirmDelete, onCancel, onMergeSelect, }: DeleteCalendarDialogProps) => import("preact").VNode<any>;
13
13
  export {};
@@ -2,7 +2,7 @@ import { CalendarType } from '@dayflow/core';
2
2
  interface ImportCalendarDialogProps {
3
3
  calendars: CalendarType[];
4
4
  filename: string;
5
- onConfirm: (targetCalendarId: string) => void;
5
+ onConfirm: (targetCalendarId: string) => void | Promise<void>;
6
6
  onCancel: () => void;
7
7
  }
8
8
  export declare const NEW_CALENDAR_ID = "new-calendar";
@@ -3,7 +3,7 @@ interface MergeCalendarDialogProps {
3
3
  sourceColor: string;
4
4
  targetName: string;
5
5
  targetColor: string;
6
- onConfirm: () => void;
6
+ onConfirm: () => void | Promise<void>;
7
7
  onCancel: () => void;
8
8
  }
9
9
  export declare const MergeCalendarDialog: ({ sourceName, sourceColor, targetName, targetColor, onConfirm, onCancel, }: MergeCalendarDialogProps) => import("preact").JSX.Element;
@@ -1,4 +1,4 @@
1
- import { CalendarPlugin, ICalendarApp, CalendarType, TNode, CreateCalendarDialogProps, SidebarHeaderSlotArgs } from '@dayflow/core';
1
+ import { CalendarPlugin, ICalendarApp, CalendarType, Event, TNode, CreateCalendarDialogProps, SidebarHeaderSlotArgs } from '@dayflow/core';
2
2
  export type { SidebarHeaderSlotArgs };
3
3
  export interface CalendarSidebarRenderProps {
4
4
  app: ICalendarApp;
@@ -14,6 +14,8 @@ export interface CalendarSidebarRenderProps {
14
14
  editingCalendarId?: string | null;
15
15
  setEditingCalendarId?: (id: string | null) => void;
16
16
  onCreateCalendar?: () => void;
17
+ onSubscribeCalendar?: (calendar: CalendarType, events: Event[]) => Promise<void>;
18
+ onLoadSubscription?: (calendar: CalendarType) => Promise<void>;
17
19
  }
18
20
  export interface SidebarPluginConfig {
19
21
  width?: number | string;
@@ -24,6 +26,8 @@ export interface SidebarPluginConfig {
24
26
  renderCalendarContextMenu?: (calendar: CalendarType, onClose: () => void) => TNode;
25
27
  renderSidebarHeader?: (args: SidebarHeaderSlotArgs) => TNode;
26
28
  renderCreateCalendarDialog?: (props: CreateCalendarDialogProps) => TNode;
29
+ onSubscribeCalendar?: (calendar: CalendarType, events: Event[]) => Promise<void>;
30
+ onLoadSubscription?: (calendar: CalendarType) => Promise<void>;
27
31
  [key: string]: unknown;
28
32
  }
29
33
  export declare function createSidebarPlugin(config?: SidebarPluginConfig): CalendarPlugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dayflow/plugin-sidebar",
3
- "version": "1.2.4",
3
+ "version": "1.2.6",
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.4"
40
+ "@dayflow/core": "3.3.6"
41
41
  },
42
42
  "peerDependencies": {
43
- "@dayflow/core": "3.3.4"
43
+ "@dayflow/core": "3.3.6"
44
44
  },
45
45
  "scripts": {
46
46
  "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && rollup -c",