@luckydye/calendar 1.1.0 → 1.1.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.
@@ -0,0 +1,91 @@
1
+ import type { CalendarEvent } from "./CalendarInternal.js";
2
+
3
+ export interface ScheduledNotification {
4
+ notificationId: string;
5
+ eventId: string;
6
+ eventTitle: string;
7
+ eventStart: string;
8
+ eventLocation?: string;
9
+ triggerTime: string;
10
+ triggerOffset: number;
11
+ }
12
+
13
+ export class NotificationScheduler {
14
+
15
+ constructor(private workerPromise: () => Promise<ServiceWorkerRegistration>) {}
16
+
17
+ async scheduleEventNotifications(event: CalendarEvent): Promise<void> {
18
+ await this.cancelEventNotifications(event.id);
19
+
20
+ if (!event.reminders?.length) return;
21
+
22
+ const notifications = this.buildScheduledNotifications(event);
23
+ await this.sendMessage({ type: "SCHEDULE_NOTIFICATIONS", notifications });
24
+ }
25
+
26
+ async cancelEventNotifications(eventId: string): Promise<void> {
27
+ await this.sendMessage({ type: "CANCEL_EVENT_NOTIFICATIONS", eventId });
28
+ }
29
+
30
+ async scheduleBatch(events: CalendarEvent[]): Promise<void> {
31
+ const allNotifications: ScheduledNotification[] = [];
32
+ for (const event of events) {
33
+ if (event.reminders?.length) {
34
+ allNotifications.push(...this.buildScheduledNotifications(event));
35
+ }
36
+ }
37
+
38
+ if (allNotifications.length > 0) {
39
+ await this.sendMessage({
40
+ type: "SCHEDULE_NOTIFICATIONS",
41
+ notifications: allNotifications,
42
+ });
43
+ }
44
+ }
45
+
46
+ async getScheduledNotifications(): Promise<ScheduledNotification[]> {
47
+ try {
48
+ const response = await this.sendMessage({ type: "GET_SCHEDULED_NOTIFICATIONS" });
49
+ return response.notifications || [];
50
+ } catch (error) {
51
+ console.error("Failed to get scheduled notifications:", error);
52
+ return [];
53
+ }
54
+ }
55
+
56
+ buildScheduledNotifications(event: CalendarEvent): ScheduledNotification[] {
57
+ if (!event.reminders) return [];
58
+
59
+ const now = new Date();
60
+ return event.reminders
61
+ .filter((n) => n.enabled)
62
+ .map((notif) => {
63
+ const triggerTime = new Date(
64
+ event.start.getTime() - notif.triggerOffset * 60000,
65
+ );
66
+ return {
67
+ notificationId: `${event.id}-${notif.triggerOffset}`,
68
+ eventId: event.id,
69
+ eventTitle: event.title,
70
+ eventStart: event.start.toISOString(),
71
+ eventLocation: event.location,
72
+ triggerTime: triggerTime.toISOString(),
73
+ triggerOffset: notif.triggerOffset,
74
+ };
75
+ })
76
+ .filter((n) => new Date(n.triggerTime) > now);
77
+ }
78
+
79
+ async sendMessage(message: any): Promise<any> {
80
+ const worker = await this.workerPromise?.();
81
+
82
+ return new Promise((resolve, reject) => {
83
+ const channel = new MessageChannel();
84
+ channel.port1.onmessage = (e) => {
85
+ if (e.data.type === "ERROR") reject(new Error(e.data.message));
86
+ else resolve(e.data);
87
+ };
88
+ worker.active?.postMessage(message, [channel.port2]);
89
+ });
90
+ }
91
+ }
@@ -0,0 +1,128 @@
1
+ import { LitElement, html, css } from "lit";
2
+ import "./StatusMessage.ts";
3
+
4
+ export interface StatusBarData {
5
+ currentTime: Date;
6
+ selectedEventsCount: number;
7
+ selectedDuration: string;
8
+ totalEventsCount: number;
9
+ cursorTime: { date: Date; time: string } | null;
10
+ formattedDate: string;
11
+ formattedTime: string;
12
+ formattedCursorDate: string;
13
+ }
14
+
15
+ export class StatusBarElement extends LitElement {
16
+ static properties = {
17
+ data: { type: Object },
18
+ };
19
+
20
+ data: StatusBarData | null = null;
21
+
22
+ static styles = css`
23
+ :host {
24
+ display: block;
25
+ position: relative;
26
+ z-index: 10;
27
+ }
28
+
29
+ .status-bar {
30
+ display: var(--statusbar-display, flex);
31
+ align-items: center;
32
+ justify-content: space-between;
33
+ padding: 8px 14px 10px 14px;
34
+ box-sizing: border-box;
35
+ border-top: 1px solid var(--grid-color, rgba(255, 255, 255, 0.1));
36
+ background: var(--bg-tertiary, rgba(0, 0, 0, 1));
37
+ font-size: 12px;
38
+ color: var(--text-muted, rgba(255, 255, 255, 0.4));
39
+ flex-shrink: 0;
40
+ }
41
+
42
+ .status-bar-left {
43
+ display: flex;
44
+ gap: 24px;
45
+ }
46
+
47
+ .status-bar-right {
48
+ display: flex;
49
+ gap: 16px;
50
+ }
51
+
52
+ .status-item {
53
+ display: flex;
54
+ align-items: center;
55
+ gap: 6px;
56
+ }
57
+
58
+ .status-label {
59
+ color: var(--text-muted, rgba(255, 255, 255, 0.4));
60
+ }
61
+
62
+ .status-value {
63
+ color: var(--text-secondary, rgba(255, 255, 255, 0.7));
64
+ }
65
+ `;
66
+
67
+ render() {
68
+ if (!this.data) {
69
+ return html``;
70
+ }
71
+
72
+ const hasSelection = this.data.selectedEventsCount > 0;
73
+
74
+ return html`
75
+ <div class="status-bar">
76
+ <div class="status-bar-left">
77
+ <div class="status-item">
78
+ <span class="status-value">${this.data.formattedDate}</span>
79
+ </div>
80
+ <div class="status-item">
81
+ <span class="status-value">${this.data.formattedTime}</span>
82
+ </div>
83
+
84
+ <status-message></status-message>
85
+ </div>
86
+
87
+ <div class="status-bar-right">
88
+ ${
89
+ hasSelection
90
+ ? html`
91
+ <div class="status-item">
92
+ <span class="status-label">Selected:</span>
93
+ <span class="status-value">${this.data.selectedEventsCount} event${
94
+ this.data.selectedEventsCount === 1 ? "" : "s"
95
+ }</span>
96
+ </div>
97
+ <div class="status-item">
98
+ <span class="status-label">Duration:</span>
99
+ <span class="status-value">${this.data.selectedDuration}</span>
100
+ </div>
101
+ `
102
+ : html`
103
+ <div class="status-item">
104
+ <span class="status-label">Total:</span>
105
+ <span class="status-value">${this.data.totalEventsCount} event${
106
+ this.data.totalEventsCount === 1 ? "" : "s"
107
+ }</span>
108
+ </div>
109
+ `
110
+ }
111
+
112
+ ${
113
+ this.data.cursorTime
114
+ ? html`
115
+ <div class="status-item">
116
+ <span class="status-label">→</span>
117
+ <span class="status-value">${this.data.formattedCursorDate} ${this.data.cursorTime.time}</span>
118
+ </div>
119
+ `
120
+ : html``
121
+ }
122
+ </div>
123
+ </div>
124
+ `;
125
+ }
126
+ }
127
+
128
+ customElements.define("status-bar", StatusBarElement);
@@ -0,0 +1,122 @@
1
+ import { css, html, LitElement } from "lit";
2
+
3
+ interface QueuedMessage {
4
+ text: string;
5
+ timestamp: number;
6
+ }
7
+
8
+ const messageQueue: QueuedMessage[] = [];
9
+ let activeElement: StatusMessageElement | null = null;
10
+
11
+ export function queueStatus(message: string): void {
12
+ messageQueue.push({
13
+ text: message,
14
+ timestamp: Date.now(),
15
+ });
16
+
17
+ if (activeElement) {
18
+ activeElement.processQueue();
19
+ }
20
+ }
21
+
22
+ export class StatusMessageElement extends LitElement {
23
+ static styles = css`
24
+ :host {
25
+ display: block;
26
+ position: relative;
27
+ }
28
+
29
+ .message {
30
+ background: var(--bg-tertiary, rgba(80, 80, 80, 0.9));
31
+ border-radius: 4px;
32
+ font-size: 12px;
33
+ color: var(--text-primary, rgba(255, 255, 255, 0.9));
34
+ opacity: 0;
35
+ animation: fadeIn 0.2s forwards;
36
+ }
37
+
38
+ @keyframes fadeIn {
39
+ to {
40
+ opacity: 1;
41
+ }
42
+ }
43
+
44
+ .message.fade-out {
45
+ animation: fadeOut 0.2s forwards;
46
+ }
47
+
48
+ @keyframes fadeOut {
49
+ to {
50
+ opacity: 0;
51
+ }
52
+ }
53
+ `;
54
+
55
+ static properties = {
56
+ currentMessage: { type: String },
57
+ isShowing: { type: Boolean },
58
+ };
59
+
60
+ currentMessage = "";
61
+ isShowing = false;
62
+
63
+ timeoutId: number | null = null;
64
+
65
+ connectedCallback(): void {
66
+ super.connectedCallback();
67
+ activeElement = this;
68
+ this.processQueue();
69
+ }
70
+
71
+ disconnectedCallback(): void {
72
+ super.disconnectedCallback();
73
+ if (activeElement === this) {
74
+ activeElement = null;
75
+ }
76
+ if (this.timeoutId !== null) {
77
+ clearTimeout(this.timeoutId);
78
+ }
79
+ }
80
+
81
+ processQueue(): void {
82
+ if (this.isShowing || messageQueue.length === 0) return;
83
+
84
+ const message = messageQueue.shift();
85
+ if (!message) return;
86
+
87
+ this.currentMessage = message.text;
88
+ this.isShowing = true;
89
+ this.requestUpdate();
90
+
91
+ this.timeoutId = window.setTimeout(() => {
92
+ this.hideMessage();
93
+ }, 2000);
94
+ }
95
+
96
+ hideMessage(): void {
97
+ const messageEl = this.shadowRoot?.querySelector(".message");
98
+ if (messageEl) {
99
+ messageEl.classList.add("fade-out");
100
+ setTimeout(() => {
101
+ this.isShowing = false;
102
+ this.currentMessage = "";
103
+ this.requestUpdate();
104
+ this.processQueue();
105
+ }, 200);
106
+ }
107
+ }
108
+
109
+ render() {
110
+ if (!this.isShowing || !this.currentMessage) {
111
+ return html``;
112
+ }
113
+
114
+ return html`<div class="message">${this.currentMessage}</div>`;
115
+ }
116
+ }
117
+
118
+ try {
119
+ customElements.define("status-message", StatusMessageElement);
120
+ } catch (error) {
121
+ console.error("Failed to register custom element:", error);
122
+ }
package/src/Theme.ts ADDED
@@ -0,0 +1,228 @@
1
+ export type ThemeName = "auto" | "dark" | "light" | "solarized" | "high-contrast";
2
+
3
+ export type ConcreteThemeName = "dark" | "light" | "solarized" | "high-contrast";
4
+
5
+ export interface ThemeDefinition {
6
+ name: ThemeName;
7
+ label: string;
8
+ variables: Record<string, string>;
9
+ }
10
+
11
+ export const themes: Record<ConcreteThemeName, ThemeDefinition> = {
12
+ dark: {
13
+ name: "dark",
14
+ label: "Dark",
15
+ variables: {
16
+ "--bg-primary": "rgb(30, 30, 30)",
17
+ "--bg-secondary": "hsl(0deg 0% 0% / 50%)",
18
+ "--bg-tertiary": "hsl(0deg 0% 8% / 1)",
19
+ "--bg-elevated": "#1E1E1E",
20
+ "--bg-input": "rgba(0, 0, 0, 0.3)",
21
+ "--bg-input-focus": "rgba(0, 0, 0, 0.5)",
22
+ "--bg-button-hover": "rgba(255, 255, 255, 0.05)",
23
+ "--bg-button-active": "rgba(255, 255, 255, 0.1)",
24
+ "--bg-today": "rgba(255, 255, 255, 0.05)",
25
+ "--bg-selection": "rgba(255, 255, 255, 0.1)",
26
+ "--bg-weekend": "rgba(255, 255, 255, 0.03)",
27
+ "--bg-item": "rgba(255, 255, 255, 0.05)",
28
+ "--bg-item-hover": "rgba(255, 255, 255, 0.08)",
29
+ "--grid-color": "rgba(255, 255, 255, 0.1)",
30
+ "--grid-color-hover": "rgba(255, 255, 255, 0.2)",
31
+ "--grid-color-strong": "rgba(255, 255, 255, 0.3)",
32
+ "--text-primary": "rgba(255, 255, 255, 0.9)",
33
+ "--text-secondary": "rgba(255, 255, 255, 0.7)",
34
+ "--text-muted": "rgba(255, 255, 255, 0.4)",
35
+ "--text-inverse": "rgb(0, 0, 0)",
36
+ "--accent-primary": "rgb(100, 150, 255)",
37
+ "--accent-success": "rgb(124, 179, 66)",
38
+ "--accent-error": "rgb(229, 57, 53)",
39
+ "--accent-current-time": "rgba(255, 0, 0, 0.8)",
40
+ "--event-default": "#9b59b6",
41
+ "--selection-bg": "200, 200, 255",
42
+ "--shadow-overlay": "0 4px 120px 10px rgba(0, 0, 0, 0.5)",
43
+ "--border-radius": "8px",
44
+ "--border-radius-sm": "4px",
45
+ "--border-radius-lg": "8px",
46
+ },
47
+ },
48
+ light: {
49
+ name: "light",
50
+ label: "Light",
51
+ variables: {
52
+ "--bg-primary": "rgb(245, 245, 245)",
53
+ "--bg-secondary": "rgba(255, 255, 255, 0.5)",
54
+ "--bg-tertiary": "rgb(245, 245, 245)",
55
+ "--bg-elevated": "rgb(245, 245, 245)",
56
+ "--bg-input": "rgba(0, 0, 0, 0.05)",
57
+ "--bg-input-focus": "rgba(0, 0, 0, 0.08)",
58
+ "--bg-button-hover": "rgba(0, 0, 0, 0.05)",
59
+ "--bg-button-active": "rgba(0, 0, 0, 0.08)",
60
+ "--bg-today": "rgba(0, 100, 255, 0.08)",
61
+ "--bg-selection": "rgba(0, 100, 255, 0.15)",
62
+ "--bg-weekend": "rgba(0, 0, 0, 0.02)",
63
+ "--bg-item": "rgba(0, 0, 0, 0.03)",
64
+ "--bg-item-hover": "rgba(0, 0, 0, 0.06)",
65
+ "--grid-color": "rgba(0, 0, 0, 0.08)",
66
+ "--grid-color-hover": "rgba(0, 0, 0, 0.15)",
67
+ "--grid-color-strong": "rgba(0, 0, 0, 0.25)",
68
+ "--text-primary": "rgba(0, 0, 0, 0.85)",
69
+ "--text-secondary": "rgba(0, 0, 0, 0.6)",
70
+ "--text-muted": "rgba(0, 0, 0, 0.4)",
71
+ "--text-inverse": "rgb(255, 255, 255)",
72
+ "--accent-primary": "rgb(50, 120, 220)",
73
+ "--accent-success": "rgb(76, 175, 80)",
74
+ "--accent-error": "rgb(244, 67, 54)",
75
+ "--accent-current-time": "rgba(220, 50, 50, 0.9)",
76
+ "--event-default": "#9b59b6",
77
+ "--selection-bg": "50, 120, 220",
78
+ "--shadow-overlay": "0 4px 20px rgba(0, 0, 0, 0.15)",
79
+ "--border-radius": "8px",
80
+ "--border-radius-sm": "4px",
81
+ "--border-radius-lg": "8px",
82
+ },
83
+ },
84
+ solarized: {
85
+ name: "solarized",
86
+ label: "Solarized",
87
+ variables: {
88
+ "--bg-primary": "rgb(0, 43, 54)",
89
+ "--bg-secondary": "rgb(7, 54, 66)",
90
+ "--bg-tertiary": "rgba(0, 0, 0, 0.3)",
91
+ "--bg-elevated": "rgb(0, 43, 54)",
92
+ "--bg-input": "rgba(0, 0, 0, 0.3)",
93
+ "--bg-input-focus": "rgba(0, 0, 0, 0.4)",
94
+ "--bg-button-hover": "rgba(255, 255, 255, 0.05)",
95
+ "--bg-button-active": "rgba(255, 255, 255, 0.1)",
96
+ "--bg-today": "rgba(181, 137, 0, 0.15)",
97
+ "--bg-selection": "rgba(181, 137, 0, 0.25)",
98
+ "--bg-weekend": "rgba(255, 255, 255, 0.03)",
99
+ "--bg-item": "rgba(255, 255, 255, 0.05)",
100
+ "--bg-item-hover": "rgba(255, 255, 255, 0.08)",
101
+ "--grid-color": "rgba(131, 148, 150, 0.2)",
102
+ "--grid-color-hover": "rgba(131, 148, 150, 0.3)",
103
+ "--grid-color-strong": "rgba(131, 148, 150, 0.4)",
104
+ "--text-primary": "rgb(131, 148, 150)",
105
+ "--text-secondary": "rgb(147, 161, 161)",
106
+ "--text-muted": "rgba(131, 148, 150, 0.6)",
107
+ "--text-inverse": "rgb(0, 43, 54)",
108
+ "--accent-primary": "rgb(38, 139, 210)",
109
+ "--accent-success": "rgb(133, 153, 0)",
110
+ "--accent-error": "rgb(220, 50, 47)",
111
+ "--accent-current-time": "rgb(203, 75, 22)",
112
+ "--event-default": "#b58900",
113
+ "--selection-bg": "38, 139, 210",
114
+ "--shadow-overlay": "0 4px 20px rgba(0, 0, 0, 0.5)",
115
+ "--border-radius": "8px",
116
+ "--border-radius-sm": "4px",
117
+ "--border-radius-lg": "8px",
118
+ },
119
+ },
120
+ "high-contrast": {
121
+ name: "high-contrast",
122
+ label: "High Contrast",
123
+ variables: {
124
+ "--bg-primary": "rgb(0, 0, 0)",
125
+ "--bg-secondary": "rgb(20, 20, 20)",
126
+ "--bg-tertiary": "rgb(40, 40, 40)",
127
+ "--bg-elevated": "rgb(0, 0, 0)",
128
+ "--bg-input": "rgb(20, 20, 20)",
129
+ "--bg-input-focus": "rgb(40, 40, 40)",
130
+ "--bg-button-hover": "rgb(40, 40, 40)",
131
+ "--bg-button-active": "rgb(60, 60, 60)",
132
+ "--bg-today": "rgb(30, 30, 30)",
133
+ "--bg-selection": "rgb(255, 255, 0)",
134
+ "--bg-weekend": "rgba(255, 255, 255, 0.05)",
135
+ "--bg-item": "rgb(20, 20, 20)",
136
+ "--bg-item-hover": "rgb(40, 40, 40)",
137
+ "--grid-color": "rgb(80, 80, 80)",
138
+ "--grid-color-hover": "rgb(120, 120, 120)",
139
+ "--grid-color-strong": "rgb(160, 160, 160)",
140
+ "--text-primary": "rgb(255, 255, 255)",
141
+ "--text-secondary": "rgb(200, 200, 200)",
142
+ "--text-muted": "rgb(150, 150, 150)",
143
+ "--text-inverse": "rgb(0, 0, 0)",
144
+ "--accent-primary": "rgb(0, 255, 255)",
145
+ "--accent-success": "rgb(0, 255, 0)",
146
+ "--accent-error": "rgb(255, 0, 0)",
147
+ "--accent-current-time": "rgb(255, 0, 0)",
148
+ "--event-default": "rgb(255, 255, 0)",
149
+ "--selection-bg": "255, 255, 0",
150
+ "--shadow-overlay": "0 4px 20px rgba(255, 255, 255, 0.2)",
151
+ "--border-radius": "4px",
152
+ "--border-radius-sm": "2px",
153
+ "--border-radius-lg": "4px",
154
+ },
155
+ },
156
+ };
157
+
158
+ let mediaQuery: MediaQueryList | null = null;
159
+ let onSystemThemeChange: (() => void) | null = null;
160
+
161
+ export function getSystemTheme(): ConcreteThemeName {
162
+ return window.matchMedia?.('(prefers-color-scheme: dark)')?.matches ? "dark" : "light";
163
+ }
164
+
165
+ export function getEffectiveTheme(themeName: ThemeName): ConcreteThemeName {
166
+ if (themeName === "auto") {
167
+ return getSystemTheme();
168
+ }
169
+ return themeName;
170
+ }
171
+
172
+ function updateThemeMetaTag(bgColor: string): void {
173
+ const meta = document.querySelector('meta[name="theme-color"]');
174
+ if (meta) {
175
+ meta.setAttribute("content", bgColor);
176
+ }
177
+ }
178
+
179
+ export function applyTheme(themeName: ThemeName, element: HTMLElement = document.body): void {
180
+ const effectiveTheme = getEffectiveTheme(themeName);
181
+ const theme = themes[effectiveTheme];
182
+ if (!theme) return;
183
+
184
+ for (const [key, value] of Object.entries(theme.variables)) {
185
+ element.style.setProperty(key, value);
186
+ }
187
+
188
+ element.setAttribute("data-theme", effectiveTheme);
189
+ updateThemeMetaTag(theme.variables["--bg-primary"]);
190
+ }
191
+
192
+ export function getCurrentTheme(): ThemeName {
193
+ return (document.body.getAttribute("data-theme") as ThemeName) || "dark";
194
+ }
195
+
196
+ export function saveThemePreference(themeName: ThemeName): void {
197
+ localStorage.setItem("calendar-theme", themeName);
198
+ }
199
+
200
+ export function loadThemePreference(): ThemeName {
201
+ const saved = localStorage.getItem("calendar-theme");
202
+ if (saved && (saved in themes || saved === "auto")) {
203
+ return saved as ThemeName;
204
+ }
205
+ return "auto";
206
+ }
207
+
208
+ export function initializeTheme(): void {
209
+ const theme = loadThemePreference();
210
+ applyTheme(theme);
211
+
212
+ // Listen for system theme changes when in auto mode
213
+ mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
214
+ onSystemThemeChange = () => {
215
+ if (loadThemePreference() === "auto") {
216
+ applyTheme("auto");
217
+ }
218
+ };
219
+ mediaQuery.addEventListener('change', onSystemThemeChange);
220
+ }
221
+
222
+ export const availableThemes = [
223
+ { name: "auto", label: "Auto" },
224
+ ...Object.values(themes).map((t) => ({
225
+ name: t.name,
226
+ label: t.label,
227
+ })),
228
+ ];
package/src/app.css ADDED
@@ -0,0 +1,4 @@
1
+ caldav-config {
2
+ height: 100%;
3
+ box-shadow: 0 0 24px rgba(0, 0, 0, 0.4);
4
+ }