@greatapps/greatauth-ui 0.3.15 → 0.3.17
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/dist/index.js +82 -68
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/app-sidebar.tsx +11 -3
- package/src/components/image-crop-upload.tsx +5 -3
- package/src/components/login-form.tsx +7 -5
- package/src/components/theme-toggle.tsx +2 -2
- package/src/components/users/data-table.tsx +8 -7
- package/src/components/users/user-form-dialog.tsx +7 -7
- package/src/components/users/users-page.tsx +11 -8
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
3
4
|
import { usePathname, useRouter } from "next/navigation";
|
|
4
5
|
import Link from "next/link";
|
|
5
6
|
import { ChevronsUpDown, ChevronRight, LogOut } from "lucide-react";
|
|
@@ -76,9 +77,16 @@ function CollapsibleMenuItem({ item, pathname }: { item: MenuItem; pathname: str
|
|
|
76
77
|
const isChildActive = item.children?.some((child) =>
|
|
77
78
|
child.isActive !== undefined ? child.isActive : pathname.startsWith(child.href)
|
|
78
79
|
) ?? false;
|
|
80
|
+
const isActive = isParentActive || isChildActive;
|
|
81
|
+
|
|
82
|
+
const [open, setOpen] = useState(isActive);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (isActive) setOpen(true);
|
|
86
|
+
}, [isActive]);
|
|
79
87
|
|
|
80
88
|
return (
|
|
81
|
-
<Collapsible
|
|
89
|
+
<Collapsible open={open} onOpenChange={setOpen} className="group/collapsible">
|
|
82
90
|
<SidebarMenuItem>
|
|
83
91
|
<SidebarMenuButton asChild isActive={isParentActive} tooltip={item.label}>
|
|
84
92
|
<Link href={item.href} onClick={item.onClick}>
|
|
@@ -173,7 +181,7 @@ export function AppSidebar({ config }: AppSidebarProps) {
|
|
|
173
181
|
<span className="truncate font-semibold">{userName}</span>
|
|
174
182
|
<span className="truncate text-xs text-muted-foreground">{userEmail}</span>
|
|
175
183
|
</div>
|
|
176
|
-
<ChevronsUpDown className="ml-auto size-4 group-data-[collapsible=icon]:hidden" />
|
|
184
|
+
<ChevronsUpDown aria-hidden="true" className="ml-auto size-4 group-data-[collapsible=icon]:hidden" />
|
|
177
185
|
</SidebarMenuButton>
|
|
178
186
|
</DropdownMenuTrigger>
|
|
179
187
|
<DropdownMenuContent
|
|
@@ -191,7 +199,7 @@ export function AppSidebar({ config }: AppSidebarProps) {
|
|
|
191
199
|
<DropdownMenuSeparator />
|
|
192
200
|
{config.footerExtra}
|
|
193
201
|
<DropdownMenuItem onClick={handleLogout}>
|
|
194
|
-
<LogOut className="size-4" />
|
|
202
|
+
<LogOut aria-hidden="true" className="size-4" />
|
|
195
203
|
Sair
|
|
196
204
|
</DropdownMenuItem>
|
|
197
205
|
</DropdownMenuContent>
|
|
@@ -164,8 +164,9 @@ export function ImageCropUpload({
|
|
|
164
164
|
type="button"
|
|
165
165
|
onClick={() => inputRef.current?.click()}
|
|
166
166
|
className="absolute inset-0 flex items-center justify-center rounded-full bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer"
|
|
167
|
+
aria-label="Alterar foto"
|
|
167
168
|
>
|
|
168
|
-
<Camera className="h-5 w-5 text-white" />
|
|
169
|
+
<Camera aria-hidden="true" className="h-5 w-5 text-white" />
|
|
169
170
|
</button>
|
|
170
171
|
)}
|
|
171
172
|
</div>
|
|
@@ -177,6 +178,7 @@ export function ImageCropUpload({
|
|
|
177
178
|
onChange={handleFileSelect}
|
|
178
179
|
className="hidden"
|
|
179
180
|
disabled={disabled}
|
|
181
|
+
aria-label="Selecionar imagem"
|
|
180
182
|
/>
|
|
181
183
|
|
|
182
184
|
{!disabled && value && (
|
|
@@ -187,7 +189,7 @@ export function ImageCropUpload({
|
|
|
187
189
|
onClick={handleRemove}
|
|
188
190
|
className="text-destructive hover:text-destructive"
|
|
189
191
|
>
|
|
190
|
-
<Trash2 className="h-4 w-4 mr-1" />
|
|
192
|
+
<Trash2 aria-hidden="true" className="h-4 w-4 mr-1" />
|
|
191
193
|
Remover
|
|
192
194
|
</Button>
|
|
193
195
|
)}
|
|
@@ -243,7 +245,7 @@ export function ImageCropUpload({
|
|
|
243
245
|
onClick={handleConfirmCrop}
|
|
244
246
|
disabled={isUploading || !croppedArea}
|
|
245
247
|
>
|
|
246
|
-
{isUploading ? "Enviando
|
|
248
|
+
{isUploading ? "Enviando\u2026" : "Confirmar"}
|
|
247
249
|
</Button>
|
|
248
250
|
</DialogFooter>
|
|
249
251
|
</DialogContent>
|
|
@@ -125,7 +125,7 @@ export function LoginForm({ config }: LoginFormProps) {
|
|
|
125
125
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
126
126
|
{error && (
|
|
127
127
|
<div className="flex items-center gap-2 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2.5 text-sm text-destructive">
|
|
128
|
-
<AlertCircle className="h-4 w-4 shrink-0" />
|
|
128
|
+
<AlertCircle aria-hidden="true" className="h-4 w-4 shrink-0" />
|
|
129
129
|
{error}
|
|
130
130
|
</div>
|
|
131
131
|
)}
|
|
@@ -135,9 +135,10 @@ export function LoginForm({ config }: LoginFormProps) {
|
|
|
135
135
|
Email
|
|
136
136
|
</Label>
|
|
137
137
|
<div className="relative">
|
|
138
|
-
<Mail className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
138
|
+
<Mail aria-hidden="true" className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
139
139
|
<Input
|
|
140
140
|
id="login-email"
|
|
141
|
+
name="email"
|
|
141
142
|
type="email"
|
|
142
143
|
placeholder="email@exemplo.com"
|
|
143
144
|
value={email}
|
|
@@ -154,9 +155,10 @@ export function LoginForm({ config }: LoginFormProps) {
|
|
|
154
155
|
Senha
|
|
155
156
|
</Label>
|
|
156
157
|
<div className="relative">
|
|
157
|
-
<Lock className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
158
|
+
<Lock aria-hidden="true" className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
158
159
|
<Input
|
|
159
160
|
id="login-password"
|
|
161
|
+
name="password"
|
|
160
162
|
type={showPassword ? "text" : "password"}
|
|
161
163
|
placeholder="••••••••"
|
|
162
164
|
value={password}
|
|
@@ -187,8 +189,8 @@ export function LoginForm({ config }: LoginFormProps) {
|
|
|
187
189
|
>
|
|
188
190
|
{loading ? (
|
|
189
191
|
<>
|
|
190
|
-
<Loader2 className="h-4 w-4 animate-spin" />
|
|
191
|
-
A entrar
|
|
192
|
+
<Loader2 aria-hidden="true" className="h-4 w-4 animate-spin" />
|
|
193
|
+
A entrar…
|
|
192
194
|
</>
|
|
193
195
|
) : (
|
|
194
196
|
"Entrar"
|
|
@@ -33,8 +33,8 @@ export function ThemeToggle() {
|
|
|
33
33
|
|
|
34
34
|
return (
|
|
35
35
|
<Button variant="ghost" size="icon" onClick={toggleTheme}>
|
|
36
|
-
<Sun className="h-5 w-5 rotate-0 scale-100 transition-
|
|
37
|
-
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-
|
|
36
|
+
<Sun aria-hidden="true" className="h-5 w-5 rotate-0 scale-100 transition-transform transition-opacity dark:-rotate-90 dark:scale-0" />
|
|
37
|
+
<Moon aria-hidden="true" className="absolute h-5 w-5 rotate-90 scale-0 transition-transform transition-opacity dark:rotate-0 dark:scale-100" />
|
|
38
38
|
<span className="sr-only">Alternar tema</span>
|
|
39
39
|
</Button>
|
|
40
40
|
);
|
|
@@ -83,13 +83,14 @@ export function DataTable<TData, TValue>({
|
|
|
83
83
|
type="button"
|
|
84
84
|
className="flex items-center gap-1 hover:text-foreground -ml-1 px-1 py-0.5 rounded cursor-pointer select-none"
|
|
85
85
|
onClick={header.column.getToggleSortingHandler()}
|
|
86
|
+
aria-label="Ordenar"
|
|
86
87
|
>
|
|
87
88
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
88
89
|
{{
|
|
89
|
-
asc: <ArrowUp className="h-3.5 w-3.5" />,
|
|
90
|
-
desc: <ArrowDown className="h-3.5 w-3.5" />,
|
|
90
|
+
asc: <ArrowUp aria-hidden="true" className="h-3.5 w-3.5" />,
|
|
91
|
+
desc: <ArrowDown aria-hidden="true" className="h-3.5 w-3.5" />,
|
|
91
92
|
}[header.column.getIsSorted() as string] ?? (
|
|
92
|
-
<ArrowUpDown className="h-3.5 w-3.5 text-muted-foreground/50" />
|
|
93
|
+
<ArrowUpDown aria-hidden="true" className="h-3.5 w-3.5 text-muted-foreground/50" />
|
|
93
94
|
)}
|
|
94
95
|
</button>
|
|
95
96
|
) : (
|
|
@@ -152,7 +153,7 @@ export function DataTable<TData, TValue>({
|
|
|
152
153
|
|
|
153
154
|
{showPagination && (
|
|
154
155
|
<div className="flex items-center justify-between px-2">
|
|
155
|
-
<p className="text-sm text-muted-foreground">
|
|
156
|
+
<p className="text-sm text-muted-foreground tabular-nums">
|
|
156
157
|
{total} registro{total !== 1 ? "s" : ""}
|
|
157
158
|
</p>
|
|
158
159
|
<div className="flex items-center gap-2">
|
|
@@ -162,10 +163,10 @@ export function DataTable<TData, TValue>({
|
|
|
162
163
|
onClick={() => onPageChange(page - 1)}
|
|
163
164
|
disabled={page <= 1}
|
|
164
165
|
>
|
|
165
|
-
<ChevronLeft className="h-4 w-4" />
|
|
166
|
+
<ChevronLeft aria-hidden="true" className="h-4 w-4" />
|
|
166
167
|
Anterior
|
|
167
168
|
</Button>
|
|
168
|
-
<span className="text-sm text-muted-foreground">
|
|
169
|
+
<span className="text-sm text-muted-foreground tabular-nums">
|
|
169
170
|
{page} de {totalPages}
|
|
170
171
|
</span>
|
|
171
172
|
<Button
|
|
@@ -175,7 +176,7 @@ export function DataTable<TData, TValue>({
|
|
|
175
176
|
disabled={page >= totalPages}
|
|
176
177
|
>
|
|
177
178
|
Próximo
|
|
178
|
-
<ChevronRight className="h-4 w-4" />
|
|
179
|
+
<ChevronRight aria-hidden="true" className="h-4 w-4" />
|
|
179
180
|
</Button>
|
|
180
181
|
</div>
|
|
181
182
|
</div>
|
|
@@ -119,27 +119,27 @@ export function UserFormDialog({
|
|
|
119
119
|
<div className="grid grid-cols-2 gap-4">
|
|
120
120
|
<div className="space-y-2">
|
|
121
121
|
<Label htmlFor="user-name">Nome *</Label>
|
|
122
|
-
<Input id="user-name" value={name} onChange={(e) => setName(e.target.value)} placeholder="Nome" required disabled={isPending} />
|
|
122
|
+
<Input id="user-name" name="firstName" value={name} onChange={(e) => setName(e.target.value)} placeholder="Nome" required disabled={isPending} />
|
|
123
123
|
</div>
|
|
124
124
|
<div className="space-y-2">
|
|
125
125
|
<Label htmlFor="user-lastname">Sobrenome</Label>
|
|
126
|
-
<Input id="user-lastname" value={lastName} onChange={(e) => setLastName(e.target.value)} placeholder="Sobrenome" disabled={isPending} />
|
|
126
|
+
<Input id="user-lastname" name="lastName" value={lastName} onChange={(e) => setLastName(e.target.value)} placeholder="Sobrenome" disabled={isPending} />
|
|
127
127
|
</div>
|
|
128
128
|
</div>
|
|
129
129
|
<div className="space-y-2">
|
|
130
130
|
<Label htmlFor="user-email">E-mail *</Label>
|
|
131
|
-
<Input id="user-email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="email@exemplo.com" required disabled={isPending} />
|
|
131
|
+
<Input id="user-email" name="email" type="email" autoComplete="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="email@exemplo.com" required disabled={isPending} />
|
|
132
132
|
</div>
|
|
133
133
|
{!isEditing && (
|
|
134
134
|
<div className="space-y-2">
|
|
135
135
|
<Label htmlFor="user-password">Senha</Label>
|
|
136
|
-
<Input id="user-password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Senha inicial (opcional)" disabled={isPending} />
|
|
136
|
+
<Input id="user-password" name="password" type="password" autoComplete="new-password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Senha inicial (opcional)" disabled={isPending} />
|
|
137
137
|
</div>
|
|
138
138
|
)}
|
|
139
139
|
<div className="space-y-2">
|
|
140
|
-
<Label>Perfil de acesso</Label>
|
|
140
|
+
<Label htmlFor="user-profile">Perfil de acesso</Label>
|
|
141
141
|
<Select value={profile} onValueChange={setProfile} disabled={isPending}>
|
|
142
|
-
<SelectTrigger className="w-full"><SelectValue /></SelectTrigger>
|
|
142
|
+
<SelectTrigger id="user-profile" className="w-full"><SelectValue /></SelectTrigger>
|
|
143
143
|
<SelectContent>
|
|
144
144
|
{PROFILE_OPTIONS.map((opt) => (
|
|
145
145
|
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
|
|
@@ -155,7 +155,7 @@ export function UserFormDialog({
|
|
|
155
155
|
<DialogFooter>
|
|
156
156
|
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={isPending}>Cancelar</Button>
|
|
157
157
|
<Button type="submit" disabled={isPending || !name.trim() || !email.trim()}>
|
|
158
|
-
{isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
158
|
+
{isPending && <Loader2 aria-hidden="true" className="mr-2 h-4 w-4 animate-spin" />}
|
|
159
159
|
{isEditing ? "Salvar" : "Criar"}
|
|
160
160
|
</Button>
|
|
161
161
|
</DialogFooter>
|
|
@@ -73,7 +73,7 @@ function useColumns(
|
|
|
73
73
|
accessorKey: "last_login",
|
|
74
74
|
header: "Último acesso",
|
|
75
75
|
cell: ({ row }) => (
|
|
76
|
-
<span className="text-muted-foreground text-sm">
|
|
76
|
+
<span className="text-muted-foreground text-sm tabular-nums">
|
|
77
77
|
{row.original.last_login
|
|
78
78
|
? format(new Date(row.original.last_login), "dd/MM/yyyy HH:mm", { locale: ptBR })
|
|
79
79
|
: "—"}
|
|
@@ -84,7 +84,7 @@ function useColumns(
|
|
|
84
84
|
accessorKey: "datetime_add",
|
|
85
85
|
header: "Criado em",
|
|
86
86
|
cell: ({ row }) => (
|
|
87
|
-
<span className="text-muted-foreground text-sm">
|
|
87
|
+
<span className="text-muted-foreground text-sm tabular-nums">
|
|
88
88
|
{format(new Date(row.original.datetime_add), "dd/MM/yyyy", { locale: ptBR })}
|
|
89
89
|
</span>
|
|
90
90
|
),
|
|
@@ -97,7 +97,7 @@ function useColumns(
|
|
|
97
97
|
<div className="flex items-center gap-1">
|
|
98
98
|
<Tooltip>
|
|
99
99
|
<TooltipTrigger asChild>
|
|
100
|
-
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => onResetPassword(row.original)}>
|
|
100
|
+
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => onResetPassword(row.original)} aria-label="Reenviar senha">
|
|
101
101
|
<Forward className="h-4 w-4" />
|
|
102
102
|
</Button>
|
|
103
103
|
</TooltipTrigger>
|
|
@@ -105,7 +105,7 @@ function useColumns(
|
|
|
105
105
|
</Tooltip>
|
|
106
106
|
<Tooltip>
|
|
107
107
|
<TooltipTrigger asChild>
|
|
108
|
-
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => onEdit(row.original)}>
|
|
108
|
+
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => onEdit(row.original)} aria-label="Editar">
|
|
109
109
|
<Pencil className="h-4 w-4" />
|
|
110
110
|
</Button>
|
|
111
111
|
</TooltipTrigger>
|
|
@@ -113,7 +113,7 @@ function useColumns(
|
|
|
113
113
|
</Tooltip>
|
|
114
114
|
<Tooltip>
|
|
115
115
|
<TooltipTrigger asChild>
|
|
116
|
-
<Button variant="ghost" size="icon" className="h-8 w-8 text-destructive hover:text-destructive" onClick={() => onDelete(row.original.id)}>
|
|
116
|
+
<Button variant="ghost" size="icon" className="h-8 w-8 text-destructive hover:text-destructive" onClick={() => onDelete(row.original.id)} aria-label="Excluir">
|
|
117
117
|
<Trash2 className="h-4 w-4" />
|
|
118
118
|
</Button>
|
|
119
119
|
</TooltipTrigger>
|
|
@@ -195,19 +195,22 @@ export function UsersPage({ config, renderPhones }: UsersPageProps) {
|
|
|
195
195
|
<p className="text-sm text-muted-foreground">Gestão de usuários da conta</p>
|
|
196
196
|
</div>
|
|
197
197
|
<Button onClick={() => { setEditUser(null); setFormOpen(true); }} size="sm">
|
|
198
|
-
<Plus className="mr-2 h-4 w-4" />
|
|
198
|
+
<Plus aria-hidden="true" className="mr-2 h-4 w-4" />
|
|
199
199
|
Novo Usuário
|
|
200
200
|
</Button>
|
|
201
201
|
</div>
|
|
202
202
|
|
|
203
203
|
<div className="flex items-center gap-3">
|
|
204
204
|
<div className="relative flex-1 max-w-md">
|
|
205
|
-
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
205
|
+
<Search aria-hidden="true" className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
206
206
|
<Input
|
|
207
|
-
placeholder="Buscar por nome ou e-mail
|
|
207
|
+
placeholder="Buscar por nome ou e-mail\u2026"
|
|
208
208
|
value={search}
|
|
209
209
|
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
|
|
210
210
|
className="pl-9"
|
|
211
|
+
name="search"
|
|
212
|
+
autoComplete="off"
|
|
213
|
+
aria-label="Buscar por nome ou e-mail"
|
|
211
214
|
/>
|
|
212
215
|
</div>
|
|
213
216
|
<Select value={profileFilter} onValueChange={(v) => { setProfileFilter(v); setPage(1); }}>
|