@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.
- 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 +4 -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/.nvmrc +1 -1
- 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 +1 -2
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import Link from "next/link"
|
|
5
|
+
import { useRouter } from "next/navigation"
|
|
6
|
+
|
|
7
|
+
import { OsFolderGlyph } from "@/components/data-views/os-folder-glyph"
|
|
8
|
+
import { AskLeoComposer } from "@/components/ask-leo-composer"
|
|
9
|
+
import { useAskLeo, useAskLeoPageContext } from "@/components/ask-leo-sidebar"
|
|
10
|
+
import { PrimaryPageTemplate } from "@/components/templates/primary-page-template"
|
|
11
|
+
import { Button } from "@/components/ui/button"
|
|
12
|
+
import { Shortcut } from "@/components/ui/dropdown-menu"
|
|
13
|
+
import { Kbd, KbdGroup } from "@/components/ui/kbd"
|
|
14
|
+
import { Tip } from "@/components/ui/tip"
|
|
15
|
+
import { useAltKeyLabel, useModKeyLabel } from "@/hooks/use-mod-key-label"
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_QUESTION_BANK_FOLDERS,
|
|
18
|
+
type QuestionBankFolder,
|
|
19
|
+
type QuestionBankFolderColorKey,
|
|
20
|
+
} from "@/lib/mock/question-bank-folders"
|
|
21
|
+
import { QUESTION_BANK_ITEMS, type QuestionBankItem } from "@/lib/mock/question-bank"
|
|
22
|
+
import { QUESTION_BANK_HUB_ASK_LEO_PROMPTS } from "@/lib/question-bank-hub-search"
|
|
23
|
+
import {
|
|
24
|
+
QUESTION_BANK_LIBRARY_PATH,
|
|
25
|
+
QUESTION_BANK_NAV_MY_AUTHOR,
|
|
26
|
+
questionBankNavHref,
|
|
27
|
+
} from "@/lib/question-bank-nav"
|
|
28
|
+
import { cn } from "@/lib/utils"
|
|
29
|
+
|
|
30
|
+
const NEW_QUESTION_PROMPT =
|
|
31
|
+
"Help me create a new assessment question. Start by asking for topic, item type, difficulty, and learning objective, then draft the stem, answer choices, and rationale."
|
|
32
|
+
|
|
33
|
+
const TEMPLATE_PROMPT =
|
|
34
|
+
"Walk me through choosing a question template (MCQ, OSCE, short answer, true/false) and produce a starter item with stem, options, rationale, and tags."
|
|
35
|
+
|
|
36
|
+
const IMPORT_PROMPT =
|
|
37
|
+
"Guide me through importing assessment questions in bulk. Ask about source format (CSV, QTI, copy/paste), then outline what columns and mappings I need."
|
|
38
|
+
|
|
39
|
+
/** Rotating example queries — read like something a user would actually type into search. */
|
|
40
|
+
const HUB_COMPOSER_PLACEHOLDERS = [
|
|
41
|
+
"all the questions I used in last year's assessment",
|
|
42
|
+
"what did we run for the spring OSCE checkout?",
|
|
43
|
+
"Clinical folder, cardiology tagged, for third-year summative",
|
|
44
|
+
"find QB-2024-0012 and anything like it",
|
|
45
|
+
"that anemia vignette from the shelf-style block",
|
|
46
|
+
"MCQs I still need before next week's diabetes station",
|
|
47
|
+
] as const
|
|
48
|
+
|
|
49
|
+
interface ScopeChip {
|
|
50
|
+
id: string
|
|
51
|
+
label: string
|
|
52
|
+
href: string
|
|
53
|
+
count: number
|
|
54
|
+
folderGlyph: {
|
|
55
|
+
colorKey: QuestionBankFolderColorKey
|
|
56
|
+
icon: string
|
|
57
|
+
variant?: "solid" | "outline"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface CreateTile {
|
|
62
|
+
id: string
|
|
63
|
+
label: string
|
|
64
|
+
description: string
|
|
65
|
+
icon: string
|
|
66
|
+
iconTint: string
|
|
67
|
+
onClick: () => void
|
|
68
|
+
badge?: "AI" | null
|
|
69
|
+
shortcutKeys?: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface FolderTile extends QuestionBankFolder {
|
|
73
|
+
count: number
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function buildScopeChips(items: QuestionBankItem[]): ScopeChip[] {
|
|
77
|
+
const mine = items.filter(
|
|
78
|
+
i => i.author === QUESTION_BANK_NAV_MY_AUTHOR || i.createdBy === QUESTION_BANK_NAV_MY_AUTHOR,
|
|
79
|
+
).length
|
|
80
|
+
return [
|
|
81
|
+
{
|
|
82
|
+
id: "all",
|
|
83
|
+
label: "All",
|
|
84
|
+
count: items.length,
|
|
85
|
+
href: QUESTION_BANK_LIBRARY_PATH,
|
|
86
|
+
folderGlyph: { colorKey: "muted", icon: "fa-layer-group", variant: "outline" },
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "my",
|
|
90
|
+
label: "Mine",
|
|
91
|
+
count: mine,
|
|
92
|
+
href: questionBankNavHref({ scope: "my" }),
|
|
93
|
+
folderGlyph: { colorKey: "brand", icon: "fa-user" },
|
|
94
|
+
},
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function buildFolderTiles(items: QuestionBankItem[], folders: QuestionBankFolder[]): FolderTile[] {
|
|
99
|
+
const byFolder = new Map<string, number>()
|
|
100
|
+
for (const i of items) byFolder.set(i.folderId, (byFolder.get(i.folderId) ?? 0) + 1)
|
|
101
|
+
return folders
|
|
102
|
+
.filter(f => f.parentId === null)
|
|
103
|
+
.map(f => ({ ...f, count: byFolder.get(f.id) ?? 0 }))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Static derivations of immutable mock data — computed once at module load,
|
|
107
|
+
// not per render of the hub. Re-derive only if the underlying mock arrays change.
|
|
108
|
+
const HUB_SCOPES = buildScopeChips(QUESTION_BANK_ITEMS)
|
|
109
|
+
const HUB_FOLDER_TILES = buildFolderTiles(QUESTION_BANK_ITEMS, DEFAULT_QUESTION_BANK_FOLDERS)
|
|
110
|
+
|
|
111
|
+
function isMineQuestionBankItem(i: QuestionBankItem): boolean {
|
|
112
|
+
return i.author === QUESTION_BANK_NAV_MY_AUTHOR || i.createdBy === QUESTION_BANK_NAV_MY_AUTHOR
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const HUB_MINE_RECENTS = [...QUESTION_BANK_ITEMS]
|
|
116
|
+
.filter(isMineQuestionBankItem)
|
|
117
|
+
.sort((a, b) => (a.updatedAt < b.updatedAt ? 1 : -1))
|
|
118
|
+
.slice(0, 3)
|
|
119
|
+
|
|
120
|
+
const HUB_ASK_LEO_PAGE_CONTEXT = {
|
|
121
|
+
title: "Question hub",
|
|
122
|
+
description:
|
|
123
|
+
"Browse and organize assessment items with AI-assisted workflows. The hub search field opens discovery results on `/question-bank/find` with your wording applied to the list; use the library’s Search in the sidebar for `/question-bank/list`. Pick a suggestion below when you want a full Ask Leo thread.",
|
|
124
|
+
suggestions: [...QUESTION_BANK_HUB_ASK_LEO_PROMPTS],
|
|
125
|
+
data: { surface: "question-bank-discovery-hub" as const },
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function formatRelativeDate(iso: string): string {
|
|
129
|
+
const now = Date.now()
|
|
130
|
+
const t = new Date(iso).getTime()
|
|
131
|
+
if (Number.isNaN(t)) return iso
|
|
132
|
+
const diffDays = Math.round((now - t) / 86_400_000)
|
|
133
|
+
if (diffDays <= 0) return "today"
|
|
134
|
+
if (diffDays === 1) return "yesterday"
|
|
135
|
+
if (diffDays < 7) return `${diffDays}d ago`
|
|
136
|
+
if (diffDays < 30) return `${Math.round(diffDays / 7)}w ago`
|
|
137
|
+
if (diffDays < 365) return `${Math.round(diffDays / 30)}mo ago`
|
|
138
|
+
return `${Math.round(diffDays / 365)}y ago`
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function QuestionBankHubClient() {
|
|
142
|
+
const router = useRouter()
|
|
143
|
+
const { openWithPrompt, toggle } = useAskLeo()
|
|
144
|
+
const mod = useModKeyLabel()
|
|
145
|
+
const alt = useAltKeyLabel()
|
|
146
|
+
|
|
147
|
+
const [hubComposerValue, setHubComposerValue] = React.useState("")
|
|
148
|
+
const [hubComposerExpanded, setHubComposerExpanded] = React.useState(false)
|
|
149
|
+
|
|
150
|
+
const scopes = HUB_SCOPES
|
|
151
|
+
const folderTiles = HUB_FOLDER_TILES
|
|
152
|
+
const recents = HUB_MINE_RECENTS
|
|
153
|
+
|
|
154
|
+
useAskLeoPageContext(HUB_ASK_LEO_PAGE_CONTEXT)
|
|
155
|
+
|
|
156
|
+
const sendLeoSuggestion = React.useCallback(
|
|
157
|
+
(prompt: string) => {
|
|
158
|
+
openWithPrompt(prompt)
|
|
159
|
+
},
|
|
160
|
+
[openWithPrompt],
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
const openCreateQuestion = React.useCallback(() => {
|
|
164
|
+
openWithPrompt(NEW_QUESTION_PROMPT)
|
|
165
|
+
}, [openWithPrompt])
|
|
166
|
+
|
|
167
|
+
const onHubComposerSubmit = React.useCallback(
|
|
168
|
+
(message: string) => {
|
|
169
|
+
router.push(questionBankNavHref({ scope: "all", q: message.trim(), hubFind: true }))
|
|
170
|
+
},
|
|
171
|
+
[router],
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
const createShortcut = `${mod}${alt}N`
|
|
175
|
+
|
|
176
|
+
const hubFolderBrowserTileClass = cn(
|
|
177
|
+
"flex w-full flex-col items-center gap-2 rounded-xl border border-transparent p-3 text-center transition-colors",
|
|
178
|
+
"hover:border-border/80 hover:bg-muted/35",
|
|
179
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
const createTiles = React.useMemo<CreateTile[]>(
|
|
183
|
+
() => [
|
|
184
|
+
{
|
|
185
|
+
id: "scratch",
|
|
186
|
+
label: "From scratch",
|
|
187
|
+
description: "Start with an empty editor and build the item by hand.",
|
|
188
|
+
icon: "fa-pen-to-square",
|
|
189
|
+
iconTint: "bg-brand/15 text-brand",
|
|
190
|
+
onClick: openCreateQuestion,
|
|
191
|
+
shortcutKeys: createShortcut,
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: "ask-leo",
|
|
195
|
+
label: "Draft with Ask Leo",
|
|
196
|
+
description: "Describe the outcome and let Leo propose stem, options, and rationale.",
|
|
197
|
+
icon: "fa-star-christmas",
|
|
198
|
+
iconTint: "bg-brand/15 text-brand",
|
|
199
|
+
badge: "AI",
|
|
200
|
+
onClick: openCreateQuestion,
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: "template",
|
|
204
|
+
label: "From a template",
|
|
205
|
+
description: "Pick MCQ, OSCE, short answer or true/false — Leo fills the scaffold.",
|
|
206
|
+
icon: "fa-clone",
|
|
207
|
+
iconTint: "bg-sky-500/15 text-sky-700 dark:text-sky-300",
|
|
208
|
+
onClick: () => sendLeoSuggestion(TEMPLATE_PROMPT),
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: "import",
|
|
212
|
+
label: "Import in bulk",
|
|
213
|
+
description: "Bring in CSV, QTI, or paste from another tool — Leo will map the columns.",
|
|
214
|
+
icon: "fa-file-import",
|
|
215
|
+
iconTint: "bg-muted text-muted-foreground",
|
|
216
|
+
onClick: () => sendLeoSuggestion(IMPORT_PROMPT),
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
[openCreateQuestion, sendLeoSuggestion, createShortcut],
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<PrimaryPageTemplate
|
|
224
|
+
siteHeader={{
|
|
225
|
+
breadcrumbs: [{ label: "Dashboard", href: "/dashboard" }],
|
|
226
|
+
title: "Question hub",
|
|
227
|
+
}}
|
|
228
|
+
maxWidthClassName="max-w-5xl"
|
|
229
|
+
contentClassName="px-4 py-8 md:px-6 md:py-10"
|
|
230
|
+
>
|
|
231
|
+
<Shortcut keys={createShortcut} onInvoke={openCreateQuestion} />
|
|
232
|
+
{/* ⌘⌥K (Ask Leo toggle) is bound globally in AskLeoProvider — do not double-bind here. */}
|
|
233
|
+
|
|
234
|
+
<div className="flex min-h-0 flex-1 flex-col gap-10">
|
|
235
|
+
<header>
|
|
236
|
+
<h1 className="text-2xl font-semibold tracking-tight text-foreground md:text-3xl" style={{ fontFamily: "var(--font-heading)" }}>
|
|
237
|
+
Question hub
|
|
238
|
+
</h1>
|
|
239
|
+
</header>
|
|
240
|
+
|
|
241
|
+
<div className="min-w-0">
|
|
242
|
+
<p className="sr-only">
|
|
243
|
+
Example searches rotate in the field. Type your own request in plain language, then press Enter to open
|
|
244
|
+
the library with that AI search applied to the question list. This control does not open Ask Leo.
|
|
245
|
+
</p>
|
|
246
|
+
<div
|
|
247
|
+
className={cn(
|
|
248
|
+
"min-w-0 max-w-full border border-[color:var(--control-border)] bg-card shadow-sm transition-[border-radius,padding,box-shadow] duration-200 ease-out",
|
|
249
|
+
hubComposerExpanded ? "rounded-2xl p-1.5 shadow-md" : "rounded-full px-1 py-1",
|
|
250
|
+
)}
|
|
251
|
+
>
|
|
252
|
+
<AskLeoComposer
|
|
253
|
+
value={hubComposerValue}
|
|
254
|
+
onChange={setHubComposerValue}
|
|
255
|
+
onSubmit={onHubComposerSubmit}
|
|
256
|
+
onExpandedChange={setHubComposerExpanded}
|
|
257
|
+
animatedPlaceholders={[...HUB_COMPOSER_PLACEHOLDERS]}
|
|
258
|
+
animatedPlaceholderIntervalMs={4800}
|
|
259
|
+
animatedPlaceholderMaxLines={2}
|
|
260
|
+
leadingSlot="ai-mark"
|
|
261
|
+
inputLabel="AI search"
|
|
262
|
+
submitAppearance="search"
|
|
263
|
+
submitButtonAriaLabel="Run AI search"
|
|
264
|
+
placeholder="Search the bank…"
|
|
265
|
+
className="[&_form>div]:rounded-none [&_form>div]:border-0 [&_form>div]:bg-transparent [&_form>div]:shadow-none"
|
|
266
|
+
/>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
{recents.length > 0 && (
|
|
271
|
+
<section aria-labelledby="qb-recent" className="space-y-4">
|
|
272
|
+
<div className="flex items-baseline justify-between gap-3">
|
|
273
|
+
<h2 id="qb-recent" className="text-base font-semibold tracking-tight text-foreground">
|
|
274
|
+
Continue where you left off
|
|
275
|
+
</h2>
|
|
276
|
+
<Link
|
|
277
|
+
href={questionBankNavHref({ scope: "my" })}
|
|
278
|
+
className="text-sm font-medium text-muted-foreground transition-colors hover:text-foreground"
|
|
279
|
+
>
|
|
280
|
+
View all
|
|
281
|
+
<i className="fa-light fa-arrow-right ms-1.5 text-xs" aria-hidden="true" />
|
|
282
|
+
</Link>
|
|
283
|
+
</div>
|
|
284
|
+
<ul className="grid gap-3 md:grid-cols-3" role="list">
|
|
285
|
+
{recents.map(item => (
|
|
286
|
+
<li key={item.id}>
|
|
287
|
+
<Link
|
|
288
|
+
href={questionBankNavHref({ scope: "my" })}
|
|
289
|
+
className="group flex h-full flex-col gap-3 rounded-xl border border-border bg-card p-4 transition hover:border-interactive-hover hover:bg-interactive-hover/30 hover:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
290
|
+
>
|
|
291
|
+
<div className="flex items-center gap-2">
|
|
292
|
+
<span className="truncate font-mono text-xs text-muted-foreground">{item.questionId}</span>
|
|
293
|
+
<span className="truncate text-xs text-muted-foreground">{item.topic}</span>
|
|
294
|
+
</div>
|
|
295
|
+
<p className="line-clamp-3 text-sm font-medium text-foreground group-hover:text-foreground">
|
|
296
|
+
{item.stem}
|
|
297
|
+
</p>
|
|
298
|
+
<div className="mt-auto flex justify-end text-xs text-muted-foreground">
|
|
299
|
+
<span className="shrink-0">{formatRelativeDate(item.updatedAt)}</span>
|
|
300
|
+
</div>
|
|
301
|
+
</Link>
|
|
302
|
+
</li>
|
|
303
|
+
))}
|
|
304
|
+
</ul>
|
|
305
|
+
</section>
|
|
306
|
+
)}
|
|
307
|
+
|
|
308
|
+
<section aria-labelledby="qb-browse" className="space-y-4">
|
|
309
|
+
<div className="flex items-baseline justify-between gap-3">
|
|
310
|
+
<h2 id="qb-browse" className="text-base font-semibold tracking-tight text-foreground">
|
|
311
|
+
Browse the library
|
|
312
|
+
</h2>
|
|
313
|
+
<Link
|
|
314
|
+
href={QUESTION_BANK_LIBRARY_PATH}
|
|
315
|
+
className="text-sm font-medium text-muted-foreground transition-colors hover:text-foreground"
|
|
316
|
+
>
|
|
317
|
+
Open full library
|
|
318
|
+
<i className="fa-light fa-arrow-right ms-1.5 text-xs" aria-hidden="true" />
|
|
319
|
+
</Link>
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
<div
|
|
323
|
+
className="grid w-full grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6"
|
|
324
|
+
role="list"
|
|
325
|
+
aria-label="Library scopes and top folders"
|
|
326
|
+
>
|
|
327
|
+
{scopes.map(s => (
|
|
328
|
+
<div key={s.id} role="listitem" className="min-w-0">
|
|
329
|
+
<Link
|
|
330
|
+
href={s.href}
|
|
331
|
+
className={hubFolderBrowserTileClass}
|
|
332
|
+
>
|
|
333
|
+
<OsFolderGlyph
|
|
334
|
+
colorKey={s.folderGlyph.colorKey}
|
|
335
|
+
icon={s.folderGlyph.icon}
|
|
336
|
+
size="md"
|
|
337
|
+
variant={s.folderGlyph.variant ?? "solid"}
|
|
338
|
+
/>
|
|
339
|
+
<span className="line-clamp-2 w-full text-xs font-medium text-foreground">{s.label}</span>
|
|
340
|
+
<span className="text-[11px] leading-snug text-muted-foreground tabular-nums">
|
|
341
|
+
{s.count} {s.count === 1 ? "question" : "questions"}
|
|
342
|
+
</span>
|
|
343
|
+
</Link>
|
|
344
|
+
</div>
|
|
345
|
+
))}
|
|
346
|
+
{folderTiles.map(f => (
|
|
347
|
+
<div key={f.id} role="listitem" className="min-w-0">
|
|
348
|
+
<Link
|
|
349
|
+
href={questionBankNavHref({ scope: "folder", folderId: f.id })}
|
|
350
|
+
className={hubFolderBrowserTileClass}
|
|
351
|
+
>
|
|
352
|
+
<OsFolderGlyph colorKey={f.colorKey} icon={f.icon} size="md" />
|
|
353
|
+
<span className="line-clamp-2 w-full text-xs font-medium text-foreground">{f.name}</span>
|
|
354
|
+
<span className="text-[11px] leading-snug text-muted-foreground tabular-nums">
|
|
355
|
+
{f.count} {f.count === 1 ? "question" : "questions"}
|
|
356
|
+
</span>
|
|
357
|
+
</Link>
|
|
358
|
+
</div>
|
|
359
|
+
))}
|
|
360
|
+
</div>
|
|
361
|
+
</section>
|
|
362
|
+
|
|
363
|
+
<section aria-labelledby="qb-create" className="space-y-4">
|
|
364
|
+
<div className="flex flex-wrap items-baseline justify-between gap-3">
|
|
365
|
+
<h2 id="qb-create" className="text-base font-semibold tracking-tight text-foreground">
|
|
366
|
+
Create a question
|
|
367
|
+
</h2>
|
|
368
|
+
<Tip
|
|
369
|
+
label={(
|
|
370
|
+
<span className="inline-flex items-center gap-1.5">
|
|
371
|
+
Ask Leo
|
|
372
|
+
<KbdGroup>
|
|
373
|
+
<Kbd>{mod}</Kbd>
|
|
374
|
+
<Kbd>{alt}</Kbd>
|
|
375
|
+
<Kbd>K</Kbd>
|
|
376
|
+
</KbdGroup>
|
|
377
|
+
</span>
|
|
378
|
+
)}
|
|
379
|
+
>
|
|
380
|
+
<Button type="button" variant="ghost" size="sm" onClick={toggle}>
|
|
381
|
+
<i className="fa-duotone fa-solid fa-star-christmas text-brand" aria-hidden="true" />
|
|
382
|
+
Open Ask Leo
|
|
383
|
+
</Button>
|
|
384
|
+
</Tip>
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
<ul className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4" role="list">
|
|
388
|
+
{createTiles.map(tile => (
|
|
389
|
+
<li key={tile.id}>
|
|
390
|
+
<button
|
|
391
|
+
type="button"
|
|
392
|
+
onClick={tile.onClick}
|
|
393
|
+
className="group flex h-full w-full flex-col items-start gap-3 rounded-xl border border-border bg-card p-4 text-left transition hover:border-interactive-hover hover:bg-interactive-hover/30 hover:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
394
|
+
>
|
|
395
|
+
<span className="flex w-full items-center justify-between">
|
|
396
|
+
<span
|
|
397
|
+
className={cn(
|
|
398
|
+
"inline-flex h-10 w-10 items-center justify-center rounded-lg",
|
|
399
|
+
tile.iconTint,
|
|
400
|
+
)}
|
|
401
|
+
aria-hidden="true"
|
|
402
|
+
>
|
|
403
|
+
<i
|
|
404
|
+
className={cn(
|
|
405
|
+
tile.badge === "AI" ? "fa-duotone fa-solid" : "fa-light",
|
|
406
|
+
tile.icon,
|
|
407
|
+
"text-lg",
|
|
408
|
+
)}
|
|
409
|
+
/>
|
|
410
|
+
</span>
|
|
411
|
+
{tile.badge === "AI" && (
|
|
412
|
+
<span className="inline-flex items-center rounded-full bg-brand/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-brand">
|
|
413
|
+
AI
|
|
414
|
+
</span>
|
|
415
|
+
)}
|
|
416
|
+
</span>
|
|
417
|
+
<span className="space-y-1">
|
|
418
|
+
<span className="block text-sm font-semibold text-foreground">{tile.label}</span>
|
|
419
|
+
<span className="block text-xs leading-relaxed text-muted-foreground">
|
|
420
|
+
{tile.description}
|
|
421
|
+
</span>
|
|
422
|
+
</span>
|
|
423
|
+
{tile.shortcutKeys && (
|
|
424
|
+
<KbdGroup className="mt-auto">
|
|
425
|
+
<Kbd variant="bare">{tile.shortcutKeys}</Kbd>
|
|
426
|
+
</KbdGroup>
|
|
427
|
+
)}
|
|
428
|
+
</button>
|
|
429
|
+
</li>
|
|
430
|
+
))}
|
|
431
|
+
</ul>
|
|
432
|
+
</section>
|
|
433
|
+
</div>
|
|
434
|
+
</PrimaryPageTemplate>
|
|
435
|
+
)
|
|
436
|
+
}
|
|
@@ -1,37 +1,38 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import * as React from "react"
|
|
4
3
|
import type { QuestionBankItem } from "@/lib/mock/question-bank"
|
|
5
|
-
import { ListHubStatusBadge } from "@/components/list-hub-status-badge"
|
|
6
4
|
import { ListPageBoardCard } from "@/components/data-views/list-page-board-card"
|
|
5
|
+
import { formatDateUS } from "@/lib/date-filter"
|
|
7
6
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "@/lib/list-status-badges"
|
|
7
|
+
QuestionBankFavoriteButton,
|
|
8
|
+
QUESTION_BANK_FAVORITE_HOVER_GROUP,
|
|
9
|
+
} from "@/components/question-bank-favorite-button"
|
|
12
10
|
|
|
13
|
-
function QuestionBankListRow({
|
|
11
|
+
function QuestionBankListRow({
|
|
12
|
+
row,
|
|
13
|
+
onToggleFavorite,
|
|
14
|
+
}: {
|
|
15
|
+
row: QuestionBankItem
|
|
16
|
+
onToggleFavorite: (row: QuestionBankItem) => void
|
|
17
|
+
}) {
|
|
14
18
|
return (
|
|
15
19
|
<li>
|
|
16
20
|
<ListPageBoardCard
|
|
21
|
+
className={QUESTION_BANK_FAVORITE_HOVER_GROUP}
|
|
17
22
|
layout="row"
|
|
18
23
|
rowContainerClassName="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:gap-4"
|
|
19
|
-
rowEnd={
|
|
20
|
-
<div className="flex shrink-0 items-center gap-
|
|
21
|
-
<
|
|
22
|
-
surface="board"
|
|
23
|
-
label={QUESTION_BANK_STATUS_LABEL[row.status]}
|
|
24
|
-
tintClassName={QUESTION_BANK_STATUS_BADGE_CLASS[row.status]}
|
|
25
|
-
icon={QUESTION_BANK_STATUS_ICON[row.status]}
|
|
26
|
-
/>
|
|
24
|
+
rowEnd={(
|
|
25
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
26
|
+
<QuestionBankFavoriteButton row={row} onToggleFavorite={onToggleFavorite} />
|
|
27
27
|
<i className="fa-light fa-chevron-right text-xs text-muted-foreground" aria-hidden="true" />
|
|
28
28
|
</div>
|
|
29
|
-
}
|
|
29
|
+
)}
|
|
30
30
|
>
|
|
31
31
|
<div className="space-y-0.5">
|
|
32
32
|
<p className="line-clamp-2 text-sm font-semibold text-foreground">{row.stem}</p>
|
|
33
|
+
<p className="font-mono text-xs text-muted-foreground">{row.questionId}</p>
|
|
33
34
|
<p className="text-xs text-muted-foreground">
|
|
34
|
-
{row.topic} · Updated {row.updatedAt}
|
|
35
|
+
{row.topic} · Updated {formatDateUS(row.updatedAt)}
|
|
35
36
|
</p>
|
|
36
37
|
<p className="text-xs text-muted-foreground">{row.author}</p>
|
|
37
38
|
</div>
|
|
@@ -40,7 +41,13 @@ function QuestionBankListRow({ row }: { row: QuestionBankItem }) {
|
|
|
40
41
|
)
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
export function QuestionBankListView({
|
|
44
|
+
export function QuestionBankListView({
|
|
45
|
+
rows,
|
|
46
|
+
onToggleFavorite,
|
|
47
|
+
}: {
|
|
48
|
+
rows: QuestionBankItem[]
|
|
49
|
+
onToggleFavorite: (row: QuestionBankItem) => void
|
|
50
|
+
}) {
|
|
44
51
|
if (rows.length === 0) {
|
|
45
52
|
return (
|
|
46
53
|
<div className="px-4 py-16 text-center lg:px-6">
|
|
@@ -52,7 +59,7 @@ export function QuestionBankListView({ rows }: { rows: QuestionBankItem[] }) {
|
|
|
52
59
|
return (
|
|
53
60
|
<ul className="flex list-none flex-col gap-2 px-4 pb-8 pt-2 lg:px-6">
|
|
54
61
|
{rows.map(row => (
|
|
55
|
-
<QuestionBankListRow key={row.id} row={row} />
|
|
62
|
+
<QuestionBankListRow key={row.id} row={row} onToggleFavorite={onToggleFavorite} />
|
|
56
63
|
))}
|
|
57
64
|
</ul>
|
|
58
65
|
)
|
|
@@ -131,32 +131,41 @@ export function QuestionBankNewFolderSheet({
|
|
|
131
131
|
side="right"
|
|
132
132
|
showCloseButton={false}
|
|
133
133
|
showOverlay={false}
|
|
134
|
-
className="z-[60]
|
|
134
|
+
className="z-[60] w-80 sm:max-w-80 flex flex-col gap-0 overflow-hidden rounded-xl border border-border p-0 shadow-xl"
|
|
135
135
|
style={{ top: "0.5rem", bottom: "0.5rem", right: "0.5rem", height: "calc(100vh - 1rem)" }}
|
|
136
136
|
>
|
|
137
|
-
{
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
137
|
+
<Shortcut keys="Enter" disabled={createDisabled} onInvoke={commit} />
|
|
138
|
+
|
|
139
|
+
<div className="flex items-center justify-between gap-3 px-4 pt-5 pb-3">
|
|
140
|
+
<SheetTitle className="text-base font-semibold leading-tight">
|
|
141
|
+
{customizingFolder ? "Customize folder" : "New folder"}
|
|
142
|
+
</SheetTitle>
|
|
143
|
+
<Tip label="Close" side="bottom">
|
|
144
|
+
<Button
|
|
145
|
+
type="button"
|
|
146
|
+
variant="ghost"
|
|
147
|
+
size="icon-sm"
|
|
148
|
+
aria-label="Close"
|
|
149
|
+
onClick={() => onOpenChange(false)}
|
|
150
|
+
>
|
|
151
|
+
<i className="fa-light fa-xmark text-[13px]" aria-hidden="true" />
|
|
152
|
+
</Button>
|
|
153
|
+
</Tip>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<p id="new-folder-panel-desc" className="px-4 pb-3 text-sm text-muted-foreground -mt-1">
|
|
157
|
+
{descriptionText}
|
|
158
|
+
</p>
|
|
159
|
+
|
|
160
|
+
<form
|
|
161
|
+
id="new-folder-form"
|
|
162
|
+
className="flex min-h-0 flex-1 flex-col"
|
|
163
|
+
onSubmit={event => {
|
|
164
|
+
event.preventDefault()
|
|
165
|
+
commit()
|
|
166
|
+
}}
|
|
167
|
+
>
|
|
168
|
+
<div className="flex min-h-0 flex-1 flex-col gap-6 overflow-y-auto px-4 pb-4">
|
|
160
169
|
<div className="flex flex-col items-center gap-4">
|
|
161
170
|
<FolderTilePreview
|
|
162
171
|
name={draft.name}
|
|
@@ -224,24 +233,29 @@ export function QuestionBankNewFolderSheet({
|
|
|
224
233
|
))}
|
|
225
234
|
</div>
|
|
226
235
|
</div>
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<div className="flex items-center gap-2 border-t border-border px-4 py-3">
|
|
239
|
+
<Button
|
|
240
|
+
type="button"
|
|
241
|
+
variant="outline"
|
|
242
|
+
className="flex-1"
|
|
243
|
+
onClick={() => onOpenChange(false)}
|
|
244
|
+
>
|
|
245
|
+
Cancel
|
|
246
|
+
<KbdGroup className="ml-1.5"><Kbd variant="bare">Esc</Kbd></KbdGroup>
|
|
247
|
+
</Button>
|
|
248
|
+
<Button
|
|
249
|
+
type="submit"
|
|
250
|
+
className="flex-1"
|
|
251
|
+
disabled={createDisabled}
|
|
252
|
+
>
|
|
253
|
+
<i className="fa-light fa-folder text-[13px]" aria-hidden="true" />
|
|
254
|
+
{customizingFolder ? "Update folder" : "Create folder"}
|
|
255
|
+
<KbdGroup className="ml-1.5"><Kbd variant="bare">⏎</Kbd></KbdGroup>
|
|
256
|
+
</Button>
|
|
257
|
+
</div>
|
|
258
|
+
</form>
|
|
245
259
|
</SheetContent>
|
|
246
260
|
</Sheet>
|
|
247
261
|
)
|
|
@@ -39,12 +39,6 @@ import {
|
|
|
39
39
|
type QuestionBankFolder,
|
|
40
40
|
type QuestionBankFolderColorKey,
|
|
41
41
|
} from "@/lib/mock/question-bank-folders"
|
|
42
|
-
import {
|
|
43
|
-
QUESTION_BANK_STATUS_BADGE_CLASS,
|
|
44
|
-
QUESTION_BANK_STATUS_ICON,
|
|
45
|
-
QUESTION_BANK_STATUS_LABEL,
|
|
46
|
-
} from "@/lib/list-status-badges"
|
|
47
|
-
import { ListHubStatusBadge } from "@/components/list-hub-status-badge"
|
|
48
42
|
import {
|
|
49
43
|
ListPageViewFrame,
|
|
50
44
|
LIST_PAGE_VIEW_FRAME_MAX_WIDE,
|
|
@@ -389,7 +383,7 @@ export function QuestionBankOsFolderView({
|
|
|
389
383
|
</Button>
|
|
390
384
|
</DropdownMenuTrigger>
|
|
391
385
|
</Tip>
|
|
392
|
-
<DropdownMenuContent align="end"
|
|
386
|
+
<DropdownMenuContent align="end">
|
|
393
387
|
<DropdownMenuItem
|
|
394
388
|
onSelect={() => {
|
|
395
389
|
setTimeout(() => startRename(folder), 0)
|
|
@@ -434,12 +428,7 @@ export function QuestionBankOsFolderView({
|
|
|
434
428
|
<i className="fa-solid fa-file-lines text-3xl text-muted-foreground" aria-hidden="true" />
|
|
435
429
|
</div>
|
|
436
430
|
<span className="line-clamp-2 w-full text-xs font-medium text-foreground">{q.stem}</span>
|
|
437
|
-
<
|
|
438
|
-
surface="board"
|
|
439
|
-
label={QUESTION_BANK_STATUS_LABEL[q.status]}
|
|
440
|
-
tintClassName={QUESTION_BANK_STATUS_BADGE_CLASS[q.status]}
|
|
441
|
-
icon={QUESTION_BANK_STATUS_ICON[q.status]}
|
|
442
|
-
/>
|
|
431
|
+
<span className="w-full font-mono text-xs text-muted-foreground">{q.questionId}</span>
|
|
443
432
|
</div>
|
|
444
433
|
<div className="absolute end-1.5 top-1.5 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100">
|
|
445
434
|
<DropdownMenu>
|
|
@@ -455,7 +444,7 @@ export function QuestionBankOsFolderView({
|
|
|
455
444
|
</Button>
|
|
456
445
|
</DropdownMenuTrigger>
|
|
457
446
|
</Tip>
|
|
458
|
-
<DropdownMenuContent align="end"
|
|
447
|
+
<DropdownMenuContent align="end">
|
|
459
448
|
<DropdownMenuSub>
|
|
460
449
|
<DropdownMenuSubTrigger>
|
|
461
450
|
<i className="fa-solid fa-folder-arrow-up me-2 text-xs" aria-hidden="true" />
|