mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 14:22:28 +08:00
refactor: redesign config panels with refined minimal aesthetic (#384)
- Add CSS design system tokens (surfaces, borders, animations) to globals.css - Update dialog.tsx with rounded-2xl, shadow-dialog, refined close button - Enhance input.tsx with rounded-xl and refined focus states - Refactor settings-dialog with SettingItem pattern and consistent control sizing - Refactor model-config-dialog with ConfigSection/ConfigCard helpers - Replace emerald-* classes with success design tokens - Remove unused ValidationButton component and scrollState
This commit is contained in:
142
app/globals.css
142
app/globals.css
@@ -144,6 +144,68 @@
|
|||||||
--sidebar-ring: oklch(0.7 0.16 265);
|
--sidebar-ring: oklch(0.7 0.16 265);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
REFINED MINIMAL DESIGN SYSTEM
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Surface layers for depth */
|
||||||
|
--surface-0: oklch(1 0 0);
|
||||||
|
--surface-1: oklch(0.985 0.002 240);
|
||||||
|
--surface-2: oklch(0.97 0.004 240);
|
||||||
|
--surface-elevated: oklch(1 0 0);
|
||||||
|
|
||||||
|
/* Subtle borders */
|
||||||
|
--border-subtle: oklch(0.94 0.008 260);
|
||||||
|
--border-default: oklch(0.91 0.012 260);
|
||||||
|
|
||||||
|
/* Interactive states */
|
||||||
|
--interactive-hover: oklch(0.96 0.015 260);
|
||||||
|
--interactive-active: oklch(0.93 0.02 265);
|
||||||
|
|
||||||
|
/* Success state */
|
||||||
|
--success: oklch(0.65 0.18 145);
|
||||||
|
--success-muted: oklch(0.95 0.03 145);
|
||||||
|
|
||||||
|
/* Animation timing */
|
||||||
|
--duration-fast: 120ms;
|
||||||
|
--duration-normal: 200ms;
|
||||||
|
--duration-slow: 300ms;
|
||||||
|
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--surface-0: oklch(0.15 0.015 260);
|
||||||
|
--surface-1: oklch(0.18 0.015 260);
|
||||||
|
--surface-2: oklch(0.22 0.015 260);
|
||||||
|
--surface-elevated: oklch(0.25 0.015 260);
|
||||||
|
|
||||||
|
--border-subtle: oklch(0.25 0.012 260);
|
||||||
|
--border-default: oklch(0.3 0.015 260);
|
||||||
|
|
||||||
|
--interactive-hover: oklch(0.25 0.02 265);
|
||||||
|
--interactive-active: oklch(0.3 0.025 270);
|
||||||
|
|
||||||
|
--success: oklch(0.7 0.16 145);
|
||||||
|
--success-muted: oklch(0.25 0.04 145);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Expose surface colors to Tailwind */
|
||||||
|
@theme inline {
|
||||||
|
--color-surface-0: var(--surface-0);
|
||||||
|
--color-surface-1: var(--surface-1);
|
||||||
|
--color-surface-2: var(--surface-2);
|
||||||
|
--color-surface-elevated: var(--surface-elevated);
|
||||||
|
--color-border-subtle: var(--border-subtle);
|
||||||
|
--color-border-default: var(--border-default);
|
||||||
|
--color-interactive-hover: var(--interactive-hover);
|
||||||
|
--color-interactive-active: var(--interactive-active);
|
||||||
|
--color-success: var(--success);
|
||||||
|
--color-success-muted: var(--success-muted);
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
@@ -257,3 +319,83 @@
|
|||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
REFINED DIALOG STYLES
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Refined dialog shadow - multi-layer soft shadow */
|
||||||
|
.shadow-dialog {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px oklch(0 0 0 / 0.03),
|
||||||
|
0 2px 4px oklch(0 0 0 / 0.02),
|
||||||
|
0 12px 24px oklch(0 0 0 / 0.06),
|
||||||
|
0 24px 48px oklch(0 0 0 / 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .shadow-dialog {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px oklch(1 0 0 / 0.05),
|
||||||
|
0 2px 4px oklch(0 0 0 / 0.2),
|
||||||
|
0 12px 24px oklch(0 0 0 / 0.3),
|
||||||
|
0 24px 48px oklch(0 0 0 / 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dialog animations */
|
||||||
|
@keyframes dialog-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -48%) scale(0.96);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dialog-out {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -48%) scale(0.96);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-dialog-in {
|
||||||
|
animation: dialog-in var(--duration-normal) var(--ease-out) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-dialog-out {
|
||||||
|
animation: dialog-out 150ms var(--ease-out) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check pop animation for validation success */
|
||||||
|
@keyframes check-pop {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.8);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-check-pop {
|
||||||
|
animation: check-pop 0.25s var(--ease-spring) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduced motion support */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.animate-dialog-in,
|
||||||
|
.animate-dialog-out,
|
||||||
|
.animate-check-pop {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -103,41 +103,40 @@ function ProviderLogo({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reusable validation button component
|
// Configuration section with title and optional action
|
||||||
function ValidationButton({
|
function ConfigSection({
|
||||||
status,
|
title,
|
||||||
onClick,
|
icon: Icon,
|
||||||
disabled,
|
action,
|
||||||
dict,
|
children,
|
||||||
}: {
|
}: {
|
||||||
status: ValidationStatus
|
title: string
|
||||||
onClick: () => void
|
icon: React.ComponentType<{ className?: string }>
|
||||||
disabled: boolean
|
action?: React.ReactNode
|
||||||
dict: ReturnType<typeof useDictionary>
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<div className="space-y-4">
|
||||||
variant={status === "success" ? "outline" : "default"}
|
<div className="flex items-center justify-between">
|
||||||
size="sm"
|
<div className="flex items-center gap-2">
|
||||||
onClick={onClick}
|
<Icon className="h-4 w-4 text-muted-foreground" />
|
||||||
disabled={disabled}
|
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
className={cn(
|
{title}
|
||||||
"h-9 px-4 min-w-[80px]",
|
</span>
|
||||||
status === "success" &&
|
</div>
|
||||||
"text-emerald-600 border-emerald-200 dark:border-emerald-800",
|
{action}
|
||||||
)}
|
</div>
|
||||||
>
|
{children}
|
||||||
{status === "validating" ? (
|
</div>
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
)
|
||||||
) : status === "success" ? (
|
}
|
||||||
<>
|
|
||||||
<Check className="h-4 w-4 mr-1.5" />
|
// Card wrapper with subtle depth
|
||||||
{dict.modelConfig.verified}
|
function ConfigCard({ children }: { children: React.ReactNode }) {
|
||||||
</>
|
return (
|
||||||
) : (
|
<div className="rounded-2xl border border-border-subtle bg-surface-2/50 p-5 space-y-5">
|
||||||
dict.modelConfig.test
|
{children}
|
||||||
)}
|
</div>
|
||||||
</Button>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +153,6 @@ export function ModelConfigDialog({
|
|||||||
const [validationStatus, setValidationStatus] =
|
const [validationStatus, setValidationStatus] =
|
||||||
useState<ValidationStatus>("idle")
|
useState<ValidationStatus>("idle")
|
||||||
const [validationError, setValidationError] = useState<string>("")
|
const [validationError, setValidationError] = useState<string>("")
|
||||||
const [scrollState, setScrollState] = useState({ top: false, bottom: true })
|
|
||||||
const [customModelInput, setCustomModelInput] = useState("")
|
const [customModelInput, setCustomModelInput] = useState("")
|
||||||
const scrollRef = useRef<HTMLDivElement>(null)
|
const scrollRef = useRef<HTMLDivElement>(null)
|
||||||
const validationResetTimeoutRef = useRef<ReturnType<
|
const validationResetTimeoutRef = useRef<ReturnType<
|
||||||
@@ -186,26 +184,6 @@ export function ModelConfigDialog({
|
|||||||
(p) => p.id === selectedProviderId,
|
(p) => p.id === selectedProviderId,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Track scroll position for gradient shadows
|
|
||||||
useEffect(() => {
|
|
||||||
const scrollEl = scrollRef.current?.querySelector(
|
|
||||||
"[data-radix-scroll-area-viewport]",
|
|
||||||
) as HTMLElement | null
|
|
||||||
if (!scrollEl) return
|
|
||||||
|
|
||||||
const handleScroll = () => {
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = scrollEl
|
|
||||||
setScrollState({
|
|
||||||
top: scrollTop > 10,
|
|
||||||
bottom: scrollTop < scrollHeight - clientHeight - 10,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleScroll() // Initial check
|
|
||||||
scrollEl.addEventListener("scroll", handleScroll)
|
|
||||||
return () => scrollEl.removeEventListener("scroll", handleScroll)
|
|
||||||
}, [selectedProvider])
|
|
||||||
|
|
||||||
// Cleanup validation reset timeout on unmount
|
// Cleanup validation reset timeout on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -390,34 +368,35 @@ export function ModelConfigDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="sm:max-w-3xl h-[75vh] max-h-[700px] overflow-hidden flex flex-col gap-0 p-0">
|
<DialogContent className="sm:max-w-4xl h-[80vh] max-h-[800px] overflow-hidden flex flex-col gap-0 p-0">
|
||||||
<DialogHeader className="px-6 pt-6 pb-4 border-b bg-gradient-to-r from-primary/5 via-primary/3 to-transparent">
|
{/* Header */}
|
||||||
<DialogTitle className="flex items-center gap-2.5 text-xl font-semibold">
|
<DialogHeader className="px-6 pt-6 pb-4 shrink-0">
|
||||||
<div className="p-1.5 rounded-lg bg-primary/10">
|
<DialogTitle className="flex items-center gap-3">
|
||||||
|
<div className="p-2 rounded-xl bg-surface-2">
|
||||||
<Server className="h-5 w-5 text-primary" />
|
<Server className="h-5 w-5 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
{dict.modelConfig?.title || "AI Model Configuration"}
|
{dict.modelConfig?.title || "AI Model Configuration"}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-sm">
|
<DialogDescription className="mt-1">
|
||||||
{dict.modelConfig?.description ||
|
{dict.modelConfig?.description ||
|
||||||
"Configure multiple AI providers and models for your workspace"}
|
"Configure multiple AI providers and models for your workspace"}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="flex flex-1 min-h-0 overflow-hidden">
|
<div className="flex flex-1 min-h-0 overflow-hidden border-t border-border-subtle">
|
||||||
{/* Provider List (Left Sidebar) */}
|
{/* Provider List (Left Sidebar) */}
|
||||||
<div className="w-56 flex-shrink-0 flex flex-col border-r bg-muted/20">
|
<div className="w-60 shrink-0 flex flex-col bg-surface-1/50 border-r border-border-subtle">
|
||||||
<div className="px-4 py-3 border-b">
|
<div className="px-4 py-3">
|
||||||
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
{dict.modelConfig.providers}
|
{dict.modelConfig.providers}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ScrollArea className="flex-1">
|
<ScrollArea className="flex-1 px-2">
|
||||||
<div className="p-2">
|
<div className="space-y-1 pb-2">
|
||||||
{config.providers.length === 0 ? (
|
{config.providers.length === 0 ? (
|
||||||
<div className="px-3 py-8 text-center">
|
<div className="px-3 py-8 text-center">
|
||||||
<div className="inline-flex items-center justify-center w-10 h-10 rounded-full bg-muted mb-3">
|
<div className="inline-flex items-center justify-center w-10 h-10 rounded-full bg-surface-2 mb-3">
|
||||||
<Plus className="h-5 w-5 text-muted-foreground" />
|
<Plus className="h-5 w-5 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
@@ -425,8 +404,7 @@ export function ModelConfigDialog({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-1">
|
config.providers.map((provider) => (
|
||||||
{config.providers.map((provider) => (
|
|
||||||
<button
|
<button
|
||||||
key={provider.id}
|
key={provider.id}
|
||||||
type="button"
|
type="button"
|
||||||
@@ -442,29 +420,42 @@ export function ModelConfigDialog({
|
|||||||
setShowApiKey(false)
|
setShowApiKey(false)
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group flex items-center gap-3 px-3 py-2.5 rounded-lg text-left text-sm transition-all duration-150 hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
"group flex items-center gap-3 px-3 py-2.5 rounded-xl w-full",
|
||||||
|
"text-left text-sm transition-all duration-150",
|
||||||
|
"hover:bg-interactive-hover",
|
||||||
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
selectedProviderId ===
|
selectedProviderId ===
|
||||||
provider.id &&
|
provider.id &&
|
||||||
"bg-background shadow-sm ring-1 ring-border",
|
"bg-surface-0 shadow-sm ring-1 ring-border-subtle",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-8 h-8 rounded-lg flex items-center justify-center",
|
||||||
|
"bg-surface-2 transition-colors duration-150",
|
||||||
|
selectedProviderId ===
|
||||||
|
provider.id &&
|
||||||
|
"bg-primary/10",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ProviderLogo
|
<ProviderLogo
|
||||||
provider={provider.provider}
|
provider={provider.provider}
|
||||||
className="flex-shrink-0"
|
className="flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<span className="flex-1 truncate font-medium">
|
<span className="flex-1 truncate font-medium">
|
||||||
{getProviderDisplayName(
|
{getProviderDisplayName(
|
||||||
provider,
|
provider,
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{provider.validated ? (
|
{provider.validated ? (
|
||||||
<div className="flex-shrink-0 flex items-center justify-center w-5 h-5 rounded-full bg-emerald-500/10">
|
<div className="flex-shrink-0 flex items-center justify-center w-5 h-5 rounded-full bg-success-muted">
|
||||||
<Check className="h-3 w-3 text-emerald-500" />
|
<Check className="h-3 w-3 text-success" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ChevronRight
|
<ChevronRight
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-4 w-4 text-muted-foreground/50 transition-transform",
|
"h-4 w-4 text-muted-foreground/50 transition-transform duration-150",
|
||||||
selectedProviderId ===
|
selectedProviderId ===
|
||||||
provider.id &&
|
provider.id &&
|
||||||
"translate-x-0.5",
|
"translate-x-0.5",
|
||||||
@@ -472,20 +463,19 @@ export function ModelConfigDialog({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
{/* Add Provider */}
|
{/* Add Provider */}
|
||||||
<div className="p-2 border-t">
|
<div className="p-3 border-t border-border-subtle">
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(v) =>
|
onValueChange={(v) =>
|
||||||
handleAddProvider(v as ProviderName)
|
handleAddProvider(v as ProviderName)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-9 bg-background hover:bg-accent">
|
<SelectTrigger className="w-full h-9 rounded-xl bg-surface-0 border-border-subtle hover:bg-interactive-hover">
|
||||||
<Plus className="h-4 w-4 mr-2 text-muted-foreground" />
|
<Plus className="h-4 w-4 mr-2 text-muted-foreground" />
|
||||||
<SelectValue
|
<SelectValue
|
||||||
placeholder={
|
placeholder={
|
||||||
@@ -514,41 +504,23 @@ export function ModelConfigDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Provider Details (Right Panel) */}
|
{/* Provider Details (Right Panel) */}
|
||||||
<div className="flex-1 min-w-0 overflow-hidden relative">
|
<div className="flex-1 min-w-0 flex flex-col overflow-hidden">
|
||||||
{selectedProvider ? (
|
{selectedProvider ? (
|
||||||
<>
|
<>
|
||||||
{/* Top gradient shadow */}
|
<ScrollArea className="flex-1" ref={scrollRef}>
|
||||||
<div
|
<div className="p-6 space-y-8">
|
||||||
className={cn(
|
|
||||||
"absolute top-0 left-0 right-0 h-8 bg-gradient-to-b from-background to-transparent z-10 pointer-events-none transition-opacity duration-200",
|
|
||||||
scrollState.top
|
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{/* Bottom gradient shadow */}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"absolute bottom-0 left-0 right-0 h-8 bg-gradient-to-t from-background to-transparent z-10 pointer-events-none transition-opacity duration-200",
|
|
||||||
scrollState.bottom
|
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<ScrollArea className="h-full" ref={scrollRef}>
|
|
||||||
<div className="p-6 space-y-6">
|
|
||||||
{/* Provider Header */}
|
{/* Provider Header */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-muted">
|
<div className="flex items-center justify-center w-12 h-12 rounded-xl bg-surface-2">
|
||||||
<ProviderLogo
|
<ProviderLogo
|
||||||
provider={
|
provider={
|
||||||
selectedProvider.provider
|
selectedProvider.provider
|
||||||
}
|
}
|
||||||
className="h-5 w-5"
|
className="h-6 w-6"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h3 className="font-semibold text-base">
|
<h3 className="font-semibold text-lg tracking-tight">
|
||||||
{
|
{
|
||||||
PROVIDER_INFO[
|
PROVIDER_INFO[
|
||||||
selectedProvider
|
selectedProvider
|
||||||
@@ -556,7 +528,7 @@ export function ModelConfigDialog({
|
|||||||
].label
|
].label
|
||||||
}
|
}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{selectedProvider.models
|
{selectedProvider.models
|
||||||
.length === 0
|
.length === 0
|
||||||
? dict.modelConfig
|
? dict.modelConfig
|
||||||
@@ -573,8 +545,8 @@ export function ModelConfigDialog({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{selectedProvider.validated && (
|
{selectedProvider.validated && (
|
||||||
<div className="flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-emerald-500/10 text-emerald-600 dark:text-emerald-400">
|
<div className="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-success-muted text-success">
|
||||||
<Check className="h-3.5 w-3.5" />
|
<Check className="h-3.5 w-3.5 animate-check-pop" />
|
||||||
<span className="text-xs font-medium">
|
<span className="text-xs font-medium">
|
||||||
{
|
{
|
||||||
dict.modelConfig
|
dict.modelConfig
|
||||||
@@ -586,18 +558,13 @@ export function ModelConfigDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Configuration Section */}
|
{/* Configuration Section */}
|
||||||
<div className="space-y-4">
|
<ConfigSection
|
||||||
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
|
title={
|
||||||
<Settings2 className="h-4 w-4" />
|
dict.modelConfig.configuration
|
||||||
<span>
|
|
||||||
{
|
|
||||||
dict.modelConfig
|
|
||||||
.configuration
|
|
||||||
}
|
}
|
||||||
</span>
|
icon={Settings2}
|
||||||
</div>
|
>
|
||||||
|
<ConfigCard>
|
||||||
<div className="rounded-xl border bg-card p-4 space-y-4">
|
|
||||||
{/* Display Name */}
|
{/* Display Name */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label
|
<Label
|
||||||
@@ -856,7 +823,7 @@ export function ModelConfigDialog({
|
|||||||
"h-9 px-4",
|
"h-9 px-4",
|
||||||
validationStatus ===
|
validationStatus ===
|
||||||
"success" &&
|
"success" &&
|
||||||
"text-emerald-600 border-emerald-200 dark:border-emerald-800",
|
"text-success border-success/30 bg-success-muted hover:bg-success-muted",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{validationStatus ===
|
{validationStatus ===
|
||||||
@@ -865,7 +832,7 @@ export function ModelConfigDialog({
|
|||||||
) : validationStatus ===
|
) : validationStatus ===
|
||||||
"success" ? (
|
"success" ? (
|
||||||
<>
|
<>
|
||||||
<Check className="h-4 w-4 mr-1.5" />
|
<Check className="h-4 w-4 mr-1.5 animate-check-pop" />
|
||||||
{
|
{
|
||||||
dict
|
dict
|
||||||
.modelConfig
|
.modelConfig
|
||||||
@@ -975,7 +942,7 @@ export function ModelConfigDialog({
|
|||||||
"h-9 px-4",
|
"h-9 px-4",
|
||||||
validationStatus ===
|
validationStatus ===
|
||||||
"success" &&
|
"success" &&
|
||||||
"text-emerald-600 border-emerald-200 dark:border-emerald-800",
|
"text-success border-success/30 bg-success-muted hover:bg-success-muted",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{validationStatus ===
|
{validationStatus ===
|
||||||
@@ -984,7 +951,7 @@ export function ModelConfigDialog({
|
|||||||
) : validationStatus ===
|
) : validationStatus ===
|
||||||
"success" ? (
|
"success" ? (
|
||||||
<>
|
<>
|
||||||
<Check className="h-4 w-4 mr-1.5" />
|
<Check className="h-4 w-4 mr-1.5 animate-check-pop" />
|
||||||
{
|
{
|
||||||
dict
|
dict
|
||||||
.modelConfig
|
.modelConfig
|
||||||
@@ -1053,26 +1020,19 @@ export function ModelConfigDialog({
|
|||||||
.modelConfig
|
.modelConfig
|
||||||
.customEndpoint
|
.customEndpoint
|
||||||
}
|
}
|
||||||
className="h-9 font-mono text-xs"
|
className="h-9 rounded-xl font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</ConfigCard>
|
||||||
</div>
|
</ConfigSection>
|
||||||
|
|
||||||
{/* Models Section */}
|
{/* Models Section */}
|
||||||
<div className="space-y-4">
|
<ConfigSection
|
||||||
<div className="flex items-center justify-between">
|
title={dict.modelConfig.models}
|
||||||
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
|
icon={Sparkles}
|
||||||
<Sparkles className="h-4 w-4" />
|
action={
|
||||||
<span>
|
|
||||||
{
|
|
||||||
dict.modelConfig
|
|
||||||
.models
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
@@ -1088,7 +1048,6 @@ export function ModelConfigDialog({
|
|||||||
e.target
|
e.target
|
||||||
.value,
|
.value,
|
||||||
)
|
)
|
||||||
// Clear duplicate error when typing
|
|
||||||
if (
|
if (
|
||||||
duplicateError
|
duplicateError
|
||||||
) {
|
) {
|
||||||
@@ -1117,12 +1076,11 @@ export function ModelConfigDialog({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-8 w-48 font-mono text-xs",
|
"h-8 w-44 rounded-lg font-mono text-xs",
|
||||||
duplicateError &&
|
duplicateError &&
|
||||||
"border-destructive focus-visible:ring-destructive",
|
"border-destructive focus-visible:ring-destructive",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{/* Show duplicate error for custom model input */}
|
|
||||||
{duplicateError && (
|
{duplicateError && (
|
||||||
<p className="absolute top-full left-0 mt-1 text-[11px] text-destructive">
|
<p className="absolute top-full left-0 mt-1 text-[11px] text-destructive">
|
||||||
{duplicateError}
|
{duplicateError}
|
||||||
@@ -1132,7 +1090,7 @@ export function ModelConfigDialog({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8"
|
className="h-8 rounded-lg"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (
|
if (
|
||||||
customModelInput.trim()
|
customModelInput.trim()
|
||||||
@@ -1169,7 +1127,7 @@ export function ModelConfigDialog({
|
|||||||
0
|
0
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-32 h-8 hover:bg-accent">
|
<SelectTrigger className="w-28 h-8 rounded-lg hover:bg-interactive-hover">
|
||||||
<span className="text-xs">
|
<span className="text-xs">
|
||||||
{availableSuggestions.length ===
|
{availableSuggestions.length ===
|
||||||
0
|
0
|
||||||
@@ -1202,14 +1160,14 @@ export function ModelConfigDialog({
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
|
>
|
||||||
{/* Model List */}
|
{/* Model List */}
|
||||||
<div className="rounded-xl border bg-card overflow-hidden min-h-[120px]">
|
<div className="rounded-2xl border border-border-subtle bg-surface-2/30 overflow-hidden min-h-[120px]">
|
||||||
{selectedProvider.models
|
{selectedProvider.models
|
||||||
.length === 0 ? (
|
.length === 0 ? (
|
||||||
<div className="p-4 text-center h-full flex flex-col items-center justify-center">
|
<div className="p-6 text-center h-full flex flex-col items-center justify-center">
|
||||||
<div className="inline-flex items-center justify-center w-10 h-10 rounded-full bg-muted mb-2">
|
<div className="inline-flex items-center justify-center w-10 h-10 rounded-full bg-surface-2 mb-3">
|
||||||
<Sparkles className="h-5 w-5 text-muted-foreground" />
|
<Sparkles className="h-5 w-5 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
@@ -1220,7 +1178,7 @@ export function ModelConfigDialog({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y">
|
<div className="divide-y divide-border-subtle">
|
||||||
{selectedProvider.models.map(
|
{selectedProvider.models.map(
|
||||||
(model, index) => (
|
(model, index) => (
|
||||||
<div
|
<div
|
||||||
@@ -1228,16 +1186,7 @@ export function ModelConfigDialog({
|
|||||||
model.id
|
model.id
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
"transition-colors hover:bg-muted/30",
|
"transition-colors duration-150 hover:bg-interactive-hover/50",
|
||||||
index ===
|
|
||||||
0 &&
|
|
||||||
"rounded-t-xl",
|
|
||||||
index ===
|
|
||||||
selectedProvider
|
|
||||||
.models
|
|
||||||
.length -
|
|
||||||
1 &&
|
|
||||||
"rounded-b-xl",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 p-3 min-w-0">
|
<div className="flex items-center gap-3 p-3 min-w-0">
|
||||||
@@ -1264,8 +1213,8 @@ export function ModelConfigDialog({
|
|||||||
) : model.validated ===
|
) : model.validated ===
|
||||||
true ? (
|
true ? (
|
||||||
// Valid
|
// Valid
|
||||||
<div className="w-full h-full rounded-lg bg-emerald-500/10 flex items-center justify-center">
|
<div className="w-full h-full rounded-lg bg-success-muted flex items-center justify-center">
|
||||||
<Check className="h-4 w-4 text-emerald-500" />
|
<Check className="h-4 w-4 text-success" />
|
||||||
</div>
|
</div>
|
||||||
) : model.validated ===
|
) : model.validated ===
|
||||||
false ? (
|
false ? (
|
||||||
@@ -1466,7 +1415,7 @@ export function ModelConfigDialog({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ConfigSection>
|
||||||
|
|
||||||
{/* Danger Zone */}
|
{/* Danger Zone */}
|
||||||
<div className="pt-4">
|
<div className="pt-4">
|
||||||
@@ -1476,7 +1425,7 @@ export function ModelConfigDialog({
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
setDeleteConfirmOpen(true)
|
setDeleteConfirmOpen(true)
|
||||||
}
|
}
|
||||||
className="text-muted-foreground hover:text-destructive hover:bg-destructive/10"
|
className="text-muted-foreground hover:text-destructive hover:bg-destructive/5 rounded-xl"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4 mr-2" />
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
{
|
{
|
||||||
@@ -1490,10 +1439,10 @@ export function ModelConfigDialog({
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full flex flex-col items-center justify-center p-8 text-center">
|
<div className="h-full flex flex-col items-center justify-center p-8 text-center">
|
||||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-gradient-to-br from-primary/10 to-primary/5 mb-4">
|
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-surface-2 mb-4">
|
||||||
<Server className="h-8 w-8 text-primary/60" />
|
<Server className="h-8 w-8 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-semibold mb-1">
|
<h3 className="font-semibold text-lg tracking-tight mb-1">
|
||||||
{dict.modelConfig.configureProviders}
|
{dict.modelConfig.configureProviders}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground max-w-xs">
|
<p className="text-sm text-muted-foreground max-w-xs">
|
||||||
@@ -1505,7 +1454,7 @@ export function ModelConfigDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="px-6 py-3 border-t bg-muted/20">
|
<div className="px-6 py-3 border-t border-border-subtle bg-surface-1/30 shrink-0">
|
||||||
<p className="text-xs text-muted-foreground text-center flex items-center justify-center gap-1.5">
|
<p className="text-xs text-muted-foreground text-center flex items-center justify-center gap-1.5">
|
||||||
<Key className="h-3 w-3" />
|
<Key className="h-3 w-3" />
|
||||||
{dict.modelConfig.apiKeyStored}
|
{dict.modelConfig.apiKeyStored}
|
||||||
|
|||||||
@@ -24,6 +24,32 @@ import { Switch } from "@/components/ui/switch"
|
|||||||
import { useDictionary } from "@/hooks/use-dictionary"
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
import { getApiEndpoint } from "@/lib/base-path"
|
import { getApiEndpoint } from "@/lib/base-path"
|
||||||
import { i18n, type Locale } from "@/lib/i18n/config"
|
import { i18n, type Locale } from "@/lib/i18n/config"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
// Reusable setting item component for consistent layout
|
||||||
|
function SettingItem({
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
label: string
|
||||||
|
description?: string
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between py-4 first:pt-0 last:pb-0">
|
||||||
|
<div className="space-y-0.5 pr-4">
|
||||||
|
<Label className="text-sm font-medium">{label}</Label>
|
||||||
|
{description && (
|
||||||
|
<p className="text-xs text-muted-foreground max-w-[260px]">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="shrink-0">{children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const LANGUAGE_LABELS: Record<Locale, string> = {
|
const LANGUAGE_LABELS: Record<Locale, string> = {
|
||||||
en: "English",
|
en: "English",
|
||||||
@@ -177,60 +203,76 @@ function SettingsContent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-lg p-0 gap-0">
|
||||||
<DialogHeader>
|
{/* Header */}
|
||||||
|
<DialogHeader className="px-6 pt-6 pb-4">
|
||||||
<DialogTitle>{dict.settings.title}</DialogTitle>
|
<DialogTitle>{dict.settings.title}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription className="mt-1">
|
||||||
{dict.settings.description}
|
{dict.settings.description}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 py-2">
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="px-6 pb-6">
|
||||||
|
<div className="divide-y divide-border-subtle">
|
||||||
|
{/* Access Code (conditional) */}
|
||||||
{accessCodeRequired && (
|
{accessCodeRequired && (
|
||||||
<div className="space-y-2">
|
<div className="py-4 first:pt-0 space-y-3">
|
||||||
<Label htmlFor="access-code">
|
<div className="space-y-0.5">
|
||||||
|
<Label
|
||||||
|
htmlFor="access-code"
|
||||||
|
className="text-sm font-medium"
|
||||||
|
>
|
||||||
{dict.settings.accessCode}
|
{dict.settings.accessCode}
|
||||||
</Label>
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{dict.settings.accessCodeDescription}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
id="access-code"
|
id="access-code"
|
||||||
type="password"
|
type="password"
|
||||||
value={accessCode}
|
value={accessCode}
|
||||||
onChange={(e) => setAccessCode(e.target.value)}
|
onChange={(e) =>
|
||||||
|
setAccessCode(e.target.value)
|
||||||
|
}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder={
|
placeholder={
|
||||||
dict.settings.accessCodePlaceholder
|
dict.settings.accessCodePlaceholder
|
||||||
}
|
}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
className="h-9"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={isVerifying || !accessCode.trim()}
|
disabled={isVerifying || !accessCode.trim()}
|
||||||
|
className="h-9 px-4 rounded-xl"
|
||||||
>
|
>
|
||||||
{isVerifying ? "..." : dict.common.save}
|
{isVerifying ? "..." : dict.common.save}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[0.8rem] text-muted-foreground">
|
|
||||||
{dict.settings.accessCodeDescription}
|
|
||||||
</p>
|
|
||||||
{error && (
|
{error && (
|
||||||
<p className="text-[0.8rem] text-destructive">
|
<p className="text-xs text-destructive">
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
{/* Language */}
|
||||||
<div className="space-y-0.5">
|
<SettingItem
|
||||||
<Label htmlFor="language-select">
|
label={dict.settings.language}
|
||||||
{dict.settings.language}
|
description={dict.settings.languageDescription}
|
||||||
</Label>
|
>
|
||||||
<p className="text-[0.8rem] text-muted-foreground">
|
<Select
|
||||||
{dict.settings.languageDescription}
|
value={currentLang}
|
||||||
</p>
|
onValueChange={changeLanguage}
|
||||||
</div>
|
>
|
||||||
<Select value={currentLang} onValueChange={changeLanguage}>
|
<SelectTrigger
|
||||||
<SelectTrigger id="language-select" className="w-32">
|
id="language-select"
|
||||||
|
className="w-[120px] h-9 rounded-xl"
|
||||||
|
>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -241,22 +283,19 @@ function SettingsContent({
|
|||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</SettingItem>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
{/* Theme */}
|
||||||
<div className="space-y-0.5">
|
<SettingItem
|
||||||
<Label htmlFor="theme-toggle">
|
label={dict.settings.theme}
|
||||||
{dict.settings.theme}
|
description={dict.settings.themeDescription}
|
||||||
</Label>
|
>
|
||||||
<p className="text-[0.8rem] text-muted-foreground">
|
|
||||||
{dict.settings.themeDescription}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
<Button
|
||||||
id="theme-toggle"
|
id="theme-toggle"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={onToggleDarkMode}
|
onClick={onToggleDarkMode}
|
||||||
|
className="h-9 w-9 rounded-xl border-border-subtle hover:bg-interactive-hover"
|
||||||
>
|
>
|
||||||
{darkMode ? (
|
{darkMode ? (
|
||||||
<Sun className="h-4 w-4" />
|
<Sun className="h-4 w-4" />
|
||||||
@@ -264,42 +303,35 @@ function SettingsContent({
|
|||||||
<Moon className="h-4 w-4" />
|
<Moon className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</SettingItem>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
{/* Draw.io Style */}
|
||||||
<div className="space-y-0.5">
|
<SettingItem
|
||||||
<Label htmlFor="drawio-ui">
|
label={dict.settings.drawioStyle}
|
||||||
{dict.settings.drawioStyle}
|
description={`${dict.settings.drawioStyleDescription} ${
|
||||||
</Label>
|
drawioUi === "min"
|
||||||
<p className="text-[0.8rem] text-muted-foreground">
|
|
||||||
{dict.settings.drawioStyleDescription}{" "}
|
|
||||||
{drawioUi === "min"
|
|
||||||
? dict.settings.minimal
|
? dict.settings.minimal
|
||||||
: dict.settings.sketch}
|
: dict.settings.sketch
|
||||||
</p>
|
}`}
|
||||||
</div>
|
>
|
||||||
<Button
|
<Button
|
||||||
id="drawio-ui"
|
id="drawio-ui"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
|
||||||
onClick={onToggleDrawioUi}
|
onClick={onToggleDrawioUi}
|
||||||
|
className="h-9 w-[120px] rounded-xl border-border-subtle hover:bg-interactive-hover font-normal"
|
||||||
>
|
>
|
||||||
{dict.settings.switchTo}{" "}
|
{dict.settings.switchTo}{" "}
|
||||||
{drawioUi === "min"
|
{drawioUi === "min"
|
||||||
? dict.settings.sketch
|
? dict.settings.sketch
|
||||||
: dict.settings.minimal}
|
: dict.settings.minimal}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</SettingItem>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
{/* Close Protection */}
|
||||||
<div className="space-y-0.5">
|
<SettingItem
|
||||||
<Label htmlFor="close-protection">
|
label={dict.settings.closeProtection}
|
||||||
{dict.settings.closeProtection}
|
description={dict.settings.closeProtectionDescription}
|
||||||
</Label>
|
>
|
||||||
<p className="text-[0.8rem] text-muted-foreground">
|
|
||||||
{dict.settings.closeProtectionDescription}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Switch
|
<Switch
|
||||||
id="close-protection"
|
id="close-protection"
|
||||||
checked={closeProtection}
|
checked={closeProtection}
|
||||||
@@ -312,10 +344,13 @@ function SettingsContent({
|
|||||||
onCloseProtectionChange?.(checked)
|
onCloseProtectionChange?.(checked)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</SettingItem>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-4 border-t border-border/50">
|
|
||||||
<p className="text-[0.75rem] text-muted-foreground text-center">
|
{/* Footer */}
|
||||||
|
<div className="px-6 py-4 border-t border-border-subtle bg-surface-1/50 rounded-b-2xl">
|
||||||
|
<p className="text-xs text-muted-foreground text-center">
|
||||||
Version {process.env.APP_VERSION}
|
Version {process.env.APP_VERSION}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -328,9 +363,9 @@ export function SettingsDialog(props: SettingsDialogProps) {
|
|||||||
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-lg p-0">
|
||||||
<div className="h-64 flex items-center justify-center">
|
<div className="h-80 flex items-center justify-center">
|
||||||
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full" />
|
<div className="animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full" />
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,10 @@ function DialogOverlay({
|
|||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
data-slot="dialog-overlay"
|
data-slot="dialog-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
"fixed inset-0 z-50 bg-black/40 backdrop-blur-[2px]",
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||||
|
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
"duration-200",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -57,13 +60,32 @@ function DialogContent({
|
|||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
// Base styles
|
||||||
|
"fixed top-[50%] left-[50%] z-50 w-full",
|
||||||
|
"max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%]",
|
||||||
|
"grid gap-4 p-6",
|
||||||
|
// Refined visual treatment
|
||||||
|
"bg-surface-0 rounded-2xl border border-border-subtle shadow-dialog",
|
||||||
|
// Entry/exit animations
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||||
|
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
"data-[state=closed]:zoom-out-[0.98] data-[state=open]:zoom-in-[0.98]",
|
||||||
|
"data-[state=closed]:slide-out-to-top-[2%] data-[state=open]:slide-in-from-top-[2%]",
|
||||||
|
"duration-200 sm:max-w-lg",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
|
<DialogPrimitive.Close className={cn(
|
||||||
|
"absolute top-4 right-4 rounded-xl p-1.5",
|
||||||
|
"text-muted-foreground/60 hover:text-foreground",
|
||||||
|
"hover:bg-interactive-hover",
|
||||||
|
"transition-all duration-150",
|
||||||
|
"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
|
"disabled:pointer-events-none",
|
||||||
|
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:size-4"
|
||||||
|
)}>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
@@ -102,7 +124,10 @@ function DialogTitle({
|
|||||||
return (
|
return (
|
||||||
<DialogPrimitive.Title
|
<DialogPrimitive.Title
|
||||||
data-slot="dialog-title"
|
data-slot="dialog-title"
|
||||||
className={cn("text-lg leading-none font-semibold", className)}
|
className={cn(
|
||||||
|
"text-xl font-semibold tracking-tight leading-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -115,7 +140,10 @@ function DialogDescription({
|
|||||||
return (
|
return (
|
||||||
<DialogPrimitive.Description
|
<DialogPrimitive.Description
|
||||||
data-slot="dialog-description"
|
data-slot="dialog-description"
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn(
|
||||||
|
"text-sm text-muted-foreground leading-relaxed",
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,9 +8,30 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|||||||
type={type}
|
type={type}
|
||||||
data-slot="input"
|
data-slot="input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
// Base styles
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
"flex h-10 w-full min-w-0 rounded-xl px-3.5 py-2",
|
||||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
"border border-border-subtle bg-surface-1",
|
||||||
|
"text-sm text-foreground",
|
||||||
|
// Placeholder
|
||||||
|
"placeholder:text-muted-foreground/60",
|
||||||
|
// Selection
|
||||||
|
"selection:bg-primary selection:text-primary-foreground",
|
||||||
|
// Transitions
|
||||||
|
"transition-all duration-150 ease-out",
|
||||||
|
// Hover state
|
||||||
|
"hover:border-border-default",
|
||||||
|
// Focus state - refined ring
|
||||||
|
"focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/10",
|
||||||
|
// File input
|
||||||
|
"file:text-foreground file:inline-flex file:h-7 file:border-0",
|
||||||
|
"file:bg-transparent file:text-sm file:font-medium",
|
||||||
|
// Disabled
|
||||||
|
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
// Invalid state
|
||||||
|
"aria-invalid:border-destructive aria-invalid:ring-destructive/20",
|
||||||
|
"dark:aria-invalid:ring-destructive/40",
|
||||||
|
// Dark mode background
|
||||||
|
"dark:bg-surface-1",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
Reference in New Issue
Block a user