@luckydye/calendar 1.2.3 → 1.3.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/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@luckydye/calendar",
3
- "version": "1.2.3",
3
+ "version": "1.3.1",
4
4
  "author": "Tim Havlicek",
5
5
  "contributors": [],
6
6
  "description": "",
7
7
  "type": "module",
8
8
  "main": "dist/calendar.js",
9
- "types": "src/calendar.ts",
9
+ "types": "src/lib.ts",
10
10
  "scripts": {
11
11
  "build:app": "APP=true bunx --bun vite build --outDir=dist/calendar --base=./",
12
12
  "build": "bunx --bun vite build",
@@ -44,7 +44,27 @@ interface InhouseSource extends CalendarSource {
44
44
  locked?: boolean;
45
45
  }
46
46
 
47
- type ConfigurableSource = CalDAVSourceConfig | ICalSource | GoogleSource | InhouseSource;
47
+ interface TimeseriesJsonSource extends CalendarSource {
48
+ type: "timeseries-json";
49
+ credentials: {
50
+ url: string;
51
+ };
52
+ locked?: boolean;
53
+ }
54
+
55
+ type ConfigurableSource =
56
+ | CalDAVSourceConfig
57
+ | ICalSource
58
+ | GoogleSource
59
+ | InhouseSource
60
+ | TimeseriesJsonSource;
61
+
62
+ interface SidebarCalendar {
63
+ id: string;
64
+ name: string;
65
+ color: string;
66
+ sourceId: string;
67
+ }
48
68
 
49
69
  export class CalDAVConfigElement extends LitElement {
50
70
  static styles = css`
@@ -56,114 +76,164 @@ export class CalDAVConfigElement extends LitElement {
56
76
 
57
77
  .container {
58
78
  background: var(--bg-elevated, rgba(30, 30, 30, 0.95));
59
- border: 1px solid var(--grid-color, rgba(255, 255, 255, 0.1));
60
- border-radius: var(--border-radius-lg, 8px);
61
- padding: 16px;
79
+ padding: 12px;
62
80
  box-sizing: border-box;
63
- min-width: 400px;
64
- max-width: 500px;
65
81
  height: 100%;
66
82
  }
67
83
 
84
+ :host([collapsed]) .container {
85
+ padding: 8px 6px;
86
+ display: flex;
87
+ flex-direction: column;
88
+ align-items: center;
89
+ }
90
+
68
91
  .header {
69
92
  display: flex;
70
93
  justify-content: space-between;
71
94
  align-items: center;
72
- margin-bottom: 16px;
73
- padding-bottom: 12px;
74
- border-bottom: 1px solid var(--grid-color, rgba(255, 255, 255, 0.1));
95
+ margin-bottom: 12px;
96
+ width: 100%;
97
+ }
98
+
99
+ :host([collapsed]) .header {
100
+ justify-content: center;
101
+ margin-bottom: 8px;
75
102
  }
76
103
 
77
104
  .title {
78
- font-size: 16px;
105
+ font-size: 11px;
79
106
  font-weight: 600;
80
- color: var(--text-primary, rgba(255, 255, 255, 0.9));
107
+ text-transform: uppercase;
108
+ letter-spacing: 0.5px;
109
+ color: var(--text-muted, rgba(255, 255, 255, 0.4));
110
+ }
111
+
112
+ :host([collapsed]) .title {
113
+ display: none;
81
114
  }
82
115
 
83
- .close-btn {
116
+ .collapse-btn {
84
117
  background: none;
85
118
  border: none;
86
- color: var(--text-muted, rgba(255, 255, 255, 0.5));
87
- font-size: 20px;
119
+ color: var(--text-muted, rgba(255, 255, 255, 0.4));
88
120
  cursor: pointer;
89
- padding: 4px;
121
+ padding: 2px;
122
+ font-size: 12px;
90
123
  line-height: 1;
91
124
  }
92
125
 
93
- .close-btn:hover {
126
+ .collapse-btn:hover {
94
127
  color: var(--text-primary, rgba(255, 255, 255, 0.9));
95
128
  }
96
129
 
97
130
  .sources-list {
98
131
  display: flex;
99
132
  flex-direction: column;
100
- gap: 12px;
133
+ gap: 0.25rem;
101
134
  margin-bottom: 16px;
102
135
  overflow-y: auto;
103
136
  max-height: calc(100% - 120px);
104
137
  }
105
138
 
106
- .source-item {
107
- flex: none;
108
- background: var(--bg-item, rgba(255, 255, 255, 0.05));
109
- border: 1px solid var(--grid-color, rgba(255, 255, 255, 0.1));
110
- border-radius: var(--border-radius, 6px);
111
- padding: 12px;
112
- overflow: hidden;
139
+ :host([collapsed]) .sources-list {
140
+ align-items: center;
141
+ gap: 4px;
142
+ margin-bottom: 0;
113
143
  }
114
144
 
115
- .source-header {
145
+ .source-item {
146
+ flex: none;
116
147
  display: flex;
117
148
  align-items: center;
118
149
  gap: 8px;
119
- margin-bottom: 8px;
150
+ padding: 6px 4px;
151
+ border-radius: var(--border-radius-sm, 4px);
152
+ cursor: pointer;
153
+ }
154
+
155
+ .source-item:hover {
156
+ background: var(--bg-item, rgba(255, 255, 255, 0.05));
157
+ }
158
+
159
+ :host([collapsed]) .source-item {
160
+ padding: 4px;
161
+ justify-content: center;
120
162
  }
121
163
 
122
164
  .source-color {
123
- width: 12px;
124
- height: 12px;
125
- border-radius: var(--border-radius-sm, 2px);
165
+ width: 10px;
166
+ height: 10px;
167
+ border-radius: 2px;
126
168
  flex-shrink: 0;
127
169
  }
128
170
 
171
+ .source-color.disabled {
172
+ background: transparent !important;
173
+ box-shadow: inset 0 0 0 1.5px var(--source-color);
174
+ }
175
+
129
176
  .source-name {
130
177
  flex: 1;
131
- font-weight: 500;
178
+ font-size: 13px;
132
179
  color: var(--text-primary, rgba(255, 255, 255, 0.9));
133
- font-size: 14px;
180
+ overflow: hidden;
181
+ text-overflow: ellipsis;
182
+ white-space: nowrap;
183
+ }
184
+
185
+ :host([collapsed]) .source-name,
186
+ :host([collapsed]) .source-enabled,
187
+ :host([collapsed]) .add-btn,
188
+ :host([collapsed]) .empty-state {
189
+ display: none;
134
190
  }
135
191
 
136
192
  .source-enabled {
137
193
  cursor: pointer;
138
194
  }
139
195
 
140
- .source-locked {
141
- cursor: pointer;
142
- margin-left: 8px;
196
+ .calendar-list {
197
+ display: flex;
198
+ flex-direction: column;
143
199
  }
144
200
 
145
- .source-actions {
201
+ .calendar-item {
146
202
  display: flex;
203
+ align-items: center;
147
204
  gap: 8px;
205
+ padding: 4px 4px 4px 22px;
206
+ border-radius: var(--border-radius-sm, 4px);
207
+ cursor: pointer;
208
+ font-size: 12px;
209
+ color: var(--text-secondary, rgba(255, 255, 255, 0.7));
148
210
  }
149
211
 
150
- .icon-btn {
151
- background: none;
152
- border: none;
153
- color: var(--text-muted, rgba(255, 255, 255, 0.5));
154
- cursor: pointer;
155
- padding: 4px;
156
- font-size: 14px;
157
- line-height: 1;
212
+ .calendar-item:hover {
213
+ background: var(--bg-item, rgba(255, 255, 255, 0.05));
158
214
  }
159
215
 
160
- .icon-btn:hover {
216
+ .calendar-item.active {
217
+ background: var(--bg-item, rgba(255, 255, 255, 0.08));
161
218
  color: var(--text-primary, rgba(255, 255, 255, 0.9));
219
+ font-weight: 500;
162
220
  }
163
221
 
164
- .source-url {
165
- font-size: 12px;
166
- color: var(--text-muted, rgba(255, 255, 255, 0.4));
222
+ .calendar-color {
223
+ width: 8px;
224
+ height: 8px;
225
+ border-radius: 50%;
226
+ flex-shrink: 0;
227
+ }
228
+
229
+ .calendar-name {
230
+ overflow: hidden;
231
+ text-overflow: ellipsis;
232
+ white-space: nowrap;
233
+ }
234
+
235
+ :host([collapsed]) .calendar-list {
236
+ display: none;
167
237
  }
168
238
 
169
239
  .empty-state {
@@ -175,19 +245,19 @@ export class CalDAVConfigElement extends LitElement {
175
245
 
176
246
  .add-btn {
177
247
  width: 100%;
178
- padding: 10px;
179
- background: var(--bg-button-hover, rgba(255, 255, 255, 0.1));
180
- border: 1px dashed var(--grid-color-hover, rgba(255, 255, 255, 0.3));
181
- border-radius: var(--border-radius, 6px);
182
- color: var(--text-secondary, rgba(255, 255, 255, 0.7));
248
+ padding: 6px;
249
+ background: none;
250
+ border: 1px dashed var(--grid-color, rgba(255, 255, 255, 0.15));
251
+ border-radius: var(--border-radius-sm, 4px);
252
+ color: var(--text-muted, rgba(255, 255, 255, 0.4));
183
253
  cursor: pointer;
184
- font-size: 14px;
254
+ font-size: 12px;
185
255
  transition: all 0.15s;
186
256
  }
187
257
 
188
258
  .add-btn:hover {
189
- background: var(--bg-item-hover, rgba(255, 255, 255, 0.15));
190
- border-color: var(--grid-color-strong, rgba(255, 255, 255, 0.5));
259
+ background: var(--bg-item, rgba(255, 255, 255, 0.05));
260
+ border-color: var(--grid-color-hover, rgba(255, 255, 255, 0.3));
191
261
  color: var(--text-primary, rgba(255, 255, 255, 0.9));
192
262
  }
193
263
 
@@ -232,8 +302,8 @@ export class CalDAVConfigElement extends LitElement {
232
302
  }
233
303
 
234
304
  .color-option {
235
- width: 28px;
236
- height: 28px;
305
+ width: 22px;
306
+ height: 22px;
237
307
  border-radius: var(--border-radius-sm, 4px);
238
308
  cursor: pointer;
239
309
  border: 2px solid transparent;
@@ -284,6 +354,18 @@ export class CalDAVConfigElement extends LitElement {
284
354
  color: var(--text-primary, rgba(255, 255, 255, 0.9));
285
355
  }
286
356
 
357
+ .btn-danger {
358
+ background: transparent;
359
+ border: 1px solid var(--accent-error, #e53935);
360
+ color: var(--accent-error, #e53935);
361
+ margin-right: auto;
362
+ }
363
+
364
+ .btn-danger:hover {
365
+ background: var(--accent-error, #e53935);
366
+ color: white;
367
+ }
368
+
287
369
  .sync-status {
288
370
  display: flex;
289
371
  align-items: center;
@@ -376,20 +458,26 @@ export class CalDAVConfigElement extends LitElement {
376
458
  `;
377
459
 
378
460
  sources: ConfigurableSource[] = [];
461
+ calendars: SidebarCalendar[] = [];
462
+ activeCalendarId: string | null = null;
379
463
  isAdding = false;
380
464
  editingId: string | null = null;
381
465
  isGoogleAuthenticating = false;
382
466
  googleAuthError: string | null = null;
467
+ collapsed = localStorage.getItem("caldav-sidebar-collapsed") === "true";
383
468
 
384
469
  private formData: Partial<ConfigurableSource> = {};
385
470
 
386
471
  static get properties() {
387
472
  return {
388
473
  sources: { type: Array },
474
+ calendars: { type: Array },
475
+ activeCalendarId: { type: String },
389
476
  isAdding: { type: Boolean },
390
477
  editingId: { type: String },
391
478
  isGoogleAuthenticating: { type: Boolean },
392
479
  googleAuthError: { type: String },
480
+ collapsed: { type: Boolean, reflect: true },
393
481
  };
394
482
  }
395
483
 
@@ -445,6 +533,15 @@ export class CalDAVConfigElement extends LitElement {
445
533
  this.requestUpdate();
446
534
  }
447
535
 
536
+ toggleCollapsed() {
537
+ this.collapsed = !this.collapsed;
538
+ localStorage.setItem("caldav-sidebar-collapsed", String(this.collapsed));
539
+ this.dispatchEvent(new CustomEvent("collapsed-changed", {
540
+ detail: { collapsed: this.collapsed },
541
+ bubbles: true,
542
+ }));
543
+ }
544
+
448
545
  addSource() {
449
546
  this.isAdding = true;
450
547
  this.editingId = null;
@@ -478,6 +575,14 @@ export class CalDAVConfigElement extends LitElement {
478
575
  this.saveSources();
479
576
  }
480
577
 
578
+ selectCalendar(calendarId: string) {
579
+ this.activeCalendarId = calendarId;
580
+ this.dispatchEvent(new CustomEvent("active-calendar-changed", {
581
+ detail: { calendarId },
582
+ bubbles: true,
583
+ }));
584
+ }
585
+
481
586
  saveForm() {
482
587
  if (!this.formData.name?.trim()) {
483
588
  alert('Please enter a calendar name');
@@ -529,6 +634,22 @@ export class CalDAVConfigElement extends LitElement {
529
634
  enabled: this.formData.enabled ?? true,
530
635
  locked: this.formData.locked ?? false,
531
636
  } as ICalSource;
637
+ } else if (this.formData.type === "timeseries-json") {
638
+ if (!this.formData.credentials?.url) {
639
+ return;
640
+ }
641
+
642
+ source = {
643
+ id: this.editingId || crypto.randomUUID(),
644
+ name: this.formData.name,
645
+ type: "timeseries-json",
646
+ credentials: {
647
+ url: this.formData.credentials.url,
648
+ },
649
+ color: this.formData.color || "#06B6D4",
650
+ enabled: this.formData.enabled ?? true,
651
+ locked: this.formData.locked ?? false,
652
+ } as TimeseriesJsonSource;
532
653
  } else if (this.formData.type === "google") {
533
654
  if (!this.formData.credentials?.accessToken) {
534
655
  alert('Please authenticate with Google before adding the calendar');
@@ -717,13 +838,17 @@ export class CalDAVConfigElement extends LitElement {
717
838
  return html`
718
839
  <div class="container">
719
840
  <div class="header">
720
- <span class="title">Calendar Sources</span>
721
- <button class="close-btn" @click=${this.close}>×</button>
841
+ <span class="title">Sources</span>
842
+ <button class="collapse-btn" @click=${this.toggleCollapsed} title="${this.collapsed ? "Expand" : "Collapse"}">
843
+ ${this.collapsed ? "▶" : "◀"}
844
+ </button>
722
845
  </div>
723
846
 
724
- ${this.isAdding || this.editingId
725
- ? this.renderForm(colors)
726
- : this.renderSourcesList()}
847
+ ${this.collapsed
848
+ ? this.renderSourcesList()
849
+ : (this.isAdding || this.editingId
850
+ ? this.renderForm(colors)
851
+ : this.renderSourcesList())}
727
852
  </div>
728
853
  `;
729
854
  }
@@ -735,77 +860,51 @@ export class CalDAVConfigElement extends LitElement {
735
860
  No calendar sources configured
736
861
  </div>
737
862
  <button class="add-btn" @click=${this.addSource}>
738
- + Add Calendar Source
863
+ + Add Source
739
864
  </button>
740
865
  `;
741
866
  }
742
-
743
- console.log(this.sources);
744
867
 
745
868
  return html`
746
869
  <div class="sources-list">
747
870
  ${this.sources.map(
748
871
  (source) => {
749
- let urlDisplay: string;
750
- if (source.type === "caldav") {
751
- urlDisplay = (source.credentials as CalDAVSourceConfig["credentials"]).serverUrl;
752
- } else if (source.type === "ical") {
753
- urlDisplay = (source.credentials as ICalSource["credentials"]).url;
754
- } else if (source.type === "google") {
755
- urlDisplay = (source.credentials as GoogleSource["credentials"]).calendarId || "primary";
756
- } else if (source.type === "inhouse") {
757
- urlDisplay = `Employee: ${(source.credentials as InhouseSource["credentials"]).employeeId}`;
758
- } else {
759
- urlDisplay = "Unknown source";
760
- }
761
-
872
+ const sourceCalendars = this.calendars.filter(c => c.sourceId === source.id);
762
873
  return html`
763
- <div class="source-item">
764
- <div class="source-header">
765
- <div
766
- class="source-color"
767
- style="background: ${source.color}"
768
- ></div>
769
- <span class="source-name">${source.name}</span>
770
- <input
771
- type="checkbox"
772
- class="source-enabled"
773
- .checked=${source.enabled}
774
- @change=${() => this.toggleEnabled(source)}
775
- title="${source.enabled ? "Disable" : "Enable"} sync"
776
- />
777
- <input
778
- type="checkbox"
779
- class="source-locked"
780
- .checked=${source.locked}
781
- @change=${() => this.toggleLocked(source)}
782
- title="${source.locked ? "Unlock (allow editing)" : "Lock (read-only)"}"
783
- />
784
- <div class="source-actions">
785
- <button
786
- class="icon-btn"
787
- @click=${() => this.editSource(source)}
788
- title="Edit"
789
- >
790
-
791
- </button>
792
- <button
793
- class="icon-btn"
794
- @click=${() => this.deleteSource(source.id)}
795
- title="Delete"
874
+ <div class="source-item" @click=${() => this.collapsed ? this.toggleEnabled(source) : this.editSource(source)}>
875
+ <div
876
+ class="source-color ${source.enabled ? "" : "disabled"}"
877
+ style="background: ${source.color}; --source-color: ${source.color}"
878
+ ></div>
879
+ <span class="source-name">${source.name}</span>
880
+ <input
881
+ type="checkbox"
882
+ class="source-enabled"
883
+ .checked=${source.enabled}
884
+ @click=${(e: Event) => e.stopPropagation()}
885
+ @change=${() => this.toggleEnabled(source)}
886
+ title="${source.enabled ? "Disable" : "Enable"} sync"
887
+ />
888
+ </div>
889
+ ${source.enabled && sourceCalendars.length > 0 ? html`
890
+ <div class="calendar-list">
891
+ ${sourceCalendars.map(cal => html`
892
+ <div
893
+ class="calendar-item ${this.activeCalendarId === cal.id ? "active" : ""}"
894
+ @click=${() => this.selectCalendar(cal.id)}
796
895
  >
797
- 🗑
798
- </button>
799
- </div>
896
+ <div class="calendar-color" style="background: ${cal.color}"></div>
897
+ <span class="calendar-name">${cal.name}</span>
898
+ </div>
899
+ `)}
800
900
  </div>
801
- <div class="source-url">${source.type.toUpperCase()}: ${urlDisplay || "No URL"}</div>
802
- </div>
901
+ ` : null}
803
902
  `;
804
903
  }
805
904
  )}
806
905
  </div>
807
906
  <button class="add-btn" @click=${this.addSource}>
808
- + Add Calendar Source
907
+ + Add Source
809
908
  </button>
810
909
  `;
811
910
  }
@@ -829,12 +928,15 @@ export class CalDAVConfigElement extends LitElement {
829
928
  this.updateForm("color", "#4285F4");
830
929
  } else if (type === "caldav" && !this.formData.color) {
831
930
  this.updateForm("color", "#FF6E68");
931
+ } else if (type === "timeseries-json" && !this.formData.color) {
932
+ this.updateForm("color", "#06B6D4");
832
933
  }
833
934
  }}
834
935
  ?disabled=${isEditing}
835
936
  >
836
937
  <option value="caldav">CalDAV (with credentials)</option>
837
938
  <option value="ical">iCal URL</option>
939
+ <option value="timeseries-json">Timeseries JSON (URL)</option>
838
940
  <option value="google">Google Calendar</option>
839
941
  <option value="inhouse">Inhouse Booking System</option>
840
942
  </select>
@@ -902,6 +1004,23 @@ export class CalDAVConfigElement extends LitElement {
902
1004
  />
903
1005
  </div>
904
1006
  `
1007
+ : sourceType === "timeseries-json"
1008
+ ? html`
1009
+ <div class="form-group">
1010
+ <label class="form-label">Timeseries JSON URL</label>
1011
+ <input
1012
+ class="form-input"
1013
+ type="text"
1014
+ placeholder="https://example.com/data.json"
1015
+ .value=${this.formData.credentials?.url || ""}
1016
+ @input=${(e: Event) =>
1017
+ this.updateForm("url", (e.target as HTMLInputElement).value)}
1018
+ />
1019
+ <small style="color: var(--text-muted, rgba(255, 255, 255, 0.5)); font-size: 11px;">
1020
+ Expected format: JSON array of objects with a <code>timestamp</code> field. Rendered as a heatmap backdrop.
1021
+ </small>
1022
+ </div>
1023
+ `
905
1024
  : sourceType === "google"
906
1025
  ? html`
907
1026
  <div class="google-auth-section">
@@ -1021,6 +1140,11 @@ export class CalDAVConfigElement extends LitElement {
1021
1140
  </div>
1022
1141
 
1023
1142
  <div class="form-actions">
1143
+ ${isEditing ? html`
1144
+ <button class="btn btn-danger" @click=${() => this.deleteSource(this.editingId!)}>
1145
+ Delete
1146
+ </button>
1147
+ ` : null}
1024
1148
  <button class="btn btn-secondary" @click=${this.cancelForm}>
1025
1149
  Cancel
1026
1150
  </button>
@@ -49,6 +49,7 @@ export interface CalendarEvent {
49
49
  status?: 'TENTATIVE' | 'CONFIRMED' | 'CANCELLED';
50
50
  reminders?: NotificationConfig[];
51
51
  isAllDay?: boolean;
52
+ visualStyle?: 'heatmap';
52
53
  }
53
54
 
54
55
  export interface WeekInfo {
@@ -72,8 +73,9 @@ export interface EventSegment {
72
73
  }
73
74
 
74
75
  // Configuration for the sliding window buffer
75
- const BUFFER_WEEKS = 12; // Weeks to keep as buffer outside viewport
76
- const EXTEND_WEEKS = 12; // Weeks to add when extending
76
+ const BUFFER_WEEKS = 4; // Trigger extension when within this many weeks of the edge
77
+ const EXTEND_WEEKS = 26; // Weeks to add per extension (~6 months); must be >> BUFFER_WEEKS so
78
+ // the scroll thumb lands well past the buffer entry after each extension, preventing cascade
77
79
  const MAX_WEEKS = 96; // Maximum weeks to keep in memory (trim beyond this)
78
80
 
79
81
  export class CalendarInternal {
@@ -185,6 +187,18 @@ export class CalendarInternal {
185
187
  }
186
188
 
187
189
 
190
+ applyEventOptimistically(event: CalendarEvent): void {
191
+ this.calendarEvents.set(event.id, event);
192
+ this.storage?.putEvent(event).catch(() => { /* sync will fix it */ });
193
+ this.sendEvents();
194
+ }
195
+
196
+ removeEventOptimistically(eventId: string): void {
197
+ this.calendarEvents.delete(eventId);
198
+ this.storage?.deleteEvent(eventId).catch(() => { /* sync will fix it */ });
199
+ this.sendEvents();
200
+ }
201
+
188
202
  setFilter(filter: string) {
189
203
  this.filter = filter;
190
204
  this.sendEvents();
@@ -246,18 +260,21 @@ export class CalendarInternal {
246
260
 
247
261
  getFilteredEvents(filter?: string) {
248
262
  const baseEvents = Array.from(this.calendarEvents.values());
263
+ return this.filterEvents(baseEvents, filter);
264
+ }
249
265
 
266
+ filterEvents(events: CalendarEvent[], filter?: string): CalendarEvent[] {
250
267
  // Filter by enabled calendars
251
268
  // Note: enabledCalendars contains calendar IDs, not sourceIds
252
269
  const enabledEvents = this.enabledCalendars.size > 0
253
- ? baseEvents.filter(e => {
270
+ ? events.filter(e => {
254
271
  // For backwards compatibility: check both sourceId and calendarId
255
272
  // CalDAV events: match via calendarId
256
273
  // Other sources: match via sourceId
257
274
  return (e.calendarId && this.enabledCalendars.has(e.calendarId)) ||
258
275
  (e.sourceId && this.enabledCalendars.has(e.sourceId));
259
276
  })
260
- : baseEvents;
277
+ : events;
261
278
 
262
279
  // Mark events from locked calendars as read-only
263
280
  const lockedEvents = this.lockedCalendars.size > 0
@@ -0,0 +1,28 @@
1
+ import type { WeekInfo } from "./CalendarInternal.js";
2
+
3
+ // Minimum dayHeight at which timed events are rendered at their actual time-of-day position.
4
+ export const TIME_SCALE_DAY_HEIGHT = 300;
5
+
6
+ export interface LayerContext {
7
+ ctx: CanvasRenderingContext2D;
8
+ width: number;
9
+ height: number;
10
+ scrollTop: number;
11
+ dayWidth: number;
12
+ dayHeight: number;
13
+ leftGutterWidth: number;
14
+ columnsPerRow: number;
15
+ rowsPerWeek: number;
16
+ visibleWeeks: WeekInfo[];
17
+ allWeeks: WeekInfo[];
18
+ fontFamily: string;
19
+ styles: Record<string, string>;
20
+ getDayVisualPosition: (dayIndex: number) => { row: number; col: number };
21
+ filter: string | null;
22
+ }
23
+
24
+ export interface CalendarLayer {
25
+ name: string;
26
+ enabled: boolean;
27
+ render(lc: LayerContext): void;
28
+ }
@@ -51,4 +51,9 @@ export interface CalendarStorage {
51
51
  * Upserts a single event (e.g. to persist locally-set reminders).
52
52
  */
53
53
  putEvent(event: CalendarEvent): Promise<void>;
54
+
55
+ /**
56
+ * Deletes a single event by ID (e.g. for optimistic deletes).
57
+ */
58
+ deleteEvent(id: string): Promise<void>;
54
59
  }