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,93 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
|
3
|
+
import { ChevronDown } from 'lucide-react';
|
|
4
|
+
import { cn } from '../../lib/utils';
|
|
5
|
+
|
|
6
|
+
const Accordion = AccordionPrimitive.Root;
|
|
7
|
+
|
|
8
|
+
const AccordionItem = React.forwardRef<
|
|
9
|
+
React.ElementRef<typeof AccordionPrimitive.Item>,
|
|
10
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
|
11
|
+
>(({ className, ...props }, ref) => (
|
|
12
|
+
<AccordionPrimitive.Item
|
|
13
|
+
ref={ref}
|
|
14
|
+
className={cn('border-b border-border last:border-0', className)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
));
|
|
18
|
+
AccordionItem.displayName = 'AccordionItem';
|
|
19
|
+
|
|
20
|
+
const AccordionTrigger = React.forwardRef<
|
|
21
|
+
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
|
22
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
|
23
|
+
>(({ className, children, ...props }, ref) => (
|
|
24
|
+
<AccordionPrimitive.Header className="flex">
|
|
25
|
+
<AccordionPrimitive.Trigger
|
|
26
|
+
ref={ref}
|
|
27
|
+
className={cn(
|
|
28
|
+
'flex flex-1 items-center justify-between py-5 gap-4',
|
|
29
|
+
'font-body font-medium text-text-primary text-left',
|
|
30
|
+
'transition-all duration-200',
|
|
31
|
+
'hover:text-text-primary/80',
|
|
32
|
+
'[&[data-state=open]>svg]:rotate-180',
|
|
33
|
+
'focus-visible:outline-none focus-visible:text-text-brand',
|
|
34
|
+
'group',
|
|
35
|
+
className
|
|
36
|
+
)}
|
|
37
|
+
{...props}
|
|
38
|
+
>
|
|
39
|
+
<span>{children}</span>
|
|
40
|
+
<ChevronDown className="size-4 text-text-tertiary shrink-0 transition-transform duration-300 ease-out group-hover:text-text-secondary" />
|
|
41
|
+
</AccordionPrimitive.Trigger>
|
|
42
|
+
</AccordionPrimitive.Header>
|
|
43
|
+
));
|
|
44
|
+
AccordionTrigger.displayName = 'AccordionTrigger';
|
|
45
|
+
|
|
46
|
+
const AccordionContent = React.forwardRef<
|
|
47
|
+
React.ElementRef<typeof AccordionPrimitive.Content>,
|
|
48
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
|
49
|
+
>(({ className, children, ...props }, ref) => (
|
|
50
|
+
<AccordionPrimitive.Content
|
|
51
|
+
ref={ref}
|
|
52
|
+
className="overflow-hidden data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
|
53
|
+
{...props}
|
|
54
|
+
>
|
|
55
|
+
<div className={cn('pb-5 text-sm text-text-secondary leading-relaxed', className)}>
|
|
56
|
+
{children}
|
|
57
|
+
</div>
|
|
58
|
+
</AccordionPrimitive.Content>
|
|
59
|
+
));
|
|
60
|
+
AccordionContent.displayName = 'AccordionContent';
|
|
61
|
+
|
|
62
|
+
// ─── FAQ block: self-contained FAQ section ────────────────────────────────────
|
|
63
|
+
export interface FAQItem {
|
|
64
|
+
question: string;
|
|
65
|
+
answer: React.ReactNode;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface FAQProps {
|
|
69
|
+
items: FAQItem[];
|
|
70
|
+
title?: string;
|
|
71
|
+
className?: string;
|
|
72
|
+
defaultOpen?: string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const FAQ: React.FC<FAQProps> = ({ items, title = 'Frequently asked questions', className, defaultOpen }) => (
|
|
76
|
+
<section className={cn('w-full', className)}>
|
|
77
|
+
{title && (
|
|
78
|
+
<h3 className="font-display text-2xl font-bold text-text-primary mb-8 tracking-tight">
|
|
79
|
+
{title}
|
|
80
|
+
</h3>
|
|
81
|
+
)}
|
|
82
|
+
<Accordion type="multiple" defaultValue={defaultOpen} className="w-full">
|
|
83
|
+
{items.map((item, i) => (
|
|
84
|
+
<AccordionItem key={i} value={`item-${i}`}>
|
|
85
|
+
<AccordionTrigger>{item.question}</AccordionTrigger>
|
|
86
|
+
<AccordionContent>{item.answer}</AccordionContent>
|
|
87
|
+
</AccordionItem>
|
|
88
|
+
))}
|
|
89
|
+
</Accordion>
|
|
90
|
+
</section>
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent, FAQ };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
|
|
5
|
+
const cardVariants = cva(
|
|
6
|
+
['rounded-xl border transition-all duration-200', 'relative overflow-hidden'],
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: 'bg-surface border-border',
|
|
11
|
+
raised: 'bg-surface-raised border-border shadow-md',
|
|
12
|
+
sunken: 'bg-surface-sunken border-border-subtle',
|
|
13
|
+
glass: 'bg-white/[0.03] border-white/[0.06] backdrop-blur-sm',
|
|
14
|
+
brand: 'bg-brand-500/5 border-brand-500/20',
|
|
15
|
+
accent: 'bg-accent-400/5 border-accent-400/20',
|
|
16
|
+
outline: 'bg-transparent border-border',
|
|
17
|
+
ghost: 'bg-transparent border-transparent',
|
|
18
|
+
},
|
|
19
|
+
hover: {
|
|
20
|
+
true: 'hover:border-border-strong hover:shadow-lg cursor-pointer',
|
|
21
|
+
false: '',
|
|
22
|
+
},
|
|
23
|
+
glow: {
|
|
24
|
+
none: '',
|
|
25
|
+
brand: 'shadow-glow-brand',
|
|
26
|
+
accent: 'shadow-glow-accent',
|
|
27
|
+
},
|
|
28
|
+
padding: {
|
|
29
|
+
none: 'p-0',
|
|
30
|
+
sm: 'p-4',
|
|
31
|
+
md: 'p-6',
|
|
32
|
+
lg: 'p-8',
|
|
33
|
+
xl: 'p-10',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
defaultVariants: {
|
|
37
|
+
variant: 'default',
|
|
38
|
+
hover: false,
|
|
39
|
+
glow: 'none',
|
|
40
|
+
padding: 'md',
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
export interface CardProps
|
|
46
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
47
|
+
VariantProps<typeof cardVariants> {}
|
|
48
|
+
|
|
49
|
+
const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
|
50
|
+
({ className, variant, hover, glow, padding, ...props }, ref) => (
|
|
51
|
+
<div
|
|
52
|
+
ref={ref}
|
|
53
|
+
className={cn(cardVariants({ variant, hover, glow, padding, className }))}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
)
|
|
57
|
+
);
|
|
58
|
+
Card.displayName = 'Card';
|
|
59
|
+
|
|
60
|
+
// Sub-components
|
|
61
|
+
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
62
|
+
({ className, ...props }, ref) => (
|
|
63
|
+
<div ref={ref} className={cn('flex flex-col gap-1.5', className)} {...props} />
|
|
64
|
+
)
|
|
65
|
+
);
|
|
66
|
+
CardHeader.displayName = 'CardHeader';
|
|
67
|
+
|
|
68
|
+
const CardTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
|
69
|
+
({ className, ...props }, ref) => (
|
|
70
|
+
<h3
|
|
71
|
+
ref={ref}
|
|
72
|
+
className={cn('font-display font-semibold text-text-primary leading-tight tracking-tight', className)}
|
|
73
|
+
{...props}
|
|
74
|
+
/>
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
CardTitle.displayName = 'CardTitle';
|
|
78
|
+
|
|
79
|
+
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
|
80
|
+
({ className, ...props }, ref) => (
|
|
81
|
+
<p ref={ref} className={cn('text-sm text-text-secondary leading-relaxed', className)} {...props} />
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
CardDescription.displayName = 'CardDescription';
|
|
85
|
+
|
|
86
|
+
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
87
|
+
({ className, ...props }, ref) => (
|
|
88
|
+
<div ref={ref} className={cn('', className)} {...props} />
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
CardContent.displayName = 'CardContent';
|
|
92
|
+
|
|
93
|
+
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
94
|
+
({ className, ...props }, ref) => (
|
|
95
|
+
<div ref={ref} className={cn('flex items-center gap-3 pt-4 border-t border-border mt-4', className)} {...props} />
|
|
96
|
+
)
|
|
97
|
+
);
|
|
98
|
+
CardFooter.displayName = 'CardFooter';
|
|
99
|
+
|
|
100
|
+
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, cardVariants };
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '../../lib/utils';
|
|
3
|
+
import { Button } from '../atoms/Button';
|
|
4
|
+
import { Badge } from '../atoms/Badge';
|
|
5
|
+
import { Separator } from '../atoms/Misc';
|
|
6
|
+
import { Check } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
export interface PricingLimitRow {
|
|
9
|
+
label: string;
|
|
10
|
+
value: string;
|
|
11
|
+
tooltip?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PricingFeature {
|
|
15
|
+
text: string;
|
|
16
|
+
highlight?: boolean;
|
|
17
|
+
integration?: 'shopify' | string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PricingCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
21
|
+
planName: string;
|
|
22
|
+
description: string;
|
|
23
|
+
price: number | 'custom';
|
|
24
|
+
originalPrice?: number;
|
|
25
|
+
billingPeriod?: 'monthly' | 'yearly';
|
|
26
|
+
annualTotal?: string;
|
|
27
|
+
savingsPercent?: number;
|
|
28
|
+
ctaLabel: string;
|
|
29
|
+
ctaVariant?: 'default' | 'secondary' | 'outline';
|
|
30
|
+
onCtaClick?: () => void;
|
|
31
|
+
limits?: PricingLimitRow[];
|
|
32
|
+
features?: PricingFeature[];
|
|
33
|
+
sectionLabel?: string;
|
|
34
|
+
recommended?: boolean;
|
|
35
|
+
support?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const PricingCard = React.forwardRef<HTMLDivElement, PricingCardProps>(
|
|
39
|
+
({
|
|
40
|
+
className,
|
|
41
|
+
planName,
|
|
42
|
+
description,
|
|
43
|
+
price,
|
|
44
|
+
originalPrice,
|
|
45
|
+
billingPeriod = 'monthly',
|
|
46
|
+
annualTotal,
|
|
47
|
+
savingsPercent,
|
|
48
|
+
ctaLabel,
|
|
49
|
+
ctaVariant = 'secondary',
|
|
50
|
+
onCtaClick,
|
|
51
|
+
limits = [],
|
|
52
|
+
features = [],
|
|
53
|
+
sectionLabel,
|
|
54
|
+
recommended = false,
|
|
55
|
+
support,
|
|
56
|
+
...props
|
|
57
|
+
}, ref) => {
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
ref={ref}
|
|
61
|
+
className={cn(
|
|
62
|
+
'relative flex flex-col rounded-2xl border transition-all duration-300',
|
|
63
|
+
recommended
|
|
64
|
+
? 'bg-brand-500/5 border-brand-500/40 shadow-glow-brand'
|
|
65
|
+
: 'bg-surface border-border hover:border-border-strong',
|
|
66
|
+
className
|
|
67
|
+
)}
|
|
68
|
+
{...props}
|
|
69
|
+
>
|
|
70
|
+
{/* Recommended ribbon */}
|
|
71
|
+
{recommended && (
|
|
72
|
+
<div className="absolute -top-3.5 left-1/2 -translate-x-1/2">
|
|
73
|
+
<Badge variant="solid" className="shadow-md px-3">
|
|
74
|
+
Recommended
|
|
75
|
+
</Badge>
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
<div className="p-7 flex flex-col gap-5 flex-1">
|
|
80
|
+
{/* Header */}
|
|
81
|
+
<div className="flex flex-col gap-2">
|
|
82
|
+
<h3 className="font-display text-xl font-bold text-text-primary">{planName}</h3>
|
|
83
|
+
<p className="text-sm text-text-tertiary leading-relaxed">{description}</p>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
{/* Price */}
|
|
87
|
+
<div className="flex flex-col gap-1">
|
|
88
|
+
<div className="flex items-baseline gap-2 flex-wrap">
|
|
89
|
+
{price === 'custom' ? (
|
|
90
|
+
<span className="font-display text-4xl font-bold text-text-primary tracking-tight">
|
|
91
|
+
Custom
|
|
92
|
+
</span>
|
|
93
|
+
) : (
|
|
94
|
+
<>
|
|
95
|
+
{originalPrice && (
|
|
96
|
+
<span className="font-mono text-xl text-text-tertiary line-through">
|
|
97
|
+
${originalPrice}
|
|
98
|
+
</span>
|
|
99
|
+
)}
|
|
100
|
+
<span className="font-display text-4xl font-bold text-text-primary tracking-tight">
|
|
101
|
+
${price}
|
|
102
|
+
</span>
|
|
103
|
+
<div className="flex items-center gap-1.5">
|
|
104
|
+
<span className="text-sm text-text-tertiary">/{billingPeriod === 'monthly' ? 'mo' : 'yr'}</span>
|
|
105
|
+
{savingsPercent && (
|
|
106
|
+
<Badge variant="brand">-{savingsPercent}%</Badge>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
</>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
{annualTotal && (
|
|
113
|
+
<span className="text-xs font-mono text-text-tertiary">{annualTotal}</span>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{/* CTA */}
|
|
118
|
+
<Button
|
|
119
|
+
variant={recommended ? 'default' : ctaVariant}
|
|
120
|
+
size="md"
|
|
121
|
+
className="w-full"
|
|
122
|
+
onClick={onCtaClick}
|
|
123
|
+
>
|
|
124
|
+
{ctaLabel}
|
|
125
|
+
</Button>
|
|
126
|
+
|
|
127
|
+
<Separator variant="subtle" />
|
|
128
|
+
|
|
129
|
+
{/* Limits */}
|
|
130
|
+
{limits.length > 0 && (
|
|
131
|
+
<div className="flex flex-col gap-3">
|
|
132
|
+
{limits.map((limit, i) => (
|
|
133
|
+
<div key={i} className="flex items-start justify-between gap-4">
|
|
134
|
+
<div className="flex flex-col gap-0.5 min-w-0">
|
|
135
|
+
<span className="text-sm font-medium text-text-primary">{limit.value}</span>
|
|
136
|
+
{limit.tooltip && (
|
|
137
|
+
<span className="text-xs text-text-tertiary leading-snug">{limit.tooltip}</span>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
<span className="text-xs font-mono text-text-tertiary whitespace-nowrap shrink-0 mt-0.5 text-right">
|
|
141
|
+
{limit.label}
|
|
142
|
+
</span>
|
|
143
|
+
</div>
|
|
144
|
+
))}
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
|
|
148
|
+
<Separator variant="subtle" />
|
|
149
|
+
|
|
150
|
+
{/* Features */}
|
|
151
|
+
{features.length > 0 && (
|
|
152
|
+
<div className="flex flex-col gap-2.5">
|
|
153
|
+
{sectionLabel && (
|
|
154
|
+
<p className="text-xs font-mono text-text-tertiary tracking-wider uppercase mb-1">
|
|
155
|
+
{sectionLabel}
|
|
156
|
+
</p>
|
|
157
|
+
)}
|
|
158
|
+
{features.map((feature, i) => (
|
|
159
|
+
<div key={i} className="flex items-start gap-2.5">
|
|
160
|
+
<Check className={cn(
|
|
161
|
+
'size-4 shrink-0 mt-0.5',
|
|
162
|
+
feature.highlight ? 'text-brand-400' : 'text-text-tertiary'
|
|
163
|
+
)} />
|
|
164
|
+
<span className={cn(
|
|
165
|
+
'text-sm leading-snug',
|
|
166
|
+
feature.highlight ? 'text-text-primary font-medium' : 'text-text-secondary'
|
|
167
|
+
)}>
|
|
168
|
+
{feature.integration === 'shopify' && (
|
|
169
|
+
<span className="inline-flex items-center gap-1 text-[10px] font-mono text-text-tertiary border border-border rounded px-1 mr-1.5 py-px">
|
|
170
|
+
SHOPIFY
|
|
171
|
+
</span>
|
|
172
|
+
)}
|
|
173
|
+
{feature.text}
|
|
174
|
+
</span>
|
|
175
|
+
</div>
|
|
176
|
+
))}
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
|
|
180
|
+
{/* Support */}
|
|
181
|
+
{support && (
|
|
182
|
+
<div className="mt-auto pt-3 border-t border-border">
|
|
183
|
+
<p className="text-xs text-text-tertiary leading-snug">
|
|
184
|
+
<span className="font-medium text-text-secondary">Support: </span>
|
|
185
|
+
{support}
|
|
186
|
+
</p>
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
PricingCard.displayName = 'PricingCard';
|
|
195
|
+
|
|
196
|
+
export { PricingCard };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '../../lib/utils';
|
|
3
|
+
import { Avatar } from '../atoms/Avatar';
|
|
4
|
+
import { StarRating } from '../atoms/Misc';
|
|
5
|
+
|
|
6
|
+
export interface TestimonialCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
quote: string;
|
|
8
|
+
authorName: string;
|
|
9
|
+
authorTitle: string;
|
|
10
|
+
authorAvatar?: string;
|
|
11
|
+
companyLogo?: string;
|
|
12
|
+
companyName?: string;
|
|
13
|
+
rating?: number;
|
|
14
|
+
featured?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const TestimonialCard = React.forwardRef<HTMLDivElement, TestimonialCardProps>(
|
|
18
|
+
({
|
|
19
|
+
className,
|
|
20
|
+
quote,
|
|
21
|
+
authorName,
|
|
22
|
+
authorTitle,
|
|
23
|
+
authorAvatar,
|
|
24
|
+
companyLogo,
|
|
25
|
+
companyName,
|
|
26
|
+
rating,
|
|
27
|
+
featured = false,
|
|
28
|
+
...props
|
|
29
|
+
}, ref) => {
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
ref={ref}
|
|
33
|
+
className={cn(
|
|
34
|
+
'relative flex flex-col gap-5 rounded-xl border p-6 transition-all duration-300',
|
|
35
|
+
featured
|
|
36
|
+
? 'bg-brand-500/5 border-brand-500/30 hover:border-brand-500/50 hover:shadow-glow-brand'
|
|
37
|
+
: 'bg-surface border-border hover:border-border-strong hover:bg-surface-raised',
|
|
38
|
+
className
|
|
39
|
+
)}
|
|
40
|
+
{...props}
|
|
41
|
+
>
|
|
42
|
+
{/* Quote mark */}
|
|
43
|
+
<span className="font-editorial text-5xl leading-none text-brand-500/20 select-none absolute top-4 right-6">
|
|
44
|
+
"
|
|
45
|
+
</span>
|
|
46
|
+
|
|
47
|
+
{/* Company logo */}
|
|
48
|
+
{companyLogo && (
|
|
49
|
+
<div className="flex items-center">
|
|
50
|
+
<img
|
|
51
|
+
src={companyLogo}
|
|
52
|
+
alt={companyName ?? ''}
|
|
53
|
+
className="h-6 object-contain opacity-60 filter brightness-200"
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
|
|
58
|
+
{/* Rating */}
|
|
59
|
+
{rating && <StarRating value={rating} showValue size="sm" />}
|
|
60
|
+
|
|
61
|
+
{/* Quote */}
|
|
62
|
+
<blockquote className="text-sm text-text-secondary leading-relaxed flex-1">
|
|
63
|
+
"{quote}"
|
|
64
|
+
</blockquote>
|
|
65
|
+
|
|
66
|
+
{/* Author */}
|
|
67
|
+
<div className="flex items-center gap-3 pt-2 border-t border-border">
|
|
68
|
+
<Avatar
|
|
69
|
+
src={authorAvatar}
|
|
70
|
+
fallback={authorName.slice(0, 2)}
|
|
71
|
+
alt={authorName}
|
|
72
|
+
size="sm"
|
|
73
|
+
/>
|
|
74
|
+
<div className="flex flex-col min-w-0">
|
|
75
|
+
<span className="text-sm font-semibold text-text-primary truncate">{authorName}</span>
|
|
76
|
+
<span className="text-xs text-text-tertiary truncate">{authorTitle}</span>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
TestimonialCard.displayName = 'TestimonialCard';
|
|
84
|
+
|
|
85
|
+
export { TestimonialCard };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
|
|
5
|
+
const TooltipProvider = TooltipPrimitive.Provider;
|
|
6
|
+
const TooltipRoot = TooltipPrimitive.Root;
|
|
7
|
+
const TooltipTrigger = TooltipPrimitive.Trigger;
|
|
8
|
+
|
|
9
|
+
const TooltipContent = React.forwardRef<
|
|
10
|
+
React.ElementRef<typeof TooltipPrimitive.Content>,
|
|
11
|
+
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> & {
|
|
12
|
+
variant?: 'default' | 'brand' | 'accent';
|
|
13
|
+
}
|
|
14
|
+
>(({ className, sideOffset = 6, variant = 'default', ...props }, ref) => (
|
|
15
|
+
<TooltipPrimitive.Portal>
|
|
16
|
+
<TooltipPrimitive.Content
|
|
17
|
+
ref={ref}
|
|
18
|
+
sideOffset={sideOffset}
|
|
19
|
+
className={cn(
|
|
20
|
+
'z-tooltip overflow-hidden rounded-lg border px-3 py-1.5',
|
|
21
|
+
'text-xs font-body leading-normal',
|
|
22
|
+
'animate-scale-in',
|
|
23
|
+
'shadow-lg',
|
|
24
|
+
{
|
|
25
|
+
default: 'bg-surface-raised border-border text-text-primary',
|
|
26
|
+
brand: 'bg-brand-500 border-brand-500 text-gray-950',
|
|
27
|
+
accent: 'bg-accent-400 border-accent-400 text-gray-950',
|
|
28
|
+
}[variant],
|
|
29
|
+
className
|
|
30
|
+
)}
|
|
31
|
+
{...props}
|
|
32
|
+
>
|
|
33
|
+
{props.children}
|
|
34
|
+
<TooltipPrimitive.Arrow
|
|
35
|
+
className={cn(
|
|
36
|
+
'fill-current',
|
|
37
|
+
variant === 'default' ? 'text-surface-raised' : variant === 'brand' ? 'text-brand-500' : 'text-accent-400'
|
|
38
|
+
)}
|
|
39
|
+
width={8}
|
|
40
|
+
height={4}
|
|
41
|
+
/>
|
|
42
|
+
</TooltipPrimitive.Content>
|
|
43
|
+
</TooltipPrimitive.Portal>
|
|
44
|
+
));
|
|
45
|
+
TooltipContent.displayName = 'TooltipContent';
|
|
46
|
+
|
|
47
|
+
// ─── Convenience wrapper ──────────────────────────────────────────────────────
|
|
48
|
+
export interface TooltipProps {
|
|
49
|
+
content: React.ReactNode;
|
|
50
|
+
children: React.ReactNode;
|
|
51
|
+
side?: 'top' | 'right' | 'bottom' | 'left';
|
|
52
|
+
variant?: 'default' | 'brand' | 'accent';
|
|
53
|
+
delayDuration?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const Tooltip: React.FC<TooltipProps> = ({
|
|
57
|
+
content,
|
|
58
|
+
children,
|
|
59
|
+
side = 'top',
|
|
60
|
+
variant = 'default',
|
|
61
|
+
delayDuration = 300,
|
|
62
|
+
}) => (
|
|
63
|
+
<TooltipProvider delayDuration={delayDuration}>
|
|
64
|
+
<TooltipRoot>
|
|
65
|
+
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
|
66
|
+
<TooltipContent side={side} variant={variant}>{content}</TooltipContent>
|
|
67
|
+
</TooltipRoot>
|
|
68
|
+
</TooltipProvider>
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
export { Tooltip, TooltipProvider, TooltipRoot, TooltipTrigger, TooltipContent };
|