Appearance
Vue UI Guide — OS
OS plugins run as standalone Electron windows, which changes how you structure layouts, handle navigation, and interact with the host system. This guide covers the OS-specific patterns for building your Vue pages.
Window Layout
Because your plugin occupies its own full Electron window, the root <template> of every page should fill the entire available space. Use h-full on the outermost wrapper:
vue
<template>
<div class="h-full p-3">
<!-- page content -->
</div>
</template>The m-2 wrapper is typical for content sections inside a page:
vue
<template>
<div class="h-full">
<div class="m-2">
<DataTable :value="items" />
</div>
</div>
</template>Unlike EA/SA plugins, you do not need appendTo="#containerElement" on dialogs or overlays — your plugin owns the whole window.
Navigation
Toolbar and Menu (configured in script.js)
Top-level navigation is declared in script.js via setToolbar() (icon buttons) and setMenu() (labeled menu items). When users click a toolbar button or menu item, your Vue Router handles the transition.
Back Button
For sub-pages (details, forms), use a back button that pushes to the parent route:
vue
<template>
<div class="h-full p-3">
<div class="flex align-items-center gap-2 mb-3">
<Button
icon="pi pi-arrow-left"
text
@click="$router.push('/')"
/>
<h2 class="m-0">Track Detail</h2>
</div>
<!-- content -->
</div>
</template>Root-Relative Routes
OS routes are root-relative. Always use / prefixed paths in $router.push():
js
// Navigate to settings
this.$router.push('/setting')
// Navigate to a detail page with a param
this.$router.push(`/detail/${track.id}`)
// Go back to root/home
this.$router.push('/')Data — Local Database
Use the api module imported at the top of your <script> block to read and write local JSON data. No axios, no remote server needed:
vue
<script>
import api from 'api'
export default {
data() {
return {
tracks: []
}
},
async mounted() {
const db = api.database('music')
this.tracks = await db.readJson(['data.json']) || []
},
methods: {
async save() {
const db = api.database('music')
await db.writeJson(['data.json'], this.tracks)
}
}
}
</script>api.database(name) creates or accesses a named local JSON store scoped to your plugin.
Native File Dialogs
OS plugins can open native system dialogs using ipcRenderer from Electron. This lets users pick files or folders from the OS file system:
vue
<script>
import { ipcRenderer } from 'electron'
export default {
methods: {
async pickFile() {
const result = await ipcRenderer.invoke('openDialog', {
properties: ['openFile'],
filters: [
{ name: 'Audio Files', extensions: ['mp3', 'wav', 'flac', 'm4a'] }
]
})
if (!result.canceled && result.filePaths.length) {
this.selectedFile = result.filePaths[0]
}
},
async pickFolder() {
const result = await ipcRenderer.invoke('openDialog', {
properties: ['openDirectory']
})
if (!result.canceled && result.filePaths.length) {
this.folder = result.filePaths[0]
}
}
}
}
</script>This is an OS-exclusive feature — SA and EA plugins do not have access to ipcRenderer.
Forms and Input
Use PrimeVue components and PrimeFlex utilities for forms. The same patterns from EA/SA apply:
vue
<template>
<div class="h-full p-3">
<!-- Page header with back button -->
<div class="flex align-items-center gap-2 mb-4">
<Button icon="pi pi-arrow-left" text @click="$router.push('/')" />
<h2 class="m-0">Add Track</h2>
</div>
<!-- Form -->
<div class="formgrid grid">
<div class="field col-12 md:col-6">
<label for="title">Title</label>
<InputText id="title" v-model="form.title" class="w-full" />
</div>
<div class="field col-12 md:col-6">
<label for="artist">Artist</label>
<InputText id="artist" v-model="form.artist" class="w-full" />
</div>
<div class="field col-12">
<label>File</label>
<div class="flex gap-2">
<InputText v-model="form.path" class="w-full" readonly />
<Button icon="pi pi-folder-open" @click="pickFile" />
</div>
</div>
</div>
<Button label="Save" icon="pi pi-check" @click="save" />
</div>
</template>Data Tables
Use DataTable from PrimeVue wrapped in m-2 for list views:
vue
<template>
<div class="h-full">
<div class="m-2">
<DataTable :value="tracks" paginator :rows="20" stripedRows>
<Column field="title" header="Title" />
<Column field="artist" header="Artist" />
<Column header="Actions">
<template #body="{ data }">
<Button
icon="pi pi-pencil"
text
@click="$router.push(`/detail/${data.id}`)"
/>
<Button
icon="pi pi-trash"
text
severity="danger"
@click="remove(data)"
/>
</template>
</Column>
</DataTable>
</div>
</div>
</template>Toast / Feedback
Use the useToast composable (or $toast in Options API) for feedback:
vue
<script>
import { useToast } from 'primevue/usetoast'
export default {
setup() {
const toast = useToast()
return { toast }
},
methods: {
async save() {
try {
await api.database('music').writeJson(['data.json'], this.tracks)
this.toast.add({ severity: 'success', summary: 'Saved', life: 3000 })
} catch {
this.toast.add({ severity: 'error', summary: 'Save failed', life: 3000 })
}
}
}
}
</script>
<template>
<Toast />
<!-- rest of template -->
</template>Settings Page Pattern
The setting.vue page follows a consistent pattern — a back button at the top and a form below:
vue
<template>
<div class="h-full p-3">
<div class="flex align-items-center gap-2 mb-4">
<Button icon="pi pi-arrow-left" text @click="$router.push('/')" />
<h2 class="m-0">Settings</h2>
</div>
<div class="formgrid grid">
<div class="field col-12">
<label for="musicFolder">Music Folder</label>
<div class="flex gap-2">
<InputText id="musicFolder" v-model="settings.folder" class="w-full" readonly />
<Button icon="pi pi-folder-open" @click="pickFolder" />
</div>
</div>
</div>
<Button label="Save Settings" icon="pi pi-save" @click="saveSettings" />
</div>
</template>i18n
Access translations with $t() using the namespace you defined in addLanguage():
vue
<template>
<div class="h-full p-3">
<h1>{{ $t('music.label') }}</h1>
<Button :label="$t('music.tracks')" />
</div>
</template>Key Differences from SA/EA
| Pattern | OS | SA / EA |
|---|---|---|
| Root layout | h-full full-window | Embedded container |
| Back button | $router.push('/') | Not typically needed |
| Route paths | Root-relative (/setting) | Relative (setting) |
| File dialogs | ipcRenderer.invoke('openDialog', ...) | Not available |
Dialog appendTo | Not needed | appendTo="#containerElement" |
| Content wrapper | m-2 | Component-specific |
| Main view | index.vue | index.vue |
Related Pages
- script.js API —
setFrame,setToolbar,setMenu,addPage,addLanguage - Development Guide — Full walkthrough with real code
- README Format — Plugin metadata