@jjlmoya/landings 0.4.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 (90) hide show
  1. package/package.json +65 -0
  2. package/scripts/copy-public-assets.mjs +44 -0
  3. package/src/components/PreviewNavSidebar.astro +102 -0
  4. package/src/components/PreviewToolbar.astro +139 -0
  5. package/src/entries.ts +9 -0
  6. package/src/env.d.ts +4 -0
  7. package/src/index.ts +12 -0
  8. package/src/landing/team/assets/ambar-chiquilla.webp +0 -0
  9. package/src/landing/team/assets/ambar.webp +0 -0
  10. package/src/landing/team/assets/bob-model-book/bob-pointing.webp +0 -0
  11. package/src/landing/team/assets/bob-model-book/bob-pose-05.webp +0 -0
  12. package/src/landing/team/assets/bob-model-book/bob-pose-06.webp +0 -0
  13. package/src/landing/team/assets/bob-model-book/bob-pose-07-locked.webp +0 -0
  14. package/src/landing/team/assets/bob-model-book/bob-pose-08-locked.webp +0 -0
  15. package/src/landing/team/assets/bob-model-book/bob-pose-09-locked.webp +0 -0
  16. package/src/landing/team/assets/bob-model-book/bob-pose-attitude.webp +0 -0
  17. package/src/landing/team/assets/bob-model-book/bob-pose-cover.webp +0 -0
  18. package/src/landing/team/assets/bob-model-book/bob-pose-expensive.webp +0 -0
  19. package/src/landing/team/assets/bob-model-book/bob-pose-final.webp +0 -0
  20. package/src/landing/team/assets/bob.webp +0 -0
  21. package/src/landing/team/assets/drac.webp +0 -0
  22. package/src/landing/team/assets/feedback.webp +0 -0
  23. package/src/landing/team/assets/gamebob-team-hero.png +0 -0
  24. package/src/landing/team/assets/gamebob-team-hero.webp +0 -0
  25. package/src/landing/team/assets/jjlmoya/jjlmoya-exhausted.webp +0 -0
  26. package/src/landing/team/assets/jjlmoya/tinder-profile-winked.webp +0 -0
  27. package/src/landing/team/assets/jjlmoya/tinder-profile.webp +0 -0
  28. package/src/landing/team/assets/master-plan-food.webp +0 -0
  29. package/src/landing/team/assets/master-plan-money.webp +0 -0
  30. package/src/landing/team/assets/master-plan-more-food.webp +0 -0
  31. package/src/landing/team/assets/master-plan-more-money.webp +0 -0
  32. package/src/landing/team/assets/master-plan-projects.webp +0 -0
  33. package/src/landing/team/assets/master-plan-tools.webp +0 -0
  34. package/src/landing/team/assets/memorial/ambar-chiquillo.webp +0 -0
  35. package/src/landing/team/assets/music/bob-techno.mp3 +0 -0
  36. package/src/landing/team/assets/seal-of-approval.webp +0 -0
  37. package/src/landing/team/assets/terra.webp +0 -0
  38. package/src/landing/team/assets/webcomic/scene-1.webp +0 -0
  39. package/src/landing/team/assets/webcomic/scene-2.webp +0 -0
  40. package/src/landing/team/assets/webcomic/scene-3.webp +0 -0
  41. package/src/landing/team/assets/webcomic/scene-4.webp +0 -0
  42. package/src/landing/team/components/BobMasterPlan.astro +118 -0
  43. package/src/landing/team/components/BobModelBook.astro +253 -0
  44. package/src/landing/team/components/BobModelBookFrame.astro +73 -0
  45. package/src/landing/team/components/CompanyWorks.astro +180 -0
  46. package/src/landing/team/components/HumanTouch.astro +262 -0
  47. package/src/landing/team/components/MasterPlanScene.astro +73 -0
  48. package/src/landing/team/components/MemoryWall.astro +46 -0
  49. package/src/landing/team/components/ProductManifesto.astro +46 -0
  50. package/src/landing/team/components/SupportTransition.astro +67 -0
  51. package/src/landing/team/components/TeamComicScenes.astro +67 -0
  52. package/src/landing/team/components/TeamHero.astro +42 -0
  53. package/src/landing/team/components/TeamRoster.astro +94 -0
  54. package/src/landing/team/entry.ts +23 -0
  55. package/src/landing/team/i18n/en.ts +278 -0
  56. package/src/landing/team/i18n/es.ts +278 -0
  57. package/src/landing/team/i18n/index.ts +12 -0
  58. package/src/landing/team/landing.astro +31 -0
  59. package/src/landing/team/styles/BehindJoke.css +55 -0
  60. package/src/landing/team/styles/BobMasterPlan.css +213 -0
  61. package/src/landing/team/styles/BobModelBook.css +709 -0
  62. package/src/landing/team/styles/CompanyWorks.css +652 -0
  63. package/src/landing/team/styles/HumanTouch.css +1190 -0
  64. package/src/landing/team/styles/MemoryWall.css +50 -0
  65. package/src/landing/team/styles/ProductManifesto.css +164 -0
  66. package/src/landing/team/styles/SupportTransition.css +291 -0
  67. package/src/landing/team/styles/TeamComicScenes.css +129 -0
  68. package/src/landing/team/styles/TeamHero.css +149 -0
  69. package/src/landing/team/styles/TeamRoster.css +75 -0
  70. package/src/landing/team/styles/team-landing.css +83 -0
  71. package/src/layouts/PreviewLayout.astro +110 -0
  72. package/src/pages/[locale]/[slug].astro +70 -0
  73. package/src/pages/[locale].astro +165 -0
  74. package/src/pages/index.astro +3 -0
  75. package/src/styles/global.css +37 -0
  76. package/src/styles/tokens.css +107 -0
  77. package/src/tests/diacritics_density.test.ts +62 -0
  78. package/src/tests/i18n_coverage.test.ts +32 -0
  79. package/src/tests/inverted_punctuation.test.ts +67 -0
  80. package/src/tests/landing-test-helpers.ts +89 -0
  81. package/src/tests/landing_exports.test.ts +29 -0
  82. package/src/tests/mocks/astro_mock.js +1 -0
  83. package/src/tests/no_en_dash.test.ts +45 -0
  84. package/src/tests/no_h1_in_components.test.ts +33 -0
  85. package/src/tests/pagespeed_best_practices.test.ts +143 -0
  86. package/src/tests/script_density.test.ts +57 -0
  87. package/src/tests/slug_language_code_format.test.ts +20 -0
  88. package/src/tests/slug_uniqueness.test.ts +74 -0
  89. package/src/tests/title_quality.test.ts +34 -0
  90. package/src/types.ts +38 -0
@@ -0,0 +1,149 @@
1
+ .team-hero {
2
+ --hero-code-green: #2dd4bf;
3
+
4
+ position: relative;
5
+ min-height: 660px;
6
+ display: grid;
7
+ align-items: center;
8
+ overflow: hidden;
9
+ padding: 2.8rem 0;
10
+ color: var(--team-ink);
11
+ }
12
+
13
+ .team-hero-grid {
14
+ position: relative;
15
+ z-index: 1;
16
+ display: grid;
17
+ grid-template-columns: minmax(0, 0.92fr) minmax(420px, 1.08fr);
18
+ gap: 1rem;
19
+ align-items: center;
20
+ }
21
+
22
+ .team-hero-copy {
23
+ min-width: 0;
24
+ padding-left: 0.4rem;
25
+ }
26
+
27
+ .team-hero-kicker {
28
+ display: inline-flex;
29
+ align-items: center;
30
+ gap: 0.72rem;
31
+ margin-bottom: 1.05rem;
32
+ color: var(--team-ink);
33
+ font-size: 0.92rem;
34
+ font-weight: var(--font-weight-bold, 800);
35
+ }
36
+
37
+ .team-hero-dot {
38
+ position: relative;
39
+ width: 0.78rem;
40
+ height: 0.78rem;
41
+ border-radius: 99px;
42
+ background: var(--hero-code-green);
43
+ box-shadow:
44
+ 0 0 0 5px rgba(45, 212, 191, 0.18),
45
+ 0 0 22px rgba(45, 212, 191, 0.45);
46
+ }
47
+
48
+ .team-hero-dot::before {
49
+ position: absolute;
50
+ inset: -12px;
51
+ border: 1px solid rgba(45, 212, 191, 0.55);
52
+ border-radius: 999px;
53
+ animation: mallorca-radar 2.2s ease-out infinite;
54
+ content: "";
55
+ }
56
+
57
+ .team-hero-title {
58
+ max-width: 100%;
59
+ margin: 0;
60
+ color: var(--team-ink);
61
+ font-size: clamp(4.6rem, 8vw, 7rem);
62
+ font-weight: var(--font-weight-bold, 800);
63
+ letter-spacing: 0;
64
+ line-height: 0.9;
65
+ white-space: nowrap;
66
+ }
67
+
68
+ .team-hero-title-accent {
69
+ color: var(--color-gamebob-primary, var(--team-mint));
70
+ }
71
+
72
+ .team-hero-lead {
73
+ max-width: 620px;
74
+ margin: 1rem 0 0;
75
+ color: var(--team-ink);
76
+ font-size: clamp(1.03rem, 1.8vw, 1.24rem);
77
+ font-weight: var(--font-weight-bold, 800);
78
+ letter-spacing: 0;
79
+ line-height: 1.25;
80
+ }
81
+
82
+ .team-hero-stage {
83
+ position: relative;
84
+ min-height: 520px;
85
+ min-width: 0;
86
+ }
87
+
88
+ .team-hero-scene {
89
+ position: absolute;
90
+ top: 50%;
91
+ right: 0;
92
+ width: min(590px, 100%);
93
+ height: auto;
94
+ transform: translateY(-50%);
95
+ filter: drop-shadow(0 28px 54px rgba(0, 0, 0, 0.18));
96
+ image-rendering: auto;
97
+ }
98
+
99
+ @keyframes mallorca-radar {
100
+ 0% {
101
+ transform: scale(0.55);
102
+ opacity: 0.9;
103
+ }
104
+
105
+ 55% {
106
+ opacity: 0.24;
107
+ }
108
+
109
+ 100% {
110
+ transform: scale(2.15);
111
+ opacity: 0;
112
+ }
113
+ }
114
+
115
+ @media (max-width: 980px) {
116
+ .team-hero-grid {
117
+ grid-template-columns: 1fr;
118
+ gap: 0;
119
+ }
120
+
121
+ .team-hero-copy {
122
+ padding-left: 0;
123
+ }
124
+
125
+ .team-hero-stage {
126
+ min-height: 500px;
127
+ }
128
+
129
+ .team-hero-scene {
130
+ right: 50%;
131
+ width: min(660px, 120%);
132
+ transform: translate(50%, -50%);
133
+ }
134
+ }
135
+
136
+ @media (max-width: 560px) {
137
+ .team-hero {
138
+ min-height: 640px;
139
+ padding: 2.4rem 0;
140
+ }
141
+
142
+ .team-hero-title {
143
+ font-size: clamp(3.6rem, 17vw, 5.4rem);
144
+ }
145
+
146
+ .team-hero-stage {
147
+ min-height: 360px;
148
+ }
149
+ }
@@ -0,0 +1,75 @@
1
+ .team-roster-grid {
2
+ display: grid;
3
+ grid-template-columns: repeat(3, minmax(0, 1fr));
4
+ gap: 1rem;
5
+ margin-top: 2rem;
6
+ }
7
+
8
+ .team-member-card {
9
+ position: relative;
10
+ min-height: 230px;
11
+ overflow: hidden;
12
+ border: 1px solid var(--team-border);
13
+ border-radius: 18px;
14
+ padding: 1.1rem;
15
+ transition:
16
+ transform 0.22s ease,
17
+ border-color 0.22s ease;
18
+ }
19
+
20
+ .team-member-card:hover {
21
+ transform: translateY(-6px);
22
+ border-color: color-mix(in srgb, var(--team-pink) 46%, var(--team-border));
23
+ }
24
+
25
+ .team-member-card-avatar {
26
+ display: grid;
27
+ width: 4rem;
28
+ height: 4rem;
29
+ place-items: center;
30
+ border-radius: 18px;
31
+ background: var(--member-color);
32
+ color: var(--team-deep);
33
+ font-size: 2rem;
34
+ font-weight: var(--font-weight-bold, 800);
35
+ }
36
+
37
+ .team-member-card-name {
38
+ margin: 1.3rem 0 0;
39
+ font-size: 1.45rem;
40
+ }
41
+
42
+ .team-member-card-role {
43
+ margin: 0.35rem 0 0;
44
+ color: var(--team-mint);
45
+ font-weight: var(--font-weight-bold, 800);
46
+ }
47
+
48
+ .team-member-card-bio {
49
+ margin: 1rem 0 0;
50
+ color: var(--team-muted);
51
+ line-height: 1.65;
52
+ }
53
+
54
+ .team-member-card-tag {
55
+ display: inline-flex;
56
+ margin-top: 1rem;
57
+ border: 1px solid var(--team-border);
58
+ border-radius: 999px;
59
+ padding: 0.45rem 0.68rem;
60
+ color: var(--team-muted);
61
+ font-size: 0.82rem;
62
+ font-weight: var(--font-weight-bold, 800);
63
+ }
64
+
65
+ @media (max-width: 920px) {
66
+ .team-roster-grid {
67
+ grid-template-columns: repeat(2, minmax(0, 1fr));
68
+ }
69
+ }
70
+
71
+ @media (max-width: 620px) {
72
+ .team-roster-grid {
73
+ grid-template-columns: 1fr;
74
+ }
75
+ }
@@ -0,0 +1,83 @@
1
+ .team-landing {
2
+ --team-ink: var(--text-base);
3
+ --team-muted: var(--text-muted);
4
+ --team-surface: var(--bg-surface);
5
+ --team-surface-strong: var(--bg-elevated);
6
+ --team-border: var(--border-base);
7
+ --team-pink: #f15aa0;
8
+ --team-mint: #2dd4bf;
9
+ --team-gold: #f8d66d;
10
+ --team-blue: #60a5fa;
11
+ --team-deep: #101318;
12
+ --team-page-bg: #050814;
13
+ --team-page-bg-soft: #0c111d;
14
+
15
+ position: relative;
16
+ isolation: isolate;
17
+ overflow: hidden;
18
+ color: var(--team-ink);
19
+ background:
20
+ radial-gradient(circle at 82% 9%, color-mix(in srgb, var(--team-blue) 13%, transparent), transparent 29rem),
21
+ radial-gradient(circle at 12% 24%, color-mix(in srgb, var(--team-pink) 10%, transparent), transparent 24rem),
22
+ linear-gradient(180deg, var(--team-page-bg), var(--team-page-bg-soft) 48%, var(--team-page-bg));
23
+ }
24
+
25
+ .team-landing * {
26
+ box-sizing: border-box;
27
+ }
28
+
29
+ .team-landing-inner {
30
+ width: min(1120px, calc(100% - 32px));
31
+ margin: 0 auto;
32
+ }
33
+
34
+ .team-section {
35
+ padding: 5rem 0;
36
+ }
37
+
38
+ .team-section-eyebrow {
39
+ margin: 0 0 0.75rem;
40
+ color: var(--team-mint);
41
+ font-size: 0.78rem;
42
+ font-weight: var(--font-weight-bold, 800);
43
+ letter-spacing: 0.08em;
44
+ text-transform: uppercase;
45
+ }
46
+
47
+ .team-section-title {
48
+ max-width: 760px;
49
+ margin: 0;
50
+ font-size: clamp(2rem, 6vw, 4.4rem);
51
+ line-height: 0.96;
52
+ }
53
+
54
+ .team-section-text {
55
+ max-width: 720px;
56
+ margin: 1.25rem 0 0;
57
+ color: var(--team-muted);
58
+ font-size: 1.08rem;
59
+ line-height: 1.8;
60
+ }
61
+
62
+ .theme-light .team-landing {
63
+ --team-page-bg: #f5f7fb;
64
+ --team-page-bg-soft: #eef5f2;
65
+ --team-surface: #fff;
66
+ --team-surface-strong: #f6f2ea;
67
+ --team-border: rgba(16, 19, 24, 0.14);
68
+ --team-gold: #b47a00;
69
+ }
70
+
71
+ .theme-dark .team-landing {
72
+ --team-page-bg: #050814;
73
+ --team-page-bg-soft: #0c111d;
74
+ --team-surface: #111318;
75
+ --team-surface-strong: #191c24;
76
+ --team-border: rgba(255, 255, 255, 0.13);
77
+ }
78
+
79
+ @media (max-width: 760px) {
80
+ .team-section {
81
+ padding: 3.5rem 0;
82
+ }
83
+ }
@@ -0,0 +1,110 @@
1
+ ---
2
+ import "@jjlmoya/utils-shared/theme.css";
3
+ import "../styles/global.css";
4
+ import PreviewToolbar from "../components/PreviewToolbar.astro";
5
+ import type { KnownLocale } from "../types";
6
+
7
+ interface Props {
8
+ title: string;
9
+ currentLocale?: KnownLocale;
10
+ localeUrls?: Partial<Record<KnownLocale, string>>;
11
+ hasSidebar?: boolean;
12
+ }
13
+
14
+ const { title, currentLocale = "es", localeUrls = {}, hasSidebar = false } = Astro.props;
15
+ ---
16
+
17
+ <!doctype html>
18
+ <html lang={currentLocale}>
19
+ <head>
20
+ <meta charset="UTF-8" />
21
+ <meta name="viewport" content="width=device-width" />
22
+ <title>{title} · preview</title>
23
+ <script is:inline>
24
+ (function () {
25
+ const saved = localStorage.getItem("theme") || "theme-dark";
26
+ document.documentElement.classList.add(saved);
27
+ })();
28
+ </script>
29
+ <slot name="head" />
30
+ </head>
31
+ <body>
32
+ <PreviewToolbar currentLocale={currentLocale} localeUrls={localeUrls} />
33
+ <div class:list={["page-wrapper", { "with-sidebar": hasSidebar }]}>
34
+ {
35
+ hasSidebar && (
36
+ <aside class="sidebar-area">
37
+ <slot name="sidebar" />
38
+ </aside>
39
+ )
40
+ }
41
+ <main>
42
+ <slot />
43
+ </main>
44
+ </div>
45
+ </body>
46
+ </html>
47
+
48
+ <style is:global>
49
+ :root {
50
+ --accent: #f43f5e;
51
+ --primary-base: #9f1239;
52
+ --color-gamebob-primary: var(--color-primary);
53
+ }
54
+
55
+ .theme-dark,
56
+ .theme-light {
57
+ --text-main: var(--text-base);
58
+ --border-color: var(--border-base);
59
+ }
60
+
61
+ *,
62
+ *::before,
63
+ *::after {
64
+ box-sizing: border-box;
65
+ }
66
+
67
+ body {
68
+ background-color: var(--bg-page);
69
+ color: var(--text-base);
70
+ margin: 0;
71
+ min-height: 100vh;
72
+ transition:
73
+ background-color 0.3s ease,
74
+ color 0.3s ease;
75
+ }
76
+
77
+ main {
78
+ padding: 0 2rem;
79
+ }
80
+
81
+ .page-wrapper {
82
+ display: flex;
83
+ flex-direction: column;
84
+ }
85
+
86
+ .page-wrapper.with-sidebar {
87
+ display: grid;
88
+ grid-template-columns: 240px 1fr;
89
+ min-height: 100vh;
90
+ }
91
+
92
+ .sidebar-area {
93
+ position: sticky;
94
+ top: 0;
95
+ height: 100vh;
96
+ overflow-y: auto;
97
+ border-right: 1px solid var(--border-color);
98
+ background: var(--bg-page);
99
+ }
100
+
101
+ @media (max-width: 768px) {
102
+ .page-wrapper.with-sidebar {
103
+ grid-template-columns: 1fr;
104
+ }
105
+
106
+ .sidebar-area {
107
+ display: none;
108
+ }
109
+ }
110
+ </style>
@@ -0,0 +1,70 @@
1
+ ---
2
+ import PreviewLayout from '../../layouts/PreviewLayout.astro';
3
+ import PreviewNavSidebar from '../../components/PreviewNavSidebar.astro';
4
+ import { ALL_LANDING_DEFINITIONS } from '../../entries';
5
+ import type { KnownLocale, LandingCardContent, LandingDefinition } from '../../types';
6
+ import type { AstroComponentFactory } from 'astro/runtime/server/index.js';
7
+
8
+ export async function getStaticPaths() {
9
+ const paths = [];
10
+
11
+ for (const def of ALL_LANDING_DEFINITIONS) {
12
+ const localeEntries = Object.entries(def.entry.i18n) as [KnownLocale, () => Promise<LandingCardContent>][];
13
+
14
+ const localeCards = await Promise.all(
15
+ localeEntries.map(async ([locale, loader]) => ({ locale, card: await loader() }))
16
+ );
17
+
18
+ const localeUrls: Partial<Record<KnownLocale, string>> = {};
19
+ for (const { locale, card } of localeCards) {
20
+ localeUrls[locale] = `/${locale}/${card.slug}`;
21
+ }
22
+
23
+ const allLandingsNav = await Promise.all(
24
+ ALL_LANDING_DEFINITIONS.map(async ({ entry }) => ({
25
+ id: entry.id,
26
+ i18n: entry.i18n,
27
+ }))
28
+ );
29
+
30
+ for (const { locale, card } of localeCards) {
31
+ const nav = await Promise.all(
32
+ allLandingsNav.map(async ({ id, i18n }) => {
33
+ const navLoader = i18n[locale] ?? i18n.en;
34
+ const navCard = await navLoader!();
35
+ return { id, title: navCard.title, href: `/${locale}/${navCard.slug}`, isActive: id === def.entry.id };
36
+ })
37
+ );
38
+
39
+ paths.push({
40
+ params: { locale, slug: card.slug },
41
+ props: { def, locale, card, localeUrls, nav },
42
+ });
43
+ }
44
+ }
45
+
46
+ return paths;
47
+ }
48
+
49
+ export interface Props {
50
+ def: LandingDefinition;
51
+ locale: KnownLocale;
52
+ card: LandingCardContent;
53
+ localeUrls: Partial<Record<KnownLocale, string>>;
54
+ nav: { id: string; title: string; href: string; isActive: boolean }[];
55
+ }
56
+
57
+ const { def, locale, card, localeUrls, nav } = Astro.props;
58
+ const { default: LandingComponent } = (await def.LandingComponent()) as { default: AstroComponentFactory };
59
+ const cssPath = def.entry.publicAssets?.cssPath;
60
+
61
+ const allStyles = import.meta.glob('../../landing/**/*.css');
62
+ const activeStyles = Object.keys(allStyles).filter((p) => p.includes(`/landing/${def.entry.id}/`));
63
+ await Promise.all(activeStyles.map((p) => allStyles[p]()));
64
+ ---
65
+
66
+ <PreviewLayout title={card.title} currentLocale={locale} localeUrls={localeUrls} hasSidebar={true}>
67
+ {cssPath && <link slot="head" rel="stylesheet" href={cssPath} />}
68
+ <PreviewNavSidebar slot="sidebar" categoryTitle="Landings" apps={nav} />
69
+ <LandingComponent locale={locale} />
70
+ </PreviewLayout>
@@ -0,0 +1,165 @@
1
+ ---
2
+ import PreviewLayout from '../layouts/PreviewLayout.astro';
3
+ import PreviewNavSidebar from '../components/PreviewNavSidebar.astro';
4
+ import { ALL_LANDING_DEFINITIONS } from '../entries';
5
+ import type { KnownLocale, LandingCardContent } from '../types';
6
+
7
+ export async function getStaticPaths() {
8
+ const locales: KnownLocale[] = ['es', 'en', 'fr', 'de', 'it', 'pt', 'nl', 'sv', 'pl', 'id', 'tr', 'ru', 'ja', 'ko', 'zh'];
9
+ return locales.map((locale) => ({ params: { locale } }));
10
+ }
11
+
12
+ const { locale } = Astro.params as { locale: KnownLocale };
13
+
14
+ const landingsWithContent = await Promise.all(
15
+ ALL_LANDING_DEFINITIONS.map(async ({ entry }) => {
16
+ const loader = entry.i18n[locale] ?? entry.i18n.en;
17
+ const card: LandingCardContent = await loader!();
18
+ return { entry, card };
19
+ })
20
+ );
21
+ ---
22
+
23
+ <PreviewLayout title="Landings · preview" currentLocale={locale} hasSidebar={true}>
24
+ <PreviewNavSidebar
25
+ slot="sidebar"
26
+ categoryTitle="Landings"
27
+ apps={landingsWithContent.map(({ entry, card }) => ({
28
+ id: entry.id,
29
+ title: card.title,
30
+ href: `/${locale}/${card.slug}`,
31
+ }))}
32
+ />
33
+
34
+ <div class="dashboard">
35
+ <header class="preview-header">
36
+ <span class="badge">preview · @jjlmoya/landings</span>
37
+ <h1>Landings</h1>
38
+ </header>
39
+
40
+ <div class="app-list">
41
+ {
42
+ landingsWithContent.map(({ entry, card }) => (
43
+ <article class="app-card">
44
+ <a href={`/${locale}/${card.slug}`} class="app-card-link">
45
+ <div class="app-card-content">
46
+ <h2 class="app-title">{card.title}</h2>
47
+ <p class="app-subtitle">{card.subtitle}</p>
48
+ <p class="app-description">{card.description}</p>
49
+ </div>
50
+ <div class="app-card-meta">
51
+ <span class="app-id">{entry.id}</span>
52
+ </div>
53
+ </a>
54
+ </article>
55
+ ))
56
+ }
57
+ </div>
58
+ </div>
59
+ </PreviewLayout>
60
+
61
+ <style>
62
+ .dashboard {
63
+ display: flex;
64
+ flex-direction: column;
65
+ gap: 5rem;
66
+ }
67
+
68
+ .preview-header {
69
+ text-align: center;
70
+ padding-bottom: 3rem;
71
+ border-bottom: 1px solid var(--border-color);
72
+ }
73
+
74
+ .badge {
75
+ display: inline-block;
76
+ padding: 0.25rem 0.75rem;
77
+ background: var(--accent);
78
+ border-radius: 99px;
79
+ font-size: 0.7rem;
80
+ font-weight: 800;
81
+ margin-bottom: 1.5rem;
82
+ color: var(--text-base);
83
+ letter-spacing: 0.05em;
84
+ }
85
+
86
+ h1 {
87
+ font-size: clamp(2rem, 6vw, 3.5rem);
88
+ font-weight: 900;
89
+ margin: 0 0 1rem;
90
+ background: linear-gradient(to bottom, var(--text-base), var(--text-muted));
91
+ -webkit-background-clip: text;
92
+ -webkit-text-fill-color: transparent;
93
+ background-clip: text;
94
+ }
95
+
96
+ .app-list {
97
+ display: grid;
98
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
99
+ gap: 2rem;
100
+ }
101
+
102
+ .app-card {
103
+ display: flex;
104
+ flex-direction: column;
105
+ }
106
+
107
+ .app-card-link {
108
+ flex: 1;
109
+ display: flex;
110
+ flex-direction: column;
111
+ padding: 1.5rem;
112
+ background: var(--bg-surface);
113
+ border: 1px solid var(--border-color);
114
+ border-radius: 0.75rem;
115
+ text-decoration: none;
116
+ transition: all 0.2s ease;
117
+ }
118
+
119
+ .app-card-link:hover {
120
+ border-color: var(--accent);
121
+ background: rgba(244, 63, 94, 0.05);
122
+ transform: translateY(-2px);
123
+ }
124
+
125
+ .app-card-content {
126
+ flex: 1;
127
+ }
128
+
129
+ .app-title {
130
+ font-size: 1.25rem;
131
+ font-weight: 700;
132
+ margin: 0 0 0.25rem;
133
+ color: var(--text-base);
134
+ }
135
+
136
+ .app-subtitle {
137
+ font-size: 0.9rem;
138
+ color: var(--text-muted);
139
+ margin: 0 0 0.5rem;
140
+ }
141
+
142
+ .app-description {
143
+ font-size: 0.875rem;
144
+ color: var(--text-muted);
145
+ line-height: 1.5;
146
+ margin: 0;
147
+ }
148
+
149
+ .app-card-meta {
150
+ padding-top: 1rem;
151
+ border-top: 1px solid var(--border-color);
152
+ margin-top: 1.5rem;
153
+ }
154
+
155
+ .app-id {
156
+ display: inline-block;
157
+ font-size: 0.7rem;
158
+ background: var(--bg-page);
159
+ border: 1px solid var(--border-color);
160
+ padding: 0.35rem 0.75rem;
161
+ border-radius: 0.4rem;
162
+ color: var(--accent);
163
+ font-weight: 600;
164
+ }
165
+ </style>
@@ -0,0 +1,3 @@
1
+ ---
2
+ ---
3
+ <meta http-equiv="refresh" content="0;url=/es" />
@@ -0,0 +1,37 @@
1
+ @import url("./tokens.css");
2
+
3
+ html, body, * {
4
+ font-family: var(--font-family) !important;
5
+ }
6
+
7
+ html {
8
+ background: var(--bg-main);
9
+ color: var(--text-main);
10
+ scroll-behavior: smooth;
11
+ text-size-adjust: 100%;
12
+ }
13
+
14
+ body {
15
+ margin: 0;
16
+ padding: 0;
17
+ min-height: 100vh;
18
+ display: flex;
19
+ flex-direction: column;
20
+ }
21
+
22
+ * {
23
+ box-sizing: border-box;
24
+ }
25
+
26
+ ::selection {
27
+ background-color: var(--color-primary);
28
+ color: var(--color-brand-dark);
29
+ }
30
+
31
+ :root.is-locked,
32
+ :root.is-locked body {
33
+ overflow: hidden !important;
34
+ height: 100vh !important;
35
+ position: fixed !important;
36
+ width: 100% !important;
37
+ }