@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 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
- ### Part of the Built With JavaScript Ecosystem
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
- Install the package via npm or pnpm:
23
+ Bash
17
24
 
18
- ```bash
19
- npm install @tenorlab/vue-dashboard
20
- # or
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
- ## Basic Usage
25
- Import the styles in your entry tsx file (usually main.tsx):
26
- ```typescript
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
- TODO:
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
- ## Features
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
- - **Type-Safe:** Built with TypeScript for excellent IDE support and error catching.
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 to use for students, hobbyists, and open-source projects.
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.7",
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.4.2",
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",