create-vexcms 0.1.0-alpha.2 → 0.1.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +48 -14
  2. package/dist/index.js +1714 -1504
  3. package/package.json +2 -2
  4. package/templates/base-nextjs/README.md +18 -5
  5. package/templates/base-nextjs/convex/auth/index.ts +2 -2
  6. package/templates/base-nextjs/convex/auth/options.ts +27 -26
  7. package/templates/base-nextjs/convex/auth/schema.ts +51 -0
  8. package/templates/base-nextjs/convex/auth/sessions.ts +3 -3
  9. package/templates/base-nextjs/convex/tsconfig.json +2 -2
  10. package/templates/base-nextjs/convex/vex/firstUser.ts +2 -2
  11. package/templates/base-nextjs/convex/vex/globals.ts +3 -2
  12. package/templates/base-nextjs/convex/vex/media.ts +3 -2
  13. package/templates/base-nextjs/convex/vex.schema.ts +1 -0
  14. package/templates/base-nextjs/convex/vex.ts +17 -4
  15. package/templates/base-nextjs/next.config.ts +52 -6
  16. package/templates/base-nextjs/package.json +18 -7
  17. package/templates/base-nextjs/public/favicons/favicon.ico +0 -0
  18. package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +27 -15
  19. package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +2 -2
  20. package/templates/base-nextjs/src/app/(vexcms)/admin/clientProviders.tsx +9 -14
  21. package/templates/base-nextjs/src/app/(vexcms)/admin/layout.tsx +8 -6
  22. package/templates/base-nextjs/src/app/api/vex/revalidate/route.ts +19 -0
  23. package/templates/base-nextjs/src/app/globals.css +10 -9
  24. package/templates/base-nextjs/src/app/layout.tsx +6 -0
  25. package/templates/base-nextjs/src/auth/access.ts +14 -2
  26. package/templates/base-nextjs/src/auth/serverUtils.ts +20 -6
  27. package/templates/base-nextjs/src/components/WelcomePage.tsx +54 -43
  28. package/templates/base-nextjs/src/components/component-example.tsx +1 -1
  29. package/templates/base-nextjs/src/components/providers/server.tsx +16 -8
  30. package/templates/base-nextjs/src/vex.config.server.ts +30 -0
  31. package/templates/base-nextjs/src/vex.config.ts +14 -11
  32. package/templates/base-nextjs/src/vex.types.ts +2 -1
  33. package/templates/base-nextjs/src/vexcms/collections/images.ts +1 -1
  34. package/templates/base-nextjs/src/vexcms/collections/users.ts +3 -1
  35. package/templates/marketing-site/convex/pages.ts +20 -1
  36. package/templates/marketing-site/convex/schema.ts +4 -4
  37. package/templates/marketing-site/convex/seed.ts +515 -70
  38. package/templates/marketing-site/convex/siteSettings.ts +1 -1
  39. package/templates/marketing-site/convex/theme.ts +3 -3
  40. package/templates/marketing-site/convex/vex.schema.ts +75 -38
  41. package/templates/marketing-site/public/favicon.svg +14 -0
  42. package/templates/marketing-site/public/favicons/apple-touch-icon.png +0 -0
  43. package/templates/marketing-site/public/favicons/favicon.ico +0 -0
  44. package/templates/marketing-site/src/app/(frontend)/(site)/PageContent.tsx +64 -0
  45. package/templates/marketing-site/src/app/(frontend)/(site)/SiteLivePreviewProvider.tsx +31 -0
  46. package/templates/marketing-site/src/app/(frontend)/(site)/[slug]/page.tsx +63 -0
  47. package/templates/marketing-site/src/app/(frontend)/(site)/layout.tsx +50 -0
  48. package/templates/marketing-site/src/app/(frontend)/(site)/page.tsx +33 -0
  49. package/templates/marketing-site/src/app/(frontend)/layout.tsx +10 -24
  50. package/templates/marketing-site/src/app/(vexcms)/admin/layout.tsx +32 -0
  51. package/templates/marketing-site/src/app/globals.css +413 -0
  52. package/templates/marketing-site/src/app/layout.tsx +85 -0
  53. package/templates/marketing-site/src/app/robots.ts +13 -0
  54. package/templates/marketing-site/src/app/sitemap.ts +18 -0
  55. package/templates/marketing-site/src/components/BrandMark.tsx +27 -0
  56. package/templates/marketing-site/src/components/CodeHighlightContext.tsx +76 -0
  57. package/templates/marketing-site/src/components/CodePane.tsx +98 -0
  58. package/templates/marketing-site/src/components/Container.tsx +25 -0
  59. package/templates/marketing-site/src/components/CopyButton.tsx +80 -0
  60. package/templates/marketing-site/src/components/FirstAdminBootstrap.tsx +54 -0
  61. package/templates/marketing-site/src/components/InstallCommand.tsx +29 -0
  62. package/templates/marketing-site/src/components/MediaImage.tsx +59 -0
  63. package/templates/marketing-site/src/components/SectionHeader.tsx +51 -0
  64. package/templates/marketing-site/src/components/SiteFooter.tsx +8 -6
  65. package/templates/marketing-site/src/components/SiteHeader.tsx +8 -6
  66. package/templates/marketing-site/src/components/ThemeLive.tsx +1 -2
  67. package/templates/marketing-site/src/components/ThemeStyle.tsx +3 -3
  68. package/templates/marketing-site/src/components/motion-primitives/animated-group.tsx +44 -120
  69. package/templates/marketing-site/src/components/motion-primitives/reveal.module.css +32 -0
  70. package/templates/marketing-site/src/components/motion-primitives/text-effect.tsx +47 -266
  71. package/templates/marketing-site/src/lib/codeHighlightKey.ts +27 -0
  72. package/templates/marketing-site/src/lib/highlight.ts +134 -0
  73. package/templates/marketing-site/src/lib/highlightClient.ts +37 -0
  74. package/templates/marketing-site/src/lib/metadata.ts +61 -33
  75. package/templates/marketing-site/src/lib/resolvePagePath.ts +8 -0
  76. package/templates/marketing-site/src/lib/vex.ts +18 -0
  77. package/templates/marketing-site/src/proxy.ts +179 -0
  78. package/templates/marketing-site/src/vex.config.server.ts +27 -0
  79. package/templates/marketing-site/src/vex.config.ts +27 -9
  80. package/templates/marketing-site/src/vex.types.ts +102 -76
  81. package/templates/marketing-site/src/vexcms/api.ts +2 -2
  82. package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -2
  83. package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +40 -24
  84. package/templates/marketing-site/src/vexcms/blocks/CodeShowcase/config.ts +62 -0
  85. package/templates/marketing-site/src/vexcms/blocks/CodeShowcase/index.tsx +106 -0
  86. package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -3
  87. package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +63 -50
  88. package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +1 -0
  89. package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +43 -58
  90. package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +6 -3
  91. package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +90 -53
  92. package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +10 -3
  93. package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +205 -138
  94. package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +27 -12
  95. package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +174 -119
  96. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +2 -1
  97. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +59 -51
  98. package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +26 -5
  99. package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +256 -73
  100. package/templates/marketing-site/src/vexcms/blocks/Split/config.ts +62 -0
  101. package/templates/marketing-site/src/vexcms/blocks/Split/index.tsx +186 -0
  102. package/templates/marketing-site/src/vexcms/blocks/Stats/config.ts +35 -0
  103. package/templates/marketing-site/src/vexcms/blocks/Stats/index.tsx +77 -0
  104. package/templates/marketing-site/src/vexcms/blocks/config.ts +27 -11
  105. package/templates/marketing-site/src/vexcms/blocks/constants.ts +3 -0
  106. package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -0
  107. package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -0
  108. package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -0
  109. package/templates/marketing-site/src/vexcms/collections/pages.ts +23 -1
  110. package/templates/marketing-site/src/vexcms/collections/themeColors.ts +14 -14
  111. package/templates/marketing-site/src/vexcms/collections/users.ts +20 -0
  112. package/dist/index.mjs +0 -30976
  113. package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +0 -53
  114. package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +0 -25
  115. package/templates/marketing-site/src/app/(frontend)/page.tsx +0 -21
@@ -1,71 +1,84 @@
1
1
  import type { BlockComponentProps } from "@vexcms/react"
2
2
 
3
- import Link from "next/link"
4
-
5
3
  import {
6
4
  Accordion,
7
5
  AccordionContent,
8
6
  AccordionItem,
9
7
  AccordionTrigger,
8
+ buttonVariants,
9
+ cn,
10
10
  } from "@vexcms/react"
11
+ import { ArrowRight } from "lucide-react"
12
+ import Link from "next/link"
11
13
 
12
- export { faqBlock } from "./config"
14
+ import type { FaqBlock } from "~/vex.types"
13
15
 
14
- export default function FAQBlock({ block }: BlockComponentProps) {
15
- const { heading, subheading, supportLink, items } = block as unknown as {
16
- heading: string
17
- subheading?: string
18
- supportLink?: string
19
- items?: Array<{ question: string; answer: string }>
20
- }
16
+ import { Container } from "~/components/Container"
21
17
 
22
- return (
23
- <section className="py-16 md:py-24">
24
- <div className="mx-auto max-w-5xl px-4 md:px-6">
25
- <div className="mx-auto max-w-xl text-center">
26
- <h2 className="text-3xl font-bold text-balance md:text-4xl lg:text-5xl">
27
- {heading}
28
- </h2>
29
- {subheading && (
30
- <p className="text-muted-foreground mt-4 text-balance">
31
- {subheading}
32
- </p>
33
- )}
34
- </div>
18
+ export { faqBlock } from "./config"
35
19
 
36
- <div className="mx-auto mt-12 max-w-xl">
37
- <Accordion className="bg-muted dark:bg-muted/50 w-full rounded-2xl p-1">
38
- {(items ?? []).map((faq, index) => (
39
- <div className="group" key={index}>
40
- <AccordionItem
41
- className="data-open:bg-card dark:data-open:bg-muted rounded-xl border-none px-7 py-1 data-open:border-none data-open:shadow-sm"
42
- value={`${index}`}
43
- >
44
- <AccordionTrigger className="cursor-pointer text-base hover:no-underline">
45
- {faq.question}
46
- </AccordionTrigger>
47
- <AccordionContent>
48
- <p className="text-base">{faq.answer}</p>
49
- </AccordionContent>
50
- </AccordionItem>
51
- <hr className="mx-7 border-dashed group-last:hidden" />
52
- </div>
53
- ))}
54
- </Accordion>
20
+ /**
21
+ * FAQ — question left, answer under it.
22
+ *
23
+ * Two columns rather than the sourced centred layout: it gives the support
24
+ * link a home, holds answers at a 68ch measure instead of 90ch, and stops the
25
+ * centred-heading pattern appearing three times on one page.
26
+ *
27
+ * Items are rules, not cards — no radius, no background, no shadow, which is
28
+ * the only treatment that reads the same in both themes.
29
+ */
30
+ export default function FAQBlockRenderer({ block }: BlockComponentProps) {
31
+ const { heading, items, subheading, supportLink } = block as FaqBlock
32
+ const faqs = items ?? []
33
+
34
+ if (faqs.length === 0) {return null}
55
35
 
56
- {supportLink && (
57
- <p className="text-muted-foreground mt-6 px-8">
58
- {"Can't find what you're looking for? Contact our "}
36
+ return (
37
+ <section className="py-14 md:py-20 xl:py-28">
38
+ <Container>
39
+ <div className="grid gap-8 xl:grid-cols-[4fr_7fr] xl:items-start xl:gap-14">
40
+ <div>
41
+ <h2 className="text-[26px] leading-[1.18] font-bold tracking-[-0.025em] text-balance text-foreground md:text-3xl xl:text-4xl">
42
+ {heading}
43
+ </h2>
44
+ {subheading ? (
45
+ <p className="mt-4 text-[17px] leading-[1.6] text-pretty text-muted-foreground">
46
+ {subheading}
47
+ </p>
48
+ ) : null}
49
+ {supportLink ? (
50
+ // Fixed copy in the renderer: the field stores only an href.
59
51
  <Link
60
- className="text-primary font-medium hover:underline"
52
+ className={cn(
53
+ buttonVariants({ variant: "link" }),
54
+ "mt-5 h-auto gap-1.5 p-0 text-[15px] font-medium text-primary no-underline hover:underline"
55
+ )}
61
56
  href={supportLink}
62
57
  >
63
- support team
58
+ Open an issue
59
+ <ArrowRight className="size-4" />
64
60
  </Link>
65
- </p>
66
- )}
61
+ ) : null}
62
+ </div>
63
+
64
+ <Accordion className="border-t border-border" defaultValue={["faq-0"]}>
65
+ {faqs.map((faq, index) => (
66
+ <AccordionItem
67
+ className="border-b border-border"
68
+ key={`${faq.question}-${index}`}
69
+ value={`faq-${index}`}
70
+ >
71
+ <AccordionTrigger className="flex min-h-[52px] w-full items-start justify-between gap-6 py-[18px] text-left text-base font-medium text-foreground">
72
+ {faq.question}
73
+ </AccordionTrigger>
74
+ <AccordionContent className="max-w-[68ch] pt-3 pb-[18px] text-[15px] leading-[1.65] text-muted-foreground">
75
+ {faq.answer}
76
+ </AccordionContent>
77
+ </AccordionItem>
78
+ ))}
79
+ </Accordion>
67
80
  </div>
68
- </div>
81
+ </Container>
69
82
  </section>
70
83
  )
71
84
  }
@@ -20,6 +20,7 @@ export const featuresBlock = defineBlock({
20
20
  label: "Features",
21
21
  required: true,
22
22
  items: group({
23
+ label: "Feature",
23
24
  fields: {
24
25
  title: text({ label: "Title", required: true }),
25
26
  description: text({ label: "Description", required: true }),
@@ -1,73 +1,58 @@
1
1
  import type { BlockComponentProps } from "@vexcms/react"
2
- import type { ComponentPropsWithRef } from "react"
3
2
 
4
3
  import { icons } from "lucide-react"
5
4
 
6
- import { Card, CardContent, CardHeader } from "@vexcms/react"
5
+ import type { FeaturesBlock } from "~/vex.types"
7
6
 
8
- export { featuresBlock } from "./config"
7
+ import { Container } from "~/components/Container"
8
+ import { SectionHeader } from "~/components/SectionHeader"
9
9
 
10
- function LucideIcon({ name }: { name: string }) {
11
- const Icon = icons[name as keyof typeof icons]
12
- if (!Icon) return null
13
- return <Icon className="size-4" />
14
- }
10
+ export { featuresBlock } from "./config"
15
11
 
16
- function CardDecorator({ children, ...divProps }: ComponentPropsWithRef<"div">) {
17
- return (
18
- <div
19
- className="relative mx-auto size-36 duration-200 [--color-border:color-mix(in_oklab,var(--color-zinc-950)10%,transparent)] group-hover:[--color-border:color-mix(in_oklab,var(--color-zinc-950)20%,transparent)] dark:[--color-border:color-mix(in_oklab,var(--color-white)15%,transparent)] dark:group-hover:bg-white/5 dark:group-hover:[--color-border:color-mix(in_oklab,var(--color-white)20%,transparent)]"
20
- {...divProps}
21
- >
22
- <div
23
- aria-hidden
24
- className="absolute inset-0 bg-[linear-gradient(to_right,var(--color-border)_1px,transparent_1px),linear-gradient(to_bottom,var(--color-border)_1px,transparent_1px)] bg-[size:24px_24px]"
25
- />
26
- <div
27
- aria-hidden
28
- className="to-background absolute inset-0 bg-radial from-transparent to-75%"
29
- />
30
- <div className="bg-background absolute inset-0 m-auto flex size-12 items-center justify-center border-t border-l">
31
- {children}
32
- </div>
33
- </div>
34
- )
35
- }
36
12
 
13
+ /**
14
+ * Features — six cards, hairline-joined.
15
+ *
16
+ * The grid is a 1px mesh (`gap-px` over a `bg-border` wrapper) rather than
17
+ * gapped cards with shadows. Dark mode has no box-shadow at all, so gapped
18
+ * cards would lose their separation there; the mesh reads identically in both
19
+ * modes and suits the sharp radius.
20
+ */
37
21
  export default function FeaturesBlock({ block }: BlockComponentProps) {
38
- const { heading, subheading, features } = block as unknown as {
39
- heading: string
40
- subheading?: string
41
- features?: Array<{ title: string; description: string; icon?: string }>
42
- }
22
+ const { features, heading, subheading } = block as FeaturesBlock
23
+ const items = features ?? []
24
+
25
+ if (items.length === 0) {return null}
43
26
 
44
27
  return (
45
- <section className="bg-zinc-50 py-16 md:py-32 dark:bg-transparent">
46
- <div className="@container mx-auto max-w-5xl px-6">
47
- <div className="text-center">
48
- <h2 className="text-4xl font-semibold text-balance lg:text-5xl">
49
- {heading}
50
- </h2>
51
- {subheading && <p className="mt-4">{subheading}</p>}
28
+ <section className="py-14 md:py-20 xl:py-28">
29
+ <Container>
30
+ <SectionHeader heading={heading} subheading={subheading} />
31
+
32
+ <div className="grid gap-px overflow-hidden rounded-md border border-border bg-border md:grid-cols-2 xl:grid-cols-3">
33
+ {items.map((feature, index) => {
34
+ const Glyph = feature.icon ? icons[feature.icon as keyof typeof icons] : undefined
35
+ return (
36
+ <div className="bg-background p-6" key={`${feature.title}-${index}`}>
37
+ {/* An unresolvable icon drops the frame entirely rather than
38
+ leaving an empty square; the title then starts the cell and
39
+ a mixed array still reads as intentional. */}
40
+ {Glyph ? (
41
+ <span className="mb-4 flex size-8 items-center justify-center rounded-sm border border-border">
42
+ <Glyph className="size-4 text-primary" />
43
+ </span>
44
+ ) : null}
45
+ <h3 className="text-[17px] leading-[1.35] font-semibold tracking-[-0.01em] text-foreground xl:text-lg">
46
+ {feature.title}
47
+ </h3>
48
+ <p className="mt-2 text-sm leading-[1.6] text-pretty text-muted-foreground">
49
+ {feature.description}
50
+ </p>
51
+ </div>
52
+ )
53
+ })}
52
54
  </div>
53
- <Card className="mx-auto mt-8 grid max-w-sm divide-y overflow-hidden shadow-zinc-950/5 *:text-center md:mt-16 @min-4xl:max-w-full @min-4xl:grid-cols-3 @min-4xl:divide-x @min-4xl:divide-y-0">
54
- {(features ?? []).map((feature, index) => (
55
- <div className="group shadow-zinc-950/5" key={index}>
56
- <CardHeader className="pb-3">
57
- {feature.icon && (
58
- <CardDecorator>
59
- <LucideIcon name={feature.icon} />
60
- </CardDecorator>
61
- )}
62
- <h3 className="mt-6 font-medium">{feature.title}</h3>
63
- </CardHeader>
64
- <CardContent>
65
- <p className="text-sm">{feature.description}</p>
66
- </CardContent>
67
- </div>
68
- ))}
69
- </Card>
70
- </div>
55
+ </Container>
71
56
  </section>
72
57
  )
73
58
  }
@@ -1,6 +1,7 @@
1
1
  import { array, defineBlock, group, text, upload } from "@vexcms/core"
2
2
 
3
3
  import { TABLE_SLUG_IMAGES } from "~/db/constants"
4
+
4
5
  import { BLOCK_SLUG_FOOTER } from "../constants"
5
6
 
6
7
  export const footerBlock = defineBlock({
@@ -22,6 +23,7 @@ export const footerBlock = defineBlock({
22
23
  links: array({
23
24
  label: "Links",
24
25
  items: group({
26
+ label: "Link",
25
27
  fields: {
26
28
  label: text({ label: "Label", required: true }),
27
29
  href: text({ label: "Link", required: true }),
@@ -31,8 +33,8 @@ export const footerBlock = defineBlock({
31
33
  { label: "Features", href: "/features" },
32
34
  { label: "Pricing", href: "/pricing" },
33
35
  { label: "Roadmap", href: "/roadmap" },
34
- { label: "Documentation", href: "/docs" },
35
- { label: "GitHub", href: "https://github.com/vexcms/vex" },
36
+ { label: "Documentation", href: "https://docs.vexcms.dev" },
37
+ { label: "GitHub", href: "https://github.com/ianyimi/vex" },
36
38
  { label: "npm", href: "https://www.npmjs.com/package/@vexcms/core" },
37
39
  { label: "Convex", href: "https://convex.dev" },
38
40
  ],
@@ -40,6 +42,7 @@ export const footerBlock = defineBlock({
40
42
  socialLinks: array({
41
43
  label: "Social Links",
42
44
  items: group({
45
+ label: "Social Link",
43
46
  fields: {
44
47
  platform: text({ label: "Platform", required: true }),
45
48
  href: text({ label: "URL", required: true }),
@@ -50,7 +53,7 @@ export const footerBlock = defineBlock({
50
53
  },
51
54
  }),
52
55
  defaultValue: [
53
- { platform: "GitHub", href: "https://github.com/vexcms/vex", icon: "Github" },
56
+ { platform: "GitHub", href: "https://github.com/ianyimi/vex", icon: "Github" },
54
57
  { platform: "X", href: "https://x.com/vexcms", icon: "Twitter" },
55
58
  ],
56
59
  }),
@@ -1,71 +1,108 @@
1
1
  import type { BlockComponentProps } from "@vexcms/react"
2
2
 
3
+ import { buttonVariants, cn } from "@vexcms/react"
3
4
  import { icons } from "lucide-react"
4
5
  import Link from "next/link"
5
6
 
7
+ import type { FooterBlock } from "~/vex.types"
8
+
9
+ import { BrandMark } from "~/components/BrandMark"
10
+ import { Container } from "~/components/Container"
11
+ import { MediaImage } from "~/components/MediaImage"
12
+
6
13
  export { footerBlock } from "./config"
7
14
 
8
- function SocialIcon({ name }: { name: string }) {
9
- const Icon = icons[name as keyof typeof icons]
10
- if (!Icon) return null
11
- return <Icon className="size-5" />
12
- }
15
+ /**
16
+ * Site footer — one rule, one row of links, one line of law.
17
+ *
18
+ * Deliberately not a four-column sitemap: `links` is a flat array with no
19
+ * group field, so column headings would be content the CMS cannot store. A
20
+ * single wrapping row is the shape the data actually has.
21
+ */
22
+ export default function FooterBlockRenderer({ block }: BlockComponentProps) {
23
+ const { copyright, links, logoImage, logoText, socialLinks } = block as FooterBlock
13
24
 
14
- export default function FooterBlock({ block }: BlockComponentProps) {
15
- const { logoText, copyright, links, socialLinks } = block as {
16
- logoText?: string
17
- logoImage?: string
18
- copyright?: string
19
- links?: Array<{ label: string; href: string }>
20
- socialLinks?: Array<{ platform: string; href: string; icon?: string }>
21
- }
25
+ const items = links ?? []
26
+ const socials = socialLinks ?? []
22
27
 
23
28
  return (
24
- <footer>
25
- <div className="mx-auto max-w-5xl px-6 py-8 md:py-16">
26
- <Link
27
- aria-label="go home"
28
- className="mx-auto block w-fit text-lg font-semibold"
29
- href="/"
29
+ <footer className="py-12 md:py-14">
30
+ <Container>
31
+ <div
32
+ className={cn(
33
+ "flex flex-col items-start justify-between gap-8 md:flex-row md:gap-12",
34
+ // The rule only exists to separate the links from the legal line.
35
+ items.length > 0 && "border-b border-border pb-8"
36
+ )}
30
37
  >
31
- {logoText ?? "Vex CMS"}
32
- </Link>
38
+ <div className="flex shrink-0 items-center gap-2 text-foreground">
39
+ {logoImage?.length ? (
40
+ <MediaImage alt={logoText ?? ""} className="h-5 w-auto" height={20} value={logoImage} width={96} />
41
+ ) : (
42
+ <>
43
+ <BrandMark className="size-4 text-primary" />
44
+ {logoText ? (
45
+ <span className="text-[15px] font-extrabold tracking-[-0.03em]">{logoText}</span>
46
+ ) : null}
47
+ </>
48
+ )}
49
+ </div>
33
50
 
34
- <div className="mt-6 flex flex-wrap justify-center gap-6 text-sm">
35
- {(links ?? []).map((link, index) => (
36
- <Link
37
- className="text-muted-foreground hover:text-primary block duration-150"
38
- href={link.href}
39
- key={index}
40
- >
41
- <span>{link.label}</span>
42
- </Link>
43
- ))}
51
+ {items.length > 0 ? (
52
+ <ul className="grid w-full grid-cols-2 gap-x-7 gap-y-2 md:flex md:w-auto md:max-w-[640px] md:flex-wrap md:justify-end">
53
+ {items.map((link) => (
54
+ <li key={`${link.label}-${link.href}`}>
55
+ <Link
56
+ className="text-sm text-muted-foreground transition-colors duration-[180ms] ease-[var(--ease-emphasized)] hover:text-foreground"
57
+ href={link.href}
58
+ >
59
+ {link.label}
60
+ </Link>
61
+ </li>
62
+ ))}
63
+ </ul>
64
+ ) : null}
44
65
  </div>
45
66
 
46
- {socialLinks && socialLinks.length > 0 && (
47
- <div className="my-8 flex flex-wrap justify-center gap-6 text-sm">
48
- {socialLinks.map((social, index) => (
49
- <Link
50
- key={index}
51
- href={social.href}
52
- className="text-muted-foreground hover:text-foreground transition-colors"
53
- aria-label={social.platform}
54
- >
55
- {social.icon ? (
56
- <SocialIcon name={social.icon} />
57
- ) : (
58
- <span className="text-sm">{social.platform}</span>
59
- )}
60
- </Link>
61
- ))}
62
- </div>
63
- )}
67
+ <div className="flex flex-col items-start justify-between gap-5 pt-5 md:flex-row md:items-center">
68
+ {copyright ? (
69
+ <p className="text-[13px] text-muted-foreground">
70
+ © {new Date().getFullYear()} {copyright}
71
+ </p>
72
+ ) : null}
64
73
 
65
- <span className="text-muted-foreground block text-center text-sm">
66
- &copy; {new Date().getFullYear()} {copyright ?? "All rights reserved"}
67
- </span>
68
- </div>
74
+ {socials.length > 0 ? (
75
+ <ul className="flex items-center gap-2">
76
+ {socials.map((social) => {
77
+ // An unresolvable lucide name must not render an empty square —
78
+ // fall back to the platform name as a text link.
79
+ const Glyph = social.icon ? icons[social.icon as keyof typeof icons] : undefined
80
+ return (
81
+ <li key={`${social.platform}-${social.href}`}>
82
+ <Link
83
+ aria-label={social.platform}
84
+ className={cn(
85
+ Glyph
86
+ ? cn(
87
+ buttonVariants({ size: "icon-sm", variant: "ghost" }),
88
+ "border border-border"
89
+ )
90
+ : "text-[13px] text-muted-foreground hover:text-foreground",
91
+ "transition-colors duration-[180ms] ease-[var(--ease-emphasized)] active:translate-y-px"
92
+ )}
93
+ href={social.href}
94
+ rel="noreferrer"
95
+ target="_blank"
96
+ >
97
+ {Glyph ? <Glyph className="size-4" /> : social.platform}
98
+ </Link>
99
+ </li>
100
+ )
101
+ })}
102
+ </ul>
103
+ ) : null}
104
+ </div>
105
+ </Container>
69
106
  </footer>
70
107
  )
71
108
  }
@@ -1,6 +1,7 @@
1
1
  import { array, defineBlock, group, select, text, upload } from "@vexcms/core"
2
2
 
3
3
  import { TABLE_SLUG_IMAGES } from "~/db/constants"
4
+
4
5
  import { BLOCK_SLUG_HEADER } from "../constants"
5
6
 
6
7
  export const headerBlock = defineBlock({
@@ -24,6 +25,7 @@ export const headerBlock = defineBlock({
24
25
  menuItems: array({
25
26
  label: "Menu Items",
26
27
  items: group({
28
+ label: "Menu Item",
27
29
  fields: {
28
30
  label: text({ label: "Label", required: true }),
29
31
  href: text({ label: "Link", required: true }),
@@ -33,12 +35,13 @@ export const headerBlock = defineBlock({
33
35
  { label: "Features", href: "/features" },
34
36
  { label: "Pricing", href: "/pricing" },
35
37
  { label: "Roadmap", href: "/roadmap" },
36
- { label: "Docs", href: "/docs" },
38
+ { label: "Docs", href: "https://docs.vexcms.dev" },
37
39
  ],
38
40
  }),
39
41
  actionButtons: array({
40
42
  label: "Action Buttons",
41
43
  items: group({
44
+ label: "Action Button",
42
45
  fields: {
43
46
  label: text({ label: "Button Label", required: true }),
44
47
  href: text({ label: "Button Link", required: true }),
@@ -54,8 +57,12 @@ export const headerBlock = defineBlock({
54
57
  },
55
58
  }),
56
59
  defaultValue: [
57
- { label: "GitHub", href: "https://github.com/vexcms/vex", variant: "ghost" },
58
- { label: "Get Started", href: "/docs", variant: "default" },
60
+ { label: "GitHub", href: "https://github.com/ianyimi/vex", variant: "ghost" },
61
+ {
62
+ label: "Get Started",
63
+ href: "https://docs.vexcms.dev/guides/quickstart/",
64
+ variant: "default",
65
+ },
59
66
  ],
60
67
  }),
61
68
  },