@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,262 @@
1
+ ---
2
+ import tinderProfile from "../assets/jjlmoya/tinder-profile.webp";
3
+ import tinderProfileWinked from "../assets/jjlmoya/tinder-profile-winked.webp";
4
+ import bobPointingSprite from "../assets/bob-model-book/bob-pointing.webp";
5
+ import bobTechnoTrack from "../assets/music/bob-techno.mp3";
6
+
7
+ interface Props {
8
+ t: {
9
+ textBefore: string;
10
+ wordWithRagebait: string;
11
+ textAfter: string;
12
+ ragebaitExplanation: string;
13
+ eyebrow: string;
14
+ title: string;
15
+ chipsLabel: string;
16
+ boardLabel: string;
17
+ alert: string;
18
+ portraitAlt: string;
19
+ poseLabel: string;
20
+ name: string;
21
+ role: string;
22
+ bio: string;
23
+ stats: string[][];
24
+ chips: string[];
25
+ stamp: string;
26
+ linksLabel: string;
27
+ partyPlayer: {
28
+ eyebrow: string;
29
+ title: string;
30
+ text: string;
31
+ audioLabel: string;
32
+ playLabel: string;
33
+ pauseLabel: string;
34
+ };
35
+ };
36
+ }
37
+
38
+ const { t } = Astro.props;
39
+ ---
40
+
41
+ <section class="team-section human-touch" aria-labelledby="human-touch-title" data-human-touch>
42
+ <div class="human-touch-party-player" aria-label={t.partyPlayer.audioLabel}>
43
+ <audio class="human-touch-audio" preload="metadata" data-human-touch-audio>
44
+ <source src={bobTechnoTrack} type="audio/mpeg" />
45
+ </audio>
46
+ <button
47
+ class="human-touch-player-toggle"
48
+ type="button"
49
+ aria-label={t.partyPlayer.playLabel}
50
+ data-play-label={t.partyPlayer.playLabel}
51
+ data-pause-label={t.partyPlayer.pauseLabel}
52
+ data-human-touch-toggle
53
+ >
54
+ <span class="human-touch-player-icon" aria-hidden="true"></span>
55
+ </button>
56
+ <div class="human-touch-player-wave" aria-hidden="true">
57
+ <span></span>
58
+ <span></span>
59
+ <span></span>
60
+ <span></span>
61
+ <span></span>
62
+ </div>
63
+ <div class="human-touch-disco-lights" aria-hidden="true">
64
+ <span></span>
65
+ <span></span>
66
+ <span></span>
67
+ <span></span>
68
+ <span></span>
69
+ <span></span>
70
+ </div>
71
+ <div class="human-touch-disco-bursts" aria-hidden="true">
72
+ <span></span>
73
+ <span></span>
74
+ <span></span>
75
+ <span></span>
76
+ <span></span>
77
+ <span></span>
78
+ <span></span>
79
+ <span></span>
80
+ </div>
81
+ <div class="human-touch-player-bar" data-human-touch-seek aria-hidden="true">
82
+ <span data-human-touch-progress></span>
83
+ </div>
84
+ <span class="human-touch-player-time" data-human-touch-current aria-hidden="true">0:00</span>
85
+ </div>
86
+ <div class="human-touch-equalizer" aria-hidden="true">
87
+ <span></span>
88
+ <span></span>
89
+ <span></span>
90
+ <span></span>
91
+ </div>
92
+
93
+ <div class="team-landing-inner">
94
+ <div class="human-touch-shell">
95
+ <div class="human-touch-copy">
96
+ <p class="team-section-eyebrow">{t.eyebrow}</p>
97
+ <h2 class="team-section-title" id="human-touch-title">
98
+ {t.title}
99
+ </h2>
100
+ <p class="team-section-text">
101
+ {t.textBefore}<span class="human-touch-ragebait-inline">{t.wordWithRagebait}</span>{t.textAfter}
102
+ </p>
103
+
104
+ <div class="human-touch-ragebait-box">
105
+ <p class="human-touch-ragebait-explanation">{t.ragebaitExplanation}</p>
106
+ </div>
107
+
108
+ <div
109
+ class="human-touch-chipstorm"
110
+ aria-label={t.chipsLabel}
111
+ >
112
+ {t.chips.map((chip) => <span>{chip}</span>)}
113
+ </div>
114
+ </div>
115
+
116
+ <div
117
+ class="human-touch-board"
118
+ aria-label={t.boardLabel}
119
+ >
120
+ <div class="human-touch-alert">
121
+ {t.alert}
122
+ </div>
123
+ <div class="human-touch-card">
124
+ <div class="human-touch-human">
125
+ <div class="human-touch-portrait">
126
+ <img
127
+ src={tinderProfile.src}
128
+ width={tinderProfile.width}
129
+ height={tinderProfile.height}
130
+ alt={t.portraitAlt}
131
+ loading="lazy"
132
+ class="human-touch-portrait-base"
133
+ />
134
+ <img
135
+ src={tinderProfileWinked.src}
136
+ width={tinderProfileWinked.width}
137
+ height={tinderProfileWinked.height}
138
+ alt=""
139
+ aria-hidden="true"
140
+ loading="lazy"
141
+ class="human-touch-portrait-wink"
142
+ />
143
+ </div>
144
+ <span>{t.poseLabel}</span>
145
+ </div>
146
+ <div class="human-touch-id">
147
+ <p>{t.name}</p>
148
+ <strong>{t.role}</strong>
149
+ <span>{t.bio}</span>
150
+ </div>
151
+ </div>
152
+
153
+ <div class="human-touch-stats">
154
+ {
155
+ t.stats.map(([label, value]) => (
156
+ <div>
157
+ <span>{label}</span>
158
+ <strong>{value}</strong>
159
+ </div>
160
+ ))
161
+ }
162
+ </div>
163
+
164
+ <div class="human-touch-bob-stamp" aria-hidden="true">
165
+ <img
166
+ src={bobPointingSprite.src}
167
+ width={bobPointingSprite.width}
168
+ height={bobPointingSprite.height}
169
+ alt=""
170
+ loading="lazy"
171
+ />
172
+ <span>{t.stamp}</span>
173
+ </div>
174
+
175
+ <div
176
+ class="human-touch-links"
177
+ aria-label={t.linksLabel}
178
+ >
179
+ <a
180
+ href="https://es.linkedin.com/in/jjlmoya"
181
+ target="_blank"
182
+ rel="noreferrer">LinkedIn</a
183
+ >
184
+ <a
185
+ href="https://github.com/jjlmoya"
186
+ target="_blank"
187
+ rel="noreferrer">GitHub</a
188
+ >
189
+ </div>
190
+ </div>
191
+ </div>
192
+ </div>
193
+ </section>
194
+
195
+ <script>
196
+ const humanTouch = document.querySelector<HTMLElement>('[data-human-touch]');
197
+ const humanTouchAudio = document.querySelector<HTMLAudioElement>('[data-human-touch-audio]');
198
+ const humanTouchToggle = document.querySelector<HTMLButtonElement>('[data-human-touch-toggle]');
199
+ const humanTouchProgress = document.querySelector<HTMLElement>('[data-human-touch-progress]');
200
+ const humanTouchSeek = document.querySelector<HTMLElement>('[data-human-touch-seek]');
201
+ const humanTouchCurrent = document.querySelector<HTMLElement>('[data-human-touch-current]');
202
+
203
+ humanTouchSeek?.addEventListener('click', (event) => {
204
+ if (!humanTouchAudio || !humanTouchSeek || !humanTouchAudio.duration) return;
205
+
206
+ const bounds = humanTouchSeek.getBoundingClientRect();
207
+ const ratio = Math.min(1, Math.max(0, (event.clientX - bounds.left) / bounds.width));
208
+ humanTouchAudio.currentTime = ratio * humanTouchAudio.duration;
209
+ });
210
+
211
+ const formatAudioTime = (seconds: number) => {
212
+ if (!Number.isFinite(seconds)) return '0:00';
213
+
214
+ const minutes = Math.floor(seconds / 60);
215
+ const remainingSeconds = Math.floor(seconds % 60);
216
+ return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
217
+ };
218
+
219
+ const updatePlayer = () => {
220
+ if (!humanTouchAudio) return;
221
+
222
+ const duration = humanTouchAudio.duration || 0;
223
+ const currentTime = humanTouchAudio.currentTime || 0;
224
+ const progress = duration > 0 ? currentTime / duration * 100 : 0;
225
+
226
+ if (humanTouchProgress) humanTouchProgress.style.inlineSize = `${progress}%`;
227
+ if (humanTouchCurrent) humanTouchCurrent.textContent = formatAudioTime(currentTime);
228
+ };
229
+
230
+ humanTouchAudio?.addEventListener('play', () => {
231
+ humanTouch?.classList.add('is-partying');
232
+ humanTouchToggle?.classList.add('is-playing');
233
+ humanTouchToggle?.setAttribute('aria-label', humanTouchToggle.dataset.pauseLabel ?? '');
234
+ });
235
+
236
+ humanTouchAudio?.addEventListener('pause', () => {
237
+ humanTouch?.classList.remove('is-partying');
238
+ humanTouchToggle?.classList.remove('is-playing');
239
+ humanTouchToggle?.setAttribute('aria-label', humanTouchToggle.dataset.playLabel ?? '');
240
+ });
241
+
242
+ humanTouchAudio?.addEventListener('ended', () => {
243
+ humanTouch?.classList.remove('is-partying');
244
+ humanTouchToggle?.classList.remove('is-playing');
245
+ humanTouchToggle?.setAttribute('aria-label', humanTouchToggle.dataset.playLabel ?? '');
246
+ updatePlayer();
247
+ });
248
+
249
+ humanTouchAudio?.addEventListener('loadedmetadata', updatePlayer);
250
+ humanTouchAudio?.addEventListener('timeupdate', updatePlayer);
251
+
252
+ humanTouchToggle?.addEventListener('click', () => {
253
+ if (!humanTouchAudio) return;
254
+
255
+ if (humanTouchAudio.paused) {
256
+ void humanTouchAudio.play();
257
+ return;
258
+ }
259
+
260
+ humanTouchAudio.pause();
261
+ });
262
+ </script>
@@ -0,0 +1,73 @@
1
+ ---
2
+ interface Scene {
3
+ number: string;
4
+ label: string;
5
+ title: string;
6
+ text: string;
7
+ visual: string;
8
+ asset: { src: string; width: number; height: number };
9
+ assetAlt: string;
10
+ formula: { label: string; asset: { src: string; width: number; height: number } }[];
11
+ }
12
+
13
+ interface Props {
14
+ scene: Scene;
15
+ }
16
+
17
+ const { scene } = Astro.props;
18
+ const firstFormula = scene.formula[0]!;
19
+ const secondFormula = scene.formula[1]!;
20
+ ---
21
+
22
+ <article
23
+ class={`master-plan-scene master-plan-scene-${scene.visual}`}
24
+ >
25
+ <div class="master-plan-art" aria-hidden="true">
26
+ <span class="master-plan-frame-label">
27
+ {scene.number}
28
+ </span>
29
+ <span class="master-plan-placeholder">
30
+ <img
31
+ class="master-plan-asset"
32
+ src={scene.asset.src}
33
+ width={scene.asset.width}
34
+ height={scene.asset.height}
35
+ alt={scene.assetAlt}
36
+ loading="lazy"
37
+ />
38
+ </span>
39
+ </div>
40
+ <div class="master-plan-copy">
41
+ <p class="master-plan-kicker">{scene.label}</p>
42
+ <h3>{scene.title}</h3>
43
+ {scene.formula && (
44
+ <div class="master-plan-equation">
45
+ <span class="master-plan-equation-term">
46
+ <img
47
+ src={firstFormula.asset.src}
48
+ width={firstFormula.asset.width}
49
+ height={firstFormula.asset.height}
50
+ alt={firstFormula.label}
51
+ loading="lazy"
52
+ />
53
+ </span>
54
+ <span
55
+ class="master-plan-equation-arrow"
56
+ aria-hidden="true"
57
+ >
58
+ &gt;
59
+ </span>
60
+ <span class="master-plan-equation-term">
61
+ <img
62
+ src={secondFormula.asset.src}
63
+ width={secondFormula.asset.width}
64
+ height={secondFormula.asset.height}
65
+ alt={secondFormula.label}
66
+ loading="lazy"
67
+ />
68
+ </span>
69
+ </div>
70
+ )}
71
+ <p>{scene.text}</p>
72
+ </div>
73
+ </article>
@@ -0,0 +1,46 @@
1
+ ---
2
+
3
+ interface Props {
4
+ t: {
5
+ eyebrow: string;
6
+ title: string;
7
+ text: string;
8
+ memories: {
9
+ m1: { name: string; line: string };
10
+ m2: { name: string; line: string };
11
+ m3: { name: string; line: string };
12
+ m4: { name: string; line: string };
13
+ };
14
+ };
15
+ }
16
+
17
+ const { t } = Astro.props;
18
+
19
+ const memories = [
20
+ { name: t.memories.m1.name, line: t.memories.m1.line, mark: '01', color: '#f8d66d' },
21
+ { name: t.memories.m2.name, line: t.memories.m2.line, mark: '02', color: '#2dd4bf' },
22
+ { name: t.memories.m3.name, line: t.memories.m3.line, mark: '03', color: '#f15aa0' },
23
+ { name: t.memories.m4.name, line: t.memories.m4.line, mark: '04', color: '#60a5fa' },
24
+ ];
25
+ ---
26
+
27
+ <section class="team-section memory-wall" aria-labelledby="memory-wall-title">
28
+ <div class="team-landing-inner">
29
+ <p class="team-section-eyebrow">{t.eyebrow}</p>
30
+ <h2 class="team-section-title" id="memory-wall-title">{t.title}</h2>
31
+ <p class="team-section-text">
32
+ {t.text}
33
+ </p>
34
+ <div class="memory-wall-board">
35
+ {
36
+ memories.map((memory) => (
37
+ <article class="memory-card" style={`--memory-color: ${memory.color}`}>
38
+ <div class="memory-card-photo" aria-hidden="true">{memory.mark}</div>
39
+ <h3 class="memory-card-name">{memory.name}</h3>
40
+ <p class="memory-card-line">{memory.line}</p>
41
+ </article>
42
+ ))
43
+ }
44
+ </div>
45
+ </div>
46
+ </section>
@@ -0,0 +1,46 @@
1
+ ---
2
+ import sealOfApproval from '../assets/seal-of-approval.webp';
3
+
4
+ interface Props {
5
+ t: {
6
+ eyebrow: string;
7
+ title: string;
8
+ description: string;
9
+ values: string[];
10
+ };
11
+ }
12
+
13
+ const { t } = Astro.props;
14
+
15
+ const regularValues = t.values.slice(0, -1);
16
+ const lastValue = t.values[t.values.length - 1];
17
+ ---
18
+
19
+ <section class="team-section product-manifesto" aria-labelledby="product-manifesto-title">
20
+ <div class="team-landing-inner">
21
+ <div class="product-manifesto-top">
22
+ <p class="team-section-eyebrow">{t.eyebrow}</p>
23
+ <h2 class="team-section-title" id="product-manifesto-title">{t.title}</h2>
24
+ </div>
25
+
26
+ <div class="product-manifesto-mid">
27
+ <p class="product-manifesto-description">{t.description}</p>
28
+ <div class="product-manifesto-seal" aria-hidden="true">
29
+ <img
30
+ src={sealOfApproval.src}
31
+ width={sealOfApproval.width}
32
+ height={sealOfApproval.height}
33
+ alt=""
34
+ loading="lazy"
35
+ />
36
+ </div>
37
+ </div>
38
+
39
+ <div class="product-manifesto-list" role="list" aria-label={t.eyebrow}>
40
+ {regularValues.map((value) => (
41
+ <span class="product-manifesto-item" role="listitem">{value}</span>
42
+ ))}
43
+ <span class="product-manifesto-item product-manifesto-item-miau" role="listitem">{lastValue}</span>
44
+ </div>
45
+ </div>
46
+ </section>
@@ -0,0 +1,67 @@
1
+ ---
2
+ import type { KnownLocale } from '../../../types';
3
+ import feedbackImage from '../assets/feedback.webp';
4
+ import formulaMoney from '../assets/master-plan-money.webp';
5
+ import formulaFood from '../assets/master-plan-more-food.webp';
6
+
7
+ interface Props {
8
+ locale: KnownLocale;
9
+ t: {
10
+ eyebrow: string;
11
+ title: string;
12
+ text: string;
13
+ linkText: string;
14
+ comingSoonLabel: string;
15
+ inbox: {
16
+ ideas: string;
17
+ bugs: string;
18
+ fixes: string;
19
+ support: string;
20
+ };
21
+ };
22
+ }
23
+
24
+ const { locale, t } = Astro.props;
25
+ void locale;
26
+ ---
27
+
28
+ <section class="team-section support-transition" aria-labelledby="support-transition-title">
29
+ <div class="team-landing-inner support-transition-layout">
30
+ <div class="support-transition-content">
31
+ <p class="team-section-eyebrow">{t.eyebrow}</p>
32
+ <h2 class="team-section-title" id="support-transition-title">{t.title}</h2>
33
+ <p class="team-section-text">
34
+ {t.text}
35
+ </p>
36
+ <div class="support-transition-group">
37
+ <span class="support-transition-coming-soon">{t.comingSoonLabel}</span>
38
+ <button class="support-transition-link is-disabled" type="button" disabled>
39
+ {t.linkText}
40
+ </button>
41
+ </div>
42
+ </div>
43
+
44
+ <div class="support-transition-visual" aria-hidden="true">
45
+ <div class="support-transition-zzz">
46
+ <span>Z</span>
47
+ <span>z</span>
48
+ <span>z</span>
49
+ <span class="support-transition-bubble-icon">
50
+ <img src={formulaFood.src} width={formulaFood.width} height={formulaFood.height} alt="" />
51
+ </span>
52
+ <span class="support-transition-bubble-icon">
53
+ <img src={formulaMoney.src} width={formulaMoney.width} height={formulaMoney.height} alt="" />
54
+ </span>
55
+ </div>
56
+ <div class="support-transition-frame">
57
+ <img
58
+ src={feedbackImage.src}
59
+ width={feedbackImage.width}
60
+ height={feedbackImage.height}
61
+ alt=""
62
+ loading="lazy"
63
+ />
64
+ </div>
65
+ </div>
66
+ </div>
67
+ </section>
@@ -0,0 +1,67 @@
1
+ ---
2
+
3
+ interface Props {
4
+ t: {
5
+ eyebrow: string;
6
+ title: string;
7
+ scenes: {
8
+ strategic: { title: string; setting: string; speech: string[]; propLabel: string };
9
+ qa: { title: string; setting: string; speech: string[]; propLabel: string };
10
+ implementation: { title: string; setting: string; speech: string[]; propLabel: string };
11
+ };
12
+ };
13
+ }
14
+
15
+ const { t } = Astro.props;
16
+
17
+ const scenes = [
18
+ {
19
+ title: t.scenes.strategic.title,
20
+ setting: t.scenes.strategic.setting,
21
+ speech: t.scenes.strategic.speech,
22
+ propLabel: t.scenes.strategic.propLabel,
23
+ prop: 'map',
24
+ },
25
+ {
26
+ title: t.scenes.qa.title,
27
+ setting: t.scenes.qa.setting,
28
+ speech: t.scenes.qa.speech,
29
+ propLabel: t.scenes.qa.propLabel,
30
+ prop: 'papers',
31
+ },
32
+ {
33
+ title: t.scenes.implementation.title,
34
+ setting: t.scenes.implementation.setting,
35
+ speech: t.scenes.implementation.speech,
36
+ propLabel: t.scenes.implementation.propLabel,
37
+ prop: 'keyboard',
38
+ },
39
+ ];
40
+ ---
41
+
42
+ <section class="team-section team-comic" aria-labelledby="team-comic-title">
43
+ <div class="team-landing-inner">
44
+ <p class="team-section-eyebrow">{t.eyebrow}</p>
45
+ <h2 class="team-section-title" id="team-comic-title">{t.title}</h2>
46
+ <div class="team-comic-strip">
47
+ {
48
+ scenes.map((scene) => (
49
+ <article class={`team-comic-panel team-comic-panel-${scene.prop}`}>
50
+ <div class="team-comic-art" aria-hidden="true">
51
+ <span class="team-comic-cat"></span>
52
+ <span class="team-comic-human"></span>
53
+ <span class="team-comic-prop">{scene.propLabel}</span>
54
+ </div>
55
+ <div class="team-comic-content">
56
+ <h3>{scene.title}</h3>
57
+ <p>{scene.setting}</p>
58
+ <div class="team-comic-bubbles">
59
+ {scene.speech.map((line) => <span>{line}</span>)}
60
+ </div>
61
+ </div>
62
+ </article>
63
+ ))
64
+ }
65
+ </div>
66
+ </div>
67
+ </section>
@@ -0,0 +1,42 @@
1
+ ---
2
+ import heroSceneUrl from "../assets/gamebob-team-hero.webp";
3
+
4
+ interface Props {
5
+ t: {
6
+ kicker: string;
7
+ titlePrefix: string;
8
+ titleAccent: string;
9
+ lead: string;
10
+ stageLabel: string;
11
+ sceneAlt: string;
12
+ };
13
+ }
14
+
15
+ const { t } = Astro.props;
16
+ ---
17
+
18
+ <section class="team-hero" aria-labelledby="team-hero-title">
19
+ <div class="team-landing-inner team-hero-grid">
20
+ <div class="team-hero-copy">
21
+ <div class="team-hero-kicker">
22
+ <span class="team-hero-dot" aria-hidden="true"></span>
23
+ {t.kicker}
24
+ </div>
25
+ <slot name="title" />
26
+ <p class="team-hero-lead">{t.lead}</p>
27
+ </div>
28
+ <div
29
+ class="team-hero-stage"
30
+ aria-label={t.stageLabel}
31
+ >
32
+ <img
33
+ class="team-hero-scene"
34
+ src={heroSceneUrl.src}
35
+ width={heroSceneUrl.width}
36
+ height={heroSceneUrl.height}
37
+ alt={t.sceneAlt}
38
+ loading="eager"
39
+ />
40
+ </div>
41
+ </div>
42
+ </section>