module.tsTypical pattern (Nova / Win95 / Paper):
import { createResolver, addComponentsDir, addPlugin, installModule } from '@nuxt/kit'
import { defineDesktopTheme } from '@owdproject/core'
import { registerThemeTailwindPath } from '@owdproject/kit-tailwind/kit/registerTailwindPath'
export default defineDesktopTheme({
meta: { name: 'desktop-theme-nova' },
defaults: {
name: 'nova',
systemBar: { enabled: true, position: 'top', startButton: true },
dockBar: { enabled: true, position: 'bottom' },
},
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 })
addPlugin({
src: resolve('./runtime/plugins/50.desktop-theme-nova-dialogs.client.ts'),
mode: 'client',
})
},
})
Avoid anti-patterns removed in recent themes:
paperShell object that repeats defaults.deepMerge(deepMerge(a, b), c) — use defu and module defaults instead.systemBar) — put them in defaults.runtimeConfig.public.desktopCore has already applied desktop.config.ts. The theme fills undefined keys only:
defu(existingDesktop, options)
User config wins on conflicts — correct for per-project overrides in desktop.config.ts.
Some themes (GNOME) also import a local desktop.config.ts inside the theme repo for packaged defaults:
const themeDefaults = (await import(resolve('./desktop.config.ts'))).default
nuxt.options.runtimeConfig.public.desktop = defu(
nuxt.options.runtimeConfig.public.desktop ?? {},
defu(themeDefaults, options),
)
Desktop.vueruntime/components/Desktop.vue is the theme entry component. The consumer desktop project mounts it from app.vue:
<template>
<Desktop />
</template>
| Layer | Owner | Role |
|---|---|---|
DesktopCore | @owdproject/core | Window manager, workspaces, app mounting, z-order |
Desktop.vue | Theme | Shell layout — background, system bar, dock, slots |
| Window chrome | Theme (Window*.vue) | Title bar, resize handles, theme styling |
| App content | App modules | Business UI inside window content slot |
<template>
<DesktopCore>
<template #background>
<Background />
</template>
<template #system-bar>
<SystemBar />
</template>
<template #dock>
<DockBar v-if="desktop.dockBar?.enabled" />
</template>
</DesktopCore>
</template>
Exact slot names follow DesktopCore API — inspect core’s DesktopCore.vue or reference theme-nova Desktop.
Paper uses a thinner wrapper; Nova adds dock, explorer integration, and richer chrome. Start minimal, add slots as the shell grows.
addComponentsDir({ global: true }) in module.ts auto-imports Desktop, SystemBar, etc. The component name matches the filename (Desktop.vue → <Desktop />).
| File | Role |
|---|---|
Window*.vue | Wrap DesktopWindow / DesktopWindowContent. |
DesktopWindowNavButton.vue | Title-bar button primitive (close, min/max, app tools). See Window chrome and nav buttons. |
SystemBar, Background, … | OS chrome |
| Dependency | When |
|---|---|
@owdproject/kit-primevue | Almost always for standard themes — handles PrimeVue dialogs, Tailwind CSS, and explorer UI components. |
@owdproject/module-fs | Required when adding filesystem support — provides headless virtual filesystem logic and explorer stores. |
See Theme and optional modules.
Declare @owdproject/core": "^3.4.0" and @owdproject/kit-primevue": "^3.4.0" as peers so install warns on API skew.
Themes that import PrimeVue directly should also peer primevue and @primeuix/themes.
In the client monorepo, devDependencies may use workspace:* for core while developing kits and themes together — see Package linking.
Call registerThemeTailwindPath once in theme setup, and call registerTailwindPath (both imported from @owdproject/kit-tailwind/kit/registerTailwindPath) for any custom paths containing Tailwind classes.