@exxatdesignux/ui 0.2.15 → 0.2.16
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/CHANGELOG.md +12 -0
- package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +3 -1
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +3 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +3 -1
- package/consumer-extras/patterns/collaboration-access-pattern.md +2 -0
- package/package.json +1 -1
- package/src/globals.css +21 -2
- package/src/theme.css +4 -2
- package/template/AGENTS.md +6 -4
- package/template/app/(app)/question-bank/layout.tsx +11 -4
- package/template/app/globals.css +29 -2
- package/template/components/app-sidebar.tsx +89 -41
- package/template/components/ask-leo-sidebar.tsx +1 -2
- package/template/components/data-views/finder-panel-view.tsx +2 -2
- package/template/components/data-views/index.ts +19 -0
- package/template/components/data-views/list-page-split-details-placeholder.tsx +3 -3
- package/template/components/data-views/list-page-split-hub-tokens.ts +1 -1
- package/template/components/data-views/list-page-tree-column-header.tsx +1 -1
- package/template/components/data-views/outline-tree-menu.tsx +157 -0
- package/template/components/data-views/question-bank-folder-tree-branch.tsx +210 -0
- package/template/components/exxat-product-logo.tsx +11 -72
- package/template/components/folder-details-shell.tsx +1 -1
- package/template/components/hub-tree-panel-view.tsx +88 -80
- package/template/components/key-metrics.tsx +50 -13
- package/template/components/page-header.tsx +19 -10
- package/template/components/product-switcher.tsx +1 -4
- package/template/components/question-bank-client.tsx +111 -69
- package/template/components/question-bank-page-header.tsx +18 -2
- package/template/components/question-bank-secondary-nav.tsx +12 -225
- package/template/components/secondary-panel.tsx +1 -1
- package/template/components/site-header.tsx +21 -2
- package/template/components/templates/dedicated-search-landing-template.tsx +86 -20
- package/template/components/templates/list-page.tsx +1 -3
- package/template/components/templates/nested-secondary-panel-shell.tsx +3 -4
- package/template/docs/collaboration-access-pattern.md +2 -0
- package/template/docs/question-bank-hub-header-pattern.md +25 -0
- package/template/hooks/use-secondary-panel-hub-nav.ts +17 -1
- package/template/lib/mock/navigation.tsx +30 -1
- package/template/lib/question-bank-nav.ts +26 -0
- package/template/package.json +3 -3
- package/template/components/command-menu-01.tsx +0 -133
- package/template/components/command-menu-02.tsx +0 -386
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import * as React from "react"
|
|
4
4
|
|
|
5
5
|
import { ListPageViewFrame } from "@/components/data-views"
|
|
6
|
+
import { DotPattern } from "@/components/ui/dot-pattern"
|
|
7
|
+
import { cn } from "@/lib/utils"
|
|
6
8
|
|
|
7
9
|
export interface DedicatedSearchLandingTemplateProps {
|
|
8
10
|
/** Page title — string or rich node (e.g. styled heading). */
|
|
@@ -20,6 +22,67 @@ export interface DedicatedSearchLandingTemplateProps {
|
|
|
20
22
|
const DEFAULT_GUTTER =
|
|
21
23
|
"mx-auto flex min-h-[min(72vh,36rem)] w-full min-w-0 flex-col justify-center gap-0 px-6 py-8 sm:px-8 sm:py-10 md:px-12 md:py-12 lg:px-16"
|
|
22
24
|
|
|
25
|
+
/** Feather into page white / header so the hero never reads as a hard horizontal slab. */
|
|
26
|
+
const HERO_BACKDROP_MASK =
|
|
27
|
+
"[mask-image:linear-gradient(to_bottom,transparent_0%,black_5%,black_95%,transparent_100%)] [-webkit-mask-image:linear-gradient(to_bottom,transparent_0%,black_5%,black_95%,transparent_100%)]"
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Soft blurred blobs using only Ask Leo surface tints (`--leo-surface-tint-a|b` in `globals.css`).
|
|
31
|
+
*/
|
|
32
|
+
function DedicatedSearchLandingBackdrop() {
|
|
33
|
+
return (
|
|
34
|
+
<div
|
|
35
|
+
aria-hidden
|
|
36
|
+
className={cn(
|
|
37
|
+
"pointer-events-none absolute inset-0 -z-10 select-none overflow-hidden",
|
|
38
|
+
HERO_BACKDROP_MASK,
|
|
39
|
+
)}
|
|
40
|
+
>
|
|
41
|
+
<div
|
|
42
|
+
className="absolute -left-[20%] -top-[30%] h-[min(54vmin,27rem)] w-[min(54vmin,27rem)] rounded-full blur-[76px]"
|
|
43
|
+
style={{
|
|
44
|
+
background: "radial-gradient(circle at 42% 36%, var(--leo-surface-tint-b) 0%, transparent 68%)",
|
|
45
|
+
}}
|
|
46
|
+
/>
|
|
47
|
+
<div
|
|
48
|
+
className="absolute -right-[12%] top-[2%] h-[min(46vmin,23rem)] w-[min(46vmin,23rem)] rounded-full blur-[68px]"
|
|
49
|
+
style={{
|
|
50
|
+
background: "radial-gradient(circle at 48% 48%, var(--leo-surface-tint-a) 0%, transparent 66%)",
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
<div
|
|
54
|
+
className="absolute bottom-[-16%] left-[14%] h-[min(50vmin,25rem)] w-[min(50vmin,25rem)] rounded-full blur-[84px]"
|
|
55
|
+
style={{
|
|
56
|
+
background: "radial-gradient(circle at 44% 40%, var(--leo-surface-tint-b) 0%, transparent 70%)",
|
|
57
|
+
}}
|
|
58
|
+
/>
|
|
59
|
+
<div
|
|
60
|
+
className="absolute bottom-[4%] right-[6%] h-[min(40vmin,20rem)] w-[min(40vmin,20rem)] rounded-full blur-[60px]"
|
|
61
|
+
style={{
|
|
62
|
+
background: "radial-gradient(circle at 52% 44%, var(--leo-surface-tint-a) 0%, transparent 72%)",
|
|
63
|
+
}}
|
|
64
|
+
/>
|
|
65
|
+
<div
|
|
66
|
+
className="absolute left-[36%] top-[32%] h-[min(38vmin,19rem)] w-[min(38vmin,19rem)] -translate-x-1/2 rounded-full blur-[74px]"
|
|
67
|
+
style={{
|
|
68
|
+
background: "radial-gradient(circle at 50% 50%, var(--leo-surface-tint-b) 0%, transparent 68%)",
|
|
69
|
+
}}
|
|
70
|
+
/>
|
|
71
|
+
|
|
72
|
+
{/* Static dot field — same primitive as `AiThinkingOverlay` (no motion here). */}
|
|
73
|
+
<DotPattern
|
|
74
|
+
width={15}
|
|
75
|
+
height={15}
|
|
76
|
+
cr={0.65}
|
|
77
|
+
className={cn(
|
|
78
|
+
"absolute inset-0 opacity-[0.34] mix-blend-multiply dark:opacity-[0.22] dark:mix-blend-soft-light",
|
|
79
|
+
"fill-[color-mix(in_oklch,var(--brand-color)_14%,var(--background))]",
|
|
80
|
+
)}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
23
86
|
/**
|
|
24
87
|
* Centered dedicated-search landing — empty `?q=` shell (hero title + composer + optional trailing).
|
|
25
88
|
*/
|
|
@@ -32,27 +95,30 @@ export function DedicatedSearchLandingTemplate({
|
|
|
32
95
|
gutterClassName = DEFAULT_GUTTER,
|
|
33
96
|
}: DedicatedSearchLandingTemplateProps) {
|
|
34
97
|
return (
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
98
|
+
<div className="relative isolate min-w-0 w-full overflow-hidden">
|
|
99
|
+
<DedicatedSearchLandingBackdrop />
|
|
100
|
+
<ListPageViewFrame
|
|
101
|
+
maxWidthClassName={maxWidthClassName}
|
|
102
|
+
className={cn("relative z-10", frameClassName)}
|
|
103
|
+
gutterClassName={gutterClassName}
|
|
104
|
+
>
|
|
105
|
+
<header className="min-w-0">
|
|
106
|
+
{typeof title === "string" ? (
|
|
107
|
+
<h1
|
|
108
|
+
className="text-balance text-3xl font-semibold tracking-tight text-foreground sm:text-4xl"
|
|
109
|
+
style={{ fontFamily: "var(--font-heading)" }}
|
|
110
|
+
>
|
|
111
|
+
{title}
|
|
112
|
+
</h1>
|
|
113
|
+
) : (
|
|
114
|
+
title
|
|
115
|
+
)}
|
|
116
|
+
</header>
|
|
52
117
|
|
|
53
|
-
|
|
118
|
+
<div className="min-w-0 mt-6 sm:mt-8">{composer}</div>
|
|
54
119
|
|
|
55
|
-
|
|
56
|
-
|
|
120
|
+
{trailing ? <div className="min-w-0 mt-10 sm:mt-12 md:mt-14 lg:mt-16">{trailing}</div> : null}
|
|
121
|
+
</ListPageViewFrame>
|
|
122
|
+
</div>
|
|
57
123
|
)
|
|
58
124
|
}
|
|
@@ -320,9 +320,7 @@ export function ListPageTemplate({
|
|
|
320
320
|
const count = getTabCount?.(tab.filterId)
|
|
321
321
|
const tabInner = (
|
|
322
322
|
<>
|
|
323
|
-
{
|
|
324
|
-
<i className={`fa-light ${tab.icon} text-xs`} aria-hidden="true" />
|
|
325
|
-
) : null}
|
|
323
|
+
<i className={cn("fa-light shrink-0 text-xs", tab.icon)} aria-hidden="true" />
|
|
326
324
|
{tab.label}
|
|
327
325
|
{count !== undefined && (
|
|
328
326
|
<span
|
|
@@ -14,8 +14,8 @@ export interface NestedSecondaryPanelShellProps {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Shared chrome for a nested hub rail — full width vs icon rail
|
|
18
|
-
*
|
|
17
|
+
* Shared chrome for a nested hub rail — full width vs icon rail.
|
|
18
|
+
* Fill uses `--secondary-panel-bg` (soft brand wash on `--background`).
|
|
19
19
|
*/
|
|
20
20
|
export function NestedSecondaryPanelShell({
|
|
21
21
|
open,
|
|
@@ -34,7 +34,7 @@ export function NestedSecondaryPanelShell({
|
|
|
34
34
|
"transition-[width,margin,opacity] duration-200 ease-linear",
|
|
35
35
|
open
|
|
36
36
|
? cn(
|
|
37
|
-
"shrink-0 m-2 mx-2 rounded-xl ring-1 ring-
|
|
37
|
+
"shrink-0 m-2 mx-2 rounded-xl ring-1 ring-border shadow-sm relative md:sticky md:top-2 bg-[var(--secondary-panel-bg)]",
|
|
38
38
|
compact
|
|
39
39
|
? "w-12 min-w-12 max-w-12 h-[min(calc(100svh-2rem),800px)] md:h-[min(calc(100svh-1rem),800px)]"
|
|
40
40
|
: "w-64 min-w-64 max-w-64 h-[min(calc(100svh-2rem),800px)] md:h-[min(calc(100svh-1rem),800px)]",
|
|
@@ -42,7 +42,6 @@ export function NestedSecondaryPanelShell({
|
|
|
42
42
|
: "h-0 min-h-0 shrink overflow-hidden border-0 p-0 m-0 min-w-0 w-0 max-w-0 opacity-0 pointer-events-none",
|
|
43
43
|
className,
|
|
44
44
|
)}
|
|
45
|
-
style={open ? { backgroundColor: "var(--secondary-panel-bg, #FAFBFF)" } : undefined}
|
|
46
45
|
>
|
|
47
46
|
<div
|
|
48
47
|
className={cn(
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Shared UI for **who can access a hub** (face stack in the header) and **inviting people** (floating sheet). **Reference:** Question bank — `QuestionBankPageHeader`, `QuestionBankClient`, `InviteCollaboratorsDrawer`.
|
|
4
4
|
|
|
5
|
+
**Folder-scoped question bank:** When the library URL selects a folder (`?scope=folder&folderId=`), the same header **⋯ More** menu also exposes **Customize folder** (name / color / icon) via **`QuestionBankNewFolderSheet`** mounted on **`QuestionBankClient`** so it works on every view tab. See **`docs/question-bank-hub-header-pattern.md`** and **`.cursor/rules/exxat-question-bank-hub-header.mdc`**.
|
|
6
|
+
|
|
5
7
|
## When to use
|
|
6
8
|
|
|
7
9
|
- A list hub or library is **shared** across people (not a private directory).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Question bank hub header — folder scope + Customize folder
|
|
2
|
+
|
|
3
|
+
**Audience:** Engineers extending the question bank library hub (`QuestionBankClient`, `QuestionBankPageHeader`, URL scope).
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
The library uses **`ListPageTemplate`** with multiple **view tabs** (table, panel, tree, …). **`QuestionBankNewFolderSheet`** (customize mode) is also used inside **`QuestionBankTable`** for some views (e.g. panel columns). If **Customize folder** exists only there, users on **table** or other tabs **cannot** open the sheet from a consistent chrome entry point when the URL is scoped to a folder (`?scope=folder&folderId=…`).
|
|
8
|
+
|
|
9
|
+
## Pattern
|
|
10
|
+
|
|
11
|
+
1. **`QuestionBankPageHeader`** exposes optional **`onCustomizeFolder?: () => void`**. When **`navState.scope === "folder"`** and **`navState.folderId`** is set, the hub client passes a callback that opens customize mode for the matching **`QuestionBankFolder`**.
|
|
12
|
+
2. **`QuestionBankClient`** (or equivalent hub client) mounts **`QuestionBankNewFolderSheet`** **once** beside **`SecondaryPanelHubTemplate` / `ListPageTemplate`**, with local state for **`open`** and **`customizingFolder`**. Saving updates **`folders`** the same way as table-embedded customize flows.
|
|
13
|
+
3. The header **⋯ More** menu order stays aligned with **§4.7**: **Invite people** (when collaboration variant) → **Customize folder** (when folder-scoped) → **Export** → **Show / hide metric section** (when applicable).
|
|
14
|
+
|
|
15
|
+
## References
|
|
16
|
+
|
|
17
|
+
| Piece | Location |
|
|
18
|
+
|-------|-----------|
|
|
19
|
+
| Header prop + menu item | `components/question-bank-page-header.tsx` |
|
|
20
|
+
| Client wiring + sheet | `components/question-bank-client.tsx` |
|
|
21
|
+
| URL scope | `lib/question-bank-nav.ts` (`parseQuestionBankNav`, `QuestionBankNavState`) |
|
|
22
|
+
| Sheet UI | `components/question-bank-new-folder-sheet.tsx` |
|
|
23
|
+
|
|
24
|
+
**Cursor rule:** `.cursor/rules/exxat-question-bank-hub-header.mdc`
|
|
25
|
+
**Handbook:** `AGENTS.md` §4.6 (folder-scoped hub chrome).
|
|
@@ -36,6 +36,11 @@ export interface UseSecondaryPanelHubNavOptions<TNav> {
|
|
|
36
36
|
canonicalHref?: (searchParams: URLSearchParams) => string | null
|
|
37
37
|
/** Re-open the secondary panel when the user returns to the default scope (e.g. All questions). */
|
|
38
38
|
shouldReopenPanel?: (nav: TNav) => boolean
|
|
39
|
+
/**
|
|
40
|
+
* When set, auto-reopen only runs on these pathnames (e.g. library hub, not dedicated search landings).
|
|
41
|
+
* Omit to keep legacy behavior: any {@link hubPathnames} match may reopen the panel.
|
|
42
|
+
*/
|
|
43
|
+
reopenPanelOnPathnames?: readonly string[]
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
/**
|
|
@@ -48,6 +53,7 @@ export function useSecondaryPanelHubNav<TNav>({
|
|
|
48
53
|
parseNav,
|
|
49
54
|
canonicalHref,
|
|
50
55
|
shouldReopenPanel,
|
|
56
|
+
reopenPanelOnPathnames,
|
|
51
57
|
}: UseSecondaryPanelHubNavOptions<TNav>) {
|
|
52
58
|
const pathname = usePathname()
|
|
53
59
|
const router = useRouter()
|
|
@@ -90,9 +96,19 @@ export function useSecondaryPanelHubNav<TNav>({
|
|
|
90
96
|
|
|
91
97
|
React.useEffect(() => {
|
|
92
98
|
if (!isHubPath || !shouldReopenPanel?.(navState)) return
|
|
99
|
+
if (reopenPanelOnPathnames?.length && !reopenPanelOnPathnames.includes(pathname)) return
|
|
93
100
|
if (activePanel === panelId) return
|
|
94
101
|
openPanel(panelId)
|
|
95
|
-
}, [
|
|
102
|
+
}, [
|
|
103
|
+
activePanel,
|
|
104
|
+
isHubPath,
|
|
105
|
+
navState,
|
|
106
|
+
openPanel,
|
|
107
|
+
panelId,
|
|
108
|
+
pathname,
|
|
109
|
+
reopenPanelOnPathnames,
|
|
110
|
+
shouldReopenPanel,
|
|
111
|
+
])
|
|
96
112
|
|
|
97
113
|
return { navState, searchParamsKey, hubPathname, hubBasePath, pathname, isHubPath }
|
|
98
114
|
}
|
|
@@ -7,6 +7,11 @@ import type * as React from "react"
|
|
|
7
7
|
|
|
8
8
|
import { logoDevUrl } from "@/lib/logo-dev"
|
|
9
9
|
import { stockPortraitUrl } from "@/lib/stock-portrait"
|
|
10
|
+
import {
|
|
11
|
+
QUESTION_BANK_ENTRY_PATH,
|
|
12
|
+
QUESTION_BANK_HUB_FIND_PATH,
|
|
13
|
+
QUESTION_BANK_LIBRARY_PATH,
|
|
14
|
+
} from "@/lib/question-bank-nav"
|
|
10
15
|
|
|
11
16
|
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
12
17
|
|
|
@@ -97,10 +102,34 @@ export const NAV_PRIMARY: NavLinkItem[] = [
|
|
|
97
102
|
{
|
|
98
103
|
key: "question-bank",
|
|
99
104
|
title: "Question bank",
|
|
100
|
-
url:
|
|
105
|
+
url: QUESTION_BANK_ENTRY_PATH,
|
|
101
106
|
icon: <i className="fa-light fa-books" aria-hidden="true" />,
|
|
102
107
|
iconActive: <i className="fa-solid fa-books" aria-hidden="true" />,
|
|
103
108
|
secondaryPanel: "question-bank",
|
|
109
|
+
primaryHubChildKey: "question-bank-hub",
|
|
110
|
+
children: [
|
|
111
|
+
{
|
|
112
|
+
key: "question-bank-hub",
|
|
113
|
+
title: "Question hub",
|
|
114
|
+
url: QUESTION_BANK_ENTRY_PATH,
|
|
115
|
+
icon: <i className="fa-light fa-sparkles" aria-hidden="true" />,
|
|
116
|
+
iconActive: <i className="fa-solid fa-sparkles" aria-hidden="true" />,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
key: "question-bank-search",
|
|
120
|
+
title: "Search",
|
|
121
|
+
url: QUESTION_BANK_HUB_FIND_PATH,
|
|
122
|
+
icon: <i className="fa-light fa-magnifying-glass" aria-hidden="true" />,
|
|
123
|
+
iconActive: <i className="fa-solid fa-magnifying-glass" aria-hidden="true" />,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
key: "question-bank-library",
|
|
127
|
+
title: "Library",
|
|
128
|
+
url: QUESTION_BANK_LIBRARY_PATH,
|
|
129
|
+
icon: <i className="fa-light fa-table-list" aria-hidden="true" />,
|
|
130
|
+
iconActive: <i className="fa-solid fa-table-list" aria-hidden="true" />,
|
|
131
|
+
},
|
|
132
|
+
],
|
|
104
133
|
},
|
|
105
134
|
{
|
|
106
135
|
key: "data-list",
|
|
@@ -54,6 +54,32 @@ export function isQuestionBankDedicatedSearchPathname(pathname: string): boolean
|
|
|
54
54
|
return pathname === QUESTION_BANK_LIST_PATH || pathname === QUESTION_BANK_HUB_FIND_PATH
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Whether a secondary-nav row (All / My / folder / Search) matches the current URL + parsed nav.
|
|
59
|
+
* Used by `QuestionBankSecondaryNav` and the folder tree branch.
|
|
60
|
+
*/
|
|
61
|
+
export function isQuestionBankNavActive(
|
|
62
|
+
pathname: string,
|
|
63
|
+
nav: QuestionBankNavState,
|
|
64
|
+
scope: QuestionBankNavScope,
|
|
65
|
+
folderId?: string | null,
|
|
66
|
+
): boolean {
|
|
67
|
+
const p = pathname.length > 1 && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname
|
|
68
|
+
if (!QUESTION_BANK_LIBRARY_HUB_PATHS.includes(p)) return false
|
|
69
|
+
if (scope === "all") {
|
|
70
|
+
if (isQuestionBankDedicatedSearchPathname(pathname)) return false
|
|
71
|
+
return nav.scope === "all"
|
|
72
|
+
}
|
|
73
|
+
if (scope === "my") {
|
|
74
|
+
if (isQuestionBankDedicatedSearchPathname(pathname)) return false
|
|
75
|
+
return nav.scope === "my"
|
|
76
|
+
}
|
|
77
|
+
if (scope === "folder" && folderId) {
|
|
78
|
+
return nav.scope === "folder" && nav.folderId === folderId
|
|
79
|
+
}
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
|
|
57
83
|
/** Default secondary-nav selection — All questions (no `scope` query). */
|
|
58
84
|
export const QUESTION_BANK_DEFAULT_NAV: QuestionBankNavState = {
|
|
59
85
|
scope: "all",
|
package/template/package.json
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"cmdk": "^1.1.1",
|
|
43
43
|
"lucide-react": "^0.577.0",
|
|
44
44
|
"motion": "^12.38.0",
|
|
45
|
-
"next": "16.2.
|
|
45
|
+
"next": "16.2.6",
|
|
46
46
|
"next-themes": "^0.4.6",
|
|
47
47
|
"react": "^19.2.4",
|
|
48
48
|
"react-day-picker": "^9.14.0",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@eslint/eslintrc": "^3",
|
|
63
|
-
"@next/bundle-analyzer": "
|
|
63
|
+
"@next/bundle-analyzer": "16.2.6",
|
|
64
64
|
"@tailwindcss/postcss": "^4.2.1",
|
|
65
65
|
"@testing-library/jest-dom": "^6.9.1",
|
|
66
66
|
"@testing-library/react": "^16.3.0",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"@types/react-dom": "^19.2.3",
|
|
70
70
|
"@vitejs/plugin-react": "^4.7.0",
|
|
71
71
|
"eslint": "^9.39.4",
|
|
72
|
-
"eslint-config-next": "16.
|
|
72
|
+
"eslint-config-next": "16.2.6",
|
|
73
73
|
"jsdom": "^26.1.0",
|
|
74
74
|
"pm2": "^6.0.14",
|
|
75
75
|
"postcss": "^8",
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
IconBell,
|
|
5
|
-
IconBolt,
|
|
6
|
-
IconCalendar,
|
|
7
|
-
IconChartBar,
|
|
8
|
-
IconChartPie,
|
|
9
|
-
IconClock,
|
|
10
|
-
IconFileText,
|
|
11
|
-
IconHelp,
|
|
12
|
-
IconKeyboard,
|
|
13
|
-
IconLayoutDashboard,
|
|
14
|
-
IconLayoutKanban,
|
|
15
|
-
IconLogout,
|
|
16
|
-
IconMessage,
|
|
17
|
-
IconPalette,
|
|
18
|
-
IconSettings,
|
|
19
|
-
IconSquareCheck,
|
|
20
|
-
IconTarget,
|
|
21
|
-
IconTrendingUp,
|
|
22
|
-
IconUsers,
|
|
23
|
-
} from "@tabler/icons-react";
|
|
24
|
-
import { useEffect, useState } from "react";
|
|
25
|
-
import { Button } from "@/components/ui/button";
|
|
26
|
-
import {
|
|
27
|
-
Command,
|
|
28
|
-
CommandDialog,
|
|
29
|
-
CommandEmpty,
|
|
30
|
-
CommandGroup,
|
|
31
|
-
CommandInput,
|
|
32
|
-
CommandItem,
|
|
33
|
-
CommandList,
|
|
34
|
-
} from "@/components/ui/command";
|
|
35
|
-
import { Kbd } from "@/components/ui/kbd";
|
|
36
|
-
|
|
37
|
-
const workspaceItems = [
|
|
38
|
-
{ icon: IconLayoutDashboard, label: "Dashboard" },
|
|
39
|
-
{ icon: IconLayoutKanban, label: "Projects" },
|
|
40
|
-
{ icon: IconSquareCheck, label: "Tasks" },
|
|
41
|
-
{ icon: IconCalendar, label: "Calendar" },
|
|
42
|
-
{ icon: IconUsers, label: "Team members" },
|
|
43
|
-
{ icon: IconMessage, label: "Messages" },
|
|
44
|
-
{ icon: IconFileText, label: "Documents" },
|
|
45
|
-
{ icon: IconBell, label: "Notifications" },
|
|
46
|
-
{ icon: IconClock, label: "Time tracking" },
|
|
47
|
-
{ icon: IconTarget, label: "Goals" },
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
const analyticsItems = [
|
|
51
|
-
{ icon: IconChartBar, label: "Overview" },
|
|
52
|
-
{ icon: IconTrendingUp, label: "Performance" },
|
|
53
|
-
{ icon: IconChartPie, label: "Reports" },
|
|
54
|
-
{ icon: IconBolt, label: "Insights" },
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
const settingsItems = [
|
|
58
|
-
{ icon: IconSettings, label: "Preferences" },
|
|
59
|
-
{ icon: IconPalette, label: "Appearance" },
|
|
60
|
-
{ icon: IconKeyboard, label: "Keyboard shortcuts" },
|
|
61
|
-
{ icon: IconHelp, label: "Help & support" },
|
|
62
|
-
{ icon: IconLogout, label: "Sign out" },
|
|
63
|
-
];
|
|
64
|
-
|
|
65
|
-
export function CommandMenu01() {
|
|
66
|
-
const [open, setOpen] = useState(true);
|
|
67
|
-
|
|
68
|
-
useEffect(() => {
|
|
69
|
-
const down = (e: KeyboardEvent) => {
|
|
70
|
-
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
|
71
|
-
e.preventDefault();
|
|
72
|
-
setOpen((prev) => !prev);
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
document.addEventListener("keydown", down);
|
|
77
|
-
return () => document.removeEventListener("keydown", down);
|
|
78
|
-
}, []);
|
|
79
|
-
|
|
80
|
-
return (
|
|
81
|
-
<>
|
|
82
|
-
<Button onClick={() => setOpen(true)} variant="outline">
|
|
83
|
-
Open Command Menu
|
|
84
|
-
</Button>
|
|
85
|
-
|
|
86
|
-
<CommandDialog onOpenChange={setOpen} open={open} showCloseButton={false}>
|
|
87
|
-
<Command className="rounded-none border-0 bg-transparent shadow-none">
|
|
88
|
-
<CommandInput
|
|
89
|
-
className="h-12"
|
|
90
|
-
placeholder="Type a command or search..."
|
|
91
|
-
/>
|
|
92
|
-
<CommandList className="min-h-[min(420px,50vh)] max-h-[min(560px,65vh)]">
|
|
93
|
-
<CommandEmpty>No results found.</CommandEmpty>
|
|
94
|
-
<CommandGroup heading="Workspace">
|
|
95
|
-
{workspaceItems.map((item) => (
|
|
96
|
-
<CommandItem key={item.label}>
|
|
97
|
-
<item.icon className="me-2 h-5 w-5" />
|
|
98
|
-
<span>{item.label}</span>
|
|
99
|
-
</CommandItem>
|
|
100
|
-
))}
|
|
101
|
-
</CommandGroup>
|
|
102
|
-
<CommandGroup heading="Analytics">
|
|
103
|
-
{analyticsItems.map((item) => (
|
|
104
|
-
<CommandItem key={item.label}>
|
|
105
|
-
<item.icon className="me-2 h-5 w-5" />
|
|
106
|
-
<span>{item.label}</span>
|
|
107
|
-
</CommandItem>
|
|
108
|
-
))}
|
|
109
|
-
</CommandGroup>
|
|
110
|
-
<CommandGroup heading="Settings">
|
|
111
|
-
{settingsItems.map((item) => (
|
|
112
|
-
<CommandItem key={item.label}>
|
|
113
|
-
<item.icon className="me-2 h-5 w-5" />
|
|
114
|
-
<span>{item.label}</span>
|
|
115
|
-
</CommandItem>
|
|
116
|
-
))}
|
|
117
|
-
</CommandGroup>
|
|
118
|
-
</CommandList>
|
|
119
|
-
</Command>
|
|
120
|
-
<div className="flex h-12 items-center justify-end border-t px-3">
|
|
121
|
-
<button
|
|
122
|
-
className="flex items-center gap-1 text-muted-foreground text-sm hover:text-foreground"
|
|
123
|
-
onClick={() => setOpen(false)}
|
|
124
|
-
type="button"
|
|
125
|
-
>
|
|
126
|
-
<span>Close</span>
|
|
127
|
-
<Kbd className="ms-1">Esc</Kbd>
|
|
128
|
-
</button>
|
|
129
|
-
</div>
|
|
130
|
-
</CommandDialog>
|
|
131
|
-
</>
|
|
132
|
-
);
|
|
133
|
-
}
|