@jjlmoya/utils-astronomy 1.35.1 → 1.37.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 (86) 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/category/index.ts +2 -1
  5. package/src/components/ProductionBreadcrumb.astro +134 -0
  6. package/src/components/ProductionStructuredData.astro +46 -0
  7. package/src/components/ProductionWidget.astro +182 -0
  8. package/src/entries.ts +4 -2
  9. package/src/i18n/header-ui.ts +15 -0
  10. package/src/i18n/language-ui.ts +9 -0
  11. package/src/i18n/languages.ts +24 -0
  12. package/src/identity/brands.ts +5 -0
  13. package/src/index.ts +2 -1
  14. package/src/layouts/ProductionCategoryPage.astro +184 -0
  15. package/src/layouts/ProductionPage.astro +218 -0
  16. package/src/layouts/ProductionUtilityPage.astro +283 -0
  17. package/src/mfe/assets.ts +39 -0
  18. package/src/mfe/category-ui.ts +34 -0
  19. package/src/mfe/manifest.ts +45 -0
  20. package/src/mfe/routes.ts +50 -0
  21. package/src/pages/[locale]/[utilities]/[categories]/[category]/[slug]/manifest.json.ts +49 -0
  22. package/src/pages/[locale]/[utilities]/[categories]/[category]/[slug].astro +100 -0
  23. package/src/pages/[locale]/[utilities]/[categories]/[category].astro +44 -0
  24. package/src/pages/index.astro +4 -4
  25. package/src/pages/mfe-sitemaps/[locale]/[vertical]/sitemap.xml.ts +75 -0
  26. package/src/pages/utilidades/[slug]/manifest.json.ts +28 -0
  27. package/src/pages/utilidades/[slug].astro +90 -0
  28. package/src/pages/utilidades/categorias/[category].astro +38 -0
  29. package/src/tests/locale_completeness.test.ts +2 -2
  30. package/src/tests/mfe_cache_contract.test.ts +14 -0
  31. package/src/tests/mfe_manifest.test.ts +28 -0
  32. package/src/tests/registry_contract.test.ts +67 -0
  33. package/src/tests/tool_validation.test.ts +4 -3
  34. package/src/tool/astrophotographyExposureStackPlanner/astrophotography-exposure-stacking-planner.css +496 -0
  35. package/src/tool/astrophotographyExposureStackPlanner/bibliography.astro +14 -0
  36. package/src/tool/astrophotographyExposureStackPlanner/bibliography.ts +12 -0
  37. package/src/tool/astrophotographyExposureStackPlanner/component.astro +149 -0
  38. package/src/tool/astrophotographyExposureStackPlanner/controller.ts +125 -0
  39. package/src/tool/astrophotographyExposureStackPlanner/dom-views.ts +41 -0
  40. package/src/tool/astrophotographyExposureStackPlanner/entry.ts +29 -0
  41. package/src/tool/astrophotographyExposureStackPlanner/evaluator.ts +44 -0
  42. package/src/tool/astrophotographyExposureStackPlanner/i18n/de.ts +40 -0
  43. package/src/tool/astrophotographyExposureStackPlanner/i18n/en.ts +232 -0
  44. package/src/tool/astrophotographyExposureStackPlanner/i18n/es.ts +52 -0
  45. package/src/tool/astrophotographyExposureStackPlanner/i18n/fr.ts +40 -0
  46. package/src/tool/astrophotographyExposureStackPlanner/i18n/id.ts +40 -0
  47. package/src/tool/astrophotographyExposureStackPlanner/i18n/it.ts +40 -0
  48. package/src/tool/astrophotographyExposureStackPlanner/i18n/ja.ts +40 -0
  49. package/src/tool/astrophotographyExposureStackPlanner/i18n/ko.ts +40 -0
  50. package/src/tool/astrophotographyExposureStackPlanner/i18n/nl.ts +40 -0
  51. package/src/tool/astrophotographyExposureStackPlanner/i18n/pl.ts +40 -0
  52. package/src/tool/astrophotographyExposureStackPlanner/i18n/pt.ts +40 -0
  53. package/src/tool/astrophotographyExposureStackPlanner/i18n/ru.ts +40 -0
  54. package/src/tool/astrophotographyExposureStackPlanner/i18n/sv.ts +40 -0
  55. package/src/tool/astrophotographyExposureStackPlanner/i18n/tr.ts +40 -0
  56. package/src/tool/astrophotographyExposureStackPlanner/i18n/zh.ts +40 -0
  57. package/src/tool/astrophotographyExposureStackPlanner/index.ts +11 -0
  58. package/src/tool/astrophotographyExposureStackPlanner/localized.ts +59 -0
  59. package/src/tool/astrophotographyExposureStackPlanner/logic.test.ts +74 -0
  60. package/src/tool/astrophotographyExposureStackPlanner/logic.ts +95 -0
  61. package/src/tool/astrophotographyExposureStackPlanner/seo.astro +14 -0
  62. package/src/tool/astrophotographyExposureStackPlanner/storage.ts +22 -0
  63. package/src/tool/astrophotographyExposureStackPlanner/ui.ts +102 -0
  64. package/src/tool/meteorShowerObservingPlanner/dom-views.ts +38 -16
  65. package/src/tool/meteorShowerObservingPlanner/i18n/de.ts +219 -219
  66. package/src/tool/meteorShowerObservingPlanner/i18n/en.ts +219 -219
  67. package/src/tool/meteorShowerObservingPlanner/i18n/es.ts +219 -219
  68. package/src/tool/meteorShowerObservingPlanner/i18n/fr.ts +219 -219
  69. package/src/tool/meteorShowerObservingPlanner/i18n/id.ts +223 -223
  70. package/src/tool/meteorShowerObservingPlanner/i18n/it.ts +223 -223
  71. package/src/tool/meteorShowerObservingPlanner/i18n/ja.ts +219 -219
  72. package/src/tool/meteorShowerObservingPlanner/i18n/ko.ts +219 -219
  73. package/src/tool/meteorShowerObservingPlanner/i18n/nl.ts +223 -223
  74. package/src/tool/meteorShowerObservingPlanner/i18n/pl.ts +223 -223
  75. package/src/tool/meteorShowerObservingPlanner/i18n/pt.ts +223 -223
  76. package/src/tool/meteorShowerObservingPlanner/i18n/ru.ts +219 -219
  77. package/src/tool/meteorShowerObservingPlanner/i18n/sv.ts +223 -223
  78. package/src/tool/meteorShowerObservingPlanner/i18n/tr.ts +223 -223
  79. package/src/tool/meteorShowerObservingPlanner/i18n/zh.ts +219 -219
  80. package/src/tool/meteorShowerObservingPlanner/meteor-shower-observing-planner.css +16 -9
  81. package/src/tool/meteorShowerObservingPlanner/ui.ts +87 -87
  82. package/src/tools.ts +3 -1
  83. package/src/types.ts +2 -2
  84. package/src/worker.ts +27 -0
  85. package/src/pages/[locale]/[slug].astro +0 -161
  86. package/src/pages/[locale].astro +0 -251
@@ -1,87 +1,87 @@
1
- import type { BibliographyEntry } from '../../types';
2
-
3
- export interface ShowerPreset {
4
- id: string;
5
- name: string;
6
- peakMonth: number;
7
- peakDay: number;
8
- zhr: number;
9
- radiantConstellation: string;
10
- velocityKms: number;
11
- parentBody: string;
12
- }
13
-
14
- export interface ObservingPlannerInput {
15
- showerId: string;
16
- customZhr: number;
17
- latitude: number;
18
- bortleClass: number;
19
- moonPhase: number;
20
- sessionStartHour: number;
21
- sessionEndHour: number;
22
- }
23
-
24
- export interface HourlyCalculation {
25
- hour: number;
26
- formattedTime: string;
27
- radiantElevationDeg: number;
28
- effectiveZhr: number;
29
- qualityScore: number;
30
- moonInterferencePercent: number;
31
- isPeakHour: boolean;
32
- }
33
-
34
- export interface EvaluationResult {
35
- overallScore: number;
36
- bestWindowStart: string;
37
- bestWindowEnd: string;
38
- maxEffectiveRate: number;
39
- skyDarknessRating: string;
40
- moonImpactRating: string;
41
- badges: Array<{
42
- label: string;
43
- type: 'success' | 'warning' | 'info' | 'error';
44
- }>;
45
- hourlyBreakdown: HourlyCalculation[];
46
- }
47
-
48
- export interface MeteorShowerObservingPlannerUI {
49
- [key: string]: unknown;
50
- title: string;
51
- subtitle: string;
52
- presetLabel: string;
53
- customZhrLabel: string;
54
- latitudeLabel: string;
55
- bortleLabel: string;
56
- moonPhaseLabel: string;
57
- sessionHoursLabel: string;
58
- toLabel: string;
59
- classLabel: string;
60
- radiantTelemetryLabel: string;
61
- belowHorizonLabel: string;
62
- altLabel: string;
63
- hrUnit: string;
64
- presets: Record<string, string>;
65
- bortleDescriptions: Record<number, string>;
66
- moonPhaseNames: Record<string, string>;
67
- badgeLabels: Record<string, string>;
68
- resultsTitle: string;
69
- bestWindowLabel: string;
70
- maxRateLabel: string;
71
- skyQualityLabel: string;
72
- hourlyChartTitle: string;
73
- checklistTitle: string;
74
- checklistItems: string[];
75
- }
76
-
77
- export interface MeteorShowerObservingPlannerLocaleContent {
78
- slug: string;
79
- title: string;
80
- description: string;
81
- ui: MeteorShowerObservingPlannerUI;
82
- seo: unknown[];
83
- faq: Array<{ question: string; answer: string }>;
84
- howTo: Array<{ name: string; text: string }>;
85
- bibliography: BibliographyEntry[];
86
- schemas: unknown[];
87
- }
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export interface ShowerPreset {
4
+ id: string;
5
+ name: string;
6
+ peakMonth: number;
7
+ peakDay: number;
8
+ zhr: number;
9
+ radiantConstellation: string;
10
+ velocityKms: number;
11
+ parentBody: string;
12
+ }
13
+
14
+ export interface ObservingPlannerInput {
15
+ showerId: string;
16
+ customZhr: number;
17
+ latitude: number;
18
+ bortleClass: number;
19
+ moonPhase: number;
20
+ sessionStartHour: number;
21
+ sessionEndHour: number;
22
+ }
23
+
24
+ export interface HourlyCalculation {
25
+ hour: number;
26
+ formattedTime: string;
27
+ radiantElevationDeg: number;
28
+ effectiveZhr: number;
29
+ qualityScore: number;
30
+ moonInterferencePercent: number;
31
+ isPeakHour: boolean;
32
+ }
33
+
34
+ export interface EvaluationResult {
35
+ overallScore: number;
36
+ bestWindowStart: string;
37
+ bestWindowEnd: string;
38
+ maxEffectiveRate: number;
39
+ skyDarknessRating: string;
40
+ moonImpactRating: string;
41
+ badges: Array<{
42
+ label: string;
43
+ type: 'success' | 'warning' | 'info' | 'error';
44
+ }>;
45
+ hourlyBreakdown: HourlyCalculation[];
46
+ }
47
+
48
+ export interface MeteorShowerObservingPlannerUI {
49
+ [key: string]: unknown;
50
+ title: string;
51
+ subtitle: string;
52
+ presetLabel: string;
53
+ customZhrLabel: string;
54
+ latitudeLabel: string;
55
+ bortleLabel: string;
56
+ moonPhaseLabel: string;
57
+ sessionHoursLabel: string;
58
+ toLabel: string;
59
+ classLabel: string;
60
+ radiantTelemetryLabel: string;
61
+ belowHorizonLabel: string;
62
+ altLabel: string;
63
+ hrUnit: string;
64
+ presets: Record<string, string>;
65
+ bortleDescriptions: Record<number, string>;
66
+ moonPhaseNames: Record<string, string>;
67
+ badgeLabels: Record<string, string>;
68
+ resultsTitle: string;
69
+ bestWindowLabel: string;
70
+ maxRateLabel: string;
71
+ skyQualityLabel: string;
72
+ hourlyChartTitle: string;
73
+ checklistTitle: string;
74
+ checklistItems: string[];
75
+ }
76
+
77
+ export interface MeteorShowerObservingPlannerLocaleContent {
78
+ slug: string;
79
+ title: string;
80
+ description: string;
81
+ ui: MeteorShowerObservingPlannerUI;
82
+ seo: unknown[];
83
+ faq: Array<{ question: string; answer: string }>;
84
+ howTo: Array<{ name: string; text: string }>;
85
+ bibliography: BibliographyEntry[];
86
+ schemas: unknown[];
87
+ }
package/src/tools.ts CHANGED
@@ -6,6 +6,7 @@ import { TELESCOPE_RESOLUTION_TOOL } from './tool/telescopeResolution';
6
6
  import { EYEPIECE_CALCULATOR_TOOL } from './tool/smartEyepieceCalculator';
7
7
  import { TELESCOPE_EXIT_PUPIL_PLANNER_TOOL } from './tool/telescopeExitPupilPlanner';
8
8
  import { METEOR_SHOWER_OBSERVING_PLANNER_TOOL } from './tool/meteorShowerObservingPlanner';
9
+ import { ASTROPHOTOGRAPHY_EXPOSURE_STACK_PLANNER_TOOL } from './tool/astrophotographyExposureStackPlanner';
9
10
  import type { ToolDefinition } from './types';
10
11
 
11
12
  export const ALL_TOOLS: ToolDefinition[] = [
@@ -16,6 +17,7 @@ export const ALL_TOOLS: ToolDefinition[] = [
16
17
  EYEPIECE_CALCULATOR_TOOL,
17
18
  TELESCOPE_EXIT_PUPIL_PLANNER_TOOL,
18
19
  METEOR_SHOWER_OBSERVING_PLANNER_TOOL,
20
+ ASTROPHOTOGRAPHY_EXPOSURE_STACK_PLANNER_TOOL,
19
21
  ];
20
22
 
21
23
  export {
@@ -26,5 +28,5 @@ export {
26
28
  EYEPIECE_CALCULATOR_TOOL,
27
29
  TELESCOPE_EXIT_PUPIL_PLANNER_TOOL,
28
30
  METEOR_SHOWER_OBSERVING_PLANNER_TOOL,
31
+ ASTROPHOTOGRAPHY_EXPOSURE_STACK_PLANNER_TOOL,
29
32
  };
30
-
package/src/types.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import type { SEOSection } from '@jjlmoya/utils-shared';
2
+ import type { UtilityLocale } from '@jjlmoya/utils-shared/routing';
2
3
 
3
4
  export type { SEOSection };
4
5
 
5
- export type KnownLocale =
6
- | 'de' | 'en' | 'es' | 'fr' | 'id' | 'it' | 'ja' | 'ko' | 'nl' | 'pl' | 'pt' | 'ru' | 'sv' | 'tr' | 'zh';
6
+ export type KnownLocale = UtilityLocale;
7
7
 
8
8
  export interface FAQItem {
9
9
  question: string;
package/src/worker.ts ADDED
@@ -0,0 +1,27 @@
1
+ interface UtilityMfeEnvironment {
2
+ ASSETS: { fetch(request: Request): Promise<Response> };
3
+ }
4
+
5
+ export const LONG_LIVED_ASSET_CACHE = "public, max-age=31536000, immutable";
6
+ export const SITEMAP_CACHE = "public, max-age=3600, s-maxage=3600, must-revalidate";
7
+
8
+ export const getCacheControl = (pathname: string): string | undefined => {
9
+ if (pathname.endsWith("/sitemap.xml")) return SITEMAP_CACHE;
10
+ if (pathname.endsWith("/manifest.json")) return LONG_LIVED_ASSET_CACHE;
11
+ if (pathname.startsWith("/_utilities/")) {
12
+ return LONG_LIVED_ASSET_CACHE;
13
+ }
14
+ return undefined;
15
+ };
16
+
17
+ export default {
18
+ async fetch(request: Request, environment: UtilityMfeEnvironment): Promise<Response> {
19
+ const response = await environment.ASSETS.fetch(request);
20
+ const cacheControl = getCacheControl(new URL(request.url).pathname);
21
+ if (!cacheControl) return response;
22
+
23
+ const headers = new Headers(response.headers);
24
+ headers.set("Cache-Control", cacheControl);
25
+ return new Response(response.body, { status: response.status, headers });
26
+ },
27
+ };
@@ -1,161 +0,0 @@
1
- ---
2
- import PreviewLayout from "../../layouts/PreviewLayout.astro";
3
- import PreviewNavSidebar from "../../components/PreviewNavSidebar.astro";
4
- import { ALL_TOOLS } from "../../index";
5
- import {
6
- UtilityHeader,
7
- FAQSection,
8
- Bibliography,
9
- SEORenderer,
10
- } from "@jjlmoya/utils-shared";
11
- import type { KnownLocale, ToolLocaleContent } from "../../types";
12
- import type { UtilitySEOContent } from "@jjlmoya/utils-shared";
13
-
14
- export async function getStaticPaths() {
15
- const paths = [];
16
-
17
- for (const { entry, Component: lazyComp } of ALL_TOOLS) {
18
- const { default: Component } = await lazyComp();
19
- const localeEntries = Object.entries(entry.i18n) as [
20
- KnownLocale,
21
- () => Promise<ToolLocaleContent>,
22
- ][];
23
- const localeContents = await Promise.all(
24
- localeEntries.map(async ([locale, loader]) => ({
25
- locale,
26
- content: await loader(),
27
- })),
28
- );
29
-
30
- const localeUrls = Object.fromEntries(
31
- localeContents.map(({ locale, content }) => [
32
- locale,
33
- `/${locale}/${content.slug}`,
34
- ]),
35
- ) as Partial<Record<KnownLocale, string>>;
36
-
37
- const firstLoader = entry.i18n.en ?? Object.values(entry.i18n)[0];
38
- const englishSlug = firstLoader ? (await firstLoader()).slug : entry.id;
39
-
40
- for (const { locale, content } of localeContents) {
41
- const allToolsNav = (
42
- await Promise.all(
43
- ALL_TOOLS.map(async ({ entry: navEntry }) => {
44
- const loader = navEntry.i18n[locale] ?? navEntry.i18n.en;
45
- if (!loader) return null;
46
- const navContent = await loader();
47
- return {
48
- id: navEntry.id,
49
- title: navContent.title,
50
- href: `/${locale}/${navContent.slug}`,
51
- isActive: navEntry.id === entry.id,
52
- };
53
- }),
54
- )
55
- ).filter(Boolean) as NavItem[];
56
- paths.push({
57
- params: { locale, slug: content.slug },
58
- props: { Component, locale, content, localeUrls, allToolsNav, englishSlug },
59
- });
60
- }
61
- }
62
-
63
- return paths;
64
- }
65
-
66
- interface NavItem {
67
- id: string;
68
- title: string;
69
- href: string;
70
- isActive?: boolean;
71
- }
72
-
73
- interface Props {
74
- Component: unknown;
75
- locale: KnownLocale;
76
- content: ToolLocaleContent;
77
- localeUrls: Partial<Record<KnownLocale, string>>;
78
- allToolsNav: NavItem[];
79
- englishSlug: string;
80
- }
81
-
82
- const { Component, locale, content, localeUrls, allToolsNav, englishSlug } = Astro.props;
83
-
84
- const cssFiles = import.meta.glob('../../tool/*/*.css', { query: '?raw', import: 'default' });
85
- const cssKey = Object.keys(cssFiles).find((k) => k.endsWith(`/${englishSlug}.css`));
86
- const toolCss = cssKey ? await cssFiles[cssKey]() as string : '';
87
-
88
- const seoContent: UtilitySEOContent = { locale, sections: content.seo ?? [] };
89
-
90
- const words = content.title.split(" ");
91
- const titleHighlight = words[0] || "";
92
- const titleBase = words.slice(1).join(" ") || "";
93
- ---
94
-
95
- <PreviewLayout
96
- title={content.title}
97
- currentLocale={locale}
98
- localeUrls={localeUrls}
99
- hasSidebar={true}
100
- >
101
- <PreviewNavSidebar
102
- slot="sidebar"
103
- categoryTitle="Tools"
104
- tools={allToolsNav}
105
- />
106
- <Fragment slot="head">
107
- <Fragment set:html={toolCss ? `<style>${toolCss}</style>` : ''} />
108
- {
109
- (content.schemas ?? []).map((schema) => (
110
- <script
111
- is:inline
112
- type="application/ld+json"
113
- set:html={JSON.stringify(schema)}
114
- />
115
- ))
116
- }
117
- </Fragment>
118
-
119
- <div class="tool-page">
120
- <UtilityHeader
121
- titleHighlight={titleHighlight}
122
- titleBase={titleBase}
123
- description={content.description}
124
- />
125
-
126
- <section class="section-tool">
127
- <Component ui={content.ui} />
128
- </section>
129
-
130
- <section class="section-seo">
131
- <SEORenderer content={seoContent} />
132
- </section>
133
-
134
- <section class="section-faq">
135
- <FAQSection items={content.faq} />
136
- </section>
137
-
138
- <section class="section-bibliography">
139
- <Bibliography links={content.bibliography} />
140
- </section>
141
- </div>
142
- </PreviewLayout>
143
-
144
- <style>
145
- .tool-page {
146
- display: flex;
147
- flex-direction: column;
148
- gap: 2rem;
149
- }
150
- .section-tool {
151
- max-width: 1200px;
152
- margin: 0 auto;
153
- width: 100%;
154
- }
155
- .section-seo,
156
- .section-faq,
157
- .section-bibliography {
158
- padding-top: 2rem;
159
- border-top: 1px solid var(--border-color);
160
- }
161
- </style>
@@ -1,251 +0,0 @@
1
- ---
2
- import PreviewLayout from '../layouts/PreviewLayout.astro';
3
- import PreviewNavSidebar from '../components/PreviewNavSidebar.astro';
4
- import { astronomyCategory, 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 astronomyCategory.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-astronomy</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>