@jjlmoya/utils-aquarium 1.57.0 → 1.59.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 (68) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +3 -1
  3. package/src/entries.ts +7 -1
  4. package/src/index.ts +2 -0
  5. package/src/layouts/PreviewLayout.astro +14 -1
  6. package/src/tests/i18n_coverage.test.ts +3 -1
  7. package/src/tests/tool_validation.test.ts +8 -4
  8. package/src/tool/aquariumFilterTurnoverStockingPlanner/aquarium-filter-turnover-stocking-planner.css +404 -0
  9. package/src/tool/aquariumFilterTurnoverStockingPlanner/bibliography.astro +7 -0
  10. package/src/tool/aquariumFilterTurnoverStockingPlanner/bibliography.ts +16 -0
  11. package/src/tool/aquariumFilterTurnoverStockingPlanner/component.astro +140 -0
  12. package/src/tool/aquariumFilterTurnoverStockingPlanner/content-builder.ts +43 -0
  13. package/src/tool/aquariumFilterTurnoverStockingPlanner/controller.ts +94 -0
  14. package/src/tool/aquariumFilterTurnoverStockingPlanner/dom-views.ts +63 -0
  15. package/src/tool/aquariumFilterTurnoverStockingPlanner/entry.ts +27 -0
  16. package/src/tool/aquariumFilterTurnoverStockingPlanner/evaluator.ts +17 -0
  17. package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/de.ts +48 -0
  18. package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/en.ts +124 -0
  19. package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/es.ts +41 -0
  20. package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/fr.ts +36 -0
  21. package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/id.ts +36 -0
  22. package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/it.ts +36 -0
  23. package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/ja.ts +36 -0
  24. package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/ko.ts +36 -0
  25. package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/nl.ts +36 -0
  26. package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/pl.ts +36 -0
  27. package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/pt.ts +36 -0
  28. package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/ru.ts +36 -0
  29. package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/sv.ts +36 -0
  30. package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/tr.ts +36 -0
  31. package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/zh.ts +36 -0
  32. package/src/tool/aquariumFilterTurnoverStockingPlanner/index.ts +11 -0
  33. package/src/tool/aquariumFilterTurnoverStockingPlanner/logic.test.ts +78 -0
  34. package/src/tool/aquariumFilterTurnoverStockingPlanner/logic.ts +99 -0
  35. package/src/tool/aquariumFilterTurnoverStockingPlanner/seo.astro +14 -0
  36. package/src/tool/aquariumFilterTurnoverStockingPlanner/storage.ts +21 -0
  37. package/src/tool/aquariumFilterTurnoverStockingPlanner/ui.ts +40 -0
  38. package/src/tool/aquariumLightingPhotoperiodPlanner/aquarium-lighting-photoperiod-planner.css +667 -0
  39. package/src/tool/aquariumLightingPhotoperiodPlanner/bibliography.astro +6 -0
  40. package/src/tool/aquariumLightingPhotoperiodPlanner/bibliography.ts +12 -0
  41. package/src/tool/aquariumLightingPhotoperiodPlanner/component.astro +92 -0
  42. package/src/tool/aquariumLightingPhotoperiodPlanner/controller.ts +179 -0
  43. package/src/tool/aquariumLightingPhotoperiodPlanner/dom-views.test.ts +26 -0
  44. package/src/tool/aquariumLightingPhotoperiodPlanner/dom-views.ts +187 -0
  45. package/src/tool/aquariumLightingPhotoperiodPlanner/entry.ts +29 -0
  46. package/src/tool/aquariumLightingPhotoperiodPlanner/evaluator.ts +47 -0
  47. package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/de.ts +125 -0
  48. package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/en.ts +125 -0
  49. package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/es.ts +110 -0
  50. package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/fr.ts +110 -0
  51. package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/id.ts +110 -0
  52. package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/it.ts +110 -0
  53. package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/ja.ts +110 -0
  54. package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/ko.ts +110 -0
  55. package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/nl.ts +110 -0
  56. package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/pl.ts +110 -0
  57. package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/pt.ts +110 -0
  58. package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/ru.ts +110 -0
  59. package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/sv.ts +110 -0
  60. package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/tr.ts +110 -0
  61. package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/zh.ts +110 -0
  62. package/src/tool/aquariumLightingPhotoperiodPlanner/index.ts +11 -0
  63. package/src/tool/aquariumLightingPhotoperiodPlanner/logic.test.ts +115 -0
  64. package/src/tool/aquariumLightingPhotoperiodPlanner/logic.ts +248 -0
  65. package/src/tool/aquariumLightingPhotoperiodPlanner/seo.astro +13 -0
  66. package/src/tool/aquariumLightingPhotoperiodPlanner/storage.ts +39 -0
  67. package/src/tool/aquariumLightingPhotoperiodPlanner/ui.ts +44 -0
  68. package/src/tools.ts +12 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-aquarium",
3
- "version": "1.57.0",
3
+ "version": "1.59.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -1,9 +1,11 @@
1
1
  import type { AquariumCategoryEntry } from '../types';
2
2
  import { aquariumTankVolumeWaterChangeCalculator } from '../tool/aquariumTankVolumeWaterChangeCalculator/entry';
3
+ import { aquariumFilterTurnoverStockingPlanner } from '../tool/aquariumFilterTurnoverStockingPlanner/entry';
4
+ import { aquariumLightingPhotoperiodPlanner } from '../tool/aquariumLightingPhotoperiodPlanner/entry';
3
5
 
4
6
  export const aquariumCategory: AquariumCategoryEntry = {
5
7
  icon: 'mdi:fishbowl-outline',
6
- tools: [aquariumTankVolumeWaterChangeCalculator],
8
+ tools: [aquariumTankVolumeWaterChangeCalculator, aquariumFilterTurnoverStockingPlanner, aquariumLightingPhotoperiodPlanner],
7
9
  i18n: {
8
10
  en: () => import('./i18n/en').then((m) => m.content),
9
11
  es: () => import('./i18n/es').then((m) => m.content),
package/src/entries.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  export { aquariumTankVolumeWaterChangeCalculator } from './tool/aquariumTankVolumeWaterChangeCalculator/entry';
2
2
  export type { AquariumTankVolumeWaterChangeCalculatorLocaleContent } from './tool/aquariumTankVolumeWaterChangeCalculator/entry';
3
+ export { aquariumFilterTurnoverStockingPlanner } from './tool/aquariumFilterTurnoverStockingPlanner/entry';
4
+ export type { AquariumFilterTurnoverStockingPlannerLocaleContent } from './tool/aquariumFilterTurnoverStockingPlanner/entry';
5
+ export { aquariumLightingPhotoperiodPlanner } from './tool/aquariumLightingPhotoperiodPlanner/entry';
6
+ export type { AquariumLightingPhotoperiodPlannerLocaleContent } from './tool/aquariumLightingPhotoperiodPlanner/entry';
3
7
  export { aquariumCategory } from './category';
4
8
 
5
9
  import { aquariumTankVolumeWaterChangeCalculator } from './tool/aquariumTankVolumeWaterChangeCalculator/entry';
10
+ import { aquariumFilterTurnoverStockingPlanner } from './tool/aquariumFilterTurnoverStockingPlanner/entry';
11
+ import { aquariumLightingPhotoperiodPlanner } from './tool/aquariumLightingPhotoperiodPlanner/entry';
6
12
 
7
- export const ALL_ENTRIES = [aquariumTankVolumeWaterChangeCalculator];
13
+ export const ALL_ENTRIES = [aquariumTankVolumeWaterChangeCalculator, aquariumFilterTurnoverStockingPlanner, aquariumLightingPhotoperiodPlanner];
package/src/index.ts CHANGED
@@ -18,3 +18,5 @@ export type {
18
18
  export { ALL_ENTRIES, ALL_TOOLS } from './tools';
19
19
  export { AQUARIUM_TANK_VOLUME_WATER_CHANGE_CALCULATOR_TOOL, aquariumTankVolumeWaterChangeCalculator } from './tool/aquariumTankVolumeWaterChangeCalculator/index';
20
20
  export type { AquariumTankVolumeWaterChangeCalculatorUI, AquariumTankVolumeWaterChangeCalculatorLocaleContent } from './tool/aquariumTankVolumeWaterChangeCalculator/index';
21
+ export { AQUARIUM_FILTER_TURNOVER_STOCKING_PLANNER_TOOL, aquariumFilterTurnoverStockingPlanner } from './tool/aquariumFilterTurnoverStockingPlanner/index';
22
+ export type { AquariumFilterTurnoverStockingPlannerUI, AquariumFilterTurnoverStockingPlannerLocaleContent } from './tool/aquariumFilterTurnoverStockingPlanner/index';
@@ -82,6 +82,8 @@ const { title, currentLocale = "es", localeUrls = {}, hasSidebar = false } = Ast
82
82
 
83
83
  main {
84
84
  padding: 0 2rem;
85
+ min-width: 0;
86
+ width: 100%;
85
87
  }
86
88
 
87
89
  .page-wrapper {
@@ -112,6 +114,17 @@ const { title, currentLocale = "es", localeUrls = {}, hasSidebar = false } = Ast
112
114
  .sidebar-area {
113
115
  display: none;
114
116
  }
117
+
118
+ :global(.utility-header-title),
119
+ :global(.utility-header-title > *) {
120
+ max-width: 100%;
121
+ min-width: 0;
122
+ overflow-wrap: anywhere;
123
+ }
124
+
125
+ main {
126
+ overflow-x: hidden;
127
+ padding: 0 1rem;
128
+ }
115
129
  }
116
130
  </style>
117
-
@@ -2,12 +2,14 @@ import { describe, expect, it } from 'vitest';
2
2
  import { ALL_TOOLS } from '../tools';
3
3
 
4
4
  const EXPECTED_LOCALES = ['de', 'en', 'es', 'fr', 'id', 'it', 'ja', 'ko', 'nl', 'pl', 'pt', 'ru', 'sv', 'tr', 'zh'];
5
+ const ENGLISH_FIRST_TOOL_IDS = new Set<string>([]);
5
6
 
6
7
  describe('I18n Coverage Validation', () => {
7
8
  ALL_TOOLS.forEach(({ entry }) => {
8
9
  describe('Tool: ' + entry.id, () => {
9
10
  it('has the current phase locale set', () => {
10
- expect(Object.keys(entry.i18n)).toEqual(EXPECTED_LOCALES);
11
+ const expectedLocales = ENGLISH_FIRST_TOOL_IDS.has(entry.id) ? ['en'] : EXPECTED_LOCALES;
12
+ expect(Object.keys(entry.i18n)).toEqual(expectedLocales);
11
13
  });
12
14
  });
13
15
  });
@@ -3,9 +3,13 @@ import { ALL_TOOLS } from '../tools';
3
3
  import { aquariumCategory } from '../data';
4
4
 
5
5
  describe('Tool Validation Suite', () => {
6
- it('registers exactly one aquarium production tool', () => {
7
- expect(ALL_TOOLS.length).toBe(1);
8
- expect(aquariumCategory.tools).toHaveLength(1);
9
- expect(aquariumCategory.tools[0]?.id).toBe('aquarium-tank-volume-water-change-calculator');
6
+ it('registers exactly three aquarium production tools', () => {
7
+ expect(ALL_TOOLS.length).toBe(3);
8
+ expect(aquariumCategory.tools).toHaveLength(3);
9
+ expect(aquariumCategory.tools.map((tool) => tool.id)).toEqual([
10
+ 'aquarium-tank-volume-water-change-calculator',
11
+ 'aquarium-filter-turnover-stocking-planner',
12
+ 'aquarium-lighting-photoperiod-schedule-planner',
13
+ ]);
10
14
  });
11
15
  });
@@ -0,0 +1,404 @@
1
+ :root {
2
+ --n-ink: #10272a;
3
+ --n-muted: #4d6a6c;
4
+ --n-paper: #f5fbf8;
5
+ --n-card: #fff;
6
+ --n-line: #c7ded8;
7
+ --n-water: #0f7f83;
8
+ --n-water-soft: #cbeeed;
9
+ --n-coral: #bd553c;
10
+ --n-gold: #b27a25;
11
+ --n-shadow: rgba(16, 39, 42, .12);
12
+ }
13
+
14
+ .theme-dark {
15
+ --n-ink: #effbf8;
16
+ --n-muted: #b3cbc5;
17
+ --n-paper: #101c1d;
18
+ --n-card: #192829;
19
+ --n-line: #3b5e5d;
20
+ --n-water: #56d2ca;
21
+ --n-water-soft: #1e4e50;
22
+ --n-coral: #f08b6f;
23
+ --n-gold: #e8ba59;
24
+ --n-shadow: rgba(0, 0, 0, .3);
25
+ }
26
+
27
+ .aquarium-filter-planner {
28
+ --n-ink: #10272a;
29
+ --n-muted: #4d6a6c;
30
+ --n-paper: #f5fbf8;
31
+ --n-card: #fff;
32
+ --n-line: #c7ded8;
33
+ --n-water: #0f7f83;
34
+ --n-water-soft: #cbeeed;
35
+ --n-coral: #bd553c;
36
+ --n-gold: #b27a25;
37
+ --n-shadow: rgba(16, 39, 42, .12);
38
+
39
+ box-sizing: border-box;
40
+ color: var(--n-ink);
41
+ background: var(--n-paper);
42
+ border: 1px solid var(--n-line);
43
+ border-radius: 24px;
44
+ box-shadow: 0 18px 42px var(--n-shadow);
45
+ overflow: hidden;
46
+ min-width: 0;
47
+ width: 100%;
48
+ max-width: 100%;
49
+ }
50
+
51
+ .theme-dark .aquarium-filter-planner {
52
+ --n-ink: #effbf8;
53
+ --n-muted: #b3cbc5;
54
+ --n-paper: #101c1d;
55
+ --n-card: #192829;
56
+ --n-line: #3b5e5d;
57
+ --n-water: #56d2ca;
58
+ --n-water-soft: #1e4e50;
59
+ --n-coral: #f08b6f;
60
+ --n-gold: #e8ba59;
61
+ --n-shadow: rgba(0, 0, 0, .3);
62
+ }
63
+
64
+ .planner-main {
65
+ display: grid;
66
+ grid-template-columns: minmax(250px, .8fr) minmax(320px, 1.2fr);
67
+ gap: 0;
68
+ min-width: 0;
69
+ }
70
+
71
+ .planner-controls,
72
+ .planner-result {
73
+ min-width: 0;
74
+ padding: 2rem;
75
+ }
76
+
77
+ .planner-controls {
78
+ border-right: 1px solid var(--n-line);
79
+ }
80
+
81
+ .preset-label,
82
+ .result-eyebrow,
83
+ .field-label,
84
+ .visual-caption {
85
+ color: var(--n-muted);
86
+ font-size: .72rem;
87
+ font-weight: 800;
88
+ letter-spacing: .12em;
89
+ text-transform: uppercase;
90
+ }
91
+
92
+ .preset-label,
93
+ .result-eyebrow {
94
+ margin: 0 0 .8rem;
95
+ }
96
+
97
+ .preset-row,
98
+ .setup-row {
99
+ display: flex;
100
+ flex-wrap: wrap;
101
+ gap: .5rem;
102
+ }
103
+
104
+ .preset-button,
105
+ .setup-button {
106
+ border: 1px solid var(--n-line);
107
+ border-radius: 999px;
108
+ color: var(--n-ink);
109
+ background: var(--n-card);
110
+ cursor: pointer;
111
+ font: inherit;
112
+ font-size: .82rem;
113
+ padding: .65rem .85rem;
114
+ }
115
+
116
+ .preset-button:hover,
117
+ .preset-button:focus-visible,
118
+ .setup-button:hover,
119
+ .setup-button:focus-visible,
120
+ .setup-button.is-active {
121
+ border-color: var(--n-water);
122
+ color: var(--n-water);
123
+ outline: 3px solid var(--n-water-soft);
124
+ }
125
+
126
+ .field-stack {
127
+ display: grid;
128
+ gap: 1.15rem;
129
+ margin-top: 2rem;
130
+ }
131
+
132
+ .field {
133
+ display: grid;
134
+ gap: .45rem;
135
+ }
136
+
137
+ .field-label {
138
+ color: var(--n-ink);
139
+ letter-spacing: .04em;
140
+ }
141
+
142
+ .field-hint {
143
+ color: var(--n-muted);
144
+ font-size: .78rem;
145
+ line-height: 1.45;
146
+ }
147
+
148
+ .field input[type='number'] {
149
+ width: 100%;
150
+ box-sizing: border-box;
151
+ border: 1px solid var(--n-line);
152
+ border-radius: 12px;
153
+ color: var(--n-ink);
154
+ background: var(--n-card);
155
+ font: inherit;
156
+ font-size: 1.05rem;
157
+ min-height: 3rem;
158
+ padding: .7rem .8rem;
159
+ }
160
+
161
+ .field input:focus-visible {
162
+ border-color: var(--n-water);
163
+ outline: 3px solid var(--n-water-soft);
164
+ }
165
+
166
+ .range-wrap {
167
+ display: grid;
168
+ grid-template-columns: 1fr auto;
169
+ align-items: center;
170
+ gap: .8rem;
171
+ }
172
+
173
+ .field input[type='range'] {
174
+ accent-color: var(--n-water);
175
+ width: 100%;
176
+ }
177
+
178
+ .range-value {
179
+ color: var(--n-water);
180
+ font-weight: 800;
181
+ min-width: 3.3rem;
182
+ text-align: right;
183
+ }
184
+
185
+ .setup-button[aria-pressed='true'] {
186
+ background: var(--n-water-soft);
187
+ }
188
+
189
+ .result-hero {
190
+ display: flex;
191
+ align-items: flex-end;
192
+ justify-content: space-between;
193
+ gap: 1rem;
194
+ }
195
+
196
+ .result-hero h2 {
197
+ font-size: clamp(2.5rem, 7vw, 5.5rem);
198
+ letter-spacing: -.08em;
199
+ line-height: .9;
200
+ margin: 0;
201
+ }
202
+
203
+ .result-status {
204
+ border: 1px solid var(--n-line);
205
+ border-radius: 999px;
206
+ color: var(--n-water);
207
+ font-size: .78rem;
208
+ font-weight: 800;
209
+ padding: .5rem .75rem;
210
+ text-align: center;
211
+ }
212
+
213
+ [data-status='under'] .result-status {
214
+ color: var(--n-coral);
215
+ }
216
+
217
+ [data-status='high'] .result-status {
218
+ color: var(--n-gold);
219
+ }
220
+
221
+ .tank-scene {
222
+ background: var(--n-card);
223
+ border: 1px solid var(--n-line);
224
+ border-radius: 18px;
225
+ margin-top: 1.8rem;
226
+ padding: .5rem;
227
+ }
228
+
229
+ .tank-scene svg {
230
+ display: block;
231
+ height: auto;
232
+ max-width: 100%;
233
+ min-width: 0;
234
+ width: 100%;
235
+ }
236
+
237
+ .tank-frame {
238
+ fill: var(--n-card);
239
+ stroke: var(--n-ink);
240
+ stroke-width: 2;
241
+ }
242
+
243
+ .tank-water {
244
+ fill: var(--n-water-soft);
245
+ }
246
+
247
+ .filter-body {
248
+ fill: var(--n-ink);
249
+ }
250
+
251
+ .pipe,
252
+ .flow-line {
253
+ fill: none;
254
+ stroke: var(--n-water);
255
+ stroke-linecap: round;
256
+ stroke-width: 5;
257
+ }
258
+
259
+ .flow-line {
260
+ stroke-width: calc(2px + var(--n-current) * 4px);
261
+ stroke-dasharray: 8 11;
262
+ animation: current-drift 2.4s linear infinite;
263
+ }
264
+
265
+ .water-label,
266
+ .filter-label {
267
+ fill: var(--n-ink);
268
+ font-size: 9px;
269
+ font-weight: 800;
270
+ letter-spacing: .08em;
271
+ text-transform: uppercase;
272
+ }
273
+
274
+ .filter-label {
275
+ fill: var(--n-card);
276
+ }
277
+
278
+ .bubble {
279
+ fill: var(--n-coral);
280
+ }
281
+
282
+ .metrics {
283
+ display: grid;
284
+ grid-template-columns: repeat(3, 1fr);
285
+ gap: .7rem;
286
+ margin-top: 1rem;
287
+ }
288
+
289
+ .metric {
290
+ border-top: 2px solid var(--n-line);
291
+ padding-top: .75rem;
292
+ }
293
+
294
+ .metric-label {
295
+ color: var(--n-muted);
296
+ display: block;
297
+ font-size: .75rem;
298
+ line-height: 1.3;
299
+ }
300
+
301
+ .metric-value {
302
+ display: block;
303
+ font-size: 1rem;
304
+ font-weight: 800;
305
+ margin-top: .3rem;
306
+ }
307
+
308
+ .turnover-gauge {
309
+ margin-top: 1.5rem;
310
+ }
311
+
312
+ .gauge-head {
313
+ align-items: baseline;
314
+ display: flex;
315
+ justify-content: space-between;
316
+ gap: 1rem;
317
+ }
318
+
319
+ .gauge-head strong {
320
+ font-size: .95rem;
321
+ }
322
+
323
+ .gauge-track {
324
+ background: var(--n-water-soft);
325
+ border-radius: 999px;
326
+ height: .75rem;
327
+ margin-top: .55rem;
328
+ overflow: hidden;
329
+ }
330
+
331
+ .gauge-progress {
332
+ background: var(--n-water);
333
+ border-radius: inherit;
334
+ height: 100%;
335
+ transition: width .25s ease;
336
+ width: var(--n-progress, 4%);
337
+ }
338
+
339
+ .load-line,
340
+ .caution {
341
+ border-left: 3px solid var(--n-water);
342
+ margin-top: 1.25rem;
343
+ padding-left: .85rem;
344
+ }
345
+
346
+ .load-line {
347
+ color: var(--n-muted);
348
+ font-size: .86rem;
349
+ }
350
+
351
+ .load-line strong {
352
+ color: var(--n-ink);
353
+ }
354
+
355
+ .caution {
356
+ border-left-color: var(--n-coral);
357
+ }
358
+
359
+ .caution h3 {
360
+ font-size: .9rem;
361
+ margin: 0 0 .3rem;
362
+ }
363
+
364
+ .caution p {
365
+ color: var(--n-muted);
366
+ font-size: .83rem;
367
+ line-height: 1.5;
368
+ margin: 0;
369
+ }
370
+
371
+ @keyframes current-drift {
372
+ to { stroke-dashoffset: -38; }
373
+ }
374
+
375
+ @media (max-width: 760px) {
376
+ .aquarium-filter-planner {
377
+ max-width: 100%;
378
+ width: 100%;
379
+ }
380
+
381
+ .planner-main {
382
+ grid-template-columns: 1fr;
383
+ }
384
+
385
+ .planner-controls {
386
+ border-bottom: 1px solid var(--n-line);
387
+ border-right: 0;
388
+ }
389
+
390
+ .planner-controls,
391
+ .planner-result {
392
+ padding: 1.25rem;
393
+ }
394
+
395
+ .result-hero {
396
+ align-items: flex-start;
397
+ flex-direction: column;
398
+ }
399
+ }
400
+
401
+ @media (prefers-reduced-motion: reduce) {
402
+ .flow-line { animation: none; }
403
+ .gauge-progress { transition: none; }
404
+ }
@@ -0,0 +1,7 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { bibliography } from './bibliography';
4
+
5
+ ---
6
+
7
+ <SharedBibliography links={bibliography} />
@@ -0,0 +1,16 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Fluval, 06 Series Filter Manual: turnover rates and flow rates',
6
+ url: 'https://fluvalaquatics.com/manuals/Fluval_A202-A207-A212-A217_06-Filter-Series_Manual_NA.pdf',
7
+ },
8
+ {
9
+ name: 'OATA, How to set up and look after a freshwater tank or aquarium',
10
+ url: 'https://ornamentalfish.org/what-we-do/advice-information/care-sheets/caresheets-tropical-freshwater-fish/how-to-set-up-and-look-after-a-freshwater-tank-aquarium/',
11
+ },
12
+ {
13
+ name: 'Deutscher Tierschutzbund, Die Haltung von Aquarienfischen',
14
+ url: 'https://www.tierschutzbund.de/fileadmin/Seiten/tierschutzbund.de/Downloads/Broschueren/Broschuere_Aquarienfische_Haltung.pdf',
15
+ },
16
+ ];
@@ -0,0 +1,140 @@
1
+ ---
2
+ import type { AquariumFilterTurnoverStockingPlannerUI } from './ui';
3
+ import './aquarium-filter-turnover-stocking-planner.css';
4
+
5
+ interface Props {
6
+ ui: AquariumFilterTurnoverStockingPlannerUI;
7
+ }
8
+
9
+ const { ui } = Astro.props;
10
+ ---
11
+
12
+ <div class="aquarium-filter-planner" data-aquarium-filter-planner>
13
+ <div class="planner-main">
14
+ <section class="planner-controls" aria-label="Aquarium filter inputs">
15
+ <p class="preset-label">{ui.presetsLabel}</p>
16
+ <div class="preset-row">
17
+ <button class="preset-button" type="button" data-preset="community">{ui.presetCommunity}</button>
18
+ <button class="preset-button" type="button" data-preset="planted">{ui.presetPlanted}</button>
19
+ <button class="preset-button" type="button" data-preset="highWaste">{ui.presetHighWaste}</button>
20
+ </div>
21
+
22
+ <div class="field-stack">
23
+ <label class="field">
24
+ <span class="field-label">{ui.tankVolumeLabel}</span>
25
+ <input name="tankVolumeLiters" type="number" min="10" max="2000" step="1" value="100" inputmode="decimal" />
26
+ <span class="field-hint">{ui.tankVolumeHint}</span>
27
+ </label>
28
+
29
+ <label class="field">
30
+ <span class="field-label">{ui.ratedFlowLabel}</span>
31
+ <input name="ratedFlowLitersPerHour" type="number" min="50" max="20000" step="10" value="600" inputmode="decimal" />
32
+ <span class="field-hint">{ui.ratedFlowHint}</span>
33
+ </label>
34
+
35
+ <label class="field">
36
+ <span class="field-label">{ui.flowLossLabel}</span>
37
+ <span class="range-wrap">
38
+ <input name="flowLossPercent" type="range" min="0" max="90" step="5" value="25" />
39
+ <output id="aquarium-filter-flow-loss-value" class="range-value">25%</output>
40
+ </span>
41
+ <span class="field-hint">{ui.flowLossHint}</span>
42
+ </label>
43
+
44
+ <label class="field">
45
+ <span class="field-label">{ui.fishCountLabel}</span>
46
+ <input name="fishCount" type="number" min="0" max="200" step="1" value="10" inputmode="numeric" />
47
+ </label>
48
+
49
+ <label class="field">
50
+ <span class="field-label">{ui.averageLengthLabel}</span>
51
+ <input name="averageAdultLengthCm" type="number" min="0" max="80" step="0.5" value="5" inputmode="decimal" />
52
+ </label>
53
+
54
+ <div class="field">
55
+ <span class="field-label">{ui.setupLabel}</span>
56
+ <div class="setup-row" role="group" aria-label={ui.setupLabel}>
57
+ <button class="setup-button" type="button" data-setup="community" aria-pressed="false">{ui.setupCommunity}</button>
58
+ <button class="setup-button" type="button" data-setup="planted" aria-pressed="false">{ui.setupPlanted}</button>
59
+ <button class="setup-button" type="button" data-setup="highWaste" aria-pressed="false">{ui.setupHighWaste}</button>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </section>
64
+
65
+ <section class="planner-result" aria-live="polite">
66
+ <div class="result-hero">
67
+ <div>
68
+ <p class="result-eyebrow">{ui.resultEyebrow}</p>
69
+ <h2 id="aquarium-filter-effective-turnover">4.5×</h2>
70
+ </div>
71
+ <div id="aquarium-filter-status" class="result-status">{ui.statusTarget}</div>
72
+ </div>
73
+
74
+ <div class="tank-scene">
75
+ <svg viewBox="0 0 620 250" role="img" aria-labelledby="aquarium-filter-scene-title aquarium-filter-scene-desc">
76
+ <title id="aquarium-filter-scene-title">{ui.currentTitle}</title>
77
+ <desc id="aquarium-filter-scene-desc">{ui.currentDescription}</desc>
78
+ <rect class="tank-frame" x="48" y="28" width="440" height="188" rx="10" />
79
+ <rect class="tank-water" x="50" y="92" width="436" height="122" rx="8" />
80
+ <rect class="filter-body" x="514" y="72" width="66" height="110" rx="10" />
81
+ <path class="pipe" d="M514 96 H480 V62 H370" />
82
+ <path class="pipe" d="M514 160 H472 V194 H140" />
83
+ <path id="aquarium-filter-current-ribbon" class="flow-line" d="M372 62 C310 84 260 82 190 106 C130 126 112 160 154 184 C204 213 284 192 362 194" />
84
+ <circle class="bubble" cx="102" cy="75" r="5" />
85
+ <circle class="bubble" cx="125" cy="58" r="3" />
86
+ <circle class="bubble" cx="145" cy="78" r="2" />
87
+ <text class="water-label" x="70" y="112">AQUARIUM CURRENT</text>
88
+ <text class="filter-label" x="525" y="121">FILTER</text>
89
+ <text class="filter-label" x="525" y="136">OUTPUT</text>
90
+ </svg>
91
+ </div>
92
+
93
+ <div class="metrics">
94
+ <div class="metric">
95
+ <span class="metric-label">{ui.targetLabel}</span>
96
+ <strong id="aquarium-filter-target" class="metric-value">4.0 to 6.0×</strong>
97
+ </div>
98
+ <div class="metric">
99
+ <span class="metric-label">{ui.effectiveFlowLabel}</span>
100
+ <strong id="aquarium-filter-effective-flow" class="metric-value">450 L/h</strong>
101
+ </div>
102
+ <div class="metric">
103
+ <span class="metric-label">{ui.stockingProxyLabel}</span>
104
+ <strong id="aquarium-filter-stock-proxy" class="metric-value">0.50 cm/L</strong>
105
+ </div>
106
+ </div>
107
+
108
+ <div class="turnover-gauge">
109
+ <div class="gauge-head">
110
+ <span class="visual-caption">{ui.currentTitle}</span>
111
+ <strong id="aquarium-filter-load-badge">{ui.loadLight}</strong>
112
+ </div>
113
+ <div class="gauge-track" aria-hidden="true"><div id="aquarium-filter-turnover-progress" class="gauge-progress"></div></div>
114
+ </div>
115
+
116
+ <div class="load-line">
117
+ <strong id="aquarium-filter-gap">{ui.bandSignalInside}</strong>
118
+ </div>
119
+
120
+ <div class="caution">
121
+ <h3>{ui.cautionTitle}</h3>
122
+ <p>{ui.maintenanceText}</p>
123
+ <p>{ui.useTestKits}</p>
124
+ </div>
125
+ </section>
126
+ </div>
127
+ </div>
128
+
129
+ <script is:inline type="application/json" id="aquarium-filter-planner-ui" set:html={JSON.stringify(ui)}></script>
130
+ <script>
131
+ import { initPlanner } from './controller';
132
+ import type { AquariumFilterTurnoverStockingPlannerUI } from './ui';
133
+
134
+ const root = document.querySelector<HTMLElement>('[data-aquarium-filter-planner]');
135
+ const uiElement = document.querySelector<HTMLScriptElement>('#aquarium-filter-planner-ui');
136
+ if (root && uiElement) {
137
+ const ui = JSON.parse(uiElement.textContent ?? '{}') as AquariumFilterTurnoverStockingPlannerUI;
138
+ initPlanner(root, ui);
139
+ }
140
+ </script>