@voicenter-team/voicenter-ui-plus

This is Vue 3.x + Typescript component library made for Voicenter systems.
37,359
Install
npm install @voicenter-team/voicenter-ui-plus
Latest Version:3.4.7
Vue:3
Last Updated:Oct 20, 2026
Links: npm
Maintainer: tzachish

Voicenter UI library for Vue

This is Vue 3.x + Typescript component library made for Voicenter systems.

v3.0.0 is a tree-shaking release with breaking changes. See MIGRATION_v3.md. The separate *-extended package is gone — every component lives in this package now.

Installation

npm i @voicenter-team/voicenter-ui-plus
npm i vue@^3.5 element-plus @vueuse/core dayjs lodash-es libphonenumber-js maska

Sass compiler (build-time)

This library customizes element-plus theming via SCSS variable injection at build time. Your bundler needs to be able to compile SCSS. If your project doesn't already use Sass, add it as a dev dependency:

npm i -D sass-embedded

This is a build-time only requirement — it doesn't add anything to your runtime bundle. Most Vue 3 / Vite projects already have it.

Optional peer deps — install only for the heavy components you use:

Component Peer deps
VcPdfDocument pdfjs-dist@^2.16.105
VcSoundPlayer wavesurfer.js
VcExcelFileUploader xlsx
VcImageUploader vue-advanced-cropper
VcHtmlEditor / VcJsonSchema codemirror @codemirror/lang-html @codemirror/lang-json @codemirror/lang-markdown vue-codemirror6
VcMdEditor @milkdown/crepe @milkdown/kit @milkdown/vue @prosemirror-adapter/vue

If you're on Vite 8 / rolldown (and skipping some optional peers)

Vite 8's rolldown-based bundler validates named imports against optional-peer-dep stubs and fails the build with MISSING_EXPORT errors when a heavy component's peer is uninstalled — even when you don't use that component. Add the unused optional peers to build.rollupOptions.external in your vite.config.ts:

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
    plugins: [ vue() ],
    build: {
        rollupOptions: {
            external: [
                // List only the heavy peers you DON'T install
                /^@milkdown\//, /^@prosemirror/, /^@codemirror\//, /^codemirror/,
                /^vue-codemirror6/, /^vue-advanced-cropper/,
                'wavesurfer.js', 'xlsx', 'pdfjs-dist', /^pdfjs-dist\//
            ]
        }
    }
})

Earlier Vite versions (5, 6, 7 with rollup) and webpack 5+ don't require this — the older rollup variant warns but doesn't error on optional-peer named imports.

Usage

Tree-shake mode (recommended)

import { VcButton, VcInput } from '@voicenter-team/voicenter-ui-plus'
// Use as local components — no app.use() needed.

Bundlers drop unused components and their CSS. A consumer importing only VcButton ships ~50–150 KB JS + ~10–30 KB CSS.

If you use useConfirmModal, useConfirmPopup, or NotifyService, mount global overlays in App.vue:

<script setup lang="ts">
import { VcPluginOverlays } from '@voicenter-team/voicenter-ui-plus'
</script>

<template>
  <RouterView />
  <VcPluginOverlays />
</template>

Plugin mode (global registration)

import VoicenterUI from '@voicenter-team/voicenter-ui-plus/plugin'
import '@voicenter-team/voicenter-ui-plus/style.css'

// Overlay composables can be imported from the same entry:
// import { useConfirmModal, useConfirmPopup, VcPluginOverlays } from '@voicenter-team/voicenter-ui-plus/plugin'

app.use(VoicenterUI, {
    themeConfig,             // theme config (see below)
    lang: 'en',              // localization
    injectIconFont: true     // inject icon-font <link> tags into <head>
})
// Confirm modals/popovers/notifications: auto-mounted via VcPluginOverlays
// (mountOverlays: true by default). Opt out with mountOverlays: false and
// add <VcPluginOverlays /> to App.vue — also registered globally by the plugin.

Global overlays

Several APIs are state-only — they update global reactive singletons but do not render UI by themselves:

API Overlay component
useConfirmModal VcConfirmModal
useConfirmPopup VcConfirmPopover
NotifyService / $notify VcNotification

Plugin mode: mounted automatically on app.use() (default). Pass mountOverlays: false to mount manually.

Notifications: one top-right host by default. For multiple positions, use overlayNotifications in plugin config or mount your own:

app.use(VoicenterUI, {
    overlayNotifications: [
        { group: 'top-right', position: 'top-right' },
        { group: 'bottom-right', position: 'bottom-right' },
    ],
})
// NotifyService.add({ group: 'bottom-right', ... })

Or overlayNotifications: 'none' and put <VcNotification /> in App.vue (confirms still auto-mount).

Tree-shake mode: add <VcPluginOverlays /> to App.vue (bundles confirm hosts; configure via notifications prop).

Entity components such as VcEntityListTable call useConfirmModal internally — delete confirmations require overlays to be mounted.

Tailwind

Tailwind's colours come from the library scheme, so the utility classes and the theme are one set of tokens. theme.colors replaces Tailwind's own palette on purpose: only the library's colours exist as classes.

// tailwind.config.js
const voiceTailwindScheme = require('@voicenter-team/voicenter-ui-plus/src/theme/tailwindScheme')

module.exports = {
  ...yourConfiguration,
  theme: {
    colors: { ...voiceTailwindScheme },
    borderColor: { ...voiceTailwindScheme },
    fill: voiceTailwindScheme
  }
}

Import the application's own stylesheet after the library, so the application's utilities win ties of equal specificity with the library's CSS (.hidden is the usual one).

Theming specification

The themeConfig which is passed as options to the VoicenterUI vue.use data could have the following specifications:

Local

Config:

{
    type: 'local'
    themeName: 'red' | 'blue' | '...'
    onSetupCallback: () => {
        console.log('Loaded!')
    }
}

Description:

Such configuration will append to the document element the CSS variables from one of the configuration passed in themeName parameter which it will get from theme specification delivered alongside with library

Params:

  • themeName
    • The name of the theme to be injected
  • onSetupCallback
    • The function which will be called right after the variables will be injected into the document element. First parameter of which is the theme object which was set up

Remote

Config:

{
    type: 'remote'
    brandingSectionName: 'someName'
    apiUrl: 'someUrl'
    brandingSectionName: {
        property: 'data'
    }
    onSetupCallback: () => {
        console.log('Loaded!')
    }
}

Description:

Such configuration will initiate the call to the specified apiUrl in order to retrieve the JSON object with key-value properties of the CSS variables to inject to document element

Params:

  • brandingSectionName
    • [Optional] The name of the section (tagSection) in the database configuration which will be passed to the API in payload and on response retrieved replaced from resulted values
  • apiUrl
    • The API url to which the POST request will be initiated
  • onSetupCallback
    • The function which will be called right after the variables will be injected into the document element. First parameter of which is the theme object which was set up
  • brandingSectionName
    • [Optional] The payload data that will be sent to the request

Custom JSON

Config:

{
    type: 'customJson'
    config: {
        black: '#000000'
    }
    onSetupCallback: () => {
      console.log('Loaded!')
    }
}

Description:

Such configuration will initiate the styling with custom provided theme

Params:

  • config
    • The object where the key is the variable to be set and the value is the variable value. Check the themes object to check existing variables
  • onSetupCallback
    • The function which will be called right after the variables will be injected into the document element. First parameter of which is the theme object which was set up

None

{ type: 'none', onSetupCallback?: (theme) => void } injects no theme variables; the application sets them itself.

Using ui-plus in an application

How an application builds its UI with ui-plus. Agents working in a consuming project read this section before any UI work; it describes the version installed next to it.

Where the facts are

The installed package is the truth. Read it before saying what ui-plus has or how a component behaves.

  • Every component: library/types/components/exports.d.ts. One component's props, events and slots: library/types/components/<VcName>/<VcName>.vue.d.ts.
  • Composables, services, directives, the entity base: library/types/composables/, library/types/services/, library/types/directives/, library/types/entity/.
  • Types: prop, event and configuration types are exported from the package root (import type { UIConfig } from '@voicenter-team/voicenter-ui-plus'). Generic components take type parameters in the template: <VcSelect<User, 'id', 'name'> v-model="selected" :options="users" value-key="id" label-key="name" />.
  • Colour tokens: src/theme/themes.json (each key is the CSS variable --<key>) and src/theme/tailwindScheme.js (the Tailwind colour names).
  • Examples: the live documentation.

Choosing a component

  • Interaction components come from ui-plus: buttons, inputs, selects, date pickers, checkboxes, tabs, tables, modals, drawers, tooltips, popovers, uploaders, notifications, confirmations. Search exports.d.ts by what the user is shown (a picker of people, an on/off switch, a list with a search) before concluding a component is missing.
  • A component is used as the library draws it: through its props, slots and variants. Fitting it into the layout is free: width, spacing, wrapping, alignment.
  • A custom control needs the project owner's explicit yes, asked before it is written: which element is needed, what was searched in ui-plus, why nothing fits. Custom means any of:
    • a new interaction component;
    • a ui-plus component whose own CSS variables (--el-*, the ones ui-plus sets on .vc-*) or internal classes are overridden to change how it looks or behaves as a control: its track, rows, selected, hover or active state, shape or indicator;
    • a ui-plus component used for a role it is not built for, such as a segmented switcher drawn as a searchable list.
  • Until that yes, the closest ui-plus component is used as drawn, and the difference from the design is the first open point of the report: what the design draws, which component was used, how they differ. No agent or orchestrator gives this approval in the owner's place.
  • A report names a control by what is on screen: a restyled ui-plus component is reported as custom.
  • A design hand-off (Figma, mock-up, screenshot, a specification) says what the user should see. It never justifies a restyle or a custom control.

Colours

  • Every colour is a ui-plus token: a Tailwind class from the library scheme (text-default-text, text-secondary-text, text-primary, bg-main-bg, bg-light-bg, border-ui-lines, text-success, text-error) or a theme variable in a style (color: var(--default-text)). Status colours, tags, icons and chart series included.
  • Hex values, rgb()/rgba(), arbitrary classes such as bg-[#…] and new colour variables stay out of application code. Such colours already in a project are not a precedent.
  • The theme configuration (themeConfig, usually a colors.json) sets the library theme. It is not a palette to add colours to.

Notifications and confirmations

NotifyService, useConfirmModal and useConfirmPopup render only through the hosts described in Global overlays, mounted once in the application. A VcNotification host shows only the messages of its own group, so where the application mounts one host per group, every notification names one of those groups.

Text direction

ui-plus follows the dir attribute of <html>: useRTL() reads it and updates when it changes. The application sets the direction there, once.

Server-side rendering

ui-plus is built and tested for client-rendered Vue applications. Its composables read document and window when they are called (useRTL among them), and the package ships no Nuxt module. An application with server-side rendering proves one page that uses ui-plus in development and in a production build before relying on it, and reports what fails.

Troubleshooting

  • Component not found or a blank page: in tree-shake mode a component is a local import, never a globally registered tag. In plugin mode both app.use(VoicenterUI, …) and import '@voicenter-team/voicenter-ui-plus/style.css' are present.
  • Cannot find module 'pdfjs-dist' (or another optional peer) at build time: install the peer, or stop importing its component.
  • Duplicate styles, a huge CSS payload: the two modes are mixed (named imports plus the global style.css). Pick one (MIGRATION_v3.md, §1).
  • PDF.js worker not loading: the worker is a sibling asset resolved with new URL('./pdf.worker.min.js', import.meta.url); the bundler must support that pattern (Vite, webpack 5+, Rollup and esbuild do).

Date Handling

This library uses dayjs for all date operations and formatting. When working with date-related components, use dayjs format tokens (NOT date-fns format tokens).

Format Tokens

Important: Use dayjs format tokens:

  • DD - Day of month (2 digits) - NOT dd
  • YYYY - 4-digit year - NOT yyyy
  • MM - Month (2 digits)
  • HH - 24-hour format
  • mm - Minutes
  • ss - Seconds

Common Formats

'DD/MM/YYYY'           // 31/12/2024
'DD-MM-YYYY'           // 31-12-2024
'YYYY-MM-DD'           // 2024-12-31
'DD/MM/YYYY, HH:mm'    // 31/12/2024, 14:30
'DD/MM/YYYY HH:mm:ss'  // 31/12/2024 14:30:45

Usage Example

<VcDatePicker
  v-model="date"
  format="DD/MM/YYYY"
  input-date-format="DD/MM/YYYY"
  input-time-format="HH:mm"
/>

Date Utility Functions

The library provides a comprehensive set of date utility functions built on dayjs. These utilities are available for use in external projects.

Import Date Utilities

import {
  formatDate,
  parseDate,
  addDays,
  subtractDays,
  getHours,
  getMinutes,
  getSeconds,
  setTime,
  isAfter,
  isSameAfter,
  isBefore,
  isSameBefore,
  format,
  add,
  sub,
  addFp,
  subFp
} from '@voicenter-team/voicenter-ui-plus'

Basic Date Operations

Formatting Dates
// Format a date with default format (YYYY-MM-DD)
const formatted = formatDate(new Date())
// Result: "2024-12-31"

// Format with custom format
const custom = formatDate(new Date(), 'DD/MM/YYYY')
// Result: "31/12/2024"

// Using format function (alias)
const formatted2 = format(new Date(), 'DD/MM/YYYY HH:mm')
// Result: "31/12/2024 14:30"
Parsing Dates
// Parse a date string with format
const date = parseDate('31/12/2024', 'DD/MM/YYYY')
// Returns: Date object

Date Arithmetic

Adding/Subtracting Days
// Add days
const futureDate = addDays(new Date(), 7)
// Returns: Date 7 days from now

// Subtract days
const pastDate = subtractDays(new Date(), 5)
// Returns: Date 5 days ago
Adding/Subtracting Time Units
// Add multiple time units
const newDate = add(new Date(), {
  years: 1,
  months: 2,
  days: 5,
  hours: 3,
  minutes: 30
})

// Subtract multiple time units
const earlierDate = sub(new Date(), {
  weeks: 2,
  days: 3,
  hours: 12
})

// Functional programming style (reversed arguments)
const fpDate = addFp({ days: 7 }, new Date())
const fpDate2 = subFp({ hours: 5 }, new Date())

Time Operations

Getting Time Components
const date = new Date('2024-12-31T14:30:45')

const hours = getHours(date)    // 14
const minutes = getMinutes(date) // 30
const seconds = getSeconds(date) // 45
Setting Time
// Set time on a date (returns new Date object)
const newDate = setTime(new Date(), {
  hours: 14,
  minutes: 30,
  seconds: 0
})

Date Comparisons

const date1 = new Date('2024-12-31')
const date2 = new Date('2025-01-01')

// Check if date1 is after date2
const isLater = isAfter(date1, date2, 'day')
// Returns: false

// Check if date1 is same or after date2
const isSameOrLater = isSameAfter(date1, date2, 'day')
// Returns: false

// Check if date1 is before date2
const isEarlier = isBefore(date1, date2, 'day')
// Returns: true

// Check if date1 is same or before date2
const isSameOrEarlier = isSameBefore(date1, date2, 'day')
// Returns: true

Comparison Units: The comparison functions accept a units parameter (default: 'minute'). Valid units:

  • 'year', 'month', 'week', 'day'
  • 'hour', 'minute', 'second'

Complete Usage Example

import {
  formatDate,
  addDays,
  isAfter,
  format,
  add,
  getHours,
  setTime
} from '@voicenter-team/voicenter-ui-plus'

// Format today's date
const today = formatDate(new Date(), 'DD/MM/YYYY')

// Get date 30 days from now
const futureDate = addDays(new Date(), 30)

// Check if a date is in the future
const isFuture = isAfter(futureDate, new Date(), 'day')

// Add 2 weeks and 3 days
const deadline = add(new Date(), {
  weeks: 2,
  days: 3
})

// Get current hour
const currentHour = getHours(new Date())

// Set specific time
const meetingTime = setTime(new Date(), {
  hours: 14,
  minutes: 30
})

// Format the result
const formattedDeadline = format(deadline, 'DD/MM/YYYY HH:mm')

Notes

  • All functions accept Date, string, or number as date input
  • All date manipulation functions return new Date objects (immutable)
  • Format strings use dayjs format tokens (see Format Tokens section above)
  • These utilities are built on dayjs and maintain compatibility with dayjs format patterns

Contributing: adding a new component

The public component surface is defined in one place: script/components-manifest.mjs — one line per component.

{ name: 'VcMyThing', path: 'components/VcMyThing/VcMyThing.vue' },

npm run generate:entries materializes the manifest into three fully generated files (never edit them by hand — the next generator run overwrites them, and the verifier fails the build until they match):

  • src/components.gen.ts — named exports, re-exported by src/index.ts (tree-shake mode);
  • src/plugin-components.gen.ts — registerComponents(app), called by the plugin's install (global registration mode);
  • src/components/exports.ts — the GlobalComponents template typing.

src/index.ts and src/plugin.ts themselves stay hand-written and small. The .gen modules are safe to route through only because rewriteCssImports() in vite.config.ts strips the CSS imports that vite-plugin-lib-inject-css aggregates onto them — each component's CSS stays in its own chunk, so per-component CSS tree-shaking survives. Don't rename the .gen files without updating that strip list (the verifier guards this wiring too).

Typical flows:

  • New component: node script/create-component.mjs — scaffolds the component folder, adds the manifest line, and regenerates all entries.
  • Existing .vue file: add one line to the manifest, then npm run generate:entries.

Every manifest entry is exported from the package root AND registered globally in plugin mode — there are no partial modes.

npm run verify:entries is the safety net: it independently cross-checks the manifest, the three generated files and the filesystem (a component file that exists but is missing from the manifest fails too). It runs in the pre-commit hook (together with lint-staged and ts-check; hooks are installed automatically by npm install via husky) and in CI, so an inconsistent component surface cannot reach master.

Documentation

Live Documentation

Visit documentation for interactive component examples and demos.

Project Documentation

For detailed technical documentation, see the docs/ directory:

Related Packages

@voicenter-team/eslint-config-vue

Shared vue eslint config

12,953
@voicenter-team/umbraco-headless-nuxt

Nuxt module that helps you to easily load your Umbraco data directly into your g...

6,834
@voicenter-team/stream-vue-3

A Vue 3 component for cloudflare streaming

4,600
@voicenter-team/umbraco-headless

Nuxt module that helps you to easily load your Umbraco data directly into your g...

925
@voicenter-team/nuxt-llms-generator

Nuxt 3/4 module that renders AI-optimized documentation files (llms.txt, llms-fu...

6,444

Version History

Version Released License
3.4.7
3.4.6
3.4.5
3.4.4
3.4.3