create-vexcms 0.1.0-alpha.2 → 0.1.0-alpha.21

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 (115) hide show
  1. package/README.md +48 -14
  2. package/dist/index.js +1714 -1504
  3. package/package.json +2 -2
  4. package/templates/base-nextjs/README.md +18 -5
  5. package/templates/base-nextjs/convex/auth/index.ts +2 -2
  6. package/templates/base-nextjs/convex/auth/options.ts +27 -26
  7. package/templates/base-nextjs/convex/auth/schema.ts +51 -0
  8. package/templates/base-nextjs/convex/auth/sessions.ts +3 -3
  9. package/templates/base-nextjs/convex/tsconfig.json +2 -2
  10. package/templates/base-nextjs/convex/vex/firstUser.ts +2 -2
  11. package/templates/base-nextjs/convex/vex/globals.ts +3 -2
  12. package/templates/base-nextjs/convex/vex/media.ts +3 -2
  13. package/templates/base-nextjs/convex/vex.schema.ts +1 -0
  14. package/templates/base-nextjs/convex/vex.ts +17 -4
  15. package/templates/base-nextjs/next.config.ts +52 -6
  16. package/templates/base-nextjs/package.json +18 -7
  17. package/templates/base-nextjs/public/favicons/favicon.ico +0 -0
  18. package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +27 -15
  19. package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +2 -2
  20. package/templates/base-nextjs/src/app/(vexcms)/admin/clientProviders.tsx +9 -14
  21. package/templates/base-nextjs/src/app/(vexcms)/admin/layout.tsx +8 -6
  22. package/templates/base-nextjs/src/app/api/vex/revalidate/route.ts +19 -0
  23. package/templates/base-nextjs/src/app/globals.css +10 -9
  24. package/templates/base-nextjs/src/app/layout.tsx +6 -0
  25. package/templates/base-nextjs/src/auth/access.ts +14 -2
  26. package/templates/base-nextjs/src/auth/serverUtils.ts +20 -6
  27. package/templates/base-nextjs/src/components/WelcomePage.tsx +54 -43
  28. package/templates/base-nextjs/src/components/component-example.tsx +1 -1
  29. package/templates/base-nextjs/src/components/providers/server.tsx +16 -8
  30. package/templates/base-nextjs/src/vex.config.server.ts +30 -0
  31. package/templates/base-nextjs/src/vex.config.ts +14 -11
  32. package/templates/base-nextjs/src/vex.types.ts +2 -1
  33. package/templates/base-nextjs/src/vexcms/collections/images.ts +1 -1
  34. package/templates/base-nextjs/src/vexcms/collections/users.ts +3 -1
  35. package/templates/marketing-site/convex/pages.ts +20 -1
  36. package/templates/marketing-site/convex/schema.ts +4 -4
  37. package/templates/marketing-site/convex/seed.ts +515 -70
  38. package/templates/marketing-site/convex/siteSettings.ts +1 -1
  39. package/templates/marketing-site/convex/theme.ts +3 -3
  40. package/templates/marketing-site/convex/vex.schema.ts +75 -38
  41. package/templates/marketing-site/public/favicon.svg +14 -0
  42. package/templates/marketing-site/public/favicons/apple-touch-icon.png +0 -0
  43. package/templates/marketing-site/public/favicons/favicon.ico +0 -0
  44. package/templates/marketing-site/src/app/(frontend)/(site)/PageContent.tsx +64 -0
  45. package/templates/marketing-site/src/app/(frontend)/(site)/SiteLivePreviewProvider.tsx +31 -0
  46. package/templates/marketing-site/src/app/(frontend)/(site)/[slug]/page.tsx +63 -0
  47. package/templates/marketing-site/src/app/(frontend)/(site)/layout.tsx +50 -0
  48. package/templates/marketing-site/src/app/(frontend)/(site)/page.tsx +33 -0
  49. package/templates/marketing-site/src/app/(frontend)/layout.tsx +10 -24
  50. package/templates/marketing-site/src/app/(vexcms)/admin/layout.tsx +32 -0
  51. package/templates/marketing-site/src/app/globals.css +413 -0
  52. package/templates/marketing-site/src/app/layout.tsx +85 -0
  53. package/templates/marketing-site/src/app/robots.ts +13 -0
  54. package/templates/marketing-site/src/app/sitemap.ts +18 -0
  55. package/templates/marketing-site/src/components/BrandMark.tsx +27 -0
  56. package/templates/marketing-site/src/components/CodeHighlightContext.tsx +76 -0
  57. package/templates/marketing-site/src/components/CodePane.tsx +98 -0
  58. package/templates/marketing-site/src/components/Container.tsx +25 -0
  59. package/templates/marketing-site/src/components/CopyButton.tsx +80 -0
  60. package/templates/marketing-site/src/components/FirstAdminBootstrap.tsx +54 -0
  61. package/templates/marketing-site/src/components/InstallCommand.tsx +29 -0
  62. package/templates/marketing-site/src/components/MediaImage.tsx +59 -0
  63. package/templates/marketing-site/src/components/SectionHeader.tsx +51 -0
  64. package/templates/marketing-site/src/components/SiteFooter.tsx +8 -6
  65. package/templates/marketing-site/src/components/SiteHeader.tsx +8 -6
  66. package/templates/marketing-site/src/components/ThemeLive.tsx +1 -2
  67. package/templates/marketing-site/src/components/ThemeStyle.tsx +3 -3
  68. package/templates/marketing-site/src/components/motion-primitives/animated-group.tsx +44 -120
  69. package/templates/marketing-site/src/components/motion-primitives/reveal.module.css +32 -0
  70. package/templates/marketing-site/src/components/motion-primitives/text-effect.tsx +47 -266
  71. package/templates/marketing-site/src/lib/codeHighlightKey.ts +27 -0
  72. package/templates/marketing-site/src/lib/highlight.ts +134 -0
  73. package/templates/marketing-site/src/lib/highlightClient.ts +37 -0
  74. package/templates/marketing-site/src/lib/metadata.ts +61 -33
  75. package/templates/marketing-site/src/lib/resolvePagePath.ts +8 -0
  76. package/templates/marketing-site/src/lib/vex.ts +18 -0
  77. package/templates/marketing-site/src/proxy.ts +179 -0
  78. package/templates/marketing-site/src/vex.config.server.ts +27 -0
  79. package/templates/marketing-site/src/vex.config.ts +27 -9
  80. package/templates/marketing-site/src/vex.types.ts +102 -76
  81. package/templates/marketing-site/src/vexcms/api.ts +2 -2
  82. package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -2
  83. package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +40 -24
  84. package/templates/marketing-site/src/vexcms/blocks/CodeShowcase/config.ts +62 -0
  85. package/templates/marketing-site/src/vexcms/blocks/CodeShowcase/index.tsx +106 -0
  86. package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -3
  87. package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +63 -50
  88. package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +1 -0
  89. package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +43 -58
  90. package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +6 -3
  91. package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +90 -53
  92. package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +10 -3
  93. package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +205 -138
  94. package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +27 -12
  95. package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +174 -119
  96. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +2 -1
  97. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +59 -51
  98. package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +26 -5
  99. package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +256 -73
  100. package/templates/marketing-site/src/vexcms/blocks/Split/config.ts +62 -0
  101. package/templates/marketing-site/src/vexcms/blocks/Split/index.tsx +186 -0
  102. package/templates/marketing-site/src/vexcms/blocks/Stats/config.ts +35 -0
  103. package/templates/marketing-site/src/vexcms/blocks/Stats/index.tsx +77 -0
  104. package/templates/marketing-site/src/vexcms/blocks/config.ts +27 -11
  105. package/templates/marketing-site/src/vexcms/blocks/constants.ts +3 -0
  106. package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -0
  107. package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -0
  108. package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -0
  109. package/templates/marketing-site/src/vexcms/collections/pages.ts +23 -1
  110. package/templates/marketing-site/src/vexcms/collections/themeColors.ts +14 -14
  111. package/templates/marketing-site/src/vexcms/collections/users.ts +20 -0
  112. package/dist/index.mjs +0 -30976
  113. package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +0 -53
  114. package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +0 -25
  115. package/templates/marketing-site/src/app/(frontend)/page.tsx +0 -21
@@ -0,0 +1,413 @@
1
+ /* ============================================================================
2
+ VexCMS · globals.css — Stark × Ember (shadcn-faithful translation)
3
+ ----------------------------------------------------------------------------
4
+ Source of design intent: .agent/docs/design/claude-design/admin.css (Claude Design
5
+ export). That file uses bespoke tokens (--page, --fg-muted, --line-bold,
6
+ --accent / --accent-tint / --accent-ink, …); this file translates them
7
+ into shadcn-named tokens so every React component in @vexcms/react keeps
8
+ working with no per-component overrides.
9
+
10
+ Read .agent/docs/design/claude-design/README.md before editing.
11
+
12
+ Key semantic decisions (different from naive shadcn):
13
+ - Brand CTA → --primary (admin.css `--accent`) ember solid
14
+ - Highlight → --accent (admin.css `--accent-tint`) ember tint
15
+ - Ink/active → --accent-fg (admin.css `--accent-ink`) deep ember
16
+ - Default surfaces (--card, --popover) = SAME tone (admin.css `--surface`)
17
+ - --muted / --raised = third elevation level (admin.css `--raised`)
18
+ used for table headers, modal foots, hover bgs, badge.muted.
19
+
20
+ All values are oklch.
21
+ ============================================================================ */
22
+
23
+ @import "tailwindcss";
24
+ @import "tw-animate-css";
25
+ @import "shadcn/tailwind.css";
26
+ @import "@vexcms/next/styles";
27
+
28
+ /* Scan the installed @vexcms packages for class candidates (their dist output
29
+ ships alongside this app in node_modules; @vexcms/next/styles above pulls
30
+ the stylesheet, these make sure every utility used by the admin UI exists).
31
+ Globbed to `*.js` only — the same `dist` directories also carry `.map`
32
+ sourcemaps and `.d.ts` declaration files, and Tailwind's scanner extracts
33
+ garbage "class candidates" from their base64/binary-ish content and emits
34
+ unparseable CSS, exactly like the raw `public/` binary assets below. */
35
+ @source "../../node_modules/@vexcms/react/dist/**/*.js";
36
+ @source "../../node_modules/@vexcms/next/dist/**/*.js";
37
+ @source not "../../public";
38
+
39
+ @custom-variant dark (&:is(.dark *));
40
+
41
+ @theme inline {
42
+ /* ── Surfaces ─────────────────────────────────────────────────────────── */
43
+ --color-background: var(--background);
44
+ --color-foreground: var(--foreground);
45
+ --color-card: var(--card);
46
+ --color-card-foreground: var(--card-foreground);
47
+ --color-popover: var(--popover);
48
+ --color-popover-foreground: var(--popover-foreground);
49
+
50
+ /* ── Brand / accents ──────────────────────────────────────────────────── */
51
+ --color-primary: var(--primary); /* ember solid */
52
+ --color-primary-foreground: var(--primary-foreground);
53
+ --color-secondary: var(--secondary);
54
+ --color-secondary-foreground: var(--secondary-foreground);
55
+ --color-accent: var(--accent); /* ember tint */
56
+ --color-accent-foreground: var(--accent-foreground); /* ember ink */
57
+
58
+ /* ── Neutral hover & muted ────────────────────────────────────────────── */
59
+ --color-muted: var(--muted); /* = --raised */
60
+ --color-muted-foreground: var(--muted-foreground);
61
+
62
+ /* ── Borders & inputs ─────────────────────────────────────────────────── */
63
+ --color-border: var(--border); /* default separators */
64
+ --color-input: var(--input); /* input borders */
65
+ --color-ring: var(--ring); /* solid ember focus */
66
+
67
+ /* ── Status ───────────────────────────────────────────────────────────── */
68
+ --color-destructive: var(--destructive);
69
+ --color-destructive-foreground: var(--destructive-foreground);
70
+
71
+ /* ── Sidebar ──────────────────────────────────────────────────────────── */
72
+ --color-sidebar: var(--sidebar);
73
+ --color-sidebar-foreground: var(--sidebar-foreground);
74
+ --color-sidebar-primary: var(--sidebar-primary);
75
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
76
+ --color-sidebar-accent: var(--sidebar-accent);
77
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
78
+ --color-sidebar-border: var(--sidebar-border);
79
+ --color-sidebar-ring: var(--sidebar-ring);
80
+
81
+ /* ── Charts ───────────────────────────────────────────────────────────── */
82
+ --color-chart-1: var(--chart-1);
83
+ --color-chart-2: var(--chart-2);
84
+ --color-chart-3: var(--chart-3);
85
+ --color-chart-4: var(--chart-4);
86
+ --color-chart-5: var(--chart-5);
87
+
88
+ /* ── Fonts ────────────────────────────────────────────────────────────── */
89
+ --font-sans: var(--font-sans);
90
+ --font-mono: var(--font-geist-mono);
91
+
92
+ /* ── Radii — Vex is sharp ─────────────────────────────────────────────── */
93
+ --radius-sm: 2px; /* controls: buttons, inputs, menu items, type chips */
94
+ --radius-md: 4px; /* cards, popovers, table wraps */
95
+ --radius-lg: 8px; /* modals, dialogs */
96
+ --radius-xl: 12px;
97
+ --radius-2xl: 16px;
98
+ --radius-3xl: 20px;
99
+ --radius-4xl: 9999px; /* pill badges & avatars */
100
+
101
+ /* ── Shadows ──────────────────────────────────────────────────────────── */
102
+ --shadow-xs: var(--shadow-xs-val);
103
+ --shadow-sm: var(--shadow-sm-val);
104
+ --shadow-md: var(--shadow-md-val);
105
+ --shadow-lg: var(--shadow-lg-val);
106
+ --shadow-pop: var(--shadow-pop-val);
107
+
108
+ /* ── Motion ───────────────────────────────────────────────────────────── */
109
+ --ease-emphasized: cubic-bezier(.2, .8, .2, 1);
110
+ --duration-fast: 120ms;
111
+ --duration-base: 180ms;
112
+ --duration-slow: 260ms;
113
+
114
+ /* ── Section rhythm — every block's vertical padding derives from these ── */
115
+ --spacing-section: 3.5rem; /* 56px mobile */
116
+ --spacing-section-md: 5rem; /* 80px tablet */
117
+ --spacing-section-xl: 7rem; /* 112px desktop */
118
+
119
+ /* ── Code pane — FIXED IN BOTH THEMES ─────────────────────────────────────
120
+ The one deliberate exception to "the theme decides colour": code panes
121
+ stay dark under a light theme. That is what lets shiki run a single
122
+ server pass with one theme and ship no highlighter to the client.
123
+ These are @theme tokens, never part of the themes collection record. */
124
+ --color-code-bg: oklch(11% 0 0);
125
+ --color-code-chrome: oklch(15.5% 0 0);
126
+ --color-code-border: oklch(24% 0 0);
127
+ --color-code-fg: oklch(84% 0 0);
128
+ --color-code-dim: oklch(52% 0 0); /* comments, shell prompts */
129
+ --color-code-key: oklch(72% 0.16 42); /* keywords, primitives, booleans */
130
+ --color-code-ident: oklch(96% 0 0); /* functions, type names */
131
+ --color-code-string: oklch(70% 0 0); /* strings, slugs */
132
+ --color-code-punct: oklch(45% 0 0);
133
+ }
134
+
135
+ /* ────────────────────────────────────────────────────────────────────────────
136
+ LIGHT — Stark × Ember
137
+ page #F5F5F5 · surface #FFFFFF · raised #FAFAFA · fg #0A0A0A
138
+ ──────────────────────────────────────────────────────────────────────────── */
139
+ :root {
140
+ /* Surfaces — three elevation levels */
141
+ --background: oklch(96.1% 0 0); /* #F5F5F5 */
142
+ --foreground: oklch(13.7% 0 0); /* #0A0A0A */
143
+ --card: oklch(100% 0 0); /* #FFFFFF */
144
+ --card-foreground: oklch(13.7% 0 0);
145
+ --popover: oklch(100% 0 0); /* same as card */
146
+ --popover-foreground: oklch(13.7% 0 0);
147
+
148
+ /* Brand — ember on --primary */
149
+ --primary: oklch(60.5% 0.175 42); /* #E8622A */
150
+ --primary-foreground: oklch(100% 0 0);
151
+
152
+ /* Neutral secondary */
153
+ --secondary: oklch(98% 0 0); /* #FAFAFA */
154
+ --secondary-foreground: oklch(13.7% 0 0);
155
+
156
+ /* Highlight = ember TINT (matches admin.css --accent-tint) */
157
+ --accent: oklch(96% 0.025 42); /* #FEF0E8 */
158
+ --accent-foreground: oklch(52% 0.180 40); /* ember ink */
159
+
160
+ /* Neutral hover & muted (= admin.css --hover / --raised) */
161
+ --muted: oklch(98% 0 0); /* #FAFAFA = raised */
162
+ --muted-foreground: oklch(50.5% 0 0); /* #717171 */
163
+
164
+ /* Borders */
165
+ --border: oklch(85% 0 0); /* #D4D4D4 — default */
166
+ --input: oklch(54.6% 0 0); /* #6B6B6B — inputs */
167
+ --ring: oklch(60.5% 0.175 42); /* solid ember */
168
+
169
+ /* Status */
170
+ --destructive: oklch(57.7% 0.198 27); /* #DC2626 */
171
+ --destructive-foreground: oklch(98% 0 0);
172
+
173
+ /* Base radius — used by anything reading var(--radius) directly */
174
+ --radius: 4px;
175
+
176
+ /* Sidebar */
177
+ --sidebar: oklch(98% 0 0); /* #FAFAFA */
178
+ --sidebar-foreground: oklch(13.7% 0 0);
179
+ --sidebar-primary: oklch(60.5% 0.175 42); /* ember */
180
+ --sidebar-primary-foreground: oklch(100% 0 0);
181
+ --sidebar-accent: oklch(96.1% 0 0);
182
+ --sidebar-accent-foreground: oklch(13.7% 0 0);
183
+ --sidebar-border: oklch(85% 0 0);
184
+ --sidebar-ring: oklch(60.5% 0.175 42);
185
+
186
+ /* Charts */
187
+ --chart-1: oklch(60.5% 0.175 42);
188
+ --chart-2: oklch(45% 0 0);
189
+ --chart-3: oklch(72% 0.100 60);
190
+ --chart-4: oklch(60% 0.040 30);
191
+ --chart-5: oklch(78% 0 0);
192
+
193
+ /* Shadows — soft elevation in light */
194
+ --shadow-xs-val: 0 1px 2px rgba(10, 10, 10, 0.04);
195
+ --shadow-sm-val: 0 1px 3px rgba(10, 10, 10, 0.08);
196
+ --shadow-md-val: 0 4px 12px rgba(10, 10, 10, 0.08);
197
+ --shadow-lg-val: 0 12px 32px rgba(10, 10, 10, 0.12);
198
+ --shadow-pop-val: 0 12px 28px rgba(10, 10, 10, 0.14),
199
+ 0 1px 3px rgba(10, 10, 10, 0.06);
200
+ }
201
+
202
+ /* ────────────────────────────────────────────────────────────────────────────
203
+ DARK — Stark × Ember dark
204
+ page #0A0A0A · surface #141414 · raised #1E1E1E · fg #F4F4F4
205
+ Hotter ember (#F07040) for contrast. No flat shadows.
206
+ ──────────────────────────────────────────────────────────────────────────── */
207
+ .dark {
208
+ /* Surfaces */
209
+ --background: oklch(13.7% 0 0); /* #0A0A0A */
210
+ --foreground: oklch(95% 0 0); /* #F4F4F4 */
211
+ --card: oklch(17.4% 0 0); /* #141414 */
212
+ --card-foreground: oklch(95% 0 0);
213
+ --popover: oklch(17.4% 0 0); /* same as card */
214
+ --popover-foreground: oklch(95% 0 0);
215
+
216
+ /* Brand */
217
+ --primary: oklch(72% 0.175 50); /* #F07040 */
218
+ --primary-foreground: oklch(13.7% 0 0);
219
+
220
+ /* Secondary */
221
+ --secondary: oklch(20% 0 0); /* #1E1E1E */
222
+ --secondary-foreground: oklch(95% 0 0);
223
+
224
+ /* Highlight — translucent ember tint */
225
+ --accent: oklch(72% 0.175 50 / 0.12);
226
+ --accent-foreground: oklch(72% 0.175 50); /* ember itself in dark */
227
+
228
+ /* Neutral hover & muted */
229
+ --muted: oklch(20% 0 0); /* = raised in dark */
230
+ --muted-foreground: oklch(70% 0 0); /* #A0A0A0 */
231
+
232
+ /* Borders */
233
+ --border: oklch(25% 0 0); /* #2A2A2A */
234
+ --input: oklch(40% 0 0); /* #5A5A5A */
235
+ --ring: oklch(72% 0.175 50);
236
+
237
+ /* Status */
238
+ --destructive: oklch(63% 0.210 27); /* #EF4444 */
239
+ --destructive-foreground: oklch(95% 0 0);
240
+
241
+ /* Sidebar */
242
+ --sidebar: oklch(7% 0 0); /* #050505 */
243
+ --sidebar-foreground: oklch(95% 0 0);
244
+ --sidebar-primary: oklch(72% 0.175 50);
245
+ --sidebar-primary-foreground: oklch(13.7% 0 0);
246
+ --sidebar-accent: oklch(20% 0 0);
247
+ --sidebar-accent-foreground: oklch(95% 0 0);
248
+ --sidebar-border: oklch(25% 0 0);
249
+ --sidebar-ring: oklch(72% 0.175 50);
250
+
251
+ /* Charts */
252
+ --chart-1: oklch(72% 0.175 50);
253
+ --chart-2: oklch(78% 0 0);
254
+ --chart-3: oklch(78% 0.120 65);
255
+ --chart-4: oklch(60% 0.060 30);
256
+ --chart-5: oklch(45% 0 0);
257
+
258
+ /* Shadows — flat in dark; only overlays use --shadow-pop */
259
+ --shadow-xs-val: none;
260
+ --shadow-sm-val: none;
261
+ --shadow-md-val: none;
262
+ --shadow-lg-val: none;
263
+ --shadow-pop-val: 0 18px 40px rgba(0, 0, 0, 0.55),
264
+ 0 1px 0 var(--border) inset;
265
+ }
266
+
267
+ @layer base {
268
+ * {
269
+ @apply border-border outline-ring/50;
270
+ }
271
+ body {
272
+ @apply bg-background text-foreground;
273
+ font-feature-settings:
274
+ "ss01" 1,
275
+ "cv11" 1;
276
+ -webkit-font-smoothing: antialiased;
277
+ }
278
+
279
+ /* Focus-visible is global and is never removed or restyled per block. It
280
+ must stay legible against bg-background AND the fixed dark code pane. */
281
+ :focus-visible {
282
+ outline: 2px solid var(--color-ring);
283
+ outline-offset: 2px;
284
+ }
285
+
286
+ ::selection {
287
+ background-color: color-mix(in oklch, var(--color-primary) 22%, transparent);
288
+ }
289
+
290
+ /* Links inside `text` field output. There is no richtext field, so this is
291
+ the only styling prose from the CMS ever receives. */
292
+ .prose-field a {
293
+ color: var(--color-primary);
294
+ text-decoration-line: underline;
295
+ text-underline-offset: 4px;
296
+ text-decoration-color: color-mix(in oklch, currentColor 40%, transparent);
297
+ }
298
+ .prose-field a:hover {
299
+ text-decoration-color: currentColor;
300
+ }
301
+
302
+ code,
303
+ pre {
304
+ font-feature-settings:
305
+ "ss01" 1,
306
+ "calt" 1;
307
+ tab-size: 2;
308
+ }
309
+
310
+ /* Smooth scrolling is scoped to explicit in-page anchor navigation, never
311
+ applied to `html` globally.
312
+
313
+ A global `scroll-behavior: smooth` animates *every* programmatic scroll,
314
+ including the position restoration that popovers, dialogs and scroll
315
+ locks perform. A 20px correction then reads as the page gliding away
316
+ under the cursor, and a restore-to-saved-offset reads as a scroll to the
317
+ top. `:target` limits it to the one case the design actually asked for. */
318
+ html:has(:target) {
319
+ scroll-behavior: smooth;
320
+ }
321
+ }
322
+
323
+ /* ────────────────────────────────────────────────────────────────────────────
324
+ MOTION KIT
325
+ The `reveal` keyframe and `.item` live in
326
+ src/components/motion-primitives/reveal.module.css — do not duplicate them.
327
+ Only the two rules that must be global are declared here.
328
+ ──────────────────────────────────────────────────────────────────────────── */
329
+
330
+ /* Stagger contract — the group sets the step, each item reads its index. */
331
+ .reveal-group > * {
332
+ --reveal-delay: calc(var(--reveal-stagger, 60ms) * var(--i, 0));
333
+ }
334
+
335
+ /* The one decorative loop on the entire site: the Roadmap "In progress" dot. */
336
+ @keyframes livepulse {
337
+ 0%,
338
+ 100% {
339
+ opacity: 1;
340
+ }
341
+ 50% {
342
+ opacity: 0.35;
343
+ }
344
+ }
345
+
346
+ .live-dot {
347
+ animation: livepulse 2s var(--ease-emphasized) infinite;
348
+ }
349
+
350
+ /* Reduced motion is a full stop, not a shortened duration: the final state is
351
+ painted immediately — opacity 1, no blur, no translate. */
352
+ @media (prefers-reduced-motion: reduce) {
353
+ .item,
354
+ .live-dot,
355
+ .reveal-group > * {
356
+ animation: none !important;
357
+ }
358
+
359
+ html {
360
+ scroll-behavior: auto;
361
+ }
362
+ }
363
+
364
+ /* Pangea dnd placeholder — thin dashed line instead of full-height gap */
365
+ [data-rfd-placeholder] {
366
+ height: 3px !important;
367
+ min-height: 3px !important;
368
+ margin: 0 !important;
369
+ padding: 0 !important;
370
+ background: transparent !important;
371
+ border-radius: 0 !important;
372
+ border-top: 2px dashed var(--border) !important;
373
+ }
374
+
375
+ /* ── Scrollbar ────────────────────────────────────────────────────────────
376
+ Matches the admin panel's `.vex-scroll-area` treatment (@vexcms/react's
377
+ styles.css): transparent track so the scrollbar reads as part of the
378
+ surface, and a permanently visible semi-transparent thumb rather than an
379
+ overlay bar that hides how much content is left. Applied to the document
380
+ scroller here, since the marketing site scrolls the window.
381
+ ────────────────────────────────────────────────────────────────────────── */
382
+ html {
383
+ scrollbar-width: thin;
384
+ scrollbar-color: color-mix(in oklch, var(--foreground) 25%, transparent) transparent;
385
+ }
386
+
387
+ html::-webkit-scrollbar,
388
+ body ::-webkit-scrollbar {
389
+ width: 10px;
390
+ height: 10px;
391
+ }
392
+
393
+ html::-webkit-scrollbar-track,
394
+ html::-webkit-scrollbar-corner,
395
+ body ::-webkit-scrollbar-track,
396
+ body ::-webkit-scrollbar-corner {
397
+ background: transparent;
398
+ }
399
+
400
+ html::-webkit-scrollbar-thumb,
401
+ body ::-webkit-scrollbar-thumb {
402
+ /* Transparent border + `background-clip: content-box` is the only way to
403
+ inset a webkit thumb without painting the track. */
404
+ background-color: color-mix(in oklch, var(--foreground) 25%, transparent);
405
+ background-clip: content-box;
406
+ border: 2px solid transparent;
407
+ border-radius: 9999px;
408
+ }
409
+
410
+ html::-webkit-scrollbar-thumb:hover,
411
+ body ::-webkit-scrollbar-thumb:hover {
412
+ background-color: color-mix(in oklch, var(--foreground) 45%, transparent);
413
+ }
@@ -0,0 +1,85 @@
1
+ import type { Metadata } from "next"
2
+
3
+ import "./globals.css"
4
+
5
+ import { ThemeScript } from "@vexcms/react"
6
+ import { Geist, Geist_Mono } from "next/font/google"
7
+
8
+ import ClientProviders from "~/components/providers/client"
9
+ import ServerProviders from "~/components/providers/server"
10
+ import { ThemeLive } from "~/components/ThemeLive"
11
+
12
+ /**
13
+ * Geist carries the whole type system — display through body — so weight is
14
+ * the only axis the design varies. 800 is the hero display weight; without it
15
+ * next/font would synthesise it and the -0.04em tracking would fall apart.
16
+ *
17
+ * `--font-sans` is also written by the active theme record's `fontFamily`
18
+ * (THEME_SHARED_TOKENS). Both land at specificity (0,1,0), so the theme's
19
+ * stack acts as the fallback and an editor can change the family without a
20
+ * redeploy.
21
+ */
22
+ const geistSans = Geist({
23
+ display: "swap",
24
+ subsets: ["latin"],
25
+ variable: "--font-sans",
26
+ weight: ["400", "500", "600", "700", "800"],
27
+ })
28
+
29
+ const geistMono = Geist_Mono({
30
+ display: "swap",
31
+ subsets: ["latin"],
32
+ variable: "--font-geist-mono",
33
+ weight: ["400", "500"],
34
+ })
35
+
36
+ /**
37
+ * `icons.icon` lists the SVG first: browsers pick the last format they
38
+ * understand, and every current engine understands `image/svg+xml`, so the
39
+ * `.ico` only serves older ones. All three assets are the ember chevron from
40
+ * `BrandMark` — placeholder branding, exactly like `BrandMark` itself and the
41
+ * "VexCMS" site name in `convex/seed.ts`. Replace `public/favicon.svg`,
42
+ * `public/favicons/favicon.ico` and `public/favicons/apple-touch-icon.png`
43
+ * with your own mark; `.ico` and `apple-touch-icon` must stay raster, since
44
+ * older browsers and iOS home-screen icons do not accept SVG.
45
+ */
46
+ export const metadata: Metadata = {
47
+ description: "Built with VexCMS",
48
+ icons: {
49
+ apple: "/favicons/apple-touch-icon.png",
50
+ icon: [
51
+ { url: "/favicon.svg", type: "image/svg+xml" },
52
+ { url: "/favicons/favicon.ico", sizes: "16x16 32x32 48x48" },
53
+ ],
54
+ },
55
+ title: "VexCMS",
56
+ }
57
+
58
+ export default function RootLayout({
59
+ children,
60
+ }: Readonly<{
61
+ children: React.ReactNode
62
+ }>) {
63
+ return (
64
+ <html
65
+ className={`${geistSans.variable} ${geistMono.variable} antialiased`}
66
+ lang="en"
67
+ suppressHydrationWarning
68
+ >
69
+ <head>
70
+ {/* Applies the persisted light/dark class before first paint (no
71
+ flash). Site theming now renders in `(frontend)/(site)/layout.tsx`
72
+ — the admin layout re-emits its own scope for `/admin`. */}
73
+ <ThemeScript />
74
+ </head>
75
+ <body>
76
+ <ServerProviders>
77
+ <ClientProviders>
78
+ <ThemeLive />
79
+ {children}
80
+ </ClientProviders>
81
+ </ServerProviders>
82
+ </body>
83
+ </html>
84
+ )
85
+ }
@@ -0,0 +1,13 @@
1
+ import { createVexRobots } from "@vexcms/next/seo"
2
+
3
+ import { env } from "~/env.mjs"
4
+
5
+ /**
6
+ * Generates `/robots.txt`. Allows all crawlers on the public site and points
7
+ * them at the generated sitemap; disallows the authenticated `/admin` tree
8
+ * and its `/api` routes.
9
+ */
10
+ export default createVexRobots({
11
+ siteUrl: env.NEXT_PUBLIC_SITE_URL,
12
+ disallow: ["/admin", "/api"],
13
+ })
@@ -0,0 +1,18 @@
1
+ import { api } from "@convex/_generated/api"
2
+ import { createVexSitemap } from "@vexcms/next/seo"
3
+
4
+ import { env } from "~/env.mjs"
5
+ import { vex } from "~/lib/vex"
6
+
7
+ /**
8
+ * Generates `/sitemap.xml` from every published `pages` document plus the
9
+ * site root. Degrades to `[]` (no page entries) when Convex is unreachable —
10
+ * the packed-tarball scaffold this template ships into has no live
11
+ * deployment and builds against placeholder env (P-020).
12
+ */
13
+ export default createVexSitemap({
14
+ client: vex,
15
+ query: api.pages.publishedSlugs,
16
+ toUrl: (slug) => (slug === "home" ? env.NEXT_PUBLIC_SITE_URL : `${env.NEXT_PUBLIC_SITE_URL}/${slug}`),
17
+ getSlug: (item) => item.slug,
18
+ })
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The VexCMS chevron, inline in the layout rather than in the media library.
3
+ *
4
+ * A freshly scaffolded project has an empty media library, so the mark cannot
5
+ * be an upload — the header and footer would render a wordmark with a hole
6
+ * next to it on first run. `logoImage` overrides this when an editor sets one.
7
+ */
8
+ export function BrandMark({ className }: { className?: string }) {
9
+ return (
10
+ <svg
11
+ aria-hidden
12
+ className={className}
13
+ fill="none"
14
+ height="16"
15
+ viewBox="0 0 16 16"
16
+ width="16"
17
+ xmlns="http://www.w3.org/2000/svg"
18
+ >
19
+ <path
20
+ d="M2.5 3.25 7.25 12.75a0.85 0.85 0 0 0 1.5 0L13.5 3.25"
21
+ stroke="currentColor"
22
+ strokeLinecap="square"
23
+ strokeWidth="2.25"
24
+ />
25
+ </svg>
26
+ )
27
+ }
@@ -0,0 +1,76 @@
1
+ "use client"
2
+
3
+ import type { ReactNode } from "react"
4
+
5
+ import { createContext, useContext, useEffect, useState } from "react"
6
+
7
+ import type { CodeHighlightMap, CodeLanguage } from "~/lib/highlight"
8
+
9
+ import { codeHighlightKey } from "~/lib/codeHighlightKey"
10
+
11
+ const CodeHighlightContext = createContext<CodeHighlightMap>({})
12
+
13
+ /**
14
+ * Supplies the server-built highlight map to the client tree.
15
+ *
16
+ * @param props.highlights - Map produced by `highlightPageBlocks` on the server.
17
+ * @param props.children - The page subtree containing code panes.
18
+ * @returns The provider.
19
+ */
20
+ export function CodeHighlightProvider(props: {
21
+ children: ReactNode
22
+ highlights: CodeHighlightMap
23
+ }) {
24
+ return (
25
+ <CodeHighlightContext.Provider value={props.highlights}>
26
+ {props.children}
27
+ </CodeHighlightContext.Provider>
28
+ )
29
+ }
30
+
31
+ /**
32
+ * Resolves one pane's highlighted HTML.
33
+ *
34
+ * Server-rendered code hits the precomputed map and never touches a
35
+ * highlighter. Code that only exists in the browser — what an editor is
36
+ * typing right now in live preview — misses, so this lazily imports shiki and
37
+ * highlights client-side. The import is dynamic and inside the miss branch, so
38
+ * an ordinary visitor downloads no highlighter at all.
39
+ *
40
+ * @param props.code - Source text.
41
+ * @param props.language - Resolved grammar name.
42
+ * @returns The highlighted inner HTML, or `undefined` until it is available
43
+ * (render the raw source meanwhile).
44
+ */
45
+ export function useCodeHighlight(props: {
46
+ code: string
47
+ language: CodeLanguage
48
+ }): string | undefined {
49
+ const highlights = useContext(CodeHighlightContext)
50
+ const key = codeHighlightKey(props)
51
+ const precomputed = highlights[key]
52
+ const [clientHighlighted, setClientHighlighted] = useState<Record<string, string>>({})
53
+
54
+ useEffect(() => {
55
+ if (precomputed !== undefined || clientHighlighted[key] !== undefined) return
56
+
57
+ let cancelled = false
58
+ void (async () => {
59
+ // Dynamic on purpose, and the one case the static-import rule exempts:
60
+ // a static import would pull shiki's grammars and wasm into the page's
61
+ // main client chunk for every visitor, when only an editor previewing
62
+ // freshly-typed code ever reaches this branch.
63
+ const { highlightCodeInBrowser } = await import("~/lib/highlightClient")
64
+ const html = await highlightCodeInBrowser(props)
65
+ if (cancelled) return
66
+ setClientHighlighted((previous) => ({ ...previous, [key]: html }))
67
+ })()
68
+
69
+ return () => {
70
+ cancelled = true
71
+ }
72
+ // `props.code`/`props.language` are what `key` is derived from.
73
+ }, [key, precomputed, clientHighlighted, props])
74
+
75
+ return precomputed ?? clientHighlighted[key]
76
+ }