@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,143 @@
1
+ ---
2
+ import type { KnownLocale } from '../types';
3
+
4
+ interface Props {
5
+ currentLocale: KnownLocale;
6
+ localeUrls?: Partial<Record<KnownLocale, string>>;
7
+ }
8
+
9
+ const { currentLocale, localeUrls = {} } = Astro.props;
10
+
11
+ const otherLocales = Object.entries(localeUrls).filter(([l]) => l !== currentLocale) as [
12
+ KnownLocale,
13
+ string,
14
+ ][];
15
+ ---
16
+
17
+ <nav class="preview-toolbar">
18
+ {
19
+ otherLocales.length > 0 && (
20
+ <div class="locale-group">
21
+ <span class="locale-current">{currentLocale.toUpperCase()}</span>
22
+ {otherLocales.map(([locale, url]) => (
23
+ <a href={url} class="btn-locale">
24
+ {locale.toUpperCase()}
25
+ </a>
26
+ ))}
27
+ </div>
28
+ )
29
+ }
30
+
31
+ <button id="theme-toggle" class="btn-theme" aria-label="Toggle theme" title="Toggle theme">
32
+ <span class="theme-icon"></span>
33
+ </button>
34
+ </nav>
35
+
36
+ <script>
37
+ function initToolbar() {
38
+ const btn = document.getElementById('theme-toggle');
39
+ if (!btn) return;
40
+
41
+ const applyTheme = (theme: string) => {
42
+ document.documentElement.classList.remove('theme-light', 'theme-dark');
43
+ document.documentElement.classList.add(theme);
44
+ localStorage.setItem('theme', theme);
45
+ };
46
+
47
+ btn.addEventListener('click', () => {
48
+ const next = document.documentElement.classList.contains('theme-dark')
49
+ ? 'theme-light'
50
+ : 'theme-dark';
51
+ applyTheme(next);
52
+ });
53
+ }
54
+
55
+ initToolbar();
56
+ document.addEventListener('astro:after-swap', initToolbar);
57
+ </script>
58
+
59
+ <style>
60
+ .preview-toolbar {
61
+ position: fixed;
62
+ top: 1rem;
63
+ right: 1.5rem;
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 0.5rem;
67
+ z-index: 1000;
68
+ background: var(--bg-surface);
69
+ padding: 0.4rem;
70
+ border-radius: 1rem;
71
+ border: 1px solid var(--border-color);
72
+ backdrop-filter: blur(12px);
73
+ }
74
+
75
+ .locale-group {
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 0.25rem;
79
+ padding-right: 0.5rem;
80
+ border-right: 1px solid var(--border-color);
81
+ }
82
+
83
+ .locale-current {
84
+ font-size: 0.7rem;
85
+ font-weight: 900;
86
+ color: var(--text-muted);
87
+ padding: 0.4rem 0.6rem;
88
+ letter-spacing: 0.08em;
89
+ }
90
+
91
+ .btn-locale {
92
+ background: transparent;
93
+ border: 1px solid var(--border-color);
94
+ color: var(--text-main);
95
+ cursor: pointer;
96
+ padding: 0.4rem 0.7rem;
97
+ border-radius: 0.5rem;
98
+ font-weight: 700;
99
+ font-size: 0.7rem;
100
+ text-decoration: none;
101
+ letter-spacing: 0.08em;
102
+ transition: all 0.2s ease;
103
+ }
104
+
105
+ .btn-locale:hover {
106
+ border-color: var(--accent);
107
+ color: var(--accent);
108
+ }
109
+
110
+ .btn-theme {
111
+ background: transparent;
112
+ border: none;
113
+ cursor: pointer;
114
+ padding: 0.5rem;
115
+ border-radius: 0.6rem;
116
+ display: flex;
117
+ align-items: center;
118
+ justify-content: center;
119
+ transition: background 0.2s ease;
120
+ }
121
+
122
+ .btn-theme:hover {
123
+ background: rgba(255, 255, 255, 0.08);
124
+ }
125
+
126
+ .theme-icon {
127
+ display: block;
128
+ width: 16px;
129
+ height: 16px;
130
+ border-radius: 50%;
131
+ }
132
+
133
+ :global(.theme-light) .theme-icon {
134
+ background: #f59e0b;
135
+ box-shadow: 0 0 8px #fbbf24;
136
+ }
137
+
138
+ :global(.theme-dark) .theme-icon {
139
+ background: #94a3b8;
140
+ box-shadow: inset -4px -2px 0 #1e293b;
141
+ }
142
+ </style>
143
+
package/src/data.ts ADDED
@@ -0,0 +1,11 @@
1
+ export { chronoCategory } from './category';
2
+
3
+ export type {
4
+ KnownLocale,
5
+ ToolLocaleContent,
6
+ CategoryLocaleContent,
7
+ LocaleMap,
8
+ ChronoToolEntry,
9
+ ChronoCategoryEntry,
10
+ } from './types';
11
+
package/src/entries.ts ADDED
@@ -0,0 +1,32 @@
1
+ export { watchAccuracyTracker } from './tool/watch-accuracy-tracker/entry';
2
+ export type { WatchAccuracyTrackerUI, WatchAccuracyTrackerLocaleContent } from './tool/watch-accuracy-tracker/entry';
3
+ export { wristPresenceCalculator } from './tool/wrist-presence-calculator/entry';
4
+ export type { WristPresenceCalculatorUI, WristPresenceCalculatorLocaleContent } from './tool/wrist-presence-calculator/entry';
5
+ export { demagnetizingTimer } from './tool/demagnetizing-timer/entry';
6
+ export type { DemagnetizingTimerUI, DemagnetizingTimerLocaleContent } from './tool/demagnetizing-timer/entry';
7
+ export { watchSavingsPlanner } from './tool/watch-savings-planner/entry';
8
+ export type { WatchSavingsPlannerUI, WatchSavingsPlannerLocaleContent } from './tool/watch-savings-planner/entry';
9
+ export { crownReferenceGuide } from './tool/crown-reference-guide/entry';
10
+ export type { CrownReferenceGuideUI, CrownReferenceGuideLocaleContent } from './tool/crown-reference-guide/entry';
11
+ export { powerReserveEstimator } from './tool/power-reserve-estimator/entry';
12
+ export type { PowerReserveEstimatorUI, PowerReserveEstimatorLocaleContent } from './tool/power-reserve-estimator/entry';
13
+ export { beatRateConverter } from './tool/beat-rate-converter/entry';
14
+ export type { BeatRateConverterUI, BeatRateConverterLocaleContent } from './tool/beat-rate-converter/entry';
15
+ export { waterResistanceConverter } from './tool/water-resistance-converter/entry';
16
+ export type { WaterResistanceConverterUI, WaterResistanceConverterLocaleContent } from './tool/water-resistance-converter/entry';
17
+ export { strapTaperCalculator } from './tool/strap-taper-calculator/entry';
18
+ export type { StrapTaperCalculatorUI, StrapTaperCalculatorLocaleContent } from './tool/strap-taper-calculator/entry';
19
+ export { chronoCategory } from './category';
20
+
21
+ import { watchAccuracyTracker } from './tool/watch-accuracy-tracker/entry';
22
+ import { wristPresenceCalculator } from './tool/wrist-presence-calculator/entry';
23
+ import { demagnetizingTimer } from './tool/demagnetizing-timer/entry';
24
+ import { watchSavingsPlanner } from './tool/watch-savings-planner/entry';
25
+ import { crownReferenceGuide } from './tool/crown-reference-guide/entry';
26
+ import { powerReserveEstimator } from './tool/power-reserve-estimator/entry';
27
+ import { beatRateConverter } from './tool/beat-rate-converter/entry';
28
+ import { waterResistanceConverter } from './tool/water-resistance-converter/entry';
29
+ import { strapTaperCalculator } from './tool/strap-taper-calculator/entry';
30
+
31
+ export const ALL_ENTRIES = [watchAccuracyTracker, wristPresenceCalculator, demagnetizingTimer, watchSavingsPlanner, crownReferenceGuide, powerReserveEstimator, beatRateConverter, waterResistanceConverter, strapTaperCalculator];
32
+
package/src/env.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ declare module '*.astro' {
2
+ const Component: (_props: Record<string, unknown>) => unknown;
3
+ export default Component;
4
+ }
5
+
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ export { chronoCategory } from './category';
2
+ export const chronoCategorySEO = () => import('./category/ChronoCategorySEO.astro').then((m) => m.default);
3
+
4
+ export { watchAccuracyTracker, WATCH_ACCURACY_TRACKER_TOOL } from './tool/watch-accuracy-tracker';
5
+ export { demagnetizingTimer, DEMAGNETIZING_TIMER_TOOL } from './tool/demagnetizing-timer';
6
+ export { watchSavingsPlanner, WATCH_SAVINGS_PLANNER_TOOL } from './tool/watch-savings-planner';
7
+ export { crownReferenceGuide, CROWN_REFERENCE_GUIDE_TOOL } from './tool/crown-reference-guide';
8
+ export { powerReserveEstimator, POWER_RESERVE_ESTIMATOR_TOOL } from './tool/power-reserve-estimator';
9
+ export { beatRateConverter, BEAT_RATE_CONVERTER_TOOL } from './tool/beat-rate-converter';
10
+ export { waterResistanceConverter, WATER_RESISTANCE_CONVERTER_TOOL } from './tool/water-resistance-converter';
11
+ export { strapTaperCalculator, STRAP_TAPER_CALCULATOR_TOOL } from './tool/strap-taper-calculator';
12
+
13
+ export type {
14
+ KnownLocale,
15
+ FAQItem,
16
+ BibliographyEntry,
17
+ HowToStep,
18
+ ToolLocaleContent,
19
+ CategoryLocaleContent,
20
+ LocaleLoader,
21
+ LocaleMap,
22
+ ChronoToolEntry,
23
+ ChronoCategoryEntry,
24
+ ToolDefinition,
25
+ } from './types';
26
+
27
+ export { ALL_ENTRIES, ALL_TOOLS } from './tools';
28
+
@@ -0,0 +1,117 @@
1
+ ---
2
+ import "@jjlmoya/utils-shared/theme.css";
3
+ import PreviewToolbar from "../components/PreviewToolbar.astro";
4
+ import type { KnownLocale } from "../types";
5
+
6
+ interface Props {
7
+ title: string;
8
+ currentLocale?: KnownLocale;
9
+ localeUrls?: Partial<Record<KnownLocale, string>>;
10
+ hasSidebar?: boolean;
11
+ }
12
+
13
+ const { title, currentLocale = "es", localeUrls = {}, hasSidebar = false } = Astro.props;
14
+ ---
15
+
16
+ <!doctype html>
17
+ <html lang={currentLocale}>
18
+ <head>
19
+ <meta charset="UTF-8" />
20
+ <meta name="viewport" content="width=device-width" />
21
+ <title>{title} · preview</title>
22
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
23
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
24
+ <link
25
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap"
26
+ rel="stylesheet"
27
+ />
28
+
29
+ <script is:inline>
30
+ (function () {
31
+ const saved = localStorage.getItem("theme") || "theme-dark";
32
+ document.documentElement.classList.add(saved);
33
+ })();
34
+ </script>
35
+ <slot name="head" />
36
+ </head>
37
+ <body>
38
+ <PreviewToolbar currentLocale={currentLocale} localeUrls={localeUrls} />
39
+ <div class:list={["page-wrapper", { "with-sidebar": hasSidebar }]}>
40
+ {
41
+ hasSidebar && (
42
+ <aside class="sidebar-area">
43
+ <slot name="sidebar" />
44
+ </aside>
45
+ )
46
+ }
47
+ <main>
48
+ <slot />
49
+ </main>
50
+ </div>
51
+ </body>
52
+ </html>
53
+
54
+ <style is:global>
55
+ :root {
56
+ --accent: #f43f5e;
57
+ --primary-base: #9f1239;
58
+ --cyan: #06b6d4;
59
+ }
60
+
61
+ .theme-dark,
62
+ .theme-light {
63
+ --text-main: var(--text-base);
64
+ --border-color: var(--border-base);
65
+ }
66
+
67
+ *,
68
+ *::before,
69
+ *::after {
70
+ box-sizing: border-box;
71
+ }
72
+
73
+ body {
74
+ background-color: var(--bg-page);
75
+ color: var(--text-base);
76
+ margin: 0;
77
+ min-height: 100vh;
78
+ transition:
79
+ background-color 0.3s ease,
80
+ color 0.3s ease;
81
+ }
82
+
83
+ main {
84
+ padding: 0 2rem;
85
+ }
86
+
87
+ .page-wrapper {
88
+ display: flex;
89
+ flex-direction: column;
90
+ }
91
+
92
+ .page-wrapper.with-sidebar {
93
+ display: grid;
94
+ grid-template-columns: 240px 1fr;
95
+ min-height: 100vh;
96
+ }
97
+
98
+ .sidebar-area {
99
+ position: sticky;
100
+ top: 0;
101
+ height: 100vh;
102
+ overflow-y: auto;
103
+ border-right: 1px solid var(--border-color);
104
+ background: var(--bg-page);
105
+ }
106
+
107
+ @media (max-width: 768px) {
108
+ .page-wrapper.with-sidebar {
109
+ grid-template-columns: 1fr;
110
+ }
111
+
112
+ .sidebar-area {
113
+ display: none;
114
+ }
115
+ }
116
+ </style>
117
+
@@ -0,0 +1,161 @@
1
+ ---
2
+ import PreviewLayout from "../../layouts/PreviewLayout.astro";
3
+ import PreviewNavSidebar from "../../components/PreviewNavSidebar.astro";
4
+ import { ALL_TOOLS } from "../../index";
5
+ import {
6
+ UtilityHeader,
7
+ FAQSection,
8
+ Bibliography,
9
+ SEORenderer,
10
+ } from "@jjlmoya/utils-shared";
11
+ import type { KnownLocale, ToolLocaleContent } from "../../types";
12
+ import type { UtilitySEOContent } from "@jjlmoya/utils-shared";
13
+
14
+ export async function getStaticPaths() {
15
+ const paths = [];
16
+
17
+ for (const { entry, Component: lazyComp } of ALL_TOOLS) {
18
+ const { default: Component } = await lazyComp();
19
+ const localeEntries = Object.entries(entry.i18n) as [
20
+ KnownLocale,
21
+ () => Promise<ToolLocaleContent>,
22
+ ][];
23
+ const localeContents = await Promise.all(
24
+ localeEntries.map(async ([locale, loader]) => ({
25
+ locale,
26
+ content: await loader(),
27
+ })),
28
+ );
29
+
30
+ const localeUrls = Object.fromEntries(
31
+ localeContents.map(({ locale, content }) => [
32
+ locale,
33
+ `/${locale}/${content.slug}`,
34
+ ]),
35
+ ) as Partial<Record<KnownLocale, string>>;
36
+
37
+ const firstLoader = entry.i18n.en ?? Object.values(entry.i18n)[0];
38
+ const englishSlug = firstLoader ? (await firstLoader()).slug : entry.id;
39
+
40
+ for (const { locale, content } of localeContents) {
41
+ const allToolsNav = (
42
+ await Promise.all(
43
+ ALL_TOOLS.map(async ({ entry: navEntry }) => {
44
+ const loader = navEntry.i18n[locale] ?? navEntry.i18n.en;
45
+ if (!loader) return null;
46
+ const navContent = await loader();
47
+ return {
48
+ id: navEntry.id,
49
+ title: navContent.title,
50
+ href: `/${locale}/${navContent.slug}`,
51
+ isActive: navEntry.id === entry.id,
52
+ };
53
+ }),
54
+ )
55
+ ).filter(Boolean) as NavItem[];
56
+ paths.push({
57
+ params: { locale, slug: content.slug },
58
+ props: { Component, locale, content, localeUrls, allToolsNav, englishSlug },
59
+ });
60
+ }
61
+ }
62
+
63
+ return paths;
64
+ }
65
+
66
+ interface NavItem {
67
+ id: string;
68
+ title: string;
69
+ href: string;
70
+ isActive?: boolean;
71
+ }
72
+
73
+ interface Props {
74
+ Component: unknown;
75
+ locale: KnownLocale;
76
+ content: ToolLocaleContent;
77
+ localeUrls: Partial<Record<KnownLocale, string>>;
78
+ allToolsNav: NavItem[];
79
+ englishSlug: string;
80
+ }
81
+
82
+ const { Component, locale, content, localeUrls, allToolsNav, englishSlug } = Astro.props;
83
+
84
+ const cssFiles = import.meta.glob("../../tool/*/*.css", { query: "?raw", import: "default" });
85
+ const cssKey = Object.keys(cssFiles).find((k) => k.endsWith(`/${englishSlug}.css`));
86
+ const toolCss = cssKey ? await cssFiles[cssKey]() as string : "";
87
+
88
+ const seoContent: UtilitySEOContent = { locale, sections: content.seo ?? [] };
89
+
90
+ const words = content.title.split(" ");
91
+ const titleHighlight = words[0] || "";
92
+ const titleBase = words.slice(1).join(" ") || "";
93
+ ---
94
+
95
+ <PreviewLayout
96
+ title={content.title}
97
+ currentLocale={locale}
98
+ localeUrls={localeUrls}
99
+ hasSidebar={true}
100
+ >
101
+ <PreviewNavSidebar
102
+ slot="sidebar"
103
+ categoryTitle="Tools"
104
+ tools={allToolsNav}
105
+ />
106
+ <Fragment slot="head">
107
+ {toolCss ? <Fragment set:html={`<style is:inline>${toolCss}</style>`} /> : null}
108
+ {
109
+ ( content.schemas ?? []).map((schema: unknown) => (
110
+ <script
111
+ is:inline
112
+ type="application/ld+json"
113
+ set:html={JSON.stringify(schema)}
114
+ />
115
+ ))
116
+ }
117
+ </Fragment>
118
+
119
+ <div class="tool-page">
120
+ <UtilityHeader
121
+ titleHighlight={titleHighlight}
122
+ titleBase={titleBase}
123
+ description={content.description}
124
+ />
125
+
126
+ <section class="section-tool">
127
+ <Component ui={content.ui} />
128
+ </section>
129
+
130
+ <section class="section-seo">
131
+ <SEORenderer content={seoContent} />
132
+ </section>
133
+
134
+ <section class="section-faq">
135
+ <FAQSection items={content.faq} />
136
+ </section>
137
+
138
+ <section class="section-bibliography">
139
+ <Bibliography links={content.bibliography} />
140
+ </section>
141
+ </div>
142
+ </PreviewLayout>
143
+
144
+ <style>
145
+ .tool-page {
146
+ display: flex;
147
+ flex-direction: column;
148
+ gap: 2rem;
149
+ }
150
+ .section-tool {
151
+ max-width: 1200px;
152
+ margin: 0 auto;
153
+ width: 100%;
154
+ }
155
+ .section-seo,
156
+ .section-faq,
157
+ .section-bibliography {
158
+ padding-top: 2rem;
159
+ border-top: 1px solid var(--border-color);
160
+ }
161
+ </style>