From ecde092d9ab10ed54a9fa4719f940285db59f828 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 16:26:35 +0000 Subject: [PATCH] Give the mobile view an iOS design and make kushagra486.github.io the default link Below the sm breakpoint the OS now looks and behaves like iOS: - Status bar with a Dynamic Island that briefly expands with the app just opened (tap to search) and signal/Wi-Fi/battery glyphs that open the widgets view - Swipeable home-screen pages (profile widget, live contributions widget, app grid), page dots, a Search pill, and long-press jiggle mode - A four-app dock and home indicator - Apps open full-screen with an iOS navigation bar ("Home" back) and a home indicator to tap or swipe up; window dragging is disabled on mobile only - Widgets view blurs the home screen behind it The desktop keeps the macOS design unchanged. The old Vercel URL now redirects every non-/api path to kushagra486.github.io (the /api/chat, /api/match and /api/contact functions the site depends on are untouched), and the site declares kushagra486.github.io as its canonical URL. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0143ovLZCWrfDGUX1ogXFKnY --- desktop-os/src/app/layout.tsx | 1 + desktop-os/src/app/page.tsx | 19 +- desktop-os/src/components/ios/HomeScreen.tsx | 243 ++++++++++++++++++ desktop-os/src/components/ios/IOSDock.tsx | 37 +++ desktop-os/src/components/ios/StatusBar.tsx | 124 +++++++++ .../src/components/os/DesktopAssistant.tsx | 4 +- desktop-os/src/components/os/MenuBar.tsx | 2 +- desktop-os/src/components/os/Taskbar.tsx | 2 +- desktop-os/src/components/os/WidgetsPanel.tsx | 4 +- .../src/components/os/WindowManager.tsx | 50 +++- desktop-os/src/lib/useIsMobile.ts | 17 ++ groq-proxy/vercel.json | 15 ++ 12 files changed, 496 insertions(+), 22 deletions(-) create mode 100644 desktop-os/src/components/ios/HomeScreen.tsx create mode 100644 desktop-os/src/components/ios/IOSDock.tsx create mode 100644 desktop-os/src/components/ios/StatusBar.tsx create mode 100644 desktop-os/src/lib/useIsMobile.ts create mode 100644 groq-proxy/vercel.json diff --git a/desktop-os/src/app/layout.tsx b/desktop-os/src/app/layout.tsx index 1d7d01b..06eb352 100644 --- a/desktop-os/src/app/layout.tsx +++ b/desktop-os/src/app/layout.tsx @@ -21,6 +21,7 @@ const OG_IMAGE = `${SITE_URL}/og-image.png`; export const metadata: Metadata = { metadataBase: new URL(SITE_URL), + alternates: { canonical: "/" }, title: "Kushagra OS — Desktop Portfolio", description: "Kushagra Gupta's interactive desktop-OS-style portfolio — 15+ live AI/ML projects, an AI assistant, and a working desktop environment in the browser.", diff --git a/desktop-os/src/app/page.tsx b/desktop-os/src/app/page.tsx index df95c9c..d038fd6 100644 --- a/desktop-os/src/app/page.tsx +++ b/desktop-os/src/app/page.tsx @@ -15,6 +15,9 @@ import { DesktopHero } from '@/components/os/DesktopHero'; import { MenuBar } from '@/components/os/MenuBar'; import { GlobalShortcuts } from '@/components/os/GlobalShortcuts'; import { Screensaver } from '@/components/os/Screensaver'; +import { StatusBar } from '@/components/ios/StatusBar'; +import { HomeScreen } from '@/components/ios/HomeScreen'; +import { IOSDock } from '@/components/ios/IOSDock'; import { APPS, WINDOW_ONLY_APPS } from '@/lib/apps'; import { useDesktopStore } from '@/store/useDesktopStore'; import { usePreferences } from '@/lib/preferences'; @@ -48,18 +51,11 @@ export default function Home() { + - {/* Mobile: a simple wrapping row grid. Desktop: a height-bounded column that wraps into - new columns once it runs out of vertical room — like a real OS icon grid — instead - of a single column that could overflow past the bottom of the screen. */} - - {icons} - + {/* Mobile gets an iOS home screen; desktop a height-bounded icon column that wraps into + new columns once it runs out of vertical room, like a real macOS desktop. */} + setWidgetsOpen(false)} /> + diff --git a/desktop-os/src/components/ios/HomeScreen.tsx b/desktop-os/src/components/ios/HomeScreen.tsx new file mode 100644 index 0000000..e531a27 --- /dev/null +++ b/desktop-os/src/components/ios/HomeScreen.tsx @@ -0,0 +1,243 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; +import { AnimatePresence, motion, Variants } from 'framer-motion'; +import { MapPin, Search } from 'lucide-react'; +import { APPS } from '@/lib/apps'; +import { profile } from '@/lib/portfolioData'; +import { useDesktopStore } from '@/store/useDesktopStore'; +import { sound } from '@/lib/sound'; +import { AppIcon, AppIconStyle } from '@/components/os/AppIcon'; +import { GitHubContributionsWidget } from '@/components/os/widgets/GitHubContributionsWidget'; + +export const DOCK_IDS = ['about-me', 'projects', 'ai-assistant', 'contact'] as const; +const homeApps = APPS.filter((a) => !(DOCK_IDS as readonly string[]).includes(a.id)); +const PAGES = [homeApps.slice(0, 8), homeApps.slice(8)]; +const LONG_PRESS_MS = 480; +const AI_GRADIENT = '#ff9f0a,#ff375f,#bf5af2,#0a84ff,#64d2ff,#ff9f0a'; + +const initials = profile.name + .split(' ') + .map((w) => w[0]) + .join('') + .slice(0, 2) + .toUpperCase(); + +const gridVariants: Variants = { + hidden: {}, + show: { transition: { staggerChildren: 0.035, delayChildren: 0.1 } }, +}; +const itemVariants: Variants = { + hidden: { opacity: 0, scale: 0.6, y: 14 }, + show: { opacity: 1, scale: 1, y: 0, transition: { type: 'spring', stiffness: 360, damping: 24 } }, +}; + +export function HomeIcon({ + id, + title, + appIcon, + jiggle, + index, + onLongPress, + showLabel = true, + size = 60, +}: { + id: string; + title: string; + appIcon: AppIconStyle; + jiggle: boolean; + index: number; + onLongPress: () => void; + showLabel?: boolean; + size?: number; +}) { + const openWindow = useDesktopStore((s) => s.openWindow); + const timer = useRef | null>(null); + const longPressed = useRef(false); + const start = useRef<{ x: number; y: number } | null>(null); + + function clear() { + if (timer.current) clearTimeout(timer.current); + timer.current = null; + } + + return ( + { + longPressed.current = false; + start.current = { x: e.clientX, y: e.clientY }; + timer.current = setTimeout(() => { + longPressed.current = true; + onLongPress(); + }, LONG_PRESS_MS); + }} + onPointerMove={(e) => { + // A swipe between pages is not a long press. + if (start.current && Math.hypot(e.clientX - start.current.x, e.clientY - start.current.y) > 10) clear(); + }} + onPointerUp={clear} + onPointerLeave={clear} + onContextMenu={(e) => e.preventDefault()} + onClick={() => { + if (longPressed.current || jiggle) return; + sound.open(); + openWindow(id, title); + }} + className="flex select-none flex-col items-center gap-1.5 outline-none [-webkit-touch-callout:none]" + > + + + + {showLabel && ( + + {title} + + )} + + ); +} + +function ProfileWidget() { + const openWindow = useDesktopStore((s) => s.openWindow); + return ( + { + sound.open(); + openWindow('about-me', 'About Me'); + }} + whileTap={{ scale: 0.97 }} + className="mac-widget col-span-4 flex items-center gap-4 p-4 text-left" + > + + + + {initials} + + + + {profile.name} + {profile.role} + + {profile.location} + + + + ); +} + +/** iOS home screen: swipeable pages of apps and widgets, page dots, Search pill, jiggle mode. */ +export function HomeScreen({ booted }: { booted: boolean }) { + const setSpotlightOpen = useDesktopStore((s) => s.setSpotlightOpen); + const [page, setPage] = useState(0); + const [jiggle, setJiggle] = useState(false); + const scrollerRef = useRef(null); + + useEffect(() => { + if (!jiggle) return; + const onKey = (e: KeyboardEvent) => e.key === 'Escape' && setJiggle(false); + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, [jiggle]); + + function goTo(i: number) { + const el = scrollerRef.current; + if (el) el.scrollTo({ left: i * el.clientWidth, behavior: 'smooth' }); + } + + return ( +
+ + {jiggle && ( + setJiggle(false)} + className="mac-menu absolute right-4 top-1 z-20 rounded-full px-3.5 py-1 text-[13px] font-semibold text-white" + > + Done + + )} + + +
{ + const el = e.currentTarget; + setPage(Math.round(el.scrollLeft / Math.max(1, el.clientWidth))); + }} + onClick={(e) => { + if (jiggle && e.target === e.currentTarget) setJiggle(false); + }} + className="flex h-full snap-x snap-mandatory overflow-x-auto overflow-y-hidden [scrollbar-width:none] [&::-webkit-scrollbar]:hidden" + > + {PAGES.map((apps, p) => ( + { + if (jiggle && e.target === e.currentTarget) setJiggle(false); + }} + className="grid h-full w-full shrink-0 snap-center auto-rows-min grid-cols-4 content-start justify-items-center gap-x-2 gap-y-5 px-5 pt-3" + > + {p === 0 ? ( + + ) : ( + + + + )} + {apps.map((app, i) => ( + setJiggle(true)} + /> + ))} + + ))} +
+ +
+
+ {PAGES.map((_, i) => ( +
+ +
+
+ ); +} diff --git a/desktop-os/src/components/ios/IOSDock.tsx b/desktop-os/src/components/ios/IOSDock.tsx new file mode 100644 index 0000000..fa164eb --- /dev/null +++ b/desktop-os/src/components/ios/IOSDock.tsx @@ -0,0 +1,37 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { APPS } from '@/lib/apps'; +import { DOCK_IDS, HomeIcon } from '@/components/ios/HomeScreen'; + +const dockApps = DOCK_IDS.map((id) => APPS.find((a) => a.id === id)!); + +/** iOS dock: four favourite apps on a frosted shelf, plus the home indicator. */ +export function IOSDock({ booted }: { booted: boolean }) { + return ( +
+ + {dockApps.map((app, i) => ( + {}} + showLabel={false} + size={58} + /> + ))} + + + ); +} diff --git a/desktop-os/src/components/ios/StatusBar.tsx b/desktop-os/src/components/ios/StatusBar.tsx new file mode 100644 index 0000000..75c3357 --- /dev/null +++ b/desktop-os/src/components/ios/StatusBar.tsx @@ -0,0 +1,124 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; +import { AnimatePresence, motion } from 'framer-motion'; +import { Globe } from 'lucide-react'; +import { useDesktopStore } from '@/store/useDesktopStore'; +import { APPS } from '@/lib/apps'; +import { AppIcon, AppIconStyle } from '@/components/os/AppIcon'; + +const VIEWER_ICON: AppIconStyle = { Glyph: Globe, gradient: 'from-sky-400 to-blue-600' }; + +function SignalBars() { + return ( + + ); +} + +function WifiGlyph() { + return ( + + ); +} + +function BatteryGlyph() { + return ( + + ); +} + +/** + * iOS-style status bar (mobile only): time, a Dynamic Island that briefly expands with the + * app you just opened, and status glyphs. Tapping the right side opens the widgets view. + */ +export function StatusBar() { + const windows = useDesktopStore((s) => s.windows); + const setWidgetsOpen = useDesktopStore((s) => s.setWidgetsOpen); + const setSpotlightOpen = useDesktopStore((s) => s.setSpotlightOpen); + const [now, setNow] = useState(null); + const [activity, setActivity] = useState<{ id: string; title: string } | null>(null); + const lastFocused = useRef(null); + + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect + setNow(new Date()); + const interval = setInterval(() => setNow(new Date()), 15000); + return () => clearInterval(interval); + }, []); + + const focused = windows + .filter((w) => w.isOpen && !w.isMinimized) + .reduce<(typeof windows)[number] | null>((top, w) => (!top || w.zIndex > top.zIndex ? w : top), null); + + // Expand the island for a moment whenever a different app comes to the front. + useEffect(() => { + const id = focused?.id ?? null; + if (id === lastFocused.current) return; + lastFocused.current = id; + if (!focused) return; + // eslint-disable-next-line react-hooks/set-state-in-effect + setActivity({ id: focused.id, title: focused.title }); + const t = setTimeout(() => setActivity(null), 1800); + return () => clearTimeout(t); + }, [focused]); + + const activityIcon = activity ? (APPS.find((a) => a.id === activity.id)?.appIcon ?? VIEWER_ICON) : null; + + return ( +
+ + {now ? now.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }).replace(/\s?[AP]M$/i, '') : ''} + + + setSpotlightOpen(true)} + animate={{ width: activity ? 164 : 104 }} + transition={{ type: 'spring', stiffness: 420, damping: 32 }} + className="absolute left-1/2 top-[7px] flex h-[30px] -translate-x-1/2 items-center justify-between overflow-hidden rounded-full bg-black px-2" + > + + {activity && activityIcon && ( + + + {activity.title} + + )} + + {activity && } + + + +
+ ); +} diff --git a/desktop-os/src/components/os/DesktopAssistant.tsx b/desktop-os/src/components/os/DesktopAssistant.tsx index b791cba..5e4ab5e 100644 --- a/desktop-os/src/components/os/DesktopAssistant.tsx +++ b/desktop-os/src/components/os/DesktopAssistant.tsx @@ -60,7 +60,7 @@ export function DesktopAssistant() { return ( <> -
+
{hint && (
-
+
-