Appearance
Vue UI Guide
SA plugins use Vue 3 Single File Components (SFCs) with PrimeVue v3 for UI components and PrimeFlex for CSS utility classes. All PrimeVue components are registered globally — no imports needed in templates.
Vue Files in a Plugin
| File | Purpose |
|---|---|
widget.vue | Dashboard widget (registered via addWidget) |
setting.vue | Settings page (registered via addPage(..., "setting")) |
Any other .vue | Additional pages registered via addPage() |
Import other .vue files from your own plugin using the vue: prefix:
js
import MyDialog from "vue:my-plugin/dialogs/confirm.vue";
export default {
components: { MyDialog }
}Widget Layout
Widgets must wrap their root in the SA dashboard grid column — this is required for correct placement on the dashboard:
vue
<template>
<div class="col-12 lg:col-6 xl:col-3 p-1">
<div class="card h-full">
<h5>{{ $t("my-plugin.name") }}</h5>
<p>{{ value }}</p>
</div>
</div>
</template>
<script>
import api from 'api';
export default {
data() {
return { value: null }
},
created() {
this.value = api.database("my-plugin").readJson(["data.json"]);
}
}
</script>Omitting the
col-12 lg:col-6 xl:col-3 p-1wrapper will break the dashboard grid layout.
Page Layout
Standard pages use the card class for panels:
vue
<template>
<div>
<!-- header card -->
<div class="card flex align-items-center justify-content-between">
<h5 class="m-0">{{ $t("my-plugin.name") }}</h5>
<div>
<Button icon="pi pi-save" class="p-button-text" @click="save()" />
<Button icon="pi pi-refresh" class="p-button-text" @click="init()" />
</div>
</div>
<!-- content card -->
<div class="card">
<!-- content here -->
</div>
</div>
</template>Forms
Use the p-fluid formgrid grid pattern for responsive forms:
vue
<template>
<div class="card">
<div class="p-fluid formgrid grid">
<div class="field col-12 md:col-6">
<label for="name" class="block mb-2">Name</label>
<InputText id="name" v-model="form.name" />
</div>
<div class="field col-12 md:col-6">
<label for="type" class="block mb-2">Type</label>
<Dropdown
id="type"
v-model="form.type"
:options="[{ label: 'Admin', value: 'admin' }, { label: 'User', value: 'user' }]"
optionLabel="label"
optionValue="value"
class="w-full"
/>
</div>
<div class="field col-12 md:col-6">
<label for="count" class="block mb-2">Count</label>
<InputNumber id="count" v-model="form.count" :min="0" class="w-full" />
</div>
<div class="field col-12">
<label for="notes" class="block mb-2">Notes</label>
<Textarea id="notes" v-model="form.notes" rows="4" />
</div>
<div class="field col-12 md:col-4">
<label for="active" class="block mb-2">Active</label>
<InputSwitch id="active" v-model="form.active" />
</div>
</div>
<div class="flex justify-content-end gap-2">
<Button label="Cancel" icon="pi pi-times" class="p-button-text" @click="cancel" />
<Button label="Save" icon="pi pi-save" :loading="saving" @click="save" />
</div>
</div>
</template>Inline Editable Tables
SA plugins frequently use DataTable with editMode="row" for inline editing — ideal for settings lists:
vue
<template>
<DataTable
:value="items"
v-model:editingRows="editingRows"
editMode="row"
@row-edit-save="onRowEditSave"
responsiveLayout="scroll"
class="card"
:rows="20"
:paginator="items.length > 20"
>
<template #header>
<div class="flex align-items-center justify-content-between">
{{ $t("my-plugin.name") }}
<div>
<Button icon="pi pi-save" class="p-button-text" @click="save()" />
<Button icon="pi pi-plus" class="p-button-text" @click="items.push({})" />
<Button icon="pi pi-refresh" class="p-button-text" @click="init()" />
</div>
</div>
</template>
<Column field="name" header="Name" :sortable="true">
<template #editor="{ data, field }">
<InputText v-model="data[field]" class="w-full" />
</template>
</Column>
<Column field="icon" header="Icon" :sortable="true">
<template #editor="{ data, field }">
<div class="p-inputgroup flex-1">
<InputText v-model="data[field]" readonly />
<Button icon="pi pi-search" @click="openIcons(data, field)" />
</div>
</template>
</Column>
<Column headerStyle="width:0px;" bodyStyle="text-align:end">
<template #body="{ index }">
<Button icon="pi pi-trash" class="p-button-text p-button-danger" @click="remove(index)" />
</template>
</Column>
<Column :rowEditor="true" headerStyle="width: 110px;" bodyStyle="text-align:end" />
</DataTable>
</template>
<script>
import api from 'api';
import { EventBus } from 'global';
export default {
data() {
return { items: [], editingRows: [] };
},
created() {
this.init();
},
methods: {
init() {
this.items = api.database("my-plugin").readJson(["data.json"]);
},
onRowEditSave({ newData, index }) {
this.items[index] = newData;
},
openIcons(data, field) {
EventBus.emit("openIcons", icon => {
data[field] = icon;
});
},
remove(index) {
this.$confirm.require({
header: this.$t("text.confirmation"),
message: this.$t("text.areSure"),
acceptClass: "p-button-danger",
accept: () => this.items.splice(index, 1)
});
},
save() {
api.database("my-plugin").writeJson(["data.json"], this.items);
this.$toast.add({ severity: "success", summary: this.$t("my-plugin.name"), detail: this.$t("text.done") });
}
}
}
</script>Standard DataTable (read-only list)
vue
<template>
<DataTable
:value="items"
:rows="20"
:paginator="items.length > 20"
v-model:filters="filters"
filterDisplay="row"
dataKey="id"
responsiveLayout="scroll"
class="card mb-2"
>
<template #header>
<div class="flex align-items-center justify-content-between">
Items
<div>
<Button icon="pi pi-plus" class="p-button-text" @click="open('add')" />
<Button icon="pi pi-refresh" class="p-button-text" @click="init()" />
</div>
</div>
</template>
<Column field="name" header="Name" :sortable="true">
<template #filter="{ filterModel, filterCallback }">
<InputText v-model="filterModel.value" @input="filterCallback()" placeholder="Search" />
</template>
</Column>
<Column field="type" header="Type" :sortable="true" :showFilterMenu="false">
<template #body="{ data }">
<Chip :label="data.type" />
</template>
</Column>
<Column header="Actions">
<template #body="{ data }">
<div class="flex gap-2">
<Button icon="pi pi-pencil" v-tooltip.top="'Edit'" class="p-button-text" @click="open(data.id)" />
<Button icon="pi pi-trash" v-tooltip.top="'Delete'" class="p-button-text p-button-danger" @click="remove(data)" />
</div>
</template>
</Column>
</DataTable>
</template>Dialogs
vue
<template>
<Dialog
header="Edit Item"
v-model:visible="dialog"
modal
:draggable="false"
:style="{ width: '600px' }"
>
<div class="p-fluid formgrid grid">
<div class="field col-12">
<label>Title</label>
<InputText v-model="form.title" />
</div>
</div>
<template #footer>
<Button label="Cancel" icon="pi pi-times" class="p-button-text" severity="danger" @click="dialog = false" />
<Button label="Save" icon="pi pi-check" @click="save" autofocus />
</template>
</Dialog>
</template>Notifications (Toast)
js
this.$toast.add({ severity: 'success', summary: 'Saved', detail: 'Data saved successfully' });
this.$toast.add({ severity: 'error', summary: 'Error', detail: 'Something went wrong' });
this.$toast.add({ severity: 'warn', summary: 'Warning', detail: 'Check your input' });Confirm Dialog
js
this.$confirm.require({
header: this.$t("text.confirmation"),
message: this.$t("text.areSure"),
acceptLabel: this.$t("text.yes"),
rejectLabel: this.$t("text.no"),
acceptClass: "p-button-danger",
accept: () => {
// delete logic
}
});Routing
SA routes are registered via addPage() in script.js. Navigate with $router and read params with $route:
js
// Navigate to a page
this.$router.push(`/app/my-plugin/items`);
this.$router.push(`/app/my-plugin/items/${id}`);
// Navigate to a settings page
this.$router.push(`/app/setting/my-plugin`);
// Read dynamic param
const id = this.$route.params.id;
// React to param changes
watch: {
'$route.params.id'(id, old) {
if (id && id !== old) this.init();
}
}Database (api module)
SA uses the local api module instead of HTTP calls:
js
import api from 'api';
// Read JSON file
const data = api.database("my-plugin").readJson(["data.json"]);
// Write JSON file
api.database("my-plugin").writeJson(["data.json"], data);
// Nested path
const items = api.database("my-plugin").readJson(["subfolder", "items.json"]);Icon Picker
js
import { EventBus } from 'global';
openIcons(data, field) {
EventBus.emit("openIcons", icon => {
data[field] = icon; // e.g. "mdi mdi-home" or "pi pi-star"
});
}Icons
PrimeIcons — pi prefix:
html
<i class="pi pi-check"></i>
<Button icon="pi pi-plus" />Material Design Icons (MDI) — mdi prefix:
html
<i class="mdi mdi-bell-outline"></i>
<Button icon="mdi mdi-link" />i18n
Translations registered via addLanguage() in script.js:
vue
<template>
<div>
<h5>{{ $t("my-plugin.name") }}</h5>
<Button :label="$t('button.save')" />
</div>
</template>PrimeFlex Reference
| Category | Classes |
|---|---|
| Flex layout | flex, align-items-center, justify-content-between, justify-content-end, gap-2 |
| Grid | grid, col-12, col-6, md:col-6, lg:col-6, xl:col-3 |
| Form | p-fluid, formgrid, field |
| Spacing | m-0, mb-2, mb-3, mb-4, mt-1, p-1, p-3 |
| Width | w-full |
| Text color | text-blue-500, text-red-500, text-green-500 |
Full docs: v3.primevue.org · PrimeFlex: primeflex.org
Common PrimeVue Components
| Component | Usage |
|---|---|
Button | Actions, icon buttons |
InputText | Text input |
InputNumber | Numeric input |
InputSwitch | Boolean toggle |
Textarea | Multi-line text |
Dropdown | Single select |
MultiSelect | Multi select |
DataTable + Column | Tabular data (standard or inline editable) |
Dialog | Modal overlay |
Chip | Inline label |
Card | Content surface with title slot |
Toast | Notifications via $toast.add() |
ConfirmDialog | Confirmation via $confirm.require() |