@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
@@ -412,8 +412,8 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
412
412
  the design's `.page-hdr` / `.hdr-r` layout. */}
413
413
  <div className="flex flex-wrap items-start justify-between gap-3">
414
414
  <div className="flex flex-col gap-1">
415
- <h1 className="text-foreground">Pages</h1>
416
- <p className="text-muted-foreground text-sm">
415
+ <h1 className="text-[var(--txt)]">Pages</h1>
416
+ <p className="text-sm text-[var(--sub)]">
417
417
  {treeMode
418
418
  ? `${grandTotal} page${grandTotal === 1 ? '' : 's'} total`
419
419
  : `${grandTotal} page${grandTotal === 1 ? '' : 's'} · organized by tags`}
@@ -422,7 +422,7 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
422
422
  <button
423
423
  type="button"
424
424
  onClick={() => onNavigate('/pages/new')}
425
- className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-medium shadow-sm transition-colors focus:outline-none focus-visible:ring-2"
425
+ 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 focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)]"
426
426
  >
427
427
  <Plus className="h-3.5 w-3.5" aria-hidden />
428
428
  New page
@@ -433,7 +433,7 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
433
433
  <div className="flex flex-wrap items-center gap-2">
434
434
  <div className="relative max-w-sm flex-1">
435
435
  <Search
436
- className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2"
436
+ className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-[var(--sub)]"
437
437
  aria-hidden
438
438
  />
439
439
  <input
@@ -442,7 +442,7 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
442
442
  onChange={(e) => setSearch(e.target.value)}
443
443
  aria-label="Search pages"
444
444
  placeholder="Search pages…"
445
- className="border-border bg-card text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-primary/30 w-full rounded-lg border py-2 pr-3 pl-9 text-sm shadow-sm focus:ring-2 focus:outline-none"
445
+ className="focus:border-primary focus:ring-primary/30 w-full rounded-lg border border-[var(--bdr)] bg-[var(--card)] py-2 pr-3 pl-9 text-sm text-[var(--txt)] shadow-sm placeholder:text-[var(--sub)] focus:ring-2 focus:outline-none"
446
446
  />
447
447
  </div>
448
448
 
@@ -479,7 +479,7 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
479
479
 
480
480
  <ViewModeToggle mode={viewMode} onChange={changeViewMode} />
481
481
 
482
- <span className="text-muted-foreground ml-auto text-sm tabular-nums">
482
+ <span className="ml-auto text-sm text-[var(--sub)] tabular-nums">
483
483
  {treeMode
484
484
  ? treeFiltersActive
485
485
  ? `${treeRows.length} result${treeRows.length === 1 ? '' : 's'}`
@@ -492,7 +492,7 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
492
492
  with the page hierarchy) */}
493
493
  {!treeMode && (
494
494
  <div className="flex flex-wrap items-center gap-2">
495
- <span className="text-muted-foreground text-[10px] font-medium tracking-wider uppercase">
495
+ <span className="text-[10px] font-medium tracking-wider text-[var(--sub)] uppercase">
496
496
  Tags
497
497
  </span>
498
498
  {tags.map((t) => (
@@ -509,7 +509,7 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
509
509
  <button
510
510
  type="button"
511
511
  onClick={() => setSelectedTags(new Set())}
512
- className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1 text-xs"
512
+ className="inline-flex items-center gap-1 text-xs text-[var(--sub)] hover:text-[var(--txt)]"
513
513
  >
514
514
  <X className="h-3 w-3" aria-hidden />
515
515
  Clear tags
@@ -543,7 +543,7 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
543
543
  )}
544
544
 
545
545
  {/* Table card */}
546
- <div className="bg-card border-border overflow-hidden rounded-xl border shadow-sm">
546
+ <div className="overflow-hidden rounded-xl border border-[var(--bdr)] bg-[var(--card)] shadow-sm">
547
547
  {treeMode ? (
548
548
  <PagesTreeTable
549
549
  rows={treeRows}
@@ -579,15 +579,15 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
579
579
  <col style={{ width: '80px' }} />
580
580
  <col style={{ width: '110px' }} />
581
581
  </colgroup>
582
- <thead className="border-border bg-muted/40 border-b">
583
- <tr className="text-muted-foreground text-xs">
582
+ <thead className="bg-muted/40 border-b border-[var(--bdr)]">
583
+ <tr className="text-xs text-[var(--sub)]">
584
584
  <th scope="col" className="px-3 py-2.5 text-left">
585
585
  <input
586
586
  type="checkbox"
587
587
  aria-label="Select all pages on this page"
588
588
  checked={allOnPageSelected}
589
589
  onChange={(e) => toggleAllOnPage(e.target.checked)}
590
- className="border-border accent-primary rounded"
590
+ className="accent-primary rounded border-[var(--bdr)]"
591
591
  />
592
592
  </th>
593
593
  <th
@@ -668,7 +668,7 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
668
668
  {!loading && error && (
669
669
  <tr>
670
670
  <td colSpan={8} className="px-3 py-12 text-center">
671
- <div className="text-foreground mb-2 text-sm font-medium">{error}</div>
671
+ <div className="mb-2 text-sm font-medium text-[var(--txt)]">{error}</div>
672
672
  <button
673
673
  onClick={() => setRefreshToken((t) => t + 1)}
674
674
  className="text-primary text-sm hover:underline"
@@ -779,8 +779,8 @@ function SortHeader({
779
779
  <button
780
780
  type="button"
781
781
  onClick={() => onSort(field)}
782
- className={`hover:text-foreground inline-flex items-center gap-1 text-xs font-medium tracking-wider uppercase transition-colors ${
783
- active ? 'text-foreground' : 'text-muted-foreground'
782
+ className={`inline-flex items-center gap-1 text-xs font-medium tracking-wider uppercase transition-colors hover:text-[var(--txt)] ${
783
+ active ? 'text-[var(--txt)]' : 'text-[var(--sub)]'
784
784
  }`}
785
785
  >
786
786
  <span>{label}</span>
@@ -849,12 +849,12 @@ function FilterSelect({
849
849
  options: { value: string; label: string }[]
850
850
  }) {
851
851
  return (
852
- <label className="text-foreground inline-flex items-center gap-1.5 text-sm">
853
- <span className="text-muted-foreground">{label}:</span>
852
+ <label className="inline-flex items-center gap-1.5 text-sm text-[var(--txt)]">
853
+ <span className="text-[var(--sub)]">{label}:</span>
854
854
  <select
855
855
  value={value}
856
856
  onChange={(e) => onChange(e.target.value)}
857
- className="border-border bg-card text-foreground focus:border-primary focus:ring-primary/30 rounded-lg border px-2.5 py-1.5 text-sm shadow-sm focus:ring-2 focus:outline-none"
857
+ className="focus:border-primary focus:ring-primary/30 rounded-lg border border-[var(--bdr)] bg-[var(--card)] px-2.5 py-1.5 text-sm text-[var(--txt)] shadow-sm focus:ring-2 focus:outline-none"
858
858
  >
859
859
  {options.map((o) => (
860
860
  <option key={o.value} value={o.value}>
@@ -882,12 +882,12 @@ function GroupBlock({
882
882
  return (
883
883
  <>
884
884
  {showHeader && (
885
- <tr className="bg-muted/30 border-border border-b">
885
+ <tr className="bg-muted/30 border-b border-[var(--bdr)]">
886
886
  <td colSpan={8} className="px-3 py-2">
887
- <span className="text-foreground inline-flex items-center gap-2 text-xs font-medium tracking-wider uppercase">
887
+ <span className="inline-flex items-center gap-2 text-xs font-medium tracking-wider text-[var(--txt)] uppercase">
888
888
  {color && <span aria-hidden className={`h-2 w-2 rounded-full ${dotFor(color)}`} />}
889
889
  {label}
890
- <span className="text-muted-foreground tabular-nums">{count}</span>
890
+ <span className="text-[var(--sub)] tabular-nums">{count}</span>
891
891
  </span>
892
892
  </td>
893
893
  </tr>
@@ -934,19 +934,19 @@ function BulkActionBar({
934
934
  }) {
935
935
  return (
936
936
  <div className="border-primary/30 bg-primary/5 flex flex-wrap items-center gap-2 rounded-lg border px-4 py-2">
937
- <span className="text-foreground text-sm font-medium">{count} selected</span>
937
+ <span className="text-sm font-medium text-[var(--txt)]">{count} selected</span>
938
938
  <div className="bg-border h-4 w-px" aria-hidden />
939
939
  <button
940
940
  type="button"
941
941
  onClick={onPublish}
942
- className="text-foreground hover:bg-accent rounded-md px-2 py-1 text-sm transition-colors"
942
+ className="rounded-md px-2 py-1 text-sm text-[var(--txt)] transition-colors hover:bg-[var(--acc-l)]"
943
943
  >
944
944
  Publish
945
945
  </button>
946
946
  <button
947
947
  type="button"
948
948
  onClick={onDraft}
949
- className="text-foreground hover:bg-accent rounded-md px-2 py-1 text-sm transition-colors"
949
+ className="rounded-md px-2 py-1 text-sm text-[var(--txt)] transition-colors hover:bg-[var(--acc-l)]"
950
950
  >
951
951
  Move to draft
952
952
  </button>
@@ -965,7 +965,7 @@ function BulkActionBar({
965
965
  <button
966
966
  type="button"
967
967
  onClick={onClear}
968
- className="text-muted-foreground hover:text-foreground ml-auto inline-flex items-center gap-1 rounded-md px-2 py-1 text-sm transition-colors"
968
+ className="ml-auto inline-flex items-center gap-1 rounded-md px-2 py-1 text-sm text-[var(--sub)] transition-colors hover:text-[var(--txt)]"
969
969
  >
970
970
  <X className="h-3.5 w-3.5" aria-hidden />
971
971
  Clear
@@ -988,7 +988,7 @@ function TagMenu({
988
988
  <DropdownMenu.Trigger asChild>
989
989
  <button
990
990
  type="button"
991
- className="text-foreground hover:bg-accent inline-flex items-center gap-1 rounded-md px-2 py-1 text-sm transition-colors"
991
+ className="inline-flex items-center gap-1 rounded-md px-2 py-1 text-sm text-[var(--txt)] transition-colors hover:bg-[var(--acc-l)]"
992
992
  >
993
993
  {label}
994
994
  <ChevronDown className="h-3 w-3" aria-hidden />
@@ -998,16 +998,16 @@ function TagMenu({
998
998
  <DropdownMenu.Content
999
999
  align="start"
1000
1000
  sideOffset={4}
1001
- className="border-border bg-popover z-50 max-h-64 w-44 overflow-auto rounded-lg border p-1 shadow-md"
1001
+ className="bg-popover z-50 max-h-64 w-44 overflow-auto rounded-lg border border-[var(--bdr)] p-1 shadow-md"
1002
1002
  >
1003
1003
  {tags.length === 0 ? (
1004
- <div className="text-muted-foreground px-2 py-1.5 text-xs">No tags yet</div>
1004
+ <div className="px-2 py-1.5 text-xs text-[var(--sub)]">No tags yet</div>
1005
1005
  ) : (
1006
1006
  tags.map((t) => (
1007
1007
  <DropdownMenu.Item
1008
1008
  key={t.slug}
1009
1009
  onSelect={() => onPick(t.slug)}
1010
- className="text-foreground focus:bg-accent flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none"
1010
+ className="focus:bg-accent flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm text-[var(--txt)] outline-none"
1011
1011
  >
1012
1012
  <span aria-hidden className={`h-2 w-2 rounded-full ${dotFor(t.color ?? 'gray')}`} />
1013
1013
  {t.name}
@@ -1039,8 +1039,8 @@ function PageRow({
1039
1039
  }) {
1040
1040
  return (
1041
1041
  <tr
1042
- className={`border-border hover:bg-muted/30 group border-b transition-colors last:border-b-0 ${
1043
- selected ? 'bg-primary/5' : ''
1042
+ className={`group border-b border-[var(--bdr)] transition-colors last:border-b-0 hover:bg-[var(--acc-l)] ${
1043
+ selected ? 'bg-[var(--acc-l)]' : ''
1044
1044
  }`}
1045
1045
  >
1046
1046
  <td className="px-3 py-3" onClick={(e) => e.stopPropagation()}>
@@ -1049,17 +1049,17 @@ function PageRow({
1049
1049
  aria-label={`Select "${page.title}"`}
1050
1050
  checked={selected}
1051
1051
  onChange={onSelect}
1052
- className="border-border accent-primary rounded"
1052
+ className="accent-primary rounded border-[var(--bdr)]"
1053
1053
  />
1054
1054
  </td>
1055
1055
  <td className="px-3 py-3">
1056
1056
  <button type="button" onClick={onOpen} className="flex w-full items-center gap-3 text-left">
1057
1057
  <span aria-hidden className="bg-muted block h-9 w-9 shrink-0 rounded-md" />
1058
1058
  <span className="min-w-0">
1059
- <span className="text-foreground group-hover:text-primary block truncate text-sm font-medium">
1059
+ <span className="block truncate text-sm font-medium text-[var(--txt)] group-hover:text-[var(--acc)]">
1060
1060
  {page.title}
1061
1061
  </span>
1062
- <span className="text-muted-foreground block truncate text-xs">{page.path}</span>
1062
+ <span className="block truncate text-xs text-[var(--sub)]">{page.path}</span>
1063
1063
  </span>
1064
1064
  </button>
1065
1065
  </td>
@@ -1070,17 +1070,17 @@ function PageRow({
1070
1070
  <div className="flex items-center gap-2">
1071
1071
  <span
1072
1072
  aria-hidden
1073
- className="bg-muted text-muted-foreground inline-flex h-7 w-7 items-center justify-center rounded-full text-[11px] font-medium"
1073
+ className="bg-muted inline-flex h-7 w-7 items-center justify-center rounded-full text-[11px] font-medium text-[var(--sub)]"
1074
1074
  >
1075
1075
  {page.authorInitials}
1076
1076
  </span>
1077
- <span className="text-foreground truncate text-sm">{page.authorName}</span>
1077
+ <span className="truncate text-sm text-[var(--txt)]">{page.authorName}</span>
1078
1078
  </div>
1079
1079
  </td>
1080
1080
  <td className="px-3 py-3">
1081
1081
  <PostStatusBadge status={page.status} />
1082
1082
  </td>
1083
- <td className="text-muted-foreground px-3 py-3 text-sm tabular-nums">
1083
+ <td className="px-3 py-3 text-sm text-[var(--sub)] tabular-nums">
1084
1084
  {formatDateShort(page.lastModified)}
1085
1085
  </td>
1086
1086
  <td className="px-3 py-3">
@@ -1117,7 +1117,7 @@ function LoadingRows() {
1117
1117
  return (
1118
1118
  <>
1119
1119
  {Array.from({ length: 8 }).map((_, i) => (
1120
- <tr key={i} className="border-border border-b last:border-b-0">
1120
+ <tr key={i} className="border-b border-[var(--bdr)] last:border-b-0">
1121
1121
  {Array.from({ length: 8 }).map((__, j) => (
1122
1122
  <td key={j} className="px-3 py-3">
1123
1123
  <div className="bg-muted h-4 w-full animate-pulse rounded" />
@@ -1141,10 +1141,10 @@ function EmptyRow({
1141
1141
  return (
1142
1142
  <tr>
1143
1143
  <td colSpan={8} className="px-3 py-16 text-center">
1144
- <div className="text-foreground mb-2 text-sm font-medium">
1144
+ <div className="mb-2 text-sm font-medium text-[var(--txt)]">
1145
1145
  {filtersActive ? 'No pages match your filters' : 'No pages yet'}
1146
1146
  </div>
1147
- <div className="text-muted-foreground mb-4 text-sm">
1147
+ <div className="mb-4 text-sm text-[var(--sub)]">
1148
1148
  {filtersActive
1149
1149
  ? 'Try clearing the search, tags, or status filter above.'
1150
1150
  : 'Create your first page to get started.'}
@@ -1184,8 +1184,8 @@ function PaginationFooter({
1184
1184
  const start = total === 0 ? 0 : (page - 1) * pageSize + 1
1185
1185
  const end = Math.min(total, page * pageSize)
1186
1186
  return (
1187
- <div className="border-border flex items-center justify-between border-t px-4 py-3 text-sm">
1188
- <span className="text-muted-foreground tabular-nums">
1187
+ <div className="flex items-center justify-between border-t border-[var(--bdr)] px-4 py-3 text-sm">
1188
+ <span className="text-[var(--sub)] tabular-nums">
1189
1189
  {start}–{end} of {total} page{total === 1 ? '' : 's'}
1190
1190
  </span>
1191
1191
  <nav className="flex items-center gap-1" aria-label="Pages pagination">
@@ -1194,7 +1194,7 @@ function PaginationFooter({
1194
1194
  onClick={() => onPage(Math.max(1, page - 1))}
1195
1195
  disabled={page === 1}
1196
1196
  aria-label="Previous page"
1197
- className="border-border bg-background text-foreground hover:bg-accent inline-flex h-8 w-8 items-center justify-center rounded-md border transition-colors disabled:cursor-not-allowed disabled:opacity-40"
1197
+ className="inline-flex h-8 w-8 items-center justify-center rounded-md border border-[var(--bdr)] bg-[var(--bg)] text-[var(--txt)] transition-colors hover:bg-[var(--acc-l)] disabled:cursor-not-allowed disabled:opacity-40"
1198
1198
  >
1199
1199
  <ChevronLeft className="h-4 w-4" aria-hidden />
1200
1200
  </button>
@@ -1203,7 +1203,7 @@ function PaginationFooter({
1203
1203
  <span
1204
1204
  key={`gap-${idx}`}
1205
1205
  aria-hidden
1206
- className="text-muted-foreground inline-flex h-8 w-8 items-center justify-center text-sm"
1206
+ className="inline-flex h-8 w-8 items-center justify-center text-sm text-[var(--sub)]"
1207
1207
  >
1208
1208
 
1209
1209
  </span>
@@ -1216,7 +1216,7 @@ function PaginationFooter({
1216
1216
  className={`inline-flex h-8 w-8 items-center justify-center rounded-md text-sm tabular-nums transition-colors ${
1217
1217
  p === page
1218
1218
  ? 'bg-primary text-primary-foreground'
1219
- : 'border-border bg-background text-foreground hover:bg-accent border'
1219
+ : 'border border-[var(--bdr)] bg-[var(--bg)] text-[var(--txt)] hover:bg-[var(--acc-l)]'
1220
1220
  }`}
1221
1221
  >
1222
1222
  {p}
@@ -1228,7 +1228,7 @@ function PaginationFooter({
1228
1228
  onClick={() => onPage(Math.min(totalPages, page + 1))}
1229
1229
  disabled={page === totalPages}
1230
1230
  aria-label="Next page"
1231
- className="border-border bg-background text-foreground hover:bg-accent inline-flex h-8 w-8 items-center justify-center rounded-md border transition-colors disabled:cursor-not-allowed disabled:opacity-40"
1231
+ className="inline-flex h-8 w-8 items-center justify-center rounded-md border border-[var(--bdr)] bg-[var(--bg)] text-[var(--txt)] transition-colors hover:bg-[var(--acc-l)] disabled:cursor-not-allowed disabled:opacity-40"
1232
1232
  >
1233
1233
  <ChevronRight className="h-4 w-4" aria-hidden />
1234
1234
  </button>
@@ -82,7 +82,7 @@ export function PagesTreeTable({
82
82
  aria-label="Select all visible pages"
83
83
  checked={allSelected}
84
84
  onChange={(e) => onToggleAll(e.target.checked)}
85
- className="border-border accent-primary rounded"
85
+ className="accent-primary rounded border-[var(--bdr)]"
86
86
  />
87
87
  </th>
88
88
  <th scope="col" className={`${TH_CLASS} min-w-[280px]`}>
@@ -113,8 +113,11 @@ export function PagesTreeTable({
113
113
  {!loading && error && (
114
114
  <tr>
115
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">
116
+ <div className="mb-2 text-sm font-medium text-[var(--txt)]">{error}</div>
117
+ <button
118
+ onClick={onRetry}
119
+ className="text-primary rounded-sm text-sm hover:underline focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
120
+ >
118
121
  Try again
119
122
  </button>
120
123
  </td>
@@ -123,10 +126,10 @@ export function PagesTreeTable({
123
126
  {!loading && !error && rows.length === 0 && (
124
127
  <tr>
125
128
  <td colSpan={8} className="px-3 py-16 text-center">
126
- <div className="text-foreground mb-2 text-sm font-medium">
129
+ <div className="mb-2 text-sm font-medium text-[var(--txt)]">
127
130
  {flattened ? 'No pages match your filters' : 'No pages yet'}
128
131
  </div>
129
- <div className="text-muted-foreground mb-4 text-sm">
132
+ <div className="mb-4 text-sm text-[var(--sub)]">
130
133
  {flattened
131
134
  ? 'Try clearing the search or status filter above.'
132
135
  : 'Create your first page to get started.'}
@@ -135,7 +138,7 @@ export function PagesTreeTable({
135
138
  <button
136
139
  type="button"
137
140
  onClick={onClearFilters}
138
- className="text-primary text-sm hover:underline"
141
+ className="text-primary rounded-sm text-sm hover:underline focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
139
142
  >
140
143
  Clear filters
141
144
  </button>
@@ -143,7 +146,7 @@ export function PagesTreeTable({
143
146
  <button
144
147
  type="button"
145
148
  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"
149
+ 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 focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
147
150
  >
148
151
  <Plus className="h-3.5 w-3.5" aria-hidden />
149
152
  New page
@@ -208,7 +211,7 @@ function TreeRow({
208
211
  aria-label={`Select "${page.title}"`}
209
212
  checked={isSelected}
210
213
  onChange={onToggleSelect}
211
- className="border-border accent-primary rounded"
214
+ className="accent-primary rounded border-[var(--bdr)]"
212
215
  />
213
216
  </td>
214
217
  <td className="px-3 align-middle">
package/src/views/SEO.tsx CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  } from 'lucide-react'
19
19
  import { useCallback, useEffect, useMemo, useState } from 'react'
20
20
  import { toast } from 'sonner'
21
+ import { parseRouteQuery } from '../router/index.js'
21
22
  import { ErrorBoundary } from '../components/ErrorBoundary.js'
22
23
  import { fetchSeoOverview, runSeoAudit } from '../lib/seo-service.js'
23
24
  import { fetchPlanInfo, type PlanInfo } from '../lib/plan.js'
@@ -62,7 +63,7 @@ function readUrlState(): UrlState {
62
63
  if (typeof window === 'undefined') {
63
64
  return { tab: null, entityType: null, entityId: null, issueId: null }
64
65
  }
65
- const params = new URLSearchParams(window.location.search)
66
+ const params = parseRouteQuery(window.location.search)
66
67
  return {
67
68
  tab: params.get('tab'),
68
69
  entityType: params.get('entityType'),
@@ -139,7 +139,7 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
139
139
  <span className="text-destructive flex-1 text-sm">{error}</span>
140
140
  <button
141
141
  onClick={refetch}
142
- className="border-destructive/40 text-destructive hover:bg-destructive/15 rounded-lg border px-3 py-1 text-sm transition-colors"
142
+ className="border-destructive/40 text-destructive hover:bg-destructive/15 rounded-lg border px-3 py-1 text-sm transition-colors focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
143
143
  >
144
144
  Retry
145
145
  </button>
@@ -148,8 +148,8 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
148
148
 
149
149
  <div className="mb-4 flex items-center justify-between">
150
150
  <div>
151
- <h1 className="text-foreground mb-1">Script Tags</h1>
152
- <p className="text-muted-foreground text-sm">
151
+ <h1 className="mb-1 text-[var(--txt)]">Script Tags</h1>
152
+ <p className="text-sm text-[var(--sub)]">
153
153
  Manage tracking codes, analytics, and custom scripts injected into your site
154
154
  </p>
155
155
  {tags.length > 0 && (
@@ -171,7 +171,7 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
171
171
  <button
172
172
  type="button"
173
173
  onClick={() => setSelection({ mode: 'new' })}
174
- className="bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors"
174
+ className="bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
175
175
  >
176
176
  <Plus className="h-4 w-4" />
177
177
  New Script
@@ -182,45 +182,35 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
182
182
  <SplitPaneLayout>
183
183
  <SplitPaneList className="p-3 pt-0 pr-6 sm:p-4 sm:pt-0 sm:pr-8">
184
184
  {tags.length === 0 && !error ? (
185
- <div className="border-border bg-card rounded-lg border p-12 text-center">
186
- <Code2 className="text-muted-foreground mx-auto mb-3 h-10 w-10" />
187
- <h3 className="text-foreground text-sm font-medium">No script tags yet</h3>
188
- <p className="text-muted-foreground mt-1 text-sm">
185
+ <div className="rounded-lg border border-[var(--bdr)] bg-[var(--card)] p-12 text-center">
186
+ <Code2 className="mx-auto mb-3 h-10 w-10 text-[var(--sub)]" />
187
+ <h3 className="text-sm font-medium text-[var(--txt)]">No script tags yet</h3>
188
+ <p className="mt-1 text-sm text-[var(--sub)]">
189
189
  Add tracking codes like Google Analytics, Tag Manager, or Facebook Pixel.
190
190
  </p>
191
191
  <button
192
192
  type="button"
193
193
  onClick={() => setSelection({ mode: 'new' })}
194
- className="bg-primary text-primary-foreground hover:bg-primary/90 mt-4 inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors"
194
+ className="bg-primary text-primary-foreground hover:bg-primary/90 mt-4 inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
195
195
  >
196
196
  <Plus className="h-4 w-4" />
197
197
  Add Your First Script
198
198
  </button>
199
199
  </div>
200
200
  ) : (
201
- <div className="border-border bg-card overflow-hidden rounded-lg border">
201
+ <div className="overflow-hidden rounded-lg border border-[var(--bdr)] bg-[var(--card)]">
202
202
  <table className="w-full text-sm" aria-label="Script tags">
203
203
  <thead>
204
- <tr className="border-border bg-muted border-b">
205
- <th className="text-muted-foreground px-4 py-3 text-left font-medium">
206
- Script
207
- </th>
208
- <th className="text-muted-foreground px-4 py-3 text-left font-medium">
209
- Location
210
- </th>
211
- <th className="text-muted-foreground px-4 py-3 text-center font-medium">
204
+ <tr className="bg-muted border-b border-[var(--bdr)]">
205
+ <th className="px-4 py-3 text-left font-medium text-[var(--sub)]">Script</th>
206
+ <th className="px-4 py-3 text-left font-medium text-[var(--sub)]">Location</th>
207
+ <th className="px-4 py-3 text-center font-medium text-[var(--sub)]">
212
208
  Priority
213
209
  </th>
214
- <th className="text-muted-foreground px-4 py-3 text-left font-medium">Scope</th>
215
- <th className="text-muted-foreground px-4 py-3 text-left font-medium">
216
- Consent
217
- </th>
218
- <th className="text-muted-foreground px-4 py-3 text-center font-medium">
219
- Active
220
- </th>
221
- <th className="text-muted-foreground px-4 py-3 text-left font-medium">
222
- Updated
223
- </th>
210
+ <th className="px-4 py-3 text-left font-medium text-[var(--sub)]">Scope</th>
211
+ <th className="px-4 py-3 text-left font-medium text-[var(--sub)]">Consent</th>
212
+ <th className="px-4 py-3 text-center font-medium text-[var(--sub)]">Active</th>
213
+ <th className="px-4 py-3 text-left font-medium text-[var(--sub)]">Updated</th>
224
214
  </tr>
225
215
  </thead>
226
216
  <tbody>
@@ -229,8 +219,8 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
229
219
  return (
230
220
  <tr
231
221
  key={tag.id}
232
- className={`border-border cursor-pointer border-b transition-colors last:border-0 ${
233
- isSelected ? 'bg-[var(--acc-l)]' : 'hover:bg-muted'
222
+ className={`cursor-pointer border-b border-[var(--bdr)] transition-colors last:border-0 ${
223
+ isSelected ? 'bg-[var(--acc-l)]' : 'hover:bg-[var(--acc-l)]'
234
224
  }`}
235
225
  onClick={() => toggleRow(tag)}
236
226
  >
@@ -242,29 +232,29 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
242
232
  toggleRow(tag)
243
233
  }}
244
234
  aria-expanded={isSelected}
245
- className="text-foreground hover:text-primary text-left font-medium"
235
+ className="text-left font-medium text-[var(--txt)] hover:text-[var(--acc)]"
246
236
  >
247
237
  {tag.name}
248
238
  </button>
249
239
  </td>
250
240
  <td className="px-4 py-[11px]">
251
241
  <span
252
- className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${PLACEMENT_COLORS[tag.placement] ?? 'bg-muted text-foreground'}`}
242
+ className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${PLACEMENT_COLORS[tag.placement] ?? 'bg-muted text-[var(--txt)]'}`}
253
243
  >
254
244
  {PLACEMENT_LABELS[tag.placement] ?? tag.placement}
255
245
  </span>
256
246
  </td>
257
- <td className="text-muted-foreground px-4 py-[11px] text-center font-mono">
247
+ <td className="px-4 py-[11px] text-center font-mono text-[var(--sub)]">
258
248
  {tag.priority}
259
249
  </td>
260
- <td className="text-muted-foreground px-4 py-[11px]">{scopeLabel(tag)}</td>
250
+ <td className="px-4 py-[11px] text-[var(--sub)]">{scopeLabel(tag)}</td>
261
251
  <td className="px-4 py-[11px]">
262
252
  {tag.consentCategory && tag.consentCategory !== 'none' ? (
263
253
  <span className="rounded-[5px] border border-[var(--bdr)] bg-[var(--bg)] px-1.5 py-0.5 text-[11px] text-[var(--sub)]">
264
254
  {CONSENT_LABELS[tag.consentCategory] ?? tag.consentCategory}
265
255
  </span>
266
256
  ) : (
267
- <span className="text-muted-foreground text-xs">—</span>
257
+ <span className="text-xs text-[var(--sub)]">—</span>
268
258
  )}
269
259
  </td>
270
260
  <td
@@ -278,7 +268,7 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
278
268
  aria-label={`${tag.enabled ? 'Disable' : 'Enable'} ${tag.name}`}
279
269
  />
280
270
  </td>
281
- <td className="text-muted-foreground px-4 py-[11px] text-xs">
271
+ <td className="px-4 py-[11px] text-xs text-[var(--sub)]">
282
272
  {formatUpdated(tag.updatedAt)}
283
273
  </td>
284
274
  </tr>
@@ -13,6 +13,7 @@ import {
13
13
  } from 'lucide-react'
14
14
  import { useState, useEffect, useCallback, useMemo } from 'react'
15
15
  import { toast } from 'sonner'
16
+ import { parseRouteQuery } from '../router/index.js'
16
17
  import { useApiData } from '../lib/useApiData.js'
17
18
  import { cmsApi } from '../lib/api.js'
18
19
  import { RelationshipField } from '../fields/RelationshipField.js'
@@ -35,6 +36,9 @@ export interface SettingsProps {
35
36
  /** Initial tab when arriving without a `?tab=` query param (e.g. via the
36
37
  * legacy `/users` or `/api-keys` routes). */
37
38
  initialTab?: string
39
+ /** Router path including the query string (e.g. `/users?selected=<id>`).
40
+ * Forwarded to embedded tab views so their deep links keep working. */
41
+ currentPath?: string
38
42
  }
39
43
 
40
44
  /**
@@ -48,7 +52,7 @@ const GLOBAL_SAVE_TABS = new Set(['layout'])
48
52
 
49
53
  function readSettingsTabFromUrl(): string | null {
50
54
  if (typeof window === 'undefined') return null
51
- return new URLSearchParams(window.location.search).get('tab')
55
+ return parseRouteQuery(window.location.search).get('tab')
52
56
  }
53
57
 
54
58
  export function Settings({
@@ -56,6 +60,7 @@ export function Settings({
56
60
  onNavigate,
57
61
  session,
58
62
  initialTab = 'general',
63
+ currentPath,
59
64
  }: SettingsProps = {}) {
60
65
  const { data, loading, error, refetch } = useApiData<any>('/globals/settings')
61
66
 
@@ -329,7 +334,7 @@ export function Settings({
329
334
  </Tabs.Content>
330
335
 
331
336
  <Tabs.Content value="users">
332
- <UsersView onNavigate={onNavigate} embedded />
337
+ <UsersView onNavigate={onNavigate} embedded currentPath={currentPath} />
333
338
  </Tabs.Content>
334
339
 
335
340
  <Tabs.Content value="api-keys">
@@ -22,8 +22,9 @@ import {
22
22
  Check,
23
23
  SlidersHorizontal,
24
24
  } from 'lucide-react'
25
- import { useState, useMemo, useCallback, type FormEvent } from 'react'
25
+ import { useState, useMemo, useCallback, useEffect, useRef, type FormEvent } from 'react'
26
26
  import { toast } from 'sonner'
27
+ import { parseRouteQuery } from '../router/index.js'
27
28
  import { useApiData } from '../lib/useApiData.js'
28
29
  import { cmsApi } from '../lib/api.js'
29
30
  import { Button } from '../components/ui/Button.js'
@@ -675,9 +676,12 @@ const ROLE_PILLS: Array<'All' | 'Owner' | 'Admin' | 'Editor' | 'Viewer'> = [
675
676
  export interface UsersProps {
676
677
  onNavigate?: (path: string) => void
677
678
  embedded?: boolean
679
+ /** Router path including the query string. `?selected=<userId>` (written by
680
+ * the command palette) opens that member's detail drawer once the list loads. */
681
+ currentPath?: string
678
682
  }
679
683
 
680
- export function Users({ embedded = false }: UsersProps = {}) {
684
+ export function Users({ embedded = false, currentPath }: UsersProps = {}) {
681
685
  const members = useApiData<ApiMember[]>('/users?pageSize=200')
682
686
  const invites = useApiData<ApiInvite[]>('/invites')
683
687
  const stats = useApiData<ApiStats>('/users/stats')
@@ -702,6 +706,23 @@ export function Users({ embedded = false }: UsersProps = {}) {
702
706
  review.refetch()
703
707
  }, [members, invites, stats, review])
704
708
 
709
+ // Deep link from the command palette (`/users?selected=<id>`): open the
710
+ // member's detail drawer once the list loads. Each query value is handled
711
+ // once, so closing the drawer isn't overridden on refetch; ids that don't
712
+ // match a loaded member are ignored.
713
+ const selectedUserId = useMemo(() => {
714
+ const source = currentPath ?? (typeof window !== 'undefined' ? window.location.search : '')
715
+ return parseRouteQuery(source).get('selected')
716
+ }, [currentPath])
717
+ const handledSelectedRef = useRef<string | null>(null)
718
+ useEffect(() => {
719
+ if (!selectedUserId || members.loading) return
720
+ if (handledSelectedRef.current === selectedUserId) return
721
+ if (!(members.data ?? []).some((m) => m.id === selectedUserId)) return
722
+ handledSelectedRef.current = selectedUserId
723
+ setDetailUserId(selectedUserId)
724
+ }, [selectedUserId, members.loading, members.data])
725
+
705
726
  const currentUserRole = useMemo(
706
727
  () => (members.data ?? []).find((m) => m.isCurrentUser)?.roleName ?? null,
707
728
  [members.data],