eye-care 1.0.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.
Files changed (63) hide show
  1. package/AGENTS.md +48 -0
  2. package/CODE_OF_CONDUCT.md +44 -0
  3. package/CONTRIBUTING.md +67 -0
  4. package/LICENSE +21 -0
  5. package/README.md +178 -0
  6. package/bin/eye-care.js +22 -0
  7. package/build/icon.ico +0 -0
  8. package/build/icon.png +0 -0
  9. package/build/tray.png +0 -0
  10. package/build/tray@2x.png +0 -0
  11. package/out/data/exercises.js +120 -0
  12. package/out/data/exercises.js.map +1 -0
  13. package/out/data/i18n.js +211 -0
  14. package/out/data/i18n.js.map +1 -0
  15. package/out/main/backgrounds.js +180 -0
  16. package/out/main/backgrounds.js.map +1 -0
  17. package/out/main/index.js +258 -0
  18. package/out/main/index.js.map +1 -0
  19. package/out/main/preferences.js +71 -0
  20. package/out/main/preferences.js.map +1 -0
  21. package/out/main/scheduler.js +106 -0
  22. package/out/main/scheduler.js.map +1 -0
  23. package/out/preload/break.js +9 -0
  24. package/out/preload/break.js.map +1 -0
  25. package/out/preload/settings.js +18 -0
  26. package/out/preload/settings.js.map +1 -0
  27. package/out/renderer/backgrounds/forest.svg +13 -0
  28. package/out/renderer/backgrounds/mountains.svg +13 -0
  29. package/out/renderer/backgrounds/sea.svg +13 -0
  30. package/out/renderer/backgrounds/sunny-sky.svg +16 -0
  31. package/out/renderer/backgrounds/sunset.svg +18 -0
  32. package/out/renderer/break.css +265 -0
  33. package/out/renderer/break.html +38 -0
  34. package/out/renderer/break.js +452 -0
  35. package/out/renderer/settings.css +198 -0
  36. package/out/renderer/settings.html +110 -0
  37. package/out/renderer/settings.js +407 -0
  38. package/out/shared/types.js +23 -0
  39. package/out/shared/types.js.map +1 -0
  40. package/package.json +100 -0
  41. package/scripts/copy-static.js +27 -0
  42. package/scripts/make-icon.js +120 -0
  43. package/src/data/exercises.ts +120 -0
  44. package/src/data/i18n.ts +297 -0
  45. package/src/main/backgrounds.ts +144 -0
  46. package/src/main/index.ts +233 -0
  47. package/src/main/preferences.ts +34 -0
  48. package/src/main/scheduler.ts +105 -0
  49. package/src/preload/break.ts +9 -0
  50. package/src/preload/settings.ts +18 -0
  51. package/src/renderer/backgrounds/forest.svg +13 -0
  52. package/src/renderer/backgrounds/mountains.svg +13 -0
  53. package/src/renderer/backgrounds/sea.svg +13 -0
  54. package/src/renderer/backgrounds/sunny-sky.svg +16 -0
  55. package/src/renderer/backgrounds/sunset.svg +18 -0
  56. package/src/renderer/break.css +265 -0
  57. package/src/renderer/break.html +38 -0
  58. package/src/renderer/break.js +452 -0
  59. package/src/renderer/settings.css +198 -0
  60. package/src/renderer/settings.html +110 -0
  61. package/src/renderer/settings.js +407 -0
  62. package/src/shared/types.ts +82 -0
  63. package/tsconfig.json +19 -0
@@ -0,0 +1,233 @@
1
+ import { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage, shell, BrowserWindowConstructorOptions } from "electron";
2
+ import * as path from "path";
3
+ import * as fs from "fs";
4
+ import { Preferences, DEFAULT_PREFERENCES, BreakType, BreakPlan, Language, BackgroundConfig } from "../shared/types";
5
+ import { exercisesForBreak } from "../data/exercises";
6
+ import { t, TranslationKey } from "../data/i18n";
7
+ import { loadPreferences, savePreferences } from "./preferences";
8
+ import { Scheduler } from "./scheduler";
9
+ import { registerBackgroundIpc, resolveBackgroundPath, pickRandomBackground } from "./backgrounds";
10
+
11
+ let tray: Tray | null = null;
12
+ let breakWindow: BrowserWindow | null = null;
13
+ let settingsWindow: BrowserWindow | null = null;
14
+ let scheduler: Scheduler | null = null;
15
+ let preferences: Preferences = { ...DEFAULT_PREFERENCES };
16
+
17
+ function windowOptionsForOverlay(): BrowserWindowConstructorOptions {
18
+ return {
19
+ width: 600,
20
+ height: 400,
21
+ fullscreen: true,
22
+ alwaysOnTop: true,
23
+ frame: false,
24
+ movable: false,
25
+ resizable: false,
26
+ skipTaskbar: true,
27
+ show: false,
28
+ backgroundColor: "#0b1020",
29
+ webPreferences: {
30
+ preload: path.join(__dirname, "..", "preload", "break.js"),
31
+ contextIsolation: true,
32
+ nodeIntegration: false,
33
+ sandbox: false,
34
+ },
35
+ };
36
+ }
37
+
38
+ function showBreakWindow(plan: BreakPlan): void {
39
+ if (breakWindow && !breakWindow.isDestroyed()) {
40
+ breakWindow.close();
41
+ }
42
+ breakWindow = new BrowserWindow(windowOptionsForOverlay());
43
+ breakWindow.loadFile(path.join(__dirname, "..", "renderer", "break.html"));
44
+ breakWindow.once("ready-to-show", () => {
45
+ breakWindow?.webContents.send("break:start", plan);
46
+ breakWindow?.show();
47
+ });
48
+ breakWindow.on("closed", () => {
49
+ breakWindow = null;
50
+ scheduler?.resumeAfterBreak();
51
+ });
52
+ }
53
+
54
+ function buildBreakPlan(type: BreakType): BreakPlan {
55
+ const duration =
56
+ type === "mini"
57
+ ? preferences.miniBreakDurationSeconds
58
+ : preferences.longBreakDurationSeconds;
59
+ let bgData: { mime: string; base64: string } | null = null;
60
+ if (preferences.background.mode !== "none") {
61
+ let file: string | null = null;
62
+ if (preferences.background.mode === "random") {
63
+ const pool = preferences.background.randomPool || "all";
64
+ file = pickRandomBackground(pool);
65
+ } else {
66
+ file = resolveBackgroundPath(preferences.background.selected, preferences.background.mode);
67
+ }
68
+ if (file) {
69
+ try {
70
+ const buf = fs.readFileSync(file);
71
+ const ext = path.extname(file).slice(1).toLowerCase();
72
+ const mime = ext === "svg" ? "image/svg+xml" : "image/" + ext;
73
+ bgData = { mime, base64: buf.toString("base64") };
74
+ } catch (err) {
75
+ console.error("Failed to read background:", err);
76
+ }
77
+ }
78
+ }
79
+ return {
80
+ type,
81
+ totalDurationSeconds: duration,
82
+ language: preferences.language,
83
+ soundEnabled: preferences.soundEnabled,
84
+ background: bgData ? { ...bgData, blurStrength: preferences.background.blurStrength ?? 3 } : null,
85
+ exercises: exercisesForBreak(type, duration),
86
+ };
87
+ }
88
+
89
+ function showSettingsWindow(): void {
90
+ if (settingsWindow && !settingsWindow.isDestroyed()) {
91
+ settingsWindow.focus();
92
+ return;
93
+ }
94
+ settingsWindow = new BrowserWindow({
95
+ width: 520,
96
+ height: 640,
97
+ title: "eye-care — Settings",
98
+ resizable: false,
99
+ backgroundColor: "#f7f8fa",
100
+ webPreferences: {
101
+ preload: path.join(__dirname, "..", "preload", "settings.js"),
102
+ contextIsolation: true,
103
+ nodeIntegration: false,
104
+ sandbox: false,
105
+ },
106
+ });
107
+ settingsWindow.loadFile(path.join(__dirname, "..", "renderer", "settings.html"));
108
+ settingsWindow.on("closed", () => {
109
+ settingsWindow = null;
110
+ });
111
+ }
112
+
113
+ function buildTrayMenu(): Menu {
114
+ const lang = preferences.language;
115
+ const tt = (k: TranslationKey) => t(lang, k);
116
+ const nextLabel = scheduler?.nextBreakLabel(lang) ?? tt("noBreakScheduled");
117
+ return Menu.buildFromTemplate([
118
+ { label: tt("appTitle"), enabled: false },
119
+ { type: "separator" },
120
+ { label: nextLabel, enabled: false },
121
+ { type: "separator" },
122
+ {
123
+ label: tt("trayTakeMiniNow"),
124
+ click: () => scheduler?.triggerNow("mini"),
125
+ },
126
+ {
127
+ label: tt("trayTakeLongNow"),
128
+ click: () => scheduler?.triggerNow("long"),
129
+ },
130
+ { type: "separator" },
131
+ {
132
+ label: tt("trayPause1Hour"),
133
+ click: () => scheduler?.pauseFor(60 * 60 * 1000),
134
+ },
135
+ {
136
+ label: tt("trayResume"),
137
+ click: () => scheduler?.resume(),
138
+ },
139
+ { type: "separator" },
140
+ {
141
+ label: tt("traySettings"),
142
+ click: () => showSettingsWindow(),
143
+ },
144
+ {
145
+ label: tt("trayAbout"),
146
+ click: () => {
147
+ shell.openExternal("https://github.com/uptodatelabs/eye-care");
148
+ },
149
+ },
150
+ { type: "separator" },
151
+ {
152
+ label: tt("trayQuit"),
153
+ click: () => app.quit(),
154
+ },
155
+ ]);
156
+ }
157
+
158
+ function updateTrayMenu(): void {
159
+ if (tray) tray.setContextMenu(buildTrayMenu());
160
+ }
161
+
162
+ function createTray(): void {
163
+ const iconPath = path.join(__dirname, "..", "..", "build", "tray.png");
164
+ let icon = nativeImage.createEmpty();
165
+ if (fs.existsSync(iconPath)) {
166
+ icon = nativeImage.createFromPath(iconPath);
167
+ if (process.platform === "darwin") {
168
+ icon.setTemplateImage(true);
169
+ }
170
+ }
171
+ tray = new Tray(icon);
172
+ tray.setToolTip("eye-care");
173
+ tray.on("double-click", () => showSettingsWindow());
174
+ updateTrayMenu();
175
+ }
176
+
177
+ function registerIpc(): void {
178
+ ipcMain.handle("prefs:get", () => preferences);
179
+ ipcMain.handle("prefs:update", (_e, next: Partial<Preferences>) => {
180
+ preferences = { ...preferences, ...next };
181
+ savePreferences(preferences);
182
+ scheduler?.applyPreferences(preferences);
183
+ updateTrayMenu();
184
+ return preferences;
185
+ });
186
+ ipcMain.handle("app:info", () => {
187
+ return {
188
+ version: app.getVersion(),
189
+ author: "uptodatelabs",
190
+ name: "eye-care",
191
+ homepage: "https://github.com/uptodatelabs/eye-care",
192
+ };
193
+ });
194
+ ipcMain.handle("break:skip", () => {
195
+ if (breakWindow && !breakWindow.isDestroyed()) breakWindow.close();
196
+ });
197
+ ipcMain.handle("break:openSource", (_e, url: string) => {
198
+ if (/^https:\/\//.test(url)) shell.openExternal(url);
199
+ });
200
+ }
201
+
202
+ app.whenReady().then(() => {
203
+ Menu.setApplicationMenu(null);
204
+ preferences = loadPreferences();
205
+ createTray();
206
+ registerIpc();
207
+ registerBackgroundIpc(
208
+ () => preferences.background,
209
+ (c) => {
210
+ preferences.background = c;
211
+ savePreferences(preferences);
212
+ }
213
+ );
214
+ scheduler = new Scheduler(preferences, (type) => {
215
+ showBreakWindow(buildBreakPlan(type));
216
+ updateTrayMenu();
217
+ });
218
+ scheduler.start();
219
+ if (preferences.firstRun) {
220
+ showSettingsWindow();
221
+ preferences.firstRun = false;
222
+ savePreferences(preferences);
223
+ }
224
+ });
225
+
226
+ app.on("window-all-closed", () => {
227
+ // Do nothing — keep app running in tray. Prevents default quit behavior.
228
+ });
229
+
230
+ app.on("before-quit", () => {
231
+ scheduler?.stop();
232
+ tray?.destroy();
233
+ });
@@ -0,0 +1,34 @@
1
+ import { app, ipcMain } from "electron";
2
+ import * as path from "path";
3
+ import * as fs from "fs";
4
+ import { Preferences, DEFAULT_PREFERENCES } from "../shared/types";
5
+
6
+ function preferencesPath(): string {
7
+ return path.join(app.getPath("userData"), "preferences.json");
8
+ }
9
+
10
+ export function loadPreferences(): Preferences {
11
+ try {
12
+ const file = preferencesPath();
13
+ if (!fs.existsSync(file)) return { ...DEFAULT_PREFERENCES };
14
+ const raw = fs.readFileSync(file, "utf8");
15
+ const parsed = JSON.parse(raw) as Partial<Preferences>;
16
+ return { ...DEFAULT_PREFERENCES, ...parsed };
17
+ } catch {
18
+ return { ...DEFAULT_PREFERENCES };
19
+ }
20
+ }
21
+
22
+ export function savePreferences(prefs: Preferences): void {
23
+ try {
24
+ fs.writeFileSync(preferencesPath(), JSON.stringify(prefs, null, 2), "utf8");
25
+ } catch (err) {
26
+ console.error("Failed to save preferences:", err);
27
+ }
28
+ }
29
+
30
+ ipcMain.handle("prefs:load", () => loadPreferences());
31
+ ipcMain.handle("prefs:save", (_e, prefs: Preferences) => {
32
+ savePreferences(prefs);
33
+ return prefs;
34
+ });
@@ -0,0 +1,105 @@
1
+ import { Preferences, BreakType, Language } from "../shared/types";
2
+ import { t, TranslationKey } from "../data/i18n";
3
+
4
+ type BreakCallback = (type: BreakType) => void;
5
+
6
+ export class Scheduler {
7
+ private prefs: Preferences;
8
+ private onBreak: BreakCallback;
9
+ private miniTimer: NodeJS.Timeout | null = null;
10
+ private longTimer: NodeJS.Timeout | null = null;
11
+ private pausedUntil = 0;
12
+ private nextMiniAt = 0;
13
+ private nextLongAt = 0;
14
+
15
+ constructor(prefs: Preferences, onBreak: BreakCallback) {
16
+ this.prefs = prefs;
17
+ this.onBreak = onBreak;
18
+ }
19
+
20
+ start(): void {
21
+ this.scheduleNext();
22
+ }
23
+
24
+ stop(): void {
25
+ if (this.miniTimer) clearTimeout(this.miniTimer);
26
+ if (this.longTimer) clearTimeout(this.longTimer);
27
+ this.miniTimer = null;
28
+ this.longTimer = null;
29
+ }
30
+
31
+ applyPreferences(prefs: Preferences): void {
32
+ this.prefs = prefs;
33
+ this.stop();
34
+ this.scheduleNext();
35
+ }
36
+
37
+ private scheduleNext(): void {
38
+ const now = Date.now();
39
+ if (this.prefs.miniBreakEnabled) {
40
+ this.nextMiniAt = now + this.prefs.miniBreakIntervalMinutes * 60 * 1000;
41
+ const delay = Math.max(1000, this.nextMiniAt - now);
42
+ this.miniTimer = setTimeout(() => this.fire("mini"), delay);
43
+ }
44
+ if (this.prefs.longBreakEnabled) {
45
+ this.nextLongAt = now + this.prefs.longBreakIntervalMinutes * 60 * 1000;
46
+ const delay = Math.max(1000, this.nextLongAt - now);
47
+ this.longTimer = setTimeout(() => this.fire("long"), delay);
48
+ }
49
+ }
50
+
51
+ private fire(type: BreakType): void {
52
+ if (Date.now() < this.pausedUntil) {
53
+ const rescheduleIn = this.pausedUntil - Date.now();
54
+ const t = setTimeout(() => this.fire(type), rescheduleIn);
55
+ if (type === "mini") this.miniTimer = t;
56
+ else this.longTimer = t;
57
+ return;
58
+ }
59
+ this.onBreak(type);
60
+ }
61
+
62
+ triggerNow(type: BreakType): void {
63
+ if (type === "mini" && this.miniTimer) clearTimeout(this.miniTimer);
64
+ if (type === "long" && this.longTimer) clearTimeout(this.longTimer);
65
+ this.onBreak(type);
66
+ }
67
+
68
+ pauseFor(ms: number): void {
69
+ this.pausedUntil = Date.now() + ms;
70
+ }
71
+
72
+ resume(): void {
73
+ this.pausedUntil = 0;
74
+ }
75
+
76
+ resumeAfterBreak(): void {
77
+ const now = Date.now();
78
+ if (this.prefs.miniBreakEnabled) {
79
+ this.nextMiniAt = now + this.prefs.miniBreakIntervalMinutes * 60 * 1000;
80
+ if (this.miniTimer) clearTimeout(this.miniTimer);
81
+ this.miniTimer = setTimeout(() => this.fire("mini"), this.nextMiniAt - now);
82
+ }
83
+ if (this.prefs.longBreakEnabled) {
84
+ this.nextLongAt = now + this.prefs.longBreakIntervalMinutes * 60 * 1000;
85
+ if (this.longTimer) clearTimeout(this.longTimer);
86
+ this.longTimer = setTimeout(() => this.fire("long"), this.nextLongAt - now);
87
+ }
88
+ }
89
+
90
+ nextBreakLabel(lang: Language): string {
91
+ const tt = (k: TranslationKey) => t(lang, k);
92
+ const now = Date.now();
93
+ if (now < this.pausedUntil) {
94
+ const mins = Math.ceil((this.pausedUntil - now) / 60000);
95
+ return tt("trayPaused") + mins + " min";
96
+ }
97
+ const candidates: { type: string; at: number }[] = [];
98
+ if (this.prefs.miniBreakEnabled && this.nextMiniAt) candidates.push({ type: tt("breakTitleMini"), at: this.nextMiniAt });
99
+ if (this.prefs.longBreakEnabled && this.nextLongAt) candidates.push({ type: tt("breakTitleLong"), at: this.nextLongAt });
100
+ if (candidates.length === 0) return tt("noBreakScheduled");
101
+ candidates.sort((a, b) => a.at - b.at);
102
+ const mins = Math.max(0, Math.ceil((candidates[0].at - now) / 60000));
103
+ return tt("trayNextBreak") + candidates[0].type + " ~" + mins + " min";
104
+ }
105
+ }
@@ -0,0 +1,9 @@
1
+ import { contextBridge, ipcRenderer } from "electron";
2
+ import { BreakPlan } from "../shared/types";
3
+
4
+ contextBridge.exposeInMainWorld("eyeCare", {
5
+ onBreakStart: (cb: (plan: BreakPlan) => void) =>
6
+ ipcRenderer.on("break:start", (_e, plan: BreakPlan) => cb(plan)),
7
+ skipBreak: () => ipcRenderer.invoke("break:skip"),
8
+ openSource: (url: string) => ipcRenderer.invoke("break:openSource", url),
9
+ });
@@ -0,0 +1,18 @@
1
+ import { contextBridge, ipcRenderer } from "electron";
2
+ import { Preferences, BackgroundConfig } from "../shared/types";
3
+
4
+ contextBridge.exposeInMainWorld("eyeCare", {
5
+ getPreferences: () => ipcRenderer.invoke("prefs:get"),
6
+ updatePreferences: (next: Partial<Preferences>) => ipcRenderer.invoke("prefs:update", next),
7
+ getAppInfo: () => ipcRenderer.invoke("app:info"),
8
+ backgrounds: {
9
+ listBuiltin: () => ipcRenderer.invoke("bg:listBuiltin"),
10
+ listUser: () => ipcRenderer.invoke("bg:listUser"),
11
+ addUser: () => ipcRenderer.invoke("bg:addUser"),
12
+ deleteUser: (name: string) => ipcRenderer.invoke("bg:deleteUser", name),
13
+ set: (cfg: BackgroundConfig) => ipcRenderer.invoke("bg:set", cfg),
14
+ get: () => ipcRenderer.invoke("bg:get"),
15
+ loadFile: (name: string, mode: "none" | "builtin" | "user" | "random") =>
16
+ ipcRenderer.invoke("bg:loadFile", name, mode),
17
+ },
18
+ });
@@ -0,0 +1,13 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 1080" preserveAspectRatio="xMidYMid slice">
2
+ <defs>
3
+ <linearGradient id="forest" x1="0" y1="0" x2="0" y2="1">
4
+ <stop offset="0%" stop-color="#cae8c0"/>
5
+ <stop offset="40%" stop-color="#9bc99a"/>
6
+ <stop offset="100%" stop-color="#4a7a4f"/>
7
+ </linearGradient>
8
+ </defs>
9
+ <rect width="1920" height="1080" fill="url(#forest)"/>
10
+ <path d="M0,1080 L0,720 Q200,650 400,700 T800,680 T1200,700 T1600,680 T1920,700 L1920,1080 Z" fill="#5a8a5f" opacity="0.7"/>
11
+ <path d="M0,1080 L0,820 Q240,780 480,800 T960,790 T1440,810 T1920,800 L1920,1080 Z" fill="#3d6a44" opacity="0.85"/>
12
+ <path d="M0,1080 L0,920 Q320,890 640,910 T1280,900 T1920,910 L1920,1080 Z" fill="#2a4d34"/>
13
+ </svg>
@@ -0,0 +1,13 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 1080" preserveAspectRatio="xMidYMid slice">
2
+ <defs>
3
+ <linearGradient id="mtn" x1="0" y1="0" x2="0" y2="1">
4
+ <stop offset="0%" stop-color="#dde6f0"/>
5
+ <stop offset="50%" stop-color="#a8b8cf"/>
6
+ <stop offset="100%" stop-color="#6b7e98"/>
7
+ </linearGradient>
8
+ </defs>
9
+ <rect width="1920" height="1080" fill="url(#mtn)"/>
10
+ <path d="M0,1080 L0,700 L260,500 L480,640 L740,420 L1000,600 L1280,460 L1560,620 L1920,520 L1920,1080 Z" fill="#7a8ba5" opacity="0.8"/>
11
+ <path d="M0,1080 L0,820 L320,680 L620,780 L920,640 L1260,760 L1640,700 L1920,780 L1920,1080 Z" fill="#566886" opacity="0.9"/>
12
+ <path d="M0,1080 L0,940 L400,880 L800,920 L1280,880 L1920,910 L1920,1080 Z" fill="#3a4d6a"/>
13
+ </svg>
@@ -0,0 +1,13 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 1080" preserveAspectRatio="xMidYMid slice">
2
+ <defs>
3
+ <linearGradient id="sea" x1="0" y1="0" x2="0" y2="1">
4
+ <stop offset="0%" stop-color="#cfeaf3"/>
5
+ <stop offset="35%" stop-color="#7fb8d6"/>
6
+ <stop offset="100%" stop-color="#2c5f7c"/>
7
+ </linearGradient>
8
+ </defs>
9
+ <rect width="1920" height="1080" fill="url(#sea)"/>
10
+ <path d="M0,560 Q480,520 960,560 T1920,560 L1920,1080 L0,1080 Z" fill="#4a89ab" opacity="0.6"/>
11
+ <path d="M0,720 Q480,690 960,720 T1920,720 L1920,1080 L0,1080 Z" fill="#2c5f7c" opacity="0.7"/>
12
+ <path d="M0,880 Q480,860 960,880 T1920,880 L1920,1080 L0,1080 Z" fill="#1c4a64" opacity="0.85"/>
13
+ </svg>
@@ -0,0 +1,16 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 1080" preserveAspectRatio="xMidYMid slice">
2
+ <defs>
3
+ <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
4
+ <stop offset="0%" stop-color="#a8d8f5"/>
5
+ <stop offset="60%" stop-color="#d4ecfb"/>
6
+ <stop offset="100%" stop-color="#fef9e7"/>
7
+ </linearGradient>
8
+ <radialGradient id="sun" cx="50%" cy="35%" r="25%">
9
+ <stop offset="0%" stop-color="#fff7d6" stop-opacity="0.9"/>
10
+ <stop offset="100%" stop-color="#fff7d6" stop-opacity="0"/>
11
+ </radialGradient>
12
+ </defs>
13
+ <rect width="1920" height="1080" fill="url(#sky)"/>
14
+ <rect width="1920" height="1080" fill="url(#sun)"/>
15
+ <ellipse cx="960" cy="900" rx="1400" ry="120" fill="#cfe8f5" opacity="0.6"/>
16
+ </svg>
@@ -0,0 +1,18 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 1080" preserveAspectRatio="xMidYMid slice">
2
+ <defs>
3
+ <linearGradient id="sunset" x1="0" y1="0" x2="0" y2="1">
4
+ <stop offset="0%" stop-color="#f5c26b"/>
5
+ <stop offset="35%" stop-color="#e8966b"/>
6
+ <stop offset="70%" stop-color="#9c5a7a"/>
7
+ <stop offset="100%" stop-color="#3a2e4a"/>
8
+ </linearGradient>
9
+ <radialGradient id="sun2" cx="50%" cy="62%" r="18%">
10
+ <stop offset="0%" stop-color="#fff0c2" stop-opacity="0.95"/>
11
+ <stop offset="100%" stop-color="#fff0c2" stop-opacity="0"/>
12
+ </radialGradient>
13
+ </defs>
14
+ <rect width="1920" height="1080" fill="url(#sunset)"/>
15
+ <rect width="1920" height="1080" fill="url(#sun2)"/>
16
+ <path d="M0,1080 L0,780 Q480,740 960,760 T1920,760 L1920,1080 Z" fill="#5a3a5a" opacity="0.8"/>
17
+ <path d="M0,1080 L0,900 Q480,880 960,890 T1920,900 L1920,1080 Z" fill="#2a1c3a"/>
18
+ </svg>