create-vexcms 0.0.19 → 0.1.0-alpha.2

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 (154) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +93 -83
  3. package/dist/index.js +246 -81
  4. package/dist/index.mjs +30976 -0
  5. package/package.json +35 -15
  6. package/templates/base-nextjs/README.md +53 -205
  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/vex/firstUser.ts +10 -84
  21. package/templates/base-nextjs/convex/vex/globals.ts +19 -82
  22. package/templates/base-nextjs/convex/vex/media.ts +19 -246
  23. package/templates/base-nextjs/convex/vex.schema.ts +138 -0
  24. package/templates/base-nextjs/convex/vex.ts +19 -0
  25. package/templates/base-nextjs/convex.json +6 -0
  26. package/templates/base-nextjs/eslint.config.mjs +1 -21
  27. package/templates/base-nextjs/next.config.ts +10 -0
  28. package/templates/base-nextjs/package.json +47 -48
  29. package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +17 -5
  30. package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/view.tsx +2 -4
  31. package/templates/base-nextjs/src/app/(frontend)/layout.tsx +12 -2
  32. package/templates/base-nextjs/src/app/(frontend)/page.tsx +2 -113
  33. package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +40 -0
  34. package/templates/base-nextjs/src/app/(vexcms)/admin/clientProviders.tsx +22 -0
  35. package/templates/base-nextjs/src/app/(vexcms)/admin/layout.tsx +19 -0
  36. package/templates/base-nextjs/src/app/globals.css +240 -104
  37. package/templates/base-nextjs/src/app/layout.tsx +23 -13
  38. package/templates/base-nextjs/src/app/unauthorized/page.tsx +24 -0
  39. package/templates/base-nextjs/src/auth/access.ts +46 -0
  40. package/templates/base-nextjs/src/auth/client.tsx +17 -5
  41. package/templates/base-nextjs/src/auth/hasPermission.ts +39 -0
  42. package/templates/base-nextjs/src/auth/serverUtils.ts +15 -22
  43. package/templates/base-nextjs/src/components/WelcomePage.tsx +116 -0
  44. package/templates/base-nextjs/src/components/component-example.tsx +56 -464
  45. package/templates/base-nextjs/src/components/providers/auth.tsx +13 -0
  46. package/templates/base-nextjs/src/components/providers/client.tsx +6 -1
  47. package/templates/base-nextjs/src/components/providers/convex.tsx +53 -21
  48. package/templates/base-nextjs/src/components/providers/server.tsx +10 -7
  49. package/templates/{marketing-site → base-nextjs}/src/components/ui/accordion.tsx +30 -32
  50. package/templates/base-nextjs/src/components/ui/badge.tsx +10 -13
  51. package/templates/base-nextjs/src/components/ui/input.tsx +35 -5
  52. package/templates/base-nextjs/src/context/AuthContext.tsx +29 -0
  53. package/templates/base-nextjs/src/db/constants/auth.ts +0 -1
  54. package/templates/base-nextjs/src/db/constants/index.ts +22 -35
  55. package/templates/base-nextjs/src/lib/utils.ts +0 -12
  56. package/templates/base-nextjs/src/proxy.ts +98 -11
  57. package/templates/base-nextjs/src/vex.config.ts +35 -0
  58. package/templates/base-nextjs/src/vex.types.ts +293 -0
  59. package/templates/base-nextjs/src/vexcms/collections/images.ts +15 -0
  60. package/templates/base-nextjs/src/vexcms/collections/index.ts +2 -1
  61. package/templates/base-nextjs/src/vexcms/collections/users.ts +7 -35
  62. package/templates/base-nextjs/tsconfig.json +3 -0
  63. package/templates/marketing-site/convex/_generated/api.d.ts +81 -0
  64. package/templates/marketing-site/convex/_generated/api.js +23 -0
  65. package/templates/marketing-site/convex/_generated/dataModel.d.ts +60 -0
  66. package/templates/marketing-site/convex/_generated/server.d.ts +162 -0
  67. package/templates/marketing-site/convex/_generated/server.js +101 -0
  68. package/templates/marketing-site/convex/footers.ts +11 -7
  69. package/templates/marketing-site/convex/headers.ts +15 -5
  70. package/templates/marketing-site/convex/pages.ts +20 -35
  71. package/templates/marketing-site/convex/schema.ts +119 -0
  72. package/templates/marketing-site/convex/seed.ts +582 -35
  73. package/templates/marketing-site/convex/siteSettings.ts +19 -5
  74. package/templates/marketing-site/convex/theme.ts +55 -61
  75. package/templates/marketing-site/convex/vex.schema.ts +355 -0
  76. package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +27 -160
  77. package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +6 -9
  78. package/templates/marketing-site/src/app/(frontend)/layout.tsx +4 -2
  79. package/templates/marketing-site/src/app/(frontend)/page.tsx +2 -5
  80. package/templates/marketing-site/src/components/SiteFooter.tsx +6 -3
  81. package/templates/marketing-site/src/components/SiteHeader.tsx +6 -3
  82. package/templates/marketing-site/src/components/ThemeLive.tsx +59 -0
  83. package/templates/marketing-site/src/components/ThemeStyle.tsx +45 -89
  84. package/templates/marketing-site/src/components/motion-primitives/animated-group.tsx +2 -2
  85. package/templates/marketing-site/src/lib/metadata.ts +51 -72
  86. package/templates/marketing-site/src/vex.config.ts +35 -0
  87. package/templates/marketing-site/src/vex.types.ts +634 -0
  88. package/templates/marketing-site/src/vexcms/api.ts +23 -0
  89. package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -3
  90. package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +2 -2
  91. package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -4
  92. package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +2 -2
  93. package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +4 -8
  94. package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +2 -2
  95. package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +7 -11
  96. package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +1 -1
  97. package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +9 -9
  98. package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +4 -5
  99. package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +3 -4
  100. package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +10 -19
  101. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +4 -9
  102. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +3 -6
  103. package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +28 -68
  104. package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +13 -43
  105. package/templates/marketing-site/src/vexcms/blocks/config.ts +7 -22
  106. package/templates/marketing-site/src/vexcms/blocks/constants.ts +2 -3
  107. package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -8
  108. package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -1
  109. package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -1
  110. package/templates/marketing-site/src/vexcms/collections/index.ts +2 -1
  111. package/templates/marketing-site/src/vexcms/collections/pages.ts +12 -21
  112. package/templates/marketing-site/src/vexcms/collections/themeColors.ts +118 -0
  113. package/templates/marketing-site/src/vexcms/collections/themes.ts +29 -111
  114. package/templates/marketing-site/src/vexcms/globals/index.ts +1 -1
  115. package/templates/marketing-site/src/vexcms/globals/siteSettings.ts +43 -27
  116. package/templates/base-nextjs/convex/auth/adapter/index.ts +0 -230
  117. package/templates/base-nextjs/convex/auth/adapter/utils.ts +0 -547
  118. package/templates/base-nextjs/convex/auth/config.ts +0 -10
  119. package/templates/base-nextjs/convex/vex/auth.ts +0 -38
  120. package/templates/base-nextjs/convex/vex/collections.ts +0 -321
  121. package/templates/base-nextjs/convex/vex/helpers.ts +0 -33
  122. package/templates/base-nextjs/convex/vex/impersonation.ts +0 -51
  123. package/templates/base-nextjs/convex/vex/migrate.ts +0 -74
  124. package/templates/base-nextjs/convex/vex/model/collections.ts +0 -196
  125. package/templates/base-nextjs/convex/vex/model/media.ts +0 -87
  126. package/templates/base-nextjs/convex/vex/model/versions.ts +0 -254
  127. package/templates/base-nextjs/convex/vex/previewSnapshot.ts +0 -53
  128. package/templates/base-nextjs/convex/vex/versions.ts +0 -648
  129. package/templates/base-nextjs/src/app/admin/AdminLayoutWrapper.tsx +0 -33
  130. package/templates/base-nextjs/src/app/admin/AdminPageWrapper.tsx +0 -38
  131. package/templates/base-nextjs/src/app/admin/RichTextFieldWithMedia.tsx +0 -101
  132. package/templates/base-nextjs/src/app/admin/[[...path]]/page.tsx +0 -15
  133. package/templates/base-nextjs/src/app/admin/layout.tsx +0 -51
  134. package/templates/base-nextjs/src/app/favicon.ico +0 -0
  135. package/templates/base-nextjs/src/components/providers/theme.tsx +0 -11
  136. package/templates/base-nextjs/src/components/ui/theme-toggle.tsx +0 -67
  137. package/templates/base-nextjs/src/db/types.ts +0 -6
  138. package/templates/base-nextjs/src/vexcms/access.ts +0 -44
  139. package/templates/base-nextjs/src/vexcms/auth.ts +0 -4
  140. package/templates/base-nextjs/src/vexcms/collections/media.ts +0 -15
  141. package/templates/base-nextjs/vex.config.ts +0 -25
  142. package/templates/marketing-site/public/favicons/favicon.ico +0 -0
  143. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/PreviewPageContent.tsx +0 -55
  144. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +0 -27
  145. package/templates/marketing-site/src/app/(frontend)/preview/layout.tsx +0 -18
  146. package/templates/marketing-site/src/app/admin/AdminLayoutWrapper.tsx +0 -71
  147. package/templates/marketing-site/src/components/ThemeInjector.tsx +0 -128
  148. package/templates/marketing-site/src/components/admin/IconPickerField.tsx +0 -134
  149. package/templates/marketing-site/src/components/admin/ThemeImport.tsx +0 -174
  150. package/templates/marketing-site/src/components/admin/ThemeImportField.tsx +0 -33
  151. package/templates/marketing-site/src/db/constants/index.ts +0 -52
  152. package/templates/marketing-site/src/lib/colorConvert.ts +0 -171
  153. package/templates/marketing-site/src/vexcms/access.ts +0 -36
  154. package/templates/marketing-site/vex.config.ts +0 -32
@@ -1,10 +1,9 @@
1
1
  "use client"
2
2
 
3
- import type { BlockComponentProps } from "@vexcms/ui"
3
+ import type { BlockComponentProps } from "@vexcms/react"
4
4
 
5
5
  import { Check, Clock, Compass } from "lucide-react"
6
-
7
- import { cn } from "~/lib/utils"
6
+ import { cn } from "@vexcms/react"
8
7
 
9
8
  export { roadmapBlock } from "./config"
10
9
 
@@ -12,37 +11,26 @@ const statusConfig = {
12
11
  shipped: {
13
12
  label: "Shipped",
14
13
  icon: Check,
15
- badgeClass:
16
- "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20",
14
+ badgeClass: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20",
17
15
  },
18
16
  "coming-soon": {
19
17
  label: "Coming Soon",
20
18
  icon: Clock,
21
- badgeClass:
22
- "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20",
19
+ badgeClass: "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20",
23
20
  },
24
21
  planned: {
25
22
  label: "Planned",
26
23
  icon: Compass,
27
- badgeClass:
28
- "bg-zinc-500/10 text-zinc-600 dark:text-zinc-400 border-zinc-500/20",
24
+ badgeClass: "bg-zinc-500/10 text-zinc-600 dark:text-zinc-400 border-zinc-500/20",
29
25
  },
30
26
  } as const
31
27
 
32
- type RoadmapItem = {
33
- feature: string
34
- description?: string
35
- status: "shipped" | "coming-soon" | "planned"
36
- }
37
-
28
+ type RoadmapItem = { feature: string; description?: string; status: "shipped" | "coming-soon" | "planned" }
38
29
  type StatusKey = keyof typeof statusConfig
39
30
 
40
31
  const statusOrder: StatusKey[] = ["shipped", "coming-soon", "planned"]
41
32
 
42
- export default function RoadmapBlock({
43
- block,
44
- blockStyles,
45
- }: BlockComponentProps) {
33
+ export default function RoadmapBlock({ block }: BlockComponentProps) {
46
34
  const { heading, subheading, items } = block as unknown as {
47
35
  heading: string
48
36
  subheading?: string
@@ -53,45 +41,31 @@ export default function RoadmapBlock({
53
41
  .map((status) => ({
54
42
  status,
55
43
  config: statusConfig[status],
56
- items: (items ?? []).filter(
57
- (item) => item.status === status && item.status in statusConfig
58
- ),
44
+ items: (items ?? []).filter((item) => item.status === status && item.status in statusConfig),
59
45
  }))
60
46
  .filter((group) => group.items.length > 0)
61
47
 
62
48
  return (
63
- <section className={cn("py-16 md:py-32", blockStyles)}>
49
+ <section className={cn("py-16 md:py-32")}>
64
50
  <div className="mx-auto max-w-5xl px-6">
65
51
  <div className="text-center">
66
- <h2 className="text-4xl font-semibold text-balance lg:text-5xl">
67
- {heading}
68
- </h2>
69
- {subheading && (
70
- <p className="text-muted-foreground mt-4 text-balance">
71
- {subheading}
72
- </p>
73
- )}
52
+ <h2 className="text-4xl font-semibold text-balance lg:text-5xl">{heading}</h2>
53
+ {subheading && <p className="text-muted-foreground mt-4 text-balance">{subheading}</p>}
74
54
  </div>
75
55
 
76
56
  {grouped.map((group, groupIndex) => {
77
57
  const StatusIcon = group.config.icon
78
-
79
58
  return (
80
59
  <div key={group.status} className={groupIndex === 0 ? "mt-16" : "mt-12"}>
81
60
  <div className="flex items-center gap-2">
82
61
  <StatusIcon className="size-5" />
83
62
  <h3 className="text-lg font-semibold">{group.config.label}</h3>
84
63
  </div>
85
-
86
64
  <div className="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
87
65
  {group.items.map((item, itemIndex) => {
88
66
  const BadgeIcon = group.config.icon
89
-
90
67
  return (
91
- <div
92
- key={itemIndex}
93
- className="bg-card rounded-xl border p-5"
94
- >
68
+ <div key={itemIndex} className="bg-card rounded-xl border p-5">
95
69
  <div className="flex items-center justify-between gap-4">
96
70
  <span className="font-medium">{item.feature}</span>
97
71
  <span
@@ -104,11 +78,7 @@ export default function RoadmapBlock({
104
78
  {group.config.label}
105
79
  </span>
106
80
  </div>
107
- {item.description && (
108
- <p className="text-muted-foreground mt-2 text-sm">
109
- {item.description}
110
- </p>
111
- )}
81
+ {item.description && <p className="text-muted-foreground mt-2 text-sm">{item.description}</p>}
112
82
  </div>
113
83
  )
114
84
  })}
@@ -1,40 +1,25 @@
1
- // Block configs only — no React/motion dependencies
2
- // Safe to import from collections and server-side code
1
+ // Block configs only — no React/motion dependencies. Safe to import from
2
+ // collections, vex.config.ts, and convex/seed.ts.
3
3
  import { heroBlock } from "./Hero/config"
4
4
  import { featuresBlock } from "./Features/config"
5
+ import { howItWorksBlock } from "./HowItWorks/config"
6
+ import { roadmapBlock } from "./Roadmap/config"
5
7
  import { ctaBlock } from "./CTA/config"
6
8
  import { faqBlock } from "./FAQ/config"
7
9
  import { headerBlock } from "./Header/config"
8
10
  import { footerBlock } from "./Footer/config"
9
- import { howItWorksBlock } from "./HowItWorks/config"
10
- import { roadmapBlock } from "./Roadmap/config"
11
11
 
12
- /** Page content block definitions */
13
- export const pageBlocks = [
14
- heroBlock,
15
- featuresBlock,
16
- howItWorksBlock,
17
- roadmapBlock,
18
- ctaBlock,
19
- faqBlock,
20
- ]
21
-
22
- /** Header block definitions */
12
+ export const pageBlocks = [heroBlock, featuresBlock, howItWorksBlock, roadmapBlock, ctaBlock, faqBlock]
23
13
  export const headerBlocks = [headerBlock]
24
-
25
- /** Footer block definitions */
26
14
  export const footerBlocks = [footerBlock]
27
15
 
28
- /** All block definitions combined */
29
- export const allBlocks = [...pageBlocks, ...headerBlocks, ...footerBlocks]
30
-
31
16
  export {
32
17
  heroBlock,
33
18
  featuresBlock,
19
+ howItWorksBlock,
20
+ roadmapBlock,
34
21
  ctaBlock,
35
22
  faqBlock,
36
23
  headerBlock,
37
24
  footerBlock,
38
- howItWorksBlock,
39
- roadmapBlock,
40
25
  }
@@ -1,9 +1,8 @@
1
- // Block slugs
2
1
  export const BLOCK_SLUG_HERO = "hero" as const
3
2
  export const BLOCK_SLUG_FEATURES = "features" as const
3
+ export const BLOCK_SLUG_HOW_IT_WORKS = "how_it_works" as const
4
+ export const BLOCK_SLUG_ROADMAP = "roadmap" as const
4
5
  export const BLOCK_SLUG_CTA = "cta" as const
5
6
  export const BLOCK_SLUG_FAQ = "faq" as const
6
7
  export const BLOCK_SLUG_HEADER = "header" as const
7
8
  export const BLOCK_SLUG_FOOTER = "footer" as const
8
- export const BLOCK_SLUG_HOW_IT_WORKS = "how_it_works" as const
9
- export const BLOCK_SLUG_ROADMAP = "roadmap" as const
@@ -1,4 +1,4 @@
1
- import type { BlockComponentProps } from "@vexcms/ui"
1
+ import type { BlockComponentProps } from "@vexcms/react"
2
2
  import type { ComponentType } from "react"
3
3
 
4
4
  import {
@@ -20,11 +20,13 @@ import HeroBlock from "./Hero"
20
20
  import HowItWorksBlock from "./HowItWorks"
21
21
  import RoadmapBlock from "./Roadmap"
22
22
 
23
- /** Block component map for use with RenderBlocks */
24
- export const blockComponents: Record<
25
- string,
26
- ComponentType<BlockComponentProps>
27
- > = {
23
+ /**
24
+ * Block component map for `RenderBlocks` (Contract 1). Loosely typed as
25
+ * `Record<string, …>` rather than a `TBlock`-keyed `BlockComponents<…>` —
26
+ * the real per-slug `PageBlock` union only exists after `vex generate` runs
27
+ * against this config, so this file can't reference it.
28
+ */
29
+ export const blockComponents: Record<string, ComponentType<BlockComponentProps>> = {
28
30
  [BLOCK_SLUG_CTA]: CTABlock,
29
31
  [BLOCK_SLUG_FAQ]: FAQBlock,
30
32
  [BLOCK_SLUG_FEATURES]: FeaturesBlock,
@@ -35,5 +37,4 @@ export const blockComponents: Record<
35
37
  [BLOCK_SLUG_ROADMAP]: RoadmapBlock,
36
38
  }
37
39
 
38
- // Re-export configs for convenience in client code
39
- export { allBlocks, footerBlocks, headerBlocks, pageBlocks } from "./config"
40
+ export { footerBlocks, headerBlocks, pageBlocks } from "./config"
@@ -6,13 +6,13 @@ import { footerBlocks } from "~/vexcms/blocks/config"
6
6
  export const footers = defineCollection({
7
7
  slug: TABLE_SLUG_FOOTERS,
8
8
  admin: {
9
- group: "Site Builder",
10
9
  useAsTitle: "name",
11
10
  },
12
11
  fields: {
13
12
  name: text({
14
13
  label: "Name",
15
14
  required: true,
15
+ index: "by_name",
16
16
  }),
17
17
  content: blocks({
18
18
  blocks: footerBlocks,
@@ -6,13 +6,13 @@ import { headerBlocks } from "~/vexcms/blocks/config"
6
6
  export const headers = defineCollection({
7
7
  slug: TABLE_SLUG_HEADERS,
8
8
  admin: {
9
- group: "Site Builder",
10
9
  useAsTitle: "name",
11
10
  },
12
11
  fields: {
13
12
  name: text({
14
13
  label: "Name",
15
14
  required: true,
15
+ index: "by_name",
16
16
  }),
17
17
  content: blocks({
18
18
  blocks: headerBlocks,
@@ -1,6 +1,7 @@
1
1
  export * from "./footers"
2
2
  export * from "./headers"
3
- export * from "./media"
3
+ export * from "./images"
4
4
  export * from "./pages"
5
+ export * from "./themeColors"
5
6
  export * from "./themes"
6
7
  export * from "./users"
@@ -1,16 +1,12 @@
1
- import { blocks, defineCollection, imageUrl, text } from "@vexcms/core"
1
+ import { blocks, defineCollection, text, upload } from "@vexcms/core"
2
2
 
3
- import { TABLE_SLUG_PAGES } from "~/db/constants"
3
+ import { TABLE_SLUG_IMAGES, TABLE_SLUG_PAGES } from "~/db/constants"
4
4
  import { pageBlocks } from "~/vexcms/blocks/config"
5
5
 
6
6
  export const pages = defineCollection({
7
7
  slug: TABLE_SLUG_PAGES,
8
8
  admin: {
9
- defaultColumns: ["title", "slug", "vex_status", "_id"],
10
- group: "Site Builder",
11
- livePreview: {
12
- url: (doc) => `/preview/${doc.slug ?? doc._id}`,
13
- },
9
+ table: { defaultColumns: ["title", "slug", "_id"] },
14
10
  useAsTitle: "title",
15
11
  },
16
12
  fields: {
@@ -19,48 +15,43 @@ export const pages = defineCollection({
19
15
  required: true,
20
16
  }),
21
17
  slug: text({
22
- admin: {
23
- description: "URL-friendly page path",
24
- },
18
+ description: "URL-friendly page path",
25
19
  index: "by_slug",
26
20
  label: "Slug",
27
21
  required: true,
28
22
  }),
29
- content: blocks({
23
+ blocks: blocks({
30
24
  blocks: pageBlocks,
25
+ interfaceName: "PageBlock",
31
26
  label: "Content",
32
27
  labels: {
33
28
  singular: "Block",
34
29
  plural: "Blocks",
35
30
  },
31
+ min: 1,
36
32
  }),
37
33
  metaTitle: text({
38
34
  label: "Meta Title",
35
+ description: "Custom <title> tag. Falls back to page title if empty.",
39
36
  admin: {
40
- description: "Custom <title> tag. Falls back to page title if empty.",
41
37
  position: "sidebar",
42
38
  },
43
39
  }),
44
40
  metaDescription: text({
45
41
  label: "Meta Description",
42
+ description: "Custom meta description for search results.",
46
43
  admin: {
47
- description: "Custom meta description for search results.",
48
44
  position: "sidebar",
49
45
  },
50
46
  }),
51
- ogImage: imageUrl({
47
+ ogImage: upload({
48
+ to: TABLE_SLUG_IMAGES,
52
49
  label: "OG Image",
53
- admin: {
54
- description: "Custom Open Graph image URL for social sharing.",
55
- position: "sidebar",
56
- },
50
+ admin: { position: "sidebar" },
57
51
  }),
58
52
  },
59
53
  labels: {
60
54
  plural: "Pages",
61
55
  singular: "Page",
62
56
  },
63
- versions: {
64
- drafts: true,
65
- },
66
57
  })
@@ -0,0 +1,118 @@
1
+ import {
2
+ color,
3
+ THEME_COLOR_TOKENS,
4
+ type ColorField,
5
+ type ThemeColorTokenKey,
6
+ } from "@vexcms/core";
7
+
8
+ /**
9
+ * Stark × Ember — this site's house palette, lifted verbatim from
10
+ * `globals.css`. A freshly created theme starts from these values, so "new
11
+ * theme" means "fork the current look", not "32 empty pickers".
12
+ */
13
+ const EMBER: Record<"dark" | "light", Record<ThemeColorTokenKey, string>> = {
14
+ light: {
15
+ background: "oklch(96.1% 0 0)",
16
+ foreground: "oklch(13.7% 0 0)",
17
+ card: "oklch(100% 0 0)",
18
+ cardForeground: "oklch(13.7% 0 0)",
19
+ popover: "oklch(100% 0 0)",
20
+ popoverForeground: "oklch(13.7% 0 0)",
21
+ primary: "oklch(60.5% 0.175 42)",
22
+ primaryForeground: "oklch(100% 0 0)",
23
+ secondary: "oklch(98% 0 0)",
24
+ secondaryForeground: "oklch(13.7% 0 0)",
25
+ muted: "oklch(98% 0 0)",
26
+ mutedForeground: "oklch(50.5% 0 0)",
27
+ accent: "oklch(96% 0.025 42)",
28
+ accentForeground: "oklch(52% 0.180 40)",
29
+ destructive: "oklch(57.7% 0.198 27)",
30
+ destructiveForeground: "oklch(98% 0 0)",
31
+ border: "oklch(85% 0 0)",
32
+ input: "oklch(54.6% 0 0)",
33
+ ring: "oklch(60.5% 0.175 42)",
34
+ chart1: "oklch(60.5% 0.175 42)",
35
+ chart2: "oklch(45% 0 0)",
36
+ chart3: "oklch(72% 0.100 60)",
37
+ chart4: "oklch(60% 0.040 30)",
38
+ chart5: "oklch(78% 0 0)",
39
+ sidebar: "oklch(98% 0 0)",
40
+ sidebarForeground: "oklch(13.7% 0 0)",
41
+ sidebarPrimary: "oklch(60.5% 0.175 42)",
42
+ sidebarPrimaryForeground: "oklch(100% 0 0)",
43
+ sidebarAccent: "oklch(96.1% 0 0)",
44
+ sidebarAccentForeground: "oklch(13.7% 0 0)",
45
+ sidebarBorder: "oklch(85% 0 0)",
46
+ sidebarRing: "oklch(60.5% 0.175 42)",
47
+ },
48
+ dark: {
49
+ background: "oklch(13.7% 0 0)",
50
+ foreground: "oklch(95% 0 0)",
51
+ card: "oklch(17.4% 0 0)",
52
+ cardForeground: "oklch(95% 0 0)",
53
+ popover: "oklch(17.4% 0 0)",
54
+ popoverForeground: "oklch(95% 0 0)",
55
+ primary: "oklch(72% 0.175 50)",
56
+ primaryForeground: "oklch(13.7% 0 0)",
57
+ secondary: "oklch(20% 0 0)",
58
+ secondaryForeground: "oklch(95% 0 0)",
59
+ muted: "oklch(20% 0 0)",
60
+ mutedForeground: "oklch(70% 0 0)",
61
+ accent: "oklch(72% 0.175 50 / 0.12)",
62
+ accentForeground: "oklch(72% 0.175 50)",
63
+ destructive: "oklch(63% 0.210 27)",
64
+ destructiveForeground: "oklch(95% 0 0)",
65
+ border: "oklch(25% 0 0)",
66
+ input: "oklch(40% 0 0)",
67
+ ring: "oklch(72% 0.175 50)",
68
+ chart1: "oklch(72% 0.175 50)",
69
+ chart2: "oklch(78% 0 0)",
70
+ chart3: "oklch(78% 0.120 65)",
71
+ chart4: "oklch(60% 0.060 30)",
72
+ chart5: "oklch(45% 0 0)",
73
+ sidebar: "oklch(7% 0 0)",
74
+ sidebarForeground: "oklch(95% 0 0)",
75
+ sidebarPrimary: "oklch(72% 0.175 50)",
76
+ sidebarPrimaryForeground: "oklch(13.7% 0 0)",
77
+ sidebarAccent: "oklch(20% 0 0)",
78
+ sidebarAccentForeground: "oklch(95% 0 0)",
79
+ sidebarBorder: "oklch(25% 0 0)",
80
+ sidebarRing: "oklch(72% 0.175 50)",
81
+ },
82
+ };
83
+
84
+ /**
85
+ * Derives a human label from a token key: `"cardForeground"` → `"Card
86
+ * Foreground"`, `"chart1"` → `"Chart 1"`.
87
+ *
88
+ * @param props - Input props.
89
+ * @param props.key - The camelCase token key.
90
+ * @returns The admin form label.
91
+ */
92
+ function tokenLabel(props: { key: string }): string {
93
+ const spaced = props.key.replace(/([A-Z])/g, " $1").replace(/(\d+)/g, " $1");
94
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
95
+ }
96
+
97
+ /**
98
+ * The shadcn design-token set as `color()` fields for one colour scheme.
99
+ *
100
+ * One field per `THEME_COLOR_TOKENS` entry, keyed camelCase, pinned to
101
+ * `format: "oklch"` — the notation `globals.css` declares its tokens in, so
102
+ * stored values interpolate into custom properties with no conversion.
103
+ * Defaults come from the Stark × Ember palette for the given mode.
104
+ *
105
+ * @param mode - Which scheme's defaults to apply.
106
+ * @returns One resolved `color()` field per shadcn token.
107
+ */
108
+ export function themeColors(mode: "dark" | "light"): Record<ThemeColorTokenKey, ColorField> {
109
+ const fields = {} as Record<ThemeColorTokenKey, ColorField>;
110
+ for (const key of Object.keys(THEME_COLOR_TOKENS) as ThemeColorTokenKey[]) {
111
+ fields[key] = color({
112
+ format: "oklch",
113
+ label: tokenLabel({ key }),
114
+ defaultValue: EMBER[mode][key],
115
+ });
116
+ }
117
+ return fields;
118
+ }
@@ -1,130 +1,48 @@
1
- import { color, defineCollection, tabs, text, ui } from "@vexcms/core"
1
+ import { defineCollection, group, text } from "@vexcms/core";
2
2
 
3
- import ThemeImportField from "~/components/admin/ThemeImportField"
4
- import { TABLE_SLUG_THEMES } from "~/db/constants"
3
+ import { TABLE_SLUG_THEMES } from "~/db/constants";
5
4
 
6
- /**
7
- * Helper to create a set of shadcn/Tailwind color fields for a theme mode.
8
- */
9
- function themeColorFields() {
10
- return {
11
- background: color({ label: "Background", defaultValue: "#ffffff" }),
12
- foreground: color({ label: "Foreground", defaultValue: "#0a0a0a" }),
13
- card: color({ label: "Card", defaultValue: "#ffffff" }),
14
- cardForeground: color({ label: "Card Foreground", defaultValue: "#0a0a0a" }),
15
- popover: color({ label: "Popover", defaultValue: "#ffffff" }),
16
- popoverForeground: color({ label: "Popover Foreground", defaultValue: "#0a0a0a" }),
17
- primary: color({ label: "Primary", defaultValue: "#171717" }),
18
- primaryForeground: color({ label: "Primary Foreground", defaultValue: "#fafafa" }),
19
- secondary: color({ label: "Secondary", defaultValue: "#f5f5f5" }),
20
- secondaryForeground: color({ label: "Secondary Foreground", defaultValue: "#171717" }),
21
- muted: color({ label: "Muted", defaultValue: "#f5f5f5" }),
22
- mutedForeground: color({ label: "Muted Foreground", defaultValue: "#737373" }),
23
- accent: color({ label: "Accent", defaultValue: "#f5f5f5" }),
24
- accentForeground: color({ label: "Accent Foreground", defaultValue: "#171717" }),
25
- destructive: color({ label: "Destructive", defaultValue: "#ef4444" }),
26
- destructiveForeground: color({ label: "Destructive Foreground", defaultValue: "#fafafa" }),
27
- border: color({ label: "Border", defaultValue: "#e5e5e5" }),
28
- input: color({ label: "Input", defaultValue: "#e5e5e5" }),
29
- ring: color({ label: "Ring", defaultValue: "#0a0a0a" }),
30
- chart1: color({ label: "Chart 1", defaultValue: "#e76e50" }),
31
- chart2: color({ label: "Chart 2", defaultValue: "#2a9d90" }),
32
- chart3: color({ label: "Chart 3", defaultValue: "#264653" }),
33
- chart4: color({ label: "Chart 4", defaultValue: "#e8c468" }),
34
- chart5: color({ label: "Chart 5", defaultValue: "#f4a261" }),
35
- sidebar: color({ label: "Sidebar", defaultValue: "#fafafa" }),
36
- sidebarForeground: color({ label: "Sidebar Foreground", defaultValue: "#0a0a0a" }),
37
- sidebarPrimary: color({ label: "Sidebar Primary", defaultValue: "#171717" }),
38
- sidebarPrimaryForeground: color({ label: "Sidebar Primary FG", defaultValue: "#fafafa" }),
39
- sidebarAccent: color({ label: "Sidebar Accent", defaultValue: "#f5f5f5" }),
40
- sidebarAccentForeground: color({ label: "Sidebar Accent FG", defaultValue: "#171717" }),
41
- sidebarBorder: color({ label: "Sidebar Border", defaultValue: "#e5e5e5" }),
42
- sidebarRing: color({ label: "Sidebar Ring", defaultValue: "#0a0a0a" }),
43
- } as const
44
- }
5
+ import { themeColors } from "./themeColors";
45
6
 
46
7
  export const themes = defineCollection({
47
8
  slug: TABLE_SLUG_THEMES,
9
+ interfaceName: "Theme",
10
+ labels: {
11
+ singular: "Theme",
12
+ plural: "Themes",
13
+ },
48
14
  admin: {
49
- defaultColumns: ["name", "_id", "fontFamily"],
50
- group: "Site Builder",
51
15
  useAsTitle: "name",
52
- livePreview: {
53
- url: "/preview/home",
54
- },
55
- },
56
- versions: {
57
- drafts: true,
16
+ icon: "Palette",
58
17
  },
59
18
  fields: {
60
19
  name: text({
61
20
  label: "Theme Name",
62
21
  required: true,
22
+ index: "by_name",
23
+ description:
24
+ "Internal identifier for this theme. Used for idempotent lookups and admin display.",
63
25
  }),
64
- importTheme: ui({
65
- admin: {
66
- components: {
67
- Field: ThemeImportField,
68
- },
69
- },
70
- }),
71
- colors: tabs({
72
- label: "Theme Colors",
73
- tabs: [
74
- {
75
- label: "Light",
76
- slug: "light",
77
- fields: themeColorFields(),
78
- },
79
- {
80
- label: "Dark",
81
- slug: "dark",
82
- fields: {
83
- ...themeColorFields(),
84
- // Override dark mode defaults
85
- background: color({ label: "Background", defaultValue: "#0a0a0a" }),
86
- foreground: color({ label: "Foreground", defaultValue: "#fafafa" }),
87
- card: color({ label: "Card", defaultValue: "#0a0a0a" }),
88
- cardForeground: color({ label: "Card Foreground", defaultValue: "#fafafa" }),
89
- popover: color({ label: "Popover", defaultValue: "#0a0a0a" }),
90
- popoverForeground: color({ label: "Popover Foreground", defaultValue: "#fafafa" }),
91
- primary: color({ label: "Primary", defaultValue: "#fafafa" }),
92
- primaryForeground: color({ label: "Primary Foreground", defaultValue: "#171717" }),
93
- secondary: color({ label: "Secondary", defaultValue: "#262626" }),
94
- secondaryForeground: color({ label: "Secondary Foreground", defaultValue: "#fafafa" }),
95
- muted: color({ label: "Muted", defaultValue: "#262626" }),
96
- mutedForeground: color({ label: "Muted Foreground", defaultValue: "#a3a3a3" }),
97
- accent: color({ label: "Accent", defaultValue: "#262626" }),
98
- accentForeground: color({ label: "Accent Foreground", defaultValue: "#fafafa" }),
99
- border: color({ label: "Border", defaultValue: "#262626" }),
100
- input: color({ label: "Input", defaultValue: "#262626" }),
101
- ring: color({ label: "Ring", defaultValue: "#d4d4d4" }),
102
- sidebar: color({ label: "Sidebar", defaultValue: "#171717" }),
103
- sidebarForeground: color({ label: "Sidebar Foreground", defaultValue: "#fafafa" }),
104
- sidebarPrimary: color({ label: "Sidebar Primary", defaultValue: "#fafafa" }),
105
- sidebarPrimaryForeground: color({
106
- label: "Sidebar Primary FG",
107
- defaultValue: "#171717",
108
- }),
109
- sidebarAccent: color({ label: "Sidebar Accent", defaultValue: "#262626" }),
110
- sidebarAccentForeground: color({ label: "Sidebar Accent FG", defaultValue: "#fafafa" }),
111
- sidebarBorder: color({ label: "Sidebar Border", defaultValue: "#262626" }),
112
- sidebarRing: color({ label: "Sidebar Ring", defaultValue: "#d4d4d4" }),
113
- },
114
- },
115
- ],
26
+ fontFamily: text({
27
+ label: "Font Family",
28
+ defaultValue: "Geist, Inter, system-ui, sans-serif",
29
+ description:
30
+ "CSS font-family stack applied to --font-sans. The first available font wins, so a stack naming an unloaded font degrades rather than breaking.",
116
31
  }),
117
32
  radius: text({
118
33
  label: "Border Radius",
119
- defaultValue: "0.625rem",
34
+ defaultValue: "4px",
35
+ description: "Applied to the --radius custom property. Any CSS length.",
120
36
  }),
121
- fontFamily: text({
122
- label: "Font Family",
123
- defaultValue: "Inter, sans-serif",
37
+ light: group({
38
+ label: "Light Mode",
39
+ description: "Tokens emitted under :root.",
40
+ fields: themeColors("light"),
41
+ }),
42
+ dark: group({
43
+ label: "Dark Mode",
44
+ description: "Tokens emitted under .dark.",
45
+ fields: themeColors("dark"),
124
46
  }),
125
47
  },
126
- labels: {
127
- plural: "Themes",
128
- singular: "Theme",
129
- },
130
- })
48
+ });
@@ -1 +1 @@
1
- export { siteSettings } from "./siteSettings"
1
+ export * from "./siteSettings"