@jjlmoya/utils-chrono 1.6.0 → 1.7.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 (35) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -0
  3. package/src/entries.ts +4 -1
  4. package/src/index.ts +1 -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/strap-length-calculator/bibliography.astro +16 -0
  8. package/src/tool/strap-length-calculator/bibliography.ts +12 -0
  9. package/src/tool/strap-length-calculator/client.ts +151 -0
  10. package/src/tool/strap-length-calculator/component.astro +46 -0
  11. package/src/tool/strap-length-calculator/components/BespokeResults.astro +60 -0
  12. package/src/tool/strap-length-calculator/components/CalculatorInputs.astro +64 -0
  13. package/src/tool/strap-length-calculator/components/Visualizer.astro +8 -0
  14. package/src/tool/strap-length-calculator/drawing.ts +265 -0
  15. package/src/tool/strap-length-calculator/entry.ts +52 -0
  16. package/src/tool/strap-length-calculator/helpers.ts +110 -0
  17. package/src/tool/strap-length-calculator/i18n/de.ts +311 -0
  18. package/src/tool/strap-length-calculator/i18n/en.ts +311 -0
  19. package/src/tool/strap-length-calculator/i18n/es.ts +311 -0
  20. package/src/tool/strap-length-calculator/i18n/fr.ts +311 -0
  21. package/src/tool/strap-length-calculator/i18n/id.ts +86 -0
  22. package/src/tool/strap-length-calculator/i18n/it.ts +311 -0
  23. package/src/tool/strap-length-calculator/i18n/ja.ts +86 -0
  24. package/src/tool/strap-length-calculator/i18n/ko.ts +86 -0
  25. package/src/tool/strap-length-calculator/i18n/nl.ts +311 -0
  26. package/src/tool/strap-length-calculator/i18n/pl.ts +311 -0
  27. package/src/tool/strap-length-calculator/i18n/pt.ts +311 -0
  28. package/src/tool/strap-length-calculator/i18n/ru.ts +86 -0
  29. package/src/tool/strap-length-calculator/i18n/sv.ts +86 -0
  30. package/src/tool/strap-length-calculator/i18n/tr.ts +86 -0
  31. package/src/tool/strap-length-calculator/i18n/zh.ts +86 -0
  32. package/src/tool/strap-length-calculator/index.ts +11 -0
  33. package/src/tool/strap-length-calculator/seo.astro +16 -0
  34. package/src/tool/strap-length-calculator/strap-length-calculator.css +234 -0
  35. package/src/tools.ts +2 -0
@@ -0,0 +1,265 @@
1
+ function createSvgElement(tag: string, attrs: Record<string, string>): SVGElement {
2
+ const el = document.createElementNS('http://www.w3.org/2000/svg', tag);
3
+ Object.entries(attrs).forEach(([k, v]) => el.setAttribute(k, v));
4
+ return el;
5
+ }
6
+
7
+ export interface FlatLayoutParams {
8
+ svg: SVGSVGElement;
9
+ wrist: number;
10
+ lug: number;
11
+ totalStrap: number;
12
+ bespokeLong: number;
13
+ bespokeShort: number;
14
+ }
15
+
16
+ function drawShortStrap(
17
+ svg: SVGSVGElement,
18
+ startX: number,
19
+ centerY: number,
20
+ shortWidth: number,
21
+ ): void {
22
+ svg.appendChild(createSvgElement('rect', {
23
+ x: startX.toString(),
24
+ y: (centerY - 12).toString(),
25
+ width: shortWidth.toString(),
26
+ height: '24',
27
+ rx: '4',
28
+ fill: 'rgba(160, 110, 80, 0.85)',
29
+ stroke: 'rgba(160, 110, 80, 0.95)',
30
+ 'stroke-width': '1.5',
31
+ }));
32
+ svg.appendChild(createSvgElement('rect', {
33
+ x: (startX - 14).toString(),
34
+ y: (centerY - 16).toString(),
35
+ width: '14',
36
+ height: '32',
37
+ rx: '3',
38
+ fill: 'var(--border-color, #ccc)',
39
+ stroke: 'var(--text-base, #888)',
40
+ 'stroke-width': '1.5',
41
+ }));
42
+ }
43
+
44
+ function drawWatchCase(
45
+ svg: SVGSVGElement,
46
+ watchX: number,
47
+ centerY: number,
48
+ watchWidth: number,
49
+ ): void {
50
+ svg.appendChild(createSvgElement('rect', {
51
+ x: watchX.toString(),
52
+ y: (centerY - 22).toString(),
53
+ width: watchWidth.toString(),
54
+ height: '44',
55
+ rx: '22',
56
+ fill: 'rgba(230, 230, 235, 0.15)',
57
+ stroke: 'var(--border-color, #bbb)',
58
+ 'stroke-width': '2',
59
+ }));
60
+ svg.appendChild(createSvgElement('circle', {
61
+ cx: (watchX + watchWidth / 2).toString(),
62
+ cy: centerY.toString(),
63
+ r: '16',
64
+ fill: 'rgba(255, 255, 255, 0.05)',
65
+ stroke: 'var(--border-color, #bbb)',
66
+ 'stroke-width': '1.5',
67
+ }));
68
+ }
69
+
70
+ function drawLongStrap(
71
+ svg: SVGSVGElement,
72
+ longX: number,
73
+ centerY: number,
74
+ longWidth: number,
75
+ ): void {
76
+ svg.appendChild(createSvgElement('rect', {
77
+ x: longX.toString(),
78
+ y: (centerY - 12).toString(),
79
+ width: longWidth.toString(),
80
+ height: '24',
81
+ rx: '4',
82
+ fill: 'rgba(160, 110, 80, 0.85)',
83
+ stroke: 'rgba(160, 110, 80, 0.95)',
84
+ 'stroke-width': '1.5',
85
+ }));
86
+ }
87
+
88
+ function drawStrapHoles(
89
+ svg: SVGSVGElement,
90
+ longX: number,
91
+ centerY: number,
92
+ holeParams: { scale: number; longWidth: number },
93
+ ): void {
94
+ const holeCount = 7;
95
+ const holeSpacing = 6.5 * holeParams.scale;
96
+ const firstHoleOffset = 35 * holeParams.scale;
97
+ for (let i = 0; i < holeCount; i++) {
98
+ const hx = longX + firstHoleOffset + (i * holeSpacing);
99
+ if (hx < longX + holeParams.longWidth - 8) {
100
+ svg.appendChild(createSvgElement('circle', {
101
+ cx: hx.toString(),
102
+ cy: centerY.toString(),
103
+ r: '2',
104
+ fill: 'rgba(0, 0, 0, 0.4)',
105
+ }));
106
+ }
107
+ }
108
+ }
109
+
110
+ function drawPinIndicator(
111
+ svg: SVGSVGElement,
112
+ pinX: number,
113
+ label: string,
114
+ ): void {
115
+ svg.appendChild(createSvgElement('line', {
116
+ x1: pinX.toString(),
117
+ y1: '15',
118
+ x2: pinX.toString(),
119
+ y2: '105',
120
+ stroke: 'var(--accent, #f43f5e)',
121
+ 'stroke-width': '2',
122
+ 'stroke-dasharray': '4 3',
123
+ }));
124
+ const pinLabel = createSvgElement('text', {
125
+ x: pinX.toString(),
126
+ y: '10',
127
+ fill: 'var(--accent, #f43f5e)',
128
+ 'font-size': '10',
129
+ 'font-weight': '700',
130
+ 'text-anchor': 'middle',
131
+ });
132
+ pinLabel.textContent = label;
133
+ svg.appendChild(pinLabel);
134
+ }
135
+
136
+ export function drawFlatLayout(params: FlatLayoutParams): void {
137
+ const { svg, lug, bespokeLong, bespokeShort, totalStrap } = params;
138
+ svg.innerHTML = '';
139
+
140
+ const scale = 2.2;
141
+ const centerY = 60;
142
+ const watchWidth = lug * scale;
143
+ const shortWidth = bespokeShort * scale;
144
+ const longWidth = bespokeLong * scale;
145
+
146
+ const startX = 300 - (watchWidth / 2) - shortWidth;
147
+ const watchX = startX + shortWidth;
148
+ const longX = watchX + watchWidth;
149
+
150
+ drawShortStrap(svg, startX, centerY, shortWidth);
151
+ drawWatchCase(svg, watchX, centerY, watchWidth);
152
+ drawLongStrap(svg, longX, centerY, longWidth);
153
+ drawStrapHoles(svg, longX, centerY, { scale, longWidth });
154
+
155
+ const pinDistance = totalStrap - bespokeShort;
156
+ const pinX = longX + (pinDistance * scale);
157
+ drawPinIndicator(svg, pinX, 'Active Fit');
158
+ }
159
+
160
+ function drawWristBackground(
161
+ svg: SVGSVGElement,
162
+ cx: number,
163
+ cy: number,
164
+ dims: { rx: number; ry: number },
165
+ ): void {
166
+ svg.appendChild(createSvgElement('ellipse', {
167
+ cx: cx.toString(),
168
+ cy: cy.toString(),
169
+ rx: dims.rx.toString(),
170
+ ry: dims.ry.toString(),
171
+ fill: 'rgba(230, 230, 235, 0.05)',
172
+ stroke: 'var(--border-color, #ccc)',
173
+ 'stroke-width': '1.5',
174
+ 'stroke-dasharray': '4 4',
175
+ }));
176
+ }
177
+
178
+ function drawWristWatchHead(
179
+ svg: SVGSVGElement,
180
+ cx: number,
181
+ watchParams: { watchX: number; watchY: number; watchWidth: number },
182
+ ): void {
183
+ const { watchX, watchY, watchWidth } = watchParams;
184
+ svg.appendChild(createSvgElement('rect', {
185
+ x: watchX.toString(),
186
+ y: (watchY - 6).toString(),
187
+ width: watchWidth.toString(),
188
+ height: '12',
189
+ rx: '3',
190
+ fill: 'rgba(230, 230, 235, 0.15)',
191
+ stroke: 'var(--border-color, #bbb)',
192
+ 'stroke-width': '2',
193
+ }));
194
+ svg.appendChild(createSvgElement('ellipse', {
195
+ cx: cx.toString(),
196
+ cy: (watchY - 6).toString(),
197
+ rx: (watchWidth * 0.35).toString(),
198
+ ry: '5',
199
+ fill: 'rgba(255, 255, 255, 0.05)',
200
+ stroke: 'var(--border-color, #bbb)',
201
+ 'stroke-width': '1.5',
202
+ }));
203
+ }
204
+
205
+ function drawWristStraps(
206
+ svg: SVGSVGElement,
207
+ cx: number,
208
+ cy: number,
209
+ strapParams: { watchX: number; watchY: number; watchWidth: number; rx: number; ry: number },
210
+ ): void {
211
+ const { watchX, watchY, watchWidth, rx, ry } = strapParams;
212
+ svg.appendChild(createSvgElement('path', {
213
+ d: `M ${watchX} ${watchY} Q ${cx - rx - 10} ${cy - 10} ${cx} ${cy + ry}`,
214
+ fill: 'none',
215
+ stroke: 'rgba(160, 110, 80, 0.85)',
216
+ 'stroke-width': '4',
217
+ }));
218
+ svg.appendChild(createSvgElement('path', {
219
+ d: `M ${watchX + watchWidth} ${watchY} Q ${cx + rx + 10} ${cy - 10} ${cx} ${cy + ry}`,
220
+ fill: 'none',
221
+ stroke: 'rgba(160, 110, 80, 0.85)',
222
+ 'stroke-width': '4',
223
+ }));
224
+ }
225
+
226
+ function drawWristBuckle(
227
+ svg: SVGSVGElement,
228
+ cx: number,
229
+ cy: number,
230
+ ry: number,
231
+ ): void {
232
+ svg.appendChild(createSvgElement('rect', {
233
+ x: (cx - 6).toString(),
234
+ y: (cy + ry - 4).toString(),
235
+ width: '12',
236
+ height: '8',
237
+ rx: '1.5',
238
+ fill: 'var(--border-color, #ccc)',
239
+ stroke: 'var(--text-base, #888)',
240
+ 'stroke-width': '1',
241
+ }));
242
+ }
243
+
244
+ export function drawWristProfile(
245
+ svg: SVGSVGElement,
246
+ wrist: number,
247
+ lug: number,
248
+ ): void {
249
+ svg.innerHTML = '';
250
+
251
+ const cx = 150;
252
+ const cy = 100;
253
+ const rx = 85;
254
+ const ry = 52;
255
+
256
+ drawWristBackground(svg, cx, cy, { rx, ry });
257
+
258
+ const watchY = cy - ry;
259
+ const watchWidth = Math.min(80, lug * 1.5);
260
+ const watchX = cx - watchWidth / 2;
261
+
262
+ drawWristWatchHead(svg, cx, { watchX, watchY, watchWidth });
263
+ drawWristStraps(svg, cx, cy, { watchX, watchY, watchWidth, rx, ry });
264
+ drawWristBuckle(svg, cx, cy, ry);
265
+ }
@@ -0,0 +1,52 @@
1
+ import type { ChronoToolEntry, ToolLocaleContent } from '../../types';
2
+
3
+ export type StrapLengthCalculatorUI = {
4
+ title: string;
5
+ wristLabel: string;
6
+ wristPlaceholder: string;
7
+ lugLabel: string;
8
+ lugPlaceholder: string;
9
+ fitLabel: string;
10
+ fitTight: string;
11
+ fitRegular: string;
12
+ fitLoose: string;
13
+ unitLabel: string;
14
+ standardLabel: string;
15
+ bespokeLabel: string;
16
+ natoLabel: string;
17
+ longSide: string;
18
+ shortSide: string;
19
+ totalLength: string;
20
+ millimeters: string;
21
+ inches: string;
22
+ sizeXS: string;
23
+ sizeS: string;
24
+ sizeM: string;
25
+ sizeL: string;
26
+ sizeXL: string;
27
+ sizeXXL: string;
28
+ };
29
+
30
+ export type StrapLengthCalculatorLocaleContent = ToolLocaleContent<StrapLengthCalculatorUI>;
31
+
32
+ export const strapLengthCalculator: ChronoToolEntry<StrapLengthCalculatorUI> = {
33
+ id: 'strap-length-calculator',
34
+ icons: { bg: 'mdi:ruler', fg: 'mdi:tape-measure' },
35
+ i18n: {
36
+ en: () => import('./i18n/en').then((m) => m.content),
37
+ de: () => import('./i18n/de').then((m) => m.content),
38
+ es: () => import('./i18n/es').then((m) => m.content),
39
+ fr: () => import('./i18n/fr').then((m) => m.content),
40
+ id: () => import('./i18n/id').then((m) => m.content),
41
+ it: () => import('./i18n/it').then((m) => m.content),
42
+ ja: () => import('./i18n/ja').then((m) => m.content),
43
+ ko: () => import('./i18n/ko').then((m) => m.content),
44
+ nl: () => import('./i18n/nl').then((m) => m.content),
45
+ pl: () => import('./i18n/pl').then((m) => m.content),
46
+ pt: () => import('./i18n/pt').then((m) => m.content),
47
+ ru: () => import('./i18n/ru').then((m) => m.content),
48
+ sv: () => import('./i18n/sv').then((m) => m.content),
49
+ tr: () => import('./i18n/tr').then((m) => m.content),
50
+ zh: () => import('./i18n/zh').then((m) => m.content),
51
+ },
52
+ };
@@ -0,0 +1,110 @@
1
+ export interface StrapCalculationInput {
2
+ wrist: number;
3
+ lug: number;
4
+ fit: 'tight' | 'regular' | 'loose';
5
+ unit: 'mm' | 'in';
6
+ }
7
+
8
+ export interface StrapCalculationResult {
9
+ standardText: string;
10
+ sizeKey: string;
11
+ bespokeLong: number;
12
+ bespokeShort: number;
13
+ natoText: string;
14
+ }
15
+
16
+ export function toMillimeters(val: number, unit: 'mm' | 'in'): number {
17
+ if (unit === 'in') {
18
+ return Math.round(val * 25.4);
19
+ }
20
+ return val;
21
+ }
22
+
23
+ export function formatLength(val: number, unit: 'mm' | 'in'): string {
24
+ if (unit === 'in') {
25
+ const inches = val / 25.4;
26
+ return `${inches.toFixed(2)} in`;
27
+ }
28
+ return `${Math.round(val)} mm`;
29
+ }
30
+
31
+ interface StandardSize {
32
+ long: number;
33
+ short: number;
34
+ sizeKey: string;
35
+ }
36
+
37
+ function getStandardSizing(wristMm: number): StandardSize {
38
+ if (wristMm < 152) {
39
+ return { long: 105, short: 70, sizeKey: 'xs' };
40
+ }
41
+ if (wristMm < 165) {
42
+ return { long: 110, short: 70, sizeKey: 's' };
43
+ }
44
+ if (wristMm < 178) {
45
+ return { long: 115, short: 75, sizeKey: 'm' };
46
+ }
47
+ if (wristMm < 190) {
48
+ return { long: 120, short: 80, sizeKey: 'l' };
49
+ }
50
+ if (wristMm < 203) {
51
+ return { long: 125, short: 85, sizeKey: 'xl' };
52
+ }
53
+ return { long: 130, short: 90, sizeKey: 'xxl' };
54
+ }
55
+
56
+ function getFitAllowance(fit: 'tight' | 'regular' | 'loose'): number {
57
+ if (fit === 'tight') {
58
+ return 32;
59
+ }
60
+ if (fit === 'loose') {
61
+ return 42;
62
+ }
63
+ return 37;
64
+ }
65
+
66
+ function formatStandardText(long: number, short: number, unit: 'mm' | 'in'): string {
67
+ if (unit === 'in') {
68
+ const lIn = long / 25.4;
69
+ const sIn = short / 25.4;
70
+ return `${lIn.toFixed(2)} / ${sIn.toFixed(2)} in`;
71
+ }
72
+ return `${long} / ${short} mm`;
73
+ }
74
+
75
+ function getNatoLength(wristMm: number): number {
76
+ if (wristMm < 160) {
77
+ return 260;
78
+ }
79
+ if (wristMm >= 190) {
80
+ return 300;
81
+ }
82
+ return 280;
83
+ }
84
+
85
+ function formatNatoText(len: number, unit: 'mm' | 'in'): string {
86
+ if (unit === 'in') {
87
+ const lenIn = len / 25.4;
88
+ return `${lenIn.toFixed(2)} in`;
89
+ }
90
+ return `${len} mm`;
91
+ }
92
+
93
+ export function calculateStrap(input: StrapCalculationInput): StrapCalculationResult {
94
+ const wristMm = toMillimeters(input.wrist, input.unit);
95
+ const lugMm = input.lug;
96
+ const fitAllowance = getFitAllowance(input.fit);
97
+ const std = getStandardSizing(wristMm);
98
+ const totalStrap = wristMm - lugMm + fitAllowance;
99
+ const bespokeShort = Math.round(totalStrap * 0.38);
100
+ const bespokeLong = totalStrap - bespokeShort;
101
+ const natoLen = getNatoLength(wristMm);
102
+
103
+ return {
104
+ standardText: formatStandardText(std.long, std.short, input.unit),
105
+ sizeKey: std.sizeKey,
106
+ bespokeLong,
107
+ bespokeShort,
108
+ natoText: formatNatoText(natoLen, input.unit),
109
+ };
110
+ }