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,452 @@
1
+ (function () {
2
+ const STRINGS = {
3
+ en: {
4
+ breakTitleMini: "Mini eye break",
5
+ breakSubtitleMini: "A quick reset for your eyes.",
6
+ breakTitleLong: "Long eye break",
7
+ breakSubtitleLong: "Follow the guided exercises.",
8
+ breakComplete: "Break complete",
9
+ breakCompleteSubtitle: "Great job. Back to work!",
10
+ skipBreak: "Skip break",
11
+ sourcePrefix: "Source: ",
12
+ restEyes: "Rest your eyes",
13
+ restEyesInstruction: "Look away from the screen and relax.",
14
+ guideLook20ft: "Look 20 ft (6 m) away",
15
+ guideYou: "you",
16
+ guideFarL: "far L",
17
+ guideFarR: "far R",
18
+ guideSlowBlink: "Slow blink",
19
+ guideNear: "Near",
20
+ guideFar: "Far",
21
+ guideTrace8: "Trace the 8 with your eyes",
22
+ guideWarmPalms: "Warm palms over closed eyes",
23
+ guideBreathe: "breathe",
24
+ guideSweepLR: "Sweep your gaze left to right",
25
+ guideL: "L",
26
+ guideR: "R",
27
+ guideRestEyes: "Rest your eyes",
28
+ exercises: {
29
+ "20-20-20": "20-20-20 Rule",
30
+ "conscious-blink": "Conscious Blinking",
31
+ "near-far-focus": "Near-Far Focus Shift",
32
+ "figure-eight": "Figure-8 Tracing",
33
+ "palming": "Palming",
34
+ "horizontal-rolls": "Horizontal Eye Rolls",
35
+ },
36
+ steps: {
37
+ "20-20-20": ["Look at an object at least 20 feet (6 m) away — out a window or across the room."],
38
+ "conscious-blink": ["Close your eyes gently and softly for 2 seconds.", "Open slowly. Repeat the slow blink at a relaxed pace."],
39
+ "near-far-focus": ["Hold your finger 10-15 inches (25-38 cm) from your eyes and focus on it.", "Shift focus to an object 20 feet (6 m) or farther away.", "Back to your finger.", "Back to the distant object. Repeat smoothly."],
40
+ "figure-eight": ["Imagine a large figure-8 on the wall about 10 feet (3 m) away.", "Slowly trace it one way with your eyes only (head still).", "Reverse direction and trace it back."],
41
+ "palming": ["Rub your palms together until they feel warm.", "Close your eyes and cup your palms over them without pressing on the eyeballs.", "Breathe slowly and relax. Keep the position."],
42
+ "horizontal-rolls": ["Look as far right as comfortable (head still).", "Slowly move your gaze to the far left.", "Back to center.", "Repeat the slow sweep a few more times."],
43
+ },
44
+ sources: {
45
+ "20-20-20": "American Academy of Ophthalmology",
46
+ "conscious-blink": "American Academy of Ophthalmology — Computer Vision Syndrome",
47
+ "near-far-focus": "American Optometric Association — Computer Vision Initiative",
48
+ "figure-eight": "American Academy of Ophthalmology — Eye Exercises",
49
+ "palming": "American Academy of Ophthalmology — Eye Exercises",
50
+ "horizontal-rolls": "American Academy of Ophthalmology — Eye Exercises",
51
+ },
52
+ },
53
+ ko: {
54
+ breakTitleMini: "미니 눈 휴식",
55
+ breakSubtitleMini: "눈에 빠른 휴식을 주세요.",
56
+ breakTitleLong: "긴 눈 휴식",
57
+ breakSubtitleLong: "가이드를 따라 운동해주세요.",
58
+ breakComplete: "휴식 완료",
59
+ breakCompleteSubtitle: "잘했어요. 다시 일어볼까요!",
60
+ skipBreak: "휴식 건너뛰기",
61
+ sourcePrefix: "출처: ",
62
+ restEyes: "눈 휴식",
63
+ restEyesInstruction: "화면에서 눈을 떼고 편안히 쉬세요.",
64
+ guideLook20ft: "20피트(6m) 너머 보기",
65
+ guideYou: "나",
66
+ guideFarL: "왼쪽 먼 곳",
67
+ guideFarR: "오른쪽 먼 곳",
68
+ guideSlowBlink: "천천히 깜빡이기",
69
+ guideNear: "가까이",
70
+ guideFar: "멀리",
71
+ guideTrace8: "눈으로 8자를 따라가세요",
72
+ guideWarmPalms: "따뜻한 손바닥으로 눈 덮기",
73
+ guideBreathe: "호흡",
74
+ guideSweepLR: "시선을 좌에서 우로 천천히",
75
+ guideL: "좌",
76
+ guideR: "우",
77
+ guideRestEyes: "눈 휴식",
78
+ exercises: {
79
+ "20-20-20": "20-20-20 규칙",
80
+ "conscious-blink": "의식적 깜빡임",
81
+ "near-far-focus": "원근 촛점 교대",
82
+ "figure-eight": "8자 추적",
83
+ "palming": "파밍 (손바닥 덮기)",
84
+ "horizontal-rolls": "좌우 눈 운동",
85
+ },
86
+ steps: {
87
+ "20-20-20": ["20피트(6m) 이상 먼 곳을 바라보세요 — 창밖이나 방 반대편."],
88
+ "conscious-blink": ["부드럽게 2초간 눈을 감으세요.", "천천히 뜨세요. 편안한 속도로 반복합니다."],
89
+ "near-far-focus": ["손가락을 눈에서 25-38cm 거리에 두고 응시하세요.", "20피트(6m) 이상 먼 곳의 사물로 초점을 옮기세요.", "다시 손가락으로.", "다시 먼 곳으로. 부드럽게 반복하세요."],
90
+ "figure-eight": ["약 3m 앞 벽에 큰 8자가 있다고 상상하세요.", "머리는 움직이지 말고 눈만으로 한 방향으로 천천히 따라가세요.", "반대 방향으로 되돌아 가세요."],
91
+ "palming": ["손바닥을 비벼 따뜻하게 만드세요.", "눈을 감고 손바닥을 눈 위에 덮으세요 (안구를 누르지 마세요).", "천천히 호흡하며 편안히 유지하세요."],
92
+ "horizontal-rolls": ["머리는 그대로 둔 채 시선을 편안한 오른쪽 끝으로.", "시선을 천천히 왼쪽 끝으로 옮기세요.", "다시 중앙으로.", "느린 좌우 스윕을 몇 번 더 반복하세요."],
93
+ },
94
+ sources: {
95
+ "20-20-20": "미국안과학회 (AAO)",
96
+ "conscious-blink": "미국안과학회 — 컴퓨터 시력 증후군",
97
+ "near-far-focus": "미국검안사협회 — 컴퓨터 시력 이니셔티브",
98
+ "figure-eight": "미국안과학회 — 눈 운동",
99
+ "palming": "미국안과학회 — 눈 운동",
100
+ "horizontal-rolls": "미국안과학회 — 눈 운동",
101
+ },
102
+ },
103
+ };
104
+
105
+ let lang = "en";
106
+ function tr(key) {
107
+ const s = STRINGS[lang] || STRINGS.en;
108
+ return s[key] || STRINGS.en[key] || key;
109
+ }
110
+
111
+ const titleEl = document.getElementById("title");
112
+ const subtitleEl = document.getElementById("subtitle");
113
+ const progressEl = document.getElementById("progress");
114
+ const exerciseNameEl = document.getElementById("exerciseName");
115
+ const stepTextEl = document.getElementById("stepText");
116
+ const stepTimeEl = document.getElementById("stepTime");
117
+ const sourceEl = document.getElementById("source");
118
+ const skipBtn = document.getElementById("skipBtn");
119
+ const svgEl = document.getElementById("guideSvg");
120
+
121
+ let totalElapsed = 0;
122
+ let totalDuration = 1;
123
+ let stepIndex = 0;
124
+ let stepElapsed = 0;
125
+ let currentSteps = [];
126
+ let currentExercise = null;
127
+ let plan = null;
128
+ let ticker = null;
129
+ let soundEnabled = true;
130
+
131
+ let audioCtx = null;
132
+ function getAudioCtx() {
133
+ if (!audioCtx) {
134
+ try {
135
+ audioCtx = new (window.AudioContext || window.webkitAudioContext)();
136
+ } catch (e) {
137
+ audioCtx = null;
138
+ }
139
+ }
140
+ return audioCtx;
141
+ }
142
+
143
+ function playTone(freq, startAt, duration, gainValue) {
144
+ const ctx = getAudioCtx();
145
+ if (!ctx) return;
146
+ const osc = ctx.createOscillator();
147
+ const gain = ctx.createGain();
148
+ osc.type = "sine";
149
+ osc.frequency.setValueAtTime(freq, startAt);
150
+ gain.gain.setValueAtTime(0, startAt);
151
+ gain.gain.linearRampToValueAtTime(gainValue, startAt + 0.02);
152
+ gain.gain.exponentialRampToValueAtTime(0.001, startAt + duration);
153
+ osc.connect(gain);
154
+ gain.connect(ctx.destination);
155
+ osc.start(startAt);
156
+ osc.stop(startAt + duration + 0.05);
157
+ }
158
+
159
+ function playChime(kind) {
160
+ if (!soundEnabled) return;
161
+ const ctx = getAudioCtx();
162
+ if (!ctx) return;
163
+ const now = ctx.currentTime;
164
+ if (kind === "exercise-start") {
165
+ playTone(660, now, 0.18, 0.18);
166
+ playTone(880, now + 0.08, 0.22, 0.18);
167
+ } else if (kind === "step-end") {
168
+ playTone(523.25, now, 0.14, 0.12);
169
+ } else if (kind === "break-complete") {
170
+ playTone(523.25, now, 0.2, 0.18);
171
+ playTone(659.25, now + 0.1, 0.2, 0.18);
172
+ playTone(783.99, now + 0.2, 0.32, 0.2);
173
+ }
174
+ }
175
+
176
+ function fmtTime(s) {
177
+ const m = Math.floor(s / 60);
178
+ const sec = s % 60;
179
+ if (m > 0) return m + ":" + String(sec).padStart(2, "0");
180
+ return String(sec);
181
+ }
182
+
183
+ function setProgress(pct) {
184
+ progressEl.style.width = Math.min(100, Math.max(0, pct)) + "%";
185
+ }
186
+
187
+ function clearSvg() {
188
+ while (svgEl.firstChild) svgEl.removeChild(svgEl.firstChild);
189
+ }
190
+
191
+ function el(name, attrs, children) {
192
+ const node = document.createElementNS("http://www.w3.org/2000/svg", name);
193
+ if (attrs) {
194
+ for (const k in attrs) {
195
+ if (k === "className") node.setAttribute("class", attrs[k]);
196
+ else node.setAttribute(k, attrs[k]);
197
+ }
198
+ }
199
+ if (children) {
200
+ (Array.isArray(children) ? children : [children]).forEach(function (c) {
201
+ if (c == null) return;
202
+ node.appendChild(typeof c === "string" ? document.createTextNode(c) : c);
203
+ });
204
+ }
205
+ return node;
206
+ }
207
+
208
+ function text(x, y, content, cls) {
209
+ return el("text", { x: x, y: y, "text-anchor": "middle", className: cls || "label" }, content);
210
+ }
211
+
212
+ function baseExerciseId(id) {
213
+ return id.replace(/-\d+$/, "");
214
+ }
215
+
216
+ const GUIDES = {
217
+ "20-20-20": function () {
218
+ const group = el("g", {});
219
+ group.appendChild(text(200, 30, tr("guideLook20ft"), "label"));
220
+ group.appendChild(el("circle", { cx: 200, cy: 120, r: 14, className: "target" }));
221
+ group.appendChild(text(200, 158, tr("guideYou"), "label"));
222
+ group.appendChild(el("circle", { cx: 60, cy: 50, r: 22, className: "target" }));
223
+ group.appendChild(text(60, 88, tr("guideFarL"), "label"));
224
+ group.appendChild(el("circle", { cx: 340, cy: 50, r: 22, className: "target" }));
225
+ group.appendChild(text(340, 88, tr("guideFarR"), "label"));
226
+ const dot = el("circle", { cx: 0, cy: 0, r: 10, className: "dot dot-20-20-20" });
227
+ group.appendChild(dot);
228
+ return group;
229
+ },
230
+
231
+ "conscious-blink": function () {
232
+ const group = el("g", {});
233
+ group.appendChild(text(200, 40, tr("guideSlowBlink"), "label"));
234
+ group.appendChild(el("ellipse", { cx: 200, cy: 120, rx: 60, ry: 32, className: "eye-outline" }));
235
+ group.appendChild(el("circle", { cx: 200, cy: 120, r: 10, fill: "#4f8cff" }));
236
+ const lidTop = el("ellipse", { cx: 200, cy: 88, rx: 62, ry: 32, className: "eye-lid lid-top" });
237
+ const lidBottom = el("ellipse", { cx: 200, cy: 152, rx: 62, ry: 32, className: "eye-lid lid-bottom" });
238
+ group.appendChild(lidTop);
239
+ group.appendChild(lidBottom);
240
+ return group;
241
+ },
242
+
243
+ "near-far-focus": function () {
244
+ const group = el("g", {});
245
+ group.appendChild(text(200, 40, tr("guideNear"), "label"));
246
+ group.appendChild(text(200, 200, tr("guideFar"), "label"));
247
+ group.appendChild(el("rect", { x: 185, y: 100, width: 30, height: 40, rx: 6, className: "target" }));
248
+ const bar = el("rect", { x: 175, y: 80, width: 50, height: 80, rx: 6, className: "dot dot-near-far" });
249
+ group.appendChild(bar);
250
+ return group;
251
+ },
252
+
253
+ "figure-eight": function () {
254
+ const group = el("g", {});
255
+ group.appendChild(text(200, 30, tr("guideTrace8"), "label"));
256
+ const path8 = el("path", {
257
+ d: "M120,120 C120,80 200,80 200,120 C200,160 280,160 280,120 C280,80 200,80 200,120 C200,160 120,160 120,120 Z",
258
+ fill: "none",
259
+ stroke: "#4f8cff",
260
+ "stroke-width": 2,
261
+ opacity: 0.4,
262
+ });
263
+ group.appendChild(path8);
264
+ const dot = el("circle", { r: 9, className: "dot dot-figure8", style: "offset-path: path('M120,120 C120,80 200,80 200,120 C200,160 280,160 280,120 C280,80 200,80 200,120 C200,160 120,160 120,120 Z'); offset-rotate: 0deg;" });
265
+ group.appendChild(dot);
266
+ return group;
267
+ },
268
+
269
+ "palming": function () {
270
+ const group = el("g", {});
271
+ group.appendChild(text(200, 40, tr("guideWarmPalms"), "label"));
272
+ group.appendChild(el("circle", { cx: 200, cy: 120, r: 60, fill: "#1a2240", stroke: "#4f8cff", "stroke-width": 2, opacity: 0.5 }));
273
+ const glow = el("circle", { cx: 200, cy: 120, r: 50, fill: "#6bd4c0", opacity: 0.5, className: "palm-glow" });
274
+ group.appendChild(glow);
275
+ const breathe = text(200, 130, tr("guideBreathe"), "palm-breathe-text");
276
+ group.appendChild(breathe);
277
+ return group;
278
+ },
279
+
280
+ "horizontal-rolls": function () {
281
+ const group = el("g", {});
282
+ group.appendChild(text(200, 40, tr("guideSweepLR"), "label"));
283
+ group.appendChild(el("line", { x1: 80, y1: 120, x2: 320, y2: 120, stroke: "#4f8cff", "stroke-width": 2, opacity: 0.3 }));
284
+ group.appendChild(el("circle", { cx: 80, cy: 120, r: 10, className: "target" }));
285
+ group.appendChild(el("circle", { cx: 320, cy: 120, r: 10, className: "target" }));
286
+ group.appendChild(text(80, 155, tr("guideL"), "label"));
287
+ group.appendChild(text(320, 155, tr("guideR"), "label"));
288
+ const dot = el("circle", { cx: 0, cy: 0, r: 10, className: "dot dot-horizontal" });
289
+ group.appendChild(dot);
290
+ return group;
291
+ },
292
+
293
+ rest: function () {
294
+ const group = el("g", {});
295
+ group.appendChild(text(200, 40, tr("guideRestEyes"), "label"));
296
+ const ring = el("circle", { cx: 200, cy: 130, r: 80, fill: "none", stroke: "#4f8cff", "stroke-width": 2, opacity: 0.3 });
297
+ group.appendChild(ring);
298
+ const dot = el("circle", { cx: 200, cy: 130, r: 60, fill: "#6bd4c0", opacity: 0.5, className: "dot-rest" });
299
+ group.appendChild(dot);
300
+ return group;
301
+ },
302
+ };
303
+
304
+ function renderGuide(exerciseId) {
305
+ clearSvg();
306
+ const base = baseExerciseId(exerciseId);
307
+ const key = GUIDES[base] ? base : "rest";
308
+ svgEl.appendChild(GUIDES[key]());
309
+ }
310
+
311
+ function exerciseName(id) {
312
+ const base = baseExerciseId(id);
313
+ const names = (STRINGS[lang] || STRINGS.en).exercises;
314
+ return names[base] || base;
315
+ }
316
+
317
+ function exerciseSource(id) {
318
+ const base = baseExerciseId(id);
319
+ const sources = (STRINGS[lang] || STRINGS.en).sources;
320
+ return sources[base] || "";
321
+ }
322
+
323
+ function stepTextFor(exerciseId, stepIndex) {
324
+ const base = baseExerciseId(exerciseId);
325
+ const steps = (STRINGS[lang] || STRINGS.en).steps;
326
+ const arr = steps[base];
327
+ if (arr && arr[stepIndex]) return arr[stepIndex];
328
+ return "";
329
+ }
330
+
331
+ function loadStep() {
332
+ if (!currentExercise) return;
333
+ if (stepIndex >= currentSteps.length) {
334
+ nextExercise();
335
+ return;
336
+ }
337
+ const step = currentSteps[stepIndex];
338
+ stepTextEl.textContent = stepTextFor(currentExercise.id, stepIndex) || step.text;
339
+ stepElapsed = 0;
340
+ stepTimeEl.textContent = fmtTime(step.durationSeconds);
341
+ }
342
+
343
+ function nextExercise() {
344
+ const idx = plan.exercises.indexOf(currentExercise);
345
+ if (idx + 1 >= plan.exercises.length) {
346
+ finishBreak();
347
+ return;
348
+ }
349
+ currentExercise = plan.exercises[idx + 1];
350
+ currentSteps = currentExercise.steps;
351
+ stepIndex = 0;
352
+ exerciseNameEl.textContent = exerciseName(currentExercise.id);
353
+ sourceEl.textContent = tr("sourcePrefix") + (exerciseSource(currentExercise.id) || currentExercise.source);
354
+ sourceEl.onclick = function () { window.eyeCare.openSource(currentExercise.sourceUrl); };
355
+ renderGuide(currentExercise.id);
356
+ loadStep();
357
+ playChime("exercise-start");
358
+ }
359
+
360
+ function finishBreak() {
361
+ if (ticker) clearInterval(ticker);
362
+ titleEl.textContent = tr("breakComplete");
363
+ subtitleEl.textContent = tr("breakCompleteSubtitle");
364
+ exerciseNameEl.textContent = "";
365
+ stepTextEl.textContent = "";
366
+ stepTimeEl.textContent = "";
367
+ setProgress(100);
368
+ clearSvg();
369
+ svgEl.appendChild(GUIDES.rest());
370
+ playChime("break-complete");
371
+ setTimeout(function () { window.eyeCare.skipBreak(); }, 4000);
372
+ }
373
+
374
+ function tick() {
375
+ totalElapsed += 1;
376
+ setProgress((totalElapsed / totalDuration) * 100);
377
+ if (!currentExercise) return;
378
+ stepElapsed += 1;
379
+ const step = currentSteps[stepIndex];
380
+ if (!step) return;
381
+ const remaining = step.durationSeconds - stepElapsed;
382
+ stepTimeEl.textContent = fmtTime(remaining > 0 ? remaining : 0);
383
+ if (remaining <= 0) {
384
+ playChime("step-end");
385
+ stepIndex += 1;
386
+ loadStep();
387
+ }
388
+ }
389
+
390
+ skipBtn.addEventListener("click", function () {
391
+ if (ticker) clearInterval(ticker);
392
+ window.eyeCare.skipBreak();
393
+ });
394
+
395
+ const bgLayer = document.getElementById("bgLayer");
396
+
397
+ function applyBackground(bg) {
398
+ if (!bg || !bg.base64) {
399
+ bgLayer.style.backgroundImage = "";
400
+ bgLayer.classList.remove("has-bg");
401
+ return;
402
+ }
403
+ const url = "url(data:" + bg.mime + ";base64," + bg.base64 + ")";
404
+ bgLayer.style.backgroundImage = url;
405
+ const blur = typeof bg.blurStrength === "number" ? bg.blurStrength : 3;
406
+ bgLayer.style.filter = "blur(" + blur + "px) brightness(0.8)";
407
+ bgLayer.classList.add("has-bg");
408
+ }
409
+
410
+ window.eyeCare.onBreakStart(function (p) {
411
+ plan = p;
412
+ lang = p.language || "en";
413
+ soundEnabled = p.soundEnabled !== false;
414
+ document.documentElement.lang = lang;
415
+ skipBtn.textContent = tr("skipBreak");
416
+ applyBackground(p.background);
417
+ totalDuration = p.totalDurationSeconds;
418
+ totalElapsed = 0;
419
+ if (p.type === "mini") {
420
+ titleEl.textContent = tr("breakTitleMini");
421
+ subtitleEl.textContent = tr("breakSubtitleMini");
422
+ } else {
423
+ titleEl.textContent = tr("breakTitleLong");
424
+ subtitleEl.textContent = tr("breakSubtitleLong");
425
+ }
426
+ if (p.exercises.length === 0) {
427
+ exerciseNameEl.textContent = tr("restEyes");
428
+ stepTextEl.textContent = tr("restEyesInstruction");
429
+ stepTimeEl.textContent = fmtTime(p.totalDurationSeconds);
430
+ renderGuide("rest");
431
+ playChime("exercise-start");
432
+ ticker = setInterval(function () {
433
+ totalElapsed += 1;
434
+ setProgress((totalElapsed / totalDuration) * 100);
435
+ const remaining = totalDuration - totalElapsed;
436
+ stepTimeEl.textContent = fmtTime(remaining > 0 ? remaining : 0);
437
+ if (remaining <= 0) finishBreak();
438
+ }, 1000);
439
+ return;
440
+ }
441
+ currentExercise = p.exercises[0];
442
+ currentSteps = currentExercise.steps;
443
+ stepIndex = 0;
444
+ exerciseNameEl.textContent = exerciseName(currentExercise.id);
445
+ sourceEl.textContent = tr("sourcePrefix") + (exerciseSource(currentExercise.id) || currentExercise.source);
446
+ sourceEl.onclick = function () { window.eyeCare.openSource(currentExercise.sourceUrl); };
447
+ renderGuide(currentExercise.id);
448
+ loadStep();
449
+ playChime("exercise-start");
450
+ ticker = setInterval(tick, 1000);
451
+ });
452
+ })();
@@ -0,0 +1,198 @@
1
+ * { box-sizing: border-box; margin: 0; padding: 0; }
2
+
3
+ body {
4
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
5
+ background: #f7f8fa;
6
+ color: #1a1f36;
7
+ padding: 24px;
8
+ }
9
+
10
+ .wrap { max-width: 480px; margin: 0 auto; }
11
+
12
+ h1 {
13
+ font-size: 22px;
14
+ font-weight: 600;
15
+ margin-bottom: 4px;
16
+ }
17
+
18
+ .hint {
19
+ font-size: 13px;
20
+ color: #6b7280;
21
+ margin-bottom: 24px;
22
+ }
23
+
24
+ section {
25
+ background: #fff;
26
+ border: 1px solid #e5e7eb;
27
+ border-radius: 8px;
28
+ padding: 16px 20px;
29
+ margin-bottom: 16px;
30
+ }
31
+
32
+ section h2 {
33
+ font-size: 15px;
34
+ font-weight: 600;
35
+ margin-bottom: 12px;
36
+ color: #4f8cff;
37
+ }
38
+
39
+ .row {
40
+ display: flex;
41
+ align-items: center;
42
+ justify-content: space-between;
43
+ padding: 8px 0;
44
+ font-size: 14px;
45
+ }
46
+
47
+ .row span { color: #374151; }
48
+
49
+ input[type="number"] {
50
+ width: 80px;
51
+ padding: 6px 8px;
52
+ border: 1px solid #d1d5db;
53
+ border-radius: 4px;
54
+ font-size: 14px;
55
+ text-align: right;
56
+ }
57
+
58
+ input[type="checkbox"] {
59
+ width: 18px;
60
+ height: 18px;
61
+ cursor: pointer;
62
+ }
63
+
64
+ select {
65
+ padding: 6px 8px;
66
+ border: 1px solid #d1d5db;
67
+ border-radius: 4px;
68
+ font-size: 14px;
69
+ background: #fff;
70
+ cursor: pointer;
71
+ }
72
+
73
+ .bg-gallery {
74
+ display: grid;
75
+ grid-template-columns: repeat(3, 1fr);
76
+ gap: 8px;
77
+ margin: 12px 0;
78
+ max-height: 320px;
79
+ overflow-y: auto;
80
+ }
81
+
82
+ .bg-gallery.list-mode {
83
+ grid-template-columns: repeat(2, 1fr);
84
+ }
85
+
86
+ .bg-thumb {
87
+ aspect-ratio: 16 / 9;
88
+ border: 2px solid #e5e7eb;
89
+ border-radius: 6px;
90
+ overflow: hidden;
91
+ cursor: pointer;
92
+ background: #f3f4f6;
93
+ position: relative;
94
+ }
95
+
96
+ .bg-thumb.selected {
97
+ border-color: #4f8cff;
98
+ box-shadow: 0 0 0 2px rgba(79, 140, 255, 0.3);
99
+ }
100
+
101
+ .bg-thumb img {
102
+ width: 100%;
103
+ height: 100%;
104
+ object-fit: cover;
105
+ }
106
+
107
+ .bg-thumb .bg-name {
108
+ position: absolute;
109
+ bottom: 0;
110
+ left: 0;
111
+ right: 0;
112
+ padding: 3px 6px;
113
+ background: rgba(0, 0, 0, 0.55);
114
+ color: #fff;
115
+ font-size: 11px;
116
+ text-align: center;
117
+ white-space: nowrap;
118
+ overflow: hidden;
119
+ text-overflow: ellipsis;
120
+ }
121
+
122
+ .bg-actions {
123
+ display: flex;
124
+ gap: 8px;
125
+ margin-bottom: 8px;
126
+ }
127
+
128
+ .bg-actions.hidden {
129
+ display: none;
130
+ }
131
+
132
+ .btn-secondary {
133
+ background: #fff;
134
+ border: 1px solid #d1d5db;
135
+ color: #374151;
136
+ padding: 6px 12px;
137
+ border-radius: 4px;
138
+ font-size: 13px;
139
+ cursor: pointer;
140
+ }
141
+
142
+ .btn-secondary:hover {
143
+ border-color: #4f8cff;
144
+ color: #4f8cff;
145
+ }
146
+
147
+ .bg-hint {
148
+ font-size: 11px;
149
+ color: #6b7280;
150
+ min-height: 14px;
151
+ }
152
+
153
+ input[type="range"] {
154
+ width: 140px;
155
+ cursor: pointer;
156
+ }
157
+
158
+ .range-value {
159
+ font-size: 12px;
160
+ color: #4f8cff;
161
+ min-width: 24px;
162
+ text-align: right;
163
+ font-variant-numeric: tabular-nums;
164
+ }
165
+
166
+ .app-footer {
167
+ margin-top: 20px;
168
+ padding-top: 12px;
169
+ border-top: 1px solid #e5e7eb;
170
+ text-align: center;
171
+ font-size: 11px;
172
+ color: #9ca3af;
173
+ line-height: 1.6;
174
+ }
175
+
176
+ .app-version { font-weight: 600; color: #6b7280; }
177
+ .app-author { color: #9ca3af; }
178
+ .app-author a { color: #4f8cff; text-decoration: none; }
179
+ .app-author a:hover { text-decoration: underline; }
180
+
181
+ .status {
182
+ font-size: 13px;
183
+ color: #047857;
184
+ min-height: 18px;
185
+ text-align: center;
186
+ margin-top: 8px;
187
+ }
188
+
189
+ .medical {
190
+ margin-top: 16px;
191
+ padding: 12px;
192
+ background: #fff7ed;
193
+ border: 1px solid #fed7aa;
194
+ border-radius: 6px;
195
+ font-size: 11px;
196
+ color: #9a3412;
197
+ line-height: 1.5;
198
+ }