@synopackageland/ui 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/LICENSES/THIRD_PARTY.md +1866 -0
- package/README.md +440 -0
- package/dist/components/app/app-action-bar/AppActionBar.vue.d.ts +15 -0
- package/dist/components/app/app-layout/AppLayout.vue.d.ts +26 -0
- package/dist/components/app/app-root/AppRoot.vue.d.ts +20 -0
- package/dist/components/app/app-sidebar/AppSidebar.vue.d.ts +34 -0
- package/dist/components/app/app-toolbar/AppToolbar.vue.d.ts +22 -0
- package/dist/components/app/page-header/PageHeader.vue.d.ts +17 -0
- package/dist/components/app/section-tabs/SectionTabs.vue.d.ts +16 -0
- package/dist/components/composite/callout/Callout.vue.d.ts +19 -0
- package/dist/components/composite/data-grid/DataGrid.vue.d.ts +126 -0
- package/dist/components/composite/data-grid/columnWidths.d.ts +6 -0
- package/dist/components/composite/data-grid/labels.d.ts +12 -0
- package/dist/components/composite/data-table/DataTable.vue.d.ts +34 -0
- package/dist/components/composite/empty-state/EmptyState.vue.d.ts +19 -0
- package/dist/components/composite/file-picker/FilePicker.vue.d.ts +49 -0
- package/dist/components/composite/search-field/SearchField.vue.d.ts +19 -0
- package/dist/components/composite/settings-row/SettingsRow.vue.d.ts +22 -0
- package/dist/components/composite/settings-section/SettingsSection.vue.d.ts +21 -0
- package/dist/components/composite/tree-view/TreeView.vue.d.ts +44 -0
- package/dist/components/primitives/accordion/Accordion.vue.d.ts +28 -0
- package/dist/components/primitives/badge/Badge.vue.d.ts +18 -0
- package/dist/components/primitives/button/Button.vue.d.ts +28 -0
- package/dist/components/primitives/button/IconButton.vue.d.ts +25 -0
- package/dist/components/primitives/checkbox/Checkbox.vue.d.ts +16 -0
- package/dist/components/primitives/context-menu/ContextMenu.vue.d.ts +26 -0
- package/dist/components/primitives/context-menu/ContextMenuItems.vue.d.ts +13 -0
- package/dist/components/primitives/context-menu/types.d.ts +13 -0
- package/dist/components/primitives/date-picker/DatePicker.vue.d.ts +25 -0
- package/dist/components/primitives/dialog/Dialog.vue.d.ts +44 -0
- package/dist/components/primitives/dropdown-menu/DropdownMenu.vue.d.ts +36 -0
- package/dist/components/primitives/input/Input.vue.d.ts +21 -0
- package/dist/components/primitives/input/Textarea.vue.d.ts +19 -0
- package/dist/components/primitives/loading/LoadingArea.vue.d.ts +8 -0
- package/dist/components/primitives/loading/LoadingIndicator.vue.d.ts +10 -0
- package/dist/components/primitives/loading/LoadingMask.vue.d.ts +20 -0
- package/dist/components/primitives/popover/Popover.vue.d.ts +24 -0
- package/dist/components/primitives/progress/Progress.vue.d.ts +11 -0
- package/dist/components/primitives/radio-group/RadioGroup.vue.d.ts +22 -0
- package/dist/components/primitives/select/Select.vue.d.ts +21 -0
- package/dist/components/primitives/separator/Separator.vue.d.ts +8 -0
- package/dist/components/primitives/sheet/Sheet.vue.d.ts +47 -0
- package/dist/components/primitives/skeleton/Skeleton.vue.d.ts +12 -0
- package/dist/components/primitives/spinner/Spinner.vue.d.ts +10 -0
- package/dist/components/primitives/split-button/SplitButton.vue.d.ts +30 -0
- package/dist/components/primitives/surface/Surface.vue.d.ts +22 -0
- package/dist/components/primitives/switch/Switch.vue.d.ts +15 -0
- package/dist/components/primitives/tabs/Tabs.vue.d.ts +28 -0
- package/dist/components/primitives/toast/ToastProvider.vue.d.ts +21 -0
- package/dist/components/primitives/toast/toast.d.ts +25 -0
- package/dist/components/primitives/tooltip/Tooltip.vue.d.ts +21 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +15795 -0
- package/dist/lib/appRoot.d.ts +2 -0
- package/dist/lib/cn.d.ts +2 -0
- package/dist/lib/localModal.d.ts +44 -0
- package/dist/reka-runtime.json +23 -0
- package/dist/theme.css +1 -0
- package/dist/themes/dsm7-embedded-reset.css +1 -0
- package/dist/themes/dsm7-embedded-theme.css +1 -0
- package/dist/themes/dsm7-embedded.css +1 -0
- package/dist/themes/dsm7.css +1 -0
- package/docs/app-local-modal.md +120 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
# @synopackageland/ui
|
|
2
|
+
|
|
3
|
+
Vue 3 component library with a DSM 7-inspired theme for application interiors.
|
|
4
|
+
Import the components you need into your Vue application.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
Synology Package Land UI provides Vue components for Synology package applications,
|
|
9
|
+
including an embedded theme and AppRoot for shared DSM desktop windows.
|
|
10
|
+
See the [public documentation](https://www.varvis.me/docs/).
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @synopackageland/ui --registry https://registry.npmjs.org
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Vue **3.5 or later within Vue 3** is required by the package. Building from
|
|
17
|
+
source requires Node.js **20+** and pnpm **10.33.2**.
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
### 1. Import the stylesheet once
|
|
22
|
+
|
|
23
|
+
In your application's entry point (`src/main.ts`):
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { createApp } from 'vue'
|
|
27
|
+
import App from './App.vue'
|
|
28
|
+
import '@synopackageland/ui/theme.css'
|
|
29
|
+
|
|
30
|
+
createApp(App).mount('#app')
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The stylesheet includes the DSM 7 theme and compiled component utilities. You do
|
|
34
|
+
not need to install or configure Tailwind to use these components. The package
|
|
35
|
+
also exports `themes/dsm7.css` with the same content; import only one of them.
|
|
36
|
+
For an app inside a DSM window, see [Using inside a DSM window](#using-inside-a-dsm-window).
|
|
37
|
+
|
|
38
|
+
### 2. Import and use components
|
|
39
|
+
|
|
40
|
+
Example `App.vue`:
|
|
41
|
+
|
|
42
|
+
```vue
|
|
43
|
+
<script setup lang="ts">
|
|
44
|
+
import { ref } from 'vue'
|
|
45
|
+
import { Button, Input, Select } from '@synopackageland/ui'
|
|
46
|
+
import type { SelectOption } from '@synopackageland/ui'
|
|
47
|
+
|
|
48
|
+
const name = ref('')
|
|
49
|
+
const access = ref('read')
|
|
50
|
+
const message = ref('')
|
|
51
|
+
const options: SelectOption[] = [
|
|
52
|
+
{ label: 'Read only', value: 'read' },
|
|
53
|
+
{ label: 'Read / write', value: 'write' },
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
function save() {
|
|
57
|
+
message.value = `Saved ${name.value} with ${access.value} access`
|
|
58
|
+
}
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<template>
|
|
62
|
+
<form class="settings-form" @submit.prevent="save">
|
|
63
|
+
<label for="name">Name</label>
|
|
64
|
+
<Input id="name" v-model="name" placeholder="Enter a name" />
|
|
65
|
+
<Select v-model="access" :options="options" aria-label="Access level" />
|
|
66
|
+
<Button type="submit" variant="primary" :disabled="!name.trim()">
|
|
67
|
+
Save
|
|
68
|
+
</Button>
|
|
69
|
+
<p role="status">{{ message }}</p>
|
|
70
|
+
</form>
|
|
71
|
+
</template>
|
|
72
|
+
|
|
73
|
+
<style scoped>
|
|
74
|
+
.settings-form {
|
|
75
|
+
display: grid;
|
|
76
|
+
gap: 12px;
|
|
77
|
+
max-width: 360px;
|
|
78
|
+
padding: 24px;
|
|
79
|
+
}
|
|
80
|
+
</style>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Components imported in `<script setup>` are available directly in the template.
|
|
84
|
+
No `app.use()` plugin registration is required. JavaScript consumers can omit
|
|
85
|
+
`lang="ts"`, the type import, and the `: SelectOption[]` annotation.
|
|
86
|
+
|
|
87
|
+
## Using inside a DSM window
|
|
88
|
+
|
|
89
|
+
A DSM package window is not a page of its own: every window shares the DSM
|
|
90
|
+
desktop's document and its stylesheets. Two things break `theme.css` there:
|
|
91
|
+
|
|
92
|
+
- DSM's stylesheets are not in cascade layers, and an unlayered rule beats every
|
|
93
|
+
layered one whatever its specificity. `theme.css` is layered (Tailwind 4), so
|
|
94
|
+
DSM's element resets such as `div,p,td{margin:0;padding:0}` win over the
|
|
95
|
+
library's utilities.
|
|
96
|
+
- `theme.css` puts its tokens on `:root`, where they would restyle the desktop
|
|
97
|
+
and collide with other apps that use another version of this library.
|
|
98
|
+
|
|
99
|
+
Use the embedded theme and the `AppRoot` component instead:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
// the DSM entry of your app, instead of theme.css
|
|
103
|
+
import '@synopackageland/ui/themes/dsm7-embedded.css'
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```vue
|
|
107
|
+
<script setup lang="ts">
|
|
108
|
+
import { AppRoot, ToastProvider } from '@synopackageland/ui'
|
|
109
|
+
</script>
|
|
110
|
+
|
|
111
|
+
<template>
|
|
112
|
+
<AppRoot style="height: 100%">
|
|
113
|
+
<ToastProvider>
|
|
114
|
+
<!-- your app -->
|
|
115
|
+
</ToastProvider>
|
|
116
|
+
</AppRoot>
|
|
117
|
+
</template>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`themes/dsm7-embedded.css`:
|
|
121
|
+
|
|
122
|
+
- has no `@layer`, so library classes beat DSM's element rules;
|
|
123
|
+
- scopes every token and rule to the app root, the element with the
|
|
124
|
+
`data-ui-app` attribute (`APP_ROOT_ATTRIBUTE` / `APP_ROOT_SELECTOR` are
|
|
125
|
+
exported), so nothing outside it changes;
|
|
126
|
+
- starts with a host reset that reverts, inside the app root, what DSM's
|
|
127
|
+
element-only rules set (margins, paddings, borders, outline, position,
|
|
128
|
+
display, fonts, colors, list style, `appearance`, …), including the
|
|
129
|
+
`body button:lang(…)` font rules;
|
|
130
|
+
- makes the app root the containing block of fixed overlays, so dialogs,
|
|
131
|
+
sheets and toasts stay inside the window.
|
|
132
|
+
|
|
133
|
+
`AppRoot` renders `<div data-ui-app data-syno-ui>` and routes every overlay
|
|
134
|
+
portal (Dialog, Sheet, Toast, Popover, Select, DropdownMenu, ContextMenu,
|
|
135
|
+
Tooltip) into itself, not `document.body`, where the scoped tokens
|
|
136
|
+
and reset would not reach them. Each `AppRoot` teleports into its own element;
|
|
137
|
+
theme and density tokens on sibling roots reach their own portals. Modal state
|
|
138
|
+
is shared across the document by default. Use `modal-scope="app"` for independent
|
|
139
|
+
Dialog/Sheet and popup layers (see below). `AppRoot` disables
|
|
140
|
+
reka-ui's scrollbar compensation, but this does not prevent other body mutations.
|
|
141
|
+
|
|
142
|
+
Give each independently mounted Vue application a unique ID prefix **before
|
|
143
|
+
mounting**, so generated dialog titles/descriptions and other ARIA references
|
|
144
|
+
cannot resolve to another app's elements:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const app = createApp(App)
|
|
148
|
+
app.config.idPrefix = 'my-package' // unique across apps in this document
|
|
149
|
+
app.mount('#my-package-root')
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Without `AppRoot`, put `data-ui-app` on your mount element and wrap the app in
|
|
153
|
+
reka-ui's `<ConfigProvider :teleport-to="rootElement" :scroll-body="false">`.
|
|
154
|
+
Inside `AppRoot`, a nested `ConfigProvider` (for `dir` or `locale`) must pass
|
|
155
|
+
the same `teleport-to`, and your own `<Teleport>`s must target an element inside
|
|
156
|
+
the root.
|
|
157
|
+
|
|
158
|
+
Theme overrides go on your own app root, after the library stylesheet. Use a
|
|
159
|
+
package-specific selector, so an override does not also match other apps:
|
|
160
|
+
|
|
161
|
+
```css
|
|
162
|
+
[data-ui-app][data-package='my-package'] {
|
|
163
|
+
--ui-primary: #0086e5;
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Set `data-package="my-package"` on that app's `<AppRoot>`.
|
|
168
|
+
|
|
169
|
+
Keep your app's own CSS out of cascade layers too, for the same reason.
|
|
170
|
+
|
|
171
|
+
If your app has its own base rules for bare elements, such as a button reset
|
|
172
|
+
or heading margins, load the embedded theme as its two parts, with your rules
|
|
173
|
+
between them:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import '@synopackageland/ui/themes/dsm7-embedded-reset.css'
|
|
177
|
+
import './base.css' // e.g. [data-ui-app] :where(button) { border: 0 solid; }
|
|
178
|
+
import '@synopackageland/ui/themes/dsm7-embedded-theme.css'
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Your rules and the library classes have the same specificity as the reset, so
|
|
182
|
+
order decides: after the reset, your rules are not reverted; before the theme,
|
|
183
|
+
they do not override library classes. Loaded one after the other, the two parts
|
|
184
|
+
render as `dsm7-embedded.css`. The reset reverts `font-family` on form controls
|
|
185
|
+
with a doubled root attribute, `[data-ui-app][data-ui-app] :where(button, input,
|
|
186
|
+
select, textarea)`, to beat DSM's `body button:lang(…)` rules; a font rule of
|
|
187
|
+
yours for them needs the same selector.
|
|
188
|
+
|
|
189
|
+
Known limits:
|
|
190
|
+
|
|
191
|
+
- Tailwind sizes are in `rem`, which follows the desktop's `html` font size.
|
|
192
|
+
- DSM rules more specific than one class (`.x-panel div`, `#id p`) are not
|
|
193
|
+
reverted; the reset covers element-only rules.
|
|
194
|
+
- Default Dialog/Sheet modality remains document-wide: it locks body scrolling
|
|
195
|
+
and pointer events, disables earlier panels and hides other apps.
|
|
196
|
+
- The bundled patched Reka restores host body style values/priorities after the
|
|
197
|
+
final lock release, including padding-right/margin-right. Consumers need no
|
|
198
|
+
Reka installation or package-manager patch.
|
|
199
|
+
- With `<AppRoot modal-scope="app" style="height: 100%">`, Dialog/Sheet and popup
|
|
200
|
+
layers instead block only that app's background. Other apps/host remain usable;
|
|
201
|
+
local dialogs omit aria-modal and permit deliberate focus switching. Provide
|
|
202
|
+
a bounded root size and an accessible host window switch. Select scope at mount
|
|
203
|
+
time. Nested app-local AppRoots and simultaneous app/document overlays are unsupported
|
|
204
|
+
and produce explicit errors. Multiple SDK/Reka runtime copies do not coordinate.
|
|
205
|
+
- Independently mounted Vue apps without unique `app.config.idPrefix` values
|
|
206
|
+
generate colliding IDs. `AppRoot` does not assign those prefixes automatically.
|
|
207
|
+
- Keyframe names (`ui-*`, `spin`, `pulse`) and Tailwind's `@property`
|
|
208
|
+
registrations are global, as in `theme.css`.
|
|
209
|
+
|
|
210
|
+
Body cleanup is now a normal regression test. The unprefixed ID collision
|
|
211
|
+
remains an expected failure, reproducible with `UI_ISOLATION_STRICT=1`.
|
|
212
|
+
See [App-local modal](docs/app-local-modal.md) for the implemented contract,
|
|
213
|
+
patch delivery and browser support limits. Vue stays an external peer; the
|
|
214
|
+
patched Reka implementation is bundled and tree-shakeable.
|
|
215
|
+
|
|
216
|
+
Run `pnpm check:local-browser [output-dir] [existing-ego-space-id]` for a clean
|
|
217
|
+
packed-consumer Chromium acceptance run (requires ego-browser).
|
|
218
|
+
|
|
219
|
+
## Common component patterns
|
|
220
|
+
|
|
221
|
+
### Dialog
|
|
222
|
+
|
|
223
|
+
Use `v-model:open` to control visibility. The default slot contains the body;
|
|
224
|
+
`trigger` and `footer` provide the opening button and action area.
|
|
225
|
+
|
|
226
|
+
```vue
|
|
227
|
+
<script setup lang="ts">
|
|
228
|
+
import { ref } from 'vue'
|
|
229
|
+
import { Button, Dialog } from '@synopackageland/ui'
|
|
230
|
+
|
|
231
|
+
const open = ref(false)
|
|
232
|
+
</script>
|
|
233
|
+
|
|
234
|
+
<template>
|
|
235
|
+
<Dialog v-model:open="open" title="Settings" description="Review your settings.">
|
|
236
|
+
<template #trigger>
|
|
237
|
+
<Button>Open settings</Button>
|
|
238
|
+
</template>
|
|
239
|
+
<p>Your settings content goes here.</p>
|
|
240
|
+
<template #footer>
|
|
241
|
+
<Button variant="primary" @click="open = false">Done</Button>
|
|
242
|
+
</template>
|
|
243
|
+
</Dialog>
|
|
244
|
+
</template>
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Toast notifications
|
|
248
|
+
|
|
249
|
+
Wrap your page with `ToastProvider` in `App.vue`:
|
|
250
|
+
|
|
251
|
+
```vue
|
|
252
|
+
<script setup lang="ts">
|
|
253
|
+
import { ToastProvider } from '@synopackageland/ui'
|
|
254
|
+
import SettingsPage from './SettingsPage.vue'
|
|
255
|
+
</script>
|
|
256
|
+
|
|
257
|
+
<template>
|
|
258
|
+
<ToastProvider>
|
|
259
|
+
<SettingsPage />
|
|
260
|
+
</ToastProvider>
|
|
261
|
+
</template>
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Then call `useToast()` inside a descendant, such as `SettingsPage.vue`:
|
|
265
|
+
|
|
266
|
+
```vue
|
|
267
|
+
<script setup lang="ts">
|
|
268
|
+
import { Button, useToast } from '@synopackageland/ui'
|
|
269
|
+
|
|
270
|
+
const toast = useToast()
|
|
271
|
+
</script>
|
|
272
|
+
|
|
273
|
+
<template>
|
|
274
|
+
<Button @click="toast.success('Settings applied')">Show notification</Button>
|
|
275
|
+
</template>
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
`useToast()` requires a provider ancestor; calling it in the same component that
|
|
279
|
+
renders the provider does not satisfy that requirement.
|
|
280
|
+
|
|
281
|
+
### Application layout
|
|
282
|
+
|
|
283
|
+
```vue
|
|
284
|
+
<script setup lang="ts">
|
|
285
|
+
import { AppLayout, Button, PageHeader } from '@synopackageland/ui'
|
|
286
|
+
</script>
|
|
287
|
+
|
|
288
|
+
<template>
|
|
289
|
+
<AppLayout min-height="100vh">
|
|
290
|
+
<PageHeader title="Package settings" description="Manage your application.">
|
|
291
|
+
<template #actions>
|
|
292
|
+
<Button variant="primary">Save</Button>
|
|
293
|
+
</template>
|
|
294
|
+
</PageHeader>
|
|
295
|
+
<section style="padding: 24px">Your page content goes here.</section>
|
|
296
|
+
</AppLayout>
|
|
297
|
+
</template>
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
`AppLayout` also accepts `sidebar`, `toolbar`, and `footer` slots.
|
|
301
|
+
|
|
302
|
+
## Theme override
|
|
303
|
+
|
|
304
|
+
Components use semantic CSS tokens. Import your override stylesheet after the library stylesheet to customize branding:
|
|
305
|
+
|
|
306
|
+
```css
|
|
307
|
+
:root {
|
|
308
|
+
--ui-primary: #0086e5;
|
|
309
|
+
--ui-primary-hover: #0077cf;
|
|
310
|
+
--ui-radius-md: 6px;
|
|
311
|
+
--ui-page-padding-x: 24px;
|
|
312
|
+
--ui-section-gap: 16px;
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Density can be switched per application subtree:
|
|
317
|
+
|
|
318
|
+
```html
|
|
319
|
+
<main data-ui-density="compact">
|
|
320
|
+
<!-- controls, navigation rows, toolbar and page spacing become denser -->
|
|
321
|
+
</main>
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Current component set
|
|
325
|
+
|
|
326
|
+
Primitives:
|
|
327
|
+
|
|
328
|
+
- Button / IconButton
|
|
329
|
+
- Input / Textarea
|
|
330
|
+
- Checkbox / RadioGroup / Switch
|
|
331
|
+
- Select
|
|
332
|
+
- Badge / Separator / Spinner / LoadingMask / LoadingArea / Progress / Skeleton / Surface
|
|
333
|
+
- SplitButton
|
|
334
|
+
- Tooltip / Popover / DropdownMenu / ContextMenu / Dialog
|
|
335
|
+
- Sheet / Drawer
|
|
336
|
+
- DatePicker
|
|
337
|
+
- ToastProvider + `useToast()`
|
|
338
|
+
- Tabs / Accordion
|
|
339
|
+
|
|
340
|
+
DSM-style application building blocks:
|
|
341
|
+
|
|
342
|
+
- SearchField
|
|
343
|
+
- SettingsRow / SettingsSection
|
|
344
|
+
- Callout / EmptyState
|
|
345
|
+
- DataTable / DataGrid
|
|
346
|
+
- TreeView
|
|
347
|
+
- FilePicker
|
|
348
|
+
- AppRoot (DSM-embedded app root)
|
|
349
|
+
- AppLayout / AppSidebar / AppToolbar / AppActionBar
|
|
350
|
+
- PageHeader / SectionTabs
|
|
351
|
+
|
|
352
|
+
`FilePicker` owns file selection/validation/drop-zone UI only; applications remain responsible for upload transport.
|
|
353
|
+
|
|
354
|
+
### DataGrid
|
|
355
|
+
|
|
356
|
+
```vue
|
|
357
|
+
<DataGrid
|
|
358
|
+
:columns="[{ key: 'name', label: 'Name', sortable: true, width: 170, minWidth: 80 }]"
|
|
359
|
+
:rows="rows"
|
|
360
|
+
:default-sort="{ key: 'name', direction: 'asc' }"
|
|
361
|
+
v-model:column-widths="savedWidths"
|
|
362
|
+
footer
|
|
363
|
+
refreshable
|
|
364
|
+
:labels="{ items: n => `${n} 個項目`, refresh: '重新整理', empty: '沒有資料' }"
|
|
365
|
+
@refresh="reload"
|
|
366
|
+
/>
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
- **Sort.** Click a sortable header, or focus it and press Enter/Space (`aria-sort` is set). Cycle: ascending, descending, off. Leave `sort` unbound and the grid sorts itself (start with `default-sort`); bind `v-model:sort` to control it; with `:local-sort="false"` it only emits.
|
|
370
|
+
- **Column resize.** Drag the right edge of a header, or focus the edge and press the left/right arrows (10px). Widths are px, never below the column's `minWidth` (default 40). Persist them with `v-model:column-widths` (or `@column-resize`); pass them back to restore. `resizable` is on by default; turn it off per grid or per column.
|
|
371
|
+
- **Layout.** Sticky header (default), column dividers, hover and selected row colours, `max-height` to scroll inside the grid. `selectable` adds checkboxes; with `select-on-row-click` a row click selects the row and Ctrl/Cmd-click toggles it.
|
|
372
|
+
- **Footer bar.** `footer` shows the DSM bar with the item count; `refreshable` adds the refresh button (`@refresh`, `refreshing` spins it). Without `footer` it appears only when there is more than one page or a `#footer` slot.
|
|
373
|
+
- **Localisation.** Every built-in string goes through `labels`: `empty`, `items(count)`, `page(page, count)`, `previous`, `next`, `refresh`, `selectAll`, `selectRow(key)`, `resizeColumn(label)`. `emptyText` still works.
|
|
374
|
+
- **Loading.** `loading` masks the table with a centred indicator; `loading-text` adds a message.
|
|
375
|
+
- Virtual scrolling is not provided; page long lists (`page-size`).
|
|
376
|
+
|
|
377
|
+
### Loading
|
|
378
|
+
|
|
379
|
+
`Spinner` is the DSM ring (`size` is the box in px, default 24). `LoadingMask` wraps content and covers it with a white 60% mask and a centred message box while `loading`; `LoadingArea` fills its container (give the parent a height) and centres the indicator, for whole-page or whole-panel waits. `Button loading` shows the 16px ring inline. Do not place a bare `Spinner` in a block container and expect it to be centred: use `LoadingArea`.
|
|
380
|
+
|
|
381
|
+
### Toolbar
|
|
382
|
+
|
|
383
|
+
`SplitButton` (button + menu arrow) and `SearchField variant="filter"` (the DSM funnel filter box) join `Button`, whose default look is the flat grey DSM toolbar button.
|
|
384
|
+
|
|
385
|
+
`DatePicker` uses an ISO `YYYY-MM-DD` string as its `v-model` so consumers do not need to expose the underlying date library in application state.
|
|
386
|
+
|
|
387
|
+
## Library development
|
|
388
|
+
|
|
389
|
+
This section is for changing the component library itself. Applications consuming
|
|
390
|
+
the published npm package do not need these steps.
|
|
391
|
+
|
|
392
|
+
```bash
|
|
393
|
+
pnpm install
|
|
394
|
+
pnpm build
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Use the pnpm version specified in `package.json`.
|
|
398
|
+
|
|
399
|
+
```bash
|
|
400
|
+
pnpm story # component workbench
|
|
401
|
+
pnpm dev # rebuild dist when library source changes
|
|
402
|
+
pnpm check # lint, typecheck, tests, and production build
|
|
403
|
+
pnpm story:build # build the component workbench
|
|
404
|
+
pnpm check:host # after pnpm build: DSM host simulation screenshots (Chrome)
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
`pnpm check:host` serves `tests/dsm-host` and screenshots each overlay scene in
|
|
408
|
+
English and Traditional Chinese with headless Chrome (set `CHROME` to its path
|
|
409
|
+
if it is not in `/Applications`), with and without `host-simulation.css`, a
|
|
410
|
+
stylesheet written for these tests that reproduces DSM's known element rules.
|
|
411
|
+
The images must be identical, and must match `theme.css` on a plain page. They
|
|
412
|
+
are left in `.dsm-host-shots/`.
|
|
413
|
+
|
|
414
|
+
In the workbench, open `DSM 7 / App Interior` and `DSM 7 / Advanced Components`
|
|
415
|
+
for reference compositions. See [SPEC.md](./SPEC.md) for architecture and
|
|
416
|
+
contribution rules, and [src/index.ts](./src/index.ts) for public exports.
|
|
417
|
+
|
|
418
|
+
### Try local changes in another application
|
|
419
|
+
|
|
420
|
+
Build this library first, then run the following in the consuming project,
|
|
421
|
+
replacing the path with your local checkout:
|
|
422
|
+
|
|
423
|
+
```bash
|
|
424
|
+
pnpm add link:/absolute/path/to/syno-pkg-land-ui
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
Keep `pnpm dev` running in the library checkout to rebuild `dist` as you edit.
|
|
428
|
+
Use the same package imports and stylesheet import shown above. To return to the
|
|
429
|
+
published package, replace the local link with the package:
|
|
430
|
+
|
|
431
|
+
```bash
|
|
432
|
+
pnpm add @synopackageland/ui
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
## Releases
|
|
436
|
+
|
|
437
|
+
The public npm package is `@synopackageland/ui`. Both release targets are built
|
|
438
|
+
from the same source and share runtime files, exports, MIT license and notices.
|
|
439
|
+
Public repository and issue tracker links will be added once those are selected.
|
|
440
|
+
See the [documentation](https://www.varvis.me/docs/) for usage.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
declare var __VLS_1: {}, __VLS_3: {};
|
|
2
|
+
type __VLS_Slots = {} & {
|
|
3
|
+
leading?: (props: typeof __VLS_1) => any;
|
|
4
|
+
} & {
|
|
5
|
+
default?: (props: typeof __VLS_3) => any;
|
|
6
|
+
};
|
|
7
|
+
declare const __VLS_base: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
|
|
8
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
9
|
+
declare const _default: typeof __VLS_export;
|
|
10
|
+
export default _default;
|
|
11
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
12
|
+
new (): {
|
|
13
|
+
$slots: S;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
sidebarWidth?: string;
|
|
3
|
+
minHeight?: string;
|
|
4
|
+
};
|
|
5
|
+
declare var __VLS_1: {}, __VLS_3: {}, __VLS_5: {}, __VLS_7: {};
|
|
6
|
+
type __VLS_Slots = {} & {
|
|
7
|
+
sidebar?: (props: typeof __VLS_1) => any;
|
|
8
|
+
} & {
|
|
9
|
+
toolbar?: (props: typeof __VLS_3) => any;
|
|
10
|
+
} & {
|
|
11
|
+
default?: (props: typeof __VLS_5) => any;
|
|
12
|
+
} & {
|
|
13
|
+
footer?: (props: typeof __VLS_7) => any;
|
|
14
|
+
};
|
|
15
|
+
declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
16
|
+
sidebarWidth: string;
|
|
17
|
+
minHeight: string;
|
|
18
|
+
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
19
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
20
|
+
declare const _default: typeof __VLS_export;
|
|
21
|
+
export default _default;
|
|
22
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
23
|
+
new (): {
|
|
24
|
+
$slots: S;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
modalScope?: 'document' | 'app';
|
|
3
|
+
};
|
|
4
|
+
declare var __VLS_7: {}, __VLS_9: {};
|
|
5
|
+
type __VLS_Slots = {} & {
|
|
6
|
+
default?: (props: typeof __VLS_7) => any;
|
|
7
|
+
} & {
|
|
8
|
+
default?: (props: typeof __VLS_9) => any;
|
|
9
|
+
};
|
|
10
|
+
declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
11
|
+
modalScope: "document" | "app";
|
|
12
|
+
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
13
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
14
|
+
declare const _default: typeof __VLS_export;
|
|
15
|
+
export default _default;
|
|
16
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
17
|
+
new (): {
|
|
18
|
+
$slots: S;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Component } from 'vue';
|
|
2
|
+
export type AppSidebarItem = {
|
|
3
|
+
key: string;
|
|
4
|
+
label: string;
|
|
5
|
+
icon?: Component;
|
|
6
|
+
badge?: string | number;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type AppSidebarGroup = {
|
|
10
|
+
label?: string;
|
|
11
|
+
items: AppSidebarItem[];
|
|
12
|
+
};
|
|
13
|
+
type __VLS_Props = {
|
|
14
|
+
modelValue?: string;
|
|
15
|
+
groups: AppSidebarGroup[];
|
|
16
|
+
/** Show the DSM search field above the list; it filters items by label. */
|
|
17
|
+
searchable?: boolean;
|
|
18
|
+
searchPlaceholder?: string;
|
|
19
|
+
/** Section headers get a chevron and fold their items. */
|
|
20
|
+
collapsible?: boolean;
|
|
21
|
+
};
|
|
22
|
+
declare const __VLS_export: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
|
|
23
|
+
select: (item: AppSidebarItem) => any;
|
|
24
|
+
"update:modelValue": (value: string) => any;
|
|
25
|
+
}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
26
|
+
onSelect?: ((item: AppSidebarItem) => any) | undefined;
|
|
27
|
+
"onUpdate:modelValue"?: ((value: string) => any) | undefined;
|
|
28
|
+
}>, {
|
|
29
|
+
collapsible: boolean;
|
|
30
|
+
searchable: boolean;
|
|
31
|
+
searchPlaceholder: string;
|
|
32
|
+
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
33
|
+
declare const _default: typeof __VLS_export;
|
|
34
|
+
export default _default;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
height?: string;
|
|
3
|
+
};
|
|
4
|
+
declare var __VLS_1: {}, __VLS_3: {}, __VLS_5: {};
|
|
5
|
+
type __VLS_Slots = {} & {
|
|
6
|
+
leading?: (props: typeof __VLS_1) => any;
|
|
7
|
+
} & {
|
|
8
|
+
default?: (props: typeof __VLS_3) => any;
|
|
9
|
+
} & {
|
|
10
|
+
actions?: (props: typeof __VLS_5) => any;
|
|
11
|
+
};
|
|
12
|
+
declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
13
|
+
height: string;
|
|
14
|
+
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
15
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
16
|
+
declare const _default: typeof __VLS_export;
|
|
17
|
+
export default _default;
|
|
18
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
19
|
+
new (): {
|
|
20
|
+
$slots: S;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
title: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
};
|
|
5
|
+
declare var __VLS_1: {};
|
|
6
|
+
type __VLS_Slots = {} & {
|
|
7
|
+
actions?: (props: typeof __VLS_1) => any;
|
|
8
|
+
};
|
|
9
|
+
declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
10
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
11
|
+
declare const _default: typeof __VLS_export;
|
|
12
|
+
export default _default;
|
|
13
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
14
|
+
new (): {
|
|
15
|
+
$slots: S;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type SectionTab = {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
};
|
|
6
|
+
type __VLS_Props = {
|
|
7
|
+
modelValue: string;
|
|
8
|
+
items: SectionTab[];
|
|
9
|
+
};
|
|
10
|
+
declare const __VLS_export: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
|
|
11
|
+
"update:modelValue": (value: string) => any;
|
|
12
|
+
}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
13
|
+
"onUpdate:modelValue"?: ((value: string) => any) | undefined;
|
|
14
|
+
}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
15
|
+
declare const _default: typeof __VLS_export;
|
|
16
|
+
export default _default;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
tone?: 'info' | 'success' | 'warning' | 'danger';
|
|
3
|
+
title?: string;
|
|
4
|
+
};
|
|
5
|
+
declare var __VLS_6: {};
|
|
6
|
+
type __VLS_Slots = {} & {
|
|
7
|
+
default?: (props: typeof __VLS_6) => any;
|
|
8
|
+
};
|
|
9
|
+
declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
10
|
+
tone: "info" | "success" | "warning" | "danger";
|
|
11
|
+
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
12
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
13
|
+
declare const _default: typeof __VLS_export;
|
|
14
|
+
export default _default;
|
|
15
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
16
|
+
new (): {
|
|
17
|
+
$slots: S;
|
|
18
|
+
};
|
|
19
|
+
};
|