@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.
- package/LICENSE +201 -0
- package/README.md +76 -0
- package/package.json +91 -0
- package/src/brand/index.ts +3 -0
- package/src/brand/logo/Logo.tsx +46 -0
- package/src/brand/logo/favicon.png +0 -0
- package/src/brand/logo/index.ts +2 -0
- package/src/brand/logo/zmzai-logo.png +0 -0
- package/src/brand/wordmark/Wordmark.tsx +48 -0
- package/src/brand/wordmark/index.ts +2 -0
- package/src/components/aceternity-sidebar/AceternitySidebar.tsx +133 -0
- package/src/components/aceternity-sidebar/index.ts +2 -0
- package/src/components/animated-tabs/AnimatedTabs.tsx +60 -0
- package/src/components/animated-tabs/index.ts +2 -0
- package/src/components/animated-tooltip/AnimatedTooltip.tsx +93 -0
- package/src/components/animated-tooltip/index.ts +2 -0
- package/src/components/avatar/Avatar.tsx +59 -0
- package/src/components/avatar/index.ts +2 -0
- package/src/components/badge/Badge.tsx +45 -0
- package/src/components/badge/index.ts +2 -0
- package/src/components/bento-grid/BentoGrid.tsx +85 -0
- package/src/components/bento-grid/index.ts +2 -0
- package/src/components/button/Button.tsx +66 -0
- package/src/components/button/button.styles.ts +41 -0
- package/src/components/button/index.ts +4 -0
- package/src/components/card/Card.tsx +87 -0
- package/src/components/card/card.styles.ts +35 -0
- package/src/components/card/index.ts +4 -0
- package/src/components/card-3d/Card3D.tsx +84 -0
- package/src/components/card-3d/index.ts +2 -0
- package/src/components/card-hover-effect/CardHoverEffect.tsx +83 -0
- package/src/components/card-hover-effect/index.ts +2 -0
- package/src/components/card-spotlight/CardSpotlight.tsx +63 -0
- package/src/components/card-spotlight/index.ts +2 -0
- package/src/components/carousel/Carousel.tsx +79 -0
- package/src/components/carousel/index.ts +2 -0
- package/src/components/code-block/CodeBlock.tsx +153 -0
- package/src/components/code-block/index.ts +2 -0
- package/src/components/comet-card/CometCard.tsx +106 -0
- package/src/components/comet-card/index.ts +2 -0
- package/src/components/compare/Compare.tsx +128 -0
- package/src/components/compare/index.ts +2 -0
- package/src/components/dialog/Dialog.tsx +79 -0
- package/src/components/dialog/index.ts +10 -0
- package/src/components/dropdown-menu/DropdownMenu.tsx +76 -0
- package/src/components/dropdown-menu/index.ts +8 -0
- package/src/components/empty-state/EmptyState.tsx +78 -0
- package/src/components/empty-state/index.ts +2 -0
- package/src/components/encrypt-text/EncryptText.tsx +102 -0
- package/src/components/encrypt-text/index.ts +2 -0
- package/src/components/evervault-card/EvervaultCard.tsx +114 -0
- package/src/components/evervault-card/index.ts +2 -0
- package/src/components/expandable-card/ExpandableCard.tsx +91 -0
- package/src/components/expandable-card/index.ts +2 -0
- package/src/components/file-upload/FileUpload.tsx +171 -0
- package/src/components/file-upload/index.ts +2 -0
- package/src/components/flip-words/FlipWords.tsx +70 -0
- package/src/components/flip-words/index.ts +2 -0
- package/src/components/floating-navbar/FloatingNavbar.tsx +82 -0
- package/src/components/floating-navbar/index.ts +2 -0
- package/src/components/focus-cards/FocusCards.tsx +93 -0
- package/src/components/focus-cards/index.ts +2 -0
- package/src/components/glare-card/GlareCard.tsx +66 -0
- package/src/components/glare-card/index.ts +2 -0
- package/src/components/glowing-effect/GlowingEffect.tsx +68 -0
- package/src/components/glowing-effect/index.ts +2 -0
- package/src/components/hover-border-gradient/HoverBorderGradient.tsx +80 -0
- package/src/components/hover-border-gradient/index.ts +2 -0
- package/src/components/index.ts +71 -0
- package/src/components/infinite-moving-cards/InfiniteMovingCards.tsx +84 -0
- package/src/components/infinite-moving-cards/index.ts +2 -0
- package/src/components/input/Input.tsx +35 -0
- package/src/components/input/index.ts +4 -0
- package/src/components/input/input.styles.ts +34 -0
- package/src/components/keyboard/Keyboard.tsx +38 -0
- package/src/components/keyboard/index.ts +2 -0
- package/src/components/layout-grid/LayoutGrid.tsx +121 -0
- package/src/components/layout-grid/index.ts +2 -0
- package/src/components/link-preview/LinkPreview.tsx +121 -0
- package/src/components/link-preview/index.ts +2 -0
- package/src/components/loader/Loader.tsx +130 -0
- package/src/components/loader/index.ts +2 -0
- package/src/components/magnetic-button/MagneticButton.tsx +79 -0
- package/src/components/magnetic-button/index.ts +2 -0
- package/src/components/moving-border/MovingBorder.tsx +72 -0
- package/src/components/moving-border/index.ts +2 -0
- package/src/components/multi-step-loader/MultiStepLoader.tsx +95 -0
- package/src/components/multi-step-loader/index.ts +2 -0
- package/src/components/notch/Notch.tsx +47 -0
- package/src/components/notch/index.ts +2 -0
- package/src/components/select/Select.tsx +100 -0
- package/src/components/select/index.ts +10 -0
- package/src/components/skeleton/Skeleton.tsx +34 -0
- package/src/components/skeleton/index.ts +2 -0
- package/src/components/stateful-button/StatefulButton.tsx +115 -0
- package/src/components/stateful-button/index.ts +2 -0
- package/src/components/sticky-banner/StickyBanner.tsx +74 -0
- package/src/components/sticky-banner/index.ts +2 -0
- package/src/components/sticky-scroll-reveal/StickyScrollReveal.tsx +141 -0
- package/src/components/sticky-scroll-reveal/index.ts +2 -0
- package/src/components/terminal/Terminal.tsx +55 -0
- package/src/components/terminal/index.ts +2 -0
- package/src/components/text-generate/TextGenerate.tsx +59 -0
- package/src/components/text-generate/index.ts +2 -0
- package/src/components/text-hover-effect/TextHoverEffect.tsx +90 -0
- package/src/components/text-hover-effect/index.ts +2 -0
- package/src/components/textarea/Textarea.tsx +39 -0
- package/src/components/textarea/index.ts +2 -0
- package/src/components/timeline/Timeline.tsx +106 -0
- package/src/components/timeline/index.ts +3 -0
- package/src/components/tooltip/Tooltip.tsx +51 -0
- package/src/components/tooltip/index.ts +2 -0
- package/src/components/tooltip-card/TooltipCard.tsx +130 -0
- package/src/components/tooltip-card/index.ts +2 -0
- package/src/components/tracing-beam/TracingBeam.tsx +110 -0
- package/src/components/tracing-beam/index.ts +2 -0
- package/src/components/typewriter/Typewriter.tsx +69 -0
- package/src/components/typewriter/index.ts +2 -0
- package/src/components/vanish-input/VanishInput.tsx +170 -0
- package/src/components/vanish-input/index.ts +2 -0
- package/src/components/wobble-card/WobbleCard.tsx +77 -0
- package/src/components/wobble-card/index.ts +2 -0
- package/src/index.ts +21 -0
- package/src/tokens/fonts.css +42 -0
- package/src/tokens/theme.css +120 -0
- package/src/utils/cn.ts +13 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/motion.ts +47 -0
- package/src/vite-env.d.ts +6 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRef, type ReactNode } from "react";
|
|
4
|
+
import { motion, useScroll, useTransform } from "framer-motion";
|
|
5
|
+
import { cn } from "../../utils/cn";
|
|
6
|
+
|
|
7
|
+
export interface StickyScrollItem {
|
|
8
|
+
title: string;
|
|
9
|
+
description: string;
|
|
10
|
+
content?: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface StickyScrollRevealProps {
|
|
14
|
+
items: StickyScrollItem[];
|
|
15
|
+
className?: string;
|
|
16
|
+
/** Tailwind text token applied to the active title accent underline */
|
|
17
|
+
contentClassName?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* StickyScrollReveal — content blocks that stick and swap on scroll.
|
|
22
|
+
*
|
|
23
|
+
* The left column holds a sticky title + description that changes as the
|
|
24
|
+
* user scrolls through each item; the right column shows optional content.
|
|
25
|
+
* Each item reveals with a fade/slide driven by scroll progress. Based on
|
|
26
|
+
* Aceternity's Sticky Scroll Reveal, adapted to the zmzai monochrome palette.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* <StickyScrollReveal items={[
|
|
30
|
+
* { title: "步骤一", description: "上传文件", content: <Preview /> },
|
|
31
|
+
* { title: "步骤二", description: "生成内容" },
|
|
32
|
+
* ]} />
|
|
33
|
+
*/
|
|
34
|
+
export function StickyScrollReveal({
|
|
35
|
+
items,
|
|
36
|
+
className,
|
|
37
|
+
contentClassName,
|
|
38
|
+
}: StickyScrollRevealProps) {
|
|
39
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
40
|
+
const { scrollYProgress } = useScroll({
|
|
41
|
+
target: ref,
|
|
42
|
+
offset: ["start start", "end end"],
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
ref={ref}
|
|
48
|
+
className={cn(
|
|
49
|
+
"relative flex flex-col",
|
|
50
|
+
className
|
|
51
|
+
)}
|
|
52
|
+
>
|
|
53
|
+
{items.map((item, idx) => (
|
|
54
|
+
<StickyItem
|
|
55
|
+
key={idx}
|
|
56
|
+
item={item}
|
|
57
|
+
index={idx}
|
|
58
|
+
total={items.length}
|
|
59
|
+
progress={scrollYProgress}
|
|
60
|
+
contentClassName={contentClassName}
|
|
61
|
+
/>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface StickyItemProps {
|
|
68
|
+
item: StickyScrollItem;
|
|
69
|
+
index: number;
|
|
70
|
+
total: number;
|
|
71
|
+
progress: ReturnType<typeof useScroll>["scrollYProgress"];
|
|
72
|
+
contentClassName?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function StickyItem({
|
|
76
|
+
item,
|
|
77
|
+
index,
|
|
78
|
+
total,
|
|
79
|
+
progress,
|
|
80
|
+
contentClassName,
|
|
81
|
+
}: StickyItemProps) {
|
|
82
|
+
const segment = 1 / total;
|
|
83
|
+
// active window for this item
|
|
84
|
+
const start = index * segment;
|
|
85
|
+
const end = start + segment;
|
|
86
|
+
|
|
87
|
+
// opacity/scale for left column based on scroll position within segment
|
|
88
|
+
const opacity = useTransform(
|
|
89
|
+
progress,
|
|
90
|
+
[Math.max(0, start - segment * 0.4), start, end, end + segment * 0.4],
|
|
91
|
+
[0.3, 1, 1, 0.3]
|
|
92
|
+
);
|
|
93
|
+
const translateY = useTransform(
|
|
94
|
+
progress,
|
|
95
|
+
[start - segment * 0.4, start, end, end + segment * 0.4],
|
|
96
|
+
[20, 0, 0, -20]
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div className="flex min-h-screen items-start gap-6 md:gap-10 lg:gap-16">
|
|
101
|
+
{/* Left: sticky text column */}
|
|
102
|
+
<div className="sticky top-0 flex h-screen w-full flex-col justify-center md:w-1/2">
|
|
103
|
+
<motion.div style={{ opacity, y: translateY }}>
|
|
104
|
+
{/* Index marker */}
|
|
105
|
+
<span className="mb-4 inline-flex h-7 w-7 items-center justify-center rounded-full border border-line text-xs text-ink-2">
|
|
106
|
+
{String(index + 1).padStart(2, "0")}
|
|
107
|
+
</span>
|
|
108
|
+
<h3 className="text-2xl font-semibold tracking-tight text-ink md:text-3xl lg:text-4xl">
|
|
109
|
+
{item.title}
|
|
110
|
+
</h3>
|
|
111
|
+
<p className="mt-4 max-w-md text-sm text-ink-2 md:text-base">
|
|
112
|
+
{item.description}
|
|
113
|
+
</p>
|
|
114
|
+
</motion.div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{/* Right: content column */}
|
|
118
|
+
<div className="flex w-full items-center py-24 md:w-1/2">
|
|
119
|
+
{item.content ? (
|
|
120
|
+
<div
|
|
121
|
+
className={cn(
|
|
122
|
+
"w-full rounded-xl border border-line bg-surface p-6",
|
|
123
|
+
contentClassName
|
|
124
|
+
)}
|
|
125
|
+
>
|
|
126
|
+
{item.content}
|
|
127
|
+
</div>
|
|
128
|
+
) : (
|
|
129
|
+
<div
|
|
130
|
+
className={cn(
|
|
131
|
+
"flex h-72 w-full items-center justify-center rounded-xl border border-line bg-surface text-sm text-ink-3",
|
|
132
|
+
contentClassName
|
|
133
|
+
)}
|
|
134
|
+
>
|
|
135
|
+
{item.title}
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
|
|
4
|
+
import { cn } from "../../utils/cn";
|
|
5
|
+
|
|
6
|
+
export interface TerminalProps extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
/** Title shown in the title bar */
|
|
8
|
+
title?: string;
|
|
9
|
+
/** Command-line content */
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Terminal — a Mac-style terminal window.
|
|
15
|
+
*
|
|
16
|
+
* Renders a dark window with a title bar (traffic lights + title) and a
|
|
17
|
+
* mono-font command area. A green prompt symbol prefixes the children.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* <Terminal title="bash">
|
|
21
|
+
* <span>npm run dev</span>
|
|
22
|
+
* </Terminal>
|
|
23
|
+
*/
|
|
24
|
+
export const Terminal = forwardRef<HTMLDivElement, TerminalProps>(
|
|
25
|
+
({ title = "bash", children, className, ...props }, ref) => {
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
ref={ref}
|
|
29
|
+
className={cn(
|
|
30
|
+
"overflow-hidden rounded-xl border border-dark-line bg-dark-bg shadow-lg",
|
|
31
|
+
className
|
|
32
|
+
)}
|
|
33
|
+
{...props}
|
|
34
|
+
>
|
|
35
|
+
{/* Title bar */}
|
|
36
|
+
<div className="flex items-center gap-2 border-b border-dark-line bg-dark-surface px-4 py-2.5">
|
|
37
|
+
<div className="flex gap-1.5">
|
|
38
|
+
<span className="h-3 w-3 rounded-full bg-[#ff5f56]" />
|
|
39
|
+
<span className="h-3 w-3 rounded-full bg-[#ffbd2e]" />
|
|
40
|
+
<span className="h-3 w-3 rounded-full bg-[#27c93f]" />
|
|
41
|
+
</div>
|
|
42
|
+
<span className="ml-2 font-mono text-xs text-dark-ink/60">{title}</span>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
{/* Body */}
|
|
46
|
+
<div className="p-4 font-mono text-[13px] leading-relaxed text-dark-ink">
|
|
47
|
+
<span className="mr-2 select-none text-[#27c93f]">$</span>
|
|
48
|
+
{children}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
Terminal.displayName = "Terminal";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type HTMLMotionProps } from "framer-motion";
|
|
4
|
+
import { motion } from "framer-motion";
|
|
5
|
+
import { cn } from "../../utils/cn";
|
|
6
|
+
|
|
7
|
+
export interface TextGenerateProps
|
|
8
|
+
extends Omit<HTMLMotionProps<"span">, "ref"> {
|
|
9
|
+
/** Text content to animate */
|
|
10
|
+
text: string;
|
|
11
|
+
/** Duration of each word's fade-in (seconds) */
|
|
12
|
+
duration?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* TextGenerate — text that fades in word by word on mount.
|
|
17
|
+
*
|
|
18
|
+
* Splits `text` by spaces and staggers each word's opacity 0→1.
|
|
19
|
+
* Preserves inline layout via a wrapping span.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* <TextGenerate text="Hello world from zmzai" duration={0.5} />
|
|
23
|
+
*/
|
|
24
|
+
export function TextGenerate({
|
|
25
|
+
text,
|
|
26
|
+
duration = 0.5,
|
|
27
|
+
className,
|
|
28
|
+
...props
|
|
29
|
+
}: TextGenerateProps) {
|
|
30
|
+
const words = text.split(" ");
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<motion.span
|
|
34
|
+
className={cn("inline-block", className)}
|
|
35
|
+
initial="hidden"
|
|
36
|
+
animate="visible"
|
|
37
|
+
{...props}
|
|
38
|
+
>
|
|
39
|
+
{words.map((word, i) => (
|
|
40
|
+
<motion.span
|
|
41
|
+
key={i}
|
|
42
|
+
className="inline-block will-change-transform"
|
|
43
|
+
variants={{
|
|
44
|
+
hidden: { opacity: 0, y: 4 },
|
|
45
|
+
visible: { opacity: 1, y: 0 },
|
|
46
|
+
}}
|
|
47
|
+
transition={{
|
|
48
|
+
duration,
|
|
49
|
+
delay: i * (duration * 0.4),
|
|
50
|
+
ease: [0.16, 1, 0.3, 1],
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
{word}
|
|
54
|
+
{i < words.length - 1 ? "\u00A0" : ""}
|
|
55
|
+
</motion.span>
|
|
56
|
+
))}
|
|
57
|
+
</motion.span>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type Variants } from "framer-motion";
|
|
4
|
+
import { motion } from "framer-motion";
|
|
5
|
+
import { cn } from "../../utils/cn";
|
|
6
|
+
|
|
7
|
+
export interface TextHoverEffectProps {
|
|
8
|
+
/** The text to display and animate */
|
|
9
|
+
text: string;
|
|
10
|
+
className?: string;
|
|
11
|
+
/** Per-letter animation duration in seconds (default 0.3) */
|
|
12
|
+
duration?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const containerVariants: Variants = {
|
|
16
|
+
rest: { transition: { staggerChildren: 0.025, staggerDirection: -1 } },
|
|
17
|
+
hover: { transition: { staggerChildren: 0.025 } },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function makeLetterVariants(duration: number): Variants {
|
|
21
|
+
return {
|
|
22
|
+
rest: { y: 0, opacity: 1 },
|
|
23
|
+
hover: { y: "-100%", opacity: 0 },
|
|
24
|
+
transition: { duration },
|
|
25
|
+
} as Variants;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* TextHoverEffect — text whose letters slide up and are replaced on hover.
|
|
30
|
+
*
|
|
31
|
+
* Each letter animates upward and out while a duplicate slides in from
|
|
32
|
+
* below (Aceternity-style hover reveal). Uses framer-motion `staggerChildren`
|
|
33
|
+
* so letters cascade rather than moving in lockstep.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* <TextHoverEffect text="开始使用" className="text-2xl font-semibold" />
|
|
37
|
+
*/
|
|
38
|
+
export function TextHoverEffect({
|
|
39
|
+
text,
|
|
40
|
+
className,
|
|
41
|
+
duration = 0.3,
|
|
42
|
+
}: TextHoverEffectProps) {
|
|
43
|
+
const letters = Array.from(text);
|
|
44
|
+
const letterVariants = makeLetterVariants(duration);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<motion.span
|
|
48
|
+
className={cn(
|
|
49
|
+
"relative inline-block cursor-pointer overflow-hidden align-bottom text-ink",
|
|
50
|
+
className
|
|
51
|
+
)}
|
|
52
|
+
variants={containerVariants}
|
|
53
|
+
initial="rest"
|
|
54
|
+
animate="rest"
|
|
55
|
+
whileHover="hover"
|
|
56
|
+
>
|
|
57
|
+
{/* Top row: slides up & fades out on hover */}
|
|
58
|
+
<span className="inline-flex" aria-hidden>
|
|
59
|
+
{letters.map((char, i) => (
|
|
60
|
+
<motion.span
|
|
61
|
+
key={`top-${i}`}
|
|
62
|
+
className="inline-block whitespace-pre"
|
|
63
|
+
variants={letterVariants}
|
|
64
|
+
transition={{ duration }}
|
|
65
|
+
>
|
|
66
|
+
{char === " " ? "\u00A0" : char}
|
|
67
|
+
</motion.span>
|
|
68
|
+
))}
|
|
69
|
+
</span>
|
|
70
|
+
{/* Bottom row: slides up into place on hover */}
|
|
71
|
+
<span className="absolute inset-0 inline-flex" aria-hidden>
|
|
72
|
+
{letters.map((char, i) => (
|
|
73
|
+
<motion.span
|
|
74
|
+
key={`bottom-${i}`}
|
|
75
|
+
className="inline-block whitespace-pre text-ink-2"
|
|
76
|
+
variants={{
|
|
77
|
+
rest: { y: "100%", opacity: 0 },
|
|
78
|
+
hover: { y: 0, opacity: 1 },
|
|
79
|
+
}}
|
|
80
|
+
transition={{ duration }}
|
|
81
|
+
>
|
|
82
|
+
{char === " " ? "\u00A0" : char}
|
|
83
|
+
</motion.span>
|
|
84
|
+
))}
|
|
85
|
+
</span>
|
|
86
|
+
{/* Accessible label for screen readers */}
|
|
87
|
+
<span className="sr-only">{text}</span>
|
|
88
|
+
</motion.span>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, type TextareaHTMLAttributes } from "react";
|
|
4
|
+
import { cn } from "../../utils/cn";
|
|
5
|
+
|
|
6
|
+
export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
7
|
+
variant?: "default" | "brutal";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Textarea — multi-line text input.
|
|
12
|
+
*
|
|
13
|
+
* - `default`: light border, focus border turns ink
|
|
14
|
+
* - `brutal`: 2px ink border + hard offset shadow
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* <Textarea placeholder="描述任务…" rows={4} />
|
|
18
|
+
* <Textarea variant="brutal" placeholder="描述任务…" />
|
|
19
|
+
*/
|
|
20
|
+
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
21
|
+
({ className, variant = "default", ...props }, ref) => {
|
|
22
|
+
return (
|
|
23
|
+
<textarea
|
|
24
|
+
ref={ref}
|
|
25
|
+
className={cn(
|
|
26
|
+
"w-full resize-none bg-bg font-sans text-ink placeholder:text-ink-3 outline-none transition-all",
|
|
27
|
+
variant === "default" &&
|
|
28
|
+
"border border-line rounded-lg px-4 py-2.5 text-sm focus:border-ink",
|
|
29
|
+
variant === "brutal" &&
|
|
30
|
+
"border-2 border-ink rounded-xl px-4 py-3 text-sm shadow-[4px_4px_0_var(--color-ink)] focus:shadow-[6px_6px_0_var(--color-accent)] focus:border-accent focus:-translate-x-px focus:-translate-y-px",
|
|
31
|
+
className
|
|
32
|
+
)}
|
|
33
|
+
{...props}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
Textarea.displayName = "Textarea";
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRef, type ReactNode } from "react";
|
|
4
|
+
import { motion, useScroll, useSpring } from "framer-motion";
|
|
5
|
+
import { cn } from "../../utils/cn";
|
|
6
|
+
|
|
7
|
+
export interface TimelineItem {
|
|
8
|
+
title: string;
|
|
9
|
+
content: ReactNode;
|
|
10
|
+
/** Optional timestamp shown beside title */
|
|
11
|
+
time?: string;
|
|
12
|
+
/** Status: default (gray), active (filled black), done (filled black + check) */
|
|
13
|
+
status?: "default" | "active" | "done";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TimelineProps {
|
|
17
|
+
items: TimelineItem[];
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Timeline — scroll-reveal timeline with progress beam.
|
|
23
|
+
*
|
|
24
|
+
* Minimal black-gray design: thin line fills as you scroll,
|
|
25
|
+
* dots are black/gray only (no green/blue).
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* <Timeline items={[
|
|
29
|
+
* { title: "任务创建", time: "14:32", status: "done", content: <p>用户发起请求</p> },
|
|
30
|
+
* { title: "执行中", time: "14:34", status: "active", content: <p>正在生成…</p> },
|
|
31
|
+
* { title: "等待审批", status: "default", content: <p>写入文件</p> },
|
|
32
|
+
* ]} />
|
|
33
|
+
*/
|
|
34
|
+
export function Timeline({ items, className }: TimelineProps) {
|
|
35
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
36
|
+
const { scrollYProgress } = useScroll({
|
|
37
|
+
target: ref,
|
|
38
|
+
offset: ["start center", "end end"],
|
|
39
|
+
});
|
|
40
|
+
const scaleY = useSpring(scrollYProgress, {
|
|
41
|
+
stiffness: 100,
|
|
42
|
+
damping: 30,
|
|
43
|
+
restDelta: 0.001,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div ref={ref} className={cn("relative", className)}>
|
|
48
|
+
{/* Track */}
|
|
49
|
+
<div className="absolute left-[7px] top-2 bottom-2 w-px bg-line" />
|
|
50
|
+
|
|
51
|
+
{/* Progress fill */}
|
|
52
|
+
<motion.div
|
|
53
|
+
className="absolute left-[7px] top-2 w-px origin-top bg-ink"
|
|
54
|
+
style={{ scaleY, height: "calc(100% - 1rem)" }}
|
|
55
|
+
/>
|
|
56
|
+
|
|
57
|
+
{/* Items */}
|
|
58
|
+
<div className="flex flex-col">
|
|
59
|
+
{items.map((item, index) => (
|
|
60
|
+
<TimelineItemRow key={index} item={item} isLast={index === items.length - 1} />
|
|
61
|
+
))}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function TimelineItemRow({ item, isLast }: { item: TimelineItem; isLast: boolean }) {
|
|
68
|
+
const status = item.status ?? "default";
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className={cn("relative flex gap-4", isLast ? "pb-0" : "pb-6")}>
|
|
72
|
+
{/* Dot — black/gray only */}
|
|
73
|
+
<div className="relative z-10 flex-shrink-0 pt-0.5">
|
|
74
|
+
{status === "done" && (
|
|
75
|
+
<div className="flex h-3.5 w-3.5 items-center justify-center rounded-full bg-ink">
|
|
76
|
+
<svg className="h-2 w-2 text-white" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
77
|
+
<polyline points="10 3 5 8 2 5" />
|
|
78
|
+
</svg>
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
{status === "active" && (
|
|
82
|
+
<div className="h-3.5 w-3.5 rounded-full border-2 border-ink bg-ink animate-pulse" />
|
|
83
|
+
)}
|
|
84
|
+
{status === "default" && (
|
|
85
|
+
<div className="h-3.5 w-3.5 rounded-full border-2 border-line bg-bg" />
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{/* Content */}
|
|
90
|
+
<div className="min-w-0 flex-1">
|
|
91
|
+
<div className="mb-1 flex items-center gap-2">
|
|
92
|
+
<span className={cn(
|
|
93
|
+
"text-sm font-semibold",
|
|
94
|
+
status === "default" ? "text-ink-3" : "text-ink"
|
|
95
|
+
)}>
|
|
96
|
+
{item.title}
|
|
97
|
+
</span>
|
|
98
|
+
{item.time && (
|
|
99
|
+
<span className="ml-auto font-mono text-xs text-ink-3">{item.time}</span>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
<div className="text-sm leading-relaxed text-ink-2">{item.content}</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type ReactNode } from "react";
|
|
4
|
+
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
5
|
+
import { cn } from "../../utils/cn";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* TooltipProvider — wrap app root once.
|
|
9
|
+
* Required by Radix Tooltip for delay/positioning context.
|
|
10
|
+
*/
|
|
11
|
+
export const TooltipProvider = TooltipPrimitive.Provider;
|
|
12
|
+
|
|
13
|
+
export interface TooltipProps {
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
content: ReactNode;
|
|
16
|
+
side?: "top" | "right" | "bottom" | "left";
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Tooltip — hover/focus tooltip (Radix UI).
|
|
22
|
+
*
|
|
23
|
+
* Wrap your app in `<TooltipProvider>` (once, at root), then use `<Tooltip>`.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* <Tooltip content="删除">
|
|
27
|
+
* <button>×</button>
|
|
28
|
+
* </Tooltip>
|
|
29
|
+
*/
|
|
30
|
+
export function Tooltip({ children, content, side = "top", className }: TooltipProps) {
|
|
31
|
+
return (
|
|
32
|
+
<TooltipPrimitive.Root delayDuration={200}>
|
|
33
|
+
<TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
|
|
34
|
+
<TooltipPrimitive.Portal>
|
|
35
|
+
<TooltipPrimitive.Content
|
|
36
|
+
side={side}
|
|
37
|
+
sideOffset={6}
|
|
38
|
+
className={cn(
|
|
39
|
+
"z-50 rounded-lg bg-ink px-2.5 py-1.5 text-xs font-medium text-white shadow-md",
|
|
40
|
+
"data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95",
|
|
41
|
+
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
|
42
|
+
className
|
|
43
|
+
)}
|
|
44
|
+
>
|
|
45
|
+
{content}
|
|
46
|
+
<TooltipPrimitive.Arrow className="fill-ink" />
|
|
47
|
+
</TooltipPrimitive.Content>
|
|
48
|
+
</TooltipPrimitive.Portal>
|
|
49
|
+
</TooltipPrimitive.Root>
|
|
50
|
+
);
|
|
51
|
+
}
|