@jjlmoya/utils-hardware 1.20.0 → 1.21.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/category/index.ts +2 -1
  3. package/src/entries.ts +4 -1
  4. package/src/index.ts +1 -0
  5. package/src/tests/locale_completeness.test.ts +2 -2
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/upsRuntimeCalculator/bibliography.astro +14 -0
  8. package/src/tool/upsRuntimeCalculator/bibliography.ts +16 -0
  9. package/src/tool/upsRuntimeCalculator/component.astro +384 -0
  10. package/src/tool/upsRuntimeCalculator/entry.ts +29 -0
  11. package/src/tool/upsRuntimeCalculator/i18n/de.ts +208 -0
  12. package/src/tool/upsRuntimeCalculator/i18n/en.ts +208 -0
  13. package/src/tool/upsRuntimeCalculator/i18n/es.ts +208 -0
  14. package/src/tool/upsRuntimeCalculator/i18n/fr.ts +208 -0
  15. package/src/tool/upsRuntimeCalculator/i18n/id.ts +208 -0
  16. package/src/tool/upsRuntimeCalculator/i18n/it.ts +208 -0
  17. package/src/tool/upsRuntimeCalculator/i18n/ja.ts +208 -0
  18. package/src/tool/upsRuntimeCalculator/i18n/ko.ts +208 -0
  19. package/src/tool/upsRuntimeCalculator/i18n/nl.ts +208 -0
  20. package/src/tool/upsRuntimeCalculator/i18n/pl.ts +208 -0
  21. package/src/tool/upsRuntimeCalculator/i18n/pt.ts +208 -0
  22. package/src/tool/upsRuntimeCalculator/i18n/ru.ts +208 -0
  23. package/src/tool/upsRuntimeCalculator/i18n/sv.ts +208 -0
  24. package/src/tool/upsRuntimeCalculator/i18n/tr.ts +208 -0
  25. package/src/tool/upsRuntimeCalculator/i18n/zh.ts +208 -0
  26. package/src/tool/upsRuntimeCalculator/index.ts +11 -0
  27. package/src/tool/upsRuntimeCalculator/logic.ts +48 -0
  28. package/src/tool/upsRuntimeCalculator/seo.astro +15 -0
  29. package/src/tool/upsRuntimeCalculator/ui.ts +31 -0
  30. package/src/tool/upsRuntimeCalculator/ups-runtime-calculator.css +530 -0
  31. package/src/tools.ts +2 -1
@@ -0,0 +1,48 @@
1
+ export interface UpsLoadItem {
2
+ name: string;
3
+ watts: number;
4
+ }
5
+
6
+ export interface UpsRuntimeInput {
7
+ loadItems: UpsLoadItem[];
8
+ batteryWh: number;
9
+ inverterEfficiency: number;
10
+ powerFactor: number;
11
+ reservePercent: number;
12
+ }
13
+
14
+ export interface UpsRuntimeResult {
15
+ totalWatts: number;
16
+ usableWh: number;
17
+ runtimeMinutes: number;
18
+ recommendedWatts: number;
19
+ recommendedVa: number;
20
+ loadBand: 'light' | 'balanced' | 'heavy';
21
+ }
22
+
23
+ export function calculateUpsRuntime(input: UpsRuntimeInput): UpsRuntimeResult {
24
+ const totalWatts = input.loadItems.reduce((sum, item) => sum + Math.max(0, item.watts), 0);
25
+ const efficiency = Math.min(0.98, Math.max(0.5, input.inverterEfficiency));
26
+ const reserve = Math.min(0.8, Math.max(0, input.reservePercent / 100));
27
+ const powerFactor = Math.min(1, Math.max(0.4, input.powerFactor));
28
+ const usableWh = Math.max(0, input.batteryWh) * efficiency * (1 - reserve);
29
+ const runtimeMinutes = totalWatts > 0 ? (usableWh / totalWatts) * 60 : 0;
30
+ const recommendedWatts = Math.ceil((totalWatts * 1.25) / 10) * 10;
31
+ const recommendedVa = Math.ceil((recommendedWatts / powerFactor) / 50) * 50;
32
+ const loadRatio = totalWatts / Math.max(1, recommendedWatts);
33
+ let loadBand: UpsRuntimeResult['loadBand'] = 'heavy';
34
+ if (loadRatio < 0.45) {
35
+ loadBand = 'light';
36
+ } else if (loadRatio < 0.78) {
37
+ loadBand = 'balanced';
38
+ }
39
+
40
+ return {
41
+ totalWatts,
42
+ usableWh,
43
+ runtimeMinutes,
44
+ recommendedWatts,
45
+ recommendedVa,
46
+ loadBand,
47
+ };
48
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { upsRuntimeCalculator } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const content = await upsRuntimeCalculator.i18n[locale]?.();
12
+ if (!content) return null;
13
+ ---
14
+
15
+ {content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,31 @@
1
+ export interface UpsRuntimeCalculatorUI extends Record<string, string> {
2
+ loadTitle: string;
3
+ addDevice: string;
4
+ deviceName: string;
5
+ watts: string;
6
+ remove: string;
7
+ batteryWh: string;
8
+ efficiency: string;
9
+ powerFactor: string;
10
+ reserve: string;
11
+ totalLoad: string;
12
+ runtime: string;
13
+ recommendedUps: string;
14
+ usableEnergy: string;
15
+ minutes: string;
16
+ hours: string;
17
+ wattsUnit: string;
18
+ vaUnit: string;
19
+ whUnit: string;
20
+ percentUnit: string;
21
+ assumptionsLabel: string;
22
+ presetDesktop: string;
23
+ presetMonitor: string;
24
+ presetRouter: string;
25
+ presetNas: string;
26
+ percentUnit: string;
27
+ bandLight: string;
28
+ bandBalanced: string;
29
+ bandHeavy: string;
30
+ summaryPrefix: string;
31
+ }
@@ -0,0 +1,530 @@
1
+ .ups-root {
2
+ --ups-ink: #111827;
3
+ --ups-muted: #667085;
4
+ --ups-soft: #f6f8fb;
5
+ --ups-card: #fff;
6
+ --ups-line: #d7deea;
7
+ --ups-accent: #0f9f7a;
8
+ --ups-accent-soft: #e7f6f1;
9
+ --ups-warn: #a16207;
10
+ --ups-warn-soft: #fff7e6;
11
+ --ups-shadow: rgb(17, 24, 39, 0.08);
12
+
13
+ color: var(--ups-ink);
14
+ }
15
+
16
+ .theme-dark .ups-root {
17
+ --ups-ink: #f7fafc;
18
+ --ups-muted: #a7b0bf;
19
+ --ups-soft: #151b24;
20
+ --ups-card: #1d2430;
21
+ --ups-line: #334155;
22
+ --ups-accent: #35c69b;
23
+ --ups-accent-soft: #18362e;
24
+ --ups-warn: #f4c76b;
25
+ --ups-warn-soft: #352817;
26
+ --ups-shadow: rgb(0, 0, 0, 0.24);
27
+ }
28
+
29
+ .ups-panel {
30
+ display: grid;
31
+ gap: 0.75rem;
32
+ width: 100%;
33
+ padding: 0.75rem;
34
+ border: 1px solid var(--ups-line);
35
+ border-radius: 8px;
36
+ background: var(--ups-card);
37
+ box-shadow: 0 1rem 2.5rem var(--ups-shadow);
38
+ animation: ups-rise 420ms ease-out both;
39
+ }
40
+
41
+ .ups-console,
42
+ .ups-dashboard,
43
+ .ups-machine {
44
+ min-width: 0;
45
+ }
46
+
47
+ .ups-console,
48
+ .ups-dashboard {
49
+ border: 1px solid var(--ups-line);
50
+ border-radius: 8px;
51
+ background: var(--ups-card);
52
+ transition: border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease;
53
+ }
54
+
55
+ .ups-console:focus-within,
56
+ .ups-dashboard:hover,
57
+ .ups-machine:hover {
58
+ border-color: color-mix(in srgb, var(--ups-accent) 42%, var(--ups-line));
59
+ box-shadow: 0 0.75rem 1.75rem var(--ups-shadow);
60
+ transform: translateY(-1px);
61
+ }
62
+
63
+ .ups-strip {
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: space-between;
67
+ gap: 0.75rem;
68
+ padding: 0.7rem 0.75rem 0.35rem;
69
+ color: var(--ups-muted);
70
+ font-size: 0.78rem;
71
+ font-weight: 850;
72
+ }
73
+
74
+ .ups-add,
75
+ .ups-device button {
76
+ display: grid;
77
+ place-items: center;
78
+ min-height: 2rem;
79
+ border: 1px solid var(--ups-line);
80
+ border-radius: 8px;
81
+ background: var(--ups-ink);
82
+ color: var(--ups-card);
83
+ font: inherit;
84
+ font-size: 0.8rem;
85
+ font-weight: 800;
86
+ cursor: pointer;
87
+ transition: background 160ms ease, transform 160ms ease, border-color 160ms ease;
88
+ }
89
+
90
+ .ups-add:hover,
91
+ .ups-device button:hover {
92
+ background: var(--ups-accent);
93
+ border-color: var(--ups-accent);
94
+ transform: translateY(-1px);
95
+ }
96
+
97
+ .ups-add {
98
+ padding: 0 0.75rem;
99
+ white-space: nowrap;
100
+ }
101
+
102
+ .ups-devices {
103
+ display: grid;
104
+ gap: 0.45rem;
105
+ padding: 0.5rem 0.75rem 0.75rem;
106
+ }
107
+
108
+ .ups-device {
109
+ display: grid;
110
+ grid-template-columns: minmax(0, 1fr) 7.2rem 2.25rem;
111
+ gap: 0.45rem;
112
+ align-items: end;
113
+ }
114
+
115
+ .ups-number-field input {
116
+ width: 100%;
117
+ min-height: 2.25rem;
118
+ padding: 0 0 0 0.65rem;
119
+ border: 0;
120
+ background: transparent;
121
+ color: var(--ups-ink);
122
+ font: inherit;
123
+ font-weight: 800;
124
+ }
125
+
126
+ .ups-number-field input:focus {
127
+ outline: 0;
128
+ }
129
+
130
+ .ups-number-field:focus-within {
131
+ border-color: var(--ups-accent);
132
+ background: var(--ups-card);
133
+ box-shadow: 0 0 0 3px var(--ups-accent-soft);
134
+ }
135
+
136
+ .ups-device-name {
137
+ width: 100%;
138
+ min-height: 2.25rem;
139
+ padding: 0 0.65rem;
140
+ border: 1px solid var(--ups-line);
141
+ border-radius: 8px;
142
+ background: var(--ups-soft);
143
+ color: var(--ups-ink);
144
+ font: inherit;
145
+ font-weight: 750;
146
+ transition: border-color 160ms ease, background 160ms ease, box-shadow 160ms ease;
147
+ }
148
+
149
+ .ups-device-name:focus {
150
+ outline: 0;
151
+ border-color: var(--ups-accent);
152
+ background: var(--ups-card);
153
+ box-shadow: 0 0 0 3px var(--ups-accent-soft);
154
+ }
155
+
156
+ .ups-number-field {
157
+ display: grid;
158
+ grid-template-columns: minmax(0, 1fr) auto;
159
+ align-items: center;
160
+ min-height: 2.25rem;
161
+ border: 1px solid var(--ups-line);
162
+ border-radius: 8px;
163
+ background: var(--ups-soft);
164
+ transition: border-color 160ms ease, background 160ms ease, box-shadow 160ms ease;
165
+ }
166
+
167
+ .ups-number-field em {
168
+ padding: 0 0.65rem 0 0.35rem;
169
+ color: var(--ups-muted);
170
+ font-size: 0.72rem;
171
+ font-style: normal;
172
+ font-weight: 900;
173
+ }
174
+
175
+ .ups-number-field-large {
176
+ min-height: 2.65rem;
177
+ }
178
+
179
+ .ups-number-field-large input {
180
+ font-size: 1.05rem;
181
+ }
182
+
183
+ .ups-device label,
184
+ .ups-controls label {
185
+ display: grid;
186
+ gap: 0.22rem;
187
+ color: var(--ups-muted);
188
+ font-size: 0.68rem;
189
+ font-weight: 800;
190
+ }
191
+
192
+ .ups-device button {
193
+ width: 2rem;
194
+ height: 2.25rem;
195
+ padding: 0;
196
+ }
197
+
198
+ .ups-trash-icon {
199
+ width: 1.05rem;
200
+ height: 1.05rem;
201
+ }
202
+
203
+ .ups-machine {
204
+ display: grid;
205
+ align-content: stretch;
206
+ gap: 0.65rem;
207
+ border: 1px solid var(--ups-line);
208
+ border-radius: 8px;
209
+ background: var(--ups-soft);
210
+ padding: 0.75rem;
211
+ transition: border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease;
212
+ }
213
+
214
+ .ups-shell {
215
+ position: relative;
216
+ display: grid;
217
+ place-items: center;
218
+ min-height: 13rem;
219
+ border: 1px solid var(--ups-line);
220
+ border-radius: 8px;
221
+ background: var(--ups-card);
222
+ overflow: hidden;
223
+ }
224
+
225
+ .ups-shell::before,
226
+ .ups-shell::after {
227
+ position: absolute;
228
+ content: "";
229
+ }
230
+
231
+ .ups-shell::before {
232
+ inset: 0.85rem;
233
+ border: 1px solid var(--ups-line);
234
+ border-radius: 8px;
235
+ }
236
+
237
+ .ups-shell::after {
238
+ right: 1rem;
239
+ bottom: 1rem;
240
+ width: 0.65rem;
241
+ height: 0.65rem;
242
+ border-radius: 50%;
243
+ background: var(--ups-accent);
244
+ animation: ups-blink 1400ms ease-in-out infinite;
245
+ }
246
+
247
+ .ups-port-row,
248
+ .ups-core {
249
+ display: none;
250
+ }
251
+
252
+ .ups-screen {
253
+ position: relative;
254
+ z-index: 1;
255
+ display: grid;
256
+ place-items: center;
257
+ gap: 0.15rem;
258
+ animation: ups-breathe 2200ms ease-in-out infinite;
259
+ }
260
+
261
+ .ups-screen span {
262
+ color: var(--ups-ink);
263
+ font-size: 4.8rem;
264
+ font-weight: 900;
265
+ line-height: 0.9;
266
+ }
267
+
268
+ .ups-screen small {
269
+ color: var(--ups-muted);
270
+ font-size: 0.78rem;
271
+ font-weight: 850;
272
+ }
273
+
274
+ .ups-energy-rail {
275
+ position: relative;
276
+ height: 0.55rem;
277
+ overflow: hidden;
278
+ border-radius: 8px;
279
+ background: var(--ups-line);
280
+ }
281
+
282
+ .ups-energy-rail span {
283
+ display: block;
284
+ width: var(--ups-load-fill);
285
+ height: 100%;
286
+ border-radius: inherit;
287
+ background: var(--ups-accent);
288
+ transition: width 260ms ease;
289
+ }
290
+
291
+ .ups-energy-rail::after {
292
+ position: absolute;
293
+ inset: 0;
294
+ background: color-mix(in srgb, var(--ups-card) 36%, transparent);
295
+ content: "";
296
+ transform: translateX(-100%);
297
+ animation: ups-flow 1800ms ease-in-out infinite;
298
+ }
299
+
300
+ .ups-controls {
301
+ display: grid;
302
+ gap: 0.65rem;
303
+ padding: 0.75rem;
304
+ }
305
+
306
+ .ups-controls label {
307
+ grid-template-columns: minmax(8.8rem, 1.1fr) minmax(0, 1.4fr) 3rem;
308
+ align-items: center;
309
+ }
310
+
311
+ .ups-controls .ups-capacity {
312
+ grid-template-columns: minmax(11.5rem, 1.25fr) minmax(9.5rem, 0.95fr);
313
+ gap: 0.6rem;
314
+ padding: 0.65rem;
315
+ border: 1px solid var(--ups-line);
316
+ border-radius: 8px;
317
+ background: var(--ups-soft);
318
+ }
319
+
320
+ .ups-controls .ups-capacity .ups-number-field {
321
+ background: var(--ups-card);
322
+ }
323
+
324
+ .ups-controls input[type="range"] {
325
+ width: 100%;
326
+ accent-color: var(--ups-accent);
327
+ cursor: pointer;
328
+ }
329
+
330
+ .ups-controls strong {
331
+ color: var(--ups-ink);
332
+ text-align: right;
333
+ }
334
+
335
+ .ups-dashboard {
336
+ display: grid;
337
+ gap: 0.65rem;
338
+ padding: 0.75rem;
339
+ }
340
+
341
+ .ups-metrics {
342
+ display: grid;
343
+ gap: 0.55rem;
344
+ }
345
+
346
+ .ups-metrics article {
347
+ display: grid;
348
+ gap: 0.28rem;
349
+ min-height: 4.5rem;
350
+ padding: 0.7rem;
351
+ border: 1px solid var(--ups-line);
352
+ border-radius: 8px;
353
+ background: var(--ups-soft);
354
+ transition: background 160ms ease, border-color 160ms ease, transform 160ms ease;
355
+ }
356
+
357
+ .ups-metrics article:hover {
358
+ border-color: color-mix(in srgb, var(--ups-accent) 42%, var(--ups-line));
359
+ background: var(--ups-accent-soft);
360
+ transform: translateY(-1px);
361
+ }
362
+
363
+ .ups-metrics span {
364
+ color: var(--ups-muted);
365
+ font-size: 0.68rem;
366
+ font-weight: 850;
367
+ }
368
+
369
+ .ups-metrics strong {
370
+ font-size: 1.35rem;
371
+ line-height: 1.05;
372
+ }
373
+
374
+ .ups-metrics i {
375
+ display: block;
376
+ width: 100%;
377
+ height: 0.3rem;
378
+ overflow: hidden;
379
+ border-radius: 8px;
380
+ background: var(--ups-line);
381
+ }
382
+
383
+ .ups-metrics i::before {
384
+ display: block;
385
+ width: var(--meter-fill, 40%);
386
+ height: 100%;
387
+ border-radius: inherit;
388
+ background: var(--ups-accent);
389
+ content: "";
390
+ transition: width 260ms ease;
391
+ }
392
+
393
+ .ups-summary {
394
+ margin: 0;
395
+ padding: 0.75rem;
396
+ border: 1px solid color-mix(in srgb, var(--ups-warn) 34%, var(--ups-line));
397
+ border-radius: 8px;
398
+ background: var(--ups-warn-soft);
399
+ color: var(--ups-ink);
400
+ font-size: 0.9rem;
401
+ font-weight: 750;
402
+ line-height: 1.25;
403
+ animation: ups-soft-pop 520ms ease both;
404
+ }
405
+
406
+ @media (min-width: 820px) {
407
+ .ups-panel {
408
+ grid-template-columns: minmax(18rem, 1fr) minmax(13rem, 0.75fr) minmax(18rem, 1fr);
409
+ grid-template-areas:
410
+ "load machine dashboard"
411
+ "controls machine dashboard";
412
+ align-items: stretch;
413
+ }
414
+
415
+ .ups-load {
416
+ grid-area: load;
417
+ }
418
+
419
+ .ups-controls {
420
+ grid-area: controls;
421
+ }
422
+
423
+ .ups-machine {
424
+ grid-area: machine;
425
+ }
426
+
427
+ .ups-dashboard {
428
+ grid-area: dashboard;
429
+ }
430
+ }
431
+
432
+ @media (min-width: 1080px) {
433
+ .ups-metrics {
434
+ grid-template-columns: repeat(2, minmax(0, 1fr));
435
+ }
436
+ }
437
+
438
+ @media (max-width: 560px) {
439
+ .ups-device {
440
+ grid-template-columns: minmax(0, 1fr) 6.4rem 2.25rem;
441
+ }
442
+
443
+ .ups-controls label {
444
+ grid-template-columns: 1fr;
445
+ }
446
+
447
+ .ups-controls .ups-capacity {
448
+ grid-template-columns: 1fr;
449
+ }
450
+
451
+ .ups-controls strong {
452
+ text-align: left;
453
+ }
454
+ }
455
+
456
+ @keyframes ups-rise {
457
+ from {
458
+ opacity: 0;
459
+ transform: translateY(10px);
460
+ }
461
+
462
+ to {
463
+ opacity: 1;
464
+ transform: translateY(0);
465
+ }
466
+ }
467
+
468
+ @keyframes ups-breathe {
469
+ 0%,
470
+ 100% {
471
+ transform: scale(1);
472
+ }
473
+
474
+ 50% {
475
+ transform: scale(1.025);
476
+ }
477
+ }
478
+
479
+ @keyframes ups-blink {
480
+ 0%,
481
+ 100% {
482
+ opacity: 0.35;
483
+ transform: scale(0.8);
484
+ }
485
+
486
+ 50% {
487
+ opacity: 1;
488
+ transform: scale(1);
489
+ }
490
+ }
491
+
492
+ @keyframes ups-flow {
493
+ 0% {
494
+ transform: translateX(-100%);
495
+ }
496
+
497
+ 55%,
498
+ 100% {
499
+ transform: translateX(100%);
500
+ }
501
+ }
502
+
503
+ @keyframes ups-soft-pop {
504
+ from {
505
+ opacity: 0;
506
+ }
507
+
508
+ to {
509
+ opacity: 1;
510
+ }
511
+ }
512
+
513
+ @media (prefers-reduced-motion: reduce) {
514
+ .ups-panel,
515
+ .ups-screen,
516
+ .ups-shell::after,
517
+ .ups-energy-rail::after,
518
+ .ups-summary {
519
+ animation: none;
520
+ }
521
+
522
+ .ups-console,
523
+ .ups-dashboard,
524
+ .ups-machine,
525
+ .ups-add,
526
+ .ups-device button,
527
+ .ups-metrics article {
528
+ transition: none;
529
+ }
530
+ }
package/src/tools.ts CHANGED
@@ -11,5 +11,6 @@ import { REFRESH_RATE_DETECTOR_TOOL } from './tool/refreshRateDetector/index';
11
11
  import { SPECTRUM_CANVAS_TOOL } from './tool/colorAccuracyTest/index';
12
12
  import { MOUSE_DOUBLE_CLICK_TEST_TOOL } from './tool/mouseDoubleClickTest/index';
13
13
  import { MONITOR_GHOSTING_TEST_TOOL } from './tool/monitorGhostingTest/index';
14
+ import { UPS_RUNTIME_CALCULATOR_TOOL } from './tool/upsRuntimeCalculator/index';
14
15
 
15
- export const ALL_TOOLS: ToolDefinition[] = [PIXELES_PANTALLA_TOOL, TEST_TECLADO_TOOL, TEST_MANDO_TOOL, PROBADOR_VIBRACION_MANDO_TOOL, TEST_RATON_TOOL, MOUSE_DOUBLE_CLICK_TEST_TOOL, ESTIMADOR_SALUD_BATERIA_TOOL, TONE_GENERATOR_TOOL, REFRESH_RATE_DETECTOR_TOOL, MONITOR_GHOSTING_TEST_TOOL, SPECTRUM_CANVAS_TOOL];
16
+ export const ALL_TOOLS: ToolDefinition[] = [PIXELES_PANTALLA_TOOL, TEST_TECLADO_TOOL, TEST_MANDO_TOOL, PROBADOR_VIBRACION_MANDO_TOOL, TEST_RATON_TOOL, MOUSE_DOUBLE_CLICK_TEST_TOOL, ESTIMADOR_SALUD_BATERIA_TOOL, TONE_GENERATOR_TOOL, REFRESH_RATE_DETECTOR_TOOL, MONITOR_GHOSTING_TEST_TOOL, SPECTRUM_CANVAS_TOOL, UPS_RUNTIME_CALCULATOR_TOOL];