@jjlmoya/utils-drones 1.14.0 → 1.16.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 (93) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +4 -1
  4. package/src/index.ts +1 -0
  5. package/src/pages/[locale]/[slug].astro +30 -14
  6. package/src/tests/locale_completeness.test.ts +4 -22
  7. package/src/tests/shared-test-helpers.ts +56 -0
  8. package/src/tests/tool_exports.test.ts +34 -0
  9. package/src/tests/tool_validation.test.ts +2 -2
  10. package/src/tool/antenna-length-calculator/bibliography.astro +3 -11
  11. package/src/tool/antenna-length-calculator/bibliography.ts +6 -0
  12. package/src/tool/antenna-length-calculator/i18n/de.ts +2 -8
  13. package/src/tool/antenna-length-calculator/i18n/en.ts +2 -8
  14. package/src/tool/antenna-length-calculator/i18n/es.ts +2 -8
  15. package/src/tool/antenna-length-calculator/i18n/fr.ts +2 -8
  16. package/src/tool/antenna-length-calculator/i18n/id.ts +2 -8
  17. package/src/tool/antenna-length-calculator/i18n/it.ts +2 -8
  18. package/src/tool/antenna-length-calculator/i18n/ja.ts +2 -8
  19. package/src/tool/antenna-length-calculator/i18n/ko.ts +2 -8
  20. package/src/tool/antenna-length-calculator/i18n/nl.ts +2 -8
  21. package/src/tool/antenna-length-calculator/i18n/pl.ts +2 -8
  22. package/src/tool/antenna-length-calculator/i18n/pt.ts +2 -8
  23. package/src/tool/antenna-length-calculator/i18n/ru.ts +2 -8
  24. package/src/tool/antenna-length-calculator/i18n/sv.ts +2 -8
  25. package/src/tool/antenna-length-calculator/i18n/tr.ts +2 -8
  26. package/src/tool/antenna-length-calculator/i18n/zh.ts +2 -8
  27. package/src/tool/antenna-length-calculator/seo.astro +2 -26
  28. package/src/tool/drone-flight-time/bibliography.astro +3 -11
  29. package/src/tool/drone-flight-time/bibliography.ts +7 -0
  30. package/src/tool/drone-flight-time/i18n/de.ts +2 -9
  31. package/src/tool/drone-flight-time/i18n/en.ts +2 -9
  32. package/src/tool/drone-flight-time/i18n/es.ts +2 -9
  33. package/src/tool/drone-flight-time/i18n/fr.ts +2 -9
  34. package/src/tool/drone-flight-time/i18n/id.ts +2 -9
  35. package/src/tool/drone-flight-time/i18n/it.ts +2 -9
  36. package/src/tool/drone-flight-time/i18n/ja.ts +2 -9
  37. package/src/tool/drone-flight-time/i18n/ko.ts +2 -9
  38. package/src/tool/drone-flight-time/i18n/nl.ts +2 -9
  39. package/src/tool/drone-flight-time/i18n/pl.ts +2 -9
  40. package/src/tool/drone-flight-time/i18n/pt.ts +2 -9
  41. package/src/tool/drone-flight-time/i18n/ru.ts +2 -9
  42. package/src/tool/drone-flight-time/i18n/sv.ts +2 -9
  43. package/src/tool/drone-flight-time/i18n/tr.ts +2 -9
  44. package/src/tool/drone-flight-time/i18n/zh.ts +2 -9
  45. package/src/tool/drone-flight-time/seo.astro +2 -18
  46. package/src/tool/drone-power-analyzer/bibliography.astro +6 -0
  47. package/src/tool/drone-power-analyzer/bibliography.ts +7 -0
  48. package/src/tool/drone-power-analyzer/component.astro +210 -0
  49. package/src/tool/drone-power-analyzer/components/FlightProfile.astro +37 -0
  50. package/src/tool/drone-power-analyzer/components/MotorConfig.astro +46 -0
  51. package/src/tool/drone-power-analyzer/components/PowerGauge.astro +48 -0
  52. package/src/tool/drone-power-analyzer/components/WeightConfig.astro +49 -0
  53. package/src/tool/drone-power-analyzer/drone-power-analyzer.css +535 -0
  54. package/src/tool/drone-power-analyzer/entry.ts +29 -0
  55. package/src/tool/drone-power-analyzer/i18n/de.ts +203 -0
  56. package/src/tool/drone-power-analyzer/i18n/en.ts +203 -0
  57. package/src/tool/drone-power-analyzer/i18n/es.ts +203 -0
  58. package/src/tool/drone-power-analyzer/i18n/fr.ts +203 -0
  59. package/src/tool/drone-power-analyzer/i18n/id.ts +203 -0
  60. package/src/tool/drone-power-analyzer/i18n/it.ts +203 -0
  61. package/src/tool/drone-power-analyzer/i18n/ja.ts +203 -0
  62. package/src/tool/drone-power-analyzer/i18n/ko.ts +203 -0
  63. package/src/tool/drone-power-analyzer/i18n/nl.ts +203 -0
  64. package/src/tool/drone-power-analyzer/i18n/pl.ts +203 -0
  65. package/src/tool/drone-power-analyzer/i18n/pt.ts +203 -0
  66. package/src/tool/drone-power-analyzer/i18n/ru.ts +203 -0
  67. package/src/tool/drone-power-analyzer/i18n/sv.ts +203 -0
  68. package/src/tool/drone-power-analyzer/i18n/tr.ts +203 -0
  69. package/src/tool/drone-power-analyzer/i18n/zh.ts +203 -0
  70. package/src/tool/drone-power-analyzer/index.ts +8 -0
  71. package/src/tool/drone-power-analyzer/seo.astro +15 -0
  72. package/src/tool/gps-coordinates-converter/bibliography.astro +3 -11
  73. package/src/tool/gps-coordinates-converter/bibliography.ts +12 -0
  74. package/src/tool/gps-coordinates-converter/components/GpsMap.astro +3 -2
  75. package/src/tool/gps-coordinates-converter/gps-coordinates-converter.css +196 -188
  76. package/src/tool/gps-coordinates-converter/i18n/de.ts +35 -36
  77. package/src/tool/gps-coordinates-converter/i18n/en.ts +4 -14
  78. package/src/tool/gps-coordinates-converter/i18n/es.ts +4 -14
  79. package/src/tool/gps-coordinates-converter/i18n/fr.ts +4 -14
  80. package/src/tool/gps-coordinates-converter/i18n/id.ts +36 -37
  81. package/src/tool/gps-coordinates-converter/i18n/it.ts +35 -36
  82. package/src/tool/gps-coordinates-converter/i18n/ja.ts +19 -20
  83. package/src/tool/gps-coordinates-converter/i18n/ko.ts +20 -21
  84. package/src/tool/gps-coordinates-converter/i18n/nl.ts +33 -36
  85. package/src/tool/gps-coordinates-converter/i18n/pl.ts +19 -20
  86. package/src/tool/gps-coordinates-converter/i18n/pt.ts +19 -20
  87. package/src/tool/gps-coordinates-converter/i18n/ru.ts +19 -20
  88. package/src/tool/gps-coordinates-converter/i18n/sv.ts +19 -20
  89. package/src/tool/gps-coordinates-converter/i18n/tr.ts +19 -20
  90. package/src/tool/gps-coordinates-converter/i18n/zh.ts +20 -21
  91. package/src/tool/gps-coordinates-converter/seo.astro +2 -26
  92. package/src/tools.ts +3 -0
  93. package/src/types.ts +1 -3
@@ -1,5 +1,6 @@
1
1
  import type { DroneFlightTimeLocaleContent } from '../index';
2
2
  import type { WithContext, SoftwareApplication, FAQPage, HowTo } from 'schema-dts';
3
+ import { bibliography } from '../bibliography';
3
4
 
4
5
  const slug = 'drone-flight-time-calculator';
5
6
  const title = '无人机飞行时间计算器用于评估LiPo LiIon续航';
@@ -81,11 +82,7 @@ export const content: DroneFlightTimeLocaleContent = {
81
82
  slug,
82
83
  title,
83
84
  description,
84
- faqTitle: '常见问题解答',
85
- bibliographyTitle: '参考文献',
86
85
  ui: {
87
- faqTitle: '常见问题解答',
88
- bibliographyTitle: '参考文献',
89
86
  batterySpecs: '电池规格',
90
87
  capacity: '电池容量',
91
88
  voltage: '电压 (S 电芯)',
@@ -229,11 +226,7 @@ export const content: DroneFlightTimeLocaleContent = {
229
226
  },
230
227
  ],
231
228
  faq: faqItems,
232
- bibliography: [
233
- { name: '欧洲航空局 (EASA) 有关于各种类民用级航空守则', url: 'https://www.easa.europa.eu/en/domains/civil-drones' },
234
- { name: 'ArduPilot 全面百科', url: 'https://ardupilot.org/copter/' },
235
- { name: '关于电池系统大科普学术指南网', url: 'https://batteryuniversity.com/' },
236
- ],
229
+ bibliography,
237
230
  howTo: howToSteps,
238
231
  schemas,
239
232
  };
@@ -1,5 +1,5 @@
1
1
  ---
2
- import { SEOArticle, SEOTitle, SEOList, SEOTip } from '@jjlmoya/utils-shared';
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
3
  import { droneFlightTime } from './index';
4
4
  import type { KnownLocale } from '../../types';
5
5
 
@@ -12,20 +12,4 @@ const content = await droneFlightTime.i18n[locale]?.();
12
12
  if (!content) return null;
13
13
  ---
14
14
 
15
- <SEOArticle>
16
- {content.seo.map((section: any) => {
17
- if (section.type === 'title') {
18
- return <SEOTitle title={section.text} level={section.level || 2} />;
19
- }
20
- if (section.type === 'paragraph') {
21
- return <p set:html={section.html} />;
22
- }
23
- if (section.type === 'list') {
24
- return <SEOList items={section.items} />;
25
- }
26
- if (section.type === 'tip') {
27
- return <SEOTip title="Consejos">{section.html}</SEOTip>;
28
- }
29
- return null;
30
- })}
31
- </SEOArticle>
15
+ {content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,6 @@
1
+ ---
2
+ import { Bibliography as BibliographyComponent } from '@jjlmoya/utils-shared';
3
+ import { bibliography } from './bibliography';
4
+ ---
5
+
6
+ <BibliographyComponent links={bibliography} />
@@ -0,0 +1,7 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ { name: 'GetFPV - Understanding Thrust to Weight Ratio', url: 'https://rcdrone.top/blogs/articles/fpv-compute-thrust-to-weight' },
5
+ { name: 'Oscar Liang - Drone Motor Database & Specs', url: 'https://oscarliang.com/table-prop-motor-lipo-weight/' },
6
+ { name: 'ArduPilot - Multirotor Power & Efficiency', url: 'https://ardupilot.org/plane/docs/drivecalculator-guide.html' },
7
+ ];
@@ -0,0 +1,210 @@
1
+ ---
2
+ const { ui } = Astro.props;
3
+ import PowerGauge from "./components/PowerGauge.astro";
4
+ import MotorConfig from "./components/MotorConfig.astro";
5
+ import WeightConfig from "./components/WeightConfig.astro";
6
+ import FlightProfile from "./components/FlightProfile.astro";
7
+ ---
8
+
9
+ <div class="power-analyzer-ui" data-ui={JSON.stringify(ui)}>
10
+ <div class="tech-mega-card">
11
+ <div class="card-grid">
12
+ <div class="config-sidebar">
13
+ <MotorConfig {ui} />
14
+ <WeightConfig {ui} />
15
+ </div>
16
+ <div class="main-display">
17
+ <PowerGauge {ui} />
18
+ <FlightProfile {ui} />
19
+ </div>
20
+ </div>
21
+ </div>
22
+ </div>
23
+
24
+ <script>
25
+ const container = document.querySelector(".power-analyzer-ui") as HTMLElement;
26
+ const uiData = JSON.parse(container.getAttribute("data-ui") || "{}") as Record<string, string>;
27
+
28
+ const motorCountSelect = document.getElementById("motorCount") as HTMLSelectElement;
29
+ const thrustInput = document.getElementById("thrustPerMotor") as HTMLInputElement;
30
+ const auwInput = document.getElementById("auw") as HTMLInputElement;
31
+ const unitToggle = document.getElementById("unitToggle") as HTMLInputElement;
32
+ const ratioDisplay = document.getElementById("ratioDisplay") as HTMLElement;
33
+ const ratioBadge = document.getElementById("ratioBadge") as HTMLElement;
34
+ const totalThrustDisplay = document.getElementById("totalThrust") as HTMLElement;
35
+ const gaugeNeedle = document.getElementById("gaugeNeedle") as unknown as SVGLineElement;
36
+ const profilesContainer = document.getElementById("profilesContainer") as HTMLElement;
37
+ const recommendationText = document.getElementById("recommendation-text") as HTMLElement;
38
+ const unitLabels = document.querySelectorAll(".unit") as NodeListOf<HTMLElement>;
39
+ const batteryChips = document.querySelectorAll(".battery-chip") as NodeListOf<HTMLElement>;
40
+
41
+ let currentUnit: "g" | "lbs" = "g";
42
+ const LBS_TO_G = 453.592;
43
+
44
+ const batteryWeightToKey: Record<string, string> = {
45
+ "182": "batteryName_tattu4s",
46
+ "231": "batteryName_tattu6s",
47
+ "118": "batteryName_gnb4s",
48
+ "205": "batteryName_gnb6s",
49
+ };
50
+
51
+ const addedBatteries: Set<string> = new Set();
52
+
53
+ const badgeMap: Record<number, { text: string; classes: string }> = {
54
+ 0: { text: "badge_unstable", classes: "" },
55
+ 3: { text: "badge_cinematic", classes: "optimal" },
56
+ 5: { text: "badge_sweetSpot", classes: "optimal" },
57
+ 7: { text: "badge_racing", classes: "racing" },
58
+ 8: { text: "badge_extreme", classes: "racing" },
59
+ };
60
+
61
+ const recommendationMap: Record<number, string> = {
62
+ 2: "recommendation_low",
63
+ 4: "recommendation_cinematic",
64
+ 5: "recommendation_freestyle",
65
+ 7: "recommendation_racing",
66
+ 99: "recommendation_extreme",
67
+ };
68
+
69
+ function calculate() {
70
+ const motorCount = parseInt(motorCountSelect.value);
71
+ let thrust = parseFloat(thrustInput.value) || 0;
72
+ let auw = parseFloat(auwInput.value) || 0;
73
+
74
+ if (currentUnit === "lbs") {
75
+ thrust *= LBS_TO_G;
76
+ auw *= LBS_TO_G;
77
+ }
78
+
79
+ const totalThrust = thrust * motorCount;
80
+ const ratio = auw > 0 ? totalThrust / auw : 0;
81
+
82
+ ratioDisplay.textContent = ratio.toFixed(2);
83
+ totalThrustDisplay.textContent = currentUnit === "lbs" ? `${(totalThrust / LBS_TO_G).toFixed(1)} lbs` : `${totalThrust.toFixed(0)} g`;
84
+
85
+ updateGauge(ratio);
86
+ updateProfiles(ratio);
87
+ updateRecommendation(ratio);
88
+ }
89
+
90
+ function updateGauge(ratio: number) {
91
+ const clampedRatio = Math.max(1, Math.min(ratio, 10));
92
+ let angle = 180 + ((clampedRatio - 1) / 9) * 180;
93
+ angle = Math.max(180, Math.min(angle, 360));
94
+ const rad = (angle * Math.PI) / 180;
95
+ const needleLength = 52;
96
+ const x2 = 100 + needleLength * Math.cos(rad);
97
+ const y2 = 70 + needleLength * Math.sin(rad);
98
+ gaugeNeedle.setAttribute("x2", x2.toString());
99
+ gaugeNeedle.setAttribute("y2", y2.toString());
100
+
101
+ ratioBadge.classList.remove("optimal", "racing");
102
+ const badgeThreshold = Object.keys(badgeMap).map(Number).sort((a, b) => b - a).find(t => ratio >= t) || 0;
103
+ const badge = badgeMap[badgeThreshold];
104
+ ratioBadge.textContent = uiData[badge.text] || "";
105
+ if (badge.classes) ratioBadge.classList.add(...badge.classes.split(" "));
106
+ }
107
+
108
+ function updateProfiles(ratio: number) {
109
+ const profiles = [
110
+ { key: "cinematicLabel", suitable: ratio > 2.0 },
111
+ { key: "freestyleLabel", suitable: ratio > 4.0 },
112
+ { key: "racingLabel", suitable: ratio > 5.5 },
113
+ { key: "proRacingLabel", suitable: ratio > 7.5 },
114
+ ];
115
+
116
+ profilesContainer.innerHTML = profiles
117
+ .map(p => `<div class="profile-card ${p.suitable ? "suitable" : "not-suitable"}"><div class="profile-name">${uiData[p.key] || ""}</div></div>`)
118
+ .join("");
119
+ }
120
+
121
+ function updateRecommendation(ratio: number) {
122
+ const threshold = Object.keys(recommendationMap).map(Number).sort((a, b) => a - b).find(t => ratio < t) || 99;
123
+ const key = recommendationMap[threshold];
124
+ const text = (uiData[key] || "").replace("{ratio}", ratio.toFixed(2));
125
+ recommendationText.textContent = text;
126
+ }
127
+
128
+ function toggleUnit() {
129
+ currentUnit = currentUnit === "g" ? "lbs" : "g";
130
+
131
+ const thrustVal = parseFloat(thrustInput.value) || 0;
132
+ const auwVal = parseFloat(auwInput.value) || 0;
133
+
134
+ if (currentUnit === "lbs") {
135
+ thrustInput.value = (thrustVal / LBS_TO_G).toFixed(1);
136
+ auwInput.value = (auwVal / LBS_TO_G).toFixed(1);
137
+ } else {
138
+ thrustInput.value = (thrustVal * LBS_TO_G).toFixed(0);
139
+ auwInput.value = (auwVal * LBS_TO_G).toFixed(0);
140
+ }
141
+
142
+ unitLabels.forEach(label => {
143
+ label.textContent = currentUnit === "g" ? "g" : "lbs";
144
+ });
145
+
146
+ batteryChips.forEach(chip => {
147
+ const weightG = parseFloat(chip.getAttribute("data-battery-weight")!);
148
+ const uiKey = batteryWeightToKey[weightG.toString()];
149
+ const batteryName = uiKey ? (uiData[uiKey] || "") : "";
150
+ chip.textContent = currentUnit === "lbs"
151
+ ? `${batteryName} (+${(weightG / LBS_TO_G).toFixed(1)} lbs)`
152
+ : `${batteryName} (+${weightG}g)`;
153
+ });
154
+
155
+ calculate();
156
+ }
157
+
158
+ motorCountSelect.addEventListener("change", calculate);
159
+ thrustInput.addEventListener("input", calculate);
160
+ auwInput.addEventListener("input", calculate);
161
+ unitToggle.addEventListener("change", toggleUnit);
162
+
163
+ const thrustSlider = document.querySelector('input[type="range"][min="100"][max="5000"]') as HTMLInputElement;
164
+ const auwSlider = document.querySelector('input[type="range"][min="50"][max="5000"]:last-of-type') as HTMLInputElement;
165
+
166
+ if (thrustSlider) {
167
+ thrustSlider.addEventListener("input", () => {
168
+ thrustInput.value = thrustSlider.value;
169
+ calculate();
170
+ });
171
+ thrustInput.addEventListener("input", () => {
172
+ thrustSlider.value = thrustInput.value;
173
+ });
174
+ }
175
+
176
+ if (auwSlider) {
177
+ auwSlider.addEventListener("input", () => {
178
+ auwInput.value = auwSlider.value;
179
+ calculate();
180
+ });
181
+ auwInput.addEventListener("input", () => {
182
+ auwSlider.value = auwInput.value;
183
+ });
184
+ }
185
+
186
+ document.querySelectorAll(".battery-chip").forEach((btn) => {
187
+ btn.addEventListener("click", () => {
188
+ const weightG = parseFloat(btn.getAttribute("data-battery-weight")!);
189
+ const weight = currentUnit === "lbs" ? weightG / LBS_TO_G : weightG;
190
+ const currentVal = parseFloat(auwInput.value) || 0;
191
+ const isAdded = addedBatteries.has(weightG.toString());
192
+
193
+ if (isAdded) {
194
+ auwInput.value = (currentVal - weight).toFixed(currentUnit === "lbs" ? 1 : 0);
195
+ addedBatteries.delete(weightG.toString());
196
+ btn.classList.remove("battery-chip-active");
197
+ } else {
198
+ auwInput.value = (currentVal + weight).toFixed(currentUnit === "lbs" ? 1 : 0);
199
+ addedBatteries.add(weightG.toString());
200
+ btn.classList.add("battery-chip-active");
201
+ auwInput.classList.add("pulse");
202
+ setTimeout(() => auwInput.classList.remove("pulse"), 600);
203
+ }
204
+
205
+ calculate();
206
+ });
207
+ });
208
+
209
+ calculate();
210
+ </script>
@@ -0,0 +1,37 @@
1
+ ---
2
+ interface Props {
3
+ ui: Record<string, string>;
4
+ }
5
+
6
+ const { ui } = Astro.props;
7
+ ---
8
+
9
+ <div class="flight-section">
10
+ <h3 class="flight-title">
11
+ {ui.flightProfiles || "Flight Profile Assessment"}
12
+ </h3>
13
+
14
+ <div id="profilesContainer" class="profiles-grid">
15
+ <div class="profile-card suitable">
16
+ <div class="profile-name">{ui.cinematicLabel || "Cinematic"}</div>
17
+ </div>
18
+ <div class="profile-card suitable">
19
+ <div class="profile-name">{ui.freestyleLabel || "Freestyle"}</div>
20
+ </div>
21
+ <div class="profile-card suitable">
22
+ <div class="profile-name">{ui.racingLabel || "Racing"}</div>
23
+ </div>
24
+ <div class="profile-card suitable">
25
+ <div class="profile-name">{ui.proRacingLabel || "Pro Racing"}</div>
26
+ </div>
27
+ </div>
28
+
29
+ <div class="recommendation-section">
30
+ <p class="recommendation-title">
31
+ {ui.recommendationLabel || "Flying Style Recommendation"}
32
+ </p>
33
+ <p id="recommendation-text" class="recommendation-text">
34
+ With a ratio of 2.00:1, this is ideal for heavy-lift cinematography with smooth, controlled movements.
35
+ </p>
36
+ </div>
37
+ </div>
@@ -0,0 +1,46 @@
1
+ ---
2
+ interface Props {
3
+ ui: Record<string, string>;
4
+ }
5
+
6
+ const { ui } = Astro.props;
7
+ ---
8
+
9
+ <div class="config-section">
10
+ <h3 class="section-title">
11
+ {ui.motorConfiguration || "Motor Configuration"}
12
+ </h3>
13
+
14
+ <div class="input-group">
15
+ <label for="motorCount">{ui.motorCount || "Motor Count"}</label>
16
+ <select id="motorCount">
17
+ <option value="4">{ui.quad || "Quad (4)"}</option>
18
+ <option value="6">{ui.hexa || "Hexa (6)"}</option>
19
+ <option value="8">{ui.octo || "Octo (8)"}</option>
20
+ </select>
21
+ </div>
22
+
23
+ <div class="input-group">
24
+ <label for="thrustPerMotor">{ui.thrustPerMotor || "Thrust per Motor"}</label>
25
+ <div class="input-with-unit">
26
+ <input
27
+ id="thrustPerMotor"
28
+ type="number"
29
+ min="100"
30
+ max="5000"
31
+ step="10"
32
+ value="600"
33
+ />
34
+ <span class="unit">{ui.thrustUnit || "g"}</span>
35
+ </div>
36
+ <input
37
+ type="range"
38
+ min="100"
39
+ max="5000"
40
+ step="10"
41
+ value="600"
42
+ class="thrust-slider"
43
+ onchange="document.getElementById('thrustPerMotor').value = this.value; document.getElementById('thrustPerMotor').dispatchEvent(new Event('input'));"
44
+ />
45
+ </div>
46
+ </div>
@@ -0,0 +1,48 @@
1
+ ---
2
+ interface Props {
3
+ ui: Record<string, string>;
4
+ }
5
+
6
+ const { ui } = Astro.props;
7
+ ---
8
+
9
+ <div class="gauge-container">
10
+ <div class="unit-toggle-global">
11
+ <input id="unitToggle" type="checkbox" class="unit-checkbox" />
12
+ <label for="unitToggle" class="unit-label-global">g / lbs</label>
13
+ </div>
14
+
15
+ <div class="gauge-wrapper">
16
+ <svg viewBox="0 0 200 160" class="gauge-svg">
17
+ <defs>
18
+ <linearGradient id="gaugeGradient" x1="0%" y1="0%" x2="100%" y2="100%">
19
+ <stop offset="0%" stop-color="#ef4444" />
20
+ <stop offset="33%" stop-color="#f97316" />
21
+ <stop offset="66%" stop-color="#22c55e" />
22
+ <stop offset="100%" stop-color="#6366f1" />
23
+ </linearGradient>
24
+ </defs>
25
+
26
+ <path d="M 40 70 A 60 60 0 1 1 160 70" fill="none" stroke="url(#gaugeGradient)" stroke-width="14" stroke-linecap="round" opacity="0.6" />
27
+
28
+ <g id="needleGroup">
29
+ <line id="gaugeNeedle" x1="100" y1="70" x2="160" y2="70" stroke="var(--pa-primary)" stroke-width="3" stroke-linecap="round" />
30
+ </g>
31
+
32
+ <circle cx="100" cy="70" r="8" fill="var(--pa-primary)" filter="drop-shadow(0 2px 4px rgba(0,0,0,0.3))" />
33
+ </svg>
34
+ </div>
35
+
36
+ <div class="ratio-display-main">
37
+ <span id="ratioDisplay" class="ratio-number-large">0.00</span>
38
+ <span class="ratio-label-large">:1</span>
39
+ <span id="ratioBadge" class="ratio-badge">Unstable</span>
40
+ </div>
41
+
42
+ <div class="gauge-info">
43
+ <div class="thrust-display">
44
+ <span class="thrust-label">{ui.totalThrust || "Total Thrust"}</span>
45
+ <span id="totalThrust" class="thrust-value">0g</span>
46
+ </div>
47
+ </div>
48
+ </div>
@@ -0,0 +1,49 @@
1
+ ---
2
+ interface Props {
3
+ ui: Record<string, string>;
4
+ }
5
+
6
+ const { ui } = Astro.props;
7
+ ---
8
+
9
+ <div class="config-section">
10
+ <h3 class="section-title">
11
+ {ui.weightConfiguration || "Weight"}
12
+ </h3>
13
+
14
+ <div class="input-group">
15
+ <label for="auw">{ui.auwLabel || "All-Up Weight (AUW)"}</label>
16
+ <div class="input-with-unit">
17
+ <input
18
+ id="auw"
19
+ type="number"
20
+ min="50"
21
+ max="5000"
22
+ step="10"
23
+ value="800"
24
+ />
25
+ <span class="unit">{ui.weightUnit || "g"}</span>
26
+ </div>
27
+ <input
28
+ type="range"
29
+ min="50"
30
+ max="5000"
31
+ step="10"
32
+ value="800"
33
+ class="auw-slider"
34
+ onchange="document.getElementById('auw').value = this.value; document.getElementById('auw').dispatchEvent(new Event('input'));"
35
+ />
36
+ </div>
37
+
38
+ <div class="battery-presets">
39
+ <p class="battery-title">
40
+ {ui.batteryPresets || "Quick Add"}
41
+ </p>
42
+ <div class="battery-chips">
43
+ <button class="battery-chip" data-battery-weight="182">{ui.presetTattu4s || "Tattu 4S"}</button>
44
+ <button class="battery-chip" data-battery-weight="231">{ui.presetTattu6s || "Tattu 6S"}</button>
45
+ <button class="battery-chip" data-battery-weight="118">{ui.presetGnb4s || "GNB 4S"}</button>
46
+ <button class="battery-chip" data-battery-weight="205">{ui.presetGnb6s || "GNB 6S"}</button>
47
+ </div>
48
+ </div>
49
+ </div>