@exxatdesignux/ui 0.2.9 → 0.2.10
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.
- package/consumer-extras/cursor-skills/exxat-card-vs-list-rows/SKILL.md +20 -0
- package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +33 -0
- package/consumer-extras/cursor-skills/exxat-dedicated-search-surfaces/SKILL.md +45 -0
- package/consumer-extras/cursor-skills/exxat-drawer-vs-dialog/SKILL.md +20 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +31 -5
- package/consumer-extras/cursor-skills/exxat-kpi-max-four/SKILL.md +19 -0
- package/consumer-extras/cursor-skills/exxat-kpi-trends/SKILL.md +27 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +1 -0
- package/consumer-extras/patterns/collaboration-access-pattern.md +114 -0
- package/consumer-extras/patterns/data-views-pattern.md +12 -4
- package/package.json +1 -1
- package/src/components/ui/banner.tsx +20 -7
- package/src/components/ui/date-picker-field.tsx +3 -3
- package/src/components/ui/dropdown-menu.tsx +17 -6
- package/src/components/ui/input-group.tsx +1 -1
- package/src/components/ui/input.tsx +1 -1
- package/src/components/ui/select.tsx +1 -1
- package/src/components/ui/separator.tsx +2 -2
- package/src/components/ui/sidebar.tsx +31 -3
- package/src/components/ui/textarea.tsx +1 -1
- package/src/globals.css +0 -1
- package/src/index.ts +1 -0
- package/src/lib/date-filter.ts +13 -4
- package/src/lib/dropdown-menu-surface.ts +13 -0
- package/template/.claude/skills/exxat-ds-skill/SKILL.md +27 -9
- package/template/.cursor/rules/exxat-data-tables.mdc +1 -0
- package/template/AGENTS.md +82 -27
- package/template/app/(app)/examples/page.tsx +2 -1
- package/template/app/(app)/help/page.tsx +6 -0
- package/template/app/(app)/layout.tsx +7 -4
- package/template/app/(app)/question-bank/find/page.tsx +12 -0
- package/template/app/(app)/question-bank/layout.tsx +46 -0
- package/template/app/(app)/question-bank/library/page.tsx +11 -0
- package/template/app/(app)/question-bank/list/page.tsx +12 -0
- package/template/app/(app)/question-bank/page.tsx +4 -3
- package/template/app/globals.css +1 -2
- package/template/components/app-sidebar.tsx +51 -13
- package/template/components/ask-leo-composer.tsx +173 -45
- package/template/components/ask-leo-sidebar.tsx +9 -1
- package/template/components/chart-area-interactive.tsx +3 -13
- package/template/components/charts-overview.tsx +33 -6
- package/template/components/collaboration-access-flow.tsx +144 -0
- package/template/components/compliance-page-header.tsx +1 -1
- package/template/components/compliance-table.tsx +2 -2
- package/template/components/dashboard-tabs.tsx +4 -3
- package/template/components/data-list-table-cells.tsx +1 -1
- package/template/components/data-list-table.tsx +1 -1
- package/template/components/data-table/index.tsx +5 -5
- package/template/components/data-table/use-table-state.ts +18 -2
- package/template/components/data-view-dashboard-charts-compliance.tsx +8 -5
- package/template/components/data-view-dashboard-charts-team.tsx +8 -5
- package/template/components/data-view-dashboard-charts.tsx +62 -227
- package/template/components/dedicated-search-recents.tsx +96 -0
- package/template/components/dedicated-search-url-composer.tsx +112 -0
- package/template/components/getting-started.tsx +1 -1
- package/template/components/hub-tree-panel-view.tsx +10 -26
- package/template/components/invite-collaborators-drawer.tsx +453 -0
- package/template/components/key-metrics.tsx +54 -8
- package/template/components/nav-documents.tsx +1 -1
- package/template/components/new-placement-form.tsx +3 -3
- package/template/components/page-header.tsx +76 -59
- package/template/components/placements-board-view.tsx +3 -3
- package/template/components/placements-page-header.tsx +1 -1
- package/template/components/placements-table-columns.tsx +3 -2
- package/template/components/product-switcher.tsx +0 -1
- package/template/components/question-bank-board-view.tsx +35 -47
- package/template/components/question-bank-client.tsx +293 -81
- package/template/components/question-bank-dashboard-charts.tsx +174 -0
- package/template/components/question-bank-favorite-button.tsx +46 -0
- package/template/components/question-bank-hub-client.tsx +436 -0
- package/template/components/question-bank-list-view.tsx +26 -19
- package/template/components/question-bank-new-folder-sheet.tsx +56 -42
- package/template/components/question-bank-os-folder-view.tsx +3 -14
- package/template/components/question-bank-page-header.tsx +85 -53
- package/template/components/question-bank-panel-activator.tsx +3 -4
- package/template/components/question-bank-secondary-nav.tsx +523 -65
- package/template/components/question-bank-table.tsx +125 -343
- package/template/components/secondary-panel.tsx +130 -63
- package/template/components/settings-client.tsx +3 -1
- package/template/components/sidebar-shell.tsx +2 -0
- package/template/components/sites-all-client.tsx +1 -1
- package/template/components/sites-table.tsx +1 -1
- package/template/components/system-banner-slot.tsx +2 -1
- package/template/components/table-properties/drawer.tsx +3 -3
- package/template/components/table-properties/sort-card.tsx +1 -1
- package/template/components/team-page-header.tsx +1 -1
- package/template/components/team-table.tsx +8 -4
- package/template/components/templates/dedicated-search-landing-template.tsx +58 -0
- package/template/components/templates/dedicated-search-results-template.tsx +19 -0
- package/template/components/templates/discovery-hub-template.tsx +273 -0
- package/template/components/templates/list-page.tsx +11 -4
- package/template/components/templates/nested-secondary-panel-shell.tsx +57 -0
- package/template/components/templates/secondary-panel-hub-template.tsx +54 -0
- package/template/docs/card-vs-rows-pattern.md +36 -0
- package/template/docs/collaboration-access-pattern.md +114 -0
- package/template/docs/data-views-pattern.md +12 -4
- package/template/docs/drawer-vs-dialog-pattern.md +50 -0
- package/template/docs/kpi-strip-max-four-pattern.md +29 -0
- package/template/docs/kpi-trend-pattern.md +43 -0
- package/template/fontawesome-subset.manifest.json +2 -2
- package/template/hooks/use-location-hash.ts +14 -8
- package/template/hooks/use-secondary-panel-hub-nav.ts +98 -0
- package/template/lib/ask-leo-route-context.ts +24 -0
- package/template/lib/collaborator-access.ts +92 -0
- package/template/lib/command-menu-config.ts +8 -1
- package/template/lib/command-menu-search-data.ts +11 -8
- package/template/lib/data-list-display-options.ts +1 -1
- package/template/lib/data-view-dashboard-placements-layout.ts +215 -0
- package/template/lib/date-filter.ts +1 -0
- package/template/lib/dedicated-search-recents.ts +76 -0
- package/template/lib/dedicated-search-url.ts +23 -0
- package/template/lib/discovery-hub.ts +15 -0
- package/template/lib/list-status-badges.ts +1 -21
- package/template/lib/mock/navigation.tsx +4 -2
- package/template/lib/mock/placements.ts +9 -9
- package/template/lib/mock/question-bank-folders.ts +7 -0
- package/template/lib/mock/question-bank-header-collaborators.ts +45 -5
- package/template/lib/mock/question-bank-inspector.ts +1 -2
- package/template/lib/mock/question-bank-kpi.ts +38 -26
- package/template/lib/mock/question-bank.ts +43 -16
- package/template/lib/question-bank-dedicated-search.ts +19 -0
- package/template/lib/question-bank-hub-search.ts +90 -0
- package/template/lib/question-bank-nav.ts +322 -6
- package/template/lib/question-bank-recent-searches.ts +22 -0
- package/template/package.json +0 -1
|
@@ -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
|
|
869
|
-
{isDown && <i className="fa-light fa-arrow-trend-down text-xs
|
|
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
|
|
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
|
|
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"
|
|
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 "@/
|
|
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"
|
|
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
|
|
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: "
|
|
99
|
-
{ id: 5, label: "Schedule supervisor training", due: "
|
|
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"
|
|
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 "@/
|
|
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"
|
|
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"
|
|
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"
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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 "@/
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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 "@/
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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} />
|