@ttdk-design-system/library-react 0.0.0-stage → 1.0.1

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.
Files changed (184) hide show
  1. package/AI_GUIDE.md +6518 -0
  2. package/README.md +106 -2
  3. package/llms.txt +6518 -0
  4. package/package.json +1099 -4
  5. package/src/core/index.ts +2 -0
  6. package/src/core/tailwind-preset.cjs +62 -0
  7. package/src/core/tailwind-preset.ts +85 -0
  8. package/src/core/theme/tokens.css +663 -0
  9. package/src/core/types.ts +836 -0
  10. package/src/index.ts +17 -0
  11. package/src/tailwind-preset.cjs +62 -0
  12. package/src/tailwind-preset.ts +85 -0
  13. package/src/types.ts +1 -0
  14. package/src/web/accordion/AI_GUIDE.md +149 -0
  15. package/src/web/accordion/accordion.css +1 -0
  16. package/src/web/accordion/accordion.tsx +430 -0
  17. package/src/web/accordion/index.ts +3 -0
  18. package/src/web/accordion/manifest.json +351 -0
  19. package/src/web/alert/AI_GUIDE.md +139 -0
  20. package/src/web/alert/alert.css +1 -0
  21. package/src/web/alert/alert.tsx +318 -0
  22. package/src/web/alert/index.ts +3 -0
  23. package/src/web/alert/manifest.json +240 -0
  24. package/src/web/avatar/AI_GUIDE.md +118 -0
  25. package/src/web/avatar/avatar.css +1 -0
  26. package/src/web/avatar/avatar.tsx +233 -0
  27. package/src/web/avatar/index.ts +3 -0
  28. package/src/web/avatar/manifest.json +195 -0
  29. package/src/web/badge/AI_GUIDE.md +94 -0
  30. package/src/web/badge/badge.css +1 -0
  31. package/src/web/badge/badge.tsx +140 -0
  32. package/src/web/badge/index.ts +2 -0
  33. package/src/web/badge/manifest.json +141 -0
  34. package/src/web/banner/AI_GUIDE.md +130 -0
  35. package/src/web/banner/banner.css +1 -0
  36. package/src/web/banner/banner.tsx +334 -0
  37. package/src/web/banner/index.ts +3 -0
  38. package/src/web/banner/manifest.json +240 -0
  39. package/src/web/breadcrumb/AI_GUIDE.md +120 -0
  40. package/src/web/breadcrumb/breadcrumb.css +1 -0
  41. package/src/web/breadcrumb/breadcrumb.tsx +306 -0
  42. package/src/web/breadcrumb/index.ts +3 -0
  43. package/src/web/breadcrumb/manifest.json +225 -0
  44. package/src/web/button/AI_GUIDE.md +121 -0
  45. package/src/web/button/button.css +1 -0
  46. package/src/web/button/button.tsx +287 -0
  47. package/src/web/button/index.ts +2 -0
  48. package/src/web/button/manifest.json +177 -0
  49. package/src/web/calendar/AI_GUIDE.md +151 -0
  50. package/src/web/calendar/calendar.css +1 -0
  51. package/src/web/calendar/calendar.tsx +1611 -0
  52. package/src/web/calendar/index.ts +3 -0
  53. package/src/web/calendar/manifest.json +470 -0
  54. package/src/web/card/AI_GUIDE.md +133 -0
  55. package/src/web/card/card.css +1 -0
  56. package/src/web/card/card.tsx +408 -0
  57. package/src/web/card/index.ts +4 -0
  58. package/src/web/card/manifest.json +357 -0
  59. package/src/web/checkbox/AI_GUIDE.md +146 -0
  60. package/src/web/checkbox/checkbox.css +1 -0
  61. package/src/web/checkbox/checkbox.tsx +307 -0
  62. package/src/web/checkbox/index.ts +2 -0
  63. package/src/web/checkbox/manifest.json +294 -0
  64. package/src/web/chip/AI_GUIDE.md +186 -0
  65. package/src/web/chip/chip.css +1 -0
  66. package/src/web/chip/chip.tsx +230 -0
  67. package/src/web/chip/index.ts +2 -0
  68. package/src/web/chip/manifest.json +186 -0
  69. package/src/web/dropdown/AI_GUIDE.md +167 -0
  70. package/src/web/dropdown/dropdown.css +1 -0
  71. package/src/web/dropdown/dropdown.tsx +1274 -0
  72. package/src/web/dropdown/index.ts +3 -0
  73. package/src/web/dropdown/manifest.json +576 -0
  74. package/src/web/flex/AI_GUIDE.md +113 -0
  75. package/src/web/flex/flex-item.tsx +97 -0
  76. package/src/web/flex/flex.css +1 -0
  77. package/src/web/flex/flex.tsx +108 -0
  78. package/src/web/flex/index.ts +5 -0
  79. package/src/web/flex/manifest.json +162 -0
  80. package/src/web/form/index.ts +7 -0
  81. package/src/web/grid/AI_GUIDE.md +112 -0
  82. package/src/web/grid/grid-item.tsx +112 -0
  83. package/src/web/grid/grid.css +1 -0
  84. package/src/web/grid/grid.tsx +99 -0
  85. package/src/web/grid/index.ts +5 -0
  86. package/src/web/grid/manifest.json +153 -0
  87. package/src/web/index.ts +86 -0
  88. package/src/web/input/AI_GUIDE.md +159 -0
  89. package/src/web/input/index.ts +3 -0
  90. package/src/web/input/input.css +1 -0
  91. package/src/web/input/input.tsx +537 -0
  92. package/src/web/input/manifest.json +465 -0
  93. package/src/web/input-number/AI_GUIDE.md +164 -0
  94. package/src/web/input-number/index.ts +3 -0
  95. package/src/web/input-number/input-number.css +1 -0
  96. package/src/web/input-number/input-number.tsx +1193 -0
  97. package/src/web/input-number/manifest.json +575 -0
  98. package/src/web/modal/AI_GUIDE.md +171 -0
  99. package/src/web/modal/index.ts +3 -0
  100. package/src/web/modal/manifest.json +375 -0
  101. package/src/web/modal/modal.css +1 -0
  102. package/src/web/modal/modal.tsx +571 -0
  103. package/src/web/pagination/AI_GUIDE.md +148 -0
  104. package/src/web/pagination/index.ts +21 -0
  105. package/src/web/pagination/manifest.json +432 -0
  106. package/src/web/pagination/pagination-content.tsx +45 -0
  107. package/src/web/pagination/pagination-ellipsis.tsx +54 -0
  108. package/src/web/pagination/pagination-first.tsx +155 -0
  109. package/src/web/pagination/pagination-item.tsx +45 -0
  110. package/src/web/pagination/pagination-last.tsx +155 -0
  111. package/src/web/pagination/pagination-link.tsx +121 -0
  112. package/src/web/pagination/pagination-next.tsx +158 -0
  113. package/src/web/pagination/pagination-previous.tsx +158 -0
  114. package/src/web/pagination/pagination.css +1 -0
  115. package/src/web/pagination/pagination.tsx +687 -0
  116. package/src/web/popover/AI_GUIDE.md +163 -0
  117. package/src/web/popover/index.ts +3 -0
  118. package/src/web/popover/manifest.json +396 -0
  119. package/src/web/popover/popover.css +1 -0
  120. package/src/web/popover/popover.tsx +702 -0
  121. package/src/web/radio/AI_GUIDE.md +155 -0
  122. package/src/web/radio/index.ts +2 -0
  123. package/src/web/radio/manifest.json +294 -0
  124. package/src/web/radio/radio.css +1 -0
  125. package/src/web/radio/radio.tsx +290 -0
  126. package/src/web/selectbox/AI_GUIDE.md +216 -0
  127. package/src/web/selectbox/index.ts +3 -0
  128. package/src/web/selectbox/manifest.json +477 -0
  129. package/src/web/selectbox/selectbox.css +1 -0
  130. package/src/web/selectbox/selectbox.tsx +1048 -0
  131. package/src/web/sonner/AI_GUIDE.md +98 -0
  132. package/src/web/sonner/index.ts +114 -0
  133. package/src/web/sonner/manifest.json +132 -0
  134. package/src/web/sonner/sonner.css +1 -0
  135. package/src/web/sonner/sonner.tsx +177 -0
  136. package/src/web/table/AI_GUIDE.md +145 -0
  137. package/src/web/table/index.ts +35 -0
  138. package/src/web/table/manifest.json +168 -0
  139. package/src/web/table/table-body.tsx +45 -0
  140. package/src/web/table/table-caption.tsx +45 -0
  141. package/src/web/table/table-cell.tsx +74 -0
  142. package/src/web/table/table-empty.tsx +108 -0
  143. package/src/web/table/table-footer.tsx +45 -0
  144. package/src/web/table/table-head.tsx +80 -0
  145. package/src/web/table/table-header.tsx +45 -0
  146. package/src/web/table/table-row.tsx +69 -0
  147. package/src/web/table/table.css +1 -0
  148. package/src/web/table/table.tsx +305 -0
  149. package/src/web/tabs/AI_GUIDE.md +150 -0
  150. package/src/web/tabs/index.ts +3 -0
  151. package/src/web/tabs/manifest.json +315 -0
  152. package/src/web/tabs/tabs.css +1 -0
  153. package/src/web/tabs/tabs.tsx +332 -0
  154. package/src/web/text/AI_GUIDE.md +129 -0
  155. package/src/web/text/index.ts +3 -0
  156. package/src/web/text/manifest.json +195 -0
  157. package/src/web/text/text.css +1 -0
  158. package/src/web/text/text.tsx +689 -0
  159. package/src/web/textarea/AI_GUIDE.md +141 -0
  160. package/src/web/textarea/index.ts +3 -0
  161. package/src/web/textarea/manifest.json +438 -0
  162. package/src/web/textarea/textarea.css +1 -0
  163. package/src/web/textarea/textarea.tsx +467 -0
  164. package/src/web/tooltip/AI_GUIDE.md +110 -0
  165. package/src/web/tooltip/index.ts +3 -0
  166. package/src/web/tooltip/manifest.json +222 -0
  167. package/src/web/tooltip/tooltip.css +1 -0
  168. package/src/web/tooltip/tooltip.tsx +423 -0
  169. package/src/webview/bottomsheet/AI_GUIDE.md +141 -0
  170. package/src/webview/bottomsheet/bottomsheet.css +1 -0
  171. package/src/webview/bottomsheet/bottomsheet.tsx +603 -0
  172. package/src/webview/bottomsheet/index.ts +4 -0
  173. package/src/webview/bottomsheet/manifest.json +377 -0
  174. package/src/webview/button/AI_GUIDE.md +79 -0
  175. package/src/webview/button/button-ios.css +1 -0
  176. package/src/webview/button/button-ios.tsx +138 -0
  177. package/src/webview/button/button-material.css +1 -0
  178. package/src/webview/button/button-material.tsx +138 -0
  179. package/src/webview/button/index.ts +7 -0
  180. package/src/webview/button/manifest.json +148 -0
  181. package/src/webview/index.ts +7 -0
  182. package/tailwind-preset.cjs +62 -0
  183. package/tailwind-preset.js +62 -0
  184. package/ttdk-manifest.json +10188 -0
@@ -0,0 +1,1048 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { useState, useRef, useEffect } from "react";
4
+
5
+ export type SelectboxUiProps = UiProps;
6
+ export interface SelectboxOption {
7
+ /**
8
+ * Teks label tampilan pilihan dropdown.
9
+ */
10
+ label: string;
11
+
12
+ /**
13
+ * Nilai unik (value) dari opsi pilihan.
14
+ */
15
+ value: string | number;
16
+
17
+ /**
18
+ * Status apakah opsi ini dinonaktifkan dari pemilihan.
19
+ * @default false
20
+ */
21
+ disabled?: boolean;
22
+ }
23
+ export interface SelectboxProps {
24
+ /**
25
+ * Teks label judul field yang ditampilkan di atas selectbox (14px Semibold, #141313).
26
+ */
27
+ label?: string;
28
+
29
+ /**
30
+ * Style kustom untuk elemen label judul field (UiProps).
31
+ */
32
+ labelStyle?: UiProps;
33
+
34
+ /**
35
+ * Class CSS kustom untuk elemen label judul field.
36
+ */
37
+ labelClass?: string;
38
+
39
+ /**
40
+ * Style kustom untuk elemen trigger kontrol selectbox (UiProps).
41
+ */
42
+ controlStyle?: UiProps;
43
+
44
+ /**
45
+ * Class CSS kustom untuk elemen trigger kontrol selectbox.
46
+ */
47
+ controlClass?: string;
48
+
49
+ /**
50
+ * Style kustom untuk elemen menu dropdown popover (UiProps).
51
+ */
52
+ menuStyle?: UiProps;
53
+
54
+ /**
55
+ * Class CSS kustom untuk elemen menu dropdown popover.
56
+ */
57
+ menuClass?: string;
58
+
59
+ /**
60
+ * Alias untuk menuStyle (konsistensi dengan Dropdown listStyle).
61
+ */
62
+ listStyle?: UiProps;
63
+
64
+ /**
65
+ * Class CSS kustom untuk opsi list dropdown (atau alias menuClass).
66
+ */
67
+ listClass?: string;
68
+
69
+ /**
70
+ * Style kustom untuk elemen pesan error validasi (UiProps).
71
+ */
72
+ errorMessageStyle?: UiProps;
73
+
74
+ /**
75
+ * Class CSS kustom untuk elemen pesan error validasi.
76
+ */
77
+ errorMessageClass?: string;
78
+
79
+ /**
80
+ * Style kustom untuk elemen helper text (UiProps).
81
+ */
82
+ helperTextStyle?: UiProps;
83
+
84
+ /**
85
+ * Class CSS kustom untuk elemen helper text.
86
+ */
87
+ helperTextClass?: string;
88
+
89
+ /**
90
+ * Menandai field sebagai opsi opsional dengan menambahkan teks '(Opsional)' (12px Regular, #9698A2).
91
+ * @default false
92
+ */
93
+ optional?: boolean;
94
+
95
+ /**
96
+ * Kustomisasi teks keterangan opsional.
97
+ * @default '(Opsional)'
98
+ */
99
+ optionalText?: string;
100
+
101
+ /**
102
+ * Teks petunjuk saat belum ada opsi yang dipilih.
103
+ * @default 'Pilih dropdown menu'
104
+ */
105
+ placeholder?: string;
106
+
107
+ /**
108
+ * Nilai terpilih saat ini (single string/number atau array nilai untuk mode multiple).
109
+ */
110
+ value?: any;
111
+
112
+ /**
113
+ * Nilai awal tidak terkontrol (uncontrolled initial value).
114
+ */
115
+ defaultValue?: any;
116
+
117
+ /**
118
+ * Daftar pilihan opsi dropdown. Dapat berupa array of object `{ label, value }` atau array of string.
119
+ */
120
+ options?: Array<SelectboxOption | string>;
121
+
122
+ /**
123
+ * Mode pemilihan jamak (multi-select) dengan tampilan chips/tag dan daftar checkbox.
124
+ * @default false
125
+ */
126
+ multiple?: boolean;
127
+
128
+ /**
129
+ * Mengaktifkan kotak pencarian (search input) di dalam menu dropdown.
130
+ * @default false
131
+ */
132
+ searchable?: boolean;
133
+
134
+ /**
135
+ * Placeholder pada kotak pencarian di dalam dropdown.
136
+ * @default 'Cari'
137
+ */
138
+ searchPlaceholder?: string;
139
+
140
+ /**
141
+ * Teks tombol terapkan pada footer multi-select.
142
+ * @default 'Terapkan'
143
+ */
144
+ applyText?: string;
145
+
146
+ /**
147
+ * Teks tombol reset pada footer multi-select.
148
+ * @default 'Reset'
149
+ */
150
+ resetText?: string;
151
+
152
+ /**
153
+ * Nama form field HTML.
154
+ */
155
+ name?: string;
156
+
157
+ /**
158
+ * Identifier unik elemen selectbox.
159
+ */
160
+ id?: string;
161
+
162
+ /**
163
+ * Status apakah selectbox sedang dalam kondisi error / tidak valid.
164
+ * @default false
165
+ */
166
+ error?: boolean;
167
+
168
+ /**
169
+ * Pesan teks penjelasan kesalahan validasi di bawah field.
170
+ */
171
+ errorMessage?: string;
172
+
173
+ /**
174
+ * Teks petunjuk atau keterangan bantuan di bawah field (12px Regular, #3C4245).
175
+ */
176
+ helperText?: string;
177
+
178
+ /**
179
+ * Posisi teks helper
180
+ */
181
+ helperTextPosition?: "top" | "bottom";
182
+
183
+ /**
184
+ * Skala ukuran selectbox.
185
+ * - `'sm'`: 36px
186
+ * - `'md'`: 48px (standar desain)
187
+ * - `'lg'`: 56px
188
+ * @default 'md'
189
+ */
190
+ size?: "sm" | "md" | "lg";
191
+
192
+ /**
193
+ * Bentuk sudut kelengkungan selectbox.
194
+ * - `'none'`: 0px
195
+ * - `'sm'`: 4px
196
+ * - `'md'`: 8px (standar desain)
197
+ * - `'lg'`: 12px
198
+ * - `'full'`: 9999px (kapsul)
199
+ * @default 'md'
200
+ */
201
+ rounded?: "none" | "sm" | "md" | "lg" | "full";
202
+
203
+ /**
204
+ * Menjadikan lebar field selectbox memenuhi kontainer penuh (full-width 100%).
205
+ * @default false
206
+ */
207
+ block?: boolean;
208
+
209
+ /**
210
+ * Status apakah selectbox dinonaktifkan (disabled).
211
+ * @default false
212
+ */
213
+ disabled?: boolean;
214
+
215
+ /**
216
+ * Menandai field sebagai input wajib diisi (menampilkan tanda bintang merah *).
217
+ * @default false
218
+ */
219
+ required?: boolean;
220
+
221
+ /**
222
+ * Ref instance elemen control selectbox untuk akses DOM langsung.
223
+ */
224
+ inputRef?: any;
225
+
226
+ /**
227
+ * Menampilkan tombol × untuk menghapus pilihan yang sudah dipilih.
228
+ * @default false
229
+ */
230
+ clearable?: boolean;
231
+
232
+ /**
233
+ * Menampilkan spinner loading dan menonaktifkan interaksi selectbox.
234
+ * @default false
235
+ */
236
+ loading?: boolean;
237
+
238
+ /**
239
+ * Menjadikan selectbox hanya-baca (tidak bisa dibuka/diubah).
240
+ * @default false
241
+ */
242
+ readOnly?: boolean;
243
+
244
+ /**
245
+ * Inline CSS style object native.
246
+ */
247
+ style?: React.CSSProperties;
248
+
249
+ /**
250
+ * Kustomisasi visual granular root / form-item (UiProps).
251
+ */
252
+ ui?: UiProps;
253
+
254
+ /**
255
+ * Callback event saat nilai pilihan berubah.
256
+ */
257
+ onChange?: (value: any) => void;
258
+
259
+ /**
260
+ * Callback event saat pengguna mengetik di kolom pencarian.
261
+ */
262
+ onSearch?: (query: string) => void;
263
+
264
+ /**
265
+ * Callback event saat menu dropdown terbuka.
266
+ */
267
+ onOpen?: () => void;
268
+
269
+ /**
270
+ * Class CSS kustom.
271
+ */
272
+ className?: string;
273
+
274
+ /**
275
+ * Menonaktifkan middleware `shift` pada floating-ui sehingga posisi list popover
276
+ * tidak bergeser secara horizontal mengikuti tepi layar.
277
+ * Berguna saat ingin list tetap berada di tengah atau posisi placement yang ditentukan.
278
+ * @default false
279
+ */
280
+ disableShift?: boolean;
281
+
282
+ /**
283
+ * Callback event saat menu dropdown ditutup.
284
+ */
285
+ onClose?: () => void;
286
+ }
287
+
288
+ /**
289
+ * Komponen Selectbox TTDK sesuai spesifikasi Desain:
290
+ * - Single Select dengan Checkmark indicator dan Active Teal border (#00A3A6)
291
+ * - Multi-Select dengan Checkbox list, Chip tags (Label ✕), dan Action footer (Reset & Terapkan)
292
+ * - Searchable filter bar dengan input Cari 🔍
293
+ * - Visual States: Default, Active/Open, Filled, Error (#FF3A39), Disabled (#E4E5E7)
294
+ */
295
+
296
+ import {
297
+ computePosition,
298
+ autoUpdate,
299
+ flip,
300
+ shift,
301
+ offset,
302
+ size as floatingSize,
303
+ } from "@floating-ui/dom";
304
+ import { buildUiVars, UiProps } from "../../core/types";
305
+
306
+ function Selectbox(props: SelectboxProps) {
307
+ const rootRef = useRef<HTMLDivElement>(null);
308
+ const controlRef = useRef<HTMLDivElement>(null);
309
+ const dropdownRef = useRef<HTMLDivElement>(null);
310
+ const [isOpen, setIsOpen] = useState(() => false);
311
+
312
+ const [searchQuery, setSearchQuery] = useState(() => "");
313
+
314
+ const [currentValue, setCurrentValue] = useState(() =>
315
+ props.value !== undefined
316
+ ? props.value
317
+ : props.defaultValue !== undefined
318
+ ? props.defaultValue
319
+ : props.multiple
320
+ ? []
321
+ : ""
322
+ );
323
+
324
+ const [tempMultiValue, setTempMultiValue] = useState<any[]>(() => []);
325
+
326
+ const cleanupAutoUpdateRef = useRef<any>(null);
327
+
328
+ function updatePosition() {
329
+ if (!controlRef.current || !dropdownRef.current) return;
330
+ computePosition(controlRef.current, dropdownRef.current, {
331
+ placement: "bottom-start",
332
+ strategy: "absolute",
333
+ middleware: [
334
+ offset(4),
335
+ flip({
336
+ padding: {
337
+ top: 80,
338
+ bottom: 16,
339
+ left: 16,
340
+ right: 16,
341
+ },
342
+ }),
343
+ ...(props.disableShift
344
+ ? []
345
+ : [
346
+ shift({
347
+ padding: {
348
+ top: 80,
349
+ bottom: 16,
350
+ left: 16,
351
+ right: 16,
352
+ },
353
+ }),
354
+ ]),
355
+ floatingSize({
356
+ apply({ rects, elements, availableHeight }) {
357
+ const customWidth =
358
+ props.menuStyle?.width !== undefined &&
359
+ props.menuStyle?.width !== null &&
360
+ props.menuStyle?.width !== ""
361
+ ? typeof props.menuStyle.width === "number" ||
362
+ /^\d+(\.\d+)?$/.test(String(props.menuStyle.width).trim())
363
+ ? `${String(props.menuStyle.width).trim()}px`
364
+ : String(props.menuStyle.width)
365
+ : props.listStyle?.width !== undefined &&
366
+ props.listStyle?.width !== null &&
367
+ props.listStyle?.width !== ""
368
+ ? typeof props.listStyle.width === "number" ||
369
+ /^\d+(\.\d+)?$/.test(String(props.listStyle.width).trim())
370
+ ? `${String(props.listStyle.width).trim()}px`
371
+ : String(props.listStyle.width)
372
+ : props.ui?.dropdownWidth !== undefined &&
373
+ props.ui?.dropdownWidth !== null &&
374
+ props.ui?.dropdownWidth !== ""
375
+ ? typeof props.ui.dropdownWidth === "number" ||
376
+ /^\d+(\.\d+)?$/.test(String(props.ui.dropdownWidth).trim())
377
+ ? `${String(props.ui.dropdownWidth).trim()}px`
378
+ : String(props.ui.dropdownWidth)
379
+ : undefined;
380
+ Object.assign(elements.floating.style, {
381
+ width: customWidth || `${rects.reference.width}px`,
382
+ minWidth: customWidth ? customWidth : undefined,
383
+ maxHeight: `${Math.min(availableHeight, 360)}px`,
384
+ });
385
+ },
386
+ padding: {
387
+ top: 80,
388
+ bottom: 16,
389
+ left: 16,
390
+ right: 16,
391
+ },
392
+ }),
393
+ ],
394
+ }).then(({ x, y, placement }: any) => {
395
+ if (!dropdownRef.current) return;
396
+ dropdownRef.current.style.left = `${x}px`;
397
+ dropdownRef.current.style.top = `${y}px`;
398
+ dropdownRef.current.setAttribute("data-placement", placement);
399
+ });
400
+ }
401
+
402
+ function startFloating() {
403
+ stopFloating();
404
+ if (controlRef.current && dropdownRef.current) {
405
+ cleanupAutoUpdateRef.current = autoUpdate(
406
+ controlRef.current,
407
+ dropdownRef.current,
408
+ () => {
409
+ updatePosition();
410
+ }
411
+ );
412
+ }
413
+ }
414
+
415
+ function stopFloating() {
416
+ if (typeof cleanupAutoUpdateRef.current === "function") {
417
+ cleanupAutoUpdateRef.current();
418
+ cleanupAutoUpdateRef.current = null;
419
+ }
420
+ }
421
+
422
+ function handleClickOutside(event: any) {
423
+ if (!dropdownRef.current) return;
424
+ const target = event.target;
425
+ if (
426
+ rootRef.current &&
427
+ !rootRef.current.contains(target) &&
428
+ dropdownRef.current &&
429
+ !dropdownRef.current.contains(target)
430
+ ) {
431
+ closeDropdown();
432
+ }
433
+ }
434
+
435
+ function onPointerDown(event: any) {
436
+ handleClickOutside(event);
437
+ }
438
+
439
+ function toggleDropdown() {
440
+ if (props.disabled || props.readOnly || props.loading) return;
441
+ const nextOpen = !isOpen;
442
+ setIsOpen(nextOpen);
443
+ if (nextOpen) {
444
+ if (props.multiple) {
445
+ setTempMultiValue(Array.isArray(currentValue) ? [...currentValue] : []);
446
+ }
447
+ if (props.onOpen) props.onOpen();
448
+ } else {
449
+ setSearchQuery("");
450
+ if (props.onClose) props.onClose();
451
+ }
452
+ }
453
+
454
+ function closeDropdown() {
455
+ stopFloating();
456
+ setIsOpen(false);
457
+ setSearchQuery("");
458
+ if (props.onClose) props.onClose();
459
+ }
460
+
461
+ function normalizedOptions() {
462
+ if (!props.options || !Array.isArray(props.options)) return [];
463
+ return props.options.map((opt: any) => {
464
+ if (typeof opt === "object" && opt !== null) {
465
+ return {
466
+ label: String(opt.label ?? opt.value ?? ""),
467
+ value: opt.value ?? opt.label ?? "",
468
+ disabled: Boolean(opt.disabled),
469
+ };
470
+ }
471
+ return {
472
+ label: String(opt),
473
+ value: opt,
474
+ disabled: false,
475
+ };
476
+ });
477
+ }
478
+
479
+ function filteredOptions() {
480
+ const opts = normalizedOptions();
481
+ if (!searchQuery || !searchQuery.trim()) return opts;
482
+ const q = searchQuery.toLowerCase().trim();
483
+ return opts.filter((opt: any) => opt.label.toLowerCase().includes(q));
484
+ }
485
+
486
+ function isOptionSelected(optValue: any) {
487
+ if (props.multiple) {
488
+ const list = isOpen
489
+ ? tempMultiValue
490
+ : Array.isArray(currentValue)
491
+ ? currentValue
492
+ : [];
493
+ return list.some((v: any) => String(v) === String(optValue));
494
+ }
495
+ return String(currentValue) === String(optValue);
496
+ }
497
+
498
+ function handleSingleSelect(opt: any) {
499
+ if (opt.disabled || props.disabled) return;
500
+ setCurrentValue(opt.value);
501
+ if (props.onChange) {
502
+ props.onChange(opt.value);
503
+ }
504
+ closeDropdown();
505
+ }
506
+
507
+ function handleMultiToggle(opt: any) {
508
+ if (opt.disabled || props.disabled) return;
509
+ const index = tempMultiValue.findIndex(
510
+ (v: any) => String(v) === String(opt.value)
511
+ );
512
+ if (index > -1) {
513
+ const next = [...tempMultiValue];
514
+ next.splice(index, 1);
515
+ setTempMultiValue(next);
516
+ } else {
517
+ setTempMultiValue([...tempMultiValue, opt.value]);
518
+ }
519
+ }
520
+
521
+ function handleRemoveTag(val: any, event: any) {
522
+ if (event && event.stopPropagation) event.stopPropagation();
523
+ if (props.disabled) return;
524
+ if (Array.isArray(currentValue)) {
525
+ const next = currentValue.filter((v: any) => String(v) !== String(val));
526
+ setCurrentValue(next);
527
+ setTempMultiValue([...next]);
528
+ if (props.onChange) {
529
+ props.onChange(next);
530
+ }
531
+ }
532
+ }
533
+
534
+ function handleApply() {
535
+ setCurrentValue([...tempMultiValue]);
536
+ if (props.onChange) {
537
+ props.onChange([...tempMultiValue]);
538
+ }
539
+ closeDropdown();
540
+ }
541
+
542
+ function handleReset() {
543
+ setTempMultiValue([]);
544
+ setCurrentValue([]);
545
+ if (props.onChange) {
546
+ props.onChange([]);
547
+ }
548
+ }
549
+
550
+ function handleSearchInput(e: any) {
551
+ setSearchQuery(e.target.value);
552
+ if (props.onSearch) {
553
+ props.onSearch(e.target.value);
554
+ }
555
+ }
556
+
557
+ function getSingleSelectedLabel() {
558
+ const opts = normalizedOptions();
559
+ const match = opts.find(
560
+ (opt: any) => String(opt.value) === String(currentValue)
561
+ );
562
+ return match ? match.label : "";
563
+ }
564
+
565
+ function getMultiSelectedChips() {
566
+ const list = Array.isArray(currentValue) ? currentValue : [];
567
+ const opts = normalizedOptions();
568
+ return list.map((val: any) => {
569
+ const match = opts.find((o: any) => String(o.value) === String(val));
570
+ return {
571
+ value: val,
572
+ label: match ? match.label : String(val),
573
+ };
574
+ });
575
+ }
576
+
577
+ function hasValue() {
578
+ if (props.multiple) {
579
+ return Array.isArray(currentValue) && currentValue.length > 0;
580
+ }
581
+ return (
582
+ currentValue !== undefined && currentValue !== "" && currentValue !== null
583
+ );
584
+ }
585
+
586
+ function handleClear(event: any) {
587
+ if (event && event.stopPropagation) event.stopPropagation();
588
+ if (props.disabled || props.readOnly) return;
589
+ const emptyVal = props.multiple ? [] : "";
590
+ setCurrentValue(emptyVal);
591
+ setTempMultiValue([]);
592
+ if (props.onChange) {
593
+ props.onChange(emptyVal);
594
+ }
595
+ }
596
+
597
+ useEffect(() => {
598
+ if (props.value !== undefined) {
599
+ setCurrentValue(props.value);
600
+ }
601
+ }, [props.value]);
602
+
603
+ useEffect(() => {
604
+ if (isOpen) {
605
+ startFloating();
606
+
607
+ const handlePointerDown = (event: any) => {
608
+ const target = event.target;
609
+ if (
610
+ rootRef.current &&
611
+ !rootRef.current.contains(target) &&
612
+ dropdownRef.current &&
613
+ !dropdownRef.current.contains(target)
614
+ ) {
615
+ closeDropdown();
616
+ }
617
+ };
618
+
619
+ document.addEventListener("pointerdown", handlePointerDown, true);
620
+
621
+ return () => {
622
+ stopFloating();
623
+ document.removeEventListener("pointerdown", handlePointerDown, true);
624
+ };
625
+ } else {
626
+ stopFloating();
627
+ }
628
+ }, [isOpen]);
629
+
630
+ return (
631
+ <div
632
+ ref={rootRef}
633
+ className={`ttdk-form-item ${props.className || ""}`.trim()}
634
+ data-disabled={props.disabled}
635
+ data-error={Boolean(props.error || props.errorMessage)}
636
+ data-size={props.size || "md"}
637
+ data-block={props.block ? "true" : undefined}
638
+ style={{
639
+ ...buildUiVars(props.ui),
640
+ ...((props.ui as any) || {}),
641
+ ...((props as any)["style"] || {}),
642
+ ...props.style,
643
+ }}
644
+ >
645
+ {props.label ? (
646
+ <label
647
+ className={`ttdk-form-label ${props.labelClass || ""}`.trim()}
648
+ htmlFor={props.id}
649
+ style={{
650
+ ...buildUiVars(props.labelStyle),
651
+ }}
652
+ >
653
+ <span>{props.label}</span>
654
+ {props.required ? (
655
+ <span className="ttdk-form-required" aria-hidden="true">
656
+ *
657
+ </span>
658
+ ) : null}
659
+ {props.optional ? (
660
+ <span className="ttdk-form-optional">
661
+ {props.optionalText || "(Opsional)"}
662
+ </span>
663
+ ) : null}
664
+ </label>
665
+ ) : null}
666
+ {props.helperText && props.helperTextPosition === "top" ? (
667
+ <div
668
+ className={`ttdk-form-helper ${props.helperTextClass || ""}`.trim()}
669
+ style={{
670
+ ...buildUiVars(props.helperTextStyle),
671
+ }}
672
+ >
673
+ {props.helperText}
674
+ </div>
675
+ ) : null}
676
+ <div
677
+ role="combobox"
678
+ aria-haspopup="listbox"
679
+ ref={controlRef}
680
+ className={`ttdk-selectbox-control ${props.controlClass || ""}`.trim()}
681
+ id={props.id}
682
+ tabIndex={props.disabled ? -1 : 0}
683
+ aria-expanded={isOpen}
684
+ aria-disabled={props.disabled}
685
+ aria-invalid={Boolean(props.error || props.errorMessage)}
686
+ data-open={isOpen ? "true" : "false"}
687
+ data-filled={hasValue() ? "true" : "false"}
688
+ data-error={Boolean(props.error || props.errorMessage)}
689
+ data-disabled={Boolean(props.disabled)}
690
+ data-size={props.size || "md"}
691
+ data-rounded={props.rounded || "md"}
692
+ style={{
693
+ ...buildUiVars(props.controlStyle),
694
+ }}
695
+ onClick={(event) =>
696
+ props.disabled || props.readOnly || props.loading
697
+ ? undefined
698
+ : toggleDropdown()
699
+ }
700
+ onKeyDown={(e) => {
701
+ if (e.key === "Enter" || e.key === " ") {
702
+ e.preventDefault();
703
+ toggleDropdown();
704
+ } else if (e.key === "Escape") {
705
+ closeDropdown();
706
+ }
707
+ }}
708
+ >
709
+ <div className="ttdk-selectbox-display">
710
+ {props.multiple && hasValue() ? (
711
+ <div className="ttdk-selectbox-chips">
712
+ {getMultiSelectedChips()?.map((chip) => (
713
+ <span className="ttdk-select-chip" key={chip.value}>
714
+ <span className="ttdk-select-chip-text">{chip.label}</span>
715
+ <button
716
+ type="button"
717
+ className="ttdk-select-chip-remove"
718
+ aria-label={`Hapus ${chip.label}`}
719
+ onClick={(e) => handleRemoveTag(chip.value, e)}
720
+ >
721
+ <svg
722
+ className="ttdk-select-chip-remove-icon"
723
+ viewBox="0 0 24 24"
724
+ fill="none"
725
+ >
726
+ <line x1="18" y1="6" x2="6" y2="18" />
727
+ <line x1="6" y1="6" x2="18" y2="18" />
728
+ </svg>
729
+ </button>
730
+ </span>
731
+ ))}
732
+ </div>
733
+ ) : null}
734
+ {!props.multiple && hasValue() ? (
735
+ <span className="ttdk-selectbox-value">
736
+ {getSingleSelectedLabel()}
737
+ </span>
738
+ ) : null}
739
+ {!hasValue() ? (
740
+ <span className="ttdk-selectbox-placeholder">
741
+ {props.placeholder || "Pilih dropdown menu"}
742
+ </span>
743
+ ) : null}
744
+ </div>
745
+ {props.clearable &&
746
+ hasValue() &&
747
+ !props.disabled &&
748
+ !props.readOnly &&
749
+ !props.loading ? (
750
+ <button
751
+ type="button"
752
+ className="ttdk-selectbox-clear"
753
+ aria-label="Hapus pilihan"
754
+ onClick={(e) => handleClear(e)}
755
+ >
756
+ <svg
757
+ className="ttdk-selectbox-clear-icon"
758
+ viewBox="0 0 24 24"
759
+ fill="none"
760
+ >
761
+ <line x1="18" y1="6" x2="6" y2="18" />
762
+ <line x1="6" y1="6" x2="18" y2="18" />
763
+ </svg>
764
+ </button>
765
+ ) : null}
766
+ {props.loading ? (
767
+ <span className="ttdk-selectbox-spinner" aria-hidden="true">
768
+ <svg
769
+ className="ttdk-selectbox-spinner-icon"
770
+ viewBox="0 0 24 24"
771
+ fill="none"
772
+ >
773
+ <line
774
+ x1="12"
775
+ y1="2.5"
776
+ x2="12"
777
+ y2="6.5"
778
+ opacity="0.175"
779
+ transform="rotate(0 12 12)"
780
+ />
781
+ <line
782
+ x1="12"
783
+ y1="2.5"
784
+ x2="12"
785
+ y2="6.5"
786
+ opacity="0.25"
787
+ transform="rotate(30 12 12)"
788
+ />
789
+ <line
790
+ x1="12"
791
+ y1="2.5"
792
+ x2="12"
793
+ y2="6.5"
794
+ opacity="0.325"
795
+ transform="rotate(60 12 12)"
796
+ />
797
+ <line
798
+ x1="12"
799
+ y1="2.5"
800
+ x2="12"
801
+ y2="6.5"
802
+ opacity="0.4"
803
+ transform="rotate(90 12 12)"
804
+ />
805
+ <line
806
+ x1="12"
807
+ y1="2.5"
808
+ x2="12"
809
+ y2="6.5"
810
+ opacity="0.475"
811
+ transform="rotate(120 12 12)"
812
+ />
813
+ <line
814
+ x1="12"
815
+ y1="2.5"
816
+ x2="12"
817
+ y2="6.5"
818
+ opacity="0.55"
819
+ transform="rotate(150 12 12)"
820
+ />
821
+ <line
822
+ x1="12"
823
+ y1="2.5"
824
+ x2="12"
825
+ y2="6.5"
826
+ opacity="0.625"
827
+ transform="rotate(180 12 12)"
828
+ />
829
+ <line
830
+ x1="12"
831
+ y1="2.5"
832
+ x2="12"
833
+ y2="6.5"
834
+ opacity="0.7"
835
+ transform="rotate(210 12 12)"
836
+ />
837
+ <line
838
+ x1="12"
839
+ y1="2.5"
840
+ x2="12"
841
+ y2="6.5"
842
+ opacity="0.775"
843
+ transform="rotate(240 12 12)"
844
+ />
845
+ <line
846
+ x1="12"
847
+ y1="2.5"
848
+ x2="12"
849
+ y2="6.5"
850
+ opacity="0.85"
851
+ transform="rotate(270 12 12)"
852
+ />
853
+ <line
854
+ x1="12"
855
+ y1="2.5"
856
+ x2="12"
857
+ y2="6.5"
858
+ opacity="0.925"
859
+ transform="rotate(300 12 12)"
860
+ />
861
+ <line
862
+ x1="12"
863
+ y1="2.5"
864
+ x2="12"
865
+ y2="6.5"
866
+ opacity="1"
867
+ transform="rotate(330 12 12)"
868
+ />
869
+ </svg>
870
+ </span>
871
+ ) : null}
872
+ {!props.loading ? (
873
+ <span
874
+ className="ttdk-selectbox-arrow"
875
+ data-open={isOpen ? "true" : "false"}
876
+ >
877
+ <svg
878
+ className="ttdk-selectbox-arrow-icon"
879
+ viewBox="0 0 24 24"
880
+ fill="none"
881
+ >
882
+ <polyline points="6 9 12 15 18 9" />
883
+ </svg>
884
+ </span>
885
+ ) : null}
886
+ </div>
887
+ {isOpen && !props.disabled ? (
888
+ <div
889
+ role="listbox"
890
+ ref={dropdownRef}
891
+ className={`ttdk-selectbox-dropdown ${props.menuClass || ""}`.trim()}
892
+ style={{
893
+ ...buildUiVars(props.menuStyle || props.listStyle),
894
+ }}
895
+ >
896
+ {props.searchable ? (
897
+ <div
898
+ className="ttdk-selectbox-search"
899
+ onClick={(e) => e.stopPropagation()}
900
+ >
901
+ <input
902
+ type="text"
903
+ className="ttdk-selectbox-search-input"
904
+ placeholder={props.searchPlaceholder || "Cari"}
905
+ value={searchQuery}
906
+ onInput={(e) => handleSearchInput(e)}
907
+ autoFocus
908
+ />
909
+ <span className="ttdk-selectbox-search-icon" aria-hidden="true">
910
+ <svg
911
+ className="ttdk-selectbox-search-svg"
912
+ viewBox="0 0 24 24"
913
+ fill="none"
914
+ >
915
+ <circle cx="11" cy="11" r="8" />
916
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
917
+ </svg>
918
+ </span>
919
+ </div>
920
+ ) : null}
921
+ <div className="ttdk-selectbox-options-list">
922
+ {filteredOptions()?.map((opt) => (
923
+ <div
924
+ role="option"
925
+ key={opt.value}
926
+ className={`ttdk-selectbox-option ${
927
+ props.listClass || ""
928
+ }`.trim()}
929
+ aria-selected={isOptionSelected(opt.value)}
930
+ aria-disabled={opt.disabled}
931
+ data-selected={isOptionSelected(opt.value) ? "true" : "false"}
932
+ data-disabled={opt.disabled ? "true" : "false"}
933
+ onClick={(e) => {
934
+ e.stopPropagation();
935
+ if (props.multiple) {
936
+ handleMultiToggle(opt);
937
+ } else {
938
+ handleSingleSelect(opt);
939
+ }
940
+ }}
941
+ >
942
+ {props.multiple ? (
943
+ <span
944
+ className="ttdk-selectbox-checkbox"
945
+ data-checked={
946
+ isOptionSelected(opt.value) ? "true" : "false"
947
+ }
948
+ >
949
+ {isOptionSelected(opt.value) ? (
950
+ <svg
951
+ className="ttdk-selectbox-checkbox-icon"
952
+ viewBox="0 0 24 24"
953
+ fill="none"
954
+ >
955
+ <polyline points="20 6 9 17 4 12" />
956
+ </svg>
957
+ ) : null}
958
+ </span>
959
+ ) : null}
960
+ <span className="ttdk-selectbox-option-label">{opt.label}</span>
961
+ {!props.multiple && isOptionSelected(opt.value) ? (
962
+ <span
963
+ className="ttdk-selectbox-check-icon"
964
+ aria-hidden="true"
965
+ >
966
+ <svg
967
+ className="ttdk-selectbox-check-svg"
968
+ viewBox="0 0 24 24"
969
+ fill="none"
970
+ >
971
+ <polyline points="20 6 9 17 4 12" />
972
+ </svg>
973
+ </span>
974
+ ) : null}
975
+ </div>
976
+ ))}
977
+ {filteredOptions().length === 0 ? (
978
+ <div className="ttdk-selectbox-empty">
979
+ Tidak ada pilihan ditemukan
980
+ </div>
981
+ ) : null}
982
+ </div>
983
+ {props.multiple ? (
984
+ <div
985
+ className="ttdk-selectbox-actions"
986
+ onClick={(e) => e.stopPropagation()}
987
+ >
988
+ <button
989
+ type="button"
990
+ className="ttdk-selectbox-btn-reset"
991
+ onClick={(event) => handleReset()}
992
+ >
993
+ <svg
994
+ className="ttdk-selectbox-btn-reset-icon"
995
+ viewBox="0 0 24 24"
996
+ fill="none"
997
+ >
998
+ <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
999
+ <path d="M3 3v5h5" />
1000
+ </svg>
1001
+ <span>{props.resetText || "Reset"}</span>
1002
+ </button>
1003
+ <button
1004
+ type="button"
1005
+ className="ttdk-selectbox-btn-apply"
1006
+ onClick={(event) => handleApply()}
1007
+ >
1008
+ <span>{props.applyText || "Terapkan"}</span>
1009
+ <svg
1010
+ className="ttdk-selectbox-btn-apply-icon"
1011
+ viewBox="0 0 24 24"
1012
+ fill="none"
1013
+ >
1014
+ <polyline points="20 6 9 17 4 12" />
1015
+ </svg>
1016
+ </button>
1017
+ </div>
1018
+ ) : null}
1019
+ </div>
1020
+ ) : null}
1021
+ {props.helperText && props.helperTextPosition !== "top" ? (
1022
+ <div
1023
+ className={`ttdk-form-helper ${props.helperTextClass || ""}`.trim()}
1024
+ style={{
1025
+ ...buildUiVars(props.helperTextStyle),
1026
+ }}
1027
+ >
1028
+ {props.helperText}
1029
+ </div>
1030
+ ) : null}
1031
+ {props.errorMessage ? (
1032
+ <div
1033
+ role="alert"
1034
+ className={`ttdk-form-error ${props.errorMessageClass || ""}`.trim()}
1035
+ style={{
1036
+ ...buildUiVars(props.errorMessageStyle),
1037
+ }}
1038
+ >
1039
+ {props.errorMessage}
1040
+ </div>
1041
+ ) : null}
1042
+ </div>
1043
+ );
1044
+ }
1045
+
1046
+ export default Selectbox;
1047
+
1048
+ export { Selectbox };