@jjlmoya/utils-tabletop 1.32.0 → 1.33.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 (52) hide show
  1. package/package.json +2 -2
  2. package/src/category/i18n/de.ts +1 -1
  3. package/src/category/i18n/es.ts +1 -1
  4. package/src/category/i18n/fr.ts +1 -1
  5. package/src/category/i18n/id.ts +1 -1
  6. package/src/category/i18n/it.ts +1 -1
  7. package/src/category/i18n/nl.ts +1 -1
  8. package/src/category/i18n/pl.ts +1 -1
  9. package/src/category/i18n/pt.ts +1 -1
  10. package/src/category/i18n/ru.ts +1 -1
  11. package/src/category/i18n/sv.ts +1 -1
  12. package/src/category/i18n/tr.ts +1 -1
  13. package/src/category/index.ts +25 -25
  14. package/src/components/PreviewNavSidebar.astro +1 -1
  15. package/src/components/PreviewToolbar.astro +1 -1
  16. package/src/entries.ts +43 -43
  17. package/src/env.d.ts +1 -1
  18. package/src/index.ts +24 -24
  19. package/src/layouts/PreviewLayout.astro +93 -93
  20. package/src/pages/[locale]/[slug].astro +143 -143
  21. package/src/pages/index.astro +1 -1
  22. package/src/tests/bibliography_wellformed_export.test.ts +46 -0
  23. package/src/tests/category_seo_quality.test.ts +74 -0
  24. package/src/tests/i18n_coverage.test.ts +36 -36
  25. package/src/tests/mocks/astro_mock.js +1 -1
  26. package/src/tests/no_em_dash.test.ts +47 -47
  27. package/src/tests/no_en_dash.test.ts +70 -70
  28. package/src/tests/seo_length.test.ts +3 -3
  29. package/src/tests/seo_parity.test.ts +2 -2
  30. package/src/tests/seo_wellformed_export.test.ts +65 -0
  31. package/src/tests/spanish_leakage.test.ts +175 -175
  32. package/src/tool/dungeon-map-generator/component.astro +135 -135
  33. package/src/tool/dungeon-map-generator/controller.ts +243 -243
  34. package/src/tool/dungeon-map-generator/dom-views.ts +144 -144
  35. package/src/tool/dungeon-map-generator/i18n/de.ts +172 -172
  36. package/src/tool/dungeon-map-generator/logic.ts +281 -281
  37. package/src/tool/dungeon-map-generator/random-dungeon-map-generator.css +692 -692
  38. package/src/tool/dungeon-map-generator/ui.ts +54 -54
  39. package/src/tool/fantasy-runes-translator/logic.ts +1 -1
  40. package/src/tool/first-player-selector/i18n/tr.ts +1 -1
  41. package/src/tool/investigation-board/i18n/ru.ts +209 -209
  42. package/src/tool/investigation-board/i18n/zh.ts +209 -209
  43. package/src/tool/lunar-tide-tracker/client.ts +264 -264
  44. package/src/tool/rpg-initiative-tracker/i18n/de.ts +3 -3
  45. package/src/tool/rpg-initiative-tracker/i18n/es.ts +1 -1
  46. package/src/tool/rpg-initiative-tracker/i18n/pl.ts +2 -2
  47. package/src/tool/rpg-initiative-tracker/i18n/ru.ts +1 -1
  48. package/src/tool/score-tracker/i18n/pt.ts +1 -1
  49. package/src/tool/score-tracker/i18n/sv.ts +1 -1
  50. package/src/tool/score-tracker/i18n/tr.ts +1 -1
  51. package/src/tool/score-tracker/i18n/zh.ts +1 -1
  52. package/src/tools.ts +25 -25
@@ -1,264 +1,264 @@
1
- import type { Moon, MoonPhaseType } from './types';
2
- import { calculateMoonPhase, calculateCombinedTide, generateForecast } from './logic';
3
-
4
- const tideLabelMap: Record<string, string> = {
5
- neap: 'Neap Tide', low: 'Low Tide', normal: 'Normal Tide', high: 'High Tide', spring: 'Spring Tide',
6
- };
7
-
8
- let moons: Moon[] = [{ id: '1', name: 'Solaria', period: 28, offset: 0, color: '#f59e0b' }];
9
- let currentDay = 0;
10
-
11
- const phaseUiKeys: Record<MoonPhaseType, string> = {
12
- new: 'newMoon', waxing_crescent: 'waxingCrescent', first_quarter: 'firstQuarter',
13
- waxing_gibbous: 'waxingGibbous', full: 'fullMoon', waning_gibbous: 'waningGibbous',
14
- last_quarter: 'lastQuarter', waning_crescent: 'waningCrescent',
15
- };
16
-
17
- const phaseDefault: Record<MoonPhaseType, string> = {
18
- new: 'New Moon', waxing_crescent: 'Waxing Crescent', first_quarter: 'First Quarter',
19
- waxing_gibbous: 'Waxing Gibbous', full: 'Full Moon', waning_gibbous: 'Waning Gibbous',
20
- last_quarter: 'Last Quarter', waning_crescent: 'Waning Crescent',
21
- };
22
-
23
- const presets: Record<string, () => Moon[]> = {
24
- 'lunar-preset-single': () => [{ id: '1', name: 'Solaria', period: 28, offset: 0, color: '#f59e0b' }],
25
- 'lunar-preset-double': () => [
26
- { id: '1', name: 'Solaria', period: 20, offset: 0, color: '#a78bfa' },
27
- { id: '2', name: 'Lunaria', period: 30, offset: 10, color: '#3b82f6' },
28
- ],
29
- 'lunar-preset-triple': () => [
30
- { id: '1', name: 'Solaria', period: 15, offset: 0, color: '#f87171' },
31
- { id: '2', name: 'Lunaria', period: 25, offset: 5, color: '#3b82f6' },
32
- { id: '3', name: 'Astraria', period: 40, offset: 15, color: '#34d399' },
33
- ],
34
- };
35
-
36
- function getCssVar(name: string, fallback: string): string {
37
- return ((window as Record<string, unknown>)['get' + 'Computed' + 'Style'] as (el: Element) => CSSStyleDeclaration)(document.documentElement).getPropertyValue(name).trim() || fallback;
38
- }
39
-
40
- function getUI(): Record<string, string> {
41
- const el = document.querySelector('.ltt-root');
42
- if (!el) return {};
43
- try { return JSON.parse(el.getAttribute('data-ui') || '{}'); } catch { return {}; }
44
- }
45
-
46
- function getPhaseLabel(phase: MoonPhaseType, ui: Record<string, string>): string {
47
- return ui[phaseUiKeys[phase]] || phaseDefault[phase];
48
- }
49
-
50
- function spawnParticle(x: number, y: number, text: string): void {
51
- const p = document.createElement('div');
52
- p.style.cssText = `position:fixed;left:${x}px;top:${y}px;transform:translate(-50%,-50%);pointer-events:none;color:#a78bfa;font-weight:700;font-size:0.9rem;text-shadow:0 0 8px rgba(139,92,246,0.7);animation:ltt-particle 0.8s ease-out forwards;z-index:9999`;
53
- p.innerText = text;
54
- document.body.appendChild(p);
55
- setTimeout(() => p.remove(), 800);
56
- }
57
-
58
- function getSweep(isOuter: boolean, isWaxing: boolean): number {
59
- if (isOuter && !isWaxing) return 1;
60
- if (!isOuter && isWaxing) return 1;
61
- return 0;
62
- }
63
-
64
- function buildPhasePath(pct: number, cx: number, cy: number, r: number): string {
65
- if (pct <= 0.01 || pct >= 0.99) return '';
66
- const isWaxing = pct < 0.5;
67
- const base = isWaxing ? pct : pct - 0.5;
68
- const rx = r * Math.abs(1 - 4 * base);
69
- const isOuter = pct < 0.25 || pct >= 0.75;
70
- const sweep = getSweep(isOuter, isWaxing);
71
- return `M ${cx},${cy - r} A ${r},${r} 0 0,1 ${cx},${cy + r} A ${rx},${r} 0 0,${sweep} ${cx},${cy - r}`;
72
- }
73
-
74
- function renderMoonOrbs(): void {
75
- const el = document.getElementById('ltt-moon-orbs');
76
- if (!el) return;
77
- const ui = getUI();
78
- const orbRadius = 100;
79
- const moonR = 8;
80
- const angleStep = (2 * Math.PI) / moons.length;
81
- const moonBg = getCssVar('--ltt-moon-bg', '#0f172a');
82
- const moonStroke = getCssVar('--ltt-moon-stroke', 'rgba(255,255,255,0.1)');
83
- el.innerHTML = moons.map((moon, i) => {
84
- const angle = i * angleStep - Math.PI / 2;
85
- const mx = 120 + orbRadius * Math.cos(angle);
86
- const my = 120 + orbRadius * Math.sin(angle);
87
- const info = calculateMoonPhase(currentDay, moon.period, moon.offset);
88
- const phasePath = buildPhasePath(info.percentage, mx, my, moonR);
89
- const pathSvg = phasePath ? `<path d="${phasePath}" fill="${moon.color}" style="filter:drop-shadow(0 0 3px ${moon.color})" />` : '';
90
- return `<g><circle cx="${mx}" cy="${my}" r="${moonR}" fill="${moonBg}" stroke="${moonStroke}" stroke-width="0.5" />${pathSvg}<circle cx="${mx}" cy="${my}" r="${moonR}" fill="none" stroke="${moon.color}" stroke-width="0.5" opacity="0.4" /><title>${moon.name}: ${getPhaseLabel(info.phase, ui)}</title></g>`;
91
- }).join('');
92
- }
93
-
94
- function updateCoreGlow(intensity: number): void {
95
- const core = document.getElementById('ltt-core');
96
- if (!core) return;
97
- let glow = '2';
98
- if (intensity > 75) glow = '8';
99
- else if (intensity > 40) glow = '4';
100
- core.style.filter = `drop-shadow(0 0 ${glow}px rgba(139,92,246,${intensity / 100}))`;
101
- }
102
-
103
- function updateCorePhase(): void {
104
- const pathEl = document.getElementById('ltt-phase-path');
105
- if (!pathEl) return;
106
- if (moons.length === 1) {
107
- const info = calculateMoonPhase(currentDay, moons[0].period, moons[0].offset);
108
- const d = buildPhasePath(info.percentage, 120, 120, 60);
109
- pathEl.setAttribute('d', d || '');
110
- pathEl.setAttribute('fill', d ? moons[0].color : 'transparent');
111
- if (d) pathEl.setAttribute('style', `filter:drop-shadow(0 0 8px ${moons[0].color})`);
112
- } else {
113
- pathEl.setAttribute('d', '');
114
- pathEl.setAttribute('fill', 'transparent');
115
- }
116
- }
117
-
118
- function renderPortalCore(): void {
119
- const tide = calculateCombinedTide(currentDay, moons);
120
- updateCoreGlow(tide.intensity);
121
- updateCorePhase();
122
- }
123
-
124
- function renderTideStatus(): void {
125
- const tide = calculateCombinedTide(currentDay, moons);
126
- const bar = document.getElementById('ltt-tide-bar');
127
- const pct = document.getElementById('ltt-tide-pct');
128
- const lbl = document.getElementById('ltt-tide-label');
129
- if (bar) bar.style.width = `${tide.intensity}%`;
130
- if (pct) pct.innerText = `${tide.intensity}%`;
131
- if (lbl) lbl.innerText = tideLabelMap[tide.labelKey] || tide.labelKey;
132
- }
133
-
134
- function renderMoonsList(): void {
135
- const container = document.getElementById('ltt-moons-list');
136
- if (!container) return;
137
- const ui = getUI();
138
- const moonBg = ((window as Record<string, unknown>)['get' + 'Computed' + 'Style'] as (el: Element) => CSSStyleDeclaration)(document.documentElement).getPropertyValue('--ltt-moon-bg').trim() || '#0f172a';
139
- container.innerHTML = moons.map((moon) => {
140
- const info = calculateMoonPhase(currentDay, moon.period, moon.offset);
141
- const label = getPhaseLabel(info.phase, ui);
142
- const path = buildPhasePath(info.percentage, 12, 12, 11);
143
- return `
144
- <div class="ltt-moon-row" data-id="${moon.id}">
145
- <svg class="ltt-moon-vis-mini" viewBox="0 0 24 24">
146
- <circle cx="12" cy="12" r="11" fill="${moonBg}" stroke="${getCssVar('--ltt-moon-stroke', 'rgba(255,255,255,0.1)')}" stroke-width="0.5"/>
147
- ${path ? `<path d="${path}" fill="${moon.color}" style="filter:drop-shadow(0 0 2px ${moon.color})"/>` : ''}
148
- </svg>
149
- <input type="text" class="ltt-moon-input ltt-moon-name" value="${moon.name}" placeholder="${ui.moonName || 'Name'}" />
150
- <input type="number" class="ltt-moon-num ltt-moon-period" value="${moon.period}" min="1" title="${ui.orbitalPeriod || 'Period'}" />
151
- <input type="number" class="ltt-moon-num ltt-moon-offset" value="${moon.offset}" title="${ui.startingOffset || 'Offset'}" />
152
- <input type="color" class="ltt-color-swatch" value="${moon.color}" />
153
- <button type="button" class="ltt-remove-btn" data-id="${moon.id}" title="${ui.removeMoon || 'Remove'}">x</button>
154
- </div>
155
- <div class="ltt-phase-tag"><span class="ltt-phase-dot" style="background:${moon.color};"></span>${moon.name}: ${label} (${Math.round(info.percentage * 100)}%)</div>
156
- `;
157
- }).join('');
158
- bindMoonRowEvents(container);
159
- }
160
-
161
- function bindMoonRowEvents(container: HTMLElement): void {
162
- container.querySelectorAll('.ltt-remove-btn').forEach((btn) => {
163
- btn.addEventListener('click', (e) => {
164
- const id = (e.currentTarget as HTMLElement).getAttribute('data-id');
165
- if (moons.length > 1) { moons = moons.filter((m) => m.id !== id); updateAll(); }
166
- });
167
- });
168
- container.querySelectorAll('.ltt-moon-row').forEach((row) => {
169
- const id = row.getAttribute('data-id');
170
- const moon = moons.find((m) => m.id === id);
171
- if (!moon) return;
172
- bindSingleMoon(row as HTMLElement, moon);
173
- });
174
- }
175
-
176
- function bindSingleMoon(row: HTMLElement, moon: Moon): void {
177
- row.querySelector('.ltt-moon-name')?.addEventListener('input', (e) => {
178
- moon.name = (e.target as HTMLInputElement).value;
179
- renderMoonsList(); renderMoonOrbs();
180
- });
181
- row.querySelector('.ltt-moon-period')?.addEventListener('input', (e) => {
182
- const v = parseInt((e.target as HTMLInputElement).value);
183
- moon.period = isNaN(v) || v < 1 ? 1 : v; renderAll();
184
- });
185
- row.querySelector('.ltt-moon-offset')?.addEventListener('input', (e) => {
186
- const v = parseInt((e.target as HTMLInputElement).value);
187
- moon.offset = isNaN(v) ? 0 : v; renderAll();
188
- });
189
- row.querySelector('.ltt-color-swatch')?.addEventListener('input', (e) => {
190
- moon.color = (e.target as HTMLInputElement).value; renderAll();
191
- });
192
- }
193
-
194
- function renderForecast(): void {
195
- const container = document.getElementById('ltt-forecast');
196
- if (!container) return;
197
- const ui = getUI();
198
- const forecast = generateForecast(currentDay, 10, moons);
199
- container.innerHTML = forecast.map((item) => {
200
- const isPeak = item.tideLevel >= 80;
201
- const dots = item.moons.map((m) => {
202
- const src = moons.find((mn) => mn.id === m.moonId);
203
- const col = src ? src.color : '#fff';
204
- const op = m.phase === 'new' ? '0.15' : '1';
205
- return `<div class="ltt-fc-dot" style="background:${col};opacity:${op};" title="${src?.name}: ${m.phase}"></div>`;
206
- }).join('');
207
- return `<div class="ltt-fc-cell${isPeak ? ' ltt-fc-peak' : ''}"><div class="ltt-fc-day">${ui.dayLabel || 'D'}${item.day}</div><div class="ltt-fc-dots">${dots}</div><div class="ltt-fc-pct">${item.tideLevel}%</div></div>`;
208
- }).join('');
209
- }
210
-
211
- function renderAll(): void {
212
- renderPortalCore();
213
- renderMoonOrbs();
214
- renderTideStatus();
215
- renderMoonsList();
216
- renderForecast();
217
- }
218
-
219
- function updateAll(): void {
220
- const dayVal = document.getElementById('ltt-day-val');
221
- const slider = document.getElementById('ltt-slider') as HTMLInputElement;
222
- if (dayVal) dayVal.innerText = currentDay.toString();
223
- if (slider) slider.value = currentDay.toString();
224
- renderAll();
225
- }
226
-
227
- function bindStepButtons(): void {
228
- const steps = [
229
- { id: 'lunar-btn-prev7', delta: -7 }, { id: 'lunar-btn-prev1', delta: -1 },
230
- { id: 'lunar-btn-next1', delta: 1 }, { id: 'lunar-btn-next7', delta: 7 },
231
- ];
232
- steps.forEach(({ id, delta }) => {
233
- document.getElementById(id)?.addEventListener('click', (e) => {
234
- currentDay = Math.max(0, currentDay + delta);
235
- spawnParticle(e.clientX, e.clientY, `${delta > 0 ? '+' : ''}${delta}`);
236
- updateAll();
237
- });
238
- });
239
- document.getElementById('ltt-slider')?.addEventListener('input', (e) => {
240
- currentDay = parseInt((e.target as HTMLInputElement).value) || 0;
241
- updateAll();
242
- });
243
- document.getElementById('lunar-btn-reset')?.addEventListener('click', () => { currentDay = 0; updateAll(); });
244
- }
245
-
246
- function bindPresetsAndMoon(): void {
247
- Object.entries(presets).forEach(([id, fn]) => {
248
- const btn = document.getElementById(id);
249
- btn?.addEventListener('click', () => {
250
- moons = fn();
251
- document.querySelectorAll('.ltt-preset-btn').forEach((b) => b.classList.remove('active'));
252
- btn.classList.add('active');
253
- updateAll();
254
- });
255
- });
256
- document.getElementById('ltt-add-moon')?.addEventListener('click', () => {
257
- if (moons.length >= 3) return;
258
- const colors = ['#a78bfa', '#60a5fa', '#34d399'];
259
- moons.push({ id: Date.now().toString(), name: `Moon ${moons.length + 1}`, period: 30, offset: 0, color: colors[moons.length % 3] });
260
- updateAll();
261
- });
262
- }
263
-
264
- document.addEventListener('DOMContentLoaded', () => { bindStepButtons(); bindPresetsAndMoon(); updateAll(); });
1
+ import type { Moon, MoonPhaseType } from './types';
2
+ import { calculateMoonPhase, calculateCombinedTide, generateForecast } from './logic';
3
+
4
+ const tideLabelMap: Record<string, string> = {
5
+ neap: 'Neap Tide', low: 'Low Tide', normal: 'Normal Tide', high: 'High Tide', spring: 'Spring Tide',
6
+ };
7
+
8
+ let moons: Moon[] = [{ id: '1', name: 'Solaria', period: 28, offset: 0, color: '#f59e0b' }];
9
+ let currentDay = 0;
10
+
11
+ const phaseUiKeys: Record<MoonPhaseType, string> = {
12
+ new: 'newMoon', waxing_crescent: 'waxingCrescent', first_quarter: 'firstQuarter',
13
+ waxing_gibbous: 'waxingGibbous', full: 'fullMoon', waning_gibbous: 'waningGibbous',
14
+ last_quarter: 'lastQuarter', waning_crescent: 'waningCrescent',
15
+ };
16
+
17
+ const phaseDefault: Record<MoonPhaseType, string> = {
18
+ new: 'New Moon', waxing_crescent: 'Waxing Crescent', first_quarter: 'First Quarter',
19
+ waxing_gibbous: 'Waxing Gibbous', full: 'Full Moon', waning_gibbous: 'Waning Gibbous',
20
+ last_quarter: 'Last Quarter', waning_crescent: 'Waning Crescent',
21
+ };
22
+
23
+ const presets: Record<string, () => Moon[]> = {
24
+ 'lunar-preset-single': () => [{ id: '1', name: 'Solaria', period: 28, offset: 0, color: '#f59e0b' }],
25
+ 'lunar-preset-double': () => [
26
+ { id: '1', name: 'Solaria', period: 20, offset: 0, color: '#a78bfa' },
27
+ { id: '2', name: 'Lunaria', period: 30, offset: 10, color: '#3b82f6' },
28
+ ],
29
+ 'lunar-preset-triple': () => [
30
+ { id: '1', name: 'Solaria', period: 15, offset: 0, color: '#f87171' },
31
+ { id: '2', name: 'Lunaria', period: 25, offset: 5, color: '#3b82f6' },
32
+ { id: '3', name: 'Astraria', period: 40, offset: 15, color: '#34d399' },
33
+ ],
34
+ };
35
+
36
+ function getCssVar(name: string, fallback: string): string {
37
+ return ((window as Record<string, unknown>)['get' + 'Computed' + 'Style'] as (el: Element) => CSSStyleDeclaration)(document.documentElement).getPropertyValue(name).trim() || fallback;
38
+ }
39
+
40
+ function getUI(): Record<string, string> {
41
+ const el = document.querySelector('.ltt-root');
42
+ if (!el) return {};
43
+ try { return JSON.parse(el.getAttribute('data-ui') || '{}'); } catch { return {}; }
44
+ }
45
+
46
+ function getPhaseLabel(phase: MoonPhaseType, ui: Record<string, string>): string {
47
+ return ui[phaseUiKeys[phase]] || phaseDefault[phase];
48
+ }
49
+
50
+ function spawnParticle(x: number, y: number, text: string): void {
51
+ const p = document.createElement('div');
52
+ p.style.cssText = `position:fixed;left:${x}px;top:${y}px;transform:translate(-50%,-50%);pointer-events:none;color:#a78bfa;font-weight:700;font-size:0.9rem;text-shadow:0 0 8px rgba(139,92,246,0.7);animation:ltt-particle 0.8s ease-out forwards;z-index:9999`;
53
+ p.innerText = text;
54
+ document.body.appendChild(p);
55
+ setTimeout(() => p.remove(), 800);
56
+ }
57
+
58
+ function getSweep(isOuter: boolean, isWaxing: boolean): number {
59
+ if (isOuter && !isWaxing) return 1;
60
+ if (!isOuter && isWaxing) return 1;
61
+ return 0;
62
+ }
63
+
64
+ function buildPhasePath(pct: number, cx: number, cy: number, r: number): string {
65
+ if (pct <= 0.01 || pct >= 0.99) return '';
66
+ const isWaxing = pct < 0.5;
67
+ const base = isWaxing ? pct : pct - 0.5;
68
+ const rx = r * Math.abs(1 - 4 * base);
69
+ const isOuter = pct < 0.25 || pct >= 0.75;
70
+ const sweep = getSweep(isOuter, isWaxing);
71
+ return `M ${cx},${cy - r} A ${r},${r} 0 0,1 ${cx},${cy + r} A ${rx},${r} 0 0,${sweep} ${cx},${cy - r}`;
72
+ }
73
+
74
+ function renderMoonOrbs(): void {
75
+ const el = document.getElementById('ltt-moon-orbs');
76
+ if (!el) return;
77
+ const ui = getUI();
78
+ const orbRadius = 100;
79
+ const moonR = 8;
80
+ const angleStep = (2 * Math.PI) / moons.length;
81
+ const moonBg = getCssVar('--ltt-moon-bg', '#0f172a');
82
+ const moonStroke = getCssVar('--ltt-moon-stroke', 'rgba(255,255,255,0.1)');
83
+ el.innerHTML = moons.map((moon, i) => {
84
+ const angle = i * angleStep - Math.PI / 2;
85
+ const mx = 120 + orbRadius * Math.cos(angle);
86
+ const my = 120 + orbRadius * Math.sin(angle);
87
+ const info = calculateMoonPhase(currentDay, moon.period, moon.offset);
88
+ const phasePath = buildPhasePath(info.percentage, mx, my, moonR);
89
+ const pathSvg = phasePath ? `<path d="${phasePath}" fill="${moon.color}" style="filter:drop-shadow(0 0 3px ${moon.color})" />` : '';
90
+ return `<g><circle cx="${mx}" cy="${my}" r="${moonR}" fill="${moonBg}" stroke="${moonStroke}" stroke-width="0.5" />${pathSvg}<circle cx="${mx}" cy="${my}" r="${moonR}" fill="none" stroke="${moon.color}" stroke-width="0.5" opacity="0.4" /><title>${moon.name}: ${getPhaseLabel(info.phase, ui)}</title></g>`;
91
+ }).join('');
92
+ }
93
+
94
+ function updateCoreGlow(intensity: number): void {
95
+ const core = document.getElementById('ltt-core');
96
+ if (!core) return;
97
+ let glow = '2';
98
+ if (intensity > 75) glow = '8';
99
+ else if (intensity > 40) glow = '4';
100
+ core.style.filter = `drop-shadow(0 0 ${glow}px rgba(139,92,246,${intensity / 100}))`;
101
+ }
102
+
103
+ function updateCorePhase(): void {
104
+ const pathEl = document.getElementById('ltt-phase-path');
105
+ if (!pathEl) return;
106
+ if (moons.length === 1) {
107
+ const info = calculateMoonPhase(currentDay, moons[0].period, moons[0].offset);
108
+ const d = buildPhasePath(info.percentage, 120, 120, 60);
109
+ pathEl.setAttribute('d', d || '');
110
+ pathEl.setAttribute('fill', d ? moons[0].color : 'transparent');
111
+ if (d) pathEl.setAttribute('style', `filter:drop-shadow(0 0 8px ${moons[0].color})`);
112
+ } else {
113
+ pathEl.setAttribute('d', '');
114
+ pathEl.setAttribute('fill', 'transparent');
115
+ }
116
+ }
117
+
118
+ function renderPortalCore(): void {
119
+ const tide = calculateCombinedTide(currentDay, moons);
120
+ updateCoreGlow(tide.intensity);
121
+ updateCorePhase();
122
+ }
123
+
124
+ function renderTideStatus(): void {
125
+ const tide = calculateCombinedTide(currentDay, moons);
126
+ const bar = document.getElementById('ltt-tide-bar');
127
+ const pct = document.getElementById('ltt-tide-pct');
128
+ const lbl = document.getElementById('ltt-tide-label');
129
+ if (bar) bar.style.width = `${tide.intensity}%`;
130
+ if (pct) pct.innerText = `${tide.intensity}%`;
131
+ if (lbl) lbl.innerText = tideLabelMap[tide.labelKey] || tide.labelKey;
132
+ }
133
+
134
+ function renderMoonsList(): void {
135
+ const container = document.getElementById('ltt-moons-list');
136
+ if (!container) return;
137
+ const ui = getUI();
138
+ const moonBg = ((window as Record<string, unknown>)['get' + 'Computed' + 'Style'] as (el: Element) => CSSStyleDeclaration)(document.documentElement).getPropertyValue('--ltt-moon-bg').trim() || '#0f172a';
139
+ container.innerHTML = moons.map((moon) => {
140
+ const info = calculateMoonPhase(currentDay, moon.period, moon.offset);
141
+ const label = getPhaseLabel(info.phase, ui);
142
+ const path = buildPhasePath(info.percentage, 12, 12, 11);
143
+ return `
144
+ <div class="ltt-moon-row" data-id="${moon.id}">
145
+ <svg class="ltt-moon-vis-mini" viewBox="0 0 24 24">
146
+ <circle cx="12" cy="12" r="11" fill="${moonBg}" stroke="${getCssVar('--ltt-moon-stroke', 'rgba(255,255,255,0.1)')}" stroke-width="0.5"/>
147
+ ${path ? `<path d="${path}" fill="${moon.color}" style="filter:drop-shadow(0 0 2px ${moon.color})"/>` : ''}
148
+ </svg>
149
+ <input type="text" class="ltt-moon-input ltt-moon-name" value="${moon.name}" placeholder="${ui.moonName || 'Name'}" />
150
+ <input type="number" class="ltt-moon-num ltt-moon-period" value="${moon.period}" min="1" title="${ui.orbitalPeriod || 'Period'}" />
151
+ <input type="number" class="ltt-moon-num ltt-moon-offset" value="${moon.offset}" title="${ui.startingOffset || 'Offset'}" />
152
+ <input type="color" class="ltt-color-swatch" value="${moon.color}" />
153
+ <button type="button" class="ltt-remove-btn" data-id="${moon.id}" title="${ui.removeMoon || 'Remove'}">x</button>
154
+ </div>
155
+ <div class="ltt-phase-tag"><span class="ltt-phase-dot" style="background:${moon.color};"></span>${moon.name}: ${label} (${Math.round(info.percentage * 100)}%)</div>
156
+ `;
157
+ }).join('');
158
+ bindMoonRowEvents(container);
159
+ }
160
+
161
+ function bindMoonRowEvents(container: HTMLElement): void {
162
+ container.querySelectorAll('.ltt-remove-btn').forEach((btn) => {
163
+ btn.addEventListener('click', (e) => {
164
+ const id = (e.currentTarget as HTMLElement).getAttribute('data-id');
165
+ if (moons.length > 1) { moons = moons.filter((m) => m.id !== id); updateAll(); }
166
+ });
167
+ });
168
+ container.querySelectorAll('.ltt-moon-row').forEach((row) => {
169
+ const id = row.getAttribute('data-id');
170
+ const moon = moons.find((m) => m.id === id);
171
+ if (!moon) return;
172
+ bindSingleMoon(row as HTMLElement, moon);
173
+ });
174
+ }
175
+
176
+ function bindSingleMoon(row: HTMLElement, moon: Moon): void {
177
+ row.querySelector('.ltt-moon-name')?.addEventListener('input', (e) => {
178
+ moon.name = (e.target as HTMLInputElement).value;
179
+ renderMoonsList(); renderMoonOrbs();
180
+ });
181
+ row.querySelector('.ltt-moon-period')?.addEventListener('input', (e) => {
182
+ const v = parseInt((e.target as HTMLInputElement).value);
183
+ moon.period = isNaN(v) || v < 1 ? 1 : v; renderAll();
184
+ });
185
+ row.querySelector('.ltt-moon-offset')?.addEventListener('input', (e) => {
186
+ const v = parseInt((e.target as HTMLInputElement).value);
187
+ moon.offset = isNaN(v) ? 0 : v; renderAll();
188
+ });
189
+ row.querySelector('.ltt-color-swatch')?.addEventListener('input', (e) => {
190
+ moon.color = (e.target as HTMLInputElement).value; renderAll();
191
+ });
192
+ }
193
+
194
+ function renderForecast(): void {
195
+ const container = document.getElementById('ltt-forecast');
196
+ if (!container) return;
197
+ const ui = getUI();
198
+ const forecast = generateForecast(currentDay, 10, moons);
199
+ container.innerHTML = forecast.map((item) => {
200
+ const isPeak = item.tideLevel >= 80;
201
+ const dots = item.moons.map((m) => {
202
+ const src = moons.find((mn) => mn.id === m.moonId);
203
+ const col = src ? src.color : '#fff';
204
+ const op = m.phase === 'new' ? '0.15' : '1';
205
+ return `<div class="ltt-fc-dot" style="background:${col};opacity:${op};" title="${src?.name}: ${m.phase}"></div>`;
206
+ }).join('');
207
+ return `<div class="ltt-fc-cell${isPeak ? ' ltt-fc-peak' : ''}"><div class="ltt-fc-day">${ui.dayLabel || 'D'}${item.day}</div><div class="ltt-fc-dots">${dots}</div><div class="ltt-fc-pct">${item.tideLevel}%</div></div>`;
208
+ }).join('');
209
+ }
210
+
211
+ function renderAll(): void {
212
+ renderPortalCore();
213
+ renderMoonOrbs();
214
+ renderTideStatus();
215
+ renderMoonsList();
216
+ renderForecast();
217
+ }
218
+
219
+ function updateAll(): void {
220
+ const dayVal = document.getElementById('ltt-day-val');
221
+ const slider = document.getElementById('ltt-slider') as HTMLInputElement;
222
+ if (dayVal) dayVal.innerText = currentDay.toString();
223
+ if (slider) slider.value = currentDay.toString();
224
+ renderAll();
225
+ }
226
+
227
+ function bindStepButtons(): void {
228
+ const steps = [
229
+ { id: 'lunar-btn-prev7', delta: -7 }, { id: 'lunar-btn-prev1', delta: -1 },
230
+ { id: 'lunar-btn-next1', delta: 1 }, { id: 'lunar-btn-next7', delta: 7 },
231
+ ];
232
+ steps.forEach(({ id, delta }) => {
233
+ document.getElementById(id)?.addEventListener('click', (e) => {
234
+ currentDay = Math.max(0, currentDay + delta);
235
+ spawnParticle(e.clientX, e.clientY, `${delta > 0 ? '+' : ''}${delta}`);
236
+ updateAll();
237
+ });
238
+ });
239
+ document.getElementById('ltt-slider')?.addEventListener('input', (e) => {
240
+ currentDay = parseInt((e.target as HTMLInputElement).value) || 0;
241
+ updateAll();
242
+ });
243
+ document.getElementById('lunar-btn-reset')?.addEventListener('click', () => { currentDay = 0; updateAll(); });
244
+ }
245
+
246
+ function bindPresetsAndMoon(): void {
247
+ Object.entries(presets).forEach(([id, fn]) => {
248
+ const btn = document.getElementById(id);
249
+ btn?.addEventListener('click', () => {
250
+ moons = fn();
251
+ document.querySelectorAll('.ltt-preset-btn').forEach((b) => b.classList.remove('active'));
252
+ btn.classList.add('active');
253
+ updateAll();
254
+ });
255
+ });
256
+ document.getElementById('ltt-add-moon')?.addEventListener('click', () => {
257
+ if (moons.length >= 3) return;
258
+ const colors = ['#a78bfa', '#60a5fa', '#34d399'];
259
+ moons.push({ id: Date.now().toString(), name: `Moon ${moons.length + 1}`, period: 30, offset: 0, color: colors[moons.length % 3] });
260
+ updateAll();
261
+ });
262
+ }
263
+
264
+ document.addEventListener('DOMContentLoaded', () => { bindStepButtons(); bindPresetsAndMoon(); updateAll(); });
@@ -261,6 +261,6 @@ export const content: ToolLocaleContent<InitiativeTrackerUI> = {
261
261
  },
262
262
  ],
263
263
  };
264
-
265
-
266
-
264
+
265
+
266
+
@@ -261,4 +261,4 @@ export const content: ToolLocaleContent<InitiativeTrackerUI> = {
261
261
  },
262
262
  ],
263
263
  };
264
-
264
+
@@ -260,5 +260,5 @@ export const content: ToolLocaleContent<InitiativeTrackerUI> = {
260
260
  },
261
261
  ],
262
262
  };
263
-
264
-
263
+
264
+
@@ -261,4 +261,4 @@ export const content: ToolLocaleContent<InitiativeTrackerUI> = {
261
261
  },
262
262
  ],
263
263
  };
264
-
264
+
@@ -275,4 +275,4 @@ export const content: ToolLocaleContent<ScoreTrackerUI> = {
275
275
  ],
276
276
  },
277
277
  ],
278
- };
278
+ };
@@ -275,4 +275,4 @@ export const content: ToolLocaleContent<ScoreTrackerUI> = {
275
275
  ],
276
276
  },
277
277
  ],
278
- };
278
+ };
@@ -275,4 +275,4 @@ export const content: ToolLocaleContent<ScoreTrackerUI> = {
275
275
  ],
276
276
  },
277
277
  ],
278
- };
278
+ };
@@ -275,4 +275,4 @@ export const content: ToolLocaleContent<ScoreTrackerUI> = {
275
275
  ],
276
276
  },
277
277
  ],
278
- };
278
+ };