@actuate-media/cms-admin 0.71.0 → 0.72.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 +12 -0
- package/dist/__tests__/components/author-avatar.test.d.ts +2 -0
- package/dist/__tests__/components/author-avatar.test.d.ts.map +1 -0
- package/dist/__tests__/components/author-avatar.test.js +36 -0
- package/dist/__tests__/components/author-avatar.test.js.map +1 -0
- package/dist/__tests__/views/pages-list-view.test.js +60 -0
- package/dist/__tests__/views/pages-list-view.test.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/components/seo/SeoIssueFixPanel.js +1 -1
- package/dist/components/seo/SeoIssueFixPanel.js.map +1 -1
- package/dist/components/ui/AuthorAvatar.d.ts +11 -0
- package/dist/components/ui/AuthorAvatar.d.ts.map +1 -0
- package/dist/components/ui/AuthorAvatar.js +47 -0
- package/dist/components/ui/AuthorAvatar.js.map +1 -0
- package/dist/components/ui/InspectorPane.d.ts.map +1 -1
- package/dist/components/ui/InspectorPane.js +6 -1
- package/dist/components/ui/InspectorPane.js.map +1 -1
- package/dist/views/ApiKeys.d.ts.map +1 -1
- package/dist/views/ApiKeys.js +1 -1
- package/dist/views/ApiKeys.js.map +1 -1
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +3 -29
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +1 -1
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/MediaSeoAudit.d.ts.map +1 -1
- package/dist/views/MediaSeoAudit.js +4 -2
- package/dist/views/MediaSeoAudit.js.map +1 -1
- package/dist/views/Pages/PagesListView.d.ts.map +1 -1
- package/dist/views/Pages/PagesListView.js +82 -12
- package/dist/views/Pages/PagesListView.js.map +1 -1
- package/dist/views/Pages/PagesTreeTable.d.ts.map +1 -1
- package/dist/views/Pages/PagesTreeTable.js +2 -1
- package/dist/views/Pages/PagesTreeTable.js.map +1 -1
- package/dist/views/Posts/PostsListView.d.ts.map +1 -1
- package/dist/views/Posts/PostsListView.js +2 -1
- package/dist/views/Posts/PostsListView.js.map +1 -1
- package/dist/views/ScriptTags.d.ts.map +1 -1
- package/dist/views/ScriptTags.js +1 -1
- package/dist/views/ScriptTags.js.map +1 -1
- package/dist/views/seo/AuditTab.js +1 -1
- package/dist/views/seo/AuditTab.js.map +1 -1
- package/dist/views/seo/ContentTab.js +1 -1
- package/dist/views/seo/ContentTab.js.map +1 -1
- package/dist/views/seo/OverviewTab.js +1 -1
- package/dist/views/seo/OverviewTab.js.map +1 -1
- package/dist/views/seo/RedirectsTab.js +1 -1
- package/dist/views/seo/RedirectsTab.js.map +1 -1
- package/dist/views/seo/TechnicalTab.js +1 -1
- package/dist/views/seo/TechnicalTab.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/components/author-avatar.test.tsx +40 -0
- package/src/__tests__/views/pages-list-view.test.tsx +81 -0
- package/src/components/seo/SeoIssueFixPanel.tsx +2 -2
- package/src/components/ui/AuthorAvatar.tsx +69 -0
- package/src/components/ui/InspectorPane.tsx +7 -2
- package/src/views/ApiKeys.tsx +2 -1
- package/src/views/Dashboard.tsx +3 -32
- package/src/views/MediaBrowser.tsx +3 -2
- package/src/views/MediaSeoAudit.tsx +3 -2
- package/src/views/Pages/PagesListView.tsx +216 -63
- package/src/views/Pages/PagesTreeTable.tsx +2 -6
- package/src/views/Posts/PostsListView.tsx +2 -6
- package/src/views/ScriptTags.tsx +3 -2
- package/src/views/seo/AuditTab.tsx +1 -1
- package/src/views/seo/ContentTab.tsx +1 -1
- package/src/views/seo/OverviewTab.tsx +1 -1
- package/src/views/seo/RedirectsTab.tsx +1 -1
- package/src/views/seo/TechnicalTab.tsx +1 -1
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
4
4
|
import {
|
|
5
|
+
Check,
|
|
5
6
|
ChevronDown,
|
|
6
7
|
ChevronLeft,
|
|
7
8
|
ChevronRight,
|
|
8
9
|
ChevronUp,
|
|
10
|
+
Columns3,
|
|
9
11
|
Copy,
|
|
10
12
|
List,
|
|
11
13
|
ListTree,
|
|
@@ -44,6 +46,7 @@ import {
|
|
|
44
46
|
} from '../../lib/pages-tree.js'
|
|
45
47
|
import { PostStatusBadge, SeoScoreIndicator } from '../../components/posts/badges.js'
|
|
46
48
|
import { PageTagChip, PageTagsCell } from '../../components/pages/tag-badges.js'
|
|
49
|
+
import { AuthorAvatar } from '../../components/ui/AuthorAvatar.js'
|
|
47
50
|
import { Button } from '../../components/ui/Button.js'
|
|
48
51
|
import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js'
|
|
49
52
|
import { RowActionButton, RowActions } from '../../components/ui/RowActions.js'
|
|
@@ -69,8 +72,17 @@ type SortBy = 'title' | 'author' | 'status' | 'modified' | 'seo'
|
|
|
69
72
|
type Dir = 'asc' | 'desc'
|
|
70
73
|
type ViewMode = 'list' | 'tree'
|
|
71
74
|
|
|
75
|
+
/** Optional flat-list columns the user can hide (design's "Tweaks" panel). */
|
|
76
|
+
type ToggleableColumn = 'tags' | 'author' | 'seo'
|
|
77
|
+
const TOGGLEABLE_COLUMNS: Array<{ key: ToggleableColumn; label: string }> = [
|
|
78
|
+
{ key: 'tags', label: 'Tags' },
|
|
79
|
+
{ key: 'author', label: 'Author' },
|
|
80
|
+
{ key: 'seo', label: 'SEO score' },
|
|
81
|
+
]
|
|
82
|
+
|
|
72
83
|
const PAGE_SIZE = 25
|
|
73
84
|
const VIEW_MODE_KEY = 'actuate.pages.viewMode'
|
|
85
|
+
const HIDDEN_COLUMNS_KEY = 'actuate.pages.hiddenColumns'
|
|
74
86
|
|
|
75
87
|
function readStoredViewMode(): ViewMode {
|
|
76
88
|
if (typeof window === 'undefined') return 'list'
|
|
@@ -81,6 +93,20 @@ function readStoredViewMode(): ViewMode {
|
|
|
81
93
|
}
|
|
82
94
|
}
|
|
83
95
|
|
|
96
|
+
function readStoredHiddenColumns(): Set<ToggleableColumn> {
|
|
97
|
+
if (typeof window === 'undefined') return new Set()
|
|
98
|
+
try {
|
|
99
|
+
const raw = localStorage.getItem(HIDDEN_COLUMNS_KEY)
|
|
100
|
+
if (!raw) return new Set()
|
|
101
|
+
const known = new Set(TOGGLEABLE_COLUMNS.map((c) => c.key))
|
|
102
|
+
const parsed: unknown = JSON.parse(raw)
|
|
103
|
+
if (!Array.isArray(parsed)) return new Set()
|
|
104
|
+
return new Set(parsed.filter((v): v is ToggleableColumn => known.has(v as ToggleableColumn)))
|
|
105
|
+
} catch {
|
|
106
|
+
return new Set()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
84
110
|
export function PagesListView({ onNavigate }: PagesListViewProps) {
|
|
85
111
|
const [search, setSearch] = useState('')
|
|
86
112
|
const [debouncedSearch, setDebouncedSearch] = useState('')
|
|
@@ -92,6 +118,7 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
|
|
|
92
118
|
const [page, setPage] = useState(1)
|
|
93
119
|
const [selected, setSelected] = useState<Set<string>>(new Set())
|
|
94
120
|
const [viewMode, setViewMode] = useState<ViewMode>(readStoredViewMode)
|
|
121
|
+
const [hiddenCols, setHiddenCols] = useState<Set<ToggleableColumn>>(readStoredHiddenColumns)
|
|
95
122
|
// null = "not yet touched" → falls back to the default expand state
|
|
96
123
|
// (all root nodes with children open) computed from the current tree.
|
|
97
124
|
const [expandedOverride, setExpandedOverride] = useState<Set<string> | null>(null)
|
|
@@ -223,6 +250,38 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
|
|
|
223
250
|
}
|
|
224
251
|
}
|
|
225
252
|
|
|
253
|
+
const toggleColumn = (key: ToggleableColumn) => {
|
|
254
|
+
setHiddenCols((prev) => {
|
|
255
|
+
const next = new Set(prev)
|
|
256
|
+
if (next.has(key)) next.delete(key)
|
|
257
|
+
else next.add(key)
|
|
258
|
+
try {
|
|
259
|
+
localStorage.setItem(HIDDEN_COLUMNS_KEY, JSON.stringify([...next]))
|
|
260
|
+
} catch {
|
|
261
|
+
/* storage unavailable — the choice still applies for the session */
|
|
262
|
+
}
|
|
263
|
+
return next
|
|
264
|
+
})
|
|
265
|
+
// Hiding the column you're sorted by would leave an invisible sort —
|
|
266
|
+
// fall back to the default title sort instead.
|
|
267
|
+
if ((key === 'author' && sortBy === 'author') || (key === 'seo' && sortBy === 'seo')) {
|
|
268
|
+
setSortBy('title')
|
|
269
|
+
setDir('asc')
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const showTags = !hiddenCols.has('tags')
|
|
274
|
+
const showAuthor = !hiddenCols.has('author')
|
|
275
|
+
const showSeo = !hiddenCols.has('seo')
|
|
276
|
+
// checkbox + title + status + modified + actions are always visible.
|
|
277
|
+
const columnCount = 5 + (showTags ? 1 : 0) + (showAuthor ? 1 : 0) + (showSeo ? 1 : 0)
|
|
278
|
+
// The table is `table-fixed`, so the flexible Title column only gets
|
|
279
|
+
// whatever the min-width leaves after the fixed columns (400px always
|
|
280
|
+
// visible + optional ones). Keep ≥240px for the title or it collapses
|
|
281
|
+
// and headers overlap on narrow viewports.
|
|
282
|
+
const tableMinWidth =
|
|
283
|
+
400 + 240 + (showTags ? 200 : 0) + (showAuthor ? 170 : 0) + (showSeo ? 80 : 0)
|
|
284
|
+
|
|
226
285
|
const handleSort = useCallback(
|
|
227
286
|
(field: SortBy) => {
|
|
228
287
|
if (sortBy === field) {
|
|
@@ -520,6 +579,8 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
|
|
|
520
579
|
|
|
521
580
|
<ViewModeToggle mode={viewMode} onChange={changeViewMode} />
|
|
522
581
|
|
|
582
|
+
{!treeMode && <ColumnsMenu hidden={hiddenCols} onToggle={toggleColumn} />}
|
|
583
|
+
|
|
523
584
|
<span className="ml-auto text-sm text-[var(--sub)] tabular-nums">
|
|
524
585
|
{treeMode
|
|
525
586
|
? treeFiltersActive
|
|
@@ -611,15 +672,20 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
|
|
|
611
672
|
{/* Horizontal scroll on narrow viewports: the fixed columns keep their
|
|
612
673
|
width and the table scrolls sideways instead of crushing content. */}
|
|
613
674
|
<div className="overflow-x-auto">
|
|
614
|
-
<table
|
|
675
|
+
<table
|
|
676
|
+
className="w-full table-fixed"
|
|
677
|
+
style={{ minWidth: tableMinWidth }}
|
|
678
|
+
role="table"
|
|
679
|
+
aria-label="Pages"
|
|
680
|
+
>
|
|
615
681
|
<colgroup>
|
|
616
682
|
<col style={{ width: '40px' }} />
|
|
617
683
|
<col />
|
|
618
|
-
<col style={{ width: '200px' }} />
|
|
619
|
-
<col style={{ width: '170px' }} />
|
|
684
|
+
{showTags && <col style={{ width: '200px' }} />}
|
|
685
|
+
{showAuthor && <col style={{ width: '170px' }} />}
|
|
620
686
|
<col style={{ width: '120px' }} />
|
|
621
687
|
<col style={{ width: '130px' }} />
|
|
622
|
-
<col style={{ width: '80px' }} />
|
|
688
|
+
{showSeo && <col style={{ width: '80px' }} />}
|
|
623
689
|
<col style={{ width: '110px' }} />
|
|
624
690
|
</colgroup>
|
|
625
691
|
<thead className="bg-muted/40 border-b border-[var(--bdr)]">
|
|
@@ -646,22 +712,26 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
|
|
|
646
712
|
onSort={handleSort}
|
|
647
713
|
/>
|
|
648
714
|
</th>
|
|
649
|
-
|
|
650
|
-
<
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
715
|
+
{showTags && (
|
|
716
|
+
<th scope="col" className="px-3 py-2.5 text-left">
|
|
717
|
+
<span className="text-xs font-medium tracking-wider uppercase">Tags</span>
|
|
718
|
+
</th>
|
|
719
|
+
)}
|
|
720
|
+
{showAuthor && (
|
|
721
|
+
<th
|
|
722
|
+
scope="col"
|
|
723
|
+
aria-sort={ariaSortFor('author')}
|
|
724
|
+
className="px-3 py-2.5 text-left"
|
|
725
|
+
>
|
|
726
|
+
<SortHeader
|
|
727
|
+
label="Author"
|
|
728
|
+
field="author"
|
|
729
|
+
current={sortBy}
|
|
730
|
+
dir={dir}
|
|
731
|
+
onSort={handleSort}
|
|
732
|
+
/>
|
|
733
|
+
</th>
|
|
734
|
+
)}
|
|
665
735
|
<th
|
|
666
736
|
scope="col"
|
|
667
737
|
aria-sort={ariaSortFor('status')}
|
|
@@ -688,29 +758,31 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
|
|
|
688
758
|
onSort={handleSort}
|
|
689
759
|
/>
|
|
690
760
|
</th>
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
761
|
+
{showSeo && (
|
|
762
|
+
<th
|
|
763
|
+
scope="col"
|
|
764
|
+
aria-sort={ariaSortFor('seo')}
|
|
765
|
+
className="px-3 py-2.5 text-left"
|
|
766
|
+
>
|
|
767
|
+
<SortHeader
|
|
768
|
+
label="SEO"
|
|
769
|
+
field="seo"
|
|
770
|
+
current={sortBy}
|
|
771
|
+
dir={dir}
|
|
772
|
+
onSort={handleSort}
|
|
773
|
+
/>
|
|
774
|
+
</th>
|
|
775
|
+
)}
|
|
704
776
|
<th scope="col" className="px-3 py-2.5 text-right">
|
|
705
777
|
<span className="sr-only">Actions</span>
|
|
706
778
|
</th>
|
|
707
779
|
</tr>
|
|
708
780
|
</thead>
|
|
709
781
|
<tbody>
|
|
710
|
-
{loading && pages.length === 0 && <LoadingRows />}
|
|
782
|
+
{loading && pages.length === 0 && <LoadingRows columns={columnCount} />}
|
|
711
783
|
{!loading && error && (
|
|
712
784
|
<tr>
|
|
713
|
-
<td colSpan={
|
|
785
|
+
<td colSpan={columnCount} className="px-3 py-12 text-center">
|
|
714
786
|
<div className="mb-2 text-sm font-medium text-[var(--txt)]">{error}</div>
|
|
715
787
|
<button
|
|
716
788
|
onClick={() => setRefreshToken((t) => t + 1)}
|
|
@@ -723,6 +795,7 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
|
|
|
723
795
|
)}
|
|
724
796
|
{!loading && !error && pages.length === 0 && (
|
|
725
797
|
<EmptyRow
|
|
798
|
+
columns={columnCount}
|
|
726
799
|
filtersActive={filtersAreActive}
|
|
727
800
|
onClear={clearFilters}
|
|
728
801
|
onNewPage={() => onNavigate('/pages/new')}
|
|
@@ -737,12 +810,16 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
|
|
|
737
810
|
label={group.label}
|
|
738
811
|
color={group.color}
|
|
739
812
|
count={group.pages.length}
|
|
813
|
+
columns={columnCount}
|
|
740
814
|
>
|
|
741
815
|
{group.pages.map((p) => (
|
|
742
816
|
<PageRow
|
|
743
817
|
key={p.id}
|
|
744
818
|
page={p}
|
|
745
819
|
selected={selected.has(p.id)}
|
|
820
|
+
showTags={showTags}
|
|
821
|
+
showAuthor={showAuthor}
|
|
822
|
+
showSeo={showSeo}
|
|
746
823
|
resolveTag={(slug) => tagBySlug.get(slug)}
|
|
747
824
|
onSelect={() => toggleOne(p.id)}
|
|
748
825
|
onOpen={() => onNavigate(`/pages/${p.id}/edit`)}
|
|
@@ -880,6 +957,70 @@ function ViewModeToggle({
|
|
|
880
957
|
)
|
|
881
958
|
}
|
|
882
959
|
|
|
960
|
+
/**
|
|
961
|
+
* Column visibility menu — the product translation of the design's
|
|
962
|
+
* floating "Tweaks" panel. Optional columns can be hidden per user;
|
|
963
|
+
* the choice persists in localStorage alongside the view mode.
|
|
964
|
+
*/
|
|
965
|
+
function ColumnsMenu({
|
|
966
|
+
hidden,
|
|
967
|
+
onToggle,
|
|
968
|
+
}: {
|
|
969
|
+
hidden: Set<ToggleableColumn>
|
|
970
|
+
onToggle: (key: ToggleableColumn) => void
|
|
971
|
+
}) {
|
|
972
|
+
return (
|
|
973
|
+
<DropdownMenu.Root>
|
|
974
|
+
<DropdownMenu.Trigger asChild>
|
|
975
|
+
<button
|
|
976
|
+
type="button"
|
|
977
|
+
aria-label="Choose visible columns"
|
|
978
|
+
className="inline-flex items-center gap-1.5 rounded-lg border border-[var(--bdr)] bg-[var(--card)] px-2.5 py-1.5 text-sm text-[var(--txt)] shadow-sm transition-colors hover:bg-[var(--acc-l)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
|
|
979
|
+
>
|
|
980
|
+
<Columns3 size={15} aria-hidden />
|
|
981
|
+
Columns
|
|
982
|
+
<ChevronDown className="h-3 w-3 text-[var(--sub)]" aria-hidden />
|
|
983
|
+
</button>
|
|
984
|
+
</DropdownMenu.Trigger>
|
|
985
|
+
<DropdownMenu.Portal container={adminPortalContainer()}>
|
|
986
|
+
<DropdownMenu.Content
|
|
987
|
+
align="end"
|
|
988
|
+
sideOffset={4}
|
|
989
|
+
className="bg-popover z-50 w-44 rounded-lg border border-[var(--bdr)] p-1 shadow-md"
|
|
990
|
+
>
|
|
991
|
+
<DropdownMenu.Label className="px-2 py-1.5 text-[10px] font-medium tracking-wider text-[var(--sub)] uppercase">
|
|
992
|
+
Columns
|
|
993
|
+
</DropdownMenu.Label>
|
|
994
|
+
{TOGGLEABLE_COLUMNS.map((col) => {
|
|
995
|
+
const visible = !hidden.has(col.key)
|
|
996
|
+
return (
|
|
997
|
+
<DropdownMenu.CheckboxItem
|
|
998
|
+
key={col.key}
|
|
999
|
+
checked={visible}
|
|
1000
|
+
onCheckedChange={() => onToggle(col.key)}
|
|
1001
|
+
onSelect={(e) => e.preventDefault()}
|
|
1002
|
+
className="focus:bg-accent flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm text-[var(--txt)] outline-none"
|
|
1003
|
+
>
|
|
1004
|
+
<span
|
|
1005
|
+
aria-hidden
|
|
1006
|
+
className={`flex h-4 w-4 items-center justify-center rounded border ${
|
|
1007
|
+
visible
|
|
1008
|
+
? 'bg-primary border-transparent text-white'
|
|
1009
|
+
: 'border-[var(--bdr)] bg-[var(--card)]'
|
|
1010
|
+
}`}
|
|
1011
|
+
>
|
|
1012
|
+
{visible && <Check size={11} strokeWidth={3} />}
|
|
1013
|
+
</span>
|
|
1014
|
+
{col.label}
|
|
1015
|
+
</DropdownMenu.CheckboxItem>
|
|
1016
|
+
)
|
|
1017
|
+
})}
|
|
1018
|
+
</DropdownMenu.Content>
|
|
1019
|
+
</DropdownMenu.Portal>
|
|
1020
|
+
</DropdownMenu.Root>
|
|
1021
|
+
)
|
|
1022
|
+
}
|
|
1023
|
+
|
|
883
1024
|
function FilterSelect({
|
|
884
1025
|
label,
|
|
885
1026
|
value,
|
|
@@ -914,19 +1055,21 @@ function GroupBlock({
|
|
|
914
1055
|
label,
|
|
915
1056
|
color,
|
|
916
1057
|
count,
|
|
1058
|
+
columns,
|
|
917
1059
|
children,
|
|
918
1060
|
}: {
|
|
919
1061
|
showHeader: boolean
|
|
920
1062
|
label: string
|
|
921
1063
|
color: string | null
|
|
922
1064
|
count: number
|
|
1065
|
+
columns: number
|
|
923
1066
|
children: React.ReactNode
|
|
924
1067
|
}) {
|
|
925
1068
|
return (
|
|
926
1069
|
<>
|
|
927
1070
|
{showHeader && (
|
|
928
1071
|
<tr className="bg-muted/30 border-b border-[var(--bdr)]">
|
|
929
|
-
<td colSpan={
|
|
1072
|
+
<td colSpan={columns} className="px-3 py-2">
|
|
930
1073
|
<span className="inline-flex items-center gap-2 text-xs font-medium tracking-wider text-[var(--txt)] uppercase">
|
|
931
1074
|
{color && <span aria-hidden className={`h-2 w-2 rounded-full ${dotFor(color)}`} />}
|
|
932
1075
|
{label}
|
|
@@ -1066,6 +1209,9 @@ function TagMenu({
|
|
|
1066
1209
|
function PageRow({
|
|
1067
1210
|
page,
|
|
1068
1211
|
selected,
|
|
1212
|
+
showTags,
|
|
1213
|
+
showAuthor,
|
|
1214
|
+
showSeo,
|
|
1069
1215
|
resolveTag,
|
|
1070
1216
|
onSelect,
|
|
1071
1217
|
onOpen,
|
|
@@ -1074,6 +1220,9 @@ function PageRow({
|
|
|
1074
1220
|
}: {
|
|
1075
1221
|
page: Page
|
|
1076
1222
|
selected: boolean
|
|
1223
|
+
showTags: boolean
|
|
1224
|
+
showAuthor: boolean
|
|
1225
|
+
showSeo: boolean
|
|
1077
1226
|
resolveTag: (slug: string) => { name: string; color: string | null } | undefined
|
|
1078
1227
|
onSelect: () => void
|
|
1079
1228
|
onOpen: () => void
|
|
@@ -1096,39 +1245,41 @@ function PageRow({
|
|
|
1096
1245
|
/>
|
|
1097
1246
|
</td>
|
|
1098
1247
|
<td className="px-3 py-3">
|
|
1099
|
-
<button type="button" onClick={onOpen} className="
|
|
1100
|
-
<span
|
|
1101
|
-
|
|
1102
|
-
<span className="block truncate text-sm font-medium text-[var(--txt)] group-hover:text-[var(--acc)]">
|
|
1103
|
-
{page.title}
|
|
1104
|
-
</span>
|
|
1105
|
-
<span className="block truncate text-xs text-[var(--sub)]">{page.path}</span>
|
|
1248
|
+
<button type="button" onClick={onOpen} className="block w-full min-w-0 text-left">
|
|
1249
|
+
<span className="block truncate text-sm font-medium text-[var(--txt)] group-hover:text-[var(--acc)]">
|
|
1250
|
+
{page.title}
|
|
1106
1251
|
</span>
|
|
1252
|
+
<span className="block truncate text-xs text-[var(--sub)]">{page.path}</span>
|
|
1107
1253
|
</button>
|
|
1108
1254
|
</td>
|
|
1109
|
-
|
|
1110
|
-
<
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
<
|
|
1121
|
-
|
|
1122
|
-
|
|
1255
|
+
{showTags && (
|
|
1256
|
+
<td className="px-3 py-3">
|
|
1257
|
+
<PageTagsCell
|
|
1258
|
+
tagSlugs={page.tags}
|
|
1259
|
+
primaryTagId={page.primaryTagId}
|
|
1260
|
+
resolve={resolveTag}
|
|
1261
|
+
/>
|
|
1262
|
+
</td>
|
|
1263
|
+
)}
|
|
1264
|
+
{showAuthor && (
|
|
1265
|
+
<td className="px-3 py-3">
|
|
1266
|
+
<div className="flex items-center gap-2">
|
|
1267
|
+
<AuthorAvatar name={page.authorName} size={28} />
|
|
1268
|
+
<span className="truncate text-sm text-[var(--txt)]">{page.authorName}</span>
|
|
1269
|
+
</div>
|
|
1270
|
+
</td>
|
|
1271
|
+
)}
|
|
1123
1272
|
<td className="px-3 py-3">
|
|
1124
1273
|
<PostStatusBadge status={page.status} />
|
|
1125
1274
|
</td>
|
|
1126
1275
|
<td className="px-3 py-3 text-sm text-[var(--sub)] tabular-nums">
|
|
1127
1276
|
{formatDateShort(page.lastModified)}
|
|
1128
1277
|
</td>
|
|
1129
|
-
|
|
1130
|
-
<
|
|
1131
|
-
|
|
1278
|
+
{showSeo && (
|
|
1279
|
+
<td className="px-3 py-3">
|
|
1280
|
+
<SeoScoreIndicator score={page.seoScore} />
|
|
1281
|
+
</td>
|
|
1282
|
+
)}
|
|
1132
1283
|
<td className="px-3 py-3">
|
|
1133
1284
|
<RowActions>
|
|
1134
1285
|
<RowActionButton aria-label={`Edit "${page.title}"`} title="Edit" onClick={onOpen}>
|
|
@@ -1156,12 +1307,12 @@ function PageRow({
|
|
|
1156
1307
|
)
|
|
1157
1308
|
}
|
|
1158
1309
|
|
|
1159
|
-
function LoadingRows() {
|
|
1310
|
+
function LoadingRows({ columns }: { columns: number }) {
|
|
1160
1311
|
return (
|
|
1161
1312
|
<>
|
|
1162
1313
|
{Array.from({ length: 8 }).map((_, i) => (
|
|
1163
1314
|
<tr key={i} className="border-b border-[var(--bdr)] last:border-b-0">
|
|
1164
|
-
{Array.from({ length:
|
|
1315
|
+
{Array.from({ length: columns }).map((__, j) => (
|
|
1165
1316
|
<td key={j} className="px-3 py-3">
|
|
1166
1317
|
<div className="bg-muted h-4 w-full animate-pulse rounded" />
|
|
1167
1318
|
</td>
|
|
@@ -1173,17 +1324,19 @@ function LoadingRows() {
|
|
|
1173
1324
|
}
|
|
1174
1325
|
|
|
1175
1326
|
function EmptyRow({
|
|
1327
|
+
columns,
|
|
1176
1328
|
filtersActive,
|
|
1177
1329
|
onClear,
|
|
1178
1330
|
onNewPage,
|
|
1179
1331
|
}: {
|
|
1332
|
+
columns: number
|
|
1180
1333
|
filtersActive: boolean
|
|
1181
1334
|
onClear: () => void
|
|
1182
1335
|
onNewPage: () => void
|
|
1183
1336
|
}) {
|
|
1184
1337
|
return (
|
|
1185
1338
|
<tr>
|
|
1186
|
-
<td colSpan={
|
|
1339
|
+
<td colSpan={columns} className="px-3 py-16 text-center">
|
|
1187
1340
|
<div className="mb-2 text-sm font-medium text-[var(--txt)]">
|
|
1188
1341
|
{filtersActive ? 'No pages match your filters' : 'No pages yet'}
|
|
1189
1342
|
</div>
|
|
@@ -17,6 +17,7 @@ import { ChevronDown, Copy, GripVertical, PenLine, Plus, Trash2 } from 'lucide-r
|
|
|
17
17
|
import type { Page } from '../../lib/pages-service.js'
|
|
18
18
|
import type { PageTreeRow } from '../../lib/pages-tree.js'
|
|
19
19
|
import { PostStatusBadge, SeoScoreIndicator } from '../../components/posts/badges.js'
|
|
20
|
+
import { AuthorAvatar } from '../../components/ui/AuthorAvatar.js'
|
|
20
21
|
import { Button } from '../../components/ui/Button.js'
|
|
21
22
|
import { RowActionButton, RowActions } from '../../components/ui/RowActions.js'
|
|
22
23
|
|
|
@@ -354,12 +355,7 @@ function TreeRow({
|
|
|
354
355
|
</td>
|
|
355
356
|
<td className="px-3 align-middle">
|
|
356
357
|
<div className="flex items-center gap-[7px]">
|
|
357
|
-
<
|
|
358
|
-
aria-hidden
|
|
359
|
-
className="inline-flex h-[22px] w-[22px] shrink-0 items-center justify-center rounded-full bg-[var(--acc-l)] text-[9px] font-medium text-[var(--acc)]"
|
|
360
|
-
>
|
|
361
|
-
{page.authorInitials}
|
|
362
|
-
</span>
|
|
358
|
+
<AuthorAvatar name={page.authorName} size={22} />
|
|
363
359
|
<span className="truncate text-[13px] text-[var(--txt)]">{page.authorName}</span>
|
|
364
360
|
</div>
|
|
365
361
|
</td>
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
SeoScoreIndicator,
|
|
34
34
|
paletteFor,
|
|
35
35
|
} from '../../components/posts/badges.js'
|
|
36
|
+
import { AuthorAvatar } from '../../components/ui/AuthorAvatar.js'
|
|
36
37
|
import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js'
|
|
37
38
|
import { RowActionButton, RowActions } from '../../components/ui/RowActions.js'
|
|
38
39
|
|
|
@@ -812,12 +813,7 @@ function PostRow({
|
|
|
812
813
|
</td>
|
|
813
814
|
<td className="px-3 py-3">
|
|
814
815
|
<div className="flex items-center gap-2">
|
|
815
|
-
<
|
|
816
|
-
aria-hidden
|
|
817
|
-
className="bg-muted text-muted-foreground inline-flex h-7 w-7 items-center justify-center rounded-full text-[11px] font-medium"
|
|
818
|
-
>
|
|
819
|
-
{post.authorInitials}
|
|
820
|
-
</span>
|
|
816
|
+
<AuthorAvatar name={post.authorName} size={28} />
|
|
821
817
|
<span className="text-foreground truncate text-sm">{post.authorName}</span>
|
|
822
818
|
</div>
|
|
823
819
|
</td>
|
package/src/views/ScriptTags.tsx
CHANGED
|
@@ -371,8 +371,9 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
371
371
|
</button>
|
|
372
372
|
</div>
|
|
373
373
|
) : (
|
|
374
|
-
<div className="overflow-
|
|
375
|
-
|
|
374
|
+
<div className="overflow-x-auto rounded-lg border border-[var(--bdr)] bg-[var(--card)]">
|
|
375
|
+
{/* min-w keeps the 8 columns readable; narrow viewports scroll sideways. */}
|
|
376
|
+
<table className="w-full min-w-[920px] text-sm" aria-label="Script tags">
|
|
376
377
|
<thead>
|
|
377
378
|
<tr className="bg-muted border-b border-[var(--bdr)]">
|
|
378
379
|
<th className="w-9 px-3 py-3">
|
|
@@ -282,7 +282,7 @@ export function AuditTab({
|
|
|
282
282
|
/>
|
|
283
283
|
) : (
|
|
284
284
|
<div className="overflow-x-auto">
|
|
285
|
-
<table className="w-full text-sm" aria-label="SEO issues">
|
|
285
|
+
<table className="w-full min-w-[680px] text-sm" aria-label="SEO issues">
|
|
286
286
|
<thead>
|
|
287
287
|
<tr className="text-muted-foreground border-border border-b text-left">
|
|
288
288
|
<th className="py-2 pr-4 font-medium">Severity</th>
|
|
@@ -116,7 +116,7 @@ export function ContentTab({
|
|
|
116
116
|
/>
|
|
117
117
|
) : (
|
|
118
118
|
<div className="overflow-x-auto">
|
|
119
|
-
<table className="w-full text-sm" aria-label="Content SEO">
|
|
119
|
+
<table className="w-full min-w-[640px] text-sm" aria-label="Content SEO">
|
|
120
120
|
<thead>
|
|
121
121
|
<tr className="border-b border-[var(--bdr)] text-left text-[var(--sub)]">
|
|
122
122
|
<th className="py-2 pr-4 font-medium">Title</th>
|
|
@@ -216,7 +216,7 @@ export function OverviewTab({
|
|
|
216
216
|
<SeoEmptyState title="No search performance data" />
|
|
217
217
|
) : (
|
|
218
218
|
<div className="overflow-x-auto">
|
|
219
|
-
<table className="w-full text-sm" aria-label="Search performance">
|
|
219
|
+
<table className="w-full min-w-[640px] text-sm" aria-label="Search performance">
|
|
220
220
|
<thead>
|
|
221
221
|
<tr className="text-muted-foreground border-border border-b text-left">
|
|
222
222
|
<th className="py-2 pr-4 font-medium">Page</th>
|
|
@@ -327,7 +327,7 @@ export function RedirectsTab() {
|
|
|
327
327
|
/>
|
|
328
328
|
) : (
|
|
329
329
|
<div className="overflow-x-auto">
|
|
330
|
-
<table className="w-full text-sm" aria-label="Redirect rules">
|
|
330
|
+
<table className="w-full min-w-[760px] text-sm" aria-label="Redirect rules">
|
|
331
331
|
<thead>
|
|
332
332
|
<tr className="text-muted-foreground border-border border-b text-left">
|
|
333
333
|
<th className="py-2 pr-4 font-medium">Source</th>
|
|
@@ -362,7 +362,7 @@ function AiCrawlerCard() {
|
|
|
362
362
|
</div>
|
|
363
363
|
</dl>
|
|
364
364
|
<div className="overflow-x-auto">
|
|
365
|
-
<table className="w-full text-sm" aria-label="AI crawler activity by bot">
|
|
365
|
+
<table className="w-full min-w-[640px] text-sm" aria-label="AI crawler activity by bot">
|
|
366
366
|
<thead>
|
|
367
367
|
<tr className="border-border text-muted-foreground border-b text-left">
|
|
368
368
|
<th scope="col" className="py-2 pr-3 font-medium">
|