create-vexcms 0.0.20 → 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 (153) 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 -25
  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 +97 -12
  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/lib/metadata.ts +51 -72
  85. package/templates/marketing-site/src/vex.config.ts +35 -0
  86. package/templates/marketing-site/src/vex.types.ts +634 -0
  87. package/templates/marketing-site/src/vexcms/api.ts +23 -0
  88. package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -3
  89. package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +2 -2
  90. package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -4
  91. package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +2 -2
  92. package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +4 -8
  93. package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +2 -2
  94. package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +7 -11
  95. package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +1 -1
  96. package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +9 -9
  97. package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +4 -5
  98. package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +3 -4
  99. package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +10 -19
  100. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +4 -9
  101. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +3 -6
  102. package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +28 -68
  103. package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +13 -43
  104. package/templates/marketing-site/src/vexcms/blocks/config.ts +7 -22
  105. package/templates/marketing-site/src/vexcms/blocks/constants.ts +2 -3
  106. package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -8
  107. package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -1
  108. package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -1
  109. package/templates/marketing-site/src/vexcms/collections/index.ts +2 -1
  110. package/templates/marketing-site/src/vexcms/collections/pages.ts +12 -21
  111. package/templates/marketing-site/src/vexcms/collections/themeColors.ts +118 -0
  112. package/templates/marketing-site/src/vexcms/collections/themes.ts +29 -111
  113. package/templates/marketing-site/src/vexcms/globals/index.ts +1 -1
  114. package/templates/marketing-site/src/vexcms/globals/siteSettings.ts +43 -27
  115. package/templates/base-nextjs/convex/auth/adapter/index.ts +0 -230
  116. package/templates/base-nextjs/convex/auth/adapter/utils.ts +0 -547
  117. package/templates/base-nextjs/convex/auth/config.ts +0 -10
  118. package/templates/base-nextjs/convex/vex/auth.ts +0 -38
  119. package/templates/base-nextjs/convex/vex/collections.ts +0 -333
  120. package/templates/base-nextjs/convex/vex/helpers.ts +0 -33
  121. package/templates/base-nextjs/convex/vex/impersonation.ts +0 -51
  122. package/templates/base-nextjs/convex/vex/migrate.ts +0 -74
  123. package/templates/base-nextjs/convex/vex/model/collections.ts +0 -196
  124. package/templates/base-nextjs/convex/vex/model/media.ts +0 -87
  125. package/templates/base-nextjs/convex/vex/model/versions.ts +0 -254
  126. package/templates/base-nextjs/convex/vex/previewSnapshot.ts +0 -53
  127. package/templates/base-nextjs/convex/vex/versions.ts +0 -648
  128. package/templates/base-nextjs/src/app/admin/AdminLayoutWrapper.tsx +0 -33
  129. package/templates/base-nextjs/src/app/admin/AdminPageWrapper.tsx +0 -38
  130. package/templates/base-nextjs/src/app/admin/RichTextFieldWithMedia.tsx +0 -101
  131. package/templates/base-nextjs/src/app/admin/[[...path]]/page.tsx +0 -15
  132. package/templates/base-nextjs/src/app/admin/layout.tsx +0 -51
  133. package/templates/base-nextjs/src/app/favicon.ico +0 -0
  134. package/templates/base-nextjs/src/components/providers/theme.tsx +0 -11
  135. package/templates/base-nextjs/src/components/ui/theme-toggle.tsx +0 -67
  136. package/templates/base-nextjs/src/db/types.ts +0 -6
  137. package/templates/base-nextjs/src/vexcms/access.ts +0 -44
  138. package/templates/base-nextjs/src/vexcms/auth.ts +0 -4
  139. package/templates/base-nextjs/src/vexcms/collections/media.ts +0 -15
  140. package/templates/base-nextjs/vex.config.ts +0 -25
  141. package/templates/marketing-site/public/favicons/favicon.ico +0 -0
  142. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/PreviewPageContent.tsx +0 -55
  143. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +0 -27
  144. package/templates/marketing-site/src/app/(frontend)/preview/layout.tsx +0 -18
  145. package/templates/marketing-site/src/app/admin/AdminLayoutWrapper.tsx +0 -71
  146. package/templates/marketing-site/src/components/ThemeInjector.tsx +0 -128
  147. package/templates/marketing-site/src/components/admin/IconPickerField.tsx +0 -134
  148. package/templates/marketing-site/src/components/admin/ThemeImport.tsx +0 -174
  149. package/templates/marketing-site/src/components/admin/ThemeImportField.tsx +0 -33
  150. package/templates/marketing-site/src/db/constants/index.ts +0 -52
  151. package/templates/marketing-site/src/lib/colorConvert.ts +0 -171
  152. package/templates/marketing-site/src/vexcms/access.ts +0 -36
  153. package/templates/marketing-site/vex.config.ts +0 -32
@@ -1,134 +0,0 @@
1
- "use client"
2
-
3
- import { useVexField } from "@vexcms/ui"
4
- import { icons } from "lucide-react"
5
- import { useMemo } from "react"
6
-
7
- import {
8
- Combobox,
9
- ComboboxContent,
10
- ComboboxEmpty,
11
- ComboboxInput,
12
- ComboboxItem,
13
- ComboboxList,
14
- } from "~/components/ui/combobox"
15
- import { Label } from "~/components/ui/label"
16
- import { cn } from "~/lib/utils"
17
-
18
- type IconName = keyof typeof icons
19
-
20
- const iconNames = Object.keys(icons) as IconName[]
21
-
22
- /**
23
- * Custom admin field component that renders a searchable icon picker.
24
- * Stores the selected icon name as a string (e.g. "Zap", "Shield", "Code").
25
- *
26
- * When used inside an array field, receives a `field` prop with synthetic
27
- * state/handleChange. When used at the top level, uses useVexField.
28
- */
29
- export default function IconPickerField({
30
- name,
31
- field: syntheticField,
32
- }: {
33
- name: string
34
- fieldDef?: any
35
- readOnly?: boolean
36
- field?: {
37
- state: { value: unknown }
38
- handleChange: (value: unknown) => void
39
- }
40
- }) {
41
- // Use synthetic field from array context, or useVexField for top-level
42
- const vexField = useVexField<string>({ name })
43
-
44
- const value = syntheticField
45
- ? (syntheticField.state.value as string) ?? ""
46
- : vexField.value ?? ""
47
-
48
- const setValueFn = syntheticField
49
- ? (v: string) => syntheticField.handleChange(v)
50
- : (v: string) => vexField.setValue(v)
51
-
52
- const fieldDef = vexField.fieldDef
53
- const readOnly = vexField.readOnly
54
-
55
- const selectedIcon = value && value in icons ? value : null
56
- const SelectedIconComponent = selectedIcon
57
- ? icons[selectedIcon as IconName]
58
- : null
59
-
60
- // Sort items so the selected icon appears first
61
- const sortedItems = useMemo(() => {
62
- if (!selectedIcon) return iconNames
63
- return [
64
- selectedIcon as IconName,
65
- ...iconNames.filter((n) => n !== selectedIcon),
66
- ]
67
- }, [selectedIcon])
68
-
69
- return (
70
- <div className="space-y-2">
71
- <Label>{(fieldDef as any)?.label ?? "Icon"}</Label>
72
- <div className="flex items-start gap-2">
73
- {/* Icon preview square */}
74
- <div
75
- className={cn(
76
- "flex size-9 shrink-0 items-center justify-center rounded-md border bg-background",
77
- selectedIcon
78
- ? "text-foreground"
79
- : "text-muted-foreground/40"
80
- )}
81
- >
82
- {SelectedIconComponent ? (
83
- <SelectedIconComponent className="size-4" />
84
- ) : null}
85
- </div>
86
-
87
- {/* Combobox */}
88
- <div className="flex-1">
89
- <Combobox
90
- value={selectedIcon ?? ""}
91
- onValueChange={(val) => {
92
- // Toggle: clicking the already-selected icon deselects it
93
- if (val === selectedIcon) {
94
- setValueFn("")
95
- } else {
96
- setValueFn((val as string) ?? "")
97
- }
98
- }}
99
- items={sortedItems}
100
- >
101
- <ComboboxInput
102
- placeholder="Search icons..."
103
- disabled={readOnly}
104
- />
105
- <ComboboxContent>
106
- <ComboboxEmpty>No icons found</ComboboxEmpty>
107
- <ComboboxList>
108
- {(iconName) => {
109
- const Icon = icons[iconName as IconName]
110
- const isSelected = iconName === selectedIcon
111
- return (
112
- <ComboboxItem
113
- key={iconName}
114
- value={iconName}
115
- className={cn(isSelected && "bg-accent")}
116
- >
117
- <Icon className="size-4 shrink-0" />
118
- <span>{iconName}</span>
119
- </ComboboxItem>
120
- )
121
- }}
122
- </ComboboxList>
123
- </ComboboxContent>
124
- </Combobox>
125
- </div>
126
- </div>
127
- {(fieldDef as any)?.admin?.description && (
128
- <p className="text-muted-foreground text-xs">
129
- {(fieldDef as any).admin.description}
130
- </p>
131
- )}
132
- </div>
133
- )
134
- }
@@ -1,174 +0,0 @@
1
- "use client"
2
-
3
- import { useState } from "react"
4
-
5
- import { parseThemeCSS } from "~/lib/colorConvert"
6
-
7
- /**
8
- * CSS variable name → form field path mapping.
9
- * Maps CSS custom property names (from :root / .dark blocks)
10
- * to the field paths in the themes collection form.
11
- *
12
- * Tabs with slugs create nested paths: "light.background", "dark.background"
13
- * This maps the CSS var name to the camelCase field name used in themeColorFields().
14
- */
15
- const CSS_VAR_TO_FIELD: Record<string, string> = {
16
- background: "background",
17
- foreground: "foreground",
18
- card: "card",
19
- "card-foreground": "cardForeground",
20
- popover: "popover",
21
- "popover-foreground": "popoverForeground",
22
- primary: "primary",
23
- "primary-foreground": "primaryForeground",
24
- secondary: "secondary",
25
- "secondary-foreground": "secondaryForeground",
26
- muted: "muted",
27
- "muted-foreground": "mutedForeground",
28
- accent: "accent",
29
- "accent-foreground": "accentForeground",
30
- destructive: "destructive",
31
- "destructive-foreground": "destructiveForeground",
32
- border: "border",
33
- input: "input",
34
- ring: "ring",
35
- "chart-1": "chart1",
36
- "chart-2": "chart2",
37
- "chart-3": "chart3",
38
- "chart-4": "chart4",
39
- "chart-5": "chart5",
40
- sidebar: "sidebar",
41
- "sidebar-foreground": "sidebarForeground",
42
- "sidebar-primary": "sidebarPrimary",
43
- "sidebar-primary-foreground": "sidebarPrimaryForeground",
44
- "sidebar-accent": "sidebarAccent",
45
- "sidebar-accent-foreground": "sidebarAccentForeground",
46
- "sidebar-border": "sidebarBorder",
47
- "sidebar-ring": "sidebarRing",
48
- }
49
-
50
- interface ThemeImportProps {
51
- /** Callback to set a form field value by path. e.g., setFieldValue("light.background", "#fff") */
52
- onImport: (updates: Record<string, string>) => void
53
- }
54
-
55
- /**
56
- * Theme import component — paste CSS from tweakcn/shadcn to populate color fields.
57
- *
58
- * Parses :root { } and .dark { } blocks from the CSS, maps variable names to
59
- * form field paths, and calls onImport with all matched field updates.
60
- *
61
- * Designed to work with any CSS file — only updates fields that match known
62
- * CSS variable names. Unmatched variables are silently ignored.
63
- */
64
- export function ThemeImport({ onImport }: ThemeImportProps) {
65
- const [css, setCss] = useState("")
66
- const [isExpanded, setIsExpanded] = useState(false)
67
- const [importResult, setImportResult] = useState<string | null>(null)
68
-
69
- const handleImport = () => {
70
- if (!css.trim()) return
71
-
72
- const parsed = parseThemeCSS({ css })
73
- const updates: Record<string, string> = {}
74
- let count = 0
75
-
76
- // Map light colors to light.{fieldName} paths
77
- for (const [cssVar, value] of Object.entries(parsed.light)) {
78
- const fieldName = CSS_VAR_TO_FIELD[cssVar]
79
- if (fieldName) {
80
- updates[`light.${fieldName}`] = value
81
- count++
82
- }
83
- }
84
-
85
- // Map dark colors to dark.{fieldName} paths
86
- for (const [cssVar, value] of Object.entries(parsed.dark)) {
87
- const fieldName = CSS_VAR_TO_FIELD[cssVar]
88
- if (fieldName) {
89
- updates[`dark.${fieldName}`] = value
90
- count++
91
- }
92
- }
93
-
94
- if (count > 0) {
95
- onImport(updates)
96
- setImportResult(`Imported ${count} color values`)
97
- setCss("")
98
- setTimeout(() => setImportResult(null), 3000)
99
- } else {
100
- setImportResult("No matching CSS variables found")
101
- setTimeout(() => setImportResult(null), 3000)
102
- }
103
- }
104
-
105
- return (
106
- <div className="rounded-lg border bg-card p-4 mb-6">
107
- <button
108
- type="button"
109
- onClick={() => setIsExpanded(!isExpanded)}
110
- className="flex items-center gap-2 text-sm font-medium w-full text-left"
111
- >
112
- <span className={`transition-transform ${isExpanded ? "rotate-90" : ""}`}>▶</span>
113
- Import Theme from CSS
114
- </button>
115
-
116
- {isExpanded && (
117
- <div className="mt-3 space-y-3">
118
- <p className="text-xs text-muted-foreground">
119
- Paste CSS from{" "}
120
- <a
121
- href="https://tweakcn.com"
122
- target="_blank"
123
- rel="noopener noreferrer"
124
- className="underline"
125
- >
126
- tweakcn
127
- </a>
128
- {" "}or{" "}
129
- <a
130
- href="https://ui.shadcn.com/themes"
131
- target="_blank"
132
- rel="noopener noreferrer"
133
- className="underline"
134
- >
135
- shadcn/ui themes
136
- </a>
137
- . The importer will match CSS variable names to color fields and fill them in.
138
- </p>
139
-
140
- <textarea
141
- value={css}
142
- onChange={(e) => setCss(e.target.value)}
143
- placeholder={`:root {
144
- --background: #ffffff;
145
- --foreground: #0a0a0a;
146
- --primary: #171717;
147
- ...
148
- }
149
-
150
- .dark {
151
- --background: #0a0a0a;
152
- ...
153
- }`}
154
- className="w-full h-40 rounded-md border bg-background px-3 py-2 text-xs font-mono resize-y"
155
- />
156
-
157
- <div className="flex items-center gap-3">
158
- <button
159
- type="button"
160
- onClick={handleImport}
161
- disabled={!css.trim()}
162
- className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
163
- >
164
- Import
165
- </button>
166
- {importResult && (
167
- <span className="text-xs text-muted-foreground">{importResult}</span>
168
- )}
169
- </div>
170
- </div>
171
- )}
172
- </div>
173
- )
174
- }
@@ -1,33 +0,0 @@
1
- "use client"
2
-
3
- import { useVexFormContext } from "@vexcms/ui"
4
-
5
- import { ThemeImport } from "./ThemeImport"
6
-
7
- /**
8
- * Custom admin field component that renders the theme CSS import UI.
9
- * Uses the VexForm context to programmatically set color field values
10
- * when CSS is imported.
11
- *
12
- * Register on a ui() field in the themes collection:
13
- * ```ts
14
- * importTheme: ui({ admin: { components: { Field: ThemeImportField } } })
15
- * ```
16
- */
17
- export default function ThemeImportField() {
18
- const { form } = useVexFormContext()
19
-
20
- const handleImport = (updates: Record<string, string>) => {
21
- for (const [path, value] of Object.entries(updates)) {
22
- // Path is like "light.background" or "dark.foreground"
23
- // Tabs expand to top-level keys, so the form field path is just the path directly
24
- try {
25
- form.setFieldValue(path as any, value)
26
- } catch {
27
- // Field might not exist in this form — silently skip
28
- }
29
- }
30
- }
31
-
32
- return <ThemeImport onImport={handleImport} />
33
- }
@@ -1,52 +0,0 @@
1
- export * from "./auth"
2
-
3
- // Better Auth
4
- export const TABLE_SLUG_USERS = "user" as const
5
- export const TABLE_SLUG_ACCOUNTS = "account" as const
6
- export const TABLE_SLUG_SESSIONS = "session" as const
7
- export const TABLE_SLUG_VERIFICATIONS = "verification" as const
8
- export const TABLE_SLUG_JWKS = "jwks" as const
9
-
10
- // Media
11
- export const TABLE_SLUG_MEDIA = "media" as const
12
-
13
- // Site Builder
14
- export const TABLE_SLUG_PAGES = "pages" as const
15
- export const TABLE_SLUG_HEADERS = "headers" as const
16
- export const TABLE_SLUG_FOOTERS = "footers" as const
17
- export const TABLE_SLUG_THEMES = "themes" as const
18
- export const TABLE_SLUG_SITE_SETTINGS = "site_settings" as const
19
-
20
- export const AUTH_PROVIDERS = {
21
- apple: "apple",
22
- atlassian: "atlassian",
23
- cognito: "cognito",
24
- discord: "discord",
25
- dropbox: "dropbox",
26
- facebook: "facebook",
27
- figma: "figma",
28
- github: "github",
29
- gitlab: "gitlab",
30
- google: "google",
31
- huggingface: "huggingface",
32
- kakao: "kakao",
33
- kick: "kick",
34
- line: "line",
35
- linear: "linear",
36
- linkedin: "linkedin",
37
- microsoft: "microsoft",
38
- naver: "naver",
39
- notion: "notion",
40
- paypal: "paypal",
41
- reddit: "reddit",
42
- roblox: "roblox",
43
- salesforce: "salesforce",
44
- slack: "slack",
45
- spotify: "spotify",
46
- tiktok: "tiktok",
47
- twitch: "twitch",
48
- twitter: "twitter",
49
- vk: "vk",
50
- zoom: "zoom",
51
- } as const
52
- export type AuthProvider = (typeof AUTH_PROVIDERS)[keyof typeof AUTH_PROVIDERS]
@@ -1,171 +0,0 @@
1
- // @ts-expect-error culori/fn lacks type declarations in v4
2
- import { parse, formatHex, formatHsl, converter } from "culori/fn"
3
-
4
- const toOklch = converter("oklch")
5
-
6
- /**
7
- * Convert a color string to the specified format.
8
- *
9
- * @param props.color - Input color in any CSS-parseable format
10
- * @param props.format - Target format: "hex", "hsl", or "oklch"
11
- * @returns Formatted color string, or the original if parsing fails
12
- */
13
- export function convertColor(props: {
14
- color: string
15
- format: "hex" | "hsl" | "oklch"
16
- }): string {
17
- if (!props.color || props.color.startsWith("var(")) return props.color
18
-
19
- const parsed = parse(props.color)
20
- if (!parsed) return props.color
21
-
22
- switch (props.format) {
23
- case "hex":
24
- return formatHex(parsed) ?? props.color
25
- case "hsl":
26
- return formatHsl(parsed) ?? props.color
27
- case "oklch": {
28
- const oklch = toOklch(parsed)
29
- if (!oklch) return props.color
30
- const l = (oklch.l ?? 0).toFixed(3)
31
- const c = (oklch.c ?? 0).toFixed(3)
32
- const h = (oklch.h ?? 0).toFixed(1)
33
- return `oklch(${l} ${c} ${h})`
34
- }
35
- default:
36
- return props.color
37
- }
38
- }
39
-
40
- /**
41
- * Known CSS custom property names that represent colors.
42
- * These are the standard shadcn/Tailwind CSS variables.
43
- */
44
- const COLOR_VAR_NAMES = [
45
- "background",
46
- "foreground",
47
- "card",
48
- "card-foreground",
49
- "popover",
50
- "popover-foreground",
51
- "primary",
52
- "primary-foreground",
53
- "secondary",
54
- "secondary-foreground",
55
- "muted",
56
- "muted-foreground",
57
- "accent",
58
- "accent-foreground",
59
- "destructive",
60
- "destructive-foreground",
61
- "border",
62
- "input",
63
- "ring",
64
- "chart-1",
65
- "chart-2",
66
- "chart-3",
67
- "chart-4",
68
- "chart-5",
69
- "sidebar",
70
- "sidebar-foreground",
71
- "sidebar-primary",
72
- "sidebar-primary-foreground",
73
- "sidebar-accent",
74
- "sidebar-accent-foreground",
75
- "sidebar-border",
76
- "sidebar-ring",
77
- ]
78
-
79
- /**
80
- * Extract color CSS custom properties from the document's stylesheets.
81
- * Returns light and dark values for each known color variable.
82
- */
83
- export function getThemeColorsFromDOM(): {
84
- name: string
85
- lightValue: string
86
- darkValue: string | null
87
- }[] {
88
- if (typeof document === "undefined") return []
89
-
90
- const results: { name: string; lightValue: string; darkValue: string | null }[] = []
91
-
92
- // Get computed styles for :root (light mode)
93
- const rootStyles = getComputedStyle(document.documentElement)
94
-
95
- // Try to get dark mode values by reading stylesheets
96
- const darkValues = new Map<string, string>()
97
- try {
98
- for (const sheet of document.styleSheets) {
99
- try {
100
- for (const rule of sheet.cssRules) {
101
- if (rule instanceof CSSStyleRule && rule.selectorText?.includes(".dark")) {
102
- for (const varName of COLOR_VAR_NAMES) {
103
- const value = rule.style.getPropertyValue(`--${varName}`).trim()
104
- if (value) darkValues.set(varName, value)
105
- }
106
- }
107
- }
108
- } catch {
109
- // Cross-origin stylesheet — skip
110
- }
111
- }
112
- } catch {
113
- // No stylesheet access
114
- }
115
-
116
- for (const varName of COLOR_VAR_NAMES) {
117
- const lightValue = rootStyles.getPropertyValue(`--${varName}`).trim()
118
- if (!lightValue) continue
119
-
120
- results.push({
121
- name: varName,
122
- lightValue,
123
- darkValue: darkValues.get(varName) ?? null,
124
- })
125
- }
126
-
127
- return results
128
- }
129
-
130
- /**
131
- * Parse CSS text containing :root and .dark variable declarations.
132
- * Used by the theme import feature to extract color values from pasted CSS.
133
- *
134
- * @param props.css - Raw CSS text containing :root { } and optionally .dark { }
135
- * @returns Parsed light and dark color maps
136
- */
137
- export function parseThemeCSS(props: { css: string }): {
138
- light: Record<string, string>
139
- dark: Record<string, string>
140
- } {
141
- const light: Record<string, string> = {}
142
- const dark: Record<string, string> = {}
143
-
144
- // Match :root { ... } block
145
- const rootMatch = props.css.match(/:root\s*\{([\s\S]+?)\}/)
146
- if (rootMatch) {
147
- const vars = rootMatch[1].matchAll(/--([a-z-]+)\s*:\s*([^;]+)/g)
148
- for (const match of vars) {
149
- const name = match[1].trim()
150
- const value = match[2].trim()
151
- if (COLOR_VAR_NAMES.includes(name)) {
152
- light[name] = value
153
- }
154
- }
155
- }
156
-
157
- // Match .dark { ... } block
158
- const darkMatch = props.css.match(/\.dark\s*\{([\s\S]+?)\}/)
159
- if (darkMatch) {
160
- const vars = darkMatch[1].matchAll(/--([a-z-]+)\s*:\s*([^;]+)/g)
161
- for (const match of vars) {
162
- const name = match[1].trim()
163
- const value = match[2].trim()
164
- if (COLOR_VAR_NAMES.includes(name)) {
165
- dark[name] = value
166
- }
167
- }
168
- }
169
-
170
- return { light, dark }
171
- }
@@ -1,36 +0,0 @@
1
- import { defineAccess } from "@vexcms/core"
2
-
3
- import { TABLE_SLUG_USERS, USER_ROLES } from "~/db/constants"
4
- import { footers, headers, media, pages, themes, users } from "~/vexcms/collections"
5
- import { siteSettings } from "~/vexcms/globals"
6
-
7
- export const access = defineAccess({
8
- roles: [USER_ROLES.admin, USER_ROLES.user],
9
- resources: [pages, headers, footers, themes, users, media, siteSettings],
10
- userCollection: users,
11
- permissions: {
12
- admin: {
13
- admin: true,
14
- pages: true,
15
- headers: true,
16
- footers: true,
17
- themes: true,
18
- user: true,
19
- media: true,
20
- site_settings: true,
21
- },
22
- user: {
23
- admin: false,
24
- [TABLE_SLUG_USERS]: {
25
- create: false,
26
- delete: false,
27
- read: ({ data: targetUser, user }) => {
28
- return targetUser._id === user._id
29
- },
30
- update: ({ data: targetUser, user }) => {
31
- return targetUser._id === user._id
32
- },
33
- },
34
- },
35
- },
36
- })
@@ -1,32 +0,0 @@
1
- import { defineConfig } from "@vexcms/core"
2
- import { convexFileStorage } from "@vexcms/file-storage-convex"
3
-
4
- import { auth } from "~/vexcms/auth"
5
- import { access } from "~/vexcms/access"
6
- import {
7
- footers,
8
- headers,
9
- media,
10
- pages,
11
- themes,
12
- users,
13
- } from "~/vexcms/collections"
14
- import { siteSettings } from "~/vexcms/globals"
15
-
16
- export default defineConfig({
17
- access,
18
- admin: {
19
- meta: {
20
- titleSuffix: " | My Site",
21
- },
22
- user: "user",
23
- },
24
- auth,
25
- basePath: "/admin",
26
- collections: [pages, headers, footers, themes, users],
27
- globals: [siteSettings],
28
- media: {
29
- collections: [media],
30
- storageAdapter: convexFileStorage(),
31
- },
32
- })