Content: make the desktop yours
Custom file system
Top-level keys merge directly into the desktop root — a top-level key IS a desktop item:
const myFileSystem = {
'ReadMe.txt': {
type: 'file',
name: 'ReadMe.txt',
app: 'Notepad',
content: 'Welcome to my desktop!',
},
'My Projects': {
type: 'folder',
name: 'My Projects',
children: {
'Project A.txt': { type: 'file', name: 'Project A.txt', app: 'Notepad', content: '…' },
},
},
'MyApp.lnk': { type: 'app_shortcut', name: 'MyApp.lnk', app: 'Calculator', icon: 'calculator' },
};
<WindowsXP customFileSystem={myFileSystem} />Node shape — type ('file' | 'folder' | 'app_shortcut'), name, optional icon (an XPIcon id), app (which registered app opens it), content (for files), children (for folders), and the puzzle attributes locked, password, broken.
File-type → app associations: Notepad (text), PhotoViewer (images), InternetExplorer (html/url), WindowsMediaPlayer (audio/video).
Merge vs replace. The default 'merge' overlays your nodes on the stock desktop. fileSystemMode="replace" keeps only OS scaffolding (Recycle Bin + an empty My Computer) and drops built-in shortcuts, preset content, and culture shortcuts — your customFileSystem becomes the whole world. That's the mode for portfolios, campaigns, and custom games.
Wallpapers & avatar
<WindowsXP
wallpapers={[{ id: 'brand', name: 'Brand', src: '/brand-wallpaper.jpg' }]}
defaultWallpaper="brand" // or a direct URL: "https://…/bg.jpg"
avatar="/me.png" // or an XPIcon id
/>Custom wallpapers appear in Display Settings alongside the built-ins. A culture package can also declare its default via CulturePackage.wallpaper (the defaultWallpaper prop wins).
Culture packages
A culture package defines a complete regional/era experience: desktop shortcuts, Start menu, browser homepage, sticky note, and i18n resources. Built-ins: zh (2005–2007 Chinese internet) and en (Western 2000s).
Author one with defineCulture(). It's an identity wrapper that gives you the CulturePackage type and, in dev builds, warns (naming the offending field) when a package would silently misbehave — an empty shortcut app, a duplicate item id, item locales that don't overlap the package's, or a Start-menu nameKey your i18n never defines:
import { WindowsXP, defineCulture } from '@caoergou/windows-xp';
const jpRetroCulture = defineCulture({
id: 'jp-retro',
displayName: '日本 2000s',
locales: ['ja', 'ja-JP'],
browser: { homepage: 'http://www.yahoo.co.jp' },
desktopShortcuts: [
// `app` must be a registered app id (built-in or from the `apps` prop).
{ id: 'nicovideo', name: 'ニコニコ動画', app: 'InternetExplorer', icon: 'ie' },
],
startMenu: {
pinned: [{ id: 'ie', action: 'InternetExplorer', nameKey: 'startMenu.apps.internetExplorer', icon: 'ie' }],
recent: [{ id: 'notepad', action: 'Notepad', nameKey: 'apps.notepad', icon: 'file' }],
},
stickyNote: { id: 'default', title: 'メモ', content: 'カスタム文化包のテスト' },
i18n: {
ja: {
'startMenu.apps.internetExplorer': 'Internet Explorer',
'apps.notepad': 'メモ帳',
},
},
});
<WindowsXP language="ja" cultures={[jpRetroCulture]} />Notes for third-language packages:
localesare base-aware. An item'slocales: ['ja']matches the'ja-JP'runtime language and vice versa — case-insensitively, on the base subtag. Omit item-levellocalesto show an item in every language the package covers; set them to scope an item to a subset (e.g. a shortcut only thejaaudience should see).- UI strings fall back to English for any key you don't provide in
i18n. - Start-menu items resolve names through
nameKeyonly, so provide those keys. appvalues must be registered app ids — a built-in, or one you pass via theappsprop. In dev, an unregistered id logs a warning at mount.
Wiring a culture app end-to-end (learned building the en Western-2000s pack in #123 — Winamp / Norton AntiVirus / uTorrent / iTunes / Microsoft Office):
- Build the component under
src/apps/(a rich flagship like Winamp can reuse the bundled sample clip for real audio; the rest can be themed shallow shells, like thezhThunder/Kugou apps). - Register it in
APP_REGISTRYwithlocales: ['en']so it only appears in that culture, anicon, awindowconfig, and anassociationsentry whoseappFieldequals the shortcut'sappvalue. - Add the desktop shortcut to the package's
desktopShortcuts— the shortcutnameis what users (anddata-english-testid="desktop-icon-<name>"selectors) see, so keep it exact;appmust match the registryappField. - Optionally pin it in
startMenuvia anameKey. - Assets must be original / parody artwork — no ripped third-party logos (DEVELOPMENT.md §6). The
enapp icons are hand-drawn SVGs.
Write your first app
defineApp() turns a component into a registrable app in one typed call — here's a complete, refresh-restorable hello-world in under 10 lines:
import { WindowsXP } from '@caoergou/windows-xp';
import { defineApp } from '@caoergou/windows-xp/registry';
const HelloApp = defineApp({
id: 'Hello',
name: 'Hello',
component: () => <div style={{ padding: 16 }}>Hello from Windows XP!</div>,
});
export default () => <WindowsXP apps={[HelloApp]} />;defineApp fills in the defaults (icon app_window, a 400×300 window, non-singleton) and derives restore from component for you. Open your registered app from the imperative handle:
const xp = useRef(null);
// …
<WindowsXP ref={xp} apps={[HelloApp]} />;
xp.current?.openApp('Hello'); // opens a window running HelloAppProps that survive a refresh. A window's props are persisted so it can be rebuilt on reload, so they must be JSON-serializable. defineApp enforces this at compile time — a function or element in your props is a type error, not a silent refresh bug:
const NoteApp = defineApp<{ text: string }>({
id: 'Note',
name: 'Note',
component: ({ text }) => <div>{text}</div>,
// window, nameKey, locales, lifecycle and associations are all optional.
});Rules that matter:
- Open custom apps via
ref.openApp(id)(above) — it resolves against the merged registry that includes yourapps.associations+getPropslet a filesystem node's.appfield open an app, but that path currently resolves built-in apps only; opening a custom app straight from a desktop/Explorer shortcut is being generalized (tracked with theappRoleswork in #122). - Add
nameKeyfor a translated display name;nameis the fallback. - Runtime callbacks belong on the event bus (
onEvent) orlifecycle, never in props. Reach window/session state from inside the component viauseApp(). - Need to hand-build an
AppRegistryEntry? Import therestoreApphelper from@caoergou/windows-xp/registryfor the sameunknown → propscast the built-ins use.
Build a blog on the desktop
The desktop makes a natural portfolio/blog shell — posts as .md files opened in the Markdown Viewer, permalinks, and an RSS feed + sitemap for SEO. It has its own guide: Build a blog on the desktop.