@jjlmoya/utils-home 1.27.0 → 1.29.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 (31) hide show
  1. package/package.json +1 -1
  2. package/src/entries.ts +4 -1
  3. package/src/index.ts +2 -0
  4. package/src/tests/locale_completeness.test.ts +2 -2
  5. package/src/tests/tool_validation.test.ts +2 -2
  6. package/src/tool/deskErgonomics/bibliography.astro +14 -0
  7. package/src/tool/deskErgonomics/bibliography.ts +5 -0
  8. package/src/tool/deskErgonomics/component.astro +192 -0
  9. package/src/tool/deskErgonomics/desk-ergonomics.css +361 -0
  10. package/src/tool/deskErgonomics/entry.ts +29 -0
  11. package/src/tool/deskErgonomics/i18n/de.ts +237 -0
  12. package/src/tool/deskErgonomics/i18n/en.ts +215 -0
  13. package/src/tool/deskErgonomics/i18n/es.ts +237 -0
  14. package/src/tool/deskErgonomics/i18n/fr.ts +237 -0
  15. package/src/tool/deskErgonomics/i18n/id.ts +215 -0
  16. package/src/tool/deskErgonomics/i18n/it.ts +237 -0
  17. package/src/tool/deskErgonomics/i18n/ja.ts +215 -0
  18. package/src/tool/deskErgonomics/i18n/ko.ts +215 -0
  19. package/src/tool/deskErgonomics/i18n/nl.ts +215 -0
  20. package/src/tool/deskErgonomics/i18n/pl.ts +215 -0
  21. package/src/tool/deskErgonomics/i18n/pt.ts +237 -0
  22. package/src/tool/deskErgonomics/i18n/ru.ts +215 -0
  23. package/src/tool/deskErgonomics/i18n/sv.ts +215 -0
  24. package/src/tool/deskErgonomics/i18n/tr.ts +215 -0
  25. package/src/tool/deskErgonomics/i18n/zh.ts +215 -0
  26. package/src/tool/deskErgonomics/index.ts +9 -0
  27. package/src/tool/deskErgonomics/logic.ts +49 -0
  28. package/src/tool/deskErgonomics/seo.astro +15 -0
  29. package/src/tool/deskErgonomics/ui.ts +28 -0
  30. package/src/tool/vampireDrawSimulator/bibliography.ts +3 -3
  31. package/src/tools.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-home",
3
- "version": "1.27.0",
3
+ "version": "1.29.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
package/src/entries.ts CHANGED
@@ -20,6 +20,8 @@ export { wallPaintingCalculator } from './tool/wallPaintingCalculator/entry';
20
20
  export type { WallPaintingCalculatorLocaleContent } from './tool/wallPaintingCalculator/entry';
21
21
  export { vampireDrawSimulator } from './tool/vampireDrawSimulator/entry';
22
22
  export type { VampireDrawSimulatorLocaleContent } from './tool/vampireDrawSimulator/entry';
23
+ export { deskErgonomics } from './tool/deskErgonomics/entry';
24
+ export type { DeskErgonomicsLocaleContent } from './tool/deskErgonomics/entry';
23
25
  export { homeCategory } from './category';
24
26
  import { dewPointCalculator } from './tool/dewPointCalculator/entry';
25
27
  import { heatingComparator } from './tool/heatingComparator/entry';
@@ -32,4 +34,5 @@ import { wifiRangeSimulator } from './tool/wifiRangeSimulator/entry';
32
34
  import { acTonnageCalculator } from './tool/acTonnageCalculator/entry';
33
35
  import { wallPaintingCalculator } from './tool/wallPaintingCalculator/entry';
34
36
  import { vampireDrawSimulator } from './tool/vampireDrawSimulator/entry';
35
- export const ALL_ENTRIES = [dewPointCalculator, heatingComparator, ledSavingCalculator, projectorCalculator, qrGenerator, solarCalculator, tariffComparator, wifiRangeSimulator, acTonnageCalculator, wallPaintingCalculator, vampireDrawSimulator];
37
+ import { deskErgonomics } from './tool/deskErgonomics/entry';
38
+ export const ALL_ENTRIES = [dewPointCalculator, heatingComparator, ledSavingCalculator, projectorCalculator, qrGenerator, solarCalculator, tariffComparator, wifiRangeSimulator, acTonnageCalculator, wallPaintingCalculator, vampireDrawSimulator, deskErgonomics];
package/src/index.ts CHANGED
@@ -25,4 +25,6 @@ export { LED_SAVING_CALCULATOR_TOOL } from './tool/ledSavingCalculator';
25
25
  export { TARIFF_COMPARATOR_TOOL } from './tool/tariffComparator';
26
26
  export { HEATING_COMPARATOR_TOOL } from './tool/heatingComparator';
27
27
  export { WALL_PAINTING_CALCULATOR_TOOL } from './tool/wallPaintingCalculator';
28
+ export { VAMPIRE_DRAW_SIMULATOR_TOOL } from './tool/vampireDrawSimulator';
29
+ export { DESK_ERGONOMICS_TOOL } from './tool/deskErgonomics';
28
30
 
@@ -17,8 +17,8 @@ describe('Locale Completeness Validation', () => {
17
17
  });
18
18
  });
19
19
 
20
- it('should have 11 tools registered', () => {
21
- expect(ALL_TOOLS.length).toBe(11);
20
+ it('should have 12 tools registered', () => {
21
+ expect(ALL_TOOLS.length).toBe(12);
22
22
  });
23
23
  });
24
24
 
@@ -4,8 +4,8 @@ import { homeCategory } from '../data';
4
4
 
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
- it('should have 11 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(11);
7
+ it('should have 12 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(12);
9
9
  });
10
10
 
11
11
  it('homeCategory should be defined', () => {
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { deskErgonomics } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const content = await deskErgonomics.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,5 @@
1
+ export const bibliography = [
2
+ { name: 'OSHA Computer Workstation eTool', url: 'https://www.osha.gov/etools/computer-workstations' },
3
+ { name: 'Cornell University Ergonomics Web', url: 'https://ergo.human.cornell.edu/' },
4
+ { name: 'ISO 9241-5 Ergonomic Requirements for Office Work', url: 'https://www.iso.org/obp/ui/#iso:std:iso:9241:-5:ed-1:v1:en' },
5
+ ];
@@ -0,0 +1,192 @@
1
+ ---
2
+ import type { DeskErgonomicsUI } from './ui';
3
+ import { calculateErgonomics, fmt0 } from './logic';
4
+
5
+ interface Props {
6
+ ui?: Record<string, unknown>;
7
+ }
8
+
9
+ const { ui = {} } = Astro.props;
10
+ const eUI = ui as DeskErgonomicsUI;
11
+
12
+ const defaultHeight = 175;
13
+ const initial = calculateErgonomics(defaultHeight, 'male', 'sit');
14
+
15
+ const maxVal = 140;
16
+ const chairPct = Math.min((initial.chairHeight / maxVal) * 100, 100);
17
+ const deskPct = Math.min((initial.deskHeight / maxVal) * 100, 100);
18
+ const monPct = Math.min((initial.monitorHeight / maxVal) * 100, 100);
19
+ ---
20
+
21
+ <div class="ergo-wrapper">
22
+ <div class="ergo-card" id="ergo-card">
23
+ <div class="ergo-header">
24
+ <div class="ergo-mode-toggle" id="ergo-mode-toggle">
25
+ <button class="ergo-mode-btn ergo-mode-active" data-mode="sit">{eUI.modeSit}</button>
26
+ <button class="ergo-mode-btn" data-mode="stand">{eUI.modeStand}</button>
27
+ </div>
28
+ <div class="ergo-field">
29
+ <div class="ergo-field-top">
30
+ <span class="ergo-label">{eUI.labelHeight}</span>
31
+ <span class="ergo-val" id="ergo-height-val">{defaultHeight} {eUI.unitCm}</span>
32
+ </div>
33
+ <input type="range" class="ergo-slider" id="ergo-height" min="140" max="210" step="1" value={defaultHeight} />
34
+ </div>
35
+ <div class="ergo-gender-row" id="ergo-gender-row">
36
+ <button class="ergo-gender-btn ergo-gender-active" data-gender="male">{eUI.genderMale}</button>
37
+ <button class="ergo-gender-btn" data-gender="female">{eUI.genderFemale}</button>
38
+ </div>
39
+ </div>
40
+
41
+ <div class="ergo-viz" id="ergo-viz">
42
+ <div class="ergo-ruler" id="ergo-ruler" />
43
+
44
+ <div class="ergo-arc" id="ergo-arc">
45
+ <span class="ergo-arc-label" id="arc-label">{initial.monitorDistance} {eUI.unitCm}</span>
46
+ </div>
47
+
48
+ <div class="ergo-tower">
49
+ <div class="ergo-tower-icon">
50
+ <svg viewBox="0 0 24 24"><path d="M5 11l7-7 7 7M12 4v16"/></svg>
51
+ </div>
52
+ <div class="ergo-tower-track" id="tower-chair">
53
+ <div class="ergo-tower-zone" id="zone-chair" />
54
+ <div class="ergo-tower-fill" id="fill-chair" style={`height:${chairPct}%`} />
55
+ <div class="ergo-tower-pip" id="pip-chair" style={`bottom:${chairPct}%`} />
56
+ </div>
57
+ <span class="ergo-tower-label">{eUI.labelChair}</span>
58
+ <span class="ergo-tower-val" id="val-chair">{fmt0(initial.chairHeight)}</span>
59
+ <span class="ergo-tower-unit">{eUI.unitCm}</span>
60
+ </div>
61
+
62
+ <div class="ergo-tower">
63
+ <div class="ergo-tower-icon">
64
+ <svg viewBox="0 0 24 24"><rect x="2" y="7" width="20" height="10" rx="2"/><path d="M6 17v4M18 17v4"/></svg>
65
+ </div>
66
+ <div class="ergo-tower-track" id="tower-desk">
67
+ <div class="ergo-tower-zone" id="zone-desk" />
68
+ <div class="ergo-tower-fill" id="fill-desk" style={`height:${deskPct}%`} />
69
+ <div class="ergo-tower-pip" id="pip-desk" style={`bottom:${deskPct}%`} />
70
+ </div>
71
+ <span class="ergo-tower-label">{eUI.labelDesk}</span>
72
+ <span class="ergo-tower-val" id="val-desk">{fmt0(initial.deskHeight)}</span>
73
+ <span class="ergo-tower-unit">{eUI.unitCm}</span>
74
+ </div>
75
+
76
+ <div class="ergo-tower">
77
+ <div class="ergo-tower-icon">
78
+ <svg viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
79
+ </div>
80
+ <div class="ergo-tower-track" id="tower-monitor">
81
+ <div class="ergo-tower-zone" id="zone-monitor" />
82
+ <div class="ergo-tower-fill" id="fill-monitor" style={`height:${monPct}%`} />
83
+ <div class="ergo-tower-pip" id="pip-monitor" style={`bottom:${monPct}%`} />
84
+ </div>
85
+ <span class="ergo-tower-label">{eUI.labelMonitor}</span>
86
+ <span class="ergo-tower-val" id="val-monitor">{fmt0(initial.monitorHeight)}</span>
87
+ <span class="ergo-tower-unit">{eUI.unitCm}</span>
88
+ </div>
89
+ </div>
90
+
91
+ <div class="ergo-footer">
92
+ <div class="ergo-stat">
93
+ <p class="ergo-stat-label">{eUI.labelChair}</p>
94
+ <p class="ergo-stat-value" id="stat-chair">{fmt0(initial.chairHeight)} {eUI.unitCm}</p>
95
+ </div>
96
+ <div class="ergo-stat">
97
+ <p class="ergo-stat-label">{eUI.labelDesk}</p>
98
+ <p class="ergo-stat-value" id="stat-desk">{fmt0(initial.deskHeight)} {eUI.unitCm}</p>
99
+ </div>
100
+ <div class="ergo-stat">
101
+ <p class="ergo-stat-label">{eUI.labelMonitor}</p>
102
+ <p class="ergo-stat-value" id="stat-monitor">{fmt0(initial.monitorHeight)} {eUI.unitCm}</p>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ <script>
109
+ import { calculateErgonomics, fmt0 } from './logic';
110
+
111
+ function el(id) { return document.getElementById(id); }
112
+ function setTxt(id, val) { const e = el(id); if (e) e.textContent = val; }
113
+ function setStyle(id, styles) { const e = el(id); if (!e) return; Object.entries(styles).forEach(([k, v]) => { e.style[k] = v; }); }
114
+ function toggleClass(sel, attr, val, cls) { document.querySelectorAll(sel).forEach((b) => b.classList.toggle(cls, (b as HTMLElement).dataset[attr] === val)); }
115
+
116
+ let height = 175;
117
+ let gender = 'male';
118
+ let mode = 'sit';
119
+
120
+ const MAX_V = 140;
121
+
122
+ function pct(v) { return Math.min((v / MAX_V) * 100, 100); }
123
+
124
+ function updateTower(p) {
125
+ setStyle(`fill-${p.id}`, { height: `${pct(p.val)}%` });
126
+ setStyle(`pip-${p.id}`, { bottom: `${pct(p.val)}%` });
127
+ setTxt(`val-${p.id}`, fmt0(p.val));
128
+ }
129
+
130
+ function updateZone(p) {
131
+ const zone = el(`zone-${p.id}`);
132
+ if (!zone) return;
133
+ const topPct = pct(p.optMax);
134
+ const botPct = pct(p.optMin);
135
+ zone.style.top = `${100 - topPct}%`;
136
+ zone.style.height = `${topPct - botPct}%`;
137
+ }
138
+
139
+ function calculate() {
140
+ const r = calculateErgonomics(height, gender, mode);
141
+ setTxt('stat-chair', `${fmt0(r.chairHeight)} cm`);
142
+ setTxt('stat-desk', `${fmt0(r.deskHeight)} cm`);
143
+ setTxt('stat-monitor', `${fmt0(r.monitorHeight)} cm`);
144
+ setTxt('arc-label', `${fmt0(r.monitorDistance)} cm`);
145
+
146
+ updateTower({ id: 'chair', val: r.chairHeight });
147
+ updateTower({ id: 'desk', val: r.deskHeight });
148
+ updateTower({ id: 'monitor', val: r.monitorHeight });
149
+
150
+ updateZone({ id: 'chair', optMin: r.chairHeight - 3, optMax: r.chairHeight + 3 });
151
+ updateZone({ id: 'desk', optMin: r.deskHeight - 3, optMax: r.deskHeight + 3 });
152
+ updateZone({ id: 'monitor', optMin: r.monitorHeight - 5, optMax: r.monitorHeight + 5 });
153
+ }
154
+
155
+ function bindSlider() {
156
+ const slider = el('ergo-height');
157
+ const val = el('ergo-height-val');
158
+ if (slider) {
159
+ slider.addEventListener('input', () => {
160
+ height = parseInt(slider.value, 10);
161
+ if (val) val.textContent = `${height} cm`;
162
+ calculate();
163
+ });
164
+ }
165
+ }
166
+
167
+ function bindToggles() {
168
+ document.querySelectorAll('#ergo-mode-toggle .ergo-mode-btn').forEach((btn) => {
169
+ btn.addEventListener('click', () => {
170
+ mode = (btn as HTMLElement).dataset.mode;
171
+ toggleClass('#ergo-mode-toggle .ergo-mode-btn', 'mode', mode, 'ergo-mode-active');
172
+ calculate();
173
+ });
174
+ });
175
+ document.querySelectorAll('#ergo-gender-row .ergo-gender-btn').forEach((btn) => {
176
+ btn.addEventListener('click', () => {
177
+ gender = (btn as HTMLElement).dataset.gender;
178
+ toggleClass('#ergo-gender-row .ergo-gender-btn', 'gender', gender, 'ergo-gender-active');
179
+ calculate();
180
+ });
181
+ });
182
+ }
183
+
184
+ function init() {
185
+ bindSlider();
186
+ bindToggles();
187
+ calculate();
188
+ }
189
+
190
+ document.addEventListener('astro:page-load', init);
191
+ init();
192
+ </script>
@@ -0,0 +1,361 @@
1
+ .ergo-wrapper {
2
+ --e-accent: #0ea5e9;
3
+ --e-ok: #22c55e;
4
+ --e-warn: #f59e0b;
5
+ --e-danger: #ef4444;
6
+ --e-surface: var(--bg-surface);
7
+ --e-border: var(--border-color);
8
+ --e-text: var(--text-main);
9
+ --e-muted: var(--text-muted);
10
+ --e-page: var(--bg-page);
11
+
12
+ width: 100%;
13
+ padding: 1rem 0;
14
+ }
15
+
16
+ .ergo-card {
17
+ width: 100%;
18
+ max-width: 640px;
19
+ margin: 0 auto;
20
+ background: var(--e-surface);
21
+ border: 1px solid var(--e-border);
22
+ border-radius: 24px;
23
+ overflow: hidden;
24
+ display: flex;
25
+ flex-direction: column;
26
+ }
27
+
28
+ /* ---- HEADER ---- */
29
+ .ergo-header {
30
+ padding: 1.25rem;
31
+ border-bottom: 1px solid var(--e-border);
32
+ display: flex;
33
+ flex-direction: column;
34
+ gap: 0.875rem;
35
+ }
36
+
37
+ .ergo-mode-toggle {
38
+ display: flex;
39
+ background: var(--e-page);
40
+ padding: 4px;
41
+ border-radius: 14px;
42
+ gap: 2px;
43
+ border: 1px solid var(--e-border);
44
+ }
45
+
46
+ .ergo-mode-btn {
47
+ flex: 1;
48
+ padding: 0.55rem 0.5rem;
49
+ border: none;
50
+ background: transparent;
51
+ border-radius: 10px;
52
+ font-size: 0.75rem;
53
+ font-weight: 800;
54
+ text-transform: uppercase;
55
+ letter-spacing: 0.1em;
56
+ color: var(--e-muted);
57
+ cursor: pointer;
58
+ transition: all 0.25s;
59
+ }
60
+
61
+ .ergo-mode-active {
62
+ background: var(--e-accent);
63
+ color: #fff;
64
+ box-shadow: 0 4px 14px rgba(14, 165, 233, 0.3);
65
+ }
66
+
67
+ .ergo-field {
68
+ display: flex;
69
+ flex-direction: column;
70
+ gap: 0.5rem;
71
+ }
72
+
73
+ .ergo-field-top {
74
+ display: flex;
75
+ justify-content: space-between;
76
+ align-items: center;
77
+ }
78
+
79
+ .ergo-label {
80
+ font-size: 0.625rem;
81
+ font-weight: 900;
82
+ text-transform: uppercase;
83
+ letter-spacing: 0.16em;
84
+ color: var(--e-muted);
85
+ }
86
+
87
+ .ergo-val {
88
+ font-size: 0.875rem;
89
+ font-weight: 800;
90
+ color: var(--e-text);
91
+ }
92
+
93
+ .ergo-slider {
94
+ width: 100%;
95
+ height: 6px;
96
+ border-radius: 3px;
97
+ background: var(--e-page);
98
+ outline: none;
99
+ border: none;
100
+ -webkit-appearance: none;
101
+ appearance: none;
102
+ }
103
+
104
+ .ergo-slider::-webkit-slider-thumb {
105
+ -webkit-appearance: none;
106
+ appearance: none;
107
+ width: 20px;
108
+ height: 20px;
109
+ border-radius: 50%;
110
+ background: var(--e-accent);
111
+ cursor: pointer;
112
+ border: 3px solid var(--e-surface);
113
+ box-shadow: 0 2px 8px rgba(14, 165, 233, 0.3);
114
+ }
115
+
116
+ .ergo-slider::-moz-range-thumb {
117
+ width: 20px;
118
+ height: 20px;
119
+ border-radius: 50%;
120
+ background: var(--e-accent);
121
+ cursor: pointer;
122
+ border: 3px solid var(--e-surface);
123
+ box-shadow: 0 2px 8px rgba(14, 165, 233, 0.3);
124
+ }
125
+
126
+ .ergo-gender-row {
127
+ display: flex;
128
+ gap: 0.5rem;
129
+ }
130
+
131
+ .ergo-gender-btn {
132
+ flex: 1;
133
+ padding: 0.5rem;
134
+ border-radius: 10px;
135
+ border: 1px solid var(--e-border);
136
+ background: var(--e-page);
137
+ color: var(--e-muted);
138
+ font-size: 0.8rem;
139
+ font-weight: 700;
140
+ cursor: pointer;
141
+ transition: all 0.2s;
142
+ }
143
+
144
+ .ergo-gender-active {
145
+ background: var(--e-accent);
146
+ border-color: var(--e-accent);
147
+ color: #fff;
148
+ }
149
+
150
+ /* ---- VISUALIZER ---- */
151
+ .ergo-viz {
152
+ position: relative;
153
+ flex: 1;
154
+ min-height: 340px;
155
+ background: var(--e-page);
156
+ display: flex;
157
+ align-items: flex-end;
158
+ justify-content: center;
159
+ padding: 2rem 2rem 2.5rem;
160
+ gap: 1.5rem;
161
+ overflow: hidden;
162
+ }
163
+
164
+ .ergo-tower {
165
+ position: relative;
166
+ width: 72px;
167
+ display: flex;
168
+ flex-direction: column;
169
+ align-items: center;
170
+ gap: 0.5rem;
171
+ }
172
+
173
+ .ergo-tower-track {
174
+ position: relative;
175
+ width: 100%;
176
+ height: 220px;
177
+ background: var(--e-surface);
178
+ border-radius: 16px;
179
+ border: 1px solid var(--e-border);
180
+ overflow: hidden;
181
+ }
182
+
183
+ .ergo-tower-zone {
184
+ position: absolute;
185
+ left: 0;
186
+ right: 0;
187
+ background: linear-gradient(180deg, rgba(34,197,94,0.08), rgba(34,197,94,0.03));
188
+ border-top: 1px dashed rgba(34,197,94,0.25);
189
+ border-bottom: 1px dashed rgba(34,197,94,0.25);
190
+ }
191
+
192
+ .ergo-tower-fill {
193
+ position: absolute;
194
+ left: 0;
195
+ right: 0;
196
+ bottom: 0;
197
+ background: linear-gradient(180deg, rgba(14,165,233,0.25), rgba(14,165,233,0.08));
198
+ border-radius: 0 0 14px 14px;
199
+ transition: height 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
200
+ }
201
+
202
+ .ergo-tower-pip {
203
+ position: absolute;
204
+ left: 0;
205
+ right: 0;
206
+ height: 2px;
207
+ background: var(--e-accent);
208
+ box-shadow: 0 0 8px rgba(14, 165, 233, 0.4);
209
+ transition: bottom 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
210
+ }
211
+
212
+ .ergo-tower-pip::after {
213
+ content: '';
214
+ position: absolute;
215
+ right: 4px;
216
+ top: 50%;
217
+ transform: translateY(-50%);
218
+ width: 6px;
219
+ height: 6px;
220
+ border-radius: 50%;
221
+ background: var(--e-accent);
222
+ }
223
+
224
+ .ergo-tower-icon {
225
+ width: 32px;
226
+ height: 32px;
227
+ border-radius: 10px;
228
+ background: var(--e-surface);
229
+ border: 1px solid var(--e-border);
230
+ display: flex;
231
+ align-items: center;
232
+ justify-content: center;
233
+ color: var(--e-muted);
234
+ }
235
+
236
+ .ergo-tower-icon svg {
237
+ width: 18px;
238
+ height: 18px;
239
+ stroke: currentcolor;
240
+ stroke-width: 2;
241
+ fill: none;
242
+ stroke-linecap: round;
243
+ stroke-linejoin: round;
244
+ }
245
+
246
+ .ergo-tower-label {
247
+ font-size: 0.6rem;
248
+ font-weight: 800;
249
+ text-transform: uppercase;
250
+ letter-spacing: 0.12em;
251
+ color: var(--e-muted);
252
+ }
253
+
254
+ .ergo-tower-val {
255
+ font-size: 1.25rem;
256
+ font-weight: 900;
257
+ color: var(--e-text);
258
+ line-height: 1;
259
+ }
260
+
261
+ .ergo-tower-unit {
262
+ font-size: 0.7rem;
263
+ font-weight: 600;
264
+ color: var(--e-muted);
265
+ }
266
+
267
+ /* Ruler lines */
268
+ .ergo-ruler {
269
+ position: absolute;
270
+ left: 0.75rem;
271
+ top: 2rem;
272
+ bottom: 3rem;
273
+ width: 1px;
274
+ background: linear-gradient(to bottom, transparent 0%, var(--e-border) 10%, var(--e-border) 90%, transparent 100%);
275
+ }
276
+
277
+ .ergo-ruler-mark {
278
+ position: absolute;
279
+ left: 2px;
280
+ width: 6px;
281
+ height: 1px;
282
+ background: var(--e-border);
283
+ }
284
+
285
+ /* Distance arc */
286
+ .ergo-arc {
287
+ position: absolute;
288
+ left: 50%;
289
+ top: 2rem;
290
+ transform: translateX(-50%);
291
+ width: 120px;
292
+ height: 60px;
293
+ border: 1px dashed rgba(14, 165, 233, 0.2);
294
+ border-bottom: none;
295
+ border-radius: 60px 60px 0 0;
296
+ pointer-events: none;
297
+ }
298
+
299
+ .ergo-arc-label {
300
+ position: absolute;
301
+ left: 50%;
302
+ top: 0.5rem;
303
+ transform: translateX(-50%);
304
+ font-size: 0.6rem;
305
+ font-weight: 800;
306
+ color: var(--e-accent);
307
+ letter-spacing: 0.05em;
308
+ white-space: nowrap;
309
+ }
310
+
311
+ /* ---- FOOTER ---- */
312
+ .ergo-footer {
313
+ display: grid;
314
+ grid-template-columns: repeat(3, 1fr);
315
+ gap: 0.5rem;
316
+ padding: 1rem 1.25rem;
317
+ border-top: 1px solid var(--e-border);
318
+ }
319
+
320
+ .ergo-stat {
321
+ text-align: center;
322
+ padding: 0.625rem 0.375rem;
323
+ border-radius: 14px;
324
+ background: var(--e-page);
325
+ border: 1px solid var(--e-border);
326
+ transition: transform 0.2s, border-color 0.2s;
327
+ }
328
+
329
+ .ergo-stat:hover {
330
+ transform: translateY(-2px);
331
+ border-color: rgba(14, 165, 233, 0.25);
332
+ }
333
+
334
+ .ergo-stat-label {
335
+ font-size: 0.55rem;
336
+ font-weight: 800;
337
+ text-transform: uppercase;
338
+ letter-spacing: 0.13em;
339
+ color: var(--e-muted);
340
+ margin: 0 0 0.25rem;
341
+ }
342
+
343
+ .ergo-stat-value {
344
+ font-size: 0.95rem;
345
+ font-weight: 800;
346
+ color: var(--e-text);
347
+ margin: 0;
348
+ }
349
+
350
+ /* ---- RESPONSIVE ---- */
351
+ @media (max-width: 520px) {
352
+ .ergo-wrapper { padding: 0.75rem 0; }
353
+ .ergo-card { border-radius: 20px; }
354
+ .ergo-header { padding: 1rem; }
355
+ .ergo-viz { padding: 1.5rem 1rem 2rem; gap: 1rem; min-height: 300px;
356
+ }
357
+ .ergo-tower { width: 60px; }
358
+ .ergo-tower-track { height: 180px; }
359
+ .ergo-footer { grid-template-columns: 1fr; padding: 0.75rem 1rem; gap: 0.4rem;
360
+ }
361
+ }
@@ -0,0 +1,29 @@
1
+ import type { HomeToolEntry, ToolLocaleContent } from '../../types';
2
+ import type { DeskErgonomicsUI } from './ui';
3
+
4
+ export type DeskErgonomicsLocaleContent = ToolLocaleContent<DeskErgonomicsUI>;
5
+
6
+ export const deskErgonomics: HomeToolEntry<DeskErgonomicsUI> = {
7
+ id: 'desk-ergonomics',
8
+ icons: {
9
+ bg: 'mdi:desk',
10
+ fg: 'mdi:human',
11
+ },
12
+ i18n: {
13
+ de: async () => (await import('./i18n/de')).content,
14
+ en: async () => (await import('./i18n/en')).content,
15
+ es: async () => (await import('./i18n/es')).content,
16
+ fr: async () => (await import('./i18n/fr')).content,
17
+ id: async () => (await import('./i18n/id')).content,
18
+ it: async () => (await import('./i18n/it')).content,
19
+ ja: async () => (await import('./i18n/ja')).content,
20
+ ko: async () => (await import('./i18n/ko')).content,
21
+ nl: async () => (await import('./i18n/nl')).content,
22
+ pl: async () => (await import('./i18n/pl')).content,
23
+ pt: async () => (await import('./i18n/pt')).content,
24
+ ru: async () => (await import('./i18n/ru')).content,
25
+ sv: async () => (await import('./i18n/sv')).content,
26
+ tr: async () => (await import('./i18n/tr')).content,
27
+ zh: async () => (await import('./i18n/zh')).content,
28
+ },
29
+ };