@jjlmoya/utils-chrono 1.2.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 (277) hide show
  1. package/package.json +65 -0
  2. package/scripts/postinstall.mjs +27 -0
  3. package/src/category/ChronoCategorySEO.astro +8 -0
  4. package/src/category/i18n/de.ts +23 -0
  5. package/src/category/i18n/en.ts +23 -0
  6. package/src/category/i18n/es.ts +23 -0
  7. package/src/category/i18n/fr.ts +23 -0
  8. package/src/category/i18n/id.ts +23 -0
  9. package/src/category/i18n/it.ts +23 -0
  10. package/src/category/i18n/ja.ts +23 -0
  11. package/src/category/i18n/ko.ts +23 -0
  12. package/src/category/i18n/nl.ts +23 -0
  13. package/src/category/i18n/pl.ts +23 -0
  14. package/src/category/i18n/pt.ts +23 -0
  15. package/src/category/i18n/ru.ts +23 -0
  16. package/src/category/i18n/sv.ts +23 -0
  17. package/src/category/i18n/tr.ts +23 -0
  18. package/src/category/i18n/zh.ts +23 -0
  19. package/src/category/index.ts +42 -0
  20. package/src/components/PreviewNavSidebar.astro +116 -0
  21. package/src/components/PreviewToolbar.astro +143 -0
  22. package/src/data.ts +11 -0
  23. package/src/entries.ts +32 -0
  24. package/src/env.d.ts +5 -0
  25. package/src/index.ts +28 -0
  26. package/src/layouts/PreviewLayout.astro +117 -0
  27. package/src/pages/[locale]/[slug].astro +161 -0
  28. package/src/pages/[locale].astro +251 -0
  29. package/src/pages/index.astro +4 -0
  30. package/src/tests/faq_count.test.ts +19 -0
  31. package/src/tests/i18n_coverage.test.ts +36 -0
  32. package/src/tests/locale_completeness.test.ts +29 -0
  33. package/src/tests/mocks/astro_mock.js +2 -0
  34. package/src/tests/no_h1_in_components.test.ts +48 -0
  35. package/src/tests/schemas_fulfillment.test.ts +23 -0
  36. package/src/tests/seo_length.test.ts +22 -0
  37. package/src/tests/shared-test-helpers.ts +56 -0
  38. package/src/tests/slug_language_code_format.test.ts +23 -0
  39. package/src/tests/slug_uniqueness.test.ts +81 -0
  40. package/src/tests/title_quality.test.ts +55 -0
  41. package/src/tests/tool_exports.test.ts +34 -0
  42. package/src/tests/tool_validation.test.ts +18 -0
  43. package/src/tool/beat-rate-converter/beat-rate-converter.css +301 -0
  44. package/src/tool/beat-rate-converter/bibliography.astro +16 -0
  45. package/src/tool/beat-rate-converter/bibliography.ts +12 -0
  46. package/src/tool/beat-rate-converter/client.ts +46 -0
  47. package/src/tool/beat-rate-converter/component.astro +15 -0
  48. package/src/tool/beat-rate-converter/components/ConverterPanel.astro +87 -0
  49. package/src/tool/beat-rate-converter/entry.ts +42 -0
  50. package/src/tool/beat-rate-converter/i18n/de.ts +138 -0
  51. package/src/tool/beat-rate-converter/i18n/en.ts +138 -0
  52. package/src/tool/beat-rate-converter/i18n/es.ts +138 -0
  53. package/src/tool/beat-rate-converter/i18n/fr.ts +138 -0
  54. package/src/tool/beat-rate-converter/i18n/id.ts +138 -0
  55. package/src/tool/beat-rate-converter/i18n/it.ts +138 -0
  56. package/src/tool/beat-rate-converter/i18n/ja.ts +138 -0
  57. package/src/tool/beat-rate-converter/i18n/ko.ts +138 -0
  58. package/src/tool/beat-rate-converter/i18n/nl.ts +138 -0
  59. package/src/tool/beat-rate-converter/i18n/pl.ts +138 -0
  60. package/src/tool/beat-rate-converter/i18n/pt.ts +138 -0
  61. package/src/tool/beat-rate-converter/i18n/ru.ts +138 -0
  62. package/src/tool/beat-rate-converter/i18n/sv.ts +138 -0
  63. package/src/tool/beat-rate-converter/i18n/tr.ts +138 -0
  64. package/src/tool/beat-rate-converter/i18n/zh.ts +138 -0
  65. package/src/tool/beat-rate-converter/index.ts +11 -0
  66. package/src/tool/beat-rate-converter/seo.astro +16 -0
  67. package/src/tool/crown-reference-guide/bibliography.astro +16 -0
  68. package/src/tool/crown-reference-guide/bibliography.ts +20 -0
  69. package/src/tool/crown-reference-guide/client.ts +193 -0
  70. package/src/tool/crown-reference-guide/component.astro +69 -0
  71. package/src/tool/crown-reference-guide/components/CrownView.astro +40 -0
  72. package/src/tool/crown-reference-guide/components/MovementSelector.astro +26 -0
  73. package/src/tool/crown-reference-guide/components/PositionPanel.astro +59 -0
  74. package/src/tool/crown-reference-guide/crown-reference-guide.css +383 -0
  75. package/src/tool/crown-reference-guide/entry.ts +60 -0
  76. package/src/tool/crown-reference-guide/i18n/de.ts +207 -0
  77. package/src/tool/crown-reference-guide/i18n/en.ts +207 -0
  78. package/src/tool/crown-reference-guide/i18n/es.ts +207 -0
  79. package/src/tool/crown-reference-guide/i18n/fr.ts +207 -0
  80. package/src/tool/crown-reference-guide/i18n/id.ts +207 -0
  81. package/src/tool/crown-reference-guide/i18n/it.ts +207 -0
  82. package/src/tool/crown-reference-guide/i18n/ja.ts +207 -0
  83. package/src/tool/crown-reference-guide/i18n/ko.ts +207 -0
  84. package/src/tool/crown-reference-guide/i18n/nl.ts +207 -0
  85. package/src/tool/crown-reference-guide/i18n/pl.ts +207 -0
  86. package/src/tool/crown-reference-guide/i18n/pt.ts +207 -0
  87. package/src/tool/crown-reference-guide/i18n/ru.ts +207 -0
  88. package/src/tool/crown-reference-guide/i18n/sv.ts +207 -0
  89. package/src/tool/crown-reference-guide/i18n/tr.ts +207 -0
  90. package/src/tool/crown-reference-guide/i18n/zh.ts +207 -0
  91. package/src/tool/crown-reference-guide/index.ts +11 -0
  92. package/src/tool/crown-reference-guide/seo.astro +16 -0
  93. package/src/tool/crown-reference-guide/utils.ts +0 -0
  94. package/src/tool/demagnetizing-timer/bibliography.astro +16 -0
  95. package/src/tool/demagnetizing-timer/bibliography.ts +12 -0
  96. package/src/tool/demagnetizing-timer/client.ts +221 -0
  97. package/src/tool/demagnetizing-timer/component.astro +15 -0
  98. package/src/tool/demagnetizing-timer/components/TimerPanel.astro +92 -0
  99. package/src/tool/demagnetizing-timer/demagnetizing-timer.css +389 -0
  100. package/src/tool/demagnetizing-timer/entry.ts +50 -0
  101. package/src/tool/demagnetizing-timer/helpers/field.ts +134 -0
  102. package/src/tool/demagnetizing-timer/i18n/de.ts +143 -0
  103. package/src/tool/demagnetizing-timer/i18n/en.ts +143 -0
  104. package/src/tool/demagnetizing-timer/i18n/es.ts +143 -0
  105. package/src/tool/demagnetizing-timer/i18n/fr.ts +143 -0
  106. package/src/tool/demagnetizing-timer/i18n/id.ts +143 -0
  107. package/src/tool/demagnetizing-timer/i18n/it.ts +143 -0
  108. package/src/tool/demagnetizing-timer/i18n/ja.ts +143 -0
  109. package/src/tool/demagnetizing-timer/i18n/ko.ts +143 -0
  110. package/src/tool/demagnetizing-timer/i18n/nl.ts +143 -0
  111. package/src/tool/demagnetizing-timer/i18n/pl.ts +143 -0
  112. package/src/tool/demagnetizing-timer/i18n/pt.ts +143 -0
  113. package/src/tool/demagnetizing-timer/i18n/ru.ts +143 -0
  114. package/src/tool/demagnetizing-timer/i18n/sv.ts +143 -0
  115. package/src/tool/demagnetizing-timer/i18n/tr.ts +143 -0
  116. package/src/tool/demagnetizing-timer/i18n/zh.ts +143 -0
  117. package/src/tool/demagnetizing-timer/index.ts +11 -0
  118. package/src/tool/demagnetizing-timer/seo.astro +16 -0
  119. package/src/tool/demagnetizing-timer/utils.ts +0 -0
  120. package/src/tool/power-reserve-estimator/bibliography.astro +16 -0
  121. package/src/tool/power-reserve-estimator/bibliography.ts +16 -0
  122. package/src/tool/power-reserve-estimator/client.ts +139 -0
  123. package/src/tool/power-reserve-estimator/component.astro +15 -0
  124. package/src/tool/power-reserve-estimator/components/EstimatorPanel.astro +150 -0
  125. package/src/tool/power-reserve-estimator/entry.ts +51 -0
  126. package/src/tool/power-reserve-estimator/i18n/de.ts +158 -0
  127. package/src/tool/power-reserve-estimator/i18n/en.ts +158 -0
  128. package/src/tool/power-reserve-estimator/i18n/es.ts +158 -0
  129. package/src/tool/power-reserve-estimator/i18n/fr.ts +158 -0
  130. package/src/tool/power-reserve-estimator/i18n/id.ts +158 -0
  131. package/src/tool/power-reserve-estimator/i18n/it.ts +158 -0
  132. package/src/tool/power-reserve-estimator/i18n/ja.ts +158 -0
  133. package/src/tool/power-reserve-estimator/i18n/ko.ts +158 -0
  134. package/src/tool/power-reserve-estimator/i18n/nl.ts +158 -0
  135. package/src/tool/power-reserve-estimator/i18n/pl.ts +158 -0
  136. package/src/tool/power-reserve-estimator/i18n/pt.ts +158 -0
  137. package/src/tool/power-reserve-estimator/i18n/ru.ts +158 -0
  138. package/src/tool/power-reserve-estimator/i18n/sv.ts +158 -0
  139. package/src/tool/power-reserve-estimator/i18n/tr.ts +158 -0
  140. package/src/tool/power-reserve-estimator/i18n/zh.ts +158 -0
  141. package/src/tool/power-reserve-estimator/index.ts +11 -0
  142. package/src/tool/power-reserve-estimator/power-reserve-estimator.css +402 -0
  143. package/src/tool/power-reserve-estimator/seo.astro +16 -0
  144. package/src/tool/strap-taper-calculator/bibliography.astro +16 -0
  145. package/src/tool/strap-taper-calculator/bibliography.ts +12 -0
  146. package/src/tool/strap-taper-calculator/client.ts +129 -0
  147. package/src/tool/strap-taper-calculator/component.astro +15 -0
  148. package/src/tool/strap-taper-calculator/components/CalculatorPanel.astro +114 -0
  149. package/src/tool/strap-taper-calculator/entry.ts +56 -0
  150. package/src/tool/strap-taper-calculator/i18n/de.ts +152 -0
  151. package/src/tool/strap-taper-calculator/i18n/en.ts +152 -0
  152. package/src/tool/strap-taper-calculator/i18n/es.ts +152 -0
  153. package/src/tool/strap-taper-calculator/i18n/fr.ts +152 -0
  154. package/src/tool/strap-taper-calculator/i18n/id.ts +152 -0
  155. package/src/tool/strap-taper-calculator/i18n/it.ts +152 -0
  156. package/src/tool/strap-taper-calculator/i18n/ja.ts +152 -0
  157. package/src/tool/strap-taper-calculator/i18n/ko.ts +152 -0
  158. package/src/tool/strap-taper-calculator/i18n/nl.ts +152 -0
  159. package/src/tool/strap-taper-calculator/i18n/pl.ts +152 -0
  160. package/src/tool/strap-taper-calculator/i18n/pt.ts +152 -0
  161. package/src/tool/strap-taper-calculator/i18n/ru.ts +152 -0
  162. package/src/tool/strap-taper-calculator/i18n/sv.ts +152 -0
  163. package/src/tool/strap-taper-calculator/i18n/tr.ts +152 -0
  164. package/src/tool/strap-taper-calculator/i18n/zh.ts +152 -0
  165. package/src/tool/strap-taper-calculator/index.ts +11 -0
  166. package/src/tool/strap-taper-calculator/seo.astro +16 -0
  167. package/src/tool/strap-taper-calculator/strap-taper-calculator.css +320 -0
  168. package/src/tool/watch-accuracy-tracker/bibliography.astro +16 -0
  169. package/src/tool/watch-accuracy-tracker/bibliography.ts +12 -0
  170. package/src/tool/watch-accuracy-tracker/chart.ts +126 -0
  171. package/src/tool/watch-accuracy-tracker/client.ts +287 -0
  172. package/src/tool/watch-accuracy-tracker/component.astro +35 -0
  173. package/src/tool/watch-accuracy-tracker/components/AccuracyTracker.astro +96 -0
  174. package/src/tool/watch-accuracy-tracker/components/DriftPredictor.astro +126 -0
  175. package/src/tool/watch-accuracy-tracker/components/LogHistory.astro +66 -0
  176. package/src/tool/watch-accuracy-tracker/entry.ts +94 -0
  177. package/src/tool/watch-accuracy-tracker/i18n/de.ts +167 -0
  178. package/src/tool/watch-accuracy-tracker/i18n/en.ts +167 -0
  179. package/src/tool/watch-accuracy-tracker/i18n/es.ts +167 -0
  180. package/src/tool/watch-accuracy-tracker/i18n/fr.ts +167 -0
  181. package/src/tool/watch-accuracy-tracker/i18n/id.ts +167 -0
  182. package/src/tool/watch-accuracy-tracker/i18n/it.ts +167 -0
  183. package/src/tool/watch-accuracy-tracker/i18n/ja.ts +167 -0
  184. package/src/tool/watch-accuracy-tracker/i18n/ko.ts +167 -0
  185. package/src/tool/watch-accuracy-tracker/i18n/nl.ts +167 -0
  186. package/src/tool/watch-accuracy-tracker/i18n/pl.ts +167 -0
  187. package/src/tool/watch-accuracy-tracker/i18n/pt.ts +167 -0
  188. package/src/tool/watch-accuracy-tracker/i18n/ru.ts +167 -0
  189. package/src/tool/watch-accuracy-tracker/i18n/sv.ts +167 -0
  190. package/src/tool/watch-accuracy-tracker/i18n/tr.ts +167 -0
  191. package/src/tool/watch-accuracy-tracker/i18n/zh.ts +167 -0
  192. package/src/tool/watch-accuracy-tracker/index.ts +9 -0
  193. package/src/tool/watch-accuracy-tracker/logger.ts +105 -0
  194. package/src/tool/watch-accuracy-tracker/seo.astro +16 -0
  195. package/src/tool/watch-accuracy-tracker/utils.ts +99 -0
  196. package/src/tool/watch-accuracy-tracker/watch-accuracy-tracker.css +564 -0
  197. package/src/tool/watch-savings-planner/bibliography.astro +16 -0
  198. package/src/tool/watch-savings-planner/bibliography.ts +8 -0
  199. package/src/tool/watch-savings-planner/client.ts +271 -0
  200. package/src/tool/watch-savings-planner/component.astro +33 -0
  201. package/src/tool/watch-savings-planner/components/AddGoalForm.astro +49 -0
  202. package/src/tool/watch-savings-planner/components/GoalCard.astro +58 -0
  203. package/src/tool/watch-savings-planner/entry.ts +62 -0
  204. package/src/tool/watch-savings-planner/i18n/de.ts +153 -0
  205. package/src/tool/watch-savings-planner/i18n/en.ts +155 -0
  206. package/src/tool/watch-savings-planner/i18n/es.ts +153 -0
  207. package/src/tool/watch-savings-planner/i18n/fr.ts +153 -0
  208. package/src/tool/watch-savings-planner/i18n/id.ts +153 -0
  209. package/src/tool/watch-savings-planner/i18n/it.ts +153 -0
  210. package/src/tool/watch-savings-planner/i18n/ja.ts +153 -0
  211. package/src/tool/watch-savings-planner/i18n/ko.ts +153 -0
  212. package/src/tool/watch-savings-planner/i18n/nl.ts +153 -0
  213. package/src/tool/watch-savings-planner/i18n/pl.ts +153 -0
  214. package/src/tool/watch-savings-planner/i18n/pt.ts +153 -0
  215. package/src/tool/watch-savings-planner/i18n/ru.ts +153 -0
  216. package/src/tool/watch-savings-planner/i18n/sv.ts +153 -0
  217. package/src/tool/watch-savings-planner/i18n/tr.ts +153 -0
  218. package/src/tool/watch-savings-planner/i18n/zh.ts +153 -0
  219. package/src/tool/watch-savings-planner/index.ts +11 -0
  220. package/src/tool/watch-savings-planner/seo.astro +16 -0
  221. package/src/tool/watch-savings-planner/utils.ts +0 -0
  222. package/src/tool/watch-savings-planner/watch-savings-planner.css +460 -0
  223. package/src/tool/water-resistance-converter/bibliography.astro +16 -0
  224. package/src/tool/water-resistance-converter/bibliography.ts +16 -0
  225. package/src/tool/water-resistance-converter/client.ts +56 -0
  226. package/src/tool/water-resistance-converter/component.astro +15 -0
  227. package/src/tool/water-resistance-converter/components/ConverterPanel.astro +113 -0
  228. package/src/tool/water-resistance-converter/entry.ts +52 -0
  229. package/src/tool/water-resistance-converter/i18n/de.ts +148 -0
  230. package/src/tool/water-resistance-converter/i18n/en.ts +148 -0
  231. package/src/tool/water-resistance-converter/i18n/es.ts +148 -0
  232. package/src/tool/water-resistance-converter/i18n/fr.ts +148 -0
  233. package/src/tool/water-resistance-converter/i18n/id.ts +148 -0
  234. package/src/tool/water-resistance-converter/i18n/it.ts +148 -0
  235. package/src/tool/water-resistance-converter/i18n/ja.ts +148 -0
  236. package/src/tool/water-resistance-converter/i18n/ko.ts +148 -0
  237. package/src/tool/water-resistance-converter/i18n/nl.ts +148 -0
  238. package/src/tool/water-resistance-converter/i18n/pl.ts +148 -0
  239. package/src/tool/water-resistance-converter/i18n/pt.ts +148 -0
  240. package/src/tool/water-resistance-converter/i18n/ru.ts +148 -0
  241. package/src/tool/water-resistance-converter/i18n/sv.ts +148 -0
  242. package/src/tool/water-resistance-converter/i18n/tr.ts +148 -0
  243. package/src/tool/water-resistance-converter/i18n/zh.ts +148 -0
  244. package/src/tool/water-resistance-converter/index.ts +11 -0
  245. package/src/tool/water-resistance-converter/seo.astro +16 -0
  246. package/src/tool/water-resistance-converter/water-resistance-converter.css +254 -0
  247. package/src/tool/wrist-presence-calculator/bibliography.astro +16 -0
  248. package/src/tool/wrist-presence-calculator/bibliography.ts +12 -0
  249. package/src/tool/wrist-presence-calculator/client.ts +180 -0
  250. package/src/tool/wrist-presence-calculator/component.astro +23 -0
  251. package/src/tool/wrist-presence-calculator/components/CalculatorInputs.astro +97 -0
  252. package/src/tool/wrist-presence-calculator/components/FitResult.astro +68 -0
  253. package/src/tool/wrist-presence-calculator/components/Visualizer.astro +16 -0
  254. package/src/tool/wrist-presence-calculator/entry.ts +59 -0
  255. package/src/tool/wrist-presence-calculator/helpers/canvas.ts +189 -0
  256. package/src/tool/wrist-presence-calculator/helpers/results.ts +78 -0
  257. package/src/tool/wrist-presence-calculator/i18n/de.ts +139 -0
  258. package/src/tool/wrist-presence-calculator/i18n/en.ts +155 -0
  259. package/src/tool/wrist-presence-calculator/i18n/es.ts +139 -0
  260. package/src/tool/wrist-presence-calculator/i18n/fr.ts +139 -0
  261. package/src/tool/wrist-presence-calculator/i18n/id.ts +139 -0
  262. package/src/tool/wrist-presence-calculator/i18n/it.ts +139 -0
  263. package/src/tool/wrist-presence-calculator/i18n/ja.ts +139 -0
  264. package/src/tool/wrist-presence-calculator/i18n/ko.ts +139 -0
  265. package/src/tool/wrist-presence-calculator/i18n/nl.ts +139 -0
  266. package/src/tool/wrist-presence-calculator/i18n/pl.ts +139 -0
  267. package/src/tool/wrist-presence-calculator/i18n/pt.ts +139 -0
  268. package/src/tool/wrist-presence-calculator/i18n/ru.ts +139 -0
  269. package/src/tool/wrist-presence-calculator/i18n/sv.ts +139 -0
  270. package/src/tool/wrist-presence-calculator/i18n/tr.ts +139 -0
  271. package/src/tool/wrist-presence-calculator/i18n/zh.ts +155 -0
  272. package/src/tool/wrist-presence-calculator/index.ts +11 -0
  273. package/src/tool/wrist-presence-calculator/seo.astro +16 -0
  274. package/src/tool/wrist-presence-calculator/utils.ts +30 -0
  275. package/src/tool/wrist-presence-calculator/wrist-presence-calculator.css +372 -0
  276. package/src/tools.ts +26 -0
  277. package/src/types.ts +70 -0
@@ -0,0 +1,99 @@
1
+ export function formatDrift(seconds: number): string {
2
+ const absSec = Math.abs(seconds);
3
+ const sign = seconds >= 0 ? '+' : '-';
4
+ if (absSec < 60) {
5
+ return `${sign}${absSec.toFixed(1)}s`;
6
+ }
7
+ const mins = Math.floor(absSec / 60);
8
+ const secs = Math.round(absSec % 60);
9
+ if (mins < 60) {
10
+ return `${sign}${mins}m ${secs}s`;
11
+ }
12
+ const hrs = Math.floor(mins / 60);
13
+ const remMins = mins % 60;
14
+ return `${sign}${hrs}h ${remMins}m`;
15
+ }
16
+
17
+ export function calculateIntervalRate(
18
+ currentOffset: number,
19
+ prevOffset: number,
20
+ currentTime: string,
21
+ prevTime: string
22
+ ): number | null {
23
+ const timeDiff = new Date(currentTime).getTime() - new Date(prevTime).getTime();
24
+ const daysDiff = timeDiff / (1000 * 60 * 60 * 24);
25
+ if (daysDiff > 0.01) {
26
+ return (currentOffset - prevOffset) / daysDiff;
27
+ }
28
+ return null;
29
+ }
30
+
31
+ export function calculateAverageRate(
32
+ firstOffset: number,
33
+ lastOffset: number,
34
+ firstTime: string,
35
+ lastTime: string
36
+ ): number | null {
37
+ const overallTimeDiff = new Date(lastTime).getTime() - new Date(firstTime).getTime();
38
+ const overallDays = overallTimeDiff / (1000 * 60 * 60 * 24);
39
+ if (overallDays > 0.01) {
40
+ return (lastOffset - firstOffset) / overallDays;
41
+ }
42
+ return null;
43
+ }
44
+
45
+ export function getPrecisionStatus(
46
+ avgRate: number,
47
+ ui: Record<string, string>
48
+ ): { text: string; cssClass: string } {
49
+ const absRate = Math.abs(avgRate);
50
+ if (absRate <= 2) {
51
+ return {
52
+ text: ui.coscExcellent || 'Rolex Superlative',
53
+ cssClass: 'stat-value status-superlative'
54
+ };
55
+ }
56
+ if (absRate <= 6) {
57
+ return {
58
+ text: ui.coscExcellent || 'COSC Excellent',
59
+ cssClass: 'stat-value status-excellent'
60
+ };
61
+ }
62
+ if (absRate > 20) {
63
+ return {
64
+ text: ui.needsService || 'Needs Service',
65
+ cssClass: 'stat-value status-service'
66
+ };
67
+ }
68
+ return {
69
+ text: ui.good || 'Good',
70
+ cssClass: 'stat-value status-good'
71
+ };
72
+ }
73
+
74
+ export function getTernaryDriftClass(rate: number): string {
75
+ if (rate > 0) {
76
+ return 'drift-val ahead';
77
+ }
78
+ if (rate < 0) {
79
+ return 'drift-val behind';
80
+ }
81
+ return 'drift-val neutral';
82
+ }
83
+
84
+ export function getExplanationForRate(avgRate: number): string {
85
+ const absRate = Math.abs(avgRate);
86
+ if (absRate <= 2) {
87
+ return 'Rolex Superlative: Your watch is running with exceptional precision, matching Rolex\'s Superlative Chronometer standards (±2 s/d). This is outstanding timekeeping.';
88
+ }
89
+ if (absRate <= 6) {
90
+ return 'COSC Chronometer: Excellent precision, within official Swiss Chronometer standards (-4 to +6 s/d). Your watch performs exceptionally well.';
91
+ }
92
+ if (absRate <= 10) {
93
+ return 'Excellent: Excellent performance. Gaining or losing under 10 seconds a day keeps your timepiece highly accurate for everyday use.';
94
+ }
95
+ if (absRate <= 20) {
96
+ return 'Good: Good everyday timekeeping. Standard mechanical movements are typically regulated within this range.';
97
+ }
98
+ return 'Needs Service: Significant rate deviation detected. Your watch might be magnetized, need regulation, or require a movement service (cleaning and oiling).';
99
+ }
@@ -0,0 +1,564 @@
1
+ :root {
2
+ --color-success: #10b981;
3
+ --color-success-hover: #059669;
4
+ --color-success-light: #34d399;
5
+ --color-danger: #ef4444;
6
+ --color-info: #60a5fa;
7
+ --color-warning: #f87171;
8
+ --color-white: #fff;
9
+ --color-success-bg: rgba(16, 185, 129, 0.05);
10
+ --color-chart-bg: #0f172a;
11
+ --color-accent-bg: rgba(244, 63, 94, 0.05);
12
+ }
13
+
14
+ .watch-tracker-wrapper {
15
+ display: flex;
16
+ flex-direction: column;
17
+ gap: 2rem;
18
+ color: var(--text-base);
19
+ width: 100%;
20
+ }
21
+
22
+ .tool-main-card {
23
+ background: var(--bg-surface);
24
+ border: 1px solid var(--border-color);
25
+ border-radius: 1rem;
26
+ padding: 2rem;
27
+ display: flex;
28
+ flex-direction: column;
29
+ gap: 2rem;
30
+ box-shadow: 0 4px 20px -2px rgba(0, 0, 0, 0.2);
31
+ }
32
+
33
+ .tabs-header {
34
+ display: flex;
35
+ gap: 0.5rem;
36
+ background: var(--bg-page);
37
+ padding: 0.35rem;
38
+ border-radius: 0.75rem;
39
+ border: 1px solid var(--border-color);
40
+ max-width: fit-content;
41
+ }
42
+
43
+ .tab-btn {
44
+ background: transparent;
45
+ border: none;
46
+ color: var(--text-muted);
47
+ padding: 0.6rem 1.25rem;
48
+ border-radius: 0.5rem;
49
+ font-size: 0.9375rem;
50
+ font-weight: 600;
51
+ cursor: pointer;
52
+ transition: all 0.2s ease;
53
+ }
54
+
55
+ .tab-btn:hover {
56
+ color: var(--text-base);
57
+ background: rgba(255, 255, 255, 0.04);
58
+ }
59
+
60
+ .tab-btn.active {
61
+ color: var(--accent);
62
+ background: rgba(244, 63, 94, 0.1);
63
+ }
64
+
65
+ .tab-content {
66
+ display: none;
67
+ }
68
+
69
+ .tab-content.active {
70
+ display: block;
71
+ animation: fade-in 0.25s ease-out;
72
+ }
73
+
74
+ @keyframes fade-in {
75
+ from {
76
+ opacity: 0;
77
+ transform: translateY(4px);
78
+ }
79
+ to {
80
+ opacity: 1;
81
+ transform: translateY(0);
82
+ }
83
+ }
84
+
85
+ .predictor-grid {
86
+ display: grid;
87
+ grid-template-columns: 1fr;
88
+ gap: 2rem;
89
+ margin-bottom: 2rem;
90
+ }
91
+
92
+ @media (min-width: 768px) {
93
+ .predictor-grid {
94
+ grid-template-columns: 1fr 1fr;
95
+ }
96
+ }
97
+
98
+ .tracker-layout {
99
+ display: grid;
100
+ grid-template-columns: 1fr;
101
+ gap: 2rem;
102
+ }
103
+
104
+ @media (min-width: 992px) {
105
+ .tracker-layout {
106
+ grid-template-columns: 4fr 5fr;
107
+ }
108
+ }
109
+
110
+ .drift-predictor-section,
111
+ .accuracy-tracker-section,
112
+ .log-history-section {
113
+ display: flex;
114
+ flex-direction: column;
115
+ gap: 1.5rem;
116
+ }
117
+
118
+ .history-header {
119
+ display: flex;
120
+ justify-content: space-between;
121
+ align-items: center;
122
+ }
123
+
124
+ .history-header h4 {
125
+ margin: 0;
126
+ font-size: 1.1rem;
127
+ font-weight: 700;
128
+ }
129
+
130
+ .control-label {
131
+ display: block;
132
+ font-size: 0.875rem;
133
+ font-weight: 600;
134
+ color: var(--text-muted);
135
+ margin-bottom: 0.5rem;
136
+ }
137
+
138
+ .form-control {
139
+ width: 100%;
140
+ background: var(--bg-page);
141
+ border: 1px solid var(--border-color);
142
+ color: var(--text-base);
143
+ padding: 0.75rem 1rem;
144
+ border-radius: 0.5rem;
145
+ font-size: 0.9375rem;
146
+ outline: none;
147
+ transition: all 0.2s ease;
148
+ }
149
+
150
+ .form-control:focus {
151
+ border-color: var(--accent);
152
+ box-shadow: 0 0 0 2px rgba(244, 63, 94, 0.15);
153
+ }
154
+
155
+ select.form-control {
156
+ appearance: none;
157
+ -webkit-appearance: none;
158
+ -moz-appearance: none;
159
+ background: var(--bg-page) url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23f43f5e' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E") no-repeat right 1rem center/1rem;
160
+ padding-right: 2.75rem;
161
+ cursor: pointer;
162
+ }
163
+
164
+ .input-with-unit {
165
+ position: relative;
166
+ display: flex;
167
+ align-items: center;
168
+ }
169
+
170
+ .input-with-unit .form-control {
171
+ padding-right: 3rem;
172
+ }
173
+
174
+ .unit-badge {
175
+ position: absolute;
176
+ right: 1rem;
177
+ font-size: 0.8125rem;
178
+ font-weight: 700;
179
+ color: var(--accent);
180
+ }
181
+
182
+ .presets-container {
183
+ display: flex;
184
+ flex-wrap: wrap;
185
+ gap: 0.5rem;
186
+ margin-top: 0.75rem;
187
+ }
188
+
189
+ .preset-btn {
190
+ background: var(--bg-page);
191
+ border: 1px solid var(--border-color);
192
+ color: var(--text-muted);
193
+ padding: 0.35rem 0.75rem;
194
+ border-radius: 0.375rem;
195
+ font-size: 0.8125rem;
196
+ font-weight: 600;
197
+ cursor: pointer;
198
+ transition: all 0.15s ease;
199
+ }
200
+
201
+ .preset-btn:hover {
202
+ border-color: var(--accent);
203
+ color: var(--accent);
204
+ background: rgba(244, 63, 94, 0.05);
205
+ }
206
+
207
+ .projection-table-wrapper {
208
+ overflow-x: auto;
209
+ border: 1px solid var(--border-color);
210
+ border-radius: 0.5rem;
211
+ background: var(--bg-page);
212
+ }
213
+
214
+ .projection-table,
215
+ .history-table {
216
+ width: 100%;
217
+ border-collapse: collapse;
218
+ text-align: left;
219
+ font-size: 0.875rem;
220
+ }
221
+
222
+ .projection-table th,
223
+ .projection-table td,
224
+ .history-table th,
225
+ .history-table td {
226
+ padding: 0.75rem 1rem;
227
+ border-bottom: 1px solid var(--border-color);
228
+ }
229
+
230
+ .projection-table th,
231
+ .history-table th {
232
+ background: rgba(255, 255, 255, 0.02);
233
+ font-weight: 600;
234
+ color: var(--text-muted);
235
+ }
236
+
237
+ .drift-val {
238
+ font-weight: 700;
239
+ font-size: 0.9375rem;
240
+ }
241
+
242
+ .drift-val.ahead {
243
+ color: var(--color-success);
244
+ }
245
+
246
+ .drift-val.behind {
247
+ color: var(--color-danger);
248
+ }
249
+
250
+ .drift-val.neutral {
251
+ color: var(--text-muted);
252
+ }
253
+
254
+ .standards-section {
255
+ border-top: 1px solid var(--border-color);
256
+ padding-top: 1.5rem;
257
+ }
258
+
259
+ .standards-section h4,
260
+ .projections-section h4 {
261
+ margin: 0 0 1rem;
262
+ font-size: 1.1rem;
263
+ font-weight: 700;
264
+ }
265
+
266
+ .standards-grid {
267
+ display: grid;
268
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
269
+ gap: 1rem;
270
+ }
271
+
272
+ .standard-card {
273
+ background: var(--bg-page);
274
+ border: 1px solid var(--border-color);
275
+ border-radius: 0.75rem;
276
+ padding: 1rem;
277
+ display: flex;
278
+ flex-direction: column;
279
+ gap: 0.5rem;
280
+ transition: all 0.2s ease;
281
+ }
282
+
283
+ .std-header {
284
+ display: flex;
285
+ justify-content: space-between;
286
+ align-items: center;
287
+ }
288
+
289
+ .std-name {
290
+ font-size: 0.8125rem;
291
+ font-weight: 600;
292
+ color: var(--text-muted);
293
+ }
294
+
295
+ .std-status {
296
+ font-size: 0.6875rem;
297
+ font-weight: 800;
298
+ text-transform: uppercase;
299
+ letter-spacing: 0.05em;
300
+ }
301
+
302
+ .std-tolerance {
303
+ font-size: 1rem;
304
+ font-weight: 700;
305
+ }
306
+
307
+ .standard-card.passed {
308
+ border-color: var(--color-success);
309
+ background: var(--color-success-bg);
310
+ }
311
+
312
+ .standard-card.passed .std-status {
313
+ color: var(--color-success);
314
+ }
315
+
316
+ .standard-card.failed {
317
+ border-color: var(--border-color);
318
+ opacity: 0.5;
319
+ }
320
+
321
+ .standard-card.failed .std-status {
322
+ color: var(--text-muted);
323
+ }
324
+
325
+ .watch-row-controls {
326
+ display: flex;
327
+ gap: 0.5rem;
328
+ width: 100%;
329
+ }
330
+
331
+ .watch-row-controls select {
332
+ flex: 1;
333
+ }
334
+
335
+ #delete-watch-btn {
336
+ padding: 0.75rem;
337
+ aspect-ratio: 1;
338
+ display: flex;
339
+ align-items: center;
340
+ justify-content: center;
341
+ }
342
+
343
+ .input-with-button {
344
+ display: flex;
345
+ gap: 0.5rem;
346
+ }
347
+
348
+ .btn {
349
+ display: inline-flex;
350
+ align-items: center;
351
+ justify-content: center;
352
+ gap: 0.5rem;
353
+ border: none;
354
+ font-weight: 600;
355
+ font-size: 0.875rem;
356
+ padding: 0.75rem 1.25rem;
357
+ border-radius: 0.5rem;
358
+ cursor: pointer;
359
+ transition: all 0.2s ease;
360
+ }
361
+
362
+ .btn-sm {
363
+ padding: 0.4rem 0.75rem;
364
+ font-size: 0.8125rem;
365
+ border-radius: 0.375rem;
366
+ }
367
+
368
+ .btn-full {
369
+ width: 100%;
370
+ }
371
+
372
+ .btn-primary {
373
+ background: var(--accent);
374
+ color: var(--color-white);
375
+ }
376
+
377
+ .btn-primary:hover {
378
+ background: #e11d48;
379
+ }
380
+
381
+ .btn-success {
382
+ background: var(--color-success);
383
+ color: var(--color-white);
384
+ }
385
+
386
+ .btn-success:hover {
387
+ background: var(--color-success-hover);
388
+ }
389
+
390
+ .btn-danger {
391
+ background: rgba(239, 68, 68, 0.15);
392
+ border: 1px solid rgba(239, 68, 68, 0.3);
393
+ color: var(--color-danger);
394
+ }
395
+
396
+ .btn-danger:hover {
397
+ background: var(--color-danger);
398
+ color: var(--color-white);
399
+ }
400
+
401
+ .btn-text {
402
+ background: transparent;
403
+ border: none;
404
+ color: var(--accent);
405
+ font-size: 0.8125rem;
406
+ font-weight: 600;
407
+ cursor: pointer;
408
+ }
409
+
410
+ .btn-text:hover {
411
+ text-decoration: underline;
412
+ }
413
+
414
+ .watch-selection-panel {
415
+ display: flex;
416
+ flex-direction: column;
417
+ gap: 1rem;
418
+ background: rgba(255, 255, 255, 0.01);
419
+ padding: 1rem;
420
+ border-radius: 0.75rem;
421
+ border: 1px solid var(--border-color);
422
+ }
423
+
424
+ .log-form-section {
425
+ display: flex;
426
+ flex-direction: column;
427
+ gap: 1.25rem;
428
+ transition: all 0.2s ease;
429
+ }
430
+
431
+ .log-form-section h4 {
432
+ margin: 0;
433
+ font-size: 1.1rem;
434
+ font-weight: 700;
435
+ }
436
+
437
+ .log-form-section.disabled-form {
438
+ opacity: 0.25;
439
+ pointer-events: none;
440
+ }
441
+
442
+ .form-grid {
443
+ display: grid;
444
+ grid-template-columns: 1fr;
445
+ gap: 1rem;
446
+ }
447
+
448
+ @media (min-width: 768px) {
449
+ .form-grid {
450
+ grid-template-columns: 1fr 1fr 1fr;
451
+ }
452
+ }
453
+
454
+ .label-row {
455
+ display: flex;
456
+ justify-content: space-between;
457
+ align-items: center;
458
+ }
459
+
460
+ .stats-grid {
461
+ display: grid;
462
+ grid-template-columns: repeat(3, 1fr);
463
+ gap: 1rem;
464
+ }
465
+
466
+ .stat-box {
467
+ background: var(--bg-page);
468
+ border: 1px solid var(--border-color);
469
+ padding: 1rem;
470
+ border-radius: 0.75rem;
471
+ display: flex;
472
+ flex-direction: column;
473
+ gap: 0.25rem;
474
+ }
475
+
476
+ .stat-label {
477
+ font-size: 0.75rem;
478
+ font-weight: 600;
479
+ color: var(--text-muted);
480
+ text-transform: uppercase;
481
+ }
482
+
483
+ .stat-value {
484
+ font-size: 1.125rem;
485
+ font-weight: 700;
486
+ }
487
+
488
+ .status-superlative {
489
+ color: var(--color-success);
490
+ }
491
+
492
+ .status-excellent {
493
+ color: var(--color-success-light);
494
+ }
495
+
496
+ .status-good {
497
+ color: var(--color-info);
498
+ }
499
+
500
+ .status-service {
501
+ color: var(--color-warning);
502
+ }
503
+
504
+ .no-logs-msg {
505
+ padding: 2rem;
506
+ text-align: center;
507
+ border: 1px dashed var(--border-color);
508
+ border-radius: 0.75rem;
509
+ color: var(--text-muted);
510
+ font-size: 0.875rem;
511
+ }
512
+
513
+ .no-logs-msg p {
514
+ margin: 0;
515
+ }
516
+
517
+ .table-wrapper {
518
+ border: 1px solid var(--border-color);
519
+ border-radius: 0.5rem;
520
+ background: var(--bg-page);
521
+ max-height: 300px;
522
+ overflow-y: auto;
523
+ }
524
+
525
+ .header-actions-row {
526
+ display: flex;
527
+ gap: 0.5rem;
528
+ }
529
+
530
+ .chart-card-wrapper {
531
+ background: var(--color-chart-bg);
532
+ padding: 1rem;
533
+ border-radius: 0.75rem;
534
+ border: 1px solid var(--border-color);
535
+ margin-bottom: 1rem;
536
+ }
537
+
538
+ .chart-card-wrapper canvas {
539
+ width: 100%;
540
+ height: 180px;
541
+ display: block;
542
+ }
543
+
544
+ .explanation-box {
545
+ background: var(--color-accent-bg);
546
+ border: 1px solid var(--accent);
547
+ border-radius: 0.5rem;
548
+ padding: 1rem;
549
+ font-size: 0.875rem;
550
+ color: var(--text-base);
551
+ line-height: 1.5;
552
+ }
553
+
554
+ .pos-badge {
555
+ background: rgba(255, 255, 255, 0.08);
556
+ border: 1px solid var(--border-color);
557
+ color: var(--text-muted);
558
+ font-size: 0.7rem;
559
+ font-weight: 700;
560
+ padding: 0.2rem 0.4rem;
561
+ border-radius: 0.3rem;
562
+ margin-left: 0.5rem;
563
+ text-transform: uppercase;
564
+ }
@@ -0,0 +1,16 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { watchSavingsPlanner } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props as Props;
11
+ const loader = watchSavingsPlanner.i18n[locale] || watchSavingsPlanner.i18n.en;
12
+ const content = await loader?.();
13
+ if (!content) return null;
14
+ ---
15
+
16
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,8 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Personal Finance for Watch Enthusiasts',
6
+ url: 'https://marketing.yourasset.com/blog/en/blog/the-complete-guide-to-luxury-watch-financing',
7
+ },
8
+ ];