diff --git a/app/globals.css b/app/globals.css index feaea6d..b1f23dd 100644 --- a/app/globals.css +++ b/app/globals.css @@ -144,6 +144,68 @@ --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 { * { @apply border-border outline-ring/50; @@ -257,3 +319,83 @@ -webkit-text-fill-color: transparent; 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; + } +} diff --git a/components/model-config-dialog.tsx b/components/model-config-dialog.tsx index 89f17a2..ccf3902 100644 --- a/components/model-config-dialog.tsx +++ b/components/model-config-dialog.tsx @@ -103,41 +103,40 @@ function ProviderLogo({ ) } -// Reusable validation button component -function ValidationButton({ - status, - onClick, - disabled, - dict, +// Configuration section with title and optional action +function ConfigSection({ + title, + icon: Icon, + action, + children, }: { - status: ValidationStatus - onClick: () => void - disabled: boolean - dict: ReturnType + title: string + icon: React.ComponentType<{ className?: string }> + action?: React.ReactNode + children: React.ReactNode }) { return ( - +
+
+
+ + + {title} + +
+ {action} +
+ {children} +
+ ) +} + +// Card wrapper with subtle depth +function ConfigCard({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
) } @@ -154,7 +153,6 @@ export function ModelConfigDialog({ const [validationStatus, setValidationStatus] = useState("idle") const [validationError, setValidationError] = useState("") - const [scrollState, setScrollState] = useState({ top: false, bottom: true }) const [customModelInput, setCustomModelInput] = useState("") const scrollRef = useRef(null) const validationResetTimeoutRef = useRef 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 useEffect(() => { return () => { @@ -390,34 +368,35 @@ export function ModelConfigDialog({ return ( - - - -
+ + {/* Header */} + + +
{dict.modelConfig?.title || "AI Model Configuration"}
- + {dict.modelConfig?.description || "Configure multiple AI providers and models for your workspace"}
-
+
{/* Provider List (Left Sidebar) */} -
-
+
+
{dict.modelConfig.providers}
- -
+ +
{config.providers.length === 0 ? (
-
+

@@ -425,67 +404,78 @@ export function ModelConfigDialog({

) : ( -
- {config.providers.map((provider) => ( - - ))} -
+ + {provider.validated ? ( +
+ +
+ ) : ( + + )} + + )) )}
{/* Add Provider */} -
+
- {/* Show duplicate error for custom model input */} {duplicateError && (

{duplicateError} @@ -1132,7 +1090,7 @@ export function ModelConfigDialog({

-
- + } + > {/* Model List */} -
+
{selectedProvider.models .length === 0 ? ( -
-
+
+

@@ -1220,7 +1178,7 @@ export function ModelConfigDialog({

) : ( -
+
{selectedProvider.models.map( (model, index) => (
@@ -1264,8 +1213,8 @@ export function ModelConfigDialog({ ) : model.validated === true ? ( // Valid -
- +
+
) : model.validated === false ? ( @@ -1466,7 +1415,7 @@ export function ModelConfigDialog({
)}
-
+ {/* Danger Zone */}
@@ -1476,7 +1425,7 @@ export function ModelConfigDialog({ onClick={() => 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" > { @@ -1490,10 +1439,10 @@ export function ModelConfigDialog({ ) : (
-
- +
+
-

+

{dict.modelConfig.configureProviders}

@@ -1505,7 +1454,7 @@ export function ModelConfigDialog({

{/* Footer */} -
+

{dict.modelConfig.apiKeyStored} diff --git a/components/settings-dialog.tsx b/components/settings-dialog.tsx index 1d9f710..5ef9135 100644 --- a/components/settings-dialog.tsx +++ b/components/settings-dialog.tsx @@ -24,6 +24,32 @@ import { Switch } from "@/components/ui/switch" import { useDictionary } from "@/hooks/use-dictionary" import { getApiEndpoint } from "@/lib/base-path" 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 ( +

+
+ + {description && ( +

+ {description} +

+ )} +
+
{children}
+
+ ) +} const LANGUAGE_LABELS: Record = { en: "English", @@ -177,145 +203,154 @@ function SettingsContent({ } return ( - - + + {/* Header */} + {dict.settings.title} - + {dict.settings.description} -
- {accessCodeRequired && ( -
- -
- setAccessCode(e.target.value)} - onKeyDown={handleKeyDown} - placeholder={ - dict.settings.accessCodePlaceholder - } - autoComplete="off" - /> - + + {/* Content */} +
+
+ {/* Access Code (conditional) */} + {accessCodeRequired && ( +
+
+ +

+ {dict.settings.accessCodeDescription} +

+
+
+ + setAccessCode(e.target.value) + } + onKeyDown={handleKeyDown} + placeholder={ + dict.settings.accessCodePlaceholder + } + autoComplete="off" + className="h-9" + /> + +
+ {error && ( +

+ {error} +

+ )}
-

- {dict.settings.accessCodeDescription} -

- {error && ( -

- {error} -

- )} -
- )} + )} -
-
- -

- {dict.settings.languageDescription} -

-
- -
- -
-
- -

- {dict.settings.themeDescription} -

-
- -
+ + -
-
- -

- {dict.settings.drawioStyleDescription}{" "} - {drawioUi === "min" + {/* Theme */} + + + + + {/* Draw.io Style */} + -

- -
+ + -
-
- -

- {dict.settings.closeProtectionDescription} -

-
- { - setCloseProtection(checked) - localStorage.setItem( - STORAGE_CLOSE_PROTECTION_KEY, - checked.toString(), - ) - onCloseProtectionChange?.(checked) - }} - /> + {/* Close Protection */} + + { + setCloseProtection(checked) + localStorage.setItem( + STORAGE_CLOSE_PROTECTION_KEY, + checked.toString(), + ) + onCloseProtectionChange?.(checked) + }} + /> +
-
-

+ + {/* Footer */} +

+

Version {process.env.APP_VERSION}

@@ -328,9 +363,9 @@ export function SettingsDialog(props: SettingsDialogProps) { -
-
+ +
+
} diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx index 7d7a9d3..77b681c 100644 --- a/components/ui/dialog.tsx +++ b/components/ui/dialog.tsx @@ -38,7 +38,10 @@ function DialogOverlay({ {children} - + Close @@ -102,7 +124,10 @@ function DialogTitle({ return ( ) @@ -115,7 +140,10 @@ function DialogDescription({ return ( ) diff --git a/components/ui/input.tsx b/components/ui/input.tsx index 03295ca..9605f29 100644 --- a/components/ui/input.tsx +++ b/components/ui/input.tsx @@ -8,9 +8,30 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) { type={type} data-slot="input" 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", - "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", - "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + // Base styles + "flex h-10 w-full min-w-0 rounded-xl px-3.5 py-2", + "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 )} {...props}