@jjlmoya/utils-astronomy 1.16.0 → 1.18.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 (106) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/data.ts +2 -0
  4. package/src/entries.ts +4 -1
  5. package/src/pages/[locale]/[slug].astro +30 -14
  6. package/src/tests/locale_completeness.test.ts +1 -34
  7. package/src/tests/tool_structure.test.ts +71 -0
  8. package/src/tests/tool_validation.test.ts +4 -3
  9. package/src/tool/bortleVisualizer/bibliography.ts +8 -0
  10. package/src/tool/bortleVisualizer/entry.ts +2 -0
  11. package/src/tool/bortleVisualizer/i18n/de.ts +1 -9
  12. package/src/tool/bortleVisualizer/i18n/en.ts +1 -9
  13. package/src/tool/bortleVisualizer/i18n/es.ts +1 -9
  14. package/src/tool/bortleVisualizer/i18n/fr.ts +1 -9
  15. package/src/tool/bortleVisualizer/i18n/id.ts +1 -9
  16. package/src/tool/bortleVisualizer/i18n/it.ts +1 -9
  17. package/src/tool/bortleVisualizer/i18n/ja.ts +1 -9
  18. package/src/tool/bortleVisualizer/i18n/ko.ts +1 -9
  19. package/src/tool/bortleVisualizer/i18n/nl.ts +1 -9
  20. package/src/tool/bortleVisualizer/i18n/pl.ts +1 -9
  21. package/src/tool/bortleVisualizer/i18n/pt.ts +1 -9
  22. package/src/tool/bortleVisualizer/i18n/ru.ts +1 -9
  23. package/src/tool/bortleVisualizer/i18n/sv.ts +1 -9
  24. package/src/tool/bortleVisualizer/i18n/tr.ts +1 -9
  25. package/src/tool/bortleVisualizer/i18n/zh.ts +1 -9
  26. package/src/tool/bortleVisualizer/seo.astro +2 -48
  27. package/src/tool/deepSpaceScope/bibliography.ts +6 -0
  28. package/src/tool/deepSpaceScope/entry.ts +2 -0
  29. package/src/tool/deepSpaceScope/i18n/de.ts +1 -7
  30. package/src/tool/deepSpaceScope/i18n/en.ts +1 -7
  31. package/src/tool/deepSpaceScope/i18n/es.ts +1 -7
  32. package/src/tool/deepSpaceScope/i18n/fr.ts +1 -7
  33. package/src/tool/deepSpaceScope/i18n/id.ts +1 -7
  34. package/src/tool/deepSpaceScope/i18n/it.ts +1 -7
  35. package/src/tool/deepSpaceScope/i18n/ja.ts +1 -7
  36. package/src/tool/deepSpaceScope/i18n/ko.ts +1 -7
  37. package/src/tool/deepSpaceScope/i18n/nl.ts +1 -7
  38. package/src/tool/deepSpaceScope/i18n/pl.ts +1 -7
  39. package/src/tool/deepSpaceScope/i18n/pt.ts +1 -7
  40. package/src/tool/deepSpaceScope/i18n/ru.ts +1 -7
  41. package/src/tool/deepSpaceScope/i18n/sv.ts +1 -7
  42. package/src/tool/deepSpaceScope/i18n/tr.ts +1 -7
  43. package/src/tool/deepSpaceScope/i18n/zh.ts +1 -7
  44. package/src/tool/deepSpaceScope/seo.astro +2 -48
  45. package/src/tool/smartEyepieceCalculator/app.ts +327 -0
  46. package/src/tool/smartEyepieceCalculator/bibliography.astro +14 -0
  47. package/src/tool/smartEyepieceCalculator/bibliography.ts +6 -0
  48. package/src/tool/smartEyepieceCalculator/component.astro +125 -0
  49. package/src/tool/smartEyepieceCalculator/entry.ts +67 -0
  50. package/src/tool/smartEyepieceCalculator/i18n/de.ts +165 -0
  51. package/src/tool/smartEyepieceCalculator/i18n/en.ts +166 -0
  52. package/src/tool/smartEyepieceCalculator/i18n/es.ts +165 -0
  53. package/src/tool/smartEyepieceCalculator/i18n/fr.ts +165 -0
  54. package/src/tool/smartEyepieceCalculator/i18n/id.ts +165 -0
  55. package/src/tool/smartEyepieceCalculator/i18n/it.ts +165 -0
  56. package/src/tool/smartEyepieceCalculator/i18n/ja.ts +165 -0
  57. package/src/tool/smartEyepieceCalculator/i18n/ko.ts +165 -0
  58. package/src/tool/smartEyepieceCalculator/i18n/nl.ts +165 -0
  59. package/src/tool/smartEyepieceCalculator/i18n/pl.ts +165 -0
  60. package/src/tool/smartEyepieceCalculator/i18n/pt.ts +165 -0
  61. package/src/tool/smartEyepieceCalculator/i18n/ru.ts +165 -0
  62. package/src/tool/smartEyepieceCalculator/i18n/sv.ts +165 -0
  63. package/src/tool/smartEyepieceCalculator/i18n/tr.ts +165 -0
  64. package/src/tool/smartEyepieceCalculator/i18n/zh.ts +165 -0
  65. package/src/tool/smartEyepieceCalculator/index.ts +13 -0
  66. package/src/tool/smartEyepieceCalculator/logic.ts +67 -0
  67. package/src/tool/smartEyepieceCalculator/seo.astro +15 -0
  68. package/src/tool/smartEyepieceCalculator/smart-eyepiece-calculator.css +393 -0
  69. package/src/tool/starExposureCalculator/bibliography.ts +7 -0
  70. package/src/tool/starExposureCalculator/entry.ts +2 -0
  71. package/src/tool/starExposureCalculator/i18n/de.ts +1 -8
  72. package/src/tool/starExposureCalculator/i18n/en.ts +1 -8
  73. package/src/tool/starExposureCalculator/i18n/es.ts +1 -8
  74. package/src/tool/starExposureCalculator/i18n/fr.ts +1 -8
  75. package/src/tool/starExposureCalculator/i18n/id.ts +1 -8
  76. package/src/tool/starExposureCalculator/i18n/it.ts +1 -8
  77. package/src/tool/starExposureCalculator/i18n/ja.ts +1 -8
  78. package/src/tool/starExposureCalculator/i18n/ko.ts +1 -8
  79. package/src/tool/starExposureCalculator/i18n/nl.ts +1 -8
  80. package/src/tool/starExposureCalculator/i18n/pl.ts +1 -8
  81. package/src/tool/starExposureCalculator/i18n/pt.ts +1 -8
  82. package/src/tool/starExposureCalculator/i18n/ru.ts +1 -8
  83. package/src/tool/starExposureCalculator/i18n/sv.ts +1 -8
  84. package/src/tool/starExposureCalculator/i18n/tr.ts +1 -8
  85. package/src/tool/starExposureCalculator/i18n/zh.ts +1 -8
  86. package/src/tool/starExposureCalculator/seo.astro +2 -48
  87. package/src/tool/telescopeResolution/bibliography.ts +7 -0
  88. package/src/tool/telescopeResolution/entry.ts +2 -0
  89. package/src/tool/telescopeResolution/i18n/de.ts +1 -8
  90. package/src/tool/telescopeResolution/i18n/en.ts +1 -8
  91. package/src/tool/telescopeResolution/i18n/es.ts +1 -8
  92. package/src/tool/telescopeResolution/i18n/fr.ts +1 -8
  93. package/src/tool/telescopeResolution/i18n/id.ts +1 -8
  94. package/src/tool/telescopeResolution/i18n/it.ts +1 -8
  95. package/src/tool/telescopeResolution/i18n/ja.ts +1 -8
  96. package/src/tool/telescopeResolution/i18n/ko.ts +1 -8
  97. package/src/tool/telescopeResolution/i18n/nl.ts +1 -8
  98. package/src/tool/telescopeResolution/i18n/pl.ts +1 -8
  99. package/src/tool/telescopeResolution/i18n/pt.ts +1 -8
  100. package/src/tool/telescopeResolution/i18n/ru.ts +1 -8
  101. package/src/tool/telescopeResolution/i18n/sv.ts +1 -8
  102. package/src/tool/telescopeResolution/i18n/tr.ts +1 -8
  103. package/src/tool/telescopeResolution/i18n/zh.ts +1 -8
  104. package/src/tool/telescopeResolution/seo.astro +2 -48
  105. package/src/tools.ts +3 -0
  106. package/src/types.ts +1 -5
@@ -0,0 +1,327 @@
1
+ import { calculateEyepieceStats, calculateExitPupil, getTelescopeLimits, analyzeEyepiece } from './logic';
2
+ import type { TelescopeLimits, EyepieceStats, EyepieceStatus } from './logic';
3
+
4
+ export interface EyepieceData {
5
+ id: string;
6
+ focal: number;
7
+ afov: number;
8
+ }
9
+
10
+ export interface AppState {
11
+ aperture: number;
12
+ focal: number;
13
+ eyepieces: EyepieceData[];
14
+ activeEyepiece: string | null;
15
+ }
16
+
17
+ export class EyepieceApp {
18
+ ui: Record<string, string>;
19
+ wrapper: HTMLElement;
20
+ container: HTMLElement;
21
+ appState: AppState;
22
+ targetSizes: Record<string, number> = {
23
+ moon: 0.5,
24
+ pleiades: 1.8,
25
+ andromeda: 3.16,
26
+ m13: 0.33,
27
+ saturn: 0.012
28
+ };
29
+
30
+ constructor(uiObj: Record<string, string>) {
31
+ this.ui = uiObj;
32
+ this.wrapper = document.getElementById('epc-root') as HTMLElement;
33
+ this.container = document.getElementById('epc-cards-container') as HTMLElement;
34
+ this.appState = this.loadState();
35
+ this.init();
36
+ }
37
+
38
+ loadState(): AppState {
39
+ try {
40
+ const saved = localStorage.getItem('epc_state');
41
+ const parsed = saved ? JSON.parse(saved) : null;
42
+ if (parsed && parsed.eyepieces && parsed.eyepieces.length > 0) return parsed;
43
+ } catch {}
44
+
45
+ return {
46
+ aperture: 150,
47
+ focal: 750,
48
+ eyepieces: [{ id: Date.now().toString(), focal: 10, afov: 52 }],
49
+ activeEyepiece: null
50
+ };
51
+ }
52
+
53
+ saveState() {
54
+ localStorage.setItem('epc_state', JSON.stringify(this.appState));
55
+ }
56
+
57
+ init() {
58
+ this.bindAptInputs();
59
+ this.bindFocalInputs();
60
+ this.bindPresetInputs();
61
+
62
+ document.getElementById('epc-add-btn')?.addEventListener('click', () => {
63
+ this.appState.eyepieces.push({ id: Date.now().toString(), focal: 0, afov: 52 });
64
+ this.renderCards();
65
+ this.updateAll();
66
+ });
67
+
68
+ document.getElementById('epc-target')?.addEventListener('change', () => this.updateSim());
69
+ this.renderCards();
70
+ this.updateAll();
71
+ }
72
+
73
+
74
+
75
+ bindAptInputs() {
76
+ const aptInput = document.getElementById('epc-aperture') as HTMLInputElement;
77
+ if (!aptInput) return;
78
+ aptInput.value = this.appState.aperture ? this.appState.aperture.toString() : '';
79
+ aptInput.addEventListener('input', (e) => {
80
+ this.appState.aperture = parseFloat((e.target as HTMLInputElement).value) || 0;
81
+ const sel = document.getElementById('epc-preset') as HTMLSelectElement;
82
+ if (sel) sel.value = '';
83
+ this.updateAll();
84
+ });
85
+ }
86
+
87
+ bindFocalInputs() {
88
+ const fclInput = document.getElementById('epc-focal') as HTMLInputElement;
89
+ if (!fclInput) return;
90
+ fclInput.value = this.appState.focal ? this.appState.focal.toString() : '';
91
+ fclInput.addEventListener('input', (e) => {
92
+ this.appState.focal = parseFloat((e.target as HTMLInputElement).value) || 0;
93
+ const sel = document.getElementById('epc-preset') as HTMLSelectElement;
94
+ if (sel) sel.value = '';
95
+ this.updateAll();
96
+ });
97
+ }
98
+
99
+ bindPresetInputs() {
100
+ const presetSel = document.getElementById('epc-preset') as HTMLSelectElement;
101
+ if (!presetSel) return;
102
+ presetSel.addEventListener('change', (e) => {
103
+ const val = (e.target as HTMLSelectElement).value;
104
+ if (!val) return;
105
+ const [a, f] = val.split(',').map(Number);
106
+ this.appState.aperture = a;
107
+ this.appState.focal = f;
108
+ const apt = document.getElementById('epc-aperture') as HTMLInputElement;
109
+ const fcl = document.getElementById('epc-focal') as HTMLInputElement;
110
+ if (apt) apt.value = a.toString();
111
+ if (fcl) fcl.value = f.toString();
112
+ this.updateAll();
113
+ });
114
+ }
115
+
116
+
117
+
118
+ createCardDom(ep: EyepieceData): HTMLElement {
119
+ const tmpl = document.getElementById('epc-card-template') as HTMLTemplateElement;
120
+ const clone = tmpl.content.cloneNode(true) as HTMLElement;
121
+ const card = clone.querySelector('.epc-card') as HTMLElement;
122
+ card.dataset.id = ep.id;
123
+ return clone;
124
+ }
125
+
126
+ bindCardEvents(clone: HTMLElement, ep: EyepieceData) {
127
+ const fInp = clone.querySelector('.epc-ep-focal') as HTMLInputElement;
128
+ const aInp = clone.querySelector('.epc-ep-afov') as HTMLInputElement;
129
+
130
+ if (ep.focal) fInp.value = ep.focal.toString();
131
+ if (ep.afov) aInp.value = ep.afov.toString();
132
+
133
+ fInp.addEventListener('input', (e) => {
134
+ ep.focal = parseFloat((e.target as HTMLInputElement).value) || 0;
135
+ this.updateAll();
136
+ });
137
+
138
+ aInp.addEventListener('input', (e) => {
139
+ ep.afov = parseFloat((e.target as HTMLInputElement).value) || 0;
140
+ this.updateAll();
141
+ });
142
+
143
+ clone.querySelector('.epc-card-remove')?.addEventListener('click', () => {
144
+ this.appState.eyepieces = this.appState.eyepieces.filter(e => e.id !== ep.id);
145
+ this.renderCards();
146
+ this.updateAll();
147
+ });
148
+
149
+ const card = clone.querySelector('.epc-card') as HTMLElement;
150
+ card.addEventListener('click', (e) => {
151
+ if ((e.target as HTMLElement).closest('.epc-card-remove')) return;
152
+ if ((e.target as HTMLElement).tagName.toLowerCase() === 'input') return;
153
+ this.appState.activeEyepiece = ep.id;
154
+ this.updateAll();
155
+ });
156
+ }
157
+
158
+ createCard(ep: EyepieceData): HTMLElement {
159
+ const clone = this.createCardDom(ep);
160
+ this.bindCardEvents(clone, ep);
161
+ return clone;
162
+ }
163
+
164
+ renderCards() {
165
+ this.container.innerHTML = '';
166
+ this.appState.eyepieces.forEach(ep => {
167
+ this.container.appendChild(this.createCard(ep));
168
+ });
169
+ }
170
+
171
+ setElText(root: HTMLElement, selector: string, text: string) {
172
+ const el = root.querySelector(selector);
173
+ if (el) el.textContent = text;
174
+ }
175
+
176
+ updateCardDom(cardEl: HTMLElement, stats: EyepieceStats, exitPupil: number, analysis: EyepieceStatus) {
177
+ const activeEp = this.getActiveEyepiece();
178
+ const isActive = activeEp && activeEp.id === cardEl.dataset.id;
179
+ let className = 'epc-card';
180
+
181
+ const valid = stats.magnification > 0;
182
+ this.setElText(cardEl, '[data-type="mag"]', valid ? stats.magnification.toFixed(0) : '--');
183
+ this.setElText(cardEl, '[data-type="tfov"]', valid ? stats.trueFieldOfView.toFixed(2) : '--');
184
+ this.setElText(cardEl, '[data-type="pupil"]', valid ? exitPupil.toFixed(1) : '--');
185
+
186
+ if (valid) className += ` status-${analysis.state}`;
187
+ if (isActive) className += ' active';
188
+ cardEl.className = className;
189
+ }
190
+
191
+ updateAll() {
192
+ this.saveState();
193
+ const focalRatio = this.appState.aperture > 0 ? this.appState.focal / this.appState.aperture : 0;
194
+ const limits = getTelescopeLimits(this.appState.aperture);
195
+
196
+ document.querySelectorAll('.epc-card').forEach(cardEl => {
197
+ const id = (cardEl as HTMLElement).dataset.id;
198
+ const ep = this.appState.eyepieces.find(e => e.id === id);
199
+ if (!ep) return;
200
+
201
+ const stats = calculateEyepieceStats(this.appState.focal, ep.focal, ep.afov);
202
+ const exitPupil = calculateExitPupil(ep.focal, focalRatio);
203
+ const analysis = analyzeEyepiece(stats, limits, exitPupil);
204
+
205
+ this.updateCardDom(cardEl as HTMLElement, stats, exitPupil, analysis);
206
+ });
207
+
208
+ this.updateCTA(limits);
209
+ this.updateSim();
210
+ }
211
+
212
+ updateCTA(limits: TelescopeLimits) {
213
+ const cta = document.getElementById('epc-cta');
214
+ if (!cta) return;
215
+ if (this.appState.aperture > 0 && limits.maxMagnification > 0) {
216
+ const activeEp = this.getActiveEyepiece();
217
+ let isWarning = false;
218
+
219
+ if (activeEp && activeEp.focal > 0) {
220
+ const stats = calculateEyepieceStats(this.appState.focal, activeEp.focal, activeEp.afov);
221
+ const focalRatio = this.appState.focal / this.appState.aperture;
222
+ const pupil = calculateExitPupil(activeEp.focal, focalRatio);
223
+ const analysis = analyzeEyepiece(stats, limits, pupil);
224
+ if (analysis.state !== 'perfect') isWarning = true;
225
+ }
226
+
227
+ let text = this.ui.ctaText.replace('{min}', Math.round(limits.minMagnification).toString());
228
+ text = text.replace('{max}', Math.round(limits.maxMagnification * 0.75).toString());
229
+ cta.textContent = text;
230
+
231
+ cta.className = 'epc-cta-box show';
232
+ if (isWarning) cta.classList.add('warning-border');
233
+ } else {
234
+ cta.classList.remove('show');
235
+ }
236
+ }
237
+
238
+ getActiveEyepiece(): EyepieceData | null {
239
+ const validOps = this.appState.eyepieces.filter(ep => ep.focal > 0 && this.appState.focal > 0);
240
+ if (validOps.length === 0) return null;
241
+
242
+ let epToShow = validOps[0];
243
+ if (this.appState.activeEyepiece) {
244
+ const found = validOps.find(e => e.id === this.appState.activeEyepiece);
245
+ if (found) epToShow = found;
246
+ }
247
+ return epToShow;
248
+ }
249
+
250
+ applyVisuals(ep: EyepieceData, targetDeg: number) {
251
+ const targetEl = document.getElementById('epc-sim-target');
252
+ const noiseEl = document.getElementById('epc-sim-noise');
253
+ const masksEl = document.getElementById('epc-sim-masks');
254
+ if (!targetEl || !noiseEl || !masksEl) return;
255
+
256
+ const stats = calculateEyepieceStats(this.appState.focal, ep.focal, ep.afov);
257
+ const focalRatio = this.appState.aperture > 0 ? this.appState.focal / this.appState.aperture : 0;
258
+ const exitPupil = calculateExitPupil(ep.focal, focalRatio);
259
+
260
+ const scale = targetDeg / stats.trueFieldOfView;
261
+ const sizePct = Math.min(scale * 100, 400);
262
+ targetEl.style.width = `${sizePct}%`;
263
+ targetEl.style.height = `${sizePct}%`;
264
+
265
+ const mask = document.createElement('div');
266
+ mask.className = 'epc-sim-mask';
267
+ mask.style.width = '100%';
268
+ mask.style.height = '100%';
269
+ masksEl.appendChild(mask);
270
+
271
+ if (exitPupil < 1.0) {
272
+ noiseEl.style.opacity = Math.min(1, (1.0 - exitPupil) * 1.5).toString();
273
+ }
274
+ }
275
+
276
+ getFeedbackMsg(analysis: EyepieceStatus, stats: EyepieceStats): string {
277
+ if (analysis.state === 'perfect') {
278
+ if (stats.magnification < 50) return this.ui.statusPerfectDeepSky;
279
+ if (stats.magnification > 100) return this.ui.statusPerfectPlanetary;
280
+ return this.ui.statusPerfectGeneral;
281
+ }
282
+ if (analysis.state === 'warning') {
283
+ return analysis.reason === 'lowMag' ? this.ui.statusWarningLowMag : this.ui.statusWarningHighMag;
284
+ }
285
+ return analysis.reason === 'highPupil' ? this.ui.statusDangerPupil : this.ui.statusDangerHighMag;
286
+ }
287
+
288
+ applyFeedback(ep: EyepieceData) {
289
+ const feedback = document.getElementById('epc-feedback-status');
290
+ if (!feedback) return;
291
+
292
+ const stats = calculateEyepieceStats(this.appState.focal, ep.focal, ep.afov);
293
+ const limits = getTelescopeLimits(this.appState.aperture);
294
+ const focalRatio = this.appState.aperture > 0 ? this.appState.focal / this.appState.aperture : 0;
295
+ const exitPupil = calculateExitPupil(ep.focal, focalRatio);
296
+ const analysis = analyzeEyepiece(stats, limits, exitPupil);
297
+
298
+ feedback.textContent = this.getFeedbackMsg(analysis, stats);
299
+ feedback.classList.add('show', analysis.state);
300
+ }
301
+
302
+ updateSim() {
303
+ const targetSel = document.getElementById('epc-target') as HTMLSelectElement;
304
+ if (!targetSel) return;
305
+
306
+ const targetId = targetSel.value;
307
+ const targetDeg = this.targetSizes[targetId];
308
+
309
+ const targetEl = document.getElementById('epc-sim-target');
310
+ const noiseEl = document.getElementById('epc-sim-noise');
311
+ const masksEl = document.getElementById('epc-sim-masks');
312
+ const feedback = document.getElementById('epc-feedback-status');
313
+ if (!targetEl || !noiseEl || !masksEl || !feedback) return;
314
+
315
+ targetEl.className = `epc-sim-target epc-target-${targetId}`;
316
+ masksEl.innerHTML = '';
317
+ feedback.className = 'epc-feedback-card';
318
+ feedback.textContent = '';
319
+ noiseEl.style.opacity = '0';
320
+
321
+ const epToShow = this.getActiveEyepiece();
322
+ if (!epToShow) return;
323
+
324
+ this.applyVisuals(epToShow, targetDeg);
325
+ this.applyFeedback(epToShow);
326
+ }
327
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { smartEyepieceCalculator } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const content = await smartEyepieceCalculator.entry.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,6 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ { name: 'Sky & Telescope - Eyepiece Equations', url: 'https://www.telescoop.nl/en/telescope-eyepiece-explanation/' },
5
+ { name: 'Cloudy Nights - All About Exit Pupil', url: 'https://www.cloudynights.com/forums/topic/849970-all-about-exit-pupil/' },
6
+ ];
@@ -0,0 +1,125 @@
1
+ ---
2
+ import type { EyepieceCalculatorUI } from './entry';
3
+ import './smart-eyepiece-calculator.css';
4
+
5
+ interface Props {
6
+ ui: EyepieceCalculatorUI;
7
+ }
8
+
9
+ const { ui } = Astro.props;
10
+ ---
11
+
12
+ <script define:vars={{ ui }} is:inline>
13
+ window.__epcUI = ui;
14
+ </script>
15
+
16
+ <div class="epc-wrapper" id="epc-root">
17
+ <div class="epc-layout">
18
+ <div class="epc-controls">
19
+ <section class="epc-section">
20
+ <h3 class="epc-section-title">{ui.telescopeTitle}</h3>
21
+
22
+ <div class="epc-control-group">
23
+ <label class="epc-label">{ui.presetsLabel}</label>
24
+ <select id="epc-preset" class="epc-select">
25
+ <option value="">{ui.presetsPlaceholder}</option>
26
+ <option value="60,700">Refractor 60/700 (f/11.6)</option>
27
+ <option value="72,400">Apo ED72 Astrofoto 72/400 (f/5.5)</option>
28
+ <option value="80,400">Refractor Gran Campo 80/400 (f/5)</option>
29
+ <option value="127,1000">Celestron 127EQ (f/7.9)</option>
30
+ <option value="130,650">Sky-Watcher 130/650 (f/5)</option>
31
+ <option value="150,1500">Maksutov 150/1500 (f/10)</option>
32
+ <option value="200,1200">Dobson 8" 200/1200 (f/6)</option>
33
+ <option value="300,1500">Dobson 12" Gigante 300/1500 (f/5)</option>
34
+ </select>
35
+ </div>
36
+
37
+ <div class="epc-grid-2">
38
+ <div class="epc-control-group">
39
+ <label class="epc-label" title={ui.apertureHelp}>
40
+ {ui.apertureLabel}
41
+ <svg class="epc-info-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
42
+ </label>
43
+ <input type="number" id="epc-aperture" class="epc-input" placeholder="Ej: 150" />
44
+ </div>
45
+ <div class="epc-control-group">
46
+ <label class="epc-label" title={ui.focalHelp}>
47
+ {ui.focalLabel}
48
+ <svg class="epc-info-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
49
+ </label>
50
+ <input type="number" id="epc-focal" class="epc-input" placeholder="Ej: 750" />
51
+ </div>
52
+ </div>
53
+ </section>
54
+
55
+ <section class="epc-section">
56
+ <h3 class="epc-section-title">{ui.eyepiecesTitle}</h3>
57
+ <div id="epc-cards-container" class="epc-cards-container"></div>
58
+ <button id="epc-add-btn" class="epc-btn-primary">{ui.addEyepieceBtn}</button>
59
+ </section>
60
+ </div>
61
+
62
+ <div class="epc-simulator-panel">
63
+ <h3 class="epc-section-title">{ui.simulatorTitle}</h3>
64
+
65
+ <div class="epc-control-group">
66
+ <select id="epc-target" class="epc-select">
67
+ <option value="moon">{ui.targetMoon} (0.5°)</option>
68
+ <option value="pleiades">{ui.targetPleiades} (1.8°)</option>
69
+ <option value="andromeda">{ui.targetAndromeda} (3.16°)</option>
70
+ <option value="m13">{ui.targetM13} (0.33°)</option>
71
+ <option value="saturn">{ui.targetSaturn} (0.012°)</option>
72
+ </select>
73
+ </div>
74
+
75
+ <div class="epc-sim-container">
76
+ <div class="epc-sim-canvas" id="epc-sim-canvas">
77
+ <div class="epc-sim-target" id="epc-sim-target"></div>
78
+ <div class="epc-sim-noise" id="epc-sim-noise"></div>
79
+ <div id="epc-sim-masks"></div>
80
+ </div>
81
+ </div>
82
+
83
+ <div id="epc-feedback-status" class="epc-feedback-card"></div>
84
+
85
+ <div class="epc-cta-box" id="epc-cta">
86
+ {ui.ctaText}
87
+ </div>
88
+ </div>
89
+ </div>
90
+ </div>
91
+
92
+ <template id="epc-card-template">
93
+ <div class="epc-card">
94
+ <div class="epc-card-header">
95
+ <span class="epc-card-title">Ocular</span>
96
+ <button class="epc-card-remove"><svg viewBox="0 0 24 24" fill="none" class="epc-icon"><path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button>
97
+ </div>
98
+ <div class="epc-grid-2">
99
+ <div class="epc-control-group">
100
+ <label class="epc-label" title="Focal del ocular en mm">Focal (mm)</label>
101
+ <input type="number" class="epc-input epc-ep-focal" placeholder="Ej: 10" />
102
+ </div>
103
+ <div class="epc-control-group">
104
+ <label class="epc-label" title="Campo aparente en grados">AFOV (°)</label>
105
+ <input type="number" class="epc-input epc-ep-afov" placeholder="Ej: 52" value="52" />
106
+ </div>
107
+ </div>
108
+ <div class="epc-card-results">
109
+ <div class="epc-stat"><span class="epc-stat-val" data-type="mag">--</span><span class="epc-stat-lbl">x</span></div>
110
+ <div class="epc-stat"><span class="epc-stat-val" data-type="tfov">--</span><span class="epc-stat-lbl">°</span></div>
111
+ <div class="epc-stat"><span class="epc-stat-val" data-type="pupil">--</span><span class="epc-stat-lbl">mm</span></div>
112
+ </div>
113
+ </div>
114
+ </template>
115
+
116
+
117
+
118
+ <script>
119
+ import { EyepieceApp } from './app';
120
+
121
+ if (typeof window !== 'undefined') {
122
+ const win = window as unknown as { __epcUI: Record<string, string> };
123
+ new EyepieceApp(win.__epcUI);
124
+ }
125
+ </script>
@@ -0,0 +1,67 @@
1
+ import type { AstronomyToolEntry, ToolLocaleContent } from '../../types';
2
+
3
+ export interface EyepieceCalculatorUI {
4
+ [key: string]: string;
5
+ toolTitle: string;
6
+ telescopeTitle: string;
7
+ apertureLabel: string;
8
+ focalLabel: string;
9
+ presetsLabel: string;
10
+ presetsPlaceholder: string;
11
+ addEyepieceBtn: string;
12
+ eyepiecesTitle: string;
13
+ eyepieceFocal: string;
14
+ eyepieceAfov: string;
15
+ removeEyepiece: string;
16
+ magLabel: string;
17
+ tfovLabel: string;
18
+ pupilLabel: string;
19
+ simulatorTitle: string;
20
+ targetLabel: string;
21
+ targetMoon: string;
22
+ targetSaturn: string;
23
+ targetPleiades: string;
24
+ targetAndromeda: string;
25
+ targetM13: string;
26
+ statusDangerHighMag: string;
27
+ statusDangerPupil: string;
28
+ statusWarningHighMag: string;
29
+ statusWarningLowMag: string;
30
+ statusPerfectDeepSky: string;
31
+ statusPerfectPlanetary: string;
32
+ statusPerfectGeneral: string;
33
+ ctaText: string;
34
+ compareText: string;
35
+ apertureHelp: string;
36
+ focalHelp: string;
37
+ afovHelp: string;
38
+ }
39
+
40
+ export type EyepieceCalculatorLocaleContent = ToolLocaleContent<EyepieceCalculatorUI>;
41
+
42
+ export const eyepieceCalculator: AstronomyToolEntry<EyepieceCalculatorUI> = {
43
+ id: 'smart-eyepiece-calculator',
44
+ icons: {
45
+ bg: 'mdi:eye-circle',
46
+ fg: 'mdi:telescope',
47
+ },
48
+ i18n: {
49
+ de: () => import('./i18n/de').then((m) => m.content),
50
+ en: () => import('./i18n/en').then((m) => m.content),
51
+ es: () => import('./i18n/es').then((m) => m.content),
52
+ fr: () => import('./i18n/fr').then((m) => m.content),
53
+ id: () => import('./i18n/id').then((m) => m.content),
54
+ it: () => import('./i18n/it').then((m) => m.content),
55
+ ja: () => import('./i18n/ja').then((m) => m.content),
56
+ ko: () => import('./i18n/ko').then((m) => m.content),
57
+ nl: () => import('./i18n/nl').then((m) => m.content),
58
+ pl: () => import('./i18n/pl').then((m) => m.content),
59
+ pt: () => import('./i18n/pt').then((m) => m.content),
60
+ ru: () => import('./i18n/ru').then((m) => m.content),
61
+ sv: () => import('./i18n/sv').then((m) => m.content),
62
+ tr: () => import('./i18n/tr').then((m) => m.content),
63
+ zh: () => import('./i18n/zh').then((m) => m.content),
64
+ },
65
+ };
66
+
67
+ export { bibliography } from './bibliography';