create-vexcms 0.1.0-alpha.2 → 0.1.0-alpha.4
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 +11 -3
- package/dist/index.js +1183 -1013
- package/package.json +1 -1
- package/templates/base-nextjs/README.md +11 -3
- package/templates/base-nextjs/convex/tsconfig.json +2 -2
- package/templates/base-nextjs/next.config.ts +7 -0
- package/templates/base-nextjs/package.json +6 -7
- 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/vexcms/collections/users.ts +3 -1
- package/templates/marketing-site/convex/seed.ts +247 -6
- package/templates/marketing-site/src/app/(frontend)/(site)/layout.tsx +37 -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/layout.tsx +58 -0
- package/templates/marketing-site/src/components/FirstAdminBootstrap.tsx +51 -0
- 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 +41 -267
- package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +1 -1
- package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +1 -1
- package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +2 -2
- package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +1 -1
- package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +2 -2
- package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +10 -44
- package/templates/marketing-site/src/vexcms/collections/pages.ts +3 -0
- /package/templates/marketing-site/src/app/(frontend)/{PageContent.tsx → (site)/PageContent.tsx} +0 -0
- /package/templates/marketing-site/src/app/(frontend)/{[slug] → (site)/[slug]}/page.tsx +0 -0
- /package/templates/marketing-site/src/app/(frontend)/{page.tsx → (site)/page.tsx} +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { api } from "@convex/_generated/api"
|
|
4
|
+
import { useMutation, useQuery } from "convex/react"
|
|
5
|
+
import { useRouter } from "next/navigation"
|
|
6
|
+
import { useEffect, useState } from "react"
|
|
7
|
+
|
|
8
|
+
import { useSession } from "~/auth/client"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Promotes the first signed-in user to admin when no admin exists yet, then
|
|
12
|
+
* redirects to `/admin`. Headless — renders nothing.
|
|
13
|
+
*
|
|
14
|
+
* `WelcomePage`'s own trigger (`convex/vex/firstUser.ts`'s
|
|
15
|
+
* `promoteFirstAdmin`) only runs while `PageContent` is showing its
|
|
16
|
+
* no-page-found fallback — which stops rendering the instant a `home` page
|
|
17
|
+
* document exists. A fresh marketing scaffold gets a `home` document from
|
|
18
|
+
* `pnpm seed` on day one, so that fallback may never mount again and the
|
|
19
|
+
* bootstrap trigger would never fire. This mounts unconditionally in
|
|
20
|
+
* `(frontend)/layout.tsx` instead, so the trigger survives seeding.
|
|
21
|
+
*
|
|
22
|
+
* `promoteFirstAdmin` is a Convex mutation, so concurrent signups cannot
|
|
23
|
+
* both win — Convex serializes mutations, and the loser observes
|
|
24
|
+
* `isBootstrapped` already true.
|
|
25
|
+
*/
|
|
26
|
+
export function FirstAdminBootstrap() {
|
|
27
|
+
const isBootstrapped = useQuery(api.vex.firstUser.isBootstrapped)
|
|
28
|
+
const promoteFirstAdmin = useMutation(api.vex.firstUser.promoteFirstAdmin)
|
|
29
|
+
const { data: session } = useSession()
|
|
30
|
+
const router = useRouter()
|
|
31
|
+
const [promoting, setPromoting] = useState(false)
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (session?.user && isBootstrapped === false && !promoting) {
|
|
35
|
+
setPromoting(true)
|
|
36
|
+
promoteFirstAdmin()
|
|
37
|
+
.then((result) => {
|
|
38
|
+
if (result.promoted) {
|
|
39
|
+
router.push("/admin")
|
|
40
|
+
} else {
|
|
41
|
+
setPromoting(false)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
.catch(() => {
|
|
45
|
+
setPromoting(false)
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
}, [session, isBootstrapped, promoting, promoteFirstAdmin, router])
|
|
49
|
+
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
@@ -1,138 +1,62 @@
|
|
|
1
|
-
|
|
1
|
+
import React, { type ComponentPropsWithRef, type CSSProperties } from "react"
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import React, { type ComponentPropsWithRef } from "react"
|
|
3
|
+
import { cn } from "~/lib/utils"
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
| "blur"
|
|
8
|
-
| "blur-slide"
|
|
9
|
-
| "bounce"
|
|
10
|
-
| "fade"
|
|
11
|
-
| "flip"
|
|
12
|
-
| "rotate"
|
|
13
|
-
| "scale"
|
|
14
|
-
| "slide"
|
|
15
|
-
| "swing"
|
|
16
|
-
| "zoom"
|
|
5
|
+
import styles from "./reveal.module.css"
|
|
17
6
|
|
|
18
7
|
export type AnimatedGroupProps = ComponentPropsWithRef<"div"> & {
|
|
19
8
|
as?: React.ElementType
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
container?: Variants
|
|
25
|
-
item?: Variants
|
|
26
|
-
}
|
|
9
|
+
/** Seconds before the first child starts animating. */
|
|
10
|
+
delay?: number
|
|
11
|
+
/** Seconds between each child's start. */
|
|
12
|
+
stagger?: number
|
|
27
13
|
}
|
|
28
14
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const defaultItemVariants: Variants = {
|
|
38
|
-
hidden: { opacity: 0 },
|
|
39
|
-
visible: { opacity: 1 },
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const presetVariants: Record<PresetType, Variants> = {
|
|
43
|
-
"blur-slide": {
|
|
44
|
-
hidden: { filter: "blur(4px)", y: 20 },
|
|
45
|
-
visible: { filter: "blur(0px)", y: 0 },
|
|
46
|
-
},
|
|
47
|
-
slide: {
|
|
48
|
-
hidden: { y: 20 },
|
|
49
|
-
visible: { y: 0 },
|
|
50
|
-
},
|
|
51
|
-
blur: {
|
|
52
|
-
hidden: { filter: "blur(4px)" },
|
|
53
|
-
visible: { filter: "blur(0px)" },
|
|
54
|
-
},
|
|
55
|
-
bounce: {
|
|
56
|
-
hidden: { y: -50 },
|
|
57
|
-
visible: {
|
|
58
|
-
transition: { type: "spring", damping: 10, stiffness: 400 },
|
|
59
|
-
y: 0,
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
fade: {},
|
|
63
|
-
flip: {
|
|
64
|
-
hidden: { rotateX: -90 },
|
|
65
|
-
visible: {
|
|
66
|
-
rotateX: 0,
|
|
67
|
-
transition: { type: "spring", damping: 20, stiffness: 300 },
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
rotate: {
|
|
71
|
-
hidden: { rotate: -180 },
|
|
72
|
-
visible: {
|
|
73
|
-
rotate: 0,
|
|
74
|
-
transition: { type: "spring", damping: 15, stiffness: 200 },
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
scale: {
|
|
78
|
-
hidden: { scale: 0.8 },
|
|
79
|
-
visible: { scale: 1 },
|
|
80
|
-
},
|
|
81
|
-
swing: {
|
|
82
|
-
hidden: { rotate: -10 },
|
|
83
|
-
visible: {
|
|
84
|
-
rotate: 0,
|
|
85
|
-
transition: { type: "spring", damping: 8, stiffness: 300 },
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
zoom: {
|
|
89
|
-
hidden: { scale: 0.5 },
|
|
90
|
-
visible: {
|
|
91
|
-
scale: 1,
|
|
92
|
-
transition: { type: "spring", damping: 20, stiffness: 300 },
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const addDefaultVariants = (variants: Variants) => ({
|
|
98
|
-
hidden: { ...defaultItemVariants.hidden, ...variants.hidden },
|
|
99
|
-
visible: { ...defaultItemVariants.visible, ...variants.visible },
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
function AnimatedGroup({
|
|
103
|
-
as = "div",
|
|
104
|
-
asChild = "div",
|
|
15
|
+
/**
|
|
16
|
+
* Staggers a blur + fade + rise entrance across its direct children using a
|
|
17
|
+
* pure-CSS keyframe (see `reveal.module.css`) — no animation-library
|
|
18
|
+
* dependency. Each child is wrapped in a div carrying the animation with an
|
|
19
|
+
* incremented `animation-delay`.
|
|
20
|
+
*/
|
|
21
|
+
export function AnimatedGroup({
|
|
22
|
+
as: Tag = "div",
|
|
105
23
|
children,
|
|
106
24
|
className,
|
|
107
|
-
|
|
108
|
-
|
|
25
|
+
delay = 0,
|
|
26
|
+
stagger = 0.05,
|
|
109
27
|
...divProps
|
|
110
28
|
}: AnimatedGroupProps) {
|
|
111
|
-
const selectedVariants = {
|
|
112
|
-
container: addDefaultVariants(defaultContainerVariants),
|
|
113
|
-
item: addDefaultVariants(preset ? presetVariants[preset] : {}),
|
|
114
|
-
}
|
|
115
|
-
const containerVariants = variants?.container ?? selectedVariants.container
|
|
116
|
-
const itemVariants = variants?.item ?? selectedVariants.item
|
|
117
|
-
|
|
118
|
-
const MotionComponent = motion.create(as)
|
|
119
|
-
const MotionChild = motion.create(asChild)
|
|
120
|
-
|
|
121
29
|
return (
|
|
122
|
-
<
|
|
123
|
-
animate="visible"
|
|
124
|
-
className={className}
|
|
125
|
-
initial="hidden"
|
|
126
|
-
variants={containerVariants}
|
|
127
|
-
{...divProps}
|
|
128
|
-
>
|
|
30
|
+
<Tag className={className} {...divProps}>
|
|
129
31
|
{React.Children.map(children, (child, index) => (
|
|
130
|
-
<
|
|
32
|
+
<div
|
|
33
|
+
className={styles.item}
|
|
34
|
+
style={{ "--reveal-delay": `${delay + index * stagger}s` } as CSSProperties}
|
|
35
|
+
>
|
|
131
36
|
{child}
|
|
132
|
-
</
|
|
37
|
+
</div>
|
|
133
38
|
))}
|
|
134
|
-
</
|
|
39
|
+
</Tag>
|
|
135
40
|
)
|
|
136
41
|
}
|
|
137
42
|
|
|
138
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Applies the same reveal animation to a single element without adding a
|
|
45
|
+
* wrapper div — for callers that only need one animated child.
|
|
46
|
+
*/
|
|
47
|
+
export function AnimatedItem({
|
|
48
|
+
children,
|
|
49
|
+
className,
|
|
50
|
+
delay = 0,
|
|
51
|
+
...divProps
|
|
52
|
+
}: ComponentPropsWithRef<"div"> & { delay?: number }) {
|
|
53
|
+
return (
|
|
54
|
+
<div
|
|
55
|
+
className={cn(styles.item, className)}
|
|
56
|
+
style={{ "--reveal-delay": `${delay}s` } as CSSProperties}
|
|
57
|
+
{...divProps}
|
|
58
|
+
>
|
|
59
|
+
{children}
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Shared "reveal" animation for AnimatedGroup and TextEffect — the
|
|
3
|
+
* blur + fade + rise entrance the hero uses, implemented in pure CSS so the
|
|
4
|
+
* template carries no animation-library dependency.
|
|
5
|
+
*
|
|
6
|
+
* `both` fill-mode keeps elements hidden until their delay elapses, which is
|
|
7
|
+
* what makes the stagger read as a sequence. Delay per element arrives via
|
|
8
|
+
* the `--reveal-delay` custom property.
|
|
9
|
+
*/
|
|
10
|
+
@keyframes reveal {
|
|
11
|
+
from {
|
|
12
|
+
opacity: 0;
|
|
13
|
+
filter: blur(12px);
|
|
14
|
+
transform: translateY(12px);
|
|
15
|
+
}
|
|
16
|
+
to {
|
|
17
|
+
opacity: 1;
|
|
18
|
+
filter: blur(0);
|
|
19
|
+
transform: translateY(0);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.item {
|
|
24
|
+
animation: reveal 1.1s cubic-bezier(0.25, 1, 0.5, 1) both;
|
|
25
|
+
animation-delay: var(--reveal-delay, 0s);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@media (prefers-reduced-motion: reduce) {
|
|
29
|
+
.item {
|
|
30
|
+
animation: none;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -1,292 +1,66 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
AnimatePresence,
|
|
5
|
-
motion,
|
|
6
|
-
type TargetAndTransition,
|
|
7
|
-
type Transition,
|
|
8
|
-
type Variant,
|
|
9
|
-
type Variants,
|
|
10
|
-
} from "motion/react"
|
|
11
|
-
import React from "react"
|
|
1
|
+
import React, { type CSSProperties } from "react"
|
|
12
2
|
|
|
13
3
|
import { cn } from "~/lib/utils"
|
|
14
4
|
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
import styles from "./reveal.module.css"
|
|
6
|
+
|
|
7
|
+
export type PerType = "line" | "word"
|
|
17
8
|
|
|
18
9
|
export type TextEffectProps = {
|
|
19
10
|
as?: keyof React.JSX.IntrinsicElements
|
|
20
11
|
children: string
|
|
21
12
|
className?: string
|
|
22
|
-
|
|
13
|
+
/** Seconds before the first segment starts animating. */
|
|
23
14
|
delay?: number
|
|
24
|
-
|
|
25
|
-
onAnimationStart?: () => void
|
|
15
|
+
/** Split granularity for the stagger. Lines split on `\n`. */
|
|
26
16
|
per?: PerType
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
segmentWrapperClassName?: string
|
|
30
|
-
speedReveal?: number
|
|
31
|
-
speedSegment?: number
|
|
32
|
-
style?: React.CSSProperties
|
|
33
|
-
trigger?: boolean
|
|
34
|
-
variants?: {
|
|
35
|
-
container?: Variants
|
|
36
|
-
item?: Variants
|
|
37
|
-
}
|
|
17
|
+
/** Seconds between each segment's start. */
|
|
18
|
+
stagger?: number
|
|
38
19
|
}
|
|
39
20
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
word: 0.05,
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const defaultContainerVariants: Variants = {
|
|
47
|
-
hidden: { opacity: 0 },
|
|
48
|
-
exit: {
|
|
49
|
-
transition: { staggerChildren: 0.05, staggerDirection: -1 },
|
|
50
|
-
},
|
|
51
|
-
visible: {
|
|
52
|
-
opacity: 1,
|
|
53
|
-
transition: {
|
|
54
|
-
staggerChildren: 0.05,
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const defaultItemVariants: Variants = {
|
|
60
|
-
hidden: { opacity: 0 },
|
|
61
|
-
exit: { opacity: 0 },
|
|
62
|
-
visible: {
|
|
63
|
-
opacity: 1,
|
|
64
|
-
},
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const presetVariants: Record<
|
|
68
|
-
PresetType,
|
|
69
|
-
{ container: Variants; item: Variants }
|
|
70
|
-
> = {
|
|
71
|
-
slide: {
|
|
72
|
-
container: defaultContainerVariants,
|
|
73
|
-
item: {
|
|
74
|
-
hidden: { opacity: 0, y: 20 },
|
|
75
|
-
exit: { opacity: 0, y: 20 },
|
|
76
|
-
visible: { opacity: 1, y: 0 },
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
blur: {
|
|
80
|
-
container: defaultContainerVariants,
|
|
81
|
-
item: {
|
|
82
|
-
hidden: { filter: "blur(12px)", opacity: 0 },
|
|
83
|
-
exit: { filter: "blur(12px)", opacity: 0 },
|
|
84
|
-
visible: { filter: "blur(0px)", opacity: 1 },
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
fade: {
|
|
88
|
-
container: defaultContainerVariants,
|
|
89
|
-
item: {
|
|
90
|
-
hidden: { opacity: 0 },
|
|
91
|
-
exit: { opacity: 0 },
|
|
92
|
-
visible: { opacity: 1 },
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
"fade-in-blur": {
|
|
96
|
-
container: defaultContainerVariants,
|
|
97
|
-
item: {
|
|
98
|
-
hidden: { filter: "blur(12px)", opacity: 0, y: 20 },
|
|
99
|
-
exit: { filter: "blur(12px)", opacity: 0, y: 20 },
|
|
100
|
-
visible: { filter: "blur(0px)", opacity: 1, y: 0 },
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
scale: {
|
|
104
|
-
container: defaultContainerVariants,
|
|
105
|
-
item: {
|
|
106
|
-
hidden: { opacity: 0, scale: 0 },
|
|
107
|
-
exit: { opacity: 0, scale: 0 },
|
|
108
|
-
visible: { opacity: 1, scale: 1 },
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const AnimationComponent: React.FC<{
|
|
114
|
-
per: "char" | "line" | "word"
|
|
115
|
-
segment: string
|
|
116
|
-
segmentWrapperClassName?: string
|
|
117
|
-
variants: Variants
|
|
118
|
-
}> = React.memo(({ per, segment, segmentWrapperClassName, variants }) => {
|
|
119
|
-
const content =
|
|
120
|
-
per === "line" ? (
|
|
121
|
-
<motion.span className="block" variants={variants}>
|
|
122
|
-
{segment}
|
|
123
|
-
</motion.span>
|
|
124
|
-
) : per === "word" ? (
|
|
125
|
-
<motion.span
|
|
126
|
-
aria-hidden="true"
|
|
127
|
-
className="inline-block whitespace-pre"
|
|
128
|
-
variants={variants}
|
|
129
|
-
>
|
|
130
|
-
{segment}
|
|
131
|
-
</motion.span>
|
|
132
|
-
) : (
|
|
133
|
-
<motion.span className="inline-block whitespace-pre">
|
|
134
|
-
{segment.split("").map((char, charIndex) => (
|
|
135
|
-
<motion.span
|
|
136
|
-
aria-hidden="true"
|
|
137
|
-
className="inline-block whitespace-pre"
|
|
138
|
-
key={`char-${charIndex}`}
|
|
139
|
-
variants={variants}
|
|
140
|
-
>
|
|
141
|
-
{char}
|
|
142
|
-
</motion.span>
|
|
143
|
-
))}
|
|
144
|
-
</motion.span>
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
if (!segmentWrapperClassName) {
|
|
148
|
-
return content
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const defaultWrapperClassName = per === "line" ? "block" : "inline-block"
|
|
152
|
-
|
|
153
|
-
return (
|
|
154
|
-
<span className={cn(defaultWrapperClassName, segmentWrapperClassName)}>
|
|
155
|
-
{content}
|
|
156
|
-
</span>
|
|
157
|
-
)
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
AnimationComponent.displayName = "AnimationComponent"
|
|
161
|
-
|
|
162
|
-
const splitText = (text: string, per: "char" | "line" | "word") => {
|
|
163
|
-
if (per === "line") return text.split("\n")
|
|
164
|
-
return text.split(/(\s+)/)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const hasTransition = (
|
|
168
|
-
variant: Variant,
|
|
169
|
-
): variant is TargetAndTransition & { transition?: Transition } => {
|
|
170
|
-
return (
|
|
171
|
-
typeof variant === "object" && variant !== null && "transition" in variant
|
|
172
|
-
)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const createVariantsWithTransition = (
|
|
176
|
-
baseVariants: Variants,
|
|
177
|
-
transition?: Transition & { exit?: Transition },
|
|
178
|
-
): Variants => {
|
|
179
|
-
if (!transition) return baseVariants
|
|
180
|
-
|
|
181
|
-
const { ...mainTransition } = transition
|
|
182
|
-
|
|
183
|
-
return {
|
|
184
|
-
...baseVariants,
|
|
185
|
-
exit: {
|
|
186
|
-
...baseVariants.exit,
|
|
187
|
-
transition: {
|
|
188
|
-
...(hasTransition(baseVariants.exit)
|
|
189
|
-
? baseVariants.exit.transition
|
|
190
|
-
: {}),
|
|
191
|
-
...mainTransition,
|
|
192
|
-
staggerDirection: -1,
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
visible: {
|
|
196
|
-
...baseVariants.visible,
|
|
197
|
-
transition: {
|
|
198
|
-
...(hasTransition(baseVariants.visible)
|
|
199
|
-
? baseVariants.visible.transition
|
|
200
|
-
: {}),
|
|
201
|
-
...mainTransition,
|
|
202
|
-
},
|
|
203
|
-
},
|
|
204
|
-
}
|
|
21
|
+
const defaultStagger: Record<PerType, number> = {
|
|
22
|
+
line: 0.15,
|
|
23
|
+
word: 0.04,
|
|
205
24
|
}
|
|
206
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Staggered blur + fade + rise text entrance, split per word or per line —
|
|
28
|
+
* pure CSS (see `reveal.module.css`), no animation-library dependency.
|
|
29
|
+
* Word segments keep their trailing space inside the animated span so text
|
|
30
|
+
* still wraps naturally.
|
|
31
|
+
*/
|
|
207
32
|
export function TextEffect({
|
|
208
33
|
as = "p",
|
|
209
34
|
children,
|
|
210
35
|
className,
|
|
211
|
-
containerTransition,
|
|
212
36
|
delay = 0,
|
|
213
|
-
onAnimationComplete,
|
|
214
|
-
onAnimationStart,
|
|
215
37
|
per = "word",
|
|
216
|
-
|
|
217
|
-
segmentTransition,
|
|
218
|
-
segmentWrapperClassName,
|
|
219
|
-
speedReveal = 1,
|
|
220
|
-
speedSegment = 1,
|
|
221
|
-
style,
|
|
222
|
-
trigger = true,
|
|
223
|
-
variants,
|
|
38
|
+
stagger,
|
|
224
39
|
}: TextEffectProps) {
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
? presetVariants[preset]
|
|
230
|
-
: { container: defaultContainerVariants, item: defaultItemVariants }
|
|
231
|
-
|
|
232
|
-
const stagger = defaultStaggerTimes[per] / speedReveal
|
|
233
|
-
|
|
234
|
-
const baseDuration = 0.3 / speedSegment
|
|
235
|
-
|
|
236
|
-
const customStagger = hasTransition(variants?.container?.visible ?? {})
|
|
237
|
-
? (variants?.container?.visible as TargetAndTransition).transition
|
|
238
|
-
?.staggerChildren
|
|
239
|
-
: undefined
|
|
240
|
-
|
|
241
|
-
const customDelay = hasTransition(variants?.container?.visible ?? {})
|
|
242
|
-
? (variants?.container?.visible as TargetAndTransition).transition
|
|
243
|
-
?.delayChildren
|
|
244
|
-
: undefined
|
|
245
|
-
|
|
246
|
-
const computedVariants = {
|
|
247
|
-
container: createVariantsWithTransition(
|
|
248
|
-
variants?.container ?? baseVariants.container,
|
|
249
|
-
{
|
|
250
|
-
delayChildren: customDelay ?? delay,
|
|
251
|
-
staggerChildren: customStagger ?? stagger,
|
|
252
|
-
...containerTransition,
|
|
253
|
-
exit: {
|
|
254
|
-
staggerChildren: customStagger ?? stagger,
|
|
255
|
-
staggerDirection: -1,
|
|
256
|
-
},
|
|
257
|
-
},
|
|
258
|
-
),
|
|
259
|
-
item: createVariantsWithTransition(variants?.item ?? baseVariants.item, {
|
|
260
|
-
duration: baseDuration,
|
|
261
|
-
...segmentTransition,
|
|
262
|
-
}),
|
|
263
|
-
}
|
|
40
|
+
const Tag = as as React.ElementType
|
|
41
|
+
const segments =
|
|
42
|
+
per === "line" ? children.split("\n") : children.split(/(\s+)/).filter(Boolean)
|
|
43
|
+
const step = stagger ?? defaultStagger[per]
|
|
264
44
|
|
|
45
|
+
let animatedIndex = 0
|
|
265
46
|
return (
|
|
266
|
-
<
|
|
267
|
-
{
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
segmentWrapperClassName={segmentWrapperClassName}
|
|
285
|
-
variants={computedVariants.item}
|
|
286
|
-
/>
|
|
287
|
-
))}
|
|
288
|
-
</MotionTag>
|
|
289
|
-
)}
|
|
290
|
-
</AnimatePresence>
|
|
47
|
+
<Tag className={className}>
|
|
48
|
+
{segments.map((segment, index) => {
|
|
49
|
+
if (per === "word" && /^\s+$/.test(segment)) {
|
|
50
|
+
return <React.Fragment key={index}>{segment}</React.Fragment>
|
|
51
|
+
}
|
|
52
|
+
const segmentDelay = delay + animatedIndex * step
|
|
53
|
+
animatedIndex += 1
|
|
54
|
+
return (
|
|
55
|
+
<span
|
|
56
|
+
className={cn(styles.item, per === "line" ? "block" : "inline-block")}
|
|
57
|
+
key={index}
|
|
58
|
+
style={{ "--reveal-delay": `${segmentDelay}s` } as CSSProperties}
|
|
59
|
+
>
|
|
60
|
+
{segment}
|
|
61
|
+
</span>
|
|
62
|
+
)
|
|
63
|
+
})}
|
|
64
|
+
</Tag>
|
|
291
65
|
)
|
|
292
66
|
}
|
|
@@ -26,7 +26,7 @@ export const ctaBlock = defineBlock({
|
|
|
26
26
|
}),
|
|
27
27
|
defaultValue: [
|
|
28
28
|
{ label: "Get Started", href: "/docs" },
|
|
29
|
-
{ label: "View on GitHub", href: "https://github.com/
|
|
29
|
+
{ label: "View on GitHub", href: "https://github.com/ianyimi/vex" },
|
|
30
30
|
],
|
|
31
31
|
}),
|
|
32
32
|
},
|
|
@@ -18,7 +18,7 @@ export const faqBlock = defineBlock({
|
|
|
18
18
|
}),
|
|
19
19
|
supportLink: text({
|
|
20
20
|
label: "Support Link",
|
|
21
|
-
defaultValue: "https://github.com/
|
|
21
|
+
defaultValue: "https://github.com/ianyimi/vex/issues",
|
|
22
22
|
description: "URL to contact support page",
|
|
23
23
|
}),
|
|
24
24
|
items: array({
|
|
@@ -32,7 +32,7 @@ export const footerBlock = defineBlock({
|
|
|
32
32
|
{ label: "Pricing", href: "/pricing" },
|
|
33
33
|
{ label: "Roadmap", href: "/roadmap" },
|
|
34
34
|
{ label: "Documentation", href: "/docs" },
|
|
35
|
-
{ label: "GitHub", href: "https://github.com/
|
|
35
|
+
{ label: "GitHub", href: "https://github.com/ianyimi/vex" },
|
|
36
36
|
{ label: "npm", href: "https://www.npmjs.com/package/@vexcms/core" },
|
|
37
37
|
{ label: "Convex", href: "https://convex.dev" },
|
|
38
38
|
],
|
|
@@ -50,7 +50,7 @@ export const footerBlock = defineBlock({
|
|
|
50
50
|
},
|
|
51
51
|
}),
|
|
52
52
|
defaultValue: [
|
|
53
|
-
{ platform: "GitHub", href: "https://github.com/
|
|
53
|
+
{ platform: "GitHub", href: "https://github.com/ianyimi/vex", icon: "Github" },
|
|
54
54
|
{ platform: "X", href: "https://x.com/vexcms", icon: "Twitter" },
|
|
55
55
|
],
|
|
56
56
|
}),
|
|
@@ -54,7 +54,7 @@ export const headerBlock = defineBlock({
|
|
|
54
54
|
},
|
|
55
55
|
}),
|
|
56
56
|
defaultValue: [
|
|
57
|
-
{ label: "GitHub", href: "https://github.com/
|
|
57
|
+
{ label: "GitHub", href: "https://github.com/ianyimi/vex", variant: "ghost" },
|
|
58
58
|
{ label: "Get Started", href: "/docs", variant: "default" },
|
|
59
59
|
],
|
|
60
60
|
}),
|
|
@@ -8,7 +8,7 @@ export const heroBlock = defineBlock({
|
|
|
8
8
|
fields: {
|
|
9
9
|
badgeText: text({
|
|
10
10
|
label: "Badge Text",
|
|
11
|
-
defaultValue: "Now in public
|
|
11
|
+
defaultValue: "Now in public alpha",
|
|
12
12
|
description: "Small text shown in the announcement badge",
|
|
13
13
|
}),
|
|
14
14
|
badgeLink: text({
|
|
@@ -42,7 +42,7 @@ export const heroBlock = defineBlock({
|
|
|
42
42
|
}),
|
|
43
43
|
secondaryCtaHref: text({
|
|
44
44
|
label: "Secondary CTA Link",
|
|
45
|
-
defaultValue: "https://github.com/
|
|
45
|
+
defaultValue: "https://github.com/ianyimi/vex",
|
|
46
46
|
}),
|
|
47
47
|
},
|
|
48
48
|
admin: {
|