@jjlmoya/utils-science 1.24.0 → 1.26.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 (60) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +3 -1
  3. package/src/entries.ts +5 -1
  4. package/src/index.ts +2 -1
  5. package/src/tests/locale_completeness.test.ts +2 -3
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/lorenz-attractor/i18n/es.ts +12 -4
  8. package/src/tool/lorenz-attractor/lorenz-attractor.css +56 -25
  9. package/src/tool/radioactive-decay/bibliography.astro +15 -0
  10. package/src/tool/radioactive-decay/bibliography.ts +17 -0
  11. package/src/tool/radioactive-decay/component.astro +346 -0
  12. package/src/tool/radioactive-decay/entry.ts +26 -0
  13. package/src/tool/radioactive-decay/i18n/de.ts +78 -0
  14. package/src/tool/radioactive-decay/i18n/en.ts +223 -0
  15. package/src/tool/radioactive-decay/i18n/es.ts +106 -0
  16. package/src/tool/radioactive-decay/i18n/fr.ts +78 -0
  17. package/src/tool/radioactive-decay/i18n/id.ts +66 -0
  18. package/src/tool/radioactive-decay/i18n/it.ts +79 -0
  19. package/src/tool/radioactive-decay/i18n/ja.ts +65 -0
  20. package/src/tool/radioactive-decay/i18n/ko.ts +65 -0
  21. package/src/tool/radioactive-decay/i18n/nl.ts +72 -0
  22. package/src/tool/radioactive-decay/i18n/pl.ts +65 -0
  23. package/src/tool/radioactive-decay/i18n/pt.ts +78 -0
  24. package/src/tool/radioactive-decay/i18n/ru.ts +66 -0
  25. package/src/tool/radioactive-decay/i18n/sv.ts +66 -0
  26. package/src/tool/radioactive-decay/i18n/tr.ts +66 -0
  27. package/src/tool/radioactive-decay/i18n/zh.ts +65 -0
  28. package/src/tool/radioactive-decay/index.ts +12 -0
  29. package/src/tool/radioactive-decay/logic.test.ts +20 -0
  30. package/src/tool/radioactive-decay/logic.ts +120 -0
  31. package/src/tool/radioactive-decay/radioactive-decay-half-life-calculator.css +435 -0
  32. package/src/tool/radioactive-decay/seo.astro +16 -0
  33. package/src/tool/stellar-habitability-zone/bibliography.astro +14 -0
  34. package/src/tool/stellar-habitability-zone/bibliography.ts +12 -0
  35. package/src/tool/stellar-habitability-zone/component.astro +123 -0
  36. package/src/tool/stellar-habitability-zone/dom-updater.ts +94 -0
  37. package/src/tool/stellar-habitability-zone/entry.ts +26 -0
  38. package/src/tool/stellar-habitability-zone/i18n/de.ts +189 -0
  39. package/src/tool/stellar-habitability-zone/i18n/en.ts +189 -0
  40. package/src/tool/stellar-habitability-zone/i18n/es.ts +189 -0
  41. package/src/tool/stellar-habitability-zone/i18n/fr.ts +189 -0
  42. package/src/tool/stellar-habitability-zone/i18n/id.ts +189 -0
  43. package/src/tool/stellar-habitability-zone/i18n/it.ts +189 -0
  44. package/src/tool/stellar-habitability-zone/i18n/ja.ts +189 -0
  45. package/src/tool/stellar-habitability-zone/i18n/ko.ts +189 -0
  46. package/src/tool/stellar-habitability-zone/i18n/nl.ts +189 -0
  47. package/src/tool/stellar-habitability-zone/i18n/pl.ts +189 -0
  48. package/src/tool/stellar-habitability-zone/i18n/pt.ts +189 -0
  49. package/src/tool/stellar-habitability-zone/i18n/ru.ts +189 -0
  50. package/src/tool/stellar-habitability-zone/i18n/sv.ts +189 -0
  51. package/src/tool/stellar-habitability-zone/i18n/tr.ts +189 -0
  52. package/src/tool/stellar-habitability-zone/i18n/zh.ts +189 -0
  53. package/src/tool/stellar-habitability-zone/index.ts +11 -0
  54. package/src/tool/stellar-habitability-zone/interaction.ts +45 -0
  55. package/src/tool/stellar-habitability-zone/logic/StellarHabitabilityEngine.ts +158 -0
  56. package/src/tool/stellar-habitability-zone/renderer.ts +241 -0
  57. package/src/tool/stellar-habitability-zone/script.ts +273 -0
  58. package/src/tool/stellar-habitability-zone/seo.astro +15 -0
  59. package/src/tool/stellar-habitability-zone/stellar-habitability-zone.css +375 -0
  60. package/src/tools.ts +4 -1
@@ -0,0 +1,273 @@
1
+ import { StellarHabitabilityEngine, STELLAR_PRESETS } from './logic/StellarHabitabilityEngine';
2
+ import { StellarHabitabilityRenderer } from './renderer';
3
+ import type { RenderState } from './renderer';
4
+ import { updateValTexts, updateResults, updateSliderFill } from './dom-updater';
5
+ import { setupCanvasInteraction } from './interaction';
6
+
7
+ const STORAGE_KEY = 'stellar-habitability-zone:config';
8
+ const KEYS = ['temp', 'lum', 'mass', 'rad', 'dist', 'albedo', 'green'];
9
+
10
+ function parseVal(val: string, fallback: number): number {
11
+ const parsed = parseFloat(val);
12
+ return isNaN(parsed) ? fallback : parsed;
13
+ }
14
+
15
+ export function initStellarSimulator() {
16
+ new StellarSimulator();
17
+ }
18
+
19
+ class StellarSimulator {
20
+ private curDistanceAu = 1.0; private curRunawayLimit = 0.95; private curMaxLimit = 1.67;
21
+ private curTemp = 5778; private curLuminosity = 1.0; private curMass = 1.0;
22
+ private prevStatus = 'habitable'; private isImperial = false;
23
+
24
+ private tempInput!: HTMLInputElement; private lumInput!: HTMLInputElement; private massInput!: HTMLInputElement;
25
+ private radInput!: HTMLInputElement; private distInput!: HTMLInputElement; private albedoInput!: HTMLInputElement;
26
+ private greenInput!: HTMLInputElement;
27
+
28
+ private tempVal!: HTMLElement | null; private lumVal!: HTMLElement | null; private massVal!: HTMLElement | null;
29
+ private radVal!: HTMLElement | null; private distVal!: HTMLElement | null; private albedoVal!: HTMLElement | null;
30
+ private greenVal!: HTMLElement | null;
31
+
32
+ private eqTempResult!: HTMLElement | null; private surfTempResult!: HTMLElement | null; private fluxResult!: HTMLElement | null;
33
+ private periodResult!: HTMLElement | null; private velocityResult!: HTMLElement | null; private innerResult!: HTMLElement | null;
34
+ private outerResult!: HTMLElement | null;
35
+
36
+ private statusBox!: HTMLElement | null; private statusTitle!: HTMLElement | null; private unitToggleBtn!: HTMLElement | null;
37
+
38
+ private canvas!: HTMLCanvasElement;
39
+ private canvasContainer!: HTMLElement;
40
+ private presetBtns!: NodeListOf<Element>;
41
+
42
+ private engine!: StellarHabitabilityEngine;
43
+ private renderer!: StellarHabitabilityRenderer;
44
+
45
+ constructor() {
46
+ const root = document.getElementById('hz-simulator-root');
47
+ if (!root) return;
48
+
49
+ this.getElements();
50
+ this.engine = new StellarHabitabilityEngine();
51
+ this.renderer = new StellarHabitabilityRenderer(this.canvas, this.canvasContainer);
52
+
53
+ this.bindEvents();
54
+ this.loadFromStorage();
55
+ this.update();
56
+ this.animate();
57
+ }
58
+
59
+ private getElements() {
60
+ const getEl = <T extends HTMLElement>(id: string) => document.getElementById(id) as T;
61
+ this.tempInput = getEl<HTMLInputElement>('hz-temperature');
62
+ this.lumInput = getEl<HTMLInputElement>('hz-luminosity');
63
+ this.massInput = getEl<HTMLInputElement>('hz-mass');
64
+ this.radInput = getEl<HTMLInputElement>('hz-radius');
65
+ this.distInput = getEl<HTMLInputElement>('hz-distance');
66
+ this.albedoInput = getEl<HTMLInputElement>('hz-albedo');
67
+ this.greenInput = getEl<HTMLInputElement>('hz-greenhouse');
68
+
69
+ this.tempVal = getEl('hz-temperature-val');
70
+ this.lumVal = getEl('hz-luminosity-val');
71
+ this.massVal = getEl('hz-mass-val');
72
+ this.radVal = getEl('hz-radius-val');
73
+ this.distVal = getEl('hz-distance-val');
74
+ this.albedoVal = getEl('hz-albedo-val');
75
+ this.greenVal = getEl('hz-greenhouse-val');
76
+
77
+ this.eqTempResult = getEl('hz-eq-temp');
78
+ this.surfTempResult = getEl('hz-surf-temp');
79
+ this.fluxResult = getEl('hz-stellar-flux');
80
+ this.periodResult = getEl('hz-orbit-period');
81
+ this.velocityResult = getEl('hz-orbit-velocity');
82
+ this.innerResult = getEl('hz-inner-limit');
83
+ this.outerResult = getEl('hz-outer-limit');
84
+
85
+ this.statusBox = getEl('hz-status-box');
86
+ this.statusTitle = getEl('hz-status-title');
87
+ this.unitToggleBtn = getEl('hz-unit-toggle');
88
+
89
+ this.canvas = getEl<HTMLCanvasElement>('hz-orbit-canvas');
90
+ this.canvasContainer = getEl('hz-canvas-placeholder');
91
+ this.presetBtns = document.querySelectorAll('.hz-preset-btn:not(#hz-unit-toggle)');
92
+ }
93
+
94
+ private bindEvents() {
95
+ setupCanvasInteraction({
96
+ canvasContainer: this.canvasContainer,
97
+ distInput: this.distInput,
98
+ presetBtns: this.presetBtns,
99
+ getCurDistanceAu: () => this.curDistanceAu,
100
+ getCurMaxLimit: () => this.curMaxLimit,
101
+ getCurRunawayLimit: () => this.curRunawayLimit,
102
+ onUpdate: () => this.update()
103
+ });
104
+
105
+ [this.tempInput, this.lumInput, this.massInput, this.radInput, this.distInput].forEach(inp => {
106
+ inp.addEventListener('input', () => {
107
+ this.presetBtns.forEach(b => b.classList.remove('active'));
108
+ this.update();
109
+ });
110
+ });
111
+
112
+ [this.albedoInput, this.greenInput].forEach(inp => inp.addEventListener('input', () => this.update()));
113
+ this.presetBtns.forEach(btn => btn.addEventListener('click', () => this.applyPreset(btn)));
114
+
115
+ if (this.unitToggleBtn) {
116
+ this.unitToggleBtn.addEventListener('click', () => {
117
+ this.isImperial = !this.isImperial;
118
+ this.update();
119
+ });
120
+ }
121
+ }
122
+
123
+ private applyPreset(btn: Element) {
124
+ this.presetBtns.forEach(b => b.classList.remove('active'));
125
+ btn.classList.add('active');
126
+ const type = btn.getAttribute('data-type');
127
+ const preset = STELLAR_PRESETS.find(p => p.type === type);
128
+ if (preset) {
129
+ this.tempInput.value = preset.temperature.toString();
130
+ this.lumInput.value = Math.log10(preset.luminosity).toString();
131
+ this.massInput.value = preset.mass.toString();
132
+ this.radInput.value = preset.radius.toString();
133
+
134
+ const hzLimits = this.engine.calculateLimits(preset.luminosity, preset.temperature);
135
+ this.distInput.value = Math.log10((hzLimits.runawayGreenhouse + hzLimits.maximumGreenhouse) / 2).toString();
136
+ this.update();
137
+ }
138
+ }
139
+
140
+ private getSelectedPresetType(): string | null {
141
+ const activeBtn = document.querySelector('.hz-preset-btn.active:not(#hz-unit-toggle)');
142
+ return activeBtn ? activeBtn.getAttribute('data-type') : null;
143
+ }
144
+
145
+ private saveToStorage() {
146
+ try {
147
+ localStorage.setItem(STORAGE_KEY, JSON.stringify({
148
+ temp: this.tempInput.value, lum: this.lumInput.value, mass: this.massInput.value, rad: this.radInput.value,
149
+ dist: this.distInput.value, albedo: this.albedoInput.value, green: this.greenInput.value,
150
+ preset: this.getSelectedPresetType(), imperial: this.isImperial,
151
+ }));
152
+ } catch {}
153
+ }
154
+
155
+ private loadFromStorage() {
156
+ try {
157
+ const stored = localStorage.getItem(STORAGE_KEY);
158
+ if (!stored) return;
159
+ const config = JSON.parse(stored);
160
+ const inputs: Record<string, HTMLInputElement> = {
161
+ temp: this.tempInput, lum: this.lumInput, mass: this.massInput, rad: this.radInput,
162
+ dist: this.distInput, albedo: this.albedoInput, green: this.greenInput
163
+ };
164
+ KEYS.forEach(key => {
165
+ const input = inputs[key];
166
+ if (input && config[key] !== undefined) input.value = config[key];
167
+ });
168
+ if (config.imperial !== undefined) this.isImperial = config.imperial;
169
+ this.presetBtns.forEach(b => {
170
+ const isPresetMatch = b.getAttribute('data-type') === config.preset;
171
+ b.classList.toggle('active', isPresetMatch);
172
+ });
173
+ } catch {}
174
+ }
175
+
176
+ private update() {
177
+ const temp = parseVal(this.tempInput.value, 5778);
178
+ const luminosity = Math.pow(10, parseVal(this.lumInput.value, 0));
179
+ const mass = parseVal(this.massInput.value, 1.0);
180
+ const radius = parseVal(this.radInput.value, 1.0);
181
+ const distanceAu = Math.pow(10, parseVal(this.distInput.value, 0));
182
+ const albedo = parseVal(this.albedoInput.value, 0.3);
183
+ const greenhouse = parseVal(this.greenInput.value, 33);
184
+
185
+ if (this.unitToggleBtn) {
186
+ this.unitToggleBtn.textContent = this.isImperial ? 'Units: Imperial' : 'Units: Scientific';
187
+ }
188
+
189
+ updateValTexts({
190
+ temp, luminosity, mass, radius, distanceAu, albedo, greenhouse, isImperial: this.isImperial,
191
+ tempVal: this.tempVal, lumVal: this.lumVal, massVal: this.massVal, radVal: this.radVal, distVal: this.distVal, albedoVal: this.albedoVal, greenVal: this.greenVal
192
+ });
193
+ [this.tempInput, this.lumInput, this.massInput, this.radInput, this.distInput, this.albedoInput, this.greenInput].forEach(updateSliderFill);
194
+
195
+ const result = this.engine.simulate({
196
+ luminosity, temperature: temp, mass, distanceAu, albedo, greenhouseDeltaK: greenhouse,
197
+ });
198
+
199
+ updateResults({
200
+ result, isImperial: this.isImperial, eqTempResult: this.eqTempResult, surfTempResult: this.surfTempResult,
201
+ fluxResult: this.fluxResult, periodResult: this.periodResult, velocityResult: this.velocityResult, innerResult: this.innerResult, outerResult: this.outerResult
202
+ });
203
+
204
+ this.checkStatusFlash(result.status);
205
+ this.updateStatusDisplay(result.status);
206
+ this.saveToStorage();
207
+ }
208
+
209
+ private checkStatusFlash(status: string) {
210
+ if (status !== this.prevStatus) {
211
+ if (this.eqTempResult && this.surfTempResult) {
212
+ this.eqTempResult.classList.remove('flash-cold', 'flash-hot');
213
+ this.surfTempResult.classList.remove('flash-cold', 'flash-hot');
214
+ void this.eqTempResult.offsetWidth;
215
+ void this.surfTempResult.offsetWidth;
216
+ const flashClass = status === 'too-cold' ? 'flash-cold' : 'flash-hot';
217
+ if (status !== 'habitable') {
218
+ this.eqTempResult.classList.add(flashClass);
219
+ this.surfTempResult.classList.add(flashClass);
220
+ }
221
+ }
222
+ this.prevStatus = status;
223
+ }
224
+ }
225
+
226
+ private updateStatusDisplay(status: string) {
227
+ if (this.statusBox && this.statusTitle) {
228
+ this.statusBox.className = `hz-status-box ${status}`;
229
+ let text = 'HABITABLE (Liquid water possible)';
230
+ if (status === 'too-hot') {
231
+ text = 'TOO HOT (Water vaporizes)';
232
+ } else if (status === 'too-cold') {
233
+ text = 'TOO COLD (Water freezes)';
234
+ }
235
+ this.statusTitle.textContent = text;
236
+ }
237
+ }
238
+
239
+ private animate() {
240
+ const targetDist = Math.pow(10, parseVal(this.distInput.value, 0));
241
+ const targetTemp = parseVal(this.tempInput.value, 5778);
242
+ const targetLuminosity = Math.pow(10, parseVal(this.lumInput.value, 0));
243
+ const targetMass = parseVal(this.massInput.value, 1.0);
244
+ const hzLimits = this.engine.calculateLimits(targetLuminosity, targetTemp);
245
+
246
+ this.curDistanceAu += (targetDist - this.curDistanceAu) * 0.12;
247
+ this.curRunawayLimit += (hzLimits.runawayGreenhouse - this.curRunawayLimit) * 0.12;
248
+ this.curMaxLimit += (hzLimits.maximumGreenhouse - this.curMaxLimit) * 0.12;
249
+ this.curTemp += (targetTemp - this.curTemp) * 0.12;
250
+ this.curLuminosity += (targetLuminosity - this.curLuminosity) * 0.12;
251
+ this.curMass += (targetMass - this.curMass) * 0.12;
252
+
253
+ const size = this.renderer.resize();
254
+ const canvasRect = this.canvas.getBoundingClientRect();
255
+ const containerRect = this.canvasContainer.getBoundingClientRect();
256
+ const cx = (containerRect.left - canvasRect.left) + containerRect.width / 2;
257
+ const cy = (containerRect.top - canvasRect.top) + containerRect.height / 2;
258
+ const maxDist = Math.max(this.curDistanceAu * 1.3, this.curMaxLimit * 1.25);
259
+ const scale = (Math.min(containerRect.width, containerRect.height) * 0.4) / maxDist;
260
+
261
+ this.renderer.draw({
262
+ cx, cy,
263
+ runawayGreenhousePx: this.curRunawayLimit * scale,
264
+ maximumGreenhousePx: this.curMaxLimit * scale,
265
+ planetDistPx: this.curDistanceAu * scale,
266
+ isTooHot: this.curDistanceAu < this.curRunawayLimit,
267
+ isTooCold: this.curDistanceAu > this.curMaxLimit,
268
+ curMass: this.curMass, curLuminosity: this.curLuminosity, curTemp: this.curTemp, curDistanceAu: this.curDistanceAu,
269
+ }, size.w, size.h);
270
+ requestAnimationFrame(() => this.animate());
271
+ }
272
+ }
273
+ export { type RenderState };
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { stellarHabitabilityZone } 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 stellarHabitabilityZone.i18n[locale]?.();
12
+ if (!content) return null;
13
+ ---
14
+
15
+ {content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,375 @@
1
+ :root {
2
+ --hz-card-bg: #fafaf9;
3
+ --hz-border: rgba(0, 0, 0, 0.08);
4
+ --hz-text: #1c1917;
5
+ --hz-text-muted: #78716c;
6
+ --hz-input-border: rgba(0, 0, 0, 0.1);
7
+ --hz-canvas-bg: #fafaf9;
8
+ --hz-highlight: #10b981;
9
+ --hz-number-color: #0c0a09;
10
+ --hz-slider-active: rgba(28, 25, 23, 0.3);
11
+ --hz-slider-track: rgba(28, 25, 23, 0.08);
12
+ }
13
+
14
+ .theme-dark {
15
+ --hz-card-bg: #0c0c0e;
16
+ --hz-border: rgba(255, 255, 255, 0.08);
17
+ --hz-text: #f4f4f5;
18
+ --hz-text-muted: #a1a1aa;
19
+ --hz-input-border: rgba(255, 255, 255, 0.1);
20
+ --hz-canvas-bg: #0c0c0e;
21
+ --hz-highlight: #34d399;
22
+ --hz-number-color: #fafafa;
23
+ --hz-slider-active: rgba(244, 244, 245, 0.35);
24
+ --hz-slider-track: rgba(244, 244, 245, 0.1);
25
+ }
26
+
27
+ .hz-calculator-root {
28
+ display: flex;
29
+ flex-direction: column;
30
+ gap: 2.5rem;
31
+ padding: 1.5rem;
32
+ background: var(--hz-card-bg);
33
+ color: var(--hz-text);
34
+ border: 1px solid var(--hz-border);
35
+ border-radius: 12px;
36
+ max-width: 1000px;
37
+ margin: 2rem auto;
38
+ }
39
+
40
+ @media (min-width: 769px) {
41
+ .hz-calculator-root {
42
+ display: grid;
43
+ grid-template-columns: 1fr;
44
+ grid-template-areas:
45
+ "presets"
46
+ "controls"
47
+ "viewport";
48
+ gap: 3rem;
49
+ padding: 3rem;
50
+ }
51
+ }
52
+
53
+ .hz-presets-container {
54
+ grid-area: presets;
55
+ width: 100%;
56
+ }
57
+
58
+ .hz-presets {
59
+ display: flex;
60
+ flex-wrap: wrap;
61
+ gap: 0.75rem 1.75rem;
62
+ border-bottom: 1px solid var(--hz-border);
63
+ padding-bottom: 0.75rem;
64
+ }
65
+
66
+ .hz-preset-btn {
67
+ background: transparent;
68
+ border: none;
69
+ border-bottom: 1px solid transparent;
70
+ color: var(--hz-text);
71
+ font-weight: 600;
72
+ font-size: 13px;
73
+ text-transform: uppercase;
74
+ letter-spacing: 0.1em;
75
+ cursor: pointer;
76
+ transition: opacity 0.2s, border-color 0.2s;
77
+ padding: 6px 0;
78
+ opacity: 0.45;
79
+ white-space: nowrap;
80
+ }
81
+
82
+ .hz-preset-btn:hover {
83
+ opacity: 0.8;
84
+ }
85
+
86
+ .hz-preset-btn.active {
87
+ opacity: 1;
88
+ border-bottom-color: var(--hz-text);
89
+ border-bottom-width: 1.5px;
90
+ padding-bottom: 5.5px;
91
+ }
92
+
93
+ .hz-controls-section {
94
+ grid-area: controls;
95
+ display: flex;
96
+ flex-direction: column;
97
+ gap: 1.5rem;
98
+ }
99
+
100
+ @media (min-width: 769px) {
101
+ .hz-controls-section {
102
+ display: grid;
103
+ grid-template-columns: 1fr 1fr;
104
+ gap: 1.75rem 3.5rem;
105
+ }
106
+ }
107
+
108
+ .hz-input-group {
109
+ display: grid;
110
+ grid-template-columns: 200px 1fr 90px;
111
+ align-items: center;
112
+ gap: 1.5rem;
113
+ }
114
+
115
+ @media (max-width: 600px) {
116
+ .hz-input-group {
117
+ grid-template-columns: 1fr;
118
+ gap: 0.5rem;
119
+ }
120
+ }
121
+
122
+ .hz-input-group label {
123
+ font-weight: 600;
124
+ font-size: 13px;
125
+ text-transform: uppercase;
126
+ letter-spacing: 0.1em;
127
+ color: var(--hz-text-muted);
128
+ white-space: nowrap;
129
+ }
130
+
131
+ .hz-input-val {
132
+ font-weight: 600;
133
+ font-size: 13px;
134
+ color: var(--hz-text);
135
+ text-align: right;
136
+ opacity: 0.8;
137
+ white-space: nowrap;
138
+ width: 90px;
139
+ }
140
+
141
+ .hz-slider {
142
+ -webkit-appearance: none;
143
+ appearance: none;
144
+ width: 100%;
145
+ height: 2px;
146
+ background: var(--hz-slider-track);
147
+ outline: none;
148
+ cursor: pointer;
149
+ margin: 0;
150
+ padding: 12px 0;
151
+ background-clip: content-box;
152
+ }
153
+
154
+ .hz-slider::-webkit-slider-thumb {
155
+ -webkit-appearance: none;
156
+ appearance: none;
157
+ width: 10px;
158
+ height: 10px;
159
+ border-radius: 50%;
160
+ border: 1px solid var(--hz-text);
161
+ background: var(--hz-card-bg);
162
+ cursor: pointer;
163
+ margin-top: -4px;
164
+ }
165
+
166
+ .hz-slider::-moz-range-thumb {
167
+ width: 8px;
168
+ height: 8px;
169
+ border-radius: 50%;
170
+ border: 1px solid var(--hz-text);
171
+ background: var(--hz-card-bg);
172
+ cursor: pointer;
173
+ }
174
+
175
+ .hz-slider::-webkit-slider-runnable-track {
176
+ width: 100%;
177
+ height: 2px;
178
+ }
179
+
180
+ .hz-viewport-container {
181
+ grid-area: viewport;
182
+ background: #050507;
183
+ border: 1px solid rgba(255, 255, 255, 0.08);
184
+ border-radius: 12px;
185
+ padding: 1.5rem;
186
+ display: flex;
187
+ flex-direction: column;
188
+ gap: 2rem;
189
+ position: relative;
190
+ overflow: hidden;
191
+ }
192
+
193
+ @media (min-width: 769px) {
194
+ .hz-viewport-container {
195
+ display: grid;
196
+ grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
197
+ gap: 3.5rem;
198
+ padding: 3rem;
199
+ }
200
+ }
201
+
202
+ #hz-orbit-canvas {
203
+ position: absolute;
204
+ top: 0;
205
+ left: 0;
206
+ width: 100%;
207
+ height: 100%;
208
+ z-index: 0;
209
+ pointer-events: none;
210
+ }
211
+
212
+ .hz-canvas-section {
213
+ display: flex;
214
+ flex-direction: column;
215
+ gap: 1.5rem;
216
+ position: relative;
217
+ z-index: 1;
218
+ pointer-events: none;
219
+ }
220
+
221
+ .hz-results-section {
222
+ display: flex;
223
+ flex-direction: column;
224
+ gap: 2rem;
225
+ position: relative;
226
+ z-index: 1;
227
+ background: rgba(255, 255, 255, 0.9);
228
+ backdrop-filter: blur(10px);
229
+ -webkit-backdrop-filter: blur(10px);
230
+ border: 1px solid rgba(0, 0, 0, 0.06);
231
+ border-radius: 12px;
232
+ padding: 2rem;
233
+ }
234
+
235
+ .theme-dark .hz-results-section {
236
+ background: rgba(255, 255, 255, 0.03);
237
+ border: 1px solid rgba(255, 255, 255, 0.08);
238
+ }
239
+
240
+ @media (min-width: 769px) {
241
+ .hz-results-section {
242
+ padding: 2.5rem;
243
+ }
244
+ }
245
+
246
+ .hz-result-grid {
247
+ display: flex;
248
+ flex-direction: column;
249
+ gap: 1.5rem;
250
+ }
251
+
252
+ .hz-result-block {
253
+ display: flex;
254
+ flex-direction: column;
255
+ }
256
+
257
+ .hz-result-label {
258
+ font-weight: 600;
259
+ font-size: 11px;
260
+ text-transform: uppercase;
261
+ letter-spacing: 0.1em;
262
+ color: var(--hz-text-muted);
263
+ margin-bottom: 0.15rem;
264
+ }
265
+
266
+ .hz-result-number {
267
+ font-weight: 300;
268
+ font-size: 2.2rem;
269
+ line-height: 1.0;
270
+ color: var(--hz-number-color);
271
+ letter-spacing: -0.02em;
272
+ white-space: nowrap;
273
+ }
274
+
275
+ .hz-result-number.flash-cold {
276
+ animation: flashColdAnim 0.8s cubic-bezier(0.25, 1, 0.5, 1);
277
+ }
278
+
279
+ .hz-result-number.flash-hot {
280
+ animation: flashHotAnim 0.8s cubic-bezier(0.25, 1, 0.5, 1);
281
+ }
282
+
283
+ @keyframes flashColdAnim {
284
+ 0% {
285
+ color: #3b82f6;
286
+ }
287
+
288
+ 100% {
289
+ color: var(--hz-number-color);
290
+ }
291
+ }
292
+
293
+ @keyframes flashHotAnim {
294
+ 0% {
295
+ color: #ef4444;
296
+ }
297
+
298
+ 100% {
299
+ color: var(--hz-number-color);
300
+ }
301
+ }
302
+
303
+ .hz-status-box {
304
+ margin-top: 0.75rem;
305
+ border-left: 2px solid transparent;
306
+ padding: 0.5rem 0 0.5rem 1.25rem;
307
+ background: rgba(0, 0, 0, 0.02);
308
+ backdrop-filter: blur(8px);
309
+ -webkit-backdrop-filter: blur(8px);
310
+ transition: border-color 0.3s;
311
+ }
312
+
313
+ .theme-dark .hz-status-box {
314
+ background: rgba(255, 255, 255, 0.02);
315
+ }
316
+
317
+ .hz-status-box.too-hot {
318
+ border-left-color: #ef4444;
319
+ }
320
+
321
+ .hz-status-box.too-cold {
322
+ border-left-color: #3b82f6;
323
+ }
324
+
325
+ .hz-status-box.habitable {
326
+ border-left-color: #10b981;
327
+ }
328
+
329
+ .hz-status-title {
330
+ font-weight: 400;
331
+ font-size: 1.25rem;
332
+ margin-bottom: 0.25rem;
333
+ }
334
+
335
+ .hz-status-box.too-hot .hz-status-title {
336
+ color: #ef4444;
337
+ }
338
+
339
+ .hz-status-box.too-cold .hz-status-title {
340
+ color: #3b82f6;
341
+ }
342
+
343
+ .hz-status-box.habitable .hz-status-title {
344
+ color: #10b981;
345
+ }
346
+
347
+ .hz-status-desc {
348
+ font-size: 12px;
349
+ color: var(--hz-text-muted);
350
+ line-height: 1.5;
351
+ }
352
+
353
+ .hz-canvas-section h3 {
354
+ font-weight: 600;
355
+ font-size: 11px;
356
+ text-transform: uppercase;
357
+ letter-spacing: 0.1em;
358
+ color: var(--hz-text-muted);
359
+ margin: 0;
360
+ }
361
+
362
+ .hz-canvas-container {
363
+ position: relative;
364
+ width: 100%;
365
+ height: 380px;
366
+ background: transparent;
367
+ border-radius: 8px;
368
+ pointer-events: auto;
369
+ }
370
+
371
+ @media (min-width: 769px) {
372
+ .hz-canvas-container {
373
+ height: 540px;
374
+ }
375
+ }
package/src/tools.ts CHANGED
@@ -8,6 +8,8 @@ import { CELLULAR_RENEWAL_TOOL } from './tool/cellular-renewal/index';
8
8
  import { COSMIC_INFLATION_TOOL } from './tool/cosmic-inflation/index';
9
9
  import { TEMPERATURE_TIMELINE_TOOL } from './tool/temperature-timeline/index';
10
10
  import { LORENZ_ATTRACTOR_TOOL } from './tool/lorenz-attractor/index';
11
+ import { STELLAR_HABITABILITY_ZONE_TOOL } from './tool/stellar-habitability-zone/index';
12
+ import { RADIOACTIVE_DECAY_TOOL } from './tool/radioactive-decay/index';
11
13
 
12
14
  export const ALL_TOOLS: ToolDefinition[] = [
13
15
  COLONY_COUNTER_TOOL,
@@ -18,6 +20,7 @@ export const ALL_TOOLS: ToolDefinition[] = [
18
20
  COSMIC_INFLATION_TOOL,
19
21
  TEMPERATURE_TIMELINE_TOOL,
20
22
  LORENZ_ATTRACTOR_TOOL,
23
+ STELLAR_HABITABILITY_ZONE_TOOL,
24
+ RADIOACTIVE_DECAY_TOOL,
21
25
  ];
22
26
 
23
-