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.
- package/LICENSE +202 -0
- package/README.md +93 -83
- package/dist/index.js +246 -81
- package/dist/index.mjs +30976 -0
- package/package.json +35 -15
- package/templates/base-nextjs/README.md +53 -205
- package/templates/base-nextjs/_env.example +7 -9
- package/templates/base-nextjs/_gitignore +41 -1
- package/templates/base-nextjs/convex/_generated/api.d.ts +6 -62
- package/templates/base-nextjs/convex/_generated/server.d.ts +19 -0
- package/templates/base-nextjs/convex/_generated/server.js +8 -0
- package/templates/base-nextjs/convex/auth/api.ts +49 -3
- package/templates/base-nextjs/convex/auth/db.ts +42 -300
- package/templates/base-nextjs/convex/auth/index.ts +11 -7
- package/templates/base-nextjs/convex/auth/options.ts +5 -29
- package/templates/base-nextjs/convex/auth/plugins/index.ts +15 -5
- package/templates/base-nextjs/convex/auth/sessions.ts +9 -15
- package/templates/base-nextjs/convex/http.ts +18 -3
- package/templates/base-nextjs/convex/schema.ts +104 -2
- package/templates/base-nextjs/convex/vex/firstUser.ts +10 -84
- package/templates/base-nextjs/convex/vex/globals.ts +19 -82
- package/templates/base-nextjs/convex/vex/media.ts +19 -246
- package/templates/base-nextjs/convex/vex.schema.ts +138 -0
- package/templates/base-nextjs/convex/vex.ts +19 -0
- package/templates/base-nextjs/convex.json +6 -0
- package/templates/base-nextjs/eslint.config.mjs +1 -21
- package/templates/base-nextjs/next.config.ts +10 -0
- package/templates/base-nextjs/package.json +47 -48
- package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +17 -5
- package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/view.tsx +2 -4
- package/templates/base-nextjs/src/app/(frontend)/layout.tsx +12 -2
- package/templates/base-nextjs/src/app/(frontend)/page.tsx +2 -113
- package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +40 -0
- package/templates/base-nextjs/src/app/(vexcms)/admin/clientProviders.tsx +22 -0
- package/templates/base-nextjs/src/app/(vexcms)/admin/layout.tsx +19 -0
- package/templates/base-nextjs/src/app/globals.css +240 -104
- package/templates/base-nextjs/src/app/layout.tsx +23 -13
- package/templates/base-nextjs/src/app/unauthorized/page.tsx +24 -0
- package/templates/base-nextjs/src/auth/access.ts +46 -0
- package/templates/base-nextjs/src/auth/client.tsx +17 -5
- package/templates/base-nextjs/src/auth/hasPermission.ts +39 -0
- package/templates/base-nextjs/src/auth/serverUtils.ts +15 -25
- package/templates/base-nextjs/src/components/WelcomePage.tsx +116 -0
- package/templates/base-nextjs/src/components/component-example.tsx +56 -464
- package/templates/base-nextjs/src/components/providers/auth.tsx +13 -0
- package/templates/base-nextjs/src/components/providers/client.tsx +6 -1
- package/templates/base-nextjs/src/components/providers/convex.tsx +53 -21
- package/templates/base-nextjs/src/components/providers/server.tsx +10 -7
- package/templates/{marketing-site → base-nextjs}/src/components/ui/accordion.tsx +30 -32
- package/templates/base-nextjs/src/components/ui/badge.tsx +10 -13
- package/templates/base-nextjs/src/components/ui/input.tsx +35 -5
- package/templates/base-nextjs/src/context/AuthContext.tsx +29 -0
- package/templates/base-nextjs/src/db/constants/auth.ts +0 -1
- package/templates/base-nextjs/src/db/constants/index.ts +22 -35
- package/templates/base-nextjs/src/lib/utils.ts +0 -12
- package/templates/base-nextjs/src/proxy.ts +97 -12
- package/templates/base-nextjs/src/vex.config.ts +35 -0
- package/templates/base-nextjs/src/vex.types.ts +293 -0
- package/templates/base-nextjs/src/vexcms/collections/images.ts +15 -0
- package/templates/base-nextjs/src/vexcms/collections/index.ts +2 -1
- package/templates/base-nextjs/src/vexcms/collections/users.ts +7 -35
- package/templates/base-nextjs/tsconfig.json +3 -0
- package/templates/marketing-site/convex/_generated/api.d.ts +81 -0
- package/templates/marketing-site/convex/_generated/api.js +23 -0
- package/templates/marketing-site/convex/_generated/dataModel.d.ts +60 -0
- package/templates/marketing-site/convex/_generated/server.d.ts +162 -0
- package/templates/marketing-site/convex/_generated/server.js +101 -0
- package/templates/marketing-site/convex/footers.ts +11 -7
- package/templates/marketing-site/convex/headers.ts +15 -5
- package/templates/marketing-site/convex/pages.ts +20 -35
- package/templates/marketing-site/convex/schema.ts +119 -0
- package/templates/marketing-site/convex/seed.ts +582 -35
- package/templates/marketing-site/convex/siteSettings.ts +19 -5
- package/templates/marketing-site/convex/theme.ts +55 -61
- package/templates/marketing-site/convex/vex.schema.ts +355 -0
- package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +27 -160
- package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +6 -9
- package/templates/marketing-site/src/app/(frontend)/layout.tsx +4 -2
- package/templates/marketing-site/src/app/(frontend)/page.tsx +2 -5
- package/templates/marketing-site/src/components/SiteFooter.tsx +6 -3
- package/templates/marketing-site/src/components/SiteHeader.tsx +6 -3
- package/templates/marketing-site/src/components/ThemeLive.tsx +59 -0
- package/templates/marketing-site/src/components/ThemeStyle.tsx +45 -89
- package/templates/marketing-site/src/lib/metadata.ts +51 -72
- package/templates/marketing-site/src/vex.config.ts +35 -0
- package/templates/marketing-site/src/vex.types.ts +634 -0
- package/templates/marketing-site/src/vexcms/api.ts +23 -0
- package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -3
- package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +2 -2
- package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -4
- package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +2 -2
- package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +4 -8
- package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +2 -2
- package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +7 -11
- package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +1 -1
- package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +9 -9
- package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +4 -5
- package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +3 -4
- package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +10 -19
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +4 -9
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +3 -6
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +28 -68
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +13 -43
- package/templates/marketing-site/src/vexcms/blocks/config.ts +7 -22
- package/templates/marketing-site/src/vexcms/blocks/constants.ts +2 -3
- package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -8
- package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -1
- package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -1
- package/templates/marketing-site/src/vexcms/collections/index.ts +2 -1
- package/templates/marketing-site/src/vexcms/collections/pages.ts +12 -21
- package/templates/marketing-site/src/vexcms/collections/themeColors.ts +118 -0
- package/templates/marketing-site/src/vexcms/collections/themes.ts +29 -111
- package/templates/marketing-site/src/vexcms/globals/index.ts +1 -1
- package/templates/marketing-site/src/vexcms/globals/siteSettings.ts +43 -27
- package/templates/base-nextjs/convex/auth/adapter/index.ts +0 -230
- package/templates/base-nextjs/convex/auth/adapter/utils.ts +0 -547
- package/templates/base-nextjs/convex/auth/config.ts +0 -10
- package/templates/base-nextjs/convex/vex/auth.ts +0 -38
- package/templates/base-nextjs/convex/vex/collections.ts +0 -333
- package/templates/base-nextjs/convex/vex/helpers.ts +0 -33
- package/templates/base-nextjs/convex/vex/impersonation.ts +0 -51
- package/templates/base-nextjs/convex/vex/migrate.ts +0 -74
- package/templates/base-nextjs/convex/vex/model/collections.ts +0 -196
- package/templates/base-nextjs/convex/vex/model/media.ts +0 -87
- package/templates/base-nextjs/convex/vex/model/versions.ts +0 -254
- package/templates/base-nextjs/convex/vex/previewSnapshot.ts +0 -53
- package/templates/base-nextjs/convex/vex/versions.ts +0 -648
- package/templates/base-nextjs/src/app/admin/AdminLayoutWrapper.tsx +0 -33
- package/templates/base-nextjs/src/app/admin/AdminPageWrapper.tsx +0 -38
- package/templates/base-nextjs/src/app/admin/RichTextFieldWithMedia.tsx +0 -101
- package/templates/base-nextjs/src/app/admin/[[...path]]/page.tsx +0 -15
- package/templates/base-nextjs/src/app/admin/layout.tsx +0 -51
- package/templates/base-nextjs/src/app/favicon.ico +0 -0
- package/templates/base-nextjs/src/components/providers/theme.tsx +0 -11
- package/templates/base-nextjs/src/components/ui/theme-toggle.tsx +0 -67
- package/templates/base-nextjs/src/db/types.ts +0 -6
- package/templates/base-nextjs/src/vexcms/access.ts +0 -44
- package/templates/base-nextjs/src/vexcms/auth.ts +0 -4
- package/templates/base-nextjs/src/vexcms/collections/media.ts +0 -15
- package/templates/base-nextjs/vex.config.ts +0 -25
- package/templates/marketing-site/public/favicons/favicon.ico +0 -0
- package/templates/marketing-site/src/app/(frontend)/preview/[slug]/PreviewPageContent.tsx +0 -55
- package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +0 -27
- package/templates/marketing-site/src/app/(frontend)/preview/layout.tsx +0 -18
- package/templates/marketing-site/src/app/admin/AdminLayoutWrapper.tsx +0 -71
- package/templates/marketing-site/src/components/ThemeInjector.tsx +0 -128
- package/templates/marketing-site/src/components/admin/IconPickerField.tsx +0 -134
- package/templates/marketing-site/src/components/admin/ThemeImport.tsx +0 -174
- package/templates/marketing-site/src/components/admin/ThemeImportField.tsx +0 -33
- package/templates/marketing-site/src/db/constants/index.ts +0 -52
- package/templates/marketing-site/src/lib/colorConvert.ts +0 -171
- package/templates/marketing-site/src/vexcms/access.ts +0 -36
- 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
|
-
})
|