@actuate-media/cms-admin 0.66.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 +21 -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/index.d.ts +2 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +1 -0
- package/dist/components/ui/index.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-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/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +31 -2
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +28 -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 +90 -10
- 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/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/package.json +2 -2
- 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/index.ts +10 -0
- package/src/lib/page-editor-service.ts +16 -2
- package/src/lib/pages-tree.ts +145 -0
- package/src/views/Dashboard.tsx +238 -196
- package/src/views/MediaBrowser.tsx +641 -511
- package/src/views/MediaSeoAudit.tsx +320 -0
- package/src/views/Pages/PagesListView.tsx +352 -183
- package/src/views/Pages/PagesTreeTable.tsx +339 -0
- 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/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,339 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { ChevronDown, Copy, PenLine, Plus, Trash2 } from 'lucide-react'
|
|
4
|
+
import type { Page } from '../../lib/pages-service.js'
|
|
5
|
+
import type { PageTreeRow } from '../../lib/pages-tree.js'
|
|
6
|
+
import { PostStatusBadge, SeoScoreIndicator } from '../../components/posts/badges.js'
|
|
7
|
+
import { RowActionButton, RowActions } from '../../components/ui/RowActions.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Tree-mode table for the Pages list ("Pages (tree version)" design):
|
|
11
|
+
* nested parent/child rows with indentation (24px per level), a 20×20
|
|
12
|
+
* expand chevron that rotates -90° when collapsed, 56px rows, and hover
|
|
13
|
+
* actions (labeled Edit + Add sub-page / Duplicate / Delete icons).
|
|
14
|
+
*
|
|
15
|
+
* When `flattened` is true (search or status filter active) every match
|
|
16
|
+
* renders at depth 0 and expand state is ignored — the caller passes
|
|
17
|
+
* pre-flattened rows, this component just skips the leaf spacers' tree
|
|
18
|
+
* semantics.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
interface PagesTreeTableProps {
|
|
22
|
+
rows: PageTreeRow[]
|
|
23
|
+
loading: boolean
|
|
24
|
+
error: string | null
|
|
25
|
+
/** true when search/status filters flattened the tree */
|
|
26
|
+
flattened: boolean
|
|
27
|
+
selected: Set<string>
|
|
28
|
+
expanded: Set<string>
|
|
29
|
+
onToggleExpand: (id: string) => void
|
|
30
|
+
onToggleSelect: (id: string) => void
|
|
31
|
+
onToggleAll: (checked: boolean) => void
|
|
32
|
+
onOpen: (page: Page) => void
|
|
33
|
+
onAddSubPage: (page: Page) => void
|
|
34
|
+
onDuplicate: (page: Page) => void
|
|
35
|
+
onDelete: (page: Page) => void
|
|
36
|
+
onRetry: () => void
|
|
37
|
+
onClearFilters: () => void
|
|
38
|
+
onNewPage: () => void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const TH_CLASS =
|
|
42
|
+
'px-3 py-2.5 text-left text-[11.5px] font-semibold tracking-[0.4px] uppercase text-[var(--muted)]'
|
|
43
|
+
|
|
44
|
+
export function PagesTreeTable({
|
|
45
|
+
rows,
|
|
46
|
+
loading,
|
|
47
|
+
error,
|
|
48
|
+
flattened,
|
|
49
|
+
selected,
|
|
50
|
+
expanded,
|
|
51
|
+
onToggleExpand,
|
|
52
|
+
onToggleSelect,
|
|
53
|
+
onToggleAll,
|
|
54
|
+
onOpen,
|
|
55
|
+
onAddSubPage,
|
|
56
|
+
onDuplicate,
|
|
57
|
+
onDelete,
|
|
58
|
+
onRetry,
|
|
59
|
+
onClearFilters,
|
|
60
|
+
onNewPage,
|
|
61
|
+
}: PagesTreeTableProps) {
|
|
62
|
+
const allSelected = rows.length > 0 && rows.every((r) => selected.has(r.page.id))
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div className="overflow-x-auto">
|
|
66
|
+
<table className="w-full min-w-[800px]" role="table" aria-label="Pages tree">
|
|
67
|
+
<colgroup>
|
|
68
|
+
<col style={{ width: '40px' }} />
|
|
69
|
+
<col />
|
|
70
|
+
<col style={{ width: '130px' }} />
|
|
71
|
+
<col style={{ width: '150px' }} />
|
|
72
|
+
<col style={{ width: '120px' }} />
|
|
73
|
+
<col style={{ width: '115px' }} />
|
|
74
|
+
<col style={{ width: '70px' }} />
|
|
75
|
+
<col style={{ width: '170px' }} />
|
|
76
|
+
</colgroup>
|
|
77
|
+
<thead className="border-b border-[var(--bdr)]">
|
|
78
|
+
<tr>
|
|
79
|
+
<th scope="col" className="px-3 py-2.5 text-left">
|
|
80
|
+
<input
|
|
81
|
+
type="checkbox"
|
|
82
|
+
aria-label="Select all visible pages"
|
|
83
|
+
checked={allSelected}
|
|
84
|
+
onChange={(e) => onToggleAll(e.target.checked)}
|
|
85
|
+
className="border-border accent-primary rounded"
|
|
86
|
+
/>
|
|
87
|
+
</th>
|
|
88
|
+
<th scope="col" className={`${TH_CLASS} min-w-[280px]`}>
|
|
89
|
+
Title
|
|
90
|
+
</th>
|
|
91
|
+
<th scope="col" className={TH_CLASS}>
|
|
92
|
+
Template
|
|
93
|
+
</th>
|
|
94
|
+
<th scope="col" className={TH_CLASS}>
|
|
95
|
+
Author
|
|
96
|
+
</th>
|
|
97
|
+
<th scope="col" className={TH_CLASS}>
|
|
98
|
+
Status
|
|
99
|
+
</th>
|
|
100
|
+
<th scope="col" className={TH_CLASS}>
|
|
101
|
+
Last Modified
|
|
102
|
+
</th>
|
|
103
|
+
<th scope="col" className={TH_CLASS}>
|
|
104
|
+
SEO
|
|
105
|
+
</th>
|
|
106
|
+
<th scope="col" className="px-3 py-2.5 text-right">
|
|
107
|
+
<span className="sr-only">Actions</span>
|
|
108
|
+
</th>
|
|
109
|
+
</tr>
|
|
110
|
+
</thead>
|
|
111
|
+
<tbody>
|
|
112
|
+
{loading && rows.length === 0 && <TreeLoadingRows />}
|
|
113
|
+
{!loading && error && (
|
|
114
|
+
<tr>
|
|
115
|
+
<td colSpan={8} className="px-3 py-12 text-center">
|
|
116
|
+
<div className="text-foreground mb-2 text-sm font-medium">{error}</div>
|
|
117
|
+
<button onClick={onRetry} className="text-primary text-sm hover:underline">
|
|
118
|
+
Try again
|
|
119
|
+
</button>
|
|
120
|
+
</td>
|
|
121
|
+
</tr>
|
|
122
|
+
)}
|
|
123
|
+
{!loading && !error && rows.length === 0 && (
|
|
124
|
+
<tr>
|
|
125
|
+
<td colSpan={8} className="px-3 py-16 text-center">
|
|
126
|
+
<div className="text-foreground mb-2 text-sm font-medium">
|
|
127
|
+
{flattened ? 'No pages match your filters' : 'No pages yet'}
|
|
128
|
+
</div>
|
|
129
|
+
<div className="text-muted-foreground mb-4 text-sm">
|
|
130
|
+
{flattened
|
|
131
|
+
? 'Try clearing the search or status filter above.'
|
|
132
|
+
: 'Create your first page to get started.'}
|
|
133
|
+
</div>
|
|
134
|
+
{flattened ? (
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
onClick={onClearFilters}
|
|
138
|
+
className="text-primary text-sm hover:underline"
|
|
139
|
+
>
|
|
140
|
+
Clear filters
|
|
141
|
+
</button>
|
|
142
|
+
) : (
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
onClick={onNewPage}
|
|
146
|
+
className="bg-primary text-primary-foreground hover:bg-primary/90 inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-medium shadow-sm transition-colors"
|
|
147
|
+
>
|
|
148
|
+
<Plus className="h-3.5 w-3.5" aria-hidden />
|
|
149
|
+
New page
|
|
150
|
+
</button>
|
|
151
|
+
)}
|
|
152
|
+
</td>
|
|
153
|
+
</tr>
|
|
154
|
+
)}
|
|
155
|
+
{!loading &&
|
|
156
|
+
!error &&
|
|
157
|
+
rows.map((row) => (
|
|
158
|
+
<TreeRow
|
|
159
|
+
key={row.page.id}
|
|
160
|
+
row={row}
|
|
161
|
+
isSelected={selected.has(row.page.id)}
|
|
162
|
+
isExpanded={expanded.has(row.page.id)}
|
|
163
|
+
onToggleExpand={() => onToggleExpand(row.page.id)}
|
|
164
|
+
onToggleSelect={() => onToggleSelect(row.page.id)}
|
|
165
|
+
onOpen={() => onOpen(row.page)}
|
|
166
|
+
onAddSubPage={() => onAddSubPage(row.page)}
|
|
167
|
+
onDuplicate={() => onDuplicate(row.page)}
|
|
168
|
+
onDelete={() => onDelete(row.page)}
|
|
169
|
+
/>
|
|
170
|
+
))}
|
|
171
|
+
</tbody>
|
|
172
|
+
</table>
|
|
173
|
+
</div>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function TreeRow({
|
|
178
|
+
row,
|
|
179
|
+
isSelected,
|
|
180
|
+
isExpanded,
|
|
181
|
+
onToggleExpand,
|
|
182
|
+
onToggleSelect,
|
|
183
|
+
onOpen,
|
|
184
|
+
onAddSubPage,
|
|
185
|
+
onDuplicate,
|
|
186
|
+
onDelete,
|
|
187
|
+
}: {
|
|
188
|
+
row: PageTreeRow
|
|
189
|
+
isSelected: boolean
|
|
190
|
+
isExpanded: boolean
|
|
191
|
+
onToggleExpand: () => void
|
|
192
|
+
onToggleSelect: () => void
|
|
193
|
+
onOpen: () => void
|
|
194
|
+
onAddSubPage: () => void
|
|
195
|
+
onDuplicate: () => void
|
|
196
|
+
onDelete: () => void
|
|
197
|
+
}) {
|
|
198
|
+
const { page, depth, hasChildren } = row
|
|
199
|
+
return (
|
|
200
|
+
<tr
|
|
201
|
+
className={`group h-14 border-b border-[var(--bdr)] transition-colors last:border-b-0 hover:bg-[var(--acc-l)] ${
|
|
202
|
+
isSelected ? 'bg-[var(--acc-l)]' : ''
|
|
203
|
+
}`}
|
|
204
|
+
>
|
|
205
|
+
<td className="px-3 align-middle">
|
|
206
|
+
<input
|
|
207
|
+
type="checkbox"
|
|
208
|
+
aria-label={`Select "${page.title}"`}
|
|
209
|
+
checked={isSelected}
|
|
210
|
+
onChange={onToggleSelect}
|
|
211
|
+
className="border-border accent-primary rounded"
|
|
212
|
+
/>
|
|
213
|
+
</td>
|
|
214
|
+
<td className="px-3 align-middle">
|
|
215
|
+
{/* Indentation is data-driven (24px per depth level), so it must be
|
|
216
|
+
an inline style rather than a class. */}
|
|
217
|
+
<div className="flex items-center" style={{ paddingLeft: depth * 24 }}>
|
|
218
|
+
{hasChildren ? (
|
|
219
|
+
<button
|
|
220
|
+
type="button"
|
|
221
|
+
aria-expanded={isExpanded}
|
|
222
|
+
aria-label={`${isExpanded ? 'Collapse' : 'Expand'} ${page.title}`}
|
|
223
|
+
onClick={onToggleExpand}
|
|
224
|
+
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-[5px] text-[var(--muted)] transition-colors duration-[var(--motion-fast)] hover:bg-[var(--acc-l)] hover:text-[var(--acc)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
|
|
225
|
+
>
|
|
226
|
+
<ChevronDown
|
|
227
|
+
size={13}
|
|
228
|
+
aria-hidden
|
|
229
|
+
className={`motion-safe:transition-transform motion-safe:duration-150 ${
|
|
230
|
+
isExpanded ? '' : '-rotate-90'
|
|
231
|
+
}`}
|
|
232
|
+
/>
|
|
233
|
+
</button>
|
|
234
|
+
) : (
|
|
235
|
+
<span className="w-5 shrink-0" aria-hidden />
|
|
236
|
+
)}
|
|
237
|
+
<div className="min-w-0">
|
|
238
|
+
<button
|
|
239
|
+
type="button"
|
|
240
|
+
onClick={onOpen}
|
|
241
|
+
className="block max-w-[280px] truncate text-left text-[13.5px] font-[550] text-[var(--txt)] transition-colors duration-[var(--motion-fast)] hover:text-[var(--acc)]"
|
|
242
|
+
>
|
|
243
|
+
{page.title}
|
|
244
|
+
</button>
|
|
245
|
+
<div className="max-w-[280px] truncate font-mono text-[11.5px] text-[var(--muted)]">
|
|
246
|
+
{page.path}
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
</td>
|
|
251
|
+
<td className="px-3 align-middle">
|
|
252
|
+
{page.templateId ? (
|
|
253
|
+
<span className="rounded-[5px] border border-[var(--bdr)] bg-[var(--bg)] px-2 py-0.5 text-[11.5px] whitespace-nowrap text-[var(--sub)]">
|
|
254
|
+
{page.templateId}
|
|
255
|
+
</span>
|
|
256
|
+
) : (
|
|
257
|
+
<span className="text-[var(--muted)]">—</span>
|
|
258
|
+
)}
|
|
259
|
+
</td>
|
|
260
|
+
<td className="px-3 align-middle">
|
|
261
|
+
<div className="flex items-center gap-[7px]">
|
|
262
|
+
<span
|
|
263
|
+
aria-hidden
|
|
264
|
+
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)]"
|
|
265
|
+
>
|
|
266
|
+
{page.authorInitials}
|
|
267
|
+
</span>
|
|
268
|
+
<span className="truncate text-[13px] text-[var(--txt)]">{page.authorName}</span>
|
|
269
|
+
</div>
|
|
270
|
+
</td>
|
|
271
|
+
<td className="px-3 align-middle">
|
|
272
|
+
<PostStatusBadge status={page.status} />
|
|
273
|
+
</td>
|
|
274
|
+
<td className="px-3 align-middle text-[12.5px] text-[var(--sub)] tabular-nums">
|
|
275
|
+
{formatDateShort(page.lastModified)}
|
|
276
|
+
</td>
|
|
277
|
+
<td className="px-3 align-middle">
|
|
278
|
+
<SeoScoreIndicator score={page.seoScore} />
|
|
279
|
+
</td>
|
|
280
|
+
<td className="px-3 align-middle">
|
|
281
|
+
<RowActions>
|
|
282
|
+
<button
|
|
283
|
+
type="button"
|
|
284
|
+
onClick={onOpen}
|
|
285
|
+
className="flex items-center gap-[5px] rounded-[6px] border border-[var(--bdr)] bg-[var(--card)] px-[9px] py-[5px] text-[12px] font-medium whitespace-nowrap text-[var(--txt)] transition-colors duration-[var(--motion-fast)] hover:border-[var(--acc)] hover:bg-[var(--acc-l)] hover:text-[var(--acc)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
|
|
286
|
+
>
|
|
287
|
+
<PenLine size={12} aria-hidden />
|
|
288
|
+
Edit
|
|
289
|
+
</button>
|
|
290
|
+
<RowActionButton
|
|
291
|
+
aria-label={`Add sub-page under "${page.title}"`}
|
|
292
|
+
title="Add sub-page"
|
|
293
|
+
onClick={onAddSubPage}
|
|
294
|
+
>
|
|
295
|
+
<Plus size={12} aria-hidden />
|
|
296
|
+
</RowActionButton>
|
|
297
|
+
<RowActionButton
|
|
298
|
+
aria-label={`Duplicate "${page.title}"`}
|
|
299
|
+
title="Duplicate"
|
|
300
|
+
onClick={onDuplicate}
|
|
301
|
+
>
|
|
302
|
+
<Copy size={12} aria-hidden />
|
|
303
|
+
</RowActionButton>
|
|
304
|
+
<RowActionButton
|
|
305
|
+
aria-label={`Delete "${page.title}"`}
|
|
306
|
+
title={page.isHomepage ? 'The homepage cannot be deleted' : 'Delete'}
|
|
307
|
+
danger
|
|
308
|
+
disabled={page.isHomepage}
|
|
309
|
+
onClick={onDelete}
|
|
310
|
+
>
|
|
311
|
+
<Trash2 size={12} aria-hidden />
|
|
312
|
+
</RowActionButton>
|
|
313
|
+
</RowActions>
|
|
314
|
+
</td>
|
|
315
|
+
</tr>
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function TreeLoadingRows() {
|
|
320
|
+
return (
|
|
321
|
+
<>
|
|
322
|
+
{Array.from({ length: 8 }).map((_, i) => (
|
|
323
|
+
<tr key={i} className="h-14 border-b border-[var(--bdr)] last:border-b-0">
|
|
324
|
+
{Array.from({ length: 8 }).map((__, j) => (
|
|
325
|
+
<td key={j} className="px-3 align-middle">
|
|
326
|
+
<div className="bg-muted h-4 w-full animate-pulse rounded" />
|
|
327
|
+
</td>
|
|
328
|
+
))}
|
|
329
|
+
</tr>
|
|
330
|
+
))}
|
|
331
|
+
</>
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function formatDateShort(input: string): string {
|
|
336
|
+
const d = new Date(input)
|
|
337
|
+
if (Number.isNaN(d.getTime())) return '—'
|
|
338
|
+
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
|
|
339
|
+
}
|