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
@@ -4,6 +4,8 @@
4
4
  /* eslint-disable perfectionist/sort-union-types */
5
5
  /* eslint-disable perfectionist/sort-interfaces */
6
6
  /* eslint-disable perfectionist/sort-modules */
7
+ /* eslint-disable perfectionist/sort-object-types */
8
+ /* eslint-disable @typescript-eslint/no-empty-object-type */
7
9
 
8
10
  import type { Id } from "@convex/_generated/dataModel"
9
11
  import type { VexDocument, VexDocumentGlobal } from "@vexcms/core"
@@ -28,16 +30,35 @@ export type HeroBlock = {
28
30
  blockType: "hero"
29
31
  blockName?: string
30
32
  id: string
33
+ variant?: ("full" | "compact")[]
31
34
  badgeText?: string
32
35
  badgeLink?: string
33
36
  heading: string
34
37
  subheading?: string
35
- primaryCtaLabel: string
36
- primaryCtaHref: string
38
+ installCommand?: string
39
+ primaryCtaLabel?: string
40
+ primaryCtaHref?: string
37
41
  secondaryCtaLabel?: string
38
42
  secondaryCtaHref?: string
39
43
  }
40
44
 
45
+ export type StatsBlock = {
46
+ blockType: "stats"
47
+ blockName?: string
48
+ id: string
49
+ heading?: string
50
+ subheading?: string
51
+ items: {
52
+ /**
53
+ * Free text, not a number — "0", "12" and "~30s" all render. Any unit belongs in
54
+ * the string.
55
+ */
56
+ value: string
57
+ label: string
58
+ description?: string
59
+ }[]
60
+ }
61
+
41
62
  export type FeaturesBlock = {
42
63
  blockType: "features"
43
64
  blockName?: string
@@ -54,6 +75,51 @@ export type FeaturesBlock = {
54
75
  }[]
55
76
  }
56
77
 
78
+ export type CodeShowcaseBlock = {
79
+ blockType: "code_showcase"
80
+ blockName?: string
81
+ id: string
82
+ heading: string
83
+ subheading?: string
84
+ panes: {
85
+ /**
86
+ * Shown uppercase in the chrome bar.
87
+ */
88
+ label: string
89
+ filename?: string
90
+ language?: ("ts" | "tsx" | "bash" | "json")[]
91
+ /**
92
+ * Hand-written panes carry the primary-coloured label — the only signal separating
93
+ * human input from generated output.
94
+ */
95
+ authored?: ("authored" | "generated")[]
96
+ code: string
97
+ caption?: string
98
+ }[]
99
+ }
100
+
101
+ export type SplitBlock = {
102
+ blockType: "split"
103
+ blockName?: string
104
+ id: string
105
+ eyebrow?: string
106
+ heading: string
107
+ body: string
108
+ bullets?: {
109
+ /**
110
+ * Lucide icon name
111
+ */
112
+ icon?: string
113
+ text: string
114
+ }[]
115
+ media?: ("code" | "image" | "none")[]
116
+ code?: string
117
+ codeFilename?: string
118
+ codeLanguage?: ("ts" | "tsx" | "bash" | "json")[]
119
+ image?: Id<"images">[]
120
+ mediaPosition?: ("right" | "left")[]
121
+ }
122
+
57
123
  export type HowItWorksBlock = {
58
124
  blockType: "how_it_works"
59
125
  blockName?: string
@@ -79,37 +145,45 @@ export type RoadmapBlock = {
79
145
  items: {
80
146
  feature: string
81
147
  description?: string
82
- status: string[]
148
+ status: ("shipped" | "in-progress" | "planned" | "exploring" | "future")[]
83
149
  }[]
84
150
  }
85
151
 
86
- export type CtaBlock = {
87
- blockType: "cta"
152
+ export type FaqBlock = {
153
+ blockType: "faq"
88
154
  blockName?: string
89
155
  id: string
90
156
  heading: string
91
157
  subheading?: string
92
- actions?: {
93
- label: string
94
- href: string
158
+ supportLink?: string
159
+ items: {
160
+ question: string
161
+ answer: string
95
162
  }[]
96
163
  }
97
164
 
98
- export type FaqBlock = {
99
- blockType: "faq"
165
+ export type CtaBlock = {
166
+ blockType: "cta"
100
167
  blockName?: string
101
168
  id: string
102
169
  heading: string
103
170
  subheading?: string
104
- supportLink?: string
105
- items: {
106
- question: string
107
- answer: string
171
+ actions?: {
172
+ label: string
173
+ href: string
108
174
  }[]
109
175
  }
110
176
 
111
177
  export type PageBlock =
112
- HeroBlock | FeaturesBlock | HowItWorksBlock | RoadmapBlock | CtaBlock | FaqBlock
178
+ | HeroBlock
179
+ | StatsBlock
180
+ | FeaturesBlock
181
+ | CodeShowcaseBlock
182
+ | SplitBlock
183
+ | HowItWorksBlock
184
+ | RoadmapBlock
185
+ | FaqBlock
186
+ | CtaBlock
113
187
 
114
188
  export interface PagesDocument extends VexDocument {
115
189
  _id: Id<"pages">
@@ -128,6 +202,7 @@ export interface PagesDocument extends VexDocument {
128
202
  */
129
203
  metaDescription?: string
130
204
  ogImage?: Id<"images">[]
205
+ updatedAt?: number
131
206
  }
132
207
  export type HeaderBlock = {
133
208
  blockType: "header"
@@ -143,7 +218,7 @@ export type HeaderBlock = {
143
218
  actionButtons?: {
144
219
  label: string
145
220
  href: string
146
- variant?: string[]
221
+ variant?: ("default" | "outline" | "ghost")[]
147
222
  }[]
148
223
  }
149
224
 
@@ -151,6 +226,7 @@ export interface HeadersDocument extends VexDocument {
151
226
  _id: Id<"headers">
152
227
  name: string
153
228
  content?: HeaderBlock[]
229
+ updatedAt?: number
154
230
  }
155
231
  export type FooterBlock = {
156
232
  blockType: "footer"
@@ -177,6 +253,7 @@ export interface FootersDocument extends VexDocument {
177
253
  _id: Id<"footers">
178
254
  name: string
179
255
  content?: FooterBlock[]
256
+ updatedAt?: number
180
257
  }
181
258
 
182
259
  export interface Theme extends VexDocument {
@@ -269,6 +346,7 @@ export interface Theme extends VexDocument {
269
346
  sidebarBorder?: string
270
347
  sidebarRing?: string
271
348
  }
349
+ updatedAt?: number
272
350
  }
273
351
 
274
352
  export interface SessionDocument extends VexDocument {
@@ -281,7 +359,6 @@ export interface SessionDocument extends VexDocument {
281
359
  userAgent?: string
282
360
  userId: string
283
361
  impersonatedBy?: string
284
- activeOrganizationId?: string
285
362
  }
286
363
 
287
364
  export interface AccountDocument extends VexDocument {
@@ -342,34 +419,6 @@ export interface JwksDocument extends VexDocument {
342
419
  expiresAt?: number
343
420
  }
344
421
 
345
- export interface OrganizationDocument extends VexDocument {
346
- _id: Id<"organization">
347
- name: string
348
- slug: string
349
- logo?: string
350
- createdAt: number
351
- metadata?: string
352
- }
353
-
354
- export interface MemberDocument extends VexDocument {
355
- _id: Id<"member">
356
- organizationId: string
357
- userId: string
358
- role: string
359
- createdAt: number
360
- }
361
-
362
- export interface InvitationDocument extends VexDocument {
363
- _id: Id<"invitation">
364
- organizationId: string
365
- email: string
366
- role?: string
367
- status: string
368
- expiresAt: number
369
- createdAt: number
370
- inviterId: string
371
- }
372
-
373
422
  export interface Image extends VexDocument {
374
423
  _id: Id<"images">
375
424
  filename: string
@@ -381,6 +430,7 @@ export interface Image extends VexDocument {
381
430
  src: string
382
431
  width?: number
383
432
  height?: number
433
+ updatedAt?: number
384
434
  }
385
435
 
386
436
  export interface SiteSettingsGlobal extends VexDocumentGlobal<"siteSettings"> {
@@ -434,9 +484,6 @@ export type CollectionSlug =
434
484
  | "verification"
435
485
  | "apikey"
436
486
  | "jwks"
437
- | "organization"
438
- | "member"
439
- | "invitation"
440
487
  | "images"
441
488
 
442
489
  export type GlobalSlug = "siteSettings"
@@ -456,9 +503,6 @@ export type DocumentBySlug = {
456
503
  verification: VerificationDocument
457
504
  apikey: ApikeyDocument
458
505
  jwks: JwksDocument
459
- organization: OrganizationDocument
460
- member: MemberDocument
461
- invitation: InvitationDocument
462
506
  images: Image
463
507
  }
464
508
 
@@ -479,9 +523,6 @@ declare module "@vexcms/core" {
479
523
  | "verification"
480
524
  | "apikey"
481
525
  | "jwks"
482
- | "organization"
483
- | "member"
484
- | "invitation"
485
526
  | "images"
486
527
  GlobalSlug: "siteSettings"
487
528
  MediaCollectionSlug: "images"
@@ -497,9 +538,6 @@ declare module "@vexcms/core" {
497
538
  verification: VerificationDocument
498
539
  apikey: ApikeyDocument
499
540
  jwks: JwksDocument
500
- organization: OrganizationDocument
501
- member: MemberDocument
502
- invitation: InvitationDocument
503
541
  images: Image
504
542
  }
505
543
  GlobalDocumentBySlug: {
@@ -516,23 +554,26 @@ declare module "@vexcms/core" {
516
554
  text: "title" | "slug" | "metaTitle" | "metaDescription"
517
555
  blocks: "blocks"
518
556
  upload: "ogImage"
557
+ number: "updatedAt"
519
558
  }
520
559
  headers: {
521
560
  text: "name"
522
561
  blocks: "content"
562
+ number: "updatedAt"
523
563
  }
524
564
  footers: {
525
565
  text: "name"
526
566
  blocks: "content"
567
+ number: "updatedAt"
527
568
  }
528
569
  themes: {
529
570
  text: "name" | "fontFamily" | "radius"
530
571
  group: "light" | "dark"
572
+ number: "updatedAt"
531
573
  }
532
574
  session: {
533
575
  date: "expiresAt" | "createdAt" | "updatedAt"
534
- text:
535
- "token" | "ipAddress" | "userAgent" | "userId" | "impersonatedBy" | "activeOrganizationId"
576
+ text: "token" | "ipAddress" | "userAgent" | "userId" | "impersonatedBy"
536
577
  }
537
578
  account: {
538
579
  text:
@@ -574,21 +615,9 @@ declare module "@vexcms/core" {
574
615
  text: "publicKey" | "privateKey"
575
616
  date: "createdAt" | "expiresAt"
576
617
  }
577
- organization: {
578
- text: "name" | "slug" | "logo" | "metadata"
579
- date: "createdAt"
580
- }
581
- member: {
582
- text: "organizationId" | "userId" | "role"
583
- date: "createdAt"
584
- }
585
- invitation: {
586
- text: "organizationId" | "email" | "role" | "status" | "inviterId"
587
- date: "expiresAt" | "createdAt"
588
- }
589
618
  images: {
590
619
  text: "filename" | "alt" | "mimeType" | "storageId" | "src"
591
- number: "size" | "width" | "height"
620
+ number: "size" | "width" | "height" | "updatedAt"
592
621
  checkbox: "deleted"
593
622
  }
594
623
  }
@@ -620,15 +649,12 @@ declare module "@vexcms/core" {
620
649
  by_key: readonly ["key"]
621
650
  }
622
651
  jwks: {}
623
- organization: { by_slug: readonly ["slug"] }
624
- member: { by_organizationId: readonly ["organizationId"]; by_userId: readonly ["userId"] }
625
- invitation: { by_organizationId: readonly ["organizationId"]; by_email: readonly ["email"] }
626
652
  images: { by_deleted: readonly ["deleted"] }
627
653
  }
628
654
  CustomActionsBySlug: {}
629
655
  AuthSlugs: {
630
656
  user: "user"
631
- organization: "organization"
657
+ organization: never
632
658
  }
633
659
  }
634
660
  }
@@ -3,7 +3,7 @@ import { createGetAuth } from "@vexcms/better-auth"
3
3
  import { vexServerApi } from "@vexcms/core/server"
4
4
 
5
5
  import { TABLE_SLUG_ORGANIZATIONS, TABLE_SLUG_SESSIONS, TABLE_SLUG_USERS } from "~/db/constants"
6
- import config from "~/vex.config"
6
+ import config from "~/vex.config.server"
7
7
 
8
8
  /**
9
9
  * Bound server API — `config`/`getAuth` are threaded once here so
@@ -12,7 +12,7 @@ import config from "~/vex.config"
12
12
  *
13
13
  * Mirrors `apps/test/src/vexcms/api.ts`'s `vexServerApi` binding.
14
14
  */
15
- export const { get, find, search, create, remove, update, globals } = vexServerApi<DataModel>({
15
+ export const { get, find, search, create, remove, update, globals, publishedSlugs } = vexServerApi<DataModel>({
16
16
  config,
17
17
  getAuth: createGetAuth({
18
18
  orgCollectionSlug: TABLE_SLUG_ORGANIZATIONS,
@@ -19,14 +19,15 @@ export const ctaBlock = defineBlock({
19
19
  actions: array({
20
20
  label: "Actions",
21
21
  items: group({
22
+ label: "Action",
22
23
  fields: {
23
24
  label: text({ label: "Button Label", required: true }),
24
25
  href: text({ label: "Button Link", required: true }),
25
26
  },
26
27
  }),
27
28
  defaultValue: [
28
- { label: "Get Started", href: "/docs" },
29
- { label: "View on GitHub", href: "https://github.com/vexcms/vex" },
29
+ { label: "Get Started", href: "https://docs.vexcms.dev/guides/quickstart/" },
30
+ { label: "View on GitHub", href: "https://github.com/ianyimi/vex" },
30
31
  ],
31
32
  }),
32
33
  },
@@ -1,46 +1,62 @@
1
1
  import type { BlockComponentProps } from "@vexcms/react"
2
2
 
3
+ import { buttonVariants, cn } from "@vexcms/react"
3
4
  import Link from "next/link"
4
5
 
5
- import { cn } from "~/lib/utils"
6
- import { buttonVariants } from "@vexcms/react"
6
+ import type { CtaBlock } from "~/vex.types"
7
+
8
+ import { Container } from "~/components/Container"
7
9
 
8
10
  export { ctaBlock } from "./config"
9
11
 
12
+
13
+ /**
14
+ * CTA — the page's last surface.
15
+ *
16
+ * The only block permitted more air than a standard section: it is what
17
+ * separates the page from the footer. Shares `bg-card` with Stats, so the two
18
+ * must never sit adjacent.
19
+ *
20
+ * `actions` has no variant field, so the renderer assigns by index — first
21
+ * solid, the rest outline.
22
+ */
10
23
  export default function CTABlock({ block }: BlockComponentProps) {
11
- const { heading, subheading, actions } = block as unknown as {
12
- heading: string
13
- subheading?: string
14
- actions?: Array<{ label: string; href: string }>
15
- }
24
+ const { actions, heading, subheading } = block as CtaBlock
25
+ const items = actions ?? []
16
26
 
17
27
  return (
18
- <section className="py-16 md:py-32">
19
- <div className="mx-auto max-w-5xl px-6">
20
- <div className="text-center">
21
- <h2 className="text-4xl font-semibold text-balance lg:text-5xl">
22
- {heading}
23
- </h2>
24
- {subheading && <p className="mt-4">{subheading}</p>}
25
-
26
- <div className="mt-12 flex flex-wrap justify-center gap-4">
27
- {(actions ?? []).map((action, index) => (
28
+ <section className="border-y border-border bg-card py-16 text-center md:py-24 xl:py-32">
29
+ <Container>
30
+ <h2 className="mx-auto max-w-[24ch] text-[28px] leading-[1.12] font-bold tracking-[-0.03em] text-balance text-foreground md:text-4xl xl:text-[40px]">
31
+ {heading}
32
+ </h2>
33
+
34
+ {subheading ? (
35
+ <p className="mx-auto mt-5 max-w-[60ch] text-[17px] leading-[1.6] text-pretty text-muted-foreground">
36
+ {subheading}
37
+ </p>
38
+ ) : null}
39
+
40
+ {items.length > 0 ? (
41
+ <div className="mx-auto mt-9 flex w-full max-w-[320px] flex-col gap-3 sm:max-w-none sm:flex-row sm:flex-wrap sm:justify-center">
42
+ {items.map((action, index) => (
28
43
  <Link
29
- key={index}
30
- href={action.href ?? "/"}
31
44
  className={cn(
32
45
  buttonVariants({
33
46
  size: "lg",
34
- variant: index % 2 === 1 ? "outline" : "default",
35
- })
47
+ variant: index === 0 ? "default" : "outline",
48
+ }),
49
+ "active:translate-y-px"
36
50
  )}
51
+ href={action.href}
52
+ key={`${action.label}-${index}`}
37
53
  >
38
- <span>{action.label}</span>
54
+ {action.label}
39
55
  </Link>
40
56
  ))}
41
57
  </div>
42
- </div>
43
- </div>
58
+ ) : null}
59
+ </Container>
44
60
  </section>
45
61
  )
46
62
  }
@@ -0,0 +1,62 @@
1
+ import { array, defineBlock, group, select, text } from "@vexcms/core"
2
+
3
+ import { BLOCK_SLUG_CODE_SHOWCASE } from "../constants"
4
+
5
+ export const codeShowcaseBlock = defineBlock({
6
+ slug: BLOCK_SLUG_CODE_SHOWCASE,
7
+ label: "Code Showcase",
8
+ fields: {
9
+ heading: text({
10
+ label: "Heading",
11
+ required: true,
12
+ defaultValue: "Write the collection. Get the database.",
13
+ }),
14
+ subheading: text({
15
+ label: "Subheading",
16
+ defaultValue:
17
+ "The left pane is hand-written; the right one is the Convex schema vex dev emits from it and never edited.",
18
+ }),
19
+ panes: array({
20
+ label: "Panes",
21
+ required: true,
22
+ description:
23
+ "Two panes render side by side in one frame. Three or more render as a tab set.",
24
+ items: group({
25
+ label: "Pane",
26
+ fields: {
27
+ label: text({
28
+ label: "Pane Label",
29
+ required: true,
30
+ description: "Shown uppercase in the chrome bar.",
31
+ }),
32
+ filename: text({ label: "Filename" }),
33
+ language: select({
34
+ label: "Language",
35
+ options: [
36
+ { label: "TypeScript", value: "ts" },
37
+ { label: "TSX", value: "tsx" },
38
+ { label: "Shell", value: "bash" },
39
+ { label: "JSON", value: "json" },
40
+ ],
41
+ defaultValue: ["ts"],
42
+ }),
43
+ authored: select({
44
+ label: "Origin",
45
+ description:
46
+ "Hand-written panes carry the primary-coloured label — the only signal separating human input from generated output.",
47
+ options: [
48
+ { label: "Hand-written", value: "authored" },
49
+ { label: "Generated", value: "generated" },
50
+ ],
51
+ defaultValue: ["generated"],
52
+ }),
53
+ code: text({ label: "Code", required: true }),
54
+ caption: text({ label: "Caption" }),
55
+ },
56
+ }),
57
+ }),
58
+ },
59
+ admin: {
60
+ icon: "FileCode",
61
+ },
62
+ })
@@ -0,0 +1,106 @@
1
+ import type { BlockComponentProps } from "@vexcms/react"
2
+
3
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@vexcms/react"
4
+ import { Fragment } from "react"
5
+
6
+ import type { CodeShowcaseBlock } from "~/vex.types"
7
+
8
+ import { CodePane } from "~/components/CodePane"
9
+ import { Container } from "~/components/Container"
10
+ import { SectionHeader } from "~/components/SectionHeader"
11
+
12
+ export { codeShowcaseBlock } from "./config"
13
+
14
+
15
+
16
+ /**
17
+ * CodeShowcase — the config, and the file it wrote.
18
+ *
19
+ * Two panes share one frame with a single seam, so the pairing reads as one
20
+ * artefact rather than two cards. Three or more become a tab set inside the
21
+ * same frame. Zero panes render nothing — an empty frame is worse than no
22
+ * section.
23
+ */
24
+ export default function CodeShowcaseBlock({ block }: BlockComponentProps) {
25
+ const { heading, panes, subheading } = block as CodeShowcaseBlock
26
+ const items = panes ?? []
27
+
28
+ if (items.length === 0) {return null}
29
+
30
+ const hasCaption = items.some((pane) => Boolean(pane.caption))
31
+
32
+ return (
33
+ <section className="py-14 md:py-20 xl:py-28">
34
+ <Container>
35
+ <SectionHeader heading={heading} subheading={subheading} />
36
+
37
+ {items.length > 2 ? (
38
+ <Tabs className="overflow-hidden rounded-md border border-border" defaultValue="0">
39
+ <TabsList className="h-10 w-full justify-start rounded-none border-b border-border bg-card px-2">
40
+ {items.map((pane, index) => (
41
+ <TabsTrigger
42
+ className="rounded-sm px-3 py-[5px] text-[13px] font-medium"
43
+ key={`${pane.label}-${index}`}
44
+ value={String(index)}
45
+ >
46
+ {pane.label}
47
+ </TabsTrigger>
48
+ ))}
49
+ </TabsList>
50
+ {items.map((pane, index) => (
51
+ <TabsContent key={`${pane.label}-panel-${index}`} value={String(index)}>
52
+ <CodePane
53
+ code={pane.code}
54
+ filename={pane.filename}
55
+ language={pane.language}
56
+ tone={readTone(pane.authored)}
57
+ />
58
+ </TabsContent>
59
+ ))}
60
+ </Tabs>
61
+ ) : (
62
+ // The 1px middle track is the seam. `minmax(0,1fr)` on the pane
63
+ // columns is what lets the panes scroll instead of overflowing.
64
+ <div className="grid items-stretch overflow-hidden rounded-md border border-border xl:grid-cols-[minmax(0,1fr)_1px_minmax(0,1fr)]">
65
+ {items.map((pane, index) => (
66
+ // The keyed Fragment is what React needs: keys on the children
67
+ // inside a bare `<>` do not count, so this logged "Each child in
68
+ // a list should have a unique key prop" on every render.
69
+ <Fragment key={`${pane.label}-${index}`}>
70
+ {index > 0 ? (
71
+ <div aria-hidden className="bg-border max-xl:h-px xl:w-px" />
72
+ ) : null}
73
+ <CodePane
74
+ code={pane.code}
75
+ filename={pane.filename}
76
+ label={pane.label}
77
+ language={pane.language}
78
+ tone={readTone(pane.authored)}
79
+ />
80
+ </Fragment>
81
+ ))}
82
+ </div>
83
+ )}
84
+
85
+ {hasCaption ? (
86
+ <div className="mt-3 grid gap-3 xl:grid-cols-[minmax(0,1fr)_1px_minmax(0,1fr)]">
87
+ {items.map((pane, index) => (
88
+ <Fragment key={`caption-${pane.label}-${index}`}>
89
+ {index > 0 ? <div aria-hidden /> : null}
90
+ <p className="text-[13px] leading-[1.55] text-muted-foreground">
91
+ {pane.caption ?? ""}
92
+ </p>
93
+ </Fragment>
94
+ ))}
95
+ </div>
96
+ ) : null}
97
+ </Container>
98
+ </section>
99
+ )
100
+ }
101
+
102
+ /** `select` stores an array; `[0]` with a literal fallback survives both. */
103
+ function readTone(value: string | string[] | undefined): "authored" | "generated" {
104
+ const raw = Array.isArray(value) ? value[0] : value
105
+ return raw === "authored" ? "authored" : "generated"
106
+ }
@@ -18,13 +18,14 @@ export const faqBlock = defineBlock({
18
18
  }),
19
19
  supportLink: text({
20
20
  label: "Support Link",
21
- defaultValue: "https://github.com/vexcms/vex/issues",
21
+ defaultValue: "https://github.com/ianyimi/vex/issues",
22
22
  description: "URL to contact support page",
23
23
  }),
24
24
  items: array({
25
25
  label: "FAQ Items",
26
26
  required: true,
27
27
  items: group({
28
+ label: "Question",
28
29
  fields: {
29
30
  question: text({ label: "Question", required: true }),
30
31
  answer: text({ label: "Answer", required: true }),
@@ -34,7 +35,7 @@ export const faqBlock = defineBlock({
34
35
  {
35
36
  question: "What is Vex CMS?",
36
37
  answer:
37
- "Vex CMS is a headless content management system built on Convex. It provides real-time data, type-safe schemas, draft/publish workflows, live preview, and a beautiful admin panel — all configured with TypeScript.",
38
+ "Vex CMS is a headless content management system built on Convex. It provides real-time data, type-safe schemas, database-driven theming, role-based access control, and a full admin panel — all configured in TypeScript. Versioning, drafts, and live preview are in progress; see the roadmap.",
38
39
  },
39
40
  {
40
41
  question: "How is Vex different from other headless CMS platforms?",
@@ -59,7 +60,7 @@ export const faqBlock = defineBlock({
59
60
  {
60
61
  question: "How do I get started?",
61
62
  answer:
62
- "Run `npx create-vexcms@latest` to scaffold a new project with Vex CMS pre-configured. The CLI sets up your Next.js app, Convex backend, authentication, and admin panel in under a minute.",
63
+ "Run `npx create-vexcms@alpha` to scaffold a new project with Vex CMS pre-configured. The CLI sets up your Next.js app, Convex backend, authentication, and admin panel in under a minute.",
63
64
  },
64
65
  ],
65
66
  }),