@brightweblabs/ui 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.
@@ -0,0 +1,248 @@
1
+ "use client"
2
+
3
+ import { useMemo } from "react"
4
+ import { cva, type VariantProps } from "class-variance-authority"
5
+
6
+ import { cn } from "../lib/utils"
7
+ import { Label } from "./label"
8
+ import { Separator } from "./separator"
9
+
10
+ function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
11
+ return (
12
+ <fieldset
13
+ data-slot="field-set"
14
+ className={cn(
15
+ "flex flex-col gap-6",
16
+ "has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ )
22
+ }
23
+
24
+ function FieldLegend({
25
+ className,
26
+ variant = "legend",
27
+ ...props
28
+ }: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
29
+ return (
30
+ <legend
31
+ data-slot="field-legend"
32
+ data-variant={variant}
33
+ className={cn(
34
+ "mb-3 font-medium",
35
+ "data-[variant=legend]:text-base",
36
+ "data-[variant=label]:text-sm",
37
+ className
38
+ )}
39
+ {...props}
40
+ />
41
+ )
42
+ }
43
+
44
+ function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
45
+ return (
46
+ <div
47
+ data-slot="field-group"
48
+ className={cn(
49
+ "group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
50
+ className
51
+ )}
52
+ {...props}
53
+ />
54
+ )
55
+ }
56
+
57
+ const fieldVariants = cva(
58
+ "group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
59
+ {
60
+ variants: {
61
+ orientation: {
62
+ vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
63
+ horizontal: [
64
+ "flex-row items-center",
65
+ "[&>[data-slot=field-label]]:flex-auto",
66
+ "has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
67
+ ],
68
+ responsive: [
69
+ "flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto",
70
+ "@md/field-group:[&>[data-slot=field-label]]:flex-auto",
71
+ "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
72
+ ],
73
+ },
74
+ },
75
+ defaultVariants: {
76
+ orientation: "vertical",
77
+ },
78
+ }
79
+ )
80
+
81
+ function Field({
82
+ className,
83
+ orientation = "vertical",
84
+ ...props
85
+ }: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
86
+ return (
87
+ <div
88
+ role="group"
89
+ data-slot="field"
90
+ data-orientation={orientation}
91
+ className={cn(fieldVariants({ orientation }), className)}
92
+ {...props}
93
+ />
94
+ )
95
+ }
96
+
97
+ function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
98
+ return (
99
+ <div
100
+ data-slot="field-content"
101
+ className={cn(
102
+ "group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
103
+ className
104
+ )}
105
+ {...props}
106
+ />
107
+ )
108
+ }
109
+
110
+ function FieldLabel({
111
+ className,
112
+ ...props
113
+ }: React.ComponentProps<typeof Label>) {
114
+ return (
115
+ <Label
116
+ data-slot="field-label"
117
+ className={cn(
118
+ "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
119
+ "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
120
+ "has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
121
+ className
122
+ )}
123
+ {...props}
124
+ />
125
+ )
126
+ }
127
+
128
+ function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
129
+ return (
130
+ <div
131
+ data-slot="field-label"
132
+ className={cn(
133
+ "flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
134
+ className
135
+ )}
136
+ {...props}
137
+ />
138
+ )
139
+ }
140
+
141
+ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
142
+ return (
143
+ <p
144
+ data-slot="field-description"
145
+ className={cn(
146
+ "text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
147
+ "last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
148
+ "[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
149
+ className
150
+ )}
151
+ {...props}
152
+ />
153
+ )
154
+ }
155
+
156
+ function FieldSeparator({
157
+ children,
158
+ className,
159
+ ...props
160
+ }: React.ComponentProps<"div"> & {
161
+ children?: React.ReactNode
162
+ }) {
163
+ return (
164
+ <div
165
+ data-slot="field-separator"
166
+ data-content={!!children}
167
+ className={cn(
168
+ "relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
169
+ className
170
+ )}
171
+ {...props}
172
+ >
173
+ <Separator className="absolute inset-0 top-1/2" />
174
+ {children && (
175
+ <span
176
+ className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
177
+ data-slot="field-separator-content"
178
+ >
179
+ {children}
180
+ </span>
181
+ )}
182
+ </div>
183
+ )
184
+ }
185
+
186
+ function FieldError({
187
+ className,
188
+ children,
189
+ errors,
190
+ ...props
191
+ }: React.ComponentProps<"div"> & {
192
+ errors?: Array<{ message?: string } | undefined>
193
+ }) {
194
+ const content = useMemo(() => {
195
+ if (children) {
196
+ return children
197
+ }
198
+
199
+ if (!errors?.length) {
200
+ return null
201
+ }
202
+
203
+ const uniqueErrors = [
204
+ ...new Map(errors.map((error) => [error?.message, error])).values(),
205
+ ]
206
+
207
+ if (uniqueErrors?.length == 1) {
208
+ return uniqueErrors[0]?.message
209
+ }
210
+
211
+ return (
212
+ <ul className="ml-4 flex list-disc flex-col gap-1">
213
+ {uniqueErrors.map(
214
+ (error, index) =>
215
+ error?.message && <li key={index}>{error.message}</li>
216
+ )}
217
+ </ul>
218
+ )
219
+ }, [children, errors])
220
+
221
+ if (!content) {
222
+ return null
223
+ }
224
+
225
+ return (
226
+ <div
227
+ role="alert"
228
+ data-slot="field-error"
229
+ className={cn("text-destructive text-sm font-normal", className)}
230
+ {...props}
231
+ >
232
+ {content}
233
+ </div>
234
+ )
235
+ }
236
+
237
+ export {
238
+ Field,
239
+ FieldLabel,
240
+ FieldDescription,
241
+ FieldError,
242
+ FieldGroup,
243
+ FieldLegend,
244
+ FieldSeparator,
245
+ FieldSet,
246
+ FieldContent,
247
+ FieldTitle,
248
+ }
@@ -0,0 +1,21 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "../lib/utils";
4
+
5
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6
+ return (
7
+ <input
8
+ type={type}
9
+ data-slot="input"
10
+ className={cn(
11
+ "file:text-foreground placeholder:text-foreground/30 selection:bg-primary selection:text-primary-foreground h-11 w-full min-w-0 rounded-xl border border-foreground/15 bg-foreground/5 px-3 py-1 text-base text-foreground shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-input file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12
+ "focus:border-primary focus:ring-primary/40 focus:ring-[3px]",
13
+ "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
14
+ className,
15
+ )}
16
+ {...props}
17
+ />
18
+ );
19
+ }
20
+
21
+ export { Input };
@@ -0,0 +1,24 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as LabelPrimitive from "@radix-ui/react-label"
5
+
6
+ import { cn } from "../lib/utils"
7
+
8
+ function Label({
9
+ className,
10
+ ...props
11
+ }: React.ComponentProps<typeof LabelPrimitive.Root>) {
12
+ return (
13
+ <LabelPrimitive.Root
14
+ data-slot="label"
15
+ className={cn(
16
+ "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ )
22
+ }
23
+
24
+ export { Label }
@@ -0,0 +1,117 @@
1
+ import * as React from "react";
2
+ import Link from "next/link";
3
+ import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
4
+ import { VariantProps } from "class-variance-authority";
5
+
6
+ import { cn } from "../lib/utils";
7
+ import { buttonVariants } from "./button";
8
+
9
+ function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
10
+ return (
11
+ <nav
12
+ role="navigation"
13
+ aria-label="paginação"
14
+ className={cn("mx-auto flex w-full justify-center", className)}
15
+ {...props}
16
+ />
17
+ );
18
+ }
19
+
20
+ function PaginationContent({ className, ...props }: React.ComponentProps<"ul">) {
21
+ return (
22
+ <ul
23
+ className={cn("flex flex-row items-center gap-1", className)}
24
+ {...props}
25
+ />
26
+ );
27
+ }
28
+
29
+ function PaginationItem(props: React.ComponentProps<"li">) {
30
+ return <li {...props} />;
31
+ }
32
+
33
+ type PaginationLinkProps = {
34
+ isActive?: boolean;
35
+ } & Pick<VariantProps<typeof buttonVariants>, "size"> &
36
+ React.ComponentProps<typeof Link>;
37
+
38
+ function PaginationLink({
39
+ className,
40
+ isActive,
41
+ size = "icon",
42
+ ...props
43
+ }: PaginationLinkProps) {
44
+ return (
45
+ <Link
46
+ aria-current={isActive ? "page" : undefined}
47
+ className={cn(
48
+ buttonVariants({
49
+ variant: isActive ? "outline" : "ghost",
50
+ size,
51
+ }),
52
+ className,
53
+ )}
54
+ {...props}
55
+ />
56
+ );
57
+ }
58
+
59
+ function PaginationPrevious({
60
+ className,
61
+ ...props
62
+ }: React.ComponentProps<typeof PaginationLink>) {
63
+ return (
64
+ <PaginationLink
65
+ aria-label="Ir para a página anterior"
66
+ size="default"
67
+ className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
68
+ {...props}
69
+ >
70
+ <ChevronLeft />
71
+ <span>Anterior</span>
72
+ </PaginationLink>
73
+ );
74
+ }
75
+
76
+ function PaginationNext({
77
+ className,
78
+ ...props
79
+ }: React.ComponentProps<typeof PaginationLink>) {
80
+ return (
81
+ <PaginationLink
82
+ aria-label="Ir para a próxima página"
83
+ size="default"
84
+ className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
85
+ {...props}
86
+ >
87
+ <span>Seguinte</span>
88
+ <ChevronRight />
89
+ </PaginationLink>
90
+ );
91
+ }
92
+
93
+ function PaginationEllipsis({
94
+ className,
95
+ ...props
96
+ }: React.ComponentProps<"span">) {
97
+ return (
98
+ <span
99
+ aria-hidden
100
+ className={cn("flex h-9 w-9 items-center justify-center", className)}
101
+ {...props}
102
+ >
103
+ <MoreHorizontal className="h-4 w-4" />
104
+ <span className="sr-only">Mais páginas</span>
105
+ </span>
106
+ );
107
+ }
108
+
109
+ export {
110
+ Pagination,
111
+ PaginationContent,
112
+ PaginationEllipsis,
113
+ PaginationItem,
114
+ PaginationLink,
115
+ PaginationNext,
116
+ PaginationPrevious,
117
+ };
@@ -0,0 +1,69 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "../lib/utils";
5
+ import { Input } from "./input";
6
+
7
+ interface PasswordInputProps extends Omit<React.ComponentProps<"input">, "type"> {
8
+ showToggle?: boolean;
9
+ }
10
+
11
+ const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
12
+ ({ className, showToggle = true, ...props }, ref) => {
13
+ const [showPassword, setShowPassword] = React.useState(false);
14
+
15
+ return (
16
+ <div className="relative">
17
+ <Input
18
+ type={showPassword ? "text" : "password"}
19
+ className={cn("pr-10", className)}
20
+ ref={ref}
21
+ {...props}
22
+ />
23
+ {showToggle && (
24
+ <button
25
+ type="button"
26
+ onClick={() => setShowPassword(!showPassword)}
27
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-foreground/40 hover:text-foreground/70 transition-colors"
28
+ aria-label={showPassword ? "Ocultar palavra-passe" : "Mostrar palavra-passe"}
29
+ tabIndex={-1}
30
+ >
31
+ {showPassword ? (
32
+ <svg
33
+ xmlns="http://www.w3.org/2000/svg"
34
+ viewBox="0 0 24 24"
35
+ fill="none"
36
+ stroke="currentColor"
37
+ strokeWidth="2"
38
+ strokeLinecap="round"
39
+ strokeLinejoin="round"
40
+ className="w-4 h-4"
41
+ >
42
+ <path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
43
+ <line x1="1" y1="1" x2="23" y2="23" />
44
+ </svg>
45
+ ) : (
46
+ <svg
47
+ xmlns="http://www.w3.org/2000/svg"
48
+ viewBox="0 0 24 24"
49
+ fill="none"
50
+ stroke="currentColor"
51
+ strokeWidth="2"
52
+ strokeLinecap="round"
53
+ strokeLinejoin="round"
54
+ className="w-4 h-4"
55
+ >
56
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
57
+ <circle cx="12" cy="12" r="3" />
58
+ </svg>
59
+ )}
60
+ </button>
61
+ )}
62
+ </div>
63
+ );
64
+ }
65
+ );
66
+
67
+ PasswordInput.displayName = "PasswordInput";
68
+
69
+ export { PasswordInput };
@@ -0,0 +1,88 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "../lib/utils";
5
+
6
+ interface PasswordStrengthProps {
7
+ password: string;
8
+ className?: string;
9
+ }
10
+
11
+ type StrengthLevel = "weak" | "medium" | "strong" | null;
12
+
13
+ function calculatePasswordStrength(password: string): StrengthLevel {
14
+ if (!password) return null;
15
+
16
+ let score = 0;
17
+
18
+ // Length check
19
+ if (password.length >= 8) score++;
20
+ if (password.length >= 12) score++;
21
+
22
+ // Character variety
23
+ if (/[a-z]/.test(password)) score++;
24
+ if (/[A-Z]/.test(password)) score++;
25
+ if (/[0-9]/.test(password)) score++;
26
+ if (/[^a-zA-Z0-9]/.test(password)) score++; // Special chars
27
+
28
+ // Determine strength level
29
+ if (score <= 2) return "weak";
30
+ if (score <= 4) return "medium";
31
+ return "strong";
32
+ }
33
+
34
+ function getStrengthConfig(strength: StrengthLevel) {
35
+ switch (strength) {
36
+ case "weak":
37
+ return {
38
+ label: "Fraca",
39
+ color: "bg-destructive",
40
+ width: "w-1/3",
41
+ textColor: "text-destructive",
42
+ };
43
+ case "medium":
44
+ return {
45
+ label: "Média",
46
+ color: "bg-accent",
47
+ width: "w-2/3",
48
+ textColor: "text-accent",
49
+ };
50
+ case "strong":
51
+ return {
52
+ label: "Forte",
53
+ color: "bg-secondary",
54
+ width: "w-full",
55
+ textColor: "text-secondary",
56
+ };
57
+ default:
58
+ return null;
59
+ }
60
+ }
61
+
62
+ export function PasswordStrength({ password, className }: PasswordStrengthProps) {
63
+ const strength = calculatePasswordStrength(password);
64
+ const config = getStrengthConfig(strength);
65
+
66
+ if (!config) return null;
67
+
68
+ return (
69
+ <div className={cn("flex flex-col gap-1.5", className)}>
70
+ {/* Strength bar */}
71
+ <div className="h-1 w-full bg-foreground/10 rounded-full overflow-hidden">
72
+ <div
73
+ className={cn("h-full transition-all duration-300", config.color, config.width)}
74
+ role="progressbar"
75
+ aria-valuenow={strength === "weak" ? 33 : strength === "medium" ? 66 : 100}
76
+ aria-valuemin={0}
77
+ aria-valuemax={100}
78
+ aria-label="Força da palavra-passe"
79
+ />
80
+ </div>
81
+
82
+ {/* Strength label */}
83
+ <p className={cn("paragraph-mini font-medium transition-colors", config.textColor)}>
84
+ Força: {config.label}
85
+ </p>
86
+ </div>
87
+ );
88
+ }
@@ -0,0 +1,89 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as PopoverPrimitive from "@radix-ui/react-popover"
5
+
6
+ import { cn } from "../lib/utils"
7
+
8
+ function Popover({
9
+ ...props
10
+ }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
11
+ return <PopoverPrimitive.Root data-slot="popover" {...props} />
12
+ }
13
+
14
+ function PopoverTrigger({
15
+ ...props
16
+ }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
17
+ return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
18
+ }
19
+
20
+ function PopoverContent({
21
+ className,
22
+ align = "center",
23
+ sideOffset = 4,
24
+ ...props
25
+ }: React.ComponentProps<typeof PopoverPrimitive.Content>) {
26
+ return (
27
+ <PopoverPrimitive.Portal>
28
+ <PopoverPrimitive.Content
29
+ data-slot="popover-content"
30
+ align={align}
31
+ sideOffset={sideOffset}
32
+ className={cn(
33
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
34
+ className
35
+ )}
36
+ {...props}
37
+ />
38
+ </PopoverPrimitive.Portal>
39
+ )
40
+ }
41
+
42
+ function PopoverAnchor({
43
+ ...props
44
+ }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
45
+ return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
46
+ }
47
+
48
+ function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
49
+ return (
50
+ <div
51
+ data-slot="popover-header"
52
+ className={cn("flex flex-col gap-1 text-sm", className)}
53
+ {...props}
54
+ />
55
+ )
56
+ }
57
+
58
+ function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) {
59
+ return (
60
+ <div
61
+ data-slot="popover-title"
62
+ className={cn("font-medium", className)}
63
+ {...props}
64
+ />
65
+ )
66
+ }
67
+
68
+ function PopoverDescription({
69
+ className,
70
+ ...props
71
+ }: React.ComponentProps<"p">) {
72
+ return (
73
+ <p
74
+ data-slot="popover-description"
75
+ className={cn("text-muted-foreground", className)}
76
+ {...props}
77
+ />
78
+ )
79
+ }
80
+
81
+ export {
82
+ Popover,
83
+ PopoverTrigger,
84
+ PopoverContent,
85
+ PopoverAnchor,
86
+ PopoverHeader,
87
+ PopoverTitle,
88
+ PopoverDescription,
89
+ }
@@ -0,0 +1,28 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as SeparatorPrimitive from "@radix-ui/react-separator"
5
+
6
+ import { cn } from "../lib/utils"
7
+
8
+ function Separator({
9
+ className,
10
+ orientation = "horizontal",
11
+ decorative = true,
12
+ ...props
13
+ }: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
14
+ return (
15
+ <SeparatorPrimitive.Root
16
+ data-slot="separator"
17
+ decorative={decorative}
18
+ orientation={orientation}
19
+ className={cn(
20
+ "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
21
+ className
22
+ )}
23
+ {...props}
24
+ />
25
+ )
26
+ }
27
+
28
+ export { Separator }