npm package: @gemafajarramadhan/dynamic-ui
Description: Vue 3 Dynamic UI Component Library - Compatible with Vue, React, Angular, and any other framework via Web Components
Latest version: 1.2.71

# @gemafajarramadhan/dynamic-ui

Library **Vue 3** component yang bisa digunakan di berbagai framework (Vue, React, Angular, Svelte, dll.) untuk membuat **Dynamic Form** dan **DataTable** otomatis dari JSON schema, sekaligus menyediakan komponen UI yang kaya fitur.

---

## Daftar Isi

- [Fitur](#fitur)
- [Instalasi](#instalasi)
- [⚡ Konfigurasi API (WAJIB)](#-konfigurasi-api-wajib)
  - [Vue 3](#konfigurasi-api-di-vue-3)
  - [Nuxt 3](#konfigurasi-api-di-nuxt-3)
  - [React / Next.js](#konfigurasi-api-di-react--nextjs)
  - [Angular](#konfigurasi-api-di-angular)
  - [Vanilla JS (window global)](#konfigurasi-api-via-window-global)
- [Komponen Base (Vue Components)](#komponen-base-vue-components)
  - [Daftar Komponen](#daftar-komponen)
  - [Penggunaan di Vue 3 (Import Per Komponen)](#penggunaan-di-vue-3-import-per-komponen)
  - [Penggunaan di Vue 3 (Global Plugin)](#penggunaan-di-vue-3-global-plugin)
  - [Penggunaan di Nuxt 3](#penggunaan-di-nuxt-3-vue-components)
- [DCodeDataTable – Tabel Dinamis](#dcodedatatable--tabel-dinamis)
  - [Props Utama](#props-utama)
  - [Pola 1 — fetchHandler (MFE)](#pola-1--fetchhandler--disarankan-untuk-mfe)
  - [Pola 2 — Static Data](#pola-2--static-data-consumer-fetch-di-luar)
  - [Pola 3 — Realtime Socket](#pola-3--realtime-socket-consumer-kelola-socket)
  - [Format Response](#format-response-yang-didukung)
  - [Expose Methods](#expose-methods-via-ref)
- [DCodeAutoComplete – Komponen dengan API](#dcodeautocomplete--komponen-dengan-api)
- [Web Components (Framework Agnostic)](#web-components-framework-agnostic)
  - [API Referensi Web Components](#api-referensi-web-components)
  - [Logic Registry API](#logic-registry-api)
- [Penggunaan di Berbagai Framework](#penggunaan-di-berbagai-framework)
  - [Vanilla HTML / JavaScript](#1-vanilla-html--javascript)
  - [Vue 3 (Web Components)](#2-vue-3-web-components)
  - [React](#3-react)
  - [Next.js (App Router)](#4-nextjs-app-router)
  - [Angular](#5-angular)
  - [Nuxt 3 (Web Components)](#6-nuxt-3-web-components)
- [Custom Logic Registry](#custom-logic-registry)
- [TypeScript Support](#typescript-support)

---

## Fitur

- 🧩 **Web Components** – Dapat digunakan di framework **apapun** (React, Vue, Angular, Svelte, dll.) atau di HTML biasa.
- 📋 **AutoLayoutForm** – Generate form otomatis dari JSON schema dengan validasi dan konfigurasi fleksibel.
- 📊 **AutoLayoutDatatable** – Generate tabel dengan pagination, sorting, filtering, dan action button dari JSON.
- 🔌 **Logic Registry** – Daftarkan custom business logic dari aplikasi host tanpa perlu memodifikasi library.
- 🎨 **Style Bundled** – CSS sudah ter-bundle, cukup import satu file style.
- 🌐 **i18n Ready** – Mendukung internasionalisasi (vue-i18n).
- 🔐 **API Config** – Base URL dan auth token diambil dari **host project**, bukan dari env package ini.

---

## Instalasi

```bash
# npm
npm install @gemafajarramadhan/dynamic-ui

# yarn
yarn add @gemafajarramadhan/dynamic-ui

# pnpm
pnpm add @gemafajarramadhan/dynamic-ui
```

---

## ⚡ Konfigurasi API (WAJIB)

> **Penting:** Komponen seperti `DCodeAutoComplete` melakukan HTTP request ke API. Base URL dan auth token **diambil dari host project**, bukan dari `.env` package ini. Anda **wajib** mengkonfigurasi ini di aplikasi host sebelum menggunakan komponen.

Library menggunakan sistem konfigurasi terpusat via `setApiConfig()` (atau via Vue Plugin). Berikut urutan prioritas resolusi base URL:

| Prioritas | Sumber |
|-----------|--------|
| 1️⃣ Tertinggi | `apiBaseUrl` via plugin / `setApiConfig()` |
| 2️⃣ | `window.__MICRO_API_BASE_URL__` (global) |
| 3️⃣ | `import.meta.env.VITE_API_BASE_URL` (env host) |
| 4️⃣ Terendah | `''` (same-origin fallback) |

---

### Konfigurasi API di Vue 3

Cara paling direkomendasikan: lewatkan config saat `app.use()`, atau gunakan `setApiConfig` secara langsung. Hal ini **sangat penting** agar request API dari komponen (seperti `DCodeAutoComplete`) mem-bypass native `fetch` bawaan dan menggunakan instance HTTP (seperti Axios) milik project host Anda. Dengan begitu, request otomatis melewati interceptor (untuk inject token) dan mengarah ke base URL yang tepat saat di production.

```js
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import DynamicUI, { setApiConfig } from '@gemafajarramadhan/dynamic-ui'
import '@gemafajarramadhan/dynamic-ui/style.css'

// Contoh import instance Axios / useApi dari host project Anda
import axiosInstance from './plugins/axios' 
// import { useApi } from './composables/useApi' 

const app = createApp(App)

// OPSI 1: Konfigurasi memanggil fungsi (Bisa dipanggil di luar instalasi plugin)
setApiConfig({
  // WAJIB: Bypass fetch bawaan, gunakan axios dari project host
  apiClient: async (url, method, data) => {
    return axiosInstance({ url, method, data })
    /* ATAU MENGGUNAKAN useApi bawaan project host
    const { apiUrl } = useApi()
    return apiUrl(url, method || 'GET', data, { disableGlobalLoading: true })
    */
  },
  
  // Base URL API dari env HOST project
  apiBaseUrl: import.meta.env.VITE_API_BASE_URL,
  
  // (Opsional jika apiClient sudah memiliki interceptor token)
  getAuthToken: () => localStorage.getItem('access_token'),
})

// OPSI 2: Lewatkan pada saat app.use (Berlaku jika tidak menggunakan setApiConfig di atas)
app.use(DynamicUI, {
  apiClient: async (url, method, data) => axiosInstance({ url, method, data }),
  apiBaseUrl: import.meta.env.VITE_API_BASE_URL,
})

app.mount('#app')
```

> File `.env` di host project Anda:
> ```env
> VITE_API_BASE_URL=https://api.example.com
> ```

---

### Konfigurasi API di Nuxt 3

```ts
// plugins/dynamic-ui.ts
import DynamicUI from '@gemafajarramadhan/dynamic-ui'
import '@gemafajarramadhan/dynamic-ui/style.css'

export default defineNuxtPlugin((nuxtApp) => {
  const config = useRuntimeConfig()

  nuxtApp.vueApp.use(DynamicUI, {
    // Ambil dari runtimeConfig Nuxt
    apiBaseUrl: config.public.apiBaseUrl,

    getAuthToken: () => useCookie('access_token').value ?? null,

    onUnauthorized: () => {
      navigateTo('/login')
    },
  })
})
```

> File `nuxt.config.ts` di host project:
> ```ts
> export default defineNuxtConfig({
>   runtimeConfig: {
>     public: {
>       apiBaseUrl: process.env.VITE_API_BASE_URL, // set di .env
>     },
>   },
> })
> ```

---

### Konfigurasi API di React / Next.js

Gunakan fungsi `setApiConfig()` secara langsung (tidak perlu Vue plugin).

```ts
// src/lib/dynamic-ui-config.ts (jalankan sebelum render app)
import { setApiConfig } from '@gemafajarramadhan/dynamic-ui'

setApiConfig({
  apiBaseUrl: process.env.NEXT_PUBLIC_API_BASE_URL,

  getAuthToken: () => localStorage.getItem('access_token'),

  onUnauthorized: () => {
    window.location.href = '/login'
  },
})
```

Panggil di entry point:

```tsx
// app/layout.tsx atau _app.tsx
'use client'
import '@/lib/dynamic-ui-config' // ← jalankan konfigurasi lebih dulu
import '@gemafajarramadhan/dynamic-ui/style.css'
```

> File `.env.local` di host project Next.js:
> ```env
> NEXT_PUBLIC_API_BASE_URL=https://api.example.com
> ```

---

### Konfigurasi API di Angular

```ts
// main.ts
import { setApiConfig } from '@gemafajarramadhan/dynamic-ui'
import '@gemafajarramadhan/dynamic-ui'
import '@gemafajarramadhan/dynamic-ui/style.css'

// Konfigurasi sebelum bootstrap
setApiConfig({
  apiBaseUrl: (window as any).__env?.API_BASE_URL || 'https://api.example.com',

  getAuthToken: () => localStorage.getItem('access_token'),

  onUnauthorized: () => {
    window.location.href = '/login'
  },
})

platformBrowserDynamic().bootstrapModule(AppModule)
```

---

### Konfigurasi API via Window Global

Alternatif tanpa import fungsi – set sebelum library dimuat:

```html
<script>
  // Set sebelum import library
  window.__MICRO_API_BASE_URL__ = 'https://api.example.com'
  window.__MICRO_AUTH_TOKEN__ = 'your-auth-token' // atau ambil dari cookie/localStorage
</script>
<script type="module" src="./main.js"></script>
```

---

## Komponen Base (Vue Components)

Selain Web Components, library ini juga menyediakan **27 komponen Vue** yang bisa digunakan langsung di aplikasi Vue 3, Nuxt 3, dan framework apapun yang mendukung Vue.

### Daftar Komponen

| Komponen | Deskripsi |
|---|---|
| `DCodeTextField` | Input text dengan validasi, text case, filter karakter |
| `DCodeTextarea` | Textarea dengan auto-resize dan validasi |
| `DCodeButton` | Tombol dengan variant, warna kustom, tooltip, dan animasi |
| `DCodeCard` | Kartu container dengan styling bawaan |
| `DCodeCheckbox` | Checkbox dengan label |
| `DCodeSwitch` | Toggle switch |
| `DCodeRadioCustom` | Radio button dengan opsi kustom |
| `DCodeLabel` | Label untuk form |
| `DCodeAutoComplete` | Input autocomplete dengan pencarian dan fetch API otomatis |
| `DCodeMultiSelect` | Dropdown multi-pilihan |
| `DCodeDatePicker` | Date picker kalender |
| `DCodeCurrencyField` | Input nilai mata uang |
| `DCodeOtpInput` | Input OTP digit-per-digit |
| `DCodeFileField` | Input file dengan preview |
| `DCodeImageField` | Input gambar dengan preview |
| `DCodeDropzone` | Drag-and-drop upload area |
| `DCodeUploadFile` | Upload file terintegrasi |
| `DCodeFileResult` | Tampilan hasil file yang diupload |
| `DCodeImageResult` | Tampilan hasil gambar yang diupload |
| `DCodeImageCropperDialog` | Dialog crop gambar |
| `DCodeDialog` | Modal dialog |
| `DCodeDialogCloseBtn` | Tombol tutup untuk dialog |
| `DCodeIconDropdown` | Dropdown ikon |
| `DCodeProgressBar` | Progress bar loading |
| `DCodeTimelineWithIcons` | Timeline dengan ikon |
| `DCodeWizard` | Wizard / stepper multi-langkah |

---

### Penggunaan di Vue 3 (Import Per Komponen)

Cara paling ringan: import hanya komponen yang diperlukan.

```vue
<!-- MyForm.vue -->
<script setup>
import { DCodeTextField, DCodeButton } from '@gemafajarramadhan/dynamic-ui'
import '@gemafajarramadhan/dynamic-ui/style.css'
</script>

<template>
  <form>
    <DCodeTextField
      v-model="form.name"
      label="Nama Lengkap"
      placeholder="Masukkan nama..."
      :clearable="true"
    />

    <DCodeTextField
      v-model="form.email"
      label="Email"
      valueType="email"
      placeholder="nama@email.com"
    />

    <DCodeButton
      text="Simpan"
      variant="default"
      bgColor="primary"
      type="submit"
    />
  </form>
</template>

<script setup>
import { reactive } from 'vue'

const form = reactive({ name: '', email: '' })
</script>
```

---

### Penggunaan di Vue 3 (Global Plugin)

Install semua komponen sekaligus agar bisa digunakan di mana saja tanpa import per file.

```js
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import DynamicUI from '@gemafajarramadhan/dynamic-ui'
import '@gemafajarramadhan/dynamic-ui/style.css'

const app = createApp(App)

app.use(DynamicUI, {
  apiBaseUrl: import.meta.env.VITE_API_BASE_URL,
  getAuthToken: () => localStorage.getItem('access_token'),
})

app.mount('#app')
```

Setelah itu, semua komponen bisa digunakan **tanpa import** di setiap file:

```vue
<!-- Tidak perlu import lagi! -->
<template>
  <DCodeTextField v-model="name" label="Nama" />
  <DCodeButton text="Submit" />
  <DCodeDatePicker v-model="date" label="Tanggal" />
</template>
```

---

### Penggunaan di Nuxt 3 (Vue Components)

Buat plugin Nuxt untuk mendaftarkan semua komponen secara global:

```ts
// plugins/dynamic-ui.ts
import DynamicUI from '@gemafajarramadhan/dynamic-ui'
import '@gemafajarramadhan/dynamic-ui/style.css'

export default defineNuxtPlugin((nuxtApp) => {
  const config = useRuntimeConfig()

  nuxtApp.vueApp.use(DynamicUI, {
    apiBaseUrl: config.public.apiBaseUrl,
    getAuthToken: () => useCookie('access_token').value ?? null,
  })
})
```

Kemudian gunakan di page atau component manapun:

```vue
<!-- pages/index.vue -->
<template>
  <div>
    <DCodeTextField v-model="search" label="Pencarian" :clearable="true" />
    <DCodeButton text="Cari" variant="default" />
  </div>
</template>
```

---

## DCodeDataTable – Tabel Dinamis

`DCodeDataTable` adalah komponen tabel dinamis yang mendukung **pagination server-side**, **sorting**, **filtering**, **action buttons**, dan **badge kondisional** — seluruhnya dikonfigurasi via JSON schema.

Komponen ini dirancang sebagai **micro-frontend**: consumer bertanggung jawab penuh atas HTTP client, auth token, dan sistem permission.

---

### Props Utama

| Prop | Tipe | Default | Deskripsi |
|------|------|---------|-----------|
| `table-config` | `Object` | — | Config JSON (kolom, filter, action, pagination) |
| `fetch-handler` | `(params) => Promise<any>` | — | **[MFE]** Consumer kontrol penuh fetch. Component tidak hit endpoint sama sekali. |
| `http-client` | `({ url, method, data }) => Promise<any>` | — | **[MFE]** Axios instance consumer untuk action tombol row/header (DELETE, PATCH, dll.) |
| `action-access-checker` | `(path, action) => boolean` | `() => true` | **[MFE]** Fungsi RBAC dari consumer. Default: semua action allow. |
| `ignore-permissions` | `boolean` | `false` | Matikan filter permission sepenuhnya |
| `data` | `any[]` | `[]` | Data statis (tanpa API); pakai jika consumer sudah fetch di luar |
| `loading` | `boolean` | `false` | Loading state untuk mode data statis |
| `columns` | `TableColumn[]` | `[]` | Kolom manual (tanpa table-config) |
| `per-page` | `number` | `10` | Jumlah baris per halaman awal |
| `show-pagination` | `boolean` | `true` | Tampilkan pagination |
| `has-actions` | `boolean` | `false` | Tampilkan kolom aksi (untuk mode kolom manual) |
| `api-url` | `string` | — | URL endpoint langsung (tanpa table-config) |
| `api-method` | `string` | `'POST'` | HTTP method untuk apiUrl |
| `api-params` | `object` | `{}` | Parameter tetap yang selalu disertakan di setiap request |

### Events

| Event | Payload | Deskripsi |
|-------|---------|-----------|
| `@action` | `{ action, row, selection? }` | Dipanggil saat tombol action diklik |
| `@update:selection` | `any[]` | Dipanggil saat checkbox selection berubah |
| `@update:page` | `number` | Dipanggil saat halaman berganti |

---

### Pola 1 — `fetchHandler` ⭐ Disarankan untuk MFE

Consumer kontrol penuh HTTP. Component tidak hit endpoint sama sekali — cocok untuk menghindari error 401.

```vue
<template>
  <DCodeDataTable
    :table-config="tableConfig"
    :fetch-handler="fetchUsers"
    :http-client="httpClient"
    :action-access-checker="checkPermission"
    @action="handleAction"
  />
</template>

<script setup lang="ts">
import { DCodeDataTable } from '@gemafajarramadhan/dynamic-ui'
import axios from '@/lib/axios' // axios instance consumer — interceptor token sudah terpasang

// 1. fetchHandler: dipanggil tiap page/filter/sort/search change
//    params: { page, perPage, keyword?, ...filters }
const fetchUsers = async (params: Record<string, any>) => {
  const { data } = await axios.post('/api/users/list', params)
  return data
}

// 2. httpClient: untuk call dari tombol action row (delete, toggle, dll.)
const httpClient = async ({ url, method, data }: any) => {
  const res = await axios({ url, method, data })
  return res.data
}

// 3. Permission checker dari sistem RBAC consumer
const checkPermission = (path: string, action: string): boolean => {
  return useAuthStore().can(`${path}:${action}`)
}

// 4. Handle action event dari tabel
const handleAction = ({ action, row, selection }: any) => {
  if (action === 'edit')   router.push(`/users/${row.id}/edit`)
  if (action === 'delete') deleteUser(row.id)
  if (action === 'export') exportSelected(selection)
}

// 5. Konfigurasi tabel via JSON schema
const tableConfig = {
  headers: [
    { key: 'no',    labelID: 'No',    labelEN: 'No',    align: 'CENTER' },
    { key: 'name',  labelID: 'Nama',  labelEN: 'Name',  align: 'LEFT' },
    { key: 'email', labelID: 'Email', labelEN: 'Email', align: 'LEFT' },
    {
      key: 'status',
      labelID: 'Status', labelEN: 'Status',
      custom: {
        type: 'BADGE',
        conditionBadge: [
          { value: 'active',   labelID: 'Aktif',    labelEN: 'Active',   color: 'success' },
          { value: 'inactive', labelID: 'Nonaktif', labelEN: 'Inactive', color: 'danger' },
        ],
      },
    },
    {
      key: 'action',
      labelID: 'Aksi', labelEN: 'Actions',
      custom: {
        type: 'ACTION',
        actionCustom: [
          {
            permission: 'EDIT',
            propsActionCustom: {
              labelID: 'Edit', labelEN: 'Edit',
              icon: 'SquarePen', actionType: 'edit', bgColor: 'primary',
            },
          },
          {
            permission: 'DELETE',
            requireConfirm: true,
            propCustomMessage: {
              titleID: 'Hapus Data', titleEN: 'Delete',
              messageID: 'Yakin ingin menghapus?', messageEN: 'Are you sure?',
            },
            propsActionCustom: {
              labelID: 'Hapus', labelEN: 'Delete',
              icon: 'Trash2', actionType: 'delete', bgColor: 'red',
            },
          },
        ],
      },
    },
  ],
  filters: [
    {
      seq: 1, model: 'name', labelID: 'Nama', labelEN: 'Name', type: 'TEXT',
    },
    {
      seq: 2, model: 'status', labelID: 'Status', labelEN: 'Status', type: 'SELECT',
      propsFilters: {
        items: [
          { value: 'active',   label: 'Active' },
          { value: 'inactive', label: 'Inactive' },
        ],
      },
    },
  ],
  actionGlobal: [
    {
      permission: 'CREATE',
      propsActionGlobal: {
        labelID: 'Tambah', labelEN: 'Add New',
        icon: 'Plus', variant: 'default',
        route: '/users/create',
      },
    },
  ],
  pagination: true,
}
</script>
```

---

### Pola 2 — Static Data (consumer fetch di luar)

Pakai ketika consumer sudah fetch data sendiri dan ingin sekadar menampilkan tabel.

```vue
<template>
  <DCodeDataTable
    :columns="columns"
    :data="users"
    :loading="isLoading"
    has-actions
    ignore-permissions
    @action="handleAction"
  />
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import axios from '@/lib/axios'

const users = ref([])
const isLoading = ref(false)

onMounted(async () => {
  isLoading.value = true
  const { data } = await axios.get('/api/users')
  users.value = data.data
  isLoading.value = false
})

const columns = [
  { key: 'no',    label: 'No',    align: 'center' },
  { key: 'name',  label: 'Nama',  align: 'left' },
  { key: 'email', label: 'Email', align: 'left' },
]
</script>
```

---

### Pola 3 — Realtime Socket (consumer kelola socket)

Consumer connect ke WebSocket sendiri, push data ke tabel melalui `:data`.

```vue
<template>
  <DCodeDataTable :columns="columns" :data="realtimeData" ignore-permissions />
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { io } from 'socket.io-client'

const realtimeData = ref([])
let socket: any = null

onMounted(() => {
  socket = io('wss://your-server.com', {
    auth: { token: localStorage.getItem('token') }
  })
  socket.emit('dashboard:list', { page: 1, perPage: 10 })
  socket.on('dashboard:result', (res: any) => {
    realtimeData.value = res.data ?? res
  })
})

onUnmounted(() => socket?.disconnect())
</script>
```

---

### Format Response yang Didukung

Component secara otomatis mendeteksi berbagai format response API:

```json
{ "data": [], "totalData": 100 }    ✅ (disarankan)
{ "data": [], "maxPage": 10 }       ✅
{ "items": [], "total": 100 }       ✅
{ "list": [], "total": 100 }        ✅
{ "rows": [], "count": 100 }        ✅
{ "result": [], "total": 100 }      ✅
[]                                  ✅ (array langsung)
```

---

### Expose Methods (via `ref`)

```typescript
const tableRef = ref()

tableRef.value.fetchData()      // Refresh data (panggil ulang fetchHandler)
tableRef.value.handleRefresh()  // Reset filter & refresh
tableRef.value.resetFilters()   // Reset nilai filter saja (tanpa fetch)
console.log(tableRef.value.filters) // Reactive nilai filter saat ini
```

```vue
<DCodeDataTable ref="tableRef" :fetch-handler="fetchUsers" ... />
```

---

## DCodeAutoComplete – Komponen dengan API

`DCodeAutoComplete` adalah komponen autocomplete yang bisa **langsung fetch data dari API** menggunakan konfigurasi yang sudah di-set di host project.

> ⚠️ Pastikan sudah mengatur [Konfigurasi API](#-konfigurasi-api-wajib) sebelum menggunakan fitur endpoint.

### Perilaku Default

- Setiap fetch otomatis mengirimkan `page=1` dan `perPage=100` sebagai query parameter default.
- Nilai ini bisa di-override via prop `apiParams`.

### Props Utama

| Prop | Tipe | Default | Deskripsi |
|------|------|---------|-----------|
| `modelValue` | `any` | — | Value terpilih (v-model) |
| `endpoint.apiUrl` | `string` | — | URL endpoint API (path atau full URL) |
| `endpoint.apiMethod` | `string` | `'GET'` | HTTP method |
| `apiParams` | `object` | `{}` | Query params tambahan (di-merge & bisa override default) |
| `itemTitle` | `string\|Function` | `'name'` | Field nama tampilan dari tiap item |
| `itemValue` | `string\|Function` | `'id'` | Field nilai dari tiap item |
| `returnObject` | `boolean` | `false` | Jika `true`, v-model menyimpan objek penuh, bukan hanya `id` |
| `clearable` | `boolean` | `false` | Tampilkan tombol hapus pilihan |
| `disabled` | `boolean` | `false` | Nonaktifkan komponen |
| `autoFetchOnOpen` | `boolean` | `true` | Fetch otomatis saat dropdown dibuka pertama kali |
| `autoFetchOnMount` | `boolean` | `false` | Fetch otomatis saat komponen pertama kali di-mount |

### Contoh Penggunaan

#### Fetch dari API dengan URL langsung

```vue
<template>
  <DCodeAutoComplete
    v-model="selectedId"
    label="Pilih Departemen"
    :endpoint="{ apiUrl: '/api/master/departemen', apiMethod: 'GET' }"
    item-title="nama"
    item-value="id"
    clearable
  />
</template>
```

> Request yang dikirim: `GET /api/master/departemen?page=1&perPage=100`
> Base URL diambil otomatis dari konfigurasi host project (`apiBaseUrl`).

#### Dengan Parameter Tambahan

```vue
<template>
  <DCodeAutoComplete
    v-model="selectedId"
    label="Pilih Jabatan"
    :endpoint="{ apiUrl: '/api/master/jabatan' }"
    :apiParams="{ idDepartemen: selectedDeptId, isActive: true }"
    item-title="namaJabatan"
    item-value="idJabatan"
  />
</template>
```

> Request yang dikirim: `GET /api/master/jabatan?page=1&perPage=100&idDepartemen=5&isActive=true`

#### Override Default Pagination

```vue
<template>
  <!-- Ganti perPage menjadi 50 -->
  <DCodeAutoComplete
    v-model="selected"
    label="Pilih Data"
    :endpoint="{ apiUrl: '/api/data' }"
    :apiParams="{ perPage: 50 }"
    item-title="nama"
    item-value="id"
  />
</template>
```

#### Dengan Data Statis (tanpa API)

```vue
<template>
  <DCodeAutoComplete
    v-model="selected"
    label="Pilih Status"
    :items="[
      { id: 1, name: 'Aktif' },
      { id: 0, name: 'Nonaktif' },
    ]"
    item-title="name"
    item-value="id"
  />
</template>
```

#### Return Object Penuh

```vue
<script setup>
import { ref } from 'vue'

const selectedUser = ref(null) // akan berisi { id: 1, name: 'John', email: '...' }
</script>

<template>
  <DCodeAutoComplete
    v-model="selectedUser"
    label="Pilih User"
    :endpoint="{ apiUrl: '/api/users' }"
    item-title="name"
    item-value="id"
    return-object
  />
</template>
```

### Format Response API yang Diharapkan

```json
{
  "status": true,
  "data": {
    "data": [
      { "id": 1, "name": "Item A" },
      { "id": 2, "name": "Item B" }
    ]
  }
}
```

Atau format flat:

```json
{
  "data": [
    { "id": 1, "name": "Item A" }
  ]
}
```

Atau array langsung:

```json
[
  { "id": 1, "name": "Item A" }
]
```

---

## Web Components (Framework Agnostic)

Untuk digunakan di **React, Angular, Svelte**, atau framework lainnya, gunakan mode Web Components.

---

## API Referensi Web Components

### Web Components

Setelah library di-import, dua custom element berikut akan terdaftar secara global:

| Custom Element            | Deskripsi                                                   |
| ------------------------- | ----------------------------------------------------------- |
| `<micro-dynamic-form>`    | Render form dinamis berdasarkan JSON config `config` prop.  |
| `<micro-dynamic-datatable>` | Render tabel dinamis berdasarkan JSON config `config` prop. |
| `<micro-data-table>`      | Render Data Table base component secara independen. Menerima props seperti `columns`, `data`, `apiUrl`. |

#### Properties (set via JavaScript/DOM)

Karena Web Components menerima objek JavaScript (bukan string HTML attribute), set properti ini **secara imperatif via DOM** atau `ref`:

| Property  | Tipe     | Deskripsi                               |
| --------- | -------- | --------------------------------------- |
| `config`  | `Object` | Konfigurasi form / tabel dalam bentuk JSON. |

#### Events

| Event    | `event.detail`     | Deskripsi                                   |
| -------- | ------------------ | ------------------------------------------- |
| `submit` | `Object` (payload) | Dipanggil saat form di-submit.              |
| `action` | `Object` (payload) | Dipanggil saat action button datatable diklik. |

---

### Logic Registry API

Digunakan untuk menyuntikkan business logic dari aplikasi host ke dalam komponen.

```js
import {
  registerLogic,
  unregisterLogic,
  getLogicModule
} from '@gemafajarramadhan/dynamic-ui'
```

| Fungsi                        | Deskripsi                                                   |
| ----------------------------- | ----------------------------------------------------------- |
| `registerLogic(code, module)` | Daftarkan logic module untuk layout code tertentu.          |
| `unregisterLogic(code)`       | Hapus logic module dari registry.                           |
| `getLogicModule(code)`        | Ambil logic module berdasarkan layout code.                 |

---

## Penggunaan di Berbagai Framework

---

### 1. Vanilla HTML / JavaScript

Cara paling sederhana. Import library dari bundler, lalu gunakan custom element di HTML.

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Dynamic UI</title>
    <!-- Import style -->
    <link rel="stylesheet" href="./node_modules/@gemafajarramadhan/dynamic-ui/dist/style.css" />
  </head>
  <body>

    <!-- Dynamic Form -->
    <micro-dynamic-form id="myForm"></micro-dynamic-form>

    <!-- Dynamic DataTable (AutoLayout version) -->
    <micro-dynamic-datatable id="myAutoTable"></micro-dynamic-datatable>

    <!-- Base DataTable (Stand-alone version) -->
    <micro-data-table id="myBaseTable"></micro-data-table>

    <!-- Import library (registrasi Web Components otomatis) -->
    <script type="module">
      import '@gemafajarramadhan/dynamic-ui'
      import { setApiConfig } from '@gemafajarramadhan/dynamic-ui'

      // ✅ Konfigurasi API dari host project
      setApiConfig({
        apiBaseUrl: 'https://api.example.com',
        getAuthToken: () => localStorage.getItem('access_token'),
      })

      // --- Setup Form ---
      const form = document.querySelector('#myForm')
      form.config = {
        title: 'Registrasi User',
        sections: [
          {
            title: 'Info Pribadi',
            fields: [
              {
                key: 'name',
                label: 'Nama Lengkap',
                model: 'fullName',
                component: 'DCodeTextField',
              },
              {
                key: 'email',
                label: 'Email',
                model: 'email',
                component: 'DCodeTextField',
                props: { type: 'email' },
              },
            ],
          },
        ],
        actions: [{ key: 'submit', label: 'Daftar', component: 'DCodeButton' }],
      }

      form.addEventListener('submit', (e) => {
        console.log('Data Form:', e.detail)
      })

      // --- Setup DataTable ---
      const autoTable = document.querySelector('#myAutoTable')
      autoTable.config = {
        titleID: 'Daftar User',
        headers: [
          { text: 'Nama', value: 'name' },
          { text: 'Email', value: 'email' },
        ],
      }

      autoTable.addEventListener('action', (e) => {
        console.log('Action:', e.detail)
      })

      // --- Setup Base MicroDataTable ---
      const baseTable = document.querySelector('#myBaseTable')
      // Untuk MicroDataTable (base component), Anda bisa mengirimkan prop langsung:
      baseTable.columns = [
        { key: 'name', label: 'Nama', sortable: true },
        { key: 'email', label: 'Email' }
      ]
      // Memberikan data secara statis
      baseTable.data = [
        { name: 'John Doe', email: 'john@example.com' },
        { name: 'Jane Smith', email: 'jane@example.com' }
      ]
      
      // Atau bisa diset melalui API:
      // baseTable.apiUrl = '/api/users'
      // baseTable.apiMethod = 'GET'
    </script>

  </body>
</html>
```

---

### 2. Vue 3 (Web Components)

Di Vue 3, Web Components bisa dipakai langsung seperti HTML element biasa. Gunakan `ref` untuk set properti objek.

#### Konfigurasi Vite

Agar Vue tidak menganggap `micro-dynamic-*` sebagai Vue component yang belum terdaftar, tambahkan `isCustomElement` di `vite.config.js`:

```js
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [
    vue({
      template: {
        compilerOptions: {
          // Semua element yang dimulai dengan 'micro-' dianggap custom element
          isCustomElement: (tag) => tag.startsWith('micro-'),
        },
      },
    }),
  ],
})
```

#### Entry Point (`main.js`)

```js
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import DynamicUI from '@gemafajarramadhan/dynamic-ui'
import '@gemafajarramadhan/dynamic-ui/style.css'

const app = createApp(App)

// ✅ Konfigurasi API dari env host project
app.use(DynamicUI, {
  apiBaseUrl: import.meta.env.VITE_API_BASE_URL,
  getAuthToken: () => localStorage.getItem('access_token'),
  onUnauthorized: () => router.push('/login'),
})

app.mount('#app')
```

#### Komponen Vue (`MyForm.vue`)

```vue
<script setup>
import { ref, onMounted } from 'vue'

const formRef = ref(null)

const formConfig = {
  title: 'Form User',
  sections: [
    {
      fields: [
        {
          key: 'username',
          label: 'Username',
          model: 'username',
          component: 'DCodeTextField',
        },
      ],
    },
  ],
  actions: [{ key: 'save', label: 'Simpan', component: 'DCodeButton' }],
}

onMounted(() => {
  if (formRef.value) {
    // Set config via DOM property (bukan attribute)
    formRef.value.config = formConfig

    formRef.value.addEventListener('submit', (e) => {
      console.log('Submitted:', e.detail)
    })
  }
})
</script>

<template>
  <micro-dynamic-form ref="formRef" />
</template>
```

---

### 3. React

Di React, gunakan `useRef` dan `useEffect` untuk set properti DOM secara imperatif karena React tidak meneruskan objek langsung ke Web Component attributes.

#### Entry Point (`main.jsx`)

```jsx
// main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import { setApiConfig } from '@gemafajarramadhan/dynamic-ui'
import '@gemafajarramadhan/dynamic-ui'
import '@gemafajarramadhan/dynamic-ui/style.css'

// ✅ Konfigurasi API dari env host project
setApiConfig({
  apiBaseUrl: import.meta.env.VITE_API_BASE_URL,
  getAuthToken: () => localStorage.getItem('access_token'),
  onUnauthorized: () => { window.location.href = '/login' },
})

ReactDOM.createRoot(document.getElementById('root')).render(<App />)
```

#### Dynamic Form Component

```jsx
// components/DynamicForm.jsx
import React, { useEffect, useRef } from 'react'

const formConfig = {
  title: 'Form via React',
  sections: [
    {
      fields: [
        {
          key: 'username',
          label: 'Username',
          model: 'username',
          component: 'DCodeTextField',
        },
        {
          key: 'email',
          label: 'Email',
          model: 'email',
          component: 'DCodeTextField',
          props: { type: 'email' },
        },
      ],
    },
  ],
  actions: [{ key: 'submit', label: 'Kirim', component: 'DCodeButton' }],
}

export default function DynamicForm() {
  const formRef = useRef(null)

  useEffect(() => {
    const el = formRef.current
    if (!el) return

    // Set config sebagai DOM property
    el.config = formConfig

    const handleSubmit = (event) => {
      console.log('Hasil submit:', event.detail)
    }

    el.addEventListener('submit', handleSubmit)

    // Cleanup listener saat unmount
    return () => {
      el.removeEventListener('submit', handleSubmit)
    }
  }, [])

  return <micro-dynamic-form ref={formRef}></micro-dynamic-form>
}
```

#### Dynamic DataTable Component

```jsx
// components/DynamicTable.jsx
import React, { useEffect, useRef } from 'react'

const tableConfig = {
  titleID: 'Daftar User',
  headers: [
    { text: 'Nama', value: 'name' },
    { text: 'Email', value: 'email' },
    { text: 'Status', value: 'status' },
  ],
}

export default function DynamicTable() {
  const tableRef = useRef(null)

  useEffect(() => {
    const el = tableRef.current
    if (!el) return

    el.config = tableConfig

    const handleAction = (event) => {
      console.log('Action:', event.detail)
    }

    el.addEventListener('action', handleAction)

    return () => {
      el.removeEventListener('action', handleAction)
    }
  }, [])

  return <micro-dynamic-datatable ref={tableRef}></micro-dynamic-datatable>
}
```

#### TypeScript + React

Tambahkan deklarasi tipe agar JSX tidak mengeluh tentang custom element:

```ts
// src/types/dynamic-ui.d.ts
import React from 'react'

declare global {
  namespace JSX {
    interface IntrinsicElements {
      'micro-dynamic-form': React.DetailedHTMLProps<
        React.HTMLAttributes<HTMLElement>,
        HTMLElement
      >
      'micro-dynamic-datatable': React.DetailedHTMLProps<
        React.HTMLAttributes<HTMLElement>,
        HTMLElement
      >
    }
  }
}
```

---

### 4. Next.js (App Router)

Di Next.js dengan App Router, Web Components harus di-import di sisi klien (`'use client'`).

#### Setup Konfigurasi API

```ts
// lib/dynamic-ui-config.ts
import { setApiConfig } from '@gemafajarramadhan/dynamic-ui'

setApiConfig({
  apiBaseUrl: process.env.NEXT_PUBLIC_API_BASE_URL!,
  getAuthToken: () => localStorage.getItem('access_token'),
  onUnauthorized: () => { window.location.href = '/login' },
})
```

#### Setup di Layout atau Client Component

```tsx
// components/DynamicForm.tsx
'use client'

import { useEffect, useRef } from 'react'
import '@/lib/dynamic-ui-config'
import('@gemafajarramadhan/dynamic-ui')
import '@gemafajarramadhan/dynamic-ui/style.css'

const formConfig = {
  title: 'Form Next.js',
  sections: [
    {
      fields: [
        {
          key: 'name',
          label: 'Nama',
          model: 'name',
          component: 'DCodeTextField',
        },
      ],
    },
  ],
  actions: [{ key: 'submit', label: 'Submit', component: 'DCodeButton' }],
}

export default function DynamicFormClient() {
  const formRef = useRef<HTMLElement>(null)

  useEffect(() => {
    const el = formRef.current
    if (!el) return

    ;(el as any).config = formConfig

    const handleSubmit = (event: Event) => {
      const customEvent = event as CustomEvent
      console.log('Submit:', customEvent.detail)
    }

    el.addEventListener('submit', handleSubmit)
    return () => el.removeEventListener('submit', handleSubmit)
  }, [])

  return (
    // @ts-ignore – custom element tidak dikenal TypeScript secara default
    <micro-dynamic-form ref={formRef}></micro-dynamic-form>
  )
}
```

#### Penggunaan di Page

```tsx
// app/page.tsx
import DynamicFormClient from '@/components/DynamicForm'

export default function HomePage() {
  return (
    <main>
      <h1>Dynamic Form</h1>
      <DynamicFormClient />
    </main>
  )
}
```

> **Catatan:** Gunakan `dynamic()` dari `next/dynamic` dengan `{ ssr: false }` jika terjadi error SSR.
>
> ```tsx
> import dynamic from 'next/dynamic'
> const DynamicFormClient = dynamic(() => import('@/components/DynamicForm'), { ssr: false })
> ```

> File `.env.local` di host project:
> ```env
> NEXT_PUBLIC_API_BASE_URL=https://api.example.com
> ```

---

### 5. Angular

Di Angular, tambahkan `CUSTOM_ELEMENTS_SCHEMA` agar Angular tidak melempar error pada custom element yang tidak dikenali.

#### `app.module.ts` (Module-based)

```ts
// app.module.ts
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { AppComponent } from './app.component'

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  schemas: [CUSTOM_ELEMENTS_SCHEMA], // <-- Wajib ditambahkan
  bootstrap: [AppComponent],
})
export class AppModule {}
```

#### `main.ts`

```ts
// main.ts
import { setApiConfig } from '@gemafajarramadhan/dynamic-ui'
import '@gemafajarramadhan/dynamic-ui'
import '@gemafajarramadhan/dynamic-ui/style.css'

// ✅ Konfigurasi API dari environment Angular
import { environment } from './environments/environment'

setApiConfig({
  apiBaseUrl: environment.apiBaseUrl,
  getAuthToken: () => localStorage.getItem('access_token'),
  onUnauthorized: () => { window.location.href = '/login' },
})

platformBrowserDynamic().bootstrapModule(AppModule)
```

> File `src/environments/environment.ts`:
> ```ts
> export const environment = {
>   production: false,
>   apiBaseUrl: 'https://api.example.com',
> }
> ```

#### Komponen Angular (`dynamic-form.component.ts`)

```ts
// dynamic-form.component.ts
import {
  Component,
  OnInit,
  AfterViewInit,
  ViewChild,
  ElementRef,
} from '@angular/core'

@Component({
  selector: 'app-dynamic-form',
  template: `<micro-dynamic-form #formEl></micro-dynamic-form>`,
})
export class DynamicFormComponent implements AfterViewInit {
  @ViewChild('formEl') formRef!: ElementRef

  private formConfig = {
    title: 'Form Angular',
    sections: [
      {
        fields: [
          {
            key: 'username',
            label: 'Username',
            model: 'username',
            component: 'DCodeTextField',
          },
        ],
      },
    ],
    actions: [{ key: 'submit', label: 'Simpan', component: 'DCodeButton' }],
  }

  ngAfterViewInit() {
    const el = this.formRef.nativeElement

    // Set config via DOM property
    el.config = this.formConfig

    el.addEventListener('submit', (event: CustomEvent) => {
      console.log('Submit:', event.detail)
    })
  }
}
```

#### Angular Standalone (Angular 17+)

```ts
// app.component.ts
import { Component, AfterViewInit, ViewChild, ElementRef } from '@angular/core'
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'

@Component({
  selector: 'app-root',
  standalone: true,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `<micro-dynamic-form #formEl></micro-dynamic-form>`,
})
export class AppComponent implements AfterViewInit {
  @ViewChild('formEl') formRef!: ElementRef

  ngAfterViewInit() {
    const el = this.formRef.nativeElement
    el.config = { /* config Anda di sini */ }
    el.addEventListener('submit', (e: CustomEvent) => console.log(e.detail))
  }
}
```

---

### 6. Nuxt 3

Di Nuxt 3, gunakan plugin untuk mendaftarkan Web Components dan tandai sebagai custom element.

#### Plugin (`plugins/dynamic-ui.client.ts`)

Buat file plugin dengan suffix `.client.ts` agar hanya dijalankan di sisi klien (browser):

```ts
// plugins/dynamic-ui.client.ts
import DynamicUI from '@gemafajarramadhan/dynamic-ui'
import '@gemafajarramadhan/dynamic-ui/style.css'

export default defineNuxtPlugin((nuxtApp) => {
  const config = useRuntimeConfig()

  // ✅ Konfigurasi API dari runtimeConfig Nuxt (env host project)
  nuxtApp.vueApp.use(DynamicUI, {
    apiBaseUrl: config.public.apiBaseUrl,
    getAuthToken: () => useCookie('access_token').value ?? null,
    onUnauthorized: () => navigateTo('/login'),
  })
})
```

#### Konfigurasi Nuxt (`nuxt.config.ts`)

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      apiBaseUrl: process.env.VITE_API_BASE_URL, // set di .env
    },
  },
  vue: {
    compilerOptions: {
      // Arahkan Nuxt/Vue agar tidak menganggap element ini sebagai Vue component
      isCustomElement: (tag: string) => tag.startsWith('micro-'),
    },
  },
})
```

#### Penggunaan di Page atau Component

```vue
<!-- pages/index.vue -->
<script setup>
import { ref, onMounted } from 'vue'

const formRef = ref(null)

const formConfig = {
  title: 'Form Nuxt',
  sections: [
    {
      fields: [
        {
          key: 'name',
          label: 'Nama',
          model: 'name',
          component: 'DCodeTextField',
        },
      ],
    },
  ],
  actions: [{ key: 'submit', label: 'Submit', component: 'DCodeButton' }],
}

onMounted(() => {
  if (formRef.value) {
    formRef.value.config = formConfig
    formRef.value.addEventListener('submit', (e) => {
      console.log('Submit:', e.detail)
    })
  }
})
</script>

<template>
  <div>
    <h1>Halaman Dynamic Form</h1>
    <!-- Gunakan ClientOnly agar tidak error saat SSR -->
    <ClientOnly>
      <micro-dynamic-form ref="formRef" />
    </ClientOnly>
  </div>
</template>
```

---

## Custom Logic Registry

Library menyediakan **Logic Registry** agar aplikasi host dapat menyuntikkan business logic ke dalam komponen tanpa memodifikasi library.

### Option A: via `window` (sebelum library dimuat)

```js
window.__MICRO_LOGIC_REGISTRY__ = {
  'user-management': {
    onReady(context) {
      console.log('Component siap:', context)
    },
    handleAction(action, payload) {
      if (action === 'delete') {
        // Tampilkan modal konfirmasi
        return true // intercept action
      }
    },
  },
}
```

### Option B: via fungsi `registerLogic` (setelah import library)

```js
import {
  registerLogic,
  unregisterLogic,
} from '@gemafajarramadhan/dynamic-ui'

registerLogic('user-management', {
  onReady(context) {
    console.log('Component siap, context:', context)
  },
  handleAction(action, payload) {
    console.log('Action:', action, 'Payload:', payload)
  },
})

// Untuk menghapus logic saat komponen unmount:
// unregisterLogic('user-management')
```

### Interface LogicModule

```ts
interface LogicModule {
  onReady?: (context: Record<string, any>) => void
  handleAction?: (action: string, payload: any) => boolean | void
}
```

---

## TypeScript Support

Untuk menambahkan type yang benar pada semua framework, buat file deklarasi global:

```ts
// src/types/dynamic-ui.d.ts

declare namespace DynamicUI {
  interface LogicModule {
    onReady?: (context: Record<string, any>) => void
    handleAction?: (action: string, payload: any) => boolean | void
  }
}

// Deklarasi untuk custom element di JSX (React / Next.js)
declare global {
  namespace JSX {
    interface IntrinsicElements {
      'micro-dynamic-form': any
      'micro-dynamic-datatable': any
    }
  }

  // Untuk global window registry
  interface Window {
    __MICRO_LOGIC_REGISTRY__?: Record<string, DynamicUI.LogicModule>
    __MICRO_API_BASE_URL__?: string
    __MICRO_AUTH_TOKEN__?: string
  }
}

export {}
```

---

## Referensi Props Komponen

Berikut adalah dokumentasi lengkap props untuk setiap komponen yang tersedia.

---

### `DataTable`

Komponen tabel data yang mendukung mode API (fetch otomatis) maupun data statis, dengan pagination, sorting, filtering, dan action button.

| Prop | Tipe | Default | Deskripsi |
|------|------|---------|-----------|
| `columns` | `TableColumn[]` | `[]` | Definisi kolom tabel |
| `data` | `any[]` | `[]` | Data statis (mode non-API) |
| `perPage` | `number` | `10` | Jumlah baris per halaman |
| `loading` | `boolean` | `false` | Status loading eksternal |
| `hasActions` | `boolean` | `false` | Tampilkan kolom aksi baris |
| `nested` | `boolean` | `false` | Mode nested (tampilan kompak tanpa header search) |
| `apiModule` | `string` | — | Module API untuk fetch data |
| `apiAction` | `string` | `'list'` | Action API |
| `apiMethod` | `string` | — | HTTP method (`GET`/`POST`) |
| `apiUrl` | `string` | — | URL endpoint API langsung |
| `apiParams` | `object` | `{}` | Parameter tambahan untuk request API |
| `tableConfig` | `any` | — | Konfigurasi JSON lengkap tabel (dari backend) |
| `dynamicColumns` | `boolean` | `false` | Gunakan kolom dinamis dari response API |
| `enableQuickSearch` | `boolean` | `true` | Aktifkan pencarian cepat (debounce) |
| `showPagination` | `boolean` | `true` | Tampilkan pagination |
| `ignorePermissions` | `boolean` | `false` | Abaikan cek permission |
| `permissionBase` | `string` | — | Base path untuk permission check |

```vue
<template>
  <!-- Mode API -->
  <DataTable
    api-url="/api/users"
    api-method="POST"
    :columns="[
      { key: 'name', label: 'Nama', sortable: true },
      { key: 'email', label: 'Email' },
    ]"
    :per-page="25"
  />

  <!-- Mode data statis -->
  <DataTable
    :data="myData"
    :columns="columns"
  />
</template>
```

---

### `DCodeButton`

Tombol dengan berbagai variant, warna kustom, ikon Lucide, dan tooltip.

| Prop | Tipe | Default | Deskripsi |
|------|------|---------|-----------|
| `text` | `string` | `''` | Teks tombol |
| `icon` | `string` | — | Nama ikon Lucide (e.g. `'Save'`, `'Trash2'`) |
| `variant` | `'default'\|'secondary'\|'destructive'\|'outline'\|'ghost'\|'link'\|'tonal'\|'elevated'` | `'default'` | Variant tampilan |
| `size` | `'default'\|'sm'\|'lg'\|'icon'` | `'default'` | Ukuran tombol |
| `tooltip` | `string` | `''` | Teks tooltip |
| `bgColor` | `string` | `''` | Warna background kustom (tailwind/hex) |
| `textColor` | `string` | `''` | Warna teks kustom |
| `hoverBgColor` | `string` | `''` | Warna background saat hover |
| `borderColor` | `string` | `''` | Warna border kustom |
| `disabled` | `boolean` | `false` | Nonaktifkan tombol |
| `type` | `'button'\|'submit'\|'reset'` | `'button'` | Tipe HTML button |
| `visible` | `boolean` | `true` | Tampilkan/sembunyikan tombol |
| `skeleton` | `boolean` | `false` | Tampilkan skeleton loading |

```vue
<template>
  <DCodeButton text="Simpan" icon="Save" variant="default" />
  <DCodeButton text="Hapus" icon="Trash2" variant="destructive" tooltip="Hapus data ini" />
  <DCodeButton text="Submit" type="submit" :disabled="isLoading" />
</template>
```

---

### `DCodeTextField`

Input teks dengan banyak fitur: filter karakter, transformasi case, clearable, password toggle.

| Prop | Tipe | Default | Deskripsi |
|------|------|---------|-----------|
| `modelValue` | `string\|number\|null` | — | Value input (v-model) |
| `prependInner` | `string` | — | Teks/ikon di sisi kiri input |
| `clearable` | `boolean` | — | Tampilkan tombol clear |
| `hideClearWhenEmpty` | `boolean` | — | Sembunyikan tombol clear saat kosong |
| `textCase` | `'none'\|'uppercase'\|'lowercase'\|'titlecase'\|'sentencecase'` | `'none'` | Transformasi huruf |
| `applyTextCaseToValue` | `boolean` | `true` | Terapkan transformasi ke v-model |
| `valueType` | `'free'\|'number'\|'letter'\|'letternumber'\|'email'\|'password'` | `'free'` | Filter jenis karakter yang diperbolehkan |
| `allowSpaces` | `boolean` | — | Izinkan spasi |
| `allowChars` | `string\|string[]` | — | Karakter tambahan yang diizinkan |
| `pattern` | `RegExp` | — | Regex validasi |
| `visible` | `boolean` | `true` | Tampilkan/sembunyikan |
| `skeleton` | `boolean` | `false` | Tampilkan skeleton |
| `returnNumber` | `boolean` | — | Emit nilai sebagai number |
| `error` | `string\|null` | — | Pesan error validasi |

```vue
<template>
  <DCodeTextField
    v-model="form.name"
    label="Nama Lengkap"
    placeholder="Masukkan nama..."
    text-case="titlecase"
    clearable
  />
  <DCodeTextField
    v-model="form.email"
    label="Email"
    value-type="email"
    :error="errors.email"
  />
  <DCodeTextField
    v-model="form.password"
    label="Password"
    value-type="password"
  />
</template>
```

---

### `DCodeTextarea`

Textarea dengan karakter counter, filter, dan transformasi case.

| Prop | Tipe | Default | Deskripsi |
|------|------|---------|-----------|
| `modelValue` | `string\|number\|null` | — | Value (v-model) |
| `clearable` | `boolean` | — | Tombol clear |
| `hideClearWhenEmpty` | `boolean` | `true` | Sembunyikan clear saat kosong |
| `textCase` | `'none'\|'uppercase'\|'lowercase'\|'titlecase'\|'sentencecase'` | `'none'` | Transformasi huruf |
| `applyTextCaseToValue` | `boolean` | `true` | Terapkan ke v-model |
| `valueType` | `'free'\|'number'\|'letter'\|'letternumber'` | `'free'` | Filter karakter |
| `allowSpaces` | `boolean` | `true` | Izinkan spasi |
| `allowChars` | `string\|string[]` | — | Karakter tambahan |
| `pattern` | `RegExp` | — | Regex validasi |
| `visible` | `boolean` | `true` | Tampilkan/sembunyikan |
| `skeleton` | `boolean` | `false` | Skeleton loading |
| `showCharCount` | `boolean` | `true` | Tampilkan counter karakter |
| `returnNumber` | `boolean` | `false` | Emit sebagai number |
| `error` | `string\|null` | `null` | Pesan error |

```vue
<template>
  <DCodeTextarea
    v-model="form.description"
    label="Deskripsi"
    placeholder="Masukkan deskripsi..."
    :show-char-count="true"
    maxlength="500"
  />
</template>
```

---

### `DCodeAutoComplete`

Dropdown autocomplete dengan fetch API otomatis dan pencarian.

| Prop | Tipe | Default | Deskripsi |
|------|------|---------|-----------|
| `modelValue` | `any` | — | Value terpilih (v-model) |
| `endpoint` | `{ apiUrl?, apiMethod?, apiModule?, apiAction? }` | — | Konfigurasi endpoint API |
| `apiParams` | `object` | `{}` | Parameter tambahan request |
| `fetchItems` | `Function` | — | Fungsi custom fetch (alternatif endpoint) |
| `itemTitle` | `string\|Function` | `'name'` | Field label item |
| `itemValue` | `string\|Function` | `'id'` | Field nilai item |
| `returnObject` | `boolean` | `false` | Return objek penuh |
| `autoFetchOnOpen` | `boolean` | `true` | Fetch saat dropdown terbuka |
| `autoFetchOnMount` | `boolean` | `false` | Fetch saat mount |
| `clearable` | `boolean` | `false` | Tombol clear |
| `disabled` | `boolean` | `false` | Nonaktifkan |
| `visible` | `boolean` | `true` | Tampilkan/sembunyikan |
| `skeleton` | `boolean` | `false` | Skeleton loading |
| `error` | `string\|null` | `null` | Pesan error |
| `icon` | `Component` | — | Ikon kiri input |

```vue
<template>
  <DCodeAutoComplete
    v-model="form.departemenId"
    label="Departemen"
    :endpoint="{ apiUrl: '/api/master/departemen' }"
    item-title="nama"
    item-value="id"
    clearable
  />
</template>
```

---

### `DCodeInfiniteAutoComplete`

Autocomplete dengan pagination infinite scroll (load more saat scroll ke bawah).

| Prop | Tipe | Default | Deskripsi |
|------|------|---------|-----------|
| `modelValue` | `any` | — | Value (v-model) |
| `endpoint` | `{ apiUrl?, apiMethod? }` | — | Endpoint API |
| `apiParams` | `object` | `{}` | Parameter tambahan |
| `fetchItems` | `Function` | — | Custom fetch |
| `itemTitle` | `string\|Function` | `'name'` | Field label |
| `itemValue` | `string\|Function` | `'id'` | Field nilai |
| `itemSubtitle` | `string\|Function` | — | Field subtitle item |
| `returnObject` | `boolean` | `false` | Return objek penuh |
| `autoFetchOnOpen` | `boolean` | `true` | Fetch saat buka |
| `refreshOnOpen` | `boolean` | `false` | Refresh data setiap buka |
| `clearable` | `boolean` | `false` | Tombol clear |
| `disabled` | `boolean` | `false` | Nonaktifkan |
| `visible` | `boolean` | `true` | Tampilkan |
| `skeleton` | `boolean` | `false` | Skeleton |
| `error` | `string\|null` | `null` | Error message |
| `icon` | `Component` | — | Ikon prefix |
| `pageSize` | `number` | `20` | Jumlah item per halaman |

```vue
<template>
  <DCodeInfiniteAutoComplete
    v-model="form.userId"
    label="Pilih User"
    :endpoint="{ apiUrl: '/api/users', apiMethod: 'GET' }"
    item-title="fullName"
    item-value="id"
    :page-size="15"
  />
</template>
```

---

### `DCodeMultiSelect`

Dropdown multi-pilihan dengan chip tags dan search.

| Prop | Tipe | Default | Deskripsi |
|------|------|---------|-----------|
| `modelValue` | `any[]` | — | Array nilai terpilih (v-model) |
| `items` | `object[]` | `[]` | Data statis |
| `endpoint` | `{ apiUrl?, apiMethod?, apiOptions? }` | — | Endpoint API |
| `fetchItems` | `Function` | — | Custom fetch |
| `autoFetchOnOpen` | `boolean` | `true` | Fetch saat buka |
| `autoFetchOnMount` | `boolean` | `false` | Fetch saat mount |
| `refreshOnOpen` | `boolean` | `false` | Refresh setiap buka |
| `label` | `string` | `''` | Label dropdown |
| `placeholder` | `string` | `'Pilih data…'` | Placeholder |
| `disabled` | `boolean` | `false` | Nonaktifkan |
| `clearable` | `boolean` | `false` | Tombol clear |
| `searchable` | `boolean` | `false` | Aktifkan search |
| `searchPlaceholder` | `string` | `'Cari…'` | Placeholder search |
| `visible` | `boolean` | `true` | Tampilkan |
| `skeleton` | `boolean` | `false` | Skeleton |
| `maxChips` | `number\|null` | `2` | Maks chip ditampilkan |
| `maxSelected` | `number\|null` | `3` | Maks pilihan |
| `itemTitle` | `string\|Function` | — | Field label |
| `itemValue` | `string\|Function` | — | Field nilai |
| `returnObject` | `boolean` | — | Return objek penuh |
| `error` | `string\|null` | — | Error message |

```vue
<template>
  <DCodeMultiSelect
    v-model="form.tags"
    label="Tags"
    :items="availableTags"
    item-title="name"
    item-value="id"
    :max-selected="5"
    searchable
  />
</template>
```

---

### `DCodeDatePicker`

Date picker atau date range picker dengan berbagai format.

| Prop | Tipe | Default | Deskripsi |
|------|------|---------|-----------|
| `modelValue` | `string\|[string,string]\|null` | — | Value tanggal (v-model) |
| `range` | `boolean` | `true` | Mode range (pilih 2 tanggal) |
| `autoRangeDays` | `number\|null` | `null` | Otomatis set range N hari |
| `disablePast` | `boolean` | `false` | Nonaktifkan tanggal masa lalu |
| `maxDaysAhead` | `number\|null` | `null` | Maks hari ke depan |
| `placeholder` | `string` | `'Pilih tanggal'` | Placeholder |
| `label` | `string` | — | Label |
| `disabled` | `boolean` | `false` | Nonaktifkan |
| `clearable` | `boolean` | `true` | Tombol clear |
| `displayFormat` | `string` | `'dd/MM/yyyy'` | Format tampilan |
| `modelType` | `string` | `'yyyy-MM-dd'` | Format v-model |
| `locale` | `'id'\|'en'` | `'en'` | Bahasa kalender |
| `placement` | `string` | `'bottom-start'` | Posisi dropdown |
| `prependInner` | `string` | — | Teks prefix |
| `error` | `string\|null` | `null` | Error message |
| `tone` | `string` | `'primary'` | Warna tema kalender |
| `skeleton` | `boolean` | `false` | Skeleton loading |

```vue
<template>
  <!-- Single date -->
  <DCodeDatePicker
    v-model="form.tanggal"
    label="Tanggal Lahir"
    :range="false"
    display-format="dd MMMM yyyy"
    locale="id"
  />

  <!-- Date range -->
  <DCodeDatePicker
    v-model="form.periode"
    label="Periode"
    :range="true"
  />
</template>
```

---

### `DCodeCurrencyField`

Input angka dengan format mata uang otomatis.

| Prop | Tipe | Default | Deskripsi |
|------|------|---------|-----------|
| `modelValue` | `number\|null` | — | Nilai angka (v-model) |
| `currency` | `string` | `'IDR'` | Kode mata uang (`'IDR'`, `'USD'`) |
| `locale` | `string` | `'en-US'` | Locale format angka |
| `label` | `string` | — | Label field |
| `placeholder` | `string` | — | Placeholder |
| `error` | `string\|null` | — | Error message |
| `disabled` | `boolean` | — | Nonaktifkan |
| `readonly` | `boolean` | — | Read-only |
| `clearable` | `boolean` | — | Tombol clear |
| `hideClearWhenEmpty` | `boolean` | `true` | Sembunyikan clear jika kosong |
| `prependInner` | `string` | — | Override simbol mata uang |
| `skeleton` | `boolean` | `false` | Skeleton loading |

```vue
<template>
  <DCodeCurrencyField
    v-model="form.harga"
    label="Harga"
    currency="IDR"
    clearable
    :error="errors.harga"
  />
</template>
```

---

### `DCodeCheckbox`

Checkbox dengan label dan dukungan indeterminate state.

| Prop | Tipe | Default | Deskripsi |
|------|------|---------|-----------|
| `modelValue` | `boolean\|null` | — | Nilai checkbox (v-model) |
| `visible` | `boolean` | `true` | Tampilkan |
| `skeleton` | `boolean` | `false` | Skeleton |
| `indeterminate` | `boolean` | `false` | State indeterminate |
| `disabled` | `boolean` | `false` | Nonaktifkan |

Gunakan attr `label` untuk teks di samping checkbox.

```vue
<template>
  <DCodeCheckbox
    v-model="form.setuju"
    label="Saya setuju dengan syarat & ketentuan"
    required
  />
</template>
```

---

### `DCodeSwitch`

Toggle switch dengan berbagai ukuran, warna, dan tooltip.

| Prop | Tipe | Default | Deskripsi |
|------|------|---------|-----------|
| `modelValue` | `boolean` | `false` | Status switch (v-model) |
| `label` | `string` | `''` | Label teks |
| `disabled` | `boolean` | `false` | Nonaktifkan |
| `size` | `'small'\|'medium'\|'large'` | `'medium'` | Ukuran switch |
| `color` | `'primary'\|'success'\|'warning'\|'danger'` | `'primary'` | Warna aktif |
| `loading` | `boolean` | `false` | Status loading |
| `labelPosition` | `'left'\|'right'` | `'right'` | Posisi label |
| `required` | `boolean` | `false` | Required |
| `showTooltip` | `boolean` | `false` | Tampilkan tooltip |
| `tooltipActiveText` | `string` | `'Aktif'` | Teks tooltip saat aktif |
| `tooltipInactiveText` | `string` | `'Tidak Aktif'` | Teks tooltip saat nonaktif |

```vue
<template>
  <DCodeSwitch
    v-model="form.isActive"
    label="Status Aktif"
    color="success"
    size="medium"
    :show-tooltip="true"
  />
</template>
```

---

### `DCodeRadio`

Grup radio button yang bisa fetch opsi dari API.

| Prop | Tipe | Default | Deskripsi |
|------|------|---------|-----------|
| `modelValue` | `any` | — | Value terpilih (v-model) |
| `label` | `string` | `'name'` | Key field label dari opsi |
| `options` | `any[]` | `[]` | Opsi data statis |
| `apiUrl` | `string` | `''` | URL endpoint API untuk fetch opsi |
| `apiMethod` | `'GET'\|'POST'` | `'GET'` | HTTP method |
| `dataPath` | `string` | `''` | Path ke array data dalam response |
| `layout` | `boolean` | `true` | Layout horizontal (true) / vertikal |
| `disabled` | `boolean` | `false` | Nonaktifkan |
| `required` | `boolean` | `false` | Required |
| `error` | `string\|null` | `null` | Error message |
| `size` | `'sm'\|'md'\|'lg'` | `'md'` | Ukuran radio |
| `color` | `string` | `'blue'` | Warna accent |
| `model` | `string` | `'id'` | Key field nilai dari opsi |

```vue
<template>
  <!-- Dengan opsi statis -->
  <DCodeRadio
    v-model="form.gender"
    :options="[
      { id: 'L', name: 'Laki-laki' },
      { id: 'P', name: 'Perempuan' },
    ]"
  />

  <!-- Dengan API -->
  <DCodeRadio
    v-model="form.statusId"
    api-url="/api/master/status"
    label="nama"
    model="id"
  />
</template>
```

---

### `DCodeRadioCustom`

Radio button dengan tampilan card kustom dan ikon.

| Prop | Tipe | Default | Deskripsi |
|------|------|---------|-----------|
| `modelValue` | `any` | — | Value (v-model) |
| `icon` | `string` | — | Nama ikon Lucide untuk setiap card |
| `label` | `string` | `'name'` | Key field label |
| `options` | `any[]` | `[]` | Opsi statis |
| `apiUrl` | `string` | `''` | URL API |
| `apiMethod` | `'GET'\|'POST'` | `'GET'` | HTTP method |
| `dataPath` | `string` | `''` | Path data dalam response |
| `layout` | `boolean` | `true` | Horizontal layout |
| `disabled` | `boolean` | `false` | Nonaktifkan |
| `required` | `boolean` | `false` | Required |
| `error` | `string\|null` | `null` | Error message |
| `size` | `'sm'\|'md'\|'lg'` | `'md'` | Ukuran |
| `color` | `string` | `'blue'` | Warna highlight |
| `model` | `string` | `'id'` | Key field nilai |

```vue
<template>
  <DCodeRadioCustom
    v-model="form.plan"
    :options="[
      { id: 'basic', name: 'Basic', icon: 'Star' },
      { id: 'pro', name: 'Pro', icon: 'Zap' },
    ]"
    label="name"
    model="id"
  />
</template>
```

---

### `DCodeOtpInput`

Input OTP digit-per-digit dengan submit otomatis ke API.

| Prop | Tipe | Default | Deskripsi |
|------|------|---------|-----------|
| `modelValue` | `string` | `''` | Nilai OTP (v-model) |
| `totalInput` | `number` | `6` | Jumlah digit OTP |
| `default` | `string` | `''` | Default nilai awal |
| `label` | `string` | `''` | Label |
| `endpoint` | `{ apiMethod?, apiUrl? }` | — | Endpoint untuk submit OTP |
| `submitFn` | `Function` | — | Custom submit function |

Events: `complete` (OTP penuh), `success`, `error`, `loading`

```vue
<template>
  <DCodeOtpInput
    v-model="otp"
    :total-input="6"
    label="Kode OTP"
    :endpoint="{ apiUrl: '/api/auth/verify-otp', apiMethod: 'POST' }"
    @complete="handleOtpComplete"
  />
</template>
```

---

### `DCodeFileField`

Upload file dengan validasi tipe & ukuran, preview, dan upload ke API.

| Prop | Tipe | Default | Deskripsi |
|------|------|---------|-----------|
| `modelValue` | `File\|null` | — | File terpilih (v-model) |
| `label` | `string` | `'Upload File'` | Label |
| `hint` | `string` | — | Teks petunjuk |
| `disabled` | `boolean` | `false` | Nonaktifkan |
| `previewHeight` | `number` | `190` | Tinggi preview (px) |
| `previewWidth` | `number` | — | Lebar preview (px) |
| `allowedTypes` | `string\|null` | `null` | Tipe file diizinkan (e.g. `'pdf,doc'`) |
| `accept` | `string` | — | Attribute accept HTML |
| `minSizeMb` | `number\|null` | `null` | Ukuran file minimum (MB) |
| `maxSizeMb` | `number` | `5` | Ukuran file maksimum (MB) |
| `endpoint` | `{ apiMethod?, apiUrl? }` | — | Endpoint upload |
| `uploadFn` | `Function` | — | Custom upload function |

```vue
<template>
  <DCodeFileField
    v-model="form.dokumen"
    label="Upload Dokumen"
    allowed-types="pdf,doc,docx"
    :max-size-mb="10"
    :endpoint="{ apiUrl: '/api/upload', apiMethod: 'POST' }"
    @uploaded="handleUploaded"
    @error="handleError"
  />
</template>
```

---

### `DCodeImageField`

Upload gambar dengan crop/resize dialog terintegrasi.

| Prop | Tipe | Default | Deskripsi |
|------|------|---------|-----------|
| `modelValue` | `File\|null` | — | File gambar (v-model) |
| `label` | `string` | `'Upload Gambar'` | Label |
| `hint` | `string` | `'Drag & drop atau klik area ini'` | Petunjuk |
| `disabled` | `boolean` | `false` | Nonaktifkan |
| `allowedTypes` | `string\|null` | `null` | Tipe gambar diizinkan |
| `accept` | `string` | `'image/*'` | Accept HTML |
| `minSizeMb` | `number\|null` | `null` | Min ukuran (MB) |
| `maxSizeMb` | `number` | `2` | Maks ukuran (MB) |
| `title` | `string` | `'Hasil Gambar'` | Judul preview |
| `description` | `string` | `''` | Deskripsi preview |
| `previewWidth` | `number` | `360` | Lebar preview |
| `previewHeight` | `number` | `220` | Tinggi preview |
| `aspectRatio` | `number\|null` | `null` | Rasio aspek crop |
| `outputWidth` | `number\|null` | `null` | Lebar output (px) |
| `outputHeight` | `number\|null` | `null` | Tinggi output (px) |
| `quality` | `number` | `0.92` | Kualitas output (0-1) |
| `autoOpenEditor` | `boolean` | `true` | Buka editor saat file dipilih |
| `minWidth` | `number\|null` | `null` | Min lebar gambar |
| `minHeight` | `number\|null` | `null` | Min tinggi gambar |
| `maxWidth` | `numbe