cetera-vue-utils
v0.3.61
Published
Vue 3 components, composables and utilities
Maintainers
Readme
cetera-vue-utils
Components, composables, and utilities for Vue 3.
Table of Contents
Components
- Accordion
- Button
- ButtonLink
- Checkbox
- ConfirmAction
- DataTable
- Dialog
- Draggable
- InlineLoading
- Input
- InputDate
- InputMonth
- InputFile
- InputFileImage
- InputNumber
- InputPassword
- InputSearch
- InputTime
- Menu
- MultiSelect
- Notifications
- Pagination
- Popover
- Radiobutton
- Select
- Skeleton
- Slider
- Spinner
- Tabs
- Textarea
- Toggle
Composables
Installation
npm install cetera-vue-utilsThe package requires Vue as a peer dependency:
npm install vueNuxt
Add the module to nuxt.config.ts:
export default defineNuxtConfig({
modules: ['cetera-vue-utils/nuxt'],
})The module automatically:
- registers all components globally
- adds composables to auto-imports:
useNotify,useFormElements,useErrors,useDataLoad
Since the components use Tailwind CSS classes, add a @source directive to your CSS entry point so Tailwind scans the library's dist files. Also import the library CSS (required for InputDate and InputTime datepicker styles):
/* app/assets/css/main.css */
@import "tailwindcss";
@import "cetera-vue-utils/style.css";
@source "../../../node_modules/cetera-vue-utils/dist/";Note:
cetera-vue-utils/style.cssresolves todist/style.css, which contains the compiled component CSS including the@vuepic/vue-datepickerbase styles needed byInputDateandInputTime. The@sourcedirective ensures your project's Tailwind generates theprimary-*and other utility classes used by the components.
The Notifications component must be placed manually in the root layout:
<!-- app/app.vue -->
<script setup>
import { Notifications } from 'cetera-vue-utils'
</script>
<template>
<Notifications />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>Components
Notifications
Displays active notifications. Place once in the root layout.
Styled with Tailwind CSS — requires Tailwind to be configured in your project.
<script setup>
import { Notifications, useNotify } from 'cetera-vue-utils'
const notify = useNotify()
</script>
<template>
<Notifications />
<button @click="notify.success('Done!')">Save</button>
</template>Notifications appear in the top-right corner with enter/leave animations.
Spinner
Loading overlay on top of content. Requires position: relative on the parent element.
<template>
<div class="relative">
<Spinner :isLoading="loading" size="lg" />
<p>Content</p>
</div>
</template>
<script setup>
import { Spinner } from 'cetera-vue-utils'
</script>| Prop | Type | Default | Description |
|---|---|---|---|
| isLoading | boolean | true | Show spinner |
| size | 'xs' \| 'sm' \| 'base' \| 'lg' \| 'xl' | 'xl' | Icon size |
InlineLoading
Inline loading indicator with optional text.
<template>
<InlineLoading :isLoading="loading" loadingText="Saving..." />
</template>
<script setup>
import { InlineLoading } from 'cetera-vue-utils'
</script>| Prop | Type | Default | Description |
|---|---|---|---|
| isLoading | boolean | true | Show indicator |
| loadingText | string | 'Loading ...' | Text next to icon |
Button
Button with style variants, sizes, and loading state.
<template>
<Button label="Save" @click="save" />
<Button label="Loading" :loading="true" />
<Button label="Secondary" severity="secondary" />
<Button label="Delete" severity="danger" :icon="TrashIcon" />
<Button label="Outlined" outlined />
<Button label="Small" size="sm" />
<Button label="Large" size="lg" />
</template>
<script setup>
import { Button } from 'cetera-vue-utils'
import { TrashIcon } from '@heroicons/vue/24/outline'
</script>| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | — | Button text |
| severity | 'primary' \| 'secondary' \| 'danger' | 'primary' | Color variant |
| outlined | boolean | false | Outline style (transparent background) |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Button size |
| loading | boolean | false | Spinner + disabled state |
| icon | FunctionalComponent | — | Icon (hidden when loading) |
| Slot | Description |
|---|---|
| default | Button content (alternative to label prop) |
All other attrs (type, disabled, etc.) are forwarded to <button>.
ButtonLink
Button styled as a link (<a>) with the same style variants.
<template>
<ButtonLink href="/dashboard">Dashboard</ButtonLink>
<ButtonLink href="https://example.com" target="_blank" severity="secondary">External link</ButtonLink>
<ButtonLink href="/docs" outlined size="sm">Docs</ButtonLink>
</template>
<script setup>
import { ButtonLink } from 'cetera-vue-utils'
</script>| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | — | Text (alternative to slot) |
| severity | 'primary' \| 'secondary' \| 'danger' | 'primary' | Color variant |
| outlined | boolean | false | Outline style |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Size |
| icon | FunctionalComponent | — | Icon |
| Slot | Description |
|---|---|
| default | Link content (alternative to label prop) |
All attrs (href, target, rel, etc.) are forwarded to <a>.
Dialog
Modal dialog based on @headlessui/vue with animation and backdrop.
<template>
<Button label="Open" @click="visible = true" />
<Dialog v-model:visible="visible" header="Title">
<p>Dialog content</p>
<template #footer>
<Button label="Cancel" severity="secondary" @click="visible = false" />
<Button label="Save" @click="save" />
</template>
</Dialog>
</template>
<script setup>
import { ref } from 'vue'
import { Dialog, Button } from 'cetera-vue-utils'
const visible = ref(false)
</script>Custom header via slot:
<Dialog v-model:visible="visible">
<template #header>
<h2 class="text-xl font-bold text-red-600">Warning!</h2>
</template>
Content
</Dialog>| Prop | Type | Description |
|---|---|---|
| visible (v-model) | boolean | Dialog visibility |
| header | string | Title (alternative to #header slot) |
| contentClass | string | Extra class for the content wrapper <div> |
| Slot | Description |
|---|---|
| default | Dialog content |
| header | Custom header (overrides header prop) |
| footer | Action buttons (right-aligned) |
Attrs (e.g. class) are forwarded to DialogPanel — use to override width:
<Dialog v-model:visible="visible" header="Wide dialog" class="max-w-3xl">...</Dialog>ConfirmAction
Confirmation dialog. Controlled via ref and the confirm(id) method.
<template>
<Button label="Delete" severity="danger" @click="confirmRef?.confirm(item.id)" />
<ConfirmAction ref="confirmRef" danger @confirm="onDelete">
Are you sure you want to delete this record?
</ConfirmAction>
</template>
<script setup>
import { ref } from 'vue'
import { ConfirmAction, Button } from 'cetera-vue-utils'
const confirmRef = ref()
const onDelete = (id) => {
// delete record with id
}
</script>| Prop | Type | Description |
|---|---|---|
| danger | boolean | Renders the confirm button in red |
| Event | Payload | Description |
|---|---|---|
| confirm | id: any | Fired after user confirms |
| Method | Description |
|---|---|
| confirm(id) | Opens the dialog and stores id to pass to the event |
| Slot | Description |
|---|---|
| default | Confirmation message body |
Input
Wrapper around an input field with label, validation errors, and helper text support.
<template>
<Input label="Email" v-model="email" :invalidMessage="errors.get('email')" />
<!-- With a custom component -->
<Input label="Date" :component="InputDate" v-model="date" />
<!-- With helper text -->
<Input label="Password" v-model="password" helperText="Minimum 8 characters" />
</template>
<script setup>
import { Input, InputDate } from 'cetera-vue-utils'
</script>| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | — | Field label |
| component | Component | InputText | Input component to render |
| invalidMessage | string | — | Validation error text |
| helperText | string | — | Helper text below the field |
| id | string | auto uuid | ID for label/input association |
| size | 'sm' \| 'small' \| 'md' \| 'lg' | — | Controls padding/text size (sm: px-2 py-1 text-sm, md/default: px-3 py-2, lg: px-4 py-3 text-lg) |
| Slot | Props | Description |
|---|---|---|
| default | { invalid: boolean } | Replaces the input element entirely |
| invalid-message | — | Custom validation error content |
| helper-text | — | Custom helper text content |
Checkbox
Checkbox with label. Supports indeterminate state.
<template>
<Checkbox v-model="checked" label="I agree to the terms" />
<Checkbox v-model="value" label="Option" :value="'option1'" name="group" />
<Checkbox v-model="checked" label="Partial" :indeterminate="true" />
</template>
<script setup>
import { Checkbox } from 'cetera-vue-utils'
</script>| Prop | Type | Description |
|---|---|---|
| label | string | Text next to the checkbox |
| id | string | ID for label/input association (auto-generated if omitted) |
| value | boolean \| string \| number | Value when used in a group |
| name | string | Group name |
| disabled | boolean | Disable the control |
| indeterminate | boolean | Intermediate state |
| Event | Payload | Description |
|---|---|---|
| change | value: boolean \| string \| number \| undefined | Emitted after each toggle |
Radiobutton
Radio button with label.
<template>
<Radiobutton v-model="selected" label="Option 1" value="a" name="group" />
<Radiobutton v-model="selected" label="Option 2" value="b" name="group" />
</template>
<script setup>
import { Radiobutton } from 'cetera-vue-utils'
</script>| Prop | Type | Description |
|---|---|---|
| label | string | Text next to the button |
| value | string \| number | This button's value |
| name | string | Group name |
| disabled | boolean | Disable the control |
Toggle
Toggle switch with label.
<template>
<Toggle v-model="enabled" label="Enable notifications" />
<Toggle v-model="enabled" label="Active" labelPosition="right" />
</template>
<script setup>
import { Toggle } from 'cetera-vue-utils'
</script>| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | — | Label text |
| labelPosition | 'top' \| 'right' | 'top' | Label position |
| disabled | boolean | — | Disable the control |
Select
Dropdown select backed by @headlessui/vue Listbox.
<template>
<Select v-model="selected" label="Status" :options="statuses" />
</template>
<script setup>
import { Select } from 'cetera-vue-utils'
const statuses = [
{ name: 'Active', value: 'active' },
{ name: 'Archived', value: 'archived' },
]
</script>| Prop | Type | Default | Description |
|---|---|---|---|
| options | any[] | — | List of options |
| optionLabel | string | 'name' | Field used for display |
| optionValue | string | 'value' | Field used for value |
| optionGroupChildren | string | — | Field containing child options — enables nested/grouped tree |
| placeholder | string | — | Placeholder text |
| showClear | boolean | false | Show clear button |
| filter | boolean | — | Show search input inside the dropdown |
| filterFields | string[] | — | Option fields to search across (defaults to optionLabel) |
| disabled | boolean | false | Disable the control |
| size | 'sm' \| 'small' \| 'md' \| 'lg' | — | Input size |
| Event | Payload | Description |
|---|---|---|
| filter | query: string | Emitted on every keystroke in the filter input |
| Slot | Props | Description |
|---|---|---|
| option | { option: any } | Custom option rendering |
MultiSelect
Multiple selection backed by @headlessui/vue Listbox with multiple.
<template>
<MultiSelect v-model="selected" label="Tags" :options="tags" selectedFirst />
</template>
<script setup>
import { MultiSelect } from 'cetera-vue-utils'
</script>| Prop | Type | Default | Description |
|---|---|---|---|
| options | any[] | — | List of options |
| optionLabel | string | 'name' | Field used for display |
| optionValue | string | 'value' | Field used for value |
| optionGroupChildren | string | — | Field containing child options — enables nested/grouped tree |
| optionGroupSelectable | boolean | — | Make group headers clickable to select/deselect all children |
| placeholder | string | — | Placeholder text |
| showClear | boolean | false | Show clear button |
| selectAll | boolean | — | Show Select all / Deselect all checkbox in the dropdown header |
| filter | boolean | — | Show search input inside the dropdown |
| filterFields | string[] | — | Option fields to search across (defaults to optionLabel) |
| selectedFirst | boolean | — | Show selected options at the top of the list |
| selectedLabel | (count: number) => string | — | Custom summary label when items are selected |
| disabled | boolean | — | Disable the control |
| size | 'sm' \| 'small' \| 'md' \| 'lg' | — | Input size |
| Event | Payload | Description |
|---|---|---|
| filter | query: string | Emitted on every keystroke in the filter input |
| Slot | Props | Description |
|---|---|---|
| option | { option: any } | Custom option rendering |
Menu
Dropdown menu — a Popover wrapper with a vertically stacked list of MenuItem buttons. Each MenuItem closes the menu on click. Place <Menu> inside the trigger element (e.g. <Button>) for target=true (default).
<template>
<Button label="Actions" outlined>
<Menu placement="bottom-start">
<MenuItem @click="edit">Edit</MenuItem>
<MenuItem @click="remove">Delete</MenuItem>
</Menu>
</Button>
</template>
<script setup>
import { Button, Menu, MenuItem } from 'cetera-vue-utils'
</script>Menu
Accepts the same positioning props as Popover.
| Prop | Type | Default | Description |
|---|---|---|---|
| target | boolean \| string \| HTMLElement | true | Trigger element. true = parent element, false = programmatic only, string = CSS selector, HTMLElement = ref |
| placement | 'bottom-start' \| 'bottom-end' \| 'top-start' \| 'top-end' \| 'bottom' \| 'top' | 'bottom-start' | Panel placement |
| contextMenu | boolean | false | Open on right-click at cursor position instead of click |
| hover | boolean | false | Open on mouseenter, close on mouseleave (with 80 ms grace period to move into the panel) |
| autoClose | boolean | true | Close on any click inside the menu — useful when items don't have individual click handlers |
| Slot | Description |
|---|---|
| default | MenuItem components |
| Method | Description |
|---|---|
| open(anchor?) | Programmatically open. anchor can be a MouseEvent or HTMLElement |
MenuItem
Renders a full-width <button>. Automatically closes the parent menu on click via inject.
| Prop | Type | Default | Description |
|---|---|---|---|
| disabled | boolean | — | Native disabled — dims the item and disables interaction |
Forwards all other attributes via v-bind="$attrs" (e.g. class, event handlers).
Slider
Range slider using a native <input type="range">.
<template>
<Slider v-model="value" label="Volume" :min="0" :max="100" />
</template>
<script setup>
import { Slider } from 'cetera-vue-utils'
</script>All Input props and native range attrs are forwarded.
Textarea
Multi-line text input with optional auto-resize.
<template>
<Textarea v-model="text" label="Comment" rows="4" autoResize />
</template>
<script setup>
import { Textarea } from 'cetera-vue-utils'
</script>All Input props are forwarded. Pass autoResize to grow the textarea with content.
InputDate
Date picker. Stores value as yyyy-MM-dd string.
<template>
<InputDate v-model="date" label="Date of birth" min-date="2000-01-01" :max-date="today" />
</template>
<script setup>
import { InputDate } from 'cetera-vue-utils'
const date = ref<string | null>(null) // '2024-01-15'
const today = new Date()
</script>| Prop | Type | Default | Description |
|---|---|---|---|
| modelValue | string \| null | — | Date as yyyy-MM-dd |
| minDate | string \| Date | undefined | Earliest selectable date |
| maxDate | string \| Date | undefined | Latest selectable date |
| size | InputSize | undefined | Input size |
| showClear | boolean | false | Show clear button |
| disabled | boolean | false | Disables the picker |
Requires date-fns (included as a dependency).
InputMonth
Month picker. Stores value as YYYY-MM string (e.g. "2025-06").
<template>
<InputMonth v-model="month" label="Billing month" min-date="2020-01" :max-date="today" />
</template>
<script setup>
import { InputMonth } from 'cetera-vue-utils'
const month = ref<string | null>(null) // '2025-06'
const today = new Date()
</script>| Prop | Type | Default | Description |
|---|---|---|---|
| modelValue | string \| null | — | Month as YYYY-MM |
| minDate | string \| Date | undefined | Earliest selectable month |
| maxDate | string \| Date | undefined | Latest selectable month |
| size | InputSize | undefined | Field size |
| showClear | boolean | false | Show clear button |
| disabled | boolean | false | Disables the picker |
InputTime
Time picker. Stores value as HH:mm string.
<template>
<InputTime v-model="time" label="Start time" />
</template>
<script setup>
import { InputTime } from 'cetera-vue-utils'
const time = ref<string | null>(null) // '14:30'
</script>| Prop | Type | Default | Description |
|---|---|---|---|
| modelValue | string \| null | — | Time as HH:mm |
| showClear | boolean | false | Show clear button |
| disabled | boolean | false | Disable the picker |
InputFile
File upload with drag-and-drop support. Stores selected files as File[].
<template>
<InputFile v-model="files" name="docs" label="Documents" multiple upload-max-filesize="10 MB" />
</template>
<script setup>
import { InputFile } from 'cetera-vue-utils'
const files = ref([])
</script>| Prop | Type | Default | Description |
|---|---|---|---|
| modelValue | File[] | [] | Selected files |
| name | string | — | Input name attribute |
| label | string | undefined | Field label |
| multiple | boolean | false | Allow multiple files |
| disabled | boolean | false | Disable input |
| uploadMaxFilesize | string | undefined | Max size hint shown below the dropzone |
InputFileImage
Image upload with drag-and-drop, preview, and delete button. Accepts .jpg, .jpeg, .png, .webp.
<template>
<InputFileImage v-model="imageUrl" v-model:file="imageFile" name="avatar" label="Avatar" />
</template>
<script setup>
import { InputFileImage } from 'cetera-vue-utils'
const imageUrl = ref(null) // base64 data URL of the selected image
const imageFile = ref(null) // raw File object
</script>| Prop | Type | Default | Description |
|---|---|---|---|
| modelValue | string \| null | undefined | Base64 data URL of the image |
| file (v-model) | File \| null | undefined | Raw File object |
| name | string | 'files' | Input name attribute |
| label | string | undefined | Field label |
| disabled | boolean | false | Disable input |
| invalidMessage | string | undefined | Validation error message |
| uploadMaxFilesize | string | undefined | Max size hint shown below the dropzone |
| Slot | Description |
|---|---|
| invalid-message | Custom validation error content |
InputNumber
Numeric input using a native <input type="number">.
<template>
<InputNumber v-model="amount" label="Amount" />
</template>
<script setup>
import { InputNumber } from 'cetera-vue-utils'
</script>All Input props are forwarded.
InputPassword
Password field with show/hide toggle.
<template>
<InputPassword v-model="password" label="Пароль" placeholder="Введите пароль" />
</template>
<script setup>
import { ref } from 'vue'
import { InputPassword } from 'cetera-vue-utils'
const password = ref('')
</script>All Input props (label, invalidMessage, helperText, etc.) are supported.
InputSearch
Search field with icon, clear button, and debounce. Requires @heroicons/vue and @vueuse/core.
<template>
<InputSearch v-model="query" :debounce="500" :loading="isSearching" placeholder="Search..." />
</template>
<script setup>
import { InputSearch } from 'cetera-vue-utils'
</script>| Prop | Type | Default | Description |
|---|---|---|---|
| modelValue | string | '' | Search value |
| debounce | number | 800 | Delay before emit (ms) |
| loading | boolean | false | Shows spinner instead of clear icon |
| size | 'sm' \| 'small' \| 'md' \| 'lg' | — | Controls padding/text size |
Tabs
Tab component based on provide/inject. Tab styling is fully controlled via Tailwind.
<template>
<Tabs v-model:value="activeTab">
<TabList class="border-b border-gray-200 gap-0 mb-4">
<Tab
v-for="tab in tabs"
:key="tab.value"
:value="tab.value"
:class="[
'px-4 py-2 text-sm font-medium border-b-2 -mb-px transition',
activeTab === tab.value
? 'border-primary-500 text-primary-600'
: 'border-transparent text-gray-500 hover:text-gray-700',
]"
>
{{ tab.label }}
</Tab>
</TabList>
<TabPanel value="one"><p>First tab</p></TabPanel>
<TabPanel value="two"><p>Second tab</p></TabPanel>
</Tabs>
</template>
<script setup>
import { ref } from 'vue'
import { Tabs, TabList, Tab, TabPanel } from 'cetera-vue-utils'
const activeTab = ref('one')
</script>| Component | Prop | Description |
|---|---|---|
| Tabs | value (v-model) | Active tab value |
| Tabs | lazy | Mount TabPanel content only when first activated |
| Tabs | keepHeight | Inactive panels use visibility:hidden instead of display:none. Use with <TabPanels> wrapper — it automatically becomes a CSS grid that overlaps all panels, so the container height equals the tallest panel. |
| Tab | value | Unique tab identifier |
| Tab | as | HTML tag or component (default: button) |
| TabPanel | value | Shown when it matches Tabs.value |
| TabPanels | — | Optional wrapper for keepHeight mode; auto-applies CSS grid to overlay all panels |
TabList is a role="tablist" wrapper that accepts any classes. Tab sets aria-selected automatically. The class attribute on Tab is forwarded to the overflow MenuItem, so layout classes like flex items-center gap-2 apply in both contexts.
TabList props:
| Prop | Type | Default | Description |
|---|---|---|---|
| activeVisible | boolean | false | When true, selecting a hidden tab from the overflow menu moves it to the first visible position; also highlights the "More" button border when the active tab is hidden |
| placement | 'bottom-start' \| 'bottom-end' \| 'top-start' \| 'top-end' \| 'bottom' \| 'top' | 'bottom-end' | Placement of the overflow dropdown menu |
TabList slots:
| Slot | Description |
|---|---|
| default | Tab items (<Tab> components) |
| #more | Overflow button label. Scoped: { count: number }. Default content: More N ... |
Accordion
Collapsible panels. Supports single and multiple open modes.
<template>
<Accordion :multiple="true">
<AccordionPanel value="one">
<AccordionHeader class="px-4 py-3 text-sm font-medium">
Header
</AccordionHeader>
<AccordionContent class="px-4 py-3 text-sm text-gray-600">
Panel content
</AccordionContent>
</AccordionPanel>
<AccordionPanel value="two">
<AccordionHeader class="px-4 py-3 text-sm font-medium">
Another header
</AccordionHeader>
<AccordionContent class="px-4 py-3 text-sm text-gray-600">
Another content
</AccordionContent>
</AccordionPanel>
</Accordion>
</template>
<script setup>
import { Accordion, AccordionPanel, AccordionHeader, AccordionContent } from 'cetera-vue-utils'
</script>| Prop | Component | Type | Default | Description |
|---|---|---|---|---|
| multiple | Accordion | boolean | false | Allow multiple panels open at once |
| value | Accordion | (string \| number)[] | — | Array of open panel values (v-model) |
| value | AccordionPanel | string \| number | — | Unique panel identifier |
AccordionHeader renders a button with an animated chevron. AccordionContent opens with a smooth height animation.
Skeleton
Placeholder for loading content with a pulse animation.
<template>
<!-- Text lines -->
<Skeleton height="1rem" width="100%" />
<Skeleton height="1rem" width="75%" />
<!-- Avatar + text -->
<div class="flex items-center gap-3">
<Skeleton shape="circle" width="3rem" height="3rem" />
<div class="flex-1 space-y-2">
<Skeleton height="0.875rem" width="40%" />
<Skeleton height="0.75rem" width="60%" />
</div>
</div>
</template>
<script setup>
import { Skeleton } from 'cetera-vue-utils'
</script>| Prop | Type | Default | Description |
|---|---|---|---|
| width | string | '100%' | Block width |
| height | string | '1rem' | Block height |
| shape | 'rectangle' \| 'circle' | 'rectangle' | Shape: rectangle or circle |
Draggable
Sortable list component based on SortableJS. Supports drag between lists, multi-drag, and cloning.
<template>
<!-- Sortable list -->
<Draggable v-model="items" item-key="id" tag="ul" ghost-class="opacity-40">
<template #item="{ element }">
<li class="cursor-grab">{{ element.label }}</li>
</template>
</Draggable>
<!-- Drag between two lists -->
<Draggable v-model="listA" item-key="id" group="shared">
<template #item="{ element }"><div>{{ element.label }}</div></template>
</Draggable>
<Draggable v-model="listB" item-key="id" group="shared">
<template #item="{ element }"><div>{{ element.label }}</div></template>
</Draggable>
</template>
<script setup>
import { ref } from 'vue'
import { Draggable } from 'cetera-vue-utils'
const items = ref([
{ id: 1, label: 'Item A' },
{ id: 2, label: 'Item B' },
])
</script>| Prop | Type | Default | Description |
|---|---|---|---|
| modelValue | any[] | — | List (use with v-model) |
| list | any[] | — | List (mutated in place; mutually exclusive with v-model) |
| itemKey | string \| Function | — | Field or function to produce a unique key per item |
| tag | string | 'div' | Root element tag |
| group | string \| object | — | SortableJS group — enables drag between lists |
| clone | Function | — | Custom clone function for pull: 'clone' |
| move | Function | — | Callback to validate a move; return false to cancel |
| multiDrag | boolean | false | Enable multi-item drag (requires SortableJS MultiDrag plugin) |
All other props are forwarded as SortableJS options (e.g. ghost-class, chosen-class, animation, handle, filter, sort, disabled).
@change emits { added }, { removed }, or { moved } after each reorder.
DataTable
Data table that reads column definitions from Column child components.
<template>
<DataTable :value="users" scrollable scroll-height="400px">
<Column field="name" header="Name" />
<Column field="email" header="Email" />
<Column header="Actions">
<template #body="{ data }">
<Button label="Edit" size="sm" @click="edit(data)" />
</template>
</Column>
</DataTable>
</template>
<script setup>
import { DataTable, Column, Button } from 'cetera-vue-utils'
</script>| Prop | Type | Default | Description |
|---|---|---|---|
| value | any[] | — | Row data |
| scrollable | boolean | false | Enable scrollable body |
| scrollHeight | string | — | Fixed height for scrollable body (e.g. '400px') |
| stickyHeader | boolean | false | Stick header on scroll |
| stickyTop | string | '0px' | Top offset for sticky header (e.g. '64px' when a fixed navbar is present) |
| loading | boolean | false | Show skeleton rows |
| zebra | boolean | false | Alternate row background |
| borders | boolean | false | Show column borders |
| rowClass | string \| ((row, index) => string) | — | Extra class(es) for each row |
| headerClass | string | — | Extra class for header cells |
| Event | Payload | Description |
|---|---|---|
| row-click | (row: object, index: number) | Emitted when a row is clicked |
Column props:
| Prop | Type | Description |
|---|---|---|
| field | string | Data field name |
| header | string | Column header text |
| class | string | Extra class for both header and body cells |
| headerClass | string | Extra class for the header cell only |
| bodyClass | string | Extra class for body cells only |
Column slots:
| Slot | Description |
|---|---|
| #body="{ data }" | Custom cell renderer — receives the row object |
| #filter | Filter control rendered in the filter row. If any column defines #filter, a filter row is shown below the header row. |
DataTable slots:
| Slot | Description |
|---|---|
| header | Content above the column headers (spans full width) |
| empty | Content shown when value is empty. Default: «Нет данных» |
Pagination
Page navigation with rows-per-page selector. Optionally syncs current page with a URL query param via the History API.
<template>
<!-- v-model style (preferred) -->
<Pagination :total-records="243" v-model:limit="limit" v-model:offset="offset" />
<!-- legacy @refresh style (still supported) -->
<Pagination :total-records="243" :limit="limit" :offset="offset" @refresh="onRefresh" />
</template>
<script setup>
import { ref } from 'vue'
import { Pagination } from 'cetera-vue-utils'
const limit = ref(20)
const offset = ref(0)
const onRefresh = ({ limit: l, offset: o }) => {
limit.value = l
offset.value = o
}
</script>| Prop | Type | Default | Description |
|---|---|---|---|
| totalRecords | number | 0 | Total number of records |
| limit | number | — | Records per page |
| offset | number | — | Current offset |
| limitOptions | number[] | [20, 50, 100] | Rows-per-page options |
| queryParam | string | — | When set, syncs current page with this query param (queryParam="page" → ?page=2). Restores on mount, supports browser back/forward. |
v-model:limit and v-model:offset update automatically on page/rows-per-page change. The @refresh event emits { limit, offset } on each change and remains supported for backward compatibility.
Popover
Floating panel that attaches to an external trigger element. No wrapper slot — place the component next to or inside the target element.
<!-- target=true (default): attaches to the parent element -->
<template>
<div>
<Button label="Open" outlined />
<Popover>
<div class="p-3">Panel content</div>
</Popover>
</div>
</template>
<script setup>
import { Popover, Button } from 'cetera-vue-utils'
</script>| Prop | Type | Default | Description |
|---|---|---|---|
| target | true \| false \| string \| HTMLElement | true | Trigger element. true = parent element, false = no binding (programmatic only), string = CSS selector ('#id', '.class'), HTMLElement = direct ref |
| placement | 'auto' \| 'bottom-start' \| 'bottom-end' \| 'bottom' \| 'top-start' \| 'top-end' \| 'top' | 'auto' | Panel placement (ignored when contextMenu). auto picks the side with most available space |
| contextMenu | boolean | false | Open on right-click at cursor position instead of click |
| hover | boolean | false | Open on mouseenter, close on mouseleave (with 80 ms grace period to move into the panel) |
| Slot | Description |
|---|---|
| default | Panel content. Exposes { close } to close the panel programmatically |
| Method | Description |
|---|---|
| open(anchor?) | Programmatically open the panel. anchor can be a MouseEvent (positions at cursor) or HTMLElement (positions below element) |
Closes on Escape or click outside.
target by selector:
<Button id="my-btn" label="Open" outlined />
<Popover target="#my-btn">
<div class="p-3">Content</div>
</Popover>target by ref:
<button ref="btn">Open</button>
<Popover :target="btn">
<div class="p-3">Content</div>
</Popover>target=false — programmatic only:
<Button label="Open" @click="(e) => popover.open(e)" />
<Popover ref="popover" :target="false">
<div class="p-3">Content</div>
</Popover>contextmenu — panel opens at cursor:
<div class="p-8">
Right-click here
<Popover contextMenu>
<Menu>
<MenuItem @click="edit">Edit</MenuItem>
<MenuItem @click="remove">Delete</MenuItem>
</Menu>
</Popover>
</div>Composables
useNotify
Global notification system with auto-dismiss.
import { useNotify } from 'cetera-vue-utils'
const notify = useNotify()
notify.success('Saved!')
notify.error('Something went wrong')
notify.warn('Please check the input')
notify.info('Update available')
// Custom duration (ms)
notify.add('info', 'Custom message', 5000)| Method | Type | Duration |
|---|---|---|
| success(message) | success | 3000ms |
| info(message) | info | 3000ms |
| warn(message) | warn | 4000ms |
| error(message) | error | 5000ms |
| add(type, message, life) | any | custom |
The notifications ref is shared across all useNotify() calls — readable from anywhere in the app.
useHttpClient
HTTP client based on axios with automatic error notifications and request cancellation support.
import { useHttpClient } from 'cetera-vue-utils'
const http = useHttpClient({
baseURL: 'https://api.example.com',
onUnauthorized: () => router.push('/login'),
})
// GET
const { data } = await http.get<User[]>('/users')
// POST
await http.post('/users', { data: { name: 'John' } })
// With params
await http.get('/users', { params: { page: 1 } })
// Cancel previous request on repeat call
await http.get('/search', { params: { q: 'text' }, cancelPrevious: true })
// Download file
await http.download('/reports/export.xlsx')| Method | Description |
|---|---|
| get<T>(url, config?) | GET request |
| post<T>(url, config?) | POST request |
| patch<T>(url, config?) | PATCH request |
| put<T>(url, config?) | PUT request |
| destroy<T>(url, config?) | DELETE request |
| download(url, config?) | Download file |
Automatic behavior:
2xxwith amessagefield — showsnotify.success400,5xx— showsnotify.error422— showsnotify.errorwith validation message401— callsonUnauthorized()403— showsnotify.error("Action not allowed!")419— showsnotify.errorabout expired session
useErrors
Validation error management for forms.
import { useErrors } from 'cetera-vue-utils'
const errors = useErrors()
// Record errors from server response
errors.record(response.errors)
// Check if a field has an error
errors.has('email') // true / false
// Get error text for a field
errors.get('email') // "Email is required"
// Check if any errors exist
errors.any() // true / false
// Set a single error
errors.set({ path: 'email', value: 'Invalid format' })
// Clear one field or all fields
errors.clear('email')
errors.clear()useDataLoad
Data loading via useHttpClient with managed loading and data state.
import { useDataLoad, useHttpClient } from 'cetera-vue-utils'
const http = useHttpClient({ baseURL: '/api' })
// Simple case
const { data, loading, load } = useDataLoad<User[]>(() => http.get('/users'))
// With params
const { data, meta, loading, load } = useDataLoad<User[], { page: number }>(
(params) => http.get('/users', { params })
)
// Load
await load({ page: 1 })
// Load only if data is not yet fetched (cache)
await load(true)
await load({ page: 1 }, true)
// Share state between multiple loaders
const data = ref<User[]>()
const loading = ref(false)
const { load: loadUsers } = useDataLoad(() => http.get('/users'), { data, loading })
const { load: loadMore } = useDataLoad((p) => http.get('/users', { params: p }), { data, loading })The returned meta contains total — useful for pagination.
useForm
Reactive form state with optional two-way URL query string sync.
import { useForm } from 'cetera-vue-utils'
// Basic usage
const form = useForm({
defaults: { search: '', page: 1, active: true },
onChange: () => load(),
})
// Sync form state to/from URL query params
const form = useForm({
defaults: { search: '', page: 1 },
syncQuery: true,
onChange: () => load(),
})| Option | Type | Default | Description |
|---|---|---|---|
| defaults | T | — | Initial form values |
| onChange | () => void | — | Called on any change (deep watch) |
| syncQuery | boolean | false | Sync form state to/from URL query string |
When syncQuery is enabled, form values are read from the URL on mount and written back on every change. Supports nested objects (filter[status]), dates, numbers, booleans, and arrays. Arrays are serialized as comma-separated values (e.g. ids=1,2,3) and deserialized back into arrays by checking whether the default value is an array. This works for both top-level array fields and array fields inside nested objects.
Returns a Ref<T> — use as form.value.fieldName or v-model="form.value.fieldName".
useFormElements
Helper composable for form components. Used internally by Input, but available standalone.
import { useFormElements } from 'cetera-vue-utils'
const { isInvalid, isHelper, uuid } = useFormElements(props)TypeScript
All exports are fully typed:
import type {
Notification,
HttpResponse,
UseHttpOptions,
ErrorsData,
InputProps,
SpinnerSize,
} from 'cetera-vue-utils'License
MIT
