Skip to content
Syno Package Land
Language: English

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.

Terminal window
synopkgland init -t native-go --ui plain my-plain-app

The 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)
Sourcefrontend/src/ (Vue 3, Vite, TypeScript)ui/app.js, ui/app.css
Buildnpm run build in frontend/, started by synopkgland buildnone
Files, jobs, delegate screensincludednot included
Standalone login (--with login)supportedrefused by check and init
Components@synopackageland/ui, @synopackageland/sdk/vuewrite 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:

Terminal window
npm install --save-exact @synopackageland/ui@0.9.10 --registry=https://registry.npmjs.org

For 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.

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.

<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.

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.

Import from @synopackageland/ui only. Do not import component files by path.

ComponentUse
AppRoot, ConfigProviderApp root, language
AppLayout, AppSidebar, PageContent, PageHeaderWindow layout
Button, Input, Textarea, Select, Checkbox, SwitchControls
Form, FormItem, FormActionsForms. Labels stack when the form is narrower than 520 px
SettingsSection, Callout, EmptyState, LoadingArea, ProgressSettings pages, messages, states
Dialog, ToastProvider, useToastDialogs and notifications. useToast must be called in a child of ToastProvider
TreeViewHierarchy with nodes, v-model, v-model:expanded and loadChildren to fetch children on first expand
DataGridTable with columns, rows, row-key, sorting, selectable rows and page-size paging
FilePickerChoose 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/:

Terminal window
npx --no syno-pkg-land-ui catalog
npx --no syno-pkg-land-ui describe Button

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 eventMeaning
open (v-model:open)Dialog visibility
providerFrom createNasProvider(ctx) in @synopackageland/sdk
localectx.locale
mode'file' or 'folder'
title, descriptionOptional dialog text
selected eventEmits the chosen path. Picking a path grants nothing: the backend checks the session and permissions again on use

See Files for the backend side.

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 propMeaning
v-modelSelected File[]
accept, multiple, directoryWhat can be chosen
maxFiles, maxSizeCount and size limits in bytes. A rejected file raises rejected with a reason of type, size, count or duplicate
variant'dropzone' or 'compact'
locale, textLanguage and text overrides

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.

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.

ItemLimit
Browser upload through the file browser1 file, 16 GiB
Smallest DSM window700 px wide, set by the generated window
Overlays in a DSM windowApp-local only. Do not mix app-local and document-wide overlays
Text size unitsrem, which follows the DSM desktop’s font size
Mounted apps on one pageEach needs a unique idPrefix
SymptomCauseFix
Dialog or menu appears outside the windowApp is not inside AppRootWrap the app in AppRoot
Margins and fonts look wrongtheme.css is loaded instead of the embedded themeImport themes/dsm7-embedded.css
Dialog titles point at another appTwo apps share an ID prefixSet a unique app.config.idPrefix before mount
Build says node_modules is missingbuild does not install packagesRun npm install in frontend/
init refuses --ui plain --with loginLogin needs the Vue UIUse --ui vue
Upload shows an upload-size message for a small fileThe picker rejected the file for another reason, such as more than one fileChoose a single file