ui-v2 - added pull down top filter bar from designs and dark mode (#2638)

This commit is contained in:
Zane
2025-05-27 12:18:08 -07:00
committed by GitHub
parent f3588bd54c
commit 363836f151
22 changed files with 1509 additions and 736 deletions
+14 -3
View File
@@ -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>
+493 -568
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -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
View File
@@ -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;
+62
View File
@@ -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>
);
}
+46
View File
@@ -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>
);
}
+89 -17
View File
@@ -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>
+47
View File
@@ -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>
);
};
+115
View File
@@ -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>
);
};
+133
View File
@@ -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>
);
}
+5
View File
@@ -0,0 +1,5 @@
export interface FilterOption {
id: string;
label: string;
isActive: boolean;
}
+70 -62
View File
@@ -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>
+3 -3
View File
@@ -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>
);
}
}
+3 -3
View File
@@ -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>
);
}
}
+54 -54
View File
@@ -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>
+2 -6
View File
@@ -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>
);
+61
View File
@@ -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 };
+26
View File
@@ -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;
}
+120
View File
@@ -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>
);
};
+2 -2
View File
@@ -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 {
},
},
},
};
};