@actuate-media/cms-admin 0.66.0 → 0.68.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.
Files changed (244) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/AdminRoot.d.ts.map +1 -1
  3. package/dist/AdminRoot.js +36 -9
  4. package/dist/AdminRoot.js.map +1 -1
  5. package/dist/__tests__/components/inspector-pane.test.d.ts +2 -0
  6. package/dist/__tests__/components/inspector-pane.test.d.ts.map +1 -0
  7. package/dist/__tests__/components/inspector-pane.test.js +31 -0
  8. package/dist/__tests__/components/inspector-pane.test.js.map +1 -0
  9. package/dist/__tests__/hooks/useIdleLogout.test.d.ts +2 -0
  10. package/dist/__tests__/hooks/useIdleLogout.test.d.ts.map +1 -0
  11. package/dist/__tests__/hooks/useIdleLogout.test.js +64 -0
  12. package/dist/__tests__/hooks/useIdleLogout.test.js.map +1 -0
  13. package/dist/__tests__/lib/pages-tree.test.d.ts +2 -0
  14. package/dist/__tests__/lib/pages-tree.test.d.ts.map +1 -0
  15. package/dist/__tests__/lib/pages-tree.test.js +173 -0
  16. package/dist/__tests__/lib/pages-tree.test.js.map +1 -0
  17. package/dist/__tests__/lib/post-types-service.test.d.ts +2 -0
  18. package/dist/__tests__/lib/post-types-service.test.d.ts.map +1 -0
  19. package/dist/__tests__/lib/post-types-service.test.js +88 -0
  20. package/dist/__tests__/lib/post-types-service.test.js.map +1 -0
  21. package/dist/__tests__/views/collection-list-nav.render.test.js +43 -5
  22. package/dist/__tests__/views/collection-list-nav.render.test.js.map +1 -1
  23. package/dist/__tests__/views/dashboard.test.js +7 -3
  24. package/dist/__tests__/views/dashboard.test.js.map +1 -1
  25. package/dist/__tests__/views/forms-list.render.test.js +25 -6
  26. package/dist/__tests__/views/forms-list.render.test.js.map +1 -1
  27. package/dist/__tests__/views/media-seo-audit.render.test.d.ts +2 -0
  28. package/dist/__tests__/views/media-seo-audit.render.test.d.ts.map +1 -0
  29. package/dist/__tests__/views/media-seo-audit.render.test.js +102 -0
  30. package/dist/__tests__/views/media-seo-audit.render.test.js.map +1 -0
  31. package/dist/__tests__/views/onboarding-block.render.test.d.ts +2 -0
  32. package/dist/__tests__/views/onboarding-block.render.test.d.ts.map +1 -0
  33. package/dist/__tests__/views/onboarding-block.render.test.js +146 -0
  34. package/dist/__tests__/views/onboarding-block.render.test.js.map +1 -0
  35. package/dist/__tests__/views/page-builder-sections-panel.render.test.d.ts +2 -0
  36. package/dist/__tests__/views/page-builder-sections-panel.render.test.d.ts.map +1 -0
  37. package/dist/__tests__/views/page-builder-sections-panel.render.test.js +121 -0
  38. package/dist/__tests__/views/page-builder-sections-panel.render.test.js.map +1 -0
  39. package/dist/__tests__/views/page-templates.render.test.d.ts +2 -0
  40. package/dist/__tests__/views/page-templates.render.test.d.ts.map +1 -0
  41. package/dist/__tests__/views/page-templates.render.test.js +105 -0
  42. package/dist/__tests__/views/page-templates.render.test.js.map +1 -0
  43. package/dist/__tests__/views/pages-list-view.test.js +77 -1
  44. package/dist/__tests__/views/pages-list-view.test.js.map +1 -1
  45. package/dist/__tests__/views/post-type-editor.render.test.d.ts +2 -0
  46. package/dist/__tests__/views/post-type-editor.render.test.d.ts.map +1 -0
  47. package/dist/__tests__/views/post-type-editor.render.test.js +102 -0
  48. package/dist/__tests__/views/post-type-editor.render.test.js.map +1 -0
  49. package/dist/__tests__/views/security-settings.render.test.js +85 -4
  50. package/dist/__tests__/views/security-settings.render.test.js.map +1 -1
  51. package/dist/__tests__/views/seo-content-pane.render.test.d.ts +2 -0
  52. package/dist/__tests__/views/seo-content-pane.render.test.d.ts.map +1 -0
  53. package/dist/__tests__/views/seo-content-pane.render.test.js +117 -0
  54. package/dist/__tests__/views/seo-content-pane.render.test.js.map +1 -0
  55. package/dist/actuate-admin.css +1 -1
  56. package/dist/components/collections/EntryDetailPane.d.ts +10 -0
  57. package/dist/components/collections/EntryDetailPane.d.ts.map +1 -0
  58. package/dist/components/collections/EntryDetailPane.js +92 -0
  59. package/dist/components/collections/EntryDetailPane.js.map +1 -0
  60. package/dist/components/forms/FieldsPanel.js +1 -1
  61. package/dist/components/forms/FieldsPanel.js.map +1 -1
  62. package/dist/components/forms/FormSchemaPane.d.ts +8 -0
  63. package/dist/components/forms/FormSchemaPane.d.ts.map +1 -0
  64. package/dist/components/forms/FormSchemaPane.js +95 -0
  65. package/dist/components/forms/FormSchemaPane.js.map +1 -0
  66. package/dist/components/script-tags/ScriptTagEditorPane.d.ts +26 -0
  67. package/dist/components/script-tags/ScriptTagEditorPane.d.ts.map +1 -0
  68. package/dist/components/script-tags/ScriptTagEditorPane.js +160 -0
  69. package/dist/components/script-tags/ScriptTagEditorPane.js.map +1 -0
  70. package/dist/components/seo/SeoEditorPane.d.ts +9 -0
  71. package/dist/components/seo/SeoEditorPane.d.ts.map +1 -0
  72. package/dist/components/seo/SeoEditorPane.js +175 -0
  73. package/dist/components/seo/SeoEditorPane.js.map +1 -0
  74. package/dist/components/ui/Badge.d.ts +1 -1
  75. package/dist/components/ui/Button.d.ts +1 -1
  76. package/dist/components/ui/InspectorPane.d.ts +51 -0
  77. package/dist/components/ui/InspectorPane.d.ts.map +1 -0
  78. package/dist/components/ui/InspectorPane.js +42 -0
  79. package/dist/components/ui/InspectorPane.js.map +1 -0
  80. package/dist/components/ui/StatusBadge.d.ts +1 -1
  81. package/dist/components/ui/index.d.ts +2 -0
  82. package/dist/components/ui/index.d.ts.map +1 -1
  83. package/dist/components/ui/index.js +1 -0
  84. package/dist/components/ui/index.js.map +1 -1
  85. package/dist/hooks/useIdleLogout.d.ts +18 -0
  86. package/dist/hooks/useIdleLogout.d.ts.map +1 -0
  87. package/dist/hooks/useIdleLogout.js +66 -0
  88. package/dist/hooks/useIdleLogout.js.map +1 -0
  89. package/dist/lib/api.d.ts +1 -0
  90. package/dist/lib/api.d.ts.map +1 -1
  91. package/dist/lib/api.js +7 -1
  92. package/dist/lib/api.js.map +1 -1
  93. package/dist/lib/page-editor-service.d.ts +11 -2
  94. package/dist/lib/page-editor-service.d.ts.map +1 -1
  95. package/dist/lib/page-editor-service.js +8 -2
  96. package/dist/lib/page-editor-service.js.map +1 -1
  97. package/dist/lib/pages-tree.d.ts +44 -0
  98. package/dist/lib/pages-tree.d.ts.map +1 -0
  99. package/dist/lib/pages-tree.js +106 -0
  100. package/dist/lib/pages-tree.js.map +1 -0
  101. package/dist/lib/post-types-service.d.ts +44 -0
  102. package/dist/lib/post-types-service.d.ts.map +1 -0
  103. package/dist/lib/post-types-service.js +83 -0
  104. package/dist/lib/post-types-service.js.map +1 -0
  105. package/dist/views/CollectionList.d.ts.map +1 -1
  106. package/dist/views/CollectionList.js +23 -4
  107. package/dist/views/CollectionList.js.map +1 -1
  108. package/dist/views/Dashboard.d.ts.map +1 -1
  109. package/dist/views/Dashboard.js +31 -2
  110. package/dist/views/Dashboard.js.map +1 -1
  111. package/dist/views/Forms.d.ts.map +1 -1
  112. package/dist/views/Forms.js +25 -21
  113. package/dist/views/Forms.js.map +1 -1
  114. package/dist/views/MediaBrowser.d.ts.map +1 -1
  115. package/dist/views/MediaBrowser.js +28 -2
  116. package/dist/views/MediaBrowser.js.map +1 -1
  117. package/dist/views/MediaSeoAudit.d.ts +25 -0
  118. package/dist/views/MediaSeoAudit.d.ts.map +1 -0
  119. package/dist/views/MediaSeoAudit.js +97 -0
  120. package/dist/views/MediaSeoAudit.js.map +1 -0
  121. package/dist/views/Pages/PagesListView.d.ts.map +1 -1
  122. package/dist/views/Pages/PagesListView.js +90 -10
  123. package/dist/views/Pages/PagesListView.js.map +1 -1
  124. package/dist/views/Pages/PagesTreeTable.d.ts +35 -0
  125. package/dist/views/Pages/PagesTreeTable.d.ts.map +1 -0
  126. package/dist/views/Pages/PagesTreeTable.js +28 -0
  127. package/dist/views/Pages/PagesTreeTable.js.map +1 -0
  128. package/dist/views/Posts/PostTypeEditor.d.ts +16 -0
  129. package/dist/views/Posts/PostTypeEditor.d.ts.map +1 -0
  130. package/dist/views/Posts/PostTypeEditor.js +159 -0
  131. package/dist/views/Posts/PostTypeEditor.js.map +1 -0
  132. package/dist/views/Posts/PostTypesView.d.ts +3 -2
  133. package/dist/views/Posts/PostTypesView.d.ts.map +1 -1
  134. package/dist/views/Posts/PostTypesView.js +1 -1
  135. package/dist/views/Posts/PostTypesView.js.map +1 -1
  136. package/dist/views/ScriptTags.d.ts +5 -1
  137. package/dist/views/ScriptTags.d.ts.map +1 -1
  138. package/dist/views/ScriptTags.js +32 -7
  139. package/dist/views/ScriptTags.js.map +1 -1
  140. package/dist/views/dashboard/OnboardingBlock.d.ts +29 -0
  141. package/dist/views/dashboard/OnboardingBlock.d.ts.map +1 -0
  142. package/dist/views/dashboard/OnboardingBlock.js +71 -0
  143. package/dist/views/dashboard/OnboardingBlock.js.map +1 -0
  144. package/dist/views/page-builder/BuilderSectionsPanel.d.ts +20 -0
  145. package/dist/views/page-builder/BuilderSectionsPanel.d.ts.map +1 -0
  146. package/dist/views/page-builder/BuilderSectionsPanel.js +105 -0
  147. package/dist/views/page-builder/BuilderSectionsPanel.js.map +1 -0
  148. package/dist/views/page-builder/PageBuilder.d.ts.map +1 -1
  149. package/dist/views/page-builder/PageBuilder.js +51 -2
  150. package/dist/views/page-builder/PageBuilder.js.map +1 -1
  151. package/dist/views/page-builder/PageTemplates.d.ts.map +1 -1
  152. package/dist/views/page-builder/PageTemplates.js +113 -6
  153. package/dist/views/page-builder/PageTemplates.js.map +1 -1
  154. package/dist/views/page-editor/PageSectionEditor.d.ts +7 -1
  155. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  156. package/dist/views/page-editor/PageSectionEditor.js +3 -3
  157. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  158. package/dist/views/page-editor/PageSettingsModal.d.ts +1 -1
  159. package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -1
  160. package/dist/views/page-editor/PageSettingsModal.js +42 -3
  161. package/dist/views/page-editor/PageSettingsModal.js.map +1 -1
  162. package/dist/views/seo/ContentTab.d.ts.map +1 -1
  163. package/dist/views/seo/ContentTab.js +22 -6
  164. package/dist/views/seo/ContentTab.js.map +1 -1
  165. package/dist/views/settings/AccessProtectionCard.d.ts +5 -1
  166. package/dist/views/settings/AccessProtectionCard.d.ts.map +1 -1
  167. package/dist/views/settings/AccessProtectionCard.js +13 -10
  168. package/dist/views/settings/AccessProtectionCard.js.map +1 -1
  169. package/dist/views/settings/SecurityTab.d.ts.map +1 -1
  170. package/dist/views/settings/SecurityTab.js +3 -3
  171. package/dist/views/settings/SecurityTab.js.map +1 -1
  172. package/dist/views/settings/SessionPolicyCard.d.ts +1 -2
  173. package/dist/views/settings/SessionPolicyCard.d.ts.map +1 -1
  174. package/dist/views/settings/SessionPolicyCard.js +12 -5
  175. package/dist/views/settings/SessionPolicyCard.js.map +1 -1
  176. package/dist/views/settings/useSecuritySettings.d.ts +13 -1
  177. package/dist/views/settings/useSecuritySettings.d.ts.map +1 -1
  178. package/dist/views/settings/useSecuritySettings.js +47 -4
  179. package/dist/views/settings/useSecuritySettings.js.map +1 -1
  180. package/package.json +2 -2
  181. package/src/AdminRoot.tsx +43 -10
  182. package/src/__tests__/components/inspector-pane.test.tsx +64 -0
  183. package/src/__tests__/hooks/useIdleLogout.test.tsx +86 -0
  184. package/src/__tests__/lib/pages-tree.test.ts +197 -0
  185. package/src/__tests__/lib/post-types-service.test.ts +112 -0
  186. package/src/__tests__/views/collection-list-nav.render.test.tsx +51 -5
  187. package/src/__tests__/views/dashboard.test.tsx +7 -3
  188. package/src/__tests__/views/forms-list.render.test.tsx +34 -6
  189. package/src/__tests__/views/media-seo-audit.render.test.tsx +117 -0
  190. package/src/__tests__/views/onboarding-block.render.test.tsx +193 -0
  191. package/src/__tests__/views/page-builder-sections-panel.render.test.tsx +138 -0
  192. package/src/__tests__/views/page-templates.render.test.tsx +142 -0
  193. package/src/__tests__/views/pages-list-view.test.tsx +93 -1
  194. package/src/__tests__/views/post-type-editor.render.test.tsx +116 -0
  195. package/src/__tests__/views/security-settings.render.test.tsx +111 -4
  196. package/src/__tests__/views/seo-content-pane.render.test.tsx +142 -0
  197. package/src/components/collections/EntryDetailPane.tsx +182 -0
  198. package/src/components/forms/FieldsPanel.tsx +3 -1
  199. package/src/components/forms/FormSchemaPane.tsx +192 -0
  200. package/src/components/script-tags/ScriptTagEditorPane.tsx +474 -0
  201. package/src/components/seo/SeoEditorPane.tsx +533 -0
  202. package/src/components/ui/InspectorPane.tsx +129 -0
  203. package/src/components/ui/index.ts +10 -0
  204. package/src/hooks/useIdleLogout.ts +77 -0
  205. package/src/lib/api.ts +8 -2
  206. package/src/lib/page-editor-service.ts +16 -2
  207. package/src/lib/pages-tree.ts +145 -0
  208. package/src/lib/post-types-service.ts +129 -0
  209. package/src/views/CollectionList.tsx +158 -110
  210. package/src/views/Dashboard.tsx +238 -196
  211. package/src/views/Forms.tsx +154 -126
  212. package/src/views/MediaBrowser.tsx +641 -511
  213. package/src/views/MediaSeoAudit.tsx +320 -0
  214. package/src/views/Pages/PagesListView.tsx +352 -183
  215. package/src/views/Pages/PagesTreeTable.tsx +339 -0
  216. package/src/views/Posts/PostTypeEditor.tsx +568 -0
  217. package/src/views/Posts/PostTypesView.tsx +5 -4
  218. package/src/views/ScriptTags.tsx +204 -129
  219. package/src/views/dashboard/OnboardingBlock.tsx +233 -0
  220. package/src/views/page-builder/BuilderSectionsPanel.tsx +304 -0
  221. package/src/views/page-builder/PageBuilder.tsx +69 -2
  222. package/src/views/page-builder/PageTemplates.tsx +340 -57
  223. package/src/views/page-editor/PageSectionEditor.tsx +9 -2
  224. package/src/views/page-editor/PageSettingsModal.tsx +75 -2
  225. package/src/views/seo/ContentTab.tsx +113 -80
  226. package/src/views/settings/AccessProtectionCard.tsx +83 -53
  227. package/src/views/settings/SecurityTab.tsx +23 -2
  228. package/src/views/settings/SessionPolicyCard.tsx +45 -9
  229. package/src/views/settings/useSecuritySettings.ts +78 -22
  230. package/dist/components/forms/FormSchemaDrawer.d.ts +0 -9
  231. package/dist/components/forms/FormSchemaDrawer.d.ts.map +0 -1
  232. package/dist/components/forms/FormSchemaDrawer.js +0 -99
  233. package/dist/components/forms/FormSchemaDrawer.js.map +0 -1
  234. package/dist/components/seo/SeoEditorDrawer.d.ts +0 -8
  235. package/dist/components/seo/SeoEditorDrawer.d.ts.map +0 -1
  236. package/dist/components/seo/SeoEditorDrawer.js +0 -172
  237. package/dist/components/seo/SeoEditorDrawer.js.map +0 -1
  238. package/dist/views/ScriptTagEditor.d.ts +0 -6
  239. package/dist/views/ScriptTagEditor.d.ts.map +0 -1
  240. package/dist/views/ScriptTagEditor.js +0 -111
  241. package/dist/views/ScriptTagEditor.js.map +0 -1
  242. package/src/components/forms/FormSchemaDrawer.tsx +0 -192
  243. package/src/components/seo/SeoEditorDrawer.tsx +0 -529
  244. 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
+ }