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

nicklabs-ui

v1.0.120

Published

A Vue 3 component library with glassmorphism design, built for modern web applications.

Readme

nicklabs-ui

A Vue 3 component library with glassmorphism design, built for modern web applications.

Version: 1.0.113 | Framework: Vue 3.5+


Table of Contents


Installation

npm install nicklabs-ui
# or
pnpm add nicklabs-ui

Setup

1. Import Styles

In your main entry file (e.g., main.ts):

import { createApp } from 'vue'
import App from './App.vue'

// Required: reset and CSS variables
import 'nicklabs-ui/reset.css'
import 'nicklabs-ui/variables.css'

// Required: component styles
import 'nicklabs-ui/nicklabs-ui.css'

createApp(App).mount('#app')

2. Import Components

Import components individually as needed:

import { NButton, NInput, NModal, useToast } from 'nicklabs-ui'

3. Register All Components (Optional)

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { NickLabsUI } from 'nicklabs-ui'

const app = createApp(App)
app.use(router)
app.use(NickLabsUI, { router })
app.mount('#app')

Passing router enables useBreadcrumb and useRouteModal without any additional setup.


Components


Form Components


NButton

A versatile button supporting multiple visual variants and semantic intents.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | variant | "none" \| "solid" \| "outline" \| "bordered" \| "ghost" \| "mute" \| "link" | "none" | Visual style | | intent | "none" \| "primary" \| "secondary" \| "error" \| "success" \| "warning" \| "info" \| "default" \| "purple" | "none" | Semantic color intent | | loading | boolean | false | Show spinner and disable interaction | | disabled | boolean | false | Disable interaction | | size | "sm" \| "md" \| "lg" | "md" | Button size | | radiusSize | "sm" \| "md" \| "lg" \| "xl" \| "full" | "md" | Border radius | | type | "button" \| "submit" \| "reset" | "button" | HTML button type | | square | boolean | false | Equal width/height (icon button) | | padding | string | — | Custom padding override | | width | string | — | Custom width override | | height | string | — | Custom height override |

Usage

<template>
  <!-- Basic -->
  <NButton>Click me</NButton>

  <!-- Variants -->
  <NButton variant="solid" intent="primary">Primary</NButton>
  <NButton variant="outline" intent="success">Success</NButton>
  <NButton variant="bordered" intent="error">Bordered (intent-colored border)</NButton>
  <NButton variant="ghost" intent="warning">Warning</NButton>
  <NButton variant="mute" intent="error">Danger</NButton>
  <NButton variant="link" intent="primary">Link style</NButton>

  <!-- Intents -->
  <NButton variant="solid" intent="secondary">Secondary</NButton>
  <NButton variant="solid" intent="default">Default (grey)</NButton>
  <NButton variant="solid" intent="purple">Purple</NButton>

  <!-- Loading state -->
  <NButton variant="solid" intent="primary" loading>Saving...</NButton>
  <NButton variant="solid" intent="primary" :loading="isSubmitting" @click="submit">
    Submit
  </NButton>

  <!-- Sizes -->
  <NButton size="sm">Small</NButton>
  <NButton size="md">Medium</NButton>
  <NButton size="lg">Large</NButton>

  <!-- Submit button -->
  <NButton type="submit" intent="primary">Submit</NButton>

  <!-- Disabled -->
  <NButton disabled>Disabled</NButton>

  <!-- Icon button (square) -->
  <NButton square size="sm">
    <svg>...</svg>
  </NButton>
</template>

<script setup>
import { ref } from 'vue'
import { NButton } from 'nicklabs-ui'

const isSubmitting = ref(false)

async function submit() {
  isSubmitting.value = true
  await doSomething()
  isSubmitting.value = false
}
</script>

NInput

A full-featured text input with support for password visibility, number controls, and clearing.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | modelValue | string \| number | — | Bound value (v-model) | | type | string | "text" | HTML input type | | placeholder | string | — | Placeholder text | | disabled | boolean | false | Disable input | | readonly | boolean | false | Read-only mode | | inline | boolean | false | Display title and input on the same line | | clearable | boolean | false | Show clear button | | maxlength | number | — | Maximum character length | | min | number | — | Minimum value (for type="number") | | max | number | — | Maximum value (for type="number") | | title | string | — | Label above the input | | autocomplete | string | — | HTML autocomplete attribute |

Events

| Event | Payload | Description | |-------|---------|-------------| | update:modelValue | string \| number | Value changed | | focus | FocusEvent | Input focused | | blur | FocusEvent | Input blurred | | input | Event | Input event | | change | Event | Change event | | keydown | KeyboardEvent | Key pressed | | clear | — | Clear button clicked |

Usage

<template>
  <NInput v-model="username" placeholder="Enter username" title="Username" />

  <!-- Password with visibility toggle -->
  <NInput v-model="password" type="password" placeholder="Enter password" />

  <!-- Clearable -->
  <NInput v-model="search" placeholder="Search..." clearable />

  <!-- Number with min/max -->
  <NInput v-model="age" type="number" :min="0" :max="120" title="Age" />

  <!-- Inline title -->
  <NInput v-model="username" title="Username" placeholder="Enter username" inline />
</template>

<script setup>
import { ref } from 'vue'
import { NInput } from 'nicklabs-ui'

const username = ref('')
const password = ref('')
const search = ref('')
const age = ref(0)
</script>

NTextarea

Multi-line text input with optional character count display.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | modelValue | string | — | Bound value (v-model) | | placeholder | string | — | Placeholder text | | disabled | boolean | false | Disable textarea | | readonly | boolean | false | Read-only mode | | rows | number | 4 | Visible row count | | maxLength | number | — | Maximum character count | | title | string | — | Label above the textarea | | showCount | boolean | false | Show character counter | | wrap | "soft" \| "off" | "soft" | Text wrapping behavior | | autofocus | boolean | false | Auto-focus on mount | | autocomplete | string | — | HTML autocomplete attribute |

Events

| Event | Payload | Description | |-------|---------|-------------| | update:modelValue | string | Value changed | | focus | FocusEvent | Textarea focused | | blur | FocusEvent | Textarea blurred | | input | Event | Input event | | change | Event | Change event | | keydown | KeyboardEvent | Key pressed | | paste | ClipboardEvent | Content pasted |

Usage

<template>
  <NTextarea
    v-model="description"
    title="Description"
    placeholder="Enter description..."
    :rows="5"
    :maxLength="500"
    showCount
  />
</template>

<script setup>
import { ref } from 'vue'
import { NTextarea } from 'nicklabs-ui'

const description = ref('')
</script>

NCheckbox

Checkbox input supporting both single and multi-option modes.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | modelValue | boolean \| any[] | — | Bound value (v-model) | | multiple | boolean | false | Enable multi-option mode | | options | Record<string, any>[] | — | Options for multi mode | | disabled | boolean | false | Disable input | | title | string | — | Group label displayed above options | | label | string | — | Text beside checkbox in single mode (overridable by default slot) | | inline | boolean | false | Display title and options on the same line | | direction | "row" \| "column" | "row" | Layout direction for options | | autofocus | boolean | false | Auto-focus on mount | | allowDeselect | boolean | true | Allow deselecting the current choice in both single-checkbox and single-select (options without multiple) modes; set to false to require a selection at all times | | formatLabel | (option: any) => string | (opt) => opt.label | Function to extract display text from an option | | formatValue | (option: any) => any | (opt) => opt.value | Function to extract the value from an option |

Events

| Event | Description | |-------|-------------| | update:modelValue | Value changed | | change | Generic change event | | change:item | Selected OptionItem changed | | change:value | Selected value changed | | change:values | Selected values array changed (multiple mode) |

Usage

<template>
  <!-- Single checkbox -->
  <NCheckbox v-model="agreed" title="I agree to terms" />

  <!-- Multiple checkboxes -->
  <NCheckbox
    v-model="selected"
    multiple
    :options="options"
    direction="column"
    title="Select interests"
  />

  <!-- Inline title -->
  <NCheckbox v-model="selected" multiple :options="options" title="Interests" inline />

  <!-- Custom field names -->
  <NCheckbox
    v-model="selected"
    multiple
    :options="[{ name: 'Apple', id: 'apple' }, { name: 'Banana', id: 'banana' }]"
    :format-label="(opt) => opt.name"
    :format-value="(opt) => opt.id"
  />
</template>

<script setup>
import { ref } from 'vue'
import { NCheckbox } from 'nicklabs-ui'

const agreed = ref(false)
const selected = ref([])
const options = [
  { label: 'Vue', value: 'vue' },
  { label: 'React', value: 'react' },
  { label: 'Angular', value: 'angular' },
]
</script>

NSelect

Dropdown select with search, multi-select, and clear support.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | modelValue | any \| any[] | — | Bound value (v-model) | | options | OptionItem[] | [] | Dropdown options | | multiple | boolean | false | Allow multiple selection | | searchable | boolean | false | Enable search/filter | | clearable | boolean | false | Show clear button | | placeholder | string | — | Placeholder text | | disabled | boolean | false | Disable input | | title | string | — | Label above dropdown | | multipleDisplay | "count" \| "tags" | "tags" | How selected items display | | formatLabel | (option: any) => string | (opt) => opt.label | Function to extract display text from an option | | formatValue | (option: any) => any | (opt) => opt.value | Function to extract the value from an option |

Events

| Event | Description | |-------|-------------| | update:modelValue | Value changed | | change | Generic change event | | change:item | Selected OptionItem | | change:value | Selected value | | change:values | Selected values array (multiple mode) |

Usage

<template>
  <!-- Basic select -->
  <NSelect v-model="city" :options="cities" placeholder="Select city" title="City" />

  <!-- Searchable + clearable -->
  <NSelect v-model="country" :options="countries" searchable clearable />

  <!-- Multiple selection -->
  <NSelect
    v-model="tags"
    :options="tagOptions"
    multiple
    multipleDisplay="tags"
    title="Tags"
  />

  <!-- Custom keys (API data with non-standard field names) -->
  <NSelect
    v-model="city"
    :options="[{ name: 'Taipei', id: 'taipei' }, { name: 'Tokyo', id: 'tokyo' }]"
    :format-label="(opt) => opt.name"
    :format-value="(opt) => opt.id"
    placeholder="Select city"
  />

  <!-- Combine multiple fields into display text -->
  <NSelect
    v-model="city"
    :options="apiData"
    :format-label="(opt) => `${opt.name} (${opt.code})`"
    :format-value="(opt) => opt.id"
  />
</template>

<script setup>
import { ref } from 'vue'
import { NSelect } from 'nicklabs-ui'

const city = ref('')
const country = ref(null)
const tags = ref([])

const cities = [
  { label: 'Taipei', value: 'taipei' },
  { label: 'Tokyo', value: 'tokyo' },
  { label: 'Seoul', value: 'seoul' },
]
const tagOptions = [
  { label: 'Frontend', value: 'frontend' },
  { label: 'Backend', value: 'backend' },
  { label: 'DevOps', value: 'devops' },
]
</script>

NFileSelect

Drag-and-drop file selector.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | multiple | boolean | false | Allow multiple file selection | | disabled | boolean | false | Disable input | | label | string | — | Button/drop zone label | | hint | string | — | Helper text below | | accept | string | — | Accepted MIME types (e.g. "image/*") | | title | string | — | Label above component |

Events

| Event | Payload | Description | |-------|---------|-------------| | change:file | File | Single file selected | | change:files | File[] | Files selected (multiple mode) |

Usage

<template>
  <NFileSelect
    title="Upload Avatar"
    label="Drop image here or click to browse"
    hint="Accepts PNG, JPG up to 2MB"
    accept="image/*"
    @change:file="handleFile"
  />

  <!-- Multiple files -->
  <NFileSelect multiple accept=".pdf,.doc" @change:files="handleFiles" />
</template>

<script setup>
import { NFileSelect } from 'nicklabs-ui'

function handleFile(file: File) {
  console.log('Selected:', file.name)
}

function handleFiles(files: File[]) {
  console.log('Selected:', files.length, 'files')
}
</script>

NImageSelect

A card-grid image uploader with drag-and-drop, multi-file support, automatic resolution probing, a built-in dark preview lightbox, and a confirm dialog before removal. Files are tracked as NImageSelectItem[] via v-model. Supports backfilling existing data by URL and a read-only view mode (see notes below).

Removal uses useAlert internally, so mount <NAlert /> once at the app root (see NAlert).

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | modelValue | NImageSelectItem[] | [] | Bound image list (v-model) | | accept | string | "image/*" | Accepted types — used for both the file dialog and drop validation. Supports image/*, exact MIME (image/png), extensions (.png), and comma-separated combinations | | multiple | boolean | false | Allow multiple images; when false, extra files are rejected with a count error | | maxSize | number | 0 | Per-file size limit in MB (0 = unlimited) | | disabled | boolean | false | Disable adding and removing (also dims the component) | | readonly | boolean | false | View-only mode: browse and preview only. Hides the add/delete affordances and disables drag/drop, but keeps a normal (non-dimmed) appearance | | title | string | "" | Label above the grid | | showCount | boolean | false | Show a count chip next to the title | | emptyTitle | string | "將圖片拖放到這裡" | Empty-state heading | | emptyHint | string | "或點擊以選擇檔案 · 支援 JPG / PNG / WebP / GIF,可一次多張" | Empty-state sub-text | | autoRevoke | boolean | true | Revoke the component-created object URLs on unmount. Set false to keep the URLs alive and take over their lifecycle in the parent (you must then call URL.revokeObjectURL yourself) |

Events

| Event | Payload | Description | |-------|---------|-------------| | update:modelValue | NImageSelectItem[] | The full list changed (v-model) | | change | NImageSelectItem[] | The full list changed | | add | NImageSelectItem[] | Items just added (this batch only) | | remove | NImageSelectItem | An item was removed (after confirm) | | error | NImageSelectError | A file was rejected (type / size / count) |

Usage

<template>
  <NImageSelect
    v-model="images"
    title="Product Photos"
    multiple
    :max-size="5"
    show-count
    @add="onAdd"
    @error="onError"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { NImageSelect } from 'nicklabs-ui'
import type { NImageSelectItem, NImageSelectError } from 'nicklabs-ui'

const images = ref<NImageSelectItem[]>([])

function onAdd(items: NImageSelectItem[]) {
  console.log('Added', items.length, 'images')
}

function onError(err: NImageSelectError) {
  console.warn('Rejected:', err.file.name, err.type)
}

// Each item carries the native File for upload:
async function upload() {
  const form = new FormData()
  images.value.forEach((img) => img.file && form.append('files', img.file))
  await api.upload(form)
}
</script>

width / height are probed asynchronously after a file is added and written back into the item. The component creates object URLs for previews and revokes them on removal automatically, and on unmount too unless autoRevoke is false — in which case the parent owns the URLs and must revoke them. Only blob: URLs the component created are revoked; remote URLs are never touched.

Backfilling existing data (edit mode)

After upload, the server usually returns only a URL. To show that data, push items with just id and url (leave file empty) — the component probes width/height and derives the display name from the URL:

const images = ref<NImageSelectItem[]>([
  { id: 'remote-1', url: 'https://cdn.example.com/images/cover.jpg' },
])
// <NImageSelect v-model="images" readonly title="Image content" />

NVideoSelect

A card-grid video uploader. Same interaction model as NImageSelect — drag-and-drop, multi-file, confirm-before-remove, and a built-in preview modal — but items also carry duration, width, and height. Supports backfilling existing data by URL and a read-only view mode (same as NImageSelect).

Removal uses useAlert internally, so mount <NAlert /> once at the app root (see NAlert).

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | modelValue | NVideoSelectItem[] | [] | Bound video list (v-model) | | accept | string | "video/*" | Accepted types — supports video/*, exact MIME (video/mp4), extensions (.mp4), and comma-separated combinations | | multiple | boolean | false | Allow multiple videos | | maxSize | number | 0 | Per-file size limit in MB (0 = unlimited) | | disabled | boolean | false | Disable adding and removing (also dims the component) | | readonly | boolean | false | View-only mode: browse and preview only. Hides the add/delete affordances and disables drag/drop, but keeps a normal (non-dimmed) appearance | | title | string | "" | Label above the grid | | showCount | boolean | false | Show a count chip next to the title | | emptyTitle | string | "將影片拖放到這裡" | Empty-state heading | | emptyHint | string | "或點擊以選擇檔案 · 支援 MP4 / MOV / WebM,可一次多支" | Empty-state sub-text | | autoRevoke | boolean | true | Revoke the component-created object URLs on unmount. Set false to keep the URLs alive and take over their lifecycle in the parent (you must then call URL.revokeObjectURL yourself) |

Events

| Event | Payload | Description | |-------|---------|-------------| | update:modelValue | NVideoSelectItem[] | The full list changed (v-model) | | change | NVideoSelectItem[] | The full list changed | | add | NVideoSelectItem[] | Items just added (this batch only) | | remove | NVideoSelectItem | An item was removed (after confirm) | | error | NVideoSelectError | A file was rejected (type / size / count) |

Usage

<template>
  <NVideoSelect v-model="videos" title="Clips" multiple :max-size="200" @error="onError" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { NVideoSelect } from 'nicklabs-ui'
import type { NVideoSelectItem, NVideoSelectError } from 'nicklabs-ui'

const videos = ref<NVideoSelectItem[]>([])

function onError(err: NVideoSelectError) {
  console.warn('Rejected:', err.file.name, err.type)
}
</script>

Backfilling existing data: push items with just id and url (leave file empty); duration / width / height are probed and the name is derived from the URL. Only component-created blob: URLs are revoked on remove/unmount — remote URLs are never touched.


NAudioSelect

An audio uploader that decodes each file with the Web Audio API to render a waveform, showing a skeleton row while decoding. Each row has an inline player (play/pause + click-and-drag seek on the waveform); only one track plays at a time. Same drag-and-drop / multi-file / confirm-before-remove model as the other media selectors; items carry duration. Supports backfilling existing data by URL and a read-only view mode.

Removal uses useAlert internally, so mount <NAlert /> once at the app root (see NAlert).

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | modelValue | NAudioSelectItem[] | [] | Bound audio list (v-model) | | accept | string | ".mp3,.wav,.ogg,.m4a,.aac,.flac,audio/mpeg,audio/wav,audio/ogg,audio/aac,audio/flac" | Accepted types. Defaults to an explicit extension + MIME list (rather than audio/*) so the dialog doesn't offer .mp4-style containers | | multiple | boolean | false | Allow multiple files | | maxSize | number | 0 | Per-file size limit in MB (0 = unlimited) | | disabled | boolean | false | Disable adding and removing (also dims the component) | | readonly | boolean | false | View-only mode: browse and play only. Hides the add/delete affordances and disables drag/drop, but keeps a normal (non-dimmed) appearance. Playback and seek stay enabled | | title | string | "" | Label above the list | | showCount | boolean | false | Show a count chip next to the title | | emptyTitle | string | "將音訊拖放到這裡" | Empty-state heading | | emptyHint | string | "或點擊以選擇檔案 · 支援 MP3 / WAV / OGG / M4A,可一次多首" | Empty-state sub-text | | autoRevoke | boolean | true | Revoke the component-created object URLs on unmount. Set false to keep the URLs alive and take over their lifecycle in the parent (you must then call URL.revokeObjectURL yourself) |

Events

| Event | Payload | Description | |-------|---------|-------------| | update:modelValue | NAudioSelectItem[] | The full list changed (v-model) | | change | NAudioSelectItem[] | The full list changed | | add | NAudioSelectItem[] | Items just added (this batch only) | | remove | NAudioSelectItem | An item was removed (after confirm) | | error | NAudioSelectError | A file was rejected (type / size / count) |

Usage

<template>
  <NAudioSelect v-model="tracks" title="Soundtrack" multiple @error="onError" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { NAudioSelect } from 'nicklabs-ui'
import type { NAudioSelectItem, NAudioSelectError } from 'nicklabs-ui'

const tracks = ref<NAudioSelectItem[]>([])

function onError(err: NAudioSelectError) {
  console.warn('Rejected:', err.file.name, err.type)
}
</script>

Backfilling existing data (edit mode)

Push items with just id and url (leave file empty); the display name is derived from the URL:

const tracks = ref<NAudioSelectItem[]>([
  { id: 'remote-1', url: 'https://cdn.example.com/audios/track.mp3' },
])
// <NAudioSelect v-model="tracks" readonly title="Audio content" />

Waveform vs. duration for remote URLs. The waveform needs the raw bytes (fetch + decode), which is subject to CORS — if the host doesn't allow it, the row falls back to a plain progress bar (track + played fill) instead of a waveform. Either way, click/drag seek and the progress visual stay functional — seeking only depends on the audio element's duration, not the waveform. Duration itself always resolves: it falls back to the <audio> element's metadata (no CORS needed), and playback works regardless.

Waveform cache (survives v-if remount). Decoded waveforms are kept in a module-level cache keyed by url, so toggling the component with v-if (unmount → remount) restores the waveform instantly with no re-decode and no flash. A v-if unmount does not clear the cache; only an explicit remove (the delete button) evicts the entry. Note this is independent of autoRevoke: the cache preserves the waveform, but autoRevoke: true still revokes the blob on unmount, which breaks playback of locally-added files after remount — set autoRevoke: false (and take over URL.revokeObjectURL in the parent) if you need playback to survive a remount.


NSwitch

Animated toggle switch.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | modelValue | boolean | false | Bound value (v-model) | | disabled | boolean | false | Disable switch | | size | "sm" \| "md" \| "lg" | "md" | Switch size | | title | string | — | Label above the switch | | inline | boolean | false | Display title and switch on the same line |

Events

| Event | Payload | Description | |-------|---------|-------------| | update:modelValue | boolean | Value changed | | change | boolean | Value changed |

Usage

<template>
  <!-- Basic -->
  <NSwitch v-model="enabled" />

  <!-- With title (stacked) -->
  <NSwitch v-model="enabled" title="Enable notifications" />

  <!-- Inline title -->
  <NSwitch v-model="darkMode" title="Dark mode" size="lg" inline />
</template>

<script setup>
import { ref } from 'vue'
import { NSwitch } from 'nicklabs-ui'

const enabled = ref(false)
const darkMode = ref(false)
</script>

NDatePicker

Date picker with range selection and custom format support.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | modelValue | string \| Date | — | Bound value (v-model) for single date | | start | string | — | Range start (v-model:start) | | end | string | — | Range end (v-model:end) | | placeholder | string | "請選擇日期" | Placeholder text | | disabled | boolean | false | Disable picker | | clearable | boolean | false | Show clear button | | title | string | — | Label above picker | | format | string | "YYYY-MM-DD" | Date format string (supports YYYY, MM, DD, HH, mm) | | range | boolean | false | Enable range selection mode |

Events

| Event | Payload | Description | |-------|---------|-------------| | update:modelValue | string | Single date value changed | | change | string | Date changed | | clear | — | Clear button clicked | | update:start | string | Range start changed | | update:end | string | Range end changed |

Usage

<template>
  <!-- Single date -->
  <NDatePicker v-model="date" title="Select Date" clearable />

  <!-- With time format -->
  <NDatePicker v-model="datetime" format="YYYY-MM-DD HH:mm" title="Date & Time" />

  <!-- Date range -->
  <NDatePicker
    range
    v-model:start="startDate"
    v-model:end="endDate"
    title="Date Range"
  />
</template>

<script setup>
import { ref } from 'vue'
import { NDatePicker } from 'nicklabs-ui'

const date = ref('')
const datetime = ref('')
const startDate = ref('')
const endDate = ref('')
</script>

NTimePicker

Time picker for selecting hour, minute, and second. Each unit can be toggled on or off independently, and the bound string is trimmed to match the enabled units. Uses a scrolling-list dropdown on desktop and a bottom sheet on mobile.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | modelValue | string | "" | Bound value (v-model). Format follows the enabled units, e.g. HH:mm:ss, HH:mm, mm:ss | | hour | boolean | true | Show the "hour" column | | minute | boolean | true | Show the "minute" column | | second | boolean | true | Show the "second" column | | placeholder | string | "請選擇時間" | Placeholder text | | disabled | boolean | false | Disable picker | | clearable | boolean | false | Show clear button | | title | string | "" | Label above picker | | size | "sm" \| "md" \| "lg" | "md" | Trigger size |

Events

| Event | Payload | Description | |-------|---------|-------------| | update:modelValue | string | Value changed (v-model) | | change | string | Value changed | | clear | — | Clear button clicked |

Usage

<template>
  <!-- Hour : minute : second -->
  <NTimePicker v-model="time" title="Select Time" clearable />

  <!-- Hour : minute only (value becomes "HH:mm") -->
  <NTimePicker v-model="timeHM" :second="false" title="Hour & Minute" />

  <!-- Hour only (value becomes "HH") -->
  <NTimePicker v-model="hourOnly" :minute="false" :second="false" title="Hour" />
</template>

<script setup>
import { ref } from 'vue'
import { NTimePicker } from 'nicklabs-ui'

const time = ref('09:30:00')
const timeHM = ref('14:30')
const hourOnly = ref('08')
</script>

Data Display


NTable

A type-safe, sortable data table with support for batch selection and action slots.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | columns | NTableColumn[] | [] | Column definitions | | items | T[] | [] | Row data | | bordered | boolean | false | Show borders | | hoverable | boolean | true | Highlight rows on hover | | loading | boolean | false | Show loading state | | emptyTitle | string | "目前沒有資料" | Title when no data | | emptyDescription | string | — | Description when no data | | itemKey | string | "id" | Unique key field | | stickyActions | boolean | false | Pin the actions column to the right edge during horizontal scroll. Also pinned automatically when any column has sticky: true | | stickyBatch | boolean | false | Pin the leftmost batch column to the left edge during horizontal scroll |

Events

| Event | Payload | Description | |-------|---------|-------------| | sort | NTableSortState | Column sort changed | | click | T | Row was clicked |

Slots

| Slot | Description | |------|-------------| | batch | Content in the batch selection header column | | actions-header | Custom header text for the actions column (default: "操作") | | item | Custom cell renderer for a row (scoped: { item, index, sticky }). Render one <td> per column; for a sticky column, spread v-bind="sticky(columnKey)" onto its <td> | | actions | Custom action buttons per row (scoped: { item, index }) | | empty | Custom empty state content |

NTableColumn Interface

interface NTableColumn {
  key: string       // Data field key
  label: string     // Column header text
  sortable?: boolean
  sticky?: boolean  // Pin this column to the right edge (header + actions pinned together)
}

Sticky columns

For wide tables that scroll horizontally, mark columns with sticky: true to pin them to the right edge. The actions column is pinned automatically whenever any column is sticky (or set stickyActions to pin only the actions column). Offsets are measured at runtime, so columns can be any width.

The header <th> is pinned by the component, but the body <td> is rendered by you in the item slot — so the slot exposes a sticky(key) helper that returns the binding to spread onto the matching <td>:

<template>
  <NTable :columns="columns" :items="rows" sticky-actions>
    <template #item="{ item, sticky }">
      <td>{{ item.name }}</td>
      <td>{{ item.email }}</td>
      <td>{{ item.phone }}</td>
      <!-- Sticky cells: spread the binding so the body cell pins with its header -->
      <td v-bind="sticky('role')">{{ item.role }}</td>
      <td v-bind="sticky('status')">{{ item.status }}</td>
    </template>

    <template #actions>
      <NButton size="sm" variant="ghost" intent="primary">Edit</NButton>
    </template>
  </NTable>
</template>

<script setup lang="ts">
import type { NTableColumn } from 'nicklabs-ui'

const columns: NTableColumn[] = [
  { key: 'name', label: 'Name' },
  { key: 'email', label: 'Email' },
  { key: 'phone', label: 'Phone' },
  { key: 'role', label: 'Role', sticky: true },    // pinned right
  { key: 'status', label: 'Status', sticky: true }, // pinned right
]
</script>

Usage

<template>
  <NTable
    :columns="columns"
    :items="users"
    hoverable
    @sort="handleSort"
    @click="handleItemClick"
  >
    <template #actions="{ item }">
      <NButton size="sm" variant="ghost" intent="primary" @click="edit(item)">Edit</NButton>
      <NButton size="sm" variant="ghost" intent="error" @click="remove(item)">Delete</NButton>
    </template>
  </NTable>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { NTable, NButton } from 'nicklabs-ui'
import type { NTableColumn, NTableSortState } from 'nicklabs-ui'

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

const columns: NTableColumn[] = [
  { key: 'name', label: 'Name', sortable: true },
  { key: 'email', label: 'Email' },
  { key: 'role', label: 'Role' },
]

const users = ref<User[]>([
  { id: 1, name: 'Alice', email: '[email protected]', role: 'Admin' },
  { id: 2, name: 'Bob', email: '[email protected]', role: 'User' },
])

function handleSort(state: NTableSortState) {
  console.log('Sort by:', state.key, state.order)
}

function handleItemClick(item: User) {
  console.log('Clicked:', item)
}
</script>

NVirtualTable

A virtualized data table built on @tanstack/vue-virtual. Only the visible rows are rendered, so it stays smooth with very large datasets. Shares the same look, sorting behavior, loading, and empty state as NTable, but you render each row's cells yourself through the cell scoped slot.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | columns | NVirtualTableColumn[] | [] | Column definitions | | items | T[] | [] | Row data | | bordered | boolean | false | Show borders | | hoverable | boolean | true | Highlight rows on hover | | loading | boolean | false | Show loading state (overlay) | | emptyTitle | string | "目前沒有資料" | Title when no data | | emptyDescription | string | "可以點擊上方的按鈕來新增資料或重新整理" | Description when no data | | itemKey | keyof T | "id" | Unique key field | | rowHeight | number | 56 | Estimated row height in px (drives virtualization) | | height | string | "480px" | Height of the scrollable body | | overscan | number | 8 | Extra rows rendered above/below the viewport | | actionsWidth | string | "120px" | Fixed width of the actions column (only shown when the actions slot is used). Increase it when you render more buttons, otherwise they get clipped |

Events

| Event | Payload | Description | |-------|---------|-------------| | sort | NVirtualTableSortState | Column sort changed | | click | T | Row was clicked |

Slots

| Slot | Description | |------|-------------| | cell | Renders all cells for a row (scoped: { item, columns, index }). Output one plain <div> per column — the component styles each direct child of the row as a cell, so no class is needed | | actions | Custom action buttons per row (scoped: { item, index }); the actions column only appears when this slot is provided | | actions-header | Custom header text for the actions column (default: "操作") | | empty | Custom empty state content |

NVirtualTableColumn Interface

interface NVirtualTableColumn {
  key: string       // Data field key
  label: string     // Column header text
  sortable?: boolean
  width?: string    // CSS grid track size (e.g. "120px", "2fr"); defaults to "1fr"
}

Usage

<template>
  <NVirtualTable
    :columns="columns"
    :items="users"
    :row-height="56"
    height="600px"
    hoverable
    @sort="handleSort"
    @click="handleItemClick"
  >
    <!-- Render every cell for the row yourself — one <div> per column -->
    <template #cell="{ item, columns }">
      <div v-for="column in columns" :key="column.key">
        {{ item[column.key] }}
      </div>
    </template>

    <template #actions="{ item }">
      <NButton size="sm" variant="ghost" intent="primary" @click="edit(item)">Edit</NButton>
      <NButton size="sm" variant="ghost" intent="error" @click="remove(item)">Delete</NButton>
    </template>
  </NVirtualTable>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { NVirtualTable, NButton } from 'nicklabs-ui'
import type { NVirtualTableColumn, NVirtualTableSortState } from 'nicklabs-ui'

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

const columns: NVirtualTableColumn[] = [
  { key: 'name', label: 'Name', sortable: true, width: '200px' },
  { key: 'email', label: 'Email' },
  { key: 'role', label: 'Role', width: '120px' },
]

// Virtualization shines with large datasets
const users = ref<User[]>(
  Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    name: `User ${i}`,
    email: `user${i}@example.com`,
    role: i % 2 ? 'User' : 'Admin',
  })),
)

function handleSort(state: NVirtualTableSortState) {
  console.log('Sort by:', state.key, state.order)
}

function handleItemClick(item: User) {
  console.log('Clicked:', item)
}
</script>

Sorting is emit-only. Like NTable, clicking a sortable header emits sort with the next { key, order } state — the component does not reorder items itself. Sort the data in the parent (or server-side) and feed it back via items.

Responsive width. Flexible (1fr / unset) columns won't compress below a readable minimum (120px). When the columns' combined minimum width exceeds the container — e.g. on phones — the whole table scrolls horizontally (header and body in sync) instead of squishing the columns. Fixed-width columns keep their exact width.


NList

A full-featured data management component combining table, pagination, filtering, and CRUD operations.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | title | string | — | Section title displayed in hero header | | description | string | — | Section description displayed in hero header | | items | T[] | [] | Data items (T must extend { id: string }) | | columns | NTableColumn[] | [] | Column definitions | | itemKey | keyof T | "id" | Unique key field | | totalItems | number | 0 | Total item count for pagination calculation | | currentPage | number | 1 | Current page number (controlled pagination) | | pageSize | number | 10 | Items per page | | maxPageButtons | number | 7 | Visible page buttons | | creatable | boolean | false | Show create button | | createLabel | string | "新增" | Create button label | | updatable | boolean | false | Show row edit button | | updateLabel | string | "編輯" | Edit button label | | deletable | boolean | false | Show row delete button | | deleteLabel | string | "刪除" | Delete button label | | batchDeletable | boolean | false | Enable batch delete with checkboxes | | batchDeleteLabel | string | "批量刪除" | Batch delete button label | | filterable | boolean | false | Show filter button | | filterLabel | string | "篩選" | Filter button label | | refreshable | boolean | false | Show refresh button | | refreshLabel | string | "重新整理" | Refresh button label | | emptyTitle | string | "目前沒有資料" | Empty state title | | emptyDescription | string | "可以點擊上方的按鈕來新增資料或重新整理" | Empty state description | | emptyIcon | string | — | Custom SVG string for empty state | | stickyActions | boolean | false | Pin the actions column to the right edge. Also pinned automatically when any column has sticky: true | | stickyBatch | boolean | false | Pin the leftmost batch (select-all) column to the left edge (requires batchDeletable) |

Events

| Event | Payload | Description | |-------|---------|-------------| | update | T | Row edit clicked | | delete | T | Row delete clicked | | pageChange | number | Page changed | | create | — | Create clicked | | batchDelete | string[], clearSelected: () => void | Batch delete with selected IDs; call clearSelected() after deletion to reset checkboxes | | refresh | — | Refresh clicked | | filter | — | Filter clicked | | click | T | Row clicked | | sort | NTableSortState | Sort changed |

Slots

| Slot | Description | |------|-------------| | toolbar | Extra toolbar content (alongside create/refresh buttons) | | item | Custom cell renderer (scoped: { item, index, sticky }). For a sticky column, spread v-bind="sticky(columnKey)" onto its <td> | | actions | Custom action buttons per row (scoped: { item, index }) | | actions-header | Custom header text for the actions column |

Usage

<template>
  <NList
    title="User Management"
    :items="users"
    :columns="columns"
    :page-size="20"
    filterable
    updatable
    deletable
    creatable
    refreshable
    @update="handleEdit"
    @delete="handleDelete"
    @create="handleCreate"
    @refresh="loadUsers"
    @filter="openFilter"
  />
</template>

<script setup lang="ts">
import { NList } from 'nicklabs-ui'

// T must extend { id: string }
interface User {
  id: string
  name: string
  email: string
}
</script>

Sticky columns. NList renders through NTable, so the same sticky behavior applies. Mark trailing columns with sticky: true to pin them right (the actions column follows automatically), and set stickyBatch to pin the select-all checkbox column left. The item slot exposes a sticky(key) helper — spread v-bind="sticky(columnKey)" onto the matching <td>:

<NList :items="users" :columns="columns" updatable deletable batch-deletable sticky-batch>
  <template #item="{ item, sticky }">
    <td>{{ item.name }}</td>
    <td>{{ item.email }}</td>
    <td v-bind="sticky('role')">{{ item.role }}</td>
  </template>
</NList>
<!-- columns: [..., { key: 'role', label: 'Role', sticky: true }] -->

NTag

Semantic tag/badge component.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | intent | "none" \| "primary" \| "success" \| "warning" \| "error" \| "info" | "none" | Color intent | | variant | "solid" \| "light" \| "outline" | "light" | Visual style | | size | "sm" \| "md" \| "lg" | "md" | Tag size | | closable | boolean | false | Show close button | | round | boolean | false | Fully rounded (pill shape) |

Events

| Event | Description | |-------|-------------| | close | Close button clicked |

Usage

<template>
  <NTag intent="success">Active</NTag>
  <NTag intent="warning" variant="outline">Pending</NTag>
  <NTag intent="error" variant="solid" round>Blocked</NTag>
  <NTag intent="info" closable @close="removeTag">Vue 3</NTag>
</template>

<script setup>
import { NTag } from 'nicklabs-ui'
</script>

NEmpty

Empty state placeholder component.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | title | string | — | Main message | | description | string | — | Sub-message | | size | "sm" \| "md" \| "lg" | "md" | Component size | | icon | string | — | Custom SVG string |

Usage

<template>
  <NEmpty
    title="No results found"
    description="Try adjusting your search filters"
    size="lg"
  />
</template>

<script setup>
import { NEmpty } from 'nicklabs-ui'
</script>

NCode

Syntax-highlighted code display with copy-to-clipboard.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | code | string | — | Code content to display | | language | string | — | Language for highlighting | | showLineNumbers | boolean | false | Show line numbers |

Usage

<template>
  <NCode
    :code="snippet"
    language="typescript"
    showLineNumbers
  />
</template>

<script setup>
import { NCode } from 'nicklabs-ui'

const snippet = `const greeting = (name: string) => {
  return \`Hello, \${name}!\`
}`
</script>

NImage

An image display component that behaves like a native <img> by default, with sizing, object-fit, and corner-radius props. Enable preview to make the thumbnail clickable — it opens a teleported, blurred-backdrop lightbox showing the full image, closable via the × button, the ESC key, or an overlay click.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | src | string | — | Image source (required) | | alt | string | "" | Alternative text | | width | string \| number | — | Thumbnail width (number is treated as px) | | height | string \| number | — | Thumbnail height (number is treated as px) | | fit | "fill" \| "contain" \| "cover" \| "none" \| "scale-down" | "cover" | Maps to CSS object-fit | | radiusSize | "none" \| "sm" \| "md" \| "lg" \| "xl" | "md" | Corner radius | | lazy | boolean | false | Use loading="lazy" for deferred loading | | preview | boolean | false | Click the image to open a full-size lightbox | | previewSrc | string | src | High-resolution source for the lightbox; falls back to src | | zIndex | number | 1000 | Lightbox overlay z-index | | closeOnClickOverlay | boolean | true | Close the lightbox when the backdrop is clicked |

Events

| Event | Payload | Description | |-------|---------|-------------| | load | Event | Image finished loading | | error | Event | Image failed to load | | preview-open | — | Lightbox opened | | preview-close | — | Lightbox closed |

Usage

<template>
  <!-- Basic: behaves like <img> -->
  <NImage src="/photo.jpg" :width="200" :height="140" alt="Scenery" />

  <!-- Lightbox: click to view the full image -->
  <NImage src="/photo.jpg" :width="200" :height="140" preview />

  <!-- Separate thumbnail and full-resolution sources -->
  <NImage
    src="/photo-thumb.jpg"
    preview-src="/photo-full.jpg"
    :width="200"
    :height="140"
    preview
  />

  <!-- object-fit and corner radius -->
  <NImage src="/photo.jpg" :width="160" :height="160" fit="contain" radius-size="xl" />
</template>

<script setup lang="ts">
import { NImage } from 'nicklabs-ui'
</script>

Distinct from NImageSelect: NImage displays an image (with an optional lightbox), while NImageSelect is an uploader for picking image files.


Modals & Overlays


NModal

A teleported modal dialog.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | show | boolean | false | Visibility (v-model:show) | | title | string | — | Modal title | | width | string | "500px" | Modal width | | closeOnClickOverlay | boolean | true | Close when backdrop clicked | | showClose | boolean | true | Show close button | | zIndex | number | — | Custom z-index |

Events

| Event | Description | |-------|-------------| | update:show | Visibility changed | | close | Modal closed | | open | Modal opened |

Slots

| Slot | Description | |------|-------------| | default | Modal body content | | footer | Footer actions area |

Usage

<template>
  <NButton @click="open">Open Modal</NButton>

  <NModal v-model:show="isOpen" title="Confirm Action" width="400px">
    <p>Are you sure you want to proceed?</p>

    <template #footer>
      <NButton variant="ghost" @click="isOpen = false">Cancel</NButton>
      <NButton intent="primary" @click="confirm">Confirm</NButton>
    </template>
  </NModal>
</template>

<script setup>
import { ref } from 'vue'
import { NModal, NButton } from 'nicklabs-ui'

const isOpen = ref(false)

function open() { isOpen.value = true }
function confirm() {
  isOpen.value = false
}
</script>

NDrawer

A teleported side drawer that slides in from the right or left edge of the viewport.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | show | boolean | false | Visibility (v-model:show) | | title | string | — | Drawer title | | width | string | "380px" | Drawer width | | placement | "right" \| "left" | "right" | Which side to slide in from | | closeOnClickOverlay | boolean | true | Close when backdrop clicked | | showClose | boolean | true | Show close button | | zIndex | number | 1000 | Custom z-index |

Events

| Event | Description | |-------|-------------| | update:show | Visibility changed | | close | Drawer closed | | open | Drawer opened |

Slots

| Slot | Description | |------|-------------| | default | Drawer body content | | title | Override the title area | | footer | Footer actions area (renders footer only when provided) |

Usage

<template>
  <NButton @click="isOpen = true">Open Drawer</NButton>

  <NDrawer v-model:show="isOpen" title="Edit Item">
    <NInput v-model="name" title="Name" placeholder="Enter name" />

    <template #footer>
      <NButton variant="outline" @click="isOpen = false">Cancel</NButton>
      <NButton variant="solid" intent="primary" @click="save">Save</NButton>
    </template>
  </NDrawer>
</template>

<script setup>
import { ref } from 'vue'
import { NDrawer, NButton, NInput } from 'nicklabs-ui'

const isOpen = ref(false)
const name = ref('')

function save() {
  isOpen.value = false
}
</script>

NAlert

Programmatic alert and confirm dialogs via the useAlert composable.

NAlert must be mounted once at the app root level.

Setup

<!-- App.vue -->
<template>
  <RouterView />
  <NAlert />
</template>

<script setup>
import { NAlert } from 'nicklabs-ui'
</script>

Usage via useAlert

See useAlert composable below.


NToast

Notification toasts via the useToast composable.

NToast must be mounted once at the app root level.

When only a description is passed (no title), the description is rendered with title styling (bold, larger font) for better visual clarity.

Setup

<!-- App.vue -->
<template>
  <RouterView />
  <NToast />
</template>

<script setup>
import { NToast } from 'nicklabs-ui'
</script>

Usage via useToast

See useToast composable below.


NHint

Inline notice banner for contextual hints, warnings, or reminders within a page section. Unlike the popup-style NAlert, NHint renders inline and shows an intent-matched icon automatically.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | intent | "none" \| "primary" \| "success" \| "warning" \| "error" \| "info" | "warning" | Color intent (controls background, text, border, and icon) |

Slots

| Slot | Description | |------|-------------| | default | Hint message content |

Usage

<template>
  <NHint>當 CSV 內的會員不存在於該群組中,將不會提示錯誤將直接略過</NHint>
  <NHint intent="success">資料已成功匯入</NHint>
  <NHint intent="error">匯入失敗,請檢查檔案格式</NHint>
  <NHint intent="info">支援的檔案格式為 .csv</NHint>
</template>

<script setup>
import { NHint } from 'nicklabs-ui'
</script>

NTooltip

Hover/focus tooltip with directional positioning.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | content | string | — | Tooltip text | | position | "top" \| "right" \| "bottom" \| "left" | "top" | Tooltip position | | disabled | boolean | false | Disable tooltip |

Usage

<template>
  <NTooltip content="Delete this item" position="top">
    <NButton variant="ghost" intent="error" square>
      <svg>...</svg>
    </NButton>
  </NTooltip>
</template>

<script setup>
import { NTooltip, NButton } from 'nicklabs-ui'
</script>

NLoading

Loading indicator supporting inline, overlay, and fullscreen modes.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | loading | boolean | true | Show loading state | | title | string | — | Loading message | | variant | "spinner" \| "dots" | "spinner" | Animation style | | overlay | boolean | false | Overlay mode — covers slot content with a backdrop | | fullscreen | boolean | false | Fullscreen mode — covers the entire viewport and blocks interaction |

Usage

<template>
  <!-- Inline: shows spinner while loading, slot content when done -->
  <NLoading :loading="isFetching" title="Loading data...">
    <p>Content loaded!</p>
  </NLoading>

  <!-- Overlay: spinner overlaid on top of slot content -->
  <NLoading :loading="isSubmitting" overlay title="Saving..." variant="dots">
    <div>Form content here</div>
  </NLoading>

  <!-- Fullscreen: blocks entire viewport, no slot needed -->
  <NLoading :loading="isProcessing" fullscreen title="處理中..." />
</template>

<script setup>
import { ref } from 'vue'
import { NLoading } from 'nicklabs-ui'

const isFetching = ref(true)
const isSubmitting = ref(false)
const isProcessing = ref(false)
</script>

Layout


NLayout

Main application shell integrating sidebar and main content area.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | menus | Menu[] | — | Sidebar menu items | | isShowSidebar | boolean | true | Show/hide sidebar | | copyright | string | — | Footer copyright text | | currentPath | string | "" | Active route path |

Events

| Event | Payload | Description | |-------|---------|-------------| | logout | — | Logout triggered | | navigate | MenuChild | Menu item clicked |

Slots

| Slot | Description | |------|-------------| | default | Main page content |

Usage

<template>
  <NLayout
    :menus="menus"
    :current-path="$route.path"
    copyright="© 2025 MyApp"
    @logout="handleLogout"
    @navigate="handleNavigate"
  >
    <RouterView />
  </NLayout>
</template>

<script setup>
import { NLayout } from 'nicklabs-ui'

const menus = [
  {
    icon: '<svg>...</svg>',
    title: 'Dashboard',
    children: [
      { icon: '<svg>...</svg>', title: 'Overview', route: '/dashboard' },
    ],
  },
]
</script>

NNavigation

Top navigation bar with sidebar toggle, fullscreen, and user controls.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | isShowSidebar | boolean | true | Show sidebar toggle button | | isShowFullscreen | boolean | true | Show fullscreen button | | isShowUser | boolean | true | Show user pill | | isShowLogoutButton | boolean | true | Show logout button |

Events

| Event | Description | |-------|-------------| | toggleSidebar | Sidebar toggle clicked | | logout | Logout clicked |


NSidebar

Collapsible side navigation menu with hover-expand behavior.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | isOpen | boolean | false | Sidebar open state | | menus | Menu[] | — | Menu items | | currentPath | string | "" | Active route path | | userName | string | — | Display name shown in user area | | userAvatarUrl | string | — | Avatar image URL |

Events

| Event | Payload | Description | |-------|---------|-------------| | update:isOpen | boolean | Open state changed | | logout | — | Logout triggered | | navigate | MenuChild | Menu item clicked |


NCard

Content card with glassmorphism styling.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | size | "none" \| "sm" \| "md" \| "lg" | "md" | Padding size | | radius | "none" \| "sm" \| "md" \| "lg" \| "xl" | "md" | Border radius |

Usage

<template>
  <NCard size="lg" radius="xl">
    <h2>Card Title</h2>
    <p>Card content goes here.</p>
  </NCard>
</template>

<script setup>
import { NCard } from 'nicklabs-ui'
</script>

NForm

Form wrapper with optional tab navigation and hero section header.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | model | object | {} | Form data object | | disabled | boolean | false | Disable all inputs | | icon | string | — | Header icon (SVG string) | | title | string | — | Form header title | | description | string | — | Form header description | | tabs | string[] | [] | Tab labels; active tab persisted to localStorage | | modelValue | number | — | Controlled tab index (v-model); when provided, overrides internal state |

Events

| Event | Payload | Description | |-------|---------|-------------| | submit | Record<string, any> | Form submitted with model data | | reset | — | Form reset | | update:modelValue | number | Tab changed — emitted on every tab click for v-model binding |

Slots

| Slot | Description | |------|-------------| | heroSection | Replace the entire hero header (rarely needed) | | toolbar | Toolbar area in the hero header (buttons, etc.) | | description | Custom description content in hero header | | tab0 | Content for first tab (or only content when no tabs) | | tab1, tab2, ... | Content for subsequent tabs | | footer | Form footer (submit/cancel buttons) |

Usage

<template>
  <NForm
    :model="formData"
    title="User Profile"
    description="Manage your account details"
    :tabs="['Basic Info', 'Security', 'Preferences']"
    v-model="activeTab"
    @submit="handleSubmit"
  >
    <template #toolbar>
      <NButton variant="ghost" @click="cancel">Cancel</NButton>
    </template>

    <template #tab0>
      <NInput v-model="formData.name" title="Name" />
      <NInput v-model="formData.email" title="Email" />
    </template>

    <template #tab1>
      <NInput v-model="formData.password" type="password" title="Password" />
    </template>

    <template #footer>
      <NButton type="submit" intent="primary">Save</NButton>
    </template>
  </NForm>
</template>

<script setup>
import { ref, reactive } from 'vue'
import { NForm, NInput, NButton } from 'nicklabs-ui'

const formData = reactive({ name: '', email: '', password: '' })
const activeTab = ref(0)  // control tab externally

function handleSubmit(model) {
  console.log('Submitted:', model)
}
</script>

NLoginLayout

Full-screen login page wrapper with animated card.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | backgroundImage | string | — | Background image URL (falls back to --bg-gradient) | | logo | string | — | Logo image URL or SVG string | | title | string | — | Application name | | description | string | — | Subtitle/tagline |

Usage

<template>
  <NLoginLayout
    title="MyApp"
    description="Sign in to your account"
    background-image="/images/bg.jpg"
    logo="/images/logo.png"
  >
    <NInput v-model="email" type="email" placeholder="Email" />
    <NInput v-model="password" type="password" placeholder="Password" />
    <NButton type="submit" intent="primary" style="width: 100%">Sign In</NButton>
  </NLoginLayout>
</template>

<script setup>
import { ref } from 'vue'
import { NLoginLayout, NInput, NButton } from 'nicklabs-ui'

const email = ref('')
const password = ref('')
</script>

NBreadcrumb

Breadcrumb navigation driven by useBreadcrumb. No props — reads from the composable's state automatically.

Configure breadcrumbs via useBreadcrumb.

Usage

<template>
  <NBreadcrumb />
</template>

<script setup>
import { NBreadcrumb } from 'nicklabs-ui'
</script>

NPaginate

Pagination control with smart ellipsis.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | totalItems | number | — | Total number of items | | pageSize | number | — | Items per page | | maxPageButtons | number | 7 | Max visible page buttons (min: 5) |

Events

| Event | Payload | Description | |-------|---------|-------------| | onPageChange | number | New page number |

Usage

<template>
  <NPaginate
    :total-items="totalCount"
    :page-size="20"
    :max-page-buttons="7"
    @on-page-change="loadPage"
  />
</template>

<script setup>
import { ref } from 'vue'
import { NPaginate } from 'nicklabs-ui'

const totalCount = ref(350)

function loadPage(page: number) {
  // fetch page data
}
</script>

NHeroSection

Page header with icon, title, description, breadcrumb, and toolbar slot.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | title | string | — | Section title | | description | string | — | Section description | | icon | string | — | Icon (SVG string) |

Slots

| Slot | Description | |------|-------------| | toolbar | Toolbar content (buttons, filters, etc.) | | description | Custom description content |

Usage

<template>
  <NHeroSection title="User Management" description="Manage system users" :icon="userIcon">
    <template #toolbar>
      <NButton intent="primary" @click="create">New User</NButton>
    </template>
  </NHeroSection>
</template>

<script setup>
import { NHeroSection, NButton } from 'nicklabs-ui'

const userIcon = '<svg>...</svg>'
</script>

NSideFilter

A slide-out side drawer for filter interfaces.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | open | boolean | — | Open state (v-model:open) | | title | string | — | Drawer title |

Events

| Event | Description | |-------|-------------| | update:open | Open state changed | | close | Drawer closed |

Usage

<template>
  <NButton @click="filterOpen = true">Filters</NButton>

  <NSideFilter v-model:open="filterOpen" title="Filter Results">
    <NSelect v-model="status" :options="statusOptions" title="Status" />
    <NInput v-model="search" placeholder="Search name..." title="Name" />
  </NSideFilter>
</template>

<script setup>
import { ref } from 'vue'
import { NSideFilter, NButton, NSelect, NInput } from 'nicklabs-ui'

const filterOpen = ref(false)
const status = ref(null)
const search = ref('')
</script>

Composables


useToast

Display notification toasts programmatically.

import { useToast } from 'nicklabs-ui'

const { toasts, toast, removeToast } = useToast()

Methods

| Method | Signature | Description | |--------|-----------|-------------| | toast | (message: string, options?: ToastOptions) => void | Show a toast | | toast.success | (message: string, options?) => void | Success toast | | toast.danger | (message: string, options?) => void | Error toast | | toast.warning | (message: string, options?) => void | Warning toast | | toast.info | (message: string, options?) => void | Info toast | | removeToast | (id: string) => void | Remove a specific toast |

ToastOptions

interface ToastOptions {
  title?: string    // Optional heading; when omitted, description uses title styling
  duration?: number // Auto-dismiss ms (default: 4000)
}

Usage

<script setup>
import { useToast } from 'nicklabs-ui'

const { toast } = useToast()

function save() {
  toast.success('Saved successfully!', { title: 'Done' })
}

function handleError() {
  toast.danger('Something went wrong.', { dur