@jjlmoya/utils-chrono 1.9.0 → 1.11.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 (60) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +4 -0
  3. package/src/entries.ts +7 -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/sidereal-time-tracker/bibliography.astro +16 -0
  36. package/src/tool/sidereal-time-tracker/bibliography.ts +16 -0
  37. package/src/tool/sidereal-time-tracker/client.ts +278 -0
  38. package/src/tool/sidereal-time-tracker/component.astro +15 -0
  39. package/src/tool/sidereal-time-tracker/components/SiderealPanel.astro +197 -0
  40. package/src/tool/sidereal-time-tracker/entry.ts +51 -0
  41. package/src/tool/sidereal-time-tracker/helpers.ts +80 -0
  42. package/src/tool/sidereal-time-tracker/i18n/de.ts +93 -0
  43. package/src/tool/sidereal-time-tracker/i18n/en.ts +93 -0
  44. package/src/tool/sidereal-time-tracker/i18n/es.ts +93 -0
  45. package/src/tool/sidereal-time-tracker/i18n/fr.ts +93 -0
  46. package/src/tool/sidereal-time-tracker/i18n/id.ts +93 -0
  47. package/src/tool/sidereal-time-tracker/i18n/it.ts +93 -0
  48. package/src/tool/sidereal-time-tracker/i18n/ja.ts +93 -0
  49. package/src/tool/sidereal-time-tracker/i18n/ko.ts +93 -0
  50. package/src/tool/sidereal-time-tracker/i18n/nl.ts +93 -0
  51. package/src/tool/sidereal-time-tracker/i18n/pl.ts +93 -0
  52. package/src/tool/sidereal-time-tracker/i18n/pt.ts +93 -0
  53. package/src/tool/sidereal-time-tracker/i18n/ru.ts +93 -0
  54. package/src/tool/sidereal-time-tracker/i18n/sv.ts +93 -0
  55. package/src/tool/sidereal-time-tracker/i18n/tr.ts +93 -0
  56. package/src/tool/sidereal-time-tracker/i18n/zh.ts +93 -0
  57. package/src/tool/sidereal-time-tracker/index.ts +11 -0
  58. package/src/tool/sidereal-time-tracker/seo.astro +16 -0
  59. package/src/tool/sidereal-time-tracker/sidereal-time-tracker.css +257 -0
  60. package/src/tools.ts +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-chrono",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -14,6 +14,8 @@ import { tachymeterCalculator } from '../tool/tachymeter-calculator/entry';
14
14
  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
+ import { siderealTimeTracker } from '../tool/sidereal-time-tracker/entry';
18
+ import { gearTrainExplorer } from '../tool/gear-train-explorer/entry';
17
19
 
18
20
  export const chronoCategory: ChronoCategoryEntry = {
19
21
  icon: 'mdi:clock-outline',
@@ -33,6 +35,8 @@ export const chronoCategory: ChronoCategoryEntry = {
33
35
  serviceIntervalTracker,
34
36
  strapLengthCalculator,
35
37
  telemeterCalculator,
38
+ siderealTimeTracker,
39
+ gearTrainExplorer,
36
40
  ],
37
41
  i18n: {
38
42
  de: () => import('./i18n/de').then((m) => m.content),
package/src/entries.ts CHANGED
@@ -30,6 +30,10 @@ export { strapLengthCalculator } from './tool/strap-length-calculator/entry';
30
30
  export type { StrapLengthCalculatorUI, StrapLengthCalculatorLocaleContent } from './tool/strap-length-calculator/entry';
31
31
  export { telemeterCalculator } from './tool/telemeter-calculator/entry';
32
32
  export type { TelemeterCalculatorUI, TelemeterCalculatorLocaleContent } from './tool/telemeter-calculator/entry';
33
+ export { siderealTimeTracker } from './tool/sidereal-time-tracker/entry';
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';
33
37
  export { chronoCategory } from './category';
34
38
 
35
39
  import { watchAccuracyTracker } from './tool/watch-accuracy-tracker/entry';
@@ -48,6 +52,8 @@ import { tachymeterCalculator } from './tool/tachymeter-calculator/entry';
48
52
  import { serviceIntervalTracker } from './tool/service-interval-tracker/entry';
49
53
  import { strapLengthCalculator } from './tool/strap-length-calculator/entry';
50
54
  import { telemeterCalculator } from './tool/telemeter-calculator/entry';
55
+ import { siderealTimeTracker } from './tool/sidereal-time-tracker/entry';
56
+ import { gearTrainExplorer } from './tool/gear-train-explorer/entry';
51
57
 
52
- export const ALL_ENTRIES = [watchAccuracyTracker, wristPresenceCalculator, demagnetizingTimer, watchSavingsPlanner, crownReferenceGuide, powerReserveEstimator, beatRateConverter, waterResistanceConverter, strapTaperCalculator, watchSizeComparator, lumeColorSimulator, moonPhaseVisualizer, tachymeterCalculator, serviceIntervalTracker, strapLengthCalculator, telemeterCalculator];
58
+ export const ALL_ENTRIES = [watchAccuracyTracker, wristPresenceCalculator, demagnetizingTimer, watchSavingsPlanner, crownReferenceGuide, powerReserveEstimator, beatRateConverter, waterResistanceConverter, strapTaperCalculator, watchSizeComparator, lumeColorSimulator, moonPhaseVisualizer, tachymeterCalculator, serviceIntervalTracker, strapLengthCalculator, telemeterCalculator, siderealTimeTracker, gearTrainExplorer];
53
59
 
@@ -22,7 +22,7 @@ describe('Locale Completeness Validation', () => {
22
22
  });
23
23
 
24
24
  it('all tools registered', () => {
25
- expect(ALL_TOOLS.length).toBe(16);
25
+ expect(ALL_TOOLS.length).toBe(18);
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(16);
8
+ expect(ALL_TOOLS.length).toBe(18);
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
+ }