@nikala-ui/core 0.10.1 → 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/button-group.json +20 -0
- package/registry/create-chat-scroll.json +13 -0
- package/registry/create-drop-zone.json +13 -0
- package/registry/create-pagination.json +13 -0
- package/registry/dropzone.json +21 -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 +331 -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/table.json +17 -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/button-group.tsx +42 -0
- package/src/registry/components/ui/dropzone.tsx +189 -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/table.tsx +160 -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 +141 -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
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { splitProps, type Component, type JSX } from "solid-js";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface ButtonGroupProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
/** Controls whether grouped buttons are arranged in a row or column. */
|
|
6
|
+
orientation?: "horizontal" | "vertical";
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Groups adjacent buttons into a connected control with shared borders and radii.
|
|
12
|
+
*
|
|
13
|
+
* ButtonGroup is intentionally presentational. Use Button for individual actions
|
|
14
|
+
* and compose the group with the same reactive state as the surrounding feature.
|
|
15
|
+
*/
|
|
16
|
+
export const ButtonGroup: Component<ButtonGroupProps> = (props) => {
|
|
17
|
+
const [local, rest] = splitProps(props, [
|
|
18
|
+
"orientation",
|
|
19
|
+
"class",
|
|
20
|
+
"children",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const orientation = () => local.orientation ?? "horizontal";
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
role="group"
|
|
28
|
+
data-orientation={orientation()}
|
|
29
|
+
class={cn(
|
|
30
|
+
"isolate inline-flex",
|
|
31
|
+
orientation() === "horizontal"
|
|
32
|
+
? "flex-row [&>button:not(:first-child)]:-ml-px [&>button:not(:first-child)]:rounded-l-none [&>button:not(:last-child)]:rounded-r-none"
|
|
33
|
+
: "flex-col [&>button:not(:first-child)]:-mt-px [&>button:not(:first-child)]:rounded-t-none [&>button:not(:last-child)]:rounded-b-none",
|
|
34
|
+
"[&>button:focus-visible]:z-10",
|
|
35
|
+
local.class
|
|
36
|
+
)}
|
|
37
|
+
{...rest}
|
|
38
|
+
>
|
|
39
|
+
{local.children}
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { splitProps, type Component, type JSX } from "solid-js";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
import { CloudUpload, FileText, X, AlertCircle } from "lucide-solid";
|
|
4
|
+
|
|
5
|
+
export interface DropzoneProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
6
|
+
class?: string;
|
|
7
|
+
isOver?: boolean;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Root container for the Dropzone file upload component.
|
|
13
|
+
*/
|
|
14
|
+
export const Dropzone: Component<DropzoneProps> = (props) => {
|
|
15
|
+
const [local, rest] = splitProps(props, ["class", "isOver", "disabled"]);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
class={cn(
|
|
20
|
+
"group relative flex w-full flex-col items-center justify-center rounded-lg border-2 border-dashed border-border bg-card/50 p-8 text-center transition-all",
|
|
21
|
+
"hover:border-primary/50 hover:bg-card/80",
|
|
22
|
+
local.isOver && "border-primary bg-primary/5 ring-2 ring-primary/20",
|
|
23
|
+
local.disabled && "pointer-events-none opacity-50",
|
|
24
|
+
local.class
|
|
25
|
+
)}
|
|
26
|
+
{...rest}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export interface DropzoneIconProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
32
|
+
class?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Centered icon placeholder for Dropzone.
|
|
37
|
+
*/
|
|
38
|
+
export const DropzoneIcon: Component<DropzoneIconProps> = (props) => {
|
|
39
|
+
const [local, rest] = splitProps(props, ["class", "children"]);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
class={cn(
|
|
44
|
+
"mb-3 flex size-12 items-center justify-center rounded-lg bg-primary/10 text-primary transition-transform group-hover:scale-105",
|
|
45
|
+
local.class
|
|
46
|
+
)}
|
|
47
|
+
{...rest}
|
|
48
|
+
>
|
|
49
|
+
{local.children || <CloudUpload class="size-6" />}
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export interface DropzoneTitleProps extends JSX.HTMLAttributes<HTMLHeadingElement> {
|
|
55
|
+
class?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Primary title text for the dropzone prompt.
|
|
60
|
+
*/
|
|
61
|
+
export const DropzoneTitle: Component<DropzoneTitleProps> = (props) => {
|
|
62
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<h4
|
|
66
|
+
class={cn("text-sm font-semibold tracking-tight text-foreground", local.class)}
|
|
67
|
+
{...rest}
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export interface DropzoneDescriptionProps extends JSX.HTMLAttributes<HTMLParagraphElement> {
|
|
73
|
+
class?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Subtitle description text for dropzone file specifications.
|
|
78
|
+
*/
|
|
79
|
+
export const DropzoneDescription: Component<DropzoneDescriptionProps> = (props) => {
|
|
80
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<p
|
|
84
|
+
class={cn("mt-1 text-xs text-muted-foreground", local.class)}
|
|
85
|
+
{...rest}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export interface DropzoneFileListProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
91
|
+
class?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Container list for uploaded files.
|
|
96
|
+
*/
|
|
97
|
+
export const DropzoneFileList: Component<DropzoneFileListProps> = (props) => {
|
|
98
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div
|
|
102
|
+
class={cn("mt-4 flex w-full flex-col gap-2", local.class)}
|
|
103
|
+
{...rest}
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export interface DropzoneFileItemProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
109
|
+
class?: string;
|
|
110
|
+
name: string;
|
|
111
|
+
size?: string;
|
|
112
|
+
onRemove?: () => void;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Individual uploaded file card with name, formatted size, and remove button.
|
|
117
|
+
*/
|
|
118
|
+
export const DropzoneFileItem: Component<DropzoneFileItemProps> = (props) => {
|
|
119
|
+
const [local, rest] = splitProps(props, ["class", "name", "size", "onRemove", "children"]);
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div
|
|
123
|
+
class={cn(
|
|
124
|
+
"flex items-center justify-between gap-3 rounded-md border border-border bg-card p-2.5 text-xs transition-colors",
|
|
125
|
+
local.class
|
|
126
|
+
)}
|
|
127
|
+
{...rest}
|
|
128
|
+
>
|
|
129
|
+
<div class="flex min-w-0 items-center gap-2.5">
|
|
130
|
+
<div class="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
|
|
131
|
+
{local.children || <FileText class="size-4" />}
|
|
132
|
+
</div>
|
|
133
|
+
<div class="flex min-w-0 flex-col text-left">
|
|
134
|
+
<span class="truncate font-medium text-foreground">{local.name}</span>
|
|
135
|
+
{local.size && (
|
|
136
|
+
<span class="font-mono text-[11px] text-muted-foreground">{local.size}</span>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{local.onRemove && (
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
onClick={(e) => {
|
|
145
|
+
e.stopPropagation();
|
|
146
|
+
local.onRemove?.();
|
|
147
|
+
}}
|
|
148
|
+
class="flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
|
|
149
|
+
aria-label="Remove file"
|
|
150
|
+
>
|
|
151
|
+
<X class="size-3.5" />
|
|
152
|
+
</button>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export interface DropzoneRejectedItemProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
159
|
+
class?: string;
|
|
160
|
+
name: string;
|
|
161
|
+
error?: string;
|
|
162
|
+
size?: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Card for displaying rejected files and validation errors.
|
|
167
|
+
*/
|
|
168
|
+
export const DropzoneRejectedItem: Component<DropzoneRejectedItemProps> = (props) => {
|
|
169
|
+
const [local, rest] = splitProps(props, ["class", "name", "error", "size"]);
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div
|
|
173
|
+
class={cn(
|
|
174
|
+
"flex items-center justify-between gap-3 rounded-md border border-destructive/30 bg-destructive/10 p-2.5 text-xs text-destructive",
|
|
175
|
+
local.class
|
|
176
|
+
)}
|
|
177
|
+
{...rest}
|
|
178
|
+
>
|
|
179
|
+
<div class="flex min-w-0 items-center gap-2">
|
|
180
|
+
<AlertCircle class="size-4 shrink-0" />
|
|
181
|
+
<span class="truncate font-medium">{local.name}</span>
|
|
182
|
+
{local.size && <span class="font-mono text-[11px] opacity-80">({local.size})</span>}
|
|
183
|
+
</div>
|
|
184
|
+
{local.error && (
|
|
185
|
+
<span class="shrink-0 text-[11px] font-medium">{local.error}</span>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
};
|