This walkthrough builds an app like app-about — a singleton “About” window registered via defineDesktopApp. Use it as a checklist when starting a new repository.
Create a Nuxt module package with the layout from Package layout:
@owdproject/my-app/
├── package.json
├── src/
│ ├── module.ts
│ └── runtime/
│ ├── plugin.ts
│ ├── app.config.ts
│ └── components/Window/WindowMain.vue
└── playground/
├── nuxt.config.ts
├── desktop.config.ts
└── package.json
package.json essentials:
"type": "module"exports → ./dist/module.mjspeerDependencies: "@owdproject/core": "^3.4.0"devDependencies: nuxt, @nuxt/module-builder, @nuxt/schemadev:prepare (stub + playground prepare), prepack (release build)Clone app-about and rename if you prefer starting from a working tree.
src/module.tsThe module wires components, the register plugin, and Tailwind scanning:
import {
defineNuxtModule,
createResolver,
addComponentsDir,
addPlugin,
} from '@nuxt/kit'
import { registerTailwindPath } from '@owdproject/kit-tailwind/kit/registerTailwindPath'
export default defineNuxtModule({
meta: {
name: 'owd-app-my-app',
configKey: 'myApp', // optional — see step 3
},
async setup(_options, nuxt) {
const { resolve } = createResolver(import.meta.url)
addComponentsDir({ path: resolve('./runtime/components') })
addPlugin(resolve('./runtime/plugin'))
registerTailwindPath(nuxt, resolve('./runtime/components/**/*.{vue,mjs,ts}'))
},
})
Optional configKey lets consumers override app-specific defaults from desktop.config.ts (About uses configKey: 'about' and merges into runtimeConfig.public.desktop.about).
runtime/app.config.tsExport an ApplicationConfig object. From app-about:
export default {
id: 'org.owdproject.about',
title: 'About',
category: 'system-tools',
singleton: true,
icon: 'mdi:hexagon-multiple-outline',
windows: {
main: {
component: () => import('./components/Window/WindowAbout.vue'),
resizable: false,
size: { width: 448, height: 240 },
position: { x: 400, y: 240, z: 0 },
},
},
entries: {
about: { command: 'about' },
},
commands: {
about: (app) => {
const existing = app.getFirstWindowByModel('main')
if (existing) {
existing.actions.setActive(true)
existing.actions.bringToFront()
return existing
}
return app.openWindow('main')
},
},
}
Details: Windows and commands.
runtime/plugin.tsThe register plugin must run client-side only and use a stable name for dependsOn ordering:
import { defineNuxtPlugin } from 'nuxt/app'
import { defineDesktopApp } from '@owdproject/core'
import config from './app.config'
export default defineNuxtPlugin({
name: 'desktop-app-about-register',
async setup() {
if (import.meta.server) return
await defineDesktopApp(config)
},
})
Do not wrap defineDesktopApp in app:created — the current validator and reference apps call it directly in the plugin setup. See Plugins.
Window components live under runtime/components/Window/. Keep them theme-neutral:
DesktopWindowContent and core composables where possible.About’s window is a simple panel with props read from runtimeConfig.public.desktop.about when using configKey.
playground/nuxt.config.ts:
export default defineNuxtConfig({
modules: ['@owdproject/core'],
ssr: false,
compatibilityDate: 'latest',
})
playground/desktop.config.ts:
import { defineDesktopConfig } from '@owdproject/core'
export default defineDesktopConfig({
theme: '@owdproject/theme-nova',
apps: ['@owdproject/app-about'],
modules: [],
})
Critical rule: every package listed in theme, apps, or modules must appear in playground/package.json dependencies. Mismatch causes install or runtime resolution errors.
playground/package.json (minimal):
{
"dependencies": {
"@owdproject/core": "^3.4.0",
"@owdproject/theme-nova": "^3.4.0",
"@owdproject/app-about": "^3.4.0",
"nuxt": "^4.4.4"
}
}
When the theme imports PrimeVue directly (Nova explorer), also add primevue and @primeuix/themes — see Playground and Create a theme.
cd playground && pnpm install && pnpm dev
Or from the app root: pnpm run dev (runs dev:prepare then playground dev).
For dev ergonomics and GitHub Pages, auto-open your app after boot. Create playground/app/plugins/launch-about.client.ts:
import { defineNuxtPlugin } from 'nuxt/app'
export default defineNuxtPlugin({
name: 'app-about-playground-launch',
dependsOn: ['desktop-app-about-register'],
setup(nuxtApp) {
autoStartPlaygroundApps(nuxtApp, [
{ id: 'org.owdproject.about', entry: 'about', windowModel: 'main' },
])
},
})
Do not guard with if (!import.meta.dev) return — static generate and GitHub Pages need the same path.
Full reference: app-about launch plugin. See Plugins.
From the app repo root:
desktop validate .
Checks include src/runtime/plugin.ts, app.config.ts, playground deps on your package and core, and optional launch plugin.
To test inside the client monorepo:
// desktop/package.json
"@owdproject/app-about": "workspace:*"
// desktop/desktop.config.ts
apps: ['@owdproject/app-about']
pnpm install && pnpm run dev
module.ts and Tailwind.