@nikala-ui/core 0.10.1-nightly.4e09f2a → 0.11.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.
- package/package.json +1 -1
- package/registry/bubble.json +18 -0
- package/registry/create-chat-scroll.json +13 -0
- package/registry/create-pagination.json +13 -0
- package/registry/footer.json +18 -0
- package/registry/forgot-password-01.json +28 -0
- package/registry/hero-01.json +22 -0
- package/registry/index.json +288 -0
- package/registry/login-01.json +28 -0
- package/registry/marker.json +17 -0
- package/registry/marquee.json +17 -0
- package/registry/message.json +17 -0
- package/registry/navbar.json +19 -0
- package/registry/navigation-menu.json +22 -0
- package/registry/otp-verification-01.json +26 -0
- package/registry/pagination.json +22 -0
- package/registry/rating.json +19 -0
- package/registry/register-01.json +31 -0
- package/registry/review-card.json +23 -0
- package/registry/scroll-area.json +1 -1
- package/registry/sidebar.json +24 -0
- package/registry/spinner.json +1 -1
- package/registry/stat.json +19 -0
- package/registry/timeline.json +18 -0
- package/registry/toggle-group.json +21 -0
- package/src/registry/blocks/forgot-password-01.tsx +141 -0
- package/src/registry/blocks/hero-01.tsx +28 -0
- package/src/registry/blocks/login-01.tsx +231 -0
- package/src/registry/blocks/otp-verification-01.tsx +170 -0
- package/src/registry/blocks/register-01.tsx +318 -0
- package/src/registry/components/ui/bubble.tsx +142 -0
- package/src/registry/components/ui/footer.tsx +247 -0
- package/src/registry/components/ui/marker.tsx +102 -0
- package/src/registry/components/ui/marquee.tsx +118 -0
- package/src/registry/components/ui/message.tsx +168 -0
- package/src/registry/components/ui/navbar.tsx +368 -0
- package/src/registry/components/ui/navigation-menu.tsx +358 -0
- package/src/registry/components/ui/pagination.tsx +258 -0
- package/src/registry/components/ui/rating.tsx +185 -0
- package/src/registry/components/ui/review-card.tsx +195 -0
- package/src/registry/components/ui/scroll-area.tsx +2 -2
- package/src/registry/components/ui/sidebar.tsx +692 -0
- package/src/registry/components/ui/spinner.tsx +1 -1
- package/src/registry/components/ui/stat.tsx +245 -0
- package/src/registry/components/ui/timeline.tsx +350 -0
- package/src/registry/components/ui/toggle-group.tsx +203 -0
- package/src/registry/index.ts +3 -3
- package/src/registry/metadata.ts +120 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { createSignal, Show, type Component } from "solid-js";
|
|
2
|
+
import { createForm } from "@nikala-ui/hooks";
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
CardHeader,
|
|
6
|
+
CardTitle,
|
|
7
|
+
CardDescription,
|
|
8
|
+
CardContent,
|
|
9
|
+
CardFooter,
|
|
10
|
+
} from "../components/ui/card";
|
|
11
|
+
import { Form } from "../components/ui/form";
|
|
12
|
+
import { Field, FieldLabel } from "../components/ui/field";
|
|
13
|
+
import { FormMessage } from "../components/ui/form-message";
|
|
14
|
+
import { Button } from "../components/ui/button";
|
|
15
|
+
import { Input } from "../components/ui/input";
|
|
16
|
+
import { Checkbox } from "../components/ui/checkbox";
|
|
17
|
+
import { Label } from "../components/ui/label";
|
|
18
|
+
import { Progress } from "../components/ui/progress";
|
|
19
|
+
import { Badge } from "../components/ui/badge";
|
|
20
|
+
import { Separator } from "../components/ui/separator";
|
|
21
|
+
import { Eye, EyeOff, Check, X, ArrowRight, ShieldCheck } from "lucide-solid";
|
|
22
|
+
|
|
23
|
+
export const Register01: Component = () => {
|
|
24
|
+
const [showPassword, setShowPassword] = createSignal(false);
|
|
25
|
+
|
|
26
|
+
const form = createForm({
|
|
27
|
+
initialValues: {
|
|
28
|
+
fullName: "",
|
|
29
|
+
email: "",
|
|
30
|
+
password: "",
|
|
31
|
+
agreeTerms: false,
|
|
32
|
+
},
|
|
33
|
+
validate: (values) => {
|
|
34
|
+
const errors: Record<string, string> = {};
|
|
35
|
+
if (!values.fullName.trim()) {
|
|
36
|
+
errors.fullName = "Full name is required";
|
|
37
|
+
}
|
|
38
|
+
if (!values.email.trim()) {
|
|
39
|
+
errors.email = "Email is required";
|
|
40
|
+
} else if (!values.email.includes("@")) {
|
|
41
|
+
errors.email = "Please enter a valid email address";
|
|
42
|
+
}
|
|
43
|
+
if (!values.password) {
|
|
44
|
+
errors.password = "Password is required";
|
|
45
|
+
} else if (values.password.length < 8) {
|
|
46
|
+
errors.password = "Password must be at least 8 characters";
|
|
47
|
+
}
|
|
48
|
+
if (!values.agreeTerms) {
|
|
49
|
+
errors.agreeTerms = "You must agree to the terms and privacy policy";
|
|
50
|
+
}
|
|
51
|
+
return errors;
|
|
52
|
+
},
|
|
53
|
+
onSubmit: async (values) => {
|
|
54
|
+
// Simulate API registration request
|
|
55
|
+
await new Promise((resolve) => setTimeout(resolve, 1200));
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const password = () => form.values().password;
|
|
60
|
+
|
|
61
|
+
// Reactive password criteria calculations
|
|
62
|
+
const hasMinLength = () => password().length >= 8;
|
|
63
|
+
const hasNumber = () => /\d/.test(password());
|
|
64
|
+
const hasSpecial = () => /[^A-Za-z0-9]/.test(password());
|
|
65
|
+
const hasUpperLower = () => /[a-z]/.test(password()) && /[A-Z]/.test(password());
|
|
66
|
+
|
|
67
|
+
const strengthScore = () => {
|
|
68
|
+
let score = 0;
|
|
69
|
+
if (password().length === 0) return 0;
|
|
70
|
+
if (hasMinLength()) score += 25;
|
|
71
|
+
if (hasNumber()) score += 25;
|
|
72
|
+
if (hasSpecial()) score += 25;
|
|
73
|
+
if (hasUpperLower()) score += 25;
|
|
74
|
+
return score;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const strengthLabel = () => {
|
|
78
|
+
const score = strengthScore();
|
|
79
|
+
if (score === 0) return "None";
|
|
80
|
+
if (score <= 25) return "Weak";
|
|
81
|
+
if (score <= 50) return "Fair";
|
|
82
|
+
if (score <= 75) return "Good";
|
|
83
|
+
return "Strong";
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const strengthColorClass = () => {
|
|
87
|
+
const score = strengthScore();
|
|
88
|
+
if (score <= 25) return "bg-destructive";
|
|
89
|
+
if (score <= 50) return "bg-amber-500";
|
|
90
|
+
if (score <= 75) return "bg-blue-500";
|
|
91
|
+
return "bg-emerald-500";
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const strengthBadgeVariant = () => {
|
|
95
|
+
const score = strengthScore();
|
|
96
|
+
if (score <= 25) return "destructive";
|
|
97
|
+
if (score <= 50) return "outline";
|
|
98
|
+
return "secondary";
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const isFormValid = () =>
|
|
102
|
+
form.values().fullName.trim() !== "" &&
|
|
103
|
+
form.values().email.trim() !== "" &&
|
|
104
|
+
form.values().agreeTerms &&
|
|
105
|
+
strengthScore() >= 50;
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div class="@container w-full min-h-[600px] flex items-center justify-center p-4 sm:p-6 md:p-10">
|
|
109
|
+
<Card class="w-full max-w-lg border-border shadow-md bg-card">
|
|
110
|
+
{/* Card Header */}
|
|
111
|
+
<CardHeader class="space-y-2 text-center pb-6">
|
|
112
|
+
<div class="mx-auto size-10 rounded-lg bg-primary/10 text-primary flex items-center justify-center mb-1">
|
|
113
|
+
<ShieldCheck class="size-5" />
|
|
114
|
+
</div>
|
|
115
|
+
<CardTitle class="text-2xl font-bold tracking-tight">Create your account</CardTitle>
|
|
116
|
+
<CardDescription class="text-xs sm:text-sm">
|
|
117
|
+
Join thousands of developers building fast SolidJS interfaces
|
|
118
|
+
</CardDescription>
|
|
119
|
+
</CardHeader>
|
|
120
|
+
|
|
121
|
+
<CardContent class="space-y-6">
|
|
122
|
+
{/* Social OAuth Buttons */}
|
|
123
|
+
<div class="grid grid-cols-1 @xs:grid-cols-2 gap-2.5">
|
|
124
|
+
<Button variant="outline" class="w-full justify-center text-xs h-9">
|
|
125
|
+
<svg class="size-4 mr-2 shrink-0" viewBox="0 0 24 24" fill="currentColor">
|
|
126
|
+
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
|
127
|
+
</svg>
|
|
128
|
+
GitHub
|
|
129
|
+
</Button>
|
|
130
|
+
<Button variant="outline" class="w-full justify-center text-xs h-9">
|
|
131
|
+
<svg class="size-4 mr-2 shrink-0" viewBox="0 0 24 24">
|
|
132
|
+
<path
|
|
133
|
+
fill="#4285F4"
|
|
134
|
+
d="M23.745 12.27c0-.7-.06-1.4-.19-2.07H12v4.51h6.6c-.29 1.52-1.14 2.82-2.4 3.68v3.05h3.88c2.27-2.09 3.66-5.17 3.66-9.17z"
|
|
135
|
+
/>
|
|
136
|
+
<path
|
|
137
|
+
fill="#34A853"
|
|
138
|
+
d="M12 24c3.24 0 5.95-1.08 7.93-2.91l-3.88-3.05c-1.08.72-2.45 1.16-4.05 1.16-3.12 0-5.77-2.1-6.72-4.93H1.25v3.15C3.26 21.36 7.33 24 12 24z"
|
|
139
|
+
/>
|
|
140
|
+
<path
|
|
141
|
+
fill="#FBBC05"
|
|
142
|
+
d="M5.28 14.27c-.25-.72-.38-1.49-.38-2.27s.13-1.55.38-2.27V6.58H1.25C.45 8.18 0 10.03 0 12s.45 3.82 1.25 5.42l4.03-3.15z"
|
|
143
|
+
/>
|
|
144
|
+
<path
|
|
145
|
+
fill="#EA4335"
|
|
146
|
+
d="M12 4.75c1.77 0 3.35.61 4.6 1.8l3.42-3.42C17.95 1.19 15.24 0 12 0 7.33 0 3.26 2.64 1.25 6.58l4.03 3.15c.95-2.83 3.6-4.98 6.72-4.98z"
|
|
147
|
+
/>
|
|
148
|
+
</svg>
|
|
149
|
+
Google
|
|
150
|
+
</Button>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div class="relative">
|
|
154
|
+
<div class="absolute inset-0 flex items-center">
|
|
155
|
+
<Separator />
|
|
156
|
+
</div>
|
|
157
|
+
<div class="relative flex justify-center text-xs uppercase">
|
|
158
|
+
<span class="bg-card px-3 text-muted-foreground font-medium text-[11px]">
|
|
159
|
+
Or register with email
|
|
160
|
+
</span>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
{/* Registration Form with Nikala UI Form & Field ecosystem */}
|
|
165
|
+
<Form onSubmit={form.handleSubmit} loading={form.isSubmitting()} class="space-y-4">
|
|
166
|
+
<Field>
|
|
167
|
+
<FieldLabel for="reg-name" class="text-xs sm:text-sm">Full Name</FieldLabel>
|
|
168
|
+
<Input
|
|
169
|
+
id="reg-name"
|
|
170
|
+
type="text"
|
|
171
|
+
placeholder="Niko Pirosmani"
|
|
172
|
+
value={form.values().fullName}
|
|
173
|
+
onInput={form.handleChange("fullName")}
|
|
174
|
+
onBlur={form.handleBlur("fullName")}
|
|
175
|
+
autocomplete="name"
|
|
176
|
+
/>
|
|
177
|
+
<FormMessage form={form} name="fullName" />
|
|
178
|
+
</Field>
|
|
179
|
+
|
|
180
|
+
<Field>
|
|
181
|
+
<FieldLabel for="reg-email" class="text-xs sm:text-sm">Email Address</FieldLabel>
|
|
182
|
+
<Input
|
|
183
|
+
id="reg-email"
|
|
184
|
+
type="email"
|
|
185
|
+
placeholder="niko@nikala.dev"
|
|
186
|
+
value={form.values().email}
|
|
187
|
+
onInput={form.handleChange("email")}
|
|
188
|
+
onBlur={form.handleBlur("email")}
|
|
189
|
+
autocomplete="email"
|
|
190
|
+
/>
|
|
191
|
+
<FormMessage form={form} name="email" />
|
|
192
|
+
</Field>
|
|
193
|
+
|
|
194
|
+
{/* Password with Strength Meter */}
|
|
195
|
+
<Field class="space-y-1.5">
|
|
196
|
+
<div class="flex items-center justify-between">
|
|
197
|
+
<FieldLabel for="reg-password" class="text-xs sm:text-sm">Password</FieldLabel>
|
|
198
|
+
<Show when={password().length > 0}>
|
|
199
|
+
<Badge variant={strengthBadgeVariant()} class="text-[10px] px-1.5 py-0 font-mono">
|
|
200
|
+
{strengthLabel()}
|
|
201
|
+
</Badge>
|
|
202
|
+
</Show>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<div class="relative">
|
|
206
|
+
<Input
|
|
207
|
+
id="reg-password"
|
|
208
|
+
type={showPassword() ? "text" : "password"}
|
|
209
|
+
placeholder="Create a strong password"
|
|
210
|
+
value={form.values().password}
|
|
211
|
+
onInput={form.handleChange("password")}
|
|
212
|
+
onBlur={form.handleBlur("password")}
|
|
213
|
+
autocomplete="new-password"
|
|
214
|
+
class="pr-10"
|
|
215
|
+
/>
|
|
216
|
+
<button
|
|
217
|
+
type="button"
|
|
218
|
+
onClick={() => setShowPassword(!showPassword())}
|
|
219
|
+
class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors p-1 cursor-pointer"
|
|
220
|
+
aria-label={showPassword() ? "Hide password" : "Show password"}
|
|
221
|
+
>
|
|
222
|
+
<Show when={showPassword()} fallback={<Eye class="size-4" />}>
|
|
223
|
+
<EyeOff class="size-4" />
|
|
224
|
+
</Show>
|
|
225
|
+
</button>
|
|
226
|
+
</div>
|
|
227
|
+
<FormMessage form={form} name="password" />
|
|
228
|
+
|
|
229
|
+
{/* Reactive Password Strength Progress Bar */}
|
|
230
|
+
<Show when={password().length > 0}>
|
|
231
|
+
<Progress
|
|
232
|
+
value={strengthScore()}
|
|
233
|
+
class="h-1.5 mt-2"
|
|
234
|
+
indicatorClass={strengthColorClass()}
|
|
235
|
+
/>
|
|
236
|
+
|
|
237
|
+
{/* Validation Checklist */}
|
|
238
|
+
<div class="grid grid-cols-2 gap-1.5 pt-1 text-[11px] text-muted-foreground">
|
|
239
|
+
<div class={`flex items-center gap-1.5 ${hasMinLength() ? "text-emerald-500 font-medium" : ""}`}>
|
|
240
|
+
<Show when={hasMinLength()} fallback={<X class="size-3 text-muted-foreground/60" />}>
|
|
241
|
+
<Check class="size-3" />
|
|
242
|
+
</Show>
|
|
243
|
+
<span>8+ characters</span>
|
|
244
|
+
</div>
|
|
245
|
+
<div class={`flex items-center gap-1.5 ${hasNumber() ? "text-emerald-500 font-medium" : ""}`}>
|
|
246
|
+
<Show when={hasNumber()} fallback={<X class="size-3 text-muted-foreground/60" />}>
|
|
247
|
+
<Check class="size-3" />
|
|
248
|
+
</Show>
|
|
249
|
+
<span>At least 1 number</span>
|
|
250
|
+
</div>
|
|
251
|
+
<div class={`flex items-center gap-1.5 ${hasUpperLower() ? "text-emerald-500 font-medium" : ""}`}>
|
|
252
|
+
<Show when={hasUpperLower()} fallback={<X class="size-3 text-muted-foreground/60" />}>
|
|
253
|
+
<Check class="size-3" />
|
|
254
|
+
</Show>
|
|
255
|
+
<span>Uppercase & lowercase</span>
|
|
256
|
+
</div>
|
|
257
|
+
<div class={`flex items-center gap-1.5 ${hasSpecial() ? "text-emerald-500 font-medium" : ""}`}>
|
|
258
|
+
<Show when={hasSpecial()} fallback={<X class="size-3 text-muted-foreground/60" />}>
|
|
259
|
+
<Check class="size-3" />
|
|
260
|
+
</Show>
|
|
261
|
+
<span>1 special symbol</span>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
</Show>
|
|
265
|
+
</Field>
|
|
266
|
+
|
|
267
|
+
{/* Terms of Service Agreement */}
|
|
268
|
+
<div class="space-y-1 pt-1">
|
|
269
|
+
<div class="flex items-start space-x-2.5">
|
|
270
|
+
<Checkbox
|
|
271
|
+
id="reg-terms"
|
|
272
|
+
checked={form.values().agreeTerms}
|
|
273
|
+
onChange={(checked) => form.setFieldValue("agreeTerms", checked)}
|
|
274
|
+
class="mt-0.5"
|
|
275
|
+
/>
|
|
276
|
+
<Label
|
|
277
|
+
for="reg-terms"
|
|
278
|
+
class="text-xs font-normal text-muted-foreground cursor-pointer select-none leading-tight"
|
|
279
|
+
>
|
|
280
|
+
I agree to the{" "}
|
|
281
|
+
<a href="#terms" class="text-primary font-medium underline hover:text-primary/80">
|
|
282
|
+
Terms of Service
|
|
283
|
+
</a>{" "}
|
|
284
|
+
and{" "}
|
|
285
|
+
<a href="#privacy" class="text-primary font-medium underline hover:text-primary/80">
|
|
286
|
+
Privacy Policy
|
|
287
|
+
</a>.
|
|
288
|
+
</Label>
|
|
289
|
+
</div>
|
|
290
|
+
<FormMessage form={form} name="agreeTerms" />
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
{/* Submit Button */}
|
|
294
|
+
<Button
|
|
295
|
+
type="submit"
|
|
296
|
+
class="w-full mt-3 font-medium"
|
|
297
|
+
disabled={form.isSubmitting() || !isFormValid()}
|
|
298
|
+
>
|
|
299
|
+
<Show when={form.isSubmitting()} fallback={<>Create Account <ArrowRight class="ml-2 size-4" /></>}>
|
|
300
|
+
Creating account...
|
|
301
|
+
</Show>
|
|
302
|
+
</Button>
|
|
303
|
+
</Form>
|
|
304
|
+
</CardContent>
|
|
305
|
+
|
|
306
|
+
{/* Card Footer */}
|
|
307
|
+
<CardFooter class="justify-center border-t border-border/50 py-4 text-xs text-muted-foreground">
|
|
308
|
+
Already have an account?{" "}
|
|
309
|
+
<a href="/blocks/login-01" class="text-primary font-semibold hover:underline ml-1">
|
|
310
|
+
Sign in
|
|
311
|
+
</a>
|
|
312
|
+
</CardFooter>
|
|
313
|
+
</Card>
|
|
314
|
+
</div>
|
|
315
|
+
);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
export default Register01;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
splitProps,
|
|
3
|
+
type Component,
|
|
4
|
+
type JSX,
|
|
5
|
+
type ParentComponent,
|
|
6
|
+
useContext,
|
|
7
|
+
} from "solid-js";
|
|
8
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
9
|
+
import { cn } from "@/lib/cn";
|
|
10
|
+
|
|
11
|
+
/* --- 1. Bubble Variants --- */
|
|
12
|
+
export const bubbleVariants = cva(
|
|
13
|
+
"relative max-w-full rounded-lg transition-colors break-words text-left select-text",
|
|
14
|
+
{
|
|
15
|
+
variants: {
|
|
16
|
+
variant: {
|
|
17
|
+
default: "bg-primary text-primary-foreground shadow-2xs",
|
|
18
|
+
muted: "bg-muted text-foreground border border-border/50 shadow-2xs",
|
|
19
|
+
outline: "border border-border bg-background text-foreground shadow-2xs",
|
|
20
|
+
ghost: "bg-transparent text-foreground",
|
|
21
|
+
},
|
|
22
|
+
size: {
|
|
23
|
+
sm: "px-3 py-1.5 text-xs",
|
|
24
|
+
default: "px-4 py-2.5 text-sm",
|
|
25
|
+
lg: "px-5 py-3 text-base",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
defaultVariants: {
|
|
29
|
+
variant: "default",
|
|
30
|
+
size: "default",
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
/* --- 2. BubbleGroup --- */
|
|
36
|
+
export interface BubbleGroupProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
37
|
+
class?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const BubbleGroup: ParentComponent<BubbleGroupProps> = (props) => {
|
|
41
|
+
const [local, rest] = splitProps(props, ["class", "children"]);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
class={cn("flex flex-col gap-1 w-full", local.class)}
|
|
46
|
+
{...rest}
|
|
47
|
+
>
|
|
48
|
+
{local.children}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/* --- 3. Bubble --- */
|
|
54
|
+
export interface BubbleProps
|
|
55
|
+
extends JSX.HTMLAttributes<HTMLDivElement>,
|
|
56
|
+
VariantProps<typeof bubbleVariants> {
|
|
57
|
+
class?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const Bubble: ParentComponent<BubbleProps> = (props) => {
|
|
61
|
+
const [local, rest] = splitProps(props, ["variant", "size", "class", "children"]);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div
|
|
65
|
+
class={cn(
|
|
66
|
+
bubbleVariants({
|
|
67
|
+
variant: local.variant,
|
|
68
|
+
size: local.size,
|
|
69
|
+
}),
|
|
70
|
+
local.class
|
|
71
|
+
)}
|
|
72
|
+
{...rest}
|
|
73
|
+
>
|
|
74
|
+
{local.children}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/* --- 4. BubbleContent --- */
|
|
80
|
+
export interface BubbleContentProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
81
|
+
class?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const BubbleContent: ParentComponent<BubbleContentProps> = (props) => {
|
|
85
|
+
const [local, rest] = splitProps(props, ["class", "children"]);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div
|
|
89
|
+
class={cn("leading-relaxed", local.class)}
|
|
90
|
+
{...rest}
|
|
91
|
+
>
|
|
92
|
+
{local.children}
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/* --- 5. BubbleReactions --- */
|
|
98
|
+
export interface BubbleReactionsProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
99
|
+
class?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const BubbleReactions: ParentComponent<BubbleReactionsProps> = (props) => {
|
|
103
|
+
const [local, rest] = splitProps(props, ["class", "children"]);
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div
|
|
107
|
+
class={cn("flex items-center gap-1.5 pt-2 mt-2 border-t border-border/30 z-10 flex-wrap", local.class)}
|
|
108
|
+
{...rest}
|
|
109
|
+
>
|
|
110
|
+
{local.children}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/* --- 6. BubbleReaction --- */
|
|
116
|
+
export interface BubbleReactionProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
117
|
+
active?: boolean;
|
|
118
|
+
count?: number;
|
|
119
|
+
class?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const BubbleReaction: ParentComponent<BubbleReactionProps> = (props) => {
|
|
123
|
+
const [local, rest] = splitProps(props, ["active", "count", "class", "children"]);
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
data-active={local.active ? "true" : "false"}
|
|
129
|
+
class={cn(
|
|
130
|
+
"inline-flex items-center gap-1.5 rounded-md border border-border/70 bg-background/80 px-2 py-1 text-xs text-foreground shadow-2xs transition-all hover:bg-accent hover:border-primary/40 cursor-pointer select-none",
|
|
131
|
+
local.active && "border-primary/40 bg-primary/10 text-primary font-medium",
|
|
132
|
+
local.class
|
|
133
|
+
)}
|
|
134
|
+
{...rest}
|
|
135
|
+
>
|
|
136
|
+
<span class="text-xs leading-none">{local.children}</span>
|
|
137
|
+
{local.count !== undefined && (
|
|
138
|
+
<span class="text-[11px] font-mono leading-none text-muted-foreground">{local.count}</span>
|
|
139
|
+
)}
|
|
140
|
+
</button>
|
|
141
|
+
);
|
|
142
|
+
};
|