Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,15 @@
"cmdk": "^1.1.1",
"dagre": "^0.8.5",
"dotenv": "^17.2.3",
"framer-motion": "^12.23.26",
"geist": "^1.5.1",
"lucide-react": "^0.562.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-markdown": "^10.1.0",
"rehype-raw": "^7.0.0",
"react-resizable-panels": "^3.0.6",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"zustand": "^5.0.9"
Expand Down
6 changes: 4 additions & 2 deletions apps/ui/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { useSettingsMigration } from './hooks/use-settings-migration';
import './styles/global.css';
import './styles/theme-imports';

import { Shell } from './components/layout/shell';

export default function App() {
const [showSplash, setShowSplash] = useState(() => {
// Only show splash once per session
Expand All @@ -27,9 +29,9 @@ export default function App() {
}, []);

return (
<>
<Shell>
<RouterProvider router={router} />
{showSplash && <SplashScreen onComplete={handleSplashComplete} />}
</>
</Shell>
);
}
118 changes: 118 additions & 0 deletions apps/ui/src/components/layout/floating-dock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { useRef } from 'react';
import { motion, useMotionValue, useSpring, useTransform } from 'framer-motion';
import { useNavigate, useLocation } from '@tanstack/react-router';
import {
LayoutDashboard,
Bot,
FileText,
Database,
Terminal,
Settings,
Users,
type LucideIcon,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { useAppStore } from '@/store/app-store';

export function FloatingDock() {
const mouseX = useMotionValue(Infinity);
const navigate = useNavigate();
const location = useLocation();
const { currentProject } = useAppStore();

const navItems = [
{ id: 'board', icon: LayoutDashboard, label: 'Board', path: '/board' },
{ id: 'agent', icon: Bot, label: 'Agent', path: '/agent' },
{ id: 'spec', icon: FileText, label: 'Spec', path: '/spec' },
{ id: 'context', icon: Database, label: 'Context', path: '/context' },
{ id: 'profiles', icon: Users, label: 'Profiles', path: '/profiles' },
{ id: 'terminal', icon: Terminal, label: 'Terminal', path: '/terminal' },
{ id: 'settings', icon: Settings, label: 'Settings', path: '/settings' },
];

if (!currentProject) return null;

return (
<div className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50">
<motion.div
onMouseMove={(e) => mouseX.set(e.pageX)}
onMouseLeave={() => mouseX.set(Infinity)}
className={cn(
'flex h-16 items-end gap-4 rounded-2xl px-4 pb-3',
'bg-white/5 backdrop-blur-2xl border border-white/10 shadow-2xl'
)}
>
{navItems.map((item) => (
<DockIcon
key={item.id}
mouseX={mouseX}
icon={item.icon}
path={item.path}
label={item.label}
isActive={location.pathname.startsWith(item.path)}
onClick={() => navigate({ to: item.path })}
/>
))}
</motion.div>
</div>
);
}

function DockIcon({
mouseX,
icon: Icon,
path,
label,
isActive,
onClick,
}: {
mouseX: any;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better type safety, the mouseX prop is typed as any. You should use the specific type from framer-motion, which is MotionValue<number>. You'll need to import MotionValue from framer-motion at the top of the file: import { ..., type MotionValue } from 'framer-motion';

Suggested change
mouseX: any;
mouseX: MotionValue<number>;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix the any type for mouseX parameter.

The mouseX parameter is typed as any, which bypasses TypeScript's type checking and could lead to runtime errors. It should be typed as MotionValue<number>.

🔎 Type-safe fix
+import { motion, useMotionValue, useSpring, useTransform, MotionValue } from 'framer-motion';

 function DockIcon({
   mouseX,
   icon: Icon,
   path,
   label,
   isActive,
   onClick,
 }: {
-  mouseX: any;
+  mouseX: MotionValue<number>;
   icon: LucideIcon;
   path: string;
   label: string;
   isActive: boolean;
   onClick: () => void;
 }) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
mouseX: any;
import { motion, useMotionValue, useSpring, useTransform, MotionValue } from 'framer-motion';
function DockIcon({
mouseX,
icon: Icon,
path,
label,
isActive,
onClick,
}: {
mouseX: MotionValue<number>;
icon: LucideIcon;
path: string;
label: string;
isActive: boolean;
onClick: () => void;
}) {
🤖 Prompt for AI Agents
In apps/ui/src/components/layout/floating-dock.tsx around line 69, replace the
loose any type for mouseX with a strongly typed MotionValue<number>; update the
declaration from "mouseX: any" to "mouseX: MotionValue<number>" and add/import
the type from 'framer-motion' (e.g., import type { MotionValue } from
'framer-motion') if not already present, then ensure any usages align with
MotionValue methods (.get(), onChange, etc.).

icon: LucideIcon;
path: string;
label: string;
isActive: boolean;
onClick: () => void;
}) {
const ref = useRef<HTMLDivElement>(null);

const distance = useTransform(mouseX, (val: number) => {
const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 };
return val - bounds.x - bounds.width / 2;
});

const widthSync = useTransform(distance, [-150, 0, 150], [40, 80, 40]);
const width = useSpring(widthSync, { mass: 0.1, stiffness: 150, damping: 12 });
Comment on lines +83 to +84
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add reduced motion support for dock animations.

The spring-based width animation does not respect prefers-reduced-motion, which can cause discomfort for users with motion sensitivity. This affects the interactive scaling behavior when hovering over dock icons.

🔎 Recommended fix
+import { motion, useMotionValue, useSpring, useTransform, useReducedMotion } from 'framer-motion';

 function DockIcon({
   mouseX,
   icon: Icon,
   path,
   label,
   isActive,
   onClick,
 }: {
   mouseX: MotionValue<number>;
   icon: LucideIcon;
   path: string;
   label: string;
   isActive: boolean;
   onClick: () => void;
 }) {
   const ref = useRef<HTMLDivElement>(null);
+  const shouldReduceMotion = useReducedMotion();

   const distance = useTransform(mouseX, (val: number) => {
     const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 };
     return val - bounds.x - bounds.width / 2;
   });

-  const widthSync = useTransform(distance, [-150, 0, 150], [40, 80, 40]);
-  const width = useSpring(widthSync, { mass: 0.1, stiffness: 150, damping: 12 });
+  const widthSync = useTransform(
+    distance,
+    [-150, 0, 150],
+    shouldReduceMotion ? [60, 60, 60] : [40, 80, 40]
+  );
+  const width = useSpring(widthSync, {
+    mass: 0.1,
+    stiffness: shouldReduceMotion ? 300 : 150,
+    damping: shouldReduceMotion ? 30 : 12
+  });

Also applies to: 89-89

🤖 Prompt for AI Agents
In apps/ui/src/components/layout/floating-dock.tsx around lines 83-84 (and also
line 89), the spring-based width animation ignores user prefers-reduced-motion;
detect reduced motion (e.g. using framer-motion's useReducedMotion or
window.matchMedia) and, when reduced motion is requested, avoid the spring by
using the raw transformed value (or a static width) instead of useSpring;
otherwise keep the existing useSpring behavior. Ensure you import and call the
reduced-motion check at the top of the component and branch so that width =
useSpring(widthSync, ...) is only used when reducedMotion is false, falling back
to width = widthSync (or a non-animated motion value) when reducedMotion is
true.


return (
<motion.div
ref={ref}
style={{ width }}
className="aspect-square cursor-pointer group relative"
onClick={onClick}
>
{/* Tooltip */}
<div className="absolute -top-10 left-1/2 -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity text-xs font-mono bg-black/80 text-white px-2 py-1 rounded backdrop-blur-md border border-white/10 pointer-events-none whitespace-nowrap">
{label}
</div>

<div
className={cn(
'flex h-full w-full items-center justify-center rounded-full transition-colors',
isActive
? 'bg-primary text-primary-foreground shadow-[0_0_20px_rgba(34,211,238,0.3)]'
: 'bg-white/5 text-muted-foreground hover:bg-white/10'
)}
>
<Icon className="h-[40%] w-[40%]" />
</div>

{/* Active Dot */}
{isActive && (
<motion.div
layoutId="activeDockDot"
className="absolute -bottom-2 left-1/2 w-1 h-1 bg-primary rounded-full -translate-x-1/2"
/>
)}
</motion.div>
);
}
70 changes: 70 additions & 0 deletions apps/ui/src/components/layout/hud.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { ChevronDown, Command, Folder } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useAppStore } from '@/store/app-store';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';

interface HudProps {
onOpenProjectPicker: () => void;
onOpenFolder: () => void;
}

export function Hud({ onOpenProjectPicker, onOpenFolder }: HudProps) {
const { currentProject, projects, setCurrentProject } = useAppStore();

if (!currentProject) return null;

return (
<div className="fixed top-4 left-4 z-50 flex items-center gap-3">
{/* Project Pill */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div
className={cn(
'group flex items-center gap-3 px-4 py-2 rounded-full cursor-pointer',
'bg-white/5 backdrop-blur-md border border-white/10',
'hover:bg-white/10 transition-colors'
)}
>
<div className="w-2 h-2 rounded-full bg-emerald-500 shadow-[0_0_10px_rgba(16,185,129,0.4)] animate-pulse" />
<span className="font-mono text-sm font-medium tracking-tight">
{currentProject.name}
</span>
<ChevronDown className="w-3 h-3 text-muted-foreground group-hover:text-foreground transition-colors" />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 glass border-white/10" align="start">
<DropdownMenuLabel>Switch Project</DropdownMenuLabel>
<DropdownMenuSeparator />
{projects.slice(0, 5).map((p) => (
<DropdownMenuItem
key={p.id}
onClick={() => setCurrentProject(p)}
className="font-mono text-xs"
>
{p.name}
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onOpenProjectPicker}>
<Command className="mr-2 w-3 h-3" />
All Projects...
</DropdownMenuItem>
<DropdownMenuItem onClick={onOpenFolder}>
<Folder className="mr-2 w-3 h-3" />
Open Local Folder...
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

{/* Dynamic Status / Breadcrumbs could go here */}
</div>
);
}
17 changes: 17 additions & 0 deletions apps/ui/src/components/layout/noise-overlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export function NoiseOverlay() {
return (
<div className="fixed inset-0 z-50 pointer-events-none opacity-[0.015] mix-blend-overlay">
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check z-index usage in the codebase, especially for modals, dialogs, dropdowns, toasts
rg -n "z-\[?[0-9]+" apps/ui/src --type=tsx --type=css -A 2 -B 2 | head -100

Repository: AutoMaker-Org/automaker

Length of output: 94


🏁 Script executed:

#!/bin/bash
# Check z-index usage in the codebase
rg "z-\[?[0-9]+" apps/ui/src -g "*.tsx" -g "*.ts" -g "*.css" -A 1 -B 1 | head -150

Repository: AutoMaker-Org/automaker

Length of output: 13647


Lower z-index recommended for decorative overlay.

The noise overlay uses z-50, which is the same level as modals, dialogs, dropdowns, tooltips, and other interactive components throughout the codebase. While the overlay's opacity-[0.015] and mix-blend-overlay properties mitigate visual conflicts, using z-50 for a purely decorative background element isn't ideal. Consider using z-10 or lower to maintain a clear separation from interactive UI components.

🤖 Prompt for AI Agents
In apps/ui/src/components/layout/noise-overlay.tsx around line 3, the decorative
noise overlay currently uses class "z-50", which places it at the same stacking
level as modals and other interactive components; change the z-index to a much
lower value (for example "z-10" or lower) in the className so the overlay stays
behind interactive UI elements while keeping the existing opacity and blend
settings.

<svg className="w-full h-full">
<filter id="noiseFilter">
<feTurbulence
type="fractalNoise"
baseFrequency="0.80"
numOctaves="3"
stitchTiles="stitch"
/>
</filter>
<rect width="100%" height="100%" filter="url(#noiseFilter)" />
</svg>
</div>
);
}
30 changes: 30 additions & 0 deletions apps/ui/src/components/layout/page-shell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ReactNode } from 'react';
import { cn } from '@/lib/utils';
import { motion } from 'framer-motion';

interface PageShellProps {
children: ReactNode;
className?: string;
fullWidth?: boolean;
}

export function PageShell({ children, className, fullWidth = false }: PageShellProps) {
return (
<div className="relative w-full h-full pt-16 pb-24 px-6 overflow-hidden">
<motion.div
initial={{ opacity: 0, scale: 0.98, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
transition={{ duration: 0.4, ease: [0.2, 0, 0, 1] }}
className={cn(
'w-full h-full rounded-3xl overflow-hidden',
'bg-black/20 backdrop-blur-2xl border border-white/5 shadow-2xl',
'flex flex-col',
!fullWidth && 'max-w-7xl mx-auto',
className
)}
>
{children}
</motion.div>
Comment on lines +14 to +27
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add reduced motion support for accessibility compliance.

The entrance animation does not respect the prefers-reduced-motion user preference, which can cause discomfort for users with vestibular disorders or motion sensitivity. This is a WCAG 2.1 accessibility concern.

🔎 Recommended fix using Framer Motion's useReducedMotion
 import { ReactNode } from 'react';
 import { cn } from '@/lib/utils';
-import { motion } from 'framer-motion';
+import { motion, useReducedMotion } from 'framer-motion';

 interface PageShellProps {
   children: ReactNode;
   className?: string;
   fullWidth?: boolean;
 }

 export function PageShell({ children, className, fullWidth = false }: PageShellProps) {
+  const shouldReduceMotion = useReducedMotion();
+
   return (
     <div className="relative w-full h-full pt-16 pb-24 px-6 overflow-hidden">
       <motion.div
-        initial={{ opacity: 0, scale: 0.98, y: 10 }}
-        animate={{ opacity: 1, scale: 1, y: 0 }}
-        transition={{ duration: 0.4, ease: [0.2, 0, 0, 1] }}
+        initial={shouldReduceMotion ? false : { opacity: 0, scale: 0.98, y: 10 }}
+        animate={{ opacity: 1, scale: 1, y: 0 }}
+        transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.4, ease: [0.2, 0, 0, 1] }}
         className={cn(
🤖 Prompt for AI Agents
In apps/ui/src/components/layout/page-shell.tsx around lines 14 to 27, the
motion.div animation ignores the user's prefers-reduced-motion setting; import
Framer Motion's useReducedMotion, call it, and if it returns true replace the
entrance animation with a non-animating state (e.g. set initial and animate to
the same final values or skip transform/transition or set transition duration to
0) so the element renders without movement; keep the animated variants for the
normal case and apply them conditionally based on the reduced-motion flag.

</div>
);
}
69 changes: 69 additions & 0 deletions apps/ui/src/components/layout/prism-field.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { motion } from 'framer-motion';
import { useEffect, useState } from 'react';

export function PrismField() {
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });

useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
setMousePosition({
x: e.clientX,
y: e.clientY,
});
};

window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
Comment on lines +5 to +17
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Updating React state on every mousemove event causes frequent re-renders, which can be a performance bottleneck. It's better to use useMotionValue to track mouse coordinates without triggering re-renders.

You'll need to update your import:
import { motion, useMotionValue, useTransform } from 'framer-motion';

Then, you can use these motion values in useTransform hooks and apply them to the style prop of your motion.div components. This will be much more performant. For example:
const orb1X = useTransform(mouseX, (val) => val * 0.02);
<motion.div style={{ x: orb1X, y: orb1Y }} ... />

  const mouseX = useMotionValue(0);
  const mouseY = useMotionValue(0);

  useEffect(() => {
    const handleMouseMove = (e: MouseEvent) => {
      mouseX.set(e.clientX);
      mouseY.set(e.clientY);
    };

    window.addEventListener('mousemove', handleMouseMove);
    return () => window.removeEventListener('mousemove', handleMouseMove);
  }, [mouseX, mouseY]);


return (
<div className="fixed inset-0 z-0 overflow-hidden pointer-events-none bg-[#0b101a]">
{/* Deep Space Base */}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_50%,rgba(17,24,39,1)_0%,rgba(11,16,26,1)_100%)]" />

{/* Animated Orbs */}
<motion.div
animate={{
x: mousePosition.x * 0.02,
y: mousePosition.y * 0.02,
}}
transition={{ type: 'spring', damping: 50, stiffness: 400 }}
className="absolute top-[-20%] left-[-10%] w-[70vw] h-[70vw] rounded-full bg-cyan-500/5 blur-[120px] mix-blend-screen"
/>

<motion.div
animate={{
x: mousePosition.x * -0.03,
y: mousePosition.y * -0.03,
}}
transition={{ type: 'spring', damping: 50, stiffness: 400 }}
className="absolute bottom-[-20%] right-[-10%] w-[60vw] h-[60vw] rounded-full bg-violet-600/5 blur-[120px] mix-blend-screen"
/>

<motion.div
animate={{
scale: [1, 1.1, 1],
opacity: [0.3, 0.5, 0.3],
}}
transition={{
duration: 8,
repeat: Infinity,
ease: 'easeInOut',
}}
className="absolute top-[30%] left-[50%] transform -translate-x-1/2 -translate-y-1/2 w-[40vw] h-[40vw] rounded-full bg-blue-500/5 blur-[100px] mix-blend-screen"
/>

{/* Grid Overlay */}
<div
className="absolute inset-0 z-10 opacity-[0.03]"
style={{
backgroundImage: `linear-gradient(#fff 1px, transparent 1px), linear-gradient(90deg, #fff 1px, transparent 1px)`,
backgroundSize: '50px 50px',
}}
/>

{/* Vignette */}
<div className="absolute inset-0 z-20 bg-[radial-gradient(circle_at_center,transparent_0%,rgba(11,16,26,0.8)_100%)]" />
</div>
);
}
32 changes: 32 additions & 0 deletions apps/ui/src/components/layout/shell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ReactNode } from 'react';
import { cn } from '../../lib/utils';
import { PrismField } from './prism-field';
import { NoiseOverlay } from './noise-overlay';

interface ShellProps {
children: ReactNode;
className?: string;
showBackgroundElements?: boolean;
}

export function Shell({ children, className, showBackgroundElements = true }: ShellProps) {
return (
<div
className={cn(
'relative min-h-screen w-full overflow-hidden bg-background text-foreground transition-colors duration-500',
className
)}
>
{/* Animated Background Layers */}
{showBackgroundElements && (
<>
<PrismField />
<NoiseOverlay />
</>
)}

{/* Content wrapper */}
<div className="relative z-10 flex h-screen flex-col">{children}</div>
</div>
);
}
Loading
Loading