@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.
Files changed (48) hide show
  1. package/package.json +1 -1
  2. package/registry/bubble.json +18 -0
  3. package/registry/create-chat-scroll.json +13 -0
  4. package/registry/create-pagination.json +13 -0
  5. package/registry/footer.json +18 -0
  6. package/registry/forgot-password-01.json +28 -0
  7. package/registry/hero-01.json +22 -0
  8. package/registry/index.json +288 -0
  9. package/registry/login-01.json +28 -0
  10. package/registry/marker.json +17 -0
  11. package/registry/marquee.json +17 -0
  12. package/registry/message.json +17 -0
  13. package/registry/navbar.json +19 -0
  14. package/registry/navigation-menu.json +22 -0
  15. package/registry/otp-verification-01.json +26 -0
  16. package/registry/pagination.json +22 -0
  17. package/registry/rating.json +19 -0
  18. package/registry/register-01.json +31 -0
  19. package/registry/review-card.json +23 -0
  20. package/registry/scroll-area.json +1 -1
  21. package/registry/sidebar.json +24 -0
  22. package/registry/spinner.json +1 -1
  23. package/registry/stat.json +19 -0
  24. package/registry/timeline.json +18 -0
  25. package/registry/toggle-group.json +21 -0
  26. package/src/registry/blocks/forgot-password-01.tsx +141 -0
  27. package/src/registry/blocks/hero-01.tsx +28 -0
  28. package/src/registry/blocks/login-01.tsx +231 -0
  29. package/src/registry/blocks/otp-verification-01.tsx +170 -0
  30. package/src/registry/blocks/register-01.tsx +318 -0
  31. package/src/registry/components/ui/bubble.tsx +142 -0
  32. package/src/registry/components/ui/footer.tsx +247 -0
  33. package/src/registry/components/ui/marker.tsx +102 -0
  34. package/src/registry/components/ui/marquee.tsx +118 -0
  35. package/src/registry/components/ui/message.tsx +168 -0
  36. package/src/registry/components/ui/navbar.tsx +368 -0
  37. package/src/registry/components/ui/navigation-menu.tsx +358 -0
  38. package/src/registry/components/ui/pagination.tsx +258 -0
  39. package/src/registry/components/ui/rating.tsx +185 -0
  40. package/src/registry/components/ui/review-card.tsx +195 -0
  41. package/src/registry/components/ui/scroll-area.tsx +2 -2
  42. package/src/registry/components/ui/sidebar.tsx +692 -0
  43. package/src/registry/components/ui/spinner.tsx +1 -1
  44. package/src/registry/components/ui/stat.tsx +245 -0
  45. package/src/registry/components/ui/timeline.tsx +350 -0
  46. package/src/registry/components/ui/toggle-group.tsx +203 -0
  47. package/src/registry/index.ts +3 -3
  48. 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
+ };