Design Updates (#379)

* Enhance Marketing Pages and UI Components

- Updated the marketing homepage to include an Ecosystem Showcase component, improving the presentation of the SaaS Starter Kit.
- Refined various UI components, including adjustments to spacing, typography, and layout for better visual consistency.
- Improved accessibility by adding aria-labels and ensuring proper semantic structure in components.
- Adjusted styles across multiple components to enhance responsiveness and user experience.
- Updated the pricing table and feature cards to align with the new design standards, ensuring a cohesive look and feel throughout the application.
- Updated plan picker design
This commit is contained in:
Giancarlo Buomprisco
2025-10-02 15:14:11 +08:00
committed by GitHub
parent d8bb7f56df
commit 54d6b4897f
56 changed files with 1014 additions and 1142 deletions

View File

@@ -17,7 +17,7 @@
"@kit/eslint-config": "workspace:*",
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@types/node": "^24.6.1"
"@types/node": "^24.6.2"
},
"typesVersions": {
"*": {

View File

@@ -24,7 +24,7 @@ export function BillingSessionStatus({
className={
'fade-in dark:border-border mx-auto max-w-xl rounded-xl border border-transparent p-16 xl:drop-shadow-2xl' +
' bg-background animate-in slide-in-from-bottom-8 ease-out' +
' zoom-in-50 dark:shadow-primary/20 duration-1000 dark:shadow-2xl'
' zoom-in-50 dark:shadow-primary/5 duration-1000 dark:shadow-2xl'
}
>
<div

View File

@@ -1,5 +1,5 @@
import { formatDate } from 'date-fns';
import { BadgeCheck } from 'lucide-react';
import { BadgeCheck, InfoIcon, MessageCircleWarning } from 'lucide-react';
import { PlanSchema, type ProductSchema } from '@kit/billing';
import { Tables } from '@kit/supabase/database';
@@ -58,15 +58,15 @@ export function CurrentSubscriptionCard({
<CardContent className={'space-y-4 border-t pt-4 text-sm'}>
<div className={'flex flex-col space-y-1'}>
<div className={'flex items-center gap-x-3 text-lg font-semibold'}>
<BadgeCheck
className={
's-6 fill-green-500 text-white dark:fill-white dark:text-black'
}
/>
<div className={'flex items-center gap-x-4 text-lg font-semibold'}>
<span className={'flex items-center gap-x-1.5'}>
<BadgeCheck
className={'s-6 fill-green-500 text-white dark:text-stone-900'}
/>
<span data-test={'current-plan-card-product-name'}>
<Trans i18nKey={product.name} defaults={product.name} />
<span data-test={'current-plan-card-product-name'}>
<Trans i18nKey={product.name} defaults={product.name} />
</span>
</span>
<CurrentPlanBadge status={subscription.status} />
@@ -92,38 +92,7 @@ export function CurrentSubscriptionCard({
</div>
</If>
<If condition={subscription.status === 'trialing'}>
<div className="flex flex-col gap-y-1">
<span className="font-semibold">
<Trans i18nKey="billing:trialEndsOn" />
</span>
<div className={'text-muted-foreground'}>
<span>
{subscription.trial_ends_at
? formatDate(subscription.trial_ends_at, 'P')
: ''}
</span>
</div>
</div>
</If>
<If condition={subscription.cancel_at_period_end}>
<Alert variant={'warning'}>
<AlertTitle>
<Trans i18nKey="billing:subscriptionCancelled" />
</AlertTitle>
<AlertDescription>
<Trans i18nKey="billing:cancelSubscriptionDate" />:
<span className={'ml-1'}>
{formatDate(subscription.period_ends_at ?? '', 'P')}
</span>
</AlertDescription>
</Alert>
</If>
<div className="flex flex-col gap-y-1">
<div className="flex flex-col gap-y-1 border-y border-dashed py-4">
<span className="font-semibold">
<Trans i18nKey="billing:detailsLabel" />
</span>
@@ -134,6 +103,54 @@ export function CurrentSubscriptionCard({
selectedInterval={firstLineItem.interval}
/>
</div>
<If condition={subscription.status === 'trialing'}>
{() => (
<Alert variant={'info'}>
<InfoIcon className={'h-4 w-4'} />
<AlertTitle>
<Trans i18nKey="billing:trialAlertTitle" />
</AlertTitle>
<AlertDescription>
<Trans
i18nKey="billing:trialAlertDescription"
values={{
date: formatDate(
subscription.trial_ends_at ?? '',
'MMMM d, yyyy',
),
}}
/>
</AlertDescription>
</Alert>
)}
</If>
<If condition={subscription.cancel_at_period_end}>
{() => (
<Alert variant={'warning'}>
<MessageCircleWarning className={'h-4 w-4'} />
<AlertTitle>
<Trans i18nKey="billing:subscriptionCancelled" />
</AlertTitle>
<AlertDescription>
<Trans
i18nKey="billing:cancelSubscriptionDate"
values={{
date: formatDate(
subscription.period_ends_at ?? '',
'MMMM d, yyyy',
),
}}
/>
</AlertDescription>
</Alert>
)}
</If>
</CardContent>
</Card>
);

View File

@@ -22,7 +22,6 @@ import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@kit/ui/form';
import { If } from '@kit/ui/if';
@@ -32,7 +31,6 @@ import {
RadioGroupItem,
RadioGroupItemLabel,
} from '@kit/ui/radio-group';
import { Separator } from '@kit/ui/separator';
import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils';
@@ -125,7 +123,7 @@ export function PlanPicker(
className={'flex flex-col gap-y-4 lg:flex-row lg:gap-x-4 lg:gap-y-0'}
>
<form
className={'flex w-full max-w-xl flex-col gap-y-8'}
className={'flex w-full flex-col gap-y-4'}
onSubmit={form.handleSubmit(props.onSubmit)}
>
<If condition={intervals.length}>
@@ -139,13 +137,9 @@ export function PlanPicker(
render={({ field }) => {
return (
<FormItem className={'flex flex-col gap-4'}>
<FormLabel htmlFor={'plan-picker-id'}>
<Trans i18nKey={'common:billingInterval.label'} />
</FormLabel>
<FormControl id={'plan-picker-id'}>
<RadioGroup name={field.name} value={field.value}>
<div className={'flex space-x-2.5'}>
<div className={'flex space-x-1'}>
{intervals.map((interval) => {
const selected = field.value === interval;
@@ -154,11 +148,10 @@ export function PlanPicker(
htmlFor={interval}
key={interval}
className={cn(
'focus-within:border-primary flex items-center gap-x-2.5 rounded-md border px-2.5 py-2 transition-colors',
'focus-within:border-primary flex items-center gap-x-2.5 rounded-md px-2.5 py-2 transition-colors',
{
['bg-muted border-input']: selected,
['hover:border-input border-transparent']:
!selected,
['bg-muted']: selected,
['hover:bg-muted/50']: !selected,
},
)}
>
@@ -216,12 +209,12 @@ export function PlanPicker(
name={'planId'}
render={({ field }) => (
<FormItem className={'flex flex-col gap-4'}>
<FormLabel>
<Trans i18nKey={'common:planPickerLabel'} />
</FormLabel>
<FormControl>
<RadioGroup value={field.value} name={field.name}>
<RadioGroup
value={field.value}
name={field.name}
className="gap-y-0.5"
>
{visibleProducts.map((product) => {
const plan = product.plans.find((item) => {
if (item.paymentType === 'one-time') {
@@ -251,27 +244,8 @@ export function PlanPicker(
<RadioGroupItemLabel
selected={selected}
key={primaryLineItem.id}
className="rounded-md !border-transparent"
>
<RadioGroupItem
data-test-plan={plan.id}
key={plan.id + selected}
id={plan.id}
value={plan.id}
onClick={() => {
if (selected) {
return;
}
form.setValue('planId', planId, {
shouldValidate: true,
});
form.setValue('productId', product.id, {
shouldValidate: true,
});
}}
/>
<div
className={
'flex w-full flex-col content-center gap-y-3 lg:flex-row lg:items-center lg:justify-between lg:space-y-0'
@@ -280,10 +254,30 @@ export function PlanPicker(
<Label
htmlFor={plan.id}
className={
'flex flex-col justify-center space-y-2'
'flex flex-col justify-center space-y-2.5'
}
>
<div className={'flex items-center space-x-2.5'}>
<RadioGroupItem
data-test-plan={plan.id}
key={plan.id + selected}
id={plan.id}
value={plan.id}
onClick={() => {
if (selected) {
return;
}
form.setValue('planId', planId, {
shouldValidate: true,
});
form.setValue('productId', product.id, {
shouldValidate: true,
});
}}
/>
<span className="font-semibold">
<Trans
i18nKey={`billing:plans.${product.id}.name`}
@@ -363,6 +357,14 @@ export function PlanPicker(
)}
/>
{selectedPlan && selectedInterval && selectedProduct ? (
<PlanDetails
selectedInterval={selectedInterval}
selectedPlan={selectedPlan}
selectedProduct={selectedProduct}
/>
) : null}
<div>
<Button
data-test="checkout-submit-button"
@@ -385,14 +387,6 @@ export function PlanPicker(
</Button>
</div>
</form>
{selectedPlan && selectedInterval && selectedProduct ? (
<PlanDetails
selectedInterval={selectedInterval}
selectedPlan={selectedPlan}
selectedProduct={selectedProduct}
/>
) : null}
</div>
</Form>
);
@@ -427,78 +421,49 @@ function PlanDetails({
<div
key={key}
className={
'fade-in animate-in zoom-in-95 flex w-full flex-col space-y-4 py-2 lg:px-8'
'fade-in animate-in flex w-full flex-col space-y-2 rounded-md border p-4'
}
>
<div className={'flex flex-col space-y-0.5'}>
<span className={'text-sm font-medium'}>
<b>
<Trans
i18nKey={`billing:plans.${selectedProduct.id}.name`}
defaults={selectedProduct.name}
/>
</b>{' '}
<If condition={isRecurring}>
/ <Trans i18nKey={`billing:billingInterval.${selectedInterval}`} />
</If>
</span>
<p>
<span className={'text-muted-foreground text-sm'}>
<Trans
i18nKey={`billing:plans.${selectedProduct.id}.description`}
defaults={selectedProduct.description}
/>
</span>
</p>
<div className={'flex flex-col space-y-1'}>
<span className={'text-sm font-semibold'}>{selectedProduct.name}</span>
</div>
<If condition={selectedPlan.lineItems.length > 0}>
<Separator />
<div className={'flex flex-col space-y-1'}>
<div className={'flex flex-col space-y-2'}>
<LineItemDetails
lineItems={selectedPlan.lineItems ?? []}
selectedInterval={isRecurring ? selectedInterval : undefined}
currency={selectedProduct.currency}
/>
<div className={'flex flex-col space-y-2'}>
<span className={'text-sm font-semibold'}>
<Trans i18nKey={'billing:detailsLabel'} />
</span>
<div className={'flex flex-wrap gap-1'}>
{selectedProduct.features.map((item) => {
return (
<Badge
key={item}
className={'flex items-center gap-x-2'}
variant={'outline'}
>
<CheckCircle className={'h-3 w-3 text-green-500'} />
<LineItemDetails
lineItems={selectedPlan.lineItems ?? []}
selectedInterval={isRecurring ? selectedInterval : undefined}
currency={selectedProduct.currency}
/>
<span className={'text-muted-foreground'}>
<Trans i18nKey={item} defaults={item} />
</span>
</Badge>
);
})}
</div>
</div>
</div>
</If>
<Separator />
<div className={'flex flex-col space-y-2'}>
<span className={'text-sm font-semibold'}>
<Trans i18nKey={'billing:featuresLabel'} />
</span>
{selectedProduct.features.map((item) => {
return (
<div key={item} className={'flex items-center gap-x-2 text-sm'}>
<CheckCircle className={'h-4 text-green-500'} />
<span className={'text-secondary-foreground'}>
<Trans i18nKey={item} defaults={item} />
</span>
</div>
);
})}
</div>
</div>
);
}
function Price(props: React.PropsWithChildren) {
return (
<span
className={
'animate-in slide-in-from-left-4 fade-in text-xl font-semibold tracking-tight duration-500'
}
>
<span className={'animate-in fade-in text-xl font-medium tracking-tight'}>
{props.children}
</span>
);

View File

@@ -71,7 +71,7 @@ export function PricingTable({
<div
className={
'flex flex-col items-start space-y-6 lg:space-y-0' +
' justify-center lg:flex-row lg:space-x-4'
' justify-center lg:flex-row lg:gap-x-2.5'
}
>
{visibleProducts.map((product) => {
@@ -171,17 +171,13 @@ function PricingItem(
data-cy={'subscription-plan'}
className={cn(
props.className,
`s-full relative flex flex-1 grow flex-col items-stretch justify-between self-stretch rounded-lg border px-6 py-5 lg:w-4/12 xl:max-w-[20rem]`,
{
['border-primary']: highlighted,
['border-border']: !highlighted,
},
`s-full bg-muted/50 relative flex flex-1 grow flex-col items-stretch justify-between self-stretch rounded px-6 py-5 lg:w-4/12 xl:max-w-[20rem]`,
)}
>
<If condition={props.product.badge}>
<div className={'absolute -top-2.5 left-0 flex w-full justify-center'}>
<Badge
className={highlighted ? '' : 'bg-background'}
className={highlighted ? '' : 'bg-muted'}
variant={highlighted ? 'default' : 'outline'}
>
<span>
@@ -194,12 +190,12 @@ function PricingItem(
</div>
</If>
<div className={'flex flex-col gap-y-5'}>
<div className={'flex flex-col gap-y-1'}>
<div className={'flex flex-col gap-y-4'}>
<div className={'flex flex-col'}>
<div className={'flex items-center space-x-6'}>
<b
className={
'text-secondary-foreground font-heading text-xl font-medium tracking-tight'
'text-secondary-foreground font-heading text-xl font-medium tracking-tight text-orange-800'
}
>
<Trans
@@ -208,9 +204,20 @@ function PricingItem(
/>
</b>
</div>
<span
className={cn(`text-muted-foreground text-base tracking-tight`)}
>
<Trans
i18nKey={props.product.description}
defaults={props.product.description}
/>
</span>
</div>
<div className={'mt-6 flex flex-col gap-y-1'}>
<div className={'h-px w-full border border-dashed'} />
<div className={'flex flex-col gap-y-1'}>
<Price
isMonthlyPrice={props.alwaysDisplayMonthlyPrice}
displayBillingPeriod={!props.plan.label}
@@ -296,13 +303,6 @@ function PricingItem(
</If>
</If>
<span className={cn(`text-muted-foreground text-base tracking-tight`)}>
<Trans
i18nKey={props.product.description}
defaults={props.product.description}
/>
</span>
<div className={'h-px w-full border border-dashed'} />
<div className={'flex flex-col'}>
@@ -389,9 +389,9 @@ function ListItem({
highlighted: boolean;
}>) {
return (
<li className={'flex items-center gap-x-2.5'}>
<li className={'flex items-center gap-x-2'}>
<CheckCircle
className={cn('h-4 min-h-4 w-4 min-w-4', {
className={cn('h-3.5 min-h-3.5 w-3.5 min-w-3.5', {
'text-secondary-foreground': highlighted,
'text-muted-foreground': !highlighted,
})}
@@ -417,7 +417,7 @@ function PlanIntervalSwitcher(
}>,
) {
return (
<div className={'flex gap-x-1 rounded-full border p-1.5'}>
<div className={'flex gap-x-1 rounded-full border'}>
{props.intervals.map((plan, index) => {
const selected = plan === props.interval;
@@ -434,8 +434,7 @@ function PlanIntervalSwitcher(
return (
<Button
key={plan}
size={'sm'}
variant={selected ? 'default' : 'ghost'}
variant={selected ? 'secondary' : 'ghost'}
className={className}
onClick={() => props.setInterval(plan)}
>

View File

@@ -1,3 +1,5 @@
import 'server-only';
import { z } from 'zod';
import {

View File

@@ -20,7 +20,7 @@
"@kit/shared": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/wordpress": "workspace:*",
"@types/node": "^24.6.1"
"@types/node": "^24.6.2"
},
"typesVersions": {
"*": {

View File

@@ -26,7 +26,7 @@
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
"@types/node": "^24.6.1",
"@types/node": "^24.6.2",
"@types/react": "19.1.16",
"react": "19.1.1",
"zod": "^3.25.74"

View File

@@ -20,7 +20,7 @@
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
"@types/node": "^24.6.1",
"@types/node": "^24.6.2",
"@types/react": "19.1.16",
"wp-types": "^4.68.1"
},

View File

@@ -27,7 +27,7 @@
"react-i18next": "^16.0.0"
},
"dependencies": {
"i18next": "25.5.2",
"i18next": "25.5.3",
"i18next-browser-languagedetector": "8.2.0",
"i18next-resources-to-backend": "^1.2.1"
},

View File

@@ -20,7 +20,7 @@
"@kit/resend": "workspace:*",
"@kit/shared": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@types/node": "^24.6.1",
"@types/node": "^24.6.2",
"zod": "^3.25.74"
},
"typesVersions": {

View File

@@ -17,7 +17,7 @@
"@kit/mailers-shared": "workspace:*",
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@types/node": "^24.6.1",
"@types/node": "^24.6.2",
"zod": "^3.25.74"
},
"typesVersions": {

View File

@@ -25,7 +25,7 @@
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@modelcontextprotocol/sdk": "1.18.2",
"@types/node": "^24.6.1",
"@types/node": "^24.6.2",
"postgres": "3.4.7",
"zod": "^3.25.74"
},

View File

@@ -40,7 +40,7 @@
"react-hook-form": "^7.63.0",
"react-i18next": "^16.0.0",
"sonner": "^2.0.7",
"tailwindcss": "4.1.13",
"tailwindcss": "4.1.14",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.9.3",
"zod": "^3.25.74"

View File

@@ -9,7 +9,7 @@ export function If<Value = unknown>({
}: React.PropsWithoutRef<{
condition: Condition<Value>;
children: React.ReactNode | ((value: Value) => React.ReactNode);
fallback?: React.ReactNode;
fallback?: React.ReactNode | (() => React.ReactNode);
}>) {
return useMemo(() => {
if (condition) {
@@ -21,6 +21,10 @@ export function If<Value = unknown>({
}
if (fallback) {
if (typeof fallback === 'function') {
return <>{fallback()}</>;
}
return <>{fallback}</>;
}

View File

@@ -0,0 +1,61 @@
import React from 'react';
import { cn } from '../../lib/utils';
interface EcosystemShowcaseProps extends React.HTMLAttributes<HTMLDivElement> {
heading: React.ReactNode;
description?: React.ReactNode;
textPosition?: 'left' | 'right';
}
export const EcosystemShowcase: React.FC<EcosystemShowcaseProps> =
function EcosystemShowcaseComponent({
className,
heading,
description,
textPosition = 'left',
children,
...props
}) {
return (
<div
className={cn(
'bg-muted/50 flex flex-1 flex-col space-y-8 rounded-md p-6 lg:space-y-0 lg:space-x-16',
className,
{
'lg:flex-row': textPosition === 'left',
'lg:flex-row-reverse': textPosition === 'right',
},
)}
{...props}
>
<div
className={cn(
'h-full w-full flex-col items-start gap-y-4 text-left lg:w-1/3',
{
'text-right': textPosition === 'right',
},
)}
>
<h2 className="text-secondary-foreground text-3xl font-normal tracking-tight">
{heading}
</h2>
{description && (
<p className="text-muted-foreground mt-2 text-base lg:text-lg">
{description}
</p>
)}
</div>
<div
className={cn(
'flex w-full lg:w-2/3',
textPosition === 'right' && 'm-0 text-right',
)}
>
{children}
</div>
</div>
);
};

View File

@@ -15,9 +15,9 @@ export const FeatureCard: React.FC<FeatureCardProps> = ({
...props
}) => {
return (
<div className={cn('rounded-xl border p-4', className)} {...props}>
<CardHeader>
<CardTitle className="text-xl font-medium">{label}</CardTitle>
<div className={cn('bg-muted/50 rounded', className)} {...props}>
<CardHeader className="p-4">
<CardTitle className="text-lg font-medium">{label}</CardTitle>
<CardDescription className="text-muted-foreground max-w-xs text-sm font-normal">
{description}

View File

@@ -20,12 +20,13 @@ export const FeatureShowcase: React.FC<FeatureShowcaseProps> =
className={cn('flex flex-col justify-between space-y-8', className)}
{...props}
>
<div className="flex w-full max-w-5xl flex-col gap-y-4">
<div className="flex w-full flex-col gap-y-4">
{icon && <div className="flex">{icon}</div>}
<h3 className="text-3xl font-normal tracking-tight xl:text-5xl">
{heading}
</h3>
</div>
{children}
</div>
);
@@ -40,7 +41,7 @@ export function FeatureShowcaseIconContainer(
<div className={'flex'}>
<div
className={cn(
'flex items-center justify-center space-x-4 rounded-lg p-3 font-medium',
'flex items-center justify-center space-x-2.5 font-medium',
props.className,
)}
>

View File

@@ -26,7 +26,7 @@ export const Footer: React.FC<FooterProps> = ({
return (
<footer
className={cn(
'site-footer relative mt-auto w-full py-8 2xl:py-20',
'site-footer bg-muted/20 relative mt-auto w-full py-8 2xl:py-20',
className,
)}
{...props}
@@ -39,9 +39,7 @@ export const Footer: React.FC<FooterProps> = ({
<div className="flex flex-col gap-y-4">
<div>
<p className="text-muted-foreground text-sm tracking-tight">
{description}
</p>
<p className="text-muted-foreground text-sm">{description}</p>
</div>
<div className="text-muted-foreground flex text-xs">
@@ -51,10 +49,10 @@ export const Footer: React.FC<FooterProps> = ({
</div>
</div>
<div className="flex w-full flex-col gap-y-4 lg:flex-row lg:justify-end lg:gap-x-6 lg:gap-y-0 xl:gap-x-12">
<div className="flex w-full flex-1 flex-col gap-y-4 lg:flex-row lg:justify-end lg:gap-x-6 lg:gap-y-0 xl:gap-x-12">
{sections.map((section, index) => (
<div key={index}>
<div className="flex flex-col gap-y-2.5">
<div className="flex flex-col gap-y-1">
<FooterSectionHeading>{section.heading}</FooterSectionHeading>
<FooterSectionList>
@@ -76,14 +74,14 @@ export const Footer: React.FC<FooterProps> = ({
function FooterSectionHeading(props: React.PropsWithChildren) {
return (
<span className="font-heading text-sm font-semibold tracking-tight">
<span className="font-heading text-secondary-foreground/90 text-sm font-medium">
{props.children}
</span>
);
}
function FooterSectionList(props: React.PropsWithChildren) {
return <ul className="flex flex-col gap-y-2">{props.children}</ul>;
return <ul className="flex flex-col gap-y-1">{props.children}</ul>;
}
function FooterLink({
@@ -91,7 +89,7 @@ function FooterLink({
children,
}: React.PropsWithChildren<{ href: string }>) {
return (
<li className="text-muted-foreground text-sm tracking-tight hover:underline [&>a]:transition-colors">
<li className="text-muted-foreground text-sm font-medium hover:underline [&>a]:transition-colors">
<a href={href}>{children}</a>
</li>
);

View File

@@ -16,7 +16,7 @@ export const Header: React.FC<HeaderProps> = function ({
return (
<div
className={cn(
'site-header bg-background/80 dark:bg-background/50 sticky top-0 z-10 w-full py-1 backdrop-blur-md',
'site-header bg-background/80 dark:bg-background/80 sticky top-0 z-10 w-full backdrop-blur-lg',
className,
)}
{...props}

View File

@@ -12,7 +12,7 @@ export const HeroTitle: React.FC<
return (
<Comp
className={cn(
'hero-title flex flex-col text-center font-sans text-4xl font-semibold tracking-tighter sm:text-6xl lg:max-w-5xl lg:text-7xl xl:text-[4.5rem] dark:text-white',
'hero-title flex flex-col text-center font-sans text-4xl font-medium tracking-tighter sm:text-6xl lg:max-w-5xl lg:text-7xl xl:max-w-6xl dark:text-white',
className,
)}
{...props}

View File

@@ -23,7 +23,7 @@ export function Hero({
animate = true,
}: HeroProps) {
return (
<div className={cn('mx-auto flex flex-col space-y-20', className)}>
<div className={cn('mx-auto flex flex-col space-y-14', className)}>
<div
style={{
MozAnimationDuration: '100ms',
@@ -35,7 +35,7 @@ export function Hero({
},
)}
>
<div className="flex w-full flex-1 flex-col items-center gap-y-6 xl:gap-y-8 2xl:gap-y-12">
<div className="flex w-full flex-1 flex-col items-center gap-y-6 xl:gap-y-8">
{pill && (
<div
className={cn({
@@ -51,8 +51,8 @@ export function Hero({
<HeroTitle>{title}</HeroTitle>
{subtitle && (
<div className="flex max-w-lg">
<h3 className="text-muted-foreground p-0 text-center font-sans text-2xl font-normal tracking-tight">
<div className="flex max-w-3xl">
<h3 className="text-secondary-foreground/70 p-0 text-center font-sans text-xl font-medium tracking-tight">
{subtitle}
</h3>
</div>

View File

@@ -13,3 +13,4 @@ export * from './feature-card';
export * from './newsletter-signup';
export * from './newsletter-signup-container';
export * from './coming-soon';
export * from './ecosystem-showcase';

View File

@@ -14,7 +14,7 @@ export const Pill: React.FC<
return (
<Comp
className={cn(
'bg-muted/50 flex items-center gap-x-1.5 rounded-full border px-2 py-1 pr-2 text-center text-sm font-medium text-transparent',
'bg-muted/50 flex min-h-10 items-center gap-x-1.5 rounded-full border px-2 py-1 text-center text-sm font-medium text-transparent',
className,
)}
{...props}

View File

@@ -19,7 +19,7 @@ export const SecondaryHero: React.FC<SecondaryHeroProps> =
return (
<div
className={cn(
'flex flex-col items-center space-y-6 text-center',
'flex flex-col items-center space-y-4 text-center',
className,
)}
{...props}
@@ -31,7 +31,7 @@ export const SecondaryHero: React.FC<SecondaryHeroProps> =
{heading}
</Heading>
<h3 className="text-muted-foreground font-sans text-xl font-normal tracking-tight">
<h3 className="text-secondary-foreground/70 text-center font-sans text-xl font-medium tracking-tight">
{subheading}
</h3>
</div>

View File

@@ -6,24 +6,30 @@ export function Spinner(
}>,
) {
return (
<div role="status">
<div role="status" aria-label="loading">
<svg
aria-hidden="true"
className={cn(
`fill-primary-foreground text-primary dark:fill-primary dark:text-primary/30 h-8 w-8 animate-spin`,
props.className,
'stroke-muted-foreground h-6 w-6 animate-spin',
)}
viewBox="0 0 100 101"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
<g clipPath="url(#clip0_9023_61563)">
<path
d="M14.6437 2.05426C11.9803 1.2966 9.01686 1.64245 6.50315 3.25548C1.85499 6.23817 0.504864 12.4242 3.48756 17.0724C6.47025 21.7205 12.6563 23.0706 17.3044 20.088C20.4971 18.0393 22.1338 14.4793 21.8792 10.9444"
stroke="stroke-current"
strokeWidth="1.4"
strokeLinecap="round"
/>
</g>
<defs>
<clipPath id="clip0_9023_61563">
<rect width="24" height="24" fill="white" />
</clipPath>
</defs>
</svg>
</div>
);

View File

@@ -7,7 +7,7 @@ const Card: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
...props
}) => (
<div
className={cn('bg-card text-card-foreground rounded-xl border', className)}
className={cn('bg-card text-card-foreground rounded-lg border', className)}
{...props}
/>
);

View File

@@ -25,13 +25,13 @@ const RadioGroupItem: React.FC<
return (
<RadioGroupPrimitive.Item
className={cn(
'border-primary text-primary focus-visible:ring-ring aspect-square h-4 w-4 rounded-full border shadow-xs focus:outline-hidden focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
'border-primary text-primary focus-visible:ring-ring aspect-square h-4 w-4 rounded-full border focus:outline-hidden focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<CheckIcon className="fill-primary h-3.5 w-3.5" />
<CheckIcon className="fill-primary animate-in fade-in slide-in-from-left-4 h-3.5 w-3.5" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);
@@ -46,14 +46,15 @@ const RadioGroupItemLabel = (
) => {
return (
<label
data-selected={props.selected}
className={cn(
props.className,
'flex cursor-pointer rounded-md' +
' border-input items-center space-x-4 border' +
' transition-duration-500 focus-within:border-primary p-4 text-sm transition-all',
'focus-within:border-primary active:bg-muted p-2.5 text-sm transition-all',
{
[`bg-muted`]: props.selected,
[`hover:bg-muted`]: !props.selected,
[`bg-muted/70`]: props.selected,
[`hover:bg-muted/50`]: !props.selected,
},
)}
>