@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.
- package/package.json +1 -1
- package/src/category/i18n/de.ts +1 -1
- package/src/category/i18n/en.ts +2 -2
- package/src/category/i18n/es.ts +1 -1
- package/src/category/i18n/fr.ts +2 -2
- package/src/category/i18n/id.ts +1 -1
- package/src/category/i18n/it.ts +1 -1
- package/src/category/i18n/nl.ts +1 -1
- package/src/category/i18n/pl.ts +1 -1
- package/src/category/i18n/pt.ts +1 -1
- package/src/category/i18n/sv.ts +1 -1
- package/src/category/i18n/tr.ts +1 -1
- package/src/components/PreviewNavSidebar.astro +116 -116
- package/src/components/PreviewToolbar.astro +143 -143
- package/src/data.ts +1 -1
- package/src/env.d.ts +1 -1
- package/src/layouts/PreviewLayout.astro +118 -118
- package/src/pages/[locale].astro +251 -251
- package/src/pages/index.astro +4 -4
- package/src/tests/bibliography_wellformed_export.test.ts +46 -0
- package/src/tests/category_seo_quality.test.ts +76 -0
- package/src/tests/faq_count.test.ts +1 -1
- package/src/tests/mocks/astro_mock.js +1 -1
- package/src/tests/no_h1_in_components.test.ts +1 -1
- package/src/tests/seo_translation_completeness.test.ts +69 -0
- package/src/tests/seo_wellformed_export.test.ts +65 -0
- package/src/tests/tool_validation.test.ts +15 -15
- package/src/tool/antenna-length-calculator/bibliography.astro +6 -6
- package/src/tool/antenna-length-calculator/component.astro +329 -329
- package/src/tool/antenna-length-calculator/seo.astro +15 -15
- package/src/tool/drone-battery-c-rating-calculator/i18n/de.ts +192 -192
- package/src/tool/drone-battery-c-rating-calculator/i18n/en.ts +192 -192
- package/src/tool/drone-battery-c-rating-calculator/i18n/es.ts +192 -192
- package/src/tool/drone-battery-c-rating-calculator/i18n/id.ts +192 -192
- package/src/tool/drone-battery-c-rating-calculator/i18n/ja.ts +192 -192
- package/src/tool/drone-battery-c-rating-calculator/i18n/ko.ts +192 -192
- package/src/tool/drone-battery-c-rating-calculator/i18n/nl.ts +192 -192
- package/src/tool/drone-battery-c-rating-calculator/i18n/pl.ts +192 -192
- package/src/tool/drone-battery-c-rating-calculator/i18n/ru.ts +184 -184
- package/src/tool/drone-battery-c-rating-calculator/i18n/sv.ts +189 -189
- package/src/tool/drone-battery-c-rating-calculator/i18n/tr.ts +187 -187
- package/src/tool/drone-battery-c-rating-calculator/i18n/zh.ts +192 -192
- package/src/tool/drone-flight-time/component.astro +187 -187
- package/src/tool/drone-flight-time/seo.astro +15 -15
- package/src/tool/drone-motor-propeller-calculator/i18n/de.ts +1 -1
- package/src/tool/drone-motor-propeller-calculator/i18n/fr.ts +1 -1
- package/src/tool/drone-motor-propeller-calculator/i18n/id.ts +1 -1
- package/src/tool/drone-motor-propeller-calculator/i18n/it.ts +1 -1
- package/src/tool/drone-motor-propeller-calculator/i18n/ja.ts +1 -1
- package/src/tool/drone-motor-propeller-calculator/i18n/ko.ts +1 -1
- package/src/tool/drone-motor-propeller-calculator/i18n/nl.ts +1 -1
- package/src/tool/drone-motor-propeller-calculator/i18n/pl.ts +1 -1
- package/src/tool/drone-motor-propeller-calculator/i18n/pt.ts +1 -1
- package/src/tool/drone-motor-propeller-calculator/i18n/ru.ts +1 -1
- package/src/tool/drone-motor-propeller-calculator/i18n/sv.ts +1 -1
- package/src/tool/drone-motor-propeller-calculator/i18n/tr.ts +1 -1
- package/src/tool/drone-motor-propeller-calculator/i18n/zh.ts +2 -2
- package/src/tool/drone-motor-propeller-calculator/seo.astro +7 -4
- package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/id.ts +1 -1
- package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/it.ts +1 -1
- package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/nl.ts +1 -1
- package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/pt.ts +1 -1
- package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/ru.ts +1 -1
- package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/sv.ts +2 -2
- package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/tr.ts +1 -1
- package/src/tool/gps-coordinates-converter/component.astro +275 -275
- package/src/tool/gps-coordinates-converter/seo.astro +15 -15
package/package.json
CHANGED
package/src/category/i18n/de.ts
CHANGED
|
@@ -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: '
|
|
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',
|
package/src/category/i18n/en.ts
CHANGED
|
@@ -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: '
|
|
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
|
+
|
package/src/category/i18n/es.ts
CHANGED
|
@@ -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: '
|
|
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',
|
package/src/category/i18n/fr.ts
CHANGED
|
@@ -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: '
|
|
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
|
+
|
package/src/category/i18n/id.ts
CHANGED
|
@@ -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: '
|
|
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',
|
package/src/category/i18n/it.ts
CHANGED
|
@@ -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: '
|
|
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',
|
package/src/category/i18n/nl.ts
CHANGED
|
@@ -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: '
|
|
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',
|
package/src/category/i18n/pl.ts
CHANGED
|
@@ -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: '
|
|
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',
|
package/src/category/i18n/pt.ts
CHANGED
|
@@ -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: '
|
|
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',
|
package/src/category/i18n/sv.ts
CHANGED
|
@@ -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: '
|
|
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',
|
package/src/category/i18n/tr.ts
CHANGED
|
@@ -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: '
|
|
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
package/src/env.d.ts
CHANGED