@greatapps/greatauth-ui 0.3.12 → 0.3.13

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.12",
3
+ "version": "0.3.13",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -1,16 +1,19 @@
1
1
  "use client";
2
2
 
3
- import { Fragment } from "react";
3
+ import { Fragment, useMemo } from "react";
4
4
  import { usePathname } from "next/navigation";
5
+ import Link from "next/link";
5
6
  import type { AppShellConfig } from "../types";
6
7
  import { SidebarTrigger } from "./ui/sidebar";
7
8
  import { Separator } from "./ui/separator";
8
9
  import {
9
10
  Breadcrumb,
10
11
  BreadcrumbItem,
12
+ BreadcrumbLink,
11
13
  BreadcrumbList,
12
14
  BreadcrumbPage,
13
15
  BreadcrumbSeparator,
16
+ BreadcrumbEllipsis,
14
17
  } from "./ui/breadcrumb";
15
18
  import { ThemeToggle } from "./theme-toggle";
16
19
 
@@ -21,9 +24,21 @@ interface AppHeaderProps {
21
24
  export function AppHeader({ config }: AppHeaderProps) {
22
25
  const pathname = usePathname();
23
26
  const segments = pathname.split("/").filter(Boolean);
24
- const breadcrumbs = segments
25
- .map((seg) => config.routeLabels[seg])
26
- .filter(Boolean);
27
+
28
+ const breadcrumbs = useMemo(() => {
29
+ const items: { label: string; href: string }[] = [];
30
+ for (let i = 0; i < segments.length; i++) {
31
+ const label = config.routeLabels[segments[i]];
32
+ if (label) {
33
+ const href = "/" + segments.slice(0, i + 1).join("/");
34
+ items.push({ label, href });
35
+ }
36
+ }
37
+ return items;
38
+ }, [segments, config.routeLabels]);
39
+
40
+ const isLast = (i: number) => i === breadcrumbs.length - 1;
41
+ const showEllipsis = breadcrumbs.length > 2;
27
42
 
28
43
  return (
29
44
  <header className="flex h-14 shrink-0 items-center gap-2 border-b px-4">
@@ -33,14 +48,33 @@ export function AppHeader({ config }: AppHeaderProps) {
33
48
  <Breadcrumb className="flex-1">
34
49
  <BreadcrumbList>
35
50
  {breadcrumbs.length > 0 ? (
36
- breadcrumbs.map((label, i) => (
37
- <Fragment key={i}>
38
- {i > 0 && <BreadcrumbSeparator />}
39
- <BreadcrumbItem>
40
- <BreadcrumbPage>{label}</BreadcrumbPage>
41
- </BreadcrumbItem>
42
- </Fragment>
43
- ))
51
+ breadcrumbs.map((crumb, i) => {
52
+ // On mobile (< sm), hide early items when there are more than 2
53
+ const hiddenOnMobile = showEllipsis && i < breadcrumbs.length - 2;
54
+
55
+ return (
56
+ <Fragment key={i}>
57
+ {/* Ellipsis shown on mobile only, before the last 2 items */}
58
+ {showEllipsis && i === breadcrumbs.length - 2 && (
59
+ <BreadcrumbItem className="sm:hidden">
60
+ <BreadcrumbEllipsis />
61
+ </BreadcrumbItem>
62
+ )}
63
+ {i > 0 && (
64
+ <BreadcrumbSeparator className={hiddenOnMobile ? "hidden sm:flex" : undefined} />
65
+ )}
66
+ <BreadcrumbItem className={hiddenOnMobile ? "hidden sm:flex" : undefined}>
67
+ {isLast(i) ? (
68
+ <BreadcrumbPage>{crumb.label}</BreadcrumbPage>
69
+ ) : (
70
+ <BreadcrumbLink asChild>
71
+ <Link href={crumb.href}>{crumb.label}</Link>
72
+ </BreadcrumbLink>
73
+ )}
74
+ </BreadcrumbItem>
75
+ </Fragment>
76
+ );
77
+ })
44
78
  ) : config.defaultBreadcrumb ? (
45
79
  <BreadcrumbItem>
46
80
  <BreadcrumbPage>{config.defaultBreadcrumb}</BreadcrumbPage>
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useState } from "react";
4
4
  import { useRouter, useSearchParams } from "next/navigation";
5
- import { Loader2, Mail, Lock, AlertCircle } from "lucide-react";
5
+ import { Loader2, Mail, Lock, AlertCircle, Eye, EyeOff } from "lucide-react";
6
6
  import type { LoginFormConfig } from "../types";
7
7
  import { authClient } from "../auth";
8
8
  import { Button } from "./ui/button";
@@ -10,6 +10,7 @@ import { Input } from "./ui/input";
10
10
  import { Label } from "./ui/label";
11
11
  import { Badge } from "./ui/badge";
12
12
  import { cn } from "../lib/utils";
13
+ import { ThemeToggle } from "./theme-toggle";
13
14
 
14
15
  interface LoginFormProps {
15
16
  config: LoginFormConfig;
@@ -24,6 +25,7 @@ export function LoginForm({ config }: LoginFormProps) {
24
25
  const [password, setPassword] = useState("");
25
26
  const [loading, setLoading] = useState(false);
26
27
  const [error, setError] = useState("");
28
+ const [showPassword, setShowPassword] = useState(false);
27
29
 
28
30
  const handleSubmit = async (e: React.FormEvent) => {
29
31
  e.preventDefault();
@@ -64,97 +66,145 @@ export function LoginForm({ config }: LoginFormProps) {
64
66
  };
65
67
 
66
68
  return (
67
- <div className="flex min-h-svh items-center justify-center bg-muted/30 p-4">
68
- <div className="w-full max-w-[400px]">
69
- {/* Header */}
70
- <div className="mb-8 text-center">
71
- <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-primary text-primary-foreground shadow-sm">
69
+ <div className="relative flex min-h-svh">
70
+ {/* Theme Toggle */}
71
+ <div className="fixed top-4 right-4 z-50">
72
+ <ThemeToggle />
73
+ </div>
74
+
75
+ {/* Left Panel — Branding (desktop only) */}
76
+ <div
77
+ className="relative hidden items-center justify-center overflow-hidden bg-primary md:flex md:w-1/2"
78
+ style={{
79
+ backgroundImage:
80
+ "radial-gradient(circle, rgba(255,255,255,0.1) 1px, transparent 1px)",
81
+ backgroundSize: "24px 24px",
82
+ }}
83
+ >
84
+ {/* Gradient overlay */}
85
+ <div className="absolute inset-0 bg-gradient-to-br from-primary/80 via-primary to-primary/90" />
86
+
87
+ <div className="relative z-10 flex flex-col items-center gap-4 px-8 text-center text-primary-foreground">
88
+ <div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-primary-foreground/15 shadow-lg backdrop-blur-sm">
72
89
  {config.icon}
73
90
  </div>
74
- <div className="flex items-center justify-center gap-2">
75
- <h1 className="text-2xl font-semibold tracking-tight">
76
- {config.appName}
77
- </h1>
78
- {config.appBadge && (
79
- <Badge variant={config.appBadge.variant} className="text-xs">
80
- {config.appBadge.text}
81
- </Badge>
82
- )}
83
- </div>
84
- <p className="mt-1.5 text-sm text-muted-foreground">
91
+ <h2 className="text-3xl font-bold tracking-tight">
92
+ {config.appName}
93
+ </h2>
94
+ <p className="max-w-xs text-base text-primary-foreground/80">
85
95
  {config.description}
86
96
  </p>
87
97
  </div>
98
+ </div>
88
99
 
89
- {/* Form Card */}
90
- <div className="rounded-xl border bg-card p-6 shadow-sm">
91
- <form onSubmit={handleSubmit} className="space-y-4">
92
- {error && (
93
- <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">
94
- <AlertCircle className="h-4 w-4 shrink-0" />
95
- {error}
96
- </div>
97
- )}
98
-
99
- <div className="space-y-1.5">
100
- <Label htmlFor="login-email" className="text-sm font-medium">
101
- Email
102
- </Label>
103
- <div className="relative">
104
- <Mail className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
105
- <Input
106
- id="login-email"
107
- type="email"
108
- placeholder="email@exemplo.com"
109
- value={email}
110
- onChange={(e) => setEmail(e.target.value)}
111
- className="pl-9"
112
- autoComplete="email"
113
- required
114
- />
115
- </div>
100
+ {/* Right Panel — Form */}
101
+ <div className="flex w-full items-center justify-center bg-background p-4 md:w-1/2">
102
+ <div className="w-full max-w-sm">
103
+ {/* Header (visible always, primary branding on mobile) */}
104
+ <div className="mb-8 text-center">
105
+ <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-primary text-primary-foreground shadow-sm md:hidden">
106
+ {config.icon}
116
107
  </div>
117
-
118
- <div className="space-y-1.5">
119
- <Label htmlFor="login-password" className="text-sm font-medium">
120
- Senha
121
- </Label>
122
- <div className="relative">
123
- <Lock className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
124
- <Input
125
- id="login-password"
126
- type="password"
127
- placeholder="••••••••"
128
- value={password}
129
- onChange={(e) => setPassword(e.target.value)}
130
- className="pl-9"
131
- autoComplete="current-password"
132
- required
133
- />
134
- </div>
108
+ <div className="flex items-center justify-center gap-2">
109
+ <h1 className="text-3xl font-bold tracking-tight">
110
+ {config.appName}
111
+ </h1>
112
+ {config.appBadge && (
113
+ <Badge variant={config.appBadge.variant} className="text-xs">
114
+ {config.appBadge.text}
115
+ </Badge>
116
+ )}
135
117
  </div>
118
+ <p className="mt-1.5 text-base text-muted-foreground">
119
+ {config.description}
120
+ </p>
121
+ </div>
136
122
 
137
- <Button
138
- type="submit"
139
- className={cn("w-full", loading && "cursor-wait")}
140
- disabled={loading}
141
- >
142
- {loading ? (
143
- <>
144
- <Loader2 className="h-4 w-4 animate-spin" />
145
- A entrar...
146
- </>
147
- ) : (
148
- "Entrar"
123
+ {/* Form Card */}
124
+ <div className="rounded-xl border bg-card p-6 shadow-sm">
125
+ <form onSubmit={handleSubmit} className="space-y-4">
126
+ {error && (
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" />
129
+ {error}
130
+ </div>
149
131
  )}
150
- </Button>
151
- </form>
152
- </div>
153
132
 
154
- {/* Footer */}
155
- <p className="mt-6 text-center text-xs text-muted-foreground">
156
- {config.footerText || "Acesso restrito a utilizadores autorizados"}
157
- </p>
133
+ <div className="space-y-1.5">
134
+ <Label htmlFor="login-email" className="text-sm font-medium">
135
+ Email
136
+ </Label>
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" />
139
+ <Input
140
+ id="login-email"
141
+ type="email"
142
+ placeholder="email@exemplo.com"
143
+ value={email}
144
+ onChange={(e) => setEmail(e.target.value)}
145
+ className="pl-9"
146
+ autoComplete="email"
147
+ required
148
+ />
149
+ </div>
150
+ </div>
151
+
152
+ <div className="space-y-1.5">
153
+ <Label htmlFor="login-password" className="text-sm font-medium">
154
+ Senha
155
+ </Label>
156
+ <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
+ <Input
159
+ id="login-password"
160
+ type={showPassword ? "text" : "password"}
161
+ placeholder="••••••••"
162
+ value={password}
163
+ onChange={(e) => setPassword(e.target.value)}
164
+ className="pl-9 pr-10"
165
+ autoComplete="current-password"
166
+ required
167
+ />
168
+ <button
169
+ type="button"
170
+ onClick={() => setShowPassword(!showPassword)}
171
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
172
+ aria-label={showPassword ? "Ocultar senha" : "Mostrar senha"}
173
+ >
174
+ {showPassword ? (
175
+ <EyeOff className="h-4 w-4" />
176
+ ) : (
177
+ <Eye className="h-4 w-4" />
178
+ )}
179
+ </button>
180
+ </div>
181
+ </div>
182
+
183
+ <Button
184
+ type="submit"
185
+ className={cn("w-full", loading && "cursor-wait")}
186
+ disabled={loading}
187
+ >
188
+ {loading ? (
189
+ <>
190
+ <Loader2 className="h-4 w-4 animate-spin" />
191
+ A entrar...
192
+ </>
193
+ ) : (
194
+ "Entrar"
195
+ )}
196
+ </Button>
197
+ </form>
198
+ </div>
199
+
200
+ {/* Footer */}
201
+ <p className="mt-6 text-center text-xs text-muted-foreground">
202
+ {config.footerText || "Acesso restrito a utilizadores autorizados"}
203
+ </p>
204
+ <p className="mt-1 text-center text-xs text-muted-foreground">
205
+ &copy; {new Date().getFullYear()} {config.appName}
206
+ </p>
207
+ </div>
158
208
  </div>
159
209
  </div>
160
210
  );
@@ -188,7 +188,7 @@ export function UsersPage({ config, renderPhones }: UsersPageProps) {
188
188
  }
189
189
 
190
190
  return (
191
- <div className="flex flex-col gap-4 p-4">
191
+ <div className="flex flex-col gap-4 p-4 md:p-6">
192
192
  <div className="flex items-center justify-between">
193
193
  <div>
194
194
  <h1 className="text-xl font-semibold">Usuários</h1>
package/src/theme.css CHANGED
@@ -31,6 +31,7 @@
31
31
  --color-input: var(--input);
32
32
  --color-border: var(--border);
33
33
  --color-destructive: var(--destructive);
34
+ --color-success: var(--success);
34
35
  --color-accent-foreground: var(--accent-foreground);
35
36
  --color-accent: var(--accent);
36
37
  --color-muted-foreground: var(--muted-foreground);
@@ -64,18 +65,19 @@
64
65
  --secondary: oklch(0.967 0.001 286.375);
65
66
  --secondary-foreground: oklch(0.21 0.006 285.885);
66
67
  --muted: oklch(0.967 0.001 286.375);
67
- --muted-foreground: oklch(0.552 0.016 285.938);
68
+ --muted-foreground: oklch(0.50 0.016 285.938);
68
69
  --accent: oklch(0.967 0.001 286.375);
69
70
  --accent-foreground: oklch(0.21 0.006 285.885);
70
71
  --destructive: oklch(0.577 0.245 27.325);
72
+ --success: oklch(0.527 0.154 150.069);
71
73
  --border: oklch(0.92 0.004 286.32);
72
74
  --input: oklch(0.92 0.004 286.32);
73
- --ring: oklch(0.705 0.015 286.067);
74
- --chart-1: oklch(0.646 0.222 41.116);
75
- --chart-2: oklch(0.6 0.118 184.704);
76
- --chart-3: oklch(0.398 0.07 227.392);
77
- --chart-4: oklch(0.828 0.189 84.429);
78
- --chart-5: oklch(0.769 0.188 70.08);
75
+ --ring: oklch(0.588 0.158 241.966);
76
+ --chart-1: oklch(0.588 0.158 241.966);
77
+ --chart-2: oklch(0.637 0.179 163.223);
78
+ --chart-3: oklch(0.553 0.195 255.065);
79
+ --chart-4: oklch(0.705 0.213 47.604);
80
+ --chart-5: oklch(0.637 0.237 25.331);
79
81
  --radius: 0.625rem;
80
82
  --sidebar: oklch(0.985 0 0);
81
83
  --sidebar-foreground: oklch(0.141 0.005 285.823);
@@ -103,14 +105,15 @@
103
105
  --accent: oklch(0.274 0.006 286.033);
104
106
  --accent-foreground: oklch(0.985 0 0);
105
107
  --destructive: oklch(0.704 0.191 22.216);
108
+ --success: oklch(0.696 0.17 162.48);
106
109
  --border: oklch(1 0 0 / 10%);
107
110
  --input: oklch(1 0 0 / 15%);
108
111
  --ring: oklch(0.552 0.016 285.938);
109
- --chart-1: oklch(0.488 0.243 264.376);
110
- --chart-2: oklch(0.696 0.17 162.48);
111
- --chart-3: oklch(0.769 0.188 70.08);
112
- --chart-4: oklch(0.627 0.265 303.9);
113
- --chart-5: oklch(0.645 0.246 16.439);
112
+ --chart-1: oklch(0.688 0.158 241.966);
113
+ --chart-2: oklch(0.737 0.179 163.223);
114
+ --chart-3: oklch(0.653 0.195 255.065);
115
+ --chart-4: oklch(0.765 0.183 47.604);
116
+ --chart-5: oklch(0.717 0.217 25.331);
114
117
  --sidebar: oklch(0.21 0.006 285.885);
115
118
  --sidebar-foreground: oklch(0.985 0 0);
116
119
  --sidebar-primary: oklch(0.488 0.243 264.376);
@@ -128,6 +131,9 @@
128
131
  body {
129
132
  @apply bg-background text-foreground;
130
133
  }
134
+ a, button, input, select, textarea {
135
+ @apply transition-colors duration-150;
136
+ }
131
137
  }
132
138
 
133
139
  /* View Transition — dark/light mode circle reveal */