chore:little fixes and formating and linting and patches

This commit is contained in:
2026-03-31 15:50:54 +02:00
parent a1ce71ffb6
commit 9b197abcfa
815 changed files with 22960 additions and 8982 deletions

View File

@@ -18,23 +18,23 @@ export function ArticleCard({
}) {
return (
<Link
className="col overflow-hidden rounded-lg border bg-background-light transition-all duration-300 hover:scale-105 hover:shadow-background-dark hover:shadow-lg"
href={url}
key={url}
className="border rounded-lg overflow-hidden bg-background-light col hover:scale-105 transition-all duration-300 hover:shadow-lg hover:shadow-background-dark"
>
<Image
src={cover}
alt={title}
width={323}
height={181}
className="w-full"
height={181}
src={cover}
width={323}
/>
<span className="p-4 col flex-1">
{tag && <span className="font-mono text-xs mb-2">{tag}</span>}
<span className="flex-1 mb-6">
<h2 className="text-xl font-semibold">{title}</h2>
<span className="col flex-1 p-4">
{tag && <span className="mb-2 font-mono text-xs">{tag}</span>}
<span className="mb-6 flex-1">
<h2 className="font-semibold text-xl">{title}</h2>
</span>
<p className="text-sm text-muted-foreground">
<p className="text-muted-foreground text-sm">
{[team, date.toLocaleDateString()].filter(Boolean).join(' · ')}
</p>
</span>

View File

@@ -45,31 +45,31 @@ export function Competition() {
if (!mounted) {
return (
<span className="block truncate leading-tight -mt-1" style={{ color }}>
<span className="-mt-1 block truncate leading-tight" style={{ color }}>
{word}
</span>
);
}
return (
<AnimatePresence mode="wait" initial={false}>
<AnimatePresence initial={false} mode="wait">
<motion.div
className="-mt-1 block truncate leading-tight"
key={word}
className="block truncate leading-tight -mt-1"
style={{ color }}
>
{word?.split('').map((char, index) => (
<motion.span
key={`${word}-${char}-${index.toString()}`}
initial={{ y: 10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -10, opacity: 0 }}
initial={{ y: 10, opacity: 0 }}
key={`${word}-${char}-${index.toString()}`}
style={{ display: 'inline-block', whiteSpace: 'pre' }}
transition={{
duration: 0.15,
delay: index * 0.015,
ease: 'easeOut',
}}
style={{ display: 'inline-block', whiteSpace: 'pre' }}
>
{char}
</motion.span>

View File

@@ -27,8 +27,8 @@ export function EuFlag({ className }: { className?: string }) {
{STARS.map((s, i) => (
<polygon
// biome-ignore lint/suspicious/noArrayIndexKey: static data
key={i}
fill="#FFCC00"
key={i}
points={star(s.x, s.y, 1.1, 0.45)}
/>
))}

View File

@@ -1,8 +1,7 @@
import type { LucideIcon } from 'lucide-react';
import Link from 'next/link';
import { cn } from '@/lib/utils';
import { FeatureCardHoverTrack } from '@/components/feature-card-hover-track';
import { cn } from '@/lib/utils';
interface FeatureCardProps {
link?: {

View File

@@ -1,22 +1,27 @@
import { cn } from '@/lib/utils';
import Image from 'next/image';
import { cn } from '@/lib/utils';
export function Figure({
src,
alt,
caption,
className,
}: { src: string; alt: string; caption: string; className?: string }) {
}: {
src: string;
alt: string;
caption: string;
className?: string;
}) {
return (
<figure className={cn('-mx-4', className)}>
<Image
src={src}
alt={alt || caption}
width={1200}
height={800}
className="rounded-lg"
height={800}
src={src}
width={1200}
/>
<figcaption className="text-center text-sm text-muted-foreground mt-2">
<figcaption className="mt-2 text-center text-muted-foreground text-sm">
{caption}
</figcaption>
</figure>

View File

@@ -45,15 +45,15 @@ export function FlowStep({
const Icon = iconMap[icon];
return (
<div className="relative flex gap-4 mb-4 min-w-0">
<div className="relative mb-4 flex min-w-0 gap-4">
{/* Step number and icon */}
<div className="flex flex-col items-center shrink-0">
<div className="flex shrink-0 flex-col items-center">
<div className="relative z-10 bg-background">
<div className="flex items-center justify-center size-10 rounded-full bg-primary text-primary-foreground font-semibold text-sm shadow-sm">
<div className="flex size-10 items-center justify-center rounded-full bg-primary font-semibold text-primary-foreground text-sm shadow-sm">
{step}
</div>
<div
className={`absolute -bottom-2 -right-2 flex items-center justify-center w-6 h-6 rounded-full bg-background border shadow-sm ${iconBorderColorMap[icon] || 'border-primary'}`}
className={`absolute -right-2 -bottom-2 flex h-6 w-6 items-center justify-center rounded-full border bg-background shadow-sm ${iconBorderColorMap[icon] || 'border-primary'}`}
>
<Icon
className={`size-3.5 ${iconColorMap[icon] || 'text-primary'}`}
@@ -62,14 +62,14 @@ export function FlowStep({
</div>
{/* Connector line - extends from badge through content to next step */}
{!isLast && (
<div className="w-0.5 bg-border mt-2 flex-1 min-h-[2rem]" />
<div className="mt-2 min-h-[2rem] w-0.5 flex-1 bg-border" />
)}
</div>
{/* Content */}
<div className="flex-1 pt-1 min-w-0">
<div className="min-w-0 flex-1 pt-1">
<div className="mb-2">
<span className="font-semibold text-foreground mr-2">{actor}:</span>{' '}
<span className="mr-2 font-semibold text-foreground">{actor}:</span>{' '}
<span className="text-muted-foreground">{description}</span>
</div>
{children && <div className="mt-3 min-w-0">{children}</div>}

View File

@@ -1,7 +1,7 @@
import { getGithubRepoInfo } from '@/lib/github';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { Button } from './ui/button';
import { getGithubRepoInfo } from '@/lib/github';
function formatStars(stars: number) {
if (stars >= 1000) {
@@ -12,7 +12,7 @@ function formatStars(stars: number) {
}
export function GithubButton() {
const [stars, setStars] = useState(4_800);
const [stars, setStars] = useState(4800);
useEffect(() => {
getGithubRepoInfo().then((res) => {
if (res?.stargazers_count) {
@@ -21,18 +21,18 @@ export function GithubButton() {
});
}, []);
return (
<Button variant={'secondary'} asChild>
<Link href="https://git.new/openpanel" className="hidden md:flex">
<Button asChild variant={'secondary'}>
<Link className="hidden md:flex" href="https://git.new/openpanel">
<svg
className="w-5 h-5"
aria-hidden="true"
className="h-5 w-5"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clipRule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
fillRule="evenodd"
/>
</svg>
{formatStars(stars)} stars

View File

@@ -33,32 +33,32 @@ export function GuideCard({
}) {
return (
<Link
className="col overflow-hidden rounded-lg border bg-background-light transition-all duration-300 hover:scale-105 hover:shadow-background-dark hover:shadow-lg"
href={url}
key={url}
className="border rounded-lg overflow-hidden bg-background-light col hover:scale-105 transition-all duration-300 hover:shadow-lg hover:shadow-background-dark"
>
<Image
src={cover}
alt={title}
width={323}
height={181}
className="w-full"
height={181}
src={cover}
width={323}
/>
<span className="p-4 col flex-1">
<div className="flex items-center gap-2 mb-2">
<span className="col flex-1 p-4">
<div className="mb-2 flex items-center gap-2">
<span
className={`font-mono text-xs px-2 py-1 rounded ${difficultyColors[difficulty]}`}
className={`rounded px-2 py-1 font-mono text-xs ${difficultyColors[difficulty]}`}
>
{difficultyLabels[difficulty]}
</span>
<span className="text-xs text-muted-foreground">
<span className="text-muted-foreground text-xs">
{timeToComplete} min
</span>
</div>
<span className="flex-1 mb-6">
<h2 className="text-xl font-semibold">{title}</h2>
<span className="mb-6 flex-1">
<h2 className="font-semibold text-xl">{title}</h2>
</span>
<p className="text-sm text-muted-foreground">
<p className="text-muted-foreground text-sm">
{[team, date.toLocaleDateString()].filter(Boolean).join(' · ')}
</p>
</span>

View File

@@ -7,7 +7,7 @@ const variantB = [28, 30, 32, 35, 38, 37, 40, 42, 44, 43, 47, 50];
export function ConversionsIllustration() {
return (
<div className="h-full col gap-3 px-4 pb-3 pt-5">
<div className="col h-full gap-3 px-4 pt-5 pb-3">
{/* A/B variant cards */}
<div className="row gap-3">
<div className="col flex-1 gap-1 rounded-xl border bg-card p-3 transition-all duration-300 group-hover:-translate-y-0.5">
@@ -30,7 +30,7 @@ export function ConversionsIllustration() {
Variant B
</span>
</div>
<span className="font-bold font-mono text-xl text-emerald-500">
<span className="font-bold font-mono text-emerald-500 text-xl">
41.2%
</span>
<SimpleChart
@@ -44,7 +44,7 @@ export function ConversionsIllustration() {
{/* Breakdown label */}
<div className="col gap-1 rounded-xl border bg-card/60 px-3 py-2.5">
<span className="text-[9px] uppercase tracking-wider text-muted-foreground">
<span className="text-[9px] text-muted-foreground uppercase tracking-wider">
Breakdown by experiment variant
</span>
<div className="row items-center gap-2">

View File

@@ -1,5 +1,3 @@
import React from 'react';
type IllustrationProps = {
className?: string;
};
@@ -12,15 +10,8 @@ export function DataOwnershipIllustration({
{/* Main layout */}
<div className="relative grid aspect-2/1 grid-cols-5 gap-3">
{/* Left: your server card */}
<div
className="
col-span-3 rounded-2xl border border-border bg-card/80
p-3 sm:p-4 shadow-xl backdrop-blur
transition-all duration-300
group-hover:-translate-y-1 group-hover:-translate-x-0.5
"
>
<div className="flex items-center justify-between text-xs text-foreground">
<div className="col-span-3 rounded-2xl border border-border bg-card/80 p-3 shadow-xl backdrop-blur transition-all duration-300 group-hover:-translate-x-0.5 group-hover:-translate-y-1 sm:p-4">
<div className="flex items-center justify-between text-foreground text-xs">
<span>Your server</span>
<span className="flex items-center gap-1 rounded-full bg-card/80 px-2 py-0.5 text-[10px] text-blue-300">
<span className="h-1.5 w-1.5 rounded-full bg-blue-400" />
@@ -31,15 +22,15 @@ export function DataOwnershipIllustration({
{/* "Server" visual */}
<div className="mt-3 space-y-2">
<div className="flex gap-1.5">
<div className="flex-1 rounded-xl bg-card/80 border border-border px-3 py-2">
<div className="flex-1 rounded-xl border border-border bg-card/80 px-3 py-2">
<p className="text-[10px] text-muted-foreground">Region</p>
<p className="text-xs font-medium text-foreground">
<p className="font-medium text-foreground text-xs">
EU / Custom
</p>
</div>
<div className="flex-1 rounded-xl bg-card/80 border border-border px-3 py-2">
<div className="flex-1 rounded-xl border border-border bg-card/80 px-3 py-2">
<p className="text-[10px] text-muted-foreground">Retention</p>
<p className="text-xs font-medium text-foreground">
<p className="font-medium text-foreground text-xs">
Configurable
</p>
</div>
@@ -74,15 +65,8 @@ export function DataOwnershipIllustration({
</div>
{/* Right: third-party contrast */}
<div
className="
col-span-2 rounded-2xl border border-border/80 bg-card/40
p-3 text-[11px] text-muted-foreground
transition-all duration-300
group-hover:translate-y-1 group-hover:translate-x-0.5 group-hover:opacity-70
"
>
<p className="text-xs text-muted-foreground mb-2">or use our cloud</p>
<div className="col-span-2 rounded-2xl border border-border/80 bg-card/40 p-3 text-[11px] text-muted-foreground transition-all duration-300 group-hover:translate-x-0.5 group-hover:translate-y-1 group-hover:opacity-70">
<p className="mb-2 text-muted-foreground text-xs">or use our cloud</p>
<ul className="space-y-1.5">
<li className="flex items-center gap-1.5">

View File

@@ -1,5 +1,3 @@
import React from 'react';
type IllustrationProps = {
className?: string;
};
@@ -10,15 +8,8 @@ export function PrivacyIllustration({ className = '' }: IllustrationProps) {
{/* Floating cards */}
<div className="relative aspect-3/2 md:aspect-2/1">
{/* Back card */}
<div
className="
absolute top-0 left-0 right-10 bottom-10 rounded-2xl border border-border/80 bg-card/70
backdrop-blur-sm shadow-lg
transition-all duration-300
group-hover:-translate-y-1 group-hover:-rotate-2
"
>
<div className="flex items-center justify-between px-4 pt-3 text-xs text-muted-foreground">
<div className="absolute top-0 right-10 bottom-10 left-0 rounded-2xl border border-border/80 bg-card/70 shadow-lg backdrop-blur-sm transition-all duration-300 group-hover:-translate-y-1 group-hover:-rotate-2">
<div className="flex items-center justify-between px-4 pt-3 text-muted-foreground text-xs">
<span>Session duration</span>
<span className="flex items-center gap-1">
3m 12s
@@ -29,45 +20,37 @@ export function PrivacyIllustration({ className = '' }: IllustrationProps) {
{/* Simple line chart */}
<div className="mt-3 px-4">
<svg
viewBox="0 0 120 40"
className="h-16 w-full text-muted-foreground"
viewBox="0 0 120 40"
>
<path
className="opacity-60"
d="M2 32 L22 18 L40 24 L60 10 L78 16 L96 8 L118 14"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
className="opacity-60"
strokeWidth="2"
/>
<circle cx="118" cy="14" r="2.5" className="fill-blue-400" />
<circle className="fill-blue-400" cx="118" cy="14" r="2.5" />
</svg>
</div>
</div>
{/* Front card */}
<div
className="
col
absolute top-10 left-4 right-0 bottom-0 rounded-2xl border border-border/80
bg-card shadow-xl
transition-all duration-300
group-hover:translate-y-1 group-hover:rotate-2
"
>
<div className="flex items-center justify-between px-4 pt-3 text-xs text-foreground">
<div className="col absolute top-10 right-0 bottom-0 left-4 rounded-2xl border border-border/80 bg-card shadow-xl transition-all duration-300 group-hover:translate-y-1 group-hover:rotate-2">
<div className="flex items-center justify-between px-4 pt-3 text-foreground text-xs">
<span>Anonymous visitors</span>
<span className="text-[10px] rounded-full bg-card px-2 py-0.5 text-muted-foreground">
<span className="rounded-full bg-card px-2 py-0.5 text-[10px] text-muted-foreground">
No cookies
</span>
</div>
<div className="flex items-end justify-between px-4 pt-4 pb-3">
<div>
<p className="text-[11px] text-muted-foreground mb-1">
<p className="mb-1 text-[11px] text-muted-foreground">
Active now
</p>
<p className="text-2xl font-semibold text-foreground">128</p>
<p className="font-semibold text-2xl text-foreground">128</p>
</div>
<div className="space-y-1.5 text-[10px] text-muted-foreground">
<div className="flex items-center gap-1.5">
@@ -82,12 +65,12 @@ export function PrivacyIllustration({ className = '' }: IllustrationProps) {
</div>
{/* "Sources" row */}
<div className="mt-auto flex gap-2 border-t border-border px-3 py-2.5 text-[11px]">
<div className="flex-1 rounded-xl bg-card/90 px-3 py-1.5 flex items-center justify-between">
<div className="mt-auto flex gap-2 border-border border-t px-3 py-2.5 text-[11px]">
<div className="flex flex-1 items-center justify-between rounded-xl bg-card/90 px-3 py-1.5">
<span className="text-muted-foreground">Direct</span>
<span className="text-foreground">42%</span>
</div>
<div className="flex-1 rounded-xl bg-card/90 px-3 py-1.5 flex items-center justify-between">
<div className="flex flex-1 items-center justify-between rounded-xl bg-card/90 px-3 py-1.5">
<span className="text-muted-foreground">Organic</span>
<span className="text-foreground">58%</span>
</div>

View File

@@ -1,10 +1,6 @@
'use client';
import { cn } from '@/lib/utils';
import { ResponsiveFunnel } from '@nivo/funnel';
import NumberFlow from '@number-flow/react';
import { AnimatePresence, motion, useSpring } from 'framer-motion';
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';
function useFunnelSteps() {
const { resolvedTheme } = useTheme();
@@ -12,7 +8,7 @@ function useFunnelSteps() {
{
id: 'Visitors',
label: 'Visitors',
value: 10000,
value: 10_000,
percentage: 100,
color: resolvedTheme === 'dark' ? '#333' : '#888',
},
@@ -46,7 +42,6 @@ export const PartLabel = ({ part }: { part: any }) => {
return (
<g transform={`translate(${part.x}, ${part.y})`}>
<text
textAnchor="middle"
dominantBaseline="central"
style={{
fill: resolvedTheme === 'dark' ? '#fff' : '#000',
@@ -54,6 +49,7 @@ export const PartLabel = ({ part }: { part: any }) => {
fontSize: 12,
fontWeight: 500,
}}
textAnchor="middle"
>
{part.data.label}
</text>
@@ -77,24 +73,24 @@ function FunnelVisualization() {
}));
return (
<div className="w-full h-full">
<div className="h-full w-full">
<ResponsiveFunnel
data={nivoData}
margin={{ top: 20, right: 0, bottom: 20, left: 0 }}
direction="horizontal"
shapeBlending={0.6}
colors={colors}
enableBeforeSeparators={false}
enableAfterSeparators={false}
beforeSeparatorLength={0}
afterSeparatorLength={0}
afterSeparatorOffset={0}
beforeSeparatorLength={0}
beforeSeparatorOffset={0}
currentPartSizeExtension={5}
borderWidth={20}
colors={colors}
currentBorderWidth={15}
tooltip={() => null}
currentPartSizeExtension={5}
data={nivoData}
direction="horizontal"
enableAfterSeparators={false}
enableBeforeSeparators={false}
layers={['parts', Labels]}
margin={{ top: 20, right: 0, bottom: 20, left: 0 }}
shapeBlending={0.6}
tooltip={() => null}
/>
</div>
);

View File

@@ -20,36 +20,36 @@ function cellStyle(v: number | null) {
const opacity = 0.12 + (v / 100) * 0.7;
return {
backgroundColor: `rgba(34, 197, 94, ${opacity})`,
borderColor: `rgba(34, 197, 94, 0.3)`,
borderColor: 'rgba(34, 197, 94, 0.3)',
color: v > 55 ? 'rgba(0,0,0,0.75)' : 'var(--foreground)',
};
}
export function RetentionIllustration() {
return (
<div className="h-full px-4 pb-3 pt-5">
<div className="h-full px-4 pt-5 pb-3">
<div className="col h-full gap-1.5">
<div className="row gap-1">
<div className="w-12 shrink-0" />
{headers.map((h) => (
<div
key={h}
className="flex-1 text-center text-[9px] text-muted-foreground"
key={h}
>
{h}
</div>
))}
</div>
{cohorts.map(({ label, values }) => (
<div key={label} className="row flex-1 gap-1">
<div className="row flex-1 gap-1" key={label}>
<div className="flex w-12 shrink-0 items-center text-[9px] text-muted-foreground">
{label}
</div>
{values.map((v, i) => (
<div
// biome-ignore lint/suspicious/noArrayIndexKey: static data
className="flex flex-1 items-center justify-center rounded border font-medium text-[9px] transition-all duration-300 group-hover:scale-[1.03]"
key={i}
className="flex flex-1 items-center justify-center rounded border text-[9px] font-medium transition-all duration-300 group-hover:scale-[1.03]"
style={cellStyle(v)}
>
{v !== null ? `${v}%` : '—'}

View File

@@ -13,20 +13,22 @@ const referrers = [
export function RevenueIllustration() {
return (
<div className="h-full col gap-3 px-4 pb-3 pt-5">
<div className="col h-full gap-3 px-4 pt-5 pb-3">
{/* MRR stat + chart */}
<div className="row gap-3">
<div className="col gap-1 rounded-xl border bg-card p-3 transition-all duration-300 group-hover:-translate-y-0.5">
<span className="text-[9px] uppercase tracking-wider text-muted-foreground">
<span className="text-[9px] text-muted-foreground uppercase tracking-wider">
MRR
</span>
<span className="font-bold font-mono text-xl text-emerald-500">
<span className="font-bold font-mono text-emerald-500 text-xl">
$8,420
</span>
<span className="text-[9px] text-emerald-500"> 12% this month</span>
</div>
<div className="col flex-1 gap-1 rounded-xl border bg-card px-3 py-2">
<span className="text-[9px] text-muted-foreground">MRR over time</span>
<span className="text-[9px] text-muted-foreground">
MRR over time
</span>
<SimpleChart
className="mt-1 flex-1"
height={36}
@@ -39,29 +41,29 @@ export function RevenueIllustration() {
{/* Revenue by referrer */}
<div className="flex-1 overflow-hidden rounded-xl border bg-card">
<div className="row border-b border-border px-3 py-1.5">
<span className="flex-1 text-[8px] uppercase tracking-wider text-muted-foreground">
<div className="row border-border border-b px-3 py-1.5">
<span className="flex-1 text-[8px] text-muted-foreground uppercase tracking-wider">
Referrer
</span>
<span className="text-[8px] uppercase tracking-wider text-muted-foreground">
<span className="text-[8px] text-muted-foreground uppercase tracking-wider">
Revenue
</span>
</div>
{referrers.map((r) => (
<div
className="row items-center gap-2 border-b border-border/50 px-3 py-1.5 last:border-0"
className="row items-center gap-2 border-border/50 border-b px-3 py-1.5 last:border-0"
key={r.name}
>
<span className="text-[9px] text-muted-foreground flex-none w-20 truncate">
<span className="w-20 flex-none truncate text-[9px] text-muted-foreground">
{r.name}
</span>
<div className="flex-1 h-1 rounded-full bg-muted overflow-hidden">
<div className="h-1 flex-1 overflow-hidden rounded-full bg-muted">
<div
className="h-1 rounded-full bg-emerald-500/70"
style={{ width: `${r.pct}%` }}
/>
</div>
<span className="font-mono text-[9px] text-emerald-500 flex-none">
<span className="flex-none font-mono text-[9px] text-emerald-500">
{r.amount}
</span>
</div>

View File

@@ -2,10 +2,10 @@ import { PlayIcon } from 'lucide-react';
export function SessionReplayIllustration() {
return (
<div className="h-full px-6 pb-3 pt-4">
<div className="h-full px-6 pt-4 pb-3">
<div className="col h-full overflow-hidden rounded-xl border border-border bg-background shadow-lg transition-transform duration-300 group-hover:-translate-y-0.5">
{/* Browser chrome */}
<div className="row shrink-0 items-center gap-1.5 border-b border-border bg-muted/30 px-3 py-2">
<div className="row shrink-0 items-center gap-1.5 border-border border-b bg-muted/30 px-3 py-2">
<div className="h-2 w-2 rounded-full bg-red-400" />
<div className="h-2 w-2 rounded-full bg-yellow-400" />
<div className="h-2 w-2 rounded-full bg-green-400" />
@@ -26,16 +26,10 @@ export function SessionReplayIllustration() {
<div className="h-2 w-24 rounded-full bg-muted/20" />
{/* Click heatspot */}
<div
className="absolute"
style={{ left: '62%', top: '48%' }}
>
<div className="absolute" style={{ left: '62%', top: '48%' }}>
<div className="h-4 w-4 animate-pulse rounded-full border-2 border-blue-500/70 bg-blue-500/20" />
</div>
<div
className="absolute"
style={{ left: '25%', top: '32%' }}
>
<div className="absolute" style={{ left: '25%', top: '32%' }}>
<div className="h-2.5 w-2.5 rounded-full border border-blue-500/40 bg-blue-500/25" />
</div>
@@ -71,11 +65,11 @@ export function SessionReplayIllustration() {
</div>
{/* Playback bar */}
<div className="row shrink-0 items-center gap-2 border-t border-border bg-muted/20 px-3 py-2">
<div className="row shrink-0 items-center gap-2 border-border border-t bg-muted/20 px-3 py-2">
<PlayIcon className="size-3 shrink-0 text-muted-foreground" />
<div className="relative flex-1 h-1 overflow-hidden rounded-full bg-muted">
<div className="relative h-1 flex-1 overflow-hidden rounded-full bg-muted">
<div
className="absolute left-0 top-0 h-1 rounded-full bg-blue-500"
className="absolute top-0 left-0 h-1 rounded-full bg-blue-500"
style={{ width: '42%' }}
/>
</div>

View File

@@ -9,9 +9,21 @@ const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
const STATS = [
{ label: 'Visitors', value: 4128, formatted: null, change: 12, up: true },
{ label: 'Page views', value: 12438, formatted: '12.4k', change: 8, up: true },
{
label: 'Page views',
value: 12_438,
formatted: '12.4k',
change: 8,
up: true,
},
{ label: 'Bounce rate', value: null, formatted: '42%', change: 3, up: false },
{ label: 'Avg. session', value: null, formatted: '3m 23s', change: 5, up: true },
{
label: 'Avg. session',
value: null,
formatted: '3m 23s',
change: 5,
up: true,
},
];
const SOURCES = [
@@ -38,7 +50,9 @@ function AreaChart({ data }: { data: number[] }) {
const h = 64;
const xStep = w / (data.length - 1);
const pts = data.map((v, i) => ({ x: i * xStep, y: h - (v / max) * h }));
const line = pts.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x},${p.y}`).join(' ');
const line = pts
.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x},${p.y}`)
.join(' ');
const area = `${line} L ${w},${h} L 0,${h} Z`;
const last = pts[pts.length - 1];
@@ -87,7 +101,7 @@ export function WebAnalyticsIllustration() {
}, []);
return (
<div className="aspect-video col gap-2.5 p-5">
<div className="col aspect-video gap-2.5 p-5">
{/* Header */}
<div className="row items-center justify-between">
<div className="row items-center gap-1.5">
@@ -95,7 +109,7 @@ export function WebAnalyticsIllustration() {
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75" />
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-emerald-500" />
</span>
<span className="text-[10px] font-medium text-muted-foreground">
<span className="font-medium text-[10px] text-muted-foreground">
<NumberFlow value={liveVisitors} /> online now
</span>
</div>
@@ -111,7 +125,9 @@ export function WebAnalyticsIllustration() {
className="col gap-0.5 rounded-lg border bg-card px-2 py-1.5"
key={stat.label}
>
<span className="text-[8px] text-muted-foreground">{stat.label}</span>
<span className="text-[8px] text-muted-foreground">
{stat.label}
</span>
<span className="font-mono font-semibold text-xs leading-tight">
{stat.formatted ??
(stat.value !== null ? (
@@ -128,8 +144,10 @@ export function WebAnalyticsIllustration() {
</div>
{/* Area chart */}
<div className="flex-1 col gap-1 overflow-hidden rounded-xl border bg-card px-3 pt-2 pb-1">
<span className="text-[8px] text-muted-foreground">Unique visitors</span>
<div className="col flex-1 gap-1 overflow-hidden rounded-xl border bg-card px-3 pt-2 pb-1">
<span className="text-[8px] text-muted-foreground">
Unique visitors
</span>
<AreaChart data={VISITOR_DATA} />
<div className="row justify-between px-0.5">
{DAYS.map((d) => (

View File

@@ -1,7 +1,7 @@
// Thank you: https://ui.aceternity.com/components/infinite-moving-cards
import { cn } from '@/lib/utils';
import React, { useEffect, useState } from 'react';
import { cn } from '@/lib/utils';
export const InfiniteMovingCards = <T,>({
items,
@@ -47,12 +47,12 @@ export const InfiniteMovingCards = <T,>({
if (direction === 'left') {
containerRef.current.style.setProperty(
'--animation-direction',
'forwards',
'forwards'
);
} else {
containerRef.current.style.setProperty(
'--animation-direction',
'reverse',
'reverse'
);
}
}
@@ -71,23 +71,23 @@ export const InfiniteMovingCards = <T,>({
return (
<div
ref={containerRef}
className={cn(
'scroller relative z-20 overflow-hidden -ml-4 md:-ml-[1200px] w-screen md:w-[calc(100vw+1400px)]',
className,
'scroller relative z-20 -ml-4 w-screen overflow-hidden md:-ml-[1200px] md:w-[calc(100vw+1400px)]',
className
)}
ref={containerRef}
>
<ul
ref={scrollerRef}
className={cn(
'flex min-w-full shrink-0 gap-8 py-4 w-max flex-nowrap items-start',
'flex w-max min-w-full shrink-0 flex-nowrap items-start gap-8 py-4',
start && 'animate-scroll',
pauseOnHover && 'hover:[animation-play-state:paused]',
pauseOnHover && 'hover:[animation-play-state:paused]'
)}
ref={scrollerRef}
>
{items.map((item, idx) => (
<li
className="w-[310px] max-w-full relative shrink-0 md:w-[400px]"
className="relative w-[310px] max-w-full shrink-0 md:w-[400px]"
key={idx.toString()}
>
{renderItem(item, idx)}

View File

@@ -3,29 +3,29 @@ import { cn } from '@/lib/utils';
export function Logo({ className }: { className?: string }) {
return (
<svg
className={cn('w-16 text-black dark:text-white', className)}
fill="currentColor"
viewBox="0 0 61 35"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
className={cn('text-black dark:text-white w-16', className)}
>
<rect
x="34.0269"
y="0.368164"
width="10.3474"
height="34.2258"
rx="5.17372"
width="10.3474"
x="34.0269"
y="0.368164"
/>
<rect
x="49.9458"
y="0.368164"
width="10.3474"
height="17.5109"
rx="5.17372"
width="10.3474"
x="49.9458"
y="0.368164"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14.212 0C6.36293 0 0 6.36293 0 14.212V20.02C0 27.8691 6.36293 34.232 14.212 34.232C22.0611 34.232 28.424 27.8691 28.424 20.02V14.212C28.424 6.36293 22.0611 0 14.212 0ZM14.2379 8.35999C11.3805 8.35999 9.06419 10.6763 9.06419 13.5337V20.6971C9.06419 23.5545 11.3805 25.8708 14.2379 25.8708C17.0953 25.8708 19.4116 23.5545 19.4116 20.6971V13.5337C19.4116 10.6763 17.0953 8.35999 14.2379 8.35999Z"
fillRule="evenodd"
/>
</svg>
);

View File

@@ -1,18 +1,21 @@
import type { LucideIcon } from 'lucide-react';
import type React from 'react';
import { cn } from '@/lib/utils';
import type { LucideIcon } from 'lucide-react';
type PerkIcon = LucideIcon | React.ComponentType<{ className?: string }>;
export function Perks({
perks,
className,
}: { perks: { text: string; icon: PerkIcon }[]; className?: string }) {
}: {
perks: { text: string; icon: PerkIcon }[];
className?: string;
}) {
return (
<ul className={cn('grid grid-cols-2 gap-2', className)}>
{perks.map((perk) => (
<li key={perk.text} className="text-sm text-muted-foreground">
<perk.icon className="size-4 inline-block mr-2 relative -top-px" />
<li className="text-muted-foreground text-sm" key={perk.text}>
<perk.icon className="relative -top-px mr-2 inline-block size-4" />
{perk.text}
</li>
))}

View File

@@ -1,10 +1,9 @@
'use client';
import NumberFlow from '@number-flow/react';
import { cn } from '@/lib/utils';
import { PRICING } from '@openpanel/payments/prices';
import { useState } from 'react';
import { Slider } from './ui/slider';
import { cn } from '@/lib/utils';
export function PricingSlider() {
const [index, setIndex] = useState(2);
@@ -14,15 +13,15 @@ export function PricingSlider() {
return (
<>
<Slider
value={[index]}
max={PRICING.length}
onValueChange={(value) => setIndex(value[0])}
step={1}
tooltip={
match
? `${formatNumber(match.events)} events per month`
: `More than ${formatNumber(PRICING[PRICING.length - 1].events)} events`
}
onValueChange={(value) => setIndex(value[0])}
value={[index]}
/>
{match ? (
@@ -30,7 +29,6 @@ export function PricingSlider() {
<div>
<NumberFlow
className="text-5xl"
value={match.price}
format={{
style: 'currency',
currency: 'USD',
@@ -38,13 +36,14 @@ export function PricingSlider() {
maximumFractionDigits: 1,
}}
locales={'en-US'}
value={match.price}
/>
<span className="text-sm text-muted-foreground ml-2">/ month</span>
<span className="ml-2 text-muted-foreground text-sm">/ month</span>
</div>
<span
className={cn(
'text-sm text-muted-foreground italic opacity-100',
match.price === 0 && 'opacity-0',
'text-muted-foreground text-sm italic opacity-100',
match.price === 0 && 'opacity-0'
)}
>
+ VAT if applicable

View File

@@ -20,8 +20,7 @@ export function ScrollTracker() {
const scrollTop = window.scrollY;
const docHeight =
document.documentElement.scrollHeight - window.innerHeight;
const percent =
docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
const percent = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
if (percent >= 50) {
hasFired.current = true;

View File

@@ -11,7 +11,7 @@ export function Section({
id?: string;
}) {
return (
<section id={id} className={cn('my-32 col', className)} {...props}>
<section className={cn('col my-32', className)} id={id} {...props}>
{children}
</section>
);
@@ -47,7 +47,7 @@ export function SectionHeader({
align === 'center'
? 'center-center text-center'
: 'items-start text-left',
className,
className
)}
>
{label && <SectionLabel>{label}</SectionLabel>}
@@ -55,7 +55,7 @@ export function SectionHeader({
{title}
</Heading>
{description && (
<p className={cn('text-muted-foreground max-w-3xl')}>{description}</p>
<p className={cn('max-w-3xl text-muted-foreground')}>{description}</p>
)}
</div>
);
@@ -71,8 +71,8 @@ export function SectionLabel({
return (
<span
className={cn(
'text-xs uppercase tracking-wider text-muted-foreground font-medium',
className,
'font-medium text-muted-foreground text-xs uppercase tracking-wider',
className
)}
>
{children}

View File

@@ -1,5 +1,3 @@
import { useMemo } from 'react';
interface SimpleChartProps {
width?: number;
height?: number;
@@ -18,7 +16,9 @@ export function SimpleChart({
className,
}: SimpleChartProps) {
// Skip if no points
if (!points.length) return null;
if (!points.length) {
return null;
}
// Calculate scaling factors
const maxValue = Math.max(...points);
@@ -45,8 +45,8 @@ export function SimpleChart({
return (
<svg
viewBox={`0 0 ${width} ${height}`}
className={`w-full ${className ?? ''}`}
viewBox={`0 0 ${width} ${height}`}
>
<defs>
<linearGradient id={gradientId} x1="0" x2="0" y1="0" y2="1">

View File

@@ -1,20 +1,20 @@
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
import { type VariantProps, cva } from 'class-variance-authority';
const tagVariants = cva(
'shadow-sm px-4 gap-2 center-center border self-auto text-xs rounded-full h-7',
'center-center h-7 gap-2 self-auto rounded-full border px-4 text-xs shadow-sm',
{
variants: {
variant: {
light:
'bg-background-light dark:bg-background-dark text-muted-foreground',
dark: 'bg-foreground-light dark:bg-foreground-dark text-muted border-background/10 shadow-background/5',
'bg-background-light text-muted-foreground dark:bg-background-dark',
dark: 'border-background/10 bg-foreground-light text-muted shadow-background/5 dark:bg-foreground-dark',
},
},
defaultVariants: {
variant: 'light',
},
},
}
);
interface TagProps

View File

@@ -10,20 +10,20 @@ interface Props {
export const Toc: React.FC<Props> = ({ toc }) => {
return (
<FeatureCardContainer className="gap-2">
<span className="text-lg font-semibold">Table of contents</span>
<span className="font-semibold text-lg">Table of contents</span>
<ul>
{toc.map((item) => (
<li
key={item.url}
className="py-1"
key={item.url}
style={{ marginLeft: `${(item.depth - 2) * (4 * 4)}px` }}
>
<Link
className="row group/toc-item items-center gap-2 hover:underline"
href={item.url}
className="hover:underline row gap-2 items-center group/toc-item"
title={item.title?.toString() ?? ''}
>
<ArrowRightIcon className="shrink-0 w-4 h-4 opacity-30 group-hover/toc-item:opacity-100 transition-opacity" />
<ArrowRightIcon className="h-4 w-4 shrink-0 opacity-30 transition-opacity group-hover/toc-item:opacity-100" />
<span className="truncate text-sm">{item.title}</span>
</Link>
</li>

View File

@@ -1,6 +1,5 @@
import {
BadgeIcon,
CheckCheckIcon,
CheckIcon,
HeartIcon,
MessageCircleIcon,
@@ -36,7 +35,7 @@ export function TwitterCard({
if (Array.isArray(content) && typeof content[0] === 'string') {
return content.map((line) => (
<p key={line} className="text-sm">
<p className="text-sm" key={line}>
{line}
</p>
));
@@ -46,11 +45,11 @@ export function TwitterCard({
};
return (
<div className="border rounded-3xl p-8 col gap-4 bg-background-light">
<div className="col gap-4 rounded-3xl border bg-background-light p-8">
<div className="row gap-4">
<div className="size-12 rounded-full bg-muted overflow-hidden shrink-0">
<div className="size-12 shrink-0 overflow-hidden rounded-full bg-muted">
{avatarUrl && (
<Image src={avatarUrl} alt={name} width={48} height={48} />
<Image alt={name} height={48} src={avatarUrl} width={48} />
)}
</div>
<div className="col gap-4">
@@ -58,9 +57,9 @@ export function TwitterCard({
<div className="">
<span className="font-medium">{name}</span>
{verified && (
<div className="relative inline-block top-0.5 ml-1">
<div className="relative top-0.5 ml-1 inline-block">
<BadgeIcon className="size-4 fill-[#1D9BF0] text-[#1D9BF0]" />
<div className="absolute inset-0 center-center">
<div className="center-center absolute inset-0">
<CheckIcon className="size-2 text-white" strokeWidth={3} />
</div>
</div>
@@ -73,15 +72,15 @@ export function TwitterCard({
{renderContent()}
<div className="row gap-4 text-muted-foreground text-sm">
<div className="row gap-2">
<MessageCircleIcon className="transition-all size-4 fill-background hover:fill-blue-500 hover:text-blue-500" />
<MessageCircleIcon className="size-4 fill-background transition-all hover:fill-blue-500 hover:text-blue-500" />
{/* <span>{replies}</span> */}
</div>
<div className="row gap-2">
<RefreshCwIcon className="transition-all size-4 fill-background hover:text-blue-500" />
<RefreshCwIcon className="size-4 fill-background transition-all hover:text-blue-500" />
{/* <span>{retweets}</span> */}
</div>
<div className="row gap-2">
<HeartIcon className="transition-all size-4 fill-background hover:fill-rose-500 hover:text-rose-500" />
<HeartIcon className="size-4 fill-background transition-all hover:fill-rose-500 hover:text-rose-500" />
{/* <span>{likes}</span> */}
</div>
</div>

View File

@@ -1,9 +1,8 @@
import * as AccordionPrimitive from '@radix-ui/react-accordion';
import { ChevronDown } from 'lucide-react';
import type * as React from 'react';
import { cn } from '@/lib/utils';
import { FeatureCardBackground } from '../feature-card';
import { cn } from '@/lib/utils';
const Accordion = AccordionPrimitive.Root;
@@ -15,8 +14,8 @@ const AccordionItem = ({
ref?: React.RefObject<React.ElementRef<typeof AccordionPrimitive.Item>>;
}) => (
<AccordionPrimitive.Item
ref={ref}
className={cn('border-b last:border-b-0', className)}
ref={ref}
{...props}
/>
);
@@ -30,13 +29,13 @@ const AccordionTrigger = ({
}: React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> & {
ref?: React.RefObject<React.ElementRef<typeof AccordionPrimitive.Trigger>>;
}) => (
<AccordionPrimitive.Header className="flex not-prose">
<AccordionPrimitive.Header className="not-prose flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
'group relative overflow-hidden flex flex-1 items-center justify-between py-4 font-medium transition-all [&[data-state=open]>svg]:rotate-180 cursor-pointer',
className,
'group relative flex flex-1 cursor-pointer items-center justify-between overflow-hidden py-4 font-medium transition-all [&[data-state=open]>svg]:rotate-180',
className
)}
ref={ref}
{...props}
>
<FeatureCardBackground />
@@ -56,14 +55,14 @@ const AccordionContent = ({
ref?: React.RefObject<React.ElementRef<typeof AccordionPrimitive.Content>>;
}) => (
<AccordionPrimitive.Content
className="overflow-hidden text-muted-foreground transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
ref={ref}
className="overflow-hidden transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down text-muted-foreground"
{...props}
>
<div
className={cn(
'pb-4 pt-0 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0',
className,
'pt-0 pb-4 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0',
className
)}
>
{children}

View File

@@ -1,11 +1,10 @@
import { Slot } from '@radix-ui/react-slot';
import { type VariantProps, cva } from 'class-variance-authority';
import { cva, type VariantProps } from 'class-variance-authority';
import type * as React from 'react';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
'hover:-translate-y-px inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg font-medium text-sm transition-all hover:-translate-y-px focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
@@ -19,7 +18,7 @@ const buttonVariants = cva(
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
naked:
'bg-transparent hover:bg-transparent ring-0 border-none !px-0 !py-0 shadow-none',
'!px-0 !py-0 border-none bg-transparent shadow-none ring-0 hover:bg-transparent',
},
size: {
default: 'h-8 px-4',
@@ -32,7 +31,7 @@ const buttonVariants = cva(
variant: 'default',
size: 'default',
},
},
}
);
export interface ButtonProps

View File

@@ -1,10 +1,9 @@
import { type VariantProps, cva } from 'class-variance-authority';
import { cva, type VariantProps } from 'class-variance-authority';
import type * as React from 'react';
import { cn } from '@/lib/utils';
const inputVariants = cva(
'flex w-full rounded-lg border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-colors',
'flex w-full rounded-lg border border-input bg-background px-3 py-2 text-sm ring-offset-background transition-colors file:border-0 file:bg-transparent file:font-medium file:text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
{
variants: {
size: {
@@ -16,7 +15,7 @@ const inputVariants = cva(
defaultVariants: {
size: 'default',
},
},
}
);
export interface InputProps
@@ -28,9 +27,9 @@ export interface InputProps
const Input = ({ className, type, size, ref, ...props }: InputProps) => {
return (
<input
type={type}
className={cn(inputVariants({ size, className }))}
ref={ref}
type={type}
{...props}
/>
);

View File

@@ -1,8 +1,7 @@
import * as SliderPrimitive from '@radix-ui/react-slider';
import * as React from 'react';
import { cn } from '@/lib/utils';
import { Tooltip, TooltipContent, TooltipTrigger } from './tooltip';
import { cn } from '@/lib/utils';
function useMediaQuery(query: string) {
const [matches, setMatches] = React.useState(false);
@@ -31,28 +30,28 @@ const Slider = ({
return (
<>
{isMobile && (
<div className="text-sm text-muted-foreground mb-4">{tooltip}</div>
<div className="mb-4 text-muted-foreground text-sm">{tooltip}</div>
)}
<SliderPrimitive.Root
ref={ref}
className={cn(
'relative flex w-full touch-none select-none items-center',
className,
className
)}
ref={ref}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-white/10">
<SliderPrimitive.Range className="absolute h-full bg-white/90" />
</SliderPrimitive.Track>
{tooltip && !isMobile ? (
<Tooltip open disableHoverableContent>
<Tooltip disableHoverableContent open>
<TooltipTrigger asChild>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-white bg-black ring-offset-black transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/50 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</TooltipTrigger>
<TooltipContent
className="rounded-full border-white/30 bg-black py-1 text-white/70 text-xs"
side="top"
sideOffset={10}
className="rounded-full bg-black text-white/70 py-1 text-xs border-white/30"
>
{tooltip}
</TooltipContent>

View File

@@ -1,6 +1,5 @@
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import type * as React from 'react';
import { cn } from '@/lib/utils';
const TooltipProvider = TooltipPrimitive.Provider;
@@ -20,12 +19,12 @@ const TooltipContent = ({
ref?: React.RefObject<React.ElementRef<typeof TooltipPrimitive.Content>>;
}) => (
<TooltipPrimitive.Content
className={cn(
'fade-in-0 zoom-in-95 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 animate-in overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-popover-foreground text-sm shadow-md data-[state=closed]:animate-out',
className
)}
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
{...props}
/>
);

View File

@@ -1,6 +1,6 @@
import { cn } from '@/lib/utils';
import Image from 'next/image';
import { FeatureCardContainer } from './feature-card';
import { cn } from '@/lib/utils';
interface WindowImageProps {
src?: string;
@@ -24,20 +24,20 @@ export function WindowImage({
const darkSrc = srcDark || src;
const lightSrc = srcLight || src;
if (!darkSrc || !lightSrc) {
if (!(darkSrc && lightSrc)) {
throw new Error(
'WindowImage requires either src or both srcDark and srcLight',
'WindowImage requires either src or both srcDark and srcLight'
);
}
return (
<FeatureCardContainer
className={cn([
'overflow-hidden rounded-lg border border-border bg-foreground/10 shadow-lg/5 relative z-10 [@media(min-width:1100px)]:-mx-16 p-4 md:p-16',
'relative z-10 overflow-hidden rounded-lg border border-border bg-foreground/10 p-4 shadow-lg/5 md:p-16 [@media(min-width:1100px)]:-mx-16',
className,
])}
>
<div className="rounded-lg overflow-hidden p-2 bg-card/80 border col gap-2 relative">
<div className="col relative gap-2 overflow-hidden rounded-lg border bg-card/80 p-2">
{/* Window controls */}
<div className="flex items-center gap-2">
<div className="flex gap-1.5">
@@ -46,25 +46,25 @@ export function WindowImage({
<div className="size-2 rounded-full bg-green-500" />
</div>
</div>
<div className="relative w-full border rounded-md overflow-hidden">
<div className="relative w-full overflow-hidden rounded-md border">
<Image
src={darkSrc}
alt={alt}
width={1200}
className="hidden h-auto w-full dark:block"
height={800}
className="hidden dark:block w-full h-auto"
src={darkSrc}
width={1200}
/>
<Image
src={lightSrc}
alt={alt}
width={1200}
className="h-auto w-full dark:hidden"
height={800}
className="dark:hidden w-full h-auto"
src={lightSrc}
width={1200}
/>
</div>
</div>
{caption && (
<figcaption className="text-center text-sm text-muted-foreground max-w-lg mx-auto">
<figcaption className="mx-auto max-w-lg text-center text-muted-foreground text-sm">
{caption}
</figcaption>
)}