trix-ui 0.2.1 → 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.
- package/README.md +110 -19
- package/dist/commands/add/__tests__/add.test.js +16 -4
- package/dist/commands/add/__tests__/add.test.js.map +1 -1
- package/dist/commands/add/analysis.js +6 -1
- package/dist/commands/add/analysis.js.map +1 -1
- package/dist/commands/add/command.js +6 -0
- package/dist/commands/add/command.js.map +1 -1
- package/dist/commands/add/types.d.ts +1 -0
- package/dist/commands/add/ui.js +4 -0
- package/dist/commands/add/ui.js.map +1 -1
- package/dist/commands/add-composite.d.ts +2 -0
- package/dist/commands/add-composite.js +202 -0
- package/dist/commands/add-composite.js.map +1 -0
- package/dist/commands/add-section.js +6 -0
- package/dist/commands/add-section.js.map +1 -1
- package/dist/commands/add-wrapper.js +6 -0
- package/dist/commands/add-wrapper.js.map +1 -1
- package/dist/commands/doctor.js +7 -2
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init/config.js +1 -0
- package/dist/commands/init/config.js.map +1 -1
- package/dist/commands/list.js +12 -4
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/remove.js +24 -10
- package/dist/commands/remove.js.map +1 -1
- package/dist/commands/shared/add-collection.d.ts +2 -1
- package/dist/commands/shared/add-collection.js +8 -2
- package/dist/commands/shared/add-collection.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.js +1 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/lockfile.d.ts +6 -5
- package/dist/lib/lockfile.js +3 -0
- package/dist/lib/lockfile.js.map +1 -1
- package/dist/lib/paths.d.ts +1 -0
- package/dist/lib/paths.js +1 -0
- package/dist/lib/paths.js.map +1 -1
- package/dist/lib/registry.d.ts +2 -0
- package/dist/lib/registry.js +11 -0
- package/dist/lib/registry.js.map +1 -1
- package/package.json +12 -11
- package/registry/index.json +67 -128
- package/templates/components/ui/avatar.tsx +109 -0
- package/templates/components/ui/button.tsx +48 -44
- package/templates/components/ui/label.tsx +24 -0
- package/templates/composites/feature-collection-card.tsx +113 -0
- package/templates/composites/music-player-card.tsx +221 -0
- package/templates/composites/user-profile-card.tsx +145 -0
- package/templates/sections/modern-hero.tsx +1226 -0
- package/templates/wrappers/Interative-wrapper.tsx +555 -0
- package/LICENSE.md +0 -21
- package/templates/components/ui/checkbox.tsx +0 -33
- package/templates/components/ui/dialog.tsx +0 -92
- package/templates/components/ui/dropdown.tsx +0 -75
- package/templates/components/ui/select.tsx +0 -24
- package/templates/components/ui/switch.tsx +0 -27
- package/templates/components/ui/toast.tsx +0 -100
- package/templates/sections/cta.tsx +0 -22
- package/templates/sections/feature-grid.tsx +0 -62
- package/templates/sections/hero.tsx +0 -63
- package/templates/wrappers/border-wrapper.tsx +0 -34
- package/templates/wrappers/glow-wrapper.tsx +0 -31
- package/templates/wrappers/lift-wrapper.tsx +0 -27
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
export type FeaturedCollectionCardProps = {
|
|
5
|
+
imageSrc: string;
|
|
6
|
+
title: string;
|
|
7
|
+
|
|
8
|
+
imageAlt?: string;
|
|
9
|
+
badgeText?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
|
|
12
|
+
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
|
13
|
+
|
|
14
|
+
className?: string;
|
|
15
|
+
contentClassName?: string;
|
|
16
|
+
|
|
17
|
+
minHeightClassName?: string;
|
|
18
|
+
overlayClassName?: string;
|
|
19
|
+
imageHoverScaleClassName?: string;
|
|
20
|
+
|
|
21
|
+
badgeClassName?: string;
|
|
22
|
+
titleClassName?: string;
|
|
23
|
+
descriptionClassName?: string;
|
|
24
|
+
|
|
25
|
+
disableHoverReveal?: boolean;
|
|
26
|
+
} & Omit<React.HTMLAttributes<HTMLDivElement>, "title" | "onClick">;
|
|
27
|
+
|
|
28
|
+
const FeaturedCollectionCardBase = React.forwardRef<HTMLDivElement, FeaturedCollectionCardProps>(
|
|
29
|
+
(
|
|
30
|
+
{
|
|
31
|
+
imageSrc,
|
|
32
|
+
title,
|
|
33
|
+
imageAlt = "Background",
|
|
34
|
+
badgeText = "Featured Collection",
|
|
35
|
+
description,
|
|
36
|
+
onClick,
|
|
37
|
+
|
|
38
|
+
minHeightClassName = "min-h-[300px]",
|
|
39
|
+
overlayClassName = "bg-gradient-to-t from-black/90 via-black/40 to-transparent",
|
|
40
|
+
imageHoverScaleClassName = "group-hover:scale-110",
|
|
41
|
+
|
|
42
|
+
badgeClassName = "text-cyan-400",
|
|
43
|
+
titleClassName,
|
|
44
|
+
descriptionClassName,
|
|
45
|
+
|
|
46
|
+
className,
|
|
47
|
+
contentClassName,
|
|
48
|
+
disableHoverReveal = false,
|
|
49
|
+
|
|
50
|
+
...props
|
|
51
|
+
},
|
|
52
|
+
ref
|
|
53
|
+
) => {
|
|
54
|
+
const motionWrap = disableHoverReveal
|
|
55
|
+
? "translate-y-0"
|
|
56
|
+
: "translate-y-4 group-hover:translate-y-0 transition-transform duration-300";
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
ref={ref}
|
|
61
|
+
onClick={onClick}
|
|
62
|
+
className={cn(
|
|
63
|
+
"group relative h-full rounded-2xl overflow-hidden shadow-lg cursor-pointer",
|
|
64
|
+
minHeightClassName,
|
|
65
|
+
className
|
|
66
|
+
)}
|
|
67
|
+
{...props}
|
|
68
|
+
>
|
|
69
|
+
<img
|
|
70
|
+
src={imageSrc}
|
|
71
|
+
alt={imageAlt}
|
|
72
|
+
loading="lazy"
|
|
73
|
+
decoding="async"
|
|
74
|
+
className={cn(
|
|
75
|
+
"absolute inset-0 h-full w-full object-cover transition-transform duration-700 will-change-transform",
|
|
76
|
+
imageHoverScaleClassName
|
|
77
|
+
)}
|
|
78
|
+
/>
|
|
79
|
+
|
|
80
|
+
<div className={cn("absolute inset-0", overlayClassName)} />
|
|
81
|
+
|
|
82
|
+
<div className={cn("absolute bottom-0 left-0 w-full p-6", contentClassName)}>
|
|
83
|
+
<div className={cn("transform", motionWrap)}>
|
|
84
|
+
{badgeText && (
|
|
85
|
+
<span className={cn("mb-2 block text-xs font-bold uppercase tracking-wider", badgeClassName)}>
|
|
86
|
+
{badgeText}
|
|
87
|
+
</span>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
<h3 className={cn("mb-2 text-2xl font-bold text-white", titleClassName)}>{title}</h3>
|
|
91
|
+
|
|
92
|
+
{description && (
|
|
93
|
+
<p
|
|
94
|
+
className={cn(
|
|
95
|
+
"text-sm text-stone-400 overflow-hidden transition-all duration-300",
|
|
96
|
+
disableHoverReveal
|
|
97
|
+
? "max-h-40 opacity-100"
|
|
98
|
+
: "max-h-0 opacity-0 group-hover:max-h-40 group-hover:opacity-100",
|
|
99
|
+
descriptionClassName
|
|
100
|
+
)}
|
|
101
|
+
>
|
|
102
|
+
{description}
|
|
103
|
+
</p>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
FeaturedCollectionCardBase.displayName = "FeaturedCollectionCard";
|
|
113
|
+
export const FeaturedCollectionCard = React.memo(FeaturedCollectionCardBase);
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../../lib/utils";
|
|
3
|
+
import {
|
|
4
|
+
Shuffle,
|
|
5
|
+
SkipBack,
|
|
6
|
+
Play,
|
|
7
|
+
Pause,
|
|
8
|
+
SkipForward,
|
|
9
|
+
Repeat,
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
|
|
12
|
+
export type MusicPlayerCardProps = {
|
|
13
|
+
imageSrc: string;
|
|
14
|
+
title: string;
|
|
15
|
+
subtitle?: string;
|
|
16
|
+
|
|
17
|
+
/** 0 to 100 */
|
|
18
|
+
progress?: number;
|
|
19
|
+
currentTime?: string;
|
|
20
|
+
totalTime?: string;
|
|
21
|
+
|
|
22
|
+
onShuffle?: () => void;
|
|
23
|
+
onPrev?: () => void;
|
|
24
|
+
onPlay?: () => void;
|
|
25
|
+
onNext?: () => void;
|
|
26
|
+
onRepeat?: () => void;
|
|
27
|
+
|
|
28
|
+
/** Toggle states */
|
|
29
|
+
isPlaying?: boolean;
|
|
30
|
+
isShuffleOn?: boolean;
|
|
31
|
+
isRepeatOn?: boolean;
|
|
32
|
+
|
|
33
|
+
/** Styling overrides */
|
|
34
|
+
className?: string;
|
|
35
|
+
imageWrapClassName?: string;
|
|
36
|
+
titleClassName?: string;
|
|
37
|
+
subtitleClassName?: string;
|
|
38
|
+
|
|
39
|
+
progressTrackClassName?: string;
|
|
40
|
+
progressFillClassName?: string;
|
|
41
|
+
|
|
42
|
+
controlsClassName?: string;
|
|
43
|
+
sideButtonClassName?: string;
|
|
44
|
+
midButtonClassName?: string;
|
|
45
|
+
|
|
46
|
+
accentClassName?: string; // default rose
|
|
47
|
+
} & React.HTMLAttributes<HTMLDivElement>;
|
|
48
|
+
|
|
49
|
+
const clamp = (n: number, min: number, max: number) => Math.max(min, Math.min(max, n));
|
|
50
|
+
|
|
51
|
+
const MusicPlayerCardBase = React.forwardRef<HTMLDivElement, MusicPlayerCardProps>(
|
|
52
|
+
(
|
|
53
|
+
{
|
|
54
|
+
imageSrc,
|
|
55
|
+
title,
|
|
56
|
+
subtitle = "Unknown Artist",
|
|
57
|
+
|
|
58
|
+
progress = 0,
|
|
59
|
+
currentTime = "0:00",
|
|
60
|
+
totalTime = "0:00",
|
|
61
|
+
|
|
62
|
+
onShuffle,
|
|
63
|
+
onPrev,
|
|
64
|
+
onPlay,
|
|
65
|
+
onNext,
|
|
66
|
+
onRepeat,
|
|
67
|
+
|
|
68
|
+
isPlaying = false,
|
|
69
|
+
isShuffleOn = false,
|
|
70
|
+
isRepeatOn = false,
|
|
71
|
+
|
|
72
|
+
className,
|
|
73
|
+
imageWrapClassName,
|
|
74
|
+
titleClassName,
|
|
75
|
+
subtitleClassName,
|
|
76
|
+
|
|
77
|
+
progressTrackClassName,
|
|
78
|
+
progressFillClassName,
|
|
79
|
+
|
|
80
|
+
controlsClassName,
|
|
81
|
+
sideButtonClassName,
|
|
82
|
+
midButtonClassName,
|
|
83
|
+
|
|
84
|
+
accentClassName = "bg-rose-500",
|
|
85
|
+
|
|
86
|
+
...props
|
|
87
|
+
},
|
|
88
|
+
ref
|
|
89
|
+
) => {
|
|
90
|
+
const pct = clamp(progress, 0, 100);
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div
|
|
94
|
+
ref={ref}
|
|
95
|
+
className={cn(
|
|
96
|
+
"bg-white p-5 rounded-3xl border border-stone-100 flex flex-col h-full",
|
|
97
|
+
"shadow-lg transition-all duration-500 ease-out",
|
|
98
|
+
"hover:-translate-y-1 hover:shadow-2xl",
|
|
99
|
+
className
|
|
100
|
+
)}
|
|
101
|
+
{...props}
|
|
102
|
+
>
|
|
103
|
+
{/* Album Art */}
|
|
104
|
+
<div
|
|
105
|
+
className={cn(
|
|
106
|
+
"relative aspect-square rounded-2xl overflow-hidden mb-5 shadow-inner",
|
|
107
|
+
"ring-1 ring-black/5",
|
|
108
|
+
imageWrapClassName
|
|
109
|
+
)}
|
|
110
|
+
>
|
|
111
|
+
<img
|
|
112
|
+
src={imageSrc}
|
|
113
|
+
alt="Album Art"
|
|
114
|
+
loading="lazy"
|
|
115
|
+
decoding="async"
|
|
116
|
+
className="w-full h-full object-cover"
|
|
117
|
+
/>
|
|
118
|
+
<div className="absolute inset-0 bg-black/10" />
|
|
119
|
+
|
|
120
|
+
{/* Premium Glow */}
|
|
121
|
+
<div className="pointer-events-none absolute inset-0 opacity-0 hover:opacity-100 transition-opacity duration-700 bg-[radial-gradient(circle_at_30%_20%,rgba(255,255,255,0.18),transparent_55%)]" />
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
{/* Text */}
|
|
125
|
+
<div className="mb-4">
|
|
126
|
+
<h3 className={cn("text-lg font-bold text-stone-800 truncate", titleClassName)}>
|
|
127
|
+
{title}
|
|
128
|
+
</h3>
|
|
129
|
+
<p
|
|
130
|
+
className={cn(
|
|
131
|
+
"text-stone-400 text-xs font-medium uppercase tracking-wide truncate",
|
|
132
|
+
subtitleClassName
|
|
133
|
+
)}
|
|
134
|
+
>
|
|
135
|
+
{subtitle}
|
|
136
|
+
</p>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{/* Progress Bar */}
|
|
140
|
+
<div
|
|
141
|
+
className={cn(
|
|
142
|
+
"w-full bg-stone-100 h-1.5 rounded-full mb-2 overflow-hidden",
|
|
143
|
+
progressTrackClassName
|
|
144
|
+
)}
|
|
145
|
+
>
|
|
146
|
+
<div
|
|
147
|
+
className={cn(
|
|
148
|
+
"h-full rounded-full transition-all duration-500 ease-out",
|
|
149
|
+
accentClassName,
|
|
150
|
+
progressFillClassName
|
|
151
|
+
)}
|
|
152
|
+
style={{ width: `${pct}%` }}
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div className="flex justify-between text-[10px] text-stone-400 font-mono mb-4">
|
|
157
|
+
<span>{currentTime}</span>
|
|
158
|
+
<span>{totalTime}</span>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{/* Controls */}
|
|
162
|
+
<div className={cn("flex justify-between items-center mt-auto px-2", controlsClassName)}>
|
|
163
|
+
<button
|
|
164
|
+
type="button"
|
|
165
|
+
onClick={onShuffle}
|
|
166
|
+
className={cn(
|
|
167
|
+
"text-stone-400 transition-colors hover:text-stone-800",
|
|
168
|
+
isShuffleOn && "text-stone-800",
|
|
169
|
+
sideButtonClassName
|
|
170
|
+
)}
|
|
171
|
+
>
|
|
172
|
+
<Shuffle className="h-5 w-5" />
|
|
173
|
+
</button>
|
|
174
|
+
|
|
175
|
+
<button
|
|
176
|
+
type="button"
|
|
177
|
+
onClick={onPrev}
|
|
178
|
+
className={cn("text-stone-800 transition-colors hover:text-rose-500", midButtonClassName)}
|
|
179
|
+
>
|
|
180
|
+
<SkipBack className="h-8 w-8" />
|
|
181
|
+
</button>
|
|
182
|
+
|
|
183
|
+
<button
|
|
184
|
+
type="button"
|
|
185
|
+
onClick={onPlay}
|
|
186
|
+
className={cn(
|
|
187
|
+
"w-12 h-12 text-white rounded-full flex items-center justify-center shadow-lg",
|
|
188
|
+
"transition-all duration-300 ease-out hover:scale-105 active:scale-95",
|
|
189
|
+
accentClassName
|
|
190
|
+
)}
|
|
191
|
+
>
|
|
192
|
+
{isPlaying ? <Pause className="h-6 w-6" /> : <Play className="h-6 w-6" />}
|
|
193
|
+
</button>
|
|
194
|
+
|
|
195
|
+
<button
|
|
196
|
+
type="button"
|
|
197
|
+
onClick={onNext}
|
|
198
|
+
className={cn("text-stone-800 transition-colors hover:text-rose-500", midButtonClassName)}
|
|
199
|
+
>
|
|
200
|
+
<SkipForward className="h-8 w-8" />
|
|
201
|
+
</button>
|
|
202
|
+
|
|
203
|
+
<button
|
|
204
|
+
type="button"
|
|
205
|
+
onClick={onRepeat}
|
|
206
|
+
className={cn(
|
|
207
|
+
"text-stone-400 transition-colors hover:text-stone-800",
|
|
208
|
+
isRepeatOn && "text-stone-800",
|
|
209
|
+
sideButtonClassName
|
|
210
|
+
)}
|
|
211
|
+
>
|
|
212
|
+
<Repeat className="h-5 w-5" />
|
|
213
|
+
</button>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
MusicPlayerCardBase.displayName = "MusicPlayerCard";
|
|
221
|
+
export const MusicPlayerCard = React.memo(MusicPlayerCardBase);
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
import { Card } from "@/components/ui/card";
|
|
4
|
+
import { Badge } from "@/components/ui/badge";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { ArrowRight } from "lucide-react";
|
|
7
|
+
|
|
8
|
+
type UserProfileCardTag = {
|
|
9
|
+
label: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type UserProfileCardProps = {
|
|
13
|
+
name: string;
|
|
14
|
+
role: string;
|
|
15
|
+
avatarSrc: string;
|
|
16
|
+
avatarAlt?: string;
|
|
17
|
+
|
|
18
|
+
/** Example: [{ label: "Figma" }, { label: "Design Ops" }] */
|
|
19
|
+
tags?: UserProfileCardTag[];
|
|
20
|
+
|
|
21
|
+
/** You can pass "1.2k" directly OR number like 1200 */
|
|
22
|
+
followers?: string | number;
|
|
23
|
+
|
|
24
|
+
/** Online green dot */
|
|
25
|
+
online?: boolean;
|
|
26
|
+
|
|
27
|
+
/** Right action (arrow button) */
|
|
28
|
+
onActionClick?: () => void;
|
|
29
|
+
actionAriaLabel?: string;
|
|
30
|
+
|
|
31
|
+
className?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function formatFollowers(value: string | number | undefined) {
|
|
35
|
+
if (value === undefined || value === null) return "";
|
|
36
|
+
if (typeof value === "string") return value;
|
|
37
|
+
|
|
38
|
+
// number formatting like 1200 -> 1.2k
|
|
39
|
+
const n = value;
|
|
40
|
+
if (n < 1000) return `${n}`;
|
|
41
|
+
if (n < 1_000_000) return `${(n / 1000).toFixed(n % 1000 === 0 ? 0 : 1)}k`;
|
|
42
|
+
return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function UserProfileCard({
|
|
46
|
+
name,
|
|
47
|
+
role,
|
|
48
|
+
avatarSrc,
|
|
49
|
+
avatarAlt = name,
|
|
50
|
+
tags = [],
|
|
51
|
+
followers = "1.2k",
|
|
52
|
+
online = true,
|
|
53
|
+
onActionClick,
|
|
54
|
+
actionAriaLabel = "Open profile",
|
|
55
|
+
className,
|
|
56
|
+
}: UserProfileCardProps) {
|
|
57
|
+
return (
|
|
58
|
+
<Card
|
|
59
|
+
className={cn(
|
|
60
|
+
"w-[280px] rounded-[28px] bg-white shadow-[0_18px_60px_-35px_rgba(15,23,42,0.35)]",
|
|
61
|
+
"border border-neutral-100",
|
|
62
|
+
className
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
<div className="px-6 pt-6 pb-5">
|
|
66
|
+
{/* Avatar */}
|
|
67
|
+
<div className="flex justify-center">
|
|
68
|
+
<div className="relative">
|
|
69
|
+
{/* Gradient ring */}
|
|
70
|
+
<div className="grid place-items-center rounded-full p-[3px] bg-gradient-to-br from-violet-500 to-indigo-500">
|
|
71
|
+
<div className="rounded-full bg-white p-[3px]">
|
|
72
|
+
<img
|
|
73
|
+
src={avatarSrc}
|
|
74
|
+
alt={avatarAlt}
|
|
75
|
+
className="h-[74px] w-[74px] rounded-full object-cover"
|
|
76
|
+
draggable={false}
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{/* Online dot */}
|
|
82
|
+
{online && (
|
|
83
|
+
<span className="absolute bottom-[7px] right-[7px] h-3.5 w-3.5 rounded-full bg-emerald-500 ring-4 ring-white" />
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Name + Role */}
|
|
89
|
+
<div className="mt-4 text-center">
|
|
90
|
+
<div className="text-[18px] font-semibold leading-tight text-slate-900">
|
|
91
|
+
{name}
|
|
92
|
+
</div>
|
|
93
|
+
<div className="mt-1 text-[11px] font-semibold tracking-[0.18em] text-indigo-500">
|
|
94
|
+
{role.toUpperCase()}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
{/* Tags */}
|
|
99
|
+
{tags.length > 0 && (
|
|
100
|
+
<div className="mt-4 flex justify-center gap-2">
|
|
101
|
+
{tags.map((t, idx) => (
|
|
102
|
+
<Badge
|
|
103
|
+
key={`${t.label}-${idx}`}
|
|
104
|
+
variant="secondary"
|
|
105
|
+
className={cn(
|
|
106
|
+
"rounded-full px-3 py-1 text-[10px] font-semibold tracking-widest",
|
|
107
|
+
"bg-neutral-100 text-neutral-700 shadow-none border border-neutral-200"
|
|
108
|
+
)}
|
|
109
|
+
>
|
|
110
|
+
{t.label.toUpperCase()}
|
|
111
|
+
</Badge>
|
|
112
|
+
))}
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
{/* Bottom section */}
|
|
117
|
+
<div className="mt-6 flex items-end justify-between">
|
|
118
|
+
<div>
|
|
119
|
+
<div className="text-[10px] font-semibold tracking-[0.18em] text-neutral-300">
|
|
120
|
+
FOLLOWERS
|
|
121
|
+
</div>
|
|
122
|
+
<div className="mt-1 text-[16px] font-semibold text-slate-900">
|
|
123
|
+
{formatFollowers(followers)}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<Button
|
|
128
|
+
type="button"
|
|
129
|
+
variant="ghost"
|
|
130
|
+
size="icon"
|
|
131
|
+
onClick={onActionClick}
|
|
132
|
+
aria-label={actionAriaLabel}
|
|
133
|
+
className={cn(
|
|
134
|
+
"h-10 w-10 rounded-full",
|
|
135
|
+
"text-violet-600 hover:text-violet-700",
|
|
136
|
+
"hover:bg-violet-50 active:bg-violet-100"
|
|
137
|
+
)}
|
|
138
|
+
>
|
|
139
|
+
<ArrowRight className="h-5 w-5" />
|
|
140
|
+
</Button>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</Card>
|
|
144
|
+
);
|
|
145
|
+
}
|