@exxatdesignux/ui 0.2.9 → 0.2.11

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 (126) hide show
  1. package/consumer-extras/cursor-skills/exxat-card-vs-list-rows/SKILL.md +20 -0
  2. package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +33 -0
  3. package/consumer-extras/cursor-skills/exxat-dedicated-search-surfaces/SKILL.md +45 -0
  4. package/consumer-extras/cursor-skills/exxat-drawer-vs-dialog/SKILL.md +20 -0
  5. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +31 -5
  6. package/consumer-extras/cursor-skills/exxat-kpi-max-four/SKILL.md +19 -0
  7. package/consumer-extras/cursor-skills/exxat-kpi-trends/SKILL.md +27 -0
  8. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +1 -0
  9. package/consumer-extras/patterns/collaboration-access-pattern.md +114 -0
  10. package/consumer-extras/patterns/data-views-pattern.md +12 -4
  11. package/package.json +4 -1
  12. package/src/components/ui/banner.tsx +20 -7
  13. package/src/components/ui/date-picker-field.tsx +3 -3
  14. package/src/components/ui/dropdown-menu.tsx +17 -6
  15. package/src/components/ui/input-group.tsx +1 -1
  16. package/src/components/ui/input.tsx +1 -1
  17. package/src/components/ui/select.tsx +1 -1
  18. package/src/components/ui/separator.tsx +2 -2
  19. package/src/components/ui/sidebar.tsx +31 -3
  20. package/src/components/ui/textarea.tsx +1 -1
  21. package/src/globals.css +0 -1
  22. package/src/index.ts +1 -0
  23. package/src/lib/date-filter.ts +13 -4
  24. package/src/lib/dropdown-menu-surface.ts +13 -0
  25. package/template/.claude/skills/exxat-ds-skill/SKILL.md +27 -9
  26. package/template/.cursor/rules/exxat-data-tables.mdc +1 -0
  27. package/template/.nvmrc +1 -1
  28. package/template/AGENTS.md +82 -27
  29. package/template/app/(app)/examples/page.tsx +2 -1
  30. package/template/app/(app)/help/page.tsx +6 -0
  31. package/template/app/(app)/layout.tsx +7 -4
  32. package/template/app/(app)/question-bank/find/page.tsx +12 -0
  33. package/template/app/(app)/question-bank/layout.tsx +46 -0
  34. package/template/app/(app)/question-bank/library/page.tsx +11 -0
  35. package/template/app/(app)/question-bank/list/page.tsx +12 -0
  36. package/template/app/(app)/question-bank/page.tsx +4 -3
  37. package/template/app/globals.css +1 -2
  38. package/template/components/app-sidebar.tsx +51 -13
  39. package/template/components/ask-leo-composer.tsx +173 -45
  40. package/template/components/ask-leo-sidebar.tsx +9 -1
  41. package/template/components/chart-area-interactive.tsx +3 -13
  42. package/template/components/charts-overview.tsx +33 -6
  43. package/template/components/collaboration-access-flow.tsx +144 -0
  44. package/template/components/compliance-page-header.tsx +1 -1
  45. package/template/components/compliance-table.tsx +2 -2
  46. package/template/components/dashboard-tabs.tsx +4 -3
  47. package/template/components/data-list-table-cells.tsx +1 -1
  48. package/template/components/data-list-table.tsx +1 -1
  49. package/template/components/data-table/index.tsx +5 -5
  50. package/template/components/data-table/use-table-state.ts +18 -2
  51. package/template/components/data-view-dashboard-charts-compliance.tsx +8 -5
  52. package/template/components/data-view-dashboard-charts-team.tsx +8 -5
  53. package/template/components/data-view-dashboard-charts.tsx +62 -227
  54. package/template/components/dedicated-search-recents.tsx +96 -0
  55. package/template/components/dedicated-search-url-composer.tsx +112 -0
  56. package/template/components/getting-started.tsx +1 -1
  57. package/template/components/hub-tree-panel-view.tsx +10 -26
  58. package/template/components/invite-collaborators-drawer.tsx +453 -0
  59. package/template/components/key-metrics.tsx +54 -8
  60. package/template/components/nav-documents.tsx +1 -1
  61. package/template/components/new-placement-form.tsx +3 -3
  62. package/template/components/page-header.tsx +76 -59
  63. package/template/components/placements-board-view.tsx +3 -3
  64. package/template/components/placements-page-header.tsx +1 -1
  65. package/template/components/placements-table-columns.tsx +3 -2
  66. package/template/components/product-switcher.tsx +0 -1
  67. package/template/components/question-bank-board-view.tsx +35 -47
  68. package/template/components/question-bank-client.tsx +293 -81
  69. package/template/components/question-bank-dashboard-charts.tsx +174 -0
  70. package/template/components/question-bank-favorite-button.tsx +46 -0
  71. package/template/components/question-bank-hub-client.tsx +436 -0
  72. package/template/components/question-bank-list-view.tsx +26 -19
  73. package/template/components/question-bank-new-folder-sheet.tsx +56 -42
  74. package/template/components/question-bank-os-folder-view.tsx +3 -14
  75. package/template/components/question-bank-page-header.tsx +85 -53
  76. package/template/components/question-bank-panel-activator.tsx +3 -4
  77. package/template/components/question-bank-secondary-nav.tsx +523 -65
  78. package/template/components/question-bank-table.tsx +125 -343
  79. package/template/components/secondary-panel.tsx +130 -63
  80. package/template/components/settings-client.tsx +3 -1
  81. package/template/components/sidebar-shell.tsx +2 -0
  82. package/template/components/sites-all-client.tsx +1 -1
  83. package/template/components/sites-table.tsx +1 -1
  84. package/template/components/system-banner-slot.tsx +2 -1
  85. package/template/components/table-properties/drawer.tsx +3 -3
  86. package/template/components/table-properties/sort-card.tsx +1 -1
  87. package/template/components/team-page-header.tsx +1 -1
  88. package/template/components/team-table.tsx +8 -4
  89. package/template/components/templates/dedicated-search-landing-template.tsx +58 -0
  90. package/template/components/templates/dedicated-search-results-template.tsx +19 -0
  91. package/template/components/templates/discovery-hub-template.tsx +273 -0
  92. package/template/components/templates/list-page.tsx +11 -4
  93. package/template/components/templates/nested-secondary-panel-shell.tsx +57 -0
  94. package/template/components/templates/secondary-panel-hub-template.tsx +54 -0
  95. package/template/docs/card-vs-rows-pattern.md +36 -0
  96. package/template/docs/collaboration-access-pattern.md +114 -0
  97. package/template/docs/data-views-pattern.md +12 -4
  98. package/template/docs/drawer-vs-dialog-pattern.md +50 -0
  99. package/template/docs/kpi-strip-max-four-pattern.md +29 -0
  100. package/template/docs/kpi-trend-pattern.md +43 -0
  101. package/template/fontawesome-subset.manifest.json +2 -2
  102. package/template/hooks/use-location-hash.ts +14 -8
  103. package/template/hooks/use-secondary-panel-hub-nav.ts +98 -0
  104. package/template/lib/ask-leo-route-context.ts +24 -0
  105. package/template/lib/collaborator-access.ts +92 -0
  106. package/template/lib/command-menu-config.ts +8 -1
  107. package/template/lib/command-menu-search-data.ts +11 -8
  108. package/template/lib/data-list-display-options.ts +1 -1
  109. package/template/lib/data-view-dashboard-placements-layout.ts +215 -0
  110. package/template/lib/date-filter.ts +1 -0
  111. package/template/lib/dedicated-search-recents.ts +76 -0
  112. package/template/lib/dedicated-search-url.ts +23 -0
  113. package/template/lib/discovery-hub.ts +15 -0
  114. package/template/lib/list-status-badges.ts +1 -21
  115. package/template/lib/mock/navigation.tsx +4 -2
  116. package/template/lib/mock/placements.ts +9 -9
  117. package/template/lib/mock/question-bank-folders.ts +7 -0
  118. package/template/lib/mock/question-bank-header-collaborators.ts +45 -5
  119. package/template/lib/mock/question-bank-inspector.ts +1 -2
  120. package/template/lib/mock/question-bank-kpi.ts +38 -26
  121. package/template/lib/mock/question-bank.ts +43 -16
  122. package/template/lib/question-bank-dedicated-search.ts +19 -0
  123. package/template/lib/question-bank-hub-search.ts +90 -0
  124. package/template/lib/question-bank-nav.ts +322 -6
  125. package/template/lib/question-bank-recent-searches.ts +22 -0
  126. package/template/package.json +1 -2
@@ -108,10 +108,19 @@ import {
108
108
  import { isEditableTarget } from "@/lib/editable-target"
109
109
  import { chartLineStrokeDash } from "@/lib/chart-line-dash"
110
110
  import { cn } from "@/lib/utils"
111
+ import { metricTrendTone, type MetricTrendPolarity } from "@/components/key-metrics"
111
112
 
112
113
  /** Recharts passes `index` into Line `dot` renderers; published `DotProps` omits it. */
113
114
  type LineDotRenderProps = DotProps & { index?: number }
114
115
 
116
+ type MiniMetric = {
117
+ label: string
118
+ value: string
119
+ trend?: "up" | "down" | "neutral"
120
+ /** Same semantics as `MetricItem.trendPolarity` on `KeyMetrics`. */
121
+ trendPolarity?: MetricTrendPolarity
122
+ }
123
+
115
124
  /* ── Colour tokens ────────────────────────────────────────────────────────── */
116
125
  const BRAND = "var(--brand-color)"
117
126
  const CHART_1 = "var(--color-chart-1)"
@@ -712,8 +721,6 @@ function ChartCardHeader({
712
721
  )
713
722
  }
714
723
 
715
- type MiniMetric = { label: string; value: string; trend?: "up" | "down" | "neutral" }
716
-
717
724
  export function ChartCard({
718
725
  title,
719
726
  description,
@@ -856,6 +863,19 @@ export function ChartCard({
856
863
  {metrics.map((m) => {
857
864
  const isUp = m.trend === "up"
858
865
  const isDown = m.trend === "down"
866
+ const tone = metricTrendTone(m.trend ?? "neutral", m.trendPolarity)
867
+ const upClass =
868
+ tone === "positive"
869
+ ? "text-emerald-600"
870
+ : tone === "negative"
871
+ ? "text-destructive"
872
+ : "text-muted-foreground"
873
+ const downClass =
874
+ tone === "positive"
875
+ ? "text-emerald-600"
876
+ : tone === "negative"
877
+ ? "text-destructive"
878
+ : "text-muted-foreground"
859
879
  return (
860
880
  <TabsTrigger
861
881
  key={m.label}
@@ -865,8 +885,8 @@ export function ChartCard({
865
885
  <span className="text-sm font-normal text-muted-foreground leading-none">{m.label}</span>
866
886
  <div className="flex items-baseline gap-1.5">
867
887
  <span className="text-xl font-bold tabular-nums leading-none text-foreground">{m.value}</span>
868
- {isUp && <i className="fa-light fa-arrow-trend-up text-xs text-emerald-600" aria-hidden="true" />}
869
- {isDown && <i className="fa-light fa-arrow-trend-down text-xs text-destructive" aria-hidden="true" />}
888
+ {isUp && <i className={cn("fa-light fa-arrow-trend-up text-xs", upClass)} aria-hidden="true" />}
889
+ {isDown && <i className={cn("fa-light fa-arrow-trend-down text-xs", downClass)} aria-hidden="true" />}
870
890
  </div>
871
891
  </TabsTrigger>
872
892
  )
@@ -896,6 +916,13 @@ export function ChartCard({
896
916
  const kpi = miniMetrics?.[0]
897
917
  const isUp = kpi?.trend === "up"
898
918
  const isDown = kpi?.trend === "down"
919
+ const tone = metricTrendTone(kpi?.trend ?? "neutral", kpi?.trendPolarity)
920
+ const trendClass =
921
+ tone === "positive"
922
+ ? "text-emerald-600"
923
+ : tone === "negative"
924
+ ? "text-destructive"
925
+ : "text-muted-foreground"
899
926
 
900
927
  return (
901
928
  <Card className={`flex flex-col h-full ${className}`} role="figure" aria-label={title}>
@@ -908,13 +935,13 @@ export function ChartCard({
908
935
  {kpi.value}
909
936
  </span>
910
937
  {isUp && (
911
- <span className="flex items-center gap-1 text-sm font-medium text-emerald-600">
938
+ <span className={cn("flex items-center gap-1 text-sm font-medium", trendClass)}>
912
939
  <i className="fa-light fa-arrow-trend-up" aria-hidden="true" />
913
940
  <span className="sr-only">trending up</span>
914
941
  </span>
915
942
  )}
916
943
  {isDown && (
917
- <span className="flex items-center gap-1 text-sm font-medium text-destructive">
944
+ <span className={cn("flex items-center gap-1 text-sm font-medium", trendClass)}>
918
945
  <i className="fa-light fa-arrow-trend-down" aria-hidden="true" />
919
946
  <span className="sr-only">trending down</span>
920
947
  </span>
@@ -0,0 +1,144 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import type { InviteCollaboratorFormValues } from "@/components/invite-collaborators-drawer"
6
+ import { InviteCollaboratorsDrawer } from "@/components/invite-collaborators-drawer"
7
+ import type { PageHeaderCollaborator } from "@/components/page-header"
8
+ import {
9
+ canRemoveCollaboratorFromRoster,
10
+ canSetCollaboratorAccessRole,
11
+ displayNameFromInviteEmail,
12
+ type CollaboratorAccessRole,
13
+ } from "@/lib/collaborator-access"
14
+ import { initialsFromDisplayName } from "@/lib/initials-from-name"
15
+
16
+ export interface CollaborationAccessFlowRenderProps {
17
+ collaborators: PageHeaderCollaborator[]
18
+ openInvite: () => void
19
+ }
20
+
21
+ export interface CollaborationAccessFlowProps {
22
+ initialCollaborators: PageHeaderCollaborator[]
23
+ resourceLabel: string
24
+ onInvite?: (
25
+ values: InviteCollaboratorFormValues,
26
+ collaborators: PageHeaderCollaborator[],
27
+ ) => PageHeaderCollaborator[] | void
28
+ onCollaboratorAccessChange?: (
29
+ id: string,
30
+ access: CollaboratorAccessRole,
31
+ collaborators: PageHeaderCollaborator[],
32
+ ) => PageHeaderCollaborator[] | void
33
+ onCollaboratorRemove?: (
34
+ id: string,
35
+ collaborators: PageHeaderCollaborator[],
36
+ ) => PageHeaderCollaborator[] | void
37
+ children: (props: CollaborationAccessFlowRenderProps) => React.ReactNode
38
+ }
39
+
40
+ function appendInvitedCollaborator(
41
+ collaborators: PageHeaderCollaborator[],
42
+ values: InviteCollaboratorFormValues,
43
+ ): PageHeaderCollaborator[] {
44
+ const name = displayNameFromInviteEmail(values.email)
45
+ return [
46
+ ...collaborators,
47
+ {
48
+ id: `invite-${values.email}`,
49
+ name,
50
+ email: values.email,
51
+ access: values.access,
52
+ initials: initialsFromDisplayName(name),
53
+ },
54
+ ]
55
+ }
56
+
57
+ function updateCollaboratorAccess(
58
+ collaborators: PageHeaderCollaborator[],
59
+ id: string,
60
+ access: CollaboratorAccessRole,
61
+ ): PageHeaderCollaborator[] {
62
+ const person = collaborators.find(entry => entry.id === id)
63
+ if (!person || !canSetCollaboratorAccessRole(person, collaborators, access)) {
64
+ return collaborators
65
+ }
66
+ return collaborators.map(entry => (entry.id === id ? { ...entry, access } : entry))
67
+ }
68
+
69
+ function removeCollaboratorFromRoster(
70
+ collaborators: PageHeaderCollaborator[],
71
+ id: string,
72
+ ): PageHeaderCollaborator[] {
73
+ const person = collaborators.find(entry => entry.id === id)
74
+ if (!person || !canRemoveCollaboratorFromRoster(person, collaborators)) {
75
+ return collaborators
76
+ }
77
+ return collaborators.filter(entry => entry.id !== id)
78
+ }
79
+
80
+ export function CollaborationAccessFlow({
81
+ initialCollaborators,
82
+ resourceLabel,
83
+ onInvite,
84
+ onCollaboratorAccessChange,
85
+ onCollaboratorRemove,
86
+ children,
87
+ }: CollaborationAccessFlowProps) {
88
+ const [collaborators, setCollaborators] = React.useState<PageHeaderCollaborator[]>(
89
+ () => initialCollaborators.map(person => ({ ...person })),
90
+ )
91
+ const [inviteOpen, setInviteOpen] = React.useState(false)
92
+
93
+ const openInvite = React.useCallback(() => {
94
+ setInviteOpen(true)
95
+ }, [])
96
+
97
+ const handleInvite = React.useCallback(
98
+ (values: InviteCollaboratorFormValues) => {
99
+ setCollaborators(current => {
100
+ const next = onInvite?.(values, current) ?? appendInvitedCollaborator(current, values)
101
+ return next
102
+ })
103
+ },
104
+ [onInvite],
105
+ )
106
+
107
+ const handleAccessChange = React.useCallback(
108
+ (id: string, access: CollaboratorAccessRole) => {
109
+ setCollaborators(current => {
110
+ const next =
111
+ onCollaboratorAccessChange?.(id, access, current)
112
+ ?? updateCollaboratorAccess(current, id, access)
113
+ return next
114
+ })
115
+ },
116
+ [onCollaboratorAccessChange],
117
+ )
118
+
119
+ const handleRemove = React.useCallback(
120
+ (id: string) => {
121
+ setCollaborators(current => {
122
+ const next =
123
+ onCollaboratorRemove?.(id, current) ?? removeCollaboratorFromRoster(current, id)
124
+ return next
125
+ })
126
+ },
127
+ [onCollaboratorRemove],
128
+ )
129
+
130
+ return (
131
+ <>
132
+ {children({ collaborators, openInvite })}
133
+ <InviteCollaboratorsDrawer
134
+ open={inviteOpen}
135
+ onOpenChange={setInviteOpen}
136
+ collaborators={collaborators}
137
+ resourceLabel={resourceLabel}
138
+ onInvite={handleInvite}
139
+ onCollaboratorAccessChange={handleAccessChange}
140
+ onCollaboratorRemove={handleRemove}
141
+ />
142
+ </>
143
+ )
144
+ }
@@ -59,7 +59,7 @@ export function CompliancePageHeader({
59
59
  </Button>
60
60
  </DropdownMenuTrigger>
61
61
  </Tip>
62
- <DropdownMenuContent align="end" className="w-52">
62
+ <DropdownMenuContent align="end">
63
63
  <DropdownMenuItem
64
64
  onSelect={() => {
65
65
  window.setTimeout(() => onExport(), 0)
@@ -22,7 +22,7 @@ import {
22
22
  saveComplianceDashboardLayout,
23
23
  } from "@/components/data-view-dashboard-charts-compliance"
24
24
  import { KEY_METRICS_KPI_COUNT_DEFAULT } from "@/lib/dashboard-layout-merge"
25
- import type { ChartType, DashboardLayout } from "@/components/data-view-dashboard-charts"
25
+ import type { ChartType, DashboardLayout } from "@/lib/data-view-dashboard-placements-layout"
26
26
  import { ComplianceListView } from "@/components/compliance-list-view"
27
27
  import { ComplianceBoardView, COMPLIANCE_BOARD_GROUP_OPTIONS } from "@/components/compliance-board-view"
28
28
  import { complianceKpiInsight, complianceKpiMetrics } from "@/lib/mock/compliance-kpi"
@@ -204,7 +204,7 @@ function buildComplianceColumns(items: ComplianceItem[]): ColumnDef<ComplianceIt
204
204
  <i className="fa-light fa-ellipsis text-sm" aria-hidden="true" />
205
205
  </Button>
206
206
  </DropdownMenuTrigger>
207
- <DropdownMenuContent align="end" className="w-40">
207
+ <DropdownMenuContent align="end">
208
208
  <DropdownMenuItem disabled>
209
209
  <i className="fa-light fa-eye" aria-hidden="true" />
210
210
  View details
@@ -29,6 +29,7 @@ import {
29
29
  import { DashboardPromoBanner } from "@/components/dashboard-promo-banner"
30
30
  import { CoachMark } from "@/components/ui/coach-mark"
31
31
  import { useCoachMark } from "@/hooks/use-coach-mark"
32
+ import { formatDateFromDate } from "@/lib/date-filter"
32
33
 
33
34
  /* ── Types passed from the page ─────────────────────────────────────────── */
34
35
  interface DashboardTabsProps {
@@ -59,7 +60,7 @@ function GreetingWidget({ compact = false }: { compact?: boolean }) {
59
60
  <div>
60
61
  {!compact ? (
61
62
  <p className="text-xs font-medium text-muted-foreground uppercase tracking-wider" suppressHydrationWarning>
62
- {now?.toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric" }) ?? ""}
63
+ {now ? formatDateFromDate(now) : ""}
63
64
  </p>
64
65
  ) : null}
65
66
  {compact ? (
@@ -95,8 +96,8 @@ const TASK_ITEMS: TaskListItem[] = [
95
96
  { id: 1, label: "Review pending evaluations", due: "Today", priority: "high", done: false },
96
97
  { id: 2, label: "Approve site contract — City Med", due: "Today", priority: "high", done: false },
97
98
  { id: 3, label: "Send onboarding docs to PT cohort", due: "Tomorrow", priority: "medium", done: false },
98
- { id: 4, label: "Update compliance checklist", due: "Mar 25", priority: "medium", done: false },
99
- { id: 5, label: "Schedule supervisor training", due: "Mar 28", priority: "low", done: true },
99
+ { id: 4, label: "Update compliance checklist", due: "03/25/2026", priority: "medium", done: false },
100
+ { id: 5, label: "Schedule supervisor training", due: "03/28/2026", priority: "low", done: true },
100
101
  ]
101
102
 
102
103
  /* ── Insights ─────────────────────────────────────────────────────────────── */
@@ -154,7 +154,7 @@ export function RowActions({ row, actions }: { row: Placement; actions: RowActio
154
154
  </Button>
155
155
  </DropdownMenuTrigger>
156
156
  </Tip>
157
- <DropdownMenuContent align="end" className="w-40">
157
+ <DropdownMenuContent align="end">
158
158
  {actions.map((a, i) => (
159
159
  <React.Fragment key={a.label}>
160
160
  {a.variant === "destructive" && i > 0 && <DropdownMenuSeparator />}
@@ -26,7 +26,7 @@ import {
26
26
  saveDashboardLayout,
27
27
  type ChartType,
28
28
  type DashboardLayout,
29
- } from "@/components/data-view-dashboard-charts"
29
+ } from "@/lib/data-view-dashboard-placements-layout"
30
30
  import { CoachMark } from "@/components/ui/coach-mark"
31
31
  import { useCoachMark } from "@/hooks/use-coach-mark"
32
32
  import { DASHBOARD_CUSTOMIZE_COACH_STEPS } from "@/lib/dashboard-customize-coach-mark"
@@ -502,7 +502,7 @@ export function DataTableToolbar<TData extends Record<string, unknown>>({
502
502
  Add filter
503
503
  </button>
504
504
  </DropdownMenuTrigger>
505
- <DropdownMenuContent align="start" className="w-48">
505
+ <DropdownMenuContent align="start">
506
506
  <DropdownMenuLabel className="text-xs">Filter by field</DropdownMenuLabel>
507
507
  <DropdownMenuSeparator />
508
508
  {filterableCols.map(c => (
@@ -615,7 +615,7 @@ export function DataTableToolbar<TData extends Record<string, unknown>>({
615
615
  <i className="fa-light fa-filter text-[13px]" aria-hidden="true" />
616
616
  </button>
617
617
  </DropdownMenuTrigger>
618
- <DropdownMenuContent align="end" className="w-48">
618
+ <DropdownMenuContent align="end">
619
619
  <DropdownMenuLabel className="text-xs">Filter by field</DropdownMenuLabel>
620
620
  <DropdownMenuSeparator />
621
621
  {filterableCols.map(c => (
@@ -1173,7 +1173,7 @@ function DataTableInner<TData extends Record<string, unknown>>({
1173
1173
  </button>
1174
1174
  </DropdownMenuTrigger>
1175
1175
  </Tip>
1176
- <DropdownMenuContent align="start" className="min-w-44">
1176
+ <DropdownMenuContent align="start">
1177
1177
 
1178
1178
  {/* Column quick-search */}
1179
1179
  <div className="px-2 pt-2 pb-1">
@@ -1234,14 +1234,14 @@ function DataTableInner<TData extends Record<string, unknown>>({
1234
1234
  const filtered = prev.filter(r => r.fieldKey !== col.key)
1235
1235
  return [{ id: `sort-${Date.now()}`, fieldKey: col.key, direction: "asc" as const }, ...filtered]
1236
1236
  })}>
1237
- <i className="fa-light fa-arrow-up-az" aria-hidden="true" />
1237
+ <i className="fa-light fa-arrow-up-a-z text-xs shrink-0" aria-hidden="true" />
1238
1238
  Sort Ascending
1239
1239
  </DropdownMenuItem>
1240
1240
  <DropdownMenuItem onClick={() => setSortRules(prev => {
1241
1241
  const filtered = prev.filter(r => r.fieldKey !== col.key)
1242
1242
  return [{ id: `sort-${Date.now()}`, fieldKey: col.key, direction: "desc" as const }, ...filtered]
1243
1243
  })}>
1244
- <i className="fa-light fa-arrow-down-az" aria-hidden="true" />
1244
+ <i className="fa-light fa-arrow-down-a-z text-xs shrink-0" aria-hidden="true" />
1245
1245
  Sort Descending
1246
1246
  </DropdownMenuItem>
1247
1247
  <DropdownMenuSeparator />
@@ -91,6 +91,11 @@ export function useTableState<TData extends Record<string, unknown>>(
91
91
  columns: ColumnDef<TData>[],
92
92
  defaultSort?: { key: string; dir: SortDir },
93
93
  paginationOverride?: { page: number; pageSize: number },
94
+ /**
95
+ * When defined (including `""`), toolbar search is synced from the URL (`?q=`).
96
+ * Use `searchParams.get("q") ?? ""` on question bank list routes; omit for other hubs.
97
+ */
98
+ syncedSearchFromUrl?: string,
94
99
  ) {
95
100
  // ── Sort ──────────────────────────────────────────────────────────────────
96
101
  const [sortRules, setSortRules] = React.useState<SortRule[]>(() => {
@@ -133,8 +138,12 @@ export function useTableState<TData extends Record<string, unknown>>(
133
138
  }, [setSortRules])
134
139
 
135
140
  // ── Filters ───────────────────────────────────────────────────────────────
136
- const [search, setSearch] = React.useState("")
137
- const [searchOpen, setSearchOpen] = React.useState(false)
141
+ const [search, setSearch] = React.useState(() =>
142
+ syncedSearchFromUrl !== undefined ? syncedSearchFromUrl.trim() : "",
143
+ )
144
+ const [searchOpen, setSearchOpen] = React.useState(() =>
145
+ syncedSearchFromUrl !== undefined && Boolean(syncedSearchFromUrl.trim()),
146
+ )
138
147
  const searchRef = React.useRef<HTMLInputElement>(null)
139
148
  const [activeFilters, setActiveFilters] = React.useState<ActiveFilter[]>([])
140
149
  const [filterConnectors, setFilterConnectors] = React.useState<Record<string, "and" | "or">>({})
@@ -142,6 +151,13 @@ export function useTableState<TData extends Record<string, unknown>>(
142
151
  const [filterBarVisible, setFilterBarVisible] = React.useState(true)
143
152
  const [drawerExpandedFilters, setDrawerExpandedFilters] = React.useState<Set<string>>(new Set())
144
153
 
154
+ React.useEffect(() => {
155
+ if (syncedSearchFromUrl === undefined) return
156
+ const next = syncedSearchFromUrl.trim()
157
+ setSearch(next)
158
+ setSearchOpen(next.length > 0)
159
+ }, [syncedSearchFromUrl])
160
+
145
161
  const toggleConnector = React.useCallback((leftId: string) => {
146
162
  setFilterConnectors(prev => ({ ...prev, [leftId]: prev[leftId] === "or" ? "and" : "or" }))
147
163
  }, [setFilterConnectors])
@@ -56,7 +56,7 @@ import {
56
56
  applyVisibleReorder,
57
57
  type ChartType,
58
58
  type DashboardLayout,
59
- } from "@/components/data-view-dashboard-charts"
59
+ } from "@/lib/data-view-dashboard-placements-layout"
60
60
  import {
61
61
  CHART_KBD_ACTIVE_BAR,
62
62
  CHART_KBD_ACTIVE_PIE_SHAPE,
@@ -74,6 +74,9 @@ const CAT_CFG: ChartConfig = {
74
74
  value: { label: "Items", color: "var(--primary)" },
75
75
  }
76
76
 
77
+ const CHART_MARGIN = { top: 8, right: 8, left: 0, bottom: 0 } as const
78
+ const CHART_MARGIN_HORIZONTAL = { top: 8, right: 8, left: 4, bottom: 0 } as const
79
+
77
80
  interface ComplianceDashboardCardDef {
78
81
  id: string
79
82
  title: string
@@ -254,7 +257,7 @@ function ComplianceByStatusChart({ rows, chartType }: { rows: ComplianceItem[];
254
257
  {(activeIndex) => (
255
258
  <>
256
259
  <ChartContainer config={STATUS_CFG} className="h-[220px] w-full">
257
- <BarChart data={byStatus} layout="vertical" margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
260
+ <BarChart data={byStatus} layout="vertical" margin={CHART_MARGIN}>
258
261
  <CartesianGrid horizontal={false} strokeDasharray="3 3" className="stroke-border" />
259
262
  <XAxis type="number" allowDecimals={false} tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
260
263
  <YAxis type="category" dataKey="name" width={88} tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
@@ -289,7 +292,7 @@ function ComplianceByStatusChart({ rows, chartType }: { rows: ComplianceItem[];
289
292
  {(activeIndex) => (
290
293
  <>
291
294
  <ChartContainer config={STATUS_CFG} className="h-[220px] w-full">
292
- <BarChart data={byStatus} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
295
+ <BarChart data={byStatus} margin={CHART_MARGIN}>
293
296
  <CartesianGrid vertical={false} strokeDasharray="3 3" className="stroke-border" />
294
297
  <XAxis dataKey="name" tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
295
298
  <YAxis allowDecimals={false} width={32} tick={{ fontSize: 12 }} tickLine={false} axisLine={false} />
@@ -374,7 +377,7 @@ function ComplianceByCategoryChart({ rows, chartType }: { rows: ComplianceItem[]
374
377
  {(activeIndex) => (
375
378
  <>
376
379
  <ChartContainer config={CAT_CFG} className="h-[220px] w-full">
377
- <BarChart data={byCategory} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
380
+ <BarChart data={byCategory} margin={CHART_MARGIN}>
378
381
  <CartesianGrid vertical={false} strokeDasharray="3 3" className="stroke-border" />
379
382
  <XAxis dataKey="name" tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
380
383
  <YAxis allowDecimals={false} width={32} tick={{ fontSize: 12 }} tickLine={false} axisLine={false} />
@@ -409,7 +412,7 @@ function ComplianceByCategoryChart({ rows, chartType }: { rows: ComplianceItem[]
409
412
  {(activeIndex) => (
410
413
  <>
411
414
  <ChartContainer config={CAT_CFG} className="h-[220px] w-full">
412
- <BarChart data={byCategory} layout="vertical" margin={{ top: 8, right: 8, left: 4, bottom: 0 }}>
415
+ <BarChart data={byCategory} layout="vertical" margin={CHART_MARGIN_HORIZONTAL}>
413
416
  <CartesianGrid horizontal={false} strokeDasharray="3 3" className="stroke-border" />
414
417
  <XAxis type="number" allowDecimals={false} tick={{ fontSize: 12 }} tickLine={false} axisLine={false} />
415
418
  <YAxis type="category" dataKey="name" width={100} tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
@@ -57,7 +57,7 @@ import {
57
57
  applyVisibleReorder,
58
58
  type ChartType,
59
59
  type DashboardLayout,
60
- } from "@/components/data-view-dashboard-charts"
60
+ } from "@/lib/data-view-dashboard-placements-layout"
61
61
  import {
62
62
  CHART_KBD_ACTIVE_BAR,
63
63
  CHART_KBD_ACTIVE_PIE_SHAPE,
@@ -75,6 +75,9 @@ const ROLE_CHART_CFG: ChartConfig = {
75
75
  value: { label: "Members", color: "var(--primary)" },
76
76
  }
77
77
 
78
+ const CHART_MARGIN = { top: 8, right: 8, left: 0, bottom: 0 } as const
79
+ const CHART_MARGIN_HORIZONTAL = { top: 8, right: 8, left: 4, bottom: 0 } as const
80
+
78
81
  interface TeamDashboardCardDef {
79
82
  id: string
80
83
  title: string
@@ -255,7 +258,7 @@ function TeamByStatusChart({ members, chartType }: { members: TeamMember[]; char
255
258
  {(activeIndex) => (
256
259
  <>
257
260
  <ChartContainer config={STATUS_CHART_CFG} className="h-[220px] w-full">
258
- <BarChart data={byStatus} layout="vertical" margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
261
+ <BarChart data={byStatus} layout="vertical" margin={CHART_MARGIN}>
259
262
  <CartesianGrid horizontal={false} strokeDasharray="3 3" className="stroke-border" />
260
263
  <XAxis type="number" allowDecimals={false} tick={{ fontSize: 12 }} tickLine={false} axisLine={false} />
261
264
  <YAxis type="category" dataKey="name" width={72} tick={{ fontSize: 12 }} tickLine={false} axisLine={false} />
@@ -290,7 +293,7 @@ function TeamByStatusChart({ members, chartType }: { members: TeamMember[]; char
290
293
  {(activeIndex) => (
291
294
  <>
292
295
  <ChartContainer config={STATUS_CHART_CFG} className="h-[220px] w-full">
293
- <BarChart data={byStatus} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
296
+ <BarChart data={byStatus} margin={CHART_MARGIN}>
294
297
  <CartesianGrid vertical={false} strokeDasharray="3 3" className="stroke-border" />
295
298
  <XAxis dataKey="name" tick={{ fontSize: 12 }} tickLine={false} axisLine={false} />
296
299
  <YAxis allowDecimals={false} width={36} tick={{ fontSize: 12 }} tickLine={false} axisLine={false} />
@@ -382,7 +385,7 @@ function TeamByRoleChart({ members, chartType }: { members: TeamMember[]; chartT
382
385
  {(activeIndex) => (
383
386
  <>
384
387
  <ChartContainer config={ROLE_CHART_CFG} className="h-[220px] w-full">
385
- <BarChart data={byRole} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
388
+ <BarChart data={byRole} margin={CHART_MARGIN}>
386
389
  <CartesianGrid vertical={false} strokeDasharray="3 3" className="stroke-border" />
387
390
  <XAxis dataKey="name" tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
388
391
  <YAxis allowDecimals={false} width={32} tick={{ fontSize: 12 }} tickLine={false} axisLine={false} />
@@ -417,7 +420,7 @@ function TeamByRoleChart({ members, chartType }: { members: TeamMember[]; chartT
417
420
  {(activeIndex) => (
418
421
  <>
419
422
  <ChartContainer config={ROLE_CHART_CFG} className="h-[220px] w-full">
420
- <BarChart data={byRole} layout="vertical" margin={{ top: 8, right: 8, left: 4, bottom: 0 }}>
423
+ <BarChart data={byRole} layout="vertical" margin={CHART_MARGIN_HORIZONTAL}>
421
424
  <CartesianGrid horizontal={false} strokeDasharray="3 3" className="stroke-border" />
422
425
  <XAxis type="number" allowDecimals={false} tick={{ fontSize: 12 }} tickLine={false} axisLine={false} />
423
426
  <YAxis type="category" dataKey="name" width={120} tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />