@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,104 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { render, screen } from "@testing-library/react"
3
+ import { Skeleton, SkeletonText, SkeletonAvatar, SkeletonCard } from "./skeleton"
4
+
5
+ describe("Skeleton", () => {
6
+ it("renders with default props", () => {
7
+ render(<Skeleton data-testid="skeleton" />)
8
+ expect(screen.getByTestId("skeleton")).toBeInTheDocument()
9
+ })
10
+
11
+ it("renders with animation by default", () => {
12
+ render(<Skeleton data-testid="skeleton" />)
13
+ expect(screen.getByTestId("skeleton")).toHaveClass("animate-pulse")
14
+ })
15
+
16
+ it("can disable animation", () => {
17
+ render(<Skeleton animate={false} data-testid="skeleton" />)
18
+ expect(screen.getByTestId("skeleton")).not.toHaveClass("animate-pulse")
19
+ })
20
+
21
+ it("renders with default variant (rounded-md)", () => {
22
+ render(<Skeleton data-testid="skeleton" />)
23
+ expect(screen.getByTestId("skeleton")).toHaveClass("rounded-md")
24
+ })
25
+
26
+ it("renders with circular variant", () => {
27
+ render(<Skeleton variant="circular" data-testid="skeleton" />)
28
+ expect(screen.getByTestId("skeleton")).toHaveClass("rounded-full")
29
+ })
30
+
31
+ it("renders with rectangular variant", () => {
32
+ render(<Skeleton variant="rectangular" data-testid="skeleton" />)
33
+ expect(screen.getByTestId("skeleton")).toHaveClass("rounded-none")
34
+ })
35
+
36
+ it("applies custom className", () => {
37
+ render(<Skeleton className="h-10 w-20" data-testid="skeleton" />)
38
+ expect(screen.getByTestId("skeleton")).toHaveClass("h-10", "w-20")
39
+ })
40
+ })
41
+
42
+ describe("SkeletonText", () => {
43
+ it("renders with default 3 lines", () => {
44
+ render(<SkeletonText data-testid="skeleton-text" />)
45
+ const container = screen.getByTestId("skeleton-text")
46
+ expect(container.children).toHaveLength(3)
47
+ })
48
+
49
+ it("renders with specified number of lines", () => {
50
+ render(<SkeletonText lines={5} data-testid="skeleton-text" />)
51
+ const container = screen.getByTestId("skeleton-text")
52
+ expect(container.children).toHaveLength(5)
53
+ })
54
+
55
+ it("last line is shorter when multiple lines", () => {
56
+ render(<SkeletonText lines={3} data-testid="skeleton-text" />)
57
+ const container = screen.getByTestId("skeleton-text")
58
+ const lastLine = container.children[2]
59
+ expect(lastLine).toHaveClass("w-4/5")
60
+ })
61
+
62
+ it("single line is full width", () => {
63
+ render(<SkeletonText lines={1} data-testid="skeleton-text" />)
64
+ const container = screen.getByTestId("skeleton-text")
65
+ const line = container.children[0]
66
+ expect(line).toHaveClass("w-full")
67
+ })
68
+ })
69
+
70
+ describe("SkeletonAvatar", () => {
71
+ it("renders with circular variant", () => {
72
+ render(<SkeletonAvatar data-testid="skeleton-avatar" />)
73
+ expect(screen.getByTestId("skeleton-avatar")).toHaveClass("rounded-full")
74
+ })
75
+
76
+ it("renders with default size", () => {
77
+ render(<SkeletonAvatar data-testid="skeleton-avatar" />)
78
+ expect(screen.getByTestId("skeleton-avatar")).toHaveClass("h-10", "w-10")
79
+ })
80
+
81
+ it("accepts custom className to override size", () => {
82
+ render(<SkeletonAvatar className="h-16 w-16" data-testid="skeleton-avatar" />)
83
+ expect(screen.getByTestId("skeleton-avatar")).toHaveClass("h-16", "w-16")
84
+ })
85
+ })
86
+
87
+ describe("SkeletonCard", () => {
88
+ it("renders a card container", () => {
89
+ render(<SkeletonCard data-testid="skeleton-card" />)
90
+ expect(screen.getByTestId("skeleton-card")).toHaveClass("rounded-xl", "border", "bg-card", "shadow")
91
+ })
92
+
93
+ it("contains an avatar skeleton", () => {
94
+ const { container } = render(<SkeletonCard />)
95
+ const avatars = container.querySelectorAll(".rounded-full")
96
+ expect(avatars.length).toBeGreaterThan(0)
97
+ })
98
+
99
+ it("contains text skeletons", () => {
100
+ const { container } = render(<SkeletonCard />)
101
+ const skeletons = container.querySelectorAll(".animate-pulse")
102
+ expect(skeletons.length).toBeGreaterThan(0)
103
+ })
104
+ })
@@ -0,0 +1,88 @@
1
+ import * as React from "react"
2
+ import { cn } from "../../../src/lib/utils"
3
+
4
+ export interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ /** The shape of the skeleton */
6
+ variant?: "default" | "circular" | "rectangular"
7
+ /** Whether to animate the skeleton */
8
+ animate?: boolean
9
+ }
10
+
11
+ function Skeleton({
12
+ className,
13
+ variant = "default",
14
+ animate = true,
15
+ ...props
16
+ }: SkeletonProps) {
17
+ return (
18
+ <div
19
+ className={cn(
20
+ "bg-primary/10",
21
+ animate && "animate-pulse",
22
+ variant === "default" && "rounded-md",
23
+ variant === "circular" && "rounded-full",
24
+ variant === "rectangular" && "rounded-none",
25
+ className
26
+ )}
27
+ {...props}
28
+ />
29
+ )
30
+ }
31
+
32
+ /** Pre-built skeleton for text lines */
33
+ function SkeletonText({
34
+ className,
35
+ lines = 3,
36
+ ...props
37
+ }: SkeletonProps & { lines?: number }) {
38
+ return (
39
+ <div className={cn("space-y-2", className)} {...props}>
40
+ {Array.from({ length: lines }).map((_, i) => (
41
+ <Skeleton
42
+ key={i}
43
+ className={cn(
44
+ "h-4",
45
+ i === lines - 1 && lines > 1 ? "w-4/5" : "w-full"
46
+ )}
47
+ />
48
+ ))}
49
+ </div>
50
+ )
51
+ }
52
+
53
+ /** Pre-built skeleton for avatars */
54
+ function SkeletonAvatar({ className, ...props }: SkeletonProps) {
55
+ return (
56
+ <Skeleton
57
+ variant="circular"
58
+ className={cn("h-10 w-10", className)}
59
+ {...props}
60
+ />
61
+ )
62
+ }
63
+
64
+ /** Pre-built skeleton for cards */
65
+ function SkeletonCard({ className, ...props }: SkeletonProps) {
66
+ return (
67
+ <div
68
+ className={cn(
69
+ "rounded-xl border bg-card p-6 shadow",
70
+ className
71
+ )}
72
+ {...props}
73
+ >
74
+ <div className="flex items-center space-x-4">
75
+ <SkeletonAvatar />
76
+ <div className="space-y-2 flex-1">
77
+ <Skeleton className="h-4 w-1/2" />
78
+ <Skeleton className="h-3 w-1/4" />
79
+ </div>
80
+ </div>
81
+ <div className="mt-4">
82
+ <SkeletonText lines={3} />
83
+ </div>
84
+ </div>
85
+ )
86
+ }
87
+
88
+ export { Skeleton, SkeletonText, SkeletonAvatar, SkeletonCard }
@@ -0,0 +1 @@
1
+ export { Switch, type SwitchProps } from "./switch"
@@ -0,0 +1,89 @@
1
+ import * as React from "react"
2
+ import { cn } from "../../../src/lib/utils"
3
+
4
+ export interface SwitchProps
5
+ extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type" | "size"> {
6
+ /** Label text */
7
+ label?: string
8
+ /** Description text */
9
+ description?: string
10
+ /** Size variant */
11
+ size?: "sm" | "default" | "lg"
12
+ }
13
+
14
+ const sizeClasses = {
15
+ sm: {
16
+ track: "h-4 w-7",
17
+ thumb: "h-3 w-3 data-[state=checked]:translate-x-3",
18
+ },
19
+ default: {
20
+ track: "h-5 w-9",
21
+ thumb: "h-4 w-4 data-[state=checked]:translate-x-4",
22
+ },
23
+ lg: {
24
+ track: "h-6 w-11",
25
+ thumb: "h-5 w-5 data-[state=checked]:translate-x-5",
26
+ },
27
+ }
28
+
29
+ const Switch = React.forwardRef<HTMLInputElement, SwitchProps>(
30
+ ({ className, label, description, size = "default", id, ...props }, ref) => {
31
+ const inputId = id || React.useId()
32
+ const sizes = sizeClasses[size]
33
+
34
+ const switchElement = (
35
+ <label
36
+ htmlFor={inputId}
37
+ className={cn(
38
+ "peer relative inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors",
39
+ "focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2",
40
+ "disabled:cursor-not-allowed disabled:opacity-50",
41
+ "bg-input has-[:checked]:bg-primary",
42
+ sizes.track,
43
+ className
44
+ )}
45
+ >
46
+ <input
47
+ type="checkbox"
48
+ ref={ref}
49
+ id={inputId}
50
+ className="sr-only"
51
+ {...props}
52
+ />
53
+ <span
54
+ data-state={props.checked ? "checked" : "unchecked"}
55
+ className={cn(
56
+ "pointer-events-none block rounded-full bg-background shadow-lg ring-0 transition-transform",
57
+ sizes.thumb
58
+ )}
59
+ />
60
+ </label>
61
+ )
62
+
63
+ if (!label && !description) {
64
+ return switchElement
65
+ }
66
+
67
+ return (
68
+ <div className="flex items-center justify-between gap-4">
69
+ <div className="grid gap-1">
70
+ {label && (
71
+ <label
72
+ htmlFor={inputId}
73
+ className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
74
+ >
75
+ {label}
76
+ </label>
77
+ )}
78
+ {description && (
79
+ <p className="text-xs text-muted-foreground">{description}</p>
80
+ )}
81
+ </div>
82
+ {switchElement}
83
+ </div>
84
+ )
85
+ }
86
+ )
87
+ Switch.displayName = "Switch"
88
+
89
+ export { Switch }
@@ -0,0 +1,10 @@
1
+ export {
2
+ Tabs,
3
+ TabsList,
4
+ TabsTrigger,
5
+ TabsContent,
6
+ type TabsProps,
7
+ type TabsListProps,
8
+ type TabsTriggerProps,
9
+ type TabsContentProps,
10
+ } from "./tabs"
@@ -0,0 +1,150 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../../src/lib/utils"
5
+
6
+ interface TabsContextValue {
7
+ value: string
8
+ onValueChange: (value: string) => void
9
+ }
10
+
11
+ const TabsContext = React.createContext<TabsContextValue | null>(null)
12
+
13
+ function useTabsContext() {
14
+ const context = React.useContext(TabsContext)
15
+ if (!context) {
16
+ throw new Error("Tabs components must be used within a Tabs provider")
17
+ }
18
+ return context
19
+ }
20
+
21
+ export interface TabsProps extends React.HTMLAttributes<HTMLDivElement> {
22
+ /** The controlled value of the tab to activate */
23
+ value?: string
24
+ /** The default value of the tab to activate */
25
+ defaultValue?: string
26
+ /** Called when the value changes */
27
+ onValueChange?: (value: string) => void
28
+ children: React.ReactNode
29
+ }
30
+
31
+ export function Tabs({
32
+ value: controlledValue,
33
+ defaultValue,
34
+ onValueChange,
35
+ children,
36
+ className,
37
+ ...props
38
+ }: TabsProps) {
39
+ const [uncontrolledValue, setUncontrolledValue] = React.useState(
40
+ defaultValue || ""
41
+ )
42
+ const value = controlledValue ?? uncontrolledValue
43
+
44
+ const handleValueChange = React.useCallback(
45
+ (newValue: string) => {
46
+ setUncontrolledValue(newValue)
47
+ onValueChange?.(newValue)
48
+ },
49
+ [onValueChange]
50
+ )
51
+
52
+ return (
53
+ <TabsContext.Provider value={{ value, onValueChange: handleValueChange }}>
54
+ <div className={cn("w-full", className)} {...props}>
55
+ {children}
56
+ </div>
57
+ </TabsContext.Provider>
58
+ )
59
+ }
60
+
61
+ export interface TabsListProps extends React.HTMLAttributes<HTMLDivElement> {
62
+ children: React.ReactNode
63
+ }
64
+
65
+ export function TabsList({ className, children, ...props }: TabsListProps) {
66
+ return (
67
+ <div
68
+ role="tablist"
69
+ className={cn(
70
+ "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
71
+ className
72
+ )}
73
+ {...props}
74
+ >
75
+ {children}
76
+ </div>
77
+ )
78
+ }
79
+
80
+ export interface TabsTriggerProps
81
+ extends React.ButtonHTMLAttributes<HTMLButtonElement> {
82
+ /** The unique value for the tab */
83
+ value: string
84
+ children: React.ReactNode
85
+ }
86
+
87
+ export function TabsTrigger({
88
+ value,
89
+ className,
90
+ children,
91
+ ...props
92
+ }: TabsTriggerProps) {
93
+ const { value: selectedValue, onValueChange } = useTabsContext()
94
+ const isSelected = selectedValue === value
95
+
96
+ return (
97
+ <button
98
+ role="tab"
99
+ type="button"
100
+ aria-selected={isSelected}
101
+ data-state={isSelected ? "active" : "inactive"}
102
+ onClick={() => onValueChange(value)}
103
+ className={cn(
104
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all",
105
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
106
+ "disabled:pointer-events-none disabled:opacity-50",
107
+ isSelected
108
+ ? "bg-background text-foreground shadow"
109
+ : "hover:bg-background/50 hover:text-foreground",
110
+ className
111
+ )}
112
+ {...props}
113
+ >
114
+ {children}
115
+ </button>
116
+ )
117
+ }
118
+
119
+ export interface TabsContentProps extends React.HTMLAttributes<HTMLDivElement> {
120
+ /** The unique value for the tab panel */
121
+ value: string
122
+ children: React.ReactNode
123
+ }
124
+
125
+ export function TabsContent({
126
+ value,
127
+ className,
128
+ children,
129
+ ...props
130
+ }: TabsContentProps) {
131
+ const { value: selectedValue } = useTabsContext()
132
+ const isSelected = selectedValue === value
133
+
134
+ if (!isSelected) return null
135
+
136
+ return (
137
+ <div
138
+ role="tabpanel"
139
+ data-state={isSelected ? "active" : "inactive"}
140
+ className={cn(
141
+ "mt-2 ring-offset-background",
142
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
143
+ className
144
+ )}
145
+ {...props}
146
+ >
147
+ {children}
148
+ </div>
149
+ )
150
+ }
@@ -0,0 +1 @@
1
+ export { Textarea, textareaVariants, type TextareaProps } from "./textarea"
@@ -0,0 +1,73 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { cn } from "../../../src/lib/utils"
4
+
5
+ const textareaVariants = cva(
6
+ "flex min-h-[60px] w-full rounded-md border bg-transparent px-3 py-2 text-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: "border-input shadow-sm",
11
+ error: "border-destructive focus-visible:ring-destructive",
12
+ },
13
+ resize: {
14
+ none: "resize-none",
15
+ vertical: "resize-y",
16
+ horizontal: "resize-x",
17
+ both: "resize",
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ variant: "default",
22
+ resize: "vertical",
23
+ },
24
+ }
25
+ )
26
+
27
+ export interface TextareaProps
28
+ extends React.TextareaHTMLAttributes<HTMLTextAreaElement>,
29
+ VariantProps<typeof textareaVariants> {
30
+ /** Show character count */
31
+ showCount?: boolean
32
+ /** Maximum character count */
33
+ maxLength?: number
34
+ }
35
+
36
+ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
37
+ ({ className, variant, resize, showCount, maxLength, ...props }, ref) => {
38
+ const [count, setCount] = React.useState(
39
+ (props.defaultValue as string)?.length || 0
40
+ )
41
+
42
+ const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
43
+ setCount(e.target.value.length)
44
+ props.onChange?.(e)
45
+ }
46
+
47
+ const textarea = (
48
+ <textarea
49
+ className={cn(textareaVariants({ variant, resize }), className)}
50
+ ref={ref}
51
+ maxLength={maxLength}
52
+ onChange={handleChange}
53
+ {...props}
54
+ />
55
+ )
56
+
57
+ if (!showCount) {
58
+ return textarea
59
+ }
60
+
61
+ return (
62
+ <div className="relative">
63
+ {textarea}
64
+ <div className="absolute bottom-2 right-3 text-xs text-muted-foreground">
65
+ {maxLength ? `${count}/${maxLength}` : count}
66
+ </div>
67
+ </div>
68
+ )
69
+ }
70
+ )
71
+ Textarea.displayName = "Textarea"
72
+
73
+ export { Textarea, textareaVariants }