@jjlmoya/utils-audiovisual 1.2.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 +60 -0
- package/src/category/i18n/en.ts +198 -0
- package/src/category/i18n/es.ts +198 -0
- package/src/category/i18n/fr.ts +198 -0
- package/src/category/index.ts +17 -0
- package/src/category/seo.astro +15 -0
- package/src/components/PreviewNavSidebar.astro +116 -0
- package/src/components/PreviewToolbar.astro +143 -0
- package/src/data.ts +4 -0
- package/src/env.d.ts +5 -0
- package/src/index.ts +32 -0
- package/src/layouts/PreviewLayout.astro +117 -0
- package/src/pages/[locale]/[slug].astro +146 -0
- package/src/pages/[locale].astro +251 -0
- package/src/pages/index.astro +4 -0
- package/src/tests/faq_count.test.ts +19 -0
- package/src/tests/locale_completeness.test.ts +42 -0
- package/src/tests/mocks/astro_mock.js +2 -0
- package/src/tests/no_h1_in_components.test.ts +48 -0
- package/src/tests/seo_length.test.ts +22 -0
- package/src/tests/tool_validation.test.ts +17 -0
- package/src/tool/chromaticLens/bibliography.astro +17 -0
- package/src/tool/chromaticLens/component.astro +178 -0
- package/src/tool/chromaticLens/i18n/en.ts +246 -0
- package/src/tool/chromaticLens/i18n/es.ts +244 -0
- package/src/tool/chromaticLens/i18n/fr.ts +244 -0
- package/src/tool/chromaticLens/index.ts +43 -0
- package/src/tool/chromaticLens/logic.ts +87 -0
- package/src/tool/chromaticLens/seo.astro +15 -0
- package/src/tool/chromaticLens/style.css +308 -0
- package/src/tool/chromaticLens/ui.ts +109 -0
- package/src/tool/collageMaker/bibliography.astro +17 -0
- package/src/tool/collageMaker/component.astro +302 -0
- package/src/tool/collageMaker/i18n/en.ts +233 -0
- package/src/tool/collageMaker/i18n/es.ts +231 -0
- package/src/tool/collageMaker/i18n/fr.ts +231 -0
- package/src/tool/collageMaker/index.ts +51 -0
- package/src/tool/collageMaker/logic.ts +134 -0
- package/src/tool/collageMaker/seo.astro +15 -0
- package/src/tool/collageMaker/style.css +386 -0
- package/src/tool/exifCleaner/bibliography.astro +18 -0
- package/src/tool/exifCleaner/component.astro +162 -0
- package/src/tool/exifCleaner/i18n/en.ts +277 -0
- package/src/tool/exifCleaner/i18n/es.ts +277 -0
- package/src/tool/exifCleaner/i18n/fr.ts +277 -0
- package/src/tool/exifCleaner/index.ts +57 -0
- package/src/tool/exifCleaner/logic.ts +135 -0
- package/src/tool/exifCleaner/seo.astro +18 -0
- package/src/tool/exifCleaner/style.css +289 -0
- package/src/tool/exifCleaner/ui.ts +117 -0
- package/src/tool/imageCompressor/bibliography.astro +17 -0
- package/src/tool/imageCompressor/component.astro +262 -0
- package/src/tool/imageCompressor/i18n/en.ts +232 -0
- package/src/tool/imageCompressor/i18n/es.ts +230 -0
- package/src/tool/imageCompressor/i18n/fr.ts +230 -0
- package/src/tool/imageCompressor/index.ts +50 -0
- package/src/tool/imageCompressor/logic.ts +79 -0
- package/src/tool/imageCompressor/seo.astro +15 -0
- package/src/tool/imageCompressor/style.css +503 -0
- package/src/tool/printQualityCalculator/bibliography.astro +18 -0
- package/src/tool/printQualityCalculator/component.astro +318 -0
- package/src/tool/printQualityCalculator/i18n/en.ts +247 -0
- package/src/tool/printQualityCalculator/i18n/es.ts +245 -0
- package/src/tool/printQualityCalculator/i18n/fr.ts +245 -0
- package/src/tool/printQualityCalculator/index.ts +56 -0
- package/src/tool/printQualityCalculator/logic.ts +53 -0
- package/src/tool/printQualityCalculator/seo.astro +18 -0
- package/src/tool/printQualityCalculator/style.css +491 -0
- package/src/tool/printQualityCalculator/ui.ts +122 -0
- package/src/tool/privacyBlur/bibliography.astro +17 -0
- package/src/tool/privacyBlur/component.astro +230 -0
- package/src/tool/privacyBlur/i18n/en.ts +238 -0
- package/src/tool/privacyBlur/i18n/es.ts +236 -0
- package/src/tool/privacyBlur/i18n/fr.ts +236 -0
- package/src/tool/privacyBlur/index.ts +49 -0
- package/src/tool/privacyBlur/logic.ts +249 -0
- package/src/tool/privacyBlur/seo.astro +15 -0
- package/src/tool/privacyBlur/style.css +332 -0
- package/src/tool/privacyBlur/ui.ts +124 -0
- package/src/tool/subtitleSync/bibliography.astro +17 -0
- package/src/tool/subtitleSync/component.astro +187 -0
- package/src/tool/subtitleSync/i18n/en.ts +241 -0
- package/src/tool/subtitleSync/i18n/es.ts +241 -0
- package/src/tool/subtitleSync/i18n/fr.ts +241 -0
- package/src/tool/subtitleSync/index.ts +49 -0
- package/src/tool/subtitleSync/logic.ts +91 -0
- package/src/tool/subtitleSync/seo.astro +15 -0
- package/src/tool/subtitleSync/style.css +325 -0
- package/src/tool/subtitleSync/ui.ts +152 -0
- package/src/tool/timelapseCalculator/bibliography.astro +15 -0
- package/src/tool/timelapseCalculator/component.astro +148 -0
- package/src/tool/timelapseCalculator/i18n/en.ts +169 -0
- package/src/tool/timelapseCalculator/i18n/es.ts +169 -0
- package/src/tool/timelapseCalculator/i18n/fr.ts +169 -0
- package/src/tool/timelapseCalculator/index.ts +52 -0
- package/src/tool/timelapseCalculator/logic.ts +46 -0
- package/src/tool/timelapseCalculator/seo.astro +18 -0
- package/src/tool/timelapseCalculator/style.css +285 -0
- package/src/tool/tvDistance/bibliography.astro +17 -0
- package/src/tool/tvDistance/component.astro +178 -0
- package/src/tool/tvDistance/i18n/en.ts +223 -0
- package/src/tool/tvDistance/i18n/es.ts +223 -0
- package/src/tool/tvDistance/i18n/fr.ts +223 -0
- package/src/tool/tvDistance/index.ts +49 -0
- package/src/tool/tvDistance/logic.ts +47 -0
- package/src/tool/tvDistance/seo.astro +15 -0
- package/src/tool/tvDistance/style.css +435 -0
- package/src/tool/tvDistance/ui.ts +66 -0
- package/src/tool/videoFrameExtractor/bibliography.astro +17 -0
- package/src/tool/videoFrameExtractor/component.astro +285 -0
- package/src/tool/videoFrameExtractor/i18n/en.ts +235 -0
- package/src/tool/videoFrameExtractor/i18n/es.ts +235 -0
- package/src/tool/videoFrameExtractor/i18n/fr.ts +235 -0
- package/src/tool/videoFrameExtractor/index.ts +53 -0
- package/src/tool/videoFrameExtractor/logic.ts +49 -0
- package/src/tool/videoFrameExtractor/seo.astro +15 -0
- package/src/tool/videoFrameExtractor/style.css +426 -0
- package/src/tool/videoFrameExtractor/ui.ts +179 -0
- package/src/tools.ts +25 -0
- package/src/types.ts +72 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
---
|
|
2
|
+
import PreviewLayout from '../layouts/PreviewLayout.astro';
|
|
3
|
+
import PreviewNavSidebar from '../components/PreviewNavSidebar.astro';
|
|
4
|
+
import { audiovisualCategory, ALL_TOOLS } from '../index';
|
|
5
|
+
import { Icon } from 'astro-icon/components';
|
|
6
|
+
import type { KnownLocale, ToolLocaleContent } from '../types';
|
|
7
|
+
|
|
8
|
+
export async function getStaticPaths() {
|
|
9
|
+
const locales = ['en', 'es', 'fr'] as KnownLocale[];
|
|
10
|
+
return locales.map(locale => ({ params: { locale } }));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { locale: currentLocale } = Astro.params as { locale: KnownLocale };
|
|
14
|
+
|
|
15
|
+
const categoryContent = await audiovisualCategory.i18n[currentLocale]!();
|
|
16
|
+
|
|
17
|
+
const tools = ALL_TOOLS || [];
|
|
18
|
+
|
|
19
|
+
const toolsWithContent = tools.length > 0
|
|
20
|
+
? await Promise.all(
|
|
21
|
+
tools.map(async ({ entry, Component }) => {
|
|
22
|
+
const languages = Object.keys(entry.i18n);
|
|
23
|
+
const localeEntries = await Promise.all(
|
|
24
|
+
languages.map(async (l) => {
|
|
25
|
+
const content = await entry.i18n[l as KnownLocale]!();
|
|
26
|
+
return [l, content];
|
|
27
|
+
})
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const localeContents = Object.fromEntries(localeEntries) as Record<string, ToolLocaleContent<Record<string, string>>>;
|
|
31
|
+
|
|
32
|
+
const currentLocaleContent = localeContents[currentLocale] || localeContents['en'] || localeContents['es'];
|
|
33
|
+
const availableLocales: Record<string, string> = {};
|
|
34
|
+
|
|
35
|
+
for (const l of languages) {
|
|
36
|
+
const lCont = localeContents[l];
|
|
37
|
+
if (lCont) {
|
|
38
|
+
availableLocales[l] = `/${l}/${lCont.slug}`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { entry, Component, locale: currentLocaleContent, availableLocales };
|
|
43
|
+
})
|
|
44
|
+
)
|
|
45
|
+
: [];
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
<PreviewLayout
|
|
49
|
+
title={categoryContent.title}
|
|
50
|
+
currentLocale={currentLocale}
|
|
51
|
+
hasSidebar={true}
|
|
52
|
+
>
|
|
53
|
+
<PreviewNavSidebar
|
|
54
|
+
slot="sidebar"
|
|
55
|
+
categoryTitle={categoryContent.title}
|
|
56
|
+
tools={toolsWithContent.map(({ entry, locale, availableLocales }) => {
|
|
57
|
+
const href = availableLocales[currentLocale] || (locale ? `/${currentLocale}/${locale.slug}` : '#');
|
|
58
|
+
return {
|
|
59
|
+
id: entry.id,
|
|
60
|
+
title: locale?.title || entry.id,
|
|
61
|
+
href: href,
|
|
62
|
+
};
|
|
63
|
+
})}
|
|
64
|
+
/>
|
|
65
|
+
<div class="dashboard">
|
|
66
|
+
<header class="preview-header">
|
|
67
|
+
<span class="badge">preview · @jjlmoya/utils-template</span>
|
|
68
|
+
<h1>{categoryContent.title}</h1>
|
|
69
|
+
<p>{categoryContent.description}</p>
|
|
70
|
+
</header>
|
|
71
|
+
|
|
72
|
+
<div class="tool-list">
|
|
73
|
+
{toolsWithContent?.map(({ entry, locale, availableLocales }) => (
|
|
74
|
+
<article class="tool-card">
|
|
75
|
+
<a href={availableLocales?.[currentLocale] || (locale ? `/${currentLocale}/${locale.slug}` : '#')} class="tool-card-link">
|
|
76
|
+
<div class="tool-icons">
|
|
77
|
+
<div class="icon-wrapper bg">
|
|
78
|
+
<Icon name={entry.icons.bg} />
|
|
79
|
+
</div>
|
|
80
|
+
<div class="icon-wrapper fg">
|
|
81
|
+
<Icon name={entry.icons.fg} />
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="tool-card-content">
|
|
85
|
+
<h2 class="tool-title">{locale?.title}</h2>
|
|
86
|
+
<p class="tool-description">{locale?.description}</p>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="tool-card-meta">
|
|
89
|
+
<span class="tool-id">{entry.id}</span>
|
|
90
|
+
</div>
|
|
91
|
+
</a>
|
|
92
|
+
|
|
93
|
+
{availableLocales && Object.keys(availableLocales).length > 1 && (
|
|
94
|
+
<div class="tool-locales">
|
|
95
|
+
{Object.entries(availableLocales).map(([l, url]) => (
|
|
96
|
+
<a href={url} class="locale-badge" title={`Ver en ${l.toUpperCase()}`} class:list={{ active: l === currentLocale }}>
|
|
97
|
+
{l.toUpperCase()}
|
|
98
|
+
</a>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
</article>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</PreviewLayout>
|
|
107
|
+
|
|
108
|
+
<style>
|
|
109
|
+
.dashboard {
|
|
110
|
+
display: flex;
|
|
111
|
+
flex-direction: column;
|
|
112
|
+
gap: 5rem;
|
|
113
|
+
}
|
|
114
|
+
.preview-header {
|
|
115
|
+
text-align: center;
|
|
116
|
+
padding-bottom: 3rem;
|
|
117
|
+
border-bottom: 1px solid var(--border-color);
|
|
118
|
+
}
|
|
119
|
+
.badge {
|
|
120
|
+
display: inline-block;
|
|
121
|
+
padding: 0.25rem 0.75rem;
|
|
122
|
+
background: var(--accent);
|
|
123
|
+
border-radius: 99px;
|
|
124
|
+
font-size: 0.7rem;
|
|
125
|
+
font-weight: 800;
|
|
126
|
+
margin-bottom: 1.5rem;
|
|
127
|
+
color: var(--text-base);
|
|
128
|
+
letter-spacing: 0.05em;
|
|
129
|
+
}
|
|
130
|
+
h1 {
|
|
131
|
+
font-size: clamp(2rem, 6vw, 3.5rem);
|
|
132
|
+
font-weight: 900;
|
|
133
|
+
margin: 0 0 1rem;
|
|
134
|
+
background: linear-gradient(to bottom, var(--text-base), var(--text-muted));
|
|
135
|
+
-webkit-background-clip: text;
|
|
136
|
+
-webkit-text-fill-color: transparent;
|
|
137
|
+
background-clip: text;
|
|
138
|
+
}
|
|
139
|
+
.preview-header p {
|
|
140
|
+
color: var(--text-muted);
|
|
141
|
+
font-size: 1.1rem;
|
|
142
|
+
margin: 0;
|
|
143
|
+
}
|
|
144
|
+
.tool-list {
|
|
145
|
+
display: grid;
|
|
146
|
+
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
147
|
+
gap: 2rem;
|
|
148
|
+
}
|
|
149
|
+
.tool-card {
|
|
150
|
+
display: flex;
|
|
151
|
+
flex-direction: column;
|
|
152
|
+
gap: 1rem;
|
|
153
|
+
}
|
|
154
|
+
.tool-card-link {
|
|
155
|
+
flex: 1;
|
|
156
|
+
display: flex;
|
|
157
|
+
flex-direction: column;
|
|
158
|
+
padding: 1.5rem;
|
|
159
|
+
background: var(--bg-surface);
|
|
160
|
+
border: 1px solid var(--border-color);
|
|
161
|
+
border-radius: 0.75rem;
|
|
162
|
+
text-decoration: none;
|
|
163
|
+
transition: all 0.2s ease;
|
|
164
|
+
}
|
|
165
|
+
.tool-card-link:hover {
|
|
166
|
+
border-color: var(--accent);
|
|
167
|
+
background: rgba(244, 63, 94, 0.05);
|
|
168
|
+
transform: translateY(-2px);
|
|
169
|
+
}
|
|
170
|
+
.tool-icons {
|
|
171
|
+
display: flex;
|
|
172
|
+
align-items: center;
|
|
173
|
+
gap: 1rem;
|
|
174
|
+
margin-bottom: 1.25rem;
|
|
175
|
+
}
|
|
176
|
+
.icon-wrapper {
|
|
177
|
+
display: flex;
|
|
178
|
+
align-items: center;
|
|
179
|
+
justify-content: center;
|
|
180
|
+
width: 3rem;
|
|
181
|
+
height: 3rem;
|
|
182
|
+
border-radius: 0.5rem;
|
|
183
|
+
font-size: 1.5rem;
|
|
184
|
+
}
|
|
185
|
+
.icon-wrapper.bg {
|
|
186
|
+
background: var(--accent);
|
|
187
|
+
color: var(--text-base);
|
|
188
|
+
}
|
|
189
|
+
.icon-wrapper.fg {
|
|
190
|
+
background: var(--bg-page);
|
|
191
|
+
border: 1px solid var(--border-color);
|
|
192
|
+
color: var(--accent);
|
|
193
|
+
}
|
|
194
|
+
.tool-card-content {
|
|
195
|
+
flex: 1;
|
|
196
|
+
display: flex;
|
|
197
|
+
flex-direction: column;
|
|
198
|
+
}
|
|
199
|
+
.tool-title {
|
|
200
|
+
font-size: 1.25rem;
|
|
201
|
+
font-weight: 700;
|
|
202
|
+
margin: 0 0 0.5rem;
|
|
203
|
+
color: var(--text-base);
|
|
204
|
+
}
|
|
205
|
+
.tool-description {
|
|
206
|
+
font-size: 0.9375rem;
|
|
207
|
+
color: var(--text-muted);
|
|
208
|
+
line-height: 1.5;
|
|
209
|
+
margin: 0;
|
|
210
|
+
}
|
|
211
|
+
.tool-card-meta {
|
|
212
|
+
padding-top: 1rem;
|
|
213
|
+
border-top: 1px solid var(--border-color);
|
|
214
|
+
margin-top: 1.5rem;
|
|
215
|
+
}
|
|
216
|
+
.tool-id {
|
|
217
|
+
display: inline-block;
|
|
218
|
+
font-size: 0.7rem;
|
|
219
|
+
background: var(--bg-page);
|
|
220
|
+
border: 1px solid var(--border-color);
|
|
221
|
+
padding: 0.35rem 0.75rem;
|
|
222
|
+
border-radius: 0.4rem;
|
|
223
|
+
color: var(--accent);
|
|
224
|
+
font-weight: 600;
|
|
225
|
+
}
|
|
226
|
+
.tool-locales {
|
|
227
|
+
display: flex;
|
|
228
|
+
gap: 0.5rem;
|
|
229
|
+
flex-wrap: wrap;
|
|
230
|
+
}
|
|
231
|
+
.locale-badge {
|
|
232
|
+
display: inline-block;
|
|
233
|
+
padding: 0.4rem 0.85rem;
|
|
234
|
+
background: var(--bg-page);
|
|
235
|
+
border: 1px solid var(--border-color);
|
|
236
|
+
border-radius: 0.4rem;
|
|
237
|
+
color: var(--text-muted);
|
|
238
|
+
text-decoration: none;
|
|
239
|
+
font-size: 0.75rem;
|
|
240
|
+
font-weight: 600;
|
|
241
|
+
text-transform: uppercase;
|
|
242
|
+
letter-spacing: 0.05em;
|
|
243
|
+
transition: all 0.15s ease;
|
|
244
|
+
}
|
|
245
|
+
.locale-badge:hover,
|
|
246
|
+
.locale-badge.active {
|
|
247
|
+
color: var(--accent);
|
|
248
|
+
border-color: var(--accent);
|
|
249
|
+
background: rgba(244, 63, 94, 0.1);
|
|
250
|
+
}
|
|
251
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type * as DATA from '../data';
|
|
3
|
+
|
|
4
|
+
const TOOLS: typeof DATA.audiovisualCategory[] = [];
|
|
5
|
+
|
|
6
|
+
describe('FAQ Content Validation', () => {
|
|
7
|
+
TOOLS.forEach((entry) => {
|
|
8
|
+
describe(`Tool: ${entry.icon}`, () => {
|
|
9
|
+
it('placeholder', () => {
|
|
10
|
+
expect(true).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('no tools registered yet', () => {
|
|
16
|
+
expect(TOOLS.length).toBe(0);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
import type { ToolLocaleContent } from '../types';
|
|
4
|
+
|
|
5
|
+
describe('Locale Completeness Validation', () => {
|
|
6
|
+
ALL_TOOLS.forEach((tool) => {
|
|
7
|
+
describe(`Tool: ${tool.entry.id}`, () => {
|
|
8
|
+
Object.keys(tool.entry.i18n).forEach((locale) => {
|
|
9
|
+
describe(`Locale: ${locale}`, () => {
|
|
10
|
+
it('faqTitle should be defined when faq items exist', async () => {
|
|
11
|
+
const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
|
|
12
|
+
const content = (await loader?.()) as ToolLocaleContent;
|
|
13
|
+
|
|
14
|
+
if (content.faq.length > 0) {
|
|
15
|
+
expect(
|
|
16
|
+
content.faqTitle,
|
|
17
|
+
`Tool "${tool.entry.id}" locale "${locale}" has ${content.faq.length} FAQ items but is missing faqTitle`,
|
|
18
|
+
).toBeTruthy();
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('bibliographyTitle should be defined when bibliography items exist', async () => {
|
|
23
|
+
const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
|
|
24
|
+
const content = (await loader?.()) as ToolLocaleContent;
|
|
25
|
+
|
|
26
|
+
if (content.bibliography.length > 0) {
|
|
27
|
+
expect(
|
|
28
|
+
content.bibliographyTitle,
|
|
29
|
+
`Tool "${tool.entry.id}" locale "${locale}" has ${content.bibliography.length} bibliography items but is missing bibliographyTitle`,
|
|
30
|
+
).toBeTruthy();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('all 10 tools registered', () => {
|
|
39
|
+
expect(ALL_TOOLS.length).toBe(10);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { readdirSync, readFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
const EXCLUDED_DIRS = ['node_modules', 'pages', 'layouts'];
|
|
6
|
+
|
|
7
|
+
function findAstroFiles(dir: string): string[] {
|
|
8
|
+
const files: string[] = [];
|
|
9
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
10
|
+
|
|
11
|
+
for (const entry of entries) {
|
|
12
|
+
const fullPath = join(dir, entry.name);
|
|
13
|
+
if (entry.isDirectory() && !EXCLUDED_DIRS.includes(entry.name)) {
|
|
14
|
+
files.push(...findAstroFiles(fullPath));
|
|
15
|
+
} else if (entry.isFile() && entry.name.endsWith('.astro')) {
|
|
16
|
+
files.push(fullPath);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return files;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function hasH1(content: string): boolean {
|
|
24
|
+
return /<h1[\s>]/i.test(content);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const srcDir = join(process.cwd(), 'src');
|
|
28
|
+
const astroFiles = findAstroFiles(srcDir);
|
|
29
|
+
|
|
30
|
+
describe('No H1 in Components', () => {
|
|
31
|
+
if (astroFiles.length === 0) {
|
|
32
|
+
it('no astro components found', () => {
|
|
33
|
+
expect(true).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
astroFiles.forEach((file) => {
|
|
38
|
+
const relativePath = file.replace(process.cwd(), '');
|
|
39
|
+
it(`${relativePath} should not contain <h1>`, () => {
|
|
40
|
+
const content = readFileSync(file, 'utf-8');
|
|
41
|
+
expect(
|
|
42
|
+
hasH1(content),
|
|
43
|
+
`File "${relativePath}" contains a <h1> element. Use <h2> or lower inside components — h1 belongs to the page layout.`,
|
|
44
|
+
).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import * as DATA from '../data';
|
|
3
|
+
|
|
4
|
+
const ENTRIES = [
|
|
5
|
+
{ id: 'audiovisualCategory', i18n: DATA.audiovisualCategory.i18n },
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
describe('SEO Content Length Validation', () => {
|
|
9
|
+
ENTRIES.forEach((entry) => {
|
|
10
|
+
describe(`Tool: ${entry.id}`, () => {
|
|
11
|
+
Object.keys(entry.i18n).forEach((locale) => {
|
|
12
|
+
it(`${locale}: SEO section should exist`, async () => {
|
|
13
|
+
const loader = (entry.i18n as Record<string, () => Promise<{ seo?: unknown[] }>>)[locale];
|
|
14
|
+
const content = await loader();
|
|
15
|
+
if (!content.seo) return;
|
|
16
|
+
expect(Array.isArray(content.seo)).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
import { audiovisualCategory } from '../data';
|
|
4
|
+
|
|
5
|
+
describe('Tool Validation Suite', () => {
|
|
6
|
+
describe('Library Registration', () => {
|
|
7
|
+
it('should have 10 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(10);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('audiovisualCategory should be defined', () => {
|
|
12
|
+
expect(audiovisualCategory).toBeDefined();
|
|
13
|
+
expect(audiovisualCategory.i18n).toBeDefined();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { chromaticLens } 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 chromaticLens.i18n[locale]?.();
|
|
12
|
+
if (!content || !content.bibliography || content.bibliography.length === 0) return null;
|
|
13
|
+
|
|
14
|
+
const { bibliography, bibliographyTitle = 'Bibliografía' } = content;
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<SharedBibliography title={bibliographyTitle} links={bibliography} />
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { ChromaticLensUI } from './index';
|
|
3
|
+
import './style.css';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
ui: ChromaticLensUI;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { ui } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<div class="cl-root" id="cl-root" data-ui={JSON.stringify(ui)}>
|
|
13
|
+
<div class="cl-card">
|
|
14
|
+
|
|
15
|
+
<div id="cl-empty" class="cl-drop">
|
|
16
|
+
<input type="file" id="cl-file" accept="image/*" class="cl-hidden" />
|
|
17
|
+
<div class="cl-drop-icon">
|
|
18
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
19
|
+
<path d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9c.83 0 1.5-.67 1.5-1.5 0-.39-.15-.74-.39-1.01-.23-.26-.38-.61-.38-.99 0-.83.67-1.5 1.5-1.5H16c2.76 0 5-2.24 5-5 0-4.42-4.03-8-9-8zm-5.5 9c-.83 0-1.5-.67-1.5-1.5S5.67 9 6.5 9 8 9.67 8 10.5 7.33 12 6.5 12zm3-4C8.67 8 8 7.33 8 6.5S8.67 5 9.5 5s1.5.67 1.5 1.5S10.33 8 9.5 8zm5 0c-.83 0-1.5-.67-1.5-1.5S13.67 5 14.5 5s1.5.67 1.5 1.5S15.33 8 14.5 8zm3 4c-.83 0-1.5-.67-1.5-1.5S16.67 9 17.5 9s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/>
|
|
20
|
+
</svg>
|
|
21
|
+
</div>
|
|
22
|
+
<h3 class="cl-drop-title">{ui.dropTitle}</h3>
|
|
23
|
+
<p class="cl-drop-sub">{ui.dropSubtitle}</p>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div id="cl-workspace" class="cl-workspace cl-hidden">
|
|
27
|
+
<div id="cl-mini-drop" class="cl-mini-drop">
|
|
28
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
29
|
+
<path d="M19.35 10.04A7.49 7.49 0 0 0 12 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 0 0 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM14 13v4h-4v-4H7l5-5 5 5h-3z"/>
|
|
30
|
+
</svg>
|
|
31
|
+
<span>{ui.changeImage}</span>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="cl-config-bar">
|
|
35
|
+
<div class="cl-config-item">
|
|
36
|
+
<span class="cl-config-label">{ui.colorCountLabel}</span>
|
|
37
|
+
<select id="cl-count" class="cl-count-select">
|
|
38
|
+
<option value="5">5</option>
|
|
39
|
+
<option value="8" selected>8</option>
|
|
40
|
+
<option value="12">12</option>
|
|
41
|
+
<option value="16">16</option>
|
|
42
|
+
</select>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="cl-result-layout">
|
|
47
|
+
<div class="cl-preview-col">
|
|
48
|
+
<img id="cl-preview" class="cl-preview-img" src="#" alt="" />
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div class="cl-palette-col">
|
|
52
|
+
<div class="cl-palette-header">
|
|
53
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
54
|
+
<path d="M3 3h4v4H3zm0 7h4v4H3zm0 7h4v4H3zm7-14h11v2H10zm0 7h11v2H10zm0 7h11v2H10z"/>
|
|
55
|
+
</svg>
|
|
56
|
+
<h4>{ui.paletteTitle}</h4>
|
|
57
|
+
</div>
|
|
58
|
+
<div id="cl-loader" class="cl-loader cl-hidden">
|
|
59
|
+
<div class="cl-spinner"></div>
|
|
60
|
+
<p class="cl-loader-text">{ui.processingLabel}</p>
|
|
61
|
+
</div>
|
|
62
|
+
<div id="cl-swatches" class="cl-swatches"></div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<script>
|
|
71
|
+
import { extractPalette } from './logic';
|
|
72
|
+
import type { ColorSwatch } from './logic';
|
|
73
|
+
import type { ChromaticLensUI } from './index';
|
|
74
|
+
|
|
75
|
+
function init() {
|
|
76
|
+
const root = document.getElementById('cl-root');
|
|
77
|
+
if (!root) return;
|
|
78
|
+
|
|
79
|
+
const labels = JSON.parse(root.dataset.ui ?? '{}') as ChromaticLensUI;
|
|
80
|
+
const canvas = document.createElement('canvas');
|
|
81
|
+
const ctxRaw = canvas.getContext('2d', { willReadFrequently: true });
|
|
82
|
+
if (!ctxRaw) return;
|
|
83
|
+
const ctx = ctxRaw;
|
|
84
|
+
|
|
85
|
+
const fileInput = root.querySelector('#cl-file') as HTMLInputElement;
|
|
86
|
+
const emptyState = root.querySelector('#cl-empty') as HTMLElement;
|
|
87
|
+
const workspace = root.querySelector('#cl-workspace') as HTMLElement;
|
|
88
|
+
const miniDrop = root.querySelector('#cl-mini-drop') as HTMLElement;
|
|
89
|
+
const preview = root.querySelector('#cl-preview') as HTMLImageElement;
|
|
90
|
+
const countSelect = root.querySelector('#cl-count') as HTMLSelectElement;
|
|
91
|
+
const loader = root.querySelector('#cl-loader') as HTMLElement;
|
|
92
|
+
const swatches = root.querySelector('#cl-swatches') as HTMLElement;
|
|
93
|
+
|
|
94
|
+
let currentImageData: Uint8ClampedArray | null = null;
|
|
95
|
+
let isProcessing = false;
|
|
96
|
+
|
|
97
|
+
function renderPalette(palette: ColorSwatch[]) {
|
|
98
|
+
swatches.innerHTML = '';
|
|
99
|
+
palette.forEach((item) => {
|
|
100
|
+
const el = document.createElement('div');
|
|
101
|
+
el.className = 'cl-swatch';
|
|
102
|
+
el.innerHTML = `
|
|
103
|
+
<div class="cl-swatch-color" style="background-color:${item.hex}"></div>
|
|
104
|
+
<div class="cl-swatch-info">
|
|
105
|
+
<span class="cl-swatch-hex">${item.hex}</span>
|
|
106
|
+
<span class="cl-swatch-action">${labels.copyLabel}</span>
|
|
107
|
+
</div>
|
|
108
|
+
`;
|
|
109
|
+
el.addEventListener('click', () => {
|
|
110
|
+
navigator.clipboard.writeText(item.hex);
|
|
111
|
+
const action = el.querySelector('.cl-swatch-action') as HTMLElement;
|
|
112
|
+
const prev = action.textContent;
|
|
113
|
+
action.textContent = labels.copiedLabel;
|
|
114
|
+
el.classList.add('cl-swatch-copied');
|
|
115
|
+
setTimeout(() => {
|
|
116
|
+
action.textContent = prev;
|
|
117
|
+
el.classList.remove('cl-swatch-copied');
|
|
118
|
+
}, 2000);
|
|
119
|
+
});
|
|
120
|
+
swatches.appendChild(el);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function processPalette() {
|
|
125
|
+
if (!currentImageData || isProcessing) return;
|
|
126
|
+
isProcessing = true;
|
|
127
|
+
loader.classList.remove('cl-hidden');
|
|
128
|
+
swatches.classList.add('cl-hidden');
|
|
129
|
+
const count = parseInt(countSelect.value);
|
|
130
|
+
requestAnimationFrame(() => {
|
|
131
|
+
setTimeout(() => {
|
|
132
|
+
const palette = extractPalette(currentImageData!, count);
|
|
133
|
+
renderPalette(palette);
|
|
134
|
+
loader.classList.add('cl-hidden');
|
|
135
|
+
swatches.classList.remove('cl-hidden');
|
|
136
|
+
isProcessing = false;
|
|
137
|
+
}, 50);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function handleFile(file: File) {
|
|
142
|
+
const reader = new FileReader();
|
|
143
|
+
reader.onload = (e) => {
|
|
144
|
+
const img = new Image();
|
|
145
|
+
img.onload = () => {
|
|
146
|
+
canvas.width = img.naturalWidth;
|
|
147
|
+
canvas.height = img.naturalHeight;
|
|
148
|
+
ctx.drawImage(img, 0, 0);
|
|
149
|
+
currentImageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
|
|
150
|
+
preview.src = img.src;
|
|
151
|
+
emptyState.classList.add('cl-hidden');
|
|
152
|
+
workspace.classList.remove('cl-hidden');
|
|
153
|
+
processPalette();
|
|
154
|
+
};
|
|
155
|
+
img.src = e.target?.result as string;
|
|
156
|
+
};
|
|
157
|
+
reader.readAsDataURL(file);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
emptyState.addEventListener('click', () => fileInput.click());
|
|
161
|
+
miniDrop.addEventListener('click', () => fileInput.click());
|
|
162
|
+
fileInput.addEventListener('change', () => {
|
|
163
|
+
if (fileInput.files?.[0]) handleFile(fileInput.files[0]);
|
|
164
|
+
});
|
|
165
|
+
countSelect.addEventListener('change', processPalette);
|
|
166
|
+
|
|
167
|
+
root.addEventListener('dragover', (e) => { e.preventDefault(); emptyState.classList.add('cl-drop-active'); });
|
|
168
|
+
root.addEventListener('dragleave', () => emptyState.classList.remove('cl-drop-active'));
|
|
169
|
+
root.addEventListener('drop', (e) => {
|
|
170
|
+
e.preventDefault();
|
|
171
|
+
emptyState.classList.remove('cl-drop-active');
|
|
172
|
+
if (e.dataTransfer?.files[0]) handleFile(e.dataTransfer.files[0]);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
init();
|
|
177
|
+
document.addEventListener('astro:page-load', init);
|
|
178
|
+
</script>
|