continue re-design

This commit is contained in:
Developers Digest
2025-09-05 13:06:17 -04:00
parent b96d048dbd
commit 836b085f75
270 changed files with 32269 additions and 5182 deletions
+67
View File
@@ -0,0 +1,67 @@
"use client";
import React from "react";
import { motion } from "framer-motion";
import CurvyRect from "@/components/shared/layout/curvy-rect";
import { cn } from "@/utils/cn";
import {
Dialog,
DialogTrigger,
DialogPortal,
DialogOverlay,
DialogClose,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
DialogContent as ShadDialogContent,
} from "@/components/ui/shadcn/dialog";
type AppDialogContentProps = React.ComponentPropsWithoutRef<
typeof ShadDialogContent
> & {
withCurvyRect?: boolean;
bodyClassName?: string;
};
export function AppDialogContent({
className,
children,
withCurvyRect = true,
bodyClassName,
...props
}: AppDialogContentProps) {
return (
<ShadDialogContent
className={cn(
"sm:rounded-16 p-0 border border-border-faint bg-white relative overflow-hidden",
className,
)}
{...props}
>
{withCurvyRect && (
<CurvyRect className="absolute inset-0 pointer-events-none" allSides />
)}
<motion.div
initial={{ opacity: 0, scale: 0.985, y: 24 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 24, mass: 0.9 }}
className={cn("relative p-16 pb-12", bodyClassName)}
>
{children}
</motion.div>
</ShadDialogContent>
);
}
export {
Dialog,
DialogTrigger,
DialogPortal,
DialogOverlay,
DialogClose,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};
+29
View File
@@ -0,0 +1,29 @@
"use client";
import React from "react";
import { AnimatedDotIcon } from "@/components/shared/animated-dot-icon";
interface AsciiDotLoaderProps {
size?: number;
animated?: boolean;
className?: string;
pattern?: Parameters<typeof AnimatedDotIcon>[0]["pattern"];
}
// Thin wrapper to reuse the exact ASCII pixel effect used on the home hero
export default function AsciiDotLoader({
size = 20,
animated = true,
className,
pattern = "logs",
}: AsciiDotLoaderProps) {
return (
<AnimatedDotIcon
size={size}
active={animated}
alwaysHeat
className={className}
pattern={pattern}
/>
);
}
+65
View File
@@ -0,0 +1,65 @@
"use client";
import { motion } from "motion/react";
import React from "react";
interface DotGridLoaderProps {
size?: number; // pixel size of each dot
cols?: number;
rows?: number;
className?: string;
animated?: boolean;
intensityMap?: number[]; // per-dot base opacity 0..1, length cols*rows
}
export function DotGridLoader({
size = 10,
cols = 3,
rows = 3,
className,
animated = true,
intensityMap,
}: DotGridLoaderProps) {
const total = cols * rows;
const defaultMap = Array.from({ length: total }).map((_, i) => {
// Row alphas tuned to hero prompt style: top=0.4, middle=1, bottom=0.12
const row = Math.floor(i / cols);
if (row === 0) return 0.4; // top
if (row === 1) return 1.0; // middle
return 0.12; // bottom
});
const bases =
intensityMap && intensityMap.length === total ? intensityMap : defaultMap;
return (
<div
className={className}
style={{
display: "grid",
gridTemplateColumns: `repeat(${cols}, ${size}px)`,
gap: Math.max(2, Math.round(size / 3)),
}}
>
{Array.from({ length: total }).map((_, i) => {
const base = bases[i] ?? 0.8;
return (
<motion.div
key={i}
initial={{ opacity: base }}
animate={
animated ? { opacity: [base, 1, base] } : { opacity: base }
}
transition={
animated
? { duration: 1.1, repeat: Infinity, delay: i * 0.08 }
: undefined
}
style={{ width: size, height: size }}
className="rounded-[2px] bg-heat-100"
/>
);
})}
</div>
);
}
export default DotGridLoader;
+57
View File
@@ -0,0 +1,57 @@
"use client";
import React from "react";
import { cn } from "@/utils/cn";
import { AsciiExplosion } from "@/components/shared/effects/flame";
interface EmptyStateProps {
title?: string;
description?: string;
icon?: React.ReactNode;
action?: React.ReactNode;
showFlame?: boolean;
className?: string;
}
export function EmptyState({
title = "No data yet",
description,
icon,
action,
showFlame = true,
className,
}: EmptyStateProps) {
return (
<div
className={cn(
"relative flex flex-col items-center justify-center py-12 px-4 text-center",
"min-h-[300px]",
className,
)}
>
{/* Subtle flame background */}
{showFlame && (
<div className="absolute inset-0 opacity-5">
<AsciiExplosion />
</div>
)}
<div className="relative z-10 space-y-4">
{icon && (
<div className="w-12 h-12 mx-auto text-black-alpha-40">{icon}</div>
)}
<div className="space-y-2">
<h3 className="text-label-large text-black-alpha-72">{title}</h3>
{description && (
<p className="text-body-medium text-black-alpha-56 max-w-md mx-auto">
{description}
</p>
)}
</div>
{action && <div className="pt-2">{action}</div>}
</div>
</div>
);
}
+5
View File
@@ -0,0 +1,5 @@
// UI Components
export { StatCard } from "./stat-card";
export { LoadingState } from "./loading-state";
export { EmptyState } from "./empty-state";
export { default as CurvyRect } from "@/components/shared/layout/curvy-rect";
+63
View File
@@ -0,0 +1,63 @@
"use client";
import React from "react";
import { cn } from "@/utils/cn";
import { CoreFlame } from "@/components/shared/effects/flame";
interface LoadingStateProps {
message?: string;
showFlame?: boolean;
size?: "sm" | "md" | "lg";
className?: string;
}
export function LoadingState({
message = "Loading...",
showFlame = true,
size = "md",
className,
}: LoadingStateProps) {
const sizeClasses = {
sm: "min-h-[200px]",
md: "min-h-[300px]",
lg: "min-h-[400px]",
};
const spinnerSizes = {
sm: "w-6 h-6",
md: "w-8 h-8",
lg: "w-10 h-10",
};
return (
<div
className={cn(
"relative flex flex-col items-center justify-center",
sizeClasses[size],
className,
)}
>
{/* Subtle pulsing flame */}
{showFlame && (
<div className="absolute inset-0">
<CoreFlame className="opacity-10 animate-pulse" />
</div>
)}
<div className="relative z-10 space-y-4">
{/* Spinner */}
<div
className={cn(
"mx-auto rounded-full border-2 border-black-alpha-20 border-t-heat-100 animate-spin",
spinnerSizes[size],
)}
/>
{/* Message */}
{message && (
<p className="text-body-medium text-black-alpha-64">{message}</p>
)}
</div>
</div>
);
}
+257
View File
@@ -0,0 +1,257 @@
"use client";
import React, { useEffect, useRef, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { X } from "lucide-react";
import { useAtom } from "jotai";
import { cn } from "@/utils/cn";
import { isMobileSheetOpenAtom } from "@/atoms/sheets";
interface MobileSheetProps {
isOpen: boolean;
onClose: () => void;
title?: React.ReactNode;
showCloseButton?: boolean;
position?: "top" | "bottom";
spacing?: "sm" | "md" | "lg"; // sm=16px, md=20px, lg=24px from all edges
children: React.ReactNode;
className?: string;
contentHeight?: "auto" | "fill" | "full"; // 'auto' for small content, 'fill' for lists, 'full' for nearly fullscreen
contentPadding?: boolean; // Whether to add default padding to content area (default: true)
closeOnOverlayClick?: boolean; // Whether clicking the overlay should close the sheet (default: true)
}
// Hook to auto-close mobile sheets when transitioning to desktop
function useAutoCloseOnDesktop(isOpen: boolean, onClose: () => void) {
useEffect(() => {
if (!isOpen) return;
const handleResize = () => {
// Close immediately if screen becomes larger than mobile breakpoint (768px for lg:)
if (window.innerWidth >= 1024) {
onClose();
}
};
window.addEventListener("resize", handleResize);
// Check immediately in case we're already on desktop
handleResize();
return () => {
window.removeEventListener("resize", handleResize);
};
}, [isOpen, onClose]);
}
export function MobileSheet({
isOpen,
onClose,
title,
showCloseButton = false,
position = "bottom",
spacing = "sm",
children,
className,
contentHeight = "auto",
contentPadding = true,
closeOnOverlayClick = true,
}: MobileSheetProps) {
const [, setIsMobileSheetOpen] = useAtom(isMobileSheetOpenAtom);
const scrollRef = useRef<HTMLDivElement>(null);
const [showTopGradient, setShowTopGradient] = useState(false);
const [showBottomGradient, setShowBottomGradient] = useState(false);
// Handle global mobile sheet state changes (always reflect actual open state)
useEffect(() => {
setIsMobileSheetOpen(isOpen);
return () => setIsMobileSheetOpen(false);
}, [isOpen, setIsMobileSheetOpen]);
// Lock background scroll when sheet is open, restore when closed
useEffect(() => {
if (typeof document === "undefined") return;
if (isOpen) {
const previousOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = previousOverflow;
};
} else {
document.body.style.overflow = "";
}
}, [isOpen]);
// Auto-close when transitioning to desktop
useAutoCloseOnDesktop(isOpen, onClose);
// Check initial scroll state when sheet opens
useEffect(() => {
if (isOpen && scrollRef.current) {
// Small delay to ensure content is rendered
setTimeout(() => {
const target = scrollRef.current;
if (target) {
const isAtTop = target.scrollTop <= 5;
const isAtBottom =
target.scrollHeight - target.scrollTop - target.clientHeight <= 5;
const hasScroll = target.scrollHeight > target.clientHeight;
setShowTopGradient(hasScroll && !isAtTop);
setShowBottomGradient(hasScroll && !isAtBottom);
}
}, 50);
}
}, [isOpen]);
// Enhanced close handler
const handleClose = () => {
// Re-enable page scroll after closing sheet
document.body.style.overflow = "";
onClose();
};
// Define spacing values
const getSpacingClass = () => {
const horizontalSpacing =
spacing === "sm"
? "left-16 right-16"
: spacing === "md"
? "left-20 right-20"
: "left-24 right-24";
if (contentHeight === "full") {
if (spacing === "sm") return `${horizontalSpacing} top-16 bottom-16`;
if (spacing === "md") return `${horizontalSpacing} top-20 bottom-20`;
return `${horizontalSpacing} top-24 bottom-24`;
}
const verticalSpacing =
position === "top"
? spacing === "sm"
? "top-16"
: spacing === "md"
? "top-20"
: "top-24"
: spacing === "sm"
? "bottom-16"
: spacing === "md"
? "bottom-20"
: "bottom-24";
return `${horizontalSpacing} ${verticalSpacing}`;
};
return (
<AnimatePresence>
{isOpen && (
<div
className="fixed inset-0 z-[120] lg:hidden"
onClick={() => {
if (closeOnOverlayClick) {
handleClose();
}
}}
>
{/* Sheet content - positioned from top or bottom */}
<motion.div
initial={{
opacity: 0,
y: position === "top" ? -50 : 50,
}}
animate={{ opacity: 1, y: 0 }}
exit={{
opacity: 0,
y: position === "top" ? -50 : 50,
}}
transition={{
type: "spring",
stiffness: 300,
damping: 30,
mass: 0.8,
}}
className={cn("absolute", getSpacingClass())}
onClick={(e) => e.stopPropagation()}
>
<div
className={cn(
"bg-background-base border border-border-faint rounded-12 shadow-2xl overflow-hidden",
className,
)}
>
{/* Header */}
{(title || showCloseButton) && (
<div className="px-24 py-16 border-b border-border-faint flex items-center justify-between">
{title && (
<h2 className="text-label-large text-accent-black">
{title}
</h2>
)}
{showCloseButton && (
<button
onClick={handleClose}
className={cn(
"h-32 w-32 flex items-center justify-center transition-all rounded-6 border border-border-faint",
"bg-black-alpha-4 hover:bg-black-alpha-6 active:scale-[0.98]",
"text-black-alpha-64 hover:text-accent-black",
)}
aria-label="Close"
>
<X className="w-16 h-16" />
</button>
)}
</div>
)}
{/* Content with gradient overlays */}
<div className="relative group">
<div
ref={scrollRef}
className={cn(
contentHeight === "full"
? "h-full"
: contentHeight === "fill"
? "h-[65vh]"
: "max-h-[60vh]",
"overflow-y-auto scrollbar-hide",
contentPadding && "p-16",
)}
onScroll={(e) => {
const target = e.currentTarget;
const isAtTop = target.scrollTop <= 5;
const isAtBottom =
target.scrollHeight -
target.scrollTop -
target.clientHeight <=
5;
const hasScroll = target.scrollHeight > target.clientHeight;
// Update gradient visibility states
setShowTopGradient(hasScroll && !isAtTop);
setShowBottomGradient(hasScroll && !isAtBottom);
}}
>
{children}
</div>
{/* Animated fade gradients */}
<motion.div
className="absolute top-0 left-0 right-0 h-24 bg-gradient-to-b from-white to-transparent pointer-events-none z-10"
initial={{ opacity: 0 }}
animate={{ opacity: showTopGradient ? 1 : 0 }}
transition={{ duration: 0.2, ease: "easeInOut" }}
/>
<motion.div
className="absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-t from-white to-transparent pointer-events-none z-10"
initial={{ opacity: 0 }}
animate={{ opacity: showBottomGradient ? 1 : 0 }}
transition={{ duration: 0.2, ease: "easeInOut" }}
/>
</div>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
);
}
+98
View File
@@ -0,0 +1,98 @@
"use client";
import React from "react";
import { cn } from "@/utils/cn";
import { TrendingUp, TrendingDown } from "lucide-react";
interface StatCardProps {
label: string;
value: string | number;
change?: number; // Percentage change
suffix?: string;
prefix?: string;
icon?: React.ReactNode;
trend?: "up" | "down" | "neutral";
className?: string;
}
export function StatCard({
label,
value,
change,
suffix,
prefix,
icon,
trend,
className,
}: StatCardProps) {
const trendColor =
trend === "up"
? "text-green-600"
: trend === "down"
? "text-red-600"
: "text-black-alpha-64";
const trendBg =
trend === "up"
? "bg-green-50"
: trend === "down"
? "bg-red-50"
: "bg-black-alpha-4";
return (
<div
className={cn(
"p-5 lg:p-6 border border-border-faint rounded-12 bg-white",
"hover:border-black-alpha-16 transition-colors",
className,
)}
>
<div className="flex items-start justify-between mb-3">
<p className="text-label-small text-black-alpha-64">{label}</p>
{icon && <div className="w-5 h-5 text-black-alpha-40">{icon}</div>}
</div>
<div className="space-y-2">
<div className="flex items-baseline gap-1">
{prefix && (
<span className="text-body-large text-black-alpha-64">
{prefix}
</span>
)}
<span className="text-h3 font-semibold text-accent-black">
{value}
</span>
{suffix && (
<span className="text-body-large text-black-alpha-64">
{suffix}
</span>
)}
</div>
{(change !== undefined || trend) && (
<div className="flex items-center gap-2">
{trend && (
<div
className={cn(
"w-6 h-6 rounded-6 flex items-center justify-center",
trendBg,
)}
>
{trend === "up" ? (
<TrendingUp className={cn("w-3.5 h-3.5", trendColor)} />
) : trend === "down" ? (
<TrendingDown className={cn("w-3.5 h-3.5", trendColor)} />
) : null}
</div>
)}
{change !== undefined && (
<span className={cn("text-body-small ", trendColor)}>
{change > 0 && "+"}
{change}%
</span>
)}
</div>
)}
</div>
</div>
);
}