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.
Files changed (157) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +98 -80
  3. package/dist/index.js +974 -639
  4. package/dist/index.mjs +30976 -0
  5. package/package.json +35 -15
  6. package/templates/base-nextjs/README.md +59 -203
  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/tsconfig.json +2 -2
  21. package/templates/base-nextjs/convex/vex/firstUser.ts +10 -84
  22. package/templates/base-nextjs/convex/vex/globals.ts +19 -82
  23. package/templates/base-nextjs/convex/vex/media.ts +19 -246
  24. package/templates/base-nextjs/convex/vex.schema.ts +138 -0
  25. package/templates/base-nextjs/convex/vex.ts +19 -0
  26. package/templates/base-nextjs/convex.json +6 -0
  27. package/templates/base-nextjs/eslint.config.mjs +1 -21
  28. package/templates/base-nextjs/next.config.ts +10 -0
  29. package/templates/base-nextjs/package.json +46 -48
  30. package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +17 -5
  31. package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/view.tsx +2 -4
  32. package/templates/base-nextjs/src/app/(frontend)/layout.tsx +12 -2
  33. package/templates/base-nextjs/src/app/(frontend)/page.tsx +2 -113
  34. package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +40 -0
  35. package/templates/base-nextjs/src/app/(vexcms)/admin/clientProviders.tsx +22 -0
  36. package/templates/base-nextjs/src/app/(vexcms)/admin/layout.tsx +19 -0
  37. package/templates/base-nextjs/src/app/globals.css +240 -104
  38. package/templates/base-nextjs/src/app/layout.tsx +23 -13
  39. package/templates/base-nextjs/src/app/unauthorized/page.tsx +24 -0
  40. package/templates/base-nextjs/src/auth/access.ts +46 -0
  41. package/templates/base-nextjs/src/auth/client.tsx +17 -5
  42. package/templates/base-nextjs/src/auth/hasPermission.ts +39 -0
  43. package/templates/base-nextjs/src/auth/serverUtils.ts +15 -25
  44. package/templates/base-nextjs/src/components/WelcomePage.tsx +116 -0
  45. package/templates/base-nextjs/src/components/component-example.tsx +56 -464
  46. package/templates/base-nextjs/src/components/providers/auth.tsx +13 -0
  47. package/templates/base-nextjs/src/components/providers/client.tsx +6 -1
  48. package/templates/base-nextjs/src/components/providers/convex.tsx +53 -21
  49. package/templates/base-nextjs/src/components/providers/server.tsx +10 -7
  50. package/templates/{marketing-site → base-nextjs}/src/components/ui/accordion.tsx +30 -32
  51. package/templates/base-nextjs/src/components/ui/badge.tsx +10 -13
  52. package/templates/base-nextjs/src/components/ui/input.tsx +35 -5
  53. package/templates/base-nextjs/src/context/AuthContext.tsx +29 -0
  54. package/templates/base-nextjs/src/db/constants/auth.ts +0 -1
  55. package/templates/base-nextjs/src/db/constants/index.ts +22 -35
  56. package/templates/base-nextjs/src/lib/utils.ts +0 -12
  57. package/templates/base-nextjs/src/proxy.ts +97 -12
  58. package/templates/base-nextjs/src/vex.config.ts +35 -0
  59. package/templates/base-nextjs/src/vex.types.ts +293 -0
  60. package/templates/base-nextjs/src/vexcms/collections/images.ts +15 -0
  61. package/templates/base-nextjs/src/vexcms/collections/index.ts +2 -1
  62. package/templates/base-nextjs/src/vexcms/collections/users.ts +7 -35
  63. package/templates/base-nextjs/tsconfig.json +3 -0
  64. package/templates/marketing-site/convex/_generated/api.d.ts +81 -0
  65. package/templates/marketing-site/convex/_generated/api.js +23 -0
  66. package/templates/marketing-site/convex/_generated/dataModel.d.ts +60 -0
  67. package/templates/marketing-site/convex/_generated/server.d.ts +162 -0
  68. package/templates/marketing-site/convex/_generated/server.js +101 -0
  69. package/templates/marketing-site/convex/footers.ts +11 -7
  70. package/templates/marketing-site/convex/headers.ts +15 -5
  71. package/templates/marketing-site/convex/pages.ts +20 -35
  72. package/templates/marketing-site/convex/schema.ts +119 -0
  73. package/templates/marketing-site/convex/seed.ts +582 -35
  74. package/templates/marketing-site/convex/siteSettings.ts +19 -5
  75. package/templates/marketing-site/convex/theme.ts +55 -61
  76. package/templates/marketing-site/convex/vex.schema.ts +355 -0
  77. package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +27 -160
  78. package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +6 -9
  79. package/templates/marketing-site/src/app/(frontend)/layout.tsx +4 -2
  80. package/templates/marketing-site/src/app/(frontend)/page.tsx +2 -5
  81. package/templates/marketing-site/src/components/SiteFooter.tsx +6 -3
  82. package/templates/marketing-site/src/components/SiteHeader.tsx +6 -3
  83. package/templates/marketing-site/src/components/ThemeLive.tsx +59 -0
  84. package/templates/marketing-site/src/components/ThemeStyle.tsx +45 -89
  85. package/templates/marketing-site/src/components/motion-primitives/animated-group.tsx +44 -120
  86. package/templates/marketing-site/src/components/motion-primitives/reveal.module.css +32 -0
  87. package/templates/marketing-site/src/components/motion-primitives/text-effect.tsx +41 -267
  88. package/templates/marketing-site/src/lib/metadata.ts +51 -72
  89. package/templates/marketing-site/src/vex.config.ts +35 -0
  90. package/templates/marketing-site/src/vex.types.ts +634 -0
  91. package/templates/marketing-site/src/vexcms/api.ts +23 -0
  92. package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -3
  93. package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +2 -2
  94. package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -4
  95. package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +2 -2
  96. package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +4 -8
  97. package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +2 -2
  98. package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +7 -11
  99. package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +1 -1
  100. package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +9 -9
  101. package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +4 -5
  102. package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +3 -4
  103. package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +12 -55
  104. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +4 -9
  105. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +3 -6
  106. package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +28 -68
  107. package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +13 -43
  108. package/templates/marketing-site/src/vexcms/blocks/config.ts +7 -22
  109. package/templates/marketing-site/src/vexcms/blocks/constants.ts +2 -3
  110. package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -8
  111. package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -1
  112. package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -1
  113. package/templates/marketing-site/src/vexcms/collections/index.ts +2 -1
  114. package/templates/marketing-site/src/vexcms/collections/pages.ts +12 -21
  115. package/templates/marketing-site/src/vexcms/collections/themeColors.ts +118 -0
  116. package/templates/marketing-site/src/vexcms/collections/themes.ts +29 -111
  117. package/templates/marketing-site/src/vexcms/globals/index.ts +1 -1
  118. package/templates/marketing-site/src/vexcms/globals/siteSettings.ts +43 -27
  119. package/templates/base-nextjs/convex/auth/adapter/index.ts +0 -230
  120. package/templates/base-nextjs/convex/auth/adapter/utils.ts +0 -547
  121. package/templates/base-nextjs/convex/auth/config.ts +0 -10
  122. package/templates/base-nextjs/convex/vex/auth.ts +0 -38
  123. package/templates/base-nextjs/convex/vex/collections.ts +0 -333
  124. package/templates/base-nextjs/convex/vex/helpers.ts +0 -33
  125. package/templates/base-nextjs/convex/vex/impersonation.ts +0 -51
  126. package/templates/base-nextjs/convex/vex/migrate.ts +0 -74
  127. package/templates/base-nextjs/convex/vex/model/collections.ts +0 -196
  128. package/templates/base-nextjs/convex/vex/model/media.ts +0 -87
  129. package/templates/base-nextjs/convex/vex/model/versions.ts +0 -254
  130. package/templates/base-nextjs/convex/vex/previewSnapshot.ts +0 -53
  131. package/templates/base-nextjs/convex/vex/versions.ts +0 -648
  132. package/templates/base-nextjs/src/app/admin/AdminLayoutWrapper.tsx +0 -33
  133. package/templates/base-nextjs/src/app/admin/AdminPageWrapper.tsx +0 -38
  134. package/templates/base-nextjs/src/app/admin/RichTextFieldWithMedia.tsx +0 -101
  135. package/templates/base-nextjs/src/app/admin/[[...path]]/page.tsx +0 -15
  136. package/templates/base-nextjs/src/app/admin/layout.tsx +0 -51
  137. package/templates/base-nextjs/src/app/favicon.ico +0 -0
  138. package/templates/base-nextjs/src/components/providers/theme.tsx +0 -11
  139. package/templates/base-nextjs/src/components/ui/theme-toggle.tsx +0 -67
  140. package/templates/base-nextjs/src/db/types.ts +0 -6
  141. package/templates/base-nextjs/src/vexcms/access.ts +0 -44
  142. package/templates/base-nextjs/src/vexcms/auth.ts +0 -4
  143. package/templates/base-nextjs/src/vexcms/collections/media.ts +0 -15
  144. package/templates/base-nextjs/vex.config.ts +0 -25
  145. package/templates/marketing-site/public/favicons/favicon.ico +0 -0
  146. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/PreviewPageContent.tsx +0 -55
  147. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +0 -27
  148. package/templates/marketing-site/src/app/(frontend)/preview/layout.tsx +0 -18
  149. package/templates/marketing-site/src/app/admin/AdminLayoutWrapper.tsx +0 -71
  150. package/templates/marketing-site/src/components/ThemeInjector.tsx +0 -128
  151. package/templates/marketing-site/src/components/admin/IconPickerField.tsx +0 -134
  152. package/templates/marketing-site/src/components/admin/ThemeImport.tsx +0 -174
  153. package/templates/marketing-site/src/components/admin/ThemeImportField.tsx +0 -33
  154. package/templates/marketing-site/src/db/constants/index.ts +0 -52
  155. package/templates/marketing-site/src/lib/colorConvert.ts +0 -171
  156. package/templates/marketing-site/src/vexcms/access.ts +0 -36
  157. 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
- @source "../../node_modules/@vexcms/ui/dist/**/*.js";
6
- @source "../../node_modules/@vexcms/admin-next/dist/**/*.js";
7
- @source "../../node_modules/@vexcms/admin-next/src/**/*.tsx";
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
- --color-background: var(--background);
13
- --color-foreground: var(--foreground);
14
- --font-sans: var(--font-sans);
15
- --font-mono: var(--font-geist-mono);
16
- --color-sidebar-ring: var(--sidebar-ring);
17
- --color-sidebar-border: var(--sidebar-border);
18
- --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
19
- --color-sidebar-accent: var(--sidebar-accent);
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-primary: var(--sidebar-primary);
22
- --color-sidebar-foreground: var(--sidebar-foreground);
23
- --color-sidebar: var(--sidebar);
24
- --color-chart-5: var(--chart-5);
25
- --color-chart-4: var(--chart-4);
26
- --color-chart-3: var(--chart-3);
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-ring: var(--ring);
30
- --color-input: var(--input);
31
- --color-border: var(--border);
32
- --color-destructive: var(--destructive);
33
- --color-accent-foreground: var(--accent-foreground);
34
- --color-accent: var(--accent);
35
- --color-muted-foreground: var(--muted-foreground);
36
- --color-muted: var(--muted);
37
- --color-secondary-foreground: var(--secondary-foreground);
38
- --color-secondary: var(--secondary);
39
- --color-primary-foreground: var(--primary-foreground);
40
- --color-primary: var(--primary);
41
- --color-popover-foreground: var(--popover-foreground);
42
- --color-popover: var(--popover);
43
- --color-card-foreground: var(--card-foreground);
44
- --color-card: var(--card);
45
- --radius-sm: calc(var(--radius) - 4px);
46
- --radius-md: calc(var(--radius) - 2px);
47
- --radius-lg: var(--radius);
48
- --radius-xl: calc(var(--radius) + 4px);
49
- --radius-2xl: calc(var(--radius) + 8px);
50
- --radius-3xl: calc(var(--radius) + 12px);
51
- --radius-4xl: calc(var(--radius) + 16px);
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
- --radius: 0.5rem;
56
- --background: oklch(1 0 0);
57
- --foreground: oklch(0.145 0.004 240);
58
- --card: oklch(1 0 0);
59
- --card-foreground: oklch(0.145 0.004 240);
60
- --popover: oklch(1 0 0);
61
- --popover-foreground: oklch(0.145 0.004 240);
62
- --primary: oklch(0.205 0.003 240);
63
- --primary-foreground: oklch(0.985 0 0);
64
- --secondary: oklch(0.965 0.002 240);
65
- --secondary-foreground: oklch(0.205 0.003 240);
66
- --muted: oklch(0.965 0.002 240);
67
- --muted-foreground: oklch(0.556 0.002 240);
68
- --accent: oklch(0.965 0.002 240);
69
- --accent-foreground: oklch(0.205 0.003 240);
70
- --destructive: oklch(0.577 0.184 27.3);
71
- --border: oklch(0.922 0.003 240);
72
- --input: oklch(0.922 0.003 240);
73
- --ring: oklch(0.708 0.003 240);
74
- --chart-1: oklch(0.646 0.222 41.116);
75
- --chart-2: oklch(0.6 0.118 184.714);
76
- --chart-3: oklch(0.398 0.07 227.392);
77
- --chart-4: oklch(0.828 0.189 84.429);
78
- --chart-5: oklch(0.769 0.188 70.08);
79
- --sidebar: oklch(0.985 0 0);
80
- --sidebar-foreground: oklch(0.145 0.004 240);
81
- --sidebar-primary: oklch(0.205 0.003 240);
82
- --sidebar-primary-foreground: oklch(0.985 0 0);
83
- --sidebar-accent: oklch(0.965 0.002 240);
84
- --sidebar-accent-foreground: oklch(0.205 0.003 240);
85
- --sidebar-border: oklch(0.922 0.003 240);
86
- --sidebar-ring: oklch(0.708 0.003 240);
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
- --background: oklch(0.145 0.004 240);
91
- --foreground: oklch(0.985 0 0);
92
- --card: oklch(0.145 0.004 240);
93
- --card-foreground: oklch(0.985 0 0);
94
- --popover: oklch(0.145 0.004 240);
95
- --popover-foreground: oklch(0.985 0 0);
96
- --primary: oklch(0.985 0 0);
97
- --primary-foreground: oklch(0.205 0.003 240);
98
- --secondary: oklch(0.269 0.006 240);
99
- --secondary-foreground: oklch(0.985 0 0);
100
- --muted: oklch(0.269 0.006 240);
101
- --muted-foreground: oklch(0.708 0.003 240);
102
- --accent: oklch(0.269 0.006 240);
103
- --accent-foreground: oklch(0.985 0 0);
104
- --destructive: oklch(0.396 0.141 25.768);
105
- --border: oklch(0.269 0.006 240);
106
- --input: oklch(0.269 0.006 240);
107
- --ring: oklch(0.439 0.003 240);
108
- --chart-1: oklch(0.488 0.243 264.376);
109
- --chart-2: oklch(0.696 0.17 162.48);
110
- --chart-3: oklch(0.769 0.188 70.08);
111
- --chart-4: oklch(0.627 0.265 303.9);
112
- --chart-5: oklch(0.645 0.246 16.439);
113
- --sidebar: oklch(0.205 0.003 240);
114
- --sidebar-foreground: oklch(0.985 0 0);
115
- --sidebar-primary: oklch(0.488 0.243 264.376);
116
- --sidebar-primary-foreground: oklch(0.985 0 0);
117
- --sidebar-accent: oklch(0.269 0.006 240);
118
- --sidebar-accent-foreground: oklch(0.985 0 0);
119
- --sidebar-border: oklch(0.269 0.006 240);
120
- --sidebar-ring: oklch(0.439 0.003 240);
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
- import { Geist, Geist_Mono, Inter } from "next/font/google"
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
- import "./globals.css"
10
+ const geistSans = Geist({
11
+ subsets: ["latin"],
12
+ variable: "--font-sans",
13
+ })
8
14
 
9
- const inter = Inter({ subsets: ["latin"], variable: "--font-sans" })
10
- const geistSans = Geist({ subsets: ["latin"], variable: "--font-geist-sans" })
11
- const geistMono = Geist_Mono({ subsets: ["latin"], variable: "--font-geist-mono" })
15
+ const geistMono = Geist_Mono({
16
+ subsets: ["latin"],
17
+ variable: "--font-geist-mono",
18
+ })
12
19
 
13
20
  export const metadata: Metadata = {
14
- description: "Test application for Vex CMS development",
15
- title: "Vex CMS Test App",
21
+ description: "Built with VexCMS",
22
+ icons: { icon: "/favicons/favicon.ico" },
23
+ title: "VexCMS",
16
24
  }
17
25
 
18
- export default function RootLayout({ children }: { children: React.ReactNode }) {
26
+ export default function RootLayout({
27
+ children,
28
+ }: Readonly<{
29
+ children: React.ReactNode
30
+ }>) {
19
31
  return (
20
- <html lang="en" suppressHydrationWarning className={`${inter.variable} ${geistSans.variable} ${geistMono.variable}`}>
21
- <body className="antialiased">
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, apiKeyClient } from "better-auth/client/plugins"
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
- // copied from code example, unsure if this is actually useful or not
21
- // eslint-disable-next-line @typescript-eslint/no-misused-promises, @typescript-eslint/no-empty-function
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
+ }