@jjlmoya/utils-home 1.23.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.
Files changed (31) hide show
  1. package/package.json +1 -1
  2. package/src/entries.ts +4 -1
  3. package/src/tests/locale_completeness.test.ts +2 -2
  4. package/src/tests/tool_validation.test.ts +2 -2
  5. package/src/tool/acTonnageCalculator/ac-tonnage-calculator.css +467 -0
  6. package/src/tool/acTonnageCalculator/bibliography.astro +14 -0
  7. package/src/tool/acTonnageCalculator/bibliography.ts +5 -0
  8. package/src/tool/acTonnageCalculator/client-animations.ts +80 -0
  9. package/src/tool/acTonnageCalculator/client.ts +171 -0
  10. package/src/tool/acTonnageCalculator/component.astro +186 -0
  11. package/src/tool/acTonnageCalculator/entry.ts +29 -0
  12. package/src/tool/acTonnageCalculator/i18n/de.ts +63 -0
  13. package/src/tool/acTonnageCalculator/i18n/en.ts +136 -0
  14. package/src/tool/acTonnageCalculator/i18n/es.ts +136 -0
  15. package/src/tool/acTonnageCalculator/i18n/fr.ts +61 -0
  16. package/src/tool/acTonnageCalculator/i18n/id.ts +61 -0
  17. package/src/tool/acTonnageCalculator/i18n/it.ts +61 -0
  18. package/src/tool/acTonnageCalculator/i18n/ja.ts +61 -0
  19. package/src/tool/acTonnageCalculator/i18n/ko.ts +61 -0
  20. package/src/tool/acTonnageCalculator/i18n/nl.ts +61 -0
  21. package/src/tool/acTonnageCalculator/i18n/pl.ts +61 -0
  22. package/src/tool/acTonnageCalculator/i18n/pt.ts +61 -0
  23. package/src/tool/acTonnageCalculator/i18n/ru.ts +61 -0
  24. package/src/tool/acTonnageCalculator/i18n/sv.ts +61 -0
  25. package/src/tool/acTonnageCalculator/i18n/tr.ts +61 -0
  26. package/src/tool/acTonnageCalculator/i18n/zh.ts +61 -0
  27. package/src/tool/acTonnageCalculator/index.ts +8 -0
  28. package/src/tool/acTonnageCalculator/logic.ts +56 -0
  29. package/src/tool/acTonnageCalculator/seo.astro +15 -0
  30. package/src/tool/acTonnageCalculator/ui.ts +39 -0
  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.23.0",
3
+ "version": "1.24.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
package/src/entries.ts CHANGED
@@ -14,6 +14,8 @@ export { tariffComparator } from './tool/tariffComparator/entry';
14
14
  export type { TariffComparatorLocaleContent } from './tool/tariffComparator/entry';
15
15
  export { wifiRangeSimulator } from './tool/wifiRangeSimulator/entry';
16
16
  export type { WifiRangeSimulatorLocaleContent } from './tool/wifiRangeSimulator/entry';
17
+ export { acTonnageCalculator } from './tool/acTonnageCalculator/entry';
18
+ export type { AcTonnageCalculatorLocaleContent } from './tool/acTonnageCalculator/entry';
17
19
  export { homeCategory } from './category';
18
20
  import { dewPointCalculator } from './tool/dewPointCalculator/entry';
19
21
  import { heatingComparator } from './tool/heatingComparator/entry';
@@ -23,4 +25,5 @@ import { qrGenerator } from './tool/qrGenerator/entry';
23
25
  import { solarCalculator } from './tool/solarCalculator/entry';
24
26
  import { tariffComparator } from './tool/tariffComparator/entry';
25
27
  import { wifiRangeSimulator } from './tool/wifiRangeSimulator/entry';
26
- export const ALL_ENTRIES = [dewPointCalculator, heatingComparator, ledSavingCalculator, projectorCalculator, qrGenerator, solarCalculator, tariffComparator, wifiRangeSimulator];
28
+ import { acTonnageCalculator } from './tool/acTonnageCalculator/entry';
29
+ export const ALL_ENTRIES = [dewPointCalculator, heatingComparator, ledSavingCalculator, projectorCalculator, qrGenerator, solarCalculator, tariffComparator, wifiRangeSimulator, acTonnageCalculator];
@@ -17,8 +17,8 @@ describe('Locale Completeness Validation', () => {
17
17
  });
18
18
  });
19
19
 
20
- it('should have 8 tools registered', () => {
21
- expect(ALL_TOOLS.length).toBe(8);
20
+ it('should have 9 tools registered', () => {
21
+ expect(ALL_TOOLS.length).toBe(9);
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 8 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(8);
7
+ it('should have 9 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(9);
9
9
  });
10
10
 
11
11
  it('homeCategory should be defined', () => {
@@ -0,0 +1,467 @@
1
+ .ac-wrapper {
2
+ --ac-accent: #0ea5e9;
3
+ --ac-accent-dim: rgba(14,165,233,0.12);
4
+ --ac-surface: var(--bg-surface);
5
+ --ac-border: var(--border-color);
6
+ --ac-text: var(--text-main);
7
+ --ac-muted: var(--text-muted);
8
+
9
+ width: 100%;
10
+ padding: 1rem 0;
11
+ }
12
+
13
+ .ac-card {
14
+ background: var(--ac-surface);
15
+ width: calc(100% - 24px);
16
+ max-width: 900px;
17
+ margin: 0 auto;
18
+ border-radius: 24px;
19
+ overflow: hidden;
20
+ border: 1px solid var(--ac-border);
21
+ color: var(--ac-text);
22
+ display: flex;
23
+ flex-direction: column;
24
+ }
25
+
26
+ @media (min-width: 1024px) {
27
+ .ac-card {
28
+ flex-direction: row;
29
+ min-height: 600px;
30
+ }
31
+ }
32
+
33
+ .ac-left {
34
+ flex: 0 0 auto;
35
+ padding: 24px;
36
+ display: flex;
37
+ flex-direction: column;
38
+ gap: 18px;
39
+ }
40
+
41
+ @media (min-width: 1024px) {
42
+ .ac-left {
43
+ flex: 1 1 auto;
44
+ width: auto;
45
+ min-width: 280px;
46
+ }
47
+ }
48
+
49
+ .ac-unit-toggle {
50
+ display: flex;
51
+ align-items: center;
52
+ gap: 8px;
53
+ padding: 6px 10px;
54
+ border-radius: 10px;
55
+ background: var(--bg-page);
56
+ border: 1px solid var(--ac-border);
57
+ align-self: flex-start;
58
+ }
59
+
60
+ .ac-unit-toggle button {
61
+ padding: 5px 10px;
62
+ border-radius: 6px;
63
+ border: none;
64
+ background: transparent;
65
+ color: var(--ac-muted);
66
+ font-size: 0.6875rem;
67
+ font-weight: 800;
68
+ cursor: pointer;
69
+ transition: all 0.15s;
70
+ }
71
+
72
+ .ac-power-toggle {
73
+ display: flex;
74
+ align-items: center;
75
+ gap: 4px;
76
+ padding: 4px 6px;
77
+ border-radius: 8px;
78
+ background: var(--bg-page);
79
+ border: 1px solid var(--ac-border);
80
+ }
81
+
82
+ .ac-power-toggle button {
83
+ padding: 4px 8px;
84
+ border-radius: 5px;
85
+ border: none;
86
+ background: transparent;
87
+ color: var(--ac-muted);
88
+ font-size: 0.625rem;
89
+ font-weight: 800;
90
+ cursor: pointer;
91
+ transition: all 0.15s;
92
+ white-space: nowrap;
93
+ }
94
+
95
+ .ac-power-toggle button.active {
96
+ background: var(--ac-accent);
97
+ color: #fff;
98
+ }
99
+
100
+ .ac-unit-toggle button.active {
101
+ background: var(--ac-accent);
102
+ color: #fff;
103
+ }
104
+
105
+ .ac-field {
106
+ display: flex;
107
+ flex-direction: column;
108
+ gap: 8px;
109
+ }
110
+
111
+ .ac-field-top {
112
+ display: flex;
113
+ align-items: center;
114
+ justify-content: space-between;
115
+ gap: 8px;
116
+ }
117
+
118
+ .ac-label {
119
+ font-size: 0.6875rem;
120
+ font-weight: 800;
121
+ text-transform: uppercase;
122
+ letter-spacing: 0.06em;
123
+ color: var(--ac-muted);
124
+ flex-shrink: 0;
125
+ }
126
+
127
+ .ac-val {
128
+ font-size: 0.875rem;
129
+ font-weight: 900;
130
+ color: var(--ac-accent);
131
+ white-space: nowrap;
132
+ }
133
+
134
+ .ac-slider {
135
+ width: 100%;
136
+ height: 6px;
137
+ border-radius: 3px;
138
+ background: var(--bg-muted);
139
+ outline: none;
140
+ -webkit-appearance: none;
141
+ appearance: none;
142
+ }
143
+
144
+ .ac-slider::-webkit-slider-thumb {
145
+ -webkit-appearance: none;
146
+ width: 20px;
147
+ height: 20px;
148
+ border-radius: 50%;
149
+ background: var(--ac-accent);
150
+ cursor: pointer;
151
+ box-shadow: 0 2px 8px rgba(14,165,233,0.3);
152
+ transition: transform 0.15s;
153
+ }
154
+
155
+ .ac-slider::-webkit-slider-thumb:hover {
156
+ transform: scale(1.15);
157
+ }
158
+
159
+ .ac-slider::-moz-range-thumb {
160
+ width: 20px;
161
+ height: 20px;
162
+ border-radius: 50%;
163
+ background: var(--ac-accent);
164
+ cursor: pointer;
165
+ border: none;
166
+ box-shadow: 0 2px 8px rgba(14,165,233,0.3);
167
+ }
168
+
169
+ .ac-steps {
170
+ display: flex;
171
+ gap: 6px;
172
+ }
173
+
174
+ .ac-step {
175
+ width: 28px;
176
+ height: 28px;
177
+ border-radius: 6px;
178
+ border: 1px solid var(--ac-border);
179
+ background: var(--bg-page);
180
+ color: var(--ac-text);
181
+ font-size: 1rem;
182
+ font-weight: 700;
183
+ cursor: pointer;
184
+ display: flex;
185
+ align-items: center;
186
+ justify-content: center;
187
+ transition: all 0.15s;
188
+ }
189
+
190
+ .ac-step:hover {
191
+ border-color: var(--ac-accent);
192
+ color: var(--ac-accent);
193
+ }
194
+
195
+ .ac-row {
196
+ display: grid;
197
+ grid-template-columns: 1fr 1fr;
198
+ gap: 12px;
199
+ }
200
+
201
+ .ac-select-wrap {
202
+ display: flex;
203
+ flex-direction: column;
204
+ gap: 6px;
205
+ }
206
+
207
+ .ac-select {
208
+ padding: 10px 36px 10px 12px;
209
+ border-radius: 10px;
210
+ border: 1px solid var(--ac-border);
211
+ background: var(--bg-page);
212
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
213
+ background-repeat: no-repeat;
214
+ background-position: right 12px center;
215
+ color: var(--ac-text);
216
+ font-size: 0.875rem;
217
+ font-weight: 700;
218
+ outline: none;
219
+ cursor: pointer;
220
+ -webkit-appearance: none;
221
+ appearance: none;
222
+ }
223
+
224
+ .ac-select:focus {
225
+ border-color: var(--ac-accent);
226
+ box-shadow: 0 0 0 3px var(--ac-accent-dim);
227
+ }
228
+
229
+ .ac-right {
230
+ flex: 0 0 auto;
231
+ width: 100%;
232
+ padding: 24px;
233
+ border-top: 1px solid var(--ac-border);
234
+ background: var(--bg-page);
235
+ display: flex;
236
+ flex-direction: column;
237
+ gap: 20px;
238
+ align-items: center;
239
+ position: relative;
240
+ overflow: hidden;
241
+ }
242
+
243
+ @media (min-width: 1024px) {
244
+ .ac-right {
245
+ width: 300px;
246
+ border-top: none;
247
+ border-left: 1px solid var(--ac-border);
248
+ }
249
+ }
250
+
251
+ .ac-ring-wrap {
252
+ position: relative;
253
+ width: 160px;
254
+ height: 160px;
255
+ display: flex;
256
+ align-items: center;
257
+ justify-content: center;
258
+ }
259
+
260
+ .ac-ring-wrap svg {
261
+ position: absolute;
262
+ inset: 0;
263
+ transform: rotate(-90deg);
264
+ }
265
+
266
+ .ac-ring-glow {
267
+ position: absolute;
268
+ width: 160px;
269
+ height: 160px;
270
+ border-radius: 50%;
271
+ pointer-events: none;
272
+ opacity: 0;
273
+ transition: opacity 0.5s;
274
+ }
275
+
276
+ .ac-ring-glow.active {
277
+ opacity: 1;
278
+ animation: ac-glow-pulse 2s ease-in-out infinite;
279
+ }
280
+
281
+ @keyframes ac-glow-pulse {
282
+ 0%, 100% {
283
+ box-shadow: 0 0 20px rgba(14,165,233,0.2);
284
+ transform: scale(1);
285
+ }
286
+ 50% {
287
+ box-shadow: 0 0 40px rgba(14,165,233,0.4);
288
+ transform: scale(1.05);
289
+ }
290
+ }
291
+
292
+ .ac-ring-bg {
293
+ fill: none;
294
+ stroke: var(--bg-muted);
295
+ stroke-width: 10;
296
+ }
297
+
298
+ .ac-ring-fill {
299
+ fill: none;
300
+ stroke-width: 10;
301
+ stroke-linecap: round;
302
+ transition: stroke-dashoffset 0.6s cubic-bezier(0.34, 1.56, 0.64, 1), stroke 0.5s ease;
303
+ }
304
+
305
+ .ac-ring-inner {
306
+ position: relative;
307
+ z-index: 2;
308
+ text-align: center;
309
+ }
310
+
311
+ .ac-ring-label {
312
+ font-size: 0.625rem;
313
+ font-weight: 800;
314
+ text-transform: uppercase;
315
+ letter-spacing: 0.06em;
316
+ color: var(--ac-muted);
317
+ }
318
+
319
+ .ac-ring-btu {
320
+ font-size: 1.75rem;
321
+ font-weight: 900;
322
+ line-height: 1;
323
+ color: var(--ac-text);
324
+ }
325
+
326
+ .ac-ring-unit {
327
+ font-size: 0.625rem;
328
+ font-weight: 700;
329
+ color: var(--ac-muted);
330
+ }
331
+
332
+ .ac-mini-room {
333
+ padding: 10px;
334
+ border-radius: 14px;
335
+ background: var(--ac-surface);
336
+ border: 1px solid var(--ac-border);
337
+ transition: all 0.5s;
338
+ }
339
+
340
+ .ac-mini-room svg {
341
+ display: block;
342
+ }
343
+
344
+ .ac-room-wall {
345
+ fill: var(--bg-page);
346
+ stroke: var(--ac-border);
347
+ stroke-width: 1.5;
348
+ transition: fill 0.5s;
349
+ }
350
+
351
+ .ac-room-window {
352
+ fill: #bfdbfe;
353
+ transition: fill 0.5s;
354
+ }
355
+
356
+ .ac-room-person {
357
+ fill: #94a3b8;
358
+ transition: fill 0.5s;
359
+ }
360
+
361
+ .ac-room-pc {
362
+ fill: #475569;
363
+ transition: fill 0.5s;
364
+ }
365
+
366
+ .ac-room-sun {
367
+ fill: #fbbf24;
368
+ transition: fill 0.5s;
369
+ }
370
+
371
+ .ac-theme-low .ac-room-wall { fill: #e0f2fe; }
372
+ .ac-theme-low .ac-card { border-color: #7dd3fc; }
373
+
374
+ .ac-theme-med .ac-room-wall { fill: #fef3c7; }
375
+ .ac-theme-med .ac-card { border-color: #fcd34d; }
376
+
377
+ .ac-theme-high .ac-room-wall { fill: #fee2e2; }
378
+ .ac-theme-high .ac-card { border-color: #fca5a5; }
379
+
380
+ .ac-stats {
381
+ width: 100%;
382
+ display: grid;
383
+ grid-template-columns: 1fr 1fr;
384
+ gap: 10px;
385
+ }
386
+
387
+ .ac-stat {
388
+ padding: 12px;
389
+ border-radius: 12px;
390
+ background: var(--ac-surface);
391
+ border: 1px solid var(--ac-border);
392
+ display: flex;
393
+ flex-direction: column;
394
+ gap: 4px;
395
+ text-align: center;
396
+ }
397
+
398
+ .ac-stat-label {
399
+ font-size: 0.625rem;
400
+ font-weight: 800;
401
+ text-transform: uppercase;
402
+ letter-spacing: 0.06em;
403
+ color: var(--ac-muted);
404
+ }
405
+
406
+ .ac-stat-val {
407
+ font-size: 1.25rem;
408
+ font-weight: 900;
409
+ color: var(--ac-accent);
410
+ }
411
+
412
+ .ac-breakdown {
413
+ width: 100%;
414
+ display: flex;
415
+ flex-direction: column;
416
+ gap: 8px;
417
+ }
418
+
419
+ .ac-bd-row {
420
+ display: flex;
421
+ align-items: center;
422
+ gap: 10px;
423
+ padding: 7px 9px;
424
+ border-radius: 8px;
425
+ background: var(--ac-surface);
426
+ border: 1px solid var(--ac-border);
427
+ }
428
+
429
+ .ac-bd-label {
430
+ font-size: 0.6875rem;
431
+ font-weight: 700;
432
+ color: var(--ac-text);
433
+ min-width: 100px;
434
+ }
435
+
436
+ .ac-bd-bar-wrap {
437
+ flex: 1;
438
+ height: 6px;
439
+ border-radius: 3px;
440
+ background: var(--bg-muted);
441
+ overflow: hidden;
442
+ }
443
+
444
+ .ac-bd-bar {
445
+ height: 100%;
446
+ border-radius: 3px;
447
+ background: linear-gradient(90deg, #0ea5e9, #22c55e);
448
+ transition: width 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
449
+ }
450
+
451
+ .ac-bd-val {
452
+ font-size: 0.6875rem;
453
+ font-weight: 800;
454
+ color: var(--ac-muted);
455
+ min-width: 48px;
456
+ text-align: right;
457
+ }
458
+
459
+ .ac-peak {
460
+ animation: ac-peak-in 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
461
+ }
462
+
463
+ @keyframes ac-peak-in {
464
+ 0% { transform: scale(1); }
465
+ 50% { transform: scale(1.02); }
466
+ 100% { transform: scale(1); }
467
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { acTonnageCalculator } 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 acTonnageCalculator.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,5 @@
1
+ export const bibliography = [
2
+ { name: 'ASHRAE Fundamentals Handbook 2021', url: 'https://www.ashrae.org/technical-resources/ashrae-handbook' },
3
+ { name: 'Energy Star Room Air Conditioners', url: 'https://www.energystar.gov/products/room_air_conditioners' },
4
+ { name: 'Carrier BTU Sizing Guide', url: 'https://www.carrier.com/residential/en/us/products/air-conditioners/' },
5
+ ];
@@ -0,0 +1,80 @@
1
+ function getLabels(): Record<string, string> {
2
+ const el = document.getElementById('ac-labels');
3
+ if (!el) return {};
4
+ try {
5
+ return JSON.parse(el.dataset.labels ?? '{}');
6
+ } catch {
7
+ return {};
8
+ }
9
+ }
10
+
11
+ export function updateTheme(btu: number) {
12
+ const card = document.getElementById('ac-card');
13
+ if (!card) return;
14
+ card.classList.remove('ac-theme-low', 'ac-theme-med', 'ac-theme-high');
15
+ if (btu < 18000) card.classList.add('ac-theme-low');
16
+ else if (btu < 36000) card.classList.add('ac-theme-med');
17
+ else card.classList.add('ac-theme-high');
18
+ }
19
+
20
+ function roomLabel(btu: number): string {
21
+ const labels = getLabels();
22
+ if (btu < 18000) return labels.labelRoomCozy ?? '';
23
+ if (btu < 36000) return labels.labelRoomWarm ?? '';
24
+ return labels.labelRoomHot ?? '';
25
+ }
26
+
27
+ export function updateMiniRoom(btu: number) {
28
+ const svg = document.querySelector('.ac-mini-room svg');
29
+ if (!svg) return;
30
+ const txt = svg.querySelector('text');
31
+ if (txt) txt.textContent = roomLabel(btu);
32
+ }
33
+
34
+ export function spawnRipple() {
35
+ const wrap = document.querySelector('.ac-ring-wrap');
36
+ if (!wrap) return;
37
+ const el = document.getElementById('ac-ring-fill');
38
+ const color = el ? getComputedStyle(el).stroke : '#0ea5e9';
39
+ const ripple = document.createElement('div');
40
+ ripple.className = 'ac-ripple';
41
+ ripple.style.left = '50%';
42
+ ripple.style.top = '50%';
43
+ ripple.style.transform = 'translate(-50%, -50%)';
44
+ ripple.style.border = `2px solid ${color}`;
45
+ wrap.appendChild(ripple);
46
+ setTimeout(() => ripple.remove(), 800);
47
+ }
48
+
49
+ export function pulseGlow() {
50
+ const glow = document.getElementById('ac-ring-glow');
51
+ if (!glow) return;
52
+ glow.classList.add('active');
53
+ setTimeout(() => glow.classList.remove('active'), 2000);
54
+ }
55
+
56
+ function makeParticle(): HTMLElement {
57
+ const p = document.createElement('div');
58
+ p.className = 'ac-particle';
59
+ p.style.left = `${Math.random() * 100}%`;
60
+ p.style.animationDuration = `${2 + Math.random() * 3}s`;
61
+ p.style.animationDelay = `${Math.random() * 2}s`;
62
+ return p;
63
+ }
64
+
65
+ export function spawnParticles() {
66
+ const container = document.getElementById('ac-particles');
67
+ if (!container) return;
68
+ container.innerHTML = '';
69
+ for (let i = 0; i < 12; i++) {
70
+ container.appendChild(makeParticle());
71
+ }
72
+ }
73
+
74
+ export function buildBdRow(key: string, val: number, total: number): string {
75
+ const labels = getLabels();
76
+ const labelKey = `bd${key.charAt(0).toUpperCase()}${key.slice(1)}`;
77
+ const label = labels[labelKey] ?? key;
78
+ const pct = total > 0 ? Math.min(100, (val / total) * 100) : 0;
79
+ return `<div class="ac-bd-row"><span class="ac-bd-label">${label}</span><div class="ac-bd-bar-wrap"><div class="ac-bd-bar" style="width:${pct}%"></div></div><span class="ac-bd-val">${val.toLocaleString()}</span></div>`;
80
+ }