rechta-ds 0.0.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/.changeset/config.json +11 -0
- package/.github/workflows/release.yml +53 -0
- package/.github/workflows/storybook.yml +34 -0
- package/.storybook/main.ts +17 -0
- package/.storybook/preview.ts +35 -0
- package/CHANGELOG.md +65 -0
- package/CONTRIBUTING.md +106 -0
- package/README.md +206 -0
- package/package.json +30 -0
- package/packages/tokens/build.js +357 -0
- package/packages/tokens/package.json +44 -0
- package/packages/tokens/src/tokens.json +1538 -0
- package/packages/ui/.storybook/main.ts +17 -0
- package/packages/ui/.storybook/preview.tsx +37 -0
- package/packages/ui/package.json +109 -0
- package/packages/ui/postcss.config.js +6 -0
- package/packages/ui/src/components/atoms/Avatar.tsx +139 -0
- package/packages/ui/src/components/atoms/Badge.tsx +62 -0
- package/packages/ui/src/components/atoms/Button.tsx +125 -0
- package/packages/ui/src/components/atoms/Input.tsx +116 -0
- package/packages/ui/src/components/atoms/Misc.tsx +128 -0
- package/packages/ui/src/components/atoms/Toggle.tsx +191 -0
- package/packages/ui/src/components/atoms/Typography.tsx +178 -0
- package/packages/ui/src/components/atoms/index.ts +7 -0
- package/packages/ui/src/components/charts/Charts.tsx +380 -0
- package/packages/ui/src/components/charts/DataTable.tsx +222 -0
- package/packages/ui/src/components/charts/index.ts +19 -0
- package/packages/ui/src/components/molecules/Accordion.tsx +93 -0
- package/packages/ui/src/components/molecules/Card.tsx +100 -0
- package/packages/ui/src/components/molecules/PricingCard.tsx +196 -0
- package/packages/ui/src/components/molecules/TestimonialCard.tsx +85 -0
- package/packages/ui/src/components/molecules/Tooltip.tsx +71 -0
- package/packages/ui/src/components/molecules/index.ts +5 -0
- package/packages/ui/src/components/organisms/FeatureTabs.tsx +196 -0
- package/packages/ui/src/components/organisms/LogoMarquee.tsx +119 -0
- package/packages/ui/src/components/organisms/Navbar.tsx +194 -0
- package/packages/ui/src/components/organisms/index.ts +3 -0
- package/packages/ui/src/index.ts +15 -0
- package/packages/ui/src/lib/utils.ts +12 -0
- package/packages/ui/src/stories/atoms/Avatar.stories.tsx +49 -0
- package/packages/ui/src/stories/atoms/Badge.stories.tsx +68 -0
- package/packages/ui/src/stories/atoms/Button.stories.tsx +98 -0
- package/packages/ui/src/stories/atoms/Input.stories.tsx +66 -0
- package/packages/ui/src/stories/atoms/Toggle.stories.tsx +36 -0
- package/packages/ui/src/stories/molecules/Accordion.stories.tsx +47 -0
- package/packages/ui/src/stories/molecules/Card.stories.tsx +84 -0
- package/packages/ui/src/stories/molecules/PricingCard.stories.tsx +62 -0
- package/packages/ui/src/stories/molecules/TestimonialCard.stories.tsx +52 -0
- package/packages/ui/src/stories/molecules/Tooltip.stories.tsx +66 -0
- package/packages/ui/src/stories/organisms/LogoMarquee.stories.tsx +33 -0
- package/packages/ui/src/stories/organisms/Navbar.stories.tsx +37 -0
- package/packages/ui/src/styles/globals.css +220 -0
- package/packages/ui/tailwind.config.ts +68 -0
- package/packages/ui/tsconfig.json +23 -0
- package/packages/ui/tsup.config.ts +24 -0
- package/packages/ui/vite.config.ts +17 -0
- package/pnpm-workspace.yaml +2 -0
- package/turbo.json +33 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
import { Button } from '../atoms/Button';
|
|
5
|
+
import { ArrowRight } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
// ─── Tabs primitives ──────────────────────────────────────────────────────────
|
|
8
|
+
const TabsRoot = TabsPrimitive.Root;
|
|
9
|
+
|
|
10
|
+
const TabsList = React.forwardRef<
|
|
11
|
+
React.ElementRef<typeof TabsPrimitive.List>,
|
|
12
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
|
13
|
+
>(({ className, ...props }, ref) => (
|
|
14
|
+
<TabsPrimitive.List
|
|
15
|
+
ref={ref}
|
|
16
|
+
className={cn(
|
|
17
|
+
'inline-flex items-center gap-1 rounded-xl bg-surface border border-border p-1',
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
));
|
|
23
|
+
TabsList.displayName = 'TabsList';
|
|
24
|
+
|
|
25
|
+
const TabsTrigger = React.forwardRef<
|
|
26
|
+
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
|
27
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
|
28
|
+
>(({ className, ...props }, ref) => (
|
|
29
|
+
<TabsPrimitive.Trigger
|
|
30
|
+
ref={ref}
|
|
31
|
+
className={cn(
|
|
32
|
+
'inline-flex items-center justify-center whitespace-nowrap rounded-lg',
|
|
33
|
+
'px-4 py-2 text-sm font-medium font-body',
|
|
34
|
+
'text-text-tertiary transition-all duration-200',
|
|
35
|
+
'hover:text-text-secondary',
|
|
36
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500',
|
|
37
|
+
'disabled:pointer-events-none disabled:opacity-50',
|
|
38
|
+
'data-[state=active]:bg-surface-raised data-[state=active]:text-text-primary data-[state=active]:shadow-sm',
|
|
39
|
+
'data-[state=active]:border data-[state=active]:border-border',
|
|
40
|
+
className
|
|
41
|
+
)}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
));
|
|
45
|
+
TabsTrigger.displayName = 'TabsTrigger';
|
|
46
|
+
|
|
47
|
+
const TabsContent = React.forwardRef<
|
|
48
|
+
React.ElementRef<typeof TabsPrimitive.Content>,
|
|
49
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
|
50
|
+
>(({ className, ...props }, ref) => (
|
|
51
|
+
<TabsPrimitive.Content
|
|
52
|
+
ref={ref}
|
|
53
|
+
className={cn(
|
|
54
|
+
'focus-visible:outline-none',
|
|
55
|
+
'data-[state=active]:animate-fade-up',
|
|
56
|
+
className
|
|
57
|
+
)}
|
|
58
|
+
{...props}
|
|
59
|
+
/>
|
|
60
|
+
));
|
|
61
|
+
TabsContent.displayName = 'TabsContent';
|
|
62
|
+
|
|
63
|
+
// ─── Feature Tabs organism ────────────────────────────────────────────────────
|
|
64
|
+
export interface FeatureTab {
|
|
65
|
+
id: string;
|
|
66
|
+
label: string;
|
|
67
|
+
overline: string;
|
|
68
|
+
heading: string;
|
|
69
|
+
description: string;
|
|
70
|
+
ctaLabel: string;
|
|
71
|
+
ctaHref?: string;
|
|
72
|
+
onCtaClick?: () => void;
|
|
73
|
+
media?: string; // video URL or GIF
|
|
74
|
+
mediaType?: 'video' | 'image';
|
|
75
|
+
mediaPoster?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface FeatureTabsProps {
|
|
79
|
+
tabs: FeatureTab[];
|
|
80
|
+
eyebrow?: string;
|
|
81
|
+
heading?: string;
|
|
82
|
+
productTourLabel?: string;
|
|
83
|
+
onProductTourClick?: () => void;
|
|
84
|
+
className?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const FeatureTabs: React.FC<FeatureTabsProps> = ({
|
|
88
|
+
tabs,
|
|
89
|
+
eyebrow,
|
|
90
|
+
heading,
|
|
91
|
+
productTourLabel = 'Take product tour',
|
|
92
|
+
onProductTourClick,
|
|
93
|
+
className,
|
|
94
|
+
}) => {
|
|
95
|
+
const [active, setActive] = React.useState(tabs[0]?.id ?? '');
|
|
96
|
+
const activeTab = tabs.find((t) => t.id === active) ?? tabs[0];
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<section className={cn('w-full', className)}>
|
|
100
|
+
{/* Section header */}
|
|
101
|
+
{(eyebrow || heading) && (
|
|
102
|
+
<div className="text-center mb-12 max-w-2xl mx-auto">
|
|
103
|
+
{eyebrow && (
|
|
104
|
+
<p className="text-xs font-mono text-text-tertiary tracking-widest uppercase mb-3">{eyebrow}</p>
|
|
105
|
+
)}
|
|
106
|
+
{heading && (
|
|
107
|
+
<h2 className="font-display text-4xl font-bold text-text-primary tracking-tight">{heading}</h2>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
<TabsRoot value={active} onValueChange={setActive}>
|
|
113
|
+
{/* Tab list */}
|
|
114
|
+
<div className="flex justify-center mb-10">
|
|
115
|
+
<TabsList>
|
|
116
|
+
{tabs.map((tab) => (
|
|
117
|
+
<TabsTrigger key={tab.id} value={tab.id}>
|
|
118
|
+
{tab.label}
|
|
119
|
+
</TabsTrigger>
|
|
120
|
+
))}
|
|
121
|
+
</TabsList>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
{/* Tab panels */}
|
|
125
|
+
{tabs.map((tab) => (
|
|
126
|
+
<TabsContent key={tab.id} value={tab.id}>
|
|
127
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
|
128
|
+
{/* Text side */}
|
|
129
|
+
<div className="flex flex-col gap-6">
|
|
130
|
+
<div>
|
|
131
|
+
<p className="text-xs font-mono text-brand-400 tracking-widest uppercase mb-3">
|
|
132
|
+
{tab.overline}
|
|
133
|
+
</p>
|
|
134
|
+
<h3 className="font-display text-3xl font-bold text-text-primary tracking-tight mb-4 leading-tight">
|
|
135
|
+
{tab.heading}
|
|
136
|
+
</h3>
|
|
137
|
+
<p className="text-text-secondary leading-relaxed">
|
|
138
|
+
{tab.description}
|
|
139
|
+
</p>
|
|
140
|
+
</div>
|
|
141
|
+
<Button
|
|
142
|
+
variant="ghost"
|
|
143
|
+
size="md"
|
|
144
|
+
rightIcon={<ArrowRight className="size-4" />}
|
|
145
|
+
onClick={tab.onCtaClick}
|
|
146
|
+
className="self-start"
|
|
147
|
+
>
|
|
148
|
+
{tab.ctaLabel}
|
|
149
|
+
</Button>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
{/* Media side */}
|
|
153
|
+
{tab.media && (
|
|
154
|
+
<div className="relative rounded-2xl overflow-hidden border border-border bg-surface-sunken aspect-square">
|
|
155
|
+
{tab.mediaType === 'video' ? (
|
|
156
|
+
<video
|
|
157
|
+
src={tab.media}
|
|
158
|
+
poster={tab.mediaPoster}
|
|
159
|
+
autoPlay
|
|
160
|
+
loop
|
|
161
|
+
muted
|
|
162
|
+
playsInline
|
|
163
|
+
className="w-full h-full object-cover"
|
|
164
|
+
/>
|
|
165
|
+
) : (
|
|
166
|
+
<img
|
|
167
|
+
src={tab.media}
|
|
168
|
+
alt={tab.heading}
|
|
169
|
+
className="w-full h-full object-cover"
|
|
170
|
+
/>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
</TabsContent>
|
|
176
|
+
))}
|
|
177
|
+
</TabsRoot>
|
|
178
|
+
|
|
179
|
+
{/* Product tour CTA */}
|
|
180
|
+
{productTourLabel && (
|
|
181
|
+
<div className="text-center mt-10">
|
|
182
|
+
<Button
|
|
183
|
+
variant="outline"
|
|
184
|
+
size="sm"
|
|
185
|
+
rightIcon={<ArrowRight className="size-3.5" />}
|
|
186
|
+
onClick={onProductTourClick}
|
|
187
|
+
>
|
|
188
|
+
{productTourLabel}
|
|
189
|
+
</Button>
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
</section>
|
|
193
|
+
);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export { TabsRoot, TabsList, TabsTrigger, TabsContent, FeatureTabs };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '../../lib/utils';
|
|
3
|
+
|
|
4
|
+
export interface LogoItem {
|
|
5
|
+
src: string;
|
|
6
|
+
alt: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface LogoMarqueeProps {
|
|
11
|
+
logos: LogoItem[];
|
|
12
|
+
title?: string;
|
|
13
|
+
rows?: 1 | 2;
|
|
14
|
+
speed?: 'slow' | 'normal' | 'fast';
|
|
15
|
+
direction?: 'left' | 'right';
|
|
16
|
+
pauseOnHover?: boolean;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const speedMap = {
|
|
21
|
+
slow: 'animate-[marquee_50s_linear_infinite]',
|
|
22
|
+
normal: 'animate-[marquee_30s_linear_infinite]',
|
|
23
|
+
fast: 'animate-[marquee_15s_linear_infinite]',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const LogoTile: React.FC<{ logo: LogoItem }> = ({ logo }) => (
|
|
27
|
+
<div className="flex items-center justify-center px-6 shrink-0">
|
|
28
|
+
<div className="flex flex-col items-center gap-2">
|
|
29
|
+
<div className="h-10 w-24 flex items-center justify-center">
|
|
30
|
+
<img
|
|
31
|
+
src={logo.src}
|
|
32
|
+
alt={logo.alt}
|
|
33
|
+
className="max-h-8 max-w-[80px] object-contain opacity-40 hover:opacity-70 transition-opacity duration-300 filter brightness-200"
|
|
34
|
+
/>
|
|
35
|
+
</div>
|
|
36
|
+
{logo.name && (
|
|
37
|
+
<span className="text-[10px] font-mono text-text-disabled tracking-wider uppercase whitespace-nowrap">
|
|
38
|
+
{logo.name}
|
|
39
|
+
</span>
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const MarqueeRow: React.FC<{
|
|
46
|
+
logos: LogoItem[];
|
|
47
|
+
speed: keyof typeof speedMap;
|
|
48
|
+
reverse?: boolean;
|
|
49
|
+
pauseOnHover?: boolean;
|
|
50
|
+
}> = ({ logos, speed, reverse, pauseOnHover }) => {
|
|
51
|
+
// Duplicate for seamless loop
|
|
52
|
+
const duplicated = [...logos, ...logos, ...logos, ...logos];
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className={cn('flex overflow-hidden', pauseOnHover && 'hover:[&>div]:pause')}>
|
|
56
|
+
<div
|
|
57
|
+
className={cn(
|
|
58
|
+
'flex shrink-0',
|
|
59
|
+
speedMap[speed],
|
|
60
|
+
reverse && '[animation-direction:reverse]'
|
|
61
|
+
)}
|
|
62
|
+
style={{ animationPlayState: 'running' }}
|
|
63
|
+
>
|
|
64
|
+
{duplicated.map((logo, i) => (
|
|
65
|
+
<LogoTile key={i} logo={logo} />
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const LogoMarquee: React.FC<LogoMarqueeProps> = ({
|
|
73
|
+
logos,
|
|
74
|
+
title,
|
|
75
|
+
rows = 1,
|
|
76
|
+
speed = 'normal',
|
|
77
|
+
direction = 'left',
|
|
78
|
+
pauseOnHover = true,
|
|
79
|
+
className,
|
|
80
|
+
}) => {
|
|
81
|
+
const half = Math.ceil(logos.length / 2);
|
|
82
|
+
const row1 = logos.slice(0, half);
|
|
83
|
+
const row2 = logos.slice(half);
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div className={cn('w-full overflow-hidden', className)}>
|
|
87
|
+
{title && (
|
|
88
|
+
<p className="text-xs font-mono text-text-disabled tracking-widest uppercase text-center mb-6">
|
|
89
|
+
{title}
|
|
90
|
+
</p>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
{/* Fade masks */}
|
|
94
|
+
<div className="relative">
|
|
95
|
+
<div className="absolute left-0 top-0 bottom-0 w-24 bg-gradient-to-r from-bg-base to-transparent z-10 pointer-events-none" />
|
|
96
|
+
<div className="absolute right-0 top-0 bottom-0 w-24 bg-gradient-to-l from-bg-base to-transparent z-10 pointer-events-none" />
|
|
97
|
+
|
|
98
|
+
<div className="flex flex-col gap-4">
|
|
99
|
+
<MarqueeRow
|
|
100
|
+
logos={rows === 1 ? logos : row1}
|
|
101
|
+
speed={speed}
|
|
102
|
+
reverse={direction === 'right'}
|
|
103
|
+
pauseOnHover={pauseOnHover}
|
|
104
|
+
/>
|
|
105
|
+
{rows === 2 && (
|
|
106
|
+
<MarqueeRow
|
|
107
|
+
logos={row2}
|
|
108
|
+
speed={speed}
|
|
109
|
+
reverse={direction === 'left'}
|
|
110
|
+
pauseOnHover={pauseOnHover}
|
|
111
|
+
/>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export { LogoMarquee, LogoTile };
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
export interface NavDropdownItem {
|
|
4
|
+
label: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
href: string;
|
|
7
|
+
icon?: React.ReactNode;
|
|
8
|
+
badge?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface NavDropdownGroup {
|
|
11
|
+
label?: string;
|
|
12
|
+
items: NavDropdownItem[];
|
|
13
|
+
}
|
|
14
|
+
export interface NavItem {
|
|
15
|
+
label: string;
|
|
16
|
+
href?: string;
|
|
17
|
+
groups?: NavDropdownGroup[];
|
|
18
|
+
}
|
|
19
|
+
export interface NavbarProps {
|
|
20
|
+
logo: React.ReactNode;
|
|
21
|
+
items: NavItem[];
|
|
22
|
+
announcementBar?: { message: React.ReactNode; href?: string };
|
|
23
|
+
ctaPrimary?: { label: string; href?: string; onClick?: () => void };
|
|
24
|
+
ctaSecondary?: { label: string; href?: string; onClick?: () => void };
|
|
25
|
+
loginHref?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const NAV: React.CSSProperties & Record<string, string> = {
|
|
29
|
+
fontFamily: "var(--font-sans)",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const Navbar: React.FC<NavbarProps> = ({
|
|
33
|
+
logo, items, announcementBar, ctaPrimary, ctaSecondary, loginHref,
|
|
34
|
+
}) => {
|
|
35
|
+
const [mobileOpen, setMobileOpen] = React.useState(false);
|
|
36
|
+
const [openDropdown, setOpenDropdown] = React.useState<string | null>(null);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div style={{ width: '100%', fontFamily: 'var(--font-sans)' }}>
|
|
40
|
+
{/* Announcement bar */}
|
|
41
|
+
{announcementBar && (
|
|
42
|
+
<div style={{
|
|
43
|
+
width: '100%', borderBottom: '1px solid var(--c-border-accent)',
|
|
44
|
+
background: 'var(--c-brand-subtle)', padding: '8px 24px',
|
|
45
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
46
|
+
}}>
|
|
47
|
+
<a href={announcementBar.href} style={{
|
|
48
|
+
display: 'flex', alignItems: 'center', gap: 8,
|
|
49
|
+
fontSize: 12, color: 'var(--c-text-accent)',
|
|
50
|
+
fontFamily: 'var(--font-mono)', letterSpacing: '.02em', textDecoration: 'none',
|
|
51
|
+
}}>
|
|
52
|
+
{announcementBar.message}
|
|
53
|
+
<span style={{ fontSize: 10 }}>→</span>
|
|
54
|
+
</a>
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
|
|
58
|
+
{/* Main nav — frosted glass */}
|
|
59
|
+
<nav style={{
|
|
60
|
+
width: '100%',
|
|
61
|
+
borderBottom: '1px solid var(--c-border)',
|
|
62
|
+
background: 'rgba(9,9,11,0.82)',
|
|
63
|
+
backdropFilter: 'blur(12px)',
|
|
64
|
+
WebkitBackdropFilter: 'blur(12px)',
|
|
65
|
+
position: 'sticky', top: 0, zIndex: 50,
|
|
66
|
+
}}>
|
|
67
|
+
<div style={{
|
|
68
|
+
maxWidth: 1200, margin: '0 auto', padding: '0 24px',
|
|
69
|
+
height: 60, display: 'flex', alignItems: 'center', gap: 32,
|
|
70
|
+
}}>
|
|
71
|
+
{/* Logo */}
|
|
72
|
+
<div style={{ flexShrink: 0 }}>{logo}</div>
|
|
73
|
+
|
|
74
|
+
{/* Desktop nav */}
|
|
75
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 2, flex: 1 }}>
|
|
76
|
+
{items.map(item => (
|
|
77
|
+
<div
|
|
78
|
+
key={item.label}
|
|
79
|
+
style={{ position: 'relative' }}
|
|
80
|
+
onMouseEnter={() => item.groups && setOpenDropdown(item.label)}
|
|
81
|
+
onMouseLeave={() => setOpenDropdown(null)}
|
|
82
|
+
>
|
|
83
|
+
{item.groups ? (
|
|
84
|
+
<button style={{
|
|
85
|
+
display: 'flex', alignItems: 'center', gap: 4,
|
|
86
|
+
padding: '6px 12px', borderRadius: 8, border: 'none', cursor: 'pointer',
|
|
87
|
+
fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 500,
|
|
88
|
+
color: openDropdown === item.label ? 'var(--c-text)' : 'var(--c-text-2)',
|
|
89
|
+
background: openDropdown === item.label ? 'var(--c-bg-surface-hover)' : 'transparent',
|
|
90
|
+
transition: 'all .15s',
|
|
91
|
+
}}>
|
|
92
|
+
{item.label}
|
|
93
|
+
<span style={{ fontSize: 9, color: 'var(--c-text-muted)', transform: openDropdown === item.label ? 'rotate(180deg)' : 'none', transition: 'transform .2s', display: 'inline-block' }}>▼</span>
|
|
94
|
+
</button>
|
|
95
|
+
) : (
|
|
96
|
+
<a href={item.href} style={{
|
|
97
|
+
display: 'flex', alignItems: 'center', padding: '6px 12px',
|
|
98
|
+
borderRadius: 8, fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 500,
|
|
99
|
+
color: 'var(--c-text-2)', textDecoration: 'none', transition: 'all .15s',
|
|
100
|
+
}}>
|
|
101
|
+
{item.label}
|
|
102
|
+
</a>
|
|
103
|
+
)}
|
|
104
|
+
|
|
105
|
+
{/* Dropdown */}
|
|
106
|
+
{item.groups && openDropdown === item.label && (
|
|
107
|
+
<div style={{ position: 'absolute', top: '100%', left: 0, paddingTop: 8, zIndex: 100, minWidth: 440 }}>
|
|
108
|
+
<div style={{
|
|
109
|
+
border: '1px solid var(--c-border)',
|
|
110
|
+
background: 'var(--c-bg-elevated)',
|
|
111
|
+
borderRadius: 14, padding: 16,
|
|
112
|
+
boxShadow: 'var(--shadow-xl)',
|
|
113
|
+
}}>
|
|
114
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4 }}>
|
|
115
|
+
{item.groups.map((group, gi) => (
|
|
116
|
+
<div key={gi}>
|
|
117
|
+
{group.label && (
|
|
118
|
+
<p style={{
|
|
119
|
+
fontSize: 10, fontFamily: 'var(--font-mono)', fontWeight: 700,
|
|
120
|
+
color: 'var(--c-text-muted)', letterSpacing: '.08em',
|
|
121
|
+
textTransform: 'uppercase', padding: '4px 8px 6px',
|
|
122
|
+
}}>{group.label}</p>
|
|
123
|
+
)}
|
|
124
|
+
{group.items.map((navItem, ni) => (
|
|
125
|
+
<a key={ni} href={navItem.href} style={{
|
|
126
|
+
display: 'flex', alignItems: 'flex-start', gap: 12,
|
|
127
|
+
padding: '10px 10px', borderRadius: 10, textDecoration: 'none',
|
|
128
|
+
transition: 'background .12s',
|
|
129
|
+
}}
|
|
130
|
+
onMouseEnter={e => (e.currentTarget.style.background = 'var(--c-bg-surface-hover)')}
|
|
131
|
+
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
|
|
132
|
+
>
|
|
133
|
+
{navItem.icon && (
|
|
134
|
+
<div style={{
|
|
135
|
+
width: 32, height: 32, borderRadius: 8, flexShrink: 0,
|
|
136
|
+
background: 'var(--c-bg-surface)', border: '1px solid var(--c-border)',
|
|
137
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
138
|
+
}}>{navItem.icon}</div>
|
|
139
|
+
)}
|
|
140
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
141
|
+
<span style={{ fontSize: 13, fontWeight: 500, color: 'var(--c-text)' }}>{navItem.label}</span>
|
|
142
|
+
{navItem.description && (
|
|
143
|
+
<span style={{ fontSize: 12, color: 'var(--c-text-3)', lineHeight: 1.4 }}>{navItem.description}</span>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
</a>
|
|
147
|
+
))}
|
|
148
|
+
</div>
|
|
149
|
+
))}
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{/* Right CTAs */}
|
|
159
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginLeft: 'auto' }}>
|
|
160
|
+
{loginHref && (
|
|
161
|
+
<a href={loginHref} style={{
|
|
162
|
+
fontSize: 13, fontWeight: 500, color: 'var(--c-text-2)',
|
|
163
|
+
padding: '6px 12px', borderRadius: 8, textDecoration: 'none', transition: 'color .15s',
|
|
164
|
+
}}>Log in</a>
|
|
165
|
+
)}
|
|
166
|
+
{ctaSecondary && (
|
|
167
|
+
<button onClick={ctaSecondary.onClick} style={{
|
|
168
|
+
height: 34, padding: '0 14px', borderRadius: 8, border: '1px solid var(--c-border)',
|
|
169
|
+
background: 'transparent', color: 'var(--c-text)', fontSize: 13, fontWeight: 500,
|
|
170
|
+
fontFamily: 'var(--font-sans)', cursor: 'pointer', transition: 'all .15s',
|
|
171
|
+
}}>{ctaSecondary.label}</button>
|
|
172
|
+
)}
|
|
173
|
+
{ctaPrimary && (
|
|
174
|
+
<button onClick={ctaPrimary.onClick} style={{
|
|
175
|
+
height: 34, padding: '0 16px', borderRadius: 8, border: 'none',
|
|
176
|
+
background: 'var(--c-brand)', color: '#fff', fontSize: 13, fontWeight: 500,
|
|
177
|
+
fontFamily: 'var(--font-sans)', cursor: 'pointer', transition: 'all .15s',
|
|
178
|
+
}}>{ctaPrimary.label}</button>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
{/* Mobile toggle */}
|
|
183
|
+
<button
|
|
184
|
+
style={{ display: 'none', padding: 8, background: 'none', border: 'none', cursor: 'pointer', color: 'var(--c-text-2)' }}
|
|
185
|
+
onClick={() => setMobileOpen(!mobileOpen)}
|
|
186
|
+
aria-label="Toggle menu"
|
|
187
|
+
>☰</button>
|
|
188
|
+
</div>
|
|
189
|
+
</nav>
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export { Navbar };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// ─── Atoms ────────────────────────────────────────────────────────────────────
|
|
2
|
+
export * from './components/atoms/index';
|
|
3
|
+
|
|
4
|
+
// ─── Molecules ────────────────────────────────────────────────────────────────
|
|
5
|
+
export * from './components/molecules/index';
|
|
6
|
+
|
|
7
|
+
// ─── Organisms ────────────────────────────────────────────────────────────────
|
|
8
|
+
export * from './components/organisms/index';
|
|
9
|
+
|
|
10
|
+
// ─── Charts ───────────────────────────────────────────────────────────────────
|
|
11
|
+
export * from './components/charts/index';
|
|
12
|
+
|
|
13
|
+
// ─── Utils ────────────────────────────────────────────────────────────────────
|
|
14
|
+
export { cn } from './lib/utils';
|
|
15
|
+
export type { WithClassName, AsChild, Size, Variant, Status } from './lib/utils';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ClassValue, clsx } from 'clsx';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
export function cn(...inputs: ClassValue[]) {
|
|
5
|
+
return twMerge(clsx(inputs));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type WithClassName = { className?: string };
|
|
9
|
+
export type AsChild = { asChild?: boolean };
|
|
10
|
+
export type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
11
|
+
export type Variant = 'default' | 'secondary' | 'ghost' | 'outline' | 'destructive' | 'link';
|
|
12
|
+
export type Status = 'success' | 'warning' | 'error' | 'info' | 'neutral';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Avatar } from '../../components/atoms/Avatar';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Avatar> = {
|
|
5
|
+
title: 'Atoms/Avatar',
|
|
6
|
+
component: Avatar,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
parameters: { layout: 'centered' },
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof Avatar>;
|
|
13
|
+
|
|
14
|
+
export const WithImage: Story = {
|
|
15
|
+
args: {
|
|
16
|
+
src: 'https://avatars.githubusercontent.com/u/28986134',
|
|
17
|
+
alt: 'Steven Tey',
|
|
18
|
+
fallback: 'ST',
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const WithFallback: Story = {
|
|
23
|
+
args: { fallback: 'JH' },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const Sizes: Story = {
|
|
27
|
+
render: () => (
|
|
28
|
+
<div className="flex items-end gap-4">
|
|
29
|
+
{(['xs','sm','md','lg','xl'] as const).map((size) => (
|
|
30
|
+
<Avatar key={size} size={size} fallback="RT" />
|
|
31
|
+
))}
|
|
32
|
+
</div>
|
|
33
|
+
),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const AvatarGroup: Story = {
|
|
37
|
+
render: () => (
|
|
38
|
+
<div className="flex -space-x-2">
|
|
39
|
+
{['ST','KK','GR','JH'].map((f, i) => (
|
|
40
|
+
<div key={i} className="ring-2 ring-[--c-bg] rounded-full">
|
|
41
|
+
<Avatar fallback={f} size="sm" />
|
|
42
|
+
</div>
|
|
43
|
+
))}
|
|
44
|
+
<div className="ring-2 ring-[--c-bg] rounded-full size-8 rounded-full bg-[--c-bg-surface] border border-[--c-border] text-[11px] font-medium text-[--c-text-3] flex items-center justify-center">
|
|
45
|
+
+8
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
),
|
|
49
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Badge } from '../../components/atoms/Badge';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Badge> = {
|
|
5
|
+
title: 'Atoms/Badge',
|
|
6
|
+
component: Badge,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
parameters: { layout: 'centered' },
|
|
9
|
+
argTypes: {
|
|
10
|
+
variant: {
|
|
11
|
+
control: 'select',
|
|
12
|
+
options: ['default','brand','emerald','blue','success','warning','error','info','outline','solid','solid-blue','porcelain'],
|
|
13
|
+
},
|
|
14
|
+
dot: { control: 'boolean' },
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default meta;
|
|
19
|
+
type Story = StoryObj<typeof Badge>;
|
|
20
|
+
|
|
21
|
+
export const Default: Story = {
|
|
22
|
+
args: { children: 'Badge' },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const AllVariants: Story = {
|
|
26
|
+
render: () => (
|
|
27
|
+
<div className="flex flex-wrap gap-2 items-center">
|
|
28
|
+
<Badge variant="default">Default</Badge>
|
|
29
|
+
<Badge variant="brand">Brand</Badge>
|
|
30
|
+
<Badge variant="emerald">Emerald</Badge>
|
|
31
|
+
<Badge variant="blue">Blue</Badge>
|
|
32
|
+
<Badge variant="success">Success</Badge>
|
|
33
|
+
<Badge variant="warning">Warning</Badge>
|
|
34
|
+
<Badge variant="error">Error</Badge>
|
|
35
|
+
<Badge variant="info">Info</Badge>
|
|
36
|
+
<Badge variant="outline">Outline</Badge>
|
|
37
|
+
<Badge variant="solid">Solid</Badge>
|
|
38
|
+
<Badge variant="solid-blue">Solid Blue</Badge>
|
|
39
|
+
<Badge variant="porcelain">Porcelain</Badge>
|
|
40
|
+
</div>
|
|
41
|
+
),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const WithDot: Story = {
|
|
45
|
+
render: () => (
|
|
46
|
+
<div className="flex flex-wrap gap-2 items-center">
|
|
47
|
+
<Badge variant="success" dot>Online</Badge>
|
|
48
|
+
<Badge variant="error" dot>Offline</Badge>
|
|
49
|
+
<Badge variant="warning" dot>Degraded</Badge>
|
|
50
|
+
<Badge variant="info" dot>Syncing</Badge>
|
|
51
|
+
<Badge variant="brand" dot>Active</Badge>
|
|
52
|
+
</div>
|
|
53
|
+
),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const UsageExamples: Story = {
|
|
57
|
+
render: () => (
|
|
58
|
+
<div className="flex flex-wrap gap-2 items-center">
|
|
59
|
+
<Badge variant="brand">v2.1.0</Badge>
|
|
60
|
+
<Badge variant="solid">Popular</Badge>
|
|
61
|
+
<Badge variant="blue">New</Badge>
|
|
62
|
+
<Badge variant="default">Pro</Badge>
|
|
63
|
+
<Badge variant="outline">Enterprise</Badge>
|
|
64
|
+
<Badge variant="success" dot>Verified</Badge>
|
|
65
|
+
<Badge variant="warning" dot>Pending</Badge>
|
|
66
|
+
</div>
|
|
67
|
+
),
|
|
68
|
+
};
|