npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tenorlab/vue-dashboard

v1.6.7

Published

Foundation components for creating user-configurable dashboards in Vue

Readme

@tenorlab/vue-dashboard

License: MIT Tenorlab Pro Framework: Vue

Foundation components for creating user-configurable, high-performance dashboards in Vue.

Relationship to Core

This package extends @tenorlab/dashboard-core. It provides the Vue implementation of the core logic, including specialized hooks, state management, and a suite of UI components.

Note: This package re-exports all types and utilities from @tenorlab/dashboard-core. You do not need to install the core package separately.

Tenorlab Pro Demos

  • React Demo (built with @tenorlab/react-dashboard)
  • Vue Demo (built with @tenorlab/vue-dashboard)
  • Nuxt Demo (built with @tenorlab/vue-dashboard)

✨ Features

  • Type-Safe: Deep integration with TypeScript 5.8+ for full IDE support.
  • State Management: Built-in useDashboardStore and useDashboardUndoService.
  • User Configurable: Ready-to-use components for adding, removing, and dragging widgets.
  • Themeable: Native support for CSS Variables and Tailwind CSS.
  • Vite Optimized: Full ESM support and tree-shakeable.

🚀 Quick Start

Installation

# with npm
npm i @tenorlab/vue-dashboard

# with pnpm
pnpm add @tenorlab/vue-dashboard

1. Global Styles

Import the base styles in your entry file (e.g., main.ts):

import '@tenorlab/vue-dashboard/styles.css'

🛠 Developer Guide

1. Creating a Widget

Widgets should be organized by their loading strategy.

  • Bundled Widgets: Place in src/bundled-widgets/ (loaded immediately).
  • Async Widgets: Place in src/async-widgets/ (lazy-loaded).

(NOTE: These directory names are suggestions; you can use different names, or put the widgets under src/components if you prefer)

Each widget requires a sub-directory using the widget-name-here convention.

Example: WidgetTotalOrders

Directory name widget-total-orders, files:

  • WidgetTotalOrders.vue
  • meta.ts
  • index.ts

File: src/bundled-widgets/widget-total-orders/WidgetTotalOrders.vue:

<script setup lang="ts">
import type { IDashboardWidgetProps, TWidgetEmits } from '@tenorlab/vue-dashboard'
import { DashboardWidgetBase, WrapperColumnContent, useWidgetEmits } from '@tenorlab/vue-dashboard'

const props = defineProps<IDashboardWidgetProps>()
const emits = defineEmits<TWidgetEmits>()
const emitHandlers = useWidgetEmits(emits)
</script>

<template>
  <DashboardWidgetBase v-bind="props" v-on="emitHandlers">
    <WrapperColumnContent><!-- WrapperColumnContent is optional, you are free to use a div or other html elements here -->
      <div class="dashboard-number number-xl text-primary">1,250</div>
      <div class="text-sm">Orders this month</div>
    </WrapperColumnContent>
  </DashboardWidgetBase>
</template>

File: src/bundled-widgets/widget-total-orders/meta.ts:

import type { TWidgetMetaInfo } from '@tenorlab/vue-dashboard'
import { markRaw } from 'vue'
import { ReceiptIcon as ComponentIcon } from 'lucide-vue-next'

// Define the metadata object for the plugin
export const WidgetTotalOrdersMeta: TWidgetMetaInfo = {
  name: 'Total Orders',
  categories: ['Widget'],
  icon: markRaw(ComponentIcon),
  noDuplicatedWidgets: true,
  description: 'Displays information about your total orders.',
  externalDependencies: [],
}

File: src/bundled-widgets/widget-total-orders/index.ts:

import WidgetTotalOrders from './WidgetTotalOrders.vue'
export default WidgetTotalOrders

2. Creating the Widgets Catalog

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

File: src/widgets-catalog.ts:

import { WidgetContainerColumn, WidgetContainerLarge, WidgetContainerRow } from '@tenorlab/vue-dashboard'
import { createStaticEntry, localWidgetDiscovery, remoteWidgetDiscovery } from '@tenorlab/vue-dashboard/core'
import { getWidgetsManifestUrl } from '@/utils/'

// optional: other static widgets to add manually
import {
  WidgetSmallCardSample,
} from './other-widgets/other-widgets'
// meta data map for other static widgets:
import { otherWidgetsMetaMap } from './other-widgets/other-widgets-meta'
import type {
  IDynamicWidgetCatalogEntry,
  TDashboardWidgetCatalog,
  TWidgetMetaInfoBase,
  TWidgetFactory,
  TDashboardWidgetKey,
  TWidgetMetaInfo,
} from '@tenorlab/vue-dashboard'

const bundledWidgetsSrcPath = '/src/bundled-widgets'
const asyncWidgetsSrcPath = '/src/async-widgets'

// Use Vite's Glob Import
// This creates an object where the keys are file paths, and the values are the TWidgetFactory functions.
// We target the 'index.ts' files within the widgets subdirectories.
type TGlobModuleMap = Record<string, TWidgetFactory>

// Eagerly loaded (Non-lazy / Bundled):
const bundledWidgetModules = import.meta.glob('/src/bundled-widgets/*/index.ts', {
  eager: true /* we load this immediately */,
}) as TGlobModuleMap

// Lazy loaded (Code-split / Plugins):
const asyncWidgetModules = import.meta.glob('/src/async-widgets/*/index.ts') as TGlobModuleMap

// Meta modules (Always eager so titles/icons are available immediately)
const allMetaModules = import.meta.glob('/src/**/widget-*/meta.ts', {
  eager: true,
}) as Record<string, Record<string, TWidgetMetaInfoBase>>

const hasPermission = (_user_: any, _permission: string) => true

/**
 * @name getWidgetCatalog
 * @description Dynamically builds the widgets catalog based on user type and operations/permissions.
 */
export const getWidgetCatalog = async (user: any | null): Promise<TDashboardWidgetCatalog> => {
  // A. Register Static Core Components
  const catalogMapEntries: [string, IDynamicWidgetCatalogEntry][] = [
    createStaticEntry('WidgetContainer', WidgetContainerColumn, otherWidgetsMetaMap['WidgetContainer']),
    createStaticEntry('WidgetContainerRow', WidgetContainerRow, otherWidgetsMetaMap['WidgetContainerRow']),
    createStaticEntry(
      'WidgetContainerLarge',
      WidgetContainerLarge,
      otherWidgetsMetaMap['WidgetContainerLarge'],
    ),
  ]

  // B. Optional: Register Business Static Widgets manually:
  // Example: you could filter further by permissions and user type if needed
  if (hasPermission(user, 'some-permission')) {
    // i.e.:
    // catalogMapEntries.push(
    //   createStaticEntry(
    //     'WidgetThatRequiresPermissions',
    //     WidgetThatRequiresPermissions,
    //     otherWidgetsMetaMap['WidgetThatRequiresPermissions'],
    //   ),
    // )
  }

  // C. Register widgets automatically with the localWidgetDiscovery helper:
  // (bundled widgets are included always, non-lazy)
  catalogMapEntries.push(
    ...localWidgetDiscovery(
      bundledWidgetsSrcPath,
      bundledWidgetModules,
      allMetaModules,
      false, // lazy: false
    ),
  )

  // D. Register "lazy" widgets automatically with the localWidgetDiscovery helper:
  // (async widgets are not incuded, they are lazy loaded at run time)
  catalogMapEntries.push(
    ...localWidgetDiscovery(
      asyncWidgetsSrcPath,
      asyncWidgetModules,
      allMetaModules,
      true, // lazy: true
    )
  )

  // E. Optional: Remote discovery of -pre-built widgets hosted on a CDN (requires advance importMaps setup and other configuration)
  /*const manifestUrl = getWidgetsManifestUrl()
  if (manifestUrl.length > 0) {
    const remoteResponse = await remoteWidgetDiscovery(manifestUrl)
    if (!remoteResponse.message) {
      catalogMapEntries.push(...(remoteResponse.entries || []))
    }
  }*/

  return new Map(catalogMapEntries)
}

3. Defining Dashboard Defaults

Use a dashboard-defaults.ts file to define initial layouts based on user roles.

File: src/dashboard-defaults.ts:

import { blankDashboardConfig, cssSettingsCatalog } from '@tenorlab/vue-dashboard/core'
import { getWidgetCatalog } from './widgets-catalog'
import type {
  TDashboardWidgetKey,
  IChildWidgetConfigEntry,
  IDashboardConfig,
  TDashboardWidgetCatalog,
} from '@tenorlab/vue-dashboard'

// reserved identifier to be used only for the default dashboard
const DEFAULT_DASHBOARD_ID = 'default' as const
const DEFAULT_DASHBOARD_NAME = 'Default' as const

// default dashboard config for Regular user type
const getDefaultDashboardForRegularUser = (
  user: any,
  clientAppKey: string,
  availableWidgetKeys: TDashboardWidgetKey[]
): IDashboardConfig => {
  const userID = user.userID || 0
  return {
    userID,
    clientAppKey,
    dashboardId: DEFAULT_DASHBOARD_ID,
    dashboardName: DEFAULT_DASHBOARD_NAME,
    zoomScale: 1,
    responsiveGrid: true,
    widgets: [
      'WidgetContainer_container1', // will contain other widgets specified in the childWidgetsConfig secitno below
      'WidgetBarGradients',
      ],
    childWidgetsConfig: [
      // two widgets go into container1:
      { 
        parentWidgetKey: 'WidgetContainer_container1', 
        widgetKey: 'WidgetTotalOrders' 
      },
      { 
        parentWidgetKey: 'WidgetContainer_container1', 
        widgetKey: 'WidgetTotalOrders' 
      }
    ],
    cssSettings: [...cssSettingsCatalog]
  }
}

export const getDashboardDefaults = async (
  user: any | null,
  clientAppKey: string
): Promise<{
  dashboardConfig: IDashboardConfig
  widgetsCatalog: TDashboardWidgetCatalog
}> => {
  const widgetsCatalog = await getWidgetCatalog(user)

  if (!user) {
    return {
      dashboardConfig: blankDashboardConfig,
      widgetsCatalog,
    }
  }

  return {
    // Optional, you could use different routines depending on user role:
    dashboardConfig: getDefaultDashboardForRegularUser(user, clientAppKey, [...widgetsCatalog.keys()]),
    widgetsCatalog
  }
}

4. Implementation Example: Read-Only Dashboard

Use this for a simplified, non-editable view of the dashboard.

File: src/views/DashboardReadonly.vue:

<script setup lang="ts">
import { reactive, shallowRef, watch, onMounted } from 'vue'
import {
  useDashboardStore,
} from '@tenorlab/vue-dashboard'
import {
  blankDashboardConfig,
  cssVarsUtils,
  useDashboardStorageService,
} from '@tenorlab/vue-dashboard/core'
import { DynamicWidgetLoader, DashboardGrid } from '@tenorlab/vue-dashboard'
import { getDashboardDefaults } from '../dashboard-defaults'
import type {
  IDashboardConfig,
  TDashboardWidgetCatalog,
} from '@tenorlab/vue-dashboard'

const clientAppKey = 'myclientapp'
const user = { id: 1234 }
const userId = user.id
const dashboardStore = useDashboardStore()
const dashboardStorageService = useDashboardStorageService()

const {
  isLoading: _,
  isEditing,
  currentDashboardConfig,
  targetContainerKey,
} = dashboardStore.computed

// 1. Standard reactive state for primitive data
const localState = reactive<TState>({
  defaultDashboardConfig: blankDashboardConfig,
})
// 2. Shallow ref for the Catalog (Zero deep-proxying)
const widgetsCatalog = shallowRef<TDashboardWidgetCatalog>(new Map())
const getWidgetCatalog = () => widgetsCatalog.value

const getDefaultDashboardConfig = (): IDashboardConfig => {
  return localState.defaultDashboardConfig
}

async function _fetchDashboardConfig() {
  const defaultConfig = getDefaultDashboardConfig()
  const savedConfigs = await dashboardStorageService.getSavedDashboards(
    userId,
    clientAppKey,
    getWidgetCatalog(),
    defaultConfig,
  )
  dashboardStore.setAllDashboardConfigs(savedConfigs)
  // show default dashboard or first dashboard
  const dashboardConfig =
    savedConfigs.find((x) => x.dashboardId === 'default') || savedConfigs[0] || defaultConfig
  dashboardStore.setCurrentDashboardConfig(dashboardConfig)
  cssVarsUtils.restoreCssVarsFromSettings(dashboardConfig.cssSettings || [])
  setTimeout(() => dashboardStore.setIsLoading(false), 250)
}

onMounted(async () => {
  const defaults = await getDashboardDefaults(user, clientAppKey)
  localState.defaultDashboardConfig = defaults.dashboardConfig
  widgetsCatalog.value = defaults.widgetsCatalog
  await _fetchDashboardConfig()
})
</script>

<template>
  <div class="relative flex flex-col h-full">
    <DashboardGrid
      :isEditing="false"
      :zoomScale="Number(currentDashboardConfig.zoomScale)"
      :responsiveGrid="currentDashboardConfig.responsiveGrid"
    >
      <DynamicWidgetLoader
        v-for="(widgetKey, index) in currentDashboardConfig.widgets"
        :key="`${widgetKey}_${index}`"
        :widgetKey="widgetKey"
        :parentWidgetKey="undefined"
        :targetContainerKey="targetContainerKey"
        :index="index"
        :maxIndex="currentDashboardConfig.widgets.length - 1"
        :childWidgetsConfig="currentDashboardConfig.childWidgetsConfig"
        :widgetCatalog="widgetsCatalog"
        :isEditing="isEditing"
        :extraProps="dashboardContext"
        @removeClick="() => {}"
        @moveClick="() => {}"
        @selectContainer="() => {}"
      />
    </DashboardGrid>
  </div>
</template>

5. Full Editable Dashboard

For editable dashboard examples, including Undo/Redo, Zooming, Catalog Flyouts, and Multiple Dashboards, please refer to Tenorlab Pro.


🧩 Components & Services

UI Components

  • DashboardGrid: The main dashboard layout that position widgets within a responsive grid.
  • WidgetContainer: A special "widget" that is a container for other widgets.
  • WidgetsCatalogFlyout: A slide-out panel for users to browse and add new widgets on editable dashboards.
  • DynamicWidgetLoader: The core lazy-loading widget loader that renders the widgets within the grid.

Hooks & State

  • useDashboardStore: Access the underlying reactive store to manage widget state, layout, and configuration.
  • useDashboardUndoService: Provides undo and redo functionality for user layout changes in editable dashboard (optional).

Links

Open source core packages

Tenorlab Pro Demos

  • React Demo (built with @tenorlab/react-dashboard)
  • Vue Demo (built with @tenorlab/vue-dashboard)
  • Nuxt Demo (built with @tenorlab/vue-dashboard)

Others


⚖️ Licensing & Usage

@tenorlab/vue-dashboard is MIT licensed.

It provides the foundational components and logic for building dashboards. You are free to use it in any project, personal or commercial.

⚡️ Go Pro and Save Time: Tenorlab Pro

A commercial license for a full-blown professional app code is available for purchase here and comes with:

  • Full Application Shell: A clean, optimized Vite + TypeScript project structure (with either React, Vue or Nuxt).
  • Dashboard Management: Production-ready logic for creating, listing, renaming, and deleting multiple user-defined dashboards.
  • Implementation Examples: Best patterns for both "Read-Only" (Analyst view) and "User-Editable" (Admin view) dashboard modes, a dynamic dashboard menu, etc.
  • Tenorlab Theme Engine: A sophisticated Tailwind-based system supporting multiple custom themes (not just Light/Dark mode).