@greatapps/greatauth-ui 0.3.11 → 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/dist/index.js +182 -82
- package/dist/index.js.map +1 -1
- package/dist/ui.js +12 -4
- package/dist/ui.js.map +1 -1
- package/package.json +1 -1
- package/src/components/app-header.tsx +46 -12
- package/src/components/app-sidebar.tsx +1 -1
- package/src/components/login-form.tsx +131 -81
- package/src/components/ui/sidebar.tsx +9 -5
- package/src/components/users/users-page.tsx +1 -1
- package/src/theme.css +18 -12
package/package.json
CHANGED
|
@@ -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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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((
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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>
|
|
@@ -111,7 +111,7 @@ export function AppSidebar({ config }: AppSidebarProps) {
|
|
|
111
111
|
<SidebarMenu>
|
|
112
112
|
<SidebarMenuItem>
|
|
113
113
|
<SidebarMenuButton size="lg" asChild>
|
|
114
|
-
<div className="flex items-center gap-2">
|
|
114
|
+
<div className="flex items-center gap-2 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:gap-0">
|
|
115
115
|
<div className="shrink-0">{config.appIcon}</div>
|
|
116
116
|
<span className="font-semibold truncate group-data-[collapsible=icon]:hidden">{config.appName}</span>
|
|
117
117
|
{config.appBadge && (
|
|
@@ -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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
<
|
|
71
|
-
|
|
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
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
+
© {new Date().getFullYear()} {config.appName}
|
|
206
|
+
</p>
|
|
207
|
+
</div>
|
|
158
208
|
</div>
|
|
159
209
|
</div>
|
|
160
210
|
);
|
|
@@ -24,8 +24,7 @@ import {
|
|
|
24
24
|
TooltipTrigger,
|
|
25
25
|
} from "./tooltip"
|
|
26
26
|
|
|
27
|
-
const
|
|
28
|
-
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
|
27
|
+
const SIDEBAR_STORAGE_KEY = "sidebar_state"
|
|
29
28
|
const SIDEBAR_WIDTH = "16rem"
|
|
30
29
|
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
|
31
30
|
const SIDEBAR_WIDTH_ICON = "3rem"
|
|
@@ -70,7 +69,13 @@ function SidebarProvider({
|
|
|
70
69
|
|
|
71
70
|
// This is the internal state of the sidebar.
|
|
72
71
|
// We use openProp and setOpenProp for control from outside the component.
|
|
73
|
-
const [_open, _setOpen] = React.useState(
|
|
72
|
+
const [_open, _setOpen] = React.useState(() => {
|
|
73
|
+
if (typeof window === "undefined") return defaultOpen
|
|
74
|
+
const stored = localStorage.getItem(SIDEBAR_STORAGE_KEY)
|
|
75
|
+
if (stored === "true") return true
|
|
76
|
+
if (stored === "false") return false
|
|
77
|
+
return defaultOpen
|
|
78
|
+
})
|
|
74
79
|
const open = openProp ?? _open
|
|
75
80
|
const setOpen = React.useCallback(
|
|
76
81
|
(value: boolean | ((value: boolean) => boolean)) => {
|
|
@@ -81,8 +86,7 @@ function SidebarProvider({
|
|
|
81
86
|
_setOpen(openState)
|
|
82
87
|
}
|
|
83
88
|
|
|
84
|
-
|
|
85
|
-
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
|
89
|
+
try { localStorage.setItem(SIDEBAR_STORAGE_KEY, String(openState)) } catch {}
|
|
86
90
|
},
|
|
87
91
|
[setOpenProp, open]
|
|
88
92
|
)
|
|
@@ -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.
|
|
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.
|
|
74
|
-
--chart-1: oklch(0.
|
|
75
|
-
--chart-2: oklch(0.
|
|
76
|
-
--chart-3: oklch(0.
|
|
77
|
-
--chart-4: oklch(0.
|
|
78
|
-
--chart-5: oklch(0.
|
|
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.
|
|
110
|
-
--chart-2: oklch(0.
|
|
111
|
-
--chart-3: oklch(0.
|
|
112
|
-
--chart-4: oklch(0.
|
|
113
|
-
--chart-5: oklch(0.
|
|
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 */
|