description: End-to-end guide to building an OWD theme — minimal Paper vs full Nova, playground deps, PrimeVue. navigation: icon: i-lucide-list-checks
This guide walks through creating a theme module from zero. Compare **[theme-paper](https://github.com/owdproject/theme-paper)** (minimal) and **[theme-nova](https://github.com/owdproject/theme-nova)** (full shell).
| Aspect | Paper (minimal) | Nova (full) |
|---|---|---|
**@owdproject/kit-theme** | Yes | Yes |
**defaults + defu** | Small shell config | systemBar, dockBar, explorer flags |
| Extra plugins | None or minimal | 50.desktop-theme-nova-dialogs.client.ts |
**Desktop.vue** | Simple layout | Full shell + slots |
| Explorer UI | No | Conditional on @owdproject/module-fs |
Direct primevue imports | Unlikely | Yes (ContextMenu, DataTable, …) |
| Boot pages | No | Optional — see [theme-win95](https://github.com/owdproject/theme-win95) |
Typical tree:
@owdproject/theme-mytheme/
├── package.json
├── src/
│ ├── module.ts
│ └── runtime/
│ ├── components/
│ │ ├── Desktop.vue
│ │ ├── SystemBar.vue
│ │ ├── Background.vue
│ │ └── Window/
│ ├── composables/
│ └── plugins/ # optional client plugins
├── playground/
│ ├── nuxt.config.ts
│ ├── desktop.config.ts
│ └── package.json
└── dist/
**peerDependencies:**
{
"@owdproject/core": "^3.4.0",
"@owdproject/kit-primevue": "^3.4.0",
"@owdproject/kit-tailwind": "^0.1.0",
"nuxt": "^4.4.4"
}
src/module.tsdefineDesktopTheme instead of hand-rolling defu on public.desktop. See Migrate packages (3.3.2).Minimal pattern (Paper-like):
import { createResolver, addComponentsDir } from '@nuxt/kit'
import { installModule } from '@nuxt/kit'
import { defineDesktopTheme } from '@owdproject/core/kit/authoring'
import { registerThemeTailwindPath } from '@owdproject/kit-tailwind/kit/registerTailwindPath'
export default defineDesktopTheme({
meta: { name: 'owd-theme-mytheme' },
defaults: {
name: 'mytheme',
systemBar: { enabled: true, position: 'top', startButton: true },
},
async setup(_options, nuxt) {
const { resolve } = createResolver(import.meta.url)
await installModule('@owdproject/kit-primevue')
registerThemeTailwindPath(nuxt, import.meta.url)
addComponentsDir({ path: resolve('./runtime/components'), global: true })
},
})
Nova adds:
nuxt.options.primevue**addPlugin({ src: resolve('./runtime/plugins/50....client.ts'), mode: 'client' })****installModule('@owdproject/kit-primevue')** when module-fs is loadedSee Theme anatomy and Theme plugins.
Desktop.vueThe theme root wraps **DesktopCore** from core — see Theme anatomy — Desktop.vue.
Consumer **app.vue** renders **<Desktop />** (auto-imported globally from your components dir).
| Component | Role |
|---|---|
**Window*.vue** | Chrome around **DesktopWindow** / content slots |
**SystemBar** | Taskbar / menu bar |
**Background** | Wallpaper / desktop surface |
**DockBar** | Optional (Nova) |
Apps render inside window content areas — keep window wrappers in the theme, business UI in apps.
Dialog confirmation flows use a client plugin registered from module.ts:
addPlugin({
src: resolve('./runtime/plugins/50.desktop-theme-nova-dialogs.client.ts'),
mode: 'client',
})
Details: Theme plugins.
**playground/nuxt.config.ts:**
export default defineNuxtConfig({
modules: ['@owdproject/core'],
ssr: false,
compatibilityDate: 'latest',
})
**playground/desktop.config.ts** (Nova demo):
import { defineDesktopConfig } from '@owdproject/core'
export default defineDesktopConfig({
theme: '@owdproject/theme-nova',
modules: ['@owdproject/module-fs'],
apps: ['@owdproject/app-classic-audioplayer'],
systemBar: { enabled: true, startButton: true },
fs: {
mounts: { '/mnt/test': '/test-small.zip' },
},
})
**playground/package.json** must list every dependency above, plus PrimeVue when the theme imports it:
{
"dependencies": {
"@owdproject/app-classic-audioplayer": "^3.4.0",
"@owdproject/core": "^3.4.0",
"@owdproject/module-fs": "^3.4.0",
"@owdproject/module-persistence": "^3.4.0",
"@owdproject/theme-nova": "^3.4.0",
"@primeuix/themes": "^2.0.3",
"nuxt": "^4.4.4",
"primevue": "^4.5.5"
}
}
primevue explicitly?While @owdproject/kit-primevue configures PrimeVue during boot, themes that import primevue/... components directly in their templates must declare primevue and @primeuix/themes as peerDependencies (and have them in playground dependencies for local development) to ensure pnpm resolves them correctly.
Reference upstream: [theme-nova playground](https://github.com/owdproject/theme-nova/blob/main/playground/package.json) — add PrimeVue packages if missing locally.
cd playground && pnpm install && pnpm dev
desktop validate .
Wire into client/desktop with workspace:* when developing inside the monorepo.
For start/boot screens, add **runtime/pages/** routes and composables like **useSystemLifecycle**. See Pages and boot flow and [theme-win95](https://github.com/owdproject/theme-win95).
| Issue | Workaround |
|---|---|
primevue not found in playground | Add primevue + @primeuix/themes to playground deps |
desktop.config lists modules not in playground | Align package.json with every apps / modules / theme entry |