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,128 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import * as SeparatorPrimitive from '@radix-ui/react-separator';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
|
|
5
|
+
// ─── Separator / Divider ─────────────────────────────────────────────────────
|
|
6
|
+
export interface SeparatorProps
|
|
7
|
+
extends React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> {
|
|
8
|
+
label?: string;
|
|
9
|
+
variant?: 'default' | 'subtle' | 'strong' | 'brand' | 'dashed';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const Separator = React.forwardRef<
|
|
13
|
+
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
|
14
|
+
SeparatorProps
|
|
15
|
+
>(({ className, label, variant = 'default', orientation = 'horizontal', decorative = true, ...props }, ref) => {
|
|
16
|
+
const lineClass = {
|
|
17
|
+
default: 'bg-border',
|
|
18
|
+
subtle: 'bg-border-subtle',
|
|
19
|
+
strong: 'bg-border-strong',
|
|
20
|
+
brand: 'bg-brand-500/30',
|
|
21
|
+
dashed: 'border-t border-dashed border-border bg-transparent',
|
|
22
|
+
}[variant];
|
|
23
|
+
|
|
24
|
+
if (label) {
|
|
25
|
+
return (
|
|
26
|
+
<div className={cn('flex items-center gap-4', className)}>
|
|
27
|
+
<div className={cn('flex-1 h-px', lineClass)} />
|
|
28
|
+
<span className="text-xs font-mono text-text-tertiary tracking-wider uppercase whitespace-nowrap">
|
|
29
|
+
{label}
|
|
30
|
+
</span>
|
|
31
|
+
<div className={cn('flex-1 h-px', lineClass)} />
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<SeparatorPrimitive.Root
|
|
38
|
+
ref={ref}
|
|
39
|
+
decorative={decorative}
|
|
40
|
+
orientation={orientation}
|
|
41
|
+
className={cn(
|
|
42
|
+
'shrink-0',
|
|
43
|
+
orientation === 'horizontal' ? 'h-px w-full' : 'h-full w-px',
|
|
44
|
+
lineClass,
|
|
45
|
+
className
|
|
46
|
+
)}
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
Separator.displayName = 'Separator';
|
|
52
|
+
|
|
53
|
+
// ─── Icon wrapper ─────────────────────────────────────────────────────────────
|
|
54
|
+
export interface IconProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
55
|
+
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
56
|
+
color?: 'default' | 'brand' | 'accent' | 'muted' | 'error' | 'success' | 'warning';
|
|
57
|
+
children: React.ReactNode;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const sizeClasses = {
|
|
61
|
+
xs: '[&_svg]:size-3',
|
|
62
|
+
sm: '[&_svg]:size-3.5',
|
|
63
|
+
md: '[&_svg]:size-4',
|
|
64
|
+
lg: '[&_svg]:size-5',
|
|
65
|
+
xl: '[&_svg]:size-6',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const colorClasses = {
|
|
69
|
+
default: 'text-text-secondary',
|
|
70
|
+
brand: 'text-brand-400',
|
|
71
|
+
accent: 'text-accent-400',
|
|
72
|
+
muted: 'text-text-tertiary',
|
|
73
|
+
error: 'text-red-400',
|
|
74
|
+
success: 'text-brand-400',
|
|
75
|
+
warning: 'text-amber-400',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const Icon: React.FC<IconProps> = ({ size = 'md', color = 'default', className, children, ...props }) => (
|
|
79
|
+
<span
|
|
80
|
+
className={cn(
|
|
81
|
+
'inline-flex items-center justify-center shrink-0',
|
|
82
|
+
sizeClasses[size],
|
|
83
|
+
colorClasses[color],
|
|
84
|
+
className
|
|
85
|
+
)}
|
|
86
|
+
{...props}
|
|
87
|
+
>
|
|
88
|
+
{children}
|
|
89
|
+
</span>
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// ─── Star Rating ──────────────────────────────────────────────────────────────
|
|
93
|
+
export interface StarRatingProps {
|
|
94
|
+
value: number;
|
|
95
|
+
max?: number;
|
|
96
|
+
size?: 'sm' | 'md' | 'lg';
|
|
97
|
+
showValue?: boolean;
|
|
98
|
+
className?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const StarRating: React.FC<StarRatingProps> = ({ value, max = 5, size = 'md', showValue, className }) => {
|
|
102
|
+
const starSize = { sm: 'size-3', md: 'size-4', lg: 'size-5' }[size];
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className={cn('inline-flex items-center gap-1.5', className)}>
|
|
106
|
+
<div className="flex items-center gap-0.5">
|
|
107
|
+
{Array.from({ length: max }).map((_, i) => (
|
|
108
|
+
<svg
|
|
109
|
+
key={i}
|
|
110
|
+
viewBox="0 0 20 20"
|
|
111
|
+
fill="currentColor"
|
|
112
|
+
className={cn(
|
|
113
|
+
starSize,
|
|
114
|
+
i < Math.floor(value) ? 'text-amber-400' : 'text-border-strong'
|
|
115
|
+
)}
|
|
116
|
+
>
|
|
117
|
+
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
|
118
|
+
</svg>
|
|
119
|
+
))}
|
|
120
|
+
</div>
|
|
121
|
+
{showValue && (
|
|
122
|
+
<span className="text-sm font-mono font-medium text-text-primary">{value.toFixed(1)}</span>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export { Separator, Icon, StarRating };
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import * as SwitchPrimitive from '@radix-ui/react-switch';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
|
|
5
|
+
// ─── Switch ───────────────────────────────────────────────────────────────────
|
|
6
|
+
export interface SwitchProps
|
|
7
|
+
extends React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root> {
|
|
8
|
+
size?: 'sm' | 'md' | 'lg';
|
|
9
|
+
label?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const Switch = React.forwardRef<
|
|
14
|
+
React.ElementRef<typeof SwitchPrimitive.Root>,
|
|
15
|
+
SwitchProps
|
|
16
|
+
>(({ className, size = 'md', label, description, id, ...props }, ref) => {
|
|
17
|
+
const sizeClasses = {
|
|
18
|
+
sm: { root: 'h-4 w-7', thumb: 'size-3 data-[state=checked]:translate-x-3' },
|
|
19
|
+
md: { root: 'h-5 w-9', thumb: 'size-4 data-[state=checked]:translate-x-4' },
|
|
20
|
+
lg: { root: 'h-6 w-11', thumb: 'size-5 data-[state=checked]:translate-x-5' },
|
|
21
|
+
}[size];
|
|
22
|
+
|
|
23
|
+
const switchEl = (
|
|
24
|
+
<SwitchPrimitive.Root
|
|
25
|
+
ref={ref}
|
|
26
|
+
id={id}
|
|
27
|
+
className={cn(
|
|
28
|
+
'peer inline-flex shrink-0 cursor-pointer items-center rounded-full border border-border',
|
|
29
|
+
'bg-surface transition-all duration-200',
|
|
30
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-2 focus-visible:ring-offset-bg-base',
|
|
31
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
32
|
+
'data-[state=checked]:bg-brand-500 data-[state=checked]:border-brand-500',
|
|
33
|
+
'hover:border-border-strong data-[state=checked]:hover:bg-brand-400',
|
|
34
|
+
sizeClasses.root,
|
|
35
|
+
className
|
|
36
|
+
)}
|
|
37
|
+
{...props}
|
|
38
|
+
>
|
|
39
|
+
<SwitchPrimitive.Thumb
|
|
40
|
+
className={cn(
|
|
41
|
+
'pointer-events-none block rounded-full bg-text-tertiary shadow-md',
|
|
42
|
+
'ring-0 transition-all duration-200',
|
|
43
|
+
'translate-x-0.5',
|
|
44
|
+
'data-[state=checked]:bg-gray-950',
|
|
45
|
+
sizeClasses.thumb
|
|
46
|
+
)}
|
|
47
|
+
/>
|
|
48
|
+
</SwitchPrimitive.Root>
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (!label) return switchEl;
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="flex items-start gap-3">
|
|
55
|
+
{switchEl}
|
|
56
|
+
<div className="flex flex-col gap-0.5">
|
|
57
|
+
<label htmlFor={id} className="text-sm font-medium text-text-primary cursor-pointer leading-tight">
|
|
58
|
+
{label}
|
|
59
|
+
</label>
|
|
60
|
+
{description && (
|
|
61
|
+
<span className="text-xs text-text-tertiary leading-normal">{description}</span>
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
Switch.displayName = 'Switch';
|
|
68
|
+
|
|
69
|
+
// ─── Checkbox ────────────────────────────────────────────────────────────────
|
|
70
|
+
export interface CheckboxProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
71
|
+
label?: string;
|
|
72
|
+
description?: string;
|
|
73
|
+
indeterminate?: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
|
77
|
+
({ className, label, description, id, indeterminate, ...props }, ref) => {
|
|
78
|
+
const internalRef = React.useRef<HTMLInputElement>(null);
|
|
79
|
+
const resolvedRef = (ref as React.RefObject<HTMLInputElement>) || internalRef;
|
|
80
|
+
|
|
81
|
+
React.useEffect(() => {
|
|
82
|
+
if (resolvedRef.current) {
|
|
83
|
+
resolvedRef.current.indeterminate = indeterminate ?? false;
|
|
84
|
+
}
|
|
85
|
+
}, [indeterminate, resolvedRef]);
|
|
86
|
+
|
|
87
|
+
const checkboxEl = (
|
|
88
|
+
<div className="relative flex items-center">
|
|
89
|
+
<input
|
|
90
|
+
ref={resolvedRef}
|
|
91
|
+
type="checkbox"
|
|
92
|
+
id={id}
|
|
93
|
+
className={cn(
|
|
94
|
+
'peer size-4 shrink-0 rounded border border-border bg-surface cursor-pointer',
|
|
95
|
+
'appearance-none transition-all duration-150',
|
|
96
|
+
'checked:bg-brand-500 checked:border-brand-500',
|
|
97
|
+
'indeterminate:bg-brand-500 indeterminate:border-brand-500',
|
|
98
|
+
'hover:border-border-strong',
|
|
99
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/50 focus-visible:ring-offset-1 focus-visible:ring-offset-bg-base',
|
|
100
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
101
|
+
className
|
|
102
|
+
)}
|
|
103
|
+
{...props}
|
|
104
|
+
/>
|
|
105
|
+
{/* Checkmark SVG overlay */}
|
|
106
|
+
<svg
|
|
107
|
+
className="absolute size-4 pointer-events-none hidden peer-checked:block text-gray-950"
|
|
108
|
+
viewBox="0 0 16 16" fill="none"
|
|
109
|
+
>
|
|
110
|
+
<path d="M3 8l3.5 3.5L13 5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
|
111
|
+
</svg>
|
|
112
|
+
{/* Indeterminate dash */}
|
|
113
|
+
<svg
|
|
114
|
+
className="absolute size-4 pointer-events-none hidden peer-indeterminate:block text-gray-950"
|
|
115
|
+
viewBox="0 0 16 16" fill="none"
|
|
116
|
+
>
|
|
117
|
+
<path d="M4 8h8" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
|
|
118
|
+
</svg>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (!label) return checkboxEl;
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div className="flex items-start gap-3">
|
|
126
|
+
{checkboxEl}
|
|
127
|
+
<div className="flex flex-col gap-0.5 mt-px">
|
|
128
|
+
<label htmlFor={id} className="text-sm font-medium text-text-primary cursor-pointer leading-tight">
|
|
129
|
+
{label}
|
|
130
|
+
</label>
|
|
131
|
+
{description && (
|
|
132
|
+
<span className="text-xs text-text-tertiary leading-normal">{description}</span>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
Checkbox.displayName = 'Checkbox';
|
|
140
|
+
|
|
141
|
+
// ─── Billing Toggle ───────────────────────────────────────────────────────────
|
|
142
|
+
export interface BillingToggleProps {
|
|
143
|
+
value: 'monthly' | 'yearly';
|
|
144
|
+
onChange: (value: 'monthly' | 'yearly') => void;
|
|
145
|
+
savingsLabel?: string;
|
|
146
|
+
className?: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const BillingToggle: React.FC<BillingToggleProps> = ({
|
|
150
|
+
value,
|
|
151
|
+
onChange,
|
|
152
|
+
savingsLabel = 'Save 33%',
|
|
153
|
+
className,
|
|
154
|
+
}) => {
|
|
155
|
+
return (
|
|
156
|
+
<div className={cn('inline-flex items-center gap-3', className)}>
|
|
157
|
+
<span className={cn('text-sm font-medium transition-colors', value === 'monthly' ? 'text-text-primary' : 'text-text-tertiary')}>
|
|
158
|
+
Monthly
|
|
159
|
+
</span>
|
|
160
|
+
<button
|
|
161
|
+
role="switch"
|
|
162
|
+
aria-checked={value === 'yearly'}
|
|
163
|
+
onClick={() => onChange(value === 'monthly' ? 'yearly' : 'monthly')}
|
|
164
|
+
className={cn(
|
|
165
|
+
'relative h-6 w-12 rounded-full border transition-all duration-300 cursor-pointer',
|
|
166
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500',
|
|
167
|
+
value === 'yearly'
|
|
168
|
+
? 'bg-brand-500 border-brand-500'
|
|
169
|
+
: 'bg-surface border-border'
|
|
170
|
+
)}
|
|
171
|
+
>
|
|
172
|
+
<span
|
|
173
|
+
className={cn(
|
|
174
|
+
'absolute top-0.5 size-5 rounded-full shadow-md transition-all duration-300',
|
|
175
|
+
value === 'yearly'
|
|
176
|
+
? 'left-[calc(100%-1.375rem)] bg-gray-950'
|
|
177
|
+
: 'left-0.5 bg-text-tertiary'
|
|
178
|
+
)}
|
|
179
|
+
/>
|
|
180
|
+
</button>
|
|
181
|
+
<span className={cn('text-sm font-medium transition-colors flex items-center gap-2', value === 'yearly' ? 'text-text-primary' : 'text-text-tertiary')}>
|
|
182
|
+
Yearly
|
|
183
|
+
<span className="text-[10px] font-mono font-medium bg-accent-400/10 text-accent-400 border border-accent-400/30 px-1.5 py-0.5 rounded">
|
|
184
|
+
{savingsLabel}
|
|
185
|
+
</span>
|
|
186
|
+
</span>
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export { Switch, Checkbox, BillingToggle };
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
|
|
5
|
+
// ─── Heading ──────────────────────────────────────────────────────────────────
|
|
6
|
+
const headingVariants = cva(
|
|
7
|
+
'font-display font-bold tracking-tight leading-tight text-text-primary',
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
level: {
|
|
11
|
+
display: 'text-7xl md:text-8xl tracking-tighter',
|
|
12
|
+
h1: 'text-5xl md:text-6xl',
|
|
13
|
+
h2: 'text-4xl md:text-5xl',
|
|
14
|
+
h3: 'text-3xl',
|
|
15
|
+
h4: 'text-2xl',
|
|
16
|
+
h5: 'text-xl',
|
|
17
|
+
h6: 'text-lg',
|
|
18
|
+
},
|
|
19
|
+
weight: {
|
|
20
|
+
light: 'font-light',
|
|
21
|
+
regular: 'font-normal',
|
|
22
|
+
medium: 'font-medium',
|
|
23
|
+
semibold: 'font-semibold',
|
|
24
|
+
bold: 'font-bold',
|
|
25
|
+
extrabold:'font-extrabold',
|
|
26
|
+
},
|
|
27
|
+
color: {
|
|
28
|
+
default: 'text-text-primary',
|
|
29
|
+
secondary: 'text-text-secondary',
|
|
30
|
+
brand: 'text-text-brand',
|
|
31
|
+
accent: 'text-text-accent',
|
|
32
|
+
muted: 'text-text-tertiary',
|
|
33
|
+
},
|
|
34
|
+
gradient: {
|
|
35
|
+
none: '',
|
|
36
|
+
brand: 'bg-gradient-to-r from-brand-400 to-accent-400 bg-clip-text text-transparent',
|
|
37
|
+
mono: 'bg-gradient-to-r from-text-primary to-text-tertiary bg-clip-text text-transparent',
|
|
38
|
+
white: 'bg-gradient-to-b from-white to-white/60 bg-clip-text text-transparent',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
defaultVariants: {
|
|
42
|
+
level: 'h2',
|
|
43
|
+
color: 'default',
|
|
44
|
+
gradient: 'none',
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
type HeadingElement = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
50
|
+
|
|
51
|
+
export interface HeadingProps
|
|
52
|
+
extends Omit<React.HTMLAttributes<HTMLHeadingElement>, 'color'>,
|
|
53
|
+
VariantProps<typeof headingVariants> {
|
|
54
|
+
as?: HeadingElement | 'p' | 'span' | 'div';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const Heading = React.forwardRef<HTMLHeadingElement, HeadingProps>(
|
|
58
|
+
({ className, level = 'h2', weight, color, gradient, as, children, ...props }, ref) => {
|
|
59
|
+
const tagMap: Record<string, HeadingElement> = {
|
|
60
|
+
display: 'h1', h1: 'h1', h2: 'h2', h3: 'h3', h4: 'h4', h5: 'h5', h6: 'h6',
|
|
61
|
+
};
|
|
62
|
+
const Tag = (as ?? tagMap[level ?? 'h2'] ?? 'h2') as any;
|
|
63
|
+
return (
|
|
64
|
+
<Tag
|
|
65
|
+
ref={ref}
|
|
66
|
+
className={cn(headingVariants({ level, weight, color, gradient, className }))}
|
|
67
|
+
{...props}
|
|
68
|
+
>
|
|
69
|
+
{children}
|
|
70
|
+
</Tag>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
Heading.displayName = 'Heading';
|
|
75
|
+
|
|
76
|
+
// ─── Text ─────────────────────────────────────────────────────────────────────
|
|
77
|
+
const textVariants = cva('font-body text-text-secondary', {
|
|
78
|
+
variants: {
|
|
79
|
+
size: {
|
|
80
|
+
xs: 'text-xs',
|
|
81
|
+
sm: 'text-sm',
|
|
82
|
+
base: 'text-base',
|
|
83
|
+
md: 'text-md',
|
|
84
|
+
lg: 'text-lg',
|
|
85
|
+
xl: 'text-xl',
|
|
86
|
+
},
|
|
87
|
+
weight: {
|
|
88
|
+
light: 'font-light',
|
|
89
|
+
regular: 'font-normal',
|
|
90
|
+
medium: 'font-medium',
|
|
91
|
+
semibold: 'font-semibold',
|
|
92
|
+
bold: 'font-bold',
|
|
93
|
+
},
|
|
94
|
+
color: {
|
|
95
|
+
primary: 'text-text-primary',
|
|
96
|
+
secondary: 'text-text-secondary',
|
|
97
|
+
tertiary: 'text-text-tertiary',
|
|
98
|
+
disabled: 'text-text-disabled',
|
|
99
|
+
brand: 'text-text-brand',
|
|
100
|
+
accent: 'text-text-accent',
|
|
101
|
+
error: 'text-red-400',
|
|
102
|
+
success: 'text-brand-400',
|
|
103
|
+
warning: 'text-amber-400',
|
|
104
|
+
},
|
|
105
|
+
leading: {
|
|
106
|
+
tight: 'leading-tight',
|
|
107
|
+
snug: 'leading-snug',
|
|
108
|
+
normal: 'leading-normal',
|
|
109
|
+
relaxed: 'leading-relaxed',
|
|
110
|
+
loose: 'leading-loose',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
defaultVariants: {
|
|
114
|
+
size: 'base',
|
|
115
|
+
leading: 'normal',
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
export interface TextProps
|
|
120
|
+
extends Omit<React.HTMLAttributes<HTMLParagraphElement>, 'color'>,
|
|
121
|
+
VariantProps<typeof textVariants> {
|
|
122
|
+
as?: 'p' | 'span' | 'div' | 'label' | 'small' | 'strong' | 'em';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const Text = React.forwardRef<HTMLParagraphElement, TextProps>(
|
|
126
|
+
({ className, size, weight, color, leading, as = 'p', ...props }, ref) => {
|
|
127
|
+
const Tag = as as any;
|
|
128
|
+
return (
|
|
129
|
+
<Tag
|
|
130
|
+
ref={ref}
|
|
131
|
+
className={cn(textVariants({ size, weight, color, leading, className }))}
|
|
132
|
+
{...props}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
Text.displayName = 'Text';
|
|
138
|
+
|
|
139
|
+
// ─── Label ───────────────────────────────────────────────────────────────────
|
|
140
|
+
export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
|
|
141
|
+
required?: boolean;
|
|
142
|
+
optional?: boolean;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
|
|
146
|
+
({ className, children, required, optional, ...props }, ref) => (
|
|
147
|
+
<label
|
|
148
|
+
ref={ref}
|
|
149
|
+
className={cn(
|
|
150
|
+
'block text-sm font-medium text-text-primary tracking-wide mb-1.5',
|
|
151
|
+
className
|
|
152
|
+
)}
|
|
153
|
+
{...props}
|
|
154
|
+
>
|
|
155
|
+
{children}
|
|
156
|
+
{required && <span className="ml-1 text-red-400">*</span>}
|
|
157
|
+
{optional && <span className="ml-1.5 text-text-tertiary font-normal text-xs">(optional)</span>}
|
|
158
|
+
</label>
|
|
159
|
+
)
|
|
160
|
+
);
|
|
161
|
+
Label.displayName = 'Label';
|
|
162
|
+
|
|
163
|
+
// ─── Overline / Caption ───────────────────────────────────────────────────────
|
|
164
|
+
const Overline: React.FC<React.HTMLAttributes<HTMLSpanElement>> = ({ className, ...props }) => (
|
|
165
|
+
<span
|
|
166
|
+
className={cn('text-xs font-mono font-medium tracking-widest uppercase text-text-tertiary', className)}
|
|
167
|
+
{...props}
|
|
168
|
+
/>
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const Caption: React.FC<React.HTMLAttributes<HTMLSpanElement>> = ({ className, ...props }) => (
|
|
172
|
+
<span
|
|
173
|
+
className={cn('text-xs font-body text-text-tertiary leading-normal', className)}
|
|
174
|
+
{...props}
|
|
175
|
+
/>
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
export { Heading, Text, Label, Overline, Caption, headingVariants, textVariants };
|