@actuate-media/cms-admin 0.64.0 → 0.65.0

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 (164) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/__tests__/components/ui-primitives.test.d.ts +2 -0
  3. package/dist/__tests__/components/ui-primitives.test.d.ts.map +1 -0
  4. package/dist/__tests__/components/ui-primitives.test.js +45 -0
  5. package/dist/__tests__/components/ui-primitives.test.js.map +1 -0
  6. package/dist/__tests__/views/updates-tab.render.test.js +1 -1
  7. package/dist/__tests__/views/updates-tab.render.test.js.map +1 -1
  8. package/dist/actuate-admin.css +1 -1
  9. package/dist/components/SEOPanel.d.ts.map +1 -1
  10. package/dist/components/SEOPanel.js +2 -1
  11. package/dist/components/SEOPanel.js.map +1 -1
  12. package/dist/components/forms/primitives.d.ts.map +1 -1
  13. package/dist/components/forms/primitives.js +5 -8
  14. package/dist/components/forms/primitives.js.map +1 -1
  15. package/dist/components/posts/badges.d.ts.map +1 -1
  16. package/dist/components/posts/badges.js +7 -13
  17. package/dist/components/posts/badges.js.map +1 -1
  18. package/dist/components/seo/RedirectEditorDrawer.d.ts.map +1 -1
  19. package/dist/components/seo/RedirectEditorDrawer.js +2 -1
  20. package/dist/components/seo/RedirectEditorDrawer.js.map +1 -1
  21. package/dist/components/seo/SeoEditorDrawer.d.ts.map +1 -1
  22. package/dist/components/seo/SeoEditorDrawer.js +3 -2
  23. package/dist/components/seo/SeoEditorDrawer.js.map +1 -1
  24. package/dist/components/seo/primitives.d.ts.map +1 -1
  25. package/dist/components/seo/primitives.js +9 -14
  26. package/dist/components/seo/primitives.js.map +1 -1
  27. package/dist/components/ui/FilterPill.d.ts +14 -0
  28. package/dist/components/ui/FilterPill.d.ts.map +1 -0
  29. package/dist/components/ui/FilterPill.js +8 -0
  30. package/dist/components/ui/FilterPill.js.map +1 -0
  31. package/dist/components/ui/StatPill.d.ts +18 -0
  32. package/dist/components/ui/StatPill.d.ts.map +1 -0
  33. package/dist/components/ui/StatPill.js +5 -0
  34. package/dist/components/ui/StatPill.js.map +1 -0
  35. package/dist/components/ui/StatusBadge.d.ts +25 -0
  36. package/dist/components/ui/StatusBadge.d.ts.map +1 -0
  37. package/dist/components/ui/StatusBadge.js +41 -0
  38. package/dist/components/ui/StatusBadge.js.map +1 -0
  39. package/dist/components/ui/index.d.ts +6 -0
  40. package/dist/components/ui/index.d.ts.map +1 -1
  41. package/dist/components/ui/index.js +3 -0
  42. package/dist/components/ui/index.js.map +1 -1
  43. package/dist/fields/PropInput.d.ts.map +1 -1
  44. package/dist/fields/PropInput.js +2 -1
  45. package/dist/fields/PropInput.js.map +1 -1
  46. package/dist/fields/ToggleField.d.ts.map +1 -1
  47. package/dist/fields/ToggleField.js +2 -1
  48. package/dist/fields/ToggleField.js.map +1 -1
  49. package/dist/layout/primitives/PageContainer.d.ts.map +1 -1
  50. package/dist/layout/primitives/PageContainer.js +1 -1
  51. package/dist/layout/primitives/PageContainer.js.map +1 -1
  52. package/dist/views/Dashboard.js +1 -1
  53. package/dist/views/Dashboard.js.map +1 -1
  54. package/dist/views/FormEditor.d.ts.map +1 -1
  55. package/dist/views/FormEditor.js +2 -1
  56. package/dist/views/FormEditor.js.map +1 -1
  57. package/dist/views/FormEntries.js +1 -1
  58. package/dist/views/FormEntries.js.map +1 -1
  59. package/dist/views/FormSubmissions.js +1 -1
  60. package/dist/views/FormSubmissions.js.map +1 -1
  61. package/dist/views/Forms.js +1 -1
  62. package/dist/views/Forms.js.map +1 -1
  63. package/dist/views/MediaBrowser.d.ts.map +1 -1
  64. package/dist/views/MediaBrowser.js +3 -2
  65. package/dist/views/MediaBrowser.js.map +1 -1
  66. package/dist/views/Pages/PageTagManager.d.ts.map +1 -1
  67. package/dist/views/Pages/PageTagManager.js +1 -1
  68. package/dist/views/Pages/PageTagManager.js.map +1 -1
  69. package/dist/views/Pages/PagesListView.js +1 -1
  70. package/dist/views/Pages/PagesListView.js.map +1 -1
  71. package/dist/views/Pages.js +1 -1
  72. package/dist/views/Pages.js.map +1 -1
  73. package/dist/views/Posts/NewPostPicker.d.ts.map +1 -1
  74. package/dist/views/Posts/NewPostPicker.js +1 -1
  75. package/dist/views/Posts/NewPostPicker.js.map +1 -1
  76. package/dist/views/Posts/NewPostTypeInstructions.d.ts.map +1 -1
  77. package/dist/views/Posts/NewPostTypeInstructions.js +1 -1
  78. package/dist/views/Posts/NewPostTypeInstructions.js.map +1 -1
  79. package/dist/views/Posts/PostTypesView.d.ts.map +1 -1
  80. package/dist/views/Posts/PostTypesView.js +1 -1
  81. package/dist/views/Posts/PostTypesView.js.map +1 -1
  82. package/dist/views/Posts/PostsListView.js +1 -1
  83. package/dist/views/Posts/PostsListView.js.map +1 -1
  84. package/dist/views/Profile.js +1 -1
  85. package/dist/views/Profile.js.map +1 -1
  86. package/dist/views/Redirects.js +1 -1
  87. package/dist/views/Redirects.js.map +1 -1
  88. package/dist/views/SEO.js +1 -1
  89. package/dist/views/SEO.js.map +1 -1
  90. package/dist/views/ScriptTagEditor.d.ts.map +1 -1
  91. package/dist/views/ScriptTagEditor.js +1 -1
  92. package/dist/views/ScriptTagEditor.js.map +1 -1
  93. package/dist/views/ScriptTags.d.ts.map +1 -1
  94. package/dist/views/ScriptTags.js +2 -1
  95. package/dist/views/ScriptTags.js.map +1 -1
  96. package/dist/views/Settings.js +1 -1
  97. package/dist/views/Settings.js.map +1 -1
  98. package/dist/views/Users.d.ts.map +1 -1
  99. package/dist/views/Users.js +2 -6
  100. package/dist/views/Users.js.map +1 -1
  101. package/dist/views/page-builder/BlockEditor.d.ts.map +1 -1
  102. package/dist/views/page-builder/BlockEditor.js +2 -2
  103. package/dist/views/page-builder/BlockEditor.js.map +1 -1
  104. package/dist/views/page-builder/NodeSettings.js +3 -3
  105. package/dist/views/page-builder/NodeSettings.js.map +1 -1
  106. package/dist/views/page-builder/PageTemplates.js +1 -1
  107. package/dist/views/page-builder/PageTemplates.js.map +1 -1
  108. package/dist/views/page-builder/SavedSections.js +2 -2
  109. package/dist/views/page-builder/SavedSections.js.map +1 -1
  110. package/dist/views/page-builder/Widgets.js +1 -1
  111. package/dist/views/page-builder/Widgets.js.map +1 -1
  112. package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
  113. package/dist/views/page-editor/SectionInspector.js +2 -1
  114. package/dist/views/page-editor/SectionInspector.js.map +1 -1
  115. package/dist/views/post-editor/PostHeaderPanel.d.ts.map +1 -1
  116. package/dist/views/post-editor/PostHeaderPanel.js +2 -1
  117. package/dist/views/post-editor/PostHeaderPanel.js.map +1 -1
  118. package/dist/views/settings/components.d.ts.map +1 -1
  119. package/dist/views/settings/components.js +2 -1
  120. package/dist/views/settings/components.js.map +1 -1
  121. package/package.json +3 -3
  122. package/src/__tests__/components/ui-primitives.test.tsx +58 -0
  123. package/src/__tests__/views/updates-tab.render.test.tsx +2 -1
  124. package/src/components/SEOPanel.tsx +2 -11
  125. package/src/components/forms/primitives.tsx +6 -16
  126. package/src/components/posts/badges.tsx +8 -21
  127. package/src/components/seo/RedirectEditorDrawer.tsx +12 -16
  128. package/src/components/seo/SeoEditorDrawer.tsx +16 -24
  129. package/src/components/seo/primitives.tsx +10 -27
  130. package/src/components/ui/FilterPill.tsx +43 -0
  131. package/src/components/ui/StatPill.tsx +37 -0
  132. package/src/components/ui/StatusBadge.tsx +66 -0
  133. package/src/components/ui/index.ts +6 -0
  134. package/src/fields/PropInput.tsx +6 -15
  135. package/src/fields/ToggleField.tsx +3 -15
  136. package/src/layout/primitives/PageContainer.tsx +1 -5
  137. package/src/views/Dashboard.tsx +1 -1
  138. package/src/views/FormEditor.tsx +6 -9
  139. package/src/views/FormEntries.tsx +1 -1
  140. package/src/views/FormSubmissions.tsx +1 -1
  141. package/src/views/Forms.tsx +1 -1
  142. package/src/views/MediaBrowser.tsx +4 -7
  143. package/src/views/Pages/PageTagManager.tsx +1 -3
  144. package/src/views/Pages/PagesListView.tsx +1 -1
  145. package/src/views/Pages.tsx +1 -1
  146. package/src/views/Posts/NewPostPicker.tsx +1 -3
  147. package/src/views/Posts/NewPostTypeInstructions.tsx +1 -3
  148. package/src/views/Posts/PostTypesView.tsx +1 -3
  149. package/src/views/Posts/PostsListView.tsx +1 -1
  150. package/src/views/Profile.tsx +1 -1
  151. package/src/views/Redirects.tsx +1 -1
  152. package/src/views/SEO.tsx +1 -1
  153. package/src/views/ScriptTagEditor.tsx +1 -3
  154. package/src/views/ScriptTags.tsx +17 -1
  155. package/src/views/Settings.tsx +1 -1
  156. package/src/views/Users.tsx +6 -18
  157. package/src/views/page-builder/BlockEditor.tsx +3 -6
  158. package/src/views/page-builder/NodeSettings.tsx +7 -16
  159. package/src/views/page-builder/PageTemplates.tsx +1 -1
  160. package/src/views/page-builder/SavedSections.tsx +2 -2
  161. package/src/views/page-builder/Widgets.tsx +1 -1
  162. package/src/views/page-editor/SectionInspector.tsx +9 -21
  163. package/src/views/post-editor/PostHeaderPanel.tsx +10 -20
  164. package/src/views/settings/components.tsx +8 -16
@@ -16,6 +16,7 @@ import {
16
16
  } from 'lucide-react'
17
17
  import { useApiData } from '../lib/useApiData.js'
18
18
  import { cmsApi } from '../lib/api.js'
19
+ import { Toggle } from '../components/ui/Toggle.js'
19
20
 
20
21
  interface FormField {
21
22
  id: string
@@ -463,15 +464,11 @@ export function FormEditor({ formId, onNavigate }: FormEditorProps) {
463
464
  {expandedSection === 'analytics' && (
464
465
  <div className="border-border border-t p-4">
465
466
  <div className="mb-4 flex items-center gap-3">
466
- <label className="relative inline-flex cursor-pointer items-center">
467
- <input
468
- type="checkbox"
469
- checked={analytics.enabled}
470
- onChange={(e) => setAnalytics((a) => ({ ...a, enabled: e.target.checked }))}
471
- className="peer sr-only"
472
- />
473
- <div className="bg-muted peer peer-checked:bg-primary peer-focus:ring-ring/50 after:border-border after:bg-card h-5 w-9 rounded-full peer-focus:ring-2 peer-focus:outline-none after:absolute after:top-[2px] after:left-[2px] after:h-4 after:w-4 after:rounded-full after:border after:transition-all after:content-[''] peer-checked:after:translate-x-full peer-checked:after:border-white" />
474
- </label>
467
+ <Toggle
468
+ checked={analytics.enabled}
469
+ onCheckedChange={(v) => setAnalytics((a) => ({ ...a, enabled: v }))}
470
+ aria-label="Push events to Google Analytics 4"
471
+ />
475
472
  <span className="text-foreground text-sm font-medium">
476
473
  Push events to Google Analytics 4
477
474
  </span>
@@ -234,7 +234,7 @@ export function FormEntries({ onNavigate }: FormEntriesProps) {
234
234
  <div className="space-y-5 p-6">
235
235
  <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
236
236
  <div>
237
- <h1 className="text-foreground text-2xl font-medium">All Entries</h1>
237
+ <h1 className="text-foreground">All Entries</h1>
238
238
  <p className="text-muted-foreground mt-1 text-sm">
239
239
  {total} {total === 1 ? 'submission' : 'submissions'} across your forms
240
240
  </p>
@@ -254,7 +254,7 @@ export function FormSubmissions({ formId, onNavigate }: FormSubmissionsProps) {
254
254
  </button>
255
255
  <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
256
256
  <div>
257
- <h1 className="text-foreground text-2xl font-medium">
257
+ <h1 className="text-foreground">
258
258
  {formName ? `${formName} — Submissions` : 'Submissions'}
259
259
  </h1>
260
260
  <p className="text-muted-foreground mt-1 text-sm">
@@ -158,7 +158,7 @@ export function Forms({ onNavigate }: FormsProps) {
158
158
  <div className="space-y-5 p-6">
159
159
  <header className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
160
160
  <div>
161
- <h1 className="text-foreground text-2xl font-medium">Forms</h1>
161
+ <h1 className="text-foreground">Forms</h1>
162
162
  <p className="text-muted-foreground mt-1 text-sm">
163
163
  Manage forms and view incoming submissions
164
164
  </p>
@@ -30,6 +30,7 @@ import { sortByRelevance, type SortConfig, toggleSort } from '../lib/search.js'
30
30
  import { useApiData } from '../lib/useApiData.js'
31
31
  import { cmsApi } from '../lib/api.js'
32
32
  import { FolderTree, type FolderSelection } from '../components/FolderTree.js'
33
+ import { StatusBadge } from '../components/ui/StatusBadge.js'
33
34
 
34
35
  interface MediaItem {
35
36
  id: number | string
@@ -411,7 +412,7 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
411
412
  <FolderInput className="text-muted-foreground h-5 w-5" />
412
413
  </button>
413
414
  <div>
414
- <h1 className="text-foreground mb-1 text-xl font-medium sm:text-2xl">Media Library</h1>
415
+ <h1 className="text-foreground mb-1">Media Library</h1>
415
416
  <p className="text-muted-foreground text-sm">{filteredAndSorted.length} files</p>
416
417
  </div>
417
418
  </div>
@@ -722,13 +723,9 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
722
723
  </td>
723
724
  <td className="px-3 py-2">
724
725
  {hasIssues ? (
725
- <span className="bg-warning/15 text-warning inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium">
726
- <AlertTriangle className="h-3 w-3" /> Needs attention
727
- </span>
726
+ <StatusBadge tone="warning">Needs attention</StatusBadge>
728
727
  ) : (
729
- <span className="bg-success/15 text-success inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium">
730
- Complete
731
- </span>
728
+ <StatusBadge tone="success">Complete</StatusBadge>
732
729
  )}
733
730
  </td>
734
731
  </tr>
@@ -85,9 +85,7 @@ export function PageTagManager({ onNavigate, startCreating = false }: PageTagMan
85
85
 
86
86
  <div className="mb-5 flex items-center justify-between">
87
87
  <div>
88
- <h1 className="text-foreground mb-1 text-xl font-medium tracking-tight sm:text-2xl">
89
- Page Tags
90
- </h1>
88
+ <h1 className="text-foreground mb-1">Page Tags</h1>
91
89
  <p className="text-muted-foreground text-sm">
92
90
  Organize pages with reusable, color-coded labels.
93
91
  </p>
@@ -290,7 +290,7 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
290
290
  <div className="flex h-full flex-col gap-4 p-4 sm:p-6">
291
291
  {/* Header */}
292
292
  <div className="flex flex-col gap-1">
293
- <h1 className="text-foreground text-xl font-medium tracking-tight sm:text-2xl">Pages</h1>
293
+ <h1 className="text-foreground">Pages</h1>
294
294
  <p className="text-muted-foreground text-sm">
295
295
  {grandTotal} page{grandTotal === 1 ? '' : 's'} · organized by tags
296
296
  </p>
@@ -276,7 +276,7 @@ export function Pages({ onNavigate }: PagesProps) {
276
276
  <FolderInput className="text-muted-foreground h-5 w-5" />
277
277
  </button>
278
278
  <div>
279
- <h1 className="text-foreground text-xl font-medium sm:text-2xl">Pages</h1>
279
+ <h1 className="text-foreground">Pages</h1>
280
280
  <p className="text-muted-foreground text-sm">
281
281
  {totalCount} page{totalCount !== 1 ? 's' : ''}
282
282
  </p>
@@ -62,9 +62,7 @@ export function NewPostPicker({ onNavigate }: { onNavigate: (path: string) => vo
62
62
  </button>
63
63
 
64
64
  <div className="flex flex-col gap-1">
65
- <h1 className="text-foreground text-xl font-medium tracking-tight sm:text-2xl">
66
- Choose a Post Type
67
- </h1>
65
+ <h1 className="text-foreground">Choose a Post Type</h1>
68
66
  <p className="text-muted-foreground text-sm">
69
67
  Pick which content model you want to use for this post. Each type has its own fields, URL
70
68
  base, and SEO defaults.
@@ -114,9 +114,7 @@ export function NewPostTypeInstructions({ onNavigate }: { onNavigate: (p: string
114
114
  </button>
115
115
 
116
116
  <div className="flex flex-col gap-1">
117
- <h1 className="text-foreground text-xl font-medium tracking-tight sm:text-2xl">
118
- Add a Post Type
119
- </h1>
117
+ <h1 className="text-foreground">Add a Post Type</h1>
120
118
  <p className="text-muted-foreground text-sm">
121
119
  Post Types are content models defined in{' '}
122
120
  <code className="font-mono">actuate.config.ts</code>. Start from a quick template or fill
@@ -61,9 +61,7 @@ export function PostTypesView({ onNavigate }: PostTypesViewProps) {
61
61
  return (
62
62
  <div className="flex h-full flex-col gap-6 p-4 sm:p-6">
63
63
  <div className="flex flex-col gap-1">
64
- <h1 className="text-foreground text-xl font-medium tracking-tight sm:text-2xl">
65
- Post Types
66
- </h1>
64
+ <h1 className="text-foreground">Post Types</h1>
67
65
  <p className="text-muted-foreground text-sm">
68
66
  Define content models and their fields. All types share the same default fields.
69
67
  </p>
@@ -216,7 +216,7 @@ export function PostsListView({ onNavigate }: PostsListViewProps) {
216
216
  <div className="flex h-full flex-col gap-4 p-4 sm:p-6">
217
217
  {/* Header */}
218
218
  <div className="flex flex-col gap-1">
219
- <h1 className="text-foreground text-xl font-medium tracking-tight sm:text-2xl">Posts</h1>
219
+ <h1 className="text-foreground">Posts</h1>
220
220
  <p className="text-muted-foreground text-sm">
221
221
  {totalAll} total post{totalAll === 1 ? '' : 's'} across all types
222
222
  </p>
@@ -21,7 +21,7 @@ export function Profile({ session }: ProfileProps = {}) {
21
21
  return (
22
22
  <div className="p-3 pr-6 sm:p-4 sm:pr-8">
23
23
  <div className="mb-4">
24
- <h1 className="text-foreground mb-1 text-2xl font-medium">Your profile</h1>
24
+ <h1 className="text-foreground mb-1">Your profile</h1>
25
25
  <p className="text-muted-foreground text-sm">
26
26
  Manage your account details and sign-in security.
27
27
  </p>
@@ -143,7 +143,7 @@ export function Redirects({ onNavigate }: RedirectsProps) {
143
143
 
144
144
  <div className="mb-4 flex items-center justify-between">
145
145
  <div>
146
- <h1 className="text-foreground mb-1 text-xl font-medium sm:text-2xl">Redirects</h1>
146
+ <h1 className="text-foreground mb-1">Redirects</h1>
147
147
  <p className="text-muted-foreground text-sm">
148
148
  {filteredAndSorted.length} total redirects
149
149
  </p>
package/src/views/SEO.tsx CHANGED
@@ -138,7 +138,7 @@ export function SEO({ onNavigate, initialTab = 'overview' }: SEOProps) {
138
138
  <div className="space-y-6 p-6">
139
139
  <header className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
140
140
  <div>
141
- <h1 className="text-foreground text-2xl font-medium">SEO Operations Center</h1>
141
+ <h1 className="text-foreground">SEO Operations Center</h1>
142
142
  <p className="text-muted-foreground mt-1 text-sm">
143
143
  Monitor, fix, and automate SEO across your content, redirects, and technical setup.
144
144
  </p>
@@ -135,9 +135,7 @@ export function ScriptTagEditor({ tagId, onNavigate }: ScriptTagEditorProps) {
135
135
  <ArrowLeft className="h-4 w-4" />
136
136
  Back to Script Tags
137
137
  </button>
138
- <h1 className="text-foreground text-2xl font-medium">
139
- {isNew ? 'New Script Tag' : 'Edit Script Tag'}
140
- </h1>
138
+ <h1 className="text-foreground">{isNew ? 'New Script Tag' : 'Edit Script Tag'}</h1>
141
139
  </div>
142
140
 
143
141
  <div className="border-warning/30 bg-warning/10 mb-6 rounded-lg border p-3">
@@ -4,6 +4,7 @@ import { useMemo, useState } from 'react'
4
4
  import { Code2, Plus, Loader2, AlertTriangle } from 'lucide-react'
5
5
  import { toast } from 'sonner'
6
6
  import { EnabledSwitch } from '../components/EnabledSwitch.js'
7
+ import { StatPill } from '../components/ui/StatPill.js'
7
8
  import { useApiData } from '../lib/useApiData.js'
8
9
  import { cmsApi } from '../lib/api.js'
9
10
 
@@ -123,10 +124,25 @@ export function ScriptTags({ onNavigate }: ScriptTagsProps) {
123
124
 
124
125
  <div className="mb-4 flex items-center justify-between">
125
126
  <div>
126
- <h1 className="text-foreground mb-1 text-2xl font-medium">Script Tags</h1>
127
+ <h1 className="text-foreground mb-1">Script Tags</h1>
127
128
  <p className="text-muted-foreground text-sm">
128
129
  Manage tracking codes, analytics, and custom scripts injected into your site
129
130
  </p>
131
+ {tags.length > 0 && (
132
+ <div className="mt-3 flex flex-wrap gap-1.5">
133
+ <StatPill value={tags.length} label="Total" />
134
+ <StatPill
135
+ value={tags.filter((t) => t.enabled).length}
136
+ label="Active"
137
+ valueClassName="text-[var(--suc)]"
138
+ />
139
+ <StatPill
140
+ value={tags.filter((t) => !t.enabled).length}
141
+ label="Inactive"
142
+ valueClassName="text-[var(--muted)]"
143
+ />
144
+ </div>
145
+ )}
130
146
  </div>
131
147
  <button
132
148
  type="button"
@@ -179,7 +179,7 @@ export function Settings({
179
179
  )}
180
180
 
181
181
  <div className="mb-4">
182
- <h1 className="text-foreground mb-1 text-2xl font-medium">Settings</h1>
182
+ <h1 className="text-foreground mb-1">Settings</h1>
183
183
  <p className="text-muted-foreground text-sm">Manage your CMS configuration</p>
184
184
  </div>
185
185
 
@@ -28,6 +28,7 @@ import { useApiData } from '../lib/useApiData.js'
28
28
  import { cmsApi } from '../lib/api.js'
29
29
  import { Button } from '../components/ui/Button.js'
30
30
  import { EmptyState } from '../components/ui/EmptyState.js'
31
+ import { FilterPill } from '../components/ui/FilterPill.js'
31
32
  import { Skeleton } from '../components/ui/Skeleton.js'
32
33
  import { ConfirmDialog } from '../components/ui/ConfirmDialog.js'
33
34
  import { UserDetailDrawer } from './users/UserDetailDrawer.js'
@@ -964,24 +965,11 @@ export function Users({ embedded = false }: UsersProps = {}) {
964
965
  role="group"
965
966
  aria-label="Filter by role"
966
967
  >
967
- {ROLE_PILLS.map((pill) => {
968
- const active = roleFilter === pill
969
- return (
970
- <button
971
- key={pill}
972
- type="button"
973
- onClick={() => setRoleFilter(pill)}
974
- aria-pressed={active}
975
- className={`rounded-full px-3 py-1 text-xs font-medium transition-colors ${
976
- active
977
- ? 'bg-brand text-brand-foreground'
978
- : 'bg-muted text-muted-foreground hover:bg-accent'
979
- }`}
980
- >
981
- {pill}
982
- </button>
983
- )
984
- })}
968
+ {ROLE_PILLS.map((pill) => (
969
+ <FilterPill key={pill} active={roleFilter === pill} onClick={() => setRoleFilter(pill)}>
970
+ {pill}
971
+ </FilterPill>
972
+ ))}
985
973
  </div>
986
974
 
987
975
  {/* Table / states */}
@@ -2,13 +2,13 @@
2
2
 
3
3
  import { useState, useMemo, useCallback } from 'react'
4
4
  import { Star, Copy, Trash2 } from 'lucide-react'
5
- import * as SwitchPrimitive from '@radix-ui/react-switch'
6
5
  import type { BlockNode, BlockTypeDefinition, FieldDefinition } from '@actuate-media/cms-core'
7
6
  import { BlockCatalog } from '@actuate-media/cms-core/page-builder'
8
7
  import { AIBlockAssist } from './AIBlockAssist.js'
9
8
  import { RelationshipField } from './RelationshipField.js'
10
9
  import { TipTapEditor } from '../../components/TipTapEditor.js'
11
10
  import { MediaPickerModal } from '../../components/MediaPickerModal.js'
11
+ import { Toggle } from '../../components/ui/Toggle.js'
12
12
 
13
13
  export interface BlockEditorProps {
14
14
  node: BlockNode
@@ -224,14 +224,11 @@ function FieldRenderer({ name, definition, value, onChange }: FieldRendererProps
224
224
  return (
225
225
  <div className="flex items-center justify-between">
226
226
  <label className="text-foreground text-sm font-medium">{label}</label>
227
- <SwitchPrimitive.Root
227
+ <Toggle
228
228
  checked={!!value}
229
229
  onCheckedChange={(checked) => onChange(checked)}
230
- className="bg-input data-[state=checked]:bg-primary relative h-5 w-9 rounded-full transition-colors"
231
230
  aria-label={label}
232
- >
233
- <SwitchPrimitive.Thumb className="bg-background block h-3.5 w-3.5 translate-x-0.5 rounded-full shadow-sm transition-transform data-[state=checked]:translate-x-[18px]" />
234
- </SwitchPrimitive.Root>
231
+ />
235
232
  </div>
236
233
  )
237
234
 
@@ -14,7 +14,7 @@ import {
14
14
  ArrowDownToLine,
15
15
  Minus,
16
16
  } from 'lucide-react'
17
- import * as SwitchPrimitive from '@radix-ui/react-switch'
17
+ import { Toggle } from '../../components/ui/Toggle.js'
18
18
  import type {
19
19
  BuilderNode,
20
20
  SectionNode,
@@ -231,14 +231,11 @@ function SectionFields({
231
231
  <p className={SECTION_HEADING_CLASS}>Attributes</p>
232
232
  <div className="flex items-center justify-between">
233
233
  <label className="text-foreground text-sm font-medium">Visibility</label>
234
- <SwitchPrimitive.Root
234
+ <Toggle
235
235
  checked={s.visibility !== 'hidden'}
236
236
  onCheckedChange={(checked) => updateSetting('visibility', checked ? 'visible' : 'hidden')}
237
- className="bg-input data-[state=checked]:bg-primary relative h-5 w-9 rounded-full transition-colors"
238
237
  aria-label="Section visibility"
239
- >
240
- <SwitchPrimitive.Thumb className="bg-background block h-3.5 w-3.5 translate-x-0.5 rounded-full shadow-sm transition-transform data-[state=checked]:translate-x-[18px]" />
241
- </SwitchPrimitive.Root>
238
+ />
242
239
  </div>
243
240
  <div>
244
241
  <label className={LABEL_CLASS}>HTML ID</label>
@@ -389,25 +386,19 @@ function RowFields({
389
386
  <p className={SECTION_HEADING_CLASS}>Mobile</p>
390
387
  <div className="flex items-center justify-between">
391
388
  <label className="text-foreground text-sm font-medium">Reverse on Mobile</label>
392
- <SwitchPrimitive.Root
389
+ <Toggle
393
390
  checked={!!s.reverseOnMobile}
394
391
  onCheckedChange={(checked) => updateSetting('reverseOnMobile', checked)}
395
- className="bg-input data-[state=checked]:bg-primary relative h-5 w-9 rounded-full transition-colors"
396
392
  aria-label="Reverse column order on mobile"
397
- >
398
- <SwitchPrimitive.Thumb className="bg-background block h-3.5 w-3.5 translate-x-0.5 rounded-full shadow-sm transition-transform data-[state=checked]:translate-x-[18px]" />
399
- </SwitchPrimitive.Root>
393
+ />
400
394
  </div>
401
395
  <div className="flex items-center justify-between">
402
396
  <label className="text-foreground text-sm font-medium">Wrap on Mobile</label>
403
- <SwitchPrimitive.Root
397
+ <Toggle
404
398
  checked={!!s.wrapOnMobile}
405
399
  onCheckedChange={(checked) => updateSetting('wrapOnMobile', checked)}
406
- className="bg-input data-[state=checked]:bg-primary relative h-5 w-9 rounded-full transition-colors"
407
400
  aria-label="Wrap columns on mobile"
408
- >
409
- <SwitchPrimitive.Thumb className="bg-background block h-3.5 w-3.5 translate-x-0.5 rounded-full shadow-sm transition-transform data-[state=checked]:translate-x-[18px]" />
410
- </SwitchPrimitive.Root>
401
+ />
411
402
  </div>
412
403
 
413
404
  <p className={SECTION_HEADING_CLASS}>Column Presets</p>
@@ -47,7 +47,7 @@ export function PageTemplates({ onNavigate }: PageTemplatesProps) {
47
47
 
48
48
  <div className="mb-4 flex items-center justify-between">
49
49
  <div>
50
- <h1 className="text-foreground mb-1 text-2xl font-medium">Page Templates</h1>
50
+ <h1 className="text-foreground mb-1">Page Templates</h1>
51
51
  <p className="text-muted-foreground text-sm">
52
52
  {templates.length} saved template{templates.length === 1 ? '' : 's'}
53
53
  </p>
@@ -169,7 +169,7 @@ export function SavedSections({ onNavigate, config }: SavedSectionsProps) {
169
169
  <div className="p-4 pr-8">
170
170
  <div className="mb-6 flex items-center justify-between">
171
171
  <div>
172
- <h1 className="text-foreground text-2xl font-medium">Saved Sections</h1>
172
+ <h1 className="text-foreground">Saved Sections</h1>
173
173
  <p className="text-muted-foreground mt-1 text-sm">
174
174
  Reusable sections for the page builder
175
175
  </p>
@@ -393,7 +393,7 @@ function SectionForm({ initial, saving, onSave, onCancel }: SectionFormProps) {
393
393
  <ArrowLeft size={16} />
394
394
  Back
395
395
  </button>
396
- <h1 className="text-foreground text-2xl font-medium">
396
+ <h1 className="text-foreground">
397
397
  {initial ? `Edit: ${initial.name}` : 'Create Saved Section'}
398
398
  </h1>
399
399
  </div>
@@ -18,7 +18,7 @@ export function Widgets({ onNavigate }: WidgetsProps) {
18
18
  return (
19
19
  <div className="p-4 pr-8">
20
20
  <div className="mb-4">
21
- <h1 className="text-foreground mb-1 text-2xl font-medium">Widgets</h1>
21
+ <h1 className="text-foreground mb-1">Widgets</h1>
22
22
  <p className="text-muted-foreground text-sm">
23
23
  Reusable, embeddable components for pages and saved sections.
24
24
  </p>
@@ -19,6 +19,7 @@ import {
19
19
  } from '@dnd-kit/sortable'
20
20
  import { CSS } from '@dnd-kit/utilities'
21
21
  import { MediaPickerModal } from '../../components/MediaPickerModal.js'
22
+ import { Toggle } from '../../components/ui/Toggle.js'
22
23
  import type { PageSection } from '../../lib/page-editor-service.js'
23
24
  import type { ResolvedRepeater, SectionSettings } from './section-types.js'
24
25
  import {
@@ -497,28 +498,15 @@ function BooleanField({
497
498
  }) {
498
499
  return (
499
500
  <div>
500
- <button
501
- type="button"
502
- id={id}
503
- role="switch"
504
- aria-checked={value}
505
- onClick={() => onChange(!value)}
506
- className="border-border text-foreground hover:bg-accent flex w-full items-center justify-between rounded-md border px-2.5 py-1.5 text-sm"
507
- >
501
+ <div className="border-border text-foreground flex w-full items-center justify-between rounded-md border px-2.5 py-1.5 text-sm">
508
502
  <span>{field.label}</span>
509
- <span
510
- aria-hidden
511
- className={`relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors ${
512
- value ? 'bg-primary' : 'bg-muted'
513
- }`}
514
- >
515
- <span
516
- className={`bg-card inline-block h-4 w-4 transform rounded-full transition-transform ${
517
- value ? 'translate-x-4' : 'translate-x-0.5'
518
- }`}
519
- />
520
- </span>
521
- </button>
503
+ <Toggle
504
+ id={id}
505
+ checked={value}
506
+ onCheckedChange={(v) => onChange(v)}
507
+ aria-label={field.label}
508
+ />
509
+ </div>
522
510
  {field.help && <p className="text-muted-foreground mt-1 text-xs">{field.help}</p>}
523
511
  </div>
524
512
  )
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import { X } from 'lucide-react'
4
+ import { Toggle } from '../../components/ui/Toggle.js'
4
5
  import type { PostHeaderConfig } from '../../lib/post-editor-service.js'
5
6
 
6
7
  interface PostHeaderPanelProps {
@@ -75,29 +76,18 @@ export function PostHeaderPanel({ header, canEdit, onClose, onChange }: PostHead
75
76
  {TOGGLES.map((t) => {
76
77
  const value = header[t.key] === true
77
78
  return (
78
- <button
79
+ <div
79
80
  key={t.key}
80
- type="button"
81
- role="switch"
82
- aria-checked={value}
83
- disabled={!canEdit}
84
- onClick={() => onChange({ [t.key]: !value } as Partial<PostHeaderConfig>)}
85
- className="border-border text-foreground hover:bg-accent flex w-full items-center justify-between rounded-md border px-2.5 py-1.5 text-sm disabled:opacity-50"
81
+ className="border-border text-foreground flex w-full items-center justify-between rounded-md border px-2.5 py-1.5 text-sm"
86
82
  >
87
83
  <span>{t.label}</span>
88
- <span
89
- aria-hidden
90
- className={`relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors ${
91
- value ? 'bg-primary' : 'bg-muted'
92
- }`}
93
- >
94
- <span
95
- className={`bg-card inline-block h-4 w-4 transform rounded-full transition-transform ${
96
- value ? 'translate-x-4' : 'translate-x-0.5'
97
- }`}
98
- />
99
- </span>
100
- </button>
84
+ <Toggle
85
+ checked={value}
86
+ disabled={!canEdit}
87
+ onCheckedChange={(v) => onChange({ [t.key]: v } as Partial<PostHeaderConfig>)}
88
+ aria-label={t.label}
89
+ />
90
+ </div>
101
91
  )
102
92
  })}
103
93
  </fieldset>
@@ -4,6 +4,7 @@ import * as Dialog from '@radix-ui/react-dialog'
4
4
  import { AlertTriangle, Check, Loader2, Lock } from 'lucide-react'
5
5
  import { useId, type ReactNode } from 'react'
6
6
  import { Card, CardContent, CardDescription, CardHeader } from '../../components/ui/Card.js'
7
+ import { Toggle } from '../../components/ui/Toggle.js'
7
8
  import { adminPortalContainer } from '../../lib/portal-container.js'
8
9
  import type { SettingsSourceMetadata, ValidationIssue } from './useGeneralSettings.js'
9
10
 
@@ -177,23 +178,14 @@ export function SettingsToggleRow({
177
178
  </div>
178
179
  <p className="text-muted-foreground mt-0.5 text-sm">{description}</p>
179
180
  </div>
180
- <button
181
- type="button"
182
- role="switch"
183
- aria-checked={checked}
184
- aria-label={label}
185
- disabled={disabled}
186
- onClick={() => onChange(!checked)}
187
- className={`focus-visible:ring-ring relative mt-0.5 h-6 w-11 shrink-0 rounded-full transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 ${
188
- checked ? 'bg-brand' : 'bg-switch-background'
189
- }`}
190
- >
191
- <span
192
- className={`bg-card absolute top-0.5 block h-5 w-5 rounded-full shadow-sm transition-transform ${
193
- checked ? 'translate-x-[22px]' : 'translate-x-0.5'
194
- }`}
181
+ <div className="mt-0.5 shrink-0">
182
+ <Toggle
183
+ checked={checked}
184
+ onCheckedChange={onChange}
185
+ disabled={disabled}
186
+ aria-label={label}
195
187
  />
196
- </button>
188
+ </div>
197
189
  </div>
198
190
  )
199
191
  }