@jjlmoya/utils-chrono 1.17.0 → 1.18.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 (221) hide show
  1. package/package.json +1 -1
  2. package/src/category/i18n/fr.ts +1 -1
  3. package/src/category/index.ts +2 -0
  4. package/src/entries.ts +4 -1
  5. package/src/index.ts +1 -0
  6. package/src/tests/locale_completeness.test.ts +1 -1
  7. package/src/tests/no_en_dash.test.ts +40 -6
  8. package/src/tests/title_quality.test.ts +2 -2
  9. package/src/tests/tool_validation.test.ts +1 -1
  10. package/src/tool/beat-rate-converter/i18n/de.ts +30 -30
  11. package/src/tool/beat-rate-converter/i18n/es.ts +33 -33
  12. package/src/tool/beat-rate-converter/i18n/fr.ts +45 -45
  13. package/src/tool/beat-rate-converter/i18n/id.ts +6 -6
  14. package/src/tool/beat-rate-converter/i18n/it.ts +28 -28
  15. package/src/tool/beat-rate-converter/i18n/ja.ts +56 -56
  16. package/src/tool/beat-rate-converter/i18n/ko.ts +56 -56
  17. package/src/tool/beat-rate-converter/i18n/nl.ts +14 -14
  18. package/src/tool/beat-rate-converter/i18n/pl.ts +48 -48
  19. package/src/tool/beat-rate-converter/i18n/pt.ts +30 -30
  20. package/src/tool/beat-rate-converter/i18n/ru.ts +56 -56
  21. package/src/tool/beat-rate-converter/i18n/sv.ts +44 -44
  22. package/src/tool/beat-rate-converter/i18n/tr.ts +51 -51
  23. package/src/tool/beat-rate-converter/i18n/zh.ts +56 -56
  24. package/src/tool/crown-reference-guide/i18n/de.ts +9 -9
  25. package/src/tool/crown-reference-guide/i18n/en.ts +8 -8
  26. package/src/tool/crown-reference-guide/i18n/es.ts +10 -10
  27. package/src/tool/crown-reference-guide/i18n/fr.ts +18 -18
  28. package/src/tool/crown-reference-guide/i18n/id.ts +9 -9
  29. package/src/tool/crown-reference-guide/i18n/it.ts +9 -9
  30. package/src/tool/crown-reference-guide/i18n/nl.ts +8 -8
  31. package/src/tool/crown-reference-guide/i18n/pl.ts +8 -8
  32. package/src/tool/crown-reference-guide/i18n/pt.ts +8 -8
  33. package/src/tool/crown-reference-guide/i18n/ru.ts +8 -8
  34. package/src/tool/crown-reference-guide/i18n/sv.ts +8 -8
  35. package/src/tool/crown-reference-guide/i18n/tr.ts +8 -8
  36. package/src/tool/crown-reference-guide/i18n/zh.ts +9 -9
  37. package/src/tool/demagnetizing-timer/i18n/fr.ts +1 -1
  38. package/src/tool/gear-train-explorer/i18n/de.ts +2 -2
  39. package/src/tool/gear-train-explorer/i18n/en.ts +2 -2
  40. package/src/tool/gear-train-explorer/i18n/es.ts +2 -2
  41. package/src/tool/gear-train-explorer/i18n/fr.ts +2 -2
  42. package/src/tool/gear-train-explorer/i18n/id.ts +2 -2
  43. package/src/tool/gear-train-explorer/i18n/it.ts +2 -2
  44. package/src/tool/gear-train-explorer/i18n/ja.ts +2 -2
  45. package/src/tool/gear-train-explorer/i18n/ko.ts +2 -2
  46. package/src/tool/gear-train-explorer/i18n/nl.ts +2 -2
  47. package/src/tool/gear-train-explorer/i18n/pl.ts +2 -2
  48. package/src/tool/gear-train-explorer/i18n/pt.ts +2 -2
  49. package/src/tool/gear-train-explorer/i18n/ru.ts +6 -6
  50. package/src/tool/gear-train-explorer/i18n/sv.ts +2 -2
  51. package/src/tool/gear-train-explorer/i18n/tr.ts +2 -2
  52. package/src/tool/gear-train-explorer/i18n/zh.ts +2 -2
  53. package/src/tool/gmt-world-timer/i18n/en.ts +7 -7
  54. package/src/tool/gmt-world-timer/i18n/es.ts +8 -8
  55. package/src/tool/gmt-world-timer/i18n/fr.ts +12 -12
  56. package/src/tool/gmt-world-timer/i18n/id.ts +7 -7
  57. package/src/tool/gmt-world-timer/i18n/it.ts +8 -8
  58. package/src/tool/gmt-world-timer/i18n/ja.ts +5 -5
  59. package/src/tool/gmt-world-timer/i18n/ko.ts +5 -5
  60. package/src/tool/gmt-world-timer/i18n/nl.ts +7 -7
  61. package/src/tool/gmt-world-timer/i18n/pl.ts +7 -7
  62. package/src/tool/gmt-world-timer/i18n/pt.ts +7 -7
  63. package/src/tool/gmt-world-timer/i18n/ru.ts +11 -11
  64. package/src/tool/gmt-world-timer/i18n/sv.ts +7 -7
  65. package/src/tool/gmt-world-timer/i18n/tr.ts +6 -6
  66. package/src/tool/gmt-world-timer/i18n/zh.ts +6 -6
  67. package/src/tool/lume-color-simulator/i18n/fr.ts +4 -4
  68. package/src/tool/lume-color-simulator/i18n/id.ts +5 -5
  69. package/src/tool/lume-color-simulator/i18n/it.ts +4 -4
  70. package/src/tool/lume-color-simulator/i18n/ja.ts +5 -5
  71. package/src/tool/lume-color-simulator/i18n/nl.ts +2 -2
  72. package/src/tool/lume-color-simulator/i18n/pt.ts +5 -5
  73. package/src/tool/lume-color-simulator/i18n/ru.ts +14 -14
  74. package/src/tool/lume-color-simulator/i18n/zh.ts +4 -4
  75. package/src/tool/moon-phase-visualizer/i18n/fr.ts +4 -4
  76. package/src/tool/moon-phase-visualizer/i18n/id.ts +4 -4
  77. package/src/tool/moon-phase-visualizer/i18n/it.ts +4 -4
  78. package/src/tool/moon-phase-visualizer/i18n/ja.ts +2 -2
  79. package/src/tool/moon-phase-visualizer/i18n/nl.ts +2 -2
  80. package/src/tool/moon-phase-visualizer/i18n/pt.ts +4 -4
  81. package/src/tool/moon-phase-visualizer/i18n/ru.ts +7 -7
  82. package/src/tool/moon-phase-visualizer/i18n/zh.ts +4 -4
  83. package/src/tool/perpetual-calendar/i18n/de.ts +1 -1
  84. package/src/tool/perpetual-calendar/i18n/en.ts +1 -1
  85. package/src/tool/perpetual-calendar/i18n/es.ts +1 -1
  86. package/src/tool/perpetual-calendar/i18n/fr.ts +1 -1
  87. package/src/tool/perpetual-calendar/i18n/id.ts +1 -1
  88. package/src/tool/perpetual-calendar/i18n/it.ts +1 -1
  89. package/src/tool/perpetual-calendar/i18n/ja.ts +1 -1
  90. package/src/tool/perpetual-calendar/i18n/ko.ts +1 -1
  91. package/src/tool/perpetual-calendar/i18n/nl.ts +1 -1
  92. package/src/tool/perpetual-calendar/i18n/pl.ts +1 -1
  93. package/src/tool/perpetual-calendar/i18n/pt.ts +1 -1
  94. package/src/tool/perpetual-calendar/i18n/ru.ts +2 -2
  95. package/src/tool/perpetual-calendar/i18n/sv.ts +1 -1
  96. package/src/tool/perpetual-calendar/i18n/tr.ts +1 -1
  97. package/src/tool/perpetual-calendar/i18n/zh.ts +1 -1
  98. package/src/tool/power-reserve-estimator/i18n/de.ts +26 -26
  99. package/src/tool/power-reserve-estimator/i18n/es.ts +22 -22
  100. package/src/tool/power-reserve-estimator/i18n/fr.ts +36 -36
  101. package/src/tool/power-reserve-estimator/i18n/id.ts +5 -5
  102. package/src/tool/power-reserve-estimator/i18n/it.ts +18 -18
  103. package/src/tool/power-reserve-estimator/i18n/ja.ts +3 -3
  104. package/src/tool/power-reserve-estimator/i18n/ko.ts +3 -3
  105. package/src/tool/power-reserve-estimator/i18n/nl.ts +7 -7
  106. package/src/tool/power-reserve-estimator/i18n/pl.ts +9 -9
  107. package/src/tool/power-reserve-estimator/i18n/pt.ts +25 -25
  108. package/src/tool/power-reserve-estimator/i18n/ru.ts +15 -15
  109. package/src/tool/power-reserve-estimator/i18n/sv.ts +7 -7
  110. package/src/tool/power-reserve-estimator/i18n/tr.ts +7 -7
  111. package/src/tool/power-reserve-estimator/i18n/zh.ts +7 -7
  112. package/src/tool/quartz-battery-health/bibliography.astro +16 -0
  113. package/src/tool/quartz-battery-health/bibliography.ts +16 -0
  114. package/src/tool/quartz-battery-health/client.ts +198 -0
  115. package/src/tool/quartz-battery-health/component.astro +15 -0
  116. package/src/tool/quartz-battery-health/components/BatteryHealthPanel.astro +163 -0
  117. package/src/tool/quartz-battery-health/entry.ts +57 -0
  118. package/src/tool/quartz-battery-health/i18n/de.ts +327 -0
  119. package/src/tool/quartz-battery-health/i18n/en.ts +327 -0
  120. package/src/tool/quartz-battery-health/i18n/es.ts +327 -0
  121. package/src/tool/quartz-battery-health/i18n/fr.ts +327 -0
  122. package/src/tool/quartz-battery-health/i18n/id.ts +326 -0
  123. package/src/tool/quartz-battery-health/i18n/it.ts +327 -0
  124. package/src/tool/quartz-battery-health/i18n/ja.ts +327 -0
  125. package/src/tool/quartz-battery-health/i18n/ko.ts +327 -0
  126. package/src/tool/quartz-battery-health/i18n/nl.ts +327 -0
  127. package/src/tool/quartz-battery-health/i18n/pl.ts +327 -0
  128. package/src/tool/quartz-battery-health/i18n/pt.ts +327 -0
  129. package/src/tool/quartz-battery-health/i18n/ru.ts +327 -0
  130. package/src/tool/quartz-battery-health/i18n/sv.ts +326 -0
  131. package/src/tool/quartz-battery-health/i18n/tr.ts +327 -0
  132. package/src/tool/quartz-battery-health/i18n/zh.ts +327 -0
  133. package/src/tool/quartz-battery-health/index.ts +11 -0
  134. package/src/tool/quartz-battery-health/quartz-battery-health.css +447 -0
  135. package/src/tool/quartz-battery-health/seo.astro +16 -0
  136. package/src/tool/service-interval-tracker/i18n/fr.ts +1 -1
  137. package/src/tool/sidereal-time-tracker/i18n/ru.ts +2 -2
  138. package/src/tool/strap-length-calculator/i18n/fr.ts +1 -1
  139. package/src/tool/strap-length-calculator/i18n/ru.ts +4 -4
  140. package/src/tool/strap-taper-calculator/i18n/de.ts +3 -3
  141. package/src/tool/strap-taper-calculator/i18n/en.ts +1 -1
  142. package/src/tool/strap-taper-calculator/i18n/es.ts +3 -3
  143. package/src/tool/strap-taper-calculator/i18n/fr.ts +4 -4
  144. package/src/tool/strap-taper-calculator/i18n/id.ts +3 -3
  145. package/src/tool/strap-taper-calculator/i18n/it.ts +3 -3
  146. package/src/tool/strap-taper-calculator/i18n/ja.ts +2 -2
  147. package/src/tool/strap-taper-calculator/i18n/ko.ts +2 -2
  148. package/src/tool/strap-taper-calculator/i18n/nl.ts +3 -3
  149. package/src/tool/strap-taper-calculator/i18n/pl.ts +3 -3
  150. package/src/tool/strap-taper-calculator/i18n/pt.ts +3 -3
  151. package/src/tool/strap-taper-calculator/i18n/ru.ts +2 -2
  152. package/src/tool/strap-taper-calculator/i18n/sv.ts +3 -3
  153. package/src/tool/strap-taper-calculator/i18n/tr.ts +3 -3
  154. package/src/tool/strap-taper-calculator/i18n/zh.ts +3 -3
  155. package/src/tool/tachymeter-calculator/i18n/ru.ts +6 -6
  156. package/src/tool/telemeter-calculator/i18n/ru.ts +3 -3
  157. package/src/tool/telemeter-calculator/i18n/zh.ts +2 -2
  158. package/src/tool/tourbillon-visualizer/i18n/de.ts +3 -3
  159. package/src/tool/tourbillon-visualizer/i18n/en.ts +3 -3
  160. package/src/tool/tourbillon-visualizer/i18n/es.ts +3 -3
  161. package/src/tool/tourbillon-visualizer/i18n/fr.ts +3 -3
  162. package/src/tool/tourbillon-visualizer/i18n/id.ts +3 -3
  163. package/src/tool/tourbillon-visualizer/i18n/it.ts +3 -3
  164. package/src/tool/tourbillon-visualizer/i18n/ja.ts +2 -2
  165. package/src/tool/tourbillon-visualizer/i18n/ko.ts +2 -2
  166. package/src/tool/tourbillon-visualizer/i18n/nl.ts +3 -3
  167. package/src/tool/tourbillon-visualizer/i18n/pl.ts +3 -3
  168. package/src/tool/tourbillon-visualizer/i18n/pt.ts +3 -3
  169. package/src/tool/tourbillon-visualizer/i18n/ru.ts +5 -5
  170. package/src/tool/tourbillon-visualizer/i18n/sv.ts +3 -3
  171. package/src/tool/tourbillon-visualizer/i18n/tr.ts +3 -3
  172. package/src/tool/tourbillon-visualizer/i18n/zh.ts +3 -3
  173. package/src/tool/watch-accuracy-tracker/i18n/fr.ts +1 -1
  174. package/src/tool/watch-accuracy-tracker/i18n/zh.ts +1 -1
  175. package/src/tool/watch-savings-planner/i18n/de.ts +25 -25
  176. package/src/tool/watch-savings-planner/i18n/es.ts +39 -39
  177. package/src/tool/watch-savings-planner/i18n/fr.ts +51 -51
  178. package/src/tool/watch-savings-planner/i18n/id.ts +5 -5
  179. package/src/tool/watch-savings-planner/i18n/it.ts +29 -29
  180. package/src/tool/watch-savings-planner/i18n/ja.ts +73 -73
  181. package/src/tool/watch-savings-planner/i18n/ko.ts +73 -73
  182. package/src/tool/watch-savings-planner/i18n/nl.ts +10 -10
  183. package/src/tool/watch-savings-planner/i18n/pl.ts +5 -5
  184. package/src/tool/watch-savings-planner/i18n/pt.ts +43 -43
  185. package/src/tool/watch-savings-planner/i18n/ru.ts +7 -7
  186. package/src/tool/watch-savings-planner/i18n/sv.ts +54 -54
  187. package/src/tool/watch-savings-planner/i18n/tr.ts +50 -50
  188. package/src/tool/watch-savings-planner/i18n/zh.ts +73 -73
  189. package/src/tool/watch-size-comparator/i18n/de.ts +2 -2
  190. package/src/tool/watch-size-comparator/i18n/en.ts +3 -3
  191. package/src/tool/watch-size-comparator/i18n/es.ts +1 -1
  192. package/src/tool/watch-size-comparator/i18n/fr.ts +10 -10
  193. package/src/tool/watch-size-comparator/i18n/id.ts +9 -9
  194. package/src/tool/watch-size-comparator/i18n/it.ts +8 -8
  195. package/src/tool/watch-size-comparator/i18n/ja.ts +6 -6
  196. package/src/tool/watch-size-comparator/i18n/nl.ts +7 -7
  197. package/src/tool/watch-size-comparator/i18n/pl.ts +2 -2
  198. package/src/tool/watch-size-comparator/i18n/pt.ts +9 -9
  199. package/src/tool/watch-size-comparator/i18n/ru.ts +16 -16
  200. package/src/tool/watch-size-comparator/i18n/tr.ts +1 -1
  201. package/src/tool/watch-size-comparator/i18n/zh.ts +5 -5
  202. package/src/tool/water-resistance-converter/i18n/de.ts +1 -1
  203. package/src/tool/water-resistance-converter/i18n/en.ts +1 -1
  204. package/src/tool/water-resistance-converter/i18n/es.ts +1 -1
  205. package/src/tool/water-resistance-converter/i18n/fr.ts +2 -2
  206. package/src/tool/water-resistance-converter/i18n/id.ts +1 -1
  207. package/src/tool/water-resistance-converter/i18n/it.ts +1 -1
  208. package/src/tool/water-resistance-converter/i18n/ja.ts +1 -1
  209. package/src/tool/water-resistance-converter/i18n/ko.ts +1 -1
  210. package/src/tool/water-resistance-converter/i18n/nl.ts +1 -1
  211. package/src/tool/water-resistance-converter/i18n/pl.ts +1 -1
  212. package/src/tool/water-resistance-converter/i18n/pt.ts +1 -1
  213. package/src/tool/water-resistance-converter/i18n/ru.ts +1 -1
  214. package/src/tool/water-resistance-converter/i18n/sv.ts +1 -1
  215. package/src/tool/water-resistance-converter/i18n/tr.ts +1 -1
  216. package/src/tool/water-resistance-converter/i18n/zh.ts +4 -4
  217. package/src/tool/wrist-presence-calculator/i18n/en.ts +1 -1
  218. package/src/tool/wrist-presence-calculator/i18n/fr.ts +5 -5
  219. package/src/tool/wrist-presence-calculator/i18n/id.ts +1 -1
  220. package/src/tool/wrist-presence-calculator/i18n/zh.ts +1 -1
  221. package/src/tools.ts +2 -0
@@ -0,0 +1,198 @@
1
+ interface BatteryData {
2
+ id: string;
3
+ name: string;
4
+ capacity: number;
5
+ }
6
+
7
+ const batteries: BatteryData[] = [
8
+ { id: 'sr621sw', name: 'SR621SW (364)', capacity: 18 },
9
+ { id: 'sr626sw', name: 'SR626SW (377)', capacity: 27 },
10
+ { id: 'sr716sw', name: 'SR716SW (315)', capacity: 25 },
11
+ { id: 'sr721sw', name: 'SR721SW (361)', capacity: 32 },
12
+ { id: 'sr920sw', name: 'SR920SW (371)', capacity: 40 },
13
+ { id: 'sr936sw', name: 'SR936SW (394)', capacity: 55 },
14
+ { id: 'sr1130sw', name: 'SR1130SW (389)', capacity: 80 },
15
+ { id: 'sr44', name: 'SR44 (357)', capacity: 150 },
16
+ { id: 'cr2025', name: 'CR2025', capacity: 165 },
17
+ ];
18
+
19
+ const mainEl = document.querySelector('.tool-main-card') as HTMLElement;
20
+ const ui = mainEl ? JSON.parse(mainEl.dataset.ui || '{}') : {};
21
+
22
+ const batterySelect = document.getElementById('battery-select') as HTMLSelectElement;
23
+ const customFields = document.getElementById('custom-fields') as HTMLElement;
24
+ const customCapacity = document.getElementById('custom-capacity') as HTMLInputElement;
25
+ const consumptionInput = document.getElementById('consumption-input') as HTMLInputElement;
26
+ const installMonth = document.getElementById('install-month') as HTMLSelectElement;
27
+ const installYear = document.getElementById('install-year') as HTMLInputElement;
28
+ const calcBtn = document.getElementById('calc-btn') as HTMLButtonElement;
29
+ const resultCard = document.getElementById('result-card') as HTMLElement;
30
+ const lifeYears = document.getElementById('life-years') as HTMLElement;
31
+ const lifeMonths = document.getElementById('life-months') as HTMLElement;
32
+ const lifeDays = document.getElementById('life-days') as HTMLElement;
33
+ const gaugeFill = document.getElementById('gauge-fill') as HTMLElement;
34
+ const healthBadge = document.getElementById('health-badge') as HTMLElement;
35
+ const changeDateRow = document.getElementById('change-date-row') as HTMLElement;
36
+ const changeDateValue = document.getElementById('change-date-value') as HTMLElement;
37
+ const changeDateHint = document.getElementById('change-date-hint') as HTMLElement;
38
+
39
+ const STORAGE_KEY = 'quartz-battery-health-state';
40
+
41
+ interface SavedState {
42
+ battery: string;
43
+ customCapacity: string;
44
+ consumption: string;
45
+ month: string;
46
+ year: string;
47
+ }
48
+
49
+ function saveState() {
50
+ const state: SavedState = {
51
+ battery: batterySelect.value,
52
+ customCapacity: customCapacity.value,
53
+ consumption: consumptionInput.value,
54
+ month: installMonth.value,
55
+ year: installYear.value,
56
+ };
57
+ try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch {}
58
+ }
59
+
60
+ function setDateDefaults() {
61
+ installMonth.value = String(new Date().getMonth() + 1);
62
+ installYear.value = String(new Date().getFullYear());
63
+ }
64
+
65
+ function restoreState() {
66
+ try {
67
+ const raw = localStorage.getItem(STORAGE_KEY);
68
+ if (!raw) { setDateDefaults(); return; }
69
+ const state: SavedState = JSON.parse(raw);
70
+ if (state.battery) batterySelect.value = state.battery;
71
+ if (state.customCapacity) customCapacity.value = state.customCapacity;
72
+ if (state.consumption) consumptionInput.value = state.consumption;
73
+ if (state.month) installMonth.value = state.month;
74
+ if (state.year) installYear.value = state.year;
75
+ } catch { setDateDefaults(); }
76
+ }
77
+
78
+ function parseLocalizedFloat(value: string): number {
79
+ return parseFloat(value.replace(/,/g, '.')) || 0;
80
+ }
81
+
82
+ function getBattery(id: string): BatteryData | null {
83
+ return batteries.find((b) => b.id === id) || null;
84
+ }
85
+
86
+ function getCapacity(): number {
87
+ if (batterySelect.value === 'custom') {
88
+ return parseLocalizedFloat(customCapacity.value) || 40;
89
+ }
90
+ const b = getBattery(batterySelect.value);
91
+ return b ? b.capacity : 40;
92
+ }
93
+
94
+ function calcValue(input: HTMLInputElement, delta: number) {
95
+ const step = parseFloat(input.step) || 1;
96
+ const val = parseLocalizedFloat(input.value);
97
+ const min = parseFloat(input.min) || 0;
98
+ const max = parseFloat(input.max) || 100;
99
+ const next = Math.max(min, Math.min(max, Math.round((val + delta) * (1 / step)) / (1 / step)));
100
+ input.value = String(next);
101
+ }
102
+
103
+ function getHealthStatus(pct: number): { label: string; color: string } {
104
+ if (pct >= 50) return { label: ui.healthGood || 'Good', color: 'var(--color-success, #22c55e)' };
105
+ if (pct >= 20) return { label: ui.healthModerate || 'Moderate', color: 'var(--color-warning, #f59e0b)' };
106
+ return { label: ui.healthCritical || 'Critical', color: 'var(--color-danger, #ef4444)' };
107
+ }
108
+
109
+ function formatDate(d: Date): string {
110
+ return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
111
+ }
112
+
113
+ function updateHealth(totalDays: number) {
114
+ const month = parseInt(installMonth.value, 10);
115
+ const year = parseInt(installYear.value, 10);
116
+ if (!month || !year) {
117
+ gaugeFill.style.width = '0%';
118
+ healthBadge.textContent = '--';
119
+ changeDateRow.style.display = 'none';
120
+ changeDateHint.style.display = 'block';
121
+ return;
122
+ }
123
+ const installed = new Date(year, month - 1, 1);
124
+ const elapsedDays = (Date.now() - installed.getTime()) / 86400000;
125
+ const remainingPct = Math.max(0, Math.min(100, (1 - elapsedDays / totalDays) * 100));
126
+
127
+ gaugeFill.style.width = remainingPct + '%';
128
+ const health = getHealthStatus(remainingPct);
129
+ gaugeFill.style.background = `linear-gradient(90deg, ${health.color}, color-mix(in srgb, ${health.color} 70%, #fff 30%))`;
130
+ healthBadge.textContent = health.label;
131
+ healthBadge.style.background = health.color;
132
+
133
+ const replaceBy = new Date(installed.getTime() + totalDays * 86400000);
134
+ changeDateValue.textContent = formatDate(replaceBy);
135
+ changeDateRow.style.display = 'flex';
136
+ changeDateHint.style.display = 'none';
137
+ }
138
+
139
+ function calculate() {
140
+ const capacity = getCapacity();
141
+ const consumption = parseLocalizedFloat(consumptionInput.value) || 1.5;
142
+ if (consumption <= 0) return;
143
+
144
+ const totalDays = ((capacity * 1000) / consumption) / 24;
145
+ const totalYears = totalDays / 365.25;
146
+
147
+ const years = Math.floor(totalYears);
148
+ const months = Math.floor((totalYears - years) * 12);
149
+ const days = Math.floor(totalDays - (years * 365.25 + months * 30.4375));
150
+
151
+ lifeYears.textContent = String(years);
152
+ lifeMonths.textContent = String(months);
153
+ lifeDays.textContent = String(days);
154
+
155
+ updateHealth(totalDays);
156
+ resultCard.style.display = 'flex';
157
+ }
158
+
159
+ function toggleCustom() {
160
+ const show = batterySelect.value === 'custom';
161
+ customFields.style.display = show ? 'flex' : 'none';
162
+ }
163
+
164
+ function commit() {
165
+ saveState();
166
+ calculate();
167
+ }
168
+
169
+ batterySelect.addEventListener('change', () => {
170
+ toggleCustom();
171
+ commit();
172
+ });
173
+ calcBtn.addEventListener('click', commit);
174
+
175
+ [customCapacity, consumptionInput, installMonth, installYear].forEach((el) => {
176
+ el.addEventListener('keydown', (e) => {
177
+ if ((e as KeyboardEvent).key === 'Enter') commit();
178
+ });
179
+ el.addEventListener('input', commit);
180
+ });
181
+
182
+ document.querySelectorAll('.stepper-btn').forEach((btn) => {
183
+ btn.addEventListener('click', () => {
184
+ const target = document.getElementById(btn.getAttribute('data-target') || '') as HTMLInputElement;
185
+ const dir = parseFloat(btn.getAttribute('data-dir') || '1');
186
+ if (target) {
187
+ calcValue(target, dir);
188
+ commit();
189
+ }
190
+ });
191
+ });
192
+
193
+ restoreState();
194
+ toggleCustom();
195
+ commit();
196
+
197
+ export {};
198
+
@@ -0,0 +1,15 @@
1
+ ---
2
+ import BatteryHealthPanel from './components/BatteryHealthPanel.astro';
3
+
4
+ interface Props {
5
+ ui: Record<string, string>;
6
+ }
7
+
8
+ const { ui } = Astro.props;
9
+ ---
10
+
11
+ <div class="tool-main-card" data-ui={JSON.stringify(ui)}>
12
+ <BatteryHealthPanel labels={ui} />
13
+ </div>
14
+
15
+ <script src="./client.ts"></script>
@@ -0,0 +1,163 @@
1
+ ---
2
+ interface Props {
3
+ labels: Record<string, string>;
4
+ }
5
+
6
+ const { labels } = Astro.props;
7
+ ---
8
+
9
+ <div class="battery-panel">
10
+ <div class="panel-section">
11
+ <div class="section-label">{labels.batteryLabel || "Battery"}</div>
12
+ <select class="battery-select" id="battery-select">
13
+ <option value="sr621sw">SR621SW (364) – 18 mAh</option>
14
+ <option value="sr626sw">SR626SW (377) – 27 mAh</option>
15
+ <option value="sr716sw">SR716SW (315) – 25 mAh</option>
16
+ <option value="sr721sw">SR721SW (361) – 32 mAh</option>
17
+ <option value="sr920sw" selected>SR920SW (371) – 40 mAh</option>
18
+ <option value="sr936sw">SR936SW (394) – 55 mAh</option>
19
+ <option value="sr1130sw">SR1130SW (389) – 80 mAh</option>
20
+ <option value="sr44">SR44 (357) – 150 mAh</option>
21
+ <option value="cr2025">CR2025 – 165 mAh</option>
22
+ <option value="custom">{labels.customBattery || "Custom"}</option>
23
+ </select>
24
+ </div>
25
+
26
+ <div class="panel-section" id="custom-fields" style="display: none;">
27
+ <div class="section-label">{labels.capacityLabel || "Capacity"}</div>
28
+ <div class="input-row">
29
+ <div class="input-group">
30
+ <label class="input-label" for="custom-capacity">{labels.capacityUnit || "mAh"}</label>
31
+ <div class="stepper-wrap">
32
+ <button type="button" class="stepper-btn" data-target="custom-capacity" data-dir="-1">−</button>
33
+ <input type="number" class="stepper-input" id="custom-capacity" min="1" max="500" value="40" />
34
+ <button type="button" class="stepper-btn" data-target="custom-capacity" data-dir="1">+</button>
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </div>
39
+
40
+ <div class="panel-section">
41
+ <div class="section-label">{labels.consumptionLabel || "Consumption"}</div>
42
+ <div class="input-row">
43
+ <div class="input-group">
44
+ <label class="input-label" for="consumption-input">{labels.consumptionUnit || "µA"}</label>
45
+ <div class="stepper-wrap">
46
+ <button type="button" class="stepper-btn" data-target="consumption-input" data-dir="-0.1">−</button>
47
+ <input type="number" class="stepper-input" id="consumption-input" min="0.1" max="200" step="0.1" value="1.5" />
48
+ <button type="button" class="stepper-btn" data-target="consumption-input" data-dir="0.1">+</button>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </div>
53
+
54
+ <div class="panel-section">
55
+ <div class="section-label">
56
+ {labels.installDateLabel || "Installed on"}
57
+ <span class="label-optional">{labels.installDateHint || "Optional"}</span>
58
+ </div>
59
+ <div class="input-row">
60
+ <div class="input-group">
61
+ <label class="input-label" for="install-month">{labels.monthLabel || "Month"}</label>
62
+ <select class="battery-select" id="install-month">
63
+ <option value=""></option>
64
+ <option value="1">1</option>
65
+ <option value="2">2</option>
66
+ <option value="3">3</option>
67
+ <option value="4">4</option>
68
+ <option value="5">5</option>
69
+ <option value="6">6</option>
70
+ <option value="7">7</option>
71
+ <option value="8">8</option>
72
+ <option value="9">9</option>
73
+ <option value="10">10</option>
74
+ <option value="11">11</option>
75
+ <option value="12">12</option>
76
+ </select>
77
+ </div>
78
+ <div class="input-group">
79
+ <label class="input-label" for="install-year">{labels.yearLabel || "Year"}</label>
80
+ <div class="stepper-wrap">
81
+ <button type="button" class="stepper-btn" data-target="install-year" data-dir="-1">−</button>
82
+ <input type="number" class="stepper-input" id="install-year" min="1900" max="2100" value="2025" />
83
+ <button type="button" class="stepper-btn" data-target="install-year" data-dir="1">+</button>
84
+ </div>
85
+ </div>
86
+ </div>
87
+ </div>
88
+
89
+ <button type="button" class="calc-btn" id="calc-btn">
90
+ <svg viewBox="0 0 24 24" width="16" height="16">
91
+ <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" fill="currentColor"></path>
92
+ </svg>
93
+ {labels.calculate || "Calculate"}
94
+ </button>
95
+
96
+ <div class="result-card" id="result-card" style="display: none">
97
+ <div class="result-header">
98
+ <svg viewBox="0 0 24 24" width="18" height="18">
99
+ <path d="M13 2.05v2.02c3.95.49 7 3.85 7 7.93 0 4.42-3.58 8-8 8s-8-3.58-8-8c0-4.08 3.05-7.44 7-7.93V2.05C5.05 2.58 2 6.13 2 11c0 5.52 4.48 10 10 10s10-4.48 10-10c0-4.87-3.05-8.42-7-8.95zM12 6v5l4.28 2.54.72-1.21-3.5-2.08V6H12z" fill="currentColor"></path>
100
+ </svg>
101
+ <span>{labels.resultLabel || "Estimated Life"}</span>
102
+ </div>
103
+
104
+ <div class="life-stats">
105
+ <div class="life-stat">
106
+ <span class="stat-value" id="life-years">--</span>
107
+ <span class="stat-label">{labels.yearsLabel || "years"}</span>
108
+ </div>
109
+ <div class="life-stat">
110
+ <span class="stat-value" id="life-months">--</span>
111
+ <span class="stat-label">{labels.monthsLabel || "months"}</span>
112
+ </div>
113
+ <div class="life-stat">
114
+ <span class="stat-value" id="life-days">--</span>
115
+ <span class="stat-label">{labels.daysLabel || "days"}</span>
116
+ </div>
117
+ </div>
118
+
119
+ <div class="gauge-wrap">
120
+ <div class="gauge-track">
121
+ <div class="gauge-fill" id="gauge-fill"></div>
122
+ </div>
123
+ <div class="gauge-marks">
124
+ <span class="gauge-mark gauge-danger">0</span>
125
+ <span class="gauge-mark gauge-warning">50</span>
126
+ <span class="gauge-mark gauge-ok">100%</span>
127
+ </div>
128
+ </div>
129
+
130
+ <div class="health-row">
131
+ <span class="health-label">{labels.healthLabel || "Status"}</span>
132
+ <span class="health-badge" id="health-badge">--</span>
133
+ </div>
134
+
135
+ <div class="change-date-row" id="change-date-row" style="display: none">
136
+ <span class="change-date-label">{labels.changeDateLabel || "Replace by"}</span>
137
+ <span class="change-date-value" id="change-date-value">--</span>
138
+ </div>
139
+ <div class="change-date-hint" id="change-date-hint">{labels.noDateHint || "Enter install date to see replacement date"}</div>
140
+ </div>
141
+
142
+ <div class="steps-section">
143
+ <div class="step-row">
144
+ <div class="step-marker">1</div>
145
+ <span class="step-text">{labels.step1 || "Choose a common battery or select Custom to enter capacity."}</span>
146
+ </div>
147
+ <div class="step-row">
148
+ <div class="step-marker">2</div>
149
+ <span class="step-text">{labels.step2 || "Enter the caliber power consumption in microamperes (µA)."}</span>
150
+ </div>
151
+ <div class="step-row">
152
+ <div class="step-marker">3</div>
153
+ <span class="step-text">{labels.step3 || "Optionally add the installation date, then press Calculate."}</span>
154
+ </div>
155
+ </div>
156
+
157
+ <div class="tip-row">
158
+ <svg class="tip-icon" viewBox="0 0 24 24" width="16" height="16">
159
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" fill="currentColor"></path>
160
+ </svg>
161
+ <span class="tip-text">{labels.tipContent || "Always use the consumption value from the official caliber datasheet. Real-world life can be 10-20 % shorter due to temperature and load variations."}</span>
162
+ </div>
163
+ </div>
@@ -0,0 +1,57 @@
1
+ import type { ChronoToolEntry, ToolLocaleContent } from '../../types';
2
+
3
+ export type QuartzBatteryHealthUI = {
4
+ title: string;
5
+ batteryLabel: string;
6
+ selectBattery: string;
7
+ customBattery: string;
8
+ capacityLabel: string;
9
+ capacityUnit: string;
10
+ consumptionLabel: string;
11
+ consumptionUnit: string;
12
+ installDateLabel: string;
13
+ installDateHint: string;
14
+ monthLabel: string;
15
+ yearLabel: string;
16
+ calculate: string;
17
+ resultLabel: string;
18
+ theoreticalLife: string;
19
+ yearsLabel: string;
20
+ monthsLabel: string;
21
+ daysLabel: string;
22
+ changeDateLabel: string;
23
+ noDateHint: string;
24
+ healthLabel: string;
25
+ healthGood: string;
26
+ healthModerate: string;
27
+ healthCritical: string;
28
+ step1: string;
29
+ step2: string;
30
+ step3: string;
31
+ tipTitle: string;
32
+ tipContent: string;
33
+ };
34
+
35
+ export type QuartzBatteryHealthLocaleContent = ToolLocaleContent<QuartzBatteryHealthUI>;
36
+
37
+ export const quartzBatteryHealth: ChronoToolEntry<QuartzBatteryHealthUI> = {
38
+ id: 'quartz-battery-health',
39
+ icons: { bg: 'mdi:battery', fg: 'mdi:heart-pulse' },
40
+ i18n: {
41
+ en: () => import('./i18n/en').then((m) => m.content),
42
+ de: () => import('./i18n/de').then((m) => m.content),
43
+ es: () => import('./i18n/es').then((m) => m.content),
44
+ fr: () => import('./i18n/fr').then((m) => m.content),
45
+ id: () => import('./i18n/id').then((m) => m.content),
46
+ it: () => import('./i18n/it').then((m) => m.content),
47
+ ja: () => import('./i18n/ja').then((m) => m.content),
48
+ ko: () => import('./i18n/ko').then((m) => m.content),
49
+ nl: () => import('./i18n/nl').then((m) => m.content),
50
+ pl: () => import('./i18n/pl').then((m) => m.content),
51
+ pt: () => import('./i18n/pt').then((m) => m.content),
52
+ ru: () => import('./i18n/ru').then((m) => m.content),
53
+ sv: () => import('./i18n/sv').then((m) => m.content),
54
+ tr: () => import('./i18n/tr').then((m) => m.content),
55
+ zh: () => import('./i18n/zh').then((m) => m.content),
56
+ },
57
+ };