mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
ui-v2 - added pull down top filter bar from designs and dark mode (#2638)
This commit is contained in:
+14
-3
@@ -5,12 +5,23 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' data: https://cash-f.squarecdn.com"
|
||||
content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; font-src 'self' data: https://cash-f.squarecdn.com"
|
||||
/>
|
||||
<title>Goose v2</title>
|
||||
<script>
|
||||
// Immediately check dark mode preference to avoid flash
|
||||
(function() {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
if (savedTheme === 'dark' || (!savedTheme && systemPrefersDark)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<body class="bg-background-default dark:bg-zinc-800 transition-colors duration-200">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Generated
+493
-568
File diff suppressed because it is too large
Load Diff
@@ -32,10 +32,12 @@
|
||||
"prepare": "cd ../.. && husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@tanstack/react-router": "^1.120.5",
|
||||
"@tanstack/router": "^0.0.1-beta.53",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.12.1",
|
||||
"lucide-react": "^0.511.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
|
||||
+3
-18
@@ -1,23 +1,8 @@
|
||||
import React, { Suspense } from 'react';
|
||||
|
||||
import { Outlet } from '@tanstack/react-router';
|
||||
|
||||
import SuspenseLoader from './components/SuspenseLoader';
|
||||
import React from 'react';
|
||||
import { MainLayout } from './layout/MainLayout';
|
||||
|
||||
const App: React.FC = (): React.ReactElement => {
|
||||
return (
|
||||
<div className="">
|
||||
<div className="titlebar-drag-region" />
|
||||
<div className="h-10 w-full" />
|
||||
<div className="">
|
||||
<div className="">
|
||||
<Suspense fallback={<SuspenseLoader />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <MainLayout />;
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Moon, Sun } from 'lucide-react';
|
||||
|
||||
export function DarkModeToggle() {
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize from localStorage or system preference
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
const shouldBeDark = savedTheme === 'dark' || (!savedTheme && systemPrefersDark);
|
||||
setIsDark(shouldBeDark);
|
||||
|
||||
if (shouldBeDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
const newIsDark = !isDark;
|
||||
setIsDark(newIsDark);
|
||||
|
||||
if (newIsDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('theme', 'light');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={toggleDarkMode}
|
||||
className="
|
||||
fixed bottom-4 left-4 z-50
|
||||
w-10 h-10
|
||||
rounded-full
|
||||
cursor-pointer
|
||||
flex items-center justify-center
|
||||
transition-all duration-200
|
||||
bg-white/80 dark:bg-black/80
|
||||
backdrop-blur-sm
|
||||
shadow-[0_0_13.7px_rgba(0,0,0,0.04)]
|
||||
dark:shadow-[0_0_24px_rgba(255,255,255,0.08)]
|
||||
hover:bg-white dark:hover:bg-black
|
||||
text-black/80 dark:text-white/80
|
||||
hover:text-black dark:hover:text-white
|
||||
group
|
||||
"
|
||||
title={isDark ? 'Switch to Light Mode' : 'Switch to Dark Mode'}
|
||||
>
|
||||
<span className="block dark:hidden transform transition-transform group-hover:rotate-12">
|
||||
<Moon size={18} />
|
||||
</span>
|
||||
<span className="hidden dark:block transform transition-transform group-hover:rotate-12">
|
||||
<Sun size={18} />
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTimeline } from '../contexts/TimelineContext';
|
||||
|
||||
export function DateDisplay() {
|
||||
const { currentDate } = useTimeline();
|
||||
const [displayDate, setDisplayDate] = useState(currentDate);
|
||||
const [isFlipping, setIsFlipping] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsFlipping(true);
|
||||
const timer = setTimeout(() => {
|
||||
setDisplayDate(currentDate);
|
||||
setIsFlipping(false);
|
||||
}, 50); // Reduced from 100ms to 50ms for faster flip
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [currentDate]);
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
||||
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
|
||||
return {
|
||||
month: monthNames[date.getMonth()],
|
||||
day: date.getDate(),
|
||||
weekday: dayNames[date.getDay()]
|
||||
};
|
||||
};
|
||||
|
||||
const formattedDate = formatDate(displayDate);
|
||||
|
||||
return (
|
||||
<div className="fixed top-[2px] left-1/2 -translate-x-1/2 z-40">
|
||||
<div
|
||||
className={`
|
||||
flex items-center gap-2 px-4 py-2
|
||||
text-text-default dark:text-white/70
|
||||
transition-all duration-150
|
||||
${isFlipping ? 'transform -translate-y-1 opacity-0' : 'transform translate-y-0 opacity-100'}
|
||||
`}
|
||||
>
|
||||
{formattedDate.weekday} {formattedDate.month} {formattedDate.day}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useRef, useMemo, useEffect } from 'react';
|
||||
import { useTimeline } from '../contexts/TimelineContext';
|
||||
import ChartTile from './tiles/ChartTile.tsx';
|
||||
import HighlightTile from './tiles/HighlightTile.tsx';
|
||||
import PieChartTile from './tiles/PieChartTile.tsx';
|
||||
@@ -244,6 +245,7 @@ const generateTileData = (date: Date) => {
|
||||
export default function Timeline() {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const sectionRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
const { setCurrentDate } = useTimeline();
|
||||
|
||||
const sections = useMemo(() => {
|
||||
const result = [];
|
||||
@@ -267,7 +269,7 @@ export default function Timeline() {
|
||||
}, []);
|
||||
|
||||
// Function to center the timeline in a section
|
||||
const centerTimeline = (sectionElement: HTMLDivElement) => {
|
||||
const centerTimeline = (sectionElement: HTMLDivElement, animate: boolean = true) => {
|
||||
if (!sectionElement) return;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
@@ -275,10 +277,14 @@ export default function Timeline() {
|
||||
const viewportWidth = sectionElement.clientWidth;
|
||||
const scrollToX = Math.max(0, (totalWidth - viewportWidth) / 2);
|
||||
|
||||
sectionElement.scrollTo({
|
||||
left: scrollToX,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
if (animate) {
|
||||
sectionElement.scrollTo({
|
||||
left: scrollToX,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
} else {
|
||||
sectionElement.scrollLeft = scrollToX;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -287,18 +293,72 @@ export default function Timeline() {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
const section = entry.target as HTMLDivElement;
|
||||
|
||||
// When section comes into view
|
||||
if (entry.isIntersecting) {
|
||||
const section = entry.target as HTMLDivElement;
|
||||
centerTimeline(section);
|
||||
// Update current date
|
||||
const sectionIndex = sectionRefs.current.indexOf(section);
|
||||
if (sectionIndex !== -1) {
|
||||
const date = sections[sectionIndex].date;
|
||||
setCurrentDate(date);
|
||||
}
|
||||
}
|
||||
|
||||
// When section is fully visible and centered
|
||||
if (entry.intersectionRatio > 0.8) {
|
||||
centerTimeline(section, true);
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
threshold: 0.5,
|
||||
rootMargin: '0px',
|
||||
threshold: [0, 0.8, 1], // Track when section is hidden, mostly visible, and fully visible
|
||||
rootMargin: '-10% 0px', // Slightly reduced margin for more natural triggering
|
||||
}
|
||||
);
|
||||
|
||||
// Add scroll handler for even faster updates
|
||||
const handleScroll = () => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
// Find the section closest to the middle of the viewport
|
||||
const viewportMiddle = window.innerHeight / 2;
|
||||
let closestSection: HTMLDivElement | null = null;
|
||||
let closestDistance = Infinity;
|
||||
|
||||
sectionRefs.current.forEach((section) => {
|
||||
if (!section) return;
|
||||
const rect = section.getBoundingClientRect();
|
||||
const sectionMiddle = rect.top + rect.height / 2;
|
||||
const distance = Math.abs(sectionMiddle - viewportMiddle);
|
||||
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance;
|
||||
closestSection = section;
|
||||
}
|
||||
});
|
||||
|
||||
if (closestSection) {
|
||||
const sectionIndex = sectionRefs.current.indexOf(closestSection);
|
||||
if (sectionIndex !== -1) {
|
||||
const date = sections[sectionIndex].date;
|
||||
setCurrentDate(date);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add scroll event listener with throttling
|
||||
let lastScrollTime = 0;
|
||||
const throttledScrollHandler = () => {
|
||||
const now = Date.now();
|
||||
if (now - lastScrollTime >= 150) { // Throttle to ~6-7 times per second
|
||||
handleScroll();
|
||||
lastScrollTime = now;
|
||||
}
|
||||
};
|
||||
|
||||
containerRef.current?.addEventListener('scroll', throttledScrollHandler, { passive: true });
|
||||
|
||||
// Add resize handler
|
||||
const handleResize = () => {
|
||||
// Find the currently visible section
|
||||
@@ -306,12 +366,11 @@ export default function Timeline() {
|
||||
if (!section) return false;
|
||||
const rect = section.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
// Check if the section is mostly visible in the viewport
|
||||
return rect.top >= -viewportHeight / 2 && rect.bottom <= viewportHeight * 1.5;
|
||||
});
|
||||
|
||||
if (visibleSection) {
|
||||
centerTimeline(visibleSection);
|
||||
centerTimeline(visibleSection, true); // Animate on resize
|
||||
}
|
||||
};
|
||||
|
||||
@@ -322,13 +381,14 @@ export default function Timeline() {
|
||||
sectionRefs.current.forEach((section) => {
|
||||
if (section) {
|
||||
observer.observe(section);
|
||||
centerTimeline(section);
|
||||
centerTimeline(section, false); // No animation on initial load
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
containerRef.current?.removeEventListener('scroll', throttledScrollHandler);
|
||||
sectionRefs.current.forEach((section) => {
|
||||
if (section) {
|
||||
observer.unobserve(section);
|
||||
@@ -363,7 +423,7 @@ export default function Timeline() {
|
||||
<div
|
||||
key={index}
|
||||
ref={(el) => (sectionRefs.current[index] = el)}
|
||||
className="h-screen relative snap-center snap-always overflow-y-hidden overflow-x-scroll snap-x snap-mandatory scrollbar-hide"
|
||||
className="h-screen relative snap-center snap-always overflow-y-hidden overflow-x-scroll snap-x snap-mandatory scrollbar-hide animate-[fadein_300ms_ease-in-out]"
|
||||
>
|
||||
<div className="relative min-w-[calc(200vw+100px)] h-full flex items-center">
|
||||
{/* Main flex container */}
|
||||
@@ -392,19 +452,31 @@ export default function Timeline() {
|
||||
/>
|
||||
|
||||
{/* Date Display */}
|
||||
<div className="bg-white p-4 rounded z-[3] flex flex-col items-center transition-opacity">
|
||||
<div className="bg-white dark:bg-black shadow-[0_0_13.7px_rgba(0,0,0,0.04)] dark:shadow-[0_0_24px_rgba(255,255,255,0.08)] p-4 rounded-xl z-[3] flex flex-col items-center transition-all">
|
||||
<div
|
||||
className={`font-['Cash_Sans'] text-3xl font-light ${section.isToday ? 'opacity-100' : 'opacity-20'}`}
|
||||
className={`font-['Cash_Sans'] text-3xl font-light transition-colors ${
|
||||
section.isToday
|
||||
? 'text-black dark:text-white'
|
||||
: 'text-black/40 dark:text-white/40'
|
||||
}`}
|
||||
>
|
||||
{section.date.toLocaleString('default', { month: 'short' })}
|
||||
</div>
|
||||
<div
|
||||
className={`font-['Cash_Sans'] text-[64px] font-light leading-none ${section.isToday ? 'opacity-100' : 'opacity-20'}`}
|
||||
className={`font-['Cash_Sans'] text-[64px] font-light leading-none transition-colors ${
|
||||
section.isToday
|
||||
? 'text-black dark:text-white'
|
||||
: 'text-black/40 dark:text-white/40'
|
||||
}`}
|
||||
>
|
||||
{section.date.getDate()}
|
||||
</div>
|
||||
<div
|
||||
className={`font-['Cash_Sans'] text-sm font-light mt-1 ${section.isToday ? 'opacity-100' : 'opacity-20'}`}
|
||||
className={`font-['Cash_Sans'] text-sm font-light mt-1 transition-colors ${
|
||||
section.isToday
|
||||
? 'text-black dark:text-white'
|
||||
: 'text-black/40 dark:text-white/40'
|
||||
}`}
|
||||
>
|
||||
{section.date.toLocaleString('default', { weekday: 'long' })}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface ChatDockProps {
|
||||
onTileCreatorToggle: () => void;
|
||||
}
|
||||
|
||||
export const ChatDock: React.FC<ChatDockProps> = ({ onTileCreatorToggle }) => {
|
||||
return (
|
||||
<motion.div
|
||||
className="flex items-center gap-2 mb-2 px-2"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
delay: 0.2,
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 30
|
||||
}}
|
||||
>
|
||||
<motion.button
|
||||
onClick={onTileCreatorToggle}
|
||||
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-zinc-700/50 transition-colors"
|
||||
title="Toggle Tile Creator"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<rect x="3" y="3" width="7" height="7" />
|
||||
<rect x="14" y="3" width="7" height="7" />
|
||||
<rect x="14" y="14" width="7" height="7" />
|
||||
<rect x="3" y="14" width="7" height="7" />
|
||||
</svg>
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
import React, { useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "../ui/tooltip";
|
||||
|
||||
// Define the tool items
|
||||
const CHAT_TOOLS = [
|
||||
{
|
||||
icon: (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="white" stroke="none">
|
||||
<path d="M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z"/>
|
||||
</svg>
|
||||
),
|
||||
label: "Make a Tile",
|
||||
color: "bg-[#4F6BFF] hover:bg-[#4F6BFF]/90",
|
||||
rotation: -3,
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="white" stroke="none">
|
||||
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 14l-5-5 1.41-1.41L12 14.17l4.59-4.58L18 11l-6 6z"/>
|
||||
</svg>
|
||||
),
|
||||
label: "Tasks",
|
||||
color: "bg-[#E042A5] hover:bg-[#E042A5]/90",
|
||||
rotation: 2,
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="white" stroke="none">
|
||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
</svg>
|
||||
),
|
||||
label: "Add",
|
||||
color: "bg-[#05C168] hover:bg-[#05C168]/90",
|
||||
rotation: -2,
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="white" stroke="none">
|
||||
<path d="M12 2L1 21h22L12 2zm0 3.83L19.17 19H4.83L12 5.83zM11 16h2v2h-2zm0-6h2v4h-2z"/>
|
||||
</svg>
|
||||
),
|
||||
label: "Issues",
|
||||
color: "bg-[#FF9900] hover:bg-[#FF9900]/90",
|
||||
rotation: 3,
|
||||
},
|
||||
];
|
||||
|
||||
interface ChatIconsProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ChatIcons: React.FC<ChatIconsProps> = ({ className }) => {
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
|
||||
return (
|
||||
<div className={`flex mb-4 items-start ${className}`}>
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="flex -space-x-6 relative">
|
||||
{CHAT_TOOLS.map((tool, index) => {
|
||||
const getX = () => {
|
||||
if (hoveredIndex === null) return 0;
|
||||
const spread = 16;
|
||||
const centerOffset = hoveredIndex * -spread;
|
||||
return (index * spread) + centerOffset;
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={tool.label}
|
||||
className="relative"
|
||||
animate={{
|
||||
x: getX(),
|
||||
rotate: hoveredIndex !== null ? 0 : tool.rotation,
|
||||
scale: hoveredIndex === index ? 1.1 : 1,
|
||||
zIndex: hoveredIndex === index ? 10 : CHAT_TOOLS.length - index,
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
ease: "easeOut",
|
||||
scale: { duration: 0.1 }
|
||||
}}
|
||||
onHoverStart={() => setHoveredIndex(index)}
|
||||
onHoverEnd={() => setHoveredIndex(null)}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<motion.button
|
||||
aria-label={tool.label}
|
||||
className={`
|
||||
flex h-12 w-12 items-center justify-center rounded-xl
|
||||
transition-all duration-200 shadow-sm
|
||||
${tool.color}
|
||||
${hoveredIndex !== null && hoveredIndex !== index ? "opacity-50" : ""}
|
||||
`}
|
||||
>
|
||||
{tool.icon}
|
||||
</motion.button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={5}>
|
||||
{tool.label}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,133 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface ChatInputProps {
|
||||
handleSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
|
||||
isLoading?: boolean;
|
||||
onStop?: () => void;
|
||||
initialValue?: string;
|
||||
}
|
||||
|
||||
export const ChatInput: React.FC<ChatInputProps> = ({
|
||||
handleSubmit,
|
||||
isLoading = false,
|
||||
onStop,
|
||||
initialValue = '',
|
||||
}) => {
|
||||
const [input, setInput] = useState(initialValue);
|
||||
const [key, setKey] = useState(0); // Add a key to force re-render
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const adjustTextareaHeight = () => {
|
||||
const textarea = textareaRef.current;
|
||||
if (textarea) {
|
||||
textarea.style.height = 'auto';
|
||||
textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px';
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
adjustTextareaHeight();
|
||||
}, [input]);
|
||||
|
||||
// Watch for class changes on html element (theme changes)
|
||||
useEffect(() => {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.attributeName === 'class') {
|
||||
setKey(prev => prev + 1); // Force textarea to re-render
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const htmlElement = document.documentElement;
|
||||
observer.observe(htmlElement, { attributes: true });
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (!input.trim() || isLoading) return;
|
||||
|
||||
handleSubmit(e);
|
||||
setInput('');
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = 'auto';
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
const form = (e.target as HTMLTextAreaElement).form;
|
||||
if (form) form.requestSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={containerRef}
|
||||
className="w-full bg-black dark:bg-white rounded-xl shadow-lg"
|
||||
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 30
|
||||
}}
|
||||
>
|
||||
<form onSubmit={handleFormSubmit} className="relative px-4 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<textarea
|
||||
key={key} // Force re-render when theme changes
|
||||
ref={textareaRef}
|
||||
name="message"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="What can goose help with? ⌘↑/⌘↓"
|
||||
className="flex-1 resize-none bg-transparent text-white dark:text-black
|
||||
focus:outline-none focus:ring-0 rounded-lg
|
||||
min-h-[40px] max-h-[200px] transition-all duration-200
|
||||
placeholder:text-zinc-500"
|
||||
style={{ overflow: input.split('\n').length > 1 ? 'auto' : 'hidden' }}
|
||||
/>
|
||||
|
||||
<motion.button
|
||||
type="submit"
|
||||
disabled={!input.trim()}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className={`
|
||||
p-2 rounded-lg w-10 h-10 flex items-center justify-center
|
||||
transition-colors duration-200
|
||||
${input.trim()
|
||||
? 'hover:bg-zinc-800 active:bg-zinc-700 dark:hover:bg-zinc-100 dark:active:bg-zinc-200'
|
||||
: 'cursor-not-allowed'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={input.trim() ? 'currentColor' : '#666'}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={`
|
||||
transition-colors duration-200
|
||||
${input.trim() ? 'text-white dark:text-black' : 'text-zinc-600 dark:text-zinc-400'}
|
||||
`}
|
||||
>
|
||||
<path d="M22 2L11 13M22 2L15 22L11 13L2 9L22 2Z" />
|
||||
</svg>
|
||||
</motion.button>
|
||||
</div>
|
||||
</form>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ChatIcons } from './ChatIcons';
|
||||
|
||||
interface FloatingChatProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const FloatingChat: React.FC<FloatingChatProps> = ({ children }) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
|
||||
// Create a debounced version of setIsVisible
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(isHovering);
|
||||
}, isHovering ? 0 : 200);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [isHovering]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed bottom-0 left-0 right-0 z-50"
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
>
|
||||
{/* Hover trigger area with black bar indicator */}
|
||||
<div className="absolute bottom-0 left-0 right-0 h-20 bg-transparent flex justify-center">
|
||||
<div
|
||||
className={`
|
||||
w-[600px] h-[15px]
|
||||
bg-black dark:bg-white
|
||||
rounded-t-[24px]
|
||||
transition-all duration-300
|
||||
absolute bottom-0
|
||||
${isVisible ? 'opacity-0 transform translate-y-2' : 'opacity-100 transform translate-y-0'}
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Chat container with transition */}
|
||||
<div
|
||||
className={`
|
||||
transform transition-all duration-300 ease-out
|
||||
${isVisible
|
||||
? 'translate-y-0 opacity-100'
|
||||
: '-translate-y-4 opacity-0'
|
||||
}
|
||||
`}
|
||||
style={{
|
||||
paddingBottom: "env(safe-area-inset-bottom, 16px)"
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-center w-full px-4 pb-4">
|
||||
<div className="w-[600px]">
|
||||
<ChatIcons className="mb-1" />
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { FilterOption } from './types';
|
||||
|
||||
interface FloatingFiltersProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const getBarColor = (filters: FilterOption[], isDarkMode: boolean): string => {
|
||||
const activeFilter = filters.find(f => f.isActive);
|
||||
switch (activeFilter?.id) {
|
||||
case 'tasks':
|
||||
return '#05C168';
|
||||
case 'projects':
|
||||
return '#0066FF';
|
||||
case 'automations':
|
||||
return '#B18CFF';
|
||||
case 'problems':
|
||||
return '#FF2E6C';
|
||||
default:
|
||||
return isDarkMode ? '#FFFFFF' : '#000000';
|
||||
}
|
||||
};
|
||||
|
||||
export function FloatingFilters({ children }: FloatingFiltersProps) {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [activeFilters, setActiveFilters] = useState<FilterOption[]>([]);
|
||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const filterPills = React.Children.toArray(children)[0] as React.ReactElement;
|
||||
if (filterPills?.props?.filters) {
|
||||
setActiveFilters(filterPills.props.filters);
|
||||
}
|
||||
}, [children]);
|
||||
|
||||
useEffect(() => {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
setIsDarkMode(isDark);
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.attributeName === 'class') {
|
||||
setIsDarkMode(document.documentElement.classList.contains('dark'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class']
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const barColor = getBarColor(activeFilters, isDarkMode);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed left-0 right-0 z-40"
|
||||
style={{ top: 0 }}
|
||||
onMouseEnter={() => setIsVisible(true)}
|
||||
onMouseLeave={() => setIsVisible(false)}
|
||||
>
|
||||
{/* Spacer for the titlebar area */}
|
||||
<div className="h-[56px]" />
|
||||
|
||||
{/* Indicator bar */}
|
||||
<div className="absolute left-0 right-0 h-16 bg-transparent flex justify-center">
|
||||
<div
|
||||
className={`
|
||||
w-[200px] h-[6px]
|
||||
rounded-b-[24px]
|
||||
transition-all duration-300
|
||||
absolute top-0
|
||||
${isVisible ? 'opacity-0 transform -translate-y-1' : 'opacity-100 transform translate-y-0'}
|
||||
`}
|
||||
style={{ backgroundColor: barColor }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Filters container */}
|
||||
<div
|
||||
className={`
|
||||
transform transition-all duration-300 ease-out w-full
|
||||
${isVisible
|
||||
? 'translate-y-0 opacity-100 scale-y-100 origin-top'
|
||||
: 'translate-y-[calc(-100%+6px)] opacity-0 scale-y-95 origin-top'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface FilterOption {
|
||||
id: string;
|
||||
label: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import { BarChart, Bar, LineChart, Line, CartesianGrid, XAxis, ResponsiveContainer } from 'recharts';
|
||||
import { BarChart, Bar, LineChart, Line, CartesianGrid, XAxis } from 'recharts';
|
||||
|
||||
interface ChartTileProps {
|
||||
title: string;
|
||||
@@ -59,15 +59,15 @@ export default function ChartTile({
|
||||
>
|
||||
{/* Header section with icon */}
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="w-6 h-6 text-text-default">
|
||||
<div className="w-6 h-6 text-text-default dark:text-white">
|
||||
{icon}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-text-muted text-sm mb-1">{title}</div>
|
||||
<div className="text-text-default text-2xl font-semibold">
|
||||
<div className="text-text-muted dark:text-white/60 text-sm mb-1">{title}</div>
|
||||
<div className="text-text-default dark:text-white text-2xl font-semibold">
|
||||
{value}
|
||||
{trend && <span className="ml-1 text-sm text-text-muted">{trend}</span>}
|
||||
{trend && <span className="ml-1 text-sm text-text-muted dark:text-white/60">{trend}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,63 +78,71 @@ export default function ChartTile({
|
||||
config={chartConfig}
|
||||
className="[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-tooltip-wrapper]:!pointer-events-none"
|
||||
>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
{variant === 'line' ? (
|
||||
<LineChart data={chartData} margin={{ top: 10, right: 10, bottom: 0, left: -20 }}>
|
||||
<CartesianGrid vertical={false} className="stroke-border/50" />
|
||||
<XAxis
|
||||
dataKey="point"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
height={40}
|
||||
tick={{ fill: 'var(--text-muted)' }}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
className="border-border/50 bg-background-default text-text-default min-w-[180px] [&_.flex.flex-1]:gap-4 [&_.flex.flex-1>span]:whitespace-nowrap"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke="var(--chart-2)"
|
||||
strokeWidth={2}
|
||||
dot={{ fill: 'var(--chart-2)', r: 4 }}
|
||||
/>
|
||||
</LineChart>
|
||||
) : (
|
||||
<BarChart data={chartData} margin={{ top: 10, right: 10, bottom: 0, left: 10 }}>
|
||||
<CartesianGrid vertical={false} className="stroke-border/50" />
|
||||
<XAxis
|
||||
dataKey="point"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
height={40}
|
||||
tick={{ fill: 'var(--text-muted)' }}
|
||||
interval={0}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
indicator="dashed"
|
||||
className="border-border/50 bg-background-default text-text-default min-w-[180px] [&_.flex.flex-1]:gap-4 [&_.flex.flex-1>span]:whitespace-nowrap"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="value"
|
||||
fill="var(--chart-1)"
|
||||
radius={4}
|
||||
maxBarSize={32}
|
||||
/>
|
||||
</BarChart>
|
||||
)}
|
||||
</ResponsiveContainer>
|
||||
{variant === 'line' ? (
|
||||
<LineChart
|
||||
width={288}
|
||||
height={162}
|
||||
data={chartData}
|
||||
margin={{ top: 10, right: 10, bottom: 0, left: -20 }}
|
||||
>
|
||||
<CartesianGrid vertical={false} className="stroke-border/50" />
|
||||
<XAxis
|
||||
dataKey="point"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
height={40}
|
||||
tick={{ fill: 'var(--text-muted)' }}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
className="border-border/50 bg-background-default text-text-default min-w-[180px] [&_.flex.flex-1]:gap-4 [&_.flex.flex-1>span]:whitespace-nowrap"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke="var(--chart-2)"
|
||||
strokeWidth={2}
|
||||
dot={{ fill: 'var(--chart-2)', r: 4 }}
|
||||
/>
|
||||
</LineChart>
|
||||
) : (
|
||||
<BarChart
|
||||
width={288}
|
||||
height={162}
|
||||
data={chartData}
|
||||
margin={{ top: 10, right: 10, bottom: 0, left: 10 }}
|
||||
>
|
||||
<CartesianGrid vertical={false} className="stroke-border/50" />
|
||||
<XAxis
|
||||
dataKey="point"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
height={40}
|
||||
tick={{ fill: 'var(--text-muted)' }}
|
||||
interval={0}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
indicator="dashed"
|
||||
className="border-border/50 bg-background-default text-text-default min-w-[180px] [&_.flex.flex-1]:gap-4 [&_.flex.flex-1>span]:whitespace-nowrap"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="value"
|
||||
fill="var(--chart-1)"
|
||||
radius={4}
|
||||
maxBarSize={32}
|
||||
/>
|
||||
</BarChart>
|
||||
)}
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -41,12 +41,12 @@ export default function HighlightTile({
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-4 h-full flex flex-col justify-between relative z-10">
|
||||
<div className="w-6 h-6">
|
||||
<div className="w-6 h-6 text-text-default dark:text-white">
|
||||
{icon}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-gray-600 dark:text-white/40 text-sm mb-1">{title}</div>
|
||||
<div className="text-gray-600 dark:text-white/60 text-sm mb-1">{title}</div>
|
||||
<div
|
||||
className="text-gray-900 dark:text-white text-2xl font-semibold"
|
||||
style={{ color: accentColor }}
|
||||
@@ -62,4 +62,4 @@ export default function HighlightTile({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,10 +37,10 @@ export default function ListTile({
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-4">
|
||||
<div className="w-6 h-6 mb-4">
|
||||
<div className="w-6 h-6 mb-4 text-text-default dark:text-white">
|
||||
{icon}
|
||||
</div>
|
||||
<div className="text-gray-600 dark:text-white/40 text-sm mb-4">
|
||||
<div className="text-gray-600 dark:text-white/60 text-sm mb-4">
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,4 +78,4 @@ export default function ListTile({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTimelineStyles } from '../../hooks/useTimelineStyles';
|
||||
import { ChartConfig, ChartContainer } from "@/components/ui/chart";
|
||||
import { PieChart, Pie, Cell, Sector, ResponsiveContainer } from 'recharts';
|
||||
import { PieChart, Pie, Cell, Sector } from 'recharts';
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface PieChartSegment {
|
||||
value: number;
|
||||
@@ -116,22 +116,6 @@ export default function PieChartTile({
|
||||
chartColor: `var(--chart-${index + 1})` // Use chart-1, chart-2, chart-3, etc.
|
||||
}));
|
||||
|
||||
// Create chart configuration using the chart color variables
|
||||
const chartConfig = {
|
||||
[segments[0].label.toLowerCase()]: {
|
||||
label: segments[0].label,
|
||||
color: 'var(--chart-1)'
|
||||
},
|
||||
[segments[1].label.toLowerCase()]: {
|
||||
label: segments[1].label,
|
||||
color: 'var(--chart-2)'
|
||||
},
|
||||
[segments[2].label.toLowerCase()]: {
|
||||
label: segments[2].label,
|
||||
color: 'var(--chart-3)'
|
||||
}
|
||||
} satisfies ChartConfig;
|
||||
|
||||
const onPieEnter = (_: any, index: number) => {
|
||||
setActiveIndex(index);
|
||||
};
|
||||
@@ -152,50 +136,66 @@ export default function PieChartTile({
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-4">
|
||||
<div className="w-6 h-6 mb-4 text-text-default">
|
||||
<div className="w-6 h-6 mb-4 text-text-default dark:text-white">
|
||||
{icon}
|
||||
</div>
|
||||
<div className="text-text-muted text-sm">
|
||||
<div className="text-text-muted dark:text-white/60 text-sm">
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pie Chart */}
|
||||
<div className="flex-1 flex items-center justify-center p-4">
|
||||
<div style={{ width: '100%', height: '260px', position: 'relative' }}>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<ResponsiveContainer>
|
||||
<PieChart margin={{ top: 30, right: 40, bottom: 10, left: 40 }}>
|
||||
<Pie
|
||||
activeIndex={activeIndex}
|
||||
activeShape={renderActiveShape}
|
||||
data={chartData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={45}
|
||||
outerRadius={65}
|
||||
paddingAngle={5}
|
||||
dataKey="value"
|
||||
onMouseEnter={onPieEnter}
|
||||
cornerRadius={4}
|
||||
label={renderCustomizedLabel}
|
||||
labelLine={false}
|
||||
startAngle={90}
|
||||
endAngle={-270}
|
||||
isAnimationActive={false}
|
||||
>
|
||||
{chartData.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={entry.chartColor}
|
||||
stroke="var(--background-default)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</ChartContainer>
|
||||
<div
|
||||
className={cn(
|
||||
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground",
|
||||
"[&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50",
|
||||
"[&_.recharts-curve.recharts-tooltip-cursor]:stroke-border",
|
||||
"[&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border",
|
||||
"[&_.recharts-radial-bar-background-sector]:fill-muted",
|
||||
"[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted",
|
||||
"[&_.recharts-reference-line_[stroke='#ccc']]:stroke-border",
|
||||
"flex justify-center text-xs",
|
||||
"[&_.recharts-dot[stroke='#fff']]:stroke-transparent",
|
||||
"[&_.recharts-layer]:outline-hidden",
|
||||
"[&_.recharts-sector]:outline-hidden",
|
||||
"[&_.recharts-sector[stroke='#fff']]:stroke-transparent",
|
||||
"[&_.recharts-surface]:outline-hidden"
|
||||
)}
|
||||
>
|
||||
<PieChart
|
||||
width={288}
|
||||
height={162}
|
||||
margin={{ top: 30, right: 40, bottom: 10, left: 40 }}
|
||||
>
|
||||
<Pie
|
||||
activeIndex={activeIndex}
|
||||
activeShape={renderActiveShape}
|
||||
data={chartData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={45}
|
||||
outerRadius={65}
|
||||
paddingAngle={5}
|
||||
dataKey="value"
|
||||
onMouseEnter={onPieEnter}
|
||||
cornerRadius={4}
|
||||
label={renderCustomizedLabel}
|
||||
labelLine={false}
|
||||
startAngle={90}
|
||||
endAngle={-270}
|
||||
isAnimationActive={false}
|
||||
>
|
||||
{chartData.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={entry.chartColor}
|
||||
stroke="var(--background-default)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -42,9 +42,7 @@ function ChartContainer({
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
config: ChartConfig;
|
||||
children: React.ComponentProps<
|
||||
typeof RechartsPrimitive.ResponsiveContainer
|
||||
>["children"];
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const uniqueId = React.useId();
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
|
||||
@@ -61,9 +59,7 @@ function ChartContainer({
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>
|
||||
{children}
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
{children}
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-[#2E2E2E] text-[#FFFFFF] animate-in fade-in-0 zoom-in-95 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 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="fill-[#2E2E2E]" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
@@ -0,0 +1,26 @@
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
|
||||
interface TimelineContextType {
|
||||
currentDate: Date;
|
||||
setCurrentDate: (date: Date) => void;
|
||||
}
|
||||
|
||||
const TimelineContext = createContext<TimelineContextType | undefined>(undefined);
|
||||
|
||||
export function TimelineProvider({ children }: { children: React.ReactNode }) {
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
|
||||
return (
|
||||
<TimelineContext.Provider value={{ currentDate, setCurrentDate }}>
|
||||
{children}
|
||||
</TimelineContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTimeline() {
|
||||
const context = useContext(TimelineContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTimeline must be used within a TimelineProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import React, { Suspense, useState } from 'react';
|
||||
import { Outlet } from '@tanstack/react-router';
|
||||
import { FloatingFilters } from '../components/filters/FloatingFilters';
|
||||
import SuspenseLoader from '../components/SuspenseLoader';
|
||||
import { FilterOption } from '../components/filters/types';
|
||||
import { DarkModeToggle } from '../components/DarkModeToggle';
|
||||
import { DateDisplay } from '../components/DateDisplay';
|
||||
import { TimelineProvider } from '../contexts/TimelineContext';
|
||||
import { FloatingChat } from '../components/chat/FloatingChat';
|
||||
import { ChatInput } from '../components/chat/ChatInput';
|
||||
|
||||
const defaultFilters: FilterOption[] = [
|
||||
{ id: 'all', label: 'All', isActive: true },
|
||||
{ id: 'metrics', label: 'Metrics', isActive: false },
|
||||
{ id: 'tasks', label: 'Tasks', isActive: false },
|
||||
{ id: 'projects', label: 'Projects', isActive: false },
|
||||
{ id: 'automations', label: 'Automations', isActive: false },
|
||||
{ id: 'problems', label: 'Problems', isActive: false },
|
||||
];
|
||||
|
||||
const getFilterColor = (filterId: string): string => {
|
||||
switch (filterId) {
|
||||
case 'tasks':
|
||||
return '#05C168';
|
||||
case 'projects':
|
||||
return '#0066FF';
|
||||
case 'automations':
|
||||
return '#B18CFF';
|
||||
case 'problems':
|
||||
return '#FF2E6C';
|
||||
default:
|
||||
return 'transparent';
|
||||
}
|
||||
};
|
||||
|
||||
export const MainLayout: React.FC = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(event.currentTarget);
|
||||
const message = formData.get('message') as string;
|
||||
|
||||
if (!message?.trim()) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// TODO: Implement your message handling logic here
|
||||
console.log('Sending message:', message);
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStopGeneration = () => {
|
||||
// TODO: Implement your stop generation logic here
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<TimelineProvider>
|
||||
<div className="min-h-screen bg-background-default dark:bg-zinc-800 transition-colors duration-200">
|
||||
<div className="titlebar-drag-region" />
|
||||
<DateDisplay />
|
||||
<div className="h-10 w-full" />
|
||||
|
||||
<FloatingFilters>
|
||||
<div filters={defaultFilters}>
|
||||
<div className="flex justify-center w-full px-4 pt-4">
|
||||
<div className="inline-flex gap-3 justify-center">
|
||||
{defaultFilters.map((filter) => (
|
||||
<button
|
||||
key={filter.id}
|
||||
className={`
|
||||
cursor-pointer
|
||||
px-4 py-2 rounded-full text-sm font-light transition-all
|
||||
shadow-[0_0_13.7px_rgba(0,0,0,0.04)]
|
||||
dark:shadow-[0_0_24px_rgba(255,255,255,0.08)]
|
||||
flex items-center gap-2
|
||||
${filter.isActive
|
||||
? 'bg-background-inverse text-text-inverse'
|
||||
: 'bg-background-default text-text-muted hover:text-text-default dark:text-white/60 dark:hover:text-white'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{filter.id !== 'all' && filter.id !== 'metrics' && (
|
||||
<div
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: getFilterColor(filter.id) }}
|
||||
/>
|
||||
)}
|
||||
{filter.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FloatingFilters>
|
||||
|
||||
<main className="w-full pb-32">
|
||||
<Suspense fallback={<SuspenseLoader />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</main>
|
||||
|
||||
<FloatingChat>
|
||||
<ChatInput
|
||||
handleSubmit={handleSubmit}
|
||||
isLoading={isLoading}
|
||||
onStop={handleStopGeneration}
|
||||
/>
|
||||
</FloatingChat>
|
||||
|
||||
<DarkModeToggle />
|
||||
</div>
|
||||
</TimelineProvider>
|
||||
);
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import tailwindcss_animate from 'tailwindcss-animate';
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
darkMode: 'media', // Enable dark mode and use the system preference
|
||||
darkMode: 'class', // Change to class-based dark mode
|
||||
plugins: [tailwindcss_animate, generated],
|
||||
theme: {
|
||||
extend: {
|
||||
@@ -89,4 +89,4 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user