The Architecture of Cute Desk App

Cute Desk App desktop mockup showing multiple floating widget windows — clock, weather, notes, calendar, and chatbot — on a soft gradient background
KEY TAKEAWAYS
  • Zero frameworks — Cute Desk App is built with vanilla JavaScript, HTML, and CSS. No React, no Vue, no build step.
  • 22 widgets, each an ES Module, all managed by a single app.js core framework.
  • 6 widget rendering types — from simple static HTML to real-time Canvas games at 60fps.
  • All data lives in the browser — localStorage, IndexedDB, and Service Worker cache. No user accounts, no data leaks.
  • Built with Claude as an AI coding partner inside the Cursor IDE.

People sometimes ask me: how does Cute Desk App actually work? It looks like a desktop operating system — windows you can drag, resize, and rearrange, a dock at the bottom, widgets that persist your data between sessions. But there's no Electron, no React, no server. It's a browser tab.

I built the whole thing with Cursor and Anthropic's Claude as my coding partner. Every architectural decision — from the widget registry to the state management pattern to how Canvas games pause when you switch tabs — came from conversations between me and Claude, then translated directly into code. It's one of the most technically interesting things I've built, and I wanted to share exactly how it works.

Below is the full architecture diagram. Every service, every module, every widget type. Scroll through it — if you're a developer, I think you'll find the patterns here genuinely useful, even outside of Cute Desk App.

WANT TO GO DEEPER?
Hey — do you want to see how everything works under the hood? The diagram below shows the complete architecture of Cute Desk App: every layer, every widget, every data flow. This is exactly how I build Cute Desk App with help from Claude. Scroll through it and explore the lifecycle flow. Whether you're a developer curious about the patterns or just someone who loves knowing how things work — this one is for you.
ENJOYING WHAT YOU'RE LEARNING?
If you like what you're learning here and want to support the project, consider making a donation. It means a lot and helps me keep building. ❤️
Donate ❤️
BROWSER FIREBASE 🔥 Hosting CDN · HTTPS ⚙️ Service Worker Cache · Offline 📊 GA4 Analytics 🗃️ IndexedDB Photos (db.js) CORE app.js Core Framework 🪟 Window Manager 💾 State (localStorage) 🚢 Dock Manager 🎛️ Widget Registry 🖱️ Drag & Resize 🎨 Theming & Colors 🌍 i18n.js en·es·fr·de 🗄️ db.js IndexedDB API WIDGETS (22) ① STATIC notes · todo · calc · links · search · photo · meeting · solitaire · pixelart ② REAL-TIME clock · calendar · weather · timer · pomodoro · plant ③ EXTERNAL DATA weather (Open-Meteo) · news (Guardian) · chat (OpenAI) · youtube ④ SCALE DOM solitaire · merge ⑤ CANVAS (rAF) breakout · pixelart LOCAL JSON quotes (bundled JSON) ⑥ SYSTEM widgets (launcher panel) 🛡️ Help Form → Cloudflare Worker → Turnstile siteverify Gmail SMTP → Aldo's inbox EXT. APIs 🌤️ Open-Meteo 📍 BigDataCloud 📰 Guardian API 🤖 OpenAI ▶️ YouTube 💝 Stripe 🛡️ CF Turnstile
cutedesk.app — widgets float freely, draggable & resizable
Clock
10:42
Notes
Today:
- Buy coffee
- Call team
- Deploy app
Weather
🌤️
72°F
Sunny
To-Dos
☑ Deploy
☐ Write docs
☐ Code review
☐ PR merge
Chatbot
Hi! How can I help you today? Ask me anything...
Calendar
May 2026
16
Saturday
Quotes
"The best time to plant a tree was 20 years ago." — Proverb
News
📰 AI advances reshape industry
📰 New space mission launches
📰 Markets hit record high
Breakout
🎮 ●●●●●
●●●●●
Score: 420
Calculator
1,234
7 8 9 ÷
Plant
🌱
Stage 4
🕐
🌤️
📝
✅
📅
🤖
⏱️
🌱
🎮
•••
①
Static / Turn-Based
No intervals. Renders once on init; re-renders only on user interaction. Simplest pattern.
notes · todo · calc · links
search · photo · meeting · solitaire
②
Real-Time (Interval)
Uses setInterval. Starts/stops on show, hide, and page visibility changes to save CPU.
clock (1s) · calendar (60s)
timer (1s) · pomodoro (1s) · plant
③
External Data + Cache
Fetches from an API, caches in localStorage with a TTL. Serves cache first.
weather (30 min TTL)
news (4 hr TTL) · chat (live)
youtube (embed)
④
Scale-to-Fit DOM
Fixed logical layout scaled via transform: scale(). ResizeObserver adjusts scale on resize.
solitaire · merge
⑤
Scale-to-Fit Canvas
requestAnimationFrame loop. Canvas resolution + transform matrix updated on resize via ResizeObserver.
breakout (rAF 60fps)
pixelart (turn-based canvas)
⑥
System Panel
Opens at 600×400, always re-centered to 50% viewport. Acts as a launcher — not a persistent workspace widget.
widgets (launcher panel)
① Static
② Real-Time
③ External Data
④ Scale DOM
⑤ Canvas
⑥ System
🕐
Clock
clock
220 × 180 · always visible
② Real-Time (1s)
🌤️
Weather
weather
220 × 180
② + ③ External
📅
Calendar
calendar
220 × 180
② Real-Time (60s)
💬
Quotes
quotes
220 × 180
① Local JSON
🌱
Plant
plant
220 × 180
② Real-Time (daily)
📝
Notes
notes
280 × 280
① Static
✅
To-Dos
todo
280 × 280
① Static
🖼️
Photos
photo
280 × 280
① Static + IndexedDB
🤖
Chatbot
chat
280 × 280
③ External (OpenAI)
🔍
Search
search
280 × 180
① Static
🔗
Links
links
280 × 180
① Static
🧮
Calculator
calc
280 × 180
① Static
⏱️
Timer
timer
280 × 180
② Real-Time (1s)
🍅
Pomodoro
pomodoro
280 × 180
② Real-Time (1s)
🌍
Meeting Planner
meeting
280 × 280
① Static (timezone)
📰
News
news
280 × 280
③ External (Guardian)
▶️
YouTube
youtube
220 × 180
③ External (embed)
🃏
Solitaire
solitaire
220 × 220
④ Scale DOM
🎮
Breakout
breakout
220 × 180
⑤ Canvas rAF
🔢
Merge
merge
220 × 220
④ Scale DOM
🎨
Pixel Art
pixelart
280 × 280
⑤ Canvas (turn-based)
🔲
Widgets
widgets
600 × 400 · centered
⑥ System Launcher
app.js
The heart of the app. Handles window management, state, dock, drag/resize, lifecycle, and widget registry.
statesaveStateregisterWidgettoggleWidgetrenderDockwindowElementsescapeHtmlWIDGET_COLORS
i18n.js
Internationalization. No external libs. Supports en · es · fr · de. Detects locale from URL param → localStorage → navigator.
t(key)getLocale()setLocale()SUPPORTED_LOCALES
db.js
IndexedDB helpers for the Photo widget. Stores compressed JPEG images locally — no server upload required.
savePhoto()getPhoto()deletePhoto()
sw.js
Service Worker. Precaches all critical assets for offline support. Version controlled via version.json to bust stale caches.
CACHE_VERSIONPRECACHE_URLSinstall eventfetch event
pages-analytics.js
GA4 analytics for marketing pages. Separate from the app's inline analytics in index.html.
trackPageView()
widgets/*.js
22 ES Modules, one per widget. Each calls registerWidget(id, descriptor) to hook into the core framework.
initonShowonHideonPageHiddenonPageVisible
id="window-{widgetId}"
1
2
3
4
📅Calendar
May 2026
SMTWTFS 1 2345678 9101112131415 16
1
Titlebar — .window-titlebar

Draggable handle. Contains icon, title, and the ••• options menu. Height 36px. Transparent by default — shows the widget's background color through it.

2
Options Menu — .window-menu-popup

Appended to document.body to escape overflow:hidden and backdrop-filter clipping. Contains color picker, Reset Size, and widget-specific actions.

3
Content Area — #content-{id}

Widget fills this div. On load, shows a shimmer skeleton. The widget's init() replaces innerHTML to dismiss it.

4
Resize Handle — .window-resize-handle

Bottom-right corner. Enforces per-widget MIN_WIDGET_SIZES. Saves new size to state.windows[id] on mouse/touch up.

Key
desktopWidgets.v1
(localStorage)
Persistence
300ms debounce
saveState()
saveStateImmediate()
Cross-Tab Sync
storage event
→ re-renders
widget content only
→
state.windows
id · x · y · w · h
bgColor · defaultW
defaultH
state (top-level)
focusedWindowId
topZIndex
pageBackground
hiddenWidgets[]
dockOrder[]
state.widgets
notes.text
todos.items[]
weather.location
chat.messages[]
photo.images[]
timer.timers[]
…
→
Window Manager
reads windows{}
→ CSS position
→ CSS size
→ z-index
→ bgColor
Widget Modules
read state.widgets
→ render HTML
→ mutate on event
→ saveState()
Dock
reads hiddenWidgets
reads dockOrder
→ active / inactive
icons
→
Separate Keys
cute-desk-news-cache
cute-desk-quotes-cache
cute-desk-chat-key
cute-desk-consent
(own TTL / lifecycle)
IndexedDB
photo images
compressed JPEG
db.js helpers
separate from
localStorage
Boot
loadState()
Read from localStorage or generate default state. Run schema migrations for returning users.
→
Render
renderAll()
Set page background. Create DOM window for each visible widget. Apply saved positions.
→
Init
init()
Widget fills #content-{id}. Attaches events. Starts intervals (real-time) or fetches data (external).
→
Show / Hide
toggleWidget()
onShow → start intervals.
onHide → stop intervals, remove DOM.
→
Tab Visibility
visibilitychange
onPageHidden → pause all intervals & rAF loops.
onPageVisible → resume + check stale data.
→
Cross-Tab
storage event
Another tab changed state → sync state.widgets → re-render affected widgets.
public/ ├── index.html ← Minimal shell: #desktop div + dock + menu ├── js/ │ ├── app.js ← Core framework (window mgr · state · dock · drag) │ ├── i18n.js ← Localization: t() · en · es · fr · de │ ├── db.js ← IndexedDB helpers for photo widget │ ├── pages-analytics.js ← GA4 for marketing pages │ └── widgets/ ← 22 ES Modules, one per widget │ ├── clock.js ← Type ② · 220×180 │ ├── weather.js ← Type ②③ · 220×180 · Open-Meteo │ ├── calendar.js ← Type ② · 220×180 │ ├── quotes.js ← Type ① · 220×180 · local JSON │ ├── plant.js ← Type ② · 220×180 · daily check │ ├── notes.js ← Type ① · 280×280 · simplest widget │ ├── todo.js ← Type ① · 280×280 │ ├── photo.js ← Type ① · 280×280 · IndexedDB │ ├── chat.js ← Type ③ · 280×280 · OpenAI (user key) │ ├── search.js ← Type ① · 280×180 │ ├── links.js ← Type ① · 280×180 │ ├── calc.js ← Type ① · 280×180 │ ├── timer.js ← Type ② · 280×180 │ ├── pomodoro.js ← Type ② · 280×180 │ ├── meeting.js ← Type ① · 280×280 · timezone math │ ├── news.js ← Type ③ · 280×280 · Guardian API │ ├── youtube.js ← Type ③ · 220×180 · embed │ ├── solitaire.js ← Type ④ · 220×220 · Scale DOM │ ├── breakout.js ← Type ⑤ · 220×180 · Canvas rAF 60fps │ ├── merge.js ← Type ④ · 220×220 · Scale DOM │ ├── pixelart.js ← Type ⑤ · 280×280 · Canvas turn-based │ └── widgets.js ← Type ⑥ · 600×400 · Launcher panel ├── css/ │ └── styles.css ← All styles (one file, scoped per widget) ├── data/ │ ├── inspirational_quotes.json │ ├── version.json ← bumped on every deploy │ └── locales/ ← en.json · es.json · fr.json · de.json ├── images/ ← Widget assets, solitaire cards, clock backgrounds │ └── pixelart/ ← SVG stamp library ├── sw.js ← Service Worker · CACHE_VERSION · precache all assets └── help/ about/ blog/ features/ … ← Marketing pages
🏠
Landing Page
/
✨
Features
/features/
📖
Blog
/blog/
ℹ️
About
/about/
❓
Help / Contact
/help/
📱
Mobile Page
/mobile/
🔒
Privacy Policy
/privacy/
📜
Terms of Service
/terms/
📋
How To Use
/how-to-use/
🧩
Widget Pages
/widgets/{id}.html
🚫
404 Page
/404.html

If you have questions about how any of this works, or want to dig deeper into a specific pattern — the Canvas scaling math, the state migration system, how the options menu escapes backdrop-filter — feel free to reach out via the help page. I genuinely love talking about this stuff.

Aldo Aldo

More from the blog

Make it your start page

Tons of widgets. No account. Total privacy — on every new tab you open.

or try it in this tab first

Are you enjoying Cute Desk App? It's free for everyone — help keep it that way ❤️

Donate ❤️