@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.
Files changed (125) 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 +1 -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/AGENTS.md +82 -27
  28. package/template/app/(app)/examples/page.tsx +2 -1
  29. package/template/app/(app)/help/page.tsx +6 -0
  30. package/template/app/(app)/layout.tsx +7 -4
  31. package/template/app/(app)/question-bank/find/page.tsx +12 -0
  32. package/template/app/(app)/question-bank/layout.tsx +46 -0
  33. package/template/app/(app)/question-bank/library/page.tsx +11 -0
  34. package/template/app/(app)/question-bank/list/page.tsx +12 -0
  35. package/template/app/(app)/question-bank/page.tsx +4 -3
  36. package/template/app/globals.css +1 -2
  37. package/template/components/app-sidebar.tsx +51 -13
  38. package/template/components/ask-leo-composer.tsx +173 -45
  39. package/template/components/ask-leo-sidebar.tsx +9 -1
  40. package/template/components/chart-area-interactive.tsx +3 -13
  41. package/template/components/charts-overview.tsx +33 -6
  42. package/template/components/collaboration-access-flow.tsx +144 -0
  43. package/template/components/compliance-page-header.tsx +1 -1
  44. package/template/components/compliance-table.tsx +2 -2
  45. package/template/components/dashboard-tabs.tsx +4 -3
  46. package/template/components/data-list-table-cells.tsx +1 -1
  47. package/template/components/data-list-table.tsx +1 -1
  48. package/template/components/data-table/index.tsx +5 -5
  49. package/template/components/data-table/use-table-state.ts +18 -2
  50. package/template/components/data-view-dashboard-charts-compliance.tsx +8 -5
  51. package/template/components/data-view-dashboard-charts-team.tsx +8 -5
  52. package/template/components/data-view-dashboard-charts.tsx +62 -227
  53. package/template/components/dedicated-search-recents.tsx +96 -0
  54. package/template/components/dedicated-search-url-composer.tsx +112 -0
  55. package/template/components/getting-started.tsx +1 -1
  56. package/template/components/hub-tree-panel-view.tsx +10 -26
  57. package/template/components/invite-collaborators-drawer.tsx +453 -0
  58. package/template/components/key-metrics.tsx +54 -8
  59. package/template/components/nav-documents.tsx +1 -1
  60. package/template/components/new-placement-form.tsx +3 -3
  61. package/template/components/page-header.tsx +76 -59
  62. package/template/components/placements-board-view.tsx +3 -3
  63. package/template/components/placements-page-header.tsx +1 -1
  64. package/template/components/placements-table-columns.tsx +3 -2
  65. package/template/components/product-switcher.tsx +0 -1
  66. package/template/components/question-bank-board-view.tsx +35 -47
  67. package/template/components/question-bank-client.tsx +293 -81
  68. package/template/components/question-bank-dashboard-charts.tsx +174 -0
  69. package/template/components/question-bank-favorite-button.tsx +46 -0
  70. package/template/components/question-bank-hub-client.tsx +436 -0
  71. package/template/components/question-bank-list-view.tsx +26 -19
  72. package/template/components/question-bank-new-folder-sheet.tsx +56 -42
  73. package/template/components/question-bank-os-folder-view.tsx +3 -14
  74. package/template/components/question-bank-page-header.tsx +85 -53
  75. package/template/components/question-bank-panel-activator.tsx +3 -4
  76. package/template/components/question-bank-secondary-nav.tsx +523 -65
  77. package/template/components/question-bank-table.tsx +125 -343
  78. package/template/components/secondary-panel.tsx +130 -63
  79. package/template/components/settings-client.tsx +3 -1
  80. package/template/components/sidebar-shell.tsx +2 -0
  81. package/template/components/sites-all-client.tsx +1 -1
  82. package/template/components/sites-table.tsx +1 -1
  83. package/template/components/system-banner-slot.tsx +2 -1
  84. package/template/components/table-properties/drawer.tsx +3 -3
  85. package/template/components/table-properties/sort-card.tsx +1 -1
  86. package/template/components/team-page-header.tsx +1 -1
  87. package/template/components/team-table.tsx +8 -4
  88. package/template/components/templates/dedicated-search-landing-template.tsx +58 -0
  89. package/template/components/templates/dedicated-search-results-template.tsx +19 -0
  90. package/template/components/templates/discovery-hub-template.tsx +273 -0
  91. package/template/components/templates/list-page.tsx +11 -4
  92. package/template/components/templates/nested-secondary-panel-shell.tsx +57 -0
  93. package/template/components/templates/secondary-panel-hub-template.tsx +54 -0
  94. package/template/docs/card-vs-rows-pattern.md +36 -0
  95. package/template/docs/collaboration-access-pattern.md +114 -0
  96. package/template/docs/data-views-pattern.md +12 -4
  97. package/template/docs/drawer-vs-dialog-pattern.md +50 -0
  98. package/template/docs/kpi-strip-max-four-pattern.md +29 -0
  99. package/template/docs/kpi-trend-pattern.md +43 -0
  100. package/template/fontawesome-subset.manifest.json +2 -2
  101. package/template/hooks/use-location-hash.ts +14 -8
  102. package/template/hooks/use-secondary-panel-hub-nav.ts +98 -0
  103. package/template/lib/ask-leo-route-context.ts +24 -0
  104. package/template/lib/collaborator-access.ts +92 -0
  105. package/template/lib/command-menu-config.ts +8 -1
  106. package/template/lib/command-menu-search-data.ts +11 -8
  107. package/template/lib/data-list-display-options.ts +1 -1
  108. package/template/lib/data-view-dashboard-placements-layout.ts +215 -0
  109. package/template/lib/date-filter.ts +1 -0
  110. package/template/lib/dedicated-search-recents.ts +76 -0
  111. package/template/lib/dedicated-search-url.ts +23 -0
  112. package/template/lib/discovery-hub.ts +15 -0
  113. package/template/lib/list-status-badges.ts +1 -21
  114. package/template/lib/mock/navigation.tsx +4 -2
  115. package/template/lib/mock/placements.ts +9 -9
  116. package/template/lib/mock/question-bank-folders.ts +7 -0
  117. package/template/lib/mock/question-bank-header-collaborators.ts +45 -5
  118. package/template/lib/mock/question-bank-inspector.ts +1 -2
  119. package/template/lib/mock/question-bank-kpi.ts +38 -26
  120. package/template/lib/mock/question-bank.ts +43 -16
  121. package/template/lib/question-bank-dedicated-search.ts +19 -0
  122. package/template/lib/question-bank-hub-search.ts +90 -0
  123. package/template/lib/question-bank-nav.ts +322 -6
  124. package/template/lib/question-bank-recent-searches.ts +22 -0
  125. 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 + stacked collaborator faces
10
- * and an invite control ahead of the primary `actions` slot (Question bank pattern).
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 + invite ahead of `actions`. */
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
- /** Outline control beside the stack e.g. open share / invite dialog */
55
- onAddCollaborator?: () => void
56
- /** Accessible name for the invite control — default “Invite people” */
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 PageHeaderCollaborationFaces({
73
+ function PageHeaderCollaborationAccess({
67
74
  people,
68
75
  limit,
69
- addLabel,
70
- onAdd,
76
+ onOpenCollaborators,
77
+ addCollaboratorLabel,
71
78
  }: {
72
79
  people: PageHeaderCollaborator[]
73
80
  limit: number
74
- addLabel: string
75
- onAdd?: () => void
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
- {visible.length > 0 && (
88
- <div className="flex -space-x-2 ps-0.5">
89
- {visible.map((c, index) => (
90
- <Tooltip key={c.id}>
91
- <TooltipTrigger asChild>
92
- <Avatar
93
- size="sm"
94
- shape="circle"
95
- className="relative ring-2 ring-background"
96
- style={{ zIndex: 10 + index }}
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
- </TooltipTrigger>
106
- <TooltipContent side="bottom">{c.name}</TooltipContent>
107
- </Tooltip>
108
- ))}
109
- {overflow > 0 && (
110
- <div
111
- 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 sm:size-7"
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
- variant="outline"
124
- size="icon"
125
- className="size-8 min-h-8 min-w-8 shrink-0 rounded-full border-dashed"
126
- aria-label={addLabel}
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
- <i className="fa-light fa-user-plus text-sm" aria-hidden="true" />
130
- </Button>
131
- </Tip>
132
- ) : null}
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
- onAddCollaborator,
145
- addCollaboratorLabel = "Invite people",
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 showFaceRail =
153
- isCollaboration &&
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
- {showFaceRail ? (
187
- <PageHeaderCollaborationFaces
203
+ {showCollaborationAccess ? (
204
+ <PageHeaderCollaborationAccess
188
205
  people={collaborators ?? []}
189
206
  limit={collaboratorDisplayLimit}
190
- addLabel={addCollaboratorLabel}
191
- onAdd={onAddCollaborator}
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" className="min-w-44">
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-az" aria-hidden="true" />
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-az" aria-hidden="true" />
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>
@@ -131,7 +131,7 @@ export function PlacementsPageHeader({
131
131
  </Button>
132
132
  </DropdownMenuTrigger>
133
133
  </Tip>
134
- <DropdownMenuContent align="end" className="w-52">
134
+ <DropdownMenuContent align="end">
135
135
  <DropdownMenuItem
136
136
  shortcut="⌘⇧E"
137
137
  onSelect={() => {
@@ -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,
@@ -79,7 +79,6 @@ export function ProductSwitcher() {
79
79
  </DropdownMenuTrigger>
80
80
 
81
81
  <DropdownMenuContent
82
- className="w-56"
83
82
  align="start"
84
83
  side="bottom"
85
84
  sideOffset={4}
@@ -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
- return {
140
- columns: STATUS_BOARD_COLUMNS,
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({ row }: { row: QuestionBankItem }) {
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={row.stem}
153
- titleClassName="line-clamp-2"
154
- trailing={<ListPageBoardCardAvatar initials={initials} />}
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 : "status"
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
  }