Compare commits

...

6 Commits

30 changed files with 378 additions and 210 deletions

View File

@@ -25,7 +25,7 @@ export default async function AccountPage() {
<div className="grid gap-6 md:grid-cols-2">
<UserProfileCard profile={{ ...profile, stats }} />
<ChangePasswordForm />
<ChangePasswordForm username={profile.email} />
</div>
</div>
</div>

View File

@@ -9,7 +9,11 @@ import { useToast } from "@/components/ui/use-toast";
import { Lock } from "lucide-react";
import { changePassword } from "@/app/actions/password";
export function ChangePasswordForm() {
interface ChangePasswordFormProps {
username?: string;
}
export function ChangePasswordForm({ username }: ChangePasswordFormProps) {
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
@@ -77,13 +81,26 @@ export function ChangePasswordForm() {
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<Input
type="text"
name="username"
autoComplete="username"
value={username || ""}
readOnly
tabIndex={-1}
aria-hidden="true"
className="sr-only"
/>
<div className="space-y-2">
<Label htmlFor="currentPassword">Mot de passe actuel</Label>
<div className="relative">
<Lock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
id="currentPassword"
name="currentPassword"
type="password"
autoComplete="current-password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
className="pl-9"
@@ -99,7 +116,9 @@ export function ChangePasswordForm() {
<Lock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
id="newPassword"
name="newPassword"
type="password"
autoComplete="new-password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="pl-9"
@@ -115,7 +134,9 @@ export function ChangePasswordForm() {
<Lock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
id="confirmPassword"
name="confirmPassword"
type="password"
autoComplete="new-password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="pl-9"

View File

@@ -3,7 +3,9 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
import { signIn } from "next-auth/react";
import { ErrorMessage } from "@/components/ui/ErrorMessage";
import { useTranslate } from "@/hooks/useTranslate";
import type { AppErrorType } from "@/types/global";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
@@ -16,9 +18,17 @@ interface LoginFormProps {
export function LoginForm({ from }: LoginFormProps) {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [error, setError] = useState<AppErrorType | null>(null);
const { t } = useTranslate();
const getSafeRedirectPath = (path?: string) => {
if (!path || !path.startsWith("/") || path.startsWith("//")) {
return "/";
}
return path;
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setIsLoading(true);
@@ -37,14 +47,24 @@ export function LoginForm({ from }: LoginFormProps) {
redirect: false,
});
if (result?.error) {
setError("Email ou mot de passe incorrect");
} else {
router.push(from || "/");
router.refresh();
if (!result || result.error || !result.ok) {
setError({
code: "AUTH_INVALID_CREDENTIALS",
name: "Login failed",
message: "Email ou mot de passe incorrect",
});
return;
}
} catch (_error) {
setError("Une erreur est survenue lors de la connexion : " + _error);
const redirectPath = getSafeRedirectPath(from);
window.location.assign(redirectPath);
router.refresh();
} catch {
setError({
code: "AUTH_FETCH_ERROR",
name: "Login error",
message: "Une erreur est survenue lors de la connexion",
});
} finally {
setIsLoading(false);
}
@@ -80,7 +100,7 @@ export function LoginForm({ from }: LoginFormProps) {
{t("login.form.remember")}
</Label>
</div>
{error && <div className="text-sm text-destructive">{error}</div>}
{error && <ErrorMessage errorCode={error.code} variant="form" />}
<Button type="submit" disabled={isLoading} className="w-full">
{isLoading ? t("login.form.submit.loading.login") : t("login.form.submit.login")}
</Button>

View File

@@ -15,12 +15,20 @@ interface RegisterFormProps {
from?: string;
}
export function RegisterForm({ from: _from }: RegisterFormProps) {
export function RegisterForm({ from }: RegisterFormProps) {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<AppErrorType | null>(null);
const { t } = useTranslate();
const getSafeRedirectPath = (path?: string) => {
if (!path || !path.startsWith("/") || path.startsWith("//")) {
return "/";
}
return path;
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setIsLoading(true);
@@ -61,14 +69,15 @@ export function RegisterForm({ from: _from }: RegisterFormProps) {
redirect: false,
});
if (signInResult?.error) {
if (!signInResult || signInResult.error || !signInResult.ok) {
setError({
code: "AUTH_INVALID_CREDENTIALS",
name: "Login failed",
message: "Inscription réussie mais erreur lors de la connexion automatique",
});
} else {
router.push("/");
const redirectPath = getSafeRedirectPath(from);
window.location.assign(redirectPath);
router.refresh();
}
} catch {

View File

@@ -2,18 +2,22 @@ import { useDisplayPreferences } from "@/hooks/useDisplayPreferences";
import { useTranslate } from "@/hooks/useTranslate";
import { LayoutGrid, LayoutTemplate } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface CompactModeButtonProps {
onToggle?: (isCompact: boolean) => void;
isCompact?: boolean;
className?: string;
}
function CompactModeButtonBase({
isCompact,
onToggle,
className,
}: {
isCompact: boolean;
onToggle: (isCompact: boolean) => Promise<void> | void;
className?: string;
}) {
const { t } = useTranslate();
@@ -31,15 +35,21 @@ function CompactModeButtonBase({
size="sm"
onClick={handleClick}
title={label}
className="whitespace-nowrap"
className={cn(
"h-9 rounded-full border border-border/60 bg-background/40 px-3 text-xs font-medium backdrop-blur-sm hover:bg-accent/40 sm:text-sm",
className
)}
>
<Icon className="h-4 w-4" />
<span className="hidden sm:inline ml-2">{label}</span>
<span className="ml-2 hidden whitespace-nowrap min-[420px]:inline">{label}</span>
</Button>
);
}
function CompactModeButtonUncontrolled({ onToggle }: Pick<CompactModeButtonProps, "onToggle">) {
function CompactModeButtonUncontrolled({
onToggle,
className,
}: Pick<CompactModeButtonProps, "onToggle" | "className">) {
const { isCompact, handleCompactToggle } = useDisplayPreferences();
const handleToggle = async (nextCompactMode: boolean) => {
@@ -47,15 +57,17 @@ function CompactModeButtonUncontrolled({ onToggle }: Pick<CompactModeButtonProps
onToggle?.(nextCompactMode);
};
return <CompactModeButtonBase isCompact={isCompact} onToggle={handleToggle} />;
return (
<CompactModeButtonBase isCompact={isCompact} onToggle={handleToggle} className={className} />
);
}
export function CompactModeButton({ onToggle, isCompact }: CompactModeButtonProps) {
export function CompactModeButton({ onToggle, isCompact, className }: CompactModeButtonProps) {
const isControlled = typeof isCompact === "boolean" && typeof onToggle === "function";
if (isControlled) {
return <CompactModeButtonBase isCompact={isCompact} onToggle={onToggle} />;
return <CompactModeButtonBase isCompact={isCompact} onToggle={onToggle} className={className} />;
}
return <CompactModeButtonUncontrolled onToggle={onToggle} />;
return <CompactModeButtonUncontrolled onToggle={onToggle} className={className} />;
}

View File

@@ -7,18 +7,22 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
interface PageSizeSelectProps {
onSizeChange?: (size: number) => void;
pageSize?: number;
className?: string;
}
function PageSizeSelectBase({
value,
onChange,
className,
}: {
value: number;
onChange: (size: number) => Promise<void> | void;
className?: string;
}) {
const handleChange = async (rawValue: string) => {
const size = parseInt(rawValue);
@@ -27,7 +31,12 @@ function PageSizeSelectBase({
return (
<Select value={value.toString()} onValueChange={handleChange}>
<SelectTrigger className="w-[80px]">
<SelectTrigger
className={cn(
"h-9 w-[96px] rounded-full border border-border/60 bg-background/40 text-xs font-medium backdrop-blur-sm sm:text-sm",
className
)}
>
<LayoutList className="h-4 w-4" />
<SelectValue className="ml-2" />
</SelectTrigger>
@@ -40,7 +49,10 @@ function PageSizeSelectBase({
);
}
function PageSizeSelectUncontrolled({ onSizeChange }: Pick<PageSizeSelectProps, "onSizeChange">) {
function PageSizeSelectUncontrolled({
onSizeChange,
className,
}: Pick<PageSizeSelectProps, "onSizeChange" | "className">) {
const { itemsPerPage, handlePageSizeChange } = useDisplayPreferences();
const onChange = async (size: number) => {
@@ -48,15 +60,15 @@ function PageSizeSelectUncontrolled({ onSizeChange }: Pick<PageSizeSelectProps,
onSizeChange?.(size);
};
return <PageSizeSelectBase value={itemsPerPage} onChange={onChange} />;
return <PageSizeSelectBase value={itemsPerPage} onChange={onChange} className={className} />;
}
export function PageSizeSelect({ onSizeChange, pageSize }: PageSizeSelectProps) {
export function PageSizeSelect({ onSizeChange, pageSize, className }: PageSizeSelectProps) {
const isControlled = typeof pageSize === "number" && typeof onSizeChange === "function";
if (isControlled) {
return <PageSizeSelectBase value={pageSize} onChange={onSizeChange} />;
return <PageSizeSelectBase value={pageSize} onChange={onSizeChange} className={className} />;
}
return <PageSizeSelectUncontrolled onSizeChange={onSizeChange} />;
return <PageSizeSelectUncontrolled onSizeChange={onSizeChange} className={className} />;
}

View File

@@ -3,13 +3,19 @@
import { useTranslate } from "@/hooks/useTranslate";
import { Filter } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface UnreadFilterButtonProps {
showOnlyUnread: boolean;
onToggle: () => void;
className?: string;
}
export function UnreadFilterButton({ showOnlyUnread, onToggle }: UnreadFilterButtonProps) {
export function UnreadFilterButton({
showOnlyUnread,
onToggle,
className,
}: UnreadFilterButtonProps) {
const { t } = useTranslate();
const label = showOnlyUnread ? t("series.filters.showAll") : t("series.filters.unread");
@@ -20,10 +26,16 @@ export function UnreadFilterButton({ showOnlyUnread, onToggle }: UnreadFilterBut
size="sm"
onClick={onToggle}
title={label}
className="whitespace-nowrap"
className={cn(
"h-9 rounded-full border px-3 text-xs font-medium backdrop-blur-sm sm:text-sm",
showOnlyUnread
? "border-primary/40 bg-primary/15 text-primary hover:bg-primary/20"
: "border-border/60 bg-background/40 hover:bg-accent/40",
className
)}
>
<Filter className="h-4 w-4" />
<span className="hidden sm:inline ml-2">{label}</span>
<span className="ml-2 hidden whitespace-nowrap min-[420px]:inline">{label}</span>
</Button>
);
}

View File

@@ -2,18 +2,22 @@ import { useDisplayPreferences } from "@/hooks/useDisplayPreferences";
import { useTranslate } from "@/hooks/useTranslate";
import { LayoutGrid, List } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface ViewModeButtonProps {
onToggle?: (viewMode: "grid" | "list") => void;
viewMode?: "grid" | "list";
className?: string;
}
function ViewModeButtonBase({
viewMode,
onToggle,
className,
}: {
viewMode: "grid" | "list";
onToggle: (viewMode: "grid" | "list") => Promise<void> | void;
className?: string;
}) {
const { t } = useTranslate();
@@ -31,15 +35,21 @@ function ViewModeButtonBase({
size="sm"
onClick={handleClick}
title={label}
className="whitespace-nowrap"
className={cn(
"h-9 rounded-full border border-border/60 bg-background/40 px-3 text-xs font-medium backdrop-blur-sm hover:bg-accent/40 sm:text-sm",
className
)}
>
<Icon className="h-4 w-4" />
<span className="hidden sm:inline ml-2">{label}</span>
<span className="ml-2 hidden whitespace-nowrap min-[420px]:inline">{label}</span>
</Button>
);
}
function ViewModeButtonUncontrolled({ onToggle }: Pick<ViewModeButtonProps, "onToggle">) {
function ViewModeButtonUncontrolled({
onToggle,
className,
}: Pick<ViewModeButtonProps, "onToggle" | "className">) {
const { viewMode, handleViewModeToggle } = useDisplayPreferences();
const handleToggle = async (nextViewMode: "grid" | "list") => {
@@ -47,15 +57,15 @@ function ViewModeButtonUncontrolled({ onToggle }: Pick<ViewModeButtonProps, "onT
onToggle?.(nextViewMode);
};
return <ViewModeButtonBase viewMode={viewMode} onToggle={handleToggle} />;
return <ViewModeButtonBase viewMode={viewMode} onToggle={handleToggle} className={className} />;
}
export function ViewModeButton({ onToggle, viewMode }: ViewModeButtonProps) {
export function ViewModeButton({ onToggle, viewMode, className }: ViewModeButtonProps) {
const isControlled = typeof viewMode === "string" && typeof onToggle === "function";
if (isControlled) {
return <ViewModeButtonBase viewMode={viewMode} onToggle={onToggle} />;
return <ViewModeButtonBase viewMode={viewMode} onToggle={onToggle} className={className} />;
}
return <ViewModeButtonUncontrolled onToggle={onToggle} />;
return <ViewModeButtonUncontrolled onToggle={onToggle} className={className} />;
}

View File

@@ -5,7 +5,6 @@ import { useRouter } from "next/navigation";
import { RefreshButton } from "@/components/library/RefreshButton";
import { PullToRefreshIndicator } from "@/components/common/PullToRefreshIndicator";
import { usePullToRefresh } from "@/hooks/usePullToRefresh";
import { useTranslate } from "@/hooks/useTranslate";
interface HomeClientWrapperProps {
children: ReactNode;
@@ -13,7 +12,6 @@ interface HomeClientWrapperProps {
export function HomeClientWrapper({ children }: HomeClientWrapperProps) {
const router = useRouter();
const { t } = useTranslate();
const [isRefreshing, setIsRefreshing] = useState(false);
const handleRefresh = async () => {
@@ -45,12 +43,16 @@ export function HomeClientWrapper({ children }: HomeClientWrapperProps) {
canRefresh={pullToRefresh.canRefresh}
isHiding={pullToRefresh.isHiding}
/>
<main className="container mx-auto px-4 py-8 space-y-12">
<div className="flex justify-between items-center">
<h1 className="text-3xl font-bold">{t("home.title")}</h1>
<main className="relative isolate overflow-hidden">
<div className="pointer-events-none absolute inset-0 -z-10 bg-[linear-gradient(180deg,hsl(var(--background)/0.99)_0%,hsl(var(--background)/0.95)_28%,hsl(var(--background))_100%)]" />
<div className="pointer-events-none absolute inset-0 -z-10 bg-[linear-gradient(128deg,hsl(var(--primary)/0.14)_0%,transparent_36%),linear-gradient(235deg,hsl(185_82%_54%/0.1)_4%,transparent_34%),linear-gradient(320deg,hsl(332_82%_63%/0.08)_8%,transparent_32%)]" />
<div className="container mx-auto space-y-12 px-4 py-8">
<div className="flex justify-end">
<RefreshButton libraryId="home" refreshLibrary={handleRefresh} />
</div>
{children}
</div>
</main>
</>
);

View File

@@ -29,7 +29,17 @@ const optimizeBookData = (books: KomgaBook[]) => {
export function HomeContent({ data }: HomeContentProps) {
return (
<div className="space-y-12">
<div className="space-y-10 pb-2">
{data.ongoingBooks && data.ongoingBooks.length > 0 && (
<div className="rounded-2xl border border-primary/20 bg-[linear-gradient(145deg,hsl(var(--primary)/0.12),hsl(var(--background)/0.1)_45%)] p-4 sm:p-5">
<MediaRow
titleKey="home.sections.continue_reading"
items={optimizeBookData(data.ongoingBooks)}
iconName="BookOpen"
/>
</div>
)}
{data.ongoing && data.ongoing.length > 0 && (
<MediaRow
titleKey="home.sections.continue_series"
@@ -38,14 +48,6 @@ export function HomeContent({ data }: HomeContentProps) {
/>
)}
{data.ongoingBooks && data.ongoingBooks.length > 0 && (
<MediaRow
titleKey="home.sections.continue_reading"
items={optimizeBookData(data.ongoingBooks)}
iconName="BookOpen"
/>
)}
{data.onDeck && data.onDeck.length > 0 && (
<MediaRow
titleKey="home.sections.up_next"

View File

@@ -64,7 +64,12 @@ export function MediaRow({ titleKey, items, iconName }: MediaRowProps) {
if (!items.length) return null;
return (
<Section title={t(titleKey)} icon={icon}>
<Section
title={t(titleKey)}
icon={icon}
className="space-y-5"
headerClassName="border-b border-border/50 pb-2"
>
<ScrollContainer
showArrows={true}
scrollAmount={400}
@@ -106,7 +111,7 @@ function MediaCard({ item, onClick }: MediaCardProps) {
<Card
onClick={handleClick}
className={cn(
"flex-shrink-0 w-[200px] relative flex flex-col hover:bg-accent hover:text-accent-foreground transition-colors overflow-hidden",
"relative flex w-[188px] flex-shrink-0 flex-col overflow-hidden rounded-xl border border-border/60 bg-card/85 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-card hover:shadow-md sm:w-[200px]",
!isSeries && !isAccessible ? "cursor-not-allowed" : "cursor-pointer"
)}
>
@@ -114,7 +119,7 @@ function MediaCard({ item, onClick }: MediaCardProps) {
{isSeries ? (
<>
<SeriesCover series={item as KomgaSeries} alt={`Couverture de ${title}`} />
<div className="absolute inset-0 bg-black/60 opacity-0 hover:opacity-100 transition-opacity duration-200 flex flex-col justify-end p-3">
<div className="absolute inset-0 flex flex-col justify-end bg-gradient-to-t from-black/75 via-black/30 to-transparent p-3 opacity-0 transition-opacity duration-200 hover:opacity-100">
<h3 className="font-medium text-sm text-white line-clamp-2">{title}</h3>
<p className="text-xs text-white/80 mt-1">
{t("series.books", { count: item.booksCount })}

View File

@@ -176,6 +176,14 @@ export default function ClientLayout({
userIsAdmin={userIsAdmin}
/>
)}
{!isPublicRoute && isSidebarOpen && (
<button
type="button"
aria-label="Fermer la navigation"
className="fixed inset-0 top-[calc(4rem+env(safe-area-inset-top,0px))] z-20 bg-black/35 backdrop-blur-[1px] transition-opacity lg:hidden"
onClick={handleCloseSidebar}
/>
)}
<main className={!isPublicRoute ? "pt-safe" : ""}>{children}</main>
<InstallPWA />
<Toaster />

View File

@@ -33,46 +33,56 @@ export function Header({
};
return (
<header className="sticky top-0 z-50 w-full border-b border-border/40 bg-background/70 backdrop-blur-md supports-[backdrop-filter]:bg-background/50 pt-safe">
<div className="container flex h-14 max-w-screen-2xl items-center">
<header className="sticky top-0 z-50 w-full border-b border-primary/30 bg-background/70 shadow-sm backdrop-blur-xl supports-[backdrop-filter]:bg-background/65 pt-safe relative overflow-hidden">
<div className="pointer-events-none absolute inset-0 bg-[linear-gradient(112deg,hsl(var(--primary)/0.24)_0%,hsl(192_85%_55%/0.2)_30%,transparent_56%),linear-gradient(248deg,hsl(338_82%_62%/0.16)_0%,transparent_46%),repeating-linear-gradient(135deg,hsl(var(--foreground)/0.03)_0_1px,transparent_1px_11px)]" />
<div className="container relative flex h-16 max-w-screen-2xl items-center">
<IconButton
variant="ghost"
size="icon"
icon={Menu}
onClick={onToggleSidebar}
tooltip={t("header.toggleSidebar")}
className="mr-2"
className="mr-2 h-10 w-10 rounded-full"
id="sidebar-toggle"
/>
<div className="mr-4 hidden md:flex">
<a className="mr-6 flex items-center space-x-2" href="/">
<span className="hidden font-bold sm:inline-block">StripStream</span>
<div className="mr-2 flex items-center md:mr-4">
<a className="mr-2 flex items-center md:mr-6" href="/">
<span className="inline-flex bg-gradient-to-r from-primary via-cyan-500 to-fuchsia-500 bg-clip-text text-sm font-bold uppercase tracking-[0.1em] text-transparent sm:hidden">
StripStream
</span>
<span className="hidden sm:inline-flex flex-col leading-none">
<span className="bg-gradient-to-r from-primary via-cyan-500 to-fuchsia-500 bg-clip-text text-lg font-bold tracking-[0.1em] text-transparent">
STRIPSTREAM
</span>
<span className="mt-1 text-[10px] font-medium uppercase tracking-[0.28em] text-foreground/70">
comic reader
</span>
</span>
</a>
</div>
<div className="flex flex-1 items-center justify-between space-x-2 md:justify-end">
<nav className="flex items-center space-x-2">
<div className="ml-auto flex items-center">
<nav className="flex items-center gap-1 rounded-full border border-border/60 bg-background/45 px-1 py-1 shadow-[0_4px_18px_-14px_rgba(0,0,0,0.65)] backdrop-blur-md">
{showRefreshBackground && (
<button
<IconButton
onClick={handleRefreshBackground}
disabled={isRefreshing}
className="px-2 py-1.5 hover:bg-accent hover:text-accent-foreground rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="Rafraîchir l'image de fond"
>
<RefreshCw
className={`h-[1.2rem] w-[1.2rem] ${isRefreshing ? "animate-spin" : ""}`}
variant="ghost"
size="icon"
icon={RefreshCw}
iconClassName={isRefreshing ? "animate-spin" : ""}
className="h-9 w-9 rounded-full"
tooltip="Rafraîchir l'image de fond"
/>
<span className="sr-only">Rafraîchir l&apos;image de fond</span>
</button>
)}
<LanguageSelector />
<button
onClick={toggleTheme}
className="px-2 py-1.5 hover:bg-accent hover:text-accent-foreground rounded-md"
className="rounded-full p-2 transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
aria-label={t("header.toggleTheme")}
>
<div className="relative flex items-center w-5 h-5">
<div className="relative flex h-5 w-5 items-center">
<Sun className="absolute inset-0 h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute inset-0 h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
</div>

View File

@@ -149,17 +149,19 @@ export function Sidebar({
<aside
suppressHydrationWarning
className={cn(
"fixed left-0 top-14 z-30 h-[calc(100vh-3.5rem)] w-64 border-r border-border/40",
"bg-background/70 backdrop-blur-md supports-[backdrop-filter]:bg-background/50",
"fixed left-0 top-[calc(4rem+env(safe-area-inset-top,0px))] z-30 h-[calc(100vh-4rem-env(safe-area-inset-top,0px))] w-72 border-r border-primary/30",
"bg-background/70 shadow-sm backdrop-blur-xl supports-[backdrop-filter]:bg-background/65",
"transition-transform duration-300 ease-in-out flex flex-col",
isOpen ? "translate-x-0" : "-translate-x-full"
)}
id="sidebar"
>
<div className="flex-1 space-y-4 py-4 overflow-y-auto">
<div className="px-3 py-2">
<div className="pointer-events-none absolute inset-0 bg-[linear-gradient(160deg,hsl(var(--primary)/0.12)_0%,hsl(192_85%_55%/0.08)_32%,transparent_58%),linear-gradient(332deg,hsl(338_82%_62%/0.06)_0%,transparent_42%),repeating-linear-gradient(135deg,hsl(var(--foreground)/0.02)_0_1px,transparent_1px_11px)]" />
<div className="relative flex-1 space-y-4 overflow-y-auto px-3 py-4">
<div className="rounded-xl border border-border/50 bg-background/30 p-2">
<div className="space-y-1">
<h2 className="mb-2 px-4 text-lg font-semibold tracking-tight">
<h2 className="mb-2 px-3 text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground">
{t("sidebar.navigation")}
</h2>
{mainNavItems.map((item) => (
@@ -174,10 +176,10 @@ export function Sidebar({
</div>
</div>
<div className="px-3 py-2">
<div className="rounded-xl border border-border/50 bg-background/30 p-2">
<div className="space-y-1">
<div className="mb-2 px-4 flex items-center justify-between">
<h2 className="text-lg font-semibold tracking-tight">
<div className="mb-2 flex items-center justify-between px-3">
<h2 className="text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground">
{t("sidebar.favorites.title")}
</h2>
<span className="text-xs text-muted-foreground">{favorites.length}</span>
@@ -205,10 +207,10 @@ export function Sidebar({
</div>
</div>
<div className="px-3 py-2">
<div className="rounded-xl border border-border/50 bg-background/30 p-2">
<div className="space-y-1">
<div className="mb-2 px-4 flex items-center justify-between">
<h2 className="text-lg font-semibold tracking-tight">
<div className="mb-2 flex items-center justify-between px-3">
<h2 className="text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground">
{t("sidebar.libraries.title")}
</h2>
<IconButton
@@ -244,9 +246,9 @@ export function Sidebar({
</div>
</div>
<div className="px-3 py-2">
<div className="rounded-xl border border-border/50 bg-background/30 p-2">
<div className="space-y-1">
<h2 className="mb-2 px-4 text-lg font-semibold tracking-tight">
<h2 className="mb-2 px-3 text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground">
{t("sidebar.settings.title")}
</h2>
<NavButton
@@ -273,7 +275,7 @@ export function Sidebar({
</div>
</div>
<div className="p-3 border-t border-border/40">
<div className="relative border-t border-border/50 bg-background/30 p-3">
<NavButton
icon={LogOut}
label={t("sidebar.logout")}

View File

@@ -36,14 +36,14 @@ export function LibraryHeader({
const seriesLabel = `${seriesCount} ${seriesCount > 1 ? "series" : "serie"}`;
return (
<div className="relative min-h-[200px] md:h-[200px] w-screen -ml-[calc((100vw-100%)/2)] overflow-hidden">
<div className="relative min-h-[220px] md:h-[220px] w-screen -ml-[calc((100vw-100%)/2)] overflow-hidden border-y border-border/60">
<div className="absolute inset-0">
<div className="absolute inset-0 bg-black/40" />
<div className="absolute inset-0 bg-gradient-to-r from-background/85 via-background/65 to-background/85" />
{background ? (
<SeriesCover
series={background}
alt=""
className="blur-sm scale-105 brightness-50"
className="scale-105 blur-sm brightness-50"
showProgressUi={false}
/>
) : (
@@ -51,9 +51,9 @@ export function LibraryHeader({
)}
</div>
<div className="relative container mx-auto px-4 py-8 h-full">
<div className="flex flex-col md:flex-row gap-6 items-center md:items-start h-full">
<div className="relative w-[120px] h-[120px] rounded-lg overflow-hidden shadow-lg flex-shrink-0">
<div className="relative container mx-auto h-full px-4 py-8">
<div className="flex h-full flex-col items-center gap-6 md:flex-row md:items-start">
<div className="relative h-[120px] w-[120px] flex-shrink-0 overflow-hidden rounded-xl border border-border/60 shadow-lg">
{featured ? (
<div className="relative w-full h-full">
<SeriesCover
@@ -73,10 +73,10 @@ export function LibraryHeader({
)}
</div>
<div className="flex-1 space-y-3 text-center md:text-left">
<h1 className="text-3xl md:text-4xl font-bold text-foreground">{library.name}</h1>
<div className="flex-1 space-y-4 text-center md:text-left">
<h1 className="text-3xl font-bold text-foreground md:text-4xl">{library.name}</h1>
<div className="flex items-center gap-4 justify-center md:justify-start flex-wrap">
<div className="flex flex-wrap items-center justify-center gap-3 rounded-xl border border-border/60 bg-background/45 p-2 backdrop-blur-sm md:justify-start">
<StatusBadge status="unread" icon={Library}>
{seriesLabel}
</StatusBadge>

View File

@@ -171,17 +171,39 @@ export function PaginatedSeriesGrid({
return (
<div className="space-y-8">
<div className="flex flex-col gap-4">
<p className="text-sm text-muted-foreground text-right">{getShowingText()}</p>
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
<div className="w-full">
<SearchInput placeholder={t("series.filters.search")} />
<div className="rounded-2xl border border-border/60 bg-[linear-gradient(140deg,hsl(var(--background)/0.6),hsl(var(--background)/0.38))] p-4 shadow-sm backdrop-blur-sm sm:p-5">
<div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<div className="space-y-1">
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
Explorer
</p>
<h2 className="text-xl font-semibold tracking-tight sm:text-2xl">Séries</h2>
</div>
<p className="text-sm text-muted-foreground">{getShowingText()}</p>
</div>
<div className="space-y-3">
<SearchInput placeholder={t("series.filters.search")} />
<div className="pb-1">
<div className="flex flex-wrap items-center gap-2">
<UnreadFilterButton
showOnlyUnread={showOnlyUnread}
onToggle={handleUnreadFilter}
/>
<ViewModeButton
viewMode={viewMode}
onToggle={handleViewModeToggle}
/>
<CompactModeButton
isCompact={isCompact}
onToggle={handleCompactModeToggle}
/>
<PageSizeSelect
pageSize={effectivePageSize}
onSizeChange={handlePageSizeChange}
/>
</div>
<div className="flex items-center justify-end gap-2">
<PageSizeSelect pageSize={effectivePageSize} onSizeChange={handlePageSizeChange} />
<ViewModeButton viewMode={viewMode} onToggle={handleViewModeToggle} />
<CompactModeButton isCompact={isCompact} onToggle={handleCompactModeToggle} />
<UnreadFilterButton showOnlyUnread={showOnlyUnread} onToggle={handleUnreadFilter} />
</div>
</div>
</div>

View File

@@ -34,12 +34,12 @@ export const SearchInput = ({ placeholder }: SearchInputProps) => {
}, 300);
return (
<div className="relative w-full max-w-md">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<div className="relative w-full rounded-2xl border border-border/60 bg-[linear-gradient(140deg,hsl(var(--background)/0.72),hsl(var(--background)/0.5))] px-1 shadow-sm backdrop-blur-sm transition-colors focus-within:border-primary/40 focus-within:ring-2 focus-within:ring-ring/30 sm:max-w-2xl">
<Search className="absolute left-4 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
type={isPending ? "text" : "search"}
placeholder={placeholder}
className="pl-3"
className="h-12 rounded-xl border-0 bg-transparent pl-10 pr-10 text-sm shadow-none focus-visible:border-0 focus-visible:ring-0 focus-visible:ring-offset-0"
defaultValue={searchParams.get("search") ?? ""}
onChange={(e) => handleSearch(e.target.value)}
aria-label={placeholder}

View File

@@ -36,7 +36,7 @@ const getReadingStatusInfo = (
read: series.booksReadCount,
total: series.booksCount,
}),
className: "bg-blue-500/10 text-blue-500",
className: "bg-primary/15 text-primary",
};
}
@@ -61,7 +61,7 @@ export function SeriesGrid({ series, isCompact = false }: SeriesGridProps) {
return (
<div
className={cn(
"grid gap-4",
"grid gap-4 md:gap-5",
isCompact
? "grid-cols-3 sm:grid-cols-4 lg:grid-cols-6"
: "grid-cols-2 sm:grid-cols-3 lg:grid-cols-5"
@@ -72,7 +72,7 @@ export function SeriesGrid({ series, isCompact = false }: SeriesGridProps) {
key={series.id}
onClick={() => router.push(`/series/${series.id}`)}
className={cn(
"group relative aspect-[2/3] overflow-hidden rounded-lg bg-muted",
"group relative aspect-[2/3] overflow-hidden rounded-xl border border-border/60 bg-card/80 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md",
series.booksCount === series.booksReadCount && "opacity-50",
isCompact && "aspect-[3/4]"
)}
@@ -81,7 +81,7 @@ export function SeriesGrid({ series, isCompact = false }: SeriesGridProps) {
series={series as KomgaSeries}
alt={t("series.coverAlt", { title: series.metadata.title })}
/>
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent p-4 space-y-2 translate-y-full group-hover:translate-y-0 transition-transform duration-200">
<div className="absolute inset-x-0 bottom-0 translate-y-full space-y-2 bg-gradient-to-t from-black/75 via-black/25 to-transparent p-4 transition-transform duration-200 group-hover:translate-y-0">
<h3 className="font-medium text-sm text-white line-clamp-2">{series.metadata.title}</h3>
<div className="flex items-center gap-2">
<span

View File

@@ -44,7 +44,7 @@ const getReadingStatusInfo = (
read: series.booksReadCount,
total: series.booksCount,
}),
className: "bg-blue-500/10 text-blue-500",
className: "bg-primary/15 text-primary",
};
}
@@ -72,7 +72,7 @@ function SeriesListItem({ series, isCompact = false }: SeriesListItemProps) {
return (
<div
className={cn(
"group relative flex gap-3 p-2 rounded-lg border bg-card hover:bg-accent/50 transition-colors cursor-pointer",
"group relative flex cursor-pointer gap-3 rounded-lg border border-border/60 bg-background/35 p-2 transition-colors hover:bg-accent/35",
isCompleted && "opacity-75"
)}
onClick={handleClick}
@@ -128,7 +128,7 @@ function SeriesListItem({ series, isCompact = false }: SeriesListItemProps) {
return (
<div
className={cn(
"group relative flex gap-4 p-4 rounded-lg border bg-card hover:bg-accent/50 transition-colors cursor-pointer",
"group relative flex cursor-pointer gap-4 rounded-xl border border-border/60 bg-background/35 p-4 transition-all duration-200 hover:bg-accent/35 hover:shadow-sm",
isCompleted && "opacity-75"
)}
onClick={handleClick}

View File

@@ -45,7 +45,7 @@ export const ControlButtons = ({
{/* Boutons de contrôle */}
<div
className={cn(
"absolute top-4 left-1/2 -translate-x-1/2 z-30 flex items-center gap-1.5 transition-all duration-300 p-1.5 rounded-full bg-background/70 backdrop-blur-md",
"absolute left-1/2 top-4 z-30 flex -translate-x-1/2 items-center gap-1.5 rounded-full border border-border/60 bg-background/55 p-1.5 shadow-[0_8px_28px_-18px_rgba(0,0,0,0.75)] backdrop-blur-xl transition-all duration-300",
showControls ? "opacity-100" : "opacity-0 pointer-events-none"
)}
onClick={(e) => {
@@ -178,7 +178,7 @@ export const ControlButtons = ({
tooltip={t("reader.controls.previousPage")}
iconClassName="h-8 w-8"
className={cn(
"absolute top-1/2 -translate-y-1/2 rounded-full bg-background/70 backdrop-blur-md hover:bg-background/80 transition-all duration-300 z-20",
"absolute top-1/2 z-20 -translate-y-1/2 rounded-full border border-border/60 bg-background/55 shadow-[0_8px_24px_-16px_rgba(0,0,0,0.75)] backdrop-blur-xl transition-all duration-300 hover:bg-background/70",
direction === "rtl" ? "right-4" : "left-4",
showControls ? "opacity-100" : "opacity-0 pointer-events-none"
)}
@@ -198,7 +198,7 @@ export const ControlButtons = ({
tooltip={t("reader.controls.nextPage")}
iconClassName="h-8 w-8"
className={cn(
"absolute top-1/2 -translate-y-1/2 rounded-full bg-background/70 backdrop-blur-md hover:bg-background/80 transition-all duration-300 z-20",
"absolute top-1/2 z-20 -translate-y-1/2 rounded-full border border-border/60 bg-background/55 shadow-[0_8px_24px_-16px_rgba(0,0,0,0.75)] backdrop-blur-xl transition-all duration-300 hover:bg-background/70",
direction === "rtl" ? "left-4" : "right-4",
showControls ? "opacity-100" : "opacity-0 pointer-events-none"
)}

View File

@@ -55,7 +55,7 @@ export const NavigationBar = ({
return (
<div
className={cn(
"absolute bottom-0 left-0 right-0 bg-background/70 backdrop-blur-md border-t border-border/40 transition-all duration-300 ease-in-out z-30",
"absolute bottom-0 left-0 right-0 z-30 border-t border-border/60 bg-background/60 backdrop-blur-xl transition-all duration-300 ease-in-out",
showThumbnails ? "h-52 opacity-100" : "h-0 opacity-0"
)}
>
@@ -63,7 +63,7 @@ export const NavigationBar = ({
<>
<div
id="thumbnails-container"
className="h-full overflow-x-auto flex items-center gap-2 px-4 scroll-smooth snap-x snap-mandatory"
className="flex h-full snap-x snap-mandatory items-center gap-2 overflow-x-auto px-4 scroll-smooth"
onTouchStart={(e) => e.stopPropagation()}
onTouchMove={(e) => e.stopPropagation()}
onTouchEnd={(e) => e.stopPropagation()}
@@ -90,7 +90,7 @@ export const NavigationBar = ({
</div>
{showControls && (
<div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-full px-4 py-2 rounded-full bg-background/70 backdrop-blur-md text-sm">
<div className="absolute left-1/2 top-0 -translate-x-1/2 -translate-y-full rounded-full border border-border/60 bg-background/60 px-4 py-2 text-sm shadow-[0_8px_24px_-16px_rgba(0,0,0,0.75)] backdrop-blur-xl">
Page {currentPage} / {pages.length}
</div>
)}

View File

@@ -38,8 +38,8 @@ export function PageDisplay({
}, [currentPage, isDoublePage]);
return (
<div className="relative flex-1 flex items-center justify-center overflow-hidden w-full">
<div className="relative w-full h-[calc(100vh-2rem)] flex items-center justify-center gap-1">
<div className="relative flex w-full flex-1 items-center justify-center overflow-hidden">
<div className="relative flex h-[calc(100vh-2.5rem)] w-full items-center justify-center gap-1 px-2 sm:px-4">
{/* Page 1 */}
<div
className={cn(
@@ -69,7 +69,7 @@ export function PageDisplay({
src={imageBlobUrls[currentPage] || getPageUrl(currentPage)}
alt={`Page ${currentPage}`}
className={cn(
"max-h-full max-w-full object-contain transition-opacity cursor-pointer",
"max-h-full max-w-full cursor-pointer rounded-md object-contain transition-opacity",
isLoading ? "opacity-0" : "opacity-100"
)}
loading="eager"
@@ -109,7 +109,7 @@ export function PageDisplay({
src={imageBlobUrls[currentPage + 1] || getPageUrl(currentPage + 1)}
alt={`Page ${currentPage + 1}`}
className={cn(
"max-h-full max-w-full object-contain transition-opacity cursor-pointer",
"max-h-full max-w-full cursor-pointer rounded-md object-contain transition-opacity",
secondPageLoading ? "opacity-0" : "opacity-100"
)}
loading="eager"

View File

@@ -18,7 +18,7 @@ export function ReaderContainer({ children, onContainerClick }: ReaderContainerP
return (
<div
ref={readerRef}
className="reader-zoom-enabled fixed inset-0 bg-background/95 backdrop-blur-sm z-50 overflow-hidden"
className="reader-zoom-enabled fixed inset-0 z-50 overflow-hidden bg-[radial-gradient(90%_70%_at_50%_8%,hsl(var(--primary)/0.1),transparent_50%),linear-gradient(to_bottom,hsl(var(--background)/0.97),hsl(var(--background)/0.93)_40%,hsl(var(--background)))] backdrop-blur-sm"
onClick={handleContainerClick}
>
<div className="relative h-full flex flex-col items-center justify-center">{children}</div>

View File

@@ -20,7 +20,8 @@ export function ClientSettings({ initialConfig, initialLibraries }: ClientSettin
const { t } = useTranslate();
return (
<div className="container mx-auto px-4 py-8 space-y-6">
<div className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto space-y-8">
<h1 className="text-3xl font-bold">{t("settings.title")}</h1>
<Tabs defaultValue="display" className="w-full">
@@ -47,5 +48,6 @@ export function ClientSettings({ initialConfig, initialLibraries }: ClientSettin
</TabsContent>
</Tabs>
</div>
</div>
);
}

View File

@@ -4,18 +4,18 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-all duration-200 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none",
{
variants: {
variant: {
default: "bg-primary/90 backdrop-blur-md text-primary-foreground hover:bg-primary/80",
default: "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 hover:shadow-md",
destructive:
"bg-destructive/90 backdrop-blur-md text-destructive-foreground hover:bg-destructive/80",
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90 hover:shadow-md",
outline:
"border border-input bg-background/70 backdrop-blur-md hover:bg-accent/80 hover:text-accent-foreground",
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary/80 backdrop-blur-md text-secondary-foreground hover:bg-secondary/70",
ghost: "hover:bg-accent/80 hover:backdrop-blur-md hover:text-accent-foreground",
"bg-secondary text-secondary-foreground hover:bg-secondary/85",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {

View File

@@ -7,7 +7,7 @@ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElemen
<div
ref={ref}
className={cn(
"rounded-lg border bg-card/70 backdrop-blur-md text-card-foreground shadow-sm",
"rounded-lg border bg-card text-card-foreground shadow-sm transition-colors duration-200",
className
)}
{...props}

View File

@@ -9,7 +9,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background/70 backdrop-blur-md px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm ring-offset-background transition-colors duration-200 ease-out file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:border-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}

View File

@@ -15,8 +15,8 @@ const NavButton = React.forwardRef<HTMLButtonElement, NavButtonProps>(
<button
ref={ref}
className={cn(
"w-full flex items-center justify-between rounded-lg px-3 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground transition-colors",
active && "bg-accent",
"w-full flex items-center justify-between rounded-lg px-3 py-2 text-sm font-medium transition-all duration-200 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 hover:bg-accent hover:text-accent-foreground",
active && "bg-accent text-accent-foreground shadow-sm",
className
)}
{...props}

View File

@@ -61,7 +61,7 @@ const TabsList = React.forwardRef<HTMLDivElement, TabsListProps>(({ className, .
<div
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted/80 backdrop-blur-md p-1 text-muted-foreground",
"inline-flex h-10 items-center justify-center rounded-md border border-border bg-muted p-1 text-muted-foreground",
className
)}
{...props}
@@ -86,8 +86,8 @@ const TabsTrigger = React.forwardRef<HTMLButtonElement, TabsTriggerProps>(
role="tab"
aria-selected={isSelected}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
isSelected && "bg-background/90 backdrop-blur-md text-foreground shadow-sm",
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all duration-200 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
isSelected && "bg-background text-foreground shadow-sm",
className
)}
onClick={() => onValueChange(value)}

View File

@@ -27,67 +27,84 @@ body.no-pinch-zoom * {
@layer base {
:root {
--background: 0 0% 100%;
--background-rgb: 255, 255, 255;
--foreground: 222.2 84% 4.9%;
--background: 36 33% 97%;
--background-rgb: 249, 246, 241;
--foreground: 222 33% 15%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--card-foreground: 222 33% 15%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--popover-foreground: 222 33% 15%;
--primary: 222.2 47.4% 11.2%;
--primary: 198 78% 37%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--secondary: 36 30% 92%;
--secondary-foreground: 222 33% 15%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--muted: 36 24% 90%;
--muted-foreground: 220 13% 40%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--accent: 198 52% 90%;
--accent-foreground: 222 33% 15%;
--destructive: 0 84.2% 60.2%;
--destructive: 2 72% 48%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--border: 32 18% 84%;
--input: 32 18% 84%;
--ring: 198 78% 37%;
--radius: 0.5rem;
--radius: 0.75rem;
--surface-1: 0 0% 100%;
--surface-2: 34 27% 94%;
--elevation-1: 0 1px 2px 0 rgb(23 32 46 / 0.06);
--elevation-2: 0 8px 24px -8px rgb(23 32 46 / 0.18);
--font-ui: "Avenir Next", "Segoe UI", "Noto Sans", sans-serif;
--font-display: "Baskerville", "Times New Roman", serif;
--duration-fast: 120ms;
--duration-base: 200ms;
--duration-slow: 320ms;
--ease-standard: cubic-bezier(0.2, 0.8, 0.2, 1);
}
.dark {
--background: 222.2 84% 4.9%;
--background-rgb: 12, 17, 29;
--foreground: 210 40% 98%;
--background: 222 35% 10%;
--background-rgb: 17, 24, 38;
--foreground: 38 20% 92%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--card: 221 31% 13%;
--card-foreground: 38 20% 92%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--popover: 221 31% 13%;
--popover-foreground: 38 20% 92%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--primary: 194 76% 62%;
--primary-foreground: 220 39% 11%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--secondary: 221 22% 20%;
--secondary-foreground: 38 20% 92%;
--muted: 217.2 32.6% 25%;
--muted-foreground: 215 20.2% 65.1%;
--muted: 220 17% 24%;
--muted-foreground: 218 17% 72%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--accent: 210 34% 24%;
--accent-foreground: 38 20% 92%;
--destructive: 0 62.8% 30.6%;
--destructive: 2 76% 58%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--border: 219 18% 25%;
--input: 219 18% 25%;
--ring: 194 76% 62%;
--surface-1: 221 31% 13%;
--surface-2: 221 24% 17%;
--elevation-1: 0 1px 2px 0 rgb(2 8 18 / 0.35);
--elevation-2: 0 12px 30px -12px rgb(2 8 18 / 0.55);
}
}
@@ -95,8 +112,10 @@ body.no-pinch-zoom * {
* {
@apply border-border;
}
body {
@apply text-foreground;
@apply bg-background text-foreground antialiased;
font-family: var(--font-ui);
}
/* Empêche le zoom automatique iOS sur les inputs */