@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,559 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+ import { rainHarvester } from './index';
4
+ import { ROOF_MATERIALS, FILTER_EFFICIENCY } from './logic';
5
+
6
+ const { ui: propUi, locale = 'es' } = Astro.props;
7
+ let ui = propUi;
8
+
9
+ if (!ui) {
10
+ const content = await rainHarvester.i18n[locale as keyof typeof rainHarvester.i18n]?.();
11
+ ui = content?.ui;
12
+ }
13
+
14
+ if (!ui) return null;
15
+
16
+ const materialNames: Record<string, string> = {
17
+ metal: ui.materialMetal,
18
+ clay: ui.materialClay,
19
+ concrete: ui.materialConcrete,
20
+ gravel: ui.materialGravel,
21
+ };
22
+ ---
23
+
24
+ <rain-harvester class="rh-wrap" data-filter-efficiency={FILTER_EFFICIENCY}>
25
+ <div class="rh-container">
26
+ <div class="rh-inputs">
27
+ <section class="rh-section">
28
+ <h2 class="rh-section-head">
29
+ <span class="rh-step-badge">1</span>
30
+ {ui.headInputs}
31
+ </h2>
32
+
33
+ <div class="rh-field-group">
34
+ <div class="rh-field">
35
+ <label for="area" class="rh-label">
36
+ <Icon name="mdi:home-roof" class="rh-label-icon" /> {ui.labelArea}
37
+ </label>
38
+ <div class="rh-input-wrap">
39
+ <input type="number" id="area" class="rh-input" value="100" min="0" />
40
+ <span class="rh-input-suffix">{ui.unitM2}</span>
41
+ </div>
42
+ </div>
43
+
44
+ <div class="rh-field">
45
+ <label for="rainfall" class="rh-label">
46
+ <Icon name="mdi:weather-pouring" class="rh-label-icon" /> {ui.labelRainfall}
47
+ </label>
48
+ <div class="rh-input-wrap">
49
+ <input type="number" id="rainfall" class="rh-input" value="600" min="0" />
50
+ <span class="rh-input-suffix">{ui.unitMm}</span>
51
+ </div>
52
+ <p class="rh-field-help">{ui.helpRainfall}</p>
53
+ </div>
54
+
55
+ <div class="rh-field">
56
+ <label for="material" class="rh-label">
57
+ <Icon name="mdi:texture-box" class="rh-label-icon" /> {ui.labelMaterial}
58
+ </label>
59
+ <div class="rh-select-wrap">
60
+ <select id="material" class="rh-select">
61
+ {ROOF_MATERIALS.map((m) => (
62
+ <option value={m.id} data-coeff={m.coefficient}>
63
+ {materialNames[m.id]} ({(m.coefficient * 100).toFixed(0)}%)
64
+ </option>
65
+ ))}
66
+ </select>
67
+ <Icon name="mdi:chevron-down" class="rh-select-arrow" />
68
+ </div>
69
+ </div>
70
+ </div>
71
+
72
+ <div class="rh-info-card">
73
+ <div class="rh-info-icon-box">
74
+ <Icon name="mdi:information-outline" class="rh-info-icon" />
75
+ </div>
76
+ <div class="rh-info-content">
77
+ <h4 class="rh-info-title">{ui.efficiencyTitle}</h4>
78
+ <p class="rh-info-text">{ui.efficiencyNote}</p>
79
+ </div>
80
+ </div>
81
+ </section>
82
+ </div>
83
+
84
+ <div class="rh-results">
85
+ <div class="rh-result-main">
86
+ <h3 class="rh-result-label">{ui.resultTitle}</h3>
87
+ <div class="rh-result-value-box">
88
+ <span id="resLiters" class="rh-result-big">0</span>
89
+ <span class="rh-result-unit">{ui.unitLiters}</span>
90
+ </div>
91
+ </div>
92
+
93
+ <div class="rh-equivalencies">
94
+ <h4 class="rh-equiv-title">{ui.equivalenciesTitle}</h4>
95
+ <div class="rh-equiv-grid">
96
+ <div class="rh-equiv-card">
97
+ <Icon name="mdi:toilet" class="rh-equiv-icon" />
98
+ <div class="rh-equiv-info">
99
+ <span id="resFlushes" class="rh-equiv-val">0</span>
100
+ <span class="rh-equiv-label">{ui.labelFlushes}</span>
101
+ </div>
102
+ </div>
103
+ <div class="rh-equiv-card">
104
+ <Icon name="mdi:shower-head" class="rh-equiv-icon" />
105
+ <div class="rh-equiv-info">
106
+ <span id="resShowers" class="rh-equiv-val">0</span>
107
+ <span class="rh-equiv-label">{ui.labelShowers}</span>
108
+ </div>
109
+ </div>
110
+ <div class="rh-equiv-card">
111
+ <Icon name="mdi:flower" class="rh-equiv-icon" />
112
+ <div class="rh-equiv-info">
113
+ <span id="resGarden" class="rh-equiv-val">0</span>
114
+ <span class="rh-equiv-label">{ui.labelGarden} <small>{ui.gardenArea}</small></span>
115
+ </div>
116
+ </div>
117
+ </div>
118
+ </div>
119
+
120
+ <div class="rh-bg-icon">
121
+ <Icon name="mdi:water" />
122
+ </div>
123
+
124
+ <div class="rh-liquid-container">
125
+ <div id="liquidFill" class="rh-liquid-fill">
126
+ <div class="rh-liquid-wave"></div>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ </rain-harvester>
132
+
133
+ <style>
134
+ .rh-wrap {
135
+ --rh-bg: #fff;
136
+ --rh-bg-inputs: #f8fafc;
137
+ --rh-border: #e2e8f0;
138
+ --rh-text: #1e293b;
139
+ --rh-text-muted: #64748b;
140
+ --rh-accent: #06b6d4;
141
+ --rh-accent-dark: #0891b2;
142
+ --rh-result-bg: linear-gradient(135deg, #06b6d4, #2563eb);
143
+ --rh-info-bg: #f0f9ff;
144
+ --rh-info-border: #bae6fd;
145
+ --rh-info-text: #0369a1;
146
+
147
+ display: block;
148
+ max-width: 72rem;
149
+ margin: 0 auto;
150
+ padding: 1rem;
151
+ user-select: none;
152
+ }
153
+
154
+ :global(.theme-dark) .rh-wrap {
155
+ --rh-bg: #0f172a;
156
+ --rh-bg-inputs: #1e293b;
157
+ --rh-border: #334155;
158
+ --rh-text: #f1f5f9;
159
+ --rh-text-muted: #94a3b8;
160
+ --rh-info-bg: rgba(3, 105, 161, 0.1);
161
+ --rh-info-border: #0369a1;
162
+ --rh-info-text: #7dd3fc;
163
+ }
164
+
165
+ .rh-container {
166
+ display: grid;
167
+ grid-template-columns: 1fr;
168
+ background: var(--rh-bg);
169
+ border-radius: 2rem;
170
+ overflow: hidden;
171
+ border: 1px solid var(--rh-border);
172
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.1);
173
+ }
174
+
175
+ @media (min-width: 1024px) {
176
+ .rh-container {
177
+ grid-template-columns: 1fr 1fr;
178
+ }
179
+ }
180
+
181
+ .rh-inputs {
182
+ padding: 2.5rem;
183
+ background: var(--rh-bg-inputs);
184
+ }
185
+
186
+ .rh-section-head {
187
+ font-size: 1.25rem;
188
+ font-weight: 800;
189
+ color: var(--rh-text);
190
+ display: flex;
191
+ align-items: center;
192
+ gap: 0.75rem;
193
+ margin-bottom: 2rem;
194
+ text-transform: uppercase;
195
+ letter-spacing: 0.05em;
196
+ }
197
+
198
+ .rh-step-badge {
199
+ width: 2.25rem;
200
+ height: 2.25rem;
201
+ background: var(--rh-text);
202
+ color: var(--rh-bg);
203
+ border-radius: 50%;
204
+ display: flex;
205
+ align-items: center;
206
+ justify-content: center;
207
+ font-size: 1rem;
208
+ font-weight: 900;
209
+ }
210
+
211
+ .rh-field-group {
212
+ display: flex;
213
+ flex-direction: column;
214
+ gap: 1.5rem;
215
+ margin-bottom: 2rem;
216
+ }
217
+
218
+ .rh-field {
219
+ display: flex;
220
+ flex-direction: column;
221
+ gap: 0.5rem;
222
+ }
223
+
224
+ .rh-label {
225
+ font-size: 0.75rem;
226
+ font-weight: 700;
227
+ text-transform: uppercase;
228
+ color: var(--rh-text-muted);
229
+ letter-spacing: 0.05em;
230
+ display: flex;
231
+ align-items: center;
232
+ gap: 0.5rem;
233
+ }
234
+
235
+ .rh-label-icon {
236
+ font-size: 1.25rem;
237
+ color: var(--rh-accent);
238
+ }
239
+
240
+ .rh-input-wrap, .rh-select-wrap {
241
+ position: relative;
242
+ }
243
+
244
+ .rh-input, .rh-select {
245
+ width: 100%;
246
+ background: var(--rh-bg);
247
+ border: 2px solid var(--rh-border);
248
+ border-radius: 1rem;
249
+ padding: 0.875rem 1rem;
250
+ font-size: 1.5rem;
251
+ font-weight: 700;
252
+ color: var(--rh-text);
253
+ transition: border-color 0.2s;
254
+ outline: none;
255
+ }
256
+
257
+ .rh-select {
258
+ font-size: 1.125rem;
259
+ cursor: pointer;
260
+ appearance: none;
261
+ }
262
+
263
+ .rh-input:focus, .rh-select:focus {
264
+ border-color: var(--rh-accent);
265
+ }
266
+
267
+ .rh-input-suffix {
268
+ position: absolute;
269
+ right: 1.25rem;
270
+ top: 50%;
271
+ transform: translateY(-50%);
272
+ font-weight: 700;
273
+ color: var(--rh-text-muted);
274
+ pointer-events: none;
275
+ }
276
+
277
+ .rh-field-help {
278
+ font-size: 0.75rem;
279
+ font-style: italic;
280
+ color: var(--rh-text-muted);
281
+ }
282
+
283
+ .rh-select-arrow {
284
+ position: absolute;
285
+ right: 1.25rem;
286
+ top: 50%;
287
+ transform: translateY(-50%);
288
+ font-size: 1.5rem;
289
+ color: var(--rh-text-muted);
290
+ pointer-events: none;
291
+ }
292
+
293
+ .rh-info-card {
294
+ background: var(--rh-info-bg);
295
+ border: 1px solid var(--rh-info-border);
296
+ border-radius: 1.25rem;
297
+ padding: 1.25rem;
298
+ display: flex;
299
+ gap: 1rem;
300
+ align-items: center;
301
+ }
302
+
303
+ .rh-info-icon-box {
304
+ background: var(--rh-info-border);
305
+ color: var(--rh-info-text);
306
+ padding: 0.75rem;
307
+ border-radius: 0.75rem;
308
+ display: flex;
309
+ }
310
+
311
+ .rh-info-icon {
312
+ font-size: 1.5rem;
313
+ }
314
+
315
+ .rh-info-title {
316
+ margin: 0;
317
+ font-weight: 800;
318
+ color: var(--rh-info-text);
319
+ font-size: 0.875rem;
320
+ }
321
+
322
+ .rh-info-text {
323
+ margin: 0.25rem 0 0;
324
+ font-size: 0.75rem;
325
+ color: var(--rh-info-text);
326
+ opacity: 0.9;
327
+ }
328
+
329
+ .rh-results {
330
+ background: var(--rh-result-bg);
331
+ padding: 2.5rem;
332
+ color: #fff;
333
+ display: flex;
334
+ flex-direction: column;
335
+ justify-content: space-between;
336
+ position: relative;
337
+ overflow: hidden;
338
+ }
339
+
340
+ .rh-result-main {
341
+ position: relative;
342
+ z-index: 10;
343
+ }
344
+
345
+ .rh-result-label {
346
+ font-size: 0.875rem;
347
+ font-weight: 700;
348
+ text-transform: uppercase;
349
+ letter-spacing: 0.1em;
350
+ color: rgba(255, 255, 255, 0.8);
351
+ margin: 0 0 0.5rem;
352
+ }
353
+
354
+ .rh-result-value-box {
355
+ display: flex;
356
+ align-items: baseline;
357
+ gap: 0.5rem;
358
+ }
359
+
360
+ .rh-result-big {
361
+ font-size: 5rem;
362
+ font-weight: 900;
363
+ letter-spacing: -0.05em;
364
+ line-height: 1;
365
+ }
366
+
367
+ .rh-result-unit {
368
+ font-size: 1.5rem;
369
+ font-weight: 700;
370
+ color: rgba(255, 255, 255, 0.8);
371
+ }
372
+
373
+ .rh-equivalencies {
374
+ position: relative;
375
+ z-index: 10;
376
+ margin-top: 2rem;
377
+ }
378
+
379
+ .rh-equiv-title {
380
+ font-size: 1rem;
381
+ font-weight: 800;
382
+ margin: 0 0 1.25rem;
383
+ padding-bottom: 0.75rem;
384
+ border-bottom: 1px solid rgba(255, 255, 255, 0.2);
385
+ }
386
+
387
+ .rh-equiv-grid {
388
+ display: flex;
389
+ flex-direction: column;
390
+ gap: 1rem;
391
+ }
392
+
393
+ .rh-equiv-card {
394
+ background: rgba(255, 255, 255, 0.1);
395
+ backdrop-filter: blur(8px);
396
+ border: 1px solid rgba(255, 255, 255, 0.2);
397
+ border-radius: 1rem;
398
+ padding: 1rem;
399
+ display: flex;
400
+ align-items: center;
401
+ gap: 1rem;
402
+ }
403
+
404
+ .rh-equiv-icon {
405
+ font-size: 2rem;
406
+ color: rgba(255, 255, 255, 0.9);
407
+ }
408
+
409
+ .rh-equiv-info {
410
+ display: flex;
411
+ flex-direction: column;
412
+ }
413
+
414
+ .rh-equiv-val {
415
+ font-size: 1.5rem;
416
+ font-weight: 800;
417
+ line-height: 1;
418
+ }
419
+
420
+ .rh-equiv-label {
421
+ font-size: 0.65rem;
422
+ text-transform: uppercase;
423
+ font-weight: 700;
424
+ margin-top: 0.25rem;
425
+ color: rgba(255, 255, 255, 0.7);
426
+ }
427
+
428
+ .rh-bg-icon {
429
+ position: absolute;
430
+ bottom: -2rem;
431
+ right: -2rem;
432
+ font-size: 15rem;
433
+ opacity: 0.1;
434
+ pointer-events: none;
435
+ z-index: 1;
436
+ }
437
+
438
+ .rh-liquid-container {
439
+ position: absolute;
440
+ inset: 0;
441
+ z-index: 0;
442
+ pointer-events: none;
443
+ }
444
+
445
+ .rh-liquid-fill {
446
+ position: absolute;
447
+ bottom: 0;
448
+ left: 0;
449
+ width: 100%;
450
+ background: rgba(255, 255, 255, 0.1);
451
+ transition: height 1s ease-in-out;
452
+ height: 0;
453
+ }
454
+
455
+ .rh-liquid-wave {
456
+ position: absolute;
457
+ top: -0.5rem;
458
+ left: 0;
459
+ width: 100%;
460
+ height: 1rem;
461
+ background: rgba(255, 255, 255, 0.15);
462
+ transform: skewY(1deg);
463
+ animation: rh-wave 3s infinite alternate ease-in-out;
464
+ }
465
+
466
+ @keyframes rh-wave {
467
+ from { transform: skewY(1deg); }
468
+ to { transform: skewY(-1deg); }
469
+ }
470
+ </style>
471
+
472
+ <script>
473
+ class RainHarvester extends HTMLElement {
474
+ els: {
475
+ area?: HTMLInputElement | null;
476
+ rainfall?: HTMLInputElement | null;
477
+ material?: HTMLSelectElement | null;
478
+ resLiters?: HTMLElement | null;
479
+ resFlushes?: HTMLElement | null;
480
+ resShowers?: HTMLElement | null;
481
+ resGarden?: HTMLElement | null;
482
+ liquidFill?: HTMLElement | null;
483
+ } = {};
484
+
485
+ filterEfficiency = 0.9;
486
+
487
+ connectedCallback() {
488
+ this.filterEfficiency = parseFloat(this.dataset.filterEfficiency || '0.9');
489
+ this.setupElements();
490
+ this.setupEvents();
491
+ this.calculate();
492
+ }
493
+
494
+ setupElements() {
495
+ this.els = {
496
+ area: this.querySelector('#area'),
497
+ rainfall: this.querySelector('#rainfall'),
498
+ material: this.querySelector('#material'),
499
+ resLiters: this.querySelector('#resLiters'),
500
+ resFlushes: this.querySelector('#resFlushes'),
501
+ resShowers: this.querySelector('#resShowers'),
502
+ resGarden: this.querySelector('#resGarden'),
503
+ liquidFill: this.querySelector('#liquidFill'),
504
+ };
505
+ }
506
+
507
+ setupEvents() {
508
+ this.els.area?.addEventListener('input', () => this.calculate());
509
+ this.els.rainfall?.addEventListener('input', () => this.calculate());
510
+ this.els.material?.addEventListener('change', () => this.calculate());
511
+ }
512
+
513
+ calculate() {
514
+ const area = parseFloat(this.els.area?.value || '0');
515
+ const rainfall = parseFloat(this.els.rainfall?.value || '0');
516
+ const mat = this.els.material;
517
+ const coeff = parseFloat(mat?.options[mat.selectedIndex]?.dataset.coeff || '0.85');
518
+
519
+ const netVolume = area * rainfall * coeff * this.filterEfficiency;
520
+
521
+ this.updateUIResults(netVolume);
522
+ this.updateLiquidViz(netVolume);
523
+ }
524
+
525
+ updateUIResults(volume: number) {
526
+ this.animateValue(this.els.resLiters, volume);
527
+ this.animateValue(this.els.resFlushes, Math.floor(volume / 6));
528
+ this.animateValue(this.els.resShowers, Math.floor(volume / 80));
529
+ this.animateValue(this.els.resGarden, Math.floor(volume / 500));
530
+ }
531
+
532
+ updateLiquidViz(volume: number) {
533
+ if (!this.els.liquidFill) return;
534
+ const maxViz = 500000;
535
+ const pct = Math.min(100, Math.max(5, (volume / maxViz) * 100));
536
+ this.els.liquidFill.style.height = `${pct}%`;
537
+ }
538
+
539
+ animateValue(el: HTMLElement | null | undefined, end: number) {
540
+ if (!el) return;
541
+ const start = parseInt(el.textContent?.replace(/,/g, '') || '0');
542
+ if (start === Math.floor(end)) return;
543
+
544
+ const duration = 1000;
545
+ let startTime: number | null = null;
546
+ const range = Math.floor(end) - start;
547
+
548
+ const step = (timestamp: number) => {
549
+ if (!startTime) startTime = timestamp;
550
+ const progress = Math.min((timestamp - startTime) / duration, 1);
551
+ el.textContent = Math.floor(start + range * progress).toLocaleString();
552
+ if (progress < 1) requestAnimationFrame(step);
553
+ };
554
+ requestAnimationFrame(step);
555
+ }
556
+ }
557
+
558
+ customElements.define('rain-harvester', RainHarvester);
559
+ </script>