ruler-components 0.1.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 (114) hide show
  1. package/AGENTS.md +9 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +21 -0
  4. package/README.md +45 -0
  5. package/app/docs/[slug]/page.tsx +147 -0
  6. package/app/docs/layout.tsx +26 -0
  7. package/app/docs/page.tsx +148 -0
  8. package/app/favicon.ico +0 -0
  9. package/app/globals.css +168 -0
  10. package/app/icon.png +0 -0
  11. package/app/layout.tsx +90 -0
  12. package/app/opengraph-image.tsx +10 -0
  13. package/app/page.tsx +34 -0
  14. package/app/twitter-image.tsx +10 -0
  15. package/components/docs/accordion-doc-page.tsx +352 -0
  16. package/components/docs/alert-doc-page.tsx +293 -0
  17. package/components/docs/allinput-doc-page.tsx +183 -0
  18. package/components/docs/avatar-doc-page.tsx +424 -0
  19. package/components/docs/badge-doc-page.tsx +363 -0
  20. package/components/docs/button-doc-page.tsx +406 -0
  21. package/components/docs/card-doc-page.tsx +364 -0
  22. package/components/docs/checkbox-doc-page.tsx +277 -0
  23. package/components/docs/colors-doc-page.tsx +430 -0
  24. package/components/docs/dialog-doc-page.tsx +364 -0
  25. package/components/docs/docs-footer.tsx +11 -0
  26. package/components/docs/docs-mobile-menu-bar.tsx +22 -0
  27. package/components/docs/docs-mobile-nav-context.tsx +27 -0
  28. package/components/docs/docs-page.tsx +263 -0
  29. package/components/docs/docs-search.tsx +303 -0
  30. package/components/docs/docs-sidebar.tsx +165 -0
  31. package/components/docs/docs-topbar.tsx +32 -0
  32. package/components/docs/dropdown-doc-page.tsx +522 -0
  33. package/components/docs/getting-started-doc-page.tsx +205 -0
  34. package/components/docs/label-doc-page.tsx +173 -0
  35. package/components/docs/loaders-doc-page.tsx +244 -0
  36. package/components/docs/menu-doc-page.tsx +373 -0
  37. package/components/docs/modal-doc-page.tsx +436 -0
  38. package/components/docs/next-steps.tsx +53 -0
  39. package/components/docs/pagination-doc-page.tsx +316 -0
  40. package/components/docs/progress-doc-page.tsx +387 -0
  41. package/components/docs/radio-doc-page.tsx +270 -0
  42. package/components/docs/skeleton-doc-page.tsx +230 -0
  43. package/components/docs/stepper-doc-page.tsx +309 -0
  44. package/components/docs/switch-doc-page.tsx +264 -0
  45. package/components/docs/table-doc-page.tsx +689 -0
  46. package/components/docs/tabs-doc-page.tsx +359 -0
  47. package/components/docs/theming-doc-page.tsx +243 -0
  48. package/components/docs/tooltip-doc-page.tsx +357 -0
  49. package/components/docs/usage-section.tsx +21 -0
  50. package/components/icons/discord.tsx +9 -0
  51. package/components/icons/github.tsx +9 -0
  52. package/components/icons/x.tsx +9 -0
  53. package/components/installation-section.tsx +34 -0
  54. package/components/landing/feature-highlights.tsx +105 -0
  55. package/components/landing/footer-strip.tsx +45 -0
  56. package/components/landing/hero-card.tsx +14 -0
  57. package/components/landing/hero-spotlight.tsx +126 -0
  58. package/components/landing/hero.tsx +64 -0
  59. package/components/landing/navbar.tsx +44 -0
  60. package/components/landing/quick-start-section.tsx +59 -0
  61. package/components/landing/site-footer.tsx +55 -0
  62. package/components/theme-provider.tsx +60 -0
  63. package/components/ui/accordion.tsx +174 -0
  64. package/components/ui/alert.tsx +103 -0
  65. package/components/ui/allinput.tsx +210 -0
  66. package/components/ui/avatar.tsx +179 -0
  67. package/components/ui/badge.tsx +52 -0
  68. package/components/ui/button.tsx +75 -0
  69. package/components/ui/card.tsx +105 -0
  70. package/components/ui/checkbox.tsx +130 -0
  71. package/components/ui/code-block.tsx +60 -0
  72. package/components/ui/data-table.tsx +235 -0
  73. package/components/ui/dialog.tsx +173 -0
  74. package/components/ui/dropdown.tsx +78 -0
  75. package/components/ui/feature-card.tsx +38 -0
  76. package/components/ui/input.tsx +44 -0
  77. package/components/ui/label.tsx +110 -0
  78. package/components/ui/loaders.tsx +139 -0
  79. package/components/ui/menu.tsx +118 -0
  80. package/components/ui/modal.tsx +399 -0
  81. package/components/ui/package-manager-tabs.tsx +40 -0
  82. package/components/ui/pagination.tsx +200 -0
  83. package/components/ui/progress.tsx +134 -0
  84. package/components/ui/radio.tsx +262 -0
  85. package/components/ui/skeleton.tsx +102 -0
  86. package/components/ui/steppers.tsx +309 -0
  87. package/components/ui/switch.tsx +153 -0
  88. package/components/ui/table.tsx +58 -0
  89. package/components/ui/tabs.tsx +166 -0
  90. package/components/ui/theme-toggle.tsx +60 -0
  91. package/components/ui/tooltip.tsx +238 -0
  92. package/data/color-palette.ts +35 -0
  93. package/data/docs-nav.ts +84 -0
  94. package/eslint.config.mjs +18 -0
  95. package/index.ts +8 -0
  96. package/lib/cn.ts +4 -0
  97. package/lib/color.ts +85 -0
  98. package/lib/og-image.tsx +138 -0
  99. package/lib/theme.ts +33 -0
  100. package/next.config.ts +7 -0
  101. package/package.json +30 -0
  102. package/postcss.config.mjs +7 -0
  103. package/public/dark.png +0 -0
  104. package/public/dark1.png +0 -0
  105. package/public/dark2.png +0 -0
  106. package/public/file.svg +1 -0
  107. package/public/globe.svg +1 -0
  108. package/public/light.png +0 -0
  109. package/public/light1.png +0 -0
  110. package/public/next.svg +1 -0
  111. package/public/vercel.svg +1 -0
  112. package/public/video/hero-demo.mp4 +0 -0
  113. package/public/window.svg +1 -0
  114. package/tsconfig.json +34 -0
@@ -0,0 +1,134 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "@/lib/cn";
4
+
5
+ const progressTrackVariants = cva(
6
+ "relative w-full overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-800/70",
7
+ {
8
+ variants: {
9
+ size: {
10
+ sm: "h-1",
11
+ md: "h-1.5",
12
+ lg: "h-2",
13
+ },
14
+ },
15
+ defaultVariants: { size: "md" },
16
+ },
17
+ );
18
+
19
+ const progressIndicatorVariants = cva(
20
+ "h-full rounded-full transition-[width] duration-500 ease-[cubic-bezier(0.4,0,0.2,1)]",
21
+ {
22
+ variants: {
23
+ variant: {
24
+ default: "bg-zinc-900 dark:bg-zinc-100",
25
+ success: "bg-[#28A745] dark:bg-[#5CC975]",
26
+ warning: "bg-[#E58E00] dark:bg-[#F5B84A]",
27
+ error: "bg-[#D93025] dark:bg-[#F06B62]",
28
+ },
29
+ },
30
+ defaultVariants: { variant: "default" },
31
+ },
32
+ );
33
+
34
+ const valuePillVariants = cva(
35
+ "inline-flex min-w-[2.25rem] items-center justify-end text-[11px] font-medium tabular-nums text-zinc-400 dark:text-zinc-500",
36
+ {
37
+ variants: {
38
+ variant: {
39
+ default: "",
40
+ success: "",
41
+ warning: "",
42
+ error: "",
43
+ },
44
+ },
45
+ defaultVariants: { variant: "default" },
46
+ },
47
+ );
48
+
49
+ export interface ProgressProps
50
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "children">,
51
+ VariantProps<typeof progressTrackVariants>,
52
+ VariantProps<typeof progressIndicatorVariants> {
53
+ value?: number;
54
+ max?: number;
55
+ label?: string;
56
+ showValue?: boolean;
57
+ indeterminate?: boolean;
58
+ disabled?: boolean;
59
+ }
60
+
61
+ const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
62
+ (
63
+ {
64
+ className,
65
+ value = 0,
66
+ max = 100,
67
+ variant,
68
+ size,
69
+ label,
70
+ showValue = false,
71
+ indeterminate = false,
72
+ disabled = false,
73
+ ...props
74
+ },
75
+ ref,
76
+ ) => {
77
+ const clamped = Math.min(max, Math.max(0, value));
78
+ const percentage = max > 0 ? Math.round((clamped / max) * 100) : 0;
79
+
80
+ return (
81
+ <div
82
+ className={cn(
83
+ "flex w-full flex-col gap-2.5",
84
+ disabled && "pointer-events-none opacity-50",
85
+ className,
86
+ )}
87
+ >
88
+ {(label || showValue) && (
89
+ <div className="flex items-center justify-between gap-3">
90
+ {label && (
91
+ <span className="text-sm text-zinc-600 dark:text-zinc-300">
92
+ {label}
93
+ </span>
94
+ )}
95
+ {showValue && !indeterminate && (
96
+ <span className={cn(valuePillVariants({ variant }))}>
97
+ {percentage}%
98
+ </span>
99
+ )}
100
+ </div>
101
+ )}
102
+ <div
103
+ ref={ref}
104
+ role="progressbar"
105
+ aria-valuemin={0}
106
+ aria-valuemax={max}
107
+ aria-valuenow={indeterminate || disabled ? undefined : clamped}
108
+ aria-label={label}
109
+ aria-valuetext={indeterminate ? "Loading" : `${percentage}%`}
110
+ aria-disabled={disabled || undefined}
111
+ className={cn(progressTrackVariants({ size }))}
112
+ {...props}
113
+ >
114
+ {indeterminate ? (
115
+ <div
116
+ className={cn(
117
+ progressIndicatorVariants({ variant }),
118
+ "w-[40%] animate-[progress-indeterminate_1.5s_ease-in-out_infinite]",
119
+ )}
120
+ />
121
+ ) : (
122
+ <div
123
+ className={cn(progressIndicatorVariants({ variant }))}
124
+ style={{ width: `${percentage}%` }}
125
+ />
126
+ )}
127
+ </div>
128
+ </div>
129
+ );
130
+ },
131
+ );
132
+ Progress.displayName = "Progress";
133
+
134
+ export { Progress, progressTrackVariants, progressIndicatorVariants };
@@ -0,0 +1,262 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "@/lib/cn";
4
+
5
+ const radioControlVariants = cva(
6
+ [
7
+ "pointer-events-none relative flex shrink-0 items-center justify-center rounded-full border-2 bg-transparent transition-all duration-200",
8
+ "border-zinc-500 group-hover:border-zinc-700",
9
+ "peer-checked:border-zinc-900",
10
+ "peer-focus-visible:outline-none peer-focus-visible:ring-2 peer-focus-visible:ring-zinc-900/30 peer-focus-visible:ring-offset-2",
11
+ "peer-disabled:border-zinc-300",
12
+ "peer-disabled:peer-checked:border-zinc-300",
13
+ "dark:border-zinc-500 dark:group-hover:border-zinc-400",
14
+ "dark:peer-checked:border-zinc-100",
15
+ "dark:peer-disabled:border-zinc-600 dark:peer-disabled:peer-checked:border-zinc-600",
16
+ "peer-checked:[&_.radio-dot]:scale-100 peer-checked:[&_.radio-dot]:opacity-100",
17
+ "peer-disabled:peer-checked:[&_.radio-dot]:bg-zinc-300",
18
+ "dark:peer-disabled:peer-checked:[&_.radio-dot]:bg-zinc-600",
19
+ ],
20
+ {
21
+ variants: {
22
+ size: {
23
+ sm: "h-4 w-4 [&_.radio-dot]:h-1.5 [&_.radio-dot]:w-1.5",
24
+ md: "h-5 w-5 [&_.radio-dot]:h-2.5 [&_.radio-dot]:w-2.5",
25
+ lg: "h-6 w-6 [&_.radio-dot]:h-3 [&_.radio-dot]:w-3",
26
+ },
27
+ },
28
+ defaultVariants: { size: "md" },
29
+ },
30
+ );
31
+
32
+ const radioWrapperVariants = cva("group relative flex w-full transition-colors duration-150", {
33
+ variants: {
34
+ variant: {
35
+ inline: "gap-3",
36
+ card: [
37
+ "gap-3.5 rounded-lg border border-zinc-200 bg-white px-4 py-3.5",
38
+ "hover:border-zinc-300 hover:bg-zinc-50/80",
39
+ "has-[:checked]:border-zinc-900",
40
+ "dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700",
41
+ "dark:has-[:checked]:border-zinc-500",
42
+ ],
43
+ },
44
+ },
45
+ defaultVariants: { variant: "inline" },
46
+ });
47
+
48
+ interface RadioGroupContextValue {
49
+ name: string;
50
+ value?: string;
51
+ onValueChange?: (value: string) => void;
52
+ size?: "sm" | "md" | "lg";
53
+ variant?: "inline" | "card";
54
+ appearance?: "default" | "segmented";
55
+ disabled?: boolean;
56
+ }
57
+
58
+ const RadioGroupContext = React.createContext<RadioGroupContextValue | null>(
59
+ null,
60
+ );
61
+
62
+ export interface RadioGroupProps extends React.HTMLAttributes<HTMLDivElement> {
63
+ name: string;
64
+ value?: string;
65
+ defaultValue?: string;
66
+ onValueChange?: (value: string) => void;
67
+ size?: "sm" | "md" | "lg";
68
+ variant?: "inline" | "card";
69
+ appearance?: "default" | "segmented";
70
+ disabled?: boolean;
71
+ orientation?: "vertical" | "horizontal";
72
+ }
73
+
74
+ const RadioGroup = React.forwardRef<HTMLDivElement, RadioGroupProps>(
75
+ (
76
+ {
77
+ className,
78
+ name,
79
+ value: controlledValue,
80
+ defaultValue,
81
+ onValueChange,
82
+ size = "md",
83
+ variant = "inline",
84
+ appearance = "default",
85
+ disabled,
86
+ orientation = "vertical",
87
+ children,
88
+ ...props
89
+ },
90
+ ref,
91
+ ) => {
92
+ const [uncontrolledValue, setUncontrolledValue] = React.useState(
93
+ defaultValue ?? "",
94
+ );
95
+ const value = controlledValue ?? uncontrolledValue;
96
+
97
+ const handleValueChange = (next: string) => {
98
+ if (controlledValue === undefined) {
99
+ setUncontrolledValue(next);
100
+ }
101
+ onValueChange?.(next);
102
+ };
103
+
104
+ return (
105
+ <RadioGroupContext.Provider
106
+ value={{ name, value, onValueChange: handleValueChange, size, variant, appearance, disabled }}
107
+ >
108
+ <div
109
+ ref={ref}
110
+ role="radiogroup"
111
+ className={cn(
112
+ "flex",
113
+ appearance === "segmented"
114
+ ? "inline-flex w-fit gap-0.5 rounded-lg border border-zinc-200 p-0.5 dark:border-zinc-800"
115
+ : variant === "card"
116
+ ? "gap-2.5"
117
+ : "gap-2.5",
118
+ orientation === "vertical" ? "flex-col" : "flex-row flex-wrap",
119
+ className,
120
+ )}
121
+ {...props}
122
+ >
123
+ {children}
124
+ </div>
125
+ </RadioGroupContext.Provider>
126
+ );
127
+ },
128
+ );
129
+ RadioGroup.displayName = "RadioGroup";
130
+
131
+ export interface RadioProps
132
+ extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type" | "size">,
133
+ VariantProps<typeof radioControlVariants> {
134
+ value: string;
135
+ label?: string;
136
+ description?: string;
137
+ variant?: "inline" | "card";
138
+ }
139
+
140
+ const Radio = React.forwardRef<HTMLInputElement, RadioProps>(
141
+ (
142
+ {
143
+ className,
144
+ size: sizeProp,
145
+ variant: variantProp,
146
+ value,
147
+ label,
148
+ description,
149
+ disabled: disabledProp,
150
+ id,
151
+ onChange,
152
+ checked: checkedProp,
153
+ ...inputProps
154
+ },
155
+ ref,
156
+ ) => {
157
+ const context = React.useContext(RadioGroupContext);
158
+ const generatedId = React.useId();
159
+ const inputId = id ?? generatedId;
160
+ const size = sizeProp ?? context?.size ?? "md";
161
+ const groupVariant = variantProp ?? context?.variant ?? "inline";
162
+ const appearance = context?.appearance ?? "default";
163
+ const resolvedVariant =
164
+ appearance === "segmented"
165
+ ? "inline"
166
+ : groupVariant === "card" || description
167
+ ? "card"
168
+ : "inline";
169
+ const disabled = disabledProp ?? context?.disabled;
170
+ const isControlled = context !== null || checkedProp !== undefined;
171
+ const checked = context ? context.value === value : checkedProp;
172
+ const alignStart = resolvedVariant === "card" || Boolean(description);
173
+
174
+ const input = (
175
+ <input
176
+ {...inputProps}
177
+ ref={ref}
178
+ id={inputId}
179
+ type="radio"
180
+ name={context?.name}
181
+ value={value}
182
+ disabled={disabled}
183
+ checked={isControlled ? checked : undefined}
184
+ defaultChecked={!isControlled ? checkedProp : undefined}
185
+ onChange={(event) => {
186
+ onChange?.(event);
187
+ if (event.target.checked) {
188
+ context?.onValueChange?.(value);
189
+ }
190
+ }}
191
+ className="peer absolute inset-0 z-10 cursor-pointer opacity-0 disabled:cursor-not-allowed"
192
+ />
193
+ );
194
+
195
+ return (
196
+ <label
197
+ htmlFor={inputId}
198
+ className={cn(
199
+ appearance === "segmented"
200
+ ? "relative inline-flex items-center rounded-md px-3 py-1.5 text-sm font-medium text-zinc-600 transition-colors duration-150 has-[:checked]:bg-zinc-900 has-[:checked]:text-white dark:text-zinc-400 dark:has-[:checked]:bg-zinc-100 dark:has-[:checked]:text-zinc-900"
201
+ : radioWrapperVariants({ variant: resolvedVariant }),
202
+ appearance === "segmented" ? "items-center" : alignStart ? "items-start" : "items-center",
203
+ disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
204
+ className,
205
+ )}
206
+ >
207
+ {appearance === "segmented" ? (
208
+ input
209
+ ) : (
210
+ <span
211
+ className={cn(
212
+ "relative flex shrink-0 items-center justify-center p-2.5 -m-2.5",
213
+ alignStart ? "-mt-[3px]" : undefined,
214
+ )}
215
+ >
216
+ {input}
217
+ <span
218
+ aria-hidden
219
+ className="pointer-events-none absolute inset-0 rounded-full bg-zinc-900/0 transition-colors duration-200 group-hover:bg-zinc-900/[0.08] group-active:bg-zinc-900/[0.12] peer-disabled:group-hover:bg-transparent dark:group-hover:bg-white/[0.08] dark:group-active:bg-white/[0.12]"
220
+ />
221
+ <span className={radioControlVariants({ size })}>
222
+ <span className="radio-dot scale-0 rounded-full bg-zinc-900 opacity-0 transition-all duration-200 dark:bg-zinc-100" />
223
+ </span>
224
+ </span>
225
+ )}
226
+ {(label || description) && (
227
+ <span
228
+ className={cn(
229
+ "flex min-w-0 flex-col",
230
+ appearance === "segmented" ? "gap-0" : "flex-1 gap-0.5",
231
+ )}
232
+ >
233
+ {label && (
234
+ <span
235
+ className={cn(
236
+ appearance === "segmented"
237
+ ? "leading-none"
238
+ : cn(
239
+ "text-sm leading-snug text-zinc-800 dark:text-zinc-100",
240
+ size === "sm" && "text-xs",
241
+ size === "md" && "text-sm",
242
+ size === "lg" && "text-base",
243
+ ),
244
+ )}
245
+ >
246
+ {label}
247
+ </span>
248
+ )}
249
+ {description && (
250
+ <span className="text-xs leading-relaxed text-zinc-500 dark:text-zinc-400">
251
+ {description}
252
+ </span>
253
+ )}
254
+ </span>
255
+ )}
256
+ </label>
257
+ );
258
+ },
259
+ );
260
+ Radio.displayName = "Radio";
261
+
262
+ export { Radio, RadioGroup, radioControlVariants };
@@ -0,0 +1,102 @@
1
+ import React from "react";
2
+
3
+ const SkeletonBar = ({ className }: { className?: string }) => (
4
+ <div className={`animate-pulse rounded bg-zinc-200 dark:bg-zinc-800 ${className}`} />
5
+ );
6
+
7
+ export function AccordionSkeletonPlus({ count = 3 }: { count?: number }) {
8
+ return (
9
+ <div className="w-full">
10
+ {Array.from({ length: count }).map((_, i) => (
11
+ <div key={i} className="flex items-center gap-3 py-4 border-b border-zinc-100 dark:border-zinc-800 last:border-0">
12
+ <SkeletonBar className="h-5 w-5 rounded-full shrink-0" />
13
+ <SkeletonBar className="h-4 w-2/3" />
14
+ </div>
15
+ ))}
16
+ </div>
17
+ );
18
+ }
19
+
20
+ export function AccordionSkeletonChevron({ count = 3 }: { count?: number }) {
21
+ return (
22
+ <div className="w-full">
23
+ {Array.from({ length: count }).map((_, i) => (
24
+ <div key={i} className="flex items-center justify-between py-5 border-b border-zinc-100 dark:border-zinc-800 last:border-0">
25
+ <SkeletonBar className="h-4 w-1/2" />
26
+ <SkeletonBar className="h-4 w-4 rounded-sm shrink-0" />
27
+ </div>
28
+ ))}
29
+ </div>
30
+ );
31
+ }
32
+
33
+ export function CardSkeletonGrid({ count = 3 }: { count?: number }) {
34
+ return (
35
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 w-full">
36
+ {Array.from({ length: count }).map((_, i) => (
37
+ <div key={i} className="flex flex-col gap-3">
38
+ <SkeletonBar className="aspect-video w-full rounded-md" />
39
+ <SkeletonBar className="h-4 w-full" />
40
+ <SkeletonBar className="h-4 w-[60%]" />
41
+ </div>
42
+ ))}
43
+ </div>
44
+ );
45
+ }
46
+
47
+ export function ListSkeleton({ count = 3 }: { count?: number }) {
48
+ return (
49
+ <div className="w-full space-y-4">
50
+ {Array.from({ length: count }).map((_, i) => (
51
+ <div key={i} className="flex flex-col gap-4 p-4 border border-zinc-100 dark:border-zinc-800 rounded-lg bg-white dark:bg-zinc-900/50">
52
+ <div className="flex items-center gap-3">
53
+ <SkeletonBar className="h-10 w-10 rounded-full shrink-0" />
54
+ <div className="flex flex-col gap-2">
55
+ <SkeletonBar className="h-3 w-24" />
56
+ <SkeletonBar className="h-3 w-40" />
57
+ </div>
58
+ </div>
59
+ <div className="flex flex-col gap-2 mt-1">
60
+ <SkeletonBar className="h-3 w-full" />
61
+ <SkeletonBar className="h-3 w-[92%]" />
62
+ </div>
63
+ </div>
64
+ ))}
65
+ </div>
66
+ );
67
+ }
68
+
69
+ export function TableSkeleton({ count = 5 }: { count?: number }) {
70
+ return (
71
+ <div className="w-full overflow-hidden rounded-lg border border-zinc-100 dark:border-zinc-800">
72
+ <div className="overflow-x-auto">
73
+ <table className="w-full text-sm text-left">
74
+ <thead className="bg-zinc-50/50 dark:bg-zinc-900/50 border-b border-zinc-100 dark:border-zinc-800 text-zinc-500">
75
+ <tr>
76
+ <th className="px-4 py-3 font-medium">Id</th>
77
+ <th className="px-4 py-3 font-medium">Image</th>
78
+ <th className="px-4 py-3 font-medium">Name</th>
79
+ <th className="px-4 py-3 font-medium">Category</th>
80
+ <th className="px-4 py-3 font-medium">Price</th>
81
+ <th className="px-4 py-3 font-medium">Stock</th>
82
+ <th className="px-4 py-3 font-medium">Description</th>
83
+ </tr>
84
+ </thead>
85
+ <tbody className="divide-y divide-zinc-100 dark:divide-zinc-800">
86
+ {Array.from({ length: count }).map((_, i) => (
87
+ <tr key={i}>
88
+ <td className="px-4 py-4"><SkeletonBar className="h-4 w-8" /></td>
89
+ <td className="px-4 py-4"><SkeletonBar className="h-8 w-12 rounded" /></td>
90
+ <td className="px-4 py-4"><SkeletonBar className="h-4 w-24" /></td>
91
+ <td className="px-4 py-4"><SkeletonBar className="h-4 w-20" /></td>
92
+ <td className="px-4 py-4"><SkeletonBar className="h-4 w-12" /></td>
93
+ <td className="px-4 py-4"><SkeletonBar className="h-4 w-12" /></td>
94
+ <td className="px-4 py-4"><SkeletonBar className="h-4 w-48" /></td>
95
+ </tr>
96
+ ))}
97
+ </tbody>
98
+ </table>
99
+ </div>
100
+ </div>
101
+ );
102
+ }