diff --git a/ui-v2/electron/main.ts b/ui-v2/electron/main.ts
index b329fd92..bd3e79de 100644
--- a/ui-v2/electron/main.ts
+++ b/ui-v2/electron/main.ts
@@ -26,8 +26,12 @@ async function createWindow() {
// Create the browser window with headless options when needed
const mainWindow = new BrowserWindow({
+ titleBarStyle: process.platform === 'darwin' ? 'hidden' : 'default',
+ trafficLightPosition: process.platform === 'darwin' ? { x: 16, y: 10 } : undefined,
+ frame: false,
width: 1200,
height: 800,
+ minWidth: 800,
...(isHeadless
? {
show: false,
diff --git a/ui-v2/src/App.tsx b/ui-v2/src/App.tsx
index fc11393e..937b09b8 100644
--- a/ui-v2/src/App.tsx
+++ b/ui-v2/src/App.tsx
@@ -2,19 +2,12 @@ import React, { Suspense } from 'react';
import { Outlet } from '@tanstack/react-router';
-import GooseLogo from './components/GooseLogo';
import SuspenseLoader from './components/SuspenseLoader';
const App: React.FC = (): React.ReactElement => {
return (
}>
-
+
);
};
diff --git a/ui-v2/src/assets/backgrounds/clock-bg.png b/ui-v2/src/assets/backgrounds/clock-bg.png
new file mode 100644
index 00000000..176c01a7
Binary files /dev/null and b/ui-v2/src/assets/backgrounds/clock-bg.png differ
diff --git a/ui-v2/src/assets/backgrounds/wave-bg.png b/ui-v2/src/assets/backgrounds/wave-bg.png
new file mode 100644
index 00000000..176c01a7
Binary files /dev/null and b/ui-v2/src/assets/backgrounds/wave-bg.png differ
diff --git a/ui-v2/src/assets/logo.svg b/ui-v2/src/assets/logo.svg
new file mode 100644
index 00000000..04aa42d4
--- /dev/null
+++ b/ui-v2/src/assets/logo.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui-v2/src/components/BrandCard.tsx b/ui-v2/src/components/BrandCard.tsx
new file mode 100644
index 00000000..d2512043
--- /dev/null
+++ b/ui-v2/src/components/BrandCard.tsx
@@ -0,0 +1,139 @@
+import React from 'react';
+
+interface BrandCardProps {
+ date?: Date;
+ className?: string;
+}
+
+// Array of congratulatory messages for past days
+const pastDayMessages = [
+ { title: "Great work!", message: "You accomplished so much" },
+ { title: "Well done!", message: "Another successful day" },
+ { title: "Fantastic job!", message: "Making progress every day" },
+ { title: "Nice one!", message: "Another day in the books" },
+ { title: "Awesome work!", message: "Keep up the momentum" }
+];
+
+export default function BrandCard({ date, className = '' }: BrandCardProps) {
+ const isToday = date ? new Date().toDateString() === date.toDateString() : true;
+
+ // Get a consistent message for each date
+ const getPastDayMessage = (date: Date) => {
+ // Use the date's day as an index to select a message
+ const index = date.getDate() % pastDayMessages.length;
+ return pastDayMessages[index];
+ };
+
+ // Get message for past days
+ const pastMessage = date ? getPastDayMessage(date) : pastDayMessages[0];
+
+ return (
+
+ {/* Content */}
+
+ {/* Logo */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Text content - bottom */}
+
+ {isToday ? (
+ <>
+ {/* Today's content */}
+
+ Good morning
+
+
+
+ You've got 3 major updates this morning
+
+ >
+ ) : (
+ <>
+ {/* Past/Future date content */}
+
+ {pastMessage?.title || 'Hello'}
+
+
+
+ {pastMessage?.message || 'Great work'}
+
+ >
+ )}
+
+
+ );
+}
diff --git a/ui-v2/src/components/Button.test.tsx b/ui-v2/src/components/Button.test.tsx
deleted file mode 100644
index ee16a9a8..00000000
--- a/ui-v2/src/components/Button.test.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { describe, it, expect, vi } from 'vitest';
-
-import { Button } from './Button';
-
-describe('Button', () => {
- it('renders with children', () => {
- render(Click me );
- expect(screen.getByText('Click me')).toBeInTheDocument();
- });
-
- it('calls onClick handler when clicked', async () => {
- const handleClick = vi.fn();
- render(Click me );
-
- await userEvent.click(screen.getByText('Click me'));
- expect(handleClick).toHaveBeenCalledTimes(1);
- });
-
- it('applies variant styles correctly', () => {
- render(Secondary Button );
- const button = screen.getByText('Secondary Button');
-
- expect(button).toHaveStyle({
- backgroundColor: '#6c757d',
- });
- });
-});
diff --git a/ui-v2/src/components/Button.tsx b/ui-v2/src/components/Button.tsx
deleted file mode 100644
index 8a8bbf4a..00000000
--- a/ui-v2/src/components/Button.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import React from 'react';
-
-import { electronService } from '../services/electron';
-
-interface ButtonProps {
- onClick?: () => void;
- children: React.ReactNode;
- copyText?: string;
- variant?: 'primary' | 'secondary';
- className?: string;
-}
-
-export const Button: React.FC = ({
- onClick,
- children,
- copyText,
- variant = 'primary',
- className = '',
-}) => {
- const handleClick = async () => {
- if (copyText) {
- try {
- console.log('Attempting to copy text:', copyText);
- await electronService.copyToClipboard(copyText);
- console.log('Text copied successfully');
- } catch (error) {
- console.error('Failed to copy:', error);
- }
- }
-
- if (onClick) {
- onClick();
- }
- };
-
- const getVariantStyles = () => {
- switch (variant) {
- case 'secondary':
- return {
- backgroundColor: '#6c757d',
- color: 'white',
- };
- case 'primary':
- default:
- return {
- backgroundColor: '#4CAF50',
- color: 'white',
- };
- }
- };
-
- return (
-
- {children}
-
- );
-};
diff --git a/ui-v2/src/components/ChartTile.tsx b/ui-v2/src/components/ChartTile.tsx
new file mode 100644
index 00000000..74dbece4
--- /dev/null
+++ b/ui-v2/src/components/ChartTile.tsx
@@ -0,0 +1,152 @@
+import React from 'react';
+import { useTimelineStyles } from '../hooks/useTimelineStyles';
+
+interface ChartTileProps {
+ title: string;
+ value: string;
+ trend?: string;
+ data: number[];
+ icon: React.ReactNode;
+ variant?: 'line' | 'bar';
+ date?: Date;
+}
+
+export default function ChartTile({
+ title,
+ value,
+ trend,
+ data,
+ icon,
+ variant = 'line',
+ date
+}: ChartTileProps) {
+ const { contentCardStyle } = useTimelineStyles(date);
+
+ // Convert data points to SVG coordinates
+ const createSmoothPath = () => {
+ const points = data.map((value, index) => {
+ const x = (index / (data.length - 1)) * 100;
+ const y = 100 - ((value - Math.min(...data)) / (Math.max(...data) - Math.min(...data))) * 100;
+ return [x, y];
+ });
+
+ let path = `M ${points[0][0]},${points[0][1]}`;
+ for (let i = 0; i < points.length - 1; i++) {
+ const current = points[i];
+ const next = points[i + 1];
+ const controlPoint1X = current[0] + (next[0] - current[0]) / 3;
+ const controlPoint2X = current[0] + 2 * (next[0] - current[0]) / 3;
+ path += ` C ${controlPoint1X},${current[1]} ${controlPoint2X},${next[1]} ${next[0]},${next[1]}`;
+ }
+ return path;
+ };
+
+ // Create bar chart elements
+ const createBars = () => {
+ const maxValue = Math.max(...data.map(Math.abs));
+ const barWidth = 8;
+ const spacing = (100 - (data.length * barWidth)) / (data.length - 1);
+
+ return data.map((value, index) => {
+ const x = index * (barWidth + spacing);
+ const height = Math.abs(value) / maxValue * 50;
+ const y = value > 0 ? 50 - height : 50;
+
+ return {
+ x,
+ y,
+ height,
+ isPositive: value > 0
+ };
+ });
+ };
+
+ return (
+
+ {/* Header section with icon */}
+
+
+ {icon}
+
+
+
+
{title}
+
+ {value}
+ {trend && {trend} }
+
+
+
+
+ {/* Chart Container */}
+
+
+ {variant === 'line' ? (
+ <>
+
+
+
+
+
+
+
+
+
+ {/* Data points */}
+ {data.map((value, index) => {
+ const x = (index / (data.length - 1)) * 100;
+ const y = 100 - ((value - Math.min(...data)) / (Math.max(...data) - Math.min(...data))) * 100;
+ return (
+
+ );
+ })}
+ >
+ ) : (
+ <>
+ {createBars().map((bar, index) => (
+
+ ))}
+ >
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/ui-v2/src/components/ClockTile.tsx b/ui-v2/src/components/ClockTile.tsx
new file mode 100644
index 00000000..1343e995
--- /dev/null
+++ b/ui-v2/src/components/ClockTile.tsx
@@ -0,0 +1,80 @@
+
+import React, { useState, useEffect } from 'react';
+import { useTimelineStyles } from '../hooks/useTimelineStyles';
+import waveBg from '../assets/backgrounds/wave-bg.png';
+
+interface ClockCardProps {
+ date?: Date;
+}
+
+export default function ClockTile({ date }: ClockCardProps) {
+ const { contentCardStyle, isPastDate } = useTimelineStyles(date);
+ const [currentTime, setCurrentTime] = useState(new Date());
+
+ // Don't render for past dates
+ if (isPastDate) {
+ return null;
+ }
+
+ // Update time every second for current day
+ useEffect(() => {
+ const timer = setInterval(() => {
+ setCurrentTime(new Date());
+ }, 1000);
+
+ return () => clearInterval(timer);
+ }, []);
+
+ // Format hours (12-hour format)
+ const hours = currentTime.getHours() % 12 || 12;
+ const minutes = currentTime.getMinutes().toString().padStart(2, '0');
+ const period = currentTime.getHours() >= 12 ? 'PM' : 'AM';
+
+ // Format day name
+ const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+ const dayName = dayNames[currentTime.getDay()];
+
+ return (
+
+ {/* Background Image with Gradient Overlay */}
+
+
+ {/* Gradient Overlay */}
+
+
+ {/* Time Display */}
+
+
+
+ {hours}:{minutes}
+
+
+ {period}
+
+
+
+ {dayName}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/ui-v2/src/components/GooseLogo.tsx b/ui-v2/src/components/GooseLogo.tsx
deleted file mode 100644
index bd1f026e..00000000
--- a/ui-v2/src/components/GooseLogo.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { FC } from 'react';
-
-import { Goose, Rain } from './icons/Goose';
-
-interface GooseLogoProps {
- className?: string;
- size?: 'default' | 'small';
- hover?: boolean;
-}
-
-const GooseLogo: FC = ({ className = '', size = 'default', hover = true }) => {
- const sizes = {
- default: {
- frame: 'w-16 h-16',
- rain: 'w-[275px] h-[275px]',
- goose: 'w-16 h-16',
- },
- small: {
- frame: 'w-8 h-8',
- rain: 'w-[150px] h-[150px]',
- goose: 'w-8 h-8',
- },
- };
- return (
-
-
-
-
- );
-};
-
-export default GooseLogo;
diff --git a/ui-v2/src/components/HighlightTile.tsx b/ui-v2/src/components/HighlightTile.tsx
new file mode 100644
index 00000000..cb44f526
--- /dev/null
+++ b/ui-v2/src/components/HighlightTile.tsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import { useTimelineStyles } from '../hooks/useTimelineStyles';
+
+interface HighlightTileProps {
+ title: string;
+ value: string;
+ icon: React.ReactNode;
+ subtitle?: string;
+ date?: Date;
+ accentColor?: string;
+}
+
+export default function HighlightTile({
+ title,
+ value,
+ icon,
+ subtitle,
+ date,
+ accentColor = '#00CAF7'
+}: HighlightTileProps) {
+ const { contentCardStyle } = useTimelineStyles(date);
+
+ return (
+
+ {/* Background accent */}
+
+
+ {/* Content */}
+
+
+ {icon}
+
+
+
+
{title}
+
+ {value}
+
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/ui-v2/src/components/ListTile.tsx b/ui-v2/src/components/ListTile.tsx
new file mode 100644
index 00000000..8c41cccf
--- /dev/null
+++ b/ui-v2/src/components/ListTile.tsx
@@ -0,0 +1,81 @@
+import React from 'react';
+import { useTimelineStyles } from '../hooks/useTimelineStyles';
+
+interface ListItem {
+ text: string;
+ value?: string;
+ color?: string;
+}
+
+interface ListTileProps {
+ title: string;
+ icon: React.ReactNode;
+ items: ListItem[];
+ date?: Date;
+}
+
+export default function ListTile({
+ title,
+ icon,
+ items,
+ date
+}: ListTileProps) {
+ const { contentCardStyle } = useTimelineStyles(date);
+
+ return (
+
+ {/* Header */}
+
+
+ {icon}
+
+
+ {title}
+
+
+
+ {/* List */}
+
+
+ {items.map((item, index) => (
+
+
+ {item.value && (
+
+ {item.value}
+
+ )}
+
+ ))}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/ui-v2/src/components/PieChartTile.tsx b/ui-v2/src/components/PieChartTile.tsx
new file mode 100644
index 00000000..62585218
--- /dev/null
+++ b/ui-v2/src/components/PieChartTile.tsx
@@ -0,0 +1,123 @@
+import React from 'react';
+import { useTimelineStyles } from '../hooks/useTimelineStyles';
+
+interface PieChartSegment {
+ value: number;
+ color: string;
+ label: string;
+}
+
+interface PieChartTileProps {
+ title: string;
+ icon: React.ReactNode;
+ segments: PieChartSegment[];
+ date?: Date;
+}
+
+export default function PieChartTile({
+ title,
+ icon,
+ segments,
+ date
+}: PieChartTileProps) {
+ const { contentCardStyle } = useTimelineStyles(date);
+
+ const total = segments.reduce((sum, segment) => sum + segment.value, 0);
+ let currentAngle = 0;
+
+ const createPieSegments = () => {
+ return segments.map((segment, index) => {
+ const startAngle = currentAngle;
+ const percentage = segment.value / total;
+ const angle = percentage * 360;
+ currentAngle += angle;
+
+ const startX = Math.cos((startAngle - 90) * Math.PI / 180) * 32 + 50;
+ const startY = Math.sin((startAngle - 90) * Math.PI / 180) * 32 + 50;
+ const endX = Math.cos((currentAngle - 90) * Math.PI / 180) * 32 + 50;
+ const endY = Math.sin((currentAngle - 90) * Math.PI / 180) * 32 + 50;
+
+ const largeArcFlag = angle > 180 ? 1 : 0;
+
+ const d = [
+ `M 50 50`,
+ `L ${startX} ${startY}`,
+ `A 32 32 0 ${largeArcFlag} 1 ${endX} ${endY}`,
+ 'Z'
+ ].join(' ');
+
+ return {
+ path: d,
+ color: segment.color,
+ label: segment.label,
+ percentage: (percentage * 100).toFixed(1)
+ };
+ });
+ };
+
+ const pieSegments = createPieSegments();
+
+ return (
+
+ {/* Header */}
+
+
+ {icon}
+
+
+ {title}
+
+
+
+ {/* Pie Chart */}
+
+
+
+ {pieSegments.map((segment, index) => (
+
+ ))}
+
+
+
+ {/* Legend */}
+
+ {pieSegments.map((segment, index) => (
+
+
+
+
+ {segment.label}
+
+
+
+ {segment.percentage}%
+
+
+ ))}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/ui-v2/src/components/Timeline.tsx b/ui-v2/src/components/Timeline.tsx
new file mode 100644
index 00000000..5cd6d83b
--- /dev/null
+++ b/ui-v2/src/components/Timeline.tsx
@@ -0,0 +1,413 @@
+import React, { useRef, useMemo, useEffect } from 'react';
+import ChartTile from './ChartTile';
+import HighlightTile from './HighlightTile';
+import PieChartTile from './PieChartTile';
+import ListTile from './ListTile';
+import ClockTile from './ClockTile';
+import TimelineDots from './TimelineDots';
+import { ChartLineIcon, ChartBarIcon, PieChartIcon, ListIcon, StarIcon, TrendingUpIcon } from './icons';
+
+const generateRandomData = (length: number) => Array.from({ length }, () => Math.floor(Math.random() * 100));
+
+const generateTileData = (date: Date) => {
+ const isToday = new Date().toDateString() === date.toDateString();
+
+ return {
+ left: [
+ // Performance metrics
+ {
+ type: 'chart' as const,
+ props: {
+ title: 'Daily Activity',
+ value: '487',
+ trend: '↑ 12%',
+ data: generateRandomData(7),
+ icon: ,
+ variant: 'line' as const,
+ date
+ }
+ },
+ {
+ type: 'highlight' as const,
+ props: {
+ title: 'Achievement',
+ value: isToday ? 'New Record!' : 'Great Work',
+ icon: ,
+ subtitle: isToday ? 'Personal best today' : 'Keep it up',
+ date,
+ accentColor: '#FFB800'
+ }
+ },
+ {
+ type: 'pie' as const,
+ props: {
+ title: 'Task Distribution',
+ icon: ,
+ segments: [
+ { value: 45, color: '#00CAF7', label: 'Completed' },
+ { value: 35, color: '#FFB800', label: 'In Progress' },
+ { value: 20, color: '#FF4444', label: 'Pending' }
+ ],
+ date
+ }
+ },
+ // Additional metrics
+ {
+ type: 'chart' as const,
+ props: {
+ title: 'Response Time',
+ value: '245ms',
+ trend: '↓ 18%',
+ data: generateRandomData(7),
+ icon: ,
+ variant: 'bar' as const,
+ date
+ }
+ },
+ {
+ type: 'highlight' as const,
+ props: {
+ title: 'User Satisfaction',
+ value: '98%',
+ icon: ,
+ subtitle: 'Based on feedback',
+ date,
+ accentColor: '#4CAF50'
+ }
+ },
+ {
+ type: 'list' as const,
+ props: {
+ title: 'Top Priorities',
+ icon: ,
+ items: [
+ { text: 'Project Alpha', value: '87%', color: '#00CAF7' },
+ { text: 'Team Meeting', value: '2:30 PM' },
+ { text: 'Review Code', value: '13', color: '#FFB800' },
+ { text: 'Deploy Update', value: 'Done', color: '#4CAF50' }
+ ],
+ date
+ }
+ },
+ // System metrics
+ {
+ type: 'chart' as const,
+ props: {
+ title: 'System Load',
+ value: '42%',
+ trend: '↑ 5%',
+ data: generateRandomData(7),
+ icon: ,
+ variant: 'line' as const,
+ date
+ }
+ },
+ {
+ type: 'pie' as const,
+ props: {
+ title: 'Storage Usage',
+ icon: ,
+ segments: [
+ { value: 60, color: '#4CAF50', label: 'Free' },
+ { value: 25, color: '#FFB800', label: 'Used' },
+ { value: 15, color: '#FF4444', label: 'System' }
+ ],
+ date
+ }
+ }
+ ],
+ right: [
+ // Performance metrics
+ {
+ type: 'chart' as const,
+ props: {
+ title: 'Performance',
+ value: '92%',
+ trend: '↑ 8%',
+ data: generateRandomData(7),
+ icon: ,
+ variant: 'bar' as const,
+ date
+ }
+ },
+ // Clock tile
+ {
+ type: 'clock' as const,
+ props: {
+ title: 'Current Time',
+ date
+ }
+ },
+ {
+ type: 'highlight' as const,
+ props: {
+ title: 'Efficiency',
+ value: '+28%',
+ icon: ,
+ subtitle: 'Above target',
+ date,
+ accentColor: '#4CAF50'
+ }
+ },
+ {
+ type: 'pie' as const,
+ props: {
+ title: 'Resource Usage',
+ icon: ,
+ segments: [
+ { value: 55, color: '#4CAF50', label: 'Available' },
+ { value: 30, color: '#FFB800', label: 'In Use' },
+ { value: 15, color: '#FF4444', label: 'Reserved' }
+ ],
+ date
+ }
+ },
+ // Updates and notifications
+ {
+ type: 'list' as const,
+ props: {
+ title: 'Recent Updates',
+ icon: ,
+ items: [
+ { text: 'System Update', value: 'Complete', color: '#4CAF50' },
+ { text: 'New Features', value: '3', color: '#00CAF7' },
+ { text: 'Bug Fixes', value: '7', color: '#FFB800' },
+ { text: 'Performance', value: '+15%', color: '#4CAF50' }
+ ],
+ date
+ }
+ },
+ // Additional metrics
+ {
+ type: 'chart' as const,
+ props: {
+ title: 'User Activity',
+ value: '1,247',
+ trend: '↑ 23%',
+ data: generateRandomData(7),
+ icon: ,
+ variant: 'line' as const,
+ date
+ }
+ },
+ {
+ type: 'highlight' as const,
+ props: {
+ title: 'New Users',
+ value: '+156',
+ icon: ,
+ subtitle: 'Last 24 hours',
+ date,
+ accentColor: '#00CAF7'
+ }
+ },
+ // System health
+ {
+ type: 'pie' as const,
+ props: {
+ title: 'API Health',
+ icon: ,
+ segments: [
+ { value: 75, color: '#4CAF50', label: 'Healthy' },
+ { value: 20, color: '#FFB800', label: 'Warning' },
+ { value: 5, color: '#FF4444', label: 'Critical' }
+ ],
+ date
+ }
+ },
+ {
+ type: 'list' as const,
+ props: {
+ title: 'System Status',
+ icon: ,
+ items: [
+ { text: 'Main API', value: 'Online', color: '#4CAF50' },
+ { text: 'Database', value: '98%', color: '#00CAF7' },
+ { text: 'Cache', value: 'Synced', color: '#4CAF50' },
+ { text: 'CDN', value: 'Active', color: '#4CAF50' }
+ ],
+ date
+ }
+ }
+ ]
+ };
+};
+
+export default function Timeline() {
+ const containerRef = useRef(null);
+ const sectionRefs = useRef<(HTMLDivElement | null)[]>([]);
+
+ const sections = useMemo(() => {
+ const result = [];
+ const today = new Date();
+
+ for (let i = 0; i <= 29; i++) {
+ const date = new Date(today);
+ date.setDate(today.getDate() - i);
+
+ const tileData = generateTileData(date);
+
+ result.push({
+ date,
+ isToday: i === 0,
+ leftTiles: tileData.left,
+ rightTiles: tileData.right
+ });
+ }
+
+ return result;
+ }, []);
+
+ // Function to center the timeline in a section
+ const centerTimeline = (sectionElement: HTMLDivElement) => {
+ if (!sectionElement) return;
+
+ requestAnimationFrame(() => {
+ const totalWidth = sectionElement.scrollWidth;
+ const viewportWidth = sectionElement.clientWidth;
+ const scrollToX = Math.max(0, (totalWidth - viewportWidth) / 2);
+
+ sectionElement.scrollTo({
+ left: scrollToX,
+ behavior: 'smooth'
+ });
+ });
+ };
+
+ useEffect(() => {
+ // Create the intersection observer
+ const observer = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ const section = entry.target as HTMLDivElement;
+ centerTimeline(section);
+ }
+ });
+ },
+ {
+ threshold: 0.5,
+ rootMargin: '0px'
+ }
+ );
+
+ // Add resize handler
+ const handleResize = () => {
+ // Find the currently visible section
+ const visibleSection = sectionRefs.current.find(section => {
+ 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);
+ }
+ };
+
+ // Add resize event listener
+ window.addEventListener('resize', handleResize);
+
+ // Observe all sections
+ sectionRefs.current.forEach((section) => {
+ if (section) {
+ observer.observe(section);
+ centerTimeline(section);
+ }
+ });
+
+ // Cleanup function
+ return () => {
+ window.removeEventListener('resize', handleResize);
+ sectionRefs.current.forEach((section) => {
+ if (section) {
+ observer.unobserve(section);
+ }
+ });
+ };
+ }, []);
+
+ const renderTile = (tile: any, index: number) => {
+ switch (tile.type) {
+ case 'chart':
+ return ;
+ case 'highlight':
+ return ;
+ case 'pie':
+ return ;
+ case 'list':
+ return ;
+ case 'clock':
+ return ;
+ default:
+ return null;
+ }
+ };
+
+ return (
+
+ {sections.map((section, index) => (
+
sectionRefs.current[index] = el}
+ className="h-screen relative snap-center snap-always overflow-x-scroll snap-x snap-mandatory scrollbar-hide"
+ >
+
+ {/* Main flex container */}
+
+
+ {/* Left Grid */}
+
+
+ {section.leftTiles.map((tile, i) => (
+
+ {renderTile(tile, i)}
+
+ ))}
+
+
+
+ {/* Center Timeline */}
+
+ {/* Upper Timeline Dots */}
+
+
+ {/* Date Display */}
+
+
+ {section.date.toLocaleString('default', { month: 'short' })}
+
+
+ {section.date.getDate()}
+
+
+ {section.date.toLocaleString('default', { weekday: 'long' })}
+
+
+
+ {/* Lower Timeline Dots */}
+
+
+
+ {/* Right Grid */}
+
+
+ {section.rightTiles.map((tile, i) => (
+
+ {renderTile(tile, i)}
+
+ ))}
+
+
+
+
+
+ ))}
+
+ );
+}
\ No newline at end of file
diff --git a/ui-v2/src/components/TimelineContext.tsx b/ui-v2/src/components/TimelineContext.tsx
new file mode 100644
index 00000000..65275961
--- /dev/null
+++ b/ui-v2/src/components/TimelineContext.tsx
@@ -0,0 +1,31 @@
+import React, { createContext, useContext, useState, useCallback } from 'react';
+
+interface TimelineContextType {
+ currentDate: Date;
+ setCurrentDate: (date: Date) => void;
+ isCurrentDate: (date: Date) => boolean;
+}
+
+const TimelineContext = createContext(undefined);
+
+export function TimelineProvider({ children }: { children: React.ReactNode }) {
+ const [currentDate, setCurrentDate] = useState(new Date());
+
+ const isCurrentDate = useCallback((date: Date) => {
+ return date.toDateString() === new Date().toDateString();
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useTimeline() {
+ const context = useContext(TimelineContext);
+ if (context === undefined) {
+ throw new Error('useTimeline must be used within a TimelineProvider');
+ }
+ return context;
+}
diff --git a/ui-v2/src/components/TimelineDots.tsx b/ui-v2/src/components/TimelineDots.tsx
new file mode 100644
index 00000000..ef152a76
--- /dev/null
+++ b/ui-v2/src/components/TimelineDots.tsx
@@ -0,0 +1,96 @@
+import React, { useMemo } from 'react';
+
+interface TimelineDotsProps {
+ height: number | string;
+ isUpper?: boolean;
+ isCurrentDay?: boolean;
+}
+
+interface Dot {
+ top: string;
+ size: number;
+ opacity: number;
+}
+
+export default function TimelineDots({ height, isUpper = false, isCurrentDay = false }: TimelineDotsProps) {
+ // Generate random dots with clusters
+ const dots = useMemo(() => {
+ const generateDots = () => {
+ const dots: Dot[] = [];
+ const numDots = Math.floor(Math.random() * 8) + 8; // 8-15 dots
+
+ // Create 2-3 cluster points
+ const clusterPoints = Array.from({ length: Math.floor(Math.random() * 2) + 2 },
+ () => Math.random() * 100);
+
+ for (let i = 0; i < numDots; i++) {
+ // Decide if this dot should be part of a cluster
+ const isCluster = Math.random() < 0.7; // 70% chance of being in a cluster
+
+ let top;
+ if (isCluster) {
+ // Pick a random cluster point and add some variation
+ const clusterPoint = clusterPoints[Math.floor(Math.random() * clusterPoints.length)];
+ top = clusterPoint + (Math.random() - 0.5) * 15; // ±7.5% variation
+ } else {
+ top = Math.random() * 100;
+ }
+
+ // Ensure dot is within bounds
+ top = Math.max(5, Math.min(95, top));
+
+ dots.push({
+ top: `${top}%`,
+ size: Math.random() * 2 + 2, // 2-4px
+ opacity: Math.random() * 0.5 + 0.2, // 0.2-0.7 opacity
+ });
+ }
+ return dots;
+ };
+
+ return generateDots();
+ }, []); // Empty dependency array means this only runs once
+
+ return (
+
+ {/* Main line */}
+
+ {/* Top dot for current day */}
+ {isUpper && isCurrentDay && (
+
+ )}
+
+ {/* Random dots */}
+ {dots.map((dot, index) => (
+
+ ))}
+
+
+ );
+};
diff --git a/ui-v2/src/components/ValueCard.tsx b/ui-v2/src/components/ValueCard.tsx
new file mode 100644
index 00000000..c85c3e18
--- /dev/null
+++ b/ui-v2/src/components/ValueCard.tsx
@@ -0,0 +1,130 @@
+import React from 'react';
+
+interface BrandCardProps {
+ date?: Date;
+ className?: string;
+}
+
+export default function BrandCard({ }: BrandCardProps) {
+ const isToday = date ? new Date().toDateString() === date.toDateString() : true;
+
+ // Get a consistent message for each date
+ const getPastDayMessage = (date: Date) => {
+ // Use the date's day as an index to select a message
+ const index = date.getDate() % pastDayMessages.length;
+ return pastDayMessages[index];
+ };
+
+ // Get message for past days
+ const pastMessage = date ? getPastDayMessage(date) : pastDayMessages[0];
+
+ return (
+
+ {/* Content */}
+
+ {/* Logo */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Text content - bottom */}
+
+ {isToday ? (
+ <>
+ {/* Today's content */}
+
+ Good morning
+
+
+
+ You've got 3 major updates this morning
+
+ >
+ ) : (
+ <>
+ {/* Past/Future date content */}
+
+ {pastMessage?.title || 'Hello'}
+
+
+
+ {pastMessage?.message || 'Great work'}
+
+ >
+ )}
+
+
+ );
+}
diff --git a/ui-v2/src/components/icons.tsx b/ui-v2/src/components/icons.tsx
new file mode 100644
index 00000000..3dd6fb0f
--- /dev/null
+++ b/ui-v2/src/components/icons.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+
+export const ChartLineIcon = () => (
+
+
+
+);
+
+export const ChartBarIcon = () => (
+
+
+
+);
+
+export const PieChartIcon = () => (
+
+
+
+);
+
+export const ListIcon = () => (
+
+
+
+);
+
+export const StarIcon = () => (
+
+
+
+);
+
+export const TrendingUpIcon = () => (
+
+
+
+);
\ No newline at end of file
diff --git a/ui-v2/src/components/icons/Goose.tsx b/ui-v2/src/components/icons/Goose.tsx
deleted file mode 100644
index 49d6178c..00000000
--- a/ui-v2/src/components/icons/Goose.tsx
+++ /dev/null
@@ -1,392 +0,0 @@
-import { FC } from 'react';
-
-interface IconProps {
- className?: string;
-}
-
-export const Goose: FC = ({ className = '' }) => {
- return (
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export const Rain: FC = ({ className = '' }) => {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
diff --git a/ui-v2/src/hooks/useTimelineStyles.ts b/ui-v2/src/hooks/useTimelineStyles.ts
new file mode 100644
index 00000000..60a90eaa
--- /dev/null
+++ b/ui-v2/src/hooks/useTimelineStyles.ts
@@ -0,0 +1,35 @@
+import { useTimeline } from '../components/TimelineContext';
+
+interface TimelineStyles {
+ isPastDate: boolean;
+ greetingCardStyle: {
+ background: string;
+ text: string;
+ };
+ contentCardStyle: string;
+}
+
+export function useTimelineStyles(date?: Date): TimelineStyles {
+ const { isCurrentDate } = useTimeline();
+ const isPastDate = date && date < new Date() && !isCurrentDate(date);
+
+ // Content cards match the Tasks Completed tile styling
+ const contentCardStyle = 'bg-white dark:bg-[#121212] shadow-[0_0_13.7px_rgba(0,0,0,0.04)] dark:shadow-[0_0_24px_rgba(255,255,255,0.02)]';
+
+ // Greeting card styles based on date
+ const greetingCardStyle = !isPastDate
+ ? {
+ background: 'bg-textStandard', // Black background
+ text: 'text-white' // White text
+ }
+ : {
+ background: 'bg-gray-100', // Light grey background
+ text: 'text-gray-600' // Darker grey text
+ };
+
+ return {
+ isPastDate,
+ greetingCardStyle,
+ contentCardStyle
+ };
+}
diff --git a/ui-v2/src/routeTree.ts b/ui-v2/src/routeTree.ts
index dd3ba914..d4e91817 100644
--- a/ui-v2/src/routeTree.ts
+++ b/ui-v2/src/routeTree.ts
@@ -1,8 +1,8 @@
import { createRouter } from '@tanstack/react-router';
-import { rootRoute, indexRoute, aboutRoute } from './routes';
+import { rootRoute, timelineRoute } from './routes';
-const routeTree = rootRoute.addChildren([indexRoute, aboutRoute]);
+const routeTree = rootRoute.addChildren([timelineRoute]);
export const router = createRouter({ routeTree });
diff --git a/ui-v2/src/routes/__root.tsx b/ui-v2/src/routes/__root.tsx
index 9b5d54b8..ef7d5032 100644
--- a/ui-v2/src/routes/__root.tsx
+++ b/ui-v2/src/routes/__root.tsx
@@ -6,11 +6,8 @@ export const Route = createRootRoute({
<>
- Home
+ Timeline
{' '}
-
- About
-
diff --git a/ui-v2/src/routes/index.tsx b/ui-v2/src/routes/index.tsx
index 6308b9ab..e41f49cd 100644
--- a/ui-v2/src/routes/index.tsx
+++ b/ui-v2/src/routes/index.tsx
@@ -1,28 +1,21 @@
import { createRootRoute, createRoute } from '@tanstack/react-router';
import App from '../App';
+import Timeline from '../components/Timeline';
+import { TimelineProvider } from '../components/TimelineContext';
export const rootRoute = createRootRoute({
component: App,
});
-export const indexRoute = createRoute({
+export const timelineRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: () => (
-
-
Welcome to Goose v2
-
- ),
-});
-
-export const aboutRoute = createRoute({
- getParentRoute: () => rootRoute,
- path: '/about',
- component: () => (
-
-
About Goose v2
-
An AI assistant for developers
-
+
+
+
+
+
),
});
diff --git a/ui-v2/tailwind.config.js b/ui-v2/tailwind.config.js
index e0a8acef..ff8e1f90 100644
--- a/ui-v2/tailwind.config.js
+++ b/ui-v2/tailwind.config.js
@@ -1,6 +1,7 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
+ darkMode: 'media', // Enable dark mode and use the system preference
theme: {
extend: {
colors: {
@@ -10,4 +11,4 @@ export default {
},
},
plugins: [],
-};
+};
\ No newline at end of file