@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
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@jjlmoya/landings",
3
+ "version": "0.4.0",
4
+ "type": "module",
5
+ "main": "./src/index.ts",
6
+ "types": "./src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./entries": "./src/entries.ts",
10
+ "./team": "./src/landing/team/landing.astro",
11
+ "./team/entry": "./src/landing/team/entry.ts"
12
+ },
13
+ "files": [
14
+ "src",
15
+ "scripts"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "scripts": {
21
+ "postinstall": "node ./scripts/copy-public-assets.mjs",
22
+ "dev": "astro dev",
23
+ "start": "astro dev",
24
+ "build": "astro build",
25
+ "preview": "astro preview",
26
+ "astro": "astro",
27
+ "lint": "eslint src/ --max-warnings 0 && stylelint \"src/**/*.{css,astro}\"",
28
+ "check": "astro check",
29
+ "type-check": "astro check",
30
+ "test": "vitest run",
31
+ "preversion": "npm run lint && npm run test",
32
+ "postversion": "git push && git push --tags",
33
+ "patch": "npm version patch",
34
+ "minor": "npm version minor",
35
+ "major": "npm version major"
36
+ },
37
+ "lint-staged": {
38
+ "*.{ts,tsx,astro}": [
39
+ "eslint --fix"
40
+ ]
41
+ },
42
+ "dependencies": {
43
+ "@iconify-json/mdi": "^1.2.3",
44
+ "@jjlmoya/prompagate": "^1.1.0",
45
+ "@jjlmoya/utils-shared": "1.2.0",
46
+ "astro": "^6.1.2",
47
+ "astro-icon": "^1.1.0"
48
+ },
49
+ "devDependencies": {
50
+ "@astrojs/check": "^0.9.8",
51
+ "eslint": "^9.39.4",
52
+ "eslint-plugin-astro": "^1.6.0",
53
+ "eslint-plugin-no-comments": "^1.1.10",
54
+ "husky": "^9.1.7",
55
+ "lint-staged": "^16.4.0",
56
+ "postcss-html": "^1.8.1",
57
+ "schema-dts": "^1.1.2",
58
+ "stylelint": "^17.6.0",
59
+ "stylelint-config-standard": "^40.0.0",
60
+ "stylelint-declaration-strict-value": "^1.11.1",
61
+ "typescript": "^5.4.0",
62
+ "typescript-eslint": "^8.58.0",
63
+ "vitest": "^4.1.2"
64
+ }
65
+ }
@@ -0,0 +1,44 @@
1
+ import { cp, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
6
+ const consumerRoot = process.env.INIT_CWD || process.cwd();
7
+ const publicRoot = join(consumerRoot, "public", "landings", "team");
8
+ const styleFiles = [
9
+ "team-landing.css",
10
+ "TeamHero.css",
11
+ "CompanyWorks.css",
12
+ "BobMasterPlan.css",
13
+ "TeamComicScenes.css",
14
+ "HumanTouch.css",
15
+ "ProductManifesto.css",
16
+ "BobModelBook.css",
17
+ "SupportTransition.css",
18
+ "MemoryWall.css",
19
+ "TeamRoster.css",
20
+ "BehindJoke.css",
21
+ ];
22
+
23
+ const copies = [
24
+ ["src/landing/team/assets", "assets"],
25
+ ];
26
+
27
+ await mkdir(publicRoot, { recursive: true });
28
+
29
+ for (const [from, to] of copies) {
30
+ await cp(join(packageRoot, from), join(publicRoot, to), {
31
+ recursive: true,
32
+ force: true,
33
+ errorOnExist: false,
34
+ });
35
+ }
36
+
37
+ const cssBundle = await Promise.all(
38
+ styleFiles.map(async (file) => {
39
+ const content = await readFile(join(packageRoot, "src", "landing", "team", "styles", file), "utf8");
40
+ return `/* ${file} */\n${content.trim()}\n`;
41
+ }),
42
+ );
43
+
44
+ await writeFile(join(publicRoot, "team.css"), `${cssBundle.join("\n")}\n`);
@@ -0,0 +1,102 @@
1
+ ---
2
+ interface NavItem {
3
+ id: string;
4
+ title: string;
5
+ href: string;
6
+ isActive?: boolean;
7
+ }
8
+
9
+ interface Props {
10
+ categoryTitle: string;
11
+ apps?: NavItem[];
12
+ }
13
+
14
+ const { categoryTitle, apps = [] } = Astro.props;
15
+ ---
16
+
17
+ <nav class="preview-nav-sidebar">
18
+ <div class="sidebar-header">
19
+ <h3>{categoryTitle}</h3>
20
+ </div>
21
+
22
+ <ul class="apps-list">
23
+ {
24
+ apps.map((app) => (
25
+ <li>
26
+ <a href={app.href} class:list={["app-link", { active: app.isActive }]}>
27
+ <span class="app-title">{app.title}</span>
28
+ </a>
29
+ </li>
30
+ ))
31
+ }
32
+ </ul>
33
+ </nav>
34
+
35
+ <style>
36
+ .preview-nav-sidebar {
37
+ display: flex;
38
+ flex-direction: column;
39
+ height: 100%;
40
+ background: var(--bg-surface, #0f172a);
41
+ }
42
+
43
+ .sidebar-header {
44
+ padding: 2rem 1.5rem 1.5rem;
45
+ border-bottom: 1px solid var(--border-color, #1e293b);
46
+ }
47
+
48
+ .sidebar-header h3 {
49
+ margin: 0;
50
+ font-size: 0.75rem;
51
+ font-weight: 900;
52
+ text-transform: uppercase;
53
+ letter-spacing: 0.1em;
54
+ color: var(--text-muted, #94a3b8);
55
+ }
56
+
57
+ .apps-list {
58
+ list-style: none;
59
+ margin: 0;
60
+ padding: 0.5rem 0;
61
+ display: flex;
62
+ flex-direction: column;
63
+ gap: 0.25rem;
64
+ }
65
+
66
+ .apps-list li {
67
+ margin: 0;
68
+ }
69
+
70
+ .app-link {
71
+ display: flex;
72
+ align-items: center;
73
+ padding: 0.75rem 1.5rem;
74
+ color: var(--text-muted, #94a3b8);
75
+ text-decoration: none;
76
+ font-size: 0.9375rem;
77
+ font-weight: 500;
78
+ transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
79
+ border-left: 3px solid transparent;
80
+ position: relative;
81
+ }
82
+
83
+ .app-link:hover {
84
+ color: var(--text-base, #f1f5f9);
85
+ background: rgba(255, 255, 255, 0.08);
86
+ padding-left: 1.75rem;
87
+ }
88
+
89
+ .app-link.active {
90
+ color: var(--accent, #f43f5e);
91
+ background: rgba(244, 63, 94, 0.15);
92
+ border-left-color: var(--accent, #f43f5e);
93
+ font-weight: 600;
94
+ }
95
+
96
+ .app-title {
97
+ display: block;
98
+ overflow: hidden;
99
+ text-overflow: ellipsis;
100
+ white-space: nowrap;
101
+ }
102
+ </style>
@@ -0,0 +1,139 @@
1
+ ---
2
+ import type { KnownLocale } from '../types';
3
+
4
+ interface Props {
5
+ currentLocale: KnownLocale;
6
+ localeUrls?: Partial<Record<KnownLocale, string>>;
7
+ }
8
+
9
+ const { currentLocale, localeUrls = {} } = Astro.props;
10
+
11
+ const otherLocales = Object.entries(localeUrls).filter(([l]) => l !== currentLocale) as [KnownLocale, string][];
12
+ ---
13
+
14
+ <nav class="preview-toolbar">
15
+ {
16
+ otherLocales.length > 0 && (
17
+ <div class="locale-group">
18
+ <span class="locale-current">{currentLocale.toUpperCase()}</span>
19
+ {otherLocales.map(([locale, url]) => (
20
+ <a href={url} class="btn-locale">
21
+ {locale.toUpperCase()}
22
+ </a>
23
+ ))}
24
+ </div>
25
+ )
26
+ }
27
+
28
+ <button id="theme-toggle" class="btn-theme" aria-label="Toggle theme" title="Toggle theme">
29
+ <span class="theme-icon"></span>
30
+ </button>
31
+ </nav>
32
+
33
+ <script>
34
+ function initToolbar() {
35
+ const btn = document.getElementById("theme-toggle");
36
+ if (!btn) return;
37
+
38
+ const applyTheme = (theme: string) => {
39
+ document.documentElement.classList.remove("theme-light", "theme-dark");
40
+ document.documentElement.classList.add(theme);
41
+ localStorage.setItem("theme", theme);
42
+ };
43
+
44
+ btn.addEventListener("click", () => {
45
+ const next = document.documentElement.classList.contains("theme-dark")
46
+ ? "theme-light"
47
+ : "theme-dark";
48
+ applyTheme(next);
49
+ });
50
+ }
51
+
52
+ initToolbar();
53
+ document.addEventListener("astro:after-swap", initToolbar);
54
+ </script>
55
+
56
+ <style>
57
+ .preview-toolbar {
58
+ position: fixed;
59
+ top: 1rem;
60
+ right: 1.5rem;
61
+ display: flex;
62
+ align-items: center;
63
+ gap: 0.5rem;
64
+ z-index: 1000;
65
+ background: var(--bg-surface);
66
+ padding: 0.4rem;
67
+ border-radius: 1rem;
68
+ border: 1px solid var(--border-color);
69
+ backdrop-filter: blur(12px);
70
+ }
71
+
72
+ .locale-group {
73
+ display: flex;
74
+ align-items: center;
75
+ gap: 0.25rem;
76
+ padding-right: 0.5rem;
77
+ border-right: 1px solid var(--border-color);
78
+ }
79
+
80
+ .locale-current {
81
+ font-size: 0.7rem;
82
+ font-weight: 900;
83
+ color: var(--text-muted);
84
+ padding: 0.4rem 0.6rem;
85
+ letter-spacing: 0.08em;
86
+ }
87
+
88
+ .btn-locale {
89
+ background: transparent;
90
+ border: 1px solid var(--border-color);
91
+ color: var(--text-main);
92
+ cursor: pointer;
93
+ padding: 0.4rem 0.7rem;
94
+ border-radius: 0.5rem;
95
+ font-weight: 700;
96
+ font-size: 0.7rem;
97
+ text-decoration: none;
98
+ letter-spacing: 0.08em;
99
+ transition: all 0.2s ease;
100
+ }
101
+
102
+ .btn-locale:hover {
103
+ border-color: var(--accent);
104
+ color: var(--accent);
105
+ }
106
+
107
+ .btn-theme {
108
+ background: transparent;
109
+ border: none;
110
+ cursor: pointer;
111
+ padding: 0.5rem;
112
+ border-radius: 0.6rem;
113
+ display: flex;
114
+ align-items: center;
115
+ justify-content: center;
116
+ transition: background 0.2s ease;
117
+ }
118
+
119
+ .btn-theme:hover {
120
+ background: rgba(255, 255, 255, 0.08);
121
+ }
122
+
123
+ .theme-icon {
124
+ display: block;
125
+ width: 16px;
126
+ height: 16px;
127
+ border-radius: 50%;
128
+ }
129
+
130
+ :global(.theme-light) .theme-icon {
131
+ background: #f59e0b;
132
+ box-shadow: 0 0 8px #fbbf24;
133
+ }
134
+
135
+ :global(.theme-dark) .theme-icon {
136
+ background: #94a3b8;
137
+ box-shadow: inset -4px -2px 0 #1e293b;
138
+ }
139
+ </style>
package/src/entries.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { LandingDefinition } from './types';
2
+ import { teamEntry } from './landing/team/entry';
3
+
4
+ export const ALL_LANDING_DEFINITIONS: LandingDefinition[] = [
5
+ {
6
+ entry: teamEntry,
7
+ LandingComponent: () => import('./landing/team/landing.astro'),
8
+ }
9
+ ];
package/src/env.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ declare module '*.astro' {
2
+ const Component: (_props: Record<string, unknown>) => unknown;
3
+ export default Component;
4
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ export { ALL_LANDING_DEFINITIONS } from './entries';
2
+
3
+ export type {
4
+ KnownLocale,
5
+ LocaleLoader,
6
+ LocaleMap,
7
+ LandingCardContent,
8
+ LandingPublicAssets,
9
+ LandingSeoMetadata,
10
+ LandingEntry,
11
+ LandingDefinition,
12
+ } from './types';
Binary file
@@ -0,0 +1,118 @@
1
+ ---
2
+ import MasterPlanScene from "./MasterPlanScene.astro";
3
+ import scene1 from "../assets/webcomic/scene-1.webp";
4
+ import scene2 from "../assets/webcomic/scene-2.webp";
5
+ import scene3 from "../assets/webcomic/scene-3.webp";
6
+ import scene4 from "../assets/webcomic/scene-4.webp";
7
+ import formulaMoney from "../assets/master-plan-money.webp";
8
+ import formulaFood from "../assets/master-plan-food.webp";
9
+ import formulaMoreMoney from "../assets/master-plan-more-money.webp";
10
+ import formulaMoreFood from "../assets/master-plan-more-food.webp";
11
+ import formulaTools from "../assets/master-plan-tools.webp";
12
+ import formulaProjects from "../assets/master-plan-projects.webp";
13
+
14
+ interface Props {
15
+ t: {
16
+ eyebrow: string;
17
+ title: string;
18
+ spainFood: string;
19
+ worldFood: string;
20
+ humanAssignment: string;
21
+ languagesLabel: string;
22
+ text: string;
23
+ quote: string;
24
+ closing: string;
25
+ scenes: {
26
+ label: string;
27
+ title: string;
28
+ text: string;
29
+ assetAlt: string;
30
+ formula: string[];
31
+ }[];
32
+ };
33
+ }
34
+
35
+ const { t } = Astro.props;
36
+
37
+ const scenes = [
38
+ {
39
+ number: "01",
40
+ label: t.scenes[0]!.label,
41
+ title: t.scenes[0]!.title,
42
+ text: t.scenes[0]!.text,
43
+ visual: "money",
44
+ asset: scene1,
45
+ assetAlt: t.scenes[0]!.assetAlt,
46
+ formula: [
47
+ { label: t.scenes[0]!.formula[0]!, asset: formulaMoney },
48
+ { label: t.scenes[0]!.formula[1]!, asset: formulaFood },
49
+ ],
50
+ },
51
+ {
52
+ number: "02",
53
+ label: t.scenes[1]!.label,
54
+ title: t.scenes[1]!.title,
55
+ text: t.scenes[1]!.text,
56
+ visual: "bowl",
57
+ asset: scene2,
58
+ assetAlt: t.scenes[1]!.assetAlt,
59
+ formula: [
60
+ { label: t.scenes[1]!.formula[0]!, asset: formulaMoreMoney },
61
+ { label: t.scenes[1]!.formula[1]!, asset: formulaMoreFood },
62
+ ],
63
+ },
64
+ {
65
+ number: "03",
66
+ label: t.scenes[2]!.label,
67
+ title: t.scenes[2]!.title,
68
+ text: t.scenes[2]!.text,
69
+ visual: "map",
70
+ asset: scene3,
71
+ assetAlt: t.scenes[2]!.assetAlt,
72
+ formula: [
73
+ { label: t.scenes[2]!.formula[0]!, asset: formulaTools },
74
+ { label: t.scenes[2]!.formula[1]!, asset: formulaMoney },
75
+ ],
76
+ },
77
+ {
78
+ number: "04",
79
+ label: t.scenes[3]!.label,
80
+ title: t.scenes[3]!.title,
81
+ text: t.scenes[3]!.text,
82
+ visual: "orders",
83
+ asset: scene4,
84
+ assetAlt: t.scenes[3]!.assetAlt,
85
+ formula: [
86
+ { label: t.scenes[3]!.formula[0]!, asset: formulaProjects },
87
+ { label: t.scenes[3]!.formula[1]!, asset: formulaMoreFood },
88
+ ],
89
+ },
90
+ ];
91
+
92
+
93
+ ---
94
+
95
+ <section class="team-section master-plan" aria-labelledby="master-plan-title">
96
+ <div class="team-landing-inner">
97
+ <div class="master-plan-header">
98
+ <p class="team-section-eyebrow">{t.eyebrow}</p>
99
+ <h2 class="team-section-title" id="master-plan-title">{t.title}</h2>
100
+ <p class="team-section-text">
101
+ {t.text}
102
+ </p>
103
+ </div>
104
+
105
+ <div class="master-plan-strip" aria-label={t.languagesLabel}>
106
+ {
107
+ scenes.map((scene) => (
108
+ <MasterPlanScene scene={scene} />
109
+ ))
110
+ }
111
+ </div>
112
+
113
+ <p class="master-plan-punchline">
114
+ <span>{t.quote}</span>
115
+ <strong>{t.closing}</strong>
116
+ </p>
117
+ </div>
118
+ </section>