@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/dist/calendar.js +1635 -1446
- package/package.json +2 -2
- package/src/CalDAVConfig.ts +241 -117
- package/src/CalendarInternal.ts +21 -4
- package/src/CalendarLayer.ts +28 -0
- package/src/CalendarStorage.ts +5 -0
- package/src/CalendarView.ts +360 -1196
- package/src/GoogleCalendarSource.ts +25 -0
- package/src/IndexedDBStorage.ts +16 -0
- package/src/InhouseBookingSource.ts +30 -1
- package/src/Keybinds.ts +3 -18
- package/src/StatusBar.ts +11 -0
- package/src/Theme.ts +4 -4
- package/src/TimeseriesJson.ts +114 -0
- package/src/app.css +17 -1
- package/src/app.ts +211 -186
- package/src/layers/EventsLayer.ts +958 -0
- package/src/layers/GridLayer.ts +296 -0
- package/src/layers/TimeseriesHeatmapLayer.ts +132 -0
- package/src/lib.ts +1 -0
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luckydye/calendar",
|
|
3
|
-
"version": "1.
|
|
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/
|
|
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",
|
package/src/CalDAVConfig.ts
CHANGED
|
@@ -44,7 +44,27 @@ interface InhouseSource extends CalendarSource {
|
|
|
44
44
|
locked?: boolean;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
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
|
-
|
|
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:
|
|
73
|
-
|
|
74
|
-
|
|
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:
|
|
105
|
+
font-size: 11px;
|
|
79
106
|
font-weight: 600;
|
|
80
|
-
|
|
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
|
-
.
|
|
116
|
+
.collapse-btn {
|
|
84
117
|
background: none;
|
|
85
118
|
border: none;
|
|
86
|
-
color: var(--text-muted, rgba(255, 255, 255, 0.
|
|
87
|
-
font-size: 20px;
|
|
119
|
+
color: var(--text-muted, rgba(255, 255, 255, 0.4));
|
|
88
120
|
cursor: pointer;
|
|
89
|
-
padding:
|
|
121
|
+
padding: 2px;
|
|
122
|
+
font-size: 12px;
|
|
90
123
|
line-height: 1;
|
|
91
124
|
}
|
|
92
125
|
|
|
93
|
-
.
|
|
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:
|
|
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
|
-
.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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-
|
|
145
|
+
.source-item {
|
|
146
|
+
flex: none;
|
|
116
147
|
display: flex;
|
|
117
148
|
align-items: center;
|
|
118
149
|
gap: 8px;
|
|
119
|
-
|
|
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:
|
|
124
|
-
height:
|
|
125
|
-
border-radius:
|
|
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-
|
|
178
|
+
font-size: 13px;
|
|
132
179
|
color: var(--text-primary, rgba(255, 255, 255, 0.9));
|
|
133
|
-
|
|
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
|
-
.
|
|
141
|
-
|
|
142
|
-
|
|
196
|
+
.calendar-list {
|
|
197
|
+
display: flex;
|
|
198
|
+
flex-direction: column;
|
|
143
199
|
}
|
|
144
200
|
|
|
145
|
-
.
|
|
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
|
-
.
|
|
151
|
-
background:
|
|
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
|
-
.
|
|
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
|
-
.
|
|
165
|
-
|
|
166
|
-
|
|
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:
|
|
179
|
-
background:
|
|
180
|
-
border: 1px dashed var(--grid-color
|
|
181
|
-
border-radius: var(--border-radius,
|
|
182
|
-
color: var(--text-
|
|
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:
|
|
254
|
+
font-size: 12px;
|
|
185
255
|
transition: all 0.15s;
|
|
186
256
|
}
|
|
187
257
|
|
|
188
258
|
.add-btn:hover {
|
|
189
|
-
background: var(--bg-item
|
|
190
|
-
border-color: var(--grid-color-
|
|
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:
|
|
236
|
-
height:
|
|
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">
|
|
721
|
-
<button class="
|
|
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.
|
|
725
|
-
? this.
|
|
726
|
-
: this.
|
|
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
|
|
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
|
-
|
|
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
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
-
|
|
799
|
-
|
|
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
|
-
|
|
802
|
-
</div>
|
|
901
|
+
` : null}
|
|
803
902
|
`;
|
|
804
903
|
}
|
|
805
904
|
)}
|
|
806
905
|
</div>
|
|
807
906
|
<button class="add-btn" @click=${this.addSource}>
|
|
808
|
-
+ Add
|
|
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>
|
package/src/CalendarInternal.ts
CHANGED
|
@@ -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 =
|
|
76
|
-
const EXTEND_WEEKS =
|
|
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
|
-
?
|
|
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
|
-
:
|
|
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
|
+
}
|
package/src/CalendarStorage.ts
CHANGED
|
@@ -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
|
}
|