Content: make the desktop yours
Custom file system
Pass customFileSystem to add your own files and folders to the desktop. Each top-level key becomes a desktop icon:
import { WindowsXP } from '@caoergou/windows-xp';
import '@caoergou/windows-xp/style.css';
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' },
};
export default function App() {
return <WindowsXP customFileSystem={myFileSystem} autoLogin skipBoot />;
}Node shape — every node has a type ('file' | 'folder' | 'app_shortcut') and a name. Files can have content and an app that opens them; folders have children. All nodes can optionally set an icon (an XPIcon id).
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.
File-type → app associations: Notepad (text), PhotoViewer (images), InternetExplorer (html/url), WindowsMediaPlayer (audio/video).
Puzzle attributes. Files and folders can be marked locked, password, or broken to turn them into puzzle gates. See docs/PUZZLE-DESIGN.md for the full interaction model.
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 (2000s Chinese internet) and en (English-language 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]} />;The built-in QQ client reads its identity and login defaults from the culture package's qq profile. The account field falls back to me.number; use the optional login block when a fictional desktop should show remembered credentials:
import type { QQProfile } from '@caoergou/windows-xp';
const qq: QQProfile = {
me: {
number: '123456',
nickname: 'Night Train',
avatar: 50,
status: 'online',
},
login: {
password: 'story-password',
rememberPassword: true,
},
groups: [],
buddies: [],
};These credentials only drive the simulated login form; they are not an authentication boundary. Login stays disabled until both fields are non-empty.
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.
Contributor note — wiring a culture app into the library
The steps below are for authors who are contributing a new built-in culture app to the
windows-xprepository itself (editingsrc/apps/andAPP_REGISTRY). If you are a package consumer adding a custom app to your own project, usedefineApp()(see Write your first app) and reference itsidin your culture package'sdesktopShortcuts.
Wiring a culture app end-to-end (learned building the en English-language 2000s pack — 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:
import { useRef } from 'react';
import type { XPHandle } from '@caoergou/windows-xp';
const xp = useRef<XPHandle>(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; useref.openApp(id)to open a custom app from your host code. - 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.
QQ group chat and message history
Mount authored QQ conversations through a content pack. A conversation with kind: 'group' powers both the 群/校友录 group-chat window and the separate, read-only Message History Manager:
import type { ContentPack } from '@caoergou/windows-xp';
const storyPack: ContentPack = {
id: 'story-chat',
qqArchives: [
{
id: 'summer-2006',
conversations: [
{
id: 'classmates',
title: 'Weekend LAN Party',
kind: 'group',
memberIds: ['alice', 'bob'],
messages: [
{
id: 'classmates-1',
senderId: 'alice',
senderName: 'Alice',
sentAt: '2006-08-12T17:41:00+08:00',
text: 'Eight o’clock tonight?',
},
],
},
],
},
],
};
<WindowsXP contentPacks={[storyPack]} />;Member ids resolve against the active culture's qq.buddies; sender names in the authored messages remain the fallback. Sending in the group window is session-local and does not mutate the authored archive. Selecting the panel entry also emits ui:action with appId: 'QQ', control: 'open-group-chat', and the selected conversation id as value.
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.