@luckydye/calendar 1.2.3 → 1.3.0
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 +291 -277
- package/package.json +1 -1
- package/src/CalDAVConfig.ts +191 -116
- package/src/CalendarInternal.ts +12 -0
- package/src/CalendarStorage.ts +5 -0
- package/src/CalendarView.ts +83 -86
- package/src/IndexedDBStorage.ts +13 -0
- package/src/InhouseBookingSource.ts +30 -1
- package/src/app.css +17 -1
- package/src/app.ts +168 -183
package/package.json
CHANGED
package/src/CalDAVConfig.ts
CHANGED
|
@@ -46,6 +46,13 @@ interface InhouseSource extends CalendarSource {
|
|
|
46
46
|
|
|
47
47
|
type ConfigurableSource = CalDAVSourceConfig | ICalSource | GoogleSource | InhouseSource;
|
|
48
48
|
|
|
49
|
+
interface SidebarCalendar {
|
|
50
|
+
id: string;
|
|
51
|
+
name: string;
|
|
52
|
+
color: string;
|
|
53
|
+
sourceId: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
49
56
|
export class CalDAVConfigElement extends LitElement {
|
|
50
57
|
static styles = css`
|
|
51
58
|
:host {
|
|
@@ -56,114 +63,164 @@ export class CalDAVConfigElement extends LitElement {
|
|
|
56
63
|
|
|
57
64
|
.container {
|
|
58
65
|
background: var(--bg-elevated, rgba(30, 30, 30, 0.95));
|
|
59
|
-
|
|
60
|
-
border-radius: var(--border-radius-lg, 8px);
|
|
61
|
-
padding: 16px;
|
|
66
|
+
padding: 12px;
|
|
62
67
|
box-sizing: border-box;
|
|
63
|
-
min-width: 400px;
|
|
64
|
-
max-width: 500px;
|
|
65
68
|
height: 100%;
|
|
66
69
|
}
|
|
67
70
|
|
|
71
|
+
:host([collapsed]) .container {
|
|
72
|
+
padding: 8px 6px;
|
|
73
|
+
display: flex;
|
|
74
|
+
flex-direction: column;
|
|
75
|
+
align-items: center;
|
|
76
|
+
}
|
|
77
|
+
|
|
68
78
|
.header {
|
|
69
79
|
display: flex;
|
|
70
80
|
justify-content: space-between;
|
|
71
81
|
align-items: center;
|
|
72
|
-
margin-bottom:
|
|
73
|
-
|
|
74
|
-
|
|
82
|
+
margin-bottom: 12px;
|
|
83
|
+
width: 100%;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
:host([collapsed]) .header {
|
|
87
|
+
justify-content: center;
|
|
88
|
+
margin-bottom: 8px;
|
|
75
89
|
}
|
|
76
90
|
|
|
77
91
|
.title {
|
|
78
|
-
font-size:
|
|
92
|
+
font-size: 11px;
|
|
79
93
|
font-weight: 600;
|
|
80
|
-
|
|
94
|
+
text-transform: uppercase;
|
|
95
|
+
letter-spacing: 0.5px;
|
|
96
|
+
color: var(--text-muted, rgba(255, 255, 255, 0.4));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
:host([collapsed]) .title {
|
|
100
|
+
display: none;
|
|
81
101
|
}
|
|
82
102
|
|
|
83
|
-
.
|
|
103
|
+
.collapse-btn {
|
|
84
104
|
background: none;
|
|
85
105
|
border: none;
|
|
86
|
-
color: var(--text-muted, rgba(255, 255, 255, 0.
|
|
87
|
-
font-size: 20px;
|
|
106
|
+
color: var(--text-muted, rgba(255, 255, 255, 0.4));
|
|
88
107
|
cursor: pointer;
|
|
89
|
-
padding:
|
|
108
|
+
padding: 2px;
|
|
109
|
+
font-size: 12px;
|
|
90
110
|
line-height: 1;
|
|
91
111
|
}
|
|
92
112
|
|
|
93
|
-
.
|
|
113
|
+
.collapse-btn:hover {
|
|
94
114
|
color: var(--text-primary, rgba(255, 255, 255, 0.9));
|
|
95
115
|
}
|
|
96
116
|
|
|
97
117
|
.sources-list {
|
|
98
118
|
display: flex;
|
|
99
119
|
flex-direction: column;
|
|
100
|
-
gap:
|
|
120
|
+
gap: 0.25rem;
|
|
101
121
|
margin-bottom: 16px;
|
|
102
122
|
overflow-y: auto;
|
|
103
123
|
max-height: calc(100% - 120px);
|
|
104
124
|
}
|
|
105
125
|
|
|
106
|
-
.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
border-radius: var(--border-radius, 6px);
|
|
111
|
-
padding: 12px;
|
|
112
|
-
overflow: hidden;
|
|
126
|
+
:host([collapsed]) .sources-list {
|
|
127
|
+
align-items: center;
|
|
128
|
+
gap: 4px;
|
|
129
|
+
margin-bottom: 0;
|
|
113
130
|
}
|
|
114
131
|
|
|
115
|
-
.source-
|
|
132
|
+
.source-item {
|
|
133
|
+
flex: none;
|
|
116
134
|
display: flex;
|
|
117
135
|
align-items: center;
|
|
118
136
|
gap: 8px;
|
|
119
|
-
|
|
137
|
+
padding: 6px 4px;
|
|
138
|
+
border-radius: var(--border-radius-sm, 4px);
|
|
139
|
+
cursor: pointer;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.source-item:hover {
|
|
143
|
+
background: var(--bg-item, rgba(255, 255, 255, 0.05));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
:host([collapsed]) .source-item {
|
|
147
|
+
padding: 4px;
|
|
148
|
+
justify-content: center;
|
|
120
149
|
}
|
|
121
150
|
|
|
122
151
|
.source-color {
|
|
123
|
-
width:
|
|
124
|
-
height:
|
|
125
|
-
border-radius:
|
|
152
|
+
width: 10px;
|
|
153
|
+
height: 10px;
|
|
154
|
+
border-radius: 2px;
|
|
126
155
|
flex-shrink: 0;
|
|
127
156
|
}
|
|
128
157
|
|
|
158
|
+
.source-color.disabled {
|
|
159
|
+
background: transparent !important;
|
|
160
|
+
box-shadow: inset 0 0 0 1.5px var(--source-color);
|
|
161
|
+
}
|
|
162
|
+
|
|
129
163
|
.source-name {
|
|
130
164
|
flex: 1;
|
|
131
|
-
font-
|
|
165
|
+
font-size: 13px;
|
|
132
166
|
color: var(--text-primary, rgba(255, 255, 255, 0.9));
|
|
133
|
-
|
|
167
|
+
overflow: hidden;
|
|
168
|
+
text-overflow: ellipsis;
|
|
169
|
+
white-space: nowrap;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
:host([collapsed]) .source-name,
|
|
173
|
+
:host([collapsed]) .source-enabled,
|
|
174
|
+
:host([collapsed]) .add-btn,
|
|
175
|
+
:host([collapsed]) .empty-state {
|
|
176
|
+
display: none;
|
|
134
177
|
}
|
|
135
178
|
|
|
136
179
|
.source-enabled {
|
|
137
180
|
cursor: pointer;
|
|
138
181
|
}
|
|
139
182
|
|
|
140
|
-
.
|
|
141
|
-
|
|
142
|
-
|
|
183
|
+
.calendar-list {
|
|
184
|
+
display: flex;
|
|
185
|
+
flex-direction: column;
|
|
143
186
|
}
|
|
144
187
|
|
|
145
|
-
.
|
|
188
|
+
.calendar-item {
|
|
146
189
|
display: flex;
|
|
190
|
+
align-items: center;
|
|
147
191
|
gap: 8px;
|
|
192
|
+
padding: 4px 4px 4px 22px;
|
|
193
|
+
border-radius: var(--border-radius-sm, 4px);
|
|
194
|
+
cursor: pointer;
|
|
195
|
+
font-size: 12px;
|
|
196
|
+
color: var(--text-secondary, rgba(255, 255, 255, 0.7));
|
|
148
197
|
}
|
|
149
198
|
|
|
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;
|
|
199
|
+
.calendar-item:hover {
|
|
200
|
+
background: var(--bg-item, rgba(255, 255, 255, 0.05));
|
|
158
201
|
}
|
|
159
202
|
|
|
160
|
-
.
|
|
203
|
+
.calendar-item.active {
|
|
204
|
+
background: var(--bg-item, rgba(255, 255, 255, 0.08));
|
|
161
205
|
color: var(--text-primary, rgba(255, 255, 255, 0.9));
|
|
206
|
+
font-weight: 500;
|
|
162
207
|
}
|
|
163
208
|
|
|
164
|
-
.
|
|
165
|
-
|
|
166
|
-
|
|
209
|
+
.calendar-color {
|
|
210
|
+
width: 8px;
|
|
211
|
+
height: 8px;
|
|
212
|
+
border-radius: 50%;
|
|
213
|
+
flex-shrink: 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.calendar-name {
|
|
217
|
+
overflow: hidden;
|
|
218
|
+
text-overflow: ellipsis;
|
|
219
|
+
white-space: nowrap;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
:host([collapsed]) .calendar-list {
|
|
223
|
+
display: none;
|
|
167
224
|
}
|
|
168
225
|
|
|
169
226
|
.empty-state {
|
|
@@ -175,19 +232,19 @@ export class CalDAVConfigElement extends LitElement {
|
|
|
175
232
|
|
|
176
233
|
.add-btn {
|
|
177
234
|
width: 100%;
|
|
178
|
-
padding:
|
|
179
|
-
background:
|
|
180
|
-
border: 1px dashed var(--grid-color
|
|
181
|
-
border-radius: var(--border-radius,
|
|
182
|
-
color: var(--text-
|
|
235
|
+
padding: 6px;
|
|
236
|
+
background: none;
|
|
237
|
+
border: 1px dashed var(--grid-color, rgba(255, 255, 255, 0.15));
|
|
238
|
+
border-radius: var(--border-radius-sm, 4px);
|
|
239
|
+
color: var(--text-muted, rgba(255, 255, 255, 0.4));
|
|
183
240
|
cursor: pointer;
|
|
184
|
-
font-size:
|
|
241
|
+
font-size: 12px;
|
|
185
242
|
transition: all 0.15s;
|
|
186
243
|
}
|
|
187
244
|
|
|
188
245
|
.add-btn:hover {
|
|
189
|
-
background: var(--bg-item
|
|
190
|
-
border-color: var(--grid-color-
|
|
246
|
+
background: var(--bg-item, rgba(255, 255, 255, 0.05));
|
|
247
|
+
border-color: var(--grid-color-hover, rgba(255, 255, 255, 0.3));
|
|
191
248
|
color: var(--text-primary, rgba(255, 255, 255, 0.9));
|
|
192
249
|
}
|
|
193
250
|
|
|
@@ -232,8 +289,8 @@ export class CalDAVConfigElement extends LitElement {
|
|
|
232
289
|
}
|
|
233
290
|
|
|
234
291
|
.color-option {
|
|
235
|
-
width:
|
|
236
|
-
height:
|
|
292
|
+
width: 22px;
|
|
293
|
+
height: 22px;
|
|
237
294
|
border-radius: var(--border-radius-sm, 4px);
|
|
238
295
|
cursor: pointer;
|
|
239
296
|
border: 2px solid transparent;
|
|
@@ -284,6 +341,18 @@ export class CalDAVConfigElement extends LitElement {
|
|
|
284
341
|
color: var(--text-primary, rgba(255, 255, 255, 0.9));
|
|
285
342
|
}
|
|
286
343
|
|
|
344
|
+
.btn-danger {
|
|
345
|
+
background: transparent;
|
|
346
|
+
border: 1px solid var(--accent-error, #e53935);
|
|
347
|
+
color: var(--accent-error, #e53935);
|
|
348
|
+
margin-right: auto;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.btn-danger:hover {
|
|
352
|
+
background: var(--accent-error, #e53935);
|
|
353
|
+
color: white;
|
|
354
|
+
}
|
|
355
|
+
|
|
287
356
|
.sync-status {
|
|
288
357
|
display: flex;
|
|
289
358
|
align-items: center;
|
|
@@ -376,20 +445,26 @@ export class CalDAVConfigElement extends LitElement {
|
|
|
376
445
|
`;
|
|
377
446
|
|
|
378
447
|
sources: ConfigurableSource[] = [];
|
|
448
|
+
calendars: SidebarCalendar[] = [];
|
|
449
|
+
activeCalendarId: string | null = null;
|
|
379
450
|
isAdding = false;
|
|
380
451
|
editingId: string | null = null;
|
|
381
452
|
isGoogleAuthenticating = false;
|
|
382
453
|
googleAuthError: string | null = null;
|
|
454
|
+
collapsed = localStorage.getItem("caldav-sidebar-collapsed") === "true";
|
|
383
455
|
|
|
384
456
|
private formData: Partial<ConfigurableSource> = {};
|
|
385
457
|
|
|
386
458
|
static get properties() {
|
|
387
459
|
return {
|
|
388
460
|
sources: { type: Array },
|
|
461
|
+
calendars: { type: Array },
|
|
462
|
+
activeCalendarId: { type: String },
|
|
389
463
|
isAdding: { type: Boolean },
|
|
390
464
|
editingId: { type: String },
|
|
391
465
|
isGoogleAuthenticating: { type: Boolean },
|
|
392
466
|
googleAuthError: { type: String },
|
|
467
|
+
collapsed: { type: Boolean, reflect: true },
|
|
393
468
|
};
|
|
394
469
|
}
|
|
395
470
|
|
|
@@ -445,6 +520,15 @@ export class CalDAVConfigElement extends LitElement {
|
|
|
445
520
|
this.requestUpdate();
|
|
446
521
|
}
|
|
447
522
|
|
|
523
|
+
toggleCollapsed() {
|
|
524
|
+
this.collapsed = !this.collapsed;
|
|
525
|
+
localStorage.setItem("caldav-sidebar-collapsed", String(this.collapsed));
|
|
526
|
+
this.dispatchEvent(new CustomEvent("collapsed-changed", {
|
|
527
|
+
detail: { collapsed: this.collapsed },
|
|
528
|
+
bubbles: true,
|
|
529
|
+
}));
|
|
530
|
+
}
|
|
531
|
+
|
|
448
532
|
addSource() {
|
|
449
533
|
this.isAdding = true;
|
|
450
534
|
this.editingId = null;
|
|
@@ -478,6 +562,14 @@ export class CalDAVConfigElement extends LitElement {
|
|
|
478
562
|
this.saveSources();
|
|
479
563
|
}
|
|
480
564
|
|
|
565
|
+
selectCalendar(calendarId: string) {
|
|
566
|
+
this.activeCalendarId = calendarId;
|
|
567
|
+
this.dispatchEvent(new CustomEvent("active-calendar-changed", {
|
|
568
|
+
detail: { calendarId },
|
|
569
|
+
bubbles: true,
|
|
570
|
+
}));
|
|
571
|
+
}
|
|
572
|
+
|
|
481
573
|
saveForm() {
|
|
482
574
|
if (!this.formData.name?.trim()) {
|
|
483
575
|
alert('Please enter a calendar name');
|
|
@@ -717,13 +809,17 @@ export class CalDAVConfigElement extends LitElement {
|
|
|
717
809
|
return html`
|
|
718
810
|
<div class="container">
|
|
719
811
|
<div class="header">
|
|
720
|
-
<span class="title">
|
|
721
|
-
<button class="
|
|
812
|
+
<span class="title">Sources</span>
|
|
813
|
+
<button class="collapse-btn" @click=${this.toggleCollapsed} title="${this.collapsed ? "Expand" : "Collapse"}">
|
|
814
|
+
${this.collapsed ? "▶" : "◀"}
|
|
815
|
+
</button>
|
|
722
816
|
</div>
|
|
723
817
|
|
|
724
|
-
${this.
|
|
725
|
-
? this.
|
|
726
|
-
: this.
|
|
818
|
+
${this.collapsed
|
|
819
|
+
? this.renderSourcesList()
|
|
820
|
+
: (this.isAdding || this.editingId
|
|
821
|
+
? this.renderForm(colors)
|
|
822
|
+
: this.renderSourcesList())}
|
|
727
823
|
</div>
|
|
728
824
|
`;
|
|
729
825
|
}
|
|
@@ -735,77 +831,51 @@ export class CalDAVConfigElement extends LitElement {
|
|
|
735
831
|
No calendar sources configured
|
|
736
832
|
</div>
|
|
737
833
|
<button class="add-btn" @click=${this.addSource}>
|
|
738
|
-
+ Add
|
|
834
|
+
+ Add Source
|
|
739
835
|
</button>
|
|
740
836
|
`;
|
|
741
837
|
}
|
|
742
|
-
|
|
743
|
-
console.log(this.sources);
|
|
744
838
|
|
|
745
839
|
return html`
|
|
746
840
|
<div class="sources-list">
|
|
747
841
|
${this.sources.map(
|
|
748
842
|
(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
|
-
|
|
843
|
+
const sourceCalendars = this.calendars.filter(c => c.sourceId === source.id);
|
|
762
844
|
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"
|
|
845
|
+
<div class="source-item" @click=${() => this.collapsed ? this.toggleEnabled(source) : this.editSource(source)}>
|
|
846
|
+
<div
|
|
847
|
+
class="source-color ${source.enabled ? "" : "disabled"}"
|
|
848
|
+
style="background: ${source.color}; --source-color: ${source.color}"
|
|
849
|
+
></div>
|
|
850
|
+
<span class="source-name">${source.name}</span>
|
|
851
|
+
<input
|
|
852
|
+
type="checkbox"
|
|
853
|
+
class="source-enabled"
|
|
854
|
+
.checked=${source.enabled}
|
|
855
|
+
@click=${(e: Event) => e.stopPropagation()}
|
|
856
|
+
@change=${() => this.toggleEnabled(source)}
|
|
857
|
+
title="${source.enabled ? "Disable" : "Enable"} sync"
|
|
858
|
+
/>
|
|
859
|
+
</div>
|
|
860
|
+
${source.enabled && sourceCalendars.length > 0 ? html`
|
|
861
|
+
<div class="calendar-list">
|
|
862
|
+
${sourceCalendars.map(cal => html`
|
|
863
|
+
<div
|
|
864
|
+
class="calendar-item ${this.activeCalendarId === cal.id ? "active" : ""}"
|
|
865
|
+
@click=${() => this.selectCalendar(cal.id)}
|
|
796
866
|
>
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
867
|
+
<div class="calendar-color" style="background: ${cal.color}"></div>
|
|
868
|
+
<span class="calendar-name">${cal.name}</span>
|
|
869
|
+
</div>
|
|
870
|
+
`)}
|
|
800
871
|
</div>
|
|
801
|
-
|
|
802
|
-
</div>
|
|
872
|
+
` : null}
|
|
803
873
|
`;
|
|
804
874
|
}
|
|
805
875
|
)}
|
|
806
876
|
</div>
|
|
807
877
|
<button class="add-btn" @click=${this.addSource}>
|
|
808
|
-
+ Add
|
|
878
|
+
+ Add Source
|
|
809
879
|
</button>
|
|
810
880
|
`;
|
|
811
881
|
}
|
|
@@ -1021,6 +1091,11 @@ export class CalDAVConfigElement extends LitElement {
|
|
|
1021
1091
|
</div>
|
|
1022
1092
|
|
|
1023
1093
|
<div class="form-actions">
|
|
1094
|
+
${isEditing ? html`
|
|
1095
|
+
<button class="btn btn-danger" @click=${() => this.deleteSource(this.editingId!)}>
|
|
1096
|
+
Delete
|
|
1097
|
+
</button>
|
|
1098
|
+
` : null}
|
|
1024
1099
|
<button class="btn btn-secondary" @click=${this.cancelForm}>
|
|
1025
1100
|
Cancel
|
|
1026
1101
|
</button>
|
package/src/CalendarInternal.ts
CHANGED
|
@@ -185,6 +185,18 @@ export class CalendarInternal {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
|
|
188
|
+
applyEventOptimistically(event: CalendarEvent): void {
|
|
189
|
+
this.calendarEvents.set(event.id, event);
|
|
190
|
+
this.storage?.putEvent(event).catch(() => { /* sync will fix it */ });
|
|
191
|
+
this.sendEvents();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
removeEventOptimistically(eventId: string): void {
|
|
195
|
+
this.calendarEvents.delete(eventId);
|
|
196
|
+
this.storage?.deleteEvent(eventId).catch(() => { /* sync will fix it */ });
|
|
197
|
+
this.sendEvents();
|
|
198
|
+
}
|
|
199
|
+
|
|
188
200
|
setFilter(filter: string) {
|
|
189
201
|
this.filter = filter;
|
|
190
202
|
this.sendEvents();
|
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
|
}
|