@cntyclub/ui-react 0.3.1 → 0.4.0
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/dist/index.d.ts +46 -1
- package/dist/index.js +146 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/layout/dashboard-topbar.tsx +221 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cntyclub/ui-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "React component library for the Country Club UI Kit — Base UI primitives styled with the Country Club design system (Tailwind CSS v4)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { LifeBuoy, LogOut, Search, Settings } from "lucide-react";
|
|
4
|
+
import type * as React from "react";
|
|
5
|
+
|
|
6
|
+
import { cn } from "../../lib/utils/css";
|
|
7
|
+
import { AnimatedThemeToggler } from "../ui/animated-theme-toggler";
|
|
8
|
+
import { Avatar } from "../ui/avatar";
|
|
9
|
+
import { Button } from "../ui/button";
|
|
10
|
+
import { Kbd, KbdGroup } from "../ui/kbd";
|
|
11
|
+
import {
|
|
12
|
+
Menu,
|
|
13
|
+
MenuItem,
|
|
14
|
+
MenuPopup,
|
|
15
|
+
MenuSeparator,
|
|
16
|
+
MenuTrigger,
|
|
17
|
+
} from "../ui/menu";
|
|
18
|
+
import { SidebarTrigger } from "../ui/sidebar";
|
|
19
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
|
20
|
+
|
|
21
|
+
export interface DashboardTopbarUser {
|
|
22
|
+
/** Display name, shown in the account menu and used as the avatar alt text. */
|
|
23
|
+
name: string;
|
|
24
|
+
/** Optional email, shown under the name in the account menu. */
|
|
25
|
+
email?: string;
|
|
26
|
+
/** Optional avatar image URL. Falls back to initials when omitted. */
|
|
27
|
+
avatarSrc?: string;
|
|
28
|
+
/** Initials shown when there is no avatar image. */
|
|
29
|
+
initials?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DashboardTopbarProps {
|
|
33
|
+
/** The signed-in user rendered in the account menu + avatar trigger. */
|
|
34
|
+
user: DashboardTopbarUser;
|
|
35
|
+
/** Opens the consumer's command palette. The search field is a button. */
|
|
36
|
+
onSearch?: () => void;
|
|
37
|
+
/** Placeholder text inside the search affordance. Defaults to "Search". */
|
|
38
|
+
searchPlaceholder?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Trailing shortcut hint inside the search affordance. Defaults to
|
|
41
|
+
* `<KbdGroup><Kbd>⌘</Kbd><Kbd>K</Kbd></KbdGroup>`. Pass `null` to hide it.
|
|
42
|
+
*/
|
|
43
|
+
searchShortcut?: React.ReactNode;
|
|
44
|
+
/** Account-menu "Settings" item handler. The item is omitted when undefined. */
|
|
45
|
+
onSettings?: () => void;
|
|
46
|
+
/** Account-menu "Log out" item handler. The item is omitted when undefined. */
|
|
47
|
+
onLogout?: () => void;
|
|
48
|
+
/** mailto/url for the support icon button. The button renders only when set. */
|
|
49
|
+
supportHref?: string;
|
|
50
|
+
/** Whether to render the animated light/dark theme toggle. Defaults to `true`. */
|
|
51
|
+
showThemeToggle?: boolean;
|
|
52
|
+
/** Optional extra controls rendered left of the theme toggle. */
|
|
53
|
+
actions?: React.ReactNode;
|
|
54
|
+
className?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const defaultSearchShortcut = (
|
|
58
|
+
<KbdGroup className="absolute right-3 top-1/2 hidden -translate-y-1/2 sm:inline-flex">
|
|
59
|
+
<Kbd>⌘</Kbd>
|
|
60
|
+
<Kbd>K</Kbd>
|
|
61
|
+
</KbdGroup>
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Dashboard top bar shared by the host and sponsor dashboards.
|
|
66
|
+
*
|
|
67
|
+
* Zones: mobile/desktop sidebar trigger · a global search field styled as an
|
|
68
|
+
* input that calls `onSearch` (the consumer owns the command palette + ⌘K key
|
|
69
|
+
* handler) · optional extra `actions` · the animated theme toggle · an optional
|
|
70
|
+
* support link · the account menu (Settings / Log out). The inner row shares a
|
|
71
|
+
* `max-w-[1280px]` wrapper so the search field and avatar line up with the page.
|
|
72
|
+
*/
|
|
73
|
+
export function DashboardTopbar({
|
|
74
|
+
user,
|
|
75
|
+
onSearch,
|
|
76
|
+
searchPlaceholder = "Search",
|
|
77
|
+
searchShortcut = defaultSearchShortcut,
|
|
78
|
+
onSettings,
|
|
79
|
+
onLogout,
|
|
80
|
+
supportHref,
|
|
81
|
+
showThemeToggle = true,
|
|
82
|
+
actions,
|
|
83
|
+
className,
|
|
84
|
+
}: DashboardTopbarProps) {
|
|
85
|
+
return (
|
|
86
|
+
<header
|
|
87
|
+
data-slot="dashboard-topbar"
|
|
88
|
+
className={cn("h-16 flex-shrink-0 bg-page-bg", className)}
|
|
89
|
+
>
|
|
90
|
+
<div className="mx-auto flex h-full max-w-[1280px] items-center gap-2 px-4 sm:gap-3 sm:px-6 md:gap-4 md:px-8">
|
|
91
|
+
{/* Sidebar toggle — collapse/open the navigation. */}
|
|
92
|
+
<SidebarTrigger
|
|
93
|
+
aria-label="Toggle navigation"
|
|
94
|
+
className="size-10 border border-input bg-background sm:size-9"
|
|
95
|
+
/>
|
|
96
|
+
|
|
97
|
+
{/* Search — looks like an input, opens the consumer's command palette. */}
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
onClick={onSearch}
|
|
101
|
+
aria-label={searchPlaceholder}
|
|
102
|
+
className="group min-w-0 max-w-xl flex-1 outline-none"
|
|
103
|
+
>
|
|
104
|
+
<span className="relative flex h-10 w-full items-center rounded-lg border border-input bg-background pl-10 pr-3 text-sm text-muted-foreground transition-colors group-hover:bg-accent/50 group-focus-visible:ring-2 group-focus-visible:ring-ring sm:pr-16">
|
|
105
|
+
<Search
|
|
106
|
+
className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"
|
|
107
|
+
strokeWidth={2}
|
|
108
|
+
/>
|
|
109
|
+
{searchPlaceholder}
|
|
110
|
+
{searchShortcut}
|
|
111
|
+
</span>
|
|
112
|
+
</button>
|
|
113
|
+
|
|
114
|
+
{/* Right cluster */}
|
|
115
|
+
<div className="ml-auto flex flex-shrink-0 items-center gap-2 sm:gap-3">
|
|
116
|
+
{actions}
|
|
117
|
+
|
|
118
|
+
{/* Theme toggle. The toggler is anchored inside a wrapper span rather
|
|
119
|
+
than being the TooltipTrigger's render target directly:
|
|
120
|
+
AnimatedThemeToggler spreads incoming props (including the ref Base
|
|
121
|
+
UI injects) AFTER its own `ref={buttonRef}`, so making it the
|
|
122
|
+
trigger clobbers that ref — and its click handler bails out on
|
|
123
|
+
`if (!buttonRef.current) return`, so the theme never flips.
|
|
124
|
+
Wrapping keeps the toggler's own ref + onClick intact while the
|
|
125
|
+
span carries the tooltip. */}
|
|
126
|
+
{showThemeToggle ? (
|
|
127
|
+
<Tooltip>
|
|
128
|
+
<TooltipTrigger render={<span className="inline-flex" />}>
|
|
129
|
+
<AnimatedThemeToggler
|
|
130
|
+
aria-label="Toggle light / dark theme"
|
|
131
|
+
className="size-10 border border-input bg-background sm:size-9"
|
|
132
|
+
variant="circle"
|
|
133
|
+
/>
|
|
134
|
+
</TooltipTrigger>
|
|
135
|
+
<TooltipContent>Toggle light / dark theme</TooltipContent>
|
|
136
|
+
</Tooltip>
|
|
137
|
+
) : null}
|
|
138
|
+
|
|
139
|
+
{/* Support */}
|
|
140
|
+
{supportHref ? (
|
|
141
|
+
<Tooltip>
|
|
142
|
+
<TooltipTrigger
|
|
143
|
+
render={
|
|
144
|
+
<Button
|
|
145
|
+
aria-label="Contact support"
|
|
146
|
+
className="hidden sm:inline-flex"
|
|
147
|
+
render={<a href={supportHref} />}
|
|
148
|
+
size="icon-lg"
|
|
149
|
+
variant="outline"
|
|
150
|
+
>
|
|
151
|
+
<LifeBuoy strokeWidth={1.8} />
|
|
152
|
+
</Button>
|
|
153
|
+
}
|
|
154
|
+
/>
|
|
155
|
+
<TooltipContent>Contact support</TooltipContent>
|
|
156
|
+
</Tooltip>
|
|
157
|
+
) : null}
|
|
158
|
+
|
|
159
|
+
{/* Account menu */}
|
|
160
|
+
<Menu>
|
|
161
|
+
<MenuTrigger
|
|
162
|
+
render={
|
|
163
|
+
<button
|
|
164
|
+
type="button"
|
|
165
|
+
aria-label={`Account menu — signed in as ${user.name}`}
|
|
166
|
+
className="rounded-full outline-none transition hover:ring-2 hover:ring-ring focus-visible:ring-2 focus-visible:ring-ring"
|
|
167
|
+
/>
|
|
168
|
+
}
|
|
169
|
+
>
|
|
170
|
+
<Avatar
|
|
171
|
+
alt={user.name}
|
|
172
|
+
initials={user.initials}
|
|
173
|
+
size="md"
|
|
174
|
+
src={user.avatarSrc}
|
|
175
|
+
/>
|
|
176
|
+
</MenuTrigger>
|
|
177
|
+
<MenuPopup
|
|
178
|
+
align="end"
|
|
179
|
+
className="w-[min(280px,calc(100vw-32px))]"
|
|
180
|
+
sideOffset={8}
|
|
181
|
+
>
|
|
182
|
+
<div className="flex items-center gap-3 px-2 py-2">
|
|
183
|
+
<Avatar
|
|
184
|
+
alt=""
|
|
185
|
+
initials={user.initials}
|
|
186
|
+
size="md"
|
|
187
|
+
src={user.avatarSrc}
|
|
188
|
+
/>
|
|
189
|
+
<div className="min-w-0">
|
|
190
|
+
<p className="truncate text-sm font-semibold text-foreground">
|
|
191
|
+
{user.name}
|
|
192
|
+
</p>
|
|
193
|
+
{user.email ? (
|
|
194
|
+
<p className="truncate text-xs text-muted-foreground">
|
|
195
|
+
{user.email}
|
|
196
|
+
</p>
|
|
197
|
+
) : null}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
{(onSettings || onLogout) ? <MenuSeparator /> : null}
|
|
201
|
+
{onSettings ? (
|
|
202
|
+
<MenuItem onClick={onSettings}>
|
|
203
|
+
<Settings strokeWidth={1.8} />
|
|
204
|
+
Settings
|
|
205
|
+
</MenuItem>
|
|
206
|
+
) : null}
|
|
207
|
+
{onLogout ? (
|
|
208
|
+
<MenuItem onClick={onLogout} variant="destructive">
|
|
209
|
+
<LogOut strokeWidth={1.8} />
|
|
210
|
+
Log out
|
|
211
|
+
</MenuItem>
|
|
212
|
+
) : null}
|
|
213
|
+
</MenuPopup>
|
|
214
|
+
</Menu>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
</header>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export default DashboardTopbar;
|
package/src/index.ts
CHANGED
|
@@ -99,6 +99,7 @@ export * from "./components/ui/video-player";
|
|
|
99
99
|
// Layout components
|
|
100
100
|
export * from "./components/layout/dashboard-header";
|
|
101
101
|
export * from "./components/layout/dashboard-panel";
|
|
102
|
+
export * from "./components/layout/dashboard-topbar";
|
|
102
103
|
|
|
103
104
|
// Theme
|
|
104
105
|
export * from "./components/theme-provider";
|