@jjlmoya/utils-games-development 1.65.0 → 1.67.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 +67 -67
- package/scripts/postinstall.mjs +27 -27
- package/src/category/GamesCategorySEO.astro +19 -19
- package/src/category/i18n/de.ts +15 -15
- package/src/category/i18n/en.ts +15 -15
- package/src/category/i18n/es.ts +15 -15
- package/src/category/i18n/fr.ts +15 -15
- package/src/category/i18n/id.ts +10 -10
- package/src/category/i18n/it.ts +10 -10
- package/src/category/i18n/ja.ts +10 -10
- package/src/category/i18n/ko.ts +10 -10
- package/src/category/i18n/nl.ts +10 -10
- package/src/category/i18n/pl.ts +10 -10
- package/src/category/i18n/pt.ts +10 -10
- package/src/category/i18n/ru.ts +10 -10
- package/src/category/i18n/sv.ts +10 -10
- package/src/category/i18n/tr.ts +10 -10
- package/src/category/i18n/zh.ts +10 -10
- package/src/category/seo.astro +15 -15
- package/src/components/PreviewNavSidebar.astro +116 -116
- package/src/components/PreviewToolbar.astro +143 -143
- package/src/data.ts +11 -11
- package/src/env.d.ts +5 -5
- package/src/index.ts +20 -20
- package/src/layouts/PreviewLayout.astro +122 -122
- package/src/pages/[locale]/[slug].astro +165 -165
- package/src/pages/[locale].astro +250 -250
- 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 +74 -0
- package/src/tests/diacritics_density.test.ts +118 -118
- package/src/tests/faq_count.test.ts +19 -19
- package/src/tests/i18n_coverage.test.ts +36 -36
- package/src/tests/inverted_punctuation.test.ts +84 -84
- package/src/tests/mocks/astro_mock.js +2 -2
- package/src/tests/no_en_dash.test.ts +70 -70
- package/src/tests/no_h1_in_components.test.ts +48 -48
- package/src/tests/pagespeed_best_practices.test.ts +198 -198
- package/src/tests/qa-test-helpers.ts +32 -32
- package/src/tests/qa_bibliography_links.test.ts +41 -41
- package/src/tests/qa_claim_evidence.test.ts +69 -69
- package/src/tests/qa_logic_reference_coverage.test.ts +46 -46
- package/src/tests/qa_runtime_i18n.test.ts +100 -100
- package/src/tests/schemas_fulfillment.test.ts +23 -23
- package/src/tests/script_density.test.ts +94 -94
- package/src/tests/seo_length.test.ts +23 -23
- package/src/tests/seo_wellformed_export.test.ts +65 -0
- package/src/tests/slug_language_code_format.test.ts +23 -23
- package/src/tests/slug_uniqueness.test.ts +80 -80
- package/src/tests/title_quality.test.ts +56 -56
- package/src/tool/audioLoopPointFinder/audio-loop-point-finder.css +322 -322
- package/src/tool/audioLoopPointFinder/client.ts +262 -262
- package/src/tool/damageFormulaLab/bibliography.astro +3 -21
- package/src/tool/damageFormulaLab/seo.astro +7 -5
- package/src/tool/gameDeltaTimeFixedTimestepLab/bibliography.astro +3 -19
- package/src/tool/hitboxHurtboxAnimator/hitbox-hurtbox-animator.css +614 -614
- package/src/tool/hitboxHurtboxAnimator/seo.astro +7 -5
- package/src/tool/saveFileEditor/component.astro +351 -351
- package/src/tool/saveFileEditor/game-save-file-editor.css +250 -250
- package/src/tool/spriteSheetPacker/app-client.ts +248 -248
- package/src/tool/spriteSheetPacker/bibliography.ts +12 -12
- package/src/tool/spriteSheetPacker/component.astro +229 -229
- package/src/tool/spriteSheetPacker/dropzone-client.ts +55 -55
- package/src/tool/spriteSheetPacker/entry.ts +28 -28
- package/src/tool/spriteSheetPacker/exporter.ts +116 -116
- package/src/tool/spriteSheetPacker/extractor-client.ts +127 -127
- package/src/tool/spriteSheetPacker/flipbook-client.ts +60 -60
- package/src/tool/spriteSheetPacker/logic.test.ts +155 -155
- package/src/tool/spriteSheetPacker/logic.ts +32 -32
- package/src/tool/spriteSheetPacker/packer-core.ts +121 -121
- package/src/tool/spriteSheetPacker/packer-ui.ts +86 -86
- package/src/tool/spriteSheetPacker/sprite-sheet-packer.css +578 -578
- package/src/tool/spriteSheetPacker/types.ts +89 -89
- package/src/tool/spriteSheetPacker/ui.ts +42 -42
- package/src/tool/steamCapsuleGenerator/index.ts +9 -9
- package/src/tool/steamCapsuleGenerator/validation.ts +14 -14
- package/src/types.ts +72 -72
package/src/category/i18n/tr.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { CategoryLocaleContent } from '../../types';
|
|
2
|
-
|
|
3
|
-
export const content: CategoryLocaleContent = { slug: 'oyun-gelistirme', title: 'Oyun geliştirme araçları', description: 'Mağaza görsellerinden üretim teslimlerine kadar bağımsız oyun üreticileri için pratik tarayıcı araçları.', seo: [
|
|
4
|
-
{ type: 'title', text: 'Kendi oyunlarınızı üretmek için araçlar', level: 2 },
|
|
5
|
-
{ type: 'paragraph', html: 'Oyun geliştirme birçok küçük karardan oluşur: bir kompozisyon farklı mağaza biçimlerinde çalışmalı, bir simge ilk bakışta okunmalı ve üretim görevi tekrarlanabilmelidir. Bu kategori oyun tasarlayan, yayınlayan ve tanıtan kişiler için odaklanmış araçlar sunar.' },
|
|
6
|
-
{ type: 'title', text: 'Bağımsız üreticiler için', level: 2 },
|
|
7
|
-
{ type: 'paragraph', html: 'Bağımsız ekipler grafik yazılımı, yayın panosu ve inceleme klasörleri arasında sıkça geçiş yapar. İyi bir araç bu aktarımı kısaltır, önemli kararları görünür kılar ve kaynak dosyaların kontrolünü üreticide bırakır.' },
|
|
8
|
-
{ type: 'list', items: ['Görünür sonuçlara sahip odaklı akışlar', 'Uygun olduğunda yerel işleme', 'Net boyutlar ve dışa aktarma durumları', 'Resmî platform belgelerini tamamlayan bilgiler'] },
|
|
9
|
-
{ type: 'tip', html: 'Bu araçları ön kontrol katmanı olarak kullanın. Kaynakları saklayın ve dışa aktarımları platformun güncel şartlarıyla karşılaştırın.' },
|
|
10
|
-
] };
|
|
1
|
+
import type { CategoryLocaleContent } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const content: CategoryLocaleContent = { slug: 'oyun-gelistirme', title: 'Oyun geliştirme araçları', description: 'Mağaza görsellerinden üretim teslimlerine kadar bağımsız oyun üreticileri için pratik tarayıcı araçları.', seo: [
|
|
4
|
+
{ type: 'title', text: 'Kendi oyunlarınızı üretmek için araçlar', level: 2 },
|
|
5
|
+
{ type: 'paragraph', html: 'Oyun geliştirme birçok küçük karardan oluşur: bir kompozisyon farklı mağaza biçimlerinde çalışmalı, bir simge ilk bakışta okunmalı ve üretim görevi tekrarlanabilmelidir. Bu kategori oyun tasarlayan, yayınlayan ve tanıtan kişiler için odaklanmış araçlar sunar.' },
|
|
6
|
+
{ type: 'title', text: 'Bağımsız üreticiler için', level: 2 },
|
|
7
|
+
{ type: 'paragraph', html: 'Bağımsız ekipler grafik yazılımı, yayın panosu ve inceleme klasörleri arasında sıkça geçiş yapar. İyi bir araç bu aktarımı kısaltır, önemli kararları görünür kılar ve kaynak dosyaların kontrolünü üreticide bırakır.' },
|
|
8
|
+
{ type: 'list', items: ['Görünür sonuçlara sahip odaklı akışlar', 'Uygun olduğunda yerel işleme', 'Net boyutlar ve dışa aktarma durumları', 'Resmî platform belgelerini tamamlayan bilgiler'] },
|
|
9
|
+
{ type: 'tip', html: 'Bu araçları ön kontrol katmanı olarak kullanın. Kaynakları saklayın ve dışa aktarımları platformun güncel şartlarıyla karşılaştırın.' },
|
|
10
|
+
] };
|
package/src/category/i18n/zh.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { CategoryLocaleContent } from '../../types';
|
|
2
|
-
|
|
3
|
-
export const content: CategoryLocaleContent = { slug: 'game-development', title: '游戏开发工具', description: '面向独立开发者的实用浏览器工具,帮助准备商店美术并完成制作交付。', seo: [
|
|
4
|
-
{ type: 'title', text: '为游戏创作者准备的工具', level: 2 },
|
|
5
|
-
{ type: 'paragraph', html: '游戏开发由许多小决定组成:构图要适应多种商店尺寸,图标要一眼可读,制作任务也要容易重复。本分类为制作、发布和展示游戏的人整理了目标明确的工具,把繁琐工作变成清晰的浏览器操作。' },
|
|
6
|
-
{ type: 'title', text: '适合独立开发者', level: 2 },
|
|
7
|
-
{ type: 'paragraph', html: '独立团队经常在美术软件、发布面板和审核文件夹之间切换。好的工具可以缩短交接过程,让关键决定可见,并让创作者继续掌握源文件。' },
|
|
8
|
-
{ type: 'list', items: ['结果清晰的专注流程', '适合时在浏览器本地处理', '明确的尺寸和导出状态', '补充官方平台文档的说明'] },
|
|
9
|
-
{ type: 'tip', html: '把这些工具当作发布前检查层,保留源文件,并将结果与平台当前要求进行比较。' },
|
|
10
|
-
] };
|
|
1
|
+
import type { CategoryLocaleContent } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const content: CategoryLocaleContent = { slug: 'game-development', title: '游戏开发工具', description: '面向独立开发者的实用浏览器工具,帮助准备商店美术并完成制作交付。', seo: [
|
|
4
|
+
{ type: 'title', text: '为游戏创作者准备的工具', level: 2 },
|
|
5
|
+
{ type: 'paragraph', html: '游戏开发由许多小决定组成:构图要适应多种商店尺寸,图标要一眼可读,制作任务也要容易重复。本分类为制作、发布和展示游戏的人整理了目标明确的工具,把繁琐工作变成清晰的浏览器操作。' },
|
|
6
|
+
{ type: 'title', text: '适合独立开发者', level: 2 },
|
|
7
|
+
{ type: 'paragraph', html: '独立团队经常在美术软件、发布面板和审核文件夹之间切换。好的工具可以缩短交接过程,让关键决定可见,并让创作者继续掌握源文件。' },
|
|
8
|
+
{ type: 'list', items: ['结果清晰的专注流程', '适合时在浏览器本地处理', '明确的尺寸和导出状态', '补充官方平台文档的说明'] },
|
|
9
|
+
{ type: 'tip', html: '把这些工具当作发布前检查层,保留源文件,并将结果与平台当前要求进行比较。' },
|
|
10
|
+
] };
|
package/src/category/seo.astro
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
---
|
|
2
|
-
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
-
import { gamesCategory } 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 gamesCategory.i18n[locale as KnownLocale]?.();
|
|
12
|
-
if (!content) return null;
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
<SEORenderer content={{ sections: content.seo, locale }} />
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { gamesCategory } 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 gamesCategory.i18n[locale as KnownLocale]?.();
|
|
12
|
+
if (!content) return null;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<SEORenderer content={{ sections: content.seo, locale }} />
|
|
@@ -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
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
export { gamesCategory } from './category';
|
|
2
|
-
|
|
3
|
-
export type {
|
|
4
|
-
KnownLocale,
|
|
5
|
-
ToolLocaleContent,
|
|
6
|
-
CategoryLocaleContent,
|
|
7
|
-
LocaleMap,
|
|
8
|
-
GamesToolEntry,
|
|
9
|
-
GamesCategoryEntry,
|
|
10
|
-
} from './types';
|
|
11
|
-
|
|
1
|
+
export { gamesCategory } from './category';
|
|
2
|
+
|
|
3
|
+
export type {
|
|
4
|
+
KnownLocale,
|
|
5
|
+
ToolLocaleContent,
|
|
6
|
+
CategoryLocaleContent,
|
|
7
|
+
LocaleMap,
|
|
8
|
+
GamesToolEntry,
|
|
9
|
+
GamesCategoryEntry,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
package/src/env.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
declare module '*.astro' {
|
|
2
|
-
const Component: (_props: Record<string, unknown>) => unknown;
|
|
3
|
-
export default Component;
|
|
4
|
-
}
|
|
5
|
-
|
|
1
|
+
declare module '*.astro' {
|
|
2
|
+
const Component: (_props: Record<string, unknown>) => unknown;
|
|
3
|
+
export default Component;
|
|
4
|
+
}
|
|
5
|
+
|