@greatapps/greatauth-ui 0.3.14 → 0.3.16

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@greatapps/greatauth-ui",
3
- "version": "0.3.14",
3
+ "version": "0.3.16",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -4,7 +4,7 @@ import { Fragment, useMemo } from "react";
4
4
  import { usePathname } from "next/navigation";
5
5
  import Link from "next/link";
6
6
  import type { AppShellConfig } from "../types";
7
- import { SidebarTrigger, useSidebar } from "./ui/sidebar";
7
+ import { SidebarTrigger } from "./ui/sidebar";
8
8
  import { Separator } from "./ui/separator";
9
9
  import {
10
10
  Breadcrumb,
@@ -23,7 +23,6 @@ interface AppHeaderProps {
23
23
 
24
24
  export function AppHeader({ config }: AppHeaderProps) {
25
25
  const pathname = usePathname();
26
- const { state } = useSidebar();
27
26
  const segments = pathname.split("/").filter(Boolean);
28
27
 
29
28
  const breadcrumbs = useMemo(() => {
@@ -44,12 +43,6 @@ export function AppHeader({ config }: AppHeaderProps) {
44
43
  return (
45
44
  <header className="flex h-14 shrink-0 items-center gap-2 border-b px-4">
46
45
  <SidebarTrigger className="-ml-1" />
47
- {state === "collapsed" && config.appIcon && (
48
- <>
49
- <Separator orientation="vertical" className="!h-4" />
50
- <div className="shrink-0 text-sidebar-foreground">{config.appIcon}</div>
51
- </>
52
- )}
53
46
  <Separator orientation="vertical" className="mr-2 !h-4" />
54
47
 
55
48
  <Breadcrumb className="flex-1">
@@ -173,7 +173,7 @@ export function AppSidebar({ config }: AppSidebarProps) {
173
173
  <span className="truncate font-semibold">{userName}</span>
174
174
  <span className="truncate text-xs text-muted-foreground">{userEmail}</span>
175
175
  </div>
176
- <ChevronsUpDown className="ml-auto size-4 group-data-[collapsible=icon]:hidden" />
176
+ <ChevronsUpDown aria-hidden="true" className="ml-auto size-4 group-data-[collapsible=icon]:hidden" />
177
177
  </SidebarMenuButton>
178
178
  </DropdownMenuTrigger>
179
179
  <DropdownMenuContent
@@ -191,7 +191,7 @@ export function AppSidebar({ config }: AppSidebarProps) {
191
191
  <DropdownMenuSeparator />
192
192
  {config.footerExtra}
193
193
  <DropdownMenuItem onClick={handleLogout}>
194
- <LogOut className="size-4" />
194
+ <LogOut aria-hidden="true" className="size-4" />
195
195
  Sair
196
196
  </DropdownMenuItem>
197
197
  </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..." : "Confirmar"}
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-all dark:-rotate-90 dark:scale-0" />
37
- <Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
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); }}>