fix: Actualizar con branding de GLM y restringir acceso con Supabase.
This commit is contained in:
+3
-7
@@ -2,6 +2,7 @@ import React, { Component, ReactNode } from "react";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import { AuthProvider, useAuth } from "@/context/AuthContext";
|
||||
import { LoginScreen } from "@/components/LoginScreen";
|
||||
import { GLMLogo } from "@/components/GLMLogo";
|
||||
import BoardPage from "./pages/BoardPage";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
|
||||
@@ -62,13 +63,8 @@ function AuthGate() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-primary flex items-center justify-center animate-pulse">
|
||||
<svg className="w-4 h-4 text-primary-foreground" fill="none" viewBox="0 0 24 24">
|
||||
<rect x="3" y="3" width="7" height="7" rx="1" fill="currentColor" />
|
||||
<rect x="14" y="3" width="7" height="7" rx="1" fill="currentColor" opacity="0.5" />
|
||||
<rect x="3" y="14" width="7" height="7" rx="1" fill="currentColor" opacity="0.5" />
|
||||
<rect x="14" y="14" width="7" height="7" rx="1" fill="currentColor" />
|
||||
</svg>
|
||||
<div className="animate-pulse">
|
||||
<GLMLogo showWordmark={false} />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">Cargando tablero…</p>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type GLMLogoProps = {
|
||||
size?: "sm" | "lg";
|
||||
showWordmark?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function GLMLogo({ size = "sm", className }: GLMLogoProps) {
|
||||
const imageSize = size === "lg" ? "h-auto w-[168px]" : "h-auto w-[104px]";
|
||||
const logoNudge = size === "lg" ? "-translate-x-1" : "";
|
||||
|
||||
return (
|
||||
<div className={cn("inline-flex items-center", className)} aria-label="GLM">
|
||||
<img
|
||||
src={`${import.meta.env.BASE_URL}glm-logo.png`}
|
||||
alt="GLM"
|
||||
className={cn(imageSize, logoNudge)}
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LayoutGrid } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { GLMLogo } from "@/components/GLMLogo";
|
||||
|
||||
export function LoginScreen() {
|
||||
const { loginWithGoogle, error, loading } = useAuth();
|
||||
@@ -9,13 +9,9 @@ export function LoginScreen() {
|
||||
<div className="min-h-screen bg-background flex items-center justify-center px-4">
|
||||
<div className="w-full max-w-sm">
|
||||
{/* Logo / marca */}
|
||||
<div className="flex flex-col items-center mb-8 gap-3">
|
||||
<div className="w-14 h-14 rounded-2xl bg-primary flex items-center justify-center shadow-lg">
|
||||
<LayoutGrid className="w-7 h-7 text-primary-foreground" />
|
||||
</div>
|
||||
<div className="text-center leading-tight">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Tablero CDC</h1>
|
||||
</div>
|
||||
<div className="flex flex-col items-center mb-8 gap-5">
|
||||
<GLMLogo size="lg" />
|
||||
<h1 className="text-2xl font-bold tracking-tight text-[#4F758B]">Tablero CDC</h1>
|
||||
</div>
|
||||
|
||||
{/* Card */}
|
||||
@@ -43,7 +39,7 @@ export function LoginScreen() {
|
||||
</div>
|
||||
|
||||
<p className="text-center text-xs text-muted-foreground mt-6">
|
||||
Solo acceso para usuarios <span className="font-medium">@gomezleemarketing.com</span>
|
||||
Acceso restringido a usuarios autorizados
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -63,11 +63,7 @@ export function ProjectPricingPanel({ items, onChange, loading, error }: Props)
|
||||
const [manualName, setManualName] = useState<string>("");
|
||||
const [manualDescription, setManualDescription] = useState<string>("");
|
||||
const [manualAmount, setManualAmount] = useState<string>("");
|
||||
const {
|
||||
catalog: tariffCatalog,
|
||||
loading: tariffLoading,
|
||||
error: tariffError,
|
||||
} = useTariffCatalog();
|
||||
const { catalog: tariffCatalog, loading: tariffLoading, error: tariffError } = useTariffCatalog();
|
||||
|
||||
const sectionOptions = TARIFF_SECTIONS.map((option) => option.label);
|
||||
const selectedSectionLabel = TARIFF_SECTIONS.find((option) => option.id === section)?.label || "";
|
||||
@@ -237,7 +233,6 @@ export function ProjectPricingPanel({ items, onChange, loading, error }: Props)
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{(loading || tariffLoading) && (
|
||||
<div className="mb-3 rounded-xl border border-border bg-card px-3 py-2 text-xs text-muted-foreground">
|
||||
{loading ? "Cargando costos guardados…" : "Cargando tarifario…"}
|
||||
|
||||
+36
-26
@@ -9,19 +9,7 @@ import React, {
|
||||
} from "react";
|
||||
import type { User as SupabaseUser } from "@supabase/supabase-js";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
|
||||
const ALLOWED_DOMAIN = "gomezleemarketing.com";
|
||||
const GERARDO_EMAIL = "gmarrero@gomezleemarketing.com";
|
||||
const JOSE_LEOPOLDO_EMAIL = "jgomez@gomezleemarketing.com";
|
||||
const ISAAC_EMAIL = "iaracena@gomezleemarketing.com";
|
||||
const INTERNAL_PRICING_ALLOWED_EMAILS = [GERARDO_EMAIL, ISAAC_EMAIL];
|
||||
const PRICING_SUMMARY_CONTROLLER_EMAILS = [GERARDO_EMAIL, JOSE_LEOPOLDO_EMAIL, ISAAC_EMAIL];
|
||||
const DELETE_ALLOWED_EMAILS = [GERARDO_EMAIL, "iaracena@gomezleemarketing.com"];
|
||||
|
||||
function isAllowedEmail(email: string | null | undefined): boolean {
|
||||
if (!email) return false;
|
||||
return email.toLowerCase().endsWith(`@${ALLOWED_DOMAIN}`);
|
||||
}
|
||||
import { getActiveTableroCdcAccess, type TableroCdcUserAccess } from "@/lib/accessControl";
|
||||
|
||||
function getRedirectTo(): string {
|
||||
if (typeof window === "undefined") return "/tablero-cdc/";
|
||||
@@ -75,23 +63,44 @@ const AuthContext = createContext<AuthContextValue | null>(null);
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<AuthUser | null>(null);
|
||||
const [access, setAccess] = useState<TableroCdcUserAccess | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const applyUser = useCallback(async (supabaseUser: SupabaseUser | null) => {
|
||||
if (!supabaseUser) {
|
||||
setUser(null);
|
||||
setAccess(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAllowedEmail(supabaseUser.email)) {
|
||||
let nextAccess: TableroCdcUserAccess | null = null;
|
||||
|
||||
try {
|
||||
nextAccess = await getActiveTableroCdcAccess(supabaseUser.email);
|
||||
} catch (accessError) {
|
||||
console.error("Error al validar acceso al Tablero CDC:", accessError);
|
||||
await supabase.auth.signOut();
|
||||
setUser(null);
|
||||
setError("Solo se permite acceso con correos corporativos de GomezLee Marketing.");
|
||||
setAccess(null);
|
||||
setError(
|
||||
"No se pudo validar tu acceso al Tablero CDC. Verifica que el SQL de accesos esté ejecutado o contacta a IT.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!nextAccess) {
|
||||
await supabase.auth.signOut();
|
||||
setUser(null);
|
||||
setAccess(null);
|
||||
setError(
|
||||
"Tu correo no está autorizado para acceder al Tablero CDC. Solicita acceso a IT o al equipo CDC.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setUser(normalizeSupabaseUser(supabaseUser));
|
||||
setAccess(nextAccess);
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
@@ -123,8 +132,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
});
|
||||
|
||||
const { data: listener } = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
void applyUser(session?.user ?? null);
|
||||
setLoading(false);
|
||||
setLoading(true);
|
||||
void applyUser(session?.user ?? null).finally(() => {
|
||||
if (mounted) setLoading(false);
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
@@ -142,7 +153,6 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
redirectTo: getRedirectTo(),
|
||||
queryParams: {
|
||||
prompt: "select_account",
|
||||
hd: ALLOWED_DOMAIN,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -163,22 +173,22 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
|
||||
setUser(null);
|
||||
setAccess(null);
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
const normalizedEmail = user?.email?.toLowerCase() || "";
|
||||
const isGerardo = useMemo(() => normalizedEmail === GERARDO_EMAIL, [normalizedEmail]);
|
||||
const isGerardo = useMemo(() => access?.role === "director_creativo", [access?.role]);
|
||||
const canManageInternalPricing = useMemo(
|
||||
() => INTERNAL_PRICING_ALLOWED_EMAILS.includes(normalizedEmail),
|
||||
[normalizedEmail],
|
||||
() => access?.canManageInternalPricing === true,
|
||||
[access?.canManageInternalPricing],
|
||||
);
|
||||
const canDeleteProjects = useMemo(
|
||||
() => DELETE_ALLOWED_EMAILS.includes(normalizedEmail),
|
||||
[normalizedEmail],
|
||||
() => access?.canDeleteProjects === true,
|
||||
[access?.canDeleteProjects],
|
||||
);
|
||||
const canControlPricingSummary = useMemo(
|
||||
() => PRICING_SUMMARY_CONTROLLER_EMAILS.includes(normalizedEmail),
|
||||
[normalizedEmail],
|
||||
() => access?.canControlPricingSummary === true,
|
||||
[access?.canControlPricingSummary],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { supabase } from "@/lib/supabase";
|
||||
|
||||
export type TableroCdcUserAccess = {
|
||||
email: string;
|
||||
fullName: string | null;
|
||||
role: string;
|
||||
isActive: boolean;
|
||||
canDeleteProjects: boolean;
|
||||
canManageInternalPricing: boolean;
|
||||
canControlPricingSummary: boolean;
|
||||
};
|
||||
|
||||
type AccessRow = {
|
||||
email?: string | null;
|
||||
full_name?: string | null;
|
||||
role?: string | null;
|
||||
is_active?: boolean | null;
|
||||
can_delete_projects?: boolean | null;
|
||||
can_manage_internal_pricing?: boolean | null;
|
||||
can_control_pricing_summary?: boolean | null;
|
||||
};
|
||||
|
||||
function normalizeEmail(email: string | null | undefined): string {
|
||||
return String(email ?? "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function normalizeAccessRow(row: AccessRow | null | undefined): TableroCdcUserAccess | null {
|
||||
if (!row?.email || row.is_active !== true) return null;
|
||||
|
||||
return {
|
||||
email: normalizeEmail(row.email),
|
||||
fullName: row.full_name ?? null,
|
||||
role: String(row.role || "user").trim() || "user",
|
||||
isActive: true,
|
||||
canDeleteProjects: row.can_delete_projects === true,
|
||||
canManageInternalPricing: row.can_manage_internal_pricing === true,
|
||||
canControlPricingSummary: row.can_control_pricing_summary === true,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getActiveTableroCdcAccess(
|
||||
email: string | null | undefined,
|
||||
): Promise<TableroCdcUserAccess | null> {
|
||||
const normalizedEmail = normalizeEmail(email);
|
||||
|
||||
if (!normalizedEmail) return null;
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("tablero_cdc_allowed_users")
|
||||
.select(
|
||||
"email, full_name, role, is_active, can_delete_projects, can_manage_internal_pricing, can_control_pricing_summary",
|
||||
)
|
||||
.eq("email", normalizedEmail)
|
||||
.eq("is_active", true)
|
||||
.maybeSingle();
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return normalizeAccessRow(data as AccessRow | null);
|
||||
}
|
||||
|
||||
export async function getCurrentTableroCdcAccess(): Promise<TableroCdcUserAccess> {
|
||||
const { data, error } = await supabase.auth.getUser();
|
||||
|
||||
if (error || !data.user) {
|
||||
throw new Error("No hay una sesión activa de Supabase.");
|
||||
}
|
||||
|
||||
const access = await getActiveTableroCdcAccess(data.user.email);
|
||||
|
||||
if (!access) {
|
||||
throw new Error("Tu correo no está autorizado para acceder al Tablero CDC.");
|
||||
}
|
||||
|
||||
return access;
|
||||
}
|
||||
@@ -53,9 +53,7 @@ export function usePricingSummaryVisibility(enabled = true) {
|
||||
} catch (err) {
|
||||
console.error("No se pudo leer la visibilidad del total tarifado:", err);
|
||||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: "No se pudo leer la visibilidad del total tarifado.",
|
||||
err instanceof Error ? err.message : "No se pudo leer la visibilidad del total tarifado.",
|
||||
);
|
||||
return false;
|
||||
} finally {
|
||||
@@ -63,29 +61,26 @@ export function usePricingSummaryVisibility(enabled = true) {
|
||||
}
|
||||
}, [enabled]);
|
||||
|
||||
const setPublicVisibility = useCallback(
|
||||
async (nextIsPublic: boolean) => {
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
const setPublicVisibility = useCallback(async (nextIsPublic: boolean) => {
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await savePricingSummaryPublicVisibility(nextIsPublic);
|
||||
setIsPublic(nextIsPublic);
|
||||
return nextIsPublic;
|
||||
} catch (err) {
|
||||
console.error("No se pudo actualizar la visibilidad del total tarifado:", err);
|
||||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: "No se pudo actualizar la visibilidad del total tarifado.",
|
||||
);
|
||||
throw err;
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
try {
|
||||
await savePricingSummaryPublicVisibility(nextIsPublic);
|
||||
setIsPublic(nextIsPublic);
|
||||
return nextIsPublic;
|
||||
} catch (err) {
|
||||
console.error("No se pudo actualizar la visibilidad del total tarifado:", err);
|
||||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: "No se pudo actualizar la visibilidad del total tarifado.",
|
||||
);
|
||||
throw err;
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
|
||||
+7
-9
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { getCurrentTableroCdcAccess } from "@/lib/accessControl";
|
||||
import type { ColorId, Status } from "@/data/lists";
|
||||
|
||||
export interface Project {
|
||||
@@ -178,9 +179,6 @@ type DirectProjectSummaryRow = {
|
||||
|
||||
const DEFAULT_COLOR: ColorId = "blue";
|
||||
const OPEN_STATUS = "Activo";
|
||||
const GERARDO_EMAIL = "gmarrero@gomezleemarketing.com";
|
||||
const INTERNAL_PRICING_ALLOWED_EMAILS = [GERARDO_EMAIL, "iaracena@gomezleemarketing.com"];
|
||||
const DELETE_ALLOWED_EMAILS = [GERARDO_EMAIL, "iaracena@gomezleemarketing.com"];
|
||||
const DEFAULT_PROJECT_PARAMS: ProjectQueryParams = {
|
||||
tab: "todos",
|
||||
search: "",
|
||||
@@ -478,18 +476,18 @@ async function getCurrentUserId(): Promise<string> {
|
||||
}
|
||||
|
||||
async function currentUserIsGerardo(): Promise<boolean> {
|
||||
const user = await getCurrentUser();
|
||||
return user.email?.toLowerCase() === GERARDO_EMAIL;
|
||||
const access = await getCurrentTableroCdcAccess();
|
||||
return access.role === "director_creativo";
|
||||
}
|
||||
|
||||
async function currentUserCanManageInternalPricing(): Promise<boolean> {
|
||||
const user = await getCurrentUser();
|
||||
return INTERNAL_PRICING_ALLOWED_EMAILS.includes(user.email?.toLowerCase() || "");
|
||||
const access = await getCurrentTableroCdcAccess();
|
||||
return access.canManageInternalPricing;
|
||||
}
|
||||
|
||||
async function currentUserCanDeleteProjects(): Promise<boolean> {
|
||||
const user = await getCurrentUser();
|
||||
return DELETE_ALLOWED_EMAILS.includes(user.email?.toLowerCase() || "");
|
||||
const access = await getCurrentTableroCdcAccess();
|
||||
return access.canDeleteProjects;
|
||||
}
|
||||
|
||||
function toProjectRow(project: Omit<Project, "id" | "createdAt"> | Partial<Project>) {
|
||||
|
||||
@@ -33,6 +33,7 @@ import { usePricingSummaryVisibility } from "@/lib/pricingSummaryVisibility";
|
||||
import { useAppLists } from "@/lib/appLists";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { dedupeOptions } from "@/lib/optionUtils";
|
||||
import { GLMLogo } from "@/components/GLMLogo";
|
||||
|
||||
/** Extract up-to-2-letter initials from a display name or email */
|
||||
const PROJECTS_PER_PAGE = 24;
|
||||
@@ -221,14 +222,10 @@ export default function BoardPage() {
|
||||
<header className="sticky top-0 z-30 bg-background/85 backdrop-blur-md border-b border-border">
|
||||
<div className="max-w-[1400px] mx-auto px-6 py-3.5 flex items-center gap-4">
|
||||
{/* Brand */}
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="w-9 h-9 rounded-xl bg-primary flex items-center justify-center">
|
||||
<LayoutGrid className="w-4 h-4 text-primary-foreground" />
|
||||
</div>
|
||||
<div className="leading-tight">
|
||||
<h1 className="text-[15px] font-semibold tracking-tight">Tablero CDC</h1>
|
||||
<p className="text-[11px] text-muted-foreground">Flujo de Proyectos</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<GLMLogo size="sm" />
|
||||
<div className="h-7 w-px bg-border" />
|
||||
<h1 className="text-[15px] font-semibold tracking-tight text-[#4F758B]">Tablero CDC</h1>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
|
||||
+40
-31
@@ -64,48 +64,57 @@
|
||||
:root {
|
||||
--radius: 0.875rem;
|
||||
|
||||
/* Google-like clean palette + soft purple accent */
|
||||
--background: oklch(0.99 0.003 280);
|
||||
--foreground: oklch(0.25 0.02 280);
|
||||
/* GLM palette — soft, clean and low-noise for the existing CDC UI */
|
||||
--glm-blue: #4f758b;
|
||||
--glm-blue-medium: #5b7f95;
|
||||
--glm-steel: #6b8fa3;
|
||||
--glm-teal: #2c6e6f;
|
||||
--glm-green: #6cc24a;
|
||||
--glm-green-soft: #eef6e8;
|
||||
|
||||
--background: oklch(0.99 0.004 131);
|
||||
--foreground: oklch(0.4091 0 89.9);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.25 0.02 280);
|
||||
--card-foreground: oklch(0.4091 0 89.9);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.25 0.02 280);
|
||||
--popover-foreground: oklch(0.4091 0 89.9);
|
||||
|
||||
--primary: oklch(0.55 0.18 290);
|
||||
--primary-foreground: oklch(0.99 0.005 280);
|
||||
--primary: oklch(0.5427 0.055 234.6);
|
||||
--primary-foreground: oklch(1 0 89.9);
|
||||
|
||||
--secondary: oklch(0.97 0.005 280);
|
||||
--secondary-foreground: oklch(0.3 0.02 280);
|
||||
--muted: oklch(0.965 0.004 280);
|
||||
--muted-foreground: oklch(0.5 0.015 280);
|
||||
--accent: oklch(0.96 0.02 290);
|
||||
--accent-foreground: oklch(0.35 0.05 290);
|
||||
--secondary: oklch(0.9702 0 89.9);
|
||||
--secondary-foreground: oklch(0.4091 0 89.9);
|
||||
--muted: oklch(0.9702 0 89.9);
|
||||
--muted-foreground: oklch(0.6304 0.0501 232.9);
|
||||
--accent: oklch(0.9633 0.0203 131.5);
|
||||
--accent-foreground: oklch(0.4968 0.0663 196.6);
|
||||
|
||||
--destructive: oklch(0.6 0.22 27);
|
||||
--destructive-foreground: oklch(0.99 0 0);
|
||||
|
||||
--border: oklch(0.925 0.005 280);
|
||||
--input: oklch(0.93 0.005 280);
|
||||
--ring: oklch(0.65 0.15 290);
|
||||
--border: oklch(0.9 0.01 235);
|
||||
--input: oklch(0.91 0.01 235);
|
||||
--ring: oklch(0.7344 0.1772 137.9);
|
||||
|
||||
--chart-1: oklch(0.65 0.18 250);
|
||||
--chart-2: oklch(0.65 0.18 150);
|
||||
--chart-3: oklch(0.7 0.16 80);
|
||||
--chart-4: oklch(0.6 0.2 20);
|
||||
--chart-5: oklch(0.55 0.18 290);
|
||||
--chart-1: oklch(0.5427 0.055 234.6);
|
||||
--chart-2: oklch(0.7344 0.1772 137.9);
|
||||
--chart-3: oklch(0.5778 0.0529 235.7);
|
||||
--chart-4: oklch(0.7013 0.1996 43.7);
|
||||
--chart-5: oklch(0.787 0.1232 161.2);
|
||||
|
||||
--sidebar: oklch(0.985 0.003 280);
|
||||
--sidebar-foreground: oklch(0.25 0.02 280);
|
||||
--sidebar-primary: oklch(0.55 0.18 290);
|
||||
--sidebar-primary-foreground: oklch(0.99 0 0);
|
||||
--sidebar-accent: oklch(0.96 0.02 290);
|
||||
--sidebar-accent-foreground: oklch(0.35 0.05 290);
|
||||
--sidebar-border: oklch(0.925 0.005 280);
|
||||
--sidebar-ring: oklch(0.65 0.15 290);
|
||||
--sidebar: oklch(0.985 0.004 131);
|
||||
--sidebar-foreground: oklch(0.4091 0 89.9);
|
||||
--sidebar-primary: oklch(0.5427 0.055 234.6);
|
||||
--sidebar-primary-foreground: oklch(1 0 89.9);
|
||||
--sidebar-accent: oklch(0.9633 0.0203 131.5);
|
||||
--sidebar-accent-foreground: oklch(0.4968 0.0663 196.6);
|
||||
--sidebar-border: oklch(0.9 0.01 235);
|
||||
--sidebar-ring: oklch(0.7344 0.1772 137.9);
|
||||
|
||||
--shadow-soft: 0 1px 2px oklch(0.5 0.02 280 / 0.06), 0 4px 12px oklch(0.5 0.02 280 / 0.05);
|
||||
--shadow-hover: 0 4px 8px oklch(0.5 0.02 280 / 0.08), 0 12px 28px oklch(0.5 0.02 280 / 0.10);
|
||||
--shadow-soft:
|
||||
0 1px 2px oklch(0.5427 0.055 234.6 / 0.06), 0 4px 14px oklch(0.5427 0.055 234.6 / 0.06);
|
||||
--shadow-hover:
|
||||
0 4px 10px oklch(0.5427 0.055 234.6 / 0.1), 0 14px 30px oklch(0.5427 0.055 234.6 / 0.12);
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
||||
Reference in New Issue
Block a user