feat: light theme (#638)

This commit is contained in:
Nico 2026-03-09 18:16:50 +01:00 committed by Nicolas Meienberger
parent a4cc82f526
commit b35a20f43d
30 changed files with 189 additions and 89 deletions

View file

@ -114,6 +114,7 @@
"unicorn/prefer-set-size": "warn",
"unicorn/prefer-string-starts-ends-with": "warn",
"import/no-cycle": "error",
"import/no-unused-modules": ["error", { "unusedExports": true }],
"eslint/no-console": ["warn", { "allow": ["warn", "error", "info"] }]
},
"settings": {

View file

@ -37,7 +37,6 @@ body {
}
body {
@apply bg-[#131313];
min-height: 100dvh;
}
@ -67,6 +66,8 @@ body {
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-success: var(--success);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
@ -88,38 +89,40 @@ body {
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--card-header: oklch(0.922 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--background: oklch(0.975 0.005 80);
--foreground: oklch(0.25 0.015 55);
--card: oklch(0.988 0.004 80);
--card-foreground: oklch(0.25 0.015 55);
--card-header: oklch(0.945 0.008 80);
--popover: oklch(0.988 0.004 80);
--popover-foreground: oklch(0.25 0.015 55);
--primary: oklch(0.25 0.015 55);
--primary-foreground: oklch(0.975 0.005 80);
--secondary: oklch(0.935 0.008 70);
--secondary-foreground: oklch(0.25 0.015 55);
--muted: oklch(0.935 0.008 70);
--muted-foreground: oklch(0.58 0.025 60);
--accent: oklch(0.935 0.008 70);
--accent-foreground: oklch(0.25 0.015 55);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--destructive-foreground: #fff;
--success: oklch(0.57 0.16 148);
--border: oklch(0.9 0.01 70);
--input: oklch(0.9 0.01 70);
--ring: oklch(0.7 0.015 60);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--sidebar: oklch(0.96 0.007 75);
--sidebar-foreground: oklch(0.25 0.015 55);
--sidebar-primary: oklch(0.25 0.015 55);
--sidebar-primary-foreground: oklch(0.975 0.005 80);
--sidebar-accent: oklch(0.935 0.008 70);
--sidebar-accent-foreground: oklch(0.25 0.015 55);
--sidebar-border: oklch(0.9 0.01 70);
--sidebar-ring: oklch(0.7 0.015 60);
--strong-accent: #ff543a;
}
@ -144,6 +147,8 @@ body {
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: #ff543a;
--destructive-foreground: #fff;
--success: oklch(0.696 0.17 162.48);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
@ -190,6 +195,17 @@ body {
}
}
.snapshot-scrollable::-webkit-scrollbar {
height: 6px;
}
.snapshot-scrollable::-webkit-scrollbar-track {
background: transparent;
}
.snapshot-scrollable::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 999px;
}
/* Hide built-in password reveal/clear controls (notably in Edge on Windows) */
[data-secret-input] input[type="password"]::-ms-reveal,
[data-secret-input] input[type="password"]::-ms-clear {

View file

@ -115,7 +115,7 @@ export function AppSidebar({ isInstanceAdmin }: Props) {
/>
<span
className={cn({
"text-white font-medium": isActive,
"text-foreground font-medium": isActive,
"text-muted-foreground": !isActive,
})}
>
@ -171,7 +171,7 @@ export function AppSidebar({ isInstanceAdmin }: Props) {
/>
<span
className={cn({
"text-white font-medium": isActive,
"text-foreground font-medium": isActive,
"text-muted-foreground": !isActive,
})}
>

View file

@ -59,7 +59,7 @@ export function CronInput({ value, onChange, error }: CronInputProps) {
{value && (
<div>
{isValid ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
<CheckCircle2 className="h-4 w-4 text-success" />
) : (
<AlertCircle className="h-4 w-4 text-destructive" />
)}

View file

@ -14,7 +14,7 @@ export function GridBackground({ children, className, containerClassName }: Grid
className={cn(
"pointer-events-none absolute inset-0 w-full h-full",
"bg-[size:40px_40px]",
"bg-[linear-gradient(to_right,#e4e4e7_1px,transparent_1px),linear-gradient(to_bottom,#e4e4e7_1px,transparent_1px)]",
"bg-[linear-gradient(to_right,#EAE5DF_1px,transparent_1px),linear-gradient(to_bottom,#EAE5DF_1px,transparent_1px)]",
"dark:bg-[linear-gradient(to_right,#262626_1px,transparent_1px),linear-gradient(to_bottom,#262626_1px,transparent_1px)]",
"[mask-image:radial-gradient(ellipse_at_top,black_70%,transparent_100%)]",
)}

View file

@ -10,6 +10,7 @@ import { DevPanelListener } from "./dev-panel-listener";
import { Outlet, useNavigate } from "@tanstack/react-router";
import { AppBreadcrumb } from "./app-breadcrumb";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
import { ThemeToggle } from "./theme-toggle";
type Props = {
loaderData: AppContext;
@ -46,12 +47,13 @@ export function Layout({ loaderData }: Props) {
<span className="text-sm text-muted-foreground hidden md:inline-flex pl-2 mr-5">
<span className="text-foreground">{loaderData.user.name}</span>
</span>
<ThemeToggle />
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="rounded-full h-7 text-xs text-muted-foreground hover:text-white"
className="rounded-full h-7 text-xs text-muted-foreground hover:text-foreground"
onClick={handleLogout}
>
<LogOut className="w-4 h-4" />
@ -64,7 +66,7 @@ export function Layout({ loaderData }: Props) {
<Button
variant="ghost"
size="icon"
className="relative overflow-hidden hidden lg:inline-flex rounded-full h-7 w-7 text-muted-foreground hover:text-white"
className="relative overflow-hidden hidden lg:inline-flex rounded-full h-7 w-7 text-muted-foreground hover:text-foreground"
>
<a
href="https://github.com/nicotsx/zerobyte/issues/new"

View file

@ -15,7 +15,7 @@ export const OnOff = ({ isOn, toggle, enabledLabel, disabledLabel, disabled }: P
className={cn(
"flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-semibold uppercase tracking-wide transition-colors",
isOn
? "border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-500/40 dark:bg-emerald-500/10 dark:text-emerald-200"
? "border-success/30 bg-success/10 text-success"
: "border-muted bg-muted/40 text-muted-foreground dark:border-muted/60 dark:bg-muted/10",
)}
>

View file

@ -59,7 +59,7 @@ export function OrganizationSwitcher() {
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div className="bg-black text-sidebar-primary-foreground flex aspect-square size-8 shrink-0 items-center justify-center overflow-hidden rounded-lg">
<div className="bg-foreground text-sidebar-primary-foreground flex aspect-square size-8 shrink-0 items-center justify-center overflow-hidden rounded-lg">
{activeOrganization?.logo ? (
<img
src={activeOrganization.logo}

View file

@ -12,8 +12,8 @@ interface StatusDotProps {
export const StatusDot = ({ variant, label, animated }: StatusDotProps) => {
const statusMapping = {
success: {
color: "bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]",
colorLight: "bg-emerald-400",
color: "bg-success shadow-[0_0_8px_rgba(16,185,129,0.5)]",
colorLight: "bg-success/60",
animated: animated ?? true,
},
neutral: {

View file

@ -0,0 +1,35 @@
import { createContext, useCallback, useContext, useState } from "react";
type Theme = "light" | "dark";
type ThemeContextValue = {
theme: Theme;
setTheme: (theme: Theme) => void;
};
const ThemeContext = createContext<ThemeContextValue | null>(null);
export const THEME_COOKIE_NAME = "theme";
export const THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365; // 1 year
const DEFAULT_THEME: Theme = "dark";
export function ThemeProvider({ children, initialTheme }: { children: React.ReactNode; initialTheme?: Theme }) {
const [theme, setThemeState] = useState<Theme>(initialTheme ?? DEFAULT_THEME);
const setTheme = useCallback((newTheme: Theme) => {
setThemeState(newTheme);
document.cookie = `${THEME_COOKIE_NAME}=${newTheme}; path=/; max-age=${THEME_COOKIE_MAX_AGE}`;
document.documentElement.classList.toggle("dark", newTheme === "dark");
document.documentElement.style.colorScheme = newTheme;
}, []);
return <ThemeContext value={{ theme, setTheme }}>{children}</ThemeContext>;
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
}

View file

@ -0,0 +1,26 @@
import { Moon, Sun } from "lucide-react";
import { useTheme } from "./theme-provider";
import { Button } from "./ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="relative rounded-full h-7 w-7 text-muted-foreground hover:text-foreground"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
aria-label="Toggle theme"
>
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
</Button>
</TooltipTrigger>
<TooltipContent>Toggle theme</TooltipContent>
</Tooltip>
);
}

View file

@ -11,13 +11,12 @@ const buttonVariants = cva(
{
variants: {
variant: {
default:
"bg-transparent text-white hover:bg-[#3A3A3A]/80 border hover:border-white/40 dark:text-white dark:hover:bg-[#3A3A3A]/80",
default: "bg-transparent text-foreground hover:bg-muted border hover:border-border",
primary: "bg-strong-accent text-white hover:bg-strong-accent/90 focus-visible:ring-strong-accent/50",
destructive:
"border border-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/50 text-destructive hover:text-white",
"border border-destructive text-destructive hover:bg-destructive hover:text-destructive-foreground focus-visible:ring-destructive/50",
outline: "border border-border bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-transparent text-white hover:bg-[#3A3A3A]/80 border dark:text-white dark:hover:bg-[#3A3A3A]/80",
secondary: "bg-transparent text-foreground hover:bg-muted border",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},

View file

@ -7,7 +7,7 @@ function Card({ className, children, interactive, ...props }: React.ComponentPro
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground group relative flex flex-col gap-6 border border-border py-6 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.02)] transition-colors duration-300 ",
"bg-card text-card-foreground group relative flex flex-col gap-6 border border-border py-6 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.03)] dark:shadow-[inset_0_0_0_1px_rgba(255,255,255,0.02)] transition-colors duration-300 ",
className,
)}
{...props}
@ -21,14 +21,14 @@ function Card({ className, children, interactive, ...props }: React.ComponentPro
},
)}
>
<span className="absolute -left-0.5 -top-0.5 h-0.5 w-4 bg-white/80" />
<span className="absolute -left-0.5 -top-0.5 h-4 w-0.5 bg-white/80" />
<span className="absolute -right-0.5 -top-0.5 h-0.5 w-4 bg-white/80" />
<span className="absolute -right-0.5 -top-0.5 h-4 w-0.5 bg-white/80" />
<span className="absolute -left-0.5 -bottom-0.5 h-0.5 w-4 bg-white/80" />
<span className="absolute -left-0.5 -bottom-0.5 h-4 w-0.5 bg-white/80" />
<span className="absolute -right-0.5 -bottom-0.5 h-0.5 w-4 bg-white/80" />
<span className="absolute -right-0.5 -bottom-0.5 h-4 w-0.5 bg-white/80" />
<span className="absolute -left-0.5 -top-0.5 h-0.5 w-4 bg-foreground" />
<span className="absolute -left-0.5 -top-0.5 h-4 w-0.5 bg-foreground" />
<span className="absolute -right-0.5 -top-0.5 h-0.5 w-4 bg-foreground" />
<span className="absolute -right-0.5 -top-0.5 h-4 w-0.5 bg-foreground" />
<span className="absolute -left-0.5 -bottom-0.5 h-0.5 w-4 bg-foreground" />
<span className="absolute -left-0.5 -bottom-0.5 h-4 w-0.5 bg-foreground" />
<span className="absolute -right-0.5 -bottom-0.5 h-0.5 w-4 bg-foreground" />
<span className="absolute -right-0.5 -bottom-0.5 h-4 w-0.5 bg-foreground" />
</span>
{children}
</div>

View file

@ -8,8 +8,8 @@ interface CodeBlockProps {
export const CodeBlock: React.FC<CodeBlockProps> = ({ code, filename }) => {
return (
<div className="overflow-hidden rounded-sm bg-card-header ring-1 ring-white/10">
<div className="flex items-center justify-between border-b border-white/10 px-4 py-2 text-xs">
<div className="overflow-hidden rounded-sm bg-card-header ring-1 ring-border">
<div className="flex items-center justify-between border-b border-border px-4 py-2 text-xs">
<div className="flex items-center gap-1.5">
<span className="h-2.5 w-2.5 rounded-full bg-rose-500" />
<span className="h-2.5 w-2.5 rounded-full bg-amber-500" />
@ -18,7 +18,7 @@ export const CodeBlock: React.FC<CodeBlockProps> = ({ code, filename }) => {
</div>
</div>
<pre className="text-xs m-0 px-4 py-2 bg-card-header">
<code className="text-white/80 select-all">{code}</code>
<code className="text-foreground/80 select-all">{code}</code>
</pre>
</div>
);

View file

@ -1,4 +1,4 @@
import { useTheme } from "next-themes";
import { useTheme } from "~/client/components/theme-provider";
import { Toaster as Sonner, type ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {

View file

@ -37,11 +37,11 @@ function TabsTrigger({ className, onClick, ...props }: React.ComponentProps<type
// Padding: 20px horizontal (8px for bracket tick + 12px gap to text)
"px-5",
// Transparent orange background for active state
"data-[state=active]:bg-[#FF453A]/10",
"data-[state=active]:bg-strong-accent/10",
// Left bracket - vertical line
"before:absolute before:left-0 before:top-0 before:h-7 before:w-0.5 before:bg-[#5D6570] before:transition-colors data-[state=active]:before:bg-[#FF453A]",
"before:absolute before:left-0 before:top-0 before:h-7 before:w-0.5 before:bg-muted-foreground/60 before:transition-colors data-[state=active]:before:bg-strong-accent",
// Left bracket - top tick
"after:absolute after:left-0 after:-top-px after:w-2 after:h-0.5 after:bg-[#5D6570] after:transition-colors data-[state=active]:after:bg-[#FF453A]",
"after:absolute after:left-0 after:-top-px after:w-2 after:h-0.5 after:bg-muted-foreground/60 after:transition-colors data-[state=active]:after:bg-strong-accent",
className,
)}
onClick={handleClick}
@ -49,13 +49,13 @@ function TabsTrigger({ className, onClick, ...props }: React.ComponentProps<type
>
<span className="relative z-10">{props.children}</span>
{/* Left bracket - bottom tick */}
<span className="absolute left-0 bottom-[-1px] h-0.5 w-2 bg-[#5D6570] transition-colors group-data-[state=active]:bg-[#FF453A]" />
<span className="absolute left-0 bottom-[-1px] h-0.5 w-2 bg-muted-foreground/60 transition-colors group-data-[state=active]:bg-strong-accent" />
{/* Right bracket - top tick */}
<span className="absolute right-0 top-[-1px] h-0.5 w-2 bg-[#5D6570] transition-colors group-data-[state=active]:bg-[#FF453A]" />
<span className="absolute right-0 top-[-1px] h-0.5 w-2 bg-muted-foreground/60 transition-colors group-data-[state=active]:bg-strong-accent" />
{/* Right bracket - vertical line */}
<span className="absolute right-0 top-0 h-7 w-0.5 bg-[#5D6570] transition-colors group-data-[state=active]:bg-[#FF453A]" />
<span className="absolute right-0 top-0 h-7 w-0.5 bg-muted-foreground/60 transition-colors group-data-[state=active]:bg-strong-accent" />
{/* Right bracket - bottom tick */}
<span className="absolute right-0 bottom-[-1px] h-0.5 w-2 bg-[#5D6570] transition-colors group-data-[state=active]:bg-[#FF453A]" />
<span className="absolute right-0 bottom-[-1px] h-0.5 w-2 bg-muted-foreground/60 transition-colors group-data-[state=active]:bg-strong-accent" />
</TabsPrimitive.Trigger>
);
}

View file

@ -32,21 +32,21 @@ export const BackupCard = ({ schedule }: { schedule: BackupSchedule }) => {
<CardContent className="flex-1 space-y-4">
<div className="space-y-3">
<div className="flex items-center text-sm gap-2">
<span className="text-zinc-500 shrink-0">Schedule</span>
<div className="flex-1 border-b border-dashed border-white/10" />
<code className="text-xs text-zinc-100 font-mono bg-muted px-2 py-1 rounded shrink-0">
<span className="text-muted-foreground shrink-0">Schedule</span>
<div className="flex-1 border-b border-dashed border-border/50" />
<code className="text-xs text-foreground font-mono bg-muted px-2 py-1 rounded shrink-0">
{schedule.cronExpression}
</code>
</div>
<div className="flex items-center text-sm gap-2">
<span className="text-zinc-500 shrink-0">Last backup</span>
<div className="flex-1 border-b border-dashed border-white/10" />
<span className="text-zinc-100 font-mono text-sm shrink-0">{formatTimeAgo(schedule.lastBackupAt)}</span>
<span className="text-muted-foreground shrink-0">Last backup</span>
<div className="flex-1 border-b border-dashed border-border/50" />
<span className="text-foreground font-mono text-sm shrink-0">{formatTimeAgo(schedule.lastBackupAt)}</span>
</div>
<div className="flex items-center text-sm gap-2">
<span className="text-zinc-500 shrink-0">Next backup</span>
<div className="flex-1 border-b border-dashed border-white/10" />
<span className="text-zinc-100 font-mono text-sm shrink-0">
<span className="text-muted-foreground shrink-0">Next backup</span>
<div className="flex-1 border-b border-dashed border-border/50" />
<span className="text-foreground font-mono text-sm shrink-0">
{formatShortDateTime(schedule.nextBackupAt)}
</span>
</div>

View file

@ -1,3 +1,4 @@
import { useRef } from "react";
import type { ListSnapshotsResponse } from "~/client/api-client";
import { ByteSize } from "~/client/components/bytes-size";
import { Card, CardContent } from "~/client/components/ui/card";
@ -15,6 +16,7 @@ interface Props {
export const SnapshotTimeline = (props: Props) => {
const { snapshots, snapshotId, loading, onSnapshotSelect, error } = props;
const selectedRef = useRef<HTMLButtonElement>(null);
if (error) {
return (
@ -51,13 +53,14 @@ export const SnapshotTimeline = (props: Props) => {
<div className="w-full bg-card">
<div className="relative flex items-center">
<div className="flex-1 overflow-hidden">
<div className="flex gap-4 overflow-x-auto pb-2 *:first:ml-2 *:last:mr-2">
<div className="snapshot-scrollable flex gap-4 overflow-x-auto pb-2 *:first:ml-2 *:last:mr-2">
{snapshots.map((snapshot) => {
const date = new Date(snapshot.time);
const isSelected = snapshotId === snapshot.short_id;
return (
<button
ref={isSelected ? selectedRef : undefined}
type="button"
key={snapshot.short_id}
onClick={() => onSnapshotSelect(snapshot.short_id)}

View file

@ -65,7 +65,9 @@ export function ScheduleDetailsPage(props: Props) {
const searchParams = useSearch({ from: "/(dashboard)/backups/$backupId/" });
const [isEditMode, setIsEditMode] = useState(false);
const formId = useId();
const [selectedSnapshotId, setSelectedSnapshotId] = useState<string | undefined>(initialSnapshotId);
const [selectedSnapshotId, setSelectedSnapshotId] = useState<string | undefined>(
initialSnapshotId ?? loaderData.snapshots?.at(-1)?.short_id,
);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [snapshotToDelete, setSnapshotToDelete] = useState<string | null>(null);

View file

@ -97,7 +97,7 @@ export function NotificationDetailsPage({ notificationId }: { notificationId: st
<div className="text-sm font-semibold text-muted-foreground flex items-center gap-2">
<span
className={cn("inline-flex items-center gap-2 px-2 py-1 rounded-md text-xs bg-gray-500/10 text-gray-500", {
"bg-green-500/10 text-emerald-500": data.enabled,
"bg-success/10 text-success": data.enabled,
"bg-red-500/10 text-red-500": !data.enabled,
})}
>

View file

@ -41,7 +41,7 @@ export const DoctorReport = ({ result, repositoryStatus }: Props) => {
<CollapsibleTrigger className="w-full flex items-center justify-between p-3 hover:bg-muted/50 transition-colors">
<span className="text-sm font-medium capitalize">{step.step.replaceAll("_", " ")}</span>
{step.success ? (
<CheckCircle2 className="h-4 w-4 text-emerald-500" />
<CheckCircle2 className="h-4 w-4 text-success" />
) : (
<AlertCircle className="h-4 w-4 text-red-500" />
)}

View file

@ -143,7 +143,7 @@ export function RepositoriesPage() {
className={cn(
"inline-flex items-center gap-2 px-2 py-1 rounded-md text-xs bg-gray-500/10 text-gray-500",
{
"bg-green-500/10 text-emerald-500": repository.status === "healthy",
"bg-success/10 text-success": repository.status === "healthy",
"bg-red-500/10 text-red-500": repository.status === "error",
},
)}

View file

@ -187,7 +187,7 @@ export const RepositoryInfoTabContent = ({ repository, initialStats }: Props) =>
<p className="text-sm flex items-center gap-2">
<span
className={cn("w-2 h-2 rounded-full", {
"bg-emerald-500": repository.status === "healthy",
"bg-success": repository.status === "healthy",
"bg-red-500": repository.status === "error",
"bg-amber-500": repository.status !== "healthy" && repository.status !== "error",
"animate-pulse": repository.status === "doctor",
@ -209,7 +209,7 @@ export const RepositoryInfoTabContent = ({ repository, initialStats }: Props) =>
{hasCaCert && (
<div className="flex flex-col gap-1">
<div className="text-sm font-medium text-muted-foreground">CA Certificate</div>
<p className="text-sm text-green-500">Configured</p>
<p className="text-sm text-success">Configured</p>
</div>
)}
{hasInsecureTlsConfig && (
@ -217,7 +217,7 @@ export const RepositoryInfoTabContent = ({ repository, initialStats }: Props) =>
<div className="text-sm font-medium text-muted-foreground">TLS Validation</div>
<p className="text-sm">
<span className={cn("text-red-500", { hidden: !isTlsValidationDisabled })}>Disabled</span>
<span className={cn("text-green-500", { hidden: isTlsValidationDisabled })}>Enabled</span>
<span className={cn("text-success", { hidden: isTlsValidationDisabled })}>Enabled</span>
</p>
</div>
)}

View file

@ -34,7 +34,7 @@ export const TwoFactorSection = ({ twoFactorEnabled }: TwoFactorSectionProps) =>
<p className="text-sm font-medium">
Status:&nbsp;
{twoFactorEnabled ? (
<span className="text-green-500">Enabled</span>
<span className="text-success">Enabled</span>
) : (
<span className="text-muted-foreground">Disabled</span>
)}

View file

@ -154,7 +154,7 @@ export function UserManagement({ currentUser }: { currentUser: { id: string } |
<Badge variant="outline" className={cn("text-red-500 border-red-500", { hidden: !user.banned })}>
Banned
</Badge>
<Badge variant="outline" className={cn("bg-green-500/10 text-emerald-500", { hidden: user.banned })}>
<Badge variant="outline" className={cn("bg-success/10 text-success", { hidden: user.banned })}>
Active
</Badge>
</TableCell>

View file

@ -237,7 +237,7 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
>
{testBackendConnection.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{!testBackendConnection.isPending && testMessage?.success && (
<CheckCircle className="mr-2 h-4 w-4 text-emerald-500" />
<CheckCircle className="mr-2 h-4 w-4 text-success" />
)}
{!testBackendConnection.isPending && testMessage && !testMessage.success && (
<XCircle className="mr-2 h-4 w-4 text-red-500" />
@ -255,7 +255,7 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
{testMessage && (
<div
className={cn("text-xs p-2 rounded-md text-wrap wrap-anywhere", {
"bg-green-50 text-emerald-700 border border-green-200": testMessage.success,
"bg-success/10 text-success border border-success/30": testMessage.success,
"bg-red-50 text-red-700 border border-red-200": !testMessage.success,
})}
>

View file

@ -51,7 +51,7 @@ export const HealthchecksCard = ({ volume }: Props) => {
<CardContent>
<div className="flex flex-col flex-1 justify-start">
{volume.lastError && <span className="text-sm text-red-500 wrap-break-word">{volume.lastError}</span>}
{volume.status === "mounted" && <span className="text-md text-green-500">Healthy</span>}
{volume.status === "mounted" && <span className="text-md text-success">Healthy</span>}
{volume.status !== "unmounted" && (
<span className="text-xs text-muted-foreground mb-4">Checked {formatTimeAgo(volume.lastHealthCheck)}</span>
)}

View file

@ -134,7 +134,7 @@ export function VolumesPage() {
{filteredVolumes.map((volume) => (
<TableRow
key={volume.name}
className="hover:bg-white/2 hover:cursor-pointer transition-colors h-12"
className="hover:bg-muted/50 hover:cursor-pointer transition-colors h-12"
onClick={() => navigate({ to: `/volumes/${volume.shortId}` })}
>
<TableCell className="font-medium font-mono text-strong-accent">{volume.name}</TableCell>

View file

@ -6,11 +6,23 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { Toaster } from "~/client/components/ui/sonner";
import { useServerEvents } from "~/client/hooks/use-server-events";
import { useEffect } from "react";
import { ThemeProvider, THEME_COOKIE_NAME } from "~/client/components/theme-provider";
import { createServerFn } from "@tanstack/react-start";
import { getCookie } from "@tanstack/react-start/server";
const fetchTheme = createServerFn({ method: "GET" }).handler(async () => {
const themeCookie = getCookie(THEME_COOKIE_NAME);
return (themeCookie === "light" ? "light" : "dark") as "light" | "dark";
});
export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({
server: {
middleware: [apiClientMiddleware],
},
loader: async () => {
const theme = await fetchTheme();
return { theme };
},
head: () => ({
meta: [{ title: "Zerobyte - Open Source Backup Solution" }],
links: [
@ -35,6 +47,7 @@ export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()(
});
function RootLayout() {
const { theme } = Route.useLoaderData();
useServerEvents();
useEffect(() => {
document.body.setAttribute("data-app-ready", "true");
@ -44,7 +57,7 @@ function RootLayout() {
}, []);
return (
<html lang="en">
<html lang="en" className={theme === "dark" ? "dark" : undefined} style={{ colorScheme: theme }}>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
@ -56,10 +69,12 @@ function RootLayout() {
<link rel="manifest" href="/images/favicon/site.webmanifest" />
<HeadContent />
</head>
<body className="dark">
<Outlet />
<Toaster />
<ReactQueryDevtools buttonPosition="bottom-right" />
<body>
<ThemeProvider initialTheme={theme}>
<Outlet />
<Toaster />
<ReactQueryDevtools buttonPosition="bottom-right" />
</ThemeProvider>
<Scripts />
</body>
</html>

View file

@ -7,6 +7,7 @@ const flushMicrotasks = async () => {
};
const mockTimeZone = (timeZone: string) => {
// oxlint-disable-next-line typescript/unbound-method
const resolvedOptions = Intl.DateTimeFormat.prototype.resolvedOptions;
return spyOn(Intl.DateTimeFormat.prototype, "resolvedOptions").mockImplementation(
function (this: Intl.DateTimeFormat) {