@jjlmoya/utils-drones 1.30.0 → 1.32.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 (67) hide show
  1. package/package.json +1 -1
  2. package/src/category/i18n/en.ts +1 -1
  3. package/src/category/i18n/fr.ts +1 -1
  4. package/src/category/index.ts +2 -0
  5. package/src/components/PreviewNavSidebar.astro +116 -116
  6. package/src/components/PreviewToolbar.astro +143 -143
  7. package/src/data.ts +1 -1
  8. package/src/entries.ts +4 -0
  9. package/src/env.d.ts +1 -1
  10. package/src/index.ts +1 -1
  11. package/src/layouts/PreviewLayout.astro +118 -118
  12. package/src/pages/[locale].astro +251 -251
  13. package/src/pages/index.astro +4 -4
  14. package/src/tests/faq_count.test.ts +1 -1
  15. package/src/tests/locale_completeness.test.ts +1 -2
  16. package/src/tests/mocks/astro_mock.js +1 -1
  17. package/src/tests/no_h1_in_components.test.ts +1 -1
  18. package/src/tests/tool_validation.test.ts +17 -17
  19. package/src/tool/antenna-length-calculator/bibliography.astro +6 -6
  20. package/src/tool/antenna-length-calculator/component.astro +329 -329
  21. package/src/tool/antenna-length-calculator/seo.astro +15 -15
  22. package/src/tool/drone-battery-c-rating-calculator/i18n/de.ts +192 -192
  23. package/src/tool/drone-battery-c-rating-calculator/i18n/en.ts +192 -192
  24. package/src/tool/drone-battery-c-rating-calculator/i18n/es.ts +192 -192
  25. package/src/tool/drone-battery-c-rating-calculator/i18n/id.ts +192 -192
  26. package/src/tool/drone-battery-c-rating-calculator/i18n/ja.ts +192 -192
  27. package/src/tool/drone-battery-c-rating-calculator/i18n/ko.ts +192 -192
  28. package/src/tool/drone-battery-c-rating-calculator/i18n/nl.ts +192 -192
  29. package/src/tool/drone-battery-c-rating-calculator/i18n/pl.ts +192 -192
  30. package/src/tool/drone-battery-c-rating-calculator/i18n/ru.ts +192 -192
  31. package/src/tool/drone-battery-c-rating-calculator/i18n/sv.ts +192 -192
  32. package/src/tool/drone-battery-c-rating-calculator/i18n/tr.ts +192 -192
  33. package/src/tool/drone-battery-c-rating-calculator/i18n/zh.ts +192 -192
  34. package/src/tool/drone-flight-time/component.astro +187 -187
  35. package/src/tool/drone-flight-time/seo.astro +15 -15
  36. package/src/tool/drone-motor-propeller-calculator/bibliography.astro +6 -0
  37. package/src/tool/drone-motor-propeller-calculator/bibliography.ts +12 -0
  38. package/src/tool/drone-motor-propeller-calculator/component.astro +109 -0
  39. package/src/tool/drone-motor-propeller-calculator/controller.ts +174 -0
  40. package/src/tool/drone-motor-propeller-calculator/dom-views.ts +78 -0
  41. package/src/tool/drone-motor-propeller-calculator/drone-motor-propeller-calculator.css +562 -0
  42. package/src/tool/drone-motor-propeller-calculator/entry.ts +27 -0
  43. package/src/tool/drone-motor-propeller-calculator/evaluator.ts +14 -0
  44. package/src/tool/drone-motor-propeller-calculator/i18n/de.ts +202 -0
  45. package/src/tool/drone-motor-propeller-calculator/i18n/en.ts +202 -0
  46. package/src/tool/drone-motor-propeller-calculator/i18n/es.ts +202 -0
  47. package/src/tool/drone-motor-propeller-calculator/i18n/fr.ts +202 -0
  48. package/src/tool/drone-motor-propeller-calculator/i18n/id.ts +192 -0
  49. package/src/tool/drone-motor-propeller-calculator/i18n/it.ts +192 -0
  50. package/src/tool/drone-motor-propeller-calculator/i18n/ja.ts +192 -0
  51. package/src/tool/drone-motor-propeller-calculator/i18n/ko.ts +192 -0
  52. package/src/tool/drone-motor-propeller-calculator/i18n/nl.ts +192 -0
  53. package/src/tool/drone-motor-propeller-calculator/i18n/pl.ts +192 -0
  54. package/src/tool/drone-motor-propeller-calculator/i18n/pt.ts +192 -0
  55. package/src/tool/drone-motor-propeller-calculator/i18n/ru.ts +192 -0
  56. package/src/tool/drone-motor-propeller-calculator/i18n/sv.ts +192 -0
  57. package/src/tool/drone-motor-propeller-calculator/i18n/tr.ts +192 -0
  58. package/src/tool/drone-motor-propeller-calculator/i18n/zh.ts +192 -0
  59. package/src/tool/drone-motor-propeller-calculator/index.ts +11 -0
  60. package/src/tool/drone-motor-propeller-calculator/logic.test.ts +56 -0
  61. package/src/tool/drone-motor-propeller-calculator/logic.ts +108 -0
  62. package/src/tool/drone-motor-propeller-calculator/seo.astro +15 -0
  63. package/src/tool/drone-motor-propeller-calculator/storage.ts +26 -0
  64. package/src/tool/drone-motor-propeller-calculator/ui.ts +55 -0
  65. package/src/tool/gps-coordinates-converter/component.astro +275 -275
  66. package/src/tool/gps-coordinates-converter/seo.astro +15 -15
  67. package/src/tools.ts +3 -0
@@ -1,187 +1,187 @@
1
- ---
2
- const { ui } = Astro.props;
3
- import BatterySpecs from "./components/BatterySpecs.astro";
4
- import ConsumptionStats from "./components/ConsumptionStats.astro";
5
- import FlightDashboard from "./components/FlightDashboard.astro";
6
- import AutonomyChart from "./components/AutonomyChart.astro";
7
- ---
8
-
9
- <div class="flight-calculator-ui">
10
- <div class="tech-mega-card animate-in fade-in zoom-in duration-700">
11
- <div class="card-grid">
12
- <div class="config-sidebar">
13
- <BatterySpecs ui={ui} />
14
- <div class="divider"></div>
15
- <ConsumptionStats ui={ui} />
16
- </div>
17
-
18
- <div class="main-display">
19
- <FlightDashboard ui={ui} />
20
- <div class="divider"></div>
21
- <AutonomyChart ui={ui} />
22
- </div>
23
- </div>
24
- </div>
25
- </div>
26
-
27
- <script>
28
- import { Chart, registerables } from "chart.js";
29
- Chart.register(...registerables);
30
-
31
- const capInput = document.getElementById("capacityInput") as HTMLInputElement;
32
- const cellsInput = document.getElementById("cellsInput") as HTMLSelectElement;
33
- const safetyInput = document.getElementById("safetyInput") as HTMLInputElement;
34
- const drawInput = document.getElementById("drawInput") as HTMLInputElement;
35
- const wattsInput = document.getElementById("wattsInput") as HTMLInputElement;
36
- const effInput = document.getElementById("efficiencyInput") as HTMLInputElement;
37
-
38
- const displays = {
39
- cap: document.getElementById("capDisplay"),
40
- cells: document.getElementById("cellsDisplay"),
41
- volts: document.getElementById("voltsDisplay"),
42
- safety: document.getElementById("safetyDisplay"),
43
- draw: document.getElementById("drawDisplay"),
44
- watts: document.getElementById("wattsDisplay"),
45
- eff: document.getElementById("effDisplay"),
46
- timeMain: document.getElementById("timeMainDisplay"),
47
- timeTotal: document.getElementById("timeTotalDisplay"),
48
- wh: document.getElementById("whDisplay"),
49
- co2: document.getElementById("co2Display"),
50
- circle: document.getElementById("timeCircle")
51
- };
52
-
53
- let chart: Chart | null = null;
54
-
55
- function getChartConfig(label: string) {
56
- return {
57
- type: "line",
58
- data: {
59
- labels: [],
60
- datasets: [{
61
- label,
62
- data: [],
63
- borderColor: "#f59e0b",
64
- backgroundColor: "rgba(245, 158, 11, 0.1)",
65
- borderWidth: 3,
66
- fill: true,
67
- tension: 0.4,
68
- pointRadius: 0,
69
- pointHitRadius: 10
70
- }]
71
- },
72
- options: {
73
- responsive: true,
74
- maintainAspectRatio: false,
75
- plugins: { legend: { display: false } },
76
- scales: {
77
- y: { beginAtZero: true, grid: { color: "rgba(0,0,0,0.05)" } },
78
- x: { grid: { display: false } }
79
- }
80
- }
81
- };
82
- }
83
-
84
- function initChart() {
85
- const canvas = document.getElementById("autonomyChart") as HTMLCanvasElement;
86
- const ctx = canvas?.getContext("2d");
87
- if (!ctx) return;
88
- const chartLabel = canvas.dataset.label || "Minutos";
89
- chart = new Chart(ctx, getChartConfig(chartLabel));
90
- }
91
-
92
- function calculate(source: string) {
93
- const cap = parseFloat(capInput.value);
94
- const cells = parseInt(cellsInput.value);
95
- const safety = parseFloat(safetyInput.value) / 100;
96
- let draw = parseFloat(drawInput.value);
97
- const voltage = cells * 3.7;
98
-
99
- if (source === 'watts') {
100
- draw = parseFloat(wattsInput.value) / voltage;
101
- drawInput.value = draw.toFixed(1);
102
- } else {
103
- wattsInput.value = (draw * voltage).toFixed(1);
104
- }
105
-
106
- const state = { cap, cells, safety, draw, voltage };
107
- updateDisplays(state);
108
-
109
- const usableAh = (cap / 1000) * safety;
110
- const totalAh = cap / 1000;
111
-
112
- const safeTimeMin = (usableAh / draw) * 60;
113
- const totalTimeMin = (totalAh / draw) * 60;
114
-
115
- const energyWh = (cap / 1000) * voltage;
116
-
117
- renderResults(safeTimeMin, totalTimeMin, energyWh, cap);
118
- updateChart(usableAh);
119
- }
120
-
121
- function updateDisplays(state: { cap: number; cells: number; safety: number; draw: number; voltage: number }) {
122
- if (displays.cap) displays.cap.textContent = state.cap.toString();
123
- if (displays.cells) displays.cells.textContent = state.cells.toString();
124
- if (displays.volts) displays.volts.textContent = state.voltage.toFixed(1);
125
- if (displays.safety) displays.safety.textContent = Math.round(state.safety * 100).toString();
126
- if (displays.draw) displays.draw.textContent = state.draw.toFixed(1);
127
- if (displays.watts) displays.watts.textContent = (state.draw * state.voltage).toFixed(1);
128
- if (displays.eff) displays.eff.textContent = effInput.value;
129
- }
130
-
131
- function renderResults(safeMin: number, totalMin: number, wh: number, cap: number) {
132
- if (displays.timeMain) displays.timeMain.textContent = formatTime(safeMin);
133
- if (displays.timeTotal) displays.timeTotal.textContent = formatTime(totalMin);
134
- if (displays.wh) displays.wh.textContent = `${wh.toFixed(1)} Wh`;
135
- if (displays.co2) displays.co2.textContent = `${Math.round(cap * 0.05)} g`;
136
-
137
- if (displays.circle) {
138
- const limit = 20;
139
- const progress = Math.min(safeMin / limit, 1);
140
- const offset = 283 - (progress * 283);
141
- (displays.circle as HTMLElement).style.strokeDashoffset = offset.toString();
142
- }
143
- }
144
-
145
- function formatTime(decimalMin: number) {
146
- if (isNaN(decimalMin) || decimalMin === Infinity) return "0:00";
147
- const mins = Math.floor(decimalMin);
148
- const secs = Math.floor((decimalMin - mins) * 60);
149
- return `${mins}:${secs.toString().padStart(2, '0')}`;
150
- }
151
-
152
- function updateChart(usableAh: number) {
153
- if (!chart) return;
154
- const data: number[] = [];
155
- const labels: string[] = [];
156
- const currentDraw = parseFloat(drawInput.value);
157
-
158
- for (let a = Math.max(1, currentDraw - 20); a <= currentDraw + 20; a += 2) {
159
- labels.push(`${a}A`);
160
- const val = ((usableAh / a) * 60);
161
- data.push(Number(val.toFixed(1)));
162
- }
163
-
164
- chart.data.labels = labels;
165
- if (chart.data.datasets[0]) chart.data.datasets[0].data = data;
166
- chart.update('none');
167
- }
168
-
169
- [capInput, cellsInput, safetyInput, drawInput, effInput].forEach(el => {
170
- el?.addEventListener("input", () => calculate('basic'));
171
- });
172
-
173
- wattsInput?.addEventListener("input", () => calculate('watts'));
174
-
175
- document.querySelectorAll('.preset-btn').forEach(btn => {
176
- btn.addEventListener('click', () => {
177
- capInput.value = btn.getAttribute('data-val') || "1500";
178
- calculate('basic');
179
- });
180
- });
181
-
182
- initChart();
183
- calculate('basic');
184
- </script>
185
-
186
-
187
-
1
+ ---
2
+ const { ui } = Astro.props;
3
+ import BatterySpecs from "./components/BatterySpecs.astro";
4
+ import ConsumptionStats from "./components/ConsumptionStats.astro";
5
+ import FlightDashboard from "./components/FlightDashboard.astro";
6
+ import AutonomyChart from "./components/AutonomyChart.astro";
7
+ ---
8
+
9
+ <div class="flight-calculator-ui">
10
+ <div class="tech-mega-card animate-in fade-in zoom-in duration-700">
11
+ <div class="card-grid">
12
+ <div class="config-sidebar">
13
+ <BatterySpecs ui={ui} />
14
+ <div class="divider"></div>
15
+ <ConsumptionStats ui={ui} />
16
+ </div>
17
+
18
+ <div class="main-display">
19
+ <FlightDashboard ui={ui} />
20
+ <div class="divider"></div>
21
+ <AutonomyChart ui={ui} />
22
+ </div>
23
+ </div>
24
+ </div>
25
+ </div>
26
+
27
+ <script>
28
+ import { Chart, registerables } from "chart.js";
29
+ Chart.register(...registerables);
30
+
31
+ const capInput = document.getElementById("capacityInput") as HTMLInputElement;
32
+ const cellsInput = document.getElementById("cellsInput") as HTMLSelectElement;
33
+ const safetyInput = document.getElementById("safetyInput") as HTMLInputElement;
34
+ const drawInput = document.getElementById("drawInput") as HTMLInputElement;
35
+ const wattsInput = document.getElementById("wattsInput") as HTMLInputElement;
36
+ const effInput = document.getElementById("efficiencyInput") as HTMLInputElement;
37
+
38
+ const displays = {
39
+ cap: document.getElementById("capDisplay"),
40
+ cells: document.getElementById("cellsDisplay"),
41
+ volts: document.getElementById("voltsDisplay"),
42
+ safety: document.getElementById("safetyDisplay"),
43
+ draw: document.getElementById("drawDisplay"),
44
+ watts: document.getElementById("wattsDisplay"),
45
+ eff: document.getElementById("effDisplay"),
46
+ timeMain: document.getElementById("timeMainDisplay"),
47
+ timeTotal: document.getElementById("timeTotalDisplay"),
48
+ wh: document.getElementById("whDisplay"),
49
+ co2: document.getElementById("co2Display"),
50
+ circle: document.getElementById("timeCircle")
51
+ };
52
+
53
+ let chart: Chart | null = null;
54
+
55
+ function getChartConfig(label: string) {
56
+ return {
57
+ type: "line",
58
+ data: {
59
+ labels: [],
60
+ datasets: [{
61
+ label,
62
+ data: [],
63
+ borderColor: "#f59e0b",
64
+ backgroundColor: "rgba(245, 158, 11, 0.1)",
65
+ borderWidth: 3,
66
+ fill: true,
67
+ tension: 0.4,
68
+ pointRadius: 0,
69
+ pointHitRadius: 10
70
+ }]
71
+ },
72
+ options: {
73
+ responsive: true,
74
+ maintainAspectRatio: false,
75
+ plugins: { legend: { display: false } },
76
+ scales: {
77
+ y: { beginAtZero: true, grid: { color: "rgba(0,0,0,0.05)" } },
78
+ x: { grid: { display: false } }
79
+ }
80
+ }
81
+ };
82
+ }
83
+
84
+ function initChart() {
85
+ const canvas = document.getElementById("autonomyChart") as HTMLCanvasElement;
86
+ const ctx = canvas?.getContext("2d");
87
+ if (!ctx) return;
88
+ const chartLabel = canvas.dataset.label || "Minutos";
89
+ chart = new Chart(ctx, getChartConfig(chartLabel));
90
+ }
91
+
92
+ function calculate(source: string) {
93
+ const cap = parseFloat(capInput.value);
94
+ const cells = parseInt(cellsInput.value);
95
+ const safety = parseFloat(safetyInput.value) / 100;
96
+ let draw = parseFloat(drawInput.value);
97
+ const voltage = cells * 3.7;
98
+
99
+ if (source === 'watts') {
100
+ draw = parseFloat(wattsInput.value) / voltage;
101
+ drawInput.value = draw.toFixed(1);
102
+ } else {
103
+ wattsInput.value = (draw * voltage).toFixed(1);
104
+ }
105
+
106
+ const state = { cap, cells, safety, draw, voltage };
107
+ updateDisplays(state);
108
+
109
+ const usableAh = (cap / 1000) * safety;
110
+ const totalAh = cap / 1000;
111
+
112
+ const safeTimeMin = (usableAh / draw) * 60;
113
+ const totalTimeMin = (totalAh / draw) * 60;
114
+
115
+ const energyWh = (cap / 1000) * voltage;
116
+
117
+ renderResults(safeTimeMin, totalTimeMin, energyWh, cap);
118
+ updateChart(usableAh);
119
+ }
120
+
121
+ function updateDisplays(state: { cap: number; cells: number; safety: number; draw: number; voltage: number }) {
122
+ if (displays.cap) displays.cap.textContent = state.cap.toString();
123
+ if (displays.cells) displays.cells.textContent = state.cells.toString();
124
+ if (displays.volts) displays.volts.textContent = state.voltage.toFixed(1);
125
+ if (displays.safety) displays.safety.textContent = Math.round(state.safety * 100).toString();
126
+ if (displays.draw) displays.draw.textContent = state.draw.toFixed(1);
127
+ if (displays.watts) displays.watts.textContent = (state.draw * state.voltage).toFixed(1);
128
+ if (displays.eff) displays.eff.textContent = effInput.value;
129
+ }
130
+
131
+ function renderResults(safeMin: number, totalMin: number, wh: number, cap: number) {
132
+ if (displays.timeMain) displays.timeMain.textContent = formatTime(safeMin);
133
+ if (displays.timeTotal) displays.timeTotal.textContent = formatTime(totalMin);
134
+ if (displays.wh) displays.wh.textContent = `${wh.toFixed(1)} Wh`;
135
+ if (displays.co2) displays.co2.textContent = `${Math.round(cap * 0.05)} g`;
136
+
137
+ if (displays.circle) {
138
+ const limit = 20;
139
+ const progress = Math.min(safeMin / limit, 1);
140
+ const offset = 283 - (progress * 283);
141
+ (displays.circle as HTMLElement).style.strokeDashoffset = offset.toString();
142
+ }
143
+ }
144
+
145
+ function formatTime(decimalMin: number) {
146
+ if (isNaN(decimalMin) || decimalMin === Infinity) return "0:00";
147
+ const mins = Math.floor(decimalMin);
148
+ const secs = Math.floor((decimalMin - mins) * 60);
149
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
150
+ }
151
+
152
+ function updateChart(usableAh: number) {
153
+ if (!chart) return;
154
+ const data: number[] = [];
155
+ const labels: string[] = [];
156
+ const currentDraw = parseFloat(drawInput.value);
157
+
158
+ for (let a = Math.max(1, currentDraw - 20); a <= currentDraw + 20; a += 2) {
159
+ labels.push(`${a}A`);
160
+ const val = ((usableAh / a) * 60);
161
+ data.push(Number(val.toFixed(1)));
162
+ }
163
+
164
+ chart.data.labels = labels;
165
+ if (chart.data.datasets[0]) chart.data.datasets[0].data = data;
166
+ chart.update('none');
167
+ }
168
+
169
+ [capInput, cellsInput, safetyInput, drawInput, effInput].forEach(el => {
170
+ el?.addEventListener("input", () => calculate('basic'));
171
+ });
172
+
173
+ wattsInput?.addEventListener("input", () => calculate('watts'));
174
+
175
+ document.querySelectorAll('.preset-btn').forEach(btn => {
176
+ btn.addEventListener('click', () => {
177
+ capInput.value = btn.getAttribute('data-val') || "1500";
178
+ calculate('basic');
179
+ });
180
+ });
181
+
182
+ initChart();
183
+ calculate('basic');
184
+ </script>
185
+
186
+
187
+
@@ -1,15 +1,15 @@
1
- ---
2
- import { SEORenderer } from '@jjlmoya/utils-shared';
3
- import { droneFlightTime } from './index';
4
- import type { KnownLocale } from '../../types';
5
-
6
- interface Props {
7
- locale?: KnownLocale;
8
- }
9
-
10
- const { locale = 'es' } = Astro.props;
11
- const content = await droneFlightTime.i18n[locale]?.();
12
- if (!content) return null;
13
- ---
14
-
15
- {content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { droneFlightTime } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const content = await droneFlightTime.i18n[locale]?.();
12
+ if (!content) return null;
13
+ ---
14
+
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_ITEMS } from './bibliography';
4
+ ---
5
+
6
+ <BibliographyComponent links={BIBLIOGRAPHY_ITEMS} />
@@ -0,0 +1,12 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const BIBLIOGRAPHY_ITEMS: BibliographyEntry[] = [
4
+ {
5
+ name: 'NASA Thrust Coefficient Definition for Propellers',
6
+ url: 'https://ntrs.nasa.gov/api/citations/19900020078/downloads/19900020078.pdf',
7
+ },
8
+ {
9
+ name: 'University of Illinois UIUC Propeller Data Site',
10
+ url: 'https://m-selig.ae.illinois.edu/props/propDB.html',
11
+ },
12
+ ];
@@ -0,0 +1,109 @@
1
+ ---
2
+ import type { DroneMotorPropellerUI } from './ui';
3
+
4
+ interface Props {
5
+ ui: DroneMotorPropellerUI;
6
+ }
7
+
8
+ const { ui } = Astro.props;
9
+ ---
10
+
11
+ <div class="drone-motor-tool" data-drone-motor-calculator data-unit="metric">
12
+ <script is:inline type="application/json" data-drone-motor-ui set:html={JSON.stringify(ui)}></script>
13
+ <div class="tool-controls">
14
+ <section class="setup-card presets-card" aria-labelledby="presets-title">
15
+ <div class="eyebrow">{ui.presetsHeader}</div>
16
+ <div class="preset-row" id="presets-title">
17
+ <button type="button" class="preset-button" data-preset="tiny-cruiser">{ui.presetTinyCruiser}</button>
18
+ <button type="button" class="preset-button is-selected" data-preset="freestyle">{ui.presetFreestyle}</button>
19
+ <button type="button" class="preset-button" data-preset="long-range">{ui.presetLongRange}</button>
20
+ <button type="button" class="preset-button" data-preset="cinelifter">{ui.presetCinelifter}</button>
21
+ </div>
22
+ </section>
23
+
24
+ <section class="setup-card unit-card" aria-labelledby="unit-title">
25
+ <div class="eyebrow" id="unit-title">{ui.unitHeader}</div>
26
+ <div class="unit-toggle" role="group" aria-label={ui.unitHeader}>
27
+ <button type="button" class="unit-button is-selected" data-unit="metric" aria-pressed="true">{ui.metricUnit}</button>
28
+ <button type="button" class="unit-button" data-unit="imperial" aria-pressed="false">{ui.imperialUnit}</button>
29
+ </div>
30
+ </section>
31
+
32
+ <section class="setup-card specs-card" aria-labelledby="setup-title">
33
+ <div class="section-heading">
34
+ <div class="eyebrow">01</div>
35
+ <h2 id="setup-title">{ui.setupHeader}</h2>
36
+ </div>
37
+ <div class="input-grid">
38
+ <label class="field"><span>{ui.motorKvLabel}</span><input data-field="motorKv" type="number" value="1950" min="100" max="10000" step="50" inputmode="decimal" /><small>KV</small></label>
39
+ <label class="field"><span>{ui.batteryVoltageLabel}</span><input data-field="batteryVoltage" type="number" value="22.2" min="1" max="60" step="0.1" inputmode="decimal" /><small>V</small></label>
40
+ <label class="field"><span>{ui.propDiameterLabel}</span><input data-field="propDiameterInches" type="number" value="5.1" min="1" max="30" step="0.1" inputmode="decimal" /><small data-unit-label="length">in</small></label>
41
+ <label class="field"><span>{ui.propPitchLabel}</span><input data-field="propPitchInches" type="number" value="4.3" min="0.5" max="25" step="0.1" inputmode="decimal" /><small data-unit-label="length">in</small></label>
42
+ <div class="choice-field"><span>{ui.bladeCountLabel}</span><div class="choice-row" role="group" aria-label={ui.bladeCountLabel}><button type="button" data-choice="bladeCount" data-value="2" aria-pressed="false">{ui.twoBlades}</button><button type="button" class="is-selected" data-choice="bladeCount" data-value="3" aria-pressed="true">{ui.threeBlades}</button><button type="button" data-choice="bladeCount" data-value="4" aria-pressed="false">{ui.fourBlades}</button></div></div>
43
+ <div class="choice-field"><span>{ui.motorCountLabel}</span><div class="choice-row" role="group" aria-label={ui.motorCountLabel}><button type="button" data-choice="motorCount" data-value="2" aria-pressed="false">{ui.twoMotors}</button><button type="button" class="is-selected" data-choice="motorCount" data-value="4" aria-pressed="true">{ui.fourMotors}</button><button type="button" data-choice="motorCount" data-value="6" aria-pressed="false">{ui.sixMotors}</button><button type="button" data-choice="motorCount" data-value="8" aria-pressed="false">{ui.eightMotors}</button></div></div>
44
+ <label class="field field-wide"><span>{ui.droneWeightLabel}</span><input data-field="droneWeightGrams" type="number" value="700" min="1" max="50000" step="10" inputmode="decimal" /><small data-unit-label="mass">g</small></label>
45
+ </div>
46
+ </section>
47
+
48
+ <section class="setup-card bench-card" aria-labelledby="bench-title">
49
+ <div class="section-heading">
50
+ <div class="eyebrow">02</div>
51
+ <h2 id="bench-title">{ui.benchDataHeader} <span>{ui.optionalLabel}</span></h2>
52
+ </div>
53
+ <div class="input-grid bench-grid">
54
+ <label class="field"><span>{ui.benchThrustLabel}</span><input data-field="benchThrustGrams" type="number" value="1900" min="1" max="30000" step="10" inputmode="decimal" /><small data-unit-label="mass">g</small></label>
55
+ <label class="field"><span>{ui.benchVoltageLabel}</span><input data-field="benchVoltage" type="number" value="22.2" min="1" max="60" step="0.1" inputmode="decimal" /><small>V</small></label>
56
+ </div>
57
+ <div class="bench-foot">
58
+ <p class="fine-print"><span data-output="source-note">{ui.benchBasedLabel}</span></p>
59
+ <button type="button" class="clear-button" data-action="clear-bench">{ui.clearBenchData}</button>
60
+ </div>
61
+ </section>
62
+ </div>
63
+
64
+ <section class="results-card" aria-labelledby="results-title">
65
+ <div class="results-topline">
66
+ <div>
67
+ <div class="eyebrow">03</div>
68
+ <h2 id="results-title">{ui.resultsHeader}</h2>
69
+ </div>
70
+ <div class="status-pill" data-output="status" aria-live="polite">{ui.headroomStatus}</div>
71
+ </div>
72
+ <div class="wind-tunnel">
73
+ <div class="tunnel-label tunnel-label-top">{ui.thrustAxisLabel}</div>
74
+ <div class="thrust-stream stream-left"></div>
75
+ <div class="thrust-stream stream-right"></div>
76
+ <div class="rotor-stage" data-role="rotor-stage">
77
+ <div class="rotor-shadow"></div>
78
+ <div class="rotor-disc">
79
+ <span class="rotor-blade blade-one"></span>
80
+ <span class="rotor-blade blade-two"></span>
81
+ <span class="rotor-blade blade-three"></span>
82
+ <span class="rotor-blade blade-four"></span>
83
+ <span class="rotor-hub"></span>
84
+ </div>
85
+ <div class="rotor-readout"><strong data-output="loaded-rpm">15,000 rpm</strong><span>{ui.sceneCaption}</span></div>
86
+ </div>
87
+ <div class="weight-marker"><span class="marker-line"></span><span>{ui.weightAxisLabel}</span></div>
88
+ <div class="lift-beam"><span></span></div>
89
+ </div>
90
+ <p class="status-advice" data-output="status-advice" aria-live="polite">{ui.headroomAdvice}</p>
91
+ <div class="result-grid">
92
+ <div class="result-cell accent"><span>{ui.thrustPerMotorLabel}</span><strong data-output="thrust-per-motor">1,900 g</strong></div>
93
+ <div class="result-cell accent"><span>{ui.totalThrustLabel}</span><strong data-output="total-thrust">7,600 g</strong></div>
94
+ <div class="result-cell"><span>{ui.pitchSpeedLabel}</span><strong data-output="pitch-speed">1,250 km/h</strong></div>
95
+ <div class="result-cell"><span>{ui.totalPowerLabel}</span><strong data-output="total-power">1,200 W</strong></div>
96
+ <div class="result-cell"><span>{ui.totalCurrentLabel}</span><strong data-output="total-current">54 A</strong></div>
97
+ <div class="result-cell"><span>{ui.thrustMarginLabel}</span><strong data-output="thrust-margin">986%</strong></div>
98
+ <div class="result-cell"><span>{ui.hoverThrottleLabel}</span><strong data-output="hover-throttle">30%</strong></div>
99
+ </div>
100
+ <div class="notes-row"><p>{ui.safetyNote}</p><p>{ui.modelNote}</p></div>
101
+ </section>
102
+ </div>
103
+
104
+ <script>
105
+ import { initDroneMotorPropellerCalculator } from './controller';
106
+
107
+ const root = document.querySelector<HTMLElement>('[data-drone-motor-calculator]');
108
+ if (root) initDroneMotorPropellerCalculator(root);
109
+ </script>