convex-cms 0.0.7-alpha.0 → 0.0.8

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 (134) hide show
  1. package/README.md +22 -17
  2. package/admin/src/components/BreakingChangesWarningDialog.tsx +5 -5
  3. package/admin/src/components/BulkOperationModal.tsx +14 -14
  4. package/admin/src/components/ContentEntryEditor.tsx +6 -6
  5. package/admin/src/components/ContentTypeFormModal.tsx +1 -1
  6. package/admin/src/components/Header.tsx +5 -2
  7. package/admin/src/components/SchemaDriftWarning.tsx +126 -0
  8. package/admin/src/components/TaxonomyEditor.tsx +2 -2
  9. package/admin/src/components/TermTree.tsx +3 -3
  10. package/admin/src/components/UploadDropzone.tsx +7 -7
  11. package/admin/src/components/VersionCompare.tsx +13 -13
  12. package/admin/src/components/VersionHistory.tsx +2 -2
  13. package/admin/src/components/VersionRollbackModal.tsx +5 -5
  14. package/admin/src/components/cmsds/CmsButton.tsx +2 -2
  15. package/admin/src/components/cmsds/CmsDialog.tsx +4 -1
  16. package/admin/src/components/cmsds/CmsStatusBadge.tsx +5 -5
  17. package/admin/src/components/fields/JsonField.tsx +1 -1
  18. package/admin/src/components/fields/TagField.tsx +1 -1
  19. package/admin/src/contexts/SettingsConfigContext.tsx +10 -3
  20. package/admin/src/embed/index.tsx +29 -9
  21. package/admin/src/embed/pages/ContentTypeEntries.tsx +25 -0
  22. package/admin/src/embed/pages/Entry.tsx +114 -0
  23. package/admin/src/embed/pages/Media.tsx +3 -1
  24. package/admin/src/embed/pages/NewEntry.tsx +83 -0
  25. package/admin/src/embed/pages/index.ts +3 -0
  26. package/admin/src/pages/ContentPage.tsx +16 -1
  27. package/admin/src/pages/ContentTypeEntriesPage.tsx +466 -0
  28. package/admin/src/pages/ContentTypesPage.tsx +3 -3
  29. package/admin/src/pages/DashboardPage.tsx +3 -0
  30. package/admin/src/pages/SettingsPage.tsx +4 -4
  31. package/admin/src/pages/index.ts +1 -0
  32. package/admin/src/routes/__root.tsx +10 -10
  33. package/admin/src/styles/globals.css +31 -5
  34. package/admin/src/styles/tailwind-config.css +25 -0
  35. package/admin/src/styles/theme.css +50 -0
  36. package/admin-dist/nitro.json +1 -1
  37. package/admin-dist/public/assets/{CmsEmptyState-CXVkI3FZ.js → CmsEmptyState-6-PLaXtD.js} +1 -1
  38. package/admin-dist/public/assets/{CmsPageHeader-DU9fD34s.js → CmsPageHeader-SoF4Epu9.js} +1 -1
  39. package/admin-dist/public/assets/CmsStatusBadge-D7kYaohx.js +1 -0
  40. package/admin-dist/public/assets/{CmsSurface-DF7OcKg_.js → CmsSurface-BvksBm6W.js} +1 -1
  41. package/admin-dist/public/assets/{CmsToolbar-5S8FQrSx.js → CmsToolbar-DlZPMe2B.js} +1 -1
  42. package/admin-dist/public/assets/ContentEntryEditor-C6n9xLS9.js +4 -0
  43. package/admin-dist/public/assets/{TaxonomyFilter-DEN2Q9Lo.js → TaxonomyFilter-CFX1_g8s.js} +1 -1
  44. package/admin-dist/public/assets/{_contentTypeId-Ba5iowxH.js → _contentTypeId-DTv8UoTp.js} +1 -1
  45. package/admin-dist/public/assets/_entryId-D3lr5Dvy.js +1 -0
  46. package/admin-dist/public/assets/alert-BAHTL6ao.js +1 -0
  47. package/admin-dist/public/assets/badge-oJv4Eai8.js +1 -0
  48. package/admin-dist/public/assets/{circle-check-big-B7eCOM8r.js → circle-check-big-3OHxNDhO.js} +1 -1
  49. package/admin-dist/public/assets/{command-BIjzeKOv.js → command-DwgQs69u.js} +1 -1
  50. package/admin-dist/public/assets/content-CKQ4QwW2.js +1 -0
  51. package/admin-dist/public/assets/content-types-BrttaLpc.js +1 -0
  52. package/admin-dist/public/assets/globals-CoCRjt0K.css +1 -0
  53. package/admin-dist/public/assets/index-DOkgTSx0.js +1 -0
  54. package/admin-dist/public/assets/{main-BZB1uYTH.js → main-DV6oxWnU.js} +5 -5
  55. package/admin-dist/public/assets/media-B2i-mCbx.js +1 -0
  56. package/admin-dist/public/assets/new._contentTypeId-VF63rpic.js +1 -0
  57. package/admin-dist/public/assets/{pencil-BDQ1ZWRw.js → pencil-CX1CiTDD.js} +1 -1
  58. package/admin-dist/public/assets/refresh-cw-Cm-YOeFI.js +1 -0
  59. package/admin-dist/public/assets/{rotate-ccw-BWblSIsl.js → rotate-ccw-B45JsL5f.js} +1 -1
  60. package/admin-dist/public/assets/{scroll-area-BoaB6x8v.js → scroll-area-b3A1HHR7.js} +1 -1
  61. package/admin-dist/public/assets/{search-CYMIpd39.js → search-DKKh_DdH.js} +1 -1
  62. package/admin-dist/public/assets/settings-CGVDEV1r.js +1 -0
  63. package/admin-dist/public/assets/{switch-DN7TOCa5.js → switch-BTMY8Qnk.js} +1 -1
  64. package/admin-dist/public/assets/tabs-DUQwUoIb.js +1 -0
  65. package/admin-dist/public/assets/{tanstack-adapter-DQcKErwf.js → tanstack-adapter-f7AHmQ5L.js} +1 -1
  66. package/admin-dist/public/assets/taxonomies-DvMppdiD.js +1 -0
  67. package/admin-dist/public/assets/{trash-Dp_a2mpb.js → trash-D7e0uKd9.js} +1 -1
  68. package/admin-dist/public/assets/{useBreadcrumbLabel-BQ9dJI6T.js → useBreadcrumbLabel-CF2KYwsw.js} +1 -1
  69. package/admin-dist/public/assets/{usePermissions-WUBNg_Id.js → usePermissions-DWBImEOW.js} +1 -1
  70. package/admin-dist/server/_libs/lucide-react.mjs +50 -43
  71. package/admin-dist/server/_ssr/{CmsEmptyState-DYh_PPQE.mjs → CmsEmptyState-BM8DghTl.mjs} +1 -1
  72. package/admin-dist/server/_ssr/{CmsPageHeader-BcniLh49.mjs → CmsPageHeader-BHUmrIWD.mjs} +1 -1
  73. package/admin-dist/server/_ssr/{CmsStatusBadge-BShWDxwE.mjs → CmsStatusBadge-D0Zb0oRl.mjs} +7 -7
  74. package/admin-dist/server/_ssr/{CmsSurface-CHEv-Kba.mjs → CmsSurface-B2eBr-47.mjs} +1 -1
  75. package/admin-dist/server/_ssr/{CmsToolbar-Dqqb216_.mjs → CmsToolbar-BCrwg7OL.mjs} +1 -1
  76. package/admin-dist/server/_ssr/{ContentEntryEditor-DOIAyWME.mjs → ContentEntryEditor-Cjfm0uhr.mjs} +37 -37
  77. package/admin-dist/server/_ssr/{TaxonomyFilter-BfsPAZ-Y.mjs → TaxonomyFilter-C4pD0kfM.mjs} +3 -3
  78. package/admin-dist/server/_ssr/{_contentTypeId-CPjmri90.mjs → _contentTypeId-CiDiX-p7.mjs} +11 -11
  79. package/admin-dist/server/_ssr/{_entryId-D0yu8HuP.mjs → _entryId-9GxatOkL.mjs} +11 -11
  80. package/admin-dist/server/_ssr/_tanstack-start-manifest_v-CC7UrHKE.mjs +4 -0
  81. package/admin-dist/server/_ssr/{badge-Cp61aQNc.mjs → badge-EI998zba.mjs} +1 -1
  82. package/admin-dist/server/_ssr/{command-BfjE1WJf.mjs → command-BLAWQhUw.mjs} +1 -1
  83. package/admin-dist/server/_ssr/{content-DrODe6sA.mjs → content-BHX39L4D.mjs} +31 -22
  84. package/admin-dist/server/_ssr/{content-types-BPgMwxiT.mjs → content-types-DCzrBhTH.mjs} +9 -9
  85. package/admin-dist/server/_ssr/{index-BTHmIC9W.mjs → index-DwM_5VNP.mjs} +92 -6
  86. package/admin-dist/server/_ssr/index.mjs +2 -2
  87. package/admin-dist/server/_ssr/{media-DkvBfmD9.mjs → media-CbzgTRRQ.mjs} +9 -9
  88. package/admin-dist/server/_ssr/{new._contentTypeId-Co_73sDJ.mjs → new._contentTypeId-6Ph-Gtlw.mjs} +10 -10
  89. package/admin-dist/server/_ssr/{router-CaDgRHfQ.mjs → router-vd1nySeP.mjs} +45 -35
  90. package/admin-dist/server/_ssr/{scroll-area-D3v-O_jk.mjs → scroll-area--B9snFTJ.mjs} +1 -1
  91. package/admin-dist/server/_ssr/{settings-MaEXh2Hz.mjs → settings-DlTO2JSj.mjs} +11 -11
  92. package/admin-dist/server/_ssr/{switch-DmbR03dm.mjs → switch-C05NgNW0.mjs} +1 -1
  93. package/admin-dist/server/_ssr/{tabs-5oFlAGLz.mjs → tabs-DAk2J5xy.mjs} +8 -8
  94. package/admin-dist/server/_ssr/{tanstack-adapter-DNaUioIZ.mjs → tanstack-adapter-DWbaPByn.mjs} +15 -1
  95. package/admin-dist/server/_ssr/{taxonomies-D3xMK23a.mjs → taxonomies-B8nqce6u.mjs} +12 -12
  96. package/admin-dist/server/_ssr/{trash-CNw1mtF1.mjs → trash-zdlZgpTo.mjs} +7 -7
  97. package/admin-dist/server/_ssr/{useBreadcrumbLabel-BQGjOTcy.mjs → useBreadcrumbLabel-DpEKyG1h.mjs} +1 -1
  98. package/admin-dist/server/_ssr/{usePermissions-D0qtvmNi.mjs → usePermissions-olYRd9S9.mjs} +1 -1
  99. package/admin-dist/server/index.mjs +164 -157
  100. package/dist/client/admin/contentTypes.d.ts +25 -0
  101. package/dist/client/admin/contentTypes.d.ts.map +1 -1
  102. package/dist/client/admin/contentTypes.js +212 -6
  103. package/dist/client/admin/contentTypes.js.map +1 -1
  104. package/dist/client/admin/entries.d.ts.map +1 -1
  105. package/dist/client/admin/entries.js +27 -0
  106. package/dist/client/admin/entries.js.map +1 -1
  107. package/dist/client/admin/index.d.ts +4 -0
  108. package/dist/client/admin/index.d.ts.map +1 -1
  109. package/dist/client/admin/index.js +16 -0
  110. package/dist/client/admin/index.js.map +1 -1
  111. package/dist/client/admin/types.d.ts +4 -0
  112. package/dist/client/admin/types.d.ts.map +1 -1
  113. package/dist/client/schema/defineContentType.d.ts.map +1 -1
  114. package/dist/client/schema/defineContentType.js +99 -80
  115. package/dist/client/schema/defineContentType.js.map +1 -1
  116. package/dist/component/contentTypeMutations.d.ts.map +1 -1
  117. package/dist/component/contentTypeMutations.js +5 -4
  118. package/dist/component/contentTypeMutations.js.map +1 -1
  119. package/package.json +2 -2
  120. package/admin-dist/public/assets/CmsStatusBadge-nZ9TeLBL.js +0 -1
  121. package/admin-dist/public/assets/ContentEntryEditor-BDb44eTo.js +0 -4
  122. package/admin-dist/public/assets/_entryId-OY3sLz6O.js +0 -1
  123. package/admin-dist/public/assets/alert-BbW1Q9CR.js +0 -1
  124. package/admin-dist/public/assets/badge-DdM8Eua8.js +0 -1
  125. package/admin-dist/public/assets/content-BV3YeSSW.js +0 -1
  126. package/admin-dist/public/assets/content-types-Bm4b2tf8.js +0 -1
  127. package/admin-dist/public/assets/globals-D41WzvyZ.css +0 -1
  128. package/admin-dist/public/assets/index-DnJ5Twlv.js +0 -1
  129. package/admin-dist/public/assets/media-BIMN5jXt.js +0 -1
  130. package/admin-dist/public/assets/new._contentTypeId-DTWb8ZDl.js +0 -1
  131. package/admin-dist/public/assets/settings-DaNDUtr5.js +0 -1
  132. package/admin-dist/public/assets/tabs-RN__emeJ.js +0 -1
  133. package/admin-dist/public/assets/taxonomies-DylY9HE1.js +0 -1
  134. package/admin-dist/server/_ssr/_tanstack-start-manifest_v-DgCpSt_y.mjs +0 -4
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://badge.fury.io/js/convex-cms.svg)](https://www.npmjs.com/package/convex-cms)
4
4
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
5
5
 
6
- > **Alpha (v0.0.6)** Actively developed. APIs may change. [Report issues](https://github.com/obkaro/convex-cms/issues).
6
+ > **Alpha (v0.0.7)** Actively developed. APIs may change. [Report issues](https://github.com/obkaro/convex-cms/issues).
7
7
 
8
8
  A headless CMS built as a [Convex Component](https://docs.convex.dev/components). Content management that runs inside your Convex app.
9
9
 
@@ -20,14 +20,14 @@ pnpm add convex-cms
20
20
  ```typescript
21
21
  // convex/convex.config.ts
22
22
  import { defineApp } from "convex/server";
23
- import convexCms from "convex-cms/convex.config";
23
+ import cms from "convex-cms/convex.config";
24
24
 
25
25
  const app = defineApp();
26
- app.use(convexCms);
26
+ app.use(cms);
27
27
  export default app;
28
28
  ```
29
29
 
30
- ### 3. Choose Your Setup
30
+ ### 3. Initialize
31
31
 
32
32
  **For Admin UI:** Run `pnpm convex-cms init` then `pnpm convex-cms admin`
33
33
  → [Full Admin UI Setup](./docs/guides/admin-ui-setup.md)
@@ -37,13 +37,15 @@ export default app;
37
37
 
38
38
  ## Why Convex CMS?
39
39
 
40
- If you're building on Convex and need content management, this is the most integrated option:
40
+ If you're building on Convex and need content management without the overhead of all the CMS plumbing, this is the most integrated option.
41
41
 
42
- - **Zero infrastructure.** Runs entirely within your Convex deployment
43
- - **True real-time.** Content updates via Convex subscriptions, not polling
44
- - **Type-safe.** Code-first schemas with full TypeScript inference
45
- - **Component isolation.** Separate database tables, versioned independently
46
- - **Agent-native.** 23 pre-built tools for AI agent integration via `@convex-dev/agent`
42
+ ### What you get:
43
+
44
+ - **Typesafe admin API:** Admin APIs exported directly from your backend for use in your React queries and mutations
45
+ - **Built in admin UI:** A well designed admin UI that you can view and edit content from
46
+ - **Embeddable content manager:** Ability to embed and serve the prebuilt UI as part of you React application
47
+ - **Data independence:** CMS that lives in your own convex deployment, extendable and customizable with your convex functions
48
+ - **Agent-native content management:** Pre-built tools useful for AI agent integration with `@convex-dev/agent`
47
49
 
48
50
  ## Features
49
51
 
@@ -51,13 +53,11 @@ If you're building on Convex and need content management, this is the most integ
51
53
  |---------|--------------|
52
54
  | **Code-first config** | Define content types in TypeScript with full type inference |
53
55
  | **UI-defined config** | Create and modify content types through the admin interface |
54
- | **CMS Client** | Programmatic access via `createCmsClient` for custom queries and mutations |
55
- | **Admin API** | Pre-built functions via `defineAdminAPI` that power the admin UI |
56
- | **CLI Admin UI** | Run `npx convex-cms admin` for local development |
56
+ | **CMS Client** | Programmatic access for custom queries and mutations |
57
+ | **Admin API** | Pre-built functions that power the admin UI |
58
+ | **CLI Admin UI** | Run for local development, content entry, and management |
57
59
  | **Embedded Admin UI** | Ship the admin interface as part of your React app |
58
60
 
59
- Any combination of these features works together seamlessly. Pick what fits your workflow.
60
-
61
61
  ## In Practice
62
62
 
63
63
  **Full control over the editorial experience?**
@@ -70,9 +70,14 @@ Code-first config + Admin API + Embedded Admin UI. Type-safe schemas with a work
70
70
  UI-defined config + Admin API + Embedded Admin UI. Non-developers can add fields and content types.
71
71
 
72
72
  **Automated content pipelines?**
73
- CMS Client + agent tools. 23 pre-built tools for AI-driven workflows.
73
+ CMS Client + agent tools. Pre-built tools for AI-driven workflows.
74
+
75
+ *Any combination of these features works together seamlessly. Pick what fits your workflow.*
76
+
77
+
78
+ ## Batteries Included
74
79
 
75
- ## What's Included
80
+ Leverage included features or extend and customize within your own convex functions to your desire.
76
81
 
77
82
  ### Core Content
78
83
  - **13 field types.** text, richText, number, boolean, date, datetime, select, multiSelect, reference, media, json, tags, category
@@ -42,13 +42,13 @@ export function BreakingChangesWarningDialog({
42
42
  }
43
43
  >
44
44
  <div className="space-y-4">
45
- <div className="flex items-start gap-3 rounded-lg border border-amber-200 bg-amber-50 p-3">
46
- <AlertTriangle className="mt-0.5 size-5 shrink-0 text-amber-600" />
45
+ <div className="diff-modified flex items-start gap-3 rounded-lg border p-3">
46
+ <AlertTriangle className="mt-0.5 size-5 shrink-0 text-diff-modified" />
47
47
  <div className="space-y-1">
48
- <p className="text-sm font-medium text-amber-800">
48
+ <p className="text-sm font-medium text-diff-modified">
49
49
  These changes may affect existing content
50
50
  </p>
51
- <p className="text-sm text-amber-700">
51
+ <p className="text-sm text-diff-modified-foreground">
52
52
  The following changes could cause data loss or validation errors for existing entries.
53
53
  Review carefully before proceeding.
54
54
  </p>
@@ -65,7 +65,7 @@ export function BreakingChangesWarningDialog({
65
65
  key={index}
66
66
  className="flex items-start gap-2 rounded-md border bg-muted/30 px-3 py-2 text-sm"
67
67
  >
68
- <span className="mt-0.5 size-1.5 shrink-0 rounded-full bg-amber-500" />
68
+ <span className="mt-0.5 size-1.5 shrink-0 rounded-full bg-warning" />
69
69
  <span className="text-muted-foreground">{change}</span>
70
70
  </li>
71
71
  ))}
@@ -102,7 +102,7 @@ export function BulkOperationModal({
102
102
  <div className="space-y-4">
103
103
  {result.failed === 0 ? (
104
104
  <div className="flex flex-col items-center gap-3 py-4 text-center">
105
- <div className="flex size-12 items-center justify-center rounded-full bg-emerald-100 text-emerald-600">
105
+ <div className="flex size-12 items-center justify-center rounded-full bg-diff-added-bg text-diff-added">
106
106
  <CheckCircle className="size-6" />
107
107
  </div>
108
108
  <p className="text-sm text-muted-foreground">
@@ -116,33 +116,33 @@ export function BulkOperationModal({
116
116
  ) : (
117
117
  <div className="space-y-4">
118
118
  <div className="grid grid-cols-2 gap-4">
119
- <div className="rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-center">
120
- <p className="text-2xl font-semibold text-emerald-700">
119
+ <div className="diff-added rounded-lg border p-3 text-center">
120
+ <p className="text-2xl font-semibold text-diff-added">
121
121
  {result.succeeded}
122
122
  </p>
123
- <p className="text-xs text-emerald-600">Succeeded</p>
123
+ <p className="text-xs text-diff-added-foreground">Succeeded</p>
124
124
  </div>
125
- <div className="rounded-lg border border-red-200 bg-red-50 p-3 text-center">
126
- <p className="text-2xl font-semibold text-red-700">
125
+ <div className="diff-removed rounded-lg border p-3 text-center">
126
+ <p className="text-2xl font-semibold text-diff-removed">
127
127
  {result.failed}
128
128
  </p>
129
- <p className="text-xs text-red-600">Failed</p>
129
+ <p className="text-xs text-diff-removed-foreground">Failed</p>
130
130
  </div>
131
131
  </div>
132
132
 
133
133
  {result.errors && result.errors.length > 0 && (
134
- <div className="rounded-lg border border-amber-200 bg-amber-50 p-3">
135
- <p className="mb-2 text-sm font-medium text-amber-800">
134
+ <div className="diff-modified rounded-lg border p-3">
135
+ <p className="mb-2 text-sm font-medium text-diff-modified">
136
136
  Errors:
137
137
  </p>
138
- <ul className="space-y-1 text-xs text-amber-700">
138
+ <ul className="space-y-1 text-xs text-diff-modified-foreground">
139
139
  {result.errors.slice(0, 5).map((error, index) => (
140
140
  <li key={index} className="truncate">
141
141
  • {error}
142
142
  </li>
143
143
  ))}
144
144
  {result.errors.length > 5 && (
145
- <li className="text-amber-600">
145
+ <li className="text-diff-modified-foreground/80">
146
146
  ...and {result.errors.length - 5} more errors
147
147
  </li>
148
148
  )}
@@ -163,9 +163,9 @@ export function BulkOperationModal({
163
163
  </p>
164
164
  <p className="text-sm text-muted-foreground">{config.description}</p>
165
165
  {config.warning && (
166
- <div className="flex items-start gap-2 rounded-lg border border-amber-200 bg-amber-50 p-3">
167
- <AlertTriangle className="mt-0.5 size-4 shrink-0 text-amber-600" />
168
- <p className="text-sm text-amber-800">
166
+ <div className="diff-modified flex items-start gap-2 rounded-lg border p-3">
167
+ <AlertTriangle className="mt-0.5 size-4 shrink-0 text-diff-modified" />
168
+ <p className="text-sm">
169
169
  <span className="font-medium">Note:</span> {config.warning}
170
170
  </p>
171
171
  </div>
@@ -725,8 +725,8 @@ export function ContentEntryEditor({
725
725
  className={cn(
726
726
  'flex items-center gap-1.5 text-sm',
727
727
  autosaveStatus === 'saving' && 'text-muted-foreground',
728
- autosaveStatus === 'saved' && 'text-emerald-600',
729
- autosaveStatus === 'error' && 'text-red-600'
728
+ autosaveStatus === 'saved' && 'text-success',
729
+ autosaveStatus === 'error' && 'text-destructive'
730
730
  )}
731
731
  data-testid="autosave-status"
732
732
  >
@@ -755,7 +755,7 @@ export function ContentEntryEditor({
755
755
  )}
756
756
 
757
757
  {isDirty && (
758
- <span className="text-sm text-amber-600">Unsaved changes</span>
758
+ <span className="text-sm text-warning">Unsaved changes</span>
759
759
  )}
760
760
  </div>
761
761
  </div>
@@ -763,7 +763,7 @@ export function ContentEntryEditor({
763
763
  {/* Success/Error Messages */}
764
764
  {saveSuccess && (
765
765
  <div
766
- className="flex items-center gap-2 rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800"
766
+ className="diff-added flex items-center gap-2 rounded-lg border px-4 py-3 text-sm"
767
767
  role="status"
768
768
  >
769
769
  <CheckCircle className="size-4" />
@@ -773,7 +773,7 @@ export function ContentEntryEditor({
773
773
 
774
774
  {(submitError || publishError) && (
775
775
  <div
776
- className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800"
776
+ className="diff-removed rounded-lg border px-4 py-3 text-sm"
777
777
  role="alert"
778
778
  >
779
779
  <span className="font-medium">Error:</span> {submitError || publishError}
@@ -886,7 +886,7 @@ export function ContentEntryEditor({
886
886
  entry.status === 'scheduled' &&
887
887
  entry.scheduledPublishAt && (
888
888
  <span
889
- className="flex items-center gap-1 text-xs text-blue-600"
889
+ className="flex items-center gap-1 text-xs text-info"
890
890
  data-testid="scheduled-time"
891
891
  >
892
892
  <Clock className="size-3" />
@@ -940,7 +940,7 @@ export function ContentTypeFormModal({
940
940
 
941
941
  {submitError && (
942
942
  <div
943
- className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800"
943
+ className="diff-removed rounded-lg border px-3 py-2 text-sm"
944
944
  role="alert"
945
945
  data-testid="submit-error"
946
946
  >
@@ -78,11 +78,14 @@ function getBreadcrumbs(
78
78
  ): BreadcrumbData[] {
79
79
  const breadcrumbs: BreadcrumbData[] = [{ label: appName, to: '/' }]
80
80
 
81
- if (pathname === '/') {
81
+ // Decode pathname to match non-encoded override keys
82
+ const decodedPathname = decodeURIComponent(pathname)
83
+
84
+ if (decodedPathname === '/') {
82
85
  return breadcrumbs
83
86
  }
84
87
 
85
- const segments = pathname.split('/').filter(Boolean)
88
+ const segments = decodedPathname.split('/').filter(Boolean)
86
89
  let currentPath = ''
87
90
 
88
91
  segments.forEach((segment, index) => {
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Schema Drift Warning Component
3
+ *
4
+ * Displays a warning banner when code-defined content types
5
+ * are out of sync with the database schema.
6
+ */
7
+
8
+ import { useQuery, useMutation } from "convex/react";
9
+ import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
10
+ import { Button } from "~/components/ui/button";
11
+ import { AlertTriangle, RefreshCw, XCircle, AlertCircle } from "lucide-react";
12
+ import type { CmsAdminApi } from "~/embed/contexts/ApiContext";
13
+ import { useState } from "react";
14
+
15
+ interface DriftIssue {
16
+ type: string;
17
+ severity: "error" | "warning" | "info";
18
+ contentTypeName: string;
19
+ fieldName?: string;
20
+ message: string;
21
+ }
22
+
23
+ interface SchemaDriftWarningProps {
24
+ api: CmsAdminApi;
25
+ }
26
+
27
+ export function SchemaDriftWarning({ api }: SchemaDriftWarningProps) {
28
+ const driftResults = useQuery(api.checkSchemaDrift, {});
29
+ const syncMutation = useMutation(api.syncCodeDefinedTypes);
30
+ const [isSyncing, setIsSyncing] = useState(false);
31
+ const [syncResult, setSyncResult] = useState<{
32
+ created: number;
33
+ updated: number;
34
+ } | null>(null);
35
+
36
+ if (driftResults === undefined) {
37
+ return null;
38
+ }
39
+
40
+ if (!driftResults || driftResults.length === 0) {
41
+ if (syncResult && (syncResult.created > 0 || syncResult.updated > 0)) {
42
+ return (
43
+ <Alert className="border-success/50 bg-success/10">
44
+ <AlertCircle className="size-4 text-success" />
45
+ <AlertTitle className="text-diff-added-foreground">
46
+ Sync Complete
47
+ </AlertTitle>
48
+ <AlertDescription className="text-diff-added-foreground/80">
49
+ {syncResult.created > 0 && (
50
+ <span>{syncResult.created} content type(s) created. </span>
51
+ )}
52
+ {syncResult.updated > 0 && (
53
+ <span>{syncResult.updated} content type(s) updated.</span>
54
+ )}
55
+ </AlertDescription>
56
+ </Alert>
57
+ );
58
+ }
59
+ return null;
60
+ }
61
+
62
+ const errors = driftResults.filter((d: DriftIssue) => d.severity === "error");
63
+ const warnings = driftResults.filter((d: DriftIssue) => d.severity === "warning");
64
+
65
+ const handleSync = async () => {
66
+ setIsSyncing(true);
67
+ setSyncResult(null);
68
+ try {
69
+ const result = await syncMutation({});
70
+ setSyncResult(result);
71
+ } catch (error) {
72
+ console.error("Sync failed:", error);
73
+ } finally {
74
+ setIsSyncing(false);
75
+ }
76
+ };
77
+
78
+ const variant = errors.length > 0 ? "destructive" : "default";
79
+ const Icon = errors.length > 0 ? XCircle : AlertTriangle;
80
+ const iconColor =
81
+ errors.length > 0 ? "text-destructive" : "text-warning";
82
+
83
+ return (
84
+ <Alert variant={variant} className={errors.length === 0 ? "border-warning/50 bg-warning/10" : ""}>
85
+ <Icon className={`size-4 ${iconColor}`} />
86
+ <AlertTitle className={errors.length === 0 ? "text-diff-modified-foreground" : ""}>
87
+ Schema Drift Detected
88
+ </AlertTitle>
89
+ <AlertDescription className={errors.length === 0 ? "text-diff-modified-foreground/80" : ""}>
90
+ <div className="space-y-2">
91
+ {errors.length > 0 && (
92
+ <p>
93
+ <strong>{errors.length} error(s)</strong>:{" "}
94
+ {errors
95
+ .slice(0, 3)
96
+ .map((e: DriftIssue) => e.message)
97
+ .join(" ")}
98
+ {errors.length > 3 && ` ...and ${errors.length - 3} more`}
99
+ </p>
100
+ )}
101
+ {warnings.length > 0 && (
102
+ <p>
103
+ <strong>{warnings.length} warning(s)</strong>: Code-defined types
104
+ may need syncing.
105
+ </p>
106
+ )}
107
+ <div className="mt-3 flex items-center gap-2">
108
+ <Button
109
+ size="sm"
110
+ variant={errors.length > 0 ? "destructive" : "outline"}
111
+ onClick={handleSync}
112
+ disabled={isSyncing}
113
+ className={errors.length === 0 ? "border-warning/50 text-diff-modified-foreground hover:bg-warning/20" : ""}
114
+ >
115
+ <RefreshCw className={`mr-1.5 size-3.5 ${isSyncing ? "animate-spin" : ""}`} />
116
+ {isSyncing ? "Syncing..." : "Sync Now"}
117
+ </Button>
118
+ <span className="text-xs opacity-75">
119
+ Syncs code-defined types to the database
120
+ </span>
121
+ </div>
122
+ </div>
123
+ </AlertDescription>
124
+ </Alert>
125
+ );
126
+ }
@@ -187,7 +187,7 @@ export function TaxonomyEditor({
187
187
  >
188
188
  <form onSubmit={handleSubmit} className="space-y-4">
189
189
  {submitError && (
190
- <div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800">
190
+ <div className="diff-removed rounded-lg border px-3 py-2 text-sm">
191
191
  {submitError}
192
192
  </div>
193
193
  )}
@@ -294,7 +294,7 @@ export function TaxonomyEditor({
294
294
  Terms can have parent-child relationships (like categories)
295
295
  </p>
296
296
  {isEditing && (
297
- <p className="text-xs text-amber-600">
297
+ <p className="text-xs text-warning">
298
298
  Hierarchy type cannot be changed after creation
299
299
  </p>
300
300
  )}
@@ -196,12 +196,12 @@ export function TermTree({
196
196
  </div>
197
197
 
198
198
  {error && (
199
- <div className="flex items-center justify-between rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800">
199
+ <div className="diff-removed flex items-center justify-between rounded-lg border px-3 py-2 text-sm">
200
200
  {error}
201
201
  <button
202
202
  type="button"
203
203
  onClick={() => setError(null)}
204
- className="text-red-600 hover:text-red-800"
204
+ className="text-diff-removed hover:text-diff-removed-foreground"
205
205
  >
206
206
  ×
207
207
  </button>
@@ -436,7 +436,7 @@ function TermEditModal({
436
436
  >
437
437
  <form onSubmit={handleSubmit} className="space-y-4">
438
438
  {submitError && (
439
- <div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800">
439
+ <div className="diff-removed rounded-lg border px-3 py-2 text-sm">
440
440
  {submitError}
441
441
  </div>
442
442
  )}
@@ -158,10 +158,10 @@ export function UploadDropzone({
158
158
  const getStatusIcon = (status: UploadQueueFileStatus) => {
159
159
  switch (status) {
160
160
  case 'complete':
161
- return <Check className="size-4 text-emerald-500" />
161
+ return <Check className="size-4 text-success" />
162
162
  case 'error':
163
163
  case 'cancelled':
164
- return <X className="size-4 text-red-500" />
164
+ return <X className="size-4 text-destructive" />
165
165
  case 'uploading':
166
166
  return (
167
167
  <div className="size-4 animate-spin rounded-full border-2 border-muted border-t-primary" />
@@ -247,10 +247,10 @@ export function UploadDropzone({
247
247
  {!queue.isUploading && hasCompletedOrFailed && (
248
248
  <div className="flex items-center gap-2">
249
249
  {completedCount > 0 && (
250
- <span className="text-emerald-600">{completedCount} completed</span>
250
+ <span className="text-success">{completedCount} completed</span>
251
251
  )}
252
252
  {errorCount > 0 && (
253
- <span className="text-red-500">{errorCount} failed</span>
253
+ <span className="text-destructive">{errorCount} failed</span>
254
254
  )}
255
255
  </div>
256
256
  )}
@@ -293,8 +293,8 @@ export function UploadDropzone({
293
293
  key={uploadFile.id}
294
294
  className={cn(
295
295
  'flex items-center gap-3 rounded-lg border bg-card p-3',
296
- uploadFile.status === 'error' && 'border-red-200 bg-red-50 dark:border-red-900 dark:bg-red-950/20',
297
- uploadFile.status === 'complete' && 'border-emerald-200 bg-emerald-50 dark:border-emerald-900 dark:bg-emerald-950/20'
296
+ uploadFile.status === 'error' && 'border-diff-removed-border bg-diff-removed-bg',
297
+ uploadFile.status === 'complete' && 'border-diff-added-border bg-diff-added-bg'
298
298
  )}
299
299
  >
300
300
  <div className="shrink-0">{getStatusIcon(uploadFile.status)}</div>
@@ -311,7 +311,7 @@ export function UploadDropzone({
311
311
  {formatFileSize(uploadFile.file.size)}
312
312
  </span>
313
313
  {uploadFile.error && (
314
- <span className="text-xs text-red-500">{uploadFile.error}</span>
314
+ <span className="text-xs text-destructive">{uploadFile.error}</span>
315
315
  )}
316
316
  </div>
317
317
  </div>
@@ -82,11 +82,11 @@ export function VersionCompare({
82
82
  const getChangeStyles = (changeType: string) => {
83
83
  switch (changeType) {
84
84
  case 'added':
85
- return 'border-emerald-200 bg-emerald-50'
85
+ return 'diff-added border'
86
86
  case 'removed':
87
- return 'border-red-200 bg-red-50'
87
+ return 'diff-removed border'
88
88
  case 'modified':
89
- return 'border-amber-200 bg-amber-50'
89
+ return 'diff-modified border'
90
90
  default:
91
91
  return 'border-border bg-card'
92
92
  }
@@ -95,11 +95,11 @@ export function VersionCompare({
95
95
  const getChangeIconStyles = (changeType: string) => {
96
96
  switch (changeType) {
97
97
  case 'added':
98
- return 'bg-emerald-100 text-emerald-700'
98
+ return 'diff-icon-added'
99
99
  case 'removed':
100
- return 'bg-red-100 text-red-700'
100
+ return 'diff-icon-removed'
101
101
  case 'modified':
102
- return 'bg-amber-100 text-amber-700'
102
+ return 'diff-icon-modified'
103
103
  default:
104
104
  return 'bg-muted text-muted-foreground'
105
105
  }
@@ -144,7 +144,7 @@ export function VersionCompare({
144
144
  </p>
145
145
  </div>
146
146
  ) : !comparison ? (
147
- <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
147
+ <div className="diff-removed rounded-lg border px-4 py-3 text-sm">
148
148
  Could not load version comparison
149
149
  </div>
150
150
  ) : !comparison.hasChanges ? (
@@ -209,21 +209,21 @@ export function VersionCompare({
209
209
 
210
210
  <div className="mt-2 space-y-2">
211
211
  {change.changeType !== 'added' && (
212
- <div className="rounded border border-red-200 bg-white p-2">
213
- <p className="mb-1 text-xs font-medium text-red-700">
212
+ <div className="rounded border border-diff-removed-border bg-card p-2">
213
+ <p className="mb-1 text-xs font-medium text-diff-removed">
214
214
  Before:
215
215
  </p>
216
- <pre className="overflow-x-auto whitespace-pre-wrap break-words font-mono text-xs text-red-900">
216
+ <pre className="overflow-x-auto whitespace-pre-wrap break-words font-mono text-xs text-diff-removed-foreground">
217
217
  {formatValue(change.fromValue)}
218
218
  </pre>
219
219
  </div>
220
220
  )}
221
221
  {change.changeType !== 'removed' && (
222
- <div className="rounded border border-emerald-200 bg-white p-2">
223
- <p className="mb-1 text-xs font-medium text-emerald-700">
222
+ <div className="rounded border border-diff-added-border bg-card p-2">
223
+ <p className="mb-1 text-xs font-medium text-diff-added">
224
224
  After:
225
225
  </p>
226
- <pre className="overflow-x-auto whitespace-pre-wrap break-words font-mono text-xs text-emerald-900">
226
+ <pre className="overflow-x-auto whitespace-pre-wrap break-words font-mono text-xs text-diff-added-foreground">
227
227
  {formatValue(change.toValue)}
228
228
  </pre>
229
229
  </div>
@@ -149,7 +149,7 @@ export function VersionHistory({
149
149
  </div>
150
150
 
151
151
  {rollbackSuccess && (
152
- <div className="flex items-center gap-2 border-b bg-emerald-50 px-4 py-2 text-sm text-emerald-800">
152
+ <div className="flex items-center gap-2 border-b bg-diff-added-bg px-4 py-2 text-sm text-diff-added-foreground">
153
153
  <CheckCircle className="size-4" />
154
154
  Successfully rolled back to previous version
155
155
  </div>
@@ -198,7 +198,7 @@ export function VersionHistory({
198
198
  {version.wasPublished && (
199
199
  <Badge
200
200
  variant="outline"
201
- className="border-emerald-500/50 text-emerald-600"
201
+ className="border-success/50 text-success"
202
202
  >
203
203
  Published
204
204
  </Badge>
@@ -49,14 +49,14 @@ export function VersionRollbackModal({
49
49
  .
50
50
  </p>
51
51
 
52
- <div className="rounded-lg border border-amber-200 bg-amber-50 p-3">
52
+ <div className="diff-modified rounded-lg border p-3">
53
53
  <div className="flex gap-2">
54
- <AlertTriangle className="mt-0.5 size-4 shrink-0 text-amber-600" />
54
+ <AlertTriangle className="mt-0.5 size-4 shrink-0 text-diff-modified" />
55
55
  <div className="space-y-2">
56
- <p className="text-sm font-medium text-amber-800">
56
+ <p className="text-sm font-medium text-diff-modified">
57
57
  This action will:
58
58
  </p>
59
- <ul className="space-y-1 text-sm text-amber-700">
59
+ <ul className="space-y-1 text-sm text-diff-modified-foreground">
60
60
  <li>
61
61
  • Create a new version with the content from version{' '}
62
62
  {targetVersion}
@@ -69,7 +69,7 @@ export function VersionRollbackModal({
69
69
  </div>
70
70
 
71
71
  {error && (
72
- <div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800">
72
+ <div className="diff-removed rounded-lg border px-3 py-2 text-sm">
73
73
  <span className="font-medium">Error:</span> {error}
74
74
  </div>
75
75
  )}
@@ -31,9 +31,9 @@ const variantMap = {
31
31
 
32
32
  const customVariantClasses = {
33
33
  success:
34
- 'bg-emerald-600 text-white hover:bg-emerald-700 focus-visible:ring-emerald-500',
34
+ 'bg-success text-success-foreground hover:bg-success/90 focus-visible:ring-success',
35
35
  warning:
36
- 'bg-amber-500 text-white hover:bg-amber-600 focus-visible:ring-amber-500',
36
+ 'bg-warning text-warning-foreground hover:bg-warning/90 focus-visible:ring-warning',
37
37
  } as Record<string, string>
38
38
 
39
39
  const LoadingSpinner = () => (
@@ -52,7 +52,10 @@ export function CmsDialog({
52
52
  <DialogTitle>{title}</DialogTitle>
53
53
  {description && <DialogDescription>{description}</DialogDescription>}
54
54
  </DialogHeader>
55
- <div className="min-h-0 flex-1 py-4 scrollbar-none">
55
+ <div
56
+ className="min-h-0 flex-1 overflow-y-auto py-4 [&::-webkit-scrollbar]:hidden"
57
+ style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
58
+ >
56
59
  {children}
57
60
  </div>
58
61
  {footer && <DialogFooter className="shrink-0 border-t pt-4">{footer}</DialogFooter>}
@@ -92,12 +92,12 @@ const statusConfig: Record<
92
92
 
93
93
  const colorToClassName: Record<WorkflowStateColor, string> = {
94
94
  gray: "bg-muted text-muted-foreground",
95
- yellow: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400",
96
- blue: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400",
97
- green: "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400",
98
- red: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400",
95
+ yellow: "bg-diff-modified-bg text-diff-modified-foreground",
96
+ blue: "bg-info-bg text-info-foreground",
97
+ green: "bg-diff-added-bg text-diff-added-foreground",
98
+ red: "bg-diff-removed-bg text-diff-removed-foreground",
99
99
  purple: "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400",
100
- orange: "bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400",
100
+ orange: "bg-diff-modified-bg text-diff-modified-foreground",
101
101
  };
102
102
 
103
103
  function getDefaultIcon() {
@@ -160,7 +160,7 @@ export function JsonField({
160
160
  </span>
161
161
  )}
162
162
  {!syntaxError && textValue.trim() !== '' && (
163
- <span className="flex items-center gap-1 text-emerald-600 dark:text-emerald-400">
163
+ <span className="flex items-center gap-1 text-success">
164
164
  <Check className="size-3" />
165
165
  Valid JSON
166
166
  </span>