@jjlmoya/utils-drones 1.1.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 (56) hide show
  1. package/package.json +61 -0
  2. package/src/category/i18n/en.ts +107 -0
  3. package/src/category/i18n/es.ts +106 -0
  4. package/src/category/i18n/fr.ts +107 -0
  5. package/src/category/index.ts +15 -0
  6. package/src/category/seo.astro +92 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +11 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +22 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +19 -0
  17. package/src/tests/locale_completeness.test.ts +42 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/no_h1_in_components.test.ts +48 -0
  20. package/src/tests/seo_length.test.ts +22 -0
  21. package/src/tests/seo_section_types.test.ts +75 -0
  22. package/src/tests/tool_validation.test.ts +17 -0
  23. package/src/tool/antenna-length-calculator/AntennaLengthCalculator.css +684 -0
  24. package/src/tool/antenna-length-calculator/bibliography.astro +14 -0
  25. package/src/tool/antenna-length-calculator/component.astro +360 -0
  26. package/src/tool/antenna-length-calculator/i18n/en.ts +204 -0
  27. package/src/tool/antenna-length-calculator/i18n/es.ts +204 -0
  28. package/src/tool/antenna-length-calculator/i18n/fr.ts +204 -0
  29. package/src/tool/antenna-length-calculator/index.ts +27 -0
  30. package/src/tool/antenna-length-calculator/seo.astro +39 -0
  31. package/src/tool/drone-flight-time/FlightTimeCalculator.css +363 -0
  32. package/src/tool/drone-flight-time/bibliography.astro +14 -0
  33. package/src/tool/drone-flight-time/component.astro +262 -0
  34. package/src/tool/drone-flight-time/components/AutonomyChart.astro +13 -0
  35. package/src/tool/drone-flight-time/components/BatterySpecs.astro +46 -0
  36. package/src/tool/drone-flight-time/components/ConsumptionStats.astro +33 -0
  37. package/src/tool/drone-flight-time/components/FlightDashboard.astro +33 -0
  38. package/src/tool/drone-flight-time/i18n/en.ts +200 -0
  39. package/src/tool/drone-flight-time/i18n/es.ts +200 -0
  40. package/src/tool/drone-flight-time/i18n/fr.ts +200 -0
  41. package/src/tool/drone-flight-time/index.ts +27 -0
  42. package/src/tool/drone-flight-time/seo.astro +31 -0
  43. package/src/tool/gps-coordinates-converter/GpsCoordinatesConverter.css +310 -0
  44. package/src/tool/gps-coordinates-converter/bibliography.astro +14 -0
  45. package/src/tool/gps-coordinates-converter/component.astro +355 -0
  46. package/src/tool/gps-coordinates-converter/components/GpsHistory.astro +36 -0
  47. package/src/tool/gps-coordinates-converter/components/GpsInputs.astro +92 -0
  48. package/src/tool/gps-coordinates-converter/components/GpsMap.astro +18 -0
  49. package/src/tool/gps-coordinates-converter/components/GpsResults.astro +50 -0
  50. package/src/tool/gps-coordinates-converter/i18n/en.ts +201 -0
  51. package/src/tool/gps-coordinates-converter/i18n/es.ts +201 -0
  52. package/src/tool/gps-coordinates-converter/i18n/fr.ts +201 -0
  53. package/src/tool/gps-coordinates-converter/index.ts +27 -0
  54. package/src/tool/gps-coordinates-converter/seo.astro +39 -0
  55. package/src/tools.ts +16 -0
  56. package/src/types.ts +72 -0
@@ -0,0 +1,363 @@
1
+ .flight-calculator-ui {
2
+ --dft-accent: #f97316;
3
+ --dft-accent-glow: rgba(249, 115, 22, 0.15);
4
+ --dft-bg: #fff;
5
+ --dft-bg-muted: #f8fafc;
6
+ --dft-bg-surface: #f1f5f9;
7
+ --dft-text: #0f172a;
8
+ --dft-text-muted: #64748b;
9
+ --dft-text-dim: #94a3b8;
10
+ --dft-border: #e2e8f0;
11
+ --dft-border-light: rgba(0, 0, 0, 0.05);
12
+ --dft-border-dark-light: rgba(255, 255, 255, 0.05);
13
+ --dft-shadow: 0 30px 60px rgba(0, 0, 0, 0.08);
14
+ --dft-success: #15803d;
15
+ --dft-success-bg: #dcfce7;
16
+ --dft-warning: #a16207;
17
+ --dft-warning-bg: #fef9c3;
18
+ --dft-danger: #b91c1c;
19
+ --dft-danger-bg: #fee2e2;
20
+ }
21
+
22
+ [data-theme="dark"] .flight-calculator-ui,
23
+ .theme-dark .flight-calculator-ui {
24
+ --dft-bg: #0f172a;
25
+ --dft-bg-muted: #1e293b;
26
+ --dft-bg-surface: #334155;
27
+ --dft-text: #f1f5f9;
28
+ --dft-text-muted: #94a3b8;
29
+ --dft-text-dim: #cbd5e1;
30
+ --dft-border: #334155;
31
+ --dft-border-light: rgba(255, 255, 255, 0.08);
32
+ --dft-border-dark-light: rgba(255, 255, 255, 0.05);
33
+ --dft-success: #4ade80;
34
+ --dft-success-bg: rgba(21, 128, 61, 0.15);
35
+ --dft-warning: #facc15;
36
+ --dft-warning-bg: rgba(161, 98, 7, 0.15);
37
+ --dft-danger: #f87171;
38
+ --dft-danger-bg: rgba(185, 28, 28, 0.15);
39
+
40
+ width: 100%;
41
+ max-width: 1100px;
42
+ margin: 0 auto;
43
+ }
44
+
45
+ .tech-mega-card {
46
+ background: var(--dft-bg);
47
+ backdrop-filter: blur(20px);
48
+ border: 1px solid var(--dft-border);
49
+ border-radius: 32px;
50
+ box-shadow: var(--dft-shadow);
51
+ overflow: hidden;
52
+ }
53
+
54
+ .card-grid {
55
+ display: grid;
56
+ grid-template-columns: 380px 1fr;
57
+ }
58
+
59
+ .config-sidebar {
60
+ padding: 2.5rem;
61
+ background: rgba(255, 255, 255, 0.3);
62
+ display: flex;
63
+ flex-direction: column;
64
+ gap: 2.5rem;
65
+ border-right: 1px solid var(--dft-border-light);
66
+ }
67
+
68
+ [data-theme="dark"] .config-sidebar,
69
+ .theme-dark .config-sidebar {
70
+ background: rgba(255, 255, 255, 0.02);
71
+ border-right-color: var(--dft-border-dark-light);
72
+ }
73
+
74
+ .main-display {
75
+ padding: 2.5rem;
76
+ display: flex;
77
+ flex-direction: column;
78
+ gap: 2.5rem;
79
+ }
80
+
81
+ .divider {
82
+ height: 1px;
83
+ width: 100%;
84
+ background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.05), transparent);
85
+ }
86
+
87
+ [data-theme="dark"] .divider,
88
+ .theme-dark .divider {
89
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.05), transparent);
90
+ }
91
+
92
+ .tech-section {
93
+ display: flex;
94
+ flex-direction: column;
95
+ gap: 1.5rem;
96
+ }
97
+
98
+ .section-title {
99
+ font-size: 0.95rem;
100
+ font-weight: 800;
101
+ color: var(--dft-text);
102
+ text-transform: uppercase;
103
+ letter-spacing: 0.05em;
104
+ margin: 0;
105
+ }
106
+
107
+ .input-group {
108
+ display: flex;
109
+ flex-direction: column;
110
+ gap: 0.75rem;
111
+ }
112
+
113
+ .label-row {
114
+ display: flex;
115
+ justify-content: space-between;
116
+ align-items: center;
117
+ }
118
+
119
+ .input-group label {
120
+ font-size: 0.85rem;
121
+ font-weight: 700;
122
+ color: var(--dft-text-muted);
123
+ }
124
+
125
+ .value-badge {
126
+ padding: 0.25rem 0.75rem;
127
+ background: rgba(249, 115, 22, 0.1);
128
+ border-radius: 12px;
129
+ color: var(--dft-accent);
130
+ font-weight: 800;
131
+ font-size: 0.9rem;
132
+ }
133
+
134
+ .tech-slider {
135
+ width: 100%;
136
+ height: 6px;
137
+ appearance: none;
138
+ background: var(--dft-border);
139
+ border-radius: 3px;
140
+ outline: none;
141
+ }
142
+
143
+ .tech-slider::-webkit-slider-thumb {
144
+ appearance: none;
145
+ width: 18px;
146
+ height: 18px;
147
+ background: var(--dft-accent);
148
+ border: 3px solid white;
149
+ border-radius: 50%;
150
+ cursor: pointer;
151
+ }
152
+
153
+ .tech-slider::-moz-range-thumb {
154
+ width: 18px;
155
+ height: 18px;
156
+ background: var(--dft-accent);
157
+ border: 3px solid white;
158
+ border-radius: 50%;
159
+ cursor: pointer;
160
+ }
161
+
162
+ .tech-select {
163
+ width: 100%;
164
+ padding: 0.75rem;
165
+ background: var(--dft-bg-surface);
166
+ border: 1px solid var(--dft-border);
167
+ border-radius: 12px;
168
+ font-weight: 600;
169
+ color: var(--dft-text);
170
+ }
171
+
172
+ .tech-input {
173
+ width: 100%;
174
+ padding: 0.75rem;
175
+ background: var(--dft-bg-surface);
176
+ border: 1px solid var(--dft-border);
177
+ border-radius: 12px;
178
+ font-weight: 600;
179
+ color: var(--dft-text);
180
+ }
181
+
182
+ .presets {
183
+ display: flex;
184
+ gap: 0.5rem;
185
+ flex-wrap: wrap;
186
+ }
187
+
188
+ .preset-btn {
189
+ padding: 0.35rem 0.65rem;
190
+ background: var(--dft-bg-surface);
191
+ border: none;
192
+ border-radius: 8px;
193
+ font-size: 0.75rem;
194
+ font-weight: 700;
195
+ color: var(--dft-text-muted);
196
+ cursor: pointer;
197
+ transition: all 0.2s;
198
+ }
199
+
200
+ .preset-btn:hover {
201
+ background: var(--dft-border);
202
+ color: var(--dft-text);
203
+ }
204
+
205
+ .hint {
206
+ font-size: 0.75rem;
207
+ color: var(--dft-text-dim);
208
+ }
209
+
210
+ .dashboard-section {
211
+ display: flex;
212
+ flex-direction: column;
213
+ align-items: center;
214
+ gap: 2rem;
215
+ }
216
+
217
+ .main-result {
218
+ display: flex;
219
+ justify-content: center;
220
+ }
221
+
222
+ .radial-container {
223
+ position: relative;
224
+ width: 240px;
225
+ height: 240px;
226
+ display: flex;
227
+ justify-content: center;
228
+ align-items: center;
229
+ }
230
+
231
+ .radial-svg {
232
+ transform: rotate(-90deg);
233
+ width: 100%;
234
+ height: 100%;
235
+ }
236
+
237
+ .bg-circle {
238
+ fill: none;
239
+ stroke: var(--dft-bg-surface);
240
+ stroke-width: 8;
241
+ }
242
+
243
+ .fg-circle {
244
+ fill: none;
245
+ stroke: var(--dft-accent);
246
+ stroke-width: 8;
247
+ stroke-linecap: round;
248
+ stroke-dasharray: 283;
249
+ stroke-dashoffset: 0;
250
+ transition: stroke-dashoffset 0.8s cubic-bezier(0.4, 0, 0.2, 1);
251
+ }
252
+
253
+ .result-text {
254
+ position: absolute;
255
+ display: flex;
256
+ flex-direction: column;
257
+ align-items: center;
258
+ gap: 0.25rem;
259
+ }
260
+
261
+ .time-primary {
262
+ font-size: 4.5rem;
263
+ font-weight: 950;
264
+ color: var(--dft-text);
265
+ letter-spacing: -0.05em;
266
+ line-height: 1;
267
+ }
268
+
269
+ .time-label {
270
+ font-size: 0.9rem;
271
+ font-weight: 800;
272
+ color: var(--dft-text-muted);
273
+ text-transform: uppercase;
274
+ letter-spacing: 0.1em;
275
+ }
276
+
277
+ .secondary-stats {
278
+ display: grid;
279
+ grid-template-columns: repeat(3, 1fr);
280
+ gap: 1.5rem;
281
+ width: 100%;
282
+ }
283
+
284
+ .stat-box {
285
+ display: flex;
286
+ flex-direction: column;
287
+ align-items: center;
288
+ gap: 0.25rem;
289
+ padding: 1.25rem;
290
+ background: rgba(255, 255, 255, 0.5);
291
+ border: 1px solid rgba(0, 0, 0, 0.03);
292
+ border-radius: 20px;
293
+ }
294
+
295
+ [data-theme="dark"] .stat-box,
296
+ .theme-dark .stat-box {
297
+ background: rgba(255, 255, 255, 0.03);
298
+ border-color: rgba(255, 255, 255, 0.05);
299
+ }
300
+
301
+ .stat-label {
302
+ font-size: 0.7rem;
303
+ font-weight: 700;
304
+ color: var(--dft-text-dim);
305
+ text-transform: uppercase;
306
+ text-align: center;
307
+ }
308
+
309
+ .stat-value {
310
+ font-size: 1.2rem;
311
+ font-weight: 900;
312
+ color: var(--dft-text);
313
+ }
314
+
315
+ .chart-header {
316
+ display: flex;
317
+ justify-content: space-between;
318
+ align-items: center;
319
+ margin-bottom: 1rem;
320
+ }
321
+
322
+ .chart-subtitle {
323
+ font-size: 0.85rem;
324
+ font-weight: 600;
325
+ color: var(--dft-text-muted);
326
+ }
327
+
328
+ .chart-body {
329
+ position: relative;
330
+ height: 300px;
331
+ width: 100%;
332
+ }
333
+
334
+ @media (max-width: 992px) {
335
+ .card-grid {
336
+ grid-template-columns: 1fr;
337
+ }
338
+
339
+ .config-sidebar {
340
+ border-right: none;
341
+ border-bottom: 1px solid var(--dft-border-light);
342
+ }
343
+
344
+ [data-theme="dark"] .config-sidebar,
345
+ .theme-dark .config-sidebar {
346
+ border-bottom-color: var(--dft-border-dark-light);
347
+ }
348
+ }
349
+
350
+ @media (max-width: 600px) {
351
+ .secondary-stats {
352
+ grid-template-columns: 1fr;
353
+ }
354
+
355
+ .radial-container {
356
+ width: 200px;
357
+ height: 200px;
358
+ }
359
+
360
+ .time-primary {
361
+ font-size: 3.5rem;
362
+ }
363
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography } 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
+ ---
13
+
14
+ {content && <Bibliography links={content.bibliography} />}
@@ -0,0 +1,262 @@
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
+ import "./FlightTimeCalculator.css";
8
+ ---
9
+
10
+ <div class="flight-calculator-ui">
11
+ <div class="tech-mega-card animate-in fade-in zoom-in duration-700">
12
+ <div class="card-grid">
13
+ <div class="config-sidebar">
14
+ <BatterySpecs ui={ui} />
15
+ <div class="divider"></div>
16
+ <ConsumptionStats ui={ui} />
17
+ </div>
18
+
19
+ <div class="main-display">
20
+ <FlightDashboard ui={ui} />
21
+ <div class="divider"></div>
22
+ <AutonomyChart ui={ui} />
23
+ </div>
24
+ </div>
25
+ </div>
26
+ </div>
27
+
28
+ <script>
29
+ import { Chart, registerables } from "chart.js";
30
+ Chart.register(...registerables);
31
+
32
+ const capInput = document.getElementById("capacityInput") as HTMLInputElement;
33
+ const cellsInput = document.getElementById("cellsInput") as HTMLSelectElement;
34
+ const safetyInput = document.getElementById("safetyInput") as HTMLInputElement;
35
+ const drawInput = document.getElementById("drawInput") as HTMLInputElement;
36
+ const wattsInput = document.getElementById("wattsInput") as HTMLInputElement;
37
+ const effInput = document.getElementById("efficiencyInput") as HTMLInputElement;
38
+
39
+ const displays = {
40
+ cap: document.getElementById("capDisplay"),
41
+ cells: document.getElementById("cellsDisplay"),
42
+ volts: document.getElementById("voltsDisplay"),
43
+ safety: document.getElementById("safetyDisplay"),
44
+ draw: document.getElementById("drawDisplay"),
45
+ watts: document.getElementById("wattsDisplay"),
46
+ eff: document.getElementById("effDisplay"),
47
+ timeMain: document.getElementById("timeMainDisplay"),
48
+ timeTotal: document.getElementById("timeTotalDisplay"),
49
+ wh: document.getElementById("whDisplay"),
50
+ co2: document.getElementById("co2Display"),
51
+ circle: document.getElementById("timeCircle")
52
+ };
53
+
54
+ let chart: Chart | null = null;
55
+
56
+ function getChartConfig(label: string) {
57
+ return {
58
+ type: "line",
59
+ data: {
60
+ labels: [],
61
+ datasets: [{
62
+ label,
63
+ data: [],
64
+ borderColor: "#f97316",
65
+ backgroundColor: "rgba(249, 115, 22, 0.1)",
66
+ borderWidth: 3,
67
+ fill: true,
68
+ tension: 0.4,
69
+ pointRadius: 0,
70
+ pointHitRadius: 10
71
+ }]
72
+ },
73
+ options: {
74
+ responsive: true,
75
+ maintainAspectRatio: false,
76
+ plugins: { legend: { display: false } },
77
+ scales: {
78
+ y: { beginAtZero: true, grid: { color: "rgba(0,0,0,0.05)" } },
79
+ x: { grid: { display: false } }
80
+ }
81
+ }
82
+ };
83
+ }
84
+
85
+ function initChart() {
86
+ const canvas = document.getElementById("autonomyChart") as HTMLCanvasElement;
87
+ const ctx = canvas?.getContext("2d");
88
+ if (!ctx) return;
89
+ const chartLabel = canvas.dataset.label || "Minutos";
90
+ chart = new Chart(ctx, getChartConfig(chartLabel));
91
+ }
92
+
93
+ function calculate(source: string) {
94
+ const cap = parseFloat(capInput.value);
95
+ const cells = parseInt(cellsInput.value);
96
+ const safety = parseFloat(safetyInput.value) / 100;
97
+ let draw = parseFloat(drawInput.value);
98
+ const voltage = cells * 3.7;
99
+
100
+ if (source === 'watts') {
101
+ draw = parseFloat(wattsInput.value) / voltage;
102
+ drawInput.value = draw.toFixed(1);
103
+ } else {
104
+ wattsInput.value = (draw * voltage).toFixed(1);
105
+ }
106
+
107
+ const state = { cap, cells, safety, draw, voltage };
108
+ updateDisplays(state);
109
+
110
+ const usableAh = (cap / 1000) * safety;
111
+ const totalAh = cap / 1000;
112
+
113
+ const safeTimeMin = (usableAh / draw) * 60;
114
+ const totalTimeMin = (totalAh / draw) * 60;
115
+
116
+ const energyWh = (cap / 1000) * voltage;
117
+
118
+ renderResults(safeTimeMin, totalTimeMin, energyWh, cap);
119
+ updateChart(usableAh);
120
+ }
121
+
122
+ function updateDisplays(state: { cap: number; cells: number; safety: number; draw: number; voltage: number }) {
123
+ if (displays.cap) displays.cap.textContent = state.cap.toString();
124
+ if (displays.cells) displays.cells.textContent = state.cells.toString();
125
+ if (displays.volts) displays.volts.textContent = state.voltage.toFixed(1);
126
+ if (displays.safety) displays.safety.textContent = Math.round(state.safety * 100).toString();
127
+ if (displays.draw) displays.draw.textContent = state.draw.toFixed(1);
128
+ if (displays.watts) displays.watts.textContent = (state.draw * state.voltage).toFixed(1);
129
+ if (displays.eff) displays.eff.textContent = effInput.value;
130
+ }
131
+
132
+ function renderResults(safeMin: number, totalMin: number, wh: number, cap: number) {
133
+ if (displays.timeMain) displays.timeMain.textContent = formatTime(safeMin);
134
+ if (displays.timeTotal) displays.timeTotal.textContent = formatTime(totalMin);
135
+ if (displays.wh) displays.wh.textContent = `${wh.toFixed(1)} Wh`;
136
+ if (displays.co2) displays.co2.textContent = `${Math.round(cap * 0.05)} g`;
137
+
138
+ if (displays.circle) {
139
+ const limit = 20;
140
+ const progress = Math.min(safeMin / limit, 1);
141
+ const offset = 283 - (progress * 283);
142
+ displays.circle.style.strokeDashoffset = offset.toString();
143
+ }
144
+ }
145
+
146
+ function formatTime(decimalMin: number) {
147
+ if (isNaN(decimalMin) || decimalMin === Infinity) return "0:00";
148
+ const mins = Math.floor(decimalMin);
149
+ const secs = Math.floor((decimalMin - mins) * 60);
150
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
151
+ }
152
+
153
+ function updateChart(usableAh: number) {
154
+ if (!chart) return;
155
+ const data: number[] = [];
156
+ const labels: string[] = [];
157
+ const currentDraw = parseFloat(drawInput.value);
158
+
159
+ for (let a = Math.max(1, currentDraw - 20); a <= currentDraw + 20; a += 2) {
160
+ labels.push(`${a}A`);
161
+ const val = ((usableAh / a) * 60);
162
+ data.push(Number(val.toFixed(1)));
163
+ }
164
+
165
+ chart.data.labels = labels;
166
+ if (chart.data.datasets[0]) chart.data.datasets[0].data = data;
167
+ chart.update('none');
168
+ }
169
+
170
+ [capInput, cellsInput, safetyInput, drawInput, effInput].forEach(el => {
171
+ el?.addEventListener("input", () => calculate('basic'));
172
+ });
173
+
174
+ wattsInput?.addEventListener("input", () => calculate('watts'));
175
+
176
+ document.querySelectorAll('.preset-btn').forEach(btn => {
177
+ btn.addEventListener('click', () => {
178
+ capInput.value = btn.getAttribute('data-val') || "1500";
179
+ calculate('basic');
180
+ });
181
+ });
182
+
183
+ initChart();
184
+ calculate('basic');
185
+ </script>
186
+
187
+ <style>
188
+ .flight-calculator-ui {
189
+ width: 100%;
190
+ max-width: 1100px;
191
+ margin: 0 auto;
192
+ }
193
+
194
+ .tech-mega-card {
195
+ background: rgba(255, 255, 255, 0.7);
196
+ backdrop-filter: blur(20px);
197
+ border: 1px solid rgba(255, 255, 255, 0.4);
198
+ border-radius: 32px;
199
+ box-shadow: 0 30px 60px rgba(0, 0, 0, 0.08);
200
+ overflow: hidden;
201
+ }
202
+
203
+ [data-theme="dark"] .tech-mega-card,
204
+ .theme-dark .tech-mega-card {
205
+ background: rgba(15, 23, 42, 0.7);
206
+ border-color: rgba(255, 255, 255, 0.05);
207
+ }
208
+
209
+ .card-grid {
210
+ display: grid;
211
+ grid-template-columns: 380px 1fr;
212
+ }
213
+
214
+ .config-sidebar {
215
+ padding: 2.5rem;
216
+ background: rgba(255, 255, 255, 0.3);
217
+ display: flex;
218
+ flex-direction: column;
219
+ gap: 2.5rem;
220
+ border-right: 1px solid rgba(0, 0, 0, 0.05);
221
+ }
222
+
223
+ [data-theme="dark"],
224
+ .theme-dark .config-sidebar {
225
+ background: rgba(255, 255, 255, 0.02);
226
+ border-right-color: rgba(255, 255, 255, 0.05);
227
+ }
228
+
229
+ .main-display {
230
+ padding: 2.5rem;
231
+ display: flex;
232
+ flex-direction: column;
233
+ gap: 2.5rem;
234
+ }
235
+
236
+ .divider {
237
+ height: 1px;
238
+ width: 100%;
239
+ background: linear-gradient(90deg, transparent, rgba(0,0,0,0.05), transparent);
240
+ }
241
+
242
+ [data-theme="dark"],
243
+ .theme-dark .divider {
244
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.05), transparent);
245
+ }
246
+
247
+ @media (max-width: 992px) {
248
+ .card-grid {
249
+ grid-template-columns: 1fr;
250
+ }
251
+
252
+ .config-sidebar {
253
+ border-right: none;
254
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
255
+ }
256
+
257
+ [data-theme="dark"],
258
+ .theme-dark .config-sidebar {
259
+ border-bottom-color: rgba(255, 255, 255, 0.05);
260
+ }
261
+ }
262
+ </style>
@@ -0,0 +1,13 @@
1
+ ---
2
+ const { ui } = Astro.props;
3
+ ---
4
+
5
+ <div class="tech-section">
6
+ <div class="chart-header">
7
+ <h3 class="section-title">{ui.autonomyChart}</h3>
8
+ <span class="chart-subtitle">{ui.chartSubtitle}</span>
9
+ </div>
10
+ <div class="chart-body">
11
+ <canvas id="autonomyChart" data-label={ui.chartLabel}></canvas>
12
+ </div>
13
+ </div>
@@ -0,0 +1,46 @@
1
+ ---
2
+ const { ui } = Astro.props;
3
+ ---
4
+
5
+ <div class="tech-section">
6
+ <h3 class="section-title">{ui.batterySpecs}</h3>
7
+
8
+ <div class="input-group">
9
+ <div class="label-row">
10
+ <label for="capacityInput">{ui.capacity}</label>
11
+ <div class="value-badge"><span id="capDisplay">1500</span> mAh</div>
12
+ </div>
13
+ <input type="range" id="capacityInput" min="300" max="10000" step="50" value="1500" class="tech-slider" />
14
+ <div class="presets">
15
+ <button class="preset-btn" data-val="450">450</button>
16
+ <button class="preset-btn" data-val="1300">1300</button>
17
+ <button class="preset-btn" data-val="1550">1550</button>
18
+ <button class="preset-btn" data-val="5000">5000</button>
19
+ </div>
20
+ </div>
21
+
22
+ <div class="input-group">
23
+ <div class="label-row">
24
+ <label for="cellsInput">{ui.voltage}</label>
25
+ <div class="value-badge"><span id="cellsDisplay">4</span>S (<span id="voltsDisplay">14.8</span>V)</div>
26
+ </div>
27
+ <select id="cellsInput" class="tech-select">
28
+ <option value="1">1S (3.7V)</option>
29
+ <option value="2">2S (7.4V)</option>
30
+ <option value="3">3S (11.1V)</option>
31
+ <option value="4" selected>4S (14.8V)</option>
32
+ <option value="6">6S (22.2V)</option>
33
+ <option value="8">8S (29.6V)</option>
34
+ <option value="12">12S (44.4V)</option>
35
+ </select>
36
+ </div>
37
+
38
+ <div class="input-group">
39
+ <div class="label-row">
40
+ <label for="safetyInput">{ui.safetyMargin}</label>
41
+ <div class="value-badge"><span id="safetyDisplay">80</span>%</div>
42
+ </div>
43
+ <input type="range" id="safetyInput" min="50" max="100" step="5" value="80" class="tech-slider" />
44
+ <small class="hint">{ui.landingHint}</small>
45
+ </div>
46
+ </div>