fix(ui): resolve 9 animated design system bugs including Monaco tab height accumulation

- fix(editor): Monaco compose↔.env tab switching no longer accumulates height
  each switch — root cause was Monaco's position:static internal child creating
  a circular CSS dependency with the CSS grid; broken by calling layout({0,0})
  to force reflow before re-measuring. Added overflow-hidden to Monaco container.
- fix(ui): sliding tab indicator on compose/env tabs via motion.div layoutId
- fix(ui): sliding tab indicator on Notification tabs (Discord/Slack/Webhook)
- fix(ui): switch thumb now animates position (Radix data-attribute CSS translation)
- fix(ui): 'Always Local' tooltip no longer crashes — replaced animate-ui tooltip
  (getStrictContext throws outside provider) with pure Radix primitives + CSS animation
- feat(settings): Auto theme option added (light/dark/auto with matchMedia listener)
- fix(ui): NodeManager dialog buttons no longer stuck together (DialogFooter gap)
- fix(ui): AlertDialog spring animation + Quick Clean button text wraps correctly
- fix(ui): Resources/App Store/Logs buttons toggle off on second click
This commit is contained in:
SaelixCode
2026-03-20 23:51:54 -04:00
parent 0cb5fae947
commit 22e646286e
10 changed files with 203 additions and 127 deletions
+1
View File
@@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
- **Fixed:** Animated design system overhaul — 9 UI bugs resolved: (1) compose.yaml ↔ .env tab switching no longer accumulates height (Monaco's internal `position:static` child created a circular CSS dependency with the grid; fixed by resetting Monaco to 0×0 then forcing a synchronous reflow before re-measuring); (2) sliding tab indicator on compose/env tabs; (3) sliding tab indicator on Notification tabs (Discord/Slack/Webhook); (4) switch thumb now animates position (added Radix data-attribute CSS translation); (5) "Always Local" badge tooltip no longer crashes the page (replaced animate-ui tooltip that called `getStrictContext` with pure Radix primitives); (6) Auto theme option added to Appearance settings (light/dark/auto with `window.matchMedia` listener); (7) Cancel/Add Node buttons in NodeManager dialogs no longer stuck together (added custom `DialogFooter` with proper gap classes); (8) AlertDialog now uses spring animation (`stiffness: 380, damping: 32`) and Quick Clean button text wraps correctly; (9) Resources/App Store/Logs menu buttons now toggle off on second click. Monaco container received `overflow-hidden` to prevent height escape.
- **Added:** `motion` package and `animate-ui` animated component library as the animation foundation for the design system.
- **Changed:** Design system overhauled — new brand cyan token (`--brand`, `oklch(0.72 0.14 200)` dark / `oklch(0.50 0.14 200)` light) applied to focus rings across both themes. CSS custom properties added for motion easing curves (`--ease-spring`, `--ease-out-expo`), animation durations, and stagger delay utilities. `prefers-reduced-motion` respected globally.
- **Changed:** Geist font loaded via Google Fonts CDN (was declared but never imported — no visual change, now actually resolved).
+3 -1
View File
@@ -19,5 +19,7 @@
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
"registries": {
"@animate-ui": "https://animate-ui.com/r/{name}.json"
}
}
+61 -27
View File
@@ -1,4 +1,7 @@
import { useState, useEffect, useRef } from 'react';
import { motion } from 'motion/react';
type Theme = 'light' | 'dark' | 'auto';
import Editor from '@monaco-editor/react';
import TerminalComponent from './Terminal';
import ErrorBoundary from './ErrorBoundary';
@@ -75,6 +78,7 @@ export default function EditorLayout() {
// the stale-closure bug that occurs when reading containerStats directly.
const rawBytesRef = useRef<Record<string, { lastRx: number; lastTx: number }>>({});
const [activeTab, setActiveTab] = useState<'compose' | 'env'>('compose');
const monacoEditorRef = useRef<import('monaco-editor').editor.IStandaloneCodeEditor | null>(null);
const [createDialogOpen, setCreateDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [newStackName, setNewStackName] = useState('');
@@ -82,13 +86,15 @@ export default function EditorLayout() {
const [isLoading, setIsLoading] = useState(false);
const [loadingAction, setLoadingAction] = useState<string | null>(null);
const [isFileLoading, setIsFileLoading] = useState(false);
const [isDarkMode, setIsDarkMode] = useState(() => {
const saved = localStorage.getItem('sencho-theme');
if (saved !== null) {
return saved === 'dark';
}
return true; // Default to dark mode
const [theme, setTheme] = useState<Theme>(() => {
const saved = localStorage.getItem('sencho-theme') as Theme | null;
if (saved === 'light' || saved === 'dark' || saved === 'auto') return saved;
return 'dark'; // Default to dark mode
});
const [systemDark, setSystemDark] = useState(() =>
window.matchMedia('(prefers-color-scheme: dark)').matches
);
const isDarkMode = theme === 'dark' || (theme === 'auto' && systemDark);
const [activeView, setActiveView] = useState<'dashboard' | 'editor' | 'host-console' | 'resources' | 'templates' | 'global-observability'>('dashboard');
const [isEditing, setIsEditing] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
@@ -117,17 +123,34 @@ export default function EditorLayout() {
setAlertSheetOpen(true);
};
// Theme toggle effect
// Listen for system dark mode changes (for 'auto' theme)
useEffect(() => {
const html = document.documentElement;
if (isDarkMode) {
html.classList.add('dark');
localStorage.setItem('sencho-theme', 'dark');
} else {
html.classList.remove('dark');
localStorage.setItem('sencho-theme', 'light');
}
}, [isDarkMode]);
const mq = window.matchMedia('(prefers-color-scheme: dark)');
const handler = (e: MediaQueryListEvent) => setSystemDark(e.matches);
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, []);
// Apply dark class and persist theme preference
useEffect(() => {
document.documentElement.classList.toggle('dark', isDarkMode);
localStorage.setItem('sencho-theme', theme);
}, [isDarkMode, theme]);
// Force Monaco to re-measure its container after the tab switch DOM settles.
// Monaco's internal child is position:static with an explicit pixel height that
// creates a circular CSS dependency (Monaco drives card height → grid height → Monaco).
// Fix: reset Monaco to 0×0 first (breaks the cycle), then trigger a forced synchronous
// reflow so the container has its CSS-correct size before Monaco re-measures.
useEffect(() => {
const id = requestAnimationFrame(() => {
const editor = monacoEditorRef.current;
if (!editor) return;
editor.layout({ width: 0, height: 0 }); // collapse → breaks CSS circular dependency
editor.layout(); // forced reflow → measures correct container size
});
return () => cancelAnimationFrame(id);
}, [activeTab]);
const refreshStacks = async (background = false) => {
if (!background) setIsLoading(true);
@@ -930,10 +953,10 @@ export default function EditorLayout() {
</Button>
{/* Resources Toggle */}
<Button
variant="outline"
variant={activeView === 'resources' ? 'default' : 'outline'}
size="sm"
className="rounded-lg"
onClick={() => setActiveView('resources')}
onClick={() => setActiveView(activeView === 'resources' ? (selectedFile ? 'editor' : 'dashboard') : 'resources')}
title="System Resources"
>
<HardDrive className="w-4 h-4 mr-2" />
@@ -941,10 +964,10 @@ export default function EditorLayout() {
</Button>
{/* App Store Toggle */}
<Button
variant="outline"
variant={activeView === 'templates' ? 'default' : 'outline'}
size="sm"
className="rounded-lg"
onClick={() => setActiveView('templates')}
onClick={() => setActiveView(activeView === 'templates' ? (selectedFile ? 'editor' : 'dashboard') : 'templates')}
title="App Store"
>
<CloudDownload className="w-4 h-4 mr-2" />
@@ -952,10 +975,10 @@ export default function EditorLayout() {
</Button>
{/* Global Observability Toggle */}
<Button
variant="outline"
variant={activeView === 'global-observability' ? 'default' : 'outline'}
size="sm"
className="rounded-lg"
onClick={() => setActiveView('global-observability')}
onClick={() => setActiveView(activeView === 'global-observability' ? (selectedFile ? 'editor' : 'dashboard') : 'global-observability')}
title="Global Logs"
>
<Activity className="w-4 h-4 mr-2" />
@@ -1234,8 +1257,18 @@ export default function EditorLayout() {
<div className="flex items-center gap-4">
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as 'compose' | 'env')}>
<TabsList className="bg-muted">
<TabsTrigger value="compose" className="rounded-lg">compose.yaml</TabsTrigger>
<TabsTrigger value="env" disabled={!envExists} className="rounded-lg">.env</TabsTrigger>
<TabsTrigger value="compose" className="relative rounded-lg data-[state=active]:bg-transparent data-[state=active]:shadow-none">
{activeTab === 'compose' && (
<motion.div layoutId="editor-tab-indicator" className="absolute inset-0 rounded-md bg-background shadow-sm" transition={{ type: 'spring', stiffness: 400, damping: 30 }} />
)}
<span className="relative z-10">compose.yaml</span>
</TabsTrigger>
<TabsTrigger value="env" disabled={!envExists} className="relative rounded-lg data-[state=active]:bg-transparent data-[state=active]:shadow-none">
{activeTab === 'env' && (
<motion.div layoutId="editor-tab-indicator" className="absolute inset-0 rounded-md bg-background shadow-sm" transition={{ type: 'spring', stiffness: 400, damping: 30 }} />
)}
<span className="relative z-10">.env</span>
</TabsTrigger>
</TabsList>
</Tabs>
@@ -1286,13 +1319,14 @@ export default function EditorLayout() {
</span>
</div>
)}
<div className="flex-1 min-h-0">
<div className="flex-1 min-h-0 overflow-hidden">
{!isFileLoading && (
<Editor
height="100%"
language={activeTab === 'compose' ? 'yaml' : 'plaintext'}
theme={isDarkMode ? 'vs-dark' : 'vs'}
value={activeTab === 'compose' ? safeContent : safeEnvContent}
onMount={(editor) => { monacoEditorRef.current = editor; }}
onChange={(value) => {
if (!isEditing) return; // Prevent changes in view mode
if (activeTab === 'compose') {
@@ -1369,8 +1403,8 @@ export default function EditorLayout() {
<SettingsModal
isOpen={settingsModalOpen}
onClose={() => setSettingsModalOpen(false)}
isDarkMode={isDarkMode}
setIsDarkMode={setIsDarkMode}
theme={theme}
setTheme={setTheme}
/>
{/* Stack Alert Sheet */}
+12 -12
View File
@@ -244,21 +244,21 @@ export default function ResourcesView() {
</CardHeader>
<CardContent className="flex-1 flex flex-col justify-center">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<Button variant="outline" className="h-24 flex flex-col gap-2 hover:bg-muted/50 transition-colors" onClick={() => setConfirmPruneType('images')}>
<PackageMinus className="w-8 h-8 text-blue-500" />
<span className="text-xs font-semibold">Prune Unused Images</span>
<Button variant="outline" className="h-24 flex flex-col gap-2 hover:bg-muted/50 transition-colors whitespace-normal" onClick={() => setConfirmPruneType('images')}>
<PackageMinus className="w-8 h-8 text-blue-500 shrink-0" />
<span className="text-xs font-semibold text-center leading-tight">Prune Unused Images</span>
</Button>
<Button variant="outline" className="h-24 flex flex-col gap-2 hover:bg-muted/50 transition-colors" onClick={() => setConfirmPruneType('volumes')}>
<HardDrive className="w-8 h-8 text-purple-500" />
<span className="text-xs font-semibold">Prune Unused Volumes</span>
<Button variant="outline" className="h-24 flex flex-col gap-2 hover:bg-muted/50 transition-colors whitespace-normal" onClick={() => setConfirmPruneType('volumes')}>
<HardDrive className="w-8 h-8 text-purple-500 shrink-0" />
<span className="text-xs font-semibold text-center leading-tight">Prune Unused Volumes</span>
</Button>
<Button variant="outline" className="h-24 flex flex-col gap-2 hover:bg-muted/50 transition-colors" onClick={() => setConfirmPruneType('networks')}>
<Network className="w-8 h-8 text-green-500" />
<span className="text-xs font-semibold">Prune Dead Networks</span>
<Button variant="outline" className="h-24 flex flex-col gap-2 hover:bg-muted/50 transition-colors whitespace-normal" onClick={() => setConfirmPruneType('networks')}>
<Network className="w-8 h-8 text-green-500 shrink-0" />
<span className="text-xs font-semibold text-center leading-tight">Prune Dead Networks</span>
</Button>
<Button variant="outline" className="h-24 flex flex-col gap-2 hover:bg-muted/50 transition-colors" onClick={() => setConfirmPruneType('containers')}>
<MonitorX className="w-8 h-8 text-orange-500" />
<span className="text-xs font-semibold">Purge Ghost Containers</span>
<Button variant="outline" className="h-24 flex flex-col gap-2 hover:bg-muted/50 transition-colors whitespace-normal" onClick={() => setConfirmPruneType('containers')}>
<MonitorX className="w-8 h-8 text-orange-500 shrink-0" />
<span className="text-xs font-semibold text-center leading-tight">Purge Ghost Containers</span>
</Button>
</div>
</CardContent>
+43 -14
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef } from 'react';
import { motion } from 'motion/react';
import {
Dialog,
DialogContent,
@@ -14,7 +15,7 @@ import { Badge } from '@/components/ui/badge';
import { toast } from 'sonner';
import { apiFetch } from '@/lib/api';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Shield, Activity, Bell, Palette, Moon, Sun, Code, Server, Package, RefreshCw, Database, Info } from 'lucide-react';
import { Shield, Activity, Bell, Palette, Moon, Sun, Monitor, Code, Server, Package, RefreshCw, Database, Info } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { NodeManager } from './NodeManager';
import { useNodes } from '@/context/NodeContext';
@@ -41,11 +42,13 @@ interface PatchableSettings {
type SectionId = 'account' | 'system' | 'notifications' | 'appearance' | 'developer' | 'nodes' | 'appstore';
type Theme = 'light' | 'dark' | 'auto';
interface SettingsModalProps {
isOpen: boolean;
onClose: () => void;
isDarkMode: boolean;
setIsDarkMode: (mode: boolean) => void;
theme: Theme;
setTheme: (theme: Theme) => void;
}
const DEFAULT_SETTINGS: PatchableSettings = {
@@ -61,7 +64,7 @@ const DEFAULT_SETTINGS: PatchableSettings = {
log_retention_days: '30',
};
export function SettingsModal({ isOpen, onClose, isDarkMode, setIsDarkMode }: SettingsModalProps) {
export function SettingsModal({ isOpen, onClose, theme, setTheme }: SettingsModalProps) {
const { activeNode } = useNodes();
const isRemote = activeNode?.type === 'remote';
const [activeSection, setActiveSection] = useState<SectionId>('account');
@@ -73,6 +76,9 @@ export function SettingsModal({ isOpen, onClose, isDarkMode, setIsDarkMode }: Se
}
}, [isRemote]); // eslint-disable-line react-hooks/exhaustive-deps
// Notification tab state (controlled for sliding indicator)
const [notifTab, setNotifTab] = useState<'discord' | 'slack' | 'webhook'>('discord');
// Auth State
const [authData, setAuthData] = useState({ oldPassword: '', newPassword: '', confirmPassword: '' });
@@ -571,11 +577,26 @@ export function SettingsModal({ isOpen, onClose, isDarkMode, setIsDarkMode }: Se
<h3 className="text-lg font-semibold tracking-tight">Notifications & Alerts</h3>
<p className="text-sm text-muted-foreground">Configure external integrations for crash alerts.</p>
</div>
<Tabs defaultValue="discord" className="w-full">
<TabsList className="grid w-full grid-cols-3 mb-4">
<TabsTrigger value="discord">Discord</TabsTrigger>
<TabsTrigger value="slack">Slack</TabsTrigger>
<TabsTrigger value="webhook">Webhook</TabsTrigger>
<Tabs value={notifTab} onValueChange={(v) => setNotifTab(v as 'discord' | 'slack' | 'webhook')} className="w-full">
<TabsList className="w-full mb-4 grid grid-cols-3">
<TabsTrigger value="discord" className="relative data-[state=active]:bg-transparent data-[state=active]:shadow-none">
{notifTab === 'discord' && (
<motion.div layoutId="notif-tab-indicator" className="absolute inset-0 rounded-md bg-background shadow-sm" transition={{ type: 'spring', stiffness: 350, damping: 30 }} />
)}
<span className="relative z-10">Discord</span>
</TabsTrigger>
<TabsTrigger value="slack" className="relative data-[state=active]:bg-transparent data-[state=active]:shadow-none">
{notifTab === 'slack' && (
<motion.div layoutId="notif-tab-indicator" className="absolute inset-0 rounded-md bg-background shadow-sm" transition={{ type: 'spring', stiffness: 350, damping: 30 }} />
)}
<span className="relative z-10">Slack</span>
</TabsTrigger>
<TabsTrigger value="webhook" className="relative data-[state=active]:bg-transparent data-[state=active]:shadow-none">
{notifTab === 'webhook' && (
<motion.div layoutId="notif-tab-indicator" className="absolute inset-0 rounded-md bg-background shadow-sm" transition={{ type: 'spring', stiffness: 350, damping: 30 }} />
)}
<span className="relative z-10">Webhook</span>
</TabsTrigger>
</TabsList>
<TabsContent value="discord">{renderAgentTab('discord', 'Discord')}</TabsContent>
<TabsContent value="slack">{renderAgentTab('slack', 'Slack')}</TabsContent>
@@ -590,23 +611,31 @@ export function SettingsModal({ isOpen, onClose, isDarkMode, setIsDarkMode }: Se
<h3 className="text-lg font-semibold tracking-tight">Appearance</h3>
<p className="text-sm text-muted-foreground">Customize Sencho's visual theme.</p>
</div>
<div className="flex items-center space-x-4 mt-6">
<div className="flex items-center gap-3 mt-6 flex-wrap">
<Button
variant={!isDarkMode ? 'default' : 'outline'}
variant={theme === 'light' ? 'default' : 'outline'}
className="w-32 h-20 flex flex-col gap-2 rounded-xl"
onClick={() => setIsDarkMode(false)}
onClick={() => setTheme('light')}
>
<Sun className="w-6 h-6" />
Light
</Button>
<Button
variant={isDarkMode ? 'default' : 'outline'}
variant={theme === 'dark' ? 'default' : 'outline'}
className="w-32 h-20 flex flex-col gap-2 rounded-xl"
onClick={() => setIsDarkMode(true)}
onClick={() => setTheme('dark')}
>
<Moon className="w-6 h-6" />
Dark
</Button>
<Button
variant={theme === 'auto' ? 'default' : 'outline'}
className="w-32 h-20 flex flex-col gap-2 rounded-xl"
onClick={() => setTheme('auto')}
>
<Monitor className="w-6 h-6" />
Auto
</Button>
</div>
</div>
)}
+42 -49
View File
@@ -1,14 +1,13 @@
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import * as React from 'react';
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
import { motion } from 'motion/react';
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { cn } from '@/lib/utils';
import { buttonVariants } from '@/components/ui/button';
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialog = AlertDialogPrimitive.Root;
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
const AlertDialogPortal = AlertDialogPrimitive.Portal;
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
@@ -16,60 +15,59 @@ const AlertDialogOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 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/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
>(({ className, children, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
<AlertDialogPrimitive.Content ref={ref} asChild {...props}>
<motion.div
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg sm:rounded-lg',
className
)}
{...props}
/>
initial={{ opacity: 0, scale: 0.92, y: -12 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
transition={{ type: 'spring', stiffness: 380, damping: 32 }}
>
{children}
</motion.div>
</AlertDialogPrimitive.Content>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
className={cn('flex flex-col space-y-2 text-center sm:text-left', className)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
);
AlertDialogHeader.displayName = 'AlertDialogHeader';
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
);
AlertDialogFooter.displayName = 'AlertDialogFooter';
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
@@ -77,11 +75,11 @@ const AlertDialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
className={cn('text-lg font-semibold', className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
@@ -89,12 +87,11 @@ const AlertDialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
));
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
@@ -105,8 +102,8 @@ const AlertDialogAction = React.forwardRef<
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
@@ -114,15 +111,11 @@ const AlertDialogCancel = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
className={cn(buttonVariants({ variant: 'outline' }), className)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export {
AlertDialog,
@@ -136,4 +129,4 @@ export {
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}
};
+12 -1
View File
@@ -12,7 +12,6 @@ import {
DialogTrigger,
DialogContent as AnimateDialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
} from '@/components/animate-ui/primitives/radix/dialog';
@@ -51,6 +50,18 @@ const DialogOverlay = React.forwardRef<
));
DialogOverlay.displayName = 'DialogOverlay';
// Override DialogFooter to add proper spacing (animate-ui's version has no classes)
const DialogFooter = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
className={cn(
'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
className
)}
{...props}
/>
);
DialogFooter.displayName = 'DialogFooter';
export {
Dialog,
DialogPortal,
+1 -1
View File
@@ -22,7 +22,7 @@ const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
{...props}
>
<SwitchThumb
className="pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0"
className="pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
pressedAnimation={{ scale: 1.15 }}
/>
</AnimateSwitch>
+9 -5
View File
@@ -1,11 +1,13 @@
import * as React from 'react';
import { Tabs as RadixTabs } from 'radix-ui';
import { cn } from '@/lib/utils';
import {
Tabs,
TabsHighlight,
TabsHighlightItem,
TabsList as AnimateTabsList,
TabsTrigger as AnimateTabsTrigger,
TabsContent as AnimateTabsContent,
} from '@/components/animate-ui/primitives/radix/tabs';
const TabsList = React.forwardRef<
@@ -38,20 +40,22 @@ const TabsTrigger = React.forwardRef<
));
TabsTrigger.displayName = 'TabsTrigger';
// Custom TabsContent: uses Radix directly (no Framer Motion `layout` prop)
// to prevent unintended height expansion when switching tabs.
const TabsContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof AnimateTabsContent>
React.ComponentProps<typeof RadixTabs.Content>
>(({ className, ...props }, ref) => (
<AnimateTabsContent
<RadixTabs.Content
ref={ref}
className={cn(
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
'data-[state=active]:animate-in data-[state=active]:fade-in-0 data-[state=active]:duration-200',
className
)}
transition={{ duration: 0.2, ease: 'easeOut' }}
{...props}
/>
));
TabsContent.displayName = 'TabsContent';
export { Tabs, TabsList, TabsTrigger, TabsContent };
export { Tabs, TabsList, TabsTrigger, TabsContent, TabsHighlight, TabsHighlightItem };
+16 -14
View File
@@ -1,33 +1,35 @@
'use client';
import * as React from 'react';
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import { cn } from '@/lib/utils';
import {
TooltipProvider,
Tooltip,
TooltipTrigger,
TooltipPortal,
TooltipContent as AnimateTooltipContent,
} from '@/components/animate-ui/primitives/radix/tooltip';
// Use Radix primitives directly to avoid animate-ui context dependencies
// that can cause crashes in certain component tree configurations.
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
// Bundles the portal so consumers keep the same <TooltipContent> API
const TooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof AnimateTooltipContent>
React.ComponentProps<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPortal>
<AnimateTooltipContent
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground origin-[--radix-tooltip-content-transform-origin]',
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground',
'origin-[--radix-tooltip-content-transform-origin]',
'animate-in fade-in-0 zoom-in-95 duration-150',
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2',
'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
transition={{ type: 'spring', stiffness: 350, damping: 28 }}
{...props}
/>
</TooltipPortal>
</TooltipPrimitive.Portal>
));
TooltipContent.displayName = 'TooltipContent';