@zmzai/theme 0.1.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.
Files changed (129) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +76 -0
  3. package/package.json +91 -0
  4. package/src/brand/index.ts +3 -0
  5. package/src/brand/logo/Logo.tsx +46 -0
  6. package/src/brand/logo/favicon.png +0 -0
  7. package/src/brand/logo/index.ts +2 -0
  8. package/src/brand/logo/zmzai-logo.png +0 -0
  9. package/src/brand/wordmark/Wordmark.tsx +48 -0
  10. package/src/brand/wordmark/index.ts +2 -0
  11. package/src/components/aceternity-sidebar/AceternitySidebar.tsx +133 -0
  12. package/src/components/aceternity-sidebar/index.ts +2 -0
  13. package/src/components/animated-tabs/AnimatedTabs.tsx +60 -0
  14. package/src/components/animated-tabs/index.ts +2 -0
  15. package/src/components/animated-tooltip/AnimatedTooltip.tsx +93 -0
  16. package/src/components/animated-tooltip/index.ts +2 -0
  17. package/src/components/avatar/Avatar.tsx +59 -0
  18. package/src/components/avatar/index.ts +2 -0
  19. package/src/components/badge/Badge.tsx +45 -0
  20. package/src/components/badge/index.ts +2 -0
  21. package/src/components/bento-grid/BentoGrid.tsx +85 -0
  22. package/src/components/bento-grid/index.ts +2 -0
  23. package/src/components/button/Button.tsx +66 -0
  24. package/src/components/button/button.styles.ts +41 -0
  25. package/src/components/button/index.ts +4 -0
  26. package/src/components/card/Card.tsx +87 -0
  27. package/src/components/card/card.styles.ts +35 -0
  28. package/src/components/card/index.ts +4 -0
  29. package/src/components/card-3d/Card3D.tsx +84 -0
  30. package/src/components/card-3d/index.ts +2 -0
  31. package/src/components/card-hover-effect/CardHoverEffect.tsx +83 -0
  32. package/src/components/card-hover-effect/index.ts +2 -0
  33. package/src/components/card-spotlight/CardSpotlight.tsx +63 -0
  34. package/src/components/card-spotlight/index.ts +2 -0
  35. package/src/components/carousel/Carousel.tsx +79 -0
  36. package/src/components/carousel/index.ts +2 -0
  37. package/src/components/code-block/CodeBlock.tsx +153 -0
  38. package/src/components/code-block/index.ts +2 -0
  39. package/src/components/comet-card/CometCard.tsx +106 -0
  40. package/src/components/comet-card/index.ts +2 -0
  41. package/src/components/compare/Compare.tsx +128 -0
  42. package/src/components/compare/index.ts +2 -0
  43. package/src/components/dialog/Dialog.tsx +79 -0
  44. package/src/components/dialog/index.ts +10 -0
  45. package/src/components/dropdown-menu/DropdownMenu.tsx +76 -0
  46. package/src/components/dropdown-menu/index.ts +8 -0
  47. package/src/components/empty-state/EmptyState.tsx +78 -0
  48. package/src/components/empty-state/index.ts +2 -0
  49. package/src/components/encrypt-text/EncryptText.tsx +102 -0
  50. package/src/components/encrypt-text/index.ts +2 -0
  51. package/src/components/evervault-card/EvervaultCard.tsx +114 -0
  52. package/src/components/evervault-card/index.ts +2 -0
  53. package/src/components/expandable-card/ExpandableCard.tsx +91 -0
  54. package/src/components/expandable-card/index.ts +2 -0
  55. package/src/components/file-upload/FileUpload.tsx +171 -0
  56. package/src/components/file-upload/index.ts +2 -0
  57. package/src/components/flip-words/FlipWords.tsx +70 -0
  58. package/src/components/flip-words/index.ts +2 -0
  59. package/src/components/floating-navbar/FloatingNavbar.tsx +82 -0
  60. package/src/components/floating-navbar/index.ts +2 -0
  61. package/src/components/focus-cards/FocusCards.tsx +93 -0
  62. package/src/components/focus-cards/index.ts +2 -0
  63. package/src/components/glare-card/GlareCard.tsx +66 -0
  64. package/src/components/glare-card/index.ts +2 -0
  65. package/src/components/glowing-effect/GlowingEffect.tsx +68 -0
  66. package/src/components/glowing-effect/index.ts +2 -0
  67. package/src/components/hover-border-gradient/HoverBorderGradient.tsx +80 -0
  68. package/src/components/hover-border-gradient/index.ts +2 -0
  69. package/src/components/index.ts +71 -0
  70. package/src/components/infinite-moving-cards/InfiniteMovingCards.tsx +84 -0
  71. package/src/components/infinite-moving-cards/index.ts +2 -0
  72. package/src/components/input/Input.tsx +35 -0
  73. package/src/components/input/index.ts +4 -0
  74. package/src/components/input/input.styles.ts +34 -0
  75. package/src/components/keyboard/Keyboard.tsx +38 -0
  76. package/src/components/keyboard/index.ts +2 -0
  77. package/src/components/layout-grid/LayoutGrid.tsx +121 -0
  78. package/src/components/layout-grid/index.ts +2 -0
  79. package/src/components/link-preview/LinkPreview.tsx +121 -0
  80. package/src/components/link-preview/index.ts +2 -0
  81. package/src/components/loader/Loader.tsx +130 -0
  82. package/src/components/loader/index.ts +2 -0
  83. package/src/components/magnetic-button/MagneticButton.tsx +79 -0
  84. package/src/components/magnetic-button/index.ts +2 -0
  85. package/src/components/moving-border/MovingBorder.tsx +72 -0
  86. package/src/components/moving-border/index.ts +2 -0
  87. package/src/components/multi-step-loader/MultiStepLoader.tsx +95 -0
  88. package/src/components/multi-step-loader/index.ts +2 -0
  89. package/src/components/notch/Notch.tsx +47 -0
  90. package/src/components/notch/index.ts +2 -0
  91. package/src/components/select/Select.tsx +100 -0
  92. package/src/components/select/index.ts +10 -0
  93. package/src/components/skeleton/Skeleton.tsx +34 -0
  94. package/src/components/skeleton/index.ts +2 -0
  95. package/src/components/stateful-button/StatefulButton.tsx +115 -0
  96. package/src/components/stateful-button/index.ts +2 -0
  97. package/src/components/sticky-banner/StickyBanner.tsx +74 -0
  98. package/src/components/sticky-banner/index.ts +2 -0
  99. package/src/components/sticky-scroll-reveal/StickyScrollReveal.tsx +141 -0
  100. package/src/components/sticky-scroll-reveal/index.ts +2 -0
  101. package/src/components/terminal/Terminal.tsx +55 -0
  102. package/src/components/terminal/index.ts +2 -0
  103. package/src/components/text-generate/TextGenerate.tsx +59 -0
  104. package/src/components/text-generate/index.ts +2 -0
  105. package/src/components/text-hover-effect/TextHoverEffect.tsx +90 -0
  106. package/src/components/text-hover-effect/index.ts +2 -0
  107. package/src/components/textarea/Textarea.tsx +39 -0
  108. package/src/components/textarea/index.ts +2 -0
  109. package/src/components/timeline/Timeline.tsx +106 -0
  110. package/src/components/timeline/index.ts +3 -0
  111. package/src/components/tooltip/Tooltip.tsx +51 -0
  112. package/src/components/tooltip/index.ts +2 -0
  113. package/src/components/tooltip-card/TooltipCard.tsx +130 -0
  114. package/src/components/tooltip-card/index.ts +2 -0
  115. package/src/components/tracing-beam/TracingBeam.tsx +110 -0
  116. package/src/components/tracing-beam/index.ts +2 -0
  117. package/src/components/typewriter/Typewriter.tsx +69 -0
  118. package/src/components/typewriter/index.ts +2 -0
  119. package/src/components/vanish-input/VanishInput.tsx +170 -0
  120. package/src/components/vanish-input/index.ts +2 -0
  121. package/src/components/wobble-card/WobbleCard.tsx +77 -0
  122. package/src/components/wobble-card/index.ts +2 -0
  123. package/src/index.ts +21 -0
  124. package/src/tokens/fonts.css +42 -0
  125. package/src/tokens/theme.css +120 -0
  126. package/src/utils/cn.ts +13 -0
  127. package/src/utils/index.ts +2 -0
  128. package/src/utils/motion.ts +47 -0
  129. package/src/vite-env.d.ts +6 -0
@@ -0,0 +1,93 @@
1
+ "use client";
2
+
3
+ import {
4
+ useRef,
5
+ useState,
6
+ type ReactNode,
7
+ type MouseEvent,
8
+ } from "react";
9
+ import {
10
+ motion,
11
+ useMotionValue,
12
+ useSpring,
13
+ AnimatePresence,
14
+ } from "framer-motion";
15
+ import { cn } from "../../utils/cn";
16
+
17
+ export interface AnimatedTooltipProps {
18
+ /** Tooltip body content */
19
+ content: ReactNode;
20
+ /** The trigger element */
21
+ children: ReactNode;
22
+ className?: string;
23
+ }
24
+
25
+ /**
26
+ * AnimatedTooltip — a custom tooltip that slides in following the cursor.
27
+ *
28
+ * Unlike the Radix-based tooltip, this is built purely with framer-motion:
29
+ * on hover, a small card slides in from the side with spring physics and
30
+ * tracks the mouse X position. Based on Aceternity's Animated Tooltip,
31
+ * adapted to the zmzai monochrome palette.
32
+ *
33
+ * @example
34
+ * <AnimatedTooltip content="复制到剪贴板">
35
+ * <button>复制</button>
36
+ * </AnimatedTooltip>
37
+ */
38
+ export function AnimatedTooltip({
39
+ content,
40
+ children,
41
+ className,
42
+ }: AnimatedTooltipProps) {
43
+ const ref = useRef<HTMLDivElement>(null);
44
+ const [hovered, setHovered] = useState(false);
45
+
46
+ const mouseX = useMotionValue(0);
47
+ const mouseY = useMotionValue(0);
48
+
49
+ const springConfig = { stiffness: 200, damping: 22 };
50
+ const x = useSpring(mouseX, springConfig);
51
+ const y = useSpring(mouseY, springConfig);
52
+
53
+ const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
54
+ const rect = e.currentTarget.getBoundingClientRect();
55
+ mouseX.set(e.clientX - rect.left);
56
+ mouseY.set(e.clientY - rect.top);
57
+ };
58
+
59
+ return (
60
+ <div
61
+ ref={ref}
62
+ onMouseEnter={() => setHovered(true)}
63
+ onMouseLeave={() => setHovered(false)}
64
+ onMouseMove={handleMouseMove}
65
+ className={cn("relative inline-flex", className)}
66
+ >
67
+ {children}
68
+
69
+ <AnimatePresence>
70
+ {hovered && (
71
+ <motion.div
72
+ initial={{ opacity: 0, scale: 0.8, x: -10 }}
73
+ animate={{ opacity: 1, scale: 1, x: 0 }}
74
+ exit={{ opacity: 0, scale: 0.8, x: -10 }}
75
+ transition={{ type: "spring", stiffness: 350, damping: 24 }}
76
+ style={{
77
+ position: "absolute",
78
+ top: 0,
79
+ left: 0,
80
+ x,
81
+ // position above the cursor, centered horizontally
82
+ translateY: "-120%",
83
+ translateX: "-50%",
84
+ }}
85
+ className="pointer-events-none z-50 whitespace-nowrap rounded-lg border border-line bg-bg px-3 py-1.5 text-xs font-medium text-ink shadow-md"
86
+ >
87
+ {content}
88
+ </motion.div>
89
+ )}
90
+ </AnimatePresence>
91
+ </div>
92
+ );
93
+ }
@@ -0,0 +1,2 @@
1
+ export { AnimatedTooltip } from "./AnimatedTooltip";
2
+ export type { AnimatedTooltipProps } from "./AnimatedTooltip";
@@ -0,0 +1,59 @@
1
+ "use client";
2
+
3
+ import { useState, type HTMLAttributes } from "react";
4
+ import { cn } from "../../utils/cn";
5
+
6
+ export interface AvatarProps extends HTMLAttributes<HTMLDivElement> {
7
+ /** Image source URL */
8
+ src?: string;
9
+ /** Alt text / fallback initials */
10
+ alt?: string;
11
+ /** Size in px (default 32) */
12
+ size?: number;
13
+ /** Shape (default circle) */
14
+ shape?: "circle" | "square";
15
+ }
16
+
17
+ /**
18
+ * Avatar — user/agent avatar with image + initials fallback.
19
+ *
20
+ * If `src` fails to load, shows initials from `alt` on a neutral background.
21
+ *
22
+ * @example
23
+ * <Avatar src="/me.png" alt="牧之" size={32} />
24
+ * <Avatar alt="AI" size={28} shape="square" />
25
+ */
26
+ export function Avatar({
27
+ src,
28
+ alt = "",
29
+ size = 32,
30
+ shape = "circle",
31
+ className,
32
+ ...props
33
+ }: AvatarProps) {
34
+ const [error, setError] = useState(false);
35
+ const initials = alt.slice(0, 2).toUpperCase();
36
+
37
+ return (
38
+ <div
39
+ className={cn(
40
+ "flex flex-shrink-0 items-center justify-center overflow-hidden bg-surface-2 text-ink-2 font-semibold",
41
+ shape === "circle" ? "rounded-full" : "rounded-lg",
42
+ className
43
+ )}
44
+ style={{ width: size, height: size, fontSize: size * 0.38 }}
45
+ {...props}
46
+ >
47
+ {src && !error ? (
48
+ <img
49
+ src={src}
50
+ alt={alt}
51
+ className="h-full w-full object-cover"
52
+ onError={() => setError(true)}
53
+ />
54
+ ) : (
55
+ <span>{initials}</span>
56
+ )}
57
+ </div>
58
+ );
59
+ }
@@ -0,0 +1,2 @@
1
+ export { Avatar } from "./Avatar";
2
+ export type { AvatarProps } from "./Avatar";
@@ -0,0 +1,45 @@
1
+ "use client";
2
+
3
+ import { type HTMLAttributes } from "react";
4
+ import { cva } from "class-variance-authority";
5
+ import { cn } from "../../utils/cn";
6
+
7
+ export const badgeVariants = cva(
8
+ "inline-flex items-center gap-1.5 rounded-full font-medium text-xs whitespace-nowrap",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ solid: "bg-ink text-white",
13
+ outline: "border border-line text-ink-2",
14
+ success: "border border-success/30 bg-success/10 text-success",
15
+ warning: "border border-warning/30 bg-warning/10 text-warning",
16
+ danger: "border border-danger/30 bg-danger/10 text-danger",
17
+ accent: "border border-accent/30 bg-accent/10 text-accent",
18
+ },
19
+ size: {
20
+ sm: "px-2 py-0.5 text-[10px]",
21
+ md: "px-2.5 py-0.5 text-xs",
22
+ },
23
+ },
24
+ defaultVariants: { variant: "outline", size: "md" },
25
+ }
26
+ );
27
+
28
+ export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
29
+ variant?: "solid" | "outline" | "success" | "warning" | "danger" | "accent";
30
+ size?: "sm" | "md";
31
+ }
32
+
33
+ /**
34
+ * Badge — small status/label indicator.
35
+ *
36
+ * @example
37
+ * <Badge variant="solid">活跃</Badge>
38
+ * <Badge variant="success">● 完成</Badge>
39
+ * <Badge variant="accent">需要审批</Badge>
40
+ */
41
+ export function Badge({ className, variant, size, ...props }: BadgeProps) {
42
+ return (
43
+ <span className={cn(badgeVariants({ variant, size }), className)} {...props} />
44
+ );
45
+ }
@@ -0,0 +1,2 @@
1
+ export { Badge, badgeVariants } from "./Badge";
2
+ export type { BadgeProps } from "./Badge";
@@ -0,0 +1,85 @@
1
+ "use client";
2
+
3
+ import { type ReactNode } from "react";
4
+ import { motion } from "framer-motion";
5
+ import { cn } from "../../utils/cn";
6
+
7
+ export type BentoSpan = "1x1" | "2x1" | "1x2" | "2x2";
8
+
9
+ export interface BentoGridItem {
10
+ title: string;
11
+ description: string;
12
+ icon?: ReactNode;
13
+ /** Custom className to override/extend the card styling */
14
+ className?: string;
15
+ /** Grid span control. Default "1x1". Format: "cols x rows" */
16
+ span?: BentoSpan;
17
+ }
18
+
19
+ export interface BentoGridProps {
20
+ items: BentoGridItem[];
21
+ className?: string;
22
+ }
23
+
24
+ /** Map span tokens to Tailwind grid-column / grid-row utilities */
25
+ const spanClassMap: Record<BentoSpan, string> = {
26
+ "1x1": "col-span-1 row-span-1",
27
+ "2x1": "col-span-2 row-span-1",
28
+ "1x2": "col-span-1 row-span-2",
29
+ "2x2": "col-span-2 row-span-2",
30
+ };
31
+
32
+ /**
33
+ * BentoGrid — asymmetric grid layout (like Apple's bento).
34
+ *
35
+ * CSS grid with 2 columns + auto-rows. Each item's `span` controls its
36
+ * footprint. Items lift on hover via framer-motion.
37
+ *
38
+ * @example
39
+ * <BentoGrid items={[
40
+ * { title: "性能", description: "极速响应", span: "2x1" },
41
+ * { title: "安全", description: "端到端加密", span: "1x1" },
42
+ * ]} />
43
+ */
44
+ export function BentoGrid({ items, className }: BentoGridProps) {
45
+ return (
46
+ <div
47
+ className={cn(
48
+ "grid auto-rows-[minmax(140px,1fr)] grid-cols-1 gap-3 sm:grid-cols-2",
49
+ className
50
+ )}
51
+ >
52
+ {items.map((item, index) => (
53
+ <BentoCard key={index} item={item} />
54
+ ))}
55
+ </div>
56
+ );
57
+ }
58
+
59
+ function BentoCard({ item }: { item: BentoGridItem }) {
60
+ const span = item.span ?? "1x1";
61
+
62
+ return (
63
+ <motion.div
64
+ whileHover={{ y: -4 }}
65
+ transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
66
+ className={cn(
67
+ "group flex flex-col justify-between rounded-xl border border-line bg-bg p-5 shadow-sm transition-colors hover:border-line-strong",
68
+ spanClassMap[span],
69
+ item.className
70
+ )}
71
+ >
72
+ {item.icon && (
73
+ <div className="mb-3 flex h-8 w-8 items-center justify-center text-ink">
74
+ {item.icon}
75
+ </div>
76
+ )}
77
+ <div className="mt-auto">
78
+ <h3 className="text-base font-semibold text-ink">{item.title}</h3>
79
+ <p className="mt-1 text-sm leading-relaxed text-ink-2">
80
+ {item.description}
81
+ </p>
82
+ </div>
83
+ </motion.div>
84
+ );
85
+ }
@@ -0,0 +1,2 @@
1
+ export { BentoGrid } from "./BentoGrid";
2
+ export type { BentoGridProps, BentoGridItem, BentoSpan } from "./BentoGrid";
@@ -0,0 +1,66 @@
1
+ "use client";
2
+
3
+ import { forwardRef, type ButtonHTMLAttributes } from "react";
4
+ import { motion, type HTMLMotionProps } from "framer-motion";
5
+ import { cn } from "../../utils/cn";
6
+ import { buttonVariants, type ButtonVariant, type ButtonSize } from "./button.styles";
7
+
8
+ export interface ButtonProps
9
+ extends Omit<HTMLMotionProps<"button">, "ref" | "children"> {
10
+ variant?: ButtonVariant;
11
+ size?: ButtonSize;
12
+ loading?: boolean;
13
+ children?: React.ReactNode;
14
+ }
15
+
16
+ /**
17
+ * Button — primary interactive element.
18
+ *
19
+ * - Uses framer-motion for hover/active micro-interactions
20
+ * - Pill radius on all variants
21
+ * - `loading` prop shows a spinner and disables
22
+ *
23
+ * @example
24
+ * <Button variant="primary" size="md">发送</Button>
25
+ * <Button variant="secondary" loading>保存中…</Button>
26
+ */
27
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
28
+ ({ className, variant, size, loading, children, disabled, ...props }, ref) => {
29
+ return (
30
+ <motion.button
31
+ ref={ref}
32
+ whileHover={disabled || loading ? undefined : { scale: 1.03 }}
33
+ whileTap={disabled || loading ? undefined : { scale: 0.98 }}
34
+ transition={{ duration: 0.15, ease: [0.16, 1, 0.3, 1] }}
35
+ className={cn(buttonVariants({ variant, size }), className)}
36
+ disabled={disabled || loading}
37
+ {...props}
38
+ >
39
+ {loading && (
40
+ <svg
41
+ className="h-4 w-4 animate-spin"
42
+ viewBox="0 0 24 24"
43
+ fill="none"
44
+ >
45
+ <circle
46
+ className="opacity-25"
47
+ cx="12"
48
+ cy="12"
49
+ r="10"
50
+ stroke="currentColor"
51
+ strokeWidth="3"
52
+ />
53
+ <path
54
+ className="opacity-90"
55
+ fill="currentColor"
56
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
57
+ />
58
+ </svg>
59
+ )}
60
+ {children}
61
+ </motion.button>
62
+ );
63
+ }
64
+ );
65
+
66
+ Button.displayName = "Button";
@@ -0,0 +1,41 @@
1
+ import { cva } from "class-variance-authority";
2
+
3
+ /**
4
+ * Button variant styles.
5
+ *
6
+ * Design:
7
+ * - All variants use pill radius (rounded-full)
8
+ * - primary: black fill, white text, hover scale + shadow
9
+ * - secondary: transparent, border, hover fill
10
+ * - ghost: no border, subtle hover bg
11
+ */
12
+ export const buttonVariants = cva(
13
+ "inline-flex cursor-pointer items-center justify-center gap-2 rounded-full font-semibold transition-colors transition-transform disabled:pointer-events-none disabled:opacity-40 select-none",
14
+ {
15
+ variants: {
16
+ variant: {
17
+ primary:
18
+ "bg-ink text-white hover:shadow-md active:scale-[0.98]",
19
+ secondary:
20
+ "border border-line text-ink hover:border-ink hover:bg-surface-2",
21
+ ghost:
22
+ "text-ink-2 hover:bg-surface-2 hover:text-ink",
23
+ danger:
24
+ "bg-danger text-white hover:shadow-md active:scale-[0.98]",
25
+ },
26
+ size: {
27
+ sm: "h-8 px-4 text-xs",
28
+ md: "h-10 px-6 text-sm",
29
+ lg: "h-12 px-8 text-base",
30
+ icon: "h-10 w-10 p-0",
31
+ },
32
+ },
33
+ defaultVariants: {
34
+ variant: "primary",
35
+ size: "md",
36
+ },
37
+ }
38
+ );
39
+
40
+ export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
41
+ export type ButtonSize = "sm" | "md" | "lg" | "icon";
@@ -0,0 +1,4 @@
1
+ export { Button } from "./Button";
2
+ export type { ButtonProps } from "./Button";
3
+ export { buttonVariants } from "./button.styles";
4
+ export type { ButtonVariant, ButtonSize } from "./button.styles";
@@ -0,0 +1,87 @@
1
+ "use client";
2
+
3
+ import { forwardRef, type HTMLAttributes } from "react";
4
+ import { motion, type HTMLMotionProps } from "framer-motion";
5
+ import { cn } from "../../utils/cn";
6
+ import { cardVariants, type CardVariant, type CardPadding } from "./card.styles";
7
+
8
+ export interface CardProps extends Omit<HTMLMotionProps<"div">, "ref"> {
9
+ variant?: CardVariant;
10
+ padding?: CardPadding;
11
+ /** Enable framer-motion hover lift (for interactive cards) */
12
+ animatedHover?: boolean;
13
+ }
14
+
15
+ /**
16
+ * Card — surface container.
17
+ *
18
+ * - `default`: static white card with light border
19
+ * - `interactive`: hover border darkens + shadow + lift (via framer-motion if animatedHover)
20
+ * - `surface`: slightly grey background for nested panels
21
+ *
22
+ * @example
23
+ * <Card padding="lg">内容</Card>
24
+ * <Card variant="interactive" animatedHover>可点击卡片</Card>
25
+ */
26
+ export const Card = forwardRef<HTMLDivElement, CardProps>(
27
+ ({ className, variant, padding, animatedHover, children, ...props }, ref) => {
28
+ const shouldAnimate = variant === "interactive" && animatedHover;
29
+
30
+ const MotionDiv = motion.div;
31
+
32
+ return (
33
+ <MotionDiv
34
+ ref={ref}
35
+ whileHover={
36
+ shouldAnimate
37
+ ? { y: -2, transition: { duration: 0.2, ease: [0.16, 1, 0.3, 1] } }
38
+ : undefined
39
+ }
40
+ className={cn(cardVariants({ variant, padding }), className)}
41
+ {...props}
42
+ >
43
+ {children}
44
+ </MotionDiv>
45
+ );
46
+ }
47
+ );
48
+
49
+ Card.displayName = "Card";
50
+
51
+ /**
52
+ * Card.Header — top section of a card (often with title + actions).
53
+ */
54
+ export const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
55
+ ({ className, ...props }, ref) => (
56
+ <div
57
+ ref={ref}
58
+ className={cn("flex items-center justify-between border-b border-line px-4 py-3", className)}
59
+ {...props}
60
+ />
61
+ )
62
+ );
63
+ CardHeader.displayName = "CardHeader";
64
+
65
+ /**
66
+ * Card.Body — main content area.
67
+ */
68
+ export const CardBody = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
69
+ ({ className, ...props }, ref) => (
70
+ <div ref={ref} className={cn("px-4 py-3", className)} {...props} />
71
+ )
72
+ );
73
+ CardBody.displayName = "CardBody";
74
+
75
+ /**
76
+ * Card.Footer — bottom section.
77
+ */
78
+ export const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
79
+ ({ className, ...props }, ref) => (
80
+ <div
81
+ ref={ref}
82
+ className={cn("flex items-center gap-2 border-t border-line px-4 py-3", className)}
83
+ {...props}
84
+ />
85
+ )
86
+ );
87
+ CardFooter.displayName = "CardFooter";
@@ -0,0 +1,35 @@
1
+ import { cva } from "class-variance-authority";
2
+
3
+ /**
4
+ * Card variant styles.
5
+ *
6
+ * Design:
7
+ * - default: white bg, light border, no hover
8
+ * - interactive: hover lifts up + shadow + border darkens
9
+ */
10
+ export const cardVariants = cva(
11
+ "bg-bg border rounded-xl transition-colors transition-transform duration-200",
12
+ {
13
+ variants: {
14
+ variant: {
15
+ default: "border-line",
16
+ interactive:
17
+ "border-line hover:border-ink hover:-translate-y-0.5 hover:shadow-[0_8px_24px_rgba(0,0,0,0.08)] cursor-pointer",
18
+ surface: "bg-surface border-line",
19
+ },
20
+ padding: {
21
+ none: "",
22
+ sm: "p-3",
23
+ md: "p-4",
24
+ lg: "p-6",
25
+ },
26
+ },
27
+ defaultVariants: {
28
+ variant: "default",
29
+ padding: "md",
30
+ },
31
+ }
32
+ );
33
+
34
+ export type CardVariant = "default" | "interactive" | "surface";
35
+ export type CardPadding = "none" | "sm" | "md" | "lg";
@@ -0,0 +1,4 @@
1
+ export { Card, CardHeader, CardBody, CardFooter } from "./Card";
2
+ export type { CardProps } from "./Card";
3
+ export { cardVariants } from "./card.styles";
4
+ export type { CardVariant, CardPadding } from "./card.styles";
@@ -0,0 +1,84 @@
1
+ "use client";
2
+
3
+ import { useRef, type ReactNode, type MouseEvent } from "react";
4
+ import {
5
+ motion,
6
+ useMotionValue,
7
+ useSpring,
8
+ useTransform,
9
+ } from "framer-motion";
10
+ import { cn } from "../../utils/cn";
11
+
12
+ export interface Card3DProps {
13
+ children: ReactNode;
14
+ className?: string;
15
+ }
16
+
17
+ const MAX_TILT = 10; // deg
18
+
19
+ /**
20
+ * Card3D — a card that tilts in 3D space following the cursor, with
21
+ * depth-shifted content and a slight lift on hover.
22
+ *
23
+ * The container sets CSS `perspective`; on mouse move the cursor's
24
+ * normalized position maps to `rotateX`/`rotateY` (max ±10°) smoothed via
25
+ * `useSpring`. Children are wrapped in a layer translated along Z for a
26
+ * parallax pop, and the whole card scales up slightly on hover.
27
+ *
28
+ * @example
29
+ * <Card3D className="p-8">
30
+ * <h3>立体卡片</h3>
31
+ * </Card3D>
32
+ */
33
+ export function Card3D({ children, className }: Card3DProps) {
34
+ const ref = useRef<HTMLDivElement>(null);
35
+ const mx = useMotionValue(0);
36
+ const my = useMotionValue(0);
37
+
38
+ const rotateX = useSpring(
39
+ useTransform(my, [-0.5, 0.5], [MAX_TILT, -MAX_TILT]),
40
+ { stiffness: 150, damping: 18 }
41
+ );
42
+ const rotateY = useSpring(
43
+ useTransform(mx, [-0.5, 0.5], [-MAX_TILT, MAX_TILT]),
44
+ { stiffness: 150, damping: 18 }
45
+ );
46
+
47
+ function handleMouseMove(e: MouseEvent<HTMLDivElement>) {
48
+ const el = ref.current;
49
+ if (!el) return;
50
+ const rect = el.getBoundingClientRect();
51
+ mx.set((e.clientX - rect.left) / rect.width - 0.5);
52
+ my.set((e.clientY - rect.top) / rect.height - 0.5);
53
+ }
54
+
55
+ function handleMouseLeave() {
56
+ mx.set(0);
57
+ my.set(0);
58
+ }
59
+
60
+ return (
61
+ <div
62
+ ref={ref}
63
+ onMouseMove={handleMouseMove}
64
+ onMouseLeave={handleMouseLeave}
65
+ style={{ perspective: 1000 }}
66
+ className={cn("relative", className)}
67
+ >
68
+ <motion.div
69
+ style={{ rotateX, rotateY, transformStyle: "preserve-3d" }}
70
+ whileHover={{ scale: 1.03 }}
71
+ transition={{ type: "spring", stiffness: 200, damping: 20 }}
72
+ className="relative rounded-xl border border-line bg-surface"
73
+ >
74
+ {/* Depth-shifted content layer */}
75
+ <div
76
+ className="relative z-10 p-8"
77
+ style={{ transform: "translateZ(50px)" }}
78
+ >
79
+ {children}
80
+ </div>
81
+ </motion.div>
82
+ </div>
83
+ );
84
+ }
@@ -0,0 +1,2 @@
1
+ export { Card3D } from "./Card3D";
2
+ export type { Card3DProps } from "./Card3D";