@jjlmoya/utils-nature 1.20.0 → 1.22.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 (139) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +3 -2
  3. package/src/entries.ts +5 -1
  4. package/src/index.ts +4 -0
  5. package/src/pages/[locale]/[slug].astro +5 -4
  6. package/src/tests/diacritics_density.test.ts +1 -1
  7. package/src/tests/inverted_punctuation.test.ts +1 -1
  8. package/src/tests/script_density.test.ts +1 -1
  9. package/src/tests/seo_length.test.ts +1 -0
  10. package/src/tests/seo_translation_completeness.test.ts +69 -0
  11. package/src/tests/title_quality.test.ts +1 -1
  12. package/src/tests/tool_validation.test.ts +2 -3
  13. package/src/tests/translation_copy.test.ts +2 -2
  14. package/src/tool/compostBinVolumeRatioCalculator/bibliography.astro +18 -0
  15. package/src/tool/compostBinVolumeRatioCalculator/bibliography.ts +12 -0
  16. package/src/tool/compostBinVolumeRatioCalculator/component.astro +77 -0
  17. package/src/tool/compostBinVolumeRatioCalculator/compost-bin-volume-ratio-calculator.css +509 -0
  18. package/src/tool/compostBinVolumeRatioCalculator/controller.ts +189 -0
  19. package/src/tool/compostBinVolumeRatioCalculator/dom-views.ts +162 -0
  20. package/src/tool/compostBinVolumeRatioCalculator/entry.ts +28 -0
  21. package/src/tool/compostBinVolumeRatioCalculator/evaluator.ts +26 -0
  22. package/src/tool/compostBinVolumeRatioCalculator/i18n/de.ts +193 -0
  23. package/src/tool/compostBinVolumeRatioCalculator/i18n/en.ts +193 -0
  24. package/src/tool/compostBinVolumeRatioCalculator/i18n/es.ts +193 -0
  25. package/src/tool/compostBinVolumeRatioCalculator/i18n/fr.ts +193 -0
  26. package/src/tool/compostBinVolumeRatioCalculator/i18n/id.ts +185 -0
  27. package/src/tool/compostBinVolumeRatioCalculator/i18n/it.ts +193 -0
  28. package/src/tool/compostBinVolumeRatioCalculator/i18n/ja.ts +190 -0
  29. package/src/tool/compostBinVolumeRatioCalculator/i18n/ko.ts +190 -0
  30. package/src/tool/compostBinVolumeRatioCalculator/i18n/nl.ts +190 -0
  31. package/src/tool/compostBinVolumeRatioCalculator/i18n/pl.ts +190 -0
  32. package/src/tool/compostBinVolumeRatioCalculator/i18n/pt.ts +190 -0
  33. package/src/tool/compostBinVolumeRatioCalculator/i18n/ru.ts +190 -0
  34. package/src/tool/compostBinVolumeRatioCalculator/i18n/sv.ts +190 -0
  35. package/src/tool/compostBinVolumeRatioCalculator/i18n/tr.ts +190 -0
  36. package/src/tool/compostBinVolumeRatioCalculator/i18n/zh.ts +190 -0
  37. package/src/tool/compostBinVolumeRatioCalculator/index.ts +11 -0
  38. package/src/tool/compostBinVolumeRatioCalculator/logic.test.ts +55 -0
  39. package/src/tool/compostBinVolumeRatioCalculator/logic.ts +165 -0
  40. package/src/tool/compostBinVolumeRatioCalculator/seo.astro +23 -0
  41. package/src/tool/compostBinVolumeRatioCalculator/storage.ts +25 -0
  42. package/src/tool/compostBinVolumeRatioCalculator/ui.ts +54 -0
  43. package/src/tool/cricketThermometer/component.astro +1 -1
  44. package/src/tool/cricketThermometer/i18n/de.ts +2 -2
  45. package/src/tool/cricketThermometer/i18n/en.ts +2 -2
  46. package/src/tool/cricketThermometer/i18n/es.ts +2 -2
  47. package/src/tool/cricketThermometer/i18n/fr.ts +2 -2
  48. package/src/tool/cricketThermometer/i18n/id.ts +2 -2
  49. package/src/tool/cricketThermometer/i18n/it.ts +2 -2
  50. package/src/tool/cricketThermometer/i18n/ja.ts +2 -2
  51. package/src/tool/cricketThermometer/i18n/ko.ts +2 -2
  52. package/src/tool/cricketThermometer/i18n/nl.ts +2 -2
  53. package/src/tool/cricketThermometer/i18n/pl.ts +2 -2
  54. package/src/tool/cricketThermometer/i18n/pt.ts +2 -2
  55. package/src/tool/cricketThermometer/i18n/ru.ts +2 -2
  56. package/src/tool/cricketThermometer/i18n/sv.ts +2 -2
  57. package/src/tool/cricketThermometer/i18n/tr.ts +2 -2
  58. package/src/tool/cricketThermometer/i18n/zh.ts +2 -2
  59. package/src/tool/cricketThermometer/index.ts +1 -0
  60. package/src/tool/digitalCarbon/component.astro +3 -3
  61. package/src/tool/digitalCarbon/i18n/de.ts +2 -2
  62. package/src/tool/digitalCarbon/i18n/en.ts +2 -2
  63. package/src/tool/digitalCarbon/i18n/es.ts +2 -2
  64. package/src/tool/digitalCarbon/i18n/fr.ts +2 -2
  65. package/src/tool/digitalCarbon/i18n/id.ts +2 -2
  66. package/src/tool/digitalCarbon/i18n/it.ts +2 -2
  67. package/src/tool/digitalCarbon/i18n/ja.ts +2 -2
  68. package/src/tool/digitalCarbon/i18n/ko.ts +2 -2
  69. package/src/tool/digitalCarbon/i18n/nl.ts +2 -2
  70. package/src/tool/digitalCarbon/i18n/pl.ts +2 -2
  71. package/src/tool/digitalCarbon/i18n/pt.ts +2 -2
  72. package/src/tool/digitalCarbon/i18n/ru.ts +2 -2
  73. package/src/tool/digitalCarbon/i18n/sv.ts +2 -2
  74. package/src/tool/digitalCarbon/i18n/tr.ts +2 -2
  75. package/src/tool/digitalCarbon/i18n/zh.ts +2 -2
  76. package/src/tool/digitalCarbon/index.ts +1 -0
  77. package/src/tool/rainHarvester/i18n/de.ts +2 -2
  78. package/src/tool/rainHarvester/i18n/en.ts +2 -2
  79. package/src/tool/rainHarvester/i18n/es.ts +2 -2
  80. package/src/tool/rainHarvester/i18n/fr.ts +2 -2
  81. package/src/tool/rainHarvester/i18n/id.ts +2 -2
  82. package/src/tool/rainHarvester/i18n/it.ts +2 -2
  83. package/src/tool/rainHarvester/i18n/ja.ts +2 -2
  84. package/src/tool/rainHarvester/i18n/ko.ts +2 -2
  85. package/src/tool/rainHarvester/i18n/nl.ts +2 -2
  86. package/src/tool/rainHarvester/i18n/pl.ts +2 -2
  87. package/src/tool/rainHarvester/i18n/pt.ts +2 -2
  88. package/src/tool/rainHarvester/i18n/ru.ts +2 -2
  89. package/src/tool/rainHarvester/i18n/sv.ts +2 -2
  90. package/src/tool/rainHarvester/i18n/tr.ts +2 -2
  91. package/src/tool/rainHarvester/i18n/zh.ts +2 -2
  92. package/src/tool/rainHarvester/index.ts +1 -0
  93. package/src/tool/seedCalculator/i18n/de.ts +2 -2
  94. package/src/tool/seedCalculator/i18n/en.ts +2 -2
  95. package/src/tool/seedCalculator/i18n/es.ts +2 -2
  96. package/src/tool/seedCalculator/i18n/fr.ts +2 -2
  97. package/src/tool/seedCalculator/i18n/id.ts +2 -2
  98. package/src/tool/seedCalculator/i18n/it.ts +2 -2
  99. package/src/tool/seedCalculator/i18n/ja.ts +2 -2
  100. package/src/tool/seedCalculator/i18n/ko.ts +2 -2
  101. package/src/tool/seedCalculator/i18n/nl.ts +2 -2
  102. package/src/tool/seedCalculator/i18n/pl.ts +2 -2
  103. package/src/tool/seedCalculator/i18n/pt.ts +2 -2
  104. package/src/tool/seedCalculator/i18n/ru.ts +2 -2
  105. package/src/tool/seedCalculator/i18n/sv.ts +2 -2
  106. package/src/tool/seedCalculator/i18n/tr.ts +2 -2
  107. package/src/tool/seedCalculator/i18n/zh.ts +2 -2
  108. package/src/tool/seedCalculator/index.ts +1 -0
  109. package/src/tool/urbanGardenPlanner/bibliography.astro +9 -0
  110. package/src/tool/urbanGardenPlanner/bibliography.ts +16 -0
  111. package/src/tool/urbanGardenPlanner/component.astro +131 -0
  112. package/src/tool/urbanGardenPlanner/controller.ts +88 -0
  113. package/src/tool/urbanGardenPlanner/dom-views.ts +84 -0
  114. package/src/tool/urbanGardenPlanner/entry.ts +29 -0
  115. package/src/tool/urbanGardenPlanner/evaluator.ts +27 -0
  116. package/src/tool/urbanGardenPlanner/i18n/de.ts +50 -0
  117. package/src/tool/urbanGardenPlanner/i18n/en.ts +179 -0
  118. package/src/tool/urbanGardenPlanner/i18n/es.ts +50 -0
  119. package/src/tool/urbanGardenPlanner/i18n/fr.ts +50 -0
  120. package/src/tool/urbanGardenPlanner/i18n/id.ts +50 -0
  121. package/src/tool/urbanGardenPlanner/i18n/it.ts +45 -0
  122. package/src/tool/urbanGardenPlanner/i18n/ja.ts +39 -0
  123. package/src/tool/urbanGardenPlanner/i18n/ko.ts +39 -0
  124. package/src/tool/urbanGardenPlanner/i18n/nl.ts +39 -0
  125. package/src/tool/urbanGardenPlanner/i18n/pl.ts +39 -0
  126. package/src/tool/urbanGardenPlanner/i18n/pt.ts +39 -0
  127. package/src/tool/urbanGardenPlanner/i18n/ru.ts +39 -0
  128. package/src/tool/urbanGardenPlanner/i18n/sv.ts +39 -0
  129. package/src/tool/urbanGardenPlanner/i18n/tr.ts +104 -0
  130. package/src/tool/urbanGardenPlanner/i18n/zh.ts +104 -0
  131. package/src/tool/urbanGardenPlanner/index.ts +11 -0
  132. package/src/tool/urbanGardenPlanner/logic.test.ts +45 -0
  133. package/src/tool/urbanGardenPlanner/logic.ts +55 -0
  134. package/src/tool/urbanGardenPlanner/seo.astro +15 -0
  135. package/src/tool/urbanGardenPlanner/storage.ts +25 -0
  136. package/src/tool/urbanGardenPlanner/ui.ts +38 -0
  137. package/src/tool/urbanGardenPlanner/urban-garden-rainwater-and-soil-planner.css +531 -0
  138. package/src/tools.ts +5 -2
  139. package/src/types.ts +4 -4
@@ -0,0 +1,509 @@
1
+ :root {
2
+ --n-paper: #f4efe4;
3
+ --n-paper-deep: #e8ddc9;
4
+ --n-ink: #2d2b25;
5
+ --n-muted: #716b5e;
6
+ --n-line: #cfc1a9;
7
+ --n-brown: #a96e45;
8
+ --n-brown-soft: #d9ae80;
9
+ --n-green: #708b58;
10
+ --n-green-soft: #b8c89c;
11
+ --n-water: #7a9e9e;
12
+ --n-red: #a95042;
13
+ --n-white: #fffdf7;
14
+ }
15
+
16
+ .theme-dark {
17
+ --n-paper: #211f1a;
18
+ --n-paper-deep: #2c2922;
19
+ --n-ink: #f4efe4;
20
+ --n-muted: #c2b9a9;
21
+ --n-line: #625947;
22
+ --n-brown: #d39764;
23
+ --n-brown-soft: #ad7952;
24
+ --n-green: #abc989;
25
+ --n-green-soft: #6e8c59;
26
+ --n-water: #a8d0cd;
27
+ --n-red: #df8d7d;
28
+ --n-white: #302c24;
29
+ }
30
+
31
+ .compost-tool {
32
+ --compost-bg: var(--n-paper);
33
+ --compost-card: var(--n-white);
34
+ --compost-ink: var(--n-ink);
35
+ --compost-muted: var(--n-muted);
36
+ --compost-line: var(--n-line);
37
+ --compost-brown: var(--n-brown);
38
+ --compost-green: var(--n-green);
39
+ --compost-water: var(--n-water);
40
+
41
+ background: var(--compost-bg);
42
+ border: 1px solid var(--compost-line);
43
+ border-radius: 28px;
44
+ color: var(--compost-ink);
45
+ overflow: hidden;
46
+ font:
47
+ 400 1em ui-monospace,
48
+ SFMono-Regular,
49
+ Menlo,
50
+ Consolas,
51
+ monospace;
52
+ }
53
+
54
+ .compost-tool-bar {
55
+ display: flex;
56
+ justify-content: space-between;
57
+ align-items: center;
58
+ padding: 18px 22px;
59
+ border-bottom: 1px solid var(--compost-line);
60
+ }
61
+ .compost-tool-units {
62
+ display: inline-flex;
63
+ border: 1px solid var(--compost-line);
64
+ border-radius: 99px;
65
+ padding: 3px;
66
+ }
67
+ .compost-tool button {
68
+ border: 0;
69
+ cursor: pointer;
70
+ font: inherit;
71
+ color: var(--compost-muted);
72
+ background: transparent;
73
+ }
74
+ .compost-tool-units button {
75
+ border-radius: 99px;
76
+ padding: 8px 12px;
77
+ font-size: 0.72rem;
78
+ letter-spacing: 0.04em;
79
+ }
80
+ .compost-tool-units button[aria-pressed="true"] {
81
+ background: var(--compost-ink);
82
+ color: var(--compost-bg);
83
+ }
84
+ .compost-tool-reset {
85
+ text-decoration: underline;
86
+ text-underline-offset: 4px;
87
+ font-size: 0.7rem;
88
+ }
89
+ .compost-tool-body {
90
+ display: grid;
91
+ grid-template-columns: minmax(230px, 0.8fr) minmax(310px, 1.4fr) minmax(
92
+ 220px,
93
+ 0.8fr
94
+ );
95
+ min-height: 560px;
96
+ }
97
+ .compost-controls,
98
+ .compost-scene,
99
+ .compost-result {
100
+ padding: 26px;
101
+ }
102
+ .compost-controls {
103
+ border-right: 1px solid var(--compost-line);
104
+ }
105
+ .compost-scene {
106
+ display: flex;
107
+ flex-direction: column;
108
+ justify-content: center;
109
+ background: var(--compost-card);
110
+ }
111
+ .compost-result {
112
+ border-left: 1px solid var(--compost-line);
113
+ display: flex;
114
+ flex-direction: column;
115
+ justify-content: center;
116
+ }
117
+ .compost-section-heading {
118
+ display: flex;
119
+ gap: 10px;
120
+ align-items: baseline;
121
+ margin-bottom: 20px;
122
+ }
123
+ .compost-section-heading span {
124
+ color: var(--compost-brown);
125
+ font-size: 0.66rem;
126
+ }
127
+ .compost-section-heading h2 {
128
+ margin: 0;
129
+ font:
130
+ 700 1rem/1.2 Georgia,
131
+ serif;
132
+ }
133
+ .compost-section-heading-materials {
134
+ margin-top: 34px;
135
+ }
136
+ .compost-section-heading-moisture {
137
+ margin-top: 34px;
138
+ }
139
+ .compost-choice-group-label {
140
+ display: block;
141
+ color: var(--compost-muted);
142
+ font-size: 0.7rem;
143
+ margin-bottom: 9px;
144
+ }
145
+ .compost-choice-group-buttons {
146
+ display: grid;
147
+ grid-template-columns: 1fr 1fr;
148
+ gap: 8px;
149
+ }
150
+ .compost-choice-group button {
151
+ display: grid;
152
+ gap: 6px;
153
+ justify-items: center;
154
+ padding: 10px 6px;
155
+ border: 1px solid var(--compost-line);
156
+ border-radius: 12px;
157
+ font-size: 0.65rem;
158
+ }
159
+ .compost-choice-group button.is-active {
160
+ border-color: var(--compost-brown);
161
+ color: var(--compost-ink);
162
+ background: color-mix(in srgb, var(--compost-brown) 12%, transparent);
163
+ }
164
+ .compost-shape-icon {
165
+ width: 30px;
166
+ height: 25px;
167
+ display: block;
168
+ border: 2px solid currentcolor;
169
+ opacity: 0.7;
170
+ }
171
+ .compost-shape-icon-box {
172
+ border-radius: 3px;
173
+ }
174
+ .compost-shape-icon-cylinder {
175
+ border-radius: 50% / 18%;
176
+ }
177
+ .compost-dimensions {
178
+ display: grid;
179
+ grid-template-columns: 1fr 1fr;
180
+ gap: 12px;
181
+ margin-top: 18px;
182
+ }
183
+ .compost-dimensions[data-shape="cylindrical"] [data-rectangular-only] {
184
+ display: none;
185
+ }
186
+ .compost-dimensions[data-shape="rectangular"] [data-cylinder-only] {
187
+ display: none;
188
+ }
189
+ .compost-tool label {
190
+ color: var(--compost-muted);
191
+ font-size: 0.66rem;
192
+ }
193
+ .compost-tool label > span {
194
+ display: block;
195
+ margin-bottom: 6px;
196
+ }
197
+ .compost-tool label > div {
198
+ display: flex;
199
+ align-items: center;
200
+ border-bottom: 1px solid var(--compost-line);
201
+ }
202
+ .compost-tool input {
203
+ width: 100%;
204
+ min-width: 0;
205
+ padding: 8px 0;
206
+ border: 0;
207
+ outline: 0;
208
+ color: var(--compost-ink);
209
+ background: transparent;
210
+ font:
211
+ 700 1rem/1.2 ui-monospace,
212
+ monospace;
213
+ }
214
+ .compost-tool input:focus {
215
+ border-bottom: 2px solid var(--compost-brown);
216
+ }
217
+ .compost-tool em {
218
+ color: var(--compost-muted);
219
+ font-style: normal;
220
+ font-size: 0.64rem;
221
+ white-space: nowrap;
222
+ }
223
+ .compost-volume-inputs {
224
+ display: grid;
225
+ gap: 13px;
226
+ }
227
+ .compost-volume-inputs label {
228
+ display: grid;
229
+ grid-template-columns: 14px 1fr;
230
+ column-gap: 7px;
231
+ align-items: start;
232
+ }
233
+ .compost-volume-inputs label > div {
234
+ grid-column: 2;
235
+ }
236
+ .compost-label-dot {
237
+ width: 9px;
238
+ height: 9px;
239
+ border-radius: 50%;
240
+ margin-top: 3px;
241
+ }
242
+ .compost-label-dot-brown {
243
+ background: var(--compost-brown);
244
+ }
245
+ .compost-label-dot-green {
246
+ background: var(--compost-green);
247
+ }
248
+ .compost-presets {
249
+ display: flex;
250
+ flex-wrap: wrap;
251
+ gap: 6px;
252
+ margin-top: 19px;
253
+ }
254
+ .compost-presets button,
255
+ .compost-moisture button {
256
+ padding: 7px 8px;
257
+ border: 1px solid var(--compost-line);
258
+ border-radius: 8px;
259
+ font-size: 0.6rem;
260
+ }
261
+ .compost-presets button:hover,
262
+ .compost-moisture button:hover {
263
+ border-color: var(--compost-brown);
264
+ color: var(--compost-ink);
265
+ }
266
+ .compost-moisture {
267
+ display: flex;
268
+ flex-wrap: wrap;
269
+ gap: 6px;
270
+ }
271
+ .compost-moisture button[aria-pressed="true"] {
272
+ background: var(--compost-green);
273
+ border-color: var(--compost-green);
274
+ color: var(--n-white);
275
+ }
276
+ .compost-scene-heading {
277
+ display: flex;
278
+ justify-content: space-between;
279
+ align-items: baseline;
280
+ border-bottom: 1px solid var(--compost-line);
281
+ padding-bottom: 12px;
282
+ color: var(--compost-muted);
283
+ font-size: 0.7rem;
284
+ }
285
+ .compost-scene-heading strong {
286
+ color: var(--compost-ink);
287
+ font:
288
+ 700 0.95rem Georgia,
289
+ serif;
290
+ }
291
+ .compost-scene-svg {
292
+ width: 100%;
293
+ min-height: 350px;
294
+ overflow: visible;
295
+ }
296
+ .compost-scene-sun,
297
+ .compost-scene-ground {
298
+ fill: none;
299
+ stroke: var(--compost-line);
300
+ stroke-width: 3;
301
+ stroke-linecap: round;
302
+ }
303
+ .compost-bin {
304
+ fill: var(--compost-bg);
305
+ stroke: var(--compost-ink);
306
+ stroke-width: 3;
307
+ }
308
+ .compost-fill {
309
+ fill: var(--compost-water);
310
+ opacity: 0.25;
311
+ }
312
+ .compost-layer-brown {
313
+ fill: var(--compost-brown);
314
+ }
315
+ .compost-layer-green {
316
+ fill: var(--compost-green);
317
+ }
318
+ .compost-scene-rim,
319
+ .compost-scene-marker {
320
+ fill: none;
321
+ stroke: var(--compost-ink);
322
+ stroke-width: 3;
323
+ stroke-linecap: round;
324
+ }
325
+ .compost-scene-scale {
326
+ fill: var(--compost-muted);
327
+ font:
328
+ 12px ui-monospace,
329
+ monospace;
330
+ }
331
+ .compost-scene-label {
332
+ font:
333
+ 13px ui-monospace,
334
+ monospace;
335
+ }
336
+ .compost-scene-label-brown {
337
+ fill: var(--compost-brown);
338
+ }
339
+ .compost-scene-label-green {
340
+ fill: var(--compost-green);
341
+ }
342
+ .compost-scene-empty {
343
+ fill: var(--compost-muted);
344
+ font:
345
+ 13px ui-monospace,
346
+ monospace;
347
+ text-anchor: middle;
348
+ }
349
+ .compost-result-eyebrow {
350
+ color: var(--compost-muted);
351
+ font-size: 0.7rem;
352
+ }
353
+ .compost-result-capacity {
354
+ margin: 8px 0 28px;
355
+ font:
356
+ 700 clamp(2rem, 4vw, 3.25rem)/1 Georgia,
357
+ serif;
358
+ letter-spacing: -0.06em;
359
+ }
360
+ .compost-result-capacity span {
361
+ font:
362
+ 700 0.85rem ui-monospace,
363
+ monospace;
364
+ letter-spacing: 0;
365
+ color: var(--compost-muted);
366
+ }
367
+ .compost-result-stats {
368
+ display: grid;
369
+ gap: 14px;
370
+ border-top: 1px solid var(--compost-line);
371
+ border-bottom: 1px solid var(--compost-line);
372
+ padding: 17px 0;
373
+ }
374
+ .compost-result-stats div {
375
+ display: flex;
376
+ justify-content: space-between;
377
+ gap: 12px;
378
+ font-size: 0.66rem;
379
+ }
380
+ .compost-result-stats span {
381
+ color: var(--compost-muted);
382
+ }
383
+ .compost-result-stats strong {
384
+ text-align: right;
385
+ }
386
+ .compost-result-status {
387
+ display: flex;
388
+ justify-content: space-between;
389
+ gap: 12px;
390
+ align-items: center;
391
+ margin-top: 22px;
392
+ padding: 11px 12px;
393
+ border-left: 4px solid var(--compost-brown);
394
+ background: color-mix(in srgb, var(--compost-brown) 10%, transparent);
395
+ font-size: 0.68rem;
396
+ }
397
+ .compost-result-status[data-tone="good"] {
398
+ border-color: var(--compost-green);
399
+ background: color-mix(in srgb, var(--compost-green) 12%, transparent);
400
+ }
401
+ .compost-result-status[data-tone="alert"] {
402
+ border-color: var(--n-red);
403
+ background: color-mix(in srgb, var(--n-red) 12%, transparent);
404
+ }
405
+ .compost-result-status strong {
406
+ font-size: 0.78rem;
407
+ }
408
+ .compost-result-advice {
409
+ margin-top: 16px;
410
+ color: var(--compost-muted);
411
+ font-size: 0.66rem;
412
+ line-height: 1.6;
413
+ }
414
+ .compost-result-advice p {
415
+ margin: 9px 0;
416
+ }
417
+ .compost-result-advice b {
418
+ color: var(--compost-ink);
419
+ }
420
+ .compost-result-diagnostics {
421
+ display: flex;
422
+ flex-wrap: wrap;
423
+ gap: 6px;
424
+ margin-top: 13px;
425
+ }
426
+ .compost-result-diagnostics span {
427
+ border: 1px solid var(--compost-line);
428
+ border-radius: 99px;
429
+ padding: 5px 7px;
430
+ font-size: 0.58rem;
431
+ }
432
+ .compost-result-diagnostics span[data-tone="good"] {
433
+ border-color: var(--compost-green);
434
+ color: var(--compost-green);
435
+ }
436
+ .compost-result-diagnostics span[data-tone="alert"] {
437
+ border-color: var(--n-red);
438
+ color: var(--n-red);
439
+ }
440
+ .compost-tool-note {
441
+ display: flex;
442
+ gap: 8px;
443
+ align-items: center;
444
+ padding: 15px 22px;
445
+ border-top: 1px solid var(--compost-line);
446
+ color: var(--compost-muted);
447
+ font-size: 0.62rem;
448
+ }
449
+ .compost-tool-note span {
450
+ color: var(--compost-brown);
451
+ font-size: 1.1rem;
452
+ }
453
+ .compost-seo,
454
+ .compost-references {
455
+ color: var(--n-ink);
456
+ }
457
+ .compost-seo h2,
458
+ .compost-references h2 {
459
+ font:
460
+ 700 1em Georgia,
461
+ serif;
462
+ }
463
+ .compost-seo p,
464
+ .compost-seo li,
465
+ .compost-references li {
466
+ line-height: 1.7;
467
+ }
468
+ .compost-seo aside {
469
+ border-left: 3px solid var(--n-brown);
470
+ padding-left: 16px;
471
+ }
472
+ .compost-references a {
473
+ color: var(--n-brown);
474
+ }
475
+
476
+ @media (max-width: 900px) {
477
+ .compost-tool-body {
478
+ grid-template-columns: 1fr 1fr;
479
+ }
480
+ .compost-controls {
481
+ border-right: 1px solid var(--compost-line);
482
+ }
483
+ .compost-scene {
484
+ border-right: 0;
485
+ }
486
+ .compost-result {
487
+ grid-column: 1 / -1;
488
+ border-top: 1px solid var(--compost-line);
489
+ border-left: 0;
490
+ }
491
+ }
492
+
493
+ @media (max-width: 620px) {
494
+ .compost-tool-body {
495
+ display: block;
496
+ }
497
+ .compost-controls,
498
+ .compost-scene,
499
+ .compost-result {
500
+ border: 0;
501
+ border-bottom: 1px solid var(--compost-line);
502
+ }
503
+ .compost-scene-svg {
504
+ min-height: 300px;
505
+ }
506
+ .compost-result {
507
+ min-height: 360px;
508
+ }
509
+ }
@@ -0,0 +1,189 @@
1
+ import {
2
+ applyPreset,
3
+ calculateCompost,
4
+ convertInputs,
5
+ type BinShape,
6
+ type CompostInputs,
7
+ type MoistureLevel,
8
+ type UnitSystem,
9
+ } from "./logic";
10
+ import { renderCompost, syncInputLabels } from "./dom-views";
11
+ import { loadCompostState, saveCompostState } from "./storage";
12
+ import type { CompostBinVolumeRatioCalculatorUI } from "./ui";
13
+
14
+ const defaults: CompostInputs = {
15
+ unitSystem: "metric",
16
+ shape: "rectangular",
17
+ width: 80,
18
+ depth: 60,
19
+ diameter: 60,
20
+ height: 80,
21
+ browns: 36,
22
+ greens: 14,
23
+ moisture: "sponge",
24
+ };
25
+
26
+ function validNumber(value: string | undefined, fallback: number): number {
27
+ const parsed = Number(value);
28
+ return Number.isFinite(parsed) ? Math.max(0, parsed) : fallback;
29
+ }
30
+
31
+ function readInputs(root: HTMLElement, previous: CompostInputs): CompostInputs {
32
+ const value = (name: string): string | undefined =>
33
+ root.querySelector<HTMLInputElement>(`[data-input="${name}"]`)?.value;
34
+ return {
35
+ ...previous,
36
+ width: validNumber(value("width"), previous.width),
37
+ depth: validNumber(value("depth"), previous.depth),
38
+ diameter: validNumber(value("diameter"), previous.diameter),
39
+ height: validNumber(value("height"), previous.height),
40
+ browns: validNumber(value("browns"), previous.browns),
41
+ greens: validNumber(value("greens"), previous.greens),
42
+ };
43
+ }
44
+
45
+ function writeInputs(root: HTMLElement, inputs: CompostInputs): void {
46
+ const values: Record<string, number> = {
47
+ width: inputs.width,
48
+ depth: inputs.depth,
49
+ diameter: inputs.diameter,
50
+ height: inputs.height,
51
+ browns: inputs.browns,
52
+ greens: inputs.greens,
53
+ };
54
+ Object.entries(values).forEach(([name, value]) => {
55
+ const input = root.querySelector<HTMLInputElement>(
56
+ `[data-input="${name}"]`,
57
+ );
58
+ if (input) input.value = String(Number(value.toFixed(2)));
59
+ });
60
+ }
61
+
62
+ function setShape(root: HTMLElement, shape: BinShape): void {
63
+ root
64
+ .querySelector<HTMLElement>("[data-dimensions]")
65
+ ?.setAttribute("data-shape", shape);
66
+ root
67
+ .querySelector<HTMLElement>("[data-shape-rectangular]")
68
+ ?.classList.toggle("is-active", shape === "rectangular");
69
+ root
70
+ .querySelector<HTMLElement>("[data-shape-cylindrical]")
71
+ ?.classList.toggle("is-active", shape === "cylindrical");
72
+ }
73
+
74
+ function renderState(
75
+ root: HTMLElement,
76
+ inputs: CompostInputs,
77
+ ui: CompostBinVolumeRatioCalculatorUI,
78
+ ): void {
79
+ const result = calculateCompost(inputs);
80
+ syncInputLabels(root, inputs, ui);
81
+ setShape(root, inputs.shape);
82
+ renderCompost(root, inputs, result, ui);
83
+ saveCompostState(inputs);
84
+ }
85
+
86
+ function bindNumberInputs(
87
+ root: HTMLElement,
88
+ getState: () => CompostInputs,
89
+ setState: (next: CompostInputs) => void,
90
+ ui: CompostBinVolumeRatioCalculatorUI,
91
+ ): void {
92
+ root.querySelectorAll<HTMLInputElement>("[data-input]").forEach((input) =>
93
+ input.addEventListener("input", () => {
94
+ const next = readInputs(root, getState());
95
+ setState(next);
96
+ renderState(root, next, ui);
97
+ }),
98
+ );
99
+ }
100
+
101
+ function bindPresets(
102
+ root: HTMLElement,
103
+ getState: () => CompostInputs,
104
+ setState: (next: CompostInputs) => void,
105
+ ui: CompostBinVolumeRatioCalculatorUI,
106
+ ): void {
107
+ root.querySelectorAll<HTMLButtonElement>("[data-preset]").forEach((button) =>
108
+ button.addEventListener("click", () => {
109
+ const next = applyPreset(
110
+ button.dataset.preset as "kitchen" | "garden" | "leafy",
111
+ getState().unitSystem,
112
+ );
113
+ setState(next);
114
+ writeInputs(root, next);
115
+ renderState(root, next, ui);
116
+ }),
117
+ );
118
+ }
119
+
120
+ function bindChoiceButtons(
121
+ root: HTMLElement,
122
+ getState: () => CompostInputs,
123
+ setState: (next: CompostInputs) => void,
124
+ ui: CompostBinVolumeRatioCalculatorUI,
125
+ ): void {
126
+ root.querySelectorAll<HTMLButtonElement>("[data-shape]").forEach((button) =>
127
+ button.addEventListener("click", () => {
128
+ const next = { ...getState(), shape: button.dataset.shape as BinShape };
129
+ setState(next);
130
+ renderState(root, next, ui);
131
+ }),
132
+ );
133
+ root
134
+ .querySelectorAll<HTMLButtonElement>("[data-moisture]")
135
+ .forEach((button) =>
136
+ button.addEventListener("click", () => {
137
+ const next = {
138
+ ...getState(),
139
+ moisture: button.dataset.moisture as MoistureLevel,
140
+ };
141
+ setState(next);
142
+ renderState(root, next, ui);
143
+ }),
144
+ );
145
+ }
146
+
147
+ function bindUnits(
148
+ root: HTMLElement,
149
+ getState: () => CompostInputs,
150
+ setState: (next: CompostInputs) => void,
151
+ ui: CompostBinVolumeRatioCalculatorUI,
152
+ ): void {
153
+ root.querySelectorAll<HTMLButtonElement>("[data-unit]").forEach((button) =>
154
+ button.addEventListener("click", () => {
155
+ const target = button.dataset.unit as UnitSystem;
156
+ const next = convertInputs(getState(), target);
157
+ setState(next);
158
+ writeInputs(root, next);
159
+ renderState(root, next, ui);
160
+ }),
161
+ );
162
+ }
163
+
164
+ export function mountCompostCalculator(
165
+ root: HTMLElement,
166
+ ui: CompostBinVolumeRatioCalculatorUI,
167
+ ): void {
168
+ let state: CompostInputs = {
169
+ ...defaults,
170
+ ...(loadCompostState() ?? {}),
171
+ } as CompostInputs;
172
+ const getState = (): CompostInputs => state;
173
+ const setState = (next: CompostInputs): void => {
174
+ state = next;
175
+ };
176
+ bindNumberInputs(root, getState, setState, ui);
177
+ bindPresets(root, getState, setState, ui);
178
+ bindChoiceButtons(root, getState, setState, ui);
179
+ bindUnits(root, getState, setState, ui);
180
+ root
181
+ .querySelector<HTMLButtonElement>("[data-reset]")
182
+ ?.addEventListener("click", () => {
183
+ state = { ...defaults };
184
+ writeInputs(root, state);
185
+ renderState(root, state, ui);
186
+ });
187
+ writeInputs(root, state);
188
+ renderState(root, state, ui);
189
+ }