@smicolon/fasla-ui 0.2.4 → 0.4.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 (72) hide show
  1. package/README.md +44 -20
  2. package/dist/index.cjs +319 -0
  3. package/dist/index.d.cts +5 -0
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +92 -24
  6. package/dist/lib/utils.cjs +11 -0
  7. package/dist/lib/utils.d.cts +9 -0
  8. package/dist/motion/index.cjs +61 -0
  9. package/dist/motion/index.d.cts +33 -0
  10. package/dist/registry/index.cjs +179 -0
  11. package/dist/registry/index.d.cts +17 -0
  12. package/dist/registry/index.js +32 -3
  13. package/dist/tokens/index.cjs +77 -0
  14. package/dist/tokens/index.d.cts +124 -0
  15. package/dist/tokens/index.d.ts +75 -6
  16. package/dist/tokens/index.js +59 -20
  17. package/package.json +17 -7
  18. package/registry/blocks/app-shell/AppShell.tsx +1 -1
  19. package/registry/blocks/data-table/DataTable.test.tsx +61 -0
  20. package/registry/blocks/data-table/DataTable.tsx +53 -16
  21. package/registry/blocks/data-table/index.ts +1 -0
  22. package/registry/blocks/empty-state/EmptyState.test.tsx +23 -0
  23. package/registry/blocks/empty-state/EmptyState.tsx +32 -33
  24. package/registry/blocks/form-section/FormSection.tsx +1 -1
  25. package/registry/blocks/navbar/Navbar.test.tsx +12 -0
  26. package/registry/blocks/navbar/Navbar.tsx +8 -23
  27. package/registry/blocks/page-header/PageHeader.tsx +5 -2
  28. package/registry/blocks/sidebar/Sidebar.test.tsx +39 -0
  29. package/registry/blocks/sidebar/Sidebar.tsx +36 -26
  30. package/registry/blocks/stats-card/StatsCard.tsx +12 -29
  31. package/registry/effects/animated-gradient/animated-gradient.tsx +19 -8
  32. package/registry/effects/border-beam/border-beam.tsx +77 -53
  33. package/registry/effects/effects.test.tsx +266 -0
  34. package/registry/effects/glow-card/glow-card.tsx +68 -43
  35. package/registry/effects/shimmer-button/shimmer-button.tsx +62 -23
  36. package/registry/effects/spotlight/spotlight.tsx +73 -29
  37. package/registry/effects/text-reveal/index.ts +1 -0
  38. package/registry/effects/text-reveal/text-reveal.test.tsx +66 -0
  39. package/registry/effects/text-reveal/text-reveal.tsx +70 -21
  40. package/registry/effects/typewriter-text/typewriter-text.tsx +39 -20
  41. package/registry/next14-tsconfig.test.ts +43 -0
  42. package/registry/text-props-undefined.test.tsx +73 -0
  43. package/registry/ui/avatar/avatar.test.tsx +316 -0
  44. package/registry/ui/avatar/avatar.tsx +235 -108
  45. package/registry/ui/avatar/index.ts +1 -1
  46. package/registry/ui/badge/badge.test.tsx +267 -32
  47. package/registry/ui/badge/badge.tsx +233 -21
  48. package/registry/ui/button/button.test.tsx +6 -0
  49. package/registry/ui/button/button.tsx +6 -23
  50. package/registry/ui/card/card.tsx +1 -1
  51. package/registry/ui/checkbox/checkbox.test.tsx +23 -0
  52. package/registry/ui/checkbox/checkbox.tsx +16 -27
  53. package/registry/ui/combobox/combobox.test.tsx +72 -0
  54. package/registry/ui/combobox/combobox.tsx +34 -59
  55. package/registry/ui/input/input.test.tsx +3 -3
  56. package/registry/ui/input/input.tsx +5 -5
  57. package/registry/ui/radio/index.ts +1 -0
  58. package/registry/ui/radio/radio.test.tsx +248 -0
  59. package/registry/ui/radio/radio.tsx +208 -0
  60. package/registry/ui/select/select.tsx +4 -14
  61. package/registry/ui/skeleton/index.ts +1 -0
  62. package/registry/ui/skeleton/skeleton.test.tsx +113 -29
  63. package/registry/ui/skeleton/skeleton.tsx +101 -41
  64. package/registry/ui/status-indicator/index.ts +6 -0
  65. package/registry/ui/status-indicator/status-indicator.test.tsx +148 -0
  66. package/registry/ui/status-indicator/status-indicator.tsx +87 -0
  67. package/registry/ui/switch/index.ts +1 -1
  68. package/registry/ui/switch/switch.test.tsx +323 -0
  69. package/registry/ui/switch/switch.tsx +254 -57
  70. package/registry/ui/textarea/textarea.tsx +1 -1
  71. package/registry.json +79 -16
  72. package/tailwind-preset.ts +143 -0
@@ -1,6 +1,7 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from "react"
4
+ import { ChevronsLeftIcon } from "lucide-react"
4
5
  import { cn } from "../../../src/lib/utils"
5
6
 
6
7
  export interface SidebarProps extends React.HTMLAttributes<HTMLElement> {
@@ -19,6 +20,11 @@ const widthClasses = {
19
20
  lg: "w-72",
20
21
  }
21
22
 
23
+ /** The enclosing Sidebar's onCollapsedChange, for SidebarCollapseButton. */
24
+ const SidebarCollapsedChangeContext = React.createContext<
25
+ ((collapsed: boolean) => void) | undefined
26
+ >(undefined)
27
+
22
28
  export function Sidebar({
23
29
  collapsed = false,
24
30
  onCollapsedChange,
@@ -28,16 +34,18 @@ export function Sidebar({
28
34
  ...props
29
35
  }: SidebarProps) {
30
36
  return (
31
- <aside
32
- className={cn(
33
- "flex h-full flex-col border-r bg-background transition-all duration-200",
34
- collapsed ? "w-16" : widthClasses[width],
35
- className
36
- )}
37
- {...props}
38
- >
39
- {children}
40
- </aside>
37
+ <SidebarCollapsedChangeContext.Provider value={onCollapsedChange}>
38
+ <aside
39
+ className={cn(
40
+ "flex h-full flex-col border-e bg-background transition-all duration-200",
41
+ collapsed ? "w-16" : widthClasses[width],
42
+ className
43
+ )}
44
+ {...props}
45
+ >
46
+ {children}
47
+ </aside>
48
+ </SidebarCollapsedChangeContext.Provider>
41
49
  )
42
50
  }
43
51
 
@@ -116,7 +124,7 @@ export function SidebarGroup({
116
124
  return (
117
125
  <div className={cn("py-2", className)} {...props}>
118
126
  {label && (
119
- <div className="mb-2 px-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
127
+ <div className="mb-2 px-2 text-xs font-semibold uppercase text-muted-foreground">
120
128
  {label}
121
129
  </div>
122
130
  )}
@@ -173,9 +181,19 @@ export function SidebarCollapseButton({
173
181
  className,
174
182
  ...props
175
183
  }: SidebarCollapseButtonProps) {
184
+ const onSidebarCollapsedChange = React.useContext(SidebarCollapsedChangeContext)
185
+
186
+ // Tells the enclosing Sidebar too, once if both were given the same handler
187
+ const toggle = () => {
188
+ onCollapsedChange(!collapsed)
189
+ if (onSidebarCollapsedChange && onSidebarCollapsedChange !== onCollapsedChange) {
190
+ onSidebarCollapsedChange(!collapsed)
191
+ }
192
+ }
193
+
176
194
  return (
177
195
  <button
178
- onClick={() => onCollapsedChange(!collapsed)}
196
+ onClick={toggle}
179
197
  className={cn(
180
198
  "flex h-8 w-8 items-center justify-center rounded-md hover:bg-accent",
181
199
  className
@@ -183,20 +201,12 @@ export function SidebarCollapseButton({
183
201
  aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
184
202
  {...props}
185
203
  >
186
- <svg
187
- width="16"
188
- height="16"
189
- viewBox="0 0 24 24"
190
- fill="none"
191
- stroke="currentColor"
192
- strokeWidth="2"
193
- strokeLinecap="round"
194
- strokeLinejoin="round"
195
- className={cn("transition-transform", collapsed && "rotate-180")}
196
- >
197
- <polyline points="11 17 6 12 11 7" />
198
- <polyline points="18 17 13 12 18 7" />
199
- </svg>
204
+ {/* Points at the edge the sidebar collapses toward, so it mirrors in RTL. */}
205
+ <ChevronsLeftIcon
206
+ size={16}
207
+ strokeWidth={1.5}
208
+ className={cn("transition-transform motion-reduce:transition-none rtl:-scale-x-100", collapsed && "rotate-180")}
209
+ />
200
210
  </button>
201
211
  )
202
212
  }
@@ -1,4 +1,5 @@
1
1
  import * as React from "react"
2
+ import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"
2
3
  import { cn } from "../../../src/lib/utils"
3
4
 
4
5
  export interface StatsCardProps extends React.HTMLAttributes<HTMLDivElement> {
@@ -43,7 +44,7 @@ export function StatsCard({
43
44
  {loading ? (
44
45
  <div className="h-8 w-24 animate-pulse rounded bg-muted" />
45
46
  ) : (
46
- <p className="text-2xl font-bold tracking-tight">{value}</p>
47
+ <p className="text-2xl font-bold">{value}</p>
47
48
  )}
48
49
  </div>
49
50
  {icon && (
@@ -59,41 +60,23 @@ export function StatsCard({
59
60
  <span
60
61
  className={cn(
61
62
  "inline-flex items-center gap-1 font-medium",
62
- trend.direction === "up" && "text-green-600 dark:text-green-400",
63
- trend.direction === "down" && "text-red-600 dark:text-red-400",
63
+ // The tokens already change with the mode, so no dark: variants.
64
+ trend.direction === "up" && "text-success",
65
+ trend.direction === "down" && "text-destructive",
64
66
  trend.direction === "neutral" && "text-muted-foreground"
65
67
  )}
66
68
  >
67
69
  {trend.direction === "up" && (
68
- <svg
69
- width="16"
70
- height="16"
71
- viewBox="0 0 24 24"
72
- fill="none"
73
- stroke="currentColor"
74
- strokeWidth="2"
75
- strokeLinecap="round"
76
- strokeLinejoin="round"
77
- >
78
- <polyline points="18 15 12 9 6 15" />
79
- </svg>
70
+ <ChevronUpIcon size={16} strokeWidth={1.5} />
80
71
  )}
81
72
  {trend.direction === "down" && (
82
- <svg
83
- width="16"
84
- height="16"
85
- viewBox="0 0 24 24"
86
- fill="none"
87
- stroke="currentColor"
88
- strokeWidth="2"
89
- strokeLinecap="round"
90
- strokeLinejoin="round"
91
- >
92
- <polyline points="6 9 12 15 18 9" />
93
- </svg>
73
+ <ChevronDownIcon size={16} strokeWidth={1.5} />
94
74
  )}
95
- {trend.value > 0 ? "+" : ""}
96
- {trend.value}%
75
+ {/* One LTR unit: in RTL the sign would otherwise jump to the far end, "12.5%+". */}
76
+ <bdi dir="ltr">
77
+ {trend.value > 0 ? "+" : ""}
78
+ {trend.value}%
79
+ </bdi>
97
80
  </span>
98
81
  )}
99
82
  {description && (
@@ -4,6 +4,20 @@ import * as React from "react"
4
4
  import { motion, useReducedMotion } from "framer-motion"
5
5
  import { cn } from "../../../src/lib/utils"
6
6
 
7
+ /**
8
+ * The colours, without blank entries (a cleared control, say); the fallback
9
+ * when none are left, since an empty list would draw nothing.
10
+ */
11
+ const filledList = (colors: string[] | undefined, fallback: string[]) => {
12
+ const usable = (colors ?? []).filter((color) => typeof color === "string" && color.trim())
13
+ return usable.length ? usable : fallback
14
+ }
15
+
16
+ // Theme tokens only, so the gradient follows the project's palette and mode.
17
+ const GRADIENT_COLORS = ["var(--primary)", "var(--secondary)", "var(--chart-5)", "var(--info)"]
18
+
19
+ const TEXT_COLORS = ["var(--primary)", "var(--chart-5)", "var(--primary)"]
20
+
7
21
  export interface AnimatedGradientProps
8
22
  extends React.HTMLAttributes<HTMLDivElement> {
9
23
  /** Gradient colors */
@@ -28,12 +42,7 @@ const blurValues = {
28
42
  * Respects prefers-reduced-motion by stopping animation.
29
43
  */
30
44
  export function AnimatedGradient({
31
- colors = [
32
- "hsl(var(--primary))",
33
- "hsl(var(--secondary))",
34
- "hsl(280 100% 70%)",
35
- "hsl(200 100% 70%)",
36
- ],
45
+ colors: colorsProp,
37
46
  speed = 10,
38
47
  blur = "3xl",
39
48
  className,
@@ -41,10 +50,11 @@ export function AnimatedGradient({
41
50
  ...props
42
51
  }: AnimatedGradientProps) {
43
52
  const prefersReducedMotion = useReducedMotion()
53
+ const colors = filledList(colorsProp, GRADIENT_COLORS)
44
54
 
45
55
  return (
46
56
  <div
47
- className={cn("relative overflow-hidden", className)}
57
+ className={cn("relative isolate overflow-hidden", className)}
48
58
  {...props}
49
59
  >
50
60
  {/* Gradient blobs */}
@@ -100,12 +110,13 @@ export interface AnimatedGradientTextProps {
100
110
  }
101
111
 
102
112
  export function AnimatedGradientText({
103
- colors = ["hsl(var(--primary))", "hsl(280 100% 70%)", "hsl(var(--primary))"],
113
+ colors: colorsProp,
104
114
  speed = 3,
105
115
  className,
106
116
  children,
107
117
  }: AnimatedGradientTextProps) {
108
118
  const prefersReducedMotion = useReducedMotion()
119
+ const colors = filledList(colorsProp, TEXT_COLORS)
109
120
 
110
121
  return (
111
122
  <motion.span
@@ -1,9 +1,13 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from "react"
4
- import { motion, useReducedMotion } from "framer-motion"
4
+ import { animate, motion, useMotionValue, useReducedMotion, useTransform } from "framer-motion"
5
5
  import { cn } from "../../../src/lib/utils"
6
6
 
7
+ /** The value, or the fallback when it is missing or blank (a cleared control, say). */
8
+ const filled = (value: string | undefined, fallback: string) =>
9
+ value && value.trim() ? value : fallback
10
+
7
11
  export interface BorderBeamProps extends React.HTMLAttributes<HTMLDivElement> {
8
12
  /** Duration of the animation in seconds */
9
13
  duration?: number
@@ -24,62 +28,68 @@ export interface BorderBeamProps extends React.HTMLAttributes<HTMLDivElement> {
24
28
  export function BorderBeam({
25
29
  duration = 4,
26
30
  borderWidth = 2,
27
- colorFrom = "hsl(var(--primary))",
28
- colorTo = "transparent",
31
+ colorFrom: colorFromProp,
32
+ colorTo: colorToProp,
29
33
  delay = 0,
30
34
  className,
31
35
  children,
32
36
  ...props
33
37
  }: BorderBeamProps) {
38
+ const colorFrom = filled(colorFromProp, "var(--primary)")
39
+ const colorTo = filled(colorToProp, "transparent")
34
40
  const prefersReducedMotion = useReducedMotion()
35
41
 
42
+ // The beam is a conic gradient whose start angle turns, so it runs round
43
+ // the border of any shape; rotating a layer only works for a square.
44
+ const angle = useMotionValue(0)
45
+ React.useEffect(() => {
46
+ if (prefersReducedMotion) return
47
+ const controls = animate(angle, 360, {
48
+ duration,
49
+ delay,
50
+ ease: "linear",
51
+ repeat: Infinity,
52
+ })
53
+ return () => controls.stop()
54
+ }, [angle, duration, delay, prefersReducedMotion])
55
+ const background = useTransform(
56
+ angle,
57
+ (a) => `conic-gradient(from ${a}deg, transparent 0deg 270deg, ${colorTo} 270deg, ${colorFrom} 360deg)`
58
+ )
59
+
60
+ /** Paints only a borderWidth ring: the content box is masked out. */
61
+ const ring: React.CSSProperties = {
62
+ padding: borderWidth,
63
+ mask: "linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)",
64
+ maskComposite: "exclude",
65
+ WebkitMask: "linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)",
66
+ WebkitMaskComposite: "xor",
67
+ }
68
+
36
69
  return (
37
70
  <div
38
- className={cn("relative overflow-hidden rounded-lg", className)}
71
+ className={cn("relative isolate overflow-hidden rounded-lg", className)}
39
72
  {...props}
40
73
  >
41
- {/* Border beam */}
42
- {!prefersReducedMotion && (
43
- <motion.div
44
- className="pointer-events-none absolute inset-0 rounded-[inherit]"
45
- style={{
46
- padding: borderWidth,
47
- background: `linear-gradient(90deg, ${colorFrom}, ${colorTo})`,
48
- mask: "linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)",
49
- maskComposite: "exclude",
50
- WebkitMaskComposite: "xor",
51
- }}
52
- animate={{
53
- rotate: [0, 360],
54
- }}
55
- transition={{
56
- duration,
57
- delay,
58
- repeat: Infinity,
59
- ease: "linear",
60
- }}
61
- />
62
- )}
74
+ {/* Content */}
75
+ <div className="relative rounded-[inherit] bg-background">
76
+ {children}
77
+ </div>
63
78
 
64
- {/* Static border fallback for reduced motion */}
65
- {prefersReducedMotion && (
79
+ {/* The ring sits above the content, over its outer edge, so the content cannot cover it */}
80
+ {prefersReducedMotion ? (
66
81
  <div
67
- className="pointer-events-none absolute inset-0 rounded-[inherit]"
68
- style={{
69
- padding: borderWidth,
70
- background: colorFrom,
71
- mask: "linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)",
72
- maskComposite: "exclude",
73
- WebkitMaskComposite: "xor",
74
- opacity: 0.5,
75
- }}
82
+ aria-hidden="true"
83
+ className="pointer-events-none absolute inset-0 z-10 rounded-[inherit]"
84
+ style={{ ...ring, background: colorFrom, opacity: 0.5 }}
85
+ />
86
+ ) : (
87
+ <motion.div
88
+ aria-hidden="true"
89
+ className="pointer-events-none absolute inset-0 z-10 rounded-[inherit]"
90
+ style={{ ...ring, background }}
76
91
  />
77
92
  )}
78
-
79
- {/* Content */}
80
- <div className="relative z-10 rounded-[inherit] bg-background">
81
- {children}
82
- </div>
83
93
  </div>
84
94
  )
85
95
  }
@@ -93,40 +103,54 @@ export interface GlowingBorderProps extends React.HTMLAttributes<HTMLDivElement>
93
103
  intensity?: "sm" | "md" | "lg"
94
104
  }
95
105
 
106
+ /**
107
+ * The ring's width and the blur of the glow that hugs it, in px. The glow has
108
+ * no spread, so it stays close to the edge instead of casting a shadow.
109
+ */
96
110
  const intensityValues = {
97
- sm: "0 0 10px 2px",
98
- md: "0 0 20px 4px",
99
- lg: "0 0 30px 6px",
111
+ sm: { ring: 1, glow: 4 },
112
+ md: { ring: 1, glow: 8 },
113
+ lg: { ring: 2, glow: 12 },
100
114
  }
101
115
 
102
116
  /**
103
- * Static glowing border effect.
117
+ * Static glowing border: a coloured ring on the element's edge with a soft,
118
+ * tight glow around it. The ring is the border, so the content needs none.
104
119
  */
105
120
  export function GlowingBorder({
106
- glowColor = "hsl(var(--primary) / 0.5)",
107
- borderRadius = "0.5rem",
121
+ glowColor: glowColorProp,
122
+ borderRadius: borderRadiusProp,
108
123
  intensity = "md",
109
124
  className,
110
125
  children,
111
126
  ...props
112
127
  }: GlowingBorderProps) {
128
+ const glowColor = filled(glowColorProp, "color-mix(in oklch, var(--primary) 50%, transparent)")
129
+ const borderRadius = filled(borderRadiusProp, "0.5rem")
130
+ const { ring, glow } = intensityValues[intensity]
113
131
  return (
114
132
  <div
115
- className={cn("relative", className)}
133
+ className={cn("relative isolate", className)}
116
134
  style={{
117
135
  borderRadius,
118
136
  }}
119
137
  {...props}
120
138
  >
121
- {/* Glow */}
139
+ {children}
140
+ {/*
141
+ Above the content, so a card's own background cannot cover it. The inset
142
+ shadow draws the ring just inside the edge, over any neutral border the
143
+ content has; the outer ones are the glow.
144
+ */}
122
145
  <div
123
- className="absolute inset-0 -z-10"
146
+ aria-hidden="true"
147
+ className="pointer-events-none absolute inset-0 z-10"
124
148
  style={{
125
149
  borderRadius,
126
- boxShadow: `${intensityValues[intensity]} ${glowColor}`,
150
+ // Two stacked blurs, no spread: a denser glow at the edge that fades fast.
151
+ boxShadow: `inset 0 0 0 ${ring}px ${glowColor}, 0 0 ${glow / 2}px 0 ${glowColor}, 0 0 ${glow}px 0 ${glowColor}`,
127
152
  }}
128
153
  />
129
- {children}
130
154
  </div>
131
155
  )
132
156
  }