@luckydye/calendar 1.2.2 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luckydye/calendar",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "author": "Tim Havlicek",
5
5
  "contributors": [],
6
6
  "description": "",
@@ -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
- border: 1px solid var(--grid-color, rgba(255, 255, 255, 0.1));
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: 16px;
73
- padding-bottom: 12px;
74
- border-bottom: 1px solid var(--grid-color, rgba(255, 255, 255, 0.1));
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: 16px;
92
+ font-size: 11px;
79
93
  font-weight: 600;
80
- color: var(--text-primary, rgba(255, 255, 255, 0.9));
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
- .close-btn {
103
+ .collapse-btn {
84
104
  background: none;
85
105
  border: none;
86
- color: var(--text-muted, rgba(255, 255, 255, 0.5));
87
- font-size: 20px;
106
+ color: var(--text-muted, rgba(255, 255, 255, 0.4));
88
107
  cursor: pointer;
89
- padding: 4px;
108
+ padding: 2px;
109
+ font-size: 12px;
90
110
  line-height: 1;
91
111
  }
92
112
 
93
- .close-btn:hover {
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: 12px;
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
- .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;
126
+ :host([collapsed]) .sources-list {
127
+ align-items: center;
128
+ gap: 4px;
129
+ margin-bottom: 0;
113
130
  }
114
131
 
115
- .source-header {
132
+ .source-item {
133
+ flex: none;
116
134
  display: flex;
117
135
  align-items: center;
118
136
  gap: 8px;
119
- margin-bottom: 8px;
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: 12px;
124
- height: 12px;
125
- border-radius: var(--border-radius-sm, 2px);
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-weight: 500;
165
+ font-size: 13px;
132
166
  color: var(--text-primary, rgba(255, 255, 255, 0.9));
133
- font-size: 14px;
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
- .source-locked {
141
- cursor: pointer;
142
- margin-left: 8px;
183
+ .calendar-list {
184
+ display: flex;
185
+ flex-direction: column;
143
186
  }
144
187
 
145
- .source-actions {
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
- .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;
199
+ .calendar-item:hover {
200
+ background: var(--bg-item, rgba(255, 255, 255, 0.05));
158
201
  }
159
202
 
160
- .icon-btn:hover {
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
- .source-url {
165
- font-size: 12px;
166
- color: var(--text-muted, rgba(255, 255, 255, 0.4));
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: 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));
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: 14px;
241
+ font-size: 12px;
185
242
  transition: all 0.15s;
186
243
  }
187
244
 
188
245
  .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));
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: 28px;
236
- height: 28px;
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">Calendar Sources</span>
721
- <button class="close-btn" @click=${this.close}>×</button>
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.isAdding || this.editingId
725
- ? this.renderForm(colors)
726
- : this.renderSourcesList()}
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 Calendar Source
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
- 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
-
843
+ const sourceCalendars = this.calendars.filter(c => c.sourceId === source.id);
762
844
  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"
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
- </button>
799
- </div>
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
- <div class="source-url">${source.type.toUpperCase()}: ${urlDisplay || "No URL"}</div>
802
- </div>
872
+ ` : null}
803
873
  `;
804
874
  }
805
875
  )}
806
876
  </div>
807
877
  <button class="add-btn" @click=${this.addSource}>
808
- + Add Calendar Source
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>
@@ -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();
@@ -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
  }