@actuate-media/cms-admin 0.70.0 → 0.71.1
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__/views/script-tags.render.test.d.ts +2 -0
- package/dist/__tests__/views/script-tags.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/script-tags.render.test.js +148 -0
- package/dist/__tests__/views/script-tags.render.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/components/script-tags/ScriptTagEditorPane.d.ts +10 -0
- package/dist/components/script-tags/ScriptTagEditorPane.d.ts.map +1 -1
- package/dist/components/script-tags/ScriptTagEditorPane.js +21 -2
- package/dist/components/script-tags/ScriptTagEditorPane.js.map +1 -1
- package/dist/components/seo/SeoIssueFixPanel.js +1 -1
- package/dist/components/seo/SeoIssueFixPanel.js.map +1 -1
- 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/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/ScriptTags.d.ts.map +1 -1
- package/dist/views/ScriptTags.js +121 -15
- 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__/views/script-tags.render.test.tsx +178 -0
- package/src/components/script-tags/ScriptTagEditorPane.tsx +40 -4
- package/src/components/seo/SeoIssueFixPanel.tsx +2 -2
- package/src/components/ui/InspectorPane.tsx +7 -2
- package/src/views/ApiKeys.tsx +2 -1
- package/src/views/MediaBrowser.tsx +3 -2
- package/src/views/MediaSeoAudit.tsx +3 -2
- package/src/views/ScriptTags.tsx +260 -25
- 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
package/src/views/ScriptTags.tsx
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useMemo, useState } from 'react'
|
|
4
|
-
import { Code2, Plus, Loader2, AlertTriangle } from 'lucide-react'
|
|
4
|
+
import { Code2, Copy, Plus, Loader2, AlertTriangle, Shield, Trash2 } from 'lucide-react'
|
|
5
5
|
import { toast } from 'sonner'
|
|
6
6
|
import { EnabledSwitch } from '../components/EnabledSwitch.js'
|
|
7
7
|
import { Button } from '../components/ui/Button.js'
|
|
8
|
+
import { ConfirmDialog } from '../components/ui/ConfirmDialog.js'
|
|
9
|
+
import { FilterPill } from '../components/ui/FilterPill.js'
|
|
10
|
+
import { RowActions, RowActionButton } from '../components/ui/RowActions.js'
|
|
11
|
+
import { SearchInput } from '../components/ui/SearchInput.js'
|
|
8
12
|
import { StatPill } from '../components/ui/StatPill.js'
|
|
9
13
|
import { InspectorPane, SplitPaneLayout, SplitPaneList } from '../components/ui/InspectorPane.js'
|
|
10
14
|
import {
|
|
15
|
+
CONSENT_COLORS,
|
|
11
16
|
CONSENT_LABELS,
|
|
17
|
+
NO_CONSENT_COLORS,
|
|
18
|
+
PLACEMENT_COLORS,
|
|
12
19
|
PLACEMENT_LABELS,
|
|
13
20
|
ScriptTagEditorPane,
|
|
14
21
|
type ScriptTag,
|
|
@@ -24,11 +31,21 @@ export interface ScriptTagsProps {
|
|
|
24
31
|
initialNew?: boolean
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
34
|
+
const LOCATION_FILTERS: Array<{ value: string; label: string }> = [
|
|
35
|
+
{ value: 'all', label: 'All' },
|
|
36
|
+
{ value: 'head', label: '<head>' },
|
|
37
|
+
{ value: 'body_open', label: 'Body start' },
|
|
38
|
+
{ value: 'body_close', label: 'Body end' },
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
const CONSENT_FILTERS: Array<{ value: string; label: string }> = [
|
|
42
|
+
{ value: 'all', label: 'All consent' },
|
|
43
|
+
{ value: 'none', label: 'No consent' },
|
|
44
|
+
{ value: 'analytics', label: 'Analytics' },
|
|
45
|
+
{ value: 'marketing', label: 'Marketing' },
|
|
46
|
+
{ value: 'functional', label: 'Functional' },
|
|
47
|
+
{ value: 'preferences', label: 'Preferences' },
|
|
48
|
+
]
|
|
32
49
|
|
|
33
50
|
function normalizeEnabled(value: unknown): boolean {
|
|
34
51
|
if (typeof value === 'boolean') return value
|
|
@@ -52,18 +69,27 @@ function scopeLabel(tag: ScriptTag): string {
|
|
|
52
69
|
return scope || 'Custom'
|
|
53
70
|
}
|
|
54
71
|
|
|
55
|
-
function
|
|
72
|
+
function formatDate(value: string): string {
|
|
56
73
|
const date = new Date(value)
|
|
57
74
|
if (Number.isNaN(date.getTime())) return '—'
|
|
58
75
|
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
|
|
59
76
|
}
|
|
60
77
|
|
|
78
|
+
function consentCategoryOf(tag: ScriptTag): string {
|
|
79
|
+
return tag.consentCategory && tag.consentCategory !== '' ? tag.consentCategory : 'none'
|
|
80
|
+
}
|
|
81
|
+
|
|
61
82
|
type Selection = { mode: 'edit'; id: string } | { mode: 'new' } | null
|
|
62
83
|
|
|
63
84
|
export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
64
85
|
const { data, loading, error, refetch } = useApiData<ScriptTag[]>('/script-tags')
|
|
65
86
|
const [togglingId, setTogglingId] = useState<string | null>(null)
|
|
66
87
|
const [enabledOverrides, setEnabledOverrides] = useState<Record<string, boolean>>({})
|
|
88
|
+
const [search, setSearch] = useState('')
|
|
89
|
+
const [locationFilter, setLocationFilter] = useState('all')
|
|
90
|
+
const [consentFilter, setConsentFilter] = useState('all')
|
|
91
|
+
const [checkedIds, setCheckedIds] = useState<Set<string>>(new Set())
|
|
92
|
+
const [pendingDelete, setPendingDelete] = useState<ScriptTag | null>(null)
|
|
67
93
|
const [selection, setSelection] = useState<Selection>(
|
|
68
94
|
initialNew
|
|
69
95
|
? { mode: 'new' }
|
|
@@ -81,6 +107,20 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
81
107
|
[data, enabledOverrides],
|
|
82
108
|
)
|
|
83
109
|
|
|
110
|
+
const filteredTags = useMemo(() => {
|
|
111
|
+
const query = search.trim().toLowerCase()
|
|
112
|
+
return tags.filter((tag) => {
|
|
113
|
+
if (locationFilter !== 'all' && tag.placement !== locationFilter) return false
|
|
114
|
+
if (consentFilter !== 'all' && consentCategoryOf(tag) !== consentFilter) return false
|
|
115
|
+
if (query && !tag.name.toLowerCase().includes(query)) return false
|
|
116
|
+
return true
|
|
117
|
+
})
|
|
118
|
+
}, [tags, locationFilter, consentFilter, search])
|
|
119
|
+
|
|
120
|
+
const consentGatedCount = tags.filter((t) => consentCategoryOf(t) !== 'none').length
|
|
121
|
+
const checkedVisible = filteredTags.filter((t) => checkedIds.has(t.id))
|
|
122
|
+
const allVisibleChecked = filteredTags.length > 0 && checkedVisible.length === filteredTags.length
|
|
123
|
+
|
|
84
124
|
const selectedTag =
|
|
85
125
|
selection?.mode === 'edit' ? (tags.find((t) => t.id === selection.id) ?? null) : null
|
|
86
126
|
const paneOpen = selection?.mode === 'new' || selectedTag !== null
|
|
@@ -91,6 +131,25 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
91
131
|
)
|
|
92
132
|
}
|
|
93
133
|
|
|
134
|
+
const toggleChecked = (id: string) => {
|
|
135
|
+
setCheckedIds((prev) => {
|
|
136
|
+
const next = new Set(prev)
|
|
137
|
+
if (next.has(id)) next.delete(id)
|
|
138
|
+
else next.add(id)
|
|
139
|
+
return next
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const toggleAllChecked = () => {
|
|
144
|
+
setCheckedIds(allVisibleChecked ? new Set() : new Set(filteredTags.map((t) => t.id)))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const clearFilters = () => {
|
|
148
|
+
setSearch('')
|
|
149
|
+
setLocationFilter('all')
|
|
150
|
+
setConsentFilter('all')
|
|
151
|
+
}
|
|
152
|
+
|
|
94
153
|
const toggleEnabled = async (tag: ScriptTag, nextEnabled: boolean) => {
|
|
95
154
|
setEnabledOverrides((prev) => ({ ...prev, [tag.id]: nextEnabled }))
|
|
96
155
|
setTogglingId(tag.id)
|
|
@@ -117,6 +176,48 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
117
176
|
toast.success(saved ? 'Tag enabled' : 'Tag disabled')
|
|
118
177
|
}
|
|
119
178
|
|
|
179
|
+
const duplicateTag = async (tag: ScriptTag) => {
|
|
180
|
+
const res = await cmsApi<ScriptTag>('/script-tags', {
|
|
181
|
+
method: 'POST',
|
|
182
|
+
body: JSON.stringify({
|
|
183
|
+
name: `${tag.name} (Copy)`,
|
|
184
|
+
code: tag.code,
|
|
185
|
+
placement: tag.placement,
|
|
186
|
+
scope: tag.scope,
|
|
187
|
+
targetPaths: tag.targetPaths,
|
|
188
|
+
priority: tag.priority,
|
|
189
|
+
enabled: false,
|
|
190
|
+
loadAsync: tag.loadAsync,
|
|
191
|
+
disableInPreview: tag.disableInPreview,
|
|
192
|
+
consentCategory: tag.consentCategory,
|
|
193
|
+
}),
|
|
194
|
+
})
|
|
195
|
+
if (res.error) {
|
|
196
|
+
toast.error(res.error)
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
toast.success('Script duplicated (created disabled)')
|
|
200
|
+
refetch()
|
|
201
|
+
if (res.data?.id) setSelection({ mode: 'edit', id: res.data.id })
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const deleteTag = async (tag: ScriptTag) => {
|
|
205
|
+
setPendingDelete(null)
|
|
206
|
+
const res = await cmsApi(`/script-tags/${tag.id}`, { method: 'DELETE' })
|
|
207
|
+
if (res.error) {
|
|
208
|
+
toast.error(res.error)
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
toast.success('Script deleted')
|
|
212
|
+
setCheckedIds((prev) => {
|
|
213
|
+
const next = new Set(prev)
|
|
214
|
+
next.delete(tag.id)
|
|
215
|
+
return next
|
|
216
|
+
})
|
|
217
|
+
if (selection?.mode === 'edit' && selection.id === tag.id) setSelection(null)
|
|
218
|
+
refetch()
|
|
219
|
+
}
|
|
220
|
+
|
|
120
221
|
const handlePaneChanged = (savedId?: string) => {
|
|
121
222
|
setEnabledOverrides({})
|
|
122
223
|
refetch()
|
|
@@ -151,7 +252,7 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
151
252
|
<div>
|
|
152
253
|
<h1 className="mb-1 text-[var(--txt)]">Script Tags</h1>
|
|
153
254
|
<p className="text-sm text-[var(--sub)]">
|
|
154
|
-
|
|
255
|
+
Inject scripts into the frontend — never into CMS rendering
|
|
155
256
|
</p>
|
|
156
257
|
{tags.length > 0 && (
|
|
157
258
|
<div className="mt-3 flex flex-wrap gap-1.5">
|
|
@@ -166,6 +267,11 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
166
267
|
label="Inactive"
|
|
167
268
|
valueClassName="text-[var(--muted)]"
|
|
168
269
|
/>
|
|
270
|
+
<StatPill
|
|
271
|
+
value={consentGatedCount}
|
|
272
|
+
label="Consent-gated"
|
|
273
|
+
icon={<Shield size={12} className="text-[var(--acc)]" aria-hidden />}
|
|
274
|
+
/>
|
|
169
275
|
</div>
|
|
170
276
|
)}
|
|
171
277
|
</div>
|
|
@@ -179,6 +285,52 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
179
285
|
New Script
|
|
180
286
|
</Button>
|
|
181
287
|
</div>
|
|
288
|
+
|
|
289
|
+
{tags.length > 0 && (
|
|
290
|
+
<div className="mb-3 flex flex-wrap items-center gap-2">
|
|
291
|
+
<div className="w-full max-w-[260px]">
|
|
292
|
+
<SearchInput value={search} onChange={setSearch} placeholder="Search scripts…" />
|
|
293
|
+
</div>
|
|
294
|
+
<div className="flex gap-1" role="group" aria-label="Filter by location">
|
|
295
|
+
{LOCATION_FILTERS.map((f) => (
|
|
296
|
+
<FilterPill
|
|
297
|
+
key={f.value}
|
|
298
|
+
active={locationFilter === f.value}
|
|
299
|
+
onClick={() => setLocationFilter(f.value)}
|
|
300
|
+
>
|
|
301
|
+
{f.label}
|
|
302
|
+
</FilterPill>
|
|
303
|
+
))}
|
|
304
|
+
</div>
|
|
305
|
+
<div className="ml-2 flex flex-wrap gap-1" role="group" aria-label="Filter by consent">
|
|
306
|
+
{CONSENT_FILTERS.map((f) => (
|
|
307
|
+
<FilterPill
|
|
308
|
+
key={f.value}
|
|
309
|
+
active={consentFilter === f.value}
|
|
310
|
+
onClick={() => setConsentFilter(f.value)}
|
|
311
|
+
>
|
|
312
|
+
{f.label}
|
|
313
|
+
</FilterPill>
|
|
314
|
+
))}
|
|
315
|
+
</div>
|
|
316
|
+
<span className="ml-auto text-xs text-[var(--muted)]" aria-live="polite">
|
|
317
|
+
{checkedIds.size > 0 ? (
|
|
318
|
+
<>
|
|
319
|
+
{checkedIds.size} selected ·{' '}
|
|
320
|
+
<button
|
|
321
|
+
type="button"
|
|
322
|
+
onClick={() => setCheckedIds(new Set())}
|
|
323
|
+
className="rounded-sm text-[var(--acc)] hover:underline focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
|
|
324
|
+
>
|
|
325
|
+
Clear
|
|
326
|
+
</button>
|
|
327
|
+
</>
|
|
328
|
+
) : (
|
|
329
|
+
`${filteredTags.length} script${filteredTags.length !== 1 ? 's' : ''}`
|
|
330
|
+
)}
|
|
331
|
+
</span>
|
|
332
|
+
</div>
|
|
333
|
+
)}
|
|
182
334
|
</div>
|
|
183
335
|
|
|
184
336
|
<SplitPaneLayout>
|
|
@@ -201,11 +353,40 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
201
353
|
Add Your First Script
|
|
202
354
|
</Button>
|
|
203
355
|
</div>
|
|
356
|
+
) : filteredTags.length === 0 ? (
|
|
357
|
+
<div className="rounded-lg border border-[var(--bdr)] bg-[var(--card)] p-12 text-center">
|
|
358
|
+
<Code2 className="mx-auto mb-3 h-10 w-10 text-[var(--sub)]" />
|
|
359
|
+
<h3 className="text-sm font-medium text-[var(--txt)]">
|
|
360
|
+
No scripts match your filters
|
|
361
|
+
</h3>
|
|
362
|
+
<p className="mt-1 text-sm text-[var(--sub)]">
|
|
363
|
+
Try a different search term or clear the filters.
|
|
364
|
+
</p>
|
|
365
|
+
<button
|
|
366
|
+
type="button"
|
|
367
|
+
onClick={clearFilters}
|
|
368
|
+
className="mt-3 rounded-sm text-sm text-[var(--acc)] hover:underline focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
|
|
369
|
+
>
|
|
370
|
+
Clear filters
|
|
371
|
+
</button>
|
|
372
|
+
</div>
|
|
204
373
|
) : (
|
|
205
|
-
<div className="overflow-
|
|
206
|
-
|
|
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">
|
|
207
377
|
<thead>
|
|
208
378
|
<tr className="bg-muted border-b border-[var(--bdr)]">
|
|
379
|
+
<th className="w-9 px-3 py-3">
|
|
380
|
+
<input
|
|
381
|
+
type="checkbox"
|
|
382
|
+
checked={allVisibleChecked}
|
|
383
|
+
onChange={toggleAllChecked}
|
|
384
|
+
aria-label={
|
|
385
|
+
allVisibleChecked ? 'Deselect all scripts' : 'Select all scripts'
|
|
386
|
+
}
|
|
387
|
+
className="h-[15px] w-[15px] cursor-pointer accent-[var(--acc)]"
|
|
388
|
+
/>
|
|
389
|
+
</th>
|
|
209
390
|
<th className="px-4 py-3 text-left font-medium text-[var(--sub)]">Script</th>
|
|
210
391
|
<th className="px-4 py-3 text-left font-medium text-[var(--sub)]">Location</th>
|
|
211
392
|
<th className="px-4 py-3 text-center font-medium text-[var(--sub)]">
|
|
@@ -214,20 +395,30 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
214
395
|
<th className="px-4 py-3 text-left font-medium text-[var(--sub)]">Scope</th>
|
|
215
396
|
<th className="px-4 py-3 text-left font-medium text-[var(--sub)]">Consent</th>
|
|
216
397
|
<th className="px-4 py-3 text-center font-medium text-[var(--sub)]">Active</th>
|
|
217
|
-
<th className="px-4 py-3 text-
|
|
398
|
+
<th className="px-4 py-3 text-right font-medium text-[var(--sub)]">Updated</th>
|
|
218
399
|
</tr>
|
|
219
400
|
</thead>
|
|
220
401
|
<tbody>
|
|
221
|
-
{
|
|
402
|
+
{filteredTags.map((tag) => {
|
|
222
403
|
const isSelected = selection?.mode === 'edit' && selection.id === tag.id
|
|
404
|
+
const consent = CONSENT_COLORS[consentCategoryOf(tag)] ?? NO_CONSENT_COLORS
|
|
223
405
|
return (
|
|
224
406
|
<tr
|
|
225
407
|
key={tag.id}
|
|
226
|
-
className={`cursor-pointer border-b border-[var(--bdr)] transition-colors last:border-0 ${
|
|
408
|
+
className={`group cursor-pointer border-b border-[var(--bdr)] transition-colors last:border-0 ${
|
|
227
409
|
isSelected ? 'bg-[var(--acc-l)]' : 'hover:bg-[var(--acc-l)]'
|
|
228
410
|
}`}
|
|
229
411
|
onClick={() => toggleRow(tag)}
|
|
230
412
|
>
|
|
413
|
+
<td className="px-3 py-[11px]" onClick={(e) => e.stopPropagation()}>
|
|
414
|
+
<input
|
|
415
|
+
type="checkbox"
|
|
416
|
+
checked={checkedIds.has(tag.id)}
|
|
417
|
+
onChange={() => toggleChecked(tag.id)}
|
|
418
|
+
aria-label={`Select ${tag.name}`}
|
|
419
|
+
className="h-[15px] w-[15px] cursor-pointer accent-[var(--acc)]"
|
|
420
|
+
/>
|
|
421
|
+
</td>
|
|
231
422
|
<td className="px-4 py-[11px]">
|
|
232
423
|
<button
|
|
233
424
|
type="button"
|
|
@@ -240,26 +431,33 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
240
431
|
>
|
|
241
432
|
{tag.name}
|
|
242
433
|
</button>
|
|
434
|
+
<div className="mt-0.5 text-[11.5px] text-[var(--muted)]">
|
|
435
|
+
Added {formatDate(tag.createdAt)}
|
|
436
|
+
</div>
|
|
243
437
|
</td>
|
|
244
438
|
<td className="px-4 py-[11px]">
|
|
245
439
|
<span
|
|
246
|
-
className={`inline-flex items-center rounded-
|
|
440
|
+
className={`inline-flex items-center rounded-[5px] px-2 py-[3px] text-[11.5px] font-medium ${PLACEMENT_COLORS[tag.placement] ?? 'bg-muted text-[var(--txt)]'}`}
|
|
247
441
|
>
|
|
248
442
|
{PLACEMENT_LABELS[tag.placement] ?? tag.placement}
|
|
249
443
|
</span>
|
|
250
444
|
</td>
|
|
251
|
-
<td className="px-4 py-[11px] text-center
|
|
252
|
-
|
|
445
|
+
<td className="px-4 py-[11px] text-center">
|
|
446
|
+
<span className="inline-flex h-6 w-6 items-center justify-center rounded-full border-[1.5px] border-[var(--bdr)] bg-[var(--bg)] text-[11.5px] font-semibold text-[var(--sub)]">
|
|
447
|
+
{tag.priority}
|
|
448
|
+
</span>
|
|
253
449
|
</td>
|
|
254
450
|
<td className="px-4 py-[11px] text-[var(--sub)]">{scopeLabel(tag)}</td>
|
|
255
451
|
<td className="px-4 py-[11px]">
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
452
|
+
<span
|
|
453
|
+
className={`inline-flex items-center gap-[3px] rounded-[4px] px-[7px] py-0.5 text-[11px] font-medium ${consent.badge}`}
|
|
454
|
+
>
|
|
455
|
+
<span
|
|
456
|
+
className={`inline-block h-[5px] w-[5px] rounded-full ${consent.dot}`}
|
|
457
|
+
aria-hidden
|
|
458
|
+
/>
|
|
459
|
+
{CONSENT_LABELS[consentCategoryOf(tag)] ?? tag.consentCategory}
|
|
460
|
+
</span>
|
|
263
461
|
</td>
|
|
264
462
|
<td
|
|
265
463
|
className="px-4 py-[11px] text-center"
|
|
@@ -272,8 +470,33 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
272
470
|
aria-label={`${tag.enabled ? 'Disable' : 'Enable'} ${tag.name}`}
|
|
273
471
|
/>
|
|
274
472
|
</td>
|
|
275
|
-
<td className="px-4 py-[11px]
|
|
276
|
-
|
|
473
|
+
<td className="px-4 py-[11px]">
|
|
474
|
+
<div className="flex items-center justify-end gap-2">
|
|
475
|
+
<RowActions>
|
|
476
|
+
<RowActionButton
|
|
477
|
+
aria-label={`Duplicate ${tag.name}`}
|
|
478
|
+
onClick={(e) => {
|
|
479
|
+
e.stopPropagation()
|
|
480
|
+
void duplicateTag(tag)
|
|
481
|
+
}}
|
|
482
|
+
>
|
|
483
|
+
<Copy size={12} aria-hidden />
|
|
484
|
+
</RowActionButton>
|
|
485
|
+
<RowActionButton
|
|
486
|
+
danger
|
|
487
|
+
aria-label={`Delete ${tag.name}`}
|
|
488
|
+
onClick={(e) => {
|
|
489
|
+
e.stopPropagation()
|
|
490
|
+
setPendingDelete(tag)
|
|
491
|
+
}}
|
|
492
|
+
>
|
|
493
|
+
<Trash2 size={12} aria-hidden />
|
|
494
|
+
</RowActionButton>
|
|
495
|
+
</RowActions>
|
|
496
|
+
<span className="text-xs whitespace-nowrap text-[var(--sub)]">
|
|
497
|
+
{formatDate(tag.updatedAt)}
|
|
498
|
+
</span>
|
|
499
|
+
</div>
|
|
277
500
|
</td>
|
|
278
501
|
</tr>
|
|
279
502
|
)
|
|
@@ -293,6 +516,18 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
|
293
516
|
/>
|
|
294
517
|
</InspectorPane>
|
|
295
518
|
</SplitPaneLayout>
|
|
519
|
+
|
|
520
|
+
<ConfirmDialog
|
|
521
|
+
open={pendingDelete !== null}
|
|
522
|
+
title="Delete script?"
|
|
523
|
+
description={`"${pendingDelete?.name ?? 'This script'}" will stop firing on your site immediately.`}
|
|
524
|
+
confirmLabel="Delete"
|
|
525
|
+
destructive
|
|
526
|
+
onConfirm={() => {
|
|
527
|
+
if (pendingDelete) void deleteTag(pendingDelete)
|
|
528
|
+
}}
|
|
529
|
+
onClose={() => setPendingDelete(null)}
|
|
530
|
+
/>
|
|
296
531
|
</div>
|
|
297
532
|
)
|
|
298
533
|
}
|
|
@@ -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">
|