@dayflow/plugin-sidebar 1.2.0 → 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.esm.js CHANGED
@@ -1,4 +1,4 @@
1
- import { useLocale, createPortal, cancelButton, ChevronsUpDown, Check, ChevronRight, sidebarHeader, sidebarHeaderToggle, PanelRightClose, PanelRightOpen, sidebarHeaderTitle, getCalendarColorsForHex, generateUniKey, downloadICS, ContentSlot, MiniCalendar, ContextMenu, ContextMenuLabel, ContextMenuItem, ContextMenuSeparator, ContextMenuColorPicker, DefaultColorPicker, BlossomColorPicker, importICSFile, sidebarContainer, normalizeCssWidth, registerSidebarImplementation, CreateCalendarDialog } from '@dayflow/core';
1
+ import { 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';
@@ -179,14 +179,14 @@ const CalendarList = ({ calendars, onToggleVisibility, onReorder, onRename, onCo
179
179
  editingId === calendar.id;
180
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-[var(--df-color-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: 'df-portal fixed inset-0 z-[9999] flex items-center justify-center bg-black/50', children: u("div", { className: 'w-full max-w-md rounded-lg bg-background p-6 shadow-xl', children: step === 'initial' ? (u(Fragment, { children: [u("h2", { className: 'text-lg font-semibold text-gray-900 dark:text-white', children: t('deleteCalendar', { calendarName }) }), u("p", { className: 'mt-3 text-sm text-gray-600 dark:text-gray-300', children: t('deleteCalendarMessage', { calendarName }) }), u("div", { className: 'mt-6 flex items-center justify-between', children: [u("div", { className: 'relative', children: [u("button", { type: 'button', onClick: () => setShowMergeDropdown(!showMergeDropdown), className: 'flex items-center gap-1 rounded-md border border-gray-300 px-4 py-2 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-slate-700', children: t('merge') }), showMergeDropdown && (u("div", { className: 'absolute top-full left-0 z-10 mt-1 max-h-60 w-max min-w-full overflow-y-auto rounded-md border border-gray-200 bg-background shadow-lg dark:border-slate-700', children: calendars
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,6 +243,7 @@ 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
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') ||
@@ -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();
@@ -302,6 +303,36 @@ 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
 
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
+
305
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();
@@ -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];
@@ -563,11 +641,12 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
563
641
  createPortal(u(ContextMenu, { x: sidebarContextMenu.x, y: sidebarContextMenu.y, onClose: handleCloseSidebarContextMenu, className: 'w-max p-2', children: [u(ContextMenuItem, { onClick: () => {
564
642
  onCreateCalendar === null || onCreateCalendar === void 0 ? void 0 : onCreateCalendar();
565
643
  handleCloseSidebarContextMenu();
566
- }, 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: () => {
567
645
  app.triggerRender();
568
646
  handleCloseSidebarContextMenu();
569
647
  }, children: t('refreshAll') || 'Refresh All' })] }), document.body), u("input", { ref: fileInputRef, type: 'file', accept: '.ics', style: { display: 'none' }, onChange: handleFileChange }), importState &&
570
- createPortal(u(ImportCalendarDialog, { calendars: calendars, filename: importState.filename, onConfirm: handleImportConfirm, onCancel: () => setImportState(null) }), document.body), mergeState &&
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 &&
571
650
  createPortal(u(MergeCalendarDialog, { sourceName: sourceCalendarName, targetName: targetCalendarName, onConfirm: handleMergeConfirm, onCancel: () => setMergeState(null) }), document.body), deleteState &&
572
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 &&
573
652
  createPortal(u("div", { className: 'fixed inset-0 z-50', onMouseDown: () => {
package/dist/index.js CHANGED
@@ -181,14 +181,14 @@ const CalendarList = ({ calendars, onToggleVisibility, onReorder, onRename, onCo
181
181
  editingId === calendar.id;
182
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-[var(--df-color-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: 'df-portal fixed inset-0 z-[9999] flex items-center justify-center bg-black/50', children: u("div", { className: 'w-full max-w-md rounded-lg bg-background p-6 shadow-xl', children: step === 'initial' ? (u(preact.Fragment, { children: [u("h2", { className: 'text-lg font-semibold text-gray-900 dark:text-white', children: t('deleteCalendar', { calendarName }) }), u("p", { className: 'mt-3 text-sm text-gray-600 dark:text-gray-300', children: t('deleteCalendarMessage', { calendarName }) }), u("div", { className: 'mt-6 flex items-center justify-between', children: [u("div", { className: 'relative', children: [u("button", { type: 'button', onClick: () => setShowMergeDropdown(!showMergeDropdown), className: 'flex items-center gap-1 rounded-md border border-gray-300 px-4 py-2 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-slate-700', children: t('merge') }), showMergeDropdown && (u("div", { className: 'absolute top-full left-0 z-10 mt-1 max-h-60 w-max min-w-full overflow-y-auto rounded-md border border-gray-200 bg-background shadow-lg dark:border-slate-700', children: calendars
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,6 +245,7 @@ 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
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') ||
@@ -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();
@@ -304,6 +305,36 @@ 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
 
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
+
307
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();
@@ -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];
@@ -565,11 +643,12 @@ const DefaultCalendarSidebar = ({ app, calendars, toggleCalendarVisibility, isCo
565
643
  core.createPortal(u(core.ContextMenu, { x: sidebarContextMenu.x, y: sidebarContextMenu.y, onClose: handleCloseSidebarContextMenu, className: 'w-max p-2', children: [u(core.ContextMenuItem, { onClick: () => {
566
644
  onCreateCalendar === null || onCreateCalendar === void 0 ? void 0 : onCreateCalendar();
567
645
  handleCloseSidebarContextMenu();
568
- }, 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: () => {
569
647
  app.triggerRender();
570
648
  handleCloseSidebarContextMenu();
571
649
  }, children: t('refreshAll') || 'Refresh All' })] }), document.body), u("input", { ref: fileInputRef, type: 'file', accept: '.ics', style: { display: 'none' }, onChange: handleFileChange }), importState &&
572
- core.createPortal(u(ImportCalendarDialog, { calendars: calendars, filename: importState.filename, onConfirm: handleImportConfirm, onCancel: () => setImportState(null) }), document.body), mergeState &&
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 &&
573
652
  core.createPortal(u(MergeCalendarDialog, { sourceName: sourceCalendarName, targetName: targetCalendarName, onConfirm: handleMergeConfirm, onCancel: () => setMergeState(null) }), document.body), deleteState &&
574
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 &&
575
654
  core.createPortal(u("div", { className: 'fixed inset-0 z-50', onMouseDown: () => {
@@ -0,0 +1,6 @@
1
+ interface SubscribeCalendarDialogProps {
2
+ onSubscribe: (url: string) => Promise<void>;
3
+ onCancel: () => void;
4
+ }
5
+ export declare const SubscribeCalendarDialog: ({ onSubscribe, onCancel, }: SubscribeCalendarDialogProps) => import("preact").JSX.Element;
6
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dayflow/plugin-sidebar",
3
- "version": "1.2.0",
3
+ "version": "1.2.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.3.0"
40
+ "@dayflow/core": "3.3.1"
41
41
  },
42
42
  "peerDependencies": {
43
- "@dayflow/core": "3.3.0"
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",