@actuate-media/cms-admin 0.68.0 → 0.69.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.
Files changed (166) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/AdminRoot.d.ts.map +1 -1
  3. package/dist/AdminRoot.js +31 -11
  4. package/dist/AdminRoot.js.map +1 -1
  5. package/dist/__tests__/hooks/useIdleLogout.test.js +57 -2
  6. package/dist/__tests__/hooks/useIdleLogout.test.js.map +1 -1
  7. package/dist/__tests__/router/query-string.test.d.ts +2 -0
  8. package/dist/__tests__/router/query-string.test.d.ts.map +1 -0
  9. package/dist/__tests__/router/query-string.test.js +44 -0
  10. package/dist/__tests__/router/query-string.test.js.map +1 -0
  11. package/dist/__tests__/views/form-entries.render.test.js +14 -4
  12. package/dist/__tests__/views/form-entries.render.test.js.map +1 -1
  13. package/dist/__tests__/views/form-submissions.render.test.js +14 -4
  14. package/dist/__tests__/views/form-submissions.render.test.js.map +1 -1
  15. package/dist/__tests__/views/media-seo-audit.render.test.js +26 -0
  16. package/dist/__tests__/views/media-seo-audit.render.test.js.map +1 -1
  17. package/dist/__tests__/views/redirects-pane.render.test.d.ts +2 -0
  18. package/dist/__tests__/views/redirects-pane.render.test.d.ts.map +1 -0
  19. package/dist/__tests__/views/redirects-pane.render.test.js +121 -0
  20. package/dist/__tests__/views/redirects-pane.render.test.js.map +1 -0
  21. package/dist/__tests__/views/users-selected-deeplink.render.test.d.ts +2 -0
  22. package/dist/__tests__/views/users-selected-deeplink.render.test.d.ts.map +1 -0
  23. package/dist/__tests__/views/users-selected-deeplink.render.test.js +91 -0
  24. package/dist/__tests__/views/users-selected-deeplink.render.test.js.map +1 -0
  25. package/dist/actuate-admin.css +1 -1
  26. package/dist/components/SEOPanel.js +3 -3
  27. package/dist/components/SEOPanel.js.map +1 -1
  28. package/dist/components/collections/EntryDetailPane.js +1 -1
  29. package/dist/components/collections/EntryDetailPane.js.map +1 -1
  30. package/dist/components/forms/EntriesTable.d.ts +3 -1
  31. package/dist/components/forms/EntriesTable.d.ts.map +1 -1
  32. package/dist/components/forms/EntriesTable.js +5 -2
  33. package/dist/components/forms/EntriesTable.js.map +1 -1
  34. package/dist/components/forms/FormEntryDetailPane.d.ts +10 -0
  35. package/dist/components/forms/FormEntryDetailPane.d.ts.map +1 -0
  36. package/dist/components/forms/FormEntryDetailPane.js +137 -0
  37. package/dist/components/forms/FormEntryDetailPane.js.map +1 -0
  38. package/dist/components/forms/FormSchemaPane.js +1 -1
  39. package/dist/components/forms/FormSchemaPane.js.map +1 -1
  40. package/dist/components/script-tags/ScriptTagEditorPane.js +1 -1
  41. package/dist/components/script-tags/ScriptTagEditorPane.js.map +1 -1
  42. package/dist/components/seo/RedirectEditorPane.d.ts +12 -0
  43. package/dist/components/seo/RedirectEditorPane.d.ts.map +1 -0
  44. package/dist/components/seo/RedirectEditorPane.js +89 -0
  45. package/dist/components/seo/RedirectEditorPane.js.map +1 -0
  46. package/dist/components/seo/SeoEditorPane.js +1 -1
  47. package/dist/components/seo/SeoEditorPane.js.map +1 -1
  48. package/dist/components/ui/DataTable.js +2 -2
  49. package/dist/components/ui/DataTable.js.map +1 -1
  50. package/dist/components/ui/Skeleton.js +3 -3
  51. package/dist/components/ui/Skeleton.js.map +1 -1
  52. package/dist/components/ui/Toggle.js +1 -1
  53. package/dist/components/ui/Toggle.js.map +1 -1
  54. package/dist/fields/BlockBuilderField.js +1 -1
  55. package/dist/fields/BlockBuilderField.js.map +1 -1
  56. package/dist/fields/MediaField.js +1 -1
  57. package/dist/fields/MediaField.js.map +1 -1
  58. package/dist/fields/PropInput.js +1 -1
  59. package/dist/fields/PropInput.js.map +1 -1
  60. package/dist/hooks/useIdleLogout.d.ts +21 -6
  61. package/dist/hooks/useIdleLogout.d.ts.map +1 -1
  62. package/dist/hooks/useIdleLogout.js +41 -8
  63. package/dist/hooks/useIdleLogout.js.map +1 -1
  64. package/dist/layout/Header.js +1 -1
  65. package/dist/layout/Header.js.map +1 -1
  66. package/dist/router/index.d.ts +7 -0
  67. package/dist/router/index.d.ts.map +1 -1
  68. package/dist/router/index.js +23 -4
  69. package/dist/router/index.js.map +1 -1
  70. package/dist/views/DocumentEdit.js +1 -1
  71. package/dist/views/DocumentEdit.js.map +1 -1
  72. package/dist/views/FormEntries.d.ts.map +1 -1
  73. package/dist/views/FormEntries.js +18 -14
  74. package/dist/views/FormEntries.js.map +1 -1
  75. package/dist/views/FormSubmissions.d.ts.map +1 -1
  76. package/dist/views/FormSubmissions.js +18 -15
  77. package/dist/views/FormSubmissions.js.map +1 -1
  78. package/dist/views/Forms.js +5 -5
  79. package/dist/views/Forms.js.map +1 -1
  80. package/dist/views/MediaBrowser.d.ts +4 -1
  81. package/dist/views/MediaBrowser.d.ts.map +1 -1
  82. package/dist/views/MediaBrowser.js +28 -6
  83. package/dist/views/MediaBrowser.js.map +1 -1
  84. package/dist/views/MediaSeoAudit.d.ts +4 -1
  85. package/dist/views/MediaSeoAudit.d.ts.map +1 -1
  86. package/dist/views/MediaSeoAudit.js +12 -2
  87. package/dist/views/MediaSeoAudit.js.map +1 -1
  88. package/dist/views/Pages/PagesListView.js +14 -14
  89. package/dist/views/Pages/PagesListView.js.map +1 -1
  90. package/dist/views/Pages/PagesTreeTable.d.ts.map +1 -1
  91. package/dist/views/Pages/PagesTreeTable.js +3 -3
  92. package/dist/views/Pages/PagesTreeTable.js.map +1 -1
  93. package/dist/views/SEO.d.ts.map +1 -1
  94. package/dist/views/SEO.js +2 -1
  95. package/dist/views/SEO.js.map +1 -1
  96. package/dist/views/ScriptTags.d.ts.map +1 -1
  97. package/dist/views/ScriptTags.js +3 -3
  98. package/dist/views/ScriptTags.js.map +1 -1
  99. package/dist/views/Settings.d.ts +4 -1
  100. package/dist/views/Settings.d.ts.map +1 -1
  101. package/dist/views/Settings.js +4 -3
  102. package/dist/views/Settings.js.map +1 -1
  103. package/dist/views/Users.d.ts +4 -1
  104. package/dist/views/Users.d.ts.map +1 -1
  105. package/dist/views/Users.js +22 -2
  106. package/dist/views/Users.js.map +1 -1
  107. package/dist/views/seo/ContentTab.js +6 -6
  108. package/dist/views/seo/ContentTab.js.map +1 -1
  109. package/dist/views/seo/RedirectsTab.d.ts.map +1 -1
  110. package/dist/views/seo/RedirectsTab.js +23 -8
  111. package/dist/views/seo/RedirectsTab.js.map +1 -1
  112. package/dist/views/settings/SecurityTab.d.ts.map +1 -1
  113. package/dist/views/settings/SecurityTab.js +2 -1
  114. package/dist/views/settings/SecurityTab.js.map +1 -1
  115. package/package.json +2 -2
  116. package/src/AdminRoot.tsx +53 -12
  117. package/src/__tests__/hooks/useIdleLogout.test.tsx +103 -1
  118. package/src/__tests__/router/query-string.test.ts +54 -0
  119. package/src/__tests__/views/form-entries.render.test.tsx +16 -4
  120. package/src/__tests__/views/form-submissions.render.test.tsx +16 -4
  121. package/src/__tests__/views/media-seo-audit.render.test.tsx +34 -0
  122. package/src/__tests__/views/redirects-pane.render.test.tsx +156 -0
  123. package/src/__tests__/views/users-selected-deeplink.render.test.tsx +102 -0
  124. package/src/components/SEOPanel.tsx +9 -9
  125. package/src/components/collections/EntryDetailPane.tsx +1 -1
  126. package/src/components/forms/EntriesTable.tsx +12 -3
  127. package/src/components/forms/{EntryDetailDrawer.tsx → FormEntryDetailPane.tsx} +112 -102
  128. package/src/components/forms/FormSchemaPane.tsx +1 -1
  129. package/src/components/script-tags/ScriptTagEditorPane.tsx +1 -1
  130. package/src/components/seo/RedirectEditorPane.tsx +271 -0
  131. package/src/components/seo/SeoEditorPane.tsx +1 -1
  132. package/src/components/ui/DataTable.tsx +3 -3
  133. package/src/components/ui/Skeleton.tsx +8 -8
  134. package/src/components/ui/Toggle.tsx +1 -1
  135. package/src/fields/BlockBuilderField.tsx +1 -1
  136. package/src/fields/MediaField.tsx +1 -1
  137. package/src/fields/PropInput.tsx +1 -1
  138. package/src/hooks/useIdleLogout.ts +65 -13
  139. package/src/layout/Header.tsx +2 -2
  140. package/src/router/index.ts +25 -4
  141. package/src/styles/actuate-tokens.css +4 -0
  142. package/src/styles/theme.css +9 -3
  143. package/src/views/DocumentEdit.tsx +4 -4
  144. package/src/views/FormEntries.tsx +81 -66
  145. package/src/views/FormSubmissions.tsx +78 -64
  146. package/src/views/Forms.tsx +16 -16
  147. package/src/views/MediaBrowser.tsx +34 -6
  148. package/src/views/MediaSeoAudit.tsx +39 -2
  149. package/src/views/Pages/PagesListView.tsx +46 -46
  150. package/src/views/Pages/PagesTreeTable.tsx +11 -8
  151. package/src/views/SEO.tsx +2 -1
  152. package/src/views/ScriptTags.tsx +26 -36
  153. package/src/views/Settings.tsx +7 -2
  154. package/src/views/Users.tsx +23 -2
  155. package/src/views/seo/ContentTab.tsx +10 -10
  156. package/src/views/seo/RedirectsTab.tsx +145 -109
  157. package/src/views/settings/SecurityTab.tsx +2 -1
  158. package/dist/components/forms/EntryDetailDrawer.d.ts +0 -11
  159. package/dist/components/forms/EntryDetailDrawer.d.ts.map +0 -1
  160. package/dist/components/forms/EntryDetailDrawer.js +0 -145
  161. package/dist/components/forms/EntryDetailDrawer.js.map +0 -1
  162. package/dist/components/seo/RedirectEditorDrawer.d.ts +0 -9
  163. package/dist/components/seo/RedirectEditorDrawer.d.ts.map +0 -1
  164. package/dist/components/seo/RedirectEditorDrawer.js +0 -95
  165. package/dist/components/seo/RedirectEditorDrawer.js.map +0 -1
  166. package/src/components/seo/RedirectEditorDrawer.tsx +0 -231
@@ -435,7 +435,7 @@ function Section({
435
435
  <div className="overflow-hidden rounded-lg border border-[var(--border)]">
436
436
  <button
437
437
  onClick={() => onToggle(id)}
438
- className="flex w-full items-center justify-between px-4 py-2.5 text-left text-sm font-medium text-[var(--foreground)] transition-colors hover:bg-[var(--muted)]"
438
+ className="hover:bg-muted flex w-full items-center justify-between px-4 py-2.5 text-left text-sm font-medium text-[var(--foreground)] transition-colors"
439
439
  >
440
440
  <span className="flex items-center gap-2">
441
441
  {icon}
@@ -646,7 +646,7 @@ export function SEOPanel({
646
646
  expanded={expandedSections.includes('analysis')}
647
647
  onToggle={toggleSection}
648
648
  badge={
649
- <span className="ml-1 inline-flex items-center rounded-full bg-[var(--muted)] px-2 py-0.5 text-[10px] font-medium text-[var(--muted-foreground)]">
649
+ <span className="bg-muted ml-1 inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium text-[var(--muted-foreground)]">
650
650
  {goodCount}/{checks.length}
651
651
  </span>
652
652
  }
@@ -673,26 +673,26 @@ export function SEOPanel({
673
673
  onToggle={toggleSection}
674
674
  >
675
675
  <div className="grid grid-cols-2 gap-3">
676
- <div className="rounded-lg bg-[var(--muted)] p-2.5">
676
+ <div className="bg-muted rounded-lg p-2.5">
677
677
  <p className="text-xs text-[var(--muted-foreground)]">Flesch Score</p>
678
678
  <p className={`text-lg font-medium ${fleschColor}`}>{readability.fleschScore}</p>
679
679
  <p className={`text-[10px] ${fleschColor}`}>{fleschLabel}</p>
680
680
  </div>
681
- <div className="rounded-lg bg-[var(--muted)] p-2.5">
681
+ <div className="bg-muted rounded-lg p-2.5">
682
682
  <p className="text-xs text-[var(--muted-foreground)]">Word Count</p>
683
683
  <p className="text-lg font-medium text-[var(--foreground)]">
684
684
  {readability.wordCount}
685
685
  </p>
686
686
  <p className="text-[10px] text-[var(--muted-foreground)]">words</p>
687
687
  </div>
688
- <div className="rounded-lg bg-[var(--muted)] p-2.5">
688
+ <div className="bg-muted rounded-lg p-2.5">
689
689
  <p className="text-xs text-[var(--muted-foreground)]">Avg. Sentence</p>
690
690
  <p className="text-lg font-medium text-[var(--foreground)]">
691
691
  {readability.avgSentenceLength.toFixed(1)}
692
692
  </p>
693
693
  <p className="text-[10px] text-[var(--muted-foreground)]">words/sentence</p>
694
694
  </div>
695
- <div className="rounded-lg bg-[var(--muted)] p-2.5">
695
+ <div className="bg-muted rounded-lg p-2.5">
696
696
  <p className="text-xs text-[var(--muted-foreground)]">Reading Time</p>
697
697
  <p className="text-lg font-medium text-[var(--foreground)]">
698
698
  {readability.readingTime}
@@ -907,9 +907,9 @@ export function SEOPanel({
907
907
  <p className="mb-2 text-xs font-medium text-[var(--muted-foreground)]">
908
908
  Social Preview
909
909
  </p>
910
- <div className="overflow-hidden rounded-lg border border-[var(--border)] bg-[var(--muted)]">
910
+ <div className="bg-muted overflow-hidden rounded-lg border border-[var(--border)]">
911
911
  {seoData.ogImage ? (
912
- <div className="flex aspect-video items-center justify-center overflow-hidden bg-[var(--muted)]">
912
+ <div className="bg-muted flex aspect-video items-center justify-center overflow-hidden">
913
913
  <img
914
914
  src={seoData.ogImage}
915
915
  alt="OG preview"
@@ -917,7 +917,7 @@ export function SEOPanel({
917
917
  />
918
918
  </div>
919
919
  ) : (
920
- <div className="flex aspect-video items-center justify-center bg-[var(--muted)] text-sm text-[var(--muted-foreground)]">
920
+ <div className="bg-muted flex aspect-video items-center justify-center text-sm text-[var(--muted-foreground)]">
921
921
  No OG image set
922
922
  </div>
923
923
  )}
@@ -171,7 +171,7 @@ export function EntryDetailPane({
171
171
  <button
172
172
  type="button"
173
173
  onClick={onOpenEditor}
174
- className="flex w-full items-center justify-center gap-1.5 rounded-[7px] bg-[var(--acc)] px-3 py-2 text-[13px] font-semibold text-white transition-colors duration-[var(--motion-fast)] hover:opacity-90 focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
174
+ className="bg-primary text-primary-foreground flex w-full items-center justify-center gap-1.5 rounded-[7px] px-3 py-2 text-[13px] font-semibold transition-colors duration-[var(--motion-fast)] hover:opacity-90 focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
175
175
  >
176
176
  <ExternalLink size={14} aria-hidden />
177
177
  Open full editor
@@ -32,6 +32,8 @@ export interface EntriesTableProps {
32
32
  onToggleSelect: (id: string) => void
33
33
  onOpen: (id: string) => void
34
34
  onToggleStar: (entry: FormSubmission) => void
35
+ /** Entry currently open in the inline detail pane (drives aria-expanded). */
36
+ activeId?: string | null
35
37
  /** Show a Form column (used by the cross-form All Entries inbox). */
36
38
  showForm?: boolean
37
39
  /** Navigate to a form's submissions when its name is clicked (All Entries). */
@@ -50,6 +52,7 @@ export function EntriesTable({
50
52
  onToggleSelect,
51
53
  onOpen,
52
54
  onToggleStar,
55
+ activeId = null,
53
56
  showForm = false,
54
57
  onOpenForm,
55
58
  ariaLabel = 'Submissions',
@@ -129,11 +132,17 @@ export function EntriesTable({
129
132
  aria-label="Unread"
130
133
  />
131
134
  )}
132
- <span
133
- className={`text-foreground truncate ${entry.unread ? 'font-medium' : 'font-medium'}`}
135
+ <button
136
+ type="button"
137
+ onClick={(e) => {
138
+ e.stopPropagation()
139
+ onOpen(entry.id)
140
+ }}
141
+ aria-expanded={entry.id === activeId}
142
+ className="text-foreground focus-visible:ring-ring truncate rounded text-left font-medium hover:underline focus-visible:ring-2 focus-visible:outline-none"
134
143
  >
135
144
  {name}
136
- </span>
145
+ </button>
137
146
  </div>
138
147
  {entry.senderEmail && (
139
148
  <span className="text-muted-foreground block truncate text-xs">
@@ -1,10 +1,11 @@
1
1
  'use client'
2
2
 
3
3
  /**
4
- * Entry detail drawershows a single submission rendered against the schema
5
- * version it was captured with, plus sender, attribution, and per-entry
6
- * actions (star, archive, mark unread, delete). Opening an unread entry marks
7
- * it read. Token-only styling; built on the shared `Drawer`.
4
+ * Form entry detail paneinline InspectorPane replacement for the old
5
+ * `EntryDetailDrawer` overlay. Shows a single submission rendered against the
6
+ * schema version it was captured with, plus sender, attribution, and
7
+ * per-entry actions (star, archive, mark unread, delete). Opening an unread
8
+ * entry marks it read. Rendered beside the entries table (400px split pane).
8
9
  */
9
10
  import { useCallback, useEffect, useState } from 'react'
10
11
  import { Archive, ArchiveRestore, Loader2, Mail, Star, Trash2 } from 'lucide-react'
@@ -19,8 +20,16 @@ import {
19
20
  } from '../../lib/forms-service.js'
20
21
  import { emitFormsChanged } from '../../lib/forms-events.js'
21
22
  import { ConfirmDialog } from '../ui/ConfirmDialog.js'
22
- import { Drawer } from './Drawer.js'
23
- import { FormsErrorState, btnGhostIcon } from './primitives.js'
23
+ import {
24
+ InspectorPaneBody,
25
+ InspectorPaneFooter,
26
+ InspectorPaneHeader,
27
+ InspectorPaneSection,
28
+ } from '../ui/InspectorPane.js'
29
+ import { FormsErrorState } from './primitives.js'
30
+
31
+ const iconBtn =
32
+ 'flex h-7 w-7 shrink-0 items-center justify-center rounded-[6px] text-[var(--muted)] transition-colors duration-[var(--motion-fast)] hover:bg-[var(--bg)] hover:text-[var(--txt)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none disabled:opacity-60'
24
33
 
25
34
  function formatDateTime(iso: string | null | undefined): string {
26
35
  if (!iso) return '—'
@@ -59,25 +68,23 @@ function attributionRows(entry: FormSubmission): AttrRow[] {
59
68
  ].filter((r) => r.value)
60
69
  }
61
70
 
62
- export interface EntryDetailDrawerProps {
63
- entryId: string | null
64
- open: boolean
65
- onOpenChange: (open: boolean) => void
71
+ export interface FormEntryDetailPaneProps {
72
+ entryId: string
73
+ onClose: () => void
66
74
  /** Called after a mutation (star/archive/read/delete) so the list refreshes. */
67
75
  onChanged?: () => void
68
76
  /** Called specifically after a delete so the caller can drop the row/selection. */
69
77
  onDeleted?: (id: string) => void
70
78
  }
71
79
 
72
- export function EntryDetailDrawer({
80
+ export function FormEntryDetailPane({
73
81
  entryId,
74
- open,
75
- onOpenChange,
82
+ onClose,
76
83
  onChanged,
77
84
  onDeleted,
78
- }: EntryDetailDrawerProps) {
85
+ }: FormEntryDetailPaneProps) {
79
86
  const [detail, setDetail] = useState<EntryDetail | null>(null)
80
- const [loading, setLoading] = useState(false)
87
+ const [loading, setLoading] = useState(true)
81
88
  const [error, setError] = useState<string | null>(null)
82
89
  const [busy, setBusy] = useState(false)
83
90
  const [confirmDelete, setConfirmDelete] = useState(false)
@@ -87,9 +94,8 @@ export function EntryDetailDrawer({
87
94
  onChanged?.()
88
95
  }, [onChanged])
89
96
 
90
- // Load the entry (and its schema) when the drawer opens, marking it read.
97
+ // Load the entry (and its schema) when the pane opens, marking it read.
91
98
  useEffect(() => {
92
- if (!open || !entryId) return
93
99
  let cancelled = false
94
100
  setLoading(true)
95
101
  setError(null)
@@ -117,11 +123,10 @@ export function EntryDetailDrawer({
117
123
  return () => {
118
124
  cancelled = true
119
125
  }
120
- }, [open, entryId, notifyChanged])
126
+ }, [entryId, notifyChanged])
121
127
 
122
128
  const mutate = useCallback(
123
129
  async (patch: { unread?: boolean; starred?: boolean; archived?: boolean }) => {
124
- if (!entryId) return
125
130
  setBusy(true)
126
131
  const res = await updateEntry(entryId, patch)
127
132
  setBusy(false)
@@ -136,7 +141,6 @@ export function EntryDetailDrawer({
136
141
  )
137
142
 
138
143
  const handleDelete = useCallback(async () => {
139
- if (!entryId) return
140
144
  setBusy(true)
141
145
  const res = await deleteEntry(entryId)
142
146
  setBusy(false)
@@ -147,8 +151,7 @@ export function EntryDetailDrawer({
147
151
  toast.success('Submission deleted')
148
152
  notifyChanged()
149
153
  onDeleted?.(entryId)
150
- onOpenChange(false)
151
- }, [entryId, notifyChanged, onDeleted, onOpenChange])
154
+ }, [entryId, notifyChanged, onDeleted])
152
155
 
153
156
  const entry = detail?.entry
154
157
  const senderName = entry?.senderName || 'Anonymous'
@@ -156,114 +159,122 @@ export function EntryDetailDrawer({
156
159
 
157
160
  return (
158
161
  <>
159
- <Drawer
160
- open={open}
161
- onOpenChange={onOpenChange}
162
- title={entry ? `Entry${entry.entryNumber ? ` #${entry.entryNumber}` : ''}` : 'Entry'}
163
- description={entry ? formatDateTime(entry.submittedAt) : undefined}
164
- headerExtra={
165
- entry ? (
166
- <>
162
+ <InspectorPaneHeader onClose={onClose} closeLabel="Close entry details">
163
+ <div className="flex items-start justify-between gap-2">
164
+ <div className="min-w-0">
165
+ <div className="truncate text-[13.5px] font-semibold text-[var(--txt)]">
166
+ {entry ? `Entry${entry.entryNumber ? ` #${entry.entryNumber}` : ''}` : 'Entry'}
167
+ </div>
168
+ {entry && (
169
+ <div className="mt-0.5 truncate text-[11.5px] text-[var(--muted)]">
170
+ {formatDateTime(entry.submittedAt)}
171
+ </div>
172
+ )}
173
+ </div>
174
+ {entry && (
175
+ <div className="flex shrink-0 gap-1">
167
176
  <button
168
177
  type="button"
169
- className={btnGhostIcon}
178
+ className={iconBtn}
170
179
  aria-pressed={entry.starred}
171
180
  aria-label={entry.starred ? 'Unstar entry' : 'Star entry'}
172
181
  disabled={busy}
173
182
  onClick={() => void mutate({ starred: !entry.starred })}
174
183
  >
175
184
  <Star
176
- size={18}
185
+ size={16}
177
186
  aria-hidden
178
- className={entry.starred ? 'fill-warning text-warning' : ''}
187
+ className={entry.starred ? 'fill-[var(--warn)] text-[var(--warn)]' : ''}
179
188
  />
180
189
  </button>
181
190
  <button
182
191
  type="button"
183
- className={btnGhostIcon}
192
+ className={iconBtn}
184
193
  aria-pressed={entry.archived}
185
194
  aria-label={entry.archived ? 'Unarchive entry' : 'Archive entry'}
186
195
  disabled={busy}
187
196
  onClick={() => void mutate({ archived: !entry.archived })}
188
197
  >
189
198
  {entry.archived ? (
190
- <ArchiveRestore size={18} aria-hidden />
199
+ <ArchiveRestore size={16} aria-hidden />
191
200
  ) : (
192
- <Archive size={18} aria-hidden />
201
+ <Archive size={16} aria-hidden />
193
202
  )}
194
203
  </button>
195
- </>
196
- ) : undefined
197
- }
198
- footer={
199
- entry ? (
200
- <>
201
- <button
202
- type="button"
203
- className="text-destructive hover:bg-destructive/10 focus-visible:ring-ring inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:opacity-60"
204
- disabled={busy}
205
- onClick={() => setConfirmDelete(true)}
206
- >
207
- <Trash2 size={16} aria-hidden />
208
- Delete
209
- </button>
210
- <button
211
- type="button"
212
- className="border-border text-foreground hover:bg-accent focus-visible:ring-ring inline-flex items-center gap-2 rounded-md border px-3 py-2 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:opacity-60"
213
- disabled={busy}
214
- onClick={() => void mutate({ unread: !entry.unread })}
215
- >
216
- <Mail size={16} aria-hidden />
217
- Mark {entry.unread ? 'read' : 'unread'}
218
- </button>
219
- </>
220
- ) : undefined
221
- }
222
- >
204
+ </div>
205
+ )}
206
+ </div>
207
+ </InspectorPaneHeader>
208
+
209
+ <InspectorPaneBody>
223
210
  {loading ? (
224
211
  <div className="flex h-40 items-center justify-center" role="status" aria-live="polite">
225
- <Loader2 className="text-muted-foreground h-5 w-5 animate-spin" aria-hidden />
212
+ <Loader2 className="h-5 w-5 animate-spin text-[var(--muted)]" aria-hidden />
226
213
  <span className="sr-only">Loading entry…</span>
227
214
  </div>
228
215
  ) : error ? (
229
- <FormsErrorState message={error} />
216
+ <div className="px-4 py-3.5">
217
+ <FormsErrorState message={error} />
218
+ </div>
230
219
  ) : entry ? (
231
- <div className="space-y-6">
232
- <section className="space-y-1">
233
- <h3 className="text-foreground text-sm font-medium">{senderName}</h3>
220
+ <>
221
+ <InspectorPaneSection title="Sender">
222
+ <div className="text-[12.5px] font-medium text-[var(--txt)]">{senderName}</div>
234
223
  {entry.senderEmail && (
235
224
  <a
236
225
  href={`mailto:${entry.senderEmail}`}
237
- className="text-primary block text-sm hover:underline"
226
+ className="block truncate text-[12px] text-[var(--acc)] hover:underline"
238
227
  >
239
228
  {entry.senderEmail}
240
229
  </a>
241
230
  )}
242
231
  {entry.senderPhone && (
243
- <span className="text-muted-foreground block text-sm">{entry.senderPhone}</span>
232
+ <div className="text-[12px] text-[var(--sub)]">{entry.senderPhone}</div>
244
233
  )}
245
- </section>
234
+ </InspectorPaneSection>
246
235
 
247
236
  <FieldValues entry={entry} fields={detail.fields} />
248
237
 
249
238
  {attrRows.length > 0 && (
250
- <section>
251
- <h4 className="text-muted-foreground mb-2 text-xs font-medium tracking-wide uppercase">
252
- Attribution
253
- </h4>
254
- <dl className="divide-border divide-y text-sm">
239
+ <InspectorPaneSection title="Attribution">
240
+ <dl>
255
241
  {attrRows.map((row) => (
256
- <div key={row.label} className="flex justify-between gap-4 py-1.5">
257
- <dt className="text-muted-foreground shrink-0">{row.label}</dt>
258
- <dd className="text-foreground min-w-0 truncate text-right">{row.value}</dd>
242
+ <div key={row.label} className="flex justify-between gap-4 py-1 text-[12px]">
243
+ <dt className="shrink-0 text-[var(--muted)]">{row.label}</dt>
244
+ <dd className="min-w-0 truncate text-right text-[var(--sub)]">{row.value}</dd>
259
245
  </div>
260
246
  ))}
261
247
  </dl>
262
- </section>
248
+ </InspectorPaneSection>
263
249
  )}
264
- </div>
250
+ </>
265
251
  ) : null}
266
- </Drawer>
252
+ </InspectorPaneBody>
253
+
254
+ {entry && (
255
+ <InspectorPaneFooter>
256
+ <div className="flex items-center gap-2">
257
+ <button
258
+ type="button"
259
+ className="flex min-w-0 flex-1 items-center justify-center gap-1.5 rounded-[7px] border border-[var(--bdr)] px-3 py-2 text-[12.5px] font-medium text-[var(--txt)] transition-colors duration-[var(--motion-fast)] hover:bg-[var(--bg)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none disabled:opacity-60"
260
+ disabled={busy}
261
+ onClick={() => void mutate({ unread: !entry.unread })}
262
+ >
263
+ <Mail size={14} aria-hidden />
264
+ Mark {entry.unread ? 'read' : 'unread'}
265
+ </button>
266
+ <button
267
+ type="button"
268
+ className="flex shrink-0 items-center justify-center gap-1.5 rounded-[7px] border border-[var(--bdr)] px-3 py-2 text-[12.5px] font-medium text-[var(--err)] transition-colors duration-[var(--motion-fast)] hover:bg-[var(--err-l)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none disabled:opacity-60"
269
+ disabled={busy}
270
+ onClick={() => setConfirmDelete(true)}
271
+ >
272
+ <Trash2 size={14} aria-hidden />
273
+ Delete
274
+ </button>
275
+ </div>
276
+ </InspectorPaneFooter>
277
+ )}
267
278
 
268
279
  <ConfirmDialog
269
280
  open={confirmDelete}
@@ -288,25 +299,24 @@ function FieldValues({ entry, fields }: { entry: FormSubmission; fields: FormFie
288
299
  ? inputFields.map((f) => ({ key: f.key, label: f.label }))
289
300
  : Object.keys(entry.data ?? {}).map((k) => ({ key: k, label: k }))
290
301
 
291
- if (rows.length === 0) {
292
- return <p className="text-muted-foreground text-sm">No field data captured.</p>
293
- }
294
-
295
302
  return (
296
- <section>
297
- <h4 className="text-muted-foreground mb-2 text-xs font-medium tracking-wide uppercase">
298
- Submission
299
- </h4>
300
- <dl className="space-y-3">
301
- {rows.map((row) => (
302
- <div key={row.key}>
303
- <dt className="text-muted-foreground text-xs">{row.label}</dt>
304
- <dd className="text-foreground mt-0.5 text-sm wrap-break-word whitespace-pre-wrap">
305
- {displayValue(entry.data?.[row.key])}
306
- </dd>
307
- </div>
308
- ))}
309
- </dl>
310
- </section>
303
+ <InspectorPaneSection title="Submission">
304
+ {rows.length === 0 ? (
305
+ <p className="text-[12px] text-[var(--muted)]">No field data captured.</p>
306
+ ) : (
307
+ <dl>
308
+ {rows.map((row) => (
309
+ <div key={row.key} className="mb-2.5 last:mb-0">
310
+ <dt className="text-[10.5px] font-semibold tracking-[0.5px] text-[var(--muted)] uppercase">
311
+ {row.label}
312
+ </dt>
313
+ <dd className="mt-0.5 text-[12.5px] break-words whitespace-pre-wrap text-[var(--txt)]">
314
+ {displayValue(entry.data?.[row.key])}
315
+ </dd>
316
+ </div>
317
+ ))}
318
+ </dl>
319
+ )}
320
+ </InspectorPaneSection>
311
321
  )
312
322
  }
@@ -122,7 +122,7 @@ export function FormSchemaPane({
122
122
  <Tabs.Trigger
123
123
  key={t.id}
124
124
  value={t.id}
125
- className="text-muted-foreground hover:text-foreground data-[state=active]:text-foreground data-[state=active]:border-primary focus-visible:ring-ring -mb-px border-b-2 border-transparent px-2.5 py-2 text-[12.5px] font-medium whitespace-nowrap transition-colors focus-visible:ring-2 focus-visible:outline-none"
125
+ className="-mb-px border-b-2 border-transparent px-2.5 py-2 text-[12.5px] font-medium whitespace-nowrap text-[var(--sub)] transition-colors hover:text-[var(--txt)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none data-[state=active]:border-[var(--acc)] data-[state=active]:text-[var(--txt)]"
126
126
  >
127
127
  {t.label}
128
128
  </Tabs.Trigger>
@@ -434,7 +434,7 @@ export function ScriptTagEditorPane({ tag, onClose, onChanged }: ScriptTagEditor
434
434
  type="button"
435
435
  onClick={() => void handleSave()}
436
436
  disabled={saving}
437
- className="w-full rounded-[7px] bg-[var(--acc)] px-3 py-2 text-[13px] font-semibold text-white transition-colors duration-[var(--motion-fast)] hover:opacity-90 focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
437
+ className="bg-primary text-primary-foreground w-full rounded-[7px] px-3 py-2 text-[13px] font-semibold transition-colors duration-[var(--motion-fast)] hover:opacity-90 focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
438
438
  >
439
439
  {saving ? 'Saving…' : 'Save Script'}
440
440
  </button>