@actuate-media/cms-admin 0.67.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 (146) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/AdminRoot.d.ts.map +1 -1
  3. package/dist/AdminRoot.js +29 -5
  4. package/dist/AdminRoot.js.map +1 -1
  5. package/dist/__tests__/hooks/useIdleLogout.test.d.ts +2 -0
  6. package/dist/__tests__/hooks/useIdleLogout.test.d.ts.map +1 -0
  7. package/dist/__tests__/hooks/useIdleLogout.test.js +64 -0
  8. package/dist/__tests__/hooks/useIdleLogout.test.js.map +1 -0
  9. package/dist/__tests__/lib/post-types-service.test.d.ts +2 -0
  10. package/dist/__tests__/lib/post-types-service.test.d.ts.map +1 -0
  11. package/dist/__tests__/lib/post-types-service.test.js +88 -0
  12. package/dist/__tests__/lib/post-types-service.test.js.map +1 -0
  13. package/dist/__tests__/views/collection-list-nav.render.test.js +43 -5
  14. package/dist/__tests__/views/collection-list-nav.render.test.js.map +1 -1
  15. package/dist/__tests__/views/forms-list.render.test.js +25 -6
  16. package/dist/__tests__/views/forms-list.render.test.js.map +1 -1
  17. package/dist/__tests__/views/page-builder-sections-panel.render.test.d.ts +2 -0
  18. package/dist/__tests__/views/page-builder-sections-panel.render.test.d.ts.map +1 -0
  19. package/dist/__tests__/views/page-builder-sections-panel.render.test.js +121 -0
  20. package/dist/__tests__/views/page-builder-sections-panel.render.test.js.map +1 -0
  21. package/dist/__tests__/views/page-templates.render.test.d.ts +2 -0
  22. package/dist/__tests__/views/page-templates.render.test.d.ts.map +1 -0
  23. package/dist/__tests__/views/page-templates.render.test.js +105 -0
  24. package/dist/__tests__/views/page-templates.render.test.js.map +1 -0
  25. package/dist/__tests__/views/post-type-editor.render.test.d.ts +2 -0
  26. package/dist/__tests__/views/post-type-editor.render.test.d.ts.map +1 -0
  27. package/dist/__tests__/views/post-type-editor.render.test.js +102 -0
  28. package/dist/__tests__/views/post-type-editor.render.test.js.map +1 -0
  29. package/dist/__tests__/views/security-settings.render.test.js +85 -4
  30. package/dist/__tests__/views/security-settings.render.test.js.map +1 -1
  31. package/dist/__tests__/views/seo-content-pane.render.test.d.ts +2 -0
  32. package/dist/__tests__/views/seo-content-pane.render.test.d.ts.map +1 -0
  33. package/dist/__tests__/views/seo-content-pane.render.test.js +117 -0
  34. package/dist/__tests__/views/seo-content-pane.render.test.js.map +1 -0
  35. package/dist/actuate-admin.css +1 -1
  36. package/dist/components/collections/EntryDetailPane.d.ts +10 -0
  37. package/dist/components/collections/EntryDetailPane.d.ts.map +1 -0
  38. package/dist/components/collections/EntryDetailPane.js +92 -0
  39. package/dist/components/collections/EntryDetailPane.js.map +1 -0
  40. package/dist/components/forms/FieldsPanel.js +1 -1
  41. package/dist/components/forms/FieldsPanel.js.map +1 -1
  42. package/dist/components/forms/FormSchemaPane.d.ts +8 -0
  43. package/dist/components/forms/FormSchemaPane.d.ts.map +1 -0
  44. package/dist/components/forms/FormSchemaPane.js +95 -0
  45. package/dist/components/forms/FormSchemaPane.js.map +1 -0
  46. package/dist/components/seo/SeoEditorPane.d.ts +9 -0
  47. package/dist/components/seo/SeoEditorPane.d.ts.map +1 -0
  48. package/dist/components/seo/SeoEditorPane.js +175 -0
  49. package/dist/components/seo/SeoEditorPane.js.map +1 -0
  50. package/dist/components/ui/Badge.d.ts +1 -1
  51. package/dist/components/ui/Button.d.ts +1 -1
  52. package/dist/components/ui/StatusBadge.d.ts +1 -1
  53. package/dist/hooks/useIdleLogout.d.ts +18 -0
  54. package/dist/hooks/useIdleLogout.d.ts.map +1 -0
  55. package/dist/hooks/useIdleLogout.js +66 -0
  56. package/dist/hooks/useIdleLogout.js.map +1 -0
  57. package/dist/lib/api.d.ts +1 -0
  58. package/dist/lib/api.d.ts.map +1 -1
  59. package/dist/lib/api.js +7 -1
  60. package/dist/lib/api.js.map +1 -1
  61. package/dist/lib/post-types-service.d.ts +44 -0
  62. package/dist/lib/post-types-service.d.ts.map +1 -0
  63. package/dist/lib/post-types-service.js +83 -0
  64. package/dist/lib/post-types-service.js.map +1 -0
  65. package/dist/views/CollectionList.d.ts.map +1 -1
  66. package/dist/views/CollectionList.js +23 -4
  67. package/dist/views/CollectionList.js.map +1 -1
  68. package/dist/views/Forms.d.ts.map +1 -1
  69. package/dist/views/Forms.js +25 -21
  70. package/dist/views/Forms.js.map +1 -1
  71. package/dist/views/Posts/PostTypeEditor.d.ts +16 -0
  72. package/dist/views/Posts/PostTypeEditor.d.ts.map +1 -0
  73. package/dist/views/Posts/PostTypeEditor.js +159 -0
  74. package/dist/views/Posts/PostTypeEditor.js.map +1 -0
  75. package/dist/views/Posts/PostTypesView.d.ts +3 -2
  76. package/dist/views/Posts/PostTypesView.d.ts.map +1 -1
  77. package/dist/views/Posts/PostTypesView.js +1 -1
  78. package/dist/views/Posts/PostTypesView.js.map +1 -1
  79. package/dist/views/page-builder/BuilderSectionsPanel.d.ts +20 -0
  80. package/dist/views/page-builder/BuilderSectionsPanel.d.ts.map +1 -0
  81. package/dist/views/page-builder/BuilderSectionsPanel.js +105 -0
  82. package/dist/views/page-builder/BuilderSectionsPanel.js.map +1 -0
  83. package/dist/views/page-builder/PageBuilder.d.ts.map +1 -1
  84. package/dist/views/page-builder/PageBuilder.js +51 -2
  85. package/dist/views/page-builder/PageBuilder.js.map +1 -1
  86. package/dist/views/page-builder/PageTemplates.d.ts.map +1 -1
  87. package/dist/views/page-builder/PageTemplates.js +113 -6
  88. package/dist/views/page-builder/PageTemplates.js.map +1 -1
  89. package/dist/views/seo/ContentTab.d.ts.map +1 -1
  90. package/dist/views/seo/ContentTab.js +22 -6
  91. package/dist/views/seo/ContentTab.js.map +1 -1
  92. package/dist/views/settings/AccessProtectionCard.d.ts +5 -1
  93. package/dist/views/settings/AccessProtectionCard.d.ts.map +1 -1
  94. package/dist/views/settings/AccessProtectionCard.js +13 -10
  95. package/dist/views/settings/AccessProtectionCard.js.map +1 -1
  96. package/dist/views/settings/SecurityTab.d.ts.map +1 -1
  97. package/dist/views/settings/SecurityTab.js +3 -3
  98. package/dist/views/settings/SecurityTab.js.map +1 -1
  99. package/dist/views/settings/SessionPolicyCard.d.ts +1 -2
  100. package/dist/views/settings/SessionPolicyCard.d.ts.map +1 -1
  101. package/dist/views/settings/SessionPolicyCard.js +12 -5
  102. package/dist/views/settings/SessionPolicyCard.js.map +1 -1
  103. package/dist/views/settings/useSecuritySettings.d.ts +13 -1
  104. package/dist/views/settings/useSecuritySettings.d.ts.map +1 -1
  105. package/dist/views/settings/useSecuritySettings.js +47 -4
  106. package/dist/views/settings/useSecuritySettings.js.map +1 -1
  107. package/package.json +2 -2
  108. package/src/AdminRoot.tsx +36 -7
  109. package/src/__tests__/hooks/useIdleLogout.test.tsx +86 -0
  110. package/src/__tests__/lib/post-types-service.test.ts +112 -0
  111. package/src/__tests__/views/collection-list-nav.render.test.tsx +51 -5
  112. package/src/__tests__/views/forms-list.render.test.tsx +34 -6
  113. package/src/__tests__/views/page-builder-sections-panel.render.test.tsx +138 -0
  114. package/src/__tests__/views/page-templates.render.test.tsx +142 -0
  115. package/src/__tests__/views/post-type-editor.render.test.tsx +116 -0
  116. package/src/__tests__/views/security-settings.render.test.tsx +111 -4
  117. package/src/__tests__/views/seo-content-pane.render.test.tsx +142 -0
  118. package/src/components/collections/EntryDetailPane.tsx +182 -0
  119. package/src/components/forms/FieldsPanel.tsx +3 -1
  120. package/src/components/forms/FormSchemaPane.tsx +192 -0
  121. package/src/components/seo/SeoEditorPane.tsx +533 -0
  122. package/src/hooks/useIdleLogout.ts +77 -0
  123. package/src/lib/api.ts +8 -2
  124. package/src/lib/post-types-service.ts +129 -0
  125. package/src/views/CollectionList.tsx +158 -110
  126. package/src/views/Forms.tsx +154 -126
  127. package/src/views/Posts/PostTypeEditor.tsx +568 -0
  128. package/src/views/Posts/PostTypesView.tsx +5 -4
  129. package/src/views/page-builder/BuilderSectionsPanel.tsx +304 -0
  130. package/src/views/page-builder/PageBuilder.tsx +69 -2
  131. package/src/views/page-builder/PageTemplates.tsx +340 -57
  132. package/src/views/seo/ContentTab.tsx +113 -80
  133. package/src/views/settings/AccessProtectionCard.tsx +83 -53
  134. package/src/views/settings/SecurityTab.tsx +23 -2
  135. package/src/views/settings/SessionPolicyCard.tsx +45 -9
  136. package/src/views/settings/useSecuritySettings.ts +78 -22
  137. package/dist/components/forms/FormSchemaDrawer.d.ts +0 -9
  138. package/dist/components/forms/FormSchemaDrawer.d.ts.map +0 -1
  139. package/dist/components/forms/FormSchemaDrawer.js +0 -99
  140. package/dist/components/forms/FormSchemaDrawer.js.map +0 -1
  141. package/dist/components/seo/SeoEditorDrawer.d.ts +0 -8
  142. package/dist/components/seo/SeoEditorDrawer.d.ts.map +0 -1
  143. package/dist/components/seo/SeoEditorDrawer.js +0 -172
  144. package/dist/components/seo/SeoEditorDrawer.js.map +0 -1
  145. package/src/components/forms/FormSchemaDrawer.tsx +0 -192
  146. package/src/components/seo/SeoEditorDrawer.tsx +0 -529
@@ -0,0 +1,129 @@
1
+ import { cmsApi } from './api.js'
2
+
3
+ /**
4
+ * Detail metadata for one post type (collection), sourced from the public
5
+ * `GET /collections` introspection endpoint. Read-only by design: collection
6
+ * schemas are file-defined in `actuate.config.ts`, so the Post Type editor
7
+ * inspects the live schema and generates config snippets for changes.
8
+ */
9
+ export interface PostTypeFieldSummary {
10
+ name: string
11
+ type: string
12
+ required: boolean
13
+ relationTo?: string | string[]
14
+ options?: unknown[]
15
+ }
16
+
17
+ export interface PostTypeDetail {
18
+ slug: string
19
+ singular: string
20
+ plural: string
21
+ description: string | null
22
+ urlPrefix: string
23
+ icon: string | null
24
+ color: string | null
25
+ fields: PostTypeFieldSummary[]
26
+ }
27
+
28
+ interface CollectionDetailRow {
29
+ slug: string
30
+ labels: { singular: string; plural: string }
31
+ type: string
32
+ urlPrefix: string
33
+ description: string | null
34
+ icon: string | null
35
+ color: string | null
36
+ hidden: boolean
37
+ fields: PostTypeFieldSummary[]
38
+ }
39
+
40
+ export async function fetchPostTypeDetail(slug: string): Promise<PostTypeDetail | null> {
41
+ const res = await cmsApi<CollectionDetailRow[]>('/collections')
42
+ if (res.error || !res.data) {
43
+ throw new Error(res.error ?? 'Failed to load collections')
44
+ }
45
+ const row = res.data.find((c) => c.slug === slug)
46
+ if (!row) return null
47
+ return {
48
+ slug: row.slug,
49
+ singular: row.labels?.singular ?? slug,
50
+ plural: row.labels?.plural ?? slug,
51
+ description: row.description ?? null,
52
+ urlPrefix: row.urlPrefix || slug,
53
+ icon: row.icon ?? null,
54
+ color: row.color ?? null,
55
+ fields: Array.isArray(row.fields) ? row.fields : [],
56
+ }
57
+ }
58
+
59
+ /** Friendly display names for cms-core field types (design's type badges). */
60
+ export const FIELD_TYPE_LABELS: Record<string, string> = {
61
+ text: 'Short Text',
62
+ richText: 'Rich Text',
63
+ number: 'Number',
64
+ boolean: 'Boolean',
65
+ date: 'Date',
66
+ slug: 'Slug',
67
+ select: 'Select',
68
+ relationship: 'Relation',
69
+ media: 'Media',
70
+ json: 'JSON',
71
+ color: 'Color',
72
+ url: 'URL',
73
+ email: 'Email',
74
+ array: 'List',
75
+ blocks: 'Blocks',
76
+ }
77
+
78
+ export function fieldTypeLabel(type: string): string {
79
+ return FIELD_TYPE_LABELS[type] ?? type
80
+ }
81
+
82
+ /** "clientName" / "client_name" → "Client Name" for display. */
83
+ export function humanizeFieldName(name: string): string {
84
+ return name
85
+ .replace(/[_-]+/g, ' ')
86
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
87
+ .replace(/\s+/g, ' ')
88
+ .trim()
89
+ .replace(/\b\w/g, (c) => c.toUpperCase())
90
+ }
91
+
92
+ /** "Client Name" → "clientName" for generated field keys. */
93
+ export function fieldKeyFromName(name: string): string {
94
+ const words = name
95
+ .replace(/[^a-zA-Z0-9]+/g, ' ')
96
+ .trim()
97
+ .split(/\s+/)
98
+ .filter(Boolean)
99
+ if (words.length === 0) return ''
100
+ return words
101
+ .map((w, i) => (i === 0 ? w.toLowerCase() : w[0]!.toUpperCase() + w.slice(1).toLowerCase()))
102
+ .join('')
103
+ }
104
+
105
+ export interface FieldSnippetInput {
106
+ key: string
107
+ label: string
108
+ type: string
109
+ required: boolean
110
+ helpText?: string
111
+ }
112
+
113
+ /**
114
+ * Paste-ready field entry for the collection's `fields` map in
115
+ * `actuate.config.ts` — same escape rules as the New Post Type snippet.
116
+ */
117
+ export function renderFieldSnippet(input: FieldSnippetInput): string {
118
+ const esc = (s: string) => s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
119
+ const parts = [`type: '${input.type}'`, `label: '${esc(input.label)}'`]
120
+ if (input.required) parts.push('required: true')
121
+ if (input.helpText?.trim()) parts.push(`admin: { description: '${esc(input.helpText.trim())}' }`)
122
+ if (input.type === 'select') {
123
+ parts.push(`options: [{ label: 'Option A', value: 'option-a' }]`)
124
+ }
125
+ if (input.type === 'relationship') {
126
+ parts.push(`relationTo: 'posts'`)
127
+ }
128
+ return `${input.key}: { ${parts.join(', ')} },`
129
+ }
@@ -15,6 +15,8 @@ import { toast } from 'sonner'
15
15
  import { cmsApi } from '../lib/api.js'
16
16
  import { useApiData } from '../lib/useApiData.js'
17
17
  import { editPathForDoc, newPathForCollection } from '../lib/collection-routes.js'
18
+ import { InspectorPane, SplitPaneLayout, SplitPaneList } from '../components/ui/InspectorPane.js'
19
+ import { EntryDetailPane } from '../components/collections/EntryDetailPane.js'
18
20
 
19
21
  export interface CollectionListProps {
20
22
  collectionSlug: string
@@ -164,6 +166,8 @@ export function CollectionList({ collectionSlug, config, onNavigate }: Collectio
164
166
  const [sort, setSort] = useState<SortField | null>(null)
165
167
  const [order, setOrder] = useState<SortOrder>('asc')
166
168
  const [selected, setSelected] = useState<Set<string>>(new Set())
169
+ // Row selected into the inline detail pane (design Entries split-pane).
170
+ const [detailId, setDetailId] = useState<string | null>(null)
167
171
  const [filters, setFilters] = useState<Record<string, string>>({})
168
172
  // In-use values per `values`-sourced facet, fetched once per collection.
169
173
  const [facetValues, setFacetValues] = useState<Record<string, string[]>>({})
@@ -196,6 +200,7 @@ export function CollectionList({ collectionSlug, config, onNavigate }: Collectio
196
200
  useEffect(() => {
197
201
  setPage(1)
198
202
  setSelected(new Set())
203
+ setDetailId(null)
199
204
  setSearch('')
200
205
  setFilters({})
201
206
  setFacetValues({})
@@ -242,6 +247,12 @@ export function CollectionList({ collectionSlug, config, onNavigate }: Collectio
242
247
  })
243
248
  }, [])
244
249
 
250
+ const detailDoc = useMemo(
251
+ () => (detailId ? (docs.find((d: any) => String(d.id) === detailId) ?? null) : null),
252
+ [docs, detailId],
253
+ )
254
+ const toggleDetail = (id: string) => setDetailId((prev) => (prev === id ? null : id))
255
+
245
256
  const toggleSelect = (id: string) =>
246
257
  setSelected((prev) => {
247
258
  const s = new Set(prev)
@@ -454,120 +465,157 @@ export function CollectionList({ collectionSlug, config, onNavigate }: Collectio
454
465
  </button>
455
466
  </div>
456
467
  ) : (
457
- <div
458
- className="flex-1 overflow-auto rounded-lg border"
459
- style={{ borderColor: 'var(--actuate-border, #d1d5db)' }}
460
- >
461
- <table className="w-full text-sm">
462
- <thead
463
- className="sticky top-0"
464
- style={{
465
- background: 'var(--actuate-surface-alt, #f9fafb)',
466
- borderBottom: '1px solid var(--actuate-border, #d1d5db)',
467
- }}
468
+ <SplitPaneLayout>
469
+ <SplitPaneList>
470
+ <div
471
+ className="h-full overflow-auto rounded-lg border"
472
+ style={{ borderColor: 'var(--actuate-border, #d1d5db)' }}
468
473
  >
469
- <tr>
470
- <th className="w-10 px-3 py-2 text-left">
471
- <input
472
- type="checkbox"
473
- checked={selected.size === docs.length && docs.length > 0}
474
- onChange={(e) => toggleAll(e.target.checked)}
475
- className="rounded"
476
- />
477
- </th>
478
- <th className="px-3 py-2 text-left">
479
- <SortCol field="title">Title</SortCol>
480
- </th>
481
- {facets.map((facet) => (
482
- <th
483
- key={facet.name}
484
- className="px-3 py-2 text-left text-xs font-medium"
485
- style={{ color: 'var(--actuate-text-secondary, #6b7280)' }}
486
- >
487
- {facet.label}
488
- </th>
489
- ))}
490
- <th className="px-3 py-2 text-left">
491
- <SortCol field="status">Status</SortCol>
492
- </th>
493
- <th className="px-3 py-2 text-left">
494
- <SortCol field="updatedAt">Updated</SortCol>
495
- </th>
496
- <th
497
- className="px-3 py-2 text-left text-xs font-medium"
498
- style={{ color: 'var(--actuate-text-secondary, #6b7280)' }}
474
+ <table className="w-full text-sm">
475
+ <thead
476
+ className="sticky top-0"
477
+ style={{
478
+ background: 'var(--actuate-surface-alt, #f9fafb)',
479
+ borderBottom: '1px solid var(--actuate-border, #d1d5db)',
480
+ }}
499
481
  >
500
- Actions
501
- </th>
502
- </tr>
503
- </thead>
504
- <tbody>
505
- {docs.map((doc: any) => (
506
- <tr
507
- key={doc.id}
508
- className="transition-colors hover:opacity-95"
509
- style={{ borderBottom: '1px solid var(--actuate-border, #e5e7eb)' }}
510
- >
511
- <td className="px-3 py-2">
512
- <input
513
- type="checkbox"
514
- checked={selected.has(String(doc.id))}
515
- onChange={() => toggleSelect(String(doc.id))}
516
- className="rounded"
517
- />
518
- </td>
519
- <td className="px-3 py-2">
520
- <button
521
- type="button"
522
- onClick={() => onNavigate(editPath(doc.id))}
523
- className="text-left font-medium hover:underline"
524
- style={{ color: 'var(--actuate-text, #111827)' }}
525
- >
526
- {doc.title || doc.name || `#${doc.id}`}
527
- </button>
528
- </td>
529
- {facets.map((facet) => (
530
- <td
531
- key={facet.name}
532
- className="max-w-[200px] truncate px-3 py-2"
482
+ <tr>
483
+ <th className="w-10 px-3 py-2 text-left">
484
+ <input
485
+ type="checkbox"
486
+ checked={selected.size === docs.length && docs.length > 0}
487
+ onChange={(e) => toggleAll(e.target.checked)}
488
+ className="rounded"
489
+ />
490
+ </th>
491
+ <th className="px-3 py-2 text-left">
492
+ <SortCol field="title">Title</SortCol>
493
+ </th>
494
+ {facets.map((facet) => (
495
+ <th
496
+ key={facet.name}
497
+ className="px-3 py-2 text-left text-xs font-medium"
498
+ style={{ color: 'var(--actuate-text-secondary, #6b7280)' }}
499
+ >
500
+ {facet.label}
501
+ </th>
502
+ ))}
503
+ <th className="px-3 py-2 text-left">
504
+ <SortCol field="status">Status</SortCol>
505
+ </th>
506
+ <th className="px-3 py-2 text-left">
507
+ <SortCol field="updatedAt">Updated</SortCol>
508
+ </th>
509
+ <th
510
+ className="px-3 py-2 text-left text-xs font-medium"
533
511
  style={{ color: 'var(--actuate-text-secondary, #6b7280)' }}
534
- title={facetCellValue(doc, facet) || undefined}
535
- >
536
- {facetCellValue(doc, facet) || '—'}
537
- </td>
538
- ))}
539
- <td className="px-3 py-2">
540
- <span
541
- className="inline-block rounded-full px-2 py-0.5 text-xs font-medium"
542
- style={{ background: statusColor(doc.status), color: statusText(doc.status) }}
543
512
  >
544
- {doc.status ?? 'DRAFT'}
545
- </span>
546
- </td>
547
- <td
548
- className="px-3 py-2"
549
- style={{ color: 'var(--actuate-text-secondary, #6b7280)' }}
550
- >
551
- {formatDate(doc.updatedAt)}
552
- </td>
553
- <td className="px-3 py-2">
554
- <button
555
- type="button"
556
- onClick={() => onNavigate(editPath(doc.id))}
557
- className="rounded p-1.5 transition-opacity hover:opacity-75"
558
- title="Edit"
559
- >
560
- <MoreHorizontal
561
- className="h-4 w-4"
562
- style={{ color: 'var(--actuate-text-secondary, #6b7280)' }}
563
- />
564
- </button>
565
- </td>
566
- </tr>
567
- ))}
568
- </tbody>
569
- </table>
570
- </div>
513
+ Actions
514
+ </th>
515
+ </tr>
516
+ </thead>
517
+ <tbody>
518
+ {docs.map((doc: any) => {
519
+ const isDetail = detailId === String(doc.id)
520
+ return (
521
+ <tr
522
+ key={doc.id}
523
+ className={`cursor-pointer transition-colors ${isDetail ? 'bg-[var(--acc-l)]' : 'hover:opacity-95'}`}
524
+ style={{ borderBottom: '1px solid var(--actuate-border, #e5e7eb)' }}
525
+ onClick={() => toggleDetail(String(doc.id))}
526
+ onDoubleClick={() => onNavigate(editPath(doc.id))}
527
+ >
528
+ <td className="px-3 py-2" onClick={(e) => e.stopPropagation()}>
529
+ <input
530
+ type="checkbox"
531
+ checked={selected.has(String(doc.id))}
532
+ onChange={() => toggleSelect(String(doc.id))}
533
+ className="rounded"
534
+ />
535
+ </td>
536
+ <td className="px-3 py-2">
537
+ <button
538
+ type="button"
539
+ onClick={(e) => {
540
+ e.stopPropagation()
541
+ toggleDetail(String(doc.id))
542
+ }}
543
+ aria-expanded={isDetail}
544
+ className="text-left font-medium hover:underline"
545
+ style={{ color: 'var(--actuate-text, #111827)' }}
546
+ >
547
+ {doc.title || doc.name || `#${doc.id}`}
548
+ </button>
549
+ </td>
550
+ {facets.map((facet) => (
551
+ <td
552
+ key={facet.name}
553
+ className="max-w-[200px] truncate px-3 py-2"
554
+ style={{ color: 'var(--actuate-text-secondary, #6b7280)' }}
555
+ title={facetCellValue(doc, facet) || undefined}
556
+ >
557
+ {facetCellValue(doc, facet) || '—'}
558
+ </td>
559
+ ))}
560
+ <td className="px-3 py-2">
561
+ <span
562
+ className="inline-block rounded-full px-2 py-0.5 text-xs font-medium"
563
+ style={{
564
+ background: statusColor(doc.status),
565
+ color: statusText(doc.status),
566
+ }}
567
+ >
568
+ {doc.status ?? 'DRAFT'}
569
+ </span>
570
+ </td>
571
+ <td
572
+ className="px-3 py-2"
573
+ style={{ color: 'var(--actuate-text-secondary, #6b7280)' }}
574
+ >
575
+ {formatDate(doc.updatedAt)}
576
+ </td>
577
+ <td className="px-3 py-2">
578
+ <button
579
+ type="button"
580
+ onClick={(e) => {
581
+ e.stopPropagation()
582
+ onNavigate(editPath(doc.id))
583
+ }}
584
+ className="rounded p-1.5 transition-opacity hover:opacity-75"
585
+ title="Edit"
586
+ aria-label={`Edit ${doc.title || doc.name || `#${doc.id}`}`}
587
+ >
588
+ <MoreHorizontal
589
+ className="h-4 w-4"
590
+ style={{ color: 'var(--actuate-text-secondary, #6b7280)' }}
591
+ />
592
+ </button>
593
+ </td>
594
+ </tr>
595
+ )
596
+ })}
597
+ </tbody>
598
+ </table>
599
+ </div>
600
+ </SplitPaneList>
601
+
602
+ <InspectorPane
603
+ open={detailDoc !== null}
604
+ width={400}
605
+ aria-label={`${labels.singular} details`}
606
+ >
607
+ {detailDoc && (
608
+ <EntryDetailPane
609
+ key={String(detailDoc.id)}
610
+ doc={detailDoc}
611
+ collection={collectionDef}
612
+ singularLabel={labels.singular}
613
+ onClose={() => setDetailId(null)}
614
+ onOpenEditor={() => onNavigate(editPath(detailDoc.id))}
615
+ />
616
+ )}
617
+ </InspectorPane>
618
+ </SplitPaneLayout>
571
619
  )}
572
620
 
573
621
  {/* Pagination */}