UI components
@synopackageland/ui is a Vue 3 component library that follows DSM 7 styling. The default native-go project is a Vue app that uses it: a sidebar shell, a status page, and pages for the capabilities you enable. Those shell and capability pages come from @synopackageland/sdk/vue in the SDK. You can also write the window in plain JavaScript.
Set it up
Section titled “Set it up”synopkgland init -t native-go --ui plain my-plain-appThe default --ui vue project needs no extra flag. Its frontend/ folder pins exact versions of @synopackageland/ui 0.9.10 and @synopackageland/sdk 0.10.12. Run npm install once and commit the package-lock.json it writes; after that, npm ci installs exactly what the project was built with. The NAS never downloads npm packages: the build bundles everything.
| Vue (default) | Plain (--ui plain) | |
|---|---|---|
| Source | frontend/src/ (Vue 3, Vite, TypeScript) | ui/app.js, ui/app.css |
| Build | npm run build in frontend/, started by synopkgland build | none |
| Files, jobs, delegate screens | included | not included |
Standalone login (--with login) | supported | refused by check and init |
| Components | @synopackageland/ui, @synopackageland/sdk/vue | write your own |
Before the first build of a Vue project, run npm install in frontend/. build does not install packages.
To install the library in another Vue project:
npm install --save-exact @synopackageland/ui@0.9.10 --registry=https://registry.npmjs.orgFor a UI built with another tool, point ui in dsm-package.yaml at the build output and give the build command as a list. The build must emit app.js and app.css.
ui: dir: web/dist build: cwd: web command: [npm, run, build]app.js registers the window with window.DsmPackageApps[<package>].mount(root, ctx). The generated plain UI shows how to read the package name from document.currentScript.
Use it
Section titled “Use it”Mount inside a DSM window
Section titled “Mount inside a DSM window”Every DSM window shares one page with the desktop. Import the embedded theme instead of theme.css, wrap the app in AppRoot, and give each mounted app a unique idPrefix.
import { createApp } from 'vue'import App from './App.vue'import '@synopackageland/ui/themes/dsm7-embedded.css'
export function mount(root: HTMLElement, ctx: unknown) { const app = createApp(App, { ctx }) app.config.idPrefix = `my-package-${crypto.getRandomValues(new Uint32Array(4)).join('-')}` app.mount(root) return { unmount: () => app.unmount() }}The generated frontend/src/main.ts also registers mount for the DSM window loader. Keep that part when you edit it.
<script setup lang="ts">import { AppRoot, ConfigProvider } from '@synopackageland/ui'</script>
<template> <AppRoot modal-scope="app" style="height: 100%"> <ConfigProvider locale="en"> <!-- your pages --> </ConfigProvider> </AppRoot></template>AppRoot scopes the theme to your app and sends dialogs, menus and toasts into itself, so they stay inside the window. With modal-scope="app" (the default) a dialog blocks only your app. Give the root a bounded size such as height: 100%. A full-page web or Portal app can use modal-scope="document". Do not use that inside a DSM window. Do not import theme.css in a window.
For a full-page Application Portal launch, pass ctx to AppShell. When DSM’s visible banner names the app, the shell hides its duplicate brand; while DSM’s account menu is visible, it hides the default account footer. A custom sidebar-footer remains. Hidden, missing or unrecognized banner markup keeps the shell’s own controls. Detection reads only the containing DSM standalone window’s DOM and requires no administrator permission.
Layout
Section titled “Layout”<script setup lang="ts">import { AppLayout, AppSidebar, PageContent, PageHeader } from '@synopackageland/ui'</script>
<template> <AppLayout min-height="0" wide-layout="window"> <template #sidebar> <AppSidebar v-model="page" :groups="groups" /> </template> <PageContent> <PageHeader title="Status" description="MyApp" /> </PageContent> </AppLayout></template>AppLayout has sidebar, toolbar, default and footer slots. Keep wide-layout="window" in a DSM window, so a maximized window keeps its layout. Use wide-layout="web" only for a full-page Portal app. Below 600 px of layout width, the navigation turns into a slide-out. The generated shell keeps every page mounted and switches with v-show, so polling on one page continues while the user views another.
Page notes and header controls
Section titled “Page notes and header controls”Import PageInfo from @synopackageland/sdk/vue in a page rendered by AppShell. It puts one information button beside the page title, before custom #meta content. Nonempty notes appear as a list; empty entries are omitted and without custom slot content, an empty list hides the button. The button uses the shell’s language and note count. Click or keyboard focus opens it; Escape closes it.
<script setup lang="ts">import { PageInfo } from '@synopackageland/sdk/vue'</script>
<template> <PageInfo :notes="['Sizes include snapshots.', 'Refresh to read current usage.']" /> <PageInfo inline label="Search details" :notes="['Search covers indexed files.']" /> <PageInfo label="About this page" :notes="['Details']"> <p>Custom content replaces the notes list.</p> </PageInfo></template>inline keeps the button at its declared position. Without AppShell, it also stays in place. label overrides the accessible name. The default slot replaces the list and also shows the button without notes. side and align pass through to InfoTip and default to bottom and start. A standalone page fixture can import the PageInfoTarget injection key from the same SDK entry and call provide(PageInfoTarget, shallowRef<HTMLElement | null>(null)); set that ref to its header element to supply the destination, as with PageActionsTarget.
PageHeader keeps its title and meta on one row. When actions do not fit, their group moves below the title and aligns right. A title that still cannot fit truncates with an ellipsis; its title attribute preserves the full text.
InfoTip from @synopackageland/ui accepts align="start|center|end" (default center) and icon="help|info" (default help). Its bubble uses the current App window as its collision boundary, with 8 px padding, so it stays within a bounded AppRoot.
Components
Section titled “Components”Import from @synopackageland/ui only. Do not import component files by path.
| Component | Use |
|---|---|
AppRoot, ConfigProvider | App root, language |
AppLayout, AppSidebar, PageContent, PageHeader | Window layout |
Button, Input, Textarea, Select, Checkbox, Switch | Controls |
Form, FormItem, FormActions | Forms. Labels stack when the form is narrower than 520 px |
SettingsSection, Callout, EmptyState, LoadingArea, Progress | Settings pages, messages, states |
Dialog, ToastProvider, useToast | Dialogs and notifications. useToast must be called in a child of ToastProvider |
TreeView | Hierarchy with nodes, v-model, v-model:expanded and loadChildren to fetch children on first expand |
DataGrid | Table with columns, rows, row-key, sorting, selectable rows and page-size paging |
FilePicker | Choose files from the user’s computer |
Use library controls instead of native <select>, <input>, <label> or <a> elements. Icons come from @lucide/vue, which the project lists as its own dependency.
To discover props, slots and events, run from frontend/:
npx --no syno-pkg-land-ui catalognpx --no syno-pkg-land-ui describe ButtonPick NAS files
Section titled “Pick NAS files”NasPicker and FileBrowser are exports of @synopackageland/sdk/vue, not of @synopackageland/ui. FileBrowser combines TreeView and DataGrid over the signed-in user’s File Station view. NasPicker puts it in a dialog.
<NasPicker v-model:open="open" :provider="provider" :locale="ctx.locale" mode="file" @selected="path = $event" />NasPicker prop or event | Meaning |
|---|---|
open (v-model:open) | Dialog visibility |
provider | From createNasProvider(ctx) in @synopackageland/sdk |
locale | ctx.locale |
mode | 'file' or 'folder' |
title, description | Optional dialog text |
selected event | Emits the chosen path. Picking a path grants nothing: the backend checks the session and permissions again on use |
See Files for the backend side.
Upload from the user’s computer
Section titled “Upload from the user’s computer”FilePicker returns browser File objects. It does not know the NAS. The generated file browser uses it as the upload input and limits it to one file of at most 16 GiB:
<FilePicker v-model="uploads" variant="compact" :max-files="1" :max-size="16 * 1024 ** 3" @rejected="onRejected" />FilePicker prop | Meaning |
|---|---|
v-model | Selected File[] |
accept, multiple, directory | What can be chosen |
maxFiles, maxSize | Count and size limits in bytes. A rejected file raises rejected with a reason of type, size, count or duplicate |
variant | 'dropzone' or 'compact' |
locale, text | Language and text overrides |
Theme and density
Section titled “Theme and density”Override tokens on your own app root, after the library stylesheet, with a selector that cannot match other apps. Set data-package on AppRoot.
[data-ui-app][data-package='my-package'] { --ui-primary: #0086e5;}Put data-ui-density="compact" on an element to tighten controls and spacing inside it. Keep your own CSS out of cascade layers. If you need base rules for bare elements, load dsm7-embedded-reset.css, then your CSS, then dsm7-embedded-theme.css. Together the two files equal dsm7-embedded.css.
Languages
Section titled “Languages”The library translates its own text (file picker, dialog buttons, link annotations) in the nine app languages: en, zh-TW, zh-CN, ja, ko, de, fr, es and it. Set ConfigProvider locale from ctx.locale; uiLocale from @synopackageland/sdk maps DSM language codes such as cht to these names. Your own text lives in frontend/src/i18n/<locale>.json, see Native apps.
Limits
Section titled “Limits”| Item | Limit |
|---|---|
| Browser upload through the file browser | 1 file, 16 GiB |
| Smallest DSM window | 700 px wide, set by the generated window |
| Overlays in a DSM window | App-local only. Do not mix app-local and document-wide overlays |
| Text size units | rem, which follows the DSM desktop’s font size |
| Mounted apps on one page | Each needs a unique idPrefix |
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause | Fix |
|---|---|---|
| Dialog or menu appears outside the window | App is not inside AppRoot | Wrap the app in AppRoot |
| Margins and fonts look wrong | theme.css is loaded instead of the embedded theme | Import themes/dsm7-embedded.css |
| Dialog titles point at another app | Two apps share an ID prefix | Set a unique app.config.idPrefix before mount |
Build says node_modules is missing | build does not install packages | Run npm install in frontend/ |
init refuses --ui plain --with login | Login needs the Vue UI | Use --ui vue |
| Upload shows an upload-size message for a small file | The picker rejected the file for another reason, such as more than one file | Choose a single file |