aetherx-dt-ui

Lightweight, customizable Vue components for DT projects
16,162
Install
npm install aetherx-dt-ui
Latest Version:0.1.96
Vue:3
License:MIT
Last Updated:Aug 16, 2026
Links: GitHub  ·  npm
Maintainer: ashomurodov

dt-ui

Vue 3 UI components for DT ecosystem modules.

dt-ui is a copy-based component registry, similar in spirit to shadcn. You install the package for the CLI and registry, then copy the components you need into your project. After that, the component files live in your app and can be customized by your team.

Vue 3 + TypeScript | Copy-based registry | Design tokens | Dark mode | Accessible primitives

Why Copy-Based?

Company microservices need the same UI language, but every product still needs room for local business rules. dt-ui gives you:

  • Shared defaults for spacing, colors, layout, tables, forms, and modals.
  • Local component ownership after install.
  • TypeScript-friendly APIs.
  • Components that are easy to inspect, change, and commit in each service.
  • A predictable update flow through the dt-ui CLI.

Quick Start

Install the package:

npm install aetherx-dt-ui

Initialize the project:

npx dt-ui init

Add the components you need:

npx dt-ui add button input select layout

Import the generated styles once in src/main.ts:

import '@/styles/base.css'
import '@/styles/table-cells.css'

Use copied components from your local app:

<script setup lang="ts">
import { DtButton } from '@/components/ui/button'
</script>

<template>
  <DtButton>Create</DtButton>
</template>

CLI Commands

npx dt-ui init
npx dt-ui list
npx dt-ui add button
npx dt-ui add button input select
npx dt-ui add layout
npx dt-ui update

init creates the local config, styles, and helper files.

add copies component source into src/components/ui.

update refreshes registry-managed files. It should ask before overwriting component files that may contain local edits.

Project Setup

Make sure your Vue app has the @ alias configured.

tsconfig.app.json:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

For SVG icon imports, create or update src/env.d.ts:

declare module '*.svg' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent
  export default component
}

Components

npx dt-ui list
Component Install Description
DtButton add button Button variants, sizes, loading state, icon slots
DtInput add input Input with label, hint, error, success, sizes
DtSelect add select Select with trigger, content, items, keyboard support
DtToggle add toggle Switch toggle in multiple sizes
DtCheckbox add checkbox Checkbox with label and size variants
DtRadio add radio Radio button and radio group patterns
DtBadge add badge Color badges with optional dot
DtStatusBadge add status-badge Semantic entity status pills
DtCard add card Card, header, content, footer
DtDialog add dialog Accessible modal primitives
DtDataTable add data-table Slot-based data table with loading and empty states
DtPagination add pagination Pagination with page window and ellipsis
DtTabSwitcher add tab-switcher Tab switcher with active indicator and badges
DtSearchToolbar add search-toolbar Search input with action button
DtLayout add layout App shell, header, sidebar, modules modal, profile modal, page view, divider

Import Pattern

Each component folder has its own index.ts.

import { DtButton } from '@/components/ui/button'
import { DtInput } from '@/components/ui/input'
import { DtSelect, DtSelectTrigger, DtSelectContent, DtSelectItem } from '@/components/ui/select'

Layout exports components and useful types:

import {
  DtLayout,
  DtLayoutHeader,
  DtLayoutSidebar,
  DtModulesModal,
  DtPageView,
  DtProfileModal,
  type DtModuleClickPayload,
  type DtModuleItem,
  type DtNavItem,
  type DtNavSection,
  type DtSidebarItemClickPayload,
} from '@/components/ui/layout'

Basic Components

Button

<script setup lang="ts">
import { ref } from 'vue'
import { Plus } from 'lucide-vue-next'
import { DtButton } from '@/components/ui/button'

const saving = ref(false)
</script>

<template>
  <DtButton size="md" variant="primary">
    <template #icon-left>
      <Plus :size="18" />
    </template>
    Create
  </DtButton>

  <DtButton variant="outline" :loading="saving">
    Save
  </DtButton>
</template>

Input

<script setup lang="ts">
import { ref } from 'vue'
import { DtInput } from '@/components/ui/input'

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

<template>
  <DtInput
    v-model="search"
    placeholder="Search documents"
    size="md"
  >
    <template #label>Search</template>
  </DtInput>
</template>

Select

<script setup lang="ts">
import { ref } from 'vue'
import {
  DtSelect,
  DtSelectTrigger,
  DtSelectContent,
  DtSelectItem,
} from '@/components/ui/select'

const status = ref<string | number | null>(null)
</script>

<template>
  <DtSelect v-model="status" placeholder="Choose status">
    <DtSelectTrigger />
    <DtSelectContent>
      <DtSelectItem value="published">Published</DtSelectItem>
      <DtSelectItem value="inactive">Inactive</DtSelectItem>
      <DtSelectItem value="moderation">In moderation</DtSelectItem>
    </DtSelectContent>
  </DtSelect>
</template>

Dialog

<script setup lang="ts">
import { ref } from 'vue'
import {
  DtDialog,
  DtDialogTrigger,
  DtDialogContent,
  DtDialogHeader,
  DtDialogFooter,
} from '@/components/ui/dialog'
import { DtButton } from '@/components/ui/button'

const open = ref(false)
</script>

<template>
  <DtDialog v-model="open">
    <DtDialogTrigger>
      <DtButton>Create</DtButton>
    </DtDialogTrigger>

    <DtDialogContent>
      <DtDialogHeader>
        <h2>Create item</h2>
        <p>Fill the form and save.</p>
      </DtDialogHeader>

      <!-- form fields -->

      <DtDialogFooter>
        <DtButton variant="outline" @click="open = false">Cancel</DtButton>
        <DtButton>Save</DtButton>
      </DtDialogFooter>
    </DtDialogContent>
  </DtDialog>
</template>

App Shell

Install the full layout system:

npx dt-ui add layout

dt-ui does not require CDN assets. The header modules button can open the built-in DtModulesModal when you pass a typed modules array.

If your project does not use a modules modal, hide the header modules button:

<DtLayoutHeader :show-modules-button="false" />

If your product has its own modules/app switcher, listen to @modules-click or pass custom controls through the header actions slot.

Example app shell:

<script setup lang="ts">
import { ref } from 'vue'
import { Home, FileText } from 'lucide-vue-next'
import {
  DtLayout,
  DtLayoutHeader,
  DtLayoutSidebar,
  DtProfileModal,
  type DtModuleItem,
  type DtNavItem,
  type DtUser,
} from '@/components/ui/layout'

const showProfile = ref(false)

const user: DtUser = {
  first_name: 'John',
  last_name: 'Doe',
  phone_numbers: [{ number: '+998 90 000 00 00' }],
}

const navItems: DtNavItem[] = [
  { key: 'dashboard', to: '/', icon: Home, label: 'Dashboard' },
  { key: 'documents', to: '/documents', icon: FileText, label: 'Documents', badge: 12 },
]

const modules: DtModuleItem[] = [
  { key: 'crm', label: 'CRM', icon: Home, href: '/crm' },
  { key: 'documents', label: 'Documents', icon: FileText, href: '/documents' },
  {
    key: 'settings',
    label: 'Settings',
    icon: Home,
    onClick: () => {
      // Open your own settings modal or route from the app.
    },
  },
]

function logout() {
  // Clear auth and redirect from your app.
}
</script>

<template>
  <DtLayout>
    <template #header>
      <DtLayoutHeader
        badge="CRM"
        active-module="crm"
        :modules="modules"
        :profile-name="user.first_name + ' ' + user.last_name"
        @toggle-profile="showProfile = !showProfile"
      >
        <template #logo>
          <RouterLink to="/">Logo</RouterLink>
        </template>

        <template #profile-dropdown>
          <DtProfileModal
            v-model="showProfile"
            :user="user"
            @logout="logout"
          />
        </template>
      </DtLayoutHeader>
    </template>

    <template #sidebar>
      <DtLayoutSidebar :items="navItems" />
    </template>

    <RouterView />
  </DtLayout>
</template>

Header Modules Modal

DtLayoutHeader opens DtModulesModal automatically when modules has visible items. Consumers own the module list, URLs, icons/logos, and any auth checks.

interface DtModuleItem {
  key: string
  label: string
  href?: string
  logo?: string
  icon?: any
  badge?: string | number
  description?: string
  target?: '_self' | '_blank' | '_parent' | '_top'
  rel?: string
  active?: boolean
  disabled?: boolean
  hidden?: boolean
  span?: 'default' | 'full'
  onClick?: (payload: DtModuleClickPayload) => void | Promise<void>
}
  • Pass href for normal navigation.
  • Pass onClick without href for custom app-owned behavior.
  • Use logo for image URLs or icon for Vue icon components.
  • Use activeModule on the header or active: true per item for active state.
  • Use @module-click for analytics, auth guards, or custom routing. Call payload.event.preventDefault() if you need to stop a link.

Sidebar API

The sidebar supports simple route links, action rows, badges, disabled/hidden rows, sections, and expandable parent items.

If an item has children, it renders as an expandable parent button. Parent route navigation is intentionally not mixed into that same row; put the route on a child item for v1.

Use exported types for editor hints:

import { ref } from 'vue'
import type {
  DtNavItem,
  DtNavSection,
  DtSidebarItemClickPayload,
} from '@/components/ui/layout'

DtNavItem fields:

Field Type Notes
key string Recommended for dynamic items and action rows
to string | Record<string, any> Passed to RouterLink for route navigation
label string Visible item label
icon any Vue component, for example from lucide-vue-next
badge string | number Count/status badge on the right side
onClick (payload) => void | Promise<void> Use for modal openers and custom actions
children DtNavItem[] Makes the item an expandable parent
active boolean Force active state when route matching is not enough
disabled boolean Prevents navigation, toggle, and callbacks
hidden boolean Skips rendering
defaultOpen boolean Opens this parent by default

Badges render in the desktop sidebar. Mobile bottom navigation intentionally omits badges to keep the compact nav clean.

Typed nav items:

import { FileText, Grid2x2, Plus } from 'lucide-vue-next'

const openKeys = ref<string[]>(['deals'])

function openCreateDealModal() {
  // Open your product modal here.
}

const navItems: DtNavItem[] = [
  {
    key: 'deals',
    label: 'Deals',
    icon: Grid2x2,
    badge: 3,
    defaultOpen: true,
    children: [
      {
        key: 'deal-test',
        label: 'test',
        badge: 3,
        to: { path: '/documents', query: { deal: 'test' } },
      },
      {
        key: 'add-deal',
        label: 'Add',
        icon: Plus,
        onClick: () => openCreateDealModal(),
      },
    ],
  },
  {
    key: 'open-profile',
    label: 'Open profile',
    icon: FileText,
    onClick: ({ item }: DtSidebarItemClickPayload) => {
      console.log(item.label)
    },
  },
]

Sections:

const sections: DtNavSection[] = [
  {
    title: 'Widgets',
    items: [
      { key: 'components', label: 'Components', to: '/components' },
      { key: 'disabled', label: 'Disabled action', disabled: true, badge: 'Soon' },
    ],
  },
]

Usage:

<DtLayoutSidebar
  v-model:open-keys="openKeys"
  :items="navItems"
  :sections="sections"
  :mobile-items="4"
  @item-click="handleSidebarClick"
/>

Click handling:

function handleSidebarClick(payload: DtSidebarItemClickPayload) {
  console.log(payload.item.label, payload.level, payload.parentKey)
}

Reactive updates are normal Vue updates. If you push a child into children or replace the items array from modal submit logic, the sidebar renders it immediately. Put the route in the new item's to field when the new sidebar row should navigate.

Sidebar Slots

Use slots when a product needs custom content above or below the generated nav.

<DtLayoutSidebar :items="navItems" :sections="sections">
  <template #top>
    <WorkspaceSwitcher
      :workspace="workspace"
      @change="openWorkspaceModal"
    />
  </template>

  <template #bottom>
    <SidebarFooter :version="appVersion" />
  </template>
</DtLayoutSidebar>

Existing compatibility slots still work:

<template #desktop-extra>
  <!-- legacy desktop-only extra content -->
</template>

<template #mobile-extra>
  <!-- mobile bottom-nav extra content -->
</template>

List Page Pattern

This is the common DT module page shape: page title, tabs, search/action toolbar, table, pagination.

<script setup lang="ts">
import { ref } from 'vue'
import { DtPageView, DtDivider } from '@/components/ui/layout'
import { DtTabSwitcher, type DtTab } from '@/components/ui/tab-switcher'
import { DtSearchToolbar } from '@/components/ui/search-toolbar'
import { DtDataTable, type DtColumn } from '@/components/ui/data-table'
import { DtPagination } from '@/components/ui/pagination'
import { DtStatusBadge, type StatusVariant } from '@/components/ui/status-badge'

const page = ref(1)
const search = ref('')
const activeTab = ref('all')
const loading = ref(false)
const totalCount = ref(2)

const tabs: DtTab[] = [
  { key: 'all', label: 'All documents' },
  { key: 'published', label: 'Published' },
  { key: 'inactive', label: 'Inactive' },
]

type DocumentItem = {
  no: number
  name: string
  status: StatusVariant
  statusLabel: string
}

const documents = ref<DocumentItem[]>([
  { no: 1, name: 'Contract', status: 'active', statusLabel: 'Active' },
  { no: 2, name: 'Invoice', status: 'moderation', statusLabel: 'In moderation' },
])

const columns: DtColumn[] = [
  { key: 'no', label: 'No', width: '64px' },
  { key: 'name', label: 'Name' },
  { key: 'status', label: 'Status' },
  { key: 'actions', label: 'Actions', align: 'right' },
]

function createDocument() {
  // Open your product modal here.
}

function fetchDocuments() {
  // Call your API here.
}

function editDocument(item: DocumentItem) {
  console.log(item.name)
}
</script>

<template>
  <DtPageView title="Documents">
    <DtTabSwitcher v-model="activeTab" :tabs="tabs" />

    <DtSearchToolbar
      v-model:search="search"
      add-label="Create"
      @add="createDocument"
      @search="fetchDocuments"
    />

    <DtDivider />

    <DtDataTable
      :columns="columns"
      :items="documents"
      :loading="loading"
      :page="page"
      :page-size="10"
      empty-text="No documents found"
    >
      <template #status="{ item }">
        <DtStatusBadge :variant="item.status">
          {{ item.statusLabel }}
        </DtStatusBadge>
      </template>

      <template #actions="{ item }">
        <button @click="editDocument(item)">Edit</button>
      </template>
    </DtDataTable>

    <DtPagination
      v-model:page="page"
      :total-count="totalCount"
      :page-size="10"
    />
  </DtPageView>
</template>

Design Tokens

All components use CSS custom properties from base.css.

Color scales:

--dt-brand-100 ... --dt-brand-900
--dt-gray-100 ... --dt-gray-900
--dt-success-100 ... --dt-success-900
--dt-error-100 ... --dt-error-900
--dt-warning-100 ... --dt-warning-900
--dt-blue-100 ... --dt-blue-900
--dt-yellow-100 ... --dt-yellow-900

Semantic tokens:

--dt-color-accent: var(--dt-brand-500);
--dt-color-text: var(--dt-gray-900);
--dt-color-border: var(--dt-gray-200);
--dt-color-success: var(--dt-success-600);
--dt-color-error: var(--dt-error-500);

Use tokens for product-specific overrides:

:root {
  --dt-color-accent: #0096b2;
  --dt-sidebar-width: 240px;
}

Dark Mode

Set data-theme="dark" on html:

document.documentElement.dataset.theme = 'dark'

Use data-theme="light" or remove the attribute to return to light mode, depending on your app theme strategy.

Updating Components

Install the latest package:

npm install aetherx-dt-ui@latest

Update generated files:

npx dt-ui update

The CLI should keep local ownership in mind:

  • Shared styles and helper files can be refreshed.
  • Component files may ask before overwrite.
  • If you customized a copied component, review the diff before accepting an overwrite.

To refresh a single component manually, remove it from .dtui.json and run add again:

npx dt-ui add layout

TypeScript Notes

Many component folders export useful types:

import type { DtNavItem, DtNavSection } from '@/components/ui/layout'
import type { DtColumn } from '@/components/ui/data-table'

Prefer typing configuration arrays:

const items: DtNavItem[] = []
const columns: DtColumn[] = []

This gives autocomplete for valid fields and catches invalid values early.

Tech Stack

  • Vue 3 Composition API
  • TypeScript
  • Pure CSS custom properties
  • Reka UI for behavior-heavy primitives such as Dialog and Select
  • Compatible with Vue Router and Vite
  • Works well with lucide-vue-next or SVG icon components

Troubleshooting

Styles look missing

Make sure these are imported in main.ts:

import '@/styles/base.css'
import '@/styles/table-cells.css'

TypeScript cannot resolve @/...

Check tsconfig.app.json and Vite alias setup.

Sidebar routes do not navigate

to is rendered through RouterLink, so your app must use Vue Router. For non-route rows, use onClick.

Icons do not render

Pass Vue components as icon, for example from lucide-vue-next, or configure SVG imports with vite-svg-loader.

License

MIT

Related Packages

@mudakit/ui

Vue components for Muda

870
@stonecrop/beam

UI component library for Stonecrop

22,847 13
@pisandelli/daredash

Provides a design token-powered UI component library and styling system for Nuxt...

6,023 3
integrasoft-shared-ui

Librería de componentes UI en Vue 3 + TypeScript para estandarizar estilos entre...

2,758
@maiadiary/abyss-design

Abyss Design System — komponenty UI dla Quasar / Vue 3

1,584 0

Version History

Version Released Vue License
0.1.96 ^3.3.0 MIT
0.1.95 ^3.3.0 MIT
0.1.94 ^3.3.0 MIT
0.1.93 ^3.3.0 MIT
0.1.92 ^3.3.0 MIT