Package Exports
- @vibrant-wellness/va-responsive-components-library
- @vibrant-wellness/va-responsive-components-library/style.css
Readme
VA Responsive Components Library
A comprehensive Vue 3 component library built with modern development practices, providing reusable UI components, composables, and directives for building robust web applications.
🚀 Features
- 32 Production-Ready Components - Form controls, layout, navigation, feedback, data-display, and utility components
- 3 Powerful Composables - Reusable logic for common patterns
- 2 Custom Directives - Loading states and UI enhancements
- Vue 3 Composition API - Modern, performant, and type-safe
- Fully Responsive UI & UX - Components automatically adapt to all screen sizes with optimized mobile-first design and touch-friendly interactions
- Accessibility - ARIA attributes and keyboard navigation support
- Customizable Themes - Multiple built-in color schemes
Newly released public components:
FileUploader,DropdownMenu,Badge,Tab,FloatingActionButton,DynamicColorResponsiveButton,FoldableButton,Snackbar,Notification,Breadcrumb.
🆕 What's New
2026-05-08
Tab— addedequal_widthprop. Whentrue, the tab bar fills 100% of its container and tabs share that width equally (flex: 1 1 0; long labels ellipsis). For full-width header bars with no trailing space and no separate right-side tab. Ignored forverticaland whenindie_tabis set. Overridestab_widthwhen both are passed.
2026-05-07 (v0.0.2)
ResponsiveButton— addedis_activeprop: renders with disabled-like grey styling whenfalse, but stays fully clickable and continues to emitclick-button.- Package renamed to
@vibrant-wellness/va-responsive-components-library. Update yourpackage.jsonand imports to use the new scoped name. FoldableButton— refined padding/layout for tighter alignment.Dialog— addedheaderIconWidthandheaderIconHeightprops to control the header icon container size (default'24px').AreaCodePhoneInput—focusstate now only tracks the phone number input, not the country selector.- Documentation now covers
Tooltip,Pagination,InlineNotification,Dialog,StepAccordion,ProgressIndicator, andRepeater(all previously registered but undocumented).
2026-05-01
Tab— responsive sizing props added. Tab can now adapt to any container width with fine-grained control:width— total group width (Number → px, or any CSS length string)tab_width— fixed per-tab width (e.g. EHR ez-bill main-tab135px)font_size,icon_size— independent label/icon scalinggap,padding— per-tab spacing for compact / spacious presetsgroup_padding— horizontal padding on the group container itself (overrideoutlined/filleddefault0 24px, e.g. set0so tabs span the full configuredwidth)overflow_mode—'scroll'(default, hidden horizontal scroll) or'ellipsis'(tabs shrink, labels truncate in place)- Default: tabs keep their natural content width and the main row scrolls horizontally if content exceeds the container; labels never get aggressively cropped unless explicitly opted in.
2026-04-23
- Public release of:
FileUploader— fully-controlled file uploader with drag-and-drop, a 7-state row status machine (default / uploading / pending-success / pending-failure / failed / hide / hidden), and parent-driven progress. Follows the Figma File Uploader design.
2026-04-22
- Public release of:
Breadcrumb
2025-10-28
- Public release of:
DropdownMenuBadgeFloatingActionButtonDynamicColorResponsiveButton
Import options:
- Global (plugin):
app.use(VaResponsiveComponentsLibrary)then use components directly in templates.
- Named import:
import { DropdownMenu, Badge, FloatingActionButton, DynamicColorResponsiveButton, FoldableButton } from '@vibrant-wellness/va-responsive-components-library'
📦 Installation
npm install @vibrant-wellness/va-responsive-components-libraryor
npm install @vibrant-wellness/va-responsive-components-library@latest🎯 Quick Start
1. Install and Setup
First, import and register the library in your main.js:
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import VaResponsiveComponentsLibrary from '@vibrant-wellness/va-responsive-components-library'
import '@vibrant-wellness/va-responsive-components-library/style.css'
const app = createApp(App)
app.use(VaResponsiveComponentsLibrary)
app.mount('#app')2. Use Components
<template>
<div>
<!-- Basic checkbox -->
<Checkbox v-model="isChecked" label="Accept terms" />
<!-- Phone input with country selection -->
<AreaCodePhoneInput v-model="phoneData" />
<!-- Dynamic color responsive button (Recommended) -->
<DynamicColorResponsiveButton
display_name="Click me"
button_type="filled"
built_in_theme="primary"
@click-button="handleClick"
/>
<!-- Legacy button (Deprecated but still works; not maintained). New projects should migrate to DynamicColorResponsiveButton. -->
<ResponsiveButton @click="handleClick">
Click me
</ResponsiveButton>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isChecked = ref(false)
const phoneData = ref({ area_code: '', phone_number: '' })
const handleClick = () => {
console.log('Button clicked!')
}
</script>📚 Components
Form Components
AreaCodePhoneInput
International phone number input with country selection and area code handling. Features automatic country flag display, smart focus management, and a clear button that appears on hover/focus.
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| v-model / modelValue | binding value - object containing area_code (string) and phone_number (string) |
{area_code: string, phone_number: string} |
() => ({}) |
| country_options | custom country list with country_name and country_code properties (country_code is 2-byte uppercase, e.g. 'US') |
Array<{country_name: string, country_code: string}> |
() => [] |
| enable_backup_country_options | use built-in country list when no custom options provided | boolean |
true |
| country_filterable | enable country search/filtering in dropdown | boolean |
true |
| disabled | whether input is disabled | boolean |
false |
| size | size of the country selector and phone input | 'small' | 'medium' | 'large' |
'medium' |
| clearable | show clear button on the phone input when it has a value | boolean |
true |
Events
| Event | Description | Parameters |
|---|---|---|
| change | triggers when the binding value changes | {area_code: string, phone_number: string} |
| focus | triggers when any part of the input gains focus | {area_code: string, phone_number: string} |
| blur | triggers when the entire input loses focus | {area_code: string, phone_number: string} |
| clear | triggers when clear button is clicked | {area_code: string, phone_number: string} |
Exposes
| Method | Description | Type |
|---|---|---|
| focus | focus the appropriate input (country selector if no country selected, phone input if country selected) | () => void |
| blur | blur both country selector and phone input | () => void |
| clear | clear both country selection and phone number | () => void |
| alert | show alert message below the input | (message: string) => void |
| error | show error message below the input | (message: string) => void |
| removeAlertOrErrorEffect | clear alert/error state and message | () => void |
<template>
<AreaCodePhoneInput
v-model="phoneData"
:country_filterable="true"
@change="handlePhoneChange"
/>
</template>Navigation Components
Breadcrumb
Navigation breadcrumb component that displays a hierarchical path of items. Each breadcrumb item is clickable except for the current (last) item. Features separator chevrons between items and distinct styling for the current location.
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| stack | breadcrumb items array - each item should have id and name properties |
Array<{id: string | number, name: string}> |
[] |
Events
| Event | Description | Parameters |
|---|---|---|
| item-click | triggers when a breadcrumb item is clicked | (item: {id: string | number, name: string}, index: number) |
Usage
<template>
<Breadcrumb
:stack="breadcrumbStack"
@item-click="handleBreadcrumbClick"
/>
</template>
<script setup>
import { ref } from 'vue'
const breadcrumbStack = ref([
{ id: 'home', name: 'Home' },
{ id: 'products', name: 'Products' },
{ id: 'electronics', name: 'Electronics' },
{ id: 'current', name: 'Current Page' }
])
const handleBreadcrumbClick = (item, index) => {
console.log(`Clicked: ${item.name} at index ${index}`)
// Navigate to the clicked item
}
</script>Theme/Behavior Notes
Visual Styling:
- Non-current items use
var(--IconGrey)text color and are clickable with pointer cursor - Current item (last item in stack) uses
var(--VibrantDarkBlue)text color with underline decoration - Typography class:
EHR_BodyM_14_Web(14px medium weight body text) - Separator chevrons between items with gray fill (
#999999)
Interaction Rules:
- Only non-current items emit
item-clickevents - Current item is visually distinct but does not emit click events
- Each item must have a unique
idand a displayname
Tab
Tab group component with 7 type variants: outlined, filled, underline, button, vertical, capsule, and oval. Supports optional icons, badge indicators (dot, number, 99+ overflow, text pill), an independent separated tab, and v-model selection.
Icons must be extracted to standalone SVG files. This component library never inlines
<svg>markup in templates — every icon used in atab.icon(orindie_tab.icon) field is a separate.svgasset that youimportand pass as a string path. Don't paste raw SVG markup into theiconfield. If you don't yet have the icon as a file, save the SVG to@/assets/icons/<name>.svgfirst, thenimport iconX from '@/assets/icons/<name>.svg'and referenceiconX. This keeps the icon set auditable, lets the build cache & deduplicate assets, and is required for the auto dark-bg invert (filter: brightness(0) invert(1)) to work uniformly across variants. To bypass the inversion or render custom markup, use the#icon-{index}/#icon-indieslots — that is the only sanctioned place for inline SVG.Icon color on dark backgrounds: for variants with a dark selected background (
filledselected,button,capsuleselected,verticalselected, andovalwithcolor="vibrant-dark"selected), Tab automatically inverts theiconimage to white viafilter: brightness(0) invert(1). If you're passing a pre-colored or already-white icon, use the#icon-{index}slot to render your own markup and bypass this filter.
Default selection: Tab does not auto-select the first tab on mount. The selected tab is whichever one has
value === modelValue; ifmodelValueis''(the default) or doesn't match anytab.value, no tab is highlighted. Initialize yourv-modelref to the desired tab'svalue(e.g.const selected = ref('tab1')) to pre-select on mount.
Badge clearing: badges are purely data-driven. Clicking a tab does not clear its red dot / content badge automatically — if you want "click to mark as read" behavior, mutate the
tabsarray (or theindie_tabobject) yourself in the@changehandler.
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| v-model / modelValue | currently selected tab value. Must equal a tab.value for that tab to render as selected — no auto-selection on mount. Default '' means nothing is highlighted. |
string | number |
'' |
| tabs | array of tab objects — each with value (required), label (required), icon? (string or false), badge? (true for dot, number for circle, string for pill, or object for full Badge props). Badges are data-driven: clicking a tab does not clear its badge; mutate the array yourself in @change if you want "click to mark as read". |
Array |
— (required) |
| type | tab type variant | 'outlined' | 'filled' | 'underline' | 'button' | 'vertical' | 'capsule' | 'oval' |
'underline' |
| color | color theme for capsule and oval types | 'sky' | 'vibrant-dark' |
'sky' |
| width | total group width. Number → px, String accepts any CSS length ('50%', '32rem', '400px'). Omit to inherit parent (100%). |
string | number |
'' |
| tab_width | fixed width applied to each tab. For horizontal types uses flex: 0 0 <w>; for vertical sets width / min-width directly (column flexbox would otherwise size height). Long labels ellipsis. Useful for grid-like layouts (e.g. EHR ez-bill main-tab 135px). |
string | number |
'' |
| font_size | label font size. Number → px. When set, also relaxes line-height to 1.5 so labels don't clip when the font exceeds a per-type hard-coded line-height (e.g. underline's 20px). Leave empty to keep the variant's default line-height. |
string | number |
'' |
| icon_size | icon box + SVG placeholder size. Number → px. Lets icons scale independently from font_size. |
string | number |
'' |
| gap | gap between tabs inside the main group. Overrides per-variant default (outlined / filled default 8px). |
string | number |
'' |
| padding | horizontal padding on each tab (padding-inline). Vertical padding from CSS is preserved. |
string | number |
'' |
| group_padding | horizontal padding on the group container itself (padding-inline). outlined and filled types add 0 24px by default; set this to 0 so tabs span the full configured width. Number → px; string accepts any CSS length. |
string | number |
'' |
| equal_width | when true, the tab bar fills 100% of its container and tabs share that width equally (flex: 1 1 0; long labels ellipsis). For full-width header bars with no separate right-side tab. Ignored for vertical and when indie_tab is set. Overrides tab_width when both are passed. |
boolean |
false |
| overflow_mode | how to handle content exceeding the group width: 'scroll' (default, hidden horizontal scroll) or 'ellipsis' (tabs shrink in place, labels truncate). |
'scroll' | 'ellipsis' |
'scroll' |
| indie_tab | separated independent tab: { value, label, icon?, badge? }. Positioning: underline → right-aligned (margin-left: auto); vertical → pinned to bottom (margin-top: auto, requires sized parent); all other types render immediately after the main group. |
object |
null |
Events
| Event | Description | Parameters |
|---|---|---|
| update:modelValue | syncs v-model when tab selection changes | (value: string | number) |
| change | emitted when a different tab is selected | (value: string | number) |
Slots
| Name | Description |
|---|---|
| #icon-{index} | Custom icon content for tab at given index. Receives { tab, selected } |
| #icon-indie | Custom icon for the indie tab |
Usage
Each of the 7 type variants below has two example sets: "Dot badge" (boolean badge: true) and "Badge with content" (number / string / object).
Outlined (Large Page Tab)
The icon field on each tab accepts an imported SVG/PNG path (or false to hide the icon). Tab automatically inverts dark-background icons to white where appropriate.
<template>
<!-- Dot badge -->
<Tab v-model="selected" type="outlined" :tabs="primaryTabs" :indie_tab="primaryIndie" />
<!-- Badge with content: dot, number, 99+, text pill -->
<Tab v-model="selected2" type="outlined" :tabs="badgeTabs" :indie_tab="badgeIndie" />
</template>
<script setup>
import { ref } from 'vue';
import { Tab } from '@vibrant-wellness/va-responsive-components-library';
// Icons — any SVG/PNG path; pass `false` on a tab to hide its icon
import iconVP from '@/assets/icons/vibrant-products.svg';
import iconSP from '@/assets/icons/signature-programs.svg';
import iconPP from '@/assets/icons/practice-products.svg';
import iconPB from '@/assets/icons/product-bundles.svg';
import iconCF from '@/assets/icons/customized-fee.svg';
import iconEZ from '@/assets/icons/ez-bill.svg';
import iconDT from '@/assets/icons/document-template.svg';
import iconAU from '@/assets/icons/automation.svg';
import iconTM from '@/assets/icons/template-marketplace.svg';
import iconAR from '@/assets/icons/archive.svg';
import iconMS from '@/assets/icons/messages.svg';
import iconIN from '@/assets/icons/inbox.svg';
const selected = ref('tab1');
const selected2 = ref('tab1');
const primaryTabs = [
{ value: 'tab1', label: 'Option 1', icon: iconVP, badge: true },
{ value: 'tab2', label: 'Option 2', icon: iconSP, badge: true },
{ value: 'tab3', label: 'Option 3', icon: iconPP, badge: true },
{ value: 'tab4', label: 'Option 4', icon: iconPB, badge: true },
{ value: 'tab5', label: 'Option 5', icon: iconCF, badge: true },
];
const primaryIndie = { value: 'indie', label: 'Option 6', icon: iconEZ, badge: true };
const badgeTabs = [
{ value: 'tab1', label: 'Messages', icon: iconMS, badge: true },
{ value: 'tab2', label: 'Inbox', icon: iconIN, badge: 3 },
{ value: 'tab3', label: 'Alerts', icon: iconAU, badge: 12 },
{ value: 'tab4', label: 'Pending', icon: iconTM, badge: 100 },
{ value: 'tab5', label: 'Spam', icon: iconCF, badge: 'New' },
];
const badgeIndie = { value: 'tab6', label: 'Archive', icon: iconAR };
</script>The
Filled,Underline,Button,Vertical,Capsule, andOvalexamples below are self-contained — each one imports only the icons it uses and defines its owntabs/indie_tab. Only thetype(and optionallycolor, sizing props) changes per variant.
Filled (Large Page Tab)
Selected tab uses a dark-blue fill; icon images are auto-inverted to white on the selected tab.
<template>
<!-- Dot badge -->
<Tab v-model="selected" type="filled" :tabs="primaryTabs" :indie_tab="primaryIndie" />
<!-- Badge with content (selected tab renders badge as white with dark text) -->
<Tab v-model="selected2" type="filled" :tabs="badgeTabs" :indie_tab="badgeIndie" />
</template>
<script setup>
import { ref } from 'vue';
import { Tab } from '@vibrant-wellness/va-responsive-components-library';
import iconVP from '@/assets/icons/vibrant-products.svg';
import iconSP from '@/assets/icons/signature-programs.svg';
import iconPP from '@/assets/icons/practice-products.svg';
import iconPB from '@/assets/icons/product-bundles.svg';
import iconCF from '@/assets/icons/customized-fee.svg';
import iconEZ from '@/assets/icons/ez-bill.svg';
import iconDT from '@/assets/icons/document-template.svg';
import iconAU from '@/assets/icons/automation.svg';
import iconTM from '@/assets/icons/template-marketplace.svg';
import iconAR from '@/assets/icons/archive.svg';
import iconMS from '@/assets/icons/messages.svg';
import iconIN from '@/assets/icons/inbox.svg';
const selected = ref('tab1');
const selected2 = ref('tab1');
const primaryTabs = [
{ value: 'tab1', label: 'Option 1', icon: iconVP, badge: true },
{ value: 'tab2', label: 'Option 2', icon: iconSP, badge: true },
{ value: 'tab3', label: 'Option 3', icon: iconPP, badge: true },
{ value: 'tab4', label: 'Option 4', icon: iconPB, badge: true },
{ value: 'tab5', label: 'Option 5', icon: iconCF, badge: true },
];
const primaryIndie = { value: 'indie', label: 'Option 6', icon: iconEZ, badge: true };
const badgeTabs = [
{ value: 'tab1', label: 'Messages', icon: iconMS, badge: true },
{ value: 'tab2', label: 'Inbox', icon: iconIN, badge: 3 },
{ value: 'tab3', label: 'Alerts', icon: iconAU, badge: 12 },
{ value: 'tab4', label: 'Pending', icon: iconTM, badge: 100 },
{ value: 'tab5', label: 'Spam', icon: iconCF, badge: 'New' },
];
const badgeIndie = { value: 'tab6', label: 'Archive', icon: iconAR };
</script>Underline (Medium Section Tab)
<template>
<!-- Dot badge -->
<Tab v-model="selected" type="underline" :tabs="primaryTabs" :indie_tab="primaryIndie" />
<!-- Badge with content -->
<Tab v-model="selected2" type="underline" :tabs="badgeTabs" :indie_tab="badgeIndie" />
</template>
<script setup>
import { ref } from 'vue';
import { Tab } from '@vibrant-wellness/va-responsive-components-library';
import iconVP from '@/assets/icons/vibrant-products.svg';
import iconSP from '@/assets/icons/signature-programs.svg';
import iconPP from '@/assets/icons/practice-products.svg';
import iconPB from '@/assets/icons/product-bundles.svg';
import iconCF from '@/assets/icons/customized-fee.svg';
import iconEZ from '@/assets/icons/ez-bill.svg';
import iconDT from '@/assets/icons/document-template.svg';
import iconAU from '@/assets/icons/automation.svg';
import iconTM from '@/assets/icons/template-marketplace.svg';
import iconAR from '@/assets/icons/archive.svg';
const selected = ref('tab1');
const selected2 = ref('tab1');
const primaryTabs = [
{ value: 'tab1', label: 'Option 1', icon: iconVP },
{ value: 'tab2', label: 'Option 2', icon: iconSP },
{ value: 'tab3', label: 'Option 3', icon: iconPP },
{ value: 'tab4', label: 'Option 4', icon: iconPB, badge: true },
{ value: 'tab5', label: 'Option 5', icon: iconCF, badge: true },
];
const primaryIndie = { value: 'indie', label: 'Option 6', icon: iconEZ, badge: true };
const badgeTabs = [
{ value: 'tab1', label: 'Messages', icon: iconEZ, badge: true },
{ value: 'tab2', label: 'Inbox', icon: iconDT, badge: 3 },
{ value: 'tab3', label: 'Alerts', icon: iconAU, badge: 12 },
{ value: 'tab4', label: 'Pending', icon: iconTM, badge: 100 },
{ value: 'tab5', label: 'Spam', icon: iconAR, badge: 'New' },
];
const badgeIndie = { value: 'tab6', label: 'Archive', icon: iconVP };
</script>Button Tab
Full-width dark-blue button row; all tabs have a dark background, so icon images are always inverted to white. indie_tab is not typically used with this type.
<template>
<!-- Dot badge -->
<Tab v-model="selected" type="button" :tabs="primaryTabs" />
<!-- Badge with content -->
<Tab v-model="selected2" type="button" :tabs="badgeTabs" />
</template>
<script setup>
import { ref } from 'vue';
import { Tab } from '@vibrant-wellness/va-responsive-components-library';
import iconVP from '@/assets/icons/vibrant-products.svg';
import iconSP from '@/assets/icons/signature-programs.svg';
import iconPP from '@/assets/icons/practice-products.svg';
import iconPB from '@/assets/icons/product-bundles.svg';
import iconCF from '@/assets/icons/customized-fee.svg';
import iconEZ from '@/assets/icons/ez-bill.svg';
import iconDT from '@/assets/icons/document-template.svg';
import iconAU from '@/assets/icons/automation.svg';
import iconTM from '@/assets/icons/template-marketplace.svg';
import iconAR from '@/assets/icons/archive.svg';
const selected = ref('tab1');
const selected2 = ref('tab1');
const primaryTabs = [
{ value: 'tab1', label: 'Option 1', icon: iconVP, badge: true },
{ value: 'tab2', label: 'Option 2', icon: iconSP, badge: true },
{ value: 'tab3', label: 'Option 3', icon: iconPP, badge: true },
{ value: 'tab4', label: 'Option 4', icon: iconPB, badge: true },
{ value: 'tab5', label: 'Option 5', icon: iconCF, badge: true },
];
const badgeTabs = [
{ value: 'tab1', label: 'Messages', icon: iconEZ, badge: true },
{ value: 'tab2', label: 'Inbox', icon: iconDT, badge: 3 },
{ value: 'tab3', label: 'Alerts', icon: iconAU, badge: 12 },
{ value: 'tab4', label: 'Pending', icon: iconTM, badge: 100 },
{ value: 'tab5', label: 'Spam', icon: iconAR, badge: 'New' },
];
</script>Vertical Menu Tab
Requires a sized parent container (the component fills 100% height). indie_tab is pinned to the bottom via margin-top: auto.
<template>
<!-- Dot badge -->
<div style="width: 320px; height: 900px; border: 1px solid #e5e7eb; border-radius: 8px; background: #fff;">
<Tab v-model="selected" type="vertical" :tabs="navTabs" :indie_tab="navIndie" />
</div>
<!-- Badge with content -->
<div style="width: 320px; height: 900px; border: 1px solid #e5e7eb; border-radius: 8px; background: #fff;">
<Tab v-model="selected2" type="vertical" :tabs="navBadgeTabs" :indie_tab="navIndie" />
</div>
</template>
<script setup>
import { ref } from 'vue';
import { Tab } from '@vibrant-wellness/va-responsive-components-library';
import iconEZ from '@/assets/icons/ez-bill.svg';
import iconDT from '@/assets/icons/document-template.svg';
import iconAU from '@/assets/icons/automation.svg';
import iconTM from '@/assets/icons/template-marketplace.svg';
import iconAR from '@/assets/icons/archive.svg';
import iconCF from '@/assets/icons/customized-fee.svg';
const selected = ref('ez-bill');
const selected2 = ref('ez-bill');
const navTabs = [
{ value: 'ez-bill', label: 'eZ-Bill', icon: iconEZ, badge: true },
{ value: 'document-template', label: 'Document Template', icon: iconDT, badge: true },
{ value: 'automation', label: 'Automation', icon: iconAU },
{ value: 'template-marketplace', label: 'Template Marketplace', icon: iconTM },
{ value: 'archive', label: 'Archive', icon: iconAR, badge: true },
];
const navBadgeTabs = [
{ value: 'ez-bill', label: 'eZ-Bill', icon: iconEZ, badge: true },
{ value: 'document-template', label: 'Document Template', icon: iconDT, badge: 5 },
{ value: 'automation', label: 'Automation', icon: iconAU, badge: 12 },
{ value: 'template-marketplace', label: 'Template Marketplace', icon: iconTM, badge: 100 },
{ value: 'archive', label: 'Archive', icon: iconAR, badge: 'New' },
];
const navIndie = { value: 'customized-fee', label: 'Customized Fee', icon: iconCF };
</script>Capsule Switch Tab (two color variants: sky default, vibrant-dark)
<template>
<!-- Sky (default) — dot badge -->
<div style="width: 540px;">
<Tab v-model="selected" type="capsule" :tabs="switchTabs" />
</div>
<!-- Sky — badge with content -->
<div style="width: 540px;">
<Tab v-model="selected2" type="capsule" :tabs="switchBadgeTabs" />
</div>
<!-- Vibrant Dark — dot badge -->
<div style="width: 540px;">
<Tab v-model="selected3" type="capsule" color="vibrant-dark" :tabs="switchTabs" />
</div>
<!-- Vibrant Dark — badge with content -->
<div style="width: 540px;">
<Tab v-model="selected4" type="capsule" color="vibrant-dark" :tabs="switchBadgeTabs" />
</div>
</template>
<script setup>
import { ref } from 'vue';
import { Tab } from '@vibrant-wellness/va-responsive-components-library';
import iconVP from '@/assets/icons/vibrant-products.svg';
import iconPP from '@/assets/icons/practice-products.svg';
const selected = ref('vibrant');
const selected2 = ref('vibrant');
const selected3 = ref('vibrant');
const selected4 = ref('vibrant');
const switchTabs = [
{ value: 'vibrant', label: 'Vibrant Products', icon: iconVP, badge: true },
{ value: 'practice', label: 'Practice Products', icon: iconPP, badge: true },
];
const switchBadgeTabs = [
{ value: 'vibrant', label: 'Vibrant Products', icon: iconVP, badge: 3 },
{ value: 'practice', label: 'Practice Products', icon: iconPP, badge: 12 },
];
</script>Oval Switch Tab (two color variants: sky default, vibrant-dark)
<template>
<!-- Sky (default) — dot badge -->
<div style="width: 580px;">
<Tab v-model="selected" type="oval" :tabs="switchTabs" />
</div>
<!-- Sky — badge with content -->
<div style="width: 580px;">
<Tab v-model="selected2" type="oval" :tabs="switchBadgeTabs" />
</div>
<!-- Vibrant Dark — dot badge -->
<div style="width: 580px;">
<Tab v-model="selected3" type="oval" color="vibrant-dark" :tabs="switchTabs" />
</div>
<!-- Vibrant Dark — badge with content -->
<div style="width: 580px;">
<Tab v-model="selected4" type="oval" color="vibrant-dark" :tabs="switchBadgeTabs" />
</div>
</template>
<script setup>
import { ref } from 'vue';
import { Tab } from '@vibrant-wellness/va-responsive-components-library';
import iconVP from '@/assets/icons/vibrant-products.svg';
import iconPP from '@/assets/icons/practice-products.svg';
const selected = ref('vibrant');
const selected2 = ref('vibrant');
const selected3 = ref('vibrant');
const selected4 = ref('vibrant');
const switchTabs = [
{ value: 'vibrant', label: 'Vibrant Products', icon: iconVP, badge: true },
{ value: 'practice', label: 'Practice Products', icon: iconPP, badge: true },
];
const switchBadgeTabs = [
{ value: 'vibrant', label: 'Vibrant Products', icon: iconVP, badge: 3 },
{ value: 'practice', label: 'Practice Products', icon: iconPP, badge: 12 },
];
</script>Click-to-clear-badge pattern (badges are data-driven, so you clear them yourself in @change)
<template>
<Tab v-model="selected" type="underline" :tabs="tabs" @change="onChange" />
</template>
<script setup>
import { ref } from 'vue';
import { Tab } from '@vibrant-wellness/va-responsive-components-library';
const selected = ref('tab1');
const tabs = ref([
{ value: 'tab1', label: 'Messages', badge: true },
{ value: 'tab2', label: 'Inbox', badge: 3 },
{ value: 'tab3', label: 'Alerts', badge: 12 },
]);
function onChange(value) {
const t = tabs.value.find(x => x.value === value);
if (t) t.badge = false; // mark-as-read on click
}
</script>Responsive Sizing
Tab adapts to any container by default (inherits parent width, scrolls horizontally when content overflows). For finer control, combine the responsive props below.
1. Total group width — width
<!-- Tab fills 380px regardless of parent. Number → px, or any CSS length string. -->
<Tab v-model="selected" type="filled" :width="380" :tabs="tabs" />
<Tab v-model="selected" type="oval" width="50%" :tabs="tabs" />
<Tab v-model="selected" type="outlined" width="32rem" :tabs="tabs" />2. Fixed per-tab width + label font — tab_width + font_size (e.g. EHR ez-bill main-tab style)
<Tab
v-model="selected"
type="filled"
:tab_width="135"
:font_size="12"
:tabs="tabs"
/>3. Density presets — gap + padding + font_size + icon_size
<!-- Compact (sidebar / dense list) -->
<Tab
v-model="selected"
type="underline"
:gap="2"
:padding="6"
:font_size="12"
:icon_size="14"
:tabs="tabs"
/>
<!-- Spacious (page header) -->
<Tab
v-model="selected"
type="underline"
:gap="24"
:padding="20"
:font_size="16"
:icon_size="24"
:tabs="tabs"
/>4. Overflow strategy — overflow_mode
<!-- Default: horizontal scroll (scrollbar hidden) — labels stay readable -->
<Tab v-model="selected" type="filled" :width="380" overflow_mode="scroll" :tabs="tabs" />
<!-- Or: ellipsis — tabs shrink in place, labels truncate where they sit -->
<Tab v-model="selected" type="filled" :width="380" overflow_mode="ellipsis" :tabs="tabs" />All responsive props are independent — combine any of them.
capsulealways shares parent width evenly across its tabs because that is its design semantic.
ResponsiveButton
Responsive button component with multiple button types (filled, outlined, text), built-in themes (light, dark, error), and flexible sizing. Supports prefix/suffix slots, custom styling, and interaction effects.
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| display_name | button text content | String |
'' |
| size | button size variant | 'small' | 'medium' | 'large' |
'small' |
| width_type | button width behavior | 'fill-whole' | 'fit-content' |
'fit-content' |
| button_type | visual style type | 'filled' | 'outlined' | 'text' |
'filled' |
| built_in_theme | color theme (for text buttons, only error theme has effect; light/dark have no effect) |
'light' | 'dark' | 'error' |
'dark' |
| button_border_radius | CSS border-radius value | String |
'4px' |
| customized_class | additional CSS class(es) | String |
'' |
| button_id | identifier for click event payload | String |
'' |
| disabled | disable button interaction and click emission | Boolean |
false |
| is_active | when false, renders with disabled-like styling (grey) but the button remains fully clickable and emits click-button normally |
Boolean |
true |
Events
| Event | Description | Parameters |
|---|---|---|
| click-button | triggers when button is clicked | (buttonId: String, displayName: String) |
Slots
| Name | Description |
|---|---|
| default | button text content (alternative to display_name) |
| prefix | content before button text |
| suffix | content after button text |
<template>
<ResponsiveButton
display_name="Save Changes"
size="medium"
button_type="filled"
built_in_theme="dark"
width_type="fit-content"
@click-button="handleSave"
>
<template #prefix>
<svg viewBox="0 0 16 16"><path d="M8 2L6 6H2L5 9L4 14L8 11L12 14L11 9L14 6H10L8 2Z"/></svg>
</template>
</ResponsiveButton>
</template>
<script setup>
function handleSave(buttonId, displayName) {
console.log('Button clicked:', buttonId, displayName)
}
</script>Theme/Behavior Notes
Button Types:
- filled: Solid background with theme color, white text
- outlined: Transparent background with colored border, theme-colored text
- text: No background or border, blue text by default. The
errortheme applies red color with appropriate hover/active states.lightanddarkthemes have no visual effect.
Built-in Theme Colors:
- dark:
var(--VibrantDarkBlue)- primary dark blue - light:
var(--New_Button_SkyBlue)- sky blue - error:
var(--ErrorRed)- error red
Interaction States:
- Hover: Filled buttons change to green hover color; outlined buttons darken; text buttons darken (or use error red hover if
errortheme) - Active: Filled buttons change to press blue color; outlined buttons show pressed state; text buttons show pressed background (or use error red pressed if
errortheme) - Disabled (
disabled="true"): Gray background/border with gray text,not-allowedcursor, click events are not emitted - Inactive (
is_active="false"): Same grey visual as disabled, but cursor stays normal, the element remains clickable, andclick-buttonis still emitted — use when a button should look deselected/inactive but still respond to interaction
Size Variants:
- small:
4px 24pxpadding,4pxgap - medium:
8px 24pxpadding,6pxgap - large:
12px 32pxpadding,8pxgap - text buttons use
8pxhorizontal padding regardless of size
Deprecated Components
InputBox
Versatile input field with clearable icon, inside/outside label, styled themes, and validation helpers. The component dynamically adjusts border/background/label colors based on state (hover, focus, disabled, alert, error) and supports prefix/suffix slots.
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| v-model / modelValue | bound input value | string |
'' |
| label | label text (optional) | string |
'' |
| label_type | where to render the label | 'inside' | 'outside' |
'outside' |
| theme_type | visual styling theme | 'filled' | 'outlined' |
'outlined' |
| customized_theme_color | focus/brand color used for caret color and focus border or underline; any valid CSS color or CSS var | string |
'' |
| type | native input type | string |
'text' |
| placeholder | placeholder text | string |
'Enter' |
| clearable | show a clear icon when focused/hovered and value is non-empty | boolean |
false |
| required | show a required asterisk on the label. Visual-only; does not enforce validation | boolean |
false |
| disabled | disable the input | boolean |
false |
| autocomplete | native autocomplete |
string |
'off' |
Behavior notes:
- In
filledtheme, a bottom inset shadow emulates the underline. Inoutlinedtheme, an inset border is used. Colors are derived from:- Focus:
customized_theme_color(orvar(--Schemes-primary)fallback) - Hover:
#17181C - Disabled:
var(--disabled-color--)andvar(--disabled-button-background-color--) - Alert/Error:
var(--semantic-alert-color--)/var(--semantic-error-color--)
- Focus:
- Inside label floats above the input content area and inherits a state color similar to the border color.
- A message region appears below the field when either
#supportingTextslot has content or when alert/error is active with a message. #supportingTextis neutral and does not adopt alert/error colors; alert/error messages are colored separately.- Alert/Error messages are set via exposes:
alert(message)/error(message); callremoveAlertOrErrorEffect()to clear. - Disabled state down-weights message colors.
Events
| Event | Description | Parameters |
|---|---|---|
| input | fires on each keystroke | (value: string) |
| change | fires on Enter or blur | (value: string) |
| focus | input focused | () |
| blur | input blurred | () |
| clear | clear button clicked | () |
Exposes
| Method | Description |
|---|---|
| focus | programmatically focus the input |
| blur | programmatically blur the input |
| alert | show alert state with a message (yellow) |
| error | show error state with a message (red) |
| removeAlertOrErrorEffect | clear alert/error state |
Slots
| Name | Description |
|---|---|
| prefix | content rendered before the input (e.g., an icon) |
| suffix | content rendered after the input (e.g., an icon) |
| supportingText | optional helper text rendered below the field (e.g., character counter like 0/100). This is distinct from alert/error messages and uses a neutral color. When alert/error is active, their messages render alongside (with their own colors) |
Tip: For prefix/suffix icons, prefer inline SVG that uses
fill="currentColor". This way the icon color automatically follows the component state (hover/focus/disabled/alert/error). If you use<img>sources, they won't inherit color.
Usage
<template>
<InputBox
v-model="value"
label="Email"
label_type="inside"
theme_type="outlined"
customized_theme_color="var(--Schemes-primary)"
type="email"
placeholder="name@example.com"
clearable
autocomplete="email"
>
<template #prefix>
<img src="/icons/mail.svg" style="width:100%;height:100%"/>
</template>
</InputBox>
</template>
<script setup>
import { ref } from 'vue'
const value = ref('')
</script>
#### SingleSelector
Dropdown selector with inside/outside label, themed styling, filtering, optional remote search with pagination, and a fully customizable per-option layout. Mirrors `InputBox` visual behavior (hover/focus/disabled/alert/error), supports prefix/suffix slots plus a neutral `supportingText` message region, a helper info line below the field, and an overridable background color.
##### Attributes
| Attribute | Description | Type | Default |
|-----------|-------------|------|---------|
| v-model / modelValue | binding value (selected option's `value`) | `string \| number \| boolean \| object \| array` | `''` |
| label | label text | `string` | `''` |
| label_type | label position — `'inside'` (inside the input box) or `'outside'` | `'inside' \| 'outside'` | `'outside'` |
| size | selector size | `'small' \| 'medium' \| 'large'` | `'small'` |
| theme_type | visual theme | `'filled' \| 'outlined'` | `'outlined'` |
| customized_theme_color | caret color and focus/brand color; any CSS color or CSS var | `string` | `''` |
| required | show red asterisk next to label | `boolean` | `false` |
| options | options array — each option supports `value`, `label`, `prefix?`, `suffix?`, `prefix_slot_raw_html_content?`, `suffix_slot_raw_html_content?`, `disabled?`, `is_selected?` | `Array<Option>` | `() => []` |
| filterable | enable client-side filter input | `boolean` | `false` |
| remote_search | emit query outward instead of local filtering | `boolean` | `false` |
| remote_search_pagination | (with `remote_search`) enable infinite-scroll pagination — emits `remote-search-load-more` | `boolean` | `false` |
| remote_search_has_more | parent-controlled flag: whether more pages are available | `boolean` | `false` |
| remote_search_load_more_loading | parent-controlled flag: whether the next page is being fetched; shows a "Loading more..." row | `boolean` | `false` |
| remote_search_scroll_threshold | px from bottom of the scroll area that triggers `remote-search-load-more` | `number` | `80` |
| disabled | disable selector | `boolean` | `false` |
| placeholder | placeholder when no value | `string` | `'Select'` |
| clearable | show clear icon when there is a value and focused/hovered | `boolean` | `false` |
| dropdown_max_height | max height of the dropdown scroll area (any CSS height) | `string` | `'400px'` |
| dropdown_width | override the dropdown width (any CSS width, e.g. `'300px'`); empty = match the trigger width | `string` | `''` |
| loading | loading state (mirrors dropdown's `is_loading`) | `boolean` | `false` |
| loading_text | loading text | `string` | `'Loading'` |
| no_data_text | empty-state text | `string` | `'No data'` |
| filter_only_among_options_value | filter against serialized `value` only | `boolean` | `false` |
| filter_only_among_options_label | filter against `label` only | `boolean` | `false` |
| helper_info | grey hint text displayed below the field | `string` | `''` |
| background_color_override | override the trigger's background color (ignored when `disabled`) | `string` | `''` |
| customizedOptionStyle | render each option via the `#option` scoped slot (slot props: `{ option, index, is_selected, disabled }`) instead of the default prefix/label/suffix layout | `boolean` | `false` |
| customizeOptionItem | render the entire option element via the `#option_item` scoped slot — no built-in `<button>` wrapper, click handler, or default row layout. Slot props: `{ option, index, is_selected, disabled, select }`. Call `select()` from your own element to trigger the normal selection. Takes precedence over `customizedOptionStyle`. | `boolean` | `false` |
Notes:
- `filter_only_among_options_value` and `filter_only_among_options_label` cannot both be `true`. If both are `true`, filtering uses `value`.
- Option `value` may be primitive or object; selection compares by `value` plus `label` for marking `is_selected` in lists.
##### Events
| Event | Description | Parameters |
|-------|-------------|------------|
| update:modelValue | syncs v-model when the selected value changes | `(value: any)` |
| change | triggers when the binding value changes | `(value: any)` |
| remote-search | emitted while typing when `remote_search=true` | `(query: string)` |
| remote-search-load-more | emitted when scrolling near the bottom with `remote_search_pagination=true` and `remote_search_has_more=true` | `(query: string)` |
| filter-change | emitted whenever the filter input text changes | `(query: string)` |
| visible-change | dropdown visibility changes | `(visible: boolean)` |
| focus | selector focused | `()` |
| blur | selector blurred | `()` |
| clear | clear button clicked | `()` |
##### Exposes
| Method | Description | Type |
|--------|-------------|------|
| focus | programmatically focus the selector | `() => void` |
| blur | programmatically blur the selector | `() => void` |
| alert | show alert state with a message (yellow) | `(msg: string) => void` |
| error | show error state with a message (red) | `(msg: string) => void` |
| removeAlertOrErrorEffect | clear alert/error state | `() => void` |
| setDropdownContentLoading | toggle the dropdown's internal loading spinner | `(val: boolean) => void` |
##### Slots
| Name | Slot props | Description |
|------|------------|-------------|
| prefix | — | content before the selected value (e.g. icon, flag) |
| suffix | — | replaces the default chevron; content after the selected value |
| supportingText | — | neutral helper text under the field; alert/error messages render alongside in their own colors |
| option | `{ option, index, is_selected, disabled }` | per-option custom content; only rendered when `customizedOptionStyle` is `true` |
| option_item | `{ option, index, is_selected, disabled, select }` | full per-option element (no built-in wrapper); only rendered when `customizeOptionItem` is `true`. Call `select()` to trigger the normal selection. |
```vue
<SingleSelector
v-model="selected"
:options="options"
filterable
clearable
placeholder="Choose option"
:customizedOptionStyle="true"
>
<template #prefix>
<Icon name="search" />
</template>
<template #option="{ option, is_selected }">
<span :class="['my-option', { selected: is_selected }]">
<img :src="option.flag" />
<span>{{ option.label }}</span>
<small>{{ option.meta }}</small>
</span>
</template>
<template #supportingText>
Pick one option
</template>
</SingleSelector>DropdownMenu (dev-only)
Used internally by components to render options lists.
- Props:
width_type: 'fill-whole'|'fit-content',options(same schema as above),size: 'small'|'medium'|'large',with_box_shadow,is_loading,loading_text,no_data_text. - Events:
select-dropdown-option(value, label, in_dropdown_level)add-new-shown-nested-dropdown(target_item_props, trigger)remove-shown-nested-dropdown(target_item_props)update-shown-nested-dropdown(target_item_props)click-outside-dropdown-menu(event)
MultiSelector
Multi-select dropdown with selected values rendered as inline chips, built-in search, optional remote search with pagination, per-option action link, and a fully customizable per-option layout (checkbox kept on the left). The dropdown is split into an opaque outer box and an inner scroll element so rubber-band bounce never reveals content behind the dropdown.
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| v-model / modelValue | array of selected values | Array |
() => [] |
| options | options array — each option supports value, label, prefix_slot_raw_html_content?, action_label?, disabled? |
Array |
() => [] |
| size | selector size | 'small' | 'medium' | 'large' |
'medium' |
| placeholder | placeholder in the search input when no chips are selected | string |
'Type to search' |
| disabled | disable selector | boolean |
false |
| empty_text | text shown when no options match the search | string |
'No results' |
| remote_search | disable internal keyword filtering; options are managed externally via @search |
boolean |
false |
| remote_search_pagination | (with remote_search) enable infinite-scroll pagination — emits remote-search-load-more |
boolean |
false |
| remote_search_has_more | parent-controlled flag: whether more pages are available | boolean |
false |
| remote_search_load_more_loading | parent-controlled flag: whether the next page is being fetched; shows a "Loading more..." row | boolean |
false |
| remote_search_scroll_threshold | px from bottom of the scroll area that triggers remote-search-load-more |
number |
80 |
| loading | show a loading spinner in the dropdown instead of options | boolean |
false |
| helper_info | grey hint text displayed below the trigger | string |
'' |
| dropdown_max_height | max height of the dropdown scroll area (any CSS height, e.g. '400px', '50vh') |
string |
'400px' |
| customizedOptionStyle | render each option's content (after the checkbox) via the #option scoped slot |
boolean |
false |
| customizeOptionItem | render the entire option element via the #option_item scoped slot — no built-in <button> wrapper, no default checkbox or tint-box. Slot props: { option, index, is_selected, disabled, toggle }. Call toggle() from your own element to trigger the normal selection logic. Takes precedence over customizedOptionStyle. |
boolean |
false |
Events
| Event | Description | Parameters |
|---|---|---|
| update:modelValue | syncs v-model when the selected values change | (values: Array) |
| change | emitted when the selected values change | (values: Array) |
| visible-change | dropdown visibility changes | (visible: boolean) |
| focus | search input focused | () |
| blur | search input blurred | () |
| clear | all chips cleared | (value: null) |
| option-action | an option's action link clicked | (option: Option) |
| search | emitted while typing when remote_search=true |
(query: string) |
| remote-search-load-more | emitted when scrolling near the bottom with remote_search_pagination=true and remote_search_has_more=true |
(query: string) |
Exposes
| Method | Description | Type |
|---|---|---|
| focus | programmatically focus the search input | () => void |
| blur | programmatically blur the search input | () => void |
| clear | remove all selected chips | () => void |
Slots
| Name | Slot props | Description |
|---|---|---|
| option | { option, index, is_selected, disabled } |
per-option custom content rendered to the right of the checkbox; only used when customizedOptionStyle is true |
| option_item | { option, index, is_selected, disabled, toggle } |
full per-option element (no built-in wrapper or checkbox); only rendered when customizeOptionItem is true. Call toggle() to trigger the normal selection. |
<MultiSelector
v-model="selected"
:options="options"
size="medium"
placeholder="Type to search"
helper_info="Pick one or more"
:customizedOptionStyle="true"
@change="onChange"
@option-action="onOptionAction"
>
<template #option="{ option, is_selected }">
<span :class="['opt', { selected: is_selected }]">
<span class="opt-name">{{ option.label }}</span>
<small class="opt-meta">{{ option.code }}</small>
</span>
</template>
</MultiSelector>Theme/Behavior Notes
- Chip layout: selected values render as
Chipcomponents inside the trigger; chiptypefollows size (small→'regular',medium/large→'inside-combo-selector'). - Remote-search pagination: when
remote_search,remote_search_pagination, andremote_search_has_moreare alltrue, the component watches the dropdown scroll and emitsremote-search-load-moreonce per threshold crossing. Setremote_search_load_more_loadingtotruewhile fetching to show the inline "Loading more..." row and suppress further emits. - Dropdown structure: the outer
.multi-selector-dropdownis the opaque styled box (border, radius, white bg,isolation: isolate); the inner.multi-selector-dropdown-scrollhandles overflow withoverscroll-behavior: contain. - Option values must be JSON-serializable: selection equality, the selected-set membership check,
toggleOption, andremoveSelectedItemall compare viaJSON.stringify(option.value). Values containing circular references,BigInt, functions, or symbols will throw or compare incorrectly. Thev-for:keyfalls back to the option index for non-serializable values so rendering never crashes, but equality logic will still misbehave — prefer primitives or plain data objects forvalue.
TextInput
Single-line text input with small/medium/large sizes, clearable button, prefix/suffix slots, helper info, and an optional attached SingleSelector prefix (with forwarded #prefix and per-option #option slots). Supports alert/error states with a supporting message region.
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| v-model / modelValue | bound value | string | number |
'' |
| size | input size (32 / 40 / 48 px) | 'small' | 'medium' | 'large' |
'medium' |
| type | native input type |
string |
'text' |
| placeholder | placeholder text | string |
'Type to enter ...' |
| clearable | show inline clear button when value is non-empty | boolean |
true |
| disabled | disable the input | boolean |
false |
| autocomplete | native autocomplete attribute |
string |
'off' |
| helper_info | grey hint text displayed below the input | string |
'' |
| customized_theme_color | caret color and focus/brand color; any CSS color or CSS var | string |
'' |
| prefix_selector_options | when non-empty, attaches a SingleSelector on the left — array of { value, label } |
Array |
() => [] |
| v-model:prefix_selector_value / prefix_selector_value | selected value of the prefix selector | string | number | boolean | object |
null |
| prefix_selector_placeholder | placeholder for the prefix selector | string |
'Select' |
| prefix_selector_dropdown_width | forwarded to the inner SingleSelector's dropdown_width |
string |
'' |
| isDateOfBirth | enable date of birth input mode with automatic formatting (placeholder is automatically set to dateFormat) | boolean |
false |
| dateFormat | format for date of birth input | 'MM-DD-YYYY' | 'MM/DD/YYYY' |
'MM-DD-YYYY' |
Events
| Event | Description | Parameters |
|---|---|---|
| update:modelValue | emitted on every keystroke | (value: string) |
| input | emitted on every keystroke | (value: string) |
| change | emitted when Enter is pressed | (value: string) |
| focus | native input focused | () |
| blur | native input blurred | () |
| clear | inline clear button clicked | () |
| update:prefix_selector_value | v-model update for the prefix selector | (value: any) |
| prefix_selector_change | prefix selector selection changed | (value: any) |
Exposes
| Method | Description | Type |
|---|---|---|
| focus | focus the input | () => void |
| blur | blur the input | () => void |
| alert | show alert message below the input | (msg: string) => void |
| error | show error message below the input | (msg: string) => void |
| removeAlertOrErrorEffect | clear alert/error state | () => void |
Slots
| Name | Slot props | Description |
|---|---|---|
| prefix | — | content before the input field (icon/text) |
| suffix | — | content after the input field (icon/text) |
| supportingText | — | neutral helper text in the message region below the input; coexists with alert/error messages |
| prefix_selector_prefix | — | forwarded to the inner SingleSelector's #prefix slot (e.g. a flag or icon shown inside the prefix selector's selected value) |
| prefix_selector_option | { option, index, is_selected, disabled } |
forwarded to the inner SingleSelector's #option scoped slot — when present, customizedOptionStyle is auto-enabled on the inner selector |
<TextInput
v-model="phone"
size="large"
placeholder="Phone number"
v-model:prefix_selector_value="country"
:prefix_selector_options="countryOptions"
prefix_selector_dropdown_width="300px"
helper_info="We will call this number"
>
<template #prefix_selector_prefix>
<span>{{ currentFlag }} +{{ currentCode }}</span>
</template>
<template #prefix_selector_option="{ option, is_selected }">
<div :class="['opt', { selected: is_selected }]">
<span>{{ option.flag }}</span>
<span>{{ option.country }}</span>
<span>+{{ option.code }}</span>
</div>
</template>
</TextInput>Date of Birth Mode
When isDateOfBirth is true, the input automatically formats dates as the user types. The dateFormat prop controls the separator (- or /).
<!-- MM-DD-YYYY format -->
<TextInput
v-model="birthDate"
size="large"
:isDateOfBirth="true"
dateFormat="MM-DD-YYYY"
/>
<!-- MM/DD/YYYY format -->
<TextInput
v-model="birthDate"
size="large"
:isDateOfBirth="true"
dateFormat="MM/DD/YYYY"
/>As the user types, the input automatically formats:
- Typing
01152000→01-15-2000(or01/15/2000) - Backspace behavior respects separators
- Placeholder shows the selected format
TextArea
Multi-line text input with a built-in toolbar for character count and a Clear button, fixed or stretch-to-fill height, and a helper info line. Visually matches TextInput (same border, hover tint, disabled state).
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| v-model / modelValue | bound text | string |
'' |
| placeholder | placeholder text | string |
'Text Area' |
| rows | initial visible row count (the textarea grows naturally beyond this) | number |
3 |
| max_length | when set, shows the character counter in the toolbar | number |
1000 |
| show_char_count | show the {count}/{max_length} characters counter |
boolean |
true |
| clearable | show the Clear toolbar button when the textarea has a value |
boolean |
true |
| disabled | disable the textarea | boolean |
false |
| helper_info | grey hint text displayed below the textarea | string |
'' |
| height | fixed textarea height — number (px), any CSS string (e.g. '8em'), or 'full' to stretch to the outer container |
number | string |
84 |
Events
| Event | Description | Parameters |
|---|---|---|
| update:modelValue | emitted on every keystroke | (value: string) |
| input | emitted on every keystroke | (value: string) |
| change | native change event |
(value: string) |
| focus | textarea focused | () |
| blur | textarea blurred | () |
| clear | Clear button clicked | () |
Exposes
| Method | Description | Type |
|---|---|---|
| focus | focus the textarea | () => void |
| blur | blur the textarea | () => void |
| alert | show alert message (internal state) | (msg: string) => void |
| error | show error message (internal state) | (msg: string) => void |
| removeAlertOrErrorEffect | clear alert/error state | () => void |
<TextArea
v-model="bio"
placeholder="Tell us about yourself..."
:rows="4"
:max_length="500"
helper_info="Max 500 characters"
height="120"
/>Label
Layout wrapper that places a label row above or beside any content with configurable gap and width. Supports vertical/horizontal layouts, required marker, optional icon slot, and a fully customizable label slot (like SingleSelector's customizedOptionStyle).
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| label | label text | string |
'' |
| required | show (Required) marker in red |
boolean |
false |
| gap | vertical layout only: gap between label row and content (number = px, or any CSS string) | number | string |
4 |
| layout | layout direction — 'vertical' (default) puts label above content; 'horizontal' puts label left with a 20 px gap |
'vertical' | 'horizontal' |
'vertical' |
| label_width | horizontal layout only: fixed width of the label column (number = px, or any CSS string) | number | string |
null |
| label_align | horizontal layout only: vertical alignment of the label column | 'top' | 'center' | 'bottom' |
'top' |
| customizedLabelStyle | when true, the label row's inner content is rendered via the #label scoped slot instead of the default icon + text + (Required) layout |
boolean |
false |
Slots
| Name | Slot props | Description |
|---|---|---|
| default | — | content placed below (vertical) or beside (horizontal) the label |
| icon | — | 24×24 icon displayed to the left of the label text (default layout only) |
| label | { label, required, layout } |
fully replaces the default label row content; only rendered when customizedLabelStyle is true |
<!-- Default layout -->
<Label label="Email" required>
<TextInput v-model="email" />
</Label>
<!-- Horizontal with fixed width -->
<Label label="Billing Address" layout="horizontal" :label_width="160">
<TextInput v-model="addr" />
</Label>
<!-- Fully customized label -->
<Label label="API Key" :customizedLabelStyle="true">
<template #label="{ label, required }">
<span class="custom-label">
<strong>{{ label }}</strong>
<em v-if="required">must fill</em>
<span title="Secret">(?)</span>
</span>
</template>
<TextInput v-model="key" />
</Label>InfoContainer
Container that wraps any content and attaches an inline Notification below it for error/alert messages. When message is empty the wrapper is transparent (no border) and only the slot content is rendered.
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| message | notification message; when empty/null the container is transparent and renders only the slot | string |
'' |
| state | notification state — maps to Notification theme (error → error, alert → warning) |
'error' | 'alert' |
'error' |
| action_label | optional action link text shown on the right of the inline notification | string |
'' |
Events
| Event | Description | Parameters |
|---|---|---|
| action | emitted when the action link is clicked | () |
Slots
| Name | Slot props | Description |
|---|---|---|
| default | — | the content to wrap (typically one or more form fields) |
<InfoContainer
:message="formError"
state="error"
action_label="Retry"
@action="retrySubmit"
>
<Label label="Email" required>
<TextInput v-model="email" />
</Label>
<Label label="Password" required>
<TextInput v-model="pw" type="password" />
</Label>
</InfoContainer>Theme/Behavior Notes
- State mapping:
state="error"→ red border +Notificationthemeerror;state="alert"→ amber border (CSS var--semantic-alert-color--) +Notificationthemewarning. - No message, no border: when
messageis falsy,state-noneis applied and the border becomes transparent so the container is invisible in the layout.
Checkbox
Customizable checkbox with multiple themes, indeterminate state, and width control. Supports integration with GroupCheckbox for managing multiple related checkboxes.
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| v-model / modelValue | binding value - true when checked, false when unchecked |
boolean |
false |
| v-model:indeterminate | indeterminate state binding | boolean |
false |
| isLabelClickable | whether clicking the label toggles the checkbox | boolean |
true |
| label | checkbox label text | string |
'' |
| width | width behavior | 'fit-content' | 'fill-whole' |
'fit-content' |
| built_in_theme | built-in color theme | 'green' | 'primary-blue' | 'secondary-blue' | 'red' |
'green' |
| customized_theme_color | custom theme color (overrides built_in_theme) | string (any valid CSS color value) |
'' |
| customized_class | custom CSS class for complete styling override | string |
'' |
| disabled | whether checkbox is disabled | boolean |
false |
| autofocus | whether to auto focus on mount | boolean |
false |
Events
| Event | Description | Parameters |
|---|---|---|
| update:modelValue | syncs v-model when checked state changes | (value: boolean) |
| update:indeterminate | syncs v-model:indeterminate when indeterminate state changes | (value: boolean) |
| change | triggered when the checkbox is checked or unchecked | (value: boolean) |
Exposes
| Method | Description | Type |
|---|---|---|
| focus | programmatically focus the checkbox | () => void |
| blur | programmatically blur the checkbox | () => void |
Slots
| Name | Description |
|---|---|
| default | custom label content (overrides label prop) |
<template>
<!-- Basic usage with default theme -->
<Checkbox
v-model="agreed"
label="I agree to terms"
/>
<!-- With custom theme color (overrides built_in_theme) -->
<Checkbox
v-model="agreed"
label="I agree to terms"
built_in_theme="primary-blue"
customized_theme_color="#FF0000"
@change="handleChange"
/>
<!-- Using CSS variable for theme color -->
<Checkbox
v-model="agreed"
label="I agree to terms"
customized_theme_color="var(--Schemes-primary)"
@change="handleChange"
/>
<!-- With indeterminate state -->
<Checkbox
v-model="checked"
v-model:indeterminate="indeterminate"
label="Select all"
/>
<!-- Full width with non-clickable label -->
<Checkbox
v-model="checked"
label="Non-clickable label"
:isLabelClickable="false"
width="fill-whole"
/>
<!-- Using slot for custom label content -->
<Checkbox v-model="checked">
<div>
<strong>Bold Label</strong> with custom content
</div>
</Checkbox>
</template>
<script setup>
import { ref } from 'vue'
const agreed = ref(false)
const checked = ref(false)
const indeterminate = ref(false)
const handleChange = (value) => {
console.log('Checkbox changed:', value)
}
</script>Theme Behavior Notes
Theme Color Priority (highest to lowest):
- Disabled: Always uses
var(--DisabledGrey_20)regardless of other theme settings - customized_theme_color: When provided (non-empty), overrides
built_in_theme - built_in_theme: Falls back to built-in theme when no customized color is set
green:var(--New_Library_HoverGreen)primary-blue:var(--VibrantDarkBlue)secondary-blue:var(--New_Button_SkyBlue)red:var(--ErrorRed)
Theme Color Applies To:
- Checkbox border (all states: unchecked, checked, indeterminate)
- Checked/indeterminate background color
- Hover effect overlay (8% color mix of customized_theme_color)
- Hover effect checked/indeterminate border color (50% color mix of customized_theme_color)
- Active/hover border width changes
Indeterminate & Checked State Interaction:
- When
checkedbecomestruewhileindeterminateistrue:indeterminateautomatically becomesfalse - When
indeterminatebecomestruewhilecheckedistrue:checkedautomatically becomesfalse - When both are
true: checkbox displays as indeterminate - Visual representation: Indeterminate state takes precedence over checked state
GroupCheckbox
Container for managing multiple related checkboxes as a group. Automatically tracks which child checkboxes are checked and provides a reactive array of selected labels.
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| v-model / modelValue | binding value - array of selected checkbox labels from child Checkbox components | array |
[] |
Events
| Event | Description | Parameters |
|---|---|---|
| update:modelValue | syncs v-model when selected checkboxes change | (value: array) |
| change | triggers when the binding value changes | (value: array) |
Usage Notes
How It Works:
- GroupCheckbox uses Vue's
provide/injectto communicate with childCheckboxcomponents - Each child checkbox's
labelprop is used as its identifier in the selected array - When a checkbox is toggled, GroupCheckbox automatically updates the
modelValuearray with the labels of all checked checkboxes - Changes to
modelValuefrom outside the component will sync back to the child checkboxes
Important Requirements:
- All child
Checkboxcomponents must have a uniquelabelprop (or use the default slot with unique content) - The label is used as the identifier in the selected array
<template>
<!-- Basic usage with string array -->
<GroupCheckbox v-model="selectedFruits">
<Checkbox label="Apple" />
<Checkbox label="Banana" />
<Checkbox label="Orange" />
</GroupCheckbox>
<!-- With v-for loop -->
<GroupCheckbox v-model="selectedItems">
<Checkbox
v-for="item in options"
:key="item"
:label="item"
/>
</GroupCheckbox>
<!-- With themed checkboxes -->
<GroupCheckbox v-model="selectedCategories">
<Checkbox
v-for="category in categories"
:key="category.id"
:label="category.name"
built_in_theme="primary-blue"
/>
</GroupCheckbox>
<!-- Pre-selecting items -->
<GroupCheckbox v-model="preSelected">
<Checkbox label="Option A" />
<Checkbox label="Option B" />
<Checkbox label="Option C" />
</GroupCheckbox>
<!-- Listening to changes -->
<GroupCheckbox
v-model="selectedItems"
@change="handleSelectionChange"
>
<Checkbox
v-for="item in options"
:key="item.id"
:label="item.name"
/>
</GroupCheckbox>
</template>
<script setup>
import { ref } from 'vue'
// Basic example
const selectedFruits = ref([])
// v-for example
const selectedItems = ref([])
const options = ['Option 1', 'Option 2', 'Option 3']
// With themed checkboxes
const selectedCategories = ref([])
const categories = [
{ id: 1, name: 'Work' },
{ id: 2, name: 'Personal' },
{ id: 3, name: 'Travel' }
]
// Pre-selecting items
const preSelected = ref(['Option A', 'Option C'])
// Listening to changes
const handleSelectionChange = (selected) => {
console.log('Selected items:', selected)
}
</script>Common Patterns:
<template>
<!-- "Select All" functionality with indeterminate state -->
<div>
<Checkbox
v-model="allSelected"
v-model:indeterminate="indeterminate"
label="Select All"
@change="toggleAll"
/>
<GroupCheckbox v-model="selectedItems">
<Checkbox
v-for="item in items"
:key="item"
:label="item"
/>
</GroupCheckbox>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const items = ['Item 1', 'Item 2', 'Item 3', 'Item 4']
const selectedItems = ref([])
const allSelected = ref(false)
const indeterminate = ref(false)
watch(selectedItems, (newVal) => {
allSelected.value = newVal.length === items.length
indeterminate.value = newVal.length > 0 && newVal.length < items.length
})
const toggleAll = (checked) => {
selectedItems.value = checked ? [...items] : []
}
</script>Chip
Colorful chip component for displaying status, categories, or tags with optional clear button. Supports two types: regular (smaller, mobile-optimized) and inside-combo-selector (larger, web-optimized).
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| size | chip size variant | 'extra-small' | 'small' | 'medium' | 'large' |
'' |
| type | chip type variant | 'regular' | 'inside-combo-selector' |
'regular' |
| backgroundColor | background color (CSS value or variable) | string |
'var(--Chip-bg-red)' |
| titleTextColor | title text color (CSS value or variable) | string |
'var(--SecondaryBlack)' |
| contentTextColor | content text color (CSS value or variable) | string |
'var(--Chip-Text-Red)' |
| title | title text | string |
'' |
| content | content text | string |
'Chip Name' |
| clearable | show clear button | boolean |
true |
| customized_class | custom CSS class for complete styling override. Use if you don't want to obey dynamic color rules and want to define your own rules of color and interaction effects. | string |
'' |
Events
| Event | Description | Parameters |
|---|---|---|
| clear | triggers when clear button is clicked | — |
Slots
| Name | Description |
|---|---|
| prefix | content before chip text (e.g., icon or image). Dimensions: 12×12px for regular, 16×16px for inside-combo-selector |
| title | custom title content (overrides title prop) |
| content | custom content text (overrides content prop) |
Size Specifications
| Type | Size | Padding | Prefix/Icon Size | Clear Button Size | Title Font Class | Content Font Class |
|---|---|---|---|---|---|---|
| regular | extra-small | 2px 8px | 12×12px | 12×12px | EHR_BodyS_10_Mobile | EHR_BodyS_10_Mobile |
| regular | small | 4px 8px | 12×12px | 12×12px | EHR_BodyS_10_Mobile | EHR_BodyS_10_Mobile |
| regular | medium | 8px 8px | 12×12px | 12×12px | EHR_BodyS_10_Mobile | EHR_BodyS_10_Mobile |
| regular | large | 8px 14px | 12×12px | 12×12px | EHR_BodyS_10_Mobile | EHR_BodyS_10_Mobile |
| inside-combo-selector | extra-small | 4px 24px | 16×16px | 18×18px (with #99B6C9 bg) | EHR_BodyM_14_Web | EHR_BodyM_14_Web |
| inside-combo-selector | small | 8px 24px | 16×16px | 18×18px (with #99B6C9 bg) | EHR_BodyM_14_Web | EHR_BodyM_14_Web |
| inside-combo-selector | medium | 12px 24px | 16×16px | 18×18px (with #99B6C9 bg) | EHR_BodyM_14_Web | EHR_BodyM_14_Web |
| inside-combo-selector | large | 4px 24px | 16×16px | 18×18px (with #99B6C9 bg) | EHR_BodyM_14_Web | EHR_BodyM_14_Web |
<template>
<!-- Basic regular chip with default red theme -->
<Chip
title="Status"
content="Chip Name"
/>
<!-- Regular chip with green theme -->
<Chip
type="regular"
background-color="var(--Light-Green-Bg-Color)"
title-text-color="var(--SecondaryBlack)"
content-text-color="var(--OldVibrantGreen)"
title="Category"
content="Completed"
/>
<!-- Inside-combo-selector chip (larger, web-optimized) -->
<Chip
type="inside-combo-selector"
background-color="var(--Big-chip_BG-color)"
title-text-color="var(--TrueBlack)"
content-text-color="var(--IconGrey)"
title="Selected"
content="Option Name"
/>
<!-- With prefix slot -->
<Chip
title="Status"
content="With Icon"
>
<template #prefix>
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="10"/>
</svg>
</template>
</Chip>
<!-- Non-clearable chip -->
<Chip
title="Permanent"
content="Cannot Remove"
:clearable="false"
/>
<!-- Custom size -->
<Chip
size="large"
title="Large"
content="Big Chip"
/>
</template>
<script setup>
import { Chip } from '@vibrant-wellness/va-responsive-components-library'
</script>Theme/Behavior Notes
Dynamic Color Variables:
--chip-bg: Background color--chip-title-text-color: Title text color--chip-content-text-color: Content text color
Type Differences:
- regular: Smaller dimensions, 12×12px prefix/clear button, 4px column gap, uses EHR_BodyS_10_Mobile font class
- inside-combo-selector: Larger dimensions, 16×16px prefix, 18×18px clear button with #99B6C9 background, 9px column gap, uses EHR_BodyM_14_Web font class
Common Color Combinations:
- Red: Background
var(--Chip-bg-red), Contentvar(--Chip-Text-Red) - Green: Background
var(--Light-Green-Bg-Color), Contentvar(--OldVibrantGreen) - Yellow: Background
var(--Chip-bg-yellow), Contentvar(--Chip-text-yellow) - Blue: Background
var(--Chip-text-blue), Contentvar(--Chip-bg-blue) - Purple Light: Background
var(--Chip-bg-purple-light), Contentvar(--Chip-text-purple-light) - Orange: Background
var(--Chip-bg-orange), Contentvar(--Chip-text-orange) - Brown: Background
var(--Chip-bg-brown), Contentvar(--Chip-text-brown) - Olive Brown: Background
var(--Chip-bg-olive-brown), Contentvar(--Chip-text-olive-brown) - Dark Green: Background
var(--Chip-bg-green), Contentvar(--Chip-text-green) - Purple: Background
var(--Chip-Background-Purple), Contentvar(--Chip-Text-Purple) - Warning: Background
var(--warning-yellow-light), Contentvar(--Chip-Text-Orange) - Disabled: Background
var(--DisabledGrey_20), Contentvar(--IconGrey) - Big Chip: Background
var(--Big-chip_BG-color), Titlevar(--TrueBlack), Contentvar(--IconGrey)(works for both regular and inside-combo-selector)
DropdownMenu
Public dropdown menu primitive with nested dropdown support and responsive behavior (web vs. mobile). The companion DropdownMenuItem remains internal and is not exposed publically.
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| options | menu options tree (see schema below) | Array<Option> |
[] |
| size | menu item sizing | 'small' | 'medium' | 'large' |
'small' |
| width_type | width behavior | 'fill-whole' | 'fit-content' |
'fill-whole' |
| with_box_shadow | whether the menu container has a shadow | boolean |
true |
Option schema (recursive):
type Option = {
value: string,
label: string,
// Optional visuals (priority: slot > raw HTML > image URL handled inside items)
prefix?: string,
suffix?: string,
prefix_slot_raw_html_content?: string,
suffix_slot_raw_html_content?: string,
// Nesting
options?: Option[],
// Visual divider hint for the following item
has_divider?: boolean,
// Disabled state
disabled?: boolean,
}Behavior notes:
- On wide screens (width > 1024), nested dropdowns open as side menus on hover/click with smart positioning.
- On small screens (≤ 1024), a single column flow is used; nested levels replace the list view using an internal history stack and a back affordance.
- The component emits a click-outside event when the user clicks outside of the root dropdown.
Events
| Event | Description | Parameters |
|---|---|---|
| select-dropdown-option | emitted when an option without further nesting is selected | (value: string, in_dropdown_level: number) |
| click-outside-dropdown-menu | emitted when clicking outside the root menu | — |
Usage
<template>
<DropdownMenu
:options="options"
:size="'small'"
:width_type="'fill-whole'"
@click-outside-dropdown-menu="onOutside"
@select-dropdown-option="onSelect"
/>
</template>
<script setup>
import { DropdownMenu } from 'pns-component-library'
const options = [
{
value: 'option1',
label: 'Option 1',
options: [
{ value: 'option1.1', label: 'Option 1.1' },
{ value: 'option1.2', label: 'Option 1.2', options: [
{ value: 'option1.2.1', label: 'Option 1.2.1' },
]},
],
},
{ value: 'option2', label: 'Option 2' },
]
function onOutside(){ /* handle outside click */ }
function onSelect(value, level){ /* handle select */ }
</script>
#### Radio
Radio button component for single selection from multiple options.
##### Attributes
| Attribute | Description | Type | Default |
|-----------|-------------|------|---------|
| v-model / modelValue | binding value - the `value` of the currently selected radio button in the group | `string` | — |
| value | radio value | `string` | — |
| label | radio label | `string` | — |
| name | group name for radio group | `string` | — |
| size | radio size | `'small' \| 'medium'` | `'medium'` |
| built_in_theme | color theme | `'primary-blue' \| 'secondary-blue' \| 'red'` | `'primary-blue'` |
| customized_class | custom CSS class | `string` | — |
| disabled | whether radio is disabled | `boolean` | `false` |
| autofocus | whether to auto focus | `boolean` | `false` |
##### Events
| Event | Description | Parameters |
|-------|-------------|------------|
| change | triggers when the binding value changes | `(value: string)` |
##### Exposes
| Method | Description | Type |
|--------|-------------|------|
| focus | focus the radio | `() => void` |
| blur | blur the radio | `() => void` |
| alert | show alert message | `(message: string) => void` |
| removeAlertOrErrorEffect | clear alert/error state | `() => void` |
```vue
<!-- Radio Group -->
<Radio v-model="selected" value="option1" name="group1" label="Option 1" />
<Radio v-model="selected" value="option2" name="group1" label="Option 2" />
<!-- Single Radio -->
<Radio v-model="single" value="yes" label="Agree to terms" />Badge
Notification badge with three shape variants: small dot, large numbered circle, and text pill. Supports two built-in themes (default blue, alert red), custom colors, three positioning modes, max overflow, and disabled state.
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| content | badge content. Numbers display in a circle; strings display in a pill. Empty = dot only | string | number |
'' |
| max | maximum numeric value before showing "max+" (e.g. 99 → "99+"). Only applies to numeric content | number |
99 |
| size | badge size when no content | 'small' | 'large' |
'small' |
| position_type | badge position relative to slot content | 'standalone' | 'top-right' | 'center-right' |
'standalone' |
| customized_top_distance | top offset for top-right position (any CSS length) | string |
'' |
| customized_right_distance | right offset for top-right position (any CSS length) | string |
'' |
| theme | color theme | 'default' | 'alert' |
'default' |
| customized_theme_color | custom background color (overrides theme) | string |
'' |
| customized_text_color | custom text color (overrides default white). Useful for inverted badges (e.g. white badge on dark background) | string |
'' |
| customized_class | custom CSS class on the badge element | string |
'' |
| customized_size | custom badge diameter (e.g. '16px', '32px'). Border-radius auto-adjusts to stay round. Font size scales proportionally |
string |
'' |
| disabled | hide the badge indicator (slot content remains visible) | boolean |
false |
Shape Rules (auto-determined)
| Condition | Shape | Example |
|---|---|---|
| No content (empty) | Small dot (10px) | <Badge /> |
| Display text ≤ 2 characters | Circle (24px) | <Badge :content="5" />, <Badge :content="15" :max="9" /> → "9+" |
| Display text ≥ 3 characters | Pill (auto width) | <Badge :content="100" /> → "99+", <Badge content="New" /> |
Size Specifications
| Shape | Dimensions | Min | Max |
|---|---|---|---|
| small (dot) | 10×10px | 10×10px | — |
| large (circle) | 24×24px | 16×16px | 34×34px |
| text-badge (pill) | auto × 20px | 28px wide, 16px tall | — |
Position Types
| Position | Behavior |
|---|---|
| standalone | Inline, no positioning. No slot needed |
| top-right | Absolute at top-right corner of slotted element. Use customized_top_distance / customized_right_distance to offset |
| center-right | Inline-flex at right side of slotted element with 4px gap |
Built-in Themes
| Theme | Background | Text Color |
|---|---|---|
| default | var(--VibrantDarkBlue, #004879) |
#FFFFFF |
| alert | var(--ErrorRed, #D10000) |
#FFFFFF |
Slots
| Name | Description |
|---|---|
| default | Content to wrap with the badge. Required for top-right and center-right positions |
Usage
Standalone Badges (no slot — dot / number / text-pill)
<template>
<!-- Small dot (10px), no content -->
<Badge />
<Badge theme="alert" />
<!-- Large circle (24px), with number -->
<Badge :content="3" />
<Badge :content="3" theme="alert" />
<Badge :content="100" /> <!-- overflows → "99+" -->
<Badge :content="100" theme="alert" />
<!-- Text pill, with string content -->
<Badge content="New" />
<Badge content="New" theme="alert" />
</template>Positioned Badges (wrapped around slot content)
<template>
<!-- top-right (default offset): dot / number / text / custom offset -->
<Badge position_type="top-right">
<MyIcon />
</Badge>
<Badge position_type="top-right" :content="5">
<MyIcon />
</Badge>
<Badge position_type="top-right" content="New">
<MyIcon />
</Badge>
<Badge
position_type="top-right"
:content="7"
customized_top_distance="-4px"
customized_right_distance="-4px"
>
<MyIcon />
</Badge>
<!-- center-right inline: good for text labels + counter -->
<Badge position_type="center-right">
<span>Messages</span>
</Badge>
<Badge position_type="center-right" :content="12">
<span>Inbox</span>
</Badge>
<Badge position_type="center-right" content="New">
<span>Features</span>
</Badge>
</template>Built-in Themes + Custom Color
<template>
<Badge :content="8" /> <!-- default blue -->
<Badge :content="8" theme="alert" /> <!-- alert red -->
<Badge :content="8" customized_theme_color="#10B981" /> <!-- custom green -->
<Badge :content="8" customized_theme_color="#8B5CF6" /> <!-- custom purple -->
</template>Custom Text Color (for inverted / high-contrast badges)
<template>
<!-- White badge with dark blue text — for use on dark backgrounds -->
<Badge :content="8" customized_theme_color="#FFFFFF" customized_text_color="#004879" />
<!-- White badge with red text -->
<Badge :content="5" customized_theme_color="#FFFFFF" customized_text_color="#D10000" />
<!-- Black text on yellow pill -->
<Badge content="Hot" customized_theme_color="#FBBF24" customized_text_color="#1F2937" />
</template>Max Overflow
<template>
<Badge :content="100" /> <!-- max=99 (default) → "99+" -->
<Badge :content="15" :max="9" /> <!-- → "9+" -->
<Badge :content="1500" :max="999" /> <!-- → "999+" -->
<Badge :content="5" /> <!-- under max, shows "5" -->
</template>Custom Size
<template>
<!-- Dot (no content) — override default 10px -->
<Badge customized_size="4px" />
<Badge /> <!-- 10px default -->
<Badge customized_size="16px" />
<!-- Circle (with number) — override default 24px -->
<Badge :content="5" customized_size="16px" />
<Badge :content="5" /> <!-- 24px default -->
<Badge :content="5" customized_size="32px" />
<Badge :content="5" customized_size="40px" />
</template>Disabled (badge indicator is hidden, but slot content remains visible)
<template>
<Badge :disabled="isDisabled" :content="9" position_type="top-right">
<MyIcon />
</Badge>
<Badge :disabled="isDisabled" position_type="top-right">
<MyIcon />
</Badge>
<Badge :disabled="isDisabled" content="New" position_type="center-right">
<span>Feature</span>
</Badge>
</template>Real-world Example (patient card with standalone counter badge, from Figma)
<template>
<div class="patient-card">
<div class="patient-card-upper">
<div class="avatar">JD</div>
<div class="info">
<div class="name">Harrier Du Bois</div>
<div class="time">14 mins ago</div>
</div>
<Badge :content="3" position_type="standalone" />
</div>
<div class="message">tO tHE teQuIlA sUNsEt!!!</div>
</div>
<div class="patient-card">
<div class="patient-card-upper">
<div class="avatar">JD</div>
<div class="info">
<div class="name">Harrier Du Bois</div>
<div class="time">14 mins ago</div>
</div>
<Badge :content="100" position_type="standalone" /> <!-- → "99+" pill -->
</div>
<div class="message">tO tHE teQuIlA sUNsEt!!!</div>
</div>
</template>Interactive Components
DynamicColorResponsiveButton
Modern button with dynamic color logic and responsive behavior.
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| button_type | button style variant | 'filled' | 'outlined' | 'text' | 'elevated' |
'filled' |
| button_border_radius | CSS border-radius value | string |
'100px' |
| built_in_theme | built-in color theme | 'primary' | 'secondary' | 'tertiary' | 'primary-container' | 'secondary-container' | 'tertiary-container' |
'primary' |
| customized_theme_color | overrides built_in_theme with any CSS color (e.g., #3d5c8f, rgb(...), hsl(...), var(--Schemes-primary)) |
string |
'' |
| customized_class | custom CSS class for outer container | string |
'' |
| width_type | width mode | 'fill-whole' | 'fit-content' (auto-handles icon-only with narrow/wide) |
'fit-content' |
| button_id | explicit id used in emitted event; falls back to display_name |
string |
'' |
| display_name | button text (used when default slot empty) | string |
'button name' |
| prefix | image url for prefix when not using slot | string |
'' |
| suffix | image url for suffix when not using slot | string |
'' |
| prefix_slot_raw_html_content | raw html for prefix (when not using named slot) | string |
'' |
| suffix_slot_raw_html_content | raw html for suffix (when not using named slot) | string |
'' |
| disabled | disable interaction | boolean |
false |
| has_interaction_effect | enable hover/focus/active visual effects (mask and slight radius changes). Set to false to keep the button static (useful when embedding purely for coloring icons/slots) |
boolean |
true |
| size | size (Web/Tablet enum) | 'small' | 'medium' | 'large' | 'xlarge' |
'small' |
Events
| Event | Description | Parameters |
|---|---|---|
| click-button | emitted on button click | (button_id: string, display_name: string) |
Exposes
| Method | Description | Type |
|---|---|---|
| focus | programmatically apply focus style | () => void |
| blur | remove focus style | () => void |
Slots
| Name | Description |
|---|---|
| prefix | content before the label (e.g., icon) |
| default | label content; falls back to display_name |
| suffix | content after the label (e.g., icon) |
<template>
<DynamicColorResponsiveButton
button_id="save-changes"
display_name="Save Changes"
button_type="filled"
built_in_theme="primary"
:has_interaction_effect="true"
width_type="fit-content"
@click-button="onSave"
>
<template #prefix>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M5 12h14v2H5z"/></svg>
</template>
</DynamicColorResponsiveButton>
</template>
<script setup>
function onSave(id, name){
console.log('clicked:', id, name)
}
</script>FloatingActionButton
Mobile-only floating action button that anchors to the bottom-right of the viewport on small screens. It supports three sizes, built-in themes or a custom theme color, named slots for icons, and an optional one-level options menu. The button is automatically hidden on wide screens (when window.width > 1024).
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| display_name | button text | string |
'button' |
| size | button size | 'small' | 'medium' | 'large' |
'small' |
| build_in_theme | built-in color theme | 'primary' | 'secondary' | 'tertiary' | 'primary-container' | 'secondary-container' | 'tertiary-container' |
'primary' |
| customized_theme_color | custom theme color (overrides built_in_theme). Accepts any valid CSS color string (e.g., #3d5c8f, rgb(61, 92, 143), hsl(217, 40%, 40%), or var(--Schemes-primary)). |
string |
'' |
| customized_class | custom CSS class for the outer container | string |
'' |
| options | optional menu options (one level). Each item is { value: any, label: string, prefix?: string, suffix?: string }. prefix/suffix are icon URLs for the option. |
array |
[] |
Notes
- When
optionsis non-empty, clicking the FAB toggles the options menu. A cross button is provided to close the menu.- The component is hidden on wide screens (
width > 1024) by design to target mobile usage.customized_theme_coloroverridesbuild_in_theme. If it is not provided (empty string), thebuild_in_themecolor will be used instead.customized_theme_coloraccepts any valid CSS color value: e.g.hsl(217, 40%, 40%),#ff0000,rgb(255, 0, 0),rgba(255, 0, 0, 0.5),hsla(217, 40%, 40%, 0.5), orvar(--Schemes-primary).- When using
customized_theme_color:
- The FAB background color uses the provided value directly.
- Related colors (text color, inline SVG color via currentColor, menu cross button colors, menu child button colors, and interaction masks) are dynamically calculated based on the
customized_theme_colorto ensure contrast and visual harmony.
Events
| Event | Description | Parameters |
|---|---|---|
| floating-action-button-click | emitted when the floating action button is clicked. If options is provided, this also toggles the menu visibility. |
— |
| option-click | emitted when a menu option is clicked | (value: any) |
Slots
| Name | Description |
|---|---|
| prefix | content rendered before the button text. Commonly used for an icon. |
| default | default slot for button label content. Falls back to display_name when empty. |
| suffix | content rendered after the button text. Commonly used for an icon. |
<script setup>
const quickCreateOptions = [
{ value: 'photo', label: 'New Photo' },
{ value: 'video', label: 'New Video' },
{ value: 'doc', label: 'New Doc' },
]
const handleFabClick = () => {
console.log('FAB clicked')
}
const handleOptionClick = (val) => {
console.log('Option clicked:', val)
}
</script>
<template>
<!-- Will only be visible on small screens (hidden on width > 1024) -->
<FloatingActionButton
display_name="New"
size="small"
build_in_theme="tertiary"
:options="quickCreateOptions"
@floating-action-button-click="handleFabClick"
@option-click="handleOptionClick"
>
<template #prefix>
<!-- Example icon (uses currentColor to adapt) -->
<!-- Pros of using inline SVG:
- Inherits currentColor automatically, so it adapts to theme/text color without extra CSS
- Reacts naturally to hover/active color-mix effects used by the component
- No additional network request for external assets
Using a regular <img src="..."> is also fine if you prefer image assets;
just note that <img> won't inherit currentColor by default (you'd need pre-colored assets or additional CSS). -->
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M6 16L10 12.95L14 16L12.5 11.05L16.5 8.2H11.6L10 3L8.4 8.2H3.5L7.5 11.05L6 16ZM10 20C8.61667 20 7.31667 19.7375 6.1 19.2125C4.88333 18.6875 3.825 17.975 2.925 17.075C2.025 16.175 1.3125 15.1167 0.7875 13.9C0.2625 12.6833 0 11.3833 0 10C0 8.61667 0.2625 7.31667 0.7875 6.1C1.3125 4.88333 2.025 3.825 2.925 2.925C3.825 2.025 4.88333 1.3125 6.1 0.7875C7.31667 0.2625 8.61667 0 10 0C11.3833 0 12.6833 0.2625 13.9 0.7875C15.1167 1.3125 16.175 2.025 17.075 2.925C17.975 3.825 18.6875 4.88333 19.2125 6.1C19.7375 7.31667 20 8.61667 20 10C20 11.3833 19.7375 12.6833 19.2125 13.9C18.6875 15.1167 17.975 16.175 17.075 17.075C16.175 17.975 15.1167 18.6875 13.9 19.2125C12.6833 19.7375 11.3833 20 10 20Z" fill="currentColor"/>
</svg>
</template>
</FloatingActionButton>
<!-- Custom theme color overrides built-in theme -->
<FloatingActionButton
display_name="Custom"
size="medium"
customized_theme_color="hsl(217, 40%, 40%)"
@floating-action-button-click="handleFabClick"
/>
</template>FoldableButton
Compact action button that shows only an icon initially and expands on hover to reveal text. Ideal for space-efficient interfaces with contextual actions.
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| text | button text content that expands on hover | String |
required |
| built_in_theme | built-in color theme | 'default' | 'red' |
'default' |
| customized_class | additional CSS class for complete styling override | String |
'' |
| transition_duration | transition duration in seconds for hover expand/collapse animation | Number |
0.35 |
| disabled | disable button interaction and visual feedback | Boolean |
false |
Events
| Event | Description | Parameters |
|---|---|---|
| click | triggered when the button is clicked (only when not disabled) | (event: MouseEvent) |
Slots
| Name | Description |
|---|---|
| prefix | content displayed before the text (e.g., icon). Always visible; text expands on hover. |
<template>
<FoldableButton
text="Edit"
built_in_theme="default"
:transition_duration="0.5"
@click="handleEdit"
>
<template #prefix>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
<path d="M7 0C3.13131 0 0 3.13131 0 7C0 10.8687 3.13131 14 7 14C10.8687 14 14 10.8687 14 7C14 3.13131 10.8636 0 7 0ZM9.22727 4C9.57576 4 9.85859 4.28283 9.85859 4.63131C9.85859 4.9798 9.57576 5.26263 9.22727 5.26263C8.87879 5.26263 8.59596 4.9798 8.59596 4.63131C8.59596 4.28283 8.87879 4 9.22727 4ZM2.89394 10.2172C2.40404 10.2172 2.09596 9.73737 2.34343 9.35859L5.19192 4.92424C5.43939 4.5404 6.05051 4.5404 6.29293 4.92424L9.14141 9.35859C9.38889 9.74242 9.08081 10.2172 8.59091 10.2172H2.89394ZM11.2828 10.202H8.99495C9.41919 10.1364 9.66667 9.70707 9.43939 9.35354L8.19192 7.40909L8.84343 6.39899C9.0202 6.12626 9.4596 6.12626 9.63636 6.39899L11.6818 9.58081C11.8586 9.85354 11.6364 10.197 11.2879 10.197L11.2828 10.202Z" fill="currentColor"/>
</svg>
</template>
</FoldableButton>
</template>
<script setup>
function handleEdit(event) {
console.log('Edit button clicked', event)
}
</script>Theme/Behavior Notes
Built-in Theme Colors:
- default:
var(--TextButton_Blue, #1E4670)- primary blue - red:
var(--ErrorRed, #D10000)- error red (with hover:var(--ErrorRed_Hover, #F63B3F), active:var(--ErrorRed_Pressed, #A60B0B))
Foldable Behavior:
- Initial State: Shows only the prefix slot content in a 24×24px square
- Hover State: Expands to show text with padding (2px 8px)
- Active State: Uses 12% color mix for background
- Text Animation: Text expands with underline effect on hover (duration controlled by
transition_durationprop, default 0.35s)
Dimensions:
- Height: 24px
- Minimum width: 24px
- Max icon size: 18×18px (with 2px padding)
- Hover padding: 2px 8px
- Border radius: 4px
Interaction States:
- Hover: Background color with 8% color mix of theme color
- Active: Background color with 12% color mix of theme color
- Disabled: Gray color (
var(--ContentMediumGrey, #999)) with 12% color mix background; cursor changes tonot-allowed
Typography:
- Applied class:
EHR_TitleS_14_WM(14px title semibold weight) - Text decoration: underline on hover
FileUploader
Fully-controlled file uploader with drag-and-drop, file-size / extension validation, and a 7-state row status machine. The parent owns the files array via v-model:files and drives upload progress / status; the component renders UI and emits user-intent events (add, remove, reject, replace). Supports single and multi-file modes, filled / outlined button variants, and a read-only list mode.
Controlled by design: the component never performs the actual upload. After a user picks files, it emits
addwith new items instatus: 'uploading'andprogress: 0. Your parent code drives progress (e.g. from XHRonprogress) and eventually flips status topending-success→default(row stays visible until the user clicks ×), orpending-failure→failed→hide→hiddento play out the failure animation.
Always locate files by their stable
id, never by array index. The user may add or remove other rows mid-upload, which shifts indices and corrupts the wrong row.
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| v-model:files / files | controlled list of file items (shape below). The component never mutates it directly — it emits update:files with the next array. |
Array<FileItem> |
[] |
| title | section title above the list. Empty string hides the title row. | string |
'Upload files' |
| description | helper text under the title (e.g. size/type constraints). Empty hides the row. | string |
'' |
| drop_hint | text shown inside the drop zone. | string |
'Drag and drop your files here to add.' |
| button_text | label on the Add button. | string |
'Add File' |
| button_variant | visual style of the Add button. | 'filled' | 'outlined' |
'filled' |
| accept | native <input accept> filter (MIME / extensions). |
string |
'' |
| multiple | allow picking multiple files at once. When false, picking a new file replaces the existing one. |
boolean |
true |
| max_size | max bytes per file (0 = unlimited). Files over this are sent to @reject and not added. |
number |
0 |
| allowed_extensions | whitelist of extensions (no dot), e.g. ['pdf','docx']. Empty = accept all. |
Array<string> |
[] |
| hide_drop_zone | hide the drop area (useful for a read-only file list). | boolean |
false |
| disabled | disable the whole uploader (no picker, no drop). | boolean |
false |
| customized_class | extra class on the root element. | string |
'' |
Events
| Event | Description | Parameters |
|---|---|---|
| update:files | syncs v-model with the next files array | (files: Array<FileItem>) |
| add | new items just added by the user (status='uploading', progress=0). Start your upload here. |
(items: Array<FileItem>) |
| remove | user clicked the × on a row | ({ file, index }) |
| reject | files rejected by max_size / allowed_extensions |
(rejected: Array<{ file: File, reason: string }>) |
| replace | single-file mode only: a new file is replacing existing files. Use this to clean up the previous file (e.g. delete from server). | ({ previous: Array<FileItem>, next: Array<FileItem> }) |
File Item Shape
Parent-maintained. The component reads status and progress to drive visuals; other fields are pass-through.
| Field | Type | Notes |
|---|---|---|
| id | string | number |
Stable key for v-for (strongly recommended). |
| name | string |
Display file name. |
| size | number |
Bytes; used to render the meta line. |
| type | string |
Extension, e.g. 'png' / 'pdf'. |
| status | string |
One of: 'default', 'uploading', 'pending-success', 'pending-failure', 'failed', 'hide', 'hidden'. |
| progress | number |
0–100. Used when status is 'uploading' or 'pending-failure'. |
| error | string |
Optional label that overrides the default 'Upload Failed' text. |
| raw | File |
Native File reference (set by the component when the user picks a file). |
State Machine
| Status | Visual | Meaning |
|---|---|---|
default |
Grey row, × shown | Settled / already-uploaded item. User can click × to remove. (Use the disabled prop on the uploader for true read-only mode.) |
uploading |
Teal progress bar (#D3F1F3) filling 0 → 100% based on progress, × shown |
Upload in progress. |
pending-success |
Cool-white row (#F9FBFD), "Upload Success!" text |
Parent's short celebration hold before settling into default (row stays visible). |
pending-failure |
Red band (#F6E0E0) at progress width, × shown |
Parent detected failure; showing partial progress + × to let user retry. |
failed |
Full red row, "Upload Failed" text, × shown | Terminal failure state. |
hide |
Row slides left off screen | Transient — parent sets this just before removing. |
hidden |
Row height = 0 (fully collapsed) | Transient — equivalent to a removed row, useful if you want to keep the item in memory. |
Usage
Basic (empty state — drives a real upload)
<template>
<FileUploader
v-model:files="files"
description="Max file size is 5MB. Supported file types are PDF, docx, or jpg."
:max_size="5 * 1024 * 1024"
:allowed_extensions="['pdf','docx','jpg','jpeg','png']"
@reject="onReject"
/>
</template>
<script>
import axios from 'axios';
// IMPORTANT: always locate files by their stable `id`, never by array index.
// The user may add/remove other rows mid-upload, which shifts indices and
// corrupts the wrong row.
export default {
data() { return { files: [] }; },
watch: {
files: {
handler(list) {
list.forEach((f) => {
if (f.status === 'uploading' && !f._started) {
f._started = true;
this.doUpload(f.id);
}
});
},
deep: true,
},
},
methods: {
patch(id, changes) {
const idx = this.files.findIndex((f) => f.id === id);
if (idx === -1) return;
this.files[idx] = { ...this.files[idx], ...changes };
},
drop(id) {
this.files = this.files.filter((f) => f.id !== id);
},
async doUpload(id) {
const file = this.files.find((f) => f.id === id);
if (!file) return;
const form = new FormData();
form.append('file', file.raw);
try {
await axios.post('/api/upload', form, {
onUploadProgress: (e) => {
const pct = Math.round((e.loaded / e.total) * 100);
this.patch(id, { progress: pct });
},
});
// Show "Upload Success!" briefly, then settle into 'default'
// so the user keeps seeing what they uploaded.
this.patch(id, { status: 'pending-success' });
setTimeout(() => this.patch(id, { status: 'default' }), 1200);
} catch (err) {
// Failure: play pending-failure → failed → hide → remove animation.
this.patch(id, { status: 'pending-failure', progress: 70 });
setTimeout(() => this.patch(id, { status: 'failed' }), 800);
setTimeout(() => this.patch(id, { status: 'hide' }), 2200);
setTimeout(() => this.drop(id), 2700);
}
},
onReject(rejected) {
alert(rejected.map((r) => `${r.file.name}: ${r.reason}`).join('\n'));
},
},
};
</script>All row states (read-only showcase)
<template>
<FileUploader
v-model:files="files"
title="State showcase"
description="One row per status: default, uploading, pending-success, pending-failure, failed, hiding."
hide_drop_zone
/>
<button @click="pumpProgress">Step uploading +10%</button>
<button @click="resetStates">Reset</button>
</template>
<script>
export default {
data() {
return {
files: [
{ id:'s1', name:'default-state.pdf', size:512000, type:'pdf', status:'default' },
{ id:'s2', name:'uploading-state.pdf', size:512000, type:'pdf', status:'uploading', progress:45 },
{ id:'s3', name:'pending-success.pdf', size:512000, type:'pdf', status:'pending-success' },
{ id:'s4', name:'pending-failure.pdf', size:512000, type:'pdf', status:'pending-failure', progress:72 },
{ id:'s5', name:'failed-state.pdf', size:512000, type:'pdf', status:'failed' },
// Transient states (set by your parent during the failure animation):
// 'hide' → row slides off-screen to the left
// 'hidden' → row collapses to height 0 (fully removed)
],
};
},
methods: {
pumpProgress() {
const cur = this.files.find((f) => f.status === 'uploading');
if (!cur) return;
const idx = this.files.findIndex((f) => f.id === cur.id);
this.files[idx] = { ...cur, progress: Math.min(100, (cur.progress || 0) + 10) };
},
resetStates() {
// Re-assign the array to reset progress back to 45%, etc.
},
},
};
</script>Outlined button variant
<FileUploader
v-model:files="files"
button_variant="outlined"
description="Outlined variant — white button with dark-blue border."
@reject="onReject"
/>Two-column layout (Figma Structure)
<div style="display:flex; gap:24px; flex-wrap:wrap;">
<FileUploader
v-model:files="leftFiles"
title="Upload files"
description="Max file size is 5MB. Supported file types are PDF, docx, or jpg."
@reject="onReject"
/>
<FileUploader
v-model:files="rightFiles"
title="Upload files"
description="Max file size is 5MB. Supported file types are PDF, docx, or jpg."
button_variant="outlined"
@reject="onReject"
/>
</div>Single-file mode
When multiple="false", picking a new file replaces the existing one. Listen to @replace if you need to clean up the previous file (e.g. delete from server) — the component does not do that for you.
<FileUploader
v-model:files="files"
:multiple="false"
:allowed_extensions="['pdf','docx','jpg','jpeg','png']"
title="Profile document"
description="Only one file allowed — uploading a new one replaces the current."
@reject="onReject"
@replace="onReplace"
/>
<script>
export default {
methods: {
async onReplace({ previous /*, next */ }) {
// Clean up the previous file on the server before the new one uploads.
for (const old of previous) {
if (old.id) await axios.delete(`/api/files/${old.id}`);
}
},
},
};
</script>Disabled (read-only list)
<FileUploader
v-model:files="files"
description="Disabled uploader — drop zone is non-interactive."
disabled
/>
// data:
files = [
{ id: 'd1', name: 'Read-only.pdf', size: 1024000, type: 'pdf', status: 'default' },
];Full props reference
<FileUploader
v-model:files="files" // controlled list
title="Upload files"
description="..."
button_variant="filled" // 'filled' | 'outlined'
:multiple="true"
:max_size="5 * 1024 * 1024"
:allowed_extensions="['pdf','docx']"
:disabled="false"
:hide_drop_zone="false"
@add="onAdd"
@remove="onRemove"
@reject="onReject"
/>Feedback Components
SimplifiedNotification
Toast notification component for user feedback.
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| content | notification content | string |
— |
| build_in_theme | color theme | 'blue' | 'red' | 'yellow' | 'green' |
'blue' |
| title | notification title | string |
— |
| customized_alert_icon_src | custom icon URL | string |
— |
| customized_class | custom CSS class | string |
— |
| with_close_btn | whether to show close button | boolean |
true |
| mounted_programmatically | whether mounted programmatically | boolean |
false |
| hide_after | auto-hide after milliseconds (0 = never) | number |
0 |
Events
| Event | Description | Parameters |
|---|---|---|
| close | triggers when notification is closed | — |
Slots
| Name | Description |
|---|---|
| content | custom notification content |
<SimplifiedNotification
title="Success"
content="Operation completed successfully"
build_in_theme="green"
:hide_after="3000"
@close="handleClose"
/>Notification
Unified notification component supporting both inline (embedded in page flow) and toast (floating, programmatic) variants. Four themes: info, warning, error, and disabled. Features action buttons, close behavior, auto-hide with fade-out animation, and a static Notification.toast() method for programmatic toast creation.
Note:
Notificationis the recommended notification component for new development. It supersedesSimplifiedNotification(which is still exported for backward compatibility).
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| message | notification body text (supports HTML) | string |
'' |
| theme | color theme | 'info' | 'warning' | 'error' | 'disabled' |
'info' |
| variant | display variant | 'inline' | 'toast' |
'inline' |
| disabled | grey out the notification and disable interactions | boolean |
false |
| action_text | action button text. In inline variant, displayed inline after the message. In toast variant, displayed in the footer. Hidden if empty | string |
'' |
| with_close_btn | show close button (hidden when disabled) | boolean |
true |
| hide_after | auto-dismiss after N ms with fade-out animation. 0 means never auto-dismiss | number |
0 |
| customized_icon_src | override the default theme icon with a custom image URL | string |
'' |
| customized_class | custom CSS class | string |
'' |
Events
| Event | Description | Parameters |
|---|---|---|
| close | emitted when the close button is clicked or auto-dismiss triggers | (el: HTMLElement) |
| action | emitted when the action text is clicked (blocked in disabled state) | — |
Slots
| Name | Description |
|---|---|
| message | override the default message content |
Theme Backgrounds
| Theme | Background |
|---|---|
| info | var(--MessageBlue_BG, #E8F4FD) |
| warning | var(--warning-yellow-light, #FEF9E9) |
| error | var(--ErrorRed_BG, #FAEAEA) — message text also turns red |
| disabled | var(--DisabledGrey_20, #E0E0E0) |
Variant Differences
| Behavior | Inline | Toast |
|---|---|---|
| Width | 100% (fills parent) | Fixed 350px |
| Action placement | Inline after message text | Footer row below message |
| Message font | Inter | IBM Plex Sans Hebrew |
| Typical usage | Embedded in page flow | Programmatic floating via Notification.toast() |
Inline Usage
<template>
<!-- Basic inline notification -->
<Notification
message="This is an inline info notification"
theme="info"
action_text="Action"
@action="handleAction"
@close="handleClose"
/>
<!-- Warning -->
<Notification
message="This is an inline warning notification"
theme="warning"
action_text="Action"
/>
<!-- Error -->
<Notification
message="This is an inline error notification"
theme="error"
action_text="Action"
/>
<!-- Disabled -->
<Notification
message="This is an inline disabled notification"
theme="disabled"
action_text="Action"
:disabled="true"
/>
<!-- With custom slot content -->
<Notification theme="info">
<template #message>
<span>Custom <strong>HTML</strong> content</span>
</template>
</Notification>
<!-- Long text wraps naturally; icon, action, and close stay aligned -->
<Notification
message="This is a very long inline info notification that spans multiple lines to demonstrate how the component handles long text content. The message should wrap naturally and the icon, action button, and close button should remain properly aligned."
theme="info"
action_text="Action"
@action="handleAction"
@close="handleClose"
/>
<Notification
message="This is a very long inline warning notification that spans multiple lines to demonstrate how the component handles long text content. The message should wrap naturally and the icon, action button, and close button should remain properly aligned."
theme="warning"
action_text="Action"
/>
<Notification
message="This is a very long inline error notification that spans multiple lines to demonstrate how the component handles long text content. The message should wrap naturally and the icon, action button, and close button should remain properly aligned."
theme="error"
action_text="Action"
/>
<Notification
message="This is a very long inline disabled notification that spans multiple lines to demonstrate how the component handles long text content. The message should wrap naturally and the icon and action button should remain properly aligned."
theme="disabled"
action_text="Action"
:disabled="true"
/>
<!-- Auto-dismiss after 3 seconds with fade-out animation -->
<Notification
message="This notification will auto-dismiss after 3 seconds"
theme="info"
:hide_after="3000"
/>
<!-- No close button -->
<Notification
message="Close button hidden"
theme="info"
:with_close_btn="false"
/>
<!-- Custom icon -->
<Notification
message="Custom icon override"
theme="info"
customized_icon_src="/path/to/your/icon.svg"
/>
</template>
<script setup>
import { Notification } from '@vibrant-wellness/va-responsive-components-library'
function handleAction() {
console.log('Action clicked')
}
function handleClose() {
console.log('Notification closed')
}
</script>Toast Usage (Programmatic)
import { Notification } from '@vibrant-wellness/va-responsive-components-library'
// Basic toast — auto-dismisses after 5000ms by default
Notification.toast({
message: 'Operation completed successfully.',
theme: 'info',
})
// Toast with action button
Notification.toast({
message: 'Order submitted.',
theme: 'info',
action_text: 'View Order',
})
// Warning toast
Notification.toast({
message: 'Patient profile is missing required fields.',
theme: 'warning',
action_text: 'Fix Now',
})
// Error toast
Notification.toast({
message: 'Payment processing failed.',
theme: 'error',
})
// Disabled toast
Notification.toast({
message: 'This notification is archived.',
theme: 'disabled',
disabled: true,
})
// Custom auto-dismiss duration
Notification.toast({
message: 'Saved.',
theme: 'info',
hide_after: 2000,
})
// Never auto-dismiss (hide_after: 0)
Notification.toast({
message: 'Persistent toast — user must close manually',
theme: 'info',
hide_after: 0,
})
// Custom icon override
Notification.toast({
message: 'Custom icon toast',
theme: 'info',
customized_icon_src: '/path/to/your/icon.svg',
})
// Long-text toasts (wrap naturally inside the 350px toast width)
Notification.toast({
message: 'Your order #ORD-2024-88432 has been successfully submitted and is now being processed. You will receive a confirmation email shortly with tracking details and estimated delivery date for the test kits.',
theme: 'info',
action_text: 'View Order',
})
Notification.toast({
message: 'The patient profile for John Smith is missing required fields: date of birth and insurance ID. Please update the profile before submitting the order to avoid processing delays.',
theme: 'warning',
action_text: 'Fix Now',
})
Notification.toast({
message: 'Failed to process payment for order #ORD-2024-88432. The payment gateway returned an unexpected response (code: GATEWAY_TIMEOUT_503). Please verify the payment method and retry or contact billing support.',
theme: 'error',
action_text: 'Retry',
})
Notification.toast({
message: 'This notification has been archived and is no longer actionable. The associated order was cancelled on 2024-03-15. Please create a new order if you need to resubmit the test request for this patient.',
theme: 'disabled',
disabled: true,
})Notification.toast() Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| message | string |
— | Notification body text (supports HTML) |
| theme | string |
'info' |
'info' | 'warning' | 'error' | 'disabled' |
| action_text | string |
'' |
Action button text in toast footer |
| disabled | boolean |
false |
Grey out and disable interactions |
| hide_after | number |
5000 |
Auto-dismiss after N ms (0 = never) |
Snackbar
Bottom-positioned snackbar component with optional action button, auto-dismiss, and responsive layout. Supports both static usage and programmatic creation via FloatingSnackbar composable.
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| content | snackbar message text | string |
— |
| action | action button label (optional) | string |
— |
| padding_size | padding size | 'normal' | 'large' |
'normal' |
| with_close_icon | show close icon | boolean |
true |
| customized_class | custom CSS class | string |
— |
| mounted_programmatically | internal flag for programmatic mounting | boolean |
false |
| hide_after | auto-hide after milliseconds (0 = never, default: 3000) | number |
3000 |
Events
| Event | Description | Parameters |
|---|---|---|
| close | triggers when snackbar is closed | — |
| action | triggers when action button is clicked | — |
Slots
| Name | Description |
|---|---|
| default | custom snackbar content (overrides content prop) |
Layout Behavior
- Row Layout (default): Content on left, action button + close icon on right
- Used when action button text is ≤10 characters
- Stacked Layout (column): Content above, action button + close icon below (right-aligned)
- Automatically triggered when action button text is >10 characters
- Prevents text overflow and maintains readability
Padding Specifications
| Size | Top/Bottom | Left | Right |
|---|---|---|---|
| normal | 0px | 16px | 0px |
| large | 10px | 16px | 0px |
Note: Text content has internal padding of 14px top/bottom and 14px right for proper spacing.
Usage
<template>
<!-- Basic snackbar with action -->
<Snackbar
content="File deleted successfully"
action="Undo"
:with_close_icon="true"
/>
<!-- Snackbar without action button -->
<Snackbar
content="Changes saved"
:with_close_icon="true"
/>
<!-- Snackbar with larger padding -->
<Snackbar
content="Snackbar with larger padding"
action="Action"
padding_size="large"
/>
<!-- Snackbar with long action text (auto stacked layout) -->
<Snackbar
content="This will use column layout"
action="Long Action Button"
/>
<!-- Disable auto-hide -->
<Snackbar
content="This will not auto-hide"
action="Dismiss"
:hide_after="0"
/>
<!-- With event handlers -->
<Snackbar
content="Click action to see event"
action="Click Me"
@action="handleAction"
@close="handleClose"
/>
</template>
<script setup>
function handleAction() {
console.log('Action clicked')
}
function handleClose() {
console.log('Snackbar closed')
}
</script>FloatingSnackbar Composable
For programmatic snackbar creation (recommended approach):
import { FloatingSnackbar } from 'pns-component-library'
// Basic usage
FloatingSnackbar({
content: 'File deleted successfully',
action: 'Undo',
with_close_icon: true,
onAction: () => {
console.log('Undo clicked')
},
onClose: () => {
console.log('Snackbar closed')
}
})
// Without auto-hide
FloatingSnackbar({
content: 'This will not auto-hide',
action: 'Dismiss',
hide_after: 0
})
// With custom slot content
FloatingSnackbar({
action: 'Action',
with_close_icon: true
}, {
default: () => h('div', { style: { fontWeight: 'bold' } }, 'Custom content')
})WholePageErrorPopup
Full-page error overlay for critical error states.
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| title | error title | string |
'Error' |
| content | error message | string |
'An error has occured' |
Events
| Event | Description | Parameters |
|---|---|---|
| retry | triggers when retry button is clicked | — |
Slots
| Name | Description |
|---|---|
| content | custom error content |
<WholePageErrorPopup
title="Connection Error"
content="Unable to connect to server. Please check your internet connection."
@retry="handleRetry"
/>Overlay Components
Tooltip
Lightweight tooltip popup with up / down / left / right positioning, single- or multi-line text, and hover / click / manual triggers. Wraps any trigger element via the default slot; tooltip content can be set via the text prop or the content slot.
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| text | tooltip text content (ignored when content slot is used) |
string |
'' |
| position | placement relative to the trigger | 'up' | 'down' | 'left' | 'right' |
'up' |
| multi_line | wrap to multiple lines (max-width 340px) instead of single-line ellipsis (max-width 280px) | boolean |
false |
| trigger | how the tooltip is shown — hover, click-to-toggle (closes on outside click), or manual via visible |
'hover' | 'click' | 'manual' |
'hover' |
| visible | manual visibility control; only applied when trigger === 'manual' |
boolean |
false |
| disabled | when true, the tooltip popup is never rendered |
boolean |
false |
Slots
| Name | Description |
|---|---|
| default | the trigger element the tooltip wraps |
| content | custom tooltip body (overrides text) |
<Tooltip text="Save changes" position="up">
<button>Save</button>
</Tooltip>
<Tooltip multi_line position="right" trigger="click">
<button>Details</button>
<template #content>
<strong>Patient:</strong> Jane Doe<br />
DOB 01/02/1985 — last visit 2 days ago.
</template>
</Tooltip>Dialog
Teleported modal dialog with v-model open state, slide-up enter/exit animation, body scroll lock, Escape-to-close, focus restoration, and three visual types (primary, secondary, error). Supports a structured footer button API as well as legacy primary_button_text / secondary_button_text props.
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| v-model / modelValue | open state | boolean |
required |
| title | header title text | string |
'' |
| titleTag | HTML tag for the title element | 'div' | 'h1' | 'h2' | 'h3' | 'h4' | 'p' | 'span' |
'div' |
| titleVariant | title size preset | 'small' | 'medium' | 'large' |
'medium' |
| description | optional description rendered above the slot content | string |
'' |
| dialogType | visual variant — also accepts legacy aliases form/modal/general/default |
'primary' | 'secondary' | 'error' |
'secondary' |
| dialogSize | width preset (xsmall is an alias for extra-small) |
'extra-small' | 'small' | 'medium' | 'large' |
'small' |
| headerIconSrc | custom header icon path (defaults to a type-appropriate built-in icon) | string |
'' |
| headerIconWidth | width of the header icon container; accepts a number (px) or any CSS length string | string | number |
'24px' |
| headerIconHeight | height of the header icon container; accepts a number (px) or any CSS length string | string | number |
'24px' |
| hideHeaderIcon | hide the header icon entirely | boolean |
false |
| showClose | show the top-right close button | boolean |
true |
| closeOnOverlay | close the dialog when the backdrop is clicked | boolean |
true |
| primary_button_text | shortcut to render a single filled primary action in the footer | string |
'' |
| secondary_button_text | shortcut to render a leading text-style secondary action in the footer | string |
'' |
| footerLeadingButtonText | text for a structured leading footer action | string |
'' |
| footerLeadingButtonStyle | variant for the leading footer action | 'filled' | 'outlined' | 'text' |
'text' |
| footerButtons | structured footer actions (max 3); each item: { key?, text, variant? } |
Array<{key?: string, text: string, variant?: 'filled' | 'outlined' | 'text'}> |
[] |
Events
| Event | Description | Parameters |
|---|---|---|
| update:modelValue | sync open state | boolean |
| close | dialog was closed (overlay click, close button, Escape, or programmatic) | — |
| primary-action | the legacy primary footer button was clicked | — |
| secondary-action | the legacy secondary footer button was clicked (also closes the dialog) | — |
| footer-action | a structured footerButtons action was clicked |
key |
Slots
| Name | Description |
|---|---|
| default | dialog body content (rendered inside the scrollable region) |
| header-icon | replace the default header icon |
| footer | replace the entire footer area (overrides built-in button layout) |
<Dialog
v-model="isOpen"
title="Confirm deletion"
description="This will permanently remove the record."
dialogType="error"
dialogSize="extra-small"
:footerButtons="[
{ key: 'cancel', text: 'Cancel', variant: 'text' },
{ key: 'delete', text: 'Delete', variant: 'filled' },
]"
@footer-action="onFooterAction"
@close="isOpen = false"
/>Navigation & Status Components
StepAccordion
Multi-step accordion supporting two modes: a multi-step flow (when steps is provided) where one step is expanded at a time, and a legacy single-step accordion (when steps is empty). Renders a built-in indicator (step number / completed check / error icon), title, body slot, and a Next/Previous/Done footer driven by the current step's state.
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| steps | step definitions — each: { id?, title, stepLabel?, state?: 'notstart' | 'ongoing' | 'completed' | 'error' }. When empty, the legacy single-step API is used. |
Array |
[] |
| title | title for legacy single-step mode | string |
'Title of accordion' |
| step | step number for legacy single-step mode | string | number |
1 |
| state | state for legacy single-step mode | 'notstart' | 'ongoing' | 'completed' | 'error' |
'notstart' |
| primaryButtonText | label for the primary footer action (legacy mode) | string |
'Button' |
| secondaryButtonText | label for the secondary footer action (legacy mode) | string |
'Cancel' |
| hideSecondaryAction | hide the secondary footer action | boolean |
false |
| expanded | controlled expanded state (legacy mode); supports v-model:expanded | boolean |
undefined |
| defaultExpanded | initial expanded state when uncontrolled (legacy mode) | boolean |
false |
| currentStep | controlled current step index (multi-step); supports v-model:currentStep | number |
undefined |
| defaultStep | initial current step index when uncontrolled | number |
0 |
| beforeNext | async/sync guard called before advancing; return false (or a rejected Promise) to block |
Function |
undefined |
| collapsible | allow the active step header to toggle collapse | boolean |
true |
| nextButtonText | label for the Next button | string |
'Next' |
| prevButtonText | label for the Previous button | string |
'Previous' |
| finishButtonText | label for the final-step button | string |
'Done' |
| hidePrevButton | hide the Previous button | boolean |
false |
Events
| Event | Description | Parameters |
|---|---|---|
| update:expanded | sync expanded state (legacy mode) | boolean |
| toggle | header was clicked | expanded: boolean |
| update:currentStep | sync current step index | number |
| change | current step changed | { currentStep: number, previousStep: number } |
| step-change | alias for change |
same as change |
| next | Next button was clicked (after beforeNext resolves) |
currentStep |
| prev | Previous button was clicked | currentStep |
| primary-action | the legacy primary footer button was clicked | — |
| secondary-action | the legacy secondary footer button was clicked | — |
Slots
| Name | Description |
|---|---|
| header | replace the header (gets step, index, state, is-current, is-expanded, go-to-step) |
| default | step body (gets step, index, state, next, prev, go-to-step) |
| step-{id-or-index} | per-step body override |
| footer | replace the entire footer (gets step, index, is-first, is-last, next, prev, go-to-step) |
<StepAccordion
v-model:currentStep="step"
:steps="[
{ id: 'patient', title: 'Patient info', state: 'completed' },
{ id: 'insurance',title: 'Insurance', state: 'ongoing' },
{ id: 'review', title: 'Review & submit',state: 'notstart' },
]"
:beforeNext="validateCurrentStep"
@next="onNext"
>
<template #step-patient>
<PatientForm v-model="patient" />
</template>
<template #step-insurance>
<InsuranceForm v-model="insurance" />
</template>
<template #step-review>
<ReviewSummary :patient="patient" :insurance="insurance" />
</template>
</StepAccordion>ProgressIndicator
Horizontal or vertical step tracker. Each step renders a status icon (completed check, current half-fill, dashed upcoming, error exclamation, or dashed-grey disabled) plus a name and optional description, joined by a colored connector line.
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| steps | step definitions — each: { id?, label, description?, status } |
Array<{id?: string | number, label: string, description?: string, status: 'completed' | 'current' | 'upcoming' | 'error' | 'disabled'}> |
required |
| orientation | layout orientation | 'horizontal' | 'vertical' |
'horizontal' |
| show_description | show the secondary description line under each step | boolean |
true |
| show_number | prefix the step name with 1., 2., … (the status icon glyph stays unchanged) |
boolean |
true |
<ProgressIndicator
:steps="[
{ label: 'Account', description: 'Created', status: 'completed' },
{ label: 'Profile', description: 'In progress', status: 'current' },
{ label: 'Confirm', description: 'Pending', status: 'upcoming' },
]"
/>Data Display Components
Pagination
Three-section pagination control: a "Showing X to Y of Z" summary, a centered page-number row with prev/next chevrons (always 5 page numbers max with ellipses around the current page), and a per-page SingleSelector on the right.
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| total_items | total number of items across all pages | number |
required (0) |
| current_page | currently selected page (1-based); supports v-model:current_page | number |
1 |
| per_page | items per page; supports v-model:per_page | number |
5 |
| per_page_options | options shown in the per-page dropdown | Array<number> |
[5, 10, 15, 20] |
| item_label | noun used in the summary text (e.g. 'patients', 'results') |
string |
'items' |
| show_summary | show the left-hand summary text | boolean |
true |
| show_per_page | show the right-hand per-page dropdown | boolean |
true |
| disabled | disable the entire control | boolean |
false |
Events
| Event | Description | Parameters |
|---|---|---|
| update:current_page | current page changed | number |
| update:per_page | per-page count changed | number |
| change | either current page or per-page changed | { current_page, per_page } |
<Pagination
v-model:current_page="page"
v-model:per_page="perPage"
:total_items="patients.length"
item_label="patients"
/>Repeater
Data table with column-based layout, fixed/flex/percent/fr column widths, optional header band, optional row selection, optional expandable rows, configurable size presets, and built-in Pagination integration (client-side or server-side). Per-cell content is fully overridable via scoped slots.
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| columns | column definitions — each: { key, label, align?: 'left' | 'center' | 'right', width?, description_key?, value?: (row) => any } |
Array |
required |
| rows | row data objects | Array |
[] |
| row_key | property name used for row identity (selection / expansion / v-for) |
string |
'id' |
| title | optional header band title | string |
'' |
| description | optional header band description | string |
'' |
| size | row height + cell padding preset | 'extra_large' | 'large' | 'medium' | 'small' | 'extra_small' |
'large' |
| selectable | render a leading checkbox column with select-all in the header | boolean |
false |
| selected_ids | currently selected row ids; supports v-model:selected_ids | Array |
[] |
| expandable | render a leading chevron column; expanded content is provided via the #expanded-content slot |
boolean |
false |
| expanded_ids | currently expanded row ids; supports v-model:expanded_ids | Array |
[] |
| show_pagination | show the built-in Pagination footer |
boolean |
false |
| paginate_client_side | when true, the component slices rows itself; when false, parent supplies the slice and total_items |
boolean |
true |
| current_page | current page (server-side mode); supports v-model:current_page | number |
1 |
| per_page | items per page; supports v-model:per_page | number |
10 |
| per_page_options | per-page dropdown options | Array<number> |
[5, 10, 15, 20] |
| total_items | total items (required for server-side pagination) | number |
null |
| item_label | noun used in the pagination summary | string |
'items' |
| show_summary | show the pagination summary | boolean |
true |
| show_per_page | show the pagination per-page dropdown | boolean |
true |
| empty_text | text rendered when rows is empty |
string |
'No data' |
| disabled | disable selection, expansion, and pagination | boolean |
false |
| hover_color | row hover background color | string |
'rgba(0, 72, 121, 0.08)' |
| vertical_divider | column divider style | 'solid' | 'dashed' | 'none' |
'dashed' |
| horizontal_divider | row divider style | 'solid' | 'dashed' | 'none' |
'dashed' |
| column_widths | positional widths matching columns 1:1 — accepts numbers (px), '<n>px', '<n>%', '<n>fr', 'auto', or null to fall back to col.width. The expand and checkbox columns are always 40px and are NOT part of this array. |
Array |
[] |
Events
| Event | Description | Parameters |
|---|---|---|
| update:selected_ids | selection changed | Array |
| update:expanded_ids | expansion changed | Array |
| update:current_page | current page changed | number |
| update:per_page | per-page count changed | number |
| change | pagination changed | { current_page, per_page } |
Slots
| Name | Description |
|---|---|
| header | replace the entire header band |
| header-{col.key} | replace a single column header |
| cell-{col.key} | replace a single cell (gets row, column, row-index, value) |
| expanded-content | content rendered when a row is expanded (gets row, row-index) |
| empty | replace the empty state |
<Repeater
v-model:selected_ids="selected"
v-model:expanded_ids="expanded"
:columns="[
{ key: 'name', label: 'Name', align: 'left' },
{ key: 'status', label: 'Status', align: 'center' },
{ key: 'amount', label: 'Amount', align: 'right', width: '120px' },
]"
:rows="rows"
selectable
expandable
show_pagination
size="medium"
>
<template #cell-status="{ value }">
<Chip :title="value" />
</template>
<template #expanded-content="{ row }">
<PatientDetail :patient="row" />
</template>
</Repeater>Additional Feedback Components
InlineNotification
Inline notification banner with info / warning / error / disabled themes, optional action text, and an optional close button. Renders inline within parent layout (no overlay, no auto-dismiss). For toast/floating presentations, use Notification or the FloatingNotification composable instead.
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
| message | message text (rendered as HTML via v-html) |
string |
'' |
| theme | visual theme | 'info' | 'warning' | 'error' | 'disabled' |
'info' |
| action_text | optional inline action link (right side of the message) | string |
'' |
| with_close_btn | show the close button | boolean |
true |
| customized_icon_src | override the leading icon | string |
'' |
| customized_class | extra class appended to the root element | string |
'' |
Events
| Event | Description | Parameters |
|---|---|---|
| action | action text was clicked | — |
| close | close button was clicked | — |
Slots
| Name | Description |
|---|---|
| default | custom message body (overrides message) |
<InlineNotification
theme="warning"
message="Verify the patient's insurance before continuing."
action_text="Verify now"
@action="goToVerify"
@close="dismiss"
/>🔧 Composables
FloatingNotification
Programmatically create floating notifications.
Parameters
| Parameter | Description | Type | Default |
|---|---|---|---|
| type | notification type | 'success' | 'error' | 'warning' | 'info' |
'info' |
| message | notification message | string |
— |
| duration | auto-hide duration in milliseconds | number |
3000 |
| title | notification title | string |
— |
Returns
| Type | Description |
|---|---|
void |
Creates and displays the notification |
import { FloatingNotification } from 'pns-component-library/composables'
// Show notification
FloatingNotification({
type: 'success',
message: 'Operation completed!',
duration: 3000
})useWindowSize
Reactive window size tracking composable.
Returns
| Property | Description | Type |
|---|---|---|
| width | current window width | Ref<number> |
| height | current window height | Ref<number> |
import { useWindowSize } from 'pns-component-library/composables'
const { width, height } = useWindowSize()
// Use in template or computed
watchEffect(() => {
console.log(`Window size: ${width.value}x${height.value}`)
})🎨 Directives
v-component-loading
Add loading state to any component with overlay spinner.
Usage
| Binding | Description | Type |
|---|---|---|
| value | whether to show loading state | boolean |
<template>
<div v-component-loading="isLoading">
Content here
</div>
</template>
<script setup>
import { ref } from 'vue'
const isLoading = ref(false)
// Toggle loading
const startLoading = () => {
isLoading.value = true
setTimeout(() => {
isLoading.value = false
}, 2000)
}
</script>v-whole-page-loading
Full-page loading overlay that covers the entire viewport.
Usage
| Binding | Description | Type |
|---|---|---|
| value | whether to show full-page loading | boolean |
<template>
<div v-whole-page-loading="isPageLoading">
App content
</div>
</template>
<script setup>
import { ref } from 'vue'
const isPageLoading = ref(false)
// Show page loading
const loadPage = async () => {
isPageLoading.value = true
try {
await fetchData()
} finally {
isPageLoading.value = false
}
}
</script>🎨 Style Guide
Color Variables
The library provides predefined CSS custom properties for consistent theming across all components.
Base Colors
| Variable | Value | Usage |
|---|---|---|
--TrueBlack |
#333333 |
Primary text, headings |
--SecondaryBlack |
#666666 |
Secondary text, descriptions |
--ContentMediumGrey |
#999999 |
Disabled content, placeholders |
--DividerGray |
#C4C4C4 |
Dividers, borders |
--DisabledGrey_20 |
#E0E0E0 |
Disabled backgrounds |
--Gray-White |
#F2F2F2 |
Subtle backgrounds |
--F9_Bg-gray |
#F9F9F9 |
Page background |
--White |
#FFFFFF |
Cards, containers |
--IconGrey |
#374957 |
Icon fill |
--TextButton_Blue |
#1E4670 |
Text links, buttons |
--VibrantDarkBlue |
#004879 |
Primary brand color |
Chip Colors
| Variable | Value | Usage |
|---|---|---|
--Chip-Background-Purple |
#E1D3F1 |
Purple chip background |
--Chip-Text-Purple |
#6F4999 |
Purple chip text |
--Chip-bg-red |
#FDEAEA |
Red chip background |
--Chip-Text-Red |
#B93A3A |
Red chip text |
--Chip-bg-yellow |
#FFF8D9 |
Yellow chip background |
--Chip-text-yellow |
#AD8A1F |
Yellow chip text |
--Chip-bg-blue |
#E6F3FF |
Blue chip background |
--Chip-text-blue |
#246A99 |
Blue chip text |
--Chip-bg-purple-light |
#F3E9FF |
Light purple chip background |
--Chip-text-purple-light |
#6E45A1 |
Light purple chip text |
--Chip-bg-orange |
#FFF1E6 |
Orange chip background |
--Chip-text-orange |
#B86527 |
Orange chip text |
--Chip-bg-brown |
#F7EFE6 |
Brown chip background |
--Chip-text-brown |
#8A6329 |
Brown chip text |
--Chip-bg-olive-brown |
#F5F2E0 |
Olive brown chip background |
--Chip-text-olive-brown |
#7A6A2A |
Olive brown chip text |
--Chip-bg-green |
#F0FFF4 |
Green chip background |
--Chip-text-green |
#38A169 |
Green chip text |
--Chip-Green-Grey |
#C4DCDF |
Green grey chip |
--Chip-Text-Orange |
#F06539 |
Orange chip text |
--Big-chip_BG-color |
#E6EDF2 |
Large chip background |
State Colors
| Variable | Value | Usage |
|---|---|---|
--OldVibrantGreen |
#25B8C2 |
Success states |
--NewVibrantGreen |
#8BBDC7 |
Success hover |
--ErrorRed_Pressed |
#A60B0B |
Error pressed |
--ErrorRed |
#D10000 |
Error states |
--ErrorRed_Hover |
#F63B3B |
Error hover |
--Pending-Yellow |
#FEB52E |
Warning states |
--Success-Text-Green |
#45960D |
Success text |
--New_Library_PressBlue |
#5587BC |
Pressed blue |
--New_Library_CheckGreen |
#50C5CE |
Checkbox green |
--New_Library_HoverGreen |
#20A7C1 |
Hover green |
--New_Button_SkyBlue |
#81B0E1 |
Sky blue buttons |
Background Colors
| Variable | Value | Usage |
|---|---|---|
--Warm-White |
#FDF9F6 |
Warm section backgrounds |
--Cool-White |
#F9FBFD |
Cool section backgrounds |
--MessageBlue_BG |
#E8F4FD |
Info message backgrounds |
--ErrorRed_BG |
#FAEAEA |
Error message backgrounds |
--error-red-medium-bg |
#F6E0E0 |
Medium error backgrounds |
--warning-yellow-medium-bg |
#F7EECE |
Medium warning backgrounds |
--warning-yellow-light |
#FEF9E9 |
Light warning backgrounds |
--Chat-Bubble-Blue |
#E4F3F5 |
Chat bubbles |
--Success-Green-Background |
#F1F9F5 |
Success backgrounds |
--Special-Section-Green |
#D5F5E0 |
Special section green |
--Tab-blue-bg_Light |
#F0F7FA |
Tab backgrounds |
--Tab-blue-bg_Light_hover |
#D3E5F1 |
Tab hover |
--text-highlight-blue |
#0089EA |
Text highlights |
--Bg_text-highlibht-blue |
#E9F2FD |
Text highlight bg |
--Special-Section-Pink-bg |
#FFF6MA |
Pink section backgrounds |
--Special-section-pink |
#EC4899 |
Pink accents |
--Normal-Orange |
#FF956C |
Orange accents |
--add-on-card-bg-orange |
#FFFAF7 |
Add-on card orange bg |
--add-on-card-stroke-orange |
#F1D5C6 |
Add-on card orange stroke |
--add-on-card-bg-Purple |
#FCF9FF |
Add-on card purple bg |
--add-on-card-stroke-purple |
#C8CAED |
Add-on card purple stroke |
--Light-Green-Bg-Color |
#EBFFF8 |
Light green backgrounds |
--BlueIconBg_Hover |
#C5E3E7 |
Icon hover backgrounds |
Gradients
| Variable | Value | Usage |
|---|---|---|
--Motion-Basic |
linear-gradient(180deg, #004879 0%, #8BBDC7 100%) |
Motion backgrounds |
--Section-background-blue-purple-gradient |
linear-gradient(90deg, rgba(230, 220, 255, 0.28) 50.65%, rgba(7, 136, 187, 0.04) 100%) |
Section gradients |
--Bg_Gradiation_blue_alt |
linear-gradient(89deg, #EAEEF1 0.83%, #EFF6FE 99.91%) |
Blue gradient |
--AI-thinking-gradient |
linear-gradient(90deg, #E3E3E3 0%, #AFC3CE 52.16%, #004879 104.31%) |
AI thinking states |
Typography Classes
Global typography utility classes available throughout the library. All classes use the Inter font family.
Display Styles
| Class | Size | Weight | Line Height | Platform |
|---|---|---|---|---|
EHR_DisplayLarge_56px_SemiBold |
56px | 600 | 64px | Web |
EHR_DisplayLarge_42M_Mobile |
42px | 500 | 48px | Mobile |
EHR_MediumDisplay_42_Web_Regular |
42px | 400 | 54px | Web |
EHR_MediumDisplay_32_Mobile |
32px | 500 | 40px | Mobile |
EHR_MediumDisplay_32_Mobile_Thin |
32px | 300 | 40px | Mobile |
EHR_SmallDisplay_36_Web |
36px | 400 | 48px | Web |
EHR_SmallDisplay_30_Mobile |
30px | 400 | 38px | Mobile |
EHR_XSDisplay_28_Web |
28px | 500 | 40px | Web |
EHR_XSDisplay_24_Mobile |
24px | 400 | 36px | Mobile |
Headline Styles
| Class | Size | Weight | Line Height | Platform |
|---|---|---|---|---|
EHR_HeadlineL_30_Web |
30px | 500 | 38px | Web |
EHR_Headline_large_28px_regular |
28px | 400 | 36px | Web |
EHR_headline_20pxR_web |
20px | 400 | 28px | Web |
EHR_headline_20pxM_web |
20px | 500 | 28px | Web |
EHR_HeadlineM_20B_Web |
20px | 600 | 28px | Web |
EHR_HeadlineM_16B_Mobile |
16px | 500 | 28px | Mobile |
EHR_HeadlineM_16R_Mobile |
16px | 400 | 28px | Mobile |
EHR_HeadlineS_16_Web |
16px | 400 | 24px | Web |
EHR_Headline_14px_regular_mobile |
14px | 400 | 20px | Mobile |
Title Styles
| Class | Size | Weight | Line Height | Letter Spacing | Platform |
|---|---|---|---|---|---|
EHR_TitleL_22_Web |
22px | 500 | 24px | — | Web |
EHR_TitleL_18_Mobile |
18px | 500 | 22px | — | Mobile |
EHR_TitleM_16B_WM |
16px | 700 | 24px | 0.15px | Web/Mobile |
EHR_TitleM_16INT_WM |
16px | 600 | 24px | 0.15px | Web/Mobile |
EHR_TitleM_16_WM |
16px | 500 | 24px | 0.15px | Web/Mobile |
EHR_InputText_16_WM |
16px | 400 | 20px | 0.15px | Web/Mobile |
EHR_TitleS_14_WM |
14px | 500 | 20px | 0.1px | Web/Mobile |
Body Text Styles
| Class | Size | Weight | Line Height | Letter Spacing | Platform |
|---|---|---|---|---|---|
EHR_TextL_16_Web |
16px | 400 | 24px | 0.5px | Web |
EHR_TextL_16_Mobile |
16px | 400 | 24px | 0.25px | Mobile |
EHR_BodyM_14_Web |
14px | 400 | 20px | 0.25px | Web |
EHR_BodyM_12_Mobile |
12px | 400 | 16px | 0.2px | Mobile |
EHR_BodyM_14_Mobile |
14px | 400 | 20px | 0.25px | Mobile |
EHR_BodyS_12_Web |
12px | 400 | 16px | 0.4px | Web |
EHR_BodyS_10_Mobile |
10px | 400 | 12px | 0.2px | Mobile |
EHR_BodyXS_8_Mobile |
8px | 400 | 10px | 0.2px | Mobile |
EHR_MessageText_14_Web |
14px | 300 | 18px | — | Web |
EHR_Body_18px_medium_mobile |
18px | 500 | 22px | 0.2px | Mobile |
Label Styles
| Class | Size | Weight | Line Height | Letter Spacing | Platform |
|---|---|---|---|---|---|
EHR_LableXS_11_Web |
11px | 500 | 16px | 0.5px | Web |
EHR_LableXS_10_Mobile |
10px | 500 | 12px | 0.25px | Mobile |
Usage Example:
<template>
<!-- Display styles -->
<h1 class="EHR_DisplayLarge_56px_SemiBold">Page Title</h1>
<h2 class="EHR_MediumDisplay_42_Web_Regular">Section Title</h2>
<!-- Body text -->
<p class="EHR_BodyM_14_Web">Body text content goes here.</p>
<p class="EHR_BodyS_10_Mobile">Small caption text</p>
<!-- Labels -->
<span class="EHR_LableXS_11_Web">LABEL TEXT</span>
</template>🎨 Theming
The library provides multiple built-in themes:
- Green (default) - Primary green theme
- Primary Blue - Professional blue theme
- Secondary Blue - Light blue accent theme
- Red - Error/warning red theme
<Checkbox built_in_theme="primary-blue" />
<DynamicColorResponsiveButton button_type="filled" built_in_theme="primary" />📱 Responsive Design
Components automatically adapt to different screen sizes:
- Desktop: Full feature set with optimal spacing
- Mobile: Compact layouts with touch-friendly interactions
- Breakpoint: 767px for mobile/desktop switching
♿ Accessibility
All components include:
- ARIA attributes for screen readers
- Keyboard navigation support
- Focus management
- High contrast support
- Semantic HTML structure
🛠 Development
Project Setup
npm installDevelopment Server
npm run devBuild for Production
npm run buildComponent Documentation
Visit the demo pages to see all components in action with interactive examples.
📄 License
MIT License - see LICENSE file for details.
🤝 Contributing
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests and documentation
- Submit a pull request
📞 Support
For questions and support, please open an issue on the GitHub repository.
Built with ❤️ using Vue 3 and modern web technologies.