@jjlmoya/utils-chrono 1.18.0 → 1.20.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 (57) 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/index.ts +2 -0
  5. package/src/tests/locale_completeness.test.ts +1 -1
  6. package/src/tests/tool_validation.test.ts +1 -1
  7. package/src/tool/altitude-watch-accuracy-estimator/altitude-watch-accuracy-estimator.css +488 -0
  8. package/src/tool/altitude-watch-accuracy-estimator/bibliography.astro +16 -0
  9. package/src/tool/altitude-watch-accuracy-estimator/bibliography.ts +12 -0
  10. package/src/tool/altitude-watch-accuracy-estimator/client.ts +167 -0
  11. package/src/tool/altitude-watch-accuracy-estimator/component.astro +15 -0
  12. package/src/tool/altitude-watch-accuracy-estimator/components/AltitudeScene.astro +138 -0
  13. package/src/tool/altitude-watch-accuracy-estimator/entry.ts +61 -0
  14. package/src/tool/altitude-watch-accuracy-estimator/helpers.ts +49 -0
  15. package/src/tool/altitude-watch-accuracy-estimator/i18n/de.ts +101 -0
  16. package/src/tool/altitude-watch-accuracy-estimator/i18n/en.ts +101 -0
  17. package/src/tool/altitude-watch-accuracy-estimator/i18n/es.ts +101 -0
  18. package/src/tool/altitude-watch-accuracy-estimator/i18n/fr.ts +101 -0
  19. package/src/tool/altitude-watch-accuracy-estimator/i18n/id.ts +101 -0
  20. package/src/tool/altitude-watch-accuracy-estimator/i18n/it.ts +101 -0
  21. package/src/tool/altitude-watch-accuracy-estimator/i18n/ja.ts +101 -0
  22. package/src/tool/altitude-watch-accuracy-estimator/i18n/ko.ts +101 -0
  23. package/src/tool/altitude-watch-accuracy-estimator/i18n/nl.ts +101 -0
  24. package/src/tool/altitude-watch-accuracy-estimator/i18n/pl.ts +101 -0
  25. package/src/tool/altitude-watch-accuracy-estimator/i18n/pt.ts +101 -0
  26. package/src/tool/altitude-watch-accuracy-estimator/i18n/ru.ts +101 -0
  27. package/src/tool/altitude-watch-accuracy-estimator/i18n/sv.ts +101 -0
  28. package/src/tool/altitude-watch-accuracy-estimator/i18n/tr.ts +101 -0
  29. package/src/tool/altitude-watch-accuracy-estimator/i18n/zh.ts +101 -0
  30. package/src/tool/altitude-watch-accuracy-estimator/index.ts +11 -0
  31. package/src/tool/altitude-watch-accuracy-estimator/logic.ts +61 -0
  32. package/src/tool/altitude-watch-accuracy-estimator/seo.astro +16 -0
  33. package/src/tool/mainspring-finder/bibliography.astro +16 -0
  34. package/src/tool/mainspring-finder/bibliography.ts +16 -0
  35. package/src/tool/mainspring-finder/client.ts +89 -0
  36. package/src/tool/mainspring-finder/component.astro +72 -0
  37. package/src/tool/mainspring-finder/entry.ts +50 -0
  38. package/src/tool/mainspring-finder/helpers.ts +52 -0
  39. package/src/tool/mainspring-finder/i18n/de.ts +212 -0
  40. package/src/tool/mainspring-finder/i18n/en.ts +212 -0
  41. package/src/tool/mainspring-finder/i18n/es.ts +212 -0
  42. package/src/tool/mainspring-finder/i18n/fr.ts +212 -0
  43. package/src/tool/mainspring-finder/i18n/id.ts +212 -0
  44. package/src/tool/mainspring-finder/i18n/it.ts +212 -0
  45. package/src/tool/mainspring-finder/i18n/ja.ts +212 -0
  46. package/src/tool/mainspring-finder/i18n/ko.ts +212 -0
  47. package/src/tool/mainspring-finder/i18n/nl.ts +212 -0
  48. package/src/tool/mainspring-finder/i18n/pl.ts +212 -0
  49. package/src/tool/mainspring-finder/i18n/pt.ts +212 -0
  50. package/src/tool/mainspring-finder/i18n/ru.ts +212 -0
  51. package/src/tool/mainspring-finder/i18n/sv.ts +212 -0
  52. package/src/tool/mainspring-finder/i18n/tr.ts +212 -0
  53. package/src/tool/mainspring-finder/i18n/zh.ts +212 -0
  54. package/src/tool/mainspring-finder/index.ts +11 -0
  55. package/src/tool/mainspring-finder/mainspring-finder.css +242 -0
  56. package/src/tool/mainspring-finder/seo.astro +16 -0
  57. package/src/tools.ts +4 -0
@@ -0,0 +1,101 @@
1
+ import type { ToolLocaleContent } from '../../../types';
2
+ import type { AltitudeWatchAccuracyEstimatorUI } from '../entry';
3
+ import { bibliography } from '../bibliography';
4
+ import { buildSchemas } from '../helpers';
5
+
6
+ const faq = [
7
+ {
8
+ question: '为什么机械表在高海拔地区走得更快?',
9
+ answer: '机械表在高海拔地区走得更快的主要原因是空气密度降低。较稀薄的空气对摆轮产生的空气阻力更小,使其能够以稍大的振幅摆动。这种增加的振幅会导致手表走快 - 通常每升高1,000米,每天快2-6秒,具体取决于机芯设计。',
10
+ },
11
+ {
12
+ question: '海拔高度也会影响石英表吗?',
13
+ answer: '石英表由于没有摆动的机械摆轮,受海拔影响极小。然而,极端海拔变化可能因温度变化影响电池性能。与机械表相比,这种影响可以忽略不计。',
14
+ },
15
+ {
16
+ question: '海拔变化会损坏我的手表吗?',
17
+ answer: '单纯的海拔变化很少损坏机械表。但是,快速减压(如在飞机上)可能会导致某些手表的防水出现问题。旅行中的正常海拔变化完全在任何手表的设计公差范围内。',
18
+ },
19
+ ];
20
+
21
+ const howTo = [
22
+ {
23
+ name: '选择海拔高度',
24
+ text: '上下拖动海拔滑块,模拟从海平面到8,000米的不同海拔高度。观察摆轮摆动和大气数据如何实时变化。',
25
+ },
26
+ {
27
+ name: '读取偏差',
28
+ text: '速率偏差显示区显示选定海拔高度下每天估计的快慢秒数。下方的偏差图表显示所有海拔高度的趋势。',
29
+ },
30
+ {
31
+ name: '考虑影响因素',
32
+ text: '观察空气密度如何随海拔升高而降低,同时速率偏差如何增加。温度和压力数据为环境变化提供了背景信息。',
33
+ },
34
+ ];
35
+
36
+ const title = '海拔精度估算器:海拔高度对机械表的影响';
37
+
38
+ export const content: ToolLocaleContent<AltitudeWatchAccuracyEstimatorUI> = {
39
+ slug: 'altitude-watch-accuracy-estimator',
40
+ title,
41
+ description: '探索海拔高度如何影响机械表的精度。从海平面到山顶调节海拔高度,实时查看摆轮摆动、速率偏差、空气密度、压力和温度的变化。',
42
+ ui: {
43
+ title: '海拔精度估算器',
44
+ altitudeLabel: '海拔',
45
+ altitudeUnit: '米',
46
+ seaLevel: '海平面',
47
+ deviationLabel: '速率偏差',
48
+ deviationUnit: '秒/天',
49
+ pressureLabel: '气压',
50
+ pressureUnit: 'hPa',
51
+ densityLabel: '空气密度',
52
+ densityUnit: 'kg/m³',
53
+ temperatureLabel: '温度',
54
+ temperatureUnit: '°C',
55
+ oscillationLabel: '摆轮',
56
+ oscillationsPerSec: '次/秒',
57
+ rateLabel: '速率',
58
+ atmDataTitle: '大气条件',
59
+ howItWorks: '工作原理',
60
+ howItWorksDesc: '高海拔地区较低的空气密度减少了摆轮的阻力,增加了摆动幅度,导致手表走快。此工具基于标准大气模型估算速率偏差。',
61
+ negligible: '可忽略',
62
+ minor: '轻微',
63
+ noticeable: '明显',
64
+ significant: '显著',
65
+ severe: '严重',
66
+ step1: '拖动滑块模拟从海平面到8,000米的海拔高度。',
67
+ step2: '观察摆轮动画和偏差指示器的实时响应。',
68
+ step3: '查看大气数据以了解环境因素的影响。',
69
+ tipTitle: '提示',
70
+ tipContent: '效果因机芯而异:高振频机芯(36,000次/小时)通常比 vintage 低振频机芯(18,000次/小时)受影响更小。',
71
+ deviationChart: '偏差 vs 海拔',
72
+ altitudeM: '海拔(米)',
73
+ secondsPerDay: '秒/天',
74
+ particleLabel: '空气分子',
75
+ airDensity: '空气密度',
76
+ },
77
+ seo: [
78
+ { type: 'title', text: '机械表交互式海拔精度估算工具', level: 2 },
79
+ { type: 'paragraph', html: '<strong>海拔精度估算器</strong>是一个交互式工具,可可视化海拔变化如何影响机械表的精度。通过模拟从海平面到8,000米的海拔高度,您可以查看由空气密度、压力和温度变化引起的估计速率偏差。' },
80
+ { type: 'title', text: '海拔如何影响手表精度', level: 3 },
81
+ { type: 'paragraph', html: '在较高海拔高度,<strong>空气密度降低</strong>,从而减少摆轮的空气阻力。这使得摆轮能够以更大的振幅摆动,导致手表略微走快。效果通常为每升高1,000米<strong>每天快2-6秒</strong>。' },
82
+ { type: 'title', text: '不同海拔高度的速率偏差', level: 3 },
83
+ {
84
+ type: 'table', headers: ['海拔', '空气密度', '气压', '温度', '估计偏差'], rows: [
85
+ ['海平面(0米)', '1.225 kg/m³', '1013 hPa', '15°C', '基准'],
86
+ ['1,000米', '1.112 kg/m³', '898 hPa', '8.5°C', '+0.4 秒/天'],
87
+ ['2,000米', '1.007 kg/m³', '795 hPa', '2°C', '+0.9 秒/天'],
88
+ ['3,000米', '0.909 kg/m³', '701 hPa', '-4.5°C', '+1.5 秒/天'],
89
+ ['4,000米', '0.819 kg/m³', '616 hPa', '-11°C', '+2.1 秒/天'],
90
+ ['5,000米', '0.736 kg/m³', '540 hPa', '-17.5°C', '+2.8 秒/天'],
91
+ ]
92
+ },
93
+ { type: 'title', text: '环境因素', level: 3 },
94
+ { type: 'paragraph', html: '除了空气密度,高海拔地区的其他环境因素也会影响手表性能:<strong>温度</strong>影响润滑油粘度和发条弹性,<strong>压力变化</strong>可能影响表壳密封。然而,空气密度对摆轮阻力的影响是与海拔相关的速率变化的主导因素。' },
95
+ { type: 'diagnostic', variant: 'info', title: '交互式模拟工具', icon: 'mdi:axis-arrow', badge: '钟表学', html: '此工具基于国际标准大气(ISA)模型和经验观测提供估计值。实际结果因机芯型号、状况和制造公差而异。' },
96
+ ],
97
+ faq,
98
+ bibliography,
99
+ howTo,
100
+ schemas: buildSchemas(title, faq, howTo),
101
+ };
@@ -0,0 +1,11 @@
1
+ import type { ToolDefinition } from '../../types';
2
+ import { altitudeWatchAccuracyEstimator } from './entry';
3
+
4
+ export * from './entry';
5
+
6
+ export const ALTITUDE_WATCH_ACCURACY_ESTIMATOR_TOOL: ToolDefinition = {
7
+ entry: altitudeWatchAccuracyEstimator,
8
+ Component: () => import('./component.astro'),
9
+ SEOComponent: () => import('./seo.astro'),
10
+ BibliographyComponent: () => import('./bibliography.astro'),
11
+ };
@@ -0,0 +1,61 @@
1
+ export interface AtmosphericData {
2
+ altitude: number;
3
+ pressure: number;
4
+ density: number;
5
+ temperature: number;
6
+ deviation: number;
7
+ deviationDesc: string;
8
+ }
9
+
10
+ const SEA_LEVEL_PRESSURE = 1013.25;
11
+ const SEA_LEVEL_TEMP_K = 288.15;
12
+ const SEA_LEVEL_TEMP_C = 15;
13
+ const LAPSE_RATE = 0.0065;
14
+ const DEVIATION_FACTOR = 4.2;
15
+
16
+ export function getTemperature(altitude: number): number {
17
+ return SEA_LEVEL_TEMP_C - LAPSE_RATE * altitude;
18
+ }
19
+
20
+ export function getPressure(altitude: number): number {
21
+ const tRatio = 1 - LAPSE_RATE * altitude / SEA_LEVEL_TEMP_K;
22
+ if (tRatio <= 0) return 0;
23
+ return SEA_LEVEL_PRESSURE * Math.pow(tRatio, 5.255);
24
+ }
25
+
26
+ export function getAirDensity(altitude: number): number {
27
+ const T = getTemperature(altitude) + 273.15;
28
+ const P = getPressure(altitude) * 100;
29
+ return P / (287.058 * T);
30
+ }
31
+
32
+ export function getRateDeviation(altitude: number): number {
33
+ const rho0 = getAirDensity(0);
34
+ const rhoH = getAirDensity(altitude);
35
+ return DEVIATION_FACTOR * (rho0 / rhoH - 1);
36
+ }
37
+
38
+ export function getDeviationDescription(deviation: number): string {
39
+ if (deviation < 0.5) return 'negligible';
40
+ if (deviation < 2) return 'minor';
41
+ if (deviation < 5) return 'noticeable';
42
+ if (deviation < 10) return 'significant';
43
+ return 'severe';
44
+ }
45
+
46
+ export function getAtmosphericData(altitude: number): AtmosphericData {
47
+ const deviation = getRateDeviation(altitude);
48
+ return {
49
+ altitude,
50
+ pressure: Math.round(getPressure(altitude) * 10) / 10,
51
+ density: Math.round(getAirDensity(altitude) * 1000) / 1000,
52
+ temperature: Math.round(getTemperature(altitude) * 10) / 10,
53
+ deviation: Math.round(deviation * 10) / 10,
54
+ deviationDesc: getDeviationDescription(deviation),
55
+ };
56
+ }
57
+
58
+ export const DEVIATION_CHART_POINTS = Array.from({ length: 81 }, (_, i) => {
59
+ const alt = i * 100;
60
+ return { altitude: alt, deviation: Math.round(getRateDeviation(alt) * 10) / 10 };
61
+ });
@@ -0,0 +1,16 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { altitudeWatchAccuracyEstimator } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const loader = altitudeWatchAccuracyEstimator.i18n[locale] || altitudeWatchAccuracyEstimator.i18n.en;
12
+ const content = await loader?.();
13
+ if (!content) return null;
14
+ ---
15
+
16
+ {content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,16 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { mainspringFinder } 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 = mainspringFinder.i18n[locale] || mainspringFinder.i18n.en;
12
+ const content = await loader?.();
13
+ if (!content) return null;
14
+ ---
15
+
16
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,16 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Mainspring Dimensions & Specifications - Ranfft',
6
+ url: 'https://ranfft.org/caliber/5755-FHF-76-2',
7
+ },
8
+ {
9
+ name: 'Watch Mainspring Guide',
10
+ url: 'https://www.vintagewatchstraps.com/mainsprings.php',
11
+ },
12
+ {
13
+ name: 'Mainspring Sizing Chart - Cousins UK',
14
+ url: 'https://www.cousinsuk.com/category/filter/mainsprings-by-size-search-watch-pocket',
15
+ },
16
+ ];
@@ -0,0 +1,89 @@
1
+ import { calculateMainspring, fmt } from './helpers';
2
+
3
+ const el = document.querySelector('.msf') as HTMLElement;
4
+ const UI = JSON.parse(el.dataset.ui || '{}');
5
+
6
+ const barrelIn = document.getElementById('msf-barrel') as HTMLInputElement;
7
+ const arborIn = document.getElementById('msf-arbor') as HTMLInputElement;
8
+ const heightIn = document.getElementById('msf-height') as HTMLInputElement;
9
+ const turnsIn = document.getElementById('msf-turns') as HTMLInputElement;
10
+ const unitBtns = document.querySelectorAll('.msf-u-btn') as NodeListOf<HTMLElement>;
11
+ const result = document.getElementById('msf-result') as HTMLElement;
12
+ const thickV = document.getElementById('msf-thick') as HTMLElement;
13
+ const heightV = document.getElementById('msf-h') as HTMLElement;
14
+ const lenV = document.getElementById('msf-len') as HTMLElement;
15
+ const strengthB = document.getElementById('msf-strength') as HTMLElement;
16
+ const commV = document.getElementById('msf-comm') as HTMLElement;
17
+
18
+ const sLabels = [UI.strengthWeak || 'Light', UI.strengthMedium || 'Medium', UI.strengthStrong || 'Strong'];
19
+ const sColors = ['#10b981', '#f59e0b', '#ef4444'];
20
+ const KEY = 'jjlmoya_chrono_msf';
21
+ let cur: 'mm' | 'in' = 'mm';
22
+
23
+ interface S { b: string; a: string; h: string; t: string; u: string }
24
+
25
+ function parseNum(v: string): number {
26
+ return parseFloat(v.replace(',', '.')) || 0;
27
+ }
28
+
29
+ function save() {
30
+ try { localStorage.setItem(KEY, JSON.stringify({ b: barrelIn.value, a: arborIn.value, h: heightIn.value, t: turnsIn.value, u: cur } as S)); } catch {}
31
+ }
32
+
33
+ function load() {
34
+ try {
35
+ const r = localStorage.getItem(KEY);
36
+ if (!r) return;
37
+ const s: S = JSON.parse(r);
38
+ const pairs: [string | undefined, HTMLInputElement][] = [[s.b, barrelIn], [s.a, arborIn], [s.h, heightIn], [s.t, turnsIn]];
39
+ for (const [v, el] of pairs) { if (v) el.value = v; }
40
+ if (s.u === 'in' || s.u === 'mm') cur = s.u;
41
+ } catch {}
42
+ }
43
+
44
+ function convertVal(v: number, from: 'mm' | 'in', to: 'mm' | 'in'): number {
45
+ if (from === to) return v;
46
+ return to === 'in' ? v / 25.4 : v * 25.4;
47
+ }
48
+
49
+ function setUnit(u: 'mm' | 'in') {
50
+ if (u === cur) return;
51
+ const b = parseNum(barrelIn.value);
52
+ const a = parseNum(arborIn.value);
53
+ const h = parseNum(heightIn.value);
54
+ barrelIn.value = convertVal(b, cur, u).toFixed(u === 'in' ? 2 : 1);
55
+ arborIn.value = convertVal(a, cur, u).toFixed(u === 'in' ? 3 : 1);
56
+ heightIn.value = convertVal(h, cur, u).toFixed(u === 'in' ? 3 : 2);
57
+ barrelIn.step = ''; arborIn.step = ''; heightIn.step = '';
58
+ cur = u;
59
+ unitBtns.forEach(b => b.classList.toggle('msf-u-on', b.dataset.unit === u));
60
+ calc();
61
+ }
62
+
63
+ function calc() {
64
+ const barrelId = parseNum(barrelIn.value);
65
+ const arborD = parseNum(arborIn.value);
66
+ const height = parseNum(heightIn.value);
67
+ const turns = parseInt(turnsIn.value, 10) || 6;
68
+ const r = calculateMainspring({ barrelId, barrelH: height, arborD, turns, unit: cur });
69
+ if (!r) { result.style.display = 'none'; return; }
70
+
71
+ const ul = cur;
72
+ thickV.textContent = `${fmt(r.thickness, cur, cur === 'in' ? 4 : 3)} ${ul}`;
73
+ heightV.textContent = `${fmt(r.height, cur, 2)} ${ul}`;
74
+ lenV.textContent = `${fmt(r.length, cur, 1)} ${ul}`;
75
+ strengthB.textContent = sLabels[r.strengthIndex];
76
+ strengthB.style.background = sColors[r.strengthIndex];
77
+ commV.textContent = `${fmt(r.height, cur, 2)} x ${fmt(r.thickness, cur, cur === 'in' ? 4 : 3)} x ${fmt(r.length, cur, 1)} ${ul}`;
78
+ result.style.display = 'flex';
79
+ save();
80
+ }
81
+
82
+ unitBtns.forEach(b => b.addEventListener('click', () => setUnit(b.dataset.unit as 'mm' | 'in')));
83
+ barrelIn.addEventListener('input', calc);
84
+ arborIn.addEventListener('input', calc);
85
+ heightIn.addEventListener('input', calc);
86
+ turnsIn.addEventListener('input', calc);
87
+ load();
88
+ setUnit(cur);
89
+ calc();
@@ -0,0 +1,72 @@
1
+ ---
2
+ interface Props { ui: Record<string, string> }
3
+ const { ui } = Astro.props;
4
+ ---
5
+
6
+ <div class="msf" data-ui={JSON.stringify(ui)}>
7
+ <div class="msf-top">
8
+ <div class="msf-u">
9
+ <button class="msf-u-btn msf-u-on" data-unit="mm" type="button">{ui.mm || "mm"}</button>
10
+ <button class="msf-u-btn" data-unit="in" type="button">{ui.inch || "in"}</button>
11
+ </div>
12
+ </div>
13
+
14
+ <div class="msf-grid">
15
+ <label class="msf-f">
16
+ <span class="msf-f-l">{ui.barrelLabel || "Barrel ID"}</span>
17
+ <input class="msf-f-i" id="msf-barrel" type="text" inputmode="decimal" value="14.0" />
18
+ </label>
19
+ <label class="msf-f">
20
+ <span class="msf-f-l">{ui.arborLabel || "Arbor OD"}</span>
21
+ <input class="msf-f-i" id="msf-arbor" type="text" inputmode="decimal" value="3.5" />
22
+ </label>
23
+ <label class="msf-f">
24
+ <span class="msf-f-l">{ui.heightLabel || "Height"}</span>
25
+ <input class="msf-f-i" id="msf-height" type="text" inputmode="decimal" value="1.50" />
26
+ </label>
27
+ <label class="msf-f">
28
+ <span class="msf-f-l">{ui.turnsLabel || "Turns"}</span>
29
+ <input class="msf-f-i" id="msf-turns" type="text" inputmode="numeric" value="6" />
30
+ </label>
31
+ </div>
32
+
33
+ <div class="msf-r" id="msf-result">
34
+ <div class="msf-r-grid">
35
+ <div class="msf-r-c">
36
+ <span class="msf-r-c-l">{ui.resultThickness || "Thickness"}</span>
37
+ <span class="msf-r-c-v" id="msf-thick">--</span>
38
+ </div>
39
+ <div class="msf-r-c">
40
+ <span class="msf-r-c-l">{ui.resultHeight || "Height"}</span>
41
+ <span class="msf-r-c-v" id="msf-h">--</span>
42
+ </div>
43
+ <div class="msf-r-c">
44
+ <span class="msf-r-c-l">{ui.resultLength || "Length"}</span>
45
+ <span class="msf-r-c-v" id="msf-len">--</span>
46
+ </div>
47
+ </div>
48
+ <div class="msf-r-bot">
49
+ <div class="msf-r-bot-i">
50
+ <span class="msf-r-bot-l">{ui.resultStrength || "Strength"}</span>
51
+ <span class="msf-badge" id="msf-strength">--</span>
52
+ </div>
53
+ <div class="msf-r-bot-i">
54
+ <span class="msf-r-bot-l">{ui.commercial || "Size"}</span>
55
+ <span class="msf-r-bot-v" id="msf-comm">--</span>
56
+ </div>
57
+ </div>
58
+ </div>
59
+
60
+ <div class="msf-steps">
61
+ <div class="msf-step"><span class="msf-step-n">1</span><span>{ui.step1 || "Measure barrel ID, arbor OD, and height."}</span></div>
62
+ <div class="msf-step"><span class="msf-step-n">2</span><span>{ui.step2 || "Set winding turns (5-8 manual, 6-10 auto)."}</span></div>
63
+ <div class="msf-step"><span class="msf-step-n">3</span><span>{ui.step3 || "Toggle mm/in for your unit."}</span></div>
64
+ </div>
65
+
66
+ <div class="msf-tip">
67
+ <svg viewBox="0 0 24 24" width="14" height="14" style="flex-shrink:0;margin-top:0.05rem"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" fill="currentColor"/></svg>
68
+ <span>{ui.tipContent || "Always verify against manufacturer specs before ordering."}</span>
69
+ </div>
70
+ </div>
71
+
72
+ <script src="./client.ts"></script>
@@ -0,0 +1,50 @@
1
+ import type { ChronoToolEntry, ToolLocaleContent } from '../../types';
2
+
3
+ export type MainspringFinderUI = {
4
+ title: string;
5
+ barrelLabel: string;
6
+ arborLabel: string;
7
+ heightLabel: string;
8
+ turnsLabel: string;
9
+ calculate: string;
10
+ resultThickness: string;
11
+ resultHeight: string;
12
+ resultLength: string;
13
+ resultStrength: string;
14
+ strengthWeak: string;
15
+ strengthMedium: string;
16
+ strengthStrong: string;
17
+ commercial: string;
18
+ unitLabel: string;
19
+ mm: string;
20
+ inch: string;
21
+ step1: string;
22
+ step2: string;
23
+ step3: string;
24
+ tipTitle: string;
25
+ tipContent: string;
26
+ };
27
+
28
+ export type MainspringFinderLocaleContent = ToolLocaleContent<MainspringFinderUI>;
29
+
30
+ export const mainspringFinder: ChronoToolEntry<MainspringFinderUI> = {
31
+ id: 'mainspring-finder',
32
+ icons: { bg: 'mdi:ruler', fg: 'mdi:cog-clockwise' },
33
+ i18n: {
34
+ en: () => import('./i18n/en').then((m) => m.content),
35
+ de: () => import('./i18n/de').then((m) => m.content),
36
+ es: () => import('./i18n/es').then((m) => m.content),
37
+ fr: () => import('./i18n/fr').then((m) => m.content),
38
+ id: () => import('./i18n/id').then((m) => m.content),
39
+ it: () => import('./i18n/it').then((m) => m.content),
40
+ ja: () => import('./i18n/ja').then((m) => m.content),
41
+ ko: () => import('./i18n/ko').then((m) => m.content),
42
+ nl: () => import('./i18n/nl').then((m) => m.content),
43
+ pl: () => import('./i18n/pl').then((m) => m.content),
44
+ pt: () => import('./i18n/pt').then((m) => m.content),
45
+ ru: () => import('./i18n/ru').then((m) => m.content),
46
+ sv: () => import('./i18n/sv').then((m) => m.content),
47
+ tr: () => import('./i18n/tr').then((m) => m.content),
48
+ zh: () => import('./i18n/zh').then((m) => m.content),
49
+ },
50
+ };
@@ -0,0 +1,52 @@
1
+ export interface MainspringInput {
2
+ barrelId: number;
3
+ barrelH: number;
4
+ arborD: number;
5
+ turns: number;
6
+ unit: 'mm' | 'in';
7
+ }
8
+
9
+ export interface MainspringResult {
10
+ thickness: number;
11
+ height: number;
12
+ length: number;
13
+ strengthIndex: number;
14
+ }
15
+
16
+ export function toMm(val: number, unit: 'mm' | 'in'): number {
17
+ return unit === 'in' ? val * 25.4 : val;
18
+ }
19
+
20
+ export function fmt(val: number, unit: 'mm' | 'in', dec: number): string {
21
+ const v = unit === 'in' ? val / 25.4 : val;
22
+ return v.toFixed(dec);
23
+ }
24
+
25
+ function getStrengthIndex(thickness: number): number {
26
+ if (thickness <= 0.10) return 0;
27
+ if (thickness <= 0.18) return 1;
28
+ return 2;
29
+ }
30
+
31
+ export function calculateMainspring(input: MainspringInput): MainspringResult | null {
32
+ let { barrelId, barrelH, arborD } = input;
33
+ const { turns, unit } = input;
34
+ barrelId = toMm(barrelId, unit);
35
+ barrelH = toMm(barrelH, unit);
36
+ arborD = toMm(arborD, unit);
37
+
38
+ if (barrelId <= 0 || arborD <= 0 || barrelH <= 0 || arborD >= barrelId) return null;
39
+
40
+ const clearance = 0.1;
41
+ const height = barrelH - clearance;
42
+
43
+ const thickness = (barrelId - arborD) / (4 * Math.PI * turns);
44
+ const length = (Math.PI * turns * (barrelId + arborD)) / 2;
45
+
46
+ return {
47
+ thickness: Math.round(thickness * 1000) / 1000,
48
+ height: Math.round(height * 100) / 100,
49
+ length: Math.round(length * 10) / 10,
50
+ strengthIndex: getStrengthIndex(thickness),
51
+ };
52
+ }