@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,79 @@
1
+ "use client";
2
+
3
+ import { useRef, type MouseEvent, type ReactNode } from "react";
4
+ import {
5
+ motion,
6
+ useMotionValue,
7
+ useSpring,
8
+ type HTMLMotionProps,
9
+ } from "framer-motion";
10
+ import { cn } from "../../utils/cn";
11
+
12
+ export interface MagneticButtonProps
13
+ extends Omit<HTMLMotionProps<"button">, "ref" | "children"> {
14
+ children?: ReactNode;
15
+ /** How strongly the button drifts toward the cursor (default 0.3 = 30%) */
16
+ magnetism?: number;
17
+ }
18
+
19
+ /**
20
+ * MagneticButton — a button that subtly drifts toward the cursor.
21
+ *
22
+ * Uses framer-motion `useMotionValue` + `useSpring` to translate the
23
+ * button toward the mouse position by a fraction of the distance, then
24
+ * springs back to center on mouse leave. Pure micro-interaction, no
25
+ * layout shift.
26
+ *
27
+ * @example
28
+ * <MagneticButton className="rounded-full bg-ink px-6 py-3 text-white">
29
+ * 立即开始
30
+ * </MagneticButton>
31
+ */
32
+ export function MagneticButton({
33
+ children,
34
+ className,
35
+ magnetism = 0.3,
36
+ onMouseMove,
37
+ onMouseLeave,
38
+ ...props
39
+ }: MagneticButtonProps) {
40
+ const ref = useRef<HTMLButtonElement>(null);
41
+ const x = useMotionValue(0);
42
+ const y = useMotionValue(0);
43
+
44
+ const springX = useSpring(x, { stiffness: 200, damping: 15 });
45
+ const springY = useSpring(y, { stiffness: 200, damping: 15 });
46
+
47
+ function handleMouseMove(e: MouseEvent<HTMLButtonElement>) {
48
+ const el = ref.current;
49
+ if (!el) return;
50
+ const rect = el.getBoundingClientRect();
51
+ const relX = e.clientX - (rect.left + rect.width / 2);
52
+ const relY = e.clientY - (rect.top + rect.height / 2);
53
+ x.set(relX * magnetism);
54
+ y.set(relY * magnetism);
55
+ onMouseMove?.(e);
56
+ }
57
+
58
+ function handleMouseLeave(e: MouseEvent<HTMLButtonElement>) {
59
+ x.set(0);
60
+ y.set(0);
61
+ onMouseLeave?.(e);
62
+ }
63
+
64
+ return (
65
+ <motion.button
66
+ ref={ref}
67
+ onMouseMove={handleMouseMove}
68
+ onMouseLeave={handleMouseLeave}
69
+ style={{ x: springX, y: springY }}
70
+ className={cn(
71
+ "relative inline-flex cursor-pointer items-center justify-center rounded-full bg-ink px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-ink/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg disabled:cursor-not-allowed disabled:opacity-50",
72
+ className
73
+ )}
74
+ {...props}
75
+ >
76
+ {children}
77
+ </motion.button>
78
+ );
79
+ }
@@ -0,0 +1,2 @@
1
+ export { MagneticButton } from "./MagneticButton";
2
+ export type { MagneticButtonProps } from "./MagneticButton";
@@ -0,0 +1,72 @@
1
+ "use client";
2
+
3
+ import { type ReactNode } from "react";
4
+ import { cn } from "../../utils/cn";
5
+
6
+ export interface MovingBorderProps {
7
+ children: ReactNode;
8
+ className?: string;
9
+ /** Border rotation duration in seconds (default 5) */
10
+ duration?: number;
11
+ /** Border color (default accent blue) */
12
+ borderColor?: string;
13
+ /** Inner background color (default bg) */
14
+ backgroundColor?: string;
15
+ /** Border radius (default rounded-xl = 12px) */
16
+ borderRadius?: string;
17
+ }
18
+
19
+ /**
20
+ * MovingBorder — container with rotating conic-gradient border beam.
21
+ *
22
+ * Uses CSS `@property --angle` for a smooth rotating light beam around
23
+ * the border. Based on Aceternity's Moving Border, adapted to zmzai's
24
+ * blue accent. Only the border moves — inner content stays static.
25
+ *
26
+ * @example
27
+ * <MovingBorder duration={5}>
28
+ * <input placeholder="聚焦看边框光束…" />
29
+ * </MovingBorder>
30
+ */
31
+ export function MovingBorder({
32
+ children,
33
+ className,
34
+ duration = 5,
35
+ borderColor = "var(--color-accent)",
36
+ backgroundColor = "var(--color-bg)",
37
+ borderRadius = "12px",
38
+ }: MovingBorderProps) {
39
+ return (
40
+ <>
41
+ {/* @property must be in global scope — inject once */}
42
+ <style>{`
43
+ @property --mb-angle {
44
+ syntax: '<angle>';
45
+ initial-value: 0deg;
46
+ inherits: false;
47
+ }
48
+ @keyframes mb-rotate { to { --mb-angle: 360deg; } }
49
+ `}</style>
50
+
51
+ <div
52
+ className={cn("relative", className)}
53
+ style={{
54
+ borderRadius,
55
+ padding: "1px",
56
+ background: `conic-gradient(from var(--mb-angle), transparent 0%, transparent 60%, ${borderColor} 80%, transparent 95%)`,
57
+ animation: `mb-rotate ${duration}s linear infinite`,
58
+ }}
59
+ >
60
+ <div
61
+ className="relative h-full w-full"
62
+ style={{
63
+ borderRadius: `calc(${borderRadius} - 1px)`,
64
+ background: backgroundColor,
65
+ }}
66
+ >
67
+ {children}
68
+ </div>
69
+ </div>
70
+ </>
71
+ );
72
+ }
@@ -0,0 +1,2 @@
1
+ export { MovingBorder } from "./MovingBorder";
2
+ export type { MovingBorderProps } from "./MovingBorder";
@@ -0,0 +1,95 @@
1
+ "use client";
2
+
3
+ import { motion, AnimatePresence } from "framer-motion";
4
+ import { cn } from "../../utils/cn";
5
+
6
+ export interface MultiStepLoaderStep {
7
+ /** Display text for this step */
8
+ text: string;
9
+ }
10
+
11
+ export interface MultiStepLoaderProps {
12
+ /** Steps to display. Each shows sequentially. */
13
+ steps: MultiStepLoaderStep[];
14
+ /** Currently active step index (0-based). Steps before are completed. */
15
+ currentIndex: number;
16
+ className?: string;
17
+ }
18
+
19
+ /**
20
+ * MultiStepLoader — sequential step loader for long-running tasks.
21
+ *
22
+ * Each step has 3 states: completed (checkmark), active (spinner), pending (dot).
23
+ * Perfect for Agent task execution feedback.
24
+ *
25
+ * @example
26
+ * const steps = [{ text: "分析需求" }, { text: "生成代码" }, { text: "运行测试" }];
27
+ * <MultiStepLoader steps={steps} currentIndex={1} />
28
+ */
29
+ export function MultiStepLoader({
30
+ steps,
31
+ currentIndex,
32
+ className,
33
+ }: MultiStepLoaderProps) {
34
+ return (
35
+ <div className={cn("flex flex-col gap-3", className)}>
36
+ {steps.map((step, index) => {
37
+ const isCompleted = index < currentIndex;
38
+ const isActive = index === currentIndex;
39
+ const isPending = index > currentIndex;
40
+
41
+ return (
42
+ <div key={index} className="flex items-center gap-3">
43
+ {/* Status indicator */}
44
+ <div className="flex h-5 w-5 flex-shrink-0 items-center justify-center">
45
+ {isCompleted && (
46
+ <motion.div
47
+ initial={{ scale: 0 }}
48
+ animate={{ scale: 1 }}
49
+ transition={{ type: "spring", stiffness: 400, damping: 25 }}
50
+ className="flex h-5 w-5 items-center justify-center rounded-full bg-ink"
51
+ >
52
+ <svg className="h-3 w-3 text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
53
+ <polyline points="20 6 9 17 4 12" />
54
+ </svg>
55
+ </motion.div>
56
+ )}
57
+ {isActive && (
58
+ <svg className="h-5 w-5 animate-spin text-accent" viewBox="0 0 24 24" fill="none">
59
+ <circle className="opacity-20" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="3" />
60
+ <path className="opacity-90" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
61
+ </svg>
62
+ )}
63
+ {isPending && (
64
+ <div className="h-2 w-2 rounded-full border border-line" />
65
+ )}
66
+ </div>
67
+
68
+ {/* Text */}
69
+ <AnimatePresence mode="wait">
70
+ <motion.span
71
+ key={`${index}-${isCompleted}-${isActive}`}
72
+ initial={{ opacity: 0 }}
73
+ animate={{ opacity: 1 }}
74
+ transition={{ duration: 0.2 }}
75
+ className={cn(
76
+ "text-sm transition-colors",
77
+ isCompleted && "text-ink-3 line-through",
78
+ isActive && "font-medium text-ink",
79
+ isPending && "text-ink-3"
80
+ )}
81
+ >
82
+ {step.text}
83
+ </motion.span>
84
+ </AnimatePresence>
85
+
86
+ {/* Step number */}
87
+ <span className="ml-auto font-mono text-xs text-ink-3">
88
+ {String(index + 1).padStart(2, "0")}
89
+ </span>
90
+ </div>
91
+ );
92
+ })}
93
+ </div>
94
+ );
95
+ }
@@ -0,0 +1,2 @@
1
+ export { MultiStepLoader } from "./MultiStepLoader";
2
+ export type { MultiStepLoaderProps, MultiStepLoaderStep } from "./MultiStepLoader";
@@ -0,0 +1,47 @@
1
+ "use client";
2
+
3
+ import { type ReactNode } from "react";
4
+ import { AnimatePresence, motion, type HTMLMotionProps } from "framer-motion";
5
+ import { cn } from "../../utils/cn";
6
+
7
+ export interface NotchProps extends Omit<HTMLMotionProps<"div">, "ref"> {
8
+ /** Notification content */
9
+ children?: ReactNode;
10
+ /** Whether the notch is visible */
11
+ visible?: boolean;
12
+ }
13
+
14
+ /**
15
+ * Notch — a floating notification bar pinned to the top center.
16
+ *
17
+ * Inspired by the macOS Dynamic Island: a pill that slides down from the
18
+ * top of the viewport when `visible` becomes true and slides up on hide.
19
+ * Rendered in a fixed overlay so it floats above page content.
20
+ *
21
+ * @example
22
+ * const [open, setOpen] = useState(true)
23
+ * <Notch visible={open} onClose={() => setOpen(false)}>新消息</Notch>
24
+ */
25
+ export function Notch({ children, visible = false, className, ...props }: NotchProps) {
26
+ return (
27
+ <div className="pointer-events-none fixed inset-x-0 top-0 z-50 flex justify-center">
28
+ <AnimatePresence>
29
+ {visible && (
30
+ <motion.div
31
+ initial={{ y: -60, opacity: 0 }}
32
+ animate={{ y: 24, opacity: 1 }}
33
+ exit={{ y: -60, opacity: 0 }}
34
+ transition={{ duration: 0.35, ease: [0.16, 1, 0.3, 1] }}
35
+ className={cn(
36
+ "pointer-events-auto flex items-center gap-2 rounded-full border border-line bg-ink px-4 py-2 text-sm text-white shadow-lg",
37
+ className
38
+ )}
39
+ {...props}
40
+ >
41
+ {children}
42
+ </motion.div>
43
+ )}
44
+ </AnimatePresence>
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,2 @@
1
+ export { Notch } from "./Notch";
2
+ export type { NotchProps } from "./Notch";
@@ -0,0 +1,100 @@
1
+ "use client";
2
+
3
+ import { type ReactNode } from "react";
4
+ import * as SelectPrimitive from "@radix-ui/react-select";
5
+ import { cn } from "../../utils/cn";
6
+
7
+ export const Select = SelectPrimitive.Root;
8
+ export const SelectGroup = SelectPrimitive.Group;
9
+ export const SelectValue = SelectPrimitive.Value;
10
+
11
+ export function SelectTrigger({
12
+ children,
13
+ className,
14
+ }: {
15
+ children: ReactNode;
16
+ className?: string;
17
+ }) {
18
+ return (
19
+ <SelectPrimitive.Trigger
20
+ className={cn(
21
+ "inline-flex h-9 items-center justify-between gap-2 rounded-lg border border-line bg-bg px-3 py-2 text-sm font-medium text-ink outline-none transition-colors",
22
+ "hover:border-ink/40 focus:border-ink data-[placeholder]:text-ink-3",
23
+ className
24
+ )}
25
+ >
26
+ {children}
27
+ <SelectPrimitive.Icon asChild>
28
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
29
+ <path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
30
+ </svg>
31
+ </SelectPrimitive.Icon>
32
+ </SelectPrimitive.Trigger>
33
+ );
34
+ }
35
+
36
+ export function SelectContent({
37
+ children,
38
+ className,
39
+ position = "popper",
40
+ }: {
41
+ children: ReactNode;
42
+ className?: string;
43
+ position?: "popper" | "item-aligned";
44
+ }) {
45
+ return (
46
+ <SelectPrimitive.Portal>
47
+ <SelectPrimitive.Content
48
+ position={position}
49
+ sideOffset={4}
50
+ className={cn(
51
+ "z-50 min-w-[8rem] overflow-hidden rounded-lg border border-line bg-bg p-1 shadow-lg",
52
+ "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
53
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
54
+ className
55
+ )}
56
+ >
57
+ <SelectPrimitive.Viewport className="w-full">{children}</SelectPrimitive.Viewport>
58
+ </SelectPrimitive.Content>
59
+ </SelectPrimitive.Portal>
60
+ );
61
+ }
62
+
63
+ export function SelectItem({
64
+ children,
65
+ value,
66
+ className,
67
+ }: {
68
+ children: ReactNode;
69
+ value: string;
70
+ className?: string;
71
+ }) {
72
+ return (
73
+ <SelectPrimitive.Item
74
+ value={value}
75
+ className={cn(
76
+ "relative flex cursor-pointer select-none items-center rounded-md py-1.5 pl-7 pr-2 text-sm outline-none transition-colors",
77
+ "focus:bg-surface-2 focus:text-ink data-[state=checked]:font-semibold data-[highlighted]:bg-surface-2",
78
+ "data-[state=unchecked]:text-ink-2",
79
+ className
80
+ )}
81
+ >
82
+ <SelectPrimitive.ItemIndicator className="absolute left-2">
83
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
84
+ <path d="M3 7L6 10L11 4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
85
+ </svg>
86
+ </SelectPrimitive.ItemIndicator>
87
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
88
+ </SelectPrimitive.Item>
89
+ );
90
+ }
91
+
92
+ export function SelectLabel({ children }: { children: ReactNode }) {
93
+ return (
94
+ <div className="px-2 py-1.5 text-xs font-semibold text-ink-3">{children}</div>
95
+ );
96
+ }
97
+
98
+ export function SelectSeparator() {
99
+ return <div className="my-1 h-px bg-line" />;
100
+ }
@@ -0,0 +1,10 @@
1
+ export {
2
+ Select,
3
+ SelectGroup,
4
+ SelectValue,
5
+ SelectTrigger,
6
+ SelectContent,
7
+ SelectItem,
8
+ SelectLabel,
9
+ SelectSeparator,
10
+ } from "./Select";
@@ -0,0 +1,34 @@
1
+ "use client";
2
+
3
+ import { forwardRef, type HTMLAttributes } from "react";
4
+ import { cn } from "../../utils/cn";
5
+
6
+ export interface SkeletonProps extends HTMLAttributes<HTMLDivElement> {}
7
+
8
+ /**
9
+ * Skeleton — a shimmering placeholder block.
10
+ *
11
+ * Renders a neutral block sized by `className` (width/height/radius) with
12
+ * a subtle pulse animation between `bg-surface-2` and `bg-surface`.
13
+ *
14
+ * @example
15
+ * <Skeleton className="h-4 w-48 rounded-md" />
16
+ * <Skeleton className="h-32 w-full rounded-xl" />
17
+ */
18
+ export const Skeleton = forwardRef<HTMLDivElement, SkeletonProps>(
19
+ ({ className, ...props }, ref) => {
20
+ return (
21
+ <div
22
+ ref={ref}
23
+ aria-hidden="true"
24
+ className={cn(
25
+ "animate-pulse rounded-md bg-surface-2",
26
+ className
27
+ )}
28
+ {...props}
29
+ />
30
+ );
31
+ }
32
+ );
33
+
34
+ Skeleton.displayName = "Skeleton";
@@ -0,0 +1,2 @@
1
+ export { Skeleton } from "./Skeleton";
2
+ export type { SkeletonProps } from "./Skeleton";
@@ -0,0 +1,115 @@
1
+ "use client";
2
+
3
+ import { useState, type ReactNode } from "react";
4
+ import { motion, AnimatePresence } from "framer-motion";
5
+ import { cn } from "../../utils/cn";
6
+ import { buttonVariants, type ButtonVariant, type ButtonSize } from "../button/button.styles";
7
+
8
+ export interface StatefulButtonProps {
9
+ children: ReactNode;
10
+ /** Async function called on click. Button shows loading then success. */
11
+ onClick?: () => Promise<void> | void;
12
+ variant?: ButtonVariant;
13
+ size?: ButtonSize;
14
+ className?: string;
15
+ /** Success message shown briefly after completion (default "完成") */
16
+ successText?: string;
17
+ /** Duration of success state before resetting (default 1500ms) */
18
+ successDuration?: number;
19
+ disabled?: boolean;
20
+ }
21
+
22
+ type State = "idle" | "loading" | "success";
23
+
24
+ /**
25
+ * StatefulButton — auto-manages idle → loading → success lifecycle.
26
+ *
27
+ * Wrap any async action: the button shows a spinner during execution,
28
+ * then briefly displays a checkmark + success text before resetting.
29
+ * Built on zmzai Button variants + framer-motion state transitions.
30
+ *
31
+ * @example
32
+ * <StatefulButton onClick={async () => { await deploy(); }}>
33
+ * 部署
34
+ * </StatefulButton>
35
+ */
36
+ export function StatefulButton({
37
+ children,
38
+ onClick,
39
+ variant = "primary",
40
+ size = "md",
41
+ className,
42
+ successText = "完成",
43
+ successDuration = 1500,
44
+ disabled,
45
+ }: StatefulButtonProps) {
46
+ const [state, setState] = useState<State>("idle");
47
+
48
+ const handleClick = async () => {
49
+ if (state !== "idle" || disabled) return;
50
+ setState("loading");
51
+ try {
52
+ await onClick?.();
53
+ setState("success");
54
+ setTimeout(() => setState("idle"), successDuration);
55
+ } catch {
56
+ setState("idle");
57
+ }
58
+ };
59
+
60
+ return (
61
+ <motion.button
62
+ onClick={handleClick}
63
+ disabled={disabled || state === "loading"}
64
+ whileHover={disabled || state !== "idle" ? undefined : { scale: 1.03 }}
65
+ whileTap={disabled || state !== "idle" ? undefined : { scale: 0.98 }}
66
+ transition={{ duration: 0.15, ease: [0.16, 1, 0.3, 1] }}
67
+ className={cn(buttonVariants({ variant, size }), "overflow-hidden", className)}
68
+ >
69
+ <AnimatePresence mode="wait" initial={false}>
70
+ {state === "loading" && (
71
+ <motion.span
72
+ key="loading"
73
+ initial={{ opacity: 0, y: 8 }}
74
+ animate={{ opacity: 1, y: 0 }}
75
+ exit={{ opacity: 0, y: -8 }}
76
+ transition={{ duration: 0.15 }}
77
+ className="flex items-center gap-2"
78
+ >
79
+ <svg className="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
80
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="3" />
81
+ <path className="opacity-90" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
82
+ </svg>
83
+ 处理中…
84
+ </motion.span>
85
+ )}
86
+ {state === "success" && (
87
+ <motion.span
88
+ key="success"
89
+ initial={{ opacity: 0, y: 8 }}
90
+ animate={{ opacity: 1, y: 0 }}
91
+ exit={{ opacity: 0, y: -8 }}
92
+ transition={{ duration: 0.15 }}
93
+ className="flex items-center gap-2"
94
+ >
95
+ <svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
96
+ <polyline points="20 6 9 17 4 12" />
97
+ </svg>
98
+ {successText}
99
+ </motion.span>
100
+ )}
101
+ {state === "idle" && (
102
+ <motion.span
103
+ key="idle"
104
+ initial={{ opacity: 0, y: 8 }}
105
+ animate={{ opacity: 1, y: 0 }}
106
+ exit={{ opacity: 0, y: -8 }}
107
+ transition={{ duration: 0.15 }}
108
+ >
109
+ {children}
110
+ </motion.span>
111
+ )}
112
+ </AnimatePresence>
113
+ </motion.button>
114
+ );
115
+ }
@@ -0,0 +1,2 @@
1
+ export { StatefulButton } from "./StatefulButton";
2
+ export type { StatefulButtonProps } from "./StatefulButton";
@@ -0,0 +1,74 @@
1
+ "use client";
2
+
3
+ import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
4
+ import { cn } from "../../utils/cn";
5
+
6
+ export type BannerVariant = "info" | "warning" | "danger";
7
+
8
+ export interface StickyBannerProps extends HTMLAttributes<HTMLDivElement> {
9
+ /** Banner content */
10
+ children?: ReactNode;
11
+ /** Visual severity */
12
+ variant?: BannerVariant;
13
+ /** Called when the X button is clicked */
14
+ onDismiss?: () => void;
15
+ }
16
+
17
+ const variantStyles: Record<BannerVariant, string> = {
18
+ info: "bg-surface text-ink border-line",
19
+ warning: "bg-ink text-white border-ink",
20
+ danger: "bg-bg text-ink border-ink border-2",
21
+ };
22
+
23
+ /**
24
+ * StickyBanner — a dismissible banner that sticks to the top.
25
+ *
26
+ * Renders a full-width bar pinned to the top of the viewport with an
27
+ * X button. Use `variant` to convey severity. Stays in place on scroll.
28
+ *
29
+ * @example
30
+ * <StickyBanner variant="warning" onDismiss={() => setGone(true)}>
31
+ * 系统将于今晚维护
32
+ * </StickyBanner>
33
+ */
34
+ export const StickyBanner = forwardRef<HTMLDivElement, StickyBannerProps>(
35
+ ({ children, variant = "info", onDismiss, className, ...props }, ref) => {
36
+ return (
37
+ <div
38
+ ref={ref}
39
+ role="banner"
40
+ className={cn(
41
+ "sticky top-0 z-40 flex w-full items-center justify-between gap-3 border-b px-4 py-2.5 text-sm",
42
+ variantStyles[variant],
43
+ className
44
+ )}
45
+ {...props}
46
+ >
47
+ <div className="flex-1">{children}</div>
48
+ {onDismiss && (
49
+ <button
50
+ type="button"
51
+ aria-label="dismiss"
52
+ onClick={onDismiss}
53
+ className="shrink-0 rounded-md p-1 text-current opacity-60 transition-opacity hover:opacity-100"
54
+ >
55
+ <svg
56
+ className="h-4 w-4"
57
+ viewBox="0 0 24 24"
58
+ fill="none"
59
+ stroke="currentColor"
60
+ strokeWidth="2"
61
+ strokeLinecap="round"
62
+ strokeLinejoin="round"
63
+ >
64
+ <line x1="18" y1="6" x2="6" y2="18" />
65
+ <line x1="6" y1="6" x2="18" y2="18" />
66
+ </svg>
67
+ </button>
68
+ )}
69
+ </div>
70
+ );
71
+ }
72
+ );
73
+
74
+ StickyBanner.displayName = "StickyBanner";
@@ -0,0 +1,2 @@
1
+ export { StickyBanner } from "./StickyBanner";
2
+ export type { StickyBannerProps, BannerVariant } from "./StickyBanner";