@smicolon/fasla-ui 0.2.2

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 (74) hide show
  1. package/README.md +141 -0
  2. package/bin/fasla-ui.mjs +54 -0
  3. package/dist/index.d.ts +5 -0
  4. package/dist/index.js +241 -0
  5. package/dist/lib/utils.d.ts +9 -0
  6. package/dist/lib/utils.js +9 -0
  7. package/dist/motion/index.d.ts +33 -0
  8. package/dist/motion/index.js +59 -0
  9. package/dist/registry/index.d.ts +17 -0
  10. package/dist/registry/index.js +145 -0
  11. package/dist/tokens/index.d.ts +55 -0
  12. package/dist/tokens/index.js +34 -0
  13. package/package.json +97 -0
  14. package/registry/blocks/app-shell/AppShell.tsx +66 -0
  15. package/registry/blocks/app-shell/index.ts +1 -0
  16. package/registry/blocks/data-table/DataTable.tsx +292 -0
  17. package/registry/blocks/data-table/index.ts +7 -0
  18. package/registry/blocks/empty-state/EmptyState.test.tsx +120 -0
  19. package/registry/blocks/empty-state/EmptyState.tsx +164 -0
  20. package/registry/blocks/empty-state/index.ts +6 -0
  21. package/registry/blocks/form-section/FormSection.tsx +140 -0
  22. package/registry/blocks/form-section/index.ts +8 -0
  23. package/registry/blocks/navbar/Navbar.tsx +190 -0
  24. package/registry/blocks/navbar/index.ts +16 -0
  25. package/registry/blocks/page-header/PageHeader.tsx +64 -0
  26. package/registry/blocks/page-header/index.ts +1 -0
  27. package/registry/blocks/sidebar/Sidebar.tsx +202 -0
  28. package/registry/blocks/sidebar/index.ts +16 -0
  29. package/registry/blocks/stats-card/StatsCard.tsx +134 -0
  30. package/registry/blocks/stats-card/index.ts +6 -0
  31. package/registry/effects/animated-gradient/animated-gradient.tsx +136 -0
  32. package/registry/effects/animated-gradient/index.ts +6 -0
  33. package/registry/effects/border-beam/border-beam.tsx +132 -0
  34. package/registry/effects/border-beam/index.ts +6 -0
  35. package/registry/effects/glow-card/glow-card.tsx +145 -0
  36. package/registry/effects/glow-card/index.ts +6 -0
  37. package/registry/effects/shimmer-button/index.ts +1 -0
  38. package/registry/effects/shimmer-button/shimmer-button.tsx +91 -0
  39. package/registry/effects/spotlight/index.ts +6 -0
  40. package/registry/effects/spotlight/spotlight.tsx +162 -0
  41. package/registry/effects/text-reveal/index.ts +6 -0
  42. package/registry/effects/text-reveal/text-reveal.tsx +139 -0
  43. package/registry/effects/typewriter-text/index.ts +6 -0
  44. package/registry/effects/typewriter-text/typewriter-text.tsx +220 -0
  45. package/registry/ui/avatar/avatar.tsx +135 -0
  46. package/registry/ui/avatar/index.ts +1 -0
  47. package/registry/ui/badge/badge.test.tsx +51 -0
  48. package/registry/ui/badge/badge.tsx +39 -0
  49. package/registry/ui/badge/index.ts +1 -0
  50. package/registry/ui/button/button.test.tsx +87 -0
  51. package/registry/ui/button/button.tsx +88 -0
  52. package/registry/ui/button/index.ts +1 -0
  53. package/registry/ui/card/card.test.tsx +119 -0
  54. package/registry/ui/card/card.tsx +75 -0
  55. package/registry/ui/card/index.ts +8 -0
  56. package/registry/ui/checkbox/checkbox.tsx +113 -0
  57. package/registry/ui/checkbox/index.ts +1 -0
  58. package/registry/ui/combobox/combobox.tsx +410 -0
  59. package/registry/ui/combobox/index.ts +1 -0
  60. package/registry/ui/input/index.ts +1 -0
  61. package/registry/ui/input/input.test.tsx +78 -0
  62. package/registry/ui/input/input.tsx +76 -0
  63. package/registry/ui/select/index.ts +1 -0
  64. package/registry/ui/select/select.tsx +94 -0
  65. package/registry/ui/skeleton/index.ts +7 -0
  66. package/registry/ui/skeleton/skeleton.test.tsx +104 -0
  67. package/registry/ui/skeleton/skeleton.tsx +88 -0
  68. package/registry/ui/switch/index.ts +1 -0
  69. package/registry/ui/switch/switch.tsx +89 -0
  70. package/registry/ui/tabs/index.ts +10 -0
  71. package/registry/ui/tabs/tabs.tsx +150 -0
  72. package/registry/ui/textarea/index.ts +1 -0
  73. package/registry/ui/textarea/textarea.tsx +73 -0
  74. package/registry.json +247 -0
@@ -0,0 +1,145 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../../src/lib/utils"
5
+
6
+ export interface GlowCardProps extends React.HTMLAttributes<HTMLDivElement> {
7
+ /** Glow color (any CSS color) */
8
+ glowColor?: string
9
+ /** Glow intensity (blur radius in pixels) */
10
+ glowIntensity?: number
11
+ /** Whether glow follows mouse */
12
+ followMouse?: boolean
13
+ /** Whether to show glow on hover only */
14
+ hoverOnly?: boolean
15
+ children: React.ReactNode
16
+ }
17
+
18
+ export function GlowCard({
19
+ glowColor = "hsl(var(--primary))",
20
+ glowIntensity = 60,
21
+ followMouse = false,
22
+ hoverOnly = true,
23
+ className,
24
+ children,
25
+ ...props
26
+ }: GlowCardProps) {
27
+ const cardRef = React.useRef<HTMLDivElement>(null)
28
+ const [mousePosition, setMousePosition] = React.useState({ x: 50, y: 50 })
29
+ const [isHovering, setIsHovering] = React.useState(false)
30
+
31
+ // Check for reduced motion preference
32
+ const prefersReducedMotion = React.useMemo(() => {
33
+ if (typeof window === "undefined") return false
34
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches
35
+ }, [])
36
+
37
+ const handleMouseMove = React.useCallback(
38
+ (e: React.MouseEvent<HTMLDivElement>) => {
39
+ if (!followMouse || !cardRef.current || prefersReducedMotion) return
40
+
41
+ const rect = cardRef.current.getBoundingClientRect()
42
+ const x = ((e.clientX - rect.left) / rect.width) * 100
43
+ const y = ((e.clientY - rect.top) / rect.height) * 100
44
+ setMousePosition({ x, y })
45
+ },
46
+ [followMouse, prefersReducedMotion]
47
+ )
48
+
49
+ const showGlow = prefersReducedMotion ? false : hoverOnly ? isHovering : true
50
+
51
+ return (
52
+ <div
53
+ ref={cardRef}
54
+ onMouseMove={handleMouseMove}
55
+ onMouseEnter={() => setIsHovering(true)}
56
+ onMouseLeave={() => setIsHovering(false)}
57
+ className={cn("relative overflow-hidden rounded-xl", className)}
58
+ {...props}
59
+ >
60
+ {/* Glow effect */}
61
+ <div
62
+ className={cn(
63
+ "pointer-events-none absolute -inset-px rounded-xl opacity-0 transition-opacity duration-300",
64
+ showGlow && "opacity-100"
65
+ )}
66
+ style={{
67
+ background: followMouse
68
+ ? `radial-gradient(circle at ${mousePosition.x}% ${mousePosition.y}%, ${glowColor}, transparent 70%)`
69
+ : `radial-gradient(circle at 50% 50%, ${glowColor}, transparent 70%)`,
70
+ filter: `blur(${glowIntensity}px)`,
71
+ }}
72
+ />
73
+
74
+ {/* Border glow */}
75
+ <div
76
+ className={cn(
77
+ "pointer-events-none absolute inset-0 rounded-xl opacity-0 transition-opacity duration-300",
78
+ showGlow && "opacity-50"
79
+ )}
80
+ style={{
81
+ background: followMouse
82
+ ? `radial-gradient(circle at ${mousePosition.x}% ${mousePosition.y}%, ${glowColor}, transparent 50%)`
83
+ : undefined,
84
+ padding: "1px",
85
+ WebkitMask:
86
+ "linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)",
87
+ WebkitMaskComposite: "xor",
88
+ maskComposite: "exclude",
89
+ }}
90
+ />
91
+
92
+ {/* Card content */}
93
+ <div className="relative z-10 rounded-xl border bg-card">
94
+ {children}
95
+ </div>
96
+ </div>
97
+ )
98
+ }
99
+
100
+ export interface GlowContainerProps extends React.HTMLAttributes<HTMLDivElement> {
101
+ /** Glow color */
102
+ glowColor?: string
103
+ /** Animation duration in seconds */
104
+ duration?: number
105
+ children: React.ReactNode
106
+ }
107
+
108
+ export function GlowContainer({
109
+ glowColor = "hsl(var(--primary))",
110
+ duration = 3,
111
+ className,
112
+ children,
113
+ ...props
114
+ }: GlowContainerProps) {
115
+ const prefersReducedMotion = React.useMemo(() => {
116
+ if (typeof window === "undefined") return false
117
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches
118
+ }, [])
119
+
120
+ return (
121
+ <div
122
+ className={cn("relative overflow-hidden rounded-xl p-px", className)}
123
+ style={
124
+ !prefersReducedMotion
125
+ ? {
126
+ background: `linear-gradient(90deg, ${glowColor}, transparent, ${glowColor})`,
127
+ backgroundSize: "200% 100%",
128
+ animation: `glow-slide ${duration}s linear infinite`,
129
+ }
130
+ : { background: glowColor }
131
+ }
132
+ {...props}
133
+ >
134
+ <style dangerouslySetInnerHTML={{ __html: `
135
+ @keyframes glow-slide {
136
+ 0% { background-position: 200% 0; }
137
+ 100% { background-position: -200% 0; }
138
+ }
139
+ ` }} />
140
+ <div className="relative z-10 rounded-[11px] bg-background">
141
+ {children}
142
+ </div>
143
+ </div>
144
+ )
145
+ }
@@ -0,0 +1,6 @@
1
+ export {
2
+ GlowCard,
3
+ GlowContainer,
4
+ type GlowCardProps,
5
+ type GlowContainerProps,
6
+ } from "./glow-card"
@@ -0,0 +1 @@
1
+ export { ShimmerButton, type ShimmerButtonProps } from "./shimmer-button"
@@ -0,0 +1,91 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { useReducedMotion } from "framer-motion"
5
+ import { cn } from "../../../src/lib/utils"
6
+
7
+ export interface ShimmerButtonProps
8
+ extends React.ButtonHTMLAttributes<HTMLButtonElement> {
9
+ /** Shimmer color */
10
+ shimmerColor?: string
11
+ /** Shimmer size in pixels */
12
+ shimmerSize?: string
13
+ /** Border radius */
14
+ borderRadius?: string
15
+ /** Shimmer duration in seconds */
16
+ shimmerDuration?: string
17
+ /** Background color */
18
+ background?: string
19
+ }
20
+
21
+ /**
22
+ * A button with an animated shimmer effect.
23
+ * The shimmer respects prefers-reduced-motion.
24
+ */
25
+ export const ShimmerButton = React.forwardRef<
26
+ HTMLButtonElement,
27
+ ShimmerButtonProps
28
+ >(
29
+ (
30
+ {
31
+ shimmerColor = "hsl(var(--primary) / 0.2)",
32
+ shimmerSize = "0.1em",
33
+ shimmerDuration = "2s",
34
+ borderRadius = "0.5rem",
35
+ background = "hsl(var(--primary))",
36
+ className,
37
+ children,
38
+ ...props
39
+ },
40
+ ref
41
+ ) => {
42
+ const prefersReducedMotion = useReducedMotion()
43
+
44
+ return (
45
+ <button
46
+ ref={ref}
47
+ className={cn(
48
+ "group relative inline-flex h-10 items-center justify-center overflow-hidden whitespace-nowrap px-6 py-2 text-sm font-medium text-primary-foreground transition-all hover:scale-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
49
+ className
50
+ )}
51
+ style={
52
+ {
53
+ "--shimmer-color": shimmerColor,
54
+ "--shimmer-size": shimmerSize,
55
+ "--shimmer-duration": shimmerDuration,
56
+ "--border-radius": borderRadius,
57
+ "--background": background,
58
+ borderRadius: "var(--border-radius)",
59
+ background: "var(--background)",
60
+ } as React.CSSProperties
61
+ }
62
+ {...props}
63
+ >
64
+ {/* Shimmer effect */}
65
+ {!prefersReducedMotion && (
66
+ <div
67
+ className="absolute inset-0 overflow-hidden"
68
+ style={{ borderRadius: "var(--border-radius)" }}
69
+ >
70
+ <div
71
+ className="animate-shimmer absolute inset-0 -translate-x-full"
72
+ style={{
73
+ background: `linear-gradient(
74
+ 90deg,
75
+ transparent 0%,
76
+ var(--shimmer-color) 50%,
77
+ transparent 100%
78
+ )`,
79
+ animationDuration: "var(--shimmer-duration)",
80
+ }}
81
+ />
82
+ </div>
83
+ )}
84
+
85
+ {/* Content */}
86
+ <span className="relative z-10 flex items-center gap-2">{children}</span>
87
+ </button>
88
+ )
89
+ }
90
+ )
91
+ ShimmerButton.displayName = "ShimmerButton"
@@ -0,0 +1,6 @@
1
+ export {
2
+ Spotlight,
3
+ SpotlightCard,
4
+ type SpotlightProps,
5
+ type SpotlightCardProps,
6
+ } from "./spotlight"
@@ -0,0 +1,162 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { motion, useMotionValue, useSpring, useReducedMotion } from "framer-motion"
5
+ import { cn } from "../../../src/lib/utils"
6
+
7
+ export interface SpotlightProps extends React.HTMLAttributes<HTMLDivElement> {
8
+ /** Spotlight color */
9
+ color?: string
10
+ /** Spotlight size in pixels */
11
+ size?: number
12
+ /** Spotlight blur in pixels */
13
+ blur?: number
14
+ /** Opacity of the spotlight */
15
+ opacity?: number
16
+ }
17
+
18
+ /**
19
+ * Cursor-following spotlight effect.
20
+ * Respects prefers-reduced-motion by disabling the effect.
21
+ */
22
+ export function Spotlight({
23
+ color = "hsl(var(--primary) / 0.15)",
24
+ size = 400,
25
+ blur = 80,
26
+ opacity = 1,
27
+ className,
28
+ children,
29
+ ...props
30
+ }: SpotlightProps) {
31
+ const prefersReducedMotion = useReducedMotion()
32
+ const containerRef = React.useRef<HTMLDivElement>(null)
33
+
34
+ const mouseX = useMotionValue(0)
35
+ const mouseY = useMotionValue(0)
36
+
37
+ const springConfig = { damping: 25, stiffness: 200 }
38
+ const springX = useSpring(mouseX, springConfig)
39
+ const springY = useSpring(mouseY, springConfig)
40
+
41
+ const handleMouseMove = React.useCallback(
42
+ (e: React.MouseEvent<HTMLDivElement>) => {
43
+ if (!containerRef.current || prefersReducedMotion) return
44
+
45
+ const rect = containerRef.current.getBoundingClientRect()
46
+ const x = e.clientX - rect.left - size / 2
47
+ const y = e.clientY - rect.top - size / 2
48
+
49
+ mouseX.set(x)
50
+ mouseY.set(y)
51
+ },
52
+ [mouseX, mouseY, size, prefersReducedMotion]
53
+ )
54
+
55
+ if (prefersReducedMotion) {
56
+ return (
57
+ <div className={cn("relative", className)} {...props}>
58
+ {children}
59
+ </div>
60
+ )
61
+ }
62
+
63
+ return (
64
+ <div
65
+ ref={containerRef}
66
+ onMouseMove={handleMouseMove}
67
+ className={cn("relative overflow-hidden", className)}
68
+ {...props}
69
+ >
70
+ {/* Spotlight */}
71
+ <motion.div
72
+ className="pointer-events-none absolute -z-10 rounded-full"
73
+ style={{
74
+ x: springX,
75
+ y: springY,
76
+ width: size,
77
+ height: size,
78
+ background: `radial-gradient(circle, ${color} 0%, transparent 70%)`,
79
+ filter: `blur(${blur}px)`,
80
+ opacity,
81
+ }}
82
+ />
83
+
84
+ {/* Content */}
85
+ {children}
86
+ </div>
87
+ )
88
+ }
89
+
90
+ export interface SpotlightCardProps extends React.HTMLAttributes<HTMLDivElement> {
91
+ /** Spotlight color */
92
+ spotlightColor?: string
93
+ /** Spotlight size */
94
+ spotlightSize?: number
95
+ }
96
+
97
+ /**
98
+ * Card with spotlight effect on hover.
99
+ */
100
+ export function SpotlightCard({
101
+ spotlightColor = "hsl(var(--primary) / 0.1)",
102
+ spotlightSize = 300,
103
+ className,
104
+ children,
105
+ ...props
106
+ }: SpotlightCardProps) {
107
+ const prefersReducedMotion = useReducedMotion()
108
+ const containerRef = React.useRef<HTMLDivElement>(null)
109
+ const [isHovered, setIsHovered] = React.useState(false)
110
+
111
+ const mouseX = useMotionValue(0)
112
+ const mouseY = useMotionValue(0)
113
+
114
+ const handleMouseMove = React.useCallback(
115
+ (e: React.MouseEvent<HTMLDivElement>) => {
116
+ if (!containerRef.current || prefersReducedMotion) return
117
+
118
+ const rect = containerRef.current.getBoundingClientRect()
119
+ const x = e.clientX - rect.left
120
+ const y = e.clientY - rect.top
121
+
122
+ mouseX.set(x)
123
+ mouseY.set(y)
124
+ },
125
+ [mouseX, mouseY, prefersReducedMotion]
126
+ )
127
+
128
+ return (
129
+ <div
130
+ ref={containerRef}
131
+ onMouseMove={handleMouseMove}
132
+ onMouseEnter={() => setIsHovered(true)}
133
+ onMouseLeave={() => setIsHovered(false)}
134
+ className={cn(
135
+ "relative overflow-hidden rounded-xl border bg-card p-6 shadow",
136
+ className
137
+ )}
138
+ {...props}
139
+ >
140
+ {/* Spotlight */}
141
+ {!prefersReducedMotion && isHovered && (
142
+ <motion.div
143
+ className="pointer-events-none absolute -z-10 rounded-full"
144
+ style={{
145
+ x: mouseX,
146
+ y: mouseY,
147
+ width: spotlightSize,
148
+ height: spotlightSize,
149
+ background: `radial-gradient(circle, ${spotlightColor} 0%, transparent 70%)`,
150
+ transform: "translate(-50%, -50%)",
151
+ }}
152
+ initial={{ opacity: 0 }}
153
+ animate={{ opacity: 1 }}
154
+ exit={{ opacity: 0 }}
155
+ />
156
+ )}
157
+
158
+ {/* Content */}
159
+ {children}
160
+ </div>
161
+ )
162
+ }
@@ -0,0 +1,6 @@
1
+ export {
2
+ TextReveal,
3
+ WordReveal,
4
+ type TextRevealProps,
5
+ type WordRevealProps,
6
+ } from "./text-reveal"
@@ -0,0 +1,139 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { motion, useReducedMotion, useInView } from "framer-motion"
5
+ import { cn } from "../../../src/lib/utils"
6
+
7
+ export interface TextRevealProps extends React.HTMLAttributes<HTMLDivElement> {
8
+ /** Text to reveal */
9
+ text: string
10
+ /** Delay between each character in seconds */
11
+ delay?: number
12
+ /** Duration of each character animation */
13
+ duration?: number
14
+ /** Whether to trigger on view */
15
+ triggerOnView?: boolean
16
+ }
17
+
18
+ /**
19
+ * Text reveal animation that reveals text character by character.
20
+ * Respects prefers-reduced-motion by showing text immediately.
21
+ */
22
+ export function TextReveal({
23
+ text,
24
+ delay = 0.03,
25
+ duration = 0.3,
26
+ triggerOnView = true,
27
+ className,
28
+ ...props
29
+ }: TextRevealProps) {
30
+ const prefersReducedMotion = useReducedMotion()
31
+ const ref = React.useRef<HTMLDivElement>(null)
32
+ const isInView = useInView(ref, { once: true, amount: 0.5 })
33
+
34
+ const shouldAnimate = triggerOnView ? isInView : true
35
+ const characters = text.split("")
36
+
37
+ if (prefersReducedMotion) {
38
+ return (
39
+ <div className={className} {...props}>
40
+ {text}
41
+ </div>
42
+ )
43
+ }
44
+
45
+ return (
46
+ <div
47
+ ref={ref}
48
+ className={cn("inline-block", className)}
49
+ {...props}
50
+ >
51
+ {characters.map((char, i) => (
52
+ <motion.span
53
+ key={`${char}-${i}`}
54
+ className="inline-block"
55
+ initial={{ opacity: 0, y: 10 }}
56
+ animate={
57
+ shouldAnimate
58
+ ? { opacity: 1, y: 0 }
59
+ : { opacity: 0, y: 10 }
60
+ }
61
+ transition={{
62
+ duration,
63
+ delay: i * delay,
64
+ ease: [0.2, 0.65, 0.3, 0.9],
65
+ }}
66
+ >
67
+ {char === " " ? "\u00A0" : char}
68
+ </motion.span>
69
+ ))}
70
+ </div>
71
+ )
72
+ }
73
+
74
+ export interface WordRevealProps extends React.HTMLAttributes<HTMLDivElement> {
75
+ /** Text to reveal */
76
+ text: string
77
+ /** Delay between each word in seconds */
78
+ delay?: number
79
+ /** Duration of each word animation */
80
+ duration?: number
81
+ /** Whether to trigger on view */
82
+ triggerOnView?: boolean
83
+ }
84
+
85
+ /**
86
+ * Word-by-word reveal animation.
87
+ */
88
+ export function WordReveal({
89
+ text,
90
+ delay = 0.1,
91
+ duration = 0.4,
92
+ triggerOnView = true,
93
+ className,
94
+ ...props
95
+ }: WordRevealProps) {
96
+ const prefersReducedMotion = useReducedMotion()
97
+ const ref = React.useRef<HTMLDivElement>(null)
98
+ const isInView = useInView(ref, { once: true, amount: 0.5 })
99
+
100
+ const shouldAnimate = triggerOnView ? isInView : true
101
+ const words = text.split(" ")
102
+
103
+ if (prefersReducedMotion) {
104
+ return (
105
+ <div className={className} {...props}>
106
+ {text}
107
+ </div>
108
+ )
109
+ }
110
+
111
+ return (
112
+ <div
113
+ ref={ref}
114
+ className={cn("inline-block", className)}
115
+ {...props}
116
+ >
117
+ {words.map((word, i) => (
118
+ <motion.span
119
+ key={`${word}-${i}`}
120
+ className="inline-block"
121
+ initial={{ opacity: 0, y: 20, filter: "blur(10px)" }}
122
+ animate={
123
+ shouldAnimate
124
+ ? { opacity: 1, y: 0, filter: "blur(0px)" }
125
+ : { opacity: 0, y: 20, filter: "blur(10px)" }
126
+ }
127
+ transition={{
128
+ duration,
129
+ delay: i * delay,
130
+ ease: [0.2, 0.65, 0.3, 0.9],
131
+ }}
132
+ >
133
+ {word}
134
+ {i < words.length - 1 && "\u00A0"}
135
+ </motion.span>
136
+ ))}
137
+ </div>
138
+ )
139
+ }
@@ -0,0 +1,6 @@
1
+ export {
2
+ TypewriterText,
3
+ TypewriterWords,
4
+ type TypewriterTextProps,
5
+ type TypewriterWordsProps,
6
+ } from "./typewriter-text"