create-tigra 2.6.5 → 2.6.8
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 +1 -1
- package/template/_claude/rules/client/01-project-structure.md +2 -5
- package/template/_claude/rules/client/04-design-system.md +48 -43
- package/template/_claude/rules/client/core.md +2 -2
- package/template/client/src/app/globals.css +12 -12
- package/template/client/src/app/layout.tsx +1 -1
- package/template/client/src/app/page.tsx +5 -5
- package/template/client/src/app/providers.tsx +1 -1
- package/template/client/src/components/common/ThemeToggle.tsx +59 -0
- package/template/client/src/features/admin/hooks/useAdminSessions.ts +68 -0
- package/template/client/src/features/admin/hooks/useAdminStats.ts +27 -0
- package/template/client/src/features/admin/hooks/useAdminUsers.ts +132 -0
- package/template/client/src/features/admin/services/admin.service.ts +94 -0
- package/template/client/src/features/admin/types/admin.types.ts +65 -0
- package/template/client/src/features/auth/components/AuthInitializer.tsx +18 -1
- package/template/client/src/lib/api/axios.config.ts +20 -1
- package/template/client/src/lib/constants/api-endpoints.ts +9 -0
- package/template/client/src/lib/constants/app.constants.ts +3 -1
- package/template/client/src/lib/constants/routes.ts +6 -0
- package/template/client/src/lib/env.ts +35 -0
- package/template/client/src/styles/themes/default.css +92 -0
- package/template/server/package.json +1 -0
- package/template/server/postman/collection.json +168 -50
- package/template/server/prisma/schema.prisma +2 -0
- package/template/server/src/jobs/cleanup-deleted-accounts.job.ts +14 -4
- package/template/server/src/libs/prisma.ts +13 -0
- package/template/server/src/modules/admin/admin.controller.ts +130 -1
- package/template/server/src/modules/admin/admin.repo.ts +289 -0
- package/template/server/src/modules/admin/admin.routes.ts +113 -7
- package/template/server/src/modules/admin/admin.schemas.ts +49 -0
- package/template/server/src/modules/admin/admin.service.ts +154 -0
- package/template/server/src/modules/auth/auth.repo.ts +5 -18
- package/template/server/src/modules/auth/auth.service.ts +20 -28
- package/template/server/src/modules/auth/session.repo.ts +10 -5
- package/template/client/src/components/common/ThemeSwitcher.tsx +0 -112
- package/template/client/src/styles/themes/electric-indigo.css +0 -90
- package/template/client/src/styles/themes/ocean-teal.css +0 -90
- package/template/client/src/styles/themes/rose-pink.css +0 -90
- package/template/client/src/styles/themes/warm-orange.css +0 -90
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { apiClient } from '@/lib/api/axios.config';
|
|
2
|
+
import { API_ENDPOINTS } from '@/lib/constants/api-endpoints';
|
|
3
|
+
|
|
4
|
+
import type { ApiResponse, PaginatedApiResponse } from '@/lib/api/api.types';
|
|
5
|
+
import type {
|
|
6
|
+
IAdminUser,
|
|
7
|
+
IAdminUserDetail,
|
|
8
|
+
IAdminSession,
|
|
9
|
+
IDashboardStats,
|
|
10
|
+
IGetUsersParams,
|
|
11
|
+
IGetSessionsParams,
|
|
12
|
+
IUpdateUserStatusRequest,
|
|
13
|
+
IUpdateUserRoleRequest,
|
|
14
|
+
} from '../types/admin.types';
|
|
15
|
+
|
|
16
|
+
interface PaginatedData<T> {
|
|
17
|
+
items: T[];
|
|
18
|
+
pagination: {
|
|
19
|
+
page: number;
|
|
20
|
+
limit: number;
|
|
21
|
+
totalItems: number;
|
|
22
|
+
totalPages: number;
|
|
23
|
+
hasNextPage: boolean;
|
|
24
|
+
hasPreviousPage: boolean;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class AdminService {
|
|
29
|
+
// ─── Dashboard Stats ────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
async getDashboardStats(): Promise<IDashboardStats> {
|
|
32
|
+
const response = await apiClient.get<ApiResponse<IDashboardStats>>(
|
|
33
|
+
API_ENDPOINTS.ADMIN.STATS,
|
|
34
|
+
);
|
|
35
|
+
return response.data.data;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── User Management ───────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
async getUsers(params?: IGetUsersParams): Promise<PaginatedData<IAdminUser>> {
|
|
41
|
+
const response = await apiClient.get<PaginatedApiResponse<IAdminUser>>(
|
|
42
|
+
API_ENDPOINTS.ADMIN.USERS,
|
|
43
|
+
{ params },
|
|
44
|
+
);
|
|
45
|
+
return response.data.data;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async getUserDetail(userId: string): Promise<IAdminUserDetail> {
|
|
49
|
+
const response = await apiClient.get<ApiResponse<IAdminUserDetail>>(
|
|
50
|
+
API_ENDPOINTS.ADMIN.USER_DETAIL(userId),
|
|
51
|
+
);
|
|
52
|
+
return response.data.data;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async updateUserStatus(
|
|
56
|
+
userId: string,
|
|
57
|
+
data: IUpdateUserStatusRequest,
|
|
58
|
+
): Promise<IAdminUser> {
|
|
59
|
+
const response = await apiClient.patch<ApiResponse<IAdminUser>>(
|
|
60
|
+
API_ENDPOINTS.ADMIN.USER_STATUS(userId),
|
|
61
|
+
data,
|
|
62
|
+
);
|
|
63
|
+
return response.data.data;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async updateUserRole(
|
|
67
|
+
userId: string,
|
|
68
|
+
data: IUpdateUserRoleRequest,
|
|
69
|
+
): Promise<IAdminUser> {
|
|
70
|
+
const response = await apiClient.patch<ApiResponse<IAdminUser>>(
|
|
71
|
+
API_ENDPOINTS.ADMIN.USER_ROLE(userId),
|
|
72
|
+
data,
|
|
73
|
+
);
|
|
74
|
+
return response.data.data;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── Session Management ────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
async getSessions(
|
|
80
|
+
params?: IGetSessionsParams,
|
|
81
|
+
): Promise<PaginatedData<IAdminSession>> {
|
|
82
|
+
const response = await apiClient.get<PaginatedApiResponse<IAdminSession>>(
|
|
83
|
+
API_ENDPOINTS.ADMIN.SESSIONS,
|
|
84
|
+
{ params },
|
|
85
|
+
);
|
|
86
|
+
return response.data.data;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async deleteSession(sessionId: string): Promise<void> {
|
|
90
|
+
await apiClient.delete(API_ENDPOINTS.ADMIN.SESSION_DELETE(sessionId));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const adminService = new AdminService();
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { IUser } from '@/features/auth/types/auth.types';
|
|
2
|
+
import type { PaginationParams } from '@/lib/api/api.types';
|
|
3
|
+
|
|
4
|
+
// ─── Admin User ─────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export interface IAdminUser extends IUser {
|
|
7
|
+
deletedAt: string | null;
|
|
8
|
+
failedLoginAttempts: number;
|
|
9
|
+
lockedUntil: string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface IAdminUserDetail extends IAdminUser {
|
|
13
|
+
sessionCount: number;
|
|
14
|
+
lastLogin: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ─── Admin Session ──────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export interface IAdminSession {
|
|
20
|
+
id: string;
|
|
21
|
+
userId: string;
|
|
22
|
+
deviceInfo: string | null;
|
|
23
|
+
ipAddress: string | null;
|
|
24
|
+
lastActiveAt: string;
|
|
25
|
+
expiresAt: string;
|
|
26
|
+
createdAt: string;
|
|
27
|
+
user: {
|
|
28
|
+
id: string;
|
|
29
|
+
email: string;
|
|
30
|
+
firstName: string;
|
|
31
|
+
lastName: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── Dashboard Stats ────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
export interface IDashboardStats {
|
|
38
|
+
totalUsers: number;
|
|
39
|
+
activeUsers: number;
|
|
40
|
+
adminCount: number;
|
|
41
|
+
recentSignups: number;
|
|
42
|
+
activeSessions: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── Request Params ─────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
export interface IGetUsersParams extends PaginationParams {
|
|
48
|
+
search?: string;
|
|
49
|
+
role?: 'USER' | 'ADMIN';
|
|
50
|
+
isActive?: boolean;
|
|
51
|
+
sortBy?: 'createdAt' | 'email' | 'firstName';
|
|
52
|
+
sortOrder?: 'asc' | 'desc';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface IGetSessionsParams extends PaginationParams {
|
|
56
|
+
userId?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface IUpdateUserStatusRequest {
|
|
60
|
+
isActive: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface IUpdateUserRoleRequest {
|
|
64
|
+
role: 'USER' | 'ADMIN';
|
|
65
|
+
}
|
|
@@ -12,6 +12,11 @@ import { setUser, setInitialized } from '../store/authSlice';
|
|
|
12
12
|
|
|
13
13
|
const PROTECTED_PATHS: string[] = [ROUTES.DASHBOARD, ROUTES.PROFILE, '/admin'];
|
|
14
14
|
|
|
15
|
+
// Auth pages where getMe() should never fire — there is no session to hydrate
|
|
16
|
+
// on login/register/reset-password, and calling getMe() here would trigger the
|
|
17
|
+
// 401 → refresh → fail → redirect chain for no reason.
|
|
18
|
+
const AUTH_PATHS: string[] = [ROUTES.LOGIN, ROUTES.REGISTER, ROUTES.RESET_PASSWORD, ROUTES.VERIFY_EMAIL];
|
|
19
|
+
|
|
15
20
|
interface AuthInitializerProps {
|
|
16
21
|
children: React.ReactNode;
|
|
17
22
|
}
|
|
@@ -20,6 +25,10 @@ function isProtectedPath(pathname: string): boolean {
|
|
|
20
25
|
return PROTECTED_PATHS.some((path) => pathname.startsWith(path));
|
|
21
26
|
}
|
|
22
27
|
|
|
28
|
+
function isAuthPage(pathname: string): boolean {
|
|
29
|
+
return AUTH_PATHS.some((path) => pathname.startsWith(path));
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
export const AuthInitializer = ({ children }: AuthInitializerProps): React.ReactElement => {
|
|
24
33
|
const dispatch = useAppDispatch();
|
|
25
34
|
const pathname = usePathname();
|
|
@@ -27,7 +36,15 @@ export const AuthInitializer = ({ children }: AuthInitializerProps): React.React
|
|
|
27
36
|
const { isAuthenticated, isLoggingOut } = useAppSelector((state) => state.auth);
|
|
28
37
|
|
|
29
38
|
useEffect(() => {
|
|
30
|
-
// On
|
|
39
|
+
// On auth pages (login, register, etc.), never call getMe().
|
|
40
|
+
// There is no session to hydrate, and a 401 here would trigger
|
|
41
|
+
// the refresh → fail → redirect chain, causing an infinite loop.
|
|
42
|
+
if (isAuthPage(pathname)) {
|
|
43
|
+
dispatch(setInitialized());
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// On other public pages, skip auth hydration — just mark as initialized
|
|
31
48
|
if (!isProtectedPath(pathname)) {
|
|
32
49
|
dispatch(setInitialized());
|
|
33
50
|
return;
|
|
@@ -4,9 +4,10 @@ import type { InternalAxiosRequestConfig } from 'axios';
|
|
|
4
4
|
|
|
5
5
|
import { API_ENDPOINTS } from '@/lib/constants/api-endpoints';
|
|
6
6
|
import { ROUTES } from '@/lib/constants/routes';
|
|
7
|
+
import { env } from '@/lib/env';
|
|
7
8
|
|
|
8
9
|
const apiClient = axios.create({
|
|
9
|
-
baseURL:
|
|
10
|
+
baseURL: env.NEXT_PUBLIC_API_BASE_URL,
|
|
10
11
|
timeout: 30000,
|
|
11
12
|
withCredentials: true, // Send cookies with every request
|
|
12
13
|
});
|
|
@@ -18,6 +19,14 @@ let refreshTimestamp = 0;
|
|
|
18
19
|
const REFRESH_TIMEOUT_MS = 15000;
|
|
19
20
|
let failedQueue: { resolve: () => void; reject: (error: unknown) => void }[] = [];
|
|
20
21
|
|
|
22
|
+
// Circuit breaker — once a refresh fails with a definitive auth error (4xx),
|
|
23
|
+
// this flag is set BEFORE dispatching logout or redirecting. All subsequent
|
|
24
|
+
// 401s are immediately rejected without entering the refresh flow, preventing
|
|
25
|
+
// the race condition where Redux state changes trigger re-renders that fire
|
|
26
|
+
// new API calls before the hard redirect completes.
|
|
27
|
+
// Resets automatically on page reload (window.location.href re-initializes the module).
|
|
28
|
+
let isSessionDead = false;
|
|
29
|
+
|
|
21
30
|
const processQueue = (error: unknown): void => {
|
|
22
31
|
failedQueue.forEach((promise) => {
|
|
23
32
|
if (error) {
|
|
@@ -43,6 +52,12 @@ apiClient.interceptors.response.use(
|
|
|
43
52
|
return Promise.reject(error);
|
|
44
53
|
}
|
|
45
54
|
|
|
55
|
+
// Circuit breaker tripped — session is dead, redirect is pending.
|
|
56
|
+
// Reject immediately without attempting refresh or queuing.
|
|
57
|
+
if (isSessionDead) {
|
|
58
|
+
return Promise.reject(error);
|
|
59
|
+
}
|
|
60
|
+
|
|
46
61
|
// Don't retry auth endpoints that don't use tokens —
|
|
47
62
|
// a 401 here means wrong credentials, not an expired token.
|
|
48
63
|
const noRetryEndpoints: string[] = [
|
|
@@ -93,6 +108,10 @@ apiClient.interceptors.response.use(
|
|
|
93
108
|
refreshError.response.status < 500;
|
|
94
109
|
|
|
95
110
|
if (isAuthFailure) {
|
|
111
|
+
// Trip the circuit breaker FIRST — prevents any subsequent 401s
|
|
112
|
+
// from re-entering this flow while the redirect is pending.
|
|
113
|
+
isSessionDead = true;
|
|
114
|
+
|
|
96
115
|
const { logout } = await import('@/features/auth/store/authSlice');
|
|
97
116
|
const { store } = await import('@/store');
|
|
98
117
|
store.dispatch(logout());
|
|
@@ -13,4 +13,13 @@ export const API_ENDPOINTS = {
|
|
|
13
13
|
UPDATE_ME: '/users/me',
|
|
14
14
|
DELETE_ME: '/users/me',
|
|
15
15
|
},
|
|
16
|
+
ADMIN: {
|
|
17
|
+
STATS: '/admin/stats',
|
|
18
|
+
USERS: '/admin/users',
|
|
19
|
+
USER_DETAIL: (userId: string) => `/admin/users/${userId}`,
|
|
20
|
+
USER_STATUS: (userId: string) => `/admin/users/${userId}/status`,
|
|
21
|
+
USER_ROLE: (userId: string) => `/admin/users/${userId}/role`,
|
|
22
|
+
SESSIONS: '/admin/sessions',
|
|
23
|
+
SESSION_DELETE: (sessionId: string) => `/admin/sessions/${sessionId}`,
|
|
24
|
+
},
|
|
16
25
|
} as const;
|
|
@@ -6,4 +6,10 @@ export const ROUTES = {
|
|
|
6
6
|
RESET_PASSWORD: '/reset-password',
|
|
7
7
|
DASHBOARD: '/dashboard',
|
|
8
8
|
PROFILE: '/profile',
|
|
9
|
+
ADMIN: {
|
|
10
|
+
DASHBOARD: '/admin',
|
|
11
|
+
USERS: '/admin/users',
|
|
12
|
+
USER_DETAIL: (userId: string) => `/admin/users/${userId}`,
|
|
13
|
+
SESSIONS: '/admin/sessions',
|
|
14
|
+
},
|
|
9
15
|
} as const;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
// Every field MUST have a .default() so the dev fallback (envSchema.parse({}))
|
|
4
|
+
// can produce valid defaults when env vars are missing in development.
|
|
5
|
+
const envSchema = z.object({
|
|
6
|
+
NEXT_PUBLIC_API_BASE_URL: z
|
|
7
|
+
.string()
|
|
8
|
+
.url('NEXT_PUBLIC_API_BASE_URL must be a valid URL')
|
|
9
|
+
.default('http://localhost:8000/api/v1'),
|
|
10
|
+
NEXT_PUBLIC_APP_NAME: z.string().default('My App'),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function validateEnv(): z.infer<typeof envSchema> {
|
|
14
|
+
const result = envSchema.safeParse({
|
|
15
|
+
NEXT_PUBLIC_API_BASE_URL: process.env.NEXT_PUBLIC_API_BASE_URL,
|
|
16
|
+
NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (!result.success) {
|
|
20
|
+
const formatted = result.error.issues
|
|
21
|
+
.map((issue) => ` - ${issue.path.join('.')}: ${issue.message}`)
|
|
22
|
+
.join('\n');
|
|
23
|
+
|
|
24
|
+
console.error(`\n❌ Invalid environment variables:\n${formatted}\n`);
|
|
25
|
+
|
|
26
|
+
// In production, fail hard. In development, warn but continue with defaults.
|
|
27
|
+
if (process.env.NODE_ENV === 'production') {
|
|
28
|
+
throw new Error('Invalid environment variables');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return result.success ? result.data : envSchema.parse({});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const env = validateEnv();
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Default Theme — Claude-inspired warm palette
|
|
3
|
+
* Accent: Terracotta orange
|
|
4
|
+
* Vibe: Earthy, warm, approachable
|
|
5
|
+
*
|
|
6
|
+
* This is the single theme file. Colors use HEX values.
|
|
7
|
+
* Light mode: :root selectors. Dark mode: .dark selectors.
|
|
8
|
+
* To customize: edit the HEX values below.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
:root {
|
|
12
|
+
--radius: 0.625rem;
|
|
13
|
+
/* Warm cream background with terracotta orange accent */
|
|
14
|
+
--background: #f4f3ee;
|
|
15
|
+
--foreground: #1a170f;
|
|
16
|
+
--card: #ffffff;
|
|
17
|
+
--card-foreground: #1a170f;
|
|
18
|
+
--popover: #ffffff;
|
|
19
|
+
--popover-foreground: #1a170f;
|
|
20
|
+
--primary: #c15f3c;
|
|
21
|
+
--primary-foreground: #ffffff;
|
|
22
|
+
--secondary: #ebeae3;
|
|
23
|
+
--secondary-foreground: #302e25;
|
|
24
|
+
--muted: #ebeae3;
|
|
25
|
+
--muted-foreground: #b1ada1;
|
|
26
|
+
--accent: #ebeae3;
|
|
27
|
+
--accent-foreground: #302e25;
|
|
28
|
+
--destructive: #e7000b;
|
|
29
|
+
--border: #deddd4;
|
|
30
|
+
--input: #deddd4;
|
|
31
|
+
--ring: #c15f3c;
|
|
32
|
+
--success: #008339;
|
|
33
|
+
--success-foreground: #ffffff;
|
|
34
|
+
--warning: #e99b2a;
|
|
35
|
+
--warning-foreground: #242119;
|
|
36
|
+
--info: #0079bf;
|
|
37
|
+
--info-foreground: #ffffff;
|
|
38
|
+
--chart-1: #c15f3c;
|
|
39
|
+
--chart-2: #009689;
|
|
40
|
+
--chart-3: #104e64;
|
|
41
|
+
--chart-4: #ebc065;
|
|
42
|
+
--chart-5: #b1ada1;
|
|
43
|
+
--sidebar: #f0efe9;
|
|
44
|
+
--sidebar-foreground: #1a170f;
|
|
45
|
+
--sidebar-primary: #302e25;
|
|
46
|
+
--sidebar-primary-foreground: #f4f3ee;
|
|
47
|
+
--sidebar-accent: #ebeae3;
|
|
48
|
+
--sidebar-accent-foreground: #302e25;
|
|
49
|
+
--sidebar-border: #deddd4;
|
|
50
|
+
--sidebar-ring: #c15f3c;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.dark {
|
|
54
|
+
/* Dark mode: inverted with same warm undertone */
|
|
55
|
+
--background: #15130d;
|
|
56
|
+
--foreground: #e9e8e3;
|
|
57
|
+
--card: #201e18;
|
|
58
|
+
--card-foreground: #e9e8e3;
|
|
59
|
+
--popover: #201e18;
|
|
60
|
+
--popover-foreground: #e9e8e3;
|
|
61
|
+
--primary: #d6724f;
|
|
62
|
+
--primary-foreground: #15130d;
|
|
63
|
+
--secondary: #2b2922;
|
|
64
|
+
--secondary-foreground: #e9e8e3;
|
|
65
|
+
--muted: #2b2922;
|
|
66
|
+
--muted-foreground: #928f85;
|
|
67
|
+
--accent: #2b2922;
|
|
68
|
+
--accent-foreground: #e9e8e3;
|
|
69
|
+
--destructive: #ff6467;
|
|
70
|
+
--border: rgba(255, 255, 250, 0.12);
|
|
71
|
+
--input: rgba(255, 255, 250, 0.15);
|
|
72
|
+
--ring: #d6724f;
|
|
73
|
+
--success: #009c50;
|
|
74
|
+
--success-foreground: #ffffff;
|
|
75
|
+
--warning: #faab3f;
|
|
76
|
+
--warning-foreground: #242119;
|
|
77
|
+
--info: #0099e0;
|
|
78
|
+
--info-foreground: #ffffff;
|
|
79
|
+
--chart-1: #d6724f;
|
|
80
|
+
--chart-2: #00bc7d;
|
|
81
|
+
--chart-3: #e5a658;
|
|
82
|
+
--chart-4: #a066df;
|
|
83
|
+
--chart-5: #e25969;
|
|
84
|
+
--sidebar: #201e18;
|
|
85
|
+
--sidebar-foreground: #e9e8e3;
|
|
86
|
+
--sidebar-primary: #d6724f;
|
|
87
|
+
--sidebar-primary-foreground: #e9e8e3;
|
|
88
|
+
--sidebar-accent: #2b2922;
|
|
89
|
+
--sidebar-accent-foreground: #e9e8e3;
|
|
90
|
+
--sidebar-border: rgba(255, 255, 250, 0.12);
|
|
91
|
+
--sidebar-ring: #d6724f;
|
|
92
|
+
}
|