@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
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* Sits at the top of a page's main content, BELOW the breadcrumb/topbar.
|
|
7
7
|
* Uses Ivy Presto (Adobe Fonts) for the title via font-heading CSS variable.
|
|
8
8
|
*
|
|
9
|
-
* **Variant `collaboration`** — optional access line +
|
|
10
|
-
*
|
|
9
|
+
* **Variant `collaboration`** — optional access line + collaborator faces (or **Add collaborator**
|
|
10
|
+
* when the roster is empty) ahead of the primary `actions` slot; **Invite people** stays in **⋯ More**.
|
|
11
11
|
*
|
|
12
12
|
* WCAG 2.1 AA:
|
|
13
13
|
* ✓ <h1> landmark — one per page (WCAG 1.3.1)
|
|
@@ -17,9 +17,12 @@
|
|
|
17
17
|
|
|
18
18
|
import * as React from "react"
|
|
19
19
|
import { cn } from "@/lib/utils"
|
|
20
|
+
import {
|
|
21
|
+
COLLABORATION_HEADER_ADD_LABEL,
|
|
22
|
+
type CollaboratorAccessRole,
|
|
23
|
+
} from "@/lib/collaborator-access"
|
|
20
24
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
|
21
25
|
import { Button } from "@/components/ui/button"
|
|
22
|
-
import { Tip } from "@/components/ui/tip"
|
|
23
26
|
import {
|
|
24
27
|
Tooltip,
|
|
25
28
|
TooltipContent,
|
|
@@ -33,6 +36,10 @@ export interface PageHeaderCollaborator {
|
|
|
33
36
|
name: string
|
|
34
37
|
imageUrl?: string | null
|
|
35
38
|
initials?: string
|
|
39
|
+
email?: string
|
|
40
|
+
access?: CollaboratorAccessRole
|
|
41
|
+
/** Org / directory role tags (e.g. Faculty, Program coordinator). */
|
|
42
|
+
roles?: string[]
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
export interface PageHeaderProps {
|
|
@@ -40,7 +47,7 @@ export interface PageHeaderProps {
|
|
|
40
47
|
title: string
|
|
41
48
|
/** Short descriptor or date shown below the title (and below `accessInfo` when set) */
|
|
42
49
|
subtitle?: string
|
|
43
|
-
/** Layout preset — `collaboration` enables access line + face stack
|
|
50
|
+
/** Layout preset — `collaboration` enables access line + face stack ahead of `actions`. */
|
|
44
51
|
variant?: PageHeaderVariant
|
|
45
52
|
/**
|
|
46
53
|
* Role / access copy or badges — rendered between the title and subtitle when
|
|
@@ -51,9 +58,9 @@ export interface PageHeaderProps {
|
|
|
51
58
|
collaborators?: PageHeaderCollaborator[]
|
|
52
59
|
/** Max faces before a `+N` chip — default 4 */
|
|
53
60
|
collaboratorDisplayLimit?: number
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
/**
|
|
61
|
+
/** Opens the invite collaborators sheet when a face, overflow chip, or empty-state CTA is activated. */
|
|
62
|
+
onCollaboratorsOpen?: () => void
|
|
63
|
+
/** Label for the empty-roster header control — default **Add collaborator**. */
|
|
57
64
|
addCollaboratorLabel?: string
|
|
58
65
|
/** Optional slot for right-aligned actions (buttons, selectors, etc.) */
|
|
59
66
|
actions?: React.ReactNode
|
|
@@ -63,17 +70,38 @@ export interface PageHeaderProps {
|
|
|
63
70
|
showTitleBlock?: boolean
|
|
64
71
|
}
|
|
65
72
|
|
|
66
|
-
function
|
|
73
|
+
function PageHeaderCollaborationAccess({
|
|
67
74
|
people,
|
|
68
75
|
limit,
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
onOpenCollaborators,
|
|
77
|
+
addCollaboratorLabel,
|
|
71
78
|
}: {
|
|
72
79
|
people: PageHeaderCollaborator[]
|
|
73
80
|
limit: number
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
onOpenCollaborators?: () => void
|
|
82
|
+
addCollaboratorLabel: string
|
|
76
83
|
}) {
|
|
84
|
+
if (people.length === 0) {
|
|
85
|
+
return (
|
|
86
|
+
<div
|
|
87
|
+
role="group"
|
|
88
|
+
aria-label="People with access"
|
|
89
|
+
className="flex shrink-0 items-center"
|
|
90
|
+
>
|
|
91
|
+
<Button
|
|
92
|
+
type="button"
|
|
93
|
+
variant="outline"
|
|
94
|
+
size="lg"
|
|
95
|
+
onClick={onOpenCollaborators}
|
|
96
|
+
disabled={!onOpenCollaborators}
|
|
97
|
+
>
|
|
98
|
+
<i className="fa-light fa-user-plus" aria-hidden="true" />
|
|
99
|
+
{addCollaboratorLabel}
|
|
100
|
+
</Button>
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
77
105
|
const visible = people.slice(0, limit)
|
|
78
106
|
const overflow = Math.max(0, people.length - visible.length)
|
|
79
107
|
const names = people.map(p => p.name).join(", ")
|
|
@@ -84,17 +112,19 @@ function PageHeaderCollaborationFaces({
|
|
|
84
112
|
aria-label={names ? `People with access: ${names}` : "People with access"}
|
|
85
113
|
className="flex shrink-0 items-center gap-2 sm:gap-2.5"
|
|
86
114
|
>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
{
|
|
90
|
-
<
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
115
|
+
<div className="flex -space-x-2 ps-0.5">
|
|
116
|
+
{visible.map((c, index) => (
|
|
117
|
+
<Tooltip key={c.id}>
|
|
118
|
+
<TooltipTrigger asChild>
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
className="relative rounded-full ring-2 ring-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
122
|
+
style={{ zIndex: 10 + index }}
|
|
123
|
+
aria-label={`Open collaborators — ${c.name}`}
|
|
124
|
+
onClick={onOpenCollaborators}
|
|
125
|
+
disabled={!onOpenCollaborators}
|
|
126
|
+
>
|
|
127
|
+
<Avatar size="sm" shape="circle" className="pointer-events-none">
|
|
98
128
|
{c.imageUrl ? (
|
|
99
129
|
<AvatarImage src={c.imageUrl} alt="" referrerPolicy="no-referrer" />
|
|
100
130
|
) : null}
|
|
@@ -102,34 +132,23 @@ function PageHeaderCollaborationFaces({
|
|
|
102
132
|
{(c.initials ?? c.name.slice(0, 2)).toUpperCase()}
|
|
103
133
|
</AvatarFallback>
|
|
104
134
|
</Avatar>
|
|
105
|
-
</
|
|
106
|
-
|
|
107
|
-
</
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
aria-label={`${overflow} more people with access`}
|
|
113
|
-
>
|
|
114
|
-
+{overflow}
|
|
115
|
-
</div>
|
|
116
|
-
)}
|
|
117
|
-
</div>
|
|
118
|
-
)}
|
|
119
|
-
{onAdd ? (
|
|
120
|
-
<Tip side="bottom" label={addLabel}>
|
|
121
|
-
<Button
|
|
135
|
+
</button>
|
|
136
|
+
</TooltipTrigger>
|
|
137
|
+
<TooltipContent side="bottom">{c.name}</TooltipContent>
|
|
138
|
+
</Tooltip>
|
|
139
|
+
))}
|
|
140
|
+
{overflow > 0 && (
|
|
141
|
+
<button
|
|
122
142
|
type="button"
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
onClick={onAdd}
|
|
143
|
+
className="relative z-30 flex size-6 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-medium tabular-nums text-muted-foreground ring-2 ring-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:size-7"
|
|
144
|
+
aria-label={`Open collaborators — ${overflow} more people with access`}
|
|
145
|
+
onClick={onOpenCollaborators}
|
|
146
|
+
disabled={!onOpenCollaborators}
|
|
128
147
|
>
|
|
129
|
-
|
|
130
|
-
</
|
|
131
|
-
|
|
132
|
-
|
|
148
|
+
+{overflow}
|
|
149
|
+
</button>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
133
152
|
</div>
|
|
134
153
|
)
|
|
135
154
|
}
|
|
@@ -141,18 +160,16 @@ export function PageHeader({
|
|
|
141
160
|
accessInfo,
|
|
142
161
|
collaborators,
|
|
143
162
|
collaboratorDisplayLimit = 4,
|
|
144
|
-
|
|
145
|
-
addCollaboratorLabel =
|
|
163
|
+
onCollaboratorsOpen,
|
|
164
|
+
addCollaboratorLabel = COLLABORATION_HEADER_ADD_LABEL,
|
|
146
165
|
actions,
|
|
147
166
|
className,
|
|
148
167
|
showTitleBlock = true,
|
|
149
168
|
}: PageHeaderProps) {
|
|
150
169
|
const isCollaboration = variant === "collaboration"
|
|
151
170
|
const showAccess = Boolean(isCollaboration && accessInfo)
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
((collaborators && collaborators.length > 0) || Boolean(onAddCollaborator))
|
|
155
|
-
const showActionsColumn = Boolean(actions) || showFaceRail
|
|
171
|
+
const showCollaborationAccess = isCollaboration
|
|
172
|
+
const showActionsColumn = Boolean(actions) || showCollaborationAccess
|
|
156
173
|
|
|
157
174
|
return (
|
|
158
175
|
<div
|
|
@@ -183,12 +200,12 @@ export function PageHeader({
|
|
|
183
200
|
|
|
184
201
|
{showActionsColumn && (
|
|
185
202
|
<div className="flex flex-wrap items-center gap-2 sm:gap-3 shrink-0 sm:ms-auto sm:justify-end">
|
|
186
|
-
{
|
|
187
|
-
<
|
|
203
|
+
{showCollaborationAccess ? (
|
|
204
|
+
<PageHeaderCollaborationAccess
|
|
188
205
|
people={collaborators ?? []}
|
|
189
206
|
limit={collaboratorDisplayLimit}
|
|
190
|
-
|
|
191
|
-
|
|
207
|
+
onOpenCollaborators={onCollaboratorsOpen}
|
|
208
|
+
addCollaboratorLabel={addCollaboratorLabel}
|
|
192
209
|
/>
|
|
193
210
|
) : null}
|
|
194
211
|
{actions}
|
|
@@ -164,7 +164,7 @@ function BoardPhaseColumnHeader({
|
|
|
164
164
|
</button>
|
|
165
165
|
</DropdownMenuTrigger>
|
|
166
166
|
</Tip>
|
|
167
|
-
<DropdownMenuContent align="end"
|
|
167
|
+
<DropdownMenuContent align="end">
|
|
168
168
|
<div className="px-2 pt-2 pb-1">
|
|
169
169
|
<div className="relative">
|
|
170
170
|
<i
|
|
@@ -222,11 +222,11 @@ function BoardPhaseColumnHeader({
|
|
|
222
222
|
{menu.sortableColumns.map(col => (
|
|
223
223
|
<React.Fragment key={col.key}>
|
|
224
224
|
<DropdownMenuItem onClick={() => menu.onSortByField(col.key, "asc")}>
|
|
225
|
-
<i className="fa-light fa-arrow-up-
|
|
225
|
+
<i className="fa-light fa-arrow-up-a-z" aria-hidden="true" />
|
|
226
226
|
{col.label} — ascending
|
|
227
227
|
</DropdownMenuItem>
|
|
228
228
|
<DropdownMenuItem onClick={() => menu.onSortByField(col.key, "desc")}>
|
|
229
|
-
<i className="fa-light fa-arrow-down-
|
|
229
|
+
<i className="fa-light fa-arrow-down-a-z" aria-hidden="true" />
|
|
230
230
|
{col.label} — descending
|
|
231
231
|
</DropdownMenuItem>
|
|
232
232
|
</React.Fragment>
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
WeeksProgressCell,
|
|
19
19
|
} from "@/components/data-list-table-cells"
|
|
20
20
|
import { uniquePlacementFieldOptions, type Placement } from "@/lib/mock/placements"
|
|
21
|
+
import { formatDateUS } from "@/lib/date-filter"
|
|
21
22
|
import type { PlacementLifecycleTabId } from "@/lib/placement-lifecycle"
|
|
22
23
|
|
|
23
24
|
const COLUMN_SELECT: ColumnDef<Placement> = {
|
|
@@ -443,7 +444,7 @@ const PLACEMENT_COLUMNS_ONGOING: ColumnDef<Placement>[] = [
|
|
|
443
444
|
sortKey: "endDate",
|
|
444
445
|
filter: { type: "date", icon: "fa-calendar-days", operators: ["is", "is_not"] },
|
|
445
446
|
cell: (row) => (
|
|
446
|
-
<span className="text-sm text-foreground/80 tabular-nums whitespace-nowrap">{row.endDate}</span>
|
|
447
|
+
<span className="text-sm text-foreground/80 tabular-nums whitespace-nowrap">{formatDateUS(row.endDate)}</span>
|
|
447
448
|
),
|
|
448
449
|
},
|
|
449
450
|
{
|
|
@@ -454,7 +455,7 @@ const PLACEMENT_COLUMNS_ONGOING: ColumnDef<Placement>[] = [
|
|
|
454
455
|
sortable: true,
|
|
455
456
|
sortKey: "lastCheckin",
|
|
456
457
|
cell: (row) => (
|
|
457
|
-
<span className="text-sm text-foreground/80 whitespace-nowrap">{row.lastCheckin}</span>
|
|
458
|
+
<span className="text-sm text-foreground/80 whitespace-nowrap">{formatDateUS(row.lastCheckin)}</span>
|
|
458
459
|
),
|
|
459
460
|
},
|
|
460
461
|
COLUMN_ACTIONS,
|
|
@@ -13,24 +13,25 @@ import {
|
|
|
13
13
|
LIST_HUB_STATUS_TINT_NEUTRAL,
|
|
14
14
|
LIST_HUB_STATUS_TINT_SUCCESS,
|
|
15
15
|
LIST_HUB_STATUS_TINT_WARNING,
|
|
16
|
-
QUESTION_BANK_STATUS_BADGE_CLASS,
|
|
17
|
-
QUESTION_BANK_STATUS_ICON,
|
|
18
|
-
QUESTION_BANK_STATUS_LABEL,
|
|
19
16
|
} from "@/lib/list-status-badges"
|
|
17
|
+
import { formatDateUS } from "@/lib/date-filter"
|
|
20
18
|
import { BoardCardTwoLineBlock } from "@/components/data-views/board-card-primitives"
|
|
21
19
|
import {
|
|
22
20
|
ListPageBoardCard,
|
|
23
21
|
ListPageBoardCardAvatar,
|
|
24
|
-
ListPageBoardCardBadgeRow,
|
|
25
22
|
ListPageBoardCardBody,
|
|
26
23
|
ListPageBoardCardHeader,
|
|
27
24
|
ListPageBoardCardTitleRow,
|
|
28
25
|
} from "@/components/data-views/list-page-board-card"
|
|
29
|
-
import { ListHubStatusBadge } from "@/components/list-hub-status-badge"
|
|
30
26
|
import {
|
|
31
27
|
ListPageBoardTemplate,
|
|
32
28
|
type ListPageBoardColumnDef,
|
|
33
29
|
} from "@/components/data-views/list-page-board-template"
|
|
30
|
+
import {
|
|
31
|
+
QuestionBankFavoriteButton,
|
|
32
|
+
QUESTION_BANK_FAVORITE_HOVER_GROUP,
|
|
33
|
+
} from "@/components/question-bank-favorite-button"
|
|
34
|
+
import { cn } from "@/lib/utils"
|
|
34
35
|
|
|
35
36
|
const NEUTRAL_COUNT_BADGE = "bg-muted/90 text-foreground"
|
|
36
37
|
|
|
@@ -58,27 +59,6 @@ const TYPE_BADGE: Record<QuestionBankType, string> = {
|
|
|
58
59
|
short_answer: LIST_HUB_STATUS_TINT_WARNING,
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
const STATUS_BOARD_COLUMNS: ListPageBoardColumnDef<QuestionBankItem>[] = [
|
|
62
|
-
{
|
|
63
|
-
id: "published",
|
|
64
|
-
label: "Published",
|
|
65
|
-
description: "Live in bank",
|
|
66
|
-
filter: r => r.status === "published",
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
id: "draft",
|
|
70
|
-
label: "Draft",
|
|
71
|
-
description: "Work in progress",
|
|
72
|
-
filter: r => r.status === "draft",
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
id: "in_review",
|
|
76
|
-
label: "In review",
|
|
77
|
-
description: "Awaiting approval",
|
|
78
|
-
filter: r => r.status === "in_review",
|
|
79
|
-
},
|
|
80
|
-
]
|
|
81
|
-
|
|
82
62
|
const DIFF_ORDER: QuestionBankDifficulty[] = ["easy", "medium", "hard"]
|
|
83
63
|
const TYPE_ORDER: QuestionBankType[] = ["multiple_choice", "true_false", "short_answer"]
|
|
84
64
|
|
|
@@ -136,38 +116,45 @@ function useQuestionBankBoardModel(rows: QuestionBankItem[], groupByColumnKey: s
|
|
|
136
116
|
const { columns, badgeMap } = typeBoardColumns()
|
|
137
117
|
return { columns, badgeMap }
|
|
138
118
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
badgeMap: QUESTION_BANK_STATUS_BADGE_CLASS as Record<string, string>,
|
|
142
|
-
}
|
|
119
|
+
const { columns, badgeMap } = topicBoardColumns(rows)
|
|
120
|
+
return { columns, badgeMap }
|
|
143
121
|
}, [rows, groupByColumnKey])
|
|
144
122
|
}
|
|
145
123
|
|
|
146
|
-
function QuestionBankBoardCard({
|
|
124
|
+
function QuestionBankBoardCard({
|
|
125
|
+
row,
|
|
126
|
+
onToggleFavorite,
|
|
127
|
+
}: {
|
|
128
|
+
row: QuestionBankItem
|
|
129
|
+
onToggleFavorite: (row: QuestionBankItem) => void
|
|
130
|
+
}) {
|
|
147
131
|
const initials = initialsFromDisplayName(row.author)
|
|
148
132
|
return (
|
|
149
|
-
<ListPageBoardCard className="w-full">
|
|
133
|
+
<ListPageBoardCard className={cn(QUESTION_BANK_FAVORITE_HOVER_GROUP, "w-full")}>
|
|
150
134
|
<ListPageBoardCardHeader>
|
|
151
135
|
<ListPageBoardCardTitleRow
|
|
152
|
-
title={
|
|
153
|
-
|
|
154
|
-
|
|
136
|
+
title={(
|
|
137
|
+
<span className="block">
|
|
138
|
+
<span className="line-clamp-2">{row.stem}</span>
|
|
139
|
+
<span className="mt-0.5 block font-mono text-xs font-normal text-muted-foreground">
|
|
140
|
+
{row.questionId}
|
|
141
|
+
</span>
|
|
142
|
+
</span>
|
|
143
|
+
)}
|
|
144
|
+
trailing={(
|
|
145
|
+
<div className="flex shrink-0 items-start gap-1">
|
|
146
|
+
<QuestionBankFavoriteButton row={row} onToggleFavorite={onToggleFavorite} />
|
|
147
|
+
<ListPageBoardCardAvatar initials={initials} />
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
155
150
|
/>
|
|
156
|
-
<ListPageBoardCardBadgeRow>
|
|
157
|
-
<ListHubStatusBadge
|
|
158
|
-
surface="board"
|
|
159
|
-
label={QUESTION_BANK_STATUS_LABEL[row.status]}
|
|
160
|
-
tintClassName={QUESTION_BANK_STATUS_BADGE_CLASS[row.status]}
|
|
161
|
-
icon={QUESTION_BANK_STATUS_ICON[row.status]}
|
|
162
|
-
/>
|
|
163
|
-
</ListPageBoardCardBadgeRow>
|
|
164
151
|
<ListPageBoardCardBody>
|
|
165
152
|
<BoardCardTwoLineBlock
|
|
166
153
|
iconClass="fa-tag"
|
|
167
154
|
line1={row.topic}
|
|
168
155
|
line2={row.type.replace(/_/g, " ")}
|
|
169
156
|
/>
|
|
170
|
-
<BoardCardTwoLineBlock iconClass="fa-calendar-days" line1={row.updatedAt} line2="Updated" />
|
|
157
|
+
<BoardCardTwoLineBlock iconClass="fa-calendar-days" line1={formatDateUS(row.updatedAt)} line2="Updated" />
|
|
171
158
|
</ListPageBoardCardBody>
|
|
172
159
|
</ListPageBoardCardHeader>
|
|
173
160
|
</ListPageBoardCard>
|
|
@@ -175,7 +162,6 @@ function QuestionBankBoardCard({ row }: { row: QuestionBankItem }) {
|
|
|
175
162
|
}
|
|
176
163
|
|
|
177
164
|
export const QUESTION_BANK_BOARD_GROUP_OPTIONS = [
|
|
178
|
-
{ key: "status", label: "Status" },
|
|
179
165
|
{ key: "topic", label: "Topic" },
|
|
180
166
|
{ key: "difficulty", label: "Difficulty" },
|
|
181
167
|
{ key: "type", label: "Type" },
|
|
@@ -184,12 +170,14 @@ export const QUESTION_BANK_BOARD_GROUP_OPTIONS = [
|
|
|
184
170
|
export function QuestionBankBoardView({
|
|
185
171
|
rows,
|
|
186
172
|
groupByColumnKey,
|
|
173
|
+
onToggleFavorite,
|
|
187
174
|
}: {
|
|
188
175
|
rows: QuestionBankItem[]
|
|
189
176
|
groupByColumnKey: string
|
|
177
|
+
onToggleFavorite: (row: QuestionBankItem) => void
|
|
190
178
|
}) {
|
|
191
179
|
const allowed = QUESTION_BANK_BOARD_GROUP_OPTIONS.some(o => o.key === groupByColumnKey)
|
|
192
|
-
const key = allowed ? groupByColumnKey : "
|
|
180
|
+
const key = allowed ? groupByColumnKey : "topic"
|
|
193
181
|
const { columns, badgeMap } = useQuestionBankBoardModel(rows, key)
|
|
194
182
|
|
|
195
183
|
return (
|
|
@@ -199,7 +187,7 @@ export function QuestionBankBoardView({
|
|
|
199
187
|
getRowKey={r => r.id}
|
|
200
188
|
columnCountBadgeClassName={badgeMap}
|
|
201
189
|
emptyColumnLabel="No questions"
|
|
202
|
-
renderCard={row => <QuestionBankBoardCard row={row} />}
|
|
190
|
+
renderCard={row => <QuestionBankBoardCard row={row} onToggleFavorite={onToggleFavorite} />}
|
|
203
191
|
/>
|
|
204
192
|
)
|
|
205
193
|
}
|