@jjlmoya/utils-chrono 1.10.0 → 1.16.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 (117) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +8 -0
  3. package/src/entries.ts +13 -1
  4. package/src/tests/locale_completeness.test.ts +1 -1
  5. package/src/tests/tool_validation.test.ts +1 -1
  6. package/src/tool/gear-train-explorer/bibliography.astro +16 -0
  7. package/src/tool/gear-train-explorer/bibliography.ts +12 -0
  8. package/src/tool/gear-train-explorer/client.ts +146 -0
  9. package/src/tool/gear-train-explorer/component.astro +17 -0
  10. package/src/tool/gear-train-explorer/components/GearPanel.astro +102 -0
  11. package/src/tool/gear-train-explorer/entry.ts +53 -0
  12. package/src/tool/gear-train-explorer/gear-train-explorer.css +172 -0
  13. package/src/tool/gear-train-explorer/gears.ts +148 -0
  14. package/src/tool/gear-train-explorer/helpers.ts +49 -0
  15. package/src/tool/gear-train-explorer/i18n/de.ts +99 -0
  16. package/src/tool/gear-train-explorer/i18n/en.ts +98 -0
  17. package/src/tool/gear-train-explorer/i18n/es.ts +99 -0
  18. package/src/tool/gear-train-explorer/i18n/fr.ts +99 -0
  19. package/src/tool/gear-train-explorer/i18n/id.ts +98 -0
  20. package/src/tool/gear-train-explorer/i18n/it.ts +99 -0
  21. package/src/tool/gear-train-explorer/i18n/ja.ts +98 -0
  22. package/src/tool/gear-train-explorer/i18n/ko.ts +98 -0
  23. package/src/tool/gear-train-explorer/i18n/nl.ts +99 -0
  24. package/src/tool/gear-train-explorer/i18n/pl.ts +99 -0
  25. package/src/tool/gear-train-explorer/i18n/pt.ts +99 -0
  26. package/src/tool/gear-train-explorer/i18n/ru.ts +99 -0
  27. package/src/tool/gear-train-explorer/i18n/sv.ts +99 -0
  28. package/src/tool/gear-train-explorer/i18n/tr.ts +98 -0
  29. package/src/tool/gear-train-explorer/i18n/zh.ts +98 -0
  30. package/src/tool/gear-train-explorer/index.ts +11 -0
  31. package/src/tool/gear-train-explorer/movements.ts +61 -0
  32. package/src/tool/gear-train-explorer/scene.ts +120 -0
  33. package/src/tool/gear-train-explorer/seo.astro +16 -0
  34. package/src/tool/gear-train-explorer/state.ts +30 -0
  35. package/src/tool/gmt-world-timer/bibliography.astro +11 -0
  36. package/src/tool/gmt-world-timer/bibliography.ts +7 -0
  37. package/src/tool/gmt-world-timer/client.ts +250 -0
  38. package/src/tool/gmt-world-timer/component.astro +13 -0
  39. package/src/tool/gmt-world-timer/components/GmtPanel.astro +18 -0
  40. package/src/tool/gmt-world-timer/entry.ts +34 -0
  41. package/src/tool/gmt-world-timer/gmt-world-timer.css +239 -0
  42. package/src/tool/gmt-world-timer/helpers.ts +28 -0
  43. package/src/tool/gmt-world-timer/i18n/de.ts +72 -0
  44. package/src/tool/gmt-world-timer/i18n/en.ts +72 -0
  45. package/src/tool/gmt-world-timer/i18n/es.ts +72 -0
  46. package/src/tool/gmt-world-timer/i18n/fr.ts +72 -0
  47. package/src/tool/gmt-world-timer/i18n/id.ts +72 -0
  48. package/src/tool/gmt-world-timer/i18n/it.ts +72 -0
  49. package/src/tool/gmt-world-timer/i18n/ja.ts +72 -0
  50. package/src/tool/gmt-world-timer/i18n/ko.ts +72 -0
  51. package/src/tool/gmt-world-timer/i18n/nl.ts +72 -0
  52. package/src/tool/gmt-world-timer/i18n/pl.ts +72 -0
  53. package/src/tool/gmt-world-timer/i18n/pt.ts +72 -0
  54. package/src/tool/gmt-world-timer/i18n/ru.ts +72 -0
  55. package/src/tool/gmt-world-timer/i18n/sv.ts +72 -0
  56. package/src/tool/gmt-world-timer/i18n/tr.ts +72 -0
  57. package/src/tool/gmt-world-timer/i18n/zh.ts +72 -0
  58. package/src/tool/gmt-world-timer/index.ts +11 -0
  59. package/src/tool/gmt-world-timer/seo.astro +11 -0
  60. package/src/tool/perpetual-calendar/bibliography.astro +16 -0
  61. package/src/tool/perpetual-calendar/bibliography.ts +16 -0
  62. package/src/tool/perpetual-calendar/calendar.ts +24 -0
  63. package/src/tool/perpetual-calendar/client.ts +98 -0
  64. package/src/tool/perpetual-calendar/component.astro +17 -0
  65. package/src/tool/perpetual-calendar/components/CalendarPanel.astro +49 -0
  66. package/src/tool/perpetual-calendar/dial.ts +176 -0
  67. package/src/tool/perpetual-calendar/entry.ts +48 -0
  68. package/src/tool/perpetual-calendar/helpers.ts +49 -0
  69. package/src/tool/perpetual-calendar/i18n/de.ts +85 -0
  70. package/src/tool/perpetual-calendar/i18n/en.ts +102 -0
  71. package/src/tool/perpetual-calendar/i18n/es.ts +85 -0
  72. package/src/tool/perpetual-calendar/i18n/fr.ts +85 -0
  73. package/src/tool/perpetual-calendar/i18n/id.ts +85 -0
  74. package/src/tool/perpetual-calendar/i18n/it.ts +85 -0
  75. package/src/tool/perpetual-calendar/i18n/ja.ts +85 -0
  76. package/src/tool/perpetual-calendar/i18n/ko.ts +85 -0
  77. package/src/tool/perpetual-calendar/i18n/nl.ts +85 -0
  78. package/src/tool/perpetual-calendar/i18n/pl.ts +85 -0
  79. package/src/tool/perpetual-calendar/i18n/pt.ts +85 -0
  80. package/src/tool/perpetual-calendar/i18n/ru.ts +85 -0
  81. package/src/tool/perpetual-calendar/i18n/sv.ts +85 -0
  82. package/src/tool/perpetual-calendar/i18n/tr.ts +85 -0
  83. package/src/tool/perpetual-calendar/i18n/zh.ts +85 -0
  84. package/src/tool/perpetual-calendar/index.ts +11 -0
  85. package/src/tool/perpetual-calendar/perpetual-calendar.css +181 -0
  86. package/src/tool/perpetual-calendar/seo.astro +16 -0
  87. package/src/tool/perpetual-calendar/state.ts +26 -0
  88. package/src/tool/tourbillon-visualizer/bibliography.astro +11 -0
  89. package/src/tool/tourbillon-visualizer/bibliography.ts +7 -0
  90. package/src/tool/tourbillon-visualizer/client.ts +122 -0
  91. package/src/tool/tourbillon-visualizer/component.astro +126 -0
  92. package/src/tool/tourbillon-visualizer/components/TourbillonPanel.astro +66 -0
  93. package/src/tool/tourbillon-visualizer/entry.ts +51 -0
  94. package/src/tool/tourbillon-visualizer/helpers.ts +35 -0
  95. package/src/tool/tourbillon-visualizer/i18n/de.ts +96 -0
  96. package/src/tool/tourbillon-visualizer/i18n/en.ts +96 -0
  97. package/src/tool/tourbillon-visualizer/i18n/es.ts +96 -0
  98. package/src/tool/tourbillon-visualizer/i18n/fr.ts +96 -0
  99. package/src/tool/tourbillon-visualizer/i18n/id.ts +96 -0
  100. package/src/tool/tourbillon-visualizer/i18n/it.ts +96 -0
  101. package/src/tool/tourbillon-visualizer/i18n/ja.ts +96 -0
  102. package/src/tool/tourbillon-visualizer/i18n/ko.ts +96 -0
  103. package/src/tool/tourbillon-visualizer/i18n/nl.ts +96 -0
  104. package/src/tool/tourbillon-visualizer/i18n/pl.ts +96 -0
  105. package/src/tool/tourbillon-visualizer/i18n/pt.ts +96 -0
  106. package/src/tool/tourbillon-visualizer/i18n/ru.ts +96 -0
  107. package/src/tool/tourbillon-visualizer/i18n/sv.ts +96 -0
  108. package/src/tool/tourbillon-visualizer/i18n/tr.ts +96 -0
  109. package/src/tool/tourbillon-visualizer/i18n/zh.ts +96 -0
  110. package/src/tool/tourbillon-visualizer/index.ts +11 -0
  111. package/src/tool/tourbillon-visualizer/renderer/base.ts +78 -0
  112. package/src/tool/tourbillon-visualizer/renderer/cage.ts +115 -0
  113. package/src/tool/tourbillon-visualizer/renderer/esc.ts +160 -0
  114. package/src/tool/tourbillon-visualizer/seo.astro +11 -0
  115. package/src/tool/tourbillon-visualizer/state.ts +21 -0
  116. package/src/tool/tourbillon-visualizer/tourbillon.ts +9 -0
  117. package/src/tools.ts +8 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-chrono",
3
- "version": "1.10.0",
3
+ "version": "1.16.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -15,6 +15,10 @@ import { serviceIntervalTracker } from '../tool/service-interval-tracker/entry';
15
15
  import { strapLengthCalculator } from '../tool/strap-length-calculator/entry';
16
16
  import { telemeterCalculator } from '../tool/telemeter-calculator/entry';
17
17
  import { siderealTimeTracker } from '../tool/sidereal-time-tracker/entry';
18
+ import { gearTrainExplorer } from '../tool/gear-train-explorer/entry';
19
+ import { perpetualCalendar } from '../tool/perpetual-calendar/entry';
20
+ import { tourbillonVisualizer } from '../tool/tourbillon-visualizer/entry';
21
+ import { gmtWorldTimer } from '../tool/gmt-world-timer/entry';
18
22
 
19
23
  export const chronoCategory: ChronoCategoryEntry = {
20
24
  icon: 'mdi:clock-outline',
@@ -35,6 +39,10 @@ export const chronoCategory: ChronoCategoryEntry = {
35
39
  strapLengthCalculator,
36
40
  telemeterCalculator,
37
41
  siderealTimeTracker,
42
+ gearTrainExplorer,
43
+ perpetualCalendar,
44
+ tourbillonVisualizer,
45
+ gmtWorldTimer,
38
46
  ],
39
47
  i18n: {
40
48
  de: () => import('./i18n/de').then((m) => m.content),
package/src/entries.ts CHANGED
@@ -32,6 +32,14 @@ export { telemeterCalculator } from './tool/telemeter-calculator/entry';
32
32
  export type { TelemeterCalculatorUI, TelemeterCalculatorLocaleContent } from './tool/telemeter-calculator/entry';
33
33
  export { siderealTimeTracker } from './tool/sidereal-time-tracker/entry';
34
34
  export type { SiderealTimeTrackerUI, SiderealTimeTrackerLocaleContent } from './tool/sidereal-time-tracker/entry';
35
+ export { gearTrainExplorer } from './tool/gear-train-explorer/entry';
36
+ export type { GearTrainExplorerUI, GearTrainExplorerLocaleContent } from './tool/gear-train-explorer/entry';
37
+ export { perpetualCalendar } from './tool/perpetual-calendar/entry';
38
+ export type { PerpetualCalendarUI, PerpetualCalendarLocaleContent } from './tool/perpetual-calendar/entry';
39
+ export { tourbillonVisualizer } from './tool/tourbillon-visualizer/entry';
40
+ export type { TourbillonUI, TourbillonLocaleContent } from './tool/tourbillon-visualizer/entry';
41
+ export { gmtWorldTimer } from './tool/gmt-world-timer/entry';
42
+ export type { GMTWorldTimerUI, GMTWorldTimerLocaleContent } from './tool/gmt-world-timer/entry';
35
43
  export { chronoCategory } from './category';
36
44
 
37
45
  import { watchAccuracyTracker } from './tool/watch-accuracy-tracker/entry';
@@ -51,6 +59,10 @@ import { serviceIntervalTracker } from './tool/service-interval-tracker/entry';
51
59
  import { strapLengthCalculator } from './tool/strap-length-calculator/entry';
52
60
  import { telemeterCalculator } from './tool/telemeter-calculator/entry';
53
61
  import { siderealTimeTracker } from './tool/sidereal-time-tracker/entry';
62
+ import { gearTrainExplorer } from './tool/gear-train-explorer/entry';
63
+ import { perpetualCalendar } from './tool/perpetual-calendar/entry';
64
+ import { tourbillonVisualizer } from './tool/tourbillon-visualizer/entry';
65
+ import { gmtWorldTimer } from './tool/gmt-world-timer/entry';
54
66
 
55
- export const ALL_ENTRIES = [watchAccuracyTracker, wristPresenceCalculator, demagnetizingTimer, watchSavingsPlanner, crownReferenceGuide, powerReserveEstimator, beatRateConverter, waterResistanceConverter, strapTaperCalculator, watchSizeComparator, lumeColorSimulator, moonPhaseVisualizer, tachymeterCalculator, serviceIntervalTracker, strapLengthCalculator, telemeterCalculator, siderealTimeTracker];
67
+ export const ALL_ENTRIES = [watchAccuracyTracker, wristPresenceCalculator, demagnetizingTimer, watchSavingsPlanner, crownReferenceGuide, powerReserveEstimator, beatRateConverter, waterResistanceConverter, strapTaperCalculator, watchSizeComparator, lumeColorSimulator, moonPhaseVisualizer, tachymeterCalculator, serviceIntervalTracker, strapLengthCalculator, telemeterCalculator, siderealTimeTracker, gearTrainExplorer, perpetualCalendar, tourbillonVisualizer, gmtWorldTimer];
56
68
 
@@ -22,7 +22,7 @@ describe('Locale Completeness Validation', () => {
22
22
  });
23
23
 
24
24
  it('all tools registered', () => {
25
- expect(ALL_TOOLS.length).toBe(17);
25
+ expect(ALL_TOOLS.length).toBe(21);
26
26
  });
27
27
 
28
28
  });
@@ -5,7 +5,7 @@ import { chronoCategory } from '../data';
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
7
  it('should have tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(17);
8
+ expect(ALL_TOOLS.length).toBe(21);
9
9
  });
10
10
 
11
11
 
@@ -0,0 +1,16 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { gearTrainExplorer } 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 = gearTrainExplorer.i18n[locale] || gearTrainExplorer.i18n.en;
12
+ const content = await loader?.();
13
+ if (!content) return null;
14
+ ---
15
+
16
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,12 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Watch Gear Train Explained - Horology 101',
6
+ url: 'https://en.wikipedia.org/wiki/Wheel_train_(horology)',
7
+ },
8
+ {
9
+ name: 'ETA 2824-2 Technical Documentation',
10
+ url: 'https://shopb2b.eta.ch/en/mecaline/2824-2-2824-2-5.html',
11
+ }
12
+ ];
@@ -0,0 +1,146 @@
1
+ const canvas = document.getElementById('gear-canvas') as HTMLCanvasElement;
2
+ const ctx = canvas.getContext('2d')!;
3
+
4
+ import { MOVEMENTS } from './movements';
5
+ import type { MovementDef } from './movements';
6
+ import { setCtx, setMov as setMovement, setHovered } from './state';
7
+ import { drawScene } from './scene';
8
+
9
+ setCtx(ctx);
10
+
11
+ const REF_W = 900;
12
+ const REF_H = 520;
13
+
14
+ let currentMov: MovementDef = MOVEMENTS['2824'];
15
+ let speedMult = 1;
16
+ let paused = false;
17
+ let angles: number[] = currentMov.gears.map(() => 0);
18
+ let palletPhase = 0;
19
+ let balancePhase = 0;
20
+ let highlightedGear: number | null = null;
21
+ let hoveredGear: number | null = null;
22
+
23
+ function resizeCanvas() {
24
+ const parent = canvas.parentElement!;
25
+ const displayW = parent.clientWidth;
26
+ const displayH = displayW * (REF_H / REF_W);
27
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
28
+ canvas.style.width = displayW + 'px';
29
+ canvas.style.height = displayH + 'px';
30
+ canvas.width = displayW * dpr;
31
+ canvas.height = displayH * dpr;
32
+ ctx.setTransform(displayW / REF_W * dpr, 0, 0, displayH / REF_H * dpr, 0, 0);
33
+ }
34
+
35
+ function updateStats() {
36
+ const g = currentMov.gears;
37
+ const setText = (id: string, v: string) => { const el = document.getElementById('rpm-' + id); if (el) el.textContent = v; };
38
+ setText('barrel', (g[0].rpm * 60).toFixed(2) + '/h');
39
+ setText('center', (g[1].rpm * 60).toFixed(1) + '/h');
40
+ setText('third', (g[2].rpm * 60).toFixed(1) + '/h');
41
+ setText('fourth', g[3].rpm.toFixed(1) + ' rpm');
42
+ setText('escape', g[4].rpm.toFixed(0) + ' rpm');
43
+ const bphEl = document.getElementById('bph-pallet');
44
+ if (bphEl) bphEl.textContent = currentMov.pallet.bph.toString();
45
+ const hzEl = document.getElementById('hz-balance');
46
+ if (hzEl) hzEl.textContent = currentMov.balance.hz.toString();
47
+ const vphEl = document.getElementById('vph-balance');
48
+ if (vphEl) vphEl.textContent = currentMov.balance.vph.toString();
49
+ }
50
+
51
+ function render() {
52
+ setMovement(currentMov);
53
+ setHovered(hoveredGear);
54
+ drawScene(currentMov, { angles, palletPhase, balancePhase, highlight: highlightedGear, hover: hoveredGear });
55
+ const step = highlightedGear !== null ? highlightedGear : -1;
56
+ document.querySelectorAll('.flow-bar .step').forEach((el, idx) => {
57
+ el.classList.toggle('active', idx <= step);
58
+ });
59
+ }
60
+
61
+ function tick() {
62
+ if (paused) return;
63
+ const dt = 1;
64
+ const gears = currentMov.gears;
65
+ for (let i = 0; i < gears.length; i++) {
66
+ angles[i] += gears[i].visualSpeed * speedMult * dt * 0.08;
67
+ }
68
+ const bf = currentMov.pallet.bph / 3600;
69
+ palletPhase += bf * speedMult * dt * 0.06;
70
+ balancePhase += currentMov.balance.hz * speedMult * dt * 0.2;
71
+ }
72
+
73
+ function animate() {
74
+ tick();
75
+ render();
76
+ requestAnimationFrame(animate);
77
+ }
78
+
79
+ function switchMovement(id: string) {
80
+ currentMov = MOVEMENTS[id];
81
+ angles = currentMov.gears.map(() => 0);
82
+ palletPhase = 0;
83
+ balancePhase = 0;
84
+ highlightedGear = null;
85
+ updateStats();
86
+ document.querySelectorAll('[data-mov]').forEach((b) => {
87
+ b.classList.toggle('active', (b as HTMLElement).dataset.mov === id);
88
+ });
89
+ }
90
+
91
+ function setSpeed(mult: number) {
92
+ speedMult = mult;
93
+ paused = mult === 0;
94
+ document.querySelectorAll('[data-spd]').forEach((b) => {
95
+ const spd = parseFloat((b as HTMLElement).dataset.spd || '1');
96
+ b.classList.toggle('active', spd === mult);
97
+ });
98
+ }
99
+
100
+ function hitTest(mx: number, my: number): number | null {
101
+ for (let i = 0; i < currentMov.gears.length; i++) {
102
+ if (Math.sqrt((mx - currentMov.gears[i].x) ** 2 + (my - currentMov.gears[i].y) ** 2) < currentMov.gears[i].r + 10) return i;
103
+ }
104
+ if (Math.sqrt((mx - currentMov.pallet.x) ** 2 + (my - currentMov.pallet.y) ** 2) < 20) return 5;
105
+ if (Math.sqrt((mx - currentMov.balance.x) ** 2 + (my - currentMov.balance.y) ** 2) < currentMov.balance.r + 8) return 6;
106
+ return null;
107
+ }
108
+
109
+ function onCanvasMove(e: MouseEvent) {
110
+ const rect = canvas.getBoundingClientRect();
111
+ const mx = (e.clientX - rect.left) / rect.width * REF_W;
112
+ const my = (e.clientY - rect.top) / rect.height * REF_H;
113
+ const found = hitTest(mx, my);
114
+ if (found !== hoveredGear) highlightGear(found);
115
+ canvas.style.cursor = found !== null ? 'pointer' : 'default';
116
+ }
117
+
118
+ function highlightGear(idx: number | null) {
119
+ hoveredGear = idx;
120
+ highlightedGear = idx;
121
+ document.querySelectorAll('.data-card').forEach((c) => c.classList.remove('highlighted'));
122
+ if (idx !== null && idx < 7) {
123
+ document.querySelectorAll('.data-card')[idx]?.classList.add('highlighted');
124
+ }
125
+ }
126
+
127
+ function initControls() {
128
+ document.querySelectorAll('[data-mov]').forEach((b) => {
129
+ b.addEventListener('click', () => switchMovement((b as HTMLElement).dataset.mov || '2824'));
130
+ });
131
+ document.querySelectorAll('[data-spd]').forEach((b) => {
132
+ b.addEventListener('click', () => setSpeed(parseFloat((b as HTMLElement).dataset.spd || '1')));
133
+ });
134
+ document.querySelectorAll('.data-card').forEach((card, idx) => {
135
+ card.addEventListener('mouseenter', () => highlightGear(idx));
136
+ card.addEventListener('mouseleave', () => highlightGear(null));
137
+ });
138
+ canvas.addEventListener('mousemove', onCanvasMove);
139
+ canvas.addEventListener('mouseleave', () => highlightGear(null));
140
+ }
141
+
142
+ new ResizeObserver(resizeCanvas).observe(canvas.parentElement!);
143
+ resizeCanvas();
144
+ updateStats();
145
+ initControls();
146
+ animate();
@@ -0,0 +1,17 @@
1
+ ---
2
+ import GearPanel from './components/GearPanel.astro';
3
+
4
+ interface Props {
5
+ ui: Record<string, string>;
6
+ }
7
+
8
+ const { ui } = Astro.props;
9
+ ---
10
+
11
+ <link href="./gear-train-explorer.css" rel="stylesheet" />
12
+
13
+ <div class="tool-main-card" data-ui={JSON.stringify(ui)}>
14
+ <GearPanel labels={ui} />
15
+ </div>
16
+
17
+ <script src="./client.ts"></script>
@@ -0,0 +1,102 @@
1
+ ---
2
+ interface Props {
3
+ labels: Record<string, string>;
4
+ }
5
+
6
+ const { labels } = Astro.props;
7
+ ---
8
+
9
+ <div class="canvas-wrapper">
10
+ <canvas id="gear-canvas" width="900" height="520"></canvas>
11
+ </div>
12
+
13
+ <div class="controls-row">
14
+ <div class="control-group">
15
+ <span class="control-label">{labels.movementLabel || 'Movement'}</span>
16
+ <div class="btn-group">
17
+ <button class="active" data-mov="2824">ETA 2824</button>
18
+ <button data-mov="elprimero">El Primero</button>
19
+ <button data-mov="vintage">Vintage 18k</button>
20
+ </div>
21
+ </div>
22
+ <div class="control-group">
23
+ <span class="control-label">{labels.speedLabel || 'Speed'}</span>
24
+ <div class="btn-group">
25
+ <button data-spd="0.5">0.5x</button>
26
+ <button class="active" data-spd="1">1x</button>
27
+ <button data-spd="2">2x</button>
28
+ <button data-spd="5">5x</button>
29
+ <button data-spd="0">P</button>
30
+ </div>
31
+ </div>
32
+ </div>
33
+
34
+ <div class="data-grid" id="gear-data-grid">
35
+ <div class="data-card" data-gear="barrel">
36
+ <div class="name">{labels.barrelLabel || 'Barrel'}</div>
37
+ <div class="stats">
38
+ <span><span class="val" id="rpm-barrel">0</span> rpm</span>
39
+ <span>72t</span>
40
+ </div>
41
+ </div>
42
+ <div class="data-card" data-gear="center">
43
+ <div class="name">{labels.centerWheelLabel || 'Center'}</div>
44
+ <div class="stats">
45
+ <span><span class="val" id="rpm-center">0</span> rpm</span>
46
+ <span>60t</span>
47
+ </div>
48
+ </div>
49
+ <div class="data-card" data-gear="third">
50
+ <div class="name">{labels.thirdWheelLabel || 'Third'}</div>
51
+ <div class="stats">
52
+ <span><span class="val" id="rpm-third">0</span> rpm</span>
53
+ <span>50t</span>
54
+ </div>
55
+ </div>
56
+ <div class="data-card" data-gear="fourth">
57
+ <div class="name">{labels.fourthWheelLabel || 'Fourth'}</div>
58
+ <div class="stats">
59
+ <span><span class="val" id="rpm-fourth">0</span> rpm</span>
60
+ <span>60t</span>
61
+ </div>
62
+ </div>
63
+ <div class="data-card" data-gear="escape">
64
+ <div class="name">{labels.escapeWheelLabel || 'Escape'}</div>
65
+ <div class="stats">
66
+ <span><span class="val" id="rpm-escape">0</span> rpm</span>
67
+ <span id="escape-teeth">15t</span>
68
+ </div>
69
+ </div>
70
+ <div class="data-card" data-gear="pallet">
71
+ <div class="name">{labels.palletForkLabel || 'Pallet'}</div>
72
+ <div class="stats">
73
+ <span><span class="val" id="bph-pallet">28800</span> bph</span>
74
+ </div>
75
+ </div>
76
+ <div class="data-card" data-gear="balance">
77
+ <div class="name">{labels.balanceWheelLabel || 'Balance'}</div>
78
+ <div class="stats">
79
+ <span><span class="val" id="hz-balance">4</span> Hz</span>
80
+ <span><span class="val" id="vph-balance">28800</span> vph</span>
81
+ </div>
82
+ </div>
83
+ </div>
84
+
85
+ <div class="flow-bar">
86
+ <div class="flow-label">{labels.powerFlowLabel || 'Power Flow'}</div>
87
+ <div class="flow-path">
88
+ <span class="step active" data-step="0">Barrel</span>
89
+ <span class="arrow">&rarr;</span>
90
+ <span class="step" data-step="1">Center</span>
91
+ <span class="arrow">&rarr;</span>
92
+ <span class="step" data-step="2">Third</span>
93
+ <span class="arrow">&rarr;</span>
94
+ <span class="step" data-step="3">Fourth</span>
95
+ <span class="arrow">&rarr;</span>
96
+ <span class="step" data-step="4">Escape</span>
97
+ <span class="arrow">&rarr;</span>
98
+ <span class="step" data-step="5">Pallet</span>
99
+ <span class="arrow">&rarr;</span>
100
+ <span class="step" data-step="6">Balance</span>
101
+ </div>
102
+ </div>
@@ -0,0 +1,53 @@
1
+ import type { ChronoToolEntry, ToolLocaleContent } from '../../types';
2
+
3
+ export type GearTrainExplorerUI = {
4
+ title: string;
5
+ barrelLabel: string;
6
+ centerWheelLabel: string;
7
+ thirdWheelLabel: string;
8
+ fourthWheelLabel: string;
9
+ escapeWheelLabel: string;
10
+ palletForkLabel: string;
11
+ balanceWheelLabel: string;
12
+ rpmLabel: string;
13
+ teethLabel: string;
14
+ gearRatioLabel: string;
15
+ powerFlowLabel: string;
16
+ movementLabel: string;
17
+ speedLabel: string;
18
+ speedNormal: string;
19
+ speedSlow: string;
20
+ speedPaused: string;
21
+ mov2824: string;
22
+ movElPrimero: string;
23
+ movVintage: string;
24
+ step1: string;
25
+ step2: string;
26
+ step3: string;
27
+ tipTitle: string;
28
+ tipContent: string;
29
+ };
30
+
31
+ export type GearTrainExplorerLocaleContent = ToolLocaleContent<GearTrainExplorerUI>;
32
+
33
+ export const gearTrainExplorer: ChronoToolEntry<GearTrainExplorerUI> = {
34
+ id: 'gear-train-explorer',
35
+ icons: { bg: 'mdi:cog-transfer', fg: 'mdi:cog-clockwise' },
36
+ i18n: {
37
+ de: () => import('./i18n/de').then((m) => m.content),
38
+ en: () => import('./i18n/en').then((m) => m.content),
39
+ es: () => import('./i18n/es').then((m) => m.content),
40
+ fr: () => import('./i18n/fr').then((m) => m.content),
41
+ id: () => import('./i18n/id').then((m) => m.content),
42
+ it: () => import('./i18n/it').then((m) => m.content),
43
+ ja: () => import('./i18n/ja').then((m) => m.content),
44
+ ko: () => import('./i18n/ko').then((m) => m.content),
45
+ nl: () => import('./i18n/nl').then((m) => m.content),
46
+ pl: () => import('./i18n/pl').then((m) => m.content),
47
+ pt: () => import('./i18n/pt').then((m) => m.content),
48
+ ru: () => import('./i18n/ru').then((m) => m.content),
49
+ sv: () => import('./i18n/sv').then((m) => m.content),
50
+ tr: () => import('./i18n/tr').then((m) => m.content),
51
+ zh: () => import('./i18n/zh').then((m) => m.content),
52
+ },
53
+ };
@@ -0,0 +1,172 @@
1
+ .tool-main-card {
2
+ background: var(--bg-surface);
3
+ border: 1px solid var(--border-color);
4
+ border-radius: 1.25rem;
5
+ margin: 0 auto;
6
+ padding: 1.25rem;
7
+ display: flex;
8
+ flex-direction: column;
9
+ gap: 0.75rem;
10
+ box-shadow: var(--shadow-base);
11
+ }
12
+
13
+ .canvas-wrapper {
14
+ background: radial-gradient(ellipse at center, #1a1a2e 0%, #0f0f1a 100%);
15
+ border-radius: 0.75rem;
16
+ overflow: hidden;
17
+ border: 1px solid rgba(212, 175, 55, 0.15);
18
+ }
19
+
20
+ canvas#gear-canvas {
21
+ display: block;
22
+ width: 100%;
23
+ height: auto;
24
+ }
25
+
26
+ .controls-row {
27
+ display: flex;
28
+ flex-wrap: wrap;
29
+ gap: 0.75rem;
30
+ }
31
+
32
+ .control-group {
33
+ display: flex;
34
+ flex-direction: column;
35
+ gap: 0.2rem;
36
+ }
37
+
38
+ .control-label {
39
+ font-size: 0.6rem;
40
+ font-weight: 600;
41
+ text-transform: uppercase;
42
+ letter-spacing: 0.06em;
43
+ color: var(--text-base);
44
+ opacity: 0.5;
45
+ }
46
+
47
+ .control-group .btn-group {
48
+ display: flex;
49
+ gap: 0.25rem;
50
+ }
51
+
52
+ .control-group .btn-group button {
53
+ padding: 0.25rem 0.55rem;
54
+ font-size: 0.7rem;
55
+ font-weight: 500;
56
+ border: 1px solid var(--border-base);
57
+ border-radius: 0.4rem;
58
+ background: var(--bg-page);
59
+ color: var(--text-base);
60
+ cursor: pointer;
61
+ transition: all 0.2s ease;
62
+ }
63
+
64
+ .control-group .btn-group button:hover {
65
+ border-color: var(--accent);
66
+ color: var(--accent);
67
+ }
68
+
69
+ .control-group .btn-group button.active {
70
+ background: color-mix(in srgb, var(--accent) 10%, var(--bg-page));
71
+ border-color: var(--accent);
72
+ color: var(--accent);
73
+ box-shadow: 0 0 8px color-mix(in srgb, var(--accent) 12%, transparent);
74
+ }
75
+
76
+ .data-grid {
77
+ display: grid;
78
+ grid-template-columns: repeat(auto-fill, minmax(95px, 1fr));
79
+ gap: 0.35rem;
80
+ }
81
+
82
+ .data-card {
83
+ background: var(--bg-page);
84
+ border: 1px solid var(--border-base);
85
+ border-radius: 0.5rem;
86
+ padding: 0.4rem 0.5rem;
87
+ transition: all 0.2s ease;
88
+ cursor: default;
89
+ }
90
+
91
+ .data-card:hover {
92
+ border-color: var(--accent);
93
+ }
94
+
95
+ .data-card.highlighted {
96
+ border-color: var(--accent);
97
+ background: color-mix(in srgb, var(--accent) 6%, var(--bg-page));
98
+ box-shadow: 0 0 10px color-mix(in srgb, var(--accent) 8%, transparent);
99
+ }
100
+
101
+ .data-card .name {
102
+ font-size: 0.65rem;
103
+ font-weight: 600;
104
+ text-transform: uppercase;
105
+ letter-spacing: 0.04em;
106
+ color: var(--accent);
107
+ margin-bottom: 0.15rem;
108
+ }
109
+
110
+ .data-card .stats {
111
+ display: flex;
112
+ gap: 0.35rem;
113
+ flex-wrap: wrap;
114
+ }
115
+
116
+ .data-card .stats span {
117
+ font-size: 0.65rem;
118
+ color: var(--text-base);
119
+ opacity: 0.75;
120
+ }
121
+
122
+ .data-card .stats .val {
123
+ color: var(--accent);
124
+ font-weight: 600;
125
+ font-variant-numeric: tabular-nums;
126
+ opacity: 1;
127
+ }
128
+
129
+ .flow-bar {
130
+ background: var(--bg-page);
131
+ border: 1px solid var(--border-base);
132
+ border-radius: 0.5rem;
133
+ padding: 0.4rem 0.6rem;
134
+ }
135
+
136
+ .flow-bar .flow-label {
137
+ font-size: 0.6rem;
138
+ font-weight: 600;
139
+ text-transform: uppercase;
140
+ letter-spacing: 0.05em;
141
+ color: var(--text-base);
142
+ opacity: 0.5;
143
+ margin-bottom: 0.2rem;
144
+ }
145
+
146
+ .flow-bar .flow-path {
147
+ display: flex;
148
+ align-items: center;
149
+ gap: 0.2rem;
150
+ flex-wrap: wrap;
151
+ }
152
+
153
+ .flow-bar .flow-path .step {
154
+ font-size: 0.65rem;
155
+ color: var(--text-base);
156
+ opacity: 0.4;
157
+ padding: 0.1rem 0.3rem;
158
+ border-radius: 0.2rem;
159
+ transition: all 0.25s ease;
160
+ }
161
+
162
+ .flow-bar .flow-path .step.active {
163
+ opacity: 1;
164
+ color: var(--accent);
165
+ background: color-mix(in srgb, var(--accent) 8%, var(--bg-page));
166
+ }
167
+
168
+ .flow-bar .flow-path .arrow {
169
+ color: var(--text-base);
170
+ opacity: 0.2;
171
+ font-size: 0.7rem;
172
+ }