@luckydye/calendar 1.1.1 → 1.1.3

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,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,32 @@
1
+ caldav-config {
2
+ height: 100%;
3
+ box-shadow: 0 0 24px rgba(0, 0, 0, 0.4);
4
+ }
5
+
6
+ .toolbar-button {
7
+ background: transparent;
8
+ border: 1px solid var(--grid-color, rgba(255, 255, 255, 0.1));
9
+ color: var(--text-primary, rgba(255, 255, 255, 0.9));
10
+ padding: 4px 8px;
11
+ border-radius: var(--border-radius-sm, 4px);
12
+ font-size: 13px;
13
+ line-height: 16px;
14
+ cursor: pointer;
15
+ display: flex;
16
+ align-items: center;
17
+ gap: 6px;
18
+ transition: background 0.15s, border-color 0.15s;
19
+ }
20
+
21
+ .toolbar-button:hover {
22
+ background: var(--bg-button-hover, rgba(255, 255, 255, 0.05));
23
+ border-color: var(--grid-color-hover, rgba(255, 255, 255, 0.2));
24
+ }
25
+
26
+ .toolbar-button:active {
27
+ background: var(--bg-button-active, rgba(255, 255, 255, 0.1));
28
+ }
29
+
30
+ .toolbar-button[disabled] {
31
+ opacity: 0.5;
32
+ }