@smicolon/fasla-ui 0.2.4 → 0.3.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 (57) hide show
  1. package/README.md +1 -0
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.js +91 -23
  4. package/dist/registry/index.js +31 -2
  5. package/dist/tokens/index.d.ts +75 -6
  6. package/dist/tokens/index.js +59 -20
  7. package/package.json +4 -1
  8. package/registry/blocks/app-shell/AppShell.tsx +1 -1
  9. package/registry/blocks/data-table/DataTable.test.tsx +61 -0
  10. package/registry/blocks/data-table/DataTable.tsx +53 -16
  11. package/registry/blocks/data-table/index.ts +1 -0
  12. package/registry/blocks/empty-state/EmptyState.test.tsx +12 -0
  13. package/registry/blocks/empty-state/EmptyState.tsx +22 -31
  14. package/registry/blocks/form-section/FormSection.tsx +1 -1
  15. package/registry/blocks/navbar/Navbar.test.tsx +12 -0
  16. package/registry/blocks/navbar/Navbar.tsx +8 -23
  17. package/registry/blocks/page-header/PageHeader.tsx +5 -2
  18. package/registry/blocks/sidebar/Sidebar.tsx +9 -16
  19. package/registry/blocks/stats-card/StatsCard.tsx +9 -27
  20. package/registry/effects/animated-gradient/animated-gradient.tsx +23 -8
  21. package/registry/effects/border-beam/border-beam.tsx +77 -53
  22. package/registry/effects/effects.test.tsx +266 -0
  23. package/registry/effects/glow-card/glow-card.tsx +68 -43
  24. package/registry/effects/shimmer-button/shimmer-button.tsx +62 -23
  25. package/registry/effects/spotlight/spotlight.tsx +73 -29
  26. package/registry/effects/text-reveal/index.ts +1 -0
  27. package/registry/effects/text-reveal/text-reveal.test.tsx +66 -0
  28. package/registry/effects/text-reveal/text-reveal.tsx +68 -21
  29. package/registry/effects/typewriter-text/typewriter-text.tsx +39 -20
  30. package/registry/text-props-undefined.test.tsx +73 -0
  31. package/registry/ui/avatar/avatar.test.tsx +316 -0
  32. package/registry/ui/avatar/avatar.tsx +235 -108
  33. package/registry/ui/avatar/index.ts +1 -1
  34. package/registry/ui/badge/badge.test.tsx +267 -32
  35. package/registry/ui/badge/badge.tsx +233 -21
  36. package/registry/ui/button/button.test.tsx +6 -0
  37. package/registry/ui/button/button.tsx +6 -23
  38. package/registry/ui/card/card.tsx +1 -1
  39. package/registry/ui/checkbox/checkbox.tsx +13 -26
  40. package/registry/ui/combobox/combobox.test.tsx +43 -0
  41. package/registry/ui/combobox/combobox.tsx +27 -55
  42. package/registry/ui/input/input.test.tsx +2 -2
  43. package/registry/ui/input/input.tsx +4 -4
  44. package/registry/ui/radio/index.ts +1 -0
  45. package/registry/ui/radio/radio.test.tsx +248 -0
  46. package/registry/ui/radio/radio.tsx +208 -0
  47. package/registry/ui/select/select.tsx +4 -14
  48. package/registry/ui/skeleton/skeleton.tsx +1 -1
  49. package/registry/ui/status-indicator/index.ts +6 -0
  50. package/registry/ui/status-indicator/status-indicator.test.tsx +148 -0
  51. package/registry/ui/status-indicator/status-indicator.tsx +87 -0
  52. package/registry/ui/switch/index.ts +1 -1
  53. package/registry/ui/switch/switch.test.tsx +323 -0
  54. package/registry/ui/switch/switch.tsx +254 -57
  55. package/registry/ui/textarea/textarea.tsx +1 -1
  56. package/registry.json +78 -15
  57. package/tailwind-preset.ts +143 -0
@@ -13,8 +13,8 @@ export interface Column<T> {
13
13
  sortable?: boolean
14
14
  /** Column width class */
15
15
  width?: string
16
- /** Alignment */
17
- align?: "left" | "center" | "right"
16
+ /** Alignment. `start` and `end` follow the reading direction; `left` and `right` are physical sides. Defaults to `start`. */
17
+ align?: "start" | "center" | "end" | "left" | "right"
18
18
  }
19
19
 
20
20
  export interface DataTableProps<T> extends React.HTMLAttributes<HTMLDivElement> {
@@ -37,8 +37,10 @@ export interface DataTableProps<T> extends React.HTMLAttributes<HTMLDivElement>
37
37
  }
38
38
 
39
39
  const alignClasses = {
40
- left: "text-left",
40
+ start: "text-start",
41
41
  center: "text-center",
42
+ end: "text-end",
43
+ left: "text-left",
42
44
  right: "text-right",
43
45
  }
44
46
 
@@ -70,7 +72,7 @@ export function DataTable<T>({
70
72
  key={column.id}
71
73
  className={cn(
72
74
  "h-10 px-4 font-medium text-muted-foreground",
73
- alignClasses[column.align || "left"],
75
+ alignClasses[column.align || "start"],
74
76
  column.width
75
77
  )}
76
78
  >
@@ -108,7 +110,7 @@ export function DataTable<T>({
108
110
  key={column.id}
109
111
  className={cn(
110
112
  "h-10 px-4 font-medium text-muted-foreground",
111
- alignClasses[column.align || "left"],
113
+ alignClasses[column.align || "start"],
112
114
  column.width
113
115
  )}
114
116
  >
@@ -141,7 +143,7 @@ export function DataTable<T>({
141
143
  key={column.id}
142
144
  className={cn(
143
145
  "h-10 px-4 font-medium text-muted-foreground",
144
- alignClasses[column.align || "left"],
146
+ alignClasses[column.align || "start"],
145
147
  column.width
146
148
  )}
147
149
  >
@@ -170,7 +172,7 @@ export function DataTable<T>({
170
172
  key={column.id}
171
173
  className={cn(
172
174
  "p-4",
173
- alignClasses[column.align || "left"],
175
+ alignClasses[column.align || "start"],
174
176
  column.width
175
177
  )}
176
178
  >
@@ -197,6 +199,38 @@ export interface PaginationProps {
197
199
  /** Show page numbers */
198
200
  showPageNumbers?: boolean
199
201
  className?: string
202
+ /** Text and accessible names; each defaults to English */
203
+ labels?: Partial<PaginationLabels>
204
+ }
205
+
206
+ export interface PaginationLabels {
207
+ /** Accessible name of the pagination landmark */
208
+ pagination: string
209
+ /** Visible status, given the page and the total */
210
+ status: (page: number, totalPages: number) => string
211
+ previous: string
212
+ next: string
213
+ goToPrevious: string
214
+ goToNext: string
215
+ goToPage: (page: number) => string
216
+ }
217
+
218
+ /** The entries of a partial object whose values are not undefined. */
219
+ function definedOnly<T extends object>(value: Partial<T> | undefined): Partial<T> {
220
+ if (!value) return {}
221
+ return Object.fromEntries(
222
+ Object.entries(value).filter(([, entry]) => entry !== undefined)
223
+ ) as Partial<T>
224
+ }
225
+
226
+ const DEFAULT_PAGINATION_LABELS: PaginationLabels = {
227
+ pagination: "Pagination",
228
+ status: (page, totalPages) => `Page ${page} of ${totalPages}`,
229
+ previous: "Previous",
230
+ next: "Next",
231
+ goToPrevious: "Go to previous page",
232
+ goToNext: "Go to next page",
233
+ goToPage: (page) => `Go to page ${page}`,
200
234
  }
201
235
 
202
236
  /**
@@ -208,7 +242,12 @@ export function Pagination({
208
242
  onPageChange,
209
243
  showPageNumbers = true,
210
244
  className,
245
+ labels,
211
246
  }: PaginationProps) {
247
+ // A label passed as undefined (a missed translation key, say) keeps its
248
+ // English default instead of replacing it: a plain spread would leave
249
+ // status or goToPage undefined, and calling them would crash the render.
250
+ const text = { ...DEFAULT_PAGINATION_LABELS, ...definedOnly(labels) }
212
251
  const canGoPrevious = page > 1
213
252
  const canGoNext = page < totalPages
214
253
 
@@ -241,19 +280,17 @@ export function Pagination({
241
280
  return (
242
281
  <nav
243
282
  className={cn("flex items-center justify-between px-2 py-3", className)}
244
- aria-label="Pagination"
283
+ aria-label={text.pagination}
245
284
  >
246
- <p className="text-sm text-muted-foreground">
247
- Page {page} of {totalPages}
248
- </p>
285
+ <p className="text-sm text-muted-foreground">{text.status(page, totalPages)}</p>
249
286
  <div className="flex items-center gap-1">
250
287
  <button
251
288
  onClick={() => onPageChange(page - 1)}
252
289
  disabled={!canGoPrevious}
253
290
  className="inline-flex h-8 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50"
254
- aria-label="Go to previous page"
291
+ aria-label={text.goToPrevious}
255
292
  >
256
- Previous
293
+ {text.previous}
257
294
  </button>
258
295
  {showPageNumbers &&
259
296
  getPageNumbers().map((pageNum, i) =>
@@ -271,7 +308,7 @@ export function Pagination({
271
308
  ? "bg-primary text-primary-foreground"
272
309
  : "hover:bg-accent hover:text-accent-foreground"
273
310
  )}
274
- aria-label={`Go to page ${pageNum}`}
311
+ aria-label={text.goToPage(pageNum)}
275
312
  aria-current={pageNum === page ? "page" : undefined}
276
313
  >
277
314
  {pageNum}
@@ -282,9 +319,9 @@ export function Pagination({
282
319
  onClick={() => onPageChange(page + 1)}
283
320
  disabled={!canGoNext}
284
321
  className="inline-flex h-8 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50"
285
- aria-label="Go to next page"
322
+ aria-label={text.goToNext}
286
323
  >
287
- Next
324
+ {text.next}
288
325
  </button>
289
326
  </div>
290
327
  </nav>
@@ -4,4 +4,5 @@ export {
4
4
  type Column,
5
5
  type DataTableProps,
6
6
  type PaginationProps,
7
+ type PaginationLabels,
7
8
  } from "./DataTable"
@@ -118,3 +118,15 @@ describe("EmptyData", () => {
118
118
  expect(svg).toBeInTheDocument()
119
119
  })
120
120
  })
121
+
122
+ describe("EmptyState presets", () => {
123
+ it("take a title and description in place of their English", () => {
124
+ render(<EmptySearchResults title="لا توجد نتائج" description="جرّب كلمة بحث أخرى." />)
125
+ expect(screen.getByText("لا توجد نتائج")).toBeInTheDocument()
126
+ expect(screen.getByText("جرّب كلمة بحث أخرى.")).toBeInTheDocument()
127
+ expect(screen.queryByText("No results found")).not.toBeInTheDocument()
128
+
129
+ render(<EmptyData title="لا توجد طلبات بعد" description="أنشئ طلبك الأول للبدء." />)
130
+ expect(screen.getByText("لا توجد طلبات بعد")).toBeInTheDocument()
131
+ })
132
+ })
@@ -1,4 +1,5 @@
1
1
  import * as React from "react"
2
+ import { FilePlusIcon, SearchIcon } from "lucide-react"
2
3
  import { cn } from "../../../src/lib/utils"
3
4
 
4
5
  export interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
@@ -99,33 +100,29 @@ export function EmptyState({
99
100
  export function EmptySearchResults({
100
101
  query,
101
102
  onClear,
103
+ title,
104
+ description,
102
105
  ...props
103
106
  }: Omit<EmptyStateProps, "title" | "description"> & {
107
+ /** Replaces the English title */
108
+ title?: string
109
+ /** Replaces the English description */
110
+ description?: string
104
111
  query?: string
105
112
  onClear?: () => void
106
113
  }) {
107
114
  return (
108
115
  <EmptyState
109
- title="No results found"
116
+ // ?? rather than a later spread, so an undefined title keeps the English one
117
+ title={title ?? "No results found"}
110
118
  description={
111
- query
119
+ description ??
120
+ (query
112
121
  ? `No results for "${query}". Try a different search term.`
113
- : "Try adjusting your search or filters."
122
+ : "Try adjusting your search or filters.")
114
123
  }
115
124
  icon={
116
- <svg
117
- xmlns="http://www.w3.org/2000/svg"
118
- fill="none"
119
- viewBox="0 0 24 24"
120
- strokeWidth={1.5}
121
- stroke="currentColor"
122
- >
123
- <path
124
- strokeLinecap="round"
125
- strokeLinejoin="round"
126
- d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"
127
- />
128
- </svg>
125
+ <SearchIcon strokeWidth={1.5} />
129
126
  }
130
127
  {...props}
131
128
  />
@@ -135,28 +132,22 @@ export function EmptySearchResults({
135
132
  /** Pre-built empty state for no data */
136
133
  export function EmptyData({
137
134
  resourceName = "items",
135
+ title,
136
+ description,
138
137
  ...props
139
138
  }: Omit<EmptyStateProps, "title" | "description"> & {
139
+ /** Replaces the English title */
140
+ title?: string
141
+ /** Replaces the English description */
142
+ description?: string
140
143
  resourceName?: string
141
144
  }) {
142
145
  return (
143
146
  <EmptyState
144
- title={`No ${resourceName} yet`}
145
- description={`Get started by creating your first ${resourceName.replace(/s$/, "")}.`}
147
+ title={title ?? `No ${resourceName} yet`}
148
+ description={description ?? `Get started by creating your first ${resourceName.replace(/s$/, "")}.`}
146
149
  icon={
147
- <svg
148
- xmlns="http://www.w3.org/2000/svg"
149
- fill="none"
150
- viewBox="0 0 24 24"
151
- strokeWidth={1.5}
152
- stroke="currentColor"
153
- >
154
- <path
155
- strokeLinecap="round"
156
- strokeLinejoin="round"
157
- d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m3.75 9v6m3-3H9m1.5-12H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"
158
- />
159
- </svg>
150
+ <FilePlusIcon strokeWidth={1.5} />
160
151
  }
161
152
  {...props}
162
153
  />
@@ -82,7 +82,7 @@ export function FormField({
82
82
  )}
83
83
  >
84
84
  {label}
85
- {required && <span className="ml-1 text-destructive">*</span>}
85
+ {required && <span className="ms-1 text-destructive">*</span>}
86
86
  </label>
87
87
  {children}
88
88
  {description && !error && (
@@ -0,0 +1,12 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { render, screen } from "@testing-library/react"
3
+ import { NavbarToggle } from "./Navbar"
4
+
5
+ describe("NavbarToggle", () => {
6
+ it("tells assistive tech whether the menu is showing", () => {
7
+ const { rerender } = render(<NavbarToggle />)
8
+ expect(screen.getByRole("button").getAttribute("aria-expanded")).toBe("false")
9
+ rerender(<NavbarToggle open />)
10
+ expect(screen.getByRole("button", { name: "Close menu" }).getAttribute("aria-expanded")).toBe("true")
11
+ })
12
+ })
@@ -1,4 +1,5 @@
1
1
  import * as React from "react"
2
+ import { MenuIcon, XIcon } from "lucide-react"
2
3
  import { cn } from "../../../src/lib/utils"
3
4
 
4
5
  export interface NavbarProps extends React.HTMLAttributes<HTMLElement> {
@@ -160,31 +161,15 @@ export function NavbarToggle({
160
161
  className
161
162
  )}
162
163
  aria-label={open ? "Close menu" : "Open menu"}
164
+ // Screen readers announce whether the menu it controls is showing.
165
+ aria-expanded={open ?? false}
163
166
  {...props}
164
167
  >
165
- <svg
166
- width="20"
167
- height="20"
168
- viewBox="0 0 24 24"
169
- fill="none"
170
- stroke="currentColor"
171
- strokeWidth="2"
172
- strokeLinecap="round"
173
- strokeLinejoin="round"
174
- >
175
- {open ? (
176
- <>
177
- <line x1="18" y1="6" x2="6" y2="18" />
178
- <line x1="6" y1="6" x2="18" y2="18" />
179
- </>
180
- ) : (
181
- <>
182
- <line x1="4" y1="12" x2="20" y2="12" />
183
- <line x1="4" y1="6" x2="20" y2="6" />
184
- <line x1="4" y1="18" x2="20" y2="18" />
185
- </>
186
- )}
187
- </svg>
168
+ {open ? (
169
+ <XIcon size={20} strokeWidth={1.5} />
170
+ ) : (
171
+ <MenuIcon size={20} strokeWidth={1.5} />
172
+ )}
188
173
  </button>
189
174
  )
190
175
  }
@@ -14,6 +14,8 @@ export interface PageHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
14
14
  bordered?: boolean
15
15
  /** Semantic heading level for the title */
16
16
  headingLevel?: 1 | 2 | 3
17
+ /** Accessible name of the breadcrumb landmark */
18
+ breadcrumbLabel?: string
17
19
  }
18
20
 
19
21
  /**
@@ -27,6 +29,7 @@ export function PageHeader({
27
29
  breadcrumb,
28
30
  bordered = true,
29
31
  headingLevel = 1,
32
+ breadcrumbLabel = "Breadcrumb",
30
33
  className,
31
34
  ...props
32
35
  }: PageHeaderProps) {
@@ -44,11 +47,11 @@ export function PageHeader({
44
47
  <div className="flex items-start justify-between gap-4">
45
48
  <div className="min-w-0 flex-1">
46
49
  {breadcrumb && (
47
- <nav className="mb-2 text-sm text-muted-foreground" aria-label="Breadcrumb">
50
+ <nav className="mb-2 text-sm text-muted-foreground" aria-label={breadcrumbLabel}>
48
51
  {breadcrumb}
49
52
  </nav>
50
53
  )}
51
- <Heading className="truncate text-xl font-semibold tracking-tight md:text-2xl">
54
+ <Heading className="truncate text-xl font-semibold md:text-2xl">
52
55
  {title}
53
56
  </Heading>
54
57
  {description && (
@@ -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> {
@@ -30,7 +31,7 @@ export function Sidebar({
30
31
  return (
31
32
  <aside
32
33
  className={cn(
33
- "flex h-full flex-col border-r bg-background transition-all duration-200",
34
+ "flex h-full flex-col border-e bg-background transition-all duration-200",
34
35
  collapsed ? "w-16" : widthClasses[width],
35
36
  className
36
37
  )}
@@ -116,7 +117,7 @@ export function SidebarGroup({
116
117
  return (
117
118
  <div className={cn("py-2", className)} {...props}>
118
119
  {label && (
119
- <div className="mb-2 px-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
120
+ <div className="mb-2 px-2 text-xs font-semibold uppercase text-muted-foreground">
120
121
  {label}
121
122
  </div>
122
123
  )}
@@ -183,20 +184,12 @@ export function SidebarCollapseButton({
183
184
  aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
184
185
  {...props}
185
186
  >
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>
187
+ {/* Points at the edge the sidebar collapses toward, so it mirrors in RTL. */}
188
+ <ChevronsLeftIcon
189
+ size={16}
190
+ strokeWidth={1.5}
191
+ className={cn("transition-transform motion-reduce:transition-none rtl:-scale-x-100", collapsed && "rotate-180")}
192
+ />
200
193
  </button>
201
194
  )
202
195
  }
@@ -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 && (
@@ -65,35 +66,16 @@ export function StatsCard({
65
66
  )}
66
67
  >
67
68
  {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>
69
+ <ChevronUpIcon size={16} strokeWidth={1.5} />
80
70
  )}
81
71
  {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>
72
+ <ChevronDownIcon size={16} strokeWidth={1.5} />
94
73
  )}
95
- {trend.value > 0 ? "+" : ""}
96
- {trend.value}%
74
+ {/* One LTR unit: in RTL the sign would otherwise jump to the far end, "12.5%+". */}
75
+ <bdi dir="ltr">
76
+ {trend.value > 0 ? "+" : ""}
77
+ {trend.value}%
78
+ </bdi>
97
79
  </span>
98
80
  )}
99
81
  {description && (
@@ -4,6 +4,24 @@ 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
+ const GRADIENT_COLORS = [
17
+ "var(--primary)",
18
+ "var(--secondary)",
19
+ "hsl(280 100% 70%)",
20
+ "hsl(200 100% 70%)",
21
+ ]
22
+
23
+ const TEXT_COLORS = ["var(--primary)", "hsl(280 100% 70%)", "var(--primary)"]
24
+
7
25
  export interface AnimatedGradientProps
8
26
  extends React.HTMLAttributes<HTMLDivElement> {
9
27
  /** Gradient colors */
@@ -28,12 +46,7 @@ const blurValues = {
28
46
  * Respects prefers-reduced-motion by stopping animation.
29
47
  */
30
48
  export function AnimatedGradient({
31
- colors = [
32
- "hsl(var(--primary))",
33
- "hsl(var(--secondary))",
34
- "hsl(280 100% 70%)",
35
- "hsl(200 100% 70%)",
36
- ],
49
+ colors: colorsProp,
37
50
  speed = 10,
38
51
  blur = "3xl",
39
52
  className,
@@ -41,10 +54,11 @@ export function AnimatedGradient({
41
54
  ...props
42
55
  }: AnimatedGradientProps) {
43
56
  const prefersReducedMotion = useReducedMotion()
57
+ const colors = filledList(colorsProp, GRADIENT_COLORS)
44
58
 
45
59
  return (
46
60
  <div
47
- className={cn("relative overflow-hidden", className)}
61
+ className={cn("relative isolate overflow-hidden", className)}
48
62
  {...props}
49
63
  >
50
64
  {/* Gradient blobs */}
@@ -100,12 +114,13 @@ export interface AnimatedGradientTextProps {
100
114
  }
101
115
 
102
116
  export function AnimatedGradientText({
103
- colors = ["hsl(var(--primary))", "hsl(280 100% 70%)", "hsl(var(--primary))"],
117
+ colors: colorsProp,
104
118
  speed = 3,
105
119
  className,
106
120
  children,
107
121
  }: AnimatedGradientTextProps) {
108
122
  const prefersReducedMotion = useReducedMotion()
123
+ const colors = filledList(colorsProp, TEXT_COLORS)
109
124
 
110
125
  return (
111
126
  <motion.span