Build a boilerplate/framework for a browser-based desktop OS simulator. The goal is an extensible, theme-switchable "OS in the browser" that others can fork and populate with their own content. Inspired by PostHog's website demo. The project lives at /Users/skyline/TEST/desktop-layout (currently empty).
User choices:
- Framework: Next.js 15 (App Router, Turbopack)
- Glass FX: WebGL GLSL Shader (full refraction, shimmer)
- App types: Custom React components + Iframe wrapper + MDX pages + Demo apps
- Purpose: Boilerplate / Framework
next@15 react@19 typescript@5
tailwindcss@4 clsx + tailwind-merge
motion zustand@5 + immer @tanstack/react-query@5
nuqs@2 zod@3 next-pwa
@radix-ui/* lucide-react
src/
app/
layout.tsx # Root: providers, font, SW registration, NuqsAdapter
page.tsx # Redirects → /desktop
desktop/page.tsx # Client island: ThemeProvider + Desktop
globals.css # @import tailwindcss; @custom-variant per theme; CSS tokens
components/
desktop/
Desktop.tsx # Full-screen canvas (wallpaper, icon grid, context menu)
Wallpaper.tsx # <img> background with right-click context menu
IconGrid.tsx # CSS grid of draggable app icons
AppIcon.tsx # Single icon: drag, dblclick→open, right-click menu
WallpaperPicker.tsx # Dialog for wallpaper selection
window/
WindowManager.tsx # AnimatePresence wrapper, renders all open windows
Window.tsx # Frame: drag, resize, chrome, Motion layout
WindowChrome.tsx # Traffic lights (macOS) or Win11 controls (theme-aware)
WindowMenuBar.tsx # Optional top per-window menu bar
WindowStatusBar.tsx # Optional bottom per-window status bar
ResizeHandle.tsx # 8 edge/corner zones, cursor changes on hover
useWindowDrag.ts # Pointer-capture drag + snap-to-edge resistance
useWindowResize.ts # 8-direction resize, min-size clamp
dock/
Dock.tsx # macOS dock (LiquidGlass-wrapped, magnification)
Taskbar.tsx # Windows 11 centered taskbar
NavBar.tsx # iOS/Android bottom tab bar
DockItem.tsx # Icon with bounce, badge, tooltip
menubar/
MenuBar.tsx # macOS top bar shell
MenuBarClock.tsx # Time + date
MenuBarWifi.tsx # WiFi indicator
ControlCenter.tsx # Popover panel (brightness, volume, theme switch)
SystemTray.tsx # Windows right-side tray
liquid-glass/
LiquidGlass.tsx # Composites CSS + SVG + WebGL layers
GlassFilter.tsx # SVG <defs> singleton at root (feTurbulence + feDisplacement)
GlassShimmer.tsx # <canvas> for WebGL animated caustic shimmer
useGlassShimmer.ts # WebGL2 setup, RAF loop, graceful degradation
themes/
ThemeProvider.tsx # Sets data-os-theme on <html>, picks layout compositor
MacOSTheme.tsx # MenuBar top + Desktop + Dock bottom
IPadTheme.tsx # Home screen grid + Dock
IPhoneTheme.tsx # Full-screen apps + bottom NavBar
WindowsTheme.tsx # Desktop + Taskbar bottom
AndroidTheme.tsx # Material layout + NavBar
apps/
AppRegistry.tsx # ID → lazy(import()) map
Finder.tsx # File browser (sidebar + grid)
Terminal.tsx # Textarea-based emulator
Browser.tsx # Iframe wrapper with address bar
Settings.tsx # Theme switcher, wallpaper, glass toggle
TextEditor.tsx # contenteditable editor
IframeApp.tsx # Generic iframe window (used from config)
MdxApp.tsx # Renders MDX content in a window
store/
index.ts # create() with immer + persist; combines all slices
window-slice.ts # WindowState CRUD + z-index management
app-slice.ts # App registry + running app tracking
theme-slice.ts # OSTheme, ColorScheme, glassEnabled
desktop-slice.ts # Wallpaper, icon layout, dock order
hooks/
useWindowUrlSync.ts # nuqs ↔ Zustand window state bridge
usePointerDrag.ts # Generic pointerId-capture drag primitive
useResizeObserver.ts # ResizeObserver hook
useLocalStorage.ts # SSR-safe localStorage
useTheme.ts # Theme-aware helpers
config/
apps.config.ts # AppConfig[] — add new apps here
wallpapers.config.ts # Per-theme wallpaper definitions
themes.config.ts # Theme metadata
types/
app.ts # AppConfig, ContextMenuAction, AppInstance
window.ts # WindowState, WindowRect, ResizeEdge
theme.ts # OSTheme, ColorScheme, ThemeConfig
desktop.ts # DesktopConfig, WallpaperConfig
lib/
utils.ts # cn() = clsx + twMerge
url-codec.ts # WindowState ↔ URL param string
window-math.ts # snapZone, clamp, edgeResistance, 8-dir resize delta
glsl/
shimmer.vert.ts # Vertex shader source (pass-through)
shimmer.frag.ts # Fragment shader: time-driven caustic shimmer
public/
wallpapers/ # jpg/webp wallpapers per theme
icons/ # SVG app icons
manifest.json # PWA manifest
// types/app.ts
interface AppConfig {
id: string;
name: string;
icon: string; // /icons/*.svg or lucide icon name
component: string; // key into AppRegistry
disabled?: boolean;
defaultSize?: { width: number; height: number };
defaultPosition?: { x: number; y: number };
minSize?: { width: number; height: number };
contextMenu?: ContextMenuAction[];
hasMenuBar?: boolean;
hasStatusBar?: boolean;
category?: string;
mobileFullscreen?: boolean;
// For iframe apps:
iframeUrl?: string;
// For MDX apps:
mdxSlug?: string;
}
// types/window.ts
type ResizeEdge = 'n'|'ne'|'e'|'se'|'s'|'sw'|'w'|'nw';
interface WindowRect { x: number; y: number; width: number; height: number }
interface WindowState {
id: string;
appId: string;
rect: WindowRect;
prevRect: WindowRect | null; // saved before maximize
zIndex: number;
isMinimized: boolean;
isMaximized: boolean;
isFocused: boolean;
title: string;
hasMenuBar: boolean;
hasStatusBar: boolean;
}| Slice | Persisted to localStorage | Synced to URL |
|---|---|---|
| window-slice | No | Yes (via nuqs) |
| theme-slice | Yes (osTheme, colorScheme, glassEnabled) | No |
| desktop-slice | Yes (wallpaper, iconLayout, dockAppIds) | No |
| app-slice | No | No |
Store setup: create()(immer(persist(...slices, { partialize })))
Window state per open window encoded as repeated ?w= params:
?w=finder:100,200,800,600&w=terminal:300,400,600,400:1
Format: appId:x,y,width,height[:flagsBitmask]
Flags: 1=minimized, 2=maximized
useWindowUrlSync.ts — on mount: decode URL → hydrate store; on store change: debounced encode → history.replaceState (shallow, via nuqs).
globals.css defines:
@custom-variant macos ([data-os-theme="macos"] &)× 5 themes- Per-theme CSS custom properties on
[data-os-theme="X"]selectors:--dock-height,--menubar-height,--window-chrome-height--dock-bg,--menubar-bg,--window-chrome-bg(rgba values)--radius-window,--radius-dock--traffic-light-*colors (macOS only)--win-accent(Windows only)
@theme inlineexposes tokens as Tailwind utilitieshtml, body { overflow: hidden; height: 100%; user-select: none }
Components use: className="macos:flex windows:hidden" — visibility per theme via variant utilities.
Drag (useWindowDrag.ts):
onPointerDown→setPointerCapture(pointerId)on chromepointermoveonwindow: compute delta, apply edge resistance withinSNAP_ZONE=24pxapplyEdgeResistance(x, vpWidth, zone)→ exponential deceleration near screen edgepointerup: snap if within zone, commit final rect to Zustand (single write)- Motion
useMotionValuefor x/y — bypasses React reconciliation during drag
Resize (useWindowResize.ts):
- 8
ResizeHandlezones withcursor-*CSS per edge (n/s = ns-resize, ne/sw = nesw-resize, etc.) - Each handle
onPointerDownrecordsResizeEdge, captures pointer pointermove: compute new rect viacalcResizeDelta(edge, dx, dy, currentRect)- Clamp to
minSizefromAppConfig - Motion
useMotionValuefor width/height too — no re-renders during resize
Maximize/Restore:
maximizeWindow(id, viewportRect)→ savesprevRect, sets rect = viewport minus chrome heights- Restore → reads
prevRect, animated via Motionlayoutprop
Three-layer composition in LiquidGlass.tsx:
- CSS base —
backdrop-filter: blur(20px) saturate(180%)+ RGBA background - SVG distortion —
filter: url(#glass-distortion)referencing singletonGlassFilter.tsx'sfeTurbulence+feDisplacementMap - WebGL shimmer —
GlassShimmer.tsxcanvas,mix-blend-mode: overlay, absolute positioned
GLSL Shimmer Fragment (shimmer.frag.ts):
- Time-driven dual sine waves → caustic pattern
- Alpha ~0.08 (very subtle)
- Graceful degradation: if WebGL2 unavailable, layers 1+2 still render
useGlassShimmer.ts:
getContext('webgl2')→ null guard- Compile vert + frag from string constants
- Upload
u_timeuniform per RAF frame - Cleanup:
cancelAnimationFrameon unmount
| Theme | Layout | Responsive behavior |
|---|---|---|
| macOS | MenuBar top + Desktop + Dock bottom | md: → iPad layout, sm: → iPhone layout |
| iPad OS | Home grid + Dock | Standalone or macOS responsive target |
| iPhone OS | Fullscreen apps + NavBar bottom | Standalone or macOS sm: target |
| Windows 11 | Desktop + Taskbar bottom | sm: → compact mobile layout |
| Android | Material + NavBar bottom | sm: → compact mobile layout |
iOS/Android: windows always fullscreen (w-full h-full ignoring WindowRect).
macOS/Windows: windows floating with drag/resize.
// next.config.ts
withPWA({
dest: 'public',
disable: process.env.NODE_ENV === 'development',
runtimeCaching: [
{ urlPattern: /\/wallpapers\/|\/icons\//, handler: 'CacheFirst' },
{ urlPattern: /\/_next\/static\//, handler: 'CacheFirst' },
{ urlPattern: /^https?.*/, handler: 'NetworkFirst', options: { networkTimeoutSeconds: 3 } },
]
})Add an entry to src/config/apps.config.ts:
{
id: 'my-app',
name: 'My App',
icon: '/icons/my-app.svg',
component: 'MyApp', // matches key in AppRegistry.tsx
defaultSize: { width: 800, height: 600 },
hasMenuBar: true,
contextMenu: [
{ key: 'new-window', label: 'New Window', action: 'newWindow' }
]
}Then add MyApp: lazy(() => import('./MyApp')) to AppRegistry.tsx.
For iframe: component: 'IframeApp', add iframeUrl: 'https://...'
For MDX: component: 'MdxApp', add mdxSlug: 'my-page'
npx create-next-app@latest desktop-layout --typescript --eslint --app --turbopack- Install all dependencies (see stack above)
- Set up
globals.css(Tailwind v4 +@custom-variant+ CSS tokens) tsconfig.jsonpath aliases:@/*→./src/*- Create all
types/files lib/utils.tswithcn()
- All 4 Zustand slice files with full actions
store/index.tscombining slices with immer + persistconfig/apps.config.tswith 5 demo appslib/url-codec.ts+lib/window-math.tshooks/useWindowUrlSync.tsvia nuqs
Desktop.tsx+Wallpaper.tsx+WallpaperPicker.tsxIconGrid.tsx+AppIcon.tsx(drag reorder, dblclick open, right-click menu)DesktopContextMenu.tsx(right-click on wallpaper)
hooks/usePointerDrag.ts(primitive)hooks/useWindowDrag.ts(snap physics)hooks/useWindowResize.ts(8-direction)ResizeHandle.tsx(cursor zones)WindowChrome.tsx(theme-adaptive controls)WindowMenuBar.tsx+WindowStatusBar.tsxWindow.tsx(full compositor with Motion)WindowManager.tsx(AnimatePresence)
GlassFilter.tsx(SVG singleton)lib/glsl/shimmer.vert.ts+shimmer.frag.tshooks/useGlassShimmer.ts(WebGL2 setup)GlassShimmer.tsx+LiquidGlass.tsx
DockItem.tsx(magnification, bounce, badge)Dock.tsx+Taskbar.tsx+NavBar.tsxMenuBar.tsx+MenuBarClock.tsx+ControlCenter.tsx+SystemTray.tsx
- All 5 theme compositor components
ThemeProvider.tsx(data attribute sync)- Test theme switching end-to-end
AppRegistry.tsxwith lazy importsSettings.tsx(theme/wallpaper/glass controls)Finder.tsx,Terminal.tsx,Browser.tsx,TextEditor.tsxIframeApp.tsx,MdxApp.tsx
next.config.tswith next-pwa configpublic/manifest.json- Offline fallback test
React.memoonAppIcon,DockItem, Window children- Keyboard shortcuts: Cmd+W close, Cmd+M minimize, Cmd+Tab app switch
- URL round-trip test (open 3 windows, share URL, verify restore)
- WebGL degradation test (disable WebGL, confirm glass still renders)
| File | Why Critical |
|---|---|
src/store/index.ts |
Everything depends on correct slice composition |
src/components/window/Window.tsx |
Most complex component — drag + resize + motion + z-index |
src/app/globals.css |
All theme-specific styling flows from CSS custom properties here |
src/components/themes/ThemeProvider.tsx |
Bridge between Zustand state and CSS data attributes |
src/lib/url-codec.ts |
Window state URL contract — wrong here = broken deep links |
src/config/apps.config.ts |
The user-facing extension point for adding new apps |
- Dev server:
npm run dev→ desktop loads with macOS theme and wallpaper - Window system: dblclick icon → window opens, drag title bar, resize edges, maximize/restore
- Snap physics: drag window to screen edge → resistance felt, releases past threshold
- Liquid glass: dock + menubar show refraction + shimmer (inspect WebGL context in DevTools)
- Theme switch: Settings app → switch to Windows 11 → layout changes, glass disappears
- URL state: open 2 windows, copy URL, open in new tab → same windows restore
- Offline: disable network in DevTools → desktop still loads from SW cache
- Mobile: resize browser to 375px → macOS theme switches to iPhone layout