create-vexcms 0.0.20 → 0.1.0-alpha.3

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 (157) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +98 -80
  3. package/dist/index.js +974 -639
  4. package/dist/index.mjs +30976 -0
  5. package/package.json +35 -15
  6. package/templates/base-nextjs/README.md +59 -203
  7. package/templates/base-nextjs/_env.example +7 -9
  8. package/templates/base-nextjs/_gitignore +41 -1
  9. package/templates/base-nextjs/convex/_generated/api.d.ts +6 -62
  10. package/templates/base-nextjs/convex/_generated/server.d.ts +19 -0
  11. package/templates/base-nextjs/convex/_generated/server.js +8 -0
  12. package/templates/base-nextjs/convex/auth/api.ts +49 -3
  13. package/templates/base-nextjs/convex/auth/db.ts +42 -300
  14. package/templates/base-nextjs/convex/auth/index.ts +11 -7
  15. package/templates/base-nextjs/convex/auth/options.ts +5 -29
  16. package/templates/base-nextjs/convex/auth/plugins/index.ts +15 -5
  17. package/templates/base-nextjs/convex/auth/sessions.ts +9 -15
  18. package/templates/base-nextjs/convex/http.ts +18 -3
  19. package/templates/base-nextjs/convex/schema.ts +104 -2
  20. package/templates/base-nextjs/convex/tsconfig.json +2 -2
  21. package/templates/base-nextjs/convex/vex/firstUser.ts +10 -84
  22. package/templates/base-nextjs/convex/vex/globals.ts +19 -82
  23. package/templates/base-nextjs/convex/vex/media.ts +19 -246
  24. package/templates/base-nextjs/convex/vex.schema.ts +138 -0
  25. package/templates/base-nextjs/convex/vex.ts +19 -0
  26. package/templates/base-nextjs/convex.json +6 -0
  27. package/templates/base-nextjs/eslint.config.mjs +1 -21
  28. package/templates/base-nextjs/next.config.ts +10 -0
  29. package/templates/base-nextjs/package.json +46 -48
  30. package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +17 -5
  31. package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/view.tsx +2 -4
  32. package/templates/base-nextjs/src/app/(frontend)/layout.tsx +12 -2
  33. package/templates/base-nextjs/src/app/(frontend)/page.tsx +2 -113
  34. package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +40 -0
  35. package/templates/base-nextjs/src/app/(vexcms)/admin/clientProviders.tsx +22 -0
  36. package/templates/base-nextjs/src/app/(vexcms)/admin/layout.tsx +19 -0
  37. package/templates/base-nextjs/src/app/globals.css +240 -104
  38. package/templates/base-nextjs/src/app/layout.tsx +23 -13
  39. package/templates/base-nextjs/src/app/unauthorized/page.tsx +24 -0
  40. package/templates/base-nextjs/src/auth/access.ts +46 -0
  41. package/templates/base-nextjs/src/auth/client.tsx +17 -5
  42. package/templates/base-nextjs/src/auth/hasPermission.ts +39 -0
  43. package/templates/base-nextjs/src/auth/serverUtils.ts +15 -25
  44. package/templates/base-nextjs/src/components/WelcomePage.tsx +116 -0
  45. package/templates/base-nextjs/src/components/component-example.tsx +56 -464
  46. package/templates/base-nextjs/src/components/providers/auth.tsx +13 -0
  47. package/templates/base-nextjs/src/components/providers/client.tsx +6 -1
  48. package/templates/base-nextjs/src/components/providers/convex.tsx +53 -21
  49. package/templates/base-nextjs/src/components/providers/server.tsx +10 -7
  50. package/templates/{marketing-site → base-nextjs}/src/components/ui/accordion.tsx +30 -32
  51. package/templates/base-nextjs/src/components/ui/badge.tsx +10 -13
  52. package/templates/base-nextjs/src/components/ui/input.tsx +35 -5
  53. package/templates/base-nextjs/src/context/AuthContext.tsx +29 -0
  54. package/templates/base-nextjs/src/db/constants/auth.ts +0 -1
  55. package/templates/base-nextjs/src/db/constants/index.ts +22 -35
  56. package/templates/base-nextjs/src/lib/utils.ts +0 -12
  57. package/templates/base-nextjs/src/proxy.ts +97 -12
  58. package/templates/base-nextjs/src/vex.config.ts +35 -0
  59. package/templates/base-nextjs/src/vex.types.ts +293 -0
  60. package/templates/base-nextjs/src/vexcms/collections/images.ts +15 -0
  61. package/templates/base-nextjs/src/vexcms/collections/index.ts +2 -1
  62. package/templates/base-nextjs/src/vexcms/collections/users.ts +7 -35
  63. package/templates/base-nextjs/tsconfig.json +3 -0
  64. package/templates/marketing-site/convex/_generated/api.d.ts +81 -0
  65. package/templates/marketing-site/convex/_generated/api.js +23 -0
  66. package/templates/marketing-site/convex/_generated/dataModel.d.ts +60 -0
  67. package/templates/marketing-site/convex/_generated/server.d.ts +162 -0
  68. package/templates/marketing-site/convex/_generated/server.js +101 -0
  69. package/templates/marketing-site/convex/footers.ts +11 -7
  70. package/templates/marketing-site/convex/headers.ts +15 -5
  71. package/templates/marketing-site/convex/pages.ts +20 -35
  72. package/templates/marketing-site/convex/schema.ts +119 -0
  73. package/templates/marketing-site/convex/seed.ts +582 -35
  74. package/templates/marketing-site/convex/siteSettings.ts +19 -5
  75. package/templates/marketing-site/convex/theme.ts +55 -61
  76. package/templates/marketing-site/convex/vex.schema.ts +355 -0
  77. package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +27 -160
  78. package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +6 -9
  79. package/templates/marketing-site/src/app/(frontend)/layout.tsx +4 -2
  80. package/templates/marketing-site/src/app/(frontend)/page.tsx +2 -5
  81. package/templates/marketing-site/src/components/SiteFooter.tsx +6 -3
  82. package/templates/marketing-site/src/components/SiteHeader.tsx +6 -3
  83. package/templates/marketing-site/src/components/ThemeLive.tsx +59 -0
  84. package/templates/marketing-site/src/components/ThemeStyle.tsx +45 -89
  85. package/templates/marketing-site/src/components/motion-primitives/animated-group.tsx +44 -120
  86. package/templates/marketing-site/src/components/motion-primitives/reveal.module.css +32 -0
  87. package/templates/marketing-site/src/components/motion-primitives/text-effect.tsx +41 -267
  88. package/templates/marketing-site/src/lib/metadata.ts +51 -72
  89. package/templates/marketing-site/src/vex.config.ts +35 -0
  90. package/templates/marketing-site/src/vex.types.ts +634 -0
  91. package/templates/marketing-site/src/vexcms/api.ts +23 -0
  92. package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -3
  93. package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +2 -2
  94. package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -4
  95. package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +2 -2
  96. package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +4 -8
  97. package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +2 -2
  98. package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +7 -11
  99. package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +1 -1
  100. package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +9 -9
  101. package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +4 -5
  102. package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +3 -4
  103. package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +12 -55
  104. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +4 -9
  105. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +3 -6
  106. package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +28 -68
  107. package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +13 -43
  108. package/templates/marketing-site/src/vexcms/blocks/config.ts +7 -22
  109. package/templates/marketing-site/src/vexcms/blocks/constants.ts +2 -3
  110. package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -8
  111. package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -1
  112. package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -1
  113. package/templates/marketing-site/src/vexcms/collections/index.ts +2 -1
  114. package/templates/marketing-site/src/vexcms/collections/pages.ts +12 -21
  115. package/templates/marketing-site/src/vexcms/collections/themeColors.ts +118 -0
  116. package/templates/marketing-site/src/vexcms/collections/themes.ts +29 -111
  117. package/templates/marketing-site/src/vexcms/globals/index.ts +1 -1
  118. package/templates/marketing-site/src/vexcms/globals/siteSettings.ts +43 -27
  119. package/templates/base-nextjs/convex/auth/adapter/index.ts +0 -230
  120. package/templates/base-nextjs/convex/auth/adapter/utils.ts +0 -547
  121. package/templates/base-nextjs/convex/auth/config.ts +0 -10
  122. package/templates/base-nextjs/convex/vex/auth.ts +0 -38
  123. package/templates/base-nextjs/convex/vex/collections.ts +0 -333
  124. package/templates/base-nextjs/convex/vex/helpers.ts +0 -33
  125. package/templates/base-nextjs/convex/vex/impersonation.ts +0 -51
  126. package/templates/base-nextjs/convex/vex/migrate.ts +0 -74
  127. package/templates/base-nextjs/convex/vex/model/collections.ts +0 -196
  128. package/templates/base-nextjs/convex/vex/model/media.ts +0 -87
  129. package/templates/base-nextjs/convex/vex/model/versions.ts +0 -254
  130. package/templates/base-nextjs/convex/vex/previewSnapshot.ts +0 -53
  131. package/templates/base-nextjs/convex/vex/versions.ts +0 -648
  132. package/templates/base-nextjs/src/app/admin/AdminLayoutWrapper.tsx +0 -33
  133. package/templates/base-nextjs/src/app/admin/AdminPageWrapper.tsx +0 -38
  134. package/templates/base-nextjs/src/app/admin/RichTextFieldWithMedia.tsx +0 -101
  135. package/templates/base-nextjs/src/app/admin/[[...path]]/page.tsx +0 -15
  136. package/templates/base-nextjs/src/app/admin/layout.tsx +0 -51
  137. package/templates/base-nextjs/src/app/favicon.ico +0 -0
  138. package/templates/base-nextjs/src/components/providers/theme.tsx +0 -11
  139. package/templates/base-nextjs/src/components/ui/theme-toggle.tsx +0 -67
  140. package/templates/base-nextjs/src/db/types.ts +0 -6
  141. package/templates/base-nextjs/src/vexcms/access.ts +0 -44
  142. package/templates/base-nextjs/src/vexcms/auth.ts +0 -4
  143. package/templates/base-nextjs/src/vexcms/collections/media.ts +0 -15
  144. package/templates/base-nextjs/vex.config.ts +0 -25
  145. package/templates/marketing-site/public/favicons/favicon.ico +0 -0
  146. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/PreviewPageContent.tsx +0 -55
  147. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +0 -27
  148. package/templates/marketing-site/src/app/(frontend)/preview/layout.tsx +0 -18
  149. package/templates/marketing-site/src/app/admin/AdminLayoutWrapper.tsx +0 -71
  150. package/templates/marketing-site/src/components/ThemeInjector.tsx +0 -128
  151. package/templates/marketing-site/src/components/admin/IconPickerField.tsx +0 -134
  152. package/templates/marketing-site/src/components/admin/ThemeImport.tsx +0 -174
  153. package/templates/marketing-site/src/components/admin/ThemeImportField.tsx +0 -33
  154. package/templates/marketing-site/src/db/constants/index.ts +0 -52
  155. package/templates/marketing-site/src/lib/colorConvert.ts +0 -171
  156. package/templates/marketing-site/src/vexcms/access.ts +0 -36
  157. package/templates/marketing-site/vex.config.ts +0 -32
@@ -1,138 +1,62 @@
1
- "use client"
1
+ import React, { type ComponentPropsWithRef, type CSSProperties } from "react"
2
2
 
3
- import { motion, type Variants } from "motion/react"
4
- import React, { type ComponentPropsWithRef } from "react"
3
+ import { cn } from "~/lib/utils"
5
4
 
6
- export type PresetType =
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
- asChild?: React.ElementType
21
- className?: string
22
- preset?: PresetType
23
- variants?: {
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
- const defaultContainerVariants: Variants = {
30
- visible: {
31
- transition: {
32
- staggerChildren: 0.1,
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
- preset,
108
- variants,
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
- <MotionComponent
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
- <MotionChild key={index} variants={itemVariants}>
32
+ <div
33
+ className={styles.item}
34
+ style={{ "--reveal-delay": `${delay + index * stagger}s` } as CSSProperties}
35
+ >
131
36
  {child}
132
- </MotionChild>
37
+ </div>
133
38
  ))}
134
- </MotionComponent>
39
+ </Tag>
135
40
  )
136
41
  }
137
42
 
138
- export { AnimatedGroup }
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
- "use client"
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
- export type PerType = "char" | "line" | "word"
16
- export type PresetType = "blur" | "fade" | "fade-in-blur" | "scale" | "slide"
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
- containerTransition?: Transition
13
+ /** Seconds before the first segment starts animating. */
23
14
  delay?: number
24
- onAnimationComplete?: () => void
25
- onAnimationStart?: () => void
15
+ /** Split granularity for the stagger. Lines split on `\n`. */
26
16
  per?: PerType
27
- preset?: PresetType
28
- segmentTransition?: Transition
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 defaultStaggerTimes: Record<PerType, number> = {
41
- char: 0.03,
42
- line: 0.1,
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
- preset = "fade",
217
- segmentTransition,
218
- segmentWrapperClassName,
219
- speedReveal = 1,
220
- speedSegment = 1,
221
- style,
222
- trigger = true,
223
- variants,
38
+ stagger,
224
39
  }: TextEffectProps) {
225
- const segments = splitText(children, per)
226
- const MotionTag = motion[as as keyof typeof motion] as typeof motion.div
227
-
228
- const baseVariants = preset
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
- <AnimatePresence mode="popLayout">
267
- {trigger && (
268
- <MotionTag
269
- animate="visible"
270
- className={className}
271
- exit="exit"
272
- initial="hidden"
273
- onAnimationComplete={onAnimationComplete}
274
- onAnimationStart={onAnimationStart}
275
- style={style}
276
- variants={computedVariants.container}
277
- >
278
- {per !== "line" ? <span className="sr-only">{children}</span> : null}
279
- {segments.map((segment, index) => (
280
- <AnimationComponent
281
- key={`${per}-${index}-${segment}`}
282
- per={per}
283
- segment={segment}
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
  }
@@ -1,99 +1,78 @@
1
1
  import type { Metadata } from "next"
2
2
 
3
- import { buildSiteMetadata } from "@vexcms/core"
4
3
  import { fetchQuery } from "convex/nextjs"
5
4
 
6
5
  import { api } from "@convex/_generated/api"
7
6
  import { anyApi } from "convex/server"
8
7
 
8
+ const TITLE_SUFFIX = " | Vex CMS"
9
+
10
+ /**
11
+ * Resolves an `upload()` field's stored media id to a public URL via the
12
+ * Convex file storage adapter. Returns `undefined` when the media document
13
+ * no longer exists or the deployment is unreachable.
14
+ */
15
+ async function resolveMediaUrl(mediaId: string): Promise<string | undefined> {
16
+ try {
17
+ const result = (await fetchQuery(anyApi.vex.media.getUrl, {
18
+ adapter: "convex",
19
+ mediaId,
20
+ })) as { url?: string; error?: string }
21
+ return result.url
22
+ } catch {
23
+ return undefined
24
+ }
25
+ }
26
+
9
27
  /**
10
28
  * Generate Next.js Metadata for a page.
11
- * Fetches site settings and optionally a page document,
12
- * merges with buildSiteMetadata, and returns Next.js Metadata format.
29
+ *
30
+ * Fetches site settings and, when a slug is given, the matching page
31
+ * document, then merges them — page-level `metaTitle`/`metaDescription`/
32
+ * `ogImage` win over the site's defaults from `siteSettings`.
13
33
  *
14
34
  * @param props.slug - Optional page slug to fetch per-page SEO overrides
15
35
  */
16
- export async function generatePageMetadata(props: {
17
- slug?: string
18
- }): Promise<Metadata> {
36
+ export async function generatePageMetadata(props: { slug?: string }): Promise<Metadata> {
19
37
  try {
20
- // Fetch site settings
21
- const settings = (await fetchQuery(api.siteSettings.get)) as Record<
22
- string,
23
- unknown
24
- > | null
25
-
38
+ const settings = (await fetchQuery(api.siteSettings.get)) as Record<string, unknown> | null
26
39
  if (!settings) {
27
40
  return { title: "Untitled" }
28
41
  }
29
42
 
30
- // Resolve the ogImage URL from site settings (upload ID → URL)
31
- let siteOgImageUrl: string | undefined
32
- const ogImageId = settings.ogImage as string | undefined
33
- if (ogImageId) {
34
- try {
35
- const mediaDoc = (await fetchQuery(
36
- anyApi.vex.collections.getDocument,
37
- {
38
- collectionSlug: "media",
39
- documentId: ogImageId,
40
- }
41
- )) as Record<string, unknown> | null
42
- siteOgImageUrl = (mediaDoc?.url as string) || undefined
43
- } catch {
44
- // Media document not found — skip ogImage
45
- }
46
- }
47
-
48
- // Fetch page data for per-page overrides
49
- let pageData: Record<string, unknown> | null = null
43
+ let pageData: Record<string, unknown> | undefined
50
44
  if (props.slug) {
51
- pageData = await fetchQuery(api.pages.getBySlug, {
52
- slug: props.slug,
53
- _vexDrafts: false,
54
- })
45
+ const pages = (await fetchQuery(api.pages.getBySlug, { slug: props.slug })) as
46
+ | Record<string, unknown>[]
47
+ | undefined
48
+ pageData = pages?.[0]
55
49
  }
56
50
 
57
- // Build framework-agnostic metadata
58
- const meta = buildSiteMetadata({
59
- site: {
60
- name: settings.name as string | undefined,
61
- metaTitle: settings.metaTitle as string | undefined,
62
- metaDescription: settings.metaDescription as string | undefined,
63
- description: settings.description as string | undefined,
64
- ogImage: siteOgImageUrl,
65
- twitterHandle: settings.twitterHandle as string | undefined,
66
- },
67
- page: pageData
68
- ? {
69
- title: pageData.title as string | undefined,
70
- metaTitle: pageData.metaTitle as string | undefined,
71
- metaDescription: pageData.metaDescription as string | undefined,
72
- ogImage: pageData.ogImage as string | undefined,
73
- }
74
- : undefined,
75
- titleSuffix: " | Vex CMS",
76
- })
51
+ const pageTitle = (pageData?.metaTitle as string | undefined) ?? (pageData?.title as string | undefined)
52
+ const siteName = settings.name as string | undefined
53
+ const title = (pageTitle ?? (settings.metaTitle as string | undefined) ?? siteName ?? "Untitled") + TITLE_SUFFIX
54
+ const description =
55
+ (pageData?.metaDescription as string | undefined) ??
56
+ (settings.metaDescription as string | undefined) ??
57
+ (settings.description as string | undefined)
77
58
 
78
- // Map to Next.js Metadata
79
- const metadata: Metadata = {
80
- title: meta.title,
81
- description: meta.description,
82
- }
59
+ // `upload()` fields always store an array of media ids — the first entry
60
+ // is the selection. Page-level ogImage wins over the site default.
61
+ const pageOgImageId = (pageData?.ogImage as string[] | undefined)?.[0]
62
+ const siteOgImageId = (settings.ogImage as string[] | undefined)?.[0]
63
+ const ogImageId = pageOgImageId ?? siteOgImageId
64
+ const ogImageUrl = ogImageId ? await resolveMediaUrl(ogImageId) : undefined
65
+
66
+ const twitterHandle = settings.twitterHandle as string | undefined
67
+
68
+ const metadata: Metadata = { title, description }
83
69
 
84
- if (meta.ogImage) {
85
- metadata.openGraph = {
86
- title: meta.title,
87
- description: meta.description,
88
- images: [{ url: meta.ogImage }],
89
- }
70
+ if (ogImageUrl) {
71
+ metadata.openGraph = { title, description, images: [{ url: ogImageUrl }] }
90
72
  }
91
73
 
92
- if (meta.twitterHandle) {
93
- metadata.twitter = {
94
- card: "summary_large_image",
95
- site: meta.twitterHandle,
96
- }
74
+ if (twitterHandle) {
75
+ metadata.twitter = { card: "summary_large_image", site: twitterHandle }
97
76
  }
98
77
 
99
78
  return metadata