feat: light theme (#638)
This commit is contained in:
parent
a4cc82f526
commit
b35a20f43d
30 changed files with 189 additions and 89 deletions
|
|
@ -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": {
|
||||
|
|
|
|||
70
app/app.css
70
app/app.css
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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%)]",
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
35
app/client/components/theme-provider.tsx
Normal file
35
app/client/components/theme-provider.tsx
Normal 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;
|
||||
}
|
||||
26
app/client/components/theme-toggle.tsx
Normal file
26
app/client/components/theme-toggle.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export const TwoFactorSection = ({ twoFactorEnabled }: TwoFactorSectionProps) =>
|
|||
<p className="text-sm font-medium">
|
||||
Status:
|
||||
{twoFactorEnabled ? (
|
||||
<span className="text-green-500">Enabled</span>
|
||||
<span className="text-success">Enabled</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">Disabled</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue