@luckydye/calendar 1.3.1 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luckydye/calendar",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "author": "Tim Havlicek",
5
5
  "contributors": [],
6
6
  "description": "",
@@ -74,13 +74,18 @@ export class CalDAVSource implements CalendarSource {
74
74
  * Make an authenticated request to the CalDAV server.
75
75
  */
76
76
  async request(url: string, method: string, body?: string, headers: Record<string, string> = {}): Promise<Response> {
77
+ const requestHeaders: Record<string, string> = {
78
+ 'Authorization': this.getAuthHeader(),
79
+ ...headers,
80
+ };
81
+
82
+ if (body && !Object.keys(requestHeaders).some((key) => key.toLowerCase() === 'content-type')) {
83
+ requestHeaders['Content-Type'] = 'application/xml; charset=utf-8';
84
+ }
85
+
77
86
  const response = await fetch(url, {
78
87
  method,
79
- headers: {
80
- 'Authorization': this.getAuthHeader(),
81
- 'Content-Type': 'application/xml; charset=utf-8',
82
- ...headers,
83
- },
88
+ headers: requestHeaders,
84
89
  body,
85
90
  });
86
91
 
@@ -242,7 +247,7 @@ export class CalDAVSource implements CalendarSource {
242
247
  /**
243
248
  * Fetch calendar objects (events) from a specific calendar.
244
249
  */
245
- private async fetchCalendarObjects(calendarUrl: string): Promise<string[]> {
250
+ private async fetchCalendarObjects(calendarUrl: string): Promise<Array<{ href: string; icalData: string }>> {
246
251
  const response = await this.request(
247
252
  calendarUrl,
248
253
  'REPORT',
@@ -263,11 +268,17 @@ export class CalDAVSource implements CalendarSource {
263
268
 
264
269
  const text = await response.text();
265
270
 
266
- const calendarData: string[] = [];
267
- const calendarDataRegex = /<[^:]+:calendar-data[^>]*>(.*?)<\/[^:]+:calendar-data>/gs;
271
+ const calendarObjects: Array<{ href: string; icalData: string }> = [];
272
+ const responseRegex = /<[^:]+:response[^>]*>([\s\S]*?)<\/[^:]+:response>/g;
273
+
274
+ for (const match of text.matchAll(responseRegex)) {
275
+ const responseBlock = match[1];
276
+ const hrefMatch = responseBlock.match(/<[^:]+:href[^>]*>(.*?)<\/[^:]+:href>/);
277
+ const calendarDataMatch = responseBlock.match(/<[^:]+:calendar-data[^>]*>([\s\S]*?)<\/[^:]+:calendar-data>/);
278
+ if (!hrefMatch || !calendarDataMatch) continue;
268
279
 
269
- for (const match of text.matchAll(calendarDataRegex)) {
270
- let icalData = match[1].trim();
280
+ const href = new URL(hrefMatch[1], this.credentials.serverUrl).toString();
281
+ let icalData = calendarDataMatch[1].trim();
271
282
  // Unescape XML entities
272
283
  icalData = icalData
273
284
  .replace(/&lt;/g, '<')
@@ -277,11 +288,11 @@ export class CalDAVSource implements CalendarSource {
277
288
  .replace(/&apos;/g, "'");
278
289
 
279
290
  if (icalData) {
280
- calendarData.push(icalData);
291
+ calendarObjects.push({ href, icalData });
281
292
  }
282
293
  }
283
294
 
284
- return calendarData;
295
+ return calendarObjects;
285
296
  }
286
297
 
287
298
 
@@ -366,7 +377,13 @@ export class CalDAVSource implements CalendarSource {
366
377
  /**
367
378
  * Map a parsed VCALENDAR event to CalendarEvent.
368
379
  */
369
- private mapToCalendarEvent(parsed: Partial<CalendarEvent>, calendarDisplayName: string, calendarUrl: string, calendarColor?: string): CalendarEvent | null {
380
+ private mapToCalendarEvent(
381
+ parsed: Partial<CalendarEvent>,
382
+ calendarDisplayName: string,
383
+ calendarUrl: string,
384
+ calendarColor?: string,
385
+ resourceUrl?: string
386
+ ): CalendarEvent | null {
370
387
  if (!parsed.start || !parsed.id) return null;
371
388
 
372
389
  return {
@@ -387,6 +404,7 @@ export class CalDAVSource implements CalendarSource {
387
404
  readOnly: false,
388
405
  isAllDay: parsed.isAllDay,
389
406
  reminders: parsed.reminders,
407
+ resourceUrl,
390
408
  };
391
409
  }
392
410
 
@@ -395,9 +413,21 @@ export class CalDAVSource implements CalendarSource {
395
413
  */
396
414
  async fetchEventsForCalendar(calendarUrl: string, displayName: string, color?: string): Promise<CalendarEvent[]> {
397
415
  const calendarObjects = await this.fetchCalendarObjects(calendarUrl);
398
- return calendarObjects
399
- .map(ical => this.mapToCalendarEvent(parseSingleICalEvent(ical), displayName, calendarUrl, color))
400
- .filter((e): e is CalendarEvent => e !== null);
416
+ const events: CalendarEvent[] = [];
417
+
418
+ for (const calendarObject of calendarObjects) {
419
+ const event = this.mapToCalendarEvent(
420
+ parseSingleICalEvent(calendarObject.icalData),
421
+ displayName,
422
+ calendarUrl,
423
+ color,
424
+ calendarObject.href
425
+ );
426
+ if (!event) continue;
427
+ events.push(event);
428
+ }
429
+
430
+ return events;
401
431
  }
402
432
 
403
433
  /**
@@ -440,6 +470,7 @@ export class CalDAVSource implements CalendarSource {
440
470
  await this.request(eventUrl, 'PUT', icalData, {
441
471
  'Content-Type': 'text/calendar; charset=utf-8',
442
472
  });
473
+ fullEvent.resourceUrl = eventUrl;
443
474
 
444
475
  return fullEvent;
445
476
  }
@@ -448,10 +479,11 @@ export class CalDAVSource implements CalendarSource {
448
479
  * Update an existing event on the CalDAV server.
449
480
  * Derives the calendar URL from updates.calendarId, falling back to the event cache.
450
481
  */
451
- async updateEvent(id: string, updates: Partial<CalendarEvent>): Promise<CalendarEvent> {
452
- const calendarUrl = updates.calendarId ?? this.eventCalendarMap.get(id);
482
+ async updateEvent(event: CalendarEvent, updates: Partial<CalendarEvent>): Promise<CalendarEvent> {
483
+ const id = event.id;
484
+ const calendarUrl = updates.calendarId ?? event.calendarId;
453
485
  if (!calendarUrl) throw new Error(`Cannot update event ${id}: calendar URL unknown`);
454
- const eventUrl = `${calendarUrl.replace(/\/$/, '')}/${id}.ics`;
486
+ const eventUrl = updates.resourceUrl ?? event.resourceUrl ?? `${calendarUrl.replace(/\/$/, '')}/${id}.ics`;
455
487
 
456
488
  // For updates, we need to fetch the existing event first
457
489
  // This is a simplified implementation
@@ -480,6 +512,7 @@ export class CalDAVSource implements CalendarSource {
480
512
  readOnly: false,
481
513
  isAllDay: updates.isAllDay !== undefined ? updates.isAllDay : existing.isAllDay,
482
514
  reminders: updates.reminders !== undefined ? updates.reminders : existing.reminders,
515
+ resourceUrl: updates.resourceUrl ?? eventUrl,
483
516
  };
484
517
 
485
518
  const icalData = this.serializeEventToICal(updatedEvent);
@@ -11,7 +11,7 @@ export interface Calendar {
11
11
  calendarUrl?: string;
12
12
  fetchEvents(): Promise<CalendarEvent[]>;
13
13
  createEvent?(event: Omit<CalendarEvent, 'calendar' | 'color'>): Promise<CalendarEvent>;
14
- updateEvent?(id: string, updates: Partial<CalendarEvent>): Promise<CalendarEvent>;
14
+ updateEvent?(event: CalendarEvent, updates: Partial<CalendarEvent>): Promise<CalendarEvent>;
15
15
  deleteEvent?(id: string): Promise<void>;
16
16
  }
17
17
 
@@ -28,7 +28,7 @@ export interface CalendarSource {
28
28
  enabled: boolean;
29
29
  fetchEvents(): Promise<CalendarEvent[]>;
30
30
  createEvent?(event: Omit<CalendarEvent, 'calendar' | 'color'>): Promise<CalendarEvent>;
31
- updateEvent?(id: string, updates: Partial<CalendarEvent>): Promise<CalendarEvent>;
31
+ updateEvent?(event: CalendarEvent, updates: Partial<CalendarEvent>): Promise<CalendarEvent>;
32
32
  deleteEvent?(id: string): Promise<void>;
33
33
  }
34
34
 
@@ -50,6 +50,7 @@ export interface CalendarEvent {
50
50
  reminders?: NotificationConfig[];
51
51
  isAllDay?: boolean;
52
52
  visualStyle?: 'heatmap';
53
+ resourceUrl?: string;
53
54
  }
54
55
 
55
56
  export interface WeekInfo {
@@ -18,6 +18,7 @@ import { serializeEventsToICal } from "./ICal.js";
18
18
  import { queueStatus } from "./StatusMessage.js";
19
19
  import type { StatusBarData } from "./StatusBar.js";
20
20
  import { IndexedDBStorage } from "./IndexedDBStorage.js";
21
+ import { sanitizeEventDescription } from "./DescriptionSanitizer.js";
21
22
  import { TIME_SCALE_DAY_HEIGHT, type CalendarLayer, type LayerContext } from "./CalendarLayer.js";
22
23
  import { createGridLayer } from "./layers/GridLayer.js";
23
24
  import { createEventsLayer, type EventRect, type EventsState } from "./layers/EventsLayer.js";
@@ -259,7 +260,7 @@ export class CalendarViewElement extends LitElement {
259
260
  left: 60px;
260
261
  right: 12px;
261
262
  pointer-events: none;
262
- z-index: 2;
263
+ z-index: 101;
263
264
  overflow: hidden;
264
265
  }
265
266
 
@@ -300,7 +301,7 @@ export class CalendarViewElement extends LitElement {
300
301
  flex-direction: row;
301
302
  backdrop-filter: blur(10px);
302
303
  box-shadow: var(--shadow-overlay, 0 4px 12px rgba(0, 0, 0, 0.9));
303
- z-index: 100;
304
+ z-index: 102;
304
305
  }
305
306
 
306
307
  .event-detail-header {
@@ -679,6 +680,19 @@ export class CalendarViewElement extends LitElement {
679
680
  text-align: left;
680
681
  }
681
682
 
683
+ .description-actions {
684
+ display: flex;
685
+ gap: 12px;
686
+ align-items: center;
687
+ flex-wrap: wrap;
688
+ }
689
+
690
+ .description-actions .description-see-more {
691
+ display: inline-block;
692
+ margin-top: 8px;
693
+ padding: 0;
694
+ }
695
+
682
696
  .description-see-more:hover {
683
697
  color: var(--accent-hover, rgb(120, 170, 255));
684
698
  }
@@ -992,6 +1006,11 @@ export class CalendarViewElement extends LitElement {
992
1006
  height: number;
993
1007
  } | null = null;
994
1008
  isDescriptionExpanded = false;
1009
+ descriptionSummaryTargetKey: string | null = null;
1010
+ descriptionSummaryText = "";
1011
+ descriptionSummaryLoading = false;
1012
+ descriptionSummaryError: string | null = null;
1013
+ private requestedDescriptionSummaryKey: string | null = null;
995
1014
 
996
1015
  notificationPopoverOpen = false;
997
1016
  scheduledNotifications: any[] = [];
@@ -1359,6 +1378,97 @@ export class CalendarViewElement extends LitElement {
1359
1378
  this.repositionEventDetailOverlay();
1360
1379
  }
1361
1380
 
1381
+ clearDescriptionSummary(): void {
1382
+ if (this.requestedDescriptionSummaryKey) {
1383
+ this.dispatchEvent(
1384
+ new CustomEvent("cancel-description-summary", {
1385
+ detail: { key: this.requestedDescriptionSummaryKey },
1386
+ bubbles: true,
1387
+ composed: true,
1388
+ }),
1389
+ );
1390
+ }
1391
+ this.requestedDescriptionSummaryKey = null;
1392
+ this.descriptionSummaryTargetKey = null;
1393
+ this.descriptionSummaryText = "";
1394
+ this.descriptionSummaryLoading = false;
1395
+ this.descriptionSummaryError = null;
1396
+ this.requestUpdate();
1397
+ }
1398
+
1399
+ private getDescriptionSummaryTargetKey(event: CalendarEvent): string {
1400
+ return `${event.id}:${sanitizeEventDescription(event.description ?? "")}`;
1401
+ }
1402
+
1403
+ private requestDescriptionSummary(event: CalendarEvent): void {
1404
+ const description = sanitizeEventDescription(event.description ?? "").trim();
1405
+ if (description.length <= 200) return;
1406
+
1407
+ const targetKey = this.getDescriptionSummaryTargetKey(event);
1408
+ this.clearDescriptionSummary();
1409
+ this.requestedDescriptionSummaryKey = targetKey;
1410
+ this.descriptionSummaryTargetKey = targetKey;
1411
+ this.descriptionSummaryText = "";
1412
+ this.descriptionSummaryLoading = true;
1413
+ this.descriptionSummaryError = null;
1414
+ this.dispatchEvent(
1415
+ new CustomEvent("request-description-summary", {
1416
+ detail: {
1417
+ event,
1418
+ key: targetKey,
1419
+ description,
1420
+ },
1421
+ bubbles: true,
1422
+ composed: true,
1423
+ }),
1424
+ );
1425
+ this.requestUpdate();
1426
+ }
1427
+
1428
+ startDescriptionSummary(key: string): void {
1429
+ if (key !== this.descriptionSummaryTargetKey) return;
1430
+ this.descriptionSummaryText = "";
1431
+ this.descriptionSummaryLoading = true;
1432
+ this.descriptionSummaryError = null;
1433
+ this.requestUpdate();
1434
+ }
1435
+
1436
+ appendDescriptionSummaryChunk(key: string, chunk: string): void {
1437
+ if (key !== this.descriptionSummaryTargetKey) return;
1438
+ if (!chunk) return;
1439
+ this.descriptionSummaryText += chunk;
1440
+ this.descriptionSummaryLoading = true;
1441
+ this.requestUpdate();
1442
+ }
1443
+
1444
+ finishDescriptionSummary(key: string, error: string | null = null): void {
1445
+ if (key !== this.descriptionSummaryTargetKey) return;
1446
+ this.descriptionSummaryLoading = false;
1447
+ this.descriptionSummaryError = error;
1448
+ this.requestUpdate();
1449
+ }
1450
+
1451
+ failDescriptionSummary(key: string, message: string): void {
1452
+ if (key !== this.descriptionSummaryTargetKey) return;
1453
+ this.descriptionSummaryLoading = false;
1454
+ this.descriptionSummaryError = message;
1455
+ if (!this.descriptionSummaryText && this.selectedEventForDetail?.description) {
1456
+ this.descriptionSummaryText = this.selectedEventForDetail.description.replaceAll(
1457
+ "\\n",
1458
+ "\n",
1459
+ );
1460
+ }
1461
+ this.requestUpdate();
1462
+ }
1463
+
1464
+ cancelDescriptionSummary(key: string): void {
1465
+ if (key !== this.descriptionSummaryTargetKey) {
1466
+ return;
1467
+ }
1468
+ this.descriptionSummaryLoading = false;
1469
+ this.requestUpdate();
1470
+ }
1471
+
1362
1472
  repositionEventDetailOverlay(): void {
1363
1473
  if (!this.selectedEventForDetail || !this.selectedEventRect) return;
1364
1474
 
@@ -2254,6 +2364,9 @@ export class CalendarViewElement extends LitElement {
2254
2364
  this.resizingOriginalStart = null;
2255
2365
  this.resizingOriginalEnd = null;
2256
2366
  this.isResizingEvent = false;
2367
+ if (this.scrollContainer) {
2368
+ this.scrollContainer.style.cursor = "";
2369
+ }
2257
2370
  this.renderCanvas();
2258
2371
  this.requestUpdate();
2259
2372
  }
@@ -2273,6 +2386,7 @@ export class CalendarViewElement extends LitElement {
2273
2386
  // Close event detail overlay
2274
2387
  this.selectedEventForDetail = null;
2275
2388
  this.selectedEventRect = null;
2389
+ this.clearDescriptionSummary();
2276
2390
 
2277
2391
  if (hadSelection) {
2278
2392
  this.dispatchEvent(
@@ -2323,6 +2437,7 @@ export class CalendarViewElement extends LitElement {
2323
2437
  this.internal.clearSelection();
2324
2438
  this.selectedEventForDetail = null;
2325
2439
  this.selectedEventRect = null;
2440
+ this.clearDescriptionSummary();
2326
2441
  this.dispatchEvent(
2327
2442
  new CustomEvent("selection-change", {
2328
2443
  detail: { selectedEvents: [] },
@@ -2355,6 +2470,7 @@ export class CalendarViewElement extends LitElement {
2355
2470
  this.selectedEventForDetail = null;
2356
2471
  this.selectedEventRect = null;
2357
2472
  this.isDescriptionExpanded = false;
2473
+ this.clearDescriptionSummary();
2358
2474
  this.renderCanvas();
2359
2475
  this.requestUpdate();
2360
2476
  this.dispatchEvent(
@@ -2379,7 +2495,8 @@ export class CalendarViewElement extends LitElement {
2379
2495
  // Check for resize handles first — suppressed when alt key is active
2380
2496
  const resizeHandle = !e.altKey ? this.getResizeHandle(x, y) : null;
2381
2497
  if (resizeHandle && !this.isResizingEvent && !this.isDraggingEvent) {
2382
- this.scrollContainer.style.cursor = "ns-resize";
2498
+ this.scrollContainer.style.cursor =
2499
+ this.dayHeight >= TIME_SCALE_DAY_HEIGHT ? "ns-resize" : "ew-resize";
2383
2500
  } else if (!this.isResizingEvent && !this.isDraggingEvent) {
2384
2501
  this.scrollContainer.style.cursor = "";
2385
2502
  }
@@ -2518,11 +2635,10 @@ export class CalendarViewElement extends LitElement {
2518
2635
  } else {
2519
2636
  // Move event
2520
2637
  this.dispatchEvent(
2521
- new CustomEvent("move-event", {
2638
+ new CustomEvent("update-event", {
2522
2639
  detail: {
2523
2640
  event: this.movingEvent,
2524
- start: newStart,
2525
- end: newEnd,
2641
+ updates: { start: newStart, end: newEnd },
2526
2642
  },
2527
2643
  bubbles: true,
2528
2644
  }),
@@ -2682,6 +2798,7 @@ export class CalendarViewElement extends LitElement {
2682
2798
  y: number,
2683
2799
  ): { event: CalendarEvent; edge: "start" | "end" } | null {
2684
2800
  const RESIZE_HANDLE_SIZE = 8; // pixels from edge to detect resize
2801
+ const isZoomedIn = this.dayHeight >= TIME_SCALE_DAY_HEIGHT;
2685
2802
 
2686
2803
  // Check in reverse order (top to bottom rendering)
2687
2804
  for (let i = this.eventRects.length - 1; i >= 0; i--) {
@@ -2691,18 +2808,33 @@ export class CalendarViewElement extends LitElement {
2691
2808
  // Skip read-only events
2692
2809
  if (rect.event.readOnly) continue;
2693
2810
 
2694
- // Check if within horizontal bounds
2695
- if (x < rect.x || x > rect.x + rect.width) continue;
2811
+ if (isZoomedIn) {
2812
+ // In time-scale view, resize vertically from the top/bottom edges.
2813
+ if (x < rect.x || x > rect.x + rect.width) continue;
2696
2814
 
2697
- // Check top edge (resize start)
2698
- if (y >= rect.y && y <= rect.y + RESIZE_HANDLE_SIZE) {
2815
+ if (y >= rect.y && y <= rect.y + RESIZE_HANDLE_SIZE) {
2816
+ return { event: rect.event, edge: "start" };
2817
+ }
2818
+
2819
+ if (
2820
+ y >= rect.y + rect.height - RESIZE_HANDLE_SIZE &&
2821
+ y <= rect.y + rect.height
2822
+ ) {
2823
+ return { event: rect.event, edge: "end" };
2824
+ }
2825
+ continue;
2826
+ }
2827
+
2828
+ // In zoomed-out view, resize horizontally from the left/right edges.
2829
+ if (y < rect.y || y > rect.y + rect.height) continue;
2830
+
2831
+ if (x >= rect.x && x <= rect.x + RESIZE_HANDLE_SIZE) {
2699
2832
  return { event: rect.event, edge: "start" };
2700
2833
  }
2701
2834
 
2702
- // Check bottom edge (resize end)
2703
2835
  if (
2704
- y >= rect.y + rect.height - RESIZE_HANDLE_SIZE &&
2705
- y <= rect.y + rect.height
2836
+ x >= rect.x + rect.width - RESIZE_HANDLE_SIZE &&
2837
+ x <= rect.x + rect.width
2706
2838
  ) {
2707
2839
  return { event: rect.event, edge: "end" };
2708
2840
  }
@@ -2742,6 +2874,8 @@ export class CalendarViewElement extends LitElement {
2742
2874
  this.resizingOriginalStart = new Date(resizeHandle.event.start);
2743
2875
  this.resizingOriginalEnd = new Date(resizeHandle.event.end);
2744
2876
  this.isResizingEvent = false; // Will be set to true on first mouse move
2877
+ this.scrollContainer.style.cursor =
2878
+ this.dayHeight >= TIME_SCALE_DAY_HEIGHT ? "ns-resize" : "ew-resize";
2745
2879
  return;
2746
2880
  }
2747
2881
 
@@ -2764,6 +2898,7 @@ export class CalendarViewElement extends LitElement {
2764
2898
  // Close event detail overlay
2765
2899
  this.selectedEventForDetail = null;
2766
2900
  this.selectedEventRect = null;
2901
+ this.clearDescriptionSummary();
2767
2902
 
2768
2903
  this.movingEvent = clickedEvent;
2769
2904
  this.movingEventOrigin = { x, y };
@@ -2779,6 +2914,7 @@ export class CalendarViewElement extends LitElement {
2779
2914
  // Close event detail overlay when clicking outside of events
2780
2915
  this.selectedEventForDetail = null;
2781
2916
  this.selectedEventRect = null;
2917
+ this.clearDescriptionSummary();
2782
2918
 
2783
2919
  // Clear selection when clicking on empty space
2784
2920
  const hadSelection = this.internal.getSelectedEvents().length > 0;
@@ -2986,6 +3122,7 @@ export class CalendarViewElement extends LitElement {
2986
3122
  // Close event detail overlay when replacing selection
2987
3123
  this.selectedEventForDetail = null;
2988
3124
  this.selectedEventRect = null;
3125
+ this.clearDescriptionSummary();
2989
3126
  }
2990
3127
  for (const event of selectedEvents) {
2991
3128
  this.internal.selectEvent(event, "add");
@@ -3195,6 +3332,7 @@ export class CalendarViewElement extends LitElement {
3195
3332
 
3196
3333
  // Show event detail overlay for single selection
3197
3334
  if (!isCmdOrCtrl) {
3335
+ this.clearDescriptionSummary();
3198
3336
  this.selectedEventForDetail = event;
3199
3337
  this.isDescriptionExpanded = false;
3200
3338
  // Find the event's rect and store it for positioning
@@ -3214,6 +3352,7 @@ export class CalendarViewElement extends LitElement {
3214
3352
  this.selectedEventForDetail = null;
3215
3353
  this.selectedEventRect = null;
3216
3354
  this.isDescriptionExpanded = false;
3355
+ this.clearDescriptionSummary();
3217
3356
  this.requestUpdate();
3218
3357
  }
3219
3358
 
@@ -3275,6 +3414,7 @@ export class CalendarViewElement extends LitElement {
3275
3414
  // Close event detail overlay when clearing selection
3276
3415
  this.selectedEventForDetail = null;
3277
3416
  this.selectedEventRect = null;
3417
+ this.clearDescriptionSummary();
3278
3418
 
3279
3419
  if (hadSelection) {
3280
3420
  this.dispatchEvent(
@@ -4122,6 +4262,7 @@ export class CalendarViewElement extends LitElement {
4122
4262
 
4123
4263
  this.selectedEventForDetail = null;
4124
4264
  this.selectedEventRect = null;
4265
+ this.clearDescriptionSummary();
4125
4266
  this.requestUpdate();
4126
4267
  }
4127
4268
 
@@ -4261,6 +4402,31 @@ export class CalendarViewElement extends LitElement {
4261
4402
  }
4262
4403
 
4263
4404
  const event = this.selectedEventForDetail;
4405
+ const eventDescription = sanitizeEventDescription(event.description ?? "");
4406
+ const eventDescriptionText = eventDescription.replaceAll("\\n", "\n");
4407
+ const useDescriptionSummary = eventDescription.trim().length > 200;
4408
+ const currentSummaryKey = this.getDescriptionSummaryTargetKey(event);
4409
+ const hasRequestedSummaryForCurrentEvent =
4410
+ this.requestedDescriptionSummaryKey === currentSummaryKey;
4411
+ const isSummaryForCurrentEvent =
4412
+ this.descriptionSummaryTargetKey === currentSummaryKey;
4413
+ const hasSummaryStateForCurrentEvent =
4414
+ hasRequestedSummaryForCurrentEvent || isSummaryForCurrentEvent;
4415
+ const streamedSummaryText = hasSummaryStateForCurrentEvent
4416
+ ? this.descriptionSummaryText
4417
+ : "";
4418
+ const isSummaryLoading =
4419
+ useDescriptionSummary && hasSummaryStateForCurrentEvent && this.descriptionSummaryLoading;
4420
+ const shouldRenderSummary =
4421
+ useDescriptionSummary && hasSummaryStateForCurrentEvent;
4422
+ const descriptionToRender = shouldRenderSummary
4423
+ ? streamedSummaryText || (isSummaryLoading ? "Generating summary..." : eventDescriptionText)
4424
+ : eventDescriptionText;
4425
+ const summarizeButtonLabel = isSummaryLoading
4426
+ ? "Summarizing..."
4427
+ : shouldRenderSummary
4428
+ ? "Summarize again"
4429
+ : "Summarize";
4264
4430
 
4265
4431
  const formatDate = (date: Date) => {
4266
4432
  return new Intl.DateTimeFormat(this.locale, {
@@ -4399,6 +4565,7 @@ export class CalendarViewElement extends LitElement {
4399
4565
  this.selectedEventForDetail = null;
4400
4566
  this.selectedEventRect = null;
4401
4567
  this.isDescriptionExpanded = false;
4568
+ this.clearDescriptionSummary();
4402
4569
  this.requestUpdate();
4403
4570
  }}
4404
4571
  >×</button>
@@ -4406,7 +4573,7 @@ export class CalendarViewElement extends LitElement {
4406
4573
 
4407
4574
  <div class="event-detail-body">
4408
4575
  ${(() => {
4409
- const teamsMatch = event.description?.match(/https:\/\/teams\.microsoft\.com\/[^\s<>"]+/);
4576
+ const teamsMatch = eventDescription?.match(/https:\/\/teams\.microsoft\.com\/[^\s<>"]+/);
4410
4577
  const teamsUrl = teamsMatch ? teamsMatch[0] : null;
4411
4578
  if (!event.location && !teamsUrl) return null;
4412
4579
  return html`
@@ -4509,47 +4676,84 @@ export class CalendarViewElement extends LitElement {
4509
4676
  }
4510
4677
 
4511
4678
  ${
4512
- event.readOnly ||
4513
- (event.organizer != null &&
4514
- !this.currentUserEmails.has(event.organizer.email))
4515
- ? event.description
4679
+ useDescriptionSummary
4516
4680
  ? html`
4517
4681
  <div class="event-detail-section">
4518
4682
  <div class="event-detail-label">Description</div>
4519
4683
  <div class="event-detail-description ${
4520
- this.isDescriptionExpanded ? "expanded" : ""
4684
+ shouldRenderSummary || this.isDescriptionExpanded
4685
+ ? "expanded"
4686
+ : ""
4521
4687
  }">
4522
- <pre class="event-detail-value">${event.description.replaceAll(
4523
- "\\n",
4524
- "\n",
4525
- )}</pre>
4688
+ <pre class="event-detail-value">${descriptionToRender}</pre>
4689
+ </div>
4690
+ ${
4691
+ shouldRenderSummary && this.descriptionSummaryError
4692
+ ? html`<div class="event-detail-value" style="opacity: 0.7;">Summary unavailable: ${this.descriptionSummaryError}. Showing original description.</div>`
4693
+ : null
4694
+ }
4695
+ <div class="description-actions">
4696
+ ${
4697
+ !shouldRenderSummary
4698
+ ? html`<button
4699
+ class="description-see-more"
4700
+ @click=${() => {
4701
+ this.isDescriptionExpanded = !this.isDescriptionExpanded;
4702
+ this.requestUpdate();
4703
+ }}
4704
+ >
4705
+ ${this.isDescriptionExpanded ? "Show less" : "Show more"}
4706
+ </button>`
4707
+ : null
4708
+ }
4709
+ <button
4710
+ class="description-see-more"
4711
+ ?disabled=${isSummaryLoading}
4712
+ @click=${() => this.requestDescriptionSummary(event)}
4713
+ >
4714
+ ${summarizeButtonLabel}
4715
+ </button>
4716
+ </div>
4717
+ </div>
4718
+ `
4719
+ : event.readOnly ||
4720
+ (event.organizer != null &&
4721
+ !this.currentUserEmails.has(event.organizer.email))
4722
+ ? eventDescription
4723
+ ? html`
4724
+ <div class="event-detail-section">
4725
+ <div class="event-detail-label">Description</div>
4726
+ <div class="event-detail-description ${
4727
+ this.isDescriptionExpanded ? "expanded" : ""
4728
+ }">
4729
+ <pre class="event-detail-value">${descriptionToRender}</pre>
4526
4730
  </div>
4527
4731
  ${
4528
- event.description.length > 300 ||
4529
- event.description.split("\n").length > 8
4530
- ? html`
4732
+ eventDescription.length > 300 ||
4733
+ eventDescription.split("\n").length > 8
4734
+ ? html`
4531
4735
  <button
4532
4736
  class="description-see-more"
4533
4737
  @click=${() => {
4534
- this.isDescriptionExpanded =
4535
- !this.isDescriptionExpanded;
4536
- this.requestUpdate();
4537
- }}
4738
+ this.isDescriptionExpanded =
4739
+ !this.isDescriptionExpanded;
4740
+ this.requestUpdate();
4741
+ }}
4538
4742
  >
4539
4743
  ${this.isDescriptionExpanded ? "See less" : "See more"}
4540
4744
  </button>
4541
4745
  `
4542
- : null
4543
- }
4746
+ : null
4747
+ }
4544
4748
  </div>
4545
4749
  `
4546
- : null
4547
- : html`
4750
+ : null
4751
+ : html`
4548
4752
  <div class="event-detail-section">
4549
4753
  <div class="event-detail-label">Description</div>
4550
4754
  <textarea
4551
4755
  class="event-detail-description-input"
4552
- .value=${event.description ?? ""}
4756
+ .value=${eventDescription}
4553
4757
  placeholder="Add description..."
4554
4758
  rows="3"
4555
4759
  @input=${(e: Event) => {
@@ -4579,7 +4783,7 @@ export class CalendarViewElement extends LitElement {
4579
4783
  clearTimeout(this.updateEventTimeout);
4580
4784
  this.updateEventTimeout = null;
4581
4785
  }
4582
- if (newDescription !== (event.description ?? "")) {
4786
+ if (newDescription !== eventDescription) {
4583
4787
  this.dispatchEvent(
4584
4788
  new CustomEvent("update-event", {
4585
4789
  detail: {
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Removes transport/tracking links commonly injected by email/calendar clients.
3
+ * First pass intentionally targets angle-bracket URI blobs like
4
+ * <https://...>, <mailto:...>, <tel:...>, <im:...>.
5
+ */
6
+ export function sanitizeEventDescription(input: string): string {
7
+ if (!input) return input;
8
+
9
+ return input.replace(/<(?:https?|mailto|tel|im):[^>\s]+>/gi, "");
10
+ }