@jjlmoya/utils-chrono 1.22.0 → 1.24.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.
- package/package.json +1 -1
- package/src/layouts/PreviewLayout.astro +1 -1
- package/src/tests/diacritics_density.test.ts +118 -0
- package/src/tests/inverted_punctuation.test.ts +84 -0
- package/src/tests/script_density.test.ts +94 -0
- package/src/tool/gear-train-explorer/client.ts +13 -12
- package/src/tool/gear-train-explorer/component.astro +1 -1
- package/src/tool/gear-train-explorer/gear-train-explorer.css +49 -24
- package/src/tool/mainspring-finder/i18n/es.ts +6 -6
- package/src/tool/mainspring-finder/i18n/it.ts +1 -1
- package/src/tool/power-reserve-estimator/client.ts +13 -12
- package/src/tool/power-reserve-estimator/component.astro +1 -1
- package/src/tool/power-reserve-estimator/power-reserve-estimator.css +69 -53
- package/src/tool/quartz-battery-health/client.ts +20 -20
- package/src/tool/quartz-battery-health/component.astro +1 -1
- package/src/tool/quartz-battery-health/i18n/es.ts +1 -1
- package/src/tool/quartz-battery-health/i18n/fr.ts +1 -1
- package/src/tool/quartz-battery-health/i18n/pt.ts +1 -1
- package/src/tool/quartz-battery-health/quartz-battery-health.css +77 -59
- package/src/tool/sidereal-time-tracker/component.astro +1 -1
- package/src/tool/sidereal-time-tracker/sidereal-time-tracker.css +60 -1
- package/src/tool/tachymeter-calculator/i18n/es.ts +6 -6
- package/src/tool/tachymeter-calculator/i18n/fr.ts +1 -1
- package/src/tool/tachymeter-calculator/i18n/it.ts +1 -1
- package/src/tool/tachymeter-calculator/i18n/pl.ts +1 -1
- package/src/tool/tachymeter-calculator/i18n/pt.ts +1 -1
- package/src/tool/tachymeter-calculator/i18n/tr.ts +1 -1
- package/src/tool/telemeter-calculator/component.astro +1 -1
- package/src/tool/telemeter-calculator/i18n/es.ts +1 -1
- package/src/tool/telemeter-calculator/telemeter-calculator.css +51 -6
- package/src/tool/watch-savings-planner/i18n/es.ts +2 -2
- package/src/tool/watch-savings-planner/i18n/pl.ts +1 -1
- package/src/tool/wrist-presence-calculator/client.ts +1 -1
- package/src/tool/wrist-presence-calculator/component.astro +3 -3
- package/src/tool/wrist-presence-calculator/wrist-presence-calculator.css +30 -8
package/package.json
CHANGED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
|
|
4
|
+
type LocaleWithDiacritics = keyof typeof DIACRITIC_RULES;
|
|
5
|
+
|
|
6
|
+
const DIACRITIC_RULES = {
|
|
7
|
+
de: {
|
|
8
|
+
language: 'German',
|
|
9
|
+
expectedCharacters: 'ä ö ü ß',
|
|
10
|
+
characters: /[äöüÄÖÜß]/g,
|
|
11
|
+
minPerThousandLetters: 0.1,
|
|
12
|
+
},
|
|
13
|
+
es: {
|
|
14
|
+
language: 'Spanish',
|
|
15
|
+
expectedCharacters: 'á é í ó ú ü ñ',
|
|
16
|
+
characters: /[áéíóúüñÁÉÍÓÚÜÑ]/g,
|
|
17
|
+
minPerThousandLetters: 0.1,
|
|
18
|
+
},
|
|
19
|
+
fr: {
|
|
20
|
+
language: 'French',
|
|
21
|
+
expectedCharacters: 'à â æ ç é è ê ë î ï ô œ ù û ü ÿ',
|
|
22
|
+
characters: /[àâæçéèêëîïôœùûüÿÀÂÆÇÉÈÊËÎÏÔŒÙÛÜŸ]/g,
|
|
23
|
+
minPerThousandLetters: 0.1,
|
|
24
|
+
},
|
|
25
|
+
it: {
|
|
26
|
+
language: 'Italian',
|
|
27
|
+
expectedCharacters: 'à è é ì í î ò ó ù ú',
|
|
28
|
+
characters: /[àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ]/g,
|
|
29
|
+
minPerThousandLetters: 0.1,
|
|
30
|
+
},
|
|
31
|
+
pl: {
|
|
32
|
+
language: 'Polish',
|
|
33
|
+
expectedCharacters: 'ą ć ę ł ń ó ś ź ż',
|
|
34
|
+
characters: /[ąćęłńóśźżĄĆĘŁŃÓŚŹŻ]/g,
|
|
35
|
+
minPerThousandLetters: 0.1,
|
|
36
|
+
},
|
|
37
|
+
pt: {
|
|
38
|
+
language: 'Portuguese',
|
|
39
|
+
expectedCharacters: 'á â ã à ç é ê í ó ô õ ú ü',
|
|
40
|
+
characters: /[áâãàçéêíóôõúüÁÂÃÀÇÉÊÍÓÔÕÚÜ]/g,
|
|
41
|
+
minPerThousandLetters: 0.1,
|
|
42
|
+
},
|
|
43
|
+
sv: {
|
|
44
|
+
language: 'Swedish',
|
|
45
|
+
expectedCharacters: 'å ä ö',
|
|
46
|
+
characters: /[åäöÅÄÖ]/g,
|
|
47
|
+
minPerThousandLetters: 0.1,
|
|
48
|
+
},
|
|
49
|
+
tr: {
|
|
50
|
+
language: 'Turkish',
|
|
51
|
+
expectedCharacters: 'ç ğ ı İ ö ş ü',
|
|
52
|
+
characters: /[çğıöşüÇĞİÖŞÜ]/g,
|
|
53
|
+
minPerThousandLetters: 0.1,
|
|
54
|
+
},
|
|
55
|
+
} as const;
|
|
56
|
+
|
|
57
|
+
const LETTERS = /\p{L}/gu;
|
|
58
|
+
const TRANSLATABLE_KEYS = ['title', 'description', 'ui', 'seo', 'faq', 'howTo'] as const;
|
|
59
|
+
|
|
60
|
+
function collectStrings(value: unknown): string[] {
|
|
61
|
+
if (typeof value === 'string') return [value];
|
|
62
|
+
if (!value || typeof value !== 'object') return [];
|
|
63
|
+
if (Array.isArray(value)) return value.flatMap(collectStrings);
|
|
64
|
+
return Object.values(value).flatMap(collectStrings);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeText(value: unknown): string {
|
|
68
|
+
return collectStrings(value).join(' ').normalize('NFC');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function translatableContent(content: Record<string, unknown>) {
|
|
72
|
+
return TRANSLATABLE_KEYS.map((key) => content[key]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function letterCount(text: string): number {
|
|
76
|
+
return text.match(LETTERS)?.length ?? 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function diacriticCount(text: string, locale: LocaleWithDiacritics): number {
|
|
80
|
+
return text.match(DIACRITIC_RULES[locale].characters)?.length ?? 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function diacriticsPerThousandLetters(text: string, locale: LocaleWithDiacritics): number {
|
|
84
|
+
const letters = letterCount(text);
|
|
85
|
+
if (letters === 0) return 0;
|
|
86
|
+
return diacriticCount(text, locale) / letters * 1000;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe('Diacritics density validation', () => {
|
|
90
|
+
ALL_TOOLS.forEach((tool) => {
|
|
91
|
+
describe(`Tool: ${tool.entry.id}`, () => {
|
|
92
|
+
Object.keys(DIACRITIC_RULES).forEach((locale) => {
|
|
93
|
+
it(`${locale} keeps the expected accent and special-letter set`, async () => {
|
|
94
|
+
const typedLocale = locale as LocaleWithDiacritics;
|
|
95
|
+
const loader = tool.entry.i18n[typedLocale];
|
|
96
|
+
if (!loader) return;
|
|
97
|
+
|
|
98
|
+
const content = await loader();
|
|
99
|
+
const text = normalizeText(translatableContent(content as Record<string, unknown>));
|
|
100
|
+
const rule = DIACRITIC_RULES[typedLocale];
|
|
101
|
+
const letters = letterCount(text);
|
|
102
|
+
const matches = diacriticCount(text, typedLocale);
|
|
103
|
+
const density = diacriticsPerThousandLetters(text, typedLocale);
|
|
104
|
+
|
|
105
|
+
expect(
|
|
106
|
+
density,
|
|
107
|
+
[
|
|
108
|
+
`Possible spelling or encoding issue detected in ${tool.entry.id}/${typedLocale} (${rule.language}).`,
|
|
109
|
+
`The text has ${matches} special characters (${density.toFixed(2)} per 1000 letters, ${letters} letters analyzed).`,
|
|
110
|
+
`This locale should include some of these characters: ${rule.expectedCharacters}.`,
|
|
111
|
+
'If the count is 0 or near 0, accents, tildes, or special letters were probably stripped by encoding or normalization.',
|
|
112
|
+
].join(' '),
|
|
113
|
+
).toBeGreaterThanOrEqual(rule.minPerThousandLetters);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
|
|
4
|
+
const INVERTED_PUNCTUATION_LOCALES = {
|
|
5
|
+
es: {
|
|
6
|
+
language: 'Spanish',
|
|
7
|
+
questionStart: '¿',
|
|
8
|
+
questionEnd: '?',
|
|
9
|
+
exclamationStart: '¡',
|
|
10
|
+
exclamationEnd: '!',
|
|
11
|
+
},
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
type InvertedPunctuationLocale = keyof typeof INVERTED_PUNCTUATION_LOCALES;
|
|
15
|
+
|
|
16
|
+
const TRANSLATABLE_KEYS = ['title', 'description', 'ui', 'seo', 'faq', 'howTo'] as const;
|
|
17
|
+
|
|
18
|
+
function collectStrings(value: unknown): string[] {
|
|
19
|
+
if (typeof value === 'string') return [value];
|
|
20
|
+
if (!value || typeof value !== 'object') return [];
|
|
21
|
+
if (Array.isArray(value)) return value.flatMap(collectStrings);
|
|
22
|
+
return Object.values(value).flatMap(collectStrings);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function translatableStrings(content: Record<string, unknown>): string[] {
|
|
26
|
+
return TRANSLATABLE_KEYS.flatMap((key) => collectStrings(content[key]));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sentenceStart(text: string, endIndex: number): string {
|
|
30
|
+
const beforeMark = text.slice(0, endIndex).trimEnd();
|
|
31
|
+
const boundary = Math.max(
|
|
32
|
+
beforeMark.lastIndexOf('.'),
|
|
33
|
+
beforeMark.lastIndexOf(':'),
|
|
34
|
+
beforeMark.lastIndexOf(';'),
|
|
35
|
+
beforeMark.lastIndexOf('\n'),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return beforeMark.slice(boundary + 1).trimStart();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function findMissingInvertedMarks(
|
|
42
|
+
text: string,
|
|
43
|
+
startMark: string,
|
|
44
|
+
endMark: string,
|
|
45
|
+
): string[] {
|
|
46
|
+
return [...text.matchAll(new RegExp(`\\${endMark}`, 'g'))]
|
|
47
|
+
.map((match) => sentenceStart(text, match.index ?? 0))
|
|
48
|
+
.filter((segment) => segment.length > 0 && !segment.includes(startMark))
|
|
49
|
+
.map((segment) => `${segment}${endMark}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('Inverted punctuation validation', () => {
|
|
53
|
+
ALL_TOOLS.forEach((tool) => {
|
|
54
|
+
describe(`Tool: ${tool.entry.id}`, () => {
|
|
55
|
+
Object.keys(INVERTED_PUNCTUATION_LOCALES).forEach((locale) => {
|
|
56
|
+
it(`${locale} uses opening punctuation marks for questions and exclamations`, async () => {
|
|
57
|
+
const typedLocale = locale as InvertedPunctuationLocale;
|
|
58
|
+
const loader = tool.entry.i18n[typedLocale];
|
|
59
|
+
if (!loader) return;
|
|
60
|
+
|
|
61
|
+
const rule = INVERTED_PUNCTUATION_LOCALES[typedLocale];
|
|
62
|
+
const content = await loader();
|
|
63
|
+
const strings = translatableStrings(content as Record<string, unknown>);
|
|
64
|
+
const missingQuestions = strings.flatMap((text) =>
|
|
65
|
+
findMissingInvertedMarks(text, rule.questionStart, rule.questionEnd)
|
|
66
|
+
);
|
|
67
|
+
const missingExclamations = strings.flatMap((text) =>
|
|
68
|
+
findMissingInvertedMarks(text, rule.exclamationStart, rule.exclamationEnd)
|
|
69
|
+
);
|
|
70
|
+
const failures = [...missingQuestions, ...missingExclamations];
|
|
71
|
+
|
|
72
|
+
expect(
|
|
73
|
+
failures,
|
|
74
|
+
[
|
|
75
|
+
`Missing opening punctuation marks in ${tool.entry.id}/${typedLocale} (${rule.language}).`,
|
|
76
|
+
`Questions must use ${rule.questionStart}...${rule.questionEnd} and exclamations must use ${rule.exclamationStart}...${rule.exclamationEnd}.`,
|
|
77
|
+
`Examples: ${failures.slice(0, 5).join(' | ')}`,
|
|
78
|
+
].join(' '),
|
|
79
|
+
).toHaveLength(0);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
|
|
4
|
+
type ScriptLocale = keyof typeof SCRIPT_RULES;
|
|
5
|
+
|
|
6
|
+
const SCRIPT_RULES = {
|
|
7
|
+
ja: {
|
|
8
|
+
language: 'Japanese',
|
|
9
|
+
scriptName: 'kana/kanji',
|
|
10
|
+
scriptCharacters: /[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}]/gu,
|
|
11
|
+
minScriptRatio: 0.45,
|
|
12
|
+
},
|
|
13
|
+
ko: {
|
|
14
|
+
language: 'Korean',
|
|
15
|
+
scriptName: 'hangul',
|
|
16
|
+
scriptCharacters: /\p{Script=Hangul}/gu,
|
|
17
|
+
minScriptRatio: 0.55,
|
|
18
|
+
},
|
|
19
|
+
ru: {
|
|
20
|
+
language: 'Russian',
|
|
21
|
+
scriptName: 'cyrillic',
|
|
22
|
+
scriptCharacters: /\p{Script=Cyrillic}/gu,
|
|
23
|
+
minScriptRatio: 0.65,
|
|
24
|
+
},
|
|
25
|
+
zh: {
|
|
26
|
+
language: 'Chinese',
|
|
27
|
+
scriptName: 'han',
|
|
28
|
+
scriptCharacters: /\p{Script=Han}/gu,
|
|
29
|
+
minScriptRatio: 0.45,
|
|
30
|
+
},
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
const LETTERS = /\p{L}/gu;
|
|
34
|
+
const TRANSLATABLE_KEYS = ['title', 'description', 'ui', 'seo', 'faq', 'howTo'] as const;
|
|
35
|
+
|
|
36
|
+
function collectStrings(value: unknown): string[] {
|
|
37
|
+
if (typeof value === 'string') return [value];
|
|
38
|
+
if (!value || typeof value !== 'object') return [];
|
|
39
|
+
if (Array.isArray(value)) return value.flatMap(collectStrings);
|
|
40
|
+
return Object.values(value).flatMap(collectStrings);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeText(value: unknown): string {
|
|
44
|
+
return collectStrings(value).join(' ').normalize('NFC');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function translatableContent(content: Record<string, unknown>) {
|
|
48
|
+
return TRANSLATABLE_KEYS.map((key) => content[key]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function letterCount(text: string): number {
|
|
52
|
+
return text.match(LETTERS)?.length ?? 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function scriptCount(text: string, locale: ScriptLocale): number {
|
|
56
|
+
return text.match(SCRIPT_RULES[locale].scriptCharacters)?.length ?? 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function scriptRatio(text: string, locale: ScriptLocale): number {
|
|
60
|
+
const letters = letterCount(text);
|
|
61
|
+
if (letters === 0) return 0;
|
|
62
|
+
return scriptCount(text, locale) / letters;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe('Native script density validation', () => {
|
|
66
|
+
ALL_TOOLS.forEach((tool) => {
|
|
67
|
+
describe(`Tool: ${tool.entry.id}`, () => {
|
|
68
|
+
Object.keys(SCRIPT_RULES).forEach((locale) => {
|
|
69
|
+
it(`${locale} keeps most translated text in its native script`, async () => {
|
|
70
|
+
const typedLocale = locale as ScriptLocale;
|
|
71
|
+
const loader = tool.entry.i18n[typedLocale];
|
|
72
|
+
if (!loader) return;
|
|
73
|
+
|
|
74
|
+
const content = await loader();
|
|
75
|
+
const rule = SCRIPT_RULES[typedLocale];
|
|
76
|
+
const text = normalizeText(translatableContent(content as Record<string, unknown>));
|
|
77
|
+
const letters = letterCount(text);
|
|
78
|
+
const matches = scriptCount(text, typedLocale);
|
|
79
|
+
const ratio = scriptRatio(text, typedLocale);
|
|
80
|
+
|
|
81
|
+
expect(
|
|
82
|
+
ratio,
|
|
83
|
+
[
|
|
84
|
+
`Possible broken translation detected in ${tool.entry.id}/${typedLocale} (${rule.language}).`,
|
|
85
|
+
`The text has ${matches} ${rule.scriptName} characters out of ${letters} analyzed letters (${(ratio * 100).toFixed(1)}%).`,
|
|
86
|
+
`Most translatable content should be written in ${rule.scriptName} script.`,
|
|
87
|
+
'Non-translatable fields such as slug, bibliography, and schemas are ignored to avoid false positives.',
|
|
88
|
+
].join(' '),
|
|
89
|
+
).toBeGreaterThanOrEqual(rule.minScriptRatio);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const canvas = document.getElementById('gear-canvas') as HTMLCanvasElement;
|
|
2
2
|
const ctx = canvas.getContext('2d')!;
|
|
3
|
+
const root = canvas.closest('.gear-train-card') || document;
|
|
3
4
|
|
|
4
5
|
import { MOVEMENTS } from './movements';
|
|
5
6
|
import type { MovementDef } from './movements';
|
|
@@ -34,17 +35,17 @@ function resizeCanvas() {
|
|
|
34
35
|
|
|
35
36
|
function updateStats() {
|
|
36
37
|
const g = currentMov.gears;
|
|
37
|
-
const setText = (id: string, v: string) => { const el =
|
|
38
|
+
const setText = (id: string, v: string) => { const el = root.querySelector('#rpm-' + id); if (el) el.textContent = v; };
|
|
38
39
|
setText('barrel', (g[0].rpm * 60).toFixed(2) + '/h');
|
|
39
40
|
setText('center', (g[1].rpm * 60).toFixed(1) + '/h');
|
|
40
41
|
setText('third', (g[2].rpm * 60).toFixed(1) + '/h');
|
|
41
42
|
setText('fourth', g[3].rpm.toFixed(1) + ' rpm');
|
|
42
43
|
setText('escape', g[4].rpm.toFixed(0) + ' rpm');
|
|
43
|
-
const bphEl =
|
|
44
|
+
const bphEl = root.querySelector('#bph-pallet');
|
|
44
45
|
if (bphEl) bphEl.textContent = currentMov.pallet.bph.toString();
|
|
45
|
-
const hzEl =
|
|
46
|
+
const hzEl = root.querySelector('#hz-balance');
|
|
46
47
|
if (hzEl) hzEl.textContent = currentMov.balance.hz.toString();
|
|
47
|
-
const vphEl =
|
|
48
|
+
const vphEl = root.querySelector('#vph-balance');
|
|
48
49
|
if (vphEl) vphEl.textContent = currentMov.balance.vph.toString();
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -53,7 +54,7 @@ function render() {
|
|
|
53
54
|
setHovered(hoveredGear);
|
|
54
55
|
drawScene(currentMov, { angles, palletPhase, balancePhase, highlight: highlightedGear, hover: hoveredGear });
|
|
55
56
|
const step = highlightedGear !== null ? highlightedGear : -1;
|
|
56
|
-
|
|
57
|
+
root.querySelectorAll('.flow-bar .step').forEach((el, idx) => {
|
|
57
58
|
el.classList.toggle('active', idx <= step);
|
|
58
59
|
});
|
|
59
60
|
}
|
|
@@ -83,7 +84,7 @@ function switchMovement(id: string) {
|
|
|
83
84
|
balancePhase = 0;
|
|
84
85
|
highlightedGear = null;
|
|
85
86
|
updateStats();
|
|
86
|
-
|
|
87
|
+
root.querySelectorAll('[data-mov]').forEach((b) => {
|
|
87
88
|
b.classList.toggle('active', (b as HTMLElement).dataset.mov === id);
|
|
88
89
|
});
|
|
89
90
|
}
|
|
@@ -91,7 +92,7 @@ function switchMovement(id: string) {
|
|
|
91
92
|
function setSpeed(mult: number) {
|
|
92
93
|
speedMult = mult;
|
|
93
94
|
paused = mult === 0;
|
|
94
|
-
|
|
95
|
+
root.querySelectorAll('[data-spd]').forEach((b) => {
|
|
95
96
|
const spd = parseFloat((b as HTMLElement).dataset.spd || '1');
|
|
96
97
|
b.classList.toggle('active', spd === mult);
|
|
97
98
|
});
|
|
@@ -118,20 +119,20 @@ function onCanvasMove(e: MouseEvent) {
|
|
|
118
119
|
function highlightGear(idx: number | null) {
|
|
119
120
|
hoveredGear = idx;
|
|
120
121
|
highlightedGear = idx;
|
|
121
|
-
|
|
122
|
+
root.querySelectorAll('.data-card').forEach((c) => c.classList.remove('highlighted'));
|
|
122
123
|
if (idx !== null && idx < 7) {
|
|
123
|
-
|
|
124
|
+
root.querySelectorAll('.data-card')[idx]?.classList.add('highlighted');
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
function initControls() {
|
|
128
|
-
|
|
129
|
+
root.querySelectorAll('[data-mov]').forEach((b) => {
|
|
129
130
|
b.addEventListener('click', () => switchMovement((b as HTMLElement).dataset.mov || '2824'));
|
|
130
131
|
});
|
|
131
|
-
|
|
132
|
+
root.querySelectorAll('[data-spd]').forEach((b) => {
|
|
132
133
|
b.addEventListener('click', () => setSpeed(parseFloat((b as HTMLElement).dataset.spd || '1')));
|
|
133
134
|
});
|
|
134
|
-
|
|
135
|
+
root.querySelectorAll('.data-card').forEach((card, idx) => {
|
|
135
136
|
card.addEventListener('mouseenter', () => highlightGear(idx));
|
|
136
137
|
card.addEventListener('mouseleave', () => highlightGear(null));
|
|
137
138
|
});
|
|
@@ -10,7 +10,7 @@ const { ui } = Astro.props;
|
|
|
10
10
|
|
|
11
11
|
<link href="./gear-train-explorer.css" rel="stylesheet" />
|
|
12
12
|
|
|
13
|
-
<div class="
|
|
13
|
+
<div class="gear-train-card" data-ui={JSON.stringify(ui)}>
|
|
14
14
|
<GearPanel labels={ui} />
|
|
15
15
|
</div>
|
|
16
16
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
.
|
|
1
|
+
.gear-train-card {
|
|
2
2
|
background: var(--bg-surface);
|
|
3
3
|
border: 1px solid var(--border-color);
|
|
4
4
|
border-radius: 1.25rem;
|
|
@@ -10,32 +10,32 @@
|
|
|
10
10
|
box-shadow: var(--shadow-base);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
.canvas-wrapper {
|
|
13
|
+
.gear-train-card .canvas-wrapper {
|
|
14
14
|
background: radial-gradient(ellipse at center, #1a1a2e 0%, #0f0f1a 100%);
|
|
15
15
|
border-radius: 0.75rem;
|
|
16
16
|
overflow: hidden;
|
|
17
17
|
border: 1px solid rgba(212, 175, 55, 0.15);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
canvas#gear-canvas {
|
|
20
|
+
.gear-train-card canvas#gear-canvas {
|
|
21
21
|
display: block;
|
|
22
22
|
width: 100%;
|
|
23
23
|
height: auto;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
.controls-row {
|
|
26
|
+
.gear-train-card .controls-row {
|
|
27
27
|
display: flex;
|
|
28
28
|
flex-wrap: wrap;
|
|
29
29
|
gap: 0.75rem;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
.control-group {
|
|
32
|
+
.gear-train-card .control-group {
|
|
33
33
|
display: flex;
|
|
34
34
|
flex-direction: column;
|
|
35
35
|
gap: 0.2rem;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
.control-label {
|
|
38
|
+
.gear-train-card .control-label {
|
|
39
39
|
font-size: 0.6rem;
|
|
40
40
|
font-weight: 600;
|
|
41
41
|
text-transform: uppercase;
|
|
@@ -44,12 +44,12 @@ canvas#gear-canvas {
|
|
|
44
44
|
opacity: 0.5;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
.control-group .btn-group {
|
|
47
|
+
.gear-train-card .control-group .btn-group {
|
|
48
48
|
display: flex;
|
|
49
49
|
gap: 0.25rem;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
.control-group .btn-group button {
|
|
52
|
+
.gear-train-card .control-group .btn-group button {
|
|
53
53
|
padding: 0.25rem 0.55rem;
|
|
54
54
|
font-size: 0.7rem;
|
|
55
55
|
font-weight: 500;
|
|
@@ -61,25 +61,25 @@ canvas#gear-canvas {
|
|
|
61
61
|
transition: all 0.2s ease;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
.control-group .btn-group button:hover {
|
|
64
|
+
.gear-train-card .control-group .btn-group button:hover {
|
|
65
65
|
border-color: var(--accent);
|
|
66
66
|
color: var(--accent);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
.control-group .btn-group button.active {
|
|
69
|
+
.gear-train-card .control-group .btn-group button.active {
|
|
70
70
|
background: color-mix(in srgb, var(--accent) 10%, var(--bg-page));
|
|
71
71
|
border-color: var(--accent);
|
|
72
72
|
color: var(--accent);
|
|
73
73
|
box-shadow: 0 0 8px color-mix(in srgb, var(--accent) 12%, transparent);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
.data-grid {
|
|
76
|
+
.gear-train-card .data-grid {
|
|
77
77
|
display: grid;
|
|
78
78
|
grid-template-columns: repeat(auto-fill, minmax(95px, 1fr));
|
|
79
79
|
gap: 0.35rem;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
.data-card {
|
|
82
|
+
.gear-train-card .data-card {
|
|
83
83
|
background: var(--bg-page);
|
|
84
84
|
border: 1px solid var(--border-base);
|
|
85
85
|
border-radius: 0.5rem;
|
|
@@ -88,17 +88,17 @@ canvas#gear-canvas {
|
|
|
88
88
|
cursor: default;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
.data-card:hover {
|
|
91
|
+
.gear-train-card .data-card:hover {
|
|
92
92
|
border-color: var(--accent);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
.data-card.highlighted {
|
|
95
|
+
.gear-train-card .data-card.highlighted {
|
|
96
96
|
border-color: var(--accent);
|
|
97
97
|
background: color-mix(in srgb, var(--accent) 6%, var(--bg-page));
|
|
98
98
|
box-shadow: 0 0 10px color-mix(in srgb, var(--accent) 8%, transparent);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
.data-card .name {
|
|
101
|
+
.gear-train-card .data-card .name {
|
|
102
102
|
font-size: 0.65rem;
|
|
103
103
|
font-weight: 600;
|
|
104
104
|
text-transform: uppercase;
|
|
@@ -107,33 +107,33 @@ canvas#gear-canvas {
|
|
|
107
107
|
margin-bottom: 0.15rem;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
.data-card .stats {
|
|
110
|
+
.gear-train-card .data-card .stats {
|
|
111
111
|
display: flex;
|
|
112
112
|
gap: 0.35rem;
|
|
113
113
|
flex-wrap: wrap;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
.data-card .stats span {
|
|
116
|
+
.gear-train-card .data-card .stats span {
|
|
117
117
|
font-size: 0.65rem;
|
|
118
118
|
color: var(--text-base);
|
|
119
119
|
opacity: 0.75;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
.data-card .stats .val {
|
|
122
|
+
.gear-train-card .data-card .stats .val {
|
|
123
123
|
color: var(--accent);
|
|
124
124
|
font-weight: 600;
|
|
125
125
|
font-variant-numeric: tabular-nums;
|
|
126
126
|
opacity: 1;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
.flow-bar {
|
|
129
|
+
.gear-train-card .flow-bar {
|
|
130
130
|
background: var(--bg-page);
|
|
131
131
|
border: 1px solid var(--border-base);
|
|
132
132
|
border-radius: 0.5rem;
|
|
133
133
|
padding: 0.4rem 0.6rem;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
.flow-bar .flow-label {
|
|
136
|
+
.gear-train-card .flow-bar .flow-label {
|
|
137
137
|
font-size: 0.6rem;
|
|
138
138
|
font-weight: 600;
|
|
139
139
|
text-transform: uppercase;
|
|
@@ -143,14 +143,14 @@ canvas#gear-canvas {
|
|
|
143
143
|
margin-bottom: 0.2rem;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
.flow-bar .flow-path {
|
|
146
|
+
.gear-train-card .flow-bar .flow-path {
|
|
147
147
|
display: flex;
|
|
148
148
|
align-items: center;
|
|
149
149
|
gap: 0.2rem;
|
|
150
150
|
flex-wrap: wrap;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
.flow-bar .flow-path .step {
|
|
153
|
+
.gear-train-card .flow-bar .flow-path .step {
|
|
154
154
|
font-size: 0.65rem;
|
|
155
155
|
color: var(--text-base);
|
|
156
156
|
opacity: 0.4;
|
|
@@ -159,14 +159,39 @@ canvas#gear-canvas {
|
|
|
159
159
|
transition: all 0.25s ease;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
.flow-bar .flow-path .step.active {
|
|
162
|
+
.gear-train-card .flow-bar .flow-path .step.active {
|
|
163
163
|
opacity: 1;
|
|
164
164
|
color: var(--accent);
|
|
165
165
|
background: color-mix(in srgb, var(--accent) 8%, var(--bg-page));
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
.flow-bar .flow-path .arrow {
|
|
168
|
+
.gear-train-card .flow-bar .flow-path .arrow {
|
|
169
169
|
color: var(--text-base);
|
|
170
170
|
opacity: 0.2;
|
|
171
171
|
font-size: 0.7rem;
|
|
172
172
|
}
|
|
173
|
+
|
|
174
|
+
@media (max-width: 520px) {
|
|
175
|
+
.gear-train-card {
|
|
176
|
+
padding: 0.875rem;
|
|
177
|
+
max-width: 100%;
|
|
178
|
+
overflow: hidden;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.gear-train-card .canvas-wrapper {
|
|
182
|
+
width: 100%;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.gear-train-card .control-group,
|
|
186
|
+
.gear-train-card .control-group .btn-group {
|
|
187
|
+
width: 100%;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.gear-train-card .control-group .btn-group {
|
|
191
|
+
flex-wrap: wrap;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.gear-train-card .control-group .btn-group button {
|
|
195
|
+
flex: 1 1 auto;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -5,7 +5,7 @@ import { bibliography } from '../bibliography';
|
|
|
5
5
|
export const content: ToolLocaleContent<MainspringFinderUI> = {
|
|
6
6
|
slug: 'buscador-muelle-real',
|
|
7
7
|
title: 'Buscador de Muelle Real',
|
|
8
|
-
description: 'Calcule las dimensiones del muelle real a partir de las medidas del barrilete para movimientos de reloj antiguos.',
|
|
8
|
+
description: 'Calcule las dimensiones del muelle real a partir de las medidas del barrilete para movimientos de reloj antiguos con precisión, diámetro y árbol correctos.',
|
|
9
9
|
ui: {
|
|
10
10
|
title: 'Buscador de Muelle Real',
|
|
11
11
|
barrelLabel: 'DI del Barrilete',
|
|
@@ -90,23 +90,23 @@ export const content: ToolLocaleContent<MainspringFinderUI> = {
|
|
|
90
90
|
],
|
|
91
91
|
faq: [
|
|
92
92
|
{
|
|
93
|
-
question: 'Que precision tienen las dimensiones calculadas del muelle real?',
|
|
93
|
+
question: '¿Que precision tienen las dimensiones calculadas del muelle real?',
|
|
94
94
|
answer: 'Los calculos se basan en formulas estandar de relojeria que asumen una geometria ideal del barrilete. Las dimensiones reales pueden variar hasta un 5-10 % debido a las tolerancias de fabricacion, las configuraciones del extremo del muelle (anclaje, ojal o extremo en T) y la aleacion especifica utilizada. Utilice siempre los valores calculados como un punto de referencia solido, pero compare con las hojas de datos del fabricante o los catalogos de proveedores comerciales antes de hacer su pedido.',
|
|
95
95
|
},
|
|
96
96
|
{
|
|
97
|
-
question: 'Que sucede si no encuentro el tamano comercial exacto?',
|
|
97
|
+
question: '¿Que sucede si no encuentro el tamano comercial exacto?',
|
|
98
98
|
answer: 'Cuando el tamano exacto no esta disponible, priorice coincidir exactamente con la altura del muelle, luego el grosor dentro de 0.005 mm. La longitud se puede ajustar seleccionando un muelle diferente de la misma familia de altura y grosor. Un muelle ligeramente mas largo funcionara si el barrilete tiene suficiente espacio, pero un muelle mas corto reducira la reserva de marcha.',
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
|
-
question: 'Como mido las dimensiones del barrilete sin quitar el muelle real?',
|
|
101
|
+
question: '¿Como mido las dimensiones del barrilete sin quitar el muelle real?',
|
|
102
102
|
answer: 'Si el barrilete aun contiene el muelle viejo, puede medir el diametro exterior del barrilete desde fuera (luego reste el grosor de la pared, tipicamente 0.2-0.4 mm) y la altura total (luego reste el grosor de la tapa). Para obtener los resultados mas precisos, retire el muelle viejo y limpie el barrilete antes de medir.',
|
|
103
103
|
},
|
|
104
104
|
{
|
|
105
|
-
question: 'Cual es la diferencia entre un extremo de muelle de anclaje y uno de ojal?',
|
|
105
|
+
question: '¿Cual es la diferencia entre un extremo de muelle de anclaje y uno de ojal?',
|
|
106
106
|
answer: 'Un extremo de anclaje (tambien llamado extremo en T) tiene una pequena lengueta en forma de T que se engancha en la pared del barrilete. La mayoria de los calibres suizos y japoneses modernos usan este tipo. Un extremo de ojal tiene un pequeno agujero que se ajusta sobre una clavija en el arbol. Esta herramienta calcula solo las dimensiones de la cinta; debe verificar que el tipo de extremo coincida con su barrilete antes de hacer su pedido.',
|
|
107
107
|
},
|
|
108
108
|
{
|
|
109
|
-
question: 'Puedo usar esta herramienta para movimientos automaticos o cronografos?',
|
|
109
|
+
question: '¿Puedo usar esta herramienta para movimientos automaticos o cronografos?',
|
|
110
110
|
answer: 'Si, pero tenga en cuenta que los movimientos automaticos a menudo tienen un numero mayor de vueltas (8-10) y pueden requerir un muelle ligeramente mas delgado para acomodar el modulo de cuerda adicional. Los movimientos de cronografo tipicamente necesitan muelles mas gruesos para accionar el mecanismo del cronografo. Ajuste el valor de Vueltas en consecuencia y verifique contra las especificaciones del fabricante.',
|
|
111
111
|
},
|
|
112
112
|
],
|
|
@@ -5,7 +5,7 @@ import { bibliography } from '../bibliography';
|
|
|
5
5
|
export const content: ToolLocaleContent<MainspringFinderUI> = {
|
|
6
6
|
slug: 'trova-molla-reale',
|
|
7
7
|
title: 'Trova Molla Reale',
|
|
8
|
-
description: 'Calcola le dimensioni della molla reale a partire dalle misure del bariletto per movimenti di orologi vintage
|
|
8
|
+
description: 'Calcola le dimensioni della molla reale a partire dalle misure del bariletto per movimenti di orologi vintage; è utile per più verifiche di compatibilità.',
|
|
9
9
|
ui: {
|
|
10
10
|
title: 'Trova Molla Reale',
|
|
11
11
|
barrelLabel: 'Diametro interno bariletto',
|