convex-cms 0.0.9-alpha.7 → 0.0.9-alpha.9

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 (87) hide show
  1. package/README.md +27 -0
  2. package/admin/src/components/cmsds/CmsFilterBar.tsx +74 -0
  3. package/admin/src/components/cmsds/CmsInput.tsx +24 -0
  4. package/admin/src/components/cmsds/CmsPagination.tsx +79 -0
  5. package/admin/src/components/cmsds/CmsSelect.tsx +59 -0
  6. package/admin/src/components/cmsds/CmsStatCard.tsx +79 -0
  7. package/admin/src/components/cmsds/CmsStatusBadge.tsx +1 -1
  8. package/admin/src/components/cmsds/index.ts +5 -0
  9. package/admin/src/contexts/ThemeContext.tsx +85 -17
  10. package/admin/src/embed/components/EmbedHeader.tsx +11 -9
  11. package/admin/src/embed/index.tsx +10 -6
  12. package/admin/src/embed/types.ts +12 -0
  13. package/admin/src/pages/ContentPage.tsx +116 -172
  14. package/admin/src/pages/ContentTypeEntriesPage.tsx +120 -194
  15. package/admin/src/pages/ContentTypesPage.tsx +136 -139
  16. package/admin/src/pages/DashboardPage.tsx +13 -52
  17. package/admin/src/pages/MediaPage.tsx +31 -57
  18. package/admin/src/pages/SettingsPage.tsx +5 -1
  19. package/admin/src/pages/TrashPage.tsx +115 -170
  20. package/admin/src/styles/globals.css +18 -31
  21. package/admin/src/styles/tailwind-config.css +12 -0
  22. package/admin/src/styles/theme.css +299 -38
  23. package/admin-dist/nitro.json +1 -1
  24. package/admin-dist/public/assets/{CmsEmptyState-gxhf-b6F.js → CmsEmptyState-DTlpzjOI.js} +1 -1
  25. package/admin-dist/public/assets/{CmsPageHeader-equV7Sd9.js → CmsPageHeader-0REGRH4X.js} +1 -1
  26. package/admin-dist/public/assets/{CmsStatusBadge-DQAslyW4.js → CmsStatusBadge-D_n8u8xa.js} +1 -1
  27. package/admin-dist/public/assets/{CmsSurface-DdC_aGB5.js → CmsSurface-BHmvNai4.js} +1 -1
  28. package/admin-dist/public/assets/{CmsToolbar-Crleacii.js → CmsToolbar-CY6GV2L8.js} +1 -1
  29. package/admin-dist/public/assets/{ContentEntryEditor-RmtIo3lE.js → ContentEntryEditor-CRgcRkk5.js} +1 -1
  30. package/admin-dist/public/assets/{TaxonomyFilter-BsoK90hw.js → TaxonomyFilter-Ohv5Jg9c.js} +1 -1
  31. package/admin-dist/public/assets/{_contentTypeId-Bn2ItET5.js → _contentTypeId-C_vJq22X.js} +1 -1
  32. package/admin-dist/public/assets/{_entryId-CkZWLvOZ.js → _entryId-jPXz4z9T.js} +1 -1
  33. package/admin-dist/public/assets/{alert-C7q0k4u0.js → alert-CG97cMfC.js} +1 -1
  34. package/admin-dist/public/assets/{badge-DiaAY1It.js → badge-C6qt24oj.js} +1 -1
  35. package/admin-dist/public/assets/{circle-check-big-Bl0y10am.js → circle-check-big-PltpxuB1.js} +1 -1
  36. package/admin-dist/public/assets/{command-QyTDg7pa.js → command-CJ8i86fd.js} +1 -1
  37. package/admin-dist/public/assets/{content-D868GT7T.js → content-pKaIL2ru.js} +1 -1
  38. package/admin-dist/public/assets/{content-types-DD7fJA5i.js → content-types-Bl_8I1Re.js} +1 -1
  39. package/admin-dist/public/assets/{index-CMnzrG_D.js → index-CtHq_P5q.js} +1 -1
  40. package/admin-dist/public/assets/{main-DWSY6jZL.js → main-CA-4LyFT.js} +2 -2
  41. package/admin-dist/public/assets/{media-aqxopgtw.js → media-Bl1tBbJQ.js} +1 -1
  42. package/admin-dist/public/assets/{new._contentTypeId-9ji3Hibs.js → new._contentTypeId-qsvo01mH.js} +1 -1
  43. package/admin-dist/public/assets/{pencil-D8GqMaV3.js → pencil-gAL0R34f.js} +1 -1
  44. package/admin-dist/public/assets/{refresh-cw-JipRPLLT.js → refresh-cw-sdVUGJNs.js} +1 -1
  45. package/admin-dist/public/assets/{rotate-ccw-CK11hP79.js → rotate-ccw-6OcXCcxb.js} +1 -1
  46. package/admin-dist/public/assets/{scroll-area-CJS1P20j.js → scroll-area-CJBhf9pf.js} +1 -1
  47. package/admin-dist/public/assets/{search-BT8HTHxb.js → search-WXp6KxDJ.js} +1 -1
  48. package/admin-dist/public/assets/settings-D8crrFCn.js +1 -0
  49. package/admin-dist/public/assets/{switch-Cb-ecsrJ.js → switch-Ck9ecqEX.js} +1 -1
  50. package/admin-dist/public/assets/{tabs-CFEXN2p7.js → tabs-vQYu8rjC.js} +1 -1
  51. package/admin-dist/public/assets/{tanstack-adapter-CGxC-fmP.js → tanstack-adapter-BRt2CUCw.js} +1 -1
  52. package/admin-dist/public/assets/{taxonomies-C21Z8CBa.js → taxonomies-DvILUNvr.js} +1 -1
  53. package/admin-dist/public/assets/{trash-CMRJlzc0.js → trash-YyYaC3L9.js} +1 -1
  54. package/admin-dist/public/assets/{useBreadcrumbLabel-ZZFYdqzi.js → useBreadcrumbLabel-tlSh7dtO.js} +1 -1
  55. package/admin-dist/public/assets/{usePermissions-C2FRye75.js → usePermissions-BTGdTOJS.js} +1 -1
  56. package/admin-dist/server/_ssr/{CmsEmptyState-DWqt3y_O.mjs → CmsEmptyState-CB6e53i5.mjs} +1 -1
  57. package/admin-dist/server/_ssr/{CmsPageHeader-BuN0dOPA.mjs → CmsPageHeader-COUHuECp.mjs} +1 -1
  58. package/admin-dist/server/_ssr/{CmsStatusBadge-CV35-X_8.mjs → CmsStatusBadge-kMTL6koE.mjs} +2 -2
  59. package/admin-dist/server/_ssr/{CmsSurface-DEcWf_aJ.mjs → CmsSurface-D1HDYjRg.mjs} +1 -1
  60. package/admin-dist/server/_ssr/{CmsToolbar-BMBEZVgb.mjs → CmsToolbar-NB014hsd.mjs} +1 -1
  61. package/admin-dist/server/_ssr/{ContentEntryEditor-Db9Sy_0y.mjs → ContentEntryEditor-Bq8FR_uK.mjs} +8 -8
  62. package/admin-dist/server/_ssr/{TaxonomyFilter-D_xDfC8t.mjs → TaxonomyFilter-bm_p4ADg.mjs} +3 -3
  63. package/admin-dist/server/_ssr/{_contentTypeId-HZlfcQi-.mjs → _contentTypeId-B7obLmi_.mjs} +10 -10
  64. package/admin-dist/server/_ssr/{_entryId-Cc_Ry7AV.mjs → _entryId-B4zhQqFg.mjs} +11 -11
  65. package/admin-dist/server/_ssr/_tanstack-start-manifest_v-DndoqCo7.mjs +4 -0
  66. package/admin-dist/server/_ssr/{badge-CmG74mbX.mjs → badge-NOEC9bkk.mjs} +1 -1
  67. package/admin-dist/server/_ssr/{command-DWXiOsOb.mjs → command-h4-OYNBo.mjs} +1 -1
  68. package/admin-dist/server/_ssr/{content-CAgFQzx-.mjs → content-CShtLuhK.mjs} +8 -8
  69. package/admin-dist/server/_ssr/{content-types-CqKvAZ8P.mjs → content-types-PeyRyfbc.mjs} +6 -6
  70. package/admin-dist/server/_ssr/{index--qYdIqvh.mjs → index-CplFXpGg.mjs} +3 -3
  71. package/admin-dist/server/_ssr/index.mjs +2 -2
  72. package/admin-dist/server/_ssr/{media-AXePwPAK.mjs → media-QAkNdX54.mjs} +9 -9
  73. package/admin-dist/server/_ssr/{new._contentTypeId-DNWIl-Ha.mjs → new._contentTypeId-DEJyMphJ.mjs} +10 -10
  74. package/admin-dist/server/_ssr/{router-B_gIkxi2.mjs → router-CQXMuGMF.mjs} +10 -10
  75. package/admin-dist/server/_ssr/{scroll-area-Cz-9ry0J.mjs → scroll-area-B7zoNyWB.mjs} +1 -1
  76. package/admin-dist/server/_ssr/{settings-BjSxo5d6.mjs → settings-CNaqVa4D.mjs} +9 -9
  77. package/admin-dist/server/_ssr/{switch-IsC1gdb1.mjs → switch-BKZhvryc.mjs} +1 -1
  78. package/admin-dist/server/_ssr/{tabs-BdgLwrYe.mjs → tabs-DtIIQxiD.mjs} +1 -1
  79. package/admin-dist/server/_ssr/{tanstack-adapter-CFwjrqRl.mjs → tanstack-adapter-CLavdbUY.mjs} +1 -1
  80. package/admin-dist/server/_ssr/{taxonomies-D5Di9EgA.mjs → taxonomies-vIZYICzr.mjs} +7 -7
  81. package/admin-dist/server/_ssr/{trash-DokZl1yA.mjs → trash-7yGR4-dF.mjs} +7 -7
  82. package/admin-dist/server/_ssr/{useBreadcrumbLabel-C4TsA5z0.mjs → useBreadcrumbLabel-DR5FaAMf.mjs} +1 -1
  83. package/admin-dist/server/_ssr/{usePermissions-COsRlMp-.mjs → usePermissions-DKkpETj_.mjs} +1 -1
  84. package/admin-dist/server/index.mjs +155 -155
  85. package/package.json +1 -1
  86. package/admin-dist/public/assets/settings-DCY0s2hR.js +0 -1
  87. package/admin-dist/server/_ssr/_tanstack-start-manifest_v-DhspKP9e.mjs +0 -4
@@ -13,8 +13,7 @@ import {
13
13
  CardHeader,
14
14
  CardTitle,
15
15
  } from "~/components/ui/card";
16
- import { Skeleton } from "~/components/ui/skeleton";
17
- import { CmsPageHeader } from "~/components/cmsds";
16
+ import { CmsPageHeader, CmsStatCard } from "~/components/cmsds";
18
17
  import { SchemaDriftWarning } from "~/components/SchemaDriftWarning";
19
18
  import { FileText, Image, Layers, Settings, TrendingUp } from "lucide-react";
20
19
  import type { AdminNavigation } from "~/lib/navigation";
@@ -72,40 +71,24 @@ export function DashboardPage({ api, navigation }: DashboardPageProps) {
72
71
  <h2 className="text-lg font-semibold">Quick Stats</h2>
73
72
  </div>
74
73
  <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
75
- <StatCard
76
- label="Content Types"
77
- value={
78
- isLoading
79
- ? undefined
80
- : hasError
81
- ? "—"
82
- : String(stats.contentTypes)
83
- }
74
+ <CmsStatCard
75
+ title="Content Types"
76
+ value={hasError ? "—" : String(stats?.contentTypes ?? 0)}
84
77
  isLoading={isLoading}
85
78
  />
86
- <StatCard
87
- label="Content Entries"
88
- value={
89
- isLoading
90
- ? undefined
91
- : hasError
92
- ? "—"
93
- : String(stats.contentEntries)
94
- }
79
+ <CmsStatCard
80
+ title="Content Entries"
81
+ value={hasError ? "—" : String(stats?.contentEntries ?? 0)}
95
82
  isLoading={isLoading}
96
83
  />
97
- <StatCard
98
- label="Media Assets"
99
- value={
100
- isLoading ? undefined : hasError ? "—" : String(stats.mediaAssets)
101
- }
84
+ <CmsStatCard
85
+ title="Media Assets"
86
+ value={hasError ? "—" : String(stats?.mediaAssets ?? 0)}
102
87
  isLoading={isLoading}
103
88
  />
104
- <StatCard
105
- label="Published"
106
- value={
107
- isLoading ? undefined : hasError ? "—" : String(stats.published)
108
- }
89
+ <CmsStatCard
90
+ title="Published"
91
+ value={hasError ? "—" : String(stats?.published ?? 0)}
109
92
  isLoading={isLoading}
110
93
  />
111
94
  </div>
@@ -142,25 +125,3 @@ function DashboardCard({
142
125
  );
143
126
  }
144
127
 
145
- function StatCard({
146
- label,
147
- value,
148
- isLoading = false,
149
- }: {
150
- label: string;
151
- value?: string;
152
- isLoading?: boolean;
153
- }) {
154
- return (
155
- <Card>
156
- <CardContent className="p-4">
157
- {isLoading ? (
158
- <Skeleton className="mb-1 h-8 w-16" />
159
- ) : (
160
- <div className="text-2xl font-bold">{value}</div>
161
- )}
162
- <div className="text-sm text-muted-foreground">{label}</div>
163
- </CardContent>
164
- </Card>
165
- );
166
- }
@@ -8,10 +8,12 @@
8
8
  import { useState, useMemo, useCallback, useEffect } from "react";
9
9
  import { useQuery, useMutation } from "convex/react";
10
10
  import { UploadDropzone, type UploadedFile } from "~/components/UploadDropzone";
11
- import { CmsPageHeader } from "~/components/cmsds/CmsPageHeader";
12
- import { CmsToolbar } from "~/components/cmsds/CmsToolbar";
13
- import { CmsEmptyState } from "~/components/cmsds/CmsEmptyState";
14
- import { CmsButton } from "~/components/cmsds/CmsButton";
11
+ import {
12
+ CmsPageHeader,
13
+ CmsEmptyState,
14
+ CmsButton,
15
+ CmsFilterBar,
16
+ } from "~/components/cmsds";
15
17
  import { TaxonomyFilter } from "~/components/filters/TaxonomyFilter";
16
18
  import {
17
19
  Dialog,
@@ -22,13 +24,6 @@ import {
22
24
  } from "~/components/ui/dialog";
23
25
  import { Input } from "~/components/ui/input";
24
26
  import { Label } from "~/components/ui/label";
25
- import {
26
- Select,
27
- SelectContent,
28
- SelectItem,
29
- SelectTrigger,
30
- SelectValue,
31
- } from "~/components/ui/select";
32
27
  import { Checkbox } from "~/components/ui/checkbox";
33
28
  import { cn } from "~/lib/cn";
34
29
  import {
@@ -43,7 +38,6 @@ import {
43
38
  FolderPlus,
44
39
  Upload,
45
40
  Search,
46
- X,
47
41
  Trash2,
48
42
  RotateCcw,
49
43
  } from "lucide-react";
@@ -544,48 +538,31 @@ export function MediaPage({ api, navigation, settings }: MediaPageProps) {
544
538
  </nav>
545
539
  )}
546
540
 
547
- <CmsToolbar
548
- left={
541
+ <CmsFilterBar
542
+ search={{
543
+ value: searchQuery,
544
+ onChange: setSearchQuery,
545
+ placeholder: "Search files...",
546
+ className: "w-64",
547
+ }}
548
+ filters={[
549
+ {
550
+ key: "type",
551
+ value: typeFilter || "all",
552
+ onChange: (v) => setTypeFilter(v === "all" ? "" : (v as MediaType)),
553
+ options: [
554
+ { value: "all", label: "All Types" },
555
+ { value: "image", label: "Images" },
556
+ { value: "video", label: "Videos" },
557
+ { value: "audio", label: "Audio" },
558
+ { value: "document", label: "Documents" },
559
+ { value: "other", label: "Other" },
560
+ ],
561
+ className: "w-36",
562
+ },
563
+ ]}
564
+ actions={
549
565
  <div className="flex items-center gap-3">
550
- <div className="relative">
551
- <Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
552
- <Input
553
- type="search"
554
- placeholder="Search files..."
555
- value={searchQuery}
556
- onChange={(e) => setSearchQuery(e.target.value)}
557
- className="w-64 pl-9"
558
- />
559
- {searchQuery && (
560
- <button
561
- className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 hover:bg-muted"
562
- onClick={() => setSearchQuery("")}
563
- aria-label="Clear search"
564
- >
565
- <X className="size-4 text-muted-foreground" />
566
- </button>
567
- )}
568
- </div>
569
-
570
- <Select
571
- value={typeFilter || "all"}
572
- onValueChange={(v) =>
573
- setTypeFilter(v === "all" ? "" : (v as MediaType))
574
- }
575
- >
576
- <SelectTrigger className="w-36">
577
- <SelectValue placeholder="All Types" />
578
- </SelectTrigger>
579
- <SelectContent>
580
- <SelectItem value="all">All Types</SelectItem>
581
- <SelectItem value="image">Images</SelectItem>
582
- <SelectItem value="video">Videos</SelectItem>
583
- <SelectItem value="audio">Audio</SelectItem>
584
- <SelectItem value="document">Documents</SelectItem>
585
- <SelectItem value="other">Other</SelectItem>
586
- </SelectContent>
587
- </Select>
588
-
589
566
  <TaxonomyFilter
590
567
  selectedTermIds={selectedTermIds}
591
568
  onChange={setSelectedTermIds}
@@ -606,10 +583,7 @@ export function MediaPage({ api, navigation, settings }: MediaPageProps) {
606
583
  Selection Mode
607
584
  </label>
608
585
  )}
609
- </div>
610
- }
611
- right={
612
- <div className="flex items-center gap-2">
586
+
613
587
  {isSelectionMode && selectedAssets.size > 0 && (
614
588
  <span className="text-sm text-muted-foreground">
615
589
  {selectedAssets.size} selected
@@ -84,7 +84,11 @@ const THEME_OPTIONS: {
84
84
  ];
85
85
 
86
86
  function AppearanceSection() {
87
- const { theme, setTheme } = useTheme();
87
+ const { theme, setTheme, canToggleDarkMode } = useTheme();
88
+
89
+ if (!canToggleDarkMode) {
90
+ return null;
91
+ }
88
92
 
89
93
  return (
90
94
  <CmsSurface elevation="base" className="p-6">
@@ -5,27 +5,21 @@
5
5
  * Used by both CLI routes and embed pages.
6
6
  */
7
7
 
8
- import { useState, useCallback } from "react";
8
+ import { useState, useCallback, useMemo } from "react";
9
9
  import { useQuery, useMutation } from "convex/react";
10
- import { CmsPageHeader } from "~/components/cmsds/CmsPageHeader";
11
- import { CmsToolbar } from "~/components/cmsds/CmsToolbar";
12
- import { CmsEmptyState } from "~/components/cmsds/CmsEmptyState";
13
- import { CmsSurface } from "~/components/cmsds/CmsSurface";
14
- import { CmsButton } from "~/components/cmsds/CmsButton";
15
- import { CmsConfirmDialog } from "~/components/cmsds/CmsDialog";
16
- import { Input } from "~/components/ui/input";
17
10
  import {
18
- Select,
19
- SelectContent,
20
- SelectItem,
21
- SelectTrigger,
22
- SelectValue,
23
- } from "~/components/ui/select";
24
- import { Checkbox } from "~/components/ui/checkbox";
11
+ CmsPageHeader,
12
+ CmsEmptyState,
13
+ CmsSurface,
14
+ CmsButton,
15
+ CmsConfirmDialog,
16
+ CmsFilterBar,
17
+ CmsTable,
18
+ type CmsTableColumn,
19
+ } from "~/components/cmsds";
25
20
  import { Badge } from "~/components/ui/badge";
26
21
  import { Alert, AlertDescription } from "~/components/ui/alert";
27
- import { cn } from "~/lib/cn";
28
- import { Search, Trash2, RotateCcw, AlertTriangle, X } from "lucide-react";
22
+ import { Trash2, RotateCcw, AlertTriangle, X } from "lucide-react";
29
23
  import type { AdminNavigation } from "~/lib/navigation";
30
24
  import { CmsAdminApi } from "~/embed/contexts/ApiContext";
31
25
 
@@ -77,26 +71,6 @@ export function TrashPage({ api, navigation: _navigation }: TrashPageProps) {
77
71
  const config = configQuery;
78
72
  const stats = statsQuery;
79
73
 
80
- const handleSelectItem = useCallback((itemId: string, selected: boolean) => {
81
- setSelectedItems((prev) => {
82
- const next = new Set(prev);
83
- if (selected) {
84
- next.add(itemId);
85
- } else {
86
- next.delete(itemId);
87
- }
88
- return next;
89
- });
90
- }, []);
91
-
92
- const handleSelectAll = useCallback(() => {
93
- if (selectedItems.size === trashItems.length) {
94
- setSelectedItems(new Set());
95
- } else {
96
- setSelectedItems(new Set(trashItems.map((item) => item._id)));
97
- }
98
- }, [selectedItems.size, trashItems]);
99
-
100
74
  const handleRestore = useCallback(
101
75
  async (ids: string[]) => {
102
76
  setIsRestoring(true);
@@ -166,6 +140,79 @@ export function TrashPage({ api, navigation: _navigation }: TrashPageProps) {
166
140
  return item.slug || item._id;
167
141
  };
168
142
 
143
+ const trashColumns: CmsTableColumn<TrashItem>[] = useMemo(
144
+ () => [
145
+ {
146
+ key: "name",
147
+ header: "Name",
148
+ cell: (item) => (
149
+ <>
150
+ <span className="font-medium text-foreground">{getItemTitle(item)}</span>
151
+ {item.slug && (
152
+ <span className="block text-xs text-muted-foreground">{item.slug}</span>
153
+ )}
154
+ </>
155
+ ),
156
+ },
157
+ {
158
+ key: "type",
159
+ header: "Type",
160
+ cell: (item) => (
161
+ <span className="text-sm text-muted-foreground">
162
+ {item.contentTypeName || "Unknown"}
163
+ </span>
164
+ ),
165
+ },
166
+ {
167
+ key: "deleted",
168
+ header: "Deleted",
169
+ cell: (item) => (
170
+ <>
171
+ <span className="text-sm text-muted-foreground">
172
+ {formatDate(item.deletedAt)}
173
+ </span>
174
+ {item.deletedBy && (
175
+ <span className="block text-xs text-muted-foreground">
176
+ by {item.deletedBy}
177
+ </span>
178
+ )}
179
+ </>
180
+ ),
181
+ },
182
+ {
183
+ key: "expires",
184
+ header: "Expires In",
185
+ cell: (item) => {
186
+ const daysLeft = getDaysUntilDeletion(item.deletedAt);
187
+ return daysLeft !== null ? (
188
+ <Badge
189
+ variant={daysLeft <= 3 ? "destructive" : "secondary"}
190
+ className="font-normal"
191
+ >
192
+ {daysLeft} {daysLeft === 1 ? "day" : "days"}
193
+ </Badge>
194
+ ) : null;
195
+ },
196
+ },
197
+ {
198
+ key: "actions",
199
+ header: "Actions",
200
+ cell: (item) => (
201
+ <CmsButton
202
+ variant="outline"
203
+ size="sm"
204
+ onClick={() => handleRestore([item._id])}
205
+ loading={isRestoring}
206
+ >
207
+ <RotateCcw className="size-4" />
208
+ Restore
209
+ </CmsButton>
210
+ ),
211
+ },
212
+ ],
213
+ [isRestoring, handleRestore, config?.retentionDays]
214
+ );
215
+
169
216
  return (
170
217
  <div className="space-y-6 p-6">
171
218
  <div className="flex items-start justify-between">
@@ -200,39 +247,28 @@ export function TrashPage({ api, navigation: _navigation }: TrashPageProps) {
200
247
  </div>
201
248
  )}
202
249
 
203
- <CmsToolbar
204
- left={
205
- <div className="flex items-center gap-3">
206
- <div className="relative">
207
- <Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
208
- <Input
209
- type="text"
210
- placeholder="Search deleted items..."
211
- value={searchQuery}
212
- onChange={(e) => setSearchQuery(e.target.value)}
213
- className="w-64 pl-9"
214
- />
215
- </div>
216
- <Select
217
- value={selectedContentType || "all"}
218
- onValueChange={(v) =>
219
- setSelectedContentType(v === "all" ? "" : v)
220
- }
221
- >
222
- <SelectTrigger className="w-48">
223
- <SelectValue placeholder="All Content Types" />
224
- </SelectTrigger>
225
- <SelectContent>
226
- <SelectItem value="all">All Content Types</SelectItem>
227
- {contentTypes.map((type: any) => (
228
- <SelectItem key={type._id} value={type._id}>
229
- {type.displayName}
230
- </SelectItem>
231
- ))}
232
- </SelectContent>
233
- </Select>
234
- </div>
235
- }
250
+ <CmsFilterBar
251
+ search={{
252
+ value: searchQuery,
253
+ onChange: setSearchQuery,
254
+ placeholder: "Search deleted items...",
255
+ className: "w-64",
256
+ }}
257
+ filters={[
258
+ {
259
+ key: "contentType",
260
+ value: selectedContentType || "all",
261
+ onChange: (v) => setSelectedContentType(v === "all" ? "" : v),
262
+ options: [
263
+ { value: "all", label: "All Content Types" },
264
+ ...contentTypes.map((type: { _id: string; displayName: string }) => ({
265
+ value: type._id,
266
+ label: type.displayName,
267
+ })),
268
+ ],
269
+ className: "w-48",
270
+ },
271
+ ]}
236
272
  />
237
273
 
238
274
  {restoreError && (
@@ -304,106 +340,15 @@ export function TrashPage({ api, navigation: _navigation }: TrashPageProps) {
304
340
  description="Deleted items will appear here"
305
341
  />
306
342
  ) : (
307
- <div className="rounded-lg border bg-card">
308
- <table className="w-full">
309
- <thead>
310
- <tr className="border-b">
311
- <th className="w-10 p-3 text-left">
312
- <Checkbox
313
- checked={
314
- selectedItems.size === trashItems.length &&
315
- trashItems.length > 0
316
- }
317
- onCheckedChange={handleSelectAll}
318
- />
319
- </th>
320
- <th className="p-3 text-left text-sm font-medium text-muted-foreground">
321
- Name
322
- </th>
323
- <th className="p-3 text-left text-sm font-medium text-muted-foreground">
324
- Type
325
- </th>
326
- <th className="p-3 text-left text-sm font-medium text-muted-foreground">
327
- Deleted
328
- </th>
329
- <th className="p-3 text-left text-sm font-medium text-muted-foreground">
330
- Expires In
331
- </th>
332
- <th className="p-3 text-left text-sm font-medium text-muted-foreground">
333
- Actions
334
- </th>
335
- </tr>
336
- </thead>
337
- <tbody>
338
- {trashItems.map((item) => {
339
- const daysLeft = getDaysUntilDeletion(item.deletedAt);
340
-
341
- return (
342
- <tr
343
- key={item._id}
344
- className={cn(
345
- "border-b last:border-0 transition-colors hover:bg-muted/50",
346
- selectedItems.has(item._id) && "bg-primary/5",
347
- )}
348
- >
349
- <td className="p-3">
350
- <Checkbox
351
- checked={selectedItems.has(item._id)}
352
- onCheckedChange={(checked) =>
353
- handleSelectItem(item._id, checked as boolean)
354
- }
355
- />
356
- </td>
357
- <td className="p-3">
358
- <span className="font-medium text-foreground">
359
- {getItemTitle(item)}
360
- </span>
361
- {item.slug && (
362
- <span className="block text-xs text-muted-foreground">
363
- {item.slug}
364
- </span>
365
- )}
366
- </td>
367
- <td className="p-3 text-sm text-muted-foreground">
368
- {item.contentTypeName || "Unknown"}
369
- </td>
370
- <td className="p-3">
371
- <span className="text-sm text-muted-foreground">
372
- {formatDate(item.deletedAt)}
373
- </span>
374
- {item.deletedBy && (
375
- <span className="block text-xs text-muted-foreground">
376
- by {item.deletedBy}
377
- </span>
378
- )}
379
- </td>
380
- <td className="p-3">
381
- {daysLeft !== null && (
382
- <Badge
383
- variant={daysLeft <= 3 ? "destructive" : "secondary"}
384
- className="font-normal"
385
- >
386
- {daysLeft} {daysLeft === 1 ? "day" : "days"}
387
- </Badge>
388
- )}
389
- </td>
390
- <td className="p-3">
391
- <CmsButton
392
- variant="outline"
393
- size="sm"
394
- onClick={() => handleRestore([item._id])}
395
- loading={isRestoring}
396
- >
397
- <RotateCcw className="size-4" />
398
- Restore
399
- </CmsButton>
400
- </td>
401
- </tr>
402
- );
403
- })}
404
- </tbody>
405
- </table>
406
- </div>
343
+ <CmsTable
344
+ columns={trashColumns}
345
+ data={trashItems}
346
+ getRowId={(item) => item._id}
347
+ selectable
348
+ selectedIds={selectedItems}
349
+ onSelectionChange={setSelectedItems}
350
+ emptyMessage="No items in trash"
351
+ />
407
352
  )}
408
353
 
409
354
  <CmsConfirmDialog
@@ -12,56 +12,43 @@
12
12
  @apply border-border;
13
13
  }
14
14
 
15
+ /* Standalone mode - admin owns the document */
15
16
  body {
16
17
  @apply bg-background text-foreground;
17
18
  font-feature-settings: 'rlig' 1, 'calt' 1;
18
19
  -webkit-font-smoothing: antialiased;
19
20
  -moz-osx-font-smoothing: grayscale;
20
21
  }
22
+
23
+ /* Embedded mode - scoped base styles */
24
+ [data-cms-admin] {
25
+ @apply bg-background text-foreground;
26
+ font-feature-settings: 'rlig' 1, 'calt' 1;
27
+ -webkit-font-smoothing: antialiased;
28
+ -moz-osx-font-smoothing: grayscale;
29
+ }
21
30
  }
22
31
 
23
32
  @layer utilities {
24
- /* Status badge colors - using semantic variables */
33
+ /* Status badge colors - using CSS variables for theme consistency */
25
34
  .status-draft {
26
- @apply bg-muted text-muted-foreground;
35
+ background-color: var(--muted);
36
+ color: var(--muted-foreground);
27
37
  }
28
38
 
29
39
  .status-published {
30
- @apply bg-diff-added-bg text-diff-added-foreground;
40
+ background-color: var(--diff-added-bg);
41
+ color: var(--diff-added-foreground);
31
42
  }
32
43
 
33
44
  .status-scheduled {
34
- @apply bg-info-bg text-info-foreground;
45
+ background-color: var(--info-bg);
46
+ color: var(--info-foreground);
35
47
  }
36
48
 
37
49
  .status-archived {
38
- @apply bg-diff-modified-bg text-diff-modified-foreground;
39
- }
40
-
41
- /* Diff change indicators */
42
- .diff-added {
43
- @apply bg-diff-added-bg border-diff-added-border text-diff-added-foreground;
44
- }
45
-
46
- .diff-removed {
47
- @apply bg-diff-removed-bg border-diff-removed-border text-diff-removed-foreground;
48
- }
49
-
50
- .diff-modified {
51
- @apply bg-diff-modified-bg border-diff-modified-border text-diff-modified-foreground;
52
- }
53
-
54
- /* Icon badges for diff */
55
- .diff-icon-added {
56
- @apply bg-diff-added/20 text-diff-added;
57
- }
58
-
59
- .diff-icon-removed {
60
- @apply bg-diff-removed/20 text-diff-removed;
61
- }
62
-
63
- .diff-icon-modified {
64
- @apply bg-diff-modified/20 text-diff-modified;
50
+ background-color: var(--diff-modified-bg);
51
+ color: var(--diff-modified-foreground);
65
52
  }
66
53
 
67
54
  /* Surface elevations */
@@ -96,4 +96,16 @@
96
96
  --color-info: var(--info);
97
97
  --color-info-bg: var(--info-bg);
98
98
  --color-info-foreground: var(--info-foreground);
99
+
100
+ /* Purple (workflow states) */
101
+ --color-purple: var(--purple);
102
+ --color-purple-bg: var(--purple-bg);
103
+ --color-purple-foreground: var(--purple-foreground);
104
+
105
+ /* Chart colors */
106
+ --color-chart-1: var(--chart-1);
107
+ --color-chart-2: var(--chart-2);
108
+ --color-chart-3: var(--chart-3);
109
+ --color-chart-4: var(--chart-4);
110
+ --color-chart-5: var(--chart-5);
99
111
  }