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.
- package/README.md +48 -14
- package/dist/index.js +1714 -1504
- package/package.json +2 -2
- package/templates/base-nextjs/README.md +18 -5
- package/templates/base-nextjs/convex/auth/index.ts +2 -2
- package/templates/base-nextjs/convex/auth/options.ts +27 -26
- package/templates/base-nextjs/convex/auth/schema.ts +51 -0
- package/templates/base-nextjs/convex/auth/sessions.ts +3 -3
- package/templates/base-nextjs/convex/tsconfig.json +2 -2
- package/templates/base-nextjs/convex/vex/firstUser.ts +2 -2
- package/templates/base-nextjs/convex/vex/globals.ts +3 -2
- package/templates/base-nextjs/convex/vex/media.ts +3 -2
- package/templates/base-nextjs/convex/vex.schema.ts +1 -0
- package/templates/base-nextjs/convex/vex.ts +17 -4
- package/templates/base-nextjs/next.config.ts +52 -6
- package/templates/base-nextjs/package.json +18 -7
- package/templates/base-nextjs/public/favicons/favicon.ico +0 -0
- package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +27 -15
- package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +2 -2
- package/templates/base-nextjs/src/app/(vexcms)/admin/clientProviders.tsx +9 -14
- package/templates/base-nextjs/src/app/(vexcms)/admin/layout.tsx +8 -6
- package/templates/base-nextjs/src/app/api/vex/revalidate/route.ts +19 -0
- package/templates/base-nextjs/src/app/globals.css +10 -9
- package/templates/base-nextjs/src/app/layout.tsx +6 -0
- package/templates/base-nextjs/src/auth/access.ts +14 -2
- package/templates/base-nextjs/src/auth/serverUtils.ts +20 -6
- package/templates/base-nextjs/src/components/WelcomePage.tsx +54 -43
- package/templates/base-nextjs/src/components/component-example.tsx +1 -1
- package/templates/base-nextjs/src/components/providers/server.tsx +16 -8
- package/templates/base-nextjs/src/vex.config.server.ts +30 -0
- package/templates/base-nextjs/src/vex.config.ts +14 -11
- package/templates/base-nextjs/src/vex.types.ts +2 -1
- package/templates/base-nextjs/src/vexcms/collections/images.ts +1 -1
- package/templates/base-nextjs/src/vexcms/collections/users.ts +3 -1
- package/templates/marketing-site/convex/pages.ts +20 -1
- package/templates/marketing-site/convex/schema.ts +4 -4
- package/templates/marketing-site/convex/seed.ts +515 -70
- package/templates/marketing-site/convex/siteSettings.ts +1 -1
- package/templates/marketing-site/convex/theme.ts +3 -3
- package/templates/marketing-site/convex/vex.schema.ts +75 -38
- package/templates/marketing-site/public/favicon.svg +14 -0
- package/templates/marketing-site/public/favicons/apple-touch-icon.png +0 -0
- package/templates/marketing-site/public/favicons/favicon.ico +0 -0
- package/templates/marketing-site/src/app/(frontend)/(site)/PageContent.tsx +64 -0
- package/templates/marketing-site/src/app/(frontend)/(site)/SiteLivePreviewProvider.tsx +31 -0
- package/templates/marketing-site/src/app/(frontend)/(site)/[slug]/page.tsx +63 -0
- package/templates/marketing-site/src/app/(frontend)/(site)/layout.tsx +50 -0
- package/templates/marketing-site/src/app/(frontend)/(site)/page.tsx +33 -0
- package/templates/marketing-site/src/app/(frontend)/layout.tsx +10 -24
- package/templates/marketing-site/src/app/(vexcms)/admin/layout.tsx +32 -0
- package/templates/marketing-site/src/app/globals.css +413 -0
- package/templates/marketing-site/src/app/layout.tsx +85 -0
- package/templates/marketing-site/src/app/robots.ts +13 -0
- package/templates/marketing-site/src/app/sitemap.ts +18 -0
- package/templates/marketing-site/src/components/BrandMark.tsx +27 -0
- package/templates/marketing-site/src/components/CodeHighlightContext.tsx +76 -0
- package/templates/marketing-site/src/components/CodePane.tsx +98 -0
- package/templates/marketing-site/src/components/Container.tsx +25 -0
- package/templates/marketing-site/src/components/CopyButton.tsx +80 -0
- package/templates/marketing-site/src/components/FirstAdminBootstrap.tsx +54 -0
- package/templates/marketing-site/src/components/InstallCommand.tsx +29 -0
- package/templates/marketing-site/src/components/MediaImage.tsx +59 -0
- package/templates/marketing-site/src/components/SectionHeader.tsx +51 -0
- package/templates/marketing-site/src/components/SiteFooter.tsx +8 -6
- package/templates/marketing-site/src/components/SiteHeader.tsx +8 -6
- package/templates/marketing-site/src/components/ThemeLive.tsx +1 -2
- package/templates/marketing-site/src/components/ThemeStyle.tsx +3 -3
- package/templates/marketing-site/src/components/motion-primitives/animated-group.tsx +44 -120
- package/templates/marketing-site/src/components/motion-primitives/reveal.module.css +32 -0
- package/templates/marketing-site/src/components/motion-primitives/text-effect.tsx +47 -266
- package/templates/marketing-site/src/lib/codeHighlightKey.ts +27 -0
- package/templates/marketing-site/src/lib/highlight.ts +134 -0
- package/templates/marketing-site/src/lib/highlightClient.ts +37 -0
- package/templates/marketing-site/src/lib/metadata.ts +61 -33
- package/templates/marketing-site/src/lib/resolvePagePath.ts +8 -0
- package/templates/marketing-site/src/lib/vex.ts +18 -0
- package/templates/marketing-site/src/proxy.ts +179 -0
- package/templates/marketing-site/src/vex.config.server.ts +27 -0
- package/templates/marketing-site/src/vex.config.ts +27 -9
- package/templates/marketing-site/src/vex.types.ts +102 -76
- package/templates/marketing-site/src/vexcms/api.ts +2 -2
- package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -2
- package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +40 -24
- package/templates/marketing-site/src/vexcms/blocks/CodeShowcase/config.ts +62 -0
- package/templates/marketing-site/src/vexcms/blocks/CodeShowcase/index.tsx +106 -0
- package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -3
- package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +63 -50
- package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +1 -0
- package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +43 -58
- package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +6 -3
- package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +90 -53
- package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +10 -3
- package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +205 -138
- package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +27 -12
- package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +174 -119
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +2 -1
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +59 -51
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +26 -5
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +256 -73
- package/templates/marketing-site/src/vexcms/blocks/Split/config.ts +62 -0
- package/templates/marketing-site/src/vexcms/blocks/Split/index.tsx +186 -0
- package/templates/marketing-site/src/vexcms/blocks/Stats/config.ts +35 -0
- package/templates/marketing-site/src/vexcms/blocks/Stats/index.tsx +77 -0
- package/templates/marketing-site/src/vexcms/blocks/config.ts +27 -11
- package/templates/marketing-site/src/vexcms/blocks/constants.ts +3 -0
- package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -0
- package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -0
- package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -0
- package/templates/marketing-site/src/vexcms/collections/pages.ts +23 -1
- package/templates/marketing-site/src/vexcms/collections/themeColors.ts +14 -14
- package/templates/marketing-site/src/vexcms/collections/users.ts +20 -0
- package/dist/index.mjs +0 -30976
- package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +0 -53
- package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +0 -25
- 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 {
|
|
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
|
-
|
|
40
|
-
primaryCtaLabel,
|
|
30
|
+
installCommand,
|
|
41
31
|
primaryCtaHref,
|
|
42
|
-
|
|
32
|
+
primaryCtaLabel,
|
|
43
33
|
secondaryCtaHref,
|
|
44
|
-
|
|
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
|
-
<
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
</
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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@
|
|
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 {
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
64
|
-
</
|
|
71
|
+
) : null}
|
|
72
|
+
</li>
|
|
65
73
|
)
|
|
66
74
|
})}
|
|
67
|
-
</
|
|
68
|
-
</
|
|
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: "
|
|
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
|
|
92
|
+
description: "Draft/publish workflow with autosave and version history — in active development.",
|
|
90
93
|
status: "coming-soon",
|
|
91
94
|
},
|
|
92
95
|
{
|
|
93
|
-
feature: "
|
|
94
|
-
description:
|
|
95
|
-
|
|
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
|
},
|