@actuate-media/cms-admin 0.63.2 → 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 (245) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/design-system/README.md +53 -0
  3. package/design-system/tokens.css +114 -0
  4. package/design-system/tokens.json +149 -0
  5. package/design-system/tokens.scss +108 -0
  6. package/design-system/tokens.tailwind.cjs +80 -0
  7. package/dist/__tests__/components/ui-primitives.test.d.ts +2 -0
  8. package/dist/__tests__/components/ui-primitives.test.d.ts.map +1 -0
  9. package/dist/__tests__/components/ui-primitives.test.js +45 -0
  10. package/dist/__tests__/components/ui-primitives.test.js.map +1 -0
  11. package/dist/__tests__/layout/primitives.test.js +9 -2
  12. package/dist/__tests__/layout/primitives.test.js.map +1 -1
  13. package/dist/__tests__/views/updates-tab.render.test.js +1 -1
  14. package/dist/__tests__/views/updates-tab.render.test.js.map +1 -1
  15. package/dist/actuate-admin.css +1 -1
  16. package/dist/components/EnabledSwitch.d.ts +2 -5
  17. package/dist/components/EnabledSwitch.d.ts.map +1 -1
  18. package/dist/components/EnabledSwitch.js +4 -7
  19. package/dist/components/EnabledSwitch.js.map +1 -1
  20. package/dist/components/SEOPanel.d.ts.map +1 -1
  21. package/dist/components/SEOPanel.js +2 -1
  22. package/dist/components/SEOPanel.js.map +1 -1
  23. package/dist/components/ThemeProvider.d.ts.map +1 -1
  24. package/dist/components/ThemeProvider.js +5 -3
  25. package/dist/components/ThemeProvider.js.map +1 -1
  26. package/dist/components/forms/FieldsPanel.js +2 -2
  27. package/dist/components/forms/FieldsPanel.js.map +1 -1
  28. package/dist/components/forms/NotificationsPanel.js +2 -2
  29. package/dist/components/forms/NotificationsPanel.js.map +1 -1
  30. package/dist/components/forms/WebhooksPanel.d.ts.map +1 -1
  31. package/dist/components/forms/WebhooksPanel.js +2 -2
  32. package/dist/components/forms/WebhooksPanel.js.map +1 -1
  33. package/dist/components/forms/primitives.d.ts.map +1 -1
  34. package/dist/components/forms/primitives.js +5 -8
  35. package/dist/components/forms/primitives.js.map +1 -1
  36. package/dist/components/posts/badges.d.ts.map +1 -1
  37. package/dist/components/posts/badges.js +7 -13
  38. package/dist/components/posts/badges.js.map +1 -1
  39. package/dist/components/seo/RedirectEditorDrawer.d.ts.map +1 -1
  40. package/dist/components/seo/RedirectEditorDrawer.js +2 -1
  41. package/dist/components/seo/RedirectEditorDrawer.js.map +1 -1
  42. package/dist/components/seo/SeoEditorDrawer.d.ts.map +1 -1
  43. package/dist/components/seo/SeoEditorDrawer.js +3 -2
  44. package/dist/components/seo/SeoEditorDrawer.js.map +1 -1
  45. package/dist/components/seo/primitives.d.ts.map +1 -1
  46. package/dist/components/seo/primitives.js +11 -15
  47. package/dist/components/seo/primitives.js.map +1 -1
  48. package/dist/components/ui/Button.d.ts +1 -1
  49. package/dist/components/ui/Button.js +1 -1
  50. package/dist/components/ui/Button.js.map +1 -1
  51. package/dist/components/ui/Card.d.ts +6 -5
  52. package/dist/components/ui/Card.d.ts.map +1 -1
  53. package/dist/components/ui/Card.js +20 -18
  54. package/dist/components/ui/Card.js.map +1 -1
  55. package/dist/components/ui/FilterPill.d.ts +14 -0
  56. package/dist/components/ui/FilterPill.d.ts.map +1 -0
  57. package/dist/components/ui/FilterPill.js +8 -0
  58. package/dist/components/ui/FilterPill.js.map +1 -0
  59. package/dist/components/ui/Input.d.ts +2 -1
  60. package/dist/components/ui/Input.d.ts.map +1 -1
  61. package/dist/components/ui/Input.js +9 -8
  62. package/dist/components/ui/Input.js.map +1 -1
  63. package/dist/components/ui/SearchInput.d.ts.map +1 -1
  64. package/dist/components/ui/SearchInput.js +2 -1
  65. package/dist/components/ui/SearchInput.js.map +1 -1
  66. package/dist/components/ui/Select.d.ts.map +1 -1
  67. package/dist/components/ui/Select.js +9 -10
  68. package/dist/components/ui/Select.js.map +1 -1
  69. package/dist/components/ui/StatPill.d.ts +18 -0
  70. package/dist/components/ui/StatPill.d.ts.map +1 -0
  71. package/dist/components/ui/StatPill.js +5 -0
  72. package/dist/components/ui/StatPill.js.map +1 -0
  73. package/dist/components/ui/StatusBadge.d.ts +25 -0
  74. package/dist/components/ui/StatusBadge.d.ts.map +1 -0
  75. package/dist/components/ui/StatusBadge.js +41 -0
  76. package/dist/components/ui/StatusBadge.js.map +1 -0
  77. package/dist/components/ui/Toggle.d.ts +31 -0
  78. package/dist/components/ui/Toggle.d.ts.map +1 -0
  79. package/dist/components/ui/Toggle.js +41 -0
  80. package/dist/components/ui/Toggle.js.map +1 -0
  81. package/dist/components/ui/form-control.d.ts +11 -0
  82. package/dist/components/ui/form-control.d.ts.map +1 -0
  83. package/dist/components/ui/form-control.js +11 -0
  84. package/dist/components/ui/form-control.js.map +1 -0
  85. package/dist/components/ui/index.d.ts +9 -0
  86. package/dist/components/ui/index.d.ts.map +1 -1
  87. package/dist/components/ui/index.js +5 -0
  88. package/dist/components/ui/index.js.map +1 -1
  89. package/dist/fields/PropInput.d.ts.map +1 -1
  90. package/dist/fields/PropInput.js +2 -1
  91. package/dist/fields/PropInput.js.map +1 -1
  92. package/dist/fields/ToggleField.d.ts.map +1 -1
  93. package/dist/fields/ToggleField.js +2 -1
  94. package/dist/fields/ToggleField.js.map +1 -1
  95. package/dist/layout/Header.d.ts.map +1 -1
  96. package/dist/layout/Header.js +5 -2
  97. package/dist/layout/Header.js.map +1 -1
  98. package/dist/layout/Sidebar.js +1 -1
  99. package/dist/layout/Sidebar.js.map +1 -1
  100. package/dist/layout/primitives/PageContainer.d.ts.map +1 -1
  101. package/dist/layout/primitives/PageContainer.js +1 -1
  102. package/dist/layout/primitives/PageContainer.js.map +1 -1
  103. package/dist/layout/primitives/tokens.d.ts +10 -4
  104. package/dist/layout/primitives/tokens.d.ts.map +1 -1
  105. package/dist/layout/primitives/tokens.js +10 -4
  106. package/dist/layout/primitives/tokens.js.map +1 -1
  107. package/dist/views/Dashboard.js +1 -1
  108. package/dist/views/Dashboard.js.map +1 -1
  109. package/dist/views/FormEditor.d.ts.map +1 -1
  110. package/dist/views/FormEditor.js +2 -1
  111. package/dist/views/FormEditor.js.map +1 -1
  112. package/dist/views/FormEntries.js +1 -1
  113. package/dist/views/FormEntries.js.map +1 -1
  114. package/dist/views/FormSubmissions.js +1 -1
  115. package/dist/views/FormSubmissions.js.map +1 -1
  116. package/dist/views/Forms.js +3 -3
  117. package/dist/views/Forms.js.map +1 -1
  118. package/dist/views/MediaBrowser.d.ts.map +1 -1
  119. package/dist/views/MediaBrowser.js +3 -2
  120. package/dist/views/MediaBrowser.js.map +1 -1
  121. package/dist/views/Pages/PageTagManager.d.ts.map +1 -1
  122. package/dist/views/Pages/PageTagManager.js +1 -1
  123. package/dist/views/Pages/PageTagManager.js.map +1 -1
  124. package/dist/views/Pages/PagesListView.js +1 -1
  125. package/dist/views/Pages/PagesListView.js.map +1 -1
  126. package/dist/views/Pages.js +1 -1
  127. package/dist/views/Pages.js.map +1 -1
  128. package/dist/views/Posts/NewPostPicker.d.ts.map +1 -1
  129. package/dist/views/Posts/NewPostPicker.js +1 -1
  130. package/dist/views/Posts/NewPostPicker.js.map +1 -1
  131. package/dist/views/Posts/NewPostTypeInstructions.d.ts.map +1 -1
  132. package/dist/views/Posts/NewPostTypeInstructions.js +1 -1
  133. package/dist/views/Posts/NewPostTypeInstructions.js.map +1 -1
  134. package/dist/views/Posts/PostTypesView.d.ts.map +1 -1
  135. package/dist/views/Posts/PostTypesView.js +1 -1
  136. package/dist/views/Posts/PostTypesView.js.map +1 -1
  137. package/dist/views/Posts/PostsListView.js +1 -1
  138. package/dist/views/Posts/PostsListView.js.map +1 -1
  139. package/dist/views/Profile.js +1 -1
  140. package/dist/views/Profile.js.map +1 -1
  141. package/dist/views/Redirects.js +1 -1
  142. package/dist/views/Redirects.js.map +1 -1
  143. package/dist/views/SEO.js +1 -1
  144. package/dist/views/SEO.js.map +1 -1
  145. package/dist/views/ScriptTagEditor.d.ts.map +1 -1
  146. package/dist/views/ScriptTagEditor.js +1 -1
  147. package/dist/views/ScriptTagEditor.js.map +1 -1
  148. package/dist/views/ScriptTags.d.ts.map +1 -1
  149. package/dist/views/ScriptTags.js +2 -1
  150. package/dist/views/ScriptTags.js.map +1 -1
  151. package/dist/views/Settings.js +1 -1
  152. package/dist/views/Settings.js.map +1 -1
  153. package/dist/views/Users.d.ts.map +1 -1
  154. package/dist/views/Users.js +2 -6
  155. package/dist/views/Users.js.map +1 -1
  156. package/dist/views/page-builder/BlockEditor.d.ts.map +1 -1
  157. package/dist/views/page-builder/BlockEditor.js +2 -2
  158. package/dist/views/page-builder/BlockEditor.js.map +1 -1
  159. package/dist/views/page-builder/BottomBar.d.ts.map +1 -1
  160. package/dist/views/page-builder/BottomBar.js +2 -2
  161. package/dist/views/page-builder/BottomBar.js.map +1 -1
  162. package/dist/views/page-builder/NodeSettings.js +3 -3
  163. package/dist/views/page-builder/NodeSettings.js.map +1 -1
  164. package/dist/views/page-builder/PageTemplates.js +1 -1
  165. package/dist/views/page-builder/PageTemplates.js.map +1 -1
  166. package/dist/views/page-builder/SavedSections.js +2 -2
  167. package/dist/views/page-builder/SavedSections.js.map +1 -1
  168. package/dist/views/page-builder/Widgets.js +1 -1
  169. package/dist/views/page-builder/Widgets.js.map +1 -1
  170. package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
  171. package/dist/views/page-editor/SectionInspector.js +2 -1
  172. package/dist/views/page-editor/SectionInspector.js.map +1 -1
  173. package/dist/views/post-editor/PostHeaderPanel.d.ts.map +1 -1
  174. package/dist/views/post-editor/PostHeaderPanel.js +2 -1
  175. package/dist/views/post-editor/PostHeaderPanel.js.map +1 -1
  176. package/dist/views/seo/TechnicalTab.d.ts.map +1 -1
  177. package/dist/views/seo/TechnicalTab.js +2 -2
  178. package/dist/views/seo/TechnicalTab.js.map +1 -1
  179. package/dist/views/settings/components.d.ts.map +1 -1
  180. package/dist/views/settings/components.js +4 -2
  181. package/dist/views/settings/components.js.map +1 -1
  182. package/package.json +9 -4
  183. package/src/__tests__/components/ui-primitives.test.tsx +58 -0
  184. package/src/__tests__/layout/primitives.test.ts +10 -2
  185. package/src/__tests__/views/updates-tab.render.test.tsx +2 -1
  186. package/src/components/EnabledSwitch.tsx +4 -22
  187. package/src/components/SEOPanel.tsx +2 -11
  188. package/src/components/ThemeProvider.tsx +5 -3
  189. package/src/components/forms/FieldsPanel.tsx +4 -6
  190. package/src/components/forms/NotificationsPanel.tsx +2 -9
  191. package/src/components/forms/WebhooksPanel.tsx +4 -6
  192. package/src/components/forms/primitives.tsx +6 -16
  193. package/src/components/posts/badges.tsx +8 -21
  194. package/src/components/seo/RedirectEditorDrawer.tsx +12 -16
  195. package/src/components/seo/SeoEditorDrawer.tsx +16 -24
  196. package/src/components/seo/primitives.tsx +18 -34
  197. package/src/components/ui/Button.tsx +1 -1
  198. package/src/components/ui/Card.tsx +35 -18
  199. package/src/components/ui/FilterPill.tsx +43 -0
  200. package/src/components/ui/Input.tsx +15 -17
  201. package/src/components/ui/SearchInput.tsx +9 -4
  202. package/src/components/ui/Select.tsx +15 -18
  203. package/src/components/ui/StatPill.tsx +37 -0
  204. package/src/components/ui/StatusBadge.tsx +66 -0
  205. package/src/components/ui/Toggle.tsx +85 -0
  206. package/src/components/ui/form-control.ts +12 -0
  207. package/src/components/ui/index.ts +9 -0
  208. package/src/fields/PropInput.tsx +6 -15
  209. package/src/fields/ToggleField.tsx +3 -15
  210. package/src/layout/Header.tsx +6 -2
  211. package/src/layout/Sidebar.tsx +3 -3
  212. package/src/layout/primitives/PageContainer.tsx +1 -5
  213. package/src/layout/primitives/tokens.ts +11 -4
  214. package/src/styles/actuate-tokens.css +97 -0
  215. package/src/styles/theme.css +88 -156
  216. package/src/views/Dashboard.tsx +1 -1
  217. package/src/views/FormEditor.tsx +6 -9
  218. package/src/views/FormEntries.tsx +1 -1
  219. package/src/views/FormSubmissions.tsx +1 -1
  220. package/src/views/Forms.tsx +5 -7
  221. package/src/views/MediaBrowser.tsx +4 -7
  222. package/src/views/Pages/PageTagManager.tsx +1 -3
  223. package/src/views/Pages/PagesListView.tsx +1 -1
  224. package/src/views/Pages.tsx +1 -1
  225. package/src/views/Posts/NewPostPicker.tsx +1 -3
  226. package/src/views/Posts/NewPostTypeInstructions.tsx +1 -3
  227. package/src/views/Posts/PostTypesView.tsx +1 -3
  228. package/src/views/Posts/PostsListView.tsx +1 -1
  229. package/src/views/Profile.tsx +1 -1
  230. package/src/views/Redirects.tsx +1 -1
  231. package/src/views/SEO.tsx +1 -1
  232. package/src/views/ScriptTagEditor.tsx +1 -3
  233. package/src/views/ScriptTags.tsx +17 -1
  234. package/src/views/Settings.tsx +1 -1
  235. package/src/views/Users.tsx +6 -18
  236. package/src/views/page-builder/BlockEditor.tsx +3 -6
  237. package/src/views/page-builder/BottomBar.tsx +3 -6
  238. package/src/views/page-builder/NodeSettings.tsx +7 -16
  239. package/src/views/page-builder/PageTemplates.tsx +1 -1
  240. package/src/views/page-builder/SavedSections.tsx +2 -2
  241. package/src/views/page-builder/Widgets.tsx +1 -1
  242. package/src/views/page-editor/SectionInspector.tsx +9 -21
  243. package/src/views/post-editor/PostHeaderPanel.tsx +10 -20
  244. package/src/views/seo/TechnicalTab.tsx +3 -6
  245. package/src/views/settings/components.tsx +19 -26
@@ -89,7 +89,7 @@ const ThemeContext = createContext<ThemeContextValue>({
89
89
  setAccent: () => {},
90
90
  density: 'default',
91
91
  setDensity: () => {},
92
- font: 'DM Sans',
92
+ font: 'System UI',
93
93
  setFont: () => {},
94
94
  reduceMotion: false,
95
95
  setReduceMotion: () => {},
@@ -126,6 +126,8 @@ function applyColorMode(resolved: 'light' | 'dark') {
126
126
  const el = adminRootEl()
127
127
  if (!el) return
128
128
  el.classList.toggle('dark', resolved === 'dark')
129
+ // Mirror handoff tokens.css selector so unscoped handoff styles can target the admin root.
130
+ el.setAttribute('data-theme', resolved)
129
131
  }
130
132
 
131
133
  function applyAccent(accent: string, resolved: 'light' | 'dark') {
@@ -166,7 +168,7 @@ export function ThemeProvider({ children, defaultDarkMode }: ThemeProviderProps)
166
168
  const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('light')
167
169
  const [accent, setAccentState] = useState('purple')
168
170
  const [density, setDensityState] = useState<SidebarDensity>('default')
169
- const [font, setFontState] = useState('DM Sans')
171
+ const [font, setFontState] = useState('System UI')
170
172
  const [reduceMotion, setReduceMotionState] = useState(false)
171
173
  const [adminBrand, setAdminBrandState] = useState<AdminBrandCache>(DEFAULT_BRAND)
172
174
 
@@ -187,7 +189,7 @@ export function ThemeProvider({ children, defaultDarkMode }: ThemeProviderProps)
187
189
 
188
190
  const storedAccent = localStorage.getItem(KEY_ACCENT) || 'purple'
189
191
  const storedDensity = (localStorage.getItem(KEY_DENSITY) as SidebarDensity | null) || 'default'
190
- const storedFont = localStorage.getItem(KEY_FONT) || 'DM Sans'
192
+ const storedFont = localStorage.getItem(KEY_FONT) || 'System UI'
191
193
  const storedMotion = localStorage.getItem(KEY_MOTION) === 'true'
192
194
  let storedBrand = DEFAULT_BRAND
193
195
  try {
@@ -23,7 +23,7 @@ import {
23
23
  } from '@dnd-kit/sortable'
24
24
  import { CSS } from '@dnd-kit/utilities'
25
25
  import { ChevronDown, GripVertical, Plus, Trash2 } from 'lucide-react'
26
- import * as Switch from '@radix-ui/react-switch'
26
+ import { Toggle } from '../ui/Toggle.js'
27
27
  import { useId, useState } from 'react'
28
28
  import type { FormField, FormFieldType } from '../../lib/forms-service.js'
29
29
  import { FieldTypeBadge, FormsEmptyState } from './primitives.js'
@@ -353,14 +353,12 @@ function FieldEditor({
353
353
  <label htmlFor={reqId} className="text-foreground text-sm font-medium">
354
354
  Required
355
355
  </label>
356
- <Switch.Root
356
+ <Toggle
357
357
  id={reqId}
358
358
  checked={field.required}
359
359
  onCheckedChange={(v) => onUpdate({ required: v })}
360
- className="bg-input-background data-[state=checked]:bg-primary focus-visible:ring-ring relative h-[18px] w-8 shrink-0 rounded-full transition-colors focus-visible:ring-2 focus-visible:outline-none"
361
- >
362
- <Switch.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-[14px]" />
363
- </Switch.Root>
360
+ aria-labelledby={reqId}
361
+ />
364
362
  </div>
365
363
  </div>
366
364
  )
@@ -6,7 +6,7 @@
6
6
  * autoresponder). Email delivery uses the configured email adapter when enabled.
7
7
  */
8
8
  import { useEffect, useState } from 'react'
9
- import * as Switch from '@radix-ui/react-switch'
9
+ import { Toggle } from '../ui/Toggle.js'
10
10
  import { toast } from 'sonner'
11
11
  import {
12
12
  fetchNotificationSettings,
@@ -210,14 +210,7 @@ function ToggleRow({
210
210
  <span className="text-foreground block text-sm font-medium">{label}</span>
211
211
  <span className="text-muted-foreground block text-sm">{help}</span>
212
212
  </span>
213
- <Switch.Root
214
- checked={checked}
215
- onCheckedChange={onChange}
216
- aria-label={label}
217
- className="bg-destructive data-[state=checked]:bg-success focus-visible:ring-ring relative h-[18px] w-8 shrink-0 rounded-full transition-colors focus-visible:ring-2 focus-visible:outline-none"
218
- >
219
- <Switch.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-[14px]" />
220
- </Switch.Root>
213
+ <Toggle checked={checked} onCheckedChange={onChange} aria-label={label} />
221
214
  </div>
222
215
  )
223
216
  }
@@ -5,7 +5,7 @@
5
5
  * (Zapier Catch Hook, Make, custom CRM endpoints) and guides editors through
6
6
  * the default Zapier → Webhooks by Zapier → Catch Hook setup.
7
7
  */
8
- import * as Switch from '@radix-ui/react-switch'
8
+ import { Toggle } from '../ui/Toggle.js'
9
9
  import { ExternalLink, Loader2, Plus, Trash2, Zap } from 'lucide-react'
10
10
  import { useEffect, useState } from 'react'
11
11
  import { toast } from 'sonner'
@@ -265,14 +265,12 @@ export function WebhooksPanel({ formId, formSlug }: { formId: string; formSlug:
265
265
  </p>
266
266
  </div>
267
267
  <div className="flex shrink-0 flex-wrap items-center gap-2">
268
- <Switch.Root
268
+ <Toggle
269
269
  checked={webhook.enabled}
270
270
  onCheckedChange={(v) => void toggleEnabled(webhook, v)}
271
271
  aria-label={`Enable webhook ${webhook.url}`}
272
- className="bg-destructive data-[state=checked]:bg-success focus-visible:ring-ring relative h-[18px] w-8 rounded-full transition-colors focus-visible:ring-2 focus-visible:outline-none"
273
- >
274
- <Switch.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-[14px]" />
275
- </Switch.Root>
272
+ variant="enabled"
273
+ />
276
274
  <button
277
275
  type="button"
278
276
  className={btnSecondary}
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import type { ReactNode } from 'react'
11
11
  import { AlertTriangle, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react'
12
+ import { StatusBadge, type StatusBadgeTone } from '../ui/StatusBadge.js'
12
13
  import type { FormFieldType, FormStatus } from '../../lib/forms-service.js'
13
14
 
14
15
  // ─── Field-type badges ─────────────────────────────────────────────────────
@@ -70,26 +71,15 @@ export function FieldTypeBadge({ type }: { type: FormFieldType }) {
70
71
 
71
72
  // ─── Status badges ─────────────────────────────────────────────────────────
72
73
 
73
- const STATUS_META: Record<FormStatus, { label: string; cls: string; dot: string }> = {
74
- active: { label: 'Active', cls: 'bg-success/10 text-success', dot: 'bg-success' },
75
- draft: { label: 'Draft', cls: 'bg-warning/10 text-warning', dot: 'bg-warning' },
76
- archived: {
77
- label: 'Archived',
78
- cls: 'bg-muted text-muted-foreground',
79
- dot: 'bg-muted-foreground',
80
- },
74
+ const STATUS_META: Record<FormStatus, { label: string; tone: StatusBadgeTone }> = {
75
+ active: { label: 'Active', tone: 'success' },
76
+ draft: { label: 'Draft', tone: 'warning' },
77
+ archived: { label: 'Archived', tone: 'muted' },
81
78
  }
82
79
 
83
80
  export function FormStatusBadge({ status }: { status: FormStatus }) {
84
81
  const meta = STATUS_META[status] ?? STATUS_META.draft
85
- return (
86
- <span
87
- className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium ${meta.cls}`}
88
- >
89
- <span className={`h-1.5 w-1.5 rounded-full ${meta.dot}`} aria-hidden />
90
- {meta.label}
91
- </span>
92
- )
82
+ return <StatusBadge tone={meta.tone}>{meta.label}</StatusBadge>
93
83
  }
94
84
 
95
85
  /** Small red count badge for unread submissions. Hidden when count is 0. */
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import type { ReactNode } from 'react'
4
+ import { StatusBadge, type StatusBadgeTone } from '../ui/StatusBadge.js'
4
5
  import type { PostStatus } from '../../lib/posts-service.js'
5
6
 
6
7
  /**
@@ -65,32 +66,18 @@ const STATUS_LABEL: Record<PostStatus, string> = {
65
66
  ARCHIVED: 'Archived',
66
67
  }
67
68
 
68
- const STATUS_PALETTE: Record<PostStatus, string> = {
69
- PUBLISHED: 'green',
70
- DRAFT: 'gray',
71
- SCHEDULED: 'orange',
72
- ARCHIVED: 'gray',
73
- }
74
-
75
- const STATUS_GLYPH: Record<PostStatus, string> = {
76
- PUBLISHED: '●',
77
- DRAFT: '○',
78
- SCHEDULED: '◐',
79
- ARCHIVED: '◌',
69
+ const STATUS_TONE: Record<PostStatus, StatusBadgeTone> = {
70
+ PUBLISHED: 'success',
71
+ DRAFT: 'neutral',
72
+ SCHEDULED: 'warning',
73
+ ARCHIVED: 'muted',
80
74
  }
81
75
 
82
76
  export function PostStatusBadge({ status }: { status: PostStatus }) {
83
- const p = paletteFor(STATUS_PALETTE[status])
84
77
  return (
85
- <span
86
- className={`inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium ${p.bg} ${p.text}`}
87
- aria-label={`Status: ${STATUS_LABEL[status]}`}
88
- >
89
- <span aria-hidden className="text-[10px] leading-none">
90
- {STATUS_GLYPH[status]}
91
- </span>
78
+ <StatusBadge tone={STATUS_TONE[status]} aria-label={`Status: ${STATUS_LABEL[status]}`}>
92
79
  {STATUS_LABEL[status]}
93
- </span>
80
+ </StatusBadge>
94
81
  )
95
82
  }
96
83
 
@@ -11,6 +11,7 @@ import {
11
11
  } from '../../lib/seo-service.js'
12
12
  import { Drawer } from './Drawer.js'
13
13
  import { btnPrimary, btnSecondary } from './primitives.js'
14
+ import { Toggle } from '../ui/Toggle.js'
14
15
 
15
16
  const STATUS_CODES: { value: RedirectStatusCode; label: string }[] = [
16
17
  { value: 301, label: '301 — Permanent' },
@@ -197,19 +198,17 @@ export function RedirectEditorDrawer({
197
198
  </div>
198
199
 
199
200
  <div className="border-border space-y-3 rounded-lg border p-4">
200
- <label className="flex items-center justify-between gap-3">
201
+ <div className="flex items-center justify-between gap-3">
201
202
  <span className="text-foreground text-sm font-medium">Enabled</span>
202
- <input
203
- type="checkbox"
204
- role="switch"
205
- aria-checked={!!form.enabled}
203
+ <Toggle
206
204
  checked={!!form.enabled}
207
- onChange={(e) => update({ enabled: e.target.checked })}
208
- className="accent-primary h-4 w-4"
205
+ onCheckedChange={(v) => update({ enabled: v })}
206
+ variant="enabled"
207
+ aria-label="Enabled"
209
208
  />
210
- </label>
209
+ </div>
211
210
  {!isGone && (
212
- <label className="flex items-center justify-between gap-3">
211
+ <div className="flex items-center justify-between gap-3">
213
212
  <span className="min-w-0">
214
213
  <span className="text-foreground block text-sm font-medium">
215
214
  Preserve query string
@@ -218,15 +217,12 @@ export function RedirectEditorDrawer({
218
217
  Append the original query params to the destination.
219
218
  </span>
220
219
  </span>
221
- <input
222
- type="checkbox"
223
- role="switch"
224
- aria-checked={!!form.preserveQuery}
220
+ <Toggle
225
221
  checked={!!form.preserveQuery}
226
- onChange={(e) => update({ preserveQuery: e.target.checked })}
227
- className="accent-primary h-4 w-4 shrink-0"
222
+ onCheckedChange={(v) => update({ preserveQuery: v })}
223
+ aria-label="Preserve query string"
228
224
  />
229
- </label>
225
+ </div>
230
226
  )}
231
227
  </div>
232
228
  </div>
@@ -17,6 +17,7 @@ import { fixBrandVoiceContent } from '../../lib/brand-voice-client.js'
17
17
  import { BrandVoiceAlignmentBadge } from '../BrandVoiceAlignmentBadge.js'
18
18
  import { Drawer } from './Drawer.js'
19
19
  import { SeoLoading, SeoErrorState, btnPrimary, btnSecondary, gradeMeta } from './primitives.js'
20
+ import { Toggle } from '../ui/Toggle.js'
20
21
 
21
22
  const META_TITLE_MAX = 60
22
23
  const META_DESC_MAX = 160
@@ -422,38 +423,32 @@ export function SeoEditorDrawer({
422
423
 
423
424
  {/* Indexing toggles */}
424
425
  <div className="border-border space-y-3 rounded-lg border p-4">
425
- <label className="flex items-center justify-between gap-3">
426
+ <div className="flex items-center justify-between gap-3">
426
427
  <span className="min-w-0">
427
428
  <span className="text-foreground block text-sm font-medium">No-index</span>
428
429
  <span className="text-muted-foreground block text-sm">
429
430
  Hide this page from search engines.
430
431
  </span>
431
432
  </span>
432
- <input
433
- type="checkbox"
434
- role="switch"
435
- aria-checked={!!form.noindex}
433
+ <Toggle
436
434
  checked={!!form.noindex}
437
- onChange={(e) => update({ noindex: e.target.checked })}
438
- className="accent-primary h-4 w-4"
435
+ onCheckedChange={(v) => update({ noindex: v })}
436
+ aria-label="No-index"
439
437
  />
440
- </label>
441
- <label className="flex items-center justify-between gap-3">
438
+ </div>
439
+ <div className="flex items-center justify-between gap-3">
442
440
  <span className="min-w-0">
443
441
  <span className="text-foreground block text-sm font-medium">No-follow</span>
444
442
  <span className="text-muted-foreground block text-sm">
445
443
  Tell crawlers not to follow links on this page.
446
444
  </span>
447
445
  </span>
448
- <input
449
- type="checkbox"
450
- role="switch"
451
- aria-checked={!!form.nofollow}
446
+ <Toggle
452
447
  checked={!!form.nofollow}
453
- onChange={(e) => update({ nofollow: e.target.checked })}
454
- className="accent-primary h-4 w-4"
448
+ onCheckedChange={(v) => update({ nofollow: v })}
449
+ aria-label="No-follow"
455
450
  />
456
- </label>
451
+ </div>
457
452
  </div>
458
453
 
459
454
  {/* llms.txt curation */}
@@ -499,22 +494,19 @@ export function SeoEditorDrawer({
499
494
  placeholder="Defaults to the content type"
500
495
  />
501
496
  </Field>
502
- <label className="flex items-center justify-between gap-3">
497
+ <div className="flex items-center justify-between gap-3">
503
498
  <span className="min-w-0">
504
499
  <span className="text-foreground block text-sm font-medium">Optional</span>
505
500
  <span className="text-muted-foreground block text-sm">
506
501
  Group under “Optional” — agents skip these first when context is tight.
507
502
  </span>
508
503
  </span>
509
- <input
510
- type="checkbox"
511
- role="switch"
512
- aria-checked={!!form.llmsOptional}
504
+ <Toggle
513
505
  checked={!!form.llmsOptional}
514
- onChange={(e) => update({ llmsOptional: e.target.checked })}
515
- className="accent-primary h-4 w-4"
506
+ onCheckedChange={(v) => update({ llmsOptional: v })}
507
+ aria-label="Optional in llms.txt"
516
508
  />
517
- </label>
509
+ </div>
518
510
  </div>
519
511
 
520
512
  {/* Score summary */}
@@ -7,6 +7,8 @@
7
7
  */
8
8
  import type { ReactNode } from 'react'
9
9
  import { AlertTriangle, Loader2 } from 'lucide-react'
10
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/Card.js'
11
+ import { StatusBadge, type StatusBadgeTone } from '../ui/StatusBadge.js'
10
12
  import type { SeoGrade, SeoSeverity } from '../../lib/seo-service.js'
11
13
 
12
14
  // ─── Grade + severity styling ────────────────────────────────────────
@@ -72,6 +74,14 @@ export function severityMeta(severity: SeoSeverity): GradeMeta {
72
74
 
73
75
  // ─── Badges + bars ───────────────────────────────────────────────────
74
76
 
77
+ const SEO_TONE: Record<'good' | 'fair' | 'poor' | 'critical' | 'neutral', StatusBadgeTone> = {
78
+ good: 'success',
79
+ fair: 'warning',
80
+ poor: 'danger',
81
+ critical: 'danger',
82
+ neutral: 'neutral',
83
+ }
84
+
75
85
  export function SeoStatusBadge({
76
86
  label,
77
87
  tone,
@@ -79,33 +89,7 @@ export function SeoStatusBadge({
79
89
  label: string
80
90
  tone: 'good' | 'fair' | 'poor' | 'critical' | 'neutral'
81
91
  }) {
82
- const cls =
83
- tone === 'good'
84
- ? 'bg-success/10 text-success'
85
- : tone === 'fair'
86
- ? 'bg-warning/10 text-warning'
87
- : tone === 'poor' || tone === 'critical'
88
- ? 'bg-destructive/10 text-destructive'
89
- : 'bg-muted text-muted-foreground'
90
- return (
91
- <span
92
- className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium ${cls}`}
93
- >
94
- <span
95
- className={`h-1.5 w-1.5 rounded-full ${
96
- tone === 'good'
97
- ? 'bg-success'
98
- : tone === 'fair'
99
- ? 'bg-warning'
100
- : tone === 'neutral'
101
- ? 'bg-muted-foreground'
102
- : 'bg-destructive'
103
- }`}
104
- aria-hidden
105
- />
106
- {label}
107
- </span>
108
- )
92
+ return <StatusBadge tone={SEO_TONE[tone]}>{label}</StatusBadge>
109
93
  }
110
94
 
111
95
  /** A horizontal score bar (0-100), coloured by grade. */
@@ -159,18 +143,18 @@ export function SectionCard({
159
143
  className?: string
160
144
  }) {
161
145
  return (
162
- <section className={`border-border bg-card rounded-lg border ${className}`}>
146
+ <Card padding="none" className={className} role="region">
163
147
  {(title || action) && (
164
- <header className="border-border flex items-center justify-between gap-3 border-b px-4 py-3">
148
+ <CardHeader className="flex items-center justify-between gap-3">
165
149
  <div className="min-w-0">
166
- {title && <h3 className="text-foreground text-base font-medium">{title}</h3>}
167
- {description && <p className="text-muted-foreground mt-0.5 text-sm">{description}</p>}
150
+ {title && <CardTitle>{title}</CardTitle>}
151
+ {description && <CardDescription>{description}</CardDescription>}
168
152
  </div>
169
153
  {action}
170
- </header>
154
+ </CardHeader>
171
155
  )}
172
- <div className="p-4">{children}</div>
173
- </section>
156
+ <CardContent className={title || action ? 'pt-0' : ''}>{children}</CardContent>
157
+ </Card>
174
158
  )
175
159
  }
176
160
 
@@ -5,7 +5,7 @@ import { cv, type VariantProps } from '../../lib/cv.js'
5
5
 
6
6
  const button = cv(
7
7
  // Base classes apply to every variant.
8
- 'focus-visible:ring-offset-background inline-flex items-center justify-center gap-2 rounded-[var(--radius)] font-medium transition-colors focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50',
8
+ 'focus-visible:ring-offset-background inline-flex items-center justify-center gap-2 rounded-[var(--r-control)] font-medium transition-colors duration-[var(--motion-fast)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50',
9
9
  {
10
10
  variants: {
11
11
  variant: {
@@ -2,29 +2,31 @@ import { forwardRef, type HTMLAttributes, type ReactNode } from 'react'
2
2
  import { cv, type VariantProps } from '../../lib/cv.js'
3
3
 
4
4
  const card = cv(
5
- 'rounded-[var(--radius)] border border-[var(--border)] bg-[var(--card)] text-[var(--card-foreground)]',
5
+ 'overflow-hidden rounded-[var(--r)] border border-[var(--bdr)] bg-[var(--card)] text-[var(--txt)]',
6
6
  {
7
7
  variants: {
8
8
  padding: {
9
9
  none: 'p-0',
10
- sm: 'p-4',
11
- md: 'p-6',
12
- lg: 'p-8',
10
+ /** Stat / compact surfaces (`.dstat`). */
11
+ sm: 'px-4 py-3.5',
12
+ /** Simple padded panel (`.dcard-b`). */
13
+ md: 'px-[18px] py-4',
14
+ lg: 'px-[18px] py-6',
13
15
  },
14
16
  elevation: {
15
17
  flat: 'shadow-none',
16
- sm: 'shadow-sm',
17
- md: 'shadow-md',
18
- lg: 'shadow-lg',
18
+ resting: 'shadow-[var(--shd)]',
19
+ elevated: 'shadow-[var(--shd2)]',
19
20
  },
20
21
  },
21
- defaultVariants: { padding: 'md', elevation: 'sm' },
22
+ defaultVariants: { padding: 'none', elevation: 'resting' },
22
23
  },
23
24
  )
24
25
 
25
26
  export type CardVariants = VariantProps<typeof card>
26
27
 
27
- const INTERACTIVE_CLASS = 'transition-shadow hover:shadow-md focus-within:shadow-md'
28
+ const INTERACTIVE_CLASS =
29
+ 'motion-safe:transition-shadow motion-safe:duration-[var(--motion-fast)] hover:shadow-[var(--shd2)] focus-within:shadow-[var(--shd2)]'
28
30
 
29
31
  export interface CardProps extends HTMLAttributes<HTMLDivElement>, CardVariants {
30
32
  /** Apply hover/focus elevation transitions (e.g. for clickable cards). */
@@ -44,12 +46,20 @@ export const Card = forwardRef<HTMLDivElement, CardProps>(function Card(
44
46
  )
45
47
  })
46
48
 
47
- /**
48
- * Header slot — sits at the top of a card. Pair with CardTitle / CardDescription.
49
- */
50
- export function CardHeader({ className = '', children, ...rest }: HTMLAttributes<HTMLDivElement>) {
49
+ /** Header slot — `.dcard-h` in the design system handoff. */
50
+ export function CardHeader({
51
+ className = '',
52
+ bordered = true,
53
+ children,
54
+ ...rest
55
+ }: HTMLAttributes<HTMLDivElement> & { bordered?: boolean }) {
51
56
  return (
52
- <div className={`flex flex-col space-y-1.5 ${className}`} {...rest}>
57
+ <div
58
+ className={[bordered ? 'border-b border-[var(--bdr)] px-[18px] py-[14px]' : '', className]
59
+ .filter(Boolean)
60
+ .join(' ')}
61
+ {...rest}
62
+ >
53
63
  {children}
54
64
  </div>
55
65
  )
@@ -62,7 +72,7 @@ export function CardTitle({
62
72
  }: HTMLAttributes<HTMLHeadingElement>) {
63
73
  return (
64
74
  <h3
65
- className={`text-lg leading-tight font-medium tracking-tight text-[var(--card-foreground)] ${className}`}
75
+ className={`text-[length:var(--fs-card)] font-semibold text-[var(--txt)] ${className}`}
66
76
  {...rest}
67
77
  >
68
78
  {children}
@@ -76,15 +86,19 @@ export function CardDescription({
76
86
  ...rest
77
87
  }: HTMLAttributes<HTMLParagraphElement>) {
78
88
  return (
79
- <p className={`text-sm text-[var(--muted-foreground)] ${className}`} {...rest}>
89
+ <p className={`mt-px text-[length:var(--fs-hint)] text-[var(--muted)] ${className}`} {...rest}>
80
90
  {children}
81
91
  </p>
82
92
  )
83
93
  }
84
94
 
95
+ /** Body slot — `.dcard-b` in the design system handoff. */
85
96
  export function CardContent({ className = '', children, ...rest }: HTMLAttributes<HTMLDivElement>) {
86
97
  return (
87
- <div className={className} {...rest}>
98
+ <div
99
+ className={`px-[18px] py-4 text-[length:var(--fs-body)] leading-[1.55] text-[var(--sub)] ${className}`}
100
+ {...rest}
101
+ >
88
102
  {children}
89
103
  </div>
90
104
  )
@@ -92,7 +106,10 @@ export function CardContent({ className = '', children, ...rest }: HTMLAttribute
92
106
 
93
107
  export function CardFooter({ className = '', children, ...rest }: HTMLAttributes<HTMLDivElement>) {
94
108
  return (
95
- <div className={`flex items-center justify-end gap-2 pt-4 ${className}`} {...rest}>
109
+ <div
110
+ className={`flex items-center justify-end gap-2 border-t border-[var(--bdr)] px-[18px] py-3 ${className}`}
111
+ {...rest}
112
+ >
96
113
  {children}
97
114
  </div>
98
115
  )
@@ -0,0 +1,43 @@
1
+ 'use client'
2
+
3
+ import type { ButtonHTMLAttributes, ReactNode } from 'react'
4
+
5
+ /**
6
+ * Design-system filter pill (`.pill` / `.pill.act`): bordered card-surface
7
+ * chip that fills with the accent color when active. Used for status / role /
8
+ * category filter rows above list views.
9
+ */
10
+ export interface FilterPillProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'type'> {
11
+ active?: boolean
12
+ /** Optional trailing count rendered in tabular numerals. */
13
+ count?: number
14
+ children: ReactNode
15
+ }
16
+
17
+ export function FilterPill({
18
+ active = false,
19
+ count,
20
+ className,
21
+ children,
22
+ ...rest
23
+ }: FilterPillProps) {
24
+ return (
25
+ <button
26
+ type="button"
27
+ aria-pressed={active}
28
+ className={`inline-flex items-center gap-1.5 rounded-[7px] border px-3 py-1.5 text-[12.5px] font-medium transition-colors duration-[var(--motion-fast)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none ${
29
+ active
30
+ ? 'border-[var(--acc)] bg-[var(--acc)] text-white'
31
+ : 'border-[var(--bdr)] bg-[var(--card)] text-[var(--sub)] hover:border-[var(--acc-m)] hover:text-[var(--acc)]'
32
+ } ${className ?? ''}`}
33
+ {...rest}
34
+ >
35
+ {children}
36
+ {count !== undefined && (
37
+ <span className={`tabular-nums ${active ? 'text-white/80' : 'text-[var(--muted)]'}`}>
38
+ {count}
39
+ </span>
40
+ )}
41
+ </button>
42
+ )
43
+ }
@@ -1,24 +1,22 @@
1
1
  import { forwardRef, type InputHTMLAttributes } from 'react'
2
2
  import { cv, type VariantProps } from '../../lib/cv.js'
3
+ import { formControlBase, formControlState } from './form-control.js'
3
4
 
4
- const input = cv(
5
- 'focus-visible:ring-offset-background flex w-full rounded-[var(--radius)] border bg-[var(--background)] text-[var(--foreground)] file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-[var(--muted-foreground)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
6
- {
7
- variants: {
8
- size: {
9
- sm: 'h-8 px-2.5 text-xs',
10
- md: 'h-9 px-3 text-sm',
11
- lg: 'h-10 px-4 text-base',
12
- },
13
- state: {
14
- default: 'border-[var(--border)]',
15
- invalid: 'border-[var(--destructive)] focus-visible:ring-[var(--destructive)]',
16
- valid: 'border-emerald-500 focus-visible:ring-emerald-500',
17
- },
5
+ const input = cv(formControlBase, {
6
+ variants: {
7
+ size: {
8
+ sm: 'py-1.5 text-[length:var(--fs-sub)]',
9
+ md: '',
10
+ lg: 'py-2.5 text-[length:var(--fs-body)]',
11
+ },
12
+ state: {
13
+ default: formControlState.default,
14
+ invalid: formControlState.invalid,
15
+ valid: formControlState.valid,
18
16
  },
19
- defaultVariants: { size: 'md', state: 'default' },
20
17
  },
21
- )
18
+ defaultVariants: { size: 'md', state: 'default' },
19
+ })
22
20
 
23
21
  export type InputVariants = VariantProps<typeof input>
24
22
 
@@ -32,4 +30,4 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
32
30
  return <input ref={ref} className={input({ size, state, class: className })} {...rest} />
33
31
  })
34
32
 
35
- export { input as inputVariants }
33
+ export { input as inputVariants, formControlBase, formControlState }