@jjlmoya/utils-nature 1.1.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 (61) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +110 -0
  3. package/src/category/i18n/es.ts +127 -0
  4. package/src/category/i18n/fr.ts +110 -0
  5. package/src/category/index.ts +14 -0
  6. package/src/category/seo.astro +15 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +11 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +30 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +19 -0
  17. package/src/tests/locale_completeness.test.ts +42 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/no_h1_in_components.test.ts +48 -0
  20. package/src/tests/schemas_fulfillment.test.ts +23 -0
  21. package/src/tests/seo_length.test.ts +22 -0
  22. package/src/tests/title_quality.test.ts +55 -0
  23. package/src/tests/tool_validation.test.ts +17 -0
  24. package/src/tool/cricketThermometer/bibliography.astro +14 -0
  25. package/src/tool/cricketThermometer/component.astro +549 -0
  26. package/src/tool/cricketThermometer/i18n/en.ts +181 -0
  27. package/src/tool/cricketThermometer/i18n/es.ts +181 -0
  28. package/src/tool/cricketThermometer/i18n/fr.ts +181 -0
  29. package/src/tool/cricketThermometer/index.ts +34 -0
  30. package/src/tool/cricketThermometer/logic.ts +6 -0
  31. package/src/tool/cricketThermometer/seo.astro +15 -0
  32. package/src/tool/cricketThermometer/ui.ts +11 -0
  33. package/src/tool/digitalCarbon/bibliography.astro +9 -0
  34. package/src/tool/digitalCarbon/component.astro +582 -0
  35. package/src/tool/digitalCarbon/i18n/en.ts +235 -0
  36. package/src/tool/digitalCarbon/i18n/es.ts +235 -0
  37. package/src/tool/digitalCarbon/i18n/fr.ts +235 -0
  38. package/src/tool/digitalCarbon/index.ts +33 -0
  39. package/src/tool/digitalCarbon/logic.ts +107 -0
  40. package/src/tool/digitalCarbon/seo.astro +14 -0
  41. package/src/tool/digitalCarbon/ui.ts +38 -0
  42. package/src/tool/rainHarvester/bibliography.astro +9 -0
  43. package/src/tool/rainHarvester/component.astro +559 -0
  44. package/src/tool/rainHarvester/i18n/en.ts +185 -0
  45. package/src/tool/rainHarvester/i18n/es.ts +185 -0
  46. package/src/tool/rainHarvester/i18n/fr.ts +185 -0
  47. package/src/tool/rainHarvester/index.ts +33 -0
  48. package/src/tool/rainHarvester/logic.ts +12 -0
  49. package/src/tool/rainHarvester/seo.astro +14 -0
  50. package/src/tool/rainHarvester/ui.ts +23 -0
  51. package/src/tool/seedCalculator/bibliography.astro +8 -0
  52. package/src/tool/seedCalculator/component.astro +812 -0
  53. package/src/tool/seedCalculator/i18n/en.ts +213 -0
  54. package/src/tool/seedCalculator/i18n/es.ts +213 -0
  55. package/src/tool/seedCalculator/i18n/fr.ts +213 -0
  56. package/src/tool/seedCalculator/index.ts +34 -0
  57. package/src/tool/seedCalculator/logic.ts +19 -0
  58. package/src/tool/seedCalculator/seo.astro +9 -0
  59. package/src/tool/seedCalculator/ui.ts +39 -0
  60. package/src/tools.ts +12 -0
  61. package/src/types.ts +72 -0
@@ -0,0 +1,812 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+ import { seedCalculator } from './index';
4
+ import { CROPS, SPEED_OPTIONS } from './logic';
5
+
6
+ const { ui: propUi, locale = 'es' } = Astro.props;
7
+ let ui = propUi;
8
+
9
+ if (!ui) {
10
+ const content = await seedCalculator.i18n[locale as keyof typeof seedCalculator.i18n]?.();
11
+ ui = content?.ui;
12
+ }
13
+
14
+ if (!ui) return null;
15
+
16
+ const cropBg: Record<string, string> = {
17
+ corn: 'rgba(234,179,8,0.15)',
18
+ silage: 'rgba(101,163,13,0.15)',
19
+ sunflower: 'rgba(249,115,22,0.15)',
20
+ sorghum: 'rgba(180,83,9,0.15)',
21
+ soy: 'rgba(22,163,74,0.15)',
22
+ beet: 'rgba(244,63,94,0.15)',
23
+ rapeseed: 'rgba(202,138,4,0.15)',
24
+ };
25
+
26
+ const cropColor: Record<string, string> = {
27
+ corn: '#a16207',
28
+ silage: '#3f6212',
29
+ sunflower: '#c2410c',
30
+ sorghum: '#92400e',
31
+ soy: '#166534',
32
+ beet: '#be123c',
33
+ rapeseed: '#92400e',
34
+ };
35
+
36
+ const cropName: Record<string, string> = {
37
+ corn: ui.cropCorn,
38
+ silage: ui.cropSilage,
39
+ sunflower: ui.cropSunflower,
40
+ sorghum: ui.cropSorghum,
41
+ soy: ui.cropSoy,
42
+ beet: ui.cropBeet,
43
+ rapeseed: ui.cropRapeseed,
44
+ };
45
+
46
+ const cropNote: Record<string, string> = {
47
+ corn: ui.noteCorn,
48
+ silage: ui.noteSilage,
49
+ sunflower: ui.noteSunflower,
50
+ sorghum: ui.noteSorghum,
51
+ soy: ui.noteSoy,
52
+ beet: ui.noteBeet,
53
+ rapeseed: ui.noteRapeseed,
54
+ };
55
+ ---
56
+
57
+ <calc-sembradora
58
+ class="sc-wrap"
59
+ data-status-standby={ui.statusStandby}
60
+ data-status-volumetric={ui.statusVolumetric}
61
+ data-status-optimal={ui.statusOptimal}
62
+ data-status-high-speed={ui.statusHighSpeed}
63
+ data-status-plate-limiter={ui.statusPlateLimiter}
64
+ >
65
+ <section class="sc-section">
66
+ <h2 class="sc-section-head">
67
+ <span class="sc-step-badge">1</span>
68
+ {ui.headCrop}
69
+ </h2>
70
+
71
+ <div class="sc-crop-grid">
72
+ {CROPS.map((c) => (
73
+ <button
74
+ class="crop-btn"
75
+ data-id={c.id}
76
+ data-pop={c.pop}
77
+ data-row={c.row}
78
+ >
79
+ <div class="sc-crop-icon" style={`background-color:${cropBg[c.id]}`}>
80
+ <Icon
81
+ name={c.icon}
82
+ class="sc-crop-glyph"
83
+ style={`color:${cropColor[c.id]}`}
84
+ width="24"
85
+ height="24"
86
+ aria-hidden="true"
87
+ />
88
+ </div>
89
+ <span class="sc-crop-name">{cropName[c.id]}</span>
90
+ <span class="sc-crop-note">{cropNote[c.id]}</span>
91
+ <div class="sc-selection-ring"></div>
92
+ </button>
93
+ ))}
94
+ </div>
95
+ </section>
96
+
97
+ <div class="sc-dashboard" data-disabled id="mainDash">
98
+ <div class="sc-params-col">
99
+ <h2 class="sc-section-head">
100
+ <span class="sc-step-badge">2</span>
101
+ {ui.headParams}
102
+ </h2>
103
+
104
+ <div class="sc-params-card">
105
+ <div class="sc-slider-group">
106
+ <label class="sc-slider-label">
107
+ {ui.labelPopulation}
108
+ <span class="sc-label-unit">{ui.unitSeedsHa}</span>
109
+ </label>
110
+ <div class="sc-slider-row">
111
+ <input
112
+ type="range"
113
+ id="inPop"
114
+ min="10000"
115
+ max="600000"
116
+ step="1000"
117
+ class="sc-range"
118
+ />
119
+ <span id="valPop" class="sc-range-val">0</span>
120
+ </div>
121
+ </div>
122
+
123
+ <div class="sc-slider-group">
124
+ <label class="sc-slider-label">
125
+ {ui.labelRowWidth}
126
+ <span class="sc-label-unit">{ui.unitCm}</span>
127
+ </label>
128
+ <div class="sc-slider-row">
129
+ <input
130
+ type="range"
131
+ id="inRow"
132
+ min="15"
133
+ max="100"
134
+ step="1"
135
+ class="sc-range"
136
+ />
137
+ <span id="valRow" class="sc-range-val">0</span>
138
+ </div>
139
+ </div>
140
+
141
+ <div class="sc-speed-group">
142
+ <label class="sc-slider-label">
143
+ {ui.labelWorkSpeed}
144
+ <span class="sc-label-unit sc-label-unit-speed">{ui.unitKmh}</span>
145
+ </label>
146
+ <div class="sc-speed-bar">
147
+ {SPEED_OPTIONS.map((s) => (
148
+ <button class="speed-btn" data-speed={s}>{s}</button>
149
+ ))}
150
+ </div>
151
+ <input type="hidden" id="inSpeed" value="6" />
152
+ </div>
153
+ </div>
154
+ </div>
155
+
156
+ <div class="sc-analysis-col">
157
+ <h2 class="sc-section-head">
158
+ <span class="sc-step-badge">3</span>
159
+ {ui.headAnalysis}
160
+ </h2>
161
+
162
+ <div class="sc-result-card">
163
+ <div class="sc-result-main">
164
+ <div class="sc-result-label">{ui.labelCalibration}</div>
165
+ <div class="sc-result-value-row">
166
+ <span id="outSpacing" class="sc-result-big">--</span>
167
+ <span class="sc-result-unit">cm</span>
168
+ </div>
169
+ <p class="sc-result-desc">{ui.labelSpacingDesc}</p>
170
+ </div>
171
+
172
+ <div class="sc-hz-card">
173
+ <div class="sc-hz-header">
174
+ <Icon name="mdi:engine" class="sc-hz-icon" />
175
+ <span class="sc-hz-title">{ui.labelMachineStress}</span>
176
+ </div>
177
+ <div class="sc-hz-track">
178
+ <div id="hzBar" class="sc-hz-bar"></div>
179
+ </div>
180
+ <div class="sc-hz-footer">
181
+ <span id="hzVal" class="sc-hz-val">0 Hz</span>
182
+ <span id="hzStatus" class="sc-hz-status">{ui.statusStandby}</span>
183
+ </div>
184
+ </div>
185
+ </div>
186
+
187
+ <div class="sc-metrics-grid">
188
+ <div class="sc-metric-card">
189
+ <span id="outSeedsM" class="sc-metric-val">--</span>
190
+ <span class="sc-metric-label">{ui.labelSeedsM}</span>
191
+ </div>
192
+ <div class="sc-metric-card">
193
+ <span id="outPlantsM2" class="sc-metric-val">--</span>
194
+ <span class="sc-metric-label">{ui.labelPlantsM2}</span>
195
+ </div>
196
+ <div class="sc-metric-card">
197
+ <span id="outSpeedMs" class="sc-metric-val">--</span>
198
+ <span class="sc-metric-label">{ui.labelSpeedMs}</span>
199
+ </div>
200
+ <div class="sc-metric-card sc-metric-card-amber">
201
+ <span id="outHaBag" class="sc-metric-val sc-metric-val-amber">--</span>
202
+ <span class="sc-metric-label sc-metric-label-amber">{ui.labelHaBag}</span>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ </div>
207
+ </calc-sembradora>
208
+
209
+ <style>
210
+ .sc-wrap {
211
+ --sc-bg: #fff;
212
+ --sc-bg-params: #fff;
213
+ --sc-bg-speed: #f4f4f5;
214
+ --sc-border: #e4e4e7;
215
+ --sc-text: #18181b;
216
+ --sc-text-muted: #71717a;
217
+ --sc-accent: #059669;
218
+ --sc-accent-light: #d1fae5;
219
+ --sc-result-from: #059669;
220
+ --sc-result-to: #0d9488;
221
+ --sc-badge-bg: #000;
222
+ --sc-badge-text: #fff;
223
+ --sc-amber-bg: #fffbeb;
224
+ --sc-amber-border: #fde68a;
225
+ --sc-amber-text: #d97706;
226
+ --sc-range-track: #e4e4e7;
227
+
228
+ display: block;
229
+ max-width: 72rem;
230
+ margin: 0 auto;
231
+ padding: 1rem;
232
+ user-select: none;
233
+ }
234
+
235
+ :global(.theme-dark) .sc-wrap {
236
+ --sc-bg: #18181b;
237
+ --sc-bg-params: #18181b;
238
+ --sc-bg-speed: #27272a;
239
+ --sc-border: #3f3f46;
240
+ --sc-text: #fff;
241
+ --sc-text-muted: #a1a1aa;
242
+ --sc-badge-bg: #fff;
243
+ --sc-badge-text: #000;
244
+ --sc-amber-bg: rgba(120,53,15,0.1);
245
+ --sc-amber-border: #92400e;
246
+ --sc-amber-text: #fbbf24;
247
+ --sc-range-track: #3f3f46;
248
+ }
249
+
250
+ .sc-section {
251
+ margin-bottom: 3rem;
252
+ }
253
+
254
+ .sc-section-head {
255
+ font-size: 1.125rem;
256
+ font-weight: 700;
257
+ color: var(--sc-text);
258
+ display: flex;
259
+ align-items: center;
260
+ gap: 0.5rem;
261
+ margin-bottom: 1.5rem;
262
+ }
263
+
264
+ .sc-step-badge {
265
+ width: 2rem;
266
+ height: 2rem;
267
+ border-radius: 9999px;
268
+ background: var(--sc-badge-bg);
269
+ color: var(--sc-badge-text);
270
+ display: flex;
271
+ align-items: center;
272
+ justify-content: center;
273
+ font-size: 0.875rem;
274
+ font-weight: 700;
275
+ flex-shrink: 0;
276
+ }
277
+
278
+ .sc-crop-grid {
279
+ display: grid;
280
+ grid-template-columns: repeat(2, 1fr);
281
+ gap: 0.75rem;
282
+ }
283
+
284
+ @media (min-width: 768px) {
285
+ .sc-crop-grid {
286
+ grid-template-columns: repeat(4, 1fr);
287
+ }
288
+ }
289
+
290
+ @media (min-width: 1024px) {
291
+ .sc-crop-grid {
292
+ grid-template-columns: repeat(7, 1fr);
293
+ }
294
+ }
295
+
296
+ .crop-btn {
297
+ position: relative;
298
+ display: flex;
299
+ flex-direction: column;
300
+ align-items: center;
301
+ padding: 0.75rem;
302
+ border-radius: 1rem;
303
+ background: var(--sc-bg);
304
+ border: 2px solid transparent;
305
+ cursor: pointer;
306
+ box-shadow: 0 1px 3px rgba(0,0,0,0.08);
307
+ transition: border-color 0.15s, box-shadow 0.15s;
308
+ }
309
+
310
+ .crop-btn:hover {
311
+ border-color: var(--sc-border);
312
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
313
+ }
314
+
315
+ .sc-crop-icon {
316
+ width: 3rem;
317
+ height: 3rem;
318
+ border-radius: 9999px;
319
+ display: flex;
320
+ align-items: center;
321
+ justify-content: center;
322
+ margin-bottom: 0.75rem;
323
+ transition: filter 0.15s;
324
+ }
325
+
326
+ .sc-crop-glyph {
327
+ font-size: 1.5rem;
328
+ display: block;
329
+ }
330
+
331
+ .sc-crop-name {
332
+ font-size: 0.75rem;
333
+ font-weight: 700;
334
+ color: var(--sc-text);
335
+ text-align: center;
336
+ line-height: 1.2;
337
+ }
338
+
339
+ .sc-crop-note {
340
+ font-size: 0.625rem;
341
+ color: var(--sc-text-muted);
342
+ text-align: center;
343
+ margin-top: 0.25rem;
344
+ line-height: 1.2;
345
+ }
346
+
347
+ .sc-selection-ring {
348
+ position: absolute;
349
+ inset: 0;
350
+ border-radius: 1rem;
351
+ border: 2px solid transparent;
352
+ opacity: 0;
353
+ transform: scale(0.95);
354
+ transition: opacity 0.15s, transform 0.15s;
355
+ pointer-events: none;
356
+ }
357
+
358
+ :global(.crop-btn.active) .sc-selection-ring {
359
+ opacity: 1;
360
+ transform: scale(1);
361
+ border-width: 3px;
362
+ border-color: #10b981;
363
+ }
364
+
365
+ .sc-dashboard {
366
+ display: grid;
367
+ gap: 2rem;
368
+ transition: opacity 0.5s;
369
+ }
370
+
371
+ @media (min-width: 1024px) {
372
+ .sc-dashboard {
373
+ grid-template-columns: 1fr 2fr;
374
+ }
375
+ }
376
+
377
+ .sc-dashboard[data-disabled] {
378
+ opacity: 0.5;
379
+ pointer-events: none;
380
+ }
381
+
382
+ .sc-params-col,
383
+ .sc-analysis-col {
384
+ display: flex;
385
+ flex-direction: column;
386
+ gap: 1.5rem;
387
+ }
388
+
389
+ .sc-params-card {
390
+ background: var(--sc-bg-params);
391
+ padding: 1.5rem;
392
+ border-radius: 1.5rem;
393
+ box-shadow: 0 4px 20px rgba(0,0,0,0.08);
394
+ border: 1px solid var(--sc-border);
395
+ display: flex;
396
+ flex-direction: column;
397
+ gap: 2rem;
398
+ }
399
+
400
+ .sc-slider-group {
401
+ display: flex;
402
+ flex-direction: column;
403
+ gap: 0.5rem;
404
+ }
405
+
406
+ .sc-slider-label {
407
+ display: flex;
408
+ justify-content: space-between;
409
+ font-size: 0.75rem;
410
+ font-weight: 700;
411
+ color: var(--sc-text-muted);
412
+ text-transform: uppercase;
413
+ letter-spacing: 0.05em;
414
+ }
415
+
416
+ .sc-label-unit {
417
+ color: var(--sc-accent);
418
+ }
419
+
420
+ .sc-label-unit-speed {
421
+ color: #3b82f6;
422
+ }
423
+
424
+ .sc-slider-row {
425
+ display: flex;
426
+ align-items: center;
427
+ gap: 1rem;
428
+ }
429
+
430
+ .sc-range {
431
+ flex: 1;
432
+ height: 0.5rem;
433
+ background: var(--sc-range-track);
434
+ border-radius: 9999px;
435
+ appearance: none;
436
+ cursor: pointer;
437
+ accent-color: var(--sc-text);
438
+ }
439
+
440
+ .sc-range-val {
441
+ width: 6rem;
442
+ text-align: right;
443
+ font-weight: 900;
444
+ font-size: 1.25rem;
445
+ color: var(--sc-text);
446
+ font-variant-numeric: tabular-nums;
447
+ }
448
+
449
+ .sc-speed-group {
450
+ display: flex;
451
+ flex-direction: column;
452
+ gap: 1rem;
453
+ }
454
+
455
+ .sc-speed-bar {
456
+ display: flex;
457
+ justify-content: space-between;
458
+ gap: 0.25rem;
459
+ padding: 0.5rem;
460
+ background: var(--sc-bg-speed);
461
+ border-radius: 1rem;
462
+ }
463
+
464
+ .speed-btn {
465
+ flex: 1;
466
+ padding: 0.75rem 0;
467
+ border-radius: 0.75rem;
468
+ font-size: 0.875rem;
469
+ font-weight: 700;
470
+ color: var(--sc-text-muted);
471
+ background: transparent;
472
+ border: none;
473
+ cursor: pointer;
474
+ transition: background 0.15s, color 0.15s, box-shadow 0.15s;
475
+ }
476
+
477
+ .speed-btn:hover {
478
+ background: var(--sc-bg);
479
+ color: var(--sc-text);
480
+ }
481
+
482
+ :global(.speed-btn.active) {
483
+ background-color: #059669;
484
+ color: #fff;
485
+ box-shadow: 0 4px 6px -1px rgba(5,150,105,0.4);
486
+ }
487
+
488
+ .sc-result-card {
489
+ background: linear-gradient(to top right, var(--sc-result-from), var(--sc-result-to));
490
+ color: #fff;
491
+ padding: 2rem;
492
+ border-radius: 2rem;
493
+ box-shadow: 0 20px 40px rgba(5,150,105,0.25);
494
+ display: grid;
495
+ gap: 2rem;
496
+ }
497
+
498
+ @media (min-width: 768px) {
499
+ .sc-result-card {
500
+ grid-template-columns: 1fr 1fr;
501
+ align-items: center;
502
+ }
503
+ }
504
+
505
+ .sc-result-label {
506
+ font-size: 0.7rem;
507
+ font-weight: 700;
508
+ text-transform: uppercase;
509
+ letter-spacing: 0.15em;
510
+ color: rgba(255,255,255,0.75);
511
+ margin-bottom: 0.25rem;
512
+ }
513
+
514
+ .sc-result-value-row {
515
+ display: flex;
516
+ align-items: baseline;
517
+ gap: 0.5rem;
518
+ }
519
+
520
+ .sc-result-big {
521
+ font-size: 5rem;
522
+ font-weight: 900;
523
+ letter-spacing: -0.04em;
524
+ line-height: 1;
525
+ }
526
+
527
+ .sc-result-unit {
528
+ font-size: 1.75rem;
529
+ font-weight: 700;
530
+ color: rgba(255,255,255,0.75);
531
+ }
532
+
533
+ .sc-result-desc {
534
+ margin-top: 1rem;
535
+ font-size: 0.875rem;
536
+ font-weight: 500;
537
+ color: rgba(255,255,255,0.8);
538
+ }
539
+
540
+ .sc-hz-card {
541
+ background: rgba(255,255,255,0.1);
542
+ border-radius: 1.5rem;
543
+ padding: 1.5rem;
544
+ border: 1px solid rgba(255,255,255,0.1);
545
+ }
546
+
547
+ .sc-hz-header {
548
+ display: flex;
549
+ align-items: center;
550
+ gap: 0.75rem;
551
+ margin-bottom: 1rem;
552
+ }
553
+
554
+ .sc-hz-icon {
555
+ font-size: 1.5rem;
556
+ color: rgba(255,255,255,0.75);
557
+ }
558
+
559
+ .sc-hz-title {
560
+ font-weight: 700;
561
+ text-transform: uppercase;
562
+ font-size: 0.75rem;
563
+ letter-spacing: 0.08em;
564
+ color: #fff;
565
+ }
566
+
567
+ .sc-hz-track {
568
+ position: relative;
569
+ height: 1rem;
570
+ background: rgba(0,0,0,0.2);
571
+ border-radius: 9999px;
572
+ margin-bottom: 0.5rem;
573
+ overflow: hidden;
574
+ }
575
+
576
+ .sc-hz-bar {
577
+ height: 100%;
578
+ background: #fff;
579
+ border-radius: 9999px;
580
+ width: 0;
581
+ transition: width 0.3s;
582
+ }
583
+
584
+ :global(.sc-hz-bar[data-state="volumetric"]) {
585
+ background: #3b82f6;
586
+ box-shadow: 0 0 10px rgba(59,130,246,0.5);
587
+ }
588
+
589
+ :global(.sc-hz-bar[data-state="optimal"]) {
590
+ background: #34d399;
591
+ box-shadow: 0 0 10px rgba(52,211,153,0.5);
592
+ }
593
+
594
+ :global(.sc-hz-bar[data-state="high"]) {
595
+ background: #facc15;
596
+ box-shadow: 0 0 10px rgba(250,204,21,0.5);
597
+ }
598
+
599
+ :global(.sc-hz-bar[data-state="limit"]) {
600
+ background: #ef4444;
601
+ box-shadow: 0 0 10px rgba(239,68,68,0.5);
602
+ }
603
+
604
+ .sc-hz-footer {
605
+ display: flex;
606
+ justify-content: space-between;
607
+ font-size: 0.75rem;
608
+ font-weight: 700;
609
+ color: rgba(255,255,255,0.75);
610
+ }
611
+
612
+ .sc-hz-val {
613
+ color: rgba(255,255,255,0.75);
614
+ }
615
+
616
+ :global(.sc-hz-val[data-state="volumetric"]) { color: #bfdbfe; }
617
+ :global(.sc-hz-val[data-state="optimal"]) { color: #6ee7b7; }
618
+ :global(.sc-hz-val[data-state="high"]) { color: #fef08a; }
619
+ :global(.sc-hz-val[data-state="limit"]) { color: #fca5a5; }
620
+
621
+ .sc-hz-status {
622
+ color: rgba(255,255,255,0.75);
623
+ }
624
+
625
+ .sc-metrics-grid {
626
+ display: grid;
627
+ grid-template-columns: repeat(2, 1fr);
628
+ gap: 1rem;
629
+ }
630
+
631
+ @media (min-width: 768px) {
632
+ .sc-metrics-grid {
633
+ grid-template-columns: repeat(4, 1fr);
634
+ }
635
+ }
636
+
637
+ .sc-metric-card {
638
+ background: var(--sc-bg);
639
+ padding: 1.25rem;
640
+ border-radius: 1.5rem;
641
+ border: 1px solid var(--sc-border);
642
+ box-shadow: 0 1px 3px rgba(0,0,0,0.06);
643
+ text-align: center;
644
+ display: flex;
645
+ flex-direction: column;
646
+ align-items: center;
647
+ gap: 0.25rem;
648
+ }
649
+
650
+ .sc-metric-card-amber {
651
+ background: var(--sc-amber-bg);
652
+ border-color: var(--sc-amber-border);
653
+ }
654
+
655
+ .sc-metric-val {
656
+ display: block;
657
+ font-size: 1.75rem;
658
+ font-weight: 900;
659
+ color: var(--sc-text);
660
+ font-variant-numeric: tabular-nums;
661
+ }
662
+
663
+ .sc-metric-val-amber {
664
+ color: var(--sc-amber-text);
665
+ }
666
+
667
+ .sc-metric-label {
668
+ font-size: 0.625rem;
669
+ text-transform: uppercase;
670
+ font-weight: 700;
671
+ color: var(--sc-text-muted);
672
+ }
673
+
674
+ .sc-metric-label-amber {
675
+ color: var(--sc-amber-text);
676
+ opacity: 0.7;
677
+ }
678
+ </style>
679
+
680
+ <script>
681
+ class CalcSembradora extends HTMLElement {
682
+ els: {
683
+ mainDash?: HTMLElement | null;
684
+ inPop?: HTMLInputElement | null;
685
+ valPop?: HTMLElement | null;
686
+ inRow?: HTMLInputElement | null;
687
+ valRow?: HTMLElement | null;
688
+ speedBtns?: NodeListOf<HTMLElement>;
689
+ cropBtns?: NodeListOf<HTMLElement>;
690
+ outSpacing?: HTMLElement | null;
691
+ hzBar?: HTMLElement | null;
692
+ hzVal?: HTMLElement | null;
693
+ hzStatus?: HTMLElement | null;
694
+ outSeedsM?: HTMLElement | null;
695
+ outPlantsM2?: HTMLElement | null;
696
+ outSpeedMs?: HTMLElement | null;
697
+ outHaBag?: HTMLElement | null;
698
+ } = {};
699
+
700
+ state = { pop: 0, row: 0, speed: 6 };
701
+
702
+ connectedCallback() {
703
+ this.setupElements();
704
+ this.setupEvents();
705
+ const def = Array.from(this.els.speedBtns ?? []).find(
706
+ (b) => (b as HTMLElement).dataset.speed === '6'
707
+ ) as HTMLElement | undefined;
708
+ if (def) this.setSpeed(def);
709
+ }
710
+
711
+ setupElements() {
712
+ const q = <T extends HTMLElement>(s: string) => this.querySelector<T>(s);
713
+ this.els = {
714
+ mainDash: q<HTMLElement>('#mainDash'),
715
+ inPop: q<HTMLInputElement>('#inPop'),
716
+ valPop: q('#valPop'),
717
+ inRow: q<HTMLInputElement>('#inRow'),
718
+ valRow: q('#valRow'),
719
+ speedBtns: this.querySelectorAll<HTMLElement>('.speed-btn'),
720
+ cropBtns: this.querySelectorAll<HTMLElement>('.crop-btn'),
721
+ outSpacing: q('#outSpacing'),
722
+ hzBar: q('#hzBar'),
723
+ hzVal: q('#hzVal'),
724
+ hzStatus: q('#hzStatus'),
725
+ outSeedsM: q('#outSeedsM'),
726
+ outPlantsM2: q('#outPlantsM2'),
727
+ outSpeedMs: q('#outSpeedMs'),
728
+ outHaBag: q('#outHaBag'),
729
+ };
730
+ }
731
+
732
+ setupEvents() {
733
+ this.els.cropBtns?.forEach((btn) => {
734
+ btn.addEventListener('click', () => this.selectCrop(btn));
735
+ });
736
+ this.els.inPop?.addEventListener('input', (e) => {
737
+ this.updateParam('pop', parseInt((e.target as HTMLInputElement).value));
738
+ });
739
+ this.els.inRow?.addEventListener('input', (e) => {
740
+ this.updateParam('row', parseInt((e.target as HTMLInputElement).value));
741
+ });
742
+ this.els.speedBtns?.forEach((btn) => {
743
+ btn.addEventListener('click', () => this.setSpeed(btn));
744
+ });
745
+ }
746
+
747
+ setText(el: HTMLElement | null | undefined, val: string) {
748
+ if (el) el.textContent = val;
749
+ }
750
+
751
+ selectCrop(btn: HTMLElement) {
752
+ if (this.els.mainDash) delete this.els.mainDash.dataset.disabled;
753
+ this.els.cropBtns?.forEach((b) => b.classList.remove('active'));
754
+ btn.classList.add('active');
755
+ const pop = parseInt(btn.dataset.pop ?? '0');
756
+ const row = parseInt(btn.dataset.row ?? '0');
757
+ if (this.els.inPop) this.els.inPop.value = String(pop);
758
+ this.updateParam('pop', pop);
759
+ if (this.els.inRow) this.els.inRow.value = String(row);
760
+ this.updateParam('row', row);
761
+ }
762
+
763
+ setSpeed(btn: HTMLElement) {
764
+ this.els.speedBtns?.forEach((b) => b.classList.remove('active'));
765
+ btn.classList.add('active');
766
+ this.state.speed = parseInt(btn.dataset.speed ?? '6');
767
+ this.calc();
768
+ }
769
+
770
+ updateParam(key: 'pop' | 'row', val: number) {
771
+ this.state[key] = val;
772
+ if (key === 'pop') this.setText(this.els.valPop, val.toLocaleString());
773
+ if (key === 'row') this.setText(this.els.valRow, String(val));
774
+ this.calc();
775
+ }
776
+
777
+ resolveHzState(hz: number): { state: string; label: string; pct: number } {
778
+ const d = this.dataset;
779
+ const pct60 = Math.min(100, (hz / 60) * 100);
780
+ if (hz > 60) return { state: 'volumetric', label: d.statusVolumetric as string, pct: Math.min(100, (hz / 400) * 100) };
781
+ if (hz < 25) return { state: 'optimal', label: d.statusOptimal as string, pct: pct60 };
782
+ if (hz < 50) return { state: 'high', label: d.statusHighSpeed as string, pct: pct60 };
783
+ return { state: 'limit', label: d.statusPlateLimiter as string, pct: pct60 };
784
+ }
785
+
786
+ setHzState(state: string, label: string, pct: number) {
787
+ if (this.els.hzBar) {
788
+ this.els.hzBar.style.width = `${pct}%`;
789
+ this.els.hzBar.dataset.state = state;
790
+ }
791
+ if (this.els.hzVal) this.els.hzVal.dataset.state = state;
792
+ this.setText(this.els.hzStatus, label);
793
+ }
794
+
795
+ calc() {
796
+ const { pop, row, speed } = this.state;
797
+ if (pop <= 0 || row <= 0) return;
798
+ const spacing = 10_000_000 / (pop * row);
799
+ const hz = (speed * 27.778) / spacing;
800
+ const { state, label, pct } = this.resolveHzState(hz);
801
+ this.setText(this.els.outSpacing, spacing.toFixed(1));
802
+ this.setText(this.els.hzVal, hz.toFixed(1) + ' Hz');
803
+ this.setHzState(state, label, pct);
804
+ this.setText(this.els.outSeedsM, (100 / spacing).toFixed(1));
805
+ this.setText(this.els.outPlantsM2, (pop / 10000).toFixed(1));
806
+ this.setText(this.els.outSpeedMs, (speed / 3.6).toFixed(1));
807
+ this.setText(this.els.outHaBag, (80000 / pop).toFixed(2));
808
+ }
809
+ }
810
+
811
+ customElements.define('calc-sembradora', CalcSembradora);
812
+ </script>