@actuate-media/cms-admin 0.65.0 → 0.67.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 +39 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +7 -4
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/components/inspector-pane.test.d.ts +2 -0
- package/dist/__tests__/components/inspector-pane.test.d.ts.map +1 -0
- package/dist/__tests__/components/inspector-pane.test.js +31 -0
- package/dist/__tests__/components/inspector-pane.test.js.map +1 -0
- package/dist/__tests__/lib/pages-tree.test.d.ts +2 -0
- package/dist/__tests__/lib/pages-tree.test.d.ts.map +1 -0
- package/dist/__tests__/lib/pages-tree.test.js +173 -0
- package/dist/__tests__/lib/pages-tree.test.js.map +1 -0
- package/dist/__tests__/views/dashboard.test.js +7 -3
- package/dist/__tests__/views/dashboard.test.js.map +1 -1
- package/dist/__tests__/views/media-seo-audit.render.test.d.ts +2 -0
- package/dist/__tests__/views/media-seo-audit.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/media-seo-audit.render.test.js +102 -0
- package/dist/__tests__/views/media-seo-audit.render.test.js.map +1 -0
- package/dist/__tests__/views/onboarding-block.render.test.d.ts +2 -0
- package/dist/__tests__/views/onboarding-block.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/onboarding-block.render.test.js +146 -0
- package/dist/__tests__/views/onboarding-block.render.test.js.map +1 -0
- package/dist/__tests__/views/pages-list-view.test.js +77 -1
- package/dist/__tests__/views/pages-list-view.test.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/components/script-tags/ScriptTagEditorPane.d.ts +26 -0
- package/dist/components/script-tags/ScriptTagEditorPane.d.ts.map +1 -0
- package/dist/components/script-tags/ScriptTagEditorPane.js +160 -0
- package/dist/components/script-tags/ScriptTagEditorPane.js.map +1 -0
- package/dist/components/ui/InspectorPane.d.ts +51 -0
- package/dist/components/ui/InspectorPane.d.ts.map +1 -0
- package/dist/components/ui/InspectorPane.js +42 -0
- package/dist/components/ui/InspectorPane.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/index.d.ts +4 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +2 -0
- package/dist/components/ui/index.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/lib/page-editor-service.d.ts +11 -2
- package/dist/lib/page-editor-service.d.ts.map +1 -1
- package/dist/lib/page-editor-service.js +8 -2
- package/dist/lib/page-editor-service.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/pages-tree.d.ts +44 -0
- package/dist/lib/pages-tree.d.ts.map +1 -0
- package/dist/lib/pages-tree.js +106 -0
- package/dist/lib/pages-tree.js.map +1 -0
- 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 +40 -3
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +55 -2
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/MediaSeoAudit.d.ts +25 -0
- package/dist/views/MediaSeoAudit.d.ts.map +1 -0
- package/dist/views/MediaSeoAudit.js +97 -0
- package/dist/views/MediaSeoAudit.js.map +1 -0
- package/dist/views/Pages/PagesListView.d.ts.map +1 -1
- package/dist/views/Pages/PagesListView.js +145 -16
- package/dist/views/Pages/PagesListView.js.map +1 -1
- package/dist/views/Pages/PagesTreeTable.d.ts +35 -0
- package/dist/views/Pages/PagesTreeTable.d.ts.map +1 -0
- package/dist/views/Pages/PagesTreeTable.js +28 -0
- package/dist/views/Pages/PagesTreeTable.js.map +1 -0
- 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/ScriptTags.d.ts +5 -1
- package/dist/views/ScriptTags.d.ts.map +1 -1
- package/dist/views/ScriptTags.js +32 -7
- package/dist/views/ScriptTags.js.map +1 -1
- package/dist/views/dashboard/OnboardingBlock.d.ts +29 -0
- package/dist/views/dashboard/OnboardingBlock.d.ts.map +1 -0
- package/dist/views/dashboard/OnboardingBlock.js +71 -0
- package/dist/views/dashboard/OnboardingBlock.js.map +1 -0
- package/dist/views/page-editor/PageSectionEditor.d.ts +7 -1
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.js +3 -3
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/page-editor/PageSettingsModal.d.ts +1 -1
- package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -1
- package/dist/views/page-editor/PageSettingsModal.js +42 -3
- package/dist/views/page-editor/PageSettingsModal.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/package.json +3 -3
- package/src/AdminRoot.tsx +7 -3
- package/src/__tests__/components/inspector-pane.test.tsx +64 -0
- package/src/__tests__/lib/pages-tree.test.ts +197 -0
- package/src/__tests__/views/dashboard.test.tsx +7 -3
- package/src/__tests__/views/media-seo-audit.render.test.tsx +117 -0
- package/src/__tests__/views/onboarding-block.render.test.tsx +193 -0
- package/src/__tests__/views/pages-list-view.test.tsx +93 -1
- package/src/components/script-tags/ScriptTagEditorPane.tsx +474 -0
- package/src/components/ui/InspectorPane.tsx +129 -0
- package/src/components/ui/RowActions.tsx +50 -0
- package/src/components/ui/index.ts +12 -0
- package/src/layout/Header.tsx +2 -2
- package/src/layout/Layout.tsx +8 -80
- package/src/lib/page-editor-service.ts +16 -2
- package/src/lib/pages-service.ts +18 -0
- package/src/lib/pages-tree.ts +145 -0
- package/src/lib/posts-service.ts +26 -0
- package/src/lib/seo-service.ts +3 -0
- package/src/views/Dashboard.tsx +286 -214
- package/src/views/MediaBrowser.tsx +673 -512
- package/src/views/MediaSeoAudit.tsx +320 -0
- package/src/views/Pages/PagesListView.tsx +459 -185
- package/src/views/Pages/PagesTreeTable.tsx +339 -0
- package/src/views/Posts/PostsListView.tsx +135 -10
- package/src/views/ScriptTags.tsx +204 -129
- package/src/views/dashboard/OnboardingBlock.tsx +233 -0
- package/src/views/page-editor/PageSectionEditor.tsx +9 -2
- package/src/views/page-editor/PageSettingsModal.tsx +75 -2
- package/src/views/seo/TechnicalTab.tsx +15 -0
- package/dist/views/ScriptTagEditor.d.ts +0 -6
- package/dist/views/ScriptTagEditor.d.ts.map +0 -1
- package/dist/views/ScriptTagEditor.js +0 -111
- package/dist/views/ScriptTagEditor.js.map +0 -1
- package/src/views/ScriptTagEditor.tsx +0 -353
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Check, FileImage, ImageIcon, SquarePen, TriangleAlert, X } from 'lucide-react'
|
|
4
|
+
import { useMemo, useState } from 'react'
|
|
5
|
+
import { FilterPill } from '../components/ui/FilterPill.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The subset of the `GET /media` item shape the audit needs. `seoScore` is
|
|
9
|
+
* derived server-side by `calculateMediaSeoScore()` in cms-core — the audit
|
|
10
|
+
* never re-scores client-side, so this table can't drift from the SEO drawer
|
|
11
|
+
* or the MCP `validateMediaSEO` tool.
|
|
12
|
+
*/
|
|
13
|
+
export interface MediaSeoAuditItem {
|
|
14
|
+
id: number | string
|
|
15
|
+
name: string
|
|
16
|
+
type: string
|
|
17
|
+
size: string
|
|
18
|
+
url: string
|
|
19
|
+
format?: string
|
|
20
|
+
altTag?: string
|
|
21
|
+
title?: string
|
|
22
|
+
description?: string
|
|
23
|
+
seoScore?: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface MediaSeoAuditProps {
|
|
27
|
+
items: MediaSeoAuditItem[]
|
|
28
|
+
/** Fired when a row / Edit button is activated — opens the item in the Library tab. */
|
|
29
|
+
onEdit: (item: MediaSeoAuditItem) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type AuditFilter = 'all' | 'alt' | 'title' | 'poor'
|
|
33
|
+
|
|
34
|
+
const FILTERS: Array<[AuditFilter, string]> = [
|
|
35
|
+
['all', 'All items'],
|
|
36
|
+
['alt', 'Missing Alt'],
|
|
37
|
+
['title', 'Missing Title'],
|
|
38
|
+
['poor', 'Poor score'],
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
const hasValue = (value?: string): boolean => Boolean(value?.trim())
|
|
42
|
+
|
|
43
|
+
const itemScore = (item: MediaSeoAuditItem): number => item.seoScore ?? 0
|
|
44
|
+
|
|
45
|
+
/** Presentational buckets for the score bar/number — score itself comes from cms-core. */
|
|
46
|
+
function scoreToneClass(score: number): string {
|
|
47
|
+
if (score >= 80) return 'text-[var(--suc)]'
|
|
48
|
+
if (score >= 30) return 'text-[var(--warn)]'
|
|
49
|
+
return 'text-[var(--err)]'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function scoreBarClass(score: number): string {
|
|
53
|
+
if (score >= 80) return 'bg-[var(--suc)]'
|
|
54
|
+
if (score >= 30) return 'bg-[var(--warn)]'
|
|
55
|
+
return 'bg-[var(--err)]'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function scoreLabel(score: number): string {
|
|
59
|
+
if (score >= 80) return 'Good'
|
|
60
|
+
if (score >= 55) return 'Fair'
|
|
61
|
+
if (score >= 30) return 'Needs work'
|
|
62
|
+
return 'Poor'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isImageItem(item: MediaSeoAuditItem): boolean {
|
|
66
|
+
return (
|
|
67
|
+
Boolean(item.url) &&
|
|
68
|
+
(item.type?.startsWith('image/') || /\.(avif|gif|jpe?g|png|webp|svg)$/i.test(item.url))
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function typeLabel(item: MediaSeoAuditItem): string {
|
|
73
|
+
return (item.format ?? item.type ?? '').replace(/^.*\//, '').toUpperCase()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function ScoreBar({ score }: { score: number }) {
|
|
77
|
+
return (
|
|
78
|
+
<div className="flex items-center gap-[7px]">
|
|
79
|
+
<div
|
|
80
|
+
role="meter"
|
|
81
|
+
aria-label="SEO score"
|
|
82
|
+
aria-valuenow={score}
|
|
83
|
+
aria-valuemin={0}
|
|
84
|
+
aria-valuemax={100}
|
|
85
|
+
className="h-[5px] w-[60px] overflow-hidden rounded-full bg-[var(--bdr)]"
|
|
86
|
+
>
|
|
87
|
+
<div
|
|
88
|
+
className={`h-full rounded-full motion-safe:transition-[width] motion-safe:duration-300 ${scoreBarClass(score)}`}
|
|
89
|
+
style={{ width: `${score}%` }}
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
<span
|
|
93
|
+
className={`w-7 text-right text-xs font-semibold tabular-nums ${scoreToneClass(score)}`}
|
|
94
|
+
>
|
|
95
|
+
{score}
|
|
96
|
+
</span>
|
|
97
|
+
</div>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function SeoCheck({ set, missTone }: { set: boolean; missTone: 'err' | 'warn' }) {
|
|
102
|
+
if (set) {
|
|
103
|
+
return (
|
|
104
|
+
<span className="inline-flex items-center gap-[3px] rounded-[4px] bg-[var(--suc-l)] px-[7px] py-[2px] text-[11px] font-medium text-[var(--suc)]">
|
|
105
|
+
<Check size={10} aria-hidden />
|
|
106
|
+
Set
|
|
107
|
+
</span>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
const Icon = missTone === 'err' ? X : TriangleAlert
|
|
111
|
+
const tone =
|
|
112
|
+
missTone === 'err'
|
|
113
|
+
? 'bg-[var(--err-l)] text-[var(--err)]'
|
|
114
|
+
: 'bg-[var(--warn-l)] text-[var(--warn)]'
|
|
115
|
+
return (
|
|
116
|
+
<span
|
|
117
|
+
className={`inline-flex items-center gap-[3px] rounded-[4px] px-[7px] py-[2px] text-[11px] font-medium ${tone}`}
|
|
118
|
+
>
|
|
119
|
+
<Icon size={10} aria-hidden />
|
|
120
|
+
Missing
|
|
121
|
+
</span>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function StatCard({
|
|
126
|
+
value,
|
|
127
|
+
label,
|
|
128
|
+
sub,
|
|
129
|
+
toneClass,
|
|
130
|
+
}: {
|
|
131
|
+
value: string
|
|
132
|
+
label: string
|
|
133
|
+
sub: string
|
|
134
|
+
toneClass: string
|
|
135
|
+
}) {
|
|
136
|
+
return (
|
|
137
|
+
<div className="rounded-[10px] border border-[var(--bdr)] bg-[var(--card)] px-4 py-3.5">
|
|
138
|
+
<div className={`text-[26px] leading-none font-bold tracking-[-0.8px] ${toneClass}`}>
|
|
139
|
+
{value}
|
|
140
|
+
</div>
|
|
141
|
+
<div className="mt-1 text-xs text-[var(--sub)]">{label}</div>
|
|
142
|
+
<div className={`mt-[3px] text-[11px] font-medium ${toneClass}`}>{sub}</div>
|
|
143
|
+
</div>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function MediaSeoAudit({ items, onEdit }: MediaSeoAuditProps) {
|
|
148
|
+
const [filter, setFilter] = useState<AuditFilter>('all')
|
|
149
|
+
|
|
150
|
+
const stats = useMemo(() => {
|
|
151
|
+
const total = items.length
|
|
152
|
+
const missingAlt = items.filter((it) => !hasValue(it.altTag)).length
|
|
153
|
+
const missingTitle = items.filter((it) => !hasValue(it.title)).length
|
|
154
|
+
const missingDesc = items.filter((it) => !hasValue(it.description)).length
|
|
155
|
+
const avgScore =
|
|
156
|
+
total === 0 ? 0 : Math.round(items.reduce((sum, it) => sum + itemScore(it), 0) / total)
|
|
157
|
+
return { total, missingAlt, missingTitle, missingDesc, avgScore }
|
|
158
|
+
}, [items])
|
|
159
|
+
|
|
160
|
+
const filtered = useMemo(() => {
|
|
161
|
+
switch (filter) {
|
|
162
|
+
case 'all':
|
|
163
|
+
return items
|
|
164
|
+
case 'alt':
|
|
165
|
+
return items.filter((it) => !hasValue(it.altTag))
|
|
166
|
+
case 'title':
|
|
167
|
+
return items.filter((it) => !hasValue(it.title))
|
|
168
|
+
case 'poor':
|
|
169
|
+
return items.filter((it) => itemScore(it) < 40)
|
|
170
|
+
default: {
|
|
171
|
+
const exhaustive: never = filter
|
|
172
|
+
return exhaustive
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}, [items, filter])
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
179
|
+
<div className="mb-[18px] grid grid-cols-2 gap-2.5 lg:grid-cols-4">
|
|
180
|
+
<StatCard
|
|
181
|
+
value={`${stats.avgScore}/100`}
|
|
182
|
+
label="Avg SEO Score"
|
|
183
|
+
sub={scoreLabel(stats.avgScore)}
|
|
184
|
+
toneClass={scoreToneClass(stats.avgScore)}
|
|
185
|
+
/>
|
|
186
|
+
<StatCard
|
|
187
|
+
value={String(stats.missingAlt)}
|
|
188
|
+
label="Missing Alt Text"
|
|
189
|
+
sub={`${stats.total - stats.missingAlt} have alt text`}
|
|
190
|
+
toneClass={stats.missingAlt > 0 ? 'text-[var(--err)]' : 'text-[var(--suc)]'}
|
|
191
|
+
/>
|
|
192
|
+
<StatCard
|
|
193
|
+
value={String(stats.missingTitle)}
|
|
194
|
+
label="Missing Titles"
|
|
195
|
+
sub={`${stats.total - stats.missingTitle} have titles`}
|
|
196
|
+
toneClass={stats.missingTitle > 0 ? 'text-[var(--warn)]' : 'text-[var(--suc)]'}
|
|
197
|
+
/>
|
|
198
|
+
<StatCard
|
|
199
|
+
value={String(stats.missingDesc)}
|
|
200
|
+
label="Missing Descriptions"
|
|
201
|
+
sub={`${stats.total - stats.missingDesc} have descriptions`}
|
|
202
|
+
toneClass={stats.missingDesc > 5 ? 'text-[var(--warn)]' : 'text-[var(--sub)]'}
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<div className="mb-3 flex flex-wrap items-center gap-2">
|
|
207
|
+
<span className="text-[12.5px] font-medium text-[var(--sub)]">Show:</span>
|
|
208
|
+
{FILTERS.map(([value, label]) => (
|
|
209
|
+
<FilterPill key={value} active={filter === value} onClick={() => setFilter(value)}>
|
|
210
|
+
{label}
|
|
211
|
+
</FilterPill>
|
|
212
|
+
))}
|
|
213
|
+
<span className="ml-auto text-xs text-[var(--muted)]">
|
|
214
|
+
{filtered.length} item{filtered.length !== 1 ? 's' : ''}
|
|
215
|
+
</span>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<div className="overflow-hidden rounded-[10px] border border-[var(--bdr)] bg-[var(--card)]">
|
|
219
|
+
{filtered.length === 0 ? (
|
|
220
|
+
<div className="flex flex-col items-center justify-center px-4 py-14 text-center">
|
|
221
|
+
<ImageIcon size={24} className="mb-3 text-[var(--muted)]" aria-hidden />
|
|
222
|
+
<p className="text-sm font-medium text-[var(--txt)]">
|
|
223
|
+
{items.length === 0 ? 'No media to audit' : 'No items match this filter'}
|
|
224
|
+
</p>
|
|
225
|
+
<p className="mt-1 text-xs text-[var(--muted)]">
|
|
226
|
+
{items.length === 0
|
|
227
|
+
? 'Upload files to the library to see their SEO health here.'
|
|
228
|
+
: 'Try a different filter to see more items.'}
|
|
229
|
+
</p>
|
|
230
|
+
</div>
|
|
231
|
+
) : (
|
|
232
|
+
<table className="w-full border-collapse" aria-label="Media SEO audit">
|
|
233
|
+
<thead>
|
|
234
|
+
<tr>
|
|
235
|
+
{['File', 'Type', 'Alt Text', 'Title', 'Description', 'SEO Score', 'Action'].map(
|
|
236
|
+
(heading) => (
|
|
237
|
+
<th
|
|
238
|
+
key={heading}
|
|
239
|
+
scope="col"
|
|
240
|
+
className={`border-b border-[var(--bdr)] bg-[var(--bg)] px-3 py-2 text-[11px] font-semibold tracking-[0.5px] whitespace-nowrap text-[var(--muted)] uppercase ${
|
|
241
|
+
heading === 'Action' ? 'text-right' : 'text-left'
|
|
242
|
+
}`}
|
|
243
|
+
>
|
|
244
|
+
{heading}
|
|
245
|
+
</th>
|
|
246
|
+
),
|
|
247
|
+
)}
|
|
248
|
+
</tr>
|
|
249
|
+
</thead>
|
|
250
|
+
<tbody>
|
|
251
|
+
{filtered.map((item) => {
|
|
252
|
+
const score = itemScore(item)
|
|
253
|
+
return (
|
|
254
|
+
<tr
|
|
255
|
+
key={item.id}
|
|
256
|
+
onClick={() => onEdit(item)}
|
|
257
|
+
className="cursor-pointer border-b border-[var(--bdr)] transition-colors last:border-b-0 hover:bg-[var(--acc-l)]"
|
|
258
|
+
>
|
|
259
|
+
<td className="px-3 py-[9px] align-middle">
|
|
260
|
+
<div className="flex items-center gap-2.5">
|
|
261
|
+
<div className="flex h-[30px] w-[38px] shrink-0 items-center justify-center overflow-hidden rounded-[5px] border border-[var(--bdr)] bg-[var(--bg)]">
|
|
262
|
+
{isImageItem(item) ? (
|
|
263
|
+
<img
|
|
264
|
+
src={item.url}
|
|
265
|
+
alt=""
|
|
266
|
+
className="h-full w-full object-cover"
|
|
267
|
+
loading="lazy"
|
|
268
|
+
/>
|
|
269
|
+
) : (
|
|
270
|
+
<FileImage size={14} className="text-[var(--muted)]" aria-hidden />
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
273
|
+
<div className="min-w-0">
|
|
274
|
+
<div className="max-w-[160px] truncate text-[12.5px] font-medium text-[var(--txt)]">
|
|
275
|
+
{item.name}
|
|
276
|
+
</div>
|
|
277
|
+
<div className="text-[11px] text-[var(--muted)]">{item.size}</div>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</td>
|
|
281
|
+
<td className="px-3 py-[9px] align-middle">
|
|
282
|
+
<span className="text-[11.5px] font-semibold tracking-[0.3px] text-[var(--sub)] uppercase">
|
|
283
|
+
{typeLabel(item)}
|
|
284
|
+
</span>
|
|
285
|
+
</td>
|
|
286
|
+
<td className="px-3 py-[9px] align-middle">
|
|
287
|
+
<SeoCheck set={hasValue(item.altTag)} missTone="err" />
|
|
288
|
+
</td>
|
|
289
|
+
<td className="px-3 py-[9px] align-middle">
|
|
290
|
+
<SeoCheck set={hasValue(item.title)} missTone="warn" />
|
|
291
|
+
</td>
|
|
292
|
+
<td className="px-3 py-[9px] align-middle">
|
|
293
|
+
<SeoCheck set={hasValue(item.description)} missTone="warn" />
|
|
294
|
+
</td>
|
|
295
|
+
<td className="px-3 py-[9px] align-middle">
|
|
296
|
+
<ScoreBar score={score} />
|
|
297
|
+
</td>
|
|
298
|
+
<td className="px-3 py-[9px] text-right align-middle">
|
|
299
|
+
<button
|
|
300
|
+
type="button"
|
|
301
|
+
onClick={(e) => {
|
|
302
|
+
e.stopPropagation()
|
|
303
|
+
onEdit(item)
|
|
304
|
+
}}
|
|
305
|
+
className="inline-flex items-center gap-[5px] rounded-[7px] px-2 py-1 text-[11.5px] text-[var(--sub)] transition-colors hover:bg-[var(--acc-l)] hover:text-[var(--acc)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
|
|
306
|
+
>
|
|
307
|
+
<SquarePen size={11} aria-hidden />
|
|
308
|
+
Edit
|
|
309
|
+
</button>
|
|
310
|
+
</td>
|
|
311
|
+
</tr>
|
|
312
|
+
)
|
|
313
|
+
})}
|
|
314
|
+
</tbody>
|
|
315
|
+
</table>
|
|
316
|
+
)}
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
)
|
|
320
|
+
}
|