@jjlmoya/utils-drones 1.30.0 → 1.31.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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -0
  3. package/src/entries.ts +4 -0
  4. package/src/index.ts +1 -1
  5. package/src/tests/locale_completeness.test.ts +1 -2
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/drone-motor-propeller-calculator/bibliography.astro +6 -0
  8. package/src/tool/drone-motor-propeller-calculator/bibliography.ts +12 -0
  9. package/src/tool/drone-motor-propeller-calculator/component.astro +109 -0
  10. package/src/tool/drone-motor-propeller-calculator/controller.ts +174 -0
  11. package/src/tool/drone-motor-propeller-calculator/dom-views.ts +78 -0
  12. package/src/tool/drone-motor-propeller-calculator/drone-motor-propeller-calculator.css +562 -0
  13. package/src/tool/drone-motor-propeller-calculator/entry.ts +27 -0
  14. package/src/tool/drone-motor-propeller-calculator/evaluator.ts +14 -0
  15. package/src/tool/drone-motor-propeller-calculator/i18n/de.ts +202 -0
  16. package/src/tool/drone-motor-propeller-calculator/i18n/en.ts +202 -0
  17. package/src/tool/drone-motor-propeller-calculator/i18n/es.ts +202 -0
  18. package/src/tool/drone-motor-propeller-calculator/i18n/fr.ts +202 -0
  19. package/src/tool/drone-motor-propeller-calculator/i18n/id.ts +192 -0
  20. package/src/tool/drone-motor-propeller-calculator/i18n/it.ts +192 -0
  21. package/src/tool/drone-motor-propeller-calculator/i18n/ja.ts +192 -0
  22. package/src/tool/drone-motor-propeller-calculator/i18n/ko.ts +192 -0
  23. package/src/tool/drone-motor-propeller-calculator/i18n/nl.ts +192 -0
  24. package/src/tool/drone-motor-propeller-calculator/i18n/pl.ts +192 -0
  25. package/src/tool/drone-motor-propeller-calculator/i18n/pt.ts +192 -0
  26. package/src/tool/drone-motor-propeller-calculator/i18n/ru.ts +192 -0
  27. package/src/tool/drone-motor-propeller-calculator/i18n/sv.ts +192 -0
  28. package/src/tool/drone-motor-propeller-calculator/i18n/tr.ts +192 -0
  29. package/src/tool/drone-motor-propeller-calculator/i18n/zh.ts +192 -0
  30. package/src/tool/drone-motor-propeller-calculator/index.ts +11 -0
  31. package/src/tool/drone-motor-propeller-calculator/logic.test.ts +56 -0
  32. package/src/tool/drone-motor-propeller-calculator/logic.ts +108 -0
  33. package/src/tool/drone-motor-propeller-calculator/seo.astro +12 -0
  34. package/src/tool/drone-motor-propeller-calculator/storage.ts +26 -0
  35. package/src/tool/drone-motor-propeller-calculator/ui.ts +55 -0
  36. package/src/tools.ts +3 -0
@@ -0,0 +1,562 @@
1
+ :root {
2
+ --n-ink: #182333;
3
+ --n-muted: #657084;
4
+ --n-paper: #f7f4ee;
5
+ --n-card: #fffdf8;
6
+ --n-line: #d9d5cc;
7
+ --n-blue: #1553c7;
8
+ --n-blue-soft: #dce8ff;
9
+ --n-orange: #e76b35;
10
+ --n-orange-soft: #ffe5d6;
11
+ --n-green: #18765d;
12
+ --n-green-soft: #dff2e9;
13
+ --n-red: #b84b43;
14
+ --n-red-soft: #f8dfdb;
15
+ --n-shadow: rgba(24, 35, 51, 0.12);
16
+ --n-stage: #eaf1f3;
17
+ --n-grid: rgba(24, 35, 51, 0.1);
18
+ --n-white: #fff;
19
+ }
20
+
21
+ .theme-dark .drone-motor-tool {
22
+ --n-ink: #edf2fb;
23
+ --n-muted: #a4b0c3;
24
+ --n-paper: #111824;
25
+ --n-card: #192333;
26
+ --n-line: #354156;
27
+ --n-blue: #80a9ff;
28
+ --n-blue-soft: #21385f;
29
+ --n-orange: #ff9d69;
30
+ --n-orange-soft: #513022;
31
+ --n-green: #62caa4;
32
+ --n-green-soft: #173b32;
33
+ --n-red: #ff8f83;
34
+ --n-red-soft: #4b2728;
35
+ --n-shadow: rgba(0, 0, 0, 0.3);
36
+ --n-stage: #1c2a36;
37
+ --n-grid: rgba(255, 255, 255, 0.11);
38
+ --n-white: #fff;
39
+ }
40
+
41
+ .drone-motor-tool {
42
+ display: grid;
43
+ gap: 0;
44
+ overflow: hidden;
45
+ border: 1px solid var(--n-line);
46
+ border-radius: 1.25rem;
47
+ background: var(--n-card);
48
+ box-shadow: 0 18px 45px var(--n-shadow);
49
+ color: var(--n-ink);
50
+ padding: clamp(1rem, 3vw, 2rem);
51
+ }
52
+
53
+ .tool-controls {
54
+ display: grid;
55
+ gap: 0;
56
+ grid-template-columns: 1fr;
57
+ }
58
+
59
+ .setup-card,
60
+ .results-card {
61
+ border: 0;
62
+ border-radius: 0;
63
+ background: transparent;
64
+ box-shadow: none;
65
+ }
66
+
67
+ .setup-card {
68
+ padding: 0;
69
+ }
70
+
71
+ .presets-card,
72
+ .specs-card,
73
+ .bench-card {
74
+ grid-column: 1 / -1;
75
+ border-bottom: 1px solid var(--n-line);
76
+ padding: 0 0 1.25rem;
77
+ }
78
+
79
+ .unit-card {
80
+ align-self: stretch;
81
+ min-width: 0;
82
+ border-bottom: 1px solid var(--n-line);
83
+ padding: 1.25rem 0;
84
+ }
85
+
86
+ .eyebrow {
87
+ color: var(--n-orange);
88
+ font-size: .7rem;
89
+ font-weight: 800;
90
+ letter-spacing: .15em;
91
+ text-transform: uppercase;
92
+ }
93
+
94
+ .preset-row,
95
+ .unit-toggle,
96
+ .choice-row {
97
+ display: flex;
98
+ flex-wrap: wrap;
99
+ gap: .5rem;
100
+ }
101
+
102
+ .preset-row {
103
+ margin-top: .75rem;
104
+ }
105
+
106
+ button {
107
+ cursor: pointer;
108
+ font: inherit;
109
+ }
110
+
111
+ .preset-button,
112
+ .unit-button,
113
+ .choice-row button {
114
+ border: 1px solid var(--n-line);
115
+ border-radius: 999px;
116
+ background: var(--n-paper);
117
+ color: var(--n-muted);
118
+ font-size: .8rem;
119
+ font-weight: 700;
120
+ padding: .6rem .85rem;
121
+ transition: border-color .2s ease, background .2s ease, color .2s ease, transform .2s ease;
122
+ }
123
+
124
+ .preset-button:hover,
125
+ .unit-button:hover,
126
+ .choice-row button:hover {
127
+ border-color: var(--n-blue);
128
+ color: var(--n-blue);
129
+ transform: translateY(-1px);
130
+ }
131
+
132
+ .preset-button.is-selected,
133
+ .unit-button.is-selected,
134
+ .choice-row button.is-selected {
135
+ border-color: var(--n-blue);
136
+ background: var(--n-blue);
137
+ color: var(--n-white);
138
+ }
139
+
140
+ .unit-toggle {
141
+ margin-top: .75rem;
142
+ }
143
+
144
+ .section-heading,
145
+ .results-topline {
146
+ display: flex;
147
+ align-items: flex-start;
148
+ gap: 1rem;
149
+ }
150
+
151
+ .section-heading {
152
+ display: grid;
153
+ justify-content: start;
154
+ }
155
+
156
+ h2 {
157
+ margin: .25rem 0 0;
158
+ color: var(--n-ink);
159
+ font-size: clamp(1.25rem, 2vw, 1.65rem);
160
+ line-height: 1.1;
161
+ }
162
+
163
+ h2 span {
164
+ color: var(--n-muted);
165
+ font-size: .7rem;
166
+ font-weight: 600;
167
+ letter-spacing: .05em;
168
+ text-transform: uppercase;
169
+ }
170
+
171
+ .input-grid {
172
+ display: grid;
173
+ gap: .85rem;
174
+ grid-template-columns: repeat(4, minmax(0, 1fr));
175
+ margin-top: 1rem;
176
+ }
177
+
178
+ .field,
179
+ .choice-field {
180
+ display: grid;
181
+ align-content: start;
182
+ gap: .4rem;
183
+ min-width: 0;
184
+ }
185
+
186
+ .field > span,
187
+ .choice-field > span {
188
+ color: var(--n-muted);
189
+ font-size: .75rem;
190
+ font-weight: 700;
191
+ }
192
+
193
+ .field {
194
+ position: relative;
195
+ }
196
+
197
+ .field input {
198
+ width: 100%;
199
+ box-sizing: border-box;
200
+ border: 1px solid var(--n-line);
201
+ border-radius: .75rem;
202
+ background: var(--n-paper);
203
+ color: var(--n-ink);
204
+ font-size: 1.05rem;
205
+ font-weight: 800;
206
+ padding: .8rem 2.3rem .8rem .8rem;
207
+ }
208
+
209
+ .field input:focus-visible,
210
+ button:focus-visible {
211
+ outline: 3px solid var(--n-orange);
212
+ outline-offset: 2px;
213
+ }
214
+
215
+ .field small {
216
+ position: absolute;
217
+ right: .75rem;
218
+ bottom: .83rem;
219
+ color: var(--n-muted);
220
+ font-size: .7rem;
221
+ font-weight: 800;
222
+ }
223
+
224
+ .choice-row button {
225
+ flex: 1;
226
+ padding: .7rem .45rem;
227
+ white-space: nowrap;
228
+ }
229
+
230
+ .field-wide {
231
+ grid-column: span 2;
232
+ }
233
+
234
+ .fine-print,
235
+ .notes-row p {
236
+ color: var(--n-muted);
237
+ font-size: .76rem;
238
+ line-height: 1.5;
239
+ }
240
+
241
+ .fine-print {
242
+ margin: 1rem 0 0;
243
+ }
244
+
245
+ .bench-foot {
246
+ display: flex;
247
+ align-items: center;
248
+ justify-content: space-between;
249
+ gap: 1rem;
250
+ }
251
+
252
+ .clear-button {
253
+ flex: 0 0 auto;
254
+ border: 0;
255
+ background: transparent;
256
+ color: var(--n-blue);
257
+ font-size: .75rem;
258
+ font-weight: 800;
259
+ padding: .5rem 0;
260
+ text-decoration: underline;
261
+ text-underline-offset: .2rem;
262
+ }
263
+
264
+ .clear-button:hover {
265
+ color: var(--n-orange);
266
+ }
267
+
268
+ .fine-print span {
269
+ color: var(--n-blue);
270
+ font-weight: 800;
271
+ }
272
+
273
+ .results-card {
274
+ overflow: hidden;
275
+ padding: 1.5rem 0 0;
276
+ }
277
+
278
+ .status-pill {
279
+ border: 1px solid var(--n-green);
280
+ border-radius: 999px;
281
+ background: var(--n-green-soft);
282
+ color: var(--n-green);
283
+ font-size: .75rem;
284
+ font-weight: 800;
285
+ padding: .55rem .75rem;
286
+ text-align: center;
287
+ }
288
+
289
+ [data-status="underpowered"] .status-pill {
290
+ border-color: var(--n-red);
291
+ background: var(--n-red-soft);
292
+ color: var(--n-red);
293
+ }
294
+
295
+ [data-status="workable"] .status-pill {
296
+ border-color: var(--n-orange);
297
+ background: var(--n-orange-soft);
298
+ color: var(--n-orange);
299
+ }
300
+
301
+ .wind-tunnel {
302
+ position: relative;
303
+ min-height: 18rem;
304
+ margin: 1.5rem 0 1rem;
305
+ overflow: hidden;
306
+ border: 1px solid var(--n-line);
307
+ border-radius: 1rem;
308
+ background-color: var(--n-stage);
309
+ background-image: linear-gradient(var(--n-grid) 1px, transparent 1px), linear-gradient(90deg, var(--n-grid) 1px, transparent 1px);
310
+ background-size: 2rem 2rem;
311
+ }
312
+
313
+ .tunnel-label {
314
+ position: absolute;
315
+ z-index: 2;
316
+ color: var(--n-muted);
317
+ font-size: .65rem;
318
+ font-weight: 800;
319
+ letter-spacing: .1em;
320
+ text-transform: uppercase;
321
+ }
322
+
323
+ .tunnel-label-top {
324
+ top: 1rem;
325
+ left: 1rem;
326
+ }
327
+
328
+ .rotor-stage {
329
+ position: absolute;
330
+ top: 50%;
331
+ left: 50%;
332
+ z-index: 3;
333
+ width: 10rem;
334
+ height: 10rem;
335
+ transform: translate(-50%, -50%);
336
+ }
337
+
338
+ .rotor-shadow {
339
+ position: absolute;
340
+ inset: 1rem;
341
+ border-radius: 50%;
342
+ background: var(--n-blue-soft);
343
+ opacity: .7;
344
+ }
345
+
346
+ .rotor-disc {
347
+ position: absolute;
348
+ inset: 1.6rem;
349
+ border: 2px solid var(--n-blue);
350
+ border-radius: 50%;
351
+ background: var(--n-card);
352
+ box-shadow: 0 0 0 .7rem var(--n-blue-soft), 0 14px 28px var(--n-shadow);
353
+ animation: rotor-spin var(--rotor-speed, 150ms) linear infinite;
354
+ }
355
+
356
+ .rotor-blade {
357
+ position: absolute;
358
+ top: 50%;
359
+ left: 50%;
360
+ width: .65rem;
361
+ height: 3.7rem;
362
+ border-radius: 999px 999px .35rem .35rem;
363
+ background: var(--n-orange);
364
+ transform: translate(-50%, -100%);
365
+ transform-origin: 50% 100%;
366
+ }
367
+
368
+ .blade-two { transform: translate(-50%, -100%) rotate(90deg); }
369
+ .blade-three { transform: translate(-50%, -100%) rotate(180deg); }
370
+ .blade-four { transform: translate(-50%, -100%) rotate(270deg); }
371
+
372
+ .rotor-hub {
373
+ position: absolute;
374
+ top: 50%;
375
+ left: 50%;
376
+ width: 1.3rem;
377
+ height: 1.3rem;
378
+ border: .25rem solid var(--n-blue);
379
+ border-radius: 50%;
380
+ background: var(--n-orange-soft);
381
+ transform: translate(-50%, -50%);
382
+ }
383
+
384
+ .rotor-readout {
385
+ position: absolute;
386
+ right: 50%;
387
+ bottom: -3.3rem;
388
+ display: grid;
389
+ gap: .15rem;
390
+ width: 12rem;
391
+ color: var(--n-ink);
392
+ text-align: center;
393
+ transform: translateX(50%);
394
+ }
395
+
396
+ .rotor-readout strong {
397
+ font-size: 1rem;
398
+ }
399
+
400
+ .rotor-readout span {
401
+ color: var(--n-muted);
402
+ font-size: .7rem;
403
+ }
404
+
405
+ .thrust-stream {
406
+ position: absolute;
407
+ top: 50%;
408
+ width: 35%;
409
+ height: 1px;
410
+ background: var(--n-orange);
411
+ opacity: .7;
412
+ }
413
+
414
+ .stream-left {
415
+ left: 3%;
416
+ transform: translateY(-50%) rotate(-8deg);
417
+ }
418
+
419
+ .stream-right {
420
+ right: 3%;
421
+ transform: translateY(-50%) rotate(8deg);
422
+ }
423
+
424
+ .thrust-stream::after {
425
+ position: absolute;
426
+ top: -.2rem;
427
+ width: .4rem;
428
+ height: .4rem;
429
+ border-top: 1px solid var(--n-orange);
430
+ border-right: 1px solid var(--n-orange);
431
+ content: '';
432
+ }
433
+
434
+ .stream-left::after {
435
+ right: 0;
436
+ transform: rotate(45deg);
437
+ }
438
+
439
+ .stream-right::after {
440
+ left: 0;
441
+ transform: rotate(-135deg);
442
+ }
443
+
444
+ .weight-marker {
445
+ position: absolute;
446
+ right: 1rem;
447
+ bottom: 1rem;
448
+ display: flex;
449
+ align-items: center;
450
+ gap: .4rem;
451
+ color: var(--n-muted);
452
+ font-size: .65rem;
453
+ font-weight: 800;
454
+ text-transform: uppercase;
455
+ }
456
+
457
+ .marker-line {
458
+ display: block;
459
+ width: 2.8rem;
460
+ border-top: 2px dashed var(--n-red);
461
+ }
462
+
463
+ .lift-beam {
464
+ position: absolute;
465
+ top: 1.5rem;
466
+ left: 50%;
467
+ width: 2px;
468
+ height: 30%;
469
+ background: var(--n-blue);
470
+ transform: translateX(-50%);
471
+ }
472
+
473
+ .lift-beam span {
474
+ position: absolute;
475
+ bottom: 0;
476
+ left: 50%;
477
+ width: 12rem;
478
+ height: .3rem;
479
+ border-radius: 999px;
480
+ background: var(--n-orange);
481
+ transform: translateX(-50%) scaleX(var(--lift-ratio, .8));
482
+ transform-origin: center;
483
+ }
484
+
485
+ .status-advice {
486
+ margin: 0 0 1rem;
487
+ color: var(--n-muted);
488
+ font-size: .85rem;
489
+ line-height: 1.5;
490
+ }
491
+
492
+ .result-grid {
493
+ display: grid;
494
+ grid-template-columns: repeat(7, minmax(0, 1fr));
495
+ border-top: 1px solid var(--n-line);
496
+ border-left: 1px solid var(--n-line);
497
+ }
498
+
499
+ .result-cell {
500
+ display: grid;
501
+ gap: .4rem;
502
+ min-height: 5.5rem;
503
+ align-content: center;
504
+ border-right: 1px solid var(--n-line);
505
+ border-bottom: 1px solid var(--n-line);
506
+ padding: .75rem;
507
+ }
508
+
509
+ .result-cell span {
510
+ color: var(--n-muted);
511
+ font-size: .67rem;
512
+ font-weight: 700;
513
+ line-height: 1.25;
514
+ }
515
+
516
+ .result-cell strong {
517
+ color: var(--n-ink);
518
+ font-size: 1rem;
519
+ }
520
+
521
+ .result-cell.accent {
522
+ background: var(--n-blue-soft);
523
+ }
524
+
525
+ .result-cell.accent strong {
526
+ color: var(--n-blue);
527
+ font-size: 1.1rem;
528
+ }
529
+
530
+ .notes-row {
531
+ display: grid;
532
+ gap: 1rem;
533
+ grid-template-columns: 1fr 1fr;
534
+ margin-top: 1rem;
535
+ }
536
+
537
+ .notes-row p { margin: 0; }
538
+
539
+ @keyframes rotor-spin {
540
+ to { transform: rotate(360deg); }
541
+ }
542
+
543
+ @media (max-width: 850px) {
544
+ .input-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
545
+ .field-wide { grid-column: span 1; }
546
+ .result-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
547
+ }
548
+
549
+ @media (max-width: 540px) {
550
+ .drone-motor-tool { border-radius: .9rem; }
551
+ .input-grid,
552
+ .notes-row { grid-template-columns: 1fr; }
553
+ .bench-foot {
554
+ align-items: flex-start;
555
+ flex-direction: column;
556
+ gap: .25rem;
557
+ }
558
+ .choice-row button { font-size: .7rem; }
559
+ .wind-tunnel { min-height: 16rem; }
560
+ .rotor-stage { transform: translate(-50%, -50%) scale(.82); }
561
+ .result-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
562
+ }
@@ -0,0 +1,27 @@
1
+ import type { DronesToolEntry, ToolLocaleContent } from '../../types';
2
+ import type { DroneMotorPropellerUI } from './ui';
3
+
4
+ export type { DroneMotorPropellerUI };
5
+ export type DroneMotorPropellerLocaleContent = ToolLocaleContent<DroneMotorPropellerUI>;
6
+
7
+ export const droneMotorPropellerCalculator: DronesToolEntry<DroneMotorPropellerUI> = {
8
+ id: 'drone-motor-propeller-calculator',
9
+ icons: { bg: 'mdi:drone', fg: 'mdi:fan' },
10
+ i18n: {
11
+ es: () => import('./i18n/es').then((module) => module.content),
12
+ en: () => import('./i18n/en').then((module) => module.content),
13
+ fr: () => import('./i18n/fr').then((module) => module.content),
14
+ de: () => import('./i18n/de').then((module) => module.content),
15
+ it: () => import('./i18n/it').then((module) => module.content),
16
+ pt: () => import('./i18n/pt').then((module) => module.content),
17
+ nl: () => import('./i18n/nl').then((module) => module.content),
18
+ pl: () => import('./i18n/pl').then((module) => module.content),
19
+ ru: () => import('./i18n/ru').then((module) => module.content),
20
+ ja: () => import('./i18n/ja').then((module) => module.content),
21
+ ko: () => import('./i18n/ko').then((module) => module.content),
22
+ zh: () => import('./i18n/zh').then((module) => module.content),
23
+ tr: () => import('./i18n/tr').then((module) => module.content),
24
+ sv: () => import('./i18n/sv').then((module) => module.content),
25
+ id: () => import('./i18n/id').then((module) => module.content),
26
+ },
27
+ };
@@ -0,0 +1,14 @@
1
+ import type { DroneMotorResults } from './logic';
2
+
3
+ export type SetupStatus = 'underpowered' | 'workable' | 'headroom';
4
+
5
+ export interface SetupEvaluation {
6
+ status: SetupStatus;
7
+ score: number;
8
+ }
9
+
10
+ export function evaluateSetup(results: DroneMotorResults): SetupEvaluation {
11
+ if (results.thrustToWeight < 2) return { status: 'underpowered', score: results.thrustToWeight };
12
+ if (results.thrustToWeight < 3.5) return { status: 'workable', score: results.thrustToWeight };
13
+ return { status: 'headroom', score: results.thrustToWeight };
14
+ }