@skyworx/dynamic-ui 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,2 +1,2740 @@
1
- # PDG-DYNAMIC-BUILDER
2
- PDG-DYNAMIC-BUILDER
1
+ # @gemafajarramadhan/dynamic-ui
2
+
3
+ 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.
4
+
5
+ ---
6
+
7
+ ## Daftar Isi
8
+
9
+ - [Fitur](#fitur)
10
+ - [Instalasi](#instalasi)
11
+ - [⚡ Konfigurasi API (WAJIB)](#-konfigurasi-api-wajib)
12
+ - [Vue 3](#konfigurasi-api-di-vue-3)
13
+ - [Nuxt 3](#konfigurasi-api-di-nuxt-3)
14
+ - [React / Next.js](#konfigurasi-api-di-react--nextjs)
15
+ - [Angular](#konfigurasi-api-di-angular)
16
+ - [Vanilla JS (window global)](#konfigurasi-api-via-window-global)
17
+ - [Komponen Base (Vue Components)](#komponen-base-vue-components)
18
+ - [Daftar Komponen](#daftar-komponen)
19
+ - [Penggunaan di Vue 3 (Import Per Komponen)](#penggunaan-di-vue-3-import-per-komponen)
20
+ - [Penggunaan di Vue 3 (Global Plugin)](#penggunaan-di-vue-3-global-plugin)
21
+ - [Penggunaan di Nuxt 3](#penggunaan-di-nuxt-3-vue-components)
22
+ - [DCodeDataTable – Tabel Dinamis](#dcodedatatable--tabel-dinamis)
23
+ - [Props Utama](#props-utama)
24
+ - [Pola 1 — fetchHandler (MFE)](#pola-1--fetchhandler--disarankan-untuk-mfe)
25
+ - [Pola 2 — Static Data](#pola-2--static-data-consumer-fetch-di-luar)
26
+ - [Pola 3 — Realtime Socket](#pola-3--realtime-socket-consumer-kelola-socket)
27
+ - [Format Response](#format-response-yang-didukung)
28
+ - [Expose Methods](#expose-methods-via-ref)
29
+ - [DCodeAutoComplete – Komponen dengan API](#dcodeautocomplete--komponen-dengan-api)
30
+ - [Web Components (Framework Agnostic)](#web-components-framework-agnostic)
31
+ - [API Referensi Web Components](#api-referensi-web-components)
32
+ - [Logic Registry API](#logic-registry-api)
33
+ - [Penggunaan di Berbagai Framework](#penggunaan-di-berbagai-framework)
34
+ - [Vanilla HTML / JavaScript](#1-vanilla-html--javascript)
35
+ - [Vue 3 (Web Components)](#2-vue-3-web-components)
36
+ - [React](#3-react)
37
+ - [Next.js (App Router)](#4-nextjs-app-router)
38
+ - [Angular](#5-angular)
39
+ - [Nuxt 3 (Web Components)](#6-nuxt-3-web-components)
40
+ - [Custom Logic Registry](#custom-logic-registry)
41
+ - [TypeScript Support](#typescript-support)
42
+
43
+ ---
44
+
45
+ ## Fitur
46
+
47
+ - 🧩 **Web Components** – Dapat digunakan di framework **apapun** (React, Vue, Angular, Svelte, dll.) atau di HTML biasa.
48
+ - 📋 **AutoLayoutForm** – Generate form otomatis dari JSON schema dengan validasi dan konfigurasi fleksibel.
49
+ - 📊 **AutoLayoutDatatable** – Generate tabel dengan pagination, sorting, filtering, dan action button dari JSON.
50
+ - 🔌 **Logic Registry** – Daftarkan custom business logic dari aplikasi host tanpa perlu memodifikasi library.
51
+ - 🎨 **Style Bundled** – CSS sudah ter-bundle, cukup import satu file style.
52
+ - 🌐 **i18n Ready** – Mendukung internasionalisasi (vue-i18n).
53
+ - 🔐 **API Config** – Base URL dan auth token diambil dari **host project**, bukan dari env package ini.
54
+
55
+ ---
56
+
57
+ ## Instalasi
58
+
59
+ ```bash
60
+ # npm
61
+ npm install @gemafajarramadhan/dynamic-ui
62
+
63
+ # yarn
64
+ yarn add @gemafajarramadhan/dynamic-ui
65
+
66
+ # pnpm
67
+ pnpm add @gemafajarramadhan/dynamic-ui
68
+ ```
69
+
70
+ ---
71
+
72
+ ## ⚡ Konfigurasi API (WAJIB)
73
+
74
+ > **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.
75
+
76
+ Library menggunakan sistem konfigurasi terpusat via `setApiConfig()` (atau via Vue Plugin). Berikut urutan prioritas resolusi base URL:
77
+
78
+ | Prioritas | Sumber |
79
+ |-----------|--------|
80
+ | 1️⃣ Tertinggi | `apiBaseUrl` via plugin / `setApiConfig()` |
81
+ | 2️⃣ | `window.__MICRO_API_BASE_URL__` (global) |
82
+ | 3️⃣ | `import.meta.env.VITE_API_BASE_URL` (env host) |
83
+ | 4️⃣ Terendah | `''` (same-origin fallback) |
84
+
85
+ ---
86
+
87
+ ### Konfigurasi API di Vue 3
88
+
89
+ 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.
90
+
91
+ ```js
92
+ // main.js
93
+ import { createApp } from 'vue'
94
+ import App from './App.vue'
95
+ import DynamicUI, { setApiConfig } from '@gemafajarramadhan/dynamic-ui'
96
+ import '@gemafajarramadhan/dynamic-ui/style.css'
97
+
98
+ // Contoh import instance Axios / useApi dari host project Anda
99
+ import axiosInstance from './plugins/axios'
100
+ // import { useApi } from './composables/useApi'
101
+
102
+ const app = createApp(App)
103
+
104
+ // OPSI 1: Konfigurasi memanggil fungsi (Bisa dipanggil di luar instalasi plugin)
105
+ setApiConfig({
106
+ // WAJIB: Bypass fetch bawaan, gunakan axios dari project host
107
+ apiClient: async (url, method, data) => {
108
+ return axiosInstance({ url, method, data })
109
+ /* ATAU MENGGUNAKAN useApi bawaan project host
110
+ const { apiUrl } = useApi()
111
+ return apiUrl(url, method || 'GET', data, { disableGlobalLoading: true })
112
+ */
113
+ },
114
+
115
+ // Base URL API dari env HOST project
116
+ apiBaseUrl: import.meta.env.VITE_API_BASE_URL,
117
+
118
+ // (Opsional jika apiClient sudah memiliki interceptor token)
119
+ getAuthToken: () => localStorage.getItem('access_token'),
120
+ })
121
+
122
+ // OPSI 2: Lewatkan pada saat app.use (Berlaku jika tidak menggunakan setApiConfig di atas)
123
+ app.use(DynamicUI, {
124
+ apiClient: async (url, method, data) => axiosInstance({ url, method, data }),
125
+ apiBaseUrl: import.meta.env.VITE_API_BASE_URL,
126
+ })
127
+
128
+ app.mount('#app')
129
+ ```
130
+
131
+ > File `.env` di host project Anda:
132
+ > ```env
133
+ > VITE_API_BASE_URL=https://api.example.com
134
+ > ```
135
+
136
+ ---
137
+
138
+ ### Konfigurasi API di Nuxt 3
139
+
140
+ ```ts
141
+ // plugins/dynamic-ui.ts
142
+ import DynamicUI from '@gemafajarramadhan/dynamic-ui'
143
+ import '@gemafajarramadhan/dynamic-ui/style.css'
144
+
145
+ export default defineNuxtPlugin((nuxtApp) => {
146
+ const config = useRuntimeConfig()
147
+
148
+ nuxtApp.vueApp.use(DynamicUI, {
149
+ // Ambil dari runtimeConfig Nuxt
150
+ apiBaseUrl: config.public.apiBaseUrl,
151
+
152
+ getAuthToken: () => useCookie('access_token').value ?? null,
153
+
154
+ onUnauthorized: () => {
155
+ navigateTo('/login')
156
+ },
157
+ })
158
+ })
159
+ ```
160
+
161
+ > File `nuxt.config.ts` di host project:
162
+ > ```ts
163
+ > export default defineNuxtConfig({
164
+ > runtimeConfig: {
165
+ > public: {
166
+ > apiBaseUrl: process.env.VITE_API_BASE_URL, // set di .env
167
+ > },
168
+ > },
169
+ > })
170
+ > ```
171
+
172
+ ---
173
+
174
+ ### Konfigurasi API di React / Next.js
175
+
176
+ Gunakan fungsi `setApiConfig()` secara langsung (tidak perlu Vue plugin).
177
+
178
+ ```ts
179
+ // src/lib/dynamic-ui-config.ts (jalankan sebelum render app)
180
+ import { setApiConfig } from '@gemafajarramadhan/dynamic-ui'
181
+
182
+ setApiConfig({
183
+ apiBaseUrl: process.env.NEXT_PUBLIC_API_BASE_URL,
184
+
185
+ getAuthToken: () => localStorage.getItem('access_token'),
186
+
187
+ onUnauthorized: () => {
188
+ window.location.href = '/login'
189
+ },
190
+ })
191
+ ```
192
+
193
+ Panggil di entry point:
194
+
195
+ ```tsx
196
+ // app/layout.tsx atau _app.tsx
197
+ 'use client'
198
+ import '@/lib/dynamic-ui-config' // ← jalankan konfigurasi lebih dulu
199
+ import '@gemafajarramadhan/dynamic-ui/style.css'
200
+ ```
201
+
202
+ > File `.env.local` di host project Next.js:
203
+ > ```env
204
+ > NEXT_PUBLIC_API_BASE_URL=https://api.example.com
205
+ > ```
206
+
207
+ ---
208
+
209
+ ### Konfigurasi API di Angular
210
+
211
+ ```ts
212
+ // main.ts
213
+ import { setApiConfig } from '@gemafajarramadhan/dynamic-ui'
214
+ import '@gemafajarramadhan/dynamic-ui'
215
+ import '@gemafajarramadhan/dynamic-ui/style.css'
216
+
217
+ // Konfigurasi sebelum bootstrap
218
+ setApiConfig({
219
+ apiBaseUrl: (window as any).__env?.API_BASE_URL || 'https://api.example.com',
220
+
221
+ getAuthToken: () => localStorage.getItem('access_token'),
222
+
223
+ onUnauthorized: () => {
224
+ window.location.href = '/login'
225
+ },
226
+ })
227
+
228
+ platformBrowserDynamic().bootstrapModule(AppModule)
229
+ ```
230
+
231
+ ---
232
+
233
+ ### Konfigurasi API via Window Global
234
+
235
+ Alternatif tanpa import fungsi – set sebelum library dimuat:
236
+
237
+ ```html
238
+ <script>
239
+ // Set sebelum import library
240
+ window.__MICRO_API_BASE_URL__ = 'https://api.example.com'
241
+ window.__MICRO_AUTH_TOKEN__ = 'your-auth-token' // atau ambil dari cookie/localStorage
242
+ </script>
243
+ <script type="module" src="./main.js"></script>
244
+ ```
245
+
246
+ ---
247
+
248
+ ## Komponen Base (Vue Components)
249
+
250
+ 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.
251
+
252
+ ### Daftar Komponen
253
+
254
+ | Komponen | Deskripsi |
255
+ |---|---|
256
+ | `DCodeTextField` | Input text dengan validasi, text case, filter karakter |
257
+ | `DCodeTextarea` | Textarea dengan auto-resize dan validasi |
258
+ | `DCodeButton` | Tombol dengan variant, warna kustom, tooltip, dan animasi |
259
+ | `DCodeCard` | Kartu container dengan styling bawaan |
260
+ | `DCodeCheckbox` | Checkbox dengan label |
261
+ | `DCodeSwitch` | Toggle switch |
262
+ | `DCodeRadioCustom` | Radio button dengan opsi kustom |
263
+ | `DCodeLabel` | Label untuk form |
264
+ | `DCodeAutoComplete` | Input autocomplete dengan pencarian dan fetch API otomatis |
265
+ | `DCodeMultiSelect` | Dropdown multi-pilihan |
266
+ | `DCodeDatePicker` | Date picker kalender |
267
+ | `DCodeCurrencyField` | Input nilai mata uang |
268
+ | `DCodeOtpInput` | Input OTP digit-per-digit |
269
+ | `DCodeFileField` | Input file dengan preview |
270
+ | `DCodeImageField` | Input gambar dengan preview |
271
+ | `DCodeDropzone` | Drag-and-drop upload area |
272
+ | `DCodeUploadFile` | Upload file terintegrasi |
273
+ | `DCodeFileResult` | Tampilan hasil file yang diupload |
274
+ | `DCodeImageResult` | Tampilan hasil gambar yang diupload |
275
+ | `DCodeImageCropperDialog` | Dialog crop gambar |
276
+ | `DCodeDialog` | Modal dialog |
277
+ | `DCodeDialogCloseBtn` | Tombol tutup untuk dialog |
278
+ | `DCodeIconDropdown` | Dropdown ikon |
279
+ | `DCodeProgressBar` | Progress bar loading |
280
+ | `DCodeTimelineWithIcons` | Timeline dengan ikon |
281
+ | `DCodeWizard` | Wizard / stepper multi-langkah |
282
+
283
+ ---
284
+
285
+ ### Penggunaan di Vue 3 (Import Per Komponen)
286
+
287
+ Cara paling ringan: import hanya komponen yang diperlukan.
288
+
289
+ ```vue
290
+ <!-- MyForm.vue -->
291
+ <script setup>
292
+ import { DCodeTextField, DCodeButton } from '@gemafajarramadhan/dynamic-ui'
293
+ import '@gemafajarramadhan/dynamic-ui/style.css'
294
+ </script>
295
+
296
+ <template>
297
+ <form>
298
+ <DCodeTextField
299
+ v-model="form.name"
300
+ label="Nama Lengkap"
301
+ placeholder="Masukkan nama..."
302
+ :clearable="true"
303
+ />
304
+
305
+ <DCodeTextField
306
+ v-model="form.email"
307
+ label="Email"
308
+ valueType="email"
309
+ placeholder="nama@email.com"
310
+ />
311
+
312
+ <DCodeButton
313
+ text="Simpan"
314
+ variant="default"
315
+ bgColor="primary"
316
+ type="submit"
317
+ />
318
+ </form>
319
+ </template>
320
+
321
+ <script setup>
322
+ import { reactive } from 'vue'
323
+
324
+ const form = reactive({ name: '', email: '' })
325
+ </script>
326
+ ```
327
+
328
+ ---
329
+
330
+ ### Penggunaan di Vue 3 (Global Plugin)
331
+
332
+ Install semua komponen sekaligus agar bisa digunakan di mana saja tanpa import per file.
333
+
334
+ ```js
335
+ // main.js
336
+ import { createApp } from 'vue'
337
+ import App from './App.vue'
338
+ import DynamicUI from '@gemafajarramadhan/dynamic-ui'
339
+ import '@gemafajarramadhan/dynamic-ui/style.css'
340
+
341
+ const app = createApp(App)
342
+
343
+ app.use(DynamicUI, {
344
+ apiBaseUrl: import.meta.env.VITE_API_BASE_URL,
345
+ getAuthToken: () => localStorage.getItem('access_token'),
346
+ })
347
+
348
+ app.mount('#app')
349
+ ```
350
+
351
+ Setelah itu, semua komponen bisa digunakan **tanpa import** di setiap file:
352
+
353
+ ```vue
354
+ <!-- Tidak perlu import lagi! -->
355
+ <template>
356
+ <DCodeTextField v-model="name" label="Nama" />
357
+ <DCodeButton text="Submit" />
358
+ <DCodeDatePicker v-model="date" label="Tanggal" />
359
+ </template>
360
+ ```
361
+
362
+ ---
363
+
364
+ ### Penggunaan di Nuxt 3 (Vue Components)
365
+
366
+ Buat plugin Nuxt untuk mendaftarkan semua komponen secara global:
367
+
368
+ ```ts
369
+ // plugins/dynamic-ui.ts
370
+ import DynamicUI from '@gemafajarramadhan/dynamic-ui'
371
+ import '@gemafajarramadhan/dynamic-ui/style.css'
372
+
373
+ export default defineNuxtPlugin((nuxtApp) => {
374
+ const config = useRuntimeConfig()
375
+
376
+ nuxtApp.vueApp.use(DynamicUI, {
377
+ apiBaseUrl: config.public.apiBaseUrl,
378
+ getAuthToken: () => useCookie('access_token').value ?? null,
379
+ })
380
+ })
381
+ ```
382
+
383
+ Kemudian gunakan di page atau component manapun:
384
+
385
+ ```vue
386
+ <!-- pages/index.vue -->
387
+ <template>
388
+ <div>
389
+ <DCodeTextField v-model="search" label="Pencarian" :clearable="true" />
390
+ <DCodeButton text="Cari" variant="default" />
391
+ </div>
392
+ </template>
393
+ ```
394
+
395
+ ---
396
+
397
+ ## DCodeDataTable – Tabel Dinamis
398
+
399
+ `DCodeDataTable` adalah komponen tabel dinamis yang mendukung **pagination server-side**, **sorting**, **filtering**, **action buttons**, dan **badge kondisional** — seluruhnya dikonfigurasi via JSON schema.
400
+
401
+ Komponen ini dirancang sebagai **micro-frontend**: consumer bertanggung jawab penuh atas HTTP client, auth token, dan sistem permission.
402
+
403
+ ---
404
+
405
+ ### Props Utama
406
+
407
+ | Prop | Tipe | Default | Deskripsi |
408
+ |------|------|---------|-----------|
409
+ | `table-config` | `Object` | — | Config JSON (kolom, filter, action, pagination) |
410
+ | `fetch-handler` | `(params) => Promise<any>` | — | **[MFE]** Consumer kontrol penuh fetch. Component tidak hit endpoint sama sekali. |
411
+ | `http-client` | `({ url, method, data }) => Promise<any>` | — | **[MFE]** Axios instance consumer untuk action tombol row/header (DELETE, PATCH, dll.) |
412
+ | `action-access-checker` | `(path, action) => boolean` | `() => true` | **[MFE]** Fungsi RBAC dari consumer. Default: semua action allow. |
413
+ | `ignore-permissions` | `boolean` | `false` | Matikan filter permission sepenuhnya |
414
+ | `data` | `any[]` | `[]` | Data statis (tanpa API); pakai jika consumer sudah fetch di luar |
415
+ | `loading` | `boolean` | `false` | Loading state untuk mode data statis |
416
+ | `columns` | `TableColumn[]` | `[]` | Kolom manual (tanpa table-config) |
417
+ | `per-page` | `number` | `10` | Jumlah baris per halaman awal |
418
+ | `show-pagination` | `boolean` | `true` | Tampilkan pagination |
419
+ | `has-actions` | `boolean` | `false` | Tampilkan kolom aksi (untuk mode kolom manual) |
420
+ | `api-url` | `string` | — | URL endpoint langsung (tanpa table-config) |
421
+ | `api-method` | `string` | `'POST'` | HTTP method untuk apiUrl |
422
+ | `api-params` | `object` | `{}` | Parameter tetap yang selalu disertakan di setiap request |
423
+
424
+ ### Events
425
+
426
+ | Event | Payload | Deskripsi |
427
+ |-------|---------|-----------|
428
+ | `@action` | `{ action, row, selection? }` | Dipanggil saat tombol action diklik |
429
+ | `@update:selection` | `any[]` | Dipanggil saat checkbox selection berubah |
430
+ | `@update:page` | `number` | Dipanggil saat halaman berganti |
431
+
432
+ ---
433
+
434
+ ### Pola 1 — `fetchHandler` ⭐ Disarankan untuk MFE
435
+
436
+ Consumer kontrol penuh HTTP. Component tidak hit endpoint sama sekali — cocok untuk menghindari error 401.
437
+
438
+ ```vue
439
+ <template>
440
+ <DCodeDataTable
441
+ :table-config="tableConfig"
442
+ :fetch-handler="fetchUsers"
443
+ :http-client="httpClient"
444
+ :action-access-checker="checkPermission"
445
+ @action="handleAction"
446
+ />
447
+ </template>
448
+
449
+ <script setup lang="ts">
450
+ import { DCodeDataTable } from '@gemafajarramadhan/dynamic-ui'
451
+ import axios from '@/lib/axios' // axios instance consumer — interceptor token sudah terpasang
452
+
453
+ // 1. fetchHandler: dipanggil tiap page/filter/sort/search change
454
+ // params: { page, perPage, keyword?, ...filters }
455
+ const fetchUsers = async (params: Record<string, any>) => {
456
+ const { data } = await axios.post('/api/users/list', params)
457
+ return data
458
+ }
459
+
460
+ // 2. httpClient: untuk call dari tombol action row (delete, toggle, dll.)
461
+ const httpClient = async ({ url, method, data }: any) => {
462
+ const res = await axios({ url, method, data })
463
+ return res.data
464
+ }
465
+
466
+ // 3. Permission checker dari sistem RBAC consumer
467
+ const checkPermission = (path: string, action: string): boolean => {
468
+ return useAuthStore().can(`${path}:${action}`)
469
+ }
470
+
471
+ // 4. Handle action event dari tabel
472
+ const handleAction = ({ action, row, selection }: any) => {
473
+ if (action === 'edit') router.push(`/users/${row.id}/edit`)
474
+ if (action === 'delete') deleteUser(row.id)
475
+ if (action === 'export') exportSelected(selection)
476
+ }
477
+
478
+ // 5. Konfigurasi tabel via JSON schema
479
+ const tableConfig = {
480
+ headers: [
481
+ { key: 'no', labelID: 'No', labelEN: 'No', align: 'CENTER' },
482
+ { key: 'name', labelID: 'Nama', labelEN: 'Name', align: 'LEFT' },
483
+ { key: 'email', labelID: 'Email', labelEN: 'Email', align: 'LEFT' },
484
+ {
485
+ key: 'status',
486
+ labelID: 'Status', labelEN: 'Status',
487
+ custom: {
488
+ type: 'BADGE',
489
+ conditionBadge: [
490
+ { value: 'active', labelID: 'Aktif', labelEN: 'Active', color: 'success' },
491
+ { value: 'inactive', labelID: 'Nonaktif', labelEN: 'Inactive', color: 'danger' },
492
+ ],
493
+ },
494
+ },
495
+ {
496
+ key: 'action',
497
+ labelID: 'Aksi', labelEN: 'Actions',
498
+ custom: {
499
+ type: 'ACTION',
500
+ actionCustom: [
501
+ {
502
+ permission: 'EDIT',
503
+ propsActionCustom: {
504
+ labelID: 'Edit', labelEN: 'Edit',
505
+ icon: 'SquarePen', actionType: 'edit', bgColor: 'primary',
506
+ },
507
+ },
508
+ {
509
+ permission: 'DELETE',
510
+ requireConfirm: true,
511
+ propCustomMessage: {
512
+ titleID: 'Hapus Data', titleEN: 'Delete',
513
+ messageID: 'Yakin ingin menghapus?', messageEN: 'Are you sure?',
514
+ },
515
+ propsActionCustom: {
516
+ labelID: 'Hapus', labelEN: 'Delete',
517
+ icon: 'Trash2', actionType: 'delete', bgColor: 'red',
518
+ },
519
+ },
520
+ ],
521
+ },
522
+ },
523
+ ],
524
+ filters: [
525
+ {
526
+ seq: 1, model: 'name', labelID: 'Nama', labelEN: 'Name', type: 'TEXT',
527
+ },
528
+ {
529
+ seq: 2, model: 'status', labelID: 'Status', labelEN: 'Status', type: 'SELECT',
530
+ propsFilters: {
531
+ items: [
532
+ { value: 'active', label: 'Active' },
533
+ { value: 'inactive', label: 'Inactive' },
534
+ ],
535
+ },
536
+ },
537
+ ],
538
+ actionGlobal: [
539
+ {
540
+ permission: 'CREATE',
541
+ propsActionGlobal: {
542
+ labelID: 'Tambah', labelEN: 'Add New',
543
+ icon: 'Plus', variant: 'default',
544
+ route: '/users/create',
545
+ },
546
+ },
547
+ ],
548
+ pagination: true,
549
+ }
550
+ </script>
551
+ ```
552
+
553
+ ---
554
+
555
+ ### Pola 2 — Static Data (consumer fetch di luar)
556
+
557
+ Pakai ketika consumer sudah fetch data sendiri dan ingin sekadar menampilkan tabel.
558
+
559
+ ```vue
560
+ <template>
561
+ <DCodeDataTable
562
+ :columns="columns"
563
+ :data="users"
564
+ :loading="isLoading"
565
+ has-actions
566
+ ignore-permissions
567
+ @action="handleAction"
568
+ />
569
+ </template>
570
+
571
+ <script setup lang="ts">
572
+ import { ref, onMounted } from 'vue'
573
+ import axios from '@/lib/axios'
574
+
575
+ const users = ref([])
576
+ const isLoading = ref(false)
577
+
578
+ onMounted(async () => {
579
+ isLoading.value = true
580
+ const { data } = await axios.get('/api/users')
581
+ users.value = data.data
582
+ isLoading.value = false
583
+ })
584
+
585
+ const columns = [
586
+ { key: 'no', label: 'No', align: 'center' },
587
+ { key: 'name', label: 'Nama', align: 'left' },
588
+ { key: 'email', label: 'Email', align: 'left' },
589
+ ]
590
+ </script>
591
+ ```
592
+
593
+ ---
594
+
595
+ ### Pola 3 — Realtime Socket (consumer kelola socket)
596
+
597
+ Consumer connect ke WebSocket sendiri, push data ke tabel melalui `:data`.
598
+
599
+ ```vue
600
+ <template>
601
+ <DCodeDataTable :columns="columns" :data="realtimeData" ignore-permissions />
602
+ </template>
603
+
604
+ <script setup lang="ts">
605
+ import { ref, onMounted, onUnmounted } from 'vue'
606
+ import { io } from 'socket.io-client'
607
+
608
+ const realtimeData = ref([])
609
+ let socket: any = null
610
+
611
+ onMounted(() => {
612
+ socket = io('wss://your-server.com', {
613
+ auth: { token: localStorage.getItem('token') }
614
+ })
615
+ socket.emit('dashboard:list', { page: 1, perPage: 10 })
616
+ socket.on('dashboard:result', (res: any) => {
617
+ realtimeData.value = res.data ?? res
618
+ })
619
+ })
620
+
621
+ onUnmounted(() => socket?.disconnect())
622
+ </script>
623
+ ```
624
+
625
+ ---
626
+
627
+ ### Format Response yang Didukung
628
+
629
+ Component secara otomatis mendeteksi berbagai format response API:
630
+
631
+ ```json
632
+ { "data": [], "totalData": 100 } ✅ (disarankan)
633
+ { "data": [], "maxPage": 10 } ✅
634
+ { "items": [], "total": 100 } ✅
635
+ { "list": [], "total": 100 } ✅
636
+ { "rows": [], "count": 100 } ✅
637
+ { "result": [], "total": 100 } ✅
638
+ [] ✅ (array langsung)
639
+ ```
640
+
641
+ ---
642
+
643
+ ### Expose Methods (via `ref`)
644
+
645
+ ```typescript
646
+ const tableRef = ref()
647
+
648
+ tableRef.value.fetchData() // Refresh data (panggil ulang fetchHandler)
649
+ tableRef.value.handleRefresh() // Reset filter & refresh
650
+ tableRef.value.resetFilters() // Reset nilai filter saja (tanpa fetch)
651
+ // Reactive nilai filter saat ini
652
+ ```
653
+
654
+ ```vue
655
+ <DCodeDataTable ref="tableRef" :fetch-handler="fetchUsers" ... />
656
+ ```
657
+
658
+ ---
659
+
660
+ ## DCodeAutoComplete – Komponen dengan API
661
+
662
+ `DCodeAutoComplete` adalah komponen autocomplete yang bisa **langsung fetch data dari API** menggunakan konfigurasi yang sudah di-set di host project.
663
+
664
+ > ⚠️ Pastikan sudah mengatur [Konfigurasi API](#-konfigurasi-api-wajib) sebelum menggunakan fitur endpoint.
665
+
666
+ ### Perilaku Default
667
+
668
+ - Setiap fetch otomatis mengirimkan `page=1` dan `perPage=100` sebagai query parameter default.
669
+ - Nilai ini bisa di-override via prop `apiParams`.
670
+
671
+ ### Props Utama
672
+
673
+ | Prop | Tipe | Default | Deskripsi |
674
+ |------|------|---------|-----------|
675
+ | `modelValue` | `any` | — | Value terpilih (v-model) |
676
+ | `endpoint.apiUrl` | `string` | — | URL endpoint API (path atau full URL) |
677
+ | `endpoint.apiMethod` | `string` | `'GET'` | HTTP method |
678
+ | `apiParams` | `object` | `{}` | Query params tambahan (di-merge & bisa override default) |
679
+ | `itemTitle` | `string\|Function` | `'name'` | Field nama tampilan dari tiap item |
680
+ | `itemValue` | `string\|Function` | `'id'` | Field nilai dari tiap item |
681
+ | `returnObject` | `boolean` | `false` | Jika `true`, v-model menyimpan objek penuh, bukan hanya `id` |
682
+ | `clearable` | `boolean` | `false` | Tampilkan tombol hapus pilihan |
683
+ | `disabled` | `boolean` | `false` | Nonaktifkan komponen |
684
+ | `autoFetchOnOpen` | `boolean` | `true` | Fetch otomatis saat dropdown dibuka pertama kali |
685
+ | `autoFetchOnMount` | `boolean` | `false` | Fetch otomatis saat komponen pertama kali di-mount |
686
+
687
+ ### Contoh Penggunaan
688
+
689
+ #### Fetch dari API dengan URL langsung
690
+
691
+ ```vue
692
+ <template>
693
+ <DCodeAutoComplete
694
+ v-model="selectedId"
695
+ label="Pilih Departemen"
696
+ :endpoint="{ apiUrl: '/api/master/departemen', apiMethod: 'GET' }"
697
+ item-title="nama"
698
+ item-value="id"
699
+ clearable
700
+ />
701
+ </template>
702
+ ```
703
+
704
+ > Request yang dikirim: `GET /api/master/departemen?page=1&perPage=100`
705
+ > Base URL diambil otomatis dari konfigurasi host project (`apiBaseUrl`).
706
+
707
+ #### Dengan Parameter Tambahan
708
+
709
+ ```vue
710
+ <template>
711
+ <DCodeAutoComplete
712
+ v-model="selectedId"
713
+ label="Pilih Jabatan"
714
+ :endpoint="{ apiUrl: '/api/master/jabatan' }"
715
+ :apiParams="{ idDepartemen: selectedDeptId, isActive: true }"
716
+ item-title="namaJabatan"
717
+ item-value="idJabatan"
718
+ />
719
+ </template>
720
+ ```
721
+
722
+ > Request yang dikirim: `GET /api/master/jabatan?page=1&perPage=100&idDepartemen=5&isActive=true`
723
+
724
+ #### Override Default Pagination
725
+
726
+ ```vue
727
+ <template>
728
+ <!-- Ganti perPage menjadi 50 -->
729
+ <DCodeAutoComplete
730
+ v-model="selected"
731
+ label="Pilih Data"
732
+ :endpoint="{ apiUrl: '/api/data' }"
733
+ :apiParams="{ perPage: 50 }"
734
+ item-title="nama"
735
+ item-value="id"
736
+ />
737
+ </template>
738
+ ```
739
+
740
+ #### Dengan Data Statis (tanpa API)
741
+
742
+ ```vue
743
+ <template>
744
+ <DCodeAutoComplete
745
+ v-model="selected"
746
+ label="Pilih Status"
747
+ :items="[
748
+ { id: 1, name: 'Aktif' },
749
+ { id: 0, name: 'Nonaktif' },
750
+ ]"
751
+ item-title="name"
752
+ item-value="id"
753
+ />
754
+ </template>
755
+ ```
756
+
757
+ #### Return Object Penuh
758
+
759
+ ```vue
760
+ <script setup>
761
+ import { ref } from 'vue'
762
+
763
+ const selectedUser = ref(null) // akan berisi { id: 1, name: 'John', email: '...' }
764
+ </script>
765
+
766
+ <template>
767
+ <DCodeAutoComplete
768
+ v-model="selectedUser"
769
+ label="Pilih User"
770
+ :endpoint="{ apiUrl: '/api/users' }"
771
+ item-title="name"
772
+ item-value="id"
773
+ return-object
774
+ />
775
+ </template>
776
+ ```
777
+
778
+ ### Format Response API yang Diharapkan
779
+
780
+ ```json
781
+ {
782
+ "status": true,
783
+ "data": {
784
+ "data": [
785
+ { "id": 1, "name": "Item A" },
786
+ { "id": 2, "name": "Item B" }
787
+ ]
788
+ }
789
+ }
790
+ ```
791
+
792
+ Atau format flat:
793
+
794
+ ```json
795
+ {
796
+ "data": [
797
+ { "id": 1, "name": "Item A" }
798
+ ]
799
+ }
800
+ ```
801
+
802
+ Atau array langsung:
803
+
804
+ ```json
805
+ [
806
+ { "id": 1, "name": "Item A" }
807
+ ]
808
+ ```
809
+
810
+ ---
811
+
812
+ ## Web Components (Framework Agnostic)
813
+
814
+ Untuk digunakan di **React, Angular, Svelte**, atau framework lainnya, gunakan mode Web Components.
815
+
816
+ ---
817
+
818
+ ## API Referensi Web Components
819
+
820
+ ### Web Components
821
+
822
+ Setelah library di-import, dua custom element berikut akan terdaftar secara global:
823
+
824
+ | Custom Element | Deskripsi |
825
+ | ------------------------- | ----------------------------------------------------------- |
826
+ | `<micro-dynamic-form>` | Render form dinamis berdasarkan JSON config `config` prop. |
827
+ | `<micro-dynamic-datatable>` | Render tabel dinamis berdasarkan JSON config `config` prop. |
828
+ | `<micro-data-table>` | Render Data Table base component secara independen. Menerima props seperti `columns`, `data`, `apiUrl`. |
829
+
830
+ #### Properties (set via JavaScript/DOM)
831
+
832
+ Karena Web Components menerima objek JavaScript (bukan string HTML attribute), set properti ini **secara imperatif via DOM** atau `ref`:
833
+
834
+ | Property | Tipe | Deskripsi |
835
+ | --------- | -------- | --------------------------------------- |
836
+ | `config` | `Object` | Konfigurasi form / tabel dalam bentuk JSON. |
837
+
838
+ #### Events
839
+
840
+ | Event | `event.detail` | Deskripsi |
841
+ | -------- | ------------------ | ------------------------------------------- |
842
+ | `submit` | `Object` (payload) | Dipanggil saat form di-submit. |
843
+ | `action` | `Object` (payload) | Dipanggil saat action button datatable diklik. |
844
+
845
+ ---
846
+
847
+ ### Logic Registry API
848
+
849
+ Digunakan untuk menyuntikkan business logic dari aplikasi host ke dalam komponen.
850
+
851
+ ```js
852
+ import {
853
+ registerLogic,
854
+ unregisterLogic,
855
+ getLogicModule
856
+ } from '@gemafajarramadhan/dynamic-ui'
857
+ ```
858
+
859
+ | Fungsi | Deskripsi |
860
+ | ----------------------------- | ----------------------------------------------------------- |
861
+ | `registerLogic(code, module)` | Daftarkan logic module untuk layout code tertentu. |
862
+ | `unregisterLogic(code)` | Hapus logic module dari registry. |
863
+ | `getLogicModule(code)` | Ambil logic module berdasarkan layout code. |
864
+
865
+ ---
866
+
867
+ ## Penggunaan di Berbagai Framework
868
+
869
+ ---
870
+
871
+ ### 1. Vanilla HTML / JavaScript
872
+
873
+ Cara paling sederhana. Import library dari bundler, lalu gunakan custom element di HTML.
874
+
875
+ ```html
876
+ <!DOCTYPE html>
877
+ <html lang="en">
878
+ <head>
879
+ <meta charset="UTF-8" />
880
+ <title>Dynamic UI</title>
881
+ <!-- Import style -->
882
+ <link rel="stylesheet" href="./node_modules/@gemafajarramadhan/dynamic-ui/dist/style.css" />
883
+ </head>
884
+ <body>
885
+
886
+ <!-- Dynamic Form -->
887
+ <micro-dynamic-form id="myForm"></micro-dynamic-form>
888
+
889
+ <!-- Dynamic DataTable (AutoLayout version) -->
890
+ <micro-dynamic-datatable id="myAutoTable"></micro-dynamic-datatable>
891
+
892
+ <!-- Base DataTable (Stand-alone version) -->
893
+ <micro-data-table id="myBaseTable"></micro-data-table>
894
+
895
+ <!-- Import library (registrasi Web Components otomatis) -->
896
+ <script type="module">
897
+ import '@gemafajarramadhan/dynamic-ui'
898
+ import { setApiConfig } from '@gemafajarramadhan/dynamic-ui'
899
+
900
+ // ✅ Konfigurasi API dari host project
901
+ setApiConfig({
902
+ apiBaseUrl: 'https://api.example.com',
903
+ getAuthToken: () => localStorage.getItem('access_token'),
904
+ })
905
+
906
+ // --- Setup Form ---
907
+ const form = document.querySelector('#myForm')
908
+ form.config = {
909
+ title: 'Registrasi User',
910
+ sections: [
911
+ {
912
+ title: 'Info Pribadi',
913
+ fields: [
914
+ {
915
+ key: 'name',
916
+ label: 'Nama Lengkap',
917
+ model: 'fullName',
918
+ component: 'DCodeTextField',
919
+ },
920
+ {
921
+ key: 'email',
922
+ label: 'Email',
923
+ model: 'email',
924
+ component: 'DCodeTextField',
925
+ props: { type: 'email' },
926
+ },
927
+ ],
928
+ },
929
+ ],
930
+ actions: [{ key: 'submit', label: 'Daftar', component: 'DCodeButton' }],
931
+ }
932
+
933
+ form.addEventListener('submit', (e) => {
934
+
935
+ })
936
+
937
+ // --- Setup DataTable ---
938
+ const autoTable = document.querySelector('#myAutoTable')
939
+ autoTable.config = {
940
+ titleID: 'Daftar User',
941
+ headers: [
942
+ { text: 'Nama', value: 'name' },
943
+ { text: 'Email', value: 'email' },
944
+ ],
945
+ }
946
+
947
+ autoTable.addEventListener('action', (e) => {
948
+
949
+ })
950
+
951
+ // --- Setup Base MicroDataTable ---
952
+ const baseTable = document.querySelector('#myBaseTable')
953
+ // Untuk MicroDataTable (base component), Anda bisa mengirimkan prop langsung:
954
+ baseTable.columns = [
955
+ { key: 'name', label: 'Nama', sortable: true },
956
+ { key: 'email', label: 'Email' }
957
+ ]
958
+ // Memberikan data secara statis
959
+ baseTable.data = [
960
+ { name: 'John Doe', email: 'john@example.com' },
961
+ { name: 'Jane Smith', email: 'jane@example.com' }
962
+ ]
963
+
964
+ // Atau bisa diset melalui API:
965
+ // baseTable.apiUrl = '/api/users'
966
+ // baseTable.apiMethod = 'GET'
967
+ </script>
968
+
969
+ </body>
970
+ </html>
971
+ ```
972
+
973
+ ---
974
+
975
+ ### 2. Vue 3 (Web Components)
976
+
977
+ Di Vue 3, Web Components bisa dipakai langsung seperti HTML element biasa. Gunakan `ref` untuk set properti objek.
978
+
979
+ #### Konfigurasi Vite
980
+
981
+ Agar Vue tidak menganggap `micro-dynamic-*` sebagai Vue component yang belum terdaftar, tambahkan `isCustomElement` di `vite.config.js`:
982
+
983
+ ```js
984
+ // vite.config.js
985
+ import { defineConfig } from 'vite'
986
+ import vue from '@vitejs/plugin-vue'
987
+
988
+ export default defineConfig({
989
+ plugins: [
990
+ vue({
991
+ template: {
992
+ compilerOptions: {
993
+ // Semua element yang dimulai dengan 'micro-' dianggap custom element
994
+ isCustomElement: (tag) => tag.startsWith('micro-'),
995
+ },
996
+ },
997
+ }),
998
+ ],
999
+ })
1000
+ ```
1001
+
1002
+ #### Entry Point (`main.js`)
1003
+
1004
+ ```js
1005
+ // main.js
1006
+ import { createApp } from 'vue'
1007
+ import App from './App.vue'
1008
+ import DynamicUI from '@gemafajarramadhan/dynamic-ui'
1009
+ import '@gemafajarramadhan/dynamic-ui/style.css'
1010
+
1011
+ const app = createApp(App)
1012
+
1013
+ // ✅ Konfigurasi API dari env host project
1014
+ app.use(DynamicUI, {
1015
+ apiBaseUrl: import.meta.env.VITE_API_BASE_URL,
1016
+ getAuthToken: () => localStorage.getItem('access_token'),
1017
+ onUnauthorized: () => router.push('/login'),
1018
+ })
1019
+
1020
+ app.mount('#app')
1021
+ ```
1022
+
1023
+ #### Komponen Vue (`MyForm.vue`)
1024
+
1025
+ ```vue
1026
+ <script setup>
1027
+ import { ref, onMounted } from 'vue'
1028
+
1029
+ const formRef = ref(null)
1030
+
1031
+ const formConfig = {
1032
+ title: 'Form User',
1033
+ sections: [
1034
+ {
1035
+ fields: [
1036
+ {
1037
+ key: 'username',
1038
+ label: 'Username',
1039
+ model: 'username',
1040
+ component: 'DCodeTextField',
1041
+ },
1042
+ ],
1043
+ },
1044
+ ],
1045
+ actions: [{ key: 'save', label: 'Simpan', component: 'DCodeButton' }],
1046
+ }
1047
+
1048
+ onMounted(() => {
1049
+ if (formRef.value) {
1050
+ // Set config via DOM property (bukan attribute)
1051
+ formRef.value.config = formConfig
1052
+
1053
+ formRef.value.addEventListener('submit', (e) => {
1054
+
1055
+ })
1056
+ }
1057
+ })
1058
+ </script>
1059
+
1060
+ <template>
1061
+ <micro-dynamic-form ref="formRef" />
1062
+ </template>
1063
+ ```
1064
+
1065
+ ---
1066
+
1067
+ ### 3. React
1068
+
1069
+ Di React, gunakan `useRef` dan `useEffect` untuk set properti DOM secara imperatif karena React tidak meneruskan objek langsung ke Web Component attributes.
1070
+
1071
+ #### Entry Point (`main.jsx`)
1072
+
1073
+ ```jsx
1074
+ // main.jsx
1075
+ import React from 'react'
1076
+ import ReactDOM from 'react-dom/client'
1077
+ import App from './App'
1078
+ import { setApiConfig } from '@gemafajarramadhan/dynamic-ui'
1079
+ import '@gemafajarramadhan/dynamic-ui'
1080
+ import '@gemafajarramadhan/dynamic-ui/style.css'
1081
+
1082
+ // ✅ Konfigurasi API dari env host project
1083
+ setApiConfig({
1084
+ apiBaseUrl: import.meta.env.VITE_API_BASE_URL,
1085
+ getAuthToken: () => localStorage.getItem('access_token'),
1086
+ onUnauthorized: () => { window.location.href = '/login' },
1087
+ })
1088
+
1089
+ ReactDOM.createRoot(document.getElementById('root')).render(<App />)
1090
+ ```
1091
+
1092
+ #### Dynamic Form Component
1093
+
1094
+ ```jsx
1095
+ // components/DynamicForm.jsx
1096
+ import React, { useEffect, useRef } from 'react'
1097
+
1098
+ const formConfig = {
1099
+ title: 'Form via React',
1100
+ sections: [
1101
+ {
1102
+ fields: [
1103
+ {
1104
+ key: 'username',
1105
+ label: 'Username',
1106
+ model: 'username',
1107
+ component: 'DCodeTextField',
1108
+ },
1109
+ {
1110
+ key: 'email',
1111
+ label: 'Email',
1112
+ model: 'email',
1113
+ component: 'DCodeTextField',
1114
+ props: { type: 'email' },
1115
+ },
1116
+ ],
1117
+ },
1118
+ ],
1119
+ actions: [{ key: 'submit', label: 'Kirim', component: 'DCodeButton' }],
1120
+ }
1121
+
1122
+ export default function DynamicForm() {
1123
+ const formRef = useRef(null)
1124
+
1125
+ useEffect(() => {
1126
+ const el = formRef.current
1127
+ if (!el) return
1128
+
1129
+ // Set config sebagai DOM property
1130
+ el.config = formConfig
1131
+
1132
+ const handleSubmit = (event) => {
1133
+
1134
+ }
1135
+
1136
+ el.addEventListener('submit', handleSubmit)
1137
+
1138
+ // Cleanup listener saat unmount
1139
+ return () => {
1140
+ el.removeEventListener('submit', handleSubmit)
1141
+ }
1142
+ }, [])
1143
+
1144
+ return <micro-dynamic-form ref={formRef}></micro-dynamic-form>
1145
+ }
1146
+ ```
1147
+
1148
+ #### Dynamic DataTable Component
1149
+
1150
+ ```jsx
1151
+ // components/DynamicTable.jsx
1152
+ import React, { useEffect, useRef } from 'react'
1153
+
1154
+ const tableConfig = {
1155
+ titleID: 'Daftar User',
1156
+ headers: [
1157
+ { text: 'Nama', value: 'name' },
1158
+ { text: 'Email', value: 'email' },
1159
+ { text: 'Status', value: 'status' },
1160
+ ],
1161
+ }
1162
+
1163
+ export default function DynamicTable() {
1164
+ const tableRef = useRef(null)
1165
+
1166
+ useEffect(() => {
1167
+ const el = tableRef.current
1168
+ if (!el) return
1169
+
1170
+ el.config = tableConfig
1171
+
1172
+ const handleAction = (event) => {
1173
+
1174
+ }
1175
+
1176
+ el.addEventListener('action', handleAction)
1177
+
1178
+ return () => {
1179
+ el.removeEventListener('action', handleAction)
1180
+ }
1181
+ }, [])
1182
+
1183
+ return <micro-dynamic-datatable ref={tableRef}></micro-dynamic-datatable>
1184
+ }
1185
+ ```
1186
+
1187
+ #### TypeScript + React
1188
+
1189
+ Tambahkan deklarasi tipe agar JSX tidak mengeluh tentang custom element:
1190
+
1191
+ ```ts
1192
+ // src/types/dynamic-ui.d.ts
1193
+ import React from 'react'
1194
+
1195
+ declare global {
1196
+ namespace JSX {
1197
+ interface IntrinsicElements {
1198
+ 'micro-dynamic-form': React.DetailedHTMLProps<
1199
+ React.HTMLAttributes<HTMLElement>,
1200
+ HTMLElement
1201
+ >
1202
+ 'micro-dynamic-datatable': React.DetailedHTMLProps<
1203
+ React.HTMLAttributes<HTMLElement>,
1204
+ HTMLElement
1205
+ >
1206
+ }
1207
+ }
1208
+ }
1209
+ ```
1210
+
1211
+ ---
1212
+
1213
+ ### 4. Next.js (App Router)
1214
+
1215
+ Di Next.js dengan App Router, Web Components harus di-import di sisi klien (`'use client'`).
1216
+
1217
+ #### Setup Konfigurasi API
1218
+
1219
+ ```ts
1220
+ // lib/dynamic-ui-config.ts
1221
+ import { setApiConfig } from '@gemafajarramadhan/dynamic-ui'
1222
+
1223
+ setApiConfig({
1224
+ apiBaseUrl: process.env.NEXT_PUBLIC_API_BASE_URL!,
1225
+ getAuthToken: () => localStorage.getItem('access_token'),
1226
+ onUnauthorized: () => { window.location.href = '/login' },
1227
+ })
1228
+ ```
1229
+
1230
+ #### Setup di Layout atau Client Component
1231
+
1232
+ ```tsx
1233
+ // components/DynamicForm.tsx
1234
+ 'use client'
1235
+
1236
+ import { useEffect, useRef } from 'react'
1237
+ import '@/lib/dynamic-ui-config'
1238
+ import('@gemafajarramadhan/dynamic-ui')
1239
+ import '@gemafajarramadhan/dynamic-ui/style.css'
1240
+
1241
+ const formConfig = {
1242
+ title: 'Form Next.js',
1243
+ sections: [
1244
+ {
1245
+ fields: [
1246
+ {
1247
+ key: 'name',
1248
+ label: 'Nama',
1249
+ model: 'name',
1250
+ component: 'DCodeTextField',
1251
+ },
1252
+ ],
1253
+ },
1254
+ ],
1255
+ actions: [{ key: 'submit', label: 'Submit', component: 'DCodeButton' }],
1256
+ }
1257
+
1258
+ export default function DynamicFormClient() {
1259
+ const formRef = useRef<HTMLElement>(null)
1260
+
1261
+ useEffect(() => {
1262
+ const el = formRef.current
1263
+ if (!el) return
1264
+
1265
+ ;(el as any).config = formConfig
1266
+
1267
+ const handleSubmit = (event: Event) => {
1268
+ const customEvent = event as CustomEvent
1269
+
1270
+ }
1271
+
1272
+ el.addEventListener('submit', handleSubmit)
1273
+ return () => el.removeEventListener('submit', handleSubmit)
1274
+ }, [])
1275
+
1276
+ return (
1277
+ // @ts-ignore – custom element tidak dikenal TypeScript secara default
1278
+ <micro-dynamic-form ref={formRef}></micro-dynamic-form>
1279
+ )
1280
+ }
1281
+ ```
1282
+
1283
+ #### Penggunaan di Page
1284
+
1285
+ ```tsx
1286
+ // app/page.tsx
1287
+ import DynamicFormClient from '@/components/DynamicForm'
1288
+
1289
+ export default function HomePage() {
1290
+ return (
1291
+ <main>
1292
+ <h1>Dynamic Form</h1>
1293
+ <DynamicFormClient />
1294
+ </main>
1295
+ )
1296
+ }
1297
+ ```
1298
+
1299
+ > **Catatan:** Gunakan `dynamic()` dari `next/dynamic` dengan `{ ssr: false }` jika terjadi error SSR.
1300
+ >
1301
+ > ```tsx
1302
+ > import dynamic from 'next/dynamic'
1303
+ > const DynamicFormClient = dynamic(() => import('@/components/DynamicForm'), { ssr: false })
1304
+ > ```
1305
+
1306
+ > File `.env.local` di host project:
1307
+ > ```env
1308
+ > NEXT_PUBLIC_API_BASE_URL=https://api.example.com
1309
+ > ```
1310
+
1311
+ ---
1312
+
1313
+ ### 5. Angular
1314
+
1315
+ Di Angular, tambahkan `CUSTOM_ELEMENTS_SCHEMA` agar Angular tidak melempar error pada custom element yang tidak dikenali.
1316
+
1317
+ #### `app.module.ts` (Module-based)
1318
+
1319
+ ```ts
1320
+ // app.module.ts
1321
+ import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
1322
+ import { BrowserModule } from '@angular/platform-browser'
1323
+ import { AppComponent } from './app.component'
1324
+
1325
+ @NgModule({
1326
+ declarations: [AppComponent],
1327
+ imports: [BrowserModule],
1328
+ schemas: [CUSTOM_ELEMENTS_SCHEMA], // <-- Wajib ditambahkan
1329
+ bootstrap: [AppComponent],
1330
+ })
1331
+ export class AppModule {}
1332
+ ```
1333
+
1334
+ #### `main.ts`
1335
+
1336
+ ```ts
1337
+ // main.ts
1338
+ import { setApiConfig } from '@gemafajarramadhan/dynamic-ui'
1339
+ import '@gemafajarramadhan/dynamic-ui'
1340
+ import '@gemafajarramadhan/dynamic-ui/style.css'
1341
+
1342
+ // ✅ Konfigurasi API dari environment Angular
1343
+ import { environment } from './environments/environment'
1344
+
1345
+ setApiConfig({
1346
+ apiBaseUrl: environment.apiBaseUrl,
1347
+ getAuthToken: () => localStorage.getItem('access_token'),
1348
+ onUnauthorized: () => { window.location.href = '/login' },
1349
+ })
1350
+
1351
+ platformBrowserDynamic().bootstrapModule(AppModule)
1352
+ ```
1353
+
1354
+ > File `src/environments/environment.ts`:
1355
+ > ```ts
1356
+ > export const environment = {
1357
+ > production: false,
1358
+ > apiBaseUrl: 'https://api.example.com',
1359
+ > }
1360
+ > ```
1361
+
1362
+ #### Komponen Angular (`dynamic-form.component.ts`)
1363
+
1364
+ ```ts
1365
+ // dynamic-form.component.ts
1366
+ import {
1367
+ Component,
1368
+ OnInit,
1369
+ AfterViewInit,
1370
+ ViewChild,
1371
+ ElementRef,
1372
+ } from '@angular/core'
1373
+
1374
+ @Component({
1375
+ selector: 'app-dynamic-form',
1376
+ template: `<micro-dynamic-form #formEl></micro-dynamic-form>`,
1377
+ })
1378
+ export class DynamicFormComponent implements AfterViewInit {
1379
+ @ViewChild('formEl') formRef!: ElementRef
1380
+
1381
+ private formConfig = {
1382
+ title: 'Form Angular',
1383
+ sections: [
1384
+ {
1385
+ fields: [
1386
+ {
1387
+ key: 'username',
1388
+ label: 'Username',
1389
+ model: 'username',
1390
+ component: 'DCodeTextField',
1391
+ },
1392
+ ],
1393
+ },
1394
+ ],
1395
+ actions: [{ key: 'submit', label: 'Simpan', component: 'DCodeButton' }],
1396
+ }
1397
+
1398
+ ngAfterViewInit() {
1399
+ const el = this.formRef.nativeElement
1400
+
1401
+ // Set config via DOM property
1402
+ el.config = this.formConfig
1403
+
1404
+ el.addEventListener('submit', (event: CustomEvent) => {
1405
+
1406
+ })
1407
+ }
1408
+ }
1409
+ ```
1410
+
1411
+ #### Angular Standalone (Angular 17+)
1412
+
1413
+ ```ts
1414
+ // app.component.ts
1415
+ import { Component, AfterViewInit, ViewChild, ElementRef } from '@angular/core'
1416
+ import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
1417
+
1418
+ @Component({
1419
+ selector: 'app-root',
1420
+ standalone: true,
1421
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
1422
+ template: `<micro-dynamic-form #formEl></micro-dynamic-form>`,
1423
+ })
1424
+ export class AppComponent implements AfterViewInit {
1425
+ @ViewChild('formEl') formRef!: ElementRef
1426
+
1427
+ ngAfterViewInit() {
1428
+ const el = this.formRef.nativeElement
1429
+ el.config = { /* config Anda di sini */ }
1430
+ el.addEventListener('submit', (e: CustomEvent) =>
1431
+ }
1432
+ }
1433
+ ```
1434
+
1435
+ ---
1436
+
1437
+ ### 6. Nuxt 3
1438
+
1439
+ Di Nuxt 3, gunakan plugin untuk mendaftarkan Web Components dan tandai sebagai custom element.
1440
+
1441
+ #### Plugin (`plugins/dynamic-ui.client.ts`)
1442
+
1443
+ Buat file plugin dengan suffix `.client.ts` agar hanya dijalankan di sisi klien (browser):
1444
+
1445
+ ```ts
1446
+ // plugins/dynamic-ui.client.ts
1447
+ import DynamicUI from '@gemafajarramadhan/dynamic-ui'
1448
+ import '@gemafajarramadhan/dynamic-ui/style.css'
1449
+
1450
+ export default defineNuxtPlugin((nuxtApp) => {
1451
+ const config = useRuntimeConfig()
1452
+
1453
+ // ✅ Konfigurasi API dari runtimeConfig Nuxt (env host project)
1454
+ nuxtApp.vueApp.use(DynamicUI, {
1455
+ apiBaseUrl: config.public.apiBaseUrl,
1456
+ getAuthToken: () => useCookie('access_token').value ?? null,
1457
+ onUnauthorized: () => navigateTo('/login'),
1458
+ })
1459
+ })
1460
+ ```
1461
+
1462
+ #### Konfigurasi Nuxt (`nuxt.config.ts`)
1463
+
1464
+ ```ts
1465
+ // nuxt.config.ts
1466
+ export default defineNuxtConfig({
1467
+ runtimeConfig: {
1468
+ public: {
1469
+ apiBaseUrl: process.env.VITE_API_BASE_URL, // set di .env
1470
+ },
1471
+ },
1472
+ vue: {
1473
+ compilerOptions: {
1474
+ // Arahkan Nuxt/Vue agar tidak menganggap element ini sebagai Vue component
1475
+ isCustomElement: (tag: string) => tag.startsWith('micro-'),
1476
+ },
1477
+ },
1478
+ })
1479
+ ```
1480
+
1481
+ #### Penggunaan di Page atau Component
1482
+
1483
+ ```vue
1484
+ <!-- pages/index.vue -->
1485
+ <script setup>
1486
+ import { ref, onMounted } from 'vue'
1487
+
1488
+ const formRef = ref(null)
1489
+
1490
+ const formConfig = {
1491
+ title: 'Form Nuxt',
1492
+ sections: [
1493
+ {
1494
+ fields: [
1495
+ {
1496
+ key: 'name',
1497
+ label: 'Nama',
1498
+ model: 'name',
1499
+ component: 'DCodeTextField',
1500
+ },
1501
+ ],
1502
+ },
1503
+ ],
1504
+ actions: [{ key: 'submit', label: 'Submit', component: 'DCodeButton' }],
1505
+ }
1506
+
1507
+ onMounted(() => {
1508
+ if (formRef.value) {
1509
+ formRef.value.config = formConfig
1510
+ formRef.value.addEventListener('submit', (e) => {
1511
+
1512
+ })
1513
+ }
1514
+ })
1515
+ </script>
1516
+
1517
+ <template>
1518
+ <div>
1519
+ <h1>Halaman Dynamic Form</h1>
1520
+ <!-- Gunakan ClientOnly agar tidak error saat SSR -->
1521
+ <ClientOnly>
1522
+ <micro-dynamic-form ref="formRef" />
1523
+ </ClientOnly>
1524
+ </div>
1525
+ </template>
1526
+ ```
1527
+
1528
+ ---
1529
+
1530
+ ## Custom Logic Registry
1531
+
1532
+ Library menyediakan **Logic Registry** agar aplikasi host dapat menyuntikkan business logic ke dalam komponen tanpa memodifikasi library.
1533
+
1534
+ ### Option A: via `window` (sebelum library dimuat)
1535
+
1536
+ ```js
1537
+ window.__MICRO_LOGIC_REGISTRY__ = {
1538
+ 'user-management': {
1539
+ onReady(context) {
1540
+
1541
+ },
1542
+ handleAction(action, payload) {
1543
+ if (action === 'delete') {
1544
+ // Tampilkan modal konfirmasi
1545
+ return true // intercept action
1546
+ }
1547
+ },
1548
+ },
1549
+ }
1550
+ ```
1551
+
1552
+ ### Option B: via fungsi `registerLogic` (setelah import library)
1553
+
1554
+ ```js
1555
+ import {
1556
+ registerLogic,
1557
+ unregisterLogic,
1558
+ } from '@gemafajarramadhan/dynamic-ui'
1559
+
1560
+ registerLogic('user-management', {
1561
+ onReady(context) {
1562
+
1563
+ },
1564
+ handleAction(action, payload) {
1565
+
1566
+ },
1567
+ })
1568
+
1569
+ // Untuk menghapus logic saat komponen unmount:
1570
+ // unregisterLogic('user-management')
1571
+ ```
1572
+
1573
+ ### Interface LogicModule
1574
+
1575
+ ```ts
1576
+ interface LogicModule {
1577
+ onReady?: (context: Record<string, any>) => void
1578
+ handleAction?: (action: string, payload: any) => boolean | void
1579
+ }
1580
+ ```
1581
+
1582
+ ---
1583
+
1584
+ ## TypeScript Support
1585
+
1586
+ Untuk menambahkan type yang benar pada semua framework, buat file deklarasi global:
1587
+
1588
+ ```ts
1589
+ // src/types/dynamic-ui.d.ts
1590
+
1591
+ declare namespace DynamicUI {
1592
+ interface LogicModule {
1593
+ onReady?: (context: Record<string, any>) => void
1594
+ handleAction?: (action: string, payload: any) => boolean | void
1595
+ }
1596
+ }
1597
+
1598
+ // Deklarasi untuk custom element di JSX (React / Next.js)
1599
+ declare global {
1600
+ namespace JSX {
1601
+ interface IntrinsicElements {
1602
+ 'micro-dynamic-form': any
1603
+ 'micro-dynamic-datatable': any
1604
+ }
1605
+ }
1606
+
1607
+ // Untuk global window registry
1608
+ interface Window {
1609
+ __MICRO_LOGIC_REGISTRY__?: Record<string, DynamicUI.LogicModule>
1610
+ __MICRO_API_BASE_URL__?: string
1611
+ __MICRO_AUTH_TOKEN__?: string
1612
+ }
1613
+ }
1614
+
1615
+ export {}
1616
+ ```
1617
+
1618
+ ---
1619
+
1620
+ ## Referensi Props Komponen
1621
+
1622
+ Berikut adalah dokumentasi lengkap props untuk setiap komponen yang tersedia.
1623
+
1624
+ ---
1625
+
1626
+ ### `DataTable`
1627
+
1628
+ Komponen tabel data yang mendukung mode API (fetch otomatis) maupun data statis, dengan pagination, sorting, filtering, dan action button.
1629
+
1630
+ | Prop | Tipe | Default | Deskripsi |
1631
+ |------|------|---------|-----------|
1632
+ | `columns` | `TableColumn[]` | `[]` | Definisi kolom tabel |
1633
+ | `data` | `any[]` | `[]` | Data statis (mode non-API) |
1634
+ | `perPage` | `number` | `10` | Jumlah baris per halaman |
1635
+ | `loading` | `boolean` | `false` | Status loading eksternal |
1636
+ | `hasActions` | `boolean` | `false` | Tampilkan kolom aksi baris |
1637
+ | `nested` | `boolean` | `false` | Mode nested (tampilan kompak tanpa header search) |
1638
+ | `apiModule` | `string` | — | Module API untuk fetch data |
1639
+ | `apiAction` | `string` | `'list'` | Action API |
1640
+ | `apiMethod` | `string` | — | HTTP method (`GET`/`POST`) |
1641
+ | `apiUrl` | `string` | — | URL endpoint API langsung |
1642
+ | `apiParams` | `object` | `{}` | Parameter tambahan untuk request API |
1643
+ | `tableConfig` | `any` | — | Konfigurasi JSON lengkap tabel (dari backend) |
1644
+ | `dynamicColumns` | `boolean` | `false` | Gunakan kolom dinamis dari response API |
1645
+ | `enableQuickSearch` | `boolean` | `true` | Aktifkan pencarian cepat (debounce) |
1646
+ | `showPagination` | `boolean` | `true` | Tampilkan pagination |
1647
+ | `ignorePermissions` | `boolean` | `false` | Abaikan cek permission |
1648
+ | `permissionBase` | `string` | — | Base path untuk permission check |
1649
+
1650
+ ```vue
1651
+ <template>
1652
+ <!-- Mode API -->
1653
+ <DataTable
1654
+ api-url="/api/users"
1655
+ api-method="POST"
1656
+ :columns="[
1657
+ { key: 'name', label: 'Nama', sortable: true },
1658
+ { key: 'email', label: 'Email' },
1659
+ ]"
1660
+ :per-page="25"
1661
+ />
1662
+
1663
+ <!-- Mode data statis -->
1664
+ <DataTable
1665
+ :data="myData"
1666
+ :columns="columns"
1667
+ />
1668
+ </template>
1669
+ ```
1670
+
1671
+ ---
1672
+
1673
+ ### `DCodeButton`
1674
+
1675
+ Tombol dengan berbagai variant, warna kustom, ikon Lucide, dan tooltip.
1676
+
1677
+ | Prop | Tipe | Default | Deskripsi |
1678
+ |------|------|---------|-----------|
1679
+ | `text` | `string` | `''` | Teks tombol |
1680
+ | `icon` | `string` | — | Nama ikon Lucide (e.g. `'Save'`, `'Trash2'`) |
1681
+ | `variant` | `'default'\|'secondary'\|'destructive'\|'outline'\|'ghost'\|'link'\|'tonal'\|'elevated'` | `'default'` | Variant tampilan |
1682
+ | `size` | `'default'\|'sm'\|'lg'\|'icon'` | `'default'` | Ukuran tombol |
1683
+ | `tooltip` | `string` | `''` | Teks tooltip |
1684
+ | `bgColor` | `string` | `''` | Warna background kustom (tailwind/hex) |
1685
+ | `textColor` | `string` | `''` | Warna teks kustom |
1686
+ | `hoverBgColor` | `string` | `''` | Warna background saat hover |
1687
+ | `borderColor` | `string` | `''` | Warna border kustom |
1688
+ | `disabled` | `boolean` | `false` | Nonaktifkan tombol |
1689
+ | `type` | `'button'\|'submit'\|'reset'` | `'button'` | Tipe HTML button |
1690
+ | `visible` | `boolean` | `true` | Tampilkan/sembunyikan tombol |
1691
+ | `skeleton` | `boolean` | `false` | Tampilkan skeleton loading |
1692
+
1693
+ ```vue
1694
+ <template>
1695
+ <DCodeButton text="Simpan" icon="Save" variant="default" />
1696
+ <DCodeButton text="Hapus" icon="Trash2" variant="destructive" tooltip="Hapus data ini" />
1697
+ <DCodeButton text="Submit" type="submit" :disabled="isLoading" />
1698
+ </template>
1699
+ ```
1700
+
1701
+ ---
1702
+
1703
+ ### `DCodeTextField`
1704
+
1705
+ Input teks dengan banyak fitur: filter karakter, transformasi case, clearable, password toggle.
1706
+
1707
+ | Prop | Tipe | Default | Deskripsi |
1708
+ |------|------|---------|-----------|
1709
+ | `modelValue` | `string\|number\|null` | — | Value input (v-model) |
1710
+ | `prependInner` | `string` | — | Teks/ikon di sisi kiri input |
1711
+ | `clearable` | `boolean` | — | Tampilkan tombol clear |
1712
+ | `hideClearWhenEmpty` | `boolean` | — | Sembunyikan tombol clear saat kosong |
1713
+ | `textCase` | `'none'\|'uppercase'\|'lowercase'\|'titlecase'\|'sentencecase'` | `'none'` | Transformasi huruf |
1714
+ | `applyTextCaseToValue` | `boolean` | `true` | Terapkan transformasi ke v-model |
1715
+ | `valueType` | `'free'\|'number'\|'letter'\|'letternumber'\|'email'\|'password'` | `'free'` | Filter jenis karakter yang diperbolehkan |
1716
+ | `allowSpaces` | `boolean` | — | Izinkan spasi |
1717
+ | `allowChars` | `string\|string[]` | — | Karakter tambahan yang diizinkan |
1718
+ | `pattern` | `RegExp` | — | Regex validasi |
1719
+ | `visible` | `boolean` | `true` | Tampilkan/sembunyikan |
1720
+ | `skeleton` | `boolean` | `false` | Tampilkan skeleton |
1721
+ | `returnNumber` | `boolean` | — | Emit nilai sebagai number |
1722
+ | `error` | `string\|null` | — | Pesan error validasi |
1723
+
1724
+ ```vue
1725
+ <template>
1726
+ <DCodeTextField
1727
+ v-model="form.name"
1728
+ label="Nama Lengkap"
1729
+ placeholder="Masukkan nama..."
1730
+ text-case="titlecase"
1731
+ clearable
1732
+ />
1733
+ <DCodeTextField
1734
+ v-model="form.email"
1735
+ label="Email"
1736
+ value-type="email"
1737
+ :error="errors.email"
1738
+ />
1739
+ <DCodeTextField
1740
+ v-model="form.password"
1741
+ label="Password"
1742
+ value-type="password"
1743
+ />
1744
+ </template>
1745
+ ```
1746
+
1747
+ ---
1748
+
1749
+ ### `DCodeTextarea`
1750
+
1751
+ Textarea dengan karakter counter, filter, dan transformasi case.
1752
+
1753
+ | Prop | Tipe | Default | Deskripsi |
1754
+ |------|------|---------|-----------|
1755
+ | `modelValue` | `string\|number\|null` | — | Value (v-model) |
1756
+ | `clearable` | `boolean` | — | Tombol clear |
1757
+ | `hideClearWhenEmpty` | `boolean` | `true` | Sembunyikan clear saat kosong |
1758
+ | `textCase` | `'none'\|'uppercase'\|'lowercase'\|'titlecase'\|'sentencecase'` | `'none'` | Transformasi huruf |
1759
+ | `applyTextCaseToValue` | `boolean` | `true` | Terapkan ke v-model |
1760
+ | `valueType` | `'free'\|'number'\|'letter'\|'letternumber'` | `'free'` | Filter karakter |
1761
+ | `allowSpaces` | `boolean` | `true` | Izinkan spasi |
1762
+ | `allowChars` | `string\|string[]` | — | Karakter tambahan |
1763
+ | `pattern` | `RegExp` | — | Regex validasi |
1764
+ | `visible` | `boolean` | `true` | Tampilkan/sembunyikan |
1765
+ | `skeleton` | `boolean` | `false` | Skeleton loading |
1766
+ | `showCharCount` | `boolean` | `true` | Tampilkan counter karakter |
1767
+ | `returnNumber` | `boolean` | `false` | Emit sebagai number |
1768
+ | `error` | `string\|null` | `null` | Pesan error |
1769
+
1770
+ ```vue
1771
+ <template>
1772
+ <DCodeTextarea
1773
+ v-model="form.description"
1774
+ label="Deskripsi"
1775
+ placeholder="Masukkan deskripsi..."
1776
+ :show-char-count="true"
1777
+ maxlength="500"
1778
+ />
1779
+ </template>
1780
+ ```
1781
+
1782
+ ---
1783
+
1784
+ ### `DCodeAutoComplete`
1785
+
1786
+ Dropdown autocomplete dengan fetch API otomatis dan pencarian.
1787
+
1788
+ | Prop | Tipe | Default | Deskripsi |
1789
+ |------|------|---------|-----------|
1790
+ | `modelValue` | `any` | — | Value terpilih (v-model) |
1791
+ | `endpoint` | `{ apiUrl?, apiMethod?, apiModule?, apiAction? }` | — | Konfigurasi endpoint API |
1792
+ | `apiParams` | `object` | `{}` | Parameter tambahan request |
1793
+ | `fetchItems` | `Function` | — | Fungsi custom fetch (alternatif endpoint) |
1794
+ | `itemTitle` | `string\|Function` | `'name'` | Field label item |
1795
+ | `itemValue` | `string\|Function` | `'id'` | Field nilai item |
1796
+ | `returnObject` | `boolean` | `false` | Return objek penuh |
1797
+ | `autoFetchOnOpen` | `boolean` | `true` | Fetch saat dropdown terbuka |
1798
+ | `autoFetchOnMount` | `boolean` | `false` | Fetch saat mount |
1799
+ | `clearable` | `boolean` | `false` | Tombol clear |
1800
+ | `disabled` | `boolean` | `false` | Nonaktifkan |
1801
+ | `visible` | `boolean` | `true` | Tampilkan/sembunyikan |
1802
+ | `skeleton` | `boolean` | `false` | Skeleton loading |
1803
+ | `error` | `string\|null` | `null` | Pesan error |
1804
+ | `icon` | `Component` | — | Ikon kiri input |
1805
+
1806
+ ```vue
1807
+ <template>
1808
+ <DCodeAutoComplete
1809
+ v-model="form.departemenId"
1810
+ label="Departemen"
1811
+ :endpoint="{ apiUrl: '/api/master/departemen' }"
1812
+ item-title="nama"
1813
+ item-value="id"
1814
+ clearable
1815
+ />
1816
+ </template>
1817
+ ```
1818
+
1819
+ ---
1820
+
1821
+ ### `DCodeInfiniteAutoComplete`
1822
+
1823
+ Autocomplete dengan pagination infinite scroll (load more saat scroll ke bawah).
1824
+
1825
+ | Prop | Tipe | Default | Deskripsi |
1826
+ |------|------|---------|-----------|
1827
+ | `modelValue` | `any` | — | Value (v-model) |
1828
+ | `endpoint` | `{ apiUrl?, apiMethod? }` | — | Endpoint API |
1829
+ | `apiParams` | `object` | `{}` | Parameter tambahan |
1830
+ | `fetchItems` | `Function` | — | Custom fetch |
1831
+ | `itemTitle` | `string\|Function` | `'name'` | Field label |
1832
+ | `itemValue` | `string\|Function` | `'id'` | Field nilai |
1833
+ | `itemSubtitle` | `string\|Function` | — | Field subtitle item |
1834
+ | `returnObject` | `boolean` | `false` | Return objek penuh |
1835
+ | `autoFetchOnOpen` | `boolean` | `true` | Fetch saat buka |
1836
+ | `refreshOnOpen` | `boolean` | `false` | Refresh data setiap buka |
1837
+ | `clearable` | `boolean` | `false` | Tombol clear |
1838
+ | `disabled` | `boolean` | `false` | Nonaktifkan |
1839
+ | `visible` | `boolean` | `true` | Tampilkan |
1840
+ | `skeleton` | `boolean` | `false` | Skeleton |
1841
+ | `error` | `string\|null` | `null` | Error message |
1842
+ | `icon` | `Component` | — | Ikon prefix |
1843
+ | `pageSize` | `number` | `20` | Jumlah item per halaman |
1844
+
1845
+ ```vue
1846
+ <template>
1847
+ <DCodeInfiniteAutoComplete
1848
+ v-model="form.userId"
1849
+ label="Pilih User"
1850
+ :endpoint="{ apiUrl: '/api/users', apiMethod: 'GET' }"
1851
+ item-title="fullName"
1852
+ item-value="id"
1853
+ :page-size="15"
1854
+ />
1855
+ </template>
1856
+ ```
1857
+
1858
+ ---
1859
+
1860
+ ### `DCodeMultiSelect`
1861
+
1862
+ Dropdown multi-pilihan dengan chip tags dan search.
1863
+
1864
+ | Prop | Tipe | Default | Deskripsi |
1865
+ |------|------|---------|-----------|
1866
+ | `modelValue` | `any[]` | — | Array nilai terpilih (v-model) |
1867
+ | `items` | `object[]` | `[]` | Data statis |
1868
+ | `endpoint` | `{ apiUrl?, apiMethod?, apiOptions? }` | — | Endpoint API |
1869
+ | `fetchItems` | `Function` | — | Custom fetch |
1870
+ | `autoFetchOnOpen` | `boolean` | `true` | Fetch saat buka |
1871
+ | `autoFetchOnMount` | `boolean` | `false` | Fetch saat mount |
1872
+ | `refreshOnOpen` | `boolean` | `false` | Refresh setiap buka |
1873
+ | `label` | `string` | `''` | Label dropdown |
1874
+ | `placeholder` | `string` | `'Pilih data…'` | Placeholder |
1875
+ | `disabled` | `boolean` | `false` | Nonaktifkan |
1876
+ | `clearable` | `boolean` | `false` | Tombol clear |
1877
+ | `searchable` | `boolean` | `false` | Aktifkan search |
1878
+ | `searchPlaceholder` | `string` | `'Cari…'` | Placeholder search |
1879
+ | `visible` | `boolean` | `true` | Tampilkan |
1880
+ | `skeleton` | `boolean` | `false` | Skeleton |
1881
+ | `maxChips` | `number\|null` | `2` | Maks chip ditampilkan |
1882
+ | `maxSelected` | `number\|null` | `3` | Maks pilihan |
1883
+ | `itemTitle` | `string\|Function` | — | Field label |
1884
+ | `itemValue` | `string\|Function` | — | Field nilai |
1885
+ | `returnObject` | `boolean` | — | Return objek penuh |
1886
+ | `error` | `string\|null` | — | Error message |
1887
+
1888
+ ```vue
1889
+ <template>
1890
+ <DCodeMultiSelect
1891
+ v-model="form.tags"
1892
+ label="Tags"
1893
+ :items="availableTags"
1894
+ item-title="name"
1895
+ item-value="id"
1896
+ :max-selected="5"
1897
+ searchable
1898
+ />
1899
+ </template>
1900
+ ```
1901
+
1902
+ ---
1903
+
1904
+ ### `DCodeDatePicker`
1905
+
1906
+ Date picker atau date range picker dengan berbagai format.
1907
+
1908
+ | Prop | Tipe | Default | Deskripsi |
1909
+ |------|------|---------|-----------|
1910
+ | `modelValue` | `string\|[string,string]\|null` | — | Value tanggal (v-model) |
1911
+ | `range` | `boolean` | `true` | Mode range (pilih 2 tanggal) |
1912
+ | `autoRangeDays` | `number\|null` | `null` | Otomatis set range N hari |
1913
+ | `disablePast` | `boolean` | `false` | Nonaktifkan tanggal masa lalu |
1914
+ | `maxDaysAhead` | `number\|null` | `null` | Maks hari ke depan |
1915
+ | `placeholder` | `string` | `'Pilih tanggal'` | Placeholder |
1916
+ | `label` | `string` | — | Label |
1917
+ | `disabled` | `boolean` | `false` | Nonaktifkan |
1918
+ | `clearable` | `boolean` | `true` | Tombol clear |
1919
+ | `displayFormat` | `string` | `'dd/MM/yyyy'` | Format tampilan |
1920
+ | `modelType` | `string` | `'yyyy-MM-dd'` | Format v-model |
1921
+ | `locale` | `'id'\|'en'` | `'en'` | Bahasa kalender |
1922
+ | `placement` | `string` | `'bottom-start'` | Posisi dropdown |
1923
+ | `prependInner` | `string` | — | Teks prefix |
1924
+ | `error` | `string\|null` | `null` | Error message |
1925
+ | `tone` | `string` | `'primary'` | Warna tema kalender |
1926
+ | `skeleton` | `boolean` | `false` | Skeleton loading |
1927
+
1928
+ ```vue
1929
+ <template>
1930
+ <!-- Single date -->
1931
+ <DCodeDatePicker
1932
+ v-model="form.tanggal"
1933
+ label="Tanggal Lahir"
1934
+ :range="false"
1935
+ display-format="dd MMMM yyyy"
1936
+ locale="id"
1937
+ />
1938
+
1939
+ <!-- Date range -->
1940
+ <DCodeDatePicker
1941
+ v-model="form.periode"
1942
+ label="Periode"
1943
+ :range="true"
1944
+ />
1945
+ </template>
1946
+ ```
1947
+
1948
+ ---
1949
+
1950
+ ### `DCodeCurrencyField`
1951
+
1952
+ Input angka dengan format mata uang otomatis.
1953
+
1954
+ | Prop | Tipe | Default | Deskripsi |
1955
+ |------|------|---------|-----------|
1956
+ | `modelValue` | `number\|null` | — | Nilai angka (v-model) |
1957
+ | `currency` | `string` | `'IDR'` | Kode mata uang (`'IDR'`, `'USD'`) |
1958
+ | `locale` | `string` | `'en-US'` | Locale format angka |
1959
+ | `label` | `string` | — | Label field |
1960
+ | `placeholder` | `string` | — | Placeholder |
1961
+ | `error` | `string\|null` | — | Error message |
1962
+ | `disabled` | `boolean` | — | Nonaktifkan |
1963
+ | `readonly` | `boolean` | — | Read-only |
1964
+ | `clearable` | `boolean` | — | Tombol clear |
1965
+ | `hideClearWhenEmpty` | `boolean` | `true` | Sembunyikan clear jika kosong |
1966
+ | `prependInner` | `string` | — | Override simbol mata uang |
1967
+ | `skeleton` | `boolean` | `false` | Skeleton loading |
1968
+
1969
+ ```vue
1970
+ <template>
1971
+ <DCodeCurrencyField
1972
+ v-model="form.harga"
1973
+ label="Harga"
1974
+ currency="IDR"
1975
+ clearable
1976
+ :error="errors.harga"
1977
+ />
1978
+ </template>
1979
+ ```
1980
+
1981
+ ---
1982
+
1983
+ ### `DCodeCheckbox`
1984
+
1985
+ Checkbox dengan label dan dukungan indeterminate state.
1986
+
1987
+ | Prop | Tipe | Default | Deskripsi |
1988
+ |------|------|---------|-----------|
1989
+ | `modelValue` | `boolean\|null` | — | Nilai checkbox (v-model) |
1990
+ | `visible` | `boolean` | `true` | Tampilkan |
1991
+ | `skeleton` | `boolean` | `false` | Skeleton |
1992
+ | `indeterminate` | `boolean` | `false` | State indeterminate |
1993
+ | `disabled` | `boolean` | `false` | Nonaktifkan |
1994
+
1995
+ Gunakan attr `label` untuk teks di samping checkbox.
1996
+
1997
+ ```vue
1998
+ <template>
1999
+ <DCodeCheckbox
2000
+ v-model="form.setuju"
2001
+ label="Saya setuju dengan syarat & ketentuan"
2002
+ required
2003
+ />
2004
+ </template>
2005
+ ```
2006
+
2007
+ ---
2008
+
2009
+ ### `DCodeSwitch`
2010
+
2011
+ Toggle switch dengan berbagai ukuran, warna, dan tooltip.
2012
+
2013
+ | Prop | Tipe | Default | Deskripsi |
2014
+ |------|------|---------|-----------|
2015
+ | `modelValue` | `boolean` | `false` | Status switch (v-model) |
2016
+ | `label` | `string` | `''` | Label teks |
2017
+ | `disabled` | `boolean` | `false` | Nonaktifkan |
2018
+ | `size` | `'small'\|'medium'\|'large'` | `'medium'` | Ukuran switch |
2019
+ | `color` | `'primary'\|'success'\|'warning'\|'danger'` | `'primary'` | Warna aktif |
2020
+ | `loading` | `boolean` | `false` | Status loading |
2021
+ | `labelPosition` | `'left'\|'right'` | `'right'` | Posisi label |
2022
+ | `required` | `boolean` | `false` | Required |
2023
+ | `showTooltip` | `boolean` | `false` | Tampilkan tooltip |
2024
+ | `tooltipActiveText` | `string` | `'Aktif'` | Teks tooltip saat aktif |
2025
+ | `tooltipInactiveText` | `string` | `'Tidak Aktif'` | Teks tooltip saat nonaktif |
2026
+
2027
+ ```vue
2028
+ <template>
2029
+ <DCodeSwitch
2030
+ v-model="form.isActive"
2031
+ label="Status Aktif"
2032
+ color="success"
2033
+ size="medium"
2034
+ :show-tooltip="true"
2035
+ />
2036
+ </template>
2037
+ ```
2038
+
2039
+ ---
2040
+
2041
+ ### `DCodeRadio`
2042
+
2043
+ Grup radio button yang bisa fetch opsi dari API.
2044
+
2045
+ | Prop | Tipe | Default | Deskripsi |
2046
+ |------|------|---------|-----------|
2047
+ | `modelValue` | `any` | — | Value terpilih (v-model) |
2048
+ | `label` | `string` | `'name'` | Key field label dari opsi |
2049
+ | `options` | `any[]` | `[]` | Opsi data statis |
2050
+ | `apiUrl` | `string` | `''` | URL endpoint API untuk fetch opsi |
2051
+ | `apiMethod` | `'GET'\|'POST'` | `'GET'` | HTTP method |
2052
+ | `dataPath` | `string` | `''` | Path ke array data dalam response |
2053
+ | `layout` | `boolean` | `true` | Layout horizontal (true) / vertikal |
2054
+ | `disabled` | `boolean` | `false` | Nonaktifkan |
2055
+ | `required` | `boolean` | `false` | Required |
2056
+ | `error` | `string\|null` | `null` | Error message |
2057
+ | `size` | `'sm'\|'md'\|'lg'` | `'md'` | Ukuran radio |
2058
+ | `color` | `string` | `'blue'` | Warna accent |
2059
+ | `model` | `string` | `'id'` | Key field nilai dari opsi |
2060
+
2061
+ ```vue
2062
+ <template>
2063
+ <!-- Dengan opsi statis -->
2064
+ <DCodeRadio
2065
+ v-model="form.gender"
2066
+ :options="[
2067
+ { id: 'L', name: 'Laki-laki' },
2068
+ { id: 'P', name: 'Perempuan' },
2069
+ ]"
2070
+ />
2071
+
2072
+ <!-- Dengan API -->
2073
+ <DCodeRadio
2074
+ v-model="form.statusId"
2075
+ api-url="/api/master/status"
2076
+ label="nama"
2077
+ model="id"
2078
+ />
2079
+ </template>
2080
+ ```
2081
+
2082
+ ---
2083
+
2084
+ ### `DCodeRadioCustom`
2085
+
2086
+ Radio button dengan tampilan card kustom dan ikon.
2087
+
2088
+ | Prop | Tipe | Default | Deskripsi |
2089
+ |------|------|---------|-----------|
2090
+ | `modelValue` | `any` | — | Value (v-model) |
2091
+ | `icon` | `string` | — | Nama ikon Lucide untuk setiap card |
2092
+ | `label` | `string` | `'name'` | Key field label |
2093
+ | `options` | `any[]` | `[]` | Opsi statis |
2094
+ | `apiUrl` | `string` | `''` | URL API |
2095
+ | `apiMethod` | `'GET'\|'POST'` | `'GET'` | HTTP method |
2096
+ | `dataPath` | `string` | `''` | Path data dalam response |
2097
+ | `layout` | `boolean` | `true` | Horizontal layout |
2098
+ | `disabled` | `boolean` | `false` | Nonaktifkan |
2099
+ | `required` | `boolean` | `false` | Required |
2100
+ | `error` | `string\|null` | `null` | Error message |
2101
+ | `size` | `'sm'\|'md'\|'lg'` | `'md'` | Ukuran |
2102
+ | `color` | `string` | `'blue'` | Warna highlight |
2103
+ | `model` | `string` | `'id'` | Key field nilai |
2104
+
2105
+ ```vue
2106
+ <template>
2107
+ <DCodeRadioCustom
2108
+ v-model="form.plan"
2109
+ :options="[
2110
+ { id: 'basic', name: 'Basic', icon: 'Star' },
2111
+ { id: 'pro', name: 'Pro', icon: 'Zap' },
2112
+ ]"
2113
+ label="name"
2114
+ model="id"
2115
+ />
2116
+ </template>
2117
+ ```
2118
+
2119
+ ---
2120
+
2121
+ ### `DCodeOtpInput`
2122
+
2123
+ Input OTP digit-per-digit dengan submit otomatis ke API.
2124
+
2125
+ | Prop | Tipe | Default | Deskripsi |
2126
+ |------|------|---------|-----------|
2127
+ | `modelValue` | `string` | `''` | Nilai OTP (v-model) |
2128
+ | `totalInput` | `number` | `6` | Jumlah digit OTP |
2129
+ | `default` | `string` | `''` | Default nilai awal |
2130
+ | `label` | `string` | `''` | Label |
2131
+ | `endpoint` | `{ apiMethod?, apiUrl? }` | — | Endpoint untuk submit OTP |
2132
+ | `submitFn` | `Function` | — | Custom submit function |
2133
+
2134
+ Events: `complete` (OTP penuh), `success`, `error`, `loading`
2135
+
2136
+ ```vue
2137
+ <template>
2138
+ <DCodeOtpInput
2139
+ v-model="otp"
2140
+ :total-input="6"
2141
+ label="Kode OTP"
2142
+ :endpoint="{ apiUrl: '/api/auth/verify-otp', apiMethod: 'POST' }"
2143
+ @complete="handleOtpComplete"
2144
+ />
2145
+ </template>
2146
+ ```
2147
+
2148
+ ---
2149
+
2150
+ ### `DCodeFileField`
2151
+
2152
+ Upload file dengan validasi tipe & ukuran, preview, dan upload ke API.
2153
+
2154
+ | Prop | Tipe | Default | Deskripsi |
2155
+ |------|------|---------|-----------|
2156
+ | `modelValue` | `File\|null` | — | File terpilih (v-model) |
2157
+ | `label` | `string` | `'Upload File'` | Label |
2158
+ | `hint` | `string` | — | Teks petunjuk |
2159
+ | `disabled` | `boolean` | `false` | Nonaktifkan |
2160
+ | `previewHeight` | `number` | `190` | Tinggi preview (px) |
2161
+ | `previewWidth` | `number` | — | Lebar preview (px) |
2162
+ | `allowedTypes` | `string\|null` | `null` | Tipe file diizinkan (e.g. `'pdf,doc'`) |
2163
+ | `accept` | `string` | — | Attribute accept HTML |
2164
+ | `minSizeMb` | `number\|null` | `null` | Ukuran file minimum (MB) |
2165
+ | `maxSizeMb` | `number` | `5` | Ukuran file maksimum (MB) |
2166
+ | `endpoint` | `{ apiMethod?, apiUrl? }` | — | Endpoint upload |
2167
+ | `uploadFn` | `Function` | — | Custom upload function |
2168
+
2169
+ ```vue
2170
+ <template>
2171
+ <DCodeFileField
2172
+ v-model="form.dokumen"
2173
+ label="Upload Dokumen"
2174
+ allowed-types="pdf,doc,docx"
2175
+ :max-size-mb="10"
2176
+ :endpoint="{ apiUrl: '/api/upload', apiMethod: 'POST' }"
2177
+ @uploaded="handleUploaded"
2178
+ @error="handleError"
2179
+ />
2180
+ </template>
2181
+ ```
2182
+
2183
+ ---
2184
+
2185
+ ### `DCodeImageField`
2186
+
2187
+ Upload gambar dengan crop/resize dialog terintegrasi.
2188
+
2189
+ | Prop | Tipe | Default | Deskripsi |
2190
+ |------|------|---------|-----------|
2191
+ | `modelValue` | `File\|null` | — | File gambar (v-model) |
2192
+ | `label` | `string` | `'Upload Gambar'` | Label |
2193
+ | `hint` | `string` | `'Drag & drop atau klik area ini'` | Petunjuk |
2194
+ | `disabled` | `boolean` | `false` | Nonaktifkan |
2195
+ | `allowedTypes` | `string\|null` | `null` | Tipe gambar diizinkan |
2196
+ | `accept` | `string` | `'image/*'` | Accept HTML |
2197
+ | `minSizeMb` | `number\|null` | `null` | Min ukuran (MB) |
2198
+ | `maxSizeMb` | `number` | `2` | Maks ukuran (MB) |
2199
+ | `title` | `string` | `'Hasil Gambar'` | Judul preview |
2200
+ | `description` | `string` | `''` | Deskripsi preview |
2201
+ | `previewWidth` | `number` | `360` | Lebar preview |
2202
+ | `previewHeight` | `number` | `220` | Tinggi preview |
2203
+ | `aspectRatio` | `number\|null` | `null` | Rasio aspek crop |
2204
+ | `outputWidth` | `number\|null` | `null` | Lebar output (px) |
2205
+ | `outputHeight` | `number\|null` | `null` | Tinggi output (px) |
2206
+ | `quality` | `number` | `0.92` | Kualitas output (0-1) |
2207
+ | `autoOpenEditor` | `boolean` | `true` | Buka editor saat file dipilih |
2208
+ | `minWidth` | `number\|null` | `null` | Min lebar gambar |
2209
+ | `minHeight` | `number\|null` | `null` | Min tinggi gambar |
2210
+ | `maxWidth` | `number\|null` | `null` | Maks lebar gambar |
2211
+ | `maxHeight` | `number\|null` | `null` | Maks tinggi gambar |
2212
+ | `endpoint` | `{ apiMethod?, apiUrl? }` | — | Endpoint upload |
2213
+ | `uploadFn` | `Function` | — | Custom upload |
2214
+ | `editUseOriginal` | `boolean` | `false` | Edit dari foto asli (bukan ter-crop) |
2215
+
2216
+ ```vue
2217
+ <template>
2218
+ <DCodeImageField
2219
+ v-model="form.foto"
2220
+ label="Foto Profil"
2221
+ :aspect-ratio="1"
2222
+ :output-width="300"
2223
+ :output-height="300"
2224
+ :max-size-mb="2"
2225
+ />
2226
+ </template>
2227
+ ```
2228
+
2229
+ ---
2230
+
2231
+ ### `DCodeDropzone`
2232
+
2233
+ Area drag-and-drop untuk upload file.
2234
+
2235
+ | Prop | Tipe | Default | Deskripsi |
2236
+ |------|------|---------|-----------|
2237
+ | `label` | `string` | `'Upload File'` | Label |
2238
+ | `hint` | `string` | `'Drag & drop file di sini atau klik area ini'` | Petunjuk |
2239
+ | `accept` | `string` | `'image/*'` | Tipe file diterima |
2240
+ | `maxSizeMb` | `number` | `2` | Maks ukuran (MB) |
2241
+ | `mode` | `'single'\|'multiple'` | `'single'` | Mode pilih satu/banyak |
2242
+ | `disabled` | `boolean` | `false` | Nonaktifkan |
2243
+ | `showSelected` | `boolean` | `true` | Tampilkan nama file terpilih |
2244
+ | `error` | `string\|null` | `null` | Error message |
2245
+
2246
+ Events: `picked` (File), `picked:multiple` (File[]), `error` (string)
2247
+
2248
+ ```vue
2249
+ <template>
2250
+ <DCodeDropzone
2251
+ label="Upload CSV"
2252
+ accept=".csv,.xlsx"
2253
+ :max-size-mb="5"
2254
+ mode="single"
2255
+ @picked="handleFile"
2256
+ />
2257
+ </template>
2258
+ ```
2259
+
2260
+ ---
2261
+
2262
+ ### `DCodeUploadFile`
2263
+
2264
+ Komponen upload file sederhana dengan tombol browse.
2265
+
2266
+ | Prop | Tipe | Default | Deskripsi |
2267
+ |------|------|---------|-----------|
2268
+ | `modelValue` | `any` | — | File (v-model) |
2269
+ | `label` | `string` | — | Label |
2270
+ | `accept` | `string` | — | Tipe file diterima |
2271
+ | `errorMessage` | `string` | — | Pesan error |
2272
+
2273
+ ```vue
2274
+ <template>
2275
+ <DCodeUploadFile
2276
+ v-model="form.file"
2277
+ label="Upload Berkas"
2278
+ accept=".pdf,.doc"
2279
+ />
2280
+ </template>
2281
+ ```
2282
+
2283
+ ---
2284
+
2285
+ ### `DCodeFileResult`
2286
+
2287
+ Menampilkan preview file yang sudah dipilih dengan opsi edit/replace/remove.
2288
+
2289
+ | Prop | Tipe | Default | Deskripsi |
2290
+ |------|------|---------|-----------|
2291
+ | `file` | `File\|null` | — | File untuk ditampilkan |
2292
+ | `title` | `string` | `'File Terpilih'` | Judul card |
2293
+ | `description` | `string` | `''` | Deskripsi |
2294
+ | `disabled` | `boolean` | `false` | Nonaktifkan aksi |
2295
+ | `previewHeight` | `number` | `190` | Tinggi preview |
2296
+ | `previewWidth` | `number` | `220` | Lebar preview |
2297
+ | `showEdit` | `boolean` | `true` | Tampilkan tombol Edit |
2298
+ | `showReplace` | `boolean` | `true` | Tampilkan tombol Ganti |
2299
+ | `showRemove` | `boolean` | `true` | Tampilkan tombol Hapus |
2300
+ | `forceFileIcon` | `boolean` | `false` | Selalu tampilkan ikon file (bukan preview gambar) |
2301
+
2302
+ Events: `edit`, `replace`, `remove`
2303
+
2304
+ ```vue
2305
+ <template>
2306
+ <DCodeFileResult
2307
+ :file="uploadedFile"
2308
+ title="Dokumen Saya"
2309
+ :show-edit="false"
2310
+ @replace="handleReplace"
2311
+ @remove="handleRemove"
2312
+ />
2313
+ </template>
2314
+ ```
2315
+
2316
+ ---
2317
+
2318
+ ### `DCodeImageResult`
2319
+
2320
+ Menampilkan preview gambar yang sudah dipilih.
2321
+
2322
+ | Prop | Tipe | Default | Deskripsi |
2323
+ |------|------|---------|-----------|
2324
+ | `file` | `File\|null` | — | File gambar |
2325
+ | `title` | `string` | `'Hasil Gambar'` | Judul card |
2326
+ | `description` | `string` | `''` | Deskripsi |
2327
+ | `disabled` | `boolean` | `false` | Nonaktifkan |
2328
+ | `previewHeight` | `number` | `190` | Tinggi preview |
2329
+ | `previewWidth` | `number` | `220` | Lebar preview |
2330
+ | `showEdit` | `boolean` | `true` | Tombol edit |
2331
+ | `showReplace` | `boolean` | `true` | Tombol ganti |
2332
+ | `showRemove` | `boolean` | `true` | Tombol hapus |
2333
+
2334
+ Events: `edit`, `replace`, `remove`
2335
+
2336
+ ```vue
2337
+ <template>
2338
+ <DCodeImageResult
2339
+ :file="imageFile"
2340
+ title="Foto Profil"
2341
+ @edit="openCropper"
2342
+ @remove="clearImage"
2343
+ />
2344
+ </template>
2345
+ ```
2346
+
2347
+ ---
2348
+
2349
+ ### `DCodeImageCropperDialog`
2350
+
2351
+ Dialog crop / resize gambar menggunakan Uppy + ImageEditor.
2352
+
2353
+ | Prop | Tipe | Default | Deskripsi |
2354
+ |------|------|---------|-----------|
2355
+ | `modelValue` | `boolean` | — | Status dialog (v-model) |
2356
+ | `file` | `File\|null` | — | File gambar sumber |
2357
+ | `title` | `string` | `'Resize & Crop Gambar'` | Judul dialog |
2358
+ | `description` | `string` | `''` | Deskripsi |
2359
+ | `aspectRatio` | `number\|null` | `null` | Rasio aspek (e.g. `16/9`) |
2360
+ | `outputWidth` | `number\|null` | `null` | Lebar output (px) |
2361
+ | `outputHeight` | `number\|null` | `null` | Tinggi output (px) |
2362
+ | `quality` | `number` | `0.92` | Kualitas gambar output |
2363
+ | `autoOpenEditor` | `boolean` | `false` | Buka editor otomatis |
2364
+
2365
+ Events: `update:modelValue`, `done` (File), `cancel`
2366
+
2367
+ ```vue
2368
+ <template>
2369
+ <DCodeImageCropperDialog
2370
+ v-model="cropperOpen"
2371
+ :file="selectedFile"
2372
+ :aspect-ratio="16/9"
2373
+ :output-width="1280"
2374
+ :output-height="720"
2375
+ @done="handleCropDone"
2376
+ />
2377
+ </template>
2378
+ ```
2379
+
2380
+ ---
2381
+
2382
+ ### `DCodeDialog`
2383
+
2384
+ Modal dialog yang dapat dikonfigurasi dengan slot.
2385
+
2386
+ | Prop | Tipe | Default | Deskripsi |
2387
+ |------|------|---------|-----------|
2388
+ | `modelValue` | `boolean` | `false` | Status buka/tutup (v-model) |
2389
+ | `title` | `string` | `''` | Judul dialog |
2390
+ | `icon` | `string` | `''` | Nama ikon Lucide di header |
2391
+ | `maxWidth` | `string\|number` | `500` | Lebar maksimum (px atau string CSS) |
2392
+ | `persistent` | `boolean` | `true` | Tidak tutup saat klik di luar |
2393
+ | `contentClass` | `string` | `'p-5'` | Class pada area konten |
2394
+ | `contentStyle` | `object\|string` | `{}` | Style tambahan konten |
2395
+
2396
+ ```vue
2397
+ <template>
2398
+ <DCodeDialog v-model="dialogOpen" title="Konfirmasi Hapus" icon="Trash2" :max-width="400">
2399
+ <p>Apakah Anda yakin ingin menghapus data ini?</p>
2400
+ <template #footer>
2401
+ <DCodeButton text="Batal" variant="outline" @click="dialogOpen = false" />
2402
+ <DCodeButton text="Hapus" variant="destructive" @click="confirmDelete" />
2403
+ </template>
2404
+ </DCodeDialog>
2405
+ </template>
2406
+ ```
2407
+
2408
+ ---
2409
+
2410
+ ### `DCodeDialogCloseBtn`
2411
+
2412
+ Tombol close berbentuk lingkaran untuk digunakan di dalam dialog.
2413
+
2414
+ | Prop | Tipe | Default | Deskripsi |
2415
+ |------|------|---------|-----------|
2416
+ | `icon` | `string` | `'X'` | Nama ikon Lucide |
2417
+ | `iconSize` | `string\|number` | `22` | Ukuran ikon |
2418
+
2419
+ ```vue
2420
+ <template>
2421
+ <DCodeDialogCloseBtn @click="dialogOpen = false" />
2422
+ </template>
2423
+ ```
2424
+
2425
+ ---
2426
+
2427
+ ### `DCodeIconDropdown`
2428
+
2429
+ Dropdown untuk memilih ikon dari library Lucide.
2430
+
2431
+ | Prop | Tipe | Default | Deskripsi |
2432
+ |------|------|---------|-----------|
2433
+ | `modelValue` | `string\|null` | — | Nama ikon terpilih (v-model) |
2434
+ | `label` | `string` | — | Label field |
2435
+ | `placeholder` | `string` | `'Pilih icon…'` | Placeholder |
2436
+ | `disabled` | `boolean` | `false` | Nonaktifkan |
2437
+ | `required` | `boolean` | `false` | Required |
2438
+ | `clearable` | `boolean` | `true` | Tombol clear |
2439
+ | `columns` | `number` | `4` | Jumlah kolom grid ikon |
2440
+ | `maxHeight` | `string` | `'335px'` | Tinggi maksimum dropdown |
2441
+ | `iconSize` | `number` | `20` | Ukuran ikon dalam grid (px) |
2442
+ | `pageSize` | `number` | `120` | Jumlah ikon per halaman |
2443
+
2444
+ ```vue
2445
+ <template>
2446
+ <DCodeIconDropdown
2447
+ v-model="form.icon"
2448
+ label="Pilih Ikon Menu"
2449
+ :columns="5"
2450
+ clearable
2451
+ />
2452
+ </template>
2453
+ ```
2454
+
2455
+ ---
2456
+
2457
+ ### `DCodeLabel`
2458
+
2459
+ Label teks dengan ikon opsional dan berbagai ukuran.
2460
+
2461
+ | Prop | Tipe | Default | Deskripsi |
2462
+ |------|------|---------|-----------|
2463
+ | `label` | `string` | — | Teks label (alias dari `text`) |
2464
+ | `text` | `string` | — | Teks label |
2465
+ | `textSize` | `'xs'\|'sm'\|'base'\|'lg'\|'xl'\|'2xl'\|'3xl'\|'4xl'\|'5xl'\|'6xl'` | `'xs'` | Ukuran teks |
2466
+ | `textType` | `'BOLD'\|'ITALIC'\|'REGULAR'` | `'REGULAR'` | Gaya teks |
2467
+ | `icon` | `string` | — | Nama ikon Lucide |
2468
+ | `iconClass` | `string` | — | Class tambahan untuk ikon |
2469
+
2470
+ ```vue
2471
+ <template>
2472
+ <DCodeLabel label="Total Transaksi" text-size="lg" text-type="BOLD" />
2473
+ <DCodeLabel text="Info" icon="Info" text-size="sm" />
2474
+ </template>
2475
+ ```
2476
+
2477
+ ---
2478
+
2479
+ ### `DCodeCard`
2480
+
2481
+ Kontainer kartu dengan header, subtitle, actions slot, dan footer slot.
2482
+
2483
+ | Prop | Tipe | Default | Deskripsi |
2484
+ |------|------|---------|-----------|
2485
+ | `level` | `1\|2\|3` | `1` | Level nesting (mempengaruhi background) |
2486
+ | `title` | `string` | — | Judul card |
2487
+ | `subtitle` | `string` | — | Subtitle card |
2488
+ | `shadow` | `boolean` | `false` | Tambahkan drop shadow |
2489
+ | `border` | `boolean` | `true` | Tampilkan border |
2490
+ | `padding` | `string` | `'p-6'` | Class padding (tailwind) |
2491
+
2492
+ Slots: `default` (konten), `header`, `actions`, `footer`
2493
+
2494
+ ```vue
2495
+ <template>
2496
+ <DCodeCard title="Data Pengguna" subtitle="Daftar semua user" shadow>
2497
+ <template #actions>
2498
+ <DCodeButton text="Tambah" icon="Plus" />
2499
+ </template>
2500
+
2501
+ <!-- Konten -->
2502
+ <p>Isi card di sini...</p>
2503
+
2504
+ <template #footer>
2505
+ <span class="text-sm text-gray-500">Updated 5 minutes ago</span>
2506
+ </template>
2507
+ </DCodeCard>
2508
+ </template>
2509
+ ```
2510
+
2511
+ ---
2512
+
2513
+ ### `DCodeProgressBar`
2514
+
2515
+ Progress bar global yang biasanya dipasang di layout untuk indikator loading API.
2516
+
2517
+ | Prop | Tipe | Default | Deskripsi |
2518
+ |------|------|---------|-----------|
2519
+ | `isApiLoading` | `boolean` | `false` | Tampilkan progress bar |
2520
+ | `apiProgress` | `number` | `0` | Persentase progress (0-100) |
2521
+
2522
+ ```vue
2523
+ <template>
2524
+ <!-- Biasanya di App.vue atau Layout -->
2525
+ <DCodeProgressBar :is-api-loading="isLoading" :api-progress="loadingProgress" />
2526
+ </template>
2527
+ ```
2528
+
2529
+ ---
2530
+
2531
+ ### `DCodeProgress`
2532
+
2533
+ Progress bar inline yang bisa digunakan di dalam konten.
2534
+
2535
+ | Prop | Tipe | Default | Deskripsi |
2536
+ |------|------|---------|-----------|
2537
+ | `modelValue` | `number` | `0` | Nilai progress (v-model) |
2538
+ | `max` | `number` | `100` | Nilai maksimum |
2539
+ | `label` | `string` | `''` | Label teks |
2540
+ | `showValue` | `boolean` | `false` | Tampilkan persentase |
2541
+ | `color` | `string` | `'blue'` | Warna bar (`'blue'`, `'green'`, `'red'`, `'yellow'`, `'purple'`, `'gray'`) |
2542
+ | `size` | `'sm'\|'md'\|'lg'` | `'md'` | Ketebalan bar |
2543
+ | `striped` | `boolean` | `false` | Tampilan garis-garis |
2544
+ | `animated` | `boolean` | `false` | Animasi gerak |
2545
+
2546
+ ```vue
2547
+ <template>
2548
+ <DCodeProgress
2549
+ :model-value="75"
2550
+ label="Progress Upload"
2551
+ color="green"
2552
+ size="lg"
2553
+ :show-value="true"
2554
+ animated
2555
+ />
2556
+ </template>
2557
+ ```
2558
+
2559
+ ---
2560
+
2561
+ ### `DCodeSlider`
2562
+
2563
+ Input slider (range) dengan label, min/max, dan mark.
2564
+
2565
+ | Prop | Tipe | Default | Deskripsi |
2566
+ |------|------|---------|-----------|
2567
+ | `modelValue` | `number` | `0` | Nilai slider (v-model) |
2568
+ | `min` | `number` | `0` | Nilai minimum |
2569
+ | `max` | `number` | `100` | Nilai maksimum |
2570
+ | `step` | `number` | `1` | Langkah perubahan |
2571
+ | `disabled` | `boolean` | `false` | Nonaktifkan |
2572
+ | `label` | `string` | `''` | Label |
2573
+ | `required` | `boolean` | `false` | Required |
2574
+ | `error` | `string\|null` | `null` | Error message |
2575
+ | `showValue` | `boolean` | `true` | Tampilkan nilai saat ini |
2576
+ | `valuePrefix` | `string` | `''` | Prefix nilai (e.g. `'Rp'`) |
2577
+ | `valueSuffix` | `string` | `''` | Suffix nilai (e.g. `'%'`) |
2578
+ | `marks` | `{ value: number; label: string }[]` | `[]` | Mark pada slider |
2579
+ | `color` | `string` | `'blue'` | Warna slider |
2580
+ | `size` | `'sm'\|'md'\|'lg'` | `'md'` | Ukuran track |
2581
+
2582
+ ```vue
2583
+ <template>
2584
+ <DCodeSlider
2585
+ v-model="form.diskon"
2586
+ label="Diskon"
2587
+ :min="0"
2588
+ :max="100"
2589
+ :step="5"
2590
+ value-suffix="%"
2591
+ color="green"
2592
+ :marks="[
2593
+ { value: 0, label: '0%' },
2594
+ { value: 50, label: '50%' },
2595
+ { value: 100, label: '100%' },
2596
+ ]"
2597
+ />
2598
+ </template>
2599
+ ```
2600
+
2601
+ ---
2602
+
2603
+ ### `DCodeChart`
2604
+
2605
+ Komponen chart berbasis Chart.js yang mendukung berbagai tipe grafik.
2606
+
2607
+ | Prop | Tipe | Default | Deskripsi |
2608
+ |------|------|---------|-----------|
2609
+ | `type` | `string` | `'line'` | Tipe chart: `'line'`, `'bar'`, `'pie'`, `'doughnut'` |
2610
+ | `modelValue` | `any` | — | Data chart (v-model) – format Chart.js |
2611
+ | `data` | `any` | — | Data chart (alternatif v-model) |
2612
+ | `options` | `any` | — | Opsi Chart.js custom |
2613
+ | `width` | `number` | `400` | Lebar canvas |
2614
+ | `height` | `number` | `300` | Tinggi canvas (px) |
2615
+ | `loading` | `boolean` | `false` | Status loading |
2616
+ | `error` | `string\|null` | `null` | Pesan error |
2617
+ | `showInfo` | `boolean` | `false` | Tampilkan info dataset/datapoints |
2618
+ | `responsive` | `boolean` | `true` | Responsif |
2619
+ | `maintainAspectRatio` | `boolean` | `false` | Pertahankan rasio aspek |
2620
+ | `theme` | `string` | `'light'` | Tema: `'light'` / `'dark'` |
2621
+ | `colors` | `string[]` | 10 warna default | Palet warna |
2622
+ | `label` | `string` | — | Label di atas chart |
2623
+ | `required` | `boolean` | `false` | Required indicator |
2624
+
2625
+ Events: `click`, `hover`, `ready`
2626
+
2627
+ ```vue
2628
+ <script setup>
2629
+ const chartData = {
2630
+ labels: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei'],
2631
+ datasets: [{
2632
+ label: 'Penjualan',
2633
+ data: [120, 190, 300, 250, 400],
2634
+ }]
2635
+ }
2636
+ </script>
2637
+
2638
+ <template>
2639
+ <DCodeChart
2640
+ type="bar"
2641
+ :model-value="chartData"
2642
+ :height="350"
2643
+ label="Laporan Penjualan"
2644
+ theme="light"
2645
+ />
2646
+ </template>
2647
+ ```
2648
+
2649
+ ---
2650
+
2651
+ ### `DCodeWizard`
2652
+
2653
+ Komponen wizard/stepper multi-langkah dengan navigasi dan loading data per tab.
2654
+
2655
+ | Prop | Tipe | Default | Deskripsi |
2656
+ |------|------|---------|-----------|
2657
+ | `tabs` | `WizardTab[]` | — | **Required.** Definisi tab/langkah |
2658
+ | `modelValue` | `number\|string` | — | Index/key tab aktif (v-model) |
2659
+ | `defaultTab` | `number` | — | Tab default yang aktif |
2660
+ | `allowTabClick` | `boolean` | — | Izinkan klik langsung ke tab |
2661
+ | `disabled` | `boolean` | — | Nonaktifkan navigasi |
2662
+ | `cache` | `boolean` | — | Cache data tab yang sudah dimuat |
2663
+ | `autoFetchOnMount` | `boolean` | — | Fetch data tab aktif saat mount |
2664
+ | `apiOptions` | `object\|Function` | — | Opsi API tambahan |
2665
+ | `isVertical` | `boolean` | — | Layout vertical |
2666
+ | `isWizard` | `boolean` | — | Mode wizard (step-by-step, tidak bisa loncat) |
2667
+ | `typeWizard` | `'number'\|'icon'\|'none'\|null` | — | Tipe indikator langkah |
2668
+ | `showNavigation` | `boolean` | — | Tampilkan tombol navigasi Prev/Next |
2669
+ | `submitText` | `string` | — | Teks tombol submit di langkah terakhir |
2670
+ | `loading` | `boolean` | — | Loading eksternal |
2671
+
2672
+ **`WizardTab` type:**
2673
+ ```ts
2674
+ type WizardTab = {
2675
+ key: string
2676
+ title: string
2677
+ description?: string
2678
+ icon?: Component
2679
+ disabled?: boolean
2680
+ endpoint?: { apiUrl?, apiMethod?, apiModule?, apiAction? }
2681
+ fetchData?: () => Promise<any>
2682
+ isDataTable?: boolean
2683
+ }
2684
+ ```
2685
+
2686
+ ```vue
2687
+ <script setup>
2688
+ import { ref } from 'vue'
2689
+ const steps = [
2690
+ { key: 'info', title: 'Info Dasar' },
2691
+ { key: 'detail', title: 'Detail' },
2692
+ { key: 'review', title: 'Review' },
2693
+ ]
2694
+ const activeStep = ref(0)
2695
+ </script>
2696
+
2697
+ <template>
2698
+ <DCodeWizard
2699
+ v-model="activeStep"
2700
+ :tabs="steps"
2701
+ is-wizard
2702
+ type-wizard="number"
2703
+ :show-navigation="true"
2704
+ submit-text="Selesai"
2705
+ @submit="handleSubmit"
2706
+ >
2707
+ <template #info>
2708
+ <!-- Konten step Info Dasar -->
2709
+ </template>
2710
+ <template #detail>
2711
+ <!-- Konten step Detail -->
2712
+ </template>
2713
+ <template #review>
2714
+ <!-- Konten step Review -->
2715
+ </template>
2716
+ </DCodeWizard>
2717
+ </template>
2718
+ ```
2719
+
2720
+ ---
2721
+
2722
+ ### `DCodeTimelineWithIcons`
2723
+
2724
+ Komponen timeline interaktif dengan berbagai tipe item (flight, interview, dll.).
2725
+
2726
+ Komponen ini merender timeline visual dari array item yang sudah terdefinisi di dalamnya. Cocok digunakan untuk menampilkan riwayat proses, activity log, atau alur perjalanan.
2727
+
2728
+ > ℹ️ Komponen ini tidak memiliki props eksternal — data dan tampilan dikonfigurasi secara internal. Gunakan sebagai komponen display/demo timeline.
2729
+
2730
+ ```vue
2731
+ <template>
2732
+ <DCodeTimelineWithIcons />
2733
+ </template>
2734
+ ```
2735
+
2736
+ ---
2737
+
2738
+ ## Lisensi
2739
+
2740
+ MIT © [Gema Fajar Ramadhan](https://github.com/gemafajarramadhan)