@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.
- package/package.json +42 -0
- package/src/components/alert-dialog.tsx +144 -0
- package/src/components/badge.tsx +48 -0
- package/src/components/breadcrumb.tsx +109 -0
- package/src/components/button.tsx +64 -0
- package/src/components/calendar.tsx +220 -0
- package/src/components/card.tsx +68 -0
- package/src/components/chart.tsx +381 -0
- package/src/components/dropdown-menu.tsx +257 -0
- package/src/components/field.tsx +248 -0
- package/src/components/input.tsx +21 -0
- package/src/components/label.tsx +24 -0
- package/src/components/pagination.tsx +117 -0
- package/src/components/password-input.tsx +69 -0
- package/src/components/password-strength.tsx +88 -0
- package/src/components/popover.tsx +89 -0
- package/src/components/separator.tsx +28 -0
- package/src/components/sheet.tsx +143 -0
- package/src/components/sonner.tsx +47 -0
- package/src/components/table.tsx +72 -0
- package/src/components/tooltip.tsx +53 -0
- package/src/index.ts +29 -0
- package/src/lib/utils.ts +6 -0
|
@@ -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 }
|