@dayflow/plugin-sidebar 1.1.3 → 1.2.1

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,5 @@
1
- import { ICalendarApp, CalendarType, TNode, CreateCalendarDialogProps, CalendarPlugin } from '@dayflow/core';
1
+ import { ICalendarApp, CalendarType, TNode, SidebarHeaderSlotArgs, CreateCalendarDialogProps, CalendarPlugin } from '@dayflow/core';
2
+ export { SidebarHeaderSlotArgs } from '@dayflow/core';
2
3
 
3
4
  interface CalendarSidebarRenderProps {
4
5
  app: ICalendarApp;
@@ -8,6 +9,7 @@ interface CalendarSidebarRenderProps {
8
9
  isCollapsed: boolean;
9
10
  setCollapsed: (collapsed: boolean) => void;
10
11
  renderCalendarContextMenu?: (calendar: CalendarType, onClose: () => void) => TNode;
12
+ renderSidebarHeader?: (args: SidebarHeaderSlotArgs) => TNode;
11
13
  createCalendarMode?: 'inline' | 'modal';
12
14
  renderCreateCalendarDialog?: (props: CreateCalendarDialogProps) => TNode;
13
15
  editingCalendarId?: string | null;
@@ -21,6 +23,7 @@ interface SidebarPluginConfig {
21
23
  createCalendarMode?: 'inline' | 'modal';
22
24
  render?: (props: CalendarSidebarRenderProps) => TNode;
23
25
  renderCalendarContextMenu?: (calendar: CalendarType, onClose: () => void) => TNode;
26
+ renderSidebarHeader?: (args: SidebarHeaderSlotArgs) => TNode;
24
27
  renderCreateCalendarDialog?: (props: CreateCalendarDialogProps) => TNode;
25
28
  [key: string]: unknown;
26
29
  }
package/dist/index.esm.js CHANGED
@@ -1,4 +1,4 @@
1
- import { useLocale, createPortal, cancelButton, ChevronsUpDown, Check, ChevronRight, sidebarHeader, sidebarHeaderToggle, PanelRightClose, PanelRightOpen, sidebarHeaderTitle, getCalendarColorsForHex, generateUniKey, downloadICS, MiniCalendar, ContextMenu, ContentSlot, ContextMenuLabel, ContextMenuItem, ContextMenuSeparator, ContextMenuColorPicker, DefaultColorPicker, BlossomColorPicker, importICSFile, sidebarContainer, normalizeCssWidth, registerSidebarImplementation, CreateCalendarDialog } from '@dayflow/core';
1
+ import { AudioLines, useLocale, createPortal, cancelButton, ChevronsUpDown, Check, ChevronRight, sidebarHeader, sidebarHeaderToggle, PanelRightClose, PanelRightOpen, sidebarHeaderTitle, getCalendarColorsForHex, generateUniKey, downloadICS, ContentSlot, MiniCalendar, ContextMenu, ContextMenuLabel, ContextMenuItem, ContextMenuSeparator, ContextMenuColorPicker, DefaultColorPicker, BlossomColorPicker, importICSFile, parseICS, sidebarContainer, normalizeCssWidth, registerSidebarImplementation, CreateCalendarDialog } from '@dayflow/core';
2
2
  import { options, Fragment, h } from 'preact';
3
3
  import { useState, useRef, useCallback, useEffect, useMemo } from 'preact/hooks';
4
4
  import { hexToHsl, lightnessToSliderValue } from '@dayflow/blossom-color-picker';
@@ -177,16 +177,16 @@ const CalendarList = ({ calendars, onToggleVisibility, onReorder, onRename, onCo
177
177
  const isDropTarget = (dropTarget === null || dropTarget === void 0 ? void 0 : dropTarget.id) === calendar.id;
178
178
  const isActive = activeContextMenuCalendarId === calendar.id ||
179
179
  editingId === calendar.id;
180
- return (u("li", { className: 'df-calendar-list-item relative', onDragOver: e => handleDragOver(e, calendar.id), onDragLeave: handleDragLeave, onDrop: () => handleDrop(calendar), onContextMenu: e => onContextMenu(e, calendar.id), children: [isDropTarget && dropTarget.position === 'top' && (u("div", { className: 'pointer-events-none absolute top-0 right-0 left-0 z-10 h-0.5 bg-primary' })), u("div", { draggable: isDraggable && !editingId, onDragStart: e => handleDragStart(calendar, e), onDragEnd: handleDragEnd, className: `rounded transition ${draggedCalendarId === calendar.id ? 'opacity-50' : ''} ${isDraggable ? 'cursor-grab' : 'cursor-default'}`, children: u("div", { className: `group flex items-center rounded px-2 py-2 transition hover:bg-gray-100 dark:hover:bg-slate-800 ${isActive ? 'bg-gray-100 dark:bg-slate-800' : ''}`, title: calendar.name, children: [u("input", { type: 'checkbox', className: 'calendar-checkbox shrink-0 cursor-pointer', style: {
180
+ return (u("li", { className: 'df-calendar-list-item relative', onDragOver: e => handleDragOver(e, calendar.id), onDragLeave: handleDragLeave, onDrop: () => handleDrop(calendar), onContextMenu: e => onContextMenu(e, calendar.id), children: [isDropTarget && dropTarget.position === 'top' && (u("div", { className: 'pointer-events-none absolute top-0 right-0 left-0 z-10 h-0.5 bg-[var(--df-color-primary)]' })), u("div", { draggable: isDraggable && !editingId, onDragStart: e => handleDragStart(calendar, e), onDragEnd: handleDragEnd, className: `rounded transition ${draggedCalendarId === calendar.id ? 'opacity-50' : ''} ${isDraggable ? 'cursor-grab' : 'cursor-default'}`, children: u("div", { className: `group flex items-center rounded px-2 py-2 transition hover:bg-gray-100 dark:hover:bg-slate-800 ${isActive ? 'bg-gray-100 dark:bg-slate-800' : ''}`, title: calendar.name, children: [u("input", { type: 'checkbox', className: 'df-calendar-checkbox shrink-0 cursor-pointer', style: {
181
181
  '--checkbox-color': calendarColor,
182
- }, checked: isVisible, onChange: event => onToggleVisibility(calendar.id, event.target.checked) }), showIcon && (u("span", { className: 'ml-2 flex h-5 w-5 shrink-0 items-center justify-center text-xs font-semibold text-white', "aria-hidden": 'true', children: getCalendarInitials(calendar) })), editingId === calendar.id ? (u("input", { ref: editInputRef, type: 'text', value: editingName, onChange: e => setEditingName(e.target.value), onBlur: handleRenameSave, onKeyDown: handleRenameKeyDown, className: 'ml-2 h-5 min-w-0 flex-1 rounded bg-white px-0 py-0 text-sm text-gray-900 focus:outline-none dark:bg-slate-700 dark:text-gray-100', onClick: e => e.stopPropagation() })) : (u("span", { className: 'ml-2 flex-1 truncate pl-1 text-sm text-gray-700 group-hover:text-gray-900 dark:text-gray-200 dark:group-hover:text-white', onDblClick: () => handleRenameStart(calendar), children: calendar.name || calendar.id }))] }) }), isDropTarget && dropTarget.position === 'bottom' && (u("div", { className: 'pointer-events-none absolute right-0 bottom-0 left-0 z-10 h-0.5 bg-primary' }))] }, calendar.id));
182
+ }, checked: isVisible, onChange: event => onToggleVisibility(calendar.id, event.target.checked) }), showIcon && (u("span", { className: 'ml-2 flex h-5 w-5 shrink-0 items-center justify-center text-xs font-semibold text-white', "aria-hidden": 'true', children: getCalendarInitials(calendar) })), editingId === calendar.id ? (u("input", { ref: editInputRef, type: 'text', value: editingName, onChange: e => setEditingName(e.target.value), onBlur: handleRenameSave, onKeyDown: handleRenameKeyDown, className: 'ml-2 h-5 min-w-0 flex-1 rounded bg-white px-0 py-0 text-sm text-gray-900 focus:outline-none dark:bg-slate-700 dark:text-gray-100', onClick: e => e.stopPropagation() })) : (u(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: () => handleRenameStart(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));
183
183
  }) }) }));
184
184
  };
185
185
 
186
186
  const DeleteCalendarDialog = ({ calendarId, calendarName, calendars, step, onStepChange, onConfirmDelete, onCancel, onMergeSelect, }) => {
187
187
  const [showMergeDropdown, setShowMergeDropdown] = useState(false);
188
188
  const { t } = useLocale();
189
- return createPortal(u("div", { className: 'fixed inset-0 z-[9999] flex items-center justify-center bg-black/50', children: u("div", { className: 'w-full max-w-md rounded-lg bg-background p-6 shadow-xl', children: step === 'initial' ? (u(Fragment, { children: [u("h2", { className: 'text-lg font-semibold text-gray-900 dark:text-white', children: t('deleteCalendar', { calendarName }) }), u("p", { className: 'mt-3 text-sm text-gray-600 dark:text-gray-300', children: t('deleteCalendarMessage', { calendarName }) }), u("div", { className: 'mt-6 flex items-center justify-between', children: [u("div", { className: 'relative', children: [u("button", { type: 'button', onClick: () => setShowMergeDropdown(!showMergeDropdown), className: 'flex items-center gap-1 rounded-md border border-gray-300 px-4 py-2 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-slate-700', children: t('merge') }), showMergeDropdown && (u("div", { className: 'absolute top-full left-0 z-10 mt-1 max-h-60 w-max min-w-full overflow-y-auto rounded-md border border-gray-200 bg-background shadow-lg dark:border-slate-700', children: calendars
189
+ return createPortal(u("div", { className: 'df-portal fixed inset-0 z-[9999] flex items-center justify-center bg-black/50', children: u("div", { className: 'w-full max-w-md rounded-lg bg-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 text-sm text-gray-600 dark:text-gray-300', children: t('deleteCalendarMessage', { calendarName }) }), u("div", { className: 'mt-6 flex items-center justify-between', children: [u("div", { className: 'relative', children: [u("button", { type: 'button', onClick: () => setShowMergeDropdown(!showMergeDropdown), className: 'flex items-center gap-1 rounded-md border border-gray-300 px-4 py-2 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-slate-700', children: t('merge') }), showMergeDropdown && (u("div", { className: 'absolute top-full left-0 z-10 mt-1 max-h-60 w-max min-w-full overflow-y-auto rounded-md border border-gray-200 bg-white shadow-lg dark:border-slate-700 dark:bg-gray-800', children: calendars
190
190
  .filter(c => c.id !== calendarId)
191
191
  .map(calendar => (u("div", { className: 'flex cursor-pointer items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-slate-700', onClick: () => {
192
192
  onMergeSelect(calendar.id);
@@ -243,9 +243,10 @@ const ImportCalendarDialog = ({ calendars, filename, onConfirm, onCancel, }) =>
243
243
  top: rect.bottom,
244
244
  left: rect.left,
245
245
  width: rect.width,
246
+ overscrollBehavior: 'none',
246
247
  }, children: u("div", { className: 'py-1', children: [calendars.map(calendar => (u("div", { className: `flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100 dark:hover:bg-slate-700 ${selectedCalendarId === calendar.id ? 'bg-primary/10' : ''}`, onClick: () => handleSelect(calendar.id), children: [u("div", { className: 'mr-3 h-3 w-3 shrink-0 rounded-sm', style: { backgroundColor: calendar.colors.lineColor } }), u("span", { className: `flex-1 truncate text-sm ${selectedCalendarId === calendar.id ? 'font-medium text-primary' : 'text-gray-700 dark:text-gray-200'}`, children: calendar.name || calendar.id }), selectedCalendarId === calendar.id && (u(Check, { className: 'ml-2 h-4 w-4 shrink-0 text-primary' }))] }, calendar.id))), u("div", { className: 'my-1 border-t border-gray-100 dark:border-slate-700' }), u("div", { className: `flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100 dark:hover:bg-slate-700 ${isNewSelected ? 'bg-primary/10' : ''}`, onClick: () => handleSelect(NEW_CALENDAR_ID), children: [u("span", { className: `flex-1 truncate text-sm ${isNewSelected ? 'font-medium text-primary' : 'pl-6 text-gray-700 dark:text-gray-200'}`, children: [t('newCalendar') || 'New Calendar', ": ", filename] }), isNewSelected && (u(Check, { className: 'ml-2 h-4 w-4 shrink-0 text-primary' }))] })] }) }), document.body);
247
248
  };
248
- return (u("div", { className: 'fixed inset-0 z-100 flex items-center justify-center bg-black/50', children: u("div", { className: 'w-full max-w-md rounded-lg bg-white p-6 shadow-xl dark:bg-slate-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') ||
249
+ 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') ||
249
250
  '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
250
251
  ? `${t('newCalendar')}: ${filename}`
251
252
  : (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' })] })] }) }));
@@ -253,7 +254,7 @@ const ImportCalendarDialog = ({ calendars, filename, onConfirm, onCancel, }) =>
253
254
 
254
255
  const MergeCalendarDialog = ({ sourceName, targetName, onConfirm, onCancel, }) => {
255
256
  const { t } = useLocale();
256
- return (u("div", { className: 'fixed inset-0 z-100 flex items-center justify-center bg-black/50', children: u("div", { className: 'w-full max-w-md rounded-lg bg-background p-6 shadow-xl', children: [u("h2", { className: 'text-lg font-semibold text-gray-900 dark:text-white', children: t('mergeConfirmTitle', { sourceName, targetName }) }), u("p", { className: 'mt-3 text-sm text-gray-600 dark:text-gray-300', children: t('mergeConfirmMessage', { sourceName, targetName }) }), u("div", { className: 'mt-6 flex justify-end gap-3', children: [u("button", { type: 'button', onClick: onCancel, className: cancelButton, children: t('cancel') }), u("button", { type: 'button', onClick: onConfirm, className: 'rounded-md bg-destructive px-3 py-2 text-xs font-medium text-destructive-foreground hover:bg-destructive/90', children: t('merge') })] })] }) }));
257
+ 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-md 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("p", { className: 'mt-3 text-sm text-gray-600 dark:text-gray-300', children: t('mergeConfirmMessage', { sourceName, targetName }) }), u("div", { className: 'mt-6 flex justify-end gap-3', children: [u("button", { type: 'button', onClick: onCancel, className: cancelButton, children: t('cancel') }), u("button", { type: 'button', onClick: onConfirm, className: 'rounded-md bg-destructive px-3 py-2 text-xs font-medium text-destructive-foreground hover:bg-destructive/90', children: t('merge') })] })] }) }));
257
258
  };
258
259
 
259
260
  const stopPropagation = (e) => e.stopPropagation();
@@ -290,8 +291,8 @@ const MergeMenuItem = ({ calendars, currentCalendarId, onMergeSelect, }) => {
290
291
  const availableCalendars = calendars.filter(c => c.id !== currentCalendarId);
291
292
  if (availableCalendars.length === 0)
292
293
  return null;
293
- return (u(Fragment, { children: [u("div", { ref: itemRef, className: 'relative flex cursor-default items-center justify-between rounded-sm px-3 py-0.5 text-[12px] transition-colors outline-none select-none hover:bg-primary hover:text-white dark:text-slate-200 dark:hover:bg-primary dark:hover:text-white', onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: [u("span", { children: t('merge') }), u(ChevronRight, { className: 'h-4 w-4' })] }), isHovered &&
294
- createPortal(u("div", { ref: submenuRef, "data-submenu-content": 'true', className: 'animate-in fade-in-0 zoom-in-95 fixed z-60 min-w-48 overflow-hidden rounded-md border border-slate-200 bg-white p-1 shadow-md duration-100 dark:border-slate-800 dark:bg-slate-950', style: { top: position.y, left: position.x }, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onMouseDown: e => e.stopPropagation(), children: availableCalendars.map(calendar => (u("div", { className: 'flex cursor-pointer items-center rounded-sm px-3 py-1 text-[12px] text-slate-900 transition-colors hover:bg-primary hover:text-white dark:text-slate-50 dark:hover:bg-primary dark:hover:text-white', onClick: e => {
294
+ return (u(Fragment, { children: [u("div", { ref: itemRef, className: 'relative flex cursor-default items-center justify-between rounded-sm px-3 py-0.5 text-[12px] text-[var(--df-color-foreground)] transition-colors outline-none select-none hover:bg-[var(--df-color-primary)] hover:text-[var(--df-color-primary-foreground)]', onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: [u("span", { children: t('merge') }), u(ChevronRight, { className: 'h-4 w-4' })] }), isHovered &&
295
+ createPortal(u("div", { ref: submenuRef, "data-submenu-content": 'true', className: 'df-portal df-animate-in df-fade-in df-zoom-in-95 fixed z-60 min-w-48 overflow-hidden rounded-md border border-slate-200 bg-white p-1 shadow-md duration-100 dark:border-slate-800 dark:bg-slate-950', style: { top: position.y, left: position.x }, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onMouseDown: e => e.stopPropagation(), children: availableCalendars.map(calendar => (u("div", { className: 'flex cursor-pointer items-center rounded-sm px-3 py-1 text-[12px] text-[var(--df-color-foreground)] transition-colors hover:bg-[var(--df-color-primary)] hover:text-[var(--df-color-primary-foreground)]', onClick: e => {
295
296
  e.stopPropagation();
296
297
  onMergeSelect(calendar.id);
297
298
  }, children: [u("div", { className: 'mr-2 h-3 w-3 shrink-0 rounded-sm', style: { backgroundColor: calendar.colors.lineColor } }), u("span", { className: 'truncate', children: calendar.name || calendar.id })] }, calendar.id))) }), document.body)] }));
@@ -302,7 +303,37 @@ const SidebarHeader = ({ isCollapsed, onCollapseToggle, }) => {
302
303
  return (u("div", { className: sidebarHeader, children: [u("button", { type: 'button', "aria-label": isCollapsed ? t('expandSidebar') : t('collapseSidebar'), className: sidebarHeaderToggle, onClick: onCollapseToggle, children: isCollapsed ? (u(PanelRightClose, { className: 'h-4 w-4 text-gray-500 dark:text-gray-400' })) : (u(PanelRightOpen, { className: 'h-4 w-4 text-gray-500 dark:text-gray-400' })) }), !isCollapsed && (u("div", { className: 'ml-3 flex flex-1 items-center justify-between', children: u("span", { className: sidebarHeaderTitle, children: t('calendars') }) }))] }));
303
304
  };
304
305
 
305
- const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCollapsed, setCollapsed, renderCalendarContextMenu, editingCalendarId: propEditingCalendarId, setEditingCalendarId: propSetEditingCalendarId, onCreateCalendar, }) => {
306
+ const SubscribeCalendarDialog = ({ onSubscribe, onCancel, }) => {
307
+ const { t } = useLocale();
308
+ const [url, setUrl] = useState('');
309
+ const [loading, setLoading] = useState(false);
310
+ const [error, setError] = useState(null);
311
+ const handleSubmit = () => __awaiter(void 0, void 0, void 0, function* () {
312
+ const trimmed = url.trim();
313
+ if (!trimmed)
314
+ return;
315
+ setError(null);
316
+ setLoading(true);
317
+ try {
318
+ yield onSubscribe(trimmed);
319
+ }
320
+ catch (_a) {
321
+ setError(t('subscribeError'));
322
+ }
323
+ finally {
324
+ setLoading(false);
325
+ }
326
+ });
327
+ const handleKeyDown = (e) => {
328
+ if (e.key === 'Enter')
329
+ handleSubmit();
330
+ if (e.key === 'Escape')
331
+ onCancel();
332
+ };
333
+ 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') })] })] }) }));
334
+ };
335
+
336
+ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCollapsed, setCollapsed, renderCalendarContextMenu, renderSidebarHeader, editingCalendarId: propEditingCalendarId, setEditingCalendarId: propSetEditingCalendarId, onCreateCalendar, }) => {
306
337
  var _a, _b, _c, _d;
307
338
  const { t } = useLocale();
308
339
  // Detect if custom color picker slot is provided
@@ -331,6 +362,8 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
331
362
  const [deleteState, setDeleteState] = useState(null);
332
363
  // Import Calendar State
333
364
  const [importState, setImportState] = useState(null);
365
+ // Subscribe Calendar State
366
+ const [subscribeDialogOpen, setSubscribeDialogOpen] = useState(false);
334
367
  const handleContextMenu = useCallback((e, calendarId) => {
335
368
  e.preventDefault();
336
369
  e.stopPropagation(); // Stop propagation to prevent sidebar context menu
@@ -445,6 +478,51 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
445
478
  (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click();
446
479
  handleCloseSidebarContextMenu();
447
480
  }, [handleCloseSidebarContextMenu]);
481
+ // Subscribe Calendar handler
482
+ const handleSubscribeClick = useCallback(() => {
483
+ setSubscribeDialogOpen(true);
484
+ handleCloseSidebarContextMenu();
485
+ }, [handleCloseSidebarContextMenu]);
486
+ const handleSubscribeConfirm = useCallback((url) => __awaiter(void 0, void 0, void 0, function* () {
487
+ const response = yield fetch(url);
488
+ if (!response.ok)
489
+ throw new Error(`HTTP ${response.status}`);
490
+ const icsContent = yield response.text();
491
+ const result = parseICS(icsContent);
492
+ // Extract calendar name from X-WR-CALNAME if present
493
+ const nameMatch = icsContent.match(/X-WR-CALNAME[^:]*:([^\r\n]+)/);
494
+ const calendarName = nameMatch
495
+ ? nameMatch[1].trim()
496
+ : new URL(url).hostname;
497
+ const presetColors = [
498
+ '#3b82f6',
499
+ '#10b981',
500
+ '#8b5cf6',
501
+ '#f59e0b',
502
+ '#ef4444',
503
+ '#f97316',
504
+ '#ec4899',
505
+ '#14b8a6',
506
+ '#6366f1',
507
+ '#6b7280',
508
+ ];
509
+ const randomColor = presetColors[Math.floor(Math.random() * presetColors.length)];
510
+ const { colors: calendarColors, darkColors } = getCalendarColorsForHex(randomColor);
511
+ const calendarId = generateUniKey();
512
+ app.createCalendar({
513
+ id: calendarId,
514
+ name: calendarName,
515
+ isDefault: false,
516
+ colors: calendarColors,
517
+ darkColors,
518
+ isVisible: true,
519
+ subscribed: true,
520
+ });
521
+ result.events.forEach(event => {
522
+ app.addEvent(Object.assign(Object.assign({}, event), { calendarId }));
523
+ });
524
+ setSubscribeDialogOpen(false);
525
+ }), [app]);
448
526
  const handleFileChange = useCallback((e) => __awaiter(void 0, void 0, void 0, function* () {
449
527
  var _a;
450
528
  const file = (_a = e.currentTarget.files) === null || _a === void 0 ? void 0 : _a[0];
@@ -526,7 +604,13 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
526
604
  const readOnlyConfig = app.getReadOnlyConfig();
527
605
  const isEditable = !app.state.readOnly;
528
606
  const isDraggable = readOnlyConfig.draggable !== false;
529
- return (u("div", { className: sidebarContainer, onContextMenu: isEditable ? handleSidebarContextMenu : undefined, children: [u(SidebarHeader, { isCollapsed: isCollapsed, onCollapseToggle: () => setCollapsed(!isCollapsed) }), isCollapsed ? (u(CalendarList, { calendars: calendars, onToggleVisibility: toggleCalendarVisibility, onReorder: isDraggable
607
+ return (u("div", { className: sidebarContainer, onContextMenu: isEditable ? handleSidebarContextMenu : undefined, children: [u(ContentSlot, { generatorName: 'sidebarHeader', generatorArgs: {
608
+ isCollapsed,
609
+ onCollapseToggle: () => setCollapsed(!isCollapsed),
610
+ }, defaultContent: renderSidebarHeader ? (renderSidebarHeader({
611
+ isCollapsed,
612
+ onCollapseToggle: () => setCollapsed(!isCollapsed),
613
+ })) : (u(SidebarHeader, { isCollapsed: isCollapsed, onCollapseToggle: () => setCollapsed(!isCollapsed) })) }), isCollapsed ? (u(CalendarList, { calendars: calendars, onToggleVisibility: toggleCalendarVisibility, onReorder: isDraggable
530
614
  ? app.reorderCalendars
531
615
  : () => {
532
616
  /* noop */
@@ -557,11 +641,12 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
557
641
  createPortal(u(ContextMenu, { x: sidebarContextMenu.x, y: sidebarContextMenu.y, onClose: handleCloseSidebarContextMenu, className: 'w-max p-2', children: [u(ContextMenuItem, { onClick: () => {
558
642
  onCreateCalendar === null || onCreateCalendar === void 0 ? void 0 : onCreateCalendar();
559
643
  handleCloseSidebarContextMenu();
560
- }, children: t('newCalendar') || 'New Calendar' }), u(ContextMenuItem, { onClick: handleImportClick, children: t('importCalendar') || 'Import Calendar' }), u(ContextMenuItem, { onClick: () => {
644
+ }, 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: () => {
561
645
  app.triggerRender();
562
646
  handleCloseSidebarContextMenu();
563
647
  }, children: t('refreshAll') || 'Refresh All' })] }), document.body), u("input", { ref: fileInputRef, type: 'file', accept: '.ics', style: { display: 'none' }, onChange: handleFileChange }), importState &&
564
- createPortal(u(ImportCalendarDialog, { calendars: calendars, filename: importState.filename, onConfirm: handleImportConfirm, onCancel: () => setImportState(null) }), document.body), mergeState &&
648
+ createPortal(u(ImportCalendarDialog, { calendars: calendars, filename: importState.filename, onConfirm: handleImportConfirm, onCancel: () => setImportState(null) }), document.body), subscribeDialogOpen &&
649
+ createPortal(u(SubscribeCalendarDialog, { onSubscribe: handleSubscribeConfirm, onCancel: () => setSubscribeDialogOpen(false) }), document.body), mergeState &&
565
650
  createPortal(u(MergeCalendarDialog, { sourceName: sourceCalendarName, targetName: targetCalendarName, onConfirm: handleMergeConfirm, onCancel: () => setMergeState(null) }), document.body), deleteState &&
566
651
  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 &&
567
652
  createPortal(u("div", { className: 'fixed inset-0 z-50', onMouseDown: () => {
@@ -589,7 +674,7 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
589
674
  darkColors,
590
675
  });
591
676
  },
592
- }, defaultContent: u("div", { className: 'rounded-lg border border-gray-200 bg-white p-3 shadow-xl dark:border-gray-700 dark:bg-slate-900', children: u(DefaultColorPicker, { color: customColorPicker.currentColor, onChange: (color, isPending) => {
677
+ }, defaultContent: u("div", { className: 'rounded-lg border border-gray-200 bg-white p-3 shadow-xl dark:border-gray-700 dark:bg-gray-900', children: u(DefaultColorPicker, { color: customColorPicker.currentColor, onChange: (color, isPending) => {
593
678
  setCustomColorPicker(prev => prev ? Object.assign(Object.assign({}, prev), { currentColor: color.hex }) : null);
594
679
  const { colors, darkColors } = getCalendarColorsForHex(color.hex);
595
680
  app.updateCalendar(customColorPicker.calendarId, {
@@ -678,6 +763,7 @@ function createSidebarPlugin(config = {}) {
678
763
  isCollapsed,
679
764
  setCollapsed: setIsCollapsed,
680
765
  renderCalendarContextMenu: config.renderCalendarContextMenu,
766
+ renderSidebarHeader: config.renderSidebarHeader,
681
767
  createCalendarMode: config.createCalendarMode,
682
768
  renderCreateCalendarDialog: config.renderCreateCalendarDialog,
683
769
  editingCalendarId,
package/dist/index.js CHANGED
@@ -179,16 +179,16 @@ const CalendarList = ({ calendars, onToggleVisibility, onReorder, onRename, onCo
179
179
  const isDropTarget = (dropTarget === null || dropTarget === void 0 ? void 0 : dropTarget.id) === calendar.id;
180
180
  const isActive = activeContextMenuCalendarId === calendar.id ||
181
181
  editingId === calendar.id;
182
- return (u("li", { className: 'df-calendar-list-item relative', onDragOver: e => handleDragOver(e, calendar.id), onDragLeave: handleDragLeave, onDrop: () => handleDrop(calendar), onContextMenu: e => onContextMenu(e, calendar.id), children: [isDropTarget && dropTarget.position === 'top' && (u("div", { className: 'pointer-events-none absolute top-0 right-0 left-0 z-10 h-0.5 bg-primary' })), u("div", { draggable: isDraggable && !editingId, onDragStart: e => handleDragStart(calendar, e), onDragEnd: handleDragEnd, className: `rounded transition ${draggedCalendarId === calendar.id ? 'opacity-50' : ''} ${isDraggable ? 'cursor-grab' : 'cursor-default'}`, children: u("div", { className: `group flex items-center rounded px-2 py-2 transition hover:bg-gray-100 dark:hover:bg-slate-800 ${isActive ? 'bg-gray-100 dark:bg-slate-800' : ''}`, title: calendar.name, children: [u("input", { type: 'checkbox', className: 'calendar-checkbox shrink-0 cursor-pointer', style: {
182
+ return (u("li", { className: 'df-calendar-list-item relative', onDragOver: e => handleDragOver(e, calendar.id), onDragLeave: handleDragLeave, onDrop: () => handleDrop(calendar), onContextMenu: e => onContextMenu(e, calendar.id), children: [isDropTarget && dropTarget.position === 'top' && (u("div", { className: 'pointer-events-none absolute top-0 right-0 left-0 z-10 h-0.5 bg-[var(--df-color-primary)]' })), u("div", { draggable: isDraggable && !editingId, onDragStart: e => handleDragStart(calendar, e), onDragEnd: handleDragEnd, className: `rounded transition ${draggedCalendarId === calendar.id ? 'opacity-50' : ''} ${isDraggable ? 'cursor-grab' : 'cursor-default'}`, children: u("div", { className: `group flex items-center rounded px-2 py-2 transition hover:bg-gray-100 dark:hover:bg-slate-800 ${isActive ? 'bg-gray-100 dark:bg-slate-800' : ''}`, title: calendar.name, children: [u("input", { type: 'checkbox', className: 'df-calendar-checkbox shrink-0 cursor-pointer', style: {
183
183
  '--checkbox-color': calendarColor,
184
- }, checked: isVisible, onChange: event => onToggleVisibility(calendar.id, event.target.checked) }), showIcon && (u("span", { className: 'ml-2 flex h-5 w-5 shrink-0 items-center justify-center text-xs font-semibold text-white', "aria-hidden": 'true', children: getCalendarInitials(calendar) })), editingId === calendar.id ? (u("input", { ref: editInputRef, type: 'text', value: editingName, onChange: e => setEditingName(e.target.value), onBlur: handleRenameSave, onKeyDown: handleRenameKeyDown, className: 'ml-2 h-5 min-w-0 flex-1 rounded bg-white px-0 py-0 text-sm text-gray-900 focus:outline-none dark:bg-slate-700 dark:text-gray-100', onClick: e => e.stopPropagation() })) : (u("span", { className: 'ml-2 flex-1 truncate pl-1 text-sm text-gray-700 group-hover:text-gray-900 dark:text-gray-200 dark:group-hover:text-white', onDblClick: () => handleRenameStart(calendar), children: calendar.name || calendar.id }))] }) }), isDropTarget && dropTarget.position === 'bottom' && (u("div", { className: 'pointer-events-none absolute right-0 bottom-0 left-0 z-10 h-0.5 bg-primary' }))] }, calendar.id));
184
+ }, checked: isVisible, onChange: event => onToggleVisibility(calendar.id, event.target.checked) }), showIcon && (u("span", { className: 'ml-2 flex h-5 w-5 shrink-0 items-center justify-center text-xs font-semibold text-white', "aria-hidden": 'true', children: getCalendarInitials(calendar) })), editingId === calendar.id ? (u("input", { ref: editInputRef, type: 'text', value: editingName, onChange: e => setEditingName(e.target.value), onBlur: handleRenameSave, onKeyDown: handleRenameKeyDown, className: 'ml-2 h-5 min-w-0 flex-1 rounded bg-white px-0 py-0 text-sm text-gray-900 focus:outline-none dark:bg-slate-700 dark:text-gray-100', onClick: e => e.stopPropagation() })) : (u(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: () => handleRenameStart(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));
185
185
  }) }) }));
186
186
  };
187
187
 
188
188
  const DeleteCalendarDialog = ({ calendarId, calendarName, calendars, step, onStepChange, onConfirmDelete, onCancel, onMergeSelect, }) => {
189
189
  const [showMergeDropdown, setShowMergeDropdown] = hooks.useState(false);
190
190
  const { t } = core.useLocale();
191
- return core.createPortal(u("div", { className: 'fixed inset-0 z-[9999] flex items-center justify-center bg-black/50', children: u("div", { className: 'w-full max-w-md rounded-lg bg-background p-6 shadow-xl', children: step === 'initial' ? (u(preact.Fragment, { children: [u("h2", { className: 'text-lg font-semibold text-gray-900 dark:text-white', children: t('deleteCalendar', { calendarName }) }), u("p", { className: 'mt-3 text-sm text-gray-600 dark:text-gray-300', children: t('deleteCalendarMessage', { calendarName }) }), u("div", { className: 'mt-6 flex items-center justify-between', children: [u("div", { className: 'relative', children: [u("button", { type: 'button', onClick: () => setShowMergeDropdown(!showMergeDropdown), className: 'flex items-center gap-1 rounded-md border border-gray-300 px-4 py-2 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-slate-700', children: t('merge') }), showMergeDropdown && (u("div", { className: 'absolute top-full left-0 z-10 mt-1 max-h-60 w-max min-w-full overflow-y-auto rounded-md border border-gray-200 bg-background shadow-lg dark:border-slate-700', children: calendars
191
+ return core.createPortal(u("div", { className: 'df-portal fixed inset-0 z-[9999] flex items-center justify-center bg-black/50', children: u("div", { className: 'w-full max-w-md rounded-lg bg-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 text-sm text-gray-600 dark:text-gray-300', children: t('deleteCalendarMessage', { calendarName }) }), u("div", { className: 'mt-6 flex items-center justify-between', children: [u("div", { className: 'relative', children: [u("button", { type: 'button', onClick: () => setShowMergeDropdown(!showMergeDropdown), className: 'flex items-center gap-1 rounded-md border border-gray-300 px-4 py-2 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-slate-700', children: t('merge') }), showMergeDropdown && (u("div", { className: 'absolute top-full left-0 z-10 mt-1 max-h-60 w-max min-w-full overflow-y-auto rounded-md border border-gray-200 bg-white shadow-lg dark:border-slate-700 dark:bg-gray-800', children: calendars
192
192
  .filter(c => c.id !== calendarId)
193
193
  .map(calendar => (u("div", { className: 'flex cursor-pointer items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-slate-700', onClick: () => {
194
194
  onMergeSelect(calendar.id);
@@ -245,9 +245,10 @@ const ImportCalendarDialog = ({ calendars, filename, onConfirm, onCancel, }) =>
245
245
  top: rect.bottom,
246
246
  left: rect.left,
247
247
  width: rect.width,
248
+ overscrollBehavior: 'none',
248
249
  }, children: u("div", { className: 'py-1', children: [calendars.map(calendar => (u("div", { className: `flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100 dark:hover:bg-slate-700 ${selectedCalendarId === calendar.id ? 'bg-primary/10' : ''}`, onClick: () => handleSelect(calendar.id), children: [u("div", { className: 'mr-3 h-3 w-3 shrink-0 rounded-sm', style: { backgroundColor: calendar.colors.lineColor } }), u("span", { className: `flex-1 truncate text-sm ${selectedCalendarId === calendar.id ? 'font-medium text-primary' : 'text-gray-700 dark:text-gray-200'}`, children: calendar.name || calendar.id }), selectedCalendarId === calendar.id && (u(core.Check, { className: 'ml-2 h-4 w-4 shrink-0 text-primary' }))] }, calendar.id))), u("div", { className: 'my-1 border-t border-gray-100 dark:border-slate-700' }), u("div", { className: `flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100 dark:hover:bg-slate-700 ${isNewSelected ? 'bg-primary/10' : ''}`, onClick: () => handleSelect(NEW_CALENDAR_ID), children: [u("span", { className: `flex-1 truncate text-sm ${isNewSelected ? 'font-medium text-primary' : 'pl-6 text-gray-700 dark:text-gray-200'}`, children: [t('newCalendar') || 'New Calendar', ": ", filename] }), isNewSelected && (u(core.Check, { className: 'ml-2 h-4 w-4 shrink-0 text-primary' }))] })] }) }), document.body);
249
250
  };
250
- return (u("div", { className: 'fixed inset-0 z-100 flex items-center justify-center bg-black/50', children: u("div", { className: 'w-full max-w-md rounded-lg bg-white p-6 shadow-xl dark:bg-slate-900', children: [u("h2", { className: 'mb-4 text-lg font-semibold text-gray-900 dark:text-white', children: t('addSchedule') || 'Add Schedule' }), u("p", { className: 'mb-4 text-sm text-gray-600 dark:text-gray-300', children: t('importCalendarMessage') ||
251
+ return (u("div", { className: 'df-portal fixed inset-0 z-100 flex items-center justify-center bg-black/50', children: u("div", { className: 'w-full max-w-md rounded-lg bg-white p-6 shadow-xl dark:bg-gray-900', children: [u("h2", { className: 'mb-4 text-lg font-semibold text-gray-900 dark:text-white', children: t('addSchedule') || 'Add Schedule' }), u("p", { className: 'mb-4 text-sm text-gray-600 dark:text-gray-300', children: t('importCalendarMessage') ||
251
252
  '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
252
253
  ? `${t('newCalendar')}: ${filename}`
253
254
  : (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' })] })] }) }));
@@ -255,7 +256,7 @@ const ImportCalendarDialog = ({ calendars, filename, onConfirm, onCancel, }) =>
255
256
 
256
257
  const MergeCalendarDialog = ({ sourceName, targetName, onConfirm, onCancel, }) => {
257
258
  const { t } = core.useLocale();
258
- return (u("div", { className: 'fixed inset-0 z-100 flex items-center justify-center bg-black/50', children: u("div", { className: 'w-full max-w-md rounded-lg bg-background p-6 shadow-xl', children: [u("h2", { className: 'text-lg font-semibold text-gray-900 dark:text-white', children: t('mergeConfirmTitle', { sourceName, targetName }) }), u("p", { className: 'mt-3 text-sm text-gray-600 dark:text-gray-300', children: t('mergeConfirmMessage', { sourceName, targetName }) }), u("div", { className: 'mt-6 flex justify-end gap-3', children: [u("button", { type: 'button', onClick: onCancel, className: core.cancelButton, children: t('cancel') }), u("button", { type: 'button', onClick: onConfirm, className: 'rounded-md bg-destructive px-3 py-2 text-xs font-medium text-destructive-foreground hover:bg-destructive/90', children: t('merge') })] })] }) }));
259
+ 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-md 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("p", { className: 'mt-3 text-sm text-gray-600 dark:text-gray-300', children: t('mergeConfirmMessage', { sourceName, targetName }) }), u("div", { className: 'mt-6 flex justify-end gap-3', children: [u("button", { type: 'button', onClick: onCancel, className: core.cancelButton, children: t('cancel') }), u("button", { type: 'button', onClick: onConfirm, className: 'rounded-md bg-destructive px-3 py-2 text-xs font-medium text-destructive-foreground hover:bg-destructive/90', children: t('merge') })] })] }) }));
259
260
  };
260
261
 
261
262
  const stopPropagation = (e) => e.stopPropagation();
@@ -292,8 +293,8 @@ const MergeMenuItem = ({ calendars, currentCalendarId, onMergeSelect, }) => {
292
293
  const availableCalendars = calendars.filter(c => c.id !== currentCalendarId);
293
294
  if (availableCalendars.length === 0)
294
295
  return null;
295
- return (u(preact.Fragment, { children: [u("div", { ref: itemRef, className: 'relative flex cursor-default items-center justify-between rounded-sm px-3 py-0.5 text-[12px] transition-colors outline-none select-none hover:bg-primary hover:text-white dark:text-slate-200 dark:hover:bg-primary dark:hover:text-white', onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: [u("span", { children: t('merge') }), u(core.ChevronRight, { className: 'h-4 w-4' })] }), isHovered &&
296
- core.createPortal(u("div", { ref: submenuRef, "data-submenu-content": 'true', className: 'animate-in fade-in-0 zoom-in-95 fixed z-60 min-w-48 overflow-hidden rounded-md border border-slate-200 bg-white p-1 shadow-md duration-100 dark:border-slate-800 dark:bg-slate-950', style: { top: position.y, left: position.x }, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onMouseDown: e => e.stopPropagation(), children: availableCalendars.map(calendar => (u("div", { className: 'flex cursor-pointer items-center rounded-sm px-3 py-1 text-[12px] text-slate-900 transition-colors hover:bg-primary hover:text-white dark:text-slate-50 dark:hover:bg-primary dark:hover:text-white', onClick: e => {
296
+ return (u(preact.Fragment, { children: [u("div", { ref: itemRef, className: 'relative flex cursor-default items-center justify-between rounded-sm px-3 py-0.5 text-[12px] text-[var(--df-color-foreground)] transition-colors outline-none select-none hover:bg-[var(--df-color-primary)] hover:text-[var(--df-color-primary-foreground)]', onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: [u("span", { children: t('merge') }), u(core.ChevronRight, { className: 'h-4 w-4' })] }), isHovered &&
297
+ core.createPortal(u("div", { ref: submenuRef, "data-submenu-content": 'true', className: 'df-portal df-animate-in df-fade-in df-zoom-in-95 fixed z-60 min-w-48 overflow-hidden rounded-md border border-slate-200 bg-white p-1 shadow-md duration-100 dark:border-slate-800 dark:bg-slate-950', style: { top: position.y, left: position.x }, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onMouseDown: e => e.stopPropagation(), children: availableCalendars.map(calendar => (u("div", { className: 'flex cursor-pointer items-center rounded-sm px-3 py-1 text-[12px] text-[var(--df-color-foreground)] transition-colors hover:bg-[var(--df-color-primary)] hover:text-[var(--df-color-primary-foreground)]', onClick: e => {
297
298
  e.stopPropagation();
298
299
  onMergeSelect(calendar.id);
299
300
  }, children: [u("div", { className: 'mr-2 h-3 w-3 shrink-0 rounded-sm', style: { backgroundColor: calendar.colors.lineColor } }), u("span", { className: 'truncate', children: calendar.name || calendar.id })] }, calendar.id))) }), document.body)] }));
@@ -304,7 +305,37 @@ const SidebarHeader = ({ isCollapsed, onCollapseToggle, }) => {
304
305
  return (u("div", { className: core.sidebarHeader, children: [u("button", { type: 'button', "aria-label": isCollapsed ? t('expandSidebar') : t('collapseSidebar'), className: core.sidebarHeaderToggle, onClick: onCollapseToggle, children: isCollapsed ? (u(core.PanelRightClose, { className: 'h-4 w-4 text-gray-500 dark:text-gray-400' })) : (u(core.PanelRightOpen, { className: 'h-4 w-4 text-gray-500 dark:text-gray-400' })) }), !isCollapsed && (u("div", { className: 'ml-3 flex flex-1 items-center justify-between', children: u("span", { className: core.sidebarHeaderTitle, children: t('calendars') }) }))] }));
305
306
  };
306
307
 
307
- const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCollapsed, setCollapsed, renderCalendarContextMenu, editingCalendarId: propEditingCalendarId, setEditingCalendarId: propSetEditingCalendarId, onCreateCalendar, }) => {
308
+ const SubscribeCalendarDialog = ({ onSubscribe, onCancel, }) => {
309
+ const { t } = core.useLocale();
310
+ const [url, setUrl] = hooks.useState('');
311
+ const [loading, setLoading] = hooks.useState(false);
312
+ const [error, setError] = hooks.useState(null);
313
+ const handleSubmit = () => __awaiter(void 0, void 0, void 0, function* () {
314
+ const trimmed = url.trim();
315
+ if (!trimmed)
316
+ return;
317
+ setError(null);
318
+ setLoading(true);
319
+ try {
320
+ yield onSubscribe(trimmed);
321
+ }
322
+ catch (_a) {
323
+ setError(t('subscribeError'));
324
+ }
325
+ finally {
326
+ setLoading(false);
327
+ }
328
+ });
329
+ const handleKeyDown = (e) => {
330
+ if (e.key === 'Enter')
331
+ handleSubmit();
332
+ if (e.key === 'Escape')
333
+ onCancel();
334
+ };
335
+ 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') })] })] }) }));
336
+ };
337
+
338
+ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCollapsed, setCollapsed, renderCalendarContextMenu, renderSidebarHeader, editingCalendarId: propEditingCalendarId, setEditingCalendarId: propSetEditingCalendarId, onCreateCalendar, }) => {
308
339
  var _a, _b, _c, _d;
309
340
  const { t } = core.useLocale();
310
341
  // Detect if custom color picker slot is provided
@@ -333,6 +364,8 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
333
364
  const [deleteState, setDeleteState] = hooks.useState(null);
334
365
  // Import Calendar State
335
366
  const [importState, setImportState] = hooks.useState(null);
367
+ // Subscribe Calendar State
368
+ const [subscribeDialogOpen, setSubscribeDialogOpen] = hooks.useState(false);
336
369
  const handleContextMenu = hooks.useCallback((e, calendarId) => {
337
370
  e.preventDefault();
338
371
  e.stopPropagation(); // Stop propagation to prevent sidebar context menu
@@ -447,6 +480,51 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
447
480
  (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click();
448
481
  handleCloseSidebarContextMenu();
449
482
  }, [handleCloseSidebarContextMenu]);
483
+ // Subscribe Calendar handler
484
+ const handleSubscribeClick = hooks.useCallback(() => {
485
+ setSubscribeDialogOpen(true);
486
+ handleCloseSidebarContextMenu();
487
+ }, [handleCloseSidebarContextMenu]);
488
+ const handleSubscribeConfirm = hooks.useCallback((url) => __awaiter(void 0, void 0, void 0, function* () {
489
+ const response = yield fetch(url);
490
+ if (!response.ok)
491
+ throw new Error(`HTTP ${response.status}`);
492
+ const icsContent = yield response.text();
493
+ const result = core.parseICS(icsContent);
494
+ // Extract calendar name from X-WR-CALNAME if present
495
+ const nameMatch = icsContent.match(/X-WR-CALNAME[^:]*:([^\r\n]+)/);
496
+ const calendarName = nameMatch
497
+ ? nameMatch[1].trim()
498
+ : new URL(url).hostname;
499
+ const presetColors = [
500
+ '#3b82f6',
501
+ '#10b981',
502
+ '#8b5cf6',
503
+ '#f59e0b',
504
+ '#ef4444',
505
+ '#f97316',
506
+ '#ec4899',
507
+ '#14b8a6',
508
+ '#6366f1',
509
+ '#6b7280',
510
+ ];
511
+ const randomColor = presetColors[Math.floor(Math.random() * presetColors.length)];
512
+ const { colors: calendarColors, darkColors } = core.getCalendarColorsForHex(randomColor);
513
+ const calendarId = core.generateUniKey();
514
+ app.createCalendar({
515
+ id: calendarId,
516
+ name: calendarName,
517
+ isDefault: false,
518
+ colors: calendarColors,
519
+ darkColors,
520
+ isVisible: true,
521
+ subscribed: true,
522
+ });
523
+ result.events.forEach(event => {
524
+ app.addEvent(Object.assign(Object.assign({}, event), { calendarId }));
525
+ });
526
+ setSubscribeDialogOpen(false);
527
+ }), [app]);
450
528
  const handleFileChange = hooks.useCallback((e) => __awaiter(void 0, void 0, void 0, function* () {
451
529
  var _a;
452
530
  const file = (_a = e.currentTarget.files) === null || _a === void 0 ? void 0 : _a[0];
@@ -528,7 +606,13 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
528
606
  const readOnlyConfig = app.getReadOnlyConfig();
529
607
  const isEditable = !app.state.readOnly;
530
608
  const isDraggable = readOnlyConfig.draggable !== false;
531
- return (u("div", { className: core.sidebarContainer, onContextMenu: isEditable ? handleSidebarContextMenu : undefined, children: [u(SidebarHeader, { isCollapsed: isCollapsed, onCollapseToggle: () => setCollapsed(!isCollapsed) }), isCollapsed ? (u(CalendarList, { calendars: calendars, onToggleVisibility: toggleCalendarVisibility, onReorder: isDraggable
609
+ return (u("div", { className: core.sidebarContainer, onContextMenu: isEditable ? handleSidebarContextMenu : undefined, children: [u(core.ContentSlot, { generatorName: 'sidebarHeader', generatorArgs: {
610
+ isCollapsed,
611
+ onCollapseToggle: () => setCollapsed(!isCollapsed),
612
+ }, defaultContent: renderSidebarHeader ? (renderSidebarHeader({
613
+ isCollapsed,
614
+ onCollapseToggle: () => setCollapsed(!isCollapsed),
615
+ })) : (u(SidebarHeader, { isCollapsed: isCollapsed, onCollapseToggle: () => setCollapsed(!isCollapsed) })) }), isCollapsed ? (u(CalendarList, { calendars: calendars, onToggleVisibility: toggleCalendarVisibility, onReorder: isDraggable
532
616
  ? app.reorderCalendars
533
617
  : () => {
534
618
  /* noop */
@@ -559,11 +643,12 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
559
643
  core.createPortal(u(core.ContextMenu, { x: sidebarContextMenu.x, y: sidebarContextMenu.y, onClose: handleCloseSidebarContextMenu, className: 'w-max p-2', children: [u(core.ContextMenuItem, { onClick: () => {
560
644
  onCreateCalendar === null || onCreateCalendar === void 0 ? void 0 : onCreateCalendar();
561
645
  handleCloseSidebarContextMenu();
562
- }, children: t('newCalendar') || 'New Calendar' }), u(core.ContextMenuItem, { onClick: handleImportClick, children: t('importCalendar') || 'Import Calendar' }), u(core.ContextMenuItem, { onClick: () => {
646
+ }, 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: () => {
563
647
  app.triggerRender();
564
648
  handleCloseSidebarContextMenu();
565
649
  }, children: t('refreshAll') || 'Refresh All' })] }), document.body), u("input", { ref: fileInputRef, type: 'file', accept: '.ics', style: { display: 'none' }, onChange: handleFileChange }), importState &&
566
- core.createPortal(u(ImportCalendarDialog, { calendars: calendars, filename: importState.filename, onConfirm: handleImportConfirm, onCancel: () => setImportState(null) }), document.body), mergeState &&
650
+ core.createPortal(u(ImportCalendarDialog, { calendars: calendars, filename: importState.filename, onConfirm: handleImportConfirm, onCancel: () => setImportState(null) }), document.body), subscribeDialogOpen &&
651
+ core.createPortal(u(SubscribeCalendarDialog, { onSubscribe: handleSubscribeConfirm, onCancel: () => setSubscribeDialogOpen(false) }), document.body), mergeState &&
567
652
  core.createPortal(u(MergeCalendarDialog, { sourceName: sourceCalendarName, targetName: targetCalendarName, onConfirm: handleMergeConfirm, onCancel: () => setMergeState(null) }), document.body), deleteState &&
568
653
  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 &&
569
654
  core.createPortal(u("div", { className: 'fixed inset-0 z-50', onMouseDown: () => {
@@ -591,7 +676,7 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
591
676
  darkColors,
592
677
  });
593
678
  },
594
- }, defaultContent: u("div", { className: 'rounded-lg border border-gray-200 bg-white p-3 shadow-xl dark:border-gray-700 dark:bg-slate-900', children: u(core.DefaultColorPicker, { color: customColorPicker.currentColor, onChange: (color, isPending) => {
679
+ }, defaultContent: u("div", { className: 'rounded-lg border border-gray-200 bg-white p-3 shadow-xl dark:border-gray-700 dark:bg-gray-900', children: u(core.DefaultColorPicker, { color: customColorPicker.currentColor, onChange: (color, isPending) => {
595
680
  setCustomColorPicker(prev => prev ? Object.assign(Object.assign({}, prev), { currentColor: color.hex }) : null);
596
681
  const { colors, darkColors } = core.getCalendarColorsForHex(color.hex);
597
682
  app.updateCalendar(customColorPicker.calendarId, {
@@ -680,6 +765,7 @@ function createSidebarPlugin(config = {}) {
680
765
  isCollapsed,
681
766
  setCollapsed: setIsCollapsed,
682
767
  renderCalendarContextMenu: config.renderCalendarContextMenu,
768
+ renderSidebarHeader: config.renderSidebarHeader,
683
769
  createCalendarMode: config.createCalendarMode,
684
770
  renderCreateCalendarDialog: config.renderCreateCalendarDialog,
685
771
  editingCalendarId,
@@ -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, editingCalendarId: propEditingCalendarId, setEditingCalendarId: propSetEditingCalendarId, onCreateCalendar, }: CalendarSidebarRenderProps) => JSX.Element;
3
+ declare const DefaultCalendarSidebar: ({ app, calendars, toggleCalendarVisibility, isCollapsed, setCollapsed, renderCalendarContextMenu, renderSidebarHeader, editingCalendarId: propEditingCalendarId, setEditingCalendarId: propSetEditingCalendarId, onCreateCalendar, }: CalendarSidebarRenderProps) => JSX.Element;
4
4
  export default DefaultCalendarSidebar;
@@ -0,0 +1,6 @@
1
+ interface SubscribeCalendarDialogProps {
2
+ onSubscribe: (url: string) => Promise<void>;
3
+ onCancel: () => void;
4
+ }
5
+ export declare const SubscribeCalendarDialog: ({ onSubscribe, onCancel, }: SubscribeCalendarDialogProps) => import("preact").JSX.Element;
6
+ export {};
@@ -1,4 +1,5 @@
1
- import { CalendarPlugin, ICalendarApp, CalendarType, TNode, CreateCalendarDialogProps } from '@dayflow/core';
1
+ import { CalendarPlugin, ICalendarApp, CalendarType, TNode, CreateCalendarDialogProps, SidebarHeaderSlotArgs } from '@dayflow/core';
2
+ export type { SidebarHeaderSlotArgs };
2
3
  export interface CalendarSidebarRenderProps {
3
4
  app: ICalendarApp;
4
5
  calendars: CalendarType[];
@@ -7,6 +8,7 @@ export interface CalendarSidebarRenderProps {
7
8
  isCollapsed: boolean;
8
9
  setCollapsed: (collapsed: boolean) => void;
9
10
  renderCalendarContextMenu?: (calendar: CalendarType, onClose: () => void) => TNode;
11
+ renderSidebarHeader?: (args: SidebarHeaderSlotArgs) => TNode;
10
12
  createCalendarMode?: 'inline' | 'modal';
11
13
  renderCreateCalendarDialog?: (props: CreateCalendarDialogProps) => TNode;
12
14
  editingCalendarId?: string | null;
@@ -20,6 +22,7 @@ export interface SidebarPluginConfig {
20
22
  createCalendarMode?: 'inline' | 'modal';
21
23
  render?: (props: CalendarSidebarRenderProps) => TNode;
22
24
  renderCalendarContextMenu?: (calendar: CalendarType, onClose: () => void) => TNode;
25
+ renderSidebarHeader?: (args: SidebarHeaderSlotArgs) => TNode;
23
26
  renderCreateCalendarDialog?: (props: CreateCalendarDialogProps) => TNode;
24
27
  [key: string]: unknown;
25
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dayflow/plugin-sidebar",
3
- "version": "1.1.3",
3
+ "version": "1.2.1",
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.2.3"
40
+ "@dayflow/core": "3.3.1"
41
41
  },
42
42
  "peerDependencies": {
43
- "@dayflow/core": "3.2.3"
43
+ "@dayflow/core": "3.3.1"
44
44
  },
45
45
  "scripts": {
46
46
  "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && rollup -c",