npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

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

About

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

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

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

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

Open Software & Tools

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

© 2026 – Pkg Stats / Ryan Hefner

cisse-vue-ui

v1.0.0

Published

Vue 3 + TypeScript + Tailwind CSS v4 component library

Readme

cisse-vue-ui

A Vue 3 component library built with TypeScript and Tailwind CSS v4.

View Storybook Documentation

Installation

npm install cisse-vue-ui
# or
bun add cisse-vue-ui

Peer Dependencies

npm install vue@^3.4 tailwindcss@^4 @iconify/vue@^4

Setup

1. Import Styles

Add the pre-compiled CSS to your main CSS file:

@import 'cisse-vue-ui/style.css';
@import 'tailwindcss';

2. Configure Primary Color (Optional)

Override the default primary color in your CSS:

@theme {
  --color-primary-50: oklch(97% 0.02 142);
  --color-primary-100: oklch(94% 0.05 142);
  --color-primary-200: oklch(88% 0.10 142);
  --color-primary-300: oklch(78% 0.15 142);
  --color-primary-400: oklch(65% 0.20 142);
  --color-primary-500: oklch(55% 0.22 142);
  --color-primary-600: oklch(48% 0.20 142);
  --color-primary-700: oklch(40% 0.17 142);
  --color-primary-800: oklch(32% 0.14 142);
  --color-primary-900: oklch(25% 0.10 142);
  --color-primary-950: oklch(18% 0.08 142);
}

Usage

Tree-Shaken Imports (Recommended)

<script setup lang="ts">
import { Button, CardComponent, FormInput } from 'cisse-vue-ui'
</script>

Category Imports

import { Button, Tabs, TabPanel } from 'cisse-vue-ui/components/core'
import { FormInput, FormSelect, Switch } from 'cisse-vue-ui/components/form'
import { Modal, Alert, LoadingSpinner } from 'cisse-vue-ui/components/feedback'
import { BaseLayout, PageLayout } from 'cisse-vue-ui/components/layout'

Global Registration (Vue Plugin)

import { createApp } from 'vue'
import { VueTailwindUI } from 'cisse-vue-ui'

const app = createApp(App)

// Register all components
app.use(VueTailwindUI)

// Or with a prefix
app.use(VueTailwindUI, { prefix: 'Ui' }) // <UiButton>, <UiCard>, etc.

// Or specific components only
app.use(VueTailwindUI, { components: ['Button', 'CardComponent'] })

Components

Core

| Component | Description | |-----------|-------------| | Button | Button with variants (primary, secondary, outline, ghost, danger, success), sizes, icons, loading state | | CardComponent | Card container with header, content, and footer slots | | CardWrapper | Advanced card with shadow/border/padding variants, image positions, clickable/selected states, loading skeleton | | DataTable | Full-featured data table with sorting, selection, pagination, error states, striped/bordered/compact options | | Table | Atomic table wrapper with context provider for styling (striped, bordered, hover, compact, stickyHeader) | | Colgroup / Col | Column grouping and column styling | | Thead / Tbody / Tfoot | Table section wrappers (support multiple sections) | | Tr | Table row with selected, clickable, disabled states | | Th | Header cell with sortable, colspan/rowspan, scope, alignment, and sticky column support | | Td | Data cell with colspan/rowspan, alignment, main column styling, truncate, and custom classes | | TableHeader | Composed header row with sort and select-all functionality | | TableRow | Composed data row with selection and type rendering | | TableFooter | Composed footer row for summary/pagination | | TableComponent | Alias for DataTable (backwards compatibility) | | MobileList | Mobile-optimized card-based list with selection support | | ResponsiveList | Combines MobileList (mobile) and TableComponent (desktop) with automatic breakpoint switching | | Tabs | Tab navigation with variants (underline, pills, boxed) | | TabPanel | Tab content panel (use with Tabs) | | Dropdown | Dropdown menu with items, icons, and dividers | | Avatar | User avatar with image, initials, or icon fallback | | AutocompleteComponent | Searchable select with keyboard navigation | | MenuItem | Navigation menu item with icon, active state detection, flyout submenus, and route support | | StatusBadge | Colored status indicator badge | | TableAction | Icon button for table row actions | | Stepper | Multi-step progress indicator with horizontal/vertical orientation | | CollapsibleCard | Card that can expand/collapse its content | | Accordion | Expandable content sections with single/multiple mode | | AccordionItem | Individual accordion panel (use with Accordion) | | Breadcrumb | Navigation breadcrumb trail | | Drawer | Slide-out panel from any edge (left, right, top, bottom) | | Popover | Floating content panel triggered by click or hover | | Timeline | Vertical timeline for events/history display | | Tooltip | Hover tooltip with customizable position | | DarkModeToggle | Dark mode toggle button with icon variants | | StatsCard | Statistics display card with icon, value, and trend | | StatsGrid | Grid layout for multiple StatsCard components | | FilterTabs | Tabbed filter buttons with counts |

Form

| Component | Description | |-----------|-------------| | FormInput | Text input with validation states, sizes (sm/md/lg), and ARIA support | | FormSelect | Select dropdown with search, multi-select, and validation | | FormGroup | Form field wrapper with label, help text, and error states | | FormLabel | Styled form label with required indicator | | FormHelp | Help/error text for form fields | | FormSection | Grouped form section with title and description | | FormActions | Form button container with alignment options | | InputWrapper | Wrapper for consistent input styling | | SearchInput | Search input with icon and clear button | | Switch | Toggle switch with label and description | | Checkbox | Checkbox with label, description, and indeterminate state | | CheckboxGroup | Group of related checkboxes with select all | | Combobox | Multi-select combobox with search and tags | | TagsInput | Tag input with add/remove functionality | | TextArea | Multi-line text input with auto-resize | | DatePicker | Calendar date picker with min/max dates | | ColorPicker | Color selection with swatches and custom input | | IconPicker | Icon selection from Iconify with search | | FileUpload | Drag-and-drop file upload with preview | | Rating | Star rating input with half-star support | | Slider | Single value slider input | | RangeSlider | Dual-handle range slider | | EmailInput | Email input with validation | | PasswordInput | Password input with visibility toggle | | PhoneInput | Phone number input with formatting | | NumberInput | Numeric input with increment/decrement | | MoneyInput | Currency input with formatting | | PercentInput | Percentage input with formatting | | QuantityInput | Quantity input with +/- buttons | | URLInput | URL input with validation | | OTPInput | One-time password input with multiple digits |

Feedback

| Component | Description | |-----------|-------------| | Modal | Modal dialog with focus trap, ARIA support, and slots | | ConfirmDialog | Confirmation modal with customizable actions | | Alert | Alert banner with variants (info, success, warning, error) | | Toast | Individual toast notification with auto-dismiss | | ToastContainer | Toast notification container with positioning | | LoadingSpinner | Loading indicator with size variants | | Progress | Progress bar with percentage display | | Skeleton | Loading placeholder with animation | | CardSkeleton | Card loading skeleton | | ListSkeleton | List loading skeleton | | TableSkeleton | Table loading skeleton | | PaginationControls | Pagination with page numbers and navigation | | NotificationList | Notification list container | | NotificationComponent | Individual notification item | | EmptyState | Placeholder for empty content with icon and action slot |

Layout

| Component | Description | |-----------|-------------| | AuthLayout | Split-panel authentication layout with branding and form sections | | BaseLayout | App shell with sidebar, header, main content area, and route-aware menu | | PageLayout | Page wrapper with breadcrumbs | | PageHero | Hero section with title, description, and action slots |

Type Display

| Component | Description | |-----------|-------------| | TextType | Text value display | | NumberType | Formatted number display | | DateType | Formatted date display | | BooleanType | Boolean value display (check/cross icons) | | BadgeType | Badge value display with colors |

Atomic Table Usage

Build custom tables with granular control using atomic components:

<script setup lang="ts">
import { Table, Thead, Tbody, Tfoot, Tr, Th, Td, Colgroup, Col } from 'cisse-vue-ui'

const data = [
  { id: 1, name: 'John Doe', amount: 1250.00 },
  { id: 2, name: 'Jane Smith', amount: 890.50 },
]
const total = data.reduce((sum, row) => sum + row.amount, 0)
</script>

<template>
  <Table striped bordered>
    <Colgroup>
      <Col width="200px" />
      <Col width="150px" />
    </Colgroup>
    <Thead>
      <Tr>
        <Th sortable>Name</Th>
        <Th align="right">Amount</Th>
      </Tr>
    </Thead>
    <Tbody>
      <Tr v-for="row in data" :key="row.id" clickable @click="select(row)">
        <Td main>{{ row.name }}</Td>
        <Td align="right">{{ row.amount.toFixed(2) }}</Td>
      </Tr>
    </Tbody>
    <Tfoot>
      <Tr>
        <Td>Total</Td>
        <Td align="right">{{ total.toFixed(2) }}</Td>
      </Tr>
    </Tfoot>
  </Table>
</template>

Advanced: Colspan, Rowspan & Row Grouping

<Table bordered>
  <Thead>
    <Tr>
      <Th rowspan="2">Product</Th>
      <Th colspan="2" align="center">Sales</Th>
    </Tr>
    <Tr>
      <Th align="right">Q1</Th>
      <Th align="right">Q2</Th>
    </Tr>
  </Thead>
  <Tbody>
    <!-- Row grouping with scope="rowgroup" -->
    <Tr>
      <Th rowspan="2" scope="rowgroup">Electronics</Th>
      <Td align="right">$45,000</Td>
      <Td align="right">$52,000</Td>
    </Tr>
    <Tr>
      <Td align="right">$38,000</Td>
      <Td align="right">$41,000</Td>
    </Tr>
  </Tbody>
</Table>

All atomic components support v-bind="$attrs" for full customization.

Or use the full-featured DataTable for common use cases:

<script setup lang="ts">
import { DataTable } from 'cisse-vue-ui'
import type { Property } from 'cisse-vue-ui/types'

const properties: Property[] = [
  { name: 'name', label: 'Name', main: true, sortable: true },
  { name: 'email', label: 'Email' },
  { name: 'status', label: 'Active', type: 'boolean' },
]
</script>

<template>
  <DataTable
    :items="users"
    :properties="properties"
    selectable
    striped
    bordered
    @select="handleSelect"
    @sort="handleSort"
  >
    <template #action="{ item }">
      <Button variant="ghost" size="sm" icon="lucide:edit" />
    </template>
  </DataTable>
</template>

Composables

import {
  useNotifications,
  useDarkMode,
  useExportCSV,
  useDropdown,
  useModal,
  useToast,
  useFocusTrap,
  useId,
  useInputStyles
} from 'cisse-vue-ui/composables'

useModal

Manage modal state with data support:

import { useModal } from 'cisse-vue-ui/composables'

// Simple modal
const createModal = useModal()
createModal.open()
createModal.close()

// Modal with data (e.g., for editing)
const editModal = useModal<User>()
editModal.open(selectedUser)
// Access editModal.data.value in template

// With callbacks
const deleteModal = useModal<Item>({
  onOpen: (data) => console.log('Opening with:', data),
  onClose: () => refetchData()
})
<template>
  <!-- Use isOpen for v-model binding -->
  <Modal v-model="editModal.isOpen.value" title="Edit User">
    <FormInput v-model="editModal.data.value.name" label="Name" />
    <template #footer>
      <Button @click="editModal.close()">Cancel</Button>
      <Button variant="primary" @click="save">Save</Button>
    </template>
  </Modal>
</template>

useDropdown

Shared dropdown logic for custom dropdown components (used internally by Dropdown, FormSelect, AutocompleteComponent):

import { useDropdown } from 'cisse-vue-ui/composables'
import { ref } from 'vue'

const triggerRef = ref<HTMLElement>()
const dropdownRef = ref<HTMLElement>()

const {
  isOpen,
  highlightedIndex,
  dropdownStyle,
  open,
  close,
  toggle,
  handleKeydown,
  scrollToHighlighted,
} = useDropdown(triggerRef, dropdownRef, {
  teleport: true,
  align: 'left',
  gap: 8,
  onOpen: () => console.log('Opened'),
  onClose: () => console.log('Closed'),
})

useNotifications

const { notifications, addNotification, removeNotification } = useNotifications()

addNotification({
  type: 'success',
  title: 'Saved',
  message: 'Your changes have been saved.'
})

useDarkMode

const { isDark, toggle, enable, disable } = useDarkMode({
  selector: 'html',      // Element to add .dark class
  storageKey: 'theme',   // localStorage key
  defaultDark: false     // Default state
})

useExportCSV

const { exportToCSV } = useExportCSV()

exportToCSV(data, columns, 'export.csv')

useToast

Toast notification system with positioning and auto-dismiss:

import { useToast } from 'cisse-vue-ui/composables'

const { toasts, addToast, removeToast, clearToasts } = useToast()

// Add a toast
addToast({
  type: 'success',
  title: 'Success!',
  message: 'Your changes have been saved.',
  duration: 5000  // auto-dismiss after 5s
})

// Different toast types
addToast({ type: 'error', title: 'Error', message: 'Something went wrong' })
addToast({ type: 'warning', title: 'Warning', message: 'Please review' })
addToast({ type: 'info', title: 'Info', message: 'New update available' })
<template>
  <!-- Add ToastContainer to your app root -->
  <ToastContainer position="top-right" />
</template>

useFocusTrap

Trap focus within a container (used internally by Modal):

import { useFocusTrap } from 'cisse-vue-ui/composables'
import { ref } from 'vue'

const isActive = ref(true)
const { containerRef } = useFocusTrap({
  active: isActive,
  focusFirst: true,      // Focus first focusable element on activate
  restoreFocus: true     // Restore focus on deactivate
})

useId

Generate unique IDs for accessibility (ARIA relationships):

import { useId } from 'cisse-vue-ui/composables'

const { id, related } = useId({ prefix: 'modal' })
// id.value = 'cisse-modal-1'
// related('title') = 'cisse-modal-1-title'
// related('description') = 'cisse-modal-1-description'
<template>
  <div :id="id" role="dialog" :aria-labelledby="related('title')">
    <h2 :id="related('title')">Dialog Title</h2>
  </div>
</template>

useInputStyles

Centralized input styling for form components with consistent sizing, states, and dark mode:

import { useInputStyles } from 'cisse-vue-ui/composables'
import { ref, computed } from 'vue'

const size = ref<'sm' | 'md' | 'lg'>('md')
const disabled = ref(false)
const invalid = ref(false)

const {
  inputClasses,      // Classes for input elements
  triggerClasses,    // Classes for dropdown triggers
  wrapperClasses,    // Classes for input wrappers
  iconClasses,       // Classes for input icons
} = useInputStyles({
  size,
  disabled,
  invalid,
  focused: computed(() => false)
})

Size variants:

  • sm: Smaller text (text-sm), reduced padding (px-2.5 py-1.5)
  • md: Default size (text-sm), standard padding (px-3 py-2)
  • lg: Larger text (text-base), increased padding (px-4 py-2.5)

Table Composables

Powerful composables for building advanced data tables:

import {
  usePagination,
  useTableKeyboardNavigation,
  useColumnVisibility,
  useColumnResize,
  usePinnedRows,
  useEditableCell,
  useVirtualScroll
} from 'cisse-vue-ui/composables'

usePagination

Client-side pagination with page size selector support:

import { usePagination } from 'cisse-vue-ui/composables'
import { ref } from 'vue'

const items = ref([/* your data */])

const {
  currentPage,      // Current page (1-indexed)
  pageSize,         // Items per page
  totalPages,       // Total number of pages
  paginatedItems,   // Items for current page
  startIndex,       // First item index (0-indexed)
  endIndex,         // Last item index (0-indexed)
  hasNextPage,      // Can go forward?
  hasPreviousPage,  // Can go back?
  goToPage,         // Navigate to specific page
  nextPage,         // Go to next page
  previousPage,     // Go to previous page
  setPageSize,      // Change items per page
} = usePagination(items, {
  initialPage: 1,
  initialPageSize: 10,
  pageSizes: [10, 25, 50, 100]
})
<template>
  <DataTable
    :items="items"
    :properties="properties"
    paginated
    :page-size="25"
    :page-sizes="[10, 25, 50, 100]"
    @page-change="handlePageChange"
  />
</template>

useTableKeyboardNavigation

Keyboard navigation for accessible tables:

import { useTableKeyboardNavigation } from 'cisse-vue-ui/composables'
import { ref } from 'vue'

const tableRef = ref<HTMLElement>()
const items = ref([/* your data */])

const {
  focusedRowIndex,  // Currently focused row
  focusedCellIndex, // Currently focused cell
  handleKeydown,    // Keyboard event handler
  focusRow,         // Focus specific row
  focusCell,        // Focus specific cell
} = useTableKeyboardNavigation({
  tableRef,
  items,
  onSelect: (index) => console.log('Selected row:', index),
  onActivate: (index) => console.log('Activated row:', index),
})
<template>
  <Table ref="tableRef" @keydown="handleKeydown">
    <Tbody>
      <Tr
        v-for="(item, index) in items"
        :key="item.id"
        :tabindex="focusedRowIndex === index ? 0 : -1"
        :class="{ 'ring-2 ring-primary-500': focusedRowIndex === index }"
      >
        <Td>{{ item.name }}</Td>
      </Tr>
    </Tbody>
  </Table>
</template>

useColumnVisibility

Toggle column visibility with persistence:

import { useColumnVisibility, type Column } from 'cisse-vue-ui/composables'

const columns: Column[] = [
  { key: 'name', label: 'Name', visible: true, required: true },
  { key: 'email', label: 'Email', visible: true },
  { key: 'phone', label: 'Phone', visible: false },
  { key: 'address', label: 'Address', visible: false },
]

const {
  visibleColumns,     // Columns with visible: true
  hiddenColumns,      // Columns with visible: false
  toggleColumn,       // Toggle single column visibility
  showColumn,         // Show a column
  hideColumn,         // Hide a column
  showAllColumns,     // Show all columns
  hideAllColumns,     // Hide all except required
  resetColumns,       // Reset to initial state
  isColumnVisible,    // Check if column is visible
} = useColumnVisibility({
  columns,
  storageKey: 'my-table-columns' // Optional: persist to localStorage
})
<template>
  <div class="mb-4 flex gap-2">
    <Checkbox
      v-for="col in columns"
      :key="col.key"
      :model-value="isColumnVisible(col.key)"
      :disabled="col.required"
      :label="col.label"
      @update:model-value="toggleColumn(col.key)"
    />
  </div>

  <Table>
    <Thead>
      <Tr>
        <Th v-for="col in visibleColumns" :key="col.key">{{ col.label }}</Th>
      </Tr>
    </Thead>
    <Tbody>
      <Tr v-for="item in items" :key="item.id">
        <Td v-for="col in visibleColumns" :key="col.key">
          {{ item[col.key] }}
        </Td>
      </Tr>
    </Tbody>
  </Table>
</template>

useColumnResize

Drag-to-resize columns with constraints:

import { useColumnResize } from 'cisse-vue-ui/composables'

const {
  columnWidths,     // Map of column key to width
  isResizing,       // Currently resizing?
  resizingColumn,   // Which column is being resized
  startResize,      // Begin resize operation
  getColumnWidth,   // Get width for column
  setColumnWidth,   // Set width for column
  resetColumnWidth, // Reset column to default
  resetAllWidths,   // Reset all columns
} = useColumnResize({
  defaultWidth: 150,
  minWidth: 80,
  maxWidth: 500,
  storageKey: 'my-table-widths' // Optional: persist to localStorage
})
<template>
  <Table>
    <Thead>
      <Tr>
        <Th
          v-for="col in columns"
          :key="col.key"
          :style="{ width: getColumnWidth(col.key) + 'px' }"
          resizable
          :resizing="resizingColumn === col.key"
          @resize-start="(e) => startResize(col.key, e)"
        >
          {{ col.label }}
        </Th>
      </Tr>
    </Thead>
  </Table>
</template>

usePinnedRows

Pin rows to top or bottom of table:

import { usePinnedRows } from 'cisse-vue-ui/composables'

interface User {
  id: number
  name: string
  email: string
}

const items = ref<User[]>([/* your data */])

const {
  pinnedTop,        // Items pinned to top
  pinnedBottom,     // Items pinned to bottom
  unpinnedItems,    // Items not pinned
  isPinned,         // Check if item is pinned
  getPinPosition,   // Get pin position ('top' | 'bottom' | null)
  pinToTop,         // Pin item to top
  pinToBottom,      // Pin item to bottom
  unpin,            // Unpin item
  togglePin,        // Toggle pin state
  clearPinned,      // Clear all pinned items
} = usePinnedRows<User>({
  items,
  keyField: 'id',
  maxPinned: 5      // Optional: limit pinned rows
})
<template>
  <Table>
    <Tbody>
      <!-- Pinned top rows -->
      <Tr
        v-for="item in pinnedTop"
        :key="item.id"
        class="bg-yellow-50 dark:bg-yellow-900/20"
      >
        <Td>
          <Button size="sm" @click="unpin(String(item.id))">Unpin</Button>
        </Td>
        <Td>{{ item.name }}</Td>
      </Tr>

      <!-- Regular rows -->
      <Tr v-for="item in unpinnedItems" :key="item.id">
        <Td>
          <Button size="sm" @click="pinToTop(String(item.id), item)">Pin</Button>
        </Td>
        <Td>{{ item.name }}</Td>
      </Tr>

      <!-- Pinned bottom rows -->
      <Tr
        v-for="item in pinnedBottom"
        :key="item.id"
        class="bg-blue-50 dark:bg-blue-900/20"
      >
        <Td>
          <Button size="sm" @click="unpin(String(item.id))">Unpin</Button>
        </Td>
        <Td>{{ item.name }}</Td>
      </Tr>
    </Tbody>
  </Table>
</template>

useEditableCell

Inline cell editing with validation:

import { useEditableCell } from 'cisse-vue-ui/composables'

interface User {
  id: number
  name: string
  email: string
}

const {
  editingCell,      // Current cell being edited { rowKey, field }
  editingValue,     // Current edit value
  isEditing,        // Is any cell being edited?
  isSaving,         // Is save in progress?
  startEditing,     // Start editing a cell
  saveEdit,         // Save current edit
  cancelEdit,       // Cancel current edit
  getCellValue,     // Get original value for cell
} = useEditableCell<User>({
  onSave: async ({ rowKey, field, oldValue, newValue }) => {
    // Save to backend
    await api.updateUser(rowKey, { [field]: newValue })
  },
  validate: ({ field, newValue }) => {
    if (field === 'email' && !newValue.includes('@')) {
      return 'Invalid email address'
    }
    return true
  }
})
<template>
  <Table>
    <Tbody>
      <Tr v-for="item in items" :key="item.id">
        <Td @dblclick="startEditing(String(item.id), 'name', item.name)">
          <template v-if="editingCell?.rowKey === String(item.id) && editingCell?.field === 'name'">
            <input
              v-model="editingValue"
              @keydown.enter="saveEdit"
              @keydown.escape="cancelEdit"
              @blur="saveEdit"
              class="border rounded px-2 py-1"
            />
          </template>
          <template v-else>
            {{ item.name }}
          </template>
        </Td>
      </Tr>
    </Tbody>
  </Table>
</template>

useVirtualScroll

Virtual scrolling for large datasets:

import { useVirtualScroll } from 'cisse-vue-ui/composables'

const items = ref([/* 10,000+ items */])

const {
  visibleItems,     // Items to render
  totalHeight,      // Total scroll height
  offsetY,          // Top offset for positioning
  startIndex,       // First visible item index
  endIndex,         // Last visible item index
  scrollTop,        // Current scroll position
  onScroll,         // Scroll event handler
  scrollToIndex,    // Scroll to specific item
  containerRef,     // Ref for scroll container
} = useVirtualScroll({
  items,
  rowHeight: 48,         // Height of each row in px
  containerHeight: 500,  // Visible container height
  overscan: 5            // Extra rows to render (buffer)
})
<template>
  <div
    ref="containerRef"
    class="overflow-auto"
    :style="{ height: '500px' }"
    @scroll="onScroll"
  >
    <!-- Spacer for total height -->
    <div :style="{ height: totalHeight + 'px', position: 'relative' }">
      <!-- Positioned visible items -->
      <div :style="{ transform: `translateY(${offsetY}px)` }">
        <Table>
          <Tbody>
            <Tr
              v-for="(item, index) in visibleItems"
              :key="item.id"
              :style="{ height: '48px' }"
            >
              <Td>{{ startIndex + index + 1 }}</Td>
              <Td>{{ item.name }}</Td>
            </Tr>
          </Tbody>
        </Table>
      </div>
    </div>
  </div>

  <!-- Jump to row -->
  <Button @click="scrollToIndex(5000)">Jump to row 5000</Button>
</template>

Types

import type { Property, Notification, Breadcrumb } from 'cisse-vue-ui/types'

// Table column definition
const columns: Property[] = [
  { key: 'name', label: 'Name', sortable: true },
  { key: 'email', label: 'Email' },
  { key: 'status', label: 'Status', type: 'badge' }
]

// Breadcrumb navigation
const breadcrumbs: Breadcrumb[] = [
  { label: 'Home', to: '/' },
  { label: 'Users', to: '/users' },
  { label: 'Edit' }
]

Component Examples

Button

<Button variant="primary" size="md" :loading="isLoading">
  Save Changes
</Button>

<Button variant="outline" icon="lucide:plus">
  Add Item
</Button>

<Button variant="danger" icon="lucide:trash">
  Delete
</Button>

Tabs

<script setup>
import { ref } from 'vue'
import { Tabs, TabPanel } from 'cisse-vue-ui'

const activeTab = ref('profile')
const tabs = [
  { key: 'profile', label: 'Profile' },
  { key: 'settings', label: 'Settings' },
  { key: 'notifications', label: 'Notifications' }
]
</script>

<template>
  <Tabs v-model="activeTab" :tabs="tabs" variant="underline">
    <TabPanel value="profile">Profile content</TabPanel>
    <TabPanel value="settings">Settings content</TabPanel>
    <TabPanel value="notifications">Notifications content</TabPanel>
  </Tabs>
</template>

Switch

<Switch
  v-model="emailNotifications"
  label="Email notifications"
  description="Receive email updates about your account"
/>

Alert

<Alert variant="success" title="Success!" dismissible>
  Your changes have been saved successfully.
</Alert>

<Alert variant="error" title="Error">
  Something went wrong. Please try again.
</Alert>

Dropdown

<script setup>
import { Dropdown } from 'cisse-vue-ui'

const items = [
  { key: 'edit', label: 'Edit', icon: 'lucide:edit' },
  { key: 'duplicate', label: 'Duplicate', icon: 'lucide:copy' },
  { key: 'divider', divider: true },
  { key: 'delete', label: 'Delete', icon: 'lucide:trash', danger: true }
]

const handleSelect = (item) => {
  console.log('Selected:', item.key)
}
</script>

<template>
  <Dropdown :items="items" @select="handleSelect">
    <template #trigger-label>Actions</template>
  </Dropdown>
</template>

Stepper

<script setup>
import { ref } from 'vue'
import { Stepper } from 'cisse-vue-ui'

const currentStep = ref('step2')
const steps = [
  { key: 'step1', title: 'Account', description: 'Create account', icon: 'lucide:user' },
  { key: 'step2', title: 'Profile', description: 'Set up profile', icon: 'lucide:settings' },
  { key: 'step3', title: 'Complete', description: 'Ready to go!', icon: 'lucide:check' }
]
</script>

<template>
  <Stepper v-model="currentStep" :steps="steps" />
</template>

EmptyState

<EmptyState
  title="No results found"
  message="Try adjusting your search or filters"
  icon="lucide:search-x"
>
  <template #action>
    <Button variant="primary" size="sm">Clear filters</Button>
  </template>
</EmptyState>

Checkbox

<Checkbox
  v-model="accepted"
  label="Accept terms"
  description="I agree to the terms and conditions"
/>

CardWrapper

<script setup>
import { CardWrapper, Button } from 'cisse-vue-ui'
</script>

<template>
  <!-- Basic card with title and icon -->
  <CardWrapper
    title="Dashboard"
    description="Overview of your account"
    icon="lucide:layout-dashboard"
  >
    <div class="p-5">
      <p>Card content goes here.</p>
    </div>
  </CardWrapper>

  <!-- Card with image -->
  <CardWrapper
    title="Product"
    image="/product.jpg"
    image-position="top"
    image-height="200px"
  >
    <div class="p-5">Product description</div>
  </CardWrapper>

  <!-- Clickable card with accent -->
  <CardWrapper
    title="Clickable Card"
    accent="primary"
    clickable
    @click="handleClick"
  >
    <div class="p-5">Click me!</div>
  </CardWrapper>

  <!-- Card with actions -->
  <CardWrapper title="Settings" icon="lucide:settings">
    <template #actions>
      <Button size="sm" variant="outline">Cancel</Button>
      <Button size="sm">Save</Button>
    </template>
    <div class="p-5">Settings content</div>
    <template #footer>
      <p class="text-sm text-gray-500">Last updated: Today</p>
    </template>
  </CardWrapper>
</template>

CardWrapper Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | title | string | - | Card title | | description | string | - | Card description | | icon | string | - | Header icon (Iconify format) | | shadow | 'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' | 'md' | Shadow level | | rounded | 'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full' | 'lg' | Border radius | | padding | 'none' \| 'sm' \| 'md' \| 'lg' | 'none' | Content padding | | border | 'none' \| 'default' \| 'primary' \| 'secondary' | 'none' | Border style | | variant | 'default' \| 'glass' \| 'outline' \| 'flat' | 'default' | Visual variant | | accent | 'none' \| 'primary' \| 'secondary' \| 'success' \| 'warning' \| 'danger' \| 'info' | 'none' | Colored accent border | | clickable | boolean | false | Make card clickable with hover effects | | selected | boolean | false | Selected state with ring indicator | | disabled | boolean | false | Disabled state | | image | string | - | Image URL | | imagePosition | 'top' \| 'bottom' \| 'left' \| 'right' \| 'background' | 'top' | Image position | | loading | boolean | false | Show loading skeleton |

IconPicker

<script setup>
import { ref } from 'vue'
import { IconPicker } from 'cisse-vue-ui'

const selectedIcon = ref('mdi:heart')
</script>

<template>
  <IconPicker
    v-model="selectedIcon"
    label="Select Icon"
    help="Choose an icon for your item"
    :collections="['mdi', 'heroicons', 'lucide']"
  />
</template>

TableComponent

<script setup>
import { ref } from 'vue'
import { TableComponent } from 'cisse-vue-ui'

const properties = [
  { name: 'name', label: 'Name', main: true },
  { name: 'email', label: 'Email' },
  { name: 'role', label: 'Role', type: 'badge' }
]

const items = [
  { id: 1, name: 'John Doe', email: '[email protected]', role: 'Admin' },
  { id: 2, name: 'Jane Smith', email: '[email protected]', role: 'User' }
]

// Selection support
const selectedItems = ref(new Set())
const toggleSelect = (id) => {
  if (selectedItems.value.has(id)) {
    selectedItems.value.delete(id)
  } else {
    selectedItems.value.add(id)
  }
}
</script>

<template>
  <TableComponent
    :properties="properties"
    :items="items"
    selectable
    :selected-items="selectedItems"
    @select="toggleSelect"
    @select-all="toggleSelectAll"
  >
    <template #action="{ item }">
      <TableAction icon="lucide:edit" @click="edit(item)" />
      <TableAction icon="lucide:trash" variant="danger" @click="delete(item)" />
    </template>
  </TableComponent>
</template>

ResponsiveList

A component that automatically switches between a mobile card layout and a desktop table layout based on screen size.

<script setup>
import { ref } from 'vue'
import { ResponsiveList } from 'cisse-vue-ui'

const columns = [
  { key: 'name', label: 'Name' },
  { key: 'email', label: 'Email' },
  { key: 'status', label: 'Status' }
]

const items = [
  { id: 1, name: 'John Doe', email: '[email protected]', status: 'active' },
  { id: 2, name: 'Jane Smith', email: '[email protected]', status: 'inactive' }
]

const selectedItems = ref(new Set())

const toggleSelect = (id) => {
  if (selectedItems.value.has(id)) {
    selectedItems.value.delete(id)
  } else {
    selectedItems.value.add(id)
  }
}

const toggleSelectAll = () => {
  if (selectedItems.value.size === items.length) {
    selectedItems.value.clear()
  } else {
    items.forEach(item => selectedItems.value.add(String(item.id)))
  }
}
</script>

<template>
  <ResponsiveList
    :items="items"
    :columns="columns"
    key-field="id"
    selectable
    :selected-items="selectedItems"
    breakpoint="lg"
    @select="toggleSelect"
    @select-all="toggleSelectAll"
  >
    <!-- Mobile view: avatar -->
    <template #avatar="{ item }">
      <div class="w-10 h-10 rounded-full bg-primary-500 flex items-center justify-center text-white">
        {{ item.name[0] }}
      </div>
    </template>

    <!-- Mobile view: content -->
    <template #mobileContent="{ item }">
      <h3 class="font-semibold">{{ item.name }}</h3>
      <p class="text-sm text-gray-500">{{ item.email }}</p>
    </template>

    <!-- Mobile view: actions -->
    <template #mobileActions="{ item }">
      <button @click="viewItem(item)">View</button>
    </template>

    <!-- Desktop table: custom cell rendering -->
    <template #cell-name="{ item }">
      <span class="font-medium">{{ item.name }}</span>
    </template>

    <template #cell-status="{ item }">
      <span :class="item.status === 'active' ? 'text-green-600' : 'text-red-600'">
        {{ item.status }}
      </span>
    </template>

    <!-- Desktop table: actions column -->
    <template #actions="{ item }">
      <Button size="sm" variant="ghost" @click="edit(item)">Edit</Button>
    </template>

    <!-- Empty state -->
    <template #empty>
      <EmptyState title="No items" message="No items to display" />
    </template>
  </ResponsiveList>
</template>

ResponsiveList Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | items | Array | required | Array of items to display | | columns | Array | required | Column definitions with key or name, label, and optional type | | keyField | string | 'id' | Field to use as unique key for items | | selectable | boolean | false | Enable selection mode | | selectedItems | Set<string> | - | Set of selected item keys | | selectableFilter | Function | - | Filter function to determine if an item is selectable | | breakpoint | string | 'lg' | Breakpoint for switching views: 'sm', 'md', 'lg', 'xl', '2xl' |

MobileList

A mobile-optimized card-based list component with selection support.

<script setup>
import { MobileList } from 'cisse-vue-ui'
</script>

<template>
  <MobileList
    :items="items"
    key-field="id"
    selectable
    :selected-items="selectedItems"
    @select="toggleSelect"
    @select-all="toggleSelectAll"
  >
    <template #avatar="{ item }">
      <div class="w-12 h-12 rounded-full bg-blue-500" />
    </template>

    <template #content="{ item }">
      <h3>{{ item.name }}</h3>
      <p>{{ item.description }}</p>
    </template>

    <template #actions="{ item }">
      <button>View</button>
    </template>
  </MobileList>
</template>

MenuItem

<script setup>
import { useRoute } from 'vue-router'
import { MenuItem } from 'cisse-vue-ui'

const route = useRoute()

const menuItem = {
  label: 'Dashboard',
  link: '/dashboard',
  icon: 'lucide:layout-dashboard'
}
</script>

<template>
  <!-- Auto-detect active state from current route -->
  <MenuItem :menu-item="menuItem" :current-path="route.path" />

  <!-- Or manually control active state -->
  <MenuItem :menu-item="menuItem" :active="true" />
</template>

AuthLayout

Split-panel layout for authentication pages (login, register, password reset).

<script setup>
import { ref } from 'vue'
import { AuthLayout, type AuthFeature } from 'cisse-vue-ui'

const email = ref('')
const password = ref('')

const features: AuthFeature[] = [
  { icon: 'lucide:shield', text: 'Secure authentication' },
  { icon: 'lucide:zap', text: 'Fast login' },
  { icon: 'lucide:users', text: 'Team collaboration' },
]

function handleSubmit() {
  // Handle login
}
</script>

<template>
  <AuthLayout
    app-name="My App"
    app-icon="lucide:box"
    headline="Welcome to"
    sub-headline="My Application"
    description="Sign in to access your account."
    :features="features"
    form-title="Sign In"
    form-subtitle="Enter your credentials"
    home-link="/"
  >
    <form @submit.prevent="handleSubmit" class="space-y-4">
      <input v-model="email" type="email" placeholder="Email" class="..." />
      <input v-model="password" type="password" placeholder="Password" class="..." />
      <button type="submit">Sign In</button>
    </form>

    <template #form-footer>
      <p class="text-center mt-6">
        Don't have an account? <a href="/register">Sign up</a>
      </p>
    </template>
  </AuthLayout>
</template>

AuthLayout Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | appName | string | '' | Application name displayed in logo | | appIcon | string | 'lucide:box' | Iconify icon for app logo | | headline | string | '' | First line of headline | | subHeadline | string | '' | Second line with decorative underline | | description | string | '' | Description paragraph | | features | AuthFeature[] | [] | List of features with icon and text | | gradientFrom | string | 'from-primary-700' | Tailwind gradient start class | | gradientVia | string | '' | Tailwind gradient middle class | | gradientTo | string | 'to-primary-800' | Tailwind gradient end class | | showDecorations | boolean | true | Show floating decorative shapes | | showPattern | boolean | true | Show dot pattern overlay | | underlineColor | string | 'rgba(165, 180, 252, 0.5)' | Sub-headline underline color | | formTitle | string | '' | Form panel title | | formSubtitle | string | '' | Form panel subtitle | | homeLink | string | '/' | Mobile logo link URL | | brandingAnimation | string | '' | CSS class for branding animation | | formAnimation | string | '' | CSS class for form panel animation |

AuthLayout Slots

| Slot | Description | |------|-------------| | default | Form content (inside the white card) | | branding-panel | Complete override of the branding panel | | branding-logo | Custom logo in branding panel | | branding-headline | Custom headline content | | branding-features | Custom features list | | branding-content | Additional content after features | | mobile-logo | Custom mobile logo | | form-header | Content above the form card | | form-footer | Content below the form card |

BaseLayout

<script setup>
import { useRoute } from 'vue-router'
import { BaseLayout } from 'cisse-vue-ui'

const route = useRoute()

const menuItems = [
  { label: 'Dashboard', link: '/', icon: 'lucide:home' },
  { label: 'Users', link: '/users', icon: 'lucide:users' },
  { label: 'Settings', link: '/settings', icon: 'lucide:settings' }
]
</script>

<template>
  <BaseLayout
    :menu-items="menuItems"
    :current-path="route.path"
    :show-dark-toggle="true"
    menu-position="top"
  >
    <template #logo>
      <img src="/logo.svg" alt="Logo" class="h-8" />
    </template>

    <RouterView />
  </BaseLayout>
</template>

BaseLayout Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | menuItems | MenuItemProps[] | [] | Menu items for the sidebar | | appName | string | 'App' | App/brand name displayed in sidebar | | appIcon | string | 'lucide:box' | App icon (Iconify icon name) | | sidebarOpen | boolean | true | Whether sidebar is open (v-model:sidebarOpen) | | dark | boolean | false | Whether dark mode is enabled (v-model:dark) | | showDarkToggle | boolean | true | Show dark mode toggle in header | | sidebarClass | string | 'bg-[#172b4c]...' | CSS classes for sidebar background | | currentPath | string | - | Current route path for menu active state | | userName | string | - | User display name | | userAvatar | string | - | User avatar (initials or image URL) | | userMenuItems | UserMenuItem[] | [] | User menu dropdown items | | menuPosition | 'top' \| 'center' \| 'bottom' | 'top' | Menu vertical position in sidebar |

BaseLayout Slots

| Slot | Description | |------|-------------| | default | Main content area (or renders RouterView if available) | | logo | Custom logo in sidebar header | | menu | Custom menu content (receives currentPath) | | sidebar-footer | Content at bottom of sidebar | | header-center | Center content in header | | header-actions | Action buttons in header (before dark toggle) |

Dark Mode

Components support dark mode via the .dark class on a parent element:

<html class="dark">
  <!-- Components will use dark theme -->
</html>

Use the useDarkMode composable or implement your own toggle:

const { isDark, toggle } = useDarkMode()

License

MIT