create-vexcms 0.0.20 → 0.1.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +98 -80
- package/dist/index.js +974 -639
- package/dist/index.mjs +30976 -0
- package/package.json +35 -15
- package/templates/base-nextjs/README.md +59 -203
- 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/tsconfig.json +2 -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 +46 -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/components/motion-primitives/animated-group.tsx +44 -120
- package/templates/marketing-site/src/components/motion-primitives/reveal.module.css +32 -0
- package/templates/marketing-site/src/components/motion-primitives/text-effect.tsx +41 -267
- 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 +12 -55
- 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,123 +1,243 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
VexCMS · globals.css — Stark × Ember (shadcn-faithful translation)
|
|
3
|
+
----------------------------------------------------------------------------
|
|
4
|
+
Source of design intent: .agent/docs/design/claude-design/admin.css (Claude Design
|
|
5
|
+
export). That file uses bespoke tokens (--page, --fg-muted, --line-bold,
|
|
6
|
+
--accent / --accent-tint / --accent-ink, …); this file translates them
|
|
7
|
+
into shadcn-named tokens so every React component in @vexcms/react keeps
|
|
8
|
+
working with no per-component overrides.
|
|
9
|
+
|
|
10
|
+
Read .agent/docs/design/claude-design/README.md before editing.
|
|
11
|
+
|
|
12
|
+
Key semantic decisions (different from naive shadcn):
|
|
13
|
+
- Brand CTA → --primary (admin.css `--accent`) ember solid
|
|
14
|
+
- Highlight → --accent (admin.css `--accent-tint`) ember tint
|
|
15
|
+
- Ink/active → --accent-fg (admin.css `--accent-ink`) deep ember
|
|
16
|
+
- Default surfaces (--card, --popover) = SAME tone (admin.css `--surface`)
|
|
17
|
+
- --muted / --raised = third elevation level (admin.css `--raised`)
|
|
18
|
+
used for table headers, modal foots, hover bgs, badge.muted.
|
|
19
|
+
|
|
20
|
+
All values are oklch.
|
|
21
|
+
============================================================================ */
|
|
22
|
+
|
|
1
23
|
@import "tailwindcss";
|
|
2
24
|
@import "tw-animate-css";
|
|
3
25
|
@import "shadcn/tailwind.css";
|
|
26
|
+
@import "@vexcms/next/styles";
|
|
4
27
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@
|
|
28
|
+
/* Workspace source scan (belt-and-suspenders — normal dev relies on tsup
|
|
29
|
+
watchers rebuilding each package dist/, which Tailwind already scans via
|
|
30
|
+
@vexcms/next/styles). These two lines also point Tailwind at the raw
|
|
31
|
+
.tsx so classes appear immediately even if a tsup watcher dies. They
|
|
32
|
+
are workspace-relative and resolve only inside this monorepo. */
|
|
33
|
+
@source "../../../../packages/react/src/**/*.ts";
|
|
34
|
+
@source "../../../../packages/react/src/**/*.tsx";
|
|
35
|
+
@source "../../../../packages/next/src/**/*.ts";
|
|
36
|
+
@source "../../../../packages/next/src/**/*.tsx";
|
|
8
37
|
|
|
9
38
|
@custom-variant dark (&:is(.dark *));
|
|
10
39
|
|
|
11
40
|
@theme inline {
|
|
12
|
-
|
|
13
|
-
--color-
|
|
14
|
-
--
|
|
15
|
-
--
|
|
16
|
-
--color-
|
|
17
|
-
--color-
|
|
18
|
-
--color-
|
|
19
|
-
|
|
41
|
+
/* ── Surfaces ─────────────────────────────────────────────────────────── */
|
|
42
|
+
--color-background: var(--background);
|
|
43
|
+
--color-foreground: var(--foreground);
|
|
44
|
+
--color-card: var(--card);
|
|
45
|
+
--color-card-foreground: var(--card-foreground);
|
|
46
|
+
--color-popover: var(--popover);
|
|
47
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
48
|
+
|
|
49
|
+
/* ── Brand / accents ──────────────────────────────────────────────────── */
|
|
50
|
+
--color-primary: var(--primary); /* ember solid */
|
|
51
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
52
|
+
--color-secondary: var(--secondary);
|
|
53
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
54
|
+
--color-accent: var(--accent); /* ember tint */
|
|
55
|
+
--color-accent-foreground: var(--accent-foreground); /* ember ink */
|
|
56
|
+
|
|
57
|
+
/* ── Neutral hover & muted ────────────────────────────────────────────── */
|
|
58
|
+
--color-muted: var(--muted); /* = --raised */
|
|
59
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
60
|
+
|
|
61
|
+
/* ── Borders & inputs ─────────────────────────────────────────────────── */
|
|
62
|
+
--color-border: var(--border); /* default separators */
|
|
63
|
+
--color-input: var(--input); /* input borders */
|
|
64
|
+
--color-ring: var(--ring); /* solid ember focus */
|
|
65
|
+
|
|
66
|
+
/* ── Status ───────────────────────────────────────────────────────────── */
|
|
67
|
+
--color-destructive: var(--destructive);
|
|
68
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
69
|
+
|
|
70
|
+
/* ── Sidebar ──────────────────────────────────────────────────────────── */
|
|
71
|
+
--color-sidebar: var(--sidebar);
|
|
72
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
73
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
20
74
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
21
|
-
--color-sidebar-
|
|
22
|
-
--color-sidebar-foreground:
|
|
23
|
-
--color-sidebar:
|
|
24
|
-
--color-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
--color-chart-2: var(--chart-2);
|
|
75
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
76
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
77
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
78
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
79
|
+
|
|
80
|
+
/* ── Charts ───────────────────────────────────────────────────────────── */
|
|
28
81
|
--color-chart-1: var(--chart-1);
|
|
29
|
-
--color-
|
|
30
|
-
--color-
|
|
31
|
-
--color-
|
|
32
|
-
--color-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
--
|
|
36
|
-
--
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
--
|
|
40
|
-
--
|
|
41
|
-
--
|
|
42
|
-
--
|
|
43
|
-
--
|
|
44
|
-
--
|
|
45
|
-
--radius-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
--
|
|
49
|
-
--
|
|
50
|
-
--
|
|
51
|
-
--
|
|
82
|
+
--color-chart-2: var(--chart-2);
|
|
83
|
+
--color-chart-3: var(--chart-3);
|
|
84
|
+
--color-chart-4: var(--chart-4);
|
|
85
|
+
--color-chart-5: var(--chart-5);
|
|
86
|
+
|
|
87
|
+
/* ── Fonts ────────────────────────────────────────────────────────────── */
|
|
88
|
+
--font-sans: var(--font-sans);
|
|
89
|
+
--font-mono: var(--font-geist-mono);
|
|
90
|
+
|
|
91
|
+
/* ── Radii — Vex is sharp ─────────────────────────────────────────────── */
|
|
92
|
+
--radius-sm: 2px; /* controls: buttons, inputs, menu items, type chips */
|
|
93
|
+
--radius-md: 4px; /* cards, popovers, table wraps */
|
|
94
|
+
--radius-lg: 8px; /* modals, dialogs */
|
|
95
|
+
--radius-xl: 12px;
|
|
96
|
+
--radius-2xl: 16px;
|
|
97
|
+
--radius-3xl: 20px;
|
|
98
|
+
--radius-4xl: 9999px; /* pill badges & avatars */
|
|
99
|
+
|
|
100
|
+
/* ── Shadows ──────────────────────────────────────────────────────────── */
|
|
101
|
+
--shadow-xs: var(--shadow-xs-val);
|
|
102
|
+
--shadow-sm: var(--shadow-sm-val);
|
|
103
|
+
--shadow-md: var(--shadow-md-val);
|
|
104
|
+
--shadow-lg: var(--shadow-lg-val);
|
|
105
|
+
--shadow-pop: var(--shadow-pop-val);
|
|
106
|
+
|
|
107
|
+
/* ── Motion ───────────────────────────────────────────────────────────── */
|
|
108
|
+
--ease-emphasized: cubic-bezier(.2, .8, .2, 1);
|
|
52
109
|
}
|
|
53
110
|
|
|
111
|
+
/* ────────────────────────────────────────────────────────────────────────────
|
|
112
|
+
LIGHT — Stark × Ember
|
|
113
|
+
page #F5F5F5 · surface #FFFFFF · raised #FAFAFA · fg #0A0A0A
|
|
114
|
+
──────────────────────────────────────────────────────────────────────────── */
|
|
54
115
|
:root {
|
|
55
|
-
|
|
56
|
-
--background:
|
|
57
|
-
--foreground:
|
|
58
|
-
--card:
|
|
59
|
-
--card-foreground:
|
|
60
|
-
--popover:
|
|
61
|
-
--popover-foreground:
|
|
62
|
-
|
|
63
|
-
--primary
|
|
64
|
-
--
|
|
65
|
-
--
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
--
|
|
69
|
-
--
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
--
|
|
73
|
-
--
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
--
|
|
77
|
-
--
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
--
|
|
81
|
-
--
|
|
82
|
-
--
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
--
|
|
86
|
-
--
|
|
116
|
+
/* Surfaces — three elevation levels */
|
|
117
|
+
--background: oklch(96.1% 0 0); /* #F5F5F5 */
|
|
118
|
+
--foreground: oklch(13.7% 0 0); /* #0A0A0A */
|
|
119
|
+
--card: oklch(100% 0 0); /* #FFFFFF */
|
|
120
|
+
--card-foreground: oklch(13.7% 0 0);
|
|
121
|
+
--popover: oklch(100% 0 0); /* same as card */
|
|
122
|
+
--popover-foreground: oklch(13.7% 0 0);
|
|
123
|
+
|
|
124
|
+
/* Brand — ember on --primary */
|
|
125
|
+
--primary: oklch(60.5% 0.175 42); /* #E8622A */
|
|
126
|
+
--primary-foreground: oklch(100% 0 0);
|
|
127
|
+
|
|
128
|
+
/* Neutral secondary */
|
|
129
|
+
--secondary: oklch(98% 0 0); /* #FAFAFA */
|
|
130
|
+
--secondary-foreground: oklch(13.7% 0 0);
|
|
131
|
+
|
|
132
|
+
/* Highlight = ember TINT (matches admin.css --accent-tint) */
|
|
133
|
+
--accent: oklch(96% 0.025 42); /* #FEF0E8 */
|
|
134
|
+
--accent-foreground: oklch(52% 0.180 40); /* ember ink */
|
|
135
|
+
|
|
136
|
+
/* Neutral hover & muted (= admin.css --hover / --raised) */
|
|
137
|
+
--muted: oklch(98% 0 0); /* #FAFAFA = raised */
|
|
138
|
+
--muted-foreground: oklch(50.5% 0 0); /* #717171 */
|
|
139
|
+
|
|
140
|
+
/* Borders */
|
|
141
|
+
--border: oklch(85% 0 0); /* #D4D4D4 — default */
|
|
142
|
+
--input: oklch(54.6% 0 0); /* #6B6B6B — inputs */
|
|
143
|
+
--ring: oklch(60.5% 0.175 42); /* solid ember */
|
|
144
|
+
|
|
145
|
+
/* Status */
|
|
146
|
+
--destructive: oklch(57.7% 0.198 27); /* #DC2626 */
|
|
147
|
+
--destructive-foreground: oklch(98% 0 0);
|
|
148
|
+
|
|
149
|
+
/* Base radius — used by anything reading var(--radius) directly */
|
|
150
|
+
--radius: 4px;
|
|
151
|
+
|
|
152
|
+
/* Sidebar */
|
|
153
|
+
--sidebar: oklch(98% 0 0); /* #FAFAFA */
|
|
154
|
+
--sidebar-foreground: oklch(13.7% 0 0);
|
|
155
|
+
--sidebar-primary: oklch(60.5% 0.175 42); /* ember */
|
|
156
|
+
--sidebar-primary-foreground: oklch(100% 0 0);
|
|
157
|
+
--sidebar-accent: oklch(96.1% 0 0);
|
|
158
|
+
--sidebar-accent-foreground: oklch(13.7% 0 0);
|
|
159
|
+
--sidebar-border: oklch(85% 0 0);
|
|
160
|
+
--sidebar-ring: oklch(60.5% 0.175 42);
|
|
161
|
+
|
|
162
|
+
/* Charts */
|
|
163
|
+
--chart-1: oklch(60.5% 0.175 42);
|
|
164
|
+
--chart-2: oklch(45% 0 0);
|
|
165
|
+
--chart-3: oklch(72% 0.100 60);
|
|
166
|
+
--chart-4: oklch(60% 0.040 30);
|
|
167
|
+
--chart-5: oklch(78% 0 0);
|
|
168
|
+
|
|
169
|
+
/* Shadows — soft elevation in light */
|
|
170
|
+
--shadow-xs-val: 0 1px 2px rgba(10, 10, 10, 0.04);
|
|
171
|
+
--shadow-sm-val: 0 1px 3px rgba(10, 10, 10, 0.08);
|
|
172
|
+
--shadow-md-val: 0 4px 12px rgba(10, 10, 10, 0.08);
|
|
173
|
+
--shadow-lg-val: 0 12px 32px rgba(10, 10, 10, 0.12);
|
|
174
|
+
--shadow-pop-val: 0 12px 28px rgba(10, 10, 10, 0.14),
|
|
175
|
+
0 1px 3px rgba(10, 10, 10, 0.06);
|
|
87
176
|
}
|
|
88
177
|
|
|
178
|
+
/* ────────────────────────────────────────────────────────────────────────────
|
|
179
|
+
DARK — Stark × Ember dark
|
|
180
|
+
page #0A0A0A · surface #141414 · raised #1E1E1E · fg #F4F4F4
|
|
181
|
+
Hotter ember (#F07040) for contrast. No flat shadows.
|
|
182
|
+
──────────────────────────────────────────────────────────────────────────── */
|
|
89
183
|
.dark {
|
|
90
|
-
|
|
91
|
-
--
|
|
92
|
-
--
|
|
93
|
-
--card
|
|
94
|
-
--
|
|
95
|
-
--popover
|
|
96
|
-
--
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
--
|
|
100
|
-
--
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
--
|
|
104
|
-
--
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
--
|
|
108
|
-
--
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
--
|
|
112
|
-
--
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
--
|
|
116
|
-
--
|
|
117
|
-
--
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
--
|
|
184
|
+
/* Surfaces */
|
|
185
|
+
--background: oklch(13.7% 0 0); /* #0A0A0A */
|
|
186
|
+
--foreground: oklch(95% 0 0); /* #F4F4F4 */
|
|
187
|
+
--card: oklch(17.4% 0 0); /* #141414 */
|
|
188
|
+
--card-foreground: oklch(95% 0 0);
|
|
189
|
+
--popover: oklch(17.4% 0 0); /* same as card */
|
|
190
|
+
--popover-foreground: oklch(95% 0 0);
|
|
191
|
+
|
|
192
|
+
/* Brand */
|
|
193
|
+
--primary: oklch(72% 0.175 50); /* #F07040 */
|
|
194
|
+
--primary-foreground: oklch(13.7% 0 0);
|
|
195
|
+
|
|
196
|
+
/* Secondary */
|
|
197
|
+
--secondary: oklch(20% 0 0); /* #1E1E1E */
|
|
198
|
+
--secondary-foreground: oklch(95% 0 0);
|
|
199
|
+
|
|
200
|
+
/* Highlight — translucent ember tint */
|
|
201
|
+
--accent: oklch(72% 0.175 50 / 0.12);
|
|
202
|
+
--accent-foreground: oklch(72% 0.175 50); /* ember itself in dark */
|
|
203
|
+
|
|
204
|
+
/* Neutral hover & muted */
|
|
205
|
+
--muted: oklch(20% 0 0); /* = raised in dark */
|
|
206
|
+
--muted-foreground: oklch(70% 0 0); /* #A0A0A0 */
|
|
207
|
+
|
|
208
|
+
/* Borders */
|
|
209
|
+
--border: oklch(25% 0 0); /* #2A2A2A */
|
|
210
|
+
--input: oklch(40% 0 0); /* #5A5A5A */
|
|
211
|
+
--ring: oklch(72% 0.175 50);
|
|
212
|
+
|
|
213
|
+
/* Status */
|
|
214
|
+
--destructive: oklch(63% 0.210 27); /* #EF4444 */
|
|
215
|
+
--destructive-foreground: oklch(95% 0 0);
|
|
216
|
+
|
|
217
|
+
/* Sidebar */
|
|
218
|
+
--sidebar: oklch(7% 0 0); /* #050505 */
|
|
219
|
+
--sidebar-foreground: oklch(95% 0 0);
|
|
220
|
+
--sidebar-primary: oklch(72% 0.175 50);
|
|
221
|
+
--sidebar-primary-foreground: oklch(13.7% 0 0);
|
|
222
|
+
--sidebar-accent: oklch(20% 0 0);
|
|
223
|
+
--sidebar-accent-foreground: oklch(95% 0 0);
|
|
224
|
+
--sidebar-border: oklch(25% 0 0);
|
|
225
|
+
--sidebar-ring: oklch(72% 0.175 50);
|
|
226
|
+
|
|
227
|
+
/* Charts */
|
|
228
|
+
--chart-1: oklch(72% 0.175 50);
|
|
229
|
+
--chart-2: oklch(78% 0 0);
|
|
230
|
+
--chart-3: oklch(78% 0.120 65);
|
|
231
|
+
--chart-4: oklch(60% 0.060 30);
|
|
232
|
+
--chart-5: oklch(45% 0 0);
|
|
233
|
+
|
|
234
|
+
/* Shadows — flat in dark; only overlays use --shadow-pop */
|
|
235
|
+
--shadow-xs-val: none;
|
|
236
|
+
--shadow-sm-val: none;
|
|
237
|
+
--shadow-md-val: none;
|
|
238
|
+
--shadow-lg-val: none;
|
|
239
|
+
--shadow-pop-val: 0 18px 40px rgba(0, 0, 0, 0.55),
|
|
240
|
+
0 1px 0 var(--border) inset;
|
|
121
241
|
}
|
|
122
242
|
|
|
123
243
|
@layer base {
|
|
@@ -126,5 +246,21 @@
|
|
|
126
246
|
}
|
|
127
247
|
body {
|
|
128
248
|
@apply bg-background text-foreground;
|
|
249
|
+
font-feature-settings:
|
|
250
|
+
"ss01" 1,
|
|
251
|
+
"cv11" 1;
|
|
252
|
+
-webkit-font-smoothing: antialiased;
|
|
129
253
|
}
|
|
130
254
|
}
|
|
255
|
+
|
|
256
|
+
/* Pangea dnd placeholder — thin dashed line instead of full-height gap */
|
|
257
|
+
[data-rfd-placeholder] {
|
|
258
|
+
height: 3px !important;
|
|
259
|
+
min-height: 3px !important;
|
|
260
|
+
margin: 0 !important;
|
|
261
|
+
padding: 0 !important;
|
|
262
|
+
background: transparent !important;
|
|
263
|
+
border-radius: 0 !important;
|
|
264
|
+
border-top: 2px dashed var(--border) !important;
|
|
265
|
+
}
|
|
266
|
+
|
|
@@ -1,28 +1,38 @@
|
|
|
1
1
|
import type { Metadata } from "next"
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import "./globals.css"
|
|
4
|
+
|
|
5
|
+
import { Geist, Geist_Mono } from "next/font/google"
|
|
3
6
|
|
|
4
7
|
import ClientProviders from "~/components/providers/client"
|
|
5
8
|
import ServerProviders from "~/components/providers/server"
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
const geistSans = Geist({
|
|
11
|
+
subsets: ["latin"],
|
|
12
|
+
variable: "--font-sans",
|
|
13
|
+
})
|
|
8
14
|
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
const geistMono = Geist_Mono({
|
|
16
|
+
subsets: ["latin"],
|
|
17
|
+
variable: "--font-geist-mono",
|
|
18
|
+
})
|
|
12
19
|
|
|
13
20
|
export const metadata: Metadata = {
|
|
14
|
-
description: "
|
|
15
|
-
|
|
21
|
+
description: "Built with VexCMS",
|
|
22
|
+
icons: { icon: "/favicons/favicon.ico" },
|
|
23
|
+
title: "VexCMS",
|
|
16
24
|
}
|
|
17
25
|
|
|
18
|
-
export default function RootLayout({
|
|
26
|
+
export default function RootLayout({
|
|
27
|
+
children,
|
|
28
|
+
}: Readonly<{
|
|
29
|
+
children: React.ReactNode
|
|
30
|
+
}>) {
|
|
19
31
|
return (
|
|
20
|
-
<html
|
|
21
|
-
<body
|
|
32
|
+
<html className={`${geistSans.variable} ${geistMono.variable} antialiased`} lang="en" suppressHydrationWarning>
|
|
33
|
+
<body>
|
|
22
34
|
<ServerProviders>
|
|
23
|
-
<ClientProviders>
|
|
24
|
-
{children}
|
|
25
|
-
</ClientProviders>
|
|
35
|
+
<ClientProviders>{children}</ClientProviders>
|
|
26
36
|
</ServerProviders>
|
|
27
37
|
</body>
|
|
28
38
|
</html>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { UnauthorizedView } from "@vexcms/react";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
|
|
4
|
+
export const metadata = {
|
|
5
|
+
title: "Access denied",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Landing page for callers who are signed in but fail the `adminPanel.access`
|
|
10
|
+
* check in the access matrix.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately lives OUTSIDE the `(vexcms)/admin` segment: that segment's layout
|
|
13
|
+
* mounts the admin shell and redirects unauthorized callers here, so rendering
|
|
14
|
+
* this inside it would loop.
|
|
15
|
+
*/
|
|
16
|
+
export default function UnauthorizedPage() {
|
|
17
|
+
return (
|
|
18
|
+
<UnauthorizedView>
|
|
19
|
+
<Link className="text-primary underline" href="/">
|
|
20
|
+
Return to site
|
|
21
|
+
</Link>
|
|
22
|
+
</UnauthorizedView>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { defineAccess } from "@vexcms/core"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
TABLE_SLUG_ORGANIZATIONS,
|
|
5
|
+
TABLE_SLUG_USERS,
|
|
6
|
+
USER_ROLES,
|
|
7
|
+
} from "~/db/constants"
|
|
8
|
+
import { images, users } from "~/vexcms/collections"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Access control (RBAC) for the admin panel and every registered collection.
|
|
12
|
+
*
|
|
13
|
+
* `admin` gets unrestricted access. `user` can read and update only their own
|
|
14
|
+
* profile row and cannot reach the admin panel. Add a resource here whenever
|
|
15
|
+
* you register a new collection in `vex.config.ts`.
|
|
16
|
+
*
|
|
17
|
+
* @see https://vexcms.dev/docs/access-control
|
|
18
|
+
*/
|
|
19
|
+
export const access = defineAccess({
|
|
20
|
+
anonRole: USER_ROLES.user,
|
|
21
|
+
roles: Object.values(USER_ROLES),
|
|
22
|
+
userRolesField: "roles",
|
|
23
|
+
userCollectionSlug: TABLE_SLUG_USERS,
|
|
24
|
+
orgCollectionSlug: TABLE_SLUG_ORGANIZATIONS,
|
|
25
|
+
resources: [images, users],
|
|
26
|
+
permissions: {
|
|
27
|
+
[USER_ROLES.admin]: {
|
|
28
|
+
"*": true,
|
|
29
|
+
},
|
|
30
|
+
[USER_ROLES.user]: {
|
|
31
|
+
"*": false,
|
|
32
|
+
adminPanel: {
|
|
33
|
+
access: false,
|
|
34
|
+
},
|
|
35
|
+
user: {
|
|
36
|
+
"*": false,
|
|
37
|
+
read: {
|
|
38
|
+
constraints: ({ user, q }) => q.withIndex("by_email", (fq) => fq.eq("email", user.email)),
|
|
39
|
+
},
|
|
40
|
+
update: {
|
|
41
|
+
constraints: ({ user, q }) => q.filter((fq) => fq.eq("email", user.email)),
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
})
|
|
@@ -1,25 +1,37 @@
|
|
|
1
1
|
import type { ReactNode } from "react"
|
|
2
2
|
|
|
3
|
+
import { apiKeyClient } from "@better-auth/api-key/client"
|
|
3
4
|
import { convexClient } from "@convex-dev/better-auth/client/plugins"
|
|
4
5
|
import { AuthUIProvider } from "@daveyplate/better-auth-ui"
|
|
5
|
-
import { adminClient,
|
|
6
|
+
import { adminClient, anonymousClient } from "better-auth/client/plugins"
|
|
6
7
|
import { createAuthClient } from "better-auth/react"
|
|
7
8
|
import Link from "next/link"
|
|
8
9
|
import { useRouter } from "next/navigation"
|
|
9
10
|
|
|
10
11
|
import { env } from "~/env.mjs"
|
|
11
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Global Better Auth client for this project.
|
|
15
|
+
*
|
|
16
|
+
* Configured with the site's auth base URL and plugins for admin roles,
|
|
17
|
+
* anonymous access, API key auth, and Convex session storage. Add
|
|
18
|
+
* `organizationClient()` from `better-auth/client/plugins` here if you enable
|
|
19
|
+
* organizations (`--orgs`, or by hand later) — the server-side `organization()`
|
|
20
|
+
* plugin alone does not surface org UI on the client.
|
|
21
|
+
*
|
|
22
|
+
* @see signIn, signOut, useSession — destructured from this client
|
|
23
|
+
* @see BetterAuthClientProvider — wraps the app with this client
|
|
24
|
+
*/
|
|
12
25
|
export const authClient = createAuthClient({
|
|
13
26
|
basePath: "/api/auth",
|
|
14
27
|
baseURL: env.NEXT_PUBLIC_SITE_URL,
|
|
15
|
-
plugins: [adminClient(), apiKeyClient(), convexClient()],
|
|
28
|
+
plugins: [adminClient(), anonymousClient(), apiKeyClient(), convexClient()],
|
|
16
29
|
})
|
|
17
30
|
|
|
18
31
|
export const { signIn, signOut, useSession } = authClient
|
|
19
32
|
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
authClient.$store.listen("$sessionSignal", async () => { })
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
34
|
+
authClient.$store.listen("$sessionSignal", () => {})
|
|
23
35
|
|
|
24
36
|
export default function BetterAuthClientProvider({ children }: { children: ReactNode }) {
|
|
25
37
|
const router = useRouter()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { hasPermission as hasPermissionCore, type HasPermissionProps } from "@vexcms/core";
|
|
4
|
+
|
|
5
|
+
import { useAuth } from "~/context/AuthContext";
|
|
6
|
+
|
|
7
|
+
import { access } from "./access";
|
|
8
|
+
|
|
9
|
+
type Subjects = NonNullable<typeof access.__subjects>;
|
|
10
|
+
/**
|
|
11
|
+
* Client-side permission check for UI affordances (advisory — server guards enforce).
|
|
12
|
+
*
|
|
13
|
+
* Kept alongside `@vexcms/react`'s `usePermission` deliberately: the frontend tree is
|
|
14
|
+
* not wrapped in the Vex providers, so this reads the app's own `AuthContext` and
|
|
15
|
+
* imports the access config directly instead of resolving both from context.
|
|
16
|
+
*
|
|
17
|
+
* @param props.resource - Subject slug (collection/global/media/`adminPanel`).
|
|
18
|
+
* @param props.action - Action to check.
|
|
19
|
+
* @param props.data - The concrete document for an exact per-doc check (edit views).
|
|
20
|
+
* @param props.scope - How to answer when a check needs the document and `data` was
|
|
21
|
+
* not supplied: `"all"` (the default) → false, `"any"` → true (sidebar/list
|
|
22
|
+
* gating), `"doc"` → throws (edit views — pass `data`). No effect when `data` is
|
|
23
|
+
* given. See `PERMISSION_SCOPES`.
|
|
24
|
+
* @returns The permission boolean. `false` for a signed-out user; `true` when no
|
|
25
|
+
* `access` config exists at all — the access system being absent is the documented
|
|
26
|
+
* open-by-default escape hatch, not a denial.
|
|
27
|
+
*/
|
|
28
|
+
export function hasPermission<TSubject extends keyof Subjects, TData extends object = object>(
|
|
29
|
+
props: Omit<HasPermissionProps<Subjects, TSubject, TData>, "access" | "organization" | "user">,
|
|
30
|
+
): boolean {
|
|
31
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
32
|
+
const { user, organization } = useAuth();
|
|
33
|
+
return hasPermissionCore({
|
|
34
|
+
access,
|
|
35
|
+
user,
|
|
36
|
+
organization,
|
|
37
|
+
...props,
|
|
38
|
+
});
|
|
39
|
+
}
|