@jjlmoya/utils-astronomy 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 (57) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +57 -0
  3. package/src/category/i18n/es.ts +57 -0
  4. package/src/category/i18n/fr.ts +58 -0
  5. package/src/category/index.ts +16 -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 +19 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +22 -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 +24 -0
  17. package/src/tests/mocks/astro_mock.js +2 -0
  18. package/src/tests/seo_length.test.ts +57 -0
  19. package/src/tests/tool_validation.test.ts +145 -0
  20. package/src/tool/bortleVisualizer/bibliography.astro +14 -0
  21. package/src/tool/bortleVisualizer/component.astro +491 -0
  22. package/src/tool/bortleVisualizer/i18n/en.ts +153 -0
  23. package/src/tool/bortleVisualizer/i18n/es.ts +161 -0
  24. package/src/tool/bortleVisualizer/i18n/fr.ts +153 -0
  25. package/src/tool/bortleVisualizer/index.ts +41 -0
  26. package/src/tool/bortleVisualizer/logic.ts +118 -0
  27. package/src/tool/bortleVisualizer/seo.astro +61 -0
  28. package/src/tool/bortleVisualizer/style.css +5 -0
  29. package/src/tool/deepSpaceScope/bibliography.astro +14 -0
  30. package/src/tool/deepSpaceScope/component.astro +849 -0
  31. package/src/tool/deepSpaceScope/i18n/en.ts +157 -0
  32. package/src/tool/deepSpaceScope/i18n/es.ts +157 -0
  33. package/src/tool/deepSpaceScope/i18n/fr.ts +157 -0
  34. package/src/tool/deepSpaceScope/index.ts +48 -0
  35. package/src/tool/deepSpaceScope/logic.ts +41 -0
  36. package/src/tool/deepSpaceScope/seo.astro +61 -0
  37. package/src/tool/deepSpaceScope/style.css +5 -0
  38. package/src/tool/starExposureCalculator/bibliography.astro +14 -0
  39. package/src/tool/starExposureCalculator/component.astro +562 -0
  40. package/src/tool/starExposureCalculator/i18n/en.ts +163 -0
  41. package/src/tool/starExposureCalculator/i18n/es.ts +163 -0
  42. package/src/tool/starExposureCalculator/i18n/fr.ts +158 -0
  43. package/src/tool/starExposureCalculator/index.ts +53 -0
  44. package/src/tool/starExposureCalculator/logic.ts +49 -0
  45. package/src/tool/starExposureCalculator/seo.astro +61 -0
  46. package/src/tool/starExposureCalculator/style.css +5 -0
  47. package/src/tool/telescopeResolution/bibliography.astro +14 -0
  48. package/src/tool/telescopeResolution/component.astro +556 -0
  49. package/src/tool/telescopeResolution/i18n/en.ts +168 -0
  50. package/src/tool/telescopeResolution/i18n/es.ts +163 -0
  51. package/src/tool/telescopeResolution/i18n/fr.ts +168 -0
  52. package/src/tool/telescopeResolution/index.ts +52 -0
  53. package/src/tool/telescopeResolution/logic.ts +39 -0
  54. package/src/tool/telescopeResolution/seo.astro +61 -0
  55. package/src/tool/telescopeResolution/style.css +5 -0
  56. package/src/tools.ts +19 -0
  57. package/src/types.ts +71 -0
@@ -0,0 +1,562 @@
1
+ ---
2
+ import type { StarExposureCalculatorUI } from './index';
3
+ import type { KnownLocale } from '../../types';
4
+
5
+ interface Props {
6
+ ui: StarExposureCalculatorUI;
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { ui } = Astro.props;
11
+ ---
12
+
13
+ <div class="star-calc-wrapper">
14
+ <div class="star-calc-grid">
15
+ <div class="star-calc-controls">
16
+ <div class="star-mode-selector">
17
+ <span class="star-mode-label">{ui.modeLabel}:</span>
18
+ <div class="star-toggle-group">
19
+ <button id="mode-classic" class="star-toggle-btn active" data-mode="classic">{ui.classicMode}</button>
20
+ <button id="mode-npf" class="star-toggle-btn" data-mode="npf">{ui.npfMode}</button>
21
+ </div>
22
+ </div>
23
+
24
+ <div class="star-control-group">
25
+ <label class="star-control-label">{ui.sensorLabel}</label>
26
+ <div class="star-select-wrapper">
27
+ <select id="sensor-type" class="star-select">
28
+ <option value="1.0">Full Frame (x1.0)</option>
29
+ <option value="1.5">APS-C Nikon/Sony/Fuji (x1.5)</option>
30
+ <option value="1.6">APS-C Canon (x1.6)</option>
31
+ <option value="2.0">Micro Four Thirds (x2.0)</option>
32
+ </select>
33
+ <svg class="star-select-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
34
+ <polyline points="6 9 12 15 18 9"></polyline>
35
+ </svg>
36
+ </div>
37
+ </div>
38
+
39
+ <div class="star-control-group">
40
+ <label class="star-control-label">{ui.focalLabel}</label>
41
+ <div class="star-stepper">
42
+ <button class="star-step-btn" data-action="dec" data-target="focal">-</button>
43
+ <input type="number" id="focal-length" value="14" min="1" max="1000" class="star-stepper-input" />
44
+ <button class="star-step-btn" data-action="inc" data-target="focal">+</button>
45
+ </div>
46
+ </div>
47
+
48
+ <div id="npf-controls" class="star-npf-controls">
49
+ <div class="star-control-group">
50
+ <label class="star-control-label">{ui.apertureLabel}</label>
51
+ <div class="star-stepper">
52
+ <button class="star-step-btn" data-action="dec" data-target="aperture">-</button>
53
+ <input type="number" id="aperture" value="2.8" step="0.1" min="0.5" max="32" class="star-stepper-input" />
54
+ <button class="star-step-btn" data-action="inc" data-target="aperture">+</button>
55
+ </div>
56
+ </div>
57
+ <div class="star-control-group">
58
+ <label class="star-control-label">{ui.megapixelsLabel}</label>
59
+ <div class="star-stepper">
60
+ <button class="star-step-btn" data-action="dec" data-target="megapixels">-</button>
61
+ <input type="number" id="megapixels" value="24" min="1" max="150" class="star-stepper-input" />
62
+ <button class="star-step-btn" data-action="inc" data-target="megapixels">+</button>
63
+ </div>
64
+ </div>
65
+ </div>
66
+
67
+ <div class="star-control-group">
68
+ <label class="star-control-label">
69
+ {ui.declinationLabel}: <span id="declination-value">0</span>°
70
+ </label>
71
+ <input type="range" id="declination" min="0" max="89" value="0" class="star-range" aria-label={ui.declinationLabel} />
72
+ <div class="star-range-labels">
73
+ <span>{ui.equatorLabel}</span>
74
+ <span>{ui.poleLabel}</span>
75
+ </div>
76
+ </div>
77
+ </div>
78
+
79
+ <div class="star-results">
80
+ <div class="star-result-main">
81
+ <div class="star-time-display">
82
+ <span id="result-time" class="star-time-value">35.7</span>
83
+ <span class="star-time-unit">{ui.secondsUnit}</span>
84
+ </div>
85
+ <p id="result-text" class="star-result-text">{ui.resultText}</p>
86
+ </div>
87
+
88
+ <div class="star-simulation">
89
+ <label class="star-sim-label">{ui.simLabel}</label>
90
+ <div class="star-sim-container">
91
+ <div class="star-sim-view">
92
+ <div id="star-preview" class="star-point"></div>
93
+ <div class="star-center-marker">+</div>
94
+ </div>
95
+ <div class="star-sim-info-row">
96
+ <span id="sim-info" class="star-sim-info">{ui.pointStars}</span>
97
+ </div>
98
+ </div>
99
+ <div class="star-sim-range-wrapper">
100
+ <input type="range" id="sim-offset" min="1" max="5" step="0.1" value="1" class="star-range" aria-label={ui.simRangeLabel} />
101
+ <span class="star-sim-range-label">{ui.simRangeLabel}</span>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ <script>
109
+ import { calculateExposureTime } from './logic';
110
+ import type { SensorCropFactor } from './logic';
111
+
112
+ class StarCalculatorApp {
113
+ mode = 'classic';
114
+ focalLengthInput: HTMLInputElement;
115
+ sensorSelect: HTMLSelectElement;
116
+ apertureInput: HTMLInputElement;
117
+ megapixelsInput: HTMLInputElement;
118
+ declinationInput: HTMLInputElement;
119
+ resultTimeDisplay: HTMLElement;
120
+ declinationValueDisplay: HTMLElement;
121
+ starPreview: HTMLElement;
122
+ simOffsetInput: HTMLInputElement;
123
+ npfControls: HTMLElement;
124
+ modeBtns: NodeListOf<HTMLButtonElement>;
125
+ stepBtns: NodeListOf<HTMLButtonElement>;
126
+
127
+ constructor() {
128
+ this.focalLengthInput = document.getElementById('focal-length') as HTMLInputElement;
129
+ this.sensorSelect = document.getElementById('sensor-type') as HTMLSelectElement;
130
+ this.apertureInput = document.getElementById('aperture') as HTMLInputElement;
131
+ this.megapixelsInput = document.getElementById('megapixels') as HTMLInputElement;
132
+ this.declinationInput = document.getElementById('declination') as HTMLInputElement;
133
+ this.resultTimeDisplay = document.getElementById('result-time') as HTMLElement;
134
+ this.declinationValueDisplay = document.getElementById('declination-value') as HTMLElement;
135
+ this.starPreview = document.getElementById('star-preview') as HTMLElement;
136
+ this.simOffsetInput = document.getElementById('sim-offset') as HTMLInputElement;
137
+ this.npfControls = document.getElementById('npf-controls') as HTMLElement;
138
+ this.modeBtns = document.querySelectorAll('.star-toggle-btn');
139
+ this.stepBtns = document.querySelectorAll('.star-step-btn');
140
+ this.init();
141
+ }
142
+
143
+ resolveStepInput(targetId: string | undefined): HTMLInputElement | null {
144
+ if (targetId === 'focal') return this.focalLengthInput;
145
+ if (targetId === 'aperture') return this.apertureInput;
146
+ if (targetId === 'megapixels') return this.megapixelsInput;
147
+ return null;
148
+ }
149
+
150
+ handleStepBtn(btn: HTMLButtonElement) {
151
+ const action = btn.dataset.action;
152
+ const targetId = btn.dataset.target;
153
+ const input = this.resolveStepInput(targetId);
154
+ if (!input) return;
155
+ let val = parseFloat(input.value);
156
+ const step = parseFloat(input.step) || 1;
157
+ if (action === 'inc') val += step;
158
+ if (action === 'dec') val -= step;
159
+ input.value = Math.max(parseFloat(input.min) || 0, val).toFixed(targetId === 'aperture' ? 1 : 0);
160
+ this.calculate();
161
+ }
162
+
163
+ bindModeButtons() {
164
+ this.modeBtns.forEach((btn) => {
165
+ btn.addEventListener('click', () => {
166
+ this.modeBtns.forEach((b) => b.classList.remove('active'));
167
+ btn.classList.add('active');
168
+ this.mode = btn.dataset.mode || 'classic';
169
+ if (this.npfControls) {
170
+ this.npfControls.style.display = this.mode === 'npf' ? 'flex' : 'none';
171
+ }
172
+ this.calculate();
173
+ });
174
+ });
175
+ }
176
+
177
+ bindInputListeners() {
178
+ this.stepBtns.forEach((btn) => btn.addEventListener('click', () => this.handleStepBtn(btn)));
179
+ [this.focalLengthInput, this.apertureInput, this.megapixelsInput].forEach((inp) => {
180
+ if (inp) inp.addEventListener('input', () => this.calculate());
181
+ });
182
+ if (this.sensorSelect) this.sensorSelect.addEventListener('change', () => this.calculate());
183
+ if (this.declinationInput) {
184
+ this.declinationInput.addEventListener('input', () => {
185
+ if (this.declinationValueDisplay) {
186
+ this.declinationValueDisplay.textContent = this.declinationInput.value;
187
+ }
188
+ this.calculate();
189
+ });
190
+ }
191
+ if (this.simOffsetInput) {
192
+ this.simOffsetInput.addEventListener('input', () => this.updateSimulation());
193
+ }
194
+ }
195
+
196
+ init() {
197
+ this.bindModeButtons();
198
+ this.bindInputListeners();
199
+ this.calculate();
200
+ }
201
+
202
+ getExposureParams() {
203
+ return {
204
+ mode: this.mode as 'classic' | 'npf',
205
+ focalLength: parseFloat(this.focalLengthInput.value) || 1,
206
+ cropFactor: parseFloat(this.sensorSelect.value) as SensorCropFactor,
207
+ aperture: parseFloat(this.apertureInput?.value || '2.8'),
208
+ megapixels: parseFloat(this.megapixelsInput?.value || '24'),
209
+ declination: parseFloat(this.declinationInput.value) || 0,
210
+ };
211
+ }
212
+
213
+ calculate() {
214
+ if (!this.focalLengthInput || !this.sensorSelect || !this.declinationInput) return;
215
+ const resultTime = calculateExposureTime(this.getExposureParams());
216
+ if (this.resultTimeDisplay) {
217
+ this.resultTimeDisplay.textContent = resultTime.toFixed(1);
218
+ }
219
+ this.updateSimulation();
220
+ }
221
+
222
+ updateSimulation() {
223
+ if (!this.simOffsetInput || !this.starPreview) return;
224
+ const excess = parseFloat(this.simOffsetInput.value);
225
+ const trailLength = (excess - 1) * 20;
226
+ const info = document.getElementById('sim-info');
227
+ if (excess > 1.1) {
228
+ this.starPreview.style.width = `${trailLength + 4}px`;
229
+ this.starPreview.style.borderRadius = '2px';
230
+ this.starPreview.classList.add('is-trail');
231
+ if (info) info.textContent = 'Trazo de estrella visible';
232
+ } else {
233
+ this.starPreview.style.width = '4px';
234
+ this.starPreview.style.borderRadius = '50%';
235
+ this.starPreview.classList.remove('is-trail');
236
+ if (info) info.textContent = 'Estrellas como puntos';
237
+ }
238
+ }
239
+ }
240
+
241
+ new StarCalculatorApp();
242
+ </script>
243
+
244
+ <style>
245
+ .star-calc-wrapper {
246
+ width: 100%;
247
+ max-width: 56rem;
248
+ margin: 0 auto;
249
+ padding: 2rem;
250
+ background: var(--bg-surface);
251
+ border: 1px solid var(--border-base);
252
+ border-radius: 1.5rem;
253
+ box-shadow: 0 25px 50px -12px var(--shadow-base);
254
+ }
255
+
256
+ .star-calc-grid {
257
+ display: grid;
258
+ grid-template-columns: 1fr;
259
+ gap: 2rem;
260
+ }
261
+
262
+ @media (min-width: 768px) {
263
+ .star-calc-grid {
264
+ grid-template-columns: 1fr 1fr;
265
+ gap: 3rem;
266
+ }
267
+ }
268
+
269
+ .star-calc-controls {
270
+ display: flex;
271
+ flex-direction: column;
272
+ gap: 1.5rem;
273
+ }
274
+
275
+ .star-mode-selector {
276
+ display: flex;
277
+ align-items: center;
278
+ gap: 1rem;
279
+ }
280
+
281
+ .star-mode-label {
282
+ font-size: 0.875rem;
283
+ font-weight: 500;
284
+ color: var(--text-muted, #cbd5e1);
285
+ }
286
+
287
+ .star-toggle-group {
288
+ display: flex;
289
+ background: var(--bg-muted);
290
+ padding: 0.25rem;
291
+ border-radius: 0.75rem;
292
+ border: 1px solid var(--border-base);
293
+ }
294
+
295
+ .star-toggle-btn {
296
+ padding: 0.5rem 1rem;
297
+ border: none;
298
+ background: transparent;
299
+ color: var(--text-muted, #94a3b8);
300
+ cursor: pointer;
301
+ border-radius: 0.5rem;
302
+ font-weight: 600;
303
+ font-size: 0.875rem;
304
+ transition: all 0.2s ease;
305
+ }
306
+
307
+ .star-toggle-btn.active {
308
+ background: var(--color-amber, #f59e0b);
309
+ color: var(--color-bg-deep, #0f172a);
310
+ box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
311
+ }
312
+
313
+ .star-control-group {
314
+ display: flex;
315
+ flex-direction: column;
316
+ gap: 0.75rem;
317
+ }
318
+
319
+ .star-control-label {
320
+ font-size: 0.9rem;
321
+ font-weight: 500;
322
+ color: var(--text-muted, #cbd5e1);
323
+ display: flex;
324
+ justify-content: space-between;
325
+ }
326
+
327
+ .star-stepper {
328
+ display: flex;
329
+ align-items: center;
330
+ background: var(--bg-muted);
331
+ border: 1px solid var(--border-base);
332
+ border-radius: 0.75rem;
333
+ overflow: hidden;
334
+ }
335
+
336
+ .star-step-btn {
337
+ width: 4rem;
338
+ height: 3.5rem;
339
+ border: none;
340
+ background: var(--bg-muted);
341
+ color: var(--color-amber, #f59e0b);
342
+ font-size: 1.5rem;
343
+ cursor: pointer;
344
+ transition: background 0.2s ease;
345
+ }
346
+
347
+ .star-step-btn:hover {
348
+ background: var(--bg-page);
349
+ }
350
+
351
+ .star-stepper-input {
352
+ flex: 1;
353
+ background: transparent;
354
+ border: none;
355
+ color: var(--text-base);
356
+ text-align: center;
357
+ font-size: 1.25rem;
358
+ font-weight: 600;
359
+ outline: none;
360
+ width: 100%;
361
+ }
362
+
363
+ .star-select-wrapper {
364
+ position: relative;
365
+ width: 100%;
366
+ }
367
+
368
+ .star-select-arrow {
369
+ position: absolute;
370
+ right: 0.875rem;
371
+ top: 50%;
372
+ transform: translateY(-50%);
373
+ width: 1rem;
374
+ height: 1rem;
375
+ color: var(--color-amber, #f59e0b);
376
+ pointer-events: none;
377
+ }
378
+
379
+ .star-select {
380
+ appearance: none;
381
+ -webkit-appearance: none;
382
+ width: 100%;
383
+ background: var(--bg-muted);
384
+ border: 1px solid var(--border-base);
385
+ color: var(--text-base);
386
+ padding: 0.875rem 3rem 0.875rem 1rem;
387
+ border-radius: 0.75rem;
388
+ font-size: 0.95rem;
389
+ outline: none;
390
+ cursor: pointer;
391
+ transition: border-color 0.2s ease;
392
+ }
393
+
394
+ .star-select:hover {
395
+ border-color: var(--text-muted);
396
+ }
397
+
398
+ .star-select:focus {
399
+ border-color: var(--color-amber, #f59e0b);
400
+ }
401
+
402
+ .star-select option {
403
+ background: var(--bg-surface);
404
+ color: var(--text-base);
405
+ }
406
+
407
+ .star-range {
408
+ width: 100%;
409
+ accent-color: var(--color-amber, #f59e0b);
410
+ height: 6px;
411
+ border-radius: 3px;
412
+ cursor: pointer;
413
+ }
414
+
415
+ .star-range-labels {
416
+ display: flex;
417
+ justify-content: space-between;
418
+ font-size: 0.75rem;
419
+ color: var(--text-muted, #64748b);
420
+ }
421
+
422
+ .star-npf-controls {
423
+ display: none;
424
+ flex-direction: column;
425
+ gap: 1.5rem;
426
+ animation: star-fade-in 0.3s ease;
427
+ }
428
+
429
+ @keyframes star-fade-in {
430
+ from {
431
+ opacity: 0;
432
+ transform: translateY(-10px);
433
+ }
434
+ to {
435
+ opacity: 1;
436
+ transform: translateY(0);
437
+ }
438
+ }
439
+
440
+ .star-results {
441
+ display: flex;
442
+ flex-direction: column;
443
+ gap: 2rem;
444
+ justify-content: center;
445
+ }
446
+
447
+ .star-result-main {
448
+ text-align: center;
449
+ background: var(--bg-muted);
450
+ padding: 2rem;
451
+ border-radius: 1.5rem;
452
+ border: 1px solid var(--border-base);
453
+ }
454
+
455
+ .star-time-display {
456
+ display: flex;
457
+ flex-direction: column;
458
+ align-items: center;
459
+ margin-bottom: 1rem;
460
+ }
461
+
462
+ .star-time-value {
463
+ font-size: 5rem;
464
+ font-weight: 800;
465
+ color: var(--color-amber, #f59e0b);
466
+ line-height: 1;
467
+ text-shadow: 0 0 30px rgba(245, 158, 11, 0.3);
468
+ }
469
+
470
+ .star-time-unit {
471
+ font-size: 1.25rem;
472
+ color: var(--text-muted, #94a3b8);
473
+ text-transform: uppercase;
474
+ letter-spacing: 0.1em;
475
+ margin-top: 0.5rem;
476
+ }
477
+
478
+ .star-result-text {
479
+ font-size: 0.95rem;
480
+ color: var(--text-muted, #94a3b8);
481
+ margin: 0;
482
+ }
483
+
484
+ .star-simulation {
485
+ background: var(--bg-muted);
486
+ padding: 1.5rem;
487
+ border-radius: 1rem;
488
+ border: 1px solid var(--border-base);
489
+ }
490
+
491
+ .star-sim-label {
492
+ display: block;
493
+ font-size: 0.85rem;
494
+ color: var(--text-muted, #94a3b8);
495
+ margin-bottom: 1rem;
496
+ text-align: center;
497
+ }
498
+
499
+ .star-sim-container {
500
+ height: 100px;
501
+ background: radial-gradient(circle at center, #0f172a 0%, #020617 100%);
502
+ border-radius: 0.5rem;
503
+ position: relative;
504
+ display: flex;
505
+ align-items: center;
506
+ justify-content: center;
507
+ overflow: hidden;
508
+ margin-bottom: 1rem;
509
+ }
510
+
511
+ .star-sim-view {
512
+ position: relative;
513
+ width: 100%;
514
+ height: 100%;
515
+ display: flex;
516
+ align-items: center;
517
+ justify-content: center;
518
+ }
519
+
520
+ .star-point {
521
+ height: 4px;
522
+ width: 4px;
523
+ background: white;
524
+ border-radius: 50%;
525
+ box-shadow: 0 0 10px white;
526
+ transition: width 0.3s ease, border-radius 0.3s ease;
527
+ }
528
+
529
+ .star-point.is-trail {
530
+ background: linear-gradient(90deg, white, rgba(255, 255, 255, 0.1));
531
+ }
532
+
533
+ .star-center-marker {
534
+ position: absolute;
535
+ color: rgba(245, 158, 11, 0.3);
536
+ font-size: 1.5rem;
537
+ pointer-events: none;
538
+ }
539
+
540
+ .star-sim-info-row {
541
+ text-align: center;
542
+ font-size: 0.85rem;
543
+ }
544
+
545
+ .star-sim-info {
546
+ color: var(--color-amber, #f59e0b);
547
+ font-weight: 500;
548
+ }
549
+
550
+ .star-sim-range-wrapper {
551
+ margin-top: 1rem;
552
+ display: flex;
553
+ flex-direction: column;
554
+ gap: 0.5rem;
555
+ }
556
+
557
+ .star-sim-range-label {
558
+ font-size: 0.75rem;
559
+ color: var(--text-muted, #64748b);
560
+ text-align: center;
561
+ }
562
+ </style>