@actuate-media/cms-admin 0.64.0 → 0.66.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.
- package/CHANGELOG.md +28 -0
- package/dist/__tests__/components/ui-primitives.test.d.ts +2 -0
- package/dist/__tests__/components/ui-primitives.test.d.ts.map +1 -0
- package/dist/__tests__/components/ui-primitives.test.js +45 -0
- package/dist/__tests__/components/ui-primitives.test.js.map +1 -0
- package/dist/__tests__/views/updates-tab.render.test.js +1 -1
- package/dist/__tests__/views/updates-tab.render.test.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/components/SEOPanel.d.ts.map +1 -1
- package/dist/components/SEOPanel.js +2 -1
- package/dist/components/SEOPanel.js.map +1 -1
- package/dist/components/forms/primitives.d.ts.map +1 -1
- package/dist/components/forms/primitives.js +5 -8
- package/dist/components/forms/primitives.js.map +1 -1
- package/dist/components/posts/badges.d.ts.map +1 -1
- package/dist/components/posts/badges.js +7 -13
- package/dist/components/posts/badges.js.map +1 -1
- package/dist/components/seo/RedirectEditorDrawer.d.ts.map +1 -1
- package/dist/components/seo/RedirectEditorDrawer.js +2 -1
- package/dist/components/seo/RedirectEditorDrawer.js.map +1 -1
- package/dist/components/seo/SeoEditorDrawer.d.ts.map +1 -1
- package/dist/components/seo/SeoEditorDrawer.js +3 -2
- package/dist/components/seo/SeoEditorDrawer.js.map +1 -1
- package/dist/components/seo/primitives.d.ts.map +1 -1
- package/dist/components/seo/primitives.js +9 -14
- package/dist/components/seo/primitives.js.map +1 -1
- package/dist/components/ui/FilterPill.d.ts +14 -0
- package/dist/components/ui/FilterPill.d.ts.map +1 -0
- package/dist/components/ui/FilterPill.js +8 -0
- package/dist/components/ui/FilterPill.js.map +1 -0
- package/dist/components/ui/RowActions.d.ts +19 -0
- package/dist/components/ui/RowActions.d.ts.map +1 -0
- package/dist/components/ui/RowActions.js +16 -0
- package/dist/components/ui/RowActions.js.map +1 -0
- package/dist/components/ui/StatPill.d.ts +18 -0
- package/dist/components/ui/StatPill.d.ts.map +1 -0
- package/dist/components/ui/StatPill.js +5 -0
- package/dist/components/ui/StatPill.js.map +1 -0
- package/dist/components/ui/StatusBadge.d.ts +25 -0
- package/dist/components/ui/StatusBadge.d.ts.map +1 -0
- package/dist/components/ui/StatusBadge.js +41 -0
- package/dist/components/ui/StatusBadge.js.map +1 -0
- package/dist/components/ui/index.d.ts +8 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +4 -0
- package/dist/components/ui/index.js.map +1 -1
- package/dist/fields/PropInput.d.ts.map +1 -1
- package/dist/fields/PropInput.js +2 -1
- package/dist/fields/PropInput.js.map +1 -1
- package/dist/fields/ToggleField.d.ts.map +1 -1
- package/dist/fields/ToggleField.js +2 -1
- package/dist/fields/ToggleField.js.map +1 -1
- package/dist/layout/Header.js +1 -1
- package/dist/layout/Header.js.map +1 -1
- package/dist/layout/Layout.js +8 -38
- package/dist/layout/Layout.js.map +1 -1
- package/dist/layout/primitives/PageContainer.d.ts.map +1 -1
- package/dist/layout/primitives/PageContainer.js +1 -1
- package/dist/layout/primitives/PageContainer.js.map +1 -1
- package/dist/lib/pages-service.d.ts +9 -0
- package/dist/lib/pages-service.d.ts.map +1 -1
- package/dist/lib/pages-service.js +14 -0
- package/dist/lib/pages-service.js.map +1 -1
- package/dist/lib/posts-service.d.ts +11 -0
- package/dist/lib/posts-service.d.ts.map +1 -1
- package/dist/lib/posts-service.js +13 -0
- package/dist/lib/posts-service.js.map +1 -1
- package/dist/lib/seo-service.d.ts +3 -0
- package/dist/lib/seo-service.d.ts.map +1 -1
- package/dist/lib/seo-service.js.map +1 -1
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +9 -1
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/FormEditor.d.ts.map +1 -1
- package/dist/views/FormEditor.js +2 -1
- package/dist/views/FormEditor.js.map +1 -1
- package/dist/views/FormEntries.js +1 -1
- package/dist/views/FormEntries.js.map +1 -1
- package/dist/views/FormSubmissions.js +1 -1
- package/dist/views/FormSubmissions.js.map +1 -1
- package/dist/views/Forms.js +1 -1
- package/dist/views/Forms.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +30 -2
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/Pages/PageTagManager.d.ts.map +1 -1
- package/dist/views/Pages/PageTagManager.js +1 -1
- package/dist/views/Pages/PageTagManager.js.map +1 -1
- package/dist/views/Pages/PagesListView.d.ts.map +1 -1
- package/dist/views/Pages/PagesListView.js +59 -10
- package/dist/views/Pages/PagesListView.js.map +1 -1
- package/dist/views/Pages.js +1 -1
- package/dist/views/Pages.js.map +1 -1
- package/dist/views/Posts/NewPostPicker.d.ts.map +1 -1
- package/dist/views/Posts/NewPostPicker.js +1 -1
- package/dist/views/Posts/NewPostPicker.js.map +1 -1
- package/dist/views/Posts/NewPostTypeInstructions.d.ts.map +1 -1
- package/dist/views/Posts/NewPostTypeInstructions.js +1 -1
- package/dist/views/Posts/NewPostTypeInstructions.js.map +1 -1
- package/dist/views/Posts/PostTypesView.d.ts.map +1 -1
- package/dist/views/Posts/PostTypesView.js +1 -1
- package/dist/views/Posts/PostTypesView.js.map +1 -1
- package/dist/views/Posts/PostsListView.d.ts.map +1 -1
- package/dist/views/Posts/PostsListView.js +66 -9
- package/dist/views/Posts/PostsListView.js.map +1 -1
- package/dist/views/Profile.js +1 -1
- package/dist/views/Profile.js.map +1 -1
- package/dist/views/Redirects.js +1 -1
- package/dist/views/Redirects.js.map +1 -1
- package/dist/views/SEO.js +1 -1
- package/dist/views/SEO.js.map +1 -1
- package/dist/views/ScriptTagEditor.d.ts.map +1 -1
- package/dist/views/ScriptTagEditor.js +1 -1
- package/dist/views/ScriptTagEditor.js.map +1 -1
- package/dist/views/ScriptTags.d.ts.map +1 -1
- package/dist/views/ScriptTags.js +2 -1
- package/dist/views/ScriptTags.js.map +1 -1
- package/dist/views/Settings.js +1 -1
- package/dist/views/Settings.js.map +1 -1
- package/dist/views/Users.d.ts.map +1 -1
- package/dist/views/Users.js +2 -6
- package/dist/views/Users.js.map +1 -1
- package/dist/views/page-builder/BlockEditor.d.ts.map +1 -1
- package/dist/views/page-builder/BlockEditor.js +2 -2
- package/dist/views/page-builder/BlockEditor.js.map +1 -1
- package/dist/views/page-builder/NodeSettings.js +3 -3
- package/dist/views/page-builder/NodeSettings.js.map +1 -1
- package/dist/views/page-builder/PageTemplates.js +1 -1
- package/dist/views/page-builder/PageTemplates.js.map +1 -1
- package/dist/views/page-builder/SavedSections.js +2 -2
- package/dist/views/page-builder/SavedSections.js.map +1 -1
- package/dist/views/page-builder/Widgets.js +1 -1
- package/dist/views/page-builder/Widgets.js.map +1 -1
- package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
- package/dist/views/page-editor/SectionInspector.js +2 -1
- package/dist/views/page-editor/SectionInspector.js.map +1 -1
- package/dist/views/post-editor/PostHeaderPanel.d.ts.map +1 -1
- package/dist/views/post-editor/PostHeaderPanel.js +2 -1
- package/dist/views/post-editor/PostHeaderPanel.js.map +1 -1
- package/dist/views/seo/TechnicalTab.d.ts.map +1 -1
- package/dist/views/seo/TechnicalTab.js +15 -0
- package/dist/views/seo/TechnicalTab.js.map +1 -1
- package/dist/views/settings/components.d.ts.map +1 -1
- package/dist/views/settings/components.js +2 -1
- package/dist/views/settings/components.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/components/ui-primitives.test.tsx +58 -0
- package/src/__tests__/views/updates-tab.render.test.tsx +2 -1
- package/src/components/SEOPanel.tsx +2 -11
- package/src/components/forms/primitives.tsx +6 -16
- package/src/components/posts/badges.tsx +8 -21
- package/src/components/seo/RedirectEditorDrawer.tsx +12 -16
- package/src/components/seo/SeoEditorDrawer.tsx +16 -24
- package/src/components/seo/primitives.tsx +10 -27
- package/src/components/ui/FilterPill.tsx +43 -0
- package/src/components/ui/RowActions.tsx +50 -0
- package/src/components/ui/StatPill.tsx +37 -0
- package/src/components/ui/StatusBadge.tsx +66 -0
- package/src/components/ui/index.ts +8 -0
- package/src/fields/PropInput.tsx +6 -15
- package/src/fields/ToggleField.tsx +3 -15
- package/src/layout/Header.tsx +2 -2
- package/src/layout/Layout.tsx +8 -80
- package/src/layout/primitives/PageContainer.tsx +1 -5
- package/src/lib/pages-service.ts +18 -0
- package/src/lib/posts-service.ts +26 -0
- package/src/lib/seo-service.ts +3 -0
- package/src/views/Dashboard.tsx +48 -18
- package/src/views/FormEditor.tsx +6 -9
- package/src/views/FormEntries.tsx +1 -1
- package/src/views/FormSubmissions.tsx +1 -1
- package/src/views/Forms.tsx +1 -1
- package/src/views/MediaBrowser.tsx +36 -8
- package/src/views/Pages/PageTagManager.tsx +1 -3
- package/src/views/Pages/PagesListView.tsx +115 -10
- package/src/views/Pages.tsx +1 -1
- package/src/views/Posts/NewPostPicker.tsx +1 -3
- package/src/views/Posts/NewPostTypeInstructions.tsx +1 -3
- package/src/views/Posts/PostTypesView.tsx +1 -3
- package/src/views/Posts/PostsListView.tsx +135 -10
- package/src/views/Profile.tsx +1 -1
- package/src/views/Redirects.tsx +1 -1
- package/src/views/SEO.tsx +1 -1
- package/src/views/ScriptTagEditor.tsx +1 -3
- package/src/views/ScriptTags.tsx +17 -1
- package/src/views/Settings.tsx +1 -1
- package/src/views/Users.tsx +6 -18
- package/src/views/page-builder/BlockEditor.tsx +3 -6
- package/src/views/page-builder/NodeSettings.tsx +7 -16
- package/src/views/page-builder/PageTemplates.tsx +1 -1
- package/src/views/page-builder/SavedSections.tsx +2 -2
- package/src/views/page-builder/Widgets.tsx +1 -1
- package/src/views/page-editor/SectionInspector.tsx +9 -21
- package/src/views/post-editor/PostHeaderPanel.tsx +10 -20
- package/src/views/seo/TechnicalTab.tsx +15 -0
- package/src/views/settings/components.tsx +8 -16
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
ChevronLeft,
|
|
7
7
|
ChevronRight,
|
|
8
8
|
ChevronUp,
|
|
9
|
+
Copy,
|
|
10
|
+
PenLine,
|
|
9
11
|
Plus,
|
|
10
12
|
Search,
|
|
11
13
|
Tag as TagIcon,
|
|
@@ -20,6 +22,8 @@ import {
|
|
|
20
22
|
bulkChangePageTags,
|
|
21
23
|
bulkDeletePages,
|
|
22
24
|
bulkUpdatePages,
|
|
25
|
+
deletePage,
|
|
26
|
+
duplicatePage,
|
|
23
27
|
fetchPagesDataset,
|
|
24
28
|
groupPages,
|
|
25
29
|
type BulkResult,
|
|
@@ -32,6 +36,7 @@ import {
|
|
|
32
36
|
import { PostStatusBadge, SeoScoreIndicator } from '../../components/posts/badges.js'
|
|
33
37
|
import { PageTagChip, PageTagsCell } from '../../components/pages/tag-badges.js'
|
|
34
38
|
import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js'
|
|
39
|
+
import { RowActionButton, RowActions } from '../../components/ui/RowActions.js'
|
|
35
40
|
|
|
36
41
|
/**
|
|
37
42
|
* Pages management surface — table with a tag-chip filter bar, group-by,
|
|
@@ -70,6 +75,7 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
|
|
|
70
75
|
const [error, setError] = useState<string | null>(null)
|
|
71
76
|
const [refreshToken, setRefreshToken] = useState(0)
|
|
72
77
|
const [pendingDelete, setPendingDelete] = useState(false)
|
|
78
|
+
const [rowDelete, setRowDelete] = useState<Page | null>(null)
|
|
73
79
|
|
|
74
80
|
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
75
81
|
useEffect(() => {
|
|
@@ -284,16 +290,69 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
|
|
|
284
290
|
afterBulk()
|
|
285
291
|
}
|
|
286
292
|
|
|
293
|
+
// ─── Row actions ───────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
const runDuplicate = async (p: Page) => {
|
|
296
|
+
const promise = duplicatePage(p.id).then((r) => {
|
|
297
|
+
if (r.error) throw new Error(r.error)
|
|
298
|
+
return r
|
|
299
|
+
})
|
|
300
|
+
toast.promise(promise, {
|
|
301
|
+
loading: `Duplicating "${p.title}"…`,
|
|
302
|
+
success: `"${p.title}" duplicated`,
|
|
303
|
+
error: (e: Error) => e.message || 'Duplicate failed',
|
|
304
|
+
})
|
|
305
|
+
try {
|
|
306
|
+
await promise
|
|
307
|
+
setRefreshToken((t) => t + 1)
|
|
308
|
+
} catch {
|
|
309
|
+
/* surfaced via toast.promise */
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const runRowDelete = async (p: Page) => {
|
|
314
|
+
const promise = deletePage(p.id).then((r) => {
|
|
315
|
+
if (r.error) throw new Error(r.error)
|
|
316
|
+
})
|
|
317
|
+
toast.promise(promise, {
|
|
318
|
+
loading: `Deleting "${p.title}"…`,
|
|
319
|
+
success: `"${p.title}" moved to trash`,
|
|
320
|
+
error: (e: Error) => e.message || 'Delete failed',
|
|
321
|
+
})
|
|
322
|
+
try {
|
|
323
|
+
await promise
|
|
324
|
+
setSelected((s) => {
|
|
325
|
+
const next = new Set(s)
|
|
326
|
+
next.delete(p.id)
|
|
327
|
+
return next
|
|
328
|
+
})
|
|
329
|
+
setRefreshToken((t) => t + 1)
|
|
330
|
+
} catch {
|
|
331
|
+
/* surfaced via toast.promise */
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
287
335
|
// ─── Render ────────────────────────────────────────────────────────
|
|
288
336
|
|
|
289
337
|
return (
|
|
290
338
|
<div className="flex h-full flex-col gap-4 p-4 sm:p-6">
|
|
291
|
-
{/* Header
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
<
|
|
295
|
-
|
|
296
|
-
|
|
339
|
+
{/* Header — the "+ New page" primary action sits on the right, per
|
|
340
|
+
the design's `.page-hdr` / `.hdr-r` layout. */}
|
|
341
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
342
|
+
<div className="flex flex-col gap-1">
|
|
343
|
+
<h1 className="text-foreground">Pages</h1>
|
|
344
|
+
<p className="text-muted-foreground text-sm">
|
|
345
|
+
{grandTotal} page{grandTotal === 1 ? '' : 's'} · organized by tags
|
|
346
|
+
</p>
|
|
347
|
+
</div>
|
|
348
|
+
<button
|
|
349
|
+
type="button"
|
|
350
|
+
onClick={() => onNavigate('/pages/new')}
|
|
351
|
+
className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-medium shadow-sm transition-colors focus:outline-none focus-visible:ring-2"
|
|
352
|
+
>
|
|
353
|
+
<Plus className="h-3.5 w-3.5" aria-hidden />
|
|
354
|
+
New page
|
|
355
|
+
</button>
|
|
297
356
|
</div>
|
|
298
357
|
|
|
299
358
|
{/* Controls row */}
|
|
@@ -410,6 +469,7 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
|
|
|
410
469
|
<col style={{ width: '120px' }} />
|
|
411
470
|
<col style={{ width: '130px' }} />
|
|
412
471
|
<col style={{ width: '80px' }} />
|
|
472
|
+
<col style={{ width: '110px' }} />
|
|
413
473
|
</colgroup>
|
|
414
474
|
<thead className="border-border bg-muted/40 border-b">
|
|
415
475
|
<tr className="text-muted-foreground text-xs">
|
|
@@ -474,13 +534,16 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
|
|
|
474
534
|
onSort={handleSort}
|
|
475
535
|
/>
|
|
476
536
|
</th>
|
|
537
|
+
<th scope="col" className="px-3 py-2.5 text-right">
|
|
538
|
+
<span className="sr-only">Actions</span>
|
|
539
|
+
</th>
|
|
477
540
|
</tr>
|
|
478
541
|
</thead>
|
|
479
542
|
<tbody>
|
|
480
543
|
{loading && pages.length === 0 && <LoadingRows />}
|
|
481
544
|
{!loading && error && (
|
|
482
545
|
<tr>
|
|
483
|
-
<td colSpan={
|
|
546
|
+
<td colSpan={8} className="px-3 py-12 text-center">
|
|
484
547
|
<div className="text-foreground mb-2 text-sm font-medium">{error}</div>
|
|
485
548
|
<button
|
|
486
549
|
onClick={() => setRefreshToken((t) => t + 1)}
|
|
@@ -516,6 +579,8 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
|
|
|
516
579
|
resolveTag={(slug) => tagBySlug.get(slug)}
|
|
517
580
|
onSelect={() => toggleOne(p.id)}
|
|
518
581
|
onOpen={() => onNavigate(`/pages/${p.id}/edit`)}
|
|
582
|
+
onDuplicate={() => void runDuplicate(p)}
|
|
583
|
+
onDelete={() => setRowDelete(p)}
|
|
519
584
|
/>
|
|
520
585
|
))}
|
|
521
586
|
</GroupBlock>
|
|
@@ -536,6 +601,19 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
|
|
|
536
601
|
)}
|
|
537
602
|
</div>
|
|
538
603
|
|
|
604
|
+
<ConfirmDialog
|
|
605
|
+
open={rowDelete !== null}
|
|
606
|
+
title={`Delete "${rowDelete?.title ?? ''}"?`}
|
|
607
|
+
description="The page moves to trash and can be restored for 30 days."
|
|
608
|
+
confirmLabel="Delete"
|
|
609
|
+
destructive
|
|
610
|
+
onClose={() => setRowDelete(null)}
|
|
611
|
+
onConfirm={() => {
|
|
612
|
+
if (rowDelete) void runRowDelete(rowDelete)
|
|
613
|
+
setRowDelete(null)
|
|
614
|
+
}}
|
|
615
|
+
/>
|
|
616
|
+
|
|
539
617
|
<ConfirmDialog
|
|
540
618
|
open={pendingDelete}
|
|
541
619
|
title={`Delete ${deletablePages.length} page${deletablePages.length === 1 ? '' : 's'}?`}
|
|
@@ -636,7 +714,7 @@ function GroupBlock({
|
|
|
636
714
|
<>
|
|
637
715
|
{showHeader && (
|
|
638
716
|
<tr className="bg-muted/30 border-border border-b">
|
|
639
|
-
<td colSpan={
|
|
717
|
+
<td colSpan={8} className="px-3 py-2">
|
|
640
718
|
<span className="text-foreground inline-flex items-center gap-2 text-xs font-medium tracking-wider uppercase">
|
|
641
719
|
{color && <span aria-hidden className={`h-2 w-2 rounded-full ${dotFor(color)}`} />}
|
|
642
720
|
{label}
|
|
@@ -779,12 +857,16 @@ function PageRow({
|
|
|
779
857
|
resolveTag,
|
|
780
858
|
onSelect,
|
|
781
859
|
onOpen,
|
|
860
|
+
onDuplicate,
|
|
861
|
+
onDelete,
|
|
782
862
|
}: {
|
|
783
863
|
page: Page
|
|
784
864
|
selected: boolean
|
|
785
865
|
resolveTag: (slug: string) => { name: string; color: string | null } | undefined
|
|
786
866
|
onSelect: () => void
|
|
787
867
|
onOpen: () => void
|
|
868
|
+
onDuplicate: () => void
|
|
869
|
+
onDelete: () => void
|
|
788
870
|
}) {
|
|
789
871
|
return (
|
|
790
872
|
<tr
|
|
@@ -835,6 +917,29 @@ function PageRow({
|
|
|
835
917
|
<td className="px-3 py-3">
|
|
836
918
|
<SeoScoreIndicator score={page.seoScore} />
|
|
837
919
|
</td>
|
|
920
|
+
<td className="px-3 py-3">
|
|
921
|
+
<RowActions>
|
|
922
|
+
<RowActionButton aria-label={`Edit "${page.title}"`} title="Edit" onClick={onOpen}>
|
|
923
|
+
<PenLine size={13} aria-hidden />
|
|
924
|
+
</RowActionButton>
|
|
925
|
+
<RowActionButton
|
|
926
|
+
aria-label={`Duplicate "${page.title}"`}
|
|
927
|
+
title="Duplicate"
|
|
928
|
+
onClick={onDuplicate}
|
|
929
|
+
>
|
|
930
|
+
<Copy size={12} aria-hidden />
|
|
931
|
+
</RowActionButton>
|
|
932
|
+
<RowActionButton
|
|
933
|
+
aria-label={`Delete "${page.title}"`}
|
|
934
|
+
title={page.isHomepage ? 'The homepage cannot be deleted' : 'Delete'}
|
|
935
|
+
danger
|
|
936
|
+
disabled={page.isHomepage}
|
|
937
|
+
onClick={onDelete}
|
|
938
|
+
>
|
|
939
|
+
<Trash2 size={12} aria-hidden />
|
|
940
|
+
</RowActionButton>
|
|
941
|
+
</RowActions>
|
|
942
|
+
</td>
|
|
838
943
|
</tr>
|
|
839
944
|
)
|
|
840
945
|
}
|
|
@@ -844,7 +949,7 @@ function LoadingRows() {
|
|
|
844
949
|
<>
|
|
845
950
|
{Array.from({ length: 8 }).map((_, i) => (
|
|
846
951
|
<tr key={i} className="border-border border-b last:border-b-0">
|
|
847
|
-
{Array.from({ length:
|
|
952
|
+
{Array.from({ length: 8 }).map((__, j) => (
|
|
848
953
|
<td key={j} className="px-3 py-3">
|
|
849
954
|
<div className="bg-muted h-4 w-full animate-pulse rounded" />
|
|
850
955
|
</td>
|
|
@@ -866,7 +971,7 @@ function EmptyRow({
|
|
|
866
971
|
}) {
|
|
867
972
|
return (
|
|
868
973
|
<tr>
|
|
869
|
-
<td colSpan={
|
|
974
|
+
<td colSpan={8} className="px-3 py-16 text-center">
|
|
870
975
|
<div className="text-foreground mb-2 text-sm font-medium">
|
|
871
976
|
{filtersActive ? 'No pages match your filters' : 'No pages yet'}
|
|
872
977
|
</div>
|
package/src/views/Pages.tsx
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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>
|
|
@@ -6,7 +6,10 @@ import {
|
|
|
6
6
|
ChevronLeft,
|
|
7
7
|
ChevronRight,
|
|
8
8
|
ChevronUp,
|
|
9
|
+
Copy,
|
|
9
10
|
Loader2,
|
|
11
|
+
PenLine,
|
|
12
|
+
Plus,
|
|
10
13
|
Search,
|
|
11
14
|
Settings,
|
|
12
15
|
Trash2,
|
|
@@ -16,6 +19,8 @@ import { toast } from 'sonner'
|
|
|
16
19
|
import {
|
|
17
20
|
bulkDeletePosts,
|
|
18
21
|
bulkUpdatePosts,
|
|
22
|
+
deletePost,
|
|
23
|
+
duplicatePost,
|
|
19
24
|
fetchPosts,
|
|
20
25
|
type Post,
|
|
21
26
|
type PostStatus,
|
|
@@ -29,6 +34,7 @@ import {
|
|
|
29
34
|
paletteFor,
|
|
30
35
|
} from '../../components/posts/badges.js'
|
|
31
36
|
import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js'
|
|
37
|
+
import { RowActionButton, RowActions } from '../../components/ui/RowActions.js'
|
|
32
38
|
|
|
33
39
|
/**
|
|
34
40
|
* All Posts management surface — table with type tabs, controls row,
|
|
@@ -69,6 +75,7 @@ export function PostsListView({ onNavigate }: PostsListViewProps) {
|
|
|
69
75
|
const [error, setError] = useState<string | null>(null)
|
|
70
76
|
const [refreshToken, setRefreshToken] = useState(0)
|
|
71
77
|
const [pendingBulk, setPendingBulk] = useState<null | 'delete' | 'publish' | 'draft'>(null)
|
|
78
|
+
const [rowDelete, setRowDelete] = useState<Post | null>(null)
|
|
72
79
|
|
|
73
80
|
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
74
81
|
useEffect(() => {
|
|
@@ -132,6 +139,14 @@ export function PostsListView({ onNavigate }: PostsListViewProps) {
|
|
|
132
139
|
() => (data?.types ?? []).reduce((sum, t) => sum + t.postCount, 0),
|
|
133
140
|
[data?.types],
|
|
134
141
|
)
|
|
142
|
+
|
|
143
|
+
// Header CTA target: one Post Type → its editor, several → the picker,
|
|
144
|
+
// none → the Post Type setup instructions.
|
|
145
|
+
const newPostPath = useMemo(() => {
|
|
146
|
+
const typeList = data?.types ?? []
|
|
147
|
+
if (typeList.length === 1) return `/${typeList[0]!.slug}/new`
|
|
148
|
+
return typeList.length > 1 ? '/posts/new' : '/posts/types/new'
|
|
149
|
+
}, [data?.types])
|
|
135
150
|
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE))
|
|
136
151
|
const posts = data?.posts ?? []
|
|
137
152
|
const types = data?.types ?? []
|
|
@@ -210,16 +225,69 @@ export function PostsListView({ onNavigate }: PostsListViewProps) {
|
|
|
210
225
|
setRefreshToken((t) => t + 1)
|
|
211
226
|
}
|
|
212
227
|
|
|
228
|
+
// ─── Row actions ──────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
const runDuplicate = async (p: Post) => {
|
|
231
|
+
const promise = duplicatePost(p.postTypeId, p.id).then((r) => {
|
|
232
|
+
if (r.error) throw new Error(r.error)
|
|
233
|
+
return r
|
|
234
|
+
})
|
|
235
|
+
toast.promise(promise, {
|
|
236
|
+
loading: `Duplicating "${p.title}"…`,
|
|
237
|
+
success: `"${p.title}" duplicated`,
|
|
238
|
+
error: (e: Error) => e.message || 'Duplicate failed',
|
|
239
|
+
})
|
|
240
|
+
try {
|
|
241
|
+
await promise
|
|
242
|
+
setRefreshToken((t) => t + 1)
|
|
243
|
+
} catch {
|
|
244
|
+
/* surfaced via toast.promise */
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const runRowDelete = async (p: Post) => {
|
|
249
|
+
const promise = deletePost(p.postTypeId, p.id).then((r) => {
|
|
250
|
+
if (r.error) throw new Error(r.error)
|
|
251
|
+
})
|
|
252
|
+
toast.promise(promise, {
|
|
253
|
+
loading: `Deleting "${p.title}"…`,
|
|
254
|
+
success: `"${p.title}" moved to trash`,
|
|
255
|
+
error: (e: Error) => e.message || 'Delete failed',
|
|
256
|
+
})
|
|
257
|
+
try {
|
|
258
|
+
await promise
|
|
259
|
+
setSelected((s) => {
|
|
260
|
+
const next = new Set(s)
|
|
261
|
+
next.delete(p.id)
|
|
262
|
+
return next
|
|
263
|
+
})
|
|
264
|
+
setRefreshToken((t) => t + 1)
|
|
265
|
+
} catch {
|
|
266
|
+
/* surfaced via toast.promise */
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
213
270
|
// ─── Render ───────────────────────────────────────────────────────
|
|
214
271
|
|
|
215
272
|
return (
|
|
216
273
|
<div className="flex h-full flex-col gap-4 p-4 sm:p-6">
|
|
217
|
-
{/* Header
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
<
|
|
221
|
-
|
|
222
|
-
|
|
274
|
+
{/* Header — the "+ New Post" primary action sits on the right, per
|
|
275
|
+
the design's `.page-hdr` / `.hdr-r` layout. */}
|
|
276
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
277
|
+
<div className="flex flex-col gap-1">
|
|
278
|
+
<h1 className="text-foreground">Posts</h1>
|
|
279
|
+
<p className="text-muted-foreground text-sm">
|
|
280
|
+
{totalAll} total post{totalAll === 1 ? '' : 's'} across all types
|
|
281
|
+
</p>
|
|
282
|
+
</div>
|
|
283
|
+
<button
|
|
284
|
+
type="button"
|
|
285
|
+
onClick={() => onNavigate(newPostPath)}
|
|
286
|
+
className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-medium shadow-sm transition-colors focus:outline-none focus-visible:ring-2"
|
|
287
|
+
>
|
|
288
|
+
<Plus className="h-3.5 w-3.5" aria-hidden />
|
|
289
|
+
New Post
|
|
290
|
+
</button>
|
|
223
291
|
</div>
|
|
224
292
|
|
|
225
293
|
{/* Type tabs */}
|
|
@@ -269,6 +337,7 @@ export function PostsListView({ onNavigate }: PostsListViewProps) {
|
|
|
269
337
|
<col style={{ width: '130px' }} />
|
|
270
338
|
<col style={{ width: '120px' }} />
|
|
271
339
|
<col style={{ width: '90px' }} />
|
|
340
|
+
<col style={{ width: '110px' }} />
|
|
272
341
|
</colgroup>
|
|
273
342
|
<thead className="border-border bg-muted/40 border-b">
|
|
274
343
|
<tr className="text-muted-foreground text-xs">
|
|
@@ -335,13 +404,16 @@ export function PostsListView({ onNavigate }: PostsListViewProps) {
|
|
|
335
404
|
onSort={handleSort}
|
|
336
405
|
/>
|
|
337
406
|
</th>
|
|
407
|
+
<th scope="col" className="px-3 py-2.5 text-right">
|
|
408
|
+
<span className="sr-only">Actions</span>
|
|
409
|
+
</th>
|
|
338
410
|
</tr>
|
|
339
411
|
</thead>
|
|
340
412
|
<tbody>
|
|
341
413
|
{loading && posts.length === 0 && <LoadingRows />}
|
|
342
414
|
{!loading && error && (
|
|
343
415
|
<tr>
|
|
344
|
-
<td colSpan={
|
|
416
|
+
<td colSpan={8} className="px-3 py-12 text-center">
|
|
345
417
|
<div className="text-foreground mb-2 text-sm font-medium">{error}</div>
|
|
346
418
|
<button
|
|
347
419
|
onClick={() => setRefreshToken((t) => t + 1)}
|
|
@@ -368,6 +440,8 @@ export function PostsListView({ onNavigate }: PostsListViewProps) {
|
|
|
368
440
|
selected={selected.has(p.id)}
|
|
369
441
|
onSelect={() => toggleOne(p.id)}
|
|
370
442
|
onOpen={() => onNavigate(`/posts/${p.postTypeId}/${p.id}/edit`)}
|
|
443
|
+
onDuplicate={() => void runDuplicate(p)}
|
|
444
|
+
onDelete={() => setRowDelete(p)}
|
|
371
445
|
/>
|
|
372
446
|
))}
|
|
373
447
|
</tbody>
|
|
@@ -384,6 +458,19 @@ export function PostsListView({ onNavigate }: PostsListViewProps) {
|
|
|
384
458
|
/>
|
|
385
459
|
</div>
|
|
386
460
|
|
|
461
|
+
<ConfirmDialog
|
|
462
|
+
open={rowDelete !== null}
|
|
463
|
+
title={`Delete "${rowDelete?.title ?? ''}"?`}
|
|
464
|
+
description="The post moves to trash and can be restored for 30 days."
|
|
465
|
+
confirmLabel="Delete"
|
|
466
|
+
destructive
|
|
467
|
+
onClose={() => setRowDelete(null)}
|
|
468
|
+
onConfirm={() => {
|
|
469
|
+
if (rowDelete) void runRowDelete(rowDelete)
|
|
470
|
+
setRowDelete(null)
|
|
471
|
+
}}
|
|
472
|
+
/>
|
|
473
|
+
|
|
387
474
|
<ConfirmDialog
|
|
388
475
|
open={pendingBulk === 'delete'}
|
|
389
476
|
title={`Delete ${selected.size} post${selected.size === 1 ? '' : 's'}?`}
|
|
@@ -669,11 +756,15 @@ function PostRow({
|
|
|
669
756
|
selected,
|
|
670
757
|
onSelect,
|
|
671
758
|
onOpen,
|
|
759
|
+
onDuplicate,
|
|
760
|
+
onDelete,
|
|
672
761
|
}: {
|
|
673
762
|
post: Post
|
|
674
763
|
selected: boolean
|
|
675
764
|
onSelect: () => void
|
|
676
765
|
onOpen: () => void
|
|
766
|
+
onDuplicate: () => void
|
|
767
|
+
onDelete: () => void
|
|
677
768
|
}) {
|
|
678
769
|
const p = paletteFor(post.postTypeAccentColor)
|
|
679
770
|
return (
|
|
@@ -693,7 +784,19 @@ function PostRow({
|
|
|
693
784
|
</td>
|
|
694
785
|
<td className="px-3 py-3">
|
|
695
786
|
<button type="button" onClick={onOpen} className="flex w-full items-center gap-3 text-left">
|
|
696
|
-
|
|
787
|
+
{post.imageUrl ? (
|
|
788
|
+
<img
|
|
789
|
+
src={post.imageUrl}
|
|
790
|
+
alt=""
|
|
791
|
+
loading="lazy"
|
|
792
|
+
className="block h-9 w-9 shrink-0 rounded-md object-cover"
|
|
793
|
+
/>
|
|
794
|
+
) : (
|
|
795
|
+
<span
|
|
796
|
+
aria-hidden
|
|
797
|
+
className={`block h-9 w-9 shrink-0 rounded-md ${p.bg.split(' ')[0]}`}
|
|
798
|
+
/>
|
|
799
|
+
)}
|
|
697
800
|
<span className="min-w-0">
|
|
698
801
|
<span className="text-foreground group-hover:text-primary block truncate text-sm font-medium">
|
|
699
802
|
{post.title}
|
|
@@ -727,6 +830,28 @@ function PostRow({
|
|
|
727
830
|
<td className="px-3 py-3">
|
|
728
831
|
<SeoScoreIndicator score={post.seoScore} />
|
|
729
832
|
</td>
|
|
833
|
+
<td className="px-3 py-3">
|
|
834
|
+
<RowActions>
|
|
835
|
+
<RowActionButton aria-label={`Edit "${post.title}"`} title="Edit" onClick={onOpen}>
|
|
836
|
+
<PenLine size={13} aria-hidden />
|
|
837
|
+
</RowActionButton>
|
|
838
|
+
<RowActionButton
|
|
839
|
+
aria-label={`Duplicate "${post.title}"`}
|
|
840
|
+
title="Duplicate"
|
|
841
|
+
onClick={onDuplicate}
|
|
842
|
+
>
|
|
843
|
+
<Copy size={12} aria-hidden />
|
|
844
|
+
</RowActionButton>
|
|
845
|
+
<RowActionButton
|
|
846
|
+
aria-label={`Delete "${post.title}"`}
|
|
847
|
+
title="Delete"
|
|
848
|
+
danger
|
|
849
|
+
onClick={onDelete}
|
|
850
|
+
>
|
|
851
|
+
<Trash2 size={12} aria-hidden />
|
|
852
|
+
</RowActionButton>
|
|
853
|
+
</RowActions>
|
|
854
|
+
</td>
|
|
730
855
|
</tr>
|
|
731
856
|
)
|
|
732
857
|
}
|
|
@@ -737,7 +862,7 @@ function LoadingRows() {
|
|
|
737
862
|
<>
|
|
738
863
|
{rows.map((_, i) => (
|
|
739
864
|
<tr key={i} className="border-border border-b last:border-b-0">
|
|
740
|
-
{Array.from({ length:
|
|
865
|
+
{Array.from({ length: 8 }).map((__, j) => (
|
|
741
866
|
<td key={j} className="px-3 py-3">
|
|
742
867
|
<div className="bg-muted h-4 w-full animate-pulse rounded" />
|
|
743
868
|
</td>
|
|
@@ -759,7 +884,7 @@ function EmptyRow({
|
|
|
759
884
|
}) {
|
|
760
885
|
return (
|
|
761
886
|
<tr>
|
|
762
|
-
<td colSpan={
|
|
887
|
+
<td colSpan={8} className="px-3 py-16 text-center">
|
|
763
888
|
<div className="text-foreground mb-2 text-sm font-medium">
|
|
764
889
|
{filtersActive ? 'No posts match the current filters.' : 'No posts yet.'}
|
|
765
890
|
</div>
|
package/src/views/Profile.tsx
CHANGED
|
@@ -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
|
|
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>
|
package/src/views/Redirects.tsx
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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">
|
package/src/views/ScriptTags.tsx
CHANGED
|
@@ -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
|
|
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"
|
package/src/views/Settings.tsx
CHANGED
|
@@ -179,7 +179,7 @@ export function Settings({
|
|
|
179
179
|
)}
|
|
180
180
|
|
|
181
181
|
<div className="mb-4">
|
|
182
|
-
<h1 className="text-foreground mb-1
|
|
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
|
|
package/src/views/Users.tsx
CHANGED
|
@@ -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
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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 */}
|