@jjlmoya/utils-drones 1.31.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 (67) hide show
  1. package/package.json +1 -1
  2. package/src/category/i18n/de.ts +1 -1
  3. package/src/category/i18n/en.ts +2 -2
  4. package/src/category/i18n/es.ts +1 -1
  5. package/src/category/i18n/fr.ts +2 -2
  6. package/src/category/i18n/id.ts +1 -1
  7. package/src/category/i18n/it.ts +1 -1
  8. package/src/category/i18n/nl.ts +1 -1
  9. package/src/category/i18n/pl.ts +1 -1
  10. package/src/category/i18n/pt.ts +1 -1
  11. package/src/category/i18n/sv.ts +1 -1
  12. package/src/category/i18n/tr.ts +1 -1
  13. package/src/components/PreviewNavSidebar.astro +116 -116
  14. package/src/components/PreviewToolbar.astro +143 -143
  15. package/src/data.ts +1 -1
  16. package/src/env.d.ts +1 -1
  17. package/src/layouts/PreviewLayout.astro +118 -118
  18. package/src/pages/[locale].astro +251 -251
  19. package/src/pages/index.astro +4 -4
  20. package/src/tests/bibliography_wellformed_export.test.ts +46 -0
  21. package/src/tests/category_seo_quality.test.ts +76 -0
  22. package/src/tests/faq_count.test.ts +1 -1
  23. package/src/tests/mocks/astro_mock.js +1 -1
  24. package/src/tests/no_h1_in_components.test.ts +1 -1
  25. package/src/tests/seo_translation_completeness.test.ts +69 -0
  26. package/src/tests/seo_wellformed_export.test.ts +65 -0
  27. package/src/tests/tool_validation.test.ts +15 -15
  28. package/src/tool/antenna-length-calculator/bibliography.astro +6 -6
  29. package/src/tool/antenna-length-calculator/component.astro +329 -329
  30. package/src/tool/antenna-length-calculator/seo.astro +15 -15
  31. package/src/tool/drone-battery-c-rating-calculator/i18n/de.ts +192 -192
  32. package/src/tool/drone-battery-c-rating-calculator/i18n/en.ts +192 -192
  33. package/src/tool/drone-battery-c-rating-calculator/i18n/es.ts +192 -192
  34. package/src/tool/drone-battery-c-rating-calculator/i18n/id.ts +192 -192
  35. package/src/tool/drone-battery-c-rating-calculator/i18n/ja.ts +192 -192
  36. package/src/tool/drone-battery-c-rating-calculator/i18n/ko.ts +192 -192
  37. package/src/tool/drone-battery-c-rating-calculator/i18n/nl.ts +192 -192
  38. package/src/tool/drone-battery-c-rating-calculator/i18n/pl.ts +192 -192
  39. package/src/tool/drone-battery-c-rating-calculator/i18n/ru.ts +184 -184
  40. package/src/tool/drone-battery-c-rating-calculator/i18n/sv.ts +189 -189
  41. package/src/tool/drone-battery-c-rating-calculator/i18n/tr.ts +187 -187
  42. package/src/tool/drone-battery-c-rating-calculator/i18n/zh.ts +192 -192
  43. package/src/tool/drone-flight-time/component.astro +187 -187
  44. package/src/tool/drone-flight-time/seo.astro +15 -15
  45. package/src/tool/drone-motor-propeller-calculator/i18n/de.ts +1 -1
  46. package/src/tool/drone-motor-propeller-calculator/i18n/fr.ts +1 -1
  47. package/src/tool/drone-motor-propeller-calculator/i18n/id.ts +1 -1
  48. package/src/tool/drone-motor-propeller-calculator/i18n/it.ts +1 -1
  49. package/src/tool/drone-motor-propeller-calculator/i18n/ja.ts +1 -1
  50. package/src/tool/drone-motor-propeller-calculator/i18n/ko.ts +1 -1
  51. package/src/tool/drone-motor-propeller-calculator/i18n/nl.ts +1 -1
  52. package/src/tool/drone-motor-propeller-calculator/i18n/pl.ts +1 -1
  53. package/src/tool/drone-motor-propeller-calculator/i18n/pt.ts +1 -1
  54. package/src/tool/drone-motor-propeller-calculator/i18n/ru.ts +1 -1
  55. package/src/tool/drone-motor-propeller-calculator/i18n/sv.ts +1 -1
  56. package/src/tool/drone-motor-propeller-calculator/i18n/tr.ts +1 -1
  57. package/src/tool/drone-motor-propeller-calculator/i18n/zh.ts +2 -2
  58. package/src/tool/drone-motor-propeller-calculator/seo.astro +7 -4
  59. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/id.ts +1 -1
  60. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/it.ts +1 -1
  61. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/nl.ts +1 -1
  62. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/pt.ts +1 -1
  63. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/ru.ts +1 -1
  64. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/sv.ts +2 -2
  65. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/tr.ts +1 -1
  66. package/src/tool/gps-coordinates-converter/component.astro +275 -275
  67. package/src/tool/gps-coordinates-converter/seo.astro +15 -15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-drones",
3
- "version": "1.31.0",
3
+ "version": "1.33.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -3,7 +3,7 @@ import type { CategoryLocaleContent } from '../../types';
3
3
  export const content: CategoryLocaleContent = {
4
4
  slug: 'drones',
5
5
  title: 'Drohnen und Funkamateur Tools & Rechner',
6
- description: 'Optimieren Sie Ihre Flüge und Kommunikation mit kostenlosen Online-Tools. Flugzeitrechner, Akkulaufzeit-Schätzer, GPS-Koordinaten-Konverter und Antennenlängen-Rechner für Hochfrequenz.',
6
+ description: 'Kostenlose Online-Rechner für Drohnenflüge und Funk: Flugzeit, Akkulaufzeit, GPS-Koordinaten und Antennenlänge schnell prüfen.',
7
7
  seo: [
8
8
  {
9
9
  type: 'title',
@@ -3,7 +3,7 @@ import type { CategoryLocaleContent } from '../../types';
3
3
  export const content: CategoryLocaleContent = {
4
4
  slug: 'drones',
5
5
  title: 'Drone and Ham Radio Tools and Calculators',
6
- description: 'Optimize your flight and communications with free online tools. Flight time calculators, battery life estimations, GPS coordinate converters, and antenna length for radio frequency.',
6
+ description: 'Free online tools for drone flight and radio planning: estimate flight time, battery life, GPS coordinates, and antenna length.',
7
7
  seo: [
8
8
  {
9
9
  type: 'title',
@@ -104,4 +104,4 @@ export const content: CategoryLocaleContent = {
104
104
  },
105
105
  ],
106
106
  };
107
-
107
+
@@ -3,7 +3,7 @@ import type { CategoryLocaleContent } from '../../types';
3
3
  export const content: CategoryLocaleContent = {
4
4
  slug: 'drones',
5
5
  title: 'Herramientas y Calculadoras para Drones y Radioaficionados',
6
- description: 'Optimiza tus vuelos y tus comunicaciones con herramientas gratuitas online. Calculadoras de tiempo de vuelo, autonomía batería, conversores de coordenadas GPS y longitudes de antena para radiofrecuencia.',
6
+ description: 'Herramientas online gratuitas para planificar vuelos de drones y radio: calcula tiempo de vuelo, batería, coordenadas GPS y antenas.',
7
7
  seo: [
8
8
  {
9
9
  type: 'title',
@@ -3,7 +3,7 @@
3
3
  export const content: CategoryLocaleContent = {
4
4
  slug: 'drones',
5
5
  title: 'Outils et Calculateurs pour Drones et Radioamateurs',
6
- description: 'Optimisez vos vols et vos communications avec des outils en ligne gratuits. Calculateurs de temps de vol, autonomie de batterie, convertisseurs de coordonnées GPS et longueurs d\'antenne pour la radiofréquence.',
6
+ description: 'Outils en ligne gratuits pour préparer vos vols et votre radio; durée de vol, autonomie, coordonnées GPS et longueur d\'antenne.',
7
7
  seo: [
8
8
  {
9
9
  type: 'title',
@@ -104,4 +104,4 @@ export const content: CategoryLocaleContent = {
104
104
  },
105
105
  ],
106
106
  };
107
-
107
+
@@ -3,7 +3,7 @@ import type { CategoryLocaleContent } from '../../types';
3
3
  export const content: CategoryLocaleContent = {
4
4
  slug: 'drones',
5
5
  title: 'Alat dan Kalkulator untuk Drone dan Amatir Radio',
6
- description: 'Optimalkan penerbangan dan komunikasi Anda dengan alat online gratis. Kalkulator waktu terbang, estimasi otonomi baterai, konverter koordinat GPS, dan panjang antena untuk frekuensi radio.',
6
+ description: 'Alat online gratis untuk merencanakan penerbangan drone dan radio: hitung waktu terbang, daya baterai, koordinat GPS, dan panjang antena.',
7
7
  seo: [
8
8
  {
9
9
  type: 'title',
@@ -3,7 +3,7 @@ import type { CategoryLocaleContent } from '../../types';
3
3
  export const content: CategoryLocaleContent = {
4
4
  slug: 'drones',
5
5
  title: 'Strumenti e Calcolatori per Droni e Radioamatori',
6
- description: 'Ottimizza i tuoi voli e le tue comunicazioni con strumenti online gratuiti. Calcolatori del tempo di volo, autonomia della batteria, convertitori di coordinate GPS e lunghezze d\'antenna per radiofrequenza.',
6
+ description: 'Strumenti online gratuiti per pianificare voli e radio: stima durata del volo, autonomia, coordinate GPS e lunghezza dell\'antenna.',
7
7
  seo: [
8
8
  {
9
9
  type: 'title',
@@ -3,7 +3,7 @@ import type { CategoryLocaleContent } from '../../types';
3
3
  export const content: CategoryLocaleContent = {
4
4
  slug: 'drones',
5
5
  title: 'Tools en Calculators voor Drones en Radioamateurs',
6
- description: 'Optimaliseer uw vluchten en communicatie met gratis online tools. Flugtijdcalculators, batterijduurmeters, GPS-coördinatenconverters en antennelengtecalculators voor radiofrequenties.',
6
+ description: 'Gratis online tools voor dronevluchten en radio: bereken vliegtijd, batterijduur, GPS-coördinaten en antennelengte.',
7
7
  seo: [
8
8
  {
9
9
  type: 'title',
@@ -3,7 +3,7 @@ import type { CategoryLocaleContent } from '../../types';
3
3
  export const content: CategoryLocaleContent = {
4
4
  slug: 'drones',
5
5
  title: 'Narzędzia i Kalkulatory dla Dronów i Krótkofalowców',
6
- description: 'Optymaulizuj swoje loty i komunikację dzięki bezpłatnym narzędziom online. Kalkulatory czasu lotu, autonomii baterii, konwertery współrzędnych GPS i długości anten dla radiofrekwencji.',
6
+ description: 'Bezpłatne narzędzia online do planowania lotów dronem i łączności: czas lotu, bateria, współrzędne GPS oraz długość anteny.',
7
7
  seo: [
8
8
  {
9
9
  type: 'title',
@@ -3,7 +3,7 @@ import type { CategoryLocaleContent } from '../../types';
3
3
  export const content: CategoryLocaleContent = {
4
4
  slug: 'drones',
5
5
  title: 'Ferramentas e Calculadoras para Drones e Radioamadores',
6
- description: 'Otimize seus voos e comunicações com ferramentas online gratuitas. Calculadoras de tempo de voo, autonomia de bateria, conversores de coordenadas GPS e comprimentos de antena para radiofrequência.',
6
+ description: 'Ferramentas online gratuitas para planejar voos e rádio: estime tempo de voo, bateria, coordenadas GPS e comprimento da antena.',
7
7
  seo: [
8
8
  {
9
9
  type: 'title',
@@ -3,7 +3,7 @@ import type { CategoryLocaleContent } from '../../types';
3
3
  export const content: CategoryLocaleContent = {
4
4
  slug: 'drones',
5
5
  title: 'Verktyg och Kalkylatorer för Drönare och Radioamatörer',
6
- description: 'Optimera dina flygningar och din kommunikation med kostnadsfria onlineverktyg. Flygtidskalkylatorer, batteritidsestimering, GPS-koordinatkonverterare och antennlängder för radiofrekvens.',
6
+ description: 'Gratis onlineverktyg för drönarflygning och radio: beräkna flygtid, batteritid, GPS-koordinater och antennlängd.',
7
7
  seo: [
8
8
  {
9
9
  type: 'title',
@@ -3,7 +3,7 @@ import type { CategoryLocaleContent } from '../../types';
3
3
  export const content: CategoryLocaleContent = {
4
4
  slug: 'drones',
5
5
  title: 'Drone ve Radyo Amatörleri İçin Araçlar ve Hesaplayıcılar',
6
- description: 'Ücretsiz çevrimiçi araçlarla uçuşlarınızı ve iletişiminizi optimize edin. Uçuş süresi hesaplayıcıları, pil ömrü tahmini, GPS koordinat dönüştürücüleri ve radyo frekansı için anten uzunlukları.',
6
+ description: 'Drone uçuşu ve telsiz planlaması için ücretsiz araçlar: uçuş süresini, pil ömrünü, GPS koordinatlarını ve anten uzunluğunu hesaplayın.',
7
7
  seo: [
8
8
  {
9
9
  type: 'title',
@@ -1,116 +1,116 @@
1
- ---
2
- interface NavItem {
3
- id: string;
4
- title: string;
5
- href: string;
6
- isActive?: boolean;
7
- }
8
-
9
- interface Props {
10
- categoryTitle: string;
11
- tools?: NavItem[];
12
- }
13
-
14
- const { categoryTitle, tools = [] } = Astro.props;
15
- ---
16
-
17
- <nav class="preview-nav-sidebar">
18
- <div class="sidebar-header">
19
- <h3>{categoryTitle}</h3>
20
- </div>
21
-
22
- <ul class="tools-list">
23
- {tools.map((tool) => (
24
- <li>
25
- <a
26
- href={tool.href}
27
- class:list={['tool-link', { active: tool.isActive }]}
28
- >
29
- <span class="tool-title">{tool.title}</span>
30
- </a>
31
- </li>
32
- ))}
33
- </ul>
34
- </nav>
35
-
36
- <style>
37
- .preview-nav-sidebar {
38
- display: flex;
39
- flex-direction: column;
40
- height: 100%;
41
- background: var(--bg-surface, #0f172a);
42
- }
43
-
44
- .sidebar-header {
45
- padding: 2rem 1.5rem 1.5rem;
46
- border-bottom: 1px solid var(--border-color, #1e293b);
47
- }
48
-
49
- .sidebar-header h3 {
50
- margin: 0;
51
- font-size: 0.75rem;
52
- font-weight: 900;
53
- text-transform: uppercase;
54
- letter-spacing: 0.1em;
55
- color: var(--text-muted, #94a3b8);
56
- }
57
-
58
- .tools-list {
59
- list-style: none;
60
- margin: 0;
61
- padding: 0.5rem 0;
62
- display: flex;
63
- flex-direction: column;
64
- gap: 0.25rem;
65
- }
66
-
67
- .tools-list li {
68
- margin: 0;
69
- }
70
-
71
- .tool-link {
72
- display: flex;
73
- align-items: center;
74
- padding: 0.75rem 1.5rem;
75
- color: var(--text-muted, #94a3b8);
76
- text-decoration: none;
77
- font-size: 0.9375rem;
78
- font-weight: 500;
79
- transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
80
- border-left: 3px solid transparent;
81
- position: relative;
82
- }
83
-
84
- .tool-link:hover {
85
- color: var(--text-base, #f1f5f9);
86
- background: rgba(255, 255, 255, 0.08);
87
- padding-left: 1.75rem;
88
- }
89
-
90
- .tool-link.active {
91
- color: var(--accent, #f43f5e);
92
- background: rgba(244, 63, 94, 0.15);
93
- border-left-color: var(--accent, #f43f5e);
94
- font-weight: 600;
95
- }
96
-
97
- .tool-link.active::before {
98
- content: '';
99
- position: absolute;
100
- left: 0;
101
- top: 50%;
102
- transform: translateY(-50%);
103
- width: 3px;
104
- height: 24px;
105
- background: var(--accent, #f43f5e);
106
- border-radius: 0 2px 2px 0;
107
- }
108
-
109
- .tool-title {
110
- display: block;
111
- overflow: hidden;
112
- text-overflow: ellipsis;
113
- white-space: nowrap;
114
- }
115
- </style>
116
-
1
+ ---
2
+ interface NavItem {
3
+ id: string;
4
+ title: string;
5
+ href: string;
6
+ isActive?: boolean;
7
+ }
8
+
9
+ interface Props {
10
+ categoryTitle: string;
11
+ tools?: NavItem[];
12
+ }
13
+
14
+ const { categoryTitle, tools = [] } = Astro.props;
15
+ ---
16
+
17
+ <nav class="preview-nav-sidebar">
18
+ <div class="sidebar-header">
19
+ <h3>{categoryTitle}</h3>
20
+ </div>
21
+
22
+ <ul class="tools-list">
23
+ {tools.map((tool) => (
24
+ <li>
25
+ <a
26
+ href={tool.href}
27
+ class:list={['tool-link', { active: tool.isActive }]}
28
+ >
29
+ <span class="tool-title">{tool.title}</span>
30
+ </a>
31
+ </li>
32
+ ))}
33
+ </ul>
34
+ </nav>
35
+
36
+ <style>
37
+ .preview-nav-sidebar {
38
+ display: flex;
39
+ flex-direction: column;
40
+ height: 100%;
41
+ background: var(--bg-surface, #0f172a);
42
+ }
43
+
44
+ .sidebar-header {
45
+ padding: 2rem 1.5rem 1.5rem;
46
+ border-bottom: 1px solid var(--border-color, #1e293b);
47
+ }
48
+
49
+ .sidebar-header h3 {
50
+ margin: 0;
51
+ font-size: 0.75rem;
52
+ font-weight: 900;
53
+ text-transform: uppercase;
54
+ letter-spacing: 0.1em;
55
+ color: var(--text-muted, #94a3b8);
56
+ }
57
+
58
+ .tools-list {
59
+ list-style: none;
60
+ margin: 0;
61
+ padding: 0.5rem 0;
62
+ display: flex;
63
+ flex-direction: column;
64
+ gap: 0.25rem;
65
+ }
66
+
67
+ .tools-list li {
68
+ margin: 0;
69
+ }
70
+
71
+ .tool-link {
72
+ display: flex;
73
+ align-items: center;
74
+ padding: 0.75rem 1.5rem;
75
+ color: var(--text-muted, #94a3b8);
76
+ text-decoration: none;
77
+ font-size: 0.9375rem;
78
+ font-weight: 500;
79
+ transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
80
+ border-left: 3px solid transparent;
81
+ position: relative;
82
+ }
83
+
84
+ .tool-link:hover {
85
+ color: var(--text-base, #f1f5f9);
86
+ background: rgba(255, 255, 255, 0.08);
87
+ padding-left: 1.75rem;
88
+ }
89
+
90
+ .tool-link.active {
91
+ color: var(--accent, #f43f5e);
92
+ background: rgba(244, 63, 94, 0.15);
93
+ border-left-color: var(--accent, #f43f5e);
94
+ font-weight: 600;
95
+ }
96
+
97
+ .tool-link.active::before {
98
+ content: '';
99
+ position: absolute;
100
+ left: 0;
101
+ top: 50%;
102
+ transform: translateY(-50%);
103
+ width: 3px;
104
+ height: 24px;
105
+ background: var(--accent, #f43f5e);
106
+ border-radius: 0 2px 2px 0;
107
+ }
108
+
109
+ .tool-title {
110
+ display: block;
111
+ overflow: hidden;
112
+ text-overflow: ellipsis;
113
+ white-space: nowrap;
114
+ }
115
+ </style>
116
+
@@ -1,143 +1,143 @@
1
- ---
2
- import type { KnownLocale } from '../types';
3
-
4
- interface Props {
5
- currentLocale: KnownLocale;
6
- localeUrls?: Partial<Record<KnownLocale, string>>;
7
- }
8
-
9
- const { currentLocale, localeUrls = {} } = Astro.props;
10
-
11
- const otherLocales = Object.entries(localeUrls).filter(([l]) => l !== currentLocale) as [
12
- KnownLocale,
13
- string,
14
- ][];
15
- ---
16
-
17
- <nav class="preview-toolbar">
18
- {
19
- otherLocales.length > 0 && (
20
- <div class="locale-group">
21
- <span class="locale-current">{currentLocale.toUpperCase()}</span>
22
- {otherLocales.map(([locale, url]) => (
23
- <a href={url} class="btn-locale">
24
- {locale.toUpperCase()}
25
- </a>
26
- ))}
27
- </div>
28
- )
29
- }
30
-
31
- <button id="theme-toggle" class="btn-theme" aria-label="Toggle theme" title="Toggle theme">
32
- <span class="theme-icon"></span>
33
- </button>
34
- </nav>
35
-
36
- <script>
37
- function initToolbar() {
38
- const btn = document.getElementById('theme-toggle');
39
- if (!btn) return;
40
-
41
- const applyTheme = (theme: string) => {
42
- document.documentElement.classList.remove('theme-light', 'theme-dark');
43
- document.documentElement.classList.add(theme);
44
- localStorage.setItem('theme', theme);
45
- };
46
-
47
- btn.addEventListener('click', () => {
48
- const next = document.documentElement.classList.contains('theme-dark')
49
- ? 'theme-light'
50
- : 'theme-dark';
51
- applyTheme(next);
52
- });
53
- }
54
-
55
- initToolbar();
56
- document.addEventListener('astro:after-swap', initToolbar);
57
- </script>
58
-
59
- <style>
60
- .preview-toolbar {
61
- position: fixed;
62
- top: 1rem;
63
- right: 1.5rem;
64
- display: flex;
65
- align-items: center;
66
- gap: 0.5rem;
67
- z-index: 1000;
68
- background: var(--bg-surface);
69
- padding: 0.4rem;
70
- border-radius: 1rem;
71
- border: 1px solid var(--border-color);
72
- backdrop-filter: blur(12px);
73
- }
74
-
75
- .locale-group {
76
- display: flex;
77
- align-items: center;
78
- gap: 0.25rem;
79
- padding-right: 0.5rem;
80
- border-right: 1px solid var(--border-color);
81
- }
82
-
83
- .locale-current {
84
- font-size: 0.7rem;
85
- font-weight: 900;
86
- color: var(--text-muted);
87
- padding: 0.4rem 0.6rem;
88
- letter-spacing: 0.08em;
89
- }
90
-
91
- .btn-locale {
92
- background: transparent;
93
- border: 1px solid var(--border-color);
94
- color: var(--text-main);
95
- cursor: pointer;
96
- padding: 0.4rem 0.7rem;
97
- border-radius: 0.5rem;
98
- font-weight: 700;
99
- font-size: 0.7rem;
100
- text-decoration: none;
101
- letter-spacing: 0.08em;
102
- transition: all 0.2s ease;
103
- }
104
-
105
- .btn-locale:hover {
106
- border-color: var(--accent);
107
- color: var(--accent);
108
- }
109
-
110
- .btn-theme {
111
- background: transparent;
112
- border: none;
113
- cursor: pointer;
114
- padding: 0.5rem;
115
- border-radius: 0.6rem;
116
- display: flex;
117
- align-items: center;
118
- justify-content: center;
119
- transition: background 0.2s ease;
120
- }
121
-
122
- .btn-theme:hover {
123
- background: rgba(255, 255, 255, 0.08);
124
- }
125
-
126
- .theme-icon {
127
- display: block;
128
- width: 16px;
129
- height: 16px;
130
- border-radius: 50%;
131
- }
132
-
133
- :global(.theme-light) .theme-icon {
134
- background: #f59e0b;
135
- box-shadow: 0 0 8px #fbbf24;
136
- }
137
-
138
- :global(.theme-dark) .theme-icon {
139
- background: #94a3b8;
140
- box-shadow: inset -4px -2px 0 #1e293b;
141
- }
142
- </style>
143
-
1
+ ---
2
+ import type { KnownLocale } from '../types';
3
+
4
+ interface Props {
5
+ currentLocale: KnownLocale;
6
+ localeUrls?: Partial<Record<KnownLocale, string>>;
7
+ }
8
+
9
+ const { currentLocale, localeUrls = {} } = Astro.props;
10
+
11
+ const otherLocales = Object.entries(localeUrls).filter(([l]) => l !== currentLocale) as [
12
+ KnownLocale,
13
+ string,
14
+ ][];
15
+ ---
16
+
17
+ <nav class="preview-toolbar">
18
+ {
19
+ otherLocales.length > 0 && (
20
+ <div class="locale-group">
21
+ <span class="locale-current">{currentLocale.toUpperCase()}</span>
22
+ {otherLocales.map(([locale, url]) => (
23
+ <a href={url} class="btn-locale">
24
+ {locale.toUpperCase()}
25
+ </a>
26
+ ))}
27
+ </div>
28
+ )
29
+ }
30
+
31
+ <button id="theme-toggle" class="btn-theme" aria-label="Toggle theme" title="Toggle theme">
32
+ <span class="theme-icon"></span>
33
+ </button>
34
+ </nav>
35
+
36
+ <script>
37
+ function initToolbar() {
38
+ const btn = document.getElementById('theme-toggle');
39
+ if (!btn) return;
40
+
41
+ const applyTheme = (theme: string) => {
42
+ document.documentElement.classList.remove('theme-light', 'theme-dark');
43
+ document.documentElement.classList.add(theme);
44
+ localStorage.setItem('theme', theme);
45
+ };
46
+
47
+ btn.addEventListener('click', () => {
48
+ const next = document.documentElement.classList.contains('theme-dark')
49
+ ? 'theme-light'
50
+ : 'theme-dark';
51
+ applyTheme(next);
52
+ });
53
+ }
54
+
55
+ initToolbar();
56
+ document.addEventListener('astro:after-swap', initToolbar);
57
+ </script>
58
+
59
+ <style>
60
+ .preview-toolbar {
61
+ position: fixed;
62
+ top: 1rem;
63
+ right: 1.5rem;
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 0.5rem;
67
+ z-index: 1000;
68
+ background: var(--bg-surface);
69
+ padding: 0.4rem;
70
+ border-radius: 1rem;
71
+ border: 1px solid var(--border-color);
72
+ backdrop-filter: blur(12px);
73
+ }
74
+
75
+ .locale-group {
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 0.25rem;
79
+ padding-right: 0.5rem;
80
+ border-right: 1px solid var(--border-color);
81
+ }
82
+
83
+ .locale-current {
84
+ font-size: 0.7rem;
85
+ font-weight: 900;
86
+ color: var(--text-muted);
87
+ padding: 0.4rem 0.6rem;
88
+ letter-spacing: 0.08em;
89
+ }
90
+
91
+ .btn-locale {
92
+ background: transparent;
93
+ border: 1px solid var(--border-color);
94
+ color: var(--text-main);
95
+ cursor: pointer;
96
+ padding: 0.4rem 0.7rem;
97
+ border-radius: 0.5rem;
98
+ font-weight: 700;
99
+ font-size: 0.7rem;
100
+ text-decoration: none;
101
+ letter-spacing: 0.08em;
102
+ transition: all 0.2s ease;
103
+ }
104
+
105
+ .btn-locale:hover {
106
+ border-color: var(--accent);
107
+ color: var(--accent);
108
+ }
109
+
110
+ .btn-theme {
111
+ background: transparent;
112
+ border: none;
113
+ cursor: pointer;
114
+ padding: 0.5rem;
115
+ border-radius: 0.6rem;
116
+ display: flex;
117
+ align-items: center;
118
+ justify-content: center;
119
+ transition: background 0.2s ease;
120
+ }
121
+
122
+ .btn-theme:hover {
123
+ background: rgba(255, 255, 255, 0.08);
124
+ }
125
+
126
+ .theme-icon {
127
+ display: block;
128
+ width: 16px;
129
+ height: 16px;
130
+ border-radius: 50%;
131
+ }
132
+
133
+ :global(.theme-light) .theme-icon {
134
+ background: #f59e0b;
135
+ box-shadow: 0 0 8px #fbbf24;
136
+ }
137
+
138
+ :global(.theme-dark) .theme-icon {
139
+ background: #94a3b8;
140
+ box-shadow: inset -4px -2px 0 #1e293b;
141
+ }
142
+ </style>
143
+
package/src/data.ts CHANGED
@@ -8,4 +8,4 @@ export type {
8
8
  DronesToolEntry,
9
9
  DronesCategoryEntry,
10
10
  } from './types';
11
-
11
+
package/src/env.d.ts CHANGED
@@ -2,4 +2,4 @@ declare module '*.astro' {
2
2
  const Component: (_props: Record<string, unknown>) => unknown;
3
3
  export default Component;
4
4
  }
5
-
5
+