@jjlmoya/utils-astronomy 1.35.1 → 1.37.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 (86) hide show
  1. package/package.json +18 -5
  2. package/scripts/postbuild.mjs +17 -0
  3. package/scripts/postinstall.mjs +2 -4
  4. package/src/category/index.ts +2 -1
  5. package/src/components/ProductionBreadcrumb.astro +134 -0
  6. package/src/components/ProductionStructuredData.astro +46 -0
  7. package/src/components/ProductionWidget.astro +182 -0
  8. package/src/entries.ts +4 -2
  9. package/src/i18n/header-ui.ts +15 -0
  10. package/src/i18n/language-ui.ts +9 -0
  11. package/src/i18n/languages.ts +24 -0
  12. package/src/identity/brands.ts +5 -0
  13. package/src/index.ts +2 -1
  14. package/src/layouts/ProductionCategoryPage.astro +184 -0
  15. package/src/layouts/ProductionPage.astro +218 -0
  16. package/src/layouts/ProductionUtilityPage.astro +283 -0
  17. package/src/mfe/assets.ts +39 -0
  18. package/src/mfe/category-ui.ts +34 -0
  19. package/src/mfe/manifest.ts +45 -0
  20. package/src/mfe/routes.ts +50 -0
  21. package/src/pages/[locale]/[utilities]/[categories]/[category]/[slug]/manifest.json.ts +49 -0
  22. package/src/pages/[locale]/[utilities]/[categories]/[category]/[slug].astro +100 -0
  23. package/src/pages/[locale]/[utilities]/[categories]/[category].astro +44 -0
  24. package/src/pages/index.astro +4 -4
  25. package/src/pages/mfe-sitemaps/[locale]/[vertical]/sitemap.xml.ts +75 -0
  26. package/src/pages/utilidades/[slug]/manifest.json.ts +28 -0
  27. package/src/pages/utilidades/[slug].astro +90 -0
  28. package/src/pages/utilidades/categorias/[category].astro +38 -0
  29. package/src/tests/locale_completeness.test.ts +2 -2
  30. package/src/tests/mfe_cache_contract.test.ts +14 -0
  31. package/src/tests/mfe_manifest.test.ts +28 -0
  32. package/src/tests/registry_contract.test.ts +67 -0
  33. package/src/tests/tool_validation.test.ts +4 -3
  34. package/src/tool/astrophotographyExposureStackPlanner/astrophotography-exposure-stacking-planner.css +496 -0
  35. package/src/tool/astrophotographyExposureStackPlanner/bibliography.astro +14 -0
  36. package/src/tool/astrophotographyExposureStackPlanner/bibliography.ts +12 -0
  37. package/src/tool/astrophotographyExposureStackPlanner/component.astro +149 -0
  38. package/src/tool/astrophotographyExposureStackPlanner/controller.ts +125 -0
  39. package/src/tool/astrophotographyExposureStackPlanner/dom-views.ts +41 -0
  40. package/src/tool/astrophotographyExposureStackPlanner/entry.ts +29 -0
  41. package/src/tool/astrophotographyExposureStackPlanner/evaluator.ts +44 -0
  42. package/src/tool/astrophotographyExposureStackPlanner/i18n/de.ts +40 -0
  43. package/src/tool/astrophotographyExposureStackPlanner/i18n/en.ts +232 -0
  44. package/src/tool/astrophotographyExposureStackPlanner/i18n/es.ts +52 -0
  45. package/src/tool/astrophotographyExposureStackPlanner/i18n/fr.ts +40 -0
  46. package/src/tool/astrophotographyExposureStackPlanner/i18n/id.ts +40 -0
  47. package/src/tool/astrophotographyExposureStackPlanner/i18n/it.ts +40 -0
  48. package/src/tool/astrophotographyExposureStackPlanner/i18n/ja.ts +40 -0
  49. package/src/tool/astrophotographyExposureStackPlanner/i18n/ko.ts +40 -0
  50. package/src/tool/astrophotographyExposureStackPlanner/i18n/nl.ts +40 -0
  51. package/src/tool/astrophotographyExposureStackPlanner/i18n/pl.ts +40 -0
  52. package/src/tool/astrophotographyExposureStackPlanner/i18n/pt.ts +40 -0
  53. package/src/tool/astrophotographyExposureStackPlanner/i18n/ru.ts +40 -0
  54. package/src/tool/astrophotographyExposureStackPlanner/i18n/sv.ts +40 -0
  55. package/src/tool/astrophotographyExposureStackPlanner/i18n/tr.ts +40 -0
  56. package/src/tool/astrophotographyExposureStackPlanner/i18n/zh.ts +40 -0
  57. package/src/tool/astrophotographyExposureStackPlanner/index.ts +11 -0
  58. package/src/tool/astrophotographyExposureStackPlanner/localized.ts +59 -0
  59. package/src/tool/astrophotographyExposureStackPlanner/logic.test.ts +74 -0
  60. package/src/tool/astrophotographyExposureStackPlanner/logic.ts +95 -0
  61. package/src/tool/astrophotographyExposureStackPlanner/seo.astro +14 -0
  62. package/src/tool/astrophotographyExposureStackPlanner/storage.ts +22 -0
  63. package/src/tool/astrophotographyExposureStackPlanner/ui.ts +102 -0
  64. package/src/tool/meteorShowerObservingPlanner/dom-views.ts +38 -16
  65. package/src/tool/meteorShowerObservingPlanner/i18n/de.ts +219 -219
  66. package/src/tool/meteorShowerObservingPlanner/i18n/en.ts +219 -219
  67. package/src/tool/meteorShowerObservingPlanner/i18n/es.ts +219 -219
  68. package/src/tool/meteorShowerObservingPlanner/i18n/fr.ts +219 -219
  69. package/src/tool/meteorShowerObservingPlanner/i18n/id.ts +223 -223
  70. package/src/tool/meteorShowerObservingPlanner/i18n/it.ts +223 -223
  71. package/src/tool/meteorShowerObservingPlanner/i18n/ja.ts +219 -219
  72. package/src/tool/meteorShowerObservingPlanner/i18n/ko.ts +219 -219
  73. package/src/tool/meteorShowerObservingPlanner/i18n/nl.ts +223 -223
  74. package/src/tool/meteorShowerObservingPlanner/i18n/pl.ts +223 -223
  75. package/src/tool/meteorShowerObservingPlanner/i18n/pt.ts +223 -223
  76. package/src/tool/meteorShowerObservingPlanner/i18n/ru.ts +219 -219
  77. package/src/tool/meteorShowerObservingPlanner/i18n/sv.ts +223 -223
  78. package/src/tool/meteorShowerObservingPlanner/i18n/tr.ts +223 -223
  79. package/src/tool/meteorShowerObservingPlanner/i18n/zh.ts +219 -219
  80. package/src/tool/meteorShowerObservingPlanner/meteor-shower-observing-planner.css +16 -9
  81. package/src/tool/meteorShowerObservingPlanner/ui.ts +87 -87
  82. package/src/tools.ts +3 -1
  83. package/src/types.ts +2 -2
  84. package/src/worker.ts +27 -0
  85. package/src/pages/[locale]/[slug].astro +0 -161
  86. package/src/pages/[locale].astro +0 -251
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createUtilityManifestResponse } from "../mfe/manifest";
3
+
4
+ describe("MFE utility manifest", () => {
5
+ it("returns a versioned installable manifest for the current utility", async () => {
6
+ const response = createUtilityManifestResponse({
7
+ title: "Card draw odds calculator",
8
+ description: "Calculate card draw probabilities.",
9
+ startUrl: "/en/utilities/categories/astronomy/card-draw-odds-calculator/",
10
+ englishSlug: "card-draw-odds-calculator",
11
+ });
12
+ const manifest = await response.json() as {
13
+ name: string;
14
+ start_url: string;
15
+ scope: string;
16
+ icons: { src: string; sizes: string; type: string; purpose: string }[];
17
+ };
18
+
19
+ expect(response.headers.get("Content-Type")).toContain("application/manifest+json");
20
+ expect(response.headers.get("Cache-Control")).toContain("immutable");
21
+ expect(manifest.name).toBe("Card draw odds calculator");
22
+ expect(manifest.start_url).toBe(manifest.scope);
23
+ expect(manifest.icons[0]?.src).toContain("/_utilities/astronomy/images/card-draw-odds-calculator.webp?version=");
24
+ expect(manifest.icons[0]?.sizes).toBe("512x512");
25
+ expect(manifest.icons[0]?.type).toBe("image/webp");
26
+ expect(manifest.icons[0]?.purpose).toBe("any");
27
+ });
28
+ });
@@ -0,0 +1,67 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { existsSync, readdirSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { pathToFileURL } from 'url';
5
+ import { ALL_ENTRIES } from '../entries';
6
+ import { ALL_TOOLS } from '../tools';
7
+ import type { ToolDefinition } from '../types';
8
+
9
+ function isToolDefinition(value: unknown): value is ToolDefinition {
10
+ if (!value || typeof value !== 'object') return false;
11
+
12
+ const candidate = value as Record<string, unknown>;
13
+ return Boolean(
14
+ candidate.entry &&
15
+ typeof candidate.entry === 'object' &&
16
+ typeof candidate.Component === 'function' &&
17
+ typeof candidate.SEOComponent === 'function' &&
18
+ typeof candidate.BibliographyComponent === 'function',
19
+ );
20
+ }
21
+
22
+ const toolRoot = join(process.cwd(), 'src', 'tool');
23
+ const toolDirectories = readdirSync(toolRoot, { withFileTypes: true })
24
+ .filter((entry) => entry.isDirectory() && existsSync(join(toolRoot, entry.name, 'index.ts')))
25
+ .map((entry) => entry.name)
26
+ .sort();
27
+
28
+ describe('Library registry contract', () => {
29
+ it('keeps ALL_ENTRIES and ALL_TOOLS synchronized by id', () => {
30
+ const entryIds = ALL_ENTRIES.map((entry) => entry.id);
31
+ const toolIds = ALL_TOOLS.map((tool) => tool.entry.id);
32
+
33
+ expect(new Set(entryIds).size).toBe(entryIds.length);
34
+ expect(new Set(toolIds).size).toBe(toolIds.length);
35
+ expect([...toolIds].sort()).toEqual([...entryIds].sort());
36
+ });
37
+
38
+ it('registers every tool runtime index in both public registries', async () => {
39
+ const failures: string[] = [];
40
+
41
+ for (const directory of toolDirectories) {
42
+ const runtimeModule = await import(
43
+ pathToFileURL(join(toolRoot, directory, 'index.ts')).href,
44
+ );
45
+ const definitions = [...new Set(Object.values(runtimeModule).filter(isToolDefinition))];
46
+
47
+ if (definitions.length !== 1) {
48
+ failures.push(`${directory}: expected exactly one ToolDefinition export, found ${definitions.length}`);
49
+ continue;
50
+ }
51
+
52
+ const [definition] = definitions;
53
+ if (!definition) {
54
+ failures.push(`${directory}: runtime definition is undefined`);
55
+ continue;
56
+ }
57
+ if (!ALL_TOOLS.some((tool) => tool.entry.id === definition.entry.id)) {
58
+ failures.push(`${directory}: runtime definition is missing from ALL_TOOLS`);
59
+ }
60
+ if (!ALL_ENTRIES.some((entry) => entry.id === definition.entry.id)) {
61
+ failures.push(`${directory}: runtime entry is missing from ALL_ENTRIES`);
62
+ }
63
+ }
64
+
65
+ expect(failures).toEqual([]);
66
+ });
67
+ });
@@ -79,6 +79,7 @@ describe('Tool Validation Suite', () => {
79
79
  'calculadora-oculares',
80
80
  'planificador-pupila-salida-aumentos-telescopio',
81
81
  'planificador-observacion-lluvia-meteoros',
82
+ 'planificador-apilado-exposiciones-astrofotografia',
82
83
  'meteor-shower-observing-planner',
83
84
  ];
84
85
  expect(validSlugs).toContain(content.slug);
@@ -105,12 +106,12 @@ describe('Tool Validation Suite', () => {
105
106
  });
106
107
 
107
108
  describe('Library Registration', () => {
108
- it('should have 7 tools in ALL_TOOLS', () => {
109
- expect(ALL_TOOLS.length).toBe(7);
109
+ it('should have 8 tools in ALL_TOOLS', () => {
110
+ expect(ALL_TOOLS.length).toBe(8);
110
111
  });
111
112
 
112
113
  it('should have all tools in astronomyCategory', () => {
113
- expect(astronomyCategory.tools.length).toBe(7);
114
+ expect(astronomyCategory.tools.length).toBe(8);
114
115
  ALL_TOOLS.forEach(({ entry }) => {
115
116
  const exists = astronomyCategory.tools.some((t: any) => t.id === entry.id);
116
117
  expect(exists).toBe(true);
@@ -0,0 +1,496 @@
1
+ :root {
2
+ --n-base-night: #f4f1ea;
3
+ --n-base-paper: #fffdf8;
4
+ --n-base-ink: #202a2e;
5
+ --n-base-muted: #657276;
6
+ --n-base-line: #d8d5cc;
7
+ --n-base-blue: #2f6677;
8
+ --n-base-gold: #b67b2c;
9
+ --n-base-alert: #b65742;
10
+ --n-base-shadow: #c9c3b8;
11
+ }
12
+
13
+ .theme-dark {
14
+ --n-dark-night: #15191c;
15
+ --n-dark-paper: #20262a;
16
+ --n-dark-ink: #f1eee5;
17
+ --n-dark-muted: #aab4b4;
18
+ --n-dark-line: #3a4447;
19
+ --n-dark-blue: #8fc2ca;
20
+ --n-dark-gold: #e1ad5a;
21
+ --n-dark-alert: #ed987f;
22
+ --n-dark-shadow: #0a0d0e;
23
+ }
24
+
25
+ .astrophotography-exposure-stack-planner {
26
+ --n-night: var(--n-base-night);
27
+ --n-paper: var(--n-base-paper);
28
+ --n-ink: var(--n-base-ink);
29
+ --n-muted: var(--n-base-muted);
30
+ --n-line: var(--n-base-line);
31
+ --n-blue: var(--n-base-blue);
32
+ --n-gold: var(--n-base-gold);
33
+ --n-alert: var(--n-base-alert);
34
+ --n-shadow: var(--n-base-shadow);
35
+
36
+ display: grid;
37
+ grid-template-columns: minmax(260px, 0.8fr) minmax(0, 1.5fr);
38
+ min-height: 690px;
39
+ overflow: hidden;
40
+ border: 1px solid var(--n-line);
41
+ background-color: var(--n-night);
42
+ color: var(--n-ink);
43
+ box-shadow: 12px 12px 0 var(--n-shadow);
44
+ min-width: 0;
45
+ }
46
+
47
+ .theme-dark .astrophotography-exposure-stack-planner {
48
+ --n-night: var(--n-dark-night);
49
+ --n-paper: var(--n-dark-paper);
50
+ --n-ink: var(--n-dark-ink);
51
+ --n-muted: var(--n-dark-muted);
52
+ --n-line: var(--n-dark-line);
53
+ --n-blue: var(--n-dark-blue);
54
+ --n-gold: var(--n-dark-gold);
55
+ --n-alert: var(--n-dark-alert);
56
+ --n-shadow: var(--n-dark-shadow);
57
+ }
58
+
59
+ body:has(.astrophotography-exposure-stack-planner) .utility-header-title {
60
+ font-size: clamp(2.15rem, 6.5vw, 4.5rem);
61
+ }
62
+
63
+ body:has(.astrophotography-exposure-stack-planner) .utility-header-title > span {
64
+ min-width: 0;
65
+ max-width: 100%;
66
+ overflow-wrap: anywhere;
67
+ text-align: center;
68
+ }
69
+
70
+ body:has(.astrophotography-exposure-stack-planner) .utility-header-description {
71
+ width: 100%;
72
+ padding: 0 10px;
73
+ }
74
+
75
+ .exposure-planner-form {
76
+ min-width: 0;
77
+ padding: 30px 26px;
78
+ border-right: 1px solid var(--n-line);
79
+ }
80
+
81
+ .control-rail,
82
+ .camera-fields,
83
+ .choice-fieldset,
84
+ .timing-fields {
85
+ display: grid;
86
+ gap: 16px;
87
+ border: 0;
88
+ margin: 0;
89
+ padding: 0;
90
+ }
91
+
92
+ .control-rail {
93
+ gap: 24px;
94
+ }
95
+
96
+ legend {
97
+ width: 100%;
98
+ margin-bottom: 2px;
99
+ color: var(--n-gold);
100
+ font-size: 0.7rem;
101
+ font-weight: 800;
102
+ letter-spacing: 0.14em;
103
+ text-transform: uppercase;
104
+ }
105
+
106
+ .camera-fields {
107
+ grid-template-columns: 1fr 1fr;
108
+ padding-bottom: 22px;
109
+ border-bottom: 1px solid var(--n-line);
110
+ }
111
+
112
+ .camera-fields legend {
113
+ grid-column: 1 / -1;
114
+ }
115
+
116
+ .timing-fields {
117
+ padding-top: 2px;
118
+ }
119
+
120
+ .field-control {
121
+ display: grid;
122
+ gap: 7px;
123
+ color: var(--n-ink);
124
+ font-size: 0.82rem;
125
+ font-weight: 700;
126
+ }
127
+
128
+ .field-control small {
129
+ color: var(--n-muted);
130
+ font-size: 0.68rem;
131
+ font-weight: 500;
132
+ line-height: 1.35;
133
+ }
134
+
135
+ input {
136
+ width: 100%;
137
+ min-height: 44px;
138
+ padding: 9px 10px;
139
+ border: 1px solid var(--n-line);
140
+ border-radius: 0;
141
+ background-color: var(--n-paper);
142
+ color: var(--n-ink);
143
+ font: inherit;
144
+ font-variant-numeric: tabular-nums;
145
+ }
146
+
147
+ input:focus-visible,
148
+ button:focus-visible,
149
+ summary:focus-visible {
150
+ outline: 3px solid var(--n-gold);
151
+ outline-offset: 2px;
152
+ }
153
+
154
+ .choice-grid {
155
+ min-width: 0;
156
+ display: grid;
157
+ grid-template-columns: 1fr 1fr;
158
+ gap: 7px;
159
+ }
160
+
161
+ .sensor-grid {
162
+ grid-template-columns: 1fr 1fr;
163
+ }
164
+
165
+ button {
166
+ min-width: 0;
167
+ min-height: 40px;
168
+ padding: 8px 9px;
169
+ border: 1px solid var(--n-line);
170
+ border-radius: 0;
171
+ background-color: var(--n-paper);
172
+ color: var(--n-muted);
173
+ cursor: pointer;
174
+ font: inherit;
175
+ font-size: 0.7rem;
176
+ font-weight: 700;
177
+ line-height: 1.2;
178
+ overflow-wrap: anywhere;
179
+ transition: border-color 160ms ease, background-color 160ms ease, color 160ms ease;
180
+ }
181
+
182
+ button:hover,
183
+ button.is-active {
184
+ border-color: var(--n-blue);
185
+ background-color: var(--n-blue);
186
+ color: var(--n-night);
187
+ }
188
+
189
+ .integration-field {
190
+ padding-top: 4px;
191
+ }
192
+
193
+ .exposure-result {
194
+ min-width: 0;
195
+ display: grid;
196
+ align-content: start;
197
+ gap: 24px;
198
+ padding: 30px 34px 28px;
199
+ background-color: var(--n-paper);
200
+ }
201
+
202
+ .result-topline,
203
+ .timeline-header,
204
+ .timeline-labels,
205
+ .result-heading,
206
+ .comparison-row,
207
+ .metric-strip {
208
+ display: flex;
209
+ align-items: center;
210
+ justify-content: space-between;
211
+ gap: 16px;
212
+ }
213
+
214
+ .result-kicker {
215
+ color: var(--n-muted);
216
+ font-size: 0.68rem;
217
+ font-weight: 800;
218
+ letter-spacing: 0.16em;
219
+ text-transform: uppercase;
220
+ }
221
+
222
+ .status-pill {
223
+ max-width: 45%;
224
+ overflow-wrap: anywhere;
225
+ padding: 6px 9px;
226
+ border: 1px solid var(--n-gold);
227
+ color: var(--n-gold);
228
+ font-size: 0.66rem;
229
+ font-weight: 800;
230
+ text-transform: uppercase;
231
+ }
232
+
233
+ .status-pill[data-tone="critical"] {
234
+ border-color: var(--n-alert);
235
+ color: var(--n-alert);
236
+ }
237
+
238
+ .result-heading {
239
+ align-items: end;
240
+ padding-bottom: 24px;
241
+ border-bottom: 1px solid var(--n-line);
242
+ }
243
+
244
+ h2,
245
+ h3,
246
+ p {
247
+ margin: 0;
248
+ }
249
+
250
+ h2 {
251
+ max-width: 420px;
252
+ font-size: clamp(1.35rem, 3vw, 2.4rem);
253
+ line-height: 1;
254
+ letter-spacing: -0.04em;
255
+ }
256
+
257
+ .result-heading p {
258
+ max-width: 500px;
259
+ margin-top: 10px;
260
+ color: var(--n-muted);
261
+ font-size: 0.82rem;
262
+ line-height: 1.5;
263
+ }
264
+
265
+ .hero-output {
266
+ display: grid;
267
+ flex: 0 0 auto;
268
+ justify-items: end;
269
+ color: var(--n-blue);
270
+ }
271
+
272
+ .hero-output strong {
273
+ font-size: clamp(2.5rem, 7vw, 5.3rem);
274
+ font-weight: 800;
275
+ letter-spacing: -0.08em;
276
+ line-height: 0.85;
277
+ font-variant-numeric: tabular-nums;
278
+ }
279
+
280
+ .hero-output span {
281
+ margin-top: 8px;
282
+ color: var(--n-muted);
283
+ font-size: 0.72rem;
284
+ font-weight: 800;
285
+ letter-spacing: 0.12em;
286
+ text-transform: uppercase;
287
+ }
288
+
289
+ .timeline-panel {
290
+ display: grid;
291
+ gap: 13px;
292
+ }
293
+
294
+ h3 {
295
+ font-size: 0.82rem;
296
+ letter-spacing: 0.02em;
297
+ }
298
+
299
+ .timeline-header > span {
300
+ color: var(--n-muted);
301
+ font-size: 0.74rem;
302
+ font-variant-numeric: tabular-nums;
303
+ }
304
+
305
+ .timeline {
306
+ min-height: 88px;
307
+ padding: 20px 0;
308
+ border-top: 1px solid var(--n-line);
309
+ border-bottom: 1px solid var(--n-line);
310
+ }
311
+
312
+ .exposure-track {
313
+ display: flex;
314
+ align-items: stretch;
315
+ height: 47px;
316
+ gap: 3px;
317
+ }
318
+
319
+ .exposure-slice {
320
+ position: relative;
321
+ min-width: 7px;
322
+ background-color: var(--n-blue);
323
+ opacity: 0.55;
324
+ }
325
+
326
+ .exposure-slice::after {
327
+ position: absolute;
328
+ right: 2px;
329
+ bottom: 6px;
330
+ left: 2px;
331
+ height: 3px;
332
+ background-color: var(--n-gold);
333
+ content: '';
334
+ }
335
+
336
+ .exposure-slice.is-final {
337
+ background-color: var(--n-gold);
338
+ opacity: 1;
339
+ }
340
+
341
+ .exposure-slice.is-final::after {
342
+ background-color: var(--n-paper);
343
+ }
344
+
345
+ .timeline-labels {
346
+ color: var(--n-muted);
347
+ font-size: 0.68rem;
348
+ }
349
+
350
+ .metric-strip {
351
+ align-items: stretch;
352
+ border-bottom: 1px solid var(--n-line);
353
+ }
354
+
355
+ .metric-strip > div {
356
+ display: grid;
357
+ flex: 1;
358
+ gap: 7px;
359
+ padding: 0 16px 20px 0;
360
+ border-right: 1px solid var(--n-line);
361
+ }
362
+
363
+ .metric-strip > div + div {
364
+ padding-left: 16px;
365
+ }
366
+
367
+ .metric-strip > div:last-child {
368
+ border-right: 0;
369
+ }
370
+
371
+ .metric-strip span,
372
+ .comparison-row span {
373
+ color: var(--n-muted);
374
+ font-size: 0.7rem;
375
+ }
376
+
377
+ .metric-strip strong,
378
+ .comparison-row strong {
379
+ font-size: 1rem;
380
+ font-variant-numeric: tabular-nums;
381
+ }
382
+
383
+ .session-detail,
384
+ .workload-note,
385
+ .sensor-warning {
386
+ color: var(--n-muted);
387
+ font-size: 0.74rem;
388
+ line-height: 1.5;
389
+ }
390
+
391
+ .session-detail {
392
+ margin-top: -15px;
393
+ }
394
+
395
+ .workload-note,
396
+ .sensor-warning {
397
+ padding: 13px 15px;
398
+ border-left: 3px solid var(--n-gold);
399
+ background-color: var(--n-night);
400
+ }
401
+
402
+ .sensor-warning {
403
+ border-left-color: var(--n-alert);
404
+ }
405
+
406
+ .comparison-panel {
407
+ display: grid;
408
+ gap: 11px;
409
+ padding: 17px;
410
+ border: 1px solid var(--n-line);
411
+ }
412
+
413
+ .comparison-panel h3 {
414
+ margin-bottom: 3px;
415
+ color: var(--n-gold);
416
+ font-size: 0.7rem;
417
+ letter-spacing: 0.14em;
418
+ text-transform: uppercase;
419
+ }
420
+
421
+ .comparison-row + .comparison-row {
422
+ padding-top: 10px;
423
+ border-top: 1px solid var(--n-line);
424
+ }
425
+
426
+ .method-details {
427
+ color: var(--n-muted);
428
+ font-size: 0.76rem;
429
+ line-height: 1.55;
430
+ }
431
+
432
+ .method-details summary {
433
+ color: var(--n-ink);
434
+ cursor: pointer;
435
+ font-weight: 800;
436
+ }
437
+
438
+ .method-details p {
439
+ margin-top: 10px;
440
+ }
441
+
442
+ .limits-note {
443
+ padding: 13px 15px;
444
+ border-left: 3px solid var(--n-alert);
445
+ background-color: var(--n-night);
446
+ color: var(--n-muted);
447
+ font-size: 0.74rem;
448
+ line-height: 1.5;
449
+ }
450
+
451
+ .limits-note strong {
452
+ color: var(--n-alert);
453
+ }
454
+
455
+ .limits-note p {
456
+ margin-top: 5px;
457
+ }
458
+
459
+ @media (max-width: 820px) {
460
+ .astrophotography-exposure-stack-planner {
461
+ grid-template-columns: 1fr;
462
+ }
463
+
464
+ .exposure-planner-form {
465
+ border-right: 0;
466
+ border-bottom: 1px solid var(--n-line);
467
+ }
468
+ }
469
+
470
+ @media (max-width: 520px) {
471
+ .exposure-planner-form,
472
+ .exposure-result {
473
+ padding: 23px 18px;
474
+ }
475
+
476
+ .camera-fields {
477
+ grid-template-columns: 1fr;
478
+ }
479
+
480
+ .camera-fields legend {
481
+ grid-column: auto;
482
+ }
483
+
484
+ .result-heading {
485
+ align-items: start;
486
+ flex-direction: column;
487
+ }
488
+
489
+ .result-topline {
490
+ align-items: flex-start;
491
+ }
492
+
493
+ .hero-output {
494
+ justify-items: start;
495
+ }
496
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { astrophotographyExposureStackPlanner } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const content = await astrophotographyExposureStackPlanner.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,12 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const ASTROPHOTOGRAPHY_EXPOSURE_BIBLIOGRAPHY: BibliographyEntry[] = [
4
+ {
5
+ name: 'Societe Astronomique du Havre, La Regle NPF',
6
+ url: 'https://sahavre.fr/wp/regle-npf-rule/',
7
+ },
8
+ {
9
+ name: 'NASA Science, Astrophotography With Your Smartphone',
10
+ url: 'https://science.nasa.gov/solar-system/skywatching/night-sky-network/astrophotography-with-your-smartphone/',
11
+ },
12
+ ];