lib-pajakio-v2 1.1.10 → 1.1.11

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.
@@ -10,13 +10,379 @@ Props:
10
10
  | --- | --- | --- | --- |
11
11
  | `id` | `String` | `""` | ID table, dipakai jika `selectorId` kosong. |
12
12
  | `selectorId` | `String` | `""` | ID table untuk automation. Prioritas: `selectorId`, `id`, lalu ID instance otomatis `base-table-{instance}`. |
13
+ | `sticky` | `Boolean` | `false` | Membuat kolom pertama sticky di kiri dan kolom terakhir sticky di kanan. |
14
+ | `stickyLeft` | `Boolean` | `false` | Membuat hanya kolom pertama sticky di kiri. Ditulis `sticky-left` di template. |
15
+ | `stickyRight` | `Boolean` | `false` | Membuat kolom terakhir sticky di kanan; bersama `stickyRightCount: 2`, mengunci dua kolom terakhir. Ditulis `sticky-right` di template. |
16
+ | `stickyRightCount` | `Number` | `1` | Gunakan `2` bersama `stickyRight` untuk dua kolom sticky kanan pada table berbasis slot manual. Ditulis `:sticky-right-count="2"`. |
17
+ | `responsive` | `Boolean` | `false` | Membungkus table dengan `div.base-table-wrapper` yang memiliki scroll horizontal. Jika `false`, table dirender langsung tanpa wrapper. |
18
+ | `columns` | `Array` | `() => []` | Definisi kolom `{ key, title, draggable?, sortable?, width?, sticky?, align?, headerAlign? }`. `key` wajib unik; `width` berupa ukuran CSS, misalnya `"180px"`; `sticky` berupa `"left"` atau `"right"`. `align` dan `headerAlign` menerima `"left"`, `"right"`, atau `"center"`. Array kosong mempertahankan slot `thead` lama. |
19
+ | `reorderable` | `Boolean` | `false` | Mengaktifkan drag handle untuk kolom kecuali `draggable: false`. `draggable: true` juga mengaktifkan handle per kolom tanpa prop ini. |
20
+ | `storageKey` | `String` | `""` | Key localStorage khusus untuk urutan kolom. Jika kosong, memakai `base-table-columns:${selectorId || id || tableId}`. Ditulis `storage-key` di template. |
21
+ | `disableStorage` | `Boolean` | `false` | Menonaktifkan pembacaan, penyimpanan, dan penghapusan preferensi localStorage. Ditulis `disable-storage` di template. |
22
+ | `showFilter` | `Boolean` | `false` | Menampilkan `tr.base-table__filter-row` sebagai baris kedua di dalam `thead`, jika `columns` tidak kosong. |
23
+ | `darkHeader` | `Boolean` | `false` | Header navy `#0a1c2f`, teks dan ikon putih. Ditulis `dark-header` di template. |
24
+
25
+ Events:
26
+
27
+ | Event | Payload | Keterangan |
28
+ | --- | --- | --- |
29
+ | `update:columns` | Array kolom baru | Urutan baru setelah drag/drop, reorder keyboard, pemulihan storage saat mounted, atau `resetColumnOrder()`. Gunakan `v-model:columns` untuk menyinkronkan parent. Array prop tidak dimutasi. |
30
+ | `reorder-column` | `{ fromKey, toKey, side, columns }` | `side` adalah `"before"` atau `"after"` relatif terhadap kolom target. Tidak dipancarkan untuk operasi tanpa perubahan atau perpindahan yang ditolak. |
31
+ | `sort-column` | `{ column, direction }` | Klik tombol sort mengganti arah `"asc"` / `"desc"`. Parent bertanggung jawab mengurutkan data. |
13
32
 
14
33
  Slots:
15
34
 
16
35
  | Slot | Keterangan |
17
36
  | --- | --- |
18
- | `thead` | Header row. |
19
- | `tbody` | Body row. |
37
+ | `thead` | Isi header row lama berupa `th`; digunakan jika `columns` kosong. |
38
+ | `header-{key}` | Isi header kolom spesifik, scoped `{ column }`; misalnya `#header-checkbox`. Tidak perlu membungkus dengan `th`. |
39
+ | `header` | Fallback isi header untuk kolom tanpa slot `header-{key}`, scoped `{ column }`. Default menampilkan `column.title`. |
40
+ | `filter-{key}` | Isi sel filter spesifik, scoped `{ column }`; misalnya `#filter-nofa` atau `#filter-date`. Tidak perlu membungkus dengan `td`. |
41
+ | `filter` | Fallback isi filter untuk kolom tanpa slot `filter-{key}`, scoped `{ column }`. Default kosong. |
42
+ | `tbody` | Isi baris body berupa `tr`, scoped `{ columns, columnStyle, stickyClass, columnClasses }`. `columns` mengikuti urutan header/filter dan menyertakan metadata `style` (body), `headerStyle` (header), `stickyClass`, dan `classes`. Helper menerima object kolom. Slot lama tanpa scope tetap didukung. |
43
+
44
+ ### Perataan Header Kolom
45
+
46
+ Opsi `headerAlign` mengatur perataan header secara terpisah dari `align` untuk body. Keduanya menerima `"left"`, `"right"`, atau `"center"`. Jika `headerAlign` tidak didefinisikan, header mewarisi `align`; jika keduanya tidak didefinisikan, header rata kiri (`"left"`). Kolom dengan `key: "checkbox"` selalu memakai header rata tengah (`"center"`), termasuk jika `headerAlign` atau `align` diisi nilai lain.
47
+
48
+ ```js
49
+ columns: [
50
+ { key: "nofa", title: "Nomor Faktur", headerAlign: "left" },
51
+ { key: "status", title: "Status", headerAlign: "center" },
52
+ { key: "total", title: "Total", align: "right" },
53
+ { key: "tax", title: "Pajak", headerAlign: "left", align: "right" },
54
+ ],
55
+ ```
56
+
57
+ Pada contoh tersebut, header dan body `total` rata kanan, sedangkan header `tax` rata kiri dan body-nya rata kanan. Terapkan `column.style` atau helper `columnStyle(column)` pada sel body seperti contoh di bawah. Metadata `column.headerStyle` menyimpan style khusus header, termasuk lebar dan offset sticky; jangan gunakan metadata ini untuk body.
58
+
59
+ Slot `thead` manual juga mendukung `<th align="right">`, `<th align="left">`, dan `<th align="center">` secara native, termasuk konten pembungkus `<div>` di dalamnya. Class utility `text-right` / `align-right`, `text-left` / `align-left`, serta `text-center` / `align-center` juga didukung pada `th`.
60
+
61
+ ```vue
62
+ <base-table>
63
+ <template #thead>
64
+ <th align="left"><div>Nomor Faktur</div></th>
65
+ <th align="right"><div>Total</div></th>
66
+ </template>
67
+ <template #tbody>
68
+ <tr>
69
+ <td>010.000-26.00000001</td>
70
+ <td align="right">1.000.000</td>
71
+ </tr>
72
+ </template>
73
+ </base-table>
74
+ ```
75
+
76
+ ### Header Dinamis Dan Filter
77
+
78
+ ```vue
79
+ <base-table
80
+ v-model:columns="columns"
81
+ reorderable
82
+ dark-header
83
+ responsive
84
+ :show-filter="showFilter"
85
+ selector-id="invoice-table"
86
+ @sort-column="sortRows"
87
+ >
88
+ <template #header-checkbox>
89
+ <input v-model="allSelected" type="checkbox" aria-label="Pilih semua faktur" />
90
+ </template>
91
+ <template #filter-checkbox>
92
+ <button type="button" @click="resetFilters">Reset</button>
93
+ </template>
94
+ <template #filter-nofa="{ column }">
95
+ <input v-model="filters.nofa" :aria-label="column.title" placeholder="Cari nomor faktur" />
96
+ </template>
97
+ <template #filter-date="{ column }">
98
+ <input v-model="filters.date" type="date" :aria-label="column.title" />
99
+ </template>
100
+ <template #filter="{ column }">
101
+ <!-- Fallback opsional untuk kolom lain. -->
102
+ </template>
103
+ <template #tbody="{ columns: orderedColumns, columnStyle, stickyClass }">
104
+ <tr v-for="row in filteredRows" :key="row.id">
105
+ <td
106
+ v-for="column in orderedColumns"
107
+ :key="column.key"
108
+ :class="column.stickyClass || stickyClass(column)"
109
+ :style="column.style || columnStyle(column)"
110
+ >
111
+ <input v-if="column.key === 'checkbox'" v-model="row.selected"
112
+ type="checkbox" :aria-label="'Pilih ' + row.nofa" />
113
+ <button v-else-if="column.key === 'action'" type="button"
114
+ @click="openInvoice(row)">Lihat</button>
115
+ <template v-else>{{ row[column.key] }}</template>
116
+ </td>
117
+ </tr>
118
+ </template>
119
+ </base-table>
120
+ ```
121
+
122
+ Definisi state parent (misalnya dalam `data()`):
123
+
124
+ ```js
125
+ columns: [
126
+ { key: "checkbox", title: "Checkbox", width: "56px", sticky: "left", draggable: false },
127
+ { key: "nofa", title: "Nomor Faktur", width: "240px", draggable: true, sortable: true },
128
+ { key: "date", title: "Tanggal", width: "180px", sortable: true },
129
+ { key: "status", title: "Status Transaksi", width: "180px", sticky: "right", draggable: false, sortable: true },
130
+ { key: "action", title: "Aksi", width: "140px", sticky: "right", draggable: false },
131
+ ],
132
+ showFilter: true,
133
+ filters: { nofa: "", date: "" },
134
+ ```
135
+
136
+ - Drag handle `drag_indicator` berukuran 16px mendukung fokus keyboard. Tekan `ArrowLeft` / `ArrowRight` untuk memindahkan kolom satu posisi. Fokus tetap pada handle yang dipindahkan.
137
+ - Kolom `draggable: false` terkunci dan tidak dapat dilewati. Perpindahan antar-group sticky juga ditolak agar checkbox/aksi tetap pada posisinya.
138
+ - Saat drag melewati separuh kiri/kanan header target, indikator `.base-table-column-drop-before::before` / `.base-table-column-drop-after::after` menunjukkan sisi drop, berwarna `#63b3ff` dengan lebar 3px.
139
+ - Gunakan `columns` dari scoped slot `tbody`, bukan urutan array statis, agar body selalu sejajar. BaseTable tidak mengurutkan ulang DOM body yang ditulis manual.
140
+ - Perubahan prop `columns` dari parent memperbarui definisi kolom dan menerapkan kembali urutan tersimpan. `update:columns` menyinkronkan parent; `reorder-column` hanya menandakan perpindahan oleh pengguna.
141
+ - BaseTable hanya menyediakan tempat filter dan tombol sort; pencarian, reset, pemilihan tanggal, dropdown, serta sorting data dikelola parent. Contoh interaktif tersedia di `/table`.
142
+ - Sel filter memakai background `#ecf0f3`, padding `6px 12px`, tinggi sekitar 50px, dan border bawah `1px solid #d4d7db`. `column.sticky` memasang class dan offset sticky pada header dan filter secara otomatis; pasang metadata `column.stickyClass` dan `column.style` pada sel body.
143
+ - Filter kiri/kanan tetap sticky dengan background filter sendiri, termasuk saat hover. Header sticky tetap berwarna navy ketika `darkHeader` aktif.
144
+ - Untuk satu kolom sticky per sisi, offset adalah nol. Untuk beberapa kolom, offset kanan menjumlahkan lebar kolom sticky di kanannya; offset kiri menjumlahkan lebar kolom sticky di kirinya. Helper `stickyRightOffset` dan `stickyLeftOffset` menghasilkan string dengan satuan px. Gunakan lebar angka dalam px atau string seperti `"140px"` untuk perhitungan offset yang konsisten.
145
+ - Lebar yang tidak tersedia atau menggunakan satuan lain (misalnya `%` atau `rem`) memakai fallback `140px` dalam perhitungan offset, bukan hasil pengukuran DOM. Untuk beberapa kolom sticky, tentukan lebar px agar offset sesuai lebar sel sebenarnya.
146
+
147
+ ### Persistensi Urutan Kolom
148
+
149
+ Urutan hasil drag & drop maupun reorder keyboard otomatis disimpan ke localStorage sebagai JSON array string `key`. Saat instance dibuat, BaseTable memulihkan urutan tanpa mengubah object definisi kolom. Saat mounted, `update:columns` dipancarkan jika pemulihan mengubah urutan prop, sehingga `v-model:columns` parent ikut sinkron.
150
+
151
+ Key default adalah `base-table-columns:${selectorId || id || tableId}`, dengan `tableId` sebagai fallback ID instance otomatis. Gunakan `selector-id` atau `id` yang stabil dan unik per tabel agar preferensi tetap tepat setelah refresh; ID otomatis dapat berubah bila urutan pembuatan instance berubah. Prop `storage-key` mengganti key sepenuhnya, misalnya `storage-key="purchase-invoices:columns"`. Prop `disable-storage` mematikan persistensi.
152
+
153
+ Key kolom yang sudah dihapus diabaikan. Kolom baru disisipkan mengikuti posisi definisi saat ini tanpa menghilangkan preferensi kolom lama. Grup sticky kiri tetap di awal, kolom non-sticky di tengah, dan grup sticky kanan di akhir. JSON tidak valid, storage yang tidak tersedia atau ditolak browser, serta lingkungan SSR/Node tidak menyebabkan tabel gagal dirender.
154
+
155
+ Method publik `resetColumnOrder()` tersedia melalui ref komponen:
156
+
157
+ ```vue
158
+ <base-table ref="invoiceTableRef" :columns="defaultColumns" selector-id="invoice-table" />
159
+ <base-button variant="secondary" icon-left="restore"
160
+ @click="$refs.invoiceTableRef.resetColumnOrder()">
161
+ Reset Urutan Kolom
162
+ </base-button>
163
+ ```
164
+
165
+ Method ini menghapus entry storage, mengembalikan urutan ke salinan prop `columns` saat ini, dan memancarkan `update:columns`. Jika memakai `v-model:columns`, simpan definisi awal secara terpisah karena prop parent sudah mengikuti hasil reorder. Untuk kembali ke urutan awal, panggil `resetColumnOrder()`, lalu ganti array parent dengan salinan definisi awal. Demo Faktur Pembelian di `/table` menerapkan alur ini melalui tombol **Reset Urutan Kolom**.
166
+
167
+ ### Dua Kolom Sticky Kanan
168
+
169
+ Array `columns` pada contoh di atas mengunci Status Transaksi dan Aksi tanpa perlu prop `stickyRight`. Tempatkan kedua kolom di ujung kanan. Status memakai `right: 140px`, sedangkan Aksi memakai `right: 0px`. Lebar Aksi menjadi nilai CSS custom property `--base-table-sticky-right-2-offset` pada table, yang dipasang pada mode responsive maupun non-responsive.
170
+
171
+ Slot `tbody` menerima salinan kolom dengan `style` (lebar, min-width, dan offset) serta `stickyClass`. Class `sticky-right` menandai seluruh grup kanan; `sticky-right-2` menandai kolom kedua dari kanan, `sticky-right--outer` menandai kolom paling kiri grup, dan `sticky-right--last` menandai kolom paling kanan. Metadata dihitung ulang mengikuti urutan kolom dan tidak mengubah array prop.
172
+
173
+ Untuk table dengan slot manual, gunakan prop berikut:
174
+
175
+ ```vue
176
+ <base-table
177
+ :sticky-right="true"
178
+ :sticky-right-count="2"
179
+ responsive
180
+ style="min-width: 1200px; --base-table-sticky-right-2-offset: 140px"
181
+ >
182
+ <template #thead>
183
+ <th>Nomor Faktur</th>
184
+ <th style="width: 180px; min-width: 180px">Status Transaksi</th>
185
+ <th style="width: 140px; min-width: 140px">Aksi</th>
186
+ </template>
187
+ <template #tbody>
188
+ <tr>
189
+ <td>010.000-26.00000001</td>
190
+ <td>Upload Sukses</td>
191
+ <td style="width: 140px; min-width: 140px">...</td>
192
+ </tr>
193
+ </template>
194
+ </base-table>
195
+ ```
196
+
197
+ Alternatif tanpa prop, pasang `sticky-right-2` dan `sticky-right` pada `th` dan `td` yang bersesuaian:
198
+
199
+ ```html
200
+ <!-- Header -->
201
+ <th class="sticky-right-2" style="width: 180px; min-width: 180px">Status Transaksi</th>
202
+ <th class="sticky-right" style="width: 140px; min-width: 140px">Aksi</th>
203
+ <!-- Body -->
204
+ <td class="sticky-right-2">Upload Sukses</td>
205
+ <td class="sticky-right" style="width: 140px; min-width: 140px">...</td>
206
+ ```
207
+
208
+ Class `sticky-col-right-2` adalah alias `sticky-right-2`. Untuk slot manual, offset default kolom kedua adalah `140px`; jika lebar kolom Aksi berbeda, sesuaikan `--base-table-sticky-right-2-offset` pada table dan lebar sel Aksi. Border kiri `1px solid #d4d7db` dan shadow hanya berada pada kolom terluar grup (Status), sedangkan Aksi memakai `border-left: none` dan `box-shadow: none`. Garis pemisah vertikal antar-kolom sticky dihilangkan agar grup tampil menyatu sebagai satu panel, termasuk pada header, filter, body, dan baris anak accordion. Header dan filter sticky memakai `z-index: 4`, body memakai `z-index: 2`.
209
+
210
+ ### Kolom Sticky
211
+
212
+ Gunakan `sticky responsive` untuk checkbox di kiri dan aksi di kanan:
213
+
214
+ ```vue
215
+ <base-table sticky responsive style="min-width: 1200px">
216
+ <template #thead>
217
+ <th>Checkbox</th>
218
+ <th>Nomor Faktur</th>
219
+ <th>Lawan Transaksi</th>
220
+ <th>Aksi</th>
221
+ </template>
222
+ <template #tbody>
223
+ <tr :class="{ selected: selected }">
224
+ <td><form-checkbox v-model="selected" :value="true" size="small" /></td>
225
+ <td>010.000-26.00000001</td>
226
+ <td>PT Contoh Indonesia</td>
227
+ <td><base-icon iconName="more_horiz" /></td>
228
+ </tr>
229
+ </template>
230
+ </base-table>
231
+ ```
232
+
233
+ Prop parsial dapat dipakai terpisah atau bersamaan. `sticky` selalu mengaktifkan kedua sisi:
234
+
235
+ ```vue
236
+ <base-table sticky-left responsive>...</base-table>
237
+ <base-table sticky-right responsive>...</base-table>
238
+ <base-table sticky-left sticky-right responsive>...</base-table>
239
+ ```
240
+
241
+ Untuk memilih cell secara manual, pasang class pada `th` dan `td` yang bersesuaian:
242
+
243
+ ```vue
244
+ <base-table responsive style="min-width: 1200px">
245
+ <template #thead>
246
+ <th class="sticky-left">Checkbox</th>
247
+ <th>Nomor Faktur</th>
248
+ <th class="sticky-right">Aksi</th>
249
+ </template>
250
+ <template #tbody>
251
+ <tr>
252
+ <td class="sticky-left">...</td>
253
+ <td>010.000-26.00000001</td>
254
+ <td class="sticky-right">...</td>
255
+ </tr>
256
+ </template>
257
+ </base-table>
258
+ ```
259
+
260
+ Catatan styling dan integrasi:
261
+
262
+ - Alias class cell: `sticky-col-left`, `sticky-col-right`, dan `sticky-col-right-2`. Class `is-sticky` pada BaseTable mengaktifkan kedua kolom tepi seperti prop `sticky`.
263
+ - Mode sticky memakai `border-collapse: separate` dan `border-spacing: 0` untuk menjaga border cell saat scroll. Tanpa sticky, table tetap memakai `border-collapse: collapse`.
264
+ - Cell sticky memiliki background putih, `z-index: 2`, dan header sticky `z-index: 4`. Background mengikuti baris hover (`#f5f6f7`); baris `.selected` atau baris dengan checkbox checked memakai `#ebf2f9`, termasuk saat hover. Warna ini adalah warna solid dari `#3e7dc01a` di atas putih, sehingga konten kolom non-sticky tidak tembus melalui sel sticky saat scroll horizontal.
265
+ - Pembatas memakai `#d4d7db`. Shadow kanan `-4px 0 8px rgba(0, 0, 0, 0.05)` berada pada kolom terluar grup sticky. Untuk satu kolom kanan, shadow berada pada kolom terakhir; untuk dua kolom, shadow berpindah ke kolom kedua dari kanan. Sisi kiri memakai shadow yang dicerminkan.
266
+ - Untuk dua kolom sticky kiri dengan class table `base-table--sticky-left-2` atau penanda cell `sticky-left--outer` / `sticky-left-2` pada kolom kedua, kolom pertama memakai `border-right: none` dan `box-shadow: none`. Hanya kolom kedua memiliki border kanan `1px solid #d4d7db` dan shadow `4px 0 8px rgba(0, 0, 0, 0.05)`, sehingga tidak ada garis pemisah di dalam panel sticky kiri pada header, filter, body, maupun baris anak accordion. Aturan ini hanya mengatur pembatas; offset kiri tetap mengikuti metadata `columns` atau style manual.
267
+ - Wrapper memiliki `width: 100%`, `overflow-x: auto`, `position: relative`, touch scrolling, dan scrollbar tipis berwarna `#cbd5e1 #f8fafc`.
268
+ - `responsive` tidak memaksa lebar kolom. Atur `min-width` table atau cell sesuai konten agar tabel lebar dapat di-scroll. Class dan style dari consumer tetap diterapkan pada table.
269
+ - Tanpa `responsive`, sediakan container scroll sendiri bila diperlukan. Sticky mengikuti ancestor scroll terdekat.
270
+ - Class manual `sticky-left` dan `sticky-right` memakai offset nol. `sticky-right-2` memakai offset dari `--base-table-sticky-right-2-offset` dengan fallback `140px`. Beberapa kolom kiri manual tetap membutuhkan offset `left` tambahan; array `columns` menghitungnya otomatis.
271
+ - Semua prop Boolean default `false` dan `columns` default array kosong, sehingga pemakaian lama tetap tanpa wrapper, tanpa sticky, dan memakai slot `thead`.
272
+
273
+ ### Baris Collapsible / Accordion Riwayat
274
+
275
+ `BaseTable` menyediakan styling accordion melalui slot `tbody`, bukan prop data atau state expand internal. Parent menyimpan `expandedRows`, menyediakan `history`, dan merender child tepat setelah parent-nya. Contoh berikut membuka baris pertama secara default:
276
+
277
+ ```vue
278
+ <template>
279
+ <div>
280
+ <button type="button" @click="expandAll">Buka Semua</button>
281
+ <button type="button" @click="expandedRows = []">Tutup Semua</button>
282
+ </div>
283
+ <base-table :columns="columns" responsive style="min-width: 680px">
284
+ <template #tbody="{ columns: orderedColumns, columnStyle, stickyClass }">
285
+ <template v-for="row in rows" :key="row.id">
286
+ <tr :class="{ selected: row.selected }">
287
+ <td
288
+ v-for="column in orderedColumns"
289
+ :key="column.key"
290
+ :data-column-key="column.key"
291
+ :class="column.stickyClass || stickyClass(column)"
292
+ :style="column.style || columnStyle(column)"
293
+ >
294
+ <span v-if="column.key === 'checkbox'" class="base-table-checkbox-cell">
295
+ <button
296
+ v-if="row.history?.length"
297
+ type="button"
298
+ class="base-table-expand-btn"
299
+ :aria-expanded="expandedRows.includes(row.id)"
300
+ :aria-label="`${expandedRows.includes(row.id) ? 'Tutup' : 'Buka'} riwayat ${row.nofa}`"
301
+ :title="expandedRows.includes(row.id) ? 'Tutup riwayat' : 'Buka riwayat'"
302
+ @click="toggleRow(row)"
303
+ >
304
+ <base-icon
305
+ :icon-name="expandedRows.includes(row.id) ? 'keyboard_arrow_down' : 'keyboard_arrow_right'"
306
+ :width="20"
307
+ :height="20"
308
+ color="#247AC0"
309
+ />
310
+ </button>
311
+ <span v-else class="base-table-expand-btn--placeholder" aria-hidden="true" />
312
+ <input v-model="row.selected" type="checkbox" :aria-label="`Pilih ${row.nofa}`" />
313
+ </span>
314
+ <template v-else>{{ row[column.key] }}</template>
315
+ </td>
316
+ </tr>
317
+ <template v-if="expandedRows.includes(row.id)">
318
+ <tr
319
+ v-for="history in row.history"
320
+ :key="`${row.id}-${history.id}`"
321
+ class="accordion-child base-table__row-child"
322
+ >
323
+ <td
324
+ v-for="column in orderedColumns"
325
+ :key="column.key"
326
+ :data-column-key="column.key"
327
+ :class="column.stickyClass || stickyClass(column)"
328
+ :style="column.style || columnStyle(column)"
329
+ >
330
+ <template v-if="column.key !== 'checkbox'">{{ history[column.key] }}</template>
331
+ </td>
332
+ </tr>
333
+ </template>
334
+ </template>
335
+ </template>
336
+ </base-table>
337
+ </template>
338
+
339
+ <script>
340
+ import { BaseIcon, BaseTable } from "lib-pajakio-v2";
341
+ import "lib-pajakio-v2/dist/lib-pajakio-v2.css";
342
+
343
+ export default {
344
+ components: { BaseIcon, BaseTable },
345
+ data() {
346
+ return {
347
+ columns: [
348
+ { key: "checkbox", title: "", width: "72px", sticky: "left", draggable: false },
349
+ { key: "nofa", title: "Nomor Bukti Potong", width: "240px" },
350
+ { key: "status", title: "Status", width: "180px", sticky: "right", draggable: false },
351
+ ],
352
+ expandedRows: ["bupot-1"],
353
+ rows: [
354
+ {
355
+ id: "bupot-1", nofa: "Belum Tersedia", status: "Upload Gagal", selected: false,
356
+ history: [{ id: "history-1", nofa: "26000HQ0A", status: "Upload Sukses" }],
357
+ },
358
+ { id: "bupot-2", nofa: "26000HPZH", status: "Upload Sukses", selected: false },
359
+ ],
360
+ };
361
+ },
362
+ methods: {
363
+ toggleRow(row) {
364
+ if (!row.history?.length) return;
365
+ this.expandedRows = this.expandedRows.includes(row.id)
366
+ ? this.expandedRows.filter(id => id !== row.id)
367
+ : [...this.expandedRows, row.id];
368
+ },
369
+ expandAll() {
370
+ this.expandedRows = this.rows.filter(row => row.history?.length).map(row => row.id);
371
+ },
372
+ },
373
+ };
374
+ </script>
375
+ ```
376
+
377
+ - Gunakan ID parent yang unik dan stabil. `nofa` juga dapat dipakai jika unik; beberapa dokumen dengan nilai `"Belum Tersedia"` harus memakai ID berbeda. Gunakan key child yang unik dalam satu parent.
378
+ - `.base-table-checkbox-cell` mengelompokkan chevron 20px dan checkbox dengan gap 4px. Lebar kolom `72px` memberi ruang keduanya. Jangan pasang class ini langsung pada `td`, agar layout table dan sticky tetap normal.
379
+ - Baris tanpa history memakai `.base-table-expand-btn--placeholder` berukuran 20px, tersembunyi dan tidak menerima interaksi, agar checkbox tetap sejajar. Klik chevron tidak mengubah pilihan checkbox.
380
+ - Tombol native dengan `type="button"` mendukung Enter/Space dan tidak melakukan submit form. Pasang `aria-expanded`, label, serta tooltip sesuai state. Warna ikon `#247AC0`; hover tombol `#f0f7fd` dan fokus keyboard memiliki outline biru.
381
+ - Pasang `accordion-child`, `base-table__row-child`, atau `base-table__row-detail` pada `tr` child; ketiganya alias styling yang sama. Sel child memakai font 13px, warna `#526474`, dan border bawah `1px solid #eef2f6`.
382
+ - Background child normal `#f8fafc`, hover `#f1f6fb`, dan `.selected` atau baris dengan checkbox checked `#e5edf6`, termasuk saat hover. `--base-table-sticky-background` diwariskan dari baris ke sel sehingga background sticky kiri, kanan, dan kedua dari kanan tetap sinkron saat scroll horizontal. Warna selected child juga solid (tanpa transparansi), sehingga konten di belakang sel sticky tidak tembus. Tidak perlu memberi background putih atau custom property per sel.
383
+ - Parent dan child harus mengulang `columns` dari scoped slot dan menerapkan `column.style || columnStyle(column)` serta `column.stickyClass || stickyClass(column)` pada setiap `td`. Ini menjaga urutan dan offset kiri/kanan tetap sejajar setelah reorder. Sel checkbox/aksi child boleh kosong, tetapi jangan dihilangkan karena jumlah kolom harus sama.
384
+ - Terapkan filter dan sort pada array parent, bukan array yang sudah di-flatten dengan history. Render history milik parent yang lolos filter tepat di bawahnya; jangan masukkan history ke hitungan pagination atau pilihan semua. State expand tetap tersimpan walaupun parent sementara tersembunyi oleh filter.
385
+ - `expandAll()` di atas membuka semua parent yang memiliki history, termasuk yang sedang terfilter; `expandedRows = []` menutup semuanya. Demo `/table` menyediakan tombol toggle Buka Semua / Tutup Semua serta contoh filter, sort, dan sticky kiri/kanan.
20
386
 
21
387
  ## `TablePagination`
22
388
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lib-pajakio-v2",
3
- "version": "1.1.10",
3
+ "version": "1.1.11",
4
4
  "private": false,
5
5
  "main": "./dist/lib-pajakio-v2.common.js",
6
6
  "scripts": {
@@ -0,0 +1,40 @@
1
+ .library-tooltip {
2
+ position: relative;
3
+ z-index: 99;
4
+ }
5
+ .library-tooltip:before {
6
+ display: block;
7
+ opacity: 0;
8
+ pointer-events: none;
9
+ position: absolute;
10
+ transform: translate3d(0,-10px,0);
11
+ transition: all .15s ease-in-out;
12
+ }
13
+ .library-tooltip:after {
14
+ display: none;
15
+ content: none;
16
+ }
17
+ .library-tooltip:before {
18
+ background: #FFFFFF;
19
+ border: 1px solid #D4D7DB;
20
+ border-radius: 6px;
21
+ -webkit-backdrop-filter: blur(2px);
22
+ backdrop-filter: blur(2px);
23
+ box-shadow: 0px 1px 6px 0px #0B3C5D1F;
24
+ color: #272B30;
25
+ content: attr(data-title);
26
+ font-family: 'Open Sans', sans-serif;
27
+ font-size: 12px;
28
+ font-weight: 400;
29
+ line-height: 16px;
30
+ letter-spacing: 0em;
31
+ padding: 4px 6px;
32
+ top: calc(100% + 4px);
33
+ left: 0;
34
+ max-width: 300px;
35
+ width: max-content;
36
+ }
37
+ .library-tooltip:hover:before {
38
+ opacity: 1;
39
+ transform: translate3d(0,0,0);
40
+ }
@@ -1,4 +1,5 @@
1
1
  @use '_color.scss' as *;
2
+ @use '_tooltip.scss';
2
3
  @import url("https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,600,700,800");
3
4
 
4
5
  body,
@@ -955,47 +956,7 @@ $lineH: 10;
955
956
  cursor: not-allowed;
956
957
  }
957
958
 
958
- // TOOLTIP
959
- .library-tooltip {
960
- position: relative;
961
- z-index: 99;
962
- }
963
- .library-tooltip:before,
964
- .library-tooltip:after {
965
- display: block;
966
- opacity: 0;
967
- pointer-events: none;
968
- position: absolute;
969
- transform: translate3d(0,-10px,0);
970
- transition: all .15s ease-in-out;
971
- }
972
- .library-tooltip:after {
973
- border-right: 6px solid transparent;
974
- border-bottom: 6px solid rgba(0,0,0,.75);
975
- border-left: 6px solid transparent;
976
- content: "";
977
- height: 0;
978
- top: 20px;
979
- left: 20px;
980
- width: 0;
981
- }
982
- .library-tooltip:before {
983
- background: rgba(0,0,0,.75);
984
- border-radius: 2px;
985
- color: #fff;
986
- content: attr(data-title);
987
- font-size: 14px;
988
- padding: 6px 10px;
989
- top: 26px;
990
- max-width: 300px;
991
- width: max-content;
992
- font-weight: 300;
993
- }
994
- .library-tooltip:hover::after,
995
- .library-tooltip:hover:before {
996
- opacity: 1;
997
- transform: translate3d(0,0,0);
998
- }
959
+ // TOOLTIP styles are loaded from _tooltip.scss above.
999
960
 
1000
961
  // BOS-SHADOW
1001
962
  .box-shadow {
@@ -1005,34 +966,124 @@ $lineH: 10;
1005
966
  }
1006
967
 
1007
968
  // BADGE
1008
- .tstatus {
1009
- padding: 3px 8px;
969
+ .tstatus,
970
+ .status-badge {
1010
971
  border-radius: 4px;
972
+ display: inline-block;
973
+ font-family: "Open Sans", sans-serif;
974
+ font-size: 12px;
975
+ font-weight: 700;
976
+ line-height: 1;
977
+ padding: 2px 4px;
978
+ white-space: nowrap;
1011
979
  width: fit-content;
1012
980
 
1013
- &-grey {
1014
- background: #b3b3b4;
1015
- color: #FFFFFF;
981
+ &.tstatus-grey,
982
+ &.status-badge--neutral {
983
+ background: #d4d7db;
984
+ color: #383e45;
985
+ }
986
+
987
+ &.tstatus-yellow,
988
+ &.status-badge--warning {
989
+ background: #fde8b3;
990
+ color: #805b00;
991
+ }
992
+
993
+ &.tstatus-red,
994
+ &.status-badge--danger {
995
+ background: #f6c0bc;
996
+ color: #701811;
1016
997
  }
1017
998
 
1018
- &-yellow {
1019
- background: #ffa000;
1020
- color: #FFFFFF;
999
+ &.tstatus-green,
1000
+ &.status-badge--success {
1001
+ background: #b7e5ce;
1002
+ color: #08542f;
1021
1003
  }
1022
1004
 
1023
- &-blue {
1024
- background: #3e7dc0;
1025
- color: #FFFFFF;
1005
+ &.tstatus-blue,
1006
+ &.status-badge--blue {
1007
+ background: #b8e5f6;
1008
+ color: #095571;
1026
1009
  }
1027
1010
 
1028
- &-red {
1029
- background: #d32f2f;
1030
- color: #FFFFFF;
1011
+ &.status-badge--with-icon,
1012
+ &.tstatus--with-icon {
1013
+ align-items: center;
1014
+ border: 1px solid transparent;
1015
+ display: inline-flex;
1016
+ overflow: hidden;
1017
+ padding: 0;
1018
+
1019
+ .status-badge__icon,
1020
+ .tstatus__icon {
1021
+ align-items: center;
1022
+ display: inline-flex;
1023
+ justify-content: center;
1024
+ padding: 4px;
1025
+ }
1026
+
1027
+ .status-badge__text,
1028
+ .tstatus__text {
1029
+ font-weight: 700;
1030
+ line-height: 1;
1031
+ padding: 2px 4px;
1032
+ }
1031
1033
  }
1032
1034
 
1033
- &-green {
1034
- background: #43A047;
1035
- color: #FFFFFF;
1035
+ &.status-badge--with-icon,
1036
+ &.tstatus--with-icon {
1037
+ &.tstatus-yellow,
1038
+ &.status-badge--warning {
1039
+ border-color: #805b00;
1040
+
1041
+ .status-badge__icon,
1042
+ .tstatus__icon {
1043
+ background: #805b00;
1044
+ color: #fde8b3;
1045
+ }
1046
+
1047
+ .status-badge__text,
1048
+ .tstatus__text {
1049
+ background: #fde8b3;
1050
+ color: #805b00;
1051
+ }
1052
+ }
1053
+
1054
+ &.tstatus-red,
1055
+ &.status-badge--danger {
1056
+ border-color: #701811;
1057
+
1058
+ .status-badge__icon,
1059
+ .tstatus__icon {
1060
+ background: #701811;
1061
+ color: #f6c0bc;
1062
+ }
1063
+
1064
+ .status-badge__text,
1065
+ .tstatus__text {
1066
+ background: #f6c0bc;
1067
+ color: #701811;
1068
+ }
1069
+ }
1070
+
1071
+ &.tstatus-green,
1072
+ &.status-badge--success {
1073
+ border-color: #08542f;
1074
+
1075
+ .status-badge__icon,
1076
+ .tstatus__icon {
1077
+ background: #08542f;
1078
+ color: #b7e5ce;
1079
+ }
1080
+
1081
+ .status-badge__text,
1082
+ .tstatus__text {
1083
+ background: #b7e5ce;
1084
+ color: #08542f;
1085
+ }
1086
+ }
1036
1087
  }
1037
1088
  }
1038
1089
 
@@ -1141,4 +1192,4 @@ $lineH: 10;
1141
1192
  100% {
1142
1193
  transform: translateY(0) scaleX(1) scaleY(1);
1143
1194
  }
1144
- }
1195
+ }