@smicolon/fasla-ui 0.3.0 → 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.
@@ -0,0 +1,43 @@
1
+ // @vitest-environment node
2
+ import { describe, it, expect } from "vitest"
3
+ import { readdirSync } from "node:fs"
4
+ import { join } from "node:path"
5
+ import ts from "typescript"
6
+
7
+ const registry = __dirname
8
+
9
+ function sourceFiles(dir: string): string[] {
10
+ return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
11
+ const path = join(dir, entry.name)
12
+ if (entry.isDirectory()) return sourceFiles(path)
13
+ return /\.tsx?$/.test(entry.name) && !/\.(test|stories)\.tsx?$/.test(entry.name) ? [path] : []
14
+ })
15
+ }
16
+
17
+ // The compiler options create-next-app@14 writes. It sets no target, so
18
+ // TypeScript falls back to ES5, and syntax that needs ES2015 or later (a u
19
+ // flag on a regex literal, for one) fails `next build`.
20
+ const next14 = {
21
+ lib: ["lib.dom.d.ts", "lib.dom.iterable.d.ts", "lib.esnext.d.ts"],
22
+ allowJs: true,
23
+ skipLibCheck: true,
24
+ strict: true,
25
+ noEmit: true,
26
+ esModuleInterop: true,
27
+ module: ts.ModuleKind.ESNext,
28
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
29
+ resolveJsonModule: true,
30
+ isolatedModules: true,
31
+ jsx: ts.JsxEmit.Preserve,
32
+ }
33
+
34
+ describe("registry under a Next.js 14 tsconfig", () => {
35
+ it("type-checks with no target set", () => {
36
+ const program = ts.createProgram(sourceFiles(registry), next14)
37
+ const errors = ts.getPreEmitDiagnostics(program).map((d) => {
38
+ const where = d.file ? `${d.file.fileName.replace(registry, "registry")}:${d.file.getLineAndCharacterOfPosition(d.start ?? 0).line + 1}` : ""
39
+ return `${where} TS${d.code} ${ts.flattenDiagnosticMessageText(d.messageText, " ")}`
40
+ })
41
+ expect(errors).toEqual([])
42
+ }, 60_000)
43
+ })
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { render, screen } from "@testing-library/react"
3
+ import { Checkbox } from "./checkbox"
4
+
5
+ describe("Checkbox id", () => {
6
+ it("uses the id it is given", () => {
7
+ render(<Checkbox id="terms" label="Accept the terms" />)
8
+ expect(screen.getByLabelText("Accept the terms").id).toBe("terms")
9
+ })
10
+
11
+ it("generates an id that ties the label to the input", () => {
12
+ render(<Checkbox label="Accept the terms" />)
13
+ expect(screen.getByLabelText("Accept the terms").id).not.toBe("")
14
+ })
15
+
16
+ it("keeps working when the id is added and removed between renders", () => {
17
+ const { rerender } = render(<Checkbox label="Accept the terms" />)
18
+ rerender(<Checkbox id="terms" label="Accept the terms" />)
19
+ expect(screen.getByLabelText("Accept the terms").id).toBe("terms")
20
+ rerender(<Checkbox label="Accept the terms" />)
21
+ expect(screen.getByLabelText("Accept the terms").id).not.toBe("terms")
22
+ })
23
+ })
@@ -26,7 +26,9 @@ const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
26
26
  }
27
27
  }, [indeterminate])
28
28
 
29
- const inputId = id || React.useId()
29
+ // Called on every render, so the hook order holds when id comes and goes
30
+ const generatedId = React.useId()
31
+ const inputId = id || generatedId
30
32
 
31
33
  const checkbox = (
32
34
  <div className="relative flex items-center">
@@ -1,4 +1,4 @@
1
- import { describe, it, expect } from "vitest"
1
+ import { describe, it, expect, beforeAll } from "vitest"
2
2
  import { render, screen, fireEvent } from "@testing-library/react"
3
3
  import { Combobox } from "./combobox"
4
4
 
@@ -41,3 +41,32 @@ describe("Combobox text", () => {
41
41
  expect(screen.getByText("جارٍ التحميل")).toBeTruthy()
42
42
  })
43
43
  })
44
+
45
+ describe("Combobox highlight", () => {
46
+ // The highlighted option is scrolled into view, which jsdom does not implement
47
+ beforeAll(() => {
48
+ Element.prototype.scrollIntoView ??= () => {}
49
+ })
50
+
51
+ it("moves back to the first option when the search changes", () => {
52
+ const cities = [...options, { value: "dammam", label: "Dammam" }]
53
+ render(<Combobox options={cities} />)
54
+ const input = screen.getByRole("combobox")
55
+
56
+ fireEvent.focus(input)
57
+ fireEvent.keyDown(input, { key: "ArrowDown" })
58
+ fireEvent.keyDown(input, { key: "ArrowDown" })
59
+ expect(screen.getByRole("option", { name: "Dammam" })).toHaveAttribute("data-highlighted", "true")
60
+
61
+ fireEvent.change(input, { target: { value: "a" } })
62
+ expect(screen.getByRole("option", { name: "Riyadh" })).toHaveAttribute("data-highlighted", "true")
63
+ expect(screen.getByRole("option", { name: "Dammam" })).toHaveAttribute("data-highlighted", "false")
64
+ })
65
+
66
+ it("quotes the search in the create option", () => {
67
+ render(<Combobox options={options} creatable onCreate={() => {}} />)
68
+ const input = screen.getByRole("combobox")
69
+ fireEvent.change(input, { target: { value: "Mecca" } })
70
+ expect(screen.getByRole("option").textContent).toBe('Create "Mecca"')
71
+ })
72
+ })
@@ -189,10 +189,13 @@ export function Combobox({
189
189
  }
190
190
  }, [highlightedIndex, open])
191
191
 
192
- // Reset highlighted index when search changes
193
- React.useEffect(() => {
192
+ // Reset highlighted index when search changes. Done while rendering rather
193
+ // than in an effect, so the list never paints with the old highlight.
194
+ const [highlightedSearch, setHighlightedSearch] = React.useState(search)
195
+ if (highlightedSearch !== search) {
196
+ setHighlightedSearch(search)
194
197
  setHighlightedIndex(0)
195
- }, [search])
198
+ }
196
199
 
197
200
  // Close on outside click
198
201
  React.useEffect(() => {
@@ -334,7 +337,7 @@ export function Combobox({
334
337
  )}
335
338
  >
336
339
  <PlusIcon size={16} strokeWidth={1.5} className="me-2" />
337
- {createText} "{search}"
340
+ {createText} &quot;{search}&quot;
338
341
  </li>
339
342
  )}
340
343
  </>
@@ -16,7 +16,7 @@ describe("Input", () => {
16
16
  expect(screen.getByTestId("input")).toHaveClass("border-destructive")
17
17
 
18
18
  rerender(<Input variant="success" data-testid="input" />)
19
- expect(screen.getByTestId("input")).toHaveClass("border-green-500")
19
+ expect(screen.getByTestId("input")).toHaveClass("border-success")
20
20
  })
21
21
 
22
22
  it("renders with different sizes", () => {
@@ -9,7 +9,7 @@ const inputVariants = cva(
9
9
  variant: {
10
10
  default: "border-input shadow-sm",
11
11
  error: "border-destructive focus-visible:ring-destructive",
12
- success: "border-green-500 focus-visible:ring-green-500",
12
+ success: "border-success focus-visible:ring-success",
13
13
  },
14
14
  inputSize: {
15
15
  default: "h-9 px-3 py-1",
@@ -2,6 +2,7 @@ export {
2
2
  Skeleton,
3
3
  SkeletonText,
4
4
  SkeletonAvatar,
5
+ SkeletonListItem,
5
6
  SkeletonCard,
6
7
  type SkeletonProps,
7
8
  } from "./skeleton"
@@ -1,6 +1,13 @@
1
+ import * as React from "react"
1
2
  import { describe, it, expect } from "vitest"
2
3
  import { render, screen } from "@testing-library/react"
3
- import { Skeleton, SkeletonText, SkeletonAvatar, SkeletonCard } from "./skeleton"
4
+ import {
5
+ Skeleton,
6
+ SkeletonText,
7
+ SkeletonAvatar,
8
+ SkeletonListItem,
9
+ SkeletonCard,
10
+ } from "./skeleton"
4
11
 
5
12
  describe("Skeleton", () => {
6
13
  it("renders with default props", () => {
@@ -8,19 +15,26 @@ describe("Skeleton", () => {
8
15
  expect(screen.getByTestId("skeleton")).toBeInTheDocument()
9
16
  })
10
17
 
11
- it("renders with animation by default", () => {
18
+ it("uses the secondary token, as Figma does", () => {
12
19
  render(<Skeleton data-testid="skeleton" />)
13
- expect(screen.getByTestId("skeleton")).toHaveClass("animate-pulse")
20
+ expect(screen.getByTestId("skeleton")).toHaveClass("bg-secondary")
21
+ })
22
+
23
+ it("pulses by default, only when motion is allowed", () => {
24
+ render(<Skeleton data-testid="skeleton" />)
25
+ const skeleton = screen.getByTestId("skeleton")
26
+ expect(skeleton).toHaveClass("motion-safe:animate-pulse")
27
+ expect(skeleton).not.toHaveClass("animate-pulse")
14
28
  })
15
29
 
16
30
  it("can disable animation", () => {
17
31
  render(<Skeleton animate={false} data-testid="skeleton" />)
18
- expect(screen.getByTestId("skeleton")).not.toHaveClass("animate-pulse")
32
+ expect(screen.getByTestId("skeleton")).not.toHaveClass("motion-safe:animate-pulse")
19
33
  })
20
34
 
21
- it("renders with default variant (rounded-md)", () => {
35
+ it("renders with default variant (4px radius)", () => {
22
36
  render(<Skeleton data-testid="skeleton" />)
23
- expect(screen.getByTestId("skeleton")).toHaveClass("rounded-md")
37
+ expect(screen.getByTestId("skeleton")).toHaveClass("rounded")
24
38
  })
25
39
 
26
40
  it("renders with circular variant", () => {
@@ -37,13 +51,31 @@ describe("Skeleton", () => {
37
51
  render(<Skeleton className="h-10 w-20" data-testid="skeleton" />)
38
52
  expect(screen.getByTestId("skeleton")).toHaveClass("h-10", "w-20")
39
53
  })
54
+
55
+ it("lets className override the radius", () => {
56
+ render(<Skeleton className="rounded-lg" data-testid="skeleton" />)
57
+ const skeleton = screen.getByTestId("skeleton")
58
+ expect(skeleton).toHaveClass("rounded-lg")
59
+ expect(skeleton).not.toHaveClass("rounded")
60
+ })
61
+
62
+ it("is hidden from assistive technology", () => {
63
+ render(<Skeleton data-testid="skeleton" />)
64
+ expect(screen.getByTestId("skeleton")).toHaveAttribute("aria-hidden", "true")
65
+ })
66
+
67
+ it("forwards a ref to the element", () => {
68
+ const ref = React.createRef<HTMLDivElement>()
69
+ render(<Skeleton ref={ref} data-testid="skeleton" />)
70
+ expect(ref.current).toBe(screen.getByTestId("skeleton"))
71
+ })
40
72
  })
41
73
 
42
74
  describe("SkeletonText", () => {
43
- it("renders with default 3 lines", () => {
75
+ it("renders 2 lines by default, as Figma does", () => {
44
76
  render(<SkeletonText data-testid="skeleton-text" />)
45
77
  const container = screen.getByTestId("skeleton-text")
46
- expect(container.children).toHaveLength(3)
78
+ expect(container.children).toHaveLength(2)
47
79
  })
48
80
 
49
81
  it("renders with specified number of lines", () => {
@@ -52,18 +84,39 @@ describe("SkeletonText", () => {
52
84
  expect(container.children).toHaveLength(5)
53
85
  })
54
86
 
55
- it("last line is shorter when multiple lines", () => {
87
+ it("makes every line a full-width 16px bar", () => {
56
88
  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")
89
+ const lines = Array.from(screen.getByTestId("skeleton-text").children)
90
+ for (const line of lines) expect(line).toHaveClass("h-4", "w-full")
91
+ })
92
+
93
+ it("stacks lines 8px apart", () => {
94
+ render(<SkeletonText data-testid="skeleton-text" />)
95
+ expect(screen.getByTestId("skeleton-text")).toHaveClass("flex", "flex-col", "gap-2")
60
96
  })
61
97
 
62
- it("single line is full width", () => {
63
- render(<SkeletonText lines={1} data-testid="skeleton-text" />)
98
+ it("passes animate to every line", () => {
99
+ render(<SkeletonText animate={false} data-testid="skeleton-text" />)
100
+ const lines = Array.from(screen.getByTestId("skeleton-text").children)
101
+ for (const line of lines) expect(line).not.toHaveClass("motion-safe:animate-pulse")
102
+ })
103
+
104
+ it("keeps animate and variant off the DOM", () => {
105
+ render(<SkeletonText animate variant="circular" data-testid="skeleton-text" />)
64
106
  const container = screen.getByTestId("skeleton-text")
65
- const line = container.children[0]
66
- expect(line).toHaveClass("w-full")
107
+ expect(container).not.toHaveAttribute("animate")
108
+ expect(container).not.toHaveAttribute("variant")
109
+ })
110
+
111
+ it("keeps variant off the DOM in the list item and card too", () => {
112
+ render(
113
+ <>
114
+ <SkeletonListItem variant="circular" data-testid="list-item" />
115
+ <SkeletonCard variant="circular" data-testid="card" />
116
+ </>
117
+ )
118
+ expect(screen.getByTestId("list-item")).not.toHaveAttribute("variant")
119
+ expect(screen.getByTestId("card")).not.toHaveAttribute("variant")
67
120
  })
68
121
  })
69
122
 
@@ -73,32 +126,63 @@ describe("SkeletonAvatar", () => {
73
126
  expect(screen.getByTestId("skeleton-avatar")).toHaveClass("rounded-full")
74
127
  })
75
128
 
76
- it("renders with default size", () => {
129
+ it("renders at 48px by default", () => {
77
130
  render(<SkeletonAvatar data-testid="skeleton-avatar" />)
78
- expect(screen.getByTestId("skeleton-avatar")).toHaveClass("h-10", "w-10")
131
+ expect(screen.getByTestId("skeleton-avatar")).toHaveClass("h-12", "w-12")
79
132
  })
80
133
 
81
134
  it("accepts custom className to override size", () => {
82
135
  render(<SkeletonAvatar className="h-16 w-16" data-testid="skeleton-avatar" />)
83
- expect(screen.getByTestId("skeleton-avatar")).toHaveClass("h-16", "w-16")
136
+ const avatar = screen.getByTestId("skeleton-avatar")
137
+ expect(avatar).toHaveClass("h-16", "w-16")
138
+ expect(avatar).not.toHaveClass("h-12", "w-12")
139
+ })
140
+ })
141
+
142
+ describe("SkeletonListItem", () => {
143
+ it("puts the avatar first, then the lines, 16px apart", () => {
144
+ render(<SkeletonListItem data-testid="skeleton-list-item" />)
145
+ const row = screen.getByTestId("skeleton-list-item")
146
+ expect(row).toHaveClass("flex", "items-center", "gap-4")
147
+ expect(row.children[0]).toHaveClass("rounded-full", "h-12", "w-12")
148
+ expect(row.children[1]!.children).toHaveLength(2)
149
+ })
150
+
151
+ it("accepts lines", () => {
152
+ render(<SkeletonListItem lines={3} data-testid="skeleton-list-item" />)
153
+ expect(screen.getByTestId("skeleton-list-item").children[1]!.children).toHaveLength(3)
154
+ })
155
+
156
+ it("passes animate to every block", () => {
157
+ const { container } = render(<SkeletonListItem animate={false} />)
158
+ expect(container.querySelectorAll('[data-slot="skeleton"]')).toHaveLength(3)
159
+ expect(container.querySelectorAll(".motion-safe\\:animate-pulse")).toHaveLength(0)
160
+ })
161
+
162
+ it("is hidden from assistive technology", () => {
163
+ render(<SkeletonListItem data-testid="skeleton-list-item" />)
164
+ expect(screen.getByTestId("skeleton-list-item")).toHaveAttribute("aria-hidden", "true")
84
165
  })
85
166
  })
86
167
 
87
168
  describe("SkeletonCard", () => {
88
- it("renders a card container", () => {
169
+ it("has no card surface of its own", () => {
89
170
  render(<SkeletonCard data-testid="skeleton-card" />)
90
- expect(screen.getByTestId("skeleton-card")).toHaveClass("rounded-xl", "border", "bg-card", "shadow")
171
+ const card = screen.getByTestId("skeleton-card")
172
+ expect(card).toHaveClass("flex", "flex-col", "gap-4")
173
+ expect(card).not.toHaveClass("border", "bg-card", "shadow")
91
174
  })
92
175
 
93
- it("contains an avatar skeleton", () => {
94
- const { container } = render(<SkeletonCard />)
95
- const avatars = container.querySelectorAll(".rounded-full")
96
- expect(avatars.length).toBeGreaterThan(0)
176
+ it("puts a 122px media block above 2 lines", () => {
177
+ render(<SkeletonCard data-testid="skeleton-card" />)
178
+ const card = screen.getByTestId("skeleton-card")
179
+ expect(card.children[0]).toHaveClass("h-[122px]", "w-full", "rounded")
180
+ expect(card.children[1]!.children).toHaveLength(2)
97
181
  })
98
182
 
99
- it("contains text skeletons", () => {
100
- const { container } = render(<SkeletonCard />)
101
- const skeletons = container.querySelectorAll(".animate-pulse")
102
- expect(skeletons.length).toBeGreaterThan(0)
183
+ it("passes animate to every block", () => {
184
+ const { container } = render(<SkeletonCard animate={false} />)
185
+ expect(container.querySelectorAll('[data-slot="skeleton"]')).toHaveLength(3)
186
+ expect(container.querySelectorAll(".motion-safe\\:animate-pulse")).toHaveLength(0)
103
187
  })
104
188
  })
@@ -1,6 +1,26 @@
1
1
  import * as React from "react"
2
2
  import { cn } from "../../../src/lib/utils"
3
3
 
4
+ /**
5
+ * Skeleton — a loading placeholder that stands in for content until it arrives.
6
+ *
7
+ * Matches the Figma set (Skeleton, `15120:8056`):
8
+ * - every block is `secondary`, the token Figma binds (`theme/secondary`),
9
+ * solid rather than a tint, so it reads the same on any surface;
10
+ * - bars are 16px tall with a 4px radius, stacked 8px apart;
11
+ * - the list item puts a 48px circle 16px before its lines;
12
+ * - the card puts a 122px media block 16px above its lines.
13
+ *
14
+ * Blocks are decorative, so each is `aria-hidden`. Announce loading on the
15
+ * region that is loading instead, with `aria-busy="true"`.
16
+ *
17
+ * The pulse is Tailwind's built-in `animate-pulse`, behind `motion-safe:`, so
18
+ * it stops when the user has asked for reduced motion.
19
+ *
20
+ * Every composition lays out with flex and logical order only, so in RTL the
21
+ * circle moves to the inline start (the right) with no `rtl:` overrides.
22
+ */
23
+
4
24
  export interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {
5
25
  /** The shape of the skeleton */
6
26
  variant?: "default" | "circular" | "rectangular"
@@ -8,18 +28,20 @@ export interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {
8
28
  animate?: boolean
9
29
  }
10
30
 
11
- function Skeleton({
12
- className,
13
- variant = "default",
14
- animate = true,
15
- ...props
16
- }: SkeletonProps) {
17
- return (
31
+ /**
32
+ * A single block. A drop-in replacement for shadcn's Skeleton: size it with
33
+ * `className`, such as `h-4 w-48`.
34
+ */
35
+ const Skeleton = React.forwardRef<HTMLDivElement, SkeletonProps>(
36
+ ({ className, variant = "default", animate = true, ...props }, ref) => (
18
37
  <div
38
+ ref={ref}
39
+ data-slot="skeleton"
40
+ aria-hidden="true"
19
41
  className={cn(
20
- "bg-primary/10",
21
- animate && "animate-pulse",
22
- variant === "default" && "rounded-md",
42
+ "bg-secondary",
43
+ animate && "motion-safe:animate-pulse",
44
+ variant === "default" && "rounded",
23
45
  variant === "circular" && "rounded-full",
24
46
  variant === "rectangular" && "rounded-none",
25
47
  className
@@ -27,62 +49,100 @@ function Skeleton({
27
49
  {...props}
28
50
  />
29
51
  )
52
+ )
53
+ Skeleton.displayName = "Skeleton"
54
+
55
+ /** Props for the compositions: `animate` reaches every block inside. */
56
+ type SkeletonGroupProps = SkeletonProps & {
57
+ /** Number of text lines */
58
+ lines?: number
59
+ }
60
+
61
+ /**
62
+ * A group has no shape of its own, so `variant` is taken out of its props and
63
+ * never reaches the DOM. The parameter's rest pattern makes the copy.
64
+ */
65
+ function withoutVariant({ ...props }: SkeletonProps): React.HTMLAttributes<HTMLDivElement> {
66
+ delete props.variant
67
+ return props
30
68
  }
31
69
 
32
- /** Pre-built skeleton for text lines */
70
+ /** Pre-built skeleton for text lines. Every line is full width, as in Figma. */
33
71
  function SkeletonText({
34
72
  className,
35
- lines = 3,
73
+ lines = 2,
74
+ animate = true,
36
75
  ...props
37
- }: SkeletonProps & { lines?: number }) {
76
+ }: SkeletonGroupProps) {
38
77
  return (
39
- <div className={cn("space-y-2", className)} {...props}>
78
+ <div
79
+ data-slot="skeleton-text"
80
+ aria-hidden="true"
81
+ className={cn("flex flex-col gap-2", className)}
82
+ {...withoutVariant(props)}
83
+ >
40
84
  {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
- />
85
+ <Skeleton key={i} animate={animate} className="h-4 w-full" />
48
86
  ))}
49
87
  </div>
50
88
  )
51
89
  }
52
90
 
53
- /** Pre-built skeleton for avatars */
91
+ /** Pre-built skeleton for avatars: a 48px circle. */
54
92
  function SkeletonAvatar({ className, ...props }: SkeletonProps) {
55
93
  return (
56
94
  <Skeleton
57
95
  variant="circular"
58
- className={cn("h-10 w-10", className)}
96
+ className={cn("h-12 w-12 shrink-0", className)}
59
97
  {...props}
60
98
  />
61
99
  )
62
100
  }
63
101
 
64
- /** Pre-built skeleton for cards */
65
- function SkeletonCard({ className, ...props }: SkeletonProps) {
102
+ /**
103
+ * Pre-built skeleton for a list row: an avatar at the inline start, then text
104
+ * lines filling the rest. For lists, comments and profile rows.
105
+ */
106
+ function SkeletonListItem({
107
+ className,
108
+ lines = 2,
109
+ animate = true,
110
+ ...props
111
+ }: SkeletonGroupProps) {
112
+ return (
113
+ <div
114
+ data-slot="skeleton-list-item"
115
+ aria-hidden="true"
116
+ className={cn("flex items-center gap-4", className)}
117
+ {...withoutVariant(props)}
118
+ >
119
+ <SkeletonAvatar animate={animate} />
120
+ <SkeletonText lines={lines} animate={animate} className="min-w-0 flex-1" />
121
+ </div>
122
+ )
123
+ }
124
+
125
+ /**
126
+ * Pre-built skeleton for cards: a media block above text lines. It has no
127
+ * border or background of its own, so it can sit inside a real Card.
128
+ */
129
+ function SkeletonCard({
130
+ className,
131
+ lines = 2,
132
+ animate = true,
133
+ ...props
134
+ }: SkeletonGroupProps) {
66
135
  return (
67
136
  <div
68
- className={cn(
69
- "rounded-xl border bg-card p-6 shadow",
70
- className
71
- )}
72
- {...props}
137
+ data-slot="skeleton-card"
138
+ aria-hidden="true"
139
+ className={cn("flex flex-col gap-4", className)}
140
+ {...withoutVariant(props)}
73
141
  >
74
- <div className="flex items-center gap-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>
142
+ <Skeleton animate={animate} className="h-[122px] w-full" />
143
+ <SkeletonText lines={lines} animate={animate} />
84
144
  </div>
85
145
  )
86
146
  }
87
147
 
88
- export { Skeleton, SkeletonText, SkeletonAvatar, SkeletonCard }
148
+ export { Skeleton, SkeletonText, SkeletonAvatar, SkeletonListItem, SkeletonCard }
package/registry.json CHANGED
@@ -421,7 +421,7 @@
421
421
  "name": "skeleton",
422
422
  "type": "registry:ui",
423
423
  "title": "Skeleton",
424
- "description": "Loading skeleton placeholders with pre-built variants for text, avatars, and cards.",
424
+ "description": "Loading placeholders that replace shadcn's Skeleton: a plain block plus text, avatar, list item and card shapes that stay still under reduced motion.",
425
425
  "dependencies": [],
426
426
  "registryDependencies": [],
427
427
  "files": [