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
@@ -1,143 +1,198 @@
1
- "use client"
2
-
3
1
  import type { BlockComponentProps } from "@vexcms/react"
4
2
 
3
+ import { buttonVariants, cn } from "@vexcms/react"
5
4
  import { ArrowRight } from "lucide-react"
6
5
  import Link from "next/link"
7
- import { buttonVariants, cn } from "@vexcms/react"
8
6
 
9
- import { AnimatedGroup } from "~/components/motion-primitives/animated-group"
7
+ import type { HeroBlock } from "~/vex.types"
8
+
9
+ import { Container } from "~/components/Container"
10
+ import { InstallCommand } from "~/components/InstallCommand"
11
+ import { AnimatedItem } from "~/components/motion-primitives/animated-group"
10
12
  import { TextEffect } from "~/components/motion-primitives/text-effect"
11
13
 
12
14
  export { heroBlock } from "./config"
13
15
 
14
- const transitionVariants = {
15
- item: {
16
- hidden: {
17
- filter: "blur(12px)",
18
- opacity: 0,
19
- y: 12,
20
- },
21
- visible: {
22
- filter: "blur(0px)",
23
- opacity: 1,
24
- transition: {
25
- type: "spring",
26
- bounce: 0.3,
27
- duration: 1.5,
28
- },
29
- y: 0,
30
- },
31
- },
32
- }
33
16
 
17
+ /**
18
+ * Hero — one headline, one command, no illustration.
19
+ *
20
+ * `full` is the landing hero: 90vh, centred, token-derived decorative
21
+ * background, and the site's only entrance animation. `compact` is the
22
+ * interior page-header band: left-aligned, no decoration, closed with a rule
23
+ * so the block below starts against a line.
24
+ */
34
25
  export default function HeroBlock({ block }: BlockComponentProps) {
35
26
  const {
36
- badgeText,
37
27
  badgeLink,
28
+ badgeText,
38
29
  heading,
39
- subheading,
40
- primaryCtaLabel,
30
+ installCommand,
41
31
  primaryCtaHref,
42
- secondaryCtaLabel,
32
+ primaryCtaLabel,
43
33
  secondaryCtaHref,
44
- } = block as unknown as Record<string, string>
34
+ secondaryCtaLabel,
35
+ subheading,
36
+ variant,
37
+ } = block as HeroBlock
38
+
39
+ const hasPrimary = Boolean(primaryCtaLabel && primaryCtaHref)
40
+ const hasSecondary = Boolean(secondaryCtaLabel && secondaryCtaHref)
41
+
42
+ if (readVariant(variant) === "compact") {
43
+ return (
44
+ <section className="border-b border-border pt-28 pb-10 md:pt-32 md:pb-14 xl:pt-36 xl:pb-16">
45
+ <Container>
46
+ {badgeText ? (
47
+ <p className="mb-5 text-xs font-medium tracking-[0.08em] text-muted-foreground uppercase">
48
+ {badgeText}
49
+ </p>
50
+ ) : null}
51
+
52
+ <TextEffect
53
+ as="h1"
54
+ className="max-w-[24ch] text-[32px] leading-[1.12] font-bold tracking-[-0.03em] text-balance md:text-4xl xl:text-5xl"
55
+ per="line"
56
+ >
57
+ {heading}
58
+ </TextEffect>
59
+
60
+ {subheading ? (
61
+ <TextEffect
62
+ as="p"
63
+ className="mt-4 max-w-[64ch] text-[17px] leading-[1.6] text-pretty text-muted-foreground md:text-lg xl:text-[19px]"
64
+ delay={0.1}
65
+ per="line"
66
+ >
67
+ {subheading}
68
+ </TextEffect>
69
+ ) : null}
70
+
71
+ {hasPrimary || hasSecondary ? (
72
+ <div className="mt-8 flex flex-wrap gap-3">
73
+ {hasPrimary ? (
74
+ <Link className={buttonVariants()} href={primaryCtaHref ?? "/"}>
75
+ {primaryCtaLabel}
76
+ </Link>
77
+ ) : null}
78
+ {hasSecondary ? (
79
+ <Link
80
+ className={buttonVariants({ variant: "outline" })}
81
+ href={secondaryCtaHref ?? "/"}
82
+ >
83
+ {secondaryCtaLabel}
84
+ </Link>
85
+ ) : null}
86
+ </div>
87
+ ) : null}
88
+ </Container>
89
+ </section>
90
+ )
91
+ }
45
92
 
46
93
  return (
47
- <div className="overflow-hidden">
94
+ <section className="relative isolate flex min-h-[calc(100svh-7rem)] flex-col items-center justify-center overflow-hidden pt-24 pb-16 md:pt-28 md:pb-20 xl:pt-32 xl:pb-24">
95
+ {/* Both layers derive from tokens, so a theme swap softens them rather
96
+ than breaking them. No image, ever — a fresh scaffold has no media. */}
48
97
  <div
49
98
  aria-hidden
50
- className="absolute inset-0 isolate hidden opacity-65 contain-strict lg:block"
51
- >
52
- <div className="absolute top-0 left-0 h-320 w-140 -translate-y-87.5 -rotate-45 rounded-full bg-[radial-gradient(68.54%_68.72%_at_55.02%_31.46%,hsla(0,0%,85%,.08)_0,hsla(0,0%,55%,.02)_50%,hsla(0,0%,45%,0)_80%)]" />
53
- <div className="absolute top-0 left-0 h-320 w-60 [translate:5%_-50%] -rotate-45 rounded-full bg-[radial-gradient(50%_50%_at_50%_50%,hsla(0,0%,85%,.06)_0,hsla(0,0%,45%,.02)_80%,transparent_100%)]" />
54
- <div className="absolute top-0 left-0 h-320 w-60 -translate-y-87.5 -rotate-45 bg-[radial-gradient(50%_50%_at_50%_50%,hsla(0,0%,85%,.04)_0,hsla(0,0%,45%,.02)_80%,transparent_100%)]" />
55
- </div>
56
- <section>
57
- <div className="relative min-h-[90vh] flex flex-col items-center justify-center">
58
- <div className="absolute inset-0 -z-10 size-full [background:radial-gradient(125%_125%_at_50%_100%,transparent_0%,var(--color-background)_75%)]" />
59
- <div className="mx-auto max-w-7xl px-6">
60
- <div className="text-center sm:mx-auto lg:mr-auto">
61
- {badgeText && badgeLink && (
62
- <AnimatedGroup
63
- // @ts-expect-error motion-primitives Variants type is stricter than the local transitionVariants shape
64
- variants={transitionVariants}
65
- >
66
- <Link
67
- className="hover:bg-background dark:hover:border-t-border bg-muted group mx-auto flex w-fit items-center gap-4 rounded-full border p-1 pl-4 shadow-md shadow-zinc-950/5 transition-colors duration-300 dark:border-t-white/5 dark:shadow-zinc-950"
68
- href={badgeLink}
69
- >
70
- <span className="text-foreground text-sm">{badgeText}</span>
71
- <span className="dark:border-background block h-4 w-0.5 border-l bg-white dark:bg-zinc-700" />
72
- <div className="bg-background group-hover:bg-muted size-6 overflow-hidden rounded-full duration-500">
73
- <div className="flex w-12 -translate-x-1/2 duration-500 ease-in-out group-hover:translate-x-0">
74
- <span className="flex size-6">
75
- <ArrowRight className="m-auto size-3" />
76
- </span>
77
- <span className="flex size-6">
78
- <ArrowRight className="m-auto size-3" />
79
- </span>
80
- </div>
81
- </div>
82
- </Link>
83
- </AnimatedGroup>
84
- )}
85
-
86
- <TextEffect
87
- as="h1"
88
- className="mt-8 text-6xl text-balance md:text-7xl lg:mt-16 xl:text-[5.25rem]"
89
- preset="fade-in-blur"
90
- speedSegment={0.3}
91
- >
92
- {heading ?? ""}
93
- </TextEffect>
94
- <TextEffect
95
- as="p"
96
- className="mx-auto mt-8 max-w-2xl text-lg text-balance text-muted-foreground"
97
- delay={0.5}
98
- per="line"
99
- preset="fade-in-blur"
100
- speedSegment={0.3}
101
- >
102
- {subheading ?? ""}
103
- </TextEffect>
104
-
105
- <AnimatedGroup
106
- className="mt-12 flex flex-col items-center justify-center gap-2 md:flex-row"
107
- // @ts-expect-error motion-primitives Variants type is stricter than the local transitionVariants shape
108
- variants={{
109
- container: {
110
- visible: {
111
- transition: {
112
- delayChildren: 0.75,
113
- staggerChildren: 0.05,
114
- },
115
- },
116
- },
117
- ...transitionVariants,
118
- }}
99
+ className="pointer-events-none absolute inset-0 -z-10 opacity-[0.22]"
100
+ style={{
101
+ backgroundImage:
102
+ "linear-gradient(to right, var(--color-border) 1px, transparent 1px), linear-gradient(to bottom, var(--color-border) 1px, transparent 1px)",
103
+ backgroundSize: "80px 80px",
104
+ maskImage: "radial-gradient(ellipse at 50% 0%, black, transparent 70%)",
105
+ WebkitMaskImage: "radial-gradient(ellipse at 50% 0%, black, transparent 70%)",
106
+ }}
107
+ />
108
+ <div
109
+ aria-hidden
110
+ className="pointer-events-none absolute top-1/3 left-1/2 -z-10 h-80 w-[40rem] -translate-x-1/2 -translate-y-1/2 rounded-[50%] bg-primary/10 blur-[120px]"
111
+ />
112
+
113
+ <Container className="flex flex-col items-center text-center">
114
+ {badgeText ? (
115
+ <AnimatedItem>
116
+ {badgeLink ? (
117
+ <Link
118
+ className="group inline-flex items-center gap-2 rounded-4xl border border-border bg-card py-[5px] pr-[6px] pl-3 text-[13px] font-medium text-muted-foreground transition-colors duration-[180ms] ease-[var(--ease-emphasized)] hover:border-primary/40"
119
+ href={badgeLink}
119
120
  >
120
- <div className="bg-foreground/10 rounded-[calc(var(--radius-xl)+0.125rem)] border p-0.5">
121
- <Link
122
- href={primaryCtaHref ?? "/"}
123
- className={cn(buttonVariants({ size: "lg" }), "rounded-xl px-5 text-base")}
124
- >
125
- <span className="text-nowrap">{primaryCtaLabel}</span>
126
- </Link>
127
- </div>
128
- {secondaryCtaLabel && (
129
- <Link
130
- href={secondaryCtaHref ?? "/"}
131
- className={cn(buttonVariants({ variant: "ghost", size: "lg" }), "h-10.5 rounded-xl px-5")}
132
- >
133
- <span className="text-nowrap">{secondaryCtaLabel}</span>
134
- </Link>
135
- )}
136
- </AnimatedGroup>
121
+ {badgeText}
122
+ <span className="flex size-5 items-center justify-center rounded-4xl bg-primary/14 text-primary transition-colors duration-[180ms] group-hover:bg-primary/22">
123
+ <ArrowRight className="size-3" />
124
+ </span>
125
+ </Link>
126
+ ) : (
127
+ <span className="inline-flex items-center rounded-4xl border border-border bg-card px-3 py-[5px] text-[13px] font-medium text-muted-foreground">
128
+ {badgeText}
129
+ </span>
130
+ )}
131
+ </AnimatedItem>
132
+ ) : null}
133
+
134
+ <TextEffect
135
+ as="h1"
136
+ className={cn(
137
+ "max-w-[18ch] text-[40px] leading-[1.02] font-extrabold tracking-[-0.04em] text-balance md:text-[56px] xl:text-[72px]",
138
+ badgeText && "mt-8"
139
+ )}
140
+ delay={0.08}
141
+ per="word"
142
+ stagger={0.04}
143
+ >
144
+ {heading}
145
+ </TextEffect>
146
+
147
+ {subheading ? (
148
+ <AnimatedItem className="mt-7" delay={0.28}>
149
+ <p className="max-w-[60ch] text-[17px] leading-[1.6] text-pretty text-muted-foreground md:text-lg xl:text-[19px]">
150
+ {subheading}
151
+ </p>
152
+ </AnimatedItem>
153
+ ) : null}
154
+
155
+ {hasPrimary || hasSecondary ? (
156
+ <AnimatedItem className="mt-10 w-full" delay={0.38}>
157
+ <div className="mx-auto flex w-full max-w-[320px] flex-col gap-3 sm:max-w-none sm:flex-row sm:justify-center">
158
+ {hasPrimary ? (
159
+ <Link
160
+ className={cn(buttonVariants({ size: "lg" }), "active:translate-y-px")}
161
+ href={primaryCtaHref ?? "/"}
162
+ >
163
+ {primaryCtaLabel}
164
+ </Link>
165
+ ) : null}
166
+ {hasSecondary ? (
167
+ <Link
168
+ className={cn(
169
+ buttonVariants({ size: "lg", variant: "outline" }),
170
+ "active:translate-y-px"
171
+ )}
172
+ href={secondaryCtaHref ?? "/"}
173
+ >
174
+ {secondaryCtaLabel}
175
+ </Link>
176
+ ) : null}
137
177
  </div>
138
- </div>
139
- </div>
140
- </section>
141
- </div>
178
+ </AnimatedItem>
179
+ ) : null}
180
+
181
+ {installCommand ? (
182
+ <AnimatedItem className="mt-7 flex w-full justify-center" delay={0.46}>
183
+ <InstallCommand command={installCommand} />
184
+ </AnimatedItem>
185
+ ) : null}
186
+ </Container>
187
+ </section>
142
188
  )
143
189
  }
190
+
191
+ /**
192
+ * `select` always stores an array. Reading `[0]` with a literal fallback keeps
193
+ * a hand-edited scalar and an empty array both landing on the default.
194
+ */
195
+ function readVariant(value: string | string[] | undefined): "compact" | "full" {
196
+ const raw = Array.isArray(value) ? value[0] : value
197
+ return raw === "compact" ? "compact" : "full"
198
+ }
@@ -20,6 +20,7 @@ export const howItWorksBlock = defineBlock({
20
20
  label: "Steps",
21
21
  required: true,
22
22
  items: group({
23
+ label: "Step",
23
24
  fields: {
24
25
  icon: text({
25
26
  label: "Icon",
@@ -34,7 +35,7 @@ export const howItWorksBlock = defineBlock({
34
35
  icon: "Terminal",
35
36
  title: "Scaffold your project",
36
37
  description:
37
- "Run npx create-vexcms@latest to get a Next.js app with Convex, authentication, and the admin panel pre-configured.",
38
+ "Run npx create-vexcms@alpha to get a Next.js app with Convex, authentication, and the admin panel pre-configured.",
38
39
  },
39
40
  {
40
41
  icon: "Code",
@@ -1,71 +1,79 @@
1
- "use client"
2
-
3
1
  import type { BlockComponentProps } from "@vexcms/react"
4
2
 
3
+ import { cn } from "@vexcms/react"
5
4
  import { icons } from "lucide-react"
6
5
 
7
- import { cn } from "~/lib/utils"
6
+ import type { HowItWorksBlock } from "~/vex.types"
7
+
8
+ import { Container } from "~/components/Container"
9
+ import { SectionHeader } from "~/components/SectionHeader"
8
10
 
9
11
  export { howItWorksBlock } from "./config"
10
12
 
11
- function LucideIcon({ name }: { name: string }) {
12
- const Icon = icons[name as keyof typeof icons]
13
- if (!Icon) return null
14
- return <Icon className="size-5" />
15
- }
13
+ /** Past this many steps a four-up rail squeezes descriptions below a readable
14
+ * measure, so the grid drops to three columns. */
15
+ const WIDE_RAIL_LIMIT = 8
16
16
 
17
- export default function HowItWorksBlock({ block }: BlockComponentProps) {
18
- const { heading, subheading, steps } = block as unknown as {
19
- heading: string
20
- subheading?: string
21
- steps?: Array<{ icon?: string; title: string; description: string }>
22
- }
17
+ /**
18
+ * HowItWorks — a numbered rail, not a timeline graphic.
19
+ *
20
+ * Origin UI's timeline supplies the connector idea; its `radix-ui`
21
+ * `Slot.Root` polymorphism is dropped for concrete elements and its
22
+ * dot-and-line SVG is replaced by border utilities, which cost nothing and
23
+ * follow the theme.
24
+ */
25
+ export default function HowItWorksBlockRenderer({ block }: BlockComponentProps) {
26
+ const { heading, steps, subheading } = block as HowItWorksBlock
27
+ const items = steps ?? []
23
28
 
24
- return (
25
- <section className="py-16 md:py-32">
26
- <div className="mx-auto max-w-3xl px-6">
27
- <div className="text-center">
28
- <h2 className="text-4xl font-semibold text-balance lg:text-5xl">
29
- {heading}
30
- </h2>
31
- {subheading && (
32
- <p className="text-muted-foreground mt-4 text-balance">
33
- {subheading}
34
- </p>
35
- )}
36
- </div>
29
+ if (items.length === 0) {return null}
37
30
 
38
- <div className="mt-16">
39
- {(steps ?? []).map((step, index) => {
40
- const isLast = index === (steps?.length ?? 1) - 1
31
+ return (
32
+ <section className="py-14 md:py-20 xl:py-28">
33
+ <Container>
34
+ <SectionHeader heading={heading} subheading={subheading} />
41
35
 
36
+ <ol
37
+ className={cn(
38
+ "grid list-none gap-8 md:grid-cols-2 md:gap-x-8 md:gap-y-10 xl:border-t xl:border-border xl:pt-6",
39
+ items.length > WIDE_RAIL_LIMIT ? "xl:grid-cols-3" : "xl:grid-cols-4"
40
+ )}
41
+ >
42
+ {items.map((step, index) => {
43
+ const Glyph = step.icon ? icons[step.icon as keyof typeof icons] : undefined
42
44
  return (
43
- <div key={index} className="flex flex-row gap-6">
44
- <div className="flex flex-col items-center">
45
- <div className="bg-primary text-primary-foreground flex size-10 shrink-0 items-center justify-center rounded-full">
46
- {step.icon ? (
47
- <LucideIcon name={step.icon} />
48
- ) : (
49
- <span className="text-sm font-semibold">
50
- {index + 1}
51
- </span>
52
- )}
53
- </div>
54
- {!isLast && (
55
- <div className="bg-border w-px flex-1" />
56
- )}
45
+ <li
46
+ className="border-t border-border pt-4 xl:border-t-0 xl:pt-0"
47
+ key={`${step.title}-${index}`}
48
+ >
49
+ <div className="flex items-baseline gap-3">
50
+ {/* Generated by the renderer, never a field — reordering the
51
+ array must not force an editor to renumber. */}
52
+ <span className="font-mono text-2xl leading-none font-semibold tracking-[-0.03em] text-primary tabular-nums">
53
+ {String(index + 1).padStart(2, "0")}
54
+ </span>
55
+ {/* The connector is what makes separate cells read as a
56
+ sequence, and is why the icon sits right, not left. The
57
+ last one is kept: it points at the CTA below. */}
58
+ <span aria-hidden className="h-px flex-1 bg-border" />
59
+ {Glyph ? (
60
+ <Glyph className="size-3.5 shrink-0 self-center text-muted-foreground/70" />
61
+ ) : null}
57
62
  </div>
58
- <div className={cn("flex-1", !isLast && "pb-10")}>
59
- <h3 className="text-lg font-semibold">{step.title}</h3>
60
- <p className="text-muted-foreground mt-1">
63
+
64
+ <h3 className="mt-4 text-[17px] leading-[1.35] font-semibold text-foreground">
65
+ {step.title}
66
+ </h3>
67
+ {step.description ? (
68
+ <p className="mt-2 text-sm leading-[1.6] text-pretty text-muted-foreground">
61
69
  {step.description}
62
70
  </p>
63
- </div>
64
- </div>
71
+ ) : null}
72
+ </li>
65
73
  )
66
74
  })}
67
- </div>
68
- </div>
75
+ </ol>
76
+ </Container>
69
77
  </section>
70
78
  )
71
79
  }
@@ -20,6 +20,7 @@ export const roadmapBlock = defineBlock({
20
20
  label: "Roadmap Items",
21
21
  required: true,
22
22
  items: group({
23
+ label: "Roadmap Item",
23
24
  fields: {
24
25
  feature: text({ label: "Feature Name", required: true }),
25
26
  description: text({ label: "Description" }),
@@ -28,8 +29,10 @@ export const roadmapBlock = defineBlock({
28
29
  required: true,
29
30
  options: [
30
31
  { label: "Shipped", value: "shipped" },
31
- { label: "Coming Soon", value: "coming-soon" },
32
+ { label: "In progress", value: "in-progress" },
32
33
  { label: "Planned", value: "planned" },
34
+ { label: "Exploring", value: "exploring" },
35
+ { label: "Future", value: "future" },
33
36
  ],
34
37
  defaultValue: ["shipped"],
35
38
  }),
@@ -86,18 +89,30 @@ export const roadmapBlock = defineBlock({
86
89
  },
87
90
  {
88
91
  feature: "Versioning & Drafts",
89
- description: "Draft/publish workflow with live preview — in active development.",
92
+ description: "Draft/publish workflow with autosave and version history — in active development.",
90
93
  status: "coming-soon",
91
94
  },
92
95
  {
93
- feature: "Richtext, JSON, Email & Textarea Fields",
94
- description: "Plate.js-powered rich text, plus structured JSON, email, and multi-line text inputs.",
95
- status: "planned",
96
+ feature: "Live Preview",
97
+ description:
98
+ "Side-by-side preview of draft content against the real frontend before publishing — builds on the drafts infrastructure.",
99
+ status: "coming-soon",
96
100
  },
97
101
  {
98
102
  feature: "Form Builder & Lifecycle Hooks",
99
103
  description:
100
104
  "Composable form fields beyond content editing, plus beforeChange/afterChange hooks for custom side effects.",
105
+ status: "coming-soon",
106
+ },
107
+ {
108
+ feature: "Field Input Consistency Pass",
109
+ description:
110
+ "Touch-ups across field inputs — starting with the relationship field — for consistent interaction patterns in the admin panel.",
111
+ status: "coming-soon",
112
+ },
113
+ {
114
+ feature: "Richtext, JSON, Email & Textarea Fields",
115
+ description: "Plate.js-powered rich text, plus structured JSON, email, and multi-line text inputs.",
101
116
  status: "planned",
102
117
  },
103
118
  {
@@ -105,6 +120,12 @@ export const roadmapBlock = defineBlock({
105
120
  description: "Invite users, assign roles, and issue scoped read-only API tokens for external integrations.",
106
121
  status: "planned",
107
122
  },
123
+ {
124
+ feature: "React Package Testing Suite",
125
+ description:
126
+ "Exportable Vitest suite from @vexcms/react for testing custom field components and admin extensions in consumer projects.",
127
+ status: "planned",
128
+ },
108
129
  ],
109
130
  }),
110
131
  },