Refactor global styles and enhance MITM functionality

- Updated global CSS to implement a new brand color palette and improve light/dark theme consistency.
- Enhanced the MitmServerCard component to provide clearer user feedback regarding admin privileges.
- Filtered LLM combos in the CombosPage to ensure only relevant data is displayed.
- Improved APIPageClient layout for better usability and visual consistency.
- Added functionality to save and load DNS tool states in the MITM manager.
- Updated OAuth configuration URLs for Qwen to reflect the new endpoint structure.
- Refined tunnel management logic to improve reliability and user experience.
This commit is contained in:
decolua 2026-05-03 18:00:35 +07:00
parent 1686adc704
commit 6cdf40b44e
40 changed files with 1029 additions and 762 deletions

View file

@ -3,8 +3,8 @@
import { cn } from "@/shared/utils/cn";
const variants = {
default: "bg-black/5 dark:bg-white/10 text-text-muted",
primary: "bg-primary/10 text-primary",
default: "bg-surface-2 text-text-muted",
primary: "bg-brand-500/10 text-brand-600 dark:text-brand-300",
success: "bg-green-500/10 text-green-600 dark:text-green-400",
warning: "bg-yellow-500/10 text-yellow-600 dark:text-yellow-400",
error: "bg-red-500/10 text-red-600 dark:text-red-400",
@ -42,7 +42,7 @@ export default function Badge({
variant === "warning" && "bg-yellow-500",
variant === "error" && "bg-red-500",
variant === "info" && "bg-blue-500",
variant === "primary" && "bg-primary",
variant === "primary" && "bg-brand-500",
variant === "default" && "bg-gray-500"
)}
/>
@ -52,4 +52,3 @@ export default function Badge({
</span>
);
}

View file

@ -3,17 +3,18 @@
import { cn } from "@/shared/utils/cn";
const variants = {
primary: "bg-gradient-to-b from-primary to-primary-hover text-white shadow-sm",
secondary: "bg-white dark:bg-white/10 border border-black/10 dark:border-white/10 text-text-main hover:bg-black/5 dark:hover:bg-white/5",
outline: "border border-black/15 dark:border-white/15 text-text-main hover:bg-black/5",
ghost: "text-text-muted hover:bg-black/5 dark:hover:bg-white/5 hover:text-text-main",
danger: "bg-red-500 text-white hover:bg-red-600 shadow-sm",
primary: "bg-brand-500 hover:bg-brand-600 text-white shadow-sm disabled:bg-surface-3 disabled:text-text-muted",
secondary: "bg-surface-2 hover:bg-surface-3 text-text-main border border-border disabled:opacity-50",
outline: "border border-border text-text-main hover:bg-surface-2 hover:border-brand-500/40",
ghost: "text-text-muted hover:bg-surface-2 hover:text-text-main",
danger: "bg-red-500 hover:bg-red-600 text-white shadow-sm disabled:bg-surface-3 disabled:text-text-muted",
success: "bg-green-600 hover:bg-green-700 text-white shadow-sm disabled:bg-surface-3 disabled:text-text-muted",
};
const sizes = {
sm: "h-7 px-3 text-xs rounded-md",
md: "h-9 px-4 text-sm rounded-lg",
lg: "h-11 px-6 text-sm rounded-lg",
sm: "h-7 px-3 text-xs rounded-[8px]",
md: "h-9 px-4 text-sm rounded-[10px]",
lg: "h-11 px-6 text-sm rounded-[10px]",
};
export default function Button({
@ -31,8 +32,8 @@ export default function Button({
return (
<button
className={cn(
"inline-flex items-center justify-center gap-2 font-medium transition-all duration-200 cursor-pointer",
"active:scale-[0.99] disabled:opacity-50 disabled:cursor-not-allowed disabled:active:scale-100",
"inline-flex items-center justify-center gap-2 font-semibold transition-all duration-150 ease-out cursor-pointer",
"active:scale-[0.97] disabled:opacity-50 disabled:cursor-not-allowed disabled:active:scale-100",
variants[variant],
sizes[size],
fullWidth && "w-full",
@ -53,4 +54,3 @@ export default function Button({
</button>
);
}

View file

@ -10,6 +10,7 @@ export default function Card({
action,
padding = "md",
hover = false,
elev = false,
className,
...props
}) {
@ -24,10 +25,9 @@ export default function Card({
return (
<div
className={cn(
"bg-surface",
"border border-black/5 dark:border-white/5",
"rounded-lg shadow-sm",
hover && "hover:shadow-md hover:border-primary/30 transition-all cursor-pointer",
"bg-surface border border-border-subtle",
elev ? "rounded-[14px] shadow-[var(--shadow-elev)]" : "rounded-[14px] shadow-[var(--shadow-soft)]",
hover && "hover:shadow-[var(--shadow-warm)] hover:border-brand-500/30 transition-all cursor-pointer",
paddings[padding],
className
)}
@ -37,7 +37,7 @@ export default function Card({
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
{icon && (
<div className="p-2 rounded-lg bg-bg text-text-muted">
<div className="p-2 rounded-[10px] bg-bg text-text-muted">
<span className="material-symbols-outlined text-[20px]">{icon}</span>
</div>
)}
@ -58,14 +58,12 @@ export default function Card({
);
}
// Sub-component: Bordered section inside Card
Card.Section = function CardSection({ children, className, ...props }) {
return (
<div
className={cn(
"p-4 rounded-lg",
"bg-black/[0.02] dark:bg-white/[0.02]",
"border border-black/5 dark:border-white/5",
"p-4 rounded-[10px]",
"bg-bg border border-border-subtle",
className
)}
{...props}
@ -75,14 +73,13 @@ Card.Section = function CardSection({ children, className, ...props }) {
);
};
// Sub-component: Hoverable row inside Card
Card.Row = function CardRow({ children, className, ...props }) {
return (
<div
className={cn(
"p-3 -mx-3 px-3 transition-colors",
"border-b border-black/5 dark:border-white/5 last:border-b-0",
"hover:bg-black/[0.02] dark:hover:bg-white/[0.02]",
"border-b border-border-subtle last:border-b-0",
"hover:bg-surface-2/50",
className
)}
{...props}
@ -92,20 +89,18 @@ Card.Row = function CardRow({ children, className, ...props }) {
);
};
// Sub-component: List item with hover actions (macOS style)
Card.ListItem = function CardListItem({
children,
Card.ListItem = function CardListItem({
children,
actions,
className,
...props
className,
...props
}) {
return (
<div
className={cn(
"group flex items-center justify-between p-3 -mx-3 px-3",
"border-b border-black/[0.03] dark:border-white/[0.03] last:border-b-0",
"hover:bg-black/[0.02] dark:hover:bg-white/[0.02]",
"transition-colors",
"border-b border-border-subtle last:border-b-0",
"hover:bg-surface-2/50 transition-colors",
className
)}
{...props}
@ -119,4 +114,3 @@ Card.ListItem = function CardListItem({
</div>
);
};

View file

@ -3,13 +3,13 @@
import { useEffect } from "react";
import { cn } from "@/shared/utils/cn";
export default function Drawer({
isOpen,
onClose,
title,
children,
export default function Drawer({
isOpen,
onClose,
title,
children,
width = "md",
className
className
}) {
const widths = {
sm: "w-[400px]",
@ -19,24 +19,18 @@ export default function Drawer({
full: "w-full",
};
// Lock body scroll when drawer is open
useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
document.body.style.overflow = "";
};
return () => { document.body.style.overflow = ""; };
}, [isOpen]);
// Handle escape key
useEffect(() => {
const handleEscape = (e) => {
if (e.key === "Escape" && isOpen) {
onClose();
}
if (e.key === "Escape" && isOpen) onClose();
};
document.addEventListener("keydown", handleEscape);
return () => document.removeEventListener("keydown", handleEscape);
@ -47,40 +41,39 @@ export default function Drawer({
return (
<div className="fixed inset-0 z-50">
{/* Overlay */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity cursor-pointer"
<div
className="absolute inset-0 bg-black/50 backdrop-blur-[2px] fade-in cursor-pointer"
onClick={onClose}
aria-hidden="true"
/>
{/* Drawer panel */}
<div className={cn(
"absolute right-0 top-0 h-full bg-surface shadow-2xl flex flex-col",
"animate-in slide-in-from-right duration-200",
"border-l border-black/10 dark:border-white/10",
"absolute right-0 top-0 h-full bg-surface flex flex-col",
"shadow-[var(--shadow-elev)]",
"slide-in-right",
"border-l border-border-subtle",
widths[width] || widths.md,
className
)}>
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-black/5 dark:border-white/5 flex-shrink-0">
<div className="flex items-center justify-between p-6 border-b border-border-subtle flex-shrink-0">
<div className="flex items-center gap-3">
{title && (
<h2 className="text-lg font-semibold text-text-main">
{title}
</h2>
<h2 className="text-lg font-semibold text-text-main">{title}</h2>
)}
</div>
<button
type="button"
onClick={onClose}
className="p-1.5 rounded-lg text-text-muted hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
className="p-1.5 rounded-[10px] text-text-muted hover:bg-surface-2 hover:text-text-main transition-colors"
>
<span className="material-symbols-outlined text-[20px]">close</span>
</button>
</div>
{/* Body */}
<div className="flex-1 overflow-y-auto p-6">
<div className="flex-1 overflow-y-auto p-6 custom-scrollbar">
{children}
</div>
</div>

View file

@ -6,6 +6,7 @@ import Link from "next/link";
import PropTypes from "prop-types";
import ProviderIcon from "@/shared/components/ProviderIcon";
import HeaderMenu from "@/shared/components/HeaderMenu";
import ThemeToggle from "@/shared/components/ThemeToggle";
import { OAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/config";
import { MEDIA_PROVIDER_KINDS, AI_PROVIDERS } from "@/shared/constants/providers";
import { translate } from "@/i18n/runtime";
@ -181,9 +182,9 @@ export default function Header({ onMenuClick, showMenuButton = true }) {
};
return (
<header className="flex items-center justify-between px-8 py-5 border-b border-black/5 dark:border-white/5 bg-bg/80 backdrop-blur-xl z-10 sticky top-0">
<header className="shrink-0 flex items-center justify-between gap-3 px-4 lg:px-8 pt-3 pb-2 border-b border-border-subtle bg-surface/60 backdrop-blur-xl lg:bg-transparent lg:backdrop-blur-none z-20">
{/* Mobile menu button */}
<div className="flex items-center gap-3 lg:hidden">
<div className="flex items-center gap-3 lg:hidden shrink-0">
{showMenuButton && (
<button
onClick={onMenuClick}
@ -194,8 +195,8 @@ export default function Header({ onMenuClick, showMenuButton = true }) {
)}
</div>
{/* Page title with breadcrumbs - desktop */}
<div className="hidden lg:flex flex-col">
{/* Page title with breadcrumbs */}
<div className="flex flex-col min-w-0 flex-1">
{breadcrumbs.length > 0 ? (
<div className="flex items-center gap-2">
{breadcrumbs.map((crumb, index) => (
@ -226,7 +227,7 @@ export default function Header({ onMenuClick, showMenuButton = true }) {
fallbackText={crumb.label.slice(0, 2).toUpperCase()}
/>
)}
<h1 className="text-2xl font-semibold text-text-main tracking-tight">
<h1 className="text-base lg:text-2xl font-semibold text-text-main tracking-tight truncate">
{translate(crumb.label)}
</h1>
</div>
@ -238,16 +239,16 @@ export default function Header({ onMenuClick, showMenuButton = true }) {
<div>
<div className="flex items-center gap-2">
{icon && (
<span className="material-symbols-outlined text-primary text-2xl">
<span className="material-symbols-outlined text-primary text-xl lg:text-2xl">
{icon}
</span>
)}
<h1 className="text-2xl font-semibold tracking-tight">
<h1 className="text-base lg:text-2xl font-semibold tracking-tight truncate">
{translate(title)}
</h1>
</div>
{description && (
<p className="text-sm text-text-muted">
<p className="hidden lg:block text-sm text-text-muted truncate">
{translate(description)}
</p>
)}
@ -255,8 +256,9 @@ export default function Header({ onMenuClick, showMenuButton = true }) {
) : null}
</div>
{/* Right actions - consolidated into dropdown menu */}
<div className="flex items-center ml-auto">
{/* Right actions */}
<div className="flex items-center gap-1 shrink-0">
<ThemeToggle />
<HeaderMenu onLogout={handleLogout} />
</div>
</header>

View file

@ -38,17 +38,14 @@ export default function Input({
onChange={onChange}
disabled={disabled}
className={cn(
"w-full py-2 px-3 text-sm text-text-main",
"bg-white dark:bg-white/5 border border-black/10 dark:border-white/10 rounded-md",
"placeholder-text-muted/60",
"focus:ring-1 focus:ring-primary/30 focus:border-primary/50 focus:outline-none",
"transition-all shadow-inner disabled:opacity-50 disabled:cursor-not-allowed",
"w-full py-2.5 px-3 text-sm text-text-main bg-surface-2 rounded-[10px]",
"border border-transparent placeholder-text-muted/70",
"focus:outline-none focus:ring-2 focus:ring-brand-500/30 focus:border-brand-500/40",
"transition-all duration-150 ease-out disabled:opacity-50 disabled:cursor-not-allowed",
// iOS zoom fix
"text-[16px] sm:text-sm",
icon && "pl-10",
error
? "border-red-500 focus:border-red-500 focus:ring-red-500/20"
: "",
error && "ring-1 ring-red-500 focus:ring-2 focus:ring-red-500/40 border-red-500/40",
inputClassName
)}
{...props}
@ -66,4 +63,3 @@ export default function Input({
</div>
);
}

View file

@ -14,7 +14,7 @@ export function Spinner({ size = "md", className }) {
return (
<span
className={cn(
"material-symbols-outlined animate-spin text-primary",
"material-symbols-outlined animate-spin text-brand-500",
sizes[size],
className
)}
@ -39,7 +39,7 @@ export function Skeleton({ className, ...props }) {
return (
<div
className={cn(
"animate-pulse rounded-lg bg-border",
"animate-pulse rounded-[10px] bg-surface-2",
className
)}
{...props}
@ -50,10 +50,10 @@ export function Skeleton({ className, ...props }) {
// Card skeleton
export function CardSkeleton() {
return (
<div className="p-6 rounded-xl border border-border bg-surface">
<div className="p-6 rounded-[14px] border border-border-subtle bg-surface shadow-[var(--shadow-soft)]">
<div className="flex items-center justify-between mb-4">
<Skeleton className="h-4 w-24" />
<Skeleton className="size-10 rounded-lg" />
<Skeleton className="size-10 rounded-[10px]" />
</div>
<Skeleton className="h-8 w-16 mb-2" />
<Skeleton className="h-3 w-20" />
@ -61,7 +61,6 @@ export function CardSkeleton() {
);
}
// Default export
export default function Loading({ type = "spinner", ...props }) {
switch (type) {
case "page":
@ -74,4 +73,3 @@ export default function Loading({ type = "spinner", ...props }) {
return <Spinner {...props} />;
}
}

View file

@ -13,6 +13,7 @@ export default function Modal({
size = "md",
closeOnOverlay = true,
showCloseButton = true,
showTrafficLights = true,
className,
}) {
const sizes = {
@ -23,24 +24,18 @@ export default function Modal({
full: "max-w-4xl",
};
// Lock body scroll when modal is open
useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
document.body.style.overflow = "";
};
return () => { document.body.style.overflow = ""; };
}, [isOpen]);
// Handle escape key
useEffect(() => {
const handleEscape = (e) => {
if (e.key === "Escape" && isOpen) {
onClose();
}
if (e.key === "Escape" && isOpen) onClose();
};
document.addEventListener("keydown", handleEscape);
return () => document.removeEventListener("keydown", handleEscape);
@ -52,7 +47,7 @@ export default function Modal({
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Overlay */}
<div
className="absolute inset-0 bg-black/30 backdrop-blur-sm"
className="absolute inset-0 bg-black/50 backdrop-blur-[2px] fade-in"
onClick={closeOnOverlay ? onClose : undefined}
/>
@ -60,32 +55,32 @@ export default function Modal({
<div
className={cn(
"relative w-full bg-surface",
"border border-black/10 dark:border-white/10",
"rounded-xl shadow-2xl",
"animate-in fade-in zoom-in-95 duration-200",
"border border-border-subtle",
"rounded-[14px] shadow-[var(--shadow-elev)]",
"fade-in",
sizes[size],
className
)}
>
{/* Header */}
{(title || showCloseButton) && (
<div className="flex items-center justify-between p-2 border-b border-black/5 dark:border-white/5">
<div className="flex items-center justify-between p-2 border-b border-border-subtle">
<div className="flex items-center">
<div className="flex items-center gap-2 mr-4">
<div className="w-3 h-3 rounded-full bg-[#FF5F56]" />
<div className="w-3 h-3 rounded-full bg-[#FFBD2E]" />
<div className="w-3 h-3 rounded-full bg-[#27C93F]" />
</div>
{showTrafficLights && (
<div className="flex items-center gap-2 mr-4 ml-2">
<div className="w-3 h-3 rounded-full bg-[#FF5F56]" />
<div className="w-3 h-3 rounded-full bg-[#FFBD2E]" />
<div className="w-3 h-3 rounded-full bg-[#27C93F]" />
</div>
)}
{title && (
<h2 className="text-lg font-semibold text-text-main">
{title}
</h2>
<h2 className="text-lg font-semibold text-text-main">{title}</h2>
)}
</div>
{showCloseButton && (
<button
onClick={onClose}
className="p-1.5 rounded-lg text-text-muted hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
className="p-1.5 rounded-[10px] text-text-muted hover:bg-surface-2 hover:text-text-main transition-colors"
>
<span className="material-symbols-outlined text-[20px]">close</span>
</button>
@ -94,11 +89,11 @@ export default function Modal({
)}
{/* Body */}
<div className="p-6 max-h-[calc(85vh-100px)] overflow-y-auto">{children}</div>
<div className="p-6 max-h-[calc(85vh-100px)] overflow-y-auto custom-scrollbar">{children}</div>
{/* Footer */}
{footer && (
<div className="flex items-center justify-end gap-3 p-6 border-t border-black/5 dark:border-white/5">
<div className="flex items-center justify-end gap-3 p-6 border-t border-border-subtle">
{footer}
</div>
)}
@ -107,7 +102,6 @@ export default function Modal({
);
}
// Confirm Modal helper
export function ConfirmModal({
isOpen,
onClose,
@ -140,4 +134,3 @@ export function ConfirmModal({
</Modal>
);
}

View file

@ -30,20 +30,20 @@ export default function NineRemotePromoModal({ isOpen, onClose }) {
return createPortal(
<div className="fixed inset-0 z-[9999] flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
<div className="absolute inset-0 bg-black/50 backdrop-blur-[2px] fade-in" onClick={onClose} />
<div className="relative w-full max-w-sm rounded-xl overflow-hidden shadow-2xl animate-in fade-in zoom-in-95 duration-200 flex flex-col bg-surface border border-black/10 dark:border-white/10">
<div className="relative w-full max-w-sm rounded-[14px] overflow-hidden shadow-[var(--shadow-elev)] fade-in flex flex-col bg-surface border border-border-subtle">
{/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-black/5 dark:border-white/5">
<div className="flex items-center justify-between px-5 py-3 border-b border-border-subtle">
<div className="flex items-center gap-3">
<div className="w-7 h-7 rounded-lg flex items-center justify-center" style={{ background: "#FF570A" }}>
<div className="w-7 h-7 rounded-[8px] flex items-center justify-center bg-primary">
<span className="material-symbols-outlined text-white text-base">terminal</span>
</div>
<span className="text-xs font-bold uppercase tracking-wider" style={{ fontFamily: "monospace", color: "#FF570A" }}>9Remote</span>
<span className="text-xs font-bold uppercase tracking-wider text-primary font-mono">9Remote</span>
</div>
<button
onClick={onClose}
className="w-7 h-7 flex items-center justify-center rounded-lg bg-black/5 dark:bg-white/5 border border-black/5 dark:border-white/10 text-text-muted hover:text-text-main transition-colors"
className="p-1.5 rounded-[10px] text-text-muted hover:bg-surface-2 hover:text-text-main transition-colors"
>
<span className="material-symbols-outlined text-base">close</span>
</button>
@ -53,11 +53,8 @@ export default function NineRemotePromoModal({ isOpen, onClose }) {
<div className="px-7 py-7 pb-9 flex flex-col gap-6">
{/* Hero */}
<div className="flex flex-col items-center gap-2 text-center mt-2">
<div
className="w-14 h-14 rounded-2xl flex items-center justify-center mb-1"
style={{ background: "#FF570A", boxShadow: "rgba(255,87,10,0.35) 0px 8px 32px" }}
>
<span className="material-symbols-outlined text-white" style={{ fontSize: 30 }}>terminal</span>
<div className="w-14 h-14 rounded-[14px] flex items-center justify-center mb-1 bg-primary shadow-[var(--shadow-warm)]">
<span className="material-symbols-outlined text-white text-[30px]">terminal</span>
</div>
<h1 className="text-lg font-bold text-text-main tracking-tight">9Remote</h1>
<p className="text-xs text-text-muted leading-5 max-w-[220px]">
@ -68,8 +65,8 @@ export default function NineRemotePromoModal({ isOpen, onClose }) {
{/* Feature cards */}
<div className="flex gap-2 w-full">
{FEATURES.map(({ icon, label, desc }) => (
<div key={label} className="flex-1 flex flex-col items-center gap-1.5 py-4 px-1 rounded-xl border border-black/10 dark:border-white/10 bg-bg-alt">
<span className="material-symbols-outlined" style={{ fontSize: 22, color: "#ff6e33" }}>{icon}</span>
<div key={label} className="flex-1 flex flex-col items-center gap-1.5 py-4 px-1 rounded-[10px] border border-border-subtle bg-surface-2">
<span className="material-symbols-outlined text-primary text-[22px]">{icon}</span>
<p className="text-xs font-semibold text-text-main">{label}</p>
<p className="text-[10px] text-text-muted text-center leading-4">{desc}</p>
</div>
@ -80,7 +77,7 @@ export default function NineRemotePromoModal({ isOpen, onClose }) {
<div className="flex flex-col gap-3 w-full">
{BULLETS.map(({ icon, text }) => (
<div key={icon} className="flex items-center gap-2.5">
<span className="material-symbols-outlined flex-shrink-0" style={{ fontSize: 16, color: "#ff6e33" }}>{icon}</span>
<span className="material-symbols-outlined flex-shrink-0 text-primary text-[16px]">{icon}</span>
<span className="text-xs text-text-muted">{text}</span>
</div>
))}
@ -89,8 +86,7 @@ export default function NineRemotePromoModal({ isOpen, onClose }) {
{/* CTA */}
<button
onClick={() => window.open(NINE_REMOTE_URL, "_blank")}
className="w-full py-3.5 flex items-center justify-center gap-2 text-sm font-semibold text-white rounded-xl hover:opacity-90 active:scale-[0.98] transition-all"
style={{ background: "#FF570A", boxShadow: "0 4px 16px rgba(255,87,10,0.35)" }}
className="w-full py-3 flex items-center justify-center gap-2 text-sm font-semibold text-white rounded-[10px] bg-primary hover:bg-primary-hover shadow-[var(--shadow-warm)] active:scale-[0.98] transition-all"
>
<span className="material-symbols-outlined text-base">open_in_new</span>
Get 9Remote

View file

@ -152,6 +152,10 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
setDeviceData(data);
// Auto-open verification URL in new tab
const verifyUrl = data.verification_uri_complete || data.verification_uri;
if (verifyUrl) window.open(verifyUrl, "_blank", "noopener,noreferrer");
// Pass extraData for Kiro (contains _clientId, _clientSecret)
const extraData = provider === "kiro"
? {

View file

@ -18,8 +18,8 @@ export default function SegmentedControl({
return (
<div
className={cn(
"inline-flex items-center p-1 rounded-lg overflow-x-auto",
"bg-black/5 dark:bg-white/5",
"inline-flex items-center p-1 rounded-[10px] overflow-x-auto",
"bg-surface-2",
className
)}
>
@ -28,10 +28,10 @@ export default function SegmentedControl({
key={option.value}
onClick={() => onChange(option.value)}
className={cn(
"shrink-0 px-4 rounded-md font-medium transition-all",
"shrink-0 px-4 rounded-[8px] font-medium transition-all",
sizes[size],
value === option.value
? "bg-white dark:bg-white/10 text-text-main shadow-sm"
? "bg-surface text-text-main shadow-sm"
: "text-text-muted hover:text-text-main"
)}
>

View file

@ -30,14 +30,12 @@ export default function Select({
onChange={onChange}
disabled={disabled}
className={cn(
"w-full py-2 px-3 pr-10 text-sm text-text-main",
"bg-white dark:bg-white/5 border border-black/10 dark:border-white/10 rounded-md appearance-none",
"focus:ring-1 focus:ring-primary/30 focus:border-primary/50 focus:outline-none",
"transition-all disabled:opacity-50 disabled:cursor-not-allowed",
"w-full py-2.5 px-3 pr-10 text-sm text-text-main",
"bg-surface-2 border border-transparent rounded-[10px] appearance-none",
"focus:outline-none focus:ring-2 focus:ring-brand-500/30 focus:border-brand-500/40",
"transition-all duration-150 disabled:opacity-50 disabled:cursor-not-allowed",
"text-[16px] sm:text-sm",
error
? "border-red-500 focus:border-red-500 focus:ring-red-500/20"
: "",
error && "ring-1 ring-red-500 focus:ring-2 focus:ring-red-500/40 border-red-500/40",
selectClassName
)}
{...props}
@ -67,4 +65,3 @@ export default function Select({
</div>
);
}

View file

@ -123,7 +123,7 @@ export default function Sidebar({ onClose }) {
return (
<>
<aside className="flex w-72 flex-col border-r border-black/5 dark:border-white/5 bg-vibrancy backdrop-blur-xl transition-colors duration-300 min-h-full">
<aside className="flex w-72 flex-col border-r border-border-subtle bg-vibrancy backdrop-blur-xl transition-colors duration-300 min-h-full">
{/* Traffic lights */}
<div className="flex items-center gap-2 px-6 pt-5 pb-2">
<div className="w-3 h-3 rounded-full bg-[#FF5F56]" />
@ -134,7 +134,7 @@ export default function Sidebar({ onClose }) {
{/* Logo */}
<div className="px-6 py-4 flex flex-col gap-2">
<Link href="/dashboard" className="flex items-center gap-3">
<div className="flex items-center justify-center size-9 rounded bg-linear-to-br from-[#f97815] to-[#c2590a]">
<div className="flex items-center justify-center size-9 rounded-[10px] bg-gradient-to-br from-brand-500 to-brand-700 shadow-[var(--shadow-warm)]">
<span className="material-symbols-outlined text-white text-[20px]">hub</span>
</div>
<div className="flex flex-col">
@ -178,10 +178,10 @@ export default function Sidebar({ onClose }) {
href={item.href}
onClick={onClose}
className={cn(
"flex items-center gap-3 px-4 py-2 rounded-lg transition-all group",
"flex items-center gap-3 px-3 py-1.5 rounded-lg transition-all group",
isActive(item.href)
? "bg-primary/10 text-primary"
: "text-text-muted hover:bg-surface/50 hover:text-text-main"
: "text-text-muted hover:bg-surface-2 hover:text-text-main"
)}
>
<span
@ -192,12 +192,12 @@ export default function Sidebar({ onClose }) {
>
{item.icon}
</span>
<span className="text-sm font-medium">{item.label}</span>
<span className="text-[13px] font-medium">{item.label}</span>
</Link>
))}
{/* System section */}
<div className="pt-4 mt-2">
<div className="pt-4 mt-2 space-y-1">
<p className="px-4 text-xs font-semibold text-text-muted/60 uppercase tracking-wider mb-2">
System
</p>
@ -206,14 +206,14 @@ export default function Sidebar({ onClose }) {
<button
onClick={() => setMediaOpen((v) => !v)}
className={cn(
"w-full flex items-center gap-3 px-4 py-2 rounded-lg transition-all group",
"w-full flex items-center gap-3 px-3 py-1.5 rounded-lg transition-all group",
pathname.startsWith("/dashboard/media-providers")
? "bg-primary/10 text-primary"
: "text-text-muted hover:bg-surface/50 hover:text-text-main"
: "text-text-muted hover:bg-surface-2 hover:text-text-main"
)}
>
<span className="material-symbols-outlined text-[18px]">perm_media</span>
<span className="text-sm font-medium flex-1 text-left">Media Providers</span>
<span className="text-[13px] font-medium flex-1 text-left">Media Providers</span>
<span className="material-symbols-outlined text-[14px] transition-transform" style={{ transform: mediaOpen ? "rotate(180deg)" : "rotate(0deg)" }}>
expand_more
</span>
@ -229,7 +229,7 @@ export default function Sidebar({ onClose }) {
"flex items-center gap-3 px-4 py-1.5 rounded-lg transition-all group",
pathname.startsWith(`/dashboard/media-providers/${kind.id}`)
? "bg-primary/10 text-primary"
: "text-text-muted hover:bg-surface/50 hover:text-text-main"
: "text-text-muted hover:bg-surface-2 hover:text-text-main"
)}
>
<span className="material-symbols-outlined text-[16px]">{kind.icon}</span>
@ -244,7 +244,7 @@ export default function Sidebar({ onClose }) {
"flex items-center gap-3 px-4 py-1.5 rounded-lg transition-all group",
pathname.startsWith(COMBINED_WEB_ITEM.href)
? "bg-primary/10 text-primary"
: "text-text-muted hover:bg-surface/50 hover:text-text-main"
: "text-text-muted hover:bg-surface-2 hover:text-text-main"
)}
>
<span className="material-symbols-outlined text-[16px]">{COMBINED_WEB_ITEM.icon}</span>
@ -259,10 +259,10 @@ export default function Sidebar({ onClose }) {
href={item.href}
onClick={onClose}
className={cn(
"flex items-center gap-3 px-4 py-2 rounded-lg transition-all group",
"flex items-center gap-3 px-3 py-1.5 rounded-lg transition-all group",
isActive(item.href)
? "bg-primary/10 text-primary"
: "text-text-muted hover:bg-surface/50 hover:text-text-main"
: "text-text-muted hover:bg-surface-2 hover:text-text-main"
)}
>
<span
@ -273,7 +273,7 @@ export default function Sidebar({ onClose }) {
>
{item.icon}
</span>
<span className="text-sm font-medium">{item.label}</span>
<span className="text-[13px] font-medium">{item.label}</span>
</Link>
))}
@ -286,10 +286,10 @@ export default function Sidebar({ onClose }) {
href={item.href}
onClick={onClose}
className={cn(
"flex items-center gap-3 px-4 py-2 rounded-lg transition-all group",
"flex items-center gap-3 px-3 py-1.5 rounded-lg transition-all group",
isActive(item.href)
? "bg-primary/10 text-primary"
: "text-text-muted hover:bg-surface/50 hover:text-text-main"
: "text-text-muted hover:bg-surface-2 hover:text-text-main"
)}
>
<span
@ -300,7 +300,7 @@ export default function Sidebar({ onClose }) {
>
{item.icon}
</span>
<span className="text-sm font-medium">{item.label}</span>
<span className="text-[13px] font-medium">{item.label}</span>
</Link>
) : null;
})}
@ -310,10 +310,10 @@ export default function Sidebar({ onClose }) {
href="/dashboard/profile"
onClick={onClose}
className={cn(
"flex items-center gap-3 px-4 py-2 rounded-lg transition-all group",
"flex items-center gap-3 px-3 py-1.5 rounded-lg transition-all group",
isActive("/dashboard/profile")
? "bg-primary/10 text-primary"
: "text-text-muted hover:bg-surface/50 hover:text-text-main"
: "text-text-muted hover:bg-surface-2 hover:text-text-main"
)}
>
<span
@ -324,13 +324,13 @@ export default function Sidebar({ onClose }) {
>
settings
</span>
<span className="text-sm font-medium">Settings</span>
<span className="text-[13px] font-medium">Settings</span>
</Link>
</div>
</nav>
{/* Footer section */}
<div className="p-3 border-t border-black/5 dark:border-white/5">
<div className="p-3 border-t border-border-subtle">
{/* Shutdown button */}
<Button
variant="outline"

View file

@ -4,24 +4,20 @@ import { useTheme } from "@/shared/hooks/useTheme";
import { cn } from "@/shared/utils/cn";
export default function ThemeToggle({ className, variant = "default" }) {
const { theme, toggleTheme, isDark } = useTheme();
const { isDark, toggleTheme } = useTheme();
const variants = {
default: cn(
"flex items-center justify-center size-10 rounded-full",
"text-text-muted",
"hover:bg-black/5",
"hover:text-text-main",
"transition-colors"
"text-text-muted hover:text-text-main",
"hover:bg-surface-2 transition-colors"
),
card: cn(
"flex items-center justify-center size-11 rounded-full",
"bg-surface/60",
"hover:bg-surface",
"bg-surface/60 hover:bg-surface",
"border border-border",
"backdrop-blur-md shadow-sm hover:shadow-md",
"text-text-muted-light hover:text-primary",
"hover:text-primary",
"backdrop-blur-md shadow-sm hover:shadow-[var(--shadow-warm)]",
"text-text-muted hover:text-brand-500",
"transition-all group"
),
};
@ -44,4 +40,3 @@ export default function ThemeToggle({ className, variant = "default" }) {
</button>
);
}

View file

@ -12,27 +12,13 @@ export default function Toggle({
className,
}) {
const sizes = {
sm: {
track: "w-8 h-4",
thumb: "size-3",
translate: "translate-x-4",
},
md: {
track: "w-11 h-6",
thumb: "size-5",
translate: "translate-x-5",
},
lg: {
track: "w-14 h-7",
thumb: "size-6",
translate: "translate-x-7",
},
sm: { track: "w-8 h-4", thumb: "size-3", translate: "translate-x-4" },
md: { track: "w-11 h-6", thumb: "size-5", translate: "translate-x-5" },
lg: { track: "w-14 h-7", thumb: "size-6", translate: "translate-x-7" },
};
const handleClick = () => {
if (!disabled && onChange) {
onChange(!checked);
}
if (!disabled && onChange) onChange(!checked);
};
return (
@ -52,10 +38,8 @@ export default function Toggle({
className={cn(
"relative inline-flex shrink-0 cursor-pointer rounded-full",
"transition-colors duration-200 ease-in-out",
"focus:outline-none focus:ring-1 focus:ring-primary/30",
checked
? "bg-primary"
: "bg-black/10 dark:bg-white/20",
"focus:outline-none focus:ring-2 focus:ring-brand-500/30",
checked ? "bg-brand-500" : "bg-surface-3",
sizes[size].track,
disabled && "cursor-not-allowed"
)}
@ -73,18 +57,13 @@ export default function Toggle({
{(label || description) && (
<div className="flex flex-col">
{label && (
<span className="text-sm font-medium text-text-main">
{label}
</span>
<span className="text-sm font-medium text-text-main">{label}</span>
)}
{description && (
<span className="text-xs text-text-muted">
{description}
</span>
<span className="text-xs text-text-muted">{description}</span>
)}
</div>
)}
</div>
);
}

View file

@ -42,7 +42,7 @@ function RecentRequests({ requests = [] }) {
<div className="flex-1 flex items-center justify-center text-text-muted text-sm">No requests yet.</div>
) : (
<div className="flex-1 overflow-y-auto">
<table className="w-full min-w-[420px] border-collapse text-xs">
<table className="w-full min-w-[300px] border-collapse text-xs">
<thead className="sticky top-0 bg-bg z-10">
<tr className="border-b border-border">
<th className="py-1.5 text-left font-semibold text-text-muted w-2"></th>
@ -181,7 +181,7 @@ const PERIODS = [
{ value: "60d", label: "60D" },
];
export default function UsageStats() {
export default function UsageStats({ period: periodProp, setPeriod: setPeriodProp, hidePeriodSelector = false } = {}) {
const router = useRouter();
const searchParams = useSearchParams();
@ -194,7 +194,9 @@ export default function UsageStats() {
const [tableView, setTableView] = useState("model");
const [viewMode, setViewMode] = useState("costs");
const [providers, setProviders] = useState([]);
const [period, setPeriod] = useState("7d");
const [periodLocal, setPeriodLocal] = useState("7d");
const period = periodProp ?? periodLocal;
const setPeriod = setPeriodProp ?? setPeriodLocal;
// Fetch connected providers once, deduplicate by provider type
// Always include noAuth free providers (e.g. opencode) regardless of connections
@ -398,24 +400,26 @@ export default function UsageStats() {
return (
<div className="flex min-w-0 flex-col gap-6">
{/* Period selector */}
<div className="flex w-full items-center gap-2 sm:w-auto sm:self-end">
<div className="grid flex-1 grid-cols-4 items-center gap-1 rounded-lg border border-border bg-bg-subtle p-1 sm:flex sm:flex-none">
{PERIODS.map((p) => (
<button
key={p.value}
onClick={() => setPeriod(p.value)}
disabled={fetching}
className={`rounded-md px-3 py-1 text-sm font-medium transition-colors ${period === p.value ? "bg-primary text-white shadow-sm" : "text-text-muted hover:bg-bg-hover hover:text-text"}`}
>
{p.label}
</button>
))}
{/* Period selector (hidden when controlled by parent) */}
{!hidePeriodSelector && (
<div className="flex w-full items-center gap-2 sm:w-auto sm:self-end">
<div className="grid flex-1 grid-cols-4 items-center gap-1 rounded-lg border border-border bg-bg-subtle p-1 sm:flex sm:flex-none">
{PERIODS.map((p) => (
<button
key={p.value}
onClick={() => setPeriod(p.value)}
disabled={fetching}
className={`rounded-md px-3 py-1 text-sm font-medium transition-colors ${period === p.value ? "bg-primary text-white shadow-sm" : "text-text-muted hover:bg-bg-hover hover:text-text"}`}
>
{p.label}
</button>
))}
</div>
{fetching && (
<span className="material-symbols-outlined text-[16px] text-text-muted animate-spin">progress_activity</span>
)}
</div>
{fetching && (
<span className="material-symbols-outlined text-[16px] text-text-muted animate-spin">progress_activity</span>
)}
</div>
)}
{/* Overview cards */}
{loading ? spinner : <OverviewCards stats={stats} />}

View file

@ -91,7 +91,9 @@ export default function DashboardLayout({ children }) {
</div>
{/* Main content */}
<main className="flex flex-col flex-1 h-full min-w-0 relative transition-colors duration-300">
<main className="flex flex-col flex-1 h-full min-w-0 relative transition-colors duration-300 isolate">
{/* Faint grid background */}
<div className="landing-grid absolute inset-0 pointer-events-none -z-10" aria-hidden="true" />
<Header key={pathname} onMenuClick={() => setSidebarOpen(true)} />
<div className={`flex-1 overflow-y-auto custom-scrollbar ${pathname === "/dashboard/basic-chat" ? "" : "p-6 lg:p-10"} ${pathname === "/dashboard/basic-chat" ? "flex flex-col overflow-hidden" : ""}`}>
<div className={`${pathname === "/dashboard/basic-chat" ? "flex-1 w-full h-full flex flex-col" : "max-w-7xl mx-auto"}`}>{children}</div>