@jjlmoya/utils-converters 1.27.0 → 1.28.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 (57) hide show
  1. package/package.json +18 -5
  2. package/scripts/postbuild.mjs +17 -0
  3. package/scripts/postinstall.mjs +2 -4
  4. package/src/components/ProductionBreadcrumb.astro +134 -0
  5. package/src/components/ProductionStructuredData.astro +46 -0
  6. package/src/components/ProductionWidget.astro +185 -0
  7. package/src/i18n/header-ui.ts +15 -0
  8. package/src/i18n/language-ui.ts +9 -0
  9. package/src/i18n/languages.ts +24 -0
  10. package/src/identity/brands.ts +5 -0
  11. package/src/layouts/ProductionCategoryPage.astro +190 -0
  12. package/src/layouts/ProductionPage.astro +218 -0
  13. package/src/layouts/ProductionUtilityPage.astro +292 -0
  14. package/src/mfe/assets.ts +39 -0
  15. package/src/mfe/category-ui.ts +34 -0
  16. package/src/mfe/manifest.ts +45 -0
  17. package/src/mfe/routes.ts +50 -0
  18. package/src/pages/[locale]/[utilities]/[categories]/[category]/[slug]/manifest.json.ts +49 -0
  19. package/src/pages/[locale]/[utilities]/[categories]/[category]/[slug].astro +100 -0
  20. package/src/pages/[locale]/[utilities]/[categories]/[category].astro +44 -0
  21. package/src/pages/index.astro +2 -3
  22. package/src/pages/mfe-sitemaps/[locale]/[vertical]/sitemap.xml.ts +75 -0
  23. package/src/pages/utilidades/[slug]/manifest.json.ts +28 -0
  24. package/src/pages/utilidades/[slug].astro +90 -0
  25. package/src/pages/utilidades/categorias/[category].astro +38 -0
  26. package/src/tests/diacritics_density.test.ts +1 -1
  27. package/src/tests/inverted_punctuation.test.ts +1 -1
  28. package/src/tests/mfe_cache_contract.test.ts +14 -0
  29. package/src/tests/mfe_manifest.test.ts +28 -0
  30. package/src/tests/registry_contract.test.ts +67 -0
  31. package/src/tests/script_density.test.ts +1 -1
  32. package/src/tests/seo_length.test.ts +1 -0
  33. package/src/tests/translation_copy.test.ts +2 -2
  34. package/src/tool/avifAJpg/index.ts +1 -0
  35. package/src/tool/avifAPng/index.ts +1 -0
  36. package/src/tool/avifAWebp/index.ts +1 -0
  37. package/src/tool/bmpAJpg/index.ts +1 -0
  38. package/src/tool/bmpAPng/index.ts +1 -0
  39. package/src/tool/bmpAWebp/index.ts +1 -0
  40. package/src/tool/gifAJpg/index.ts +1 -0
  41. package/src/tool/gifAPng/index.ts +1 -0
  42. package/src/tool/gifAWebp/index.ts +1 -0
  43. package/src/tool/jpgAIco/index.ts +1 -0
  44. package/src/tool/jpgAPng/index.ts +1 -0
  45. package/src/tool/jpgAWebp/index.ts +1 -0
  46. package/src/tool/pngAIco/index.ts +1 -0
  47. package/src/tool/pngAJpg/index.ts +1 -0
  48. package/src/tool/pngAWebp/index.ts +1 -0
  49. package/src/tool/svgAJpg/index.ts +1 -0
  50. package/src/tool/svgAPng/index.ts +1 -0
  51. package/src/tool/webpAIco/index.ts +1 -0
  52. package/src/tool/webpAJpg/index.ts +1 -0
  53. package/src/tool/webpAPng/index.ts +1 -0
  54. package/src/types.ts +4 -6
  55. package/src/worker.ts +27 -0
  56. package/src/pages/[locale]/[slug].astro +0 -163
  57. package/src/pages/[locale].astro +0 -271
@@ -0,0 +1,190 @@
1
+ ---
2
+ import ProductionPage from "./ProductionPage.astro";
3
+ import { Icon } from "astro-icon/components";
4
+ import { SEORenderer } from "@jjlmoya/utils-shared";
5
+ import ProductionBreadcrumb from "../components/ProductionBreadcrumb.astro";
6
+ import type { KnownLocale, CategoryLocaleContent } from "../types";
7
+ import { getCategoryPath } from "../mfe/routes";
8
+ import { getCategoryUi } from "../mfe/category-ui";
9
+
10
+ interface CategoryTool { id: string; icon: string; title: string; description: string; href: string; }
11
+ interface Props { locale: KnownLocale; category: CategoryLocaleContent; tools: CategoryTool[]; alternates: { language: KnownLocale; url: string }[]; image?: string | undefined; }
12
+ const { locale, category, tools, alternates, image } = Astro.props;
13
+ const publicPath = getCategoryPath(locale, category.slug);
14
+ const ui = getCategoryUi(locale);
15
+ ---
16
+
17
+ <ProductionPage title={category.title} description={category.description} {locale} {publicPath} {alternates} {image}>
18
+ <div class="category-production">
19
+ <div class="category-production-inner">
20
+ <ProductionBreadcrumb locale={locale} currentTitle={category.title} />
21
+ <header class="category-header-production">
22
+ <h1>{category.title}</h1><p>{category.description}</p>
23
+ </header>
24
+ <div class="category-tools-grid">
25
+ {tools.map((tool, index) => (
26
+ <a href={tool.href} class="category-tool-card">
27
+ <span class="category-tool-index">{String(index + 1).padStart(2, "0")}</span>
28
+ <div class="category-tool-icon"><Icon name={tool.icon} /></div>
29
+ <h2>{tool.title}</h2><p>{tool.description}</p>
30
+ <span class="category-tool-cta">{ui.useTool} <span aria-hidden="true">→</span></span>
31
+ </a>
32
+ ))}
33
+ </div>
34
+ <section class="category-seo-production"><SEORenderer content={{ locale, sections: category.seo }} /></section>
35
+ </div>
36
+ </div>
37
+ </ProductionPage>
38
+
39
+ <style>
40
+ .category-production {
41
+ min-height: 100vh;
42
+ overflow-x: clip;
43
+ background: radial-gradient(ellipse at top, var(--primary-10), var(--bg-surface) 50%, var(--bg-main));
44
+ }
45
+
46
+ .category-production-inner {
47
+ max-width: 1200px;
48
+ margin: 0 auto;
49
+ padding: var(--space-xl) var(--space-md) var(--space-4xl);
50
+ }
51
+
52
+ .category-header-production {
53
+ margin-bottom: var(--space-xl);
54
+ text-align: center;
55
+ }
56
+
57
+ .category-header-production h1 {
58
+ margin: 0;
59
+ color: var(--text-main);
60
+ font-size: clamp(3.5rem, 9vw, 7rem);
61
+ font-weight: 900;
62
+ letter-spacing: -0.06em;
63
+ line-height: 0.95;
64
+ text-transform: lowercase;
65
+ }
66
+
67
+ .category-header-production p {
68
+ max-width: 48rem;
69
+ margin: 1rem auto 0;
70
+ color: var(--text-muted);
71
+ font-size: 1.25rem;
72
+ line-height: 1.6;
73
+ }
74
+
75
+ .category-tools-grid {
76
+ display: grid;
77
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
78
+ gap: var(--space-lg);
79
+ margin-bottom: var(--space-4xl);
80
+ }
81
+
82
+ .category-tool-card {
83
+ position: relative;
84
+ display: flex;
85
+ overflow: hidden;
86
+ min-height: 230px;
87
+ flex-direction: column;
88
+ padding: var(--space-lg);
89
+ border: 1px solid color-mix(in srgb, var(--converters-accent) 18%, var(--border-base));
90
+ border-radius: 1.25rem;
91
+ background: linear-gradient(145deg, var(--bg-surface), color-mix(in srgb, var(--converters-accent) 4%, var(--bg-surface)));
92
+ color: inherit;
93
+ text-decoration: none;
94
+
95
+ --converters-card-shadow: 0 1rem 2rem color-mix(in srgb, var(--shadow-base, #000) 35%, transparent);
96
+
97
+ box-shadow: var(--converters-card-shadow);
98
+ transition: transform 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease;
99
+ }
100
+
101
+ .category-tool-card:hover,
102
+ .category-tool-card:focus-visible {
103
+ border-color: var(--converters-accent);
104
+
105
+ --converters-card-hover-shadow: 0 1.25rem 2.5rem color-mix(in srgb, var(--converters-accent) 18%, transparent);
106
+
107
+ box-shadow: var(--converters-card-hover-shadow);
108
+ transform: translateY(-4px);
109
+ }
110
+
111
+ .category-tool-card:focus-visible {
112
+ outline: 3px solid color-mix(in srgb, var(--converters-accent) 55%, transparent);
113
+ outline-offset: 4px;
114
+ }
115
+
116
+ .category-tool-card::after {
117
+ position: absolute;
118
+ right: -2rem;
119
+ bottom: -3rem;
120
+ width: 9rem;
121
+ height: 9rem;
122
+ border-radius: 50%;
123
+ background: color-mix(in srgb, var(--converters-accent) 10%, transparent);
124
+ content: "";
125
+ pointer-events: none;
126
+ }
127
+
128
+ .category-tool-icon {
129
+ position: relative;
130
+ z-index: 1;
131
+ display: grid;
132
+ width: 3.25rem;
133
+ height: 3.25rem;
134
+ place-items: center;
135
+ margin-bottom: var(--space-md);
136
+ border: 1px solid color-mix(in srgb, var(--converters-accent) 28%, transparent);
137
+ border-radius: 1rem;
138
+ background: color-mix(in srgb, var(--converters-accent) 12%, var(--bg-surface));
139
+ color: var(--converters-accent);
140
+ font-size: 1.6rem;
141
+ }
142
+
143
+ .category-tool-index {
144
+ position: absolute;
145
+ top: var(--space-md);
146
+ right: var(--space-lg);
147
+ color: var(--text-dimmed, var(--text-muted));
148
+ font-family: ui-monospace, SFMono-Regular, monospace;
149
+ font-size: 0.72rem;
150
+ letter-spacing: 0.12em;
151
+ }
152
+
153
+ .category-tool-card h2,
154
+ .category-tool-card p,
155
+ .category-tool-cta {
156
+ position: relative;
157
+ z-index: 1;
158
+ }
159
+
160
+ .category-tool-card h2 {
161
+ margin: 0 0 0.5rem;
162
+ color: var(--text-main);
163
+ font-size: 1.25rem;
164
+ }
165
+
166
+ .category-tool-card p {
167
+ margin: 0;
168
+ color: var(--text-muted);
169
+ line-height: 1.55;
170
+ }
171
+
172
+ .category-tool-cta {
173
+ margin-top: auto;
174
+ padding-top: var(--space-md);
175
+ color: var(--converters-accent);
176
+ font-weight: 700;
177
+ }
178
+
179
+ .category-seo-production {
180
+ max-width: 56rem;
181
+ margin: 0 auto;
182
+ font-size: 1.125rem;
183
+ line-height: 1.75;
184
+ }
185
+
186
+ @media (width <= 768px) {
187
+ .category-production-inner { padding: var(--space-lg) var(--space-sm) var(--space-3xl); }
188
+ .category-tools-grid { grid-template-columns: 1fr; }
189
+ }
190
+ </style>
@@ -0,0 +1,218 @@
1
+ ---
2
+ import Footer from "@jjlmoya/identity/footer";
3
+ import Header from "@jjlmoya/identity/header";
4
+ import LanguageSheet from "@jjlmoya/identity/language-sheet";
5
+ import LanguageTrigger from "@jjlmoya/identity/language-trigger";
6
+ import ThemeBoot from "@jjlmoya/identity/theme-boot";
7
+ import { getSiteNavigation } from "@jjlmoya/identity/navigation";
8
+ import "@jjlmoya/identity/styles.css";
9
+ import "@jjlmoya/utils-shared/theme.css";
10
+ import "@jjlmoya/utils-shared/styles/jjlmoya-shared.css";
11
+ import { HEADER_UI } from "../i18n/header-ui";
12
+ import { LANGUAGE_LOCALES, LANGUAGE_NAMES, type Language } from "../i18n/languages";
13
+ import { LANGUAGE_UI } from "../i18n/language-ui";
14
+ import { getBrand } from "../identity/brands";
15
+ import { getBrandAssetPath } from "../mfe/assets";
16
+
17
+ interface AlternateLink { language: Language; url: string; }
18
+ interface Props {
19
+ title: string;
20
+ description: string;
21
+ locale: Language;
22
+ image?: string | undefined;
23
+ toolCss?: string | undefined;
24
+ publicPath: string;
25
+ manifestPath?: string | undefined;
26
+ alternates: readonly AlternateLink[];
27
+ }
28
+
29
+ const { title, description, locale, image, toolCss, publicPath, manifestPath, alternates } = Astro.props;
30
+ const brand = getBrand(locale);
31
+ const brandFavicon = getBrandAssetPath(brand, "favicon");
32
+ const brandSizedFavicon = getBrandAssetPath(brand, "faviconSized");
33
+ const brandAppleTouchIcon = getBrandAssetPath(brand, "appleTouchIcon");
34
+ const origin = brand === "jjlmoya" ? "https://www.jjlmoya.es" : "https://www.gamebob.dev";
35
+ const canonical = new URL(publicPath, origin).href;
36
+ const socialImage = image ? new URL(image, canonical).href : undefined;
37
+ const languageUi = LANGUAGE_UI[locale];
38
+ const absoluteUrl = (language: Language, url: string): string => {
39
+ if (import.meta.env.DEV) return new URL(url, "http://localhost:4324").pathname;
40
+ return new URL(
41
+ url,
42
+ language === "es" ? "https://www.jjlmoya.es" : "https://www.gamebob.dev",
43
+ ).href;
44
+ };
45
+ const languageLinks = alternates.map(({ language, url }) => ({
46
+ code: language,
47
+ name: LANGUAGE_NAMES[language],
48
+ href: absoluteUrl(language, url),
49
+ current: language === locale,
50
+ }));
51
+ const homeUrl = brand === "jjlmoya" ? "https://www.jjlmoya.es/" : `https://www.gamebob.dev/${locale}/`;
52
+ const openGraphLocale = LANGUAGE_LOCALES[locale].replace("-", "_");
53
+ const xDefaultUrl = alternates.find(({ language }) => language === "en")?.url ?? canonical;
54
+ ---
55
+
56
+ <!doctype html>
57
+ <html lang={locale} data-brand={brand}>
58
+ <head>
59
+ <meta charset="UTF-8" />
60
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
61
+ <ThemeBoot defaultTheme={brand === "gamebob" ? "theme-dark" : "theme-system"} />
62
+ <title>{title}</title>
63
+ <meta name="title" content={title} />
64
+ <meta name="description" content={description} />
65
+ <meta name="author" content="jjlmoya" />
66
+ <meta name="robots" content="index, follow, max-image-preview:large" />
67
+ <meta name="referrer" content="strict-origin-when-cross-origin" />
68
+ <link rel="icon" type="image/x-icon" href={brandFavicon} />
69
+ <link rel="icon" type="image/webp" sizes={brand === "jjlmoya" ? "32x32" : "48x48"} href={brandSizedFavicon} />
70
+ <link rel="apple-touch-icon" href={brandAppleTouchIcon} />
71
+ {manifestPath && <link rel="manifest" href={manifestPath} />}
72
+ <link rel="canonical" href={canonical} />
73
+ {alternates.map(({ language, url }) => <link rel="alternate" hreflang={language} href={absoluteUrl(language, url)} />)}
74
+ <link rel="alternate" hreflang="x-default" href={absoluteUrl("en", xDefaultUrl)} />
75
+ {toolCss && <link rel="stylesheet" href={toolCss} />}
76
+ <meta property="og:type" content="website" />
77
+ <meta property="og:url" content={canonical} />
78
+ <meta property="og:title" content={title} />
79
+ <meta property="og:description" content={description} />
80
+ <meta property="og:site_name" content={brand === "jjlmoya" ? "jjlmoya" : "GameBob Studio"} />
81
+ <meta property="og:locale" content={openGraphLocale} />
82
+ {socialImage && <meta property="og:image" content={socialImage} />}
83
+ {socialImage && <meta property="og:image:secure_url" content={socialImage} />}
84
+ {socialImage && <meta property="og:image:type" content="image/webp" />}
85
+ {socialImage && <meta property="og:image:width" content="1200" />}
86
+ {socialImage && <meta property="og:image:height" content="630" />}
87
+ {socialImage && <meta property="og:image:alt" content={description} />}
88
+ <meta name="twitter:card" content={socialImage ? "summary_large_image" : "summary"} />
89
+ <meta name="twitter:url" content={canonical} />
90
+ <meta name="twitter:title" content={title} />
91
+ <meta name="twitter:description" content={description} />
92
+ {socialImage && <meta name="twitter:image" content={socialImage} />}
93
+ <script is:inline type="application/ld+json" set:html={JSON.stringify({
94
+ "@context": "https://schema.org",
95
+ "@type": "WebPage",
96
+ url: canonical,
97
+ name: title,
98
+ description,
99
+ inLanguage: LANGUAGE_LOCALES[locale],
100
+ })} />
101
+ <script>
102
+ import { installEcosystemTracking } from "@jjlmoya/tracking";
103
+ installEcosystemTracking();
104
+ </script>
105
+ <slot name="head" />
106
+ </head>
107
+ <body class="production-page">
108
+ <Header
109
+ class="utilities-site-header"
110
+ identity={brand}
111
+ {homeUrl}
112
+ navigation={getSiteNavigation({ language: locale, activeSection: "utilities" })}
113
+ labels={HEADER_UI[locale]}
114
+ >
115
+ <LanguageTrigger slot="desktop-actions" label={languageUi.change} />
116
+ <LanguageTrigger slot="mobile-actions" label={languageUi.change} />
117
+ </Header>
118
+ <LanguageSheet links={languageLinks} title={languageUi.select} closeLabel={languageUi.close} />
119
+ <main><slot /></main>
120
+ <Footer class="utilities-site-footer" identity={brand} language={locale} />
121
+ </body>
122
+ </html>
123
+
124
+ <style is:global>
125
+ :root {
126
+ --converters-accent: #10b981;
127
+ --converters-accent-strong: #0d9488;
128
+ --color-brand-blue: #1ddabc;
129
+ --color-brand-dark: #000;
130
+ --color-brand-white: #fff;
131
+ --color-primary: var(--color-brand-blue);
132
+ --color-primary-rgb: 29, 218, 188;
133
+ --space-2xs: 0.125rem;
134
+ --space-xs: 0.25rem;
135
+ --space-sm: 0.5rem;
136
+ --space-base: 0.75rem;
137
+ --space-md: 1rem;
138
+ --space-md-lg: 1.25rem;
139
+ --space-lg: 1.5rem;
140
+ --space-xl: 2rem;
141
+ --space-1-5xl: 2.5rem;
142
+ --space-2xl: 3rem;
143
+ --space-3xl: 4rem;
144
+ --space-4xl: 6rem;
145
+ --space-3-5xl: 5rem;
146
+ --radius-base: 0.5rem;
147
+ --radius-lg: 1rem;
148
+ --radius-2xl: 2rem;
149
+ --radius-full: 9999px;
150
+ --font-family: inter, system-ui, sans-serif;
151
+ --font-weight-bold: 800;
152
+ --color-white: #fff;
153
+ }
154
+
155
+ :root,
156
+ .theme-light {
157
+ --bg-main: #f0f4f8;
158
+ --bg-page: #f0f4f8;
159
+ --bg-surface: #fff;
160
+ --bg-muted: #f1f5f9;
161
+ --text-main: #000;
162
+ --text-base: #000;
163
+ --text-muted: #4a5568;
164
+ --text-dimmed: #64748b;
165
+ --border-base: #e2e8f0;
166
+ --border-color: #e2e8f0;
167
+ --glass-bg: rgba(0, 0, 0, 0.05);
168
+ --glass-border: rgba(0, 0, 0, 0.1);
169
+ --shadow-base: rgba(0, 0, 0, 0.05);
170
+ --shadow-hover: rgba(0, 0, 0, 0.1);
171
+ --primary: var(--color-primary);
172
+ --primary-10: rgba(29, 218, 188, 0.1);
173
+ }
174
+
175
+ .theme-dark {
176
+ --bg-main: #000;
177
+ --bg-page: #000;
178
+ --bg-surface: #0f172a;
179
+ --bg-muted: #1e293b;
180
+ --text-main: #fff;
181
+ --text-base: #fff;
182
+ --text-muted: #94a3b8;
183
+ --text-dimmed: #64748b;
184
+ --border-base: #1e293b;
185
+ --border-color: #1e293b;
186
+ --glass-bg: rgba(255, 255, 255, 0.03);
187
+ --glass-border: rgba(255, 255, 255, 0.08);
188
+ --shadow-base: rgba(0, 0, 0, 0.3);
189
+ --shadow-hover: rgba(0, 0, 0, 0.4);
190
+ --primary: var(--color-primary);
191
+ --primary-10: rgba(29, 218, 188, 0.1);
192
+ }
193
+
194
+ *, *::before, *::after {
195
+ box-sizing: border-box;
196
+ }
197
+
198
+ html {
199
+ background: var(--bg-main);
200
+ color: var(--text-main);
201
+ scroll-behavior: smooth;
202
+ }
203
+
204
+ body.production-page {
205
+ min-width: 320px;
206
+ min-height: 100vh;
207
+ margin: 0;
208
+ padding-top: 4.5rem;
209
+ background: var(--bg-main);
210
+ color: var(--text-main);
211
+ font-family: var(--font-family, Inter, system-ui, sans-serif);
212
+ }
213
+
214
+ body.production-page main {
215
+ width: 100%;
216
+ min-height: 100vh;
217
+ }
218
+ </style>