@tenorlab/vue-dashboard 1.4.7 → 1.4.91
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/README.md +395 -20
- package/package.json +2 -2
- package/dist/core.d.ts +0 -302
- package/dist/core.es.js +0 -369
- package/dist/styles.css +0 -1
- package/dist/vue-dashboard.d.ts +0 -845
- package/dist/vue-dashboard.es.js +0 -2037
package/README.md
CHANGED
|
@@ -1,49 +1,424 @@
|
|
|
1
1
|
# @tenorlab/vue-dashboard
|
|
2
2
|
|
|
3
|
-
Foundation components for creating user-configurable, high-performance dashboards in Vue.
|
|
3
|
+
Foundation components for creating user-configurable, high-performance dashboards in Vue. Built on top of **@tenorlab/dashboard-core**.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## 🏗 Relationship to Core
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
**Tenorlab** is the specialized software foundry for the [@builtwithjavascript](https://github.com/builtwithjavascript) ecosystem, focusing on modular, type-safe utilities and UI kits.
|
|
7
|
+
This package extends **@tenorlab/dashboard-core**. It provides the Vue implementation of the core logic, including specialized hooks, state management via **Zustand**, and a suite of UI components.
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
> **Note**: This package re-exports all types and utilities from `@tenorlab/dashboard-core`. You do not need to install the core package separately.
|
|
10
|
+
|
|
11
|
+
## ✨ Features
|
|
12
|
+
|
|
13
|
+
- **Type-Safe:** Deep integration with TypeScript 5.8+ for full IDE support.
|
|
14
|
+
- **State Management:** Built-in `useDashboardStore` (Zustand) and `useDashboardUndoService`.
|
|
15
|
+
- **User Configurable:** Ready-to-use components for adding, removing, and dragging widgets.
|
|
16
|
+
- **Themeable:** Native support for CSS Variables and Tailwind CSS.
|
|
17
|
+
- **Vite Optimized:** Full ESM support and tree-shakeable.
|
|
11
18
|
|
|
12
19
|
## 🚀 Quick Start
|
|
13
20
|
|
|
14
21
|
### Installation
|
|
15
22
|
|
|
16
|
-
|
|
23
|
+
Bash
|
|
17
24
|
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
```
|
|
26
|
+
# with npm
|
|
27
|
+
npm i @tenorlab/vue-dashboard
|
|
28
|
+
|
|
29
|
+
# with pnpm
|
|
21
30
|
pnpm add @tenorlab/vue-dashboard
|
|
22
31
|
```
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
### 1. Global Styles
|
|
34
|
+
|
|
35
|
+
Import the base styles in your entry file (e.g., `main.ts`):
|
|
36
|
+
|
|
37
|
+
```TypeScript
|
|
27
38
|
import '@tenorlab/vue-dashboard/styles.css'
|
|
28
39
|
```
|
|
29
40
|
|
|
30
|
-
|
|
41
|
+
------
|
|
42
|
+
|
|
43
|
+
## 🛠 Developer Guide
|
|
44
|
+
|
|
45
|
+
### 1. Creating a Widget
|
|
46
|
+
|
|
47
|
+
Widgets should be organized by their loading strategy.
|
|
48
|
+
|
|
49
|
+
- **Bundled Widgets**: Place in `src/bundled-widgets/` (loaded immediately).
|
|
50
|
+
- **Async Widgets**: Place in `src/async-widgets/` (lazy-loaded).
|
|
51
|
+
|
|
52
|
+
Each widget requires a sub-directory using the `widget-name-here` convention.
|
|
53
|
+
|
|
54
|
+
#### Example: `WidgetTotalOrders`
|
|
55
|
+
|
|
56
|
+
Directory name `widget-total-orders`, files:
|
|
57
|
+
- WidgetTotalOrders.vue
|
|
58
|
+
- meta.ts
|
|
59
|
+
- index.ts
|
|
60
|
+
|
|
61
|
+
```Vue
|
|
62
|
+
// file: src/bundled-widgets/widget-total-orders/WidgetTotalOrders.vue
|
|
63
|
+
<script setup lang="ts">
|
|
64
|
+
import type {
|
|
65
|
+
IDashboardWidgetProps,
|
|
66
|
+
TDashboardWidgetKey,
|
|
67
|
+
TWidgetEmits,
|
|
68
|
+
} from '@tenorlab/vue-dashboard'
|
|
69
|
+
import { DashboardWidgetBase, WrapperColumnContent, useWidgetEmits } from '@tenorlab/vue-dashboard'
|
|
70
|
+
|
|
71
|
+
const WidgetKey: TDashboardWidgetKey = 'WidgetTotalOrders'
|
|
72
|
+
|
|
73
|
+
defineProps<IDashboardWidgetProps>()
|
|
74
|
+
const emits = defineEmits<TWidgetEmits>()
|
|
75
|
+
const { removeClick: onRemoveClick, moveClick: onMoveClick } = useWidgetEmits(emits)
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<template>
|
|
79
|
+
<DashboardWidgetBase
|
|
80
|
+
:widgetKey="WidgetKey"
|
|
81
|
+
title="Total Orders"
|
|
82
|
+
:parentWidgetKey="parentWidgetKey"
|
|
83
|
+
:index="index"
|
|
84
|
+
:maxIndex="maxIndex"
|
|
85
|
+
:isEditing="isEditing"
|
|
86
|
+
@removeClick="onRemoveClick"
|
|
87
|
+
@moveClick="onMoveClick"
|
|
88
|
+
>
|
|
89
|
+
<WrapperColumnContent>
|
|
90
|
+
<div class="dashboard-number number-xl text-primary">1,250</div>
|
|
91
|
+
<div class="text-sm">Orders this month</div>
|
|
92
|
+
</WrapperColumnContent>
|
|
93
|
+
</DashboardWidgetBase>
|
|
94
|
+
</template>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// file: src/bundled-widgets/widget-total-orders/meta.ts
|
|
99
|
+
import type { TWidgetMetaInfo } from '@tenorlab/vue-dashboard'
|
|
100
|
+
import { MonitorIcon as ComponentIcon } from '@tenorlab/vue-dashboard'
|
|
101
|
+
import { markRaw } from 'vue'
|
|
102
|
+
|
|
103
|
+
// Define the metadata object for the plugin
|
|
104
|
+
export const WidgetTotalOrdersMeta: TWidgetMetaInfo = {
|
|
105
|
+
name: 'Total Orders',
|
|
106
|
+
categories: ['Widget'],
|
|
107
|
+
icon: markRaw(ComponentIcon),
|
|
108
|
+
noDuplicatedWidgets: true,
|
|
109
|
+
description: 'Displays information about your total orders.',
|
|
110
|
+
externalDependencies: [],
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// file: src/bundled-widgets/widget-total-orders/index.ts
|
|
116
|
+
import WidgetTotalOrders from './WidgetTotalOrders.vue'
|
|
117
|
+
export default WidgetTotalOrders
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
### 2. Creating the Widgets Catalog
|
|
122
|
+
|
|
123
|
+
Create `src/widgets-catalog.ts` in your project root. This file manages how widgets are discovered (locally via Vite's `import.meta.glob` or remotely via CDN).
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
// file: src/widgets-catalog.ts
|
|
128
|
+
import {
|
|
129
|
+
IDynamicWidgetCatalogEntry,
|
|
130
|
+
TDashboardWidgetCatalog,
|
|
131
|
+
TWidgetMetaInfoBase,
|
|
132
|
+
WidgetContainerColumn,
|
|
133
|
+
WidgetContainerLarge,
|
|
134
|
+
WidgetContainerRow,
|
|
135
|
+
TWidgetFactory,
|
|
136
|
+
} from '@tenorlab/vue-dashboard'
|
|
137
|
+
import {
|
|
138
|
+
createStaticEntry as _createStaticEntry,
|
|
139
|
+
localWidgetDiscovery,
|
|
140
|
+
remoteWidgetDiscovery as _remoteWidgetDiscovery,
|
|
141
|
+
} from '@tenorlab/vue-dashboard/core'
|
|
142
|
+
|
|
143
|
+
import WidgetRecentPaymentInfo from './other-widgets/WidgetRecentPaymentInfo.vue'
|
|
144
|
+
//import { getWidgetsManifestUrl } from '@/utils'
|
|
145
|
+
|
|
146
|
+
const bundledWidgetsSrcPath = '/src/bundled-widgets'
|
|
147
|
+
const asyncWidgetsSrcPath = '/src/async-widgets'
|
|
148
|
+
|
|
149
|
+
import { markRaw } from 'vue'
|
|
150
|
+
import type { Component as VueComponent } from 'vue'
|
|
151
|
+
|
|
152
|
+
// Use Vite's Glob Import
|
|
153
|
+
// This creates an object where the keys are file paths, and the values are the TWidgetFactory functions.
|
|
154
|
+
// We target the 'index.ts' files within the widgets subdirectories.
|
|
155
|
+
type TGlobModuleMap = Record<string, TWidgetFactory>
|
|
156
|
+
|
|
157
|
+
// Eagerly loaded (Non-lazy / Bundled):
|
|
158
|
+
const bundledWidgetModules = import.meta.glob('/src/bundled-widgets/*/index.ts', {
|
|
159
|
+
eager: true /* we load this immediately */,
|
|
160
|
+
}) as TGlobModuleMap
|
|
161
|
+
|
|
162
|
+
// Lazy loaded (Code-split / Plugins):
|
|
163
|
+
const asyncWidgetModules = import.meta.glob('/src/async-widgets/*/index.ts') as TGlobModuleMap
|
|
164
|
+
|
|
165
|
+
// Meta modules (Always eager so titles/icons are available immediately)
|
|
166
|
+
const allMetaModules = import.meta.glob('/src/**/widget-*/meta.ts', {
|
|
167
|
+
eager: true,
|
|
168
|
+
}) as Record<string, Record<string, TWidgetMetaInfoBase>>
|
|
169
|
+
|
|
170
|
+
const hasPermission = (_user: any, _permission: string) => true
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* @name createStaticEntry
|
|
174
|
+
* Wraps around createStaticEntry from npm package to ensure the component added is marked raw (using markRaw)
|
|
175
|
+
*/
|
|
176
|
+
export const createStaticEntry = (
|
|
177
|
+
key: string,
|
|
178
|
+
component: VueComponent,
|
|
179
|
+
metaData?: TWidgetMetaInfoBase,
|
|
180
|
+
): [string, IDynamicWidgetCatalogEntry] => {
|
|
181
|
+
return _createStaticEntry(key, markRaw(component), metaData)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @name getWidgetCatalog
|
|
186
|
+
* @description Dynamically builds the widgets catalog based on user type and operations/permissions.
|
|
187
|
+
*/
|
|
188
|
+
export const getWidgetCatalog = async (user: any | null): Promise<TDashboardWidgetCatalog> => {
|
|
189
|
+
// A. Register Static Core Components
|
|
190
|
+
const catalogMapEntries: [string, IDynamicWidgetCatalogEntry][] = [
|
|
191
|
+
createStaticEntry('WidgetContainer', WidgetContainerColumn),
|
|
192
|
+
createStaticEntry('WidgetContainerRow', WidgetContainerRow),
|
|
193
|
+
createStaticEntry('WidgetContainerLarge', WidgetContainerLarge),
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
// B. Register Business Static Widgets
|
|
197
|
+
// we could filter further by permissions and user type if needed
|
|
198
|
+
if (hasPermission(user, 'some-permission')) {
|
|
199
|
+
catalogMapEntries.push(createStaticEntry('WidgetRecentPaymentInfo', WidgetRecentPaymentInfo))
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// add bundled-widgets
|
|
203
|
+
catalogMapEntries.push(...localWidgetDiscovery(
|
|
204
|
+
bundledWidgetsSrcPath,
|
|
205
|
+
bundledWidgetModules,
|
|
206
|
+
allMetaModules,
|
|
207
|
+
false, // lazy: false
|
|
208
|
+
))
|
|
209
|
+
|
|
210
|
+
// Async widgets (dynamically loaded, like plugins)
|
|
211
|
+
catalogMapEntries.push(...localWidgetDiscovery(
|
|
212
|
+
asyncWidgetsSrcPath,
|
|
213
|
+
asyncWidgetModules,
|
|
214
|
+
allMetaModules,
|
|
215
|
+
true, // lazy: true
|
|
216
|
+
))
|
|
217
|
+
|
|
218
|
+
// Optional: Remote discovery of -pre-built widgets hosted on a CDN
|
|
219
|
+
/*const manifestUrl = getWidgetsManifestUrl()
|
|
220
|
+
if (manifestUrl.length > 0) {
|
|
221
|
+
const remoteResponse = await remoteWidgetDiscovery(manifestUrl)
|
|
222
|
+
if (!remoteResponse.message) {
|
|
223
|
+
catalogMapEntries.push(...(remoteResponse.entries || []))
|
|
224
|
+
}
|
|
225
|
+
}*/
|
|
226
|
+
|
|
227
|
+
return new Map(catalogMapEntries)
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### 3. Defining Dashboard Defaults
|
|
232
|
+
|
|
233
|
+
Use a `dashboard-defaults.ts` file to define initial layouts based on user roles.
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// file: src/dashboard-defaults.ts
|
|
237
|
+
import {
|
|
238
|
+
TDashboardWidgetKey,
|
|
239
|
+
IChildWidgetConfigEntry,
|
|
240
|
+
IDashboardConfig,
|
|
241
|
+
TDashboardWidgetCatalog,
|
|
242
|
+
} from '@tenorlab/vue-dashboard'
|
|
243
|
+
import { blankDashboardConfig, cssSettingsCatalog } from '@tenorlab/vue-dashboard/core'
|
|
244
|
+
import { getWidgetCatalog } from './widgets-catalog'
|
|
245
|
+
|
|
246
|
+
// reserved identifier to be used only for the default dashboard
|
|
247
|
+
const DEFAULT_DASHBOARD_ID = 'default' as const
|
|
248
|
+
const DEFAULT_DASHBOARD_NAME = 'Default' as const
|
|
249
|
+
|
|
250
|
+
const getDefaultDashboardForCustomerUser = (
|
|
251
|
+
user: any,
|
|
252
|
+
clientAppKey: string,
|
|
253
|
+
availableWidgetKeys: TDashboardWidgetKey[]
|
|
254
|
+
): IDashboardConfig => {
|
|
255
|
+
const userID = user.userID || 0
|
|
256
|
+
return {
|
|
257
|
+
userID,
|
|
258
|
+
clientAppKey,
|
|
259
|
+
dashboardId: DEFAULT_DASHBOARD_ID,
|
|
260
|
+
dashboardName: DEFAULT_DASHBOARD_NAME,
|
|
261
|
+
zoomScale: 1,
|
|
262
|
+
responsiveGrid: false,
|
|
263
|
+
widgets: ['WidgetContainer_container1'],
|
|
264
|
+
childWidgetsConfig: [
|
|
265
|
+
{ parentWidgetKey: 'WidgetContainer_container1', widgetKey: 'WidgetRecentPaymentInfo' }
|
|
266
|
+
],
|
|
267
|
+
cssSettings: [...cssSettingsCatalog]
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export const getDashboardDefaults = async (
|
|
272
|
+
user: any | null,
|
|
273
|
+
clientAppKey: string
|
|
274
|
+
): Promise<{
|
|
275
|
+
dashboardConfig: IDashboardConfig
|
|
276
|
+
widgetsCatalog: TDashboardWidgetCatalog
|
|
277
|
+
}> => {
|
|
278
|
+
const widgetsCatalog = await getWidgetCatalog(user)
|
|
279
|
+
|
|
280
|
+
if (!user) return { dashboardConfig: blankDashboardConfig, widgetsCatalog }
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
dashboardConfig: getDefaultDashboardForCustomerUser(user, clientAppKey, [...widgetsCatalog.keys()]),
|
|
284
|
+
widgetsCatalog
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### 4. Implementation Example: Read-Only Dashboard
|
|
290
|
+
|
|
291
|
+
Use this for a simplified, non-editable view of the dashboard.
|
|
292
|
+
|
|
293
|
+
TypeScript
|
|
294
|
+
|
|
295
|
+
```vue
|
|
296
|
+
<script setup lang="ts">
|
|
297
|
+
// file: src/views/DashboardReadonly.vue
|
|
298
|
+
import { reactive, watch, onMounted } from 'vue'
|
|
299
|
+
import {
|
|
300
|
+
IDashboardConfig,
|
|
301
|
+
TDashboardWidgetCatalog,
|
|
302
|
+
useDashboardStore,
|
|
303
|
+
} from '@tenorlab/vue-dashboard'
|
|
304
|
+
import {
|
|
305
|
+
blankDashboardConfig,
|
|
306
|
+
cssVarsUtils,
|
|
307
|
+
useDashboardStorageService,
|
|
308
|
+
} from '@tenorlab/vue-dashboard/core'
|
|
309
|
+
import { DynamicWidgetLoader, DashboardGrid } from '@tenorlab/vue-dashboard'
|
|
310
|
+
import { getDashboardDefaults } from '../dashboard-defaults'
|
|
311
|
+
|
|
312
|
+
const clientAppKey = 'myclientapp'
|
|
313
|
+
const user = { id: 1234 }
|
|
314
|
+
const userId = user.id
|
|
315
|
+
const dashboardStore = useDashboardStore()
|
|
316
|
+
const dashboardStorageService = useDashboardStorageService()
|
|
317
|
+
|
|
318
|
+
const {
|
|
319
|
+
isLoading: _,
|
|
320
|
+
isEditing,
|
|
321
|
+
currentDashboardConfig,
|
|
322
|
+
targetContainerKey,
|
|
323
|
+
} = dashboardStore.computed
|
|
324
|
+
|
|
325
|
+
type TState = {
|
|
326
|
+
defaultDashboardConfig: IDashboardConfig
|
|
327
|
+
widgetsCatalog: TDashboardWidgetCatalog
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const localState = reactive<TState>({
|
|
331
|
+
defaultDashboardConfig: blankDashboardConfig,
|
|
332
|
+
widgetsCatalog: new Map(),
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
const getDefaultDashboardConfig = (): IDashboardConfig => {
|
|
336
|
+
return localState.defaultDashboardConfig
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function _fetchDashboardConfig() {
|
|
340
|
+
const defaultConfig = getDefaultDashboardConfig()
|
|
341
|
+
const savedConfigs = await dashboardStorageService.getSavedDashboards(
|
|
342
|
+
userId,
|
|
343
|
+
clientAppKey,
|
|
344
|
+
localState.widgetsCatalog,
|
|
345
|
+
defaultConfig,
|
|
346
|
+
)
|
|
347
|
+
dashboardStore.setAllDashboardConfigs(savedConfigs)
|
|
348
|
+
// show default or first dashboard
|
|
349
|
+
const dashboardConfig =
|
|
350
|
+
savedConfigs.find((x) => x.dashboardId === 'default') || savedConfigs[0] || defaultConfig
|
|
351
|
+
dashboardStore.setCurrentDashboardConfig(dashboardConfig)
|
|
352
|
+
cssVarsUtils.restoreCssVarsFromSettings(dashboardConfig.cssSettings || [])
|
|
353
|
+
setTimeout(() => dashboardStore.setIsLoading(false), 250)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
onMounted(async () => {
|
|
357
|
+
const defaults = await getDashboardDefaults(user, clientAppKey)
|
|
358
|
+
localState.defaultDashboardConfig = defaults.dashboardConfig
|
|
359
|
+
localState.widgetsCatalog = defaults.widgetsCatalog
|
|
360
|
+
await _fetchDashboardConfig()
|
|
361
|
+
})
|
|
362
|
+
</script>
|
|
363
|
+
|
|
364
|
+
<template>
|
|
365
|
+
<div class="relative flex flex-col h-full">
|
|
366
|
+
<DashboardGrid
|
|
367
|
+
:isEditing="false"
|
|
368
|
+
:zoomScale="Number(currentDashboardConfig.zoomScale)"
|
|
369
|
+
:responsiveGrid="currentDashboardConfig.responsiveGrid"
|
|
370
|
+
>
|
|
371
|
+
<DynamicWidgetLoader
|
|
372
|
+
v-for="(widgetKey, index) in currentDashboardConfig.widgets"
|
|
373
|
+
:key="`${widgetKey}_${index}`"
|
|
374
|
+
:widgetKey="widgetKey"
|
|
375
|
+
:parentWidgetKey="undefined"
|
|
376
|
+
:targetContainerKey="targetContainerKey"
|
|
377
|
+
:index="index"
|
|
378
|
+
:maxIndex="currentDashboardConfig.widgets.length - 1"
|
|
379
|
+
:childWidgetsConfig="currentDashboardConfig.childWidgetsConfig"
|
|
380
|
+
:widgetCatalog="localState.widgetsCatalog"
|
|
381
|
+
:isEditing="isEditing"
|
|
382
|
+
:extraProps="dashboardContext"
|
|
383
|
+
@removeClick="() => {}"
|
|
384
|
+
@moveClick="() => {}"
|
|
385
|
+
@selectContainer="() => {}"
|
|
386
|
+
/>
|
|
387
|
+
</DashboardGrid>
|
|
388
|
+
</div>
|
|
389
|
+
</template>
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
#### 5. Full Editable Dashboard
|
|
394
|
+
|
|
395
|
+
For a complete example including **Undo/Redo**, **Zooming**, **Catalog Flyouts**, and **Multiple Dashboards**, please refer to the [Full Implementation Example](https://github.com/tenorlab/vue-dashboard-sample/blob/main/views/DashboardFullExample.vue).
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
------
|
|
400
|
+
|
|
401
|
+
## 🧩 Components & Services
|
|
31
402
|
|
|
403
|
+
### UI Components
|
|
32
404
|
|
|
33
|
-
|
|
405
|
+
- **`DashboardGrid`**: The main layout engine for positioning widgets.
|
|
406
|
+
- **`WidgetContainer`**: Wrapper providing common widget UI (headers, actions, loading states).
|
|
407
|
+
- **`WidgetsCatalogFlyout`**: A slide-out panel for users to browse and add new widgets.
|
|
408
|
+
- **`DynamicWidgetLoader`**: Lazy-loading utility for high-performance dashboards.
|
|
34
409
|
|
|
35
|
-
|
|
36
|
-
- **Configurable:** Allow end-users to add/remove widgets.
|
|
37
|
-
- **Vite Optimized:** Tree-shakeable and lightweight for modern build pipelines.
|
|
38
|
-
- **Themeable:** Easy integration with Tailwind CSS or CSS Variables.
|
|
410
|
+
### Hooks & State
|
|
39
411
|
|
|
412
|
+
- **`useDashboardStore`**: Access the underlying Zustand store to manage widget state, layout, and configuration.
|
|
413
|
+
- **`useDashboardUndoService`**: Provides `undo` and `redo` functionality for user layout changes.
|
|
40
414
|
|
|
415
|
+
------
|
|
41
416
|
|
|
42
|
-
## Licensing
|
|
417
|
+
## ⚖️ Licensing
|
|
43
418
|
|
|
44
419
|
This project is dual-licensed:
|
|
45
420
|
|
|
46
|
-
1. **Non-Commercial / Personal Use:** Licensed under the [Polyform Non-Commercial 1.0.0](https://polyformproject.org/licenses/non-commercial/1.0.0/). Free
|
|
421
|
+
1. **Non-Commercial / Personal Use:** Licensed under the [Polyform Non-Commercial 1.0.0](https://polyformproject.org/licenses/non-commercial/1.0.0/). Free for students, hobbyists, and open-source projects.
|
|
47
422
|
2. **Commercial Use:** Requires a **Tenorlab Commercial License**.
|
|
48
423
|
|
|
49
424
|
If you are using this library to build a revenue-generating product or within a commercial entity, please visit [https://payhip.com/b/gPBpo](https://payhip.com/b/gPBpo) to purchase a license.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tenorlab/vue-dashboard",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.91",
|
|
4
4
|
"description": "Foundation components for creating user-configurable dashboards in Vue",
|
|
5
5
|
"author": "Damiano Fusco",
|
|
6
6
|
"type": "module",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"main": "./dist/vue-dashboard.es.js",
|
|
48
48
|
"module": "./dist/vue-dashboard.es.js",
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@tenorlab/dashboard-core": "^1.
|
|
50
|
+
"@tenorlab/dashboard-core": "^1.5.1",
|
|
51
51
|
"@types/node": "^24.10.1",
|
|
52
52
|
"@vitejs/plugin-vue": "^6.0.3",
|
|
53
53
|
"prettier": "^3.7.4",
|