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:
parent
1686adc704
commit
6cdf40b44e
40 changed files with 1029 additions and 762 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
? {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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} />}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue