@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-chrono",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -12,6 +12,7 @@ import { lumeColorSimulator } from '../tool/lume-color-simulator/entry';
12
12
  import { moonPhaseVisualizer } from '../tool/moon-phase-visualizer/entry';
13
13
  import { tachymeterCalculator } from '../tool/tachymeter-calculator/entry';
14
14
  import { serviceIntervalTracker } from '../tool/service-interval-tracker/entry';
15
+ import { strapLengthCalculator } from '../tool/strap-length-calculator/entry';
15
16
 
16
17
  export const chronoCategory: ChronoCategoryEntry = {
17
18
  icon: 'mdi:clock-outline',
@@ -29,6 +30,7 @@ export const chronoCategory: ChronoCategoryEntry = {
29
30
  moonPhaseVisualizer,
30
31
  tachymeterCalculator,
31
32
  serviceIntervalTracker,
33
+ strapLengthCalculator,
32
34
  ],
33
35
  i18n: {
34
36
  de: () => import('./i18n/de').then((m) => m.content),
package/src/entries.ts CHANGED
@@ -26,6 +26,8 @@ export { tachymeterCalculator } from './tool/tachymeter-calculator/entry';
26
26
  export type { TachymeterCalculatorUI, TachymeterCalculatorLocaleContent } from './tool/tachymeter-calculator/entry';
27
27
  export { serviceIntervalTracker } from './tool/service-interval-tracker/entry';
28
28
  export type { ServiceIntervalTrackerUI, ServiceIntervalTrackerLocaleContent } from './tool/service-interval-tracker/entry';
29
+ export { strapLengthCalculator } from './tool/strap-length-calculator/entry';
30
+ export type { StrapLengthCalculatorUI, StrapLengthCalculatorLocaleContent } from './tool/strap-length-calculator/entry';
29
31
  export { chronoCategory } from './category';
30
32
 
31
33
  import { watchAccuracyTracker } from './tool/watch-accuracy-tracker/entry';
@@ -42,6 +44,7 @@ import { lumeColorSimulator } from './tool/lume-color-simulator/entry';
42
44
  import { moonPhaseVisualizer } from './tool/moon-phase-visualizer/entry';
43
45
  import { tachymeterCalculator } from './tool/tachymeter-calculator/entry';
44
46
  import { serviceIntervalTracker } from './tool/service-interval-tracker/entry';
47
+ import { strapLengthCalculator } from './tool/strap-length-calculator/entry';
45
48
 
46
- export const ALL_ENTRIES = [watchAccuracyTracker, wristPresenceCalculator, demagnetizingTimer, watchSavingsPlanner, crownReferenceGuide, powerReserveEstimator, beatRateConverter, waterResistanceConverter, strapTaperCalculator, watchSizeComparator, lumeColorSimulator, moonPhaseVisualizer, tachymeterCalculator, serviceIntervalTracker];
49
+ export const ALL_ENTRIES = [watchAccuracyTracker, wristPresenceCalculator, demagnetizingTimer, watchSavingsPlanner, crownReferenceGuide, powerReserveEstimator, beatRateConverter, waterResistanceConverter, strapTaperCalculator, watchSizeComparator, lumeColorSimulator, moonPhaseVisualizer, tachymeterCalculator, serviceIntervalTracker, strapLengthCalculator];
47
50
 
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ export { lumeColorSimulator, LUME_COLOR_SIMULATOR_TOOL } from './tool/lume-color
14
14
  export { moonPhaseVisualizer, MOON_PHASE_VISUALIZER_TOOL } from './tool/moon-phase-visualizer';
15
15
  export { tachymeterCalculator, TACHYMETER_CALCULATOR_TOOL } from './tool/tachymeter-calculator';
16
16
  export { serviceIntervalTracker, SERVICE_INTERVAL_TRACKER_TOOL } from './tool/service-interval-tracker';
17
+ export { strapLengthCalculator, STRAP_LENGTH_CALCULATOR_TOOL } from './tool/strap-length-calculator';
17
18
 
18
19
  export type {
19
20
  KnownLocale,
@@ -22,7 +22,7 @@ describe('Locale Completeness Validation', () => {
22
22
  });
23
23
 
24
24
  it('all tools registered', () => {
25
- expect(ALL_TOOLS.length).toBe(14);
25
+ expect(ALL_TOOLS.length).toBe(15);
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(14);
8
+ expect(ALL_TOOLS.length).toBe(15);
9
9
  });
10
10
 
11
11
 
@@ -0,0 +1,16 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { strapLengthCalculator } 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 = strapLengthCalculator.i18n[locale] || strapLengthCalculator.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: 'Delugs Watch Strap Size Guide',
6
+ url: 'https://delugs.com/pages/strap-length-guide',
7
+ },
8
+ {
9
+ name: 'Strapcode Watch Band Size Guide',
10
+ url: 'https://www.strapcode.com/pages/watchband-size-guide',
11
+ },
12
+ ];
@@ -0,0 +1,151 @@
1
+ import { calculateStrap, toMillimeters, formatLength } from './helpers';
2
+ import { drawFlatLayout, drawWristProfile } from './drawing';
3
+
4
+ const container = document.querySelector('.str') as HTMLElement;
5
+ const UI = JSON.parse(container.dataset.ui || '{}');
6
+
7
+ const wristIn = document.getElementById('str-wrist') as HTMLInputElement;
8
+ const lugIn = document.getElementById('str-lug') as HTMLInputElement;
9
+ const fitIn = document.getElementById('str-fit') as HTMLSelectElement;
10
+ const unitIn = document.getElementById('str-unit') as HTMLSelectElement;
11
+ const wristUnitLabel = document.getElementById('str-wrist-unit-label') as HTMLElement;
12
+
13
+ const standardVal = document.getElementById('str-standard-val') as HTMLElement;
14
+ const standardDesc = document.getElementById('str-standard-desc') as HTMLElement;
15
+ const bespokeLong = document.getElementById('str-bespoke-long') as HTMLElement;
16
+ const bespokeShort = document.getElementById('str-bespoke-short') as HTMLElement;
17
+ const natoVal = document.getElementById('str-nato-val') as HTMLElement;
18
+
19
+ const svgFlat = document.getElementById('str-svg-flat') as unknown as SVGSVGElement;
20
+ const svgWrist = document.getElementById('str-svg-wrist') as unknown as SVGSVGElement;
21
+
22
+ const STORAGE_KEY = 'jjlmoya_chrono_strap_calc';
23
+
24
+ let currentUnit: 'mm' | 'in' = 'mm';
25
+
26
+ interface StrapState {
27
+ wrist: string;
28
+ lug: string;
29
+ fit: string;
30
+ unit: string;
31
+ }
32
+
33
+ function saveState(): void {
34
+ const state: StrapState = {
35
+ wrist: wristIn.value,
36
+ lug: lugIn.value,
37
+ fit: fitIn.value,
38
+ unit: unitIn.value,
39
+ };
40
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
41
+ }
42
+
43
+ function setUnitDisplay(unit: 'mm' | 'in'): void {
44
+ currentUnit = unit;
45
+ if (unit === 'in') {
46
+ wristIn.min = '4.5';
47
+ wristIn.max = '9.5';
48
+ wristIn.step = '0.05';
49
+ wristUnitLabel.textContent = UI.inches || 'in';
50
+ } else {
51
+ wristIn.min = '110';
52
+ wristIn.max = '240';
53
+ wristIn.step = '1';
54
+ wristUnitLabel.textContent = UI.millimeters || 'mm';
55
+ }
56
+ }
57
+
58
+ function loadState(): void {
59
+ let raw: string | null;
60
+ try {
61
+ raw = localStorage.getItem(STORAGE_KEY);
62
+ } catch {
63
+ return;
64
+ }
65
+ if (!raw) return;
66
+ let state: StrapState;
67
+ try {
68
+ state = JSON.parse(raw);
69
+ } catch {
70
+ return;
71
+ }
72
+
73
+ const fits = ['tight', 'regular', 'loose'] as const;
74
+ const units = ['mm', 'in'] as const;
75
+ if (state.wrist) wristIn.value = state.wrist;
76
+ if (state.lug) lugIn.value = state.lug;
77
+ if (fits.includes(state.fit as typeof fits[number])) fitIn.value = state.fit;
78
+ if (units.includes(state.unit as typeof units[number])) {
79
+ unitIn.value = state.unit;
80
+ setUnitDisplay(state.unit);
81
+ }
82
+ }
83
+
84
+ function update() {
85
+ const wrist = parseFloat(wristIn.value) || 170;
86
+ const lug = parseFloat(lugIn.value) || 45;
87
+ const fit = fitIn.value as 'tight' | 'regular' | 'loose';
88
+ const unit = unitIn.value as 'mm' | 'in';
89
+
90
+ const res = calculateStrap({ wrist, lug, fit, unit });
91
+
92
+ standardVal.textContent = res.standardText;
93
+ standardDesc.textContent = UI[`size${res.sizeKey.toUpperCase()}`] || res.sizeKey;
94
+ bespokeLong.textContent = formatLength(res.bespokeLong, unit);
95
+ bespokeShort.textContent = formatLength(res.bespokeShort, unit);
96
+ natoVal.textContent = res.natoText;
97
+
98
+ const wristMm = toMillimeters(wrist, unit);
99
+ let fitAllowance = 37;
100
+ if (fit === 'tight') {
101
+ fitAllowance = 32;
102
+ } else if (fit === 'loose') {
103
+ fitAllowance = 42;
104
+ }
105
+ const totalStrap = wristMm - lug + fitAllowance;
106
+
107
+ drawFlatLayout({
108
+ svg: svgFlat,
109
+ wrist: wristMm,
110
+ lug,
111
+ totalStrap,
112
+ bespokeLong: res.bespokeLong,
113
+ bespokeShort: res.bespokeShort,
114
+ });
115
+ drawWristProfile(svgWrist, wristMm, lug);
116
+
117
+ saveState();
118
+ }
119
+
120
+ function handleUnitChange() {
121
+ const nextUnit = unitIn.value as 'mm' | 'in';
122
+ if (nextUnit === currentUnit) {
123
+ return;
124
+ }
125
+
126
+ const prevVal = parseFloat(wristIn.value) || 170;
127
+ if (nextUnit === 'in') {
128
+ wristIn.min = '4.5';
129
+ wristIn.max = '9.5';
130
+ wristIn.step = '0.05';
131
+ wristIn.value = (prevVal / 25.4).toFixed(2);
132
+ wristUnitLabel.textContent = UI.inches || 'in';
133
+ } else {
134
+ wristIn.min = '110';
135
+ wristIn.max = '240';
136
+ wristIn.step = '1';
137
+ wristIn.value = Math.round(prevVal * 25.4).toString();
138
+ wristUnitLabel.textContent = UI.millimeters || 'mm';
139
+ }
140
+
141
+ currentUnit = nextUnit;
142
+ update();
143
+ }
144
+
145
+ wristIn.addEventListener('input', update);
146
+ lugIn.addEventListener('input', update);
147
+ fitIn.addEventListener('change', update);
148
+ unitIn.addEventListener('change', handleUnitChange);
149
+
150
+ loadState();
151
+ update();
@@ -0,0 +1,46 @@
1
+ ---
2
+ import CalculatorInputs from './components/CalculatorInputs.astro';
3
+ import Visualizer from './components/Visualizer.astro';
4
+ import BespokeResults from './components/BespokeResults.astro';
5
+
6
+ interface Props {
7
+ ui: Record<string, string>;
8
+ }
9
+
10
+ const { ui } = Astro.props;
11
+ ---
12
+
13
+ <div class="str" data-ui={JSON.stringify(ui)}>
14
+ <header class="str-top">
15
+ <h2 class="str-h">{ui.title || "Strap Length Calculator"}</h2>
16
+ </header>
17
+
18
+ <div class="str-body">
19
+ <CalculatorInputs
20
+ wristLabel={ui.wristLabel || "Wrist circumference"}
21
+ wristPlaceholder={ui.wristPlaceholder || "e.g. 170"}
22
+ lugLabel={ui.lugLabel || "Watch lug-to-lug"}
23
+ lugPlaceholder={ui.lugPlaceholder || "e.g. 45"}
24
+ fitLabel={ui.fitLabel || "Preferred fit"}
25
+ fitTight={ui.fitTight || "Tight"}
26
+ fitRegular={ui.fitRegular || "Regular"}
27
+ fitLoose={ui.fitLoose || "Loose"}
28
+ unitLabel={ui.unitLabel || "Unit"}
29
+ millimeters={ui.millimeters || "mm"}
30
+ inches={ui.inches || "in"}
31
+ />
32
+
33
+ <Visualizer />
34
+
35
+ <BespokeResults
36
+ standardLabel={ui.standardLabel || "Standard retail size"}
37
+ bespokeLabel={ui.bespokeLabel || "Bespoke custom size"}
38
+ natoLabel={ui.natoLabel || "NATO strap length"}
39
+ longSide={ui.longSide || "Long side"}
40
+ shortSide={ui.shortSide || "Short side"}
41
+ totalLength={ui.totalLength || "Total length"}
42
+ />
43
+ </div>
44
+ </div>
45
+
46
+ <script src="./client.ts"></script>
@@ -0,0 +1,60 @@
1
+ ---
2
+ interface Props {
3
+ standardLabel: string;
4
+ bespokeLabel: string;
5
+ natoLabel: string;
6
+ longSide: string;
7
+ shortSide: string;
8
+ totalLength: string;
9
+ }
10
+
11
+ const {
12
+ standardLabel,
13
+ bespokeLabel,
14
+ natoLabel,
15
+ longSide,
16
+ shortSide,
17
+ } = Astro.props;
18
+ ---
19
+
20
+ <div class="str-results">
21
+ <div class="str-result-card">
22
+ <div class="str-result-icon">
23
+ <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>
24
+ </div>
25
+ <div class="str-result-info">
26
+ <span class="str-result-title">{standardLabel}</span>
27
+ <span class="str-result-value" id="str-standard-val">—</span>
28
+ <span class="str-result-desc" id="str-standard-desc">—</span>
29
+ </div>
30
+ </div>
31
+
32
+ <div class="str-result-card str-result-bespoke">
33
+ <div class="str-result-icon">
34
+ <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
35
+ </div>
36
+ <div class="str-result-info">
37
+ <span class="str-result-title">{bespokeLabel}</span>
38
+ <div class="str-result-split-values">
39
+ <div class="str-split-item">
40
+ <span class="str-split-lbl">{longSide}</span>
41
+ <span class="str-split-val" id="str-bespoke-long">—</span>
42
+ </div>
43
+ <div class="str-split-item">
44
+ <span class="str-split-lbl">{shortSide}</span>
45
+ <span class="str-split-val" id="str-bespoke-short">—</span>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ </div>
50
+
51
+ <div class="str-result-card">
52
+ <div class="str-result-icon">
53
+ <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line></svg>
54
+ </div>
55
+ <div class="str-result-info">
56
+ <span class="str-result-title">{natoLabel}</span>
57
+ <span class="str-result-value" id="str-nato-val">—</span>
58
+ </div>
59
+ </div>
60
+ </div>
@@ -0,0 +1,64 @@
1
+ ---
2
+ interface Props {
3
+ wristLabel: string;
4
+ wristPlaceholder: string;
5
+ lugLabel: string;
6
+ lugPlaceholder: string;
7
+ fitLabel: string;
8
+ fitTight: string;
9
+ fitRegular: string;
10
+ fitLoose: string;
11
+ unitLabel: string;
12
+ millimeters: string;
13
+ inches: string;
14
+ }
15
+
16
+ const {
17
+ wristLabel,
18
+ wristPlaceholder,
19
+ lugLabel,
20
+ lugPlaceholder,
21
+ fitLabel,
22
+ fitTight,
23
+ fitRegular,
24
+ fitLoose,
25
+ unitLabel,
26
+ millimeters,
27
+ inches,
28
+ } = Astro.props;
29
+ ---
30
+
31
+ <div class="str-inputs">
32
+ <div class="str-field">
33
+ <label class="str-label" for="str-wrist">{wristLabel}</label>
34
+ <div class="str-input-wrapper">
35
+ <input class="str-input" id="str-wrist" type="number" min="100" max="300" step="1" value="170" placeholder={wristPlaceholder} />
36
+ <span class="str-input-unit" id="str-wrist-unit-label">{millimeters}</span>
37
+ </div>
38
+ </div>
39
+
40
+ <div class="str-field">
41
+ <label class="str-label" for="str-lug">{lugLabel}</label>
42
+ <div class="str-input-wrapper">
43
+ <input class="str-input" id="str-lug" type="number" min="20" max="70" step="1" value="45" placeholder={lugPlaceholder} />
44
+ <span class="str-input-unit">{millimeters}</span>
45
+ </div>
46
+ </div>
47
+
48
+ <div class="str-field">
49
+ <label class="str-label" for="str-fit">{fitLabel}</label>
50
+ <select class="str-input str-select" id="str-fit">
51
+ <option value="tight">{fitTight}</option>
52
+ <option value="regular" selected>{fitRegular}</option>
53
+ <option value="loose">{fitLoose}</option>
54
+ </select>
55
+ </div>
56
+
57
+ <div class="str-field">
58
+ <label class="str-label" for="str-unit">{unitLabel}</label>
59
+ <select class="str-input str-select" id="str-unit">
60
+ <option value="mm" selected>{millimeters}</option>
61
+ <option value="in">{inches}</option>
62
+ </select>
63
+ </div>
64
+ </div>
@@ -0,0 +1,8 @@
1
+ <div class="str-visualizer-container">
2
+ <div class="str-vis-block">
3
+ <svg id="str-svg-flat" viewBox="0 0 600 120" width="100%" height="auto" class="str-svg"></svg>
4
+ </div>
5
+ <div class="str-vis-block">
6
+ <svg id="str-svg-wrist" viewBox="0 0 300 200" width="100%" height="auto" class="str-svg"></svg>
7
+ </div>
8
+ </div>