Skip to content

Commit 1ff0454

Browse files
committed
add menu bar component for storybook
1 parent 6e56d9b commit 1ff0454

File tree

3 files changed

+845
-1
lines changed

3 files changed

+845
-1
lines changed

app/components/ui/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
export { Button, buttonVariants } from './button';
1+
export { Button, buttonVariants, type ButtonProps } from './button';
2+
export { MenuBar, MenuItem, menuBarVariants, menuItemVariants, type MenuBarProps, type MenuItemProps } from './menubar';

app/components/ui/menubar.tsx

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import * as React from 'react';
2+
import { cva, type VariantProps } from 'class-variance-authority';
3+
import { cn } from '../../lib/utils';
4+
5+
const menuBarVariants = cva('flex items-stretch transition-all duration-300 ease-in-out', {
6+
variants: {
7+
variant: {
8+
default: 'p-0',
9+
contained:
10+
'relative overflow-visible rounded-full border-2 border-black bg-white h-12 pl-0 pr-[3px] py-0',
11+
},
12+
size: { default: '', sm: 'p-[2px]', lg: 'p-2' },
13+
collapsed: { true: 'w-auto', false: '' },
14+
},
15+
defaultVariants: { variant: 'default', size: 'default', collapsed: false },
16+
});
17+
18+
const menuItemVariants = cva(
19+
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-full px-4 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border-2 border-border shadow-[var(--shadow-shadow)]',
20+
{
21+
variants: {
22+
variant: {
23+
default: 'bg-background text-foreground hover:bg-accent hover:text-accent-foreground',
24+
active: 'bg-black text-white border-black hover:bg-gray-800',
25+
neutral: 'bg-secondary-background text-foreground hover:bg-secondary-background/80',
26+
electric: 'bg-yellow-300 text-black border-black hover:bg-yellow-400',
27+
hot: 'bg-pink-400 text-black border-black hover:bg-pink-300',
28+
cyber: 'bg-lime-400 text-black border-black hover:bg-lime-300',
29+
retro: 'bg-orange-400 text-black border-black hover:bg-orange-300',
30+
cool: 'bg-cyan-400 text-black border-black hover:bg-cyan-300',
31+
dream: 'bg-violet-400 text-black border-black hover:bg-violet-300',
32+
danger: 'bg-red-400 text-black border-black hover:bg-red-300',
33+
game: 'bg-pink-400 text-black border-black hover:bg-pink-300',
34+
health: 'bg-green-500 text-black border-black hover:bg-green-400',
35+
social: 'bg-yellow-400 text-black border-black hover:bg-yellow-300',
36+
education: 'bg-blue-500 text-black border-black hover:bg-blue-400',
37+
ghost:
38+
'bg-transparent border-transparent text-foreground hover:bg-accent hover:text-accent-foreground shadow-none',
39+
},
40+
size: {
41+
default: 'px-4 py-2 text-sm',
42+
sm: 'px-3 py-1 text-xs',
43+
lg: 'px-6 py-3 text-base',
44+
},
45+
},
46+
defaultVariants: {
47+
variant: 'default',
48+
size: 'default',
49+
},
50+
}
51+
);
52+
53+
export interface MenuBarProps
54+
extends React.HTMLAttributes<HTMLDivElement>,
55+
VariantProps<typeof menuBarVariants> {
56+
children: React.ReactNode;
57+
collapsible?: boolean;
58+
collapsed?: boolean;
59+
onToggleCollapse?: () => void;
60+
logo?: React.ReactNode;
61+
}
62+
63+
export interface MenuItemProps
64+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
65+
VariantProps<typeof menuItemVariants> {
66+
children?: React.ReactNode;
67+
active?: boolean;
68+
icon?: React.ReactNode;
69+
}
70+
71+
// Change to the vibes logo later
72+
const VibesLogo = ({
73+
className,
74+
fullHeight = false,
75+
rail = '#fff',
76+
}: {
77+
className?: string;
78+
fullHeight?: boolean;
79+
rail?: string;
80+
}) => {
81+
return (
82+
<div className={cn('flex items-center', className)}>
83+
<div
84+
className={cn(
85+
'relative isolate flex items-center justify-center overflow-visible rounded-l-full bg-black font-bold text-white',
86+
fullHeight ? 'h-full min-w-[96px] px-4' : 'h-10 px-3'
87+
)}
88+
>
89+
<span className="relative z-10 tracking-wide">VIBES</span>
90+
<div
91+
aria-hidden
92+
className="pointer-events-none absolute top-1/2 right-0 z-10 aspect-square translate-x-1/2 -translate-y-1/2 rounded-full"
93+
style={{
94+
background: rail,
95+
height: 'calc(100% + 2px)',
96+
clipPath: 'inset(0 50% 0 0)',
97+
}}
98+
/>
99+
<div
100+
aria-hidden
101+
className="pointer-events-none absolute top-1/2 right-0 z-20 aspect-square translate-x-1/2 -translate-y-1/2 rounded-full"
102+
style={{
103+
height: '100%',
104+
border: '2px solid #000',
105+
clipPath: 'inset(0 50% 0 0)',
106+
background: 'transparent',
107+
}}
108+
/>
109+
</div>
110+
</div>
111+
);
112+
};
113+
114+
const CloseButton = ({ onClick, className }: { onClick?: () => void; className?: string }) => (
115+
<button
116+
onClick={onClick}
117+
className={cn(
118+
'mr-2 flex h-8 w-8 items-center justify-center rounded-full bg-black text-white',
119+
className
120+
)}
121+
>
122+
<span className="text-sm font-bold">×</span>
123+
</button>
124+
);
125+
126+
// Expand Button Component
127+
const ExpandButton = ({ onClick, className }: { onClick?: () => void; className?: string }) => (
128+
<button
129+
onClick={onClick}
130+
className={cn(
131+
'relative z-50 flex h-10 w-10 items-center justify-center rounded-r-full bg-white text-black',
132+
className
133+
)}
134+
aria-label="Expand"
135+
>
136+
<span className="text-2xl leading-none"></span>
137+
</button>
138+
);
139+
140+
const MenuBar = React.forwardRef<HTMLDivElement, MenuBarProps>(
141+
(
142+
{
143+
className,
144+
variant,
145+
size,
146+
children,
147+
collapsible = false,
148+
collapsed = false,
149+
onToggleCollapse,
150+
logo,
151+
...props
152+
},
153+
ref
154+
) => {
155+
const [isCollapsed, setIsCollapsed] = React.useState(collapsed);
156+
157+
const handleToggle = () => {
158+
setIsCollapsed(!isCollapsed);
159+
onToggleCollapse?.();
160+
};
161+
162+
const actuallyCollapsed = collapsed !== undefined ? collapsed : isCollapsed;
163+
164+
if (collapsible && actuallyCollapsed) {
165+
return (
166+
<div
167+
className={cn(
168+
'border-border bg-background flex h-12 w-fit items-center overflow-hidden rounded-full border-2 shadow-[var(--shadow-shadow)] transition-all duration-300 ease-in-out',
169+
className
170+
)}
171+
ref={ref}
172+
{...props}
173+
>
174+
{logo || <VibesLogo className="h-full" fullHeight rail="#fff" />}
175+
<ExpandButton onClick={handleToggle} className="h-full" />
176+
</div>
177+
);
178+
}
179+
180+
return (
181+
<div
182+
className={cn(menuBarVariants({ variant, size }), 'gap-0', className)}
183+
ref={ref}
184+
{...props}
185+
>
186+
{collapsible && (logo || <VibesLogo className="h-full" fullHeight rail="#fff" />)}
187+
<div className="relative flex h-full flex-1 items-center gap-2 px-3 py-[3px]">
188+
{children}
189+
{collapsible && (
190+
<CloseButton
191+
onClick={handleToggle}
192+
className="absolute top-1/2 right-3 -translate-y-1/2"
193+
/>
194+
)}
195+
</div>
196+
</div>
197+
);
198+
}
199+
);
200+
MenuBar.displayName = 'MenuBar';
201+
202+
const MenuItem = React.forwardRef<HTMLButtonElement, MenuItemProps>(
203+
({ className, variant, size, active, icon, children, ...props }, ref) => {
204+
const itemVariant = active ? 'active' : variant;
205+
return (
206+
<button
207+
className={cn(menuItemVariants({ variant: itemVariant, size, className }))}
208+
ref={ref}
209+
{...props}
210+
>
211+
{icon && <span className="flex-shrink-0">{icon}</span>}
212+
{children && <span>{children}</span>}
213+
</button>
214+
);
215+
}
216+
);
217+
MenuItem.displayName = 'MenuItem';
218+
219+
export {
220+
MenuBar,
221+
MenuItem,
222+
menuBarVariants,
223+
menuItemVariants,
224+
VibesLogo,
225+
CloseButton,
226+
ExpandButton,
227+
};

0 commit comments

Comments
 (0)