From a078e42432bb40aea2c593f48a62590156899a7a Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Tue, 6 Jan 2026 16:05:57 -0500 Subject: [PATCH 1/2] feat(design-system): Add Python-native theme and core landing page components Part of #167 Added: - lib/python-theme.ts: Python brand colors, syntax highlighting tokens, featured repos config - lib/animations.ts: Comprehensive Framer Motion variants for search, cards, results, buttons - components/ui/GlassCard.tsx: Premium glassmorphism card with hover effects and glow options - components/ui/CodeBlock.tsx: Python syntax highlighting with line numbers and copy functionality - hooks/usePrefersReducedMotion.ts: Accessibility hook for reduced motion preference - hooks/useIsMobile.ts: Responsive design hook for mobile detection - Updated tailwind.config.js with Python colors and syntax tokens The Python theme uses the official Python blue (#4B8BBE) and yellow (#FFD43B) colors that developers recognize from the Python logo. Syntax colors match PyCharm/VS Code for zero cognitive load when reading code. --- frontend/src/components/ui/CodeBlock.tsx | 246 ++++++++++++ frontend/src/components/ui/GlassCard.tsx | 145 +++++++ frontend/src/hooks/useIsMobile.ts | 43 +++ frontend/src/hooks/usePrefersReducedMotion.ts | 42 ++ frontend/src/lib/animations.ts | 360 ++++++++++++++++++ frontend/src/lib/python-theme.ts | 144 +++++++ frontend/tailwind.config.js | 34 ++ 7 files changed, 1014 insertions(+) create mode 100644 frontend/src/components/ui/CodeBlock.tsx create mode 100644 frontend/src/components/ui/GlassCard.tsx create mode 100644 frontend/src/hooks/useIsMobile.ts create mode 100644 frontend/src/hooks/usePrefersReducedMotion.ts create mode 100644 frontend/src/lib/animations.ts create mode 100644 frontend/src/lib/python-theme.ts diff --git a/frontend/src/components/ui/CodeBlock.tsx b/frontend/src/components/ui/CodeBlock.tsx new file mode 100644 index 0000000..a7227b0 --- /dev/null +++ b/frontend/src/components/ui/CodeBlock.tsx @@ -0,0 +1,246 @@ +import { HTMLAttributes, useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { cn } from '@/lib/utils'; +import { syntax, codeBg } from '@/lib/python-theme'; +import { lineHighlightVariants } from '@/lib/animations'; +import { Check, Copy, ExternalLink } from 'lucide-react'; + +interface CodeBlockProps extends HTMLAttributes { + code: string; + language?: 'python' | 'text'; + filename?: string; + lineStart?: number; + highlightLines?: number[]; + showLineNumbers?: boolean; + maxHeight?: string; + onCopy?: () => void; + githubUrl?: string; +} + +// Simple Python tokenizer for syntax highlighting +function tokenizePython(code: string): Array<{ type: string; value: string }> { + const tokens: Array<{ type: string; value: string }> = []; + + const patterns: Array<[string, RegExp]> = [ + ['comment', /^#.*/], + ['docstring', /^("""[\s\S]*?"""|'''[\s\S]*?''')/], + ['fstring', /^f(['"])((?:\\.|(?!\1)[^\\])*)\1/], + ['string', /^(['"])((?:\\.|(?!\1)[^\\])*)\1/], + ['decorator', /^@[\w.]+/], + ['keyword', /^\b(def|class|import|from|return|if|elif|else|for|while|try|except|finally|with|as|yield|lambda|pass|break|continue|raise|assert|global|nonlocal|del|in|not|and|or|is|True|False|None|async|await)\b/], + ['builtin', /^\b(print|len|range|str|int|float|list|dict|set|tuple|bool|type|isinstance|hasattr|getattr|setattr|open|input|super|self|cls)\b/], + ['number', /^\b\d+(\.\d+)?\b/], + ['function', /^\b([a-zA-Z_][a-zA-Z0-9_]*)(?=\s*\()/], + ['className', /^\b([A-Z][a-zA-Z0-9_]*)\b/], + ['parameter', /^\b([a-zA-Z_][a-zA-Z0-9_]*)(?=\s*[=:])/], + ['operator', /^(==|!=|<=|>=|<|>|\+|-|\*|\/|%|\*\*|=|\+=|-=|\|\||&&)/], + ['punctuation', /^[()[\]{}:,.;]/], + ['variable', /^[a-zA-Z_][a-zA-Z0-9_]*/], + ['whitespace', /^\s+/], + ]; + + let remaining = code; + + while (remaining.length > 0) { + let matched = false; + + for (const [type, pattern] of patterns) { + const match = remaining.match(pattern); + if (match) { + tokens.push({ type, value: match[0] }); + remaining = remaining.slice(match[0].length); + matched = true; + break; + } + } + + if (!matched) { + tokens.push({ type: 'text', value: remaining[0] }); + remaining = remaining.slice(1); + } + } + + return tokens; +} + +function getTokenColor(type: string): string { + const colorMap: Record = { + keyword: syntax.keyword, + builtin: syntax.builtin, + function: syntax.function, + className: syntax.className, + decorator: syntax.decorator, + string: syntax.string, + fstring: syntax.string, + docstring: syntax.docstring, + comment: syntax.comment, + number: syntax.number, + parameter: syntax.parameter, + operator: syntax.operator, + punctuation: syntax.punctuation, + variable: syntax.variable, + }; + + return colorMap[type] || syntax.variable; +} + +/** + * CodeBlock - Python code display with syntax highlighting + * + * Features: + * - Python syntax highlighting + * - Line numbers + * - Line highlighting for matches + * - Copy to clipboard + * - GitHub link + * - Hover effects on lines + */ +export function CodeBlock({ + code, + language = 'python', + filename, + lineStart = 1, + highlightLines = [], + showLineNumbers = true, + maxHeight = '400px', + onCopy, + githubUrl, + className, + ...props +}: CodeBlockProps) { + const [copied, setCopied] = useState(false); + const lines = code.split('\n'); + + const handleCopy = async () => { + await navigator.clipboard.writeText(code); + setCopied(true); + onCopy?.(); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+ {/* Header */} + {(filename || githubUrl) && ( +
+ {filename && ( + + {filename} + + )} +
+ + {githubUrl && ( + + + + )} +
+
+ )} + + {/* Code */} +
+
+          
+            {lines.map((line, index) => {
+              const lineNumber = lineStart + index;
+              const isHighlighted = highlightLines.includes(lineNumber);
+              const tokens = language === 'python' ? tokenizePython(line) : [{ type: 'text', value: line }];
+              
+              return (
+                
+                  {showLineNumbers && (
+                    
+                      {lineNumber}
+                    
+                  )}
+                  
+                    {tokens.map((token, tokenIndex) => (
+                      
+                        {token.value}
+                      
+                    ))}
+                    {line === '' && '\u200B'}
+                  
+                
+              );
+            })}
+          
+        
+
+
+ ); +} + +export default CodeBlock; diff --git a/frontend/src/components/ui/GlassCard.tsx b/frontend/src/components/ui/GlassCard.tsx new file mode 100644 index 0000000..0b95f1e --- /dev/null +++ b/frontend/src/components/ui/GlassCard.tsx @@ -0,0 +1,145 @@ +import { forwardRef, HTMLAttributes, ReactNode } from 'react'; +import { motion, HTMLMotionProps } from 'framer-motion'; +import { cn } from '@/lib/utils'; +import { glassCardVariants } from '@/lib/animations'; + +export type GlowColor = 'blue' | 'yellow' | 'none'; + +interface GlassCardProps extends Omit, 'children'> { + children: ReactNode; + glow?: GlowColor; + hover?: boolean; + className?: string; + as?: 'div' | 'article' | 'section'; +} + +const glowStyles: Record = { + blue: 'hover:shadow-[0_0_30px_rgba(75,139,190,0.15)]', + yellow: 'hover:shadow-[0_0_30px_rgba(255,212,59,0.15)]', + none: '', +}; + +/** + * GlassCard - Premium glassmorphism card component + * + * Features: + * - Backdrop blur effect + * - Subtle border that brightens on hover + * - Optional glow effect (Python blue or yellow) + * - Smooth hover animations via Framer Motion + * + * @example + * + *

Search Result

+ *

Code snippet here...

+ *
+ */ +export const GlassCard = forwardRef( + ( + { + children, + glow = 'none', + hover = true, + className, + as = 'div', + ...props + }, + ref + ) => { + const Component = motion[as] as typeof motion.div; + + return ( + + {children} + + ); + } +); + +GlassCard.displayName = 'GlassCard'; + +// Convenience wrapper for content padding +interface GlassCardContentProps extends HTMLAttributes { + children: ReactNode; + padding?: 'sm' | 'md' | 'lg'; +} + +const paddingSizes = { + sm: 'p-3', + md: 'p-4', + lg: 'p-6', +}; + +export const GlassCardContent = ({ + children, + padding = 'md', + className, + ...props +}: GlassCardContentProps) => ( +
+ {children} +
+); + +// Header with border +export const GlassCardHeader = ({ + children, + className, + ...props +}: HTMLAttributes) => ( +
+ {children} +
+); + +// Footer with border +export const GlassCardFooter = ({ + children, + className, + ...props +}: HTMLAttributes) => ( +
+ {children} +
+); + +export default GlassCard; diff --git a/frontend/src/hooks/useIsMobile.ts b/frontend/src/hooks/useIsMobile.ts new file mode 100644 index 0000000..265d4fb --- /dev/null +++ b/frontend/src/hooks/useIsMobile.ts @@ -0,0 +1,43 @@ +import { useState, useEffect } from 'react'; + +const MOBILE_BREAKPOINT = 768; + +/** + * Hook to detect if viewport is mobile-sized + * Updates on window resize + * + * Usage: + * const isMobile = useIsMobile(); + * return isMobile ? : ; + */ +export function useIsMobile(): boolean { + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + if (typeof window === 'undefined') return; + + const checkMobile = () => { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + }; + + // Check initially + checkMobile(); + + // Debounced resize handler + let timeoutId: NodeJS.Timeout; + const handleResize = () => { + clearTimeout(timeoutId); + timeoutId = setTimeout(checkMobile, 100); + }; + + window.addEventListener('resize', handleResize); + return () => { + window.removeEventListener('resize', handleResize); + clearTimeout(timeoutId); + }; + }, []); + + return isMobile; +} + +export default useIsMobile; diff --git a/frontend/src/hooks/usePrefersReducedMotion.ts b/frontend/src/hooks/usePrefersReducedMotion.ts new file mode 100644 index 0000000..7ea579e --- /dev/null +++ b/frontend/src/hooks/usePrefersReducedMotion.ts @@ -0,0 +1,42 @@ +import { useState, useEffect } from 'react'; + +/** + * Hook to detect user's reduced motion preference + * Returns true if user prefers reduced motion + * + * Usage: + * const prefersReducedMotion = usePrefersReducedMotion(); + * const variants = prefersReducedMotion ? reducedVariants : fullVariants; + */ +export function usePrefersReducedMotion(): boolean { + const [prefersReducedMotion, setPrefersReducedMotion] = useState(false); + + useEffect(() => { + // Check if window is available (SSR safety) + if (typeof window === 'undefined') return; + + const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); + + // Set initial value + setPrefersReducedMotion(mediaQuery.matches); + + // Listen for changes + const handleChange = (event: MediaQueryListEvent) => { + setPrefersReducedMotion(event.matches); + }; + + // Modern browsers + if (mediaQuery.addEventListener) { + mediaQuery.addEventListener('change', handleChange); + return () => mediaQuery.removeEventListener('change', handleChange); + } else { + // Legacy support + mediaQuery.addListener(handleChange); + return () => mediaQuery.removeListener(handleChange); + } + }, []); + + return prefersReducedMotion; +} + +export default usePrefersReducedMotion; diff --git a/frontend/src/lib/animations.ts b/frontend/src/lib/animations.ts new file mode 100644 index 0000000..fef200a --- /dev/null +++ b/frontend/src/lib/animations.ts @@ -0,0 +1,360 @@ +/** + * Framer Motion Animation Variants + * Consistent, performant animations throughout the landing page + */ + +import { Variants, Transition } from 'framer-motion'; +import { animation } from './design-tokens'; + +// Default spring config - natural feeling +export const springTransition: Transition = { + type: 'spring', + stiffness: 260, + damping: 20, +}; + +export const gentleSpring: Transition = { + type: 'spring', + stiffness: 120, + damping: 14, +}; + +export const snappySpring: Transition = { + type: 'spring', + stiffness: 400, + damping: 25, +}; + +// ============================================ +// Search Bar Variants +// ============================================ + +export const searchBarVariants: Variants = { + idle: { + boxShadow: '0 0 0 1px rgba(75, 139, 190, 0.2)', + scale: 1, + }, + focused: { + boxShadow: '0 0 0 2px #4B8BBE, 0 0 20px rgba(75, 139, 190, 0.3)', + scale: 1.01, + transition: snappySpring, + }, + searching: { + boxShadow: '0 0 0 2px #FFD43B, 0 0 30px rgba(255, 212, 59, 0.25)', + transition: { duration: 0.3 }, + }, + complete: { + boxShadow: '0 0 0 2px #22c55e, 0 0 20px rgba(34, 197, 94, 0.25)', + transition: { duration: 0.2 }, + }, +}; + +// ============================================ +// Search Results Variants +// ============================================ + +export const resultListVariants: Variants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.08, + delayChildren: 0.1, + }, + }, + exit: { + opacity: 0, + transition: { + staggerChildren: 0.03, + staggerDirection: -1, + }, + }, +}; + +export const resultItemVariants: Variants = { + hidden: { + opacity: 0, + y: 20, + filter: 'blur(4px)', + }, + visible: { + opacity: 1, + y: 0, + filter: 'blur(0px)', + transition: springTransition, + }, + exit: { + opacity: 0, + y: -10, + filter: 'blur(2px)', + transition: { duration: 0.15 }, + }, +}; + +// ============================================ +// Card Variants +// ============================================ + +export const cardVariants: Variants = { + idle: { + y: 0, + boxShadow: '0 0 0 1px rgba(255, 255, 255, 0.05)', + }, + hover: { + y: -2, + boxShadow: '0 8px 30px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(255, 255, 255, 0.1)', + transition: snappySpring, + }, + tap: { + y: 0, + scale: 0.99, + }, +}; + +export const glassCardVariants: Variants = { + idle: { + backgroundColor: 'rgba(255, 255, 255, 0.03)', + borderColor: 'rgba(255, 255, 255, 0.08)', + }, + hover: { + backgroundColor: 'rgba(255, 255, 255, 0.05)', + borderColor: 'rgba(255, 255, 255, 0.15)', + transition: { duration: 0.2 }, + }, +}; + +// ============================================ +// Button Variants +// ============================================ + +export const buttonVariants: Variants = { + idle: { scale: 1 }, + hover: { scale: 1.02 }, + tap: { scale: 0.98 }, +}; + +export const primaryButtonVariants: Variants = { + idle: { + scale: 1, + boxShadow: '0 0 0 0 rgba(75, 139, 190, 0)', + }, + hover: { + scale: 1.02, + boxShadow: '0 0 25px rgba(75, 139, 190, 0.4)', + transition: snappySpring, + }, + tap: { + scale: 0.98, + }, +}; + +// ============================================ +// Pill/Tag Variants +// ============================================ + +export const pillVariants: Variants = { + idle: { + scale: 1, + backgroundColor: 'rgba(39, 39, 42, 0.5)', + }, + hover: { + scale: 1.05, + backgroundColor: 'rgba(39, 39, 42, 0.8)', + transition: { duration: 0.15 }, + }, + selected: { + scale: 1, + backgroundColor: 'rgba(75, 139, 190, 0.2)', + borderColor: '#4B8BBE', + }, + tap: { + scale: 0.95, + }, +}; + +// ============================================ +// Score/Progress Variants +// ============================================ + +export const scoreVariants: Variants = { + hidden: { width: 0, opacity: 0 }, + visible: (score: number) => ({ + width: `${score}%`, + opacity: 1, + transition: { + width: { delay: 0.2, duration: 0.6, ease: 'easeOut' }, + opacity: { delay: 0.1, duration: 0.2 }, + }, + }), +}; + +export const progressBarVariants: Variants = { + idle: { + backgroundPosition: '0% 0%', + }, + animating: { + backgroundPosition: ['0% 0%', '100% 0%'], + transition: { + duration: 2, + repeat: Infinity, + ease: 'linear', + }, + }, +}; + +// ============================================ +// Content/Text Variants +// ============================================ + +export const fadeInVariants: Variants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { duration: animation.duration.normal / 1000 }, + }, +}; + +export const fadeInUpVariants: Variants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: springTransition, + }, +}; + +export const fadeInDownVariants: Variants = { + hidden: { opacity: 0, y: -20 }, + visible: { + opacity: 1, + y: 0, + transition: springTransition, + }, +}; + +export const scaleInVariants: Variants = { + hidden: { opacity: 0, scale: 0.95 }, + visible: { + opacity: 1, + scale: 1, + transition: springTransition, + }, +}; + +export const blurInVariants: Variants = { + hidden: { opacity: 0, filter: 'blur(10px)' }, + visible: { + opacity: 1, + filter: 'blur(0px)', + transition: { duration: animation.duration.slow / 1000 }, + }, +}; + +// ============================================ +// Stagger Containers +// ============================================ + +export const staggerContainer: Variants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.05, + delayChildren: 0.1, + }, + }, +}; + +export const fastStaggerContainer: Variants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.03, + }, + }, +}; + +// ============================================ +// Code Highlight Variants +// ============================================ + +export const highlightFlashVariants: Variants = { + initial: { + backgroundColor: 'rgba(255, 212, 59, 0.5)', + }, + animate: { + backgroundColor: 'rgba(255, 212, 59, 0.15)', + transition: { duration: 0.8, delay: 0.3 }, + }, +}; + +export const lineHighlightVariants: Variants = { + idle: { backgroundColor: 'transparent' }, + hover: { + backgroundColor: 'rgba(255, 255, 255, 0.03)', + transition: { duration: 0.1 }, + }, +}; + +// ============================================ +// Explain Panel Variants +// ============================================ + +export const expandVariants: Variants = { + collapsed: { + height: 0, + opacity: 0, + transition: { duration: 0.3, ease: 'easeInOut' }, + }, + expanded: { + height: 'auto', + opacity: 1, + transition: { duration: 0.4, ease: 'easeOut' }, + }, +}; + +// ============================================ +// Loading/Spinner Variants +// ============================================ + +export const spinnerVariants: Variants = { + animate: { + rotate: 360, + transition: { + duration: 1, + repeat: Infinity, + ease: 'linear', + }, + }, +}; + +export const pulseVariants: Variants = { + animate: { + scale: [1, 1.05, 1], + opacity: [0.7, 1, 0.7], + transition: { + duration: 1.5, + repeat: Infinity, + ease: 'easeInOut', + }, + }, +}; + +// ============================================ +// Page Transition Variants +// ============================================ + +export const pageVariants: Variants = { + initial: { opacity: 0, y: 10 }, + animate: { + opacity: 1, + y: 0, + transition: { duration: 0.4, ease: [0.16, 1, 0.3, 1] }, + }, + exit: { + opacity: 0, + y: -10, + transition: { duration: 0.2 }, + }, +}; diff --git a/frontend/src/lib/python-theme.ts b/frontend/src/lib/python-theme.ts new file mode 100644 index 0000000..2d3486b --- /dev/null +++ b/frontend/src/lib/python-theme.ts @@ -0,0 +1,144 @@ +/** + * Python-Native Theme Extension + * Colors that feel like home to Python developers + */ + +// Python Brand Colors +export const python = { + blue: { + dark: '#306998', + DEFAULT: '#4B8BBE', + light: '#6BA3D6', + glow: 'rgba(75, 139, 190, 0.25)', + glowStrong: 'rgba(75, 139, 190, 0.4)', + }, + yellow: { + dark: '#E5C33B', + DEFAULT: '#FFD43B', + light: '#FFE873', + glow: 'rgba(255, 212, 59, 0.2)', + glowStrong: 'rgba(255, 212, 59, 0.35)', + }, +} as const; + +// Syntax Highlighting - Familiar to PyCharm/VS Code users +export const syntax = { + keyword: '#FF79C6', // def, class, import, from, return, if, else + function: '#50FA7B', // Function names, method calls + string: '#F1FA8C', // Single, double, triple quoted strings + comment: '#6272A4', // Comments and docstrings prefix + variable: '#F8F8F2', // Variable names + number: '#BD93F9', // Integers, floats + decorator: '#8BE9FD', // @decorators + builtin: '#FF79C6', // Built-in functions like print, len, range + className: '#8BE9FD', // Class names + parameter: '#FFB86C', // Function parameters + operator: '#FF79C6', // Operators like =, ==, +, - + punctuation: '#F8F8F2', // Brackets, colons, commas + docstring: '#6272A4', // Triple-quoted docstrings + fstring: '#F1FA8C', // f-strings + fstringBrace: '#FF79C6', // Braces inside f-strings + lineNumber: 'rgba(248, 248, 242, 0.3)', + lineNumberActive: 'rgba(248, 248, 242, 0.6)', + lineHighlight: 'rgba(255, 255, 255, 0.05)', + matchHighlight: 'rgba(255, 212, 59, 0.2)', + matchBorder: '#FFD43B', +} as const; + +// Background layers optimized for dark mode code reading +export const codeBg = { + deep: '#0D1117', // Deepest layer (GitHub dark) + primary: '#161B22', // Main code background + elevated: '#21262D', // Code blocks, cards + hover: '#30363D', // Hover states + selection: 'rgba(75, 139, 190, 0.3)', +} as const; + +// Featured Python Repositories +export const featuredRepos = [ + { + id: 'flask', + owner: 'pallets', + name: 'flask', + displayName: 'Flask', + description: 'Lightweight WSGI web framework', + stars: '67k', + color: python.blue.DEFAULT, + defaultQuery: 'authentication middleware', + }, + { + id: 'django', + owner: 'django', + name: 'django', + displayName: 'Django', + description: 'High-level Python web framework', + stars: '79k', + color: '#092E20', + defaultQuery: 'user authentication views', + }, + { + id: 'fastapi', + owner: 'tiangolo', + name: 'fastapi', + displayName: 'FastAPI', + description: 'Modern, fast web framework', + stars: '76k', + color: '#009688', + defaultQuery: 'dependency injection', + }, + { + id: 'requests', + owner: 'psf', + name: 'requests', + displayName: 'Requests', + description: 'HTTP for Humans', + stars: '52k', + color: python.yellow.DEFAULT, + defaultQuery: 'session handling cookies', + }, + { + id: 'sqlalchemy', + owner: 'sqlalchemy', + name: 'sqlalchemy', + displayName: 'SQLAlchemy', + description: 'Database toolkit', + stars: '9k', + color: '#D71F00', + defaultQuery: 'connection pooling', + }, +] as const; + +export type FeaturedRepo = typeof featuredRepos[number]; + +// CSS custom properties for Python theme +export const pythonCSSVars = { + '--python-blue': python.blue.DEFAULT, + '--python-blue-dark': python.blue.dark, + '--python-blue-light': python.blue.light, + '--python-blue-glow': python.blue.glow, + '--python-yellow': python.yellow.DEFAULT, + '--python-yellow-dark': python.yellow.dark, + '--python-yellow-light': python.yellow.light, + '--python-yellow-glow': python.yellow.glow, + '--syntax-keyword': syntax.keyword, + '--syntax-function': syntax.function, + '--syntax-string': syntax.string, + '--syntax-comment': syntax.comment, + '--syntax-variable': syntax.variable, + '--syntax-number': syntax.number, + '--syntax-decorator': syntax.decorator, + '--syntax-class': syntax.className, + '--syntax-parameter': syntax.parameter, + '--code-bg-deep': codeBg.deep, + '--code-bg-primary': codeBg.primary, + '--code-bg-elevated': codeBg.elevated, +} as const; + +// Helper to inject Python theme CSS vars +export function injectPythonTheme(): void { + if (typeof document === 'undefined') return; + const root = document.documentElement; + Object.entries(pythonCSSVars).forEach(([key, value]) => { + root.style.setProperty(key, value); + }); +} diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 712fa80..bee4e98 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -40,6 +40,40 @@ export default { 'border-hover': 'var(--glass-border-hover)', }, + // Python brand colors + python: { + blue: { + dark: '#306998', + DEFAULT: '#4B8BBE', + light: '#6BA3D6', + }, + yellow: { + dark: '#E5C33B', + DEFAULT: '#FFD43B', + light: '#FFE873', + }, + }, + + // Code background layers + code: { + deep: '#0D1117', + primary: '#161B22', + elevated: '#21262D', + hover: '#30363D', + }, + + // Syntax highlighting + syntax: { + keyword: '#FF79C6', + function: '#50FA7B', + string: '#F1FA8C', + comment: '#6272A4', + variable: '#F8F8F2', + number: '#BD93F9', + decorator: '#8BE9FD', + parameter: '#FFB86C', + }, + // Shadcn compatibility background: 'hsl(var(--background))', foreground: 'hsl(var(--foreground))', From 544b1d3329e6d4c3bb74fa3b2856ca3a55a81ed2 Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Tue, 6 Jan 2026 16:20:28 -0500 Subject: [PATCH 2/2] refactor: clean up comments to be more natural --- frontend/src/components/ui/CodeBlock.tsx | 130 +++----- frontend/src/components/ui/GlassCard.tsx | 61 +--- frontend/src/hooks/useIsMobile.ts | 32 +- frontend/src/hooks/usePrefersReducedMotion.ts | 34 +-- frontend/src/lib/animations.ts | 279 +++--------------- frontend/src/lib/python-theme.ts | 17 +- 6 files changed, 127 insertions(+), 426 deletions(-) diff --git a/frontend/src/components/ui/CodeBlock.tsx b/frontend/src/components/ui/CodeBlock.tsx index a7227b0..8c3ca65 100644 --- a/frontend/src/components/ui/CodeBlock.tsx +++ b/frontend/src/components/ui/CodeBlock.tsx @@ -17,11 +17,14 @@ interface CodeBlockProps extends HTMLAttributes { githubUrl?: string; } -// Simple Python tokenizer for syntax highlighting -function tokenizePython(code: string): Array<{ type: string; value: string }> { - const tokens: Array<{ type: string; value: string }> = []; +type Token = { type: string; value: string }; + +// Dead simple tokenizer - not trying to be a full parser, just good enough for display +function tokenizePython(code: string): Token[] { + const tokens: Token[] = []; - const patterns: Array<[string, RegExp]> = [ + // Order matters here - more specific patterns first + const patterns: [string, RegExp][] = [ ['comment', /^#.*/], ['docstring', /^("""[\s\S]*?"""|'''[\s\S]*?''')/], ['fstring', /^f(['"])((?:\\.|(?!\1)[^\\])*)\1/], @@ -40,10 +43,8 @@ function tokenizePython(code: string): Array<{ type: string; value: string }> { ]; let remaining = code; - while (remaining.length > 0) { let matched = false; - for (const [type, pattern] of patterns) { const match = remaining.match(pattern); if (match) { @@ -53,7 +54,6 @@ function tokenizePython(code: string): Array<{ type: string; value: string }> { break; } } - if (!matched) { tokens.push({ type: 'text', value: remaining[0] }); remaining = remaining.slice(1); @@ -63,38 +63,24 @@ function tokenizePython(code: string): Array<{ type: string; value: string }> { return tokens; } -function getTokenColor(type: string): string { - const colorMap: Record = { - keyword: syntax.keyword, - builtin: syntax.builtin, - function: syntax.function, - className: syntax.className, - decorator: syntax.decorator, - string: syntax.string, - fstring: syntax.string, - docstring: syntax.docstring, - comment: syntax.comment, - number: syntax.number, - parameter: syntax.parameter, - operator: syntax.operator, - punctuation: syntax.punctuation, - variable: syntax.variable, - }; - - return colorMap[type] || syntax.variable; -} +const tokenColors: Record = { + keyword: syntax.keyword, + builtin: syntax.builtin, + function: syntax.function, + className: syntax.className, + decorator: syntax.decorator, + string: syntax.string, + fstring: syntax.string, + docstring: syntax.docstring, + comment: syntax.comment, + number: syntax.number, + parameter: syntax.parameter, + operator: syntax.operator, + punctuation: syntax.punctuation, + variable: syntax.variable, +}; -/** - * CodeBlock - Python code display with syntax highlighting - * - * Features: - * - Python syntax highlighting - * - Line numbers - * - Line highlighting for matches - * - Copy to clipboard - * - GitHub link - * - Hover effects on lines - */ +// Python code block with syntax highlighting and copy button export function CodeBlock({ code, language = 'python', @@ -120,24 +106,17 @@ export function CodeBlock({ return (
- {/* Header */} {(filename || githubUrl) && (
{filename && ( - - {filename} - + {filename} )}
{githubUrl && ( @@ -182,52 +154,40 @@ export function CodeBlock({
)} - {/* Code */} -
+
           
             {lines.map((line, index) => {
               const lineNumber = lineStart + index;
               const isHighlighted = highlightLines.includes(lineNumber);
-              const tokens = language === 'python' ? tokenizePython(line) : [{ type: 'text', value: line }];
+              const tokens = language === 'python' 
+                ? tokenizePython(line) 
+                : [{ type: 'text', value: line }];
               
               return (
                 
                   {showLineNumbers && (
                     
                       {lineNumber}
                     
                   )}
                   
-                    {tokens.map((token, tokenIndex) => (
-                      
+                    {tokens.map((token, i) => (
+                      
                         {token.value}
                       
                     ))}
diff --git a/frontend/src/components/ui/GlassCard.tsx b/frontend/src/components/ui/GlassCard.tsx
index 0b95f1e..1523114 100644
--- a/frontend/src/components/ui/GlassCard.tsx
+++ b/frontend/src/components/ui/GlassCard.tsx
@@ -19,52 +19,18 @@ const glowStyles: Record = {
   none: '',
 };
 
-/**
- * GlassCard - Premium glassmorphism card component
- * 
- * Features:
- * - Backdrop blur effect
- * - Subtle border that brightens on hover
- * - Optional glow effect (Python blue or yellow)
- * - Smooth hover animations via Framer Motion
- * 
- * @example
- * 
- *   

Search Result

- *

Code snippet here...

- *
- */ +// Glassmorphism card with optional Python-colored glow export const GlassCard = forwardRef( - ( - { - children, - glow = 'none', - hover = true, - className, - as = 'div', - ...props - }, - ref - ) => { + ({ children, glow = 'none', hover = true, className, as = 'div', ...props }, ref) => { const Component = motion[as] as typeof motion.div; return ( ( GlassCard.displayName = 'GlassCard'; -// Convenience wrapper for content padding +// Compound components + interface GlassCardContentProps extends HTMLAttributes { children: ReactNode; padding?: 'sm' | 'md' | 'lg'; } -const paddingSizes = { - sm: 'p-3', - md: 'p-4', - lg: 'p-6', -}; +const paddingSizes = { sm: 'p-3', md: 'p-4', lg: 'p-6' }; export const GlassCardContent = ({ children, @@ -104,7 +67,6 @@ export const GlassCardContent = ({
); -// Header with border export const GlassCardHeader = ({ children, className, @@ -112,9 +74,7 @@ export const GlassCardHeader = ({ }: HTMLAttributes) => (
); -// Footer with border export const GlassCardFooter = ({ children, className, @@ -131,9 +90,7 @@ export const GlassCardFooter = ({ }: HTMLAttributes) => (
: ; - */ export function useIsMobile(): boolean { const [isMobile, setIsMobile] = useState(false); useEffect(() => { if (typeof window === 'undefined') return; - const checkMobile = () => { - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); - }; - - // Check initially - checkMobile(); + const check = () => setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + check(); - // Debounced resize handler - let timeoutId: NodeJS.Timeout; - const handleResize = () => { - clearTimeout(timeoutId); - timeoutId = setTimeout(checkMobile, 100); + // Debounce resize to avoid thrashing + let timeout: NodeJS.Timeout; + const onResize = () => { + clearTimeout(timeout); + timeout = setTimeout(check, 100); }; - window.addEventListener('resize', handleResize); + window.addEventListener('resize', onResize); return () => { - window.removeEventListener('resize', handleResize); - clearTimeout(timeoutId); + window.removeEventListener('resize', onResize); + clearTimeout(timeout); }; }, []); diff --git a/frontend/src/hooks/usePrefersReducedMotion.ts b/frontend/src/hooks/usePrefersReducedMotion.ts index 7ea579e..375c424 100644 --- a/frontend/src/hooks/usePrefersReducedMotion.ts +++ b/frontend/src/hooks/usePrefersReducedMotion.ts @@ -1,39 +1,21 @@ import { useState, useEffect } from 'react'; -/** - * Hook to detect user's reduced motion preference - * Returns true if user prefers reduced motion - * - * Usage: - * const prefersReducedMotion = usePrefersReducedMotion(); - * const variants = prefersReducedMotion ? reducedVariants : fullVariants; - */ +// Respects user's OS-level motion preferences export function usePrefersReducedMotion(): boolean { const [prefersReducedMotion, setPrefersReducedMotion] = useState(false); useEffect(() => { - // Check if window is available (SSR safety) if (typeof window === 'undefined') return; - const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); - - // Set initial value - setPrefersReducedMotion(mediaQuery.matches); + const mq = window.matchMedia('(prefers-reduced-motion: reduce)'); + setPrefersReducedMotion(mq.matches); - // Listen for changes - const handleChange = (event: MediaQueryListEvent) => { - setPrefersReducedMotion(event.matches); + const onChange = (e: MediaQueryListEvent) => setPrefersReducedMotion(e.matches); + + mq.addEventListener?.('change', onChange) ?? mq.addListener?.(onChange); + return () => { + mq.removeEventListener?.('change', onChange) ?? mq.removeListener?.(onChange); }; - - // Modern browsers - if (mediaQuery.addEventListener) { - mediaQuery.addEventListener('change', handleChange); - return () => mediaQuery.removeEventListener('change', handleChange); - } else { - // Legacy support - mediaQuery.addListener(handleChange); - return () => mediaQuery.removeListener(handleChange); - } }, []); return prefersReducedMotion; diff --git a/frontend/src/lib/animations.ts b/frontend/src/lib/animations.ts index fef200a..089b7a6 100644 --- a/frontend/src/lib/animations.ts +++ b/frontend/src/lib/animations.ts @@ -1,34 +1,14 @@ -/** - * Framer Motion Animation Variants - * Consistent, performant animations throughout the landing page - */ +// Framer Motion variants for landing page animations import { Variants, Transition } from 'framer-motion'; import { animation } from './design-tokens'; -// Default spring config - natural feeling -export const springTransition: Transition = { - type: 'spring', - stiffness: 260, - damping: 20, -}; - -export const gentleSpring: Transition = { - type: 'spring', - stiffness: 120, - damping: 14, -}; - -export const snappySpring: Transition = { - type: 'spring', - stiffness: 400, - damping: 25, -}; - -// ============================================ -// Search Bar Variants -// ============================================ +// Spring configs +export const springTransition: Transition = { type: 'spring', stiffness: 260, damping: 20 }; +export const gentleSpring: Transition = { type: 'spring', stiffness: 120, damping: 14 }; +export const snappySpring: Transition = { type: 'spring', stiffness: 400, damping: 25 }; +// Search bar - changes glow color based on state (blue → yellow → green) export const searchBarVariants: Variants = { idle: { boxShadow: '0 0 0 1px rgba(75, 139, 190, 0.2)', @@ -49,73 +29,38 @@ export const searchBarVariants: Variants = { }, }; -// ============================================ -// Search Results Variants -// ============================================ - +// Results list - staggered fade in from below export const resultListVariants: Variants = { hidden: { opacity: 0 }, visible: { opacity: 1, - transition: { - staggerChildren: 0.08, - delayChildren: 0.1, - }, + transition: { staggerChildren: 0.08, delayChildren: 0.1 }, }, exit: { opacity: 0, - transition: { - staggerChildren: 0.03, - staggerDirection: -1, - }, + transition: { staggerChildren: 0.03, staggerDirection: -1 }, }, }; export const resultItemVariants: Variants = { - hidden: { - opacity: 0, - y: 20, - filter: 'blur(4px)', - }, - visible: { - opacity: 1, - y: 0, - filter: 'blur(0px)', - transition: springTransition, - }, - exit: { - opacity: 0, - y: -10, - filter: 'blur(2px)', - transition: { duration: 0.15 }, - }, + hidden: { opacity: 0, y: 20, filter: 'blur(4px)' }, + visible: { opacity: 1, y: 0, filter: 'blur(0px)', transition: springTransition }, + exit: { opacity: 0, y: -10, filter: 'blur(2px)', transition: { duration: 0.15 } }, }; -// ============================================ -// Card Variants -// ============================================ - +// Cards export const cardVariants: Variants = { - idle: { - y: 0, - boxShadow: '0 0 0 1px rgba(255, 255, 255, 0.05)', - }, + idle: { y: 0, boxShadow: '0 0 0 1px rgba(255, 255, 255, 0.05)' }, hover: { y: -2, boxShadow: '0 8px 30px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(255, 255, 255, 0.1)', transition: snappySpring, }, - tap: { - y: 0, - scale: 0.99, - }, + tap: { y: 0, scale: 0.99 }, }; export const glassCardVariants: Variants = { - idle: { - backgroundColor: 'rgba(255, 255, 255, 0.03)', - borderColor: 'rgba(255, 255, 255, 0.08)', - }, + idle: { backgroundColor: 'rgba(255, 255, 255, 0.03)', borderColor: 'rgba(255, 255, 255, 0.08)' }, hover: { backgroundColor: 'rgba(255, 255, 255, 0.05)', borderColor: 'rgba(255, 255, 255, 0.15)', @@ -123,10 +68,7 @@ export const glassCardVariants: Variants = { }, }; -// ============================================ -// Button Variants -// ============================================ - +// Buttons export const buttonVariants: Variants = { idle: { scale: 1 }, hover: { scale: 1.02 }, @@ -134,48 +76,20 @@ export const buttonVariants: Variants = { }; export const primaryButtonVariants: Variants = { - idle: { - scale: 1, - boxShadow: '0 0 0 0 rgba(75, 139, 190, 0)', - }, - hover: { - scale: 1.02, - boxShadow: '0 0 25px rgba(75, 139, 190, 0.4)', - transition: snappySpring, - }, - tap: { - scale: 0.98, - }, + idle: { scale: 1, boxShadow: '0 0 0 0 rgba(75, 139, 190, 0)' }, + hover: { scale: 1.02, boxShadow: '0 0 25px rgba(75, 139, 190, 0.4)', transition: snappySpring }, + tap: { scale: 0.98 }, }; -// ============================================ -// Pill/Tag Variants -// ============================================ - +// Pills/tags (repo switcher) export const pillVariants: Variants = { - idle: { - scale: 1, - backgroundColor: 'rgba(39, 39, 42, 0.5)', - }, - hover: { - scale: 1.05, - backgroundColor: 'rgba(39, 39, 42, 0.8)', - transition: { duration: 0.15 }, - }, - selected: { - scale: 1, - backgroundColor: 'rgba(75, 139, 190, 0.2)', - borderColor: '#4B8BBE', - }, - tap: { - scale: 0.95, - }, + idle: { scale: 1, backgroundColor: 'rgba(39, 39, 42, 0.5)' }, + hover: { scale: 1.05, backgroundColor: 'rgba(39, 39, 42, 0.8)', transition: { duration: 0.15 } }, + selected: { scale: 1, backgroundColor: 'rgba(75, 139, 190, 0.2)', borderColor: '#4B8BBE' }, + tap: { scale: 0.95 }, }; -// ============================================ -// Score/Progress Variants -// ============================================ - +// Score bar fill animation export const scoreVariants: Variants = { hidden: { width: 0, opacity: 0 }, visible: (score: number) => ({ @@ -188,173 +102,76 @@ export const scoreVariants: Variants = { }), }; -export const progressBarVariants: Variants = { - idle: { - backgroundPosition: '0% 0%', - }, - animating: { - backgroundPosition: ['0% 0%', '100% 0%'], - transition: { - duration: 2, - repeat: Infinity, - ease: 'linear', - }, - }, -}; - -// ============================================ -// Content/Text Variants -// ============================================ - +// Generic fade variants export const fadeInVariants: Variants = { hidden: { opacity: 0 }, - visible: { - opacity: 1, - transition: { duration: animation.duration.normal / 1000 }, - }, + visible: { opacity: 1, transition: { duration: animation.duration.normal / 1000 } }, }; export const fadeInUpVariants: Variants = { hidden: { opacity: 0, y: 20 }, - visible: { - opacity: 1, - y: 0, - transition: springTransition, - }, + visible: { opacity: 1, y: 0, transition: springTransition }, }; export const fadeInDownVariants: Variants = { hidden: { opacity: 0, y: -20 }, - visible: { - opacity: 1, - y: 0, - transition: springTransition, - }, + visible: { opacity: 1, y: 0, transition: springTransition }, }; export const scaleInVariants: Variants = { hidden: { opacity: 0, scale: 0.95 }, - visible: { - opacity: 1, - scale: 1, - transition: springTransition, - }, + visible: { opacity: 1, scale: 1, transition: springTransition }, }; export const blurInVariants: Variants = { hidden: { opacity: 0, filter: 'blur(10px)' }, - visible: { - opacity: 1, - filter: 'blur(0px)', - transition: { duration: animation.duration.slow / 1000 }, - }, + visible: { opacity: 1, filter: 'blur(0px)', transition: { duration: animation.duration.slow / 1000 } }, }; -// ============================================ -// Stagger Containers -// ============================================ - +// Stagger containers export const staggerContainer: Variants = { hidden: { opacity: 0 }, - visible: { - opacity: 1, - transition: { - staggerChildren: 0.05, - delayChildren: 0.1, - }, - }, + visible: { opacity: 1, transition: { staggerChildren: 0.05, delayChildren: 0.1 } }, }; export const fastStaggerContainer: Variants = { hidden: { opacity: 0 }, - visible: { - opacity: 1, - transition: { - staggerChildren: 0.03, - }, - }, + visible: { opacity: 1, transition: { staggerChildren: 0.03 } }, }; -// ============================================ -// Code Highlight Variants -// ============================================ - +// Code highlighting export const highlightFlashVariants: Variants = { - initial: { - backgroundColor: 'rgba(255, 212, 59, 0.5)', - }, - animate: { - backgroundColor: 'rgba(255, 212, 59, 0.15)', - transition: { duration: 0.8, delay: 0.3 }, - }, + initial: { backgroundColor: 'rgba(255, 212, 59, 0.5)' }, + animate: { backgroundColor: 'rgba(255, 212, 59, 0.15)', transition: { duration: 0.8, delay: 0.3 } }, }; export const lineHighlightVariants: Variants = { idle: { backgroundColor: 'transparent' }, - hover: { - backgroundColor: 'rgba(255, 255, 255, 0.03)', - transition: { duration: 0.1 }, - }, + hover: { backgroundColor: 'rgba(255, 255, 255, 0.03)', transition: { duration: 0.1 } }, }; -// ============================================ -// Explain Panel Variants -// ============================================ - +// Expand/collapse (for AI explain panel) export const expandVariants: Variants = { - collapsed: { - height: 0, - opacity: 0, - transition: { duration: 0.3, ease: 'easeInOut' }, - }, - expanded: { - height: 'auto', - opacity: 1, - transition: { duration: 0.4, ease: 'easeOut' }, - }, + collapsed: { height: 0, opacity: 0, transition: { duration: 0.3, ease: 'easeInOut' } }, + expanded: { height: 'auto', opacity: 1, transition: { duration: 0.4, ease: 'easeOut' } }, }; -// ============================================ -// Loading/Spinner Variants -// ============================================ - +// Loading states export const spinnerVariants: Variants = { - animate: { - rotate: 360, - transition: { - duration: 1, - repeat: Infinity, - ease: 'linear', - }, - }, + animate: { rotate: 360, transition: { duration: 1, repeat: Infinity, ease: 'linear' } }, }; export const pulseVariants: Variants = { animate: { scale: [1, 1.05, 1], opacity: [0.7, 1, 0.7], - transition: { - duration: 1.5, - repeat: Infinity, - ease: 'easeInOut', - }, + transition: { duration: 1.5, repeat: Infinity, ease: 'easeInOut' }, }, }; -// ============================================ -// Page Transition Variants -// ============================================ - +// Page transitions export const pageVariants: Variants = { initial: { opacity: 0, y: 10 }, - animate: { - opacity: 1, - y: 0, - transition: { duration: 0.4, ease: [0.16, 1, 0.3, 1] }, - }, - exit: { - opacity: 0, - y: -10, - transition: { duration: 0.2 }, - }, + animate: { opacity: 1, y: 0, transition: { duration: 0.4, ease: [0.16, 1, 0.3, 1] } }, + exit: { opacity: 0, y: -10, transition: { duration: 0.2 } }, }; diff --git a/frontend/src/lib/python-theme.ts b/frontend/src/lib/python-theme.ts index 2d3486b..df40b40 100644 --- a/frontend/src/lib/python-theme.ts +++ b/frontend/src/lib/python-theme.ts @@ -1,9 +1,6 @@ -/** - * Python-Native Theme Extension - * Colors that feel like home to Python developers - */ +// Python brand colors + syntax highlighting for the landing page -// Python Brand Colors +// Pulled from python.org branding guidelines export const python = { blue: { dark: '#306998', @@ -21,7 +18,7 @@ export const python = { }, } as const; -// Syntax Highlighting - Familiar to PyCharm/VS Code users +// Dracula-ish syntax colors (what devs expect from VS Code / PyCharm) export const syntax = { keyword: '#FF79C6', // def, class, import, from, return, if, else function: '#50FA7B', // Function names, method calls @@ -45,7 +42,7 @@ export const syntax = { matchBorder: '#FFD43B', } as const; -// Background layers optimized for dark mode code reading +// Background layers (GitHub dark palette) export const codeBg = { deep: '#0D1117', // Deepest layer (GitHub dark) primary: '#161B22', // Main code background @@ -54,7 +51,7 @@ export const codeBg = { selection: 'rgba(75, 139, 190, 0.3)', } as const; -// Featured Python Repositories +// Repos we'll show in the demo - popular ones people recognize export const featuredRepos = [ { id: 'flask', @@ -110,7 +107,7 @@ export const featuredRepos = [ export type FeaturedRepo = typeof featuredRepos[number]; -// CSS custom properties for Python theme +// CSS vars export (for use in vanilla CSS if needed) export const pythonCSSVars = { '--python-blue': python.blue.DEFAULT, '--python-blue-dark': python.blue.dark, @@ -134,7 +131,7 @@ export const pythonCSSVars = { '--code-bg-elevated': codeBg.elevated, } as const; -// Helper to inject Python theme CSS vars +// Inject vars into :root (call once on app init) export function injectPythonTheme(): void { if (typeof document === 'undefined') return; const root = document.documentElement;